diff --git a/.gitignore b/.gitignore index efd048cc..81a70abe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,57 +1,57 @@ -__pycache__/ -*.pyc -.idea/ -【模板】模型及算法与功能对应清单.docx -【模板】GY知识-产品模块架构及功能清单和主要应用场景说明.docx -1.json -2.json -3.json -4.json -6.json -功能清单.md -模型及算法与功能对应清单(完整版).md -维修系统使用说明.md -audio_demo_config.json -create_test_audio.py -demo_universal_dialog.py -Dock按钮样式统一说明.md -Dock面板标题样式线完成说明.md -exam_result_20250926_004454.json -exam_result_20250926_010628.json -exam_result_20250926_011746.json -exam_result_20250926_090330.json -exam_result_20250926_111756.json -exam_test_config.json -Figma布局优化说明.md -Figma设计分析与菜单栏优化文档.md -Figma样式优化完成说明.md -manual_tools_config.json -pm.txt -simple_menubar_test.py -test_audio.py -test_basic_audio.py -test_exam_tool_error.py -test_full_audio_playback.py -test_menu_visibility.py -test_mode_selection.py -test_mp3_audio.py -test_pygame_audio.py -test_score_config.py -test_simple_audio.py -tool_test_config.json -UI设计文档.md -UniversalMessageDialog使用说明.md -.codex/settings/kiroCodex-settings.json -data/projects.json -RenderPipelineFile/config/daytime.yaml -Resources/audio/README.md -Resources/model/a.glb -Resources/model/b.glb -Resources/model/c.glb -Resources/model/JQB_auto_converted.glb -Resources/model/Untitled.glb -Resources/models/Women_1.glb -Subjects/test_maintenance_config.json -Resources/models/Women_1.glb -Resources/models/Women_1.glb -Resources/models/Women_1.glb +__pycache__/ +*.pyc +.idea/ +【模板】模型及算法与功能对应清单.docx +【模板】GY知识-产品模块架构及功能清单和主要应用场景说明.docx +1.json +2.json +3.json +4.json +6.json +功能清单.md +模型及算法与功能对应清单(完整版).md +维修系统使用说明.md +audio_demo_config.json +create_test_audio.py +demo_universal_dialog.py +Dock按钮样式统一说明.md +Dock面板标题样式线完成说明.md +exam_result_20250926_004454.json +exam_result_20250926_010628.json +exam_result_20250926_011746.json +exam_result_20250926_090330.json +exam_result_20250926_111756.json +exam_test_config.json +Figma布局优化说明.md +Figma设计分析与菜单栏优化文档.md +Figma样式优化完成说明.md +manual_tools_config.json +pm.txt +simple_menubar_test.py +test_audio.py +test_basic_audio.py +test_exam_tool_error.py +test_full_audio_playback.py +test_menu_visibility.py +test_mode_selection.py +test_mp3_audio.py +test_pygame_audio.py +test_score_config.py +test_simple_audio.py +tool_test_config.json +UI设计文档.md +UniversalMessageDialog使用说明.md +.codex/settings/kiroCodex-settings.json +data/projects.json +RenderPipelineFile/config/daytime.yaml +Resources/audio/README.md +Resources/model/a.glb +Resources/model/b.glb +Resources/model/c.glb +Resources/model/JQB_auto_converted.glb +Resources/model/Untitled.glb +Resources/models/Women_1.glb +Subjects/test_maintenance_config.json +Resources/models/Women_1.glb +Resources/models/Women_1.glb +Resources/models/Women_1.glb diff --git a/.idea/EG.iml b/.idea/EG.iml index 04720f01..838b16dc 100644 --- a/.idea/EG.iml +++ b/.idea/EG.iml @@ -1,15 +1,15 @@ - - - - - - - - - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 51b9fc17..505c56b5 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,7 @@ - - - - - + + + + + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 1918c832..7970ce7b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ -{ - "python-envs.pythonProjects": [], - "kiroAgent.configureMCP": "Disabled" +{ + "python-envs.pythonProjects": [], + "kiroAgent.configureMCP": "Disabled" } \ No newline at end of file diff --git a/IFLOW.md b/IFLOW.md index 8716200d..fd4e1fe4 100644 --- a/IFLOW.md +++ b/IFLOW.md @@ -1,179 +1,179 @@ -# iFlow 上下文文档 - -## 项目概述 - -这是一个基于 Panda3D 和 PyQt5 的 3D 虚拟现实 (VR) 应用程序框架。该项目旨在提供一个功能齐全、模块化的 3D 环境,支持 VR 设备(如 HTC Vive, Oculus Rift)的集成,包含场景管理、模型导入、GUI 系统、脚本系统、地形系统、碰撞检测以及完整的 VR 交互功能。 - -核心架构围绕 `MyWorld` 类构建,该类继承自 `CoreWorld`,并集成了各种管理器模块,如选择系统、工具管理器、脚本管理器、GUI 管理器、场景管理器、项目管理器、地形管理器、碰撞管理器和 VR 管理器。 - -## 核心技术栈 - -- **核心引擎**: Panda3D -- **图形渲染**: 可选择普通渲染或 RenderPipeline 高级渲染管线 -- **VR 支持**: OpenVR/SteamVR -- **用户界面**: PyQt5 -- **3D 模型格式**: 支持 glTF, FBX (需转换), BAM 等 -- **脚本语言**: Python (内嵌脚本系统) -- **物理/碰撞**: Panda3D 内置碰撞系统 - -## 项目结构 - -``` -. -├── core/ # 核心模块 -│ ├── world.py # CoreWorld 基础世界类 -│ ├── vr/ # VR 子模块 (完整模块化结构) -│ │ ├── __init__.py -│ │ ├── config/ # VR 配置管理 -│ │ ├── interaction/ # VR 交互系统 (动作、抓取、摇杆、传送) -│ │ ├── performance/ # VR 性能监控 -│ │ ├── rendering/ # VR 渲染相关 (RenderPipeline 集成) -│ │ ├── testing/ # VR 测试模式 -│ │ ├── tracking/ # VR 设备跟踪 -│ │ └── visualization/ # VR 可视化 (控制器模型) -│ ├── vr_manager.py # VR 管理器主文件 (待拆分) -│ ├── selection.py # 选择系统 -│ ├── tool_manager.py # 工具管理器 -│ ├── script_system.py # 脚本系统 -│ ├── gui_manager.py # GUI 管理器 -│ ├── terrain_manager.py # 地形管理器 -│ ├── collision_manager.py # 碰撞管理器 -│ ├── event_handler.py # 事件处理器 -│ ├── patrol_system.py # 巡检系统 -│ ├── Command_System.py # 命令系统 -│ └── InfoPanelManager.py # 信息面板管理器 -├── demo/ # 示例和测试文件 -├── gui/ # GUI 相关模块 -├── project/ # 项目管理模块 -├── scene/ # 场景管理模块 (部分代码在 core/) -├── scripts/ # 脚本文件目录 -├── ui/ # UI 组件和主窗口 -├── QPanda3D/ # Panda3D 与 PyQt 集成库 -├── Resources/ # 资源文件 (模型、纹理等) -├── config/ # 配置文件 -│ └── vr_settings.json # VR 配置文件 -├── main.py # 程序入口点 -└── Start_Run.py # 启动脚本 -``` - -## 核心功能模块 - -### 1. World (core/world.py, main.py) - -- `CoreWorld`: 基础 3D 世界设置,包括相机、光照、地面、资源路径。 -- `MyWorld`: 扩展的主世界类,整合所有管理器和功能模块。 - - **初始化**: 设置资源路径、相机、光照、地面,加载中文字体。 - - **兼容性**: 提供旧版属性访问接口。 - - **功能代理**: 将大量功能委托给专门的管理器。 - -### 2. VR 系统 (core/vr/) - -这是一个高度模块化的 VR 子系统,核心是 `VRManager` (core/vr_manager.py)。 - -- **VRManager**: - - **初始化与状态管理**: 检查 VR 可用性、初始化 OpenVR、管理启用/禁用状态。 - - **渲染系统**: - - 支持普通渲染和 RenderPipeline 高级渲染两种模式。 - - 创建和管理左右眼的渲染缓冲区和相机。 - - 实现高效的纹理提交到 OpenVR Compositor。 - - 支持动态分辨率缩放和质量预设。 - - **跟踪系统**: - - 通过 OpenVR 获取 HMD 和控制器的姿态。 - - 使用锚点层级系统 (`tracking_space`, `hmd_anchor` 等) 管理设备位置。 - - 坐标系转换 (OpenVR 到 Panda3D)。 - - **控制器**: - - `LeftController`, `RightController`: 管理具体的手柄输入和可视化。 - - 支持动作系统 (VRActionManager) 或直接输入读取。 - - **交互系统**: - - **VRInteractionManager**: 对象抓取和交互。 - - **VRTeleportSystem**: 传送功能。 - - **VRJoystickManager**: 摇杆移动控制。 - - **性能优化**: - - 对象池 (Mat4) 减少 GC 压力。 - - 纹理 ID 缓存避免重复 prepare。 - - 智能 GPU 同步策略。 - - 性能模式自动切换。 - - **配置管理**: - - `VRConfigManager`: 从 `config/vr_settings.json` 加载/保存配置。 - - **测试与调试**: - - `VRTestMode`: 提供不同的测试显示模式和功能开关。 - - `VRPerformanceMonitor`: 性能监控和报告。 - -### 3. GUI 系统 (core/gui_manager.py, gui/) - -- **GUIManager**: 管理 2D 和 3D GUI 元素的创建、编辑、删除。 -- **功能**: - - 创建按钮、标签、输入框、2D/3D 图像、视频屏幕等。 - - GUI 编辑模式,支持拖拽创建和属性编辑。 - - 与场景树和属性面板集成。 - - 独立的 GUI 预览窗口。 - -### 4. 场景与模型管理 (core/scene_manager.py) - -- **SceneManager**: 管理 3D 场景中的所有模型。 -- **功能**: - - 模型导入 (支持 glTF, FBX 转换)。 - - 材质和几何体处理。 - - 碰撞体设置。 - - 场景保存/加载 (BAM 格式)。 - - 场景树更新。 - -### 5. 脚本系统 (core/script_system.py) - -- **ScriptManager**: 嵌入式 Python 脚本系统。 -- **功能**: - - 脚本文件的创建、加载、重载。 - - 为游戏对象挂载/卸载脚本。 - - 热重载支持。 - - 脚本信息查询。 - -### 6. 地形系统 (core/terrain_manager.py) - -- **TerrainManager**: 管理 3D 地形。 -- **功能**: - - 从高度图或创建平面地形。 - - 地形 LOD 更新。 - - 地形高度查询和编辑。 - -### 7. 碰撞系统 (core/collision_manager.py) - -- **CollisionManager**: 处理场景中的碰撞检测。 -- **功能**: - - 模型间碰撞检测。 - - 碰撞历史和统计。 - -### 8. 工具与选择系统 (core/tool_manager.py, core/selection.py) - -- **ToolManager**: 管理当前使用的工具 (选择、移动、旋转、缩放)。 -- **SelectionSystem**: 处理对象选择逻辑和相机聚焦。 - -## 构建与运行 - -### 入口点 - -- `main.py`: 主程序入口,创建 `MyWorld` 实例并启动 PyQt5 主窗口。 -- `Start_Run.py`: 可能的启动脚本。 - -### 运行方式 - -1. 确保已安装所有依赖项(Panda3D, PyQt5, OpenVR 等)。 -2. 运行 `python main.py` 启动应用程序。 -3. 如果连接了 VR 设备并安装了 SteamVR,可以在应用内启用 VR 模式。 - -### 依赖项 - -项目未提供 `requirements.txt` 文件,但根据代码分析,主要依赖包括: - -- `panda3d` -- `PyQt5` -- `openvr` (用于 VR 功能) -- `numpy` (在 VR 模块中使用) - -## 开发约定 - -- **模块化设计**: 功能被分解到不同的管理器类中,`MyWorld` 主要起到集成和代理的作用。 -- **VR 子模块化**: VR 功能被组织在 `core/vr/` 目录下,具有清晰的子模块划分。 -- **配置驱动**: VR 设置通过 `config/vr_settings.json` 文件进行管理。 -- **性能意识**: VR 模块包含大量性能优化措施,如对象池、缓存、智能同步等。 -- **向后兼容**: `MyWorld` 通过属性代理保持与旧代码的兼容性。 -- **测试模式**: VR 系统包含专门的测试模式,便于调试和验证不同功能。 +# iFlow 上下文文档 + +## 项目概述 + +这是一个基于 Panda3D 和 PyQt5 的 3D 虚拟现实 (VR) 应用程序框架。该项目旨在提供一个功能齐全、模块化的 3D 环境,支持 VR 设备(如 HTC Vive, Oculus Rift)的集成,包含场景管理、模型导入、GUI 系统、脚本系统、地形系统、碰撞检测以及完整的 VR 交互功能。 + +核心架构围绕 `MyWorld` 类构建,该类继承自 `CoreWorld`,并集成了各种管理器模块,如选择系统、工具管理器、脚本管理器、GUI 管理器、场景管理器、项目管理器、地形管理器、碰撞管理器和 VR 管理器。 + +## 核心技术栈 + +- **核心引擎**: Panda3D +- **图形渲染**: 可选择普通渲染或 RenderPipeline 高级渲染管线 +- **VR 支持**: OpenVR/SteamVR +- **用户界面**: PyQt5 +- **3D 模型格式**: 支持 glTF, FBX (需转换), BAM 等 +- **脚本语言**: Python (内嵌脚本系统) +- **物理/碰撞**: Panda3D 内置碰撞系统 + +## 项目结构 + +``` +. +├── core/ # 核心模块 +│ ├── world.py # CoreWorld 基础世界类 +│ ├── vr/ # VR 子模块 (完整模块化结构) +│ │ ├── __init__.py +│ │ ├── config/ # VR 配置管理 +│ │ ├── interaction/ # VR 交互系统 (动作、抓取、摇杆、传送) +│ │ ├── performance/ # VR 性能监控 +│ │ ├── rendering/ # VR 渲染相关 (RenderPipeline 集成) +│ │ ├── testing/ # VR 测试模式 +│ │ ├── tracking/ # VR 设备跟踪 +│ │ └── visualization/ # VR 可视化 (控制器模型) +│ ├── vr_manager.py # VR 管理器主文件 (待拆分) +│ ├── selection.py # 选择系统 +│ ├── tool_manager.py # 工具管理器 +│ ├── script_system.py # 脚本系统 +│ ├── gui_manager.py # GUI 管理器 +│ ├── terrain_manager.py # 地形管理器 +│ ├── collision_manager.py # 碰撞管理器 +│ ├── event_handler.py # 事件处理器 +│ ├── patrol_system.py # 巡检系统 +│ ├── Command_System.py # 命令系统 +│ └── InfoPanelManager.py # 信息面板管理器 +├── demo/ # 示例和测试文件 +├── gui/ # GUI 相关模块 +├── project/ # 项目管理模块 +├── scene/ # 场景管理模块 (部分代码在 core/) +├── scripts/ # 脚本文件目录 +├── ui/ # UI 组件和主窗口 +├── QPanda3D/ # Panda3D 与 PyQt 集成库 +├── Resources/ # 资源文件 (模型、纹理等) +├── config/ # 配置文件 +│ └── vr_settings.json # VR 配置文件 +├── main.py # 程序入口点 +└── Start_Run.py # 启动脚本 +``` + +## 核心功能模块 + +### 1. World (core/world.py, main.py) + +- `CoreWorld`: 基础 3D 世界设置,包括相机、光照、地面、资源路径。 +- `MyWorld`: 扩展的主世界类,整合所有管理器和功能模块。 + - **初始化**: 设置资源路径、相机、光照、地面,加载中文字体。 + - **兼容性**: 提供旧版属性访问接口。 + - **功能代理**: 将大量功能委托给专门的管理器。 + +### 2. VR 系统 (core/vr/) + +这是一个高度模块化的 VR 子系统,核心是 `VRManager` (core/vr_manager.py)。 + +- **VRManager**: + - **初始化与状态管理**: 检查 VR 可用性、初始化 OpenVR、管理启用/禁用状态。 + - **渲染系统**: + - 支持普通渲染和 RenderPipeline 高级渲染两种模式。 + - 创建和管理左右眼的渲染缓冲区和相机。 + - 实现高效的纹理提交到 OpenVR Compositor。 + - 支持动态分辨率缩放和质量预设。 + - **跟踪系统**: + - 通过 OpenVR 获取 HMD 和控制器的姿态。 + - 使用锚点层级系统 (`tracking_space`, `hmd_anchor` 等) 管理设备位置。 + - 坐标系转换 (OpenVR 到 Panda3D)。 + - **控制器**: + - `LeftController`, `RightController`: 管理具体的手柄输入和可视化。 + - 支持动作系统 (VRActionManager) 或直接输入读取。 + - **交互系统**: + - **VRInteractionManager**: 对象抓取和交互。 + - **VRTeleportSystem**: 传送功能。 + - **VRJoystickManager**: 摇杆移动控制。 + - **性能优化**: + - 对象池 (Mat4) 减少 GC 压力。 + - 纹理 ID 缓存避免重复 prepare。 + - 智能 GPU 同步策略。 + - 性能模式自动切换。 + - **配置管理**: + - `VRConfigManager`: 从 `config/vr_settings.json` 加载/保存配置。 + - **测试与调试**: + - `VRTestMode`: 提供不同的测试显示模式和功能开关。 + - `VRPerformanceMonitor`: 性能监控和报告。 + +### 3. GUI 系统 (core/gui_manager.py, gui/) + +- **GUIManager**: 管理 2D 和 3D GUI 元素的创建、编辑、删除。 +- **功能**: + - 创建按钮、标签、输入框、2D/3D 图像、视频屏幕等。 + - GUI 编辑模式,支持拖拽创建和属性编辑。 + - 与场景树和属性面板集成。 + - 独立的 GUI 预览窗口。 + +### 4. 场景与模型管理 (core/scene_manager.py) + +- **SceneManager**: 管理 3D 场景中的所有模型。 +- **功能**: + - 模型导入 (支持 glTF, FBX 转换)。 + - 材质和几何体处理。 + - 碰撞体设置。 + - 场景保存/加载 (BAM 格式)。 + - 场景树更新。 + +### 5. 脚本系统 (core/script_system.py) + +- **ScriptManager**: 嵌入式 Python 脚本系统。 +- **功能**: + - 脚本文件的创建、加载、重载。 + - 为游戏对象挂载/卸载脚本。 + - 热重载支持。 + - 脚本信息查询。 + +### 6. 地形系统 (core/terrain_manager.py) + +- **TerrainManager**: 管理 3D 地形。 +- **功能**: + - 从高度图或创建平面地形。 + - 地形 LOD 更新。 + - 地形高度查询和编辑。 + +### 7. 碰撞系统 (core/collision_manager.py) + +- **CollisionManager**: 处理场景中的碰撞检测。 +- **功能**: + - 模型间碰撞检测。 + - 碰撞历史和统计。 + +### 8. 工具与选择系统 (core/tool_manager.py, core/selection.py) + +- **ToolManager**: 管理当前使用的工具 (选择、移动、旋转、缩放)。 +- **SelectionSystem**: 处理对象选择逻辑和相机聚焦。 + +## 构建与运行 + +### 入口点 + +- `main.py`: 主程序入口,创建 `MyWorld` 实例并启动 PyQt5 主窗口。 +- `Start_Run.py`: 可能的启动脚本。 + +### 运行方式 + +1. 确保已安装所有依赖项(Panda3D, PyQt5, OpenVR 等)。 +2. 运行 `python main.py` 启动应用程序。 +3. 如果连接了 VR 设备并安装了 SteamVR,可以在应用内启用 VR 模式。 + +### 依赖项 + +项目未提供 `requirements.txt` 文件,但根据代码分析,主要依赖包括: + +- `panda3d` +- `PyQt5` +- `openvr` (用于 VR 功能) +- `numpy` (在 VR 模块中使用) + +## 开发约定 + +- **模块化设计**: 功能被分解到不同的管理器类中,`MyWorld` 主要起到集成和代理的作用。 +- **VR 子模块化**: VR 功能被组织在 `core/vr/` 目录下,具有清晰的子模块划分。 +- **配置驱动**: VR 设置通过 `config/vr_settings.json` 文件进行管理。 +- **性能意识**: VR 模块包含大量性能优化措施,如对象池、缓存、智能同步等。 +- **向后兼容**: `MyWorld` 通过属性代理保持与旧代码的兼容性。 +- **测试模式**: VR 系统包含专门的测试模式,便于调试和验证不同功能。 diff --git a/QMeta3D/Helpers/Env_Grid_Maker.py b/QMeta3D/Helpers/Env_Grid_Maker.py index a498611e..a17d9006 100644 --- a/QMeta3D/Helpers/Env_Grid_Maker.py +++ b/QMeta3D/Helpers/Env_Grid_Maker.py @@ -1,271 +1,271 @@ -from panda3d.core import * -from direct.interval.IntervalGlobal import * - -class Env_Grid_Maker: - def __init__(self, XYPlaneShow = True, XZPlaneShow = False, YZPlaneShow = False, endCapLinesShow = True, XSize = 50, YSize = 50, ZSize = 50, gridStep = 10, subdiv = 10): - #Create line objects - self.axisLines = LineSegs() - self.gridLines = LineSegs() - self.subdivLines = LineSegs() - - #Init passed variables - self.XSize = XSize - self.YSize = YSize - self.ZSize = ZSize - self.gridStep = gridStep - self.subdiv = subdiv - - #Init default variables - - #Plane and end cap line visibility (1 is show, 0 is hide) - self.XYPlaneShow = XYPlaneShow - self.XZPlaneShow = XZPlaneShow - self.YZPlaneShow = YZPlaneShow - self.endCapLinesShow = endCapLinesShow - - #Alpha variables for each plane - #self.XYPlaneAlpha = 1 - #self.XZPlaneAlpha = 1 - #self.YZPlaneAlpha = 1 - - #Colors (RGBA passed as a VBase4 object) - self.XAxisColor = VBase4(1, 0, 0, 1) - self.YAxisColor = VBase4(0, 1, 0, 1) - self.ZAxisColor = VBase4(0, 0, 1, 1) - self.gridColor = VBase4(1, 1, 1, 1) - self.subdivColor = VBase4(.35, .35, .35, 1) - - #Line thicknesses (in pixels) - self.axisThickness = 3 - self.gridThickness = 1 - self.subdivThickness = 1 - - - def create(self): - #Set line thicknesses - self.axisLines.setThickness(self.axisThickness) - self.gridLines.setThickness(self.gridThickness) - self.subdivLines.setThickness(self.subdivThickness) - - if(self.XSize != 0): - #Draw X axis line - self.axisLines.setColor(self.XAxisColor) - self.axisLines.moveTo(-(self.XSize), 0, 0) - self.axisLines.drawTo(self.XSize, 0, 0) - - if(self.YSize != 0): - #Draw Y axis line - self.axisLines.setColor(self.YAxisColor) - self.axisLines.moveTo(0, -(self.YSize), 0) - self.axisLines.drawTo(0, self.YSize, 0) - - if(self.ZSize != 0): - #Draw Z axis line - self.axisLines.setColor(self.ZAxisColor) - self.axisLines.moveTo(0, 0, -(self.ZSize)) - self.axisLines.drawTo(0, 0, self.ZSize) - - #Check to see if primary grid lines should be drawn at all - if(self.gridStep != 0): - - #Draw primary grid lines - self.gridLines.setColor(self.gridColor) - - #Draw primary grid lines metering x axis if any x-length - if(self.XSize != 0): - if((self.YSize != 0) and (self.XYPlaneShow)): - #Draw y lines across x axis starting from center moving out - #XY Plane - for x in self.myfrange(0, self.XSize, self.gridStep): - self.gridLines.moveTo(x, -(self.YSize), 0) - self.gridLines.drawTo(x, self.YSize, 0) - self.gridLines.moveTo(-x, -(self.YSize), 0) - self.gridLines.drawTo(-x, self.YSize, 0) - - if(self.endCapLinesShow): - #Draw endcap lines - self.gridLines.moveTo(self.XSize, -(self.YSize), 0) - self.gridLines.drawTo(self.XSize, self.YSize, 0) - self.gridLines.moveTo(-(self.XSize), -(self.YSize), 0) - self.gridLines.drawTo(-(self.XSize), self.YSize, 0) - - if((self.ZSize != 0) and (self.XZPlaneShow)): - #Draw z lines across x axis starting from center moving out - #XZ Plane - for x in self.myfrange(0, self.XSize, self.gridStep): - self.gridLines.moveTo(x, 0, -(self.ZSize)) - self.gridLines.drawTo(x, 0, self.ZSize) - self.gridLines.moveTo(-x, 0, -(self.ZSize)) - self.gridLines.drawTo(-x, 0, self.ZSize) - - if(self.endCapLinesShow): - #Draw endcap lines - self.gridLines.moveTo(self.XSize, 0, -(self.ZSize)) - self.gridLines.drawTo(self.XSize, 0, self.ZSize) - self.gridLines.moveTo(-(self.XSize), 0, -(self.ZSize)) - self.gridLines.drawTo(-(self.XSize), 0, self.ZSize) - - #Draw primary grid lines metering y axis if any y-length - if(self.YSize != 0): - - if((self.YSize != 0) and (self.XYPlaneShow)): - #Draw x lines across y axis - #XY Plane - for y in self.myfrange(0, self.YSize, self.gridStep): - self.gridLines.moveTo(-(self.XSize), y, 0) - self.gridLines.drawTo(self.XSize, y, 0) - self.gridLines.moveTo(-(self.XSize), -y, 0) - self.gridLines.drawTo(self.XSize, -y, 0) - - if(self.endCapLinesShow): - #Draw endcap lines - self.gridLines.moveTo(-(self.XSize), self.YSize, 0) - self.gridLines.drawTo(self.XSize, self.YSize, 0) - self.gridLines.moveTo(-(self.XSize), -(self.YSize), 0) - self.gridLines.drawTo(self.XSize, -(self.YSize), 0) - - if((self.ZSize != 0) and (self.YZPlaneShow)): - #Draw z lines across y axis - #YZ Plane - for y in self.myfrange(0, self.YSize, self.gridStep): - self.gridLines.moveTo(0, y, -(self.ZSize)) - self.gridLines.drawTo(0, y, self.ZSize) - self.gridLines.moveTo(0, -y, -(self.ZSize)) - self.gridLines.drawTo(0, -y, self.ZSize) - - if(self.endCapLinesShow): - #Draw endcap lines - self.gridLines.moveTo(0, self.YSize, -(self.ZSize)) - self.gridLines.drawTo(0, self.YSize, self.ZSize) - self.gridLines.moveTo(0, -(self.YSize), -(self.ZSize)) - self.gridLines.drawTo(0, -(self.YSize), self.ZSize) - - #Draw primary grid lines metering z axis if any z-length - if(self.ZSize != 0): - if((self.XSize != 0) and (self.XZPlaneShow)): - #Draw x lines across z axis - #XZ Plane - for z in self.myfrange(0, self.ZSize, self.gridStep): - self.gridLines.moveTo(-(self.XSize), 0, z) - self.gridLines.drawTo(self.XSize, 0, z) - self.gridLines.moveTo(-(self.XSize), 0, -z) - self.gridLines.drawTo(self.XSize, 0, -z) - - if(self.endCapLinesShow): - #Draw endcap lines - self.gridLines.moveTo(-(self.XSize), 0, self.ZSize) - self.gridLines.drawTo(self.XSize, 0, self.ZSize) - self.gridLines.moveTo(-(self.XSize), 0, -(self.ZSize)) - self.gridLines.drawTo(self.XSize, 0, -(self.ZSize)) - - if((self.YSize != 0) and (self.YZPlaneShow)): - #Draw y lines across z axis - #YZ Plane - for z in self.myfrange(0, self.ZSize, self.gridStep): - self.gridLines.moveTo(0, -(self.YSize), z) - self.gridLines.drawTo(0, self.YSize, z) - self.gridLines.moveTo(0, -(self.YSize), -z) - self.gridLines.drawTo(0, self.YSize, -z) - - if(self.endCapLinesShow): - #Draw endcap lines - self.gridLines.moveTo(0, -(self.YSize), self.ZSize) - self.gridLines.drawTo(0, self.YSize, self.ZSize) - self.gridLines.moveTo(0, -(self.YSize), -(self.ZSize)) - self.gridLines.drawTo(0, self.YSize, -(self.ZSize)) - - #Check to see if secondary grid lines should be drawn - if(self.subdiv != 0): - - #Draw secondary grid lines - self.subdivLines.setColor(self.subdivColor) - - if(self.XSize != 0): - adjustedstep = self.gridStep / self.subdiv - if((self.YSize != 0) and (self.XYPlaneShow)): - #Draw y lines across x axis starting from center moving out - #XY - for x in self.myfrange(0, self.XSize, adjustedstep): - self.subdivLines.moveTo(x, -(self.YSize), 0) - self.subdivLines.drawTo(x, self.YSize, 0) - self.subdivLines.moveTo(-x, -(self.YSize), 0) - self.subdivLines.drawTo(-x, self.YSize, 0) - - if((self.ZSize != 0) and (self.XZPlaneShow)): - #Draw z lines across x axis starting from center moving out - #XZ - for x in self.myfrange(0, self.XSize, adjustedstep): - self.subdivLines.moveTo(x, 0, -(self.ZSize)) - self.subdivLines.drawTo(x, 0, self.ZSize) - self.subdivLines.moveTo(-x, 0, -(self.ZSize)) - self.subdivLines.drawTo(-x, 0, self.ZSize) - - if(self.YSize != 0): - if((self.YSize != 0) and (self.XYPlaneShow)): - #Draw x lines across y axis - #XY - for y in self.myfrange(0, self.YSize, adjustedstep): - self.subdivLines.moveTo(-(self.XSize), y, 0) - self.subdivLines.drawTo(self.XSize, y, 0) - self.subdivLines.moveTo(-(self.XSize), -y, 0) - self.subdivLines.drawTo(self.XSize, -y, 0) - if((self.ZSize != 0) and (self.YZPlaneShow)): - #Draw z lines across y axis - #YZ - for y in self.myfrange(0, self.YSize, adjustedstep): - self.subdivLines.moveTo(0, y, -(self.ZSize)) - self.subdivLines.drawTo(0, y, self.ZSize) - self.subdivLines.moveTo(0, -y, -(self.ZSize)) - self.subdivLines.drawTo(0, -y, self.ZSize) - - if(self.ZSize != 0): - - if((self.XSize != 0) and (self.XZPlaneShow)): - #Draw x lines across z axis - #XZ - for z in self.myfrange(0, self.ZSize, adjustedstep): - self.subdivLines.moveTo(-(self.XSize), 0, z) - self.subdivLines.drawTo(self.XSize, 0, z) - self.subdivLines.moveTo(-(self.XSize), 0, -z) - self.subdivLines.drawTo(self.XSize, 0, -z) - - if((self.YSize != 0) and (self.YZPlaneShow)): - #Draw y lines across z axis - #YZ - for z in self.myfrange(0, self.ZSize, adjustedstep): - self.subdivLines.moveTo(0, -(self.YSize), z) - self.subdivLines.drawTo(0, self.YSize, z) - self.subdivLines.moveTo(0, -(self.YSize), -z) - self.subdivLines.drawTo(0, self.YSize, -z) - - #Create ThreeAxisGrid nodes and nodepaths - #Create parent node and path - self.parentNode = PandaNode('threeaxisgrid-parentnode') - self.parentNodePath = NodePath(self.parentNode) - - #Create axis lines node and path, then reparent - self.axisLinesNode = self.axisLines.create() - self.axisLinesNodePath = NodePath(self.axisLinesNode) - self.axisLinesNodePath.reparentTo(self.parentNodePath) - - #Create grid lines node and path, then reparent - self.gridLinesNode = self.gridLines.create() - self.gridLinesNodePath = NodePath(self.gridLinesNode) - self.gridLinesNodePath.reparentTo(self.parentNodePath) - - #Create subdivision lines node and path then reparent - self.subdivLinesNode = self.subdivLines.create() - self.subdivLinesNodePath = NodePath(self.subdivLinesNode) - self.subdivLinesNodePath.reparentTo(self.parentNodePath) - - return self.parentNodePath - def myfrange(self, start, stop=None, step=None): - if stop is None: - stop = float(start) - start = 0.0 - if step is None: - step = 1.0 - cur = float(start) - while cur < stop: - yield cur +from panda3d.core import * +from direct.interval.IntervalGlobal import * + +class Env_Grid_Maker: + def __init__(self, XYPlaneShow = True, XZPlaneShow = False, YZPlaneShow = False, endCapLinesShow = True, XSize = 50, YSize = 50, ZSize = 50, gridStep = 10, subdiv = 10): + #Create line objects + self.axisLines = LineSegs() + self.gridLines = LineSegs() + self.subdivLines = LineSegs() + + #Init passed variables + self.XSize = XSize + self.YSize = YSize + self.ZSize = ZSize + self.gridStep = gridStep + self.subdiv = subdiv + + #Init default variables + + #Plane and end cap line visibility (1 is show, 0 is hide) + self.XYPlaneShow = XYPlaneShow + self.XZPlaneShow = XZPlaneShow + self.YZPlaneShow = YZPlaneShow + self.endCapLinesShow = endCapLinesShow + + #Alpha variables for each plane + #self.XYPlaneAlpha = 1 + #self.XZPlaneAlpha = 1 + #self.YZPlaneAlpha = 1 + + #Colors (RGBA passed as a VBase4 object) + self.XAxisColor = VBase4(1, 0, 0, 1) + self.YAxisColor = VBase4(0, 1, 0, 1) + self.ZAxisColor = VBase4(0, 0, 1, 1) + self.gridColor = VBase4(1, 1, 1, 1) + self.subdivColor = VBase4(.35, .35, .35, 1) + + #Line thicknesses (in pixels) + self.axisThickness = 3 + self.gridThickness = 1 + self.subdivThickness = 1 + + + def create(self): + #Set line thicknesses + self.axisLines.setThickness(self.axisThickness) + self.gridLines.setThickness(self.gridThickness) + self.subdivLines.setThickness(self.subdivThickness) + + if(self.XSize != 0): + #Draw X axis line + self.axisLines.setColor(self.XAxisColor) + self.axisLines.moveTo(-(self.XSize), 0, 0) + self.axisLines.drawTo(self.XSize, 0, 0) + + if(self.YSize != 0): + #Draw Y axis line + self.axisLines.setColor(self.YAxisColor) + self.axisLines.moveTo(0, -(self.YSize), 0) + self.axisLines.drawTo(0, self.YSize, 0) + + if(self.ZSize != 0): + #Draw Z axis line + self.axisLines.setColor(self.ZAxisColor) + self.axisLines.moveTo(0, 0, -(self.ZSize)) + self.axisLines.drawTo(0, 0, self.ZSize) + + #Check to see if primary grid lines should be drawn at all + if(self.gridStep != 0): + + #Draw primary grid lines + self.gridLines.setColor(self.gridColor) + + #Draw primary grid lines metering x axis if any x-length + if(self.XSize != 0): + if((self.YSize != 0) and (self.XYPlaneShow)): + #Draw y lines across x axis starting from center moving out + #XY Plane + for x in self.myfrange(0, self.XSize, self.gridStep): + self.gridLines.moveTo(x, -(self.YSize), 0) + self.gridLines.drawTo(x, self.YSize, 0) + self.gridLines.moveTo(-x, -(self.YSize), 0) + self.gridLines.drawTo(-x, self.YSize, 0) + + if(self.endCapLinesShow): + #Draw endcap lines + self.gridLines.moveTo(self.XSize, -(self.YSize), 0) + self.gridLines.drawTo(self.XSize, self.YSize, 0) + self.gridLines.moveTo(-(self.XSize), -(self.YSize), 0) + self.gridLines.drawTo(-(self.XSize), self.YSize, 0) + + if((self.ZSize != 0) and (self.XZPlaneShow)): + #Draw z lines across x axis starting from center moving out + #XZ Plane + for x in self.myfrange(0, self.XSize, self.gridStep): + self.gridLines.moveTo(x, 0, -(self.ZSize)) + self.gridLines.drawTo(x, 0, self.ZSize) + self.gridLines.moveTo(-x, 0, -(self.ZSize)) + self.gridLines.drawTo(-x, 0, self.ZSize) + + if(self.endCapLinesShow): + #Draw endcap lines + self.gridLines.moveTo(self.XSize, 0, -(self.ZSize)) + self.gridLines.drawTo(self.XSize, 0, self.ZSize) + self.gridLines.moveTo(-(self.XSize), 0, -(self.ZSize)) + self.gridLines.drawTo(-(self.XSize), 0, self.ZSize) + + #Draw primary grid lines metering y axis if any y-length + if(self.YSize != 0): + + if((self.YSize != 0) and (self.XYPlaneShow)): + #Draw x lines across y axis + #XY Plane + for y in self.myfrange(0, self.YSize, self.gridStep): + self.gridLines.moveTo(-(self.XSize), y, 0) + self.gridLines.drawTo(self.XSize, y, 0) + self.gridLines.moveTo(-(self.XSize), -y, 0) + self.gridLines.drawTo(self.XSize, -y, 0) + + if(self.endCapLinesShow): + #Draw endcap lines + self.gridLines.moveTo(-(self.XSize), self.YSize, 0) + self.gridLines.drawTo(self.XSize, self.YSize, 0) + self.gridLines.moveTo(-(self.XSize), -(self.YSize), 0) + self.gridLines.drawTo(self.XSize, -(self.YSize), 0) + + if((self.ZSize != 0) and (self.YZPlaneShow)): + #Draw z lines across y axis + #YZ Plane + for y in self.myfrange(0, self.YSize, self.gridStep): + self.gridLines.moveTo(0, y, -(self.ZSize)) + self.gridLines.drawTo(0, y, self.ZSize) + self.gridLines.moveTo(0, -y, -(self.ZSize)) + self.gridLines.drawTo(0, -y, self.ZSize) + + if(self.endCapLinesShow): + #Draw endcap lines + self.gridLines.moveTo(0, self.YSize, -(self.ZSize)) + self.gridLines.drawTo(0, self.YSize, self.ZSize) + self.gridLines.moveTo(0, -(self.YSize), -(self.ZSize)) + self.gridLines.drawTo(0, -(self.YSize), self.ZSize) + + #Draw primary grid lines metering z axis if any z-length + if(self.ZSize != 0): + if((self.XSize != 0) and (self.XZPlaneShow)): + #Draw x lines across z axis + #XZ Plane + for z in self.myfrange(0, self.ZSize, self.gridStep): + self.gridLines.moveTo(-(self.XSize), 0, z) + self.gridLines.drawTo(self.XSize, 0, z) + self.gridLines.moveTo(-(self.XSize), 0, -z) + self.gridLines.drawTo(self.XSize, 0, -z) + + if(self.endCapLinesShow): + #Draw endcap lines + self.gridLines.moveTo(-(self.XSize), 0, self.ZSize) + self.gridLines.drawTo(self.XSize, 0, self.ZSize) + self.gridLines.moveTo(-(self.XSize), 0, -(self.ZSize)) + self.gridLines.drawTo(self.XSize, 0, -(self.ZSize)) + + if((self.YSize != 0) and (self.YZPlaneShow)): + #Draw y lines across z axis + #YZ Plane + for z in self.myfrange(0, self.ZSize, self.gridStep): + self.gridLines.moveTo(0, -(self.YSize), z) + self.gridLines.drawTo(0, self.YSize, z) + self.gridLines.moveTo(0, -(self.YSize), -z) + self.gridLines.drawTo(0, self.YSize, -z) + + if(self.endCapLinesShow): + #Draw endcap lines + self.gridLines.moveTo(0, -(self.YSize), self.ZSize) + self.gridLines.drawTo(0, self.YSize, self.ZSize) + self.gridLines.moveTo(0, -(self.YSize), -(self.ZSize)) + self.gridLines.drawTo(0, self.YSize, -(self.ZSize)) + + #Check to see if secondary grid lines should be drawn + if(self.subdiv != 0): + + #Draw secondary grid lines + self.subdivLines.setColor(self.subdivColor) + + if(self.XSize != 0): + adjustedstep = self.gridStep / self.subdiv + if((self.YSize != 0) and (self.XYPlaneShow)): + #Draw y lines across x axis starting from center moving out + #XY + for x in self.myfrange(0, self.XSize, adjustedstep): + self.subdivLines.moveTo(x, -(self.YSize), 0) + self.subdivLines.drawTo(x, self.YSize, 0) + self.subdivLines.moveTo(-x, -(self.YSize), 0) + self.subdivLines.drawTo(-x, self.YSize, 0) + + if((self.ZSize != 0) and (self.XZPlaneShow)): + #Draw z lines across x axis starting from center moving out + #XZ + for x in self.myfrange(0, self.XSize, adjustedstep): + self.subdivLines.moveTo(x, 0, -(self.ZSize)) + self.subdivLines.drawTo(x, 0, self.ZSize) + self.subdivLines.moveTo(-x, 0, -(self.ZSize)) + self.subdivLines.drawTo(-x, 0, self.ZSize) + + if(self.YSize != 0): + if((self.YSize != 0) and (self.XYPlaneShow)): + #Draw x lines across y axis + #XY + for y in self.myfrange(0, self.YSize, adjustedstep): + self.subdivLines.moveTo(-(self.XSize), y, 0) + self.subdivLines.drawTo(self.XSize, y, 0) + self.subdivLines.moveTo(-(self.XSize), -y, 0) + self.subdivLines.drawTo(self.XSize, -y, 0) + if((self.ZSize != 0) and (self.YZPlaneShow)): + #Draw z lines across y axis + #YZ + for y in self.myfrange(0, self.YSize, adjustedstep): + self.subdivLines.moveTo(0, y, -(self.ZSize)) + self.subdivLines.drawTo(0, y, self.ZSize) + self.subdivLines.moveTo(0, -y, -(self.ZSize)) + self.subdivLines.drawTo(0, -y, self.ZSize) + + if(self.ZSize != 0): + + if((self.XSize != 0) and (self.XZPlaneShow)): + #Draw x lines across z axis + #XZ + for z in self.myfrange(0, self.ZSize, adjustedstep): + self.subdivLines.moveTo(-(self.XSize), 0, z) + self.subdivLines.drawTo(self.XSize, 0, z) + self.subdivLines.moveTo(-(self.XSize), 0, -z) + self.subdivLines.drawTo(self.XSize, 0, -z) + + if((self.YSize != 0) and (self.YZPlaneShow)): + #Draw y lines across z axis + #YZ + for z in self.myfrange(0, self.ZSize, adjustedstep): + self.subdivLines.moveTo(0, -(self.YSize), z) + self.subdivLines.drawTo(0, self.YSize, z) + self.subdivLines.moveTo(0, -(self.YSize), -z) + self.subdivLines.drawTo(0, self.YSize, -z) + + #Create ThreeAxisGrid nodes and nodepaths + #Create parent node and path + self.parentNode = PandaNode('threeaxisgrid-parentnode') + self.parentNodePath = NodePath(self.parentNode) + + #Create axis lines node and path, then reparent + self.axisLinesNode = self.axisLines.create() + self.axisLinesNodePath = NodePath(self.axisLinesNode) + self.axisLinesNodePath.reparentTo(self.parentNodePath) + + #Create grid lines node and path, then reparent + self.gridLinesNode = self.gridLines.create() + self.gridLinesNodePath = NodePath(self.gridLinesNode) + self.gridLinesNodePath.reparentTo(self.parentNodePath) + + #Create subdivision lines node and path then reparent + self.subdivLinesNode = self.subdivLines.create() + self.subdivLinesNodePath = NodePath(self.subdivLinesNode) + self.subdivLinesNodePath.reparentTo(self.parentNodePath) + + return self.parentNodePath + def myfrange(self, start, stop=None, step=None): + if stop is None: + stop = float(start) + start = 0.0 + if step is None: + step = 1.0 + cur = float(start) + while cur < stop: + yield cur cur += step \ No newline at end of file diff --git a/QMeta3D/Helpers/__init__.py b/QMeta3D/Helpers/__init__.py index e58d61a7..18352747 100644 --- a/QMeta3D/Helpers/__init__.py +++ b/QMeta3D/Helpers/__init__.py @@ -1,2 +1,2 @@ -name="QMeta3D" +name="QMeta3D" __all__ = ["Env_Grid_Maker"] \ No newline at end of file diff --git a/QMeta3D/Meta3DWorld.py b/QMeta3D/Meta3DWorld.py index c325da5c..a6b88ffc 100644 --- a/QMeta3D/Meta3DWorld.py +++ b/QMeta3D/Meta3DWorld.py @@ -1,192 +1,192 @@ -import sys -import os - -from core.CustomMouseController import CustomMouseController - -# 获取 RenderPipelineFile 的路径 -render_pipeline_path = './RenderPipelineFile' -# 将该路径添加到 sys.path 中,确保 Python 能够找到它 -project_root = os.path.dirname(os.path.abspath(__file__)) -sys.path.insert(0, project_root) -sys.path.insert(0, render_pipeline_path) - -from RenderPipelineFile.rpcore import RenderPipeline -_global_render_pipeline = None - -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * - -from panda3d.core import * -from panda3d.core import GraphicsOutput, Texture, ConfigVariableManager, WindowProperties - -from direct.showbase.ShowBase import ShowBase -import platform - -from QMeta3D.QMouseWatcherNode import QMouseWatcherNode - -from RenderPipelineFile.rpcore.render_target import RenderTarget -__all__ = ["Meta3DWorld"] - -_global_world_instance=None -import builtins - - -class Meta3DWorld(ShowBase): - def __init__(self, width=1380, height=750, is_fullscreen=False, size=1.0, clear_color=LVecBase4f(0, 0.5, 0, 1), - name="qMeta3D"): - - global _global_world_instance - global _global_render_pipeline - - _global_world_instance = self - - sort = -100 - self.parent = None - - # 设置基本配置 - loadPrcFileData("", "show-frame-rate-meter 0") - loadPrcFileData("", "window-type offscreen") # 设置为离屏渲染 - loadPrcFileData("", f"win-size {width} {height}") - loadPrcFileData("", "win-fixed-size #f") # 允许窗口调整大小 - - # 🚀 VR性能优化配置 - loadPrcFileData("", "prefer-single-buffer true") # 减少缓冲区交换开销 - loadPrcFileData("", "gl-force-flush false") # 避免强制glFlush导致的性能损失 - loadPrcFileData("", "sync-video false") # 禁用默认VSync,让OpenVR控制 - loadPrcFileData("", "support-stencil false") # 禁用不必要的模板缓冲区 - loadPrcFileData("", "clock-mode non-real-time") - # loadPrcFileData("", "gl-debug true") - - if (is_fullscreen): - loadPrcFileData("", "fullscreen #t") - - self.render_pipeline = RenderPipeline() - self.render_pipeline.pre_showbase_init() - - ShowBase.__init__(self) - - # 初始化渲染管线并设置可调整大小的标志 - self.render_pipeline.create(self) - _global_render_pipeline = self.render_pipeline - - # 创建 Qt 能读的 RGBA8 贴图 - self.qt_output_tex = Texture("qt_output_tex") - self.qt_output_tex.set_format(Texture.F_rgba8) - self.qt_output_tex.set_component_type(Texture.T_unsigned_byte) - - # # 获取图形管线对象 - # gsg = self.win.getGsg() - # host = gsg.getEngine().getHost() - - self.win.add_render_texture(self.qt_output_tex, GraphicsOutput.RTM_copy_ram) - - #self.screenTexture = Texture() - #self.screenTexture.setMinfilter(Texture.FTLinear) - #self.screenTexture.setFormat(Texture.FRgba32) - #self.screenTexture.set_wrap_u(Texture.WM_clamp) - #self.screenTexture.set_wrap_v(Texture.WM_clamp) - - # buff_size_x = int(self.win.get_x_size() * size) - # buff_size_y = int(self.win.get_y_size() * size) - # winprops = WindowProperties() - # winprops.set_size(buff_size_x, buff_size_y) - # - # - # props = FrameBufferProperties() - # props.set_rgb_color(True) - # props.set_rgba_bits(8, 8, 8, 8) - # props.set_depth_bits(8) - - # self.buff = self.graphicsEngine.make_output( - # self.pipe, name, sort, - # props, winprops, - # GraphicsPipe.BF_resizeable, - # self.win.get_gsg(), self.win) - - #self.screenTexture = render_pipeline._final_stage.target.color_tex - - #self.buff.addRenderTexture(self.screenTexture, GraphicsOutput.RTMCopyRam) - # self.buff.set_sort(sort) - - - - #self.cam = self.makeCamera(self.buff) - #self.render_pipeline._showbase.cam=self.makeCamera(self.buff) - - #self.render_pipeline._showbase.cam=self.makeCamera(self.buff) - - #self.render_pipeline._showbase.camera.reparentTo(self.render) - #base.camera.reparentTo(self.render) - #self.cam.reparentTo(self.render) # 可选同步 - - #render_pipeline.set_camera(self.cam) - - #添加渲染效果 - #self.cam = self.render_pipeline._showbase.cam - #self.camNode = self.cam.node() - #self.camLens = self.camNode.get_lens() - self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam - #self.render_pipeline.daytime_mgr.update() - - - # if clear_color is None: - # self.buff.set_clear_active(GraphicsOutput.RTPColor, False) - # else: - # self.buff.set_clear_color(clear_color) - # self.buff.set_clear_active(GraphicsOutput.RTPColor, True) - - # self.disableMouse() - self.mouse_controller = CustomMouseController(self) - self.mouse_controller.setUp() - - # 添加错误处理钩子 - self.accept("transform_state_error", self._handle_transform_error) - - def _handle_transform_error(self): - """处理TransformState相关的错误""" - try: - from panda3d.core import TransformState, RenderState - TransformState.clear_cache() - RenderState.clear_cache() - print("已清理TransformState和RenderState缓存") - except Exception as e: - print(f"清理缓存时出错: {e}") - - def render_pipeline(self): - """获取 RenderPipeline 实例""" - return self._render_pipeline - - - def set_parent(self, parent: QWidget): - self.parent = parent - self.mouseWatcherNode = QMouseWatcherNode(parent) - - def getAspectRatio(self, win = None): - if win is None and self.parent is not None: - return float(self.parent.width()) / float(self.parent.height()) - else: - return super().getAspectRatio(win) - -def get_render_pipeline(): - """获取全局 RenderPipeline 单例""" - if _global_render_pipeline is None: - raise RuntimeError( - "RenderPipeline has not been initialized yet. Please create a 3DWorld instance first.") - return _global_render_pipeline - -def resize_buffer(self, width: int, height: int): - props = WindowProperties() - props.set_size(width, height) - self.win.request_properties(props) - - # 重新分配输出贴图的大小 - self.qt_output_tex.set_x_size(width) - self.qt_output_tex.set_y_size(height) - - # 更新 lens 的 aspect ratio - if self.camLens: - self.camLens.set_film_size(width, height) # 或 set_aspect_ratio(width / height) - - # 强制更新窗口(有时在 Qt 内嵌时需要) +import sys +import os + +from core.CustomMouseController import CustomMouseController + +# 获取 RenderPipelineFile 的路径 +render_pipeline_path = './RenderPipelineFile' +# 将该路径添加到 sys.path 中,确保 Python 能够找到它 +project_root = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, project_root) +sys.path.insert(0, render_pipeline_path) + +from RenderPipelineFile.rpcore import RenderPipeline +_global_render_pipeline = None + +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * + +from panda3d.core import * +from panda3d.core import GraphicsOutput, Texture, ConfigVariableManager, WindowProperties + +from direct.showbase.ShowBase import ShowBase +import platform + +from QMeta3D.QMouseWatcherNode import QMouseWatcherNode + +from RenderPipelineFile.rpcore.render_target import RenderTarget +__all__ = ["Meta3DWorld"] + +_global_world_instance=None +import builtins + + +class Meta3DWorld(ShowBase): + def __init__(self, width=1380, height=750, is_fullscreen=False, size=1.0, clear_color=LVecBase4f(0, 0.5, 0, 1), + name="qMeta3D"): + + global _global_world_instance + global _global_render_pipeline + + _global_world_instance = self + + sort = -100 + self.parent = None + + # 设置基本配置 + loadPrcFileData("", "show-frame-rate-meter 0") + loadPrcFileData("", "window-type offscreen") # 设置为离屏渲染 + loadPrcFileData("", f"win-size {width} {height}") + loadPrcFileData("", "win-fixed-size #f") # 允许窗口调整大小 + + # 🚀 VR性能优化配置 + loadPrcFileData("", "prefer-single-buffer true") # 减少缓冲区交换开销 + loadPrcFileData("", "gl-force-flush false") # 避免强制glFlush导致的性能损失 + loadPrcFileData("", "sync-video false") # 禁用默认VSync,让OpenVR控制 + loadPrcFileData("", "support-stencil false") # 禁用不必要的模板缓冲区 + loadPrcFileData("", "clock-mode non-real-time") + # loadPrcFileData("", "gl-debug true") + + if (is_fullscreen): + loadPrcFileData("", "fullscreen #t") + + self.render_pipeline = RenderPipeline() + self.render_pipeline.pre_showbase_init() + + ShowBase.__init__(self) + + # 初始化渲染管线并设置可调整大小的标志 + self.render_pipeline.create(self) + _global_render_pipeline = self.render_pipeline + + # 创建 Qt 能读的 RGBA8 贴图 + self.qt_output_tex = Texture("qt_output_tex") + self.qt_output_tex.set_format(Texture.F_rgba8) + self.qt_output_tex.set_component_type(Texture.T_unsigned_byte) + + # # 获取图形管线对象 + # gsg = self.win.getGsg() + # host = gsg.getEngine().getHost() + + self.win.add_render_texture(self.qt_output_tex, GraphicsOutput.RTM_copy_ram) + + #self.screenTexture = Texture() + #self.screenTexture.setMinfilter(Texture.FTLinear) + #self.screenTexture.setFormat(Texture.FRgba32) + #self.screenTexture.set_wrap_u(Texture.WM_clamp) + #self.screenTexture.set_wrap_v(Texture.WM_clamp) + + # buff_size_x = int(self.win.get_x_size() * size) + # buff_size_y = int(self.win.get_y_size() * size) + # winprops = WindowProperties() + # winprops.set_size(buff_size_x, buff_size_y) + # + # + # props = FrameBufferProperties() + # props.set_rgb_color(True) + # props.set_rgba_bits(8, 8, 8, 8) + # props.set_depth_bits(8) + + # self.buff = self.graphicsEngine.make_output( + # self.pipe, name, sort, + # props, winprops, + # GraphicsPipe.BF_resizeable, + # self.win.get_gsg(), self.win) + + #self.screenTexture = render_pipeline._final_stage.target.color_tex + + #self.buff.addRenderTexture(self.screenTexture, GraphicsOutput.RTMCopyRam) + # self.buff.set_sort(sort) + + + + #self.cam = self.makeCamera(self.buff) + #self.render_pipeline._showbase.cam=self.makeCamera(self.buff) + + #self.render_pipeline._showbase.cam=self.makeCamera(self.buff) + + #self.render_pipeline._showbase.camera.reparentTo(self.render) + #base.camera.reparentTo(self.render) + #self.cam.reparentTo(self.render) # 可选同步 + + #render_pipeline.set_camera(self.cam) + + #添加渲染效果 + #self.cam = self.render_pipeline._showbase.cam + #self.camNode = self.cam.node() + #self.camLens = self.camNode.get_lens() + self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam + #self.render_pipeline.daytime_mgr.update() + + + # if clear_color is None: + # self.buff.set_clear_active(GraphicsOutput.RTPColor, False) + # else: + # self.buff.set_clear_color(clear_color) + # self.buff.set_clear_active(GraphicsOutput.RTPColor, True) + + # self.disableMouse() + self.mouse_controller = CustomMouseController(self) + self.mouse_controller.setUp() + + # 添加错误处理钩子 + self.accept("transform_state_error", self._handle_transform_error) + + def _handle_transform_error(self): + """处理TransformState相关的错误""" + try: + from panda3d.core import TransformState, RenderState + TransformState.clear_cache() + RenderState.clear_cache() + print("已清理TransformState和RenderState缓存") + except Exception as e: + print(f"清理缓存时出错: {e}") + + def render_pipeline(self): + """获取 RenderPipeline 实例""" + return self._render_pipeline + + + def set_parent(self, parent: QWidget): + self.parent = parent + self.mouseWatcherNode = QMouseWatcherNode(parent) + + def getAspectRatio(self, win = None): + if win is None and self.parent is not None: + return float(self.parent.width()) / float(self.parent.height()) + else: + return super().getAspectRatio(win) + +def get_render_pipeline(): + """获取全局 RenderPipeline 单例""" + if _global_render_pipeline is None: + raise RuntimeError( + "RenderPipeline has not been initialized yet. Please create a 3DWorld instance first.") + return _global_render_pipeline + +def resize_buffer(self, width: int, height: int): + props = WindowProperties() + props.set_size(width, height) + self.win.request_properties(props) + + # 重新分配输出贴图的大小 + self.qt_output_tex.set_x_size(width) + self.qt_output_tex.set_y_size(height) + + # 更新 lens 的 aspect ratio + if self.camLens: + self.camLens.set_film_size(width, height) # 或 set_aspect_ratio(width / height) + + # 强制更新窗口(有时在 Qt 内嵌时需要) self.graphicsEngine.open_windows() \ No newline at end of file diff --git a/QMeta3D/QMeta3DWidget.py b/QMeta3D/QMeta3DWidget.py index a39e1869..287b5565 100644 --- a/QMeta3D/QMeta3DWidget.py +++ b/QMeta3D/QMeta3DWidget.py @@ -1,252 +1,252 @@ -from PyQt5 import QtWidgets, QtGui -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -from direct.task.TaskManagerGlobal import taskMgr - -from QMeta3D.QMeta3D_Buttons_Translation import QMeta3D_Button_translation -from QMeta3D.QMeta3D_Keys_Translation import QMeta3D_Key_translation -from QMeta3D.QMeta3D_Modifiers_Translation import QMeta3D_Modifier_translation - -__all__ = ["QMeta3DWidget"] - - -class QMeta3DSynchronizer(QTimer): - def __init__(self, qMeta3DWidget, FPS=60): - QTimer.__init__(self) - self.qMeta3DWidget = qMeta3DWidget - dt = 1000 / FPS - self.setInterval(int(dt)) - self.timeout.connect(self.tick) - - def tick(self): - try: - # 在渲染前清理可能损坏的TransformState对象 - from panda3d.core import TransformState - TransformState.clear_cache() - - taskMgr.step() - self.qMeta3DWidget.update() - except AssertionError as e: - # 专门处理 TransformState has_mat() 断言错误 - if "has_mat" in str(e): - print(f"警告: 检测到TransformState断言错误,已静默处理: {e}") - # 尝试恢复渲染状态 - try: - # 强制清理缓存并重试 - from panda3d.core import TransformState, RenderState - TransformState.clear_cache() - RenderState.clear_cache() - taskMgr.step() - self.qMeta3DWidget.update() - except: - pass - else: - # 重新抛出其他断言错误 - raise - except Exception as e: - # 静默处理其他所有异常 - print(f"警告: 检测到异常,已静默处理: {e}") - - -def get_panda_key_modifiers(evt): - panda_mods = [] - qt_mods = evt.modifiers() - for qt_mod, panda_mod in QMeta3D_Modifier_translation.items(): - if (qt_mods & qt_mod) == qt_mod: - panda_mods.append(panda_mod) - return panda_mods - - -def get_panda_key_modifiers_prefix(evt): - # join all modifiers (except NoModifier, which is None) with '-' - prefix = "-".join([mod for mod in get_panda_key_modifiers(evt) if mod is not None]) - - # if the prefix is not empty, append a '-' - if prefix: - prefix += '-' - - return prefix - - -class QMeta3DWidget(QWidget): - def __init__(self, Meta3DWorld, parent=None, FPS=60, debug=False): - QWidget.__init__(self, parent) - self.rp_sync_requested = False - # set fixed geometry - self.Meta3DWorld = Meta3DWorld - self.Meta3DWorld.set_parent(self) - # Setup a timer in Qt that runs taskMgr.step() to simulate Panda's own main loop - # pandaTimer = QTimer(self) - # pandaTimer.timeout.connect() - # pandaTimer.start(0) - - self.setFocusPolicy(Qt.StrongFocus) - - # Setup another timer that redraws this widget in a specific FPS - # redrawTimer = QTimer(self) - # redrawTimer.timeout.connect(self.update) - # redrawTimer.start(1000/FPS) - - self.paintSurface = QPainter() - self.rotate = QTransform() - self.rotate.rotate(180) - self.out_image = QImage() - - size = self.Meta3DWorld.cam.node().get_lens().get_film_size() - self.initial_film_size = QSizeF(size.x, size.y) - self.initial_size = self.size() - - self.synchronizer = QMeta3DSynchronizer(self, FPS) - self.synchronizer.start() - - self.debug = debug - - def mousePressEvent(self, evt): - button = evt.button() - try: - b = "{}{}".format(get_panda_key_modifiers_prefix(evt), QMeta3D_Button_translation[button]) - if self.debug: - print(b) - messenger.send(b,[{"x":evt.x(),"y":evt.y()}]) - except: - print("Unimplemented button. Please send an issue on github to fix this problem") - - def mouseMoveEvent(self, evt:QtGui.QMouseEvent): - button = evt.button() - try: - b = "mouse-move" - if self.debug: - print(b) - messenger.send(b,[{"x":evt.x(),"y":evt.y()}]) - except: - print("Unimplemented button. Please send an issue on github to fix this problem") - - - def mouseReleaseEvent(self, evt): - button = evt.button() - try: - b = "{}{}-up".format(get_panda_key_modifiers_prefix(evt), QMeta3D_Button_translation[button]) - if self.debug: - print(b) - messenger.send(b,[{"x":evt.x(),"y":evt.y()}]) - except: - print("Unimplemented button. Please send an issue on github to fix this problem") - - def wheelEvent(self, evt): - delta = evt.angleDelta().y() - try: - if self.debug: - print(f"wheel {delta}") - messenger.send('wheel',[{"delta":delta}]) - except: - print("Unimplemented button. Please send an issue on github to fix this problem") - - def keyPressEvent(self, evt): - key = evt.key() - try: - k = "{}{}".format(get_panda_key_modifiers_prefix(evt), QMeta3D_Key_translation[key]) - if self.debug: - print(k) - messenger.send(k) - except: - print("Unimplemented key. Please send an issue on github to fix this problem") - - def keyReleaseEvent(self, evt): - key = evt.key() - try: - k = "{}{}-up".format(get_panda_key_modifiers_prefix(evt), QMeta3D_Key_translation[key]) - if self.debug: - print(k) - messenger.send(k) - except: - print("Unimplemented key. Please send an issue on github to fix this problem") - - def resizeEvent(self, evt): - width = evt.size().width() - height = evt.size().height() - #print(f"width:{width}") - #print(f"height:{height}") - - from Meta3DWorld import resize_buffer - - lens = self.Meta3DWorld.cam.node().get_lens() - - def minimumSizeHint(self): - return QSize(400, 300) - - def paintEvent(self, event): - tex = self.Meta3DWorld.qt_output_tex - gsg = base.win.getGsg() - - if not gsg: - self.update() - return - - if not tex.hasRamImage(): - base.graphicsEngine.extractTextureData(tex, gsg) - self.update() - return - - data = tex.getRamImage().getData() - tex_width = tex.getXSize() - tex_height = tex.getYSize() - expected_len = tex_width * tex_height * 4 - - if len(data) != expected_len: - print(f"⚠️ 像素数据长度异常({len(data)} != {expected_len}),跳过绘制") - self.update() - return - - img = QImage(data, tex_width, tex_height, QImage.Format_ARGB32).mirrored() - - widget_width = self.width() - widget_height = self.height() - - painter = QPainter(self) - - # 【保持宽高比的缩放】 - scaled_img = img.scaled(widget_width, widget_height, Qt.KeepAspectRatio, Qt.SmoothTransformation) - - # 居中绘制 - x_offset = (widget_width - scaled_img.width()) // 2 - y_offset = (widget_height - scaled_img.height()) // 2 - - painter.drawImage(x_offset, y_offset, scaled_img) - painter.end() - - def sync_Meta3d_window_size(self, width, height): - try: - from panda3d.core import WindowProperties, LVecBase2i - - # 确保尺寸是4的倍数(RenderPipeline 要求) - adjusted_width = width - width % 4 - adjusted_height = height - height % 4 - - # 更新窗口属性 - props = WindowProperties() - props.setSize(adjusted_width, adjusted_height) - - # 对于 offscreen 渲染,直接更新窗口属性 - if self.Meta3DWorld.win: - self.Meta3DWorld.win.request_properties(props) - - # 手动触发 RenderPipeline 的尺寸更新逻辑 - if hasattr(self.Meta3DWorld, 'render_pipeline'): - # 更新全局分辨率 - from RenderPipelineFile.rpcore.globals import Globals - Globals.native_resolution = LVecBase2i(adjusted_width, adjusted_height) - - # 重新计算渲染分辨率 - self.Meta3DWorld.render_pipeline._compute_render_resolution() - - # 通知各个管理器处理尺寸变化 - self.Meta3DWorld.render_pipeline.light_mgr.compute_tile_size() - self.Meta3DWorld.render_pipeline.stage_mgr.handle_window_resize() - if hasattr(self.Meta3DWorld.render_pipeline, 'debugger'): - self.Meta3DWorld.render_pipeline.debugger.handle_window_resize() - - # 触发插件的窗口尺寸变化钩子 - self.Meta3DWorld.render_pipeline.plugin_mgr.trigger_hook("window_resized") - except Exception as e: +from PyQt5 import QtWidgets, QtGui +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from direct.task.TaskManagerGlobal import taskMgr + +from QMeta3D.QMeta3D_Buttons_Translation import QMeta3D_Button_translation +from QMeta3D.QMeta3D_Keys_Translation import QMeta3D_Key_translation +from QMeta3D.QMeta3D_Modifiers_Translation import QMeta3D_Modifier_translation + +__all__ = ["QMeta3DWidget"] + + +class QMeta3DSynchronizer(QTimer): + def __init__(self, qMeta3DWidget, FPS=60): + QTimer.__init__(self) + self.qMeta3DWidget = qMeta3DWidget + dt = 1000 / FPS + self.setInterval(int(dt)) + self.timeout.connect(self.tick) + + def tick(self): + try: + # 在渲染前清理可能损坏的TransformState对象 + from panda3d.core import TransformState + TransformState.clear_cache() + + taskMgr.step() + self.qMeta3DWidget.update() + except AssertionError as e: + # 专门处理 TransformState has_mat() 断言错误 + if "has_mat" in str(e): + print(f"警告: 检测到TransformState断言错误,已静默处理: {e}") + # 尝试恢复渲染状态 + try: + # 强制清理缓存并重试 + from panda3d.core import TransformState, RenderState + TransformState.clear_cache() + RenderState.clear_cache() + taskMgr.step() + self.qMeta3DWidget.update() + except: + pass + else: + # 重新抛出其他断言错误 + raise + except Exception as e: + # 静默处理其他所有异常 + print(f"警告: 检测到异常,已静默处理: {e}") + + +def get_panda_key_modifiers(evt): + panda_mods = [] + qt_mods = evt.modifiers() + for qt_mod, panda_mod in QMeta3D_Modifier_translation.items(): + if (qt_mods & qt_mod) == qt_mod: + panda_mods.append(panda_mod) + return panda_mods + + +def get_panda_key_modifiers_prefix(evt): + # join all modifiers (except NoModifier, which is None) with '-' + prefix = "-".join([mod for mod in get_panda_key_modifiers(evt) if mod is not None]) + + # if the prefix is not empty, append a '-' + if prefix: + prefix += '-' + + return prefix + + +class QMeta3DWidget(QWidget): + def __init__(self, Meta3DWorld, parent=None, FPS=60, debug=False): + QWidget.__init__(self, parent) + self.rp_sync_requested = False + # set fixed geometry + self.Meta3DWorld = Meta3DWorld + self.Meta3DWorld.set_parent(self) + # Setup a timer in Qt that runs taskMgr.step() to simulate Panda's own main loop + # pandaTimer = QTimer(self) + # pandaTimer.timeout.connect() + # pandaTimer.start(0) + + self.setFocusPolicy(Qt.StrongFocus) + + # Setup another timer that redraws this widget in a specific FPS + # redrawTimer = QTimer(self) + # redrawTimer.timeout.connect(self.update) + # redrawTimer.start(1000/FPS) + + self.paintSurface = QPainter() + self.rotate = QTransform() + self.rotate.rotate(180) + self.out_image = QImage() + + size = self.Meta3DWorld.cam.node().get_lens().get_film_size() + self.initial_film_size = QSizeF(size.x, size.y) + self.initial_size = self.size() + + self.synchronizer = QMeta3DSynchronizer(self, FPS) + self.synchronizer.start() + + self.debug = debug + + def mousePressEvent(self, evt): + button = evt.button() + try: + b = "{}{}".format(get_panda_key_modifiers_prefix(evt), QMeta3D_Button_translation[button]) + if self.debug: + print(b) + messenger.send(b,[{"x":evt.x(),"y":evt.y()}]) + except: + print("Unimplemented button. Please send an issue on github to fix this problem") + + def mouseMoveEvent(self, evt:QtGui.QMouseEvent): + button = evt.button() + try: + b = "mouse-move" + if self.debug: + print(b) + messenger.send(b,[{"x":evt.x(),"y":evt.y()}]) + except: + print("Unimplemented button. Please send an issue on github to fix this problem") + + + def mouseReleaseEvent(self, evt): + button = evt.button() + try: + b = "{}{}-up".format(get_panda_key_modifiers_prefix(evt), QMeta3D_Button_translation[button]) + if self.debug: + print(b) + messenger.send(b,[{"x":evt.x(),"y":evt.y()}]) + except: + print("Unimplemented button. Please send an issue on github to fix this problem") + + def wheelEvent(self, evt): + delta = evt.angleDelta().y() + try: + if self.debug: + print(f"wheel {delta}") + messenger.send('wheel',[{"delta":delta}]) + except: + print("Unimplemented button. Please send an issue on github to fix this problem") + + def keyPressEvent(self, evt): + key = evt.key() + try: + k = "{}{}".format(get_panda_key_modifiers_prefix(evt), QMeta3D_Key_translation[key]) + if self.debug: + print(k) + messenger.send(k) + except: + print("Unimplemented key. Please send an issue on github to fix this problem") + + def keyReleaseEvent(self, evt): + key = evt.key() + try: + k = "{}{}-up".format(get_panda_key_modifiers_prefix(evt), QMeta3D_Key_translation[key]) + if self.debug: + print(k) + messenger.send(k) + except: + print("Unimplemented key. Please send an issue on github to fix this problem") + + def resizeEvent(self, evt): + width = evt.size().width() + height = evt.size().height() + #print(f"width:{width}") + #print(f"height:{height}") + + from Meta3DWorld import resize_buffer + + lens = self.Meta3DWorld.cam.node().get_lens() + + def minimumSizeHint(self): + return QSize(400, 300) + + def paintEvent(self, event): + tex = self.Meta3DWorld.qt_output_tex + gsg = base.win.getGsg() + + if not gsg: + self.update() + return + + if not tex.hasRamImage(): + base.graphicsEngine.extractTextureData(tex, gsg) + self.update() + return + + data = tex.getRamImage().getData() + tex_width = tex.getXSize() + tex_height = tex.getYSize() + expected_len = tex_width * tex_height * 4 + + if len(data) != expected_len: + print(f"⚠️ 像素数据长度异常({len(data)} != {expected_len}),跳过绘制") + self.update() + return + + img = QImage(data, tex_width, tex_height, QImage.Format_ARGB32).mirrored() + + widget_width = self.width() + widget_height = self.height() + + painter = QPainter(self) + + # 【保持宽高比的缩放】 + scaled_img = img.scaled(widget_width, widget_height, Qt.KeepAspectRatio, Qt.SmoothTransformation) + + # 居中绘制 + x_offset = (widget_width - scaled_img.width()) // 2 + y_offset = (widget_height - scaled_img.height()) // 2 + + painter.drawImage(x_offset, y_offset, scaled_img) + painter.end() + + def sync_Meta3d_window_size(self, width, height): + try: + from panda3d.core import WindowProperties, LVecBase2i + + # 确保尺寸是4的倍数(RenderPipeline 要求) + adjusted_width = width - width % 4 + adjusted_height = height - height % 4 + + # 更新窗口属性 + props = WindowProperties() + props.setSize(adjusted_width, adjusted_height) + + # 对于 offscreen 渲染,直接更新窗口属性 + if self.Meta3DWorld.win: + self.Meta3DWorld.win.request_properties(props) + + # 手动触发 RenderPipeline 的尺寸更新逻辑 + if hasattr(self.Meta3DWorld, 'render_pipeline'): + # 更新全局分辨率 + from RenderPipelineFile.rpcore.globals import Globals + Globals.native_resolution = LVecBase2i(adjusted_width, adjusted_height) + + # 重新计算渲染分辨率 + self.Meta3DWorld.render_pipeline._compute_render_resolution() + + # 通知各个管理器处理尺寸变化 + self.Meta3DWorld.render_pipeline.light_mgr.compute_tile_size() + self.Meta3DWorld.render_pipeline.stage_mgr.handle_window_resize() + if hasattr(self.Meta3DWorld.render_pipeline, 'debugger'): + self.Meta3DWorld.render_pipeline.debugger.handle_window_resize() + + # 触发插件的窗口尺寸变化钩子 + self.Meta3DWorld.render_pipeline.plugin_mgr.trigger_hook("window_resized") + except Exception as e: print(f"同步 Meta3D 窗口尺寸失败: {str(e)}") \ No newline at end of file diff --git a/QMeta3D/QMeta3D_Buttons_Translation.py b/QMeta3D/QMeta3D_Buttons_Translation.py index 4a69d0d6..3ee09486 100644 --- a/QMeta3D/QMeta3D_Buttons_Translation.py +++ b/QMeta3D/QMeta3D_Buttons_Translation.py @@ -1,43 +1,43 @@ -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -import sys -__all__ = ["QMeta3D_Keys_Translation.py"] - -QMeta3D_Button_translation ={ -Qt.NoButton:'', -Qt.AllButtons:'unknown', -Qt.LeftButton:'mouse1', -Qt.RightButton:'mouse2', -Qt.MidButton:'mouse3', -Qt.MiddleButton:'mouse3', -Qt.BackButton:'unknown', -Qt.XButton1:'unknown', -Qt.ExtraButton1:'unknown', -Qt.ForwardButton:'unknown', -Qt.XButton2:'unknown', -Qt.ExtraButton2:'unknown', -Qt.TaskButton:'unknown', -Qt.ExtraButton3:'unknown', -Qt.ExtraButton4:'unknown', -Qt.ExtraButton5:'unknown', -Qt.ExtraButton6:'unknown', -Qt.ExtraButton7:'unknown', -Qt.ExtraButton8:'unknown', -Qt.ExtraButton9:'unknown', -Qt.ExtraButton10:'unknown', -Qt.ExtraButton11:'unknown', -Qt.ExtraButton12:'unknown', -Qt.ExtraButton13:'unknown', -Qt.ExtraButton14:'unknown', -Qt.ExtraButton15:'unknown', -Qt.ExtraButton16:'unknown', -Qt.ExtraButton17:'unknown', -Qt.ExtraButton18:'unknown', -Qt.ExtraButton19:'unknown', -Qt.ExtraButton20:'unknown', -Qt.ExtraButton21:'unknown', -Qt.ExtraButton22:'unknown', -Qt.ExtraButton23:'unknown', -Qt.ExtraButton24:'unknown', +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +import sys +__all__ = ["QMeta3D_Keys_Translation.py"] + +QMeta3D_Button_translation ={ +Qt.NoButton:'', +Qt.AllButtons:'unknown', +Qt.LeftButton:'mouse1', +Qt.RightButton:'mouse2', +Qt.MidButton:'mouse3', +Qt.MiddleButton:'mouse3', +Qt.BackButton:'unknown', +Qt.XButton1:'unknown', +Qt.ExtraButton1:'unknown', +Qt.ForwardButton:'unknown', +Qt.XButton2:'unknown', +Qt.ExtraButton2:'unknown', +Qt.TaskButton:'unknown', +Qt.ExtraButton3:'unknown', +Qt.ExtraButton4:'unknown', +Qt.ExtraButton5:'unknown', +Qt.ExtraButton6:'unknown', +Qt.ExtraButton7:'unknown', +Qt.ExtraButton8:'unknown', +Qt.ExtraButton9:'unknown', +Qt.ExtraButton10:'unknown', +Qt.ExtraButton11:'unknown', +Qt.ExtraButton12:'unknown', +Qt.ExtraButton13:'unknown', +Qt.ExtraButton14:'unknown', +Qt.ExtraButton15:'unknown', +Qt.ExtraButton16:'unknown', +Qt.ExtraButton17:'unknown', +Qt.ExtraButton18:'unknown', +Qt.ExtraButton19:'unknown', +Qt.ExtraButton20:'unknown', +Qt.ExtraButton21:'unknown', +Qt.ExtraButton22:'unknown', +Qt.ExtraButton23:'unknown', +Qt.ExtraButton24:'unknown', } \ No newline at end of file diff --git a/QMeta3D/QMeta3D_Keys_Translation.py b/QMeta3D/QMeta3D_Keys_Translation.py index 0b0c8dda..24d29e79 100644 --- a/QMeta3D/QMeta3D_Keys_Translation.py +++ b/QMeta3D/QMeta3D_Keys_Translation.py @@ -1,477 +1,477 @@ -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -import sys -__all__ = ["QMeta3D_Keys_Translation.py"] - -QMeta3D_Key_translation ={ -Qt.Key_0:'0', -Qt.Key_1:'1', -Qt.Key_2:'2', -Qt.Key_3:'3', -Qt.Key_4:'4', -Qt.Key_5:'5', -Qt.Key_6:'6', -Qt.Key_7:'7', -Qt.Key_8:'8', -Qt.Key_9:'9', -Qt.Key_A:'a', -Qt.Key_AE:'ae', -Qt.Key_Aacute:'unknown', -Qt.Key_Acircumflex:'unknown', -Qt.Key_AddFavorite:'unknown', -Qt.Key_Adiaeresis:'unknown', -Qt.Key_Agrave:'unknown', -Qt.Key_Alt:'lalt', -Qt.Key_AltGr:'unknown', -Qt.Key_Ampersand:'unknown', -Qt.Key_Any:'unknown', -Qt.Key_Apostrophe:'unknown', -Qt.Key_ApplicationLeft:'unknown', -Qt.Key_ApplicationRight:'unknown', -Qt.Key_Aring:'unknown', -Qt.Key_AsciiCircum:'unknown', -Qt.Key_AsciiTilde:'unknown', -Qt.Key_Asterisk:'unknown', -Qt.Key_At:'unknown', -Qt.Key_Atilde:'unknown', -Qt.Key_AudioCycleTrack:'unknown', -Qt.Key_AudioForward:'unknown', -Qt.Key_AudioRandomPlay:'unknown', -Qt.Key_AudioRepeat:'unknown', -Qt.Key_AudioRewind:'unknown', -Qt.Key_Away:'unknown', -Qt.Key_B:'b', -Qt.Key_Back:'unknown', -Qt.Key_BackForward:'unknown', -Qt.Key_Backslash:'unknown', -Qt.Key_Backspace:'unknown', -Qt.Key_Backtab:'unknown', -Qt.Key_Bar:'unknown', -Qt.Key_BassBoost:'unknown', -Qt.Key_BassDown:'unknown', -Qt.Key_BassUp:'unknown', -Qt.Key_Battery:'unknown', -Qt.Key_Blue:'unknown', -Qt.Key_Bluetooth:'unknown', -Qt.Key_Book:'unknown', -Qt.Key_BraceLeft:'unknown', -Qt.Key_BraceRight:'unknown', -Qt.Key_BracketLeft:'unknown', -Qt.Key_BracketRight:'unknown', -Qt.Key_BrightnessAdjust:'unknown', -Qt.Key_C:'c', -Qt.Key_CD:'unknown', -Qt.Key_Calculator:'unknown', -Qt.Key_Calendar:'unknown', -Qt.Key_Call:'unknown', -Qt.Key_Camera:'unknown', -Qt.Key_CameraFocus:'unknown', -Qt.Key_Cancel:'unknown', -Qt.Key_CapsLock:'unknown', -Qt.Key_Ccedilla:'unknown', -Qt.Key_ChannelDown:'unknown', -Qt.Key_ChannelUp:'unknown', -Qt.Key_Clear:'unknown', -Qt.Key_ClearGrab:'unknown', -Qt.Key_Close:'unknown', -Qt.Key_Codeinput:'unknown', -Qt.Key_Colon:'unknown', -Qt.Key_Comma:'unknown', -Qt.Key_Community:'unknown', -Qt.Key_Context1:'unknown', -Qt.Key_Context2:'unknown', -Qt.Key_Context3:'unknown', -Qt.Key_Context4:'unknown', -Qt.Key_ContrastAdjust:'unknown', -Qt.Key_Control:'control', -Qt.Key_Copy:'unknown', -Qt.Key_Cut:'unknown', -Qt.Key_D:'d', -Qt.Key_DOS:'unknown', -Qt.Key_Dead_A:'unknown', -Qt.Key_Dead_Abovecomma:'unknown', -Qt.Key_Dead_Abovedot:'unknown', -Qt.Key_Dead_Abovereversedcomma:'unknown', -Qt.Key_Dead_Abovering:'unknown', -Qt.Key_Dead_Aboveverticalline:'unknown', -Qt.Key_Dead_Acute:'unknown', -Qt.Key_Dead_Belowbreve:'unknown', -Qt.Key_Dead_Belowcircumflex:'unknown', -Qt.Key_Dead_Belowcomma:'unknown', -Qt.Key_Dead_Belowdiaeresis:'unknown', -Qt.Key_Dead_Belowdot:'unknown', -Qt.Key_Dead_Belowmacron:'unknown', -Qt.Key_Dead_Belowring:'unknown', -Qt.Key_Dead_Belowtilde:'unknown', -Qt.Key_Dead_Belowverticalline:'unknown', -Qt.Key_Dead_Breve:'unknown', -Qt.Key_Dead_Capital_Schwa:'unknown', -Qt.Key_Dead_Caron:'unknown', -Qt.Key_Dead_Cedilla:'unknown', -Qt.Key_Dead_Circumflex:'unknown', -Qt.Key_Dead_Currency:'unknown', -Qt.Key_Dead_Diaeresis:'unknown', -Qt.Key_Dead_Doubleacute:'unknown', -Qt.Key_Dead_Doublegrave:'unknown', -Qt.Key_Dead_E:'unknown', -Qt.Key_Dead_Grave:'unknown', -Qt.Key_Dead_Greek:'unknown', -Qt.Key_Dead_Hook:'unknown', -Qt.Key_Dead_Horn:'unknown', -Qt.Key_Dead_I:'unknown', -Qt.Key_Dead_Invertedbreve:'unknown', -Qt.Key_Dead_Iota:'unknown', -Qt.Key_Dead_Longsolidusoverlay:'unknown', -Qt.Key_Dead_Lowline:'unknown', -Qt.Key_Dead_Macron:'unknown', -Qt.Key_Dead_O:'unknown', -Qt.Key_Dead_Ogonek:'unknown', -Qt.Key_Dead_Semivoiced_Sound:'unknown', -Qt.Key_Dead_Small_Schwa:'unknown', -Qt.Key_Dead_Stroke:'unknown', -Qt.Key_Dead_Tilde:'unknown', -Qt.Key_Dead_U:'unknown', -Qt.Key_Dead_Voiced_Sound:'unknown', -Qt.Key_Dead_a:'unknown', -Qt.Key_Dead_e:'unknown', -Qt.Key_Dead_i:'unknown', -Qt.Key_Dead_o:'unknown', -Qt.Key_Dead_u:'unknown', -Qt.Key_Delete:'delete', -Qt.Key_Direction_L:'unknown', -Qt.Key_Direction_R:'unknown', -Qt.Key_Display:'unknown', -Qt.Key_Documents:'unknown', -Qt.Key_Dollar:'unknown', -Qt.Key_Down:'arrow_down', -Qt.Key_E:'e', -Qt.Key_ETH:'unknown', -Qt.Key_Eacute:'unknown', -Qt.Key_Ecircumflex:'unknown', -Qt.Key_Ediaeresis:'unknown', -Qt.Key_Egrave:'unknown', -Qt.Key_Eisu_Shift:'unknown', -Qt.Key_Eisu_toggle:'unknown', -Qt.Key_Eject:'unknown', -Qt.Key_End:'unknown', -Qt.Key_Enter:'unknown', -Qt.Key_Equal:'unknown', -Qt.Key_Escape:'escape', -Qt.Key_Excel:'unknown', -Qt.Key_Exclam:'unknown', -Qt.Key_Execute:'unknown', -Qt.Key_Exit:'unknown', -Qt.Key_Explorer:'unknown', -Qt.Key_F:'f', -Qt.Key_F1:'f1', -Qt.Key_F10:'f10', -Qt.Key_F11:'f11', -Qt.Key_F12:'f12', -Qt.Key_F13:'f13', -Qt.Key_F14:'f14', -Qt.Key_F15:'f15', -Qt.Key_F16:'f16', -Qt.Key_F17:'f17', -Qt.Key_F18:'f18', -Qt.Key_F19:'unknown', -Qt.Key_F2:'unknown', -Qt.Key_F20:'unknown', -Qt.Key_F21:'unknown', -Qt.Key_F22:'unknown', -Qt.Key_F23:'unknown', -Qt.Key_F24:'unknown', -Qt.Key_F25:'unknown', -Qt.Key_F26:'unknown', -Qt.Key_F27:'unknown', -Qt.Key_F28:'unknown', -Qt.Key_F29:'unknown', -Qt.Key_F3:'f3', -Qt.Key_F30:'unknown', -Qt.Key_F31:'unknown', -Qt.Key_F32:'unknown', -Qt.Key_F33:'unknown', -Qt.Key_F34:'unknown', -Qt.Key_F35:'unknown', -Qt.Key_F4:'f4', -Qt.Key_F5:'f5', -Qt.Key_F6:'f6', -Qt.Key_F7:'f7', -Qt.Key_F8:'f8', -Qt.Key_F9:'f9', -Qt.Key_Favorites:'unknown', -Qt.Key_Finance:'unknown', -Qt.Key_Find:'unknown', -Qt.Key_Flip:'unknown', -Qt.Key_Forward:'unknown', -Qt.Key_G:'g', -Qt.Key_Game:'unknown', -Qt.Key_Go:'unknown', -Qt.Key_Greater:'unknown', -Qt.Key_Green:'unknown', -Qt.Key_Guide:'unknown', -Qt.Key_H:'h', -Qt.Key_Hangul:'unknown', -Qt.Key_Hangul_Banja:'unknown', -Qt.Key_Hangul_End:'unknown', -Qt.Key_Hangul_Hanja:'unknown', -Qt.Key_Hangul_Jamo:'unknown', -Qt.Key_Hangul_Jeonja:'unknown', -Qt.Key_Hangul_PostHanja:'unknown', -Qt.Key_Hangul_PreHanja:'unknown', -Qt.Key_Hangul_Romaja:'unknown', -Qt.Key_Hangul_Special:'unknown', -Qt.Key_Hangul_Start:'unknown', -Qt.Key_Hangup:'unknown', -Qt.Key_Hankaku:'unknown', -Qt.Key_Help:'unknown', -Qt.Key_Henkan:'unknown', -Qt.Key_Hibernate:'unknown', -Qt.Key_Hiragana:'unknown', -Qt.Key_Hiragana_Katakana:'unknown', -Qt.Key_History:'unknown', -Qt.Key_Home:'home', -Qt.Key_HomePage:'unknown', -Qt.Key_HotLinks:'unknown', -Qt.Key_Hyper_L:'unknown', -Qt.Key_Hyper_R:'unknown', -Qt.Key_I:'i', -Qt.Key_Iacute:'unknown', -Qt.Key_Icircumflex:'unknown', -Qt.Key_Idiaeresis:'unknown', -Qt.Key_Igrave:'unknown', -Qt.Key_Info:'unknown', -Qt.Key_Insert:'unknown', -Qt.Key_J:'j', -Qt.Key_K:'k', -Qt.Key_Kana_Lock:'unknown', -Qt.Key_Kana_Shift:'unknown', -Qt.Key_Kanji:'unknown', -Qt.Key_Katakana:'unknown', -Qt.Key_KeyboardBrightnessDown:'unknown', -Qt.Key_KeyboardBrightnessUp:'unknown', -Qt.Key_KeyboardLightOnOff:'unknown', -Qt.Key_L:'l', -Qt.Key_LastNumberRedial:'unknown', -Qt.Key_Launch0:'unknown', -Qt.Key_Launch1:'unknown', -Qt.Key_Launch2:'unknown', -Qt.Key_Launch3:'unknown', -Qt.Key_Launch4:'unknown', -Qt.Key_Launch5:'unknown', -Qt.Key_Launch6:'unknown', -Qt.Key_Launch7:'unknown', -Qt.Key_Launch8:'unknown', -Qt.Key_Launch9:'unknown', -Qt.Key_LaunchA:'unknown', -Qt.Key_LaunchB:'unknown', -Qt.Key_LaunchC:'unknown', -Qt.Key_LaunchD:'unknown', -Qt.Key_LaunchE:'unknown', -Qt.Key_LaunchF:'unknown', -Qt.Key_LaunchG:'unknown', -Qt.Key_LaunchH:'unknown', -Qt.Key_LaunchMail:'unknown', -Qt.Key_LaunchMedia:'unknown', -Qt.Key_Left:'arrow_left', -Qt.Key_Less:'unknown', -Qt.Key_LightBulb:'unknown', -Qt.Key_LogOff:'unknown', -Qt.Key_M:'m', -Qt.Key_MailForward:'unknown', -Qt.Key_Market:'unknown', -Qt.Key_Massyo:'unknown', -Qt.Key_MediaLast:'unknown', -Qt.Key_MediaNext:'unknown', -Qt.Key_MediaPause:'unknown', -Qt.Key_MediaPlay:'unknown', -Qt.Key_MediaPrevious:'unknown', -Qt.Key_MediaRecord:'unknown', -Qt.Key_MediaStop:'unknown', -Qt.Key_MediaTogglePlayPause:'unknown', -Qt.Key_Meeting:'unknown', -Qt.Key_Memo:'unknown', -Qt.Key_Menu:'unknown', -Qt.Key_MenuKB:'unknown', -Qt.Key_MenuPB:'unknown', -Qt.Key_Messenger:'unknown', -Qt.Key_Meta:'unknown', -Qt.Key_MicMute:'unknown', -Qt.Key_MicVolumeDown:'unknown', -Qt.Key_MicVolumeUp:'unknown', -Qt.Key_Minus:'unknown', -Qt.Key_Mode_switch:'unknown', -Qt.Key_MonBrightnessDown:'unknown', -Qt.Key_MonBrightnessUp:'unknown', -Qt.Key_Muhenkan:'unknown', -Qt.Key_Multi_key:'unknown', -Qt.Key_MultipleCandidate:'unknown', -Qt.Key_Music:'unknown', -Qt.Key_MySites:'unknown', -Qt.Key_N:'n', -Qt.Key_New:'unknown', -Qt.Key_News:'unknown', -Qt.Key_No:'unknown', -Qt.Key_Ntilde:'unknown', -Qt.Key_NumLock:'unknown', -Qt.Key_NumberSign:'unknown', -Qt.Key_O:'o', -Qt.Key_Oacute:'unknown', -Qt.Key_Ocircumflex:'unknown', -Qt.Key_Odiaeresis:'unknown', -Qt.Key_OfficeHome:'unknown', -Qt.Key_Ograve:'unknown', -Qt.Key_Ooblique:'unknown', -Qt.Key_Open:'unknown', -Qt.Key_OpenUrl:'unknown', -Qt.Key_Option:'unknown', -Qt.Key_Otilde:'unknown', -Qt.Key_P:'p', -Qt.Key_PageDown:'unknown', -Qt.Key_PageUp:'unknown', -Qt.Key_ParenLeft:'unknown', -Qt.Key_ParenRight:'unknown', -Qt.Key_Paste:'unknown', -Qt.Key_Pause:'unknown', -Qt.Key_Percent:'unknown', -Qt.Key_Period:'unknown', -Qt.Key_Phone:'unknown', -Qt.Key_Pictures:'unknown', -Qt.Key_Play:'unknown', -Qt.Key_Plus:'unknown', -Qt.Key_PowerDown:'unknown', -Qt.Key_PowerOff:'unknown', -Qt.Key_PreviousCandidate:'unknown', -Qt.Key_Print:'unknown', -Qt.Key_Printer:'unknown', -Qt.Key_Q:'q', -Qt.Key_Question:'unknown', -Qt.Key_QuoteDbl:'unknown', -Qt.Key_QuoteLeft:'unknown', -Qt.Key_R:'r', -Qt.Key_Red:'unknown', -Qt.Key_Redo:'unknown', -Qt.Key_Refresh:'unknown', -Qt.Key_Reload:'unknown', -Qt.Key_Reply:'unknown', -Qt.Key_Return:'unknown', -Qt.Key_Right:'arrow_right', -Qt.Key_Romaji:'unknown', -Qt.Key_RotateWindows:'unknown', -Qt.Key_RotationKB:'unknown', -Qt.Key_RotationPB:'unknown', -Qt.Key_S:'s', -Qt.Key_Save:'unknown', -Qt.Key_ScreenSaver:'unknown', -Qt.Key_ScrollLock:'unknown', -Qt.Key_Search:'unknown', -Qt.Key_Select:'unknown', -Qt.Key_Semicolon:'unknown', -Qt.Key_Send:'unknown', -Qt.Key_Settings:'unknown', -Qt.Key_Shift:'unknown', -Qt.Key_Shop:'unknown', -Qt.Key_SingleCandidate:'unknown', -Qt.Key_Slash:'unknown', -Qt.Key_Sleep:'unknown', -Qt.Key_Space:'space', -Qt.Key_Spell:'unknown', -Qt.Key_SplitScreen:'unknown', -Qt.Key_Standby:'unknown', -Qt.Key_Stop:'unknown', -Qt.Key_Subtitle:'unknown', -Qt.Key_Super_L:'unknown', -Qt.Key_Super_R:'unknown', -Qt.Key_Support:'unknown', -Qt.Key_Suspend:'unknown', -Qt.Key_SysReq:'unknown', -Qt.Key_T:'t', -Qt.Key_THORN:'unknown', -Qt.Key_Tab:'unknown', -Qt.Key_TaskPane:'unknown', -Qt.Key_Terminal:'unknown', -Qt.Key_Time:'unknown', -Qt.Key_ToDoList:'unknown', -Qt.Key_ToggleCallHangup:'unknown', -Qt.Key_Tools:'unknown', -Qt.Key_TopMenu:'unknown', -Qt.Key_TouchpadOff:'unknown', -Qt.Key_TouchpadOn:'unknown', -Qt.Key_TouchpadToggle:'unknown', -Qt.Key_Touroku:'unknown', -Qt.Key_Travel:'unknown', -Qt.Key_TrebleDown:'unknown', -Qt.Key_TrebleUp:'unknown', -Qt.Key_U:'u', -Qt.Key_UWB:'unknown', -Qt.Key_Uacute:'unknown', -Qt.Key_Ucircumflex:'unknown', -Qt.Key_Udiaeresis:'unknown', -Qt.Key_Ugrave:'unknown', -Qt.Key_Underscore:'unknown', -Qt.Key_Undo:'unknown', -Qt.Key_Up:'arrow_up', -Qt.Key_V:'v', -Qt.Key_Video:'unknown', -Qt.Key_View:'unknown', -Qt.Key_VoiceDial:'unknown', -Qt.Key_VolumeDown:'unknown', -Qt.Key_VolumeMute:'unknown', -Qt.Key_VolumeUp:'unknown', -Qt.Key_W:'w', -Qt.Key_WLAN:'unknown', -Qt.Key_WWW:'unknown', -Qt.Key_WakeUp:'unknown', -Qt.Key_WebCam:'unknown', -Qt.Key_Word:'unknown', -Qt.Key_X:'x', -Qt.Key_Xfer:'unknown', -Qt.Key_Y:'y', -Qt.Key_Yacute:'unknown', -Qt.Key_Yellow:'unknown', -Qt.Key_Yes:'unknown', -Qt.Key_Z:'z', -Qt.Key_Zenkaku:'unknown', -Qt.Key_Zenkaku_Hankaku:'unknown', -Qt.Key_Zoom:'unknown', -Qt.Key_ZoomIn:'unknown', -Qt.Key_ZoomOut:'unknown', -Qt.Key_acute:'unknown', -Qt.Key_brokenbar:'unknown', -Qt.Key_cedilla:'unknown', -Qt.Key_cent:'unknown', -Qt.Key_copyright:'unknown', -Qt.Key_currency:'unknown', -Qt.Key_degree:'unknown', -Qt.Key_diaeresis:'unknown', -Qt.Key_division:'unknown', -Qt.Key_exclamdown:'unknown', -Qt.Key_guillemotleft:'unknown', -Qt.Key_guillemotright:'unknown', -Qt.Key_hyphen:'unknown', -Qt.Key_iTouch:'unknown', -Qt.Key_macron:'unknown', -Qt.Key_masculine:'unknown', -Qt.Key_mu:'unknown', -Qt.Key_multiply:'unknown', -Qt.Key_nobreakspace:'unknown', -Qt.Key_notsign:'unknown', -Qt.Key_onehalf:'unknown', -Qt.Key_onequarter:'unknown', -Qt.Key_onesuperior:'unknown', -Qt.Key_ordfeminine:'unknown', -Qt.Key_paragraph:'unknown', -Qt.Key_periodcentered:'unknown', -Qt.Key_plusminus:'unknown', -Qt.Key_questiondown:'unknown', -Qt.Key_registered:'unknown', -Qt.Key_section:'unknown', -Qt.Key_ssharp:'unknown', -Qt.Key_sterling:'unknown', -Qt.Key_threequarters:'unknown', -Qt.Key_threesuperior:'unknown', -Qt.Key_twosuperior:'unknown', -Qt.Key_unknown:'unknown', -Qt.Key_ydiaeresis:'unknown', -Qt.Key_yen:'unknown', +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +import sys +__all__ = ["QMeta3D_Keys_Translation.py"] + +QMeta3D_Key_translation ={ +Qt.Key_0:'0', +Qt.Key_1:'1', +Qt.Key_2:'2', +Qt.Key_3:'3', +Qt.Key_4:'4', +Qt.Key_5:'5', +Qt.Key_6:'6', +Qt.Key_7:'7', +Qt.Key_8:'8', +Qt.Key_9:'9', +Qt.Key_A:'a', +Qt.Key_AE:'ae', +Qt.Key_Aacute:'unknown', +Qt.Key_Acircumflex:'unknown', +Qt.Key_AddFavorite:'unknown', +Qt.Key_Adiaeresis:'unknown', +Qt.Key_Agrave:'unknown', +Qt.Key_Alt:'lalt', +Qt.Key_AltGr:'unknown', +Qt.Key_Ampersand:'unknown', +Qt.Key_Any:'unknown', +Qt.Key_Apostrophe:'unknown', +Qt.Key_ApplicationLeft:'unknown', +Qt.Key_ApplicationRight:'unknown', +Qt.Key_Aring:'unknown', +Qt.Key_AsciiCircum:'unknown', +Qt.Key_AsciiTilde:'unknown', +Qt.Key_Asterisk:'unknown', +Qt.Key_At:'unknown', +Qt.Key_Atilde:'unknown', +Qt.Key_AudioCycleTrack:'unknown', +Qt.Key_AudioForward:'unknown', +Qt.Key_AudioRandomPlay:'unknown', +Qt.Key_AudioRepeat:'unknown', +Qt.Key_AudioRewind:'unknown', +Qt.Key_Away:'unknown', +Qt.Key_B:'b', +Qt.Key_Back:'unknown', +Qt.Key_BackForward:'unknown', +Qt.Key_Backslash:'unknown', +Qt.Key_Backspace:'unknown', +Qt.Key_Backtab:'unknown', +Qt.Key_Bar:'unknown', +Qt.Key_BassBoost:'unknown', +Qt.Key_BassDown:'unknown', +Qt.Key_BassUp:'unknown', +Qt.Key_Battery:'unknown', +Qt.Key_Blue:'unknown', +Qt.Key_Bluetooth:'unknown', +Qt.Key_Book:'unknown', +Qt.Key_BraceLeft:'unknown', +Qt.Key_BraceRight:'unknown', +Qt.Key_BracketLeft:'unknown', +Qt.Key_BracketRight:'unknown', +Qt.Key_BrightnessAdjust:'unknown', +Qt.Key_C:'c', +Qt.Key_CD:'unknown', +Qt.Key_Calculator:'unknown', +Qt.Key_Calendar:'unknown', +Qt.Key_Call:'unknown', +Qt.Key_Camera:'unknown', +Qt.Key_CameraFocus:'unknown', +Qt.Key_Cancel:'unknown', +Qt.Key_CapsLock:'unknown', +Qt.Key_Ccedilla:'unknown', +Qt.Key_ChannelDown:'unknown', +Qt.Key_ChannelUp:'unknown', +Qt.Key_Clear:'unknown', +Qt.Key_ClearGrab:'unknown', +Qt.Key_Close:'unknown', +Qt.Key_Codeinput:'unknown', +Qt.Key_Colon:'unknown', +Qt.Key_Comma:'unknown', +Qt.Key_Community:'unknown', +Qt.Key_Context1:'unknown', +Qt.Key_Context2:'unknown', +Qt.Key_Context3:'unknown', +Qt.Key_Context4:'unknown', +Qt.Key_ContrastAdjust:'unknown', +Qt.Key_Control:'control', +Qt.Key_Copy:'unknown', +Qt.Key_Cut:'unknown', +Qt.Key_D:'d', +Qt.Key_DOS:'unknown', +Qt.Key_Dead_A:'unknown', +Qt.Key_Dead_Abovecomma:'unknown', +Qt.Key_Dead_Abovedot:'unknown', +Qt.Key_Dead_Abovereversedcomma:'unknown', +Qt.Key_Dead_Abovering:'unknown', +Qt.Key_Dead_Aboveverticalline:'unknown', +Qt.Key_Dead_Acute:'unknown', +Qt.Key_Dead_Belowbreve:'unknown', +Qt.Key_Dead_Belowcircumflex:'unknown', +Qt.Key_Dead_Belowcomma:'unknown', +Qt.Key_Dead_Belowdiaeresis:'unknown', +Qt.Key_Dead_Belowdot:'unknown', +Qt.Key_Dead_Belowmacron:'unknown', +Qt.Key_Dead_Belowring:'unknown', +Qt.Key_Dead_Belowtilde:'unknown', +Qt.Key_Dead_Belowverticalline:'unknown', +Qt.Key_Dead_Breve:'unknown', +Qt.Key_Dead_Capital_Schwa:'unknown', +Qt.Key_Dead_Caron:'unknown', +Qt.Key_Dead_Cedilla:'unknown', +Qt.Key_Dead_Circumflex:'unknown', +Qt.Key_Dead_Currency:'unknown', +Qt.Key_Dead_Diaeresis:'unknown', +Qt.Key_Dead_Doubleacute:'unknown', +Qt.Key_Dead_Doublegrave:'unknown', +Qt.Key_Dead_E:'unknown', +Qt.Key_Dead_Grave:'unknown', +Qt.Key_Dead_Greek:'unknown', +Qt.Key_Dead_Hook:'unknown', +Qt.Key_Dead_Horn:'unknown', +Qt.Key_Dead_I:'unknown', +Qt.Key_Dead_Invertedbreve:'unknown', +Qt.Key_Dead_Iota:'unknown', +Qt.Key_Dead_Longsolidusoverlay:'unknown', +Qt.Key_Dead_Lowline:'unknown', +Qt.Key_Dead_Macron:'unknown', +Qt.Key_Dead_O:'unknown', +Qt.Key_Dead_Ogonek:'unknown', +Qt.Key_Dead_Semivoiced_Sound:'unknown', +Qt.Key_Dead_Small_Schwa:'unknown', +Qt.Key_Dead_Stroke:'unknown', +Qt.Key_Dead_Tilde:'unknown', +Qt.Key_Dead_U:'unknown', +Qt.Key_Dead_Voiced_Sound:'unknown', +Qt.Key_Dead_a:'unknown', +Qt.Key_Dead_e:'unknown', +Qt.Key_Dead_i:'unknown', +Qt.Key_Dead_o:'unknown', +Qt.Key_Dead_u:'unknown', +Qt.Key_Delete:'delete', +Qt.Key_Direction_L:'unknown', +Qt.Key_Direction_R:'unknown', +Qt.Key_Display:'unknown', +Qt.Key_Documents:'unknown', +Qt.Key_Dollar:'unknown', +Qt.Key_Down:'arrow_down', +Qt.Key_E:'e', +Qt.Key_ETH:'unknown', +Qt.Key_Eacute:'unknown', +Qt.Key_Ecircumflex:'unknown', +Qt.Key_Ediaeresis:'unknown', +Qt.Key_Egrave:'unknown', +Qt.Key_Eisu_Shift:'unknown', +Qt.Key_Eisu_toggle:'unknown', +Qt.Key_Eject:'unknown', +Qt.Key_End:'unknown', +Qt.Key_Enter:'unknown', +Qt.Key_Equal:'unknown', +Qt.Key_Escape:'escape', +Qt.Key_Excel:'unknown', +Qt.Key_Exclam:'unknown', +Qt.Key_Execute:'unknown', +Qt.Key_Exit:'unknown', +Qt.Key_Explorer:'unknown', +Qt.Key_F:'f', +Qt.Key_F1:'f1', +Qt.Key_F10:'f10', +Qt.Key_F11:'f11', +Qt.Key_F12:'f12', +Qt.Key_F13:'f13', +Qt.Key_F14:'f14', +Qt.Key_F15:'f15', +Qt.Key_F16:'f16', +Qt.Key_F17:'f17', +Qt.Key_F18:'f18', +Qt.Key_F19:'unknown', +Qt.Key_F2:'unknown', +Qt.Key_F20:'unknown', +Qt.Key_F21:'unknown', +Qt.Key_F22:'unknown', +Qt.Key_F23:'unknown', +Qt.Key_F24:'unknown', +Qt.Key_F25:'unknown', +Qt.Key_F26:'unknown', +Qt.Key_F27:'unknown', +Qt.Key_F28:'unknown', +Qt.Key_F29:'unknown', +Qt.Key_F3:'f3', +Qt.Key_F30:'unknown', +Qt.Key_F31:'unknown', +Qt.Key_F32:'unknown', +Qt.Key_F33:'unknown', +Qt.Key_F34:'unknown', +Qt.Key_F35:'unknown', +Qt.Key_F4:'f4', +Qt.Key_F5:'f5', +Qt.Key_F6:'f6', +Qt.Key_F7:'f7', +Qt.Key_F8:'f8', +Qt.Key_F9:'f9', +Qt.Key_Favorites:'unknown', +Qt.Key_Finance:'unknown', +Qt.Key_Find:'unknown', +Qt.Key_Flip:'unknown', +Qt.Key_Forward:'unknown', +Qt.Key_G:'g', +Qt.Key_Game:'unknown', +Qt.Key_Go:'unknown', +Qt.Key_Greater:'unknown', +Qt.Key_Green:'unknown', +Qt.Key_Guide:'unknown', +Qt.Key_H:'h', +Qt.Key_Hangul:'unknown', +Qt.Key_Hangul_Banja:'unknown', +Qt.Key_Hangul_End:'unknown', +Qt.Key_Hangul_Hanja:'unknown', +Qt.Key_Hangul_Jamo:'unknown', +Qt.Key_Hangul_Jeonja:'unknown', +Qt.Key_Hangul_PostHanja:'unknown', +Qt.Key_Hangul_PreHanja:'unknown', +Qt.Key_Hangul_Romaja:'unknown', +Qt.Key_Hangul_Special:'unknown', +Qt.Key_Hangul_Start:'unknown', +Qt.Key_Hangup:'unknown', +Qt.Key_Hankaku:'unknown', +Qt.Key_Help:'unknown', +Qt.Key_Henkan:'unknown', +Qt.Key_Hibernate:'unknown', +Qt.Key_Hiragana:'unknown', +Qt.Key_Hiragana_Katakana:'unknown', +Qt.Key_History:'unknown', +Qt.Key_Home:'home', +Qt.Key_HomePage:'unknown', +Qt.Key_HotLinks:'unknown', +Qt.Key_Hyper_L:'unknown', +Qt.Key_Hyper_R:'unknown', +Qt.Key_I:'i', +Qt.Key_Iacute:'unknown', +Qt.Key_Icircumflex:'unknown', +Qt.Key_Idiaeresis:'unknown', +Qt.Key_Igrave:'unknown', +Qt.Key_Info:'unknown', +Qt.Key_Insert:'unknown', +Qt.Key_J:'j', +Qt.Key_K:'k', +Qt.Key_Kana_Lock:'unknown', +Qt.Key_Kana_Shift:'unknown', +Qt.Key_Kanji:'unknown', +Qt.Key_Katakana:'unknown', +Qt.Key_KeyboardBrightnessDown:'unknown', +Qt.Key_KeyboardBrightnessUp:'unknown', +Qt.Key_KeyboardLightOnOff:'unknown', +Qt.Key_L:'l', +Qt.Key_LastNumberRedial:'unknown', +Qt.Key_Launch0:'unknown', +Qt.Key_Launch1:'unknown', +Qt.Key_Launch2:'unknown', +Qt.Key_Launch3:'unknown', +Qt.Key_Launch4:'unknown', +Qt.Key_Launch5:'unknown', +Qt.Key_Launch6:'unknown', +Qt.Key_Launch7:'unknown', +Qt.Key_Launch8:'unknown', +Qt.Key_Launch9:'unknown', +Qt.Key_LaunchA:'unknown', +Qt.Key_LaunchB:'unknown', +Qt.Key_LaunchC:'unknown', +Qt.Key_LaunchD:'unknown', +Qt.Key_LaunchE:'unknown', +Qt.Key_LaunchF:'unknown', +Qt.Key_LaunchG:'unknown', +Qt.Key_LaunchH:'unknown', +Qt.Key_LaunchMail:'unknown', +Qt.Key_LaunchMedia:'unknown', +Qt.Key_Left:'arrow_left', +Qt.Key_Less:'unknown', +Qt.Key_LightBulb:'unknown', +Qt.Key_LogOff:'unknown', +Qt.Key_M:'m', +Qt.Key_MailForward:'unknown', +Qt.Key_Market:'unknown', +Qt.Key_Massyo:'unknown', +Qt.Key_MediaLast:'unknown', +Qt.Key_MediaNext:'unknown', +Qt.Key_MediaPause:'unknown', +Qt.Key_MediaPlay:'unknown', +Qt.Key_MediaPrevious:'unknown', +Qt.Key_MediaRecord:'unknown', +Qt.Key_MediaStop:'unknown', +Qt.Key_MediaTogglePlayPause:'unknown', +Qt.Key_Meeting:'unknown', +Qt.Key_Memo:'unknown', +Qt.Key_Menu:'unknown', +Qt.Key_MenuKB:'unknown', +Qt.Key_MenuPB:'unknown', +Qt.Key_Messenger:'unknown', +Qt.Key_Meta:'unknown', +Qt.Key_MicMute:'unknown', +Qt.Key_MicVolumeDown:'unknown', +Qt.Key_MicVolumeUp:'unknown', +Qt.Key_Minus:'unknown', +Qt.Key_Mode_switch:'unknown', +Qt.Key_MonBrightnessDown:'unknown', +Qt.Key_MonBrightnessUp:'unknown', +Qt.Key_Muhenkan:'unknown', +Qt.Key_Multi_key:'unknown', +Qt.Key_MultipleCandidate:'unknown', +Qt.Key_Music:'unknown', +Qt.Key_MySites:'unknown', +Qt.Key_N:'n', +Qt.Key_New:'unknown', +Qt.Key_News:'unknown', +Qt.Key_No:'unknown', +Qt.Key_Ntilde:'unknown', +Qt.Key_NumLock:'unknown', +Qt.Key_NumberSign:'unknown', +Qt.Key_O:'o', +Qt.Key_Oacute:'unknown', +Qt.Key_Ocircumflex:'unknown', +Qt.Key_Odiaeresis:'unknown', +Qt.Key_OfficeHome:'unknown', +Qt.Key_Ograve:'unknown', +Qt.Key_Ooblique:'unknown', +Qt.Key_Open:'unknown', +Qt.Key_OpenUrl:'unknown', +Qt.Key_Option:'unknown', +Qt.Key_Otilde:'unknown', +Qt.Key_P:'p', +Qt.Key_PageDown:'unknown', +Qt.Key_PageUp:'unknown', +Qt.Key_ParenLeft:'unknown', +Qt.Key_ParenRight:'unknown', +Qt.Key_Paste:'unknown', +Qt.Key_Pause:'unknown', +Qt.Key_Percent:'unknown', +Qt.Key_Period:'unknown', +Qt.Key_Phone:'unknown', +Qt.Key_Pictures:'unknown', +Qt.Key_Play:'unknown', +Qt.Key_Plus:'unknown', +Qt.Key_PowerDown:'unknown', +Qt.Key_PowerOff:'unknown', +Qt.Key_PreviousCandidate:'unknown', +Qt.Key_Print:'unknown', +Qt.Key_Printer:'unknown', +Qt.Key_Q:'q', +Qt.Key_Question:'unknown', +Qt.Key_QuoteDbl:'unknown', +Qt.Key_QuoteLeft:'unknown', +Qt.Key_R:'r', +Qt.Key_Red:'unknown', +Qt.Key_Redo:'unknown', +Qt.Key_Refresh:'unknown', +Qt.Key_Reload:'unknown', +Qt.Key_Reply:'unknown', +Qt.Key_Return:'unknown', +Qt.Key_Right:'arrow_right', +Qt.Key_Romaji:'unknown', +Qt.Key_RotateWindows:'unknown', +Qt.Key_RotationKB:'unknown', +Qt.Key_RotationPB:'unknown', +Qt.Key_S:'s', +Qt.Key_Save:'unknown', +Qt.Key_ScreenSaver:'unknown', +Qt.Key_ScrollLock:'unknown', +Qt.Key_Search:'unknown', +Qt.Key_Select:'unknown', +Qt.Key_Semicolon:'unknown', +Qt.Key_Send:'unknown', +Qt.Key_Settings:'unknown', +Qt.Key_Shift:'unknown', +Qt.Key_Shop:'unknown', +Qt.Key_SingleCandidate:'unknown', +Qt.Key_Slash:'unknown', +Qt.Key_Sleep:'unknown', +Qt.Key_Space:'space', +Qt.Key_Spell:'unknown', +Qt.Key_SplitScreen:'unknown', +Qt.Key_Standby:'unknown', +Qt.Key_Stop:'unknown', +Qt.Key_Subtitle:'unknown', +Qt.Key_Super_L:'unknown', +Qt.Key_Super_R:'unknown', +Qt.Key_Support:'unknown', +Qt.Key_Suspend:'unknown', +Qt.Key_SysReq:'unknown', +Qt.Key_T:'t', +Qt.Key_THORN:'unknown', +Qt.Key_Tab:'unknown', +Qt.Key_TaskPane:'unknown', +Qt.Key_Terminal:'unknown', +Qt.Key_Time:'unknown', +Qt.Key_ToDoList:'unknown', +Qt.Key_ToggleCallHangup:'unknown', +Qt.Key_Tools:'unknown', +Qt.Key_TopMenu:'unknown', +Qt.Key_TouchpadOff:'unknown', +Qt.Key_TouchpadOn:'unknown', +Qt.Key_TouchpadToggle:'unknown', +Qt.Key_Touroku:'unknown', +Qt.Key_Travel:'unknown', +Qt.Key_TrebleDown:'unknown', +Qt.Key_TrebleUp:'unknown', +Qt.Key_U:'u', +Qt.Key_UWB:'unknown', +Qt.Key_Uacute:'unknown', +Qt.Key_Ucircumflex:'unknown', +Qt.Key_Udiaeresis:'unknown', +Qt.Key_Ugrave:'unknown', +Qt.Key_Underscore:'unknown', +Qt.Key_Undo:'unknown', +Qt.Key_Up:'arrow_up', +Qt.Key_V:'v', +Qt.Key_Video:'unknown', +Qt.Key_View:'unknown', +Qt.Key_VoiceDial:'unknown', +Qt.Key_VolumeDown:'unknown', +Qt.Key_VolumeMute:'unknown', +Qt.Key_VolumeUp:'unknown', +Qt.Key_W:'w', +Qt.Key_WLAN:'unknown', +Qt.Key_WWW:'unknown', +Qt.Key_WakeUp:'unknown', +Qt.Key_WebCam:'unknown', +Qt.Key_Word:'unknown', +Qt.Key_X:'x', +Qt.Key_Xfer:'unknown', +Qt.Key_Y:'y', +Qt.Key_Yacute:'unknown', +Qt.Key_Yellow:'unknown', +Qt.Key_Yes:'unknown', +Qt.Key_Z:'z', +Qt.Key_Zenkaku:'unknown', +Qt.Key_Zenkaku_Hankaku:'unknown', +Qt.Key_Zoom:'unknown', +Qt.Key_ZoomIn:'unknown', +Qt.Key_ZoomOut:'unknown', +Qt.Key_acute:'unknown', +Qt.Key_brokenbar:'unknown', +Qt.Key_cedilla:'unknown', +Qt.Key_cent:'unknown', +Qt.Key_copyright:'unknown', +Qt.Key_currency:'unknown', +Qt.Key_degree:'unknown', +Qt.Key_diaeresis:'unknown', +Qt.Key_division:'unknown', +Qt.Key_exclamdown:'unknown', +Qt.Key_guillemotleft:'unknown', +Qt.Key_guillemotright:'unknown', +Qt.Key_hyphen:'unknown', +Qt.Key_iTouch:'unknown', +Qt.Key_macron:'unknown', +Qt.Key_masculine:'unknown', +Qt.Key_mu:'unknown', +Qt.Key_multiply:'unknown', +Qt.Key_nobreakspace:'unknown', +Qt.Key_notsign:'unknown', +Qt.Key_onehalf:'unknown', +Qt.Key_onequarter:'unknown', +Qt.Key_onesuperior:'unknown', +Qt.Key_ordfeminine:'unknown', +Qt.Key_paragraph:'unknown', +Qt.Key_periodcentered:'unknown', +Qt.Key_plusminus:'unknown', +Qt.Key_questiondown:'unknown', +Qt.Key_registered:'unknown', +Qt.Key_section:'unknown', +Qt.Key_ssharp:'unknown', +Qt.Key_sterling:'unknown', +Qt.Key_threequarters:'unknown', +Qt.Key_threesuperior:'unknown', +Qt.Key_twosuperior:'unknown', +Qt.Key_unknown:'unknown', +Qt.Key_ydiaeresis:'unknown', +Qt.Key_yen:'unknown', } \ No newline at end of file diff --git a/QMeta3D/QMeta3D_Modifiers_Translation.py b/QMeta3D/QMeta3D_Modifiers_Translation.py index 17684818..45489333 100644 --- a/QMeta3D/QMeta3D_Modifiers_Translation.py +++ b/QMeta3D/QMeta3D_Modifiers_Translation.py @@ -1,15 +1,15 @@ -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * -import sys -__all__ = ["QMeta3D_Modifiers_Translation.py"] - -QMeta3D_Modifier_translation ={ -Qt.NoModifier:None, -Qt.ShiftModifier:'shift', -Qt.ControlModifier:'control', -Qt.AltModifier:'alt', -Qt.MetaModifier:'unknown', -Qt.KeypadModifier:'unknown', -Qt.GroupSwitchModifier:'unknown', +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +import sys +__all__ = ["QMeta3D_Modifiers_Translation.py"] + +QMeta3D_Modifier_translation ={ +Qt.NoModifier:None, +Qt.ShiftModifier:'shift', +Qt.ControlModifier:'control', +Qt.AltModifier:'alt', +Qt.MetaModifier:'unknown', +Qt.KeypadModifier:'unknown', +Qt.GroupSwitchModifier:'unknown', } \ No newline at end of file diff --git a/QMeta3D/QMouseWatcherNode.py b/QMeta3D/QMouseWatcherNode.py index e75d86cc..195e6457 100644 --- a/QMeta3D/QMouseWatcherNode.py +++ b/QMeta3D/QMouseWatcherNode.py @@ -1,27 +1,27 @@ -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * - -from panda3d.core import * - -__all__ = ["QMouseWatcherNode"] - - -class QMouseWatcherNode(MouseWatcher): - def __init__(self, parent): - super().__init__() - - self.parent = parent - - def getMouse(self, *args, **kwargs): - pos = self.parent.mapFromGlobal(QCursor.pos()) - - rel_x = -1 + 2 * pos.x() / self.parent.width() - rel_y = -1 + 2 * pos.y() / self.parent.height() - - rel_y = -rel_y - - return LPoint2(rel_x, rel_y) - - def hasMouse(self): - return isinstance(self.parent, QWidget) +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * + +from panda3d.core import * + +__all__ = ["QMouseWatcherNode"] + + +class QMouseWatcherNode(MouseWatcher): + def __init__(self, parent): + super().__init__() + + self.parent = parent + + def getMouse(self, *args, **kwargs): + pos = self.parent.mapFromGlobal(QCursor.pos()) + + rel_x = -1 + 2 * pos.x() / self.parent.width() + rel_y = -1 + 2 * pos.y() / self.parent.height() + + rel_y = -rel_y + + return LPoint2(rel_x, rel_y) + + def hasMouse(self): + return isinstance(self.parent, QWidget) diff --git a/QMeta3D/Tools/__init__.py b/QMeta3D/Tools/__init__.py index 449353ac..0697112f 100644 --- a/QMeta3D/Tools/__init__.py +++ b/QMeta3D/Tools/__init__.py @@ -1,2 +1,2 @@ -name="QMeta3D" +name="QMeta3D" __all__ = ["generate_qt_to_pd3d_translator"] \ No newline at end of file diff --git a/QMeta3D/Tools/generate_qt_to_pd3d_translator.py b/QMeta3D/Tools/generate_qt_to_pd3d_translator.py index 3f296230..d3b2c071 100644 --- a/QMeta3D/Tools/generate_qt_to_pd3d_translator.py +++ b/QMeta3D/Tools/generate_qt_to_pd3d_translator.py @@ -1,11 +1,11 @@ -from PyQt5.QtCore import * -from PyQt5.QtGui import * -from PyQt5.QtWidgets import * - -H=Qt.__dict__ -lst = [a for a in H if a.startswith("Key_")] -QMeta3D_Key_translation = "QMeta3D_Key_translation ={" -for i in range(len(lst)): - QMeta3D_Key_translation += "Qt.{}:'unknown',\n".format(lst[i]) -QMeta3D_Key_translation += "}" +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * + +H=Qt.__dict__ +lst = [a for a in H if a.startswith("Key_")] +QMeta3D_Key_translation = "QMeta3D_Key_translation ={" +for i in range(len(lst)): + QMeta3D_Key_translation += "Qt.{}:'unknown',\n".format(lst[i]) +QMeta3D_Key_translation += "}" print(QMeta3D_Key_translation) \ No newline at end of file diff --git a/QMeta3D/__init__.py b/QMeta3D/__init__.py index b94cb4e0..e5c409c5 100644 --- a/QMeta3D/__init__.py +++ b/QMeta3D/__init__.py @@ -1,2 +1,2 @@ -name="QMeta3D" +name="QMeta3D" __all__ = ["QMeta3DWidget.py", "Meta3DWorld.py", "QMeta3D_Keys_Translation.py"] \ No newline at end of file diff --git a/RenderPipelineFile/.flake8 b/RenderPipelineFile/.flake8 index bdcfd32e..967dd77c 100644 --- a/RenderPipelineFile/.flake8 +++ b/RenderPipelineFile/.flake8 @@ -1,4 +1,4 @@ -[flake8] -max-line-length=100 -exclude=rplibs/*,data/*,hosek_wilkie_scattering,bake_gi,_DEV,*_generated.py,resources_rc.py - +[flake8] +max-line-length=100 +exclude=rplibs/*,data/*,hosek_wilkie_scattering,bake_gi,_DEV,*_generated.py,resources_rc.py + diff --git a/RenderPipelineFile/.gitignore b/RenderPipelineFile/.gitignore index 9258aa60..825ac9ed 100644 --- a/RenderPipelineFile/.gitignore +++ b/RenderPipelineFile/.gitignore @@ -1,60 +1,60 @@ -# --- Ignored extensions --- -*.pyd -*.pyc -*.ignore -*.old -*.egg -*.psd -*.blend1 -*.blend2 -*.pdb -*.stackdump -*.exr -*.trace -*.autogen* -*.mip -mitsuba.*.log -*.exr -*.txo.pz - - -# --- Ignored folders and patterns --- -temp/ -models/ -data/film_grain/*.png -data/default_cubemap/filtered/ -_DEV -toolkit/pathtracing_reference/mitsuba/ -toolkit/pathtracing_reference/scene*.png -toolkit/pathtracing_reference/Scene*.blend -toolkit/pathtracing_reference/envmap.png -rpplugins/clouds/resources/slices/* - -toolkit/rp_distributor/built/ - -# --- Ignored files --- -TODO.txt -*.flag -desktop.ini -scattering_lut.png -old/ -data/gui/loading_screen -data/environment_brdf/scene.png -data/environment_brdf/res/scene.png -toolkit/pathtracing_reference/batch_compare/ -toolkit/pathtracing_reference/difference.png -toolkit/pathtracing_reference/res/Scene.blend -toolkit/pathtracing_reference/res/scene.png -toolkit/pathtracing_reference/scene*.png -toolkit/pathtracing_reference/res/tex/envmap.png -_tmp_material.py - -*/poisson_disk_generator/source/config_module* - -_bake_params* -raw-bake.png -toolkit/bake_gi/resources/*.bam -toolkit/bake_gi/resources/*.blend -toolkit/bake_gi/scene - -.vscode +# --- Ignored extensions --- +*.pyd +*.pyc +*.ignore +*.old +*.egg +*.psd +*.blend1 +*.blend2 +*.pdb +*.stackdump +*.exr +*.trace +*.autogen* +*.mip +mitsuba.*.log +*.exr +*.txo.pz + + +# --- Ignored folders and patterns --- +temp/ +models/ +data/film_grain/*.png +data/default_cubemap/filtered/ +_DEV +toolkit/pathtracing_reference/mitsuba/ +toolkit/pathtracing_reference/scene*.png +toolkit/pathtracing_reference/Scene*.blend +toolkit/pathtracing_reference/envmap.png +rpplugins/clouds/resources/slices/* + +toolkit/rp_distributor/built/ + +# --- Ignored files --- +TODO.txt +*.flag +desktop.ini +scattering_lut.png +old/ +data/gui/loading_screen +data/environment_brdf/scene.png +data/environment_brdf/res/scene.png +toolkit/pathtracing_reference/batch_compare/ +toolkit/pathtracing_reference/difference.png +toolkit/pathtracing_reference/res/Scene.blend +toolkit/pathtracing_reference/res/scene.png +toolkit/pathtracing_reference/scene*.png +toolkit/pathtracing_reference/res/tex/envmap.png +_tmp_material.py + +*/poisson_disk_generator/source/config_module* + +_bake_params* +raw-bake.png +toolkit/bake_gi/resources/*.bam +toolkit/bake_gi/resources/*.blend +toolkit/bake_gi/scene + +.vscode diff --git a/RenderPipelineFile/.idea/inspectionProfiles/Project_Default.xml b/RenderPipelineFile/.idea/inspectionProfiles/Project_Default.xml index 2bd29215..c0a14875 100644 --- a/RenderPipelineFile/.idea/inspectionProfiles/Project_Default.xml +++ b/RenderPipelineFile/.idea/inspectionProfiles/Project_Default.xml @@ -1,12 +1,12 @@ - - - + + + \ No newline at end of file diff --git a/RenderPipelineFile/.idea/inspectionProfiles/profiles_settings.xml b/RenderPipelineFile/.idea/inspectionProfiles/profiles_settings.xml index 105ce2da..20fc29e7 100644 --- a/RenderPipelineFile/.idea/inspectionProfiles/profiles_settings.xml +++ b/RenderPipelineFile/.idea/inspectionProfiles/profiles_settings.xml @@ -1,6 +1,6 @@ - - - + + + \ No newline at end of file diff --git a/RenderPipelineFile/.pylintrc b/RenderPipelineFile/.pylintrc index b9dbdcff..44a9eebd 100644 --- a/RenderPipelineFile/.pylintrc +++ b/RenderPipelineFile/.pylintrc @@ -1,379 +1,379 @@ -[MASTER] - -# Specify a configuration file. -#rcfile= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -init-hook= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS,rplibs,.git,scripts,build.py - -# Pickle collected data for later comparisons. -persistent=yes - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Use multiple processes to speed up Pylint. -jobs=1 - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist= - -# Allow optimization of some AST trees. This will activate a peephole AST -# optimizer, which will apply various small optimizations. For instance, it can -# be used to obtain the result of joining multiple strings with the addition -# operator. Joining a lot of strings can lead to a maximum recursion error in -# Pylint and this flag can prevent that. It has one side effect, the resulting -# AST will be different than the one from reality. -optimize-ast=yes - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time. See also the "--disable" option for examples. -#enable= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -disable=import-star-module-level,old-octal-literal,oct-method,print-statement,unpacking-in-except,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating,E0611,W0622,W0201,C0111,R0201,R0913,R0902,R0904,R0914 - - -[REPORTS] - -# Set the output format. Available formats are text, parseable, colorized, msvs -# (visual studio) and html. You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Put messages in a separate file for each module / package specified on the -# command line instead of printing them on stdout. Reports (if any) will be -# written in a file name "pylint_global.[txt|html]". -files-output=no - -# Tells whether to display a full report or only the messages -reports=yes - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - - -[BASIC] - -# List of builtins function names that should not be used, separated by a comma -bad-functions=map,filter,input,xrange,iteritems,raw_input - -# Good variable names which should always be accepted, separated by a comma -good-names=i,j,k,ex,Run,_,x,y,r,g,b,a,uv,w,h,np - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo,bar,baz,toto,tutu,tata,tmp,temp - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no - -# Regular expression matching correct function names -function-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for function names -function-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct variable names -variable-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for variable names -variable-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct constant names -const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Naming hint for constant names -const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ - -# Regular expression matching correct attribute names -attr-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for attribute names -attr-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct argument names -argument-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for argument names -argument-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression matching correct class attribute names -class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Naming hint for class attribute names -class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ - -# Regular expression matching correct inline iteration names -inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ - -# Naming hint for inline iteration names -inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ - -# Regular expression matching correct class names -class-rgx=[A-Z_][a-zA-Z0-9]+$ - -# Naming hint for class names -class-name-hint=[A-Z_][a-zA-Z0-9]+$ - -# Regular expression matching correct module names -module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Naming hint for module names -module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ - -# Regular expression matching correct method names -method-rgx=[a-z_][a-z0-9_]{2,30}$ - -# Naming hint for method names -method-name-hint=[a-z_][a-z0-9_]{2,30}$ - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - - -[ELIF] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=4 - - -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=100 - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma,dict-separator - -# Maximum number of lines in a module -max-module-lines=1000 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes= - - -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=8 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - - -[SPELLING] - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[TYPECHECK] - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# List of classes names for which member attributes should not be checked -# (useful for classes with attributes dynamically set). This supports can work -# with qualified names. -ignored-classes= - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - - -[VARIABLES] - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=_$|dummy - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_,_cb - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__,__new__,setup,init,load,create - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict,_fields,_replace,_source,_make - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=5 - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.* - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of branch for function / method body -max-branches=12 - -# Maximum number of statements in function / method body -max-statements=50 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of boolean expressions in a if statement -max-bool-expr=5 - - -[IMPORTS] - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub,TERMIOS,Bastion,rexec - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception - +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +init-hook= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS,rplibs,.git,scripts,build.py + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Allow optimization of some AST trees. This will activate a peephole AST +# optimizer, which will apply various small optimizations. For instance, it can +# be used to obtain the result of joining multiple strings with the addition +# operator. Joining a lot of strings can lead to a maximum recursion error in +# Pylint and this flag can prevent that. It has one side effect, the resulting +# AST will be different than the one from reality. +optimize-ast=yes + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=import-star-module-level,old-octal-literal,oct-method,print-statement,unpacking-in-except,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating,E0611,W0622,W0201,C0111,R0201,R0913,R0902,R0904,R0914 + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells whether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[BASIC] + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter,input,xrange,iteritems,raw_input + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_,x,y,r,g,b,a,uv,w,h,np + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata,tmp,temp + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for function names +function-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for variable names +variable-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for attribute names +attr-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for argument names +argument-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for method names +method-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[ELIF] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=4 + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=100 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes= + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=8 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). This supports can work +# with qualified names. +ignored-classes= + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_$|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setup,init,load,create + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception + diff --git a/RenderPipelineFile/.travis.yml b/RenderPipelineFile/.travis.yml index e7cad254..50351a15 100644 --- a/RenderPipelineFile/.travis.yml +++ b/RenderPipelineFile/.travis.yml @@ -1,18 +1,18 @@ -language: cpp -sudo: required -dist: trusty -compiler: gcc -addons: - apt: - sources: - - sourceline: "deb http://archive.panda3d.org/ubuntu/ trusty-dev main" - packages: - - cmake - - libeigen3-dev - - libfreetype6-dev - - panda3d1.10 - -script: - - export PYTHONPATH=${PYTHONPATH}:/usr/lib/python2.7/dist-packages - - export PYTHONPATH=${PYTHONPATH}:/usr/share/panda3d - - python2.7 setup.py --ci-build +language: cpp +sudo: required +dist: trusty +compiler: gcc +addons: + apt: + sources: + - sourceline: "deb http://archive.panda3d.org/ubuntu/ trusty-dev main" + packages: + - cmake + - libeigen3-dev + - libfreetype6-dev + - panda3d1.10 + +script: + - export PYTHONPATH=${PYTHONPATH}:/usr/lib/python2.7/dist-packages + - export PYTHONPATH=${PYTHONPATH}:/usr/share/panda3d + - python2.7 setup.py --ci-build diff --git a/RenderPipelineFile/LICENSE.txt b/RenderPipelineFile/LICENSE.txt index 38e9c74d..d60f2b57 100644 --- a/RenderPipelineFile/LICENSE.txt +++ b/RenderPipelineFile/LICENSE.txt @@ -1,22 +1,22 @@ - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/RenderPipelineFile/README.md b/RenderPipelineFile/README.md index 35d3903f..c962071a 100644 --- a/RenderPipelineFile/README.md +++ b/RenderPipelineFile/README.md @@ -1,70 +1,70 @@ -[![Join the chat at https://gitter.im/tobspr/RenderPipeline](https://badges.gitter.im/tobspr/RenderPipeline.svg)](https://gitter.im/tobspr/RenderPipeline?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Build Status](https://travis-ci.org/tobspr/RenderPipeline.svg?branch=master)](https://travis-ci.org/tobspr/RenderPipeline) - - -Deferred Rendering Pipeline with Physically Based Shading - -Deferred Realtime Rendering Pipeline with Physically Based Shading for the Panda3D Game Engine. - -### Core Features - -- Physically Based Shading -- Deferred Rendering -- Advanced Post-Processing Effects and Framework -- Time of Day System -- Plugin System - -## Screenshots - -You can click on the images to enlarge them. Besides of that, you can find many more screenshots in my dropbox folder. - -**Forest** - - -**Material demo** - - -**Screen space reflections** - - -**Car rendering** -Car rendering - -**Plugin and Time of Day editor:** - - -**Terrain and volumetric clouds** - - - -See the Feature List -for a list of features, and list of techniques I intend to implement. - -You can find my todo list for the render pipeline here: Render Pipeline Roadmap. - -### Getting Started / Wiki - -You should checkout the wiki if you want to find out more about the pipeline: -Render Pipeline WIKI - -There is also a page about getting started there: Getting Started - -### Requirements - -- OpenGL 4.3 capable GPU (and drivers) -- Panda3D Development Build -- 1 GB Graphics Memory recommended *(Can run with less, depends on enabled plugins and resolution)* - -**Notice**: It seems that the drivers for Intel HD Graphics on Linux are not -capable of all 4.3 features, so the pipeline is not able to run there! - -If you want to use the C++ Modules, checkout -Building the C++ Modules to get a list of requirements for them. - -### Reporting Bugs / Contributing - -If you find bugs, or find information missing in the wiki, or want to contribute, -you can find me most of the time in the `#panda3d` channel on freenode. - -If I shouldn't be there, feel free to contact me per E-Mail: `tobias.springer1@googlemail.com` - +[![Join the chat at https://gitter.im/tobspr/RenderPipeline](https://badges.gitter.im/tobspr/RenderPipeline.svg)](https://gitter.im/tobspr/RenderPipeline?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Build Status](https://travis-ci.org/tobspr/RenderPipeline.svg?branch=master)](https://travis-ci.org/tobspr/RenderPipeline) + + +Deferred Rendering Pipeline with Physically Based Shading + +Deferred Realtime Rendering Pipeline with Physically Based Shading for the Panda3D Game Engine. + +### Core Features + +- Physically Based Shading +- Deferred Rendering +- Advanced Post-Processing Effects and Framework +- Time of Day System +- Plugin System + +## Screenshots + +You can click on the images to enlarge them. Besides of that, you can find many more screenshots in my dropbox folder. + +**Forest** + + +**Material demo** + + +**Screen space reflections** + + +**Car rendering** +Car rendering + +**Plugin and Time of Day editor:** + + +**Terrain and volumetric clouds** + + + +See the Feature List +for a list of features, and list of techniques I intend to implement. + +You can find my todo list for the render pipeline here: Render Pipeline Roadmap. + +### Getting Started / Wiki + +You should checkout the wiki if you want to find out more about the pipeline: +Render Pipeline WIKI + +There is also a page about getting started there: Getting Started + +### Requirements + +- OpenGL 4.3 capable GPU (and drivers) +- Panda3D Development Build +- 1 GB Graphics Memory recommended *(Can run with less, depends on enabled plugins and resolution)* + +**Notice**: It seems that the drivers for Intel HD Graphics on Linux are not +capable of all 4.3 features, so the pipeline is not able to run there! + +If you want to use the C++ Modules, checkout +Building the C++ Modules to get a list of requirements for them. + +### Reporting Bugs / Contributing + +If you find bugs, or find information missing in the wiki, or want to contribute, +you can find me most of the time in the `#panda3d` channel on freenode. + +If I shouldn't be there, feel free to contact me per E-Mail: `tobias.springer1@googlemail.com` + diff --git a/RenderPipelineFile/config/Meta3d-config.prc b/RenderPipelineFile/config/Meta3d-config.prc index e47a54b8..b4401d83 100644 --- a/RenderPipelineFile/config/Meta3d-config.prc +++ b/RenderPipelineFile/config/Meta3d-config.prc @@ -1,174 +1,174 @@ - - -# This is the config file used to configure basic settings for Panda3D. -# The pipeline loads it at startup to ensure the environment is setup properly. - -# -------------- Development options -------------- - -pstats-gpu-timing #t -gl-debug #t -gl-debug-object-labels #t - -# -------------- Production options --------------- - -# pstats-gpu-timing #f -# gl-debug #f -# gl-debug-object-labels #f - -# ----------------- Misc Settings ----------------- - -# Disable V-Sync -sync-video #f - -# Limit the pstats-rate. This causes huge lag on windows 10. -pstats-max-rate 200 - -# No stack trace on assertion, set this to true to make panda crash on assertions -# (which will allow to debug it) -# assert-abort #t -# show-dll-error-dialog #f - -# File system should be case sensitive -# NOTICE: Set this to #f if you are using tempfile, since it returns -# wrong cased directory paths -vfs-case-sensitive #t - -# Enable state cache, this seems to actually help the performance by a lot -state-cache #t -transform-cache #t - -# Hide frame rate meter (we have our own) -show-frame-rate-meter #f - -# Set text settings -text-minfilter linear -text-magfilter linear -text-page-size 512 512 -text-rwap-mode WM_border_clor - -# Better text performance since rdb's patch -text-flatten 0 -text-dynamic-merge 1 - -# For smoother animations -# even-animation #t - -# Threading, really buggy! -#threading-model App/Cull/Draw - -# Disable stencil, not supported/required -support-stencil #f -framebuffer-stencil #f - -# Don't use srgb correction, we do that in the final shader -framebuffer-srgb #f - -# Don't use multisamples -framebuffer-multisample #f -multisamples 0 - -# Don't rescale textures which are no power-of-2 -textures-power-2 none - -# Set default texture filters -texture-anisotropic-degree 16 -texture-magfilter linear -texture-minfilter linear -texture-quality-level fastest - -# Enable seamless cubemap filtering, important for environment filtering -gl-cube-map-seamless #t - -# Disable caching of textures -model-cache-textures #f - -# Disable the annoying SRGB warning from pnmimage -notify-level-pnmimage error - -# Disable the buffer viewer, we have our own -show-buffers #f - -# Use the default coordinate system, this makes our matrix transformations -# faster because we don't have have to transform them to a different coordinate -# system before -gl-coordinate-system default - -# This makes animations smoother, especially if they were exported at 30 FPS -# and are played at 60 FPS -interpolate-frames #t - -# Disable workarround in panda which causes our shadow atlas to take twice -# the amount of vram it should, due to an intel driver bug. -gl-force-fbo-color #f - -# ----------- OpenGL / Performance Settings ------------ - -# Require OpenGL 4.3 at least, necessary for Intel drivers on Linux -gl-version 4 3 - -# Animations on the gpu. The default shader has to get adjusted to support this -# feature before this option can be turned on. -# hardware-animated-vertices #t - -# Try this options for performance -# uniquify-matrix #t -# uniquify-transforms #t -# uniquify-states #t -# uniquify-attribs #f - -# Enable garbarge collection -garbage-collect-states #t -# garbage-collect-states-rate 0.2 - -# Compress textures on the drivers? -# driver-compress-textures #t - -# Faster animations? (Have to test) -# matrix-palette #t -# display-list-animation #t - -# Better GL performance by not using gl-finish and so on -gl-finish #f -gl-force-no-error #t -gl-check-errors #f -gl-force-no-flush #t -gl-force-no-scissor #t - -# Eventually disable memory barriers, have to check if this is faster -gl-enable-memory-barriers #f - -# Disable threading -lock-to-one-cpu #t -support-threads #f - -# Let the driver generate the mipmaps -driver-generate-mipmaps #t - -# Use immutable texture storage, it is *supposed* to be faster, but might not be -# XXX: Seems to produce an GL_INVALID_VALUE when disabled -gl-immutable-texture-storage #t - -# Default window settings -# depth-bits 0 -color-bits 0 -framebuffer-depth #f - -# Small performance gain by specifying fixed vertex attribute locations. -# Might cause issues with some (incorrectly converted/loaded) meshes though -gl-fixed-vertex-attrib-locations #f - -# Disable the fragment shader performance warning -gl-validate-shaders #f -gl-skip-shader-recompilation-warnings #t - -alpha-scale-via-texture #f -pstats-name Render Pipeline Stats -rescale-normals #f -screenshot-extension png - -# Required for correct velocity -always-store-prev-transform #t -allow-incomplete-render #t - - -no-singular-invert #f + + +# This is the config file used to configure basic settings for Panda3D. +# The pipeline loads it at startup to ensure the environment is setup properly. + +# -------------- Development options -------------- + +pstats-gpu-timing #t +gl-debug #t +gl-debug-object-labels #t + +# -------------- Production options --------------- + +# pstats-gpu-timing #f +# gl-debug #f +# gl-debug-object-labels #f + +# ----------------- Misc Settings ----------------- + +# Disable V-Sync +sync-video #f + +# Limit the pstats-rate. This causes huge lag on windows 10. +pstats-max-rate 200 + +# No stack trace on assertion, set this to true to make panda crash on assertions +# (which will allow to debug it) +# assert-abort #t +# show-dll-error-dialog #f + +# File system should be case sensitive +# NOTICE: Set this to #f if you are using tempfile, since it returns +# wrong cased directory paths +vfs-case-sensitive #t + +# Enable state cache, this seems to actually help the performance by a lot +state-cache #t +transform-cache #t + +# Hide frame rate meter (we have our own) +show-frame-rate-meter #f + +# Set text settings +text-minfilter linear +text-magfilter linear +text-page-size 512 512 +text-rwap-mode WM_border_clor + +# Better text performance since rdb's patch +text-flatten 0 +text-dynamic-merge 1 + +# For smoother animations +# even-animation #t + +# Threading, really buggy! +#threading-model App/Cull/Draw + +# Disable stencil, not supported/required +support-stencil #f +framebuffer-stencil #f + +# Don't use srgb correction, we do that in the final shader +framebuffer-srgb #f + +# Don't use multisamples +framebuffer-multisample #f +multisamples 0 + +# Don't rescale textures which are no power-of-2 +textures-power-2 none + +# Set default texture filters +texture-anisotropic-degree 16 +texture-magfilter linear +texture-minfilter linear +texture-quality-level fastest + +# Enable seamless cubemap filtering, important for environment filtering +gl-cube-map-seamless #t + +# Disable caching of textures +model-cache-textures #f + +# Disable the annoying SRGB warning from pnmimage +notify-level-pnmimage error + +# Disable the buffer viewer, we have our own +show-buffers #f + +# Use the default coordinate system, this makes our matrix transformations +# faster because we don't have have to transform them to a different coordinate +# system before +gl-coordinate-system default + +# This makes animations smoother, especially if they were exported at 30 FPS +# and are played at 60 FPS +interpolate-frames #t + +# Disable workarround in panda which causes our shadow atlas to take twice +# the amount of vram it should, due to an intel driver bug. +gl-force-fbo-color #f + +# ----------- OpenGL / Performance Settings ------------ + +# Require OpenGL 4.3 at least, necessary for Intel drivers on Linux +gl-version 4 3 + +# Animations on the gpu. The default shader has to get adjusted to support this +# feature before this option can be turned on. +# hardware-animated-vertices #t + +# Try this options for performance +# uniquify-matrix #t +# uniquify-transforms #t +# uniquify-states #t +# uniquify-attribs #f + +# Enable garbarge collection +garbage-collect-states #t +# garbage-collect-states-rate 0.2 + +# Compress textures on the drivers? +# driver-compress-textures #t + +# Faster animations? (Have to test) +# matrix-palette #t +# display-list-animation #t + +# Better GL performance by not using gl-finish and so on +gl-finish #f +gl-force-no-error #t +gl-check-errors #f +gl-force-no-flush #t +gl-force-no-scissor #t + +# Eventually disable memory barriers, have to check if this is faster +gl-enable-memory-barriers #f + +# Disable threading +lock-to-one-cpu #t +support-threads #f + +# Let the driver generate the mipmaps +driver-generate-mipmaps #t + +# Use immutable texture storage, it is *supposed* to be faster, but might not be +# XXX: Seems to produce an GL_INVALID_VALUE when disabled +gl-immutable-texture-storage #t + +# Default window settings +# depth-bits 0 +color-bits 0 +framebuffer-depth #f + +# Small performance gain by specifying fixed vertex attribute locations. +# Might cause issues with some (incorrectly converted/loaded) meshes though +gl-fixed-vertex-attrib-locations #f + +# Disable the fragment shader performance warning +gl-validate-shaders #f +gl-skip-shader-recompilation-warnings #t + +alpha-scale-via-texture #f +pstats-name Render Pipeline Stats +rescale-normals #f +screenshot-extension png + +# Required for correct velocity +always-store-prev-transform #t +allow-incomplete-render #t + + +no-singular-invert #f diff --git a/RenderPipelineFile/config/daytime.yaml b/RenderPipelineFile/config/daytime.yaml index 030a6111..1a5bbccc 100644 --- a/RenderPipelineFile/config/daytime.yaml +++ b/RenderPipelineFile/config/daytime.yaml @@ -1,27 +1,27 @@ - -# Render Pipeline Time Of Day Configuration -# Instead of editing this file, prefer to use the Time Of Day Editor -# Any formatting and comments will be lost - -control_points: - clouds: - cloud_brightness: [[[0.4558541267,0.9574780059],[0.2744727256,0.8944309678],[0.1938559693,0.5249266862],[0.8905932438,0.2375366569],[0.6429901728,0.9589492375],[0.7581600000,0.8914912023],[0.2226478119,0.7859268915],[0.8406909789,0.4897360704],[0.5451055662,0.9618768328],[0.9462571977,0.0000000000],[1.0000000000,0.0000000000],[0.3646833013,0.9472140762],[0.3186180422,0.9325513197],[0.0823264879,0.0000000000]]] - color_correction: - camera_iso: [[[0.4708024067,0.2757660168]]] - camera_shutter: [[[0.5134061147,0.0552217053]]] - camera_aperture: [[[0.5076849397,0.3299161078]]] - env_probes: - ambient_scale: [[[0.5038535646,0.3091922005],[0.1368015414,0.3621169916],[0.8420038536,0.3983286908]]] - sky_ambient_scale: [[[0.4836223507,0.0974930362],[0.8497109827,0.0000000000],[0.0867052023,0.0027855153]]] - sun_ambient_scale: [[[0.4913294797,0.0584958217],[0.8612716763,0.0306406685],[0.1425818882,0.0445682451]]] - scattering: - sun_intensity: [[[0.0000000000,0.0000000000],[0.0041666667,0.0000000000],[0.0083333333,0.0000000000],[0.0125000000,0.0000000000],[0.0166666667,0.0000000000],[0.0208333333,0.0000000000],[0.0250000000,0.0000000000],[0.0291666667,0.0000000000],[0.0333333333,0.0000000000],[0.0375000000,0.0000000000],[0.0416666667,0.0000000000],[0.0458333333,0.0000000000],[0.0500000000,0.0000000000],[0.0541666667,0.0000000000],[0.0583333333,0.0000000000],[0.0625000000,0.0000000000],[0.0666666667,0.0000000000],[0.0708333333,0.0000000000],[0.0750000000,0.0000000000],[0.0791666667,0.0000000000],[0.0833333333,0.0000000000],[0.0875000000,0.0000000000],[0.0916666667,0.0000000000],[0.0958333333,0.0000000000],[0.1000000000,0.0000000000],[0.1041666667,0.0000000000],[0.1083333333,0.0000000000],[0.1125000000,0.0000000000],[0.1166666667,0.0000000000],[0.1208333333,0.0000000000],[0.1250000000,0.0000000000],[0.1291666667,0.0000000000],[0.1333333333,0.0000000000],[0.1375000000,0.0000000000],[0.1416666667,0.0000000000],[0.1458333333,0.0000000000],[0.1500000000,0.0000000000],[0.1541666667,0.0000000000],[0.1583333333,0.0000028805],[0.1625000000,0.0003577724],[0.1666666667,0.0013331400],[0.1708333333,0.0029671803],[0.1750000000,0.0052963381],[0.1791666667,0.0083550556],[0.1833333333,0.0121755589],[0.1875000000,0.0167876159],[0.1916666667,0.0222183530],[0.1958333333,0.0284919947],[0.2000000000,0.0356297193],[0.2041666667,0.0436494349],[0.2083333333,0.0525656099],[0.2125000000,0.0623891610],[0.2166666667,0.0731272461],[0.2208333333,0.0847831708],[0.2250000000,0.0973563167],[0.2291666667,0.1108419698],[0.2333333333,0.1252313631],[0.2375000000,0.1405115250],[0.2416666667,0.1566653434],[0.2458333333,0.1736715009],[0.2500000000,0.1915046014],[0.2541666667,0.2101350464],[0.2583333333,0.2295292930],[0.2625000000,0.2496498145],[0.2666666667,0.2704552670],[0.2708333333,0.2919006662],[0.2750000000,0.3139375192],[0.2791666667,0.3365139497],[0.2833333333,0.3595750662],[0.2875000000,0.3830630359],[0.2916666667,0.4069173972],[0.2958333333,0.4310753462],[0.3000000000,0.4554720417],[0.3041666667,0.4800408236],[0.3083333333,0.5047136020],[0.3125000000,0.5294212108],[0.3166666667,0.5540936424],[0.3208333333,0.5786605298],[0.3250000000,0.6030514553],[0.3291666667,0.6271963182],[0.3333333333,0.6510256858],[0.3375000000,0.6744711982],[0.3416666667,0.6974659988],[0.3458333333,0.7199450163],[0.3500000000,0.7418453485],[0.3541666667,0.7631067095],[0.3583333333,0.7836717291],[0.3625000000,0.8034862953],[0.3666666667,0.8224999302],[0.3708333333,0.8406661079],[0.3750000000,0.8579425235],[0.3791666667,0.8742914270],[0.3833333333,0.8896799131],[0.3875000000,0.9040801386],[0.3916666667,0.9174695289],[0.3958333333,0.9298310650],[0.4000000000,0.9411533765],[0.4041666667,0.9514309312],[0.4083333333,0.9606641691],[0.4125000000,0.9688595571],[0.4166666667,0.9760296330],[0.4208333333,0.9821930708],[0.4250000000,0.9873746114],[0.4291666667,0.9916050060],[0.4333333333,0.9949209310],[0.4375000000,0.9973647924],[0.4416666667,0.9989845508],[0.4458333333,0.9998334497],[0.4500000000,0.9999696949],[0.4541666667,0.9994560801],[0.4583333333,0.9983595429],[0.4625000000,0.9967506613],[0.4666666667,0.9947030614],[0.4708333333,0.9922927758],[0.4750000000,0.9895975125],[0.4791666667,0.9866958610],[0.4833333333,0.9836664262],[0.4875000000,0.9805868867],[0.4916666667,0.9775330316],[0.4958333333,0.9745777179],[0.5000000000,0.9717898417],[0.5041666667,0.9692332877],[0.5083333333,0.9669658924],[0.5125000000,0.9650384806],[0.5089595376,0.9690650222],[0.5208333333,0.9623666659],[0.5250000000,0.9616814371],[0.5291666667,0.9614534423],[0.5333333333,0.9616877089],[0.5375000000,0.9623790807],[0.5416666667,0.9635123329],[0.5458333333,0.9650624244],[0.5500000000,0.9669949804],[0.5541666667,0.9692669864],[0.5583333333,0.9718275065],[0.5625000000,0.9746185969],[0.5666666667,0.9775762863],[0.5708333333,0.9806315864],[0.5750000000,0.9837115661],[0.5791666667,0.9867403433],[0.5833333333,0.9896401655],[0.5875000000,0.9923323562],[0.5916666667,0.9947382579],[0.5958333333,0.9967800977],[0.6000000000,0.9983817820],[0.6041666667,0.9994696263],[0.6083333333,0.9999730028],[0.6125000000,0.9998249266],[0.6166666667,0.9989625601],[0.6208333333,0.9973276624],[0.6250000000,0.9948669567],[0.6291666667,0.9915324664],[0.6333333333,0.9872817545],[0.6375000000,0.9820781426],[0.6416666667,0.9758908775],[0.6458333333,0.9686952146],[0.6500000000,0.9604725211],[0.6541666667,0.9512102537],[0.6583333333,0.9409019858],[0.6625000000,0.9295473441],[0.6666666667,0.9171518878],[0.6708333333,0.9037270619],[0.6750000000,0.8892899902],[0.6791666667,0.8738633008],[0.6833333333,0.8574749656],[0.6875000000,0.8401579787],[0.6916666667,0.8219502453],[0.6958333333,0.8028941798],[0.7000000000,0.7830364456],[0.7041666667,0.7624277344],[0.7083333333,0.7411222520],[0.7125000000,0.7191776044],[0.7166666667,0.6966542563],[0.7208333333,0.6736152714],[0.7250000000,0.6501259629],[0.7291666667,0.6262533880],[0.7333333333,0.6020661121],[0.7375000000,0.5776338043],[0.7416666667,0.5530267796],[0.7458333333,0.5283156992],[0.7500000000,0.5035711751],[0.7541666667,0.4788634341],[0.7583333333,0.4542618347],[0.7625000000,0.4298347613],[0.7666666667,0.4056490351],[0.7708333333,0.3817697830],[0.7750000000,0.3582600107],[0.7791666667,0.3351803495],[0.7833333333,0.3125888445],[0.7875000000,0.2905406366],[0.7916666667,0.2690876955],[0.7958333333,0.2482787388],[0.8000000000,0.2281588906],[0.8041666667,0.2087696425],[0.8083333333,0.1901486315],[0.8125000000,0.1723295359],[0.8166666667,0.1553419918],[0.8208333333,0.1392115328],[0.8250000000,0.1239595144],[0.8291666667,0.1096030703],[0.8333333333,0.0961551918],[0.8375000000,0.0836246599],[0.8416666667,0.0720161369],[0.8458333333,0.0613302273],[0.8500000000,0.0515635598],[0.8541666667,0.0427088803],[0.8583333333,0.0347551990],[0.8625000000,0.0276878920],[0.8666666667,0.0214889271],[0.8708333333,0.0161369711],[0.8750000000,0.0116076130],[0.8791666667,0.0078735477],[0.8833333333,0.0049047927],[0.8875000000,0.0026688977],[0.8916666667,0.0011311782],[0.8958333333,0.0002549473],[0.9000000000,0.0000000000],[0.9041666667,0.0000000000],[0.9083333333,0.0000000000],[0.9125000000,0.0000000000],[0.9166666667,0.0000000000],[0.9208333333,0.0000000000],[0.9250000000,0.0000000000],[0.9291666667,0.0000000000],[0.9333333333,0.0000000000],[0.9375000000,0.0000000000],[0.9416666667,0.0000000000],[0.9458333333,0.0000000000],[0.9500000000,0.0000000000],[0.9541666667,0.0000000000],[0.9583333333,0.0000000000],[0.9625000000,0.0000000000],[0.9666666667,0.0000000000],[0.9708333333,0.0000000000],[0.9750000000,0.0000000000],[0.9791666667,0.0000000000],[0.9833333333,0.0000000000],[0.9875000000,0.0000000000],[0.9916666667,0.0000000000],[0.9958333333,0.0000000000]]] - sun_color: [[[0.5010435645,0.5818710306],[0.0433100000,0.8999700000],[0.8635787716,0.9130000000],[0.1785000000,0.8973600000],[0.8099800000,0.8651100000],[0.2360800000,0.7712700000],[0.6583432177,0.8485126184],[0.1266806142,0.9648102053],[0.9558541267,0.9090909091],[0.5568400771,0.7353760446]],[[0.5001318426,0.5160300000],[0.0572700000,0.6541600000],[0.2395000000,0.5976800000],[0.8104600000,0.6009000000],[0.6967400000,0.5483900000]],[[0.0862400000,0.4257800000],[0.4955600000,0.4033000000],[0.8234200000,0.4340200000]]] - sun_azimuth: [[[0.5000000000,0.4972222222]]] - sun_altitude: [[[0.5000000000,0.9555555556]]] - extinction: [[[0.4913294798,0.6378830084]]] - volumetrics: - fog_ramp_size: [[[0.5510597303,0.7409470752]]] - fog_color: [[[0.5827924660,0.6183844011],[0.7616411304,0.9173984431],[0.1996161228,0.4985337243],[0.6696215593,0.5832754719],[0.5183044316,0.6629526462]],[[0.4922410956,0.5637316326],[0.8541266795,0.4985337243],[0.2168905950,0.4809384164],[0.3666026871,0.5571847507],[0.7552986513,0.5738161560],[0.5838150289,0.6880222841]],[[0.5250555661,0.9247910863],[0.8598848369,0.5249266862],[0.2168905950,0.5278592375],[0.3666026871,0.7008797654],[0.7620183506,0.4861173510],[0.5741811175,0.9972144846]]] - fog_brightness: [[[0.6637912862,0.3537604457],[0.0000000000,0.0000000000],[1.0000000000,0.0000000000],[0.2708922740,0.3198196359],[0.1727447217,0.1026392962],[0.1401151631,0.0000000000],[0.8984426717,0.1352486133],[0.9232245681,0.0000000000],[0.7071290944,0.2228412257],[0.5491329479,0.3231197772]]] - fog_intensity: [[[0.5317919075,0.0083565460]]] + +# Render Pipeline Time Of Day Configuration +# Instead of editing this file, prefer to use the Time Of Day Editor +# Any formatting and comments will be lost + +control_points: + clouds: + cloud_brightness: [[[0.4558541267,0.9574780059],[0.2744727256,0.8944309678],[0.1938559693,0.5249266862],[0.8905932438,0.2375366569],[0.6429901728,0.9589492375],[0.7581600000,0.8914912023],[0.2226478119,0.7859268915],[0.8406909789,0.4897360704],[0.5451055662,0.9618768328],[0.9462571977,0.0000000000],[1.0000000000,0.0000000000],[0.3646833013,0.9472140762],[0.3186180422,0.9325513197],[0.0823264879,0.0000000000]]] + color_correction: + camera_iso: [[[0.4708024067,0.2757660168]]] + camera_shutter: [[[0.5134061147,0.0552217053]]] + camera_aperture: [[[0.5076849397,0.3299161078]]] + env_probes: + ambient_scale: [[[0.5038535646,0.3091922005],[0.1368015414,0.3621169916],[0.8420038536,0.3983286908]]] + sky_ambient_scale: [[[0.4836223507,0.0974930362],[0.8497109827,0.0000000000],[0.0867052023,0.0027855153]]] + sun_ambient_scale: [[[0.4913294797,0.0584958217],[0.8612716763,0.0306406685],[0.1425818882,0.0445682451]]] + scattering: + sun_intensity: [[[0.0000000000,0.0000000000],[0.0041666667,0.0000000000],[0.0083333333,0.0000000000],[0.0125000000,0.0000000000],[0.0166666667,0.0000000000],[0.0208333333,0.0000000000],[0.0250000000,0.0000000000],[0.0291666667,0.0000000000],[0.0333333333,0.0000000000],[0.0375000000,0.0000000000],[0.0416666667,0.0000000000],[0.0458333333,0.0000000000],[0.0500000000,0.0000000000],[0.0541666667,0.0000000000],[0.0583333333,0.0000000000],[0.0625000000,0.0000000000],[0.0666666667,0.0000000000],[0.0708333333,0.0000000000],[0.0750000000,0.0000000000],[0.0791666667,0.0000000000],[0.0833333333,0.0000000000],[0.0875000000,0.0000000000],[0.0916666667,0.0000000000],[0.0958333333,0.0000000000],[0.1000000000,0.0000000000],[0.1041666667,0.0000000000],[0.1083333333,0.0000000000],[0.1125000000,0.0000000000],[0.1166666667,0.0000000000],[0.1208333333,0.0000000000],[0.1250000000,0.0000000000],[0.1291666667,0.0000000000],[0.1333333333,0.0000000000],[0.1375000000,0.0000000000],[0.1416666667,0.0000000000],[0.1458333333,0.0000000000],[0.1500000000,0.0000000000],[0.1541666667,0.0000000000],[0.1583333333,0.0000028805],[0.1625000000,0.0003577724],[0.1666666667,0.0013331400],[0.1708333333,0.0029671803],[0.1750000000,0.0052963381],[0.1791666667,0.0083550556],[0.1833333333,0.0121755589],[0.1875000000,0.0167876159],[0.1916666667,0.0222183530],[0.1958333333,0.0284919947],[0.2000000000,0.0356297193],[0.2041666667,0.0436494349],[0.2083333333,0.0525656099],[0.2125000000,0.0623891610],[0.2166666667,0.0731272461],[0.2208333333,0.0847831708],[0.2250000000,0.0973563167],[0.2291666667,0.1108419698],[0.2333333333,0.1252313631],[0.2375000000,0.1405115250],[0.2416666667,0.1566653434],[0.2458333333,0.1736715009],[0.2500000000,0.1915046014],[0.2541666667,0.2101350464],[0.2583333333,0.2295292930],[0.2625000000,0.2496498145],[0.2666666667,0.2704552670],[0.2708333333,0.2919006662],[0.2750000000,0.3139375192],[0.2791666667,0.3365139497],[0.2833333333,0.3595750662],[0.2875000000,0.3830630359],[0.2916666667,0.4069173972],[0.2958333333,0.4310753462],[0.3000000000,0.4554720417],[0.3041666667,0.4800408236],[0.3083333333,0.5047136020],[0.3125000000,0.5294212108],[0.3166666667,0.5540936424],[0.3208333333,0.5786605298],[0.3250000000,0.6030514553],[0.3291666667,0.6271963182],[0.3333333333,0.6510256858],[0.3375000000,0.6744711982],[0.3416666667,0.6974659988],[0.3458333333,0.7199450163],[0.3500000000,0.7418453485],[0.3541666667,0.7631067095],[0.3583333333,0.7836717291],[0.3625000000,0.8034862953],[0.3666666667,0.8224999302],[0.3708333333,0.8406661079],[0.3750000000,0.8579425235],[0.3791666667,0.8742914270],[0.3833333333,0.8896799131],[0.3875000000,0.9040801386],[0.3916666667,0.9174695289],[0.3958333333,0.9298310650],[0.4000000000,0.9411533765],[0.4041666667,0.9514309312],[0.4083333333,0.9606641691],[0.4125000000,0.9688595571],[0.4166666667,0.9760296330],[0.4208333333,0.9821930708],[0.4250000000,0.9873746114],[0.4291666667,0.9916050060],[0.4333333333,0.9949209310],[0.4375000000,0.9973647924],[0.4416666667,0.9989845508],[0.4458333333,0.9998334497],[0.4500000000,0.9999696949],[0.4541666667,0.9994560801],[0.4583333333,0.9983595429],[0.4625000000,0.9967506613],[0.4666666667,0.9947030614],[0.4708333333,0.9922927758],[0.4750000000,0.9895975125],[0.4791666667,0.9866958610],[0.4833333333,0.9836664262],[0.4875000000,0.9805868867],[0.4916666667,0.9775330316],[0.4958333333,0.9745777179],[0.5000000000,0.9717898417],[0.5041666667,0.9692332877],[0.5083333333,0.9669658924],[0.5125000000,0.9650384806],[0.5089595376,0.9690650222],[0.5208333333,0.9623666659],[0.5250000000,0.9616814371],[0.5291666667,0.9614534423],[0.5333333333,0.9616877089],[0.5375000000,0.9623790807],[0.5416666667,0.9635123329],[0.5458333333,0.9650624244],[0.5500000000,0.9669949804],[0.5541666667,0.9692669864],[0.5583333333,0.9718275065],[0.5625000000,0.9746185969],[0.5666666667,0.9775762863],[0.5708333333,0.9806315864],[0.5750000000,0.9837115661],[0.5791666667,0.9867403433],[0.5833333333,0.9896401655],[0.5875000000,0.9923323562],[0.5916666667,0.9947382579],[0.5958333333,0.9967800977],[0.6000000000,0.9983817820],[0.6041666667,0.9994696263],[0.6083333333,0.9999730028],[0.6125000000,0.9998249266],[0.6166666667,0.9989625601],[0.6208333333,0.9973276624],[0.6250000000,0.9948669567],[0.6291666667,0.9915324664],[0.6333333333,0.9872817545],[0.6375000000,0.9820781426],[0.6416666667,0.9758908775],[0.6458333333,0.9686952146],[0.6500000000,0.9604725211],[0.6541666667,0.9512102537],[0.6583333333,0.9409019858],[0.6625000000,0.9295473441],[0.6666666667,0.9171518878],[0.6708333333,0.9037270619],[0.6750000000,0.8892899902],[0.6791666667,0.8738633008],[0.6833333333,0.8574749656],[0.6875000000,0.8401579787],[0.6916666667,0.8219502453],[0.6958333333,0.8028941798],[0.7000000000,0.7830364456],[0.7041666667,0.7624277344],[0.7083333333,0.7411222520],[0.7125000000,0.7191776044],[0.7166666667,0.6966542563],[0.7208333333,0.6736152714],[0.7250000000,0.6501259629],[0.7291666667,0.6262533880],[0.7333333333,0.6020661121],[0.7375000000,0.5776338043],[0.7416666667,0.5530267796],[0.7458333333,0.5283156992],[0.7500000000,0.5035711751],[0.7541666667,0.4788634341],[0.7583333333,0.4542618347],[0.7625000000,0.4298347613],[0.7666666667,0.4056490351],[0.7708333333,0.3817697830],[0.7750000000,0.3582600107],[0.7791666667,0.3351803495],[0.7833333333,0.3125888445],[0.7875000000,0.2905406366],[0.7916666667,0.2690876955],[0.7958333333,0.2482787388],[0.8000000000,0.2281588906],[0.8041666667,0.2087696425],[0.8083333333,0.1901486315],[0.8125000000,0.1723295359],[0.8166666667,0.1553419918],[0.8208333333,0.1392115328],[0.8250000000,0.1239595144],[0.8291666667,0.1096030703],[0.8333333333,0.0961551918],[0.8375000000,0.0836246599],[0.8416666667,0.0720161369],[0.8458333333,0.0613302273],[0.8500000000,0.0515635598],[0.8541666667,0.0427088803],[0.8583333333,0.0347551990],[0.8625000000,0.0276878920],[0.8666666667,0.0214889271],[0.8708333333,0.0161369711],[0.8750000000,0.0116076130],[0.8791666667,0.0078735477],[0.8833333333,0.0049047927],[0.8875000000,0.0026688977],[0.8916666667,0.0011311782],[0.8958333333,0.0002549473],[0.9000000000,0.0000000000],[0.9041666667,0.0000000000],[0.9083333333,0.0000000000],[0.9125000000,0.0000000000],[0.9166666667,0.0000000000],[0.9208333333,0.0000000000],[0.9250000000,0.0000000000],[0.9291666667,0.0000000000],[0.9333333333,0.0000000000],[0.9375000000,0.0000000000],[0.9416666667,0.0000000000],[0.9458333333,0.0000000000],[0.9500000000,0.0000000000],[0.9541666667,0.0000000000],[0.9583333333,0.0000000000],[0.9625000000,0.0000000000],[0.9666666667,0.0000000000],[0.9708333333,0.0000000000],[0.9750000000,0.0000000000],[0.9791666667,0.0000000000],[0.9833333333,0.0000000000],[0.9875000000,0.0000000000],[0.9916666667,0.0000000000],[0.9958333333,0.0000000000]]] + sun_color: [[[0.5010435645,0.5818710306],[0.0433100000,0.8999700000],[0.8635787716,0.9130000000],[0.1785000000,0.8973600000],[0.8099800000,0.8651100000],[0.2360800000,0.7712700000],[0.6583432177,0.8485126184],[0.1266806142,0.9648102053],[0.9558541267,0.9090909091],[0.5568400771,0.7353760446]],[[0.5001318426,0.5160300000],[0.0572700000,0.6541600000],[0.2395000000,0.5976800000],[0.8104600000,0.6009000000],[0.6967400000,0.5483900000]],[[0.0862400000,0.4257800000],[0.4955600000,0.4033000000],[0.8234200000,0.4340200000]]] + sun_azimuth: [[[0.5000000000,0.4972222222]]] + sun_altitude: [[[0.5000000000,0.9555555556]]] + extinction: [[[0.4913294798,0.6378830084]]] + volumetrics: + fog_ramp_size: [[[0.5510597303,0.7409470752]]] + fog_color: [[[0.5827924660,0.6183844011],[0.7616411304,0.9173984431],[0.1996161228,0.4985337243],[0.6696215593,0.5832754719],[0.5183044316,0.6629526462]],[[0.4922410956,0.5637316326],[0.8541266795,0.4985337243],[0.2168905950,0.4809384164],[0.3666026871,0.5571847507],[0.7552986513,0.5738161560],[0.5838150289,0.6880222841]],[[0.5250555661,0.9247910863],[0.8598848369,0.5249266862],[0.2168905950,0.5278592375],[0.3666026871,0.7008797654],[0.7620183506,0.4861173510],[0.5741811175,0.9972144846]]] + fog_brightness: [[[0.6637912862,0.3537604457],[0.0000000000,0.0000000000],[1.0000000000,0.0000000000],[0.2708922740,0.3198196359],[0.1727447217,0.1026392962],[0.1401151631,0.0000000000],[0.8984426717,0.1352486133],[0.9232245681,0.0000000000],[0.7071290944,0.2228412257],[0.5491329479,0.3231197772]]] + fog_intensity: [[[0.5317919075,0.0083565460]]] diff --git a/RenderPipelineFile/config/debugging.yaml b/RenderPipelineFile/config/debugging.yaml index a4787465..3fdf4835 100644 --- a/RenderPipelineFile/config/debugging.yaml +++ b/RenderPipelineFile/config/debugging.yaml @@ -1,30 +1,30 @@ - -# This file contains all available debug modes. -# You can add or remove debug modes here, aswell as -# disable or enable them, whatever fits to you. - -# NOTICE: This file only has effect if debugging is enabled -# in the pipeline.yaml (settings.pipeline.display_debuger == True) - -# Usually you don't have to touch this file, except when developing -# new plugins or adding new features. - -render_modes: - - { name: "Shading Model", key: "SHADING_MODEL" } - - { name: "Diffuse", key: "DIFFUSE" } - - { name: "Roughness", key: "ROUGHNESS" } - - { name: "Specular", key: "SPECULAR" } - - { name: "Normal", key: "NORMAL" } - - { name: "Metallic", key: "METALLIC" } - - { name: "Translucency", key: "TRANSLUCENCY" } - - { name: "PSSM Splits", key: "PSSM_SPLITS", requires: "pssm", cxx_only: true } - - { name: "Ambient Occlusion", key: "OCCLUSION", requires: "ao" } - - { name: "Sky AO", key: "SKY_AO", requires: "sky_ao" } - - { name: "Scattering", key: "SCATTERING", requires: "scattering" } - - { name: "Velocity", key: "VELOCITY" } - - { name: "Diffuse Ambient", key: "DIFFUSE_AMBIENT" } - - { name: "Specular Ambient", key: "SPECULAR_AMBIENT" } - - { name: "Luminance", key: "LUMINANCE", special: true } - - { name: "Probe Count", key: "ENVPROBE_COUNT", requires: "env_probes" } - - { name: "Light Count", key: "LIGHT_COUNT" } - - { name: "Light Tiles", key: "LIGHT_TILES", special: true } + +# This file contains all available debug modes. +# You can add or remove debug modes here, aswell as +# disable or enable them, whatever fits to you. + +# NOTICE: This file only has effect if debugging is enabled +# in the pipeline.yaml (settings.pipeline.display_debuger == True) + +# Usually you don't have to touch this file, except when developing +# new plugins or adding new features. + +render_modes: + - { name: "Shading Model", key: "SHADING_MODEL" } + - { name: "Diffuse", key: "DIFFUSE" } + - { name: "Roughness", key: "ROUGHNESS" } + - { name: "Specular", key: "SPECULAR" } + - { name: "Normal", key: "NORMAL" } + - { name: "Metallic", key: "METALLIC" } + - { name: "Translucency", key: "TRANSLUCENCY" } + - { name: "PSSM Splits", key: "PSSM_SPLITS", requires: "pssm", cxx_only: true } + - { name: "Ambient Occlusion", key: "OCCLUSION", requires: "ao" } + - { name: "Sky AO", key: "SKY_AO", requires: "sky_ao" } + - { name: "Scattering", key: "SCATTERING", requires: "scattering" } + - { name: "Velocity", key: "VELOCITY" } + - { name: "Diffuse Ambient", key: "DIFFUSE_AMBIENT" } + - { name: "Specular Ambient", key: "SPECULAR_AMBIENT" } + - { name: "Luminance", key: "LUMINANCE", special: true } + - { name: "Probe Count", key: "ENVPROBE_COUNT", requires: "env_probes" } + - { name: "Light Count", key: "LIGHT_COUNT" } + - { name: "Light Tiles", key: "LIGHT_TILES", special: true } diff --git a/RenderPipelineFile/config/pipeline.yaml b/RenderPipelineFile/config/pipeline.yaml index d060d12a..5f0036a6 100644 --- a/RenderPipelineFile/config/pipeline.yaml +++ b/RenderPipelineFile/config/pipeline.yaml @@ -1,82 +1,82 @@ -# This file stores internal settings of the pipeline. It does not contain the -# plugin settings, but just the basic settings of the internal pipeline components. -pipeline: - - # This controls whether to show or hide the onscreen debugger. Not showing - # it will also disable the hotkeys, and give a small performance boost. - # Most likely you also don't want to show it in your own game, so set - # it to false in that case. - display_debugger: False - - # Affects which debugging information is displayed. If this is set to false, - # only frame time is displayed, otherwise much more information is visible. - # Has no effect when display_debugger is set to false. - advanced_debugging_info: False - - # Whether to use the GL_R11F_G11F_B10F texture format to save memory - # and bandwidth. Usually you want to enable this, however it can - # cause banding sometimes, in which case you can disable this setting. - use_r11_g11_b10: false - - # Enables to render at a higher or lower resolution than the window size. - # A value of 2.0 for example renders at twice the resolution (supoersampling) - # whereas a value of 0.5 would render at half resolution. - resolution_scale: 1.0 - - # Whether to render in a special reference mode, which displays the - # environment map as a background, and disable special effects like color - # grading and so on. This is used by the pathtracing reference. - reference_mode: false - -# This are the settings affecting the lighting part of the pipeline, -# including builtin shadows and lights. -lighting: - - # The pipeline uses clustered deferred shading, this means that the - # screen gets divided into tiles, and for each tile, the lights affecting - # that tile are accumulated. You can adjust the tile size here (in pixels), - # optimal is a tile size which is not too big (to avoid unecessary shading), - # but also not too small (to avoid excessive culling). - culling_grid_size_x: 24 - culling_grid_size_y: 16 - - # The view frustum is additionally divived into slices, to be able to do - # better culling. If you use a higher amount of slices, the culling will - # get more exact, but also more expensive. You have to find the optimal - # size depending on your scene. - culling_grid_slices: 32 - - # This controls the maximum culling distance in world space. After this - # distance, no lights are rendered anymore. If you choose a lower - # distance, this *can* positively impact performance, but you should not - # set it too low, to avoid getting artifacts. - culling_max_distance: 500.0 - - # Controls the size of a slice in culling. Lower values might produce - # better performance for less amount of lights, but higher values should - # be used when using many lights, e.g. > 1024, to get better coherency. - culling_slice_width: 2048 - - # Controls the maximum amount of lights for each cell. If this value - # is set too low, you might get artifacts when having many lights. - # In general, try to set this value as low as possible without getting - # artifacts - max_lights_per_cell: 64 - -shadows: - - # The size of the global shadow atlas, used for point and spot light - # shadows. This should be a power of 2. - atlas_size: 4096 - - # Maximum of shadow updates which may occur at one time. All updates - # which are beyond that count will get delayed to the next frame. - # If you set this too low, artifacts may occur because of shadows not - # getting updated fast enough. However, this also affects the performance - # quite a bit, since for every shadow map a part of the scene has - # to get re-rendered. - max_updates: 16 - - # Sets the maximum distance until which shadows are updated. If a shadow - # source is further away, it will no longer recieve updates +# This file stores internal settings of the pipeline. It does not contain the +# plugin settings, but just the basic settings of the internal pipeline components. +pipeline: + + # This controls whether to show or hide the onscreen debugger. Not showing + # it will also disable the hotkeys, and give a small performance boost. + # Most likely you also don't want to show it in your own game, so set + # it to false in that case. + display_debugger: False + + # Affects which debugging information is displayed. If this is set to false, + # only frame time is displayed, otherwise much more information is visible. + # Has no effect when display_debugger is set to false. + advanced_debugging_info: False + + # Whether to use the GL_R11F_G11F_B10F texture format to save memory + # and bandwidth. Usually you want to enable this, however it can + # cause banding sometimes, in which case you can disable this setting. + use_r11_g11_b10: false + + # Enables to render at a higher or lower resolution than the window size. + # A value of 2.0 for example renders at twice the resolution (supoersampling) + # whereas a value of 0.5 would render at half resolution. + resolution_scale: 1.0 + + # Whether to render in a special reference mode, which displays the + # environment map as a background, and disable special effects like color + # grading and so on. This is used by the pathtracing reference. + reference_mode: false + +# This are the settings affecting the lighting part of the pipeline, +# including builtin shadows and lights. +lighting: + + # The pipeline uses clustered deferred shading, this means that the + # screen gets divided into tiles, and for each tile, the lights affecting + # that tile are accumulated. You can adjust the tile size here (in pixels), + # optimal is a tile size which is not too big (to avoid unecessary shading), + # but also not too small (to avoid excessive culling). + culling_grid_size_x: 24 + culling_grid_size_y: 16 + + # The view frustum is additionally divived into slices, to be able to do + # better culling. If you use a higher amount of slices, the culling will + # get more exact, but also more expensive. You have to find the optimal + # size depending on your scene. + culling_grid_slices: 32 + + # This controls the maximum culling distance in world space. After this + # distance, no lights are rendered anymore. If you choose a lower + # distance, this *can* positively impact performance, but you should not + # set it too low, to avoid getting artifacts. + culling_max_distance: 500.0 + + # Controls the size of a slice in culling. Lower values might produce + # better performance for less amount of lights, but higher values should + # be used when using many lights, e.g. > 1024, to get better coherency. + culling_slice_width: 2048 + + # Controls the maximum amount of lights for each cell. If this value + # is set too low, you might get artifacts when having many lights. + # In general, try to set this value as low as possible without getting + # artifacts + max_lights_per_cell: 64 + +shadows: + + # The size of the global shadow atlas, used for point and spot light + # shadows. This should be a power of 2. + atlas_size: 4096 + + # Maximum of shadow updates which may occur at one time. All updates + # which are beyond that count will get delayed to the next frame. + # If you set this too low, artifacts may occur because of shadows not + # getting updated fast enough. However, this also affects the performance + # quite a bit, since for every shadow map a part of the scene has + # to get re-rendered. + max_updates: 16 + + # Sets the maximum distance until which shadows are updated. If a shadow + # source is further away, it will no longer recieve updates max_update_distance: 300.0 \ No newline at end of file diff --git a/RenderPipelineFile/config/plugins.yaml b/RenderPipelineFile/config/plugins.yaml index 717e6e6d..4b0f5d64 100644 --- a/RenderPipelineFile/config/plugins.yaml +++ b/RenderPipelineFile/config/plugins.yaml @@ -1,199 +1,199 @@ - -# Render Pipeline Plugin Configuration -# Instead of editing this file, prefer to use the Plugin Configurator -# Any formatting and comments will be lost - -enabled: - - ao - - bloom - - color_correction - - env_probes - - forward_shading - - motion_blur - - pssm - - scattering - - skin_shading - - sky_ao - - smaa - - ssr - - clouds - #- dof - #- fxaa - #- volumetrics - #- vxgi - - -overrides: - ao: - blur_quality: LOW - blur_normal_factor: 2.97 - blur_depth_factor: 0.88158 - occlusion_strength: 2.19 - clip_length: 4 - technique: SSAO - ssao_sample_radius: 95.29 - ssao_sequence: halton_3D_8 - ssao_bias: 0.0143 - ssao_max_distance: 7.5 - hbao_sample_radius: 255.0 - hbao_ray_count: 4 - hbao_ray_steps: 3 - hbao_tangent_bias: 0.64997 - hbao_max_distance: 11.5 - ssvo_sequence: halton_2D_8 - ssvo_sphere_radius: 18.0 - ssvo_max_distance: 3.19 - alchemy_sample_radius: 38.86 - alchemy_sequence: halton_2D_8 - alchemy_max_distance: 5.86 - ue4ao_sample_radius: 50.14286 - ue4ao_sample_sequence: halton_2D_8 - ue4ao_max_distance: 1.47 - - bloom: - num_mipmaps: 6 - bloom_strength: 0.8003 - remove_fireflies: False - lens_dirt_factor: 0.0 - - clouds: - raymarch_steps: 160 - - color_correction: - tonemap_operator: optimized - reinhard_version: rgb - exponential_factor: 1.23 - uc2t_shoulder_strength: 0.3352 - uc2t_linear_strength: 0.5339 - uc2t_linear_angle: 0.1797 - uc2t_toe_strength: 0.3919 - uc2t_toe_numerator: 0.0029 - uc2t_toe_denumerator: 0.2787 - uc2t_reference_white: 10.05 - vignette_strength: 0.1286 - film_grain_strength: 0.1286 - color_lut: film_luts/default_lut.png - use_chromatic_aberration: True - chromatic_aberration_strength: 0.019 - chromatic_aberration_samples: 2 - manual_camera_parameters: False - min_exposure_value: 0.01 - max_exposure_value: 1.0 - exposure_scale: 1.0 - brightness_adaption_rate: 3.6 - darkness_adaption_rate: 0.7 - use_sharpen: True - sharpen_strength: 0.5 - sharpen_twice: False - - dof: - focal_point: 5 - focal_size: 1 - blur_strength: 1 - near_blur_strength: 1 - - env_probes: - probe_resolution: 128 - diffuse_probe_resolution: 2 - max_probes: 16 - max_probes_per_cell: 3 - - forward_shading: - - fxaa: - quality: ultra - subpixel_quality: 0.5 - edge_threshold: 0.166 - min_threshold: 0.833 - - motion_blur: - num_camera_samples: 6 - camera_blur_factor: 0.4 - enable_object_blur: False - blur_factor: 0.5 - tile_size: 32 - max_blur_radius: 10.0 - num_samples: 12 - - pssm: - max_distance: 50.0 - logarithmic_factor: 3.0 - sun_distance: 100.0 - split_count: 4 - resolution: 1024 - border_bias: 0.058 - use_pcf: True - filter_sequence: halton_2D_32 - filter_radius: 0.7 - fixed_bias: 0.11429 - slope_bias: 0.0 - normal_bias: 0.67 - use_pcss: False - pcss_sequence: halton_2D_16 - pcss_penumbra_size: 2.38 - pcss_min_penumbra_size: 7.0 - use_distant_shadows: True - dist_shadow_resolution: 4096 - dist_shadow_clipsize: 400.0 - dist_shadow_sundist: 300.0 - scene_shadow_resolution: 512 - scene_shadow_sundist: 300.0 - - scattering: - scattering_method: eric_bruneton - ground_reflectance: 0.1231 - rayleigh_factor: 0.5 - rayleigh_height_scale: 8.0 - mie_height_scale: 1.3 - mie_phase_factor: 0.3 - beta_mie_scattering: 4.0 - enable_godrays: False - atmosphere_start: 549.61 - - skin_shading: - quality: medium - blur_scale: 0.43 - - sky_ao: - sample_radius: 17.17 - max_radius: 500.0 - resolution: 1024 - sample_sequence: poisson_2D_32 - ao_multiplier: 0.83 - ao_bias: 0.0 - blend_factor: 0.01 - capture_height: 568.75 - - smaa: - use_reprojection: True - smaa_quality: ultra - jitter_pattern: halton8 - history_length: 8 - jitter_amount: 0.16143 - - ssr: - effect_scale: 1.0 - trace_steps: 64 - history_length: 16 - abort_on_object_infront: True - intial_bias: 0.1 - hit_tolerance: 0.1 - roughness_fade: 0.72 - skip_invalid_samples: False - border_fade: 0.005 - - volumetrics: - enable_volumetric_shadows: True - volumetric_shadow_intensity: 8.31 - volumetric_shadow_brightness: 1.66 - volumetric_shadow_pow: 1.15999 - volumetric_max_distance: 79.41429 - volumetric_shadow_fadein_distance: 9.49 - volumetric_num_steps: 128 - - vxgi: - grid_resolution: 256 - grid_ws_size: 100.0 - diffuse_cone_steps: 32 - specular_cone_steps: 150 - + +# Render Pipeline Plugin Configuration +# Instead of editing this file, prefer to use the Plugin Configurator +# Any formatting and comments will be lost + +enabled: + - ao + - bloom + - color_correction + - env_probes + - forward_shading + - motion_blur + - pssm + - scattering + - skin_shading + - sky_ao + - smaa + - ssr + - clouds + #- dof + #- fxaa + #- volumetrics + #- vxgi + + +overrides: + ao: + blur_quality: LOW + blur_normal_factor: 2.97 + blur_depth_factor: 0.88158 + occlusion_strength: 2.19 + clip_length: 4 + technique: SSAO + ssao_sample_radius: 95.29 + ssao_sequence: halton_3D_8 + ssao_bias: 0.0143 + ssao_max_distance: 7.5 + hbao_sample_radius: 255.0 + hbao_ray_count: 4 + hbao_ray_steps: 3 + hbao_tangent_bias: 0.64997 + hbao_max_distance: 11.5 + ssvo_sequence: halton_2D_8 + ssvo_sphere_radius: 18.0 + ssvo_max_distance: 3.19 + alchemy_sample_radius: 38.86 + alchemy_sequence: halton_2D_8 + alchemy_max_distance: 5.86 + ue4ao_sample_radius: 50.14286 + ue4ao_sample_sequence: halton_2D_8 + ue4ao_max_distance: 1.47 + + bloom: + num_mipmaps: 6 + bloom_strength: 0.8003 + remove_fireflies: False + lens_dirt_factor: 0.0 + + clouds: + raymarch_steps: 160 + + color_correction: + tonemap_operator: optimized + reinhard_version: rgb + exponential_factor: 1.23 + uc2t_shoulder_strength: 0.3352 + uc2t_linear_strength: 0.5339 + uc2t_linear_angle: 0.1797 + uc2t_toe_strength: 0.3919 + uc2t_toe_numerator: 0.0029 + uc2t_toe_denumerator: 0.2787 + uc2t_reference_white: 10.05 + vignette_strength: 0.1286 + film_grain_strength: 0.1286 + color_lut: film_luts/default_lut.png + use_chromatic_aberration: True + chromatic_aberration_strength: 0.019 + chromatic_aberration_samples: 2 + manual_camera_parameters: False + min_exposure_value: 0.01 + max_exposure_value: 1.0 + exposure_scale: 1.0 + brightness_adaption_rate: 3.6 + darkness_adaption_rate: 0.7 + use_sharpen: True + sharpen_strength: 0.5 + sharpen_twice: False + + dof: + focal_point: 5 + focal_size: 1 + blur_strength: 1 + near_blur_strength: 1 + + env_probes: + probe_resolution: 128 + diffuse_probe_resolution: 2 + max_probes: 16 + max_probes_per_cell: 3 + + forward_shading: + + fxaa: + quality: ultra + subpixel_quality: 0.5 + edge_threshold: 0.166 + min_threshold: 0.833 + + motion_blur: + num_camera_samples: 6 + camera_blur_factor: 0.4 + enable_object_blur: False + blur_factor: 0.5 + tile_size: 32 + max_blur_radius: 10.0 + num_samples: 12 + + pssm: + max_distance: 50.0 + logarithmic_factor: 3.0 + sun_distance: 100.0 + split_count: 4 + resolution: 1024 + border_bias: 0.058 + use_pcf: True + filter_sequence: halton_2D_32 + filter_radius: 0.7 + fixed_bias: 0.11429 + slope_bias: 0.0 + normal_bias: 0.67 + use_pcss: False + pcss_sequence: halton_2D_16 + pcss_penumbra_size: 2.38 + pcss_min_penumbra_size: 7.0 + use_distant_shadows: True + dist_shadow_resolution: 4096 + dist_shadow_clipsize: 400.0 + dist_shadow_sundist: 300.0 + scene_shadow_resolution: 512 + scene_shadow_sundist: 300.0 + + scattering: + scattering_method: eric_bruneton + ground_reflectance: 0.1231 + rayleigh_factor: 0.5 + rayleigh_height_scale: 8.0 + mie_height_scale: 1.3 + mie_phase_factor: 0.3 + beta_mie_scattering: 4.0 + enable_godrays: False + atmosphere_start: 549.61 + + skin_shading: + quality: medium + blur_scale: 0.43 + + sky_ao: + sample_radius: 17.17 + max_radius: 500.0 + resolution: 1024 + sample_sequence: poisson_2D_32 + ao_multiplier: 0.83 + ao_bias: 0.0 + blend_factor: 0.01 + capture_height: 568.75 + + smaa: + use_reprojection: True + smaa_quality: ultra + jitter_pattern: halton8 + history_length: 8 + jitter_amount: 0.16143 + + ssr: + effect_scale: 1.0 + trace_steps: 64 + history_length: 16 + abort_on_object_infront: True + intial_bias: 0.1 + hit_tolerance: 0.1 + roughness_fade: 0.72 + skip_invalid_samples: False + border_fade: 0.005 + + volumetrics: + enable_volumetric_shadows: True + volumetric_shadow_intensity: 8.31 + volumetric_shadow_brightness: 1.66 + volumetric_shadow_pow: 1.15999 + volumetric_max_distance: 79.41429 + volumetric_shadow_fadein_distance: 9.49 + volumetric_num_steps: 128 + + vxgi: + grid_resolution: 256 + grid_ws_size: 100.0 + diffuse_cone_steps: 32 + specular_cone_steps: 150 + diff --git a/RenderPipelineFile/config/stages.yaml b/RenderPipelineFile/config/stages.yaml index 7366794f..a6a8382b 100644 --- a/RenderPipelineFile/config/stages.yaml +++ b/RenderPipelineFile/config/stages.yaml @@ -1,67 +1,67 @@ - -# This file controls the order of all render stages. - -# Usually you do not have to modify this, except if you are adding new plugins. -# If you move stages, you should know what you do. - -# Commenting out a stage won't disable it, but instead produce an error when -# it is created. - -global_stage_order: - - # Shadows, Environment and Voxelization - - PSSMShadowStage - - PSSMDistShadowStage - - PSSMSceneShadowStage - - ScatteringEnvmapStage - - SkyAOCaptureStage - - EnvironmentCaptureStage - - VoxelizationStage - - # Main scene - - GBufferStage - - ShadowStage - - DownscaleZStage - - CombineVelocityStage - - # Light(-culling) - - FlagUsedCellsStage - - CollectUsedCellsStage - - CullLightsStage - - CullProbesStage - - # Plugins - - CloudVoxelStage - - AOStage - - SkyAOStage - - ApplyLightsStage - - PSSMStage - - ScatteringStage - - ApplyEnvprobesStage - - VXGIStage - - SSRStage - - - AmbientStage - - ForwardStage - - - ApplyCloudsStage - - GodrayStage - - VolumetricsStage - - AutoExposureStage - - ManualExposureStage - - # Post AA & Effects - - BloomStage - - TonemappingStage - - SMAAStage - - FXAAStage - - DoFStage - - MotionBlurStage - - SkinShadingStage - - SharpenStage - - ColorCorrectionStage - - # Finishing stages, do not insert anything below - - UpscaleStage - - FinalStage - - UpdatePreviousPipesStage + +# This file controls the order of all render stages. + +# Usually you do not have to modify this, except if you are adding new plugins. +# If you move stages, you should know what you do. + +# Commenting out a stage won't disable it, but instead produce an error when +# it is created. + +global_stage_order: + + # Shadows, Environment and Voxelization + - PSSMShadowStage + - PSSMDistShadowStage + - PSSMSceneShadowStage + - ScatteringEnvmapStage + - SkyAOCaptureStage + - EnvironmentCaptureStage + - VoxelizationStage + + # Main scene + - GBufferStage + - ShadowStage + - DownscaleZStage + - CombineVelocityStage + + # Light(-culling) + - FlagUsedCellsStage + - CollectUsedCellsStage + - CullLightsStage + - CullProbesStage + + # Plugins + - CloudVoxelStage + - AOStage + - SkyAOStage + - ApplyLightsStage + - PSSMStage + - ScatteringStage + - ApplyEnvprobesStage + - VXGIStage + - SSRStage + + - AmbientStage + - ForwardStage + + - ApplyCloudsStage + - GodrayStage + - VolumetricsStage + - AutoExposureStage + - ManualExposureStage + + # Post AA & Effects + - BloomStage + - TonemappingStage + - SMAAStage + - FXAAStage + - DoFStage + - MotionBlurStage + - SkinShadingStage + - SharpenStage + - ColorCorrectionStage + + # Finishing stages, do not insert anything below + - UpscaleStage + - FinalStage + - UpdatePreviousPipesStage diff --git a/RenderPipelineFile/config/task-scheduler.yaml b/RenderPipelineFile/config/task-scheduler.yaml index 23d6c7f3..b5007a59 100644 --- a/RenderPipelineFile/config/task-scheduler.yaml +++ b/RenderPipelineFile/config/task-scheduler.yaml @@ -1,33 +1,33 @@ - -# This file controls which tasks are allowed to run each frame. -# Usually you do not have to edit this file, except when developing plugins. - -frame_cycles: !!omap - - - frame1: - - envprobes_select_and_cull - - pssm_scene_shadows - - - frame2: - - envprobes_capture_envmap_face0 - - pssm_distant_shadows - - - frame3: - - envprobes_capture_envmap_face1 - - pssm_convert_distant_to_esm - - - frame4: - - envprobes_capture_envmap_face2 - - pssm_blur_distant_vert - - - frame5: - - pssm_blur_distant_horiz - - envprobes_capture_envmap_face3 - - envprobes_capture_envmap_face4 - - - frame6: - - envprobes_capture_envmap_face5 - - scattering_update_envmap - - - frame7: - - envprobes_filter_and_store_envmap + +# This file controls which tasks are allowed to run each frame. +# Usually you do not have to edit this file, except when developing plugins. + +frame_cycles: !!omap + + - frame1: + - envprobes_select_and_cull + - pssm_scene_shadows + + - frame2: + - envprobes_capture_envmap_face0 + - pssm_distant_shadows + + - frame3: + - envprobes_capture_envmap_face1 + - pssm_convert_distant_to_esm + + - frame4: + - envprobes_capture_envmap_face2 + - pssm_blur_distant_vert + + - frame5: + - pssm_blur_distant_horiz + - envprobes_capture_envmap_face3 + - envprobes_capture_envmap_face4 + + - frame6: + - envprobes_capture_envmap_face5 + - scattering_update_envmap + + - frame7: + - envprobes_filter_and_store_envmap diff --git a/RenderPipelineFile/data/Materials/GroundMaterial.yaml b/RenderPipelineFile/data/Materials/GroundMaterial.yaml index 22b2191b..3e3ab4a2 100644 --- a/RenderPipelineFile/data/Materials/GroundMaterial.yaml +++ b/RenderPipelineFile/data/Materials/GroundMaterial.yaml @@ -1,7 +1,7 @@ -material: - name: GroundMaterial - base_color: "../Textures/ground_diffuse.png" - normal: "../Textures/ground_normal.png" - roughness: 0.8 - metallic: 0.0 - two_sided: true +material: + name: GroundMaterial + base_color: "../Textures/ground_diffuse.png" + normal: "../Textures/ground_normal.png" + roughness: 0.8 + metallic: 0.0 + two_sided: true diff --git a/RenderPipelineFile/data/builtin_models/skybox/SOURCE.txt b/RenderPipelineFile/data/builtin_models/skybox/SOURCE.txt index 68971e96..ea53184d 100644 --- a/RenderPipelineFile/data/builtin_models/skybox/SOURCE.txt +++ b/RenderPipelineFile/data/builtin_models/skybox/SOURCE.txt @@ -1,16 +1,16 @@ - -The skydomes for the skybox were taken from: - - -Skybox.jpg: - -http://www.gravis.com.ua/gal/content/photoprint/sky/sk_21.jpg - - -Skybox2.jpg: - -http://www.artist-reference.com/wp-content/gallery/marlin-studios-gallery/sky-dome-panorma.jpg - - - -I couldn't find a direct license for those photos. If there is one, please contact me. + +The skydomes for the skybox were taken from: + + +Skybox.jpg: + +http://www.gravis.com.ua/gal/content/photoprint/sky/sk_21.jpg + + +Skybox2.jpg: + +http://www.artist-reference.com/wp-content/gallery/marlin-studios-gallery/sky-dome-panorma.jpg + + + +I couldn't find a direct license for those photos. If there is one, please contact me. diff --git a/RenderPipelineFile/data/default_cubemap/filter.compute.glsl b/RenderPipelineFile/data/default_cubemap/filter.compute.glsl index 892e8b63..ffb0878a 100644 --- a/RenderPipelineFile/data/default_cubemap/filter.compute.glsl +++ b/RenderPipelineFile/data/default_cubemap/filter.compute.glsl @@ -1,116 +1,116 @@ -#version 430 - -// Shader to pre-filter the cubemap using importance sampling - -layout (local_size_x = 16, local_size_y = 16, local_size_z = 1) in; - - -#define M_PI 3.1415926535897932384626433 - -vec3 get_transformed_coord(vec2 coord, uint face) { - float f = 1.0; - switch (face) { - case 1: return vec3(-f, coord); - case 2: return vec3(coord, -f); - case 0: return vec3(f, -coord.x, coord.y); - case 3: return vec3(coord.xy * vec2(1, -1), f); - case 4: return vec3(coord.x, f, coord.y); - case 5: return vec3(-coord.x, -f, coord.y); - } - return vec3(0); -} - -// From: -// http://www.trentreed.net/blog/physically-based-shading-and-image-based-lighting/ -vec2 hammersley(uint i, uint N) -{ - return vec2(float(i) / float(N), float(bitfieldReverse(i)) * 2.3283064365386963e-10); -} - -// From: -// http://www.gamedev.net/topic/655431-ibl-problem-with-consistency-using-ggx-anisotropy/ -vec3 importance_sample_ggx(vec2 xi, float roughness) -{ - float r_square = roughness * roughness; - float phi = 2 * M_PI * xi.x; - float cos_theta = sqrt((1 - xi.y) / (1 + (r_square * r_square - 1) * xi.y)); - float sin_theta = sqrt(1 - cos_theta * cos_theta); - - return vec3(sin_theta * cos(phi), sin_theta * sin(phi), cos_theta); -} - -// Converts a normalized spherical coordinate (r = 1) to cartesian coordinates -vec3 spherical_to_vector(float theta, float phi) { - float sin_theta = sin(theta); - return normalize(vec3( - sin_theta * cos(phi), - sin_theta * sin(phi), - cos(theta) - )); -} - -float brdf_distribution_ggx(float NxH, float roughness) { - float nxh_sq = NxH * NxH; - float tan_sq = (1 - nxh_sq) / nxh_sq; - float f = roughness / max(1e-10, nxh_sq * (roughness * roughness + tan_sq)); - return f * f / M_PI; -} - -// Finds a tangent and bitangent vector based on a given normal -void find_arbitrary_tangent(vec3 normal, out vec3 tangent, out vec3 bitangent) { - vec3 v0 = abs(normal.z) < 0.999 ? vec3(0, 0, 1) : vec3(0, 1, 0); - tangent = normalize(cross(v0, normal)); - bitangent = normalize(cross(tangent, normal)); -} - -vec3 transform_cubemap_coordinates(vec3 coord) { - return normalize(coord.xyz * vec3(1, -1, 1)); -} - -uniform samplerCube SourceTex; -uniform int currentSize; -uniform int currentMip; -uniform int currentFace; -layout(rgba16f) uniform imageCube DestTex; - -void main() { - ivec2 coord = ivec2(gl_GlobalInvocationID.xy); - - vec2 texcoord = vec2(coord + 0.5) / float(currentSize); - texcoord = texcoord * 2.0 - 1.0; - - vec3 n = get_transformed_coord(texcoord, currentFace); - n = normalize(n); - n = transform_cubemap_coordinates(n); - float roughness = clamp(float(currentMip) / 7.0, 0.001, 1.0); - // roughness *= roughness; - - vec3 tangent, binormal; - find_arbitrary_tangent(n, tangent, binormal); - - vec4 accum = vec4(0); - const uint num_samples = 512; - - // Ultra high quality, might cause TDR on low-end systems - // const uint num_samples = 4096; - for (uint i = 0; i < num_samples; ++i) { - vec2 xi = hammersley(i, num_samples); - vec3 r = importance_sample_ggx(xi, roughness); - vec3 h = normalize(r.x * tangent + r.y * binormal + r.z * n); - vec3 l = 2.0 * dot(n, h) * h - n; - - float NxL = clamp(dot(n, l), 0.0, 1.0); - float NxH = clamp(dot(n, h), 0.0, 1.0); - - vec3 sampled = textureLod(SourceTex, l, 0).rgb; - - float weight = 1; - weight = clamp(weight, 0.0, 1.0); - accum += vec4(sampled, 1) * weight; - } - - accum /= max(0.1, accum.w); - accum.xyz = pow(accum.xyz, vec3(2.2)); - - imageStore(DestTex, ivec3(coord, currentFace), vec4(accum.xyz, 1.0)); -} +#version 430 + +// Shader to pre-filter the cubemap using importance sampling + +layout (local_size_x = 16, local_size_y = 16, local_size_z = 1) in; + + +#define M_PI 3.1415926535897932384626433 + +vec3 get_transformed_coord(vec2 coord, uint face) { + float f = 1.0; + switch (face) { + case 1: return vec3(-f, coord); + case 2: return vec3(coord, -f); + case 0: return vec3(f, -coord.x, coord.y); + case 3: return vec3(coord.xy * vec2(1, -1), f); + case 4: return vec3(coord.x, f, coord.y); + case 5: return vec3(-coord.x, -f, coord.y); + } + return vec3(0); +} + +// From: +// http://www.trentreed.net/blog/physically-based-shading-and-image-based-lighting/ +vec2 hammersley(uint i, uint N) +{ + return vec2(float(i) / float(N), float(bitfieldReverse(i)) * 2.3283064365386963e-10); +} + +// From: +// http://www.gamedev.net/topic/655431-ibl-problem-with-consistency-using-ggx-anisotropy/ +vec3 importance_sample_ggx(vec2 xi, float roughness) +{ + float r_square = roughness * roughness; + float phi = 2 * M_PI * xi.x; + float cos_theta = sqrt((1 - xi.y) / (1 + (r_square * r_square - 1) * xi.y)); + float sin_theta = sqrt(1 - cos_theta * cos_theta); + + return vec3(sin_theta * cos(phi), sin_theta * sin(phi), cos_theta); +} + +// Converts a normalized spherical coordinate (r = 1) to cartesian coordinates +vec3 spherical_to_vector(float theta, float phi) { + float sin_theta = sin(theta); + return normalize(vec3( + sin_theta * cos(phi), + sin_theta * sin(phi), + cos(theta) + )); +} + +float brdf_distribution_ggx(float NxH, float roughness) { + float nxh_sq = NxH * NxH; + float tan_sq = (1 - nxh_sq) / nxh_sq; + float f = roughness / max(1e-10, nxh_sq * (roughness * roughness + tan_sq)); + return f * f / M_PI; +} + +// Finds a tangent and bitangent vector based on a given normal +void find_arbitrary_tangent(vec3 normal, out vec3 tangent, out vec3 bitangent) { + vec3 v0 = abs(normal.z) < 0.999 ? vec3(0, 0, 1) : vec3(0, 1, 0); + tangent = normalize(cross(v0, normal)); + bitangent = normalize(cross(tangent, normal)); +} + +vec3 transform_cubemap_coordinates(vec3 coord) { + return normalize(coord.xyz * vec3(1, -1, 1)); +} + +uniform samplerCube SourceTex; +uniform int currentSize; +uniform int currentMip; +uniform int currentFace; +layout(rgba16f) uniform imageCube DestTex; + +void main() { + ivec2 coord = ivec2(gl_GlobalInvocationID.xy); + + vec2 texcoord = vec2(coord + 0.5) / float(currentSize); + texcoord = texcoord * 2.0 - 1.0; + + vec3 n = get_transformed_coord(texcoord, currentFace); + n = normalize(n); + n = transform_cubemap_coordinates(n); + float roughness = clamp(float(currentMip) / 7.0, 0.001, 1.0); + // roughness *= roughness; + + vec3 tangent, binormal; + find_arbitrary_tangent(n, tangent, binormal); + + vec4 accum = vec4(0); + const uint num_samples = 512; + + // Ultra high quality, might cause TDR on low-end systems + // const uint num_samples = 4096; + for (uint i = 0; i < num_samples; ++i) { + vec2 xi = hammersley(i, num_samples); + vec3 r = importance_sample_ggx(xi, roughness); + vec3 h = normalize(r.x * tangent + r.y * binormal + r.z * n); + vec3 l = 2.0 * dot(n, h) * h - n; + + float NxL = clamp(dot(n, l), 0.0, 1.0); + float NxH = clamp(dot(n, h), 0.0, 1.0); + + vec3 sampled = textureLod(SourceTex, l, 0).rgb; + + float weight = 1; + weight = clamp(weight, 0.0, 1.0); + accum += vec4(sampled, 1) * weight; + } + + accum /= max(0.1, accum.w); + accum.xyz = pow(accum.xyz, vec3(2.2)); + + imageStore(DestTex, ivec3(coord, currentFace), vec4(accum.xyz, 1.0)); +} diff --git a/RenderPipelineFile/data/default_cubemap/filter.py b/RenderPipelineFile/data/default_cubemap/filter.py index c019112b..0decec58 100644 --- a/RenderPipelineFile/data/default_cubemap/filter.py +++ b/RenderPipelineFile/data/default_cubemap/filter.py @@ -1,106 +1,106 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import print_function, division - -import os -import shutil -from os.path import dirname, realpath -from panda3d.core import * -from direct.stdpy.file import isdir, isfile, join -from direct.showbase.ShowBase import ShowBase - -class Application(ShowBase): - def __init__(self): - load_prc_file_data("", """ - textures-power-2 none - window-type offscreen - win-size 100 100 - gl-coordinate-system default - notify-level-display error - print-pipe-types #f - gl-version 4 3 - """) - - ShowBase.__init__(self) - - base_path = realpath(dirname(__file__)) - os.chdir(base_path) - filter_dir = join(base_path, "tmp/") - if isdir(filter_dir): - shutil.rmtree(filter_dir) - os.makedirs(filter_dir) - - source_path = join(base_path, "source") - extension = ".jpg" - if isfile(join(source_path, "1.png")): - extension = ".png" - - cubemap = self.loader.loadCubeMap( - Filename.from_os_specific(join(source_path, "#" + extension))) - mipmap, size = -1, 1024 - - cshader = Shader.load_compute(Shader.SL_GLSL, "filter.compute.glsl") - - while size > 1: - size = size // 2 - mipmap += 1 - print("Filtering mipmap", mipmap) - - dest_cubemap = Texture("Dest") - dest_cubemap.setup_cube_map(size, Texture.T_float, Texture.F_rgba16) - node = NodePath("") - - for i in range(6): - node.set_shader(cshader) - node.set_shader_inputs( - SourceTex=cubemap, - DestTex=dest_cubemap, - currentSize=size, - currentMip=mipmap, - currentFace=i) - attr = node.get_attrib(ShaderAttrib) - self.graphicsEngine.dispatch_compute( - ( (size + 15) // 16, (size+15) // 16, 1), attr, self.win.gsg) - - print(" Extracting data ..") - - self.graphicsEngine.extract_texture_data(dest_cubemap, self.win.gsg) - - print(" Writing data ..") - dest_cubemap.write(join(filter_dir, "{}-#.png".format(mipmap)), 0, 0, True, False) - - - print("Reading in data back in ..") - tex = self.loader.loadCubeMap(Filename.from_os_specific(join(base_path, "tmp/#-#.png")), readMipmaps="True") - - print("Writing txo ..") - tex.write("cubemap.txo.pz") - - shutil.rmtree(join(base_path, "tmp")) - - -Application() +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import print_function, division + +import os +import shutil +from os.path import dirname, realpath +from panda3d.core import * +from direct.stdpy.file import isdir, isfile, join +from direct.showbase.ShowBase import ShowBase + +class Application(ShowBase): + def __init__(self): + load_prc_file_data("", """ + textures-power-2 none + window-type offscreen + win-size 100 100 + gl-coordinate-system default + notify-level-display error + print-pipe-types #f + gl-version 4 3 + """) + + ShowBase.__init__(self) + + base_path = realpath(dirname(__file__)) + os.chdir(base_path) + filter_dir = join(base_path, "tmp/") + if isdir(filter_dir): + shutil.rmtree(filter_dir) + os.makedirs(filter_dir) + + source_path = join(base_path, "source") + extension = ".jpg" + if isfile(join(source_path, "1.png")): + extension = ".png" + + cubemap = self.loader.loadCubeMap( + Filename.from_os_specific(join(source_path, "#" + extension))) + mipmap, size = -1, 1024 + + cshader = Shader.load_compute(Shader.SL_GLSL, "filter.compute.glsl") + + while size > 1: + size = size // 2 + mipmap += 1 + print("Filtering mipmap", mipmap) + + dest_cubemap = Texture("Dest") + dest_cubemap.setup_cube_map(size, Texture.T_float, Texture.F_rgba16) + node = NodePath("") + + for i in range(6): + node.set_shader(cshader) + node.set_shader_inputs( + SourceTex=cubemap, + DestTex=dest_cubemap, + currentSize=size, + currentMip=mipmap, + currentFace=i) + attr = node.get_attrib(ShaderAttrib) + self.graphicsEngine.dispatch_compute( + ( (size + 15) // 16, (size+15) // 16, 1), attr, self.win.gsg) + + print(" Extracting data ..") + + self.graphicsEngine.extract_texture_data(dest_cubemap, self.win.gsg) + + print(" Writing data ..") + dest_cubemap.write(join(filter_dir, "{}-#.png".format(mipmap)), 0, 0, True, False) + + + print("Reading in data back in ..") + tex = self.loader.loadCubeMap(Filename.from_os_specific(join(base_path, "tmp/#-#.png")), readMipmaps="True") + + print("Writing txo ..") + tex.write("cubemap.txo.pz") + + shutil.rmtree(join(base_path, "tmp")) + + +Application() diff --git a/RenderPipelineFile/data/empty_textures/README.md b/RenderPipelineFile/data/empty_textures/README.md index 2b68381c..eec19ca8 100644 --- a/RenderPipelineFile/data/empty_textures/README.md +++ b/RenderPipelineFile/data/empty_textures/README.md @@ -1,7 +1,7 @@ - -### Empty textures - -This are the empty textures you can use in your models in case you don't have -a detailmap for example. - -See the wiki for further information. + +### Empty textures + +This are the empty textures you can use in your models in case you don't have +a detailmap for example. + +See the wiki for further information. diff --git a/RenderPipelineFile/data/environment_brdf/generate_reference.py b/RenderPipelineFile/data/environment_brdf/generate_reference.py index a13bcd0a..8ea20194 100644 --- a/RenderPipelineFile/data/environment_brdf/generate_reference.py +++ b/RenderPipelineFile/data/environment_brdf/generate_reference.py @@ -1,120 +1,120 @@ -""" - -Uses mitsuba to generate the environment brdf - -""" - -from __future__ import print_function - -import os -import sys -import math -from panda3d.core import PNMImage, load_prc_file_data, Vec3 - -load_prc_file_data("", "notify-level error") -load_prc_file_data("", "notify-level-pnmimage error") - -configs = { - "normal": { - "out_dir": "slices", - "out_name": "env_brdf_{}.png", - "template_suffix": "", - "sequence": xrange(7), - "samples": 32, - }, - "metallic": { - "out_dir": "slices_metal", - "out_name": "env_brdf.png", - "template_suffix": "-metal", - "sequence": [1], - "samples": 32, - }, - "clearcoat": { - "out_dir": "slices_coat", - "out_name": "env_brdf.png", - "template_suffix": "-coat", - "sequence": [1], - "samples": 2048, - } -} - -# configs_to_run = ["normal", "metallic", "clearcoat"] -configs_to_run = ["clearcoat"] - -for config_name in configs_to_run: - - config = configs[config_name] - - if not os.path.isdir(config["out_dir"]): - os.makedirs(config["out_dir"]) - - - for pass_index in config["sequence"]: - - ior = 1.01 + 0.2 * pass_index - - dest_size = 512 - dest_h = 32 - dest = PNMImage(dest_size, dest_h) - - # run mitsuba - print("Running mitsuba for ior =", ior, "( index =", pass_index,")") - with open("res/scene" + config["template_suffix"] + ".templ.xml", "r") as handle: - content = handle.read() - - content = content.replace("%IOR%", str(ior)) - content = content.replace("%SAMPLES%", str(config["samples"])) - - with open("res/scene.xml", "w") as handle: - handle.write(content) - - os.system("run_mitsuba.bat") - - img = PNMImage("scene.png") - source_w = img.get_x_size() - - print("Converting ..") - - indices = [] - nxv_values = [] - - # Generate nonlinear NxV sequence - for i in xrange(source_w): - v = 1 - i / float(source_w) - NxV = math.sqrt(1 - v*v) - nxv_values.append(NxV) - - # Generate lerp indices and weights - for x in xrange(dest_size): - NxV = (x) / float(dest_size) - index = 0 - for i, s_nxv in enumerate(reversed(nxv_values)): - if NxV >= s_nxv: - index = i - break - index = len(nxv_values) - index - 1 - next_index = index + 1 if index < dest_size - 1 else index - - curr_nxv = nxv_values[index] - next_nxv = nxv_values[next_index] - - lerp_factor = (NxV - curr_nxv) / max(1e-10, abs(next_nxv - curr_nxv)) - lerp_factor = max(0.0, min(1.0, lerp_factor)) - indices.append((index, next_index, lerp_factor)) - - # Generate the final linear lut using the lerp weights - for y in xrange(dest_h): - for x in xrange(dest_size): - curr_i, next_i, lerp = indices[x] - curr_v = img.get_xel(curr_i, y) - next_v = img.get_xel(next_i, y) - dest.set_xel(x, y, curr_v * (1 - lerp) + next_v * lerp) - - - out_name = config["out_name"].replace("{}", str(pass_index)) - dest.write(config["out_dir"] + "/" + out_name) - -try: - os.remove("scene.png") -except: - pass +""" + +Uses mitsuba to generate the environment brdf + +""" + +from __future__ import print_function + +import os +import sys +import math +from panda3d.core import PNMImage, load_prc_file_data, Vec3 + +load_prc_file_data("", "notify-level error") +load_prc_file_data("", "notify-level-pnmimage error") + +configs = { + "normal": { + "out_dir": "slices", + "out_name": "env_brdf_{}.png", + "template_suffix": "", + "sequence": xrange(7), + "samples": 32, + }, + "metallic": { + "out_dir": "slices_metal", + "out_name": "env_brdf.png", + "template_suffix": "-metal", + "sequence": [1], + "samples": 32, + }, + "clearcoat": { + "out_dir": "slices_coat", + "out_name": "env_brdf.png", + "template_suffix": "-coat", + "sequence": [1], + "samples": 2048, + } +} + +# configs_to_run = ["normal", "metallic", "clearcoat"] +configs_to_run = ["clearcoat"] + +for config_name in configs_to_run: + + config = configs[config_name] + + if not os.path.isdir(config["out_dir"]): + os.makedirs(config["out_dir"]) + + + for pass_index in config["sequence"]: + + ior = 1.01 + 0.2 * pass_index + + dest_size = 512 + dest_h = 32 + dest = PNMImage(dest_size, dest_h) + + # run mitsuba + print("Running mitsuba for ior =", ior, "( index =", pass_index,")") + with open("res/scene" + config["template_suffix"] + ".templ.xml", "r") as handle: + content = handle.read() + + content = content.replace("%IOR%", str(ior)) + content = content.replace("%SAMPLES%", str(config["samples"])) + + with open("res/scene.xml", "w") as handle: + handle.write(content) + + os.system("run_mitsuba.bat") + + img = PNMImage("scene.png") + source_w = img.get_x_size() + + print("Converting ..") + + indices = [] + nxv_values = [] + + # Generate nonlinear NxV sequence + for i in xrange(source_w): + v = 1 - i / float(source_w) + NxV = math.sqrt(1 - v*v) + nxv_values.append(NxV) + + # Generate lerp indices and weights + for x in xrange(dest_size): + NxV = (x) / float(dest_size) + index = 0 + for i, s_nxv in enumerate(reversed(nxv_values)): + if NxV >= s_nxv: + index = i + break + index = len(nxv_values) - index - 1 + next_index = index + 1 if index < dest_size - 1 else index + + curr_nxv = nxv_values[index] + next_nxv = nxv_values[next_index] + + lerp_factor = (NxV - curr_nxv) / max(1e-10, abs(next_nxv - curr_nxv)) + lerp_factor = max(0.0, min(1.0, lerp_factor)) + indices.append((index, next_index, lerp_factor)) + + # Generate the final linear lut using the lerp weights + for y in xrange(dest_h): + for x in xrange(dest_size): + curr_i, next_i, lerp = indices[x] + curr_v = img.get_xel(curr_i, y) + next_v = img.get_xel(next_i, y) + dest.set_xel(x, y, curr_v * (1 - lerp) + next_v * lerp) + + + out_name = config["out_name"].replace("{}", str(pass_index)) + dest.write(config["out_dir"] + "/" + out_name) + +try: + os.remove("scene.png") +except: + pass diff --git a/RenderPipelineFile/data/environment_brdf/res/scene-coat.templ.xml b/RenderPipelineFile/data/environment_brdf/res/scene-coat.templ.xml index 0906e4d6..7d124be0 100644 --- a/RenderPipelineFile/data/environment_brdf/res/scene-coat.templ.xml +++ b/RenderPipelineFile/data/environment_brdf/res/scene-coat.templ.xml @@ -1,68 +1,68 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RenderPipelineFile/data/environment_brdf/res/scene-metal.templ.xml b/RenderPipelineFile/data/environment_brdf/res/scene-metal.templ.xml index 4b0c191a..94788c69 100644 --- a/RenderPipelineFile/data/environment_brdf/res/scene-metal.templ.xml +++ b/RenderPipelineFile/data/environment_brdf/res/scene-metal.templ.xml @@ -1,56 +1,56 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RenderPipelineFile/data/environment_brdf/res/scene.templ.xml b/RenderPipelineFile/data/environment_brdf/res/scene.templ.xml index 8f4a9701..6894208f 100644 --- a/RenderPipelineFile/data/environment_brdf/res/scene.templ.xml +++ b/RenderPipelineFile/data/environment_brdf/res/scene.templ.xml @@ -1,62 +1,62 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RenderPipelineFile/data/environment_brdf/res/scene.xml b/RenderPipelineFile/data/environment_brdf/res/scene.xml index 32e22b23..2459d298 100644 --- a/RenderPipelineFile/data/environment_brdf/res/scene.xml +++ b/RenderPipelineFile/data/environment_brdf/res/scene.xml @@ -1,68 +1,68 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RenderPipelineFile/data/environment_brdf/run_mitsuba.bat b/RenderPipelineFile/data/environment_brdf/run_mitsuba.bat index 00a6e437..50aff402 100644 --- a/RenderPipelineFile/data/environment_brdf/run_mitsuba.bat +++ b/RenderPipelineFile/data/environment_brdf/run_mitsuba.bat @@ -1,10 +1,10 @@ -@echo off - -"C:/mitsuba/mitsuba" -p 6 res/scene.xml -echo "" -echo Rendering done -echo "" -echo "" -echo "" -"C:/mitsuba/mtsutil" tonemap -g 2.0 res/scene.exr -copy res\scene.png scene.png +@echo off + +"C:/mitsuba/mitsuba" -p 6 res/scene.xml +echo "" +echo Rendering done +echo "" +echo "" +echo "" +"C:/mitsuba/mtsutil" tonemap -g 2.0 res/scene.exr +copy res\scene.png scene.png diff --git a/RenderPipelineFile/data/film_grain/generate.py b/RenderPipelineFile/data/film_grain/generate.py index c0f1f71a..60f48e55 100644 --- a/RenderPipelineFile/data/film_grain/generate.py +++ b/RenderPipelineFile/data/film_grain/generate.py @@ -1,77 +1,77 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -# This script precomputes the noise for the film grain to improve performance - -import os -curr_dir = os.path.dirname(os.path.realpath(__file__)) -os.chdir(curr_dir) - -import sys -sys.path.insert(0, "../../rpcore") -sys.path.insert(0, "../../") - -from panda3d.core import load_prc_file_data, NodePath, Shader, Texture, ShaderAttrib -from panda3d.core import PNMImage - -from direct.showbase.ShowBase import ShowBase - -class Application(ShowBase): - def __init__(self): - load_prc_file_data("", """ - textures-power-2 none - window-type offscreen - win-size 100 100 - gl-coordinate-system default - notify-level-display error - print-pipe-types #f - gl-version 4 3 - """) - - ShowBase.__init__(self) - - dest_tex = Texture() - dest_tex.setup_2d_texture(2048, 2048, Texture.T_unsigned_byte, Texture.F_rgba8) - cshader = Shader.load_compute(Shader.SL_GLSL, "grain.compute.glsl") - node = NodePath("") - node.set_shader(cshader) - node.set_shader_input("DestTex", dest_tex) - attr = node.get_attrib(ShaderAttrib) - self.graphicsEngine.dispatch_compute( - (2048 // 16, 2048 // 16, 1), attr, self.win.gsg) - - base.graphicsEngine.extract_texture_data(dest_tex, base.win.gsg) - - # Convert to single channel - img = PNMImage(2048, 2048, 1, 255) - dest_tex.store(img) - img.set_num_channels(1) - - tex = Texture() - tex.load(img) - tex.write("grain.txo.pz") - -Application() +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +# This script precomputes the noise for the film grain to improve performance + +import os +curr_dir = os.path.dirname(os.path.realpath(__file__)) +os.chdir(curr_dir) + +import sys +sys.path.insert(0, "../../rpcore") +sys.path.insert(0, "../../") + +from panda3d.core import load_prc_file_data, NodePath, Shader, Texture, ShaderAttrib +from panda3d.core import PNMImage + +from direct.showbase.ShowBase import ShowBase + +class Application(ShowBase): + def __init__(self): + load_prc_file_data("", """ + textures-power-2 none + window-type offscreen + win-size 100 100 + gl-coordinate-system default + notify-level-display error + print-pipe-types #f + gl-version 4 3 + """) + + ShowBase.__init__(self) + + dest_tex = Texture() + dest_tex.setup_2d_texture(2048, 2048, Texture.T_unsigned_byte, Texture.F_rgba8) + cshader = Shader.load_compute(Shader.SL_GLSL, "grain.compute.glsl") + node = NodePath("") + node.set_shader(cshader) + node.set_shader_input("DestTex", dest_tex) + attr = node.get_attrib(ShaderAttrib) + self.graphicsEngine.dispatch_compute( + (2048 // 16, 2048 // 16, 1), attr, self.win.gsg) + + base.graphicsEngine.extract_texture_data(dest_tex, base.win.gsg) + + # Convert to single channel + img = PNMImage(2048, 2048, 1, 255) + dest_tex.store(img) + img.set_num_channels(1) + + tex = Texture() + tex.load(img) + tex.write("grain.txo.pz") + +Application() diff --git a/RenderPipelineFile/data/film_grain/grain.compute.glsl b/RenderPipelineFile/data/film_grain/grain.compute.glsl index 5d6d1011..af334ea2 100644 --- a/RenderPipelineFile/data/film_grain/grain.compute.glsl +++ b/RenderPipelineFile/data/film_grain/grain.compute.glsl @@ -1,25 +1,25 @@ -#version 430 - -// Precomputes the film grain - -layout (local_size_x = 16, local_size_y = 16, local_size_z = 1) in; - -#define ASPECT_RATIO 1 -#define SCREEN_SIZE vec2(2048, 2048) -#define gl_FragCoord vec4(0) -#pragma include "../../rpcore/shader/includes/noise.inc.glsl" - -layout(rgba8) uniform image2D DestTex; - -float generate_grain(vec2 texCoord, vec2 resolution, float frame, float multiplier) { - vec2 mult = texCoord * resolution; - float offset = snoise3D(vec3(mult / multiplier, frame)); - float n1 = pnoise3D(vec3(mult, offset), vec3(1.0 / texCoord * resolution, 1.0)); - return n1 * 0.5 + 0.5; -} - -void main() { - vec2 texcoord = vec2(ivec2(gl_GlobalInvocationID.xy) + 0.5) / 2048.0; - vec4 result = vec4(generate_grain(texcoord * 2048.0, vec2(4096.0), 0.0, 4.634523)); - imageStore(DestTex, ivec2(gl_GlobalInvocationID.xy), result); -} +#version 430 + +// Precomputes the film grain + +layout (local_size_x = 16, local_size_y = 16, local_size_z = 1) in; + +#define ASPECT_RATIO 1 +#define SCREEN_SIZE vec2(2048, 2048) +#define gl_FragCoord vec4(0) +#pragma include "../../rpcore/shader/includes/noise.inc.glsl" + +layout(rgba8) uniform image2D DestTex; + +float generate_grain(vec2 texCoord, vec2 resolution, float frame, float multiplier) { + vec2 mult = texCoord * resolution; + float offset = snoise3D(vec3(mult / multiplier, frame)); + float n1 = pnoise3D(vec3(mult, offset), vec3(1.0 / texCoord * resolution, 1.0)); + return n1 * 0.5 + 0.5; +} + +void main() { + vec2 texcoord = vec2(ivec2(gl_GlobalInvocationID.xy) + 0.5) / 2048.0; + vec4 result = vec4(generate_grain(texcoord * 2048.0, vec2(4096.0), 0.0, 4.634523)); + imageStore(DestTex, ivec2(gl_GlobalInvocationID.xy), result); +} diff --git a/RenderPipelineFile/data/font/roboto-LICENSE.txt b/RenderPipelineFile/data/font/roboto-LICENSE.txt index 817dcace..8b2b0b8f 100644 --- a/RenderPipelineFile/data/font/roboto-LICENSE.txt +++ b/RenderPipelineFile/data/font/roboto-LICENSE.txt @@ -1,178 +1,178 @@ -Font data copyright Google 2012 - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - “License” shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - “Licensor” shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - “Legal Entity” shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - “control” means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - “You” (or “Your”) shall mean an individual or Legal Entity - exercising permissions granted by this License. - - “Source” form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - “Object” form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - “Work” shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - “Derivative Works” shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - “Contribution” shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, “submitted” - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as “Not a Contribution.” - - “Contributor” shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a “NOTICE” text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an “AS IS” BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS +Font data copyright Google 2012 + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + “License” shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + “Licensor” shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + “Legal Entity” shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + “control” means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + “You” (or “Your”) shall mean an individual or Legal Entity + exercising permissions granted by this License. + + “Source” form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + “Object” form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + “Work” shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + “Derivative Works” shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + “Contribution” shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, “submitted” + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as “Not a Contribution.” + + “Contributor” shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a “NOTICE” text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an “AS IS” BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/RenderPipelineFile/data/generate_txo_files.py b/RenderPipelineFile/data/generate_txo_files.py index 6e83e695..045b8ca1 100644 --- a/RenderPipelineFile/data/generate_txo_files.py +++ b/RenderPipelineFile/data/generate_txo_files.py @@ -1,37 +1,37 @@ -""" - -Converts pipeline resources to TXO files to speed up loading. -This is called during the setup. - -""" - -from __future__ import print_function - -import os - -from panda3d.core import Filename, Texture, load_prc_file_data -load_prc_file_data("", "window-type none") -load_prc_file_data("", "notify-level-pnmimage error") -load_prc_file_data("", "textures-power-2 none") - -files_to_convert = [ - "data/gui/loading_screen_bg.png", - "rpplugins/bloom/resources/lens_dirt.png", - "data/builtin_models/skybox/skybox.jpg" -] - -this_dir = os.path.realpath(os.path.dirname(__file__)) -os.chdir(this_dir) - -pipeline_dir = "../" - -import direct.directbase.DirectStart - -for filename in files_to_convert: - src_path = os.path.abspath(os.path.join(pipeline_dir, filename)) - fullpath = Filename.from_os_specific(src_path).get_fullpath() - dest_path = fullpath.replace(".png", ".txo.pz") - dest_path = dest_path.replace(".jpg", ".txo.pz") - - print(src_path, "->", dest_path) - loader.load_texture(fullpath).write(dest_path) +""" + +Converts pipeline resources to TXO files to speed up loading. +This is called during the setup. + +""" + +from __future__ import print_function + +import os + +from panda3d.core import Filename, Texture, load_prc_file_data +load_prc_file_data("", "window-type none") +load_prc_file_data("", "notify-level-pnmimage error") +load_prc_file_data("", "textures-power-2 none") + +files_to_convert = [ + "data/gui/loading_screen_bg.png", + "rpplugins/bloom/resources/lens_dirt.png", + "data/builtin_models/skybox/skybox.jpg" +] + +this_dir = os.path.realpath(os.path.dirname(__file__)) +os.chdir(this_dir) + +pipeline_dir = "../" + +import direct.directbase.DirectStart + +for filename in files_to_convert: + src_path = os.path.abspath(os.path.join(pipeline_dir, filename)) + fullpath = Filename.from_os_specific(src_path).get_fullpath() + dest_path = fullpath.replace(".png", ".txo.pz") + dest_path = dest_path.replace(".jpg", ".txo.pz") + + print(src_path, "->", dest_path) + loader.load_texture(fullpath).write(dest_path) diff --git a/RenderPipelineFile/data/ies_profiles/README.md b/RenderPipelineFile/data/ies_profiles/README.md index 673600b9..a0e58175 100644 --- a/RenderPipelineFile/data/ies_profiles/README.md +++ b/RenderPipelineFile/data/ies_profiles/README.md @@ -1,25 +1,25 @@ - -# IES Profiles - - -### Source - -This files were aqcuired from: -http://www.derekjenson.com/3d-blog/ies-light-profiles - -### Finding new IES Profiles - -You can download IES profile files from the vendors directly. -Here are some links to start with: - -- http://www.colorkinetics.com/support/ies/ -- http://www.seleconlight.com/index.php?option=com_content&view=article&id=475 -- http://www.lithonia.com/photometrics.aspx - - -### Previewing IES Profiles - -You can preview the profiles with a viewer like this: - -http://www.photometricviewer.com - + +# IES Profiles + + +### Source + +This files were aqcuired from: +http://www.derekjenson.com/3d-blog/ies-light-profiles + +### Finding new IES Profiles + +You can download IES profile files from the vendors directly. +Here are some links to start with: + +- http://www.colorkinetics.com/support/ies/ +- http://www.seleconlight.com/index.php?option=com_content&view=article&id=475 +- http://www.lithonia.com/photometrics.aspx + + +### Previewing IES Profiles + +You can preview the profiles with a viewer like this: + +http://www.photometricviewer.com + diff --git a/RenderPipelineFile/data/ies_profiles/area_light.ies b/RenderPipelineFile/data/ies_profiles/area_light.ies index 0981e5e7..93c304b2 100644 --- a/RenderPipelineFile/data/ies_profiles/area_light.ies +++ b/RenderPipelineFile/data/ies_profiles/area_light.ies @@ -1,83 +1,83 @@ -IESNA:LM-63-1995 -[ISSUEDATE] 07-16-2008 -[TEST] S0710292-R1 -[MANUFAC] QSSI -[LUMCAT] KH40A-Type III -[LUMINAIRE] KH40 with Adjustable Reflector in Type III position -[LAMP] 400W Venture Uni-Form Pulse Start Lamp, Rated at 41,000 Lumens -[SKTPOSITION] Horizontal Socket Position 5, Street Side Lamp Holder -TILT=NONE -1 41000 1.27 35 35 1 1 1.25 1.25 0 -1 1 433.62 -0 5 10 15 20 25 30 35 40 45 50 55 60 62.5 65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 95 105 115 125 135 145 155 165 175 180 -0 5 15 25 35 45 47.5 50 52.5 55 57.5 60 62.5 65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 92.5 95 105 115 125 135 145 155 165 175 180 -7417.00 7382.00 7263.00 7121.00 6990.00 6758.00 6434.00 6118.00 5692.00 4547.00 3496.00 4195.00 4796.00 5633.00 5783.00 5751.00 -4859.00 3975.00 2344.00 1366.00 160.00 72.00 39.00 24.00 21.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7405.00 7283.00 7135.00 7003.00 6820.00 6491.00 6141.00 5694.00 4606.00 3541.00 4075.00 4778.00 5313.00 5496.00 5075.00 -4280.00 3598.00 1962.00 1151.00 141.00 67.00 40.00 23.00 21.00 19.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7383.00 7267.00 7116.00 6985.00 6824.00 6642.00 6130.00 5292.00 4304.00 3494.00 3652.00 4934.00 5076.00 4892.00 4279.00 -2986.00 2173.00 1626.00 732.00 190.00 67.00 42.00 23.00 21.00 19.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7338.00 7186.00 7004.00 6792.00 6636.00 6374.00 5738.00 4946.00 4762.00 4024.00 3997.00 5536.00 5402.00 5123.00 4709.00 -3693.00 2178.00 1438.00 685.00 161.00 72.00 39.00 23.00 20.00 19.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7324.00 7066.00 6717.00 6486.00 6325.00 5900.00 5379.00 5421.00 4917.00 4550.00 4575.00 5901.00 8328.00 7841.00 6686.00 -5417.00 2837.00 1896.00 1123.00 177.00 71.00 41.00 24.00 21.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7284.00 6859.00 6523.00 6335.00 6048.00 5654.00 5112.00 5736.00 5706.00 5144.00 5769.00 10385.00 10952.00 10744.00 9057.00 -6083.00 3307.00 2350.00 1042.00 167.00 76.00 41.00 26.00 22.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7272.00 6802.00 6505.00 6253.00 5947.00 5546.00 5083.00 5713.00 5777.00 5236.00 7030.00 10955.00 11004.00 11331.00 9180.00 -5616.00 3669.00 2326.00 700.00 150.00 76.00 42.00 26.00 23.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7271.00 6780.00 6512.00 6255.00 5925.00 5422.00 5108.00 5698.00 5856.00 5267.00 7787.00 11280.00 11056.00 11588.00 8604.00 -5357.00 4075.00 2388.00 624.00 150.00 77.00 44.00 26.00 23.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7283.00 6781.00 6529.00 6229.00 5824.00 5318.00 5151.00 5653.00 5917.00 5389.00 7867.00 11388.00 11116.00 10907.00 7978.00 -5353.00 4324.00 2291.00 595.00 163.00 89.00 43.00 26.00 23.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7252.00 6706.00 6496.00 6161.00 5735.00 5214.00 5111.00 5586.00 5951.00 5500.00 8022.00 11522.00 11494.00 9242.00 7462.00 -5174.00 4516.00 2094.00 594.00 191.00 87.00 43.00 26.00 23.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7224.00 6669.00 6459.00 6086.00 5610.00 5150.00 5099.00 5499.00 5899.00 5565.00 7933.00 11560.00 11858.00 8529.00 7029.00 -4917.00 4292.00 2027.00 619.00 209.00 88.00 42.00 26.00 23.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7219.00 6654.00 6449.00 6073.00 5494.00 5090.00 5076.00 5442.00 5831.00 5637.00 7786.00 11528.00 11335.00 8436.00 6553.00 -4715.00 3972.00 1899.00 626.00 241.00 94.00 42.00 26.00 22.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7225.00 6658.00 6469.00 6030.00 5441.00 5046.00 5116.00 5435.00 5771.00 5787.00 7685.00 11568.00 10502.00 8333.00 6219.00 -4476.00 3582.00 1644.00 641.00 253.00 90.00 43.00 26.00 22.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7287.00 6700.00 6520.00 6031.00 5380.00 5032.00 5191.00 5403.00 5729.00 5891.00 7489.00 11227.00 9688.00 8036.00 5924.00 -4353.00 3030.00 1294.00 604.00 257.00 87.00 49.00 26.00 22.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7238.00 6625.00 6449.00 5934.00 5232.00 4936.00 5183.00 5393.00 5653.00 5980.00 7355.00 10352.00 8867.00 7433.00 5389.00 -4232.00 2454.00 1000.00 488.00 280.00 78.00 45.00 26.00 22.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7215.00 6607.00 6445.00 5862.00 5161.00 4900.00 5195.00 5390.00 5590.00 6054.00 7236.00 9281.00 8026.00 6696.00 5128.00 -4013.00 2035.00 778.00 409.00 215.00 79.00 41.00 25.00 22.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7202.00 6596.00 6417.00 5833.00 5078.00 4804.00 5214.00 5410.00 5505.00 6057.00 7124.00 8613.00 7409.00 6003.00 5068.00 -4005.00 1857.00 568.00 257.00 131.00 69.00 38.00 25.00 22.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7198.00 6559.00 6398.00 5781.00 5001.00 4741.00 5227.00 5452.00 5376.00 6037.00 6982.00 7652.00 6384.00 5441.00 4949.00 -4260.00 1950.00 441.00 169.00 109.00 64.00 35.00 25.00 22.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7209.00 6581.00 6423.00 5768.00 4945.00 4717.00 5255.00 5458.00 5273.00 6009.00 6841.00 6818.00 5427.00 5345.00 5378.00 -5257.00 2594.00 517.00 194.00 158.00 67.00 34.00 25.00 22.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7249.00 6627.00 6441.00 5774.00 4912.00 4687.00 5213.00 5383.00 5201.00 5955.00 6672.00 6098.00 5163.00 5212.00 5595.00 -5994.00 3246.00 995.00 262.00 147.00 74.00 36.00 25.00 22.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7220.00 6574.00 6416.00 5722.00 4852.00 4628.00 5136.00 5255.00 5090.00 5723.00 6408.00 5524.00 5543.00 5326.00 5697.00 -5900.00 2620.00 1258.00 332.00 134.00 72.00 33.00 24.00 22.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7204.00 6563.00 6422.00 5659.00 4808.00 4586.00 5061.00 5175.00 5011.00 5620.00 6185.00 5303.00 5609.00 5550.00 6641.00 -6023.00 2144.00 1230.00 275.00 129.00 70.00 33.00 24.00 21.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7213.00 6587.00 6404.00 5652.00 4780.00 4572.00 4995.00 5118.00 4963.00 5582.00 5940.00 5607.00 5486.00 5667.00 7137.00 -5903.00 2045.00 900.00 218.00 113.00 66.00 34.00 24.00 21.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7206.00 6542.00 6407.00 5637.00 4739.00 4549.00 4938.00 5067.00 4970.00 5547.00 5724.00 5658.00 5314.00 6006.00 7655.00 -5363.00 1908.00 642.00 179.00 91.00 58.00 32.00 24.00 21.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7217.00 6564.00 6406.00 5634.00 4719.00 4566.00 4855.00 5050.00 5103.00 5396.00 5597.00 5391.00 5163.00 6692.00 8064.00 -5238.00 1781.00 490.00 167.00 95.00 53.00 31.00 24.00 22.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7286.00 6641.00 6476.00 5639.00 4713.00 4554.00 4773.00 5052.00 5127.00 5168.00 5272.00 5191.00 5129.00 7315.00 7707.00 -5151.00 1457.00 411.00 163.00 91.00 54.00 31.00 24.00 22.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7243.00 6589.00 6437.00 5748.00 4721.00 4489.00 4610.00 5047.00 4800.00 4975.00 5468.00 5017.00 5275.00 5262.00 4489.00 -3129.00 839.00 279.00 151.00 101.00 90.00 30.00 23.00 22.00 22.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7268.00 6633.00 6416.00 5949.00 4866.00 4390.00 4377.00 4763.00 4275.00 4100.00 4194.00 3792.00 3797.00 3409.00 3078.00 -2472.00 738.00 230.00 137.00 129.00 69.00 28.00 22.00 22.00 23.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7292.00 6733.00 6404.00 6097.00 5175.00 4364.00 4151.00 4142.00 3841.00 3661.00 3526.00 3221.00 3089.00 2900.00 2676.00 -2374.00 1206.00 314.00 138.00 77.00 54.00 26.00 22.00 22.00 23.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7297.00 6902.00 6409.00 6182.00 5542.00 4511.00 4002.00 3796.00 3539.00 3411.00 3264.00 2961.00 2817.00 2632.00 2439.00 -2127.00 1415.00 388.00 129.00 157.00 47.00 25.00 21.00 21.00 22.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7310.00 7105.00 6591.00 6171.00 5732.00 4813.00 4065.00 3605.00 3299.00 3404.00 3190.00 2840.00 2596.00 2425.00 2177.00 -1837.00 994.00 356.00 153.00 78.00 42.00 24.00 21.00 21.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7332.00 7219.00 6927.00 6432.00 5846.00 5194.00 4422.00 3788.00 3171.00 3017.00 2873.00 2689.00 2478.00 2121.00 1807.00 -1441.00 740.00 332.00 169.00 93.00 49.00 24.00 20.00 20.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7319.00 7224.00 7077.00 6815.00 6270.00 5499.00 4735.00 3994.00 3098.00 2739.00 2489.00 2235.00 1983.00 1672.00 1383.00 -1057.00 553.00 272.00 168.00 89.00 44.00 23.00 20.00 19.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7337.00 7218.00 7041.00 6856.00 6402.00 5656.00 4670.00 3658.00 2889.00 2547.00 2219.00 1898.00 1632.00 1349.00 1086.00 -792.00 492.00 283.00 213.00 136.00 56.00 23.00 19.00 19.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 -7417.00 7348.00 7218.00 7033.00 6787.00 6357.00 5609.00 4578.00 3463.00 2809.00 2508.00 2217.00 1920.00 1583.00 1285.00 987.00 -694.00 490.00 238.00 142.00 86.00 44.00 22.00 19.00 20.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +IESNA:LM-63-1995 +[ISSUEDATE] 07-16-2008 +[TEST] S0710292-R1 +[MANUFAC] QSSI +[LUMCAT] KH40A-Type III +[LUMINAIRE] KH40 with Adjustable Reflector in Type III position +[LAMP] 400W Venture Uni-Form Pulse Start Lamp, Rated at 41,000 Lumens +[SKTPOSITION] Horizontal Socket Position 5, Street Side Lamp Holder +TILT=NONE +1 41000 1.27 35 35 1 1 1.25 1.25 0 +1 1 433.62 +0 5 10 15 20 25 30 35 40 45 50 55 60 62.5 65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 95 105 115 125 135 145 155 165 175 180 +0 5 15 25 35 45 47.5 50 52.5 55 57.5 60 62.5 65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 92.5 95 105 115 125 135 145 155 165 175 180 +7417.00 7382.00 7263.00 7121.00 6990.00 6758.00 6434.00 6118.00 5692.00 4547.00 3496.00 4195.00 4796.00 5633.00 5783.00 5751.00 +4859.00 3975.00 2344.00 1366.00 160.00 72.00 39.00 24.00 21.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7405.00 7283.00 7135.00 7003.00 6820.00 6491.00 6141.00 5694.00 4606.00 3541.00 4075.00 4778.00 5313.00 5496.00 5075.00 +4280.00 3598.00 1962.00 1151.00 141.00 67.00 40.00 23.00 21.00 19.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7383.00 7267.00 7116.00 6985.00 6824.00 6642.00 6130.00 5292.00 4304.00 3494.00 3652.00 4934.00 5076.00 4892.00 4279.00 +2986.00 2173.00 1626.00 732.00 190.00 67.00 42.00 23.00 21.00 19.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7338.00 7186.00 7004.00 6792.00 6636.00 6374.00 5738.00 4946.00 4762.00 4024.00 3997.00 5536.00 5402.00 5123.00 4709.00 +3693.00 2178.00 1438.00 685.00 161.00 72.00 39.00 23.00 20.00 19.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7324.00 7066.00 6717.00 6486.00 6325.00 5900.00 5379.00 5421.00 4917.00 4550.00 4575.00 5901.00 8328.00 7841.00 6686.00 +5417.00 2837.00 1896.00 1123.00 177.00 71.00 41.00 24.00 21.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7284.00 6859.00 6523.00 6335.00 6048.00 5654.00 5112.00 5736.00 5706.00 5144.00 5769.00 10385.00 10952.00 10744.00 9057.00 +6083.00 3307.00 2350.00 1042.00 167.00 76.00 41.00 26.00 22.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7272.00 6802.00 6505.00 6253.00 5947.00 5546.00 5083.00 5713.00 5777.00 5236.00 7030.00 10955.00 11004.00 11331.00 9180.00 +5616.00 3669.00 2326.00 700.00 150.00 76.00 42.00 26.00 23.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7271.00 6780.00 6512.00 6255.00 5925.00 5422.00 5108.00 5698.00 5856.00 5267.00 7787.00 11280.00 11056.00 11588.00 8604.00 +5357.00 4075.00 2388.00 624.00 150.00 77.00 44.00 26.00 23.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7283.00 6781.00 6529.00 6229.00 5824.00 5318.00 5151.00 5653.00 5917.00 5389.00 7867.00 11388.00 11116.00 10907.00 7978.00 +5353.00 4324.00 2291.00 595.00 163.00 89.00 43.00 26.00 23.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7252.00 6706.00 6496.00 6161.00 5735.00 5214.00 5111.00 5586.00 5951.00 5500.00 8022.00 11522.00 11494.00 9242.00 7462.00 +5174.00 4516.00 2094.00 594.00 191.00 87.00 43.00 26.00 23.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7224.00 6669.00 6459.00 6086.00 5610.00 5150.00 5099.00 5499.00 5899.00 5565.00 7933.00 11560.00 11858.00 8529.00 7029.00 +4917.00 4292.00 2027.00 619.00 209.00 88.00 42.00 26.00 23.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7219.00 6654.00 6449.00 6073.00 5494.00 5090.00 5076.00 5442.00 5831.00 5637.00 7786.00 11528.00 11335.00 8436.00 6553.00 +4715.00 3972.00 1899.00 626.00 241.00 94.00 42.00 26.00 22.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7225.00 6658.00 6469.00 6030.00 5441.00 5046.00 5116.00 5435.00 5771.00 5787.00 7685.00 11568.00 10502.00 8333.00 6219.00 +4476.00 3582.00 1644.00 641.00 253.00 90.00 43.00 26.00 22.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7287.00 6700.00 6520.00 6031.00 5380.00 5032.00 5191.00 5403.00 5729.00 5891.00 7489.00 11227.00 9688.00 8036.00 5924.00 +4353.00 3030.00 1294.00 604.00 257.00 87.00 49.00 26.00 22.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7238.00 6625.00 6449.00 5934.00 5232.00 4936.00 5183.00 5393.00 5653.00 5980.00 7355.00 10352.00 8867.00 7433.00 5389.00 +4232.00 2454.00 1000.00 488.00 280.00 78.00 45.00 26.00 22.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7215.00 6607.00 6445.00 5862.00 5161.00 4900.00 5195.00 5390.00 5590.00 6054.00 7236.00 9281.00 8026.00 6696.00 5128.00 +4013.00 2035.00 778.00 409.00 215.00 79.00 41.00 25.00 22.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7202.00 6596.00 6417.00 5833.00 5078.00 4804.00 5214.00 5410.00 5505.00 6057.00 7124.00 8613.00 7409.00 6003.00 5068.00 +4005.00 1857.00 568.00 257.00 131.00 69.00 38.00 25.00 22.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7198.00 6559.00 6398.00 5781.00 5001.00 4741.00 5227.00 5452.00 5376.00 6037.00 6982.00 7652.00 6384.00 5441.00 4949.00 +4260.00 1950.00 441.00 169.00 109.00 64.00 35.00 25.00 22.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7209.00 6581.00 6423.00 5768.00 4945.00 4717.00 5255.00 5458.00 5273.00 6009.00 6841.00 6818.00 5427.00 5345.00 5378.00 +5257.00 2594.00 517.00 194.00 158.00 67.00 34.00 25.00 22.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7249.00 6627.00 6441.00 5774.00 4912.00 4687.00 5213.00 5383.00 5201.00 5955.00 6672.00 6098.00 5163.00 5212.00 5595.00 +5994.00 3246.00 995.00 262.00 147.00 74.00 36.00 25.00 22.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7220.00 6574.00 6416.00 5722.00 4852.00 4628.00 5136.00 5255.00 5090.00 5723.00 6408.00 5524.00 5543.00 5326.00 5697.00 +5900.00 2620.00 1258.00 332.00 134.00 72.00 33.00 24.00 22.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7204.00 6563.00 6422.00 5659.00 4808.00 4586.00 5061.00 5175.00 5011.00 5620.00 6185.00 5303.00 5609.00 5550.00 6641.00 +6023.00 2144.00 1230.00 275.00 129.00 70.00 33.00 24.00 21.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7213.00 6587.00 6404.00 5652.00 4780.00 4572.00 4995.00 5118.00 4963.00 5582.00 5940.00 5607.00 5486.00 5667.00 7137.00 +5903.00 2045.00 900.00 218.00 113.00 66.00 34.00 24.00 21.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7206.00 6542.00 6407.00 5637.00 4739.00 4549.00 4938.00 5067.00 4970.00 5547.00 5724.00 5658.00 5314.00 6006.00 7655.00 +5363.00 1908.00 642.00 179.00 91.00 58.00 32.00 24.00 21.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7217.00 6564.00 6406.00 5634.00 4719.00 4566.00 4855.00 5050.00 5103.00 5396.00 5597.00 5391.00 5163.00 6692.00 8064.00 +5238.00 1781.00 490.00 167.00 95.00 53.00 31.00 24.00 22.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7286.00 6641.00 6476.00 5639.00 4713.00 4554.00 4773.00 5052.00 5127.00 5168.00 5272.00 5191.00 5129.00 7315.00 7707.00 +5151.00 1457.00 411.00 163.00 91.00 54.00 31.00 24.00 22.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7243.00 6589.00 6437.00 5748.00 4721.00 4489.00 4610.00 5047.00 4800.00 4975.00 5468.00 5017.00 5275.00 5262.00 4489.00 +3129.00 839.00 279.00 151.00 101.00 90.00 30.00 23.00 22.00 22.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7268.00 6633.00 6416.00 5949.00 4866.00 4390.00 4377.00 4763.00 4275.00 4100.00 4194.00 3792.00 3797.00 3409.00 3078.00 +2472.00 738.00 230.00 137.00 129.00 69.00 28.00 22.00 22.00 23.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7292.00 6733.00 6404.00 6097.00 5175.00 4364.00 4151.00 4142.00 3841.00 3661.00 3526.00 3221.00 3089.00 2900.00 2676.00 +2374.00 1206.00 314.00 138.00 77.00 54.00 26.00 22.00 22.00 23.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7297.00 6902.00 6409.00 6182.00 5542.00 4511.00 4002.00 3796.00 3539.00 3411.00 3264.00 2961.00 2817.00 2632.00 2439.00 +2127.00 1415.00 388.00 129.00 157.00 47.00 25.00 21.00 21.00 22.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7310.00 7105.00 6591.00 6171.00 5732.00 4813.00 4065.00 3605.00 3299.00 3404.00 3190.00 2840.00 2596.00 2425.00 2177.00 +1837.00 994.00 356.00 153.00 78.00 42.00 24.00 21.00 21.00 21.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7332.00 7219.00 6927.00 6432.00 5846.00 5194.00 4422.00 3788.00 3171.00 3017.00 2873.00 2689.00 2478.00 2121.00 1807.00 +1441.00 740.00 332.00 169.00 93.00 49.00 24.00 20.00 20.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7319.00 7224.00 7077.00 6815.00 6270.00 5499.00 4735.00 3994.00 3098.00 2739.00 2489.00 2235.00 1983.00 1672.00 1383.00 +1057.00 553.00 272.00 168.00 89.00 44.00 23.00 20.00 19.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7337.00 7218.00 7041.00 6856.00 6402.00 5656.00 4670.00 3658.00 2889.00 2547.00 2219.00 1898.00 1632.00 1349.00 1086.00 +792.00 492.00 283.00 213.00 136.00 56.00 23.00 19.00 19.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 +7417.00 7348.00 7218.00 7033.00 6787.00 6357.00 5609.00 4578.00 3463.00 2809.00 2508.00 2217.00 1920.00 1583.00 1285.00 987.00 +694.00 490.00 238.00 142.00 86.00 44.00 22.00 19.00 20.00 20.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 diff --git a/RenderPipelineFile/data/ies_profiles/bollard.ies b/RenderPipelineFile/data/ies_profiles/bollard.ies index a98b5948..dd0debf9 100644 --- a/RenderPipelineFile/data/ies_profiles/bollard.ies +++ b/RenderPipelineFile/data/ies_profiles/bollard.ies @@ -1,16 +1,16 @@ -IESNA:LM-63-2002 -[TEST]BALLABS TEST NO. 14397.0 -[TESTLAB] BUILDING ACOUSTICS & LIGHTING LABORATORIES, INC -[ISSUEDATE] 03-DEC-2008 -[MANUFAC] H.E. WILLIAMS, INC - CARTHAGE, MISSOURI -[LUMINAIRE] 1/100W CLEAR ED17 LAMP 6"DIA 38.25"TALL WHITE BOLLARD -[MORE] 4.5"DIA SPECULAR CONE REFLECTOR w/5.875"TALL ACRYLIC -[MORE] CYLINDER LENS -[LUMCAT] OSA6R-100PSMH120-AC -[LAMPCAT] M90 MH100W/U/PS -TILT=NONE -1 9000 1 35 1 1 1 -0.49 -0.49 0.406 -1 1 100 -0 5 10 15 20 25 30 35 40 45 50 55 60 62.5 65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 95 105 115 125 135 145 155 165 175 180 -0 -0.00 12.00 87.00 182.00 167.00 300.00 655.00 944.00 822.00 703.00 614.00 565.00 487.00 438.00 372.00 309.00 262.00 216.00 182.00 142.00 111.00 83.00 61.00 42.00 25.00 19.00 14.00 14.00 15.00 20.00 33.00 36.00 27.00 4.00 0.00 +IESNA:LM-63-2002 +[TEST]BALLABS TEST NO. 14397.0 +[TESTLAB] BUILDING ACOUSTICS & LIGHTING LABORATORIES, INC +[ISSUEDATE] 03-DEC-2008 +[MANUFAC] H.E. WILLIAMS, INC - CARTHAGE, MISSOURI +[LUMINAIRE] 1/100W CLEAR ED17 LAMP 6"DIA 38.25"TALL WHITE BOLLARD +[MORE] 4.5"DIA SPECULAR CONE REFLECTOR w/5.875"TALL ACRYLIC +[MORE] CYLINDER LENS +[LUMCAT] OSA6R-100PSMH120-AC +[LAMPCAT] M90 MH100W/U/PS +TILT=NONE +1 9000 1 35 1 1 1 -0.49 -0.49 0.406 +1 1 100 +0 5 10 15 20 25 30 35 40 45 50 55 60 62.5 65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 95 105 115 125 135 145 155 165 175 180 +0 +0.00 12.00 87.00 182.00 167.00 300.00 655.00 944.00 822.00 703.00 614.00 565.00 487.00 438.00 372.00 309.00 262.00 216.00 182.00 142.00 111.00 83.00 61.00 42.00 25.00 19.00 14.00 14.00 15.00 20.00 33.00 36.00 27.00 4.00 0.00 diff --git a/RenderPipelineFile/data/ies_profiles/comet.ies b/RenderPipelineFile/data/ies_profiles/comet.ies index 8ae2f268..deeff16b 100644 --- a/RenderPipelineFile/data/ies_profiles/comet.ies +++ b/RenderPipelineFile/data/ies_profiles/comet.ies @@ -1,13 +1,13 @@ -IESNA:LM-63-1995 -[TEST] INFINITY LIGHTING PHOTOMETRIC REPORT NO. W00006 -[LUMINAIRE] ICEAL8-2X150-FL-CG1 -TILT=NONE -2 11000 1 19 5 1 1 1.375 1.04167 0 -1 1 314 -0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 -0 22.5 45 67.5 90 -9085.62 9033.58 8923.24 8687.43 8145.53 7454.18 6336.9 5050.9 3180.16 2162.21 1292.02 677.24 243.87 48.44 16.1 6.48 6.25 6.99 3.31 -9085.62 8902.2 8825.21 8578.38 8462.81 7787.37 6902.89 5633.64 3822.33 2423.91 1614.81 927.01 382.27 72.18 18.92 8.91 6.41 5.19 2.16 -9085.62 9029.2 9168.27 8989.93 8818.57 8434.18 7791.45 6806.71 5489.39 3939.15 2518.39 1573.92 770.93 308.04 81.18 28.92 14.71 8.88 4.75 -9085.62 9404.94 9244.69 9202.21 9146.45 8779.2 8121.15 7346.47 6513.38 5407.45 3976.98 2453.4 1106.06 428.6 117.57 35.96 12.34 7.19 1.55 -9085.62 9315.27 9317.59 9459.68 9433.84 9128.13 8380.85 7752.85 6655.4 5439.5 3990.21 2472.54 1140.85 377.1 65.79 17.41 9 4.71 2.77 +IESNA:LM-63-1995 +[TEST] INFINITY LIGHTING PHOTOMETRIC REPORT NO. W00006 +[LUMINAIRE] ICEAL8-2X150-FL-CG1 +TILT=NONE +2 11000 1 19 5 1 1 1.375 1.04167 0 +1 1 314 +0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 +0 22.5 45 67.5 90 +9085.62 9033.58 8923.24 8687.43 8145.53 7454.18 6336.9 5050.9 3180.16 2162.21 1292.02 677.24 243.87 48.44 16.1 6.48 6.25 6.99 3.31 +9085.62 8902.2 8825.21 8578.38 8462.81 7787.37 6902.89 5633.64 3822.33 2423.91 1614.81 927.01 382.27 72.18 18.92 8.91 6.41 5.19 2.16 +9085.62 9029.2 9168.27 8989.93 8818.57 8434.18 7791.45 6806.71 5489.39 3939.15 2518.39 1573.92 770.93 308.04 81.18 28.92 14.71 8.88 4.75 +9085.62 9404.94 9244.69 9202.21 9146.45 8779.2 8121.15 7346.47 6513.38 5407.45 3976.98 2453.4 1106.06 428.6 117.57 35.96 12.34 7.19 1.55 +9085.62 9315.27 9317.59 9459.68 9433.84 9128.13 8380.85 7752.85 6655.4 5439.5 3990.21 2472.54 1140.85 377.1 65.79 17.41 9 4.71 2.77 diff --git a/RenderPipelineFile/data/ies_profiles/cylinder_narrow.ies b/RenderPipelineFile/data/ies_profiles/cylinder_narrow.ies index 7d9efdc8..8de41aee 100644 --- a/RenderPipelineFile/data/ies_profiles/cylinder_narrow.ies +++ b/RenderPipelineFile/data/ies_profiles/cylinder_narrow.ies @@ -1,16 +1,16 @@ - IESNA:LM-63-1995 - [TEST]BALLABS TEST NO. 10974.0 - [MANUFAC] INFINITY LIGHTING INC - CARTHAGE, MISSOURI - [LUMINAIRE] 1/250W HALOGEN CLEAR MINICAN 7.5"DIA RECESSED LUMINAIRE - [LUMINAIRE] FACETED HYDROFORMED SEMI-SPECULAR REFLECTOR - [LUMINAIRE] CLEAR GLASS LENS w/4"DEEPSPUN SPECULAR ALUMINUM CONE -[LUMCAT]CATALOG NO. : TH75-250-T4 NARROW w/SPACER I -[LAMPCAT]LAMP CODE : 250Q/CL/MC (EHT) -TILT=NONE - 1 5000. 1.000000 19 1 1 1 .625 .625 .000 - 1.0000 1.0000 247.0000 - .0 5.0 10.0 15.0 20.0 25.0 30.0 35.0 40.0 45.0 - 50.0 55.0 60.0 65.0 70.0 75.0 80.0 85.0 90.0 - .0 - 8183. 6729. 4775. 3866. 2714. 1600. 992. 715. 373. 119. - 46. 20. 8. 4. 0. 0. 0. 0. 0. + IESNA:LM-63-1995 + [TEST]BALLABS TEST NO. 10974.0 + [MANUFAC] INFINITY LIGHTING INC - CARTHAGE, MISSOURI + [LUMINAIRE] 1/250W HALOGEN CLEAR MINICAN 7.5"DIA RECESSED LUMINAIRE + [LUMINAIRE] FACETED HYDROFORMED SEMI-SPECULAR REFLECTOR + [LUMINAIRE] CLEAR GLASS LENS w/4"DEEPSPUN SPECULAR ALUMINUM CONE +[LUMCAT]CATALOG NO. : TH75-250-T4 NARROW w/SPACER I +[LAMPCAT]LAMP CODE : 250Q/CL/MC (EHT) +TILT=NONE + 1 5000. 1.000000 19 1 1 1 .625 .625 .000 + 1.0000 1.0000 247.0000 + .0 5.0 10.0 15.0 20.0 25.0 30.0 35.0 40.0 45.0 + 50.0 55.0 60.0 65.0 70.0 75.0 80.0 85.0 90.0 + .0 + 8183. 6729. 4775. 3866. 2714. 1600. 992. 715. 373. 119. + 46. 20. 8. 4. 0. 0. 0. 0. 0. diff --git a/RenderPipelineFile/data/ies_profiles/cylinder_wide.ies b/RenderPipelineFile/data/ies_profiles/cylinder_wide.ies index 8bf4be03..59e3a51c 100644 --- a/RenderPipelineFile/data/ies_profiles/cylinder_wide.ies +++ b/RenderPipelineFile/data/ies_profiles/cylinder_wide.ies @@ -1,16 +1,16 @@ - IESNA:LM-63-1995 - [TEST]BALLABS TEST NO. 10979.0 - [MANUFAC] INFINITY LIGHTING INC - CARTHAGE, MISSOURI - [LUMINAIRE] 1/500W HALOGEN CLEAR MINICAN 7.5"DIA RECESSED LUMINAIRE - [LUMINAIRE] FACETED HYDROFORMED SEMI-SPECULAR REFLECTOR - [LUMINAIRE] CLEAR GLASS LENS w/4"DEEP SPUN SPECULAR ALUMINUM CONE -[LUMCAT]CATALOG NO. : TH75-500-T4 WIDE w/SPACER D -[LAMPCAT]LAMP CODE : 500Q/CL/MC (EVR) -TILT=NONE - 1 10450. 1.000000 19 1 1 1 .625 .625 .000 - 1.0000 1.0000 503.0000 - .0 5.0 10.0 15.0 20.0 25.0 30.0 35.0 40.0 45.0 - 50.0 55.0 60.0 65.0 70.0 75.0 80.0 85.0 90.0 - .0 - 4612. 5322. 6414. 6917. 6708. 5675. 4019. 2703. 1870. 872. - 169. 42. 9. 0. 0. 0. 0. 0. 0. + IESNA:LM-63-1995 + [TEST]BALLABS TEST NO. 10979.0 + [MANUFAC] INFINITY LIGHTING INC - CARTHAGE, MISSOURI + [LUMINAIRE] 1/500W HALOGEN CLEAR MINICAN 7.5"DIA RECESSED LUMINAIRE + [LUMINAIRE] FACETED HYDROFORMED SEMI-SPECULAR REFLECTOR + [LUMINAIRE] CLEAR GLASS LENS w/4"DEEP SPUN SPECULAR ALUMINUM CONE +[LUMCAT]CATALOG NO. : TH75-500-T4 WIDE w/SPACER D +[LAMPCAT]LAMP CODE : 500Q/CL/MC (EVR) +TILT=NONE + 1 10450. 1.000000 19 1 1 1 .625 .625 .000 + 1.0000 1.0000 503.0000 + .0 5.0 10.0 15.0 20.0 25.0 30.0 35.0 40.0 45.0 + 50.0 55.0 60.0 65.0 70.0 75.0 80.0 85.0 90.0 + .0 + 4612. 5322. 6414. 6917. 6708. 5675. 4019. 2703. 1870. 872. + 169. 42. 9. 0. 0. 0. 0. 0. 0. diff --git a/RenderPipelineFile/data/ies_profiles/defined.ies b/RenderPipelineFile/data/ies_profiles/defined.ies index c40a9418..526e11b6 100644 --- a/RenderPipelineFile/data/ies_profiles/defined.ies +++ b/RenderPipelineFile/data/ies_profiles/defined.ies @@ -1,16 +1,16 @@ -IESNA91 -[TEST] Test unknown -[MANUFAC] Manufacturing company unknown -TILT=NONE -1 13172.61 1 -37 1 -1 -2 -0 0 0 -1 1 0 - 0.00 2.50 5.00 7.50 9.11 10.00 10.80 12.50 15.83 22.50 25.00 27.50 32.50 34.28 35.00 42.50 47.02 47.50 47.81 50.00 50.78 52.50 - 55.00 57.50 60.00 62.50 65.00 67.50 70.00 72.50 75.00 77.50 80.00 82.50 85.00 87.50 90.00 - 0.00 - 8379.00 8409.00 8528.00 8126.65 8500.00 8564.00 8301.00 8158.61 8411.98 8036.59 7865.61 7283.76 5209.38 6038.93 4533.49 3091.00 - 3665.38 2996.97 3625.45 2194.62 3170.85 1115.28 651.00 450.00 244.00 102.00 80.00 43.00 5.00 2.00 0.00 0.00 0.00 0.00 0.00 0.00 - 0.00 +IESNA91 +[TEST] Test unknown +[MANUFAC] Manufacturing company unknown +TILT=NONE +1 13172.61 1 +37 1 +1 +2 +0 0 0 +1 1 0 + 0.00 2.50 5.00 7.50 9.11 10.00 10.80 12.50 15.83 22.50 25.00 27.50 32.50 34.28 35.00 42.50 47.02 47.50 47.81 50.00 50.78 52.50 + 55.00 57.50 60.00 62.50 65.00 67.50 70.00 72.50 75.00 77.50 80.00 82.50 85.00 87.50 90.00 + 0.00 + 8379.00 8409.00 8528.00 8126.65 8500.00 8564.00 8301.00 8158.61 8411.98 8036.59 7865.61 7283.76 5209.38 6038.93 4533.49 3091.00 + 3665.38 2996.97 3625.45 2194.62 3170.85 1115.28 651.00 450.00 244.00 102.00 80.00 43.00 5.00 2.00 0.00 0.00 0.00 0.00 0.00 0.00 + 0.00 diff --git a/RenderPipelineFile/data/ies_profiles/defined_diffuse.ies b/RenderPipelineFile/data/ies_profiles/defined_diffuse.ies index 60738a51..678d939c 100644 --- a/RenderPipelineFile/data/ies_profiles/defined_diffuse.ies +++ b/RenderPipelineFile/data/ies_profiles/defined_diffuse.ies @@ -1,64 +1,64 @@ -ERCO Leuchten GmbH -3399_0 - IES format -ERCO 89064 -ERCO Double Focus Downlight -DL40-QT12-2HA-SI-DOUBLE -1*QT-ax12 100W/12V -light output ratio: 0.5912 -ident no. for symmetry: 1 -D694 -TILT=NONE - 1 - 2200 - 2.2 - 19 - 1 - 1 - 2 - 0 - 0 - 0 - 1 - 1 - 100 - 0 - 5 - 10 - 15 - 20 - 25 - 30 - 35 - 40 - 45 - 50 - 55 - 60 - 65 - 70 - 75 - 80 - 85 - 90 - 0 - 681.879276637 - 557.110079287 - 490.896871945 - 628.387368307 - 682.069349408 - 430.426577604 - 286.398935592 - 254.901162159 - 76.9183773216 - 18.9393939394 - 11.0377973281 - .488758553275 - .00678831323993 - 0 - 0 - 0 - 0 - 0 - 0 -END +ERCO Leuchten GmbH +3399_0 - IES format +ERCO 89064 +ERCO Double Focus Downlight +DL40-QT12-2HA-SI-DOUBLE +1*QT-ax12 100W/12V +light output ratio: 0.5912 +ident no. for symmetry: 1 +D694 +TILT=NONE + 1 + 2200 + 2.2 + 19 + 1 + 1 + 2 + 0 + 0 + 0 + 1 + 1 + 100 + 0 + 5 + 10 + 15 + 20 + 25 + 30 + 35 + 40 + 45 + 50 + 55 + 60 + 65 + 70 + 75 + 80 + 85 + 90 + 0 + 681.879276637 + 557.110079287 + 490.896871945 + 628.387368307 + 682.069349408 + 430.426577604 + 286.398935592 + 254.901162159 + 76.9183773216 + 18.9393939394 + 11.0377973281 + .488758553275 + .00678831323993 + 0 + 0 + 0 + 0 + 0 + 0 +END  \ No newline at end of file diff --git a/RenderPipelineFile/data/ies_profiles/defined_diffuse_spot.ies b/RenderPipelineFile/data/ies_profiles/defined_diffuse_spot.ies index f3cb4c3e..d1207a40 100644 --- a/RenderPipelineFile/data/ies_profiles/defined_diffuse_spot.ies +++ b/RenderPipelineFile/data/ies_profiles/defined_diffuse_spot.ies @@ -1,29 +1,29 @@ -ERCO Leuchten GmbH BY: ERCO/LUM650/8701 -74216023+Q50MR16/C/CG55-FNV.IES -- -- -- -- -- -- -TILT=NONE -1 825 .825 73 2 1 2 -.2 0 0 -1.00 1.00 50 -0 1.25 2.5 3.75 5 6.25 7.5 8.75 10 11.25 12.5 13.75 15 16.25 17.5 18.75 20 -21.25 22.5 23.75 25 26.25 27.5 28.75 30 31.25 32.5 33.75 35 36.25 37.5 38.75 -40 41.25 42.5 43.75 45 46.25 47.5 48.75 50 51.25 52.5 53.75 55 56.25 57.5 -58.75 60 61.25 62.5 63.75 65 66.25 67.5 68.75 70 71.25 72.5 73.75 75 76.25 -77.5 78.75 80 81.25 82.5 83.75 85 86.25 87.5 88.75 90 -0 90 -1091 1113 1132 1160 1183 1206 1219 1223 1217 1207 1190 1162 1120 1063 995.2 -919 837.6 753.2 666.7 579.4 495.8 418.3 338.2 250.3 175.8 135.5 123.9 128.9 -138.2 142.1 140.9 137.1 133.3 131.4 129.6 125.5 116.4 100.9 81.69 62.57 47.27 -38.54 35 34.26 33.94 32.17 29.22 25.91 23.03 21.2 20.24 19.77 19.39 18.83 -18.14 17.47 16.97 16.74 16.74 16.85 16.97 17.03 17.03 17 16.97 16.95 16.95 -16.96 16.97 16.97 16.97 16.97 16.97 -1091 1113 1132 1160 1183 1206 1219 1223 1217 1207 1190 1162 1120 1063 995.2 -919 837.6 753.2 666.7 579.4 495.8 418.3 338.2 250.3 175.8 135.5 123.9 128.9 -138.2 142.1 140.9 137.1 133.3 131.4 129.6 125.5 116.4 100.9 81.69 62.57 47.27 -38.54 35 34.26 33.94 32.17 29.22 25.91 23.03 21.2 20.24 19.77 19.39 18.83 -18.14 17.47 16.97 16.74 16.74 16.85 16.97 17.03 17.03 17 16.97 16.95 16.95 -16.96 16.97 16.97 16.97 16.97 16.97 +ERCO Leuchten GmbH BY: ERCO/LUM650/8701 +74216023+Q50MR16/C/CG55-FNV.IES +- +- +- +- +- +- +TILT=NONE +1 825 .825 73 2 1 2 -.2 0 0 +1.00 1.00 50 +0 1.25 2.5 3.75 5 6.25 7.5 8.75 10 11.25 12.5 13.75 15 16.25 17.5 18.75 20 +21.25 22.5 23.75 25 26.25 27.5 28.75 30 31.25 32.5 33.75 35 36.25 37.5 38.75 +40 41.25 42.5 43.75 45 46.25 47.5 48.75 50 51.25 52.5 53.75 55 56.25 57.5 +58.75 60 61.25 62.5 63.75 65 66.25 67.5 68.75 70 71.25 72.5 73.75 75 76.25 +77.5 78.75 80 81.25 82.5 83.75 85 86.25 87.5 88.75 90 +0 90 +1091 1113 1132 1160 1183 1206 1219 1223 1217 1207 1190 1162 1120 1063 995.2 +919 837.6 753.2 666.7 579.4 495.8 418.3 338.2 250.3 175.8 135.5 123.9 128.9 +138.2 142.1 140.9 137.1 133.3 131.4 129.6 125.5 116.4 100.9 81.69 62.57 47.27 +38.54 35 34.26 33.94 32.17 29.22 25.91 23.03 21.2 20.24 19.77 19.39 18.83 +18.14 17.47 16.97 16.74 16.74 16.85 16.97 17.03 17.03 17 16.97 16.95 16.95 +16.96 16.97 16.97 16.97 16.97 16.97 +1091 1113 1132 1160 1183 1206 1219 1223 1217 1207 1190 1162 1120 1063 995.2 +919 837.6 753.2 666.7 579.4 495.8 418.3 338.2 250.3 175.8 135.5 123.9 128.9 +138.2 142.1 140.9 137.1 133.3 131.4 129.6 125.5 116.4 100.9 81.69 62.57 47.27 +38.54 35 34.26 33.94 32.17 29.22 25.91 23.03 21.2 20.24 19.77 19.39 18.83 +18.14 17.47 16.97 16.74 16.74 16.85 16.97 17.03 17.03 17 16.97 16.95 16.95 +16.96 16.97 16.97 16.97 16.97 16.97 diff --git a/RenderPipelineFile/data/ies_profiles/defined_spot.ies b/RenderPipelineFile/data/ies_profiles/defined_spot.ies index 4bfcc86e..96972a91 100644 --- a/RenderPipelineFile/data/ies_profiles/defined_spot.ies +++ b/RenderPipelineFile/data/ies_profiles/defined_spot.ies @@ -1,14 +1,14 @@ -IESNA:LM-63-1995 -[TEST] 100068_0 BY: ERCO / LUM650 -[DATE]02.12.2004 -[MANUFAC] ERCO Leuchten GmbH -[LUMCAT] 22619000_83670000 -[LUMINAIRE] Lightcast Downlight -[LAMPCAT] HIPAR-L30 35W 30 -TILT=NONE -1 2200 1 19 1 1 2 -.097 0 0 -1.00 1.00 35 -0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 -0 -7399.9 7219.5 6097.7 3707.0 836.0 281.6 239.8 145.2 90.2 46.2 33.0 17.6 12.1 -11.0 9.9 9.0 0.0 0.0 0.0 +IESNA:LM-63-1995 +[TEST] 100068_0 BY: ERCO / LUM650 +[DATE]02.12.2004 +[MANUFAC] ERCO Leuchten GmbH +[LUMCAT] 22619000_83670000 +[LUMINAIRE] Lightcast Downlight +[LAMPCAT] HIPAR-L30 35W 30 +TILT=NONE +1 2200 1 19 1 1 2 -.097 0 0 +1.00 1.00 35 +0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 +0 +7399.9 7219.5 6097.7 3707.0 836.0 281.6 239.8 145.2 90.2 46.2 33.0 17.6 12.1 +11.0 9.9 9.0 0.0 0.0 0.0 diff --git a/RenderPipelineFile/data/ies_profiles/display.ies b/RenderPipelineFile/data/ies_profiles/display.ies index 0f819b34..60b78a9f 100644 --- a/RenderPipelineFile/data/ies_profiles/display.ies +++ b/RenderPipelineFile/data/ies_profiles/display.ies @@ -1,29 +1,29 @@ -ERCO Leuchten GmbH BY: ERCO/LUM650/8701 -73011023+CHM70/U/PAR38/12.IES -- -- -- -- -- -- -TILT=NONE -1 4100 4.1 73 2 1 2 -.12 0 0 -1.00 1.00 92 -0 1.25 2.5 3.75 5 6.25 7.5 8.75 10 11.25 12.5 13.75 15 16.25 17.5 18.75 20 -21.25 22.5 23.75 25 26.25 27.5 28.75 30 31.25 32.5 33.75 35 36.25 37.5 38.75 -40 41.25 42.5 43.75 45 46.25 47.5 48.75 50 51.25 52.5 53.75 55 56.25 57.5 -58.75 60 61.25 62.5 63.75 65 66.25 67.5 68.75 70 71.25 72.5 73.75 75 76.25 -77.5 78.75 80 81.25 82.5 83.75 85 86.25 87.5 88.75 90 -0 90 -13800 13140 11450 9275 7132 5238 3687 2529 1731 1206 867.8 644.4 493.7 385.4 -306.8 250.1 211 186.1 170.7 160.7 153.9 149.1 145.6 142.8 140 136.7 134.3 -134.7 139.8 150.2 162.8 173.3 177.3 171.9 159.2 142.4 124.9 109.4 96.15 84.85 -75.12 66.63 59 51.87 44.88 37.79 30.91 24.68 19.51 15.73 13.11 11.31 10 8.932 -8.275 8.298 9.268 11.25 13.52 15.12 15.12 12.93 9.3 5.354 2.195 .6462 .3783 -.7748 1.22 1.234 .8939 .4114 0 -13800 13140 11450 9275 7132 5238 3687 2529 1731 1206 867.8 644.4 493.7 385.4 -306.8 250.1 211 186.1 170.7 160.7 153.9 149.1 145.6 142.8 140 136.7 134.3 -134.7 139.8 150.2 162.8 173.3 177.3 171.9 159.2 142.4 124.9 109.4 96.15 84.85 -75.12 66.63 59 51.87 44.88 37.79 30.91 24.68 19.51 15.73 13.11 11.31 10 8.932 -8.275 8.298 9.268 11.25 13.52 15.12 15.12 12.93 9.3 5.354 2.195 .6462 .3783 -.7748 1.22 1.234 .8939 .4114 0 +ERCO Leuchten GmbH BY: ERCO/LUM650/8701 +73011023+CHM70/U/PAR38/12.IES +- +- +- +- +- +- +TILT=NONE +1 4100 4.1 73 2 1 2 -.12 0 0 +1.00 1.00 92 +0 1.25 2.5 3.75 5 6.25 7.5 8.75 10 11.25 12.5 13.75 15 16.25 17.5 18.75 20 +21.25 22.5 23.75 25 26.25 27.5 28.75 30 31.25 32.5 33.75 35 36.25 37.5 38.75 +40 41.25 42.5 43.75 45 46.25 47.5 48.75 50 51.25 52.5 53.75 55 56.25 57.5 +58.75 60 61.25 62.5 63.75 65 66.25 67.5 68.75 70 71.25 72.5 73.75 75 76.25 +77.5 78.75 80 81.25 82.5 83.75 85 86.25 87.5 88.75 90 +0 90 +13800 13140 11450 9275 7132 5238 3687 2529 1731 1206 867.8 644.4 493.7 385.4 +306.8 250.1 211 186.1 170.7 160.7 153.9 149.1 145.6 142.8 140 136.7 134.3 +134.7 139.8 150.2 162.8 173.3 177.3 171.9 159.2 142.4 124.9 109.4 96.15 84.85 +75.12 66.63 59 51.87 44.88 37.79 30.91 24.68 19.51 15.73 13.11 11.31 10 8.932 +8.275 8.298 9.268 11.25 13.52 15.12 15.12 12.93 9.3 5.354 2.195 .6462 .3783 +.7748 1.22 1.234 .8939 .4114 0 +13800 13140 11450 9275 7132 5238 3687 2529 1731 1206 867.8 644.4 493.7 385.4 +306.8 250.1 211 186.1 170.7 160.7 153.9 149.1 145.6 142.8 140 136.7 134.3 +134.7 139.8 150.2 162.8 173.3 177.3 171.9 159.2 142.4 124.9 109.4 96.15 84.85 +75.12 66.63 59 51.87 44.88 37.79 30.91 24.68 19.51 15.73 13.11 11.31 10 8.932 +8.275 8.298 9.268 11.25 13.52 15.12 15.12 12.93 9.3 5.354 2.195 .6462 .3783 +.7748 1.22 1.234 .8939 .4114 0 diff --git a/RenderPipelineFile/data/ies_profiles/jelly_fish.ies b/RenderPipelineFile/data/ies_profiles/jelly_fish.ies index 1edf6501..9be75b19 100644 --- a/RenderPipelineFile/data/ies_profiles/jelly_fish.ies +++ b/RenderPipelineFile/data/ies_profiles/jelly_fish.ies @@ -1,80 +1,80 @@ -IESNA:LM-63-1995 -[TEST]BALLABS TEST NO. 12788.0 -[MANUFAC] INFINITY LIGHTING INC - CARTHAGE, MISSOURI -[LUMINAIRE] 1/100W ICETRON INDUCTION 20x15"DIRECT AREALIGHT LUMINAIRE -[LUMINAIRE] MIRO4 SPEC ALUM TYPE II OPTICS & CLEAR FLAT GLASS LENS -[LUMINAIRE] BLACK PANEL STREET SIDE SYLVANIA # QT 1x150 ICE/UNV-T -[LUMCAT] ICEAL2-150S-TYPE2/CG1-277 -[LAMPCAT] ICE100/QT150 2PIN -TILT=NONE - 1 11000. 1.000000 25 21 1 1 .938 1.266 .000 - 1.0000 1.0000 140.0000 - .0 5.0 10.0 15.0 20.0 25.0 30.0 35.0 40.0 45.0 - 50.0 55.0 60.0 62.5 65.0 67.5 70.0 72.5 75.0 77.5 - 80.0 82.5 85.0 87.5 90.0 - .0 5.0 15.0 25.0 35.0 45.0 55.0 65.0 75.0 85.0 - 90.0 95.0 105.0 115.0 125.0 135.0 145.0 155.0 165.0 175.0 - 180.0 - 1789. 1841. 1911. 1979. 2023. 2025. 1994. 1867. 1718. 1551. - 1374. 1208. 990. 808. 637. 469. 347. 202. 89. 18. - 9. 7. 7. 6. 0. - 1789. 1831. 1908. 1969. 2013. 2018. 1983. 1865. 1701. 1548. - 1353. 1193. 981. 842. 661. 503. 352. 172. 68. 19. - 8. 6. 6. 5. 0. - 1789. 1828. 1891. 1939. 1958. 1939. 1908. 1808. 1668. 1559. - 1428. 1300. 1222. 1161. 999. 778. 580. 318. 129. 39. - 10. 7. 7. 4. 0. - 1789. 1818. 1865. 1891. 1942. 1959. 1927. 1843. 1761. 1714. - 1673. 1599. 1445. 1325. 1165. 948. 668. 400. 186. 75. - 30. 9. 7. 2. 0. - 1789. 1811. 1821. 1891. 1941. 1974. 1980. 1943. 1993. 2082. - 1963. 1808. 1621. 1601. 1424. 1166. 829. 487. 279. 152. - 65. 15. 8. 2. 0. - 1789. 1794. 1804. 1893. 1935. 1995. 2020. 2145. 2342. 2221. - 2063. 2135. 2157. 1694. 948. 340. 101. 8. 0. 185. - 94. 32. 9. 0. 0. - 1789. 1775. 1803. 1868. 1931. 1992. 2076. 2357. 2333. 2285. - 2254. 2608. 2375. 2010. 1722. 1448. 981. 661. 376. 244. - 122. 38. 8. 0. 0. - 1789. 1763. 1798. 1839. 1918. 1961. 2151. 2291. 2310. 2263. - 2575. 2700. 2380. 2075. 1792. 1483. 1102. 702. 430. 298. - 192. 67. 11. 2. 0. - 1789. 1746. 1801. 1827. 1871. 1923. 2176. 2119. 2124. 2062. - 2553. 2596. 2201. 1907. 1678. 1368. 1015. 673. 440. 303. - 178. 63. 10. 1. 0. - 1789. 1738. 1788. 1808. 1830. 1895. 2081. 2024. 1953. 1883. - 2313. 2285. 1854. 1661. 1400. 1139. 833. 577. 404. 267. - 157. 54. 12. 1. 0. - 1789. 1735. 1783. 1797. 1812. 1868. 2046. 1983. 1911. 1805. - 2191. 2151. 1759. 1513. 1288. 1033. 802. 568. 386. 243. - 134. 56. 10. 3. 0. - 1789. 1734. 1778. 1786. 1797. 1838. 2016. 1951. 1871. 1766. - 2128. 2078. 1681. 1474. 1248. 1009. 739. 523. 349. 228. - 144. 52. 13. 2. 0. - 1789. 1733. 1774. 1773. 1771. 1777. 1959. 1895. 1811. 1647. - 2034. 1957. 1595. 1293. 1118. 925. 717. 481. 319. 208. - 125. 51. 7. 1. 0. - 1789. 1738. 1763. 1755. 1744. 1704. 1880. 1794. 1752. 1541. - 1772. 1720. 1427. 1191. 1003. 795. 588. 427. 274. 166. - 98. 39. 8. 3. 0. - 1789. 1734. 1744. 1732. 1708. 1647. 1674. 1703. 1620. 1413. - 1325. 1423. 1205. 983. 808. 642. 464. 312. 192. 107. - 57. 18. 8. 3. 0. - 1789. 1735. 1714. 1725. 1673. 1590. 1481. 1511. 1373. 1230. - 982. 993. 902. 771. 626. 456. 300. 187. 112. 60. - 31. 14. 8. 4. 0. - 1789. 1736. 1681. 1700. 1617. 1511. 1381. 1252. 1210. 1043. - 875. 657. 554. 487. 398. 294. 195. 122. 70. 40. - 19. 11. 8. 3. 0. - 1789. 1737. 1666. 1641. 1577. 1471. 1333. 1172. 1015. 890. - 728. 574. 409. 326. 243. 181. 114. 72. 40. 24. - 14. 9. 7. 4. 0. - 1789. 1740. 1679. 1601. 1493. 1388. 1268. 1114. 956. 769. - 624. 481. 347. 267. 201. 141. 85. 41. 25. 16. - 10. 7. 6. 3. 0. - 1789. 1737. 1693. 1616. 1479. 1340. 1187. 1041. 896. 724. - 572. 431. 303. 233. 167. 117. 62. 29. 18. 12. - 9. 6. 6. 4. 1. - 1789. 1742. 1698. 1623. 1491. 1354. 1196. 1040. 894. 722. - 565. 421. 302. 227. 159. 108. 57. 20. 14. 10. - 8. 6. 6. 5. 0. +IESNA:LM-63-1995 +[TEST]BALLABS TEST NO. 12788.0 +[MANUFAC] INFINITY LIGHTING INC - CARTHAGE, MISSOURI +[LUMINAIRE] 1/100W ICETRON INDUCTION 20x15"DIRECT AREALIGHT LUMINAIRE +[LUMINAIRE] MIRO4 SPEC ALUM TYPE II OPTICS & CLEAR FLAT GLASS LENS +[LUMINAIRE] BLACK PANEL STREET SIDE SYLVANIA # QT 1x150 ICE/UNV-T +[LUMCAT] ICEAL2-150S-TYPE2/CG1-277 +[LAMPCAT] ICE100/QT150 2PIN +TILT=NONE + 1 11000. 1.000000 25 21 1 1 .938 1.266 .000 + 1.0000 1.0000 140.0000 + .0 5.0 10.0 15.0 20.0 25.0 30.0 35.0 40.0 45.0 + 50.0 55.0 60.0 62.5 65.0 67.5 70.0 72.5 75.0 77.5 + 80.0 82.5 85.0 87.5 90.0 + .0 5.0 15.0 25.0 35.0 45.0 55.0 65.0 75.0 85.0 + 90.0 95.0 105.0 115.0 125.0 135.0 145.0 155.0 165.0 175.0 + 180.0 + 1789. 1841. 1911. 1979. 2023. 2025. 1994. 1867. 1718. 1551. + 1374. 1208. 990. 808. 637. 469. 347. 202. 89. 18. + 9. 7. 7. 6. 0. + 1789. 1831. 1908. 1969. 2013. 2018. 1983. 1865. 1701. 1548. + 1353. 1193. 981. 842. 661. 503. 352. 172. 68. 19. + 8. 6. 6. 5. 0. + 1789. 1828. 1891. 1939. 1958. 1939. 1908. 1808. 1668. 1559. + 1428. 1300. 1222. 1161. 999. 778. 580. 318. 129. 39. + 10. 7. 7. 4. 0. + 1789. 1818. 1865. 1891. 1942. 1959. 1927. 1843. 1761. 1714. + 1673. 1599. 1445. 1325. 1165. 948. 668. 400. 186. 75. + 30. 9. 7. 2. 0. + 1789. 1811. 1821. 1891. 1941. 1974. 1980. 1943. 1993. 2082. + 1963. 1808. 1621. 1601. 1424. 1166. 829. 487. 279. 152. + 65. 15. 8. 2. 0. + 1789. 1794. 1804. 1893. 1935. 1995. 2020. 2145. 2342. 2221. + 2063. 2135. 2157. 1694. 948. 340. 101. 8. 0. 185. + 94. 32. 9. 0. 0. + 1789. 1775. 1803. 1868. 1931. 1992. 2076. 2357. 2333. 2285. + 2254. 2608. 2375. 2010. 1722. 1448. 981. 661. 376. 244. + 122. 38. 8. 0. 0. + 1789. 1763. 1798. 1839. 1918. 1961. 2151. 2291. 2310. 2263. + 2575. 2700. 2380. 2075. 1792. 1483. 1102. 702. 430. 298. + 192. 67. 11. 2. 0. + 1789. 1746. 1801. 1827. 1871. 1923. 2176. 2119. 2124. 2062. + 2553. 2596. 2201. 1907. 1678. 1368. 1015. 673. 440. 303. + 178. 63. 10. 1. 0. + 1789. 1738. 1788. 1808. 1830. 1895. 2081. 2024. 1953. 1883. + 2313. 2285. 1854. 1661. 1400. 1139. 833. 577. 404. 267. + 157. 54. 12. 1. 0. + 1789. 1735. 1783. 1797. 1812. 1868. 2046. 1983. 1911. 1805. + 2191. 2151. 1759. 1513. 1288. 1033. 802. 568. 386. 243. + 134. 56. 10. 3. 0. + 1789. 1734. 1778. 1786. 1797. 1838. 2016. 1951. 1871. 1766. + 2128. 2078. 1681. 1474. 1248. 1009. 739. 523. 349. 228. + 144. 52. 13. 2. 0. + 1789. 1733. 1774. 1773. 1771. 1777. 1959. 1895. 1811. 1647. + 2034. 1957. 1595. 1293. 1118. 925. 717. 481. 319. 208. + 125. 51. 7. 1. 0. + 1789. 1738. 1763. 1755. 1744. 1704. 1880. 1794. 1752. 1541. + 1772. 1720. 1427. 1191. 1003. 795. 588. 427. 274. 166. + 98. 39. 8. 3. 0. + 1789. 1734. 1744. 1732. 1708. 1647. 1674. 1703. 1620. 1413. + 1325. 1423. 1205. 983. 808. 642. 464. 312. 192. 107. + 57. 18. 8. 3. 0. + 1789. 1735. 1714. 1725. 1673. 1590. 1481. 1511. 1373. 1230. + 982. 993. 902. 771. 626. 456. 300. 187. 112. 60. + 31. 14. 8. 4. 0. + 1789. 1736. 1681. 1700. 1617. 1511. 1381. 1252. 1210. 1043. + 875. 657. 554. 487. 398. 294. 195. 122. 70. 40. + 19. 11. 8. 3. 0. + 1789. 1737. 1666. 1641. 1577. 1471. 1333. 1172. 1015. 890. + 728. 574. 409. 326. 243. 181. 114. 72. 40. 24. + 14. 9. 7. 4. 0. + 1789. 1740. 1679. 1601. 1493. 1388. 1268. 1114. 956. 769. + 624. 481. 347. 267. 201. 141. 85. 41. 25. 16. + 10. 7. 6. 3. 0. + 1789. 1737. 1693. 1616. 1479. 1340. 1187. 1041. 896. 724. + 572. 431. 303. 233. 167. 117. 62. 29. 18. 12. + 9. 6. 6. 4. 1. + 1789. 1742. 1698. 1623. 1491. 1354. 1196. 1040. 894. 722. + 565. 421. 302. 227. 159. 108. 57. 20. 14. 10. + 8. 6. 6. 5. 0. diff --git a/RenderPipelineFile/data/ies_profiles/medium_scatter.ies b/RenderPipelineFile/data/ies_profiles/medium_scatter.ies index 76845924..f375bbed 100644 --- a/RenderPipelineFile/data/ies_profiles/medium_scatter.ies +++ b/RenderPipelineFile/data/ies_profiles/medium_scatter.ies @@ -1,385 +1,385 @@ -IESNA:LM-63-2002 -[TEST] 5890_0 -[TESTLAB] ERCO GmbH -[ISSUEDATE] 02-12-2008 -[MANUFAC] ERCO GmbH -[LUMCAT] 33519000 -[LUMINAIRE] Kubus Floor washlight -[LAMP] HIT-TC-CE 20W -TILT=NONE -1 1615 1 37 73 1 2 0.103 0.059 0 -1.0 1.0 24 -0 2.5 5 7.5 10 12.5 15 17.5 20 22.5 25 27.5 30 -32.5 35 37.5 40 42.5 45 47.5 50 52.5 55 57.5 60 62.5 -65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 -0 2.5 5 7.5 10 12.5 15 17.5 20 22.5 25 27.5 30 -32.5 35 37.5 40 42.5 45 47.5 50 52.5 55 57.5 60 62.5 -65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 92.5 95 -97.5 100 102.5 105 107.5 110 112.5 115 117.5 120 122.5 125 127.5 -130 132.5 135 137.5 140 142.5 145 147.5 150 152.5 155 157.5 160 -162.5 165 167.5 170 172.5 175 177.5 180 -174.408695 186.514735 207.74714 232.0109 259.393225 290.905105 329.41801 371.299805 413.25912 -451.245535 487.59757 510.86326 514.272525 504.50339 491.06659 481.24739 473.75702 470.79188 -471.491175 465.94688 448.826265 421.89291 385.692685 336.53693 275.843615 220.833485 172.667725 -138.452335 109.86845 87.28106 67.431095 50.761065 36.44086 23.40458 12.382205 3.77264 -0 -174.408695 186.24503 208.21226 231.893005 258.99432 290.456135 328.705795 370.82015 411.190305 -451.636365 486.478375 509.308015 514.198235 503.661975 490.829185 481.08266 472.12264 470.44627 -469.93593 463.828 448.5824 424.378395 389.798015 336.6629 278.547125 220.067975 173.953265 -138.071195 110.2722 87.81078 67.763785 50.964555 36.345575 23.34321 12.27077 3.75649 -0 -174.408695 186.0157 207.63086 230.823875 258.333785 290.4416 328.276205 369.71872 411.584365 -449.65153 484.175385 507.77215 512.935305 503.86708 488.84435 479.21249 469.905245 467.854195 -466.145525 462.72011 447.43575 420.92068 387.178485 339.510145 278.682785 222.855465 176.492045 -140.47916 111.16368 87.650895 68.65688 51.0017 36.442475 23.260845 12.34506 3.72096 -0 -174.408695 185.77345 207.59856 230.94177 258.35478 288.73293 327.291055 368.62052 409.81917 -449.25424 483.86692 504.188465 511.136195 501.5544 486.959645 476.44115 467.35193 462.392265 -461.99982 458.337 441.14371 417.88771 386.903935 340.17714 282.22448 226.653945 178.465575 -140.67942 111.313875 88.38249 68.78285 51.319855 36.348805 23.29153 12.31599 3.74357 -0 -174.408695 185.81867 206.934795 230.462115 258.078615 287.920585 325.67121 366.390205 407.695445 -447.705455 480.947 502.689745 508.27926 499.385455 486.3088 474.42886 464.59028 457.1419 -454.82276 449.010375 435.418535 415.26818 383.87258 338.59121 284.75034 231.444035 182.480465 -144.773445 114.2774 89.45808 68.965345 51.22134 36.58298 23.16556 12.336985 3.74034 -0 -174.408695 185.96079 206.6231 229.86618 257.293725 285.88084 323.179265 363.877265 405.780055 -444.85175 477.25511 499.88772 505.199455 498.019165 484.188305 472.73311 459.288235 450.66575 -446.08238 437.337155 424.565735 406.19834 378.51401 336.5983 285.727415 230.462115 184.02602 -145.044765 114.05776 89.47746 68.874905 51.26979 36.290665 23.170405 12.16418 3.741955 -0 -174.408695 185.080615 205.778455 229.060295 256.58797 284.425725 320.876275 361.193135 402.785845 -439.17018 474.37718 496.98718 502.315065 496.097315 482.618525 467.20335 455.396085 444.68379 -436.20181 425.79475 415.253645 395.80743 367.84209 328.870525 279.27226 228.09291 181.41941 -142.81122 111.34133 88.20161 67.660425 50.496205 35.347505 22.71013 11.8541 3.64667 -0 -174.408695 185.16944 205.778455 228.01862 254.89545 283.282305 317.630125 357.39304 399.119795 -435.883655 468.8991 491.830485 500.61447 494.71003 479.5581 463.735945 448.241635 437.19019 -426.04023 416.629625 404.18282 382.9488 355.27093 319.555205 273.821635 224.84676 178.866095 -142.686865 109.811925 86.98713 66.442715 49.49329 34.44472 22.05767 11.51172 3.596605 -0 -174.408695 184.83352 204.96934 227.102915 252.97683 281.0746 314.581005 353.40722 394.09553 -431.413335 463.55668 487.50713 496.41224 491.533325 477.0064 460.560855 444.61273 429.75796 -418.42389 407.062365 392.67433 372.40285 342.278255 310.327095 271.008305 225.303805 181.03181 -143.474985 111.09908 86.539775 65.80479 49.13476 34.26384 21.964 11.471345 3.561075 -0 -174.408695 184.61388 204.66895 225.662335 251.44581 279.35301 311.732145 348.838385 389.20854 -425.237575 458.577635 478.813585 491.26685 487.78168 473.40172 459.32861 439.76773 423.69848 -410.85277 395.89464 383.12968 362.301025 335.831175 306.092565 270.66108 227.96371 185.74438 -147.867785 115.01707 90.105695 67.922055 50.4203 35.024505 22.307995 11.5957 3.632135 -0 -174.408695 184.3684 203.498075 224.45916 249.356 276.85622 307.88683 344.129045 382.67102 -419.811175 451.79302 472.605525 485.17184 485.580435 472.779945 456.84797 436.20504 418.64999 -402.03487 388.609375 373.25557 354.79612 330.240045 302.678455 272.830025 233.504775 195.090385 -156.071985 121.898585 94.663225 71.05677 52.58117 36.04034 22.97499 11.89609 3.72419 -0 -174.408695 183.685255 202.709955 222.878075 247.94126 274.57907 304.72466 339.216215 376.800495 -412.563055 445.169905 466.672015 479.269015 481.484795 470.04575 454.24782 433.264125 412.986185 -393.296105 376.638995 363.499355 346.72112 324.616615 300.136445 272.33422 237.707005 202.08495 -163.588195 129.40995 100.606425 75.519015 54.960065 37.72317 23.732425 12.249775 3.79525 -0 -174.408695 183.698175 202.34981 221.40681 245.207065 271.96923 300.452985 332.875725 368.97582 -404.827205 436.15659 459.50949 472.34551 475.85652 467.62002 451.28914 430.344205 407.524255 -386.422665 368.55915 352.41238 336.667745 315.92307 294.106035 269.95048 238.42245 206.09338 -169.021055 135.04307 105.23986 79.136615 58.03664 39.594955 24.95175 12.727815 3.94706 -0 -174.408695 183.362255 201.279065 219.593165 242.287145 267.93496 295.45133 326.92445 361.6631 -395.453745 426.23726 450.832095 464.575745 468.219185 463.316045 447.524575 426.09191 402.365945 -381.08832 361.018715 341.90519 325.356285 307.681725 286.76263 266.65265 238.2125 207.50812 -172.196145 141.26728 110.629115 84.08013 61.09545 42.08367 26.23729 13.39481 4.119865 -0 -174.408695 182.945585 200.197015 218.37707 239.758055 264.564455 290.078225 319.84106 353.27156 -386.06898 417.316 440.39112 455.34279 459.488495 455.677095 442.967045 422.369335 397.143035 -372.546585 352.20243 332.633475 315.611375 297.135775 279.874655 259.251105 234.095865 206.73615 -174.828595 144.48113 114.372685 88.81208 64.07997 44.720965 27.60681 14.17001 4.349195 -0 -174.408695 182.40133 199.173105 216.615105 237.471215 259.97624 284.44995 313.453735 345.023755 -376.21748 405.19381 429.769265 445.040705 449.74197 448.03653 438.248015 419.145795 394.97409 -368.08757 345.46465 323.909245 306.0425 286.615665 269.506355 251.515255 229.3946 204.609195 -175.930025 147.56255 118.830085 93.23718 67.569985 48.015565 29.31548 15.28113 4.665735 -0 -174.408695 182.451395 198.15081 214.48169 234.806465 255.9452 279.306175 306.43979 336.45941 -365.60047 394.78998 418.26885 432.44209 438.041295 439.065205 430.508935 413.95034 390.747635 -363.189275 340.133535 317.4444 296.87253 277.213135 258.398385 241.721895 219.583475 196.73607 -172.06856 147.98568 120.060715 95.751735 70.318715 50.02301 30.76898 16.00465 4.92898 -0 -174.408695 182.04603 197.529035 213.217145 232.16594 252.60861 274.01382 299.32733 327.76425 -354.45051 382.578965 404.51228 419.220085 425.17782 426.267945 419.220085 406.04976 384.66716 -358.06488 332.93871 309.041555 288.10631 267.28573 249.26233 230.17626 210.71551 190.364895 -168.20871 144.069305 119.61013 96.11188 71.200505 51.47974 31.6217 16.671645 5.033955 -0 -174.408695 181.676195 196.29679 211.09342 228.63555 248.30625 269.251185 291.64316 317.42825 -343.00016 369.3505 390.43271 404.33786 411.280745 412.42901 407.16734 397.54517 377.446495 -351.0364 325.69705 300.985935 277.3924 255.271745 236.02902 217.516275 197.474125 178.68037 -159.589455 138.03082 116.139495 93.6054 70.66917 51.45713 31.87364 16.852525 5.10986 -0 -174.408695 181.203 194.80453 209.263625 225.701095 244.48516 263.230465 284.5953 308.511835 -332.18612 356.661445 376.949075 388.55931 395.143665 397.201175 392.65172 384.181045 365.81688 -343.747905 317.0568 290.56111 265.281515 241.55878 221.750805 201.70381 181.824775 166.181885 -148.051895 130.196455 109.337115 89.80692 68.093245 49.762995 30.883645 16.37287 4.90314 -0 -174.408695 180.62483 193.659495 207.1076 223.297975 239.993845 257.35671 277.717015 299.85059 -321.383385 343.19719 361.583965 373.266875 379.119635 380.686185 377.17679 368.61083 353.34908 -333.124435 307.584825 281.76259 254.60152 229.619085 208.191265 186.960475 167.83403 150.002815 -133.833435 116.81941 99.475925 82.00324 62.52634 45.954825 28.67594 15.095405 4.54138 -0 -174.408695 180.374505 192.331965 205.481295 219.609315 235.145615 251.65576 271.001845 290.536885 -308.931735 328.68803 345.282155 357.617525 362.75807 362.74838 358.896605 350.485685 338.001735 -321.325245 297.13739 273.072275 245.37664 218.78728 195.72508 173.753005 154.392385 136.343145 -120.936045 104.64231 88.135395 71.281255 56.19877 40.44929 25.74633 13.377045 4.058495 -0 -174.408695 179.880315 191.446945 203.469005 216.81698 231.182405 246.211595 263.30637 280.77744 -297.23752 315.824555 329.794305 340.80699 345.07382 344.1565 340.139995 332.29917 320.785835 -303.54571 282.390825 260.29278 235.429855 208.775895 184.379705 161.83269 142.74985 124.689305 -109.673035 93.229105 78.34365 62.783125 49.036245 35.114945 22.36452 11.571475 3.490015 -0 -174.408695 179.17456 190.038665 201.33236 213.33181 226.71047 240.778735 255.6222 271.670455 -285.772635 302.394215 315.40627 325.062355 328.29397 326.34305 322.249025 313.67822 302.148735 -286.48808 267.7347 245.39602 222.196545 197.21088 173.517215 152.102315 131.985875 113.29548 -97.783405 82.02585 68.332265 54.188095 42.4422 29.99055 19.28956 9.824045 2.989365 -0 -174.408695 178.988835 188.701445 198.966385 210.274615 222.206235 234.903365 248.827895 262.73466 -274.713115 289.40154 300.36416 308.708865 311.65624 308.628115 304.15618 295.365735 283.45188 -267.303495 250.55433 230.18272 207.64055 184.24243 161.705105 141.608045 121.21867 103.57964 -88.16931 72.57164 60.15875 47.1903 36.6605 25.64297 16.55052 8.299485 2.57108 -0 -174.408695 178.69652 187.04284 196.76837 207.15928 217.98301 229.467275 241.17764 253.42257 -263.81348 275.950205 285.804935 291.993615 295.246225 291.547875 285.05719 276.234445 263.970135 -248.47421 231.321295 211.831475 192.180155 170.95421 150.42756 130.877985 111.446305 94.93293 -79.66795 65.967905 54.149335 42.66507 32.902395 22.72951 14.704575 7.33533 0 -0 -174.408695 178.239475 186.19658 194.864285 204.22321 213.691955 223.51923 233.569375 244.72418 -253.19324 263.492095 271.65269 277.710555 277.952805 274.50478 266.75601 256.42324 244.98904 -228.485355 213.30274 193.830685 176.185195 156.010615 137.987215 119.747405 102.363545 86.54785 -72.43275 60.16521 49.04432 38.231895 29.352625 20.40714 13.041125 6.49876 0 -0 -174.408695 177.793735 184.86259 192.27221 200.81556 208.96162 217.803745 225.99018 235.68664 -243.634055 251.5847 258.98463 262.4052 263.482405 259.29148 250.31531 239.044225 227.254725 -210.89639 196.811975 177.61447 159.721885 141.88744 125.748745 109.088405 93.76367 79.11885 -66.058345 54.20909 44.43834 34.35751 26.322885 18.131605 11.510105 5.75909 0 -0 -174.408695 177.590245 183.833835 190.342285 197.826195 204.557515 212.002665 218.966545 228.04123 -234.221835 240.33784 246.4167 249.464205 248.98778 244.144395 236.51675 223.514385 211.553695 -195.529665 180.51178 162.489995 146.727595 129.301745 114.02223 98.658735 85.464185 71.749605 -60.57542 49.116995 40.008395 30.8465 23.545085 16.2469 10.22618 5.11309 0 -0 -174.408695 177.154195 182.46916 188.09905 194.67533 200.503865 206.673165 213.485235 219.71752 -225.174605 229.85972 234.325195 236.8236 236.45215 231.15818 224.464005 210.445805 197.648545 -182.80831 167.0556 149.79448 134.86542 118.41826 104.31608 90.525595 78.009345 66.00505 -55.564075 45.39765 36.684725 28.412695 21.406825 14.77725 9.226495 4.626975 0 -0 -174.408695 176.256255 181.094795 186.272485 191.61006 196.479285 201.770025 207.416065 212.11733 -216.90096 220.187485 223.614515 225.177835 224.51084 220.113195 212.525925 200.513555 186.007625 -172.38833 157.23317 140.03019 126.4222 110.454695 97.357045 84.34499 72.57164 61.1116 -51.357 42.08367 34.126565 26.351955 19.610945 13.566 8.45937 4.23776 0 -0 -174.408695 175.73138 179.76242 184.66233 188.632 192.645275 196.97832 201.495475 204.793305 -209.286235 211.834705 213.62574 214.19422 213.265595 210.22778 203.685415 192.11071 177.603165 -163.5026 151.472465 134.812125 121.074935 105.02345 92.340855 79.721245 69.084855 57.55537 -48.4177 39.81298 32.178875 24.714345 18.496595 12.67775 7.94903 3.96321 0 -0 -174.408695 175.613485 178.6836 182.267285 185.820285 189.315145 192.125245 195.97702 199.305535 -202.393415 204.48807 205.019405 205.41508 205.3634 202.38857 196.944405 185.96402 173.59958 -160.138555 148.16979 132.954875 119.311355 103.600635 90.74685 77.9399 66.67689 55.974285 -46.920595 37.860445 30.55903 23.590305 17.731085 12.2417 7.621185 3.806555 0 -0 -174.408695 175.10799 177.76305 180.836395 183.66103 186.15136 188.46081 191.488935 194.10685 -197.357845 198.94539 199.118195 199.760965 199.518715 196.76514 191.34843 182.335115 171.995885 -160.36627 147.55286 134.4326 120.79231 105.412665 92.05177 79.175375 67.402025 56.626745 -46.99327 38.238355 30.328085 23.34321 17.3451 11.91224 7.437075 3.70804 0 -0 -174.408695 174.828595 176.863495 179.45557 181.78763 183.617425 185.55704 188.145885 190.55708 -193.097475 195.229275 195.63141 196.17405 195.445685 192.54353 189.00022 180.035355 172.041105 -161.233525 149.59099 136.47719 124.091755 109.01896 96.05374 82.159895 70.493135 59.00564 -48.887665 38.78261 31.019305 23.80187 17.391935 12.139955 7.395085 3.74357 0 -0 -174.408695 174.440995 175.96394 178.01822 179.80118 181.82639 183.8839 185.306715 187.88587 -190.261535 193.25413 193.7354 195.287415 193.26059 191.921755 188.149115 180.84447 173.081165 -162.91474 152.118465 140.422635 127.951605 114.760285 101.18621 88.103095 76.126255 62.618395 -51.65093 40.43314 32.382365 23.92784 17.72624 11.936465 7.340175 3.68543 0 -0 -174.408695 174.024325 175.10476 176.50981 178.92585 180.594145 182.984345 184.35871 187.919785 -189.289305 192.222145 193.940505 195.41177 192.7018 192.68888 188.04091 183.685255 172.364105 -164.502285 153.76738 145.487275 132.864435 119.589135 106.14426 95.835715 81.85143 68.9282 -55.98559 43.85694 34.32844 25.424945 18.163905 12.030135 7.330485 3.606295 0 -0 -174.408695 173.61896 174.54274 176.065685 178.006915 179.854475 183.016645 185.05962 188.413975 -189.49764 192.212455 194.786765 194.56874 191.93629 189.91431 183.37356 177.880945 166.09629 -158.64468 150.2596 143.726925 132.08439 121.730625 108.846155 99.198145 83.61501 71.50251 -56.894835 47.311425 35.25868 26.67657 18.37547 12.275615 7.330485 3.61114 0 -0 -174.408695 173.756235 174.45553 175.49882 177.14612 180.154865 183.8839 185.736305 190.18563 -191.53577 191.908835 191.818395 188.56094 185.60872 178.313765 171.340195 161.23837 153.355555 -145.84096 137.832175 129.3292 118.24707 110.286735 99.595435 90.012025 75.26546 66.239225 -53.54371 45.03589 34.38658 26.439165 18.10738 12.74558 7.40962 3.75649 0 -0 -174.408695 173.585045 173.95811 174.99171 177.44005 181.33543 185.490825 187.7922 191.614905 -192.083255 188.82903 186.149745 178.67391 172.028185 160.942825 150.82485 141.67103 133.22135 -124.178965 116.11527 106.610995 96.239465 88.77009 81.032625 71.487975 62.754055 53.984605 -45.586605 38.183445 29.691775 23.7405 17.04794 12.198095 7.2352 3.70804 0 -0 -174.408695 173.265275 173.491375 174.689705 177.935855 182.661345 187.66623 189.744735 192.267365 -190.04674 180.955905 174.47168 164.38762 153.798065 141.520835 131.0411 121.204135 113.80259 -102.489515 94.524335 86.759415 76.943445 68.85391 63.267625 54.02175 49.41577 41.85111 -36.145315 29.59326 24.6126 19.604485 14.759485 10.36184 6.42124 3.254225 0 -0 -174.408695 173.236205 173.774 175.20812 178.942 184.40393 189.163335 190.901075 189.825485 -182.373875 171.369265 160.85723 148.17302 135.180345 123.28264 112.244115 103.4892 95.092815 -85.105655 77.379495 68.810305 63.206255 55.62383 50.347625 43.370825 38.852055 32.621385 -28.745385 23.52086 19.62548 15.68165 12.06728 8.328555 5.35211 2.73258 0 -0 -174.408695 173.239435 173.891895 175.91226 179.90454 185.797675 190.46987 190.14041 185.81544 -173.213595 160.513235 146.33515 132.166755 120.306195 107.38458 97.973975 89.366025 80.89858 -72.179195 66.06642 57.84607 52.87833 46.54107 42.19026 36.4021 32.27416 27.53575 -23.8374 19.94202 16.781465 13.02336 10.105055 7.106 4.59629 0 0 -0 -174.408695 173.20552 173.711015 176.71653 181.104485 187.09452 190.474715 187.0816 178.90647 -164.02909 146.5774 131.882515 118.54423 107.00021 95.40774 86.281375 77.637895 70.993785 -62.77828 57.305045 50.88542 45.589835 40.631785 36.048415 31.555485 27.818375 23.79218 -20.70107 17.61965 14.63836 11.52787 8.880885 6.29204 4.06011 0 0 -0 -174.408695 173.065015 173.988795 177.33023 182.373875 187.4369 188.785425 182.138085 170.57953 -152.97926 133.791445 118.523235 106.14749 95.951995 85.415735 76.97736 69.25443 62.770205 -55.66259 50.52689 44.59661 39.808135 35.03258 31.59263 27.611655 24.178165 20.91748 -18.173595 15.224605 12.766575 10.14543 7.844055 5.5879 3.635365 0 0 -0 -174.408695 172.98911 174.14545 178.15388 182.761475 187.45305 186.065765 176.462975 160.29521 -141.369025 122.00033 107.53639 95.81795 86.152175 77.279365 70.12976 62.579635 56.000125 -49.375395 44.746805 39.41246 35.19731 31.025765 27.695635 24.036045 21.284085 18.52082 -16.057945 13.57246 11.274315 8.977785 6.984875 5.001655 3.33336 0 0 -0 -174.408695 172.806615 174.45876 178.895165 183.724015 187.5984 182.0751 169.398965 150.803855 -130.74071 112.386235 98.88322 87.615365 78.43409 70.72731 64.033135 56.70588 50.386385 -44.19609 39.87435 35.063265 31.368145 27.682715 24.426875 21.60224 19.40907 16.75401 -14.69973 12.391895 10.50719 8.43676 6.535905 4.743255 3.17509 0 0 -0 -174.408695 172.53045 174.86897 179.73335 184.53636 185.84774 177.2947 161.223835 140.929745 -120.923125 103.36 91.22166 80.073315 72.41014 64.588695 58.03018 50.86927 45.45902 -39.9228 35.454095 31.28901 28.050935 24.86131 22.349985 20.00339 18.15906 15.883525 -13.86962 11.888015 9.911255 8.076615 6.232285 4.55753 3.050735 0 0 -0 -174.408695 172.59505 175.206505 180.846085 184.73985 182.926205 171.92967 152.75962 130.564675 -111.436615 95.987525 83.76682 74.78419 67.043495 58.480765 52.46489 45.98874 41.200265 -35.935365 32.256395 28.645255 25.93367 23.283455 21.15973 19.152285 17.159375 15.273055 -13.322135 11.371215 9.52204 7.74554 6.00457 4.429945 2.96837 0 0 -0 -174.408695 172.65319 175.79921 181.81347 184.10354 179.8787 166.0866 144.311555 121.771 -103.31155 89.721325 78.138545 70.134605 61.927175 54.165485 48.031715 41.86403 37.450235 -33.034825 29.79998 26.809 24.56092 22.301535 20.47497 18.580575 16.62158 14.68358 -12.834405 10.964235 9.239415 7.521055 5.8463 4.300745 2.91669 0 0 -0 -174.408695 172.54983 176.288555 182.402945 183.501145 176.62932 159.27776 135.890945 113.87365 -96.914535 83.396985 73.513185 65.93399 57.41325 49.785605 43.77942 38.499985 34.433415 -30.7819 28.15914 25.65266 23.68236 21.48596 19.64809 17.85544 15.990115 14.139325 -12.404815 10.615395 8.90834 7.24166 5.676725 4.207075 2.866625 0 0 -0 -174.408695 172.6112 176.75206 182.687185 182.399715 172.335035 152.725705 128.623445 106.87747 -91.30887 77.97543 69.29642 61.817355 53.01722 45.56561 40.444445 35.609135 32.146575 -29.349395 27.07709 24.817705 22.78119 20.77213 19.09576 17.25466 15.450705 13.606375 -11.907395 10.25848 8.680625 7.12861 5.586285 4.168315 2.84563 0 0 -0 -174.408695 172.65319 177.25271 182.793775 180.37289 168.55432 146.091285 121.685405 100.83414 -86.28945 74.537095 66.0212 57.721715 49.23812 42.501955 37.76193 33.60492 30.625245 -28.325485 26.255055 24.032815 22.38713 20.347385 18.635485 16.61835 14.885455 13.263995 -11.68291 10.13897 8.580495 7.085005 5.54591 4.148935 2.83917 0 0 -0 -174.408695 172.829225 177.63385 182.6565 177.72106 163.88374 140.047955 115.761585 95.45942 -81.27972 71.46698 62.38745 53.858635 46.13732 39.93572 35.57845 32.14173 29.65463 -27.70694 25.53638 23.709815 21.804115 19.872575 18.01694 16.24367 14.659355 13.131565 -11.59893 10.0776 8.538505 7.04786 5.531375 4.148935 2.85855 0 0 -0 -174.408695 172.39802 178.06021 182.82123 175.63448 159.2067 134.075685 110.622655 91.1506 -77.954435 68.312885 58.95719 50.370235 43.374055 37.750625 33.890775 31.080675 29.097455 -26.881675 24.97436 23.196245 21.27924 19.355775 17.68102 16.07248 14.55438 13.054045 -11.542405 10.050145 8.50459 7.03171 5.51361 4.14732 2.86178 0 0 -0 -174.408695 172.6758 178.2637 181.937825 173.00526 154.634635 128.86408 106.006985 87.4684 -75.36236 65.722425 56.02112 47.76201 41.135665 36.129165 32.883015 30.466975 28.41431 -26.435935 24.55446 22.48726 20.64939 19.05054 17.49368 15.99173 14.44456 13.02013 -11.51495 10.02915 8.490055 7.03171 5.51361 4.148935 2.86178 0 0 -0 -174.408695 172.515915 178.862865 181.262755 170.42772 149.673355 123.88019 101.937185 84.398285 -73.20472 63.22402 53.49849 45.563995 39.003865 34.850085 32.140115 29.83228 28.13007 -25.880375 24.04412 21.968845 20.298935 18.78245 17.330565 15.93036 14.43487 13.0492 -11.53433 10.04207 8.49813 7.039785 5.534605 4.15701 2.882775 0 0 -0 -174.408695 172.59182 179.02598 180.31475 167.93739 145.270865 119.443785 98.58606 82.00001 -71.45083 60.47529 51.22457 43.32076 37.60043 33.96345 31.77674 29.41238 27.715015 -25.70111 23.40781 21.67976 20.166505 18.69847 17.31603 15.977195 14.468785 13.070195 -11.55694 10.069525 8.53689 7.065625 5.56529 4.19577 2.908615 0 0 -0 -174.408695 172.767855 179.47818 179.489485 165.471285 141.220445 115.71152 95.709745 79.947345 -69.614575 58.067325 49.042705 41.78974 36.591055 33.50156 31.28578 29.220195 27.495375 -25.211765 23.141335 21.545715 20.10675 18.75338 17.35156 16.03372 14.51562 13.118645 -11.60539 10.11636 8.599875 7.130225 5.625045 4.232915 2.934455 0 0 -0 -174.408695 172.909975 179.447495 178.6513 163.139225 137.88547 112.179515 93.27917 78.199915 -67.78155 56.080875 47.405095 40.680235 35.912755 33.0752 30.867495 28.96341 27.091625 -24.859695 22.966915 21.46335 20.143895 18.813135 17.47753 16.15646 14.64805 13.24623 -11.67968 10.21972 8.680625 7.18029 5.67834 4.278135 2.969985 0 0 -0 -174.408695 173.139305 179.970755 177.85026 160.4341 135.02692 109.924975 91.110225 77.014505 -65.945295 54.485255 46.096945 39.66117 35.463785 32.82972 30.54611 28.787375 26.823535 -24.42526 22.831255 21.458505 20.237565 18.890655 17.64872 16.264665 14.79986 13.338285 -11.797575 10.29401 8.751685 7.23843 5.73325 4.307205 2.995825 0 0 -0 -174.408695 173.071475 180.19847 176.521115 158.25385 132.2362 107.44918 89.33857 75.993825 -64.449805 52.99784 44.927685 38.7923 35.16824 32.744125 30.6527 28.819675 26.41817 -24.320285 22.85548 21.52472 20.32316 19.0247 17.766615 16.35672 14.91291 13.409345 -11.894475 10.373145 8.825975 7.307875 5.76878 4.33466 3.01682 0 0 -0 -174.408695 172.867985 180.46333 175.94456 156.314235 129.686115 105.2657 87.72357 75.150795 -63.254705 51.894795 44.181555 38.31103 35.29098 32.602005 30.88526 28.729235 26.054795 -24.36066 22.966915 21.642615 20.437825 19.152285 17.882895 16.41163 15.00012 13.4691 -11.99622 10.42321 8.88573 7.34502 5.815615 4.36696 3.03297 0 0 -0 -174.408695 172.968115 180.125795 174.931955 154.44568 127.386355 103.58287 86.03751 74.07359 -62.411675 51.384455 43.42735 38.13338 35.23607 32.63592 30.9111 28.44015 26.038645 -24.47371 23.067045 21.771815 20.567025 19.29925 17.957185 16.54406 15.085715 13.56277 -12.09312 10.51042 8.96971 7.391855 5.860835 4.394415 3.05235 0 0 -0 -174.408695 173.28627 180.15971 173.8063 152.98249 125.438665 102.208505 85.024905 73.51157 -61.80282 51.17612 43.06236 38.105925 35.16501 32.92339 30.83035 28.319025 25.980505 -24.517315 23.154255 21.952695 20.641315 19.400995 18.008865 16.661955 15.13901 13.658055 -12.17064 10.60086 9.044 7.472605 5.902825 4.426715 3.07496 0 0 -0 -174.408695 173.00526 180.31475 173.14738 151.5516 123.797825 101.15068 84.252935 72.912405 -61.75114 50.996855 42.608545 37.8233 34.969595 32.74897 30.73668 28.134915 25.88845 -24.568995 23.21724 22.05121 20.757595 19.48659 18.08477 16.72817 15.23591 13.780795 -12.22232 10.670305 9.090835 7.537205 5.93674 4.47032 3.086265 0 0 -0 -174.408695 173.44131 179.99821 172.276895 149.95275 122.51713 100.154225 83.836265 72.550645 -61.232725 50.61087 42.136965 37.55844 34.940525 32.602005 30.79159 28.012175 25.941745 -24.656205 23.286685 22.16426 20.83027 19.578645 18.13322 16.823455 15.31343 13.884155 -12.285305 10.757515 9.161895 7.60665 5.9755 4.488085 3.09757 0 0 -0 -174.408695 173.22813 180.219465 171.378955 148.47341 121.071705 99.278895 83.151505 72.38107 -61.003395 50.43645 41.927015 37.398555 34.830705 32.640765 30.69469 27.97826 25.86261 -24.649745 23.341595 22.18364 20.951395 19.62548 18.26565 16.849295 15.399025 13.9213 -12.36444 10.812425 9.21519 7.634105 6.01103 4.51231 3.11049 0 0 -0 -174.408695 173.55113 180.193625 170.946135 147.2557 119.82331 98.89614 82.822045 71.756065 -60.55281 50.08115 41.755825 37.556825 34.884 32.91047 30.521885 27.805455 25.896525 -24.48986 23.396505 22.188485 21.035375 19.659395 18.28503 16.87998 15.44586 13.96329 -12.39351 10.81404 9.249105 7.653485 6.03364 4.51231 3.12018 0 0 -0 -174.408695 173.139305 180.642595 170.65382 146.259245 118.99643 98.4827 82.53619 71.355545 -60.18136 50.01655 41.97385 37.679565 35.10687 33.060665 30.431445 27.847445 25.91752 -24.43818 23.41427 22.236935 21.05637 19.71592 18.318945 16.942965 15.44909 13.964905 -12.427425 10.835035 9.255565 7.66479 6.038485 4.522 3.121795 0 0 -0 -174.408695 173.50591 180.703965 170.3179 145.79574 118.22123 98.6119 82.14536 70.777375 -59.55151 50.024625 42.02553 37.640805 34.969595 32.83941 30.499275 27.695635 25.873915 -24.50278 23.377125 22.356445 21.09836 19.762755 18.34963 16.942965 15.470085 14.003665 -12.443575 10.86249 9.27656 7.679325 6.06917 4.53815 3.12664 0 0 -0 -174.408695 173.89351 180.962365 169.941605 145.24018 117.70443 98.65389 81.82882 70.26865 -59.369015 50.176435 42.107895 37.73609 34.80325 32.844255 30.525115 27.50668 25.780245 -24.48017 23.40135 22.40651 21.103205 19.80959 18.357705 16.99626 15.50077 14.029505 -12.44842 10.886715 9.30563 7.70678 6.07886 4.54138 3.131485 0 0 -0 -174.408695 173.807915 180.787945 170.032045 145.329005 117.612375 98.72172 81.55104 70.476985 -59.446535 50.31371 41.962545 37.8233 34.859775 32.76512 30.418525 27.43239 25.70434 -24.40911 23.42396 22.333835 21.112895 19.76437 18.377085 17.01887 15.518535 14.00851 -12.462955 10.87541 9.310475 7.705165 6.06917 4.54784 3.131485 0 0 -0 +IESNA:LM-63-2002 +[TEST] 5890_0 +[TESTLAB] ERCO GmbH +[ISSUEDATE] 02-12-2008 +[MANUFAC] ERCO GmbH +[LUMCAT] 33519000 +[LUMINAIRE] Kubus Floor washlight +[LAMP] HIT-TC-CE 20W +TILT=NONE +1 1615 1 37 73 1 2 0.103 0.059 0 +1.0 1.0 24 +0 2.5 5 7.5 10 12.5 15 17.5 20 22.5 25 27.5 30 +32.5 35 37.5 40 42.5 45 47.5 50 52.5 55 57.5 60 62.5 +65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 +0 2.5 5 7.5 10 12.5 15 17.5 20 22.5 25 27.5 30 +32.5 35 37.5 40 42.5 45 47.5 50 52.5 55 57.5 60 62.5 +65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 92.5 95 +97.5 100 102.5 105 107.5 110 112.5 115 117.5 120 122.5 125 127.5 +130 132.5 135 137.5 140 142.5 145 147.5 150 152.5 155 157.5 160 +162.5 165 167.5 170 172.5 175 177.5 180 +174.408695 186.514735 207.74714 232.0109 259.393225 290.905105 329.41801 371.299805 413.25912 +451.245535 487.59757 510.86326 514.272525 504.50339 491.06659 481.24739 473.75702 470.79188 +471.491175 465.94688 448.826265 421.89291 385.692685 336.53693 275.843615 220.833485 172.667725 +138.452335 109.86845 87.28106 67.431095 50.761065 36.44086 23.40458 12.382205 3.77264 +0 +174.408695 186.24503 208.21226 231.893005 258.99432 290.456135 328.705795 370.82015 411.190305 +451.636365 486.478375 509.308015 514.198235 503.661975 490.829185 481.08266 472.12264 470.44627 +469.93593 463.828 448.5824 424.378395 389.798015 336.6629 278.547125 220.067975 173.953265 +138.071195 110.2722 87.81078 67.763785 50.964555 36.345575 23.34321 12.27077 3.75649 +0 +174.408695 186.0157 207.63086 230.823875 258.333785 290.4416 328.276205 369.71872 411.584365 +449.65153 484.175385 507.77215 512.935305 503.86708 488.84435 479.21249 469.905245 467.854195 +466.145525 462.72011 447.43575 420.92068 387.178485 339.510145 278.682785 222.855465 176.492045 +140.47916 111.16368 87.650895 68.65688 51.0017 36.442475 23.260845 12.34506 3.72096 +0 +174.408695 185.77345 207.59856 230.94177 258.35478 288.73293 327.291055 368.62052 409.81917 +449.25424 483.86692 504.188465 511.136195 501.5544 486.959645 476.44115 467.35193 462.392265 +461.99982 458.337 441.14371 417.88771 386.903935 340.17714 282.22448 226.653945 178.465575 +140.67942 111.313875 88.38249 68.78285 51.319855 36.348805 23.29153 12.31599 3.74357 +0 +174.408695 185.81867 206.934795 230.462115 258.078615 287.920585 325.67121 366.390205 407.695445 +447.705455 480.947 502.689745 508.27926 499.385455 486.3088 474.42886 464.59028 457.1419 +454.82276 449.010375 435.418535 415.26818 383.87258 338.59121 284.75034 231.444035 182.480465 +144.773445 114.2774 89.45808 68.965345 51.22134 36.58298 23.16556 12.336985 3.74034 +0 +174.408695 185.96079 206.6231 229.86618 257.293725 285.88084 323.179265 363.877265 405.780055 +444.85175 477.25511 499.88772 505.199455 498.019165 484.188305 472.73311 459.288235 450.66575 +446.08238 437.337155 424.565735 406.19834 378.51401 336.5983 285.727415 230.462115 184.02602 +145.044765 114.05776 89.47746 68.874905 51.26979 36.290665 23.170405 12.16418 3.741955 +0 +174.408695 185.080615 205.778455 229.060295 256.58797 284.425725 320.876275 361.193135 402.785845 +439.17018 474.37718 496.98718 502.315065 496.097315 482.618525 467.20335 455.396085 444.68379 +436.20181 425.79475 415.253645 395.80743 367.84209 328.870525 279.27226 228.09291 181.41941 +142.81122 111.34133 88.20161 67.660425 50.496205 35.347505 22.71013 11.8541 3.64667 +0 +174.408695 185.16944 205.778455 228.01862 254.89545 283.282305 317.630125 357.39304 399.119795 +435.883655 468.8991 491.830485 500.61447 494.71003 479.5581 463.735945 448.241635 437.19019 +426.04023 416.629625 404.18282 382.9488 355.27093 319.555205 273.821635 224.84676 178.866095 +142.686865 109.811925 86.98713 66.442715 49.49329 34.44472 22.05767 11.51172 3.596605 +0 +174.408695 184.83352 204.96934 227.102915 252.97683 281.0746 314.581005 353.40722 394.09553 +431.413335 463.55668 487.50713 496.41224 491.533325 477.0064 460.560855 444.61273 429.75796 +418.42389 407.062365 392.67433 372.40285 342.278255 310.327095 271.008305 225.303805 181.03181 +143.474985 111.09908 86.539775 65.80479 49.13476 34.26384 21.964 11.471345 3.561075 +0 +174.408695 184.61388 204.66895 225.662335 251.44581 279.35301 311.732145 348.838385 389.20854 +425.237575 458.577635 478.813585 491.26685 487.78168 473.40172 459.32861 439.76773 423.69848 +410.85277 395.89464 383.12968 362.301025 335.831175 306.092565 270.66108 227.96371 185.74438 +147.867785 115.01707 90.105695 67.922055 50.4203 35.024505 22.307995 11.5957 3.632135 +0 +174.408695 184.3684 203.498075 224.45916 249.356 276.85622 307.88683 344.129045 382.67102 +419.811175 451.79302 472.605525 485.17184 485.580435 472.779945 456.84797 436.20504 418.64999 +402.03487 388.609375 373.25557 354.79612 330.240045 302.678455 272.830025 233.504775 195.090385 +156.071985 121.898585 94.663225 71.05677 52.58117 36.04034 22.97499 11.89609 3.72419 +0 +174.408695 183.685255 202.709955 222.878075 247.94126 274.57907 304.72466 339.216215 376.800495 +412.563055 445.169905 466.672015 479.269015 481.484795 470.04575 454.24782 433.264125 412.986185 +393.296105 376.638995 363.499355 346.72112 324.616615 300.136445 272.33422 237.707005 202.08495 +163.588195 129.40995 100.606425 75.519015 54.960065 37.72317 23.732425 12.249775 3.79525 +0 +174.408695 183.698175 202.34981 221.40681 245.207065 271.96923 300.452985 332.875725 368.97582 +404.827205 436.15659 459.50949 472.34551 475.85652 467.62002 451.28914 430.344205 407.524255 +386.422665 368.55915 352.41238 336.667745 315.92307 294.106035 269.95048 238.42245 206.09338 +169.021055 135.04307 105.23986 79.136615 58.03664 39.594955 24.95175 12.727815 3.94706 +0 +174.408695 183.362255 201.279065 219.593165 242.287145 267.93496 295.45133 326.92445 361.6631 +395.453745 426.23726 450.832095 464.575745 468.219185 463.316045 447.524575 426.09191 402.365945 +381.08832 361.018715 341.90519 325.356285 307.681725 286.76263 266.65265 238.2125 207.50812 +172.196145 141.26728 110.629115 84.08013 61.09545 42.08367 26.23729 13.39481 4.119865 +0 +174.408695 182.945585 200.197015 218.37707 239.758055 264.564455 290.078225 319.84106 353.27156 +386.06898 417.316 440.39112 455.34279 459.488495 455.677095 442.967045 422.369335 397.143035 +372.546585 352.20243 332.633475 315.611375 297.135775 279.874655 259.251105 234.095865 206.73615 +174.828595 144.48113 114.372685 88.81208 64.07997 44.720965 27.60681 14.17001 4.349195 +0 +174.408695 182.40133 199.173105 216.615105 237.471215 259.97624 284.44995 313.453735 345.023755 +376.21748 405.19381 429.769265 445.040705 449.74197 448.03653 438.248015 419.145795 394.97409 +368.08757 345.46465 323.909245 306.0425 286.615665 269.506355 251.515255 229.3946 204.609195 +175.930025 147.56255 118.830085 93.23718 67.569985 48.015565 29.31548 15.28113 4.665735 +0 +174.408695 182.451395 198.15081 214.48169 234.806465 255.9452 279.306175 306.43979 336.45941 +365.60047 394.78998 418.26885 432.44209 438.041295 439.065205 430.508935 413.95034 390.747635 +363.189275 340.133535 317.4444 296.87253 277.213135 258.398385 241.721895 219.583475 196.73607 +172.06856 147.98568 120.060715 95.751735 70.318715 50.02301 30.76898 16.00465 4.92898 +0 +174.408695 182.04603 197.529035 213.217145 232.16594 252.60861 274.01382 299.32733 327.76425 +354.45051 382.578965 404.51228 419.220085 425.17782 426.267945 419.220085 406.04976 384.66716 +358.06488 332.93871 309.041555 288.10631 267.28573 249.26233 230.17626 210.71551 190.364895 +168.20871 144.069305 119.61013 96.11188 71.200505 51.47974 31.6217 16.671645 5.033955 +0 +174.408695 181.676195 196.29679 211.09342 228.63555 248.30625 269.251185 291.64316 317.42825 +343.00016 369.3505 390.43271 404.33786 411.280745 412.42901 407.16734 397.54517 377.446495 +351.0364 325.69705 300.985935 277.3924 255.271745 236.02902 217.516275 197.474125 178.68037 +159.589455 138.03082 116.139495 93.6054 70.66917 51.45713 31.87364 16.852525 5.10986 +0 +174.408695 181.203 194.80453 209.263625 225.701095 244.48516 263.230465 284.5953 308.511835 +332.18612 356.661445 376.949075 388.55931 395.143665 397.201175 392.65172 384.181045 365.81688 +343.747905 317.0568 290.56111 265.281515 241.55878 221.750805 201.70381 181.824775 166.181885 +148.051895 130.196455 109.337115 89.80692 68.093245 49.762995 30.883645 16.37287 4.90314 +0 +174.408695 180.62483 193.659495 207.1076 223.297975 239.993845 257.35671 277.717015 299.85059 +321.383385 343.19719 361.583965 373.266875 379.119635 380.686185 377.17679 368.61083 353.34908 +333.124435 307.584825 281.76259 254.60152 229.619085 208.191265 186.960475 167.83403 150.002815 +133.833435 116.81941 99.475925 82.00324 62.52634 45.954825 28.67594 15.095405 4.54138 +0 +174.408695 180.374505 192.331965 205.481295 219.609315 235.145615 251.65576 271.001845 290.536885 +308.931735 328.68803 345.282155 357.617525 362.75807 362.74838 358.896605 350.485685 338.001735 +321.325245 297.13739 273.072275 245.37664 218.78728 195.72508 173.753005 154.392385 136.343145 +120.936045 104.64231 88.135395 71.281255 56.19877 40.44929 25.74633 13.377045 4.058495 +0 +174.408695 179.880315 191.446945 203.469005 216.81698 231.182405 246.211595 263.30637 280.77744 +297.23752 315.824555 329.794305 340.80699 345.07382 344.1565 340.139995 332.29917 320.785835 +303.54571 282.390825 260.29278 235.429855 208.775895 184.379705 161.83269 142.74985 124.689305 +109.673035 93.229105 78.34365 62.783125 49.036245 35.114945 22.36452 11.571475 3.490015 +0 +174.408695 179.17456 190.038665 201.33236 213.33181 226.71047 240.778735 255.6222 271.670455 +285.772635 302.394215 315.40627 325.062355 328.29397 326.34305 322.249025 313.67822 302.148735 +286.48808 267.7347 245.39602 222.196545 197.21088 173.517215 152.102315 131.985875 113.29548 +97.783405 82.02585 68.332265 54.188095 42.4422 29.99055 19.28956 9.824045 2.989365 +0 +174.408695 178.988835 188.701445 198.966385 210.274615 222.206235 234.903365 248.827895 262.73466 +274.713115 289.40154 300.36416 308.708865 311.65624 308.628115 304.15618 295.365735 283.45188 +267.303495 250.55433 230.18272 207.64055 184.24243 161.705105 141.608045 121.21867 103.57964 +88.16931 72.57164 60.15875 47.1903 36.6605 25.64297 16.55052 8.299485 2.57108 +0 +174.408695 178.69652 187.04284 196.76837 207.15928 217.98301 229.467275 241.17764 253.42257 +263.81348 275.950205 285.804935 291.993615 295.246225 291.547875 285.05719 276.234445 263.970135 +248.47421 231.321295 211.831475 192.180155 170.95421 150.42756 130.877985 111.446305 94.93293 +79.66795 65.967905 54.149335 42.66507 32.902395 22.72951 14.704575 7.33533 0 +0 +174.408695 178.239475 186.19658 194.864285 204.22321 213.691955 223.51923 233.569375 244.72418 +253.19324 263.492095 271.65269 277.710555 277.952805 274.50478 266.75601 256.42324 244.98904 +228.485355 213.30274 193.830685 176.185195 156.010615 137.987215 119.747405 102.363545 86.54785 +72.43275 60.16521 49.04432 38.231895 29.352625 20.40714 13.041125 6.49876 0 +0 +174.408695 177.793735 184.86259 192.27221 200.81556 208.96162 217.803745 225.99018 235.68664 +243.634055 251.5847 258.98463 262.4052 263.482405 259.29148 250.31531 239.044225 227.254725 +210.89639 196.811975 177.61447 159.721885 141.88744 125.748745 109.088405 93.76367 79.11885 +66.058345 54.20909 44.43834 34.35751 26.322885 18.131605 11.510105 5.75909 0 +0 +174.408695 177.590245 183.833835 190.342285 197.826195 204.557515 212.002665 218.966545 228.04123 +234.221835 240.33784 246.4167 249.464205 248.98778 244.144395 236.51675 223.514385 211.553695 +195.529665 180.51178 162.489995 146.727595 129.301745 114.02223 98.658735 85.464185 71.749605 +60.57542 49.116995 40.008395 30.8465 23.545085 16.2469 10.22618 5.11309 0 +0 +174.408695 177.154195 182.46916 188.09905 194.67533 200.503865 206.673165 213.485235 219.71752 +225.174605 229.85972 234.325195 236.8236 236.45215 231.15818 224.464005 210.445805 197.648545 +182.80831 167.0556 149.79448 134.86542 118.41826 104.31608 90.525595 78.009345 66.00505 +55.564075 45.39765 36.684725 28.412695 21.406825 14.77725 9.226495 4.626975 0 +0 +174.408695 176.256255 181.094795 186.272485 191.61006 196.479285 201.770025 207.416065 212.11733 +216.90096 220.187485 223.614515 225.177835 224.51084 220.113195 212.525925 200.513555 186.007625 +172.38833 157.23317 140.03019 126.4222 110.454695 97.357045 84.34499 72.57164 61.1116 +51.357 42.08367 34.126565 26.351955 19.610945 13.566 8.45937 4.23776 0 +0 +174.408695 175.73138 179.76242 184.66233 188.632 192.645275 196.97832 201.495475 204.793305 +209.286235 211.834705 213.62574 214.19422 213.265595 210.22778 203.685415 192.11071 177.603165 +163.5026 151.472465 134.812125 121.074935 105.02345 92.340855 79.721245 69.084855 57.55537 +48.4177 39.81298 32.178875 24.714345 18.496595 12.67775 7.94903 3.96321 0 +0 +174.408695 175.613485 178.6836 182.267285 185.820285 189.315145 192.125245 195.97702 199.305535 +202.393415 204.48807 205.019405 205.41508 205.3634 202.38857 196.944405 185.96402 173.59958 +160.138555 148.16979 132.954875 119.311355 103.600635 90.74685 77.9399 66.67689 55.974285 +46.920595 37.860445 30.55903 23.590305 17.731085 12.2417 7.621185 3.806555 0 +0 +174.408695 175.10799 177.76305 180.836395 183.66103 186.15136 188.46081 191.488935 194.10685 +197.357845 198.94539 199.118195 199.760965 199.518715 196.76514 191.34843 182.335115 171.995885 +160.36627 147.55286 134.4326 120.79231 105.412665 92.05177 79.175375 67.402025 56.626745 +46.99327 38.238355 30.328085 23.34321 17.3451 11.91224 7.437075 3.70804 0 +0 +174.408695 174.828595 176.863495 179.45557 181.78763 183.617425 185.55704 188.145885 190.55708 +193.097475 195.229275 195.63141 196.17405 195.445685 192.54353 189.00022 180.035355 172.041105 +161.233525 149.59099 136.47719 124.091755 109.01896 96.05374 82.159895 70.493135 59.00564 +48.887665 38.78261 31.019305 23.80187 17.391935 12.139955 7.395085 3.74357 0 +0 +174.408695 174.440995 175.96394 178.01822 179.80118 181.82639 183.8839 185.306715 187.88587 +190.261535 193.25413 193.7354 195.287415 193.26059 191.921755 188.149115 180.84447 173.081165 +162.91474 152.118465 140.422635 127.951605 114.760285 101.18621 88.103095 76.126255 62.618395 +51.65093 40.43314 32.382365 23.92784 17.72624 11.936465 7.340175 3.68543 0 +0 +174.408695 174.024325 175.10476 176.50981 178.92585 180.594145 182.984345 184.35871 187.919785 +189.289305 192.222145 193.940505 195.41177 192.7018 192.68888 188.04091 183.685255 172.364105 +164.502285 153.76738 145.487275 132.864435 119.589135 106.14426 95.835715 81.85143 68.9282 +55.98559 43.85694 34.32844 25.424945 18.163905 12.030135 7.330485 3.606295 0 +0 +174.408695 173.61896 174.54274 176.065685 178.006915 179.854475 183.016645 185.05962 188.413975 +189.49764 192.212455 194.786765 194.56874 191.93629 189.91431 183.37356 177.880945 166.09629 +158.64468 150.2596 143.726925 132.08439 121.730625 108.846155 99.198145 83.61501 71.50251 +56.894835 47.311425 35.25868 26.67657 18.37547 12.275615 7.330485 3.61114 0 +0 +174.408695 173.756235 174.45553 175.49882 177.14612 180.154865 183.8839 185.736305 190.18563 +191.53577 191.908835 191.818395 188.56094 185.60872 178.313765 171.340195 161.23837 153.355555 +145.84096 137.832175 129.3292 118.24707 110.286735 99.595435 90.012025 75.26546 66.239225 +53.54371 45.03589 34.38658 26.439165 18.10738 12.74558 7.40962 3.75649 0 +0 +174.408695 173.585045 173.95811 174.99171 177.44005 181.33543 185.490825 187.7922 191.614905 +192.083255 188.82903 186.149745 178.67391 172.028185 160.942825 150.82485 141.67103 133.22135 +124.178965 116.11527 106.610995 96.239465 88.77009 81.032625 71.487975 62.754055 53.984605 +45.586605 38.183445 29.691775 23.7405 17.04794 12.198095 7.2352 3.70804 0 +0 +174.408695 173.265275 173.491375 174.689705 177.935855 182.661345 187.66623 189.744735 192.267365 +190.04674 180.955905 174.47168 164.38762 153.798065 141.520835 131.0411 121.204135 113.80259 +102.489515 94.524335 86.759415 76.943445 68.85391 63.267625 54.02175 49.41577 41.85111 +36.145315 29.59326 24.6126 19.604485 14.759485 10.36184 6.42124 3.254225 0 +0 +174.408695 173.236205 173.774 175.20812 178.942 184.40393 189.163335 190.901075 189.825485 +182.373875 171.369265 160.85723 148.17302 135.180345 123.28264 112.244115 103.4892 95.092815 +85.105655 77.379495 68.810305 63.206255 55.62383 50.347625 43.370825 38.852055 32.621385 +28.745385 23.52086 19.62548 15.68165 12.06728 8.328555 5.35211 2.73258 0 +0 +174.408695 173.239435 173.891895 175.91226 179.90454 185.797675 190.46987 190.14041 185.81544 +173.213595 160.513235 146.33515 132.166755 120.306195 107.38458 97.973975 89.366025 80.89858 +72.179195 66.06642 57.84607 52.87833 46.54107 42.19026 36.4021 32.27416 27.53575 +23.8374 19.94202 16.781465 13.02336 10.105055 7.106 4.59629 0 0 +0 +174.408695 173.20552 173.711015 176.71653 181.104485 187.09452 190.474715 187.0816 178.90647 +164.02909 146.5774 131.882515 118.54423 107.00021 95.40774 86.281375 77.637895 70.993785 +62.77828 57.305045 50.88542 45.589835 40.631785 36.048415 31.555485 27.818375 23.79218 +20.70107 17.61965 14.63836 11.52787 8.880885 6.29204 4.06011 0 0 +0 +174.408695 173.065015 173.988795 177.33023 182.373875 187.4369 188.785425 182.138085 170.57953 +152.97926 133.791445 118.523235 106.14749 95.951995 85.415735 76.97736 69.25443 62.770205 +55.66259 50.52689 44.59661 39.808135 35.03258 31.59263 27.611655 24.178165 20.91748 +18.173595 15.224605 12.766575 10.14543 7.844055 5.5879 3.635365 0 0 +0 +174.408695 172.98911 174.14545 178.15388 182.761475 187.45305 186.065765 176.462975 160.29521 +141.369025 122.00033 107.53639 95.81795 86.152175 77.279365 70.12976 62.579635 56.000125 +49.375395 44.746805 39.41246 35.19731 31.025765 27.695635 24.036045 21.284085 18.52082 +16.057945 13.57246 11.274315 8.977785 6.984875 5.001655 3.33336 0 0 +0 +174.408695 172.806615 174.45876 178.895165 183.724015 187.5984 182.0751 169.398965 150.803855 +130.74071 112.386235 98.88322 87.615365 78.43409 70.72731 64.033135 56.70588 50.386385 +44.19609 39.87435 35.063265 31.368145 27.682715 24.426875 21.60224 19.40907 16.75401 +14.69973 12.391895 10.50719 8.43676 6.535905 4.743255 3.17509 0 0 +0 +174.408695 172.53045 174.86897 179.73335 184.53636 185.84774 177.2947 161.223835 140.929745 +120.923125 103.36 91.22166 80.073315 72.41014 64.588695 58.03018 50.86927 45.45902 +39.9228 35.454095 31.28901 28.050935 24.86131 22.349985 20.00339 18.15906 15.883525 +13.86962 11.888015 9.911255 8.076615 6.232285 4.55753 3.050735 0 0 +0 +174.408695 172.59505 175.206505 180.846085 184.73985 182.926205 171.92967 152.75962 130.564675 +111.436615 95.987525 83.76682 74.78419 67.043495 58.480765 52.46489 45.98874 41.200265 +35.935365 32.256395 28.645255 25.93367 23.283455 21.15973 19.152285 17.159375 15.273055 +13.322135 11.371215 9.52204 7.74554 6.00457 4.429945 2.96837 0 0 +0 +174.408695 172.65319 175.79921 181.81347 184.10354 179.8787 166.0866 144.311555 121.771 +103.31155 89.721325 78.138545 70.134605 61.927175 54.165485 48.031715 41.86403 37.450235 +33.034825 29.79998 26.809 24.56092 22.301535 20.47497 18.580575 16.62158 14.68358 +12.834405 10.964235 9.239415 7.521055 5.8463 4.300745 2.91669 0 0 +0 +174.408695 172.54983 176.288555 182.402945 183.501145 176.62932 159.27776 135.890945 113.87365 +96.914535 83.396985 73.513185 65.93399 57.41325 49.785605 43.77942 38.499985 34.433415 +30.7819 28.15914 25.65266 23.68236 21.48596 19.64809 17.85544 15.990115 14.139325 +12.404815 10.615395 8.90834 7.24166 5.676725 4.207075 2.866625 0 0 +0 +174.408695 172.6112 176.75206 182.687185 182.399715 172.335035 152.725705 128.623445 106.87747 +91.30887 77.97543 69.29642 61.817355 53.01722 45.56561 40.444445 35.609135 32.146575 +29.349395 27.07709 24.817705 22.78119 20.77213 19.09576 17.25466 15.450705 13.606375 +11.907395 10.25848 8.680625 7.12861 5.586285 4.168315 2.84563 0 0 +0 +174.408695 172.65319 177.25271 182.793775 180.37289 168.55432 146.091285 121.685405 100.83414 +86.28945 74.537095 66.0212 57.721715 49.23812 42.501955 37.76193 33.60492 30.625245 +28.325485 26.255055 24.032815 22.38713 20.347385 18.635485 16.61835 14.885455 13.263995 +11.68291 10.13897 8.580495 7.085005 5.54591 4.148935 2.83917 0 0 +0 +174.408695 172.829225 177.63385 182.6565 177.72106 163.88374 140.047955 115.761585 95.45942 +81.27972 71.46698 62.38745 53.858635 46.13732 39.93572 35.57845 32.14173 29.65463 +27.70694 25.53638 23.709815 21.804115 19.872575 18.01694 16.24367 14.659355 13.131565 +11.59893 10.0776 8.538505 7.04786 5.531375 4.148935 2.85855 0 0 +0 +174.408695 172.39802 178.06021 182.82123 175.63448 159.2067 134.075685 110.622655 91.1506 +77.954435 68.312885 58.95719 50.370235 43.374055 37.750625 33.890775 31.080675 29.097455 +26.881675 24.97436 23.196245 21.27924 19.355775 17.68102 16.07248 14.55438 13.054045 +11.542405 10.050145 8.50459 7.03171 5.51361 4.14732 2.86178 0 0 +0 +174.408695 172.6758 178.2637 181.937825 173.00526 154.634635 128.86408 106.006985 87.4684 +75.36236 65.722425 56.02112 47.76201 41.135665 36.129165 32.883015 30.466975 28.41431 +26.435935 24.55446 22.48726 20.64939 19.05054 17.49368 15.99173 14.44456 13.02013 +11.51495 10.02915 8.490055 7.03171 5.51361 4.148935 2.86178 0 0 +0 +174.408695 172.515915 178.862865 181.262755 170.42772 149.673355 123.88019 101.937185 84.398285 +73.20472 63.22402 53.49849 45.563995 39.003865 34.850085 32.140115 29.83228 28.13007 +25.880375 24.04412 21.968845 20.298935 18.78245 17.330565 15.93036 14.43487 13.0492 +11.53433 10.04207 8.49813 7.039785 5.534605 4.15701 2.882775 0 0 +0 +174.408695 172.59182 179.02598 180.31475 167.93739 145.270865 119.443785 98.58606 82.00001 +71.45083 60.47529 51.22457 43.32076 37.60043 33.96345 31.77674 29.41238 27.715015 +25.70111 23.40781 21.67976 20.166505 18.69847 17.31603 15.977195 14.468785 13.070195 +11.55694 10.069525 8.53689 7.065625 5.56529 4.19577 2.908615 0 0 +0 +174.408695 172.767855 179.47818 179.489485 165.471285 141.220445 115.71152 95.709745 79.947345 +69.614575 58.067325 49.042705 41.78974 36.591055 33.50156 31.28578 29.220195 27.495375 +25.211765 23.141335 21.545715 20.10675 18.75338 17.35156 16.03372 14.51562 13.118645 +11.60539 10.11636 8.599875 7.130225 5.625045 4.232915 2.934455 0 0 +0 +174.408695 172.909975 179.447495 178.6513 163.139225 137.88547 112.179515 93.27917 78.199915 +67.78155 56.080875 47.405095 40.680235 35.912755 33.0752 30.867495 28.96341 27.091625 +24.859695 22.966915 21.46335 20.143895 18.813135 17.47753 16.15646 14.64805 13.24623 +11.67968 10.21972 8.680625 7.18029 5.67834 4.278135 2.969985 0 0 +0 +174.408695 173.139305 179.970755 177.85026 160.4341 135.02692 109.924975 91.110225 77.014505 +65.945295 54.485255 46.096945 39.66117 35.463785 32.82972 30.54611 28.787375 26.823535 +24.42526 22.831255 21.458505 20.237565 18.890655 17.64872 16.264665 14.79986 13.338285 +11.797575 10.29401 8.751685 7.23843 5.73325 4.307205 2.995825 0 0 +0 +174.408695 173.071475 180.19847 176.521115 158.25385 132.2362 107.44918 89.33857 75.993825 +64.449805 52.99784 44.927685 38.7923 35.16824 32.744125 30.6527 28.819675 26.41817 +24.320285 22.85548 21.52472 20.32316 19.0247 17.766615 16.35672 14.91291 13.409345 +11.894475 10.373145 8.825975 7.307875 5.76878 4.33466 3.01682 0 0 +0 +174.408695 172.867985 180.46333 175.94456 156.314235 129.686115 105.2657 87.72357 75.150795 +63.254705 51.894795 44.181555 38.31103 35.29098 32.602005 30.88526 28.729235 26.054795 +24.36066 22.966915 21.642615 20.437825 19.152285 17.882895 16.41163 15.00012 13.4691 +11.99622 10.42321 8.88573 7.34502 5.815615 4.36696 3.03297 0 0 +0 +174.408695 172.968115 180.125795 174.931955 154.44568 127.386355 103.58287 86.03751 74.07359 +62.411675 51.384455 43.42735 38.13338 35.23607 32.63592 30.9111 28.44015 26.038645 +24.47371 23.067045 21.771815 20.567025 19.29925 17.957185 16.54406 15.085715 13.56277 +12.09312 10.51042 8.96971 7.391855 5.860835 4.394415 3.05235 0 0 +0 +174.408695 173.28627 180.15971 173.8063 152.98249 125.438665 102.208505 85.024905 73.51157 +61.80282 51.17612 43.06236 38.105925 35.16501 32.92339 30.83035 28.319025 25.980505 +24.517315 23.154255 21.952695 20.641315 19.400995 18.008865 16.661955 15.13901 13.658055 +12.17064 10.60086 9.044 7.472605 5.902825 4.426715 3.07496 0 0 +0 +174.408695 173.00526 180.31475 173.14738 151.5516 123.797825 101.15068 84.252935 72.912405 +61.75114 50.996855 42.608545 37.8233 34.969595 32.74897 30.73668 28.134915 25.88845 +24.568995 23.21724 22.05121 20.757595 19.48659 18.08477 16.72817 15.23591 13.780795 +12.22232 10.670305 9.090835 7.537205 5.93674 4.47032 3.086265 0 0 +0 +174.408695 173.44131 179.99821 172.276895 149.95275 122.51713 100.154225 83.836265 72.550645 +61.232725 50.61087 42.136965 37.55844 34.940525 32.602005 30.79159 28.012175 25.941745 +24.656205 23.286685 22.16426 20.83027 19.578645 18.13322 16.823455 15.31343 13.884155 +12.285305 10.757515 9.161895 7.60665 5.9755 4.488085 3.09757 0 0 +0 +174.408695 173.22813 180.219465 171.378955 148.47341 121.071705 99.278895 83.151505 72.38107 +61.003395 50.43645 41.927015 37.398555 34.830705 32.640765 30.69469 27.97826 25.86261 +24.649745 23.341595 22.18364 20.951395 19.62548 18.26565 16.849295 15.399025 13.9213 +12.36444 10.812425 9.21519 7.634105 6.01103 4.51231 3.11049 0 0 +0 +174.408695 173.55113 180.193625 170.946135 147.2557 119.82331 98.89614 82.822045 71.756065 +60.55281 50.08115 41.755825 37.556825 34.884 32.91047 30.521885 27.805455 25.896525 +24.48986 23.396505 22.188485 21.035375 19.659395 18.28503 16.87998 15.44586 13.96329 +12.39351 10.81404 9.249105 7.653485 6.03364 4.51231 3.12018 0 0 +0 +174.408695 173.139305 180.642595 170.65382 146.259245 118.99643 98.4827 82.53619 71.355545 +60.18136 50.01655 41.97385 37.679565 35.10687 33.060665 30.431445 27.847445 25.91752 +24.43818 23.41427 22.236935 21.05637 19.71592 18.318945 16.942965 15.44909 13.964905 +12.427425 10.835035 9.255565 7.66479 6.038485 4.522 3.121795 0 0 +0 +174.408695 173.50591 180.703965 170.3179 145.79574 118.22123 98.6119 82.14536 70.777375 +59.55151 50.024625 42.02553 37.640805 34.969595 32.83941 30.499275 27.695635 25.873915 +24.50278 23.377125 22.356445 21.09836 19.762755 18.34963 16.942965 15.470085 14.003665 +12.443575 10.86249 9.27656 7.679325 6.06917 4.53815 3.12664 0 0 +0 +174.408695 173.89351 180.962365 169.941605 145.24018 117.70443 98.65389 81.82882 70.26865 +59.369015 50.176435 42.107895 37.73609 34.80325 32.844255 30.525115 27.50668 25.780245 +24.48017 23.40135 22.40651 21.103205 19.80959 18.357705 16.99626 15.50077 14.029505 +12.44842 10.886715 9.30563 7.70678 6.07886 4.54138 3.131485 0 0 +0 +174.408695 173.807915 180.787945 170.032045 145.329005 117.612375 98.72172 81.55104 70.476985 +59.446535 50.31371 41.962545 37.8233 34.859775 32.76512 30.418525 27.43239 25.70434 +24.40911 23.42396 22.333835 21.112895 19.76437 18.377085 17.01887 15.518535 14.00851 +12.462955 10.87541 9.310475 7.705165 6.06917 4.54784 3.131485 0 0 +0 diff --git a/RenderPipelineFile/data/ies_profiles/overhead.ies b/RenderPipelineFile/data/ies_profiles/overhead.ies index 80f4fa51..99ceacc7 100644 --- a/RenderPipelineFile/data/ies_profiles/overhead.ies +++ b/RenderPipelineFile/data/ies_profiles/overhead.ies @@ -1,24 +1,24 @@ -IESNA:LM-63-1995 -[TEST]BALLABS TEST NO. 14061.0 -[MANUFAC] H.E. WILLIAMS, INC - CARTHAGE, MISSOURI -[LUMINAIRE] 8/54W T5HO LAMP 25x48"CABLE MOUNT LUMINAIRE -[MORE] MIRO4 SPECULAR REFLECTOR w/OPEN BOTTOM -[MORE] 2 UNIVERSAL BALLAST #B454PUNV-E WATTS=432 -[LUMCAT] HL-4-854T5H-MD-EB4/4-BD-UNV -[LAMPCAT] FP54T5HO/835 -TILT=NONE - 8 5000. 1.000000 19 5 1 1 2.083 4.000 .000 - 1.0000 1.0000 432.0000 - .0 5.0 10.0 15.0 20.0 25.0 30.0 35.0 40.0 45.0 - 50.0 55.0 60.0 65.0 70.0 75.0 80.0 85.0 90.0 - .0 22.5 45.0 67.5 90.0 - 19229. 19192. 19185. 18789. 18134. 17000. 15662. 14433. 13163. 11892. - 10500. 8916. 7319. 5817. 4110. 2580. 1269. 327. 0. - 19229. 19239. 19253. 18898. 18188. 16905. 15184. 13505. 11443. 9375. - 7606. 5721. 4260. 3031. 2007. 1447. 1037. 396. 0. - 19229. 19140. 19267. 18557. 17041. 14788. 12139. 10050. 8097. 6603. - 5311. 4123. 3523. 3004. 2621. 1925. 1106. 368. 0. - 19229. 19168. 19090. 17820. 15280. 12808. 10091. 8466. 6854. 5739. - 4833. 4192. 3919. 3577. 2826. 1898. 1078. 300. 0. - 19229. 19182. 18980. 17451. 14652. 12139. 9613. 8042. 6513. 5624. - 4861. 4328. 4096. 3686. 2812. 1884. 1065. 259. 0. +IESNA:LM-63-1995 +[TEST]BALLABS TEST NO. 14061.0 +[MANUFAC] H.E. WILLIAMS, INC - CARTHAGE, MISSOURI +[LUMINAIRE] 8/54W T5HO LAMP 25x48"CABLE MOUNT LUMINAIRE +[MORE] MIRO4 SPECULAR REFLECTOR w/OPEN BOTTOM +[MORE] 2 UNIVERSAL BALLAST #B454PUNV-E WATTS=432 +[LUMCAT] HL-4-854T5H-MD-EB4/4-BD-UNV +[LAMPCAT] FP54T5HO/835 +TILT=NONE + 8 5000. 1.000000 19 5 1 1 2.083 4.000 .000 + 1.0000 1.0000 432.0000 + .0 5.0 10.0 15.0 20.0 25.0 30.0 35.0 40.0 45.0 + 50.0 55.0 60.0 65.0 70.0 75.0 80.0 85.0 90.0 + .0 22.5 45.0 67.5 90.0 + 19229. 19192. 19185. 18789. 18134. 17000. 15662. 14433. 13163. 11892. + 10500. 8916. 7319. 5817. 4110. 2580. 1269. 327. 0. + 19229. 19239. 19253. 18898. 18188. 16905. 15184. 13505. 11443. 9375. + 7606. 5721. 4260. 3031. 2007. 1447. 1037. 396. 0. + 19229. 19140. 19267. 18557. 17041. 14788. 12139. 10050. 8097. 6603. + 5311. 4123. 3523. 3004. 2621. 1925. 1106. 368. 0. + 19229. 19168. 19090. 17820. 15280. 12808. 10091. 8466. 6854. 5739. + 4833. 4192. 3919. 3577. 2826. 1898. 1078. 300. 0. + 19229. 19182. 18980. 17451. 14652. 12139. 9613. 8042. 6513. 5624. + 4861. 4328. 4096. 3686. 2812. 1884. 1065. 259. 0. diff --git a/RenderPipelineFile/data/ies_profiles/parallel_beam.ies b/RenderPipelineFile/data/ies_profiles/parallel_beam.ies index 5a521c06..d737b917 100644 --- a/RenderPipelineFile/data/ies_profiles/parallel_beam.ies +++ b/RenderPipelineFile/data/ies_profiles/parallel_beam.ies @@ -1,35 +1,35 @@ -IESNA:LM-63-2002 -[TEST] 3907_1 -[TESTLAB] ERCO GmbH -[ISSUEDATE] 02-12-2008 -[MANUFAC] ERCO GmbH -[LUMCAT] 81051000 -[LUMINAIRE] Lightcast Directional luminaire -[LAMP] HIT-CE 70W -TILT=NONE -1 6600 1 19 7 1 2 -0.127 -0.127 0 -1.0 1.0 79 -0 5 10 15 20 25 30 35 40 45 50 55 60 -65 70 75 80 85 90 -0 15 30 45 60 75 90 -11501.82 8886.24 4579.74 2596.836 1430.154 810.546 487.8588 266.8446 163.6338 -108.933 71.247 41.349 20.0178 10.2234 0 0 0 0 -0 -11501.82 8992.5 4689.564 2685.144 1510.806 853.512 524.139 292.0962 171.7056 -113.3682 72.9366 43.0452 20.4006 10.0782 0 0 0 0 -0 -11501.82 9288.84 5057.58 2943.864 1719.432 987.162 595.7424 347.8464 193.71 -120.1464 74.7186 45.5994 22.1232 9.8736 0 0 0 0 -0 -11501.82 9724.44 5794.14 3453.186 2080.782 1230.108 722.238 427.614 235.719 -138.6726 83.4702 49.1502 24.0768 9.207 0 0 0 0 -0 -11501.82 10198.32 7048.8 4538.688 2758.668 1656.402 938.916 532.158 289.707 -157.3836 89.9844 53.1366 24.783 8.1642 0 0 0 0 -0 -11501.82 10580.46 8413.02 6409.458 4582.182 2761.044 1490.742 710.754 335.8674 -173.3754 96.4194 55.9944 26.4858 8.4678 0 0 0 0 -0 -11501.82 10746.12 9057.18 7549.74 6203.736 3923.898 1914.198 784.278 360.2148 -177.9558 99.2772 57.8556 27.5748 8.976 0 0 0 0 -0 +IESNA:LM-63-2002 +[TEST] 3907_1 +[TESTLAB] ERCO GmbH +[ISSUEDATE] 02-12-2008 +[MANUFAC] ERCO GmbH +[LUMCAT] 81051000 +[LUMINAIRE] Lightcast Directional luminaire +[LAMP] HIT-CE 70W +TILT=NONE +1 6600 1 19 7 1 2 -0.127 -0.127 0 +1.0 1.0 79 +0 5 10 15 20 25 30 35 40 45 50 55 60 +65 70 75 80 85 90 +0 15 30 45 60 75 90 +11501.82 8886.24 4579.74 2596.836 1430.154 810.546 487.8588 266.8446 163.6338 +108.933 71.247 41.349 20.0178 10.2234 0 0 0 0 +0 +11501.82 8992.5 4689.564 2685.144 1510.806 853.512 524.139 292.0962 171.7056 +113.3682 72.9366 43.0452 20.4006 10.0782 0 0 0 0 +0 +11501.82 9288.84 5057.58 2943.864 1719.432 987.162 595.7424 347.8464 193.71 +120.1464 74.7186 45.5994 22.1232 9.8736 0 0 0 0 +0 +11501.82 9724.44 5794.14 3453.186 2080.782 1230.108 722.238 427.614 235.719 +138.6726 83.4702 49.1502 24.0768 9.207 0 0 0 0 +0 +11501.82 10198.32 7048.8 4538.688 2758.668 1656.402 938.916 532.158 289.707 +157.3836 89.9844 53.1366 24.783 8.1642 0 0 0 0 +0 +11501.82 10580.46 8413.02 6409.458 4582.182 2761.044 1490.742 710.754 335.8674 +173.3754 96.4194 55.9944 26.4858 8.4678 0 0 0 0 +0 +11501.82 10746.12 9057.18 7549.74 6203.736 3923.898 1914.198 784.278 360.2148 +177.9558 99.2772 57.8556 27.5748 8.976 0 0 0 0 +0 diff --git a/RenderPipelineFile/data/ies_profiles/pear.ies b/RenderPipelineFile/data/ies_profiles/pear.ies index 3c8ad63a..3bbd6731 100644 --- a/RenderPipelineFile/data/ies_profiles/pear.ies +++ b/RenderPipelineFile/data/ies_profiles/pear.ies @@ -1,24 +1,24 @@ -IESNA:LM-63-1995 -[TEST]BALLABS TEST NO. 10495.0 -[MANUFAC] H.E. WILLIAMS, INC - CARTHAGE, MISSOURI -[LUMINAIRE] 1/150W ICETRON LAMP 2x2 DIRECT RECESSED LUMINAIRE -[LUMINAIRE] SPECULAR REFLECTOR w/PRISMATIC ACRYLIC PAT-12 LENS -[LUMINAIRE] SYLVANIA BALLAST #QT1x150/120-240 ICE WATTS=155 -[LUMCAT]CATALOG NO. : MPG-S22-1/150I LER -[LAMPCAT]LAMP CODE : ICE150/835 -TILT=NONE - 1 12000. 1.000000 19 5 1 1 1.688 1.677 .000 - 1.0000 1.0000 155.0000 - .0 5.0 10.0 15.0 20.0 25.0 30.0 35.0 40.0 45.0 - 50.0 55.0 60.0 65.0 70.0 75.0 80.0 85.0 90.0 - .0 22.5 45.0 67.5 90.0 - 3962. 3933. 3802. 3619. 3384. 3120. 2804. 2463. 2013. 1517. - 1089. 770. 568. 415. 333. 270. 189. 79. 0. - 3962. 3912. 3776. 3603. 3409. 3178. 2885. 2522. 2071. 1569. - 1115. 784. 573. 429. 343. 266. 185. 73. 0. - 3962. 3918. 3798. 3612. 3402. 3195. 2919. 2615. 2199. 1693. - 1203. 843. 601. 449. 337. 265. 179. 85. 0. - 3962. 3901. 3765. 3603. 3418. 3210. 2932. 2592. 2147. 1660. - 1183. 818. 577. 428. 333. 261. 179. 69. 0. - 3962. 3920. 3792. 3613. 3430. 3266. 3056. 2776. 2335. 1816. - 1338. 927. 640. 453. 342. 271. 182. 68. 0. +IESNA:LM-63-1995 +[TEST]BALLABS TEST NO. 10495.0 +[MANUFAC] H.E. WILLIAMS, INC - CARTHAGE, MISSOURI +[LUMINAIRE] 1/150W ICETRON LAMP 2x2 DIRECT RECESSED LUMINAIRE +[LUMINAIRE] SPECULAR REFLECTOR w/PRISMATIC ACRYLIC PAT-12 LENS +[LUMINAIRE] SYLVANIA BALLAST #QT1x150/120-240 ICE WATTS=155 +[LUMCAT]CATALOG NO. : MPG-S22-1/150I LER +[LAMPCAT]LAMP CODE : ICE150/835 +TILT=NONE + 1 12000. 1.000000 19 5 1 1 1.688 1.677 .000 + 1.0000 1.0000 155.0000 + .0 5.0 10.0 15.0 20.0 25.0 30.0 35.0 40.0 45.0 + 50.0 55.0 60.0 65.0 70.0 75.0 80.0 85.0 90.0 + .0 22.5 45.0 67.5 90.0 + 3962. 3933. 3802. 3619. 3384. 3120. 2804. 2463. 2013. 1517. + 1089. 770. 568. 415. 333. 270. 189. 79. 0. + 3962. 3912. 3776. 3603. 3409. 3178. 2885. 2522. 2071. 1569. + 1115. 784. 573. 429. 343. 266. 185. 73. 0. + 3962. 3918. 3798. 3612. 3402. 3195. 2919. 2615. 2199. 1693. + 1203. 843. 601. 449. 337. 265. 179. 85. 0. + 3962. 3901. 3765. 3603. 3418. 3210. 2932. 2592. 2147. 1660. + 1183. 818. 577. 428. 333. 261. 179. 69. 0. + 3962. 3920. 3792. 3613. 3430. 3266. 3056. 2776. 2335. 1816. + 1338. 927. 640. 453. 342. 271. 182. 68. 0. diff --git a/RenderPipelineFile/data/ies_profiles/scatter_light.ies b/RenderPipelineFile/data/ies_profiles/scatter_light.ies index 338da4e6..1d3745fa 100644 --- a/RenderPipelineFile/data/ies_profiles/scatter_light.ies +++ b/RenderPipelineFile/data/ies_profiles/scatter_light.ies @@ -1,36 +1,36 @@ -IESNA:LM-63-2002 -[TEST]BALLABS TEST NO. 14353.0 -[TESTLAB] BUILDING ACOUSTICS & LIGHTING LABORATORIES, INC -[ISSUEDATE] 29-DEC-2008 -[MANUFAC] H.E. WILLIAMS, INC - CARTHAGE, MISSOURI -[LUMINAIRE] 1/250W ED28 PS MH LAMP 17x17x10"SQ AREA LUMINAIRE -[MORE] MIRO4 SPECULAR FORWARD THROW DISTRIBUTION REFLECTOR -[MORE] CLEAR FLAT GLASS LENS -[LUMCAT] OER1717-250PSMH120-TFT -[LAMPCAT] M153/E MH250W/H75/P -TILT=NONE -1 22000 1 25 21 1 1 1.11 1.11 0 -1 1 250 -0 5 10 15 20 25 30 35 40 45 50 55 60 62.5 65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 -0 5 15 25 35 45 55 65 75 85 90 95 105 115 125 135 145 155 165 175 180 -2698.00 3943.00 5458.00 6672.00 6831.00 7214.00 7080.00 5916.00 5057.00 5401.00 5200.00 3823.00 4642.00 4546.00 3326.00 4307.00 4716.00 3352.00 2186.00 1024.00 164.00 33.00 1.00 0.00 0.00 -2698.00 3973.00 5563.00 7147.00 7067.00 7341.00 7279.00 6574.00 5462.00 5861.00 5381.00 3878.00 4745.00 4638.00 3478.00 4444.00 4903.00 3779.00 2312.00 1321.00 268.00 34.00 0.00 0.00 0.00 -2698.00 4247.00 5895.00 7809.00 7305.00 7481.00 7591.00 7469.00 5914.00 5507.00 5457.00 3972.00 4789.00 4514.00 3983.00 4291.00 4333.00 4226.00 3418.00 1555.00 301.00 33.00 0.00 0.00 0.00 -2698.00 3962.00 5992.00 8215.00 7325.00 7430.00 7666.00 6840.00 5236.00 4909.00 5078.00 4158.00 4647.00 4356.00 4229.00 3282.00 4660.00 3998.00 3538.00 1950.00 464.00 38.00 0.00 0.00 0.00 -2698.00 3718.00 5991.00 8093.00 7644.00 7666.00 7065.00 6385.00 6315.00 4905.00 5666.00 4104.00 4832.00 6475.00 4871.00 2674.00 5890.00 4150.00 5346.00 2833.00 702.00 47.00 1.00 0.00 0.00 -2698.00 3315.00 5882.00 7298.00 8906.00 7613.00 6407.00 6351.00 7849.00 5321.00 7181.00 4739.00 5685.00 4241.00 3129.00 2071.00 3098.00 3571.00 3435.00 2095.00 1203.00 271.00 0.00 0.00 0.00 -2698.00 3110.00 4667.00 6205.00 7897.00 8795.00 6483.00 7120.00 6544.00 5762.00 10333.00 2340.00 3481.00 3706.00 3439.00 2736.00 1690.00 2207.00 2514.00 1436.00 644.00 109.00 0.00 0.00 0.00 -2698.00 2701.00 3758.00 4788.00 6029.00 7687.00 8309.00 7108.00 5106.00 7821.00 3277.00 2142.00 2247.00 2162.00 2026.00 1480.00 1290.00 1412.00 387.00 543.00 182.00 117.00 0.00 0.00 0.00 -2698.00 2771.00 2944.00 4037.00 4601.00 4975.00 5264.00 3669.00 4336.00 7602.00 3156.00 3270.00 2701.00 2160.00 2660.00 1389.00 1533.00 1199.00 1021.00 224.00 180.00 54.00 0.00 0.00 0.00 -2698.00 2732.00 3015.00 3506.00 4038.00 3169.00 3471.00 2415.00 3423.00 7272.00 2935.00 1882.00 1764.00 1895.00 2225.00 1567.00 1584.00 1262.00 703.00 192.00 346.00 47.00 0.00 0.00 0.00 -2698.00 2743.00 3286.00 4036.00 4457.00 3456.00 2094.00 2262.00 4356.00 7484.00 3955.00 1670.00 1163.00 1062.00 943.00 857.00 701.00 465.00 189.00 78.00 56.00 6.00 0.00 0.00 0.00 -2698.00 2918.00 4025.00 5141.00 5140.00 3749.00 2006.00 1904.00 2552.00 2538.00 1859.00 1553.00 1125.00 1073.00 958.00 1141.00 672.00 478.00 182.00 64.00 25.00 0.00 0.00 0.00 0.00 -2698.00 3464.00 4778.00 5035.00 4629.00 3210.00 2471.00 2042.00 3098.00 2579.00 1434.00 1252.00 1146.00 1423.00 982.00 1008.00 1184.00 353.00 97.00 35.00 5.00 0.00 0.00 0.00 0.00 -2698.00 4025.00 4809.00 4376.00 3673.00 2601.00 1992.00 2043.00 1859.00 1968.00 2114.00 1492.00 1216.00 1066.00 686.00 354.00 220.00 95.00 53.00 17.00 4.00 0.00 0.00 0.00 0.00 -2698.00 4236.00 4435.00 3765.00 2559.00 2564.00 1816.00 1794.00 1815.00 1890.00 1660.00 1338.00 687.00 351.00 558.00 101.00 59.00 55.00 61.00 41.00 10.00 0.00 0.00 0.00 0.00 -2698.00 4418.00 4174.00 2760.00 2602.00 2238.00 1959.00 1846.00 1693.00 1664.00 1665.00 796.00 306.00 125.00 94.00 86.00 76.00 72.00 71.00 59.00 36.00 17.00 0.00 0.00 0.00 -2698.00 4391.00 3812.00 2473.00 2438.00 2231.00 2219.00 1983.00 1899.00 1760.00 889.00 331.00 130.00 166.00 309.00 301.00 94.00 58.00 47.00 34.00 21.00 7.00 0.00 0.00 0.00 -2698.00 4257.00 3531.00 2438.00 2319.00 2044.00 2167.00 2131.00 1947.00 1503.00 668.00 224.00 285.00 212.00 105.00 80.00 70.00 65.00 53.00 38.00 25.00 11.00 0.00 0.00 0.00 -2698.00 4110.00 3488.00 2406.00 2227.00 2084.00 2068.00 2015.00 1882.00 1212.00 589.00 195.00 283.00 276.00 162.00 109.00 105.00 90.00 82.00 50.00 35.00 19.00 5.00 0.00 0.00 -2698.00 4024.00 3228.00 2390.00 2172.00 2042.00 2014.00 1959.00 1846.00 1134.00 463.00 188.00 265.00 235.00 153.00 108.00 110.00 90.00 76.00 53.00 38.00 23.00 8.00 0.00 0.00 -2698.00 3931.00 3193.00 2575.00 2140.00 2066.00 1998.00 1956.00 1841.00 1186.00 544.00 205.00 274.00 230.00 167.00 112.00 115.00 93.00 76.00 56.00 41.00 25.00 9.00 0.00 0.00 +IESNA:LM-63-2002 +[TEST]BALLABS TEST NO. 14353.0 +[TESTLAB] BUILDING ACOUSTICS & LIGHTING LABORATORIES, INC +[ISSUEDATE] 29-DEC-2008 +[MANUFAC] H.E. WILLIAMS, INC - CARTHAGE, MISSOURI +[LUMINAIRE] 1/250W ED28 PS MH LAMP 17x17x10"SQ AREA LUMINAIRE +[MORE] MIRO4 SPECULAR FORWARD THROW DISTRIBUTION REFLECTOR +[MORE] CLEAR FLAT GLASS LENS +[LUMCAT] OER1717-250PSMH120-TFT +[LAMPCAT] M153/E MH250W/H75/P +TILT=NONE +1 22000 1 25 21 1 1 1.11 1.11 0 +1 1 250 +0 5 10 15 20 25 30 35 40 45 50 55 60 62.5 65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 +0 5 15 25 35 45 55 65 75 85 90 95 105 115 125 135 145 155 165 175 180 +2698.00 3943.00 5458.00 6672.00 6831.00 7214.00 7080.00 5916.00 5057.00 5401.00 5200.00 3823.00 4642.00 4546.00 3326.00 4307.00 4716.00 3352.00 2186.00 1024.00 164.00 33.00 1.00 0.00 0.00 +2698.00 3973.00 5563.00 7147.00 7067.00 7341.00 7279.00 6574.00 5462.00 5861.00 5381.00 3878.00 4745.00 4638.00 3478.00 4444.00 4903.00 3779.00 2312.00 1321.00 268.00 34.00 0.00 0.00 0.00 +2698.00 4247.00 5895.00 7809.00 7305.00 7481.00 7591.00 7469.00 5914.00 5507.00 5457.00 3972.00 4789.00 4514.00 3983.00 4291.00 4333.00 4226.00 3418.00 1555.00 301.00 33.00 0.00 0.00 0.00 +2698.00 3962.00 5992.00 8215.00 7325.00 7430.00 7666.00 6840.00 5236.00 4909.00 5078.00 4158.00 4647.00 4356.00 4229.00 3282.00 4660.00 3998.00 3538.00 1950.00 464.00 38.00 0.00 0.00 0.00 +2698.00 3718.00 5991.00 8093.00 7644.00 7666.00 7065.00 6385.00 6315.00 4905.00 5666.00 4104.00 4832.00 6475.00 4871.00 2674.00 5890.00 4150.00 5346.00 2833.00 702.00 47.00 1.00 0.00 0.00 +2698.00 3315.00 5882.00 7298.00 8906.00 7613.00 6407.00 6351.00 7849.00 5321.00 7181.00 4739.00 5685.00 4241.00 3129.00 2071.00 3098.00 3571.00 3435.00 2095.00 1203.00 271.00 0.00 0.00 0.00 +2698.00 3110.00 4667.00 6205.00 7897.00 8795.00 6483.00 7120.00 6544.00 5762.00 10333.00 2340.00 3481.00 3706.00 3439.00 2736.00 1690.00 2207.00 2514.00 1436.00 644.00 109.00 0.00 0.00 0.00 +2698.00 2701.00 3758.00 4788.00 6029.00 7687.00 8309.00 7108.00 5106.00 7821.00 3277.00 2142.00 2247.00 2162.00 2026.00 1480.00 1290.00 1412.00 387.00 543.00 182.00 117.00 0.00 0.00 0.00 +2698.00 2771.00 2944.00 4037.00 4601.00 4975.00 5264.00 3669.00 4336.00 7602.00 3156.00 3270.00 2701.00 2160.00 2660.00 1389.00 1533.00 1199.00 1021.00 224.00 180.00 54.00 0.00 0.00 0.00 +2698.00 2732.00 3015.00 3506.00 4038.00 3169.00 3471.00 2415.00 3423.00 7272.00 2935.00 1882.00 1764.00 1895.00 2225.00 1567.00 1584.00 1262.00 703.00 192.00 346.00 47.00 0.00 0.00 0.00 +2698.00 2743.00 3286.00 4036.00 4457.00 3456.00 2094.00 2262.00 4356.00 7484.00 3955.00 1670.00 1163.00 1062.00 943.00 857.00 701.00 465.00 189.00 78.00 56.00 6.00 0.00 0.00 0.00 +2698.00 2918.00 4025.00 5141.00 5140.00 3749.00 2006.00 1904.00 2552.00 2538.00 1859.00 1553.00 1125.00 1073.00 958.00 1141.00 672.00 478.00 182.00 64.00 25.00 0.00 0.00 0.00 0.00 +2698.00 3464.00 4778.00 5035.00 4629.00 3210.00 2471.00 2042.00 3098.00 2579.00 1434.00 1252.00 1146.00 1423.00 982.00 1008.00 1184.00 353.00 97.00 35.00 5.00 0.00 0.00 0.00 0.00 +2698.00 4025.00 4809.00 4376.00 3673.00 2601.00 1992.00 2043.00 1859.00 1968.00 2114.00 1492.00 1216.00 1066.00 686.00 354.00 220.00 95.00 53.00 17.00 4.00 0.00 0.00 0.00 0.00 +2698.00 4236.00 4435.00 3765.00 2559.00 2564.00 1816.00 1794.00 1815.00 1890.00 1660.00 1338.00 687.00 351.00 558.00 101.00 59.00 55.00 61.00 41.00 10.00 0.00 0.00 0.00 0.00 +2698.00 4418.00 4174.00 2760.00 2602.00 2238.00 1959.00 1846.00 1693.00 1664.00 1665.00 796.00 306.00 125.00 94.00 86.00 76.00 72.00 71.00 59.00 36.00 17.00 0.00 0.00 0.00 +2698.00 4391.00 3812.00 2473.00 2438.00 2231.00 2219.00 1983.00 1899.00 1760.00 889.00 331.00 130.00 166.00 309.00 301.00 94.00 58.00 47.00 34.00 21.00 7.00 0.00 0.00 0.00 +2698.00 4257.00 3531.00 2438.00 2319.00 2044.00 2167.00 2131.00 1947.00 1503.00 668.00 224.00 285.00 212.00 105.00 80.00 70.00 65.00 53.00 38.00 25.00 11.00 0.00 0.00 0.00 +2698.00 4110.00 3488.00 2406.00 2227.00 2084.00 2068.00 2015.00 1882.00 1212.00 589.00 195.00 283.00 276.00 162.00 109.00 105.00 90.00 82.00 50.00 35.00 19.00 5.00 0.00 0.00 +2698.00 4024.00 3228.00 2390.00 2172.00 2042.00 2014.00 1959.00 1846.00 1134.00 463.00 188.00 265.00 235.00 153.00 108.00 110.00 90.00 76.00 53.00 38.00 23.00 8.00 0.00 0.00 +2698.00 3931.00 3193.00 2575.00 2140.00 2066.00 1998.00 1956.00 1841.00 1186.00 544.00 205.00 274.00 230.00 167.00 112.00 115.00 93.00 76.00 56.00 41.00 25.00 9.00 0.00 0.00 diff --git a/RenderPipelineFile/data/ies_profiles/soft_arrow.ies b/RenderPipelineFile/data/ies_profiles/soft_arrow.ies index ea0c47cb..013b588b 100644 --- a/RenderPipelineFile/data/ies_profiles/soft_arrow.ies +++ b/RenderPipelineFile/data/ies_profiles/soft_arrow.ies @@ -1,14 +1,14 @@ -IESNA91 -[MANUFAC] Halo,Recessed -[LUMCAT] H7t-301 -[LUMINAIRE] Open Trim (75W R-30 Flood) -[LAMP] -[REPORT] -TILT=NONE -1 900.0 1.0 36 1 1 1 0.0 -0.32 0.0 -1.0 1.0 75.0 -0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 11.0 12.0 13.0 14.0 15.0 17.5 20.0 22.5 25.0 27.5 30.0 32.5 35.0 37.5 40.0 45.0 -50.0 55.0 60.0 65.0 70.0 75.0 80.0 85.0 90.0 -0.0 -1512.0 1516.0 1508.0 1487.0 1453.0 1409.0 1357.0 1294.0 1229.0 1158.0 1085.0 1008.0 934.0 862.0 795.0 727.0 578.0 459.0 368.0 -304.0 256.0 222.0 197.0 177.0 163.0 153.0 141.0 124.0 73.0 28.0 23.0 18.0 13.0 9.0 4.0 1.0 +IESNA91 +[MANUFAC] Halo,Recessed +[LUMCAT] H7t-301 +[LUMINAIRE] Open Trim (75W R-30 Flood) +[LAMP] +[REPORT] +TILT=NONE +1 900.0 1.0 36 1 1 1 0.0 -0.32 0.0 +1.0 1.0 75.0 +0.0 1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 9.0 10.0 11.0 12.0 13.0 14.0 15.0 17.5 20.0 22.5 25.0 27.5 30.0 32.5 35.0 37.5 40.0 45.0 +50.0 55.0 60.0 65.0 70.0 75.0 80.0 85.0 90.0 +0.0 +1512.0 1516.0 1508.0 1487.0 1453.0 1409.0 1357.0 1294.0 1229.0 1158.0 1085.0 1008.0 934.0 862.0 795.0 727.0 578.0 459.0 368.0 +304.0 256.0 222.0 197.0 177.0 163.0 153.0 141.0 124.0 73.0 28.0 23.0 18.0 13.0 9.0 4.0 1.0 diff --git a/RenderPipelineFile/data/ies_profiles/soft_display.ies b/RenderPipelineFile/data/ies_profiles/soft_display.ies index f3cb4c3e..d1207a40 100644 --- a/RenderPipelineFile/data/ies_profiles/soft_display.ies +++ b/RenderPipelineFile/data/ies_profiles/soft_display.ies @@ -1,29 +1,29 @@ -ERCO Leuchten GmbH BY: ERCO/LUM650/8701 -74216023+Q50MR16/C/CG55-FNV.IES -- -- -- -- -- -- -TILT=NONE -1 825 .825 73 2 1 2 -.2 0 0 -1.00 1.00 50 -0 1.25 2.5 3.75 5 6.25 7.5 8.75 10 11.25 12.5 13.75 15 16.25 17.5 18.75 20 -21.25 22.5 23.75 25 26.25 27.5 28.75 30 31.25 32.5 33.75 35 36.25 37.5 38.75 -40 41.25 42.5 43.75 45 46.25 47.5 48.75 50 51.25 52.5 53.75 55 56.25 57.5 -58.75 60 61.25 62.5 63.75 65 66.25 67.5 68.75 70 71.25 72.5 73.75 75 76.25 -77.5 78.75 80 81.25 82.5 83.75 85 86.25 87.5 88.75 90 -0 90 -1091 1113 1132 1160 1183 1206 1219 1223 1217 1207 1190 1162 1120 1063 995.2 -919 837.6 753.2 666.7 579.4 495.8 418.3 338.2 250.3 175.8 135.5 123.9 128.9 -138.2 142.1 140.9 137.1 133.3 131.4 129.6 125.5 116.4 100.9 81.69 62.57 47.27 -38.54 35 34.26 33.94 32.17 29.22 25.91 23.03 21.2 20.24 19.77 19.39 18.83 -18.14 17.47 16.97 16.74 16.74 16.85 16.97 17.03 17.03 17 16.97 16.95 16.95 -16.96 16.97 16.97 16.97 16.97 16.97 -1091 1113 1132 1160 1183 1206 1219 1223 1217 1207 1190 1162 1120 1063 995.2 -919 837.6 753.2 666.7 579.4 495.8 418.3 338.2 250.3 175.8 135.5 123.9 128.9 -138.2 142.1 140.9 137.1 133.3 131.4 129.6 125.5 116.4 100.9 81.69 62.57 47.27 -38.54 35 34.26 33.94 32.17 29.22 25.91 23.03 21.2 20.24 19.77 19.39 18.83 -18.14 17.47 16.97 16.74 16.74 16.85 16.97 17.03 17.03 17 16.97 16.95 16.95 -16.96 16.97 16.97 16.97 16.97 16.97 +ERCO Leuchten GmbH BY: ERCO/LUM650/8701 +74216023+Q50MR16/C/CG55-FNV.IES +- +- +- +- +- +- +TILT=NONE +1 825 .825 73 2 1 2 -.2 0 0 +1.00 1.00 50 +0 1.25 2.5 3.75 5 6.25 7.5 8.75 10 11.25 12.5 13.75 15 16.25 17.5 18.75 20 +21.25 22.5 23.75 25 26.25 27.5 28.75 30 31.25 32.5 33.75 35 36.25 37.5 38.75 +40 41.25 42.5 43.75 45 46.25 47.5 48.75 50 51.25 52.5 53.75 55 56.25 57.5 +58.75 60 61.25 62.5 63.75 65 66.25 67.5 68.75 70 71.25 72.5 73.75 75 76.25 +77.5 78.75 80 81.25 82.5 83.75 85 86.25 87.5 88.75 90 +0 90 +1091 1113 1132 1160 1183 1206 1219 1223 1217 1207 1190 1162 1120 1063 995.2 +919 837.6 753.2 666.7 579.4 495.8 418.3 338.2 250.3 175.8 135.5 123.9 128.9 +138.2 142.1 140.9 137.1 133.3 131.4 129.6 125.5 116.4 100.9 81.69 62.57 47.27 +38.54 35 34.26 33.94 32.17 29.22 25.91 23.03 21.2 20.24 19.77 19.39 18.83 +18.14 17.47 16.97 16.74 16.74 16.85 16.97 17.03 17.03 17 16.97 16.95 16.95 +16.96 16.97 16.97 16.97 16.97 16.97 +1091 1113 1132 1160 1183 1206 1219 1223 1217 1207 1190 1162 1120 1063 995.2 +919 837.6 753.2 666.7 579.4 495.8 418.3 338.2 250.3 175.8 135.5 123.9 128.9 +138.2 142.1 140.9 137.1 133.3 131.4 129.6 125.5 116.4 100.9 81.69 62.57 47.27 +38.54 35 34.26 33.94 32.17 29.22 25.91 23.03 21.2 20.24 19.77 19.39 18.83 +18.14 17.47 16.97 16.74 16.74 16.85 16.97 17.03 17.03 17 16.97 16.95 16.95 +16.96 16.97 16.97 16.97 16.97 16.97 diff --git a/RenderPipelineFile/data/ies_profiles/star_focused.ies b/RenderPipelineFile/data/ies_profiles/star_focused.ies index d2decb09..cb1cbe6a 100644 --- a/RenderPipelineFile/data/ies_profiles/star_focused.ies +++ b/RenderPipelineFile/data/ies_profiles/star_focused.ies @@ -1,48 +1,48 @@ -IESNA91 -[TEST] NO. 12296 -[MANUFAC]LITHONIA - ESG MODEL HQM LAMPHEAD -[LUMCAT]ELA CDS N0606 -[LUMINAIRE]SPECULAR REFLECTOR WITH TRANSLUCENT PLASTIC LENS -[LAMP]ONE GE#939 INCANDESCENT WEDGE MINIATURE LAMP.. LUMEN RATING = 68 LMS. -[MORE]6 VOLTS DC OPERATING AT .9 AMPS AND 5.4 WATTS -[OTHER]LITHONIA- ESG LIGHTING -[MORE]DECATUR, GA -[OTHER]Version: 4/1/97 - 12:00:00 -TILT=NONE -1 -68 -1 -37 -5 -1 -1 -.33 -.33 -.00 -1 -1 -5.4000 -0,2.5,5,7.5,10,12.5,15,17.5,20,22.5,25,27.5,30,32.5,35,37.5 -40,42.5,45,47.5,50,52.5,55,57.5,60,62.5,65,67.5,70,72.5,75 -77.5,80,82.5,85,87.5,90 -0,22.5,45,67.5,90 -166,160,125,88,53,40,27,19,14,11 -9,7,7,6,6,5,5,5,5,4 -4,4,3,3,3,3,3,2,2,1 -1,1,1,1,0,0,0 -166,158,124,89,59,40,26,18,13,10 -8,7,6,6,5,5,5,5,4,4 -4,4,4,3,3,2,2,2,1,1 -1,1,1,0,0,0,0 -166,153,120,85,56,37,24,17,12,9 -8,7,6,5,5,5,5,4,4,4 -4,4,3,3,3,2,2,2,1,1 -1,1,1,0,0,0,0 -166,148,114,80,52,34,22,15,11,9 -7,6,6,6,5,5,5,4,4,4 -4,4,3,3,3,2,2,2,1,1 -1,1,1,0,0,0,0 -166,142,108,75,49,32,21,15,11,9 -7,6,6,6,5,5,5,5,4,4 -4,4,3,3,3,2,2,2,1,1 -1,1,1,0,0,0,0 +IESNA91 +[TEST] NO. 12296 +[MANUFAC]LITHONIA - ESG MODEL HQM LAMPHEAD +[LUMCAT]ELA CDS N0606 +[LUMINAIRE]SPECULAR REFLECTOR WITH TRANSLUCENT PLASTIC LENS +[LAMP]ONE GE#939 INCANDESCENT WEDGE MINIATURE LAMP.. LUMEN RATING = 68 LMS. +[MORE]6 VOLTS DC OPERATING AT .9 AMPS AND 5.4 WATTS +[OTHER]LITHONIA- ESG LIGHTING +[MORE]DECATUR, GA +[OTHER]Version: 4/1/97 - 12:00:00 +TILT=NONE +1 +68 +1 +37 +5 +1 +1 +.33 +.33 +.00 +1 +1 +5.4000 +0,2.5,5,7.5,10,12.5,15,17.5,20,22.5,25,27.5,30,32.5,35,37.5 +40,42.5,45,47.5,50,52.5,55,57.5,60,62.5,65,67.5,70,72.5,75 +77.5,80,82.5,85,87.5,90 +0,22.5,45,67.5,90 +166,160,125,88,53,40,27,19,14,11 +9,7,7,6,6,5,5,5,5,4 +4,4,3,3,3,3,3,2,2,1 +1,1,1,1,0,0,0 +166,158,124,89,59,40,26,18,13,10 +8,7,6,6,5,5,5,5,4,4 +4,4,4,3,3,2,2,2,1,1 +1,1,1,0,0,0,0 +166,153,120,85,56,37,24,17,12,9 +8,7,6,5,5,5,5,4,4,4 +4,4,3,3,3,2,2,2,1,1 +1,1,1,0,0,0,0 +166,148,114,80,52,34,22,15,11,9 +7,6,6,6,5,5,5,4,4,4 +4,4,3,3,3,2,2,2,1,1 +1,1,1,0,0,0,0 +166,142,108,75,49,32,21,15,11,9 +7,6,6,6,5,5,5,5,4,4 +4,4,3,3,3,2,2,2,1,1 +1,1,1,0,0,0,0 diff --git a/RenderPipelineFile/data/ies_profiles/three_lobe_umbrella.ies b/RenderPipelineFile/data/ies_profiles/three_lobe_umbrella.ies index cf7881b1..132a58c8 100644 --- a/RenderPipelineFile/data/ies_profiles/three_lobe_umbrella.ies +++ b/RenderPipelineFile/data/ies_profiles/three_lobe_umbrella.ies @@ -1,16 +1,16 @@ -IESNA:LM-63-1995 -[TEST] 100071_0 BY: ERCO / LUM650 -[DATE]02.12.2004 -[MANUFAC] ERCO Leuchten GmbH -[LUMCAT] 22625000 -[LUMINAIRE] Lightcast Directional luminaire -[LAMPCAT] QPAR30 75W 30 -TILT=NONE -1 1234 1 37 1 1 2 -.097 0 0 -1.00 1.00 75 -0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 -125 130 135 140 145 150 155 160 165 170 175 180 -0 -2400.1 2237.2 1869.5 1201.9 501.0 362.8 273.9 155.5 125.9 108.6 100.0 86.4 -50.6 28.4 18.5 11.1 10.4 4.9 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.0 0.0 0.0 0.0 0.0 0.0 0.0 +IESNA:LM-63-1995 +[TEST] 100071_0 BY: ERCO / LUM650 +[DATE]02.12.2004 +[MANUFAC] ERCO Leuchten GmbH +[LUMCAT] 22625000 +[LUMINAIRE] Lightcast Directional luminaire +[LAMPCAT] QPAR30 75W 30 +TILT=NONE +1 1234 1 37 1 1 2 -.097 0 0 +1.00 1.00 75 +0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 95 100 105 110 115 120 +125 130 135 140 145 150 155 160 165 170 175 180 +0 +2400.1 2237.2 1869.5 1201.9 501.0 362.8 273.9 155.5 125.9 108.6 100.0 86.4 +50.6 28.4 18.5 11.1 10.4 4.9 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +0.0 0.0 0.0 0.0 0.0 0.0 0.0 diff --git a/RenderPipelineFile/data/ies_profiles/three_lobe_vee.ies b/RenderPipelineFile/data/ies_profiles/three_lobe_vee.ies index a07a0273..2b75a888 100644 --- a/RenderPipelineFile/data/ies_profiles/three_lobe_vee.ies +++ b/RenderPipelineFile/data/ies_profiles/three_lobe_vee.ies @@ -1,13 +1,13 @@ -IESNA:LM-63-1995 -[TEST] 100069_0 BY: ERCO / LUM650 -[DATE]02.12.2004 -[MANUFAC] ERCO Leuchten GmbH -[LUMCAT] 22619000_83671000 -[LUMINAIRE] Lightcast Downlight -[LAMPCAT] HIPAR-L30 70W 10 -TILT=NONE -1 4850 1 19 1 1 2 -.097 0 0 -1.00 1.00 70 -0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 -0 -68000 34008 6853 3982 936 582 543 514 349 189 92 58 44 34 0 0 0 0 0 +IESNA:LM-63-1995 +[TEST] 100069_0 BY: ERCO / LUM650 +[DATE]02.12.2004 +[MANUFAC] ERCO Leuchten GmbH +[LUMCAT] 22619000_83671000 +[LUMINAIRE] Lightcast Downlight +[LAMPCAT] HIPAR-L30 70W 10 +TILT=NONE +1 4850 1 19 1 1 2 -.097 0 0 +1.00 1.00 70 +0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 +0 +68000 34008 6853 3982 936 582 543 514 349 189 92 58 44 34 0 0 0 0 0 diff --git a/RenderPipelineFile/data/ies_profiles/tight_focused.ies b/RenderPipelineFile/data/ies_profiles/tight_focused.ies index d89c1f33..4bad0306 100644 --- a/RenderPipelineFile/data/ies_profiles/tight_focused.ies +++ b/RenderPipelineFile/data/ies_profiles/tight_focused.ies @@ -1,32 +1,32 @@ -IESNA:LM-63-1995 -[TEST]BALLABS TEST NO. 13700.0 -[MANUFAC] INFINITY LIGHTING INC - CARTHAGE, MISSOURI -[LUMINAIRE] 1/175W CLEAR ED17 MH LAMP 16x16"DIRECT WALL MNT LUMINAIRE -[MORE] SEMI-SPECULAR REFLECTOR SYSTEMS w/BLACK BLADE BAFFLES -[MORE] .1875"THICK CLEAR TEMPERED GLASS LENS -[LUMCAT] WPSQ-MH-175-MED-120 -[LAMPCAT] M57 (M175/U/MED) -TILT=NONE - 1 12800. 1.000000 19 9 1 1 .943 .917 .000 - 1.0000 1.0000 175.0000 - .0 5.0 10.0 15.0 20.0 25.0 30.0 35.0 40.0 45.0 - 50.0 55.0 60.0 65.0 70.0 75.0 80.0 85.0 90.0 - .0 22.5 45.0 67.5 90.0 112.5 135.0 157.5 180.0 - 25790. 25293. 19946. 12741. 6548. 3620. 2037. 1395. 1016. 765. - 587. 461. 338. 221. 19. 4. 0. 0. 0. - 25790. 24593. 19253. 12080. 6261. 3230. 1942. 1387. 1049. 590. - 231. 107. 49. 29. 9. 0. 0. 0. 0. - 25790. 24242. 18086. 10203. 4745. 2374. 1247. 460. 193. 91. - 54. 35. 10. 3. 2. 0. 0. 0. 0. - 25790. 23677. 17161. 9024. 3716. 1603. 608. 256. 142. 94. - 38. 10. 4. 6. 1. 0. 0. 0. 0. - 25790. 23467. 16339. 7925. 3242. 1304. 490. 249. 131. 90. - 29. 7. 4. 2. 1. 0. 0. 0. 0. - 25790. 22684. 15653. 7409. 3273. 1538. 520. 213. 113. 83. - 31. 7. 3. 4. 0. 0. 0. 0. 0. - 25790. 22633. 16005. 7769. 3449. 2045. 1312. 474. 147. 72. - 54. 45. 8. 2. 2. 0. 0. 0. 0. - 25790. 22392. 15298. 8244. 3996. 2118. 1372. 1090. 986. 833. - 486. 178. 62. 60. 7. 0. 0. 0. 0. - 25790. 23109. 15969. 8525. 3888. 2119. 1337. 1025. 835. 699. - 578. 473. 373. 251. 23. 7. 0. 0. 0. +IESNA:LM-63-1995 +[TEST]BALLABS TEST NO. 13700.0 +[MANUFAC] INFINITY LIGHTING INC - CARTHAGE, MISSOURI +[LUMINAIRE] 1/175W CLEAR ED17 MH LAMP 16x16"DIRECT WALL MNT LUMINAIRE +[MORE] SEMI-SPECULAR REFLECTOR SYSTEMS w/BLACK BLADE BAFFLES +[MORE] .1875"THICK CLEAR TEMPERED GLASS LENS +[LUMCAT] WPSQ-MH-175-MED-120 +[LAMPCAT] M57 (M175/U/MED) +TILT=NONE + 1 12800. 1.000000 19 9 1 1 .943 .917 .000 + 1.0000 1.0000 175.0000 + .0 5.0 10.0 15.0 20.0 25.0 30.0 35.0 40.0 45.0 + 50.0 55.0 60.0 65.0 70.0 75.0 80.0 85.0 90.0 + .0 22.5 45.0 67.5 90.0 112.5 135.0 157.5 180.0 + 25790. 25293. 19946. 12741. 6548. 3620. 2037. 1395. 1016. 765. + 587. 461. 338. 221. 19. 4. 0. 0. 0. + 25790. 24593. 19253. 12080. 6261. 3230. 1942. 1387. 1049. 590. + 231. 107. 49. 29. 9. 0. 0. 0. 0. + 25790. 24242. 18086. 10203. 4745. 2374. 1247. 460. 193. 91. + 54. 35. 10. 3. 2. 0. 0. 0. 0. + 25790. 23677. 17161. 9024. 3716. 1603. 608. 256. 142. 94. + 38. 10. 4. 6. 1. 0. 0. 0. 0. + 25790. 23467. 16339. 7925. 3242. 1304. 490. 249. 131. 90. + 29. 7. 4. 2. 1. 0. 0. 0. 0. + 25790. 22684. 15653. 7409. 3273. 1538. 520. 213. 113. 83. + 31. 7. 3. 4. 0. 0. 0. 0. 0. + 25790. 22633. 16005. 7769. 3449. 2045. 1312. 474. 147. 72. + 54. 45. 8. 2. 2. 0. 0. 0. 0. + 25790. 22392. 15298. 8244. 3996. 2118. 1372. 1090. 986. 833. + 486. 178. 62. 60. 7. 0. 0. 0. 0. + 25790. 23109. 15969. 8525. 3888. 2119. 1337. 1025. 835. 699. + 578. 473. 373. 251. 23. 7. 0. 0. 0. diff --git a/RenderPipelineFile/data/ies_profiles/top_post.ies b/RenderPipelineFile/data/ies_profiles/top_post.ies index bfd13433..9ea166ee 100644 --- a/RenderPipelineFile/data/ies_profiles/top_post.ies +++ b/RenderPipelineFile/data/ies_profiles/top_post.ies @@ -1,16 +1,16 @@ -IESNA:LM-63-2002 -[TEST]BALLABS TEST NO. 14501.0 -[TESTLAB] BUILDING ACOUSTICS & LIGHTING LABORATORIES, INC -[ISSUEDATE] 04-MAR-2009 -[MANUFAC] H.E. WILLIAMS, INC - CARTHAGE, MISSOURI -[LUMINAIRE] 1/100W CLEAR ED17PS MH HORIZ LAMP LS SERIES POST LUMINAIRE -[MORE] WHITE TOP REFLECTOR w/FROSTED GLASS CHIMNEY -[MORE] CLEAR ACRYLIC PANELS -[LUMCAT] HLSC15-100PSMH120-BLK -[LAMPCAT] M90 -TILT=NONE -1 9000 1 35 1 1 1 0.885 0.885 0.781 -1 1 100 -0 5 10 15 20 25 30 35 40 45 50 55 60 62.5 65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 95 105 115 125 135 145 155 165 175 180 -0 -67.00 113.00 238.00 460.00 973.00 1277.00 1388.00 1425.00 1408.00 1338.00 1199.00 1018.00 700.00 430.00 320.00 335.00 227.00 209.00 154.00 137.00 125.00 109.00 91.00 65.00 46.00 38.00 25.00 18.00 10.00 5.00 3.00 3.00 0.00 0.00 0.00 +IESNA:LM-63-2002 +[TEST]BALLABS TEST NO. 14501.0 +[TESTLAB] BUILDING ACOUSTICS & LIGHTING LABORATORIES, INC +[ISSUEDATE] 04-MAR-2009 +[MANUFAC] H.E. WILLIAMS, INC - CARTHAGE, MISSOURI +[LUMINAIRE] 1/100W CLEAR ED17PS MH HORIZ LAMP LS SERIES POST LUMINAIRE +[MORE] WHITE TOP REFLECTOR w/FROSTED GLASS CHIMNEY +[MORE] CLEAR ACRYLIC PANELS +[LUMCAT] HLSC15-100PSMH120-BLK +[LAMPCAT] M90 +TILT=NONE +1 9000 1 35 1 1 1 0.885 0.885 0.781 +1 1 100 +0 5 10 15 20 25 30 35 40 45 50 55 60 62.5 65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 95 105 115 125 135 145 155 165 175 180 +0 +67.00 113.00 238.00 460.00 973.00 1277.00 1388.00 1425.00 1408.00 1338.00 1199.00 1018.00 700.00 430.00 320.00 335.00 227.00 209.00 154.00 137.00 125.00 109.00 91.00 65.00 46.00 38.00 25.00 18.00 10.00 5.00 3.00 3.00 0.00 0.00 0.00 diff --git a/RenderPipelineFile/data/ies_profiles/trapezoid.ies b/RenderPipelineFile/data/ies_profiles/trapezoid.ies index 48cfa709..62d68854 100644 --- a/RenderPipelineFile/data/ies_profiles/trapezoid.ies +++ b/RenderPipelineFile/data/ies_profiles/trapezoid.ies @@ -1,36 +1,36 @@ -IESNA:LM-63-2002 -[TEST]BALLABS TEST NO. 14477.0 -[TESTLAB] BUILDING ACOUSTICS & LIGHTING LABORATORIES, INC -[ISSUEDATE] 11-FEB-2009 -[MANUFAC] H.E. WILLIAMS, INC - CARTHAGE, MISSOURI -[LUMINAIRE] 1/400W CLEAR ED37 PS MH 18x17x9"WALL PACK AREA LUMINAIRE -[MORE] HAMMERTONE REFLECTOR w/FLAT CLEAR GLASS LENS -[MORE] -[LUMCAT] WPTZ3-PSMH-400-MED-FD-120 -[LAMPCAT] M155/E MH400W/H75/P -TILT=NONE -1 40000 1 25 21 1 1 1.25 0.687 0 -1 1 400 -0 5 10 15 20 25 30 35 40 45 50 55 60 62.5 65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 -0 5 15 25 35 45 55 65 75 85 90 95 105 115 125 135 145 155 165 175 180 -9079.00 7777.00 6697.00 6001.00 5457.00 5017.00 5081.00 5849.00 6929.00 7921.00 7361.00 6753.00 4769.00 2896.00 2672.00 2472.00 2192.00 1720.00 1336.00 960.00 568.00 152.00 0.00 0.00 0.00 -9079.00 7769.00 6681.00 6025.00 5425.00 5001.00 5057.00 5785.00 7089.00 8033.00 7569.00 6929.00 5697.00 3056.00 2712.00 2528.00 2224.00 1728.00 1400.00 1000.00 592.00 184.00 0.00 0.00 0.00 -9079.00 7833.00 6849.00 6121.00 5481.00 4977.00 4889.00 5633.00 6945.00 8305.00 7953.00 7009.00 6313.00 3784.00 2840.00 2576.00 2328.00 1888.00 1504.00 1056.00 648.00 224.00 0.00 0.00 0.00 -9079.00 7985.00 7001.00 6209.00 5465.00 4945.00 4680.00 5177.00 6281.00 7705.00 8033.00 6609.00 5681.00 4624.00 2296.00 1904.00 1616.00 1248.00 888.00 544.00 336.00 80.00 0.00 0.00 0.00 -9079.00 8129.00 7257.00 6329.00 5489.00 4793.00 4368.00 4456.00 4985.00 5345.00 4905.00 3936.00 3416.00 3248.00 2616.00 696.00 536.00 400.00 264.00 152.00 64.00 8.00 0.00 0.00 0.00 -9079.00 8281.00 7465.00 6505.00 5505.00 4737.00 4168.00 3896.00 3832.00 3744.00 3384.00 3136.00 2880.00 2176.00 1272.00 680.00 616.00 544.00 408.00 232.00 104.00 24.00 0.00 0.00 0.00 -9079.00 8529.00 7793.00 6825.00 5745.00 4753.00 4016.00 3528.00 3224.00 3048.00 3088.00 3184.00 2536.00 1624.00 1064.00 832.00 712.00 616.00 528.00 320.00 144.00 32.00 0.00 0.00 0.00 -9079.00 8689.00 8121.00 7353.00 6057.00 4969.00 4000.00 3456.00 3080.00 2864.00 2800.00 2800.00 2008.00 1512.00 1392.00 1200.00 928.00 616.00 440.00 272.00 136.00 40.00 0.00 0.00 0.00 -9079.00 8961.00 8577.00 7841.00 6721.00 5553.00 4352.00 3560.00 3096.00 2872.00 2688.00 2464.00 1360.00 1128.00 1120.00 1072.00 976.00 824.00 640.00 392.00 200.00 64.00 0.00 0.00 0.00 -9079.00 9089.00 8817.00 8161.00 7185.00 5953.00 4576.00 3664.00 3208.00 2992.00 2792.00 2472.00 1352.00 1048.00 1024.00 944.00 856.00 720.00 608.00 424.00 200.00 56.00 0.00 0.00 0.00 -9079.00 9297.00 9121.00 8609.00 7737.00 6641.00 5129.00 3840.00 3344.00 3168.00 2976.00 2544.00 1368.00 1032.00 936.00 808.00 712.00 608.00 472.00 328.00 176.00 48.00 0.00 0.00 0.00 -9079.00 9466.00 9401.00 8977.00 8161.00 7297.00 5513.00 3968.00 3448.00 3240.00 2848.00 2080.00 1224.00 1016.00 872.00 736.00 600.00 448.00 336.00 208.00 104.00 32.00 0.00 0.00 0.00 -9079.00 9666.00 9706.00 9313.00 8473.00 7369.00 5313.00 2864.00 1744.00 1352.00 1344.00 1264.00 1016.00 856.00 752.00 584.00 472.00 320.00 224.00 128.00 64.00 16.00 0.00 0.00 0.00 -9079.00 9874.00 10034.00 9642.00 8233.00 5769.00 3600.00 2352.00 1592.00 1256.00 1208.00 1240.00 904.00 696.00 536.00 384.00 272.00 168.00 104.00 56.00 24.00 0.00 0.00 0.00 0.00 -9079.00 10002.00 10210.00 9225.00 6513.00 4504.00 3880.00 3464.00 2496.00 1456.00 984.00 792.00 504.00 360.00 272.00 200.00 160.00 104.00 80.00 32.00 16.00 0.00 0.00 0.00 0.00 -9079.00 10162.00 10338.00 8361.00 5553.00 4721.00 4440.00 4256.00 3624.00 2056.00 768.00 456.00 280.00 200.00 160.00 120.00 96.00 56.00 40.00 16.00 8.00 0.00 0.00 0.00 0.00 -9079.00 10242.00 10226.00 7433.00 5529.00 5033.00 4897.00 4560.00 3376.00 1536.00 568.00 312.00 200.00 152.00 120.00 88.00 80.00 40.00 32.00 16.00 8.00 0.00 0.00 0.00 0.00 -9079.00 10330.00 9938.00 6873.00 5721.00 5409.00 5177.00 4352.00 2552.00 1032.00 544.00 360.00 248.00 176.00 136.00 96.00 72.00 40.00 32.00 24.00 8.00 0.00 0.00 0.00 0.00 -9079.00 10234.00 9794.00 6873.00 5913.00 5641.00 5273.00 4016.00 2120.00 1120.00 736.00 552.00 384.00 280.00 184.00 112.00 80.00 48.00 40.00 24.00 8.00 0.00 0.00 0.00 0.00 -9079.00 10242.00 9794.00 6825.00 5985.00 5777.00 5313.00 4000.00 2104.00 1160.00 840.00 640.00 456.00 320.00 232.00 152.00 104.00 56.00 48.00 24.00 16.00 0.00 0.00 0.00 0.00 -9079.00 10298.00 9666.00 6905.00 6097.00 5849.00 5401.00 4152.00 2128.00 1248.00 904.00 720.00 488.00 352.00 248.00 168.00 120.00 64.00 48.00 32.00 24.00 8.00 0.00 0.00 0.00 +IESNA:LM-63-2002 +[TEST]BALLABS TEST NO. 14477.0 +[TESTLAB] BUILDING ACOUSTICS & LIGHTING LABORATORIES, INC +[ISSUEDATE] 11-FEB-2009 +[MANUFAC] H.E. WILLIAMS, INC - CARTHAGE, MISSOURI +[LUMINAIRE] 1/400W CLEAR ED37 PS MH 18x17x9"WALL PACK AREA LUMINAIRE +[MORE] HAMMERTONE REFLECTOR w/FLAT CLEAR GLASS LENS +[MORE] +[LUMCAT] WPTZ3-PSMH-400-MED-FD-120 +[LAMPCAT] M155/E MH400W/H75/P +TILT=NONE +1 40000 1 25 21 1 1 1.25 0.687 0 +1 1 400 +0 5 10 15 20 25 30 35 40 45 50 55 60 62.5 65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 +0 5 15 25 35 45 55 65 75 85 90 95 105 115 125 135 145 155 165 175 180 +9079.00 7777.00 6697.00 6001.00 5457.00 5017.00 5081.00 5849.00 6929.00 7921.00 7361.00 6753.00 4769.00 2896.00 2672.00 2472.00 2192.00 1720.00 1336.00 960.00 568.00 152.00 0.00 0.00 0.00 +9079.00 7769.00 6681.00 6025.00 5425.00 5001.00 5057.00 5785.00 7089.00 8033.00 7569.00 6929.00 5697.00 3056.00 2712.00 2528.00 2224.00 1728.00 1400.00 1000.00 592.00 184.00 0.00 0.00 0.00 +9079.00 7833.00 6849.00 6121.00 5481.00 4977.00 4889.00 5633.00 6945.00 8305.00 7953.00 7009.00 6313.00 3784.00 2840.00 2576.00 2328.00 1888.00 1504.00 1056.00 648.00 224.00 0.00 0.00 0.00 +9079.00 7985.00 7001.00 6209.00 5465.00 4945.00 4680.00 5177.00 6281.00 7705.00 8033.00 6609.00 5681.00 4624.00 2296.00 1904.00 1616.00 1248.00 888.00 544.00 336.00 80.00 0.00 0.00 0.00 +9079.00 8129.00 7257.00 6329.00 5489.00 4793.00 4368.00 4456.00 4985.00 5345.00 4905.00 3936.00 3416.00 3248.00 2616.00 696.00 536.00 400.00 264.00 152.00 64.00 8.00 0.00 0.00 0.00 +9079.00 8281.00 7465.00 6505.00 5505.00 4737.00 4168.00 3896.00 3832.00 3744.00 3384.00 3136.00 2880.00 2176.00 1272.00 680.00 616.00 544.00 408.00 232.00 104.00 24.00 0.00 0.00 0.00 +9079.00 8529.00 7793.00 6825.00 5745.00 4753.00 4016.00 3528.00 3224.00 3048.00 3088.00 3184.00 2536.00 1624.00 1064.00 832.00 712.00 616.00 528.00 320.00 144.00 32.00 0.00 0.00 0.00 +9079.00 8689.00 8121.00 7353.00 6057.00 4969.00 4000.00 3456.00 3080.00 2864.00 2800.00 2800.00 2008.00 1512.00 1392.00 1200.00 928.00 616.00 440.00 272.00 136.00 40.00 0.00 0.00 0.00 +9079.00 8961.00 8577.00 7841.00 6721.00 5553.00 4352.00 3560.00 3096.00 2872.00 2688.00 2464.00 1360.00 1128.00 1120.00 1072.00 976.00 824.00 640.00 392.00 200.00 64.00 0.00 0.00 0.00 +9079.00 9089.00 8817.00 8161.00 7185.00 5953.00 4576.00 3664.00 3208.00 2992.00 2792.00 2472.00 1352.00 1048.00 1024.00 944.00 856.00 720.00 608.00 424.00 200.00 56.00 0.00 0.00 0.00 +9079.00 9297.00 9121.00 8609.00 7737.00 6641.00 5129.00 3840.00 3344.00 3168.00 2976.00 2544.00 1368.00 1032.00 936.00 808.00 712.00 608.00 472.00 328.00 176.00 48.00 0.00 0.00 0.00 +9079.00 9466.00 9401.00 8977.00 8161.00 7297.00 5513.00 3968.00 3448.00 3240.00 2848.00 2080.00 1224.00 1016.00 872.00 736.00 600.00 448.00 336.00 208.00 104.00 32.00 0.00 0.00 0.00 +9079.00 9666.00 9706.00 9313.00 8473.00 7369.00 5313.00 2864.00 1744.00 1352.00 1344.00 1264.00 1016.00 856.00 752.00 584.00 472.00 320.00 224.00 128.00 64.00 16.00 0.00 0.00 0.00 +9079.00 9874.00 10034.00 9642.00 8233.00 5769.00 3600.00 2352.00 1592.00 1256.00 1208.00 1240.00 904.00 696.00 536.00 384.00 272.00 168.00 104.00 56.00 24.00 0.00 0.00 0.00 0.00 +9079.00 10002.00 10210.00 9225.00 6513.00 4504.00 3880.00 3464.00 2496.00 1456.00 984.00 792.00 504.00 360.00 272.00 200.00 160.00 104.00 80.00 32.00 16.00 0.00 0.00 0.00 0.00 +9079.00 10162.00 10338.00 8361.00 5553.00 4721.00 4440.00 4256.00 3624.00 2056.00 768.00 456.00 280.00 200.00 160.00 120.00 96.00 56.00 40.00 16.00 8.00 0.00 0.00 0.00 0.00 +9079.00 10242.00 10226.00 7433.00 5529.00 5033.00 4897.00 4560.00 3376.00 1536.00 568.00 312.00 200.00 152.00 120.00 88.00 80.00 40.00 32.00 16.00 8.00 0.00 0.00 0.00 0.00 +9079.00 10330.00 9938.00 6873.00 5721.00 5409.00 5177.00 4352.00 2552.00 1032.00 544.00 360.00 248.00 176.00 136.00 96.00 72.00 40.00 32.00 24.00 8.00 0.00 0.00 0.00 0.00 +9079.00 10234.00 9794.00 6873.00 5913.00 5641.00 5273.00 4016.00 2120.00 1120.00 736.00 552.00 384.00 280.00 184.00 112.00 80.00 48.00 40.00 24.00 8.00 0.00 0.00 0.00 0.00 +9079.00 10242.00 9794.00 6825.00 5985.00 5777.00 5313.00 4000.00 2104.00 1160.00 840.00 640.00 456.00 320.00 232.00 152.00 104.00 56.00 48.00 24.00 16.00 0.00 0.00 0.00 0.00 +9079.00 10298.00 9666.00 6905.00 6097.00 5849.00 5401.00 4152.00 2128.00 1248.00 904.00 720.00 488.00 352.00 248.00 168.00 120.00 64.00 48.00 32.00 24.00 8.00 0.00 0.00 0.00 diff --git a/RenderPipelineFile/data/ies_profiles/umbrella.ies b/RenderPipelineFile/data/ies_profiles/umbrella.ies index f3fa89cd..2f5fa5b3 100644 --- a/RenderPipelineFile/data/ies_profiles/umbrella.ies +++ b/RenderPipelineFile/data/ies_profiles/umbrella.ies @@ -1,24 +1,24 @@ -IESNA:LM-63-1995 -[TEST]BALLABS TEST NO. 12447.0 -[MANUFAC] INFINITY LIGHTING INC - CARTHAGE, MISSOURI -[LUMINAIRE] 1/100W ICETRON INDUCTION LAMP 20x15"AREALIGHT LUMINAIRE -[LUMINAIRE] MIRO4 HIGHLY SPEC FLOOD OPTICS w/CLEAR GLASS LENS -[LUMINAIRE] SYLVANIA BALLAST #QT1x150 ICE/UNV-T WATTS=145 -[LUMCAT] ICE DC-150W/41K-FL/CGI-MTV @277VOLTS -[LAMPCAT] ICETRON100/QT150 -TILT=NONE - 1 11000. 1.000000 19 5 1 1 .938 1.266 .000 - 1.0000 1.0000 145.0000 - .0 5.0 10.0 15.0 20.0 25.0 30.0 35.0 40.0 45.0 - 50.0 55.0 60.0 65.0 70.0 75.0 80.0 85.0 90.0 - .0 22.5 45.0 67.5 90.0 - 5122. 5161. 5246. 5337. 5360. 5211. 4910. 4510. 3890. 3056. - 1952. 1153. 451. 139. 30. 15. 3. 0. 0. - 5122. 5138. 5234. 5291. 5289. 5129. 4754. 4318. 3676. 2838. - 2069. 1240. 482. 172. 37. 11. 6. 1. 0. - 5122. 5134. 5160. 5118. 4967. 4781. 4477. 3831. 2940. 1851. - 1139. 685. 306. 85. 25. 13. 7. 1. 0. - 5122. 5119. 5076. 4880. 4697. 4385. 3786. 2876. 1738. 1121. - 711. 388. 120. 30. 11. 6. 4. 1. 0. - 5122. 5113. 5032. 4843. 4615. 4130. 3464. 2374. 1519. 1017. - 589. 293. 73. 27. 11. 6. 3. 1. 0. +IESNA:LM-63-1995 +[TEST]BALLABS TEST NO. 12447.0 +[MANUFAC] INFINITY LIGHTING INC - CARTHAGE, MISSOURI +[LUMINAIRE] 1/100W ICETRON INDUCTION LAMP 20x15"AREALIGHT LUMINAIRE +[LUMINAIRE] MIRO4 HIGHLY SPEC FLOOD OPTICS w/CLEAR GLASS LENS +[LUMINAIRE] SYLVANIA BALLAST #QT1x150 ICE/UNV-T WATTS=145 +[LUMCAT] ICE DC-150W/41K-FL/CGI-MTV @277VOLTS +[LAMPCAT] ICETRON100/QT150 +TILT=NONE + 1 11000. 1.000000 19 5 1 1 .938 1.266 .000 + 1.0000 1.0000 145.0000 + .0 5.0 10.0 15.0 20.0 25.0 30.0 35.0 40.0 45.0 + 50.0 55.0 60.0 65.0 70.0 75.0 80.0 85.0 90.0 + .0 22.5 45.0 67.5 90.0 + 5122. 5161. 5246. 5337. 5360. 5211. 4910. 4510. 3890. 3056. + 1952. 1153. 451. 139. 30. 15. 3. 0. 0. + 5122. 5138. 5234. 5291. 5289. 5129. 4754. 4318. 3676. 2838. + 2069. 1240. 482. 172. 37. 11. 6. 1. 0. + 5122. 5134. 5160. 5118. 4967. 4781. 4477. 3831. 2940. 1851. + 1139. 685. 306. 85. 25. 13. 7. 1. 0. + 5122. 5119. 5076. 4880. 4697. 4385. 3786. 2876. 1738. 1121. + 711. 388. 120. 30. 11. 6. 4. 1. 0. + 5122. 5113. 5032. 4843. 4615. 4130. 3464. 2374. 1519. 1017. + 589. 293. 73. 27. 11. 6. 3. 1. 0. diff --git a/RenderPipelineFile/data/ies_profiles/vee.ies b/RenderPipelineFile/data/ies_profiles/vee.ies index 6786cab0..378c4833 100644 --- a/RenderPipelineFile/data/ies_profiles/vee.ies +++ b/RenderPipelineFile/data/ies_profiles/vee.ies @@ -1,15 +1,15 @@ -ERCO Leuchten GmbH BY: ERCO/LUM650/8701 -73608023+Q100T3/12V/CL 70.IES -- -- -- -- -- -- -TILT=NONE -1 2350 2.35 19 1 1 2 -.09 0 0 -1.00 1.00 100 -0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 -0 -270.9 258 264.8 271.4 288.6 330.7 402.9 109.6 6.511 5.297 3.399 1.846 1.063 -.5676 .4306 .03262 0 0 0 +ERCO Leuchten GmbH BY: ERCO/LUM650/8701 +73608023+Q100T3/12V/CL 70.IES +- +- +- +- +- +- +TILT=NONE +1 2350 2.35 19 1 1 2 -.09 0 0 +1.00 1.00 100 +0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 80 85 90 +0 +270.9 258 264.8 271.4 288.6 330.7 402.9 109.6 6.511 5.297 3.399 1.846 1.063 +.5676 .4306 .03262 0 0 0 diff --git a/RenderPipelineFile/data/ies_profiles/x_arrow.ies b/RenderPipelineFile/data/ies_profiles/x_arrow.ies index 81a5e6e3..b5fcf691 100644 --- a/RenderPipelineFile/data/ies_profiles/x_arrow.ies +++ b/RenderPipelineFile/data/ies_profiles/x_arrow.ies @@ -1,13 +1,13 @@ -IESNA:LM-63-1995 -[TEST] BE1680 -[DATE] 12-FEB-96 -[MANUFAC] BEGA -[LUMCAT] 6340 -[LUMINAIRE] SURFACE MOUNTED WALL LUMINAIRE -[LAMP] (1) 100W A-19 INC -TILT=NONE - 1 1750 1.75 73 1 1 2 -.1 0 .05 - 1 1 100 - 0 2.5 5 7.5 10 12.5 15 17.5 20 22.5 25 27.5 30 32.5 35 37.5 40 42.5 45 47.5 50 52.5 55 57.5 60 62.5 65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 92.5 95 97.5 100 102.5 105 107.5 110 112.5 115 117.5 120 122.5 125 127.5 130 132.5 135 137.5 140 142.5 145 147.5 150 152.5 155 157.5 160 162.5 165 167.5 170 172.5 175 177.5 180 - 0 - 167.3 168.9 173 179.9 179.2 151.2 119.4 95.63 81.03 71.95 66.46 62.67 60.25 57.67 52.18 46.62 48.91 63.15 83.15 95.41 97.4 87.75 62.6 43.08 39.26 47.36 52.89 45.74 31.45 18.17 10.5 7.888 8.112 7.592 3.665 .6467 .498 .4252 .3735 .3185 .2765 .2279 .2199 .2021 .1746 .1407 .1358 .1277 .1326 .1342 .1406 .1374 .1358 .1375 .1488 .1488 .1536 .1925 .2183 .2328 .2345 .249 .2765 .3023 .3266 .3476 .3557 .3638 .3541 .3573 .3638 .3719 .3816 +IESNA:LM-63-1995 +[TEST] BE1680 +[DATE] 12-FEB-96 +[MANUFAC] BEGA +[LUMCAT] 6340 +[LUMINAIRE] SURFACE MOUNTED WALL LUMINAIRE +[LAMP] (1) 100W A-19 INC +TILT=NONE + 1 1750 1.75 73 1 1 2 -.1 0 .05 + 1 1 100 + 0 2.5 5 7.5 10 12.5 15 17.5 20 22.5 25 27.5 30 32.5 35 37.5 40 42.5 45 47.5 50 52.5 55 57.5 60 62.5 65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 92.5 95 97.5 100 102.5 105 107.5 110 112.5 115 117.5 120 122.5 125 127.5 130 132.5 135 137.5 140 142.5 145 147.5 150 152.5 155 157.5 160 162.5 165 167.5 170 172.5 175 177.5 180 + 0 + 167.3 168.9 173 179.9 179.2 151.2 119.4 95.63 81.03 71.95 66.46 62.67 60.25 57.67 52.18 46.62 48.91 63.15 83.15 95.41 97.4 87.75 62.6 43.08 39.26 47.36 52.89 45.74 31.45 18.17 10.5 7.888 8.112 7.592 3.665 .6467 .498 .4252 .3735 .3185 .2765 .2279 .2199 .2021 .1746 .1407 .1358 .1277 .1326 .1342 .1406 .1374 .1358 .1375 .1488 .1488 .1536 .1925 .2183 .2328 .2345 .249 .2765 .3023 .3266 .3476 .3557 .3638 .3541 .3573 .3638 .3719 .3816 diff --git a/RenderPipelineFile/data/ies_profiles/x_arrow_diffuse.ies b/RenderPipelineFile/data/ies_profiles/x_arrow_diffuse.ies index 77a91bb7..84b4a845 100644 --- a/RenderPipelineFile/data/ies_profiles/x_arrow_diffuse.ies +++ b/RenderPipelineFile/data/ies_profiles/x_arrow_diffuse.ies @@ -1,13 +1,13 @@ -IESNA:LM-63-1995 -[TEST] BE1396 -[DATE] 22-SEPT-96 -[MANUFAC] BEGA -[LUMCAT] 6690 -[LUMINAIRE] SURFACE MOUNTED WALL LUMINAIRE -[LAMP] (1) 100W A-19 INC -TILT=NONE - 1 1750 1.75 37 1 1 2 -.12 0 0 - 1 1 100 - 0 2.5 5 7.5 10 12.5 15 17.5 20 22.5 25 27.5 30 32.5 35 37.5 40 42.5 45 47.5 50 52.5 55 57.5 60 62.5 65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 - 0 - 283.4 280.7 270 250.8 228 207.8 185.8 160.1 140.4 133 131.4 131.4 130.9 127.9 120.7 109.3 99.46 95.13 98.4 106.8 116 122.2 124.1 119.1 97.99 57.94 26.92 11.71 4.582 1.544 .9573 .6497 .4341 .2616 .1724 .04603 .005727 +IESNA:LM-63-1995 +[TEST] BE1396 +[DATE] 22-SEPT-96 +[MANUFAC] BEGA +[LUMCAT] 6690 +[LUMINAIRE] SURFACE MOUNTED WALL LUMINAIRE +[LAMP] (1) 100W A-19 INC +TILT=NONE + 1 1750 1.75 37 1 1 2 -.12 0 0 + 1 1 100 + 0 2.5 5 7.5 10 12.5 15 17.5 20 22.5 25 27.5 30 32.5 35 37.5 40 42.5 45 47.5 50 52.5 55 57.5 60 62.5 65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 + 0 + 283.4 280.7 270 250.8 228 207.8 185.8 160.1 140.4 133 131.4 131.4 130.9 127.9 120.7 109.3 99.46 95.13 98.4 106.8 116 122.2 124.1 119.1 97.99 57.94 26.92 11.71 4.582 1.544 .9573 .6497 .4341 .2616 .1724 .04603 .005727 diff --git a/RenderPipelineFile/data/ies_profiles/x_arrow_soft.ies b/RenderPipelineFile/data/ies_profiles/x_arrow_soft.ies index c3905c00..13d282e6 100644 --- a/RenderPipelineFile/data/ies_profiles/x_arrow_soft.ies +++ b/RenderPipelineFile/data/ies_profiles/x_arrow_soft.ies @@ -1,13 +1,13 @@ -IESNA:LM-63-1995 -[TEST] BE1667 -[DATE] 12-FEB-96 -[MANUFAC] BEGA -[LUMCAT] 6339 -[LUMINAIRE] SURFACE MOUNTED WALL LUMINAIRE -[LAMP] (1) 60W A-19 INC -TILT=NONE - 1 890 .89 73 1 1 2 -.1 0 0 - 1 1 60 - 0 2.5 5 7.5 10 12.5 15 17.5 20 22.5 25 27.5 30 32.5 35 37.5 40 42.5 45 47.5 50 52.5 55 57.5 60 62.5 65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 92.5 95 97.5 100 102.5 105 107.5 110 112.5 115 117.5 120 122.5 125 127.5 130 132.5 135 137.5 140 142.5 145 147.5 150 152.5 155 157.5 160 162.5 165 167.5 170 172.5 175 177.5 180 - 0 - 178.4 176.7 170.8 160.9 147 133 114.6 96.13 80.57 67.13 56.54 47.61 40.87 36.97 36.86 39.76 45.47 54.35 66.39 75.84 79.96 73.53 62.87 52.35 44.33 39.14 36.31 33.87 29.1 22.59 15.67 9.13 5.317 2.654 1.382 .3416 .2 .09695 .2025 .175 .1518 .1455 .1328 .1307 .1139 .1097 .097 .1075 .1076 .07175 .1243 .1073 .1328 .1349 .1391 .1307 .1412 .1475 .156 .1813 .1876 .2087 .2466 .2466 .2782 .2993 .3035 .3035 .3035 .2993 .2993 .3035 .3035 +IESNA:LM-63-1995 +[TEST] BE1667 +[DATE] 12-FEB-96 +[MANUFAC] BEGA +[LUMCAT] 6339 +[LUMINAIRE] SURFACE MOUNTED WALL LUMINAIRE +[LAMP] (1) 60W A-19 INC +TILT=NONE + 1 890 .89 73 1 1 2 -.1 0 0 + 1 1 60 + 0 2.5 5 7.5 10 12.5 15 17.5 20 22.5 25 27.5 30 32.5 35 37.5 40 42.5 45 47.5 50 52.5 55 57.5 60 62.5 65 67.5 70 72.5 75 77.5 80 82.5 85 87.5 90 92.5 95 97.5 100 102.5 105 107.5 110 112.5 115 117.5 120 122.5 125 127.5 130 132.5 135 137.5 140 142.5 145 147.5 150 152.5 155 157.5 160 162.5 165 167.5 170 172.5 175 177.5 180 + 0 + 178.4 176.7 170.8 160.9 147 133 114.6 96.13 80.57 67.13 56.54 47.61 40.87 36.97 36.86 39.76 45.47 54.35 66.39 75.84 79.96 73.53 62.87 52.35 44.33 39.14 36.31 33.87 29.1 22.59 15.67 9.13 5.317 2.654 1.382 .3416 .2 .09695 .2025 .175 .1518 .1455 .1328 .1307 .1139 .1097 .097 .1075 .1076 .07175 .1243 .1073 .1328 .1349 .1391 .1307 .1412 .1475 .156 .1813 .1876 .2087 .2466 .2466 .2782 .2993 .3035 .3035 .3035 .2993 .2993 .3035 .3035 diff --git a/RenderPipelineFile/data/panda3d_patches/README.md b/RenderPipelineFile/data/panda3d_patches/README.md index db5a669a..e5b8384e 100644 --- a/RenderPipelineFile/data/panda3d_patches/README.md +++ b/RenderPipelineFile/data/panda3d_patches/README.md @@ -1,16 +1,16 @@ - -## Patches for Panda3D - -This folder contains patches for the Panda3D Engine which have not yet made -it to the Panda3D-Repository, but are used by some render pipeline features. - -**IMPORTANT**: After you applied a patch, delete it, so the render pipeline -knows the patch is available. - -## List of patches - -#### prev-model-view-matrix.diff - -This patch adds the p3d_PrevModelViewMatrix to add support for per object -velocities. When this patch is enabled, moving objects recieve motion blur (per -object motion blur). Also don't forget to apply the second part of the patch, too. + +## Patches for Panda3D + +This folder contains patches for the Panda3D Engine which have not yet made +it to the Panda3D-Repository, but are used by some render pipeline features. + +**IMPORTANT**: After you applied a patch, delete it, so the render pipeline +knows the patch is available. + +## List of patches + +#### prev-model-view-matrix.diff + +This patch adds the p3d_PrevModelViewMatrix to add support for per object +velocities. When this patch is enabled, moving objects recieve motion blur (per +object motion blur). Also don't forget to apply the second part of the patch, too. diff --git a/RenderPipelineFile/data/panda3d_patches/prev-model-view-matrix-part2.diff b/RenderPipelineFile/data/panda3d_patches/prev-model-view-matrix-part2.diff index 3472914d..d07e44ff 100644 --- a/RenderPipelineFile/data/panda3d_patches/prev-model-view-matrix-part2.diff +++ b/RenderPipelineFile/data/panda3d_patches/prev-model-view-matrix-part2.diff @@ -1,609 +1,609 @@ -From 098218a265d582bec22c0398ca9901fa770235ae Mon Sep 17 00:00:00 2001 -From: tobspr -Date: Thu, 3 Mar 2016 10:10:24 +0100 -Subject: [PATCH] Add PrevModelMatrix - Part2 - ---- - panda/src/collide/collisionVisualizer.cxx | 4 ++-- - panda/src/display/callbackGraphicsWindow.cxx | 2 +- - panda/src/display/graphicsEngine.cxx | 4 ---- - panda/src/display/graphicsStateGuardian.I | 4 ++-- - panda/src/display/graphicsStateGuardian.cxx | 12 ++++++------ - panda/src/display/graphicsStateGuardian.h | 8 ++++---- - panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx | 4 ++-- - panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 6 +++--- - panda/src/glstuff/glShaderContext_src.cxx | 6 +++--- - panda/src/gobj/shader.cxx | 4 ++-- - panda/src/gobj/shader.h | 4 ++-- - panda/src/parametrics/ropeNode.cxx | 8 ++++---- - panda/src/parametrics/sheetNode.cxx | 2 +- - panda/src/pgraph/cullTraverserData.I | 8 -------- - panda/src/pgraph/cullTraverserData.cxx | 4 ---- - panda/src/pgraph/cullTraverserData.h | 1 - - panda/src/pgraph/cullableObject.I | 14 +++++++------- - panda/src/pgraph/cullableObject.h | 4 ++-- - panda/src/pgraph/geomNode.cxx | 2 +- - panda/src/pgraph/occluderNode.cxx | 4 ++-- - panda/src/pgraph/planeNode.cxx | 2 +- - panda/src/pgraph/sceneSetup.I | 18 ------------------ - panda/src/pgraph/sceneSetup.h | 3 --- - panda/src/pgraphnodes/callbackNode.cxx | 2 +- - panda/src/pgraphnodes/computeNode.cxx | 2 +- - panda/src/pgraphnodes/nodeCullCallbackData.cxx | 2 +- - panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx | 4 ++-- - 27 files changed, 50 insertions(+), 88 deletions(-) - -diff --git a/panda/src/collide/collisionVisualizer.cxx b/panda/src/collide/collisionVisualizer.cxx -index cceb6c5..da34f7f 100644 ---- a/panda/src/collide/collisionVisualizer.cxx -+++ b/panda/src/collide/collisionVisualizer.cxx -@@ -189,7 +189,7 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { - CullableObject *object = - new CullableObject(geom, point_state, - xform_data.get_internal_transform(trav), -- xform_data.get_prev_internal_transform(trav)); -+ xform_data.get_prev_net_transform(trav)); - - trav->get_cull_handler()->record_object(object, trav); - } -@@ -219,7 +219,7 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { - CullableObject *object = - new CullableObject(geom, empty_state, - xform_data.get_internal_transform(trav), -- xform_data.get_prev_internal_transform(trav)); -+ xform_data.get_prev_net_transform(trav)); - - trav->get_cull_handler()->record_object(object, trav); - } -diff --git a/panda/src/display/callbackGraphicsWindow.cxx b/panda/src/display/callbackGraphicsWindow.cxx -index ebd5b37..69dd9fb 100644 ---- a/panda/src/display/callbackGraphicsWindow.cxx -+++ b/panda/src/display/callbackGraphicsWindow.cxx -@@ -109,7 +109,7 @@ end_frame(FrameMode mode, Thread *current_thread) { - // In case the callback or the application hosting the OpenGL context - // wants to do more rendering, let's give it a blank slate. - _gsg->set_state_and_transform(RenderState::make_empty(), _gsg->get_internal_transform(), -- _gsg->get_prev_internal_transform()); -+ _gsg->get_prev_net_transform()); - _gsg->clear_before_callback(); - - RenderCallbackData data(this, RCT_end_frame, mode); -diff --git a/panda/src/display/graphicsEngine.cxx b/panda/src/display/graphicsEngine.cxx -index 5578121..42ae88c 100644 ---- a/panda/src/display/graphicsEngine.cxx -+++ b/panda/src/display/graphicsEngine.cxx -@@ -1806,7 +1806,6 @@ do_setup_scene(GraphicsStateGuardian *gsg, DisplayRegionPipelineReader *dr) { - NodePath scene_parent = scene_root.get_parent(current_thread); - CPT(TransformState) camera_transform = camera.get_transform(scene_parent, current_thread); - CPT(TransformState) world_transform = scene_parent.get_transform(camera, current_thread); -- CPT(TransformState) prev_world_transform = scene_parent.get_prev_transform(camera, current_thread); - - if (camera_transform->is_invalid()) { - // There must be a singular transform over the scene. -@@ -1859,9 +1858,6 @@ do_setup_scene(GraphicsStateGuardian *gsg, DisplayRegionPipelineReader *dr) { - CPT(TransformState) cs_world_transform = cs_transform->compose(world_transform); - scene_setup->set_cs_world_transform(cs_world_transform); - -- CPT(TransformState) prev_cs_world_transform = cs_transform->compose(prev_world_transform); -- scene_setup->set_prev_cs_world_transform(prev_cs_world_transform); -- - // Make sure that the GSG has a ShaderGenerator for the munger to use. We - // have to do this here because the ShaderGenerator needs a host window - // pointer. Hopefully we'll be able to eliminate that requirement in the -diff --git a/panda/src/display/graphicsStateGuardian.I b/panda/src/display/graphicsStateGuardian.I -index d330100..9fa45c6 100644 ---- a/panda/src/display/graphicsStateGuardian.I -+++ b/panda/src/display/graphicsStateGuardian.I -@@ -852,8 +852,8 @@ get_internal_transform() const { - * function is typically only meaningful during the geometry rendering process. - */ - INLINE CPT(TransformState) GraphicsStateGuardian:: --get_prev_internal_transform() const { -- return _prev_internal_transform; -+get_prev_net_transform() const { -+ return _prev_net_transform; - } - - /** -diff --git a/panda/src/display/graphicsStateGuardian.cxx b/panda/src/display/graphicsStateGuardian.cxx -index 8d3fbae..8663288 100644 ---- a/panda/src/display/graphicsStateGuardian.cxx -+++ b/panda/src/display/graphicsStateGuardian.cxx -@@ -146,7 +146,7 @@ GraphicsStateGuardian(CoordinateSystem internal_coordinate_system, - { - _coordinate_system = CS_invalid; - _internal_transform = TransformState::make_identity(); -- _prev_internal_transform = _prev_internal_transform; -+ _prev_net_transform = _prev_net_transform; - - set_coordinate_system(get_default_coordinate_system()); - -@@ -1208,9 +1208,6 @@ fetch_specified_part(Shader::ShaderMatInput part, InternalName *name, - case Shader::SMO_model_to_apiview: { - return &(get_internal_transform()->get_mat()); - } -- case Shader::SMO_prev_model_to_apiview: { -- return &(get_prev_internal_transform()->get_mat()); -- } - case Shader::SMO_view_to_model: { - t = get_external_transform()->get_inverse()->get_mat(); - return &t; -@@ -1219,8 +1216,11 @@ fetch_specified_part(Shader::ShaderMatInput part, InternalName *name, - t = get_internal_transform()->get_inverse()->get_mat(); - return &t; - } -- case Shader::SMO_prev_apiview_to_model: { -- t = get_prev_internal_transform()->get_inverse()->get_mat(); -+ case Shader::SMO_prev_net_transform: { -+ return &(get_prev_net_transform()->get_mat()); -+ } -+ case Shader::SMO_prev_inv_net_transform: { -+ t = get_prev_net_transform()->get_inverse()->get_mat(); - return &t; - } - case Shader::SMO_apiview_to_view: { -diff --git a/panda/src/display/graphicsStateGuardian.h b/panda/src/display/graphicsStateGuardian.h -index a1cc245..7233075 100644 ---- a/panda/src/display/graphicsStateGuardian.h -+++ b/panda/src/display/graphicsStateGuardian.h -@@ -389,7 +389,7 @@ public: - - INLINE CPT(TransformState) get_external_transform() const; - INLINE CPT(TransformState) get_internal_transform() const; -- INLINE CPT(TransformState) get_prev_internal_transform() const; -+ INLINE CPT(TransformState) get_prev_net_transform() const; - - RenderBuffer get_render_buffer(int buffer_type, const FrameBufferProperties &prop); - -@@ -479,9 +479,9 @@ protected: - // The current transform, as of the last call to set_state_and_transform(). - CPT(TransformState) _internal_transform; - -- // The previous transform, as of the last call to the 3 parameter overload of -- // set_state_and_transform() -- CPT(TransformState) _prev_internal_transform; -+ // The previous net transform, as of the last call to the 3 parameter overload -+ // of set_state_and_transform() -+ CPT(TransformState) _prev_net_transform; - - // The current TextureAttrib is a special case; we may further restrict it - // (according to graphics cards limits) or extend it (according to -diff --git a/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx b/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx -index 66d007d..7609cae 100644 ---- a/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx -+++ b/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx -@@ -758,7 +758,7 @@ clear(DrawableRegion *clearable) { - return; - } - -- set_state_and_transform(RenderState::make_empty(), _internal_transform, _prev_internal_transform); -+ set_state_and_transform(RenderState::make_empty(), _internal_transform, _prev_net_transform); - - D3DCOLOR color_clear_value = LColor_to_D3DCOLOR(clearable->get_clear_color()); - PN_stdfloat depth_clear_value = clearable->get_clear_depth(); -@@ -3037,7 +3037,7 @@ set_state_and_transform(const RenderState *target, - #endif - _state_pcollector.add_level(1); - PStatTimer timer1(_draw_set_state_pcollector); -- _prev_internal_transform = prev_transform; -+ _prev_net_transform = prev_transform; - - if (transform != _internal_transform) { - // PStatTimer timer(_draw_set_state_transform_pcollector); -diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx -index 3dca96f..b7ca084 100644 ---- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx -+++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx -@@ -2875,7 +2875,7 @@ clear(DrawableRegion *clearable) { - // XXX rdb: Is this line really necessary? Could we perhaps just reset the - // color write mask and other relevant attributes? - set_state_and_transform(RenderState::make_empty(), _internal_transform, -- _prev_internal_transform); -+ _prev_net_transform); - - int mask = 0; - -@@ -5897,7 +5897,7 @@ framebuffer_copy_to_ram(Texture *tex, int view, int z, - // glReadPixels() to work NOTE: reading the depth buffer is *much* slower - // than reading the color buffer - set_state_and_transform(RenderState::make_empty(), _internal_transform, -- _prev_internal_transform); -+ _prev_net_transform); - - int xo, yo, w, h; - dr->get_region_pixels(xo, yo, w, h); -@@ -9522,7 +9522,7 @@ set_state_and_transform(const RenderState *target, - _state_pcollector.add_level(1); - PStatGPUTimer timer1(this, _draw_set_state_pcollector); - -- _prev_internal_transform = prev_transform; -+ _prev_net_transform = prev_transform; - - if (transform != _internal_transform) { - // PStatGPUTimer timer(this, _draw_set_state_transform_pcollector); -diff --git a/panda/src/glstuff/glShaderContext_src.cxx b/panda/src/glstuff/glShaderContext_src.cxx -index a7759af..a7466bb 100644 ---- a/panda/src/glstuff/glShaderContext_src.cxx -+++ b/panda/src/glstuff/glShaderContext_src.cxx -@@ -769,10 +769,10 @@ reflect_uniform(int i, char *name_buffer, GLsizei name_buflen) { - : Shader::SMO_model_to_apiview; - bind._part[1] = Shader::SMO_identity; - -- } else if (matrix_name == "PrevModelViewMatrix") { -+ } else if (matrix_name == "PrevModelMatrix") { - bind._func = Shader::SMF_first; -- bind._part[0] = inverse ? Shader::SMO_prev_apiview_to_model -- : Shader::SMO_prev_model_to_apiview; -+ bind._part[0] = inverse ? Shader::SMO_prev_inv_net_transform -+ : Shader::SMO_prev_net_transform; - bind._part[1] = Shader::SMO_identity; - - } else if (matrix_name == "ProjectionMatrix") { -diff --git a/panda/src/gobj/shader.cxx b/panda/src/gobj/shader.cxx -index 552f665..95bc136a 100644 ---- a/panda/src/gobj/shader.cxx -+++ b/panda/src/gobj/shader.cxx -@@ -390,8 +390,8 @@ cp_dependency(ShaderMatInput inp) { - (inp == SMO_view_to_model) || - (inp == SMO_model_to_apiview) || - (inp == SMO_apiview_to_model) || -- (inp == SMO_prev_model_to_apiview) || -- (inp == SMO_prev_apiview_to_model) || -+ (inp == SMO_prev_net_transform) || -+ (inp == SMO_prev_inv_net_transform) || - (inp == SMO_view_to_world) || - (inp == SMO_world_to_view) || - (inp == SMO_view_x_to_view) || -diff --git a/panda/src/gobj/shader.h b/panda/src/gobj/shader.h -index f517565..e5d244d 100644 ---- a/panda/src/gobj/shader.h -+++ b/panda/src/gobj/shader.h -@@ -202,8 +202,8 @@ public: - SMO_tex_is_alpha_i, - - // Previous frame transform -- SMO_prev_apiview_to_model, -- SMO_prev_model_to_apiview, -+ SMO_prev_net_transform, -+ SMO_prev_inv_net_transform, - - SMO_INVALID - }; -diff --git a/panda/src/parametrics/ropeNode.cxx b/panda/src/parametrics/ropeNode.cxx -index 6705f66..28160b9 100644 ---- a/panda/src/parametrics/ropeNode.cxx -+++ b/panda/src/parametrics/ropeNode.cxx -@@ -335,7 +335,7 @@ render_thread(CullTraverser *trav, CullTraverserData &data, - CullableObject *object = - new CullableObject(geom, state, - data.get_internal_transform(trav), -- data.get_prev_internal_transform(trav)); -+ data.get_prev_net_transform(trav)); - trav->get_cull_handler()->record_object(object, trav); - } - -@@ -382,7 +382,7 @@ render_tape(CullTraverser *trav, CullTraverserData &data, - CullableObject *object = - new CullableObject(geom, state, - data.get_internal_transform(trav), -- data.get_prev_internal_transform(trav)); -+ data.get_prev_net_transform(trav)); - trav->get_cull_handler()->record_object(object, trav); - } - -@@ -436,7 +436,7 @@ render_billboard(CullTraverser *trav, CullTraverserData &data, - CullableObject *object = - new CullableObject(geom, state, - data.get_internal_transform(trav), -- data.get_prev_internal_transform(trav)); -+ data.get_prev_net_transform(trav)); - trav->get_cull_handler()->record_object(object, trav); - } - -@@ -498,7 +498,7 @@ render_tube(CullTraverser *trav, CullTraverserData &data, - CullableObject *object = - new CullableObject(geom, state, - data.get_internal_transform(trav), -- data.get_prev_internal_transform(trav)); -+ data.get_prev_net_transform(trav)); - trav->get_cull_handler()->record_object(object, trav); - } - -diff --git a/panda/src/parametrics/sheetNode.cxx b/panda/src/parametrics/sheetNode.cxx -index f745d6e..b950105 100644 ---- a/panda/src/parametrics/sheetNode.cxx -+++ b/panda/src/parametrics/sheetNode.cxx -@@ -332,7 +332,7 @@ render_sheet(CullTraverser *trav, CullTraverserData &data, - CullableObject *object = - new CullableObject(geom, state, - data.get_internal_transform(trav), -- data.get_prev_internal_transform(trav)); -+ data.get_prev_net_transform(trav)); - trav->get_cull_handler()->record_object(object, trav); - } - -diff --git a/panda/src/pgraph/cullTraverserData.I b/panda/src/pgraph/cullTraverserData.I -index 7c1a79f..3ec4380 100644 ---- a/panda/src/pgraph/cullTraverserData.I -+++ b/panda/src/pgraph/cullTraverserData.I -@@ -139,14 +139,6 @@ INLINE CPT(TransformState) CullTraverserData:: - get_internal_transform(const CullTraverser *trav) const { - return trav->get_scene()->get_cs_world_transform()->compose(_net_transform); - } --/** -- * Returns the previous internal transform: the modelview transform in the GSG's -- * internal coordinate system in the last frame. -- */ --INLINE CPT(TransformState) CullTraverserData:: --get_prev_internal_transform(const CullTraverser *trav) const { -- return trav->get_scene()->get_prev_cs_world_transform()->compose(_prev_net_transform); --} - - /** - * Returns the net transform: the relative transform from root of the scene -diff --git a/panda/src/pgraph/cullTraverserData.cxx b/panda/src/pgraph/cullTraverserData.cxx -index e84a3d8..95b9013 100644 ---- a/panda/src/pgraph/cullTraverserData.cxx -+++ b/panda/src/pgraph/cullTraverserData.cxx -@@ -47,12 +47,8 @@ apply_transform_and_state(CullTraverser *trav) { - _node_reader.compose_draw_mask(_draw_mask); - - apply_transform_and_state(trav, _node_reader.get_transform(), --<<<<<<< HEAD - _node_reader.get_prev_transform(), -- node_state, _node_reader.get_effects(), --======= - MOVE(node_state), _node_reader.get_effects(), -->>>>>>> 2bf886fc5b086e92ef86b25c652f7257521365d7 - _node_reader.get_off_clip_planes()); - } - -diff --git a/panda/src/pgraph/cullTraverserData.h b/panda/src/pgraph/cullTraverserData.h -index 30b8d68..cb1e147 100644 ---- a/panda/src/pgraph/cullTraverserData.h -+++ b/panda/src/pgraph/cullTraverserData.h -@@ -61,7 +61,6 @@ public: - PUBLISHED: - INLINE CPT(TransformState) get_modelview_transform(const CullTraverser *trav) const; - INLINE CPT(TransformState) get_internal_transform(const CullTraverser *trav) const; -- INLINE CPT(TransformState) get_prev_internal_transform(const CullTraverser *trav) const; - INLINE const TransformState *get_net_transform(const CullTraverser *trav) const; - INLINE const TransformState *get_prev_net_transform(const CullTraverser *trav) const; - -diff --git a/panda/src/pgraph/cullableObject.I b/panda/src/pgraph/cullableObject.I -index 5fbe70c..7481f12 100644 ---- a/panda/src/pgraph/cullableObject.I -+++ b/panda/src/pgraph/cullableObject.I -@@ -28,11 +28,11 @@ CullableObject() { - INLINE CullableObject:: - CullableObject(const Geom *geom, const RenderState *state, - const TransformState *internal_transform, -- const TransformState *prev_internal_transform) : -+ const TransformState *prev_net_transform) : - _geom(geom), - _state(state), - _internal_transform(internal_transform), -- _prev_internal_transform(prev_internal_transform) -+ _prev_net_transform(prev_net_transform) - { - #ifdef DO_MEMORY_USAGE - MemoryUsage::update_type(this, get_class_type()); -@@ -49,7 +49,7 @@ CullableObject(const Geom *geom, const RenderState *state, - _geom(geom), - _state(state), - _internal_transform(internal_transform), -- _prev_internal_transform(internal_transform) -+ _prev_net_transform(internal_transform) - { - #ifdef DO_MEMORY_USAGE - MemoryUsage::update_type(this, get_class_type()); -@@ -66,7 +66,7 @@ CullableObject(const CullableObject ©) : - _munged_data(copy._munged_data), - _state(copy._state), - _internal_transform(copy._internal_transform), -- _prev_internal_transform(copy._prev_internal_transform) -+ _prev_net_transform(copy._prev_net_transform) - { - #ifdef DO_MEMORY_USAGE - MemoryUsage::update_type(this, get_class_type()); -@@ -83,7 +83,7 @@ operator = (const CullableObject ©) { - _munged_data = copy._munged_data; - _state = copy._state; - _internal_transform = copy._internal_transform; -- _prev_internal_transform = copy._prev_internal_transform; -+ _prev_net_transform = copy._prev_net_transform; - _draw_callback = copy._draw_callback; - } - -@@ -96,7 +96,7 @@ draw(GraphicsStateGuardianBase *gsg, bool force, Thread *current_thread) { - if (_draw_callback != (CallbackObject *)NULL) { - // It has a callback associated. - gsg->clear_before_callback(); -- gsg->set_state_and_transform(_state, _internal_transform, _prev_internal_transform); -+ gsg->set_state_and_transform(_state, _internal_transform, _prev_net_transform); - GeomDrawCallbackData cbdata(this, gsg, force); - _draw_callback->do_callback(&cbdata); - if (cbdata.get_lost_state()) { -@@ -106,7 +106,7 @@ draw(GraphicsStateGuardianBase *gsg, bool force, Thread *current_thread) { - // Now the callback has taken care of drawing. - } else { - nassertv(_geom != (Geom *)NULL); -- gsg->set_state_and_transform(_state, _internal_transform, _prev_internal_transform); -+ gsg->set_state_and_transform(_state, _internal_transform, _prev_net_transform); - draw_inline(gsg, force, current_thread); - } - } -diff --git a/panda/src/pgraph/cullableObject.h b/panda/src/pgraph/cullableObject.h -index f450cbc..5887ac9 100644 ---- a/panda/src/pgraph/cullableObject.h -+++ b/panda/src/pgraph/cullableObject.h -@@ -51,7 +51,7 @@ public: - - INLINE CullableObject(const Geom *geom, const RenderState *state, - const TransformState *internal_transform, -- const TransformState *prev_internal_transform); -+ const TransformState *prev_net_transform); - - INLINE CullableObject(const CullableObject ©); - INLINE void operator = (const CullableObject ©); -@@ -78,7 +78,7 @@ public: - CPT(GeomVertexData) _munged_data; - CPT(RenderState) _state; - CPT(TransformState) _internal_transform; -- CPT(TransformState) _prev_internal_transform; -+ CPT(TransformState) _prev_net_transform; - PT(CallbackObject) _draw_callback; - - private: -diff --git a/panda/src/pgraph/geomNode.cxx b/panda/src/pgraph/geomNode.cxx -index 5ea321e..1813431 100644 ---- a/panda/src/pgraph/geomNode.cxx -+++ b/panda/src/pgraph/geomNode.cxx -@@ -559,7 +559,7 @@ add_for_draw(CullTraverser *trav, CullTraverserData &data) { - CullableObject *object = - new CullableObject(geom, state, - data.get_internal_transform(trav), -- data.get_prev_internal_transform(trav)); -+ data.get_prev_net_transform(trav)); - trav->get_cull_handler()->record_object(object, trav); - } - } -diff --git a/panda/src/pgraph/occluderNode.cxx b/panda/src/pgraph/occluderNode.cxx -index 83695fb..f41a220 100644 ---- a/panda/src/pgraph/occluderNode.cxx -+++ b/panda/src/pgraph/occluderNode.cxx -@@ -143,7 +143,7 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { - CullableObject *occluder_viz = - new CullableObject(get_occluder_viz(trav, data), get_occluder_viz_state(trav, data), - data.get_internal_transform(trav), -- data.get_prev_internal_transform(trav)); -+ data.get_prev_net_transform(trav)); - trav->get_cull_handler()->record_object(occluder_viz, trav); - - // Also get the frame. -@@ -151,7 +151,7 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { - CullableObject *frame_viz = - new CullableObject(_frame_viz, get_frame_viz_state(trav, data), - data.get_internal_transform(trav), -- data.get_prev_internal_transform(trav)); -+ data.get_prev_net_transform(trav)); - trav->get_cull_handler()->record_object(frame_viz, trav); - - // Now carry on to render our child nodes. -diff --git a/panda/src/pgraph/planeNode.cxx b/panda/src/pgraph/planeNode.cxx -index a643862..cfc098b 100644 ---- a/panda/src/pgraph/planeNode.cxx -+++ b/panda/src/pgraph/planeNode.cxx -@@ -142,7 +142,7 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { - CullableObject *plane_viz = - new CullableObject(get_viz(trav, data), data._state, - data.get_internal_transform(trav), -- data.get_prev_internal_transform(trav)); -+ data.get_prev_net_transform(trav)); - trav->get_cull_handler()->record_object(plane_viz, trav); - - // Now carry on to render our child nodes. -diff --git a/panda/src/pgraph/sceneSetup.I b/panda/src/pgraph/sceneSetup.I -index 1b150ff..4a29d5a 100644 ---- a/panda/src/pgraph/sceneSetup.I -+++ b/panda/src/pgraph/sceneSetup.I -@@ -269,21 +269,3 @@ INLINE const TransformState *SceneSetup:: - get_cs_world_transform() const { - return _cs_world_transform; - } -- --/** -- * Specifies the position from the starting node relative to the camera, in -- * the GSG's internal coordinate system. -- */ --INLINE void SceneSetup:: --set_prev_cs_world_transform(const TransformState *prev_cs_world_transform) { -- _prev_cs_world_transform = prev_cs_world_transform; --} -- --/** -- * Returns the position from the starting node relative to the camera, in the -- * GSG's internal coordinate system. -- */ --INLINE const TransformState *SceneSetup:: --get_prev_cs_world_transform() const { -- return _prev_cs_world_transform; --} -diff --git a/panda/src/pgraph/sceneSetup.h b/panda/src/pgraph/sceneSetup.h -index 72c0599..180f4ba 100644 ---- a/panda/src/pgraph/sceneSetup.h -+++ b/panda/src/pgraph/sceneSetup.h -@@ -74,9 +74,6 @@ PUBLISHED: - INLINE void set_cs_world_transform(const TransformState *cs_world_transform); - INLINE const TransformState *get_cs_world_transform() const; - -- INLINE void set_prev_cs_world_transform(const TransformState *prev_cs_world_transform); -- INLINE const TransformState *get_prev_cs_world_transform() const; -- - private: - DisplayRegion *_display_region; - int _viewport_width; -diff --git a/panda/src/pgraphnodes/callbackNode.cxx b/panda/src/pgraphnodes/callbackNode.cxx -index 2ced441..c6b3d9b 100644 ---- a/panda/src/pgraphnodes/callbackNode.cxx -+++ b/panda/src/pgraphnodes/callbackNode.cxx -@@ -134,7 +134,7 @@ add_for_draw(CullTraverser *trav, CullTraverserData &data) { - CullableObject *object = - new CullableObject(NULL, data._state, - data.get_internal_transform(trav), -- data.get_prev_internal_transform(trav)); -+ data.get_prev_net_transform(trav)); - object->set_draw_callback(cbobj); - trav->get_cull_handler()->record_object(object, trav); - } -diff --git a/panda/src/pgraphnodes/computeNode.cxx b/panda/src/pgraphnodes/computeNode.cxx -index 2f40dd6..b5efe4a 100644 ---- a/panda/src/pgraphnodes/computeNode.cxx -+++ b/panda/src/pgraphnodes/computeNode.cxx -@@ -95,7 +95,7 @@ add_for_draw(CullTraverser *trav, CullTraverserData &data) { - CullableObject *object = - new CullableObject(NULL, data._state, - data.get_internal_transform(trav), -- data.get_prev_internal_transform(trav)); -+ data.get_prev_net_transform(trav)); - object->set_draw_callback(_dispatcher); - trav->get_cull_handler()->record_object(object, trav); - } -diff --git a/panda/src/pgraphnodes/nodeCullCallbackData.cxx b/panda/src/pgraphnodes/nodeCullCallbackData.cxx -index b89ac5f..8542712 100644 ---- a/panda/src/pgraphnodes/nodeCullCallbackData.cxx -+++ b/panda/src/pgraphnodes/nodeCullCallbackData.cxx -@@ -51,7 +51,7 @@ upcall() { - CullableObject *object = - new CullableObject(NULL, _data._state, - _data.get_internal_transform(_trav), -- _data.get_prev_internal_transform(_trav)); -+ _data.get_prev_net_transform(_trav)); - object->set_draw_callback(cbobj); - _trav->get_cull_handler()->record_object(object, _trav); - } -diff --git a/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx b/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx -index ef6fcb7..fdfad22 100644 ---- a/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx -+++ b/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx -@@ -206,7 +206,7 @@ clear(DrawableRegion *clearable) { - return; - } - -- set_state_and_transform(RenderState::make_empty(), _internal_transform, _prev_internal_transform); -+ set_state_and_transform(RenderState::make_empty(), _internal_transform, _prev_net_transform); - - bool clear_color = false; - PIXEL color = 0; -@@ -1496,7 +1496,7 @@ set_state_and_transform(const RenderState *target, - _state_pcollector.add_level(1); - PStatTimer timer1(_draw_set_state_pcollector); - -- _prev_internal_transform = prev_transform; -+ _prev_net_transform = prev_transform; - - if (transform != _internal_transform) { - PStatTimer timer(_draw_set_state_transform_pcollector); --- -1.9.5.msysgit.0 - +From 098218a265d582bec22c0398ca9901fa770235ae Mon Sep 17 00:00:00 2001 +From: tobspr +Date: Thu, 3 Mar 2016 10:10:24 +0100 +Subject: [PATCH] Add PrevModelMatrix - Part2 + +--- + panda/src/collide/collisionVisualizer.cxx | 4 ++-- + panda/src/display/callbackGraphicsWindow.cxx | 2 +- + panda/src/display/graphicsEngine.cxx | 4 ---- + panda/src/display/graphicsStateGuardian.I | 4 ++-- + panda/src/display/graphicsStateGuardian.cxx | 12 ++++++------ + panda/src/display/graphicsStateGuardian.h | 8 ++++---- + panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx | 4 ++-- + panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 6 +++--- + panda/src/glstuff/glShaderContext_src.cxx | 6 +++--- + panda/src/gobj/shader.cxx | 4 ++-- + panda/src/gobj/shader.h | 4 ++-- + panda/src/parametrics/ropeNode.cxx | 8 ++++---- + panda/src/parametrics/sheetNode.cxx | 2 +- + panda/src/pgraph/cullTraverserData.I | 8 -------- + panda/src/pgraph/cullTraverserData.cxx | 4 ---- + panda/src/pgraph/cullTraverserData.h | 1 - + panda/src/pgraph/cullableObject.I | 14 +++++++------- + panda/src/pgraph/cullableObject.h | 4 ++-- + panda/src/pgraph/geomNode.cxx | 2 +- + panda/src/pgraph/occluderNode.cxx | 4 ++-- + panda/src/pgraph/planeNode.cxx | 2 +- + panda/src/pgraph/sceneSetup.I | 18 ------------------ + panda/src/pgraph/sceneSetup.h | 3 --- + panda/src/pgraphnodes/callbackNode.cxx | 2 +- + panda/src/pgraphnodes/computeNode.cxx | 2 +- + panda/src/pgraphnodes/nodeCullCallbackData.cxx | 2 +- + panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx | 4 ++-- + 27 files changed, 50 insertions(+), 88 deletions(-) + +diff --git a/panda/src/collide/collisionVisualizer.cxx b/panda/src/collide/collisionVisualizer.cxx +index cceb6c5..da34f7f 100644 +--- a/panda/src/collide/collisionVisualizer.cxx ++++ b/panda/src/collide/collisionVisualizer.cxx +@@ -189,7 +189,7 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { + CullableObject *object = + new CullableObject(geom, point_state, + xform_data.get_internal_transform(trav), +- xform_data.get_prev_internal_transform(trav)); ++ xform_data.get_prev_net_transform(trav)); + + trav->get_cull_handler()->record_object(object, trav); + } +@@ -219,7 +219,7 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { + CullableObject *object = + new CullableObject(geom, empty_state, + xform_data.get_internal_transform(trav), +- xform_data.get_prev_internal_transform(trav)); ++ xform_data.get_prev_net_transform(trav)); + + trav->get_cull_handler()->record_object(object, trav); + } +diff --git a/panda/src/display/callbackGraphicsWindow.cxx b/panda/src/display/callbackGraphicsWindow.cxx +index ebd5b37..69dd9fb 100644 +--- a/panda/src/display/callbackGraphicsWindow.cxx ++++ b/panda/src/display/callbackGraphicsWindow.cxx +@@ -109,7 +109,7 @@ end_frame(FrameMode mode, Thread *current_thread) { + // In case the callback or the application hosting the OpenGL context + // wants to do more rendering, let's give it a blank slate. + _gsg->set_state_and_transform(RenderState::make_empty(), _gsg->get_internal_transform(), +- _gsg->get_prev_internal_transform()); ++ _gsg->get_prev_net_transform()); + _gsg->clear_before_callback(); + + RenderCallbackData data(this, RCT_end_frame, mode); +diff --git a/panda/src/display/graphicsEngine.cxx b/panda/src/display/graphicsEngine.cxx +index 5578121..42ae88c 100644 +--- a/panda/src/display/graphicsEngine.cxx ++++ b/panda/src/display/graphicsEngine.cxx +@@ -1806,7 +1806,6 @@ do_setup_scene(GraphicsStateGuardian *gsg, DisplayRegionPipelineReader *dr) { + NodePath scene_parent = scene_root.get_parent(current_thread); + CPT(TransformState) camera_transform = camera.get_transform(scene_parent, current_thread); + CPT(TransformState) world_transform = scene_parent.get_transform(camera, current_thread); +- CPT(TransformState) prev_world_transform = scene_parent.get_prev_transform(camera, current_thread); + + if (camera_transform->is_invalid()) { + // There must be a singular transform over the scene. +@@ -1859,9 +1858,6 @@ do_setup_scene(GraphicsStateGuardian *gsg, DisplayRegionPipelineReader *dr) { + CPT(TransformState) cs_world_transform = cs_transform->compose(world_transform); + scene_setup->set_cs_world_transform(cs_world_transform); + +- CPT(TransformState) prev_cs_world_transform = cs_transform->compose(prev_world_transform); +- scene_setup->set_prev_cs_world_transform(prev_cs_world_transform); +- + // Make sure that the GSG has a ShaderGenerator for the munger to use. We + // have to do this here because the ShaderGenerator needs a host window + // pointer. Hopefully we'll be able to eliminate that requirement in the +diff --git a/panda/src/display/graphicsStateGuardian.I b/panda/src/display/graphicsStateGuardian.I +index d330100..9fa45c6 100644 +--- a/panda/src/display/graphicsStateGuardian.I ++++ b/panda/src/display/graphicsStateGuardian.I +@@ -852,8 +852,8 @@ get_internal_transform() const { + * function is typically only meaningful during the geometry rendering process. + */ + INLINE CPT(TransformState) GraphicsStateGuardian:: +-get_prev_internal_transform() const { +- return _prev_internal_transform; ++get_prev_net_transform() const { ++ return _prev_net_transform; + } + + /** +diff --git a/panda/src/display/graphicsStateGuardian.cxx b/panda/src/display/graphicsStateGuardian.cxx +index 8d3fbae..8663288 100644 +--- a/panda/src/display/graphicsStateGuardian.cxx ++++ b/panda/src/display/graphicsStateGuardian.cxx +@@ -146,7 +146,7 @@ GraphicsStateGuardian(CoordinateSystem internal_coordinate_system, + { + _coordinate_system = CS_invalid; + _internal_transform = TransformState::make_identity(); +- _prev_internal_transform = _prev_internal_transform; ++ _prev_net_transform = _prev_net_transform; + + set_coordinate_system(get_default_coordinate_system()); + +@@ -1208,9 +1208,6 @@ fetch_specified_part(Shader::ShaderMatInput part, InternalName *name, + case Shader::SMO_model_to_apiview: { + return &(get_internal_transform()->get_mat()); + } +- case Shader::SMO_prev_model_to_apiview: { +- return &(get_prev_internal_transform()->get_mat()); +- } + case Shader::SMO_view_to_model: { + t = get_external_transform()->get_inverse()->get_mat(); + return &t; +@@ -1219,8 +1216,11 @@ fetch_specified_part(Shader::ShaderMatInput part, InternalName *name, + t = get_internal_transform()->get_inverse()->get_mat(); + return &t; + } +- case Shader::SMO_prev_apiview_to_model: { +- t = get_prev_internal_transform()->get_inverse()->get_mat(); ++ case Shader::SMO_prev_net_transform: { ++ return &(get_prev_net_transform()->get_mat()); ++ } ++ case Shader::SMO_prev_inv_net_transform: { ++ t = get_prev_net_transform()->get_inverse()->get_mat(); + return &t; + } + case Shader::SMO_apiview_to_view: { +diff --git a/panda/src/display/graphicsStateGuardian.h b/panda/src/display/graphicsStateGuardian.h +index a1cc245..7233075 100644 +--- a/panda/src/display/graphicsStateGuardian.h ++++ b/panda/src/display/graphicsStateGuardian.h +@@ -389,7 +389,7 @@ public: + + INLINE CPT(TransformState) get_external_transform() const; + INLINE CPT(TransformState) get_internal_transform() const; +- INLINE CPT(TransformState) get_prev_internal_transform() const; ++ INLINE CPT(TransformState) get_prev_net_transform() const; + + RenderBuffer get_render_buffer(int buffer_type, const FrameBufferProperties &prop); + +@@ -479,9 +479,9 @@ protected: + // The current transform, as of the last call to set_state_and_transform(). + CPT(TransformState) _internal_transform; + +- // The previous transform, as of the last call to the 3 parameter overload of +- // set_state_and_transform() +- CPT(TransformState) _prev_internal_transform; ++ // The previous net transform, as of the last call to the 3 parameter overload ++ // of set_state_and_transform() ++ CPT(TransformState) _prev_net_transform; + + // The current TextureAttrib is a special case; we may further restrict it + // (according to graphics cards limits) or extend it (according to +diff --git a/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx b/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx +index 66d007d..7609cae 100644 +--- a/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx ++++ b/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx +@@ -758,7 +758,7 @@ clear(DrawableRegion *clearable) { + return; + } + +- set_state_and_transform(RenderState::make_empty(), _internal_transform, _prev_internal_transform); ++ set_state_and_transform(RenderState::make_empty(), _internal_transform, _prev_net_transform); + + D3DCOLOR color_clear_value = LColor_to_D3DCOLOR(clearable->get_clear_color()); + PN_stdfloat depth_clear_value = clearable->get_clear_depth(); +@@ -3037,7 +3037,7 @@ set_state_and_transform(const RenderState *target, + #endif + _state_pcollector.add_level(1); + PStatTimer timer1(_draw_set_state_pcollector); +- _prev_internal_transform = prev_transform; ++ _prev_net_transform = prev_transform; + + if (transform != _internal_transform) { + // PStatTimer timer(_draw_set_state_transform_pcollector); +diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +index 3dca96f..b7ca084 100644 +--- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx ++++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +@@ -2875,7 +2875,7 @@ clear(DrawableRegion *clearable) { + // XXX rdb: Is this line really necessary? Could we perhaps just reset the + // color write mask and other relevant attributes? + set_state_and_transform(RenderState::make_empty(), _internal_transform, +- _prev_internal_transform); ++ _prev_net_transform); + + int mask = 0; + +@@ -5897,7 +5897,7 @@ framebuffer_copy_to_ram(Texture *tex, int view, int z, + // glReadPixels() to work NOTE: reading the depth buffer is *much* slower + // than reading the color buffer + set_state_and_transform(RenderState::make_empty(), _internal_transform, +- _prev_internal_transform); ++ _prev_net_transform); + + int xo, yo, w, h; + dr->get_region_pixels(xo, yo, w, h); +@@ -9522,7 +9522,7 @@ set_state_and_transform(const RenderState *target, + _state_pcollector.add_level(1); + PStatGPUTimer timer1(this, _draw_set_state_pcollector); + +- _prev_internal_transform = prev_transform; ++ _prev_net_transform = prev_transform; + + if (transform != _internal_transform) { + // PStatGPUTimer timer(this, _draw_set_state_transform_pcollector); +diff --git a/panda/src/glstuff/glShaderContext_src.cxx b/panda/src/glstuff/glShaderContext_src.cxx +index a7759af..a7466bb 100644 +--- a/panda/src/glstuff/glShaderContext_src.cxx ++++ b/panda/src/glstuff/glShaderContext_src.cxx +@@ -769,10 +769,10 @@ reflect_uniform(int i, char *name_buffer, GLsizei name_buflen) { + : Shader::SMO_model_to_apiview; + bind._part[1] = Shader::SMO_identity; + +- } else if (matrix_name == "PrevModelViewMatrix") { ++ } else if (matrix_name == "PrevModelMatrix") { + bind._func = Shader::SMF_first; +- bind._part[0] = inverse ? Shader::SMO_prev_apiview_to_model +- : Shader::SMO_prev_model_to_apiview; ++ bind._part[0] = inverse ? Shader::SMO_prev_inv_net_transform ++ : Shader::SMO_prev_net_transform; + bind._part[1] = Shader::SMO_identity; + + } else if (matrix_name == "ProjectionMatrix") { +diff --git a/panda/src/gobj/shader.cxx b/panda/src/gobj/shader.cxx +index 552f665..95bc136a 100644 +--- a/panda/src/gobj/shader.cxx ++++ b/panda/src/gobj/shader.cxx +@@ -390,8 +390,8 @@ cp_dependency(ShaderMatInput inp) { + (inp == SMO_view_to_model) || + (inp == SMO_model_to_apiview) || + (inp == SMO_apiview_to_model) || +- (inp == SMO_prev_model_to_apiview) || +- (inp == SMO_prev_apiview_to_model) || ++ (inp == SMO_prev_net_transform) || ++ (inp == SMO_prev_inv_net_transform) || + (inp == SMO_view_to_world) || + (inp == SMO_world_to_view) || + (inp == SMO_view_x_to_view) || +diff --git a/panda/src/gobj/shader.h b/panda/src/gobj/shader.h +index f517565..e5d244d 100644 +--- a/panda/src/gobj/shader.h ++++ b/panda/src/gobj/shader.h +@@ -202,8 +202,8 @@ public: + SMO_tex_is_alpha_i, + + // Previous frame transform +- SMO_prev_apiview_to_model, +- SMO_prev_model_to_apiview, ++ SMO_prev_net_transform, ++ SMO_prev_inv_net_transform, + + SMO_INVALID + }; +diff --git a/panda/src/parametrics/ropeNode.cxx b/panda/src/parametrics/ropeNode.cxx +index 6705f66..28160b9 100644 +--- a/panda/src/parametrics/ropeNode.cxx ++++ b/panda/src/parametrics/ropeNode.cxx +@@ -335,7 +335,7 @@ render_thread(CullTraverser *trav, CullTraverserData &data, + CullableObject *object = + new CullableObject(geom, state, + data.get_internal_transform(trav), +- data.get_prev_internal_transform(trav)); ++ data.get_prev_net_transform(trav)); + trav->get_cull_handler()->record_object(object, trav); + } + +@@ -382,7 +382,7 @@ render_tape(CullTraverser *trav, CullTraverserData &data, + CullableObject *object = + new CullableObject(geom, state, + data.get_internal_transform(trav), +- data.get_prev_internal_transform(trav)); ++ data.get_prev_net_transform(trav)); + trav->get_cull_handler()->record_object(object, trav); + } + +@@ -436,7 +436,7 @@ render_billboard(CullTraverser *trav, CullTraverserData &data, + CullableObject *object = + new CullableObject(geom, state, + data.get_internal_transform(trav), +- data.get_prev_internal_transform(trav)); ++ data.get_prev_net_transform(trav)); + trav->get_cull_handler()->record_object(object, trav); + } + +@@ -498,7 +498,7 @@ render_tube(CullTraverser *trav, CullTraverserData &data, + CullableObject *object = + new CullableObject(geom, state, + data.get_internal_transform(trav), +- data.get_prev_internal_transform(trav)); ++ data.get_prev_net_transform(trav)); + trav->get_cull_handler()->record_object(object, trav); + } + +diff --git a/panda/src/parametrics/sheetNode.cxx b/panda/src/parametrics/sheetNode.cxx +index f745d6e..b950105 100644 +--- a/panda/src/parametrics/sheetNode.cxx ++++ b/panda/src/parametrics/sheetNode.cxx +@@ -332,7 +332,7 @@ render_sheet(CullTraverser *trav, CullTraverserData &data, + CullableObject *object = + new CullableObject(geom, state, + data.get_internal_transform(trav), +- data.get_prev_internal_transform(trav)); ++ data.get_prev_net_transform(trav)); + trav->get_cull_handler()->record_object(object, trav); + } + +diff --git a/panda/src/pgraph/cullTraverserData.I b/panda/src/pgraph/cullTraverserData.I +index 7c1a79f..3ec4380 100644 +--- a/panda/src/pgraph/cullTraverserData.I ++++ b/panda/src/pgraph/cullTraverserData.I +@@ -139,14 +139,6 @@ INLINE CPT(TransformState) CullTraverserData:: + get_internal_transform(const CullTraverser *trav) const { + return trav->get_scene()->get_cs_world_transform()->compose(_net_transform); + } +-/** +- * Returns the previous internal transform: the modelview transform in the GSG's +- * internal coordinate system in the last frame. +- */ +-INLINE CPT(TransformState) CullTraverserData:: +-get_prev_internal_transform(const CullTraverser *trav) const { +- return trav->get_scene()->get_prev_cs_world_transform()->compose(_prev_net_transform); +-} + + /** + * Returns the net transform: the relative transform from root of the scene +diff --git a/panda/src/pgraph/cullTraverserData.cxx b/panda/src/pgraph/cullTraverserData.cxx +index e84a3d8..95b9013 100644 +--- a/panda/src/pgraph/cullTraverserData.cxx ++++ b/panda/src/pgraph/cullTraverserData.cxx +@@ -47,12 +47,8 @@ apply_transform_and_state(CullTraverser *trav) { + _node_reader.compose_draw_mask(_draw_mask); + + apply_transform_and_state(trav, _node_reader.get_transform(), +-<<<<<<< HEAD + _node_reader.get_prev_transform(), +- node_state, _node_reader.get_effects(), +-======= + MOVE(node_state), _node_reader.get_effects(), +->>>>>>> 2bf886fc5b086e92ef86b25c652f7257521365d7 + _node_reader.get_off_clip_planes()); + } + +diff --git a/panda/src/pgraph/cullTraverserData.h b/panda/src/pgraph/cullTraverserData.h +index 30b8d68..cb1e147 100644 +--- a/panda/src/pgraph/cullTraverserData.h ++++ b/panda/src/pgraph/cullTraverserData.h +@@ -61,7 +61,6 @@ public: + PUBLISHED: + INLINE CPT(TransformState) get_modelview_transform(const CullTraverser *trav) const; + INLINE CPT(TransformState) get_internal_transform(const CullTraverser *trav) const; +- INLINE CPT(TransformState) get_prev_internal_transform(const CullTraverser *trav) const; + INLINE const TransformState *get_net_transform(const CullTraverser *trav) const; + INLINE const TransformState *get_prev_net_transform(const CullTraverser *trav) const; + +diff --git a/panda/src/pgraph/cullableObject.I b/panda/src/pgraph/cullableObject.I +index 5fbe70c..7481f12 100644 +--- a/panda/src/pgraph/cullableObject.I ++++ b/panda/src/pgraph/cullableObject.I +@@ -28,11 +28,11 @@ CullableObject() { + INLINE CullableObject:: + CullableObject(const Geom *geom, const RenderState *state, + const TransformState *internal_transform, +- const TransformState *prev_internal_transform) : ++ const TransformState *prev_net_transform) : + _geom(geom), + _state(state), + _internal_transform(internal_transform), +- _prev_internal_transform(prev_internal_transform) ++ _prev_net_transform(prev_net_transform) + { + #ifdef DO_MEMORY_USAGE + MemoryUsage::update_type(this, get_class_type()); +@@ -49,7 +49,7 @@ CullableObject(const Geom *geom, const RenderState *state, + _geom(geom), + _state(state), + _internal_transform(internal_transform), +- _prev_internal_transform(internal_transform) ++ _prev_net_transform(internal_transform) + { + #ifdef DO_MEMORY_USAGE + MemoryUsage::update_type(this, get_class_type()); +@@ -66,7 +66,7 @@ CullableObject(const CullableObject ©) : + _munged_data(copy._munged_data), + _state(copy._state), + _internal_transform(copy._internal_transform), +- _prev_internal_transform(copy._prev_internal_transform) ++ _prev_net_transform(copy._prev_net_transform) + { + #ifdef DO_MEMORY_USAGE + MemoryUsage::update_type(this, get_class_type()); +@@ -83,7 +83,7 @@ operator = (const CullableObject ©) { + _munged_data = copy._munged_data; + _state = copy._state; + _internal_transform = copy._internal_transform; +- _prev_internal_transform = copy._prev_internal_transform; ++ _prev_net_transform = copy._prev_net_transform; + _draw_callback = copy._draw_callback; + } + +@@ -96,7 +96,7 @@ draw(GraphicsStateGuardianBase *gsg, bool force, Thread *current_thread) { + if (_draw_callback != (CallbackObject *)NULL) { + // It has a callback associated. + gsg->clear_before_callback(); +- gsg->set_state_and_transform(_state, _internal_transform, _prev_internal_transform); ++ gsg->set_state_and_transform(_state, _internal_transform, _prev_net_transform); + GeomDrawCallbackData cbdata(this, gsg, force); + _draw_callback->do_callback(&cbdata); + if (cbdata.get_lost_state()) { +@@ -106,7 +106,7 @@ draw(GraphicsStateGuardianBase *gsg, bool force, Thread *current_thread) { + // Now the callback has taken care of drawing. + } else { + nassertv(_geom != (Geom *)NULL); +- gsg->set_state_and_transform(_state, _internal_transform, _prev_internal_transform); ++ gsg->set_state_and_transform(_state, _internal_transform, _prev_net_transform); + draw_inline(gsg, force, current_thread); + } + } +diff --git a/panda/src/pgraph/cullableObject.h b/panda/src/pgraph/cullableObject.h +index f450cbc..5887ac9 100644 +--- a/panda/src/pgraph/cullableObject.h ++++ b/panda/src/pgraph/cullableObject.h +@@ -51,7 +51,7 @@ public: + + INLINE CullableObject(const Geom *geom, const RenderState *state, + const TransformState *internal_transform, +- const TransformState *prev_internal_transform); ++ const TransformState *prev_net_transform); + + INLINE CullableObject(const CullableObject ©); + INLINE void operator = (const CullableObject ©); +@@ -78,7 +78,7 @@ public: + CPT(GeomVertexData) _munged_data; + CPT(RenderState) _state; + CPT(TransformState) _internal_transform; +- CPT(TransformState) _prev_internal_transform; ++ CPT(TransformState) _prev_net_transform; + PT(CallbackObject) _draw_callback; + + private: +diff --git a/panda/src/pgraph/geomNode.cxx b/panda/src/pgraph/geomNode.cxx +index 5ea321e..1813431 100644 +--- a/panda/src/pgraph/geomNode.cxx ++++ b/panda/src/pgraph/geomNode.cxx +@@ -559,7 +559,7 @@ add_for_draw(CullTraverser *trav, CullTraverserData &data) { + CullableObject *object = + new CullableObject(geom, state, + data.get_internal_transform(trav), +- data.get_prev_internal_transform(trav)); ++ data.get_prev_net_transform(trav)); + trav->get_cull_handler()->record_object(object, trav); + } + } +diff --git a/panda/src/pgraph/occluderNode.cxx b/panda/src/pgraph/occluderNode.cxx +index 83695fb..f41a220 100644 +--- a/panda/src/pgraph/occluderNode.cxx ++++ b/panda/src/pgraph/occluderNode.cxx +@@ -143,7 +143,7 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { + CullableObject *occluder_viz = + new CullableObject(get_occluder_viz(trav, data), get_occluder_viz_state(trav, data), + data.get_internal_transform(trav), +- data.get_prev_internal_transform(trav)); ++ data.get_prev_net_transform(trav)); + trav->get_cull_handler()->record_object(occluder_viz, trav); + + // Also get the frame. +@@ -151,7 +151,7 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { + CullableObject *frame_viz = + new CullableObject(_frame_viz, get_frame_viz_state(trav, data), + data.get_internal_transform(trav), +- data.get_prev_internal_transform(trav)); ++ data.get_prev_net_transform(trav)); + trav->get_cull_handler()->record_object(frame_viz, trav); + + // Now carry on to render our child nodes. +diff --git a/panda/src/pgraph/planeNode.cxx b/panda/src/pgraph/planeNode.cxx +index a643862..cfc098b 100644 +--- a/panda/src/pgraph/planeNode.cxx ++++ b/panda/src/pgraph/planeNode.cxx +@@ -142,7 +142,7 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { + CullableObject *plane_viz = + new CullableObject(get_viz(trav, data), data._state, + data.get_internal_transform(trav), +- data.get_prev_internal_transform(trav)); ++ data.get_prev_net_transform(trav)); + trav->get_cull_handler()->record_object(plane_viz, trav); + + // Now carry on to render our child nodes. +diff --git a/panda/src/pgraph/sceneSetup.I b/panda/src/pgraph/sceneSetup.I +index 1b150ff..4a29d5a 100644 +--- a/panda/src/pgraph/sceneSetup.I ++++ b/panda/src/pgraph/sceneSetup.I +@@ -269,21 +269,3 @@ INLINE const TransformState *SceneSetup:: + get_cs_world_transform() const { + return _cs_world_transform; + } +- +-/** +- * Specifies the position from the starting node relative to the camera, in +- * the GSG's internal coordinate system. +- */ +-INLINE void SceneSetup:: +-set_prev_cs_world_transform(const TransformState *prev_cs_world_transform) { +- _prev_cs_world_transform = prev_cs_world_transform; +-} +- +-/** +- * Returns the position from the starting node relative to the camera, in the +- * GSG's internal coordinate system. +- */ +-INLINE const TransformState *SceneSetup:: +-get_prev_cs_world_transform() const { +- return _prev_cs_world_transform; +-} +diff --git a/panda/src/pgraph/sceneSetup.h b/panda/src/pgraph/sceneSetup.h +index 72c0599..180f4ba 100644 +--- a/panda/src/pgraph/sceneSetup.h ++++ b/panda/src/pgraph/sceneSetup.h +@@ -74,9 +74,6 @@ PUBLISHED: + INLINE void set_cs_world_transform(const TransformState *cs_world_transform); + INLINE const TransformState *get_cs_world_transform() const; + +- INLINE void set_prev_cs_world_transform(const TransformState *prev_cs_world_transform); +- INLINE const TransformState *get_prev_cs_world_transform() const; +- + private: + DisplayRegion *_display_region; + int _viewport_width; +diff --git a/panda/src/pgraphnodes/callbackNode.cxx b/panda/src/pgraphnodes/callbackNode.cxx +index 2ced441..c6b3d9b 100644 +--- a/panda/src/pgraphnodes/callbackNode.cxx ++++ b/panda/src/pgraphnodes/callbackNode.cxx +@@ -134,7 +134,7 @@ add_for_draw(CullTraverser *trav, CullTraverserData &data) { + CullableObject *object = + new CullableObject(NULL, data._state, + data.get_internal_transform(trav), +- data.get_prev_internal_transform(trav)); ++ data.get_prev_net_transform(trav)); + object->set_draw_callback(cbobj); + trav->get_cull_handler()->record_object(object, trav); + } +diff --git a/panda/src/pgraphnodes/computeNode.cxx b/panda/src/pgraphnodes/computeNode.cxx +index 2f40dd6..b5efe4a 100644 +--- a/panda/src/pgraphnodes/computeNode.cxx ++++ b/panda/src/pgraphnodes/computeNode.cxx +@@ -95,7 +95,7 @@ add_for_draw(CullTraverser *trav, CullTraverserData &data) { + CullableObject *object = + new CullableObject(NULL, data._state, + data.get_internal_transform(trav), +- data.get_prev_internal_transform(trav)); ++ data.get_prev_net_transform(trav)); + object->set_draw_callback(_dispatcher); + trav->get_cull_handler()->record_object(object, trav); + } +diff --git a/panda/src/pgraphnodes/nodeCullCallbackData.cxx b/panda/src/pgraphnodes/nodeCullCallbackData.cxx +index b89ac5f..8542712 100644 +--- a/panda/src/pgraphnodes/nodeCullCallbackData.cxx ++++ b/panda/src/pgraphnodes/nodeCullCallbackData.cxx +@@ -51,7 +51,7 @@ upcall() { + CullableObject *object = + new CullableObject(NULL, _data._state, + _data.get_internal_transform(_trav), +- _data.get_prev_internal_transform(_trav)); ++ _data.get_prev_net_transform(_trav)); + object->set_draw_callback(cbobj); + _trav->get_cull_handler()->record_object(object, _trav); + } +diff --git a/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx b/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx +index ef6fcb7..fdfad22 100644 +--- a/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx ++++ b/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx +@@ -206,7 +206,7 @@ clear(DrawableRegion *clearable) { + return; + } + +- set_state_and_transform(RenderState::make_empty(), _internal_transform, _prev_internal_transform); ++ set_state_and_transform(RenderState::make_empty(), _internal_transform, _prev_net_transform); + + bool clear_color = false; + PIXEL color = 0; +@@ -1496,7 +1496,7 @@ set_state_and_transform(const RenderState *target, + _state_pcollector.add_level(1); + PStatTimer timer1(_draw_set_state_pcollector); + +- _prev_internal_transform = prev_transform; ++ _prev_net_transform = prev_transform; + + if (transform != _internal_transform) { + PStatTimer timer(_draw_set_state_transform_pcollector); +-- +1.9.5.msysgit.0 + diff --git a/RenderPipelineFile/data/panda3d_patches/prev-model-view-matrix.diff b/RenderPipelineFile/data/panda3d_patches/prev-model-view-matrix.diff index 76b89162..5c8f86db 100644 --- a/RenderPipelineFile/data/panda3d_patches/prev-model-view-matrix.diff +++ b/RenderPipelineFile/data/panda3d_patches/prev-model-view-matrix.diff @@ -1,1452 +1,1452 @@ -From 47c83fce1f4a96954a5db33c6be875e17a4675cc Mon Sep 17 00:00:00 2001 -From: tobspr -Date: Sat, 27 Feb 2016 17:35:53 +0100 -Subject: [PATCH] Support for p3d_PrevModelViewMatrix, populate previous - transform during cull stage - ---- - panda/src/collide/collisionVisualizer.cxx | 7 +- - panda/src/display/callbackGraphicsWindow.cxx | 3 +- - panda/src/display/graphicsEngine.cxx | 4 ++ - panda/src/display/graphicsStateGuardian.I | 9 +++ - panda/src/display/graphicsStateGuardian.cxx | 21 +++++- - panda/src/display/graphicsStateGuardian.h | 9 +++ - panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx | 8 ++- - panda/src/dxgsg9/dxGraphicsStateGuardian9.h | 4 +- - panda/src/glstuff/glCgShaderContext_src.cxx | 8 +++ - panda/src/glstuff/glCgShaderContext_src.h | 2 + - panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 13 ++-- - panda/src/glstuff/glGraphicsStateGuardian_src.h | 3 +- - panda/src/glstuff/glShaderContext_src.cxx | 12 ++++ - panda/src/glstuff/glShaderContext_src.h | 2 + - panda/src/gobj/shader.cxx | 2 + - panda/src/gobj/shader.h | 4 ++ - panda/src/gobj/shaderContext.h | 2 +- - panda/src/gsgbase/graphicsStateGuardianBase.h | 4 ++ - panda/src/parametrics/ropeNode.cxx | 12 ++-- - panda/src/parametrics/sheetNode.cxx | 3 +- - panda/src/pgraph/config_pgraph.cxx | 7 ++ - panda/src/pgraph/config_pgraph.h | 1 + - panda/src/pgraph/cullTraverser.I | 3 + - panda/src/pgraph/cullTraverser.cxx | 10 ++- - panda/src/pgraph/cullTraverserData.I | 22 ++++++ - panda/src/pgraph/cullTraverserData.cxx | 8 +++ - panda/src/pgraph/cullTraverserData.h | 5 ++ - panda/src/pgraph/cullableObject.I | 29 ++++++-- - panda/src/pgraph/cullableObject.h | 5 ++ - panda/src/pgraph/geomNode.cxx | 5 +- - panda/src/pgraph/nodePath.cxx | 66 +++++++++++++----- - panda/src/pgraph/nodePath.h | 81 ++++++++++++++-------- - panda/src/pgraph/occluderNode.cxx | 6 +- - panda/src/pgraph/planeNode.cxx | 3 +- - panda/src/pgraph/portalNode.cxx | 2 + - panda/src/pgraph/sceneSetup.I | 19 +++++ - panda/src/pgraph/sceneSetup.h | 4 ++ - panda/src/pgraphnodes/callbackNode.cxx | 3 +- - panda/src/pgraphnodes/computeNode.cxx | 3 +- - panda/src/pgraphnodes/lodNode.cxx | 2 + - panda/src/pgraphnodes/nodeCullCallbackData.cxx | 3 +- - .../src/tinydisplay/tinyGraphicsStateGuardian.cxx | 7 +- - panda/src/tinydisplay/tinyGraphicsStateGuardian.h | 3 +- - 43 files changed, 349 insertions(+), 80 deletions(-) - -diff --git a/panda/src/collide/collisionVisualizer.cxx b/panda/src/collide/collisionVisualizer.cxx -index 19699b9..cceb6c5 100644 ---- a/panda/src/collide/collisionVisualizer.cxx -+++ b/panda/src/collide/collisionVisualizer.cxx -@@ -112,6 +112,7 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { - xform_data._net_transform = TransformState::make_identity(); - xform_data._view_frustum = trav->get_view_frustum(); - xform_data.apply_transform_and_state(trav, net_transform, -+ TransformState::make_identity(), - RenderState::make_empty(), - RenderEffects::make_empty(), - ClipPlaneAttrib::make()); -@@ -187,7 +188,8 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { - - CullableObject *object = - new CullableObject(geom, point_state, -- xform_data.get_internal_transform(trav)); -+ xform_data.get_internal_transform(trav), -+ xform_data.get_prev_internal_transform(trav)); - - trav->get_cull_handler()->record_object(object, trav); - } -@@ -216,7 +218,8 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { - - CullableObject *object = - new CullableObject(geom, empty_state, -- xform_data.get_internal_transform(trav)); -+ xform_data.get_internal_transform(trav), -+ xform_data.get_prev_internal_transform(trav)); - - trav->get_cull_handler()->record_object(object, trav); - } -diff --git a/panda/src/display/callbackGraphicsWindow.cxx b/panda/src/display/callbackGraphicsWindow.cxx -index f9c2362..ebd5b37 100644 ---- a/panda/src/display/callbackGraphicsWindow.cxx -+++ b/panda/src/display/callbackGraphicsWindow.cxx -@@ -108,7 +108,8 @@ end_frame(FrameMode mode, Thread *current_thread) { - if (_render_callback != NULL) { - // In case the callback or the application hosting the OpenGL context - // wants to do more rendering, let's give it a blank slate. -- _gsg->set_state_and_transform(RenderState::make_empty(), _gsg->get_internal_transform()); -+ _gsg->set_state_and_transform(RenderState::make_empty(), _gsg->get_internal_transform(), -+ _gsg->get_prev_internal_transform()); - _gsg->clear_before_callback(); - - RenderCallbackData data(this, RCT_end_frame, mode); -diff --git a/panda/src/display/graphicsEngine.cxx b/panda/src/display/graphicsEngine.cxx -index 41f6df5..7eed22f 100644 ---- a/panda/src/display/graphicsEngine.cxx -+++ b/panda/src/display/graphicsEngine.cxx -@@ -1805,6 +1805,7 @@ setup_scene(GraphicsStateGuardian *gsg, DisplayRegionPipelineReader *dr) { - NodePath scene_parent = scene_root.get_parent(current_thread); - CPT(TransformState) camera_transform = camera.get_transform(scene_parent, current_thread); - CPT(TransformState) world_transform = scene_parent.get_transform(camera, current_thread); -+ CPT(TransformState) prev_world_transform = scene_parent.get_prev_transform(camera, current_thread); - - if (camera_transform->is_invalid()) { - // There must be a singular transform over the scene. -@@ -1857,6 +1858,9 @@ setup_scene(GraphicsStateGuardian *gsg, DisplayRegionPipelineReader *dr) { - CPT(TransformState) cs_world_transform = cs_transform->compose(world_transform); - scene_setup->set_cs_world_transform(cs_world_transform); - -+ CPT(TransformState) prev_cs_world_transform = cs_transform->compose(prev_world_transform); -+ scene_setup->set_prev_cs_world_transform(prev_cs_world_transform); -+ - // Make sure that the GSG has a ShaderGenerator for the munger to use. We - // have to do this here because the ShaderGenerator needs a host window - // pointer. Hopefully we'll be able to eliminate that requirement in the -diff --git a/panda/src/display/graphicsStateGuardian.I b/panda/src/display/graphicsStateGuardian.I -index db8e1f1..d330100 100644 ---- a/panda/src/display/graphicsStateGuardian.I -+++ b/panda/src/display/graphicsStateGuardian.I -@@ -846,6 +846,15 @@ INLINE CPT(TransformState) GraphicsStateGuardian:: - get_internal_transform() const { - return _internal_transform; - } -+/** -+ * Fetches the previous external net transform. This transform is generally -+ * only set when geometry is about to be rendered. Therefore, this "get" -+ * function is typically only meaningful during the geometry rendering process. -+ */ -+INLINE CPT(TransformState) GraphicsStateGuardian:: -+get_prev_internal_transform() const { -+ return _prev_internal_transform; -+} - - /** - * Returns the current display region being rendered to, as set by the last -diff --git a/panda/src/display/graphicsStateGuardian.cxx b/panda/src/display/graphicsStateGuardian.cxx -index 6f5e873..c0b1cab 100644 ---- a/panda/src/display/graphicsStateGuardian.cxx -+++ b/panda/src/display/graphicsStateGuardian.cxx -@@ -146,6 +146,7 @@ GraphicsStateGuardian(CoordinateSystem internal_coordinate_system, - { - _coordinate_system = CS_invalid; - _internal_transform = TransformState::make_identity(); -+ _prev_internal_transform = _prev_internal_transform; - - set_coordinate_system(get_default_coordinate_system()); - -@@ -1189,6 +1190,9 @@ fetch_specified_part(Shader::ShaderMatInput part, InternalName *name, - case Shader::SMO_model_to_apiview: { - return &(get_internal_transform()->get_mat()); - } -+ case Shader::SMO_prev_model_to_apiview: { -+ return &(get_prev_internal_transform()->get_mat()); -+ } - case Shader::SMO_view_to_model: { - t = get_external_transform()->get_inverse()->get_mat(); - return &t; -@@ -1197,6 +1201,10 @@ fetch_specified_part(Shader::ShaderMatInput part, InternalName *name, - t = get_internal_transform()->get_inverse()->get_mat(); - return &t; - } -+ case Shader::SMO_prev_apiview_to_model: { -+ t = get_prev_internal_transform()->get_inverse()->get_mat(); -+ return &t; -+ } - case Shader::SMO_apiview_to_view: { - return &(_inv_cs_transform->get_mat()); - } -@@ -2308,6 +2316,15 @@ reset() { - } - - /** -+ * @see set_state_and_transform(const RenderState*, const TransformState*, const TransformState*) -+ */ -+void GraphicsStateGuardian:: -+set_state_and_transform(const RenderState *state, -+ const TransformState *trans) { -+ set_state_and_transform(state, trans, trans); -+} -+ -+/** - * Simultaneously resets the render state and the transform state. - * - * This transform specified is the "internal" net transform, already converted -@@ -2321,9 +2338,11 @@ reset() { - */ - void GraphicsStateGuardian:: - set_state_and_transform(const RenderState *state, -- const TransformState *trans) { -+ const TransformState *trans, -+ const TransformState *prev_transform) { - } - -+ - /** - * Clears the framebuffer within the current DisplayRegion, according to the - * flags indicated by the given DrawableRegion object. -diff --git a/panda/src/display/graphicsStateGuardian.h b/panda/src/display/graphicsStateGuardian.h -index 401c538..ad81f17 100644 ---- a/panda/src/display/graphicsStateGuardian.h -+++ b/panda/src/display/graphicsStateGuardian.h -@@ -320,6 +320,10 @@ public: - virtual void set_state_and_transform(const RenderState *state, - const TransformState *transform); - -+ virtual void set_state_and_transform(const RenderState *state, -+ const TransformState *transform, -+ const TransformState *prev_transform); -+ - virtual PN_stdfloat compute_distance_to(const LPoint3 &point) const; - - virtual void clear(DrawableRegion *clearable); -@@ -384,6 +388,7 @@ public: - - INLINE CPT(TransformState) get_external_transform() const; - INLINE CPT(TransformState) get_internal_transform() const; -+ INLINE CPT(TransformState) get_prev_internal_transform() const; - - RenderBuffer get_render_buffer(int buffer_type, const FrameBufferProperties &prop); - -@@ -473,6 +478,10 @@ protected: - // The current transform, as of the last call to set_state_and_transform(). - CPT(TransformState) _internal_transform; - -+ // The previous transform, as of the last call to the 3 parameter overload of -+ // set_state_and_transform() -+ CPT(TransformState) _prev_internal_transform; -+ - // The current TextureAttrib is a special case; we may further restrict it - // (according to graphics cards limits) or extend it (according to - // ColorScaleAttribs in effect) beyond what is specifically requested in the -diff --git a/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx b/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx -index 5cc7d8d..da726f6 100644 ---- a/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx -+++ b/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx -@@ -758,7 +758,7 @@ clear(DrawableRegion *clearable) { - return; - } - -- set_state_and_transform(RenderState::make_empty(), _internal_transform); -+ set_state_and_transform(RenderState::make_empty(), _internal_transform, _prev_internal_transform); - - D3DCOLOR color_clear_value = LColor_to_D3DCOLOR(clearable->get_clear_color()); - PN_stdfloat depth_clear_value = clearable->get_clear_depth(); -@@ -3027,7 +3027,8 @@ do_issue_shade_model() { - */ - void DXGraphicsStateGuardian9:: - set_state_and_transform(const RenderState *target, -- const TransformState *transform) { -+ const TransformState *transform, -+ const TransformState *prev_transform) { - #ifndef NDEBUG - if (gsg_cat.is_spam()) { - gsg_cat.spam() << "Setting GSG state to " << (void *)target << ":\n"; -@@ -3036,6 +3037,7 @@ set_state_and_transform(const RenderState *target, - #endif - _state_pcollector.add_level(1); - PStatTimer timer1(_draw_set_state_pcollector); -+ _prev_internal_transform = prev_transform; - - if (transform != _internal_transform) { - // PStatTimer timer(_draw_set_state_transform_pcollector); -@@ -3044,6 +3046,7 @@ set_state_and_transform(const RenderState *target, - do_issue_transform(); - } - -+ - if (target == _state_rs && (_state_mask | _inv_state_mask).is_all_on()) { - return; - } -@@ -3233,6 +3236,7 @@ set_state_and_transform(const RenderState *target, - _state_rs = _target_rs; - } - -+ - /** - * Called the first time a particular light has been bound to a given id - * within a frame, this should set up the associated hardware light with the -diff --git a/panda/src/dxgsg9/dxGraphicsStateGuardian9.h b/panda/src/dxgsg9/dxGraphicsStateGuardian9.h -index 4a51d21..b6afb00 100644 ---- a/panda/src/dxgsg9/dxGraphicsStateGuardian9.h -+++ b/panda/src/dxgsg9/dxGraphicsStateGuardian9.h -@@ -150,7 +150,9 @@ public: - INLINE static DWORD LColor_to_D3DCOLOR(const LColor &cLColor); - - virtual void set_state_and_transform(const RenderState *state, -- const TransformState *transform); -+ const TransformState *transform, -+ const TransformState *prev_transform); -+ - - bool check_dx_allocation (HRESULT result, int allocation_size, int attempts); - -diff --git a/panda/src/glstuff/glCgShaderContext_src.cxx b/panda/src/glstuff/glCgShaderContext_src.cxx -index c857948..5a0aa4e 100644 ---- a/panda/src/glstuff/glCgShaderContext_src.cxx -+++ b/panda/src/glstuff/glCgShaderContext_src.cxx -@@ -384,6 +384,7 @@ unbind() { - } - - /** -+ * - * This function gets called whenever the RenderState or TransformState has - * changed, but the Shader itself has not changed. It loads new values into - * the shader's parameters. -@@ -391,6 +392,7 @@ unbind() { - void CLP(CgShaderContext):: - set_state_and_transform(const RenderState *target_rs, - const TransformState *modelview_transform, -+ const TransformState *prev_modelview_transform, - const TransformState *projection_transform) { - - if (!valid()) { -@@ -404,6 +406,12 @@ set_state_and_transform(const RenderState *target_rs, - _modelview_transform = modelview_transform; - altered |= Shader::SSD_transform; - } -+ -+ if (_prev_modelview_transform != prev_modelview_transform) { -+ _prev_modelview_transform = prev_modelview_transform; -+ altered |= Shader::SSD_transform; -+ } -+ - if (_projection_transform != projection_transform) { - _projection_transform = projection_transform; - altered |= Shader::SSD_projection; -diff --git a/panda/src/glstuff/glCgShaderContext_src.h b/panda/src/glstuff/glCgShaderContext_src.h -index ae76989..c229444 100644 ---- a/panda/src/glstuff/glCgShaderContext_src.h -+++ b/panda/src/glstuff/glCgShaderContext_src.h -@@ -39,6 +39,7 @@ public: - - void set_state_and_transform(const RenderState *state, - const TransformState *modelview_transform, -+ const TransformState *prev_modelview_transform, - const TransformState *projection_transform); - - void issue_parameters(int altered) OVERRIDE; -@@ -77,6 +78,7 @@ private: - - WCPT(RenderState) _state_rs; - CPT(TransformState) _modelview_transform; -+ CPT(TransformState) _prev_modelview_transform; - CPT(TransformState) _projection_transform; - GLint _frame_number; - -diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx -index 2a9ace7..1fb4fcd 100644 ---- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx -+++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx -@@ -2869,7 +2869,8 @@ clear(DrawableRegion *clearable) { - - // XXX rdb: Is this line really necessary? Could we perhaps just reset the - // color write mask and other relevant attributes? -- set_state_and_transform(RenderState::make_empty(), _internal_transform); -+ set_state_and_transform(RenderState::make_empty(), _internal_transform, -+ _prev_internal_transform); - - int mask = 0; - -@@ -5889,7 +5890,8 @@ framebuffer_copy_to_ram(Texture *tex, int view, int z, - // Bug fix for RE, RE2, and VTX - need to disable texturing in order for - // glReadPixels() to work NOTE: reading the depth buffer is *much* slower - // than reading the color buffer -- set_state_and_transform(RenderState::make_empty(), _internal_transform); -+ set_state_and_transform(RenderState::make_empty(), _internal_transform, -+ _prev_internal_transform); - - int xo, yo, w, h; - dr->get_region_pixels(xo, yo, w, h); -@@ -9486,7 +9488,8 @@ end_bind_clip_planes() { - */ - void CLP(GraphicsStateGuardian):: - set_state_and_transform(const RenderState *target, -- const TransformState *transform) { -+ const TransformState *transform, -+ const TransformState *prev_transform) { - report_my_gl_errors(); - #ifndef NDEBUG - if (gsg_cat.is_spam()) { -@@ -9498,6 +9501,8 @@ set_state_and_transform(const RenderState *target, - _state_pcollector.add_level(1); - PStatGPUTimer timer1(this, _draw_set_state_pcollector); - -+ _prev_internal_transform = prev_transform; -+ - if (transform != _internal_transform) { - // PStatGPUTimer timer(this, _draw_set_state_transform_pcollector); - _transform_state_pcollector.add_level(1); -@@ -9529,7 +9534,7 @@ set_state_and_transform(const RenderState *target, - #endif - - if (_current_shader_context != NULL) { -- _current_shader_context->set_state_and_transform(target, transform, _projection_mat); -+ _current_shader_context->set_state_and_transform(target, transform, prev_transform, _projection_mat); - } - #endif - -diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.h b/panda/src/glstuff/glGraphicsStateGuardian_src.h -index c2909e3..b1267ba 100644 ---- a/panda/src/glstuff/glGraphicsStateGuardian_src.h -+++ b/panda/src/glstuff/glGraphicsStateGuardian_src.h -@@ -375,7 +375,8 @@ public: - INLINE int get_gl_version_minor() const; - - virtual void set_state_and_transform(const RenderState *state, -- const TransformState *transform); -+ const TransformState *transform, -+ const TransformState *prev_transform); - - void bind_fbo(GLuint fbo); - virtual bool get_supports_cg_profile(const string &name) const; -diff --git a/panda/src/glstuff/glShaderContext_src.cxx b/panda/src/glstuff/glShaderContext_src.cxx -index 708347a..a7759af 100644 ---- a/panda/src/glstuff/glShaderContext_src.cxx -+++ b/panda/src/glstuff/glShaderContext_src.cxx -@@ -769,6 +769,12 @@ reflect_uniform(int i, char *name_buffer, GLsizei name_buflen) { - : Shader::SMO_model_to_apiview; - bind._part[1] = Shader::SMO_identity; - -+ } else if (matrix_name == "PrevModelViewMatrix") { -+ bind._func = Shader::SMF_first; -+ bind._part[0] = inverse ? Shader::SMO_prev_apiview_to_model -+ : Shader::SMO_prev_model_to_apiview; -+ bind._part[1] = Shader::SMO_identity; -+ - } else if (matrix_name == "ProjectionMatrix") { - bind._func = Shader::SMF_first; - bind._part[0] = inverse ? Shader::SMO_apiclip_to_apiview -@@ -1789,6 +1795,7 @@ unbind() { - void CLP(ShaderContext):: - set_state_and_transform(const RenderState *target_rs, - const TransformState *modelview_transform, -+ const TransformState *prev_modelview_transform, - const TransformState *projection_transform) { - - // Find out which state properties have changed. -@@ -1798,6 +1805,11 @@ set_state_and_transform(const RenderState *target_rs, - _modelview_transform = modelview_transform; - altered |= Shader::SSD_transform; - } -+ if (_prev_modelview_transform != prev_modelview_transform) { -+ _prev_modelview_transform = prev_modelview_transform; -+ altered |= Shader::SSD_transform; -+ } -+ - if (_projection_transform != projection_transform) { - _projection_transform = projection_transform; - altered |= Shader::SSD_projection; -diff --git a/panda/src/glstuff/glShaderContext_src.h b/panda/src/glstuff/glShaderContext_src.h -index 4209ec6..b260f1c 100644 ---- a/panda/src/glstuff/glShaderContext_src.h -+++ b/panda/src/glstuff/glShaderContext_src.h -@@ -48,6 +48,7 @@ public: - - void set_state_and_transform(const RenderState *state, - const TransformState *modelview_transform, -+ const TransformState *prev_modelview_transform, - const TransformState *projection_transform); - - void issue_parameters(int altered); -@@ -69,6 +70,7 @@ private: - - WCPT(RenderState) _state_rs; - CPT(TransformState) _modelview_transform; -+ CPT(TransformState) _prev_modelview_transform; - CPT(TransformState) _projection_transform; - - /* -diff --git a/panda/src/gobj/shader.cxx b/panda/src/gobj/shader.cxx -index 2905d54..552f665 100644 ---- a/panda/src/gobj/shader.cxx -+++ b/panda/src/gobj/shader.cxx -@@ -390,6 +390,8 @@ cp_dependency(ShaderMatInput inp) { - (inp == SMO_view_to_model) || - (inp == SMO_model_to_apiview) || - (inp == SMO_apiview_to_model) || -+ (inp == SMO_prev_model_to_apiview) || -+ (inp == SMO_prev_apiview_to_model) || - (inp == SMO_view_to_world) || - (inp == SMO_world_to_view) || - (inp == SMO_view_x_to_view) || -diff --git a/panda/src/gobj/shader.h b/panda/src/gobj/shader.h -index dbdde6f..f517565 100644 ---- a/panda/src/gobj/shader.h -+++ b/panda/src/gobj/shader.h -@@ -201,6 +201,10 @@ public: - // Hack for text rendering. Don't use in user shaders. - SMO_tex_is_alpha_i, - -+ // Previous frame transform -+ SMO_prev_apiview_to_model, -+ SMO_prev_model_to_apiview, -+ - SMO_INVALID - }; - -diff --git a/panda/src/gobj/shaderContext.h b/panda/src/gobj/shaderContext.h -index 2b356ce..af2c81d 100644 ---- a/panda/src/gobj/shaderContext.h -+++ b/panda/src/gobj/shaderContext.h -@@ -32,7 +32,7 @@ class EXPCL_PANDA_GOBJ ShaderContext: public SavedContext { - public: - INLINE ShaderContext(Shader *se); - -- INLINE virtual void set_state_and_transform(const RenderState *, const TransformState *, const TransformState*) {}; -+ INLINE virtual void set_state_and_transform(const RenderState *, const TransformState *, const TransformState *, const TransformState *) {}; - - INLINE virtual bool valid() { return false; } - INLINE virtual void bind() {}; -diff --git a/panda/src/gsgbase/graphicsStateGuardianBase.h b/panda/src/gsgbase/graphicsStateGuardianBase.h -index 2fecf30..ace7f38 100644 ---- a/panda/src/gsgbase/graphicsStateGuardianBase.h -+++ b/panda/src/gsgbase/graphicsStateGuardianBase.h -@@ -170,6 +170,10 @@ public: - virtual void set_state_and_transform(const RenderState *state, - const TransformState *transform)=0; - -+ virtual void set_state_and_transform(const RenderState *state, -+ const TransformState *transform, -+ const TransformState *prev_transform)=0; -+ - // This function may only be called during a render traversal; it will - // compute the distance to the indicated point, assumed to be in eye - // coordinates, from the camera plane. This is a virtual function because -diff --git a/panda/src/parametrics/ropeNode.cxx b/panda/src/parametrics/ropeNode.cxx -index c799595..6705f66 100644 ---- a/panda/src/parametrics/ropeNode.cxx -+++ b/panda/src/parametrics/ropeNode.cxx -@@ -334,7 +334,8 @@ render_thread(CullTraverser *trav, CullTraverserData &data, - - CullableObject *object = - new CullableObject(geom, state, -- data.get_internal_transform(trav)); -+ data.get_internal_transform(trav), -+ data.get_prev_internal_transform(trav)); - trav->get_cull_handler()->record_object(object, trav); - } - -@@ -380,7 +381,8 @@ render_tape(CullTraverser *trav, CullTraverserData &data, - - CullableObject *object = - new CullableObject(geom, state, -- data.get_internal_transform(trav)); -+ data.get_internal_transform(trav), -+ data.get_prev_internal_transform(trav)); - trav->get_cull_handler()->record_object(object, trav); - } - -@@ -433,7 +435,8 @@ render_billboard(CullTraverser *trav, CullTraverserData &data, - - CullableObject *object = - new CullableObject(geom, state, -- data.get_internal_transform(trav)); -+ data.get_internal_transform(trav), -+ data.get_prev_internal_transform(trav)); - trav->get_cull_handler()->record_object(object, trav); - } - -@@ -494,7 +497,8 @@ render_tube(CullTraverser *trav, CullTraverserData &data, - - CullableObject *object = - new CullableObject(geom, state, -- data.get_internal_transform(trav)); -+ data.get_internal_transform(trav), -+ data.get_prev_internal_transform(trav)); - trav->get_cull_handler()->record_object(object, trav); - } - -diff --git a/panda/src/parametrics/sheetNode.cxx b/panda/src/parametrics/sheetNode.cxx -index 34496bc..f745d6e 100644 ---- a/panda/src/parametrics/sheetNode.cxx -+++ b/panda/src/parametrics/sheetNode.cxx -@@ -331,7 +331,8 @@ render_sheet(CullTraverser *trav, CullTraverserData &data, - - CullableObject *object = - new CullableObject(geom, state, -- data.get_internal_transform(trav)); -+ data.get_internal_transform(trav), -+ data.get_prev_internal_transform(trav)); - trav->get_cull_handler()->record_object(object, trav); - } - -diff --git a/panda/src/pgraph/config_pgraph.cxx b/panda/src/pgraph/config_pgraph.cxx -index d5427a6..1360bbf 100644 ---- a/panda/src/pgraph/config_pgraph.cxx -+++ b/panda/src/pgraph/config_pgraph.cxx -@@ -369,6 +369,13 @@ ConfigVariableBool allow_live_flatten - "only has an effect when Panda is not compiled for a release " - "build.")); - -+ -+ConfigVariableBool always_store_prev_transform -+("always-store-prev-transform", false, -+ PRC_DESC("When setting this to true, the previous transform is kept on " -+ "nodes. This makes NodePath::set_pos and all other transform changes " -+ "behave like their fluid variants, e.g. set_fluid_pos.")); -+ - /** - * Initializes the library. This must be called at least once before any of - * the functions or classes in this library can be used. Normally it will be -diff --git a/panda/src/pgraph/config_pgraph.h b/panda/src/pgraph/config_pgraph.h -index 988c9b1..4ba1ad5 100644 ---- a/panda/src/pgraph/config_pgraph.h -+++ b/panda/src/pgraph/config_pgraph.h -@@ -72,6 +72,7 @@ extern ConfigVariableList load_file_type; - extern ConfigVariableString default_model_extension; - - extern ConfigVariableBool allow_live_flatten; -+extern ConfigVariableBool always_store_prev_transform; - - extern EXPCL_PANDA_PGRAPH void init_libpgraph(); - -diff --git a/panda/src/pgraph/cullTraverser.I b/panda/src/pgraph/cullTraverser.I -index 180b94b..8441ba0 100644 ---- a/panda/src/pgraph/cullTraverser.I -+++ b/panda/src/pgraph/cullTraverser.I -@@ -227,6 +227,8 @@ do_traverse(CullTraverserData &data) { - - data.apply_transform_and_state(this); - -+#if defined(HAVE_CG) || defined(SUPPORT_FIXED_FUNCTION) -+ // Fog is only supported with the shader generator or the FFP. - const FogAttrib *fog = (const FogAttrib *) - node_reader->get_state()->get_attrib(FogAttrib::get_class_slot()); - -@@ -237,6 +239,7 @@ do_traverse(CullTraverserData &data) { - // it. - fog->get_fog()->adjust_to_camera(get_camera_transform()); - } -+#endif - - if (fancy_bits & PandaNode::FB_cull_callback) { - PandaNode *node = data.node(); -diff --git a/panda/src/pgraph/cullTraverser.cxx b/panda/src/pgraph/cullTraverser.cxx -index 5d08b4e..df8ae99 100644 ---- a/panda/src/pgraph/cullTraverser.cxx -+++ b/panda/src/pgraph/cullTraverser.cxx -@@ -128,7 +128,9 @@ traverse(const NodePath &root) { - // Store this pointer in this - set_portal_clipper(&portal_viewer); - -- CullTraverserData data(root, TransformState::make_identity(), -+ CullTraverserData data(root, -+ TransformState::make_identity(), -+ TransformState::make_identity(), - _initial_state, _view_frustum, - _current_thread); - -@@ -142,13 +144,17 @@ traverse(const NodePath &root) { - // Render the frustum relative to the cull center. - NodePath cull_center = _scene_setup->get_cull_center(); - CPT(TransformState) transform = cull_center.get_transform(root); -+ CPT(TransformState) prev_transform = cull_center.get_prev_transform(root); - - CullTraverserData my_data(data, portal_viewer._previous); - my_data._net_transform = my_data._net_transform->compose(transform); -+ my_data._prev_net_transform = my_data._prev_net_transform->compose(prev_transform); - traverse(my_data); - - } else { -- CullTraverserData data(root, TransformState::make_identity(), -+ CullTraverserData data(root, -+ TransformState::make_identity(), -+ TransformState::make_identity(), - _initial_state, _view_frustum, - _current_thread); - -diff --git a/panda/src/pgraph/cullTraverserData.I b/panda/src/pgraph/cullTraverserData.I -index 4367d16..7c1a79f 100644 ---- a/panda/src/pgraph/cullTraverserData.I -+++ b/panda/src/pgraph/cullTraverserData.I -@@ -17,12 +17,14 @@ - INLINE CullTraverserData:: - CullTraverserData(const NodePath &start, - const TransformState *net_transform, -+ const TransformState *prev_net_transform, - const RenderState *state, - GeometricBoundingVolume *view_frustum, - Thread *current_thread) : - _node_path(start), - _node_reader(start.node(), current_thread), - _net_transform(net_transform), -+ _prev_net_transform(prev_net_transform), - _state(state), - _view_frustum(view_frustum), - _cull_planes(CullPlanes::make_empty()), -@@ -42,6 +44,7 @@ CullTraverserData(const CullTraverserData ©) : - _node_path(copy._node_path), - _node_reader(copy._node_reader), - _net_transform(copy._net_transform), -+ _prev_net_transform(copy._prev_net_transform), - _state(copy._state), - _view_frustum(copy._view_frustum), - _cull_planes(copy._cull_planes), -@@ -58,6 +61,7 @@ operator = (const CullTraverserData ©) { - _node_path = copy._node_path; - _node_reader = copy._node_reader; - _net_transform = copy._net_transform; -+ _prev_net_transform = copy._prev_net_transform; - _state = copy._state; - _view_frustum = copy._view_frustum; - _cull_planes = copy._cull_planes; -@@ -74,6 +78,7 @@ CullTraverserData(const CullTraverserData &parent, PandaNode *child) : - _node_path(parent._node_path, child), - _node_reader(child, parent._node_reader.get_current_thread()), - _net_transform(parent._net_transform), -+ _prev_net_transform(parent._prev_net_transform), - _state(parent._state), - _view_frustum(parent._view_frustum), - _cull_planes(parent._cull_planes), -@@ -134,6 +139,14 @@ INLINE CPT(TransformState) CullTraverserData:: - get_internal_transform(const CullTraverser *trav) const { - return trav->get_scene()->get_cs_world_transform()->compose(_net_transform); - } -+/** -+ * Returns the previous internal transform: the modelview transform in the GSG's -+ * internal coordinate system in the last frame. -+ */ -+INLINE CPT(TransformState) CullTraverserData:: -+get_prev_internal_transform(const CullTraverser *trav) const { -+ return trav->get_scene()->get_prev_cs_world_transform()->compose(_prev_net_transform); -+} - - /** - * Returns the net transform: the relative transform from root of the scene -@@ -145,6 +158,15 @@ get_net_transform(const CullTraverser *) const { - } - - /** -+ * Returns the previous net transform: the relative transform from root of the scene -+ * graph to the current node in the last frame. -+ */ -+INLINE const TransformState *CullTraverserData:: -+get_prev_net_transform(const CullTraverser *) const { -+ return _prev_net_transform; -+} -+ -+/** - * Returns true if the current node is within the view frustum, false - * otherwise. If the node's bounding volume falls completely within the view - * frustum, this will also reset the view frustum pointer, saving some work -diff --git a/panda/src/pgraph/cullTraverserData.cxx b/panda/src/pgraph/cullTraverserData.cxx -index 186869c..e67d45d 100644 ---- a/panda/src/pgraph/cullTraverserData.cxx -+++ b/panda/src/pgraph/cullTraverserData.cxx -@@ -47,6 +47,7 @@ apply_transform_and_state(CullTraverser *trav) { - _node_reader.compose_draw_mask(_draw_mask); - - apply_transform_and_state(trav, _node_reader.get_transform(), -+ _node_reader.get_prev_transform(), - node_state, _node_reader.get_effects(), - _node_reader.get_off_clip_planes()); - } -@@ -58,6 +59,7 @@ apply_transform_and_state(CullTraverser *trav) { - void CullTraverserData:: - apply_transform_and_state(CullTraverser *trav, - CPT(TransformState) node_transform, -+ CPT(TransformState) prev_node_transform, - CPT(RenderState) node_state, - CPT(RenderEffects) node_effects, - const RenderAttrib *off_clip_planes) { -@@ -96,6 +98,12 @@ apply_transform_and_state(CullTraverser *trav, - } - } - -+ if (_net_transform == _prev_net_transform && node_transform == prev_node_transform) { -+ _prev_net_transform = _net_transform; -+ } else if (!prev_node_transform->is_identity()) { -+ _prev_net_transform = _prev_net_transform->compose(prev_node_transform); -+ } -+ - _state = _state->compose(node_state); - - if (clip_plane_cull) { -diff --git a/panda/src/pgraph/cullTraverserData.h b/panda/src/pgraph/cullTraverserData.h -index 80d523f..30b8d68 100644 ---- a/panda/src/pgraph/cullTraverserData.h -+++ b/panda/src/pgraph/cullTraverserData.h -@@ -41,6 +41,7 @@ class EXPCL_PANDA_PGRAPH CullTraverserData { - public: - INLINE CullTraverserData(const NodePath &start, - const TransformState *net_transform, -+ const TransformState *prev_net_transform, - const RenderState *state, - GeometricBoundingVolume *view_frustum, - Thread *current_thread); -@@ -60,7 +61,9 @@ public: - PUBLISHED: - INLINE CPT(TransformState) get_modelview_transform(const CullTraverser *trav) const; - INLINE CPT(TransformState) get_internal_transform(const CullTraverser *trav) const; -+ INLINE CPT(TransformState) get_prev_internal_transform(const CullTraverser *trav) const; - INLINE const TransformState *get_net_transform(const CullTraverser *trav) const; -+ INLINE const TransformState *get_prev_net_transform(const CullTraverser *trav) const; - - INLINE bool is_in_view(const DrawMask &camera_mask); - INLINE bool is_this_node_hidden(const DrawMask &camera_mask) const; -@@ -68,6 +71,7 @@ PUBLISHED: - void apply_transform_and_state(CullTraverser *trav); - void apply_transform_and_state(CullTraverser *trav, - CPT(TransformState) node_transform, -+ CPT(TransformState) prev_node_transform, - CPT(RenderState) node_state, - CPT(RenderEffects) node_effects, - const RenderAttrib *off_clip_planes); -@@ -76,6 +80,7 @@ public: - WorkingNodePath _node_path; - PandaNodePipelineReader _node_reader; - CPT(TransformState) _net_transform; -+ CPT(TransformState) _prev_net_transform; - CPT(RenderState) _state; - PT(GeometricBoundingVolume) _view_frustum; - CPT(CullPlanes) _cull_planes; -diff --git a/panda/src/pgraph/cullableObject.I b/panda/src/pgraph/cullableObject.I -index 2424965..5fbe70c 100644 ---- a/panda/src/pgraph/cullableObject.I -+++ b/panda/src/pgraph/cullableObject.I -@@ -23,6 +23,24 @@ CullableObject() { - - /** - * Creates a CullableObject based the indicated geom, with the indicated -+ * render state, transform and previous transform. -+ */ -+INLINE CullableObject:: -+CullableObject(const Geom *geom, const RenderState *state, -+ const TransformState *internal_transform, -+ const TransformState *prev_internal_transform) : -+ _geom(geom), -+ _state(state), -+ _internal_transform(internal_transform), -+ _prev_internal_transform(prev_internal_transform) -+{ -+#ifdef DO_MEMORY_USAGE -+ MemoryUsage::update_type(this, get_class_type()); -+#endif -+} -+ -+/** -+ * Creates a CullableObject based the indicated geom, with the indicated - * render state and transform. - */ - INLINE CullableObject:: -@@ -30,7 +48,8 @@ CullableObject(const Geom *geom, const RenderState *state, - const TransformState *internal_transform) : - _geom(geom), - _state(state), -- _internal_transform(internal_transform) -+ _internal_transform(internal_transform), -+ _prev_internal_transform(internal_transform) - { - #ifdef DO_MEMORY_USAGE - MemoryUsage::update_type(this, get_class_type()); -@@ -46,7 +65,8 @@ CullableObject(const CullableObject ©) : - _munger(copy._munger), - _munged_data(copy._munged_data), - _state(copy._state), -- _internal_transform(copy._internal_transform) -+ _internal_transform(copy._internal_transform), -+ _prev_internal_transform(copy._prev_internal_transform) - { - #ifdef DO_MEMORY_USAGE - MemoryUsage::update_type(this, get_class_type()); -@@ -63,6 +83,7 @@ operator = (const CullableObject ©) { - _munged_data = copy._munged_data; - _state = copy._state; - _internal_transform = copy._internal_transform; -+ _prev_internal_transform = copy._prev_internal_transform; - _draw_callback = copy._draw_callback; - } - -@@ -75,7 +96,7 @@ draw(GraphicsStateGuardianBase *gsg, bool force, Thread *current_thread) { - if (_draw_callback != (CallbackObject *)NULL) { - // It has a callback associated. - gsg->clear_before_callback(); -- gsg->set_state_and_transform(_state, _internal_transform); -+ gsg->set_state_and_transform(_state, _internal_transform, _prev_internal_transform); - GeomDrawCallbackData cbdata(this, gsg, force); - _draw_callback->do_callback(&cbdata); - if (cbdata.get_lost_state()) { -@@ -85,7 +106,7 @@ draw(GraphicsStateGuardianBase *gsg, bool force, Thread *current_thread) { - // Now the callback has taken care of drawing. - } else { - nassertv(_geom != (Geom *)NULL); -- gsg->set_state_and_transform(_state, _internal_transform); -+ gsg->set_state_and_transform(_state, _internal_transform, _prev_internal_transform); - draw_inline(gsg, force, current_thread); - } - } -diff --git a/panda/src/pgraph/cullableObject.h b/panda/src/pgraph/cullableObject.h -index 82fd0df..f450cbc 100644 ---- a/panda/src/pgraph/cullableObject.h -+++ b/panda/src/pgraph/cullableObject.h -@@ -49,6 +49,10 @@ public: - INLINE CullableObject(const Geom *geom, const RenderState *state, - const TransformState *internal_transform); - -+ INLINE CullableObject(const Geom *geom, const RenderState *state, -+ const TransformState *internal_transform, -+ const TransformState *prev_internal_transform); -+ - INLINE CullableObject(const CullableObject ©); - INLINE void operator = (const CullableObject ©); - -@@ -74,6 +78,7 @@ public: - CPT(GeomVertexData) _munged_data; - CPT(RenderState) _state; - CPT(TransformState) _internal_transform; -+ CPT(TransformState) _prev_internal_transform; - PT(CallbackObject) _draw_callback; - - private: -diff --git a/panda/src/pgraph/geomNode.cxx b/panda/src/pgraph/geomNode.cxx -index bfbd24f..5ea321e 100644 ---- a/panda/src/pgraph/geomNode.cxx -+++ b/panda/src/pgraph/geomNode.cxx -@@ -512,7 +512,6 @@ add_for_draw(CullTraverser *trav, CullTraverserData &data) { - Geoms geoms = get_geoms(trav->get_current_thread()); - int num_geoms = geoms.get_num_geoms(); - trav->_geoms_pcollector.add_level(num_geoms); -- CPT(TransformState) internal_transform = data.get_internal_transform(trav); - - for (int i = 0; i < num_geoms; i++) { - const Geom *geom = geoms.get_geom(i); -@@ -558,7 +557,9 @@ add_for_draw(CullTraverser *trav, CullTraverserData &data) { - } - - CullableObject *object = -- new CullableObject(geom, state, internal_transform); -+ new CullableObject(geom, state, -+ data.get_internal_transform(trav), -+ data.get_prev_internal_transform(trav)); - trav->get_cull_handler()->record_object(object, trav); - } - } -diff --git a/panda/src/pgraph/nodePath.cxx b/panda/src/pgraph/nodePath.cxx -index c6bfff1..ecb8936 100644 ---- a/panda/src/pgraph/nodePath.cxx -+++ b/panda/src/pgraph/nodePath.cxx -@@ -906,7 +906,9 @@ void NodePath:: - set_pos(const LVecBase3 &pos) { - nassertv_always(!is_empty()); - set_transform(get_transform()->set_pos(pos)); -- node()->reset_prev_transform(); -+ if (!always_store_prev_transform) { -+ node()->reset_prev_transform(); -+ } - } - - void NodePath:: -@@ -1173,7 +1175,10 @@ set_pos_hpr(const LVecBase3 &pos, const LVecBase3 &hpr) { - transform = TransformState::make_pos_hpr_scale_shear - (pos, hpr, transform->get_scale(), transform->get_shear()); - set_transform(transform); -- node()->reset_prev_transform(); -+ -+ if (!always_store_prev_transform) { -+ node()->reset_prev_transform(); -+ } - } - - /** -@@ -1187,7 +1192,10 @@ set_pos_quat(const LVecBase3 &pos, const LQuaternion &quat) { - transform = TransformState::make_pos_quat_scale_shear - (pos, quat, transform->get_scale(), transform->get_shear()); - set_transform(transform); -- node()->reset_prev_transform(); -+ -+ if (!always_store_prev_transform) { -+ node()->reset_prev_transform(); -+ } - } - - /** -@@ -1226,7 +1234,9 @@ set_pos_hpr_scale(const LVecBase3 &pos, const LVecBase3 &hpr, - nassertv_always(!is_empty()); - set_transform(TransformState::make_pos_hpr_scale - (pos, hpr, scale)); -- node()->reset_prev_transform(); -+ if (!always_store_prev_transform) { -+ node()->reset_prev_transform(); -+ } - } - - /** -@@ -1239,7 +1249,9 @@ set_pos_quat_scale(const LVecBase3 &pos, const LQuaternion &quat, - nassertv_always(!is_empty()); - set_transform(TransformState::make_pos_quat_scale - (pos, quat, scale)); -- node()->reset_prev_transform(); -+ if (!always_store_prev_transform) { -+ node()->reset_prev_transform(); -+ } - } - - /** -@@ -1252,7 +1264,9 @@ set_pos_hpr_scale_shear(const LVecBase3 &pos, const LVecBase3 &hpr, - nassertv_always(!is_empty()); - set_transform(TransformState::make_pos_hpr_scale_shear - (pos, hpr, scale, shear)); -- node()->reset_prev_transform(); -+ if (!always_store_prev_transform) { -+ node()->reset_prev_transform(); -+ } - } - - /** -@@ -1265,7 +1279,9 @@ set_pos_quat_scale_shear(const LVecBase3 &pos, const LQuaternion &quat, - nassertv_always(!is_empty()); - set_transform(TransformState::make_pos_quat_scale_shear - (pos, quat, scale, shear)); -- node()->reset_prev_transform(); -+ if (!always_store_prev_transform) { -+ node()->reset_prev_transform(); -+ } - } - - /** -@@ -1275,7 +1291,9 @@ void NodePath:: - set_mat(const LMatrix4 &mat) { - nassertv_always(!is_empty()); - set_transform(TransformState::make_mat(mat)); -- node()->reset_prev_transform(); -+ if (!always_store_prev_transform) { -+ node()->reset_prev_transform(); -+ } - } - - /** -@@ -1334,7 +1352,9 @@ set_pos(const NodePath &other, const LVecBase3 &pos) { - // If we didn't have a componentwise transform already, never mind. - set_transform(other, rel_transform->set_pos(pos)); - } -- node()->reset_prev_transform(); -+ if (!always_store_prev_transform) { -+ node()->reset_prev_transform(); -+ } - } - - void NodePath:: -@@ -1712,7 +1732,9 @@ set_pos_hpr(const NodePath &other, const LVecBase3 &pos, - // If we didn't have a componentwise transform already, never mind. - set_transform(other, TransformState::make_pos_hpr_scale_shear - (pos, hpr, rel_transform->get_scale(), rel_transform->get_shear())); -- node()->reset_prev_transform(); -+ if (!always_store_prev_transform) { -+ node()->reset_prev_transform(); -+ } - } - } - -@@ -1747,7 +1769,9 @@ set_pos_quat(const NodePath &other, const LVecBase3 &pos, - // If we didn't have a componentwise transform already, never mind. - set_transform(other, TransformState::make_pos_quat_scale_shear - (pos, quat, rel_transform->get_scale(), rel_transform->get_shear())); -- node()->reset_prev_transform(); -+ if (!always_store_prev_transform) { -+ node()->reset_prev_transform(); -+ } - } - } - -@@ -1797,7 +1821,9 @@ set_pos_hpr_scale(const NodePath &other, - nassertv_always(!is_empty()); - set_transform(other, TransformState::make_pos_hpr_scale - (pos, hpr, scale)); -- node()->reset_prev_transform(); -+ if (!always_store_prev_transform) { -+ node()->reset_prev_transform(); -+ } - } - - /** -@@ -1811,7 +1837,9 @@ set_pos_quat_scale(const NodePath &other, - nassertv_always(!is_empty()); - set_transform(other, TransformState::make_pos_quat_scale - (pos, quat, scale)); -- node()->reset_prev_transform(); -+ if (!always_store_prev_transform) { -+ node()->reset_prev_transform(); -+ } - } - - /** -@@ -1825,7 +1853,9 @@ set_pos_hpr_scale_shear(const NodePath &other, - nassertv_always(!is_empty()); - set_transform(other, TransformState::make_pos_hpr_scale_shear - (pos, hpr, scale, shear)); -- node()->reset_prev_transform(); -+ if (!always_store_prev_transform) { -+ node()->reset_prev_transform(); -+ } - } - - /** -@@ -1839,7 +1869,9 @@ set_pos_quat_scale_shear(const NodePath &other, - nassertv_always(!is_empty()); - set_transform(other, TransformState::make_pos_quat_scale_shear - (pos, quat, scale, shear)); -- node()->reset_prev_transform(); -+ if (!always_store_prev_transform) { -+ node()->reset_prev_transform(); -+ } - } - - /** -@@ -1864,7 +1896,9 @@ void NodePath:: - set_mat(const NodePath &other, const LMatrix4 &mat) { - nassertv_always(!is_empty()); - set_transform(other, TransformState::make_mat(mat)); -- node()->reset_prev_transform(); -+ if (!always_store_prev_transform) { -+ node()->reset_prev_transform(); -+ } - } - - /** -diff --git a/panda/src/pgraph/nodePath.h b/panda/src/pgraph/nodePath.h -index 729bcf3..a9a8233 100644 ---- a/panda/src/pgraph/nodePath.h -+++ b/panda/src/pgraph/nodePath.h -@@ -66,50 +66,73 @@ class ShaderInput; - * A NodePath is the fundamental unit of high-level interaction with the scene - * graph. It encapsulates the complete path down to a node from some other - * node, usually the root of the scene graph. This is used to resolve -- * ambiguities associated with instancing. NodePath also contains a number of -- * handy high-level methods for common scene-graph manipulations, such as -- * reparenting, and common state changes, such as repositioning. There are -- * also a number of NodePath methods for finding nodes deep within the tree by -- * name or by type. These take a path string, which at its simplest consists -- * of a series of node names separated by slashes, like a directory pathname. -+ * ambiguities associated with instancing. -+ * -+ * NodePath also contains a number of handy high-level methods for common -+ * scene-graph manipulations, such as reparenting, and common state changes, -+ * such as repositioning. There are also a number of NodePath methods for -+ * finding nodes deep within the tree by name or by type. These take a path -+ * string, which at its simplest consists of a series of node names separated -+ * by slashes, like a directory pathname. -+ * - * Each component of the path string may optionally consist of one of the -- * following special names, instead of a node name: * -- matches -- * exactly one node, with any name. ** -- matches any sequence of -- * zero or more nodes. +typename -- matches any node that is or derives from -- * the given type. -typename -- matches any node that is the given type -- * exactly. =tag -- matches any node that has the indicated tag. -- * =tag=value -- matches any node whose tag matches the indicated value. -+ * following special names, instead of a node name: -+ * -+ * * -- matches exactly one node, with any name. -+ * ** -- matches any sequence of zero or more nodes. -+ * +typename -- matches any node that is or derives from the given type. -+ * -typename -- matches any node that is the given type exactly. -+ * =tag -- matches any node that has the indicated tag. -+ * =tag=value -- matches any node whose tag matches the indicated value. -+ * - * Furthermore, a node name may itself contain standard filename globbing - * characters, like *, ?, and [a-z], that will be accepted as a partial match. - * (In fact, the '*' special name may be seen as just a special case of this.) - * The globbing characters may not be used with the typename matches or with - * tag matches, but they may be used to match a tag's value in the =tag=value -- * syntax. The special characters "@@", appearing at the beginning of a node -+ * syntax. -+ * -+ * The special characters "@@", appearing at the beginning of a node - * name, indicate a stashed node. Normally, stashed nodes are not returned by - * a find (but see the special flags, below), but a stashed node may be found - * if it is explicitly named with its leading @@ characters. By extension, -- * "@@*" may be used to identify any stashed node. Examples: "roomgraph" will -- * look for a node named "graph", which is a child of an unnamed node, which -- * is a child of a node named "room", which is a child of the starting path. -- * "**red*" will look for any node anywhere in the tree (below the starting -- * path) with a name that begins with "red". "**+PartBundleNode**head" will -- * look for a node named "head", somewhere below a PartBundleNode anywhere in -- * the tree. The search is always potentially ambiguous, even if the special -+ * "@@*" may be used to identify any stashed node. -+ * -+ * Examples: -+ * -+ * "roomgraph" will look for a node named "graph", which is a child -+ * of an unnamed node, which is a child of a node named "room", which is a -+ * child of the starting path. -+ * -+ * "**red*" will look for any node anywhere in the tree (below the starting -+ * path) with a name that begins with "red". -+ * -+ * "**+PartBundleNode**head" will look for a node named "head", somewhere -+ * below a PartBundleNode anywhere in the tree. -+ * -+ * The search is always potentially ambiguous, even if the special - * wildcard operators are not used, because there may be multiple nodes in the - * tree with the same name. In general, in the case of an ambiguity, the - * shortest path is preferred; when a method (such as extend_by) must choose - * only only one of several possible paths, it will choose the shortest - * available; on the other hand, when a method (such as find_all_matches) is - * to return all of the matching paths, it will sort them so that the shortest -- * paths appear first in the output. Special flags. The entire string may -- * optionally be followed by the ";" character, followed by one or more of the -- * following special control flags, with no intervening spaces or punctuation: -- * -h Do not return hidden nodes. +h Do return hidden nodes. -s Do -- * not return stashed nodes unless explicitly referenced with @@. +s Return -- * stashed nodes even without any explicit @@ characters. -i Node name -- * comparisons are not case insensitive: case must match exactly. +i Node -- * name comparisons are case insensitive: case is not important. This affects -- * matches against the node name only; node type and tag strings are always -+ * paths appear first in the output. -+ * -+ * Special flags: -+ * -+ * The entire string may optionally be followed by the ";" character, followed -+ * by one or more of the following special control flags, with no intervening -+ * spaces or punctuation: -+ * -+ * -h Do not return hidden nodes. -+ * +h Do return hidden nodes. -+ * -s Do not return stashed nodes unless explicitly referenced with @@. -+ * +s Return stashed nodes even without any explicit @@ characters. -+ * -i Node name comparisons are not case insensitive: case must match exactly. -+ * +i Node name comparisons are case insensitive: case is not important. -+ * -+ * This affects matches against the node name only; node type and tag strings are always - * case sensitive. The default flags are +h-s-i. - */ - -diff --git a/panda/src/pgraph/occluderNode.cxx b/panda/src/pgraph/occluderNode.cxx -index c1a4c4d..83695fb 100644 ---- a/panda/src/pgraph/occluderNode.cxx -+++ b/panda/src/pgraph/occluderNode.cxx -@@ -142,14 +142,16 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { - // draw a visualization, a checkerboard-textured polygon. - CullableObject *occluder_viz = - new CullableObject(get_occluder_viz(trav, data), get_occluder_viz_state(trav, data), -- data.get_internal_transform(trav)); -+ data.get_internal_transform(trav), -+ data.get_prev_internal_transform(trav)); - trav->get_cull_handler()->record_object(occluder_viz, trav); - - // Also get the frame. - nassertr(_frame_viz != (Geom *)NULL, false); - CullableObject *frame_viz = - new CullableObject(_frame_viz, get_frame_viz_state(trav, data), -- data.get_internal_transform(trav)); -+ data.get_internal_transform(trav), -+ data.get_prev_internal_transform(trav)); - trav->get_cull_handler()->record_object(frame_viz, trav); - - // Now carry on to render our child nodes. -diff --git a/panda/src/pgraph/planeNode.cxx b/panda/src/pgraph/planeNode.cxx -index c096eae..a643862 100644 ---- a/panda/src/pgraph/planeNode.cxx -+++ b/panda/src/pgraph/planeNode.cxx -@@ -141,7 +141,8 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { - - CullableObject *plane_viz = - new CullableObject(get_viz(trav, data), data._state, -- data.get_internal_transform(trav)); -+ data.get_internal_transform(trav), -+ data.get_prev_internal_transform(trav)); - trav->get_cull_handler()->record_object(plane_viz, trav); - - // Now carry on to render our child nodes. -diff --git a/panda/src/pgraph/portalNode.cxx b/panda/src/pgraph/portalNode.cxx -index d70eacd..3e360fd 100644 ---- a/panda/src/pgraph/portalNode.cxx -+++ b/panda/src/pgraph/portalNode.cxx -@@ -228,6 +228,7 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { - - // Get the net trasform of the _cell_out as seen from the camera. - CPT(TransformState) cell_transform = _cell_out.get_net_transform(); -+ CPT(TransformState) prev_cell_transform = _cell_out.get_net_prev_transform(); - CPT(TransformState) frustum_transform = cell_transform ->invert_compose(portal_viewer->_scene_setup->get_cull_center().get_net_transform()); - - // transform to _cell_out space -@@ -283,6 +284,7 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { - - CullTraverserData next_data(_cell_out, - cell_transform, -+ prev_cell_transform, - next_state, new_bh, - current_thread); - next_data._portal_depth = data._portal_depth + 1; -diff --git a/panda/src/pgraph/sceneSetup.I b/panda/src/pgraph/sceneSetup.I -index 425c538..1b150ff 100644 ---- a/panda/src/pgraph/sceneSetup.I -+++ b/panda/src/pgraph/sceneSetup.I -@@ -25,6 +25,7 @@ SceneSetup() { - _world_transform = TransformState::make_identity(); - _cs_transform = TransformState::make_identity(); - _cs_world_transform = TransformState::make_identity(); -+ _prev_cs_world_transform = TransformState::make_identity(); - } - - /** -@@ -268,3 +269,21 @@ INLINE const TransformState *SceneSetup:: - get_cs_world_transform() const { - return _cs_world_transform; - } -+ -+/** -+ * Specifies the position from the starting node relative to the camera, in -+ * the GSG's internal coordinate system. -+ */ -+INLINE void SceneSetup:: -+set_prev_cs_world_transform(const TransformState *prev_cs_world_transform) { -+ _prev_cs_world_transform = prev_cs_world_transform; -+} -+ -+/** -+ * Returns the position from the starting node relative to the camera, in the -+ * GSG's internal coordinate system. -+ */ -+INLINE const TransformState *SceneSetup:: -+get_prev_cs_world_transform() const { -+ return _prev_cs_world_transform; -+} -diff --git a/panda/src/pgraph/sceneSetup.h b/panda/src/pgraph/sceneSetup.h -index d2ad0c0..72c0599 100644 ---- a/panda/src/pgraph/sceneSetup.h -+++ b/panda/src/pgraph/sceneSetup.h -@@ -74,6 +74,9 @@ PUBLISHED: - INLINE void set_cs_world_transform(const TransformState *cs_world_transform); - INLINE const TransformState *get_cs_world_transform() const; - -+ INLINE void set_prev_cs_world_transform(const TransformState *prev_cs_world_transform); -+ INLINE const TransformState *get_prev_cs_world_transform() const; -+ - private: - DisplayRegion *_display_region; - int _viewport_width; -@@ -88,6 +91,7 @@ private: - CPT(TransformState) _world_transform; - CPT(TransformState) _cs_transform; - CPT(TransformState) _cs_world_transform; -+ CPT(TransformState) _prev_cs_world_transform; - - public: - static TypeHandle get_class_type() { -diff --git a/panda/src/pgraphnodes/callbackNode.cxx b/panda/src/pgraphnodes/callbackNode.cxx -index 17424d5..2ced441 100644 ---- a/panda/src/pgraphnodes/callbackNode.cxx -+++ b/panda/src/pgraphnodes/callbackNode.cxx -@@ -133,7 +133,8 @@ add_for_draw(CullTraverser *trav, CullTraverserData &data) { - if (cbobj != (CallbackObject *)NULL) { - CullableObject *object = - new CullableObject(NULL, data._state, -- data.get_internal_transform(trav)); -+ data.get_internal_transform(trav), -+ data.get_prev_internal_transform(trav)); - object->set_draw_callback(cbobj); - trav->get_cull_handler()->record_object(object, trav); - } -diff --git a/panda/src/pgraphnodes/computeNode.cxx b/panda/src/pgraphnodes/computeNode.cxx -index 7962378..2f40dd6 100644 ---- a/panda/src/pgraphnodes/computeNode.cxx -+++ b/panda/src/pgraphnodes/computeNode.cxx -@@ -94,7 +94,8 @@ add_for_draw(CullTraverser *trav, CullTraverserData &data) { - // however. - CullableObject *object = - new CullableObject(NULL, data._state, -- data.get_internal_transform(trav)); -+ data.get_internal_transform(trav), -+ data.get_prev_internal_transform(trav)); - object->set_draw_callback(_dispatcher); - trav->get_cull_handler()->record_object(object, trav); - } -diff --git a/panda/src/pgraphnodes/lodNode.cxx b/panda/src/pgraphnodes/lodNode.cxx -index 5936b06..2d52b41 100644 ---- a/panda/src/pgraphnodes/lodNode.cxx -+++ b/panda/src/pgraphnodes/lodNode.cxx -@@ -394,6 +394,7 @@ show_switches_cull_callback(CullTraverser *trav, CullTraverserData &data) { - // And draw the spindle in this color. - CullTraverserData next_data2(data, sw.get_spindle_viz()); - next_data2.apply_transform_and_state(trav, viz_transform, -+ TransformState::make_identity(), - RenderState::make_empty(), - RenderEffects::make_empty(), - ClipPlaneAttrib::make()); -@@ -404,6 +405,7 @@ show_switches_cull_callback(CullTraverser *trav, CullTraverserData &data) { - // the geometry and the spindle. - CullTraverserData next_data(data, sw.get_ring_viz()); - next_data.apply_transform_and_state(trav, viz_transform, -+ TransformState::make_identity(), - RenderState::make_empty(), - RenderEffects::make_empty(), - ClipPlaneAttrib::make()); -diff --git a/panda/src/pgraphnodes/nodeCullCallbackData.cxx b/panda/src/pgraphnodes/nodeCullCallbackData.cxx -index d0bf763..b89ac5f 100644 ---- a/panda/src/pgraphnodes/nodeCullCallbackData.cxx -+++ b/panda/src/pgraphnodes/nodeCullCallbackData.cxx -@@ -50,7 +50,8 @@ upcall() { - if (cbobj != (CallbackObject *)NULL) { - CullableObject *object = - new CullableObject(NULL, _data._state, -- _data.get_internal_transform(_trav)); -+ _data.get_internal_transform(_trav), -+ _data.get_prev_internal_transform(_trav)); - object->set_draw_callback(cbobj); - _trav->get_cull_handler()->record_object(object, _trav); - } -diff --git a/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx b/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx -index ba33377..af0a608 100644 ---- a/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx -+++ b/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx -@@ -206,7 +206,7 @@ clear(DrawableRegion *clearable) { - return; - } - -- set_state_and_transform(RenderState::make_empty(), _internal_transform); -+ set_state_and_transform(RenderState::make_empty(), _internal_transform, _prev_internal_transform); - - bool clear_color = false; - PIXEL color = 0; -@@ -1467,7 +1467,8 @@ framebuffer_copy_to_ram(Texture *tex, int view, int z, - */ - void TinyGraphicsStateGuardian:: - set_state_and_transform(const RenderState *target, -- const TransformState *transform) { -+ const TransformState *transform, -+ const TransformState *prev_transform) { - #ifndef NDEBUG - if (tinydisplay_cat.is_spam()) { - tinydisplay_cat.spam() -@@ -1480,6 +1481,8 @@ set_state_and_transform(const RenderState *target, - _state_pcollector.add_level(1); - PStatTimer timer1(_draw_set_state_pcollector); - -+ _prev_internal_transform = prev_transform; -+ - if (transform != _internal_transform) { - PStatTimer timer(_draw_set_state_transform_pcollector); - _state_pcollector.add_level(1); -diff --git a/panda/src/tinydisplay/tinyGraphicsStateGuardian.h b/panda/src/tinydisplay/tinyGraphicsStateGuardian.h -index 65e01d7..69f4ed9 100644 ---- a/panda/src/tinydisplay/tinyGraphicsStateGuardian.h -+++ b/panda/src/tinydisplay/tinyGraphicsStateGuardian.h -@@ -82,7 +82,8 @@ public: - (Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb); - - virtual void set_state_and_transform(const RenderState *state, -- const TransformState *transform); -+ const TransformState *transform, -+ const TransformState *prev_transform); - - virtual TextureContext *prepare_texture(Texture *tex, int view); - virtual bool update_texture(TextureContext *tc, bool force); --- -1.9.5.msysgit.0 - +From 47c83fce1f4a96954a5db33c6be875e17a4675cc Mon Sep 17 00:00:00 2001 +From: tobspr +Date: Sat, 27 Feb 2016 17:35:53 +0100 +Subject: [PATCH] Support for p3d_PrevModelViewMatrix, populate previous + transform during cull stage + +--- + panda/src/collide/collisionVisualizer.cxx | 7 +- + panda/src/display/callbackGraphicsWindow.cxx | 3 +- + panda/src/display/graphicsEngine.cxx | 4 ++ + panda/src/display/graphicsStateGuardian.I | 9 +++ + panda/src/display/graphicsStateGuardian.cxx | 21 +++++- + panda/src/display/graphicsStateGuardian.h | 9 +++ + panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx | 8 ++- + panda/src/dxgsg9/dxGraphicsStateGuardian9.h | 4 +- + panda/src/glstuff/glCgShaderContext_src.cxx | 8 +++ + panda/src/glstuff/glCgShaderContext_src.h | 2 + + panda/src/glstuff/glGraphicsStateGuardian_src.cxx | 13 ++-- + panda/src/glstuff/glGraphicsStateGuardian_src.h | 3 +- + panda/src/glstuff/glShaderContext_src.cxx | 12 ++++ + panda/src/glstuff/glShaderContext_src.h | 2 + + panda/src/gobj/shader.cxx | 2 + + panda/src/gobj/shader.h | 4 ++ + panda/src/gobj/shaderContext.h | 2 +- + panda/src/gsgbase/graphicsStateGuardianBase.h | 4 ++ + panda/src/parametrics/ropeNode.cxx | 12 ++-- + panda/src/parametrics/sheetNode.cxx | 3 +- + panda/src/pgraph/config_pgraph.cxx | 7 ++ + panda/src/pgraph/config_pgraph.h | 1 + + panda/src/pgraph/cullTraverser.I | 3 + + panda/src/pgraph/cullTraverser.cxx | 10 ++- + panda/src/pgraph/cullTraverserData.I | 22 ++++++ + panda/src/pgraph/cullTraverserData.cxx | 8 +++ + panda/src/pgraph/cullTraverserData.h | 5 ++ + panda/src/pgraph/cullableObject.I | 29 ++++++-- + panda/src/pgraph/cullableObject.h | 5 ++ + panda/src/pgraph/geomNode.cxx | 5 +- + panda/src/pgraph/nodePath.cxx | 66 +++++++++++++----- + panda/src/pgraph/nodePath.h | 81 ++++++++++++++-------- + panda/src/pgraph/occluderNode.cxx | 6 +- + panda/src/pgraph/planeNode.cxx | 3 +- + panda/src/pgraph/portalNode.cxx | 2 + + panda/src/pgraph/sceneSetup.I | 19 +++++ + panda/src/pgraph/sceneSetup.h | 4 ++ + panda/src/pgraphnodes/callbackNode.cxx | 3 +- + panda/src/pgraphnodes/computeNode.cxx | 3 +- + panda/src/pgraphnodes/lodNode.cxx | 2 + + panda/src/pgraphnodes/nodeCullCallbackData.cxx | 3 +- + .../src/tinydisplay/tinyGraphicsStateGuardian.cxx | 7 +- + panda/src/tinydisplay/tinyGraphicsStateGuardian.h | 3 +- + 43 files changed, 349 insertions(+), 80 deletions(-) + +diff --git a/panda/src/collide/collisionVisualizer.cxx b/panda/src/collide/collisionVisualizer.cxx +index 19699b9..cceb6c5 100644 +--- a/panda/src/collide/collisionVisualizer.cxx ++++ b/panda/src/collide/collisionVisualizer.cxx +@@ -112,6 +112,7 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { + xform_data._net_transform = TransformState::make_identity(); + xform_data._view_frustum = trav->get_view_frustum(); + xform_data.apply_transform_and_state(trav, net_transform, ++ TransformState::make_identity(), + RenderState::make_empty(), + RenderEffects::make_empty(), + ClipPlaneAttrib::make()); +@@ -187,7 +188,8 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { + + CullableObject *object = + new CullableObject(geom, point_state, +- xform_data.get_internal_transform(trav)); ++ xform_data.get_internal_transform(trav), ++ xform_data.get_prev_internal_transform(trav)); + + trav->get_cull_handler()->record_object(object, trav); + } +@@ -216,7 +218,8 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { + + CullableObject *object = + new CullableObject(geom, empty_state, +- xform_data.get_internal_transform(trav)); ++ xform_data.get_internal_transform(trav), ++ xform_data.get_prev_internal_transform(trav)); + + trav->get_cull_handler()->record_object(object, trav); + } +diff --git a/panda/src/display/callbackGraphicsWindow.cxx b/panda/src/display/callbackGraphicsWindow.cxx +index f9c2362..ebd5b37 100644 +--- a/panda/src/display/callbackGraphicsWindow.cxx ++++ b/panda/src/display/callbackGraphicsWindow.cxx +@@ -108,7 +108,8 @@ end_frame(FrameMode mode, Thread *current_thread) { + if (_render_callback != NULL) { + // In case the callback or the application hosting the OpenGL context + // wants to do more rendering, let's give it a blank slate. +- _gsg->set_state_and_transform(RenderState::make_empty(), _gsg->get_internal_transform()); ++ _gsg->set_state_and_transform(RenderState::make_empty(), _gsg->get_internal_transform(), ++ _gsg->get_prev_internal_transform()); + _gsg->clear_before_callback(); + + RenderCallbackData data(this, RCT_end_frame, mode); +diff --git a/panda/src/display/graphicsEngine.cxx b/panda/src/display/graphicsEngine.cxx +index 41f6df5..7eed22f 100644 +--- a/panda/src/display/graphicsEngine.cxx ++++ b/panda/src/display/graphicsEngine.cxx +@@ -1805,6 +1805,7 @@ setup_scene(GraphicsStateGuardian *gsg, DisplayRegionPipelineReader *dr) { + NodePath scene_parent = scene_root.get_parent(current_thread); + CPT(TransformState) camera_transform = camera.get_transform(scene_parent, current_thread); + CPT(TransformState) world_transform = scene_parent.get_transform(camera, current_thread); ++ CPT(TransformState) prev_world_transform = scene_parent.get_prev_transform(camera, current_thread); + + if (camera_transform->is_invalid()) { + // There must be a singular transform over the scene. +@@ -1857,6 +1858,9 @@ setup_scene(GraphicsStateGuardian *gsg, DisplayRegionPipelineReader *dr) { + CPT(TransformState) cs_world_transform = cs_transform->compose(world_transform); + scene_setup->set_cs_world_transform(cs_world_transform); + ++ CPT(TransformState) prev_cs_world_transform = cs_transform->compose(prev_world_transform); ++ scene_setup->set_prev_cs_world_transform(prev_cs_world_transform); ++ + // Make sure that the GSG has a ShaderGenerator for the munger to use. We + // have to do this here because the ShaderGenerator needs a host window + // pointer. Hopefully we'll be able to eliminate that requirement in the +diff --git a/panda/src/display/graphicsStateGuardian.I b/panda/src/display/graphicsStateGuardian.I +index db8e1f1..d330100 100644 +--- a/panda/src/display/graphicsStateGuardian.I ++++ b/panda/src/display/graphicsStateGuardian.I +@@ -846,6 +846,15 @@ INLINE CPT(TransformState) GraphicsStateGuardian:: + get_internal_transform() const { + return _internal_transform; + } ++/** ++ * Fetches the previous external net transform. This transform is generally ++ * only set when geometry is about to be rendered. Therefore, this "get" ++ * function is typically only meaningful during the geometry rendering process. ++ */ ++INLINE CPT(TransformState) GraphicsStateGuardian:: ++get_prev_internal_transform() const { ++ return _prev_internal_transform; ++} + + /** + * Returns the current display region being rendered to, as set by the last +diff --git a/panda/src/display/graphicsStateGuardian.cxx b/panda/src/display/graphicsStateGuardian.cxx +index 6f5e873..c0b1cab 100644 +--- a/panda/src/display/graphicsStateGuardian.cxx ++++ b/panda/src/display/graphicsStateGuardian.cxx +@@ -146,6 +146,7 @@ GraphicsStateGuardian(CoordinateSystem internal_coordinate_system, + { + _coordinate_system = CS_invalid; + _internal_transform = TransformState::make_identity(); ++ _prev_internal_transform = _prev_internal_transform; + + set_coordinate_system(get_default_coordinate_system()); + +@@ -1189,6 +1190,9 @@ fetch_specified_part(Shader::ShaderMatInput part, InternalName *name, + case Shader::SMO_model_to_apiview: { + return &(get_internal_transform()->get_mat()); + } ++ case Shader::SMO_prev_model_to_apiview: { ++ return &(get_prev_internal_transform()->get_mat()); ++ } + case Shader::SMO_view_to_model: { + t = get_external_transform()->get_inverse()->get_mat(); + return &t; +@@ -1197,6 +1201,10 @@ fetch_specified_part(Shader::ShaderMatInput part, InternalName *name, + t = get_internal_transform()->get_inverse()->get_mat(); + return &t; + } ++ case Shader::SMO_prev_apiview_to_model: { ++ t = get_prev_internal_transform()->get_inverse()->get_mat(); ++ return &t; ++ } + case Shader::SMO_apiview_to_view: { + return &(_inv_cs_transform->get_mat()); + } +@@ -2308,6 +2316,15 @@ reset() { + } + + /** ++ * @see set_state_and_transform(const RenderState*, const TransformState*, const TransformState*) ++ */ ++void GraphicsStateGuardian:: ++set_state_and_transform(const RenderState *state, ++ const TransformState *trans) { ++ set_state_and_transform(state, trans, trans); ++} ++ ++/** + * Simultaneously resets the render state and the transform state. + * + * This transform specified is the "internal" net transform, already converted +@@ -2321,9 +2338,11 @@ reset() { + */ + void GraphicsStateGuardian:: + set_state_and_transform(const RenderState *state, +- const TransformState *trans) { ++ const TransformState *trans, ++ const TransformState *prev_transform) { + } + ++ + /** + * Clears the framebuffer within the current DisplayRegion, according to the + * flags indicated by the given DrawableRegion object. +diff --git a/panda/src/display/graphicsStateGuardian.h b/panda/src/display/graphicsStateGuardian.h +index 401c538..ad81f17 100644 +--- a/panda/src/display/graphicsStateGuardian.h ++++ b/panda/src/display/graphicsStateGuardian.h +@@ -320,6 +320,10 @@ public: + virtual void set_state_and_transform(const RenderState *state, + const TransformState *transform); + ++ virtual void set_state_and_transform(const RenderState *state, ++ const TransformState *transform, ++ const TransformState *prev_transform); ++ + virtual PN_stdfloat compute_distance_to(const LPoint3 &point) const; + + virtual void clear(DrawableRegion *clearable); +@@ -384,6 +388,7 @@ public: + + INLINE CPT(TransformState) get_external_transform() const; + INLINE CPT(TransformState) get_internal_transform() const; ++ INLINE CPT(TransformState) get_prev_internal_transform() const; + + RenderBuffer get_render_buffer(int buffer_type, const FrameBufferProperties &prop); + +@@ -473,6 +478,10 @@ protected: + // The current transform, as of the last call to set_state_and_transform(). + CPT(TransformState) _internal_transform; + ++ // The previous transform, as of the last call to the 3 parameter overload of ++ // set_state_and_transform() ++ CPT(TransformState) _prev_internal_transform; ++ + // The current TextureAttrib is a special case; we may further restrict it + // (according to graphics cards limits) or extend it (according to + // ColorScaleAttribs in effect) beyond what is specifically requested in the +diff --git a/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx b/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx +index 5cc7d8d..da726f6 100644 +--- a/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx ++++ b/panda/src/dxgsg9/dxGraphicsStateGuardian9.cxx +@@ -758,7 +758,7 @@ clear(DrawableRegion *clearable) { + return; + } + +- set_state_and_transform(RenderState::make_empty(), _internal_transform); ++ set_state_and_transform(RenderState::make_empty(), _internal_transform, _prev_internal_transform); + + D3DCOLOR color_clear_value = LColor_to_D3DCOLOR(clearable->get_clear_color()); + PN_stdfloat depth_clear_value = clearable->get_clear_depth(); +@@ -3027,7 +3027,8 @@ do_issue_shade_model() { + */ + void DXGraphicsStateGuardian9:: + set_state_and_transform(const RenderState *target, +- const TransformState *transform) { ++ const TransformState *transform, ++ const TransformState *prev_transform) { + #ifndef NDEBUG + if (gsg_cat.is_spam()) { + gsg_cat.spam() << "Setting GSG state to " << (void *)target << ":\n"; +@@ -3036,6 +3037,7 @@ set_state_and_transform(const RenderState *target, + #endif + _state_pcollector.add_level(1); + PStatTimer timer1(_draw_set_state_pcollector); ++ _prev_internal_transform = prev_transform; + + if (transform != _internal_transform) { + // PStatTimer timer(_draw_set_state_transform_pcollector); +@@ -3044,6 +3046,7 @@ set_state_and_transform(const RenderState *target, + do_issue_transform(); + } + ++ + if (target == _state_rs && (_state_mask | _inv_state_mask).is_all_on()) { + return; + } +@@ -3233,6 +3236,7 @@ set_state_and_transform(const RenderState *target, + _state_rs = _target_rs; + } + ++ + /** + * Called the first time a particular light has been bound to a given id + * within a frame, this should set up the associated hardware light with the +diff --git a/panda/src/dxgsg9/dxGraphicsStateGuardian9.h b/panda/src/dxgsg9/dxGraphicsStateGuardian9.h +index 4a51d21..b6afb00 100644 +--- a/panda/src/dxgsg9/dxGraphicsStateGuardian9.h ++++ b/panda/src/dxgsg9/dxGraphicsStateGuardian9.h +@@ -150,7 +150,9 @@ public: + INLINE static DWORD LColor_to_D3DCOLOR(const LColor &cLColor); + + virtual void set_state_and_transform(const RenderState *state, +- const TransformState *transform); ++ const TransformState *transform, ++ const TransformState *prev_transform); ++ + + bool check_dx_allocation (HRESULT result, int allocation_size, int attempts); + +diff --git a/panda/src/glstuff/glCgShaderContext_src.cxx b/panda/src/glstuff/glCgShaderContext_src.cxx +index c857948..5a0aa4e 100644 +--- a/panda/src/glstuff/glCgShaderContext_src.cxx ++++ b/panda/src/glstuff/glCgShaderContext_src.cxx +@@ -384,6 +384,7 @@ unbind() { + } + + /** ++ * + * This function gets called whenever the RenderState or TransformState has + * changed, but the Shader itself has not changed. It loads new values into + * the shader's parameters. +@@ -391,6 +392,7 @@ unbind() { + void CLP(CgShaderContext):: + set_state_and_transform(const RenderState *target_rs, + const TransformState *modelview_transform, ++ const TransformState *prev_modelview_transform, + const TransformState *projection_transform) { + + if (!valid()) { +@@ -404,6 +406,12 @@ set_state_and_transform(const RenderState *target_rs, + _modelview_transform = modelview_transform; + altered |= Shader::SSD_transform; + } ++ ++ if (_prev_modelview_transform != prev_modelview_transform) { ++ _prev_modelview_transform = prev_modelview_transform; ++ altered |= Shader::SSD_transform; ++ } ++ + if (_projection_transform != projection_transform) { + _projection_transform = projection_transform; + altered |= Shader::SSD_projection; +diff --git a/panda/src/glstuff/glCgShaderContext_src.h b/panda/src/glstuff/glCgShaderContext_src.h +index ae76989..c229444 100644 +--- a/panda/src/glstuff/glCgShaderContext_src.h ++++ b/panda/src/glstuff/glCgShaderContext_src.h +@@ -39,6 +39,7 @@ public: + + void set_state_and_transform(const RenderState *state, + const TransformState *modelview_transform, ++ const TransformState *prev_modelview_transform, + const TransformState *projection_transform); + + void issue_parameters(int altered) OVERRIDE; +@@ -77,6 +78,7 @@ private: + + WCPT(RenderState) _state_rs; + CPT(TransformState) _modelview_transform; ++ CPT(TransformState) _prev_modelview_transform; + CPT(TransformState) _projection_transform; + GLint _frame_number; + +diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +index 2a9ace7..1fb4fcd 100644 +--- a/panda/src/glstuff/glGraphicsStateGuardian_src.cxx ++++ b/panda/src/glstuff/glGraphicsStateGuardian_src.cxx +@@ -2869,7 +2869,8 @@ clear(DrawableRegion *clearable) { + + // XXX rdb: Is this line really necessary? Could we perhaps just reset the + // color write mask and other relevant attributes? +- set_state_and_transform(RenderState::make_empty(), _internal_transform); ++ set_state_and_transform(RenderState::make_empty(), _internal_transform, ++ _prev_internal_transform); + + int mask = 0; + +@@ -5889,7 +5890,8 @@ framebuffer_copy_to_ram(Texture *tex, int view, int z, + // Bug fix for RE, RE2, and VTX - need to disable texturing in order for + // glReadPixels() to work NOTE: reading the depth buffer is *much* slower + // than reading the color buffer +- set_state_and_transform(RenderState::make_empty(), _internal_transform); ++ set_state_and_transform(RenderState::make_empty(), _internal_transform, ++ _prev_internal_transform); + + int xo, yo, w, h; + dr->get_region_pixels(xo, yo, w, h); +@@ -9486,7 +9488,8 @@ end_bind_clip_planes() { + */ + void CLP(GraphicsStateGuardian):: + set_state_and_transform(const RenderState *target, +- const TransformState *transform) { ++ const TransformState *transform, ++ const TransformState *prev_transform) { + report_my_gl_errors(); + #ifndef NDEBUG + if (gsg_cat.is_spam()) { +@@ -9498,6 +9501,8 @@ set_state_and_transform(const RenderState *target, + _state_pcollector.add_level(1); + PStatGPUTimer timer1(this, _draw_set_state_pcollector); + ++ _prev_internal_transform = prev_transform; ++ + if (transform != _internal_transform) { + // PStatGPUTimer timer(this, _draw_set_state_transform_pcollector); + _transform_state_pcollector.add_level(1); +@@ -9529,7 +9534,7 @@ set_state_and_transform(const RenderState *target, + #endif + + if (_current_shader_context != NULL) { +- _current_shader_context->set_state_and_transform(target, transform, _projection_mat); ++ _current_shader_context->set_state_and_transform(target, transform, prev_transform, _projection_mat); + } + #endif + +diff --git a/panda/src/glstuff/glGraphicsStateGuardian_src.h b/panda/src/glstuff/glGraphicsStateGuardian_src.h +index c2909e3..b1267ba 100644 +--- a/panda/src/glstuff/glGraphicsStateGuardian_src.h ++++ b/panda/src/glstuff/glGraphicsStateGuardian_src.h +@@ -375,7 +375,8 @@ public: + INLINE int get_gl_version_minor() const; + + virtual void set_state_and_transform(const RenderState *state, +- const TransformState *transform); ++ const TransformState *transform, ++ const TransformState *prev_transform); + + void bind_fbo(GLuint fbo); + virtual bool get_supports_cg_profile(const string &name) const; +diff --git a/panda/src/glstuff/glShaderContext_src.cxx b/panda/src/glstuff/glShaderContext_src.cxx +index 708347a..a7759af 100644 +--- a/panda/src/glstuff/glShaderContext_src.cxx ++++ b/panda/src/glstuff/glShaderContext_src.cxx +@@ -769,6 +769,12 @@ reflect_uniform(int i, char *name_buffer, GLsizei name_buflen) { + : Shader::SMO_model_to_apiview; + bind._part[1] = Shader::SMO_identity; + ++ } else if (matrix_name == "PrevModelViewMatrix") { ++ bind._func = Shader::SMF_first; ++ bind._part[0] = inverse ? Shader::SMO_prev_apiview_to_model ++ : Shader::SMO_prev_model_to_apiview; ++ bind._part[1] = Shader::SMO_identity; ++ + } else if (matrix_name == "ProjectionMatrix") { + bind._func = Shader::SMF_first; + bind._part[0] = inverse ? Shader::SMO_apiclip_to_apiview +@@ -1789,6 +1795,7 @@ unbind() { + void CLP(ShaderContext):: + set_state_and_transform(const RenderState *target_rs, + const TransformState *modelview_transform, ++ const TransformState *prev_modelview_transform, + const TransformState *projection_transform) { + + // Find out which state properties have changed. +@@ -1798,6 +1805,11 @@ set_state_and_transform(const RenderState *target_rs, + _modelview_transform = modelview_transform; + altered |= Shader::SSD_transform; + } ++ if (_prev_modelview_transform != prev_modelview_transform) { ++ _prev_modelview_transform = prev_modelview_transform; ++ altered |= Shader::SSD_transform; ++ } ++ + if (_projection_transform != projection_transform) { + _projection_transform = projection_transform; + altered |= Shader::SSD_projection; +diff --git a/panda/src/glstuff/glShaderContext_src.h b/panda/src/glstuff/glShaderContext_src.h +index 4209ec6..b260f1c 100644 +--- a/panda/src/glstuff/glShaderContext_src.h ++++ b/panda/src/glstuff/glShaderContext_src.h +@@ -48,6 +48,7 @@ public: + + void set_state_and_transform(const RenderState *state, + const TransformState *modelview_transform, ++ const TransformState *prev_modelview_transform, + const TransformState *projection_transform); + + void issue_parameters(int altered); +@@ -69,6 +70,7 @@ private: + + WCPT(RenderState) _state_rs; + CPT(TransformState) _modelview_transform; ++ CPT(TransformState) _prev_modelview_transform; + CPT(TransformState) _projection_transform; + + /* +diff --git a/panda/src/gobj/shader.cxx b/panda/src/gobj/shader.cxx +index 2905d54..552f665 100644 +--- a/panda/src/gobj/shader.cxx ++++ b/panda/src/gobj/shader.cxx +@@ -390,6 +390,8 @@ cp_dependency(ShaderMatInput inp) { + (inp == SMO_view_to_model) || + (inp == SMO_model_to_apiview) || + (inp == SMO_apiview_to_model) || ++ (inp == SMO_prev_model_to_apiview) || ++ (inp == SMO_prev_apiview_to_model) || + (inp == SMO_view_to_world) || + (inp == SMO_world_to_view) || + (inp == SMO_view_x_to_view) || +diff --git a/panda/src/gobj/shader.h b/panda/src/gobj/shader.h +index dbdde6f..f517565 100644 +--- a/panda/src/gobj/shader.h ++++ b/panda/src/gobj/shader.h +@@ -201,6 +201,10 @@ public: + // Hack for text rendering. Don't use in user shaders. + SMO_tex_is_alpha_i, + ++ // Previous frame transform ++ SMO_prev_apiview_to_model, ++ SMO_prev_model_to_apiview, ++ + SMO_INVALID + }; + +diff --git a/panda/src/gobj/shaderContext.h b/panda/src/gobj/shaderContext.h +index 2b356ce..af2c81d 100644 +--- a/panda/src/gobj/shaderContext.h ++++ b/panda/src/gobj/shaderContext.h +@@ -32,7 +32,7 @@ class EXPCL_PANDA_GOBJ ShaderContext: public SavedContext { + public: + INLINE ShaderContext(Shader *se); + +- INLINE virtual void set_state_and_transform(const RenderState *, const TransformState *, const TransformState*) {}; ++ INLINE virtual void set_state_and_transform(const RenderState *, const TransformState *, const TransformState *, const TransformState *) {}; + + INLINE virtual bool valid() { return false; } + INLINE virtual void bind() {}; +diff --git a/panda/src/gsgbase/graphicsStateGuardianBase.h b/panda/src/gsgbase/graphicsStateGuardianBase.h +index 2fecf30..ace7f38 100644 +--- a/panda/src/gsgbase/graphicsStateGuardianBase.h ++++ b/panda/src/gsgbase/graphicsStateGuardianBase.h +@@ -170,6 +170,10 @@ public: + virtual void set_state_and_transform(const RenderState *state, + const TransformState *transform)=0; + ++ virtual void set_state_and_transform(const RenderState *state, ++ const TransformState *transform, ++ const TransformState *prev_transform)=0; ++ + // This function may only be called during a render traversal; it will + // compute the distance to the indicated point, assumed to be in eye + // coordinates, from the camera plane. This is a virtual function because +diff --git a/panda/src/parametrics/ropeNode.cxx b/panda/src/parametrics/ropeNode.cxx +index c799595..6705f66 100644 +--- a/panda/src/parametrics/ropeNode.cxx ++++ b/panda/src/parametrics/ropeNode.cxx +@@ -334,7 +334,8 @@ render_thread(CullTraverser *trav, CullTraverserData &data, + + CullableObject *object = + new CullableObject(geom, state, +- data.get_internal_transform(trav)); ++ data.get_internal_transform(trav), ++ data.get_prev_internal_transform(trav)); + trav->get_cull_handler()->record_object(object, trav); + } + +@@ -380,7 +381,8 @@ render_tape(CullTraverser *trav, CullTraverserData &data, + + CullableObject *object = + new CullableObject(geom, state, +- data.get_internal_transform(trav)); ++ data.get_internal_transform(trav), ++ data.get_prev_internal_transform(trav)); + trav->get_cull_handler()->record_object(object, trav); + } + +@@ -433,7 +435,8 @@ render_billboard(CullTraverser *trav, CullTraverserData &data, + + CullableObject *object = + new CullableObject(geom, state, +- data.get_internal_transform(trav)); ++ data.get_internal_transform(trav), ++ data.get_prev_internal_transform(trav)); + trav->get_cull_handler()->record_object(object, trav); + } + +@@ -494,7 +497,8 @@ render_tube(CullTraverser *trav, CullTraverserData &data, + + CullableObject *object = + new CullableObject(geom, state, +- data.get_internal_transform(trav)); ++ data.get_internal_transform(trav), ++ data.get_prev_internal_transform(trav)); + trav->get_cull_handler()->record_object(object, trav); + } + +diff --git a/panda/src/parametrics/sheetNode.cxx b/panda/src/parametrics/sheetNode.cxx +index 34496bc..f745d6e 100644 +--- a/panda/src/parametrics/sheetNode.cxx ++++ b/panda/src/parametrics/sheetNode.cxx +@@ -331,7 +331,8 @@ render_sheet(CullTraverser *trav, CullTraverserData &data, + + CullableObject *object = + new CullableObject(geom, state, +- data.get_internal_transform(trav)); ++ data.get_internal_transform(trav), ++ data.get_prev_internal_transform(trav)); + trav->get_cull_handler()->record_object(object, trav); + } + +diff --git a/panda/src/pgraph/config_pgraph.cxx b/panda/src/pgraph/config_pgraph.cxx +index d5427a6..1360bbf 100644 +--- a/panda/src/pgraph/config_pgraph.cxx ++++ b/panda/src/pgraph/config_pgraph.cxx +@@ -369,6 +369,13 @@ ConfigVariableBool allow_live_flatten + "only has an effect when Panda is not compiled for a release " + "build.")); + ++ ++ConfigVariableBool always_store_prev_transform ++("always-store-prev-transform", false, ++ PRC_DESC("When setting this to true, the previous transform is kept on " ++ "nodes. This makes NodePath::set_pos and all other transform changes " ++ "behave like their fluid variants, e.g. set_fluid_pos.")); ++ + /** + * Initializes the library. This must be called at least once before any of + * the functions or classes in this library can be used. Normally it will be +diff --git a/panda/src/pgraph/config_pgraph.h b/panda/src/pgraph/config_pgraph.h +index 988c9b1..4ba1ad5 100644 +--- a/panda/src/pgraph/config_pgraph.h ++++ b/panda/src/pgraph/config_pgraph.h +@@ -72,6 +72,7 @@ extern ConfigVariableList load_file_type; + extern ConfigVariableString default_model_extension; + + extern ConfigVariableBool allow_live_flatten; ++extern ConfigVariableBool always_store_prev_transform; + + extern EXPCL_PANDA_PGRAPH void init_libpgraph(); + +diff --git a/panda/src/pgraph/cullTraverser.I b/panda/src/pgraph/cullTraverser.I +index 180b94b..8441ba0 100644 +--- a/panda/src/pgraph/cullTraverser.I ++++ b/panda/src/pgraph/cullTraverser.I +@@ -227,6 +227,8 @@ do_traverse(CullTraverserData &data) { + + data.apply_transform_and_state(this); + ++#if defined(HAVE_CG) || defined(SUPPORT_FIXED_FUNCTION) ++ // Fog is only supported with the shader generator or the FFP. + const FogAttrib *fog = (const FogAttrib *) + node_reader->get_state()->get_attrib(FogAttrib::get_class_slot()); + +@@ -237,6 +239,7 @@ do_traverse(CullTraverserData &data) { + // it. + fog->get_fog()->adjust_to_camera(get_camera_transform()); + } ++#endif + + if (fancy_bits & PandaNode::FB_cull_callback) { + PandaNode *node = data.node(); +diff --git a/panda/src/pgraph/cullTraverser.cxx b/panda/src/pgraph/cullTraverser.cxx +index 5d08b4e..df8ae99 100644 +--- a/panda/src/pgraph/cullTraverser.cxx ++++ b/panda/src/pgraph/cullTraverser.cxx +@@ -128,7 +128,9 @@ traverse(const NodePath &root) { + // Store this pointer in this + set_portal_clipper(&portal_viewer); + +- CullTraverserData data(root, TransformState::make_identity(), ++ CullTraverserData data(root, ++ TransformState::make_identity(), ++ TransformState::make_identity(), + _initial_state, _view_frustum, + _current_thread); + +@@ -142,13 +144,17 @@ traverse(const NodePath &root) { + // Render the frustum relative to the cull center. + NodePath cull_center = _scene_setup->get_cull_center(); + CPT(TransformState) transform = cull_center.get_transform(root); ++ CPT(TransformState) prev_transform = cull_center.get_prev_transform(root); + + CullTraverserData my_data(data, portal_viewer._previous); + my_data._net_transform = my_data._net_transform->compose(transform); ++ my_data._prev_net_transform = my_data._prev_net_transform->compose(prev_transform); + traverse(my_data); + + } else { +- CullTraverserData data(root, TransformState::make_identity(), ++ CullTraverserData data(root, ++ TransformState::make_identity(), ++ TransformState::make_identity(), + _initial_state, _view_frustum, + _current_thread); + +diff --git a/panda/src/pgraph/cullTraverserData.I b/panda/src/pgraph/cullTraverserData.I +index 4367d16..7c1a79f 100644 +--- a/panda/src/pgraph/cullTraverserData.I ++++ b/panda/src/pgraph/cullTraverserData.I +@@ -17,12 +17,14 @@ + INLINE CullTraverserData:: + CullTraverserData(const NodePath &start, + const TransformState *net_transform, ++ const TransformState *prev_net_transform, + const RenderState *state, + GeometricBoundingVolume *view_frustum, + Thread *current_thread) : + _node_path(start), + _node_reader(start.node(), current_thread), + _net_transform(net_transform), ++ _prev_net_transform(prev_net_transform), + _state(state), + _view_frustum(view_frustum), + _cull_planes(CullPlanes::make_empty()), +@@ -42,6 +44,7 @@ CullTraverserData(const CullTraverserData ©) : + _node_path(copy._node_path), + _node_reader(copy._node_reader), + _net_transform(copy._net_transform), ++ _prev_net_transform(copy._prev_net_transform), + _state(copy._state), + _view_frustum(copy._view_frustum), + _cull_planes(copy._cull_planes), +@@ -58,6 +61,7 @@ operator = (const CullTraverserData ©) { + _node_path = copy._node_path; + _node_reader = copy._node_reader; + _net_transform = copy._net_transform; ++ _prev_net_transform = copy._prev_net_transform; + _state = copy._state; + _view_frustum = copy._view_frustum; + _cull_planes = copy._cull_planes; +@@ -74,6 +78,7 @@ CullTraverserData(const CullTraverserData &parent, PandaNode *child) : + _node_path(parent._node_path, child), + _node_reader(child, parent._node_reader.get_current_thread()), + _net_transform(parent._net_transform), ++ _prev_net_transform(parent._prev_net_transform), + _state(parent._state), + _view_frustum(parent._view_frustum), + _cull_planes(parent._cull_planes), +@@ -134,6 +139,14 @@ INLINE CPT(TransformState) CullTraverserData:: + get_internal_transform(const CullTraverser *trav) const { + return trav->get_scene()->get_cs_world_transform()->compose(_net_transform); + } ++/** ++ * Returns the previous internal transform: the modelview transform in the GSG's ++ * internal coordinate system in the last frame. ++ */ ++INLINE CPT(TransformState) CullTraverserData:: ++get_prev_internal_transform(const CullTraverser *trav) const { ++ return trav->get_scene()->get_prev_cs_world_transform()->compose(_prev_net_transform); ++} + + /** + * Returns the net transform: the relative transform from root of the scene +@@ -145,6 +158,15 @@ get_net_transform(const CullTraverser *) const { + } + + /** ++ * Returns the previous net transform: the relative transform from root of the scene ++ * graph to the current node in the last frame. ++ */ ++INLINE const TransformState *CullTraverserData:: ++get_prev_net_transform(const CullTraverser *) const { ++ return _prev_net_transform; ++} ++ ++/** + * Returns true if the current node is within the view frustum, false + * otherwise. If the node's bounding volume falls completely within the view + * frustum, this will also reset the view frustum pointer, saving some work +diff --git a/panda/src/pgraph/cullTraverserData.cxx b/panda/src/pgraph/cullTraverserData.cxx +index 186869c..e67d45d 100644 +--- a/panda/src/pgraph/cullTraverserData.cxx ++++ b/panda/src/pgraph/cullTraverserData.cxx +@@ -47,6 +47,7 @@ apply_transform_and_state(CullTraverser *trav) { + _node_reader.compose_draw_mask(_draw_mask); + + apply_transform_and_state(trav, _node_reader.get_transform(), ++ _node_reader.get_prev_transform(), + node_state, _node_reader.get_effects(), + _node_reader.get_off_clip_planes()); + } +@@ -58,6 +59,7 @@ apply_transform_and_state(CullTraverser *trav) { + void CullTraverserData:: + apply_transform_and_state(CullTraverser *trav, + CPT(TransformState) node_transform, ++ CPT(TransformState) prev_node_transform, + CPT(RenderState) node_state, + CPT(RenderEffects) node_effects, + const RenderAttrib *off_clip_planes) { +@@ -96,6 +98,12 @@ apply_transform_and_state(CullTraverser *trav, + } + } + ++ if (_net_transform == _prev_net_transform && node_transform == prev_node_transform) { ++ _prev_net_transform = _net_transform; ++ } else if (!prev_node_transform->is_identity()) { ++ _prev_net_transform = _prev_net_transform->compose(prev_node_transform); ++ } ++ + _state = _state->compose(node_state); + + if (clip_plane_cull) { +diff --git a/panda/src/pgraph/cullTraverserData.h b/panda/src/pgraph/cullTraverserData.h +index 80d523f..30b8d68 100644 +--- a/panda/src/pgraph/cullTraverserData.h ++++ b/panda/src/pgraph/cullTraverserData.h +@@ -41,6 +41,7 @@ class EXPCL_PANDA_PGRAPH CullTraverserData { + public: + INLINE CullTraverserData(const NodePath &start, + const TransformState *net_transform, ++ const TransformState *prev_net_transform, + const RenderState *state, + GeometricBoundingVolume *view_frustum, + Thread *current_thread); +@@ -60,7 +61,9 @@ public: + PUBLISHED: + INLINE CPT(TransformState) get_modelview_transform(const CullTraverser *trav) const; + INLINE CPT(TransformState) get_internal_transform(const CullTraverser *trav) const; ++ INLINE CPT(TransformState) get_prev_internal_transform(const CullTraverser *trav) const; + INLINE const TransformState *get_net_transform(const CullTraverser *trav) const; ++ INLINE const TransformState *get_prev_net_transform(const CullTraverser *trav) const; + + INLINE bool is_in_view(const DrawMask &camera_mask); + INLINE bool is_this_node_hidden(const DrawMask &camera_mask) const; +@@ -68,6 +71,7 @@ PUBLISHED: + void apply_transform_and_state(CullTraverser *trav); + void apply_transform_and_state(CullTraverser *trav, + CPT(TransformState) node_transform, ++ CPT(TransformState) prev_node_transform, + CPT(RenderState) node_state, + CPT(RenderEffects) node_effects, + const RenderAttrib *off_clip_planes); +@@ -76,6 +80,7 @@ public: + WorkingNodePath _node_path; + PandaNodePipelineReader _node_reader; + CPT(TransformState) _net_transform; ++ CPT(TransformState) _prev_net_transform; + CPT(RenderState) _state; + PT(GeometricBoundingVolume) _view_frustum; + CPT(CullPlanes) _cull_planes; +diff --git a/panda/src/pgraph/cullableObject.I b/panda/src/pgraph/cullableObject.I +index 2424965..5fbe70c 100644 +--- a/panda/src/pgraph/cullableObject.I ++++ b/panda/src/pgraph/cullableObject.I +@@ -23,6 +23,24 @@ CullableObject() { + + /** + * Creates a CullableObject based the indicated geom, with the indicated ++ * render state, transform and previous transform. ++ */ ++INLINE CullableObject:: ++CullableObject(const Geom *geom, const RenderState *state, ++ const TransformState *internal_transform, ++ const TransformState *prev_internal_transform) : ++ _geom(geom), ++ _state(state), ++ _internal_transform(internal_transform), ++ _prev_internal_transform(prev_internal_transform) ++{ ++#ifdef DO_MEMORY_USAGE ++ MemoryUsage::update_type(this, get_class_type()); ++#endif ++} ++ ++/** ++ * Creates a CullableObject based the indicated geom, with the indicated + * render state and transform. + */ + INLINE CullableObject:: +@@ -30,7 +48,8 @@ CullableObject(const Geom *geom, const RenderState *state, + const TransformState *internal_transform) : + _geom(geom), + _state(state), +- _internal_transform(internal_transform) ++ _internal_transform(internal_transform), ++ _prev_internal_transform(internal_transform) + { + #ifdef DO_MEMORY_USAGE + MemoryUsage::update_type(this, get_class_type()); +@@ -46,7 +65,8 @@ CullableObject(const CullableObject ©) : + _munger(copy._munger), + _munged_data(copy._munged_data), + _state(copy._state), +- _internal_transform(copy._internal_transform) ++ _internal_transform(copy._internal_transform), ++ _prev_internal_transform(copy._prev_internal_transform) + { + #ifdef DO_MEMORY_USAGE + MemoryUsage::update_type(this, get_class_type()); +@@ -63,6 +83,7 @@ operator = (const CullableObject ©) { + _munged_data = copy._munged_data; + _state = copy._state; + _internal_transform = copy._internal_transform; ++ _prev_internal_transform = copy._prev_internal_transform; + _draw_callback = copy._draw_callback; + } + +@@ -75,7 +96,7 @@ draw(GraphicsStateGuardianBase *gsg, bool force, Thread *current_thread) { + if (_draw_callback != (CallbackObject *)NULL) { + // It has a callback associated. + gsg->clear_before_callback(); +- gsg->set_state_and_transform(_state, _internal_transform); ++ gsg->set_state_and_transform(_state, _internal_transform, _prev_internal_transform); + GeomDrawCallbackData cbdata(this, gsg, force); + _draw_callback->do_callback(&cbdata); + if (cbdata.get_lost_state()) { +@@ -85,7 +106,7 @@ draw(GraphicsStateGuardianBase *gsg, bool force, Thread *current_thread) { + // Now the callback has taken care of drawing. + } else { + nassertv(_geom != (Geom *)NULL); +- gsg->set_state_and_transform(_state, _internal_transform); ++ gsg->set_state_and_transform(_state, _internal_transform, _prev_internal_transform); + draw_inline(gsg, force, current_thread); + } + } +diff --git a/panda/src/pgraph/cullableObject.h b/panda/src/pgraph/cullableObject.h +index 82fd0df..f450cbc 100644 +--- a/panda/src/pgraph/cullableObject.h ++++ b/panda/src/pgraph/cullableObject.h +@@ -49,6 +49,10 @@ public: + INLINE CullableObject(const Geom *geom, const RenderState *state, + const TransformState *internal_transform); + ++ INLINE CullableObject(const Geom *geom, const RenderState *state, ++ const TransformState *internal_transform, ++ const TransformState *prev_internal_transform); ++ + INLINE CullableObject(const CullableObject ©); + INLINE void operator = (const CullableObject ©); + +@@ -74,6 +78,7 @@ public: + CPT(GeomVertexData) _munged_data; + CPT(RenderState) _state; + CPT(TransformState) _internal_transform; ++ CPT(TransformState) _prev_internal_transform; + PT(CallbackObject) _draw_callback; + + private: +diff --git a/panda/src/pgraph/geomNode.cxx b/panda/src/pgraph/geomNode.cxx +index bfbd24f..5ea321e 100644 +--- a/panda/src/pgraph/geomNode.cxx ++++ b/panda/src/pgraph/geomNode.cxx +@@ -512,7 +512,6 @@ add_for_draw(CullTraverser *trav, CullTraverserData &data) { + Geoms geoms = get_geoms(trav->get_current_thread()); + int num_geoms = geoms.get_num_geoms(); + trav->_geoms_pcollector.add_level(num_geoms); +- CPT(TransformState) internal_transform = data.get_internal_transform(trav); + + for (int i = 0; i < num_geoms; i++) { + const Geom *geom = geoms.get_geom(i); +@@ -558,7 +557,9 @@ add_for_draw(CullTraverser *trav, CullTraverserData &data) { + } + + CullableObject *object = +- new CullableObject(geom, state, internal_transform); ++ new CullableObject(geom, state, ++ data.get_internal_transform(trav), ++ data.get_prev_internal_transform(trav)); + trav->get_cull_handler()->record_object(object, trav); + } + } +diff --git a/panda/src/pgraph/nodePath.cxx b/panda/src/pgraph/nodePath.cxx +index c6bfff1..ecb8936 100644 +--- a/panda/src/pgraph/nodePath.cxx ++++ b/panda/src/pgraph/nodePath.cxx +@@ -906,7 +906,9 @@ void NodePath:: + set_pos(const LVecBase3 &pos) { + nassertv_always(!is_empty()); + set_transform(get_transform()->set_pos(pos)); +- node()->reset_prev_transform(); ++ if (!always_store_prev_transform) { ++ node()->reset_prev_transform(); ++ } + } + + void NodePath:: +@@ -1173,7 +1175,10 @@ set_pos_hpr(const LVecBase3 &pos, const LVecBase3 &hpr) { + transform = TransformState::make_pos_hpr_scale_shear + (pos, hpr, transform->get_scale(), transform->get_shear()); + set_transform(transform); +- node()->reset_prev_transform(); ++ ++ if (!always_store_prev_transform) { ++ node()->reset_prev_transform(); ++ } + } + + /** +@@ -1187,7 +1192,10 @@ set_pos_quat(const LVecBase3 &pos, const LQuaternion &quat) { + transform = TransformState::make_pos_quat_scale_shear + (pos, quat, transform->get_scale(), transform->get_shear()); + set_transform(transform); +- node()->reset_prev_transform(); ++ ++ if (!always_store_prev_transform) { ++ node()->reset_prev_transform(); ++ } + } + + /** +@@ -1226,7 +1234,9 @@ set_pos_hpr_scale(const LVecBase3 &pos, const LVecBase3 &hpr, + nassertv_always(!is_empty()); + set_transform(TransformState::make_pos_hpr_scale + (pos, hpr, scale)); +- node()->reset_prev_transform(); ++ if (!always_store_prev_transform) { ++ node()->reset_prev_transform(); ++ } + } + + /** +@@ -1239,7 +1249,9 @@ set_pos_quat_scale(const LVecBase3 &pos, const LQuaternion &quat, + nassertv_always(!is_empty()); + set_transform(TransformState::make_pos_quat_scale + (pos, quat, scale)); +- node()->reset_prev_transform(); ++ if (!always_store_prev_transform) { ++ node()->reset_prev_transform(); ++ } + } + + /** +@@ -1252,7 +1264,9 @@ set_pos_hpr_scale_shear(const LVecBase3 &pos, const LVecBase3 &hpr, + nassertv_always(!is_empty()); + set_transform(TransformState::make_pos_hpr_scale_shear + (pos, hpr, scale, shear)); +- node()->reset_prev_transform(); ++ if (!always_store_prev_transform) { ++ node()->reset_prev_transform(); ++ } + } + + /** +@@ -1265,7 +1279,9 @@ set_pos_quat_scale_shear(const LVecBase3 &pos, const LQuaternion &quat, + nassertv_always(!is_empty()); + set_transform(TransformState::make_pos_quat_scale_shear + (pos, quat, scale, shear)); +- node()->reset_prev_transform(); ++ if (!always_store_prev_transform) { ++ node()->reset_prev_transform(); ++ } + } + + /** +@@ -1275,7 +1291,9 @@ void NodePath:: + set_mat(const LMatrix4 &mat) { + nassertv_always(!is_empty()); + set_transform(TransformState::make_mat(mat)); +- node()->reset_prev_transform(); ++ if (!always_store_prev_transform) { ++ node()->reset_prev_transform(); ++ } + } + + /** +@@ -1334,7 +1352,9 @@ set_pos(const NodePath &other, const LVecBase3 &pos) { + // If we didn't have a componentwise transform already, never mind. + set_transform(other, rel_transform->set_pos(pos)); + } +- node()->reset_prev_transform(); ++ if (!always_store_prev_transform) { ++ node()->reset_prev_transform(); ++ } + } + + void NodePath:: +@@ -1712,7 +1732,9 @@ set_pos_hpr(const NodePath &other, const LVecBase3 &pos, + // If we didn't have a componentwise transform already, never mind. + set_transform(other, TransformState::make_pos_hpr_scale_shear + (pos, hpr, rel_transform->get_scale(), rel_transform->get_shear())); +- node()->reset_prev_transform(); ++ if (!always_store_prev_transform) { ++ node()->reset_prev_transform(); ++ } + } + } + +@@ -1747,7 +1769,9 @@ set_pos_quat(const NodePath &other, const LVecBase3 &pos, + // If we didn't have a componentwise transform already, never mind. + set_transform(other, TransformState::make_pos_quat_scale_shear + (pos, quat, rel_transform->get_scale(), rel_transform->get_shear())); +- node()->reset_prev_transform(); ++ if (!always_store_prev_transform) { ++ node()->reset_prev_transform(); ++ } + } + } + +@@ -1797,7 +1821,9 @@ set_pos_hpr_scale(const NodePath &other, + nassertv_always(!is_empty()); + set_transform(other, TransformState::make_pos_hpr_scale + (pos, hpr, scale)); +- node()->reset_prev_transform(); ++ if (!always_store_prev_transform) { ++ node()->reset_prev_transform(); ++ } + } + + /** +@@ -1811,7 +1837,9 @@ set_pos_quat_scale(const NodePath &other, + nassertv_always(!is_empty()); + set_transform(other, TransformState::make_pos_quat_scale + (pos, quat, scale)); +- node()->reset_prev_transform(); ++ if (!always_store_prev_transform) { ++ node()->reset_prev_transform(); ++ } + } + + /** +@@ -1825,7 +1853,9 @@ set_pos_hpr_scale_shear(const NodePath &other, + nassertv_always(!is_empty()); + set_transform(other, TransformState::make_pos_hpr_scale_shear + (pos, hpr, scale, shear)); +- node()->reset_prev_transform(); ++ if (!always_store_prev_transform) { ++ node()->reset_prev_transform(); ++ } + } + + /** +@@ -1839,7 +1869,9 @@ set_pos_quat_scale_shear(const NodePath &other, + nassertv_always(!is_empty()); + set_transform(other, TransformState::make_pos_quat_scale_shear + (pos, quat, scale, shear)); +- node()->reset_prev_transform(); ++ if (!always_store_prev_transform) { ++ node()->reset_prev_transform(); ++ } + } + + /** +@@ -1864,7 +1896,9 @@ void NodePath:: + set_mat(const NodePath &other, const LMatrix4 &mat) { + nassertv_always(!is_empty()); + set_transform(other, TransformState::make_mat(mat)); +- node()->reset_prev_transform(); ++ if (!always_store_prev_transform) { ++ node()->reset_prev_transform(); ++ } + } + + /** +diff --git a/panda/src/pgraph/nodePath.h b/panda/src/pgraph/nodePath.h +index 729bcf3..a9a8233 100644 +--- a/panda/src/pgraph/nodePath.h ++++ b/panda/src/pgraph/nodePath.h +@@ -66,50 +66,73 @@ class ShaderInput; + * A NodePath is the fundamental unit of high-level interaction with the scene + * graph. It encapsulates the complete path down to a node from some other + * node, usually the root of the scene graph. This is used to resolve +- * ambiguities associated with instancing. NodePath also contains a number of +- * handy high-level methods for common scene-graph manipulations, such as +- * reparenting, and common state changes, such as repositioning. There are +- * also a number of NodePath methods for finding nodes deep within the tree by +- * name or by type. These take a path string, which at its simplest consists +- * of a series of node names separated by slashes, like a directory pathname. ++ * ambiguities associated with instancing. ++ * ++ * NodePath also contains a number of handy high-level methods for common ++ * scene-graph manipulations, such as reparenting, and common state changes, ++ * such as repositioning. There are also a number of NodePath methods for ++ * finding nodes deep within the tree by name or by type. These take a path ++ * string, which at its simplest consists of a series of node names separated ++ * by slashes, like a directory pathname. ++ * + * Each component of the path string may optionally consist of one of the +- * following special names, instead of a node name: * -- matches +- * exactly one node, with any name. ** -- matches any sequence of +- * zero or more nodes. +typename -- matches any node that is or derives from +- * the given type. -typename -- matches any node that is the given type +- * exactly. =tag -- matches any node that has the indicated tag. +- * =tag=value -- matches any node whose tag matches the indicated value. ++ * following special names, instead of a node name: ++ * ++ * * -- matches exactly one node, with any name. ++ * ** -- matches any sequence of zero or more nodes. ++ * +typename -- matches any node that is or derives from the given type. ++ * -typename -- matches any node that is the given type exactly. ++ * =tag -- matches any node that has the indicated tag. ++ * =tag=value -- matches any node whose tag matches the indicated value. ++ * + * Furthermore, a node name may itself contain standard filename globbing + * characters, like *, ?, and [a-z], that will be accepted as a partial match. + * (In fact, the '*' special name may be seen as just a special case of this.) + * The globbing characters may not be used with the typename matches or with + * tag matches, but they may be used to match a tag's value in the =tag=value +- * syntax. The special characters "@@", appearing at the beginning of a node ++ * syntax. ++ * ++ * The special characters "@@", appearing at the beginning of a node + * name, indicate a stashed node. Normally, stashed nodes are not returned by + * a find (but see the special flags, below), but a stashed node may be found + * if it is explicitly named with its leading @@ characters. By extension, +- * "@@*" may be used to identify any stashed node. Examples: "roomgraph" will +- * look for a node named "graph", which is a child of an unnamed node, which +- * is a child of a node named "room", which is a child of the starting path. +- * "**red*" will look for any node anywhere in the tree (below the starting +- * path) with a name that begins with "red". "**+PartBundleNode**head" will +- * look for a node named "head", somewhere below a PartBundleNode anywhere in +- * the tree. The search is always potentially ambiguous, even if the special ++ * "@@*" may be used to identify any stashed node. ++ * ++ * Examples: ++ * ++ * "roomgraph" will look for a node named "graph", which is a child ++ * of an unnamed node, which is a child of a node named "room", which is a ++ * child of the starting path. ++ * ++ * "**red*" will look for any node anywhere in the tree (below the starting ++ * path) with a name that begins with "red". ++ * ++ * "**+PartBundleNode**head" will look for a node named "head", somewhere ++ * below a PartBundleNode anywhere in the tree. ++ * ++ * The search is always potentially ambiguous, even if the special + * wildcard operators are not used, because there may be multiple nodes in the + * tree with the same name. In general, in the case of an ambiguity, the + * shortest path is preferred; when a method (such as extend_by) must choose + * only only one of several possible paths, it will choose the shortest + * available; on the other hand, when a method (such as find_all_matches) is + * to return all of the matching paths, it will sort them so that the shortest +- * paths appear first in the output. Special flags. The entire string may +- * optionally be followed by the ";" character, followed by one or more of the +- * following special control flags, with no intervening spaces or punctuation: +- * -h Do not return hidden nodes. +h Do return hidden nodes. -s Do +- * not return stashed nodes unless explicitly referenced with @@. +s Return +- * stashed nodes even without any explicit @@ characters. -i Node name +- * comparisons are not case insensitive: case must match exactly. +i Node +- * name comparisons are case insensitive: case is not important. This affects +- * matches against the node name only; node type and tag strings are always ++ * paths appear first in the output. ++ * ++ * Special flags: ++ * ++ * The entire string may optionally be followed by the ";" character, followed ++ * by one or more of the following special control flags, with no intervening ++ * spaces or punctuation: ++ * ++ * -h Do not return hidden nodes. ++ * +h Do return hidden nodes. ++ * -s Do not return stashed nodes unless explicitly referenced with @@. ++ * +s Return stashed nodes even without any explicit @@ characters. ++ * -i Node name comparisons are not case insensitive: case must match exactly. ++ * +i Node name comparisons are case insensitive: case is not important. ++ * ++ * This affects matches against the node name only; node type and tag strings are always + * case sensitive. The default flags are +h-s-i. + */ + +diff --git a/panda/src/pgraph/occluderNode.cxx b/panda/src/pgraph/occluderNode.cxx +index c1a4c4d..83695fb 100644 +--- a/panda/src/pgraph/occluderNode.cxx ++++ b/panda/src/pgraph/occluderNode.cxx +@@ -142,14 +142,16 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { + // draw a visualization, a checkerboard-textured polygon. + CullableObject *occluder_viz = + new CullableObject(get_occluder_viz(trav, data), get_occluder_viz_state(trav, data), +- data.get_internal_transform(trav)); ++ data.get_internal_transform(trav), ++ data.get_prev_internal_transform(trav)); + trav->get_cull_handler()->record_object(occluder_viz, trav); + + // Also get the frame. + nassertr(_frame_viz != (Geom *)NULL, false); + CullableObject *frame_viz = + new CullableObject(_frame_viz, get_frame_viz_state(trav, data), +- data.get_internal_transform(trav)); ++ data.get_internal_transform(trav), ++ data.get_prev_internal_transform(trav)); + trav->get_cull_handler()->record_object(frame_viz, trav); + + // Now carry on to render our child nodes. +diff --git a/panda/src/pgraph/planeNode.cxx b/panda/src/pgraph/planeNode.cxx +index c096eae..a643862 100644 +--- a/panda/src/pgraph/planeNode.cxx ++++ b/panda/src/pgraph/planeNode.cxx +@@ -141,7 +141,8 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { + + CullableObject *plane_viz = + new CullableObject(get_viz(trav, data), data._state, +- data.get_internal_transform(trav)); ++ data.get_internal_transform(trav), ++ data.get_prev_internal_transform(trav)); + trav->get_cull_handler()->record_object(plane_viz, trav); + + // Now carry on to render our child nodes. +diff --git a/panda/src/pgraph/portalNode.cxx b/panda/src/pgraph/portalNode.cxx +index d70eacd..3e360fd 100644 +--- a/panda/src/pgraph/portalNode.cxx ++++ b/panda/src/pgraph/portalNode.cxx +@@ -228,6 +228,7 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { + + // Get the net trasform of the _cell_out as seen from the camera. + CPT(TransformState) cell_transform = _cell_out.get_net_transform(); ++ CPT(TransformState) prev_cell_transform = _cell_out.get_net_prev_transform(); + CPT(TransformState) frustum_transform = cell_transform ->invert_compose(portal_viewer->_scene_setup->get_cull_center().get_net_transform()); + + // transform to _cell_out space +@@ -283,6 +284,7 @@ cull_callback(CullTraverser *trav, CullTraverserData &data) { + + CullTraverserData next_data(_cell_out, + cell_transform, ++ prev_cell_transform, + next_state, new_bh, + current_thread); + next_data._portal_depth = data._portal_depth + 1; +diff --git a/panda/src/pgraph/sceneSetup.I b/panda/src/pgraph/sceneSetup.I +index 425c538..1b150ff 100644 +--- a/panda/src/pgraph/sceneSetup.I ++++ b/panda/src/pgraph/sceneSetup.I +@@ -25,6 +25,7 @@ SceneSetup() { + _world_transform = TransformState::make_identity(); + _cs_transform = TransformState::make_identity(); + _cs_world_transform = TransformState::make_identity(); ++ _prev_cs_world_transform = TransformState::make_identity(); + } + + /** +@@ -268,3 +269,21 @@ INLINE const TransformState *SceneSetup:: + get_cs_world_transform() const { + return _cs_world_transform; + } ++ ++/** ++ * Specifies the position from the starting node relative to the camera, in ++ * the GSG's internal coordinate system. ++ */ ++INLINE void SceneSetup:: ++set_prev_cs_world_transform(const TransformState *prev_cs_world_transform) { ++ _prev_cs_world_transform = prev_cs_world_transform; ++} ++ ++/** ++ * Returns the position from the starting node relative to the camera, in the ++ * GSG's internal coordinate system. ++ */ ++INLINE const TransformState *SceneSetup:: ++get_prev_cs_world_transform() const { ++ return _prev_cs_world_transform; ++} +diff --git a/panda/src/pgraph/sceneSetup.h b/panda/src/pgraph/sceneSetup.h +index d2ad0c0..72c0599 100644 +--- a/panda/src/pgraph/sceneSetup.h ++++ b/panda/src/pgraph/sceneSetup.h +@@ -74,6 +74,9 @@ PUBLISHED: + INLINE void set_cs_world_transform(const TransformState *cs_world_transform); + INLINE const TransformState *get_cs_world_transform() const; + ++ INLINE void set_prev_cs_world_transform(const TransformState *prev_cs_world_transform); ++ INLINE const TransformState *get_prev_cs_world_transform() const; ++ + private: + DisplayRegion *_display_region; + int _viewport_width; +@@ -88,6 +91,7 @@ private: + CPT(TransformState) _world_transform; + CPT(TransformState) _cs_transform; + CPT(TransformState) _cs_world_transform; ++ CPT(TransformState) _prev_cs_world_transform; + + public: + static TypeHandle get_class_type() { +diff --git a/panda/src/pgraphnodes/callbackNode.cxx b/panda/src/pgraphnodes/callbackNode.cxx +index 17424d5..2ced441 100644 +--- a/panda/src/pgraphnodes/callbackNode.cxx ++++ b/panda/src/pgraphnodes/callbackNode.cxx +@@ -133,7 +133,8 @@ add_for_draw(CullTraverser *trav, CullTraverserData &data) { + if (cbobj != (CallbackObject *)NULL) { + CullableObject *object = + new CullableObject(NULL, data._state, +- data.get_internal_transform(trav)); ++ data.get_internal_transform(trav), ++ data.get_prev_internal_transform(trav)); + object->set_draw_callback(cbobj); + trav->get_cull_handler()->record_object(object, trav); + } +diff --git a/panda/src/pgraphnodes/computeNode.cxx b/panda/src/pgraphnodes/computeNode.cxx +index 7962378..2f40dd6 100644 +--- a/panda/src/pgraphnodes/computeNode.cxx ++++ b/panda/src/pgraphnodes/computeNode.cxx +@@ -94,7 +94,8 @@ add_for_draw(CullTraverser *trav, CullTraverserData &data) { + // however. + CullableObject *object = + new CullableObject(NULL, data._state, +- data.get_internal_transform(trav)); ++ data.get_internal_transform(trav), ++ data.get_prev_internal_transform(trav)); + object->set_draw_callback(_dispatcher); + trav->get_cull_handler()->record_object(object, trav); + } +diff --git a/panda/src/pgraphnodes/lodNode.cxx b/panda/src/pgraphnodes/lodNode.cxx +index 5936b06..2d52b41 100644 +--- a/panda/src/pgraphnodes/lodNode.cxx ++++ b/panda/src/pgraphnodes/lodNode.cxx +@@ -394,6 +394,7 @@ show_switches_cull_callback(CullTraverser *trav, CullTraverserData &data) { + // And draw the spindle in this color. + CullTraverserData next_data2(data, sw.get_spindle_viz()); + next_data2.apply_transform_and_state(trav, viz_transform, ++ TransformState::make_identity(), + RenderState::make_empty(), + RenderEffects::make_empty(), + ClipPlaneAttrib::make()); +@@ -404,6 +405,7 @@ show_switches_cull_callback(CullTraverser *trav, CullTraverserData &data) { + // the geometry and the spindle. + CullTraverserData next_data(data, sw.get_ring_viz()); + next_data.apply_transform_and_state(trav, viz_transform, ++ TransformState::make_identity(), + RenderState::make_empty(), + RenderEffects::make_empty(), + ClipPlaneAttrib::make()); +diff --git a/panda/src/pgraphnodes/nodeCullCallbackData.cxx b/panda/src/pgraphnodes/nodeCullCallbackData.cxx +index d0bf763..b89ac5f 100644 +--- a/panda/src/pgraphnodes/nodeCullCallbackData.cxx ++++ b/panda/src/pgraphnodes/nodeCullCallbackData.cxx +@@ -50,7 +50,8 @@ upcall() { + if (cbobj != (CallbackObject *)NULL) { + CullableObject *object = + new CullableObject(NULL, _data._state, +- _data.get_internal_transform(_trav)); ++ _data.get_internal_transform(_trav), ++ _data.get_prev_internal_transform(_trav)); + object->set_draw_callback(cbobj); + _trav->get_cull_handler()->record_object(object, _trav); + } +diff --git a/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx b/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx +index ba33377..af0a608 100644 +--- a/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx ++++ b/panda/src/tinydisplay/tinyGraphicsStateGuardian.cxx +@@ -206,7 +206,7 @@ clear(DrawableRegion *clearable) { + return; + } + +- set_state_and_transform(RenderState::make_empty(), _internal_transform); ++ set_state_and_transform(RenderState::make_empty(), _internal_transform, _prev_internal_transform); + + bool clear_color = false; + PIXEL color = 0; +@@ -1467,7 +1467,8 @@ framebuffer_copy_to_ram(Texture *tex, int view, int z, + */ + void TinyGraphicsStateGuardian:: + set_state_and_transform(const RenderState *target, +- const TransformState *transform) { ++ const TransformState *transform, ++ const TransformState *prev_transform) { + #ifndef NDEBUG + if (tinydisplay_cat.is_spam()) { + tinydisplay_cat.spam() +@@ -1480,6 +1481,8 @@ set_state_and_transform(const RenderState *target, + _state_pcollector.add_level(1); + PStatTimer timer1(_draw_set_state_pcollector); + ++ _prev_internal_transform = prev_transform; ++ + if (transform != _internal_transform) { + PStatTimer timer(_draw_set_state_transform_pcollector); + _state_pcollector.add_level(1); +diff --git a/panda/src/tinydisplay/tinyGraphicsStateGuardian.h b/panda/src/tinydisplay/tinyGraphicsStateGuardian.h +index 65e01d7..69f4ed9 100644 +--- a/panda/src/tinydisplay/tinyGraphicsStateGuardian.h ++++ b/panda/src/tinydisplay/tinyGraphicsStateGuardian.h +@@ -82,7 +82,8 @@ public: + (Texture *tex, int view, int z, const DisplayRegion *dr, const RenderBuffer &rb); + + virtual void set_state_and_transform(const RenderState *state, +- const TransformState *transform); ++ const TransformState *transform, ++ const TransformState *prev_transform); + + virtual TextureContext *prepare_texture(Texture *tex, int view); + virtual bool update_texture(TextureContext *tc, bool force); +-- +1.9.5.msysgit.0 + diff --git a/RenderPipelineFile/data/scripts/example_script.py b/RenderPipelineFile/data/scripts/example_script.py index ecba0da9..70e4a163 100644 --- a/RenderPipelineFile/data/scripts/example_script.py +++ b/RenderPipelineFile/data/scripts/example_script.py @@ -1,47 +1,47 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -示例脚本 - 演示如何编写脚本 -""" - -from core.script_system import ScriptBase - -class ExampleScript(ScriptBase): - """示例脚本类""" - - def __init__(self): - super().__init__() - self.counter = 0 - self.rotation_speed = 30.0 # 度/秒 - - def start(self): - """脚本开始时调用""" - self.log("示例脚本开始运行!") - self.log(f"挂载到对象: {self.gameObject.getName()}") - - def update(self, dt): - """每帧更新""" - self.counter += 1 - - # 每60帧输出一次信息 - if self.counter % 60 == 0: - self.log(f"运行了 {self.counter} 帧") - - # 让对象旋转 - if self.transform: - current_h = self.transform.getH() - new_h = current_h + self.rotation_speed * dt - self.transform.setH(new_h) - - def on_destroy(self): - """脚本销毁时调用""" - self.log("示例脚本被销毁") - - def on_enable(self): - """脚本启用时调用""" - self.log("示例脚本被启用") - - def on_disable(self): - """脚本禁用时调用""" - self.log("示例脚本被禁用") +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +示例脚本 - 演示如何编写脚本 +""" + +from core.script_system import ScriptBase + +class ExampleScript(ScriptBase): + """示例脚本类""" + + def __init__(self): + super().__init__() + self.counter = 0 + self.rotation_speed = 30.0 # 度/秒 + + def start(self): + """脚本开始时调用""" + self.log("示例脚本开始运行!") + self.log(f"挂载到对象: {self.gameObject.getName()}") + + def update(self, dt): + """每帧更新""" + self.counter += 1 + + # 每60帧输出一次信息 + if self.counter % 60 == 0: + self.log(f"运行了 {self.counter} 帧") + + # 让对象旋转 + if self.transform: + current_h = self.transform.getH() + new_h = current_h + self.rotation_speed * dt + self.transform.setH(new_h) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("示例脚本被销毁") + + def on_enable(self): + """脚本启用时调用""" + self.log("示例脚本被启用") + + def on_disable(self): + """脚本禁用时调用""" + self.log("示例脚本被禁用") diff --git a/RenderPipelineFile/data/setup/check_requirements.py b/RenderPipelineFile/data/setup/check_requirements.py index c0c93ea8..9b38fb8f 100644 --- a/RenderPipelineFile/data/setup/check_requirements.py +++ b/RenderPipelineFile/data/setup/check_requirements.py @@ -1,58 +1,58 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import print_function, division - -import os -import sys -from panda3d.core import * -from direct.showbase.ShowBase import ShowBase - -def _error(msg): - print("\n" * 4, file=sys.stderr) - print("ERROR:", msg, file=sys.stderr) - print("\n" * 4, file=sys.stderr) - sys.exit(1) - -class Application(ShowBase): - def __init__(self): - load_prc_file_data("", """ - window-type offscreen - win-size 100 100 - color-bits 0 - depth-bits 0 - back-buffers 0 - print-pipe-types #f - """) - ShowBase.__init__(self) - - if not self.win.gsg.supports_compute_shaders: - _error("Compute shaders not supported! Please update your driver, or get a newer gpu.") - - - print("All checks passed successfully") - -Application() +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import print_function, division + +import os +import sys +from panda3d.core import * +from direct.showbase.ShowBase import ShowBase + +def _error(msg): + print("\n" * 4, file=sys.stderr) + print("ERROR:", msg, file=sys.stderr) + print("\n" * 4, file=sys.stderr) + sys.exit(1) + +class Application(ShowBase): + def __init__(self): + load_prc_file_data("", """ + window-type offscreen + win-size 100 100 + color-bits 0 + depth-bits 0 + back-buffers 0 + print-pipe-types #f + """) + ShowBase.__init__(self) + + if not self.win.gsg.supports_compute_shaders: + _error("Compute shaders not supported! Please update your driver, or get a newer gpu.") + + + print("All checks passed successfully") + +Application() diff --git a/RenderPipelineFile/effects/basic_instancing.yaml b/RenderPipelineFile/effects/basic_instancing.yaml index 650edee6..dd706897 100644 --- a/RenderPipelineFile/effects/basic_instancing.yaml +++ b/RenderPipelineFile/effects/basic_instancing.yaml @@ -1,28 +1,28 @@ - -# Terrain effect -# This effect uses prodecural shader splatting, you most likely want to modify -# it with your own texture-map generation code. - -vertex: - inout: | - uniform samplerBuffer InstancingData; - - - transform: | - - int data_index = gl_InstanceID * 4; - - vec4 data_0 = texelFetch(InstancingData, data_index); - vec4 data_1 = texelFetch(InstancingData, data_index + 1); - vec4 data_2 = texelFetch(InstancingData, data_index + 2); - vec4 data_3 = texelFetch(InstancingData, data_index + 3); - - mat4 transform_mat = mat4(data_0, data_1, data_2, data_3); - - vOutput.position = (transform_mat * p3d_Vertex).xyz; - // Also transform normal, not 100% correct but works out nicely - vOutput.normal = mat3(transform_mat) * vOutput.normal; - - -fragment: - + +# Terrain effect +# This effect uses prodecural shader splatting, you most likely want to modify +# it with your own texture-map generation code. + +vertex: + inout: | + uniform samplerBuffer InstancingData; + + + transform: | + + int data_index = gl_InstanceID * 4; + + vec4 data_0 = texelFetch(InstancingData, data_index); + vec4 data_1 = texelFetch(InstancingData, data_index + 1); + vec4 data_2 = texelFetch(InstancingData, data_index + 2); + vec4 data_3 = texelFetch(InstancingData, data_index + 3); + + mat4 transform_mat = mat4(data_0, data_1, data_2, data_3); + + vOutput.position = (transform_mat * p3d_Vertex).xyz; + // Also transform normal, not 100% correct but works out nicely + vOutput.normal = mat3(transform_mat) * vOutput.normal; + + +fragment: + diff --git a/RenderPipelineFile/effects/debug_metallic.yaml b/RenderPipelineFile/effects/debug_metallic.yaml index 678bd54d..92ba78b1 100644 --- a/RenderPipelineFile/effects/debug_metallic.yaml +++ b/RenderPipelineFile/effects/debug_metallic.yaml @@ -1,35 +1,35 @@ -# Debug metallic texture effect -# This effect visualizes metallic texture directly for debugging - -fragment: - defines: | - #define DEBUG_METALLIC_TEXTURE 1 - - inout: | - uniform sampler2D p3d_Texture5; // Metallic texture - - material: | - // Debug: Show metallic texture directly - #if DEBUG_METALLIC_TEXTURE - // Sample metallic from texture - float sampled_metallic = texture(p3d_Texture5, texcoord).x; - - // For debugging: show texture value as colored visualization - // Blue tint for non-metallic, yellow tint for metallic - vec3 debug_color = mix(vec3(0.2, 0.2, 1.0), vec3(1.0, 1.0, 0.2), sampled_metallic); - - // Apply to material - m.basecolor = debug_color; // Show texture as color for debugging - m.roughness = 0.5; // Fixed roughness for clear visualization - m.metallic = sampled_metallic; // Also apply to metallic - m.specular_ior = 1.5; - m.shading_model_param0 = 0.0; - - #else - // Fallback - m.basecolor = mInput.color; - m.roughness = mInput.roughness; - m.metallic = mInput.metallic; - m.specular_ior = mInput.specular_ior; - m.shading_model_param0 = mInput.arbitrary0; - #endif +# Debug metallic texture effect +# This effect visualizes metallic texture directly for debugging + +fragment: + defines: | + #define DEBUG_METALLIC_TEXTURE 1 + + inout: | + uniform sampler2D p3d_Texture5; // Metallic texture + + material: | + // Debug: Show metallic texture directly + #if DEBUG_METALLIC_TEXTURE + // Sample metallic from texture + float sampled_metallic = texture(p3d_Texture5, texcoord).x; + + // For debugging: show texture value as colored visualization + // Blue tint for non-metallic, yellow tint for metallic + vec3 debug_color = mix(vec3(0.2, 0.2, 1.0), vec3(1.0, 1.0, 0.2), sampled_metallic); + + // Apply to material + m.basecolor = debug_color; // Show texture as color for debugging + m.roughness = 0.5; // Fixed roughness for clear visualization + m.metallic = sampled_metallic; // Also apply to metallic + m.specular_ior = 1.5; + m.shading_model_param0 = 0.0; + + #else + // Fallback + m.basecolor = mInput.color; + m.roughness = mInput.roughness; + m.metallic = mInput.metallic; + m.specular_ior = mInput.specular_ior; + m.shading_model_param0 = mInput.arbitrary0; + #endif diff --git a/RenderPipelineFile/effects/debug_roughness.yaml b/RenderPipelineFile/effects/debug_roughness.yaml index 9cec23f8..3314cbbf 100644 --- a/RenderPipelineFile/effects/debug_roughness.yaml +++ b/RenderPipelineFile/effects/debug_roughness.yaml @@ -1,35 +1,35 @@ -# Debug roughness texture effect -# This effect visualizes roughness texture directly for debugging - -fragment: - defines: | - #define DEBUG_ROUGHNESS_TEXTURE 1 - - inout: | - uniform sampler2D p3d_Texture3; // Roughness texture - - material: | - // Debug: Show roughness texture directly - #if DEBUG_ROUGHNESS_TEXTURE - // Sample roughness from texture - float sampled_roughness = texture(p3d_Texture3, texcoord).x; - - // For debugging: show texture value as grayscale color - // This helps verify the texture is being read correctly - vec3 debug_color = vec3(sampled_roughness); - - // Apply to material - m.basecolor = debug_color; // Show texture as color for debugging - m.roughness = sampled_roughness; // Also apply to roughness - m.metallic = 0.0; // Keep metallic low for clear visualization - m.specular_ior = 1.5; - m.shading_model_param0 = 0.0; - - #else - // Fallback - m.basecolor = mInput.color; - m.roughness = mInput.roughness; - m.metallic = mInput.metallic; - m.specular_ior = mInput.specular_ior; - m.shading_model_param0 = mInput.arbitrary0; - #endif +# Debug roughness texture effect +# This effect visualizes roughness texture directly for debugging + +fragment: + defines: | + #define DEBUG_ROUGHNESS_TEXTURE 1 + + inout: | + uniform sampler2D p3d_Texture3; // Roughness texture + + material: | + // Debug: Show roughness texture directly + #if DEBUG_ROUGHNESS_TEXTURE + // Sample roughness from texture + float sampled_roughness = texture(p3d_Texture3, texcoord).x; + + // For debugging: show texture value as grayscale color + // This helps verify the texture is being read correctly + vec3 debug_color = vec3(sampled_roughness); + + // Apply to material + m.basecolor = debug_color; // Show texture as color for debugging + m.roughness = sampled_roughness; // Also apply to roughness + m.metallic = 0.0; // Keep metallic low for clear visualization + m.specular_ior = 1.5; + m.shading_model_param0 = 0.0; + + #else + // Fallback + m.basecolor = mInput.color; + m.roughness = mInput.roughness; + m.metallic = mInput.metallic; + m.specular_ior = mInput.specular_ior; + m.shading_model_param0 = mInput.arbitrary0; + #endif diff --git a/RenderPipelineFile/effects/default.yaml b/RenderPipelineFile/effects/default.yaml index 9884cd13..ed85837b 100644 --- a/RenderPipelineFile/effects/default.yaml +++ b/RenderPipelineFile/effects/default.yaml @@ -1,3 +1,3 @@ - -# Default effect file, used for all objects if not overridden. -# This file actually contains no content, since no templates are overriden. + +# Default effect file, used for all objects if not overridden. +# This file actually contains no content, since no templates are overriden. diff --git a/RenderPipelineFile/effects/material_blend4.yaml b/RenderPipelineFile/effects/material_blend4.yaml index fe9fe080..00b66626 100644 --- a/RenderPipelineFile/effects/material_blend4.yaml +++ b/RenderPipelineFile/effects/material_blend4.yaml @@ -1,89 +1,89 @@ - - -# This effect blends 4 materials based on a material map -# The textures should be setup like in the 04-Material-Blending sample. - -# The last material is the base material and thus has no blending. It will -# be wherever no other material is. - -vertex: - -fragment: - inout: | - uniform sampler2D p3d_Texture0; // Alpha - Map - - uniform sampler2D p3d_Texture1; // M1 - Diffuse - uniform sampler2D p3d_Texture2; // M1 - Normal - uniform sampler2D p3d_Texture3; // M1 - Specular - uniform sampler2D p3d_Texture4; // M1 - Blendmap - - uniform sampler2D p3d_Texture5; // M2 - Diffuse - uniform sampler2D p3d_Texture6; // M2 - Normal - uniform sampler2D p3d_Texture7; // M2 - Specular - uniform sampler2D p3d_Texture8; // M2 - Blendmap - - uniform sampler2D p3d_Texture9; // M3 - Diffuse - uniform sampler2D p3d_Texture10; // M3 - Normal - uniform sampler2D p3d_Texture11; // M3 - Specular - uniform sampler2D p3d_Texture12; // M3 - Blendmap - - uniform sampler2D p3d_Texture13; // M4 - Diffuse - uniform sampler2D p3d_Texture14; // M4 - Normal - uniform sampler2D p3d_Texture15; // M4 - Specular - uniform sampler2D p3d_Texture16; // M4 - Blendmap - - uniform float detail_scale_factor; - - uniform float material_0_pow; - uniform float material_0_add; - uniform float material_1_pow; - uniform float material_1_add; - uniform float material_2_pow; - uniform float material_2_add; - - defines: | - #define DONT_FETCH_DEFAULT_TEXTURES 1 - #define DONT_SET_MATERIAL_PROPERTIES 1 - - material: | - - // Get detail coordinate - vec2 detail_coord = texcoord * detail_scale_factor; - vec4 layers = texture(p3d_Texture0, texcoord); - layers.xyz *= layers.w; - - // Blend all materials - layers.x = blend_material(layers.x, texture(p3d_Texture4, detail_coord).x, material_0_add, material_0_pow); - layers.y = blend_material(layers.y, texture(p3d_Texture8, detail_coord).x, material_1_add, material_1_pow); - layers.z = blend_material(layers.z, texture(p3d_Texture12, detail_coord).x, material_2_add, material_2_pow); - layers.w = saturate(1.0 - dot(layers.xyz, vec3(1))); // More inituitive - - // Diffuse - vec3 m_base = vec3(0); - m_base = texture(p3d_Texture1, detail_coord).xyz * layers.x; - m_base += texture(p3d_Texture5, detail_coord).xyz * layers.y; - m_base += texture(p3d_Texture9, detail_coord).xyz * layers.z; - m_base += texture(p3d_Texture13, detail_coord).xyz * layers.w; - - // Normal - vec3 m_nrm = vec3(0); - m_nrm += unpack_texture_normal(texture(p3d_Texture2, detail_coord).xyz) * layers.x; - m_nrm += unpack_texture_normal(texture(p3d_Texture6, detail_coord).xyz) * layers.y; - m_nrm += unpack_texture_normal(texture(p3d_Texture10, detail_coord).xyz) * layers.z; - m_nrm += unpack_texture_normal(texture(p3d_Texture14, detail_coord).xyz) * layers.w; - - // Specular - float spec = 0.0; - spec = texture(p3d_Texture3, detail_coord).x * layers.x; - spec += texture(p3d_Texture7, detail_coord).x * layers.y; - spec += texture(p3d_Texture11, detail_coord).x * layers.z; - spec += texture(p3d_Texture15, detail_coord).x * layers.w; - - // Material properties - m.shading_model = mInput.shading_model; - m.basecolor = m_base * mInput.color; - m.normal = apply_normal_map(vOutput.normal, m_nrm, mInput.normalfactor); - m.specular_ior = blend_ior(mInput.specular_ior, spec); - m.roughness = mInput.roughness; - m.metallic = mInput.metallic; - m.shading_model_param0 = mInput.arbitrary0; + + +# This effect blends 4 materials based on a material map +# The textures should be setup like in the 04-Material-Blending sample. + +# The last material is the base material and thus has no blending. It will +# be wherever no other material is. + +vertex: + +fragment: + inout: | + uniform sampler2D p3d_Texture0; // Alpha - Map + + uniform sampler2D p3d_Texture1; // M1 - Diffuse + uniform sampler2D p3d_Texture2; // M1 - Normal + uniform sampler2D p3d_Texture3; // M1 - Specular + uniform sampler2D p3d_Texture4; // M1 - Blendmap + + uniform sampler2D p3d_Texture5; // M2 - Diffuse + uniform sampler2D p3d_Texture6; // M2 - Normal + uniform sampler2D p3d_Texture7; // M2 - Specular + uniform sampler2D p3d_Texture8; // M2 - Blendmap + + uniform sampler2D p3d_Texture9; // M3 - Diffuse + uniform sampler2D p3d_Texture10; // M3 - Normal + uniform sampler2D p3d_Texture11; // M3 - Specular + uniform sampler2D p3d_Texture12; // M3 - Blendmap + + uniform sampler2D p3d_Texture13; // M4 - Diffuse + uniform sampler2D p3d_Texture14; // M4 - Normal + uniform sampler2D p3d_Texture15; // M4 - Specular + uniform sampler2D p3d_Texture16; // M4 - Blendmap + + uniform float detail_scale_factor; + + uniform float material_0_pow; + uniform float material_0_add; + uniform float material_1_pow; + uniform float material_1_add; + uniform float material_2_pow; + uniform float material_2_add; + + defines: | + #define DONT_FETCH_DEFAULT_TEXTURES 1 + #define DONT_SET_MATERIAL_PROPERTIES 1 + + material: | + + // Get detail coordinate + vec2 detail_coord = texcoord * detail_scale_factor; + vec4 layers = texture(p3d_Texture0, texcoord); + layers.xyz *= layers.w; + + // Blend all materials + layers.x = blend_material(layers.x, texture(p3d_Texture4, detail_coord).x, material_0_add, material_0_pow); + layers.y = blend_material(layers.y, texture(p3d_Texture8, detail_coord).x, material_1_add, material_1_pow); + layers.z = blend_material(layers.z, texture(p3d_Texture12, detail_coord).x, material_2_add, material_2_pow); + layers.w = saturate(1.0 - dot(layers.xyz, vec3(1))); // More inituitive + + // Diffuse + vec3 m_base = vec3(0); + m_base = texture(p3d_Texture1, detail_coord).xyz * layers.x; + m_base += texture(p3d_Texture5, detail_coord).xyz * layers.y; + m_base += texture(p3d_Texture9, detail_coord).xyz * layers.z; + m_base += texture(p3d_Texture13, detail_coord).xyz * layers.w; + + // Normal + vec3 m_nrm = vec3(0); + m_nrm += unpack_texture_normal(texture(p3d_Texture2, detail_coord).xyz) * layers.x; + m_nrm += unpack_texture_normal(texture(p3d_Texture6, detail_coord).xyz) * layers.y; + m_nrm += unpack_texture_normal(texture(p3d_Texture10, detail_coord).xyz) * layers.z; + m_nrm += unpack_texture_normal(texture(p3d_Texture14, detail_coord).xyz) * layers.w; + + // Specular + float spec = 0.0; + spec = texture(p3d_Texture3, detail_coord).x * layers.x; + spec += texture(p3d_Texture7, detail_coord).x * layers.y; + spec += texture(p3d_Texture11, detail_coord).x * layers.z; + spec += texture(p3d_Texture15, detail_coord).x * layers.w; + + // Material properties + m.shading_model = mInput.shading_model; + m.basecolor = m_base * mInput.color; + m.normal = apply_normal_map(vOutput.normal, m_nrm, mInput.normalfactor); + m.specular_ior = blend_ior(mInput.specular_ior, spec); + m.roughness = mInput.roughness; + m.metallic = mInput.metallic; + m.shading_model_param0 = mInput.arbitrary0; diff --git a/RenderPipelineFile/effects/metallic_only.yaml b/RenderPipelineFile/effects/metallic_only.yaml index 2c3cb8d6..4955fe0d 100644 --- a/RenderPipelineFile/effects/metallic_only.yaml +++ b/RenderPipelineFile/effects/metallic_only.yaml @@ -1,39 +1,39 @@ -# Metallic-only PBR effect -# This effect specifically handles metallic textures correctly -# Ensures metallic texture is not confused with diffuse texture - -fragment: - defines: | - #define USE_METALLIC_TEXTURE_ONLY 1 - #define DONT_FETCH_DEFAULT_TEXTURES 0 - - inout: | - uniform sampler2D p3d_Texture5; // Metallic texture only - - material: | - // Only handle metallic texture, ignore others to avoid confusion - #if USE_METALLIC_TEXTURE_ONLY - // Sample metallic from p3d_Texture5 - float sampled_metallic = texture(p3d_Texture5, texcoord).x; - - // Direct control: texture value directly controls metallic - // This gives Blender-like behavior where texture has full control - m.metallic = sampled_metallic; - - // Clamp to valid range - m.metallic = clamp(m.metallic, 0.0, 1.0); - - // Keep other properties from material (no texture interference) - m.basecolor = mInput.color; // Use material color, not texture - m.roughness = mInput.roughness; - m.specular_ior = mInput.specular_ior; - m.shading_model_param0 = mInput.arbitrary0; - - #else - // Fallback to material values only - m.basecolor = mInput.color; - m.roughness = mInput.roughness; - m.metallic = mInput.metallic; - m.specular_ior = mInput.specular_ior; - m.shading_model_param0 = mInput.arbitrary0; - #endif +# Metallic-only PBR effect +# This effect specifically handles metallic textures correctly +# Ensures metallic texture is not confused with diffuse texture + +fragment: + defines: | + #define USE_METALLIC_TEXTURE_ONLY 1 + #define DONT_FETCH_DEFAULT_TEXTURES 0 + + inout: | + uniform sampler2D p3d_Texture5; // Metallic texture only + + material: | + // Only handle metallic texture, ignore others to avoid confusion + #if USE_METALLIC_TEXTURE_ONLY + // Sample metallic from p3d_Texture5 + float sampled_metallic = texture(p3d_Texture5, texcoord).x; + + // Direct control: texture value directly controls metallic + // This gives Blender-like behavior where texture has full control + m.metallic = sampled_metallic; + + // Clamp to valid range + m.metallic = clamp(m.metallic, 0.0, 1.0); + + // Keep other properties from material (no texture interference) + m.basecolor = mInput.color; // Use material color, not texture + m.roughness = mInput.roughness; + m.specular_ior = mInput.specular_ior; + m.shading_model_param0 = mInput.arbitrary0; + + #else + // Fallback to material values only + m.basecolor = mInput.color; + m.roughness = mInput.roughness; + m.metallic = mInput.metallic; + m.specular_ior = mInput.specular_ior; + m.shading_model_param0 = mInput.arbitrary0; + #endif diff --git a/RenderPipelineFile/effects/pbr_additive_metallic.yaml b/RenderPipelineFile/effects/pbr_additive_metallic.yaml index b84bcf3c..8ea1f8ac 100644 --- a/RenderPipelineFile/effects/pbr_additive_metallic.yaml +++ b/RenderPipelineFile/effects/pbr_additive_metallic.yaml @@ -1,29 +1,29 @@ -# PBR effect with additive metallic texture control -# This effect adds metallic texture values to material metallic values -# Final metallic = material_metallic + texture_value (clamped to 0-1) - -fragment: - defines: | - #define USE_ADDITIVE_METALLIC_TEXTURE 1 - - inout: | - uniform sampler2D p3d_Texture5; // Metallic texture (additive control) - - material: | - // Additive metallic texture control - #if USE_ADDITIVE_METALLIC_TEXTURE - // Sample metallic value from texture - float sampled_metallic = texture(p3d_Texture5, texcoord).x; - - // Add texture value to material metallic value - // This allows enhancing existing metallic areas - m.metallic = clamp(mInput.metallic + sampled_metallic, 0.0, 1.0); - - // Debug: This mode is useful for: - // - Adding metallic details to non-metallic materials - // - Enhancing existing metallic areas - // - Creating metallic highlights on top of base material - #else - // Fallback to material metallic value - m.metallic = mInput.metallic; - #endif +# PBR effect with additive metallic texture control +# This effect adds metallic texture values to material metallic values +# Final metallic = material_metallic + texture_value (clamped to 0-1) + +fragment: + defines: | + #define USE_ADDITIVE_METALLIC_TEXTURE 1 + + inout: | + uniform sampler2D p3d_Texture5; // Metallic texture (additive control) + + material: | + // Additive metallic texture control + #if USE_ADDITIVE_METALLIC_TEXTURE + // Sample metallic value from texture + float sampled_metallic = texture(p3d_Texture5, texcoord).x; + + // Add texture value to material metallic value + // This allows enhancing existing metallic areas + m.metallic = clamp(mInput.metallic + sampled_metallic, 0.0, 1.0); + + // Debug: This mode is useful for: + // - Adding metallic details to non-metallic materials + // - Enhancing existing metallic areas + // - Creating metallic highlights on top of base material + #else + // Fallback to material metallic value + m.metallic = mInput.metallic; + #endif diff --git a/RenderPipelineFile/effects/pbr_direct_metallic.yaml b/RenderPipelineFile/effects/pbr_direct_metallic.yaml index 80f0b205..99919f19 100644 --- a/RenderPipelineFile/effects/pbr_direct_metallic.yaml +++ b/RenderPipelineFile/effects/pbr_direct_metallic.yaml @@ -1,29 +1,29 @@ -# PBR effect with direct metallic texture control -# This effect allows metallic textures to directly control metallic values -# instead of multiplying with material metallic value - -fragment: - defines: | - #define USE_DIRECT_METALLIC_TEXTURE 1 - - inout: | - uniform sampler2D p3d_Texture5; // Metallic texture (direct control) - - material: | - // Direct metallic texture control - #if USE_DIRECT_METALLIC_TEXTURE - // Sample metallic value directly from texture - float sampled_metallic = texture(p3d_Texture5, texcoord).x; - - // Use texture value directly instead of multiplying - // This allows the texture to have full control over metallic values - m.metallic = sampled_metallic; - - // Debug: Ensure we're using the texture value - // White areas = 1.0 (fully metallic) - // Gray areas = 0.5 (semi-metallic) - // Black areas = 0.0 (non-metallic) - #else - // Fallback to material metallic value - m.metallic = mInput.metallic; - #endif +# PBR effect with direct metallic texture control +# This effect allows metallic textures to directly control metallic values +# instead of multiplying with material metallic value + +fragment: + defines: | + #define USE_DIRECT_METALLIC_TEXTURE 1 + + inout: | + uniform sampler2D p3d_Texture5; // Metallic texture (direct control) + + material: | + // Direct metallic texture control + #if USE_DIRECT_METALLIC_TEXTURE + // Sample metallic value directly from texture + float sampled_metallic = texture(p3d_Texture5, texcoord).x; + + // Use texture value directly instead of multiplying + // This allows the texture to have full control over metallic values + m.metallic = sampled_metallic; + + // Debug: Ensure we're using the texture value + // White areas = 1.0 (fully metallic) + // Gray areas = 0.5 (semi-metallic) + // Black areas = 0.0 (non-metallic) + #else + // Fallback to material metallic value + m.metallic = mInput.metallic; + #endif diff --git a/RenderPipelineFile/effects/pbr_direct_roughness.yaml b/RenderPipelineFile/effects/pbr_direct_roughness.yaml index d6bb2ae4..4b6a3f56 100644 --- a/RenderPipelineFile/effects/pbr_direct_roughness.yaml +++ b/RenderPipelineFile/effects/pbr_direct_roughness.yaml @@ -1,38 +1,38 @@ -# PBR effect with direct roughness texture control -# This effect allows roughness textures to directly control roughness values -# instead of multiplying with material roughness value - -fragment: - defines: | - #define USE_DIRECT_ROUGHNESS_TEXTURE 1 - - inout: | - uniform sampler2D p3d_Texture3; // Roughness texture (direct control) - - material: | - // Direct roughness texture control - #if USE_DIRECT_ROUGHNESS_TEXTURE - // Sample roughness value directly from texture - float sampled_roughness = texture(p3d_Texture3, texcoord).x; - - // Use texture value directly instead of multiplying - // This allows the texture to have full control over roughness values - m.roughness = sampled_roughness; - - // Ensure minimum roughness to avoid rendering issues - m.roughness = max(m.roughness, 0.01); - - // Debug: Ensure we're using the texture value - // White areas = 1.0 (fully rough) - // Gray areas = 0.5 (semi-rough) - // Black areas = 0.01 (smooth, but not zero to avoid issues) - #else - // Fallback to material roughness value - m.roughness = mInput.roughness; - #endif - - // Keep all other properties unchanged for stability - m.basecolor = mInput.color; - m.metallic = mInput.metallic; - m.specular_ior = mInput.specular_ior; - m.shading_model_param0 = mInput.arbitrary0; +# PBR effect with direct roughness texture control +# This effect allows roughness textures to directly control roughness values +# instead of multiplying with material roughness value + +fragment: + defines: | + #define USE_DIRECT_ROUGHNESS_TEXTURE 1 + + inout: | + uniform sampler2D p3d_Texture3; // Roughness texture (direct control) + + material: | + // Direct roughness texture control + #if USE_DIRECT_ROUGHNESS_TEXTURE + // Sample roughness value directly from texture + float sampled_roughness = texture(p3d_Texture3, texcoord).x; + + // Use texture value directly instead of multiplying + // This allows the texture to have full control over roughness values + m.roughness = sampled_roughness; + + // Ensure minimum roughness to avoid rendering issues + m.roughness = max(m.roughness, 0.01); + + // Debug: Ensure we're using the texture value + // White areas = 1.0 (fully rough) + // Gray areas = 0.5 (semi-rough) + // Black areas = 0.01 (smooth, but not zero to avoid issues) + #else + // Fallback to material roughness value + m.roughness = mInput.roughness; + #endif + + // Keep all other properties unchanged for stability + m.basecolor = mInput.color; + m.metallic = mInput.metallic; + m.specular_ior = mInput.specular_ior; + m.shading_model_param0 = mInput.arbitrary0; diff --git a/RenderPipelineFile/effects/pbr_extended.yaml b/RenderPipelineFile/effects/pbr_extended.yaml index e6754d09..ae804114 100644 --- a/RenderPipelineFile/effects/pbr_extended.yaml +++ b/RenderPipelineFile/effects/pbr_extended.yaml @@ -1,51 +1,51 @@ -# Extended PBR effect with support for additional texture types -# This effect supports metallic, emission, AO, detail, and gloss textures - -fragment: - defines: | - #define USE_EXTENDED_TEXTURES 1 - - inout: | - uniform sampler2D p3d_Texture5; // Metallic texture - uniform sampler2D p3d_Texture6; // Emission texture - uniform sampler2D p3d_Texture7; // AO texture - uniform sampler2D p3d_Texture8; // Alpha texture - uniform sampler2D p3d_Texture9; // Detail texture - uniform sampler2D p3d_Texture10; // Gloss texture - - material: | - #if USE_EXTENDED_TEXTURES - - // Metallic texture (p3d_Texture5) - float sampled_metallic = texture(p3d_Texture5, texcoord).x; - m.metallic = mInput.metallic * sampled_metallic; - - // Emission texture (p3d_Texture6) - vec3 sampled_emission = texture(p3d_Texture6, texcoord).xyz; - if (mInput.shading_model == SHADING_MODEL_EMISSIVE) { - m.basecolor += sampled_emission * 2.0; // Boost emission intensity - } - - // AO texture (p3d_Texture7) - multiply with base color - float sampled_ao = texture(p3d_Texture7, texcoord).x; - m.basecolor *= sampled_ao; - - // Alpha texture (p3d_Texture8) - for transparency - float sampled_alpha = texture(p3d_Texture8, texcoord).x; - if (mInput.shading_model == SHADING_MODEL_TRANSPARENT) { - // Apply alpha testing - if (sampled_alpha < 0.5) discard; - } - - // Detail texture (p3d_Texture9) - blend with base color - vec3 sampled_detail = texture(p3d_Texture9, texcoord * 4.0).xyz; // Tiled detail - m.basecolor = mix(m.basecolor, m.basecolor * sampled_detail, 0.5); - - // Gloss texture (p3d_Texture10) - affects roughness - float sampled_gloss = texture(p3d_Texture10, texcoord).x; - m.roughness = m.roughness * (1.0 - sampled_gloss); // Gloss is inverse of roughness - - #else - // Standard behavior when extended textures are not used - m.metallic = mInput.metallic; - #endif +# Extended PBR effect with support for additional texture types +# This effect supports metallic, emission, AO, detail, and gloss textures + +fragment: + defines: | + #define USE_EXTENDED_TEXTURES 1 + + inout: | + uniform sampler2D p3d_Texture5; // Metallic texture + uniform sampler2D p3d_Texture6; // Emission texture + uniform sampler2D p3d_Texture7; // AO texture + uniform sampler2D p3d_Texture8; // Alpha texture + uniform sampler2D p3d_Texture9; // Detail texture + uniform sampler2D p3d_Texture10; // Gloss texture + + material: | + #if USE_EXTENDED_TEXTURES + + // Metallic texture (p3d_Texture5) + float sampled_metallic = texture(p3d_Texture5, texcoord).x; + m.metallic = mInput.metallic * sampled_metallic; + + // Emission texture (p3d_Texture6) + vec3 sampled_emission = texture(p3d_Texture6, texcoord).xyz; + if (mInput.shading_model == SHADING_MODEL_EMISSIVE) { + m.basecolor += sampled_emission * 2.0; // Boost emission intensity + } + + // AO texture (p3d_Texture7) - multiply with base color + float sampled_ao = texture(p3d_Texture7, texcoord).x; + m.basecolor *= sampled_ao; + + // Alpha texture (p3d_Texture8) - for transparency + float sampled_alpha = texture(p3d_Texture8, texcoord).x; + if (mInput.shading_model == SHADING_MODEL_TRANSPARENT) { + // Apply alpha testing + if (sampled_alpha < 0.5) discard; + } + + // Detail texture (p3d_Texture9) - blend with base color + vec3 sampled_detail = texture(p3d_Texture9, texcoord * 4.0).xyz; // Tiled detail + m.basecolor = mix(m.basecolor, m.basecolor * sampled_detail, 0.5); + + // Gloss texture (p3d_Texture10) - affects roughness + float sampled_gloss = texture(p3d_Texture10, texcoord).x; + m.roughness = m.roughness * (1.0 - sampled_gloss); // Gloss is inverse of roughness + + #else + // Standard behavior when extended textures are not used + m.metallic = mInput.metallic; + #endif diff --git a/RenderPipelineFile/effects/pbr_with_emission.yaml b/RenderPipelineFile/effects/pbr_with_emission.yaml index b566d8e8..0392d23c 100644 --- a/RenderPipelineFile/effects/pbr_with_emission.yaml +++ b/RenderPipelineFile/effects/pbr_with_emission.yaml @@ -1,19 +1,19 @@ -# PBR effect with emission texture support -# This effect extends the default PBR pipeline to support emission textures - -fragment: - defines: | - #define USE_EMISSION_TEXTURE 1 - - inout: | - uniform sampler2D p3d_Texture6; // Emission texture - - material: | - // Fetch emission value from texture if available - #if USE_EMISSION_TEXTURE - vec3 sampled_emission = texture(p3d_Texture6, texcoord).xyz; - // For emissive materials, add the texture emission to the base color - if (mInput.shading_model == SHADING_MODEL_EMISSIVE) { - m.basecolor += sampled_emission * 2.0; // Boost emission intensity - } - #endif +# PBR effect with emission texture support +# This effect extends the default PBR pipeline to support emission textures + +fragment: + defines: | + #define USE_EMISSION_TEXTURE 1 + + inout: | + uniform sampler2D p3d_Texture6; // Emission texture + + material: | + // Fetch emission value from texture if available + #if USE_EMISSION_TEXTURE + vec3 sampled_emission = texture(p3d_Texture6, texcoord).xyz; + // For emissive materials, add the texture emission to the base color + if (mInput.shading_model == SHADING_MODEL_EMISSIVE) { + m.basecolor += sampled_emission * 2.0; // Boost emission intensity + } + #endif diff --git a/RenderPipelineFile/effects/pbr_with_metallic.yaml b/RenderPipelineFile/effects/pbr_with_metallic.yaml index 8c2efd5d..c8224771 100644 --- a/RenderPipelineFile/effects/pbr_with_metallic.yaml +++ b/RenderPipelineFile/effects/pbr_with_metallic.yaml @@ -1,40 +1,40 @@ -# PBR effect with metallic texture support -# This effect extends the default PBR pipeline to support metallic textures -# Provides both multiplicative and direct control modes - -fragment: - defines: | - #define USE_METALLIC_TEXTURE 1 - #define METALLIC_CONTROL_MODE 0 // 0=multiply, 1=direct, 2=additive - - inout: | - uniform sampler2D p3d_Texture5; // Metallic texture - - material: | - // Add metallic texture support to the default material processing - // Note: Don't override the default material processing, just extend it - #if USE_METALLIC_TEXTURE - // Sample metallic texture - float sampled_metallic = texture(p3d_Texture5, texcoord).x; - - // Apply metallic texture based on control mode - #if METALLIC_CONTROL_MODE == 0 - // Multiplicative mode (original behavior) - m.metallic = mInput.metallic * sampled_metallic; - #elif METALLIC_CONTROL_MODE == 1 - // Direct control mode - m.metallic = sampled_metallic; - #elif METALLIC_CONTROL_MODE == 2 - // Additive mode - m.metallic = clamp(mInput.metallic + sampled_metallic, 0.0, 1.0); - #else - // Fallback to multiplicative - m.metallic = mInput.metallic * sampled_metallic; - #endif - - // Ensure valid range - m.metallic = clamp(m.metallic, 0.0, 1.0); - #endif - - // Note: Other material properties (basecolor, roughness, etc.) are handled by the default template - // We don't override them here to avoid conflicts +# PBR effect with metallic texture support +# This effect extends the default PBR pipeline to support metallic textures +# Provides both multiplicative and direct control modes + +fragment: + defines: | + #define USE_METALLIC_TEXTURE 1 + #define METALLIC_CONTROL_MODE 0 // 0=multiply, 1=direct, 2=additive + + inout: | + uniform sampler2D p3d_Texture5; // Metallic texture + + material: | + // Add metallic texture support to the default material processing + // Note: Don't override the default material processing, just extend it + #if USE_METALLIC_TEXTURE + // Sample metallic texture + float sampled_metallic = texture(p3d_Texture5, texcoord).x; + + // Apply metallic texture based on control mode + #if METALLIC_CONTROL_MODE == 0 + // Multiplicative mode (original behavior) + m.metallic = mInput.metallic * sampled_metallic; + #elif METALLIC_CONTROL_MODE == 1 + // Direct control mode + m.metallic = sampled_metallic; + #elif METALLIC_CONTROL_MODE == 2 + // Additive mode + m.metallic = clamp(mInput.metallic + sampled_metallic, 0.0, 1.0); + #else + // Fallback to multiplicative + m.metallic = mInput.metallic * sampled_metallic; + #endif + + // Ensure valid range + m.metallic = clamp(m.metallic, 0.0, 1.0); + #endif + + // Note: Other material properties (basecolor, roughness, etc.) are handled by the default template + // We don't override them here to avoid conflicts diff --git a/RenderPipelineFile/effects/pbr_with_normal.yaml b/RenderPipelineFile/effects/pbr_with_normal.yaml index f8e385be..21be170a 100644 --- a/RenderPipelineFile/effects/pbr_with_normal.yaml +++ b/RenderPipelineFile/effects/pbr_with_normal.yaml @@ -1,31 +1,31 @@ -# PBR effect with normal mapping support -# This effect ensures proper material color handling when only normal maps are applied - -fragment: - defines: | - #define USE_NORMAL_MAPPING_ONLY 1 - - material: | - // Handle normal mapping without requiring diffuse texture - #if USE_NORMAL_MAPPING_ONLY - // Use material color directly, don't multiply with sampled_diffuse - // This prevents the black color issue when no diffuse texture is present - m.basecolor = mInput.color; - - // Handle other textures normally if they exist - m.specular_ior = blend_ior(mInput.specular_ior, sampled_ior); - m.roughness = mInput.roughness * sampled_roughness; - m.metallic = mInput.metallic; - - // Ensure minimum roughness - m.roughness = max(m.roughness, 0.01); - - #else - // Standard material processing - m.basecolor = mInput.color * sampled_diffuse.xyz; - m.specular_ior = blend_ior(mInput.specular_ior, sampled_ior); - m.roughness = mInput.roughness * sampled_roughness; - m.metallic = mInput.metallic; - #endif - - m.shading_model_param0 = mInput.arbitrary0; +# PBR effect with normal mapping support +# This effect ensures proper material color handling when only normal maps are applied + +fragment: + defines: | + #define USE_NORMAL_MAPPING_ONLY 1 + + material: | + // Handle normal mapping without requiring diffuse texture + #if USE_NORMAL_MAPPING_ONLY + // Use material color directly, don't multiply with sampled_diffuse + // This prevents the black color issue when no diffuse texture is present + m.basecolor = mInput.color; + + // Handle other textures normally if they exist + m.specular_ior = blend_ior(mInput.specular_ior, sampled_ior); + m.roughness = mInput.roughness * sampled_roughness; + m.metallic = mInput.metallic; + + // Ensure minimum roughness + m.roughness = max(m.roughness, 0.01); + + #else + // Standard material processing + m.basecolor = mInput.color * sampled_diffuse.xyz; + m.specular_ior = blend_ior(mInput.specular_ior, sampled_ior); + m.roughness = mInput.roughness * sampled_roughness; + m.metallic = mInput.metallic; + #endif + + m.shading_model_param0 = mInput.arbitrary0; diff --git a/RenderPipelineFile/effects/pbr_with_roughness.yaml b/RenderPipelineFile/effects/pbr_with_roughness.yaml index 0b620cb7..f478570f 100644 --- a/RenderPipelineFile/effects/pbr_with_roughness.yaml +++ b/RenderPipelineFile/effects/pbr_with_roughness.yaml @@ -1,33 +1,33 @@ -# Stable PBR effect with roughness texture support -# Simplified version to avoid rendering issues and flickering - -fragment: - defines: | - #define USE_STABLE_ROUGHNESS_TEXTURE 1 - - inout: | - uniform sampler2D p3d_Texture3; // Roughness texture only - - material: | - // Simple and stable roughness texture handling - #if USE_STABLE_ROUGHNESS_TEXTURE - // Sample roughness from texture - float sampled_roughness = texture(p3d_Texture3, texcoord).x; - - // Use simple multiplication - stable and predictable - // This ensures the texture effect is always visible - m.roughness = mInput.roughness * sampled_roughness; - - // Ensure minimum roughness to avoid rendering issues - m.roughness = max(m.roughness, 0.01); - - #else - // Fallback to material roughness - m.roughness = mInput.roughness; - #endif - - // Keep all other properties unchanged for stability - m.basecolor = mInput.color; - m.metallic = mInput.metallic; - m.specular_ior = mInput.specular_ior; - m.shading_model_param0 = mInput.arbitrary0; +# Stable PBR effect with roughness texture support +# Simplified version to avoid rendering issues and flickering + +fragment: + defines: | + #define USE_STABLE_ROUGHNESS_TEXTURE 1 + + inout: | + uniform sampler2D p3d_Texture3; // Roughness texture only + + material: | + // Simple and stable roughness texture handling + #if USE_STABLE_ROUGHNESS_TEXTURE + // Sample roughness from texture + float sampled_roughness = texture(p3d_Texture3, texcoord).x; + + // Use simple multiplication - stable and predictable + // This ensures the texture effect is always visible + m.roughness = mInput.roughness * sampled_roughness; + + // Ensure minimum roughness to avoid rendering issues + m.roughness = max(m.roughness, 0.01); + + #else + // Fallback to material roughness + m.roughness = mInput.roughness; + #endif + + // Keep all other properties unchanged for stability + m.basecolor = mInput.color; + m.metallic = mInput.metallic; + m.specular_ior = mInput.specular_ior; + m.shading_model_param0 = mInput.arbitrary0; diff --git a/RenderPipelineFile/effects/pbr_with_textures.yaml b/RenderPipelineFile/effects/pbr_with_textures.yaml index 13749f12..9539928b 100644 --- a/RenderPipelineFile/effects/pbr_with_textures.yaml +++ b/RenderPipelineFile/effects/pbr_with_textures.yaml @@ -1,37 +1,37 @@ -# Enhanced PBR effect with metallic texture support -# This effect extends the default PBR to support metallic textures -# Avoids redeclaring textures that are already in the base template - -fragment: - defines: | - #define USE_METALLIC_TEXTURE 1 - - inout: | - uniform sampler2D p3d_Texture5; // Metallic texture (not in default template) - - material: | - // Enhanced material with metallic texture support - #if USE_METALLIC_TEXTURE - // Sample metallic texture - float sampled_metallic = texture(p3d_Texture5, texcoord).x; - - // Apply all material properties - // Use the sampled values from the base template for other textures - m.basecolor = mInput.color * sampled_diffuse.xyz; - m.specular_ior = blend_ior(mInput.specular_ior, sampled_ior); - m.roughness = mInput.roughness * sampled_roughness; - m.metallic = mInput.metallic * sampled_metallic; // Now with metallic texture support! - - // Ensure valid ranges - m.roughness = max(m.roughness, 0.01); - m.metallic = clamp(m.metallic, 0.0, 1.0); - - #else - // Fallback to standard material processing - m.basecolor = mInput.color * sampled_diffuse.xyz; - m.specular_ior = blend_ior(mInput.specular_ior, sampled_ior); - m.roughness = mInput.roughness * sampled_roughness; - m.metallic = mInput.metallic; // No metallic texture - #endif - - m.shading_model_param0 = mInput.arbitrary0; +# Enhanced PBR effect with metallic texture support +# This effect extends the default PBR to support metallic textures +# Avoids redeclaring textures that are already in the base template + +fragment: + defines: | + #define USE_METALLIC_TEXTURE 1 + + inout: | + uniform sampler2D p3d_Texture5; // Metallic texture (not in default template) + + material: | + // Enhanced material with metallic texture support + #if USE_METALLIC_TEXTURE + // Sample metallic texture + float sampled_metallic = texture(p3d_Texture5, texcoord).x; + + // Apply all material properties + // Use the sampled values from the base template for other textures + m.basecolor = mInput.color * sampled_diffuse.xyz; + m.specular_ior = blend_ior(mInput.specular_ior, sampled_ior); + m.roughness = mInput.roughness * sampled_roughness; + m.metallic = mInput.metallic * sampled_metallic; // Now with metallic texture support! + + // Ensure valid ranges + m.roughness = max(m.roughness, 0.01); + m.metallic = clamp(m.metallic, 0.0, 1.0); + + #else + // Fallback to standard material processing + m.basecolor = mInput.color * sampled_diffuse.xyz; + m.specular_ior = blend_ior(mInput.specular_ior, sampled_ior); + m.roughness = mInput.roughness * sampled_roughness; + m.metallic = mInput.metallic; // No metallic texture + #endif + + m.shading_model_param0 = mInput.arbitrary0; diff --git a/RenderPipelineFile/effects/roughness_only.yaml b/RenderPipelineFile/effects/roughness_only.yaml index a8e76f52..99469674 100644 --- a/RenderPipelineFile/effects/roughness_only.yaml +++ b/RenderPipelineFile/effects/roughness_only.yaml @@ -1,39 +1,39 @@ -# Roughness-only PBR effect -# This effect specifically handles roughness textures correctly -# Ensures roughness texture is not confused with diffuse texture - -fragment: - defines: | - #define USE_ROUGHNESS_TEXTURE_ONLY 1 - #define DONT_FETCH_DEFAULT_TEXTURES 0 - - inout: | - uniform sampler2D p3d_Texture3; // Roughness texture only - - material: | - // Only handle roughness texture, ignore others to avoid confusion - #if USE_ROUGHNESS_TEXTURE_ONLY - // Sample roughness from p3d_Texture3 - float sampled_roughness = texture(p3d_Texture3, texcoord).x; - - // Apply roughness: material_roughness * texture_value - // This gives Blender-like behavior - m.roughness = mInput.roughness * sampled_roughness; - - // Ensure minimum roughness to avoid rendering issues - m.roughness = max(m.roughness, 0.01); - - // Keep other properties from material (no texture interference) - m.basecolor = mInput.color; // Use material color, not texture - m.metallic = mInput.metallic; - m.specular_ior = mInput.specular_ior; - m.shading_model_param0 = mInput.arbitrary0; - - #else - // Fallback to material values only - m.basecolor = mInput.color; - m.roughness = mInput.roughness; - m.metallic = mInput.metallic; - m.specular_ior = mInput.specular_ior; - m.shading_model_param0 = mInput.arbitrary0; - #endif +# Roughness-only PBR effect +# This effect specifically handles roughness textures correctly +# Ensures roughness texture is not confused with diffuse texture + +fragment: + defines: | + #define USE_ROUGHNESS_TEXTURE_ONLY 1 + #define DONT_FETCH_DEFAULT_TEXTURES 0 + + inout: | + uniform sampler2D p3d_Texture3; // Roughness texture only + + material: | + // Only handle roughness texture, ignore others to avoid confusion + #if USE_ROUGHNESS_TEXTURE_ONLY + // Sample roughness from p3d_Texture3 + float sampled_roughness = texture(p3d_Texture3, texcoord).x; + + // Apply roughness: material_roughness * texture_value + // This gives Blender-like behavior + m.roughness = mInput.roughness * sampled_roughness; + + // Ensure minimum roughness to avoid rendering issues + m.roughness = max(m.roughness, 0.01); + + // Keep other properties from material (no texture interference) + m.basecolor = mInput.color; // Use material color, not texture + m.metallic = mInput.metallic; + m.specular_ior = mInput.specular_ior; + m.shading_model_param0 = mInput.arbitrary0; + + #else + // Fallback to material values only + m.basecolor = mInput.color; + m.roughness = mInput.roughness; + m.metallic = mInput.metallic; + m.specular_ior = mInput.specular_ior; + m.shading_model_param0 = mInput.arbitrary0; + #endif diff --git a/RenderPipelineFile/effects/simple_texture_test.yaml b/RenderPipelineFile/effects/simple_texture_test.yaml index 1ff9d08e..10ce88ed 100644 --- a/RenderPipelineFile/effects/simple_texture_test.yaml +++ b/RenderPipelineFile/effects/simple_texture_test.yaml @@ -1,31 +1,31 @@ -# 最简单的纹理测试效果 -# 直接将纹理显示为颜色,验证纹理是否被正确读取 - -fragment: - defines: | - #define SIMPLE_TEXTURE_TEST 1 - - inout: | - uniform sampler2D p3d_Texture3; // 粗糙度纹理槽 - - material: | - // 最简单的测试:直接显示纹理 - #if SIMPLE_TEXTURE_TEST - // 尝试读取纹理 - vec4 texture_sample = texture(p3d_Texture3, texcoord); - float texture_value = texture_sample.r; // 读取红色通道 - - // 直接将纹理值显示为颜色 - m.basecolor = vec3(texture_value); // 灰度显示 - m.roughness = 0.5; // 固定粗糙度 - m.metallic = 0.0; // 非金属 - m.specular_ior = 1.5; - m.shading_model_param0 = 0.0; - #else - // 正常材质 - m.basecolor = mInput.color; - m.roughness = mInput.roughness; - m.metallic = mInput.metallic; - m.specular_ior = mInput.specular_ior; - m.shading_model_param0 = mInput.arbitrary0; - #endif +# 最简单的纹理测试效果 +# 直接将纹理显示为颜色,验证纹理是否被正确读取 + +fragment: + defines: | + #define SIMPLE_TEXTURE_TEST 1 + + inout: | + uniform sampler2D p3d_Texture3; // 粗糙度纹理槽 + + material: | + // 最简单的测试:直接显示纹理 + #if SIMPLE_TEXTURE_TEST + // 尝试读取纹理 + vec4 texture_sample = texture(p3d_Texture3, texcoord); + float texture_value = texture_sample.r; // 读取红色通道 + + // 直接将纹理值显示为颜色 + m.basecolor = vec3(texture_value); // 灰度显示 + m.roughness = 0.5; // 固定粗糙度 + m.metallic = 0.0; // 非金属 + m.specular_ior = 1.5; + m.shading_model_param0 = 0.0; + #else + // 正常材质 + m.basecolor = mInput.color; + m.roughness = mInput.roughness; + m.metallic = mInput.metallic; + m.specular_ior = mInput.specular_ior; + m.shading_model_param0 = mInput.arbitrary0; + #endif diff --git a/RenderPipelineFile/effects/simple_transparent.yaml b/RenderPipelineFile/effects/simple_transparent.yaml index d1fbc2fc..3ce65adc 100644 --- a/RenderPipelineFile/effects/simple_transparent.yaml +++ b/RenderPipelineFile/effects/simple_transparent.yaml @@ -1,26 +1,26 @@ -# simple_transparent.yaml -vertex_shader: | - #version 330 - uniform mat4 p3d_ModelViewProjectionMatrix; - in vec4 p3d_Vertex; - in vec2 p3d_MultiTexCoord0; - out vec2 texcoord; - void main() { - gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; - texcoord = p3d_MultiTexCoord0; - } - -fragment_shader: | - #version 330 - uniform sampler2D p3d_Texture0; - uniform float material_opacity = 1.0; - in vec2 texcoord; - out vec4 o_color; - void main() { - vec4 c = texture(p3d_Texture0, texcoord); - o_color = vec4(c.rgb, c.a * material_opacity); - } - -render_states: - TransparencyAttrib: M_alpha +# simple_transparent.yaml +vertex_shader: | + #version 330 + uniform mat4 p3d_ModelViewProjectionMatrix; + in vec4 p3d_Vertex; + in vec2 p3d_MultiTexCoord0; + out vec2 texcoord; + void main() { + gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; + texcoord = p3d_MultiTexCoord0; + } + +fragment_shader: | + #version 330 + uniform sampler2D p3d_Texture0; + uniform float material_opacity = 1.0; + in vec2 texcoord; + out vec4 o_color; + void main() { + vec4 c = texture(p3d_Texture0, texcoord); + o_color = vec4(c.rgb, c.a * material_opacity); + } + +render_states: + TransparencyAttrib: M_alpha DepthWriteAttrib: 0 \ No newline at end of file diff --git a/RenderPipelineFile/effects/skybox.yaml b/RenderPipelineFile/effects/skybox.yaml index 27d6667c..f8989261 100644 --- a/RenderPipelineFile/effects/skybox.yaml +++ b/RenderPipelineFile/effects/skybox.yaml @@ -1,20 +1,20 @@ - -# Skybox effect - -vertex: - transform: | - // vOutput.position.xy += MainSceneData.camera_pos.xy; - -fragment: - defines: | - #define DONT_FETCH_DEFAULT_TEXTURES 1 - #define DONT_SET_MATERIAL_PROPERTIES 1 - - material: | - m.basecolor = vec3(0); - m.shading_model = SHADING_MODEL_EMISSIVE; - m.specular_ior = 1.0; - m.metallic = 0.0; - m.roughness = 1.0; - m.shading_model_param0 = 0.0; - m.normal = vOutput.normal; + +# Skybox effect + +vertex: + transform: | + // vOutput.position.xy += MainSceneData.camera_pos.xy; + +fragment: + defines: | + #define DONT_FETCH_DEFAULT_TEXTURES 1 + #define DONT_SET_MATERIAL_PROPERTIES 1 + + material: | + m.basecolor = vec3(0); + m.shading_model = SHADING_MODEL_EMISSIVE; + m.specular_ior = 1.0; + m.metallic = 0.0; + m.roughness = 1.0; + m.shading_model_param0 = 0.0; + m.normal = vOutput.normal; diff --git a/RenderPipelineFile/effects/terrain-effect.yaml b/RenderPipelineFile/effects/terrain-effect.yaml index f3197557..0b2ee052 100644 --- a/RenderPipelineFile/effects/terrain-effect.yaml +++ b/RenderPipelineFile/effects/terrain-effect.yaml @@ -1,142 +1,142 @@ - -# Terrain effect -# This effect uses prodecural shader splatting, you most likely want to modify -# it with your own texture-map generation code. - -vertex: - inout: | - uniform struct { - sampler2D data_texture; - sampler2D heightfield; - int view_index; - int terrain_size; - int chunk_size; - } ShaderTerrainMesh; - out vec2 terrain_uv; - - transform: | - // Terrain data has the layout: - // x: x-pos, y: y-pos, z: size, w: clod - vec4 terrain_data = texelFetch(ShaderTerrainMesh.data_texture, - ivec2(gl_InstanceID, ShaderTerrainMesh.view_index), 0); - - // Get initial chunk position in the (0, 0, 0), (1, 1, 0) range - vec3 chunk_position = p3d_Vertex.xyz; - - // CLOD implementation - float clod_factor = smoothstep(0, 1, terrain_data.w); - chunk_position.xy -= clod_factor * fract(chunk_position.xy * ShaderTerrainMesh.chunk_size / 2.0) - * 2.0 / ShaderTerrainMesh.chunk_size; - - // Scale the chunk - chunk_position *= terrain_data.z * float(ShaderTerrainMesh.chunk_size) - / float(ShaderTerrainMesh.terrain_size); - chunk_position.z *= ShaderTerrainMesh.chunk_size; - - // Offset the chunk, it is important that this happens after the scale - chunk_position.xy += terrain_data.xy / float(ShaderTerrainMesh.terrain_size); - - // Compute the terrain UV coordinates - terrain_uv = chunk_position.xy; - - // Sample the heightfield and offset the terrain - we do not need to multiply - // the height with anything since the terrain transform is included in the - // model view projection matrix. - chunk_position.z += texture(ShaderTerrainMesh.heightfield, terrain_uv).x; - - // Lower the terrain on the borders - this ensures the shadow map is generated - // correctly. - if ( min(terrain_uv.x, terrain_uv.y) < 8.0 / ShaderTerrainMesh.terrain_size || - max(terrain_uv.x, terrain_uv.y) > 1 - 9.0 / ShaderTerrainMesh.terrain_size) { - chunk_position.z = 0; - } - - vOutput.position = (p3d_ModelMatrix * vec4(chunk_position, 1)).xyz; - -fragment: - defines: | - #define DONT_FETCH_DEFAULT_TEXTURES 1 - #define DONT_SET_MATERIAL_PROPERTIES 1 - - inout: | - layout(location=4) in vec2 terrain_uv; - layout(location=5) uniform struct { - sampler2D data_texture; - sampler2D heightfield; - int view_index; - int terrain_size; - int chunk_size; - } ShaderTerrainMesh; - - material: | - - // Compute terrain normal - const float terrain_height = 1000.0; - vec3 pixel_size = vec3(1.0, -1.0, 0) / textureSize(ShaderTerrainMesh.heightfield, 0).xxx; - float h_u0 = texture(ShaderTerrainMesh.heightfield, terrain_uv + pixel_size.yz).x * terrain_height; - float h_u1 = texture(ShaderTerrainMesh.heightfield, terrain_uv + pixel_size.xz).x * terrain_height; - float h_v0 = texture(ShaderTerrainMesh.heightfield, terrain_uv + pixel_size.zy).x * terrain_height; - float h_v1 = texture(ShaderTerrainMesh.heightfield, terrain_uv + pixel_size.zx).x * terrain_height; - vec3 tangent = normalize(vec3(1, 0, h_u1 - h_u0)); - vec3 binormal = normalize(vec3(0, 1, h_v1 - h_v0)); - vec3 normal = normalize(cross(tangent, binormal)); - - normal.x *= -1; - // normal.y *= -1; - - // Material splatting - float height = (h_u0 + h_u1 + h_v0 + h_v1) / (4.0 * terrain_height); // xxx - float slope = 1.0 - normal.z; - float grass = 0.0; - float rock = 0.0; - float snow = 0.0; - - { // Snow - snow = saturate(4.0 * (height-0.49)); - snow *= saturate(pow(saturate(1.0 - slope), 2.0)) * 12.0; - - //snow -= 0.6; - //snow *= 0.5; - snow = saturate(snow); - snow = pow(snow, 2.0); - } - - { // Rock - rock = saturate((pow(slope, 1.2) * 12.0 - 0.02) * 4.5); - } - - { // Grass - grass = 1.0 - saturate(rock + snow); - } - - - // Material definitions - - MaterialShaderOutput grass_mat = make_default_material_output(); - grass_mat.basecolor = vec3(0.1, 0.2, 0.1); - grass_mat.roughness = 0.8; - - MaterialShaderOutput rock_mat = make_default_material_output(); - rock_mat.basecolor = vec3(0.13); - rock_mat.roughness = 0.8; - rock_mat.specular_ior = 1.4; - - MaterialShaderOutput snow_mat = make_default_material_output(); - snow_mat.basecolor = vec3(0.6, 0.6, 0.9); - snow_mat.roughness = 0.5; - snow_mat.specular_ior = 1.7; - - m.basecolor = vec3(0); - m.shading_model = SHADING_MODEL_DEFAULT; - m.specular_ior = 0.0; - m.metallic = 0.0; - m.roughness = 0.0; - m.shading_model_param0 = 0.0; - m.normal = vec3(0); - - merge_material_output(m, grass_mat, grass); - merge_material_output(m, rock_mat, rock); - merge_material_output(m, snow_mat, snow); - - m.normal = normal; - + +# Terrain effect +# This effect uses prodecural shader splatting, you most likely want to modify +# it with your own texture-map generation code. + +vertex: + inout: | + uniform struct { + sampler2D data_texture; + sampler2D heightfield; + int view_index; + int terrain_size; + int chunk_size; + } ShaderTerrainMesh; + out vec2 terrain_uv; + + transform: | + // Terrain data has the layout: + // x: x-pos, y: y-pos, z: size, w: clod + vec4 terrain_data = texelFetch(ShaderTerrainMesh.data_texture, + ivec2(gl_InstanceID, ShaderTerrainMesh.view_index), 0); + + // Get initial chunk position in the (0, 0, 0), (1, 1, 0) range + vec3 chunk_position = p3d_Vertex.xyz; + + // CLOD implementation + float clod_factor = smoothstep(0, 1, terrain_data.w); + chunk_position.xy -= clod_factor * fract(chunk_position.xy * ShaderTerrainMesh.chunk_size / 2.0) + * 2.0 / ShaderTerrainMesh.chunk_size; + + // Scale the chunk + chunk_position *= terrain_data.z * float(ShaderTerrainMesh.chunk_size) + / float(ShaderTerrainMesh.terrain_size); + chunk_position.z *= ShaderTerrainMesh.chunk_size; + + // Offset the chunk, it is important that this happens after the scale + chunk_position.xy += terrain_data.xy / float(ShaderTerrainMesh.terrain_size); + + // Compute the terrain UV coordinates + terrain_uv = chunk_position.xy; + + // Sample the heightfield and offset the terrain - we do not need to multiply + // the height with anything since the terrain transform is included in the + // model view projection matrix. + chunk_position.z += texture(ShaderTerrainMesh.heightfield, terrain_uv).x; + + // Lower the terrain on the borders - this ensures the shadow map is generated + // correctly. + if ( min(terrain_uv.x, terrain_uv.y) < 8.0 / ShaderTerrainMesh.terrain_size || + max(terrain_uv.x, terrain_uv.y) > 1 - 9.0 / ShaderTerrainMesh.terrain_size) { + chunk_position.z = 0; + } + + vOutput.position = (p3d_ModelMatrix * vec4(chunk_position, 1)).xyz; + +fragment: + defines: | + #define DONT_FETCH_DEFAULT_TEXTURES 1 + #define DONT_SET_MATERIAL_PROPERTIES 1 + + inout: | + layout(location=4) in vec2 terrain_uv; + layout(location=5) uniform struct { + sampler2D data_texture; + sampler2D heightfield; + int view_index; + int terrain_size; + int chunk_size; + } ShaderTerrainMesh; + + material: | + + // Compute terrain normal + const float terrain_height = 1000.0; + vec3 pixel_size = vec3(1.0, -1.0, 0) / textureSize(ShaderTerrainMesh.heightfield, 0).xxx; + float h_u0 = texture(ShaderTerrainMesh.heightfield, terrain_uv + pixel_size.yz).x * terrain_height; + float h_u1 = texture(ShaderTerrainMesh.heightfield, terrain_uv + pixel_size.xz).x * terrain_height; + float h_v0 = texture(ShaderTerrainMesh.heightfield, terrain_uv + pixel_size.zy).x * terrain_height; + float h_v1 = texture(ShaderTerrainMesh.heightfield, terrain_uv + pixel_size.zx).x * terrain_height; + vec3 tangent = normalize(vec3(1, 0, h_u1 - h_u0)); + vec3 binormal = normalize(vec3(0, 1, h_v1 - h_v0)); + vec3 normal = normalize(cross(tangent, binormal)); + + normal.x *= -1; + // normal.y *= -1; + + // Material splatting + float height = (h_u0 + h_u1 + h_v0 + h_v1) / (4.0 * terrain_height); // xxx + float slope = 1.0 - normal.z; + float grass = 0.0; + float rock = 0.0; + float snow = 0.0; + + { // Snow + snow = saturate(4.0 * (height-0.49)); + snow *= saturate(pow(saturate(1.0 - slope), 2.0)) * 12.0; + + //snow -= 0.6; + //snow *= 0.5; + snow = saturate(snow); + snow = pow(snow, 2.0); + } + + { // Rock + rock = saturate((pow(slope, 1.2) * 12.0 - 0.02) * 4.5); + } + + { // Grass + grass = 1.0 - saturate(rock + snow); + } + + + // Material definitions + + MaterialShaderOutput grass_mat = make_default_material_output(); + grass_mat.basecolor = vec3(0.1, 0.2, 0.1); + grass_mat.roughness = 0.8; + + MaterialShaderOutput rock_mat = make_default_material_output(); + rock_mat.basecolor = vec3(0.13); + rock_mat.roughness = 0.8; + rock_mat.specular_ior = 1.4; + + MaterialShaderOutput snow_mat = make_default_material_output(); + snow_mat.basecolor = vec3(0.6, 0.6, 0.9); + snow_mat.roughness = 0.5; + snow_mat.specular_ior = 1.7; + + m.basecolor = vec3(0); + m.shading_model = SHADING_MODEL_DEFAULT; + m.specular_ior = 0.0; + m.metallic = 0.0; + m.roughness = 0.0; + m.shading_model_param0 = 0.0; + m.normal = vec3(0); + + merge_material_output(m, grass_mat, grass); + merge_material_output(m, rock_mat, rock); + merge_material_output(m, snow_mat, snow); + + m.normal = normal; + diff --git a/RenderPipelineFile/effects/test_metallic_debug.yaml b/RenderPipelineFile/effects/test_metallic_debug.yaml index af8492f7..5fd90319 100644 --- a/RenderPipelineFile/effects/test_metallic_debug.yaml +++ b/RenderPipelineFile/effects/test_metallic_debug.yaml @@ -1,34 +1,34 @@ -# Test metallic texture debug effect -# This effect shows the metallic texture directly as color to verify it's working - -fragment: - defines: | - #define TEST_METALLIC_DEBUG 1 - - inout: | - uniform sampler2D p3d_Texture5; // Metallic texture - - material: | - // Debug: Show metallic texture directly as color - #if TEST_METALLIC_DEBUG - // Sample metallic texture - float metallic_value = texture(p3d_Texture5, texcoord).x; - - // Show metallic as blue-to-yellow gradient for debugging - vec3 debug_color = mix(vec3(0.2, 0.2, 1.0), vec3(1.0, 1.0, 0.2), metallic_value); - - // Apply debug visualization - m.basecolor = debug_color; // Show texture as blue-yellow gradient - m.roughness = 0.5; // Fixed roughness for consistent lighting - m.metallic = 0.0; // Non-metallic for clear visualization - m.specular_ior = 1.5; - m.shading_model_param0 = 0.0; - - #else - // Normal material processing - m.basecolor = mInput.color * sampled_diffuse.xyz; - m.specular_ior = blend_ior(mInput.specular_ior, sampled_ior); - m.roughness = mInput.roughness * sampled_roughness; - m.metallic = mInput.metallic; - m.shading_model_param0 = mInput.arbitrary0; - #endif +# Test metallic texture debug effect +# This effect shows the metallic texture directly as color to verify it's working + +fragment: + defines: | + #define TEST_METALLIC_DEBUG 1 + + inout: | + uniform sampler2D p3d_Texture5; // Metallic texture + + material: | + // Debug: Show metallic texture directly as color + #if TEST_METALLIC_DEBUG + // Sample metallic texture + float metallic_value = texture(p3d_Texture5, texcoord).x; + + // Show metallic as blue-to-yellow gradient for debugging + vec3 debug_color = mix(vec3(0.2, 0.2, 1.0), vec3(1.0, 1.0, 0.2), metallic_value); + + // Apply debug visualization + m.basecolor = debug_color; // Show texture as blue-yellow gradient + m.roughness = 0.5; // Fixed roughness for consistent lighting + m.metallic = 0.0; // Non-metallic for clear visualization + m.specular_ior = 1.5; + m.shading_model_param0 = 0.0; + + #else + // Normal material processing + m.basecolor = mInput.color * sampled_diffuse.xyz; + m.specular_ior = blend_ior(mInput.specular_ior, sampled_ior); + m.roughness = mInput.roughness * sampled_roughness; + m.metallic = mInput.metallic; + m.shading_model_param0 = mInput.arbitrary0; + #endif diff --git a/RenderPipelineFile/effects/test_roughness_debug.yaml b/RenderPipelineFile/effects/test_roughness_debug.yaml index 63fb0b22..a607e74a 100644 --- a/RenderPipelineFile/effects/test_roughness_debug.yaml +++ b/RenderPipelineFile/effects/test_roughness_debug.yaml @@ -1,34 +1,34 @@ -# Test roughness texture debug effect -# This effect shows the roughness texture directly as color to verify it's working - -fragment: - defines: | - #define TEST_ROUGHNESS_DEBUG 1 - - inout: | - uniform sampler2D p3d_Texture3; // Roughness texture - - material: | - // Debug: Show roughness texture directly as color - #if TEST_ROUGHNESS_DEBUG - // Sample roughness texture - float roughness_value = texture(p3d_Texture3, texcoord).x; - - // Show roughness as grayscale color for debugging - vec3 debug_color = vec3(roughness_value); - - // Apply debug visualization - m.basecolor = debug_color; // Show texture as grayscale - m.roughness = 0.5; // Fixed roughness for consistent lighting - m.metallic = 0.0; // Non-metallic for clear visualization - m.specular_ior = 1.5; - m.shading_model_param0 = 0.0; - - #else - // Normal material processing - m.basecolor = mInput.color * sampled_diffuse.xyz; - m.specular_ior = blend_ior(mInput.specular_ior, sampled_ior); - m.roughness = mInput.roughness * sampled_roughness; - m.metallic = mInput.metallic; - m.shading_model_param0 = mInput.arbitrary0; - #endif +# Test roughness texture debug effect +# This effect shows the roughness texture directly as color to verify it's working + +fragment: + defines: | + #define TEST_ROUGHNESS_DEBUG 1 + + inout: | + uniform sampler2D p3d_Texture3; // Roughness texture + + material: | + // Debug: Show roughness texture directly as color + #if TEST_ROUGHNESS_DEBUG + // Sample roughness texture + float roughness_value = texture(p3d_Texture3, texcoord).x; + + // Show roughness as grayscale color for debugging + vec3 debug_color = vec3(roughness_value); + + // Apply debug visualization + m.basecolor = debug_color; // Show texture as grayscale + m.roughness = 0.5; // Fixed roughness for consistent lighting + m.metallic = 0.0; // Non-metallic for clear visualization + m.specular_ior = 1.5; + m.shading_model_param0 = 0.0; + + #else + // Normal material processing + m.basecolor = mInput.color * sampled_diffuse.xyz; + m.specular_ior = blend_ior(mInput.specular_ior, sampled_ior); + m.roughness = mInput.roughness * sampled_roughness; + m.metallic = mInput.metallic; + m.shading_model_param0 = mInput.arbitrary0; + #endif diff --git a/RenderPipelineFile/rpcore/__init__.py b/RenderPipelineFile/rpcore/__init__.py index a10088ab..0a80730b 100644 --- a/RenderPipelineFile/rpcore/__init__.py +++ b/RenderPipelineFile/rpcore/__init__.py @@ -1,47 +1,47 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -# flake8: noqa - -__all__ = ("RenderPipeline", "SpotLight", "PointLight") - -# This file includes all classes from the pipeline which are exposed -from rpcore.render_pipeline import RenderPipeline -from rpcore.native import SpotLight, PointLight - -# Polyfill a set_shader_inputs function for older versions of Panda. -from panda3d.core import NodePath -from direct.extensions_native.extension_native_helpers import Dtool_funcToMethod -from rplibs.six import iteritems - -if not hasattr(NodePath, 'set_shader_inputs'): - def set_shader_inputs(self, **inputs): - set_shader_input = self.set_shader_input - for args in iteritems(inputs): - set_shader_input(*args) - - Dtool_funcToMethod(set_shader_inputs, NodePath) - del set_shader_inputs +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +# flake8: noqa + +__all__ = ("RenderPipeline", "SpotLight", "PointLight") + +# This file includes all classes from the pipeline which are exposed +from rpcore.render_pipeline import RenderPipeline +from rpcore.native import SpotLight, PointLight + +# Polyfill a set_shader_inputs function for older versions of Panda. +from panda3d.core import NodePath +from direct.extensions_native.extension_native_helpers import Dtool_funcToMethod +from rplibs.six import iteritems + +if not hasattr(NodePath, 'set_shader_inputs'): + def set_shader_inputs(self, **inputs): + set_shader_input = self.set_shader_input + for args in iteritems(inputs): + set_shader_input(*args) + + Dtool_funcToMethod(set_shader_inputs, NodePath) + del set_shader_inputs diff --git a/RenderPipelineFile/rpcore/common_resources.py b/RenderPipelineFile/rpcore/common_resources.py index 4043f384..702cf317 100644 --- a/RenderPipelineFile/rpcore/common_resources.py +++ b/RenderPipelineFile/rpcore/common_resources.py @@ -1,246 +1,246 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import division - -from panda3d.core import CS_yup_right, CS_zup_right, invert, Vec3, Mat4, Vec4 -from panda3d.core import SamplerState -from direct.stdpy.file import open - -from rpcore.globals import Globals -from rpcore.rpobject import RPObject -from rpcore.loader import RPLoader - -from rpcore.util.shader_input_blocks import GroupedInputBlock - - -class CommonResources(RPObject): - - """ This class manages the loading and binding of commonly used resources, - such as textures, models, but also shader inputs """ - - def __init__(self, pipeline): - RPObject.__init__(self) - self._pipeline = pipeline - self._showbase = Globals.base - self._ptas = {} - self._load_fonts() - self._load_textures() - self._setup_inputs() - - def _load_fonts(self): - """ Loads the default font used for rendering and assigns it to - Globals.font for further usage """ - font = RPLoader.load_font("/$$rp/data/font/Roboto-Medium.ttf") - font.set_pixels_per_unit(35) - font.set_poly_margin(0.0) - font.set_texture_margin(1) - font.set_bg(Vec4(1, 1, 1, 0)) - font.set_fg(Vec4(1, 1, 1, 1)) - Globals.font = font - - def _setup_inputs(self): - """ Creates commonly used shader inputs such as the current mvp and - registers them to the stage manager so they can be used for rendering """ - - self._input_ubo = GroupedInputBlock("MainSceneData") - inputs = ( - ("camera_pos", "vec3"), - ("view_proj_mat_no_jitter", "mat4"), - ("last_view_proj_mat_no_jitter", "mat4"), - ("last_inv_view_proj_mat_no_jitter", "mat4"), - ("view_mat_z_up", "mat4"), - ("proj_mat", "mat4"), - ("inv_proj_mat", "mat4"), - ("view_mat_billboard", "mat4"), - ("frame_delta", "float"), - ("smooth_frame_delta", "float"), - ("frame_time", "float"), - ("current_film_offset", "vec2"), - ("frame_index", "int"), - ("screen_size", "ivec2"), - ("native_screen_size", "ivec2"), - ("lc_tile_count", "ivec2"), - ("ws_frustum_directions", "mat4"), - ("vs_frustum_directions", "mat4"), - ) - for name, ipt_type in inputs: - self._input_ubo.register_pta(name, ipt_type) - - self._pipeline.stage_mgr.input_blocks.append(self._input_ubo) - - # Main camera and main render have to be regular inputs, since they are - # used in the shaders by that name. - self._pipeline.stage_mgr.inputs["mainCam"] = self._showbase.cam - self._pipeline.stage_mgr.inputs["mainRender"] = self._showbase.render - - # Set the correct frame rate interval - Globals.clock.set_average_frame_rate_interval(3.0) - - # Set initial value for view_proj_mat_no_jitter - view_mat = Globals.render.get_transform(self._showbase.cam).get_mat() - proj_mat = Mat4(self._showbase.camLens.get_projection_mat()) - proj_mat.set_cell(1, 0, 0.0) - proj_mat.set_cell(1, 1, 0.0) - self._input_ubo.update_input("view_proj_mat_no_jitter", view_mat * proj_mat) - - def write_config(self): - """ Generates the shader configuration for the common inputs """ - content = self._input_ubo.generate_shader_code() - try: - # Try to write the temporary file - with open("/$$rptemp/$$main_scene_data.inc.glsl", "w") as handle: - handle.write(content) - except IOError as msg: - self.error("Failed to write common resources shader configuration!", msg) - - def _load_textures(self): - """ Loads commonly used textures and makes them available via the - stage manager """ - self._load_environment_cubemap() - self._load_prefilter_brdf() - self._load_skydome() - - def _load_environment_cubemap(self): - """ Loads the default cubemap used for the environment, which is used - when no other environment data is available """ - envmap = RPLoader.load_cube_map( - "/$$rp/data/default_cubemap/cubemap.txo", read_mipmaps=True) - envmap.set_minfilter(SamplerState.FT_linear_mipmap_linear) - # envmap.set_format(Image.F_rgba16) - envmap.set_magfilter(SamplerState.FT_linear) - envmap.set_wrap_u(SamplerState.WM_repeat) - envmap.set_wrap_v(SamplerState.WM_repeat) - envmap.set_wrap_w(SamplerState.WM_repeat) - self._pipeline.stage_mgr.inputs["DefaultEnvmap"] = envmap - - def _load_prefilter_brdf(self): - """ Loads the prefiltered brdf """ - luts = [ - {"src": "slices/env_brdf_#.png", "input": "PrefilteredBRDF"}, - {"src": "slices_metal/env_brdf.png", "input": "PrefilteredMetalBRDF"}, - {"src": "slices_coat/env_brdf.png", "input": "PrefilteredCoatBRDF"}, - ] - - for config in luts: - loader_method = RPLoader.load_texture - if "#" in config["src"]: - loader_method = RPLoader.load_3d_texture - - brdf_tex = loader_method("/$$rp/data/environment_brdf/{}".format(config["src"])) - brdf_tex.set_minfilter(SamplerState.FT_linear) - brdf_tex.set_magfilter(SamplerState.FT_linear) - brdf_tex.set_wrap_u(SamplerState.WM_clamp) - brdf_tex.set_wrap_v(SamplerState.WM_clamp) - brdf_tex.set_wrap_w(SamplerState.WM_clamp) - brdf_tex.set_anisotropic_degree(0) - self._pipeline.stage_mgr.inputs[config["input"]] = brdf_tex - - def _load_skydome(self): - """ Loads the skydome """ - skydome = RPLoader.load_texture("/$$rp/data/builtin_models/skybox/skybox.txo") - skydome.set_wrap_u(SamplerState.WM_clamp) - skydome.set_wrap_v(SamplerState.WM_clamp) - self._pipeline.stage_mgr.inputs["DefaultSkydome"] = skydome - - def load_default_skybox(self): - skybox = RPLoader.load_model("/$$rp/data/builtin_models/skybox/skybox.bam") - return skybox - - def update(self): - """ Updates the commonly used resources, mostly the shader inputs """ - update = self._input_ubo.update_input - - # Get the current transform matrix of the camera - view_mat = Globals.render.get_transform(self._showbase.cam).get_mat() - - # Compute the view matrix, but with a z-up coordinate system - zup_conversion = Mat4.convert_mat(CS_zup_right, CS_yup_right) - update("view_mat_z_up", view_mat * zup_conversion) - - # Compute the view matrix without the camera rotation - view_mat_billboard = Mat4(view_mat) - view_mat_billboard.set_row(0, Vec3(1, 0, 0)) - view_mat_billboard.set_row(1, Vec3(0, 1, 0)) - view_mat_billboard.set_row(2, Vec3(0, 0, 1)) - update("view_mat_billboard", view_mat_billboard) - - update("camera_pos", self._showbase.camera.get_pos(Globals.render)) - - # Compute last view projection mat - curr_vp = self._input_ubo.get_input("view_proj_mat_no_jitter") - update("last_view_proj_mat_no_jitter", curr_vp) - curr_vp = Mat4(curr_vp) - curr_vp.invert_in_place() - curr_inv_vp = curr_vp - update("last_inv_view_proj_mat_no_jitter", curr_inv_vp) - - proj_mat = Mat4(self._showbase.camLens.get_projection_mat()) - - # Set the projection matrix as an input, but convert it to the correct - # coordinate system before. - proj_mat_zup = Mat4.convert_mat(CS_yup_right, CS_zup_right) * proj_mat - update("proj_mat", proj_mat_zup) - - # Set the inverse projection matrix - update("inv_proj_mat", invert(proj_mat_zup)) - - # Remove jitter and set the new view projection mat - proj_mat.set_cell(1, 0, 0.0) - proj_mat.set_cell(1, 1, 0.0) - update("view_proj_mat_no_jitter", view_mat * proj_mat) - - # Store the frame delta - update("frame_delta", Globals.clock.get_dt()) - update("smooth_frame_delta", 1.0 / max(1e-5, Globals.clock.get_average_frame_rate())) - update("frame_time", Globals.clock.get_frame_time()) - - # Store the current film offset, we use this to compute the pixel-perfect - # velocity, which is otherwise not possible. Usually this is always 0 - # except when SMAA and reprojection is enabled - update("current_film_offset", self._showbase.camLens.get_film_offset()) - update("frame_index", Globals.clock.get_frame_count()) - - # Compute frustum corners in the order BL, BR, TL, TR - ws_frustum_directions = Mat4() - vs_frustum_directions = Mat4() - inv_proj_mat = Globals.base.camLens.get_projection_mat_inv() - view_mat_inv = Mat4(view_mat) - view_mat_inv.invert_in_place() - - for i, point in enumerate(((-1, -1), (1, -1), (-1, 1), (1, 1))): - result = inv_proj_mat.xform(Vec4(point[0], point[1], 1.0, 1.0)) - vs_dir = (zup_conversion.xform(result)).xyz.normalized() - vs_frustum_directions.set_row(i, Vec4(vs_dir, 1)) - ws_dir = view_mat_inv.xform(Vec4(result.xyz, 0)) - ws_frustum_directions.set_row(i, ws_dir) - - update("vs_frustum_directions", vs_frustum_directions) - update("ws_frustum_directions", ws_frustum_directions) - - update("screen_size", Globals.resolution) - update("native_screen_size", Globals.native_resolution) - update("lc_tile_count", self._pipeline.light_mgr.num_tiles) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import division + +from panda3d.core import CS_yup_right, CS_zup_right, invert, Vec3, Mat4, Vec4 +from panda3d.core import SamplerState +from direct.stdpy.file import open + +from rpcore.globals import Globals +from rpcore.rpobject import RPObject +from rpcore.loader import RPLoader + +from rpcore.util.shader_input_blocks import GroupedInputBlock + + +class CommonResources(RPObject): + + """ This class manages the loading and binding of commonly used resources, + such as textures, models, but also shader inputs """ + + def __init__(self, pipeline): + RPObject.__init__(self) + self._pipeline = pipeline + self._showbase = Globals.base + self._ptas = {} + self._load_fonts() + self._load_textures() + self._setup_inputs() + + def _load_fonts(self): + """ Loads the default font used for rendering and assigns it to + Globals.font for further usage """ + font = RPLoader.load_font("/$$rp/data/font/Roboto-Medium.ttf") + font.set_pixels_per_unit(35) + font.set_poly_margin(0.0) + font.set_texture_margin(1) + font.set_bg(Vec4(1, 1, 1, 0)) + font.set_fg(Vec4(1, 1, 1, 1)) + Globals.font = font + + def _setup_inputs(self): + """ Creates commonly used shader inputs such as the current mvp and + registers them to the stage manager so they can be used for rendering """ + + self._input_ubo = GroupedInputBlock("MainSceneData") + inputs = ( + ("camera_pos", "vec3"), + ("view_proj_mat_no_jitter", "mat4"), + ("last_view_proj_mat_no_jitter", "mat4"), + ("last_inv_view_proj_mat_no_jitter", "mat4"), + ("view_mat_z_up", "mat4"), + ("proj_mat", "mat4"), + ("inv_proj_mat", "mat4"), + ("view_mat_billboard", "mat4"), + ("frame_delta", "float"), + ("smooth_frame_delta", "float"), + ("frame_time", "float"), + ("current_film_offset", "vec2"), + ("frame_index", "int"), + ("screen_size", "ivec2"), + ("native_screen_size", "ivec2"), + ("lc_tile_count", "ivec2"), + ("ws_frustum_directions", "mat4"), + ("vs_frustum_directions", "mat4"), + ) + for name, ipt_type in inputs: + self._input_ubo.register_pta(name, ipt_type) + + self._pipeline.stage_mgr.input_blocks.append(self._input_ubo) + + # Main camera and main render have to be regular inputs, since they are + # used in the shaders by that name. + self._pipeline.stage_mgr.inputs["mainCam"] = self._showbase.cam + self._pipeline.stage_mgr.inputs["mainRender"] = self._showbase.render + + # Set the correct frame rate interval + Globals.clock.set_average_frame_rate_interval(3.0) + + # Set initial value for view_proj_mat_no_jitter + view_mat = Globals.render.get_transform(self._showbase.cam).get_mat() + proj_mat = Mat4(self._showbase.camLens.get_projection_mat()) + proj_mat.set_cell(1, 0, 0.0) + proj_mat.set_cell(1, 1, 0.0) + self._input_ubo.update_input("view_proj_mat_no_jitter", view_mat * proj_mat) + + def write_config(self): + """ Generates the shader configuration for the common inputs """ + content = self._input_ubo.generate_shader_code() + try: + # Try to write the temporary file + with open("/$$rptemp/$$main_scene_data.inc.glsl", "w") as handle: + handle.write(content) + except IOError as msg: + self.error("Failed to write common resources shader configuration!", msg) + + def _load_textures(self): + """ Loads commonly used textures and makes them available via the + stage manager """ + self._load_environment_cubemap() + self._load_prefilter_brdf() + self._load_skydome() + + def _load_environment_cubemap(self): + """ Loads the default cubemap used for the environment, which is used + when no other environment data is available """ + envmap = RPLoader.load_cube_map( + "/$$rp/data/default_cubemap/cubemap.txo", read_mipmaps=True) + envmap.set_minfilter(SamplerState.FT_linear_mipmap_linear) + # envmap.set_format(Image.F_rgba16) + envmap.set_magfilter(SamplerState.FT_linear) + envmap.set_wrap_u(SamplerState.WM_repeat) + envmap.set_wrap_v(SamplerState.WM_repeat) + envmap.set_wrap_w(SamplerState.WM_repeat) + self._pipeline.stage_mgr.inputs["DefaultEnvmap"] = envmap + + def _load_prefilter_brdf(self): + """ Loads the prefiltered brdf """ + luts = [ + {"src": "slices/env_brdf_#.png", "input": "PrefilteredBRDF"}, + {"src": "slices_metal/env_brdf.png", "input": "PrefilteredMetalBRDF"}, + {"src": "slices_coat/env_brdf.png", "input": "PrefilteredCoatBRDF"}, + ] + + for config in luts: + loader_method = RPLoader.load_texture + if "#" in config["src"]: + loader_method = RPLoader.load_3d_texture + + brdf_tex = loader_method("/$$rp/data/environment_brdf/{}".format(config["src"])) + brdf_tex.set_minfilter(SamplerState.FT_linear) + brdf_tex.set_magfilter(SamplerState.FT_linear) + brdf_tex.set_wrap_u(SamplerState.WM_clamp) + brdf_tex.set_wrap_v(SamplerState.WM_clamp) + brdf_tex.set_wrap_w(SamplerState.WM_clamp) + brdf_tex.set_anisotropic_degree(0) + self._pipeline.stage_mgr.inputs[config["input"]] = brdf_tex + + def _load_skydome(self): + """ Loads the skydome """ + skydome = RPLoader.load_texture("/$$rp/data/builtin_models/skybox/skybox.txo") + skydome.set_wrap_u(SamplerState.WM_clamp) + skydome.set_wrap_v(SamplerState.WM_clamp) + self._pipeline.stage_mgr.inputs["DefaultSkydome"] = skydome + + def load_default_skybox(self): + skybox = RPLoader.load_model("/$$rp/data/builtin_models/skybox/skybox.bam") + return skybox + + def update(self): + """ Updates the commonly used resources, mostly the shader inputs """ + update = self._input_ubo.update_input + + # Get the current transform matrix of the camera + view_mat = Globals.render.get_transform(self._showbase.cam).get_mat() + + # Compute the view matrix, but with a z-up coordinate system + zup_conversion = Mat4.convert_mat(CS_zup_right, CS_yup_right) + update("view_mat_z_up", view_mat * zup_conversion) + + # Compute the view matrix without the camera rotation + view_mat_billboard = Mat4(view_mat) + view_mat_billboard.set_row(0, Vec3(1, 0, 0)) + view_mat_billboard.set_row(1, Vec3(0, 1, 0)) + view_mat_billboard.set_row(2, Vec3(0, 0, 1)) + update("view_mat_billboard", view_mat_billboard) + + update("camera_pos", self._showbase.camera.get_pos(Globals.render)) + + # Compute last view projection mat + curr_vp = self._input_ubo.get_input("view_proj_mat_no_jitter") + update("last_view_proj_mat_no_jitter", curr_vp) + curr_vp = Mat4(curr_vp) + curr_vp.invert_in_place() + curr_inv_vp = curr_vp + update("last_inv_view_proj_mat_no_jitter", curr_inv_vp) + + proj_mat = Mat4(self._showbase.camLens.get_projection_mat()) + + # Set the projection matrix as an input, but convert it to the correct + # coordinate system before. + proj_mat_zup = Mat4.convert_mat(CS_yup_right, CS_zup_right) * proj_mat + update("proj_mat", proj_mat_zup) + + # Set the inverse projection matrix + update("inv_proj_mat", invert(proj_mat_zup)) + + # Remove jitter and set the new view projection mat + proj_mat.set_cell(1, 0, 0.0) + proj_mat.set_cell(1, 1, 0.0) + update("view_proj_mat_no_jitter", view_mat * proj_mat) + + # Store the frame delta + update("frame_delta", Globals.clock.get_dt()) + update("smooth_frame_delta", 1.0 / max(1e-5, Globals.clock.get_average_frame_rate())) + update("frame_time", Globals.clock.get_frame_time()) + + # Store the current film offset, we use this to compute the pixel-perfect + # velocity, which is otherwise not possible. Usually this is always 0 + # except when SMAA and reprojection is enabled + update("current_film_offset", self._showbase.camLens.get_film_offset()) + update("frame_index", Globals.clock.get_frame_count()) + + # Compute frustum corners in the order BL, BR, TL, TR + ws_frustum_directions = Mat4() + vs_frustum_directions = Mat4() + inv_proj_mat = Globals.base.camLens.get_projection_mat_inv() + view_mat_inv = Mat4(view_mat) + view_mat_inv.invert_in_place() + + for i, point in enumerate(((-1, -1), (1, -1), (-1, 1), (1, 1))): + result = inv_proj_mat.xform(Vec4(point[0], point[1], 1.0, 1.0)) + vs_dir = (zup_conversion.xform(result)).xyz.normalized() + vs_frustum_directions.set_row(i, Vec4(vs_dir, 1)) + ws_dir = view_mat_inv.xform(Vec4(result.xyz, 0)) + ws_frustum_directions.set_row(i, ws_dir) + + update("vs_frustum_directions", vs_frustum_directions) + update("ws_frustum_directions", ws_frustum_directions) + + update("screen_size", Globals.resolution) + update("native_screen_size", Globals.native_resolution) + update("lc_tile_count", self._pipeline.light_mgr.num_tiles) diff --git a/RenderPipelineFile/rpcore/effect.py b/RenderPipelineFile/rpcore/effect.py index a84e3eb2..b13fe4a3 100644 --- a/RenderPipelineFile/rpcore/effect.py +++ b/RenderPipelineFile/rpcore/effect.py @@ -1,307 +1,307 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rplibs.six import iteritems, iterkeys -from rplibs.yaml import load_yaml_file - -from panda3d.core import Filename -from direct.stdpy.file import open - -from rpcore.rpobject import RPObject -from rpcore.loader import RPLoader - - -class Effect(RPObject): - - """ This class represents an instance of a compiled effect. It can be loaded - from a file. """ - - # Configuration options which can be set per effect instance. These control - # which features are available in the effect, and which passes to render. - _DEFAULT_OPTIONS = { - "render_gbuffer": True, - "render_shadow": True, - "render_voxelize": True, - "render_envmap": True, - "render_forward": False, - "alpha_testing": True, - "normal_mapping": True, - "parallax_mapping": False, - } - - # All supported render passes, should match the available passes in the - # TagStateManager class. - _PASSES = ("gbuffer", "shadow", "voxelize", "envmap", "forward") - - # Effects are cached based on their source filename and options, this is - # the cache where compiled are effects stored. - _GLOBAL_CACHE = {} - - # Global counter to store the amount of generated effects, used to create - # a unique id used for writing temporary files. - _EFFECT_ID = 0 - - @classmethod - def load(cls, filename, options): - """ Loads an effect from a given filename with the specified options. - This lookups in the global effect cache, and checks if a similar effect - (i.e. with the same hash) was already loaded, and in that case returns it. - Otherwise a new effect with the given options is created. """ - effect_hash = cls._generate_hash(filename, options) - if effect_hash in cls._GLOBAL_CACHE: - return cls._GLOBAL_CACHE[effect_hash] - effect = cls() - effect.set_options(options) - if not effect.do_load(filename): - RPObject.global_error("Effect", "Could not load effect!") - return None - return effect - - @classmethod - def _generate_hash(cls, filename, options): - """ Generates an unique hash for the effect. The effect hash is based - on the filename and the configured options, and is ensured to make the - effect unique. This is important to make sure the caching works as - intended. All options not present in options are set to the default value""" - - # Set all options which are not present in the dict to its defaults - options = {k: options.get(k, v) for k, v in iteritems(cls._DEFAULT_OPTIONS)} - - # Hash filename, make sure it has the right format and also resolve - # it to an absolute path, to make sure that relative paths are cached - # correctly (otherwise, specifying a different path to the same file - # will cause a cache miss) - filename = Filename(filename) - filename.make_absolute() - file_hash = str(hash(filename.to_os_generic())) - - # Hash the options, that is, sort the keys to make sure the values - # are always in the same order, and then convert the flags to strings using - # '1' for a set flag, and '0' for a unset flag - options_hash = "".join(["1" if options[key] else "0" for key in sorted(iterkeys(options))]) - return file_hash + "-" + options_hash - - def __init__(self): - """ Constructs a new empty effect, this is a private constructor and - should not be called. Instead, use Effect.load() """ - RPObject.__init__(self) - self.effect_id = Effect._EFFECT_ID - Effect._EFFECT_ID += 1 - self.filename = None - self._options = self._DEFAULT_OPTIONS.copy() - self._generated_shader_paths = {} - self._shader_objs = {} - - def get_option(self, name): - """ Returns a given option value by name """ - return self._options[name] - - def set_options(self, options): - """ Sets the effect options, overriding the default options """ - for key, val in iteritems(options): - if key not in self._options: - self.error("Unkown option:", key) - continue - self._options[key] = val - - def do_load(self, filename): - """ Internal method to load the effect from the given filename, do - not use this directly, instead use load(). """ - self.filename = filename - self.effect_name = self._convert_filename_to_name(filename) - self.effect_hash = self._generate_hash(filename, self._options) - - # Load the YAML file - parsed_yaml = load_yaml_file(filename) or {} - self._parse_content(parsed_yaml) - - # Construct a shader object for each pass - for pass_id in self._PASSES: - vertex_src = self._generated_shader_paths["vertex-" + pass_id] - fragment_src = self._generated_shader_paths["fragment-" + pass_id] - self._shader_objs[pass_id] = RPLoader.load_shader(vertex_src, fragment_src) - return True - - def get_shader_obj(self, pass_id): - """ Returns a handle to the compiled shader object for a given render - pass. """ - if pass_id not in self._shader_objs: - self.warn("Pass '" + pass_id + "' not found!") - return False - return self._shader_objs[pass_id] - - def _convert_filename_to_name(self, filename): - """ Constructs an effect name from a filename, this is used for writing - out temporary files """ - return filename.replace(".yaml", "").replace("effects/", "")\ - .replace("/", "_").replace("\\", "_").replace(".", "-") - - def _parse_content(self, parsed_yaml): - """ Internal method to construct the effect from a yaml object """ - vtx_data = parsed_yaml.get("vertex", None) or {} - frag_data = parsed_yaml.get("fragment", None) or {} - - for pass_id in self._PASSES: - self._parse_shader_template(pass_id, "vertex", vtx_data) - self._parse_shader_template(pass_id, "fragment", frag_data) - - def _parse_shader_template(self, pass_id, stage, data): - """ Parses a fragment template. This just finds the default template - for the shader, and redirects that to construct_shader_from_data """ - if stage == "fragment": - shader_ext = {"vertex": "vert", "fragment": "frag"}[stage] - template_src = "/$$rp/shader/templates/{}.{}.glsl".format(pass_id, shader_ext) - elif stage == "vertex": - # Using a shared vertex shader - template_src = "/$$rp/shader/templates/vertex.vert.glsl" - - shader_path = self._construct_shader_from_data(pass_id, stage, template_src, data) - self._generated_shader_paths[stage + "-" + pass_id] = shader_path - - def _construct_shader_from_data(self, pass_id, stage, template_src, data): # noqa # pylint: disable=too-many-branches - """ Constructs a shader from a given dataset """ - injects = {"defines": []} - - for key, val in iteritems(self._options): - if isinstance(val, bool): - val_str = "1" if val else "0" - else: - val_str = str(val) - injects["defines"].append("#define OPT_{} {}".format(key.upper(), val_str)) - - injects["defines"].append("#define IN_" + stage.upper() + "_SHADER 1") - injects["defines"].append("#define IN_" + pass_id.upper() + "_SHADER 1") - injects["defines"].append("#define IN_RENDERING_PASS 1") - - # Parse dependencies - if "dependencies" in data: - injects["includes"] = [] - for dependency in data["dependencies"]: - include_str = "#pragma include \"{}\"".format(dependency) - injects["includes"].append(include_str) - del data["dependencies"] - - # Append aditional injects - for key, val in iteritems(data): - if val is None: - self.warn("Empty insertion: '" + key + "'") - continue - - if isinstance(val, (list, tuple)): - self.warn("Invalid syntax, you used a list but you should have used a string:") - self.warn(val) - continue - injects[key] = injects.get(key, []) + [i for i in val.split("\n")] - - cache_key = self.effect_name + "@" + stage + "-" + pass_id + "@" + self.effect_hash - return self._process_shader_template(template_src, cache_key, injects) - - def _process_shader_template(self, template_src, cache_key, injections): # noqa # pylint: disable=too-many-branches - """ Generates a compiled shader object from a given shader - source location and code injection definitions. """ - with open(template_src, "r") as handle: - shader_lines = handle.readlines() - - parsed_lines = ["\n\n"] - addline = parsed_lines.append - - addline("/* Compiled Shader Template") - addline(" * generated from: '" + template_src + "'") - addline(" * cache key: '" + cache_key + "'") - addline(" *") - addline(" * !!! Autogenerated, do not edit! Your changes will be lost. !!!") - addline(" */\n\n") - - # Store whether we are in the main function already - we need this - # to properly insert scoped code blocks - in_main = False - - for line in shader_lines: # pylint: disable=too-many-nested-blocks - stripped_line = line.strip().lower() - - # Check if we are already in the main function - if "void main()" in stripped_line: - in_main = True - - # Check if the current line is a hook - if stripped_line.startswith("%") and stripped_line.endswith("%"): - - # If the line is a hook, get the hook name and save the - # indent so we can indent all injected lines properly. - hook_name = stripped_line[1:-1] - indent = " " * (len(line) - len(line.lstrip())) - - # Inject all registered template values into the hook - if hook_name in injections: - - # Directly remove the value from the list so we can check which - # hooks were not found in the template - insertions = injections.pop(hook_name) - - if len(insertions) > 0: - - # When we are in the main function, we have to make sure we - # use a seperate scope, so there are no conflicts with variable - # declarations - header = indent + "/* Hook: " + hook_name + " */" + (" {" if in_main else "") # noqa # pylint: disable=line-too-long - addline(header) - - for line_to_insert in insertions: - if line_to_insert is None: - self.warn("Empty insertion '" + hook_name + "'") - continue - - if not isinstance(line_to_insert, str): - self.warn("Invalid line type: ", line_to_insert) - continue - - # Dont indent defines and pragmas - if line_to_insert.startswith("#"): - addline(line_to_insert) - else: - addline(indent + line_to_insert) - - if in_main: - addline(indent + "}") - - else: - addline(line.rstrip()) - - # Add a closing newline to the file - addline("") - - # Warn the user about all unused hooks - for key in injections: - self.warn("Hook '" + key + "' not found in template '" + template_src + "'!") - - # Write the constructed shader and load it back - shader_content = "\n".join(parsed_lines) - temp_path = "/$$rptemp/$$effect-" + cache_key + ".glsl" - - with open(temp_path, "w") as handle: - handle.write(shader_content) - - return temp_path +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rplibs.six import iteritems, iterkeys +from rplibs.yaml import load_yaml_file + +from panda3d.core import Filename +from direct.stdpy.file import open + +from rpcore.rpobject import RPObject +from rpcore.loader import RPLoader + + +class Effect(RPObject): + + """ This class represents an instance of a compiled effect. It can be loaded + from a file. """ + + # Configuration options which can be set per effect instance. These control + # which features are available in the effect, and which passes to render. + _DEFAULT_OPTIONS = { + "render_gbuffer": True, + "render_shadow": True, + "render_voxelize": True, + "render_envmap": True, + "render_forward": False, + "alpha_testing": True, + "normal_mapping": True, + "parallax_mapping": False, + } + + # All supported render passes, should match the available passes in the + # TagStateManager class. + _PASSES = ("gbuffer", "shadow", "voxelize", "envmap", "forward") + + # Effects are cached based on their source filename and options, this is + # the cache where compiled are effects stored. + _GLOBAL_CACHE = {} + + # Global counter to store the amount of generated effects, used to create + # a unique id used for writing temporary files. + _EFFECT_ID = 0 + + @classmethod + def load(cls, filename, options): + """ Loads an effect from a given filename with the specified options. + This lookups in the global effect cache, and checks if a similar effect + (i.e. with the same hash) was already loaded, and in that case returns it. + Otherwise a new effect with the given options is created. """ + effect_hash = cls._generate_hash(filename, options) + if effect_hash in cls._GLOBAL_CACHE: + return cls._GLOBAL_CACHE[effect_hash] + effect = cls() + effect.set_options(options) + if not effect.do_load(filename): + RPObject.global_error("Effect", "Could not load effect!") + return None + return effect + + @classmethod + def _generate_hash(cls, filename, options): + """ Generates an unique hash for the effect. The effect hash is based + on the filename and the configured options, and is ensured to make the + effect unique. This is important to make sure the caching works as + intended. All options not present in options are set to the default value""" + + # Set all options which are not present in the dict to its defaults + options = {k: options.get(k, v) for k, v in iteritems(cls._DEFAULT_OPTIONS)} + + # Hash filename, make sure it has the right format and also resolve + # it to an absolute path, to make sure that relative paths are cached + # correctly (otherwise, specifying a different path to the same file + # will cause a cache miss) + filename = Filename(filename) + filename.make_absolute() + file_hash = str(hash(filename.to_os_generic())) + + # Hash the options, that is, sort the keys to make sure the values + # are always in the same order, and then convert the flags to strings using + # '1' for a set flag, and '0' for a unset flag + options_hash = "".join(["1" if options[key] else "0" for key in sorted(iterkeys(options))]) + return file_hash + "-" + options_hash + + def __init__(self): + """ Constructs a new empty effect, this is a private constructor and + should not be called. Instead, use Effect.load() """ + RPObject.__init__(self) + self.effect_id = Effect._EFFECT_ID + Effect._EFFECT_ID += 1 + self.filename = None + self._options = self._DEFAULT_OPTIONS.copy() + self._generated_shader_paths = {} + self._shader_objs = {} + + def get_option(self, name): + """ Returns a given option value by name """ + return self._options[name] + + def set_options(self, options): + """ Sets the effect options, overriding the default options """ + for key, val in iteritems(options): + if key not in self._options: + self.error("Unkown option:", key) + continue + self._options[key] = val + + def do_load(self, filename): + """ Internal method to load the effect from the given filename, do + not use this directly, instead use load(). """ + self.filename = filename + self.effect_name = self._convert_filename_to_name(filename) + self.effect_hash = self._generate_hash(filename, self._options) + + # Load the YAML file + parsed_yaml = load_yaml_file(filename) or {} + self._parse_content(parsed_yaml) + + # Construct a shader object for each pass + for pass_id in self._PASSES: + vertex_src = self._generated_shader_paths["vertex-" + pass_id] + fragment_src = self._generated_shader_paths["fragment-" + pass_id] + self._shader_objs[pass_id] = RPLoader.load_shader(vertex_src, fragment_src) + return True + + def get_shader_obj(self, pass_id): + """ Returns a handle to the compiled shader object for a given render + pass. """ + if pass_id not in self._shader_objs: + self.warn("Pass '" + pass_id + "' not found!") + return False + return self._shader_objs[pass_id] + + def _convert_filename_to_name(self, filename): + """ Constructs an effect name from a filename, this is used for writing + out temporary files """ + return filename.replace(".yaml", "").replace("effects/", "")\ + .replace("/", "_").replace("\\", "_").replace(".", "-") + + def _parse_content(self, parsed_yaml): + """ Internal method to construct the effect from a yaml object """ + vtx_data = parsed_yaml.get("vertex", None) or {} + frag_data = parsed_yaml.get("fragment", None) or {} + + for pass_id in self._PASSES: + self._parse_shader_template(pass_id, "vertex", vtx_data) + self._parse_shader_template(pass_id, "fragment", frag_data) + + def _parse_shader_template(self, pass_id, stage, data): + """ Parses a fragment template. This just finds the default template + for the shader, and redirects that to construct_shader_from_data """ + if stage == "fragment": + shader_ext = {"vertex": "vert", "fragment": "frag"}[stage] + template_src = "/$$rp/shader/templates/{}.{}.glsl".format(pass_id, shader_ext) + elif stage == "vertex": + # Using a shared vertex shader + template_src = "/$$rp/shader/templates/vertex.vert.glsl" + + shader_path = self._construct_shader_from_data(pass_id, stage, template_src, data) + self._generated_shader_paths[stage + "-" + pass_id] = shader_path + + def _construct_shader_from_data(self, pass_id, stage, template_src, data): # noqa # pylint: disable=too-many-branches + """ Constructs a shader from a given dataset """ + injects = {"defines": []} + + for key, val in iteritems(self._options): + if isinstance(val, bool): + val_str = "1" if val else "0" + else: + val_str = str(val) + injects["defines"].append("#define OPT_{} {}".format(key.upper(), val_str)) + + injects["defines"].append("#define IN_" + stage.upper() + "_SHADER 1") + injects["defines"].append("#define IN_" + pass_id.upper() + "_SHADER 1") + injects["defines"].append("#define IN_RENDERING_PASS 1") + + # Parse dependencies + if "dependencies" in data: + injects["includes"] = [] + for dependency in data["dependencies"]: + include_str = "#pragma include \"{}\"".format(dependency) + injects["includes"].append(include_str) + del data["dependencies"] + + # Append aditional injects + for key, val in iteritems(data): + if val is None: + self.warn("Empty insertion: '" + key + "'") + continue + + if isinstance(val, (list, tuple)): + self.warn("Invalid syntax, you used a list but you should have used a string:") + self.warn(val) + continue + injects[key] = injects.get(key, []) + [i for i in val.split("\n")] + + cache_key = self.effect_name + "@" + stage + "-" + pass_id + "@" + self.effect_hash + return self._process_shader_template(template_src, cache_key, injects) + + def _process_shader_template(self, template_src, cache_key, injections): # noqa # pylint: disable=too-many-branches + """ Generates a compiled shader object from a given shader + source location and code injection definitions. """ + with open(template_src, "r") as handle: + shader_lines = handle.readlines() + + parsed_lines = ["\n\n"] + addline = parsed_lines.append + + addline("/* Compiled Shader Template") + addline(" * generated from: '" + template_src + "'") + addline(" * cache key: '" + cache_key + "'") + addline(" *") + addline(" * !!! Autogenerated, do not edit! Your changes will be lost. !!!") + addline(" */\n\n") + + # Store whether we are in the main function already - we need this + # to properly insert scoped code blocks + in_main = False + + for line in shader_lines: # pylint: disable=too-many-nested-blocks + stripped_line = line.strip().lower() + + # Check if we are already in the main function + if "void main()" in stripped_line: + in_main = True + + # Check if the current line is a hook + if stripped_line.startswith("%") and stripped_line.endswith("%"): + + # If the line is a hook, get the hook name and save the + # indent so we can indent all injected lines properly. + hook_name = stripped_line[1:-1] + indent = " " * (len(line) - len(line.lstrip())) + + # Inject all registered template values into the hook + if hook_name in injections: + + # Directly remove the value from the list so we can check which + # hooks were not found in the template + insertions = injections.pop(hook_name) + + if len(insertions) > 0: + + # When we are in the main function, we have to make sure we + # use a seperate scope, so there are no conflicts with variable + # declarations + header = indent + "/* Hook: " + hook_name + " */" + (" {" if in_main else "") # noqa # pylint: disable=line-too-long + addline(header) + + for line_to_insert in insertions: + if line_to_insert is None: + self.warn("Empty insertion '" + hook_name + "'") + continue + + if not isinstance(line_to_insert, str): + self.warn("Invalid line type: ", line_to_insert) + continue + + # Dont indent defines and pragmas + if line_to_insert.startswith("#"): + addline(line_to_insert) + else: + addline(indent + line_to_insert) + + if in_main: + addline(indent + "}") + + else: + addline(line.rstrip()) + + # Add a closing newline to the file + addline("") + + # Warn the user about all unused hooks + for key in injections: + self.warn("Hook '" + key + "' not found in template '" + template_src + "'!") + + # Write the constructed shader and load it back + shader_content = "\n".join(parsed_lines) + temp_path = "/$$rptemp/$$effect-" + cache_key + ".glsl" + + with open(temp_path, "w") as handle: + handle.write(shader_content) + + return temp_path diff --git a/RenderPipelineFile/rpcore/globals.py b/RenderPipelineFile/rpcore/globals.py index 11b4a75e..9c71e166 100644 --- a/RenderPipelineFile/rpcore/globals.py +++ b/RenderPipelineFile/rpcore/globals.py @@ -1,47 +1,47 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -__all__ = ("Globals",) - - -class Globals(object): # pylint: disable=too-few-public-methods - - """ This class is a singleton to store globals widely used by the application. - This is a wrapper around Panda3D's globals since ShowBase writes to __builtins__ - which is bad practice. This class also attempts to help IDEs to figure out - where the variables come from and where they are defined. """ - - __init__ = None - - @staticmethod - def load(showbase): - """ Loads the globals from a given showbase """ - Globals.base = showbase - Globals.render = showbase.render - Globals.clock = showbase.taskMgr.globalClock - Globals.font = None - Globals.resolution = None - Globals.native_resolution = None +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +__all__ = ("Globals",) + + +class Globals(object): # pylint: disable=too-few-public-methods + + """ This class is a singleton to store globals widely used by the application. + This is a wrapper around Panda3D's globals since ShowBase writes to __builtins__ + which is bad practice. This class also attempts to help IDEs to figure out + where the variables come from and where they are defined. """ + + __init__ = None + + @staticmethod + def load(showbase): + """ Loads the globals from a given showbase """ + Globals.base = showbase + Globals.render = showbase.render + Globals.clock = showbase.taskMgr.globalClock + Globals.font = None + Globals.resolution = None + Globals.native_resolution = None diff --git a/RenderPipelineFile/rpcore/gpu_command_queue.py b/RenderPipelineFile/rpcore/gpu_command_queue.py index 9bfe17ae..7ddb9790 100644 --- a/RenderPipelineFile/rpcore/gpu_command_queue.py +++ b/RenderPipelineFile/rpcore/gpu_command_queue.py @@ -1,117 +1,117 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import PTAInt - -from rpcore.image import Image -from rpcore.rpobject import RPObject -from rpcore.loader import RPLoader -from rpcore.render_target import RenderTarget - -from rpcore.native import GPUCommand, GPUCommandList - - -class GPUCommandQueue(RPObject): - - """ This class offers an interface to the gpu, allowing commands to be - pushed to a queue which then get executed on the gpu """ - - def __init__(self, pipeline): - RPObject.__init__(self) - self._pipeline = pipeline - self._commands_per_frame = 1024 - self._command_list = GPUCommandList() - self._pta_num_commands = PTAInt.empty_array(1) - self._create_data_storage() - self._create_command_target() - self._commands = [] - self._register_defines() - - def clear_queue(self): - """ Clears all commands currently being in the queue """ - raise NotImplementedError() - - @property - def command_list(self): - """ Returns a handle to the command list """ - return self._command_list - - @property - def num_queued_commands(self): - """ Returns the amount of queued commands, which are waiting to get - executed on the gpu. This might be zero a lot of the time, because the - GPUCommandList clears the queue after executing, so you have to call - this after work was submitted. """ - return self._command_list.num_commands - - @property - def num_processed_commands(self): - """ Returns the amount of commands processed the last time when the - command queue was updated """ - return self._pta_num_commands[0] - - def process_queue(self): - """ Processes the n first commands of the queue """ - pointer = self._data_texture.modify_ram_image() - num_commands_exec = self._command_list.write_commands_to( - pointer, self._commands_per_frame) - self._pta_num_commands[0] = num_commands_exec - - def reload_shaders(self): - """ Reloads the command shader """ - shader = RPLoader.load_shader( - "/$$rp/shader/default_post_process.vert.glsl", - "/$$rp/shader/process_command_queue.frag.glsl") - self._command_target.shader = shader - - def register_input(self, key, val): - """ Registers an new shader input to the command target """ - self._command_target.set_shader_input(key, val) - - def _register_defines(self): - """ Registers all the command types as defines so they can be used - in a shader later on """ - for attr in dir(GPUCommand): - if attr.startswith("CMD_"): - attr_val = getattr(GPUCommand, attr) - self._pipeline.stage_mgr.defines[attr] = attr_val - self._pipeline.stage_mgr.defines["GPU_CMD_INT_AS_FLOAT"] = \ - GPUCommand.get_uses_integer_packing() - - def _create_data_storage(self): - """ Creates the buffer used to transfer commands """ - command_buffer_size = self._commands_per_frame * 32 - self.debug("Allocating command buffer of size", command_buffer_size) - self._data_texture = Image.create_buffer("CommandQueue", command_buffer_size, "R32") - - def _create_command_target(self): - """ Creates the target which processes the commands """ - self._command_target = RenderTarget("ExecCommandTarget") - self._command_target.size = 1, 1 - self._command_target.prepare_buffer() - self._command_target.set_shader_inputs( - CommandQueue=self._data_texture, - commandCount=self._pta_num_commands) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import PTAInt + +from rpcore.image import Image +from rpcore.rpobject import RPObject +from rpcore.loader import RPLoader +from rpcore.render_target import RenderTarget + +from rpcore.native import GPUCommand, GPUCommandList + + +class GPUCommandQueue(RPObject): + + """ This class offers an interface to the gpu, allowing commands to be + pushed to a queue which then get executed on the gpu """ + + def __init__(self, pipeline): + RPObject.__init__(self) + self._pipeline = pipeline + self._commands_per_frame = 1024 + self._command_list = GPUCommandList() + self._pta_num_commands = PTAInt.empty_array(1) + self._create_data_storage() + self._create_command_target() + self._commands = [] + self._register_defines() + + def clear_queue(self): + """ Clears all commands currently being in the queue """ + raise NotImplementedError() + + @property + def command_list(self): + """ Returns a handle to the command list """ + return self._command_list + + @property + def num_queued_commands(self): + """ Returns the amount of queued commands, which are waiting to get + executed on the gpu. This might be zero a lot of the time, because the + GPUCommandList clears the queue after executing, so you have to call + this after work was submitted. """ + return self._command_list.num_commands + + @property + def num_processed_commands(self): + """ Returns the amount of commands processed the last time when the + command queue was updated """ + return self._pta_num_commands[0] + + def process_queue(self): + """ Processes the n first commands of the queue """ + pointer = self._data_texture.modify_ram_image() + num_commands_exec = self._command_list.write_commands_to( + pointer, self._commands_per_frame) + self._pta_num_commands[0] = num_commands_exec + + def reload_shaders(self): + """ Reloads the command shader """ + shader = RPLoader.load_shader( + "/$$rp/shader/default_post_process.vert.glsl", + "/$$rp/shader/process_command_queue.frag.glsl") + self._command_target.shader = shader + + def register_input(self, key, val): + """ Registers an new shader input to the command target """ + self._command_target.set_shader_input(key, val) + + def _register_defines(self): + """ Registers all the command types as defines so they can be used + in a shader later on """ + for attr in dir(GPUCommand): + if attr.startswith("CMD_"): + attr_val = getattr(GPUCommand, attr) + self._pipeline.stage_mgr.defines[attr] = attr_val + self._pipeline.stage_mgr.defines["GPU_CMD_INT_AS_FLOAT"] = \ + GPUCommand.get_uses_integer_packing() + + def _create_data_storage(self): + """ Creates the buffer used to transfer commands """ + command_buffer_size = self._commands_per_frame * 32 + self.debug("Allocating command buffer of size", command_buffer_size) + self._data_texture = Image.create_buffer("CommandQueue", command_buffer_size, "R32") + + def _create_command_target(self): + """ Creates the target which processes the commands """ + self._command_target = RenderTarget("ExecCommandTarget") + self._command_target.size = 1, 1 + self._command_target.prepare_buffer() + self._command_target.set_shader_inputs( + CommandQueue=self._data_texture, + commandCount=self._pta_num_commands) diff --git a/RenderPipelineFile/rpcore/gui/README.md b/RenderPipelineFile/rpcore/gui/README.md index acbc2eb3..e7e26ad7 100644 --- a/RenderPipelineFile/rpcore/gui/README.md +++ b/RenderPipelineFile/rpcore/gui/README.md @@ -1,6 +1,6 @@ - -## rp-gui - -This folder only contains helper classes for the pipeline gui. -It is not really part of the pipeline, and thus also does not follow the -pipeline code conventions. + +## rp-gui + +This folder only contains helper classes for the pipeline gui. +It is not really part of the pipeline, and thus also does not follow the +pipeline code conventions. diff --git a/RenderPipelineFile/rpcore/gui/buffer_viewer.py b/RenderPipelineFile/rpcore/gui/buffer_viewer.py index 5dc59a9c..965118f0 100644 --- a/RenderPipelineFile/rpcore/gui/buffer_viewer.py +++ b/RenderPipelineFile/rpcore/gui/buffer_viewer.py @@ -1,255 +1,255 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import division -from functools import partial - -from panda3d.core import Texture, Vec3 - -from direct.gui.DirectFrame import DirectFrame -from direct.gui.DirectGui import DGG -from direct.gui.DirectScrolledFrame import DirectScrolledFrame - -from rplibs.six import itervalues - -from rpcore.image import Image -from rpcore.util.display_shader_builder import DisplayShaderBuilder -from rpcore.globals import Globals -from rpcore.render_target import RenderTarget -from rpcore.gui.texture_preview import TexturePreview -from rpcore.gui.sprite import Sprite -from rpcore.gui.labeled_checkbox import LabeledCheckbox -from rpcore.gui.text import Text -from rpcore.gui.draggable_window import DraggableWindow - - -class BufferViewer(DraggableWindow): - - """ This class provides a view into the buffers to inspect them """ - - def __init__(self, pipeline, parent): - """ Constructs the buffer viewer """ - DraggableWindow.__init__(self, width=1400, height=800, parent=parent, - title="Buffer- and Image-Browser") - self._pipeline = pipeline - self._scroll_height = 3000 - self._display_images = False - self._stages = [] - self._create_components() - self._tex_preview = TexturePreview(self._pipeline, parent) - self._tex_preview.hide() - self.hide() - - def toggle(self): - """ Updates all the buffers and then toggles the buffer viewer """ - if self._visible: - self._remove_components() - self.hide() - else: - self._perform_update() - self.show() - - @property - def entries(self): - """ Returns a list of all registered entries """ - return RenderTarget.REGISTERED_TARGETS + Image.REGISTERED_IMAGES - - @property - def stage_information(self): - """ Returns the amount of attached stages, and also the memory consumed - in MiB in a tuple. """ - count, memory = 0, 0 - for entry in self.entries: - if isinstance(entry, Texture): - memory += entry.estimate_texture_memory() - count += 1 - elif entry.__class__.__name__ == "RenderTarget": - for target in itervalues(entry.targets): - memory += target.estimate_texture_memory() - count += 1 - else: - self.warn("Unkown type:", entry.__class__.__name__) - return memory, count - - def _create_components(self): - """ Creates the window components """ - DraggableWindow._create_components(self) - - self._content_frame = DirectScrolledFrame( - frameSize=(0, self._width - 15, 0, self._height - 70), - canvasSize=(0, self._width - 80, 0, self._scroll_height), - autoHideScrollBars=False, - scrollBarWidth=12.0, - frameColor=(0, 0, 0, 0), - verticalScroll_relief=DGG.FLAT, - verticalScroll_incButton_relief=DGG.FLAT, - verticalScroll_decButton_relief=DGG.FLAT, - verticalScroll_thumb_relief=DGG.FLAT, - verticalScroll_frameColor=(0.05, 0.05, 0.05, 1), - verticalScroll_thumb_frameColor=(0.8, 0.8, 0.8, 1), - verticalScroll_incButton_frameColor=(0.6, 0.6, 0.6, 1), - verticalScroll_decButton_frameColor=(0.6, 0.6, 0.6, 1), - horizontalScroll_frameColor=(0, 0, 0, 0), - horizontalScroll_relief=False, - horizontalScroll_thumb_relief=False, - horizontalScroll_incButton_relief=False, - horizontalScroll_decButton_relief=False, - parent=self._node, - pos=(0, 1, -self._height)) - self._content_node = self._content_frame.getCanvas().attach_new_node( - "BufferComponents") - self._content_node.set_scale(1, 1, -1) - self._content_node.set_z(self._scroll_height) - self._chb_show_images = LabeledCheckbox( - parent=self._node, x=10, y=43, chb_callback=self._set_show_images, - chb_checked=False, text="Display image resources", - text_color=Vec3(0.4), expand_width=330) - - def _set_show_images(self, arg): - """ Sets whether images and textures will be shown """ - self._display_images = arg - self._perform_update() - - def _set_scroll_height(self, height): - """ Sets the maximum scroll height in the content frame """ - self._scroll_height = height - self._content_frame["canvasSize"] = (0, self._width - 80, 0, self._scroll_height) - self._content_node.set_z(self._scroll_height) - - def _remove_components(self): - """ Removes all components of the buffer viewer """ - self._content_node.node().remove_all_children() - self._tex_preview.hide() - - def _perform_update(self): - """ Collects all entries, extracts their images and re-renders the - window """ - - # Collect texture stages - self._stages = [] - for entry in sorted(self.entries, key=lambda entry: entry.sort): - if isinstance(entry, Texture): - if self._display_images: - self._stages.append(entry) - # Can not use isinstance or we get circular import references - elif entry.__class__.__name__ == "RenderTarget": - for target in itervalues(entry.targets): - self._stages.append(target) - else: - self.warn("Unrecognized instance!", entry.__class__) - - self._render_stages() - - def _on_texture_hovered(self, hover_frame, evt=None): # pylint: disable=unused-argument - """ Internal method when a texture is hovered """ - hover_frame["frameColor"] = (0, 0, 0, 0.1) - - def _on_texture_blurred(self, hover_frame, evt=None): # pylint: disable=unused-argument - """ Internal method when a texture is blurred """ - hover_frame["frameColor"] = (0, 0, 0, 0) - - def _on_texture_clicked(self, tex_handle, evt=None): # pylint: disable=unused-argument - """ Internal method when a texture is blurred """ - self._tex_preview.present(tex_handle) - - def _render_stages(self): - """ Renders the stages to the window """ - - self._remove_components() - entries_per_row = 6 - aspect = Globals.native_resolution.y / Globals.native_resolution.x - entry_width = 235 - entry_height = (entry_width - 20) * aspect + 55 - - # Store already processed images - processed = set() - index = -1 - # Iterate over all stages - for stage_tex in self._stages: - if stage_tex in processed: - continue - processed.add(stage_tex) - index += 1 - stage_name = stage_tex.get_name() - - xoffs = index % entries_per_row - yoffs = index // entries_per_row - node = self._content_node.attach_new_node("Preview") - node.set_sz(-1) - node.set_pos(10 + xoffs * (entry_width - 14), 1, yoffs * (entry_height - 14 + 10)) - - r, g, b = 0.2, 0.2, 0.2 - if isinstance(stage_tex, Image): - r, g, b = 0.2, 0.4, 0.6 - - stage_name = stage_name.replace("render_pipeline_internal:", "") - parts = stage_name.split(":") - stage_name = parts[-1] - DirectFrame( - parent=node, frameSize=(7, entry_width - 17, -7, -entry_height + 17), - frameColor=(r, g, b, 1.0), pos=(0, 0, 0)) - - frame_hover = DirectFrame( - parent=node, frameSize=(0, entry_width - 10, 0, -entry_height + 10), - frameColor=(0, 0, 0, 0), pos=(0, 0, 0), state=DGG.NORMAL) - frame_hover.bind( - DGG.ENTER, partial(self._on_texture_hovered, frame_hover)) - frame_hover.bind( - DGG.EXIT, partial(self._on_texture_blurred, frame_hover)) - frame_hover.bind( - DGG.B1PRESS, partial(self._on_texture_clicked, stage_tex)) - - Text(text=stage_name, x=15, y=29, parent=node, size=12, color=Vec3(0.8)) - - # Scale image so it always fits - w, h = stage_tex.get_x_size(), stage_tex.get_y_size() - padd_x, padd_y = 24, 57 - scale_x = (entry_width - padd_x) / max(1, w) - scale_y = (entry_height - padd_y) / max(1, h) - scale_factor = min(scale_x, scale_y) - - if stage_tex.get_texture_type() == Image.TT_buffer_texture: - scale_factor = 1 - w = entry_width - padd_x - h = entry_height - padd_y - - preview = Sprite( - image=stage_tex, w=scale_factor * w, h=scale_factor * h, - any_filter=False, parent=node, x=7, y=40, transparent=False) - - preview.set_shader_inputs( - mipmap=0, - slice=0, - brightness=1, - tonemap=False) - - preview_shader = DisplayShaderBuilder.build( - stage_tex, scale_factor * w, scale_factor * h) - preview.set_shader(preview_shader) - - num_rows = (index + entries_per_row) // entries_per_row - - self._set_scroll_height(50 + (entry_height - 14 + 10) * num_rows) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import division +from functools import partial + +from panda3d.core import Texture, Vec3 + +from direct.gui.DirectFrame import DirectFrame +from direct.gui.DirectGui import DGG +from direct.gui.DirectScrolledFrame import DirectScrolledFrame + +from rplibs.six import itervalues + +from rpcore.image import Image +from rpcore.util.display_shader_builder import DisplayShaderBuilder +from rpcore.globals import Globals +from rpcore.render_target import RenderTarget +from rpcore.gui.texture_preview import TexturePreview +from rpcore.gui.sprite import Sprite +from rpcore.gui.labeled_checkbox import LabeledCheckbox +from rpcore.gui.text import Text +from rpcore.gui.draggable_window import DraggableWindow + + +class BufferViewer(DraggableWindow): + + """ This class provides a view into the buffers to inspect them """ + + def __init__(self, pipeline, parent): + """ Constructs the buffer viewer """ + DraggableWindow.__init__(self, width=1400, height=800, parent=parent, + title="Buffer- and Image-Browser") + self._pipeline = pipeline + self._scroll_height = 3000 + self._display_images = False + self._stages = [] + self._create_components() + self._tex_preview = TexturePreview(self._pipeline, parent) + self._tex_preview.hide() + self.hide() + + def toggle(self): + """ Updates all the buffers and then toggles the buffer viewer """ + if self._visible: + self._remove_components() + self.hide() + else: + self._perform_update() + self.show() + + @property + def entries(self): + """ Returns a list of all registered entries """ + return RenderTarget.REGISTERED_TARGETS + Image.REGISTERED_IMAGES + + @property + def stage_information(self): + """ Returns the amount of attached stages, and also the memory consumed + in MiB in a tuple. """ + count, memory = 0, 0 + for entry in self.entries: + if isinstance(entry, Texture): + memory += entry.estimate_texture_memory() + count += 1 + elif entry.__class__.__name__ == "RenderTarget": + for target in itervalues(entry.targets): + memory += target.estimate_texture_memory() + count += 1 + else: + self.warn("Unkown type:", entry.__class__.__name__) + return memory, count + + def _create_components(self): + """ Creates the window components """ + DraggableWindow._create_components(self) + + self._content_frame = DirectScrolledFrame( + frameSize=(0, self._width - 15, 0, self._height - 70), + canvasSize=(0, self._width - 80, 0, self._scroll_height), + autoHideScrollBars=False, + scrollBarWidth=12.0, + frameColor=(0, 0, 0, 0), + verticalScroll_relief=DGG.FLAT, + verticalScroll_incButton_relief=DGG.FLAT, + verticalScroll_decButton_relief=DGG.FLAT, + verticalScroll_thumb_relief=DGG.FLAT, + verticalScroll_frameColor=(0.05, 0.05, 0.05, 1), + verticalScroll_thumb_frameColor=(0.8, 0.8, 0.8, 1), + verticalScroll_incButton_frameColor=(0.6, 0.6, 0.6, 1), + verticalScroll_decButton_frameColor=(0.6, 0.6, 0.6, 1), + horizontalScroll_frameColor=(0, 0, 0, 0), + horizontalScroll_relief=False, + horizontalScroll_thumb_relief=False, + horizontalScroll_incButton_relief=False, + horizontalScroll_decButton_relief=False, + parent=self._node, + pos=(0, 1, -self._height)) + self._content_node = self._content_frame.getCanvas().attach_new_node( + "BufferComponents") + self._content_node.set_scale(1, 1, -1) + self._content_node.set_z(self._scroll_height) + self._chb_show_images = LabeledCheckbox( + parent=self._node, x=10, y=43, chb_callback=self._set_show_images, + chb_checked=False, text="Display image resources", + text_color=Vec3(0.4), expand_width=330) + + def _set_show_images(self, arg): + """ Sets whether images and textures will be shown """ + self._display_images = arg + self._perform_update() + + def _set_scroll_height(self, height): + """ Sets the maximum scroll height in the content frame """ + self._scroll_height = height + self._content_frame["canvasSize"] = (0, self._width - 80, 0, self._scroll_height) + self._content_node.set_z(self._scroll_height) + + def _remove_components(self): + """ Removes all components of the buffer viewer """ + self._content_node.node().remove_all_children() + self._tex_preview.hide() + + def _perform_update(self): + """ Collects all entries, extracts their images and re-renders the + window """ + + # Collect texture stages + self._stages = [] + for entry in sorted(self.entries, key=lambda entry: entry.sort): + if isinstance(entry, Texture): + if self._display_images: + self._stages.append(entry) + # Can not use isinstance or we get circular import references + elif entry.__class__.__name__ == "RenderTarget": + for target in itervalues(entry.targets): + self._stages.append(target) + else: + self.warn("Unrecognized instance!", entry.__class__) + + self._render_stages() + + def _on_texture_hovered(self, hover_frame, evt=None): # pylint: disable=unused-argument + """ Internal method when a texture is hovered """ + hover_frame["frameColor"] = (0, 0, 0, 0.1) + + def _on_texture_blurred(self, hover_frame, evt=None): # pylint: disable=unused-argument + """ Internal method when a texture is blurred """ + hover_frame["frameColor"] = (0, 0, 0, 0) + + def _on_texture_clicked(self, tex_handle, evt=None): # pylint: disable=unused-argument + """ Internal method when a texture is blurred """ + self._tex_preview.present(tex_handle) + + def _render_stages(self): + """ Renders the stages to the window """ + + self._remove_components() + entries_per_row = 6 + aspect = Globals.native_resolution.y / Globals.native_resolution.x + entry_width = 235 + entry_height = (entry_width - 20) * aspect + 55 + + # Store already processed images + processed = set() + index = -1 + # Iterate over all stages + for stage_tex in self._stages: + if stage_tex in processed: + continue + processed.add(stage_tex) + index += 1 + stage_name = stage_tex.get_name() + + xoffs = index % entries_per_row + yoffs = index // entries_per_row + node = self._content_node.attach_new_node("Preview") + node.set_sz(-1) + node.set_pos(10 + xoffs * (entry_width - 14), 1, yoffs * (entry_height - 14 + 10)) + + r, g, b = 0.2, 0.2, 0.2 + if isinstance(stage_tex, Image): + r, g, b = 0.2, 0.4, 0.6 + + stage_name = stage_name.replace("render_pipeline_internal:", "") + parts = stage_name.split(":") + stage_name = parts[-1] + DirectFrame( + parent=node, frameSize=(7, entry_width - 17, -7, -entry_height + 17), + frameColor=(r, g, b, 1.0), pos=(0, 0, 0)) + + frame_hover = DirectFrame( + parent=node, frameSize=(0, entry_width - 10, 0, -entry_height + 10), + frameColor=(0, 0, 0, 0), pos=(0, 0, 0), state=DGG.NORMAL) + frame_hover.bind( + DGG.ENTER, partial(self._on_texture_hovered, frame_hover)) + frame_hover.bind( + DGG.EXIT, partial(self._on_texture_blurred, frame_hover)) + frame_hover.bind( + DGG.B1PRESS, partial(self._on_texture_clicked, stage_tex)) + + Text(text=stage_name, x=15, y=29, parent=node, size=12, color=Vec3(0.8)) + + # Scale image so it always fits + w, h = stage_tex.get_x_size(), stage_tex.get_y_size() + padd_x, padd_y = 24, 57 + scale_x = (entry_width - padd_x) / max(1, w) + scale_y = (entry_height - padd_y) / max(1, h) + scale_factor = min(scale_x, scale_y) + + if stage_tex.get_texture_type() == Image.TT_buffer_texture: + scale_factor = 1 + w = entry_width - padd_x + h = entry_height - padd_y + + preview = Sprite( + image=stage_tex, w=scale_factor * w, h=scale_factor * h, + any_filter=False, parent=node, x=7, y=40, transparent=False) + + preview.set_shader_inputs( + mipmap=0, + slice=0, + brightness=1, + tonemap=False) + + preview_shader = DisplayShaderBuilder.build( + stage_tex, scale_factor * w, scale_factor * h) + preview.set_shader(preview_shader) + + num_rows = (index + entries_per_row) // entries_per_row + + self._set_scroll_height(50 + (entry_height - 14 + 10) * num_rows) diff --git a/RenderPipelineFile/rpcore/gui/checkbox.py b/RenderPipelineFile/rpcore/gui/checkbox.py index a61aec94..b1427760 100644 --- a/RenderPipelineFile/rpcore/gui/checkbox.py +++ b/RenderPipelineFile/rpcore/gui/checkbox.py @@ -1,130 +1,130 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import TransparencyAttrib, SamplerState -from direct.gui.DirectCheckBox import DirectCheckBox -import direct.gui.DirectGuiGlobals as DGG - -from rpcore.rpobject import RPObject -from rpcore.loader import RPLoader - - -class Checkbox(RPObject): - - """ This is a wrapper around DirectCheckBox, providing a simpler interface - and better visuals """ - - def __init__(self, parent=None, x=0, y=0, callback=None, extra_args=None, - radio=False, expand_width=100, checked=False, enabled=True): - RPObject.__init__(self) - - prefix = "checkbox" if not radio else "radiobox" - - if enabled: - checked_img = RPLoader.load_texture( - "/$$rp/data/gui/" + prefix + "_checked.png") - unchecked_img = RPLoader.load_texture( - "/$$rp/data/gui/" + prefix + "_default.png") - else: - checked_img = RPLoader.load_texture( - "/$$rp/data/gui/" + prefix + "_disabled.png") - unchecked_img = checked_img - - # Set near filter, otherwise textures look like crap - for tex in [checked_img, unchecked_img]: - tex.set_minfilter(SamplerState.FT_linear) - tex.set_magfilter(SamplerState.FT_linear) - tex.set_wrap_u(SamplerState.WM_clamp) - tex.set_wrap_v(SamplerState.WM_clamp) - tex.set_anisotropic_degree(0) - - self._node = DirectCheckBox( - parent=parent, pos=(x + 11, 1, -y - 8), scale=(10 / 2.0, 1, 10 / 2.0), - checkedImage=checked_img, uncheckedImage=unchecked_img, - image=unchecked_img, extraArgs=extra_args, state=DGG.NORMAL, - relief=DGG.FLAT, command=self._update_status) - - self._node["frameColor"] = (0, 0, 0, 0) - self._node["frameSize"] = (-2.6, 2 + expand_width / 7.5, -2.35, 2.5) - self._node.set_transparency(TransparencyAttrib.M_alpha) - - self._callback = callback - self._extra_args = extra_args - self._collection = None - - if checked: - self.set_checked(True, False) - - @property - def collection(self): - """ Returns a handle to the assigned checkbox collection, or None - if no collection was assigned """ - return self._collection - - @collection.setter - def collection(self, coll): - """ Internal method to add a checkbox to a checkbox collection, this - is used for radio-buttons """ - self._collection = coll - - @property - def checked(self): - """ Returns whether the node is currently checked """ - return self._node["isChecked"] - - @property - def node(self): - """ Returns a handle to the internally used node """ - return self._node - - def _update_status(self, status): - """ Internal method when another checkbox in the same radio group - changed it's value """ - - if not status and self._collection: - self._node.commandFunc(None) - return - - if self._collection: - if status: - self._collection.on_checkbox_changed(self) - # A radio box can't be unchecked - # self._node["state"] = DGG.DISABLED - - if self._callback is not None: - self._callback(*([status] + self._extra_args)) - - def set_checked(self, val, do_callback=True): - """ Internal method to check/uncheck the checkbox """ - self._node["isChecked"] = val - - if val: - self._node["image"] = self._node["checkedImage"] - else: - self._node["image"] = self._node["uncheckedImage"] - - if do_callback and self._callback is not None: - self._callback(*([val] + self._extra_args)) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import TransparencyAttrib, SamplerState +from direct.gui.DirectCheckBox import DirectCheckBox +import direct.gui.DirectGuiGlobals as DGG + +from rpcore.rpobject import RPObject +from rpcore.loader import RPLoader + + +class Checkbox(RPObject): + + """ This is a wrapper around DirectCheckBox, providing a simpler interface + and better visuals """ + + def __init__(self, parent=None, x=0, y=0, callback=None, extra_args=None, + radio=False, expand_width=100, checked=False, enabled=True): + RPObject.__init__(self) + + prefix = "checkbox" if not radio else "radiobox" + + if enabled: + checked_img = RPLoader.load_texture( + "/$$rp/data/gui/" + prefix + "_checked.png") + unchecked_img = RPLoader.load_texture( + "/$$rp/data/gui/" + prefix + "_default.png") + else: + checked_img = RPLoader.load_texture( + "/$$rp/data/gui/" + prefix + "_disabled.png") + unchecked_img = checked_img + + # Set near filter, otherwise textures look like crap + for tex in [checked_img, unchecked_img]: + tex.set_minfilter(SamplerState.FT_linear) + tex.set_magfilter(SamplerState.FT_linear) + tex.set_wrap_u(SamplerState.WM_clamp) + tex.set_wrap_v(SamplerState.WM_clamp) + tex.set_anisotropic_degree(0) + + self._node = DirectCheckBox( + parent=parent, pos=(x + 11, 1, -y - 8), scale=(10 / 2.0, 1, 10 / 2.0), + checkedImage=checked_img, uncheckedImage=unchecked_img, + image=unchecked_img, extraArgs=extra_args, state=DGG.NORMAL, + relief=DGG.FLAT, command=self._update_status) + + self._node["frameColor"] = (0, 0, 0, 0) + self._node["frameSize"] = (-2.6, 2 + expand_width / 7.5, -2.35, 2.5) + self._node.set_transparency(TransparencyAttrib.M_alpha) + + self._callback = callback + self._extra_args = extra_args + self._collection = None + + if checked: + self.set_checked(True, False) + + @property + def collection(self): + """ Returns a handle to the assigned checkbox collection, or None + if no collection was assigned """ + return self._collection + + @collection.setter + def collection(self, coll): + """ Internal method to add a checkbox to a checkbox collection, this + is used for radio-buttons """ + self._collection = coll + + @property + def checked(self): + """ Returns whether the node is currently checked """ + return self._node["isChecked"] + + @property + def node(self): + """ Returns a handle to the internally used node """ + return self._node + + def _update_status(self, status): + """ Internal method when another checkbox in the same radio group + changed it's value """ + + if not status and self._collection: + self._node.commandFunc(None) + return + + if self._collection: + if status: + self._collection.on_checkbox_changed(self) + # A radio box can't be unchecked + # self._node["state"] = DGG.DISABLED + + if self._callback is not None: + self._callback(*([status] + self._extra_args)) + + def set_checked(self, val, do_callback=True): + """ Internal method to check/uncheck the checkbox """ + self._node["isChecked"] = val + + if val: + self._node["image"] = self._node["checkedImage"] + else: + self._node["image"] = self._node["uncheckedImage"] + + if do_callback and self._callback is not None: + self._callback(*([val] + self._extra_args)) diff --git a/RenderPipelineFile/rpcore/gui/checkbox_collection.py b/RenderPipelineFile/rpcore/gui/checkbox_collection.py index 4d8b390d..7cdbd59e 100644 --- a/RenderPipelineFile/rpcore/gui/checkbox_collection.py +++ b/RenderPipelineFile/rpcore/gui/checkbox_collection.py @@ -1,67 +1,67 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.rpobject import RPObject - -import direct.gui.DirectGuiGlobals as DGG - - -class CheckboxCollection(RPObject): - - """ This is a container for multiple Checkboxes, controlling that - only one checkbox of this collection is checked at one time - (like a radio-button) """ - - def __init__(self): - RPObject.__init__(self) - self._items = [] - - def add(self, chb): - """ Adds a Checkbox to this collection """ - if chb.collection is not None: - self.error( - "Can't add checkbox as it already belongs " - "to another collection!") - return - chb.collection = self - self._items.append(chb) - - def remove(self, chb): - """ Removes a checkbox from this collection """ - if chb.collection is not self: - self.error( - "Can't remove the checkbox from this collection as it is not " - "attached to this collection!") - return - chb.collection = None - self._items.remove(chb) - - def on_checkbox_changed(self, chb): - """ Internal callback when a checkbox got changed """ - for item in self._items: - if item is not chb: - item.set_checked(False) - item.node["state"] = DGG.NORMAL +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.rpobject import RPObject + +import direct.gui.DirectGuiGlobals as DGG + + +class CheckboxCollection(RPObject): + + """ This is a container for multiple Checkboxes, controlling that + only one checkbox of this collection is checked at one time + (like a radio-button) """ + + def __init__(self): + RPObject.__init__(self) + self._items = [] + + def add(self, chb): + """ Adds a Checkbox to this collection """ + if chb.collection is not None: + self.error( + "Can't add checkbox as it already belongs " + "to another collection!") + return + chb.collection = self + self._items.append(chb) + + def remove(self, chb): + """ Removes a checkbox from this collection """ + if chb.collection is not self: + self.error( + "Can't remove the checkbox from this collection as it is not " + "attached to this collection!") + return + chb.collection = None + self._items.remove(chb) + + def on_checkbox_changed(self, chb): + """ Internal callback when a checkbox got changed """ + for item in self._items: + if item is not chb: + item.set_checked(False) + item.node["state"] = DGG.NORMAL diff --git a/RenderPipelineFile/rpcore/gui/debugger.py b/RenderPipelineFile/rpcore/gui/debugger.py index 71dc095a..e88ca864 100644 --- a/RenderPipelineFile/rpcore/gui/debugger.py +++ b/RenderPipelineFile/rpcore/gui/debugger.py @@ -1,336 +1,336 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import division - -import os -import sys -import subprocess - -from six.moves import range # pylint: disable=import-error - -from panda3d.core import Vec4, Vec3, Vec2, RenderState, TransformState -from panda3d.core import TexturePool, SceneGraphAnalyzer -from direct.interval.IntervalGlobal import Sequence - -from rpcore.gui.sprite import Sprite -from rpcore.gui.buffer_viewer import BufferViewer -from rpcore.gui.pipe_viewer import PipeViewer -from rpcore.gui.render_mode_selector import RenderModeSelector - -from rpcore.gui.text_node import TextNode -from rpcore.gui.error_message_display import ErrorMessageDisplay -from rpcore.gui.exposure_widget import ExposureWidget -from rpcore.gui.fps_chart import FPSChart -from rpcore.gui.pixel_inspector import PixelInspector - -from rpcore.globals import Globals -from rpcore.rpobject import RPObject - -from rpcore.native import NATIVE_CXX_LOADED -from rpcore.render_target import RenderTarget -from rpcore.image import Image - - -class Debugger(RPObject): - - """ This class manages the onscreen control, and displays statistics. """ - - def __init__(self, pipeline): - RPObject.__init__(self) - self.debug("Creating debugger") - self.pipeline = pipeline - self.analyzer = SceneGraphAnalyzer() - - self.fullscreen_node = Globals.base.pixel2d.attach_new_node("rp_debugger") - self.create_components() - self.init_keybindings() - - if self.advanced_info: - Globals.base.doMethodLater( - 0.5, lambda task: self.collect_scene_data(), "RPDebugger_collectSceneData_initial") - - Globals.base.doMethodLater(0.1, self.update_stats, "RPDebugger_updateStats") - - - @property - def advanced_info(self): - return self.pipeline.settings["pipeline.advanced_debugging_info"] - - def create_components(self): - """ Creates the gui components """ - - self.debugger_width = 460 - self.debugger_visible = False - self.debugger_interval = None - - self.create_stats() - self.create_hints() - - self.pipeline_logo = Sprite( - image="/$$rp/data/gui/pipeline_logo_text.png", x=30, y=30, - parent=self.fullscreen_node) - - if self.advanced_info: - self.exposure_node = self.fullscreen_node.attach_new_node("ExposureWidget") - self.exposure_widget = ExposureWidget(self.pipeline, self.exposure_node) - - self.fps_node = self.fullscreen_node.attach_new_node("FPSChart") - self.fps_node.set_pos(Vec3(21, 1, -108 - 40)) - self.fps_widget = FPSChart(self.pipeline, self.fps_node) - - self.pixel_widget = PixelInspector(self.pipeline) - self.buffer_viewer = BufferViewer(self.pipeline, self.fullscreen_node) - self.pipe_viewer = PipeViewer(self.pipeline, self.fullscreen_node) - self.rm_selector = RenderModeSelector(self.pipeline, self.fullscreen_node) - self.error_msg_handler = ErrorMessageDisplay() - - self.handle_window_resize() - - def update(self): - """ Updates the gui """ - self.error_msg_handler.update() - self.pixel_widget.update() - - def collect_scene_data(self, task=None): - """ Analyzes the scene graph to provide useful information """ - self.analyzer.clear() - for geom_node in Globals.base.render.find_all_matches("**/+GeomNode"): - self.analyzer.add_node(geom_node.node()) - if task: - return task.again - - def create_stats(self): - """ Creates the stats overlay """ - self.overlay_node = Globals.base.aspect2d.attach_new_node("Overlay") - self.debug_lines = [] - - num_lines = 6 if self.advanced_info else 1 - for i in range(num_lines): - self.debug_lines.append(TextNode( - pos=Vec2(0, -i * 0.046), parent=self.overlay_node, align="right", color=Vec3(0.7, 1, 1))) - self.debug_lines[0].color = Vec4(1, 1, 0, 1) - - def create_hints(self): - """ Creates the hints like keybindings and when reloading shaders """ - self.hint_reloading = Sprite( - image="/$$rp/data/gui/shader_reload_hint.png", - parent=self.fullscreen_node) - self.set_reload_hint_visible(False) - - self.python_warning = None - if not NATIVE_CXX_LOADED: - # Warning when using the python version - self.python_warning = Sprite( - image="/$$rp/data/gui/python_warning.png", - parent=self.fullscreen_node) - Sequence( - self.python_warning.color_scale_interval( - 0.7, Vec4(0.3, 1, 1, 0.7), blendType="easeOut"), - self.python_warning.color_scale_interval( - 0.7, Vec4(1, 1, 1, 1.0), blendType="easeOut"), - ).loop() - - # Keybinding hints - self.keybinding_instructions = Sprite( - image="/$$rp/data/gui/keybindings.png", - parent=self.fullscreen_node, any_filter=False) - - def set_reload_hint_visible(self, flag): - """ Sets whether the shader reload hint is visible """ - if flag: - self.hint_reloading.show() - else: - self.hint_reloading.hide() - - def handle_window_resize(self): - """ Handles the window resize, repositions the GUI elements to fit on - screen. """ - # When using small resolutions, scale the GUI so its still useable, - # otherwise the sub-windows are bigger than the main window - self.gui_scale = max(0.65, min(1.0, Globals.native_resolution.x / 1920.0)) - self.fullscreen_node.set_scale(self.gui_scale) - - if self.advanced_info: - self.exposure_node.set_pos( - Globals.native_resolution.x // self.gui_scale - 200, - 1, -Globals.native_resolution.y // self.gui_scale + 120) - self.hint_reloading.set_pos( - float((Globals.native_resolution.x) // 2) / self.gui_scale - 465 // 2, 220) - self.keybinding_instructions.set_pos( - 30, Globals.native_resolution.y // self.gui_scale - 510.0,) - self.overlay_node.set_pos(Globals.base.get_aspect_ratio() - 0.07, 1, 1.0 - 0.07) - if self.python_warning: - self.python_warning.set_pos( - (Globals.native_resolution.x // self.gui_scale - 1054) // 2, - (Globals.native_resolution.y // self.gui_scale - 118 - 40)) - - for text in self.debug_lines: - text.set_pixel_size(16 * max(0.8, self.gui_scale)) - - self.buffer_viewer.center_on_screen() - self.pipe_viewer.center_on_screen() - self.rm_selector.center_on_screen() - - def init_keybindings(self): - """ Inits the debugger keybindings """ - Globals.base.accept("v", self.buffer_viewer.toggle) - Globals.base.accept("c", self.pipe_viewer.toggle) - Globals.base.accept("z", self.rm_selector.toggle) - Globals.base.accept("f5", self.toggle_gui_visible) - Globals.base.accept("f6", self.toggle_keybindings_visible) - Globals.base.accept("r", self.pipeline.reload_shaders) - Globals.base.accept("m", self.start_material_editor) - - def start_material_editor(self): - """ Starts the material editor """ - self.debug("Starting material editor") - pth = sys.executable - editor = os.path.dirname(os.path.realpath(__file__)) - editor = os.path.join(editor, "..", "..", "toolkit", "material_editor", "main.py") - subprocess.Popen([pth, editor], shell=True) - - def toggle_gui_visible(self): - """ Shows / Hides the gui """ - - if not self.fullscreen_node.is_hidden(): - self.fullscreen_node.hide() - self.overlay_node.hide() - self.error_msg_handler.hide() - else: - self.fullscreen_node.show() - self.overlay_node.show() - self.error_msg_handler.show() - - def hide_gui(self): - self.fullscreen_node.hide() - self.overlay_node.hide() - self.error_msg_handler.hide() - - def toggle_keybindings_visible(self): - """ Shows / Hides the FPS graph """ - if not self.keybinding_instructions.is_hidden(): - self.keybinding_instructions.hide() - else: - self.keybinding_instructions.show() - - def update_stats(self, task=None): - """ Updates the stats overlay """ - clock = Globals.clock - self.debug_lines[0].text = "{:3.0f} fps | {:3.1f} ms | {:3.1f} ms max".format( - clock.get_average_frame_rate(), - 1000.0 / max(0.001, clock.get_average_frame_rate()), - clock.get_max_frame_duration() * 1000.0) - - if not self.advanced_info: - return task.again if task else None - - text = "{:4d} states | {:4d} transforms " - text += "| {:4d} cmds | {:4d} lights | {:4d} shadows " - text += "| {:5.1f}% atlas usage" - self.debug_lines[1].text = text.format( - RenderState.get_num_states(), TransformState.get_num_states(), - self.pipeline.light_mgr.cmd_queue.num_processed_commands, - self.pipeline.light_mgr.num_lights, - self.pipeline.light_mgr.num_shadow_sources, - self.pipeline.light_mgr.shadow_atlas_coverage) - - text = "Internal: {:3.0f} MB VRAM | {:5d} img | {:5d} tex | " - text += "{:5d} fbos | {:3d} plugins | {:2d} views ({:2d} active)" - tex_memory, tex_count = self.buffer_viewer.stage_information - - views, active_views = 0, 0 - for target in RenderTarget.REGISTERED_TARGETS: - if not target.create_default_region: - num_regions = target.internal_buffer.get_num_display_regions() - for i, region in enumerate(target.internal_buffer.get_display_regions()): - # Skip overlay display region - if i == 0 and num_regions > 1: - continue - views += 1 - active_views += 1 if target.active and region.active else 0 - - self.debug_lines[2].text = text.format( - tex_memory / (1024**2), len(Image.REGISTERED_IMAGES), tex_count, - RenderTarget.NUM_ALLOCATED_BUFFERS, - len(self.pipeline.plugin_mgr.enabled_plugins), - views, active_views) - - text = "Scene: {:4.0f} MB VRAM | {:3d} tex | {:4d} geoms " - text += "| {:4d} nodes | {:7,.0f} vertices" - scene_tex_size = 0 - for tex in TexturePool.find_all_textures(): - scene_tex_size += tex.estimate_texture_memory() - - self.debug_lines[3].text = text.format( - scene_tex_size / (1024**2), - len(TexturePool.find_all_textures()), - self.analyzer.get_num_geoms(), - self.analyzer.get_num_nodes(), - self.analyzer.get_num_vertices(), - ) - - sun_vector = Vec3(0) - if self.pipeline.plugin_mgr.is_plugin_enabled("scattering"): - sun_vector = self.pipeline.plugin_mgr.instances["scattering"].sun_vector - - text = "Time: {} ({:1.3f}) | Sun {:0.2f} {:0.2f} {:0.2f}" - text += " | X {:3.1f} Y {:3.1f} Z {:3.1f}" - text += " | {:2d} tasks | scheduled: {:2d}" - self.debug_lines[4].text = text.format( - self.pipeline.daytime_mgr.formatted_time, - self.pipeline.daytime_mgr.time, - sun_vector.x, sun_vector.y, sun_vector.z, - Globals.base.camera.get_x(Globals.base.render), - Globals.base.camera.get_y(Globals.base.render), - Globals.base.camera.get_z(Globals.base.render), - self.pipeline.task_scheduler.num_tasks, - self.pipeline.task_scheduler.num_scheduled_tasks,) - - text = "Scene shadows: " - if "pssm" in self.pipeline.plugin_mgr.enabled_plugins: - focus = self.pipeline.plugin_mgr.instances["pssm"].scene_shadow_stage.last_focus - if focus is not None: - text += "{:3.1f} {:3.1f} {:3.1f} r {:3.1f}".format( - focus[0].x, focus[0].y, focus[0].z, focus[1]) - else: - text += "none" - else: - text += "inactive" - - text += " | HPR ({:3.1f}, {:3.1f}, {:3.1f}) | {:4d} x {:4d} pixels @ {:3.1f} %" - text += " | {:3d} x {:3d} tiles" - self.debug_lines[5].text = text.format( - Globals.base.camera.get_h(Globals.base.render), - Globals.base.camera.get_p(Globals.base.render), - Globals.base.camera.get_r(Globals.base.render), - Globals.native_resolution.x, - Globals.native_resolution.y, - self.pipeline.settings["pipeline.resolution_scale"] * 100.0, - self.pipeline.light_mgr.num_tiles.x, - self.pipeline.light_mgr.num_tiles.y,) - if task: - return task.again +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import division + +import os +import sys +import subprocess + +from six.moves import range # pylint: disable=import-error + +from panda3d.core import Vec4, Vec3, Vec2, RenderState, TransformState +from panda3d.core import TexturePool, SceneGraphAnalyzer +from direct.interval.IntervalGlobal import Sequence + +from rpcore.gui.sprite import Sprite +from rpcore.gui.buffer_viewer import BufferViewer +from rpcore.gui.pipe_viewer import PipeViewer +from rpcore.gui.render_mode_selector import RenderModeSelector + +from rpcore.gui.text_node import TextNode +from rpcore.gui.error_message_display import ErrorMessageDisplay +from rpcore.gui.exposure_widget import ExposureWidget +from rpcore.gui.fps_chart import FPSChart +from rpcore.gui.pixel_inspector import PixelInspector + +from rpcore.globals import Globals +from rpcore.rpobject import RPObject + +from rpcore.native import NATIVE_CXX_LOADED +from rpcore.render_target import RenderTarget +from rpcore.image import Image + + +class Debugger(RPObject): + + """ This class manages the onscreen control, and displays statistics. """ + + def __init__(self, pipeline): + RPObject.__init__(self) + self.debug("Creating debugger") + self.pipeline = pipeline + self.analyzer = SceneGraphAnalyzer() + + self.fullscreen_node = Globals.base.pixel2d.attach_new_node("rp_debugger") + self.create_components() + self.init_keybindings() + + if self.advanced_info: + Globals.base.doMethodLater( + 0.5, lambda task: self.collect_scene_data(), "RPDebugger_collectSceneData_initial") + + Globals.base.doMethodLater(0.1, self.update_stats, "RPDebugger_updateStats") + + + @property + def advanced_info(self): + return self.pipeline.settings["pipeline.advanced_debugging_info"] + + def create_components(self): + """ Creates the gui components """ + + self.debugger_width = 460 + self.debugger_visible = False + self.debugger_interval = None + + self.create_stats() + self.create_hints() + + self.pipeline_logo = Sprite( + image="/$$rp/data/gui/pipeline_logo_text.png", x=30, y=30, + parent=self.fullscreen_node) + + if self.advanced_info: + self.exposure_node = self.fullscreen_node.attach_new_node("ExposureWidget") + self.exposure_widget = ExposureWidget(self.pipeline, self.exposure_node) + + self.fps_node = self.fullscreen_node.attach_new_node("FPSChart") + self.fps_node.set_pos(Vec3(21, 1, -108 - 40)) + self.fps_widget = FPSChart(self.pipeline, self.fps_node) + + self.pixel_widget = PixelInspector(self.pipeline) + self.buffer_viewer = BufferViewer(self.pipeline, self.fullscreen_node) + self.pipe_viewer = PipeViewer(self.pipeline, self.fullscreen_node) + self.rm_selector = RenderModeSelector(self.pipeline, self.fullscreen_node) + self.error_msg_handler = ErrorMessageDisplay() + + self.handle_window_resize() + + def update(self): + """ Updates the gui """ + self.error_msg_handler.update() + self.pixel_widget.update() + + def collect_scene_data(self, task=None): + """ Analyzes the scene graph to provide useful information """ + self.analyzer.clear() + for geom_node in Globals.base.render.find_all_matches("**/+GeomNode"): + self.analyzer.add_node(geom_node.node()) + if task: + return task.again + + def create_stats(self): + """ Creates the stats overlay """ + self.overlay_node = Globals.base.aspect2d.attach_new_node("Overlay") + self.debug_lines = [] + + num_lines = 6 if self.advanced_info else 1 + for i in range(num_lines): + self.debug_lines.append(TextNode( + pos=Vec2(0, -i * 0.046), parent=self.overlay_node, align="right", color=Vec3(0.7, 1, 1))) + self.debug_lines[0].color = Vec4(1, 1, 0, 1) + + def create_hints(self): + """ Creates the hints like keybindings and when reloading shaders """ + self.hint_reloading = Sprite( + image="/$$rp/data/gui/shader_reload_hint.png", + parent=self.fullscreen_node) + self.set_reload_hint_visible(False) + + self.python_warning = None + if not NATIVE_CXX_LOADED: + # Warning when using the python version + self.python_warning = Sprite( + image="/$$rp/data/gui/python_warning.png", + parent=self.fullscreen_node) + Sequence( + self.python_warning.color_scale_interval( + 0.7, Vec4(0.3, 1, 1, 0.7), blendType="easeOut"), + self.python_warning.color_scale_interval( + 0.7, Vec4(1, 1, 1, 1.0), blendType="easeOut"), + ).loop() + + # Keybinding hints + self.keybinding_instructions = Sprite( + image="/$$rp/data/gui/keybindings.png", + parent=self.fullscreen_node, any_filter=False) + + def set_reload_hint_visible(self, flag): + """ Sets whether the shader reload hint is visible """ + if flag: + self.hint_reloading.show() + else: + self.hint_reloading.hide() + + def handle_window_resize(self): + """ Handles the window resize, repositions the GUI elements to fit on + screen. """ + # When using small resolutions, scale the GUI so its still useable, + # otherwise the sub-windows are bigger than the main window + self.gui_scale = max(0.65, min(1.0, Globals.native_resolution.x / 1920.0)) + self.fullscreen_node.set_scale(self.gui_scale) + + if self.advanced_info: + self.exposure_node.set_pos( + Globals.native_resolution.x // self.gui_scale - 200, + 1, -Globals.native_resolution.y // self.gui_scale + 120) + self.hint_reloading.set_pos( + float((Globals.native_resolution.x) // 2) / self.gui_scale - 465 // 2, 220) + self.keybinding_instructions.set_pos( + 30, Globals.native_resolution.y // self.gui_scale - 510.0,) + self.overlay_node.set_pos(Globals.base.get_aspect_ratio() - 0.07, 1, 1.0 - 0.07) + if self.python_warning: + self.python_warning.set_pos( + (Globals.native_resolution.x // self.gui_scale - 1054) // 2, + (Globals.native_resolution.y // self.gui_scale - 118 - 40)) + + for text in self.debug_lines: + text.set_pixel_size(16 * max(0.8, self.gui_scale)) + + self.buffer_viewer.center_on_screen() + self.pipe_viewer.center_on_screen() + self.rm_selector.center_on_screen() + + def init_keybindings(self): + """ Inits the debugger keybindings """ + Globals.base.accept("v", self.buffer_viewer.toggle) + Globals.base.accept("c", self.pipe_viewer.toggle) + Globals.base.accept("z", self.rm_selector.toggle) + Globals.base.accept("f5", self.toggle_gui_visible) + Globals.base.accept("f6", self.toggle_keybindings_visible) + Globals.base.accept("r", self.pipeline.reload_shaders) + Globals.base.accept("m", self.start_material_editor) + + def start_material_editor(self): + """ Starts the material editor """ + self.debug("Starting material editor") + pth = sys.executable + editor = os.path.dirname(os.path.realpath(__file__)) + editor = os.path.join(editor, "..", "..", "toolkit", "material_editor", "main.py") + subprocess.Popen([pth, editor], shell=True) + + def toggle_gui_visible(self): + """ Shows / Hides the gui """ + + if not self.fullscreen_node.is_hidden(): + self.fullscreen_node.hide() + self.overlay_node.hide() + self.error_msg_handler.hide() + else: + self.fullscreen_node.show() + self.overlay_node.show() + self.error_msg_handler.show() + + def hide_gui(self): + self.fullscreen_node.hide() + self.overlay_node.hide() + self.error_msg_handler.hide() + + def toggle_keybindings_visible(self): + """ Shows / Hides the FPS graph """ + if not self.keybinding_instructions.is_hidden(): + self.keybinding_instructions.hide() + else: + self.keybinding_instructions.show() + + def update_stats(self, task=None): + """ Updates the stats overlay """ + clock = Globals.clock + self.debug_lines[0].text = "{:3.0f} fps | {:3.1f} ms | {:3.1f} ms max".format( + clock.get_average_frame_rate(), + 1000.0 / max(0.001, clock.get_average_frame_rate()), + clock.get_max_frame_duration() * 1000.0) + + if not self.advanced_info: + return task.again if task else None + + text = "{:4d} states | {:4d} transforms " + text += "| {:4d} cmds | {:4d} lights | {:4d} shadows " + text += "| {:5.1f}% atlas usage" + self.debug_lines[1].text = text.format( + RenderState.get_num_states(), TransformState.get_num_states(), + self.pipeline.light_mgr.cmd_queue.num_processed_commands, + self.pipeline.light_mgr.num_lights, + self.pipeline.light_mgr.num_shadow_sources, + self.pipeline.light_mgr.shadow_atlas_coverage) + + text = "Internal: {:3.0f} MB VRAM | {:5d} img | {:5d} tex | " + text += "{:5d} fbos | {:3d} plugins | {:2d} views ({:2d} active)" + tex_memory, tex_count = self.buffer_viewer.stage_information + + views, active_views = 0, 0 + for target in RenderTarget.REGISTERED_TARGETS: + if not target.create_default_region: + num_regions = target.internal_buffer.get_num_display_regions() + for i, region in enumerate(target.internal_buffer.get_display_regions()): + # Skip overlay display region + if i == 0 and num_regions > 1: + continue + views += 1 + active_views += 1 if target.active and region.active else 0 + + self.debug_lines[2].text = text.format( + tex_memory / (1024**2), len(Image.REGISTERED_IMAGES), tex_count, + RenderTarget.NUM_ALLOCATED_BUFFERS, + len(self.pipeline.plugin_mgr.enabled_plugins), + views, active_views) + + text = "Scene: {:4.0f} MB VRAM | {:3d} tex | {:4d} geoms " + text += "| {:4d} nodes | {:7,.0f} vertices" + scene_tex_size = 0 + for tex in TexturePool.find_all_textures(): + scene_tex_size += tex.estimate_texture_memory() + + self.debug_lines[3].text = text.format( + scene_tex_size / (1024**2), + len(TexturePool.find_all_textures()), + self.analyzer.get_num_geoms(), + self.analyzer.get_num_nodes(), + self.analyzer.get_num_vertices(), + ) + + sun_vector = Vec3(0) + if self.pipeline.plugin_mgr.is_plugin_enabled("scattering"): + sun_vector = self.pipeline.plugin_mgr.instances["scattering"].sun_vector + + text = "Time: {} ({:1.3f}) | Sun {:0.2f} {:0.2f} {:0.2f}" + text += " | X {:3.1f} Y {:3.1f} Z {:3.1f}" + text += " | {:2d} tasks | scheduled: {:2d}" + self.debug_lines[4].text = text.format( + self.pipeline.daytime_mgr.formatted_time, + self.pipeline.daytime_mgr.time, + sun_vector.x, sun_vector.y, sun_vector.z, + Globals.base.camera.get_x(Globals.base.render), + Globals.base.camera.get_y(Globals.base.render), + Globals.base.camera.get_z(Globals.base.render), + self.pipeline.task_scheduler.num_tasks, + self.pipeline.task_scheduler.num_scheduled_tasks,) + + text = "Scene shadows: " + if "pssm" in self.pipeline.plugin_mgr.enabled_plugins: + focus = self.pipeline.plugin_mgr.instances["pssm"].scene_shadow_stage.last_focus + if focus is not None: + text += "{:3.1f} {:3.1f} {:3.1f} r {:3.1f}".format( + focus[0].x, focus[0].y, focus[0].z, focus[1]) + else: + text += "none" + else: + text += "inactive" + + text += " | HPR ({:3.1f}, {:3.1f}, {:3.1f}) | {:4d} x {:4d} pixels @ {:3.1f} %" + text += " | {:3d} x {:3d} tiles" + self.debug_lines[5].text = text.format( + Globals.base.camera.get_h(Globals.base.render), + Globals.base.camera.get_p(Globals.base.render), + Globals.base.camera.get_r(Globals.base.render), + Globals.native_resolution.x, + Globals.native_resolution.y, + self.pipeline.settings["pipeline.resolution_scale"] * 100.0, + self.pipeline.light_mgr.num_tiles.x, + self.pipeline.light_mgr.num_tiles.y,) + if task: + return task.again diff --git a/RenderPipelineFile/rpcore/gui/draggable_window.py b/RenderPipelineFile/rpcore/gui/draggable_window.py index 01e83d18..0efbbdbc 100644 --- a/RenderPipelineFile/rpcore/gui/draggable_window.py +++ b/RenderPipelineFile/rpcore/gui/draggable_window.py @@ -1,166 +1,166 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import Vec2, TransparencyAttrib, Vec3 -from direct.gui.DirectFrame import DirectFrame -from direct.gui.DirectButton import DirectButton -from direct.gui.DirectGui import DGG - -from rpcore.rpobject import RPObject -from rpcore.globals import Globals -from rpcore.gui.text import Text - - -class DraggableWindow(RPObject): - - """ This is a simple draggable but not resizeable window """ - - def __init__(self, width=800, height=500, title="Window", parent=None): - """ Constructs a new window with the given dimensions and title """ - RPObject.__init__(self, "Window-" + title) - self._width = width - self._height = height - self._title = title - self._visible = True - self._parent = parent if parent else Globals.base.pixel2d - self._dragging = False - self._drag_offset = Vec2(0) - self._pos = Vec2(0) - - def center_on_screen(self): - """ Centers the window on screen """ - self._context_scale = 1.0 / self._parent.get_sx() - self._context_width = Globals.native_resolution.x * self._context_scale - self._context_height = Globals.native_resolution.y * self._context_scale - self._set_pos(Vec2((self._context_width - self._width) / 2, - (self._context_height - self._height) / 2)) - - def set_title(self, title): - """ Sets the window title """ - self._title = title - self._window_title.set_text(title) - - def show(self): - """ Shows the window """ - self._visible = True - self.center_on_screen() - self._node.show() - - def hide(self): - """ Hides the window """ - self._visible = False - self._stop_drag() - self._node.hide() - - def remove(self): - """ Removes the window from the scene graph. You should still delete the - instance """ - self._stop_drag() - self._node.remove_node() - - def _create_components(self): - """ Creates the window components """ - self._node = self._parent.attach_new_node("Window") - self._node.set_pos(self._pos.x, 1, -self._pos.y) - border_px = 1 - border_frame_size = (-border_px, self._width + border_px, - border_px, -self._height - border_px) - self._border_frame = DirectFrame( - pos=(0, 1, 0), frameSize=border_frame_size, - frameColor=(24 / 255.0, 131 / 255.0, 215 / 255.0, 1), - parent=self._node, state=DGG.NORMAL) - self._background = DirectFrame( - pos=(0, 1, 0), frameSize=(0, self._width, 0, -self._height), - frameColor=(0.1, 0.1, 0.1, 1.0), parent=self._node) - self._title_bar = DirectFrame( - pos=(0, 1, 0), frameSize=(0, self._width, 0, -25), - # frameColor=(0.058, 0.058, 0.058, 1), - frameColor=(1, 1, 1, 1), - parent=self._node, - state=DGG.NORMAL) - self._window_title = Text( - parent=self._node, x=8, y=17, text=self._title, size=13, - color=Vec3(0.15), may_change=True) - self._btn_close = DirectButton( - relief=DGG.FLAT, pressEffect=1, pos=(self._width - 22, 1, -12), - frameColor=(1.0, 0.2, 0.2, 0.5), parent=self._node, - scale=(45 / 2, 1, 24 / 2), - image="/$$rp/data/gui/close_window.png") - - # Init bindings - self._btn_close.set_transparency(TransparencyAttrib.M_alpha) - self._btn_close.bind(DGG.B1CLICK, self._request_close) - self._btn_close.bind(DGG.WITHIN, self._on_close_btn_hover) - self._btn_close.bind(DGG.WITHOUT, self._on_close_btn_out) - self._title_bar.bind(DGG.B1PRESS, self._start_drag) - self._title_bar.bind(DGG.B1RELEASE, self._stop_drag) - - def _start_drag(self, evt=None): # pylint: disable=unused-argument - """ Gets called when the user starts dragging the window """ - self._dragging = True - self._node.detach_node() - self._node.reparent_to(self._parent) - Globals.base.taskMgr.add(self._on_tick, "UIWindowDrag", - uponDeath=self._stop_drag) - self._drag_offset = self._pos - self._get_mouse_pos() - - def _on_close_btn_hover(self, evt=None): # pylint: disable=unused-argument - """ Internal method when the close button got hovered """ - self._btn_close["image"] = "/$$rp/data/gui/close_window_hover.png" - - def _on_close_btn_out(self, evt=None): # pylint: disable=unused-argument - """ Internal method when the close button is no longer hovered """ - self._btn_close["image"] = "/$$rp/data/gui/close_window.png" - - def _request_close(self, evt=None): # pylint: disable=unused-argument - """ This method gets called when the close button gets clicked """ - self.hide() - - def _stop_drag(self, evt=None): # pylint: disable=unused-argument - """ Gets called when the user stops dragging the window """ - Globals.base.taskMgr.remove("UIWindowDrag") - self._dragging = False - - def _get_mouse_pos(self): - """ Internal helper function to get the mouse position, scaled by - the context scale """ - mouse_x, mouse_y = (Globals.base.win.get_pointer(0).x, - Globals.base.win.get_pointer(0).y) - return Vec2(mouse_x, mouse_y) * self._context_scale - - def _set_pos(self, pos): - """ Moves the window to the specified position """ - self._pos = pos - self._pos.x = max(self._pos.x, -self._width + 100) - self._pos.y = max(self._pos.y, 25) - self._pos.x = min(self._pos.x, self._context_width - 100) - self._pos.y = min(self._pos.y, self._context_height - 50) - self._node.set_pos(self._pos.x, 1, -self._pos.y) - - def _on_tick(self, task): - """ Task which updates the window while being dragged """ - self._set_pos(self._get_mouse_pos() + self._drag_offset) - return task.cont +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import Vec2, TransparencyAttrib, Vec3 +from direct.gui.DirectFrame import DirectFrame +from direct.gui.DirectButton import DirectButton +from direct.gui.DirectGui import DGG + +from rpcore.rpobject import RPObject +from rpcore.globals import Globals +from rpcore.gui.text import Text + + +class DraggableWindow(RPObject): + + """ This is a simple draggable but not resizeable window """ + + def __init__(self, width=800, height=500, title="Window", parent=None): + """ Constructs a new window with the given dimensions and title """ + RPObject.__init__(self, "Window-" + title) + self._width = width + self._height = height + self._title = title + self._visible = True + self._parent = parent if parent else Globals.base.pixel2d + self._dragging = False + self._drag_offset = Vec2(0) + self._pos = Vec2(0) + + def center_on_screen(self): + """ Centers the window on screen """ + self._context_scale = 1.0 / self._parent.get_sx() + self._context_width = Globals.native_resolution.x * self._context_scale + self._context_height = Globals.native_resolution.y * self._context_scale + self._set_pos(Vec2((self._context_width - self._width) / 2, + (self._context_height - self._height) / 2)) + + def set_title(self, title): + """ Sets the window title """ + self._title = title + self._window_title.set_text(title) + + def show(self): + """ Shows the window """ + self._visible = True + self.center_on_screen() + self._node.show() + + def hide(self): + """ Hides the window """ + self._visible = False + self._stop_drag() + self._node.hide() + + def remove(self): + """ Removes the window from the scene graph. You should still delete the + instance """ + self._stop_drag() + self._node.remove_node() + + def _create_components(self): + """ Creates the window components """ + self._node = self._parent.attach_new_node("Window") + self._node.set_pos(self._pos.x, 1, -self._pos.y) + border_px = 1 + border_frame_size = (-border_px, self._width + border_px, + border_px, -self._height - border_px) + self._border_frame = DirectFrame( + pos=(0, 1, 0), frameSize=border_frame_size, + frameColor=(24 / 255.0, 131 / 255.0, 215 / 255.0, 1), + parent=self._node, state=DGG.NORMAL) + self._background = DirectFrame( + pos=(0, 1, 0), frameSize=(0, self._width, 0, -self._height), + frameColor=(0.1, 0.1, 0.1, 1.0), parent=self._node) + self._title_bar = DirectFrame( + pos=(0, 1, 0), frameSize=(0, self._width, 0, -25), + # frameColor=(0.058, 0.058, 0.058, 1), + frameColor=(1, 1, 1, 1), + parent=self._node, + state=DGG.NORMAL) + self._window_title = Text( + parent=self._node, x=8, y=17, text=self._title, size=13, + color=Vec3(0.15), may_change=True) + self._btn_close = DirectButton( + relief=DGG.FLAT, pressEffect=1, pos=(self._width - 22, 1, -12), + frameColor=(1.0, 0.2, 0.2, 0.5), parent=self._node, + scale=(45 / 2, 1, 24 / 2), + image="/$$rp/data/gui/close_window.png") + + # Init bindings + self._btn_close.set_transparency(TransparencyAttrib.M_alpha) + self._btn_close.bind(DGG.B1CLICK, self._request_close) + self._btn_close.bind(DGG.WITHIN, self._on_close_btn_hover) + self._btn_close.bind(DGG.WITHOUT, self._on_close_btn_out) + self._title_bar.bind(DGG.B1PRESS, self._start_drag) + self._title_bar.bind(DGG.B1RELEASE, self._stop_drag) + + def _start_drag(self, evt=None): # pylint: disable=unused-argument + """ Gets called when the user starts dragging the window """ + self._dragging = True + self._node.detach_node() + self._node.reparent_to(self._parent) + Globals.base.taskMgr.add(self._on_tick, "UIWindowDrag", + uponDeath=self._stop_drag) + self._drag_offset = self._pos - self._get_mouse_pos() + + def _on_close_btn_hover(self, evt=None): # pylint: disable=unused-argument + """ Internal method when the close button got hovered """ + self._btn_close["image"] = "/$$rp/data/gui/close_window_hover.png" + + def _on_close_btn_out(self, evt=None): # pylint: disable=unused-argument + """ Internal method when the close button is no longer hovered """ + self._btn_close["image"] = "/$$rp/data/gui/close_window.png" + + def _request_close(self, evt=None): # pylint: disable=unused-argument + """ This method gets called when the close button gets clicked """ + self.hide() + + def _stop_drag(self, evt=None): # pylint: disable=unused-argument + """ Gets called when the user stops dragging the window """ + Globals.base.taskMgr.remove("UIWindowDrag") + self._dragging = False + + def _get_mouse_pos(self): + """ Internal helper function to get the mouse position, scaled by + the context scale """ + mouse_x, mouse_y = (Globals.base.win.get_pointer(0).x, + Globals.base.win.get_pointer(0).y) + return Vec2(mouse_x, mouse_y) * self._context_scale + + def _set_pos(self, pos): + """ Moves the window to the specified position """ + self._pos = pos + self._pos.x = max(self._pos.x, -self._width + 100) + self._pos.y = max(self._pos.y, 25) + self._pos.x = min(self._pos.x, self._context_width - 100) + self._pos.y = min(self._pos.y, self._context_height - 50) + self._node.set_pos(self._pos.x, 1, -self._pos.y) + + def _on_tick(self, task): + """ Task which updates the window while being dragged """ + self._set_pos(self._get_mouse_pos() + self._drag_offset) + return task.cont diff --git a/RenderPipelineFile/rpcore/gui/error_message_display.py b/RenderPipelineFile/rpcore/gui/error_message_display.py index b4ffac34..ed686d41 100644 --- a/RenderPipelineFile/rpcore/gui/error_message_display.py +++ b/RenderPipelineFile/rpcore/gui/error_message_display.py @@ -1,101 +1,101 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import print_function - -from panda3d.core import Notify, LineStream, Vec3 - -from rpcore.globals import Globals -from rpcore.rpobject import RPObject - -from rpcore.gui.text import Text - - -class ErrorMessageDisplay(RPObject): - - """ This is a gui element which listens to the panda output stream - and shows errors """ - - def __init__(self): - RPObject.__init__(self) - self._num_errors = 0 - self._error_node = Globals.base.pixel2d.attach_new_node("ErrorDisplay") - self._error_node.set_z(-180) - self._notify_stream = None - - def _init_notify(self): - """ Internal method to init the stream to catch all notify messages """ - self._notify_stream = LineStream() - Notify.ptr().set_ostream_ptr(self._notify_stream, False) - - def update(self): - """ Updates the error display, fetching all new messages from the notify - stream """ - - if not self._notify_stream: - self._init_notify() - - while self._notify_stream.is_text_available(): - line = self._notify_stream.get_line().strip() - if "warning" in line: - RPObject.global_warn("Panda3D", line) - # self.add_warning(line) - elif "error" in line: - RPObject.global_error("Panda3D", line) - self.add_error(line) - else: - RPObject.global_debug("Panda3D", line) - - def add_error(self, msg): - """ Adds a new error message """ - self.add_text(msg, Vec3(1, 0.2, 0.2)) - - def add_warning(self, msg): - """ Adds a new warning message """ - self.add_text(msg, Vec3(1, 1, 0.2)) - - def add_text(self, text, color): - """ Internal method to add a new text to the output """ - Text( - x=Globals.native_resolution.x - 30, y=self._num_errors * 23, - align="right", text=text, size=12, parent=self._error_node, color=color) - - self._num_errors += 1 - - if self._num_errors > 30: - self.clear_messages() - self.add_error("Error count exceeded. Cleared errors ..") - - def clear_messages(self): - """ Clears all messages / removes them """ - self._error_node.node().remove_all_children() - self._num_errors = 0 - - def show(self): - self._error_node.show() - - def hide(self): - self._error_node.hide() +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import print_function + +from panda3d.core import Notify, LineStream, Vec3 + +from rpcore.globals import Globals +from rpcore.rpobject import RPObject + +from rpcore.gui.text import Text + + +class ErrorMessageDisplay(RPObject): + + """ This is a gui element which listens to the panda output stream + and shows errors """ + + def __init__(self): + RPObject.__init__(self) + self._num_errors = 0 + self._error_node = Globals.base.pixel2d.attach_new_node("ErrorDisplay") + self._error_node.set_z(-180) + self._notify_stream = None + + def _init_notify(self): + """ Internal method to init the stream to catch all notify messages """ + self._notify_stream = LineStream() + Notify.ptr().set_ostream_ptr(self._notify_stream, False) + + def update(self): + """ Updates the error display, fetching all new messages from the notify + stream """ + + if not self._notify_stream: + self._init_notify() + + while self._notify_stream.is_text_available(): + line = self._notify_stream.get_line().strip() + if "warning" in line: + RPObject.global_warn("Panda3D", line) + # self.add_warning(line) + elif "error" in line: + RPObject.global_error("Panda3D", line) + self.add_error(line) + else: + RPObject.global_debug("Panda3D", line) + + def add_error(self, msg): + """ Adds a new error message """ + self.add_text(msg, Vec3(1, 0.2, 0.2)) + + def add_warning(self, msg): + """ Adds a new warning message """ + self.add_text(msg, Vec3(1, 1, 0.2)) + + def add_text(self, text, color): + """ Internal method to add a new text to the output """ + Text( + x=Globals.native_resolution.x - 30, y=self._num_errors * 23, + align="right", text=text, size=12, parent=self._error_node, color=color) + + self._num_errors += 1 + + if self._num_errors > 30: + self.clear_messages() + self.add_error("Error count exceeded. Cleared errors ..") + + def clear_messages(self): + """ Clears all messages / removes them """ + self._error_node.node().remove_all_children() + self._num_errors = 0 + + def show(self): + self._error_node.show() + + def hide(self): + self._error_node.hide() diff --git a/RenderPipelineFile/rpcore/gui/exposure_widget.py b/RenderPipelineFile/rpcore/gui/exposure_widget.py index 19454924..3914aa27 100644 --- a/RenderPipelineFile/rpcore/gui/exposure_widget.py +++ b/RenderPipelineFile/rpcore/gui/exposure_widget.py @@ -1,101 +1,101 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" -from __future__ import division - -from panda3d.core import ComputeNode, Vec4, Vec3 -from direct.gui.DirectFrame import DirectFrame - -from rpcore.gui.sprite import Sprite -from rpcore.gui.text import Text - -from rpcore.rpobject import RPObject -from rpcore.image import Image -from rpcore.globals import Globals -from rpcore.loader import RPLoader - - -class ExposureWidget(RPObject): - - """ Widget to show the current exposure """ - - def __init__(self, pipeline, parent): - """ Inits the widget """ - RPObject.__init__(self) - self._pipeline = pipeline - self._parent = parent - self._node = self._parent.attach_new_node("ExposureWidgetNode") - self._create_components() - - def _create_components(self): - """ Internal method to init the widgets components """ - - self._node.hide() - - # Create the texture where the gui component is rendered inside - self._storage_tex = Image.create_2d("ExposureDisplay", 140, 20, "RGBA8") - self._storage_tex.set_clear_color(Vec4(0.2, 0.6, 1.0, 1.0)) - self._storage_tex.clear_image() - - self._bg_frame = DirectFrame( - parent=self._node, frameColor=(0.1, 0.1, 0.1, 1.0), - frameSize=(200, 0, -10, -85), pos=(0, 0, 0)) - - self._display_img = Sprite( - image=self._storage_tex, parent=self._node, w=140, h=20, x=20, y=50) - - self._display_txt = Text( - text="Current Exposure".upper(), parent=self._node, x=160, y=40, - size=13, color=Vec3(0.8), align="right") - - # Create the shader which generates the visualization texture - self._cshader_node = ComputeNode("ExposureWidget") - self._cshader_node.add_dispatch(140 // 10, 20 // 4, 1) - - self._cshader_np = self._node.attach_new_node(self._cshader_node) - - # Defer the further loading - Globals.base.taskMgr.doMethodLater(1.0, self._late_init, "ExposureLateInit") - - def _late_init(self, task): - """ Gets called after the pipeline initialized, this extracts the - exposure texture from the stage manager """ - stage_mgr = self._pipeline.stage_mgr - - if "Exposure" not in stage_mgr.pipes: - self.debug("Disabling exposure widget, could not find the exposure data.") - self._node.remove_node() - return - - self._node.show() - - exposure_tex = stage_mgr.pipes["Exposure"] - self._cshader = RPLoader.load_shader("/$$rp/shader/visualize_exposure.compute.glsl") - self._cshader_np.set_shader(self._cshader) - self._cshader_np.set_shader_inputs( - DestTex=self._storage_tex, - ExposureTex=exposure_tex) - - return task.done +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +from __future__ import division + +from panda3d.core import ComputeNode, Vec4, Vec3 +from direct.gui.DirectFrame import DirectFrame + +from rpcore.gui.sprite import Sprite +from rpcore.gui.text import Text + +from rpcore.rpobject import RPObject +from rpcore.image import Image +from rpcore.globals import Globals +from rpcore.loader import RPLoader + + +class ExposureWidget(RPObject): + + """ Widget to show the current exposure """ + + def __init__(self, pipeline, parent): + """ Inits the widget """ + RPObject.__init__(self) + self._pipeline = pipeline + self._parent = parent + self._node = self._parent.attach_new_node("ExposureWidgetNode") + self._create_components() + + def _create_components(self): + """ Internal method to init the widgets components """ + + self._node.hide() + + # Create the texture where the gui component is rendered inside + self._storage_tex = Image.create_2d("ExposureDisplay", 140, 20, "RGBA8") + self._storage_tex.set_clear_color(Vec4(0.2, 0.6, 1.0, 1.0)) + self._storage_tex.clear_image() + + self._bg_frame = DirectFrame( + parent=self._node, frameColor=(0.1, 0.1, 0.1, 1.0), + frameSize=(200, 0, -10, -85), pos=(0, 0, 0)) + + self._display_img = Sprite( + image=self._storage_tex, parent=self._node, w=140, h=20, x=20, y=50) + + self._display_txt = Text( + text="Current Exposure".upper(), parent=self._node, x=160, y=40, + size=13, color=Vec3(0.8), align="right") + + # Create the shader which generates the visualization texture + self._cshader_node = ComputeNode("ExposureWidget") + self._cshader_node.add_dispatch(140 // 10, 20 // 4, 1) + + self._cshader_np = self._node.attach_new_node(self._cshader_node) + + # Defer the further loading + Globals.base.taskMgr.doMethodLater(1.0, self._late_init, "ExposureLateInit") + + def _late_init(self, task): + """ Gets called after the pipeline initialized, this extracts the + exposure texture from the stage manager """ + stage_mgr = self._pipeline.stage_mgr + + if "Exposure" not in stage_mgr.pipes: + self.debug("Disabling exposure widget, could not find the exposure data.") + self._node.remove_node() + return + + self._node.show() + + exposure_tex = stage_mgr.pipes["Exposure"] + self._cshader = RPLoader.load_shader("/$$rp/shader/visualize_exposure.compute.glsl") + self._cshader_np.set_shader(self._cshader) + self._cshader_np.set_shader_inputs( + DestTex=self._storage_tex, + ExposureTex=exposure_tex) + + return task.done diff --git a/RenderPipelineFile/rpcore/gui/fps_chart.py b/RenderPipelineFile/rpcore/gui/fps_chart.py index 8670462e..ad0c7c5f 100644 --- a/RenderPipelineFile/rpcore/gui/fps_chart.py +++ b/RenderPipelineFile/rpcore/gui/fps_chart.py @@ -1,133 +1,133 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" -from __future__ import division - -from panda3d.core import ComputeNode, Vec4, Vec3, PTAInt, PTAFloat - -from rpcore.gui.sprite import Sprite -from rpcore.gui.text import Text - -from rpcore.rpobject import RPObject -from rpcore.image import Image -from rpcore.globals import Globals -from rpcore.loader import RPLoader - - -class FPSChart(RPObject): - - """ Widget to show the FPS as a chart """ - - def __init__(self, pipeline, parent): - """ Inits the widget """ - RPObject.__init__(self) - self._pipeline = pipeline - self._parent = parent - self._node = self._parent.attach_new_node("FPSChartNode") - self._create_components() - - def _create_components(self): - """ Internal method to init the widgets components """ - - # Create the buffer which stores the last FPS values - self._storage_buffer = Image.create_buffer("FPSValues", 250, "R16") - self._storage_buffer.set_clear_color(Vec4(0)) - self._storage_buffer.clear_image() - - self._store_index = PTAInt.empty_array(1) - self._store_index[0] = 0 - - self._current_ftime = PTAFloat.empty_array(1) - self._current_ftime[0] = 16.0 - - self._chart_ms_max = PTAFloat.empty_array(1) - self._chart_ms_max[0] = 40 - - # Create the texture where the gui component is rendered inside - self._display_tex = Image.create_2d("FPSChartRender", 250, 120, "RGBA8") - self._display_tex.set_clear_color(Vec4(0)) - self._display_tex.clear_image() - self._display_img = Sprite( - image=self._display_tex, parent=self._node, w=250, h=120, x=10, y=10) - - # Defer the further loading - Globals.base.taskMgr.doMethodLater(0.3, self._late_init, "FPSChartInit") - - def _late_init(self, task): - """ Gets called after the pipeline was initialized """ - self._display_txt = Text( - text="40 ms", parent=self._node, x=20, y=25, - size=13, color=Vec3(1), may_change=True) - self._display_txt_bottom = Text( - text="0 ms", parent=self._node, x=20, y=120, - size=13, color=Vec3(1), may_change=True) - - # Create the shader which generates the visualization texture - self._cshader_node = ComputeNode("FPSChartUpdateChart") - self._cshader_node.add_dispatch(250 // 10, 120 // 4, 1) - self._cshader_np = self._node.attach_new_node(self._cshader_node) - - self._cshader = RPLoader.load_shader("/$$rp/shader/fps_chart.compute.glsl") - self._cshader_np.set_shader(self._cshader) - self._cshader_np.set_shader_inputs( - DestTex=self._display_tex, - FPSValues=self._storage_buffer, - index=self._store_index, - maxMs=self._chart_ms_max) - - self._update_shader_node = ComputeNode("FPSChartUpdateValues") - self._update_shader_node.add_dispatch(1, 1, 1) - self._update_shader_np = self._node.attach_new_node(self._update_shader_node) - self._ushader = RPLoader.load_shader("/$$rp/shader/fps_chart_update.compute.glsl") - self._update_shader_np.set_shader(self._ushader) - self._update_shader_np.set_shader_inputs( - DestTex=self._storage_buffer, - index=self._store_index, - currentData=self._current_ftime) - - Globals.base.addTask(self._update, "UpdateFPSChart", sort=-50) - - return task.done - - def _update(self, task): - """ Updates the widget """ - self._store_index[0] = (self._store_index[0] + 1) % 250 - self._current_ftime[0] = Globals.clock.get_dt() * 1000.0 - - avg_fps = Globals.clock.get_average_frame_rate() - if avg_fps > 122: - self._chart_ms_max[0] = 10.0 - elif avg_fps > 62: - self._chart_ms_max[0] = 20.0 - elif avg_fps > 32: - self._chart_ms_max[0] = 40.0 - elif avg_fps > 15: - self._chart_ms_max[0] = 70.0 - else: - self._chart_ms_max[0] = 200.0 - - self._display_txt.set_text(str(int(self._chart_ms_max[0])) + " ms") - - return task.cont +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +from __future__ import division + +from panda3d.core import ComputeNode, Vec4, Vec3, PTAInt, PTAFloat + +from rpcore.gui.sprite import Sprite +from rpcore.gui.text import Text + +from rpcore.rpobject import RPObject +from rpcore.image import Image +from rpcore.globals import Globals +from rpcore.loader import RPLoader + + +class FPSChart(RPObject): + + """ Widget to show the FPS as a chart """ + + def __init__(self, pipeline, parent): + """ Inits the widget """ + RPObject.__init__(self) + self._pipeline = pipeline + self._parent = parent + self._node = self._parent.attach_new_node("FPSChartNode") + self._create_components() + + def _create_components(self): + """ Internal method to init the widgets components """ + + # Create the buffer which stores the last FPS values + self._storage_buffer = Image.create_buffer("FPSValues", 250, "R16") + self._storage_buffer.set_clear_color(Vec4(0)) + self._storage_buffer.clear_image() + + self._store_index = PTAInt.empty_array(1) + self._store_index[0] = 0 + + self._current_ftime = PTAFloat.empty_array(1) + self._current_ftime[0] = 16.0 + + self._chart_ms_max = PTAFloat.empty_array(1) + self._chart_ms_max[0] = 40 + + # Create the texture where the gui component is rendered inside + self._display_tex = Image.create_2d("FPSChartRender", 250, 120, "RGBA8") + self._display_tex.set_clear_color(Vec4(0)) + self._display_tex.clear_image() + self._display_img = Sprite( + image=self._display_tex, parent=self._node, w=250, h=120, x=10, y=10) + + # Defer the further loading + Globals.base.taskMgr.doMethodLater(0.3, self._late_init, "FPSChartInit") + + def _late_init(self, task): + """ Gets called after the pipeline was initialized """ + self._display_txt = Text( + text="40 ms", parent=self._node, x=20, y=25, + size=13, color=Vec3(1), may_change=True) + self._display_txt_bottom = Text( + text="0 ms", parent=self._node, x=20, y=120, + size=13, color=Vec3(1), may_change=True) + + # Create the shader which generates the visualization texture + self._cshader_node = ComputeNode("FPSChartUpdateChart") + self._cshader_node.add_dispatch(250 // 10, 120 // 4, 1) + self._cshader_np = self._node.attach_new_node(self._cshader_node) + + self._cshader = RPLoader.load_shader("/$$rp/shader/fps_chart.compute.glsl") + self._cshader_np.set_shader(self._cshader) + self._cshader_np.set_shader_inputs( + DestTex=self._display_tex, + FPSValues=self._storage_buffer, + index=self._store_index, + maxMs=self._chart_ms_max) + + self._update_shader_node = ComputeNode("FPSChartUpdateValues") + self._update_shader_node.add_dispatch(1, 1, 1) + self._update_shader_np = self._node.attach_new_node(self._update_shader_node) + self._ushader = RPLoader.load_shader("/$$rp/shader/fps_chart_update.compute.glsl") + self._update_shader_np.set_shader(self._ushader) + self._update_shader_np.set_shader_inputs( + DestTex=self._storage_buffer, + index=self._store_index, + currentData=self._current_ftime) + + Globals.base.addTask(self._update, "UpdateFPSChart", sort=-50) + + return task.done + + def _update(self, task): + """ Updates the widget """ + self._store_index[0] = (self._store_index[0] + 1) % 250 + self._current_ftime[0] = Globals.clock.get_dt() * 1000.0 + + avg_fps = Globals.clock.get_average_frame_rate() + if avg_fps > 122: + self._chart_ms_max[0] = 10.0 + elif avg_fps > 62: + self._chart_ms_max[0] = 20.0 + elif avg_fps > 32: + self._chart_ms_max[0] = 40.0 + elif avg_fps > 15: + self._chart_ms_max[0] = 70.0 + else: + self._chart_ms_max[0] = 200.0 + + self._display_txt.set_text(str(int(self._chart_ms_max[0])) + " ms") + + return task.cont diff --git a/RenderPipelineFile/rpcore/gui/labeled_checkbox.py b/RenderPipelineFile/rpcore/gui/labeled_checkbox.py index d6889e6e..6b851697 100644 --- a/RenderPipelineFile/rpcore/gui/labeled_checkbox.py +++ b/RenderPipelineFile/rpcore/gui/labeled_checkbox.py @@ -1,86 +1,86 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import Vec3 -import direct.gui.DirectGuiGlobals as DGG - -from rpcore.rpobject import RPObject -from rpcore.gui.checkbox import Checkbox -from rpcore.gui.text import Text - - -class LabeledCheckbox(RPObject): - - """ This is a checkbox, combined with a label. The arguments are - equal to the Checkbox and OnscreenText arguments. """ - - def __init__(self, parent=None, x=0, y=0, chb_callback=None, - chb_args=None, chb_checked=True, text="", text_size=16, - radio=False, text_color=None, expand_width=100, enabled=True): - """ Constructs a new checkbox, forwarding most of the elements to the - underlying Checkbox and Text. """ - RPObject.__init__(self) - if chb_args is None: - chb_args = [] - - if text_color is None: - text_color = Vec3(1) - - if not enabled: - text_color = Vec3(1.0, 0, 0.28) - - self.text_color = text_color - - self._checkbox = Checkbox( - parent=parent, x=x, y=y, enabled=enabled, callback=chb_callback, - extra_args=chb_args, checked=chb_checked, radio=radio, - expand_width=expand_width) - self._text = Text( - x=x + 26, y=y + 9 + text_size // 4, text=text, align="left", - parent=parent, size=text_size, color=text_color, may_change=True) - - if enabled: - self._checkbox.node.bind(DGG.WITHIN, self._on_node_enter) - self._checkbox.node.bind(DGG.WITHOUT, self._on_node_leave) - - def _on_node_enter(self, *args): # pylint: disable=unused-argument - """ Internal callback when the node gets hovered """ - self._text.node["fg"] = (self.text_color.x + 0.1, self.text_color.y + 0.1, - self.text_color.z + 0.1, 1.0) - - def _on_node_leave(self, *args): # pylint: disable=unused-argument - """ Internal callback when the node gets no longer hovered """ - self._text.node["fg"] = (self.text_color.x, self.text_color.y, self.text_color.z, 1.0) - - @property - def checkbox(self): - """ Returns a handle to the checkbox """ - return self._checkbox - - @property - def label(self): - """ Returns a handle to the label """ - return self._text +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import Vec3 +import direct.gui.DirectGuiGlobals as DGG + +from rpcore.rpobject import RPObject +from rpcore.gui.checkbox import Checkbox +from rpcore.gui.text import Text + + +class LabeledCheckbox(RPObject): + + """ This is a checkbox, combined with a label. The arguments are + equal to the Checkbox and OnscreenText arguments. """ + + def __init__(self, parent=None, x=0, y=0, chb_callback=None, + chb_args=None, chb_checked=True, text="", text_size=16, + radio=False, text_color=None, expand_width=100, enabled=True): + """ Constructs a new checkbox, forwarding most of the elements to the + underlying Checkbox and Text. """ + RPObject.__init__(self) + if chb_args is None: + chb_args = [] + + if text_color is None: + text_color = Vec3(1) + + if not enabled: + text_color = Vec3(1.0, 0, 0.28) + + self.text_color = text_color + + self._checkbox = Checkbox( + parent=parent, x=x, y=y, enabled=enabled, callback=chb_callback, + extra_args=chb_args, checked=chb_checked, radio=radio, + expand_width=expand_width) + self._text = Text( + x=x + 26, y=y + 9 + text_size // 4, text=text, align="left", + parent=parent, size=text_size, color=text_color, may_change=True) + + if enabled: + self._checkbox.node.bind(DGG.WITHIN, self._on_node_enter) + self._checkbox.node.bind(DGG.WITHOUT, self._on_node_leave) + + def _on_node_enter(self, *args): # pylint: disable=unused-argument + """ Internal callback when the node gets hovered """ + self._text.node["fg"] = (self.text_color.x + 0.1, self.text_color.y + 0.1, + self.text_color.z + 0.1, 1.0) + + def _on_node_leave(self, *args): # pylint: disable=unused-argument + """ Internal callback when the node gets no longer hovered """ + self._text.node["fg"] = (self.text_color.x, self.text_color.y, self.text_color.z, 1.0) + + @property + def checkbox(self): + """ Returns a handle to the checkbox """ + return self._checkbox + + @property + def label(self): + """ Returns a handle to the label """ + return self._text diff --git a/RenderPipelineFile/rpcore/gui/loading_screen.py b/RenderPipelineFile/rpcore/gui/loading_screen.py index 6fbf6a94..e3acf8fa 100644 --- a/RenderPipelineFile/rpcore/gui/loading_screen.py +++ b/RenderPipelineFile/rpcore/gui/loading_screen.py @@ -1,70 +1,70 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" -from __future__ import division - -from six.moves import range # pylint: disable=import-error - -from rpcore.gui.sprite import Sprite -from rpcore.rpobject import RPObject -from rpcore.globals import Globals - - -class LoadingScreen(RPObject): - - """ This is the default loading screen used by the pipeline. It provides - the ability to display a simple image during loading. The image should be - in the format 16:9 and not too small, to avoid being blurred out. """ - - def __init__(self, pipeline, image_source="/$$rp/data/gui/loading_screen_bg.txo"): - """ Inits the loading screen with a given image source. By default, - this is the pipeline loading screen, but it can be overridden. """ - RPObject.__init__(self) - self.pipeline = pipeline - self.image_source = image_source - - def create(self): - """ Creates the gui components """ - screen_w, screen_h = Globals.native_resolution.x, Globals.native_resolution.y - self.fullscreen_node = Globals.base.pixel2dp.attach_new_node("LoadingScreen") - self.fullscreen_node.set_bin("fixed", 10) - self.fullscreen_node.set_depth_test(False) - - scale_w = screen_w / 1920.0 - scale_h = screen_h / 1080.0 - scale = max(scale_w, scale_h) - - self.fullscreen_bg = Sprite( - image=self.image_source, x=(screen_w - 1920.0 * scale) // 2, - y=(screen_h - 1080.0 * scale) // 2, w=int(1920 * scale), - h=int(1080 * scale), parent=self.fullscreen_node, near_filter=False) - - for _ in range(2): - Globals.base.graphicsEngine.render_frame() - - def remove(self): - """ Removes the loading screen """ - self.fullscreen_bg.node["image"].get_texture().release_all() - self.fullscreen_node.remove_node() +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +from __future__ import division + +from six.moves import range # pylint: disable=import-error + +from rpcore.gui.sprite import Sprite +from rpcore.rpobject import RPObject +from rpcore.globals import Globals + + +class LoadingScreen(RPObject): + + """ This is the default loading screen used by the pipeline. It provides + the ability to display a simple image during loading. The image should be + in the format 16:9 and not too small, to avoid being blurred out. """ + + def __init__(self, pipeline, image_source="/$$rp/data/gui/loading_screen_bg.txo"): + """ Inits the loading screen with a given image source. By default, + this is the pipeline loading screen, but it can be overridden. """ + RPObject.__init__(self) + self.pipeline = pipeline + self.image_source = image_source + + def create(self): + """ Creates the gui components """ + screen_w, screen_h = Globals.native_resolution.x, Globals.native_resolution.y + self.fullscreen_node = Globals.base.pixel2dp.attach_new_node("LoadingScreen") + self.fullscreen_node.set_bin("fixed", 10) + self.fullscreen_node.set_depth_test(False) + + scale_w = screen_w / 1920.0 + scale_h = screen_h / 1080.0 + scale = max(scale_w, scale_h) + + self.fullscreen_bg = Sprite( + image=self.image_source, x=(screen_w - 1920.0 * scale) // 2, + y=(screen_h - 1080.0 * scale) // 2, w=int(1920 * scale), + h=int(1080 * scale), parent=self.fullscreen_node, near_filter=False) + + for _ in range(2): + Globals.base.graphicsEngine.render_frame() + + def remove(self): + """ Removes the loading screen """ + self.fullscreen_bg.node["image"].get_texture().release_all() + self.fullscreen_node.remove_node() diff --git a/RenderPipelineFile/rpcore/gui/pipe_viewer.py b/RenderPipelineFile/rpcore/gui/pipe_viewer.py index 10b4487f..b04d4563 100644 --- a/RenderPipelineFile/rpcore/gui/pipe_viewer.py +++ b/RenderPipelineFile/rpcore/gui/pipe_viewer.py @@ -1,217 +1,217 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rplibs.six import iteritems - -from panda3d.core import Texture, Vec3 - -from direct.gui.DirectScrolledFrame import DirectScrolledFrame -from direct.gui.DirectFrame import DirectFrame - -from rpcore.globals import Globals -from rpcore.util.generic import rgb_from_string -from rpcore.util.display_shader_builder import DisplayShaderBuilder -from rpcore.util.shader_input_blocks import SimpleInputBlock, GroupedInputBlock - -from rpcore.gui.draggable_window import DraggableWindow -from rpcore.gui.text import Text -from rpcore.gui.sprite import Sprite - - -class PipeViewer(DraggableWindow): - - """ Small tool which displays the order of the graphic pipes """ - - _STAGE_MGR = None - - @classmethod - def register_stage_mgr(cls, mgr): - """ Sets the stage manager, this is a workaround to prevent - circular imports, since the pipe viewer is already included - from the StageManager """ - cls._STAGE_MGR = mgr - - def __init__(self, pipeline, parent): - """ Constructs the pipe viewer """ - DraggableWindow.__init__(self, width=1300, height=900, parent=parent, - title="Pipeline Visualizer") - self._pipeline = pipeline - self._scroll_width = 8000 - self._scroll_height = 3000 - self._created = False - self._create_components() - self.hide() - - def toggle(self): - """ Toggles the pipe viewer """ - if self._visible: - Globals.base.taskMgr.remove("RP_GUI_UpdatePipeViewer") - self.hide() - else: - Globals.base.taskMgr.add(self._update_task, "RP_GUI_UpdatePipeViewer") - if not self._created: - self._populate_content() - self.show() - - def _update_task(self, task=None): - """ Updates the viewer """ - scroll_value = self._content_frame.horizontalScroll["value"] - scroll_value *= 2.45 - self._pipe_descriptions.set_x(scroll_value * 2759.0) - return task.cont - - def _populate_content(self): # pylint: disable=too-many-branches,too-many-statements - """ Reads the pipes and stages from the stage manager and renders those - into the window """ - self._created = True - self._pipe_node = self._content_node.attach_new_node("pipes") - self._pipe_node.set_scale(1, 1, -1) - self._stage_node = self._content_node.attach_new_node("stages") - current_pipes = [] - pipe_pixel_size = 3 - pipe_height = 100 - - # Generate stages - for offs, stage in enumerate(self._STAGE_MGR.stages): - node = self._content_node.attach_new_node("stage") - node.set_pos(220 + offs * 200.0, 0, 20) - node.set_scale(1, 1, -1) - DirectFrame(parent=node, frameSize=(10, 150, 0, -3600), - frameColor=(0.2, 0.2, 0.2, 1)) - Text(text=str(stage.debug_name.replace("Stage", "")), - parent=node, x=20, y=25, size=15) - - for output_pipe, pipe_tex in iteritems(stage.produced_pipes): - pipe_idx = 0 - r, g, b = rgb_from_string(output_pipe) - if output_pipe in current_pipes: - pipe_idx = current_pipes.index(output_pipe) - else: - current_pipes.append(output_pipe) - pipe_idx = len(current_pipes) - 1 - DirectFrame(parent=node, - frameSize=(0, 8000, pipe_pixel_size / 2, - -pipe_pixel_size / 2), - frameColor=(r, g, b, 1), - pos=(10, 1, -95 - pipe_idx * pipe_height)) - w = 160 - h = Globals.native_resolution.y /\ - float(Globals.native_resolution.x) * w - DirectFrame(parent=node, - frameSize=(-pipe_pixel_size, w + pipe_pixel_size, - h / 2 + pipe_pixel_size, - -h / 2 - pipe_pixel_size), - frameColor=(r, g, b, 1), - pos=(0, 1, -95 - pipe_idx * pipe_height)) - - if isinstance(pipe_tex, (list, tuple)): - pipe_tex = pipe_tex[0] - - if isinstance(pipe_tex, (SimpleInputBlock, GroupedInputBlock)): - icon_file = "/$$rp/data/gui/icon_ubo.png" - elif pipe_tex.get_z_size() > 1: - icon_file = "/$$rp/data/gui/icon_texture.png" - elif pipe_tex.get_texture_type() == Texture.TT_buffer_texture: - icon_file = "/$$rp/data/gui/icon_buffer_texture.png" - else: - icon_file = None - preview = Sprite( - image=pipe_tex, parent=node, x=0, - y=50 + pipe_idx * pipe_height, w=w, h=h, any_filter=False, - transparent=False) - - preview_shader = DisplayShaderBuilder.build(pipe_tex, int(w), int(h)) - preview.set_shader(preview_shader) - - preview.set_shader_inputs( - mipmap=0, - slice=0, - brightness=1, - tonemap=False) - - if icon_file: - Sprite(image=icon_file, parent=node, - x=55, y=65 + pipe_idx * pipe_height, - w=48, h=48, near_filter=False, - transparent=True) - - if isinstance(pipe_tex, (SimpleInputBlock, GroupedInputBlock)): - tex_desc = "UBO" - else: - tex_desc = pipe_tex.format_texture_type(pipe_tex.get_texture_type()) - tex_desc += " - " + pipe_tex.format_format(pipe_tex.get_format()).upper() - - Text(text=tex_desc, parent=node, x=55 + 48 / 2, - y=130 + pipe_idx * pipe_height, color=Vec3(0.2), - size=12, align="center") - - for input_pipe in stage.required_pipes: - if input_pipe not in current_pipes: - self.warn("Pipe not found:", input_pipe) - continue - idx = current_pipes.index(input_pipe) - r, g, b = rgb_from_string(input_pipe) - DirectFrame(parent=node, frameSize=(0, 10, 40, -40), - frameColor=(r, g, b, 1), - pos=(5, 1, -95 - idx * pipe_height)) - - self._pipe_descriptions = self._content_node.attach_new_node( - "PipeDescriptions") - self._pipe_descriptions.set_scale(1, 1, -1) - - DirectFrame(parent=self._pipe_descriptions, frameSize=(0, 190, 0, -5000), - frameColor=(0.1, 0.1, 0.1, 1.0)) - - # Generate the pipe descriptions - for idx, pipe in enumerate(current_pipes): - r, g, b = rgb_from_string(pipe) - DirectFrame(parent=self._pipe_descriptions, - frameSize=(0, 180, -95, -135), - frameColor=(r, g, b, 1.0), pos=(0, 1, -idx * pipe_height)) - Text(parent=self._pipe_descriptions, text=pipe, - x=42, y=121 + idx * pipe_height, size=15, - color=Vec3(0.1)) - Sprite(parent=self._pipe_descriptions, x=9, y=103 + idx * pipe_height, - image="/$$rp/data/gui/icon_pipe.png", - transparent=True, near_filter=False) - - def _create_components(self): - """ Internal method to create the window components """ - DraggableWindow._create_components(self) - - self._content_frame = DirectScrolledFrame( - frameSize=(0, self._width - 40, 0, self._height - 80), - canvasSize=(0, self._scroll_width, 0, self._scroll_height), - autoHideScrollBars=False, - scrollBarWidth=20.0, - frameColor=(0, 0, 0, 0), - verticalScroll_relief=False, - horizontalScroll_relief=False, - parent=self._node, - pos=(20, 1, -self._height + 20)) - self._content_node = self._content_frame.getCanvas().attach_new_node("PipeComponents") - self._content_node.set_scale(1, 1, -1) - self._content_node.set_z(self._scroll_height) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rplibs.six import iteritems + +from panda3d.core import Texture, Vec3 + +from direct.gui.DirectScrolledFrame import DirectScrolledFrame +from direct.gui.DirectFrame import DirectFrame + +from rpcore.globals import Globals +from rpcore.util.generic import rgb_from_string +from rpcore.util.display_shader_builder import DisplayShaderBuilder +from rpcore.util.shader_input_blocks import SimpleInputBlock, GroupedInputBlock + +from rpcore.gui.draggable_window import DraggableWindow +from rpcore.gui.text import Text +from rpcore.gui.sprite import Sprite + + +class PipeViewer(DraggableWindow): + + """ Small tool which displays the order of the graphic pipes """ + + _STAGE_MGR = None + + @classmethod + def register_stage_mgr(cls, mgr): + """ Sets the stage manager, this is a workaround to prevent + circular imports, since the pipe viewer is already included + from the StageManager """ + cls._STAGE_MGR = mgr + + def __init__(self, pipeline, parent): + """ Constructs the pipe viewer """ + DraggableWindow.__init__(self, width=1300, height=900, parent=parent, + title="Pipeline Visualizer") + self._pipeline = pipeline + self._scroll_width = 8000 + self._scroll_height = 3000 + self._created = False + self._create_components() + self.hide() + + def toggle(self): + """ Toggles the pipe viewer """ + if self._visible: + Globals.base.taskMgr.remove("RP_GUI_UpdatePipeViewer") + self.hide() + else: + Globals.base.taskMgr.add(self._update_task, "RP_GUI_UpdatePipeViewer") + if not self._created: + self._populate_content() + self.show() + + def _update_task(self, task=None): + """ Updates the viewer """ + scroll_value = self._content_frame.horizontalScroll["value"] + scroll_value *= 2.45 + self._pipe_descriptions.set_x(scroll_value * 2759.0) + return task.cont + + def _populate_content(self): # pylint: disable=too-many-branches,too-many-statements + """ Reads the pipes and stages from the stage manager and renders those + into the window """ + self._created = True + self._pipe_node = self._content_node.attach_new_node("pipes") + self._pipe_node.set_scale(1, 1, -1) + self._stage_node = self._content_node.attach_new_node("stages") + current_pipes = [] + pipe_pixel_size = 3 + pipe_height = 100 + + # Generate stages + for offs, stage in enumerate(self._STAGE_MGR.stages): + node = self._content_node.attach_new_node("stage") + node.set_pos(220 + offs * 200.0, 0, 20) + node.set_scale(1, 1, -1) + DirectFrame(parent=node, frameSize=(10, 150, 0, -3600), + frameColor=(0.2, 0.2, 0.2, 1)) + Text(text=str(stage.debug_name.replace("Stage", "")), + parent=node, x=20, y=25, size=15) + + for output_pipe, pipe_tex in iteritems(stage.produced_pipes): + pipe_idx = 0 + r, g, b = rgb_from_string(output_pipe) + if output_pipe in current_pipes: + pipe_idx = current_pipes.index(output_pipe) + else: + current_pipes.append(output_pipe) + pipe_idx = len(current_pipes) - 1 + DirectFrame(parent=node, + frameSize=(0, 8000, pipe_pixel_size / 2, + -pipe_pixel_size / 2), + frameColor=(r, g, b, 1), + pos=(10, 1, -95 - pipe_idx * pipe_height)) + w = 160 + h = Globals.native_resolution.y /\ + float(Globals.native_resolution.x) * w + DirectFrame(parent=node, + frameSize=(-pipe_pixel_size, w + pipe_pixel_size, + h / 2 + pipe_pixel_size, + -h / 2 - pipe_pixel_size), + frameColor=(r, g, b, 1), + pos=(0, 1, -95 - pipe_idx * pipe_height)) + + if isinstance(pipe_tex, (list, tuple)): + pipe_tex = pipe_tex[0] + + if isinstance(pipe_tex, (SimpleInputBlock, GroupedInputBlock)): + icon_file = "/$$rp/data/gui/icon_ubo.png" + elif pipe_tex.get_z_size() > 1: + icon_file = "/$$rp/data/gui/icon_texture.png" + elif pipe_tex.get_texture_type() == Texture.TT_buffer_texture: + icon_file = "/$$rp/data/gui/icon_buffer_texture.png" + else: + icon_file = None + preview = Sprite( + image=pipe_tex, parent=node, x=0, + y=50 + pipe_idx * pipe_height, w=w, h=h, any_filter=False, + transparent=False) + + preview_shader = DisplayShaderBuilder.build(pipe_tex, int(w), int(h)) + preview.set_shader(preview_shader) + + preview.set_shader_inputs( + mipmap=0, + slice=0, + brightness=1, + tonemap=False) + + if icon_file: + Sprite(image=icon_file, parent=node, + x=55, y=65 + pipe_idx * pipe_height, + w=48, h=48, near_filter=False, + transparent=True) + + if isinstance(pipe_tex, (SimpleInputBlock, GroupedInputBlock)): + tex_desc = "UBO" + else: + tex_desc = pipe_tex.format_texture_type(pipe_tex.get_texture_type()) + tex_desc += " - " + pipe_tex.format_format(pipe_tex.get_format()).upper() + + Text(text=tex_desc, parent=node, x=55 + 48 / 2, + y=130 + pipe_idx * pipe_height, color=Vec3(0.2), + size=12, align="center") + + for input_pipe in stage.required_pipes: + if input_pipe not in current_pipes: + self.warn("Pipe not found:", input_pipe) + continue + idx = current_pipes.index(input_pipe) + r, g, b = rgb_from_string(input_pipe) + DirectFrame(parent=node, frameSize=(0, 10, 40, -40), + frameColor=(r, g, b, 1), + pos=(5, 1, -95 - idx * pipe_height)) + + self._pipe_descriptions = self._content_node.attach_new_node( + "PipeDescriptions") + self._pipe_descriptions.set_scale(1, 1, -1) + + DirectFrame(parent=self._pipe_descriptions, frameSize=(0, 190, 0, -5000), + frameColor=(0.1, 0.1, 0.1, 1.0)) + + # Generate the pipe descriptions + for idx, pipe in enumerate(current_pipes): + r, g, b = rgb_from_string(pipe) + DirectFrame(parent=self._pipe_descriptions, + frameSize=(0, 180, -95, -135), + frameColor=(r, g, b, 1.0), pos=(0, 1, -idx * pipe_height)) + Text(parent=self._pipe_descriptions, text=pipe, + x=42, y=121 + idx * pipe_height, size=15, + color=Vec3(0.1)) + Sprite(parent=self._pipe_descriptions, x=9, y=103 + idx * pipe_height, + image="/$$rp/data/gui/icon_pipe.png", + transparent=True, near_filter=False) + + def _create_components(self): + """ Internal method to create the window components """ + DraggableWindow._create_components(self) + + self._content_frame = DirectScrolledFrame( + frameSize=(0, self._width - 40, 0, self._height - 80), + canvasSize=(0, self._scroll_width, 0, self._scroll_height), + autoHideScrollBars=False, + scrollBarWidth=20.0, + frameColor=(0, 0, 0, 0), + verticalScroll_relief=False, + horizontalScroll_relief=False, + parent=self._node, + pos=(20, 1, -self._height + 20)) + self._content_node = self._content_frame.getCanvas().attach_new_node("PipeComponents") + self._content_node.set_scale(1, 1, -1) + self._content_node.set_z(self._scroll_height) diff --git a/RenderPipelineFile/rpcore/gui/pixel_inspector.py b/RenderPipelineFile/rpcore/gui/pixel_inspector.py index 42595abc..c8f07eed 100644 --- a/RenderPipelineFile/rpcore/gui/pixel_inspector.py +++ b/RenderPipelineFile/rpcore/gui/pixel_inspector.py @@ -1,81 +1,81 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import CardMaker, Vec2, GraphicsWindow -from rpcore.rpobject import RPObject -from rpcore.globals import Globals -from rpcore.loader import RPLoader - - -class PixelInspector(RPObject): - - """ Widget to analyze the rendered pixels, by zooming in """ - - def __init__(self, pipeline): - RPObject.__init__(self) - self._pipeline = pipeline - self._node = Globals.base.pixel2d.attach_new_node("ExposureWidgetNode") - self._create_components() - self.hide() - - def _create_components(self): - """ Internal method to init the widgets components """ - card_maker = CardMaker("PixelInspector") - card_maker.set_frame(-200, 200, -150, 150) - self._zoomer = self._node.attach_new_node(card_maker.generate()) - - # Defer the further loading - Globals.base.taskMgr.doMethodLater( - 1.0, self._late_init, "PixelInspectorLateInit") - Globals.base.accept("q", self.show) - Globals.base.accept("q-up", self.hide) - - def show(self): - """ Shows the inspector """ - self._node.show() - - def hide(self): - """ Shows the inspector """ - self._node.hide() - - def _late_init(self, task): - """ Gets called after the pipeline got initialized """ - scene_tex = self._pipeline.stage_mgr.pipes["ShadedScene"] - self._zoomer.set_shader(RPLoader.load_shader( - "/$$rp/shader/default_gui_shader.vert.glsl", - "/$$rp/shader/pixel_inspector.frag.glsl")) - self._zoomer.set_shader_input("SceneTex", scene_tex) - return task.done - - def update(self): - """ Updates the pixel preview """ - if isinstance(Globals.base.win, GraphicsWindow): - mouse = Globals.base.win.get_pointer(0) - if mouse.get_in_window(): - pos = mouse.get_x(), 1, -mouse.get_y() - rel_mouse_pos = Vec2(mouse.get_x(), Globals.native_resolution.y - mouse.get_y()) - self._node.set_pos(pos) - self._zoomer.set_shader_input("mousePos", rel_mouse_pos) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import CardMaker, Vec2, GraphicsWindow +from rpcore.rpobject import RPObject +from rpcore.globals import Globals +from rpcore.loader import RPLoader + + +class PixelInspector(RPObject): + + """ Widget to analyze the rendered pixels, by zooming in """ + + def __init__(self, pipeline): + RPObject.__init__(self) + self._pipeline = pipeline + self._node = Globals.base.pixel2d.attach_new_node("ExposureWidgetNode") + self._create_components() + self.hide() + + def _create_components(self): + """ Internal method to init the widgets components """ + card_maker = CardMaker("PixelInspector") + card_maker.set_frame(-200, 200, -150, 150) + self._zoomer = self._node.attach_new_node(card_maker.generate()) + + # Defer the further loading + Globals.base.taskMgr.doMethodLater( + 1.0, self._late_init, "PixelInspectorLateInit") + Globals.base.accept("q", self.show) + Globals.base.accept("q-up", self.hide) + + def show(self): + """ Shows the inspector """ + self._node.show() + + def hide(self): + """ Shows the inspector """ + self._node.hide() + + def _late_init(self, task): + """ Gets called after the pipeline got initialized """ + scene_tex = self._pipeline.stage_mgr.pipes["ShadedScene"] + self._zoomer.set_shader(RPLoader.load_shader( + "/$$rp/shader/default_gui_shader.vert.glsl", + "/$$rp/shader/pixel_inspector.frag.glsl")) + self._zoomer.set_shader_input("SceneTex", scene_tex) + return task.done + + def update(self): + """ Updates the pixel preview """ + if isinstance(Globals.base.win, GraphicsWindow): + mouse = Globals.base.win.get_pointer(0) + if mouse.get_in_window(): + pos = mouse.get_x(), 1, -mouse.get_y() + rel_mouse_pos = Vec2(mouse.get_x(), Globals.native_resolution.y - mouse.get_y()) + self._node.set_pos(pos) + self._zoomer.set_shader_input("mousePos", rel_mouse_pos) diff --git a/RenderPipelineFile/rpcore/gui/render_mode_selector.py b/RenderPipelineFile/rpcore/gui/render_mode_selector.py index 0eec7130..0ec2cc86 100644 --- a/RenderPipelineFile/rpcore/gui/render_mode_selector.py +++ b/RenderPipelineFile/rpcore/gui/render_mode_selector.py @@ -1,132 +1,132 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import division -from functools import partial - -from panda3d.core import Vec3 - -from rplibs.yaml import load_yaml_file - -from rpcore.native import NATIVE_CXX_LOADED -from rpcore.gui.draggable_window import DraggableWindow -from rpcore.gui.labeled_checkbox import LabeledCheckbox -from rpcore.gui.checkbox_collection import CheckboxCollection - - -class RenderModeSelector(DraggableWindow): - - """ Window which offers the user to select a render mode to apply """ - - def __init__(self, pipeline, parent): - DraggableWindow.__init__(self, width=690, height=340, parent=parent, - title="Select render mode") - self._pipeline = pipeline - self._selected_mode = "" - self._create_components() - self.hide() - - def _create_components(self): - """ Internal method to init the components """ - DraggableWindow._create_components(self) - self._content_node = self._node.attach_new_node("content") - self._populate_content() - - def _populate_content(self): - """ Populates the windows content """ - self._content_node.node().remove_all_children() - - # Reload config each time the window is opened so its easy to add new - # render modes - config = load_yaml_file("/$$rpconfig/debugging.yaml") - - debugger_content = self._content_node.attach_new_node("RenderModes") - debugger_content.set_z(-20) - debugger_content.set_x(20) - - render_modes = [("Default", "", False, "", False)] - - # Read modes from configuration - for mode in config["render_modes"]: - data = [mode["name"], mode["key"]] - data.append(mode.get("cxx_only", False)) - data.append(mode.get("requires", "")) - data.append(mode.get("special", False)) - render_modes.append(data) - - collection = CheckboxCollection() - - max_column_height = 9 - - for idx, (mode, mode_id, requires_cxx, requires_plugin, special) in enumerate(render_modes): - offs_y = (idx % max_column_height) * 24 + 35 - offs_x = (idx // max_column_height) * 220 - enabled = True - if requires_cxx and not NATIVE_CXX_LOADED: - enabled = False - - if requires_plugin: - if not self._pipeline.plugin_mgr.is_plugin_enabled(requires_plugin): - enabled = False - - box = LabeledCheckbox( - parent=debugger_content, x=offs_x, y=offs_y, text=mode.upper(), - text_color=Vec3(0.4), radio=True, chb_checked=(mode_id == self._selected_mode), - chb_callback=partial(self._set_render_mode, mode_id, special), - text_size=14, expand_width=230, enabled=enabled) - collection.add(box.checkbox) - - def _set_render_mode(self, mode_id, special, value): - """ Callback which gets called when a render mode got selected """ - if not value: - return - - to_remove = [] - for define in self._pipeline.stage_mgr.defines: - if define.startswith("_RM_"): - to_remove.append(define) - for define in to_remove: - del self._pipeline.stage_mgr.defines[define] - - if mode_id == "": - self._pipeline.stage_mgr.defines["ANY_DEBUG_MODE"] = 0 - else: - # Don't activate the generic debugging mode for special modes. This - # is for modes like luminance which expect the scene to be rendered - # unaltered. - self._pipeline.stage_mgr.defines["ANY_DEBUG_MODE"] = 0 if special else 1 - self._pipeline.stage_mgr.defines["_RM_" + mode_id] = 1 - - self._selected_mode = mode_id - self._pipeline.reload_shaders() - - def toggle(self): - """ Toggles the visibility of this windows """ - if self._visible: - self.hide() - else: - self._populate_content() - self.show() +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import division +from functools import partial + +from panda3d.core import Vec3 + +from rplibs.yaml import load_yaml_file + +from rpcore.native import NATIVE_CXX_LOADED +from rpcore.gui.draggable_window import DraggableWindow +from rpcore.gui.labeled_checkbox import LabeledCheckbox +from rpcore.gui.checkbox_collection import CheckboxCollection + + +class RenderModeSelector(DraggableWindow): + + """ Window which offers the user to select a render mode to apply """ + + def __init__(self, pipeline, parent): + DraggableWindow.__init__(self, width=690, height=340, parent=parent, + title="Select render mode") + self._pipeline = pipeline + self._selected_mode = "" + self._create_components() + self.hide() + + def _create_components(self): + """ Internal method to init the components """ + DraggableWindow._create_components(self) + self._content_node = self._node.attach_new_node("content") + self._populate_content() + + def _populate_content(self): + """ Populates the windows content """ + self._content_node.node().remove_all_children() + + # Reload config each time the window is opened so its easy to add new + # render modes + config = load_yaml_file("/$$rpconfig/debugging.yaml") + + debugger_content = self._content_node.attach_new_node("RenderModes") + debugger_content.set_z(-20) + debugger_content.set_x(20) + + render_modes = [("Default", "", False, "", False)] + + # Read modes from configuration + for mode in config["render_modes"]: + data = [mode["name"], mode["key"]] + data.append(mode.get("cxx_only", False)) + data.append(mode.get("requires", "")) + data.append(mode.get("special", False)) + render_modes.append(data) + + collection = CheckboxCollection() + + max_column_height = 9 + + for idx, (mode, mode_id, requires_cxx, requires_plugin, special) in enumerate(render_modes): + offs_y = (idx % max_column_height) * 24 + 35 + offs_x = (idx // max_column_height) * 220 + enabled = True + if requires_cxx and not NATIVE_CXX_LOADED: + enabled = False + + if requires_plugin: + if not self._pipeline.plugin_mgr.is_plugin_enabled(requires_plugin): + enabled = False + + box = LabeledCheckbox( + parent=debugger_content, x=offs_x, y=offs_y, text=mode.upper(), + text_color=Vec3(0.4), radio=True, chb_checked=(mode_id == self._selected_mode), + chb_callback=partial(self._set_render_mode, mode_id, special), + text_size=14, expand_width=230, enabled=enabled) + collection.add(box.checkbox) + + def _set_render_mode(self, mode_id, special, value): + """ Callback which gets called when a render mode got selected """ + if not value: + return + + to_remove = [] + for define in self._pipeline.stage_mgr.defines: + if define.startswith("_RM_"): + to_remove.append(define) + for define in to_remove: + del self._pipeline.stage_mgr.defines[define] + + if mode_id == "": + self._pipeline.stage_mgr.defines["ANY_DEBUG_MODE"] = 0 + else: + # Don't activate the generic debugging mode for special modes. This + # is for modes like luminance which expect the scene to be rendered + # unaltered. + self._pipeline.stage_mgr.defines["ANY_DEBUG_MODE"] = 0 if special else 1 + self._pipeline.stage_mgr.defines["_RM_" + mode_id] = 1 + + self._selected_mode = mode_id + self._pipeline.reload_shaders() + + def toggle(self): + """ Toggles the visibility of this windows """ + if self._visible: + self.hide() + else: + self._populate_content() + self.show() diff --git a/RenderPipelineFile/rpcore/gui/slider.py b/RenderPipelineFile/rpcore/gui/slider.py index 14226dad..1591fb08 100644 --- a/RenderPipelineFile/rpcore/gui/slider.py +++ b/RenderPipelineFile/rpcore/gui/slider.py @@ -1,68 +1,68 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -import direct.gui.DirectGuiGlobals as DGG -from direct.gui.DirectSlider import DirectSlider - -from rpcore.rpobject import RPObject - - -class Slider(RPObject): - - """ This is a simple wrapper around DirectSlider, providing a simpler - interface """ - - def __init__(self, x=0, y=0, parent=None, size=100, min_value=0, - max_value=100, value=50, page_size=1, callback=None, - extra_args=None): - """ Inits the slider """ - RPObject.__init__(self) - if extra_args is None: - extra_args = [] - - # Scale has to be 2.0, otherwise there will be an error. - self._node = DirectSlider( - pos=(size * 0.5 + x, 1, -y), parent=parent, range=(min_value, max_value), - value=value, pageSize=page_size, scale=2.0, command=callback, - extraArgs=extra_args, frameColor=(0.0, 0.0, 0.0, 1), - frameSize=(-size * 0.25, size * 0.25, -5, 5), relief=DGG.FLAT, - thumb_frameColor=(0.35, 0.53, 0.2, 1.0), thumb_relief=DGG.FLAT, - thumb_frameSize=(-2.5, 2.5, -5.0, 5.0),) - - @property - def value(self): - """ Returns the currently assigned value """ - return self._node["value"] - - @value.setter - def value(self, value): - """ Sets the value of the slider """ - self._node["value"] = value - - @property - def node(self): - """ Returns a handle to the internally used node """ - return self._node +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +import direct.gui.DirectGuiGlobals as DGG +from direct.gui.DirectSlider import DirectSlider + +from rpcore.rpobject import RPObject + + +class Slider(RPObject): + + """ This is a simple wrapper around DirectSlider, providing a simpler + interface """ + + def __init__(self, x=0, y=0, parent=None, size=100, min_value=0, + max_value=100, value=50, page_size=1, callback=None, + extra_args=None): + """ Inits the slider """ + RPObject.__init__(self) + if extra_args is None: + extra_args = [] + + # Scale has to be 2.0, otherwise there will be an error. + self._node = DirectSlider( + pos=(size * 0.5 + x, 1, -y), parent=parent, range=(min_value, max_value), + value=value, pageSize=page_size, scale=2.0, command=callback, + extraArgs=extra_args, frameColor=(0.0, 0.0, 0.0, 1), + frameSize=(-size * 0.25, size * 0.25, -5, 5), relief=DGG.FLAT, + thumb_frameColor=(0.35, 0.53, 0.2, 1.0), thumb_relief=DGG.FLAT, + thumb_frameSize=(-2.5, 2.5, -5.0, 5.0),) + + @property + def value(self): + """ Returns the currently assigned value """ + return self._node["value"] + + @value.setter + def value(self, value): + """ Sets the value of the slider """ + self._node["value"] = value + + @property + def node(self): + """ Returns a handle to the internally used node """ + return self._node diff --git a/RenderPipelineFile/rpcore/gui/sprite.py b/RenderPipelineFile/rpcore/gui/sprite.py index a505d619..4acd125e 100644 --- a/RenderPipelineFile/rpcore/gui/sprite.py +++ b/RenderPipelineFile/rpcore/gui/sprite.py @@ -1,153 +1,153 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import TransparencyAttrib, Vec3, Texture, SamplerState -from direct.gui.OnscreenImage import OnscreenImage - -from rpcore.rpobject import RPObject -from rpcore.loader import RPLoader - - -class Sprite(RPObject): - - """ Simple wrapper arround OnscreenImage, providing a simpler interface """ - - def __init__(self, image=None, parent=None, x=0, y=0, w=None, h=None, - transparent=True, near_filter=True, any_filter=True): - """ Creates a new image, taking (x,y) as topleft coordinates. - - When near_filter is set to true, a near filter will be set to the - texture passed. This provides sharper images. - - When any_filter is set to false, the passed image won't be modified at - all. This enables you to display existing textures, otherwise the - texture would get a near filter in the 3D View, too. """ - - RPObject.__init__(self) - - if not isinstance(image, Texture): - if not isinstance(image, str): - self.warn("Invalid argument to image parameter:", image) - return - image = RPLoader.load_texture(image) - - if w is None or h is None: - w, h = image.get_x_size(), image.get_y_size() - else: - if w is None or h is None: - w = 10 - h = 10 - - self._width, self._height = w, h - self._initial_pos = self._translate_pos(x, y) - - self.node = OnscreenImage( - image=image, parent=parent, pos=self._initial_pos, - scale=(self._width / 2.0, 1, self._height / 2.0)) - - if transparent: - self.node.set_transparency(TransparencyAttrib.M_alpha) - - tex = self.node.get_texture() - - # Apply a near filter, but only if the parent has no scale, otherwise - # it will look weird - if near_filter and any_filter and parent.get_sx() == 1.0: - tex.set_minfilter(SamplerState.FT_nearest) - tex.set_magfilter(SamplerState.FT_nearest) - - if any_filter: - tex.set_anisotropic_degree(8) - tex.set_wrap_u(SamplerState.WM_clamp) - tex.set_wrap_v(SamplerState.WM_clamp) - - def get_initial_pos(self): - """ Returns the initial position of the image. This can be used for - animations """ - return self._initial_pos - - def pos_interval(self, *args, **kwargs): - """ Returns a pos interval, this is a wrapper around - NodePath.posInterval """ - return self.node.posInterval(*args, **kwargs) - - def hpr_interval(self, *args, **kwargs): - """ Returns a hpr interval, this is a wrapper around - NodePath.hprInterval """ - return self.node.hprInterval(*args, **kwargs) - - def color_scale_interval(self, *args, **kwargs): - """ Returns a color scale interval, this is a wrapper around - NodePath.colorScaleInterval """ - return self.node.colorScaleInterval(*args, **kwargs) - - def set_image(self, img): - """ Sets the current image """ - self.node.set_image(img) - - def get_width(self): - """ Returns the width of the image in pixels """ - return self._width - - def get_height(self): - """ Returns the height of the image in pixels """ - return self._height - - def set_pos(self, x, y): - """ Sets the position """ - self.node.set_pos(self._translate_pos(x, y)) - - def _translate_pos(self, x, y): - """ Converts 2d coordinates to pandas coordinate system """ - return Vec3(x + self._width / 2.0, 1, -y - self._height / 2.0) - - def set_shader(self, shader): - """ Sets a shader to be used for rendering the image """ - self.node.set_shader(shader) - - def set_shader_input(self, *args): - """ Sets a shader input on the image """ - self.node.set_shader_input(*args) - - def set_shader_inputs(self, **kwargs): - """ Sets multiple shader inputs on the image """ - self.node.set_shader_inputs(**kwargs) - - def remove(self): - """ Removes the image """ - self.node.remove() - - def hide(self): - """ Hides the image """ - self.node.hide() - - def show(self): - """ Shows the image if it was previously hidden """ - self.node.show() - - def is_hidden(self): - """ Returns whether the image is hidden """ - return self.node.is_hidden() +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import TransparencyAttrib, Vec3, Texture, SamplerState +from direct.gui.OnscreenImage import OnscreenImage + +from rpcore.rpobject import RPObject +from rpcore.loader import RPLoader + + +class Sprite(RPObject): + + """ Simple wrapper arround OnscreenImage, providing a simpler interface """ + + def __init__(self, image=None, parent=None, x=0, y=0, w=None, h=None, + transparent=True, near_filter=True, any_filter=True): + """ Creates a new image, taking (x,y) as topleft coordinates. + + When near_filter is set to true, a near filter will be set to the + texture passed. This provides sharper images. + + When any_filter is set to false, the passed image won't be modified at + all. This enables you to display existing textures, otherwise the + texture would get a near filter in the 3D View, too. """ + + RPObject.__init__(self) + + if not isinstance(image, Texture): + if not isinstance(image, str): + self.warn("Invalid argument to image parameter:", image) + return + image = RPLoader.load_texture(image) + + if w is None or h is None: + w, h = image.get_x_size(), image.get_y_size() + else: + if w is None or h is None: + w = 10 + h = 10 + + self._width, self._height = w, h + self._initial_pos = self._translate_pos(x, y) + + self.node = OnscreenImage( + image=image, parent=parent, pos=self._initial_pos, + scale=(self._width / 2.0, 1, self._height / 2.0)) + + if transparent: + self.node.set_transparency(TransparencyAttrib.M_alpha) + + tex = self.node.get_texture() + + # Apply a near filter, but only if the parent has no scale, otherwise + # it will look weird + if near_filter and any_filter and parent.get_sx() == 1.0: + tex.set_minfilter(SamplerState.FT_nearest) + tex.set_magfilter(SamplerState.FT_nearest) + + if any_filter: + tex.set_anisotropic_degree(8) + tex.set_wrap_u(SamplerState.WM_clamp) + tex.set_wrap_v(SamplerState.WM_clamp) + + def get_initial_pos(self): + """ Returns the initial position of the image. This can be used for + animations """ + return self._initial_pos + + def pos_interval(self, *args, **kwargs): + """ Returns a pos interval, this is a wrapper around + NodePath.posInterval """ + return self.node.posInterval(*args, **kwargs) + + def hpr_interval(self, *args, **kwargs): + """ Returns a hpr interval, this is a wrapper around + NodePath.hprInterval """ + return self.node.hprInterval(*args, **kwargs) + + def color_scale_interval(self, *args, **kwargs): + """ Returns a color scale interval, this is a wrapper around + NodePath.colorScaleInterval """ + return self.node.colorScaleInterval(*args, **kwargs) + + def set_image(self, img): + """ Sets the current image """ + self.node.set_image(img) + + def get_width(self): + """ Returns the width of the image in pixels """ + return self._width + + def get_height(self): + """ Returns the height of the image in pixels """ + return self._height + + def set_pos(self, x, y): + """ Sets the position """ + self.node.set_pos(self._translate_pos(x, y)) + + def _translate_pos(self, x, y): + """ Converts 2d coordinates to pandas coordinate system """ + return Vec3(x + self._width / 2.0, 1, -y - self._height / 2.0) + + def set_shader(self, shader): + """ Sets a shader to be used for rendering the image """ + self.node.set_shader(shader) + + def set_shader_input(self, *args): + """ Sets a shader input on the image """ + self.node.set_shader_input(*args) + + def set_shader_inputs(self, **kwargs): + """ Sets multiple shader inputs on the image """ + self.node.set_shader_inputs(**kwargs) + + def remove(self): + """ Removes the image """ + self.node.remove() + + def hide(self): + """ Hides the image """ + self.node.hide() + + def show(self): + """ Shows the image if it was previously hidden """ + self.node.show() + + def is_hidden(self): + """ Returns whether the image is hidden """ + return self.node.is_hidden() diff --git a/RenderPipelineFile/rpcore/gui/text.py b/RenderPipelineFile/rpcore/gui/text.py index a41a706b..e74b0962 100644 --- a/RenderPipelineFile/rpcore/gui/text.py +++ b/RenderPipelineFile/rpcore/gui/text.py @@ -1,83 +1,83 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import Vec2, Vec3, TextNode, Vec4 -from direct.gui.OnscreenText import OnscreenText - -from rpcore.globals import Globals -from rpcore.rpobject import RPObject - - -class Text(RPObject): - - """ Simple wrapper around OnscreenText, providing a simpler interface """ - - def __init__(self, text="", parent=None, x=0, y=0, size=10, align="left", - color=None, may_change=False, font=None): - """ Constructs a new text. The parameters are almost equal to the - parameters of OnscreenText """ - RPObject.__init__(self) - - if color is None: - color = Vec3(1) - - align_mode = TextNode.A_left - - if align == "center": - align_mode = TextNode.A_center - elif align == "right": - align_mode = TextNode.A_right - - if font is None: - font = Globals.font - # Should always have a global font. Never use the default panda font! - assert font - - self._initial_pos = Vec2(x, -y) - self._node = OnscreenText( - text=text, parent=parent, pos=self._initial_pos, scale=size, - align=align_mode, fg=Vec4(color.x, color.y, color.z, 1.0), - font=font, mayChange=may_change) - - @property - def node(self): - """ Returns a handle to the internlally used node """ - return self._node - - def set_text(self, text): - """ Changes the text, remember to pass may_change to the constructor, - otherwise this method does not work. """ - self._node["text"] = text - - def get_initial_pos(self): - """ Returns the initial position of the text. This can be used for - animations """ - return self._initial_pos - - def pos_interval(self, *args, **kwargs): - """ Returns a pos interval, this is a wrapper around - NodePath.pos_interval """ - return self._node.pos_interval(*args, **kwargs) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import Vec2, Vec3, TextNode, Vec4 +from direct.gui.OnscreenText import OnscreenText + +from rpcore.globals import Globals +from rpcore.rpobject import RPObject + + +class Text(RPObject): + + """ Simple wrapper around OnscreenText, providing a simpler interface """ + + def __init__(self, text="", parent=None, x=0, y=0, size=10, align="left", + color=None, may_change=False, font=None): + """ Constructs a new text. The parameters are almost equal to the + parameters of OnscreenText """ + RPObject.__init__(self) + + if color is None: + color = Vec3(1) + + align_mode = TextNode.A_left + + if align == "center": + align_mode = TextNode.A_center + elif align == "right": + align_mode = TextNode.A_right + + if font is None: + font = Globals.font + # Should always have a global font. Never use the default panda font! + assert font + + self._initial_pos = Vec2(x, -y) + self._node = OnscreenText( + text=text, parent=parent, pos=self._initial_pos, scale=size, + align=align_mode, fg=Vec4(color.x, color.y, color.z, 1.0), + font=font, mayChange=may_change) + + @property + def node(self): + """ Returns a handle to the internlally used node """ + return self._node + + def set_text(self, text): + """ Changes the text, remember to pass may_change to the constructor, + otherwise this method does not work. """ + self._node["text"] = text + + def get_initial_pos(self): + """ Returns the initial position of the text. This can be used for + animations """ + return self._initial_pos + + def pos_interval(self, *args, **kwargs): + """ Returns a pos interval, this is a wrapper around + NodePath.pos_interval """ + return self._node.pos_interval(*args, **kwargs) diff --git a/RenderPipelineFile/rpcore/gui/text_node.py b/RenderPipelineFile/rpcore/gui/text_node.py index caae4822..f541ef48 100644 --- a/RenderPipelineFile/rpcore/gui/text_node.py +++ b/RenderPipelineFile/rpcore/gui/text_node.py @@ -1,87 +1,87 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - - -from panda3d.core import Vec2, Vec3, Vec4 -from panda3d.core import TextNode as TextNodeImpl - -from rpcore.globals import Globals -from rpcore.rpobject import RPObject -from rpcore.loader import RPLoader - - -class TextNode(RPObject): - - """ Interface for the Panda3D TextNode. """ - - def __init__(self, font="/$$rp/data/font/Roboto-Bold.ttf", pixel_size=16, align="left", - pos=Vec2(0), color=Vec3(1), parent=None): - """ Constructs a new text node, forwaring the parameters to the internal - panda3d implementation """ - RPObject.__init__(self) - self._node = TextNodeImpl('FTN') - self._node.set_text("") - self._node.set_align(getattr(TextNodeImpl, "A_" + align)) - self._node.set_text_color(color.x, color.y, color.z, 1) - - if parent is None: - parent = Globals.base.aspect2d - - self._nodepath = parent.attach_new_node(self._node) - self._nodepath.set_pos(pos.x, 0, pos.y) - - font = RPLoader.load_font(font) - # font.set_outline(Vec4(0, 0, 0, 0.78), 1.6, 0.37) - font.set_outline(Vec4(0, 0, 0, 1), 1.6, 0.37) - font.set_scale_factor(1.0) - font.set_texture_margin(int(pixel_size / 4.0 * 2.0)) - font.set_bg(Vec4(0, 0, 0, 0)) - self._node.set_font(font) - self.set_pixel_size(pixel_size) - - @property - def text(self): - """ Returns the current text """ - return self._node.get_text() - - @text.setter - def text(self, text): - """ Sets the current text """ - self._node.set_text(text) - - @property - def color(self): - """ Returns the current text color """ - return self._node.get_text_color() - - @color.setter - def color(self, val): - """ Sets the current text color """ - self._node.set_text_color(val) - - def set_pixel_size(self, size): - """ Sets the text size in pixels """ - self._nodepath.set_scale(size * 2.0 / float(Globals.native_resolution.y)) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + + +from panda3d.core import Vec2, Vec3, Vec4 +from panda3d.core import TextNode as TextNodeImpl + +from rpcore.globals import Globals +from rpcore.rpobject import RPObject +from rpcore.loader import RPLoader + + +class TextNode(RPObject): + + """ Interface for the Panda3D TextNode. """ + + def __init__(self, font="/$$rp/data/font/Roboto-Bold.ttf", pixel_size=16, align="left", + pos=Vec2(0), color=Vec3(1), parent=None): + """ Constructs a new text node, forwaring the parameters to the internal + panda3d implementation """ + RPObject.__init__(self) + self._node = TextNodeImpl('FTN') + self._node.set_text("") + self._node.set_align(getattr(TextNodeImpl, "A_" + align)) + self._node.set_text_color(color.x, color.y, color.z, 1) + + if parent is None: + parent = Globals.base.aspect2d + + self._nodepath = parent.attach_new_node(self._node) + self._nodepath.set_pos(pos.x, 0, pos.y) + + font = RPLoader.load_font(font) + # font.set_outline(Vec4(0, 0, 0, 0.78), 1.6, 0.37) + font.set_outline(Vec4(0, 0, 0, 1), 1.6, 0.37) + font.set_scale_factor(1.0) + font.set_texture_margin(int(pixel_size / 4.0 * 2.0)) + font.set_bg(Vec4(0, 0, 0, 0)) + self._node.set_font(font) + self.set_pixel_size(pixel_size) + + @property + def text(self): + """ Returns the current text """ + return self._node.get_text() + + @text.setter + def text(self, text): + """ Sets the current text """ + self._node.set_text(text) + + @property + def color(self): + """ Returns the current text color """ + return self._node.get_text_color() + + @color.setter + def color(self, val): + """ Sets the current text color """ + self._node.set_text_color(val) + + def set_pixel_size(self, size): + """ Sets the text size in pixels """ + self._nodepath.set_scale(size * 2.0 / float(Globals.native_resolution.y)) diff --git a/RenderPipelineFile/rpcore/gui/texture_preview.py b/RenderPipelineFile/rpcore/gui/texture_preview.py index 52013e91..ca38d426 100644 --- a/RenderPipelineFile/rpcore/gui/texture_preview.py +++ b/RenderPipelineFile/rpcore/gui/texture_preview.py @@ -1,182 +1,182 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import Vec3 - -from rpcore.image import Image -from rpcore.gui.draggable_window import DraggableWindow -from rpcore.gui.sprite import Sprite -from rpcore.gui.text import Text -from rpcore.gui.slider import Slider -from rpcore.gui.labeled_checkbox import LabeledCheckbox - -from rpcore.util.display_shader_builder import DisplayShaderBuilder - - -class TexturePreview(DraggableWindow): - - """ Small window which provides a preview of a texture """ - def __init__(self, pipeline, parent): - DraggableWindow.__init__(self, width=1600, height=900, parent=parent, - title="Texture Viewer") - self._pipeline = pipeline - self._current_tex = None - self._mip_slider = None - self._mip_text = None - self._slice_slider = None - self._slice_text = None - self._preview_image = None - self._create_components() - - def present(self, tex): - """ "Presents" a given texture and shows the window """ - self._current_tex = tex - - self.set_title(tex.get_name()) - - # tex.write(tex.get_name() + ".png") - - # Remove old content - self._content_node.node().remove_all_children() - - w, h = tex.get_x_size(), tex.get_y_size() - if h > 1: - scale_x = (self._width - 40.0) / w - scale_y = (self._height - 110.0) / h - scale_f = min(scale_x, scale_y) - display_w = scale_f * w - display_h = scale_f * h - - else: - display_w = self._width - 40 - display_h = self._height - 110 - - image = Sprite( - image=tex, parent=self._content_node, x=20, y=90, w=display_w, - h=display_h, any_filter=False, transparent=False) - description = "" - - # Image size - description += "{:d} x {:d} x {:d}".format( - tex.get_x_size(), tex.get_y_size(), tex.get_z_size()) - - # Image type - description += ", {:s}, {:s}".format( - Image.format_format(tex.get_format()).upper(), - Image.format_component_type(tex.get_component_type()).upper()) - - Text(text=description, parent=self._content_node, x=17, y=70, - size=16, color=Vec3(0.6, 0.6, 0.6)) - - estimated_bytes = tex.estimate_texture_memory() - size_desc = "Estimated memory: {:2.2f} MB".format( - estimated_bytes / (1024.0 ** 2)) - - Text(text=size_desc, parent=self._content_node, x=self._width - 20.0, - y=70, size=18, color=Vec3(0.34, 0.564, 0.192), align="right") - - x_pos = len(size_desc) * 9 + 140 - - # Slider for viewing different mipmaps - if tex.uses_mipmaps(): - max_mips = tex.get_expected_num_mipmap_levels() - 1 - self._mip_slider = Slider( - parent=self._content_node, size=140, min_value=0, max_value=max_mips, - callback=self._set_mip, x=x_pos, y=65, value=0) - x_pos += 140 + 5 - - self._mip_text = Text( - text="MIP: 5", parent=self._content_node, x=x_pos, y=72, size=18, - color=Vec3(1, 0.4, 0.4), may_change=1) - x_pos += 50 + 30 - - # Slider for viewing different Z-layers - if tex.get_z_size() > 1: - self._slice_slider = Slider( - parent=self._content_node, size=250, min_value=0, - max_value=tex.get_z_size() - 1, callback=self._set_slice, x=x_pos, - y=65, value=0) - x_pos += 250 + 5 - - self._slice_text = Text( - text="Z: 5", parent=self._content_node, x=x_pos, y=72, size=18, - color=Vec3(0.4, 1, 0.4), may_change=1) - - x_pos += 50 + 30 - - # Slider to adjust brightness - self._bright_slider = Slider( - parent=self._content_node, size=140, min_value=-14, max_value=14, - callback=self._set_brightness, x=x_pos, y=65, value=0) - x_pos += 140 + 5 - self._bright_text = Text( - text="Bright: 1", parent=self._content_node, x=x_pos, y=72, size=18, - color=Vec3(0.4, 0.4, 1), may_change=1) - x_pos += 100 + 30 - - # Slider to enable reinhard tonemapping - self._tonemap_box = LabeledCheckbox( - parent=self._content_node, x=x_pos, y=60, text="Tonemap", - text_color=Vec3(1, 0.4, 0.4), chb_checked=False, - chb_callback=self._set_enable_tonemap, - text_size=18, expand_width=90) - x_pos += 90 + 30 - - image.set_shader_inputs( - slice=0, - mipmap=0, - brightness=1, - tonemap=False) - - preview_shader = DisplayShaderBuilder.build(tex, display_w, display_h) - image.set_shader(preview_shader) - - self._preview_image = image - self.show() - - def _set_slice(self): - idx = int(self._slice_slider.value) - self._preview_image.set_shader_input("slice", idx) - self._slice_text.set_text("Z: " + str(idx)) - - def _set_mip(self): - idx = int(self._mip_slider.value) - self._preview_image.set_shader_input("mipmap", idx) - self._mip_text.set_text("MIP " + str(idx)) - - def _set_brightness(self): - val = self._bright_slider.value - scale = 2 ** val - self._bright_text.set_text("Bright: " + str(round(scale, 3))) - self._preview_image.set_shader_input("brightness", scale) - - def _set_enable_tonemap(self, enable_tonemap): - self._preview_image.set_shader_input("tonemap", enable_tonemap) - - def _create_components(self): - """ Internal method to init the components """ - DraggableWindow._create_components(self) - self._content_node = self._node.attach_new_node("content") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import Vec3 + +from rpcore.image import Image +from rpcore.gui.draggable_window import DraggableWindow +from rpcore.gui.sprite import Sprite +from rpcore.gui.text import Text +from rpcore.gui.slider import Slider +from rpcore.gui.labeled_checkbox import LabeledCheckbox + +from rpcore.util.display_shader_builder import DisplayShaderBuilder + + +class TexturePreview(DraggableWindow): + + """ Small window which provides a preview of a texture """ + def __init__(self, pipeline, parent): + DraggableWindow.__init__(self, width=1600, height=900, parent=parent, + title="Texture Viewer") + self._pipeline = pipeline + self._current_tex = None + self._mip_slider = None + self._mip_text = None + self._slice_slider = None + self._slice_text = None + self._preview_image = None + self._create_components() + + def present(self, tex): + """ "Presents" a given texture and shows the window """ + self._current_tex = tex + + self.set_title(tex.get_name()) + + # tex.write(tex.get_name() + ".png") + + # Remove old content + self._content_node.node().remove_all_children() + + w, h = tex.get_x_size(), tex.get_y_size() + if h > 1: + scale_x = (self._width - 40.0) / w + scale_y = (self._height - 110.0) / h + scale_f = min(scale_x, scale_y) + display_w = scale_f * w + display_h = scale_f * h + + else: + display_w = self._width - 40 + display_h = self._height - 110 + + image = Sprite( + image=tex, parent=self._content_node, x=20, y=90, w=display_w, + h=display_h, any_filter=False, transparent=False) + description = "" + + # Image size + description += "{:d} x {:d} x {:d}".format( + tex.get_x_size(), tex.get_y_size(), tex.get_z_size()) + + # Image type + description += ", {:s}, {:s}".format( + Image.format_format(tex.get_format()).upper(), + Image.format_component_type(tex.get_component_type()).upper()) + + Text(text=description, parent=self._content_node, x=17, y=70, + size=16, color=Vec3(0.6, 0.6, 0.6)) + + estimated_bytes = tex.estimate_texture_memory() + size_desc = "Estimated memory: {:2.2f} MB".format( + estimated_bytes / (1024.0 ** 2)) + + Text(text=size_desc, parent=self._content_node, x=self._width - 20.0, + y=70, size=18, color=Vec3(0.34, 0.564, 0.192), align="right") + + x_pos = len(size_desc) * 9 + 140 + + # Slider for viewing different mipmaps + if tex.uses_mipmaps(): + max_mips = tex.get_expected_num_mipmap_levels() - 1 + self._mip_slider = Slider( + parent=self._content_node, size=140, min_value=0, max_value=max_mips, + callback=self._set_mip, x=x_pos, y=65, value=0) + x_pos += 140 + 5 + + self._mip_text = Text( + text="MIP: 5", parent=self._content_node, x=x_pos, y=72, size=18, + color=Vec3(1, 0.4, 0.4), may_change=1) + x_pos += 50 + 30 + + # Slider for viewing different Z-layers + if tex.get_z_size() > 1: + self._slice_slider = Slider( + parent=self._content_node, size=250, min_value=0, + max_value=tex.get_z_size() - 1, callback=self._set_slice, x=x_pos, + y=65, value=0) + x_pos += 250 + 5 + + self._slice_text = Text( + text="Z: 5", parent=self._content_node, x=x_pos, y=72, size=18, + color=Vec3(0.4, 1, 0.4), may_change=1) + + x_pos += 50 + 30 + + # Slider to adjust brightness + self._bright_slider = Slider( + parent=self._content_node, size=140, min_value=-14, max_value=14, + callback=self._set_brightness, x=x_pos, y=65, value=0) + x_pos += 140 + 5 + self._bright_text = Text( + text="Bright: 1", parent=self._content_node, x=x_pos, y=72, size=18, + color=Vec3(0.4, 0.4, 1), may_change=1) + x_pos += 100 + 30 + + # Slider to enable reinhard tonemapping + self._tonemap_box = LabeledCheckbox( + parent=self._content_node, x=x_pos, y=60, text="Tonemap", + text_color=Vec3(1, 0.4, 0.4), chb_checked=False, + chb_callback=self._set_enable_tonemap, + text_size=18, expand_width=90) + x_pos += 90 + 30 + + image.set_shader_inputs( + slice=0, + mipmap=0, + brightness=1, + tonemap=False) + + preview_shader = DisplayShaderBuilder.build(tex, display_w, display_h) + image.set_shader(preview_shader) + + self._preview_image = image + self.show() + + def _set_slice(self): + idx = int(self._slice_slider.value) + self._preview_image.set_shader_input("slice", idx) + self._slice_text.set_text("Z: " + str(idx)) + + def _set_mip(self): + idx = int(self._mip_slider.value) + self._preview_image.set_shader_input("mipmap", idx) + self._mip_text.set_text("MIP " + str(idx)) + + def _set_brightness(self): + val = self._bright_slider.value + scale = 2 ** val + self._bright_text.set_text("Bright: " + str(round(scale, 3))) + self._preview_image.set_shader_input("brightness", scale) + + def _set_enable_tonemap(self, enable_tonemap): + self._preview_image.set_shader_input("tonemap", enable_tonemap) + + def _create_components(self): + """ Internal method to init the components """ + DraggableWindow._create_components(self) + self._content_node = self._node.attach_new_node("content") diff --git a/RenderPipelineFile/rpcore/image.py b/RenderPipelineFile/rpcore/image.py index 5910fc0a..59679d95 100644 --- a/RenderPipelineFile/rpcore/image.py +++ b/RenderPipelineFile/rpcore/image.py @@ -1,158 +1,158 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import Texture, GeomEnums - -from rpcore.rpobject import RPObject -from rpcore.globals import Globals -from rpcore.render_target import RenderTarget - - -class ImageFormatTypes(object): # pylint: disable=too-few-public-methods - - """ This is a small helper class to prevent pylint errors about the Image - class not defining the enums. This class just copies the enum properties - from panda3d's builtin Texture class. """ - - T_float = Texture.T_float - T_unsigned_byte = Texture.T_unsigned_byte - T_int = Texture.T_int - T_unsigned_short = Texture.T_unsigned_short - T_unsigned_int_24_8 = Texture.T_unsigned_int_24_8 - - TT_buffer_texture = Texture.TT_buffer_texture - TT_2d_texture = Texture.TT_2d_texture - TT_3d_texture = Texture.TT_3d_texture - TT_cube_map = Texture.TT_cube_map - TT_cube_map_array = Texture.TT_cube_map_array - TT_2d_texture_array = Texture.TT_2d_texture_array - - format_format = Texture.format_format - format_component_type = Texture.format_component_type - - -class Image(RPObject, Texture, ImageFormatTypes): - - """ This is a wrapper arround the Texture class from Panda3D, which keeps - track of all images and registers / unregisters them aswell as counting - the memory used. This is used by all classes instead of pandas builtin - Texture class. """ - - # All registered images - REGISTERED_IMAGES = [] - - # String formats - FORMAT_MAPPINGS = { - "R11G11B10": (Texture.T_float, Texture.F_r11_g11_b10), - "RGBA8": (Texture.T_unsigned_byte, Texture.F_rgba8), - "RGBA16": (Texture.T_float, Texture.F_rgba16), - "RGBA32": (Texture.T_float, Texture.F_rgba32), - "R8": (Texture.T_unsigned_byte, Texture.F_red), - "R8UI": (Texture.T_unsigned_byte, Texture.F_red), - "R16": (Texture.T_float, Texture.F_r16), - "R16UI": (Texture.T_unsigned_short, Texture.F_r16i), - "R32": (Texture.T_float, Texture.F_r32), - "R32I": (Texture.T_int, Texture.F_r32i), - } - - @classmethod - def create_buffer(cls, name, size, component_format): - """ Creates a new buffer texture """ - img = cls("ImgBuffer-" + name) - comp_type, comp_format = cls.convert_texture_format(component_format) - img.setup_buffer_texture(size, comp_type, comp_format, GeomEnums.UH_static) - return img - - @classmethod - def create_counter(cls, name): - """ Creates a new 1x1 R32I texture to be used as an atomic counter """ - return cls.create_buffer(name, 1, "R32I") - - @classmethod - def create_2d(cls, name, w, h, component_format): - """ Creates a new 2D texture """ - img = cls("Img2D-" + name) - comp_type, comp_format = cls.convert_texture_format(component_format) - img.setup_2d_texture(w, h, comp_type, comp_format) - return img - - @classmethod - def create_2d_array(cls, name, w, h, slices, component_format): - """ Creates a new 2D-array texture """ - img = cls("Img2DArr-" + name) - comp_type, comp_format = cls.convert_texture_format(component_format) - img.setup_2d_texture_array(w, h, slices, comp_type, comp_format) - return img - - @classmethod - def create_3d(cls, name, w, h, slices, component_format): - """ Creates a new 3D texture """ - img = cls("Img3D-" + name) - comp_type, comp_format = cls.convert_texture_format(component_format) - img.setup_3d_texture(w, h, slices, comp_type, comp_format) - return img - - @classmethod - def create_cube(cls, name, size, component_format): - """ Creates a new cubemap """ - img = cls("ImgCube-" + name) - comp_type, comp_format = cls.convert_texture_format(component_format) - img.setup_cube_map(size, comp_type, comp_format) - return img - - @classmethod - def create_cube_array(cls, name, size, num_cubemaps, component_format): - """ Creates a new cubemap """ - img = cls("ImgCubeArr-" + name) - comp_type, comp_format = cls.convert_texture_format(component_format) - img.setup_cube_map_array(size, num_cubemaps, comp_type, comp_format) - return img - - @classmethod - def convert_texture_format(cls, comp_type): - """ Converts a string like 'RGBA8' to a texture type and format """ - return cls.FORMAT_MAPPINGS[comp_type] - - def __init__(self, name): - """ Internal method to create a new image """ - RPObject.__init__(self, name) - Texture.__init__(self, name) - Image.REGISTERED_IMAGES.append(self) - self.set_clear_color(0) - self.clear_image() - self.sort = RenderTarget.CURRENT_SORT - - def __del__(self): - """ Destroys the image """ - self.warn("Image destructor not implemented yet") - - def write(self, pth): - """ Writes the image to disk """ - Globals.base.graphicsEngine.extract_texture_data(self, Globals.base.win.gsg) - if self.get_texture_type() in [Texture.TT_3d_texture, Texture.TT_cube_map]: - Texture.write(self, "#_" + pth, 0, 0, True, False) - else: - Texture.write(self, pth) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import Texture, GeomEnums + +from rpcore.rpobject import RPObject +from rpcore.globals import Globals +from rpcore.render_target import RenderTarget + + +class ImageFormatTypes(object): # pylint: disable=too-few-public-methods + + """ This is a small helper class to prevent pylint errors about the Image + class not defining the enums. This class just copies the enum properties + from panda3d's builtin Texture class. """ + + T_float = Texture.T_float + T_unsigned_byte = Texture.T_unsigned_byte + T_int = Texture.T_int + T_unsigned_short = Texture.T_unsigned_short + T_unsigned_int_24_8 = Texture.T_unsigned_int_24_8 + + TT_buffer_texture = Texture.TT_buffer_texture + TT_2d_texture = Texture.TT_2d_texture + TT_3d_texture = Texture.TT_3d_texture + TT_cube_map = Texture.TT_cube_map + TT_cube_map_array = Texture.TT_cube_map_array + TT_2d_texture_array = Texture.TT_2d_texture_array + + format_format = Texture.format_format + format_component_type = Texture.format_component_type + + +class Image(RPObject, Texture, ImageFormatTypes): + + """ This is a wrapper arround the Texture class from Panda3D, which keeps + track of all images and registers / unregisters them aswell as counting + the memory used. This is used by all classes instead of pandas builtin + Texture class. """ + + # All registered images + REGISTERED_IMAGES = [] + + # String formats + FORMAT_MAPPINGS = { + "R11G11B10": (Texture.T_float, Texture.F_r11_g11_b10), + "RGBA8": (Texture.T_unsigned_byte, Texture.F_rgba8), + "RGBA16": (Texture.T_float, Texture.F_rgba16), + "RGBA32": (Texture.T_float, Texture.F_rgba32), + "R8": (Texture.T_unsigned_byte, Texture.F_red), + "R8UI": (Texture.T_unsigned_byte, Texture.F_red), + "R16": (Texture.T_float, Texture.F_r16), + "R16UI": (Texture.T_unsigned_short, Texture.F_r16i), + "R32": (Texture.T_float, Texture.F_r32), + "R32I": (Texture.T_int, Texture.F_r32i), + } + + @classmethod + def create_buffer(cls, name, size, component_format): + """ Creates a new buffer texture """ + img = cls("ImgBuffer-" + name) + comp_type, comp_format = cls.convert_texture_format(component_format) + img.setup_buffer_texture(size, comp_type, comp_format, GeomEnums.UH_static) + return img + + @classmethod + def create_counter(cls, name): + """ Creates a new 1x1 R32I texture to be used as an atomic counter """ + return cls.create_buffer(name, 1, "R32I") + + @classmethod + def create_2d(cls, name, w, h, component_format): + """ Creates a new 2D texture """ + img = cls("Img2D-" + name) + comp_type, comp_format = cls.convert_texture_format(component_format) + img.setup_2d_texture(w, h, comp_type, comp_format) + return img + + @classmethod + def create_2d_array(cls, name, w, h, slices, component_format): + """ Creates a new 2D-array texture """ + img = cls("Img2DArr-" + name) + comp_type, comp_format = cls.convert_texture_format(component_format) + img.setup_2d_texture_array(w, h, slices, comp_type, comp_format) + return img + + @classmethod + def create_3d(cls, name, w, h, slices, component_format): + """ Creates a new 3D texture """ + img = cls("Img3D-" + name) + comp_type, comp_format = cls.convert_texture_format(component_format) + img.setup_3d_texture(w, h, slices, comp_type, comp_format) + return img + + @classmethod + def create_cube(cls, name, size, component_format): + """ Creates a new cubemap """ + img = cls("ImgCube-" + name) + comp_type, comp_format = cls.convert_texture_format(component_format) + img.setup_cube_map(size, comp_type, comp_format) + return img + + @classmethod + def create_cube_array(cls, name, size, num_cubemaps, component_format): + """ Creates a new cubemap """ + img = cls("ImgCubeArr-" + name) + comp_type, comp_format = cls.convert_texture_format(component_format) + img.setup_cube_map_array(size, num_cubemaps, comp_type, comp_format) + return img + + @classmethod + def convert_texture_format(cls, comp_type): + """ Converts a string like 'RGBA8' to a texture type and format """ + return cls.FORMAT_MAPPINGS[comp_type] + + def __init__(self, name): + """ Internal method to create a new image """ + RPObject.__init__(self, name) + Texture.__init__(self, name) + Image.REGISTERED_IMAGES.append(self) + self.set_clear_color(0) + self.clear_image() + self.sort = RenderTarget.CURRENT_SORT + + def __del__(self): + """ Destroys the image """ + self.warn("Image destructor not implemented yet") + + def write(self, pth): + """ Writes the image to disk """ + Globals.base.graphicsEngine.extract_texture_data(self, Globals.base.win.gsg) + if self.get_texture_type() in [Texture.TT_3d_texture, Texture.TT_cube_map]: + Texture.write(self, "#_" + pth, 0, 0, True, False) + else: + Texture.write(self, pth) diff --git a/RenderPipelineFile/rpcore/light_manager.py b/RenderPipelineFile/rpcore/light_manager.py index e336c3d7..df5609c3 100644 --- a/RenderPipelineFile/rpcore/light_manager.py +++ b/RenderPipelineFile/rpcore/light_manager.py @@ -1,210 +1,210 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -import math - -from panda3d.core import LVecBase2i, PTAInt - -from rpcore.globals import Globals -from rpcore.gpu_command_queue import GPUCommandQueue -from rpcore.image import Image -from rpcore.native import InternalLightManager, PointLight, ShadowManager -from rpcore.rpobject import RPObject - -from rpcore.stages.apply_lights_stage import ApplyLightsStage -from rpcore.stages.collect_used_cells_stage import CollectUsedCellsStage -from rpcore.stages.cull_lights_stage import CullLightsStage -from rpcore.stages.flag_used_cells_stage import FlagUsedCellsStage -from rpcore.stages.shadow_stage import ShadowStage - - -class LightManager(RPObject): - - """ This class is a wrapper around the InternalLightManager, and provides - additional functionality like setting up all required stages and defines.""" - - # Maximum amount of lights, has to match the definitions in the native code - MAX_LIGHTS = 65535 - - # Maximum amount of shadow sources - MAX_SOURCES = 2048 - - def __init__(self, pipeline): - """ Constructs the light manager """ - RPObject.__init__(self) - self.pipeline = pipeline - self.compute_tile_size() - self.init_internal_manager() - self.init_command_queue() - self.init_shadow_manager() - self.init_stages() - - @property - def total_tiles(self): - """ Returns the total amount of tiles """ - return self.num_tiles.x * self.num_tiles.y * \ - self.pipeline.settings["lighting.culling_grid_slices"] - - @property - def num_lights(self): - """ Returns the amount of stored lights """ - return self.internal_mgr.num_lights - - @property - def num_shadow_sources(self): - """ Returns the amount of stored shadow sources """ - return self.internal_mgr.num_shadow_sources - - @property - def shadow_atlas_coverage(self): - """ Returns the shadow atlas coverage in percentage """ - return self.internal_mgr.shadow_manager.atlas.coverage * 100.0 - - def add_light(self, light): - """ Adds a new light """ - self.internal_mgr.add_light(light) - self.pta_max_light_index[0] = self.internal_mgr.max_light_index - - def remove_light(self, light): - """ Removes a light """ - # from RenderPipelineFile.rpcore.pynative.internal_light_manager import InternalLightManager - # inter = InternalLightManager() - # inter.remove_light(light) - self.internal_mgr.remove_light(light) - self.pta_max_light_index[0] = self.internal_mgr.max_light_index - - def update(self): - """ Main update method to process the GPU commands """ - self.internal_mgr.set_camera_pos( - Globals.base.camera.get_pos(Globals.base.render)) - self.internal_mgr.update() - self.shadow_manager.update() - self.cmd_queue.process_queue() - - def reload_shaders(self): - """ Reloads all assigned shaders """ - self.cmd_queue.reload_shaders() - - def compute_tile_size(self): - """ Computes how many tiles there are on screen """ - self.tile_size = LVecBase2i( - self.pipeline.settings["lighting.culling_grid_size_x"], - self.pipeline.settings["lighting.culling_grid_size_y"]) - num_tiles_x = int(math.ceil(Globals.resolution.x / float(self.tile_size.x))) - num_tiles_y = int(math.ceil(Globals.resolution.y / float(self.tile_size.y))) - self.debug("Tile size =", self.tile_size.x, "x", self.tile_size.y, - ", Num tiles =", num_tiles_x, "x", num_tiles_y) - self.num_tiles = LVecBase2i(num_tiles_x, num_tiles_y) - - def init_command_queue(self): - """ Inits the command queue """ - self.cmd_queue = GPUCommandQueue(self.pipeline) - self.cmd_queue.register_input("LightData", self.img_light_data) - self.cmd_queue.register_input("SourceData", self.img_source_data) - self.internal_mgr.set_command_list(self.cmd_queue.command_list) - - def init_shadow_manager(self): - """ Inits the shadow manager """ - self.shadow_manager = ShadowManager() - self.shadow_manager.set_max_updates(self.pipeline.settings["shadows.max_updates"]) - self.shadow_manager.set_scene(Globals.base.render) - self.shadow_manager.set_tag_state_manager(self.pipeline.tag_mgr) - self.shadow_manager.atlas_size = self.pipeline.settings["shadows.atlas_size"] - self.internal_mgr.shadow_manager = self.shadow_manager - - def init_shadows(self): - """ Inits the shadows, this has to get called after the stages were - created, because we need the GraphicsOutput of the shadow atlas, which - is not available earlier """ - self.shadow_manager.set_atlas_graphics_output(self.shadow_stage.atlas_buffer) - self.shadow_manager.init() - - def init_internal_manager(self): - """ Creates the light storage manager and the buffer to store the light data """ - self.internal_mgr = InternalLightManager() - self.internal_mgr.set_shadow_update_distance( - self.pipeline.settings["shadows.max_update_distance"]) - - # Storage for the Lights - per_light_vec4s = 4 - self.img_light_data = Image.create_buffer( - "LightData", self.MAX_LIGHTS * per_light_vec4s, "RGBA16") - self.img_light_data.clear_image() - - self.pta_max_light_index = PTAInt.empty_array(1) - self.pta_max_light_index[0] = 0 - - # Storage for the shadow sources - per_source_vec4s = 5 - - # IMPORTANT: RGBA32 is really required here. Otherwise artifacts and bad - # shadow filtering occur due to precision issues - self.img_source_data = Image.create_buffer( - "ShadowSourceData", self.MAX_SOURCES * per_source_vec4s, "RGBA32") - self.img_light_data.clear_image() - - # Register the buffer - inputs = self.pipeline.stage_mgr.inputs - inputs["AllLightsData"] = self.img_light_data - inputs["ShadowSourceData"] = self.img_source_data - inputs["maxLightIndex"] = self.pta_max_light_index - - def init_stages(self): - """ Inits all required stages for the lighting """ - - add_stage = self.pipeline.stage_mgr.add_stage - - self.flag_cells_stage = FlagUsedCellsStage(self.pipeline) - add_stage(self.flag_cells_stage) - - self.collect_cells_stage = CollectUsedCellsStage(self.pipeline) - add_stage(self.collect_cells_stage) - - self.cull_lights_stage = CullLightsStage(self.pipeline) - add_stage(self.cull_lights_stage) - - self.apply_lights_stage = ApplyLightsStage(self.pipeline) - add_stage(self.apply_lights_stage) - - self.shadow_stage = ShadowStage(self.pipeline) - self.shadow_stage.size = self.shadow_manager.get_atlas_size() - add_stage(self.shadow_stage) - - def init_defines(self): - """ Inits the common defines """ - defines = self.pipeline.stage_mgr.defines - defines["LC_TILE_SIZE_X"] = self.tile_size.x - defines["LC_TILE_SIZE_Y"] = self.tile_size.y - defines["LC_TILE_SLICES"] = self.pipeline.settings["lighting.culling_grid_slices"] - defines["LC_MAX_DISTANCE"] = self.pipeline.settings["lighting.culling_max_distance"] - defines["LC_CULLING_SLICE_WIDTH"] = self.pipeline.settings["lighting.culling_slice_width"] - defines["LC_MAX_LIGHTS_PER_CELL"] = self.pipeline.settings["lighting.max_lights_per_cell"] - defines["SHADOW_ATLAS_SIZE"] = self.pipeline.settings["shadows.atlas_size"] - - # Register all light types as defines - for attr in dir(PointLight): - if attr.startswith("LT_"): - defines[attr.upper()] = getattr(PointLight, attr) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +import math + +from panda3d.core import LVecBase2i, PTAInt + +from rpcore.globals import Globals +from rpcore.gpu_command_queue import GPUCommandQueue +from rpcore.image import Image +from rpcore.native import InternalLightManager, PointLight, ShadowManager +from rpcore.rpobject import RPObject + +from rpcore.stages.apply_lights_stage import ApplyLightsStage +from rpcore.stages.collect_used_cells_stage import CollectUsedCellsStage +from rpcore.stages.cull_lights_stage import CullLightsStage +from rpcore.stages.flag_used_cells_stage import FlagUsedCellsStage +from rpcore.stages.shadow_stage import ShadowStage + + +class LightManager(RPObject): + + """ This class is a wrapper around the InternalLightManager, and provides + additional functionality like setting up all required stages and defines.""" + + # Maximum amount of lights, has to match the definitions in the native code + MAX_LIGHTS = 65535 + + # Maximum amount of shadow sources + MAX_SOURCES = 2048 + + def __init__(self, pipeline): + """ Constructs the light manager """ + RPObject.__init__(self) + self.pipeline = pipeline + self.compute_tile_size() + self.init_internal_manager() + self.init_command_queue() + self.init_shadow_manager() + self.init_stages() + + @property + def total_tiles(self): + """ Returns the total amount of tiles """ + return self.num_tiles.x * self.num_tiles.y * \ + self.pipeline.settings["lighting.culling_grid_slices"] + + @property + def num_lights(self): + """ Returns the amount of stored lights """ + return self.internal_mgr.num_lights + + @property + def num_shadow_sources(self): + """ Returns the amount of stored shadow sources """ + return self.internal_mgr.num_shadow_sources + + @property + def shadow_atlas_coverage(self): + """ Returns the shadow atlas coverage in percentage """ + return self.internal_mgr.shadow_manager.atlas.coverage * 100.0 + + def add_light(self, light): + """ Adds a new light """ + self.internal_mgr.add_light(light) + self.pta_max_light_index[0] = self.internal_mgr.max_light_index + + def remove_light(self, light): + """ Removes a light """ + # from RenderPipelineFile.rpcore.pynative.internal_light_manager import InternalLightManager + # inter = InternalLightManager() + # inter.remove_light(light) + self.internal_mgr.remove_light(light) + self.pta_max_light_index[0] = self.internal_mgr.max_light_index + + def update(self): + """ Main update method to process the GPU commands """ + self.internal_mgr.set_camera_pos( + Globals.base.camera.get_pos(Globals.base.render)) + self.internal_mgr.update() + self.shadow_manager.update() + self.cmd_queue.process_queue() + + def reload_shaders(self): + """ Reloads all assigned shaders """ + self.cmd_queue.reload_shaders() + + def compute_tile_size(self): + """ Computes how many tiles there are on screen """ + self.tile_size = LVecBase2i( + self.pipeline.settings["lighting.culling_grid_size_x"], + self.pipeline.settings["lighting.culling_grid_size_y"]) + num_tiles_x = int(math.ceil(Globals.resolution.x / float(self.tile_size.x))) + num_tiles_y = int(math.ceil(Globals.resolution.y / float(self.tile_size.y))) + self.debug("Tile size =", self.tile_size.x, "x", self.tile_size.y, + ", Num tiles =", num_tiles_x, "x", num_tiles_y) + self.num_tiles = LVecBase2i(num_tiles_x, num_tiles_y) + + def init_command_queue(self): + """ Inits the command queue """ + self.cmd_queue = GPUCommandQueue(self.pipeline) + self.cmd_queue.register_input("LightData", self.img_light_data) + self.cmd_queue.register_input("SourceData", self.img_source_data) + self.internal_mgr.set_command_list(self.cmd_queue.command_list) + + def init_shadow_manager(self): + """ Inits the shadow manager """ + self.shadow_manager = ShadowManager() + self.shadow_manager.set_max_updates(self.pipeline.settings["shadows.max_updates"]) + self.shadow_manager.set_scene(Globals.base.render) + self.shadow_manager.set_tag_state_manager(self.pipeline.tag_mgr) + self.shadow_manager.atlas_size = self.pipeline.settings["shadows.atlas_size"] + self.internal_mgr.shadow_manager = self.shadow_manager + + def init_shadows(self): + """ Inits the shadows, this has to get called after the stages were + created, because we need the GraphicsOutput of the shadow atlas, which + is not available earlier """ + self.shadow_manager.set_atlas_graphics_output(self.shadow_stage.atlas_buffer) + self.shadow_manager.init() + + def init_internal_manager(self): + """ Creates the light storage manager and the buffer to store the light data """ + self.internal_mgr = InternalLightManager() + self.internal_mgr.set_shadow_update_distance( + self.pipeline.settings["shadows.max_update_distance"]) + + # Storage for the Lights + per_light_vec4s = 4 + self.img_light_data = Image.create_buffer( + "LightData", self.MAX_LIGHTS * per_light_vec4s, "RGBA16") + self.img_light_data.clear_image() + + self.pta_max_light_index = PTAInt.empty_array(1) + self.pta_max_light_index[0] = 0 + + # Storage for the shadow sources + per_source_vec4s = 5 + + # IMPORTANT: RGBA32 is really required here. Otherwise artifacts and bad + # shadow filtering occur due to precision issues + self.img_source_data = Image.create_buffer( + "ShadowSourceData", self.MAX_SOURCES * per_source_vec4s, "RGBA32") + self.img_light_data.clear_image() + + # Register the buffer + inputs = self.pipeline.stage_mgr.inputs + inputs["AllLightsData"] = self.img_light_data + inputs["ShadowSourceData"] = self.img_source_data + inputs["maxLightIndex"] = self.pta_max_light_index + + def init_stages(self): + """ Inits all required stages for the lighting """ + + add_stage = self.pipeline.stage_mgr.add_stage + + self.flag_cells_stage = FlagUsedCellsStage(self.pipeline) + add_stage(self.flag_cells_stage) + + self.collect_cells_stage = CollectUsedCellsStage(self.pipeline) + add_stage(self.collect_cells_stage) + + self.cull_lights_stage = CullLightsStage(self.pipeline) + add_stage(self.cull_lights_stage) + + self.apply_lights_stage = ApplyLightsStage(self.pipeline) + add_stage(self.apply_lights_stage) + + self.shadow_stage = ShadowStage(self.pipeline) + self.shadow_stage.size = self.shadow_manager.get_atlas_size() + add_stage(self.shadow_stage) + + def init_defines(self): + """ Inits the common defines """ + defines = self.pipeline.stage_mgr.defines + defines["LC_TILE_SIZE_X"] = self.tile_size.x + defines["LC_TILE_SIZE_Y"] = self.tile_size.y + defines["LC_TILE_SLICES"] = self.pipeline.settings["lighting.culling_grid_slices"] + defines["LC_MAX_DISTANCE"] = self.pipeline.settings["lighting.culling_max_distance"] + defines["LC_CULLING_SLICE_WIDTH"] = self.pipeline.settings["lighting.culling_slice_width"] + defines["LC_MAX_LIGHTS_PER_CELL"] = self.pipeline.settings["lighting.max_lights_per_cell"] + defines["SHADOW_ATLAS_SIZE"] = self.pipeline.settings["shadows.atlas_size"] + + # Register all light types as defines + for attr in dir(PointLight): + if attr.startswith("LT_"): + defines[attr.upper()] = getattr(PointLight, attr) diff --git a/RenderPipelineFile/rpcore/loader.py b/RenderPipelineFile/rpcore/loader.py index 8dd262e7..0c4e504c 100644 --- a/RenderPipelineFile/rpcore/loader.py +++ b/RenderPipelineFile/rpcore/loader.py @@ -1,148 +1,148 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import print_function - -import time - -from panda3d.core import PNMImage, VirtualFileSystem, VirtualFileMountRamdisk -from panda3d.core import Shader - -from rpcore.globals import Globals -from rpcore.rpobject import RPObject - -__all__ = ("RPLoader",) - - -class timed_loading_operation(object): # noqa # pylint: disable=invalid-name,too-few-public-methods - - """ Context manager for a synchronous loading operation, keeping track - on how much time elapsed during the loading process, and warning about - long loading times. """ - - WARNING_COUNT = 0 - - def __init__(self, resource): - self.resource = resource - if isinstance(self.resource, (list, tuple)): - self.resource = ', '.join(self.resource) - - def __enter__(self): - self.start_time = time.process_time() - - def __exit__(self, *args): - duration = (time.process_time() - self.start_time) * 1000.0 - if duration > 80.0 and timed_loading_operation.WARNING_COUNT < 5: - RPObject.global_warn( - "RPLoader", "Loading '" + self.resource + "' took", round(duration, 2), "ms") - timed_loading_operation.WARNING_COUNT += 1 - if timed_loading_operation.WARNING_COUNT == 5: - RPObject.global_warn( - "RPLoader", "Skipping further loading warnings (max warning count reached)") - - -class RPLoader(RPObject): - - """ Generic loader class used by the pipeline. All loading of assets happens - here, which enables us to keep track of used resources """ - - @classmethod - def load_texture(cls, filename): - """ Loads a 2D-texture from disk """ - with timed_loading_operation(filename): - return Globals.base.loader.load_texture(filename) - - @classmethod - def load_cube_map(cls, filename, read_mipmaps=False): - """ Loads a cube map from disk """ - with timed_loading_operation(filename): - return Globals.base.loader.load_cube_map(filename, readMipmaps=read_mipmaps) - - @classmethod - def load_3d_texture(cls, filename): - """ Loads a 3D-texture from disk """ - with timed_loading_operation(filename): - return Globals.base.loader.load_3d_texture(filename) - - @classmethod - def load_font(cls, filename): - """ Loads a font from disk """ - with timed_loading_operation(filename): - return Globals.base.loader.load_font(filename) - - @classmethod - def load_shader(cls, *args): - """ Loads a shader from disk """ - with timed_loading_operation(args): - if len(args) == 1: - return Shader.load_compute(Shader.SL_GLSL, args[0]) - return Shader.load(Shader.SL_GLSL, *args) - - @classmethod - def load_model(cls, filename): - """ Loads a model from disk """ - with timed_loading_operation(filename): - return Globals.base.loader.load_model(filename) - - @classmethod - def load_sliced_3d_texture(cls, fname, tile_size_x, tile_size_y=None, num_tiles=None): - """ Loads a texture from the given filename and dimensions. If only - one dimensions is specified, the other dimensions are assumed to be - equal. This internally loads the texture into ram, splits it into smaller - sub-images, and then calls the load_3d_texture from the Panda loader """ - - tempfile_name = "/$$slice_loader_temp-" + str(time.time()) + "/" - tile_size_y = tile_size_x if tile_size_y is None else tile_size_y - num_tiles = tile_size_x if num_tiles is None else num_tiles - - # Load sliced image from disk - tex_handle = cls.load_texture(fname) - source = PNMImage() - tex_handle.store(source) - width = source.get_x_size() - - # Find slice properties - num_cols = width // tile_size_x - temp_img = PNMImage( - tile_size_x, tile_size_y, source.get_num_channels(), source.get_maxval()) - - # Construct a ramdisk to write the files to - vfs = VirtualFileSystem.get_global_ptr() - ramdisk = VirtualFileMountRamdisk() - vfs.mount(ramdisk, tempfile_name, 0) - - # Extract all slices and write them to the virtual disk - for z_slice in range(num_tiles): - slice_x = (z_slice % num_cols) * tile_size_x - slice_y = (z_slice // num_cols) * tile_size_y - temp_img.copy_sub_image(source, 0, 0, slice_x, slice_y, tile_size_x, tile_size_y) - temp_img.write(tempfile_name + str(z_slice) + ".png") - - # Load the de-sliced texture from the ramdisk - texture_handle = cls.load_3d_texture(tempfile_name + "/#.png") - vfs.unmount(ramdisk) - - return texture_handle +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import print_function + +import time + +from panda3d.core import PNMImage, VirtualFileSystem, VirtualFileMountRamdisk +from panda3d.core import Shader + +from rpcore.globals import Globals +from rpcore.rpobject import RPObject + +__all__ = ("RPLoader",) + + +class timed_loading_operation(object): # noqa # pylint: disable=invalid-name,too-few-public-methods + + """ Context manager for a synchronous loading operation, keeping track + on how much time elapsed during the loading process, and warning about + long loading times. """ + + WARNING_COUNT = 0 + + def __init__(self, resource): + self.resource = resource + if isinstance(self.resource, (list, tuple)): + self.resource = ', '.join(self.resource) + + def __enter__(self): + self.start_time = time.process_time() + + def __exit__(self, *args): + duration = (time.process_time() - self.start_time) * 1000.0 + if duration > 80.0 and timed_loading_operation.WARNING_COUNT < 5: + RPObject.global_warn( + "RPLoader", "Loading '" + self.resource + "' took", round(duration, 2), "ms") + timed_loading_operation.WARNING_COUNT += 1 + if timed_loading_operation.WARNING_COUNT == 5: + RPObject.global_warn( + "RPLoader", "Skipping further loading warnings (max warning count reached)") + + +class RPLoader(RPObject): + + """ Generic loader class used by the pipeline. All loading of assets happens + here, which enables us to keep track of used resources """ + + @classmethod + def load_texture(cls, filename): + """ Loads a 2D-texture from disk """ + with timed_loading_operation(filename): + return Globals.base.loader.load_texture(filename) + + @classmethod + def load_cube_map(cls, filename, read_mipmaps=False): + """ Loads a cube map from disk """ + with timed_loading_operation(filename): + return Globals.base.loader.load_cube_map(filename, readMipmaps=read_mipmaps) + + @classmethod + def load_3d_texture(cls, filename): + """ Loads a 3D-texture from disk """ + with timed_loading_operation(filename): + return Globals.base.loader.load_3d_texture(filename) + + @classmethod + def load_font(cls, filename): + """ Loads a font from disk """ + with timed_loading_operation(filename): + return Globals.base.loader.load_font(filename) + + @classmethod + def load_shader(cls, *args): + """ Loads a shader from disk """ + with timed_loading_operation(args): + if len(args) == 1: + return Shader.load_compute(Shader.SL_GLSL, args[0]) + return Shader.load(Shader.SL_GLSL, *args) + + @classmethod + def load_model(cls, filename): + """ Loads a model from disk """ + with timed_loading_operation(filename): + return Globals.base.loader.load_model(filename) + + @classmethod + def load_sliced_3d_texture(cls, fname, tile_size_x, tile_size_y=None, num_tiles=None): + """ Loads a texture from the given filename and dimensions. If only + one dimensions is specified, the other dimensions are assumed to be + equal. This internally loads the texture into ram, splits it into smaller + sub-images, and then calls the load_3d_texture from the Panda loader """ + + tempfile_name = "/$$slice_loader_temp-" + str(time.time()) + "/" + tile_size_y = tile_size_x if tile_size_y is None else tile_size_y + num_tiles = tile_size_x if num_tiles is None else num_tiles + + # Load sliced image from disk + tex_handle = cls.load_texture(fname) + source = PNMImage() + tex_handle.store(source) + width = source.get_x_size() + + # Find slice properties + num_cols = width // tile_size_x + temp_img = PNMImage( + tile_size_x, tile_size_y, source.get_num_channels(), source.get_maxval()) + + # Construct a ramdisk to write the files to + vfs = VirtualFileSystem.get_global_ptr() + ramdisk = VirtualFileMountRamdisk() + vfs.mount(ramdisk, tempfile_name, 0) + + # Extract all slices and write them to the virtual disk + for z_slice in range(num_tiles): + slice_x = (z_slice % num_cols) * tile_size_x + slice_y = (z_slice // num_cols) * tile_size_y + temp_img.copy_sub_image(source, 0, 0, slice_x, slice_y, tile_size_x, tile_size_y) + temp_img.write(tempfile_name + str(z_slice) + ".png") + + # Load the de-sliced texture from the ramdisk + texture_handle = cls.load_3d_texture(tempfile_name + "/#.png") + vfs.unmount(ramdisk) + + return texture_handle diff --git a/RenderPipelineFile/rpcore/mount_manager.py b/RenderPipelineFile/rpcore/mount_manager.py index 5deaef48..d4901dbf 100644 --- a/RenderPipelineFile/rpcore/mount_manager.py +++ b/RenderPipelineFile/rpcore/mount_manager.py @@ -1,316 +1,316 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -import os -import atexit - -from panda3d.core import Filename, VirtualFileSystem, get_model_path -from panda3d.core import VirtualFileMountRamdisk -from direct.stdpy.file import join, isdir, isfile - -from rpcore.rpobject import RPObject - - -class MountManager(RPObject): - - """ This classes mounts the required directories for the pipeline to run. - This is important if the pipeline is in a subdirectory for example. The - mount manager also handles the lock, storing the current PID into a file - named instance.pid and ensuring that there is only 1 instance of the - pipeline running at one time. """ - - def __init__(self, pipeline): - """ Creates a new mount manager """ - RPObject.__init__(self) - self._pipeline = pipeline - self._base_path = self._find_basepath() - self._lock_file = "instance.pid" - self._model_paths = [] - self._write_path = None - self._mounted = False - self._do_cleanup = True - self._config_dir = None - - self.debug("Auto-Detected base path to", self._base_path) - atexit.register(self._on_exit_cleanup) - - @property - def write_path(self): - """ Returns the write path previously set with set_write_path, or None - if no write path has been set yet. """ - return self._write_path - - @write_path.setter - def write_path(self, pth): - """ Set a writable directory for generated files. This can be a string - path name or a multifile with openReadWrite(). If no pathname is set - then the root directory is used. - - This feature is usually only used for debugging, the pipeline will dump - all generated shaders and other temporary files to that directory. - If you don't need this, you can use set_virtual_write_path(), which - will create the temporary path in the VirtualFileSystem, thus not - writing any files to disk. """ - if pth is None: - self._write_path = None - self._lock_file = "instance.pid" - else: - self._write_path = Filename.from_os_specific(pth).get_fullpath() - self._lock_file = join(self._write_path, "instance.pid") - - @property - def base_path(self): - """ Returns the base path of the pipeline. This returns the path previously - set with set_base_path, or the auto detected base path if no path was - set yet """ - return self._base_path - - @base_path.setter - def base_path(self, pth): - """ Sets the path where the base shaders and models on are contained. This - is usually the root of the rendering pipeline folder """ - self.debug("Set base path to '" + pth + "'") - self._base_path = Filename.from_os_specific(pth).get_fullpath() - - @property - def config_dir(self): - """ Returns the config directory previously set with set_config_dir, or - None if no directory was set yet """ - - @config_dir.setter - def config_dir(self, pth): - """ Sets the path to the config directory. Usually this is the config/ - directory located in the pipeline root directory. However, if you want - to load your own configuration files, you can specify a custom config - directory here. Your configuration directory should contain the - pipeline.yaml, plugins.yaml, daytime.yaml and configuration.prc. - - It is highly recommended you use the pipeline provided config files, modify - them to your needs, and as soon as you think they are in a final version, - copy them over. Please also notice that you should keep your config files - up-to-date, e.g. when new configuration variables are added. - - Also, specifying a custom configuration_dir disables the functionality - of the PluginConfigurator and DayTime editor, since they operate on the - pipelines default config files. - - Set the directory to None to use the default directory. """ - self._config_dir = Filename.from_os_specific(pth).get_fullpath() - - @property - def do_cleanup(self): - """ Returns whether the mount manager will attempt to cleanup the - generated files after the application stopped running """ - return self._do_cleanup - - @do_cleanup.setter - def do_cleanup(self, cleanup): - """ Sets whether to cleanup the tempfolder after the application stopped. - This is mostly useful for debugging, to analyze the generated tempfiles - even after the pipeline stopped running """ - self._do_cleanup = cleanup - - def get_lock(self): - """ Checks if we are the only instance running. If there is no instance - running, write the current PID to the instance.pid file. If the - instance file exists, checks if the specified process still runs. This - way only 1 instance of the pipeline can be run at one time. """ - - # Check if there is a lockfile at all - if isfile(self._lock_file): - # Read process id from lockfile - try: - with open(self._lock_file, "r") as handle: - pid = int(handle.readline()) - except IOError as msg: - self.error("Failed to read lockfile:", msg) - return False - - # Check if the process is still running - if self._is_pid_running(pid): - self.error("Found running instance") - return False - - # Process is not running anymore, we can write the lockfile - self._write_lock() - return True - - else: - # When there is no lockfile, just create it and continue - self._write_lock() - return True - - def _find_basepath(self): - """ Attempts to find the pipeline base path by looking at the location - of this file """ - pth = os.path.abspath(join(os.path.dirname(os.path.realpath(__file__)), "..")) - return Filename.from_os_specific(pth).get_fullpath() - - def _is_pid_running(self, pid): - """ Checks if a pid is still running """ - - # Code snippet from: - # http://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid - - if os.name == 'posix': - import errno - if pid < 0: - return False - try: - os.kill(pid, 0) - except OSError as err: - return err.errno == errno.EPERM - else: - return True - else: - import ctypes - kernel32 = ctypes.windll.kernel32 - process = kernel32.OpenProcess(0x100000, 0, pid) - if process != 0: - kernel32.CloseHandle(process) - return True - else: - return False - - def _write_lock(self): - """ Internal method to write the current process id to the instance.pid - lockfile. This is used to ensure no second instance of the pipeline is - running. """ - - with open(self._lock_file, "w") as handle: - handle.write(str(os.getpid())) - - def _try_remove(self, fname): - """ Tries to remove the specified filename, returns either True or False - depending if we had success or not """ - try: - os.remove(fname) - return True - except (IOError, OSError): - pass - return False - - def _on_exit_cleanup(self): - """ Gets called when the application exists """ - - if self._do_cleanup: - self.debug("Cleaning up ..") - - if self._write_path is not None: - - # Try removing the lockfile - self._try_remove(self._lock_file) - - # Check for further tempfiles in the write path - # We explicitely use os.listdir here instead of pandas listdir, - # to work with actual paths - for fname in os.listdir(self._write_path): - pth = join(self._write_path, fname) - - # Tempfiles from the pipeline start with "$$" to distinguish - # them from user created files - if isfile(pth) and fname.startswith("$$"): - self._try_remove(pth) - - # Delete the write path if no files are left - if len(os.listdir(self._write_path)) < 1: - try: - os.removedirs(self._write_path) - except IOError: - pass - - @property - def is_mounted(self): - """ Returns whether the MountManager was already mounted by calling - mount() """ - return self._mounted - - def mount(self): - """ Inits the VFS Mounts. This creates the following virtual directory - structure, from which all files can be located: - - /$$rp/ (Mounted from the render pipeline base directory) - + rpcore/ - + shader/ - + ... - - /$rpconfig/ (Mounted from config/, may be set by user) - + pipeline.yaml - + ... - - /$$rptemp/ (Either ramdisk or user specified) - + day_time_config - + shader_auto_config - + ... - - /$$rpshader/ (Link to /$$rp/rpcore/shader) - - """ - self.debug("Setting up virtual filesystem") - self._mounted = True - - def convert_path(pth): - return Filename.from_os_specific(pth).get_fullpath() - vfs = VirtualFileSystem.get_global_ptr() - - # Mount config dir as $$rpconf - if self._config_dir is None: - config_dir = convert_path(join(self._base_path, "config/")) - self.debug("Mounting auto-detected config dir:", config_dir) - vfs.mount(config_dir, "/$$rpconfig", 0) - else: - self.debug("Mounting custom config dir:", self._config_dir) - vfs.mount(convert_path(self._config_dir), "/$$rpconfig", 0) - - # Mount directory structure - vfs.mount(convert_path(self._base_path), "/$$rp", 0) - vfs.mount(convert_path(join(self._base_path, "rpcore/shader")), "/$$rp/shader", 0) - vfs.mount(convert_path(join(self._base_path, "effects")), "effects", 0) - - # Mount the pipeline temp path: - # If no write path is specified, use a virtual ramdisk - if self._write_path is None: - self.debug("Mounting ramdisk as /$$rptemp") - vfs.mount(VirtualFileMountRamdisk(), "/$$rptemp", 0) - else: - # In case an actual write path is specified: - # Ensure the pipeline write path exists, and if not, create it - if not isdir(self._write_path): - self.debug("Creating temporary path, since it does not exist yet") - try: - os.makedirs(self._write_path) - except IOError as msg: - self.fatal("Failed to create temporary path:", msg) - self.debug("Mounting", self._write_path, "as /$$rptemp") - vfs.mount(convert_path(self._write_path), '/$$rptemp', 0) - - get_model_path().prepend_directory("/$$rp") - get_model_path().prepend_directory("/$$rp/shader") - get_model_path().prepend_directory("/$$rptemp") - - def unmount(self): - """ Unmounts the VFS """ - raise NotImplementedError("TODO") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +import os +import atexit + +from panda3d.core import Filename, VirtualFileSystem, get_model_path +from panda3d.core import VirtualFileMountRamdisk +from direct.stdpy.file import join, isdir, isfile + +from rpcore.rpobject import RPObject + + +class MountManager(RPObject): + + """ This classes mounts the required directories for the pipeline to run. + This is important if the pipeline is in a subdirectory for example. The + mount manager also handles the lock, storing the current PID into a file + named instance.pid and ensuring that there is only 1 instance of the + pipeline running at one time. """ + + def __init__(self, pipeline): + """ Creates a new mount manager """ + RPObject.__init__(self) + self._pipeline = pipeline + self._base_path = self._find_basepath() + self._lock_file = "instance.pid" + self._model_paths = [] + self._write_path = None + self._mounted = False + self._do_cleanup = True + self._config_dir = None + + self.debug("Auto-Detected base path to", self._base_path) + atexit.register(self._on_exit_cleanup) + + @property + def write_path(self): + """ Returns the write path previously set with set_write_path, or None + if no write path has been set yet. """ + return self._write_path + + @write_path.setter + def write_path(self, pth): + """ Set a writable directory for generated files. This can be a string + path name or a multifile with openReadWrite(). If no pathname is set + then the root directory is used. + + This feature is usually only used for debugging, the pipeline will dump + all generated shaders and other temporary files to that directory. + If you don't need this, you can use set_virtual_write_path(), which + will create the temporary path in the VirtualFileSystem, thus not + writing any files to disk. """ + if pth is None: + self._write_path = None + self._lock_file = "instance.pid" + else: + self._write_path = Filename.from_os_specific(pth).get_fullpath() + self._lock_file = join(self._write_path, "instance.pid") + + @property + def base_path(self): + """ Returns the base path of the pipeline. This returns the path previously + set with set_base_path, or the auto detected base path if no path was + set yet """ + return self._base_path + + @base_path.setter + def base_path(self, pth): + """ Sets the path where the base shaders and models on are contained. This + is usually the root of the rendering pipeline folder """ + self.debug("Set base path to '" + pth + "'") + self._base_path = Filename.from_os_specific(pth).get_fullpath() + + @property + def config_dir(self): + """ Returns the config directory previously set with set_config_dir, or + None if no directory was set yet """ + + @config_dir.setter + def config_dir(self, pth): + """ Sets the path to the config directory. Usually this is the config/ + directory located in the pipeline root directory. However, if you want + to load your own configuration files, you can specify a custom config + directory here. Your configuration directory should contain the + pipeline.yaml, plugins.yaml, daytime.yaml and configuration.prc. + + It is highly recommended you use the pipeline provided config files, modify + them to your needs, and as soon as you think they are in a final version, + copy them over. Please also notice that you should keep your config files + up-to-date, e.g. when new configuration variables are added. + + Also, specifying a custom configuration_dir disables the functionality + of the PluginConfigurator and DayTime editor, since they operate on the + pipelines default config files. + + Set the directory to None to use the default directory. """ + self._config_dir = Filename.from_os_specific(pth).get_fullpath() + + @property + def do_cleanup(self): + """ Returns whether the mount manager will attempt to cleanup the + generated files after the application stopped running """ + return self._do_cleanup + + @do_cleanup.setter + def do_cleanup(self, cleanup): + """ Sets whether to cleanup the tempfolder after the application stopped. + This is mostly useful for debugging, to analyze the generated tempfiles + even after the pipeline stopped running """ + self._do_cleanup = cleanup + + def get_lock(self): + """ Checks if we are the only instance running. If there is no instance + running, write the current PID to the instance.pid file. If the + instance file exists, checks if the specified process still runs. This + way only 1 instance of the pipeline can be run at one time. """ + + # Check if there is a lockfile at all + if isfile(self._lock_file): + # Read process id from lockfile + try: + with open(self._lock_file, "r") as handle: + pid = int(handle.readline()) + except IOError as msg: + self.error("Failed to read lockfile:", msg) + return False + + # Check if the process is still running + if self._is_pid_running(pid): + self.error("Found running instance") + return False + + # Process is not running anymore, we can write the lockfile + self._write_lock() + return True + + else: + # When there is no lockfile, just create it and continue + self._write_lock() + return True + + def _find_basepath(self): + """ Attempts to find the pipeline base path by looking at the location + of this file """ + pth = os.path.abspath(join(os.path.dirname(os.path.realpath(__file__)), "..")) + return Filename.from_os_specific(pth).get_fullpath() + + def _is_pid_running(self, pid): + """ Checks if a pid is still running """ + + # Code snippet from: + # http://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid + + if os.name == 'posix': + import errno + if pid < 0: + return False + try: + os.kill(pid, 0) + except OSError as err: + return err.errno == errno.EPERM + else: + return True + else: + import ctypes + kernel32 = ctypes.windll.kernel32 + process = kernel32.OpenProcess(0x100000, 0, pid) + if process != 0: + kernel32.CloseHandle(process) + return True + else: + return False + + def _write_lock(self): + """ Internal method to write the current process id to the instance.pid + lockfile. This is used to ensure no second instance of the pipeline is + running. """ + + with open(self._lock_file, "w") as handle: + handle.write(str(os.getpid())) + + def _try_remove(self, fname): + """ Tries to remove the specified filename, returns either True or False + depending if we had success or not """ + try: + os.remove(fname) + return True + except (IOError, OSError): + pass + return False + + def _on_exit_cleanup(self): + """ Gets called when the application exists """ + + if self._do_cleanup: + self.debug("Cleaning up ..") + + if self._write_path is not None: + + # Try removing the lockfile + self._try_remove(self._lock_file) + + # Check for further tempfiles in the write path + # We explicitely use os.listdir here instead of pandas listdir, + # to work with actual paths + for fname in os.listdir(self._write_path): + pth = join(self._write_path, fname) + + # Tempfiles from the pipeline start with "$$" to distinguish + # them from user created files + if isfile(pth) and fname.startswith("$$"): + self._try_remove(pth) + + # Delete the write path if no files are left + if len(os.listdir(self._write_path)) < 1: + try: + os.removedirs(self._write_path) + except IOError: + pass + + @property + def is_mounted(self): + """ Returns whether the MountManager was already mounted by calling + mount() """ + return self._mounted + + def mount(self): + """ Inits the VFS Mounts. This creates the following virtual directory + structure, from which all files can be located: + + /$$rp/ (Mounted from the render pipeline base directory) + + rpcore/ + + shader/ + + ... + + /$rpconfig/ (Mounted from config/, may be set by user) + + pipeline.yaml + + ... + + /$$rptemp/ (Either ramdisk or user specified) + + day_time_config + + shader_auto_config + + ... + + /$$rpshader/ (Link to /$$rp/rpcore/shader) + + """ + self.debug("Setting up virtual filesystem") + self._mounted = True + + def convert_path(pth): + return Filename.from_os_specific(pth).get_fullpath() + vfs = VirtualFileSystem.get_global_ptr() + + # Mount config dir as $$rpconf + if self._config_dir is None: + config_dir = convert_path(join(self._base_path, "config/")) + self.debug("Mounting auto-detected config dir:", config_dir) + vfs.mount(config_dir, "/$$rpconfig", 0) + else: + self.debug("Mounting custom config dir:", self._config_dir) + vfs.mount(convert_path(self._config_dir), "/$$rpconfig", 0) + + # Mount directory structure + vfs.mount(convert_path(self._base_path), "/$$rp", 0) + vfs.mount(convert_path(join(self._base_path, "rpcore/shader")), "/$$rp/shader", 0) + vfs.mount(convert_path(join(self._base_path, "effects")), "effects", 0) + + # Mount the pipeline temp path: + # If no write path is specified, use a virtual ramdisk + if self._write_path is None: + self.debug("Mounting ramdisk as /$$rptemp") + vfs.mount(VirtualFileMountRamdisk(), "/$$rptemp", 0) + else: + # In case an actual write path is specified: + # Ensure the pipeline write path exists, and if not, create it + if not isdir(self._write_path): + self.debug("Creating temporary path, since it does not exist yet") + try: + os.makedirs(self._write_path) + except IOError as msg: + self.fatal("Failed to create temporary path:", msg) + self.debug("Mounting", self._write_path, "as /$$rptemp") + vfs.mount(convert_path(self._write_path), '/$$rptemp', 0) + + get_model_path().prepend_directory("/$$rp") + get_model_path().prepend_directory("/$$rp/shader") + get_model_path().prepend_directory("/$$rptemp") + + def unmount(self): + """ Unmounts the VFS """ + raise NotImplementedError("TODO") diff --git a/RenderPipelineFile/rpcore/native/.gitignore b/RenderPipelineFile/rpcore/native/.gitignore index a2c97840..197e239b 100644 --- a/RenderPipelineFile/rpcore/native/.gitignore +++ b/RenderPipelineFile/rpcore/native/.gitignore @@ -1,19 +1,19 @@ - -# Prefab gitignore which can be used by the user to ignore the files generated -# by the module builder - -*.pyd -*.pyc -*.so -*.pdb - -# Various output names -win_*/ -linux_*/ -source/interrogate* - -# Source and script files -scripts/ -CMakeLists.txt -build.py -LICENSE + +# Prefab gitignore which can be used by the user to ignore the files generated +# by the module builder + +*.pyd +*.pyc +*.so +*.pdb + +# Various output names +win_*/ +linux_*/ +source/interrogate* + +# Source and script files +scripts/ +CMakeLists.txt +build.py +LICENSE diff --git a/RenderPipelineFile/rpcore/native/__init__.py b/RenderPipelineFile/rpcore/native/__init__.py index a2f219a3..231388bd 100644 --- a/RenderPipelineFile/rpcore/native/__init__.py +++ b/RenderPipelineFile/rpcore/native/__init__.py @@ -1,96 +1,96 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -# pylint: disable=invalid-name - -# This file includes all modules from the native module. - -from __future__ import print_function -import sys -from os.path import dirname, realpath - -from direct.stdpy.file import join, isfile -from rpcore.rpobject import RPObject - -# Store a global flag, indicating whether the C++ modules were loaded or the python -# implemetation of them -NATIVE_CXX_LOADED = False - -# Read the configuration from the flag-file -current_path = dirname(realpath(__file__)) -cxx_flag_path = join(current_path, "use_cxx.flag") -if not isfile(cxx_flag_path): - RPObject.global_error("CORE", "Could not find cxx flag, please run the setup.py!") - sys.exit(1) -else: - with open(join(current_path, "use_cxx.flag"), "r") as handle: - NATIVE_CXX_LOADED = handle.read().strip() == "1" - -# The native module should only be imported once, and that by the internal pipeline code -assert __package__ == "rpcore.native", "You have included the pipeline in the wrong way!" - -# Classes which should get imported -classes_to_import = [ - "GPUCommand", - "GPUCommandList", - "ShadowManager", - "InternalLightManager", - "PSSMCameraRig", - "IESDataset", - "TagStateManager", -] - -# Classes which should get imported and renamed -classes_to_import_and_rename = { - "RPPointLight": "PointLight", - "RPSpotLight": "SpotLight" -} - -native_module = None - -# If the module was built, use it, otherwise use the python wrappers -if NATIVE_CXX_LOADED: - try: - from panda3d import _rplight as _native_module # pylint: disable=wrong-import-position - RPObject.global_debug("CORE", "Using panda3d-supplied core module") - except ImportError: - RPObject.global_debug("CORE", "Using native core module") - from rpcore.native import native_ as _native_module # pylint: disable=wrong-import-position -else: - print(f'343434343434343434343434343') - from rpcore import pynative as _native_module # pylint: disable=wrong-import-position - RPObject.global_debug("CORE", "Using simulated python-wrapper module") - -# Import all classes -for v in classes_to_import + list(classes_to_import_and_rename.keys()): - if hasattr(_native_module, v): - v_name = classes_to_import_and_rename[v] if v in classes_to_import_and_rename else v - globals()[v_name] = getattr(_native_module, v) - else: - print("ERROR: could not import class", v, "from", _native_module.__name__) - -# Don't export all variables, only the required ones -__all__ = classes_to_import + list(classes_to_import_and_rename.values()) + ["NATIVE_CXX_LOADED"] +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +# pylint: disable=invalid-name + +# This file includes all modules from the native module. + +from __future__ import print_function +import sys +from os.path import dirname, realpath + +from direct.stdpy.file import join, isfile +from rpcore.rpobject import RPObject + +# Store a global flag, indicating whether the C++ modules were loaded or the python +# implemetation of them +NATIVE_CXX_LOADED = False + +# Read the configuration from the flag-file +current_path = dirname(realpath(__file__)) +cxx_flag_path = join(current_path, "use_cxx.flag") +if not isfile(cxx_flag_path): + RPObject.global_error("CORE", "Could not find cxx flag, please run the setup.py!") + sys.exit(1) +else: + with open(join(current_path, "use_cxx.flag"), "r") as handle: + NATIVE_CXX_LOADED = handle.read().strip() == "1" + +# The native module should only be imported once, and that by the internal pipeline code +assert __package__ == "rpcore.native", "You have included the pipeline in the wrong way!" + +# Classes which should get imported +classes_to_import = [ + "GPUCommand", + "GPUCommandList", + "ShadowManager", + "InternalLightManager", + "PSSMCameraRig", + "IESDataset", + "TagStateManager", +] + +# Classes which should get imported and renamed +classes_to_import_and_rename = { + "RPPointLight": "PointLight", + "RPSpotLight": "SpotLight" +} + +native_module = None + +# If the module was built, use it, otherwise use the python wrappers +if NATIVE_CXX_LOADED: + try: + from panda3d import _rplight as _native_module # pylint: disable=wrong-import-position + RPObject.global_debug("CORE", "Using panda3d-supplied core module") + except ImportError: + RPObject.global_debug("CORE", "Using native core module") + from rpcore.native import native_ as _native_module # pylint: disable=wrong-import-position +else: + print(f'343434343434343434343434343') + from rpcore import pynative as _native_module # pylint: disable=wrong-import-position + RPObject.global_debug("CORE", "Using simulated python-wrapper module") + +# Import all classes +for v in classes_to_import + list(classes_to_import_and_rename.keys()): + if hasattr(_native_module, v): + v_name = classes_to_import_and_rename[v] if v in classes_to_import_and_rename else v + globals()[v_name] = getattr(_native_module, v) + else: + print("ERROR: could not import class", v, "from", _native_module.__name__) + +# Don't export all variables, only the required ones +__all__ = classes_to_import + list(classes_to_import_and_rename.values()) + ["NATIVE_CXX_LOADED"] diff --git a/RenderPipelineFile/rpcore/native/config.ini b/RenderPipelineFile/rpcore/native/config.ini index 72d084aa..72fcfa10 100644 --- a/RenderPipelineFile/rpcore/native/config.ini +++ b/RenderPipelineFile/rpcore/native/config.ini @@ -1,2 +1,2 @@ -generate_pdb=1 -module_name=native_ +generate_pdb=1 +module_name=native_ diff --git a/RenderPipelineFile/rpcore/native/source/README.md b/RenderPipelineFile/rpcore/native/source/README.md index 2f342f36..896154ba 100644 --- a/RenderPipelineFile/rpcore/native/source/README.md +++ b/RenderPipelineFile/rpcore/native/source/README.md @@ -1,7 +1,7 @@ -## Render Pipeline Native - -This contains the source code of the native C++ modules -of the RenderPipeline. - -The documentation of the -modules can be found here. +## Render Pipeline Native + +This contains the source code of the native C++ modules +of the RenderPipeline. + +The documentation of the +modules can be found here. diff --git a/RenderPipelineFile/rpcore/native/source/config_rsnative.N b/RenderPipelineFile/rpcore/native/source/config_rsnative.N index d2e84d14..ca9ba994 100644 --- a/RenderPipelineFile/rpcore/native/source/config_rsnative.N +++ b/RenderPipelineFile/rpcore/native/source/config_rsnative.N @@ -1,5 +1,5 @@ -forcetype GPUCommandList -forcetype GPUCommand -forcetype RPLight -forcetype ShadowManager -forcetype TagStateManager +forcetype GPUCommandList +forcetype GPUCommand +forcetype RPLight +forcetype ShadowManager +forcetype TagStateManager diff --git a/RenderPipelineFile/rpcore/native/source/config_rsnative.cpp b/RenderPipelineFile/rpcore/native/source/config_rsnative.cpp index fb481f78..07274c56 100644 --- a/RenderPipelineFile/rpcore/native/source/config_rsnative.cpp +++ b/RenderPipelineFile/rpcore/native/source/config_rsnative.cpp @@ -1,52 +1,52 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#include "config_rsnative.h" - -#include "rp_light.h" -#include "rp_point_light.h" - -#include "dconfig.h" - -Configure(config_rsnative); -NotifyCategoryDef(rsnative, ""); - -ConfigureFn(config_rsnative) { - init_librsnative(); -} - -void -init_librsnative() { - static bool initialized = false; - if (initialized) { - return; - } - initialized = true; - - // RPLight::init_type(); - // RPPointLight::init_type(); -} - +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "config_rsnative.h" + +#include "rp_light.h" +#include "rp_point_light.h" + +#include "dconfig.h" + +Configure(config_rsnative); +NotifyCategoryDef(rsnative, ""); + +ConfigureFn(config_rsnative) { + init_librsnative(); +} + +void +init_librsnative() { + static bool initialized = false; + if (initialized) { + return; + } + initialized = true; + + // RPLight::init_type(); + // RPPointLight::init_type(); +} + diff --git a/RenderPipelineFile/rpcore/native/source/config_rsnative.h b/RenderPipelineFile/rpcore/native/source/config_rsnative.h index 3d4f287c..e0615fef 100644 --- a/RenderPipelineFile/rpcore/native/source/config_rsnative.h +++ b/RenderPipelineFile/rpcore/native/source/config_rsnative.h @@ -1,37 +1,37 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -#include "pandabase.h" -#include "notifyCategoryProxy.h" -#include "configVariableDouble.h" -#include "configVariableString.h" -#include "configVariableInt.h" - -NotifyCategoryDecl(rsnative, EXPORT_CLASS, EXPORT_TEMPL); - -extern void init_librsnative(); +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +#include "pandabase.h" +#include "notifyCategoryProxy.h" +#include "configVariableDouble.h" +#include "configVariableString.h" +#include "configVariableInt.h" + +NotifyCategoryDecl(rsnative, EXPORT_CLASS, EXPORT_TEMPL); + +extern void init_librsnative(); diff --git a/RenderPipelineFile/rpcore/native/source/gpu_command.I b/RenderPipelineFile/rpcore/native/source/gpu_command.I index 4fdc2aa4..02a97917 100644 --- a/RenderPipelineFile/rpcore/native/source/gpu_command.I +++ b/RenderPipelineFile/rpcore/native/source/gpu_command.I @@ -1,185 +1,185 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#include "stdint.h" - -/** - * @brief Appends an integer to the GPUCommand. - * @details This adds an integer to the back of the GPUCommand. Depending on the - * setting in convert_int_to_float, this will either just convert the int to a - * float by casting it, or just do a bitwise copy. - * - * @param v The integer to append. - */ -inline void GPUCommand::push_int(int v) { - push_float(convert_int_to_float(v)); -} - -/** - * @brief Internal method to convert an integer to float - * @details This methods gets called by the GPUCommand::push_int, and manages - * storing an integer in a floating point variable. There are two options, - * which are documented inside of the method. - * - * @param v Integer to convert - * @return Float-representation of that integer, either casted or binary converted.s - */ -inline float GPUCommand::convert_int_to_float(int v) const { - - #if !PACK_INT_AS_FLOAT - // Just round to float, can cause rounding issues tho - return (float)v; - - #else - assert(sizeof(float) == 4); // We really need this for packing! Better - // throw an error if the compiler uses more - // than 4 bytes. - // Simple binary conversion, assuming sizeof(int) == sizeof(float) - union { int32_t _int; float _float; } converter = { (int32_t)v }; - return converter._float; - #endif -} - -/** - * @brief Appends a float to the GPUCommand. - * @details This adds an integer to the back of the GPUCommand. Its used by all - * other push_xxx methods, and simply stores the value, then increments the write - * pointer. When the amount of floats exceeds the capacity of the GPUCommand, - * an error will be printed, and the method returns without doing anything else. - * - * @param v The float to append. - */ -inline void GPUCommand::push_float(float v) { - if (_current_index >= GPU_COMMAND_ENTRIES) { - gpucommand_cat.error() << "Out of bounds! Exceeded command size of " << GPU_COMMAND_ENTRIES << endl; - return; - } - _data[_current_index++] = v; -} - -/** - * @brief Appends a 3-component floating point vector to the GPUCommand. - * @details This appends a 3-component floating point vector to the command. - * It basically just calls push_float() for every component, in the order - * x, y, z, which causes the vector to occupy the space of 3 floats. - * - * @param v Int-Vector to append. - */ -inline void GPUCommand::push_vec3(const LVecBase3f &v) { - push_float(v.get_x()); - push_float(v.get_y()); - push_float(v.get_z()); -} - - -/** - * @brief Appends a 3-component integer vector to the GPUCommand. - * @details This appends a 3-component integer vector to the command. - * It basically just calls push_int() for every component, in the order - * x, y, z, which causes the vector to occupy the space of 3 floats. - * - * @param v Int-Vector to append. - */ -inline void GPUCommand::push_vec3(const LVecBase3i &v) { - push_int(v.get_x()); - push_int(v.get_y()); - push_int(v.get_z()); -} - -/** - * @brief Appends a 4-component floating point vector to the GPUCommand. - * @details This appends a 4-component floating point vector to the command. - * It basically just calls push_float() for every component, in the order - * x, y, z, which causes the vector to occupy the space of 3 floats. - * - * @param v Int-Vector to append. - */ -inline void GPUCommand::push_vec4(const LVecBase4f &v) { - push_float(v.get_x()); - push_float(v.get_y()); - push_float(v.get_z()); - push_float(v.get_w()); -} - -/** - * @brief Appends a 4-component integer vector to the GPUCommand. - * @details This appends a 4-component integer vector to the command. - * It basically just calls push_int() for every component, in the order - * x, y, z, w, which causes the vector to occupy the space of 4 floats. - * - * @param v Int-Vector to append. - */ -inline void GPUCommand::push_vec4(const LVecBase4i &v) { - push_int(v.get_x()); - push_int(v.get_y()); - push_int(v.get_z()); - push_int(v.get_w()); -} - -/** - * @brief Appends a floating point 3x3 matrix to the GPUCommand. - * @details This appends a floating point 3x3 matrix to the GPUCommand, by - * pushing all components in row-order to the command. This occupies a space of - * 9 floats. - * - * @param v Matrix to append - */ -inline void GPUCommand::push_mat3(const LMatrix3f &v) { - for (size_t i = 0; i < 3; ++i) { - for (size_t j = 0; j < 3; ++j) { - push_float(v.get_cell(i, j)); - } - } -} - -/** - * @brief Appends a floating point 4x4 matrix to the GPUCommand. - * @details This appends a floating point 4x4 matrix to the GPUCommand, by - * pushing all components in row-order to the command. This occupies a space of - * 16 floats. - * - * @param v Matrix to append - */ -inline void GPUCommand::push_mat4(const LMatrix4f &v) { - for (size_t i = 0; i < 4; ++i) { - for (size_t j = 0; j < 4; ++j) { - push_float(v.get_cell(i, j)); - } - } -} - -/** - * @brief Returns whether integers are packed as floats. - * @details This returns how integer are packed into the data stream. If the - * returned value is true, then integers are packed using their binary - * representation converted to floating point format. If the returned value - * is false, then integers are packed by simply casting them to float, - * e.g. val = (float)i; - * @return The integer representation flag - */ -inline bool GPUCommand::get_uses_integer_packing() { - return PACK_INT_AS_FLOAT; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "stdint.h" + +/** + * @brief Appends an integer to the GPUCommand. + * @details This adds an integer to the back of the GPUCommand. Depending on the + * setting in convert_int_to_float, this will either just convert the int to a + * float by casting it, or just do a bitwise copy. + * + * @param v The integer to append. + */ +inline void GPUCommand::push_int(int v) { + push_float(convert_int_to_float(v)); +} + +/** + * @brief Internal method to convert an integer to float + * @details This methods gets called by the GPUCommand::push_int, and manages + * storing an integer in a floating point variable. There are two options, + * which are documented inside of the method. + * + * @param v Integer to convert + * @return Float-representation of that integer, either casted or binary converted.s + */ +inline float GPUCommand::convert_int_to_float(int v) const { + + #if !PACK_INT_AS_FLOAT + // Just round to float, can cause rounding issues tho + return (float)v; + + #else + assert(sizeof(float) == 4); // We really need this for packing! Better + // throw an error if the compiler uses more + // than 4 bytes. + // Simple binary conversion, assuming sizeof(int) == sizeof(float) + union { int32_t _int; float _float; } converter = { (int32_t)v }; + return converter._float; + #endif +} + +/** + * @brief Appends a float to the GPUCommand. + * @details This adds an integer to the back of the GPUCommand. Its used by all + * other push_xxx methods, and simply stores the value, then increments the write + * pointer. When the amount of floats exceeds the capacity of the GPUCommand, + * an error will be printed, and the method returns without doing anything else. + * + * @param v The float to append. + */ +inline void GPUCommand::push_float(float v) { + if (_current_index >= GPU_COMMAND_ENTRIES) { + gpucommand_cat.error() << "Out of bounds! Exceeded command size of " << GPU_COMMAND_ENTRIES << endl; + return; + } + _data[_current_index++] = v; +} + +/** + * @brief Appends a 3-component floating point vector to the GPUCommand. + * @details This appends a 3-component floating point vector to the command. + * It basically just calls push_float() for every component, in the order + * x, y, z, which causes the vector to occupy the space of 3 floats. + * + * @param v Int-Vector to append. + */ +inline void GPUCommand::push_vec3(const LVecBase3f &v) { + push_float(v.get_x()); + push_float(v.get_y()); + push_float(v.get_z()); +} + + +/** + * @brief Appends a 3-component integer vector to the GPUCommand. + * @details This appends a 3-component integer vector to the command. + * It basically just calls push_int() for every component, in the order + * x, y, z, which causes the vector to occupy the space of 3 floats. + * + * @param v Int-Vector to append. + */ +inline void GPUCommand::push_vec3(const LVecBase3i &v) { + push_int(v.get_x()); + push_int(v.get_y()); + push_int(v.get_z()); +} + +/** + * @brief Appends a 4-component floating point vector to the GPUCommand. + * @details This appends a 4-component floating point vector to the command. + * It basically just calls push_float() for every component, in the order + * x, y, z, which causes the vector to occupy the space of 3 floats. + * + * @param v Int-Vector to append. + */ +inline void GPUCommand::push_vec4(const LVecBase4f &v) { + push_float(v.get_x()); + push_float(v.get_y()); + push_float(v.get_z()); + push_float(v.get_w()); +} + +/** + * @brief Appends a 4-component integer vector to the GPUCommand. + * @details This appends a 4-component integer vector to the command. + * It basically just calls push_int() for every component, in the order + * x, y, z, w, which causes the vector to occupy the space of 4 floats. + * + * @param v Int-Vector to append. + */ +inline void GPUCommand::push_vec4(const LVecBase4i &v) { + push_int(v.get_x()); + push_int(v.get_y()); + push_int(v.get_z()); + push_int(v.get_w()); +} + +/** + * @brief Appends a floating point 3x3 matrix to the GPUCommand. + * @details This appends a floating point 3x3 matrix to the GPUCommand, by + * pushing all components in row-order to the command. This occupies a space of + * 9 floats. + * + * @param v Matrix to append + */ +inline void GPUCommand::push_mat3(const LMatrix3f &v) { + for (size_t i = 0; i < 3; ++i) { + for (size_t j = 0; j < 3; ++j) { + push_float(v.get_cell(i, j)); + } + } +} + +/** + * @brief Appends a floating point 4x4 matrix to the GPUCommand. + * @details This appends a floating point 4x4 matrix to the GPUCommand, by + * pushing all components in row-order to the command. This occupies a space of + * 16 floats. + * + * @param v Matrix to append + */ +inline void GPUCommand::push_mat4(const LMatrix4f &v) { + for (size_t i = 0; i < 4; ++i) { + for (size_t j = 0; j < 4; ++j) { + push_float(v.get_cell(i, j)); + } + } +} + +/** + * @brief Returns whether integers are packed as floats. + * @details This returns how integer are packed into the data stream. If the + * returned value is true, then integers are packed using their binary + * representation converted to floating point format. If the returned value + * is false, then integers are packed by simply casting them to float, + * e.g. val = (float)i; + * @return The integer representation flag + */ +inline bool GPUCommand::get_uses_integer_packing() { + return PACK_INT_AS_FLOAT; +} diff --git a/RenderPipelineFile/rpcore/native/source/gpu_command.cpp b/RenderPipelineFile/rpcore/native/source/gpu_command.cpp index d93f3a39..7cd477d0 100644 --- a/RenderPipelineFile/rpcore/native/source/gpu_command.cpp +++ b/RenderPipelineFile/rpcore/native/source/gpu_command.cpp @@ -1,87 +1,87 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#include "gpu_command.h" - -#include -#include -#include - - -NotifyCategoryDef(gpucommand, ""); - -/** - * @brief Constructs a new GPUCommand with the given command type. - * @details This will construct a new GPUCommand of the given command type. - * The command type should be of GPUCommand::CommandType, and determines - * what data the GPUCommand contains, and how it will be handled. - * - * @param command_type The type of the GPUCommand - */ -GPUCommand::GPUCommand(CommandType command_type) { - _command_type = command_type; - _current_index = 0; - memset(_data, 0x0, sizeof(float) * GPU_COMMAND_ENTRIES); - - // Store the command type as the first entry - push_int(command_type); -} - -/** - * @brief Prints out the GPUCommand to the console - * @details This method prints the type, size, and data of the GPUCommand to the - * console. This helps for debugging the contents of the GPUCommand. Keep - * in mind that integers might be shown in their binary float representation, - * depending on the setting in the GPUCommand::convert_int_to_float method. - */ -void GPUCommand::write(ostream &out) const { - out << "GPUCommand(type=" << _command_type << ", size=" << _current_index << ", data = {" << endl; - for (size_t k = 0; k < GPU_COMMAND_ENTRIES; ++k) { - out << std::setw(12) << std::fixed << std::setprecision(5) << _data[k] << " "; - if (k % 6 == 5 || k == GPU_COMMAND_ENTRIES - 1) out << endl; - } - out << "})" << endl; -} - -/** - * @brief Writes the GPU command to a given target. - * @details This method writes all the data of the GPU command to a given target. - * The target should be a pointer to memory being big enough to hold the - * data. Presumably #dest will be a handle to texture memory. - * The command_index controls the offset where the data will be written - * to. - * - * @param dest Handle to the memory to write the command to - * @param command_index Offset to write the command to. The command will write - * its data to command_index * GPU_COMMAND_ENTRIES. When writing - * the GPUCommand in a GPUCommandList, the command_index will - * most likely be the index of the command in the list. - */ -void GPUCommand::write_to(const PTA_uchar &dest, size_t command_index) { - size_t command_size = GPU_COMMAND_ENTRIES * sizeof(float); - size_t offset = command_index * command_size; - memcpy(dest.p() + offset, &_data, command_size); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "gpu_command.h" + +#include +#include +#include + + +NotifyCategoryDef(gpucommand, ""); + +/** + * @brief Constructs a new GPUCommand with the given command type. + * @details This will construct a new GPUCommand of the given command type. + * The command type should be of GPUCommand::CommandType, and determines + * what data the GPUCommand contains, and how it will be handled. + * + * @param command_type The type of the GPUCommand + */ +GPUCommand::GPUCommand(CommandType command_type) { + _command_type = command_type; + _current_index = 0; + memset(_data, 0x0, sizeof(float) * GPU_COMMAND_ENTRIES); + + // Store the command type as the first entry + push_int(command_type); +} + +/** + * @brief Prints out the GPUCommand to the console + * @details This method prints the type, size, and data of the GPUCommand to the + * console. This helps for debugging the contents of the GPUCommand. Keep + * in mind that integers might be shown in their binary float representation, + * depending on the setting in the GPUCommand::convert_int_to_float method. + */ +void GPUCommand::write(ostream &out) const { + out << "GPUCommand(type=" << _command_type << ", size=" << _current_index << ", data = {" << endl; + for (size_t k = 0; k < GPU_COMMAND_ENTRIES; ++k) { + out << std::setw(12) << std::fixed << std::setprecision(5) << _data[k] << " "; + if (k % 6 == 5 || k == GPU_COMMAND_ENTRIES - 1) out << endl; + } + out << "})" << endl; +} + +/** + * @brief Writes the GPU command to a given target. + * @details This method writes all the data of the GPU command to a given target. + * The target should be a pointer to memory being big enough to hold the + * data. Presumably #dest will be a handle to texture memory. + * The command_index controls the offset where the data will be written + * to. + * + * @param dest Handle to the memory to write the command to + * @param command_index Offset to write the command to. The command will write + * its data to command_index * GPU_COMMAND_ENTRIES. When writing + * the GPUCommand in a GPUCommandList, the command_index will + * most likely be the index of the command in the list. + */ +void GPUCommand::write_to(const PTA_uchar &dest, size_t command_index) { + size_t command_size = GPU_COMMAND_ENTRIES * sizeof(float); + size_t offset = command_index * command_size; + memcpy(dest.p() + offset, &_data, command_size); +} diff --git a/RenderPipelineFile/rpcore/native/source/gpu_command.h b/RenderPipelineFile/rpcore/native/source/gpu_command.h index 4275e4f9..69a9ecb0 100644 --- a/RenderPipelineFile/rpcore/native/source/gpu_command.h +++ b/RenderPipelineFile/rpcore/native/source/gpu_command.h @@ -1,92 +1,92 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#ifndef RP_GPU_COMMAND_H -#define RP_GPU_COMMAND_H - -#include "pandabase.h" -#include "luse.h" - -NotifyCategoryDecl(gpucommand, EXPORT_CLASS, EXPORT_TEMPL); - -#define GPU_COMMAND_ENTRIES 32 - -// Packs integers by storing their binary representation in floats -// This only works if the command and light buffer is 32bit floating point. -#define PACK_INT_AS_FLOAT 0 - - -/** - * @brief Class for storing data to be transferred to the GPU. - * @details This class can be seen like a packet, to be transferred to the GPU. - * It has a command type, which tells the GPU what to do once it recieved this - * "packet". It stores a limited amount of floating point components. - */ -class GPUCommand { - - PUBLISHED: - - /** - * The different types of GPUCommands. Each type has a special case in - * the command queue processor. When adding new types, those need to - * be handled in the command target, too. - */ - enum CommandType { - CMD_invalid = 0, - CMD_store_light = 1, - CMD_remove_light = 2, - CMD_store_source = 3, - CMD_remove_sources = 4, - }; - - GPUCommand(CommandType command_type); - - inline void push_int(int v); - inline void push_float(float v); - inline void push_vec3(const LVecBase3f &v); - inline void push_vec3(const LVecBase3i &v); - inline void push_vec4(const LVecBase4f &v); - inline void push_vec4(const LVecBase4i &v); - inline void push_mat3(const LMatrix3f &v); - inline void push_mat4(const LMatrix4f &v); - - inline static bool get_uses_integer_packing(); - - void write_to(const PTA_uchar &dest, size_t command_index); - void write(ostream &out) const; - - private: - - inline float convert_int_to_float(int v) const; - - CommandType _command_type; - size_t _current_index; - float _data[GPU_COMMAND_ENTRIES]; -}; - -#include "gpu_command.I" - -#endif // RP_GPU_COMMAND_H +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef RP_GPU_COMMAND_H +#define RP_GPU_COMMAND_H + +#include "pandabase.h" +#include "luse.h" + +NotifyCategoryDecl(gpucommand, EXPORT_CLASS, EXPORT_TEMPL); + +#define GPU_COMMAND_ENTRIES 32 + +// Packs integers by storing their binary representation in floats +// This only works if the command and light buffer is 32bit floating point. +#define PACK_INT_AS_FLOAT 0 + + +/** + * @brief Class for storing data to be transferred to the GPU. + * @details This class can be seen like a packet, to be transferred to the GPU. + * It has a command type, which tells the GPU what to do once it recieved this + * "packet". It stores a limited amount of floating point components. + */ +class GPUCommand { + + PUBLISHED: + + /** + * The different types of GPUCommands. Each type has a special case in + * the command queue processor. When adding new types, those need to + * be handled in the command target, too. + */ + enum CommandType { + CMD_invalid = 0, + CMD_store_light = 1, + CMD_remove_light = 2, + CMD_store_source = 3, + CMD_remove_sources = 4, + }; + + GPUCommand(CommandType command_type); + + inline void push_int(int v); + inline void push_float(float v); + inline void push_vec3(const LVecBase3f &v); + inline void push_vec3(const LVecBase3i &v); + inline void push_vec4(const LVecBase4f &v); + inline void push_vec4(const LVecBase4i &v); + inline void push_mat3(const LMatrix3f &v); + inline void push_mat4(const LMatrix4f &v); + + inline static bool get_uses_integer_packing(); + + void write_to(const PTA_uchar &dest, size_t command_index); + void write(ostream &out) const; + + private: + + inline float convert_int_to_float(int v) const; + + CommandType _command_type; + size_t _current_index; + float _data[GPU_COMMAND_ENTRIES]; +}; + +#include "gpu_command.I" + +#endif // RP_GPU_COMMAND_H diff --git a/RenderPipelineFile/rpcore/native/source/gpu_command_list.cpp b/RenderPipelineFile/rpcore/native/source/gpu_command_list.cpp index bf8e6838..e3bd5e81 100644 --- a/RenderPipelineFile/rpcore/native/source/gpu_command_list.cpp +++ b/RenderPipelineFile/rpcore/native/source/gpu_command_list.cpp @@ -1,82 +1,82 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#include "gpu_command_list.h" - - -/** - * @brief Constructs a new GPUCommandList - * @details This constructs a new GPUCommandList. By default, there are no commands - * in the list. - */ -GPUCommandList::GPUCommandList() { -} - -/** - * @brief Pushes a GPUCommand to the command list. - * @details This adds a new GPUCommand to the list of commands to be processed. - * - * @param cmd The command to add - */ -void GPUCommandList::add_command(const GPUCommand& cmd) { - _commands.push(cmd); -} - -/** - * @brief Returns the number of commands in this list. - * @details This returns the amount of commands which are currently stored in this - * list, and are waiting to get processed. - * @return Amount of commands - */ -size_t GPUCommandList::get_num_commands() { - return _commands.size(); -} - -/** - * @brief Writes the first n-commands to a destination. - * @details This takes the first #limit commands, and writes them to the - * destination using GPUCommand::write_to. See GPUCommand::write_to for - * further information about #dest. The limit controls after how much - * commands the processing will be stopped. All commands which got processed - * will get removed from the list. - * - * @param dest Destination to write to, see GPUCommand::write_to - * @param limit Maximum amount of commands to process - * - * @return Amount of commands processed, between 0 and #limit. - */ -size_t GPUCommandList::write_commands_to(const PTA_uchar &dest, size_t limit) { - size_t num_commands_written = 0; - - while (num_commands_written < limit && !_commands.empty()) { - // Write the first command to the stream, and delete it afterwards - _commands.front().write_to(dest, num_commands_written); - _commands.pop(); - num_commands_written ++; - } - - return num_commands_written; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "gpu_command_list.h" + + +/** + * @brief Constructs a new GPUCommandList + * @details This constructs a new GPUCommandList. By default, there are no commands + * in the list. + */ +GPUCommandList::GPUCommandList() { +} + +/** + * @brief Pushes a GPUCommand to the command list. + * @details This adds a new GPUCommand to the list of commands to be processed. + * + * @param cmd The command to add + */ +void GPUCommandList::add_command(const GPUCommand& cmd) { + _commands.push(cmd); +} + +/** + * @brief Returns the number of commands in this list. + * @details This returns the amount of commands which are currently stored in this + * list, and are waiting to get processed. + * @return Amount of commands + */ +size_t GPUCommandList::get_num_commands() { + return _commands.size(); +} + +/** + * @brief Writes the first n-commands to a destination. + * @details This takes the first #limit commands, and writes them to the + * destination using GPUCommand::write_to. See GPUCommand::write_to for + * further information about #dest. The limit controls after how much + * commands the processing will be stopped. All commands which got processed + * will get removed from the list. + * + * @param dest Destination to write to, see GPUCommand::write_to + * @param limit Maximum amount of commands to process + * + * @return Amount of commands processed, between 0 and #limit. + */ +size_t GPUCommandList::write_commands_to(const PTA_uchar &dest, size_t limit) { + size_t num_commands_written = 0; + + while (num_commands_written < limit && !_commands.empty()) { + // Write the first command to the stream, and delete it afterwards + _commands.front().write_to(dest, num_commands_written); + _commands.pop(); + num_commands_written ++; + } + + return num_commands_written; +} diff --git a/RenderPipelineFile/rpcore/native/source/gpu_command_list.h b/RenderPipelineFile/rpcore/native/source/gpu_command_list.h index 18519045..5e458f8e 100644 --- a/RenderPipelineFile/rpcore/native/source/gpu_command_list.h +++ b/RenderPipelineFile/rpcore/native/source/gpu_command_list.h @@ -1,55 +1,55 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#ifndef RP_GPU_COMMAND_LIST_H -#define RP_GPU_COMMAND_LIST_H - -#include "pandabase.h" -#include "gpu_command.h" - -#include - -/** - * @brief Class to store a list of commands. - * @details This is a class to store a list of GPUCommands. It provides - * functionality to only provide the a given amount of commands at one time. - */ -class GPUCommandList { - - PUBLISHED: - GPUCommandList(); - - void add_command(const GPUCommand& cmd); - size_t get_num_commands(); - size_t write_commands_to(const PTA_uchar &dest, size_t limit = 32); - - MAKE_PROPERTY(num_commands, get_num_commands); - - protected: - queue _commands; -}; - -#endif // RP_GPU_COMMAND_LIST_H +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef RP_GPU_COMMAND_LIST_H +#define RP_GPU_COMMAND_LIST_H + +#include "pandabase.h" +#include "gpu_command.h" + +#include + +/** + * @brief Class to store a list of commands. + * @details This is a class to store a list of GPUCommands. It provides + * functionality to only provide the a given amount of commands at one time. + */ +class GPUCommandList { + + PUBLISHED: + GPUCommandList(); + + void add_command(const GPUCommand& cmd); + size_t get_num_commands(); + size_t write_commands_to(const PTA_uchar &dest, size_t limit = 32); + + MAKE_PROPERTY(num_commands, get_num_commands); + + protected: + queue _commands; +}; + +#endif // RP_GPU_COMMAND_LIST_H diff --git a/RenderPipelineFile/rpcore/native/source/ies_dataset.cpp b/RenderPipelineFile/rpcore/native/source/ies_dataset.cpp index c658cb5e..ed9077db 100644 --- a/RenderPipelineFile/rpcore/native/source/ies_dataset.cpp +++ b/RenderPipelineFile/rpcore/native/source/ies_dataset.cpp @@ -1,233 +1,233 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -#include "ies_dataset.h" - -#define _USE_MATH_DEFINES -#include - -NotifyCategoryDef(iesdataset, "") - -/** - * @brief Constructs a new empty dataset. - * @details This constructs a new IESDataset with no data set. - */ -IESDataset::IESDataset() { -} - -/** - * @brief Sets the vertical angles of the dataset. - * @details This sets the list of vertical angles of the dataset. - * - * @param vertical_angles Vector of all vertical angles. - */ -void IESDataset::set_vertical_angles(const PTA_float &vertical_angles) { - nassertv(vertical_angles.size() > 0); - _vertical_angles = vertical_angles; -} - -/** - * @brief Sets the horizontal angles of the dataset. - * @details This sets the list of horizontal angles of the dataset. - * - * @param horizontal_angles Vector of all horizontal angles. - */ -void IESDataset::set_horizontal_angles(const PTA_float &horizontal_angles) { - nassertv(horizontal_angles.size() > 0); - _horizontal_angles = horizontal_angles; -} - -/** - * @brief Sets the candela values. - * @details This sets the candela values of the dataset. They should be an - * interleaved 2D array with the dimensions vertical_angles x horizontal_angles. - * They also should be normalized by dividing by the maximum entry. - * @param candela_values Interleaved 2D-vector of candela values. - */ -void IESDataset::set_candela_values(const PTA_float &candela_values) { - nassertv(candela_values.size() == _horizontal_angles.size() * _vertical_angles.size()); - _candela_values = candela_values; -} - -/** - * @brief Internal method to access the candela data. - * @details This lookups a candela value in the candela values. It converts a - * two dimensional index to a onedimensional index and then returns the candela - * value at that position. - * - * @param vertical_angle_idx Index of the vertical angle - * @param horizontal_angle_idx Index of the horizontal angle - * - * @return Candela value between 0 .. 1 - */ -float IESDataset::get_candela_value_from_index(size_t vertical_angle_idx, size_t horizontal_angle_idx) const { - size_t index = vertical_angle_idx + horizontal_angle_idx * _vertical_angles.size(); - nassertr(index >= 0 && index < _candela_values.size(), 0.0); - return _candela_values[index]; -} - -/** - * @brief Samples the dataset at the given position - * @details This looks up a value in the dataset, by specifying a horizontal and - * vertical angle. This is used for generating the LUT. The vertical and horizontal - * angle should be inside of the bounds of the vertical and horizontal angle arrays. - * - * @param vertical_angle Vertical angle, from 0 .. 90 or 0 .. 180 depending on the dataset - * @param horizontal_angle Horizontal angle, from 0 .. 180 or 0 .. 360 depending on the dataset. - * - * @return Candela value between 0 .. 1 - */ -float IESDataset::get_candela_value(float vertical_angle, float horizontal_angle) const { - - // Special case for datasets without horizontal angles - if (_horizontal_angles.size() == 1) { - return get_vertical_candela_value(0, vertical_angle); - } - - float max_angle = _horizontal_angles[_horizontal_angles.size() - 1]; - - // Wrap angle to fit from 0 .. 360 degree. Most profiles only distribute - // candela values from 0 .. 180 or even 0 .. 90. We have to mirror the - // values at those borders (so 2 times for 180 degree and 4 times for 90 degree) - horizontal_angle = fmod(horizontal_angle, 2.0f * max_angle); - if (horizontal_angle > max_angle) { - horizontal_angle = 2.0 * max_angle - horizontal_angle; - } - - // Simlar to the vertical step, we now try interpolating a horizontal angle, - // but we need to evaluate the vertical value for each row instead of fetching - // the value directly - for (size_t horizontal_index = 1; horizontal_index < _horizontal_angles.size(); ++horizontal_index) { - float curr_angle = _horizontal_angles[horizontal_index]; - - if (curr_angle >= horizontal_angle) { - - // Get previous angle data - float prev_angle = _horizontal_angles[horizontal_index - 1]; - float prev_value = get_vertical_candela_value(horizontal_index - 1, vertical_angle); - float curr_value = get_vertical_candela_value(horizontal_index, vertical_angle); - - // Interpolate lineary - float lerp = (horizontal_angle - prev_angle) / (curr_angle - prev_angle); - - // Should never occur, but to be safe: - if (lerp < 0.0 || lerp > 1.0) { - iesdataset_cat.error() << "Invalid horizontal lerp: " << lerp - << ", requested angle was " << horizontal_angle - << ", prev = " << prev_angle << ", cur = " << curr_angle - << endl; - } - - return curr_value * lerp + prev_value * (1-lerp); - } - } - - return 0.0; -} - -/** - * @brief Fetches a vertical candela value - * @details Fetches a vertical candela value, using a given horizontal position. - * This does an 1D interpolation in the candela values array. - * - * @param horizontal_angle_idx The index of the horizontal angle in the horizontal - * angle array. - * @param vertical_angle The vertical angle. Interpolation will be done if the - * vertical angle is not in the vertical angles array. - * - * @return Candela value between 0 .. 1 - */ -float IESDataset::get_vertical_candela_value(size_t horizontal_angle_idx, float vertical_angle) const { - nassertr(horizontal_angle_idx >= 0 && horizontal_angle_idx < _horizontal_angles.size(), 0.0); - - // Lower bound - if (vertical_angle < 0.0) return 0.0; - - // Upper bound - if (vertical_angle > _vertical_angles[_vertical_angles.size() - 1] ) return 0.0; - - // Find lowest enclosing angle - for (size_t vertical_index = 1; vertical_index < _vertical_angles.size(); ++vertical_index) { - float curr_angle = _vertical_angles[vertical_index]; - - // Found value - if (curr_angle > vertical_angle) { - - // Get previous angle data - float prev_angle = _vertical_angles[vertical_index - 1]; - float prev_value = get_candela_value_from_index(vertical_index - 1, horizontal_angle_idx); - float curr_value = get_candela_value_from_index(vertical_index, horizontal_angle_idx); - - // Interpolate lineary - float lerp = (vertical_angle - prev_angle) / (curr_angle - prev_angle); - - // Should never occur, but to be safe: - if (lerp < 0.0 || lerp > 1.0) { - iesdataset_cat.error() << "ERROR: Invalid vertical lerp: " << lerp - << ", requested angle was " << vertical_angle - << ", prev = " << prev_angle << ", cur = " << curr_angle - << endl; - } - - return curr_value * lerp + prev_value * (1-lerp); - } - } - return 0.0; -} - -/** - * @brief Generates the IES LUT - * @details This generates the LUT into a given dataset texture. The x-axis - * referes to the vertical_angle, whereas the y-axis refers to the - * horizontal angle. - * - * @param dest_tex Texture to write the LUT into - * @param z Layer to write the LUT into, in case the texture is a 3D Texture or - * 2D Texture Array. - */ -void IESDataset::generate_dataset_texture_into(Texture* dest_tex, size_t z) const { - - size_t resolution_vertical = dest_tex->get_y_size(); - size_t resolution_horizontal = dest_tex->get_x_size(); - - // Candla values are stored flippped - vertical angles in the x - Axis - // and horizontal angles in the y - Axis - PNMImage dest = PNMImage(resolution_vertical, resolution_horizontal, 1, 65535); - - for (size_t vert = 0; vert < resolution_vertical; ++vert) { - for (size_t horiz = 0; horiz < resolution_horizontal; ++horiz) { - float vert_angle = (float)vert / (float)(resolution_vertical-1); - vert_angle = cos(vert_angle * M_PI) * 90.0 + 90.0; - float horiz_angle = (float)horiz / (float)(resolution_horizontal-1) * 360.0; - float candela = get_candela_value(vert_angle, horiz_angle); - dest.set_xel(vert, horiz, candela); - } - } - - - dest_tex->load(dest, z, 0); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +#include "ies_dataset.h" + +#define _USE_MATH_DEFINES +#include + +NotifyCategoryDef(iesdataset, "") + +/** + * @brief Constructs a new empty dataset. + * @details This constructs a new IESDataset with no data set. + */ +IESDataset::IESDataset() { +} + +/** + * @brief Sets the vertical angles of the dataset. + * @details This sets the list of vertical angles of the dataset. + * + * @param vertical_angles Vector of all vertical angles. + */ +void IESDataset::set_vertical_angles(const PTA_float &vertical_angles) { + nassertv(vertical_angles.size() > 0); + _vertical_angles = vertical_angles; +} + +/** + * @brief Sets the horizontal angles of the dataset. + * @details This sets the list of horizontal angles of the dataset. + * + * @param horizontal_angles Vector of all horizontal angles. + */ +void IESDataset::set_horizontal_angles(const PTA_float &horizontal_angles) { + nassertv(horizontal_angles.size() > 0); + _horizontal_angles = horizontal_angles; +} + +/** + * @brief Sets the candela values. + * @details This sets the candela values of the dataset. They should be an + * interleaved 2D array with the dimensions vertical_angles x horizontal_angles. + * They also should be normalized by dividing by the maximum entry. + * @param candela_values Interleaved 2D-vector of candela values. + */ +void IESDataset::set_candela_values(const PTA_float &candela_values) { + nassertv(candela_values.size() == _horizontal_angles.size() * _vertical_angles.size()); + _candela_values = candela_values; +} + +/** + * @brief Internal method to access the candela data. + * @details This lookups a candela value in the candela values. It converts a + * two dimensional index to a onedimensional index and then returns the candela + * value at that position. + * + * @param vertical_angle_idx Index of the vertical angle + * @param horizontal_angle_idx Index of the horizontal angle + * + * @return Candela value between 0 .. 1 + */ +float IESDataset::get_candela_value_from_index(size_t vertical_angle_idx, size_t horizontal_angle_idx) const { + size_t index = vertical_angle_idx + horizontal_angle_idx * _vertical_angles.size(); + nassertr(index >= 0 && index < _candela_values.size(), 0.0); + return _candela_values[index]; +} + +/** + * @brief Samples the dataset at the given position + * @details This looks up a value in the dataset, by specifying a horizontal and + * vertical angle. This is used for generating the LUT. The vertical and horizontal + * angle should be inside of the bounds of the vertical and horizontal angle arrays. + * + * @param vertical_angle Vertical angle, from 0 .. 90 or 0 .. 180 depending on the dataset + * @param horizontal_angle Horizontal angle, from 0 .. 180 or 0 .. 360 depending on the dataset. + * + * @return Candela value between 0 .. 1 + */ +float IESDataset::get_candela_value(float vertical_angle, float horizontal_angle) const { + + // Special case for datasets without horizontal angles + if (_horizontal_angles.size() == 1) { + return get_vertical_candela_value(0, vertical_angle); + } + + float max_angle = _horizontal_angles[_horizontal_angles.size() - 1]; + + // Wrap angle to fit from 0 .. 360 degree. Most profiles only distribute + // candela values from 0 .. 180 or even 0 .. 90. We have to mirror the + // values at those borders (so 2 times for 180 degree and 4 times for 90 degree) + horizontal_angle = fmod(horizontal_angle, 2.0f * max_angle); + if (horizontal_angle > max_angle) { + horizontal_angle = 2.0 * max_angle - horizontal_angle; + } + + // Simlar to the vertical step, we now try interpolating a horizontal angle, + // but we need to evaluate the vertical value for each row instead of fetching + // the value directly + for (size_t horizontal_index = 1; horizontal_index < _horizontal_angles.size(); ++horizontal_index) { + float curr_angle = _horizontal_angles[horizontal_index]; + + if (curr_angle >= horizontal_angle) { + + // Get previous angle data + float prev_angle = _horizontal_angles[horizontal_index - 1]; + float prev_value = get_vertical_candela_value(horizontal_index - 1, vertical_angle); + float curr_value = get_vertical_candela_value(horizontal_index, vertical_angle); + + // Interpolate lineary + float lerp = (horizontal_angle - prev_angle) / (curr_angle - prev_angle); + + // Should never occur, but to be safe: + if (lerp < 0.0 || lerp > 1.0) { + iesdataset_cat.error() << "Invalid horizontal lerp: " << lerp + << ", requested angle was " << horizontal_angle + << ", prev = " << prev_angle << ", cur = " << curr_angle + << endl; + } + + return curr_value * lerp + prev_value * (1-lerp); + } + } + + return 0.0; +} + +/** + * @brief Fetches a vertical candela value + * @details Fetches a vertical candela value, using a given horizontal position. + * This does an 1D interpolation in the candela values array. + * + * @param horizontal_angle_idx The index of the horizontal angle in the horizontal + * angle array. + * @param vertical_angle The vertical angle. Interpolation will be done if the + * vertical angle is not in the vertical angles array. + * + * @return Candela value between 0 .. 1 + */ +float IESDataset::get_vertical_candela_value(size_t horizontal_angle_idx, float vertical_angle) const { + nassertr(horizontal_angle_idx >= 0 && horizontal_angle_idx < _horizontal_angles.size(), 0.0); + + // Lower bound + if (vertical_angle < 0.0) return 0.0; + + // Upper bound + if (vertical_angle > _vertical_angles[_vertical_angles.size() - 1] ) return 0.0; + + // Find lowest enclosing angle + for (size_t vertical_index = 1; vertical_index < _vertical_angles.size(); ++vertical_index) { + float curr_angle = _vertical_angles[vertical_index]; + + // Found value + if (curr_angle > vertical_angle) { + + // Get previous angle data + float prev_angle = _vertical_angles[vertical_index - 1]; + float prev_value = get_candela_value_from_index(vertical_index - 1, horizontal_angle_idx); + float curr_value = get_candela_value_from_index(vertical_index, horizontal_angle_idx); + + // Interpolate lineary + float lerp = (vertical_angle - prev_angle) / (curr_angle - prev_angle); + + // Should never occur, but to be safe: + if (lerp < 0.0 || lerp > 1.0) { + iesdataset_cat.error() << "ERROR: Invalid vertical lerp: " << lerp + << ", requested angle was " << vertical_angle + << ", prev = " << prev_angle << ", cur = " << curr_angle + << endl; + } + + return curr_value * lerp + prev_value * (1-lerp); + } + } + return 0.0; +} + +/** + * @brief Generates the IES LUT + * @details This generates the LUT into a given dataset texture. The x-axis + * referes to the vertical_angle, whereas the y-axis refers to the + * horizontal angle. + * + * @param dest_tex Texture to write the LUT into + * @param z Layer to write the LUT into, in case the texture is a 3D Texture or + * 2D Texture Array. + */ +void IESDataset::generate_dataset_texture_into(Texture* dest_tex, size_t z) const { + + size_t resolution_vertical = dest_tex->get_y_size(); + size_t resolution_horizontal = dest_tex->get_x_size(); + + // Candla values are stored flippped - vertical angles in the x - Axis + // and horizontal angles in the y - Axis + PNMImage dest = PNMImage(resolution_vertical, resolution_horizontal, 1, 65535); + + for (size_t vert = 0; vert < resolution_vertical; ++vert) { + for (size_t horiz = 0; horiz < resolution_horizontal; ++horiz) { + float vert_angle = (float)vert / (float)(resolution_vertical-1); + vert_angle = cos(vert_angle * M_PI) * 90.0 + 90.0; + float horiz_angle = (float)horiz / (float)(resolution_horizontal-1) * 360.0; + float candela = get_candela_value(vert_angle, horiz_angle); + dest.set_xel(vert, horiz, candela); + } + } + + + dest_tex->load(dest, z, 0); +} diff --git a/RenderPipelineFile/rpcore/native/source/ies_dataset.h b/RenderPipelineFile/rpcore/native/source/ies_dataset.h index 36a11eee..e86203ee 100644 --- a/RenderPipelineFile/rpcore/native/source/ies_dataset.h +++ b/RenderPipelineFile/rpcore/native/source/ies_dataset.h @@ -1,66 +1,66 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -#include "pandabase.h" -#include "pta_float.h" -#include "pointerToArray.h" -#include "texture.h" -#include "pnmImage.h" - -NotifyCategoryDecl(iesdataset, EXPORT_CLASS, EXPORT_TEMPL); - - -/** - * @brief This class generates a LUT from IES data. - * @details This class is used by the IESLoader to generate a LUT texture which - * is used in the shaders to perform IES lighting. It takes a set of vertical - * and horizontal angles, as well as a set of candela values, which then are - * lineary interpolated onto a 2D LUT Texture. - */ -class IESDataset { - - PUBLISHED: - IESDataset(); - - void set_vertical_angles(const PTA_float &vertical_angles); - void set_horizontal_angles(const PTA_float &horizontal_angles); - void set_candela_values(const PTA_float &candela_values); - - void generate_dataset_texture_into(Texture* dest_tex, size_t z) const; - - public: - - float get_candela_value(float vertical_angle, float horizontal_angle) const; - float get_candela_value_from_index(size_t vertical_angle_idx, size_t horizontal_angle_idx) const; - float get_vertical_candela_value(size_t horizontal_angle_idx, float vertical_angle) const; - - private: - PTA_float _vertical_angles; - PTA_float _horizontal_angles; - PTA_float _candela_values; -}; +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +#include "pandabase.h" +#include "pta_float.h" +#include "pointerToArray.h" +#include "texture.h" +#include "pnmImage.h" + +NotifyCategoryDecl(iesdataset, EXPORT_CLASS, EXPORT_TEMPL); + + +/** + * @brief This class generates a LUT from IES data. + * @details This class is used by the IESLoader to generate a LUT texture which + * is used in the shaders to perform IES lighting. It takes a set of vertical + * and horizontal angles, as well as a set of candela values, which then are + * lineary interpolated onto a 2D LUT Texture. + */ +class IESDataset { + + PUBLISHED: + IESDataset(); + + void set_vertical_angles(const PTA_float &vertical_angles); + void set_horizontal_angles(const PTA_float &horizontal_angles); + void set_candela_values(const PTA_float &candela_values); + + void generate_dataset_texture_into(Texture* dest_tex, size_t z) const; + + public: + + float get_candela_value(float vertical_angle, float horizontal_angle) const; + float get_candela_value_from_index(size_t vertical_angle_idx, size_t horizontal_angle_idx) const; + float get_vertical_candela_value(size_t horizontal_angle_idx, float vertical_angle) const; + + private: + PTA_float _vertical_angles; + PTA_float _horizontal_angles; + PTA_float _candela_values; +}; diff --git a/RenderPipelineFile/rpcore/native/source/internal_light_manager.I b/RenderPipelineFile/rpcore/native/source/internal_light_manager.I index 5d9f7ee9..3d2a86e2 100644 --- a/RenderPipelineFile/rpcore/native/source/internal_light_manager.I +++ b/RenderPipelineFile/rpcore/native/source/internal_light_manager.I @@ -1,140 +1,140 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -/** - * @brief Returns the maximum light index - * @details This returns the maximum light index (also called slot). Any lights - * after that slot are guaranteed to be zero-lights. This is useful when - * iterating over the list of lights, because iteration can be stopped when - * the maximum light index is reached. - * - * The maximum light index points to the last slot which is used. If no lights - * are attached, -1 is returned. If one light is attached at slot 0, the index - * is 0, if two are attached at the slots 0 and 1, the index is 1, and so on. - * - * If, for example, two lights are attached at the slots 2 and 5, then the - * index will be 5. Keep in mind that the max-index is not an indicator for - * how many lights are attached. Also, zero lights still may occur when iterating - * over the light lists - * - * @return Maximum light index - */ -inline int InternalLightManager::get_max_light_index() const { - return _lights.get_max_index(); -} - -/** - * @brief Returns the amount of stored lights. - * @details This returns the amount of stored lights. This behaves unlike - * InternalLightManager::get_max_light_index, and instead returns the true - * amount of lights, which is completely unrelated to the amount of used slots. - * - * @return Amount of stored lights - */ -inline size_t InternalLightManager::get_num_lights() const { - return _lights.get_num_entries(); -} - -/** - * @brief Returns the amount of shadow sources. - * @details This returns the total amount of stored shadow sources. This does - * not denote the amount of updated sources, but instead takes into account - * all sources, even those out of frustum. - * @return Amount of shadow sources. - */ -inline size_t InternalLightManager::get_num_shadow_sources() const { - return _shadow_sources.get_num_entries(); -} - -/** - * @brief Sets the handle to the shadow manager - * @details This sets the handle to the global shadow manager. It is usually - * constructed on the python side, so we need to get a handle to it. - * - * The manager should be a handle to a ShadowManager instance, and will be - * stored somewhere on the python side most likely. The light manager does not - * keep a reference to it, so the python side should make sure to keep one. - * - * Be sure to call this before the InternalLightManager::update() method is - * called, otherwise an assertion will get triggered. - * - * @param mgr The ShadowManager instance - */ -inline void InternalLightManager::set_shadow_manager(ShadowManager* mgr) { - _shadow_manager = mgr; -} - -/** - * @brief Sets a handle to the command list - * @details This sets a handle to the global GPUCommandList. This is required to - * emit GPUCommands, which are used for attaching and detaching lights, as well - * as shadow source updates. - * - * The cmd_list should be a handle to a GPUCommandList handle, and will be - * stored somewhere on the python side most likely. The light manager does not - * keep a reference to it, so the python side should make sure to keep one. - * - * Be sure to call this before the InternalLightManager::update() method is - * called, otherwise an assertion will get triggered. - * - * @param cmd_list The GPUCommandList instance - */ -inline void InternalLightManager::set_command_list(GPUCommandList *cmd_list) { - _cmd_list = cmd_list; -} - -/** - * @brief Sets the camera position - * @details This sets the camera position, which will be used to determine which - * shadow sources have to get updated - * - * @param mat View projection mat - */ -inline void InternalLightManager::set_camera_pos(const LPoint3f& pos) { - _camera_pos = pos; -} - -/** - * @brief Sets the maximum shadow update distance - * @details This controls the maximum distance until which shadows are updated. - * If a shadow source is past that distance, it is ignored and no longer recieves - * updates until it is in range again - * - * @param dist Distance in world space units - */ -inline void InternalLightManager::set_shadow_update_distance(float dist) { - _shadow_update_distance = dist; -} - -/** - * @brief Returns the internal used ShadowManager - * @details This returns a handle to the internally used shadow manager - * @return Shadow manager - */ -inline ShadowManager* InternalLightManager::get_shadow_manager() const { - return _shadow_manager; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +/** + * @brief Returns the maximum light index + * @details This returns the maximum light index (also called slot). Any lights + * after that slot are guaranteed to be zero-lights. This is useful when + * iterating over the list of lights, because iteration can be stopped when + * the maximum light index is reached. + * + * The maximum light index points to the last slot which is used. If no lights + * are attached, -1 is returned. If one light is attached at slot 0, the index + * is 0, if two are attached at the slots 0 and 1, the index is 1, and so on. + * + * If, for example, two lights are attached at the slots 2 and 5, then the + * index will be 5. Keep in mind that the max-index is not an indicator for + * how many lights are attached. Also, zero lights still may occur when iterating + * over the light lists + * + * @return Maximum light index + */ +inline int InternalLightManager::get_max_light_index() const { + return _lights.get_max_index(); +} + +/** + * @brief Returns the amount of stored lights. + * @details This returns the amount of stored lights. This behaves unlike + * InternalLightManager::get_max_light_index, and instead returns the true + * amount of lights, which is completely unrelated to the amount of used slots. + * + * @return Amount of stored lights + */ +inline size_t InternalLightManager::get_num_lights() const { + return _lights.get_num_entries(); +} + +/** + * @brief Returns the amount of shadow sources. + * @details This returns the total amount of stored shadow sources. This does + * not denote the amount of updated sources, but instead takes into account + * all sources, even those out of frustum. + * @return Amount of shadow sources. + */ +inline size_t InternalLightManager::get_num_shadow_sources() const { + return _shadow_sources.get_num_entries(); +} + +/** + * @brief Sets the handle to the shadow manager + * @details This sets the handle to the global shadow manager. It is usually + * constructed on the python side, so we need to get a handle to it. + * + * The manager should be a handle to a ShadowManager instance, and will be + * stored somewhere on the python side most likely. The light manager does not + * keep a reference to it, so the python side should make sure to keep one. + * + * Be sure to call this before the InternalLightManager::update() method is + * called, otherwise an assertion will get triggered. + * + * @param mgr The ShadowManager instance + */ +inline void InternalLightManager::set_shadow_manager(ShadowManager* mgr) { + _shadow_manager = mgr; +} + +/** + * @brief Sets a handle to the command list + * @details This sets a handle to the global GPUCommandList. This is required to + * emit GPUCommands, which are used for attaching and detaching lights, as well + * as shadow source updates. + * + * The cmd_list should be a handle to a GPUCommandList handle, and will be + * stored somewhere on the python side most likely. The light manager does not + * keep a reference to it, so the python side should make sure to keep one. + * + * Be sure to call this before the InternalLightManager::update() method is + * called, otherwise an assertion will get triggered. + * + * @param cmd_list The GPUCommandList instance + */ +inline void InternalLightManager::set_command_list(GPUCommandList *cmd_list) { + _cmd_list = cmd_list; +} + +/** + * @brief Sets the camera position + * @details This sets the camera position, which will be used to determine which + * shadow sources have to get updated + * + * @param mat View projection mat + */ +inline void InternalLightManager::set_camera_pos(const LPoint3f& pos) { + _camera_pos = pos; +} + +/** + * @brief Sets the maximum shadow update distance + * @details This controls the maximum distance until which shadows are updated. + * If a shadow source is past that distance, it is ignored and no longer recieves + * updates until it is in range again + * + * @param dist Distance in world space units + */ +inline void InternalLightManager::set_shadow_update_distance(float dist) { + _shadow_update_distance = dist; +} + +/** + * @brief Returns the internal used ShadowManager + * @details This returns a handle to the internally used shadow manager + * @return Shadow manager + */ +inline ShadowManager* InternalLightManager::get_shadow_manager() const { + return _shadow_manager; +} diff --git a/RenderPipelineFile/rpcore/native/source/internal_light_manager.cpp b/RenderPipelineFile/rpcore/native/source/internal_light_manager.cpp index 53e39c30..8d829ba1 100644 --- a/RenderPipelineFile/rpcore/native/source/internal_light_manager.cpp +++ b/RenderPipelineFile/rpcore/native/source/internal_light_manager.cpp @@ -1,441 +1,441 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -#include "internal_light_manager.h" - -#include - -NotifyCategoryDef(lightmgr, ""); - - -/** - * @brief Constructs the light manager - * @details This constructs the light manager, initializing the light and shadow - * storage. You should set a command list and shadow manager before calling - * InternalLightManager::update. s - */ -InternalLightManager::InternalLightManager() { - _shadow_update_distance = 100.0f; - _cmd_list = NULL; - _shadow_manager = NULL; -} - -/** - * @brief Adds a new light. - * @details This adds a new light to the list of lights. This will throw an - * error and return if the light is already attached. You may only call - * this after the ShadowManager was already set. - * - * While the light is attached, the light manager keeps a reference to it, so - * the light does not get destructed. - * - * This also setups the shadows on the light, in case shadows are enabled. - * While a light is attached, you can not change whether it casts shadows or not. - * To do so, detach the light, change the setting, and re-add the light. - * - * In case no free light slot is available, an error will be printed and no - * action will be performed. - * - * If no shadow manager was set, an assertion will be triggered. - * - * @param light The light to add. - */ -void InternalLightManager::add_light(PT(RPLight) light) { - nassertv(_shadow_manager != NULL); // Shadow manager not set yet! - - // Don't attach the light in case its already attached - if (light->has_slot()) { - lightmgr_cat.error() << "could not add light because it already is attached! " - << "Detach the light first, then try it again." << endl; - return; - } - - // Find a free slot - size_t slot; - if (!_lights.find_slot(slot)) { - lightmgr_cat.error() << "Light limit of " << MAX_LIGHT_COUNT << " reached, " - << "all light slots used!" << endl; - return; - } - - // Reference the light because we store it, to avoid it getting destructed - // on the python side while we still work with it. The reference will be - // removed when the light gets detached. - light->ref(); - - // Reserve the slot - light->assign_slot(slot); - _lights.reserve_slot(slot, light); - - // Setup the shadows in case the light uses them - if (light->get_casts_shadows()) { - setup_shadows(light); - } - - // Store the light on the gpu, to make sure the GPU directly knows about it. - // We could wait until the next update cycle, but then we might be one frame - // too late already. - gpu_update_light(light); -} - -/** - * @brief Internal method to setup shadows for a light - * @details This method gets called by the InternalLightManager::add_light method - * to setup a lights shadow sources, in case shadows are enabled on that light. - * - * It finds a slot for all shadow sources of the ilhgt, and inits the shadow - * sources as well. If no slot could be found, an error is printed an nothing - * happens. - * - * @param light The light to init the shadow sources for - */ -void InternalLightManager::setup_shadows(RPLight* light) { - - // Init the lights shadow sources, and also call update once to make sure - // the sources are properly initialized - light->init_shadow_sources(); - light->update_shadow_sources(); - - // Find consecutive slots, this is important for PointLights so we can just - // store the first index of the source, and get the other slots by doing - // first_index + 1, +2 and so on. - size_t base_slot; - size_t num_sources = light->get_num_shadow_sources(); - if (!_shadow_sources.find_consecutive_slots(base_slot, num_sources)) { - lightmgr_cat.error() << "Failed to find slot for shadow sources! " - << "Shadow-Source limit of " << MAX_SHADOW_SOURCES - << " reached!" << endl; - return; - } - - // Init all sources - for (int i = 0; i < num_sources; ++i) { - ShadowSource* source = light->get_shadow_source(i); - - // Set the source as dirty, so it gets updated in the beginning - source->set_needs_update(true); - - // Assign the slot to the source. Since we got consecutive slots, we can - // just do base_slot + N. - size_t slot = base_slot + i; - _shadow_sources.reserve_slot(slot, source); - source->set_slot(slot); - } -} - -/** - * @brief Removes a light - * @details This detaches a light. This prevents it from being rendered, and also - * cleans up all resources used by that light. If no reference is kept on the - * python side, the light will also get destructed. - * - * If the light was not previously attached with InternalLightManager::add_light, - * an error will be triggered and nothing happens. - * - * In case the light was set to cast shadows, all shadow sources are cleaned - * up, and their regions in the shadow atlas are freed. - * - * All resources used by the light in the light and shadow storage are also - * cleaned up, by emitting cleanup GPUCommands. - * - * If no shadow manager was set, an assertion will be triggered. - * - * @param light [description] - */ -void InternalLightManager::remove_light(PT(RPLight) light) { - nassertv(_shadow_manager != NULL); - - if (!light->has_slot()) { - lightmgr_cat.error() << "Could not detach light, light was not attached!" << endl; - return; - } - - // Free the lights slot in the light storage - _lights.free_slot(light->get_slot()); - - // Tell the GPU we no longer need the lights data - gpu_remove_light(light); - - // Mark the light as detached. After this call, we can not call get_slot - // anymore, so its important we do this after we unregistered the light - // from everywhere. - light->remove_slot(); - - // Clear shadow related stuff, in case the light casts shadows - if (light->get_casts_shadows()) { - - // Free the slots of all sources, and also unregister their regions from - // the shadow atlas. - for (size_t i = 0; i < light->get_num_shadow_sources(); ++i) { - ShadowSource* source = light->get_shadow_source(i); - if (source->has_slot()) { - _shadow_sources.free_slot(source->get_slot()); - } - if (source->has_region()) { - _shadow_manager->get_atlas()->free_region(source->get_region()); - source->clear_region(); - } - } - - // Remove all sources of the light by emitting a consecutive remove command - gpu_remove_consecutive_sources(light->get_shadow_source(0), - light->get_num_shadow_sources()); - - // Finally remove all shadow sources. This is important in case the light - // will be re-attached. Otherwise an assertion will get triggered. - light->clear_shadow_sources(); - } - - // Since we referenced the light when we stored it, we have to decrease - // the reference now. In case no reference was kept on the python side, - // the light will get destructed soon. - light->unref(); -} - -/** - * @brief Internal method to remove consecutive sources from the GPU. - * @details This emits a GPUCommand to consecutively remove shadow sources from - * the GPU. This is called when a light gets removed, to free the space its - * shadow sources took. Its not really required, because as long as the light - * is not used, there is no reference to the sources. However, it can't hurt to - * cleanup the memory. - * - * All sources starting at first_source->get_slot() until - * first_source->get_slot() + num_sources will get cleaned up. - * - * @param first_source First source of the light - * @param num_sources Amount of consecutive sources to clear - */ -void InternalLightManager::gpu_remove_consecutive_sources(ShadowSource *first_source, - size_t num_sources) { - nassertv(_cmd_list != NULL); // No command list set yet - nassertv(first_source->has_slot()); // Source has no slot! - GPUCommand cmd_remove(GPUCommand::CMD_remove_sources); - cmd_remove.push_int(first_source->get_slot()); - cmd_remove.push_int(num_sources); - _cmd_list->add_command(cmd_remove); -} - -/** - * @brief Internal method to remove a light from the GPU. - * @details This emits a GPUCommand to clear a lights data. This sets the data - * to all zeros, marking that no light is stored anymore. - * - * This throws an assertion in case the light is not currently attached. Be - * sure to call this before detaching the light. - * - * @param light The light to remove, must be attached. - */ -void InternalLightManager::gpu_remove_light(RPLight* light) { - nassertv(_cmd_list != NULL); // No command list set yet - nassertv(light->has_slot()); // Light has no slot! - GPUCommand cmd_remove(GPUCommand::CMD_remove_light); - cmd_remove.push_int(light->get_slot()); - _cmd_list->add_command(cmd_remove); -} - -/** - * @brief Updates a lights data on the GPU - * @details This method emits a GPUCommand to update a lights data. This can - * be used to initially store the lights data, or to update the data whenever - * the light changed. - * - * This throws an assertion in case the light is not currently attached. Be - * sure to call this after attaching the light. - * - * @param light The light to update - */ -void InternalLightManager::gpu_update_light(RPLight* light) { - nassertv(_cmd_list != NULL); // No command list set yet - nassertv(light->has_slot()); // Light has no slot! - GPUCommand cmd_update(GPUCommand::CMD_store_light); - cmd_update.push_int(light->get_slot()); - light->write_to_command(cmd_update); - light->set_needs_update(false); - _cmd_list->add_command(cmd_update); -} - -/** - * @brief Updates a shadow source data on the GPU - * @details This emits a GPUCommand to update a given shadow source, storing all - * data of the source on the GPU. This can also be used to initially store a - * ShadowSource, since all data will be overridden. - * - * This throws an assertion if the source has no slot yet. - * - * @param source The source to update - */ -void InternalLightManager::gpu_update_source(ShadowSource* source) { - nassertv(_cmd_list != NULL); // No command list set yet - nassertv(source->has_slot()); // Source has no slot! - GPUCommand cmd_update(GPUCommand::CMD_store_source); - cmd_update.push_int(source->get_slot()); - source->write_to_command(cmd_update); - _cmd_list->add_command(cmd_update); -} - -/** - * @brief Internal method to update all lights - * @details This is called by the main update method, and iterates over the list - * of lights. If a light is marked as dirty, it will recieve an update of its - * data and its shadow sources. - */ -void InternalLightManager::update_lights() { - for (auto iter = _lights.begin(); iter != _lights.end(); ++iter) { - RPLight* light = *iter; - if (light && light->get_needs_update()) { - if (light->get_casts_shadows()) { - light->update_shadow_sources(); - } - gpu_update_light(light); - } - } -} - -/** - * @brief Compares shadow sources by their priority - * @details Returns if a has a greater priority than b. This depends on the - * resolution of the source, and also if the source has a region or not. - * This method can be passed to std::sort. - * - * @param a First source - * @param b Second source - * - * @return true if a is more important than b, else false - */ -bool InternalLightManager::compare_shadow_sources(const ShadowSource* a, const ShadowSource* b) const { - - // Make sure that sources which already have a region (but maybe outdated) - // come after sources which have no region at all. - if (a->has_region() != b->has_region()) { - return b->has_region(); - } - - // Compare sources based on their distance to the camera - float dist_a = (_camera_pos - a->get_bounds().get_center()).length_squared(); - float dist_b = (_camera_pos - a->get_bounds().get_center()).length_squared(); - - // XXX: Should also compare based on source size, so that huge sources recieve - // more updates - - return dist_b > dist_a; -} - -/** - * @brief Internal method to update all shadow sources - * @details This updates all shadow sources which are marked dirty. It will sort - * the list of all dirty shadow sources by their resolution, take the first - * n entries, and update them. The amount of sources processed depends on the - * max_updates of the ShadowManager. - */ -void InternalLightManager::update_shadow_sources() { - - // Find all dirty shadow sources and make a list of them - vector sources_to_update; - for (auto iter = _shadow_sources.begin(); iter != _shadow_sources.end(); ++iter) { - ShadowSource* source = *iter; - if (source) { - const BoundingSphere& bounds = source->get_bounds(); - - // Check if source is in range - float distance_to_camera = (_camera_pos - bounds.get_center()).length() - bounds.get_radius(); - if (distance_to_camera < _shadow_update_distance) { - if (source->get_needs_update()) { - sources_to_update.push_back(source); - } - } else { - - // Free regions of sources which are out of the update radius, - // to make space for other regions - if (source->has_region()) { - _shadow_manager->get_atlas()->free_region(source->get_region()); - source->clear_region(); - } - } - } - - } - - // Sort the sources based on their importance, so that sources with a bigger - // priority come first. This helps to get a better packing on the shadow atlas. - // However, we also need to prioritize sources which have no current region, - // because no shadows are worse than outdated-shadows. - std::sort(sources_to_update.begin(), sources_to_update.end(), [this](const ShadowSource* a, const ShadowSource* b) { - return this->compare_shadow_sources(a, b); - }); - - // Get a handle to the atlas, will be frequently used - ShadowAtlas *atlas = _shadow_manager->get_atlas(); - - // Free the regions of all sources which will get updated. We have to take into - // account that only a limited amount of sources can get updated per frame. - size_t update_slots = min(sources_to_update.size(), - _shadow_manager->get_num_update_slots_left()); - for(size_t i = 0; i < update_slots; ++i) { - if (sources_to_update[i]->has_region()) { - atlas->free_region(sources_to_update[i]->get_region()); - } - } - - // Find an atlas spot for all regions which are supposed to get an update - for (size_t i = 0; i < update_slots; ++i) { - ShadowSource *source = sources_to_update[i]; - - if(!_shadow_manager->add_update(source)) { - // In case the ShadowManager lied about the number of updates left - lightmgr_cat.error() << "ShadowManager ensured update slot, but slot is taken!" << endl; - break; - } - - // We have an update slot, and are guaranteed to get updated as soon - // as possible, so we can start getting a new atlas position. - size_t region_size = atlas->get_required_tiles(source->get_resolution()); - LVecBase4i new_region = atlas->find_and_reserve_region(region_size, region_size); - LVecBase4f new_uv_region = atlas->region_to_uv(new_region); - source->set_region(new_region, new_uv_region); - - // Mark the source as updated - source->set_needs_update(false); - gpu_update_source(source); - } -} - -/** - * @brief Main update method - * @details This is the main update method of the InternalLightManager. It - * processes all lights and shadow sources, updates them, and notifies the - * GPU about it. This should be called on a per-frame basis. - * - * If the InternalLightManager was not initialized yet, an assertion is thrown. - */ -void InternalLightManager::update() { - nassertv(_shadow_manager != NULL); // Not initialized yet! - nassertv(_cmd_list != NULL); // Not initialized yet! - - update_lights(); - update_shadow_sources(); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +#include "internal_light_manager.h" + +#include + +NotifyCategoryDef(lightmgr, ""); + + +/** + * @brief Constructs the light manager + * @details This constructs the light manager, initializing the light and shadow + * storage. You should set a command list and shadow manager before calling + * InternalLightManager::update. s + */ +InternalLightManager::InternalLightManager() { + _shadow_update_distance = 100.0f; + _cmd_list = NULL; + _shadow_manager = NULL; +} + +/** + * @brief Adds a new light. + * @details This adds a new light to the list of lights. This will throw an + * error and return if the light is already attached. You may only call + * this after the ShadowManager was already set. + * + * While the light is attached, the light manager keeps a reference to it, so + * the light does not get destructed. + * + * This also setups the shadows on the light, in case shadows are enabled. + * While a light is attached, you can not change whether it casts shadows or not. + * To do so, detach the light, change the setting, and re-add the light. + * + * In case no free light slot is available, an error will be printed and no + * action will be performed. + * + * If no shadow manager was set, an assertion will be triggered. + * + * @param light The light to add. + */ +void InternalLightManager::add_light(PT(RPLight) light) { + nassertv(_shadow_manager != NULL); // Shadow manager not set yet! + + // Don't attach the light in case its already attached + if (light->has_slot()) { + lightmgr_cat.error() << "could not add light because it already is attached! " + << "Detach the light first, then try it again." << endl; + return; + } + + // Find a free slot + size_t slot; + if (!_lights.find_slot(slot)) { + lightmgr_cat.error() << "Light limit of " << MAX_LIGHT_COUNT << " reached, " + << "all light slots used!" << endl; + return; + } + + // Reference the light because we store it, to avoid it getting destructed + // on the python side while we still work with it. The reference will be + // removed when the light gets detached. + light->ref(); + + // Reserve the slot + light->assign_slot(slot); + _lights.reserve_slot(slot, light); + + // Setup the shadows in case the light uses them + if (light->get_casts_shadows()) { + setup_shadows(light); + } + + // Store the light on the gpu, to make sure the GPU directly knows about it. + // We could wait until the next update cycle, but then we might be one frame + // too late already. + gpu_update_light(light); +} + +/** + * @brief Internal method to setup shadows for a light + * @details This method gets called by the InternalLightManager::add_light method + * to setup a lights shadow sources, in case shadows are enabled on that light. + * + * It finds a slot for all shadow sources of the ilhgt, and inits the shadow + * sources as well. If no slot could be found, an error is printed an nothing + * happens. + * + * @param light The light to init the shadow sources for + */ +void InternalLightManager::setup_shadows(RPLight* light) { + + // Init the lights shadow sources, and also call update once to make sure + // the sources are properly initialized + light->init_shadow_sources(); + light->update_shadow_sources(); + + // Find consecutive slots, this is important for PointLights so we can just + // store the first index of the source, and get the other slots by doing + // first_index + 1, +2 and so on. + size_t base_slot; + size_t num_sources = light->get_num_shadow_sources(); + if (!_shadow_sources.find_consecutive_slots(base_slot, num_sources)) { + lightmgr_cat.error() << "Failed to find slot for shadow sources! " + << "Shadow-Source limit of " << MAX_SHADOW_SOURCES + << " reached!" << endl; + return; + } + + // Init all sources + for (int i = 0; i < num_sources; ++i) { + ShadowSource* source = light->get_shadow_source(i); + + // Set the source as dirty, so it gets updated in the beginning + source->set_needs_update(true); + + // Assign the slot to the source. Since we got consecutive slots, we can + // just do base_slot + N. + size_t slot = base_slot + i; + _shadow_sources.reserve_slot(slot, source); + source->set_slot(slot); + } +} + +/** + * @brief Removes a light + * @details This detaches a light. This prevents it from being rendered, and also + * cleans up all resources used by that light. If no reference is kept on the + * python side, the light will also get destructed. + * + * If the light was not previously attached with InternalLightManager::add_light, + * an error will be triggered and nothing happens. + * + * In case the light was set to cast shadows, all shadow sources are cleaned + * up, and their regions in the shadow atlas are freed. + * + * All resources used by the light in the light and shadow storage are also + * cleaned up, by emitting cleanup GPUCommands. + * + * If no shadow manager was set, an assertion will be triggered. + * + * @param light [description] + */ +void InternalLightManager::remove_light(PT(RPLight) light) { + nassertv(_shadow_manager != NULL); + + if (!light->has_slot()) { + lightmgr_cat.error() << "Could not detach light, light was not attached!" << endl; + return; + } + + // Free the lights slot in the light storage + _lights.free_slot(light->get_slot()); + + // Tell the GPU we no longer need the lights data + gpu_remove_light(light); + + // Mark the light as detached. After this call, we can not call get_slot + // anymore, so its important we do this after we unregistered the light + // from everywhere. + light->remove_slot(); + + // Clear shadow related stuff, in case the light casts shadows + if (light->get_casts_shadows()) { + + // Free the slots of all sources, and also unregister their regions from + // the shadow atlas. + for (size_t i = 0; i < light->get_num_shadow_sources(); ++i) { + ShadowSource* source = light->get_shadow_source(i); + if (source->has_slot()) { + _shadow_sources.free_slot(source->get_slot()); + } + if (source->has_region()) { + _shadow_manager->get_atlas()->free_region(source->get_region()); + source->clear_region(); + } + } + + // Remove all sources of the light by emitting a consecutive remove command + gpu_remove_consecutive_sources(light->get_shadow_source(0), + light->get_num_shadow_sources()); + + // Finally remove all shadow sources. This is important in case the light + // will be re-attached. Otherwise an assertion will get triggered. + light->clear_shadow_sources(); + } + + // Since we referenced the light when we stored it, we have to decrease + // the reference now. In case no reference was kept on the python side, + // the light will get destructed soon. + light->unref(); +} + +/** + * @brief Internal method to remove consecutive sources from the GPU. + * @details This emits a GPUCommand to consecutively remove shadow sources from + * the GPU. This is called when a light gets removed, to free the space its + * shadow sources took. Its not really required, because as long as the light + * is not used, there is no reference to the sources. However, it can't hurt to + * cleanup the memory. + * + * All sources starting at first_source->get_slot() until + * first_source->get_slot() + num_sources will get cleaned up. + * + * @param first_source First source of the light + * @param num_sources Amount of consecutive sources to clear + */ +void InternalLightManager::gpu_remove_consecutive_sources(ShadowSource *first_source, + size_t num_sources) { + nassertv(_cmd_list != NULL); // No command list set yet + nassertv(first_source->has_slot()); // Source has no slot! + GPUCommand cmd_remove(GPUCommand::CMD_remove_sources); + cmd_remove.push_int(first_source->get_slot()); + cmd_remove.push_int(num_sources); + _cmd_list->add_command(cmd_remove); +} + +/** + * @brief Internal method to remove a light from the GPU. + * @details This emits a GPUCommand to clear a lights data. This sets the data + * to all zeros, marking that no light is stored anymore. + * + * This throws an assertion in case the light is not currently attached. Be + * sure to call this before detaching the light. + * + * @param light The light to remove, must be attached. + */ +void InternalLightManager::gpu_remove_light(RPLight* light) { + nassertv(_cmd_list != NULL); // No command list set yet + nassertv(light->has_slot()); // Light has no slot! + GPUCommand cmd_remove(GPUCommand::CMD_remove_light); + cmd_remove.push_int(light->get_slot()); + _cmd_list->add_command(cmd_remove); +} + +/** + * @brief Updates a lights data on the GPU + * @details This method emits a GPUCommand to update a lights data. This can + * be used to initially store the lights data, or to update the data whenever + * the light changed. + * + * This throws an assertion in case the light is not currently attached. Be + * sure to call this after attaching the light. + * + * @param light The light to update + */ +void InternalLightManager::gpu_update_light(RPLight* light) { + nassertv(_cmd_list != NULL); // No command list set yet + nassertv(light->has_slot()); // Light has no slot! + GPUCommand cmd_update(GPUCommand::CMD_store_light); + cmd_update.push_int(light->get_slot()); + light->write_to_command(cmd_update); + light->set_needs_update(false); + _cmd_list->add_command(cmd_update); +} + +/** + * @brief Updates a shadow source data on the GPU + * @details This emits a GPUCommand to update a given shadow source, storing all + * data of the source on the GPU. This can also be used to initially store a + * ShadowSource, since all data will be overridden. + * + * This throws an assertion if the source has no slot yet. + * + * @param source The source to update + */ +void InternalLightManager::gpu_update_source(ShadowSource* source) { + nassertv(_cmd_list != NULL); // No command list set yet + nassertv(source->has_slot()); // Source has no slot! + GPUCommand cmd_update(GPUCommand::CMD_store_source); + cmd_update.push_int(source->get_slot()); + source->write_to_command(cmd_update); + _cmd_list->add_command(cmd_update); +} + +/** + * @brief Internal method to update all lights + * @details This is called by the main update method, and iterates over the list + * of lights. If a light is marked as dirty, it will recieve an update of its + * data and its shadow sources. + */ +void InternalLightManager::update_lights() { + for (auto iter = _lights.begin(); iter != _lights.end(); ++iter) { + RPLight* light = *iter; + if (light && light->get_needs_update()) { + if (light->get_casts_shadows()) { + light->update_shadow_sources(); + } + gpu_update_light(light); + } + } +} + +/** + * @brief Compares shadow sources by their priority + * @details Returns if a has a greater priority than b. This depends on the + * resolution of the source, and also if the source has a region or not. + * This method can be passed to std::sort. + * + * @param a First source + * @param b Second source + * + * @return true if a is more important than b, else false + */ +bool InternalLightManager::compare_shadow_sources(const ShadowSource* a, const ShadowSource* b) const { + + // Make sure that sources which already have a region (but maybe outdated) + // come after sources which have no region at all. + if (a->has_region() != b->has_region()) { + return b->has_region(); + } + + // Compare sources based on their distance to the camera + float dist_a = (_camera_pos - a->get_bounds().get_center()).length_squared(); + float dist_b = (_camera_pos - a->get_bounds().get_center()).length_squared(); + + // XXX: Should also compare based on source size, so that huge sources recieve + // more updates + + return dist_b > dist_a; +} + +/** + * @brief Internal method to update all shadow sources + * @details This updates all shadow sources which are marked dirty. It will sort + * the list of all dirty shadow sources by their resolution, take the first + * n entries, and update them. The amount of sources processed depends on the + * max_updates of the ShadowManager. + */ +void InternalLightManager::update_shadow_sources() { + + // Find all dirty shadow sources and make a list of them + vector sources_to_update; + for (auto iter = _shadow_sources.begin(); iter != _shadow_sources.end(); ++iter) { + ShadowSource* source = *iter; + if (source) { + const BoundingSphere& bounds = source->get_bounds(); + + // Check if source is in range + float distance_to_camera = (_camera_pos - bounds.get_center()).length() - bounds.get_radius(); + if (distance_to_camera < _shadow_update_distance) { + if (source->get_needs_update()) { + sources_to_update.push_back(source); + } + } else { + + // Free regions of sources which are out of the update radius, + // to make space for other regions + if (source->has_region()) { + _shadow_manager->get_atlas()->free_region(source->get_region()); + source->clear_region(); + } + } + } + + } + + // Sort the sources based on their importance, so that sources with a bigger + // priority come first. This helps to get a better packing on the shadow atlas. + // However, we also need to prioritize sources which have no current region, + // because no shadows are worse than outdated-shadows. + std::sort(sources_to_update.begin(), sources_to_update.end(), [this](const ShadowSource* a, const ShadowSource* b) { + return this->compare_shadow_sources(a, b); + }); + + // Get a handle to the atlas, will be frequently used + ShadowAtlas *atlas = _shadow_manager->get_atlas(); + + // Free the regions of all sources which will get updated. We have to take into + // account that only a limited amount of sources can get updated per frame. + size_t update_slots = min(sources_to_update.size(), + _shadow_manager->get_num_update_slots_left()); + for(size_t i = 0; i < update_slots; ++i) { + if (sources_to_update[i]->has_region()) { + atlas->free_region(sources_to_update[i]->get_region()); + } + } + + // Find an atlas spot for all regions which are supposed to get an update + for (size_t i = 0; i < update_slots; ++i) { + ShadowSource *source = sources_to_update[i]; + + if(!_shadow_manager->add_update(source)) { + // In case the ShadowManager lied about the number of updates left + lightmgr_cat.error() << "ShadowManager ensured update slot, but slot is taken!" << endl; + break; + } + + // We have an update slot, and are guaranteed to get updated as soon + // as possible, so we can start getting a new atlas position. + size_t region_size = atlas->get_required_tiles(source->get_resolution()); + LVecBase4i new_region = atlas->find_and_reserve_region(region_size, region_size); + LVecBase4f new_uv_region = atlas->region_to_uv(new_region); + source->set_region(new_region, new_uv_region); + + // Mark the source as updated + source->set_needs_update(false); + gpu_update_source(source); + } +} + +/** + * @brief Main update method + * @details This is the main update method of the InternalLightManager. It + * processes all lights and shadow sources, updates them, and notifies the + * GPU about it. This should be called on a per-frame basis. + * + * If the InternalLightManager was not initialized yet, an assertion is thrown. + */ +void InternalLightManager::update() { + nassertv(_shadow_manager != NULL); // Not initialized yet! + nassertv(_cmd_list != NULL); // Not initialized yet! + + update_lights(); + update_shadow_sources(); +} diff --git a/RenderPipelineFile/rpcore/native/source/internal_light_manager.h b/RenderPipelineFile/rpcore/native/source/internal_light_manager.h index d5c8a4b6..24aeee24 100644 --- a/RenderPipelineFile/rpcore/native/source/internal_light_manager.h +++ b/RenderPipelineFile/rpcore/native/source/internal_light_manager.h @@ -1,104 +1,104 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#ifndef RP_INTERNAL_LIGHT_MANAGER_H -#define RP_INTERNAL_LIGHT_MANAGER_H - -#include "referenceCount.h" -#include "rp_light.h" -#include "shadow_source.h" -#include "shadow_atlas.h" -#include "shadow_manager.h" -#include "pointer_slot_storage.h" -#include "gpu_command_list.h" - -#define MAX_LIGHT_COUNT 65535 -#define MAX_SHADOW_SOURCES 2048 - -NotifyCategoryDecl(lightmgr, EXPORT_CLASS, EXPORT_TEMPL); - -/** - * @brief Internal class used for handling lights and shadows. - * @details This is the internal class used by the pipeline to handle all - * lights and shadows. It stores references to the lights, manages handling - * the light and shadow slots, and also communicates with the GPU with the - * GPUCommandQueue to store light and shadow source data. - */ -class InternalLightManager { - - PUBLISHED: - InternalLightManager(); - - - void add_light(PT(RPLight) light); - void remove_light(PT(RPLight) light); - - void update(); - inline void set_camera_pos(const LPoint3f& pos); - inline void set_shadow_update_distance(float dist); - - inline int get_max_light_index() const; - MAKE_PROPERTY(max_light_index, get_max_light_index); - - inline size_t get_num_lights() const; - MAKE_PROPERTY(num_lights, get_num_lights); - - inline size_t get_num_shadow_sources() const; - MAKE_PROPERTY(num_shadow_sources, get_num_shadow_sources); - - inline void set_shadow_manager(ShadowManager* mgr); - inline ShadowManager* get_shadow_manager() const; - MAKE_PROPERTY(shadow_manager, get_shadow_manager, set_shadow_manager); - - inline void set_command_list(GPUCommandList *cmd_list); - - protected: - - void gpu_update_light(RPLight* light); - void gpu_update_source(ShadowSource* source); - void gpu_remove_light(RPLight* light); - void gpu_remove_consecutive_sources(ShadowSource *first_source, size_t num_sources); - - void setup_shadows(RPLight* light); - bool compare_shadow_sources(const ShadowSource* a, const ShadowSource* b) const; - - void update_lights(); - void update_shadow_sources(); - - GPUCommandList* _cmd_list; - ShadowManager* _shadow_manager; - - PointerSlotStorage _lights; - PointerSlotStorage _shadow_sources; - - LPoint3f _camera_pos; - float _shadow_update_distance; - -}; - -#include "internal_light_manager.I" - -#endif // RP_INTERNAL_LIGHT_MANAGER_H +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef RP_INTERNAL_LIGHT_MANAGER_H +#define RP_INTERNAL_LIGHT_MANAGER_H + +#include "referenceCount.h" +#include "rp_light.h" +#include "shadow_source.h" +#include "shadow_atlas.h" +#include "shadow_manager.h" +#include "pointer_slot_storage.h" +#include "gpu_command_list.h" + +#define MAX_LIGHT_COUNT 65535 +#define MAX_SHADOW_SOURCES 2048 + +NotifyCategoryDecl(lightmgr, EXPORT_CLASS, EXPORT_TEMPL); + +/** + * @brief Internal class used for handling lights and shadows. + * @details This is the internal class used by the pipeline to handle all + * lights and shadows. It stores references to the lights, manages handling + * the light and shadow slots, and also communicates with the GPU with the + * GPUCommandQueue to store light and shadow source data. + */ +class InternalLightManager { + + PUBLISHED: + InternalLightManager(); + + + void add_light(PT(RPLight) light); + void remove_light(PT(RPLight) light); + + void update(); + inline void set_camera_pos(const LPoint3f& pos); + inline void set_shadow_update_distance(float dist); + + inline int get_max_light_index() const; + MAKE_PROPERTY(max_light_index, get_max_light_index); + + inline size_t get_num_lights() const; + MAKE_PROPERTY(num_lights, get_num_lights); + + inline size_t get_num_shadow_sources() const; + MAKE_PROPERTY(num_shadow_sources, get_num_shadow_sources); + + inline void set_shadow_manager(ShadowManager* mgr); + inline ShadowManager* get_shadow_manager() const; + MAKE_PROPERTY(shadow_manager, get_shadow_manager, set_shadow_manager); + + inline void set_command_list(GPUCommandList *cmd_list); + + protected: + + void gpu_update_light(RPLight* light); + void gpu_update_source(ShadowSource* source); + void gpu_remove_light(RPLight* light); + void gpu_remove_consecutive_sources(ShadowSource *first_source, size_t num_sources); + + void setup_shadows(RPLight* light); + bool compare_shadow_sources(const ShadowSource* a, const ShadowSource* b) const; + + void update_lights(); + void update_shadow_sources(); + + GPUCommandList* _cmd_list; + ShadowManager* _shadow_manager; + + PointerSlotStorage _lights; + PointerSlotStorage _shadow_sources; + + LPoint3f _camera_pos; + float _shadow_update_distance; + +}; + +#include "internal_light_manager.I" + +#endif // RP_INTERNAL_LIGHT_MANAGER_H diff --git a/RenderPipelineFile/rpcore/native/source/pointer_slot_storage.h b/RenderPipelineFile/rpcore/native/source/pointer_slot_storage.h index 4c3e6964..6e1bc024 100644 --- a/RenderPipelineFile/rpcore/native/source/pointer_slot_storage.h +++ b/RenderPipelineFile/rpcore/native/source/pointer_slot_storage.h @@ -1,235 +1,235 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#ifndef RP_POINTER_SLOT_STORAGE -#define RP_POINTER_SLOT_STORAGE - - -#ifdef CPPPARSER - -// Dummy implementation for interrogate -template < typename T, int SIZE > -class PointerSlotStorage {}; - -#else // CPPPARSER - - -#include "pandabase.h" -#include - -/** - * @brief Class to keep a list of pointers and nullpointers. - * @details This class stores a fixed size list of pointers, whereas pointers - * may be a nullptr as well. It provides functionality to find free slots, - * and also to find free consecutive slots, as well as taking care of reserving slots. - * - * @tparam T* Pointer-Type - * @tparam SIZE Size of the storage - */ -template < typename T, int SIZE > -class PointerSlotStorage { - -public: - - /** - * @brief Constructs a new PointerSlotStorage - * @details This constructs a new PointerSlotStorage, with all slots - * initialized to a nullptr. - */ - PointerSlotStorage() { - _data.fill(NULL); - _max_index = 0; - _num_entries = 0; - } - - /** - * @brief Returns the maximum index of the container - * @details This returns the greatest index of any element which is not zero. - * This can be useful for iterating the container, since all elements - * coming after the returned index are guaranteed to be a nullptr. - * - * If no elements are in this container, -1 is returned. - * @return Maximum index of the container - */ - int get_max_index() const { - return _max_index; - } - - /** - * @brief Returns the amount of elements of the container - * @details This returns the amount of elements in the container which are - * no nullptr. - * @return Amount of elements - */ - size_t get_num_entries() const { - return _num_entries; - } - - /** - * @brief Finds a free slot - * @details This finds the first slot which is a nullptr and returns it. - * This is most likely useful in combination with reserve_slot. - * - * When no slot found was found, slot will be undefined, and false will - * be returned. - * - * @param slot Output-Variable, slot will be stored there - * @return true if a slot was found, otherwise false - */ - bool find_slot(size_t &slot) const { - for (size_t i = 0; i < SIZE; ++i) { - if (_data[i] == NULL) { - slot = i; - return true; - } - } - return false; - } - - /** - * @brief Finds free consecutive slots - * @details This behaves like find_slot, but it tries to find a slot - * after which free slots follow as well. - * - * When no slot found was found, slot will be undefined, and false will - * be returned. - * - * @param slot Output-Variable, index of the first slot of the consecutive - * slots will be stored there. - * @param num_consecutive Amount of consecutive slots to find, including the - * first slot. - * - * @return true if consecutive slots were found, otherwise false. - */ - bool find_consecutive_slots(size_t &slot, size_t num_consecutive) const { - nassertr(num_consecutive > 0, false); - - // Fall back to default search algorithm in case the parameters are equal - if (num_consecutive == 1) { - return find_slot(slot); - } - - // Try to find consecutive slots otherwise - for (size_t i = 0; i < SIZE; ++i) { - bool any_taken = false; - for (size_t k = 0; !any_taken && k < num_consecutive; ++k) { - any_taken = _data[i + k] != NULL; - } - if (!any_taken) { - slot = i; - return true; - } - } - return false; - } - - /** - * @brief Frees an allocated slot - * @details This frees an allocated slot. If the slot was already freed - * before, this method throws an assertion. - * - * @param slot Slot to free - */ - void free_slot(size_t slot) { - nassertv(slot >= 0 && slot < SIZE); - nassertv(_data[slot] != NULL); // Slot was already empty! - _data[slot] = NULL; - _num_entries--; - - // Update maximum index - if (slot == _max_index) { - while (_max_index >= 0 && !_data[_max_index--]); - // 正确的修复代码 -// while (_max_index >= 0 && _data[_max_index] == NULL) { -// _max_index--; -// } - } - } - } - - /** - * @brief Frees consecutive allocated slots - * @details This behaves like PointerSlotStorage::free_slot, but deletes - * consecutive slots. - * - * @param slot Start of the consecutive slots to free - * @param num_consecutive Number of consecutive slots - */ - void free_consecutive_slots(size_t slot, size_t num_consecutive) { - for (size_t i = slot; i < slot + num_consecutive; ++i) { - free_slot(i); - } - } - - /** - * @brief Reserves a slot - * @details This reserves a slot by storing a pointer in it. If the slot - * was already taken, throws an assertion. - * If the ptr is a nullptr, also throws an assertion. - * If the slot was out of bounds, also throws an assertion. - * - * @param slot Slot to reserve - * @param ptr Pointer to store - */ - void reserve_slot(size_t slot, T ptr) { - nassertv(slot >= 0 && slot < SIZE); - nassertv(_data[slot] == NULL); // Slot already taken! - nassertv(ptr != NULL); // nullptr passed as argument! - _max_index = max(_max_index, (int)slot); - _data[slot] = ptr; - _num_entries++; - } - - typedef array InternalContainer; - - /** - * @brief Returns an iterator to the begin of the container - * @details This returns an iterator to the beginning of the container - * @return Begin-Iterator - */ - typename InternalContainer::iterator begin() { - return _data.begin(); - } - - /** - * @brief Returns an iterator to the end of the container - * @details This returns an iterator to the end of the iterator. This only - * iterates to PointerSlotStorage::get_max_index() - * @return [description] - */ - typename InternalContainer::iterator end() { - return _data.begin() + _max_index + 1; - } - -private: - int _max_index; - size_t _num_entries; - InternalContainer _data; -}; - -#endif // CPPPARSER - -#endif // RP_POINTER_SLOT_STORAGE +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef RP_POINTER_SLOT_STORAGE +#define RP_POINTER_SLOT_STORAGE + + +#ifdef CPPPARSER + +// Dummy implementation for interrogate +template < typename T, int SIZE > +class PointerSlotStorage {}; + +#else // CPPPARSER + + +#include "pandabase.h" +#include + +/** + * @brief Class to keep a list of pointers and nullpointers. + * @details This class stores a fixed size list of pointers, whereas pointers + * may be a nullptr as well. It provides functionality to find free slots, + * and also to find free consecutive slots, as well as taking care of reserving slots. + * + * @tparam T* Pointer-Type + * @tparam SIZE Size of the storage + */ +template < typename T, int SIZE > +class PointerSlotStorage { + +public: + + /** + * @brief Constructs a new PointerSlotStorage + * @details This constructs a new PointerSlotStorage, with all slots + * initialized to a nullptr. + */ + PointerSlotStorage() { + _data.fill(NULL); + _max_index = 0; + _num_entries = 0; + } + + /** + * @brief Returns the maximum index of the container + * @details This returns the greatest index of any element which is not zero. + * This can be useful for iterating the container, since all elements + * coming after the returned index are guaranteed to be a nullptr. + * + * If no elements are in this container, -1 is returned. + * @return Maximum index of the container + */ + int get_max_index() const { + return _max_index; + } + + /** + * @brief Returns the amount of elements of the container + * @details This returns the amount of elements in the container which are + * no nullptr. + * @return Amount of elements + */ + size_t get_num_entries() const { + return _num_entries; + } + + /** + * @brief Finds a free slot + * @details This finds the first slot which is a nullptr and returns it. + * This is most likely useful in combination with reserve_slot. + * + * When no slot found was found, slot will be undefined, and false will + * be returned. + * + * @param slot Output-Variable, slot will be stored there + * @return true if a slot was found, otherwise false + */ + bool find_slot(size_t &slot) const { + for (size_t i = 0; i < SIZE; ++i) { + if (_data[i] == NULL) { + slot = i; + return true; + } + } + return false; + } + + /** + * @brief Finds free consecutive slots + * @details This behaves like find_slot, but it tries to find a slot + * after which free slots follow as well. + * + * When no slot found was found, slot will be undefined, and false will + * be returned. + * + * @param slot Output-Variable, index of the first slot of the consecutive + * slots will be stored there. + * @param num_consecutive Amount of consecutive slots to find, including the + * first slot. + * + * @return true if consecutive slots were found, otherwise false. + */ + bool find_consecutive_slots(size_t &slot, size_t num_consecutive) const { + nassertr(num_consecutive > 0, false); + + // Fall back to default search algorithm in case the parameters are equal + if (num_consecutive == 1) { + return find_slot(slot); + } + + // Try to find consecutive slots otherwise + for (size_t i = 0; i < SIZE; ++i) { + bool any_taken = false; + for (size_t k = 0; !any_taken && k < num_consecutive; ++k) { + any_taken = _data[i + k] != NULL; + } + if (!any_taken) { + slot = i; + return true; + } + } + return false; + } + + /** + * @brief Frees an allocated slot + * @details This frees an allocated slot. If the slot was already freed + * before, this method throws an assertion. + * + * @param slot Slot to free + */ + void free_slot(size_t slot) { + nassertv(slot >= 0 && slot < SIZE); + nassertv(_data[slot] != NULL); // Slot was already empty! + _data[slot] = NULL; + _num_entries--; + + // Update maximum index + if (slot == _max_index) { + while (_max_index >= 0 && !_data[_max_index--]); + // 正确的修复代码 +// while (_max_index >= 0 && _data[_max_index] == NULL) { +// _max_index--; +// } + } + } + } + + /** + * @brief Frees consecutive allocated slots + * @details This behaves like PointerSlotStorage::free_slot, but deletes + * consecutive slots. + * + * @param slot Start of the consecutive slots to free + * @param num_consecutive Number of consecutive slots + */ + void free_consecutive_slots(size_t slot, size_t num_consecutive) { + for (size_t i = slot; i < slot + num_consecutive; ++i) { + free_slot(i); + } + } + + /** + * @brief Reserves a slot + * @details This reserves a slot by storing a pointer in it. If the slot + * was already taken, throws an assertion. + * If the ptr is a nullptr, also throws an assertion. + * If the slot was out of bounds, also throws an assertion. + * + * @param slot Slot to reserve + * @param ptr Pointer to store + */ + void reserve_slot(size_t slot, T ptr) { + nassertv(slot >= 0 && slot < SIZE); + nassertv(_data[slot] == NULL); // Slot already taken! + nassertv(ptr != NULL); // nullptr passed as argument! + _max_index = max(_max_index, (int)slot); + _data[slot] = ptr; + _num_entries++; + } + + typedef array InternalContainer; + + /** + * @brief Returns an iterator to the begin of the container + * @details This returns an iterator to the beginning of the container + * @return Begin-Iterator + */ + typename InternalContainer::iterator begin() { + return _data.begin(); + } + + /** + * @brief Returns an iterator to the end of the container + * @details This returns an iterator to the end of the iterator. This only + * iterates to PointerSlotStorage::get_max_index() + * @return [description] + */ + typename InternalContainer::iterator end() { + return _data.begin() + _max_index + 1; + } + +private: + int _max_index; + size_t _num_entries; + InternalContainer _data; +}; + +#endif // CPPPARSER + +#endif // RP_POINTER_SLOT_STORAGE diff --git a/RenderPipelineFile/rpcore/native/source/pssm_camera_rig.I b/RenderPipelineFile/rpcore/native/source/pssm_camera_rig.I index c9bf1fbd..2a634416 100644 --- a/RenderPipelineFile/rpcore/native/source/pssm_camera_rig.I +++ b/RenderPipelineFile/rpcore/native/source/pssm_camera_rig.I @@ -1,243 +1,243 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -/** - * @brief Sets the maximum pssm distance. - * @details This sets the maximum distance in world space until which shadows - * are rendered. After this distance, no shadows will be rendered. - * - * If the distance is below zero, an assertion is triggered. - * - * @param distance Maximum distance in world space - */ -inline void PSSMCameraRig::set_pssm_distance(float distance) { - nassertv(distance > 0.0 && distance < 100000.0); - _pssm_distance = distance; -} - -/** - * @brief Sets the suns distance - * @details This sets the distance the cameras will have from the cameras frustum. - * This prevents far objects from having no shadows, which can occur when these - * objects are between the cameras frustum and the sun, but not inside of the - * cameras frustum. Setting the sun distance high enough will move the cameras - * away from the camera frustum, being able to cover those distant objects too. - * - * If the sun distance is set too high, artifacts will occur due to the reduced - * range of depth. If a value below zero is passed, an assertion will get - * triggered. - * - * @param distance The sun distance - */ -inline void PSSMCameraRig::set_sun_distance(float distance) { - nassertv(distance > 0.0 && distance < 100000.0); - _sun_distance = distance; -} - -/** - * @brief Sets the logarithmic factor - * @details This sets the logarithmic factor, which is the core of the algorithm. - * PSSM splits the camera frustum based on a linear and a logarithmic factor. - * While a linear factor provides a good distribution, it often is not applicable - * for wider distances. A logarithmic distribution provides a better distribution - * at distance, but suffers from splitting in the near areas. - * - * The logarithmic factor mixes the logarithmic and linear split distribution, - * to get the best of both. A greater factor will make the distribution more - * logarithmic, while a smaller factor will make it more linear. - * - * If the factor is below zero, an ssertion is triggered. - * - * @param factor The logarithmic factor - */ -inline void PSSMCameraRig::set_logarithmic_factor(float factor) { - nassertv(factor > 0.0); - _logarithmic_factor = factor; -} - -/** - * @brief Sets whether to use a fixed film size - * @details This controls if a fixed film size should be used. This will cause - * the camera rig to cache the current film size, and only change it in case - * it gets too small. This provides less flickering when moving, because the - * film size will stay roughly constant. However, to prevent the cached film - * size getting too big, one should call PSSMCameraRig::reset_film_size - * once in a while, otherwise there might be a lot of wasted space. - * - * @param flag Whether to use a fixed film size - */ -inline void PSSMCameraRig::set_use_fixed_film_size(bool flag) { - _use_fixed_film_size = flag; -} - -/** - * @brief Sets the resolution of each split - * @details This sets the resolution of each split. Currently it is equal for - * each split. This is required when using PSSMCameraRig::set_use_stable_csm, - * to compute how bix a texel is. - * - * It has to match the y-resolution of the pssm shadow map. If an invalid - * resolution is triggered, an assertion is thrown. - * - * @param resolution The resolution of each split. - */ -inline void PSSMCameraRig::set_resolution(size_t resolution) { - nassertv(resolution >= 0 && resolution < 65535); - _resolution = resolution; -} - -/** - * @brief Sets whether to use stable CSM snapping. - * @details This option controls if stable CSM snapping should be used. When the - * option is enabled, all splits will snap to their texels, so that when moving, - * no flickering will occur. However, this only works when the splits do not - * change their film size, rotation and angle. - * - * @param flag Whether to use stable CSM snapping - */ -inline void PSSMCameraRig::set_use_stable_csm(bool flag) { - _use_stable_csm = flag; -} - -/** - * @brief Sets the border bias for each split - * @details This sets the border bias for every split. This increases each - * splits frustum by multiplying it by (1 + bias), and helps reducing artifacts - * at the borders of the splits. Artifacts can occur when the bias is too low, - * because then the filtering will go over the bounds of the split, producing - * invalid results. - * - * If the bias is below zero, an assertion is thrown. - * - * @param bias Border bias - */ -inline void PSSMCameraRig::set_border_bias(float bias) { - nassertv(bias >= 0.0); - _border_bias = bias; -} - -/** - * @brief Resets the film size cache - * @details In case PSSMCameraRig::set_use_fixed_film_size is used, this resets - * the film size cache. This might lead to a small "jump" in the shadows, - * because the film size changes, however it leads to a better shadow distribution. - * - * This is the case because when using a fixed film size, the cache will get - * bigger and bigger, whenever the camera moves to a grazing angle. However, - * when moving back to a normal angle, the film size cache still stores this - * big angle, and thus the splits will have a much bigger film size than actualy - * required. To prevent this, call this method once in a while, so an optimal - * distribution is ensured. - */ -inline void PSSMCameraRig::reset_film_size_cache() { - for (size_t i = 0; i < _max_film_sizes.size(); ++i) { - _max_film_sizes[i].fill(0); - } -} - -/** - * @brief Returns the n-th camera - * @details This returns the n-th camera of the camera rig, which can be used - * for various stuff like showing its frustum, passing it as a shader input, - * and so on. - * - * The first camera is the camera which is the camera of the first split, - * which is the split closest to the camera. All cameras follow in descending - * order until to the last camera, which is the split furthest away from the - * camera. - * - * If an invalid index is passed, an assertion is thrown. - * - * @param index Index of the camera. - * @return [description] - */ -inline NodePath PSSMCameraRig::get_camera(size_t index) { - nassertr(index >= 0 && index < _cam_nodes.size(), NodePath()); - return _cam_nodes[index]; -} - -/** - * @brief Internal method to compute the distance of a split - * @details This is the internal method to perform the weighting of the - * logarithmic and linear distribution. It computes the distance to the - * camera from which a given split starts, by weighting the logarithmic and - * linear factor. - * - * The return value is a value ranging from 0 .. 1. To get the distance in - * world space, the value has to get multiplied with the maximum shadow distance. - * - * @param split_index The index of the split - * @return Distance of the split, ranging from 0 .. 1 - */ -inline float PSSMCameraRig::get_split_start(size_t split_index) { - float x = (float)split_index / (float)_cam_nodes.size(); - return (exp(_logarithmic_factor*x)-1) / (exp(_logarithmic_factor)-1); -} - -/** - * @brief Internal method for interpolating a point along the camera frustum - * @details This method takes a given distance in the 0 .. 1 range, whereas - * 0 denotes the camera near plane, and 1 denotes the camera far plane, - * and lineary interpolates between them. - * - * @param origin Edge of the frustum - * @param depth Distance in the 0 .. 1 range - * - * @return interpolated point in world space - */ -inline LPoint3f PSSMCameraRig::get_interpolated_point(CoordinateOrigin origin, float depth) { - nassertr(depth >= 0.0 && depth <= 1.0, LPoint3f()); - return _curr_near_points[origin] * (1.0 - depth) + _curr_far_points[origin] * depth; -} - -/** - * @brief Returns a handle to the MVP array - * @details This returns a handle to the array of view-projection matrices - * of the different splits. This can be used for computing shadows. The array - * is a PTALMatrix4f and thus can be directly bound to a shader. - * - * @return view-projection matrix array - */ -inline const PTA_LMatrix4f &PSSMCameraRig::get_mvp_array() { - return _camera_mvps; -} - -/** - * @brief Returns a handle to the near and far planes array - * @details This returns a handle to the near and far plane array. Each split - * has an entry in the array, whereas the x component of the vecto denotes the - * near plane, and the y component denotes the far plane of the split. - * - * This is required because the near and far planes of the splits change - * constantly. To access them in a shader, the shader needs access to the - * array. - * - * @return Array of near and far planes - */ -inline const PTA_LVecBase2f &PSSMCameraRig::get_nearfar_array() { - return _camera_nearfar; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +/** + * @brief Sets the maximum pssm distance. + * @details This sets the maximum distance in world space until which shadows + * are rendered. After this distance, no shadows will be rendered. + * + * If the distance is below zero, an assertion is triggered. + * + * @param distance Maximum distance in world space + */ +inline void PSSMCameraRig::set_pssm_distance(float distance) { + nassertv(distance > 0.0 && distance < 100000.0); + _pssm_distance = distance; +} + +/** + * @brief Sets the suns distance + * @details This sets the distance the cameras will have from the cameras frustum. + * This prevents far objects from having no shadows, which can occur when these + * objects are between the cameras frustum and the sun, but not inside of the + * cameras frustum. Setting the sun distance high enough will move the cameras + * away from the camera frustum, being able to cover those distant objects too. + * + * If the sun distance is set too high, artifacts will occur due to the reduced + * range of depth. If a value below zero is passed, an assertion will get + * triggered. + * + * @param distance The sun distance + */ +inline void PSSMCameraRig::set_sun_distance(float distance) { + nassertv(distance > 0.0 && distance < 100000.0); + _sun_distance = distance; +} + +/** + * @brief Sets the logarithmic factor + * @details This sets the logarithmic factor, which is the core of the algorithm. + * PSSM splits the camera frustum based on a linear and a logarithmic factor. + * While a linear factor provides a good distribution, it often is not applicable + * for wider distances. A logarithmic distribution provides a better distribution + * at distance, but suffers from splitting in the near areas. + * + * The logarithmic factor mixes the logarithmic and linear split distribution, + * to get the best of both. A greater factor will make the distribution more + * logarithmic, while a smaller factor will make it more linear. + * + * If the factor is below zero, an ssertion is triggered. + * + * @param factor The logarithmic factor + */ +inline void PSSMCameraRig::set_logarithmic_factor(float factor) { + nassertv(factor > 0.0); + _logarithmic_factor = factor; +} + +/** + * @brief Sets whether to use a fixed film size + * @details This controls if a fixed film size should be used. This will cause + * the camera rig to cache the current film size, and only change it in case + * it gets too small. This provides less flickering when moving, because the + * film size will stay roughly constant. However, to prevent the cached film + * size getting too big, one should call PSSMCameraRig::reset_film_size + * once in a while, otherwise there might be a lot of wasted space. + * + * @param flag Whether to use a fixed film size + */ +inline void PSSMCameraRig::set_use_fixed_film_size(bool flag) { + _use_fixed_film_size = flag; +} + +/** + * @brief Sets the resolution of each split + * @details This sets the resolution of each split. Currently it is equal for + * each split. This is required when using PSSMCameraRig::set_use_stable_csm, + * to compute how bix a texel is. + * + * It has to match the y-resolution of the pssm shadow map. If an invalid + * resolution is triggered, an assertion is thrown. + * + * @param resolution The resolution of each split. + */ +inline void PSSMCameraRig::set_resolution(size_t resolution) { + nassertv(resolution >= 0 && resolution < 65535); + _resolution = resolution; +} + +/** + * @brief Sets whether to use stable CSM snapping. + * @details This option controls if stable CSM snapping should be used. When the + * option is enabled, all splits will snap to their texels, so that when moving, + * no flickering will occur. However, this only works when the splits do not + * change their film size, rotation and angle. + * + * @param flag Whether to use stable CSM snapping + */ +inline void PSSMCameraRig::set_use_stable_csm(bool flag) { + _use_stable_csm = flag; +} + +/** + * @brief Sets the border bias for each split + * @details This sets the border bias for every split. This increases each + * splits frustum by multiplying it by (1 + bias), and helps reducing artifacts + * at the borders of the splits. Artifacts can occur when the bias is too low, + * because then the filtering will go over the bounds of the split, producing + * invalid results. + * + * If the bias is below zero, an assertion is thrown. + * + * @param bias Border bias + */ +inline void PSSMCameraRig::set_border_bias(float bias) { + nassertv(bias >= 0.0); + _border_bias = bias; +} + +/** + * @brief Resets the film size cache + * @details In case PSSMCameraRig::set_use_fixed_film_size is used, this resets + * the film size cache. This might lead to a small "jump" in the shadows, + * because the film size changes, however it leads to a better shadow distribution. + * + * This is the case because when using a fixed film size, the cache will get + * bigger and bigger, whenever the camera moves to a grazing angle. However, + * when moving back to a normal angle, the film size cache still stores this + * big angle, and thus the splits will have a much bigger film size than actualy + * required. To prevent this, call this method once in a while, so an optimal + * distribution is ensured. + */ +inline void PSSMCameraRig::reset_film_size_cache() { + for (size_t i = 0; i < _max_film_sizes.size(); ++i) { + _max_film_sizes[i].fill(0); + } +} + +/** + * @brief Returns the n-th camera + * @details This returns the n-th camera of the camera rig, which can be used + * for various stuff like showing its frustum, passing it as a shader input, + * and so on. + * + * The first camera is the camera which is the camera of the first split, + * which is the split closest to the camera. All cameras follow in descending + * order until to the last camera, which is the split furthest away from the + * camera. + * + * If an invalid index is passed, an assertion is thrown. + * + * @param index Index of the camera. + * @return [description] + */ +inline NodePath PSSMCameraRig::get_camera(size_t index) { + nassertr(index >= 0 && index < _cam_nodes.size(), NodePath()); + return _cam_nodes[index]; +} + +/** + * @brief Internal method to compute the distance of a split + * @details This is the internal method to perform the weighting of the + * logarithmic and linear distribution. It computes the distance to the + * camera from which a given split starts, by weighting the logarithmic and + * linear factor. + * + * The return value is a value ranging from 0 .. 1. To get the distance in + * world space, the value has to get multiplied with the maximum shadow distance. + * + * @param split_index The index of the split + * @return Distance of the split, ranging from 0 .. 1 + */ +inline float PSSMCameraRig::get_split_start(size_t split_index) { + float x = (float)split_index / (float)_cam_nodes.size(); + return (exp(_logarithmic_factor*x)-1) / (exp(_logarithmic_factor)-1); +} + +/** + * @brief Internal method for interpolating a point along the camera frustum + * @details This method takes a given distance in the 0 .. 1 range, whereas + * 0 denotes the camera near plane, and 1 denotes the camera far plane, + * and lineary interpolates between them. + * + * @param origin Edge of the frustum + * @param depth Distance in the 0 .. 1 range + * + * @return interpolated point in world space + */ +inline LPoint3f PSSMCameraRig::get_interpolated_point(CoordinateOrigin origin, float depth) { + nassertr(depth >= 0.0 && depth <= 1.0, LPoint3f()); + return _curr_near_points[origin] * (1.0 - depth) + _curr_far_points[origin] * depth; +} + +/** + * @brief Returns a handle to the MVP array + * @details This returns a handle to the array of view-projection matrices + * of the different splits. This can be used for computing shadows. The array + * is a PTALMatrix4f and thus can be directly bound to a shader. + * + * @return view-projection matrix array + */ +inline const PTA_LMatrix4f &PSSMCameraRig::get_mvp_array() { + return _camera_mvps; +} + +/** + * @brief Returns a handle to the near and far planes array + * @details This returns a handle to the near and far plane array. Each split + * has an entry in the array, whereas the x component of the vecto denotes the + * near plane, and the y component denotes the far plane of the split. + * + * This is required because the near and far planes of the splits change + * constantly. To access them in a shader, the shader needs access to the + * array. + * + * @return Array of near and far planes + */ +inline const PTA_LVecBase2f &PSSMCameraRig::get_nearfar_array() { + return _camera_nearfar; +} diff --git a/RenderPipelineFile/rpcore/native/source/pssm_camera_rig.cpp b/RenderPipelineFile/rpcore/native/source/pssm_camera_rig.cpp index ee93b665..e8eb756d 100644 --- a/RenderPipelineFile/rpcore/native/source/pssm_camera_rig.cpp +++ b/RenderPipelineFile/rpcore/native/source/pssm_camera_rig.cpp @@ -1,396 +1,396 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -#include "pssm_camera_rig.h" - -#define _USE_MATH_DEFINES -#include -#include "orthographicLens.h" - - -PStatCollector PSSMCameraRig::_update_collector("App:Show code:RP_PSSM_update"); - -/** - * @brief Constructs a new PSSM camera rig - * @details This constructs a new camera rig, with a given amount of splits. - * The splits can not be changed later on. Splits are also called Cascades. - * - * An assertion will be triggered if the splits are below zero. - * - * @param num_splits Amount of PSSM splits - */ -PSSMCameraRig::PSSMCameraRig(size_t num_splits) { - nassertv(num_splits > 0); - _num_splits = num_splits; - _pssm_distance = 100.0; - _sun_distance = 500.0; - _use_fixed_film_size = false; - _use_stable_csm = true; - _logarithmic_factor = 1.0; - _resolution = 512; - _border_bias = 0.1; - _camera_mvps = PTA_LMatrix4f::empty_array(num_splits); - _camera_nearfar = PTA_LVecBase2f::empty_array(num_splits); - init_cam_nodes(); -} - -/** - * @brief Destructs the camera rig - * @details This destructs the camera rig, cleaning up all used resources. - */ -PSSMCameraRig::~PSSMCameraRig() { - // TODO: Detach all cameras and call remove_node. Most likely this is not - // an issue tho, because the camera rig will never get destructed. -} - -/** - * @brief Internal method to init the cameras - * @details This method constructs all cameras and their required lens nodes - * for all splits. It also resets the film size array. - */ -void PSSMCameraRig::init_cam_nodes() { - _cam_nodes.reserve(_num_splits); - _max_film_sizes.resize(_num_splits); - _cameras.resize(_num_splits); - for (size_t i = 0; i < _num_splits; ++i) - { - // Construct a new lens - Lens *lens = new OrthographicLens(); - lens->set_film_size(1, 1); - lens->set_near_far(1, 1000); - - // Construct a new camera - _cameras[i] = new Camera("pssm-cam-" + to_string(((long long)i)), lens); - _cam_nodes.push_back(NodePath(_cameras[i])); - _max_film_sizes[i].fill(0); - } -} - -/** - * @brief Reparents the camera rig - * @details This reparents all cameras to the given parent. Usually the parent - * will be ShowBase.render. The parent should be the same node where the - * main camera is located in, too. - * - * If an empty parrent is passed, an assertion will get triggered. - * - * @param parent Parent node path - */ -void PSSMCameraRig::reparent_to(NodePath parent) { - nassertv(!parent.is_empty()); - for (size_t i = 0; i < _num_splits; ++i) { - _cam_nodes[i].reparent_to(parent); - } - _parent = parent; -} - -/** - * @brief Internal method to compute the view-projection matrix of a camera - * @details This returns the view-projection matrix of the given split. No bounds - * checking is done. If an invalid index is passed, undefined behaviour occurs. - * - * @param split_index Index of the split - * @return view-projection matrix of the split - */ -LMatrix4f PSSMCameraRig::compute_mvp(size_t split_index) { - LMatrix4f transform = _parent.get_transform(_cam_nodes[split_index])->get_mat(); - return transform * _cameras[split_index]->get_lens()->get_projection_mat(); -} - -/** - * @brief Internal method used for stable CSM - * @details This method is used when stable CSM is enabled. It ensures that each - * source only moves in texel-steps, thus preventing flickering. This works by - * projecting the point (0, 0, 0) to NDC space, making sure that it gets projected - * to a texel center, and then projecting that texel back. - * - * This only works if the camera does not rotate, change its film size, or change - * its angle. - * - * @param mat view-projection matrix of the camera - * @param resolution resolution of the split - * - * @return Offset to add to the camera position to achieve stable snapping - */ -LVecBase3f PSSMCameraRig::get_snap_offset(const LMatrix4f& mat, size_t resolution) { - // Transform origin to camera space - LPoint4f base_point = mat.get_row(3) * 0.5 + 0.5; - - // Compute the snap offset - float texel_size = 1.0 / (float)(resolution); - float offset_x = fmod(base_point.get_x(), texel_size); - float offset_y = fmod(base_point.get_y(), texel_size); - - // Reproject the offset back, for that we need the inverse MVP - LMatrix4f inv_mat(mat); - inv_mat.invert_in_place(); - LVecBase3f new_base_point = inv_mat.xform_point(LVecBase3f( - (base_point.get_x() - offset_x) * 2.0 - 1.0, - (base_point.get_y() - offset_y) * 2.0 - 1.0, - base_point.get_z() * 2.0 - 1.0 - )); - return -new_base_point; -} - -/** - * @brief Computes the average of a list of points - * @details This computes the average over a given set of points in 3D space. - * It returns the average of those points, namely sum_of_points / num_points. - * - * It is designed to work with a frustum, which is why it takes two arrays - * with a dimension of 4. Usually the first array are the camera near points, - * and the second array are the camera far points. - * - * @param starts First array of points - * @param ends Second array of points - * @return Average of points - */ -LPoint3f get_average_of_points(LVecBase3f const (&starts)[4], LVecBase3f const (&ends)[4]) { - LPoint3f mid_point(0, 0, 0); - for (size_t k = 0; k < 4; ++k) { - mid_point += starts[k]; - mid_point += ends[k]; - } - return mid_point / 8.0; -} - -/** - * @brief Finds the minimum and maximum extends of the given projection - * @details This projects each point of the given array of points using the - * cameras view-projection matrix, and computes the minimum and maximum - * of the projected points. - * - * @param min_extent Will store the minimum extent of the projected points in NDC space - * @param max_extent Will store the maximum extent of the projected points in NDC space - * @param transform The transformation matrix of the camera - * @param proj_points The array of points to project - * @param cam The camera to be used to project the points - */ -void find_min_max_extents(LVecBase3f &min_extent, LVecBase3f &max_extent, const LMatrix4f &transform, LVecBase3f const (&proj_points)[8], Camera *cam) { - - min_extent.fill(1e10); - max_extent.fill(-1e10); - LPoint2f screen_points[8]; - - // Now project all points to the screen space of the current camera and also - // find the minimum and maximum extents - for (size_t k = 0; k < 8; ++k) { - LVecBase4f point(proj_points[k], 1); - LPoint4f proj_point = transform.xform(point); - LPoint3f proj_point_3d(proj_point.get_x(), proj_point.get_y(), proj_point.get_z()); - cam->get_lens()->project(proj_point_3d, screen_points[k]); - - // Find min / max extents - if (screen_points[k].get_x() > max_extent.get_x()) max_extent.set_x(screen_points[k].get_x()); - if (screen_points[k].get_y() > max_extent.get_y()) max_extent.set_y(screen_points[k].get_y()); - - if (screen_points[k].get_x() < min_extent.get_x()) min_extent.set_x(screen_points[k].get_x()); - if (screen_points[k].get_y() < min_extent.get_y()) min_extent.set_y(screen_points[k].get_y()); - - // Find min / max projected depth to adjust far plane - if (proj_point.get_y() > max_extent.get_z()) max_extent.set_z(proj_point.get_y()); - if (proj_point.get_y() < min_extent.get_z()) min_extent.set_z(proj_point.get_y()); - } -} - -/** - * @brief Computes a film size from a given minimum and maximum extend - * @details This takes a minimum and maximum extent in NDC space and computes - * the film size and film offset needed to cover that extent. - * - * @param film_size Output film size, can be used for Lens::set_film_size - * @param film_offset Output film offset, can be used for Lens::set_film_offset - * @param min_extent Minimum extent - * @param max_extent Maximum extent - */ -inline void get_film_properties(LVecBase2f &film_size, LVecBase2f &film_offset, const LVecBase3f &min_extent, const LVecBase3f &max_extent) { - float x_center = (min_extent.get_x() + max_extent.get_x()) * 0.5; - float y_center = (min_extent.get_y() + max_extent.get_y()) * 0.5; - float x_size = max_extent.get_x() - x_center; - float y_size = max_extent.get_y() - y_center; - film_size.set(x_size, y_size); - film_offset.set(x_center * 0.5, y_center * 0.5); -} - -/** - * @brief Merges two arrays - * @details This takes two arrays which each 4 members and produces an array - * with both arrays contained. - * - * @param dest Destination array - * @param array1 First array - * @param array2 Second array - */ -inline void merge_points_interleaved(LVecBase3f (&dest)[8], LVecBase3f const (&array1)[4], LVecBase3f const (&array2)[4]) { - for (size_t k = 0; k < 4; ++k) { - dest[k] = array1[k]; - dest[k+4] = array2[k]; - } -} - - -/** - * @brief Internal method to compute the splits - * @details This is the internal update method to update the PSSM splits. - * It distributes the camera splits over the frustum, and updates the - * MVP array aswell as the nearfar array. - * - * @param transform Main camera transform - * @param max_distance Maximum pssm distance, relative to the camera far plane - * @param light_vector Sun-Vector - */ -void PSSMCameraRig::compute_pssm_splits(const LMatrix4f& transform, float max_distance, const LVecBase3f& light_vector) { - nassertv(!_parent.is_empty()); - - // PSSM Distance should never be smaller than camera far plane. - nassertv(max_distance <= 1.0); - - float filmsize_bias = 1.0 + _border_bias; - - // Compute the positions of all cameras - for (size_t i = 0; i < _cam_nodes.size(); ++i) { - float split_start = get_split_start(i) * max_distance; - float split_end = get_split_start(i + 1) * max_distance; - - LVecBase3f start_points[4]; - LVecBase3f end_points[4]; - LVecBase3f proj_points[8]; - - // Get split bounding box, and collect all points which define the frustum - for (size_t k = 0; k < 4; ++k) { - start_points[k] = get_interpolated_point((CoordinateOrigin)k, split_start); - end_points[k] = get_interpolated_point((CoordinateOrigin)k, split_end); - proj_points[k] = start_points[k]; - proj_points[k + 4] = end_points[k]; - } - - // Compute approximate split mid point - LPoint3f split_mid = get_average_of_points(start_points, end_points); - LPoint3f cam_start = split_mid + light_vector * _sun_distance; - - // Reset the film size, offset and far-plane - Camera* cam = DCAST(Camera, _cam_nodes[i].node()); - cam->get_lens()->set_film_size(1, 1); - cam->get_lens()->set_film_offset(0, 0); - cam->get_lens()->set_near_far(1, 100); - - // Find a good initial position - _cam_nodes[i].set_pos(cam_start); - _cam_nodes[i].look_at(split_mid); - - LVecBase3f best_min_extent, best_max_extent; - - // Find minimum and maximum extents of the points - LMatrix4f merged_transform = _parent.get_transform(_cam_nodes[i])->get_mat(); - find_min_max_extents(best_min_extent, best_max_extent, merged_transform, proj_points, cam); - - // Find the film size to cover all points - LVecBase2f film_size, film_offset; - get_film_properties(film_size, film_offset, best_min_extent, best_max_extent); - - if (_use_fixed_film_size) { - // In case we use a fixed film size, store the maximum film size, and - // only change the film size if a new maximum is there - if (_max_film_sizes[i].get_x() < film_size.get_x()) _max_film_sizes[i].set_x(film_size.get_x()); - if (_max_film_sizes[i].get_y() < film_size.get_y()) _max_film_sizes[i].set_y(film_size.get_y()); - - cam->get_lens()->set_film_size(_max_film_sizes[i] * filmsize_bias); - } else { - // If we don't use a fixed film size, we can just set the film size - // on the lens. - cam->get_lens()->set_film_size(film_size * filmsize_bias); - } - - // Compute new film offset - cam->get_lens()->set_film_offset(film_offset); - cam->get_lens()->set_near_far(10, best_max_extent.get_z()); - _camera_nearfar[i] = LVecBase2f(10, best_max_extent.get_z()); - - // Compute the camera MVP - LMatrix4f mvp = compute_mvp(i); - - // Stable CSM Snapping - if (_use_stable_csm) { - LPoint3f snap_offset = get_snap_offset(mvp, _resolution); - _cam_nodes[i].set_pos(_cam_nodes[i].get_pos() + snap_offset); - - // Compute the new mvp, since we changed the snap offset - mvp = compute_mvp(i); - } - - _camera_mvps.set_element(i, mvp); - } -} - - -/** - * @brief Updates the PSSM camera rig - * @details This updates the rig with an updated camera position, and a given - * light vector. This should be called on a per-frame basis. It will reposition - * all camera sources to fit the frustum based on the pssm distribution. - * - * The light vector should be the vector from the light source, not the - * vector to the light source. - * - * @param cam_node Target camera node - * @param light_vector The vector from the light to any point - */ -void PSSMCameraRig::update(NodePath cam_node, const LVecBase3f &light_vector) { - nassertv(!cam_node.is_empty()); - _update_collector.start(); - - // Get camera node transform - LMatrix4f transform = cam_node.get_transform()->get_mat(); - - // Get Camera and Lens pointers - Camera* cam = DCAST(Camera, cam_node.get_child(0).node()); - nassertv(cam != NULL); - Lens* lens = cam->get_lens(); - - // Extract near and far points: - lens->extrude(LPoint2f(-1, 1), _curr_near_points[UpperLeft], _curr_far_points[UpperLeft]); - lens->extrude(LPoint2f(1, 1), _curr_near_points[UpperRight], _curr_far_points[UpperRight]); - lens->extrude(LPoint2f(-1, -1), _curr_near_points[LowerLeft], _curr_far_points[LowerLeft]); - lens->extrude(LPoint2f(1, -1), _curr_near_points[LowerRight], _curr_far_points[LowerRight]); - - // Construct MVP to project points to world space - LMatrix4f mvp = transform * lens->get_view_mat(); - - // Project all points to world space - for (size_t i = 0; i < 4; ++i) { - LPoint4f ws_near = mvp.xform(_curr_near_points[i]); - LPoint4f ws_far = mvp.xform(_curr_far_points[i]); - _curr_near_points[i].set(ws_near.get_x(), ws_near.get_y(), ws_near.get_z()); - _curr_far_points[i].set(ws_far.get_x(), ws_far.get_y(), ws_far.get_z()); - } - - // Do the actual PSSM - compute_pssm_splits( transform, _pssm_distance / lens->get_far(), light_vector ); - - _update_collector.stop(); -} - +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +#include "pssm_camera_rig.h" + +#define _USE_MATH_DEFINES +#include +#include "orthographicLens.h" + + +PStatCollector PSSMCameraRig::_update_collector("App:Show code:RP_PSSM_update"); + +/** + * @brief Constructs a new PSSM camera rig + * @details This constructs a new camera rig, with a given amount of splits. + * The splits can not be changed later on. Splits are also called Cascades. + * + * An assertion will be triggered if the splits are below zero. + * + * @param num_splits Amount of PSSM splits + */ +PSSMCameraRig::PSSMCameraRig(size_t num_splits) { + nassertv(num_splits > 0); + _num_splits = num_splits; + _pssm_distance = 100.0; + _sun_distance = 500.0; + _use_fixed_film_size = false; + _use_stable_csm = true; + _logarithmic_factor = 1.0; + _resolution = 512; + _border_bias = 0.1; + _camera_mvps = PTA_LMatrix4f::empty_array(num_splits); + _camera_nearfar = PTA_LVecBase2f::empty_array(num_splits); + init_cam_nodes(); +} + +/** + * @brief Destructs the camera rig + * @details This destructs the camera rig, cleaning up all used resources. + */ +PSSMCameraRig::~PSSMCameraRig() { + // TODO: Detach all cameras and call remove_node. Most likely this is not + // an issue tho, because the camera rig will never get destructed. +} + +/** + * @brief Internal method to init the cameras + * @details This method constructs all cameras and their required lens nodes + * for all splits. It also resets the film size array. + */ +void PSSMCameraRig::init_cam_nodes() { + _cam_nodes.reserve(_num_splits); + _max_film_sizes.resize(_num_splits); + _cameras.resize(_num_splits); + for (size_t i = 0; i < _num_splits; ++i) + { + // Construct a new lens + Lens *lens = new OrthographicLens(); + lens->set_film_size(1, 1); + lens->set_near_far(1, 1000); + + // Construct a new camera + _cameras[i] = new Camera("pssm-cam-" + to_string(((long long)i)), lens); + _cam_nodes.push_back(NodePath(_cameras[i])); + _max_film_sizes[i].fill(0); + } +} + +/** + * @brief Reparents the camera rig + * @details This reparents all cameras to the given parent. Usually the parent + * will be ShowBase.render. The parent should be the same node where the + * main camera is located in, too. + * + * If an empty parrent is passed, an assertion will get triggered. + * + * @param parent Parent node path + */ +void PSSMCameraRig::reparent_to(NodePath parent) { + nassertv(!parent.is_empty()); + for (size_t i = 0; i < _num_splits; ++i) { + _cam_nodes[i].reparent_to(parent); + } + _parent = parent; +} + +/** + * @brief Internal method to compute the view-projection matrix of a camera + * @details This returns the view-projection matrix of the given split. No bounds + * checking is done. If an invalid index is passed, undefined behaviour occurs. + * + * @param split_index Index of the split + * @return view-projection matrix of the split + */ +LMatrix4f PSSMCameraRig::compute_mvp(size_t split_index) { + LMatrix4f transform = _parent.get_transform(_cam_nodes[split_index])->get_mat(); + return transform * _cameras[split_index]->get_lens()->get_projection_mat(); +} + +/** + * @brief Internal method used for stable CSM + * @details This method is used when stable CSM is enabled. It ensures that each + * source only moves in texel-steps, thus preventing flickering. This works by + * projecting the point (0, 0, 0) to NDC space, making sure that it gets projected + * to a texel center, and then projecting that texel back. + * + * This only works if the camera does not rotate, change its film size, or change + * its angle. + * + * @param mat view-projection matrix of the camera + * @param resolution resolution of the split + * + * @return Offset to add to the camera position to achieve stable snapping + */ +LVecBase3f PSSMCameraRig::get_snap_offset(const LMatrix4f& mat, size_t resolution) { + // Transform origin to camera space + LPoint4f base_point = mat.get_row(3) * 0.5 + 0.5; + + // Compute the snap offset + float texel_size = 1.0 / (float)(resolution); + float offset_x = fmod(base_point.get_x(), texel_size); + float offset_y = fmod(base_point.get_y(), texel_size); + + // Reproject the offset back, for that we need the inverse MVP + LMatrix4f inv_mat(mat); + inv_mat.invert_in_place(); + LVecBase3f new_base_point = inv_mat.xform_point(LVecBase3f( + (base_point.get_x() - offset_x) * 2.0 - 1.0, + (base_point.get_y() - offset_y) * 2.0 - 1.0, + base_point.get_z() * 2.0 - 1.0 + )); + return -new_base_point; +} + +/** + * @brief Computes the average of a list of points + * @details This computes the average over a given set of points in 3D space. + * It returns the average of those points, namely sum_of_points / num_points. + * + * It is designed to work with a frustum, which is why it takes two arrays + * with a dimension of 4. Usually the first array are the camera near points, + * and the second array are the camera far points. + * + * @param starts First array of points + * @param ends Second array of points + * @return Average of points + */ +LPoint3f get_average_of_points(LVecBase3f const (&starts)[4], LVecBase3f const (&ends)[4]) { + LPoint3f mid_point(0, 0, 0); + for (size_t k = 0; k < 4; ++k) { + mid_point += starts[k]; + mid_point += ends[k]; + } + return mid_point / 8.0; +} + +/** + * @brief Finds the minimum and maximum extends of the given projection + * @details This projects each point of the given array of points using the + * cameras view-projection matrix, and computes the minimum and maximum + * of the projected points. + * + * @param min_extent Will store the minimum extent of the projected points in NDC space + * @param max_extent Will store the maximum extent of the projected points in NDC space + * @param transform The transformation matrix of the camera + * @param proj_points The array of points to project + * @param cam The camera to be used to project the points + */ +void find_min_max_extents(LVecBase3f &min_extent, LVecBase3f &max_extent, const LMatrix4f &transform, LVecBase3f const (&proj_points)[8], Camera *cam) { + + min_extent.fill(1e10); + max_extent.fill(-1e10); + LPoint2f screen_points[8]; + + // Now project all points to the screen space of the current camera and also + // find the minimum and maximum extents + for (size_t k = 0; k < 8; ++k) { + LVecBase4f point(proj_points[k], 1); + LPoint4f proj_point = transform.xform(point); + LPoint3f proj_point_3d(proj_point.get_x(), proj_point.get_y(), proj_point.get_z()); + cam->get_lens()->project(proj_point_3d, screen_points[k]); + + // Find min / max extents + if (screen_points[k].get_x() > max_extent.get_x()) max_extent.set_x(screen_points[k].get_x()); + if (screen_points[k].get_y() > max_extent.get_y()) max_extent.set_y(screen_points[k].get_y()); + + if (screen_points[k].get_x() < min_extent.get_x()) min_extent.set_x(screen_points[k].get_x()); + if (screen_points[k].get_y() < min_extent.get_y()) min_extent.set_y(screen_points[k].get_y()); + + // Find min / max projected depth to adjust far plane + if (proj_point.get_y() > max_extent.get_z()) max_extent.set_z(proj_point.get_y()); + if (proj_point.get_y() < min_extent.get_z()) min_extent.set_z(proj_point.get_y()); + } +} + +/** + * @brief Computes a film size from a given minimum and maximum extend + * @details This takes a minimum and maximum extent in NDC space and computes + * the film size and film offset needed to cover that extent. + * + * @param film_size Output film size, can be used for Lens::set_film_size + * @param film_offset Output film offset, can be used for Lens::set_film_offset + * @param min_extent Minimum extent + * @param max_extent Maximum extent + */ +inline void get_film_properties(LVecBase2f &film_size, LVecBase2f &film_offset, const LVecBase3f &min_extent, const LVecBase3f &max_extent) { + float x_center = (min_extent.get_x() + max_extent.get_x()) * 0.5; + float y_center = (min_extent.get_y() + max_extent.get_y()) * 0.5; + float x_size = max_extent.get_x() - x_center; + float y_size = max_extent.get_y() - y_center; + film_size.set(x_size, y_size); + film_offset.set(x_center * 0.5, y_center * 0.5); +} + +/** + * @brief Merges two arrays + * @details This takes two arrays which each 4 members and produces an array + * with both arrays contained. + * + * @param dest Destination array + * @param array1 First array + * @param array2 Second array + */ +inline void merge_points_interleaved(LVecBase3f (&dest)[8], LVecBase3f const (&array1)[4], LVecBase3f const (&array2)[4]) { + for (size_t k = 0; k < 4; ++k) { + dest[k] = array1[k]; + dest[k+4] = array2[k]; + } +} + + +/** + * @brief Internal method to compute the splits + * @details This is the internal update method to update the PSSM splits. + * It distributes the camera splits over the frustum, and updates the + * MVP array aswell as the nearfar array. + * + * @param transform Main camera transform + * @param max_distance Maximum pssm distance, relative to the camera far plane + * @param light_vector Sun-Vector + */ +void PSSMCameraRig::compute_pssm_splits(const LMatrix4f& transform, float max_distance, const LVecBase3f& light_vector) { + nassertv(!_parent.is_empty()); + + // PSSM Distance should never be smaller than camera far plane. + nassertv(max_distance <= 1.0); + + float filmsize_bias = 1.0 + _border_bias; + + // Compute the positions of all cameras + for (size_t i = 0; i < _cam_nodes.size(); ++i) { + float split_start = get_split_start(i) * max_distance; + float split_end = get_split_start(i + 1) * max_distance; + + LVecBase3f start_points[4]; + LVecBase3f end_points[4]; + LVecBase3f proj_points[8]; + + // Get split bounding box, and collect all points which define the frustum + for (size_t k = 0; k < 4; ++k) { + start_points[k] = get_interpolated_point((CoordinateOrigin)k, split_start); + end_points[k] = get_interpolated_point((CoordinateOrigin)k, split_end); + proj_points[k] = start_points[k]; + proj_points[k + 4] = end_points[k]; + } + + // Compute approximate split mid point + LPoint3f split_mid = get_average_of_points(start_points, end_points); + LPoint3f cam_start = split_mid + light_vector * _sun_distance; + + // Reset the film size, offset and far-plane + Camera* cam = DCAST(Camera, _cam_nodes[i].node()); + cam->get_lens()->set_film_size(1, 1); + cam->get_lens()->set_film_offset(0, 0); + cam->get_lens()->set_near_far(1, 100); + + // Find a good initial position + _cam_nodes[i].set_pos(cam_start); + _cam_nodes[i].look_at(split_mid); + + LVecBase3f best_min_extent, best_max_extent; + + // Find minimum and maximum extents of the points + LMatrix4f merged_transform = _parent.get_transform(_cam_nodes[i])->get_mat(); + find_min_max_extents(best_min_extent, best_max_extent, merged_transform, proj_points, cam); + + // Find the film size to cover all points + LVecBase2f film_size, film_offset; + get_film_properties(film_size, film_offset, best_min_extent, best_max_extent); + + if (_use_fixed_film_size) { + // In case we use a fixed film size, store the maximum film size, and + // only change the film size if a new maximum is there + if (_max_film_sizes[i].get_x() < film_size.get_x()) _max_film_sizes[i].set_x(film_size.get_x()); + if (_max_film_sizes[i].get_y() < film_size.get_y()) _max_film_sizes[i].set_y(film_size.get_y()); + + cam->get_lens()->set_film_size(_max_film_sizes[i] * filmsize_bias); + } else { + // If we don't use a fixed film size, we can just set the film size + // on the lens. + cam->get_lens()->set_film_size(film_size * filmsize_bias); + } + + // Compute new film offset + cam->get_lens()->set_film_offset(film_offset); + cam->get_lens()->set_near_far(10, best_max_extent.get_z()); + _camera_nearfar[i] = LVecBase2f(10, best_max_extent.get_z()); + + // Compute the camera MVP + LMatrix4f mvp = compute_mvp(i); + + // Stable CSM Snapping + if (_use_stable_csm) { + LPoint3f snap_offset = get_snap_offset(mvp, _resolution); + _cam_nodes[i].set_pos(_cam_nodes[i].get_pos() + snap_offset); + + // Compute the new mvp, since we changed the snap offset + mvp = compute_mvp(i); + } + + _camera_mvps.set_element(i, mvp); + } +} + + +/** + * @brief Updates the PSSM camera rig + * @details This updates the rig with an updated camera position, and a given + * light vector. This should be called on a per-frame basis. It will reposition + * all camera sources to fit the frustum based on the pssm distribution. + * + * The light vector should be the vector from the light source, not the + * vector to the light source. + * + * @param cam_node Target camera node + * @param light_vector The vector from the light to any point + */ +void PSSMCameraRig::update(NodePath cam_node, const LVecBase3f &light_vector) { + nassertv(!cam_node.is_empty()); + _update_collector.start(); + + // Get camera node transform + LMatrix4f transform = cam_node.get_transform()->get_mat(); + + // Get Camera and Lens pointers + Camera* cam = DCAST(Camera, cam_node.get_child(0).node()); + nassertv(cam != NULL); + Lens* lens = cam->get_lens(); + + // Extract near and far points: + lens->extrude(LPoint2f(-1, 1), _curr_near_points[UpperLeft], _curr_far_points[UpperLeft]); + lens->extrude(LPoint2f(1, 1), _curr_near_points[UpperRight], _curr_far_points[UpperRight]); + lens->extrude(LPoint2f(-1, -1), _curr_near_points[LowerLeft], _curr_far_points[LowerLeft]); + lens->extrude(LPoint2f(1, -1), _curr_near_points[LowerRight], _curr_far_points[LowerRight]); + + // Construct MVP to project points to world space + LMatrix4f mvp = transform * lens->get_view_mat(); + + // Project all points to world space + for (size_t i = 0; i < 4; ++i) { + LPoint4f ws_near = mvp.xform(_curr_near_points[i]); + LPoint4f ws_far = mvp.xform(_curr_far_points[i]); + _curr_near_points[i].set(ws_near.get_x(), ws_near.get_y(), ws_near.get_z()); + _curr_far_points[i].set(ws_far.get_x(), ws_far.get_y(), ws_far.get_z()); + } + + // Do the actual PSSM + compute_pssm_splits( transform, _pssm_distance / lens->get_far(), light_vector ); + + _update_collector.stop(); +} + diff --git a/RenderPipelineFile/rpcore/native/source/pssm_camera_rig.h b/RenderPipelineFile/rpcore/native/source/pssm_camera_rig.h index b192eb32..acf3ec06 100644 --- a/RenderPipelineFile/rpcore/native/source/pssm_camera_rig.h +++ b/RenderPipelineFile/rpcore/native/source/pssm_camera_rig.h @@ -1,129 +1,129 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#ifndef PSSM_CAMERA_RIG_H -#define PSSM_CAMERA_RIG_H - -#include "pandabase.h" -#include "luse.h" -#include "camera.h" -#include "nodePath.h" -#include "pStatCollector.h" - -#include - -/** - * @brief Main class used for handling PSSM - * @details This is the main class for supporting PSSM, it is used by the PSSM - * plugin to compute the position of the splits. - * - * It supports handling a varying amount of cameras, and fitting those cameras - * into the main camera frustum, to render distant shadows. It also supports - * various optimizations for fitting the frustum, e.g. rotating the sources - * to get a better coverage. - * - * It also provides methods to get arrays of data about the used cameras - * view-projection matrices and their near and far plane, which is required for - * processing the data in the shadow sampling shader. - * - * In this class, there is often referred to "Splits" or also called "Cascades". - * These denote the different cameras which are used to split the frustum, - * and are a common term related to the PSSM algorithm. - * - * To understand the functionality of this class, a detailed knowledge of the - * PSSM algorithm is helpful. - */ -class PSSMCameraRig { - - PUBLISHED: - PSSMCameraRig(size_t num_splits); - ~PSSMCameraRig(); - - inline void set_pssm_distance(float distance); - inline void set_sun_distance(float distance); - inline void set_use_fixed_film_size(bool flag); - inline void set_resolution(size_t resolution); - inline void set_use_stable_csm(bool flag); - inline void set_logarithmic_factor(float factor); - inline void set_border_bias(float bias); - - void update(NodePath cam_node, const LVecBase3f &light_vector); - inline void reset_film_size_cache(); - - inline NodePath get_camera(size_t index); - - void reparent_to(NodePath parent); - inline const PTA_LMatrix4f &get_mvp_array(); - inline const PTA_LVecBase2f &get_nearfar_array(); - - public: - - // Used to access the near and far points in the array - enum CoordinateOrigin { - UpperLeft = 0, - UpperRight, - LowerLeft, - LowerRight - }; - - protected: - - void init_cam_nodes(); - void compute_pssm_splits(const LMatrix4f& transform, float max_distance, - const LVecBase3f &light_vector); - inline float get_split_start(size_t split_index); - LMatrix4f compute_mvp(size_t cam_index); - inline LPoint3f get_interpolated_point(CoordinateOrigin origin, float depth); - LVecBase3f get_snap_offset(const LMatrix4f& mat, size_t resolution); - - vector _cam_nodes; - vector _cameras; - vector _max_film_sizes; - - // Current near and far points - // Order: UL, UR, LL, LR (See CoordinateOrigin) - LPoint3f _curr_near_points[4]; - LPoint3f _curr_far_points[4]; - float _pssm_distance; - float _sun_distance; - float _logarithmic_factor; - float _border_bias; - bool _use_fixed_film_size; - bool _use_stable_csm; - size_t _resolution; - size_t _num_splits; - NodePath _parent; - - PTA_LMatrix4f _camera_mvps; - PTA_LVecBase2f _camera_nearfar; - - static PStatCollector _update_collector; - -}; - -#include "pssm_camera_rig.I" - -#endif // PSSM_CAMERA_RIG_H +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef PSSM_CAMERA_RIG_H +#define PSSM_CAMERA_RIG_H + +#include "pandabase.h" +#include "luse.h" +#include "camera.h" +#include "nodePath.h" +#include "pStatCollector.h" + +#include + +/** + * @brief Main class used for handling PSSM + * @details This is the main class for supporting PSSM, it is used by the PSSM + * plugin to compute the position of the splits. + * + * It supports handling a varying amount of cameras, and fitting those cameras + * into the main camera frustum, to render distant shadows. It also supports + * various optimizations for fitting the frustum, e.g. rotating the sources + * to get a better coverage. + * + * It also provides methods to get arrays of data about the used cameras + * view-projection matrices and their near and far plane, which is required for + * processing the data in the shadow sampling shader. + * + * In this class, there is often referred to "Splits" or also called "Cascades". + * These denote the different cameras which are used to split the frustum, + * and are a common term related to the PSSM algorithm. + * + * To understand the functionality of this class, a detailed knowledge of the + * PSSM algorithm is helpful. + */ +class PSSMCameraRig { + + PUBLISHED: + PSSMCameraRig(size_t num_splits); + ~PSSMCameraRig(); + + inline void set_pssm_distance(float distance); + inline void set_sun_distance(float distance); + inline void set_use_fixed_film_size(bool flag); + inline void set_resolution(size_t resolution); + inline void set_use_stable_csm(bool flag); + inline void set_logarithmic_factor(float factor); + inline void set_border_bias(float bias); + + void update(NodePath cam_node, const LVecBase3f &light_vector); + inline void reset_film_size_cache(); + + inline NodePath get_camera(size_t index); + + void reparent_to(NodePath parent); + inline const PTA_LMatrix4f &get_mvp_array(); + inline const PTA_LVecBase2f &get_nearfar_array(); + + public: + + // Used to access the near and far points in the array + enum CoordinateOrigin { + UpperLeft = 0, + UpperRight, + LowerLeft, + LowerRight + }; + + protected: + + void init_cam_nodes(); + void compute_pssm_splits(const LMatrix4f& transform, float max_distance, + const LVecBase3f &light_vector); + inline float get_split_start(size_t split_index); + LMatrix4f compute_mvp(size_t cam_index); + inline LPoint3f get_interpolated_point(CoordinateOrigin origin, float depth); + LVecBase3f get_snap_offset(const LMatrix4f& mat, size_t resolution); + + vector _cam_nodes; + vector _cameras; + vector _max_film_sizes; + + // Current near and far points + // Order: UL, UR, LL, LR (See CoordinateOrigin) + LPoint3f _curr_near_points[4]; + LPoint3f _curr_far_points[4]; + float _pssm_distance; + float _sun_distance; + float _logarithmic_factor; + float _border_bias; + bool _use_fixed_film_size; + bool _use_stable_csm; + size_t _resolution; + size_t _num_splits; + NodePath _parent; + + PTA_LMatrix4f _camera_mvps; + PTA_LVecBase2f _camera_nearfar; + + static PStatCollector _update_collector; + +}; + +#include "pssm_camera_rig.I" + +#endif // PSSM_CAMERA_RIG_H diff --git a/RenderPipelineFile/rpcore/native/source/pssm_helper.cpp b/RenderPipelineFile/rpcore/native/source/pssm_helper.cpp index df0ea354..a2bbf2fd 100644 --- a/RenderPipelineFile/rpcore/native/source/pssm_helper.cpp +++ b/RenderPipelineFile/rpcore/native/source/pssm_helper.cpp @@ -1,116 +1,116 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -// Only include the pssm helper if actually required -#ifdef RP_REQ_PSSM_HELPER - -#include "pssm_helper.h" - -#include - -/** - * @brief Internal method to generate a set of equations - * @details This generates a set of equations into a given equation system. - * Based on a given start point and end point, it inserts an equation to the - * system which will, when solved, solve the following equation: - * vec * = expected - * - * @param eq_system The equation system - * @param eq_results The target result vector of the equation system - * @param vec The actual vector to be transformed - * @param expected The expected output of the transformation - * @param offset The index of the equation - */ -void generate_equations(Eigen::MatrixXf &eq_system, Eigen::VectorXf &eq_results, const LVector4f &vec, LVector4f expected, size_t offset) { - size_t write_offset = offset * 4; - for (size_t row = 0; row < 4; ++row) { - float expected_coeff = expected.get_cell(row); - size_t col_offset = row * 4; - for (size_t col = 0; col < 4; ++col) { - eq_system(write_offset, col_offset + col) = vec.get_cell(col); - } - eq_results(write_offset++) = expected_coeff; - } -} -/** - * @brief Finds a projection mat arround the given set of points. - * @details This methods finds a projection matrix which projects the given set - * of frustum points to a unit cube, which can be used as a camera matrix. - * The eight points should determine the frustum uniquely. - * - * @param near_ul The Upper-Left point of the frustum on the near plane - * @param near_ur The Upper-Right point of the frustum on the near plane - * @param near_ll The Lower-Left point of the frustum on the near plane - * @param near_lr The Lower-Right point of the frustum on the near plane - * @param far_ul The Upper-Left point of the frustum on the far plane - * @param far_ur The Upper-Right point of the frustum on the far plane - * @param far_ll The Lower-Left point of the frustum on the far plane - * @param far_lr The Lower-Right point of the frustum on the far plane - * - */ -LMatrix4f PSSMHelper::find_projection_mat( - const LVector4f &near_ul, - const LVector4f &near_ur, - const LVector4f &near_ll, - const LVector4f &near_lr, - - const LVector4f &far_ul, - const LVector4f &far_ur, - const LVector4f &far_ll, - const LVector4f &far_lr) { - - // We have 8*4 = 32 equations, which require 16 coefficients each - Eigen::MatrixXf equation_system(32, 16); - Eigen::VectorXf equation_results(32); - equation_system.fill(0); - - // Generate the equations - size_t offset = 0; - generate_equations(equation_system, equation_results, near_ul, LVector4f(-1, 1, 0, 1), offset++); - generate_equations(equation_system, equation_results, near_ur, LVector4f( 1, 1, 0, 1), offset++); - generate_equations(equation_system, equation_results, near_ll, LVector4f(-1, -1, 0, 1), offset++); - generate_equations(equation_system, equation_results, near_lr, LVector4f( 1, -1, 0, 1), offset++); - - generate_equations(equation_system, equation_results, far_ul, LVector4f(-1, 1, 1, 1), offset++); - generate_equations(equation_system, equation_results, far_ur, LVector4f( 1, 1, 1, 1), offset++); - generate_equations(equation_system, equation_results, far_ll, LVector4f(-1, -1, 1, 1), offset++); - generate_equations(equation_system, equation_results, far_lr, LVector4f( 1, -1, 1, 1), offset++); - - // Solve the equation system - Eigen::VectorXf solved_system = equation_system.colPivHouseholderQr().solve(equation_results); - - // Construct result matrix and return it. We also need to transpose the matrix. - LMatrix4f result( - solved_system(0), solved_system(4), solved_system(8), solved_system(12), - solved_system(1), solved_system(5), solved_system(9), solved_system(13), - solved_system(2), solved_system(6), solved_system(10), solved_system(14), - solved_system(3), solved_system(7), solved_system(11), solved_system(15) - ); - return result; -} - - -#endif // RP_REQ_PSSM_HELPER +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +// Only include the pssm helper if actually required +#ifdef RP_REQ_PSSM_HELPER + +#include "pssm_helper.h" + +#include + +/** + * @brief Internal method to generate a set of equations + * @details This generates a set of equations into a given equation system. + * Based on a given start point and end point, it inserts an equation to the + * system which will, when solved, solve the following equation: + * vec * = expected + * + * @param eq_system The equation system + * @param eq_results The target result vector of the equation system + * @param vec The actual vector to be transformed + * @param expected The expected output of the transformation + * @param offset The index of the equation + */ +void generate_equations(Eigen::MatrixXf &eq_system, Eigen::VectorXf &eq_results, const LVector4f &vec, LVector4f expected, size_t offset) { + size_t write_offset = offset * 4; + for (size_t row = 0; row < 4; ++row) { + float expected_coeff = expected.get_cell(row); + size_t col_offset = row * 4; + for (size_t col = 0; col < 4; ++col) { + eq_system(write_offset, col_offset + col) = vec.get_cell(col); + } + eq_results(write_offset++) = expected_coeff; + } +} +/** + * @brief Finds a projection mat arround the given set of points. + * @details This methods finds a projection matrix which projects the given set + * of frustum points to a unit cube, which can be used as a camera matrix. + * The eight points should determine the frustum uniquely. + * + * @param near_ul The Upper-Left point of the frustum on the near plane + * @param near_ur The Upper-Right point of the frustum on the near plane + * @param near_ll The Lower-Left point of the frustum on the near plane + * @param near_lr The Lower-Right point of the frustum on the near plane + * @param far_ul The Upper-Left point of the frustum on the far plane + * @param far_ur The Upper-Right point of the frustum on the far plane + * @param far_ll The Lower-Left point of the frustum on the far plane + * @param far_lr The Lower-Right point of the frustum on the far plane + * + */ +LMatrix4f PSSMHelper::find_projection_mat( + const LVector4f &near_ul, + const LVector4f &near_ur, + const LVector4f &near_ll, + const LVector4f &near_lr, + + const LVector4f &far_ul, + const LVector4f &far_ur, + const LVector4f &far_ll, + const LVector4f &far_lr) { + + // We have 8*4 = 32 equations, which require 16 coefficients each + Eigen::MatrixXf equation_system(32, 16); + Eigen::VectorXf equation_results(32); + equation_system.fill(0); + + // Generate the equations + size_t offset = 0; + generate_equations(equation_system, equation_results, near_ul, LVector4f(-1, 1, 0, 1), offset++); + generate_equations(equation_system, equation_results, near_ur, LVector4f( 1, 1, 0, 1), offset++); + generate_equations(equation_system, equation_results, near_ll, LVector4f(-1, -1, 0, 1), offset++); + generate_equations(equation_system, equation_results, near_lr, LVector4f( 1, -1, 0, 1), offset++); + + generate_equations(equation_system, equation_results, far_ul, LVector4f(-1, 1, 1, 1), offset++); + generate_equations(equation_system, equation_results, far_ur, LVector4f( 1, 1, 1, 1), offset++); + generate_equations(equation_system, equation_results, far_ll, LVector4f(-1, -1, 1, 1), offset++); + generate_equations(equation_system, equation_results, far_lr, LVector4f( 1, -1, 1, 1), offset++); + + // Solve the equation system + Eigen::VectorXf solved_system = equation_system.colPivHouseholderQr().solve(equation_results); + + // Construct result matrix and return it. We also need to transpose the matrix. + LMatrix4f result( + solved_system(0), solved_system(4), solved_system(8), solved_system(12), + solved_system(1), solved_system(5), solved_system(9), solved_system(13), + solved_system(2), solved_system(6), solved_system(10), solved_system(14), + solved_system(3), solved_system(7), solved_system(11), solved_system(15) + ); + return result; +} + + +#endif // RP_REQ_PSSM_HELPER diff --git a/RenderPipelineFile/rpcore/native/source/pssm_helper.h b/RenderPipelineFile/rpcore/native/source/pssm_helper.h index a2bb3055..331856ca 100644 --- a/RenderPipelineFile/rpcore/native/source/pssm_helper.h +++ b/RenderPipelineFile/rpcore/native/source/pssm_helper.h @@ -1,57 +1,57 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#ifndef RP_PSSM_HELPER_H -#define RP_PSSM_HELPER_H - -// Only include the pssm helper if actually required -#ifdef RP_REQ_PSSM_HELPER - -#include "pandabase.h" -#include "luse.h" - -/** - * @brief Class to generate projection matrices. - */ -class PSSMHelper { - - PUBLISHED: - static LMatrix4f find_projection_mat( - const LVector4f &near_ul, - const LVector4f &near_ur, - const LVector4f &near_ll, - const LVector4f &near_lr, - - const LVector4f &far_ul, - const LVector4f &far_ur, - const LVector4f &far_ll, - const LVector4f &far_lr); - -}; - -#endif // RP_REQ_PSSM_HELPER - -#endif // RP_PSSM_HELPER_H +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef RP_PSSM_HELPER_H +#define RP_PSSM_HELPER_H + +// Only include the pssm helper if actually required +#ifdef RP_REQ_PSSM_HELPER + +#include "pandabase.h" +#include "luse.h" + +/** + * @brief Class to generate projection matrices. + */ +class PSSMHelper { + + PUBLISHED: + static LMatrix4f find_projection_mat( + const LVector4f &near_ul, + const LVector4f &near_ur, + const LVector4f &near_ll, + const LVector4f &near_lr, + + const LVector4f &far_ul, + const LVector4f &far_ur, + const LVector4f &far_ll, + const LVector4f &far_lr); + +}; + +#endif // RP_REQ_PSSM_HELPER + +#endif // RP_PSSM_HELPER_H diff --git a/RenderPipelineFile/rpcore/native/source/rp_light.I b/RenderPipelineFile/rpcore/native/source/rp_light.I index 756af2c5..724a0752 100644 --- a/RenderPipelineFile/rpcore/native/source/rp_light.I +++ b/RenderPipelineFile/rpcore/native/source/rp_light.I @@ -1,406 +1,406 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -/** - * @brief Returns the amount of shadow sources - * @details This returns the amount of shadow sources attached to this light. - * In case the light has no shadows enabled, or the light was not attached - * yet, this returns 0. - * - * @return Amount of shadow sources - */ -inline int RPLight::get_num_shadow_sources() const { - return _shadow_sources.size(); -} - -/** - * @brief Returns the n-th shadow source - * @details This returns the n-th attached shadow source. This ranges from - * 0 .. RPLight::get_num_shadow_sources(). If an invalid index is passed, - * an assertion is thrown. - * - * @param index Index of the source - * @return Handle to the shadow source - */ -inline ShadowSource* RPLight::get_shadow_source(size_t index) const { - nassertr(index < _shadow_sources.size(), NULL); // Invalid shadow source index - return _shadow_sources[index]; -} - -/** - * @brief Clears all shadow source - * @details This removes and destructs all shadow sources attached to this light. - * This usually gets called when the light gets detached or destructed. - * All shadows sources are freed, and then removed from the shadow source list. - */ -inline void RPLight::clear_shadow_sources() { - for (size_t i = 0; i < _shadow_sources.size(); ++i) { - delete _shadow_sources[i]; - } - _shadow_sources.clear(); -} - -/** - * @brief Sets whether the light needs an update - * @details This controls whether the light needs to get an update. This is the - * case when a property of the light changed, e.g. position or color. It does - * not affect the shadows (For that use RPLight::invalidate_shadows()). - * When this flag is set to true, the light will get resubmitted to the GPU - * in the next update cycle. - * - * You should usually never set the flag to false manually. The - * InternalLightManager will do this when the data got sucessfully updated. - * - * @param flag Update-Flag - */ -inline void RPLight::set_needs_update(bool flag) { - _needs_update = flag; -} - -/** - * @brief Returns whether the light needs an update - * @details This returns whether the light needs an update. This might be the - * case when a property of the light was changed, e.g. position or color. - * It does not affect the shadows, you have to query the update flag of each - * individual source for that. - * The return value is the value previously set with RPLight::set_needs_update. - * - * @return Update-flag - */ -inline bool RPLight::get_needs_update() const { - return _needs_update; -} - -/** - * @brief Returns whether the light has a slot - * @details This returns wheter the light currently is attached, and thus has - * a slot in the InternalLightManagers light list. When the light is attached, - * this returns true, otherwise it will return false. - * - * @return true if the light has a slot, false otherwise - */ -inline bool RPLight::has_slot() const { - return _slot >= 0; -} - -/** - * @brief Returns the slot of the light - * @details This returns the slot of the light. This is the space on the GPU - * where the light is stored. If the light is not attached yet, this will - * return -1, otherwise the index of the light. - * - * @return Light-Slot - */ -inline int RPLight::get_slot() const { - return _slot; -} - -/** - * @brief Removes the light slot - * @details This is an internal method to remove the slot of the light. It gets - * called by the InternalLightManager when a light gets detached. It internally - * sets the slot to -1 to indicate the light is no longer attached. - */ -inline void RPLight::remove_slot() { - _slot = -1; -} - -/** - * @brief Assigns a slot to the light - * @details This assigns a slot to the light, marking it as attached. The slot - * relates to the index in the GPU's storage of lights. This is an internal - * method called by the InternalLightManager when the light got attached. - * - * @param slot Slot of the light - */ -inline void RPLight::assign_slot(int slot) { - _slot = slot; -} - -/** - * @brief Invalidates the shadows - * @details This invalidates all shadows of the light, causing them to get - * regenerated. This might be the case when the lights position or similar - * changed. This will cause all shadow sources to be updated, emitting a - * shadow update. Be careful when calling this method if you don't want all - * sources to get updated. If you only have to invalidate a single shadow source, - * use get_shadow_source(n)->set_needs_update(true). - */ -inline void RPLight::invalidate_shadows() { - for (size_t i = 0; i < _shadow_sources.size(); ++i) { - _shadow_sources[i]->set_needs_update(true); - } -} - -/** - * @brief Sets the position of the light - * @details This sets the position of the light in world space. It will cause - * the light to get invalidated, and resubmitted to the GPU. - * - * @param pos Position in world space - */ -inline void RPLight::set_pos(const LVecBase3f &pos) { - set_pos(pos.get_x(), pos.get_y(), pos.get_z()); -} - -/** - * @brief Sets the position of the light - * @details @copydetails RPLight::set_pos(const LVecBase3f &pos) - * - * @param x X-component of the position - * @param y Y-component of the position - * @param z Z-component of the position - */ -inline void RPLight::set_pos(float x, float y, float z) { - _position.set(x, y, z); - set_needs_update(true); - invalidate_shadows(); -} - -/** - * @brief Returns the position of the light - * @details This returns the position of the light previously set with - * RPLight::set_pos(). The returned position is in world space. - * @return Light-position - */ -inline const LVecBase3f& RPLight::get_pos() const { - return _position; -} - -/** - * @brief Sets the lights color - * @details This sets the lights color. The color should not include the brightness - * of the light, you should control that with the energy. The color specifies - * the lights "tint" and will get multiplied with its specular and diffuse - * contribution. - * - * The color will be normalized by dividing by the colors luminance. Setting - * higher values than 1.0 will have no effect. - * - * @param color Light color - */ -inline void RPLight::set_color(const LVecBase3f &color) { - _color = color; - _color /= 0.2126 * color.get_x() + 0.7152 * color.get_y() + 0.0722 * color.get_z(); - set_needs_update(true); -} - -/** - * @brief Sets the lights color - * @details @copydetails RPLight::set_color(const LVecBase3f &color) - * - * @param r Red-component of the color - * @param g Green-component of the color - * @param b Blue-component of the color - */ -inline void RPLight::set_color(float r, float g, float b) { - set_color(LVecBase3f(r, g, b)); -} - -/** - * @brief Returns the lights color - * @details This returns the light color, previously set with RPLight::set_color. - * This does not include the energy of the light. It might differ from what - * was set with set_color, because the color is normalized by dividing it - * by its luminance. - * @return Light-color - */ -inline const LVecBase3f& RPLight::get_color() const { - return _color; -} - -/** - * @brief Sets the energy of the light - * @details This sets the energy of the light, which can be seen as the brightness - * of the light. It will get multiplied with the normalized color. - * - * @param energy energy of the light - */ -inline void RPLight::set_energy(float energy) { - _energy = energy; - set_needs_update(true); -} - -/** - * @brief Returns the energy of the light - * @details This returns the energy of the light, previously set with - * RPLight::set_energy. - * - * @return energy of the light - */ -inline float RPLight::get_energy() const { - return _energy; -} - -/** - * @brief Returns the type of the light - * @details This returns the internal type of the light, which was specified - * in the lights constructor. This can be used to distinguish between light - * types. - * @return Type of the light - */ -inline RPLight::LightType RPLight::get_light_type() const { - return _light_type; -} - -/** - * @brief Controls whether the light casts shadows - * @details This sets whether the light casts shadows. You can not change this - * while the light is attached. When flag is set to true, the light will be - * setup to cast shadows, spawning shadow sources based on the lights type. - * If the flag is set to false, the light will be inddicated to cast no shadows. - * - * @param flag Whether the light casts shadows - */ -inline void RPLight::set_casts_shadows(bool flag) { - if (has_slot()) { - cerr << "Light is already attached, can not call set_casts_shadows!" << endl; - return; - } - _casts_shadows = flag; -} - -/** - * @brief Returns whether the light casts shadows - * @details This returns whether the light casts shadows, the returned value - * is the one previously set with RPLight::set_casts_shadows. - * - * @return true if the light casts shadows, false otherwise - */ -inline bool RPLight::get_casts_shadows() const { - return _casts_shadows; -} - -/** - * @brief Sets the lights shadow map resolution - * @details This sets the lights shadow map resolution. This has no effect - * when the light is not told to cast shadows (Use RPLight::set_casts_shadows). - * - * When calling this on a light with multiple shadow sources (e.g. PointLight), - * this controls the resolution of each source. If the light has 6 shadow sources, - * and you use a resolution of 512x512, the lights shadow map will occur a - * space of 6 * 512x512 maps in the shadow atlas. - * - * @param resolution Resolution of the shadow map in pixels - */ -inline void RPLight::set_shadow_map_resolution(size_t resolution) { - nassertv(resolution >= 32 && resolution <= 16384); - _source_resolution = resolution; - invalidate_shadows(); -} - -/** - * @brief Returns the shadow map resolution - * @details This returns the shadow map resolution of each source of the light. - * If the light is not setup to cast shadows, this value is meaningless. - * The returned value is the one previously set with RPLight::set_shadow_map_resolution. - * - * @return Shadow map resolution in pixels - */ -inline size_t RPLight::get_shadow_map_resolution() const { - return _source_resolution; -} - -/** - * @brief Sets the ies profile - * @details This sets the ies profile of the light. The parameter should be a - * handle previously returned by RenderPipeline.load_ies_profile. Using a - * value of -1 indicates no ies profile. - * - * Notice that for ies profiles which cover a whole range, you should use - * PointLights, whereas for ies profiles which only cover the lower hemisphere - * you should use SpotLights for the best performance. - * - * @param profile IES Profile handle - */ -inline void RPLight::set_ies_profile(int profile) { - _ies_profile = profile; - set_needs_update(true); -} - -/** - * @brief Returns the lights ies profile - * @details This returns the ies profile of a light, previously set with - * RPLight::set_ies_profile. In case no ies profile was set, returns -1. - * - * @return IES Profile handle - */ -inline int RPLight::get_ies_profile() const { - return _ies_profile; -} - -/** - * @brief Returns whether the light has an ies profile assigned - * @details This returns whether the light has an ies profile assigned, - * previously done with RPLight::set_ies_profile. - * - * @return true if the light has an ies profile assigned, false otherwise - */ -inline bool RPLight::has_ies_profile() const { - return _ies_profile >= 0; -} - -/** - * @brief Clears the ies profile - * @details This clears the ies profile of the light, telling it to no longer - * use an ies profile, and instead use the default attenuation. - */ -inline void RPLight::clear_ies_profile() { - set_ies_profile(-1); -} - -/** - * @brief Sets the near plane of the light - * @details This sets the near plane of all shadow sources of the light. It has - * no effects if the light does not cast shadows. This prevents artifacts from - * objects near to the light. It behaves like Lens::set_near_plane. - * - * It can also help increasing shadow map precision, low near planes will - * cause the precision to suffer. Try setting the near plane as big as possible. - * - * If a negative or zero near plane is passed, an assertion is thrown. - * - * @param near_plane Near-plane - */ -inline void RPLight::set_near_plane(float near_plane) { - nassertv(near_plane > 0.00001); - _near_plane = near_plane; - invalidate_shadows(); -} - -/** - * @brief Returns the near plane of the light - * @details This returns the lights near plane, previously set with - * RPLight::set_near_plane. If the light does not cast shadows, this value - * is meaningless. - * - * @return Near-plane - */ -inline float RPLight::get_near_plane() const { - return _near_plane; -} - +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +/** + * @brief Returns the amount of shadow sources + * @details This returns the amount of shadow sources attached to this light. + * In case the light has no shadows enabled, or the light was not attached + * yet, this returns 0. + * + * @return Amount of shadow sources + */ +inline int RPLight::get_num_shadow_sources() const { + return _shadow_sources.size(); +} + +/** + * @brief Returns the n-th shadow source + * @details This returns the n-th attached shadow source. This ranges from + * 0 .. RPLight::get_num_shadow_sources(). If an invalid index is passed, + * an assertion is thrown. + * + * @param index Index of the source + * @return Handle to the shadow source + */ +inline ShadowSource* RPLight::get_shadow_source(size_t index) const { + nassertr(index < _shadow_sources.size(), NULL); // Invalid shadow source index + return _shadow_sources[index]; +} + +/** + * @brief Clears all shadow source + * @details This removes and destructs all shadow sources attached to this light. + * This usually gets called when the light gets detached or destructed. + * All shadows sources are freed, and then removed from the shadow source list. + */ +inline void RPLight::clear_shadow_sources() { + for (size_t i = 0; i < _shadow_sources.size(); ++i) { + delete _shadow_sources[i]; + } + _shadow_sources.clear(); +} + +/** + * @brief Sets whether the light needs an update + * @details This controls whether the light needs to get an update. This is the + * case when a property of the light changed, e.g. position or color. It does + * not affect the shadows (For that use RPLight::invalidate_shadows()). + * When this flag is set to true, the light will get resubmitted to the GPU + * in the next update cycle. + * + * You should usually never set the flag to false manually. The + * InternalLightManager will do this when the data got sucessfully updated. + * + * @param flag Update-Flag + */ +inline void RPLight::set_needs_update(bool flag) { + _needs_update = flag; +} + +/** + * @brief Returns whether the light needs an update + * @details This returns whether the light needs an update. This might be the + * case when a property of the light was changed, e.g. position or color. + * It does not affect the shadows, you have to query the update flag of each + * individual source for that. + * The return value is the value previously set with RPLight::set_needs_update. + * + * @return Update-flag + */ +inline bool RPLight::get_needs_update() const { + return _needs_update; +} + +/** + * @brief Returns whether the light has a slot + * @details This returns wheter the light currently is attached, and thus has + * a slot in the InternalLightManagers light list. When the light is attached, + * this returns true, otherwise it will return false. + * + * @return true if the light has a slot, false otherwise + */ +inline bool RPLight::has_slot() const { + return _slot >= 0; +} + +/** + * @brief Returns the slot of the light + * @details This returns the slot of the light. This is the space on the GPU + * where the light is stored. If the light is not attached yet, this will + * return -1, otherwise the index of the light. + * + * @return Light-Slot + */ +inline int RPLight::get_slot() const { + return _slot; +} + +/** + * @brief Removes the light slot + * @details This is an internal method to remove the slot of the light. It gets + * called by the InternalLightManager when a light gets detached. It internally + * sets the slot to -1 to indicate the light is no longer attached. + */ +inline void RPLight::remove_slot() { + _slot = -1; +} + +/** + * @brief Assigns a slot to the light + * @details This assigns a slot to the light, marking it as attached. The slot + * relates to the index in the GPU's storage of lights. This is an internal + * method called by the InternalLightManager when the light got attached. + * + * @param slot Slot of the light + */ +inline void RPLight::assign_slot(int slot) { + _slot = slot; +} + +/** + * @brief Invalidates the shadows + * @details This invalidates all shadows of the light, causing them to get + * regenerated. This might be the case when the lights position or similar + * changed. This will cause all shadow sources to be updated, emitting a + * shadow update. Be careful when calling this method if you don't want all + * sources to get updated. If you only have to invalidate a single shadow source, + * use get_shadow_source(n)->set_needs_update(true). + */ +inline void RPLight::invalidate_shadows() { + for (size_t i = 0; i < _shadow_sources.size(); ++i) { + _shadow_sources[i]->set_needs_update(true); + } +} + +/** + * @brief Sets the position of the light + * @details This sets the position of the light in world space. It will cause + * the light to get invalidated, and resubmitted to the GPU. + * + * @param pos Position in world space + */ +inline void RPLight::set_pos(const LVecBase3f &pos) { + set_pos(pos.get_x(), pos.get_y(), pos.get_z()); +} + +/** + * @brief Sets the position of the light + * @details @copydetails RPLight::set_pos(const LVecBase3f &pos) + * + * @param x X-component of the position + * @param y Y-component of the position + * @param z Z-component of the position + */ +inline void RPLight::set_pos(float x, float y, float z) { + _position.set(x, y, z); + set_needs_update(true); + invalidate_shadows(); +} + +/** + * @brief Returns the position of the light + * @details This returns the position of the light previously set with + * RPLight::set_pos(). The returned position is in world space. + * @return Light-position + */ +inline const LVecBase3f& RPLight::get_pos() const { + return _position; +} + +/** + * @brief Sets the lights color + * @details This sets the lights color. The color should not include the brightness + * of the light, you should control that with the energy. The color specifies + * the lights "tint" and will get multiplied with its specular and diffuse + * contribution. + * + * The color will be normalized by dividing by the colors luminance. Setting + * higher values than 1.0 will have no effect. + * + * @param color Light color + */ +inline void RPLight::set_color(const LVecBase3f &color) { + _color = color; + _color /= 0.2126 * color.get_x() + 0.7152 * color.get_y() + 0.0722 * color.get_z(); + set_needs_update(true); +} + +/** + * @brief Sets the lights color + * @details @copydetails RPLight::set_color(const LVecBase3f &color) + * + * @param r Red-component of the color + * @param g Green-component of the color + * @param b Blue-component of the color + */ +inline void RPLight::set_color(float r, float g, float b) { + set_color(LVecBase3f(r, g, b)); +} + +/** + * @brief Returns the lights color + * @details This returns the light color, previously set with RPLight::set_color. + * This does not include the energy of the light. It might differ from what + * was set with set_color, because the color is normalized by dividing it + * by its luminance. + * @return Light-color + */ +inline const LVecBase3f& RPLight::get_color() const { + return _color; +} + +/** + * @brief Sets the energy of the light + * @details This sets the energy of the light, which can be seen as the brightness + * of the light. It will get multiplied with the normalized color. + * + * @param energy energy of the light + */ +inline void RPLight::set_energy(float energy) { + _energy = energy; + set_needs_update(true); +} + +/** + * @brief Returns the energy of the light + * @details This returns the energy of the light, previously set with + * RPLight::set_energy. + * + * @return energy of the light + */ +inline float RPLight::get_energy() const { + return _energy; +} + +/** + * @brief Returns the type of the light + * @details This returns the internal type of the light, which was specified + * in the lights constructor. This can be used to distinguish between light + * types. + * @return Type of the light + */ +inline RPLight::LightType RPLight::get_light_type() const { + return _light_type; +} + +/** + * @brief Controls whether the light casts shadows + * @details This sets whether the light casts shadows. You can not change this + * while the light is attached. When flag is set to true, the light will be + * setup to cast shadows, spawning shadow sources based on the lights type. + * If the flag is set to false, the light will be inddicated to cast no shadows. + * + * @param flag Whether the light casts shadows + */ +inline void RPLight::set_casts_shadows(bool flag) { + if (has_slot()) { + cerr << "Light is already attached, can not call set_casts_shadows!" << endl; + return; + } + _casts_shadows = flag; +} + +/** + * @brief Returns whether the light casts shadows + * @details This returns whether the light casts shadows, the returned value + * is the one previously set with RPLight::set_casts_shadows. + * + * @return true if the light casts shadows, false otherwise + */ +inline bool RPLight::get_casts_shadows() const { + return _casts_shadows; +} + +/** + * @brief Sets the lights shadow map resolution + * @details This sets the lights shadow map resolution. This has no effect + * when the light is not told to cast shadows (Use RPLight::set_casts_shadows). + * + * When calling this on a light with multiple shadow sources (e.g. PointLight), + * this controls the resolution of each source. If the light has 6 shadow sources, + * and you use a resolution of 512x512, the lights shadow map will occur a + * space of 6 * 512x512 maps in the shadow atlas. + * + * @param resolution Resolution of the shadow map in pixels + */ +inline void RPLight::set_shadow_map_resolution(size_t resolution) { + nassertv(resolution >= 32 && resolution <= 16384); + _source_resolution = resolution; + invalidate_shadows(); +} + +/** + * @brief Returns the shadow map resolution + * @details This returns the shadow map resolution of each source of the light. + * If the light is not setup to cast shadows, this value is meaningless. + * The returned value is the one previously set with RPLight::set_shadow_map_resolution. + * + * @return Shadow map resolution in pixels + */ +inline size_t RPLight::get_shadow_map_resolution() const { + return _source_resolution; +} + +/** + * @brief Sets the ies profile + * @details This sets the ies profile of the light. The parameter should be a + * handle previously returned by RenderPipeline.load_ies_profile. Using a + * value of -1 indicates no ies profile. + * + * Notice that for ies profiles which cover a whole range, you should use + * PointLights, whereas for ies profiles which only cover the lower hemisphere + * you should use SpotLights for the best performance. + * + * @param profile IES Profile handle + */ +inline void RPLight::set_ies_profile(int profile) { + _ies_profile = profile; + set_needs_update(true); +} + +/** + * @brief Returns the lights ies profile + * @details This returns the ies profile of a light, previously set with + * RPLight::set_ies_profile. In case no ies profile was set, returns -1. + * + * @return IES Profile handle + */ +inline int RPLight::get_ies_profile() const { + return _ies_profile; +} + +/** + * @brief Returns whether the light has an ies profile assigned + * @details This returns whether the light has an ies profile assigned, + * previously done with RPLight::set_ies_profile. + * + * @return true if the light has an ies profile assigned, false otherwise + */ +inline bool RPLight::has_ies_profile() const { + return _ies_profile >= 0; +} + +/** + * @brief Clears the ies profile + * @details This clears the ies profile of the light, telling it to no longer + * use an ies profile, and instead use the default attenuation. + */ +inline void RPLight::clear_ies_profile() { + set_ies_profile(-1); +} + +/** + * @brief Sets the near plane of the light + * @details This sets the near plane of all shadow sources of the light. It has + * no effects if the light does not cast shadows. This prevents artifacts from + * objects near to the light. It behaves like Lens::set_near_plane. + * + * It can also help increasing shadow map precision, low near planes will + * cause the precision to suffer. Try setting the near plane as big as possible. + * + * If a negative or zero near plane is passed, an assertion is thrown. + * + * @param near_plane Near-plane + */ +inline void RPLight::set_near_plane(float near_plane) { + nassertv(near_plane > 0.00001); + _near_plane = near_plane; + invalidate_shadows(); +} + +/** + * @brief Returns the near plane of the light + * @details This returns the lights near plane, previously set with + * RPLight::set_near_plane. If the light does not cast shadows, this value + * is meaningless. + * + * @return Near-plane + */ +inline float RPLight::get_near_plane() const { + return _near_plane; +} + diff --git a/RenderPipelineFile/rpcore/native/source/rp_light.cpp b/RenderPipelineFile/rpcore/native/source/rp_light.cpp index 24a84b4e..d5b84e6f 100644 --- a/RenderPipelineFile/rpcore/native/source/rp_light.cpp +++ b/RenderPipelineFile/rpcore/native/source/rp_light.cpp @@ -1,137 +1,137 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -#include "rp_light.h" - - -/** - * @brief Constructs a new light with the given type - * @details This constructs a new base light with the given light type. - * Sub-Classes should call this to initialize all properties. - * - * @param light_type Type of the light - */ -RPLight::RPLight(LightType light_type) { - _light_type = light_type; - _needs_update = false; - _casts_shadows = false; - _slot = -1; - _position.fill(0); - _color.fill(1); - _ies_profile = -1; - _source_resolution = 512; - _near_plane = 0.5; - _energy = 20.0; -} - -/** - * @brief Writes the light to a GPUCommand - * @details This writes all of the lights data to the given GPUCommand handle. - * Subclasses should first call this method, and then append their own - * data. This makes sure that for unpacking a light, no information about - * the type of the light is required. - * - * @param cmd The GPUCommand to write to - */ -void RPLight::write_to_command(GPUCommand &cmd) { - cmd.push_int(_light_type); - cmd.push_int(_ies_profile); - - if (_casts_shadows) { - // If we casts shadows, write the index of the first source, we expect - // them to be consecutive - nassertv(_shadow_sources.size() >= 0); - nassertv(_shadow_sources[0]->has_slot()); - cmd.push_int(_shadow_sources[0]->get_slot()); - } else { - // If we cast no shadows, just push a negative number - cmd.push_int(-1); - } - - cmd.push_vec3(_position); - - // Get the lights color by multiplying color with energy. Divide by - // 100, since 16bit floating point buffers only go up to 65000.0, which - // prevents very bright lights - cmd.push_vec3(_color * _energy / 100.0); -} - -/** - * @brief Light destructor - * @details This destructs the light, cleaning up all resourced used. The light - * should be detached at this point, because while the Light is attached, - * the InternalLightManager holds a reference to prevent it from being - * destructed. - */ -RPLight::~RPLight() { - nassertv(!has_slot()); // Light still attached - should never happen - clear_shadow_sources(); -} - -/** - * @brief Sets the lights color from a given color temperature - * @details This sets the lights color, given a temperature. This is more - * physically based than setting a user defined color. The color will be - * computed from the given temperature. - * - * @param temperature Light temperature - */ -void RPLight::set_color_from_temperature(float temperature) { - - // Thanks to rdb for this conversion script - float mm = 1000.0 / temperature; - float mm2 = mm * mm; - float mm3 = mm2 * mm; - float x, y; - - if (temperature < 4000) { - x = -0.2661239 * mm3 - 0.2343580 * mm2 + 0.8776956 * mm + 0.179910; - } else { - x = -3.0258469 * mm3 + 2.1070379 * mm2 + 0.2226347 * mm + 0.240390; - } - - float x2 = x * x; - float x3 = x2 * x; - if (temperature < 2222) { - y = -1.1063814 * x3 - 1.34811020 * x2 + 2.18555832 * x - 0.20219683; - } else if (temperature < 4000) { - y = -0.9549476 * x3 - 1.37418593 * x2 + 2.09137015 * x - 0.16748867; - } else { - y = 3.0817580 * x3 - 5.87338670 * x2 + 3.75112997 * x - 0.37001483; - } - - // xyY to XYZ, assuming Y=1. - LVecBase3 xyz(x / y, 1, (1 - x - y) / y); - - // Convert XYZ to linearized sRGB. - const static LMatrix3 xyz_to_rgb( - 3.2406, -0.9689, 0.0557, - -1.5372, 1.8758, -0.2050, - -0.4986, 0.0415, 1.0570); - - set_color(xyz_to_rgb.xform(xyz)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +#include "rp_light.h" + + +/** + * @brief Constructs a new light with the given type + * @details This constructs a new base light with the given light type. + * Sub-Classes should call this to initialize all properties. + * + * @param light_type Type of the light + */ +RPLight::RPLight(LightType light_type) { + _light_type = light_type; + _needs_update = false; + _casts_shadows = false; + _slot = -1; + _position.fill(0); + _color.fill(1); + _ies_profile = -1; + _source_resolution = 512; + _near_plane = 0.5; + _energy = 20.0; +} + +/** + * @brief Writes the light to a GPUCommand + * @details This writes all of the lights data to the given GPUCommand handle. + * Subclasses should first call this method, and then append their own + * data. This makes sure that for unpacking a light, no information about + * the type of the light is required. + * + * @param cmd The GPUCommand to write to + */ +void RPLight::write_to_command(GPUCommand &cmd) { + cmd.push_int(_light_type); + cmd.push_int(_ies_profile); + + if (_casts_shadows) { + // If we casts shadows, write the index of the first source, we expect + // them to be consecutive + nassertv(_shadow_sources.size() >= 0); + nassertv(_shadow_sources[0]->has_slot()); + cmd.push_int(_shadow_sources[0]->get_slot()); + } else { + // If we cast no shadows, just push a negative number + cmd.push_int(-1); + } + + cmd.push_vec3(_position); + + // Get the lights color by multiplying color with energy. Divide by + // 100, since 16bit floating point buffers only go up to 65000.0, which + // prevents very bright lights + cmd.push_vec3(_color * _energy / 100.0); +} + +/** + * @brief Light destructor + * @details This destructs the light, cleaning up all resourced used. The light + * should be detached at this point, because while the Light is attached, + * the InternalLightManager holds a reference to prevent it from being + * destructed. + */ +RPLight::~RPLight() { + nassertv(!has_slot()); // Light still attached - should never happen + clear_shadow_sources(); +} + +/** + * @brief Sets the lights color from a given color temperature + * @details This sets the lights color, given a temperature. This is more + * physically based than setting a user defined color. The color will be + * computed from the given temperature. + * + * @param temperature Light temperature + */ +void RPLight::set_color_from_temperature(float temperature) { + + // Thanks to rdb for this conversion script + float mm = 1000.0 / temperature; + float mm2 = mm * mm; + float mm3 = mm2 * mm; + float x, y; + + if (temperature < 4000) { + x = -0.2661239 * mm3 - 0.2343580 * mm2 + 0.8776956 * mm + 0.179910; + } else { + x = -3.0258469 * mm3 + 2.1070379 * mm2 + 0.2226347 * mm + 0.240390; + } + + float x2 = x * x; + float x3 = x2 * x; + if (temperature < 2222) { + y = -1.1063814 * x3 - 1.34811020 * x2 + 2.18555832 * x - 0.20219683; + } else if (temperature < 4000) { + y = -0.9549476 * x3 - 1.37418593 * x2 + 2.09137015 * x - 0.16748867; + } else { + y = 3.0817580 * x3 - 5.87338670 * x2 + 3.75112997 * x - 0.37001483; + } + + // xyY to XYZ, assuming Y=1. + LVecBase3 xyz(x / y, 1, (1 - x - y) / y); + + // Convert XYZ to linearized sRGB. + const static LMatrix3 xyz_to_rgb( + 3.2406, -0.9689, 0.0557, + -1.5372, 1.8758, -0.2050, + -0.4986, 0.0415, 1.0570); + + set_color(xyz_to_rgb.xform(xyz)); +} diff --git a/RenderPipelineFile/rpcore/native/source/rp_light.h b/RenderPipelineFile/rpcore/native/source/rp_light.h index 2fbc07ef..f7202069 100644 --- a/RenderPipelineFile/rpcore/native/source/rp_light.h +++ b/RenderPipelineFile/rpcore/native/source/rp_light.h @@ -1,135 +1,135 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#ifndef RP_LIGHT_H -#define RP_LIGHT_H - -#include "referenceCount.h" -#include "luse.h" -#include "gpu_command.h" -#include "shadow_source.h" - -/** - * @brief Base class for Lights - * @details This is the base class for all lights in the render pipeline. It - * stores common properties, and provides methods to modify these. - * It also defines some interface functions which subclasses have to implement. - */ -class RPLight : public ReferenceCount { - - PUBLISHED: - - /** - * Different types of light. - */ - enum LightType { - LT_empty = 0, - LT_point_light = 1, - LT_spot_light = 2, - }; - - public: - RPLight(LightType light_type); - virtual ~RPLight(); - - virtual void init_shadow_sources() = 0; - virtual void update_shadow_sources() = 0; - virtual void write_to_command(GPUCommand &cmd); - - inline int get_num_shadow_sources() const; - inline ShadowSource* get_shadow_source(size_t index) const; - inline void clear_shadow_sources(); - - inline void set_needs_update(bool flag); - inline bool get_needs_update() const; - - inline bool has_slot() const; - inline int get_slot() const; - inline void remove_slot(); - inline void assign_slot(int slot); - - - PUBLISHED: - - inline void invalidate_shadows(); - - inline void set_pos(const LVecBase3f &pos); - inline void set_pos(float x, float y, float z); - inline const LVecBase3f& get_pos() const; - MAKE_PROPERTY(pos, get_pos, set_pos); - - inline void set_color(const LVecBase3f &color); - inline void set_color(float r, float g, float b); - inline const LVecBase3f& get_color() const; - MAKE_PROPERTY(color, get_color, set_color); - - void set_color_from_temperature(float temperature); - - inline void set_energy(float energy); - inline float get_energy() const; - MAKE_PROPERTY(energy, get_energy, set_energy); - - inline LightType get_light_type() const; - MAKE_PROPERTY(light_type, get_light_type); - - inline void set_casts_shadows(bool flag = true); - inline bool get_casts_shadows() const; - MAKE_PROPERTY(casts_shadows, get_casts_shadows, set_casts_shadows); - - inline void set_shadow_map_resolution(size_t resolution); - inline size_t get_shadow_map_resolution() const; - MAKE_PROPERTY(shadow_map_resolution, get_shadow_map_resolution, set_shadow_map_resolution); - - inline void set_ies_profile(int profile); - inline int get_ies_profile() const; - inline bool has_ies_profile() const; - inline void clear_ies_profile(); - MAKE_PROPERTY2(ies_profile, has_ies_profile, get_ies_profile, - set_ies_profile, clear_ies_profile); - - inline void set_near_plane(float near_plane); - inline float get_near_plane() const; - MAKE_PROPERTY(near_plane, get_near_plane, set_near_plane); - - protected: - - int _slot; - int _ies_profile; - size_t _source_resolution; - bool _needs_update; - bool _casts_shadows; - LVecBase3f _position; - LVecBase3f _color; - float _energy; - LightType _light_type; - float _near_plane; - - vector _shadow_sources; -}; - -#include "rp_light.I" - -#endif // RP_LIGHT_H +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef RP_LIGHT_H +#define RP_LIGHT_H + +#include "referenceCount.h" +#include "luse.h" +#include "gpu_command.h" +#include "shadow_source.h" + +/** + * @brief Base class for Lights + * @details This is the base class for all lights in the render pipeline. It + * stores common properties, and provides methods to modify these. + * It also defines some interface functions which subclasses have to implement. + */ +class RPLight : public ReferenceCount { + + PUBLISHED: + + /** + * Different types of light. + */ + enum LightType { + LT_empty = 0, + LT_point_light = 1, + LT_spot_light = 2, + }; + + public: + RPLight(LightType light_type); + virtual ~RPLight(); + + virtual void init_shadow_sources() = 0; + virtual void update_shadow_sources() = 0; + virtual void write_to_command(GPUCommand &cmd); + + inline int get_num_shadow_sources() const; + inline ShadowSource* get_shadow_source(size_t index) const; + inline void clear_shadow_sources(); + + inline void set_needs_update(bool flag); + inline bool get_needs_update() const; + + inline bool has_slot() const; + inline int get_slot() const; + inline void remove_slot(); + inline void assign_slot(int slot); + + + PUBLISHED: + + inline void invalidate_shadows(); + + inline void set_pos(const LVecBase3f &pos); + inline void set_pos(float x, float y, float z); + inline const LVecBase3f& get_pos() const; + MAKE_PROPERTY(pos, get_pos, set_pos); + + inline void set_color(const LVecBase3f &color); + inline void set_color(float r, float g, float b); + inline const LVecBase3f& get_color() const; + MAKE_PROPERTY(color, get_color, set_color); + + void set_color_from_temperature(float temperature); + + inline void set_energy(float energy); + inline float get_energy() const; + MAKE_PROPERTY(energy, get_energy, set_energy); + + inline LightType get_light_type() const; + MAKE_PROPERTY(light_type, get_light_type); + + inline void set_casts_shadows(bool flag = true); + inline bool get_casts_shadows() const; + MAKE_PROPERTY(casts_shadows, get_casts_shadows, set_casts_shadows); + + inline void set_shadow_map_resolution(size_t resolution); + inline size_t get_shadow_map_resolution() const; + MAKE_PROPERTY(shadow_map_resolution, get_shadow_map_resolution, set_shadow_map_resolution); + + inline void set_ies_profile(int profile); + inline int get_ies_profile() const; + inline bool has_ies_profile() const; + inline void clear_ies_profile(); + MAKE_PROPERTY2(ies_profile, has_ies_profile, get_ies_profile, + set_ies_profile, clear_ies_profile); + + inline void set_near_plane(float near_plane); + inline float get_near_plane() const; + MAKE_PROPERTY(near_plane, get_near_plane, set_near_plane); + + protected: + + int _slot; + int _ies_profile; + size_t _source_resolution; + bool _needs_update; + bool _casts_shadows; + LVecBase3f _position; + LVecBase3f _color; + float _energy; + LightType _light_type; + float _near_plane; + + vector _shadow_sources; +}; + +#include "rp_light.I" + +#endif // RP_LIGHT_H diff --git a/RenderPipelineFile/rpcore/native/source/rp_point_light.I b/RenderPipelineFile/rpcore/native/source/rp_point_light.I index ab85c401..1a149ccb 100644 --- a/RenderPipelineFile/rpcore/native/source/rp_point_light.I +++ b/RenderPipelineFile/rpcore/native/source/rp_point_light.I @@ -1,82 +1,82 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -/** - * @brief Sets the radius of the light - * @details This sets the radius of the light. It controls the lights - * influence. After a distance greater than this radius, the light influence - * is zero. - * - * @param radius Light radius in world space - */ -inline void RPPointLight::set_radius(float radius) { - nassertv(radius > 0); // Invalid light radius - _radius = radius; - set_needs_update(true); - invalidate_shadows(); -} - -/** - * @brief Returns the lights radius - * @details This returns the lights radius previously set with - * RPPointLight::set_radius - * @return Light radius in world space - */ -inline float RPPointLight::get_radius() const { - return _radius; -} - -/** - * @brief Sets the inner radius of the light - * @details This sets the inner radius of the light. Anything greater than - * zero causes the light to get an area light. This has influence on the - * specular highlights of the light aswell as the shadows. - * - * The inner radius controls the size of the lights sphere size in world - * space units. A radius of 0 means the light has no inner radius, and the - * light will be have like an infinite small point light source. - * A radius greater than zero will cause the light to behave like it would be - * an emissive sphere with the given inner radius emitting light. This is - * more physically correct. - * - * @param inner_radius Inner-radius in world space - */ -inline void RPPointLight::set_inner_radius(float inner_radius) { - nassertv(inner_radius >= 0.01); // Invalid inner radius - _inner_radius = inner_radius; - set_needs_update(true); -} - -/** - * @brief Returns the inner radius of the light - * @details This returns the inner radius of the light, previously set with - * RPPointLight::get_inner_radius. - * @return [description] - */ -inline float RPPointLight::get_inner_radius() const { - return _inner_radius; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +/** + * @brief Sets the radius of the light + * @details This sets the radius of the light. It controls the lights + * influence. After a distance greater than this radius, the light influence + * is zero. + * + * @param radius Light radius in world space + */ +inline void RPPointLight::set_radius(float radius) { + nassertv(radius > 0); // Invalid light radius + _radius = radius; + set_needs_update(true); + invalidate_shadows(); +} + +/** + * @brief Returns the lights radius + * @details This returns the lights radius previously set with + * RPPointLight::set_radius + * @return Light radius in world space + */ +inline float RPPointLight::get_radius() const { + return _radius; +} + +/** + * @brief Sets the inner radius of the light + * @details This sets the inner radius of the light. Anything greater than + * zero causes the light to get an area light. This has influence on the + * specular highlights of the light aswell as the shadows. + * + * The inner radius controls the size of the lights sphere size in world + * space units. A radius of 0 means the light has no inner radius, and the + * light will be have like an infinite small point light source. + * A radius greater than zero will cause the light to behave like it would be + * an emissive sphere with the given inner radius emitting light. This is + * more physically correct. + * + * @param inner_radius Inner-radius in world space + */ +inline void RPPointLight::set_inner_radius(float inner_radius) { + nassertv(inner_radius >= 0.01); // Invalid inner radius + _inner_radius = inner_radius; + set_needs_update(true); +} + +/** + * @brief Returns the inner radius of the light + * @details This returns the inner radius of the light, previously set with + * RPPointLight::get_inner_radius. + * @return [description] + */ +inline float RPPointLight::get_inner_radius() const { + return _inner_radius; +} diff --git a/RenderPipelineFile/rpcore/native/source/rp_point_light.cpp b/RenderPipelineFile/rpcore/native/source/rp_point_light.cpp index 8fb5ba70..d93e8dd5 100644 --- a/RenderPipelineFile/rpcore/native/source/rp_point_light.cpp +++ b/RenderPipelineFile/rpcore/native/source/rp_point_light.cpp @@ -1,90 +1,90 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -#include "rp_point_light.h" - - -/** - * @brief Constructs a new point light - * @details This contructs a new point light with default settings. By default - * the light is set to be an infinitely small point light source. You can - * change this with RPPointLight::set_inner_radius. - */ -RPPointLight::RPPointLight() : RPLight(RPLight::LT_point_light) { - _radius = 10.0; - _inner_radius = 0.01; -} - -/** - * @brief Writes the light to a GPUCommand - * @details This writes the point light data to a GPUCommand. - * @see RPLight::write_to_command - * - * @param cmd The target GPUCommand - */ -void RPPointLight::write_to_command(GPUCommand &cmd) { - RPLight::write_to_command(cmd); - cmd.push_float(_radius); - cmd.push_float(_inner_radius); -} - -/** - * @brief Inits the shadow sources of the light - * @details This inits all required shadow sources for the point light. - * @see RPLight::init_shadow_sources - */ -void RPPointLight::init_shadow_sources() { - nassertv(_shadow_sources.size() == 0); - // Create 6 shadow sources, one for each direction - for(size_t i = 0; i < 6; ++i) { - _shadow_sources.push_back(new ShadowSource()); - } -} - -/** - * @brief Updates the shadow sources - * @details This updates all shadow sources of the light. - * @see RPLight::update_shadow_sources - */ -void RPPointLight::update_shadow_sources() { - LVecBase3f directions[6] = { - LVecBase3f( 1, 0, 0), - LVecBase3f(-1, 0, 0), - LVecBase3f( 0, 1, 0), - LVecBase3f( 0, -1, 0), - LVecBase3f( 0, 0, 1), - LVecBase3f( 0, 0, -1) - }; - - // Increase fov to prevent artifacts at the shadow map transitions - const float fov = 90.0f + 3.0f; - for (size_t i = 0; i < _shadow_sources.size(); ++i) { - _shadow_sources[i]->set_resolution(get_shadow_map_resolution()); - _shadow_sources[i]->set_perspective_lens(fov, _near_plane, _radius, - _position, directions[i]); - } -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +#include "rp_point_light.h" + + +/** + * @brief Constructs a new point light + * @details This contructs a new point light with default settings. By default + * the light is set to be an infinitely small point light source. You can + * change this with RPPointLight::set_inner_radius. + */ +RPPointLight::RPPointLight() : RPLight(RPLight::LT_point_light) { + _radius = 10.0; + _inner_radius = 0.01; +} + +/** + * @brief Writes the light to a GPUCommand + * @details This writes the point light data to a GPUCommand. + * @see RPLight::write_to_command + * + * @param cmd The target GPUCommand + */ +void RPPointLight::write_to_command(GPUCommand &cmd) { + RPLight::write_to_command(cmd); + cmd.push_float(_radius); + cmd.push_float(_inner_radius); +} + +/** + * @brief Inits the shadow sources of the light + * @details This inits all required shadow sources for the point light. + * @see RPLight::init_shadow_sources + */ +void RPPointLight::init_shadow_sources() { + nassertv(_shadow_sources.size() == 0); + // Create 6 shadow sources, one for each direction + for(size_t i = 0; i < 6; ++i) { + _shadow_sources.push_back(new ShadowSource()); + } +} + +/** + * @brief Updates the shadow sources + * @details This updates all shadow sources of the light. + * @see RPLight::update_shadow_sources + */ +void RPPointLight::update_shadow_sources() { + LVecBase3f directions[6] = { + LVecBase3f( 1, 0, 0), + LVecBase3f(-1, 0, 0), + LVecBase3f( 0, 1, 0), + LVecBase3f( 0, -1, 0), + LVecBase3f( 0, 0, 1), + LVecBase3f( 0, 0, -1) + }; + + // Increase fov to prevent artifacts at the shadow map transitions + const float fov = 90.0f + 3.0f; + for (size_t i = 0; i < _shadow_sources.size(); ++i) { + _shadow_sources[i]->set_resolution(get_shadow_map_resolution()); + _shadow_sources[i]->set_perspective_lens(fov, _near_plane, _radius, + _position, directions[i]); + } +} diff --git a/RenderPipelineFile/rpcore/native/source/rp_point_light.h b/RenderPipelineFile/rpcore/native/source/rp_point_light.h index cca9ea11..e4f11957 100644 --- a/RenderPipelineFile/rpcore/native/source/rp_point_light.h +++ b/RenderPipelineFile/rpcore/native/source/rp_point_light.h @@ -1,66 +1,66 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#ifndef RP_POINT_LIGHT_H -#define RP_POINT_LIGHT_H - -#include "pandabase.h" -#include "rp_light.h" - -/** - * @brief PointLight class - * @details This represents a point light, a light which has a position and - * radius. Checkout the RenderPipeline documentation for more information - * about this type of light. - */ -class RPPointLight : public RPLight { - - PUBLISHED: - RPPointLight(); - - inline void set_radius(float radius); - inline float get_radius() const; - MAKE_PROPERTY(radius, get_radius, set_radius); - - inline void set_inner_radius(float inner_radius); - inline float get_inner_radius() const; - MAKE_PROPERTY(inner_radius, get_inner_radius, set_inner_radius); - - public: - virtual void write_to_command(GPUCommand &cmd); - virtual void update_shadow_sources(); - virtual void init_shadow_sources(); - - protected: - - float _radius; - float _inner_radius; - -}; - -#include "rp_point_light.I" - -#endif // RP_POINT_LIGHT_H +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef RP_POINT_LIGHT_H +#define RP_POINT_LIGHT_H + +#include "pandabase.h" +#include "rp_light.h" + +/** + * @brief PointLight class + * @details This represents a point light, a light which has a position and + * radius. Checkout the RenderPipeline documentation for more information + * about this type of light. + */ +class RPPointLight : public RPLight { + + PUBLISHED: + RPPointLight(); + + inline void set_radius(float radius); + inline float get_radius() const; + MAKE_PROPERTY(radius, get_radius, set_radius); + + inline void set_inner_radius(float inner_radius); + inline float get_inner_radius() const; + MAKE_PROPERTY(inner_radius, get_inner_radius, set_inner_radius); + + public: + virtual void write_to_command(GPUCommand &cmd); + virtual void update_shadow_sources(); + virtual void init_shadow_sources(); + + protected: + + float _radius; + float _inner_radius; + +}; + +#include "rp_point_light.I" + +#endif // RP_POINT_LIGHT_H diff --git a/RenderPipelineFile/rpcore/native/source/rp_spot_light.I b/RenderPipelineFile/rpcore/native/source/rp_spot_light.I index ba6b7d4a..817ca4d1 100644 --- a/RenderPipelineFile/rpcore/native/source/rp_spot_light.I +++ b/RenderPipelineFile/rpcore/native/source/rp_spot_light.I @@ -1,74 +1,74 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - - -inline void RPSpotLight::set_radius(float radius) { - _radius = radius; - set_needs_update(true); - invalidate_shadows(); -} - -inline float RPSpotLight::get_radius() const { - return _radius; -} - - -inline void RPSpotLight::set_fov(float fov) { - _fov = fov; - set_needs_update(true); - invalidate_shadows(); -} - -inline float RPSpotLight::get_fov() const { - return _fov; -} - -inline void RPSpotLight::set_direction(LVecBase3f direction) { - _direction = direction; - _direction.normalize(); - set_needs_update(true); - invalidate_shadows(); -} - -inline void RPSpotLight::set_direction(float dx, float dy, float dz) { - _direction.set(dx, dy, dz); - _direction.normalize(); - set_needs_update(true); - invalidate_shadows(); -} - -inline const LVecBase3f& RPSpotLight::get_direction() const { - return _direction; -} - -inline void RPSpotLight::look_at(LVecBase3f point) { - set_direction(point - _position); -} - -inline void RPSpotLight::look_at(float x, float y, float z) { - set_direction(LVecBase3f(x, y, z) - _position); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + + +inline void RPSpotLight::set_radius(float radius) { + _radius = radius; + set_needs_update(true); + invalidate_shadows(); +} + +inline float RPSpotLight::get_radius() const { + return _radius; +} + + +inline void RPSpotLight::set_fov(float fov) { + _fov = fov; + set_needs_update(true); + invalidate_shadows(); +} + +inline float RPSpotLight::get_fov() const { + return _fov; +} + +inline void RPSpotLight::set_direction(LVecBase3f direction) { + _direction = direction; + _direction.normalize(); + set_needs_update(true); + invalidate_shadows(); +} + +inline void RPSpotLight::set_direction(float dx, float dy, float dz) { + _direction.set(dx, dy, dz); + _direction.normalize(); + set_needs_update(true); + invalidate_shadows(); +} + +inline const LVecBase3f& RPSpotLight::get_direction() const { + return _direction; +} + +inline void RPSpotLight::look_at(LVecBase3f point) { + set_direction(point - _position); +} + +inline void RPSpotLight::look_at(float x, float y, float z) { + set_direction(LVecBase3f(x, y, z) - _position); +} diff --git a/RenderPipelineFile/rpcore/native/source/rp_spot_light.cpp b/RenderPipelineFile/rpcore/native/source/rp_spot_light.cpp index 2dbcf222..9596fa60 100644 --- a/RenderPipelineFile/rpcore/native/source/rp_spot_light.cpp +++ b/RenderPipelineFile/rpcore/native/source/rp_spot_light.cpp @@ -1,80 +1,80 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -#include "rp_spot_light.h" - -#define _USE_MATH_DEFINES -#include - - -/** - * @brief Creates a new spot light - * @details This creates a new spot light with default properties set. You should - * set at least a direction, fov, radius and position to make the light useful. - */ -RPSpotLight::RPSpotLight() : RPLight(RPLight::LT_spot_light) { - _radius = 10.0; - _fov = 45.0; - _direction.set(0, 0, -1); -} - -/** - * @brief Writes the light to a GPUCommand - * @details This writes the spot light data to a GPUCommand. - * @see RPLight::write_to_command - * - * @param cmd The target GPUCommand - */ -void RPSpotLight::write_to_command(GPUCommand &cmd) { - RPLight::write_to_command(cmd); - cmd.push_float(_radius); - - // Encode FOV as cos(fov) - cmd.push_float(cos(_fov / 360.0 * M_PI)); - cmd.push_vec3(_direction); -} - -/** - * @brief Inits the shadow sources of the light - * @details This inits all required shadow sources for the spot light. - * @see RPLight::init_shadow_sources - */ -void RPSpotLight::init_shadow_sources() { - nassertv(_shadow_sources.size() == 0); - _shadow_sources.push_back(new ShadowSource()); -} - -/** - * @brief Updates the shadow sources - * @details This updates all shadow sources of the light. - * @see RPLight::update_shadow_sources - */ -void RPSpotLight::update_shadow_sources() { - _shadow_sources[0]->set_resolution(get_shadow_map_resolution()); - _shadow_sources[0]->set_perspective_lens(_fov, _near_plane, _radius, _position, _direction); -} - +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +#include "rp_spot_light.h" + +#define _USE_MATH_DEFINES +#include + + +/** + * @brief Creates a new spot light + * @details This creates a new spot light with default properties set. You should + * set at least a direction, fov, radius and position to make the light useful. + */ +RPSpotLight::RPSpotLight() : RPLight(RPLight::LT_spot_light) { + _radius = 10.0; + _fov = 45.0; + _direction.set(0, 0, -1); +} + +/** + * @brief Writes the light to a GPUCommand + * @details This writes the spot light data to a GPUCommand. + * @see RPLight::write_to_command + * + * @param cmd The target GPUCommand + */ +void RPSpotLight::write_to_command(GPUCommand &cmd) { + RPLight::write_to_command(cmd); + cmd.push_float(_radius); + + // Encode FOV as cos(fov) + cmd.push_float(cos(_fov / 360.0 * M_PI)); + cmd.push_vec3(_direction); +} + +/** + * @brief Inits the shadow sources of the light + * @details This inits all required shadow sources for the spot light. + * @see RPLight::init_shadow_sources + */ +void RPSpotLight::init_shadow_sources() { + nassertv(_shadow_sources.size() == 0); + _shadow_sources.push_back(new ShadowSource()); +} + +/** + * @brief Updates the shadow sources + * @details This updates all shadow sources of the light. + * @see RPLight::update_shadow_sources + */ +void RPSpotLight::update_shadow_sources() { + _shadow_sources[0]->set_resolution(get_shadow_map_resolution()); + _shadow_sources[0]->set_perspective_lens(_fov, _near_plane, _radius, _position, _direction); +} + diff --git a/RenderPipelineFile/rpcore/native/source/rp_spot_light.h b/RenderPipelineFile/rpcore/native/source/rp_spot_light.h index aade5994..922fa9ee 100644 --- a/RenderPipelineFile/rpcore/native/source/rp_spot_light.h +++ b/RenderPipelineFile/rpcore/native/source/rp_spot_light.h @@ -1,72 +1,72 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#ifndef RP_SPOT_LIGHT_H -#define RP_SPOT_LIGHT_H - -#include "pandabase.h" -#include "rp_light.h" - -/** - * @brief SpotLight class - * @details This represents a spot light, a light which has a position, radius, - * direction and FoV. Checkout the RenderPipeline documentation for more - * information about this type of light. - */ -class RPSpotLight : public RPLight { - - PUBLISHED: - RPSpotLight(); - - inline void set_radius(float radius); - inline float get_radius() const; - MAKE_PROPERTY(radius, get_radius, set_radius); - - inline void set_fov(float fov); - inline float get_fov() const; - MAKE_PROPERTY(fov, get_fov, set_fov); - - inline void set_direction(LVecBase3f direction); - inline void set_direction(float dx, float dy, float dz); - inline const LVecBase3f& get_direction() const; - inline void look_at(LVecBase3f point); - inline void look_at(float x, float y, float z); - MAKE_PROPERTY(direction, get_direction, set_direction); - - public: - virtual void write_to_command(GPUCommand &cmd); - virtual void init_shadow_sources(); - virtual void update_shadow_sources(); - - protected: - float _radius; - float _fov; - LVecBase3f _direction; -}; - -#include "rp_spot_light.I" - -#endif // RP_SPOT_LIGHT_H +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef RP_SPOT_LIGHT_H +#define RP_SPOT_LIGHT_H + +#include "pandabase.h" +#include "rp_light.h" + +/** + * @brief SpotLight class + * @details This represents a spot light, a light which has a position, radius, + * direction and FoV. Checkout the RenderPipeline documentation for more + * information about this type of light. + */ +class RPSpotLight : public RPLight { + + PUBLISHED: + RPSpotLight(); + + inline void set_radius(float radius); + inline float get_radius() const; + MAKE_PROPERTY(radius, get_radius, set_radius); + + inline void set_fov(float fov); + inline float get_fov() const; + MAKE_PROPERTY(fov, get_fov, set_fov); + + inline void set_direction(LVecBase3f direction); + inline void set_direction(float dx, float dy, float dz); + inline const LVecBase3f& get_direction() const; + inline void look_at(LVecBase3f point); + inline void look_at(float x, float y, float z); + MAKE_PROPERTY(direction, get_direction, set_direction); + + public: + virtual void write_to_command(GPUCommand &cmd); + virtual void init_shadow_sources(); + virtual void update_shadow_sources(); + + protected: + float _radius; + float _fov; + LVecBase3f _direction; +}; + +#include "rp_spot_light.I" + +#endif // RP_SPOT_LIGHT_H diff --git a/RenderPipelineFile/rpcore/native/source/shadow_atlas.I b/RenderPipelineFile/rpcore/native/source/shadow_atlas.I index 2bb93c63..012a7d91 100644 --- a/RenderPipelineFile/rpcore/native/source/shadow_atlas.I +++ b/RenderPipelineFile/rpcore/native/source/shadow_atlas.I @@ -1,159 +1,159 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -/** - * @brief Returns the tile size of the atlas. - * @details This returns the tile size of the atlas, which was set in the - * constructor. This is the smalles unit of the atlas, and every resolution - * has to be a multiple of the tile size. - * @return Tile size in pixels - */ -inline int ShadowAtlas::get_tile_size() const { - return _tile_size; -} - -/** - * @brief Sets a specific tile status. - * @details This marks a tile to either reserved or free, depending on the flag. - * If flag is true, the tile gets marked as reserved. If flag is false, the - * tile gets marked as free. - * - * No bounds checking is done for performance reasons. Passing an invalid tile - * index causes a crash. The coordinates are expected to be in tile space. - * - * @param x x-position of the tile - * @param y y-position of the tile - * @param flag Flag to set the tile to - */ -inline void ShadowAtlas::set_tile(size_t x, size_t y, bool flag) { - _flags[x + y * _num_tiles] = flag; -} - -/** - * @brief Returns the status of a given tile. - * @details This returns the value of a tile. If the tile is true, this means - * the tile is already reserved. If the tile is false, the tile can be - * used, and is not reserved. - * - * No bounds checking is done for performance reasons. Passing an invalid tile - * index causes a crash. The coordinates are expected to be in tile space. - * - * @param x x-position of the tile - * @param y y-position of the tile - * - * @return Tile-Status - */ -inline bool ShadowAtlas::get_tile(size_t x, size_t y) const { - return _flags[x + y * _num_tiles]; -} - -/** - * @brief Checks wheter a given region is free. - * @details This checks whether a given region in the atlas is still free. This - * is true if *all* tiles in that region are false, and thus are not taken yet. - * The coordinates are expected to be in tile space. - * - * Passing an invalid region, causes an assertion, in case those are enabled. - * If assertions are optimized out, this method will crash when passing invalid - * bounds. - * - * @param x x- start position of the region - * @param y y- start position of the region - * @param w width of the region - * @param h height of the region - * @return true if the region is completely free, else false - */ -inline bool ShadowAtlas::region_is_free(size_t x, size_t y, size_t w, size_t h) const { - // Check if we are out of bounds, this should be disabled for performance - // reasons at some point. - nassertr(x >= 0 && y >= 0 && x + w <= _num_tiles && y + h <= _num_tiles, false); - - // Iterate over every tile in that region and check if it is still free. - for (size_t cx = 0; cx < w; ++cx) { - for (size_t cy = 0; cy < h; ++cy) { - if (get_tile(cx + x, cy + y)) return false; - } - } - return true; -} - -/** - * @brief Returns the amount of tiles required to store a resolution. - * @details Returns the amount of tiles which would be required to store a - * given resolution. This basically just returns resolution / tile_size. - * - * When an invalid resolution is passed (not a multiple of the tile size), - * an error is printed and 1 is returned. - * When a negative or zero resolution is passed, undefined behaviour occurs. - * - * @param resolution The resolution to compute the amount of tiles for - * @return Amount of tiles to store the resolution - */ -inline int ShadowAtlas::get_required_tiles(size_t resolution) const { - nassertr(resolution > 0, -1); - - if (resolution % _tile_size != 0) { - shadowatlas_cat.error() << "Resolution " << resolution << " is not a multiple " - << "of the shadow atlas tile size (" << _tile_size << ")!" << endl; - return 1; - } - return resolution / _tile_size; -} - -/** - * @brief Converts a tile-space region to uv space. - * @details This converts a region (presumably from ShadowAtlas::find_and_reserve_region) - * to uv space (0 .. 1 range). This can be used in shaders, since they expect - * floating point coordinates instead of integer coordinates. - * - * @param region tile-space region - * @return uv-space region - */ -inline LVecBase4f ShadowAtlas::region_to_uv(const LVecBase4i& region) { - LVecBase4f flt = LVecBase4f(region.get_x(), region.get_y(), region.get_z(), region.get_w()); - return flt * ((float)_tile_size / (float)_size); -} - -/** - * @brief Returns the amount of used tiles - * @details Returns the amount of used tiles in the atlas - * @return Amount of used tiles - */ -inline int ShadowAtlas::get_num_used_tiles() const { - return _num_used_tiles; -} - -/** - * @brief Returns the amount of used tiles in percentage - * @details This returns in percentage from 0 to 1 how much space of the atlas - * is used right now. A value of 1 means the atlas is completely full, whereas - * a value of 0 means the atlas is completely free. - * @return Atlas usage in percentage - */ -inline float ShadowAtlas::get_coverage() const { - return float(_num_used_tiles) / float(_num_tiles * _num_tiles); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +/** + * @brief Returns the tile size of the atlas. + * @details This returns the tile size of the atlas, which was set in the + * constructor. This is the smalles unit of the atlas, and every resolution + * has to be a multiple of the tile size. + * @return Tile size in pixels + */ +inline int ShadowAtlas::get_tile_size() const { + return _tile_size; +} + +/** + * @brief Sets a specific tile status. + * @details This marks a tile to either reserved or free, depending on the flag. + * If flag is true, the tile gets marked as reserved. If flag is false, the + * tile gets marked as free. + * + * No bounds checking is done for performance reasons. Passing an invalid tile + * index causes a crash. The coordinates are expected to be in tile space. + * + * @param x x-position of the tile + * @param y y-position of the tile + * @param flag Flag to set the tile to + */ +inline void ShadowAtlas::set_tile(size_t x, size_t y, bool flag) { + _flags[x + y * _num_tiles] = flag; +} + +/** + * @brief Returns the status of a given tile. + * @details This returns the value of a tile. If the tile is true, this means + * the tile is already reserved. If the tile is false, the tile can be + * used, and is not reserved. + * + * No bounds checking is done for performance reasons. Passing an invalid tile + * index causes a crash. The coordinates are expected to be in tile space. + * + * @param x x-position of the tile + * @param y y-position of the tile + * + * @return Tile-Status + */ +inline bool ShadowAtlas::get_tile(size_t x, size_t y) const { + return _flags[x + y * _num_tiles]; +} + +/** + * @brief Checks wheter a given region is free. + * @details This checks whether a given region in the atlas is still free. This + * is true if *all* tiles in that region are false, and thus are not taken yet. + * The coordinates are expected to be in tile space. + * + * Passing an invalid region, causes an assertion, in case those are enabled. + * If assertions are optimized out, this method will crash when passing invalid + * bounds. + * + * @param x x- start position of the region + * @param y y- start position of the region + * @param w width of the region + * @param h height of the region + * @return true if the region is completely free, else false + */ +inline bool ShadowAtlas::region_is_free(size_t x, size_t y, size_t w, size_t h) const { + // Check if we are out of bounds, this should be disabled for performance + // reasons at some point. + nassertr(x >= 0 && y >= 0 && x + w <= _num_tiles && y + h <= _num_tiles, false); + + // Iterate over every tile in that region and check if it is still free. + for (size_t cx = 0; cx < w; ++cx) { + for (size_t cy = 0; cy < h; ++cy) { + if (get_tile(cx + x, cy + y)) return false; + } + } + return true; +} + +/** + * @brief Returns the amount of tiles required to store a resolution. + * @details Returns the amount of tiles which would be required to store a + * given resolution. This basically just returns resolution / tile_size. + * + * When an invalid resolution is passed (not a multiple of the tile size), + * an error is printed and 1 is returned. + * When a negative or zero resolution is passed, undefined behaviour occurs. + * + * @param resolution The resolution to compute the amount of tiles for + * @return Amount of tiles to store the resolution + */ +inline int ShadowAtlas::get_required_tiles(size_t resolution) const { + nassertr(resolution > 0, -1); + + if (resolution % _tile_size != 0) { + shadowatlas_cat.error() << "Resolution " << resolution << " is not a multiple " + << "of the shadow atlas tile size (" << _tile_size << ")!" << endl; + return 1; + } + return resolution / _tile_size; +} + +/** + * @brief Converts a tile-space region to uv space. + * @details This converts a region (presumably from ShadowAtlas::find_and_reserve_region) + * to uv space (0 .. 1 range). This can be used in shaders, since they expect + * floating point coordinates instead of integer coordinates. + * + * @param region tile-space region + * @return uv-space region + */ +inline LVecBase4f ShadowAtlas::region_to_uv(const LVecBase4i& region) { + LVecBase4f flt = LVecBase4f(region.get_x(), region.get_y(), region.get_z(), region.get_w()); + return flt * ((float)_tile_size / (float)_size); +} + +/** + * @brief Returns the amount of used tiles + * @details Returns the amount of used tiles in the atlas + * @return Amount of used tiles + */ +inline int ShadowAtlas::get_num_used_tiles() const { + return _num_used_tiles; +} + +/** + * @brief Returns the amount of used tiles in percentage + * @details This returns in percentage from 0 to 1 how much space of the atlas + * is used right now. A value of 1 means the atlas is completely full, whereas + * a value of 0 means the atlas is completely free. + * @return Atlas usage in percentage + */ +inline float ShadowAtlas::get_coverage() const { + return float(_num_used_tiles) / float(_num_tiles * _num_tiles); +} diff --git a/RenderPipelineFile/rpcore/native/source/shadow_atlas.cpp b/RenderPipelineFile/rpcore/native/source/shadow_atlas.cpp index d27bc675..9133384b 100644 --- a/RenderPipelineFile/rpcore/native/source/shadow_atlas.cpp +++ b/RenderPipelineFile/rpcore/native/source/shadow_atlas.cpp @@ -1,186 +1,186 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -#include "shadow_atlas.h" -#include - -NotifyCategoryDef(shadowatlas, ""); - -/** - * @brief Constructs a new shadow atlas. - * @details This constructs a new shadow atlas with the given size and tile size. - * - * The size determines the total size of the atlas in pixels. It should be a - * power-of-two to favour the GPU. - * - * The tile_size determines the smallest unit of tiles the atlas can store. - * If, for example, a tile_size of 32 is used, then every entry stored must - * have a resolution of 32 or greater, and the resolution must be a multiple - * of 32. This is to optimize the search in the atlas, so the atlas does not - * have to check every pixel, and instead can just check whole tiles. - * - * If you want to disable the use of tiles, set the tile_size to 1, which - * will make the shadow atlas use pixels instead of tiles. - * - * @param size Atlas-size in pixels - * @param tile_size tile-size in pixels, or 1 to use no tiles. - */ -ShadowAtlas::ShadowAtlas(size_t size, size_t tile_size) { - nassertv(size > 1 && tile_size >= 1); - nassertv(tile_size < size && size % tile_size == 0); - _size = size; - _tile_size = tile_size; - _num_used_tiles = 0; - init_tiles(); -} - -/** - * @brief Destructs the shadow atlas. - * @details This destructs the shadow atlas, freeing all used resources. - */ -ShadowAtlas::~ShadowAtlas() { - delete [] _flags; -} - -/** - * @brief Internal method to init the storage. - * @details This method setups the storage used for storing the tile flags. - */ -void ShadowAtlas::init_tiles() { - _num_tiles = _size / _tile_size; - _flags = new bool[_num_tiles * _num_tiles]; - memset(_flags, 0x0, sizeof(bool) * _num_tiles * _num_tiles); -} - -/** - * @brief Internal method to reserve a region in the atlas. - * @details This reserves a given region in the shadow atlas. The region should - * be in tile space.This is called by the ShadowAtlas::find_and_reserve_region. - * It sets all flags in that region to true, indicating that those are used. - * When an invalid region is passed, an assertion is triggered. If assertions - * are optimized out, undefined behaviour occurs. - * - * @param x x- start positition of the region - * @param y y- start position of the region - * @param w width of the region - * @param h height of the region - */ -void ShadowAtlas::reserve_region(size_t x, size_t y, size_t w, size_t h) { - // Check if we are out of bounds, this should be disabled for performance - // reasons at some point. - nassertv(x >= 0 && y >= 0 && x + w <= _num_tiles && y + h <= _num_tiles); - - _num_used_tiles += w * h; - - // Iterate over every tile in the region and mark it as used - for (size_t cx = 0; cx < w; ++cx) { - for (size_t cy = 0; cy < h; ++cy) { - set_tile(cx + x, cy + y, true); - } - } -} - -/** - * @brief Finds space for a map of the given size in the atlas. - * @details This methods searches for a space to store a region of the given - * size in the atlas. tile_width and tile_height should be already in tile - * space. They can be converted using ShadowAtlas::get_required_tiles. - * - * If no region is found, or an invalid size is passed, an integer vector with - * all components set to -1 is returned. - * - * If a region is found, an integer vector with the given layout is returned: - * x: x- Start of the region - * y: y- Start of the region - * z: width of the region - * w: height of the region - * - * The layout is in tile space, and can get converted to uv space using - * ShadowAtlas::region_to_uv. - * - * @param tile_width Width of the region in tile space - * @param tile_height Height of the region in tile space - * - * @return Region, see description, or -1 when no region is found. - */ -LVecBase4i ShadowAtlas::find_and_reserve_region(size_t tile_width, size_t tile_height) { - - // Check for empty region - if (tile_width < 1 || tile_height < 1) { - shadowatlas_cat.error() << "Called find_and_reserve_region with null-region!" << endl; - return LVecBase4i(-1); - } - - // Check for region bigger than the shadow atlas - if (tile_width > _num_tiles || tile_height > _num_tiles) { - shadowatlas_cat.error() << "Requested region exceeds shadow atlas size!" << endl; - return LVecBase4i(-1); - } - - // Iterate over every possible region and check if its still free - for (size_t x = 0; x <= _num_tiles - tile_width; ++x) { - for (size_t y = 0; y <= _num_tiles - tile_height; ++y) { - if (region_is_free(x, y, tile_width, tile_height)) { - // Found free region, now reserve it - reserve_region(x, y, tile_width, tile_height); - return LVecBase4i(x, y, tile_width, tile_height); - } - } - } - - // When we reached this part, we couldn't find a free region, so the atlas - // seems to be full. - shadowatlas_cat.error() << "Failed to find a free region of size " << tile_width - << " x " << tile_height << "!" << endl; - return LVecBase4i(-1); -} - -/** - * @brief Frees a given region - * @details This frees a given region, marking it as free so that other shadow - * maps can use the space again. The region should be the same as returned - * by ShadowAtlas::find_and_reserve_region. - * - * If an invalid region is passed, an assertion is triggered. If assertions - * are compiled out, undefined behaviour will occur. - * - * @param region Region to free - */ -void ShadowAtlas::free_region(const LVecBase4i& region) { - // Out of bounds check, can't hurt - nassertv(region.get_x() >= 0 && region.get_y() >= 0); - nassertv(region.get_x() + region.get_z() <= _num_tiles && region.get_y() + region.get_w() <= _num_tiles); - - _num_used_tiles -= region.get_z() * region.get_w(); - - for (size_t x = 0; x < region.get_z(); ++x) { - for (size_t y = 0; y < region.get_w(); ++y) { - // Could do an assert here, that the tile should have been used (=true) before - set_tile(region.get_x() + x, region.get_y() + y, false); - } - } -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +#include "shadow_atlas.h" +#include + +NotifyCategoryDef(shadowatlas, ""); + +/** + * @brief Constructs a new shadow atlas. + * @details This constructs a new shadow atlas with the given size and tile size. + * + * The size determines the total size of the atlas in pixels. It should be a + * power-of-two to favour the GPU. + * + * The tile_size determines the smallest unit of tiles the atlas can store. + * If, for example, a tile_size of 32 is used, then every entry stored must + * have a resolution of 32 or greater, and the resolution must be a multiple + * of 32. This is to optimize the search in the atlas, so the atlas does not + * have to check every pixel, and instead can just check whole tiles. + * + * If you want to disable the use of tiles, set the tile_size to 1, which + * will make the shadow atlas use pixels instead of tiles. + * + * @param size Atlas-size in pixels + * @param tile_size tile-size in pixels, or 1 to use no tiles. + */ +ShadowAtlas::ShadowAtlas(size_t size, size_t tile_size) { + nassertv(size > 1 && tile_size >= 1); + nassertv(tile_size < size && size % tile_size == 0); + _size = size; + _tile_size = tile_size; + _num_used_tiles = 0; + init_tiles(); +} + +/** + * @brief Destructs the shadow atlas. + * @details This destructs the shadow atlas, freeing all used resources. + */ +ShadowAtlas::~ShadowAtlas() { + delete [] _flags; +} + +/** + * @brief Internal method to init the storage. + * @details This method setups the storage used for storing the tile flags. + */ +void ShadowAtlas::init_tiles() { + _num_tiles = _size / _tile_size; + _flags = new bool[_num_tiles * _num_tiles]; + memset(_flags, 0x0, sizeof(bool) * _num_tiles * _num_tiles); +} + +/** + * @brief Internal method to reserve a region in the atlas. + * @details This reserves a given region in the shadow atlas. The region should + * be in tile space.This is called by the ShadowAtlas::find_and_reserve_region. + * It sets all flags in that region to true, indicating that those are used. + * When an invalid region is passed, an assertion is triggered. If assertions + * are optimized out, undefined behaviour occurs. + * + * @param x x- start positition of the region + * @param y y- start position of the region + * @param w width of the region + * @param h height of the region + */ +void ShadowAtlas::reserve_region(size_t x, size_t y, size_t w, size_t h) { + // Check if we are out of bounds, this should be disabled for performance + // reasons at some point. + nassertv(x >= 0 && y >= 0 && x + w <= _num_tiles && y + h <= _num_tiles); + + _num_used_tiles += w * h; + + // Iterate over every tile in the region and mark it as used + for (size_t cx = 0; cx < w; ++cx) { + for (size_t cy = 0; cy < h; ++cy) { + set_tile(cx + x, cy + y, true); + } + } +} + +/** + * @brief Finds space for a map of the given size in the atlas. + * @details This methods searches for a space to store a region of the given + * size in the atlas. tile_width and tile_height should be already in tile + * space. They can be converted using ShadowAtlas::get_required_tiles. + * + * If no region is found, or an invalid size is passed, an integer vector with + * all components set to -1 is returned. + * + * If a region is found, an integer vector with the given layout is returned: + * x: x- Start of the region + * y: y- Start of the region + * z: width of the region + * w: height of the region + * + * The layout is in tile space, and can get converted to uv space using + * ShadowAtlas::region_to_uv. + * + * @param tile_width Width of the region in tile space + * @param tile_height Height of the region in tile space + * + * @return Region, see description, or -1 when no region is found. + */ +LVecBase4i ShadowAtlas::find_and_reserve_region(size_t tile_width, size_t tile_height) { + + // Check for empty region + if (tile_width < 1 || tile_height < 1) { + shadowatlas_cat.error() << "Called find_and_reserve_region with null-region!" << endl; + return LVecBase4i(-1); + } + + // Check for region bigger than the shadow atlas + if (tile_width > _num_tiles || tile_height > _num_tiles) { + shadowatlas_cat.error() << "Requested region exceeds shadow atlas size!" << endl; + return LVecBase4i(-1); + } + + // Iterate over every possible region and check if its still free + for (size_t x = 0; x <= _num_tiles - tile_width; ++x) { + for (size_t y = 0; y <= _num_tiles - tile_height; ++y) { + if (region_is_free(x, y, tile_width, tile_height)) { + // Found free region, now reserve it + reserve_region(x, y, tile_width, tile_height); + return LVecBase4i(x, y, tile_width, tile_height); + } + } + } + + // When we reached this part, we couldn't find a free region, so the atlas + // seems to be full. + shadowatlas_cat.error() << "Failed to find a free region of size " << tile_width + << " x " << tile_height << "!" << endl; + return LVecBase4i(-1); +} + +/** + * @brief Frees a given region + * @details This frees a given region, marking it as free so that other shadow + * maps can use the space again. The region should be the same as returned + * by ShadowAtlas::find_and_reserve_region. + * + * If an invalid region is passed, an assertion is triggered. If assertions + * are compiled out, undefined behaviour will occur. + * + * @param region Region to free + */ +void ShadowAtlas::free_region(const LVecBase4i& region) { + // Out of bounds check, can't hurt + nassertv(region.get_x() >= 0 && region.get_y() >= 0); + nassertv(region.get_x() + region.get_z() <= _num_tiles && region.get_y() + region.get_w() <= _num_tiles); + + _num_used_tiles -= region.get_z() * region.get_w(); + + for (size_t x = 0; x < region.get_z(); ++x) { + for (size_t y = 0; y < region.get_w(); ++y) { + // Could do an assert here, that the tile should have been used (=true) before + set_tile(region.get_x() + x, region.get_y() + y, false); + } + } +} diff --git a/RenderPipelineFile/rpcore/native/source/shadow_atlas.h b/RenderPipelineFile/rpcore/native/source/shadow_atlas.h index 822e9837..50fa5f75 100644 --- a/RenderPipelineFile/rpcore/native/source/shadow_atlas.h +++ b/RenderPipelineFile/rpcore/native/source/shadow_atlas.h @@ -1,82 +1,82 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#ifndef RP_SHADOW_ATLAS_H -#define RP_SHADOW_ATLAS_H - -#include "pandabase.h" -#include "lvecBase4.h" - -NotifyCategoryDecl(shadowatlas, EXPORT_CLASS, EXPORT_TEMPL); - - -/** - * @brief Class which manages distributing shadow maps in an atlas. - * @details This class manages the shadow atlas. It handles finding and reserving - * space for new shadow maps. - */ -class ShadowAtlas { - -PUBLISHED: - ShadowAtlas(size_t size, size_t tile_size = 32); - ~ShadowAtlas(); - - inline int get_num_used_tiles() const; - inline float get_coverage() const; - - MAKE_PROPERTY(num_used_tiles, get_num_used_tiles); - MAKE_PROPERTY(coverage, get_coverage); - -public: - - LVecBase4i find_and_reserve_region(size_t tile_width, size_t tile_height); - void free_region(const LVecBase4i& region); - inline LVecBase4f region_to_uv(const LVecBase4i& region); - - inline int get_tile_size() const; - inline int get_required_tiles(size_t resolution) const; - -protected: - - void init_tiles(); - - inline void set_tile(size_t x, size_t y, bool flag); - inline bool get_tile(size_t x, size_t y) const; - - inline bool region_is_free(size_t x, size_t y, size_t w, size_t h) const; - void reserve_region(size_t x, size_t y, size_t w, size_t h); - - size_t _size; - size_t _num_tiles; - size_t _tile_size; - size_t _num_used_tiles; - bool* _flags; -}; - -#include "shadow_atlas.I" - - -#endif // RP_SHADOW_ATLAS_H +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef RP_SHADOW_ATLAS_H +#define RP_SHADOW_ATLAS_H + +#include "pandabase.h" +#include "lvecBase4.h" + +NotifyCategoryDecl(shadowatlas, EXPORT_CLASS, EXPORT_TEMPL); + + +/** + * @brief Class which manages distributing shadow maps in an atlas. + * @details This class manages the shadow atlas. It handles finding and reserving + * space for new shadow maps. + */ +class ShadowAtlas { + +PUBLISHED: + ShadowAtlas(size_t size, size_t tile_size = 32); + ~ShadowAtlas(); + + inline int get_num_used_tiles() const; + inline float get_coverage() const; + + MAKE_PROPERTY(num_used_tiles, get_num_used_tiles); + MAKE_PROPERTY(coverage, get_coverage); + +public: + + LVecBase4i find_and_reserve_region(size_t tile_width, size_t tile_height); + void free_region(const LVecBase4i& region); + inline LVecBase4f region_to_uv(const LVecBase4i& region); + + inline int get_tile_size() const; + inline int get_required_tiles(size_t resolution) const; + +protected: + + void init_tiles(); + + inline void set_tile(size_t x, size_t y, bool flag); + inline bool get_tile(size_t x, size_t y) const; + + inline bool region_is_free(size_t x, size_t y, size_t w, size_t h) const; + void reserve_region(size_t x, size_t y, size_t w, size_t h); + + size_t _size; + size_t _num_tiles; + size_t _tile_size; + size_t _num_used_tiles; + bool* _flags; +}; + +#include "shadow_atlas.I" + + +#endif // RP_SHADOW_ATLAS_H diff --git a/RenderPipelineFile/rpcore/native/source/shadow_manager.I b/RenderPipelineFile/rpcore/native/source/shadow_manager.I index b5039b37..fe415bd0 100644 --- a/RenderPipelineFile/rpcore/native/source/shadow_manager.I +++ b/RenderPipelineFile/rpcore/native/source/shadow_manager.I @@ -1,192 +1,192 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -/** - * @brief Sets the maximum amount of updates per frame. - * @details This controls the maximum amount of updated ShadowSources per frame. - * The ShadowManager will take the first ShadowSources, and - * generate shadow maps for them every frame. If there are more ShadowSources - * waiting to get updated than available updates, the sources are sorted by - * priority, and the update of the less important sources is delayed to the - * next frame. - * - * If the update count is set too low, and there are a lot of ShadowSources - * waiting to get updated, artifacts will occur, and there might be ShadowSources - * which never get updated, due to low priority. - * - * If an update count of 0 is passed, no updates will happen. This also means - * that there are no shadows. This is not recommended. - * - * If an update count < 0 is passed, undefined behaviour occurs. - * - * This method has to get called before ShadowManager::init, otherwise an - * assertion will get triggered. - * - * @param max_updates Maximum amoumt of updates - */ -inline void ShadowManager::set_max_updates(size_t max_updates) { - nassertv(max_updates >= 0); - nassertv(_atlas == NULL); // ShadowManager was already initialized - if (max_updates == 0) { - shadowmanager_cat.warning() << "max_updates set to 0, no shadows will be updated." << endl; - } - _max_updates = max_updates; -} - -/** - * @brief Sets the shadow atlas size - * @details This sets the desired shadow atlas size. It should be big enough - * to store all important shadow sources, with some buffer, because the shadow - * maps usually won't be fitted perfectly, so gaps can occur. - * - * This has to get called before calling ShadowManager::init. When calling this - * method after initialization, an assertion will get triggered. - * - * @param atlas_size Size of the shadow atlas in pixels - */ -inline void ShadowManager::set_atlas_size(size_t atlas_size) { - nassertv(atlas_size >= 16 && atlas_size <= 16384); - nassertv(_atlas == NULL); // ShadowManager was already initialized - _atlas_size = atlas_size; -} - -/** - * @brief Returns the shadow atlas size. - * @details This returns the shadow atlas size previously set with - * ShadowManager::set_atlas_size. - * @return Shadow atlas size in pixels - */ -inline size_t ShadowManager::get_atlas_size() const { - return _atlas_size; -} - - -/** - * @brief Returns a handle to the shadow atlas. - * @details This returns a handle to the internal shadow atlas instance. This - * is only valid after calling ShadowManager::init. Calling this earlier will - * trigger an assertion and undefined behaviour. - * @return The internal ShadowAtlas instance - */ -inline ShadowAtlas* ShadowManager::get_atlas() const { - nassertr(_atlas != NULL, NULL); // Can't hurt to check - return _atlas; -} - -/** - * @brief Sets the target scene - * @details This sets the target scene for rendering shadows. All shadow cameras - * will be parented to this scene to render shadows. - * - * Usually the scene will be ShowBase.render. If the scene is an empty or - * invalid NodePath, an assertion will be triggered. - * - * This method has to get called before calling ShadowManager::init, or an - * assertion will get triggered. - * - * @param scene_parent The target scene - */ -inline void ShadowManager::set_scene(NodePath scene_parent) { - nassertv(!scene_parent.is_empty()); - nassertv(_atlas == NULL); // ShadowManager was already initialized - _scene_parent = scene_parent; -} - -/** - * @brief Sets the handle to the TagStageManager. - * @details This sets the handle to the TagStateManager used by the pipeline. - * Usually this is RenderPipeline.get_tag_mgr(). - * - * This has to get called before ShadowManager::init, otherwise an assertion - * will get triggered. - * - * @param tag_mgr [description] - */ -inline void ShadowManager::set_tag_state_manager(TagStateManager* tag_mgr) { - nassertv(tag_mgr != NULL); - nassertv(_atlas == NULL); // ShadowManager was already initialized - _tag_state_mgr = tag_mgr; -} - -/** - * @brief Sets the handle to the Shadow targets output - * @details This sets the handle to the GraphicsOutput of the shadow atlas. - * Usually this is RenderTarget.get_internal_buffer(), whereas the RenderTarget - * is the target of the ShadowStage. - * - * This is used for creating display regions and attaching cameras to them, - * for performing shadow updates. - * - * This has to get called before ShadowManager::init, otherwise an assertion - * will be triggered. - * - * @param graphics_output [description] - */ -inline void ShadowManager::set_atlas_graphics_output(GraphicsOutput* graphics_output) { - nassertv(graphics_output != NULL); - nassertv(_atlas == NULL); // ShadowManager was already initialized - _atlas_graphics_output = graphics_output; -} - - -/** - * @brief Adds a new shadow update - * @details This adds a new update to the update queue. When the queue is already - * full, this method returns false, otherwise it returns true. The next time - * the manager is updated, the shadow source will recieve an update of its - * shadow map. - * - * @param source The shadow source to update - * - * @return Whether the shadow source udpate was sucessfully queued. - */ -inline bool ShadowManager::add_update(const ShadowSource* source) { - nassertr(_atlas != NULL, false); // ShadowManager::init not called yet. - nassertr(source != NULL, false); // NULL-Pointer passed - - if (_queued_updates.size() >= _max_updates) { - if (shadowmanager_cat.is_debug()) { - shadowmanager_cat.debug() << "cannot update source, out of update slots" << endl; - } - return false; - } - - // Add the update to the queue - _queued_updates.push_back(source); - return true; -} - -/** - * @brief Returns how many update slots are left. - * @details This returns how many update slots are left. You can assume the - * next n calls to add_update will succeed, whereas n is the value returned - * by this function. - * @return Number of update slots left. - */ -inline size_t ShadowManager::get_num_update_slots_left() const { - return _max_updates - _queued_updates.size(); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +/** + * @brief Sets the maximum amount of updates per frame. + * @details This controls the maximum amount of updated ShadowSources per frame. + * The ShadowManager will take the first ShadowSources, and + * generate shadow maps for them every frame. If there are more ShadowSources + * waiting to get updated than available updates, the sources are sorted by + * priority, and the update of the less important sources is delayed to the + * next frame. + * + * If the update count is set too low, and there are a lot of ShadowSources + * waiting to get updated, artifacts will occur, and there might be ShadowSources + * which never get updated, due to low priority. + * + * If an update count of 0 is passed, no updates will happen. This also means + * that there are no shadows. This is not recommended. + * + * If an update count < 0 is passed, undefined behaviour occurs. + * + * This method has to get called before ShadowManager::init, otherwise an + * assertion will get triggered. + * + * @param max_updates Maximum amoumt of updates + */ +inline void ShadowManager::set_max_updates(size_t max_updates) { + nassertv(max_updates >= 0); + nassertv(_atlas == NULL); // ShadowManager was already initialized + if (max_updates == 0) { + shadowmanager_cat.warning() << "max_updates set to 0, no shadows will be updated." << endl; + } + _max_updates = max_updates; +} + +/** + * @brief Sets the shadow atlas size + * @details This sets the desired shadow atlas size. It should be big enough + * to store all important shadow sources, with some buffer, because the shadow + * maps usually won't be fitted perfectly, so gaps can occur. + * + * This has to get called before calling ShadowManager::init. When calling this + * method after initialization, an assertion will get triggered. + * + * @param atlas_size Size of the shadow atlas in pixels + */ +inline void ShadowManager::set_atlas_size(size_t atlas_size) { + nassertv(atlas_size >= 16 && atlas_size <= 16384); + nassertv(_atlas == NULL); // ShadowManager was already initialized + _atlas_size = atlas_size; +} + +/** + * @brief Returns the shadow atlas size. + * @details This returns the shadow atlas size previously set with + * ShadowManager::set_atlas_size. + * @return Shadow atlas size in pixels + */ +inline size_t ShadowManager::get_atlas_size() const { + return _atlas_size; +} + + +/** + * @brief Returns a handle to the shadow atlas. + * @details This returns a handle to the internal shadow atlas instance. This + * is only valid after calling ShadowManager::init. Calling this earlier will + * trigger an assertion and undefined behaviour. + * @return The internal ShadowAtlas instance + */ +inline ShadowAtlas* ShadowManager::get_atlas() const { + nassertr(_atlas != NULL, NULL); // Can't hurt to check + return _atlas; +} + +/** + * @brief Sets the target scene + * @details This sets the target scene for rendering shadows. All shadow cameras + * will be parented to this scene to render shadows. + * + * Usually the scene will be ShowBase.render. If the scene is an empty or + * invalid NodePath, an assertion will be triggered. + * + * This method has to get called before calling ShadowManager::init, or an + * assertion will get triggered. + * + * @param scene_parent The target scene + */ +inline void ShadowManager::set_scene(NodePath scene_parent) { + nassertv(!scene_parent.is_empty()); + nassertv(_atlas == NULL); // ShadowManager was already initialized + _scene_parent = scene_parent; +} + +/** + * @brief Sets the handle to the TagStageManager. + * @details This sets the handle to the TagStateManager used by the pipeline. + * Usually this is RenderPipeline.get_tag_mgr(). + * + * This has to get called before ShadowManager::init, otherwise an assertion + * will get triggered. + * + * @param tag_mgr [description] + */ +inline void ShadowManager::set_tag_state_manager(TagStateManager* tag_mgr) { + nassertv(tag_mgr != NULL); + nassertv(_atlas == NULL); // ShadowManager was already initialized + _tag_state_mgr = tag_mgr; +} + +/** + * @brief Sets the handle to the Shadow targets output + * @details This sets the handle to the GraphicsOutput of the shadow atlas. + * Usually this is RenderTarget.get_internal_buffer(), whereas the RenderTarget + * is the target of the ShadowStage. + * + * This is used for creating display regions and attaching cameras to them, + * for performing shadow updates. + * + * This has to get called before ShadowManager::init, otherwise an assertion + * will be triggered. + * + * @param graphics_output [description] + */ +inline void ShadowManager::set_atlas_graphics_output(GraphicsOutput* graphics_output) { + nassertv(graphics_output != NULL); + nassertv(_atlas == NULL); // ShadowManager was already initialized + _atlas_graphics_output = graphics_output; +} + + +/** + * @brief Adds a new shadow update + * @details This adds a new update to the update queue. When the queue is already + * full, this method returns false, otherwise it returns true. The next time + * the manager is updated, the shadow source will recieve an update of its + * shadow map. + * + * @param source The shadow source to update + * + * @return Whether the shadow source udpate was sucessfully queued. + */ +inline bool ShadowManager::add_update(const ShadowSource* source) { + nassertr(_atlas != NULL, false); // ShadowManager::init not called yet. + nassertr(source != NULL, false); // NULL-Pointer passed + + if (_queued_updates.size() >= _max_updates) { + if (shadowmanager_cat.is_debug()) { + shadowmanager_cat.debug() << "cannot update source, out of update slots" << endl; + } + return false; + } + + // Add the update to the queue + _queued_updates.push_back(source); + return true; +} + +/** + * @brief Returns how many update slots are left. + * @details This returns how many update slots are left. You can assume the + * next n calls to add_update will succeed, whereas n is the value returned + * by this function. + * @return Number of update slots left. + */ +inline size_t ShadowManager::get_num_update_slots_left() const { + return _max_updates - _queued_updates.size(); +} diff --git a/RenderPipelineFile/rpcore/native/source/shadow_manager.cpp b/RenderPipelineFile/rpcore/native/source/shadow_manager.cpp index fb62271a..e5ad65bf 100644 --- a/RenderPipelineFile/rpcore/native/source/shadow_manager.cpp +++ b/RenderPipelineFile/rpcore/native/source/shadow_manager.cpp @@ -1,157 +1,157 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -#include "shadow_manager.h" - -NotifyCategoryDef(shadowmanager, ""); - -/** - * @brief Constructs a new shadow atlas - * @details This constructs a new shadow atlas. There are a set of properties - * which should be set before calling ShadowManager::init, see the set-Methods. - * After all properties are set, ShadowManager::init should get called. - * ShadowManager::update should get called on a per frame basis. - */ -ShadowManager::ShadowManager() { - _max_updates = 10; - _atlas = NULL; - _atlas_size = 4096; - _tag_state_mgr = NULL; - _atlas_graphics_output = NULL; -} - -/** - * @brief Destructs the ShadowManager - * @details This destructs the shadow manager, clearing all resources used - */ -ShadowManager::~ShadowManager() { - delete _atlas; - - // Todo: Could eventually unregister all shadow cameras. Since the tag state - // manager does this on cleanup already, and we get destructed at the same - // time (if at all), this is not really necessary -} - - -/** - * @brief Initializes the ShadowManager. - * @details This initializes the ShadowManager. All properties should have - * been set before calling this, otherwise assertions will get triggered. - * - * This setups everything required for rendering shadows, including the - * shadow atlas and the various shadow cameras. After calling this method, - * no properties can be changed anymore. - */ -void ShadowManager::init() { - nassertv(!_scene_parent.is_empty()); // Scene parent not set, call set_scene_parent before init! - nassertv(_tag_state_mgr != NULL); // TagStateManager not set, call set_tag_state_mgr before init! - nassertv(_atlas_graphics_output != NULL); // AtlasGraphicsOutput not set, call set_atlas_graphics_output before init! - - _cameras.resize(_max_updates); - _display_regions.resize(_max_updates); - _camera_nps.reserve(_max_updates); - - // Create the cameras and regions - for(size_t i = 0; i < _max_updates; ++i) { - - // Create the camera - PT(Camera) camera = new Camera("ShadowCam-" + to_string((long long)i)); - camera->set_lens(new MatrixLens()); - camera->set_active(false); - camera->set_scene(_scene_parent); - _tag_state_mgr->register_camera("shadow", camera); - _camera_nps.push_back(_scene_parent.attach_new_node(camera)); - _cameras[i] = camera; - - // Create the display region - PT(DisplayRegion) region = _atlas_graphics_output->make_display_region(); - region->set_sort(1000); - region->set_clear_depth_active(true); - region->set_clear_depth(1.0); - region->set_clear_color_active(false); - region->set_camera(_camera_nps[i]); - region->set_active(false); - _display_regions[i] = region; - } - - // Create the atlas - _atlas = new ShadowAtlas(_atlas_size); - - // Reserve enough space for the updates - _queued_updates.reserve(_max_updates); -} - - -/** - * @brief Updates the ShadowManager - * @details This updates the ShadowManager, processing all shadow sources which - * need to get updated. - * - * This first collects all sources which require an update, sorts them by priority, - * and then processes the first ShadowSources. - * - * This may not get called before ShadowManager::init, or an assertion will be - * thrown. - */ -void ShadowManager::update() { - nassertv(_atlas != NULL); // ShadowManager::init not called yet - nassertv(_queued_updates.size() <= _max_updates); // Internal error, should not happen - - // Disable all cameras and regions which will not be used - for (size_t i = _queued_updates.size(); i < _max_updates; ++i) { - _cameras[i]->set_active(false); - _display_regions[i]->set_active(false); - } - - // Iterate over all queued updates - for (size_t i = 0; i < _queued_updates.size(); ++i) { - const ShadowSource* source = _queued_updates[i]; - - // Enable the camera and display region, so they perform a render - _cameras[i]->set_active(true); - _display_regions[i]->set_active(true); - - // Set the view projection matrix - DCAST(MatrixLens, _cameras[i]->get_lens())->set_user_mat(source->get_mvp()); - - // Optional: Show the camera frustum for debugging - // _cameras[i]->show_frustum(); - - // Set the correct dimensions on the display region - const LVecBase4f& uv = source->get_uv_region(); - _display_regions[i]->set_dimensions( - uv.get_x(), // left - uv.get_x() + uv.get_z(), // right - uv.get_y(), // bottom - uv.get_y() + uv.get_w() // top - ); - } - - // Clear the update list - _queued_updates.clear(); - _queued_updates.reserve(_max_updates); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +#include "shadow_manager.h" + +NotifyCategoryDef(shadowmanager, ""); + +/** + * @brief Constructs a new shadow atlas + * @details This constructs a new shadow atlas. There are a set of properties + * which should be set before calling ShadowManager::init, see the set-Methods. + * After all properties are set, ShadowManager::init should get called. + * ShadowManager::update should get called on a per frame basis. + */ +ShadowManager::ShadowManager() { + _max_updates = 10; + _atlas = NULL; + _atlas_size = 4096; + _tag_state_mgr = NULL; + _atlas_graphics_output = NULL; +} + +/** + * @brief Destructs the ShadowManager + * @details This destructs the shadow manager, clearing all resources used + */ +ShadowManager::~ShadowManager() { + delete _atlas; + + // Todo: Could eventually unregister all shadow cameras. Since the tag state + // manager does this on cleanup already, and we get destructed at the same + // time (if at all), this is not really necessary +} + + +/** + * @brief Initializes the ShadowManager. + * @details This initializes the ShadowManager. All properties should have + * been set before calling this, otherwise assertions will get triggered. + * + * This setups everything required for rendering shadows, including the + * shadow atlas and the various shadow cameras. After calling this method, + * no properties can be changed anymore. + */ +void ShadowManager::init() { + nassertv(!_scene_parent.is_empty()); // Scene parent not set, call set_scene_parent before init! + nassertv(_tag_state_mgr != NULL); // TagStateManager not set, call set_tag_state_mgr before init! + nassertv(_atlas_graphics_output != NULL); // AtlasGraphicsOutput not set, call set_atlas_graphics_output before init! + + _cameras.resize(_max_updates); + _display_regions.resize(_max_updates); + _camera_nps.reserve(_max_updates); + + // Create the cameras and regions + for(size_t i = 0; i < _max_updates; ++i) { + + // Create the camera + PT(Camera) camera = new Camera("ShadowCam-" + to_string((long long)i)); + camera->set_lens(new MatrixLens()); + camera->set_active(false); + camera->set_scene(_scene_parent); + _tag_state_mgr->register_camera("shadow", camera); + _camera_nps.push_back(_scene_parent.attach_new_node(camera)); + _cameras[i] = camera; + + // Create the display region + PT(DisplayRegion) region = _atlas_graphics_output->make_display_region(); + region->set_sort(1000); + region->set_clear_depth_active(true); + region->set_clear_depth(1.0); + region->set_clear_color_active(false); + region->set_camera(_camera_nps[i]); + region->set_active(false); + _display_regions[i] = region; + } + + // Create the atlas + _atlas = new ShadowAtlas(_atlas_size); + + // Reserve enough space for the updates + _queued_updates.reserve(_max_updates); +} + + +/** + * @brief Updates the ShadowManager + * @details This updates the ShadowManager, processing all shadow sources which + * need to get updated. + * + * This first collects all sources which require an update, sorts them by priority, + * and then processes the first ShadowSources. + * + * This may not get called before ShadowManager::init, or an assertion will be + * thrown. + */ +void ShadowManager::update() { + nassertv(_atlas != NULL); // ShadowManager::init not called yet + nassertv(_queued_updates.size() <= _max_updates); // Internal error, should not happen + + // Disable all cameras and regions which will not be used + for (size_t i = _queued_updates.size(); i < _max_updates; ++i) { + _cameras[i]->set_active(false); + _display_regions[i]->set_active(false); + } + + // Iterate over all queued updates + for (size_t i = 0; i < _queued_updates.size(); ++i) { + const ShadowSource* source = _queued_updates[i]; + + // Enable the camera and display region, so they perform a render + _cameras[i]->set_active(true); + _display_regions[i]->set_active(true); + + // Set the view projection matrix + DCAST(MatrixLens, _cameras[i]->get_lens())->set_user_mat(source->get_mvp()); + + // Optional: Show the camera frustum for debugging + // _cameras[i]->show_frustum(); + + // Set the correct dimensions on the display region + const LVecBase4f& uv = source->get_uv_region(); + _display_regions[i]->set_dimensions( + uv.get_x(), // left + uv.get_x() + uv.get_z(), // right + uv.get_y(), // bottom + uv.get_y() + uv.get_w() // top + ); + } + + // Clear the update list + _queued_updates.clear(); + _queued_updates.reserve(_max_updates); +} diff --git a/RenderPipelineFile/rpcore/native/source/shadow_manager.h b/RenderPipelineFile/rpcore/native/source/shadow_manager.h index b6e73669..1163009a 100644 --- a/RenderPipelineFile/rpcore/native/source/shadow_manager.h +++ b/RenderPipelineFile/rpcore/native/source/shadow_manager.h @@ -1,92 +1,92 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#ifndef RP_SHADOW_MANAGER_H -#define RP_SHADOW_MANAGER_H - -#include "pandabase.h" -#include "camera.h" -#include "luse.h" -#include "matrixLens.h" -#include "referenceCount.h" -#include "nodePath.h" -#include "displayRegion.h" -#include "graphicsOutput.h" - -#include "tag_state_manager.h" -#include "shadow_source.h" -#include "shadow_atlas.h" - -NotifyCategoryDecl(shadowmanager, EXPORT_CLASS, EXPORT_TEMPL); - - -class ShadowManager : public ReferenceCount { - - PUBLISHED: - ShadowManager(); - ~ShadowManager(); - - inline void set_max_updates(size_t max_updates); - inline void set_scene(NodePath scene_parent); - inline void set_tag_state_manager(TagStateManager* tag_mgr); - inline void set_atlas_graphics_output(GraphicsOutput* graphics_output); - - inline void set_atlas_size(size_t atlas_size); - inline size_t get_atlas_size() const; - MAKE_PROPERTY(atlas_size, get_atlas_size, set_atlas_size); - - inline size_t get_num_update_slots_left() const; - MAKE_PROPERTY(num_update_slots_left, get_num_update_slots_left); - - inline ShadowAtlas* get_atlas() const; - MAKE_PROPERTY(atlas, get_atlas); - - void init(); - void update(); - - public: - inline bool add_update(const ShadowSource* source); - - private: - size_t _max_updates; - size_t _atlas_size; - NodePath _scene_parent; - - pvector _cameras; - pvector _camera_nps; - pvector _display_regions; - - ShadowAtlas* _atlas; - TagStateManager* _tag_state_mgr; - GraphicsOutput* _atlas_graphics_output; - - typedef pvector UpdateQueue; - UpdateQueue _queued_updates; -}; - -#include "shadow_manager.I" - -#endif // RP_SHADOW_MANAGER_H +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef RP_SHADOW_MANAGER_H +#define RP_SHADOW_MANAGER_H + +#include "pandabase.h" +#include "camera.h" +#include "luse.h" +#include "matrixLens.h" +#include "referenceCount.h" +#include "nodePath.h" +#include "displayRegion.h" +#include "graphicsOutput.h" + +#include "tag_state_manager.h" +#include "shadow_source.h" +#include "shadow_atlas.h" + +NotifyCategoryDecl(shadowmanager, EXPORT_CLASS, EXPORT_TEMPL); + + +class ShadowManager : public ReferenceCount { + + PUBLISHED: + ShadowManager(); + ~ShadowManager(); + + inline void set_max_updates(size_t max_updates); + inline void set_scene(NodePath scene_parent); + inline void set_tag_state_manager(TagStateManager* tag_mgr); + inline void set_atlas_graphics_output(GraphicsOutput* graphics_output); + + inline void set_atlas_size(size_t atlas_size); + inline size_t get_atlas_size() const; + MAKE_PROPERTY(atlas_size, get_atlas_size, set_atlas_size); + + inline size_t get_num_update_slots_left() const; + MAKE_PROPERTY(num_update_slots_left, get_num_update_slots_left); + + inline ShadowAtlas* get_atlas() const; + MAKE_PROPERTY(atlas, get_atlas); + + void init(); + void update(); + + public: + inline bool add_update(const ShadowSource* source); + + private: + size_t _max_updates; + size_t _atlas_size; + NodePath _scene_parent; + + pvector _cameras; + pvector _camera_nps; + pvector _display_regions; + + ShadowAtlas* _atlas; + TagStateManager* _tag_state_mgr; + GraphicsOutput* _atlas_graphics_output; + + typedef pvector UpdateQueue; + UpdateQueue _queued_updates; +}; + +#include "shadow_manager.I" + +#endif // RP_SHADOW_MANAGER_H diff --git a/RenderPipelineFile/rpcore/native/source/shadow_source.I b/RenderPipelineFile/rpcore/native/source/shadow_source.I index 2c898ff6..b1a43420 100644 --- a/RenderPipelineFile/rpcore/native/source/shadow_source.I +++ b/RenderPipelineFile/rpcore/native/source/shadow_source.I @@ -1,261 +1,261 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - - -/** - * @brief Returns whether the shadow source needs an update. - * @details This returns the update flag, which was previously set with - * ShadowSource::set_needs_update. If the value is true, it means that the - * ShadowSource is invalid and should be regenerated. This can either be the - * case because the scene changed and affected the shadow source, or the light - * moved. - * @return Update-Flag - */ -inline bool ShadowSource::get_needs_update() const { - return !has_region() || _needs_update; -} - -/** - * @brief Returns the slot of the shadow source. - * @details This returns the assigned slot of the ShadowSource, or -1 if no slot - * was assigned yet. You can check if a slot exists with ShadowSource::has_slot. - * The slot is the index of the ShadowSource in the global source buffer. - * @return Slot, or -1 to indicate no slot. - */ -inline int ShadowSource::get_slot() const { - return _slot; -} - -/** - * @brief Returns whether the source has a slot. - * @details This returns whether the ShadowSource currently has an assigned slot. - * If the source has a slot assigned, this returns true, otherwise false. Cases - * where the source has no slot might be when the source just got attached, but - * never got rendered yet. - * @return [description] - */ -inline bool ShadowSource::has_slot() const { - return _slot >= 0; -} - -/** - * @brief Assigns the source a slot - * @details This assigns a slot to the ShadowSource. This is called from the - * ShadowManager, when the source gets attached first time. This should not - * get called by the user. - * - * @param slot Slot of the source, or -1 to indicate no slot. - */ -inline void ShadowSource::set_slot(int slot) { - _slot = slot; -} - -/** - * @brief Setups a perspective lens for the source. - * @details This makes the shadow source behave like a perspective lens. The - * parameters are similar to the ones of a PerspectiveLens. - * - * @param fov FoV of the lens - * @param near_plane The near plane of the lens, to avoid artifacts at low distance - * @param far_plane The far plane of the lens - * @param pos Position of the lens, in world space - * @param direction Direction (Orientation) of the lens - */ -inline void ShadowSource::set_perspective_lens(float fov, float near_plane, - float far_plane, LVecBase3f pos, - LVecBase3f direction) { - // Construct the transformation matrix - LMatrix4f transform_mat = LMatrix4f::translate_mat(-pos); - - // Construct a temporary lens to generate the lens matrix - PerspectiveLens temp_lens = PerspectiveLens(fov, fov); - temp_lens.set_film_offset(0, 0); - temp_lens.set_near_far(near_plane, far_plane); - temp_lens.set_view_vector(direction, LVector3::up()); - set_matrix_lens(transform_mat * temp_lens.get_projection_mat()); - - // Set new bounds, approximate with sphere - CPT(BoundingHexahedron) hexahedron = DCAST(BoundingHexahedron, temp_lens.make_bounds()); - LPoint3 center = (hexahedron->get_min() + hexahedron->get_max()) * 0.5f; - _bounds = BoundingSphere(pos + center, (hexahedron->get_max() - center).length()); -} - -/** - * @brief Sets a custom matrix for the source. - * @details This tells the source to use a custom matrix for rendering, just like - * the matrix lens. The matrix should include all transformations, rotations and - * scales. No other matrices will be used for rendering this shadow source (not - * even a coordinate system conversion). - * - * @param mvp Custom View-Projection matrix - */ -inline void ShadowSource::set_matrix_lens(const LMatrix4f& mvp) { - _mvp = mvp; - set_needs_update(true); -} - -/** - * @brief Sets the update flag of the source. - * @details Sets whether the source is still valid, or needs to get regenerated. - * Usually you only want to flag the shadow source as invalid, by passing - * true as the flag. However, the ShadowManager will set the flag to false - * after updating the source. - * - * @param flag The update flag - */ -inline void ShadowSource::set_needs_update(bool flag) { - _needs_update = flag; -} - -/** - * @brief Returns whether the source has a valid region. - * @details This returns whether the ShadowSource has a valid shadow atlas region - * assigned. This might be not the case when the source never was rendered yet, - * or is about to get updated. - * @return true if the source has a valid region, else false. - */ -inline bool ShadowSource::has_region() const { - return _region.get_x() >= 0 && _region.get_y() >= 0 && _region.get_z() >= 0 && _region.get_w() >= 0; -} - -/** - * @brief Returns the resolution of the source. - * @details Returns the shadow map resolution of source, in pixels. This is the - * space the source takes in the shadow atlas, in pixels. - * @return Resolution in pixels - */ -inline size_t ShadowSource::get_resolution() const { - return _resolution; -} - -/** - * @brief Returns the assigned region of the source in atlas space. - * @details This returns the region of the source, in atlas space. This is the - * region set by ShadowSource::set_region. If no region was set yet, returns - * a 4-component integer vector with all components set to -1. To check this, - * you should call ShadowSource::has_region() first. - * - * @return [description] - */ -inline const LVecBase4i& ShadowSource::get_region() const { - return _region; -} - -/** - * @brief Returns the assigned region of the source in UV space. - * @details This returns the region of the source, in UV space. This is the - * region set by ShadowSource::set_region. If no region was set yet, returns - * a 4-component integer vector with all components set to -1. To check this, - * you should call ShadowSource::has_region() first. - * - * @return [description] - */ -inline const LVecBase4f& ShadowSource::get_uv_region() const { - return _region_uv; -} - -/** - * @brief Sets the assigned region of the source in atlas and uv space. - * @details This sets the assigned region of the ShadowSource. The region in - * atlas space should be the region returned from the - * ShadowAtlas::find_and_reserve_region. The uv-region should be the same region, - * but in the 0 .. 1 range (can be converted with ShadowAtlas::region_to_uv). - * This is required for the shaders, because they expect coordinates in the - * 0 .. 1 range for sampling. - * - * @param region Atlas-Space region - * @param region_uv UV-Space region - */ -inline void ShadowSource::set_region(const LVecBase4i& region, const LVecBase4f& region_uv) { - _region = region; - _region_uv = region_uv; -} - -/** - * @brief Returns the View-Projection matrix of the source. - * @details This returns the current view-projection matrix of the ShadowSource. - * If no matrix was set yet, returns a matrix with all components zero. - * If a matrix was set with ShadowSource::set_matrix_lens, returns the matrix - * set by that function call. - * - * If a matrix was set with ShadowSource::set_perspective_lens, returns a - * perspective view-projection matrix setup by those parameters. - * - * The matrix returned is the matrix used for rendering the shadow map, and - * includes the camera transform as well as the projection matrix. - * - * @return View-Projection matrix. - */ -inline const LMatrix4f& ShadowSource::get_mvp() const { - return _mvp; -} - -/** - * @brief Writes the source to a GPUCommand. - * @details This writes the ShadowSource to a GPUCommand. This stores the - * mvp and the uv-region in the command. - * - * @param cmd GPUCommand to write to. - */ -inline void ShadowSource::write_to_command(GPUCommand &cmd) const { - // When storing on the gpu, we should already have a valid slot - nassertv(_slot >= 0); - cmd.push_mat4(_mvp); - cmd.push_vec4(_region_uv); -} - -/** - * @brief Sets the resolution of the source. - * @details This sets the resolution of the ShadowSource, in pixels. It should be - * a multiple of the tile size of the ShadowAtlas, and greater than zero. - * - * @param resolution [description] - */ -inline void ShadowSource::set_resolution(size_t resolution) { - nassertv(resolution > 0); - _resolution = resolution; - set_needs_update(true); -} - -/** - * @brief Returns the shadow sources bounds - * @details This returns the bounds of the shadow source, approximated as a sphere - * @return Bounds as a BoundingSphere - */ -inline const BoundingSphere& ShadowSource::get_bounds() const { - return _bounds; -} - -/** - * @brief Clears the assigned region of the source - * @details This unassigns any shadow atlas region from the source, previously - * set with set_region - */ -inline void ShadowSource::clear_region() { - _region.fill(-1); - _region_uv.fill(0); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + + +/** + * @brief Returns whether the shadow source needs an update. + * @details This returns the update flag, which was previously set with + * ShadowSource::set_needs_update. If the value is true, it means that the + * ShadowSource is invalid and should be regenerated. This can either be the + * case because the scene changed and affected the shadow source, or the light + * moved. + * @return Update-Flag + */ +inline bool ShadowSource::get_needs_update() const { + return !has_region() || _needs_update; +} + +/** + * @brief Returns the slot of the shadow source. + * @details This returns the assigned slot of the ShadowSource, or -1 if no slot + * was assigned yet. You can check if a slot exists with ShadowSource::has_slot. + * The slot is the index of the ShadowSource in the global source buffer. + * @return Slot, or -1 to indicate no slot. + */ +inline int ShadowSource::get_slot() const { + return _slot; +} + +/** + * @brief Returns whether the source has a slot. + * @details This returns whether the ShadowSource currently has an assigned slot. + * If the source has a slot assigned, this returns true, otherwise false. Cases + * where the source has no slot might be when the source just got attached, but + * never got rendered yet. + * @return [description] + */ +inline bool ShadowSource::has_slot() const { + return _slot >= 0; +} + +/** + * @brief Assigns the source a slot + * @details This assigns a slot to the ShadowSource. This is called from the + * ShadowManager, when the source gets attached first time. This should not + * get called by the user. + * + * @param slot Slot of the source, or -1 to indicate no slot. + */ +inline void ShadowSource::set_slot(int slot) { + _slot = slot; +} + +/** + * @brief Setups a perspective lens for the source. + * @details This makes the shadow source behave like a perspective lens. The + * parameters are similar to the ones of a PerspectiveLens. + * + * @param fov FoV of the lens + * @param near_plane The near plane of the lens, to avoid artifacts at low distance + * @param far_plane The far plane of the lens + * @param pos Position of the lens, in world space + * @param direction Direction (Orientation) of the lens + */ +inline void ShadowSource::set_perspective_lens(float fov, float near_plane, + float far_plane, LVecBase3f pos, + LVecBase3f direction) { + // Construct the transformation matrix + LMatrix4f transform_mat = LMatrix4f::translate_mat(-pos); + + // Construct a temporary lens to generate the lens matrix + PerspectiveLens temp_lens = PerspectiveLens(fov, fov); + temp_lens.set_film_offset(0, 0); + temp_lens.set_near_far(near_plane, far_plane); + temp_lens.set_view_vector(direction, LVector3::up()); + set_matrix_lens(transform_mat * temp_lens.get_projection_mat()); + + // Set new bounds, approximate with sphere + CPT(BoundingHexahedron) hexahedron = DCAST(BoundingHexahedron, temp_lens.make_bounds()); + LPoint3 center = (hexahedron->get_min() + hexahedron->get_max()) * 0.5f; + _bounds = BoundingSphere(pos + center, (hexahedron->get_max() - center).length()); +} + +/** + * @brief Sets a custom matrix for the source. + * @details This tells the source to use a custom matrix for rendering, just like + * the matrix lens. The matrix should include all transformations, rotations and + * scales. No other matrices will be used for rendering this shadow source (not + * even a coordinate system conversion). + * + * @param mvp Custom View-Projection matrix + */ +inline void ShadowSource::set_matrix_lens(const LMatrix4f& mvp) { + _mvp = mvp; + set_needs_update(true); +} + +/** + * @brief Sets the update flag of the source. + * @details Sets whether the source is still valid, or needs to get regenerated. + * Usually you only want to flag the shadow source as invalid, by passing + * true as the flag. However, the ShadowManager will set the flag to false + * after updating the source. + * + * @param flag The update flag + */ +inline void ShadowSource::set_needs_update(bool flag) { + _needs_update = flag; +} + +/** + * @brief Returns whether the source has a valid region. + * @details This returns whether the ShadowSource has a valid shadow atlas region + * assigned. This might be not the case when the source never was rendered yet, + * or is about to get updated. + * @return true if the source has a valid region, else false. + */ +inline bool ShadowSource::has_region() const { + return _region.get_x() >= 0 && _region.get_y() >= 0 && _region.get_z() >= 0 && _region.get_w() >= 0; +} + +/** + * @brief Returns the resolution of the source. + * @details Returns the shadow map resolution of source, in pixels. This is the + * space the source takes in the shadow atlas, in pixels. + * @return Resolution in pixels + */ +inline size_t ShadowSource::get_resolution() const { + return _resolution; +} + +/** + * @brief Returns the assigned region of the source in atlas space. + * @details This returns the region of the source, in atlas space. This is the + * region set by ShadowSource::set_region. If no region was set yet, returns + * a 4-component integer vector with all components set to -1. To check this, + * you should call ShadowSource::has_region() first. + * + * @return [description] + */ +inline const LVecBase4i& ShadowSource::get_region() const { + return _region; +} + +/** + * @brief Returns the assigned region of the source in UV space. + * @details This returns the region of the source, in UV space. This is the + * region set by ShadowSource::set_region. If no region was set yet, returns + * a 4-component integer vector with all components set to -1. To check this, + * you should call ShadowSource::has_region() first. + * + * @return [description] + */ +inline const LVecBase4f& ShadowSource::get_uv_region() const { + return _region_uv; +} + +/** + * @brief Sets the assigned region of the source in atlas and uv space. + * @details This sets the assigned region of the ShadowSource. The region in + * atlas space should be the region returned from the + * ShadowAtlas::find_and_reserve_region. The uv-region should be the same region, + * but in the 0 .. 1 range (can be converted with ShadowAtlas::region_to_uv). + * This is required for the shaders, because they expect coordinates in the + * 0 .. 1 range for sampling. + * + * @param region Atlas-Space region + * @param region_uv UV-Space region + */ +inline void ShadowSource::set_region(const LVecBase4i& region, const LVecBase4f& region_uv) { + _region = region; + _region_uv = region_uv; +} + +/** + * @brief Returns the View-Projection matrix of the source. + * @details This returns the current view-projection matrix of the ShadowSource. + * If no matrix was set yet, returns a matrix with all components zero. + * If a matrix was set with ShadowSource::set_matrix_lens, returns the matrix + * set by that function call. + * + * If a matrix was set with ShadowSource::set_perspective_lens, returns a + * perspective view-projection matrix setup by those parameters. + * + * The matrix returned is the matrix used for rendering the shadow map, and + * includes the camera transform as well as the projection matrix. + * + * @return View-Projection matrix. + */ +inline const LMatrix4f& ShadowSource::get_mvp() const { + return _mvp; +} + +/** + * @brief Writes the source to a GPUCommand. + * @details This writes the ShadowSource to a GPUCommand. This stores the + * mvp and the uv-region in the command. + * + * @param cmd GPUCommand to write to. + */ +inline void ShadowSource::write_to_command(GPUCommand &cmd) const { + // When storing on the gpu, we should already have a valid slot + nassertv(_slot >= 0); + cmd.push_mat4(_mvp); + cmd.push_vec4(_region_uv); +} + +/** + * @brief Sets the resolution of the source. + * @details This sets the resolution of the ShadowSource, in pixels. It should be + * a multiple of the tile size of the ShadowAtlas, and greater than zero. + * + * @param resolution [description] + */ +inline void ShadowSource::set_resolution(size_t resolution) { + nassertv(resolution > 0); + _resolution = resolution; + set_needs_update(true); +} + +/** + * @brief Returns the shadow sources bounds + * @details This returns the bounds of the shadow source, approximated as a sphere + * @return Bounds as a BoundingSphere + */ +inline const BoundingSphere& ShadowSource::get_bounds() const { + return _bounds; +} + +/** + * @brief Clears the assigned region of the source + * @details This unassigns any shadow atlas region from the source, previously + * set with set_region + */ +inline void ShadowSource::clear_region() { + _region.fill(-1); + _region_uv.fill(0); +} diff --git a/RenderPipelineFile/rpcore/native/source/shadow_source.cpp b/RenderPipelineFile/rpcore/native/source/shadow_source.cpp index 335d9287..1b84e86b 100644 --- a/RenderPipelineFile/rpcore/native/source/shadow_source.cpp +++ b/RenderPipelineFile/rpcore/native/source/shadow_source.cpp @@ -1,41 +1,41 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#include "shadow_source.h" - -/** - * @brief Constructs a new shadow source - * @details This constructs a new shadow source, with no projection setup, - * and no slot assigned. - */ -ShadowSource::ShadowSource() { - _slot = -1; - _needs_update = true; - _resolution = 512; - _mvp.fill(0.0); - _region.fill(-1); - _region_uv.fill(0); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include "shadow_source.h" + +/** + * @brief Constructs a new shadow source + * @details This constructs a new shadow source, with no projection setup, + * and no slot assigned. + */ +ShadowSource::ShadowSource() { + _slot = -1; + _needs_update = true; + _resolution = 512; + _mvp.fill(0.0); + _region.fill(-1); + _region_uv.fill(0); +} diff --git a/RenderPipelineFile/rpcore/native/source/shadow_source.h b/RenderPipelineFile/rpcore/native/source/shadow_source.h index 4b9c4cf5..eaae8242 100644 --- a/RenderPipelineFile/rpcore/native/source/shadow_source.h +++ b/RenderPipelineFile/rpcore/native/source/shadow_source.h @@ -1,93 +1,93 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -#ifndef RP_SHADOW_SOURCE_H -#define RP_SHADOW_SOURCE_H - -#include "pandabase.h" -#include "luse.h" -#include "transformState.h" -#include "look_at.h" -#include "compose_matrix.h" -#include "perspectiveLens.h" -#include "boundingVolume.h" -#include "boundingSphere.h" -#include "boundingHexahedron.h" -#include "geometricBoundingVolume.h" - -#include "gpu_command.h" - -/** - * @brief This class represents a single shadow source. - * @details The ShadowSource can be seen as a Camera. It is used by the Lights - * to render their shadows. Each ShadowSource has a position in the atlas, - * and a view-projection matrix. The shadow manager regenerates the shadow maps - * using the data from the shadow sources. - */ -class ShadowSource { - -public: - ShadowSource(); - - inline void write_to_command(GPUCommand &cmd) const; - - inline void set_needs_update(bool flag); - inline void set_slot(int slot); - inline void set_region(const LVecBase4i& region, const LVecBase4f& region_uv); - inline void set_resolution(size_t resolution); - inline void set_perspective_lens(float fov, float near_plane, - float far_plane, LVecBase3f pos, LVecBase3f direction); - inline void set_matrix_lens(const LMatrix4f& mvp); - - inline bool has_region() const; - inline bool has_slot() const; - - inline void clear_region(); - - inline int get_slot() const; - inline bool get_needs_update() const; - inline size_t get_resolution() const; - inline const LMatrix4f& get_mvp() const; - inline const LVecBase4i& get_region() const; - inline const LVecBase4f& get_uv_region() const; - - inline const BoundingSphere& get_bounds() const; - -private: - int _slot; - bool _needs_update; - size_t _resolution; - LMatrix4f _mvp; - LVecBase4i _region; - LVecBase4f _region_uv; - - BoundingSphere _bounds; -}; - -#include "shadow_source.I" - -#endif // RP_SHADOW_SOURCE_H +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +#ifndef RP_SHADOW_SOURCE_H +#define RP_SHADOW_SOURCE_H + +#include "pandabase.h" +#include "luse.h" +#include "transformState.h" +#include "look_at.h" +#include "compose_matrix.h" +#include "perspectiveLens.h" +#include "boundingVolume.h" +#include "boundingSphere.h" +#include "boundingHexahedron.h" +#include "geometricBoundingVolume.h" + +#include "gpu_command.h" + +/** + * @brief This class represents a single shadow source. + * @details The ShadowSource can be seen as a Camera. It is used by the Lights + * to render their shadows. Each ShadowSource has a position in the atlas, + * and a view-projection matrix. The shadow manager regenerates the shadow maps + * using the data from the shadow sources. + */ +class ShadowSource { + +public: + ShadowSource(); + + inline void write_to_command(GPUCommand &cmd) const; + + inline void set_needs_update(bool flag); + inline void set_slot(int slot); + inline void set_region(const LVecBase4i& region, const LVecBase4f& region_uv); + inline void set_resolution(size_t resolution); + inline void set_perspective_lens(float fov, float near_plane, + float far_plane, LVecBase3f pos, LVecBase3f direction); + inline void set_matrix_lens(const LMatrix4f& mvp); + + inline bool has_region() const; + inline bool has_slot() const; + + inline void clear_region(); + + inline int get_slot() const; + inline bool get_needs_update() const; + inline size_t get_resolution() const; + inline const LMatrix4f& get_mvp() const; + inline const LVecBase4i& get_region() const; + inline const LVecBase4f& get_uv_region() const; + + inline const BoundingSphere& get_bounds() const; + +private: + int _slot; + bool _needs_update; + size_t _resolution; + LMatrix4f _mvp; + LVecBase4i _region; + LVecBase4f _region_uv; + + BoundingSphere _bounds; +}; + +#include "shadow_source.I" + +#endif // RP_SHADOW_SOURCE_H diff --git a/RenderPipelineFile/rpcore/native/source/tag_state_manager.I b/RenderPipelineFile/rpcore/native/source/tag_state_manager.I index d448b6d3..c2127abc 100644 --- a/RenderPipelineFile/rpcore/native/source/tag_state_manager.I +++ b/RenderPipelineFile/rpcore/native/source/tag_state_manager.I @@ -1,88 +1,88 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -/** - * @brief Registers a new camera which renders a certain pass - * @details This registers a new camera which will be used to render the given - * pass. The TagStateManager will keep track of the camera and - * applies all registered states onto the camera with Camera::set_tag_state. - * It also applies the appropriate camera mask to the camera, - * and sets an initial state to disable color write depending on the pass. - * - * @param source Camera which will be used to render shadows - */ -inline void TagStateManager::register_camera(const string& name, Camera* source) { - ContainerList::iterator entry = _containers.find(name); - nassertv(entry != _containers.end()); - register_camera(entry->second, source); -} - -/** - * @brief Unregisters a camera from the list of shadow cameras - * @details This unregisters a camera from the list of shadows cameras. It also - * resets all tag states of the camera, and also its initial state. - * - * @param source Camera to unregister - */ -inline void TagStateManager::unregister_camera(const string& name, Camera* source) { - ContainerList::iterator entry = _containers.find(name); - nassertv(entry != _containers.end()); - unregister_camera(entry->second, source); -} - -/** - * @brief Applies a given state for a pass to a NodePath - * @details This applies a shader to the given NodePath which is used when the - * NodePath is rendered by any registered camera for that pass. - * It also disables color write depending on the pass. - * - * @param np The nodepath to apply the shader to - * @param shader A handle to the shader to apply - * @param name Name of the state, should be a unique identifier - * @param sort Determines the sort with which the shader will be applied. - */ -inline void TagStateManager::apply_state(const string& state, NodePath np, Shader* shader, - const string &name, int sort) { - ContainerList::iterator entry = _containers.find(state); - nassertv(entry != _containers.end()); - apply_state(entry->second, np, shader, name, sort); -} - -/** - * @brief Returns the render mask for the given state - * @details This returns the mask of a given render pass, which can be used - * to either show or hide objects from this pass. - * - * @param container_name Name of the render-pass - * @return Bit mask of the render pass - */ -inline BitMask32 TagStateManager::get_mask(const string &container_name) { - if (container_name == "gbuffer") - return BitMask32::bit(1); - ContainerList::iterator entry = _containers.find(container_name); - nassertr(entry != _containers.end(), BitMask32()); - return entry->second.mask; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +/** + * @brief Registers a new camera which renders a certain pass + * @details This registers a new camera which will be used to render the given + * pass. The TagStateManager will keep track of the camera and + * applies all registered states onto the camera with Camera::set_tag_state. + * It also applies the appropriate camera mask to the camera, + * and sets an initial state to disable color write depending on the pass. + * + * @param source Camera which will be used to render shadows + */ +inline void TagStateManager::register_camera(const string& name, Camera* source) { + ContainerList::iterator entry = _containers.find(name); + nassertv(entry != _containers.end()); + register_camera(entry->second, source); +} + +/** + * @brief Unregisters a camera from the list of shadow cameras + * @details This unregisters a camera from the list of shadows cameras. It also + * resets all tag states of the camera, and also its initial state. + * + * @param source Camera to unregister + */ +inline void TagStateManager::unregister_camera(const string& name, Camera* source) { + ContainerList::iterator entry = _containers.find(name); + nassertv(entry != _containers.end()); + unregister_camera(entry->second, source); +} + +/** + * @brief Applies a given state for a pass to a NodePath + * @details This applies a shader to the given NodePath which is used when the + * NodePath is rendered by any registered camera for that pass. + * It also disables color write depending on the pass. + * + * @param np The nodepath to apply the shader to + * @param shader A handle to the shader to apply + * @param name Name of the state, should be a unique identifier + * @param sort Determines the sort with which the shader will be applied. + */ +inline void TagStateManager::apply_state(const string& state, NodePath np, Shader* shader, + const string &name, int sort) { + ContainerList::iterator entry = _containers.find(state); + nassertv(entry != _containers.end()); + apply_state(entry->second, np, shader, name, sort); +} + +/** + * @brief Returns the render mask for the given state + * @details This returns the mask of a given render pass, which can be used + * to either show or hide objects from this pass. + * + * @param container_name Name of the render-pass + * @return Bit mask of the render pass + */ +inline BitMask32 TagStateManager::get_mask(const string &container_name) { + if (container_name == "gbuffer") + return BitMask32::bit(1); + ContainerList::iterator entry = _containers.find(container_name); + nassertr(entry != _containers.end(), BitMask32()); + return entry->second.mask; +} diff --git a/RenderPipelineFile/rpcore/native/source/tag_state_manager.cpp b/RenderPipelineFile/rpcore/native/source/tag_state_manager.cpp index 296a632b..f31a7070 100644 --- a/RenderPipelineFile/rpcore/native/source/tag_state_manager.cpp +++ b/RenderPipelineFile/rpcore/native/source/tag_state_manager.cpp @@ -1,195 +1,195 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -#include "tag_state_manager.h" - - -NotifyCategoryDef(tagstatemgr, ""); - -/** - * @brief Constructs a new TagStateManager - * @details This constructs a new TagStateManager. The #main_cam_node should - * refer to the main scene camera, and will most likely be base.cam. - * It is necessary to pass the camera because the C++ code does not have - * access to the showbase. - * - * @param main_cam_node The main scene camera - */ -TagStateManager::TagStateManager(NodePath main_cam_node) { - nassertv(!main_cam_node.is_empty()); - nassertv(DCAST(Camera, main_cam_node.node()) != NULL); - _main_cam_node = main_cam_node; - - // Set default camera mask - DCAST(Camera, _main_cam_node.node())->set_camera_mask(BitMask32::bit(1)); - - // Init containers - _containers["shadow"] = StateContainer("Shadows", 2, false); - _containers["voxelize"] = StateContainer("Voxelize", 3, false); - _containers["envmap"] = StateContainer("Envmap", 4, true); - _containers["forward"] = StateContainer("Forward", 5, true); -} - -/** - * @brief Destructs the TagStateManager - * @details This destructs the TagStateManager, and cleans up all resources used. - */ -TagStateManager::~TagStateManager() { - cleanup_states(); -} - -/** - * @brief Applies a given state to a NodePath - * @details This applies a shader to the given NodePath which is used when the - * NodePath is rendered by any registered camera of the container. - * - * @param container The container which is used to store the state - * @param np The nodepath to apply the shader to - * @param shader A handle to the shader to apply - * @param name Name of the state, should be a unique identifier - * @param sort Changes the sort with which the shader will be applied. - */ -void TagStateManager::apply_state(StateContainer& container, NodePath np, Shader* shader, - const string &name, int sort) { - if (tagstatemgr_cat.is_spam()) { - tagstatemgr_cat.spam() << "Constructing new state " << name - << " with shader " << shader << endl; - } - - // Construct the render state - CPT(RenderState) state = RenderState::make_empty(); - - // Disable color write for all stages except the environment container - if (!container.write_color) { - state = state->set_attrib(ColorWriteAttrib::make(ColorWriteAttrib::C_off), 10000); - } - state = state->set_attrib(ShaderAttrib::make(shader, sort), sort); - - // Emit a warning if we override an existing state - if (container.tag_states.count(name) != 0) { - tagstatemgr_cat.warning() << "Overriding existing state " << name << endl; - } - - // Store the state, this is required whenever we attach a new camera, so - // it can also track the existing states - container.tag_states[name] = state; - - // Save the tag on the node path - np.set_tag(container.tag_name, name); - - // Apply the state on all cameras which are attached so far - for (size_t i = 0; i < container.cameras.size(); ++i) { - container.cameras[i]->set_tag_state(name, state); - } -} - -/** - * @brief Cleans up all registered states. - * @details This cleans up all states which were registered to the TagStateManager. - * It also calls Camera::clear_tag_states() on the main_cam_node and all attached - * cameras. - */ -void TagStateManager::cleanup_states() { - if (tagstatemgr_cat.is_info()) { - tagstatemgr_cat.info() << "cleaning up states" << endl; - } - - // Clear all tag states of the main camera - DCAST(Camera, _main_cam_node.node())->clear_tag_states(); - - // Clear the containers - // XXX: Just iterate over the _container map - cleanup_container_states(_containers["shadow"]); - cleanup_container_states(_containers["voxelize"]); - cleanup_container_states(_containers["envmap"]); - cleanup_container_states(_containers["forward"]); -} - -/** - * @brief Cleans up the states of a given container - * @details This cleans all tag states of the given container, - * and also calls Camera::clear_tag_states on every assigned camera. - * - * @param container Container to clear - */ -void TagStateManager::cleanup_container_states(StateContainer& container) { - for (size_t i = 0; i < container.cameras.size(); ++i) { - container.cameras[i]->clear_tag_states(); - } - container.tag_states.clear(); -} - -/** - * @brief Registers a new camera to a given container - * @details This registers a new camera to a container, and sets its initial - * state as well as the camera mask. - * - * @param container The container to add the camera to - * @param source The camera to add - */ -void TagStateManager::register_camera(StateContainer& container, Camera* source) { - source->set_tag_state_key(container.tag_name); - source->set_camera_mask(container.mask); - - // Construct an initial state which also disables color write, additionally - // to the ColorWriteAttrib on each unique state. - CPT(RenderState) state = RenderState::make_empty(); - - if (!container.write_color) { - state = state->set_attrib(ColorWriteAttrib::make(ColorWriteAttrib::C_off), 10000); - } - source->set_initial_state(state); - - // Store the camera so we can keep track of it - container.cameras.push_back(source); -} - -/** - * @brief Unregisters a camera from a container - * @details This unregisters a camera from the list of cameras of a given - * container. It also resets all tag states of the camera, and also its initial - * state. - * - * @param source Camera to unregister - */ -void TagStateManager::unregister_camera(StateContainer& container, Camera* source) { - CameraList& cameras = container.cameras; - - // Make sure the camera was attached so far - if (std::find(cameras.begin(), cameras.end(), source) == cameras.end()) { - tagstatemgr_cat.error() - << "Called unregister_camera but camera was never registered!" << endl; - return; - } - - // Remove the camera from the list of attached cameras - cameras.erase(std::remove(cameras.begin(), cameras.end(), source), cameras.end()); - - // Reset the camera - source->clear_tag_states(); - source->set_initial_state(RenderState::make_empty()); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +#include "tag_state_manager.h" + + +NotifyCategoryDef(tagstatemgr, ""); + +/** + * @brief Constructs a new TagStateManager + * @details This constructs a new TagStateManager. The #main_cam_node should + * refer to the main scene camera, and will most likely be base.cam. + * It is necessary to pass the camera because the C++ code does not have + * access to the showbase. + * + * @param main_cam_node The main scene camera + */ +TagStateManager::TagStateManager(NodePath main_cam_node) { + nassertv(!main_cam_node.is_empty()); + nassertv(DCAST(Camera, main_cam_node.node()) != NULL); + _main_cam_node = main_cam_node; + + // Set default camera mask + DCAST(Camera, _main_cam_node.node())->set_camera_mask(BitMask32::bit(1)); + + // Init containers + _containers["shadow"] = StateContainer("Shadows", 2, false); + _containers["voxelize"] = StateContainer("Voxelize", 3, false); + _containers["envmap"] = StateContainer("Envmap", 4, true); + _containers["forward"] = StateContainer("Forward", 5, true); +} + +/** + * @brief Destructs the TagStateManager + * @details This destructs the TagStateManager, and cleans up all resources used. + */ +TagStateManager::~TagStateManager() { + cleanup_states(); +} + +/** + * @brief Applies a given state to a NodePath + * @details This applies a shader to the given NodePath which is used when the + * NodePath is rendered by any registered camera of the container. + * + * @param container The container which is used to store the state + * @param np The nodepath to apply the shader to + * @param shader A handle to the shader to apply + * @param name Name of the state, should be a unique identifier + * @param sort Changes the sort with which the shader will be applied. + */ +void TagStateManager::apply_state(StateContainer& container, NodePath np, Shader* shader, + const string &name, int sort) { + if (tagstatemgr_cat.is_spam()) { + tagstatemgr_cat.spam() << "Constructing new state " << name + << " with shader " << shader << endl; + } + + // Construct the render state + CPT(RenderState) state = RenderState::make_empty(); + + // Disable color write for all stages except the environment container + if (!container.write_color) { + state = state->set_attrib(ColorWriteAttrib::make(ColorWriteAttrib::C_off), 10000); + } + state = state->set_attrib(ShaderAttrib::make(shader, sort), sort); + + // Emit a warning if we override an existing state + if (container.tag_states.count(name) != 0) { + tagstatemgr_cat.warning() << "Overriding existing state " << name << endl; + } + + // Store the state, this is required whenever we attach a new camera, so + // it can also track the existing states + container.tag_states[name] = state; + + // Save the tag on the node path + np.set_tag(container.tag_name, name); + + // Apply the state on all cameras which are attached so far + for (size_t i = 0; i < container.cameras.size(); ++i) { + container.cameras[i]->set_tag_state(name, state); + } +} + +/** + * @brief Cleans up all registered states. + * @details This cleans up all states which were registered to the TagStateManager. + * It also calls Camera::clear_tag_states() on the main_cam_node and all attached + * cameras. + */ +void TagStateManager::cleanup_states() { + if (tagstatemgr_cat.is_info()) { + tagstatemgr_cat.info() << "cleaning up states" << endl; + } + + // Clear all tag states of the main camera + DCAST(Camera, _main_cam_node.node())->clear_tag_states(); + + // Clear the containers + // XXX: Just iterate over the _container map + cleanup_container_states(_containers["shadow"]); + cleanup_container_states(_containers["voxelize"]); + cleanup_container_states(_containers["envmap"]); + cleanup_container_states(_containers["forward"]); +} + +/** + * @brief Cleans up the states of a given container + * @details This cleans all tag states of the given container, + * and also calls Camera::clear_tag_states on every assigned camera. + * + * @param container Container to clear + */ +void TagStateManager::cleanup_container_states(StateContainer& container) { + for (size_t i = 0; i < container.cameras.size(); ++i) { + container.cameras[i]->clear_tag_states(); + } + container.tag_states.clear(); +} + +/** + * @brief Registers a new camera to a given container + * @details This registers a new camera to a container, and sets its initial + * state as well as the camera mask. + * + * @param container The container to add the camera to + * @param source The camera to add + */ +void TagStateManager::register_camera(StateContainer& container, Camera* source) { + source->set_tag_state_key(container.tag_name); + source->set_camera_mask(container.mask); + + // Construct an initial state which also disables color write, additionally + // to the ColorWriteAttrib on each unique state. + CPT(RenderState) state = RenderState::make_empty(); + + if (!container.write_color) { + state = state->set_attrib(ColorWriteAttrib::make(ColorWriteAttrib::C_off), 10000); + } + source->set_initial_state(state); + + // Store the camera so we can keep track of it + container.cameras.push_back(source); +} + +/** + * @brief Unregisters a camera from a container + * @details This unregisters a camera from the list of cameras of a given + * container. It also resets all tag states of the camera, and also its initial + * state. + * + * @param source Camera to unregister + */ +void TagStateManager::unregister_camera(StateContainer& container, Camera* source) { + CameraList& cameras = container.cameras; + + // Make sure the camera was attached so far + if (std::find(cameras.begin(), cameras.end(), source) == cameras.end()) { + tagstatemgr_cat.error() + << "Called unregister_camera but camera was never registered!" << endl; + return; + } + + // Remove the camera from the list of attached cameras + cameras.erase(std::remove(cameras.begin(), cameras.end(), source), cameras.end()); + + // Reset the camera + source->clear_tag_states(); + source->set_initial_state(RenderState::make_empty()); +} diff --git a/RenderPipelineFile/rpcore/native/source/tag_state_manager.h b/RenderPipelineFile/rpcore/native/source/tag_state_manager.h index 4b9f75dd..81b6b68e 100644 --- a/RenderPipelineFile/rpcore/native/source/tag_state_manager.h +++ b/RenderPipelineFile/rpcore/native/source/tag_state_manager.h @@ -1,95 +1,95 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#ifndef TAG_STATE_MANAGER_H -#define TAG_STATE_MANAGER_H - -#include "pandabase.h" -#include "bitMask.h" -#include "camera.h" -#include "nodePath.h" -#include "shader.h" -#include "renderState.h" -#include "shaderAttrib.h" -#include "colorWriteAttrib.h" - -NotifyCategoryDecl(tagstatemgr, EXPORT_CLASS, EXPORT_TEMPL); - -/** - * @brief This class handles all different tag states - * @details The TagStateManager stores a list of RenderStates assigned to different - * steps in the pipeline. For example, there are a list of shadow states, which - * are applied whenever objects are rendered from a shadow camera. - * - * The Manager also stores a list of all cameras used in the different stages, - * to keep track of the states used and to be able to attach new states. - */ -class TagStateManager { - - PUBLISHED: - TagStateManager(NodePath main_cam_node); - ~TagStateManager(); - - inline void apply_state(const string& state, NodePath np, Shader* shader, const string &name, int sort); - void cleanup_states(); - - inline void register_camera(const string& state, Camera* source); - inline void unregister_camera(const string& state, Camera* source); - inline BitMask32 get_mask(const string &container_name); - - private: - - typedef vector CameraList; - typedef pmap TagStateList; - - struct StateContainer { - CameraList cameras; - TagStateList tag_states; - string tag_name; - BitMask32 mask; - bool write_color; - - StateContainer() {}; - StateContainer(const string &tag_name, size_t mask, bool write_color) - : tag_name(tag_name), mask(BitMask32::bit(mask)), write_color(write_color) {}; - }; - - void apply_state(StateContainer& container, NodePath np, Shader* shader, - const string& name, int sort); - void cleanup_container_states(StateContainer& container); - void register_camera(StateContainer &container, Camera* source); - void unregister_camera(StateContainer &container, Camera* source); - - typedef pmap ContainerList; - ContainerList _containers; - - NodePath _main_cam_node; -}; - - -#include "tag_state_manager.I" - -#endif // TAG_STATE_MANAGER_H +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef TAG_STATE_MANAGER_H +#define TAG_STATE_MANAGER_H + +#include "pandabase.h" +#include "bitMask.h" +#include "camera.h" +#include "nodePath.h" +#include "shader.h" +#include "renderState.h" +#include "shaderAttrib.h" +#include "colorWriteAttrib.h" + +NotifyCategoryDecl(tagstatemgr, EXPORT_CLASS, EXPORT_TEMPL); + +/** + * @brief This class handles all different tag states + * @details The TagStateManager stores a list of RenderStates assigned to different + * steps in the pipeline. For example, there are a list of shadow states, which + * are applied whenever objects are rendered from a shadow camera. + * + * The Manager also stores a list of all cameras used in the different stages, + * to keep track of the states used and to be able to attach new states. + */ +class TagStateManager { + + PUBLISHED: + TagStateManager(NodePath main_cam_node); + ~TagStateManager(); + + inline void apply_state(const string& state, NodePath np, Shader* shader, const string &name, int sort); + void cleanup_states(); + + inline void register_camera(const string& state, Camera* source); + inline void unregister_camera(const string& state, Camera* source); + inline BitMask32 get_mask(const string &container_name); + + private: + + typedef vector CameraList; + typedef pmap TagStateList; + + struct StateContainer { + CameraList cameras; + TagStateList tag_states; + string tag_name; + BitMask32 mask; + bool write_color; + + StateContainer() {}; + StateContainer(const string &tag_name, size_t mask, bool write_color) + : tag_name(tag_name), mask(BitMask32::bit(mask)), write_color(write_color) {}; + }; + + void apply_state(StateContainer& container, NodePath np, Shader* shader, + const string& name, int sort); + void cleanup_container_states(StateContainer& container); + void register_camera(StateContainer &container, Camera* source); + void unregister_camera(StateContainer &container, Camera* source); + + typedef pmap ContainerList; + ContainerList _containers; + + NodePath _main_cam_node; +}; + + +#include "tag_state_manager.I" + +#endif // TAG_STATE_MANAGER_H diff --git a/RenderPipelineFile/rpcore/native/update_module_builder.py b/RenderPipelineFile/rpcore/native/update_module_builder.py index a4b0ee90..5c52d5a2 100644 --- a/RenderPipelineFile/rpcore/native/update_module_builder.py +++ b/RenderPipelineFile/rpcore/native/update_module_builder.py @@ -1,57 +1,57 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -# This script downloads and updates the module builder. - -# pylint: skip-file - -if __name__ == "__main__": - import os - import sys - from os.path import dirname, realpath, isfile - - # Files which are skipped from the downloaded zip - files_to_ignore = "__init__.py LICENSE README.md config.ini config_module".split() - - current_dir = dirname(realpath(__file__)) - os.chdir(current_dir) - - # Import thirdparty packages - sys.path.insert(0, "../../rpcore/util") - sys.path.insert(0, "../../") - - # Download the module - from submodule_downloader import download_submodule - download_submodule("tobspr", "P3DModuleBuilder", current_dir, files_to_ignore) - - # Make an init script so we can import the code - with open("scripts/__init__.py", "w") as handle: - handle.write("") - - # Update the gitignore using the suggested version from the module builder - if isfile(".gitignore"): - os.remove(".gitignore") - os.rename("prefab.gitignore", ".gitignore") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +# This script downloads and updates the module builder. + +# pylint: skip-file + +if __name__ == "__main__": + import os + import sys + from os.path import dirname, realpath, isfile + + # Files which are skipped from the downloaded zip + files_to_ignore = "__init__.py LICENSE README.md config.ini config_module".split() + + current_dir = dirname(realpath(__file__)) + os.chdir(current_dir) + + # Import thirdparty packages + sys.path.insert(0, "../../rpcore/util") + sys.path.insert(0, "../../") + + # Download the module + from submodule_downloader import download_submodule + download_submodule("tobspr", "P3DModuleBuilder", current_dir, files_to_ignore) + + # Make an init script so we can import the code + with open("scripts/__init__.py", "w") as handle: + handle.write("") + + # Update the gitignore using the suggested version from the module builder + if isfile(".gitignore"): + os.remove(".gitignore") + os.rename("prefab.gitignore", ".gitignore") diff --git a/RenderPipelineFile/rpcore/pluginbase/base_plugin.py b/RenderPipelineFile/rpcore/pluginbase/base_plugin.py index 3489a672..7b799def 100644 --- a/RenderPipelineFile/rpcore/pluginbase/base_plugin.py +++ b/RenderPipelineFile/rpcore/pluginbase/base_plugin.py @@ -1,101 +1,101 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from direct.stdpy.file import join - -from rpcore.rpobject import RPObject - - -class BasePlugin(RPObject): - - """ This is the base class for all plugins. All plugin classes derive from - this class. Additionally there are a lot of helpful functions provided, - such as creating render stages. """ - - # Plugins can set this to indicate they have some C++ code, so they cannot - # be used with the python version. - native_only = False - - # Plugins can set this to require other plugins - required_plugins = tuple() - - def __init__(self, pipeline): - """ Inits the plugin """ - self._pipeline = pipeline - self._assigned_stages = [] - self.plugin_id = str(self.__class__.__module__).split(".")[-2] - RPObject.__init__(self, "plugin:" + self.plugin_id) - self._set_debug_color("magenta", "bright") - - @property - def base_path(self): - """ Returns the path to the root directory of the plugin """ - return "/$$rp/rpplugins/{}/".format(self.plugin_id) - - def get_resource(self, pth): - """ Converts a local path from the plugins resource directory into - an absolute path """ - return join(self.base_path, "resources", pth) - - def get_shader_resource(self, pth): - """ Converts a local path from the plugins shader directory into - an absolute path """ - return join(self.base_path, "shader", pth) - - def create_stage(self, stage_type): - """ Shortcut to create a new render stage from a given class type. It - also registers the stage to the stage manager, and links the stage - to the current plugin instance. """ - stage_handle = stage_type(self._pipeline) - self._pipeline.stage_mgr.add_stage(stage_handle) - self._assigned_stages.append(stage_handle) - return stage_handle - - def get_setting(self, setting_id, plugin_id=None): - """ Returns the value of a setting given by its setting id. If plugin_id - is set, returns the setting of the given plugin """ - return self._pipeline.plugin_mgr.settings[plugin_id or self.plugin_id][setting_id].value - - def get_daytime_setting(self, setting_id, plugin_id=None): - """ Returns the value of a time of day setting given by its setting - id. If plugin_id is set, returns the setting of a gien plugin """ - handle = self._pipeline.plugin_mgr.day_settings[plugin_id or self.plugin_id][setting_id] - return handle.get_scaled_value_at(self._pipeline.daytime_mgr.time) - - def get_plugin_instance(self, plugin_id): - """ Returns the instance of a different plugin, given by its id. - This should be only used to access plugins which are also in the - current plugins requirements """ - return self._pipeline.plugin_mgr.instances[plugin_id] - - def is_plugin_enabled(self, plugin_id): - """ Returns whether a plugin is enabled and loaded, given is plugin id """ - return self._pipeline.plugin_mgr.is_plugin_enabled(plugin_id) - - def reload_shaders(self): - """ Reloads all shaders of the plugin """ - for stage in self._assigned_stages: - stage.reload_shaders() +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from direct.stdpy.file import join + +from rpcore.rpobject import RPObject + + +class BasePlugin(RPObject): + + """ This is the base class for all plugins. All plugin classes derive from + this class. Additionally there are a lot of helpful functions provided, + such as creating render stages. """ + + # Plugins can set this to indicate they have some C++ code, so they cannot + # be used with the python version. + native_only = False + + # Plugins can set this to require other plugins + required_plugins = tuple() + + def __init__(self, pipeline): + """ Inits the plugin """ + self._pipeline = pipeline + self._assigned_stages = [] + self.plugin_id = str(self.__class__.__module__).split(".")[-2] + RPObject.__init__(self, "plugin:" + self.plugin_id) + self._set_debug_color("magenta", "bright") + + @property + def base_path(self): + """ Returns the path to the root directory of the plugin """ + return "/$$rp/rpplugins/{}/".format(self.plugin_id) + + def get_resource(self, pth): + """ Converts a local path from the plugins resource directory into + an absolute path """ + return join(self.base_path, "resources", pth) + + def get_shader_resource(self, pth): + """ Converts a local path from the plugins shader directory into + an absolute path """ + return join(self.base_path, "shader", pth) + + def create_stage(self, stage_type): + """ Shortcut to create a new render stage from a given class type. It + also registers the stage to the stage manager, and links the stage + to the current plugin instance. """ + stage_handle = stage_type(self._pipeline) + self._pipeline.stage_mgr.add_stage(stage_handle) + self._assigned_stages.append(stage_handle) + return stage_handle + + def get_setting(self, setting_id, plugin_id=None): + """ Returns the value of a setting given by its setting id. If plugin_id + is set, returns the setting of the given plugin """ + return self._pipeline.plugin_mgr.settings[plugin_id or self.plugin_id][setting_id].value + + def get_daytime_setting(self, setting_id, plugin_id=None): + """ Returns the value of a time of day setting given by its setting + id. If plugin_id is set, returns the setting of a gien plugin """ + handle = self._pipeline.plugin_mgr.day_settings[plugin_id or self.plugin_id][setting_id] + return handle.get_scaled_value_at(self._pipeline.daytime_mgr.time) + + def get_plugin_instance(self, plugin_id): + """ Returns the instance of a different plugin, given by its id. + This should be only used to access plugins which are also in the + current plugins requirements """ + return self._pipeline.plugin_mgr.instances[plugin_id] + + def is_plugin_enabled(self, plugin_id): + """ Returns whether a plugin is enabled and loaded, given is plugin id """ + return self._pipeline.plugin_mgr.is_plugin_enabled(plugin_id) + + def reload_shaders(self): + """ Reloads all shaders of the plugin """ + for stage in self._assigned_stages: + stage.reload_shaders() diff --git a/RenderPipelineFile/rpcore/pluginbase/day_manager.py b/RenderPipelineFile/rpcore/pluginbase/day_manager.py index 681965e5..40d14f20 100644 --- a/RenderPipelineFile/rpcore/pluginbase/day_manager.py +++ b/RenderPipelineFile/rpcore/pluginbase/day_manager.py @@ -1,99 +1,99 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import division - -from rplibs.six import iteritems -from direct.stdpy.file import open - -from rpcore.rpobject import RPObject -from rpcore.util.shader_input_blocks import GroupedInputBlock -from rpcore.pluginbase.day_setting_types import ColorType - - -class DayTimeManager(RPObject): - - """ This manager handles all time of day settings, provides them as - a input to all shaders, and stores which time it currently is """ - - def __init__(self, pipeline): - RPObject.__init__(self) - self._pipeline = pipeline - self._input_ubo = GroupedInputBlock("TimeOfDay") - self._time = 0.5 - self._setting_handles = {} - - @property - def time(self): - """ Returns the current time of day as floating point number - from 0 to 1, whereas 0 means 00:00 and 1 means 24:00 (=00:00) """ - return self._time - - @time.setter - def time(self, day_time): - """ Sets the current time of day as floating point number from - 0 to 1, whereas 0 means 00:00 and 1 means 24:00 (=00:00). Any - number greater than 1 will be reduced to fit the 0 .. 1 range by - doing time modulo 1. - - Alternatively a string in the format 'hh:mm' can be passed. """ - if isinstance(day_time, float): - self._time = day_time % 1.0 - elif isinstance(day_time, str): - parts = [int(i) for i in day_time.split(":")] - self._time = (parts[0] * 60 + parts[1]) / (24 * 60) - - @property - def formatted_time(self): - """ Returns the current time as formatted string, e.g. 12:34 """ - total_minutes = int(self._time * 24 * 60) - return "{:02d}:{:02d}".format(total_minutes // 60, total_minutes % 60) - - def load_settings(self): - """ Loads all day time settings from the plugin manager and registers - them to the used input buffer """ - for plugin_id, settings in iteritems(self._pipeline.plugin_mgr.day_settings): - for setting, handle in iteritems(settings): - setting_id = "{}.{}".format(plugin_id, setting) - self._input_ubo.register_pta(setting_id, handle.glsl_type) - self._setting_handles[setting_id] = handle - self._pipeline.stage_mgr.input_blocks.append(self._input_ubo) - - # Generate UBO shader code - shader_code = self._input_ubo.generate_shader_code() - with open("/$$rptemp/$$daytime_config.inc.glsl", "w") as handle: - handle.write(shader_code) - - def update(self): - """ Internal update method which updates all day time settings """ - for setting_id, handle in iteritems(self._setting_handles): - # XXX: Find a better interface for this. Without this fix, colors - # are in the range 0 .. 255 in the shader. - if isinstance(handle, ColorType): - value = handle.get_value_at(self._time) - else: - value = handle.get_scaled_value_at(self._time) - self._input_ubo.update_input(setting_id, value) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import division + +from rplibs.six import iteritems +from direct.stdpy.file import open + +from rpcore.rpobject import RPObject +from rpcore.util.shader_input_blocks import GroupedInputBlock +from rpcore.pluginbase.day_setting_types import ColorType + + +class DayTimeManager(RPObject): + + """ This manager handles all time of day settings, provides them as + a input to all shaders, and stores which time it currently is """ + + def __init__(self, pipeline): + RPObject.__init__(self) + self._pipeline = pipeline + self._input_ubo = GroupedInputBlock("TimeOfDay") + self._time = 0.5 + self._setting_handles = {} + + @property + def time(self): + """ Returns the current time of day as floating point number + from 0 to 1, whereas 0 means 00:00 and 1 means 24:00 (=00:00) """ + return self._time + + @time.setter + def time(self, day_time): + """ Sets the current time of day as floating point number from + 0 to 1, whereas 0 means 00:00 and 1 means 24:00 (=00:00). Any + number greater than 1 will be reduced to fit the 0 .. 1 range by + doing time modulo 1. + + Alternatively a string in the format 'hh:mm' can be passed. """ + if isinstance(day_time, float): + self._time = day_time % 1.0 + elif isinstance(day_time, str): + parts = [int(i) for i in day_time.split(":")] + self._time = (parts[0] * 60 + parts[1]) / (24 * 60) + + @property + def formatted_time(self): + """ Returns the current time as formatted string, e.g. 12:34 """ + total_minutes = int(self._time * 24 * 60) + return "{:02d}:{:02d}".format(total_minutes // 60, total_minutes % 60) + + def load_settings(self): + """ Loads all day time settings from the plugin manager and registers + them to the used input buffer """ + for plugin_id, settings in iteritems(self._pipeline.plugin_mgr.day_settings): + for setting, handle in iteritems(settings): + setting_id = "{}.{}".format(plugin_id, setting) + self._input_ubo.register_pta(setting_id, handle.glsl_type) + self._setting_handles[setting_id] = handle + self._pipeline.stage_mgr.input_blocks.append(self._input_ubo) + + # Generate UBO shader code + shader_code = self._input_ubo.generate_shader_code() + with open("/$$rptemp/$$daytime_config.inc.glsl", "w") as handle: + handle.write(shader_code) + + def update(self): + """ Internal update method which updates all day time settings """ + for setting_id, handle in iteritems(self._setting_handles): + # XXX: Find a better interface for this. Without this fix, colors + # are in the range 0 .. 255 in the shader. + if isinstance(handle, ColorType): + value = handle.get_value_at(self._time) + else: + value = handle.get_scaled_value_at(self._time) + self._input_ubo.update_input(setting_id, value) diff --git a/RenderPipelineFile/rpcore/pluginbase/day_setting_types.py b/RenderPipelineFile/rpcore/pluginbase/day_setting_types.py index 2d7a14d3..c04812d5 100644 --- a/RenderPipelineFile/rpcore/pluginbase/day_setting_types.py +++ b/RenderPipelineFile/rpcore/pluginbase/day_setting_types.py @@ -1,162 +1,162 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - - - -from __future__ import division, print_function -from math import exp, log - -from panda3d.core import PTAFloat, PTALVecBase3f - - -from rpcore.rpobject import RPObject -from rpcore.util.smooth_connected_curve import SmoothConnectedCurve -from rpcore.pluginbase.setting_types import make_setting_from_factory - -__all__ = ["make_daysetting_from_data"] - - -def make_daysetting_from_data(data): - """ Constructs a new setting from a given dataset. This method will automatically - instantiate a new class matching the type of the given dataset. It will fill - all values provided by the dataset and then return the created instance """ - factory = { - "color": ColorType, - "scalar": ScalarType - } - return make_setting_from_factory(data, factory) - - -class BaseType(RPObject): - """ Base setting type for all setting types """ - - def __init__(self, data): - self.type = data.pop("type") - self.label = data.pop("label").strip() - self.description = data.pop("description").strip() - self.curves = [] - - RPObject.__init__(self, "dsetting:{}".format(self.label)) - - def get_value_at(self, offset): - """ Returns the unscaled value at the given day time offset """ - if len(self.curves) == 1: - return self.curves[0].get_value(offset) - return tuple(curve.get_value(offset) for curve in self.curves) - - def get_scaled_value_at(self, offset): - """ Returns the scaled value at a given day time offset """ - return self.get_scaled_value(self.get_value_at(offset)) - - def get_scaled_value(self, value): - """ Returns the scaled value from a given normalized value """ - raise NotImplementedError() - - def set_control_points(self, control_points): - """ Sets the control points on the curves. """ - for curve_index, points in enumerate(control_points): - self.curves[curve_index].control_points = points - - def serialize(self): - """ Serializes the setting to a yaml string """ - values = ','.join(i.serialize() for i in self.curves) - return "[{}]".format(values) - - -class ScalarType(BaseType): - """ Setting type storing a single scalar """ - - glsl_type = "float" - pta_type = PTAFloat - - def __init__(self, data): - BaseType.__init__(self, data) - self.unit = data.pop("unit", "none") - self.minvalue, self.maxvalue = data.pop("range") - self.logarithmic_factor = data.pop("logarithmic_factor", 1.0) - if self.unit not in ("degree", "meter", "percent", "klux", "none"): - raise Exception("Invalid unit type: {}".format(self.unit)) - - self.default = self.get_linear_value(data.pop("default")) - - self.curves.append(SmoothConnectedCurve()) - self.curves[0].set_single_value(self.default) - - def format(self, value): - """ Formats a given value, attaching the appropriate metric unit """ - metric = { - "degree": u'\N{DEGREE SIGN}', - "percent": u'%', - "meter": u'm', - "klux": u' L', - "none": '' - }[self.unit] - if self.unit == "percent": - value *= 100.0 - return u"{:3.1f}{}".format(value, metric) - - def get_scaled_value(self, value): - """ Scales a linear value """ - if self.logarithmic_factor != 1.0: - exp_mult = exp(self.logarithmic_factor * value * 4.0) - 1 - exp_div = exp(self.logarithmic_factor * 4.0) - 1 - return exp_mult / exp_div * (self.maxvalue - self.minvalue) + self.minvalue - else: - return value * (self.maxvalue - self.minvalue) + self.minvalue - - def get_linear_value(self, scaled_value): - """ Linearizes a scaled value """ - result = (scaled_value - self.minvalue) / (self.maxvalue - self.minvalue) - if self.logarithmic_factor != 1.0: - result *= exp(self.logarithmic_factor * 4.0) - 1 - result = log(result + 1.0) / (4.0 * self.logarithmic_factor) - return result - - -class ColorType(BaseType): - """ Setting type storing a RGB color triple """ - - glsl_type = "vec3" - pta_type = PTALVecBase3f - - def __init__(self, data): - BaseType.__init__(self, data) - self.default = self.get_linear_value(data.pop("default")) - colors = ((255, 0, 0), (0, 255, 0), (0, 0, 255)) - for i in range(3): - curve = SmoothConnectedCurve() - curve.set_single_value(self.default[i]) - curve.color = colors[i] - self.curves.append(curve) - - def format(self, value): - return "{:3d}, {:3d}, {:3d}".format(*(int(i) for i in value)) - - def get_scaled_value(self, value): - return tuple(i * 255.0 for i in value) - - def get_linear_value(self, scaled_value): - return tuple(i / 255.0 for i in scaled_value) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + + + +from __future__ import division, print_function +from math import exp, log + +from panda3d.core import PTAFloat, PTALVecBase3f + + +from rpcore.rpobject import RPObject +from rpcore.util.smooth_connected_curve import SmoothConnectedCurve +from rpcore.pluginbase.setting_types import make_setting_from_factory + +__all__ = ["make_daysetting_from_data"] + + +def make_daysetting_from_data(data): + """ Constructs a new setting from a given dataset. This method will automatically + instantiate a new class matching the type of the given dataset. It will fill + all values provided by the dataset and then return the created instance """ + factory = { + "color": ColorType, + "scalar": ScalarType + } + return make_setting_from_factory(data, factory) + + +class BaseType(RPObject): + """ Base setting type for all setting types """ + + def __init__(self, data): + self.type = data.pop("type") + self.label = data.pop("label").strip() + self.description = data.pop("description").strip() + self.curves = [] + + RPObject.__init__(self, "dsetting:{}".format(self.label)) + + def get_value_at(self, offset): + """ Returns the unscaled value at the given day time offset """ + if len(self.curves) == 1: + return self.curves[0].get_value(offset) + return tuple(curve.get_value(offset) for curve in self.curves) + + def get_scaled_value_at(self, offset): + """ Returns the scaled value at a given day time offset """ + return self.get_scaled_value(self.get_value_at(offset)) + + def get_scaled_value(self, value): + """ Returns the scaled value from a given normalized value """ + raise NotImplementedError() + + def set_control_points(self, control_points): + """ Sets the control points on the curves. """ + for curve_index, points in enumerate(control_points): + self.curves[curve_index].control_points = points + + def serialize(self): + """ Serializes the setting to a yaml string """ + values = ','.join(i.serialize() for i in self.curves) + return "[{}]".format(values) + + +class ScalarType(BaseType): + """ Setting type storing a single scalar """ + + glsl_type = "float" + pta_type = PTAFloat + + def __init__(self, data): + BaseType.__init__(self, data) + self.unit = data.pop("unit", "none") + self.minvalue, self.maxvalue = data.pop("range") + self.logarithmic_factor = data.pop("logarithmic_factor", 1.0) + if self.unit not in ("degree", "meter", "percent", "klux", "none"): + raise Exception("Invalid unit type: {}".format(self.unit)) + + self.default = self.get_linear_value(data.pop("default")) + + self.curves.append(SmoothConnectedCurve()) + self.curves[0].set_single_value(self.default) + + def format(self, value): + """ Formats a given value, attaching the appropriate metric unit """ + metric = { + "degree": u'\N{DEGREE SIGN}', + "percent": u'%', + "meter": u'm', + "klux": u' L', + "none": '' + }[self.unit] + if self.unit == "percent": + value *= 100.0 + return u"{:3.1f}{}".format(value, metric) + + def get_scaled_value(self, value): + """ Scales a linear value """ + if self.logarithmic_factor != 1.0: + exp_mult = exp(self.logarithmic_factor * value * 4.0) - 1 + exp_div = exp(self.logarithmic_factor * 4.0) - 1 + return exp_mult / exp_div * (self.maxvalue - self.minvalue) + self.minvalue + else: + return value * (self.maxvalue - self.minvalue) + self.minvalue + + def get_linear_value(self, scaled_value): + """ Linearizes a scaled value """ + result = (scaled_value - self.minvalue) / (self.maxvalue - self.minvalue) + if self.logarithmic_factor != 1.0: + result *= exp(self.logarithmic_factor * 4.0) - 1 + result = log(result + 1.0) / (4.0 * self.logarithmic_factor) + return result + + +class ColorType(BaseType): + """ Setting type storing a RGB color triple """ + + glsl_type = "vec3" + pta_type = PTALVecBase3f + + def __init__(self, data): + BaseType.__init__(self, data) + self.default = self.get_linear_value(data.pop("default")) + colors = ((255, 0, 0), (0, 255, 0), (0, 0, 255)) + for i in range(3): + curve = SmoothConnectedCurve() + curve.set_single_value(self.default[i]) + curve.color = colors[i] + self.curves.append(curve) + + def format(self, value): + return "{:3d}, {:3d}, {:3d}".format(*(int(i) for i in value)) + + def get_scaled_value(self, value): + return tuple(i * 255.0 for i in value) + + def get_linear_value(self, scaled_value): + return tuple(i / 255.0 for i in scaled_value) diff --git a/RenderPipelineFile/rpcore/pluginbase/manager.py b/RenderPipelineFile/rpcore/pluginbase/manager.py index 2e6957b4..3612b5df 100644 --- a/RenderPipelineFile/rpcore/pluginbase/manager.py +++ b/RenderPipelineFile/rpcore/pluginbase/manager.py @@ -1,282 +1,282 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -import importlib -import collections - -from rplibs.six import iteritems, itervalues -from rplibs.yaml import load_yaml_file - -from direct.stdpy.file import listdir, isdir, join, open - -from rpcore.rpobject import RPObject -from rpcore.native import NATIVE_CXX_LOADED -from rpcore.pluginbase.setting_types import make_setting_from_data -from rpcore.pluginbase.day_setting_types import make_daysetting_from_data - - -class PluginManager(RPObject): - - """ This class manages all plugins. It provides functionality to load plugin - settings, trigger callbacks on plugins, initialize the plugin instances - and much more. """ - - def __init__(self, pipeline): - """ Constructs a new manager with no plugins loaded. To load settings - and plugins, call load(). """ - RPObject.__init__(self) - self._pipeline = pipeline - self.settings = {} - self.day_settings = {} - self.instances = {} - self.enabled_plugins = set() - - # Used by the plugin configurator and to only load the required data - self.requires_daytime_settings = True - - def load(self): - """ Loads all plugins and their settings, and also constructs instances - of the main plugin classes for all enabled plugins """ - self.debug("Loading plugin settings") - self.load_base_settings("/$$rp/rpplugins") - self.load_setting_overrides("/$$rpconfig/plugins.yaml") - - if self.requires_daytime_settings: - self.load_daytime_overrides("/$$rpconfig/daytime.yaml") - - self.debug("Creating plugin instances ..") - for plugin_id in self.settings: - handle = self._load_plugin(plugin_id) - if handle: - self.instances[plugin_id] = handle - else: - self.disable_plugin(plugin_id) - - def disable_plugin(self, plugin_id): - """ Disables a plugin, given its plugin_id. This will remove it from - the list of enabled plugins, if it ever was there """ - self.warn("Disabling", plugin_id) - if plugin_id in self.enabled_plugins: - self.enabled_plugins.remove(plugin_id) - for instance in list(self.instances.values()): - if plugin_id in instance.required_plugins: - self.disable_plugin(instance.plugin_id) - # if plugin_id in self.instances: - # del self.instances[plugin_id] - - def unload(self): - """ Unloads all plugins """ - self.debug("Unloading all plugins") - self.instances = {} - self.settings = {} - self.day_settings = {} - self.enabled_plugins = set() - - def update(self): - """ Main update method """ - pass - - def load_base_settings(self, plugin_dir): - """ Loads the base settings of all plugins, even of disabled plugins. - This is required to verify all overrides. """ - for entry in listdir(plugin_dir): - abspath = join(plugin_dir, entry) - if isdir(abspath) and entry not in ("__pycache__", "plugin_prefab"): - self.load_plugin_settings(entry, abspath) - - def load_plugin_settings(self, plugin_id, plugin_pth): - """ Internal method to load all settings of a plugin, given its plugin - id and path to the plugin base directory """ - config_file = join(plugin_pth, "config.yaml") - config = load_yaml_file(config_file) - # When you don't specify anything in the settings, instead of - # returning an empty dictionary, pyyaml returns None - config["settings"] = config["settings"] or [] - config["daytime_settings"] = config["daytime_settings"] or [] - - if config["settings"] and len(config["settings"][0]) != 2: - self.fatal("Invalid plugin configuration, did you miss '!!omap'?") - - settings = collections.OrderedDict( - [(k, make_setting_from_data(v)) for k, v in config["settings"]]) - self.settings[plugin_id] = settings - - if self.requires_daytime_settings: - daysettings = collections.OrderedDict( - [(k, make_daysetting_from_data(v)) for k, v in config["daytime_settings"]]) - self.day_settings[plugin_id] = daysettings - - def load_setting_overrides(self, override_path): - """ Loads an override file for the settings, which contains values to - override the settings with """ - overrides = load_yaml_file(override_path) - if not overrides: - self.warn("Failed to load overrides") - return - self.enabled_plugins = set(overrides["enabled"] or []) - for plugin_id, pluginsettings in iteritems(overrides["overrides"] or {}): - if plugin_id not in self.settings: - self.warn("Unkown plugin in plugin config:", plugin_id) - continue - for setting_id, setting_val in iteritems(pluginsettings or {}): - if setting_id not in self.settings[plugin_id]: - self.warn("Unkown override:", plugin_id, ":", setting_id) - continue - self.settings[plugin_id][setting_id].set_value(setting_val) - - def load_daytime_overrides(self, override_path): - """ Loads an override file for the daytime settings, which contains - values to override the settings with """ - overrides = load_yaml_file(override_path) - if not overrides: - self.warn("Failed to load daytime overrides") - return - for plugin_id, settings in iteritems(overrides["control_points"] or {}): - for setting_id, control_points in iteritems(settings): - if setting_id not in self.day_settings[plugin_id]: - self.warn("Unkown daytime override:", plugin_id, ":", setting_id) - continue - self.day_settings[plugin_id][setting_id].set_control_points(control_points) - - def trigger_hook(self, hook_name): - """ Triggers a given hook on all plugins, effectively calling all - bound callbacks """ - hook_method = "on_{}".format(hook_name) - for plugin_id in self.enabled_plugins: - plugin_handle = self.instances[plugin_id] - if hasattr(plugin_handle, hook_method): - getattr(plugin_handle, hook_method)() - - def is_plugin_enabled(self, plugin_id): - """ Returns whether a plugin is currently enabled and loaded """ - return plugin_id in self.enabled_plugins - - def get_setting_handle(self, plugin_id, setting_id): - """ Returns the handle to a setting """ - return self.settings[plugin_id][setting_id] - - def init_defines(self): - """ Initializes all plugin settings as a define, so they can be queried - in a shader """ - for plugin_id in self.enabled_plugins: - pluginsettings = self.settings[plugin_id] - self._pipeline.stage_mgr.defines["HAVE_PLUGIN_{}".format(plugin_id)] = 1 - for setting_id, setting in iteritems(pluginsettings): - if setting.shader_runtime or not setting.runtime: - # Only store settings which either never change, or trigger - # a shader reload when they change - setting.add_defines(plugin_id, setting_id, self._pipeline.stage_mgr.defines) - - def _load_plugin(self, plugin_id): - """ Internal method to load a plugin """ - plugin_class = "rpplugins.{}.plugin".format(plugin_id) - module = importlib.import_module(plugin_class) - instance = module.Plugin(self._pipeline) - if instance.native_only and not NATIVE_CXX_LOADED: - if plugin_id in self.enabled_plugins: - self.warn("Cannot load", plugin_id, "since it requires the C++ modules.") - return False - for required_plugin in instance.required_plugins: - if required_plugin not in self.enabled_plugins: - if plugin_id in self.enabled_plugins: - self.warn("Cannot load {} since it requires {}".format( - plugin_id, required_plugin)) - return False - break - return instance - - def save_overrides(self, override_file): - """ Saves all overrides to the given file """ - output = "\n# Render Pipeline Plugin Configuration\n" - output += "# Instead of editing this file, prefer to use the Plugin Configurator\n" - output += "# Any formatting and comments will be lost\n\n" - output += "enabled:\n" - - def sort_criteria(pid): - return ("A" if self.is_plugin_enabled(pid) else "B") + pid - for plugin_id in sorted(self.settings, key=sort_criteria): - output += " {}- {}\n".format( - " # " if plugin_id not in self.enabled_plugins else " ", plugin_id) - output += "\n\noverrides:\n" - for plugin_id, pluginsettings in sorted(iteritems(self.settings)): - output += " " * 4 + plugin_id + ":\n" - for setting_id, setting_handle in iteritems(pluginsettings): - output += " " * 8 + "{}: {}\n".format(setting_id, setting_handle.value) - output += "\n" - with open(override_file, "w") as handle: - handle.write(output) - - def save_daytime_overrides(self, override_file): - """ Saves all time of day overrides to the given file """ - output = "\n# Render Pipeline Time Of Day Configuration\n" - output += "# Instead of editing this file, prefer to use the Time Of Day Editor\n" - output += "# Any formatting and comments will be lost\n\n" - output += "control_points:\n" - for plugin_id, settings in iteritems(self.day_settings): - if settings: - output += " " * 4 + plugin_id + ":\n" - for setting_id, setting_handle in iteritems(settings): - output += " " * 8 + setting_id + ": " - output += setting_handle.serialize() + "\n" - with open(override_file, "w") as handle: - handle.write(output) - - def set_plugin_enabled(self, plugin_id, enabled=True): - """ Sets whether a plugin is enabled or not, thus should be loaded when - the pipeline starts or not """ - if enabled: - self.enabled_plugins.add(plugin_id) - else: - self.enabled_plugins.remove(plugin_id) - - def reset_plugin_settings(self, plugin_id): - """ Resets all settings of a given plugin """ - for setting in itervalues(self.settings[plugin_id]): - setting.value = setting.default - - def on_setting_changed(self, plugin_id, setting_id, value): - """ Callback when a setting got changed. This will update the setting, - and also call the callback for that setting, in case the plugin defined - one. """ - if plugin_id not in self.settings or setting_id not in self.settings[plugin_id]: - self.warn("Got invalid setting change:", plugin_id, "/", setting_id) - return - - setting = self.settings[plugin_id][setting_id] - setting.set_value(value) - - if plugin_id not in self.enabled_plugins: - return - - if setting.runtime or setting.shader_runtime: - update_method = self.instances[plugin_id], "update_" + setting_id - if hasattr(*update_method): - getattr(*update_method)() - - if setting.shader_runtime: - self.init_defines() - self._pipeline.stage_mgr.write_autoconfig() - self.instances[plugin_id].reload_shaders() +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +import importlib +import collections + +from rplibs.six import iteritems, itervalues +from rplibs.yaml import load_yaml_file + +from direct.stdpy.file import listdir, isdir, join, open + +from rpcore.rpobject import RPObject +from rpcore.native import NATIVE_CXX_LOADED +from rpcore.pluginbase.setting_types import make_setting_from_data +from rpcore.pluginbase.day_setting_types import make_daysetting_from_data + + +class PluginManager(RPObject): + + """ This class manages all plugins. It provides functionality to load plugin + settings, trigger callbacks on plugins, initialize the plugin instances + and much more. """ + + def __init__(self, pipeline): + """ Constructs a new manager with no plugins loaded. To load settings + and plugins, call load(). """ + RPObject.__init__(self) + self._pipeline = pipeline + self.settings = {} + self.day_settings = {} + self.instances = {} + self.enabled_plugins = set() + + # Used by the plugin configurator and to only load the required data + self.requires_daytime_settings = True + + def load(self): + """ Loads all plugins and their settings, and also constructs instances + of the main plugin classes for all enabled plugins """ + self.debug("Loading plugin settings") + self.load_base_settings("/$$rp/rpplugins") + self.load_setting_overrides("/$$rpconfig/plugins.yaml") + + if self.requires_daytime_settings: + self.load_daytime_overrides("/$$rpconfig/daytime.yaml") + + self.debug("Creating plugin instances ..") + for plugin_id in self.settings: + handle = self._load_plugin(plugin_id) + if handle: + self.instances[plugin_id] = handle + else: + self.disable_plugin(plugin_id) + + def disable_plugin(self, plugin_id): + """ Disables a plugin, given its plugin_id. This will remove it from + the list of enabled plugins, if it ever was there """ + self.warn("Disabling", plugin_id) + if plugin_id in self.enabled_plugins: + self.enabled_plugins.remove(plugin_id) + for instance in list(self.instances.values()): + if plugin_id in instance.required_plugins: + self.disable_plugin(instance.plugin_id) + # if plugin_id in self.instances: + # del self.instances[plugin_id] + + def unload(self): + """ Unloads all plugins """ + self.debug("Unloading all plugins") + self.instances = {} + self.settings = {} + self.day_settings = {} + self.enabled_plugins = set() + + def update(self): + """ Main update method """ + pass + + def load_base_settings(self, plugin_dir): + """ Loads the base settings of all plugins, even of disabled plugins. + This is required to verify all overrides. """ + for entry in listdir(plugin_dir): + abspath = join(plugin_dir, entry) + if isdir(abspath) and entry not in ("__pycache__", "plugin_prefab"): + self.load_plugin_settings(entry, abspath) + + def load_plugin_settings(self, plugin_id, plugin_pth): + """ Internal method to load all settings of a plugin, given its plugin + id and path to the plugin base directory """ + config_file = join(plugin_pth, "config.yaml") + config = load_yaml_file(config_file) + # When you don't specify anything in the settings, instead of + # returning an empty dictionary, pyyaml returns None + config["settings"] = config["settings"] or [] + config["daytime_settings"] = config["daytime_settings"] or [] + + if config["settings"] and len(config["settings"][0]) != 2: + self.fatal("Invalid plugin configuration, did you miss '!!omap'?") + + settings = collections.OrderedDict( + [(k, make_setting_from_data(v)) for k, v in config["settings"]]) + self.settings[plugin_id] = settings + + if self.requires_daytime_settings: + daysettings = collections.OrderedDict( + [(k, make_daysetting_from_data(v)) for k, v in config["daytime_settings"]]) + self.day_settings[plugin_id] = daysettings + + def load_setting_overrides(self, override_path): + """ Loads an override file for the settings, which contains values to + override the settings with """ + overrides = load_yaml_file(override_path) + if not overrides: + self.warn("Failed to load overrides") + return + self.enabled_plugins = set(overrides["enabled"] or []) + for plugin_id, pluginsettings in iteritems(overrides["overrides"] or {}): + if plugin_id not in self.settings: + self.warn("Unkown plugin in plugin config:", plugin_id) + continue + for setting_id, setting_val in iteritems(pluginsettings or {}): + if setting_id not in self.settings[plugin_id]: + self.warn("Unkown override:", plugin_id, ":", setting_id) + continue + self.settings[plugin_id][setting_id].set_value(setting_val) + + def load_daytime_overrides(self, override_path): + """ Loads an override file for the daytime settings, which contains + values to override the settings with """ + overrides = load_yaml_file(override_path) + if not overrides: + self.warn("Failed to load daytime overrides") + return + for plugin_id, settings in iteritems(overrides["control_points"] or {}): + for setting_id, control_points in iteritems(settings): + if setting_id not in self.day_settings[plugin_id]: + self.warn("Unkown daytime override:", plugin_id, ":", setting_id) + continue + self.day_settings[plugin_id][setting_id].set_control_points(control_points) + + def trigger_hook(self, hook_name): + """ Triggers a given hook on all plugins, effectively calling all + bound callbacks """ + hook_method = "on_{}".format(hook_name) + for plugin_id in self.enabled_plugins: + plugin_handle = self.instances[plugin_id] + if hasattr(plugin_handle, hook_method): + getattr(plugin_handle, hook_method)() + + def is_plugin_enabled(self, plugin_id): + """ Returns whether a plugin is currently enabled and loaded """ + return plugin_id in self.enabled_plugins + + def get_setting_handle(self, plugin_id, setting_id): + """ Returns the handle to a setting """ + return self.settings[plugin_id][setting_id] + + def init_defines(self): + """ Initializes all plugin settings as a define, so they can be queried + in a shader """ + for plugin_id in self.enabled_plugins: + pluginsettings = self.settings[plugin_id] + self._pipeline.stage_mgr.defines["HAVE_PLUGIN_{}".format(plugin_id)] = 1 + for setting_id, setting in iteritems(pluginsettings): + if setting.shader_runtime or not setting.runtime: + # Only store settings which either never change, or trigger + # a shader reload when they change + setting.add_defines(plugin_id, setting_id, self._pipeline.stage_mgr.defines) + + def _load_plugin(self, plugin_id): + """ Internal method to load a plugin """ + plugin_class = "rpplugins.{}.plugin".format(plugin_id) + module = importlib.import_module(plugin_class) + instance = module.Plugin(self._pipeline) + if instance.native_only and not NATIVE_CXX_LOADED: + if plugin_id in self.enabled_plugins: + self.warn("Cannot load", plugin_id, "since it requires the C++ modules.") + return False + for required_plugin in instance.required_plugins: + if required_plugin not in self.enabled_plugins: + if plugin_id in self.enabled_plugins: + self.warn("Cannot load {} since it requires {}".format( + plugin_id, required_plugin)) + return False + break + return instance + + def save_overrides(self, override_file): + """ Saves all overrides to the given file """ + output = "\n# Render Pipeline Plugin Configuration\n" + output += "# Instead of editing this file, prefer to use the Plugin Configurator\n" + output += "# Any formatting and comments will be lost\n\n" + output += "enabled:\n" + + def sort_criteria(pid): + return ("A" if self.is_plugin_enabled(pid) else "B") + pid + for plugin_id in sorted(self.settings, key=sort_criteria): + output += " {}- {}\n".format( + " # " if plugin_id not in self.enabled_plugins else " ", plugin_id) + output += "\n\noverrides:\n" + for plugin_id, pluginsettings in sorted(iteritems(self.settings)): + output += " " * 4 + plugin_id + ":\n" + for setting_id, setting_handle in iteritems(pluginsettings): + output += " " * 8 + "{}: {}\n".format(setting_id, setting_handle.value) + output += "\n" + with open(override_file, "w") as handle: + handle.write(output) + + def save_daytime_overrides(self, override_file): + """ Saves all time of day overrides to the given file """ + output = "\n# Render Pipeline Time Of Day Configuration\n" + output += "# Instead of editing this file, prefer to use the Time Of Day Editor\n" + output += "# Any formatting and comments will be lost\n\n" + output += "control_points:\n" + for plugin_id, settings in iteritems(self.day_settings): + if settings: + output += " " * 4 + plugin_id + ":\n" + for setting_id, setting_handle in iteritems(settings): + output += " " * 8 + setting_id + ": " + output += setting_handle.serialize() + "\n" + with open(override_file, "w") as handle: + handle.write(output) + + def set_plugin_enabled(self, plugin_id, enabled=True): + """ Sets whether a plugin is enabled or not, thus should be loaded when + the pipeline starts or not """ + if enabled: + self.enabled_plugins.add(plugin_id) + else: + self.enabled_plugins.remove(plugin_id) + + def reset_plugin_settings(self, plugin_id): + """ Resets all settings of a given plugin """ + for setting in itervalues(self.settings[plugin_id]): + setting.value = setting.default + + def on_setting_changed(self, plugin_id, setting_id, value): + """ Callback when a setting got changed. This will update the setting, + and also call the callback for that setting, in case the plugin defined + one. """ + if plugin_id not in self.settings or setting_id not in self.settings[plugin_id]: + self.warn("Got invalid setting change:", plugin_id, "/", setting_id) + return + + setting = self.settings[plugin_id][setting_id] + setting.set_value(value) + + if plugin_id not in self.enabled_plugins: + return + + if setting.runtime or setting.shader_runtime: + update_method = self.instances[plugin_id], "update_" + setting_id + if hasattr(*update_method): + getattr(*update_method)() + + if setting.shader_runtime: + self.init_defines() + self._pipeline.stage_mgr.write_autoconfig() + self.instances[plugin_id].reload_shaders() diff --git a/RenderPipelineFile/rpcore/pluginbase/setting_types.py b/RenderPipelineFile/rpcore/pluginbase/setting_types.py index 45322abd..fb95510c 100644 --- a/RenderPipelineFile/rpcore/pluginbase/setting_types.py +++ b/RenderPipelineFile/rpcore/pluginbase/setting_types.py @@ -1,239 +1,239 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import print_function - -from rplibs.six import iteritems, string_types -from rpcore.rpobject import RPObject - -__all__ = ["make_setting_from_data"] - - -def make_setting_from_factory(data, factory): - """ Constructs a new setting from a given dataset, alongst with a factory - to resolve the setting type to """ - if data["type"] not in factory: - raise Exception("Unkown setting type: {}".format(data["type"])) - try: - instance = factory[data["type"]](data) - except Exception: - print("Exception occured while parsing", data) - raise - if data: - raise Exception("Unparsed data left in plugin setting: {}".format(data)) - return instance - - -def make_setting_from_data(data): - """ Constructs a new setting from a given dataset. This method will automatically - instantiate a new class matching the type of the given dataset. It will fill - all values provided by the dataset and then return the created instance """ - factory = { - "int": IntType, - "float": FloatType, - "bool": BoolType, - "enum": EnumType, - "path": PathType, - "power_of_two": PowerOfTwoType, - "sample_sequence": SampleSequenceType - } - return make_setting_from_factory(data, factory) - - -class BaseType(RPObject): - - """ This is the base setting type, all setting types derive from this """ - - def __init__(self, data): - """ Fills in all settings from the given setting data """ - self.value = None - self.type = data.pop("type") - self.label = data.pop("label").strip() - self.description = data.pop("description").strip() - self.runtime = data.pop("runtime", False) - self.shader_runtime = data.pop("shader_runtime", False) - self.display_conditions = data.pop("display_if", {}) - - RPObject.__init__(self, "psetting:{}".format(self.label)) - - def set_value(self, value): - """ Sets the value of the setting, should get overridden """ - raise NotImplementedError() - - def add_defines(self, plugin_id, setting_id, defines): - """ Makes the value of this plugin available as a define """ - defines["{}_{}".format(plugin_id, setting_id)] = self.value - - def should_be_visible(self, settings): - """ Evaluates whether the plugin should be visible, taking all display - conditions into account """ - for key, val in iteritems(self.display_conditions): - if settings[key].value != val: - return False - return True - - -class TemplatedType(BaseType): - - """ This setting stores a single type including a minimum and maximum value. - It is shared between integer and floating point types. """ - - def __init__(self, template_type, data): - BaseType.__init__(self, data) - self.template_type = template_type - self.default = template_type(data.pop("default")) - self.value = self.default - - setting_range = data.pop("range") - self.minval = template_type(setting_range[0]) - self.maxval = template_type(setting_range[1]) - - def set_value(self, value): - value = self.template_type(value) - if self.minval <= value <= self.maxval: - self.value = value - else: - self.error("Invalid value: {}".format(value)) - - -class IntType(TemplatedType): - """ Template instantiation of TemplatedType using int """ - - def __init__(self, data): - TemplatedType.__init__(self, int, data) - -class PowerOfTwoType(IntType): - """ Type for any power of two resolution """ - - def __init__(self, data): - IntType.__init__(self, data) - - def set_value(self, value): - value = int(value) - if self.minval <= value <= self.maxval: - if value in [2**i for i in range(32)]: - self.value = value - else: - self.error("Not a power of two: {}".format(value)) - else: - self.error("Invalid value: {}".format(value)) - - -class FloatType(TemplatedType): - """ Template instantiation of TemplatedType using float """ - - def __init__(self, data): - TemplatedType.__init__(self, float, data) - - -class BoolType(BaseType): - """ Boolean setting type """ - - def __init__(self, data): - BaseType.__init__(self, data) - self.default = bool(data.pop("default")) - self.value = self.default - - def set_value(self, value): - if isinstance(value, string_types): - self.value = str(value.lower()) in ("true", "1") - else: - self.value = bool(value) - - -class EnumType(BaseType): - """ Enumeration setting type """ - def __init__(self, data): - BaseType.__init__(self, data) - self.values = tuple(data.pop("values")) - self.default = data.pop("default") - if self.default not in self.values: - raise Exception("Enum default not in enum values: {}".format(self.default)) - self.value = self.default - - def set_value(self, value): - if value not in self.values: - self.error("Value '" + str(value) + "' not in enum values!") - return - self.value = value - - def add_defines(self, plugin_id, setting_id, defines): - defines["{}_{}".format(plugin_id, setting_id)] = 1000 + self.values.index(self.value) - - for i, val in enumerate(self.values): - defines["enum_{}_{}_{}".format(plugin_id, setting_id, val)] = 1000 + i - -class SampleSequenceType(BaseType): - """ Type for any 2D or 3D sample sequence """ - - POISSON_2D_SIZES = (4, 8, 12, 16, 32, 64) - POISSON_3D_SIZES = (16, 32, 64) - HALTON_SIZES = (4, 8, 16, 32, 64, 128) - - def __init__(self, data): - BaseType.__init__(self, data) - self.dimension = int(data.pop("dimension")) - if self.dimension not in (2, 3): - raise Exception("Not a valid dimension, must be 2 or 3!") - self.default = data.pop("default") - if self.default not in self.sequences: - raise Exception("Not a valid sequence: {}".format(self.default)) - self.value = self.default - - def set_value(self, value): - if value not in self.sequences: - self.error("Value '" + str(value) + "' is not a valid sequence!") - return - self.value = value - - @property - def sequences(self): - result = [] - if self.dimension == 2: - for dim in self.POISSON_2D_SIZES: - result.append("poisson_2D_" + str(dim)) - else: - for dim in self.POISSON_3D_SIZES: - result.append("poisson_3D_" + str(dim)) - for dim in self.HALTON_SIZES: - result.append("halton_" + str(self.dimension) + "D_" + str(dim)) - return result - -class PathType(BaseType): - """ Path type to specify paths to files """ - def __init__(self, data): - BaseType.__init__(self, data) - self.default = str(data.pop("default")) - self.value = self.default - self.file_type = str(data.pop("file_type")) - self.base_path = str(data.pop("base_path")) - - def set_value(self, value): - self.value = str(value) - - def add_defines(self, *args): - # Paths are not available in shaders - pass +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import print_function + +from rplibs.six import iteritems, string_types +from rpcore.rpobject import RPObject + +__all__ = ["make_setting_from_data"] + + +def make_setting_from_factory(data, factory): + """ Constructs a new setting from a given dataset, alongst with a factory + to resolve the setting type to """ + if data["type"] not in factory: + raise Exception("Unkown setting type: {}".format(data["type"])) + try: + instance = factory[data["type"]](data) + except Exception: + print("Exception occured while parsing", data) + raise + if data: + raise Exception("Unparsed data left in plugin setting: {}".format(data)) + return instance + + +def make_setting_from_data(data): + """ Constructs a new setting from a given dataset. This method will automatically + instantiate a new class matching the type of the given dataset. It will fill + all values provided by the dataset and then return the created instance """ + factory = { + "int": IntType, + "float": FloatType, + "bool": BoolType, + "enum": EnumType, + "path": PathType, + "power_of_two": PowerOfTwoType, + "sample_sequence": SampleSequenceType + } + return make_setting_from_factory(data, factory) + + +class BaseType(RPObject): + + """ This is the base setting type, all setting types derive from this """ + + def __init__(self, data): + """ Fills in all settings from the given setting data """ + self.value = None + self.type = data.pop("type") + self.label = data.pop("label").strip() + self.description = data.pop("description").strip() + self.runtime = data.pop("runtime", False) + self.shader_runtime = data.pop("shader_runtime", False) + self.display_conditions = data.pop("display_if", {}) + + RPObject.__init__(self, "psetting:{}".format(self.label)) + + def set_value(self, value): + """ Sets the value of the setting, should get overridden """ + raise NotImplementedError() + + def add_defines(self, plugin_id, setting_id, defines): + """ Makes the value of this plugin available as a define """ + defines["{}_{}".format(plugin_id, setting_id)] = self.value + + def should_be_visible(self, settings): + """ Evaluates whether the plugin should be visible, taking all display + conditions into account """ + for key, val in iteritems(self.display_conditions): + if settings[key].value != val: + return False + return True + + +class TemplatedType(BaseType): + + """ This setting stores a single type including a minimum and maximum value. + It is shared between integer and floating point types. """ + + def __init__(self, template_type, data): + BaseType.__init__(self, data) + self.template_type = template_type + self.default = template_type(data.pop("default")) + self.value = self.default + + setting_range = data.pop("range") + self.minval = template_type(setting_range[0]) + self.maxval = template_type(setting_range[1]) + + def set_value(self, value): + value = self.template_type(value) + if self.minval <= value <= self.maxval: + self.value = value + else: + self.error("Invalid value: {}".format(value)) + + +class IntType(TemplatedType): + """ Template instantiation of TemplatedType using int """ + + def __init__(self, data): + TemplatedType.__init__(self, int, data) + +class PowerOfTwoType(IntType): + """ Type for any power of two resolution """ + + def __init__(self, data): + IntType.__init__(self, data) + + def set_value(self, value): + value = int(value) + if self.minval <= value <= self.maxval: + if value in [2**i for i in range(32)]: + self.value = value + else: + self.error("Not a power of two: {}".format(value)) + else: + self.error("Invalid value: {}".format(value)) + + +class FloatType(TemplatedType): + """ Template instantiation of TemplatedType using float """ + + def __init__(self, data): + TemplatedType.__init__(self, float, data) + + +class BoolType(BaseType): + """ Boolean setting type """ + + def __init__(self, data): + BaseType.__init__(self, data) + self.default = bool(data.pop("default")) + self.value = self.default + + def set_value(self, value): + if isinstance(value, string_types): + self.value = str(value.lower()) in ("true", "1") + else: + self.value = bool(value) + + +class EnumType(BaseType): + """ Enumeration setting type """ + def __init__(self, data): + BaseType.__init__(self, data) + self.values = tuple(data.pop("values")) + self.default = data.pop("default") + if self.default not in self.values: + raise Exception("Enum default not in enum values: {}".format(self.default)) + self.value = self.default + + def set_value(self, value): + if value not in self.values: + self.error("Value '" + str(value) + "' not in enum values!") + return + self.value = value + + def add_defines(self, plugin_id, setting_id, defines): + defines["{}_{}".format(plugin_id, setting_id)] = 1000 + self.values.index(self.value) + + for i, val in enumerate(self.values): + defines["enum_{}_{}_{}".format(plugin_id, setting_id, val)] = 1000 + i + +class SampleSequenceType(BaseType): + """ Type for any 2D or 3D sample sequence """ + + POISSON_2D_SIZES = (4, 8, 12, 16, 32, 64) + POISSON_3D_SIZES = (16, 32, 64) + HALTON_SIZES = (4, 8, 16, 32, 64, 128) + + def __init__(self, data): + BaseType.__init__(self, data) + self.dimension = int(data.pop("dimension")) + if self.dimension not in (2, 3): + raise Exception("Not a valid dimension, must be 2 or 3!") + self.default = data.pop("default") + if self.default not in self.sequences: + raise Exception("Not a valid sequence: {}".format(self.default)) + self.value = self.default + + def set_value(self, value): + if value not in self.sequences: + self.error("Value '" + str(value) + "' is not a valid sequence!") + return + self.value = value + + @property + def sequences(self): + result = [] + if self.dimension == 2: + for dim in self.POISSON_2D_SIZES: + result.append("poisson_2D_" + str(dim)) + else: + for dim in self.POISSON_3D_SIZES: + result.append("poisson_3D_" + str(dim)) + for dim in self.HALTON_SIZES: + result.append("halton_" + str(self.dimension) + "D_" + str(dim)) + return result + +class PathType(BaseType): + """ Path type to specify paths to files """ + def __init__(self, data): + BaseType.__init__(self, data) + self.default = str(data.pop("default")) + self.value = self.default + self.file_type = str(data.pop("file_type")) + self.base_path = str(data.pop("base_path")) + + def set_value(self, value): + self.value = str(value) + + def add_defines(self, *args): + # Paths are not available in shaders + pass diff --git a/RenderPipelineFile/rpcore/pynative/README.md b/RenderPipelineFile/rpcore/pynative/README.md index 7aa90369..1ba224f2 100644 --- a/RenderPipelineFile/rpcore/pynative/README.md +++ b/RenderPipelineFile/rpcore/pynative/README.md @@ -1,13 +1,13 @@ - -## Python Implementation - -This is the Python implementation of the pipeline internals. - -**This folder contains no documentation!** - -To view the documented code, please head over to the C++ Modules at `code/native/source`, -which implement the same features but are commented! - -Besides of not being commented, the code is not written efficient and not in a -pythonic way, too, and instead tries to match the C++ code as close as possible, -to make updates as easy as possible. + +## Python Implementation + +This is the Python implementation of the pipeline internals. + +**This folder contains no documentation!** + +To view the documented code, please head over to the C++ Modules at `code/native/source`, +which implement the same features but are commented! + +Besides of not being commented, the code is not written efficient and not in a +pythonic way, too, and instead tries to match the C++ code as close as possible, +to make updates as easy as possible. diff --git a/RenderPipelineFile/rpcore/pynative/__init__.py b/RenderPipelineFile/rpcore/pynative/__init__.py index 26e3b5cf..986e1ccd 100644 --- a/RenderPipelineFile/rpcore/pynative/__init__.py +++ b/RenderPipelineFile/rpcore/pynative/__init__.py @@ -1,38 +1,38 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -# flake8: noqa - -from rpcore.pynative.gpu_command import GPUCommand -from rpcore.pynative.gpu_command_list import GPUCommandList -from rpcore.pynative.ies_dataset import IESDataset -from rpcore.pynative.internal_light_manager import InternalLightManager -from rpcore.pynative.rp_light import RPLight -from rpcore.pynative.rp_spot_light import RPSpotLight -from rpcore.pynative.rp_point_light import RPPointLight -from rpcore.pynative.shadow_manager import ShadowManager -from rpcore.pynative.tag_state_manager import TagStateManager -from rpcore.pynative.pssm_camera_rig import PSSMCameraRig +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +# flake8: noqa + +from rpcore.pynative.gpu_command import GPUCommand +from rpcore.pynative.gpu_command_list import GPUCommandList +from rpcore.pynative.ies_dataset import IESDataset +from rpcore.pynative.internal_light_manager import InternalLightManager +from rpcore.pynative.rp_light import RPLight +from rpcore.pynative.rp_spot_light import RPSpotLight +from rpcore.pynative.rp_point_light import RPPointLight +from rpcore.pynative.shadow_manager import ShadowManager +from rpcore.pynative.tag_state_manager import TagStateManager +from rpcore.pynative.pssm_camera_rig import PSSMCameraRig diff --git a/RenderPipelineFile/rpcore/pynative/gpu_command.py b/RenderPipelineFile/rpcore/pynative/gpu_command.py index 9cb0c528..c4b13d6c 100644 --- a/RenderPipelineFile/rpcore/pynative/gpu_command.py +++ b/RenderPipelineFile/rpcore/pynative/gpu_command.py @@ -1,85 +1,85 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import print_function -from rplibs.six.moves import range # pylint: disable=import-error - -import struct - - -class GPUCommand(object): - - """ Please refer to the native C++ implementation for docstrings and comments. - This is just the python implementation, which does not contain documentation! """ - - CMD_invalid = 0 - CMD_store_light = 1 - CMD_remove_light = 2 - CMD_store_source = 3 - CMD_remove_sources = 4 - - def __init__(self, command_type): - self._command_type = command_type - self._current_index = 0 - self._data = [0.0] * 32 - self.push_int(command_type) - - def push_int(self, value): - self.push_float(float(value)) - - def push_float(self, value): - if self._current_index >= 32: - print("GPUCommand: out of bounds!") - return - self._data[self._current_index] = float(value) - self._current_index += 1 - - def push_vec3(self, value): - self.push_float(value.x) - self.push_float(value.y) - self.push_float(value.z) - - def push_vec4(self, value): - self.push_vec3(value) - self.push_float(value.get_w()) - - def push_mat4(self, value): - for i in range(4): - for j in range(4): - self.push_float(value.get_cell(i, j)) - - @staticmethod - def get_uses_integer_packing(): - return False - - def write_to(self, dest, command_index): - data = struct.pack("f" * 32, *self._data) - offset = command_index * 32 * 4 - dest.set_subdata(offset, 32 * 4, data) - - def write(self, out=None): # pylint: disable=unused-argument - print("GPUCommand(type=", self._command_type, "size=", self._current_index, ")") - print("Data:", ', '.join([str(i) for i in self._data])) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import print_function +from rplibs.six.moves import range # pylint: disable=import-error + +import struct + + +class GPUCommand(object): + + """ Please refer to the native C++ implementation for docstrings and comments. + This is just the python implementation, which does not contain documentation! """ + + CMD_invalid = 0 + CMD_store_light = 1 + CMD_remove_light = 2 + CMD_store_source = 3 + CMD_remove_sources = 4 + + def __init__(self, command_type): + self._command_type = command_type + self._current_index = 0 + self._data = [0.0] * 32 + self.push_int(command_type) + + def push_int(self, value): + self.push_float(float(value)) + + def push_float(self, value): + if self._current_index >= 32: + print("GPUCommand: out of bounds!") + return + self._data[self._current_index] = float(value) + self._current_index += 1 + + def push_vec3(self, value): + self.push_float(value.x) + self.push_float(value.y) + self.push_float(value.z) + + def push_vec4(self, value): + self.push_vec3(value) + self.push_float(value.get_w()) + + def push_mat4(self, value): + for i in range(4): + for j in range(4): + self.push_float(value.get_cell(i, j)) + + @staticmethod + def get_uses_integer_packing(): + return False + + def write_to(self, dest, command_index): + data = struct.pack("f" * 32, *self._data) + offset = command_index * 32 * 4 + dest.set_subdata(offset, 32 * 4, data) + + def write(self, out=None): # pylint: disable=unused-argument + print("GPUCommand(type=", self._command_type, "size=", self._current_index, ")") + print("Data:", ', '.join([str(i) for i in self._data])) diff --git a/RenderPipelineFile/rpcore/pynative/gpu_command_list.py b/RenderPipelineFile/rpcore/pynative/gpu_command_list.py index 322557d0..b15d4b5e 100644 --- a/RenderPipelineFile/rpcore/pynative/gpu_command_list.py +++ b/RenderPipelineFile/rpcore/pynative/gpu_command_list.py @@ -1,50 +1,50 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - - -class GPUCommandList(object): - - """ Please refer to the native C++ implementation for docstrings and comments. - This is just the python implementation, which does not contain documentation! """ - - def __init__(self): - self._commands = [] - - def add_command(self, cmd): - self._commands.append(cmd) - - @property - def num_commands(self): - return len(self._commands) - - def write_commands_to(self, dest, limit=32): - num_commands_written = 0 - - while num_commands_written < limit and self._commands: - self._commands.pop(0).write_to(dest, num_commands_written) - num_commands_written += 1 - - return num_commands_written +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + + +class GPUCommandList(object): + + """ Please refer to the native C++ implementation for docstrings and comments. + This is just the python implementation, which does not contain documentation! """ + + def __init__(self): + self._commands = [] + + def add_command(self, cmd): + self._commands.append(cmd) + + @property + def num_commands(self): + return len(self._commands) + + def write_commands_to(self, dest, limit=32): + num_commands_written = 0 + + while num_commands_written < limit and self._commands: + self._commands.pop(0).write_to(dest, num_commands_written) + num_commands_written += 1 + + return num_commands_written diff --git a/RenderPipelineFile/rpcore/pynative/ies_dataset.py b/RenderPipelineFile/rpcore/pynative/ies_dataset.py index d24ba31d..7d686729 100644 --- a/RenderPipelineFile/rpcore/pynative/ies_dataset.py +++ b/RenderPipelineFile/rpcore/pynative/ies_dataset.py @@ -1,97 +1,97 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rplibs.six.moves import range # pylint: disable=import-error - -import math -from panda3d.core import PNMImage - - -class IESDataset(object): - - """ Please refer to the native C++ implementation for docstrings and comments. - This is just the python implementation, which does not contain documentation! """ - - def __init__(self): - self._vertical_angles = None - self._horizontal_angles = None - self._candela_values = None - - def set_vertical_angles(self, vertical_angles): - self._vertical_angles = vertical_angles - - def set_horizontal_angles(self, horizontal_angles): - self._horizontal_angles = horizontal_angles - - def set_candela_values(self, candela_values): - self._candela_values = candela_values - - def generate_dataset_texture_into(self, dest_tex, layer_index): - resolution_vertical = dest_tex.get_y_size() - resolution_horizontal = dest_tex.get_x_size() - - dest = PNMImage(resolution_vertical, resolution_horizontal, 1, 65535) - - for vert in range(resolution_vertical): - for horiz in range(resolution_horizontal): - vert_angle = vert / (resolution_vertical - 1.0) - vert_angle = math.cos(vert_angle * math.pi) * 90.0 + 90.0 - horiz_angle = horiz / (resolution_horizontal - 1.0) * 360.0 - candela = self.get_candela_value(vert_angle, horiz_angle) - dest.set_xel(vert, horiz, candela) - - dest_tex.load(dest, layer_index, 0) - - def get_candela_value(self, vertical_angle, horizontal_angle): # noqa # pylint: disable=unused-argument - # NOTICE: Since python is slower, we always only assume a dataset without - # horizontal angles. This still produces convincing results, but does - # generate much faster. - return self.get_vertical_candela_value(0, vertical_angle) - - def get_candela_value_from_index(self, vertical_angle_idx, horizontal_angle_idx): - index = vertical_angle_idx + horizontal_angle_idx * len(self._vertical_angles) - return self._candela_values[index] - - def get_vertical_candela_value(self, horizontal_angle_idx, vertical_angle): - if vertical_angle < 0.0: - return 0.0 - - if vertical_angle > self._vertical_angles[len(self._vertical_angles) - 1]: - return 0.0 - - for vertical_index in range(1, len(self._vertical_angles)): - curr_angle = self._vertical_angles[vertical_index] - - if curr_angle > vertical_angle: - prev_angle = self._vertical_angles[vertical_index - 1] - prev_value = self.get_candela_value_from_index( - vertical_index - 1, horizontal_angle_idx) - curr_value = self.get_candela_value_from_index( - vertical_index, horizontal_angle_idx) - lerp = (vertical_angle - prev_angle) / (curr_angle - prev_angle) - assert lerp >= 0.0 and lerp <= 1.0 - return curr_value * lerp + prev_value * (1.0 - lerp) - return 0.0 +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rplibs.six.moves import range # pylint: disable=import-error + +import math +from panda3d.core import PNMImage + + +class IESDataset(object): + + """ Please refer to the native C++ implementation for docstrings and comments. + This is just the python implementation, which does not contain documentation! """ + + def __init__(self): + self._vertical_angles = None + self._horizontal_angles = None + self._candela_values = None + + def set_vertical_angles(self, vertical_angles): + self._vertical_angles = vertical_angles + + def set_horizontal_angles(self, horizontal_angles): + self._horizontal_angles = horizontal_angles + + def set_candela_values(self, candela_values): + self._candela_values = candela_values + + def generate_dataset_texture_into(self, dest_tex, layer_index): + resolution_vertical = dest_tex.get_y_size() + resolution_horizontal = dest_tex.get_x_size() + + dest = PNMImage(resolution_vertical, resolution_horizontal, 1, 65535) + + for vert in range(resolution_vertical): + for horiz in range(resolution_horizontal): + vert_angle = vert / (resolution_vertical - 1.0) + vert_angle = math.cos(vert_angle * math.pi) * 90.0 + 90.0 + horiz_angle = horiz / (resolution_horizontal - 1.0) * 360.0 + candela = self.get_candela_value(vert_angle, horiz_angle) + dest.set_xel(vert, horiz, candela) + + dest_tex.load(dest, layer_index, 0) + + def get_candela_value(self, vertical_angle, horizontal_angle): # noqa # pylint: disable=unused-argument + # NOTICE: Since python is slower, we always only assume a dataset without + # horizontal angles. This still produces convincing results, but does + # generate much faster. + return self.get_vertical_candela_value(0, vertical_angle) + + def get_candela_value_from_index(self, vertical_angle_idx, horizontal_angle_idx): + index = vertical_angle_idx + horizontal_angle_idx * len(self._vertical_angles) + return self._candela_values[index] + + def get_vertical_candela_value(self, horizontal_angle_idx, vertical_angle): + if vertical_angle < 0.0: + return 0.0 + + if vertical_angle > self._vertical_angles[len(self._vertical_angles) - 1]: + return 0.0 + + for vertical_index in range(1, len(self._vertical_angles)): + curr_angle = self._vertical_angles[vertical_index] + + if curr_angle > vertical_angle: + prev_angle = self._vertical_angles[vertical_index - 1] + prev_value = self.get_candela_value_from_index( + vertical_index - 1, horizontal_angle_idx) + curr_value = self.get_candela_value_from_index( + vertical_index, horizontal_angle_idx) + lerp = (vertical_angle - prev_angle) / (curr_angle - prev_angle) + assert lerp >= 0.0 and lerp <= 1.0 + return curr_value * lerp + prev_value * (1.0 - lerp) + return 0.0 diff --git a/RenderPipelineFile/rpcore/pynative/internal_light_manager.py b/RenderPipelineFile/rpcore/pynative/internal_light_manager.py index eb4f5810..f63d4895 100644 --- a/RenderPipelineFile/rpcore/pynative/internal_light_manager.py +++ b/RenderPipelineFile/rpcore/pynative/internal_light_manager.py @@ -1,261 +1,261 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" -from __future__ import print_function -from rplibs.six.moves import range # pylint: disable=import-error - -from panda3d.core import Vec3 - -from rpcore.pynative.pointer_slot_storage import PointerSlotStorage -from rpcore.pynative.gpu_command import GPUCommand - -MAX_LIGHT_COUNT = 65535 -MAX_SHADOW_SOURCES = 2048 - - -class InternalLightManager(object): - - """ Please refer to the native C++ implementation for docstrings and comments. - This is just the python implementation, which does not contain documentation! """ - - def __init__(self): - self._lights = PointerSlotStorage(MAX_LIGHT_COUNT) - self._shadow_sources = PointerSlotStorage(MAX_SHADOW_SOURCES) - self._cmd_list = None - self._shadow_manager = None - self._camera_pos = Vec3(0) - self._shadow_update_distance = 100.0 - - def get_max_light_index(self): - return self._lights.get_max_index() - - max_light_index = property(get_max_light_index) - - def get_num_lights(self): - return self._lights.get_num_entries() - - num_lights = property(get_num_lights) - - def get_num_shadow_sources(self): - return self._shadow_sources.get_num_entries() - - num_shadow_sources = property(get_num_shadow_sources) - - def set_shadow_manager(self, shadow_manager): - self._shadow_manager = shadow_manager - - def get_shadow_manager(self): - return self._shadow_manager - - shadow_manager = property(get_shadow_manager, set_shadow_manager) - - def set_command_list(self, cmd_list): - self._cmd_list = cmd_list - - def set_camera_pos(self, pos): - self._camera_pos = pos - - def set_shadow_update_distance(self, dist): - self._shadow_update_distance = dist - - def add_light(self, light): - if light.has_slot(): - print("ERROR: Cannot add light since it already has a slot!") - return - - slot = self._lights.find_slot() - if slot < 0: - print("ERROR: Could not find a free slot for a new light!") - return - - light.assign_slot(slot) - self._lights.reserve_slot(slot, light) - - if light.get_casts_shadows(): - self.setup_shadows(light) - - self.gpu_update_light(light) - - def setup_shadows(self, light): - light.init_shadow_sources() - light.update_shadow_sources() - - num_sources = light.get_num_shadow_sources() - base_slot = self._shadow_sources.find_consecutive_slots(num_sources) - if base_slot < 0: - print("ERROR: Failed to find slot for shadow sources!") - return - - for i in range(num_sources): - source = light.get_shadow_source(i) - source.set_needs_update(True) - slot = base_slot + i - self._shadow_sources.reserve_slot(slot, source) - source.set_slot(slot) - - def remove_light(self, light): - print(f'44444444444444444444444444') - print("\n" + "=" * 50) - print(f"DEBUG: Entering remove_light for light object: {light.casts_shadows}") - if light: - print(f" - Light's Slot: {light.get_slot() if light.has_slot() else 'No Slot'}") - print(f" - Does it cast shadows? light.get_casts_shadows() -> {light.get_casts_shadows()}") - else: - print(" - Light object is None!") - - print("\n --- State of Light System BEFORE removal ---") - if hasattr(self, '_lights') and hasattr(self._lights, '_data'): - # 打印出当前所有灯光对象,看看有没有异常 - print(f" - Light Data Array (_data):") - for i, l_obj in enumerate(self._lights._data): - print(f" [{i}]: {l_obj}") - - print(f"\n - Max Index (_max_index): {self._lights._max_index}") - print(f" - Num Entries (_num_entries): {self._lights._num_entries}") - print("=" * 50 + "\n") - assert light is not None - if not light.has_slot(): - print("ERROR: Could not detach light, light was not attached!") - return - - # 保存必要信息,避免过早清理 - light_slot = light.get_slot() - has_shadows = light.get_casts_shadows() - - # 处理shadow sources(如果有的话) - if has_shadows: - num_sources = light.get_num_shadow_sources() - if num_sources > 0: - # 关键修复:先保存第一个source的slot,再清理 - first_source = light.get_shadow_source(0) - first_source_slot = first_source.get_slot() # 保存slot值 - - # --- 在这里加上打印语句 --- - print(f"DEBUG: Removing shadow sources. Start Slot: {first_source_slot}, Count: {num_sources}") - - # 先发送GPU移除命令(在清理之前) - cmd_remove = GPUCommand(GPUCommand.CMD_remove_sources) - cmd_remove.push_int(first_source_slot) # 使用保存的slot值 - cmd_remove.push_int(num_sources) - self._cmd_list.add_command(cmd_remove) - - # 然后清理所有shadow sources - for i in range(num_sources): - source = light.get_shadow_source(i) - if source.has_slot(): - self._shadow_sources.free_slot(source.get_slot()) - if source.has_region(): - self._shadow_manager.get_atlas().free_region(source.get_region()) - source.clear_region() - - # 清理light的shadow sources - light.clear_shadow_sources() - - # 发送GPU移除light命令 - self.gpu_remove_light(light) - - # 最后释放light slot和清理light信息 - self._lights.free_slot(light_slot) - light.remove_slot() - - def gpu_remove_consecutive_sources(self, first_source, num_sources): - cmd_remove = GPUCommand(GPUCommand.CMD_remove_sources) - cmd_remove.push_int(first_source.get_slot()) - cmd_remove.push_int(num_sources) - self._cmd_list.add_command(cmd_remove) - - def gpu_remove_light(self, light): - cmd_remove = GPUCommand(GPUCommand.CMD_remove_light) - cmd_remove.push_int(light.get_slot()) - self._cmd_list.add_command(cmd_remove) - - def gpu_update_light(self, light): - cmd_update = GPUCommand(GPUCommand.CMD_store_light) - cmd_update.push_int(light.get_slot()) - light.write_to_command(cmd_update) - light.set_needs_update(False) - self._cmd_list.add_command(cmd_update) - - def gpu_update_source(self, source): - cmd_update = GPUCommand(GPUCommand.CMD_store_source) - cmd_update.push_int(source.get_slot()) - source.write_to_command(cmd_update) - self._cmd_list.add_command(cmd_update) - - def update_lights(self): - for light in self._lights.begin(): - if light.get_needs_update(): - if light.casts_shadows: - light.update_shadow_sources() - self.gpu_update_light(light) - - def update_shadow_sources(self): - sources_to_update = [] - - for source in self._shadow_sources.begin(): - # if source and source.get_needs_update(): - # sources_to_update.append(source) - if source: - bounds = source.get_bounds() - distance_to_camera = (self._camera_pos - bounds.get_center()) - bounds.get_radius() - if distance_to_camera < self._shadow_update_distance: - sources_to_update.append(source) - else: - if source.has_region(): - self._shadow_manager.get_atlas().free_region(source.get_region()) - source.clear_region() - - def get_source_score(source): - dist = (source.get_bounds().get_center() - self._camera_pos).length() - return -dist + (10**10 if source.has_region() else 0) - - sorted_sources = list(sorted(sources_to_update, key=get_source_score)) - - atlas = self._shadow_manager.get_atlas() - update_slots = min( - len(sorted_sources), - self._shadow_manager.get_num_update_slots_left()) - - for i in range(update_slots): - if sorted_sources[i].has_region(): - atlas.free_region(sorted_sources[i].get_region()) - - for i in range(update_slots): - source = sorted_sources[i] - - if not self._shadow_manager.add_update(source): - print("ERROR: Shadow manager ensured update slot, but slot is taken!") - break - - region_size = atlas.get_required_tiles(source.get_resolution()) - new_region = atlas.find_and_reserve_region(region_size, region_size) - new_uv_region = atlas.region_to_uv(new_region) - source.set_region(new_region, new_uv_region) - source.set_needs_update(False) - self.gpu_update_source(source) - - def update(self): - self.update_lights() - self.update_shadow_sources() +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +from __future__ import print_function +from rplibs.six.moves import range # pylint: disable=import-error + +from panda3d.core import Vec3 + +from rpcore.pynative.pointer_slot_storage import PointerSlotStorage +from rpcore.pynative.gpu_command import GPUCommand + +MAX_LIGHT_COUNT = 65535 +MAX_SHADOW_SOURCES = 2048 + + +class InternalLightManager(object): + + """ Please refer to the native C++ implementation for docstrings and comments. + This is just the python implementation, which does not contain documentation! """ + + def __init__(self): + self._lights = PointerSlotStorage(MAX_LIGHT_COUNT) + self._shadow_sources = PointerSlotStorage(MAX_SHADOW_SOURCES) + self._cmd_list = None + self._shadow_manager = None + self._camera_pos = Vec3(0) + self._shadow_update_distance = 100.0 + + def get_max_light_index(self): + return self._lights.get_max_index() + + max_light_index = property(get_max_light_index) + + def get_num_lights(self): + return self._lights.get_num_entries() + + num_lights = property(get_num_lights) + + def get_num_shadow_sources(self): + return self._shadow_sources.get_num_entries() + + num_shadow_sources = property(get_num_shadow_sources) + + def set_shadow_manager(self, shadow_manager): + self._shadow_manager = shadow_manager + + def get_shadow_manager(self): + return self._shadow_manager + + shadow_manager = property(get_shadow_manager, set_shadow_manager) + + def set_command_list(self, cmd_list): + self._cmd_list = cmd_list + + def set_camera_pos(self, pos): + self._camera_pos = pos + + def set_shadow_update_distance(self, dist): + self._shadow_update_distance = dist + + def add_light(self, light): + if light.has_slot(): + print("ERROR: Cannot add light since it already has a slot!") + return + + slot = self._lights.find_slot() + if slot < 0: + print("ERROR: Could not find a free slot for a new light!") + return + + light.assign_slot(slot) + self._lights.reserve_slot(slot, light) + + if light.get_casts_shadows(): + self.setup_shadows(light) + + self.gpu_update_light(light) + + def setup_shadows(self, light): + light.init_shadow_sources() + light.update_shadow_sources() + + num_sources = light.get_num_shadow_sources() + base_slot = self._shadow_sources.find_consecutive_slots(num_sources) + if base_slot < 0: + print("ERROR: Failed to find slot for shadow sources!") + return + + for i in range(num_sources): + source = light.get_shadow_source(i) + source.set_needs_update(True) + slot = base_slot + i + self._shadow_sources.reserve_slot(slot, source) + source.set_slot(slot) + + def remove_light(self, light): + print(f'44444444444444444444444444') + print("\n" + "=" * 50) + print(f"DEBUG: Entering remove_light for light object: {light.casts_shadows}") + if light: + print(f" - Light's Slot: {light.get_slot() if light.has_slot() else 'No Slot'}") + print(f" - Does it cast shadows? light.get_casts_shadows() -> {light.get_casts_shadows()}") + else: + print(" - Light object is None!") + + print("\n --- State of Light System BEFORE removal ---") + if hasattr(self, '_lights') and hasattr(self._lights, '_data'): + # 打印出当前所有灯光对象,看看有没有异常 + print(f" - Light Data Array (_data):") + for i, l_obj in enumerate(self._lights._data): + print(f" [{i}]: {l_obj}") + + print(f"\n - Max Index (_max_index): {self._lights._max_index}") + print(f" - Num Entries (_num_entries): {self._lights._num_entries}") + print("=" * 50 + "\n") + assert light is not None + if not light.has_slot(): + print("ERROR: Could not detach light, light was not attached!") + return + + # 保存必要信息,避免过早清理 + light_slot = light.get_slot() + has_shadows = light.get_casts_shadows() + + # 处理shadow sources(如果有的话) + if has_shadows: + num_sources = light.get_num_shadow_sources() + if num_sources > 0: + # 关键修复:先保存第一个source的slot,再清理 + first_source = light.get_shadow_source(0) + first_source_slot = first_source.get_slot() # 保存slot值 + + # --- 在这里加上打印语句 --- + print(f"DEBUG: Removing shadow sources. Start Slot: {first_source_slot}, Count: {num_sources}") + + # 先发送GPU移除命令(在清理之前) + cmd_remove = GPUCommand(GPUCommand.CMD_remove_sources) + cmd_remove.push_int(first_source_slot) # 使用保存的slot值 + cmd_remove.push_int(num_sources) + self._cmd_list.add_command(cmd_remove) + + # 然后清理所有shadow sources + for i in range(num_sources): + source = light.get_shadow_source(i) + if source.has_slot(): + self._shadow_sources.free_slot(source.get_slot()) + if source.has_region(): + self._shadow_manager.get_atlas().free_region(source.get_region()) + source.clear_region() + + # 清理light的shadow sources + light.clear_shadow_sources() + + # 发送GPU移除light命令 + self.gpu_remove_light(light) + + # 最后释放light slot和清理light信息 + self._lights.free_slot(light_slot) + light.remove_slot() + + def gpu_remove_consecutive_sources(self, first_source, num_sources): + cmd_remove = GPUCommand(GPUCommand.CMD_remove_sources) + cmd_remove.push_int(first_source.get_slot()) + cmd_remove.push_int(num_sources) + self._cmd_list.add_command(cmd_remove) + + def gpu_remove_light(self, light): + cmd_remove = GPUCommand(GPUCommand.CMD_remove_light) + cmd_remove.push_int(light.get_slot()) + self._cmd_list.add_command(cmd_remove) + + def gpu_update_light(self, light): + cmd_update = GPUCommand(GPUCommand.CMD_store_light) + cmd_update.push_int(light.get_slot()) + light.write_to_command(cmd_update) + light.set_needs_update(False) + self._cmd_list.add_command(cmd_update) + + def gpu_update_source(self, source): + cmd_update = GPUCommand(GPUCommand.CMD_store_source) + cmd_update.push_int(source.get_slot()) + source.write_to_command(cmd_update) + self._cmd_list.add_command(cmd_update) + + def update_lights(self): + for light in self._lights.begin(): + if light.get_needs_update(): + if light.casts_shadows: + light.update_shadow_sources() + self.gpu_update_light(light) + + def update_shadow_sources(self): + sources_to_update = [] + + for source in self._shadow_sources.begin(): + # if source and source.get_needs_update(): + # sources_to_update.append(source) + if source: + bounds = source.get_bounds() + distance_to_camera = (self._camera_pos - bounds.get_center()) - bounds.get_radius() + if distance_to_camera < self._shadow_update_distance: + sources_to_update.append(source) + else: + if source.has_region(): + self._shadow_manager.get_atlas().free_region(source.get_region()) + source.clear_region() + + def get_source_score(source): + dist = (source.get_bounds().get_center() - self._camera_pos).length() + return -dist + (10**10 if source.has_region() else 0) + + sorted_sources = list(sorted(sources_to_update, key=get_source_score)) + + atlas = self._shadow_manager.get_atlas() + update_slots = min( + len(sorted_sources), + self._shadow_manager.get_num_update_slots_left()) + + for i in range(update_slots): + if sorted_sources[i].has_region(): + atlas.free_region(sorted_sources[i].get_region()) + + for i in range(update_slots): + source = sorted_sources[i] + + if not self._shadow_manager.add_update(source): + print("ERROR: Shadow manager ensured update slot, but slot is taken!") + break + + region_size = atlas.get_required_tiles(source.get_resolution()) + new_region = atlas.find_and_reserve_region(region_size, region_size) + new_uv_region = atlas.region_to_uv(new_region) + source.set_region(new_region, new_uv_region) + source.set_needs_update(False) + self.gpu_update_source(source) + + def update(self): + self.update_lights() + self.update_shadow_sources() diff --git a/RenderPipelineFile/rpcore/pynative/pointer_slot_storage.py b/RenderPipelineFile/rpcore/pynative/pointer_slot_storage.py index d5e0c3f2..c800c76d 100644 --- a/RenderPipelineFile/rpcore/pynative/pointer_slot_storage.py +++ b/RenderPipelineFile/rpcore/pynative/pointer_slot_storage.py @@ -1,90 +1,90 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rplibs.six.moves import range # pylint: disable=import-error - - -class PointerSlotStorage(object): - - """ Please refer to the native C++ implementation for docstrings and comments. - This is just the python implementation, which does not contain documentation! """ - - def __init__(self, max_size): - self._data = [None] * max_size - self._max_index = 0 - self._num_entries = 0 - - def get_max_index(self): - return self._max_index - - def get_num_entries(self): - return self._num_entries - - def find_slot(self): - # Notice: returns None in case of no free slot, and the slot otherwise, this - # is different to the C++ Module - for i, value in enumerate(self._data): - if not value: - return i - return -1 - - def find_consecutive_slots(self, num_consecutive): - if num_consecutive == 1: - return self.find_slot() - - for i in range(len(self._data)): - any_taken = False - for k in range(num_consecutive): - if self._data[i + k]: - any_taken = True - break - if not any_taken: - return i - return -1 - - def free_slot(self, slot): - self._data[slot] = None - self._num_entries -= 1 - if slot == self._max_index: - while self._max_index >= 0 and not self._data[self._max_index]: - self._max_index -= 1 - - def free_consecutive_slots(self, slot, num_consecutive): - for i in range(num_consecutive): - self.free_slot(slot + i) - - def reserve_slot(self, slot, ptr): - self._max_index = max(self._max_index, slot) - self._data[slot] = ptr - self._num_entries += 1 - - def begin(self): - for i in range(self._max_index + 1): - if self._data[i]: - yield self._data[i] - - def end(self): - raise NotImplementedError("Use .begin() as iterator when using the python side!") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rplibs.six.moves import range # pylint: disable=import-error + + +class PointerSlotStorage(object): + + """ Please refer to the native C++ implementation for docstrings and comments. + This is just the python implementation, which does not contain documentation! """ + + def __init__(self, max_size): + self._data = [None] * max_size + self._max_index = 0 + self._num_entries = 0 + + def get_max_index(self): + return self._max_index + + def get_num_entries(self): + return self._num_entries + + def find_slot(self): + # Notice: returns None in case of no free slot, and the slot otherwise, this + # is different to the C++ Module + for i, value in enumerate(self._data): + if not value: + return i + return -1 + + def find_consecutive_slots(self, num_consecutive): + if num_consecutive == 1: + return self.find_slot() + + for i in range(len(self._data)): + any_taken = False + for k in range(num_consecutive): + if self._data[i + k]: + any_taken = True + break + if not any_taken: + return i + return -1 + + def free_slot(self, slot): + self._data[slot] = None + self._num_entries -= 1 + if slot == self._max_index: + while self._max_index >= 0 and not self._data[self._max_index]: + self._max_index -= 1 + + def free_consecutive_slots(self, slot, num_consecutive): + for i in range(num_consecutive): + self.free_slot(slot + i) + + def reserve_slot(self, slot, ptr): + self._max_index = max(self._max_index, slot) + self._data[slot] = ptr + self._num_entries += 1 + + def begin(self): + for i in range(self._max_index + 1): + if self._data[i]: + yield self._data[i] + + def end(self): + raise NotImplementedError("Use .begin() as iterator when using the python side!") diff --git a/RenderPipelineFile/rpcore/pynative/pssm_camera_rig.py b/RenderPipelineFile/rpcore/pynative/pssm_camera_rig.py index fa488575..de648af6 100644 --- a/RenderPipelineFile/rpcore/pynative/pssm_camera_rig.py +++ b/RenderPipelineFile/rpcore/pynative/pssm_camera_rig.py @@ -1,101 +1,101 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import PTALVecBase2f, PTALMatrix4f, Vec2, OrthographicLens -from panda3d.core import Camera, NodePath, Mat4 - -from rpcore.util.generic import snap_shadow_map - - -class PSSMCameraRig(object): - - """ PSSM is not really supported in python yet (too slow), so this is a stub, - supporting only one cascade """ - - def __init__(self, num_splits): - self._split_count = num_splits - self._mvps = PTALMatrix4f.empty_array(num_splits) - self._nearfar = PTALVecBase2f.empty_array(num_splits) - for i in range(num_splits): - self._nearfar[i] = Vec2(20, 1000) - mat = Mat4() - mat.fill(0) - self._mvps[i] = mat - self._lens = OrthographicLens() - self._lens.set_near_far(20, 1000) - self._lens.set_film_size(100, 100) - self._camera = Camera("PSSMDummy", self._lens) - self._cam_node = NodePath(self._camera) - self._parent = None - self._resolution = 1024 - self._snap_grid = True - self._sun_distance = 500.0 - - def set_resolution(self, resolution): - self._resolution = resolution - - def update(self, cam_node, light_vector): - cam_pos = cam_node.get_pos() - self._cam_node.set_pos(cam_pos + light_vector * self._sun_distance) - self._cam_node.look_at(cam_pos) - - if self._snap_grid: - snap_shadow_map(self.compute_mvp(), self._cam_node, self._resolution) - - self._mvps[0] = self.compute_mvp() - - def compute_mvp(self): - transform = self._parent.get_transform(self._cam_node).get_mat() - return transform * self._lens.get_projection_mat() - - def get_camera(self, index): # pylint: disable=unused-argument - return self._cam_node - - def set_sun_distance(self, dist): - self._sun_distance = dist - self._lens.set_near_far(10, 2 * dist) - - def reparent_to(self, parent): - self._cam_node.reparent_to(parent) - self._parent = parent - - def get_mvp_array(self): - return self._mvps - - def get_nearfar_array(self): - return self._nearfar - - def set_use_stable_csm(self, use_stable): - self._snap_grid = use_stable - - def _stub(self, *args, **kwargs): # pylint: disable=unused-argument - return None - - set_pssm_distance = _stub - set_logarithmic_factor = _stub - set_border_bias = _stub - set_use_fixed_film_size = _stub - reset_film_size_cache = _stub +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import PTALVecBase2f, PTALMatrix4f, Vec2, OrthographicLens +from panda3d.core import Camera, NodePath, Mat4 + +from rpcore.util.generic import snap_shadow_map + + +class PSSMCameraRig(object): + + """ PSSM is not really supported in python yet (too slow), so this is a stub, + supporting only one cascade """ + + def __init__(self, num_splits): + self._split_count = num_splits + self._mvps = PTALMatrix4f.empty_array(num_splits) + self._nearfar = PTALVecBase2f.empty_array(num_splits) + for i in range(num_splits): + self._nearfar[i] = Vec2(20, 1000) + mat = Mat4() + mat.fill(0) + self._mvps[i] = mat + self._lens = OrthographicLens() + self._lens.set_near_far(20, 1000) + self._lens.set_film_size(100, 100) + self._camera = Camera("PSSMDummy", self._lens) + self._cam_node = NodePath(self._camera) + self._parent = None + self._resolution = 1024 + self._snap_grid = True + self._sun_distance = 500.0 + + def set_resolution(self, resolution): + self._resolution = resolution + + def update(self, cam_node, light_vector): + cam_pos = cam_node.get_pos() + self._cam_node.set_pos(cam_pos + light_vector * self._sun_distance) + self._cam_node.look_at(cam_pos) + + if self._snap_grid: + snap_shadow_map(self.compute_mvp(), self._cam_node, self._resolution) + + self._mvps[0] = self.compute_mvp() + + def compute_mvp(self): + transform = self._parent.get_transform(self._cam_node).get_mat() + return transform * self._lens.get_projection_mat() + + def get_camera(self, index): # pylint: disable=unused-argument + return self._cam_node + + def set_sun_distance(self, dist): + self._sun_distance = dist + self._lens.set_near_far(10, 2 * dist) + + def reparent_to(self, parent): + self._cam_node.reparent_to(parent) + self._parent = parent + + def get_mvp_array(self): + return self._mvps + + def get_nearfar_array(self): + return self._nearfar + + def set_use_stable_csm(self, use_stable): + self._snap_grid = use_stable + + def _stub(self, *args, **kwargs): # pylint: disable=unused-argument + return None + + set_pssm_distance = _stub + set_logarithmic_factor = _stub + set_border_bias = _stub + set_use_fixed_film_size = _stub + reset_film_size_cache = _stub diff --git a/RenderPipelineFile/rpcore/pynative/rp_light.py b/RenderPipelineFile/rpcore/pynative/rp_light.py index e823d5ec..01e7f02c 100644 --- a/RenderPipelineFile/rpcore/pynative/rp_light.py +++ b/RenderPipelineFile/rpcore/pynative/rp_light.py @@ -1,208 +1,208 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" -from __future__ import print_function - -from panda3d.core import Vec3, Mat3 - - -def color_from_temperature(temperature): - # Thanks to rdb for this conversion script - mm = 1000.0 / temperature # pylint: disable=invalid-name - mm2 = mm**2 - mm3 = mm2 * mm - x, y = 0, 0 - - if temperature < 4000: - x = -0.2661239 * mm3 - 0.2343580 * mm2 + 0.8776956 * mm + 0.179910 - else: - x = -3.0258469 * mm3 + 2.1070379 * mm2 + 0.2226347 * mm + 0.240390 - - x2 = x**2 # pylint: disable=invalid-name - x3 = x2 * x # pylint: disable=invalid-name - if temperature < 2222: - y = -1.1063814 * x3 - 1.34811020 * x2 + 2.18555832 * x - 0.20219683 - elif temperature < 4000: - y = -0.9549476 * x3 - 1.37418593 * x2 + 2.09137015 * x - 0.16748867 - else: - y = 3.0817580 * x3 - 5.87338670 * x2 + 3.75112997 * x - 0.37001483 - - # xyY to XYZ, assuming Y=1. - xyz = Vec3(x / y, 1, (1 - x - y) / y) - - # Convert XYZ to linearized sRGB. - xyz_to_rgb = Mat3(3.2406, -0.9689, 0.0557, -1.5372, 1.8758, -0.2050, -0.4986, 0.0415, 1.0570) - - return xyz_to_rgb.xform(xyz) - -__all__ = ["RPLight"] - - -class RPLight(object): - - """ Please refer to the native C++ implementation for docstrings and comments. - This is just the python implementation, which does not contain documentation! """ - - LT_empty = 0 - LT_point_light = 1 - LT_spot_light = 2 - - def __init__(self, light_type): - self._light_type = light_type - self._needs_update = False - self._casts_shadows = False - self._slot = -1 - self._position = Vec3(0) - self._color = Vec3(1) - self._ies_profile = -1 - self._source_resolution = 512 - self._near_plane = 0.5 - self._energy = 20 - self._shadow_sources = [] - - def get_num_shadow_sources(self): - return len(self._shadow_sources) - - def get_shadow_source(self, index): - return self._shadow_sources[index] - - def clear_shadow_sources(self): - self._shadow_sources = [] - - def set_needs_update(self, flag): - self._needs_update = flag - - def get_needs_update(self): - return self._needs_update - - def has_slot(self): - return self._slot >= 0 - - def get_slot(self): - return self._slot - - def remove_slot(self): - self._slot = -1 - - def assign_slot(self, slot): - self._slot = slot - - def invalidate_shadows(self): - for source in self._shadow_sources: - source.set_needs_update(True) - - def set_pos(self, *args): - self._position = Vec3(*args) - self.set_needs_update(True) - self.invalidate_shadows() - - def get_pos(self): - return self._position - - pos = property(get_pos, set_pos) - - def set_color(self, *args): - self._color = Vec3(*args) - self._color /= (0.2126 * self._color.x + - 0.7152 * self._color.y + - 0.0722 * self._color.z) - self.set_needs_update(True) - - def set_color_from_temperature(self, temperature): - self.set_color(color_from_temperature(temperature)) - - def get_color(self): - return self._color - - color = property(get_color, set_color) - - def set_energy(self, energy): - self._energy = energy - - def get_energy(self): - return self._energy - - energy = property(get_energy, set_energy) - - def get_light_type(self): - return self._light_type - - light_type = property(get_light_type) - - def set_casts_shadows(self, flag): - if self.has_slot(): - print("Light is already attached!") - return - self._casts_shadows = flag - - def get_casts_shadows(self): - return self._casts_shadows - - casts_shadows = property(get_casts_shadows, set_casts_shadows) - - def set_shadow_map_resolution(self, resolution): - self._source_resolution = resolution - self.invalidate_shadows() - - def get_shadow_map_resolution(self): - return self._source_resolution - - shadow_map_resolution = property(get_shadow_map_resolution, set_shadow_map_resolution) - - def set_ies_profile(self, profile): - self._ies_profile = profile - self.set_needs_update(True) - - def get_ies_profile(self): - return self._ies_profile - - def has_ies_profile(self): - return self._ies_profile >= 0 - - def clear_ies_profile(self): - self.set_ies_profile(-1) - - ies_profile = property(get_ies_profile, set_ies_profile) - - def set_near_plane(self, near_plane): - self._near_plane = near_plane - self.invalidate_shadows() - - def get_near_plane(self): - return self._near_plane - - near_plane = property(get_near_plane, set_near_plane) - - def write_to_command(self, cmd): - cmd.push_int(self._light_type) - cmd.push_int(self._ies_profile) - - if self._casts_shadows: - cmd.push_int(self._shadow_sources[0].get_slot()) - else: - cmd.push_int(-1) - - cmd.push_vec3(self._position) - cmd.push_vec3(self._color * self._energy / 100.0) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +from __future__ import print_function + +from panda3d.core import Vec3, Mat3 + + +def color_from_temperature(temperature): + # Thanks to rdb for this conversion script + mm = 1000.0 / temperature # pylint: disable=invalid-name + mm2 = mm**2 + mm3 = mm2 * mm + x, y = 0, 0 + + if temperature < 4000: + x = -0.2661239 * mm3 - 0.2343580 * mm2 + 0.8776956 * mm + 0.179910 + else: + x = -3.0258469 * mm3 + 2.1070379 * mm2 + 0.2226347 * mm + 0.240390 + + x2 = x**2 # pylint: disable=invalid-name + x3 = x2 * x # pylint: disable=invalid-name + if temperature < 2222: + y = -1.1063814 * x3 - 1.34811020 * x2 + 2.18555832 * x - 0.20219683 + elif temperature < 4000: + y = -0.9549476 * x3 - 1.37418593 * x2 + 2.09137015 * x - 0.16748867 + else: + y = 3.0817580 * x3 - 5.87338670 * x2 + 3.75112997 * x - 0.37001483 + + # xyY to XYZ, assuming Y=1. + xyz = Vec3(x / y, 1, (1 - x - y) / y) + + # Convert XYZ to linearized sRGB. + xyz_to_rgb = Mat3(3.2406, -0.9689, 0.0557, -1.5372, 1.8758, -0.2050, -0.4986, 0.0415, 1.0570) + + return xyz_to_rgb.xform(xyz) + +__all__ = ["RPLight"] + + +class RPLight(object): + + """ Please refer to the native C++ implementation for docstrings and comments. + This is just the python implementation, which does not contain documentation! """ + + LT_empty = 0 + LT_point_light = 1 + LT_spot_light = 2 + + def __init__(self, light_type): + self._light_type = light_type + self._needs_update = False + self._casts_shadows = False + self._slot = -1 + self._position = Vec3(0) + self._color = Vec3(1) + self._ies_profile = -1 + self._source_resolution = 512 + self._near_plane = 0.5 + self._energy = 20 + self._shadow_sources = [] + + def get_num_shadow_sources(self): + return len(self._shadow_sources) + + def get_shadow_source(self, index): + return self._shadow_sources[index] + + def clear_shadow_sources(self): + self._shadow_sources = [] + + def set_needs_update(self, flag): + self._needs_update = flag + + def get_needs_update(self): + return self._needs_update + + def has_slot(self): + return self._slot >= 0 + + def get_slot(self): + return self._slot + + def remove_slot(self): + self._slot = -1 + + def assign_slot(self, slot): + self._slot = slot + + def invalidate_shadows(self): + for source in self._shadow_sources: + source.set_needs_update(True) + + def set_pos(self, *args): + self._position = Vec3(*args) + self.set_needs_update(True) + self.invalidate_shadows() + + def get_pos(self): + return self._position + + pos = property(get_pos, set_pos) + + def set_color(self, *args): + self._color = Vec3(*args) + self._color /= (0.2126 * self._color.x + + 0.7152 * self._color.y + + 0.0722 * self._color.z) + self.set_needs_update(True) + + def set_color_from_temperature(self, temperature): + self.set_color(color_from_temperature(temperature)) + + def get_color(self): + return self._color + + color = property(get_color, set_color) + + def set_energy(self, energy): + self._energy = energy + + def get_energy(self): + return self._energy + + energy = property(get_energy, set_energy) + + def get_light_type(self): + return self._light_type + + light_type = property(get_light_type) + + def set_casts_shadows(self, flag): + if self.has_slot(): + print("Light is already attached!") + return + self._casts_shadows = flag + + def get_casts_shadows(self): + return self._casts_shadows + + casts_shadows = property(get_casts_shadows, set_casts_shadows) + + def set_shadow_map_resolution(self, resolution): + self._source_resolution = resolution + self.invalidate_shadows() + + def get_shadow_map_resolution(self): + return self._source_resolution + + shadow_map_resolution = property(get_shadow_map_resolution, set_shadow_map_resolution) + + def set_ies_profile(self, profile): + self._ies_profile = profile + self.set_needs_update(True) + + def get_ies_profile(self): + return self._ies_profile + + def has_ies_profile(self): + return self._ies_profile >= 0 + + def clear_ies_profile(self): + self.set_ies_profile(-1) + + ies_profile = property(get_ies_profile, set_ies_profile) + + def set_near_plane(self, near_plane): + self._near_plane = near_plane + self.invalidate_shadows() + + def get_near_plane(self): + return self._near_plane + + near_plane = property(get_near_plane, set_near_plane) + + def write_to_command(self, cmd): + cmd.push_int(self._light_type) + cmd.push_int(self._ies_profile) + + if self._casts_shadows: + cmd.push_int(self._shadow_sources[0].get_slot()) + else: + cmd.push_int(-1) + + cmd.push_vec3(self._position) + cmd.push_vec3(self._color * self._energy / 100.0) diff --git a/RenderPipelineFile/rpcore/pynative/rp_point_light.py b/RenderPipelineFile/rpcore/pynative/rp_point_light.py index c3db82cb..69a375c7 100644 --- a/RenderPipelineFile/rpcore/pynative/rp_point_light.py +++ b/RenderPipelineFile/rpcore/pynative/rp_point_light.py @@ -1,82 +1,82 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rplibs.six.moves import range # pylint: disable=import-error - -from panda3d.core import Vec3 - -from rpcore.pynative.rp_light import RPLight -from rpcore.pynative.shadow_source import ShadowSource - - -class RPPointLight(RPLight): - - """ Please refer to the native C++ implementation for docstrings and comments. - This is just the python implementation, which does not contain documentation! """ - - def __init__(self): - RPLight.__init__(self, RPLight.LT_point_light) - self._radius = 10.0 - self._inner_radius = 0.01 - - def write_to_command(self, cmd): - RPLight.write_to_command(self, cmd) - cmd.push_float(self._radius) - cmd.push_float(self._inner_radius) - - def set_radius(self, radius): - self._radius = radius - self.set_needs_update(True) - self.invalidate_shadows() - - def get_radius(self): - return self._radius - - radius = property(get_radius, set_radius) - - def set_inner_radius(self, inner_radius): - assert inner_radius >= 0.01 - self._inner_radius = inner_radius - self.set_needs_update(True) - - def get_inner_radius(self): - return self._inner_radius - - inner_radius = property(get_inner_radius, set_inner_radius) - - def init_shadow_sources(self): - for _ in range(6): - self._shadow_sources.append(ShadowSource()) - - def update_shadow_sources(self): - directions = (Vec3(1, 0, 0), Vec3(-1, 0, 0), Vec3(0, 1, 0), - Vec3(0, -1, 0), Vec3(0, 0, 1), Vec3(0, 0, -1)) - - fov = 90.0 + 3.0 - for i, source in enumerate(self._shadow_sources): - source.set_resolution(self.get_shadow_map_resolution()) - source.set_perspective_lens( - fov, self._near_plane, self._radius, self._position, directions[i]) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rplibs.six.moves import range # pylint: disable=import-error + +from panda3d.core import Vec3 + +from rpcore.pynative.rp_light import RPLight +from rpcore.pynative.shadow_source import ShadowSource + + +class RPPointLight(RPLight): + + """ Please refer to the native C++ implementation for docstrings and comments. + This is just the python implementation, which does not contain documentation! """ + + def __init__(self): + RPLight.__init__(self, RPLight.LT_point_light) + self._radius = 10.0 + self._inner_radius = 0.01 + + def write_to_command(self, cmd): + RPLight.write_to_command(self, cmd) + cmd.push_float(self._radius) + cmd.push_float(self._inner_radius) + + def set_radius(self, radius): + self._radius = radius + self.set_needs_update(True) + self.invalidate_shadows() + + def get_radius(self): + return self._radius + + radius = property(get_radius, set_radius) + + def set_inner_radius(self, inner_radius): + assert inner_radius >= 0.01 + self._inner_radius = inner_radius + self.set_needs_update(True) + + def get_inner_radius(self): + return self._inner_radius + + inner_radius = property(get_inner_radius, set_inner_radius) + + def init_shadow_sources(self): + for _ in range(6): + self._shadow_sources.append(ShadowSource()) + + def update_shadow_sources(self): + directions = (Vec3(1, 0, 0), Vec3(-1, 0, 0), Vec3(0, 1, 0), + Vec3(0, -1, 0), Vec3(0, 0, 1), Vec3(0, 0, -1)) + + fov = 90.0 + 3.0 + for i, source in enumerate(self._shadow_sources): + source.set_resolution(self.get_shadow_map_resolution()) + source.set_perspective_lens( + fov, self._near_plane, self._radius, self._position, directions[i]) diff --git a/RenderPipelineFile/rpcore/pynative/rp_spot_light.py b/RenderPipelineFile/rpcore/pynative/rp_spot_light.py index d9155fc6..1dcf14c1 100644 --- a/RenderPipelineFile/rpcore/pynative/rp_spot_light.py +++ b/RenderPipelineFile/rpcore/pynative/rp_spot_light.py @@ -1,92 +1,92 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -import math -from panda3d.core import Vec3 - -from rpcore.pynative.shadow_source import ShadowSource -from rpcore.pynative.rp_light import RPLight - - -class RPSpotLight(RPLight): - - """ Please refer to the native C++ implementation for docstrings and comments. - This is just the python implementation, which does not contain documentation! """ - - def __init__(self): - RPLight.__init__(self, RPLight.LT_spot_light) - self._radius = 10.0 - self._fov = 45.0 - self._direction = Vec3(0, 0, -1) - - def write_to_command(self, cmd): - RPLight.write_to_command(self, cmd) - cmd.push_float(self._radius) - cmd.push_float(math.cos(self._fov / 360.0 * math.pi)) - cmd.push_vec3(self._direction) - - def set_radius(self, radius): - self._radius = radius - self.set_needs_update(True) - self.invalidate_shadows() - - def get_radius(self): - return self._radius - - radius = property(get_radius, set_radius) - - def set_fov(self, fov): - self._fov = fov - self.set_needs_update(True) - self.invalidate_shadows() - - def get_fov(self): - return self._fov - - fov = property(get_fov, set_fov) - - def set_direction(self, *args): - self._direction = Vec3(*args) - self._direction.normalize() - self.set_needs_update(True) - self.invalidate_shadows() - - def get_direction(self): - return self._direction - - direction = property(get_direction, set_direction) - - def look_at(self, *args): - vec = Vec3(*args) - self._position - self.set_direction(vec) - - def init_shadow_sources(self): - self._shadow_sources.append(ShadowSource()) - - def update_shadow_sources(self): - self._shadow_sources[0].set_resolution(self.get_shadow_map_resolution()) - self._shadow_sources[0].set_perspective_lens( - self._fov, self._near_plane, self._radius, self._position, self._direction) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +import math +from panda3d.core import Vec3 + +from rpcore.pynative.shadow_source import ShadowSource +from rpcore.pynative.rp_light import RPLight + + +class RPSpotLight(RPLight): + + """ Please refer to the native C++ implementation for docstrings and comments. + This is just the python implementation, which does not contain documentation! """ + + def __init__(self): + RPLight.__init__(self, RPLight.LT_spot_light) + self._radius = 10.0 + self._fov = 45.0 + self._direction = Vec3(0, 0, -1) + + def write_to_command(self, cmd): + RPLight.write_to_command(self, cmd) + cmd.push_float(self._radius) + cmd.push_float(math.cos(self._fov / 360.0 * math.pi)) + cmd.push_vec3(self._direction) + + def set_radius(self, radius): + self._radius = radius + self.set_needs_update(True) + self.invalidate_shadows() + + def get_radius(self): + return self._radius + + radius = property(get_radius, set_radius) + + def set_fov(self, fov): + self._fov = fov + self.set_needs_update(True) + self.invalidate_shadows() + + def get_fov(self): + return self._fov + + fov = property(get_fov, set_fov) + + def set_direction(self, *args): + self._direction = Vec3(*args) + self._direction.normalize() + self.set_needs_update(True) + self.invalidate_shadows() + + def get_direction(self): + return self._direction + + direction = property(get_direction, set_direction) + + def look_at(self, *args): + vec = Vec3(*args) - self._position + self.set_direction(vec) + + def init_shadow_sources(self): + self._shadow_sources.append(ShadowSource()) + + def update_shadow_sources(self): + self._shadow_sources[0].set_resolution(self.get_shadow_map_resolution()) + self._shadow_sources[0].set_perspective_lens( + self._fov, self._near_plane, self._radius, self._position, self._direction) diff --git a/RenderPipelineFile/rpcore/pynative/shadow_atlas.py b/RenderPipelineFile/rpcore/pynative/shadow_atlas.py index 0cbdf3f1..37298a71 100644 --- a/RenderPipelineFile/rpcore/pynative/shadow_atlas.py +++ b/RenderPipelineFile/rpcore/pynative/shadow_atlas.py @@ -1,100 +1,100 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import print_function, division -from rplibs.six.moves import range # pylint: disable=import-error - -from panda3d.core import LVecBase4i, LVecBase4 - - -class ShadowAtlas(object): - - """ Please refer to the native C++ implementation for docstrings and comments. - This is just the python implementation, which does not contain documentation! """ - - def __init__(self, size, tile_size=32): - self._size = size - self._tile_size = tile_size - self._num_used_tiles = 0 - self.init_tiles() - - def init_tiles(self): - self._num_tiles = self._size // self._tile_size - - def row(): - return [False for i in range(self._num_tiles)] # pylint: disable=unused-variable - self._flags = [row() for j in range(self._num_tiles)] # pylint: disable=unused-variable - - def get_num_used_tiles(self): - return self._num_used_tiles - - num_used_tiles = property(get_num_used_tiles) - - def get_coverage(self): - return self._num_used_tiles / float(self._num_tiles ** 2) - - coverage = property(get_coverage) - - def reserve_region(self, x, y, w, h): - self._num_used_tiles += w * h - for x_offset in range(w): - for y_offset in range(h): - self._flags[x + x_offset][y + y_offset] = True - - def find_and_reserve_region(self, tile_width, tile_height): - for x in range(self._num_tiles - tile_height + 1): - for y in range(self._num_tiles - tile_width + 1): - if self.region_is_free(x, y, tile_width, tile_height): - self.reserve_region(x, y, tile_width, tile_height) - return LVecBase4i(x, y, tile_width, tile_height) - print("Failed to find a free region of size", tile_width, "x", tile_height) - return LVecBase4i(-1) - - def free_region(self, region): - self._num_used_tiles -= region.z * region.w - for x in range(region.z): - for y in range(region.w): - self._flags[region.x + x][region.y + y] = False - - def get_tile_size(self): - return self._tile_size - - def region_is_free(self, x, y, w, h): - for x_offset in range(w): - for y_offset in range(h): - if self._flags[x + x_offset][y + y_offset]: - return False - return True - - def get_required_tiles(self, resolution): - if resolution % self._tile_size != 0: - print("ShadowAtlas: Invalid atlas resolution!") - return - return resolution // self._tile_size - - def region_to_uv(self, region): - flt = LVecBase4(region.x, region.y, region.z, region.w) - return flt * (self._tile_size / self._size) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import print_function, division +from rplibs.six.moves import range # pylint: disable=import-error + +from panda3d.core import LVecBase4i, LVecBase4 + + +class ShadowAtlas(object): + + """ Please refer to the native C++ implementation for docstrings and comments. + This is just the python implementation, which does not contain documentation! """ + + def __init__(self, size, tile_size=32): + self._size = size + self._tile_size = tile_size + self._num_used_tiles = 0 + self.init_tiles() + + def init_tiles(self): + self._num_tiles = self._size // self._tile_size + + def row(): + return [False for i in range(self._num_tiles)] # pylint: disable=unused-variable + self._flags = [row() for j in range(self._num_tiles)] # pylint: disable=unused-variable + + def get_num_used_tiles(self): + return self._num_used_tiles + + num_used_tiles = property(get_num_used_tiles) + + def get_coverage(self): + return self._num_used_tiles / float(self._num_tiles ** 2) + + coverage = property(get_coverage) + + def reserve_region(self, x, y, w, h): + self._num_used_tiles += w * h + for x_offset in range(w): + for y_offset in range(h): + self._flags[x + x_offset][y + y_offset] = True + + def find_and_reserve_region(self, tile_width, tile_height): + for x in range(self._num_tiles - tile_height + 1): + for y in range(self._num_tiles - tile_width + 1): + if self.region_is_free(x, y, tile_width, tile_height): + self.reserve_region(x, y, tile_width, tile_height) + return LVecBase4i(x, y, tile_width, tile_height) + print("Failed to find a free region of size", tile_width, "x", tile_height) + return LVecBase4i(-1) + + def free_region(self, region): + self._num_used_tiles -= region.z * region.w + for x in range(region.z): + for y in range(region.w): + self._flags[region.x + x][region.y + y] = False + + def get_tile_size(self): + return self._tile_size + + def region_is_free(self, x, y, w, h): + for x_offset in range(w): + for y_offset in range(h): + if self._flags[x + x_offset][y + y_offset]: + return False + return True + + def get_required_tiles(self, resolution): + if resolution % self._tile_size != 0: + print("ShadowAtlas: Invalid atlas resolution!") + return + return resolution // self._tile_size + + def region_to_uv(self, region): + flt = LVecBase4(region.x, region.y, region.z, region.w) + return flt * (self._tile_size / self._size) diff --git a/RenderPipelineFile/rpcore/pynative/shadow_manager.py b/RenderPipelineFile/rpcore/pynative/shadow_manager.py index bd9969ae..00fc1720 100644 --- a/RenderPipelineFile/rpcore/pynative/shadow_manager.py +++ b/RenderPipelineFile/rpcore/pynative/shadow_manager.py @@ -1,123 +1,123 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import print_function - -from rplibs.six.moves import range # pylint: disable=import-error - -from panda3d.core import Camera, MatrixLens - -from rpcore.pynative.shadow_atlas import ShadowAtlas - - -class ShadowManager(object): - - """ Please refer to the native C++ implementation for docstrings and comments. - This is just the python implementation, which does not contain documentation! """ - - def __init__(self): - self._max_updates = 10 - self._atlas = None - self._atlas_size = 4096 - self._tag_state_mgr = None - self._atlas_graphics_output = None - self._display_regions = [] - self._queued_updates = [] - self._cameras = [] - self._camera_nps = [] - - def set_max_updates(self, max_updates): - if max_updates == 0: - print("Warning: max_updates set to 0, no shadow updates will happen") - self._max_updates = max_updates - - def set_atlas_size(self, atlas_size): - self._atlas_size = atlas_size - - def get_atlas_size(self): - return self._atlas_size - - atlas_size = property(get_atlas_size, set_atlas_size) - - def set_scene(self, scene_parent): - self._scene_parent = scene_parent - - def set_tag_state_manager(self, tag_mgr): - self._tag_state_mgr = tag_mgr - - def set_atlas_graphics_output(self, graphics_output): - self._atlas_graphics_output = graphics_output - - def get_num_update_slots_left(self): - return self._max_updates - len(self._queued_updates) - - num_update_slots_left = property(get_num_update_slots_left) - - def get_atlas(self): - return self._atlas - - atlas = property(get_atlas) - - def init(self): - for i in range(self._max_updates): - camera = Camera("ShadowCam-" + str(i)) - camera.set_lens(MatrixLens()) - camera.set_active(False) - camera.set_scene(self._scene_parent) - self._tag_state_mgr.register_camera("shadow", camera) - self._camera_nps.append(self._scene_parent.attach_new_node(camera)) - self._cameras.append(camera) - - region = self._atlas_graphics_output.make_display_region() - region.set_sort(1000) - region.set_clear_depth_active(True) - region.set_clear_depth(1.0) - region.set_clear_color_active(False) - region.set_camera(self._camera_nps[i]) - region.set_active(False) - self._display_regions.append(region) - - self._atlas = ShadowAtlas(self._atlas_size) - - def update(self): - for i in range(len(self._queued_updates), self._max_updates): - self._cameras[i].set_active(False) - self._display_regions[i].set_active(False) - - for i, source in enumerate(self._queued_updates): - self._cameras[i].set_active(True) - self._display_regions[i].set_active(True) - self._cameras[i].get_lens().set_user_mat(source.get_mvp()) - uv = source.get_uv_region() - self._display_regions[i].set_dimensions(uv.x, uv.x + uv.z, uv.y, uv.y + uv.w) - - self._queued_updates = [] - - def add_update(self, source): - if len(self._queued_updates) >= self._max_updates: - return False - self._queued_updates.append(source) - return True +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import print_function + +from rplibs.six.moves import range # pylint: disable=import-error + +from panda3d.core import Camera, MatrixLens + +from rpcore.pynative.shadow_atlas import ShadowAtlas + + +class ShadowManager(object): + + """ Please refer to the native C++ implementation for docstrings and comments. + This is just the python implementation, which does not contain documentation! """ + + def __init__(self): + self._max_updates = 10 + self._atlas = None + self._atlas_size = 4096 + self._tag_state_mgr = None + self._atlas_graphics_output = None + self._display_regions = [] + self._queued_updates = [] + self._cameras = [] + self._camera_nps = [] + + def set_max_updates(self, max_updates): + if max_updates == 0: + print("Warning: max_updates set to 0, no shadow updates will happen") + self._max_updates = max_updates + + def set_atlas_size(self, atlas_size): + self._atlas_size = atlas_size + + def get_atlas_size(self): + return self._atlas_size + + atlas_size = property(get_atlas_size, set_atlas_size) + + def set_scene(self, scene_parent): + self._scene_parent = scene_parent + + def set_tag_state_manager(self, tag_mgr): + self._tag_state_mgr = tag_mgr + + def set_atlas_graphics_output(self, graphics_output): + self._atlas_graphics_output = graphics_output + + def get_num_update_slots_left(self): + return self._max_updates - len(self._queued_updates) + + num_update_slots_left = property(get_num_update_slots_left) + + def get_atlas(self): + return self._atlas + + atlas = property(get_atlas) + + def init(self): + for i in range(self._max_updates): + camera = Camera("ShadowCam-" + str(i)) + camera.set_lens(MatrixLens()) + camera.set_active(False) + camera.set_scene(self._scene_parent) + self._tag_state_mgr.register_camera("shadow", camera) + self._camera_nps.append(self._scene_parent.attach_new_node(camera)) + self._cameras.append(camera) + + region = self._atlas_graphics_output.make_display_region() + region.set_sort(1000) + region.set_clear_depth_active(True) + region.set_clear_depth(1.0) + region.set_clear_color_active(False) + region.set_camera(self._camera_nps[i]) + region.set_active(False) + self._display_regions.append(region) + + self._atlas = ShadowAtlas(self._atlas_size) + + def update(self): + for i in range(len(self._queued_updates), self._max_updates): + self._cameras[i].set_active(False) + self._display_regions[i].set_active(False) + + for i, source in enumerate(self._queued_updates): + self._cameras[i].set_active(True) + self._display_regions[i].set_active(True) + self._cameras[i].get_lens().set_user_mat(source.get_mvp()) + uv = source.get_uv_region() + self._display_regions[i].set_dimensions(uv.x, uv.x + uv.z, uv.y, uv.y + uv.w) + + self._queued_updates = [] + + def add_update(self, source): + if len(self._queued_updates) >= self._max_updates: + return False + self._queued_updates.append(source) + return True diff --git a/RenderPipelineFile/rpcore/pynative/shadow_source.py b/RenderPipelineFile/rpcore/pynative/shadow_source.py index 9a0d3a73..86452b8c 100644 --- a/RenderPipelineFile/rpcore/pynative/shadow_source.py +++ b/RenderPipelineFile/rpcore/pynative/shadow_source.py @@ -1,108 +1,108 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import Mat4, PerspectiveLens, LVector3, LVecBase2i, LVecBase2f -from panda3d.core import BoundingSphere - - -class ShadowSource(object): - - """ Please refer to the native C++ implementation for docstrings and comments. - This is just the python implementation, which does not contain documentation! """ - - def __init__(self): - self._slot = -1 - self._needs_update = True - self._resolution = 512 - self._mvp = 0.0 - self._region = LVecBase2i(-1) - self._region_uv = LVecBase2f(0.0) - self._bounds = BoundingSphere() - - def set_resolution(self, resolution): - self._resolution = resolution - - def set_needs_update(self, flag): - self._needs_update = flag - - def set_slot(self, slot): - self._slot = slot - - def set_region(self, region, region_uv): - self._region = region - self._region_uv = region_uv - - def clear_region(self): - self._region = LVecBase2i(-1) - self._region_uv = LVecBase2f(0.0) - - def get_bounds(self): - return self._bounds - - def set_perspective_lens(self, fov, near_plane, far_plane, pos, direction): - transform_mat = Mat4.translate_mat(-pos) - temp_lens = PerspectiveLens(fov, fov) - temp_lens.set_film_offset(0, 0) - temp_lens.set_near_far(near_plane, far_plane) - temp_lens.set_view_vector(direction, LVector3.up()) - self.set_matrix_lens(transform_mat * temp_lens.get_projection_mat()) - - hexahedron = temp_lens.make_bounds() - center = (hexahedron.get_min() + hexahedron.get_max()) * 0.5 - self._bounds = BoundingSphere(pos + center, (hexahedron.get_max() - center).length()) - - def set_matrix_lens(self, mvp): - self._mvp = mvp - self.set_needs_update(True) - - def has_region(self): - return (self._region.x >= 0 and self._region.y >= 0 and - self._region.z >= 0 and self._region.w >= 0) - - def has_slot(self): - return self._slot >= 0 - - def get_slot(self): - return self._slot - - def get_needs_update(self): - return not self.has_region or self._needs_update - - def get_resolution(self): - return self._resolution - - def get_mvp(self): - return self._mvp - - def get_region(self): - return self._region - - def get_uv_region(self): - return self._region_uv - - def write_to_command(self, cmd): - cmd.push_mat4(self._mvp) - cmd.push_vec4(self._region_uv) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import Mat4, PerspectiveLens, LVector3, LVecBase2i, LVecBase2f +from panda3d.core import BoundingSphere + + +class ShadowSource(object): + + """ Please refer to the native C++ implementation for docstrings and comments. + This is just the python implementation, which does not contain documentation! """ + + def __init__(self): + self._slot = -1 + self._needs_update = True + self._resolution = 512 + self._mvp = 0.0 + self._region = LVecBase2i(-1) + self._region_uv = LVecBase2f(0.0) + self._bounds = BoundingSphere() + + def set_resolution(self, resolution): + self._resolution = resolution + + def set_needs_update(self, flag): + self._needs_update = flag + + def set_slot(self, slot): + self._slot = slot + + def set_region(self, region, region_uv): + self._region = region + self._region_uv = region_uv + + def clear_region(self): + self._region = LVecBase2i(-1) + self._region_uv = LVecBase2f(0.0) + + def get_bounds(self): + return self._bounds + + def set_perspective_lens(self, fov, near_plane, far_plane, pos, direction): + transform_mat = Mat4.translate_mat(-pos) + temp_lens = PerspectiveLens(fov, fov) + temp_lens.set_film_offset(0, 0) + temp_lens.set_near_far(near_plane, far_plane) + temp_lens.set_view_vector(direction, LVector3.up()) + self.set_matrix_lens(transform_mat * temp_lens.get_projection_mat()) + + hexahedron = temp_lens.make_bounds() + center = (hexahedron.get_min() + hexahedron.get_max()) * 0.5 + self._bounds = BoundingSphere(pos + center, (hexahedron.get_max() - center).length()) + + def set_matrix_lens(self, mvp): + self._mvp = mvp + self.set_needs_update(True) + + def has_region(self): + return (self._region.x >= 0 and self._region.y >= 0 and + self._region.z >= 0 and self._region.w >= 0) + + def has_slot(self): + return self._slot >= 0 + + def get_slot(self): + return self._slot + + def get_needs_update(self): + return not self.has_region or self._needs_update + + def get_resolution(self): + return self._resolution + + def get_mvp(self): + return self._mvp + + def get_region(self): + return self._region + + def get_uv_region(self): + return self._region_uv + + def write_to_command(self, cmd): + cmd.push_mat4(self._mvp) + cmd.push_vec4(self._region_uv) diff --git a/RenderPipelineFile/rpcore/pynative/tag_state_manager.py b/RenderPipelineFile/rpcore/pynative/tag_state_manager.py index 1e53a5bf..0cbdc6e2 100644 --- a/RenderPipelineFile/rpcore/pynative/tag_state_manager.py +++ b/RenderPipelineFile/rpcore/pynative/tag_state_manager.py @@ -1,101 +1,101 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import print_function -from panda3d.core import RenderState, ColorWriteAttrib, ShaderAttrib, BitMask32 - -from rplibs.six import itervalues - - -class TagStateManager(object): - - """ Please refer to the native C++ implementation for docstrings and comments. - This is just the python implementation, which does not contain documentation! """ - - class StateContainer(object): # pylint: disable=too-few-public-methods - def __init__(self, tag_name, mask, write_color): - self.cameras = [] - self.tag_states = {} - self.tag_name = tag_name - self.mask = BitMask32.bit(mask) - self.write_color = write_color - - def __init__(self, main_cam_node): - self._main_cam_node = main_cam_node - self._main_cam_node.node().set_camera_mask(BitMask32.bit(1)) - self.containers = { - "shadow": self.StateContainer("Shadows", 2, False), - "voxelize": self.StateContainer("Voxelize", 3, False), - "envmap": self.StateContainer("Envmap", 4, True), - "forward": self.StateContainer("Forward", 5, True), - } - - def get_mask(self, container_name): - if container_name == "gbuffer": - return BitMask32.bit(1) - return self.containers[container_name].mask - - def apply_state(self, container_name, np, shader, name, sort): - assert shader - state = RenderState.make_empty() - container = self.containers[container_name] - - if not container.write_color: - state = state.set_attrib(ColorWriteAttrib.make(ColorWriteAttrib.C_off), 10000) - - state = state.set_attrib(ShaderAttrib.make(shader, sort), sort) - container.tag_states[name] = state - np.set_tag(container.tag_name, name) - - for camera in container.cameras: - camera.set_tag_state(name, state) - - def cleanup_states(self): - self._main_cam_node.node().clear_tag_states() - for container in itervalues(self.containers): - for camera in container.cameras: - camera.clear_tag_states() - container.tag_states = {} - - def register_camera(self, container_name, source): - container = self.containers[container_name] - source.set_tag_state_key(container.tag_name) - source.set_camera_mask(container.mask) - state = RenderState.make_empty() - if not container.write_color: - state = state.set_attrib(ColorWriteAttrib.make(ColorWriteAttrib.C_off), 10000) - source.set_initial_state(state) - container.cameras.append(source) - - def unregister_camera(self, container_name, source): - container = self.containers[container_name] - if source not in container.cameras: - print("Could not remove source, was never attached!") - return - - container.cameras.remove(source) - source.clear_tag_states() - source.set_initial_state(RenderState.make_empty()) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import print_function +from panda3d.core import RenderState, ColorWriteAttrib, ShaderAttrib, BitMask32 + +from rplibs.six import itervalues + + +class TagStateManager(object): + + """ Please refer to the native C++ implementation for docstrings and comments. + This is just the python implementation, which does not contain documentation! """ + + class StateContainer(object): # pylint: disable=too-few-public-methods + def __init__(self, tag_name, mask, write_color): + self.cameras = [] + self.tag_states = {} + self.tag_name = tag_name + self.mask = BitMask32.bit(mask) + self.write_color = write_color + + def __init__(self, main_cam_node): + self._main_cam_node = main_cam_node + self._main_cam_node.node().set_camera_mask(BitMask32.bit(1)) + self.containers = { + "shadow": self.StateContainer("Shadows", 2, False), + "voxelize": self.StateContainer("Voxelize", 3, False), + "envmap": self.StateContainer("Envmap", 4, True), + "forward": self.StateContainer("Forward", 5, True), + } + + def get_mask(self, container_name): + if container_name == "gbuffer": + return BitMask32.bit(1) + return self.containers[container_name].mask + + def apply_state(self, container_name, np, shader, name, sort): + assert shader + state = RenderState.make_empty() + container = self.containers[container_name] + + if not container.write_color: + state = state.set_attrib(ColorWriteAttrib.make(ColorWriteAttrib.C_off), 10000) + + state = state.set_attrib(ShaderAttrib.make(shader, sort), sort) + container.tag_states[name] = state + np.set_tag(container.tag_name, name) + + for camera in container.cameras: + camera.set_tag_state(name, state) + + def cleanup_states(self): + self._main_cam_node.node().clear_tag_states() + for container in itervalues(self.containers): + for camera in container.cameras: + camera.clear_tag_states() + container.tag_states = {} + + def register_camera(self, container_name, source): + container = self.containers[container_name] + source.set_tag_state_key(container.tag_name) + source.set_camera_mask(container.mask) + state = RenderState.make_empty() + if not container.write_color: + state = state.set_attrib(ColorWriteAttrib.make(ColorWriteAttrib.C_off), 10000) + source.set_initial_state(state) + container.cameras.append(source) + + def unregister_camera(self, container_name, source): + container = self.containers[container_name] + if source not in container.cameras: + print("Could not remove source, was never attached!") + return + + container.cameras.remove(source) + source.clear_tag_states() + source.set_initial_state(RenderState.make_empty()) diff --git a/RenderPipelineFile/rpcore/render_pipeline.py b/RenderPipelineFile/rpcore/render_pipeline.py index afe22c13..0ea88f3a 100644 --- a/RenderPipelineFile/rpcore/render_pipeline.py +++ b/RenderPipelineFile/rpcore/render_pipeline.py @@ -1,699 +1,699 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import division - -import sys -import math -import time - -from panda3d.core import LVecBase2i, TransformState, RenderState, load_prc_file -from panda3d.core import PandaSystem, MaterialAttrib, WindowProperties -from panda3d.core import GeomTristrips, Vec4 - -from direct.showbase.ShowBase import ShowBase -from direct.stdpy.file import isfile - -from rplibs.yaml import load_yaml_file_flat -from six.moves import range # pylint: disable=import-error - -from rpcore.globals import Globals -from rpcore.effect import Effect -from rpcore.rpobject import RPObject -from rpcore.common_resources import CommonResources -from rpcore.native import TagStateManager, PointLight, SpotLight -from rpcore.render_target import RenderTarget -from rpcore.pluginbase.manager import PluginManager -from rpcore.pluginbase.day_manager import DayTimeManager - -from rpcore.util.task_scheduler import TaskScheduler -from rpcore.util.network_communication import NetworkCommunication -from rpcore.util.ies_profile_loader import IESProfileLoader - -from rpcore.gui.debugger import Debugger -from rpcore.gui.loading_screen import LoadingScreen - -from rpcore.mount_manager import MountManager -from rpcore.stage_manager import StageManager -from rpcore.light_manager import LightManager - -from rpcore.stages.ambient_stage import AmbientStage -from rpcore.stages.gbuffer_stage import GBufferStage -from rpcore.stages.final_stage import FinalStage -from rpcore.stages.downscale_z_stage import DownscaleZStage -from rpcore.stages.combine_velocity_stage import CombineVelocityStage -from rpcore.stages.upscale_stage import UpscaleStage - - -class RenderPipeline(RPObject): - - """ This is the main render pipeline class, it combines all components of - the pipeline to form a working system. It does not do much work itself, but - instead setups all the managers and systems to be able to do their work. """ - - def __init__(self): - """ Creates a new pipeline with a given showbase instance. This should - be done before intializing the ShowBase, the pipeline will take care of - that. If the showbase has been initialized before, have a look at - the alternative initialization of the render pipeline (the first sample).""" - RPObject.__init__(self) - self._analyze_system() - self.mount_mgr = MountManager(self) - self.settings = {} - self._applied_effects = [] - self._pre_showbase_initialized = False - self._first_frame = None - #self.set_loading_screen_image("/$$rp/data/gui/loading_screen_bg.txo") - self.set_loading_screen_image("/$$rp/data/gui/Default_Rough.png") - - def load_settings(self, path): - """ Loads the pipeline configuration from a given filename. Usually - this is the 'config/pipeline.ini' file. If you call this more than once, - only the settings of the last file will be used. """ - self.settings = load_yaml_file_flat(path) - - def reload_shaders(self): - """ Reloads all shaders. This will reload the shaders of all plugins, - as well as the pipelines internally used shaders. Because of the - complexity of some shaders, this operation take might take several - seconds. Also notice that all applied effects will be lost, and instead - the default effect will be set on all elements again. Due to this fact, - this method is primarly useful for fast iterations when developing new - shaders. """ - if self.settings["pipeline.display_debugger"]: - self.debug("Reloading shaders ..") - self.debugger.error_msg_handler.clear_messages() - self.debugger.set_reload_hint_visible(True) - self._showbase.graphicsEngine.render_frame() - self._showbase.graphicsEngine.render_frame() - self.tag_mgr.cleanup_states() - self.stage_mgr.reload_shaders() - self.light_mgr.reload_shaders() - self._set_default_effect() - self.plugin_mgr.trigger_hook("shader_reload") - if self.settings["pipeline.display_debugger"]: - self.debugger.set_reload_hint_visible(False) - self._apply_custom_shaders() - - def _apply_custom_shaders(self): - """ Re-applies all custom shaders the user applied, to avoid them getting - removed when the shaders are reloaded """ - self.debug("Re-applying", len(self._applied_effects), "custom shaders") - for args in self._applied_effects: - self._internal_set_effect(*args) - - def pre_showbase_init(self): - """ Setups all required pipeline settings and configuration which have - to be set before the showbase is setup. This is called by create(), - in case the showbase was not initialized, however you can (and have to) - call it manually before you init your custom showbase instance. - See the 00-Loading the pipeline sample for more information. """ - if not self.mount_mgr.is_mounted: - self.debug("Mount manager was not mounted, mounting now ...") - self.mount_mgr.mount() - - if not self.settings: - self.debug("No settings loaded, loading from default location") - self.load_settings("/$$rpconfig/pipeline.yaml") - - if not isfile("/$$rp/data/install.flag"): - self.fatal("You didn't setup the pipeline yet! Please run setup.py.") - - load_prc_file("/$$rpconfig/Meta3d-config.prc") - self._pre_showbase_initialized = True - - def create(self, base=None): - """ This creates the pipeline, and setups all buffers. It also - constructs the showbase. The settings should have been loaded before - calling this, and also the base and write path should have been - initialized properly (see MountManager). - - If base is None, the showbase used in the RenderPipeline constructor - will be used and initialized. Otherwise it is assumed that base is an - initialized ShowBase object. In this case, you should call - pre_showbase_init() before initializing the ShowBase""" - - start_time = time.time() - self._init_showbase(base) - - if not self._showbase.win.gsg.supports_compute_shaders: - self.fatal( - "Sorry, your GPU does not support compute shaders! Make sure\n" - "you have the latest drivers. If you already have, your gpu might\n" - "be too old, or you might be using the open source drivers on linux.") - - self._init_globals() - self.loading_screen.create() - self._adjust_camera_settings() - self._create_managers() - self.plugin_mgr.load() - self.daytime_mgr.load_settings() - self.common_resources.write_config() - self._init_debugger() - - self.plugin_mgr.trigger_hook("stage_setup") - self.plugin_mgr.trigger_hook("post_stage_setup") - - self._create_common_defines() - self._initialize_managers() - self._create_default_skybox() - - self.plugin_mgr.trigger_hook("pipeline_created") - - self._listener = NetworkCommunication(self) - self._set_default_effect() - - # Measure how long it took to initialize everything, and also store - # when we finished, so we can measure how long it took to render the - # first frame (where the shaders are actually compiled) - init_duration = (time.time() - start_time) - self._first_frame = time.process_time() - self.debug("Finished initialization in {:3.3f} s, first frame: {}".format( - init_duration, Globals.clock.get_frame_count())) - - def set_loading_screen_image(self, image_source): - """ Tells the pipeline to use the default loading screen, which consists - of a simple loading image. The image source should be a fullscreen - 16:9 image, and not too small, to avoid being blurred out. """ - self.loading_screen = LoadingScreen(self, image_source) - - def add_light(self, light): - """ Adds a new light to the rendered lights, check out the LightManager - add_light documentation for further information. """ - self.light_mgr.add_light(light) - - def remove_light(self, light): - """ Removes a previously attached light, check out the LightManager - remove_light documentation for further information. """ - self.light_mgr.remove_light(light) - - def load_ies_profile(self, filename): - """ Loads an IES profile from a given filename and returns a handle which - can be used to set an ies profile on a light """ - return self.ies_loader.load(filename) - - def _internal_set_effect(self, nodepath, effect_src, options=None, sort=30): - """ Sets an effect to the given object, using the specified options. - Check out the effect documentation for more information about possible - options and configurations. The object should be a nodepath, and the - effect will be applied to that nodepath and all nodepaths below whose - current effect sort is less than the new effect sort (passed by the - sort parameter). """ - effect = Effect.load(effect_src, options) - if effect is None: - return self.error("Could not apply effect") - - for i, stage in enumerate(("gbuffer", "shadow", "voxelize", "envmap", "forward")): - if not effect.get_option("render_" + stage): - nodepath.hide(self.tag_mgr.get_mask(stage)) - else: - shader = effect.get_shader_obj(stage) - if stage == "gbuffer": - nodepath.set_shader(shader, 25) - else: - self.tag_mgr.apply_state( - stage, nodepath, shader, str(effect.effect_id), 25 + 10 * i + sort) - nodepath.show_through(self.tag_mgr.get_mask(stage)) - - if effect.get_option("render_gbuffer") and effect.get_option("render_forward"): - self.error("You cannot render an object forward and deferred at the " - "same time! Either use render_gbuffer or use render_forward, " - "but not both.") - - def set_effect(self, nodepath, effect_src, options=None, sort=30): - """ See _internal_set_effect. """ - args = (nodepath, effect_src, options, sort) - self._applied_effects.append(args) - self._internal_set_effect(*args) - - def add_environment_probe(self): - """ Constructs a new environment probe and returns the handle, so that - the probe can be modified. In case the env_probes plugin is not activated, - this returns a dummy object which can be modified but has no impact. """ - if not self.plugin_mgr.is_plugin_enabled("env_probes"): - self.warn("env_probes plugin is not loaded - cannot add environment probe") - - class DummyEnvironmentProbe(object): # pylint: disable=too-few-public-methods - def __getattr__(self, *args, **kwargs): - return lambda *args, **kwargs: None - return DummyEnvironmentProbe() - - # Ugh .. - from rpplugins.env_probes.environment_probe import EnvironmentProbe - probe = EnvironmentProbe() - self.plugin_mgr.instances["env_probes"].probe_mgr.add_probe(probe) - return probe - - def prepare_scene(self, scene): - """ Prepares a given scene, by converting panda lights to render pipeline - lights. This also converts all empties with names starting with 'ENVPROBE' - to environment probes. Conversion of blender to render pipeline lights - is done by scaling their intensity by 100 to match lumens. - - Additionally, this finds all materials with the 'TRANSPARENT' shading - model, and sets the proper effects on them to ensure they are rendered - properly. - - This method also returns a dictionary with handles to all created - objects, that is lights, environment probes, and transparent objects. - This can be used to store them and process them later on, or delete - them when a newer scene is loaded.""" - lights = [] - for light in scene.find_all_matches("**/+PointLight"): - light_node = light.node() - rp_light = PointLight() - rp_light.pos = light.get_pos(Globals.base.render) - rp_light.radius = light_node.max_distance - rp_light.energy = 20.0 * light_node.color.w - rp_light.color = light_node.color.xyz - rp_light.casts_shadows = light_node.shadow_caster - rp_light.shadow_map_resolution = light_node.shadow_buffer_size.x - rp_light.inner_radius = 0.4 - - self.add_light(rp_light) - light.remove_node() - lights.append(rp_light) - - for light in scene.find_all_matches("**/+Spotlight"): - light_node = light.node() - rp_light = SpotLight() - rp_light.pos = light.get_pos(Globals.base.render) - rp_light.radius = light_node.max_distance - rp_light.energy = 20.0 * light_node.color.w - rp_light.color = light_node.color.xyz - rp_light.casts_shadows = light_node.shadow_caster - rp_light.shadow_map_resolution = light_node.shadow_buffer_size.x - rp_light.fov = light_node.exponent / math.pi * 180.0 - lpoint = light.get_mat(Globals.base.render).xform_vec((0, 0, -1)) - rp_light.direction = lpoint - self.add_light(rp_light) - light.remove_node() - lights.append(rp_light) - - envprobes = [] - for np in scene.find_all_matches("**/ENVPROBE*"): - probe = self.add_environment_probe() - probe.set_mat(np.get_mat()) - probe.border_smoothness = 0.0001 - probe.parallax_correction = True - np.remove_node() - envprobes.append(probe) - - tristrips_warning_emitted = False - transparent_objects = [] - for geom_np in scene.find_all_matches("**/+GeomNode"): - geom_node = geom_np.node() - geom_count = geom_node.get_num_geoms() - for i in range(geom_count): - state = geom_node.get_geom_state(i) - geom = geom_node.get_geom(i) - - needs_conversion = False - for prim in geom.get_primitives(): - if isinstance(prim, GeomTristrips): - needs_conversion = True - if not tristrips_warning_emitted: - self.warn("At least one GeomNode (", geom_node.get_name(), "and possible more..) contains tristrips.") - self.warn("Due to a NVIDIA Driver bug, we have to convert them to triangles now.") - self.warn("Consider exporting your models with the Bam Exporter to avoid this.") - tristrips_warning_emitted = True - break - - if needs_conversion: - geom_node.modify_geom(i).decompose_in_place() - - if not state.has_attrib(MaterialAttrib): - self.warn("Geom", geom_node, "has no material! Please fix this.") - continue - - material = state.get_attrib(MaterialAttrib).get_material() - if material.emission is not None: - shading_model = material.emission.x - else: - shading_model = 0.0 - - - # SHADING_MODEL_TRANSPARENT - if shading_model == 3: - if geom_count > 1: - self.error("Transparent materials must be on their own geom!\n" - "If you are exporting from blender, split them into\n" - "seperate meshes, then re-export your scene. The\n" - "problematic mesh is: " + geom_np.get_name()) - continue - self.set_effect(geom_np, "effects/default.yaml", - {"render_forward": True, "render_gbuffer": False}, 100) - - return {"lights": lights, "envprobes": envprobes, - "transparent_objects": transparent_objects} - - def _create_managers(self): - """ Internal method to create all managers and instances. This also - initializes the commonly used render stages, which are always required, - independently of which plugins are enabled. """ - self.task_scheduler = TaskScheduler(self) - self.tag_mgr = TagStateManager(Globals.base.cam) - self.plugin_mgr = PluginManager(self) - self.stage_mgr = StageManager(self) - self.light_mgr = LightManager(self) - self.daytime_mgr = DayTimeManager(self) - self.ies_loader = IESProfileLoader(self) - self.common_resources = CommonResources(self) - self._init_common_stages() - - def _analyze_system(self): - """ Prints information about the system used, including information - about the used Panda3D build. Also checks if the Panda3D build is out - of date. """ - self.debug("Using Python {}.{} with architecture {}".format( - sys.version_info.major, sys.version_info.minor, PandaSystem.get_platform())) - - build_date = getattr(PandaSystem, 'build_date', None) - if build_date: - self.debug("Using Panda3D {} built on {}".format( - PandaSystem.get_version_string(), build_date)) - else: - self.debug("Using Panda3D {}".format(PandaSystem.get_version_string())) - - if PandaSystem.get_git_commit(): - self.debug("Using git commit {}".format(PandaSystem.get_git_commit())) - else: - self.debug("Using custom Panda3D build") - if not self._check_version(): - self.fatal("Your Panda3D version is outdated! Please update to the newest \n" - "git version! Checkout https://github.com/panda3d/panda3d to " - "compile panda from source, or get a recent buildbot build.") - - def _initialize_managers(self): - """ Internal method to initialize all managers, after they have been - created earlier in _create_managers. The creation and initialization - is seperated due to the fact that plugins and various other subprocesses - have to get initialized inbetween. """ - self.stage_mgr.setup() - self.stage_mgr.reload_shaders() - self.light_mgr.reload_shaders() - self._init_bindings() - self.light_mgr.init_shadows() - - def _init_debugger(self): - """ Internal method to initialize the GUI-based debugger. In case debugging - is disabled, this constructs a dummy debugger, which does nothing. - The debugger itself handles the various onscreen components. """ - if self.settings["pipeline.display_debugger"]: - self.debugger = Debugger(self) - else: - # Use an empty onscreen debugger in case the debugger is not - # enabled, which defines all member functions as empty lambdas - class EmptyDebugger(object): # pylint: disable=too-few-public-methods - def __getattr__(self, *args, **kwargs): - return lambda *args, **kwargs: None - self.debugger = EmptyDebugger() # pylint: disable=redefined-variable-type - del EmptyDebugger - - def _init_globals(self): - """ Inits all global bindings. This includes references to the global - ShowBase instance, as well as the render resolution, the GUI font, - and various global logging and output methods. """ - Globals.load(self._showbase) - native_w, native_h = self._showbase.win.get_x_size(), self._showbase.win.get_y_size() - Globals.native_resolution = LVecBase2i(native_w, native_h) - self._last_window_dims = LVecBase2i(Globals.native_resolution) - self._compute_render_resolution() - RenderTarget.RT_OUTPUT_FUNC = lambda *args: RPObject.global_warn( - "RenderTarget", *args[1:]) - RenderTarget.USE_R11G11B10 = self.settings["pipeline.use_r11_g11_b10"] - - def _set_default_effect(self): - """ Sets the default effect used for all objects if not overridden, this - just calls set_effect with the default effect and options as parameters. - This uses a very low sort, to make sure that overriding the default - effect does not require a custom sort parameter to be passed. """ - self.set_effect(Globals.render, "effects/default.yaml", {}, -10) - - def _adjust_camera_settings(self): - """ Sets the default camera settings, this includes the cameras - near and far plane, as well as FoV. The reason for this is, that pandas - default field of view is very small, and thus we increase it. """ - self._showbase.camLens.set_near_far(0.1, 70000) - self._showbase.camLens.set_fov(40) - - def _compute_render_resolution(self): - """ Computes the internally used render resolution. This might differ - from the window dimensions in case a resolution scale is set. """ - scale_factor = self.settings["pipeline.resolution_scale"] - w = int(float(Globals.native_resolution.x) * scale_factor) - h = int(float(Globals.native_resolution.y) * scale_factor) - # Make sure the resolution is a multiple of 4 - w, h = w - w % 4, h - h % 4 - self.debug("Render resolution is", w, "x", h) - Globals.resolution = LVecBase2i(w, h) - - def _init_showbase(self, base): - """ Inits the the given showbase object. This is part of an alternative - method of initializing the showbase. In case base is None, a new - ShowBase instance will be created and initialized. Otherwise base() is - expected to either be an uninitialized ShowBase instance, or an - initialized instance with pre_showbase_init() called inbefore. """ - if not base: - self.pre_showbase_init() - self._showbase = ShowBase() - else: - if not hasattr(base, "render"): - self.pre_showbase_init() - ShowBase.__init__(base) - else: - if not self._pre_showbase_initialized: - self.fatal("You constructed your own ShowBase object but you\n" - "did not call pre_show_base_init() on the render\n" - "pipeline object before! Checkout the 00-Loading the\n" - "pipeline sample to see how to initialize the RP.") - self._showbase = base - - # Now that we have a showbase and a window, we can print out driver info - self.debug("Driver Version =", self._showbase.win.gsg.driver_version) - self.debug("Driver Vendor =", self._showbase.win.gsg.driver_vendor) - self.debug("Driver Renderer =", self._showbase.win.gsg.driver_renderer) - - def _init_bindings(self): - """ Internal method to init the tasks and keybindings. This constructs - the tasks to be run on a per-frame basis. """ - self._showbase.addTask(self._manager_update_task, "RP_UpdateManagers", sort=10) - self._showbase.addTask(self._plugin_pre_render_update, "RP_Plugin_BeforeRender", sort=12) - self._showbase.addTask(self._plugin_post_render_update, "RP_Plugin_AfterRender", sort=15) - self._showbase.addTask(self._update_inputs_and_stages, "RP_UpdateInputsAndStages", sort=18) - self._showbase.taskMgr.doMethodLater(0.5, self._clear_state_cache, "RP_ClearStateCache") - self._showbase.accept("window-event", self._handle_window_event) - - def _handle_window_event(self, event): - """ Checks for window events. This mainly handles incoming resizes, - and calls the required handlers """ - self._showbase.windowEvent(event) - window_dims = LVecBase2i(self._showbase.win.get_x_size(), self._showbase.win.get_y_size()) - if window_dims != self._last_window_dims and window_dims != Globals.native_resolution: - self._last_window_dims = LVecBase2i(window_dims) - - # Ensure the dimensions are a multiple of 4, and if not, correct it - if window_dims.x % 4 != 0 or window_dims.y % 4 != 0: - self.debug("Correcting non-multiple of 4 window size:", window_dims) - window_dims.x = window_dims.x - window_dims.x % 4 - window_dims.y = window_dims.y - window_dims.y % 4 - props = WindowProperties.size(window_dims.x, window_dims.y) - self._showbase.win.request_properties(props) - - self.debug("Resizing to", window_dims.x, "x", window_dims.y) - Globals.native_resolution = window_dims - self._compute_render_resolution() - self.light_mgr.compute_tile_size() - self.stage_mgr.handle_window_resize() - self.debugger.handle_window_resize() - self.plugin_mgr.trigger_hook("window_resized") - - def _clear_state_cache(self, task=None): - """ Task which repeatedly clears the state cache to avoid storing - unused states. While running once a while, this task prevents over-polluting - the state-cache with unused states. This complements Panda3D's internal - state garbarge collector, which does a great job, but still cannot clear - up all states. """ - task.delayTime = 2.0 - TransformState.clear_cache() - RenderState.clear_cache() - return task.again - - def _manager_update_task(self, task): - """ Update task which gets called before the rendering, and updates - all managers.""" - self.task_scheduler.step() - self._listener.update() - self.debugger.update() - self.daytime_mgr.update() - self.light_mgr.update() - - if Globals.clock.get_frame_count() == 10: - self.debug("Hiding loading screen after 10 pre-rendered frames.") - self.loading_screen.remove() - - return task.cont - - def _update_inputs_and_stages(self, task): - """ Updates the commonly used inputs each frame. This is a seperate - task to be able view detailed performance information in pstats, since - a lot of matrix calculations are involved here. """ - self.common_resources.update() - self.stage_mgr.update() - return task.cont - - def _plugin_pre_render_update(self, task): - """ Update task which gets called before the rendering, and updates the - plugins. This is a seperate task to split the work, and be able to do - better performance analysis in pstats later on. """ - self.plugin_mgr.trigger_hook("pre_render_update") - return task.cont - - def _plugin_post_render_update(self, task): - """ Update task which gets called after the rendering, and should cleanup - all unused states and objects. This also triggers the plugin post-render - update hook. """ - self.plugin_mgr.trigger_hook("post_render_update") - if self._first_frame is not None: - duration = time.process_time() - self._first_frame - self.debug("Took", round(duration, 3), "s until first frame") - self._first_frame = None - return task.cont - - def _create_common_defines(self): - """ Creates commonly used defines for the shader configuration. """ - defines = self.stage_mgr.defines - defines["CAMERA_NEAR"] = round(Globals.base.camLens.get_near(), 10) - defines["CAMERA_FAR"] = round(Globals.base.camLens.get_far(), 10) - - # Work arround buggy nvidia driver, which expects arrays to be const - if "NVIDIA 361.43" in self._showbase.win.gsg.get_driver_version(): - defines["CONST_ARRAY"] = "const" - else: - defines["CONST_ARRAY"] = "" - - # Provide driver vendor as a define - vendor = self._showbase.win.gsg.get_driver_vendor().lower() - defines["IS_NVIDIA"] = "nvidia" in vendor - defines["IS_AMD"] = vendor.startswith("ati") - defines["IS_INTEL"] = "intel" in vendor - - defines["REFERENCE_MODE"] = self.settings["pipeline.reference_mode"] - self.light_mgr.init_defines() - self.plugin_mgr.init_defines() - - def _create_default_skybox(self, size=40000): - """ Returns the default skybox, with a scale of , and all - proper effects and shaders already applied. The skybox is already - parented to render as well. """ - skybox = self.common_resources.load_default_skybox() - skybox.set_scale(size) - skybox.reparent_to(Globals.render) - skybox.set_bin("unsorted", 10000) - self.set_effect(skybox, "effects/skybox.yaml", { - "render_shadow": False, - "render_envmap": False, - "render_voxelize": False, - "alpha_testing": False, - "normal_mapping": False, - "parallax_mapping": False - }, 1000) - return skybox - - def _check_version(self): - """ Internal method to check if the required Panda3D version is met. Returns - True if the version is new enough, and False if the version is outdated. """ - from panda3d.core import Texture - if not hasattr(Texture, "F_r16i"): - return False - return True - - def _init_common_stages(self): - """ Inits the commonly used stages, which don't belong to any plugin, - but yet are necessary and widely used. """ - add_stage = self.stage_mgr.add_stage - self._ambient_stage = AmbientStage(self) - add_stage(self._ambient_stage) - self._gbuffer_stage = GBufferStage(self) - add_stage(self._gbuffer_stage) - self._final_stage = FinalStage(self) - add_stage(self._final_stage) - self._downscale_stage = DownscaleZStage(self) - add_stage(self._downscale_stage) - self._combine_velocity_stage = CombineVelocityStage(self) - add_stage(self._combine_velocity_stage) - - # Add an upscale/downscale stage in case we render at a different resolution - if abs(1 - self.settings["pipeline.resolution_scale"]) > 0.005: - self._upscale_stage = UpscaleStage(self) - add_stage(self._upscale_stage) - - def _get_serialized_material_name(self, material, index=0): - """ Returns a serializable material name """ - return str(index) + "-" + (material.get_name().replace(" ", "").strip() or "unnamed") - - def export_materials(self, pth): - """ Exports a list of all materials found in the current scene in a - serialized format to the given path """ - - with open(pth, "w") as handle: - for i, material in enumerate(Globals.render.find_all_materials()): - if not material.has_base_color() or not material.has_roughness() or not material.has_refractive_index(): - print("Skipping non-pbr material:", material.name) - continue - - handle.write(("{} " * 11).format( - self._get_serialized_material_name(material, i), - material.base_color.x, - material.base_color.y, - material.base_color.z, - material.roughness, - material.refractive_index, - material.metallic, - material.emission.x, # shading model - material.emission.y, # normal strength - material.emission.z, # arbitrary 0 - material.emission.w, # arbitrary 1 - ) + "\n") - - def update_serialized_material(self, data): - """ Internal method to update a material from a given serialized material """ - name = data[0] - - for i, material in enumerate(Globals.render.find_all_materials()): - if self._get_serialized_material_name(material, i) == name: - material.set_base_color(Vec4(float(data[1]), float(data[2]), float(data[3]), 1.0)) - material.set_roughness(float(data[4])) - material.set_refractive_index(float(data[5])) - material.set_metallic(float(data[6])) - material.set_emission(Vec4( - float(data[7]), - float(data[8]), - float(data[9]), - float(data[10]), - )) - - RenderState.clear_cache() +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import division + +import sys +import math +import time + +from panda3d.core import LVecBase2i, TransformState, RenderState, load_prc_file +from panda3d.core import PandaSystem, MaterialAttrib, WindowProperties +from panda3d.core import GeomTristrips, Vec4 + +from direct.showbase.ShowBase import ShowBase +from direct.stdpy.file import isfile + +from rplibs.yaml import load_yaml_file_flat +from six.moves import range # pylint: disable=import-error + +from rpcore.globals import Globals +from rpcore.effect import Effect +from rpcore.rpobject import RPObject +from rpcore.common_resources import CommonResources +from rpcore.native import TagStateManager, PointLight, SpotLight +from rpcore.render_target import RenderTarget +from rpcore.pluginbase.manager import PluginManager +from rpcore.pluginbase.day_manager import DayTimeManager + +from rpcore.util.task_scheduler import TaskScheduler +from rpcore.util.network_communication import NetworkCommunication +from rpcore.util.ies_profile_loader import IESProfileLoader + +from rpcore.gui.debugger import Debugger +from rpcore.gui.loading_screen import LoadingScreen + +from rpcore.mount_manager import MountManager +from rpcore.stage_manager import StageManager +from rpcore.light_manager import LightManager + +from rpcore.stages.ambient_stage import AmbientStage +from rpcore.stages.gbuffer_stage import GBufferStage +from rpcore.stages.final_stage import FinalStage +from rpcore.stages.downscale_z_stage import DownscaleZStage +from rpcore.stages.combine_velocity_stage import CombineVelocityStage +from rpcore.stages.upscale_stage import UpscaleStage + + +class RenderPipeline(RPObject): + + """ This is the main render pipeline class, it combines all components of + the pipeline to form a working system. It does not do much work itself, but + instead setups all the managers and systems to be able to do their work. """ + + def __init__(self): + """ Creates a new pipeline with a given showbase instance. This should + be done before intializing the ShowBase, the pipeline will take care of + that. If the showbase has been initialized before, have a look at + the alternative initialization of the render pipeline (the first sample).""" + RPObject.__init__(self) + self._analyze_system() + self.mount_mgr = MountManager(self) + self.settings = {} + self._applied_effects = [] + self._pre_showbase_initialized = False + self._first_frame = None + #self.set_loading_screen_image("/$$rp/data/gui/loading_screen_bg.txo") + self.set_loading_screen_image("/$$rp/data/gui/Default_Rough.png") + + def load_settings(self, path): + """ Loads the pipeline configuration from a given filename. Usually + this is the 'config/pipeline.ini' file. If you call this more than once, + only the settings of the last file will be used. """ + self.settings = load_yaml_file_flat(path) + + def reload_shaders(self): + """ Reloads all shaders. This will reload the shaders of all plugins, + as well as the pipelines internally used shaders. Because of the + complexity of some shaders, this operation take might take several + seconds. Also notice that all applied effects will be lost, and instead + the default effect will be set on all elements again. Due to this fact, + this method is primarly useful for fast iterations when developing new + shaders. """ + if self.settings["pipeline.display_debugger"]: + self.debug("Reloading shaders ..") + self.debugger.error_msg_handler.clear_messages() + self.debugger.set_reload_hint_visible(True) + self._showbase.graphicsEngine.render_frame() + self._showbase.graphicsEngine.render_frame() + self.tag_mgr.cleanup_states() + self.stage_mgr.reload_shaders() + self.light_mgr.reload_shaders() + self._set_default_effect() + self.plugin_mgr.trigger_hook("shader_reload") + if self.settings["pipeline.display_debugger"]: + self.debugger.set_reload_hint_visible(False) + self._apply_custom_shaders() + + def _apply_custom_shaders(self): + """ Re-applies all custom shaders the user applied, to avoid them getting + removed when the shaders are reloaded """ + self.debug("Re-applying", len(self._applied_effects), "custom shaders") + for args in self._applied_effects: + self._internal_set_effect(*args) + + def pre_showbase_init(self): + """ Setups all required pipeline settings and configuration which have + to be set before the showbase is setup. This is called by create(), + in case the showbase was not initialized, however you can (and have to) + call it manually before you init your custom showbase instance. + See the 00-Loading the pipeline sample for more information. """ + if not self.mount_mgr.is_mounted: + self.debug("Mount manager was not mounted, mounting now ...") + self.mount_mgr.mount() + + if not self.settings: + self.debug("No settings loaded, loading from default location") + self.load_settings("/$$rpconfig/pipeline.yaml") + + if not isfile("/$$rp/data/install.flag"): + self.fatal("You didn't setup the pipeline yet! Please run setup.py.") + + load_prc_file("/$$rpconfig/Meta3d-config.prc") + self._pre_showbase_initialized = True + + def create(self, base=None): + """ This creates the pipeline, and setups all buffers. It also + constructs the showbase. The settings should have been loaded before + calling this, and also the base and write path should have been + initialized properly (see MountManager). + + If base is None, the showbase used in the RenderPipeline constructor + will be used and initialized. Otherwise it is assumed that base is an + initialized ShowBase object. In this case, you should call + pre_showbase_init() before initializing the ShowBase""" + + start_time = time.time() + self._init_showbase(base) + + if not self._showbase.win.gsg.supports_compute_shaders: + self.fatal( + "Sorry, your GPU does not support compute shaders! Make sure\n" + "you have the latest drivers. If you already have, your gpu might\n" + "be too old, or you might be using the open source drivers on linux.") + + self._init_globals() + self.loading_screen.create() + self._adjust_camera_settings() + self._create_managers() + self.plugin_mgr.load() + self.daytime_mgr.load_settings() + self.common_resources.write_config() + self._init_debugger() + + self.plugin_mgr.trigger_hook("stage_setup") + self.plugin_mgr.trigger_hook("post_stage_setup") + + self._create_common_defines() + self._initialize_managers() + self._create_default_skybox() + + self.plugin_mgr.trigger_hook("pipeline_created") + + self._listener = NetworkCommunication(self) + self._set_default_effect() + + # Measure how long it took to initialize everything, and also store + # when we finished, so we can measure how long it took to render the + # first frame (where the shaders are actually compiled) + init_duration = (time.time() - start_time) + self._first_frame = time.process_time() + self.debug("Finished initialization in {:3.3f} s, first frame: {}".format( + init_duration, Globals.clock.get_frame_count())) + + def set_loading_screen_image(self, image_source): + """ Tells the pipeline to use the default loading screen, which consists + of a simple loading image. The image source should be a fullscreen + 16:9 image, and not too small, to avoid being blurred out. """ + self.loading_screen = LoadingScreen(self, image_source) + + def add_light(self, light): + """ Adds a new light to the rendered lights, check out the LightManager + add_light documentation for further information. """ + self.light_mgr.add_light(light) + + def remove_light(self, light): + """ Removes a previously attached light, check out the LightManager + remove_light documentation for further information. """ + self.light_mgr.remove_light(light) + + def load_ies_profile(self, filename): + """ Loads an IES profile from a given filename and returns a handle which + can be used to set an ies profile on a light """ + return self.ies_loader.load(filename) + + def _internal_set_effect(self, nodepath, effect_src, options=None, sort=30): + """ Sets an effect to the given object, using the specified options. + Check out the effect documentation for more information about possible + options and configurations. The object should be a nodepath, and the + effect will be applied to that nodepath and all nodepaths below whose + current effect sort is less than the new effect sort (passed by the + sort parameter). """ + effect = Effect.load(effect_src, options) + if effect is None: + return self.error("Could not apply effect") + + for i, stage in enumerate(("gbuffer", "shadow", "voxelize", "envmap", "forward")): + if not effect.get_option("render_" + stage): + nodepath.hide(self.tag_mgr.get_mask(stage)) + else: + shader = effect.get_shader_obj(stage) + if stage == "gbuffer": + nodepath.set_shader(shader, 25) + else: + self.tag_mgr.apply_state( + stage, nodepath, shader, str(effect.effect_id), 25 + 10 * i + sort) + nodepath.show_through(self.tag_mgr.get_mask(stage)) + + if effect.get_option("render_gbuffer") and effect.get_option("render_forward"): + self.error("You cannot render an object forward and deferred at the " + "same time! Either use render_gbuffer or use render_forward, " + "but not both.") + + def set_effect(self, nodepath, effect_src, options=None, sort=30): + """ See _internal_set_effect. """ + args = (nodepath, effect_src, options, sort) + self._applied_effects.append(args) + self._internal_set_effect(*args) + + def add_environment_probe(self): + """ Constructs a new environment probe and returns the handle, so that + the probe can be modified. In case the env_probes plugin is not activated, + this returns a dummy object which can be modified but has no impact. """ + if not self.plugin_mgr.is_plugin_enabled("env_probes"): + self.warn("env_probes plugin is not loaded - cannot add environment probe") + + class DummyEnvironmentProbe(object): # pylint: disable=too-few-public-methods + def __getattr__(self, *args, **kwargs): + return lambda *args, **kwargs: None + return DummyEnvironmentProbe() + + # Ugh .. + from rpplugins.env_probes.environment_probe import EnvironmentProbe + probe = EnvironmentProbe() + self.plugin_mgr.instances["env_probes"].probe_mgr.add_probe(probe) + return probe + + def prepare_scene(self, scene): + """ Prepares a given scene, by converting panda lights to render pipeline + lights. This also converts all empties with names starting with 'ENVPROBE' + to environment probes. Conversion of blender to render pipeline lights + is done by scaling their intensity by 100 to match lumens. + + Additionally, this finds all materials with the 'TRANSPARENT' shading + model, and sets the proper effects on them to ensure they are rendered + properly. + + This method also returns a dictionary with handles to all created + objects, that is lights, environment probes, and transparent objects. + This can be used to store them and process them later on, or delete + them when a newer scene is loaded.""" + lights = [] + for light in scene.find_all_matches("**/+PointLight"): + light_node = light.node() + rp_light = PointLight() + rp_light.pos = light.get_pos(Globals.base.render) + rp_light.radius = light_node.max_distance + rp_light.energy = 20.0 * light_node.color.w + rp_light.color = light_node.color.xyz + rp_light.casts_shadows = light_node.shadow_caster + rp_light.shadow_map_resolution = light_node.shadow_buffer_size.x + rp_light.inner_radius = 0.4 + + self.add_light(rp_light) + light.remove_node() + lights.append(rp_light) + + for light in scene.find_all_matches("**/+Spotlight"): + light_node = light.node() + rp_light = SpotLight() + rp_light.pos = light.get_pos(Globals.base.render) + rp_light.radius = light_node.max_distance + rp_light.energy = 20.0 * light_node.color.w + rp_light.color = light_node.color.xyz + rp_light.casts_shadows = light_node.shadow_caster + rp_light.shadow_map_resolution = light_node.shadow_buffer_size.x + rp_light.fov = light_node.exponent / math.pi * 180.0 + lpoint = light.get_mat(Globals.base.render).xform_vec((0, 0, -1)) + rp_light.direction = lpoint + self.add_light(rp_light) + light.remove_node() + lights.append(rp_light) + + envprobes = [] + for np in scene.find_all_matches("**/ENVPROBE*"): + probe = self.add_environment_probe() + probe.set_mat(np.get_mat()) + probe.border_smoothness = 0.0001 + probe.parallax_correction = True + np.remove_node() + envprobes.append(probe) + + tristrips_warning_emitted = False + transparent_objects = [] + for geom_np in scene.find_all_matches("**/+GeomNode"): + geom_node = geom_np.node() + geom_count = geom_node.get_num_geoms() + for i in range(geom_count): + state = geom_node.get_geom_state(i) + geom = geom_node.get_geom(i) + + needs_conversion = False + for prim in geom.get_primitives(): + if isinstance(prim, GeomTristrips): + needs_conversion = True + if not tristrips_warning_emitted: + self.warn("At least one GeomNode (", geom_node.get_name(), "and possible more..) contains tristrips.") + self.warn("Due to a NVIDIA Driver bug, we have to convert them to triangles now.") + self.warn("Consider exporting your models with the Bam Exporter to avoid this.") + tristrips_warning_emitted = True + break + + if needs_conversion: + geom_node.modify_geom(i).decompose_in_place() + + if not state.has_attrib(MaterialAttrib): + self.warn("Geom", geom_node, "has no material! Please fix this.") + continue + + material = state.get_attrib(MaterialAttrib).get_material() + if material.emission is not None: + shading_model = material.emission.x + else: + shading_model = 0.0 + + + # SHADING_MODEL_TRANSPARENT + if shading_model == 3: + if geom_count > 1: + self.error("Transparent materials must be on their own geom!\n" + "If you are exporting from blender, split them into\n" + "seperate meshes, then re-export your scene. The\n" + "problematic mesh is: " + geom_np.get_name()) + continue + self.set_effect(geom_np, "effects/default.yaml", + {"render_forward": True, "render_gbuffer": False}, 100) + + return {"lights": lights, "envprobes": envprobes, + "transparent_objects": transparent_objects} + + def _create_managers(self): + """ Internal method to create all managers and instances. This also + initializes the commonly used render stages, which are always required, + independently of which plugins are enabled. """ + self.task_scheduler = TaskScheduler(self) + self.tag_mgr = TagStateManager(Globals.base.cam) + self.plugin_mgr = PluginManager(self) + self.stage_mgr = StageManager(self) + self.light_mgr = LightManager(self) + self.daytime_mgr = DayTimeManager(self) + self.ies_loader = IESProfileLoader(self) + self.common_resources = CommonResources(self) + self._init_common_stages() + + def _analyze_system(self): + """ Prints information about the system used, including information + about the used Panda3D build. Also checks if the Panda3D build is out + of date. """ + self.debug("Using Python {}.{} with architecture {}".format( + sys.version_info.major, sys.version_info.minor, PandaSystem.get_platform())) + + build_date = getattr(PandaSystem, 'build_date', None) + if build_date: + self.debug("Using Panda3D {} built on {}".format( + PandaSystem.get_version_string(), build_date)) + else: + self.debug("Using Panda3D {}".format(PandaSystem.get_version_string())) + + if PandaSystem.get_git_commit(): + self.debug("Using git commit {}".format(PandaSystem.get_git_commit())) + else: + self.debug("Using custom Panda3D build") + if not self._check_version(): + self.fatal("Your Panda3D version is outdated! Please update to the newest \n" + "git version! Checkout https://github.com/panda3d/panda3d to " + "compile panda from source, or get a recent buildbot build.") + + def _initialize_managers(self): + """ Internal method to initialize all managers, after they have been + created earlier in _create_managers. The creation and initialization + is seperated due to the fact that plugins and various other subprocesses + have to get initialized inbetween. """ + self.stage_mgr.setup() + self.stage_mgr.reload_shaders() + self.light_mgr.reload_shaders() + self._init_bindings() + self.light_mgr.init_shadows() + + def _init_debugger(self): + """ Internal method to initialize the GUI-based debugger. In case debugging + is disabled, this constructs a dummy debugger, which does nothing. + The debugger itself handles the various onscreen components. """ + if self.settings["pipeline.display_debugger"]: + self.debugger = Debugger(self) + else: + # Use an empty onscreen debugger in case the debugger is not + # enabled, which defines all member functions as empty lambdas + class EmptyDebugger(object): # pylint: disable=too-few-public-methods + def __getattr__(self, *args, **kwargs): + return lambda *args, **kwargs: None + self.debugger = EmptyDebugger() # pylint: disable=redefined-variable-type + del EmptyDebugger + + def _init_globals(self): + """ Inits all global bindings. This includes references to the global + ShowBase instance, as well as the render resolution, the GUI font, + and various global logging and output methods. """ + Globals.load(self._showbase) + native_w, native_h = self._showbase.win.get_x_size(), self._showbase.win.get_y_size() + Globals.native_resolution = LVecBase2i(native_w, native_h) + self._last_window_dims = LVecBase2i(Globals.native_resolution) + self._compute_render_resolution() + RenderTarget.RT_OUTPUT_FUNC = lambda *args: RPObject.global_warn( + "RenderTarget", *args[1:]) + RenderTarget.USE_R11G11B10 = self.settings["pipeline.use_r11_g11_b10"] + + def _set_default_effect(self): + """ Sets the default effect used for all objects if not overridden, this + just calls set_effect with the default effect and options as parameters. + This uses a very low sort, to make sure that overriding the default + effect does not require a custom sort parameter to be passed. """ + self.set_effect(Globals.render, "effects/default.yaml", {}, -10) + + def _adjust_camera_settings(self): + """ Sets the default camera settings, this includes the cameras + near and far plane, as well as FoV. The reason for this is, that pandas + default field of view is very small, and thus we increase it. """ + self._showbase.camLens.set_near_far(0.1, 70000) + self._showbase.camLens.set_fov(40) + + def _compute_render_resolution(self): + """ Computes the internally used render resolution. This might differ + from the window dimensions in case a resolution scale is set. """ + scale_factor = self.settings["pipeline.resolution_scale"] + w = int(float(Globals.native_resolution.x) * scale_factor) + h = int(float(Globals.native_resolution.y) * scale_factor) + # Make sure the resolution is a multiple of 4 + w, h = w - w % 4, h - h % 4 + self.debug("Render resolution is", w, "x", h) + Globals.resolution = LVecBase2i(w, h) + + def _init_showbase(self, base): + """ Inits the the given showbase object. This is part of an alternative + method of initializing the showbase. In case base is None, a new + ShowBase instance will be created and initialized. Otherwise base() is + expected to either be an uninitialized ShowBase instance, or an + initialized instance with pre_showbase_init() called inbefore. """ + if not base: + self.pre_showbase_init() + self._showbase = ShowBase() + else: + if not hasattr(base, "render"): + self.pre_showbase_init() + ShowBase.__init__(base) + else: + if not self._pre_showbase_initialized: + self.fatal("You constructed your own ShowBase object but you\n" + "did not call pre_show_base_init() on the render\n" + "pipeline object before! Checkout the 00-Loading the\n" + "pipeline sample to see how to initialize the RP.") + self._showbase = base + + # Now that we have a showbase and a window, we can print out driver info + self.debug("Driver Version =", self._showbase.win.gsg.driver_version) + self.debug("Driver Vendor =", self._showbase.win.gsg.driver_vendor) + self.debug("Driver Renderer =", self._showbase.win.gsg.driver_renderer) + + def _init_bindings(self): + """ Internal method to init the tasks and keybindings. This constructs + the tasks to be run on a per-frame basis. """ + self._showbase.addTask(self._manager_update_task, "RP_UpdateManagers", sort=10) + self._showbase.addTask(self._plugin_pre_render_update, "RP_Plugin_BeforeRender", sort=12) + self._showbase.addTask(self._plugin_post_render_update, "RP_Plugin_AfterRender", sort=15) + self._showbase.addTask(self._update_inputs_and_stages, "RP_UpdateInputsAndStages", sort=18) + self._showbase.taskMgr.doMethodLater(0.5, self._clear_state_cache, "RP_ClearStateCache") + self._showbase.accept("window-event", self._handle_window_event) + + def _handle_window_event(self, event): + """ Checks for window events. This mainly handles incoming resizes, + and calls the required handlers """ + self._showbase.windowEvent(event) + window_dims = LVecBase2i(self._showbase.win.get_x_size(), self._showbase.win.get_y_size()) + if window_dims != self._last_window_dims and window_dims != Globals.native_resolution: + self._last_window_dims = LVecBase2i(window_dims) + + # Ensure the dimensions are a multiple of 4, and if not, correct it + if window_dims.x % 4 != 0 or window_dims.y % 4 != 0: + self.debug("Correcting non-multiple of 4 window size:", window_dims) + window_dims.x = window_dims.x - window_dims.x % 4 + window_dims.y = window_dims.y - window_dims.y % 4 + props = WindowProperties.size(window_dims.x, window_dims.y) + self._showbase.win.request_properties(props) + + self.debug("Resizing to", window_dims.x, "x", window_dims.y) + Globals.native_resolution = window_dims + self._compute_render_resolution() + self.light_mgr.compute_tile_size() + self.stage_mgr.handle_window_resize() + self.debugger.handle_window_resize() + self.plugin_mgr.trigger_hook("window_resized") + + def _clear_state_cache(self, task=None): + """ Task which repeatedly clears the state cache to avoid storing + unused states. While running once a while, this task prevents over-polluting + the state-cache with unused states. This complements Panda3D's internal + state garbarge collector, which does a great job, but still cannot clear + up all states. """ + task.delayTime = 2.0 + TransformState.clear_cache() + RenderState.clear_cache() + return task.again + + def _manager_update_task(self, task): + """ Update task which gets called before the rendering, and updates + all managers.""" + self.task_scheduler.step() + self._listener.update() + self.debugger.update() + self.daytime_mgr.update() + self.light_mgr.update() + + if Globals.clock.get_frame_count() == 10: + self.debug("Hiding loading screen after 10 pre-rendered frames.") + self.loading_screen.remove() + + return task.cont + + def _update_inputs_and_stages(self, task): + """ Updates the commonly used inputs each frame. This is a seperate + task to be able view detailed performance information in pstats, since + a lot of matrix calculations are involved here. """ + self.common_resources.update() + self.stage_mgr.update() + return task.cont + + def _plugin_pre_render_update(self, task): + """ Update task which gets called before the rendering, and updates the + plugins. This is a seperate task to split the work, and be able to do + better performance analysis in pstats later on. """ + self.plugin_mgr.trigger_hook("pre_render_update") + return task.cont + + def _plugin_post_render_update(self, task): + """ Update task which gets called after the rendering, and should cleanup + all unused states and objects. This also triggers the plugin post-render + update hook. """ + self.plugin_mgr.trigger_hook("post_render_update") + if self._first_frame is not None: + duration = time.process_time() - self._first_frame + self.debug("Took", round(duration, 3), "s until first frame") + self._first_frame = None + return task.cont + + def _create_common_defines(self): + """ Creates commonly used defines for the shader configuration. """ + defines = self.stage_mgr.defines + defines["CAMERA_NEAR"] = round(Globals.base.camLens.get_near(), 10) + defines["CAMERA_FAR"] = round(Globals.base.camLens.get_far(), 10) + + # Work arround buggy nvidia driver, which expects arrays to be const + if "NVIDIA 361.43" in self._showbase.win.gsg.get_driver_version(): + defines["CONST_ARRAY"] = "const" + else: + defines["CONST_ARRAY"] = "" + + # Provide driver vendor as a define + vendor = self._showbase.win.gsg.get_driver_vendor().lower() + defines["IS_NVIDIA"] = "nvidia" in vendor + defines["IS_AMD"] = vendor.startswith("ati") + defines["IS_INTEL"] = "intel" in vendor + + defines["REFERENCE_MODE"] = self.settings["pipeline.reference_mode"] + self.light_mgr.init_defines() + self.plugin_mgr.init_defines() + + def _create_default_skybox(self, size=40000): + """ Returns the default skybox, with a scale of , and all + proper effects and shaders already applied. The skybox is already + parented to render as well. """ + skybox = self.common_resources.load_default_skybox() + skybox.set_scale(size) + skybox.reparent_to(Globals.render) + skybox.set_bin("unsorted", 10000) + self.set_effect(skybox, "effects/skybox.yaml", { + "render_shadow": False, + "render_envmap": False, + "render_voxelize": False, + "alpha_testing": False, + "normal_mapping": False, + "parallax_mapping": False + }, 1000) + return skybox + + def _check_version(self): + """ Internal method to check if the required Panda3D version is met. Returns + True if the version is new enough, and False if the version is outdated. """ + from panda3d.core import Texture + if not hasattr(Texture, "F_r16i"): + return False + return True + + def _init_common_stages(self): + """ Inits the commonly used stages, which don't belong to any plugin, + but yet are necessary and widely used. """ + add_stage = self.stage_mgr.add_stage + self._ambient_stage = AmbientStage(self) + add_stage(self._ambient_stage) + self._gbuffer_stage = GBufferStage(self) + add_stage(self._gbuffer_stage) + self._final_stage = FinalStage(self) + add_stage(self._final_stage) + self._downscale_stage = DownscaleZStage(self) + add_stage(self._downscale_stage) + self._combine_velocity_stage = CombineVelocityStage(self) + add_stage(self._combine_velocity_stage) + + # Add an upscale/downscale stage in case we render at a different resolution + if abs(1 - self.settings["pipeline.resolution_scale"]) > 0.005: + self._upscale_stage = UpscaleStage(self) + add_stage(self._upscale_stage) + + def _get_serialized_material_name(self, material, index=0): + """ Returns a serializable material name """ + return str(index) + "-" + (material.get_name().replace(" ", "").strip() or "unnamed") + + def export_materials(self, pth): + """ Exports a list of all materials found in the current scene in a + serialized format to the given path """ + + with open(pth, "w") as handle: + for i, material in enumerate(Globals.render.find_all_materials()): + if not material.has_base_color() or not material.has_roughness() or not material.has_refractive_index(): + print("Skipping non-pbr material:", material.name) + continue + + handle.write(("{} " * 11).format( + self._get_serialized_material_name(material, i), + material.base_color.x, + material.base_color.y, + material.base_color.z, + material.roughness, + material.refractive_index, + material.metallic, + material.emission.x, # shading model + material.emission.y, # normal strength + material.emission.z, # arbitrary 0 + material.emission.w, # arbitrary 1 + ) + "\n") + + def update_serialized_material(self, data): + """ Internal method to update a material from a given serialized material """ + name = data[0] + + for i, material in enumerate(Globals.render.find_all_materials()): + if self._get_serialized_material_name(material, i) == name: + material.set_base_color(Vec4(float(data[1]), float(data[2]), float(data[3]), 1.0)) + material.set_roughness(float(data[4])) + material.set_refractive_index(float(data[5])) + material.set_metallic(float(data[6])) + material.set_emission(Vec4( + float(data[7]), + float(data[8]), + float(data[9]), + float(data[10]), + )) + + RenderState.clear_cache() diff --git a/RenderPipelineFile/rpcore/render_stage.py b/RenderPipelineFile/rpcore/render_stage.py index 2af34baa..1e5d95a4 100644 --- a/RenderPipelineFile/rpcore/render_stage.py +++ b/RenderPipelineFile/rpcore/render_stage.py @@ -1,179 +1,179 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rplibs.six import itervalues, iteritems - -from rpcore.rpobject import RPObject -from rpcore.render_target import RenderTarget -from rpcore.loader import RPLoader - - -class RenderStage(RPObject): - - """ This class is the abstract class for all stages used in the pipeline. - It represents a part of the pipeline render process. Each stage specifies - which pipes it uses and which pipes it produces. A pipe can be seen as a - texture, which gets modified. E.g. the gbuffer pass produces the gbuffer - pipe, the ambient occlusion pass produces the occlusion pipe and so on. The - lighting pass can then specify which pipes it needs and compute the image. - Using a pipe system ensures that new techniques can be inserted easily, - without the other techniques even being aware of them """ - - required_inputs = [] - required_pipes = [] - - produced_inputs = {} - produced_pipes = {} - produced_defines = {} - - disabled = False - - def __init__(self, pipeline): - """ Creates a new render stage """ - RPObject.__init__(self) - self.stage_id = self.__class__.__name__ - self._pipeline = pipeline - self._active = True - self._targets = {} - - def create(self): - """ This method should setup the stage and create the pipes """ - raise NotImplementedError() - - def reload_shaders(self): - """ This method should set all required shaders, there should be no - shaders set in the create method, because the shader auto config is not - generated there """ - pass - - def set_shader_input(self, *args): - """ This method sets a shader input on all stages, which is mainly used - by the stage manager """ - for target in itervalues(self._targets): - target.set_shader_input(*args) - - def set_shader_inputs(self, **kwargs): - """ This method sets shader inputs on all stages, which is mainly used - by the stage manager """ - for target in itervalues(self._targets): - target.set_shader_inputs(**kwargs) - - def update(self): - """ This method gets called every frame, and can be overridden by render - stages to perform custom updates """ - pass - - @property - def active(self): - """ Returns whether *all* targets of the stage are active """ - return self._active - - @active.setter - def active(self, state): - """ Enables or disables this stage. In case the stage is disabled, it will - not get updated anymore, and all stages are distabled """ - if self._active != state: - self._active = state - for target in itervalues(self._targets): - target.active = self._active - - def create_target(self, name): - """ Creates a new render target and binds it to this stage """ - # Format the name like Plugin:Stage:Name, so it can be easily - # found in pstats below the plugin cagetory - name = self._get_plugin_id() + ":" + self.stage_id + ":" + name - if name in self._targets: - return self.error("Overriding existing target: " + name) - self._targets[name] = RenderTarget(name) - return self._targets[name] - - def remove_target(self, target): - """ Removes a previously registered target. This unregisters the - target, as well as removing it from the list of assigned targets. """ - target.remove() - target_key = None - for key, value_target in iteritems(self._targets): - if target == value_target: - target_key = key - break - del self._targets[target_key] - - def _get_shader_handle(self, path, *args): - """ Returns a handle to a Shader object, containing all sources passed - as arguments. The path argument will be used to locate shaders if no - absolute path is given. This is the internal method used in load_shader - and load_plugin_shader. """ - assert len(args) > 0 and len(args) <= 3 - path_args = [] - - for source in args: - for prefix in ("/$$rpconfig", "/$$rp/shader", "/$$rptemp"): - if prefix in source: - path_args.append(source) - break - else: - path_args.append(path.format(source)) - - # If only one shader is specified, assume its a postprocess fragment shader, - # and use the default vertex shader - if len(args) == 1: - path_args = ["/$$rp/shader/default_post_process.vert.glsl"] + path_args - return RPLoader.load_shader(*path_args) - - def _get_plugin_id(self): - """ Returns the id of the plugin which created this stage. This is done - by extracting the name of the plugin from the module name """ - if "rpcore.stages" in self.__class__.__module__: - return "render_pipeline_internal" - return str(self.__class__.__module__).split(".")[-2] - - def load_shader(self, *args): - """ Loads a shader from the given args. If only one argument is passed, - the default template for the stage is loaded. If two arguments are - passed, the first argument should be the vertex shader and the second - argument should be the fragment shader. If three arguments are passed, - the order should be vertex, fragment, geometry """ - return self._get_shader_handle("/$$rp/shader/{0}", *args) - - def load_plugin_shader(self, *args): - """ Loads a shader from the plugin directory. This method is useful - for RenderStages created by plugins. For a description of the arguments, - see the load_shader function. """ - shader_path = "rpplugins/" + self._get_plugin_id() + "/shader/{0}" - return self._get_shader_handle(shader_path, *args) - - def handle_window_resize(self): - """ This method gets called when the window gets resized. By default, - this just resizes all render targets. """ - self.set_dimensions() - for target in itervalues(self._targets): - target.consider_resize() - - def set_dimensions(self): - """ This method should set the dimensions on all targets which don't - have a relative constraint, and also the size of all images. This - is called after initialization, and when the window resized. """ - pass +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rplibs.six import itervalues, iteritems + +from rpcore.rpobject import RPObject +from rpcore.render_target import RenderTarget +from rpcore.loader import RPLoader + + +class RenderStage(RPObject): + + """ This class is the abstract class for all stages used in the pipeline. + It represents a part of the pipeline render process. Each stage specifies + which pipes it uses and which pipes it produces. A pipe can be seen as a + texture, which gets modified. E.g. the gbuffer pass produces the gbuffer + pipe, the ambient occlusion pass produces the occlusion pipe and so on. The + lighting pass can then specify which pipes it needs and compute the image. + Using a pipe system ensures that new techniques can be inserted easily, + without the other techniques even being aware of them """ + + required_inputs = [] + required_pipes = [] + + produced_inputs = {} + produced_pipes = {} + produced_defines = {} + + disabled = False + + def __init__(self, pipeline): + """ Creates a new render stage """ + RPObject.__init__(self) + self.stage_id = self.__class__.__name__ + self._pipeline = pipeline + self._active = True + self._targets = {} + + def create(self): + """ This method should setup the stage and create the pipes """ + raise NotImplementedError() + + def reload_shaders(self): + """ This method should set all required shaders, there should be no + shaders set in the create method, because the shader auto config is not + generated there """ + pass + + def set_shader_input(self, *args): + """ This method sets a shader input on all stages, which is mainly used + by the stage manager """ + for target in itervalues(self._targets): + target.set_shader_input(*args) + + def set_shader_inputs(self, **kwargs): + """ This method sets shader inputs on all stages, which is mainly used + by the stage manager """ + for target in itervalues(self._targets): + target.set_shader_inputs(**kwargs) + + def update(self): + """ This method gets called every frame, and can be overridden by render + stages to perform custom updates """ + pass + + @property + def active(self): + """ Returns whether *all* targets of the stage are active """ + return self._active + + @active.setter + def active(self, state): + """ Enables or disables this stage. In case the stage is disabled, it will + not get updated anymore, and all stages are distabled """ + if self._active != state: + self._active = state + for target in itervalues(self._targets): + target.active = self._active + + def create_target(self, name): + """ Creates a new render target and binds it to this stage """ + # Format the name like Plugin:Stage:Name, so it can be easily + # found in pstats below the plugin cagetory + name = self._get_plugin_id() + ":" + self.stage_id + ":" + name + if name in self._targets: + return self.error("Overriding existing target: " + name) + self._targets[name] = RenderTarget(name) + return self._targets[name] + + def remove_target(self, target): + """ Removes a previously registered target. This unregisters the + target, as well as removing it from the list of assigned targets. """ + target.remove() + target_key = None + for key, value_target in iteritems(self._targets): + if target == value_target: + target_key = key + break + del self._targets[target_key] + + def _get_shader_handle(self, path, *args): + """ Returns a handle to a Shader object, containing all sources passed + as arguments. The path argument will be used to locate shaders if no + absolute path is given. This is the internal method used in load_shader + and load_plugin_shader. """ + assert len(args) > 0 and len(args) <= 3 + path_args = [] + + for source in args: + for prefix in ("/$$rpconfig", "/$$rp/shader", "/$$rptemp"): + if prefix in source: + path_args.append(source) + break + else: + path_args.append(path.format(source)) + + # If only one shader is specified, assume its a postprocess fragment shader, + # and use the default vertex shader + if len(args) == 1: + path_args = ["/$$rp/shader/default_post_process.vert.glsl"] + path_args + return RPLoader.load_shader(*path_args) + + def _get_plugin_id(self): + """ Returns the id of the plugin which created this stage. This is done + by extracting the name of the plugin from the module name """ + if "rpcore.stages" in self.__class__.__module__: + return "render_pipeline_internal" + return str(self.__class__.__module__).split(".")[-2] + + def load_shader(self, *args): + """ Loads a shader from the given args. If only one argument is passed, + the default template for the stage is loaded. If two arguments are + passed, the first argument should be the vertex shader and the second + argument should be the fragment shader. If three arguments are passed, + the order should be vertex, fragment, geometry """ + return self._get_shader_handle("/$$rp/shader/{0}", *args) + + def load_plugin_shader(self, *args): + """ Loads a shader from the plugin directory. This method is useful + for RenderStages created by plugins. For a description of the arguments, + see the load_shader function. """ + shader_path = "rpplugins/" + self._get_plugin_id() + "/shader/{0}" + return self._get_shader_handle(shader_path, *args) + + def handle_window_resize(self): + """ This method gets called when the window gets resized. By default, + this just resizes all render targets. """ + self.set_dimensions() + for target in itervalues(self._targets): + target.consider_resize() + + def set_dimensions(self): + """ This method should set the dimensions on all targets which don't + have a relative constraint, and also the size of all images. This + is called after initialization, and when the window resized. """ + pass diff --git a/RenderPipelineFile/rpcore/render_target.py b/RenderPipelineFile/rpcore/render_target.py index 43439869..3568b695 100644 --- a/RenderPipelineFile/rpcore/render_target.py +++ b/RenderPipelineFile/rpcore/render_target.py @@ -1,400 +1,400 @@ -""" - -RenderTarget - -Copyright (c) 2015 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import print_function, division - -from panda3d.core import GraphicsOutput, Texture, AuxBitplaneAttrib, NodePath -from panda3d.core import Vec4, TransparencyAttrib, ColorWriteAttrib, SamplerState -from panda3d.core import WindowProperties, FrameBufferProperties, GraphicsPipe -from panda3d.core import LVecBase2i - -from six.moves import range # pylint: disable=import-error -from rplibs.six import iterkeys, itervalues - -from rpcore.globals import Globals -from rpcore.rpobject import RPObject -from rpcore.util.post_process_region import PostProcessRegion - -__all__ = "RenderTarget", -__version__ = "2.0" - - -class setter(object): # noqa # pylint: disable=invalid-name,too-few-public-methods - """ Setter only property """ - def __init__(self, func): - self.__func = func - self.__doc__ = func.__doc__ - - def __set__(self, name, value): - return self.__func(name, value) - - -class RenderTarget(RPObject): - - """ Second version of the RenderTarget library, provides functions - to easily setup buffers in Panda3D. """ - - NUM_ALLOCATED_BUFFERS = 0 - USE_R11G11B10 = True - REGISTERED_TARGETS = [] - CURRENT_SORT = -300 - - def __init__(self, name="target"): - RPObject.__init__(self, name) - self._targets = {} - self._color_bits = (0, 0, 0, 0) - self._aux_bits = 8 - self._aux_count = 0 - self._depth_bits = 0 - self._size = LVecBase2i(-1) - self._size_constraint = LVecBase2i(-1) - self._source_window = Globals.base.win - self._source_region = None - self._active = False - self._internal_buffer = None - self.sort = None - - # Public attributes - self.engine = Globals.base.graphicsEngine - self.support_transparency = False - self.create_default_region = True - - # Disable all global clears, since they are not required - for region in Globals.base.win.get_display_regions(): - region.disable_clears() - - def add_color_attachment(self, bits=8, alpha=False): - """ Adds a new color attachment with the given amount of bits, bits can - be either a single int or a tuple determining the bits. If bits is a - single int, alpha determines whether alpha bits are requested """ - self._targets["color"] = Texture(self.debug_name + "_color") - if isinstance(bits, (list, tuple)): - self._color_bits = (bits[0], bits[1], bits[2], bits[3] if len(bits) == 4 else 0) - else: - self._color_bits = ((bits, bits, bits, (bits if alpha else 0))) - - def add_depth_attachment(self, bits=32): - """ Adds a depth attachment wit the given amount of bits """ - self._targets["depth"] = Texture(self.debug_name + "_depth") - self._depth_bits = bits - - def add_aux_attachment(self, bits=8): - """ Adds a new aux attachment with the given amount of bits. The amount - of bits passed overrides all previous bits set, since all aux textures - have to have the same amount of bits. """ - self._aux_bits = bits - self._aux_count += 1 - - def add_aux_attachments(self, bits=8, count=1): - """ Adds n new aux attachments, with the given amount of bits. All - previously set aux bits are overriden, since all aux textures have to - have the same amount of bits """ - self._aux_bits = bits - self._aux_count += count - - @setter - def size(self, *args): - """ Sets the render target size. This can be either a single integer, - in which case it applies to both dimensions. Negative integers cause - the render target to be proportional to the screen size, i.e. a value - of -4 produces a quarter resolution target, a value of -2 a half - resolution target, and a value of -1 a full resolution target - (the default). """ - self._size_constraint = LVecBase2i(*args) - - @property - def active(self): - """ Returns whether the target is currently active """ - return self._active - - @active.setter - def active(self, flag): - """ Sets whether the target is active, this just propagates the active - flag to all display regions """ - for region in self._internal_buffer.get_display_regions(): - region.set_active(flag) - - @property - def color_tex(self): - """ Returns the color attachment if present """ - return self._targets["color"] - - @property - def depth_tex(self): - """ Returns the depth attachment if present """ - return self._targets["depth"] - - @property - def aux_tex(self): - """ Returns a list of aux textures, can be used like target.aux_tex[2], - notice the indices start at zero, so the first target has the index 0. """ - return [self._targets[i] for i in sorted(iterkeys(self._targets)) if i.startswith("aux_")] - - def set_shader_input(self, *args, **kwargs): - """ Sets a shader input available to the target """ - if self.create_default_region: - self._source_region.set_shader_input(*args, **kwargs) - - def set_shader_inputs(self, **kwargs): - """ Sets shader inputs available to the target """ - if self.create_default_region: - self._source_region.set_shader_inputs(**kwargs) - - @setter - def shader(self, shader_obj): - """ Sets a shader on the target """ - if not shader_obj: - self.error("shader must not be None!") - return - self._source_region.set_shader(shader_obj) - - @property - def internal_buffer(self): - """ Returns a handle to the internal GraphicsBuffer object """ - return self._internal_buffer - - @property - def targets(self): - """ Returns the dictionary of attachments, whereas the key is the name - of the attachment and the value is the Texture handle of the attachment """ - return self._targets - - @property - def region(self): - """ Returns the internally used PostProcessRegion """ - return self._source_region - - def prepare_render(self, camera_np): - """ Prepares to render a scene """ - self.create_default_region = False - self._create_buffer() - self._source_region = self._internal_buffer.get_display_region(0) - - if camera_np: - initial_state = NodePath("rtis") - initial_state.set_state(camera_np.node().get_initial_state()) - - if self._aux_count: - initial_state.set_attrib(AuxBitplaneAttrib.make(self._aux_bits), 20) - initial_state.set_attrib(TransparencyAttrib.make(TransparencyAttrib.M_none), 20) - - if max(self._color_bits) == 0: - initial_state.set_attrib(ColorWriteAttrib.make(ColorWriteAttrib.C_off), 20) - - # Disable existing regions of the camera - for region in camera_np.node().get_display_regions(): - region.set_active(False) - - # Remove the existing display region of the camera - for region in self._source_window.get_display_regions(): - if region.get_camera() == camera_np: - self._source_window.remove_display_region(region) - - camera_np.node().set_initial_state(initial_state.get_state()) - self._source_region.set_camera(camera_np) - - self._internal_buffer.disable_clears() - self._source_region.disable_clears() - self._source_region.set_active(True) - self._source_region.set_sort(20) - - # Reenable depth-clear, usually desireable - self._source_region.set_clear_depth_active(True) - self._source_region.set_clear_depth(1.0) - self._active = True - - def prepare_buffer(self): - """ Prepares the target to render to an offscreen buffer """ - self._create_buffer() - self._active = True - - def present_on_screen(self): - """ Prepares the target to render on the main window, to present the - final rendered image """ - self._source_region = PostProcessRegion.make(self._source_window) - self._source_region.set_sort(5) - - def remove(self): - """ Deletes this buffer, restoring the previous state """ - self._internal_buffer.clear_render_textures() - self.engine.remove_window(self._internal_buffer) - self._active = False - for target in itervalues(self._targets): - target.release_all() - RenderTarget.REGISTERED_TARGETS.remove(self) - - def set_clear_color(self, *args): - """ Sets the clear color """ - self._internal_buffer.set_clear_color_active(True) - self._internal_buffer.set_clear_color(Vec4(*args)) - - @setter - def instance_count(self, count): - """ Sets the instance count """ - self._source_region.set_instance_count(count) - - def _create_buffer(self): - """ Internal method to create the buffer object """ - self._compute_size_from_constraint() - if not self._create(): - self.error("Failed to create buffer!") - return False - - if self.create_default_region: - self._source_region = PostProcessRegion.make(self._internal_buffer) - - if max(self._color_bits) == 0: - self._source_region.set_attrib(ColorWriteAttrib.make(ColorWriteAttrib.M_none), 1000) - - def _compute_size_from_constraint(self): - """ Computes the actual size in pixels from the targets size constraint """ - w, h = Globals.resolution.x, Globals.resolution.y - self._size = LVecBase2i(self._size_constraint) - if self._size_constraint.x < 0: - self._size.x = (w - self._size_constraint.x - 1) // (-self._size_constraint.x) - if self._size_constraint.y < 0: - self._size.y = (h - self._size_constraint.y - 1) // (-self._size_constraint.y) - - def _setup_textures(self): - """ Prepares all bound textures """ - for i in range(self._aux_count): - self._targets["aux_{}".format(i)] = Texture( - self.debug_name + "_aux{}".format(i)) - for tex in itervalues(self._targets): - tex.set_wrap_u(SamplerState.WM_clamp) - tex.set_wrap_v(SamplerState.WM_clamp) - tex.set_anisotropic_degree(0) - tex.set_x_size(self._size.x) - tex.set_y_size(self._size.y) - tex.set_minfilter(SamplerState.FT_linear) - tex.set_magfilter(SamplerState.FT_linear) - - def _make_properties(self): - """ Creates the window and buffer properties """ - window_props = WindowProperties.size(self._size.x, self._size.y) - buffer_props = FrameBufferProperties() - - if self._size_constraint.x == 0 or self._size_constraint.y == 0: - window_props = WindowProperties.size(1, 1) - - if self._color_bits == (16, 16, 16, 0): - if RenderTarget.USE_R11G11B10: - buffer_props.set_rgba_bits(11, 11, 10, 0) - else: - buffer_props.set_rgba_bits(*self._color_bits) - elif 8 in self._color_bits: - # When specifying 8 bits, specify 1 bit, this is a workarround - # to a legacy logic in panda - buffer_props.set_rgba_bits(*[i if i != 8 else 1 for i in self._color_bits]) - else: - buffer_props.set_rgba_bits(*self._color_bits) - - buffer_props.set_accum_bits(0) - buffer_props.set_stencil_bits(0) - buffer_props.set_back_buffers(0) - buffer_props.set_coverage_samples(0) - buffer_props.set_depth_bits(self._depth_bits) - - if self._depth_bits == 32: - buffer_props.set_float_depth(True) - - buffer_props.set_float_color(max(self._color_bits) > 8) - - buffer_props.set_force_hardware(True) - buffer_props.set_multisamples(0) - buffer_props.set_srgb_color(False) - buffer_props.set_stereo(False) - buffer_props.set_stencil_bits(0) - - if self._aux_bits == 8: - buffer_props.set_aux_rgba(self._aux_count) - elif self._aux_bits == 16: - buffer_props.set_aux_hrgba(self._aux_count) - elif self._aux_bits == 32: - buffer_props.set_aux_float(self._aux_count) - else: - self.error("Invalid aux bits") - - return window_props, buffer_props - - def _create(self): - """ Creates the internally used buffer """ - self._setup_textures() - window_props, buffer_props = self._make_properties() - - self._internal_buffer = self.engine.make_output( - self._source_window.get_pipe(), self.debug_name, 1, - buffer_props, window_props, GraphicsPipe.BF_refuse_window | GraphicsPipe.BF_resizeable, - self._source_window.gsg, self._source_window) - - if not self._internal_buffer: - self.error("Failed to create buffer") - return - - if self._depth_bits: - self._internal_buffer.add_render_texture( - self.depth_tex, GraphicsOutput.RTM_bind_or_copy, - GraphicsOutput.RTP_depth) - - if max(self._color_bits) > 0: - self._internal_buffer.add_render_texture( - self.color_tex, GraphicsOutput.RTM_bind_or_copy, - GraphicsOutput.RTP_color) - - aux_prefix = { - 8: "RTP_aux_rgba_{}", - 16: "RTP_aux_hrgba_{}", - 32: "RTP_aux_float_{}", - }[self._aux_bits] - - for i in range(self._aux_count): - target_mode = getattr(GraphicsOutput, aux_prefix.format(i)) - self._internal_buffer.add_render_texture( - self.aux_tex[i], GraphicsOutput.RTM_bind_or_copy, target_mode) - - if not self.sort: - RenderTarget.CURRENT_SORT += 20 - self.sort = RenderTarget.CURRENT_SORT - - RenderTarget.NUM_ALLOCATED_BUFFERS += 1 - self._internal_buffer.set_sort(self.sort) - self._internal_buffer.disable_clears() - self._internal_buffer.get_display_region(0).disable_clears() - self._internal_buffer.get_overlay_display_region().disable_clears() - self._internal_buffer.get_overlay_display_region().set_active(False) - - RenderTarget.REGISTERED_TARGETS.append(self) - return True - - def consider_resize(self): - """ Checks if the target has to get resized, and if this is the case, - performs the resize. This should be called when the window resolution - changed. """ - current_size = LVecBase2i(self._size) - self._compute_size_from_constraint() - if current_size != self._size: - if self._internal_buffer: - self._internal_buffer.set_size(self._size.x, self._size.y) +""" + +RenderTarget + +Copyright (c) 2015 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import print_function, division + +from panda3d.core import GraphicsOutput, Texture, AuxBitplaneAttrib, NodePath +from panda3d.core import Vec4, TransparencyAttrib, ColorWriteAttrib, SamplerState +from panda3d.core import WindowProperties, FrameBufferProperties, GraphicsPipe +from panda3d.core import LVecBase2i + +from six.moves import range # pylint: disable=import-error +from rplibs.six import iterkeys, itervalues + +from rpcore.globals import Globals +from rpcore.rpobject import RPObject +from rpcore.util.post_process_region import PostProcessRegion + +__all__ = "RenderTarget", +__version__ = "2.0" + + +class setter(object): # noqa # pylint: disable=invalid-name,too-few-public-methods + """ Setter only property """ + def __init__(self, func): + self.__func = func + self.__doc__ = func.__doc__ + + def __set__(self, name, value): + return self.__func(name, value) + + +class RenderTarget(RPObject): + + """ Second version of the RenderTarget library, provides functions + to easily setup buffers in Panda3D. """ + + NUM_ALLOCATED_BUFFERS = 0 + USE_R11G11B10 = True + REGISTERED_TARGETS = [] + CURRENT_SORT = -300 + + def __init__(self, name="target"): + RPObject.__init__(self, name) + self._targets = {} + self._color_bits = (0, 0, 0, 0) + self._aux_bits = 8 + self._aux_count = 0 + self._depth_bits = 0 + self._size = LVecBase2i(-1) + self._size_constraint = LVecBase2i(-1) + self._source_window = Globals.base.win + self._source_region = None + self._active = False + self._internal_buffer = None + self.sort = None + + # Public attributes + self.engine = Globals.base.graphicsEngine + self.support_transparency = False + self.create_default_region = True + + # Disable all global clears, since they are not required + for region in Globals.base.win.get_display_regions(): + region.disable_clears() + + def add_color_attachment(self, bits=8, alpha=False): + """ Adds a new color attachment with the given amount of bits, bits can + be either a single int or a tuple determining the bits. If bits is a + single int, alpha determines whether alpha bits are requested """ + self._targets["color"] = Texture(self.debug_name + "_color") + if isinstance(bits, (list, tuple)): + self._color_bits = (bits[0], bits[1], bits[2], bits[3] if len(bits) == 4 else 0) + else: + self._color_bits = ((bits, bits, bits, (bits if alpha else 0))) + + def add_depth_attachment(self, bits=32): + """ Adds a depth attachment wit the given amount of bits """ + self._targets["depth"] = Texture(self.debug_name + "_depth") + self._depth_bits = bits + + def add_aux_attachment(self, bits=8): + """ Adds a new aux attachment with the given amount of bits. The amount + of bits passed overrides all previous bits set, since all aux textures + have to have the same amount of bits. """ + self._aux_bits = bits + self._aux_count += 1 + + def add_aux_attachments(self, bits=8, count=1): + """ Adds n new aux attachments, with the given amount of bits. All + previously set aux bits are overriden, since all aux textures have to + have the same amount of bits """ + self._aux_bits = bits + self._aux_count += count + + @setter + def size(self, *args): + """ Sets the render target size. This can be either a single integer, + in which case it applies to both dimensions. Negative integers cause + the render target to be proportional to the screen size, i.e. a value + of -4 produces a quarter resolution target, a value of -2 a half + resolution target, and a value of -1 a full resolution target + (the default). """ + self._size_constraint = LVecBase2i(*args) + + @property + def active(self): + """ Returns whether the target is currently active """ + return self._active + + @active.setter + def active(self, flag): + """ Sets whether the target is active, this just propagates the active + flag to all display regions """ + for region in self._internal_buffer.get_display_regions(): + region.set_active(flag) + + @property + def color_tex(self): + """ Returns the color attachment if present """ + return self._targets["color"] + + @property + def depth_tex(self): + """ Returns the depth attachment if present """ + return self._targets["depth"] + + @property + def aux_tex(self): + """ Returns a list of aux textures, can be used like target.aux_tex[2], + notice the indices start at zero, so the first target has the index 0. """ + return [self._targets[i] for i in sorted(iterkeys(self._targets)) if i.startswith("aux_")] + + def set_shader_input(self, *args, **kwargs): + """ Sets a shader input available to the target """ + if self.create_default_region: + self._source_region.set_shader_input(*args, **kwargs) + + def set_shader_inputs(self, **kwargs): + """ Sets shader inputs available to the target """ + if self.create_default_region: + self._source_region.set_shader_inputs(**kwargs) + + @setter + def shader(self, shader_obj): + """ Sets a shader on the target """ + if not shader_obj: + self.error("shader must not be None!") + return + self._source_region.set_shader(shader_obj) + + @property + def internal_buffer(self): + """ Returns a handle to the internal GraphicsBuffer object """ + return self._internal_buffer + + @property + def targets(self): + """ Returns the dictionary of attachments, whereas the key is the name + of the attachment and the value is the Texture handle of the attachment """ + return self._targets + + @property + def region(self): + """ Returns the internally used PostProcessRegion """ + return self._source_region + + def prepare_render(self, camera_np): + """ Prepares to render a scene """ + self.create_default_region = False + self._create_buffer() + self._source_region = self._internal_buffer.get_display_region(0) + + if camera_np: + initial_state = NodePath("rtis") + initial_state.set_state(camera_np.node().get_initial_state()) + + if self._aux_count: + initial_state.set_attrib(AuxBitplaneAttrib.make(self._aux_bits), 20) + initial_state.set_attrib(TransparencyAttrib.make(TransparencyAttrib.M_none), 20) + + if max(self._color_bits) == 0: + initial_state.set_attrib(ColorWriteAttrib.make(ColorWriteAttrib.C_off), 20) + + # Disable existing regions of the camera + for region in camera_np.node().get_display_regions(): + region.set_active(False) + + # Remove the existing display region of the camera + for region in self._source_window.get_display_regions(): + if region.get_camera() == camera_np: + self._source_window.remove_display_region(region) + + camera_np.node().set_initial_state(initial_state.get_state()) + self._source_region.set_camera(camera_np) + + self._internal_buffer.disable_clears() + self._source_region.disable_clears() + self._source_region.set_active(True) + self._source_region.set_sort(20) + + # Reenable depth-clear, usually desireable + self._source_region.set_clear_depth_active(True) + self._source_region.set_clear_depth(1.0) + self._active = True + + def prepare_buffer(self): + """ Prepares the target to render to an offscreen buffer """ + self._create_buffer() + self._active = True + + def present_on_screen(self): + """ Prepares the target to render on the main window, to present the + final rendered image """ + self._source_region = PostProcessRegion.make(self._source_window) + self._source_region.set_sort(5) + + def remove(self): + """ Deletes this buffer, restoring the previous state """ + self._internal_buffer.clear_render_textures() + self.engine.remove_window(self._internal_buffer) + self._active = False + for target in itervalues(self._targets): + target.release_all() + RenderTarget.REGISTERED_TARGETS.remove(self) + + def set_clear_color(self, *args): + """ Sets the clear color """ + self._internal_buffer.set_clear_color_active(True) + self._internal_buffer.set_clear_color(Vec4(*args)) + + @setter + def instance_count(self, count): + """ Sets the instance count """ + self._source_region.set_instance_count(count) + + def _create_buffer(self): + """ Internal method to create the buffer object """ + self._compute_size_from_constraint() + if not self._create(): + self.error("Failed to create buffer!") + return False + + if self.create_default_region: + self._source_region = PostProcessRegion.make(self._internal_buffer) + + if max(self._color_bits) == 0: + self._source_region.set_attrib(ColorWriteAttrib.make(ColorWriteAttrib.M_none), 1000) + + def _compute_size_from_constraint(self): + """ Computes the actual size in pixels from the targets size constraint """ + w, h = Globals.resolution.x, Globals.resolution.y + self._size = LVecBase2i(self._size_constraint) + if self._size_constraint.x < 0: + self._size.x = (w - self._size_constraint.x - 1) // (-self._size_constraint.x) + if self._size_constraint.y < 0: + self._size.y = (h - self._size_constraint.y - 1) // (-self._size_constraint.y) + + def _setup_textures(self): + """ Prepares all bound textures """ + for i in range(self._aux_count): + self._targets["aux_{}".format(i)] = Texture( + self.debug_name + "_aux{}".format(i)) + for tex in itervalues(self._targets): + tex.set_wrap_u(SamplerState.WM_clamp) + tex.set_wrap_v(SamplerState.WM_clamp) + tex.set_anisotropic_degree(0) + tex.set_x_size(self._size.x) + tex.set_y_size(self._size.y) + tex.set_minfilter(SamplerState.FT_linear) + tex.set_magfilter(SamplerState.FT_linear) + + def _make_properties(self): + """ Creates the window and buffer properties """ + window_props = WindowProperties.size(self._size.x, self._size.y) + buffer_props = FrameBufferProperties() + + if self._size_constraint.x == 0 or self._size_constraint.y == 0: + window_props = WindowProperties.size(1, 1) + + if self._color_bits == (16, 16, 16, 0): + if RenderTarget.USE_R11G11B10: + buffer_props.set_rgba_bits(11, 11, 10, 0) + else: + buffer_props.set_rgba_bits(*self._color_bits) + elif 8 in self._color_bits: + # When specifying 8 bits, specify 1 bit, this is a workarround + # to a legacy logic in panda + buffer_props.set_rgba_bits(*[i if i != 8 else 1 for i in self._color_bits]) + else: + buffer_props.set_rgba_bits(*self._color_bits) + + buffer_props.set_accum_bits(0) + buffer_props.set_stencil_bits(0) + buffer_props.set_back_buffers(0) + buffer_props.set_coverage_samples(0) + buffer_props.set_depth_bits(self._depth_bits) + + if self._depth_bits == 32: + buffer_props.set_float_depth(True) + + buffer_props.set_float_color(max(self._color_bits) > 8) + + buffer_props.set_force_hardware(True) + buffer_props.set_multisamples(0) + buffer_props.set_srgb_color(False) + buffer_props.set_stereo(False) + buffer_props.set_stencil_bits(0) + + if self._aux_bits == 8: + buffer_props.set_aux_rgba(self._aux_count) + elif self._aux_bits == 16: + buffer_props.set_aux_hrgba(self._aux_count) + elif self._aux_bits == 32: + buffer_props.set_aux_float(self._aux_count) + else: + self.error("Invalid aux bits") + + return window_props, buffer_props + + def _create(self): + """ Creates the internally used buffer """ + self._setup_textures() + window_props, buffer_props = self._make_properties() + + self._internal_buffer = self.engine.make_output( + self._source_window.get_pipe(), self.debug_name, 1, + buffer_props, window_props, GraphicsPipe.BF_refuse_window | GraphicsPipe.BF_resizeable, + self._source_window.gsg, self._source_window) + + if not self._internal_buffer: + self.error("Failed to create buffer") + return + + if self._depth_bits: + self._internal_buffer.add_render_texture( + self.depth_tex, GraphicsOutput.RTM_bind_or_copy, + GraphicsOutput.RTP_depth) + + if max(self._color_bits) > 0: + self._internal_buffer.add_render_texture( + self.color_tex, GraphicsOutput.RTM_bind_or_copy, + GraphicsOutput.RTP_color) + + aux_prefix = { + 8: "RTP_aux_rgba_{}", + 16: "RTP_aux_hrgba_{}", + 32: "RTP_aux_float_{}", + }[self._aux_bits] + + for i in range(self._aux_count): + target_mode = getattr(GraphicsOutput, aux_prefix.format(i)) + self._internal_buffer.add_render_texture( + self.aux_tex[i], GraphicsOutput.RTM_bind_or_copy, target_mode) + + if not self.sort: + RenderTarget.CURRENT_SORT += 20 + self.sort = RenderTarget.CURRENT_SORT + + RenderTarget.NUM_ALLOCATED_BUFFERS += 1 + self._internal_buffer.set_sort(self.sort) + self._internal_buffer.disable_clears() + self._internal_buffer.get_display_region(0).disable_clears() + self._internal_buffer.get_overlay_display_region().disable_clears() + self._internal_buffer.get_overlay_display_region().set_active(False) + + RenderTarget.REGISTERED_TARGETS.append(self) + return True + + def consider_resize(self): + """ Checks if the target has to get resized, and if this is the case, + performs the resize. This should be called when the window resolution + changed. """ + current_size = LVecBase2i(self._size) + self._compute_size_from_constraint() + if current_size != self._size: + if self._internal_buffer: + self._internal_buffer.set_size(self._size.x, self._size.y) diff --git a/RenderPipelineFile/rpcore/rpobject.py b/RenderPipelineFile/rpcore/rpobject.py index 1fe2b640..d06feb54 100644 --- a/RenderPipelineFile/rpcore/rpobject.py +++ b/RenderPipelineFile/rpcore/rpobject.py @@ -1,144 +1,144 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -# Disable the xxx has no yyy member warning, pylint seems to be -# unable to figure out the Colorama properties, and throws an error -# for each property. -# pylint: disable=no-member - -from __future__ import print_function - -import sys - -# Load and init colorama, used to color the output -from rplibs.colorama import init as init_colorama -from rplibs.colorama import Fore, Style -init_colorama() - - -class RPObject(object): - - """ This is the base class for every object in the render pipeline. It - provides the functions debug, warn, error and fatal for classes which - inherit from this object, including the name of the class when printing out - the message. """ - - _OUTPUT_LEVEL = 0 - _OUTPUT_LEVELS = ["debug", "warning", "error", "fatal"] - - @classmethod - def set_output_level(cls, level): - """ Sets the output level, messages with a level below will not be - printed. E.g. if you set the output level to "error", only error and - fatal messages will be shown. """ - assert level in RPObject._OUTPUT_LEVELS - RPObject._OUTPUT_LEVEL = RPObject._OUTPUT_LEVELS.index(level) - - @staticmethod - def global_debug(context, *args, **kwargs): - """ This method can be used from a static context to print a debug - message. The first argument should be the name of the object / context, - all other arguments should be the message. """ - if RPObject._OUTPUT_LEVEL > 0: - return - print(kwargs.get("color", Fore.GREEN) + "[>] " + - context.ljust(25) + " " + Style.RESET_ALL + Fore.WHITE + - ' '.join([str(i) for i in args]), Fore.RESET + Style.RESET_ALL) - - @staticmethod - def global_warn(context, *args): - """ This method can be used from a static context to print a warning. - The first argument should be the name of the object / context, all - other arguments should be the message. """ - if RPObject._OUTPUT_LEVEL > 1: - return - print(Fore.YELLOW + Style.BRIGHT + "[!] " + context.ljust(25) + - Fore.YELLOW + Style.BRIGHT + " " + ' '.join([str(i) for i in args]) + - Fore.RESET + Style.RESET_ALL) - - @staticmethod - def global_error(context, *args): - """ This method can be used from a static context to print an error. - The first argument should be the name of the object / context, all - other arguments should be the message. """ - if RPObject._OUTPUT_LEVEL > 2: - return - print(Fore.RED + Style.BRIGHT + "\n[!!!] " + - context.ljust(23) + " " + ' '.join([str(i) for i in args]) + - "\n" + Fore.RESET + Style.RESET_ALL) - - def __init__(self, name=None): - """ Initiates the RPObject with a given name. The name should be - representative about the class. If no name is given, the classname - is used """ - if name is None: - name = self.__class__.__name__ - self._debug_name = name - self._debug_color = Fore.GREEN - - def _set_debug_color(self, color, style=None): - """ Sets the color used to output debug messages """ - self._debug_color = getattr(Fore, color.upper()) - if style: - self._debug_color += getattr(Style, style.upper()) - - @property - def debug_name(self): - """ Returns the name of the debug object """ - return self._debug_name - - @debug_name.setter - def debug_name(self, name): - """ Renames this object """ - self._debug_name = name - - def debug(self, *args): - """ Outputs a debug message, something that is not necessarry - but provides useful information for the developer """ - self.global_debug(self._debug_name, *args, color=self._debug_color) - - def warn(self, *args): - """ Outputs a warning message, something that failed or does - not work, but does not prevent the program from running """ - self.global_warn(self._debug_name, *args) - - def error(self, *args): - """ Outputs an error message, something really serious. - Hopefully this never get's called! """ - self.global_error(self._debug_name, *args) - - def fatal(self, *args): - """ Outputs a fatal error message, printing out the errors and then - calling sys.exit to terminate the program. This method should be called - when something failed so hard that the pipeline has to exit. """ - # We have to set output level to 0 here, so we can print out errors - RPObject._OUTPUT_LEVEL = 0 - self.error(*args) - sys.exit(0) - - def __repr__(self): - """ Represents this object. Subclasses can override this """ - return self._debug_name +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +# Disable the xxx has no yyy member warning, pylint seems to be +# unable to figure out the Colorama properties, and throws an error +# for each property. +# pylint: disable=no-member + +from __future__ import print_function + +import sys + +# Load and init colorama, used to color the output +from rplibs.colorama import init as init_colorama +from rplibs.colorama import Fore, Style +init_colorama() + + +class RPObject(object): + + """ This is the base class for every object in the render pipeline. It + provides the functions debug, warn, error and fatal for classes which + inherit from this object, including the name of the class when printing out + the message. """ + + _OUTPUT_LEVEL = 0 + _OUTPUT_LEVELS = ["debug", "warning", "error", "fatal"] + + @classmethod + def set_output_level(cls, level): + """ Sets the output level, messages with a level below will not be + printed. E.g. if you set the output level to "error", only error and + fatal messages will be shown. """ + assert level in RPObject._OUTPUT_LEVELS + RPObject._OUTPUT_LEVEL = RPObject._OUTPUT_LEVELS.index(level) + + @staticmethod + def global_debug(context, *args, **kwargs): + """ This method can be used from a static context to print a debug + message. The first argument should be the name of the object / context, + all other arguments should be the message. """ + if RPObject._OUTPUT_LEVEL > 0: + return + print(kwargs.get("color", Fore.GREEN) + "[>] " + + context.ljust(25) + " " + Style.RESET_ALL + Fore.WHITE + + ' '.join([str(i) for i in args]), Fore.RESET + Style.RESET_ALL) + + @staticmethod + def global_warn(context, *args): + """ This method can be used from a static context to print a warning. + The first argument should be the name of the object / context, all + other arguments should be the message. """ + if RPObject._OUTPUT_LEVEL > 1: + return + print(Fore.YELLOW + Style.BRIGHT + "[!] " + context.ljust(25) + + Fore.YELLOW + Style.BRIGHT + " " + ' '.join([str(i) for i in args]) + + Fore.RESET + Style.RESET_ALL) + + @staticmethod + def global_error(context, *args): + """ This method can be used from a static context to print an error. + The first argument should be the name of the object / context, all + other arguments should be the message. """ + if RPObject._OUTPUT_LEVEL > 2: + return + print(Fore.RED + Style.BRIGHT + "\n[!!!] " + + context.ljust(23) + " " + ' '.join([str(i) for i in args]) + + "\n" + Fore.RESET + Style.RESET_ALL) + + def __init__(self, name=None): + """ Initiates the RPObject with a given name. The name should be + representative about the class. If no name is given, the classname + is used """ + if name is None: + name = self.__class__.__name__ + self._debug_name = name + self._debug_color = Fore.GREEN + + def _set_debug_color(self, color, style=None): + """ Sets the color used to output debug messages """ + self._debug_color = getattr(Fore, color.upper()) + if style: + self._debug_color += getattr(Style, style.upper()) + + @property + def debug_name(self): + """ Returns the name of the debug object """ + return self._debug_name + + @debug_name.setter + def debug_name(self, name): + """ Renames this object """ + self._debug_name = name + + def debug(self, *args): + """ Outputs a debug message, something that is not necessarry + but provides useful information for the developer """ + self.global_debug(self._debug_name, *args, color=self._debug_color) + + def warn(self, *args): + """ Outputs a warning message, something that failed or does + not work, but does not prevent the program from running """ + self.global_warn(self._debug_name, *args) + + def error(self, *args): + """ Outputs an error message, something really serious. + Hopefully this never get's called! """ + self.global_error(self._debug_name, *args) + + def fatal(self, *args): + """ Outputs a fatal error message, printing out the errors and then + calling sys.exit to terminate the program. This method should be called + when something failed so hard that the pipeline has to exit. """ + # We have to set output level to 0 here, so we can print out errors + RPObject._OUTPUT_LEVEL = 0 + self.error(*args) + sys.exit(0) + + def __repr__(self): + """ Represents this object. Subclasses can override this """ + return self._debug_name diff --git a/RenderPipelineFile/rpcore/shader/ambient_stage.frag.glsl b/RenderPipelineFile/rpcore/shader/ambient_stage.frag.glsl index 51fab7e7..87ff5998 100644 --- a/RenderPipelineFile/rpcore/shader/ambient_stage.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/ambient_stage.frag.glsl @@ -1,294 +1,294 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// This shader applies the ambient term to the shaded scene - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "includes/brdf.inc.glsl" -#pragma include "includes/lights.inc.glsl" -#pragma include "includes/color_spaces.inc.glsl" - -uniform sampler2D ShadedScene; -uniform GBufferData GBuffer; -uniform sampler3D PrefilteredBRDF; -uniform sampler2D PrefilteredMetalBRDF; -uniform sampler2D PrefilteredCoatBRDF; - -uniform samplerCube DefaultEnvmap; - -#if HAVE_PLUGIN(scattering) - uniform samplerCube ScatteringIBLDiffuse; - uniform samplerCube ScatteringIBLSpecular; -#endif - -#if HAVE_PLUGIN(ao) - uniform sampler2D AmbientOcclusion; -#endif - -#if HAVE_PLUGIN(sky_ao) - uniform sampler2D SkyAO; -#endif - -#if HAVE_PLUGIN(vxgi) - // uniform sampler2D VXGISpecular; - uniform sampler2D VXGIDiffuse; -#endif - -#if HAVE_PLUGIN(env_probes) - uniform sampler2D EnvmapAmbientDiff; - uniform sampler2D EnvmapAmbientSpec; -#endif - -#if HAVE_PLUGIN(ssr) - uniform sampler2D SSRSpecular; -#endif - -out vec4 result; - -float compute_specular_occlusion(float NxV, float occlusion, float roughness) { - return saturate(pow(NxV + occlusion, roughness) - 1 + occlusion); -} - -// From: http://www.frostbite.com/wp-content/uploads/2014/11/course_notes_moving_frostbite_to_pbr.pdf -vec3 compute_bloom_luminance(vec3 bloom_color, float bloom_ec, float current_ev) { - float bloom_ev = current_ev + bloom_ec; - return bloom_color * pow(2, bloom_ev - 3); -} - -void main() { - vec2 texcoord = get_texcoord(); - Material m = unpack_material(GBuffer); - vec3 view_vector = normalize(m.position - MainSceneData.camera_pos); - - // Store the accumulated ambient term in a variable - vec3 ambient = vec3(0); - - #if !DEBUG_MODE || MODE_ACTIVE(DIFFUSE_AMBIENT) || MODE_ACTIVE(SPECULAR_AMBIENT) || MODE_ACTIVE(OCCLUSION) || MODE_ACTIVE(SKY_AO) - - // Skip skybox shading - if (is_skybox(m)) { - - #if !REFERENCE_MODE - result = textureLod(ShadedScene, texcoord, 0); - - #if !HAVE_PLUGIN(scattering) - result = textureLod( - DefaultEnvmap, view_vector.yxz * vec3(-1, 1, 1), 0) * - DEFAULT_ENVMAP_BRIGHTNESS; - #endif - #else - - // When in reference mode, display the used environment cubemap as background - #if USE_WHITE_ENVIRONMENT - result = vec4(1); - #else - result = textureLod(DefaultEnvmap, view_vector.yxz * vec3(-1, 1, 1), 0); - #endif - #endif - - return; - } - - - #if HAVE_PLUGIN(sky_ao) - float sky_ao_factor = textureLod(SkyAO, texcoord, 0).x; - sky_ao_factor = pow(sky_ao_factor, 3.0); - - // Prevent total dark ao term - sky_ao_factor = max(0.2, sky_ao_factor); - #else - float sky_ao_factor = 1.0; - #endif - - // Get reflection directory - vec3 reflected_dir = get_reflection_vector(m, view_vector); - float roughness = get_effective_roughness(m); - - // Compute angle between normal and view vector - float NxV = clamp(-dot(m.normal, view_vector), 1e-5, 1.0); - - // Get mipmap offset for the material roughness - float env_mipmap = get_mipmap_for_roughness(DefaultEnvmap, roughness , NxV); - - // Sample default environment map - vec3 ibl_specular = textureLod(DefaultEnvmap, - fix_cubemap_coord(reflected_dir), env_mipmap).xyz * DEFAULT_ENVMAP_BRIGHTNESS; - - // Get cheap irradiance by sampling low levels of the environment map - float ibl_diffuse_mip = get_mipmap_count(DefaultEnvmap) - 3.0; - vec3 ibl_diffuse = textureLod(DefaultEnvmap, fix_cubemap_coord(m.normal), - ibl_diffuse_mip).xyz * DEFAULT_ENVMAP_BRIGHTNESS; - - // Scattering specific code - #if HAVE_PLUGIN(scattering) - float scat_mipmap = m.shading_model == SHADING_MODEL_CLEARCOAT ? - CLEARCOAT_ROUGHNESS : get_specular_mipmap(m); - - ibl_specular = textureLod(ScatteringIBLSpecular, reflected_dir, scat_mipmap).xyz * sky_ao_factor; - ibl_diffuse = textureLod(ScatteringIBLDiffuse, m.normal, 0).xyz * sky_ao_factor; - #endif - - // Blend environment maps - #if HAVE_PLUGIN(env_probes) - vec4 probe_spec = textureLod(EnvmapAmbientSpec, texcoord, 0); - vec4 probe_diff = textureLod(EnvmapAmbientDiff, texcoord, 0); - ibl_diffuse = ibl_diffuse * (1 - probe_diff.w) + probe_diff.xyz; - ibl_specular = ibl_specular * (1 - probe_spec.w) + probe_spec.xyz; - #endif - - // Blend VXGI on top - #if HAVE_PLUGIN(vxgi) - // vec4 vxgi_spec = textureLod(VXGISpecular, texcoord, 0); - ibl_diffuse = textureLod(VXGIDiffuse, texcoord, 0).xyz; - // ibl_specular *= ibl_diffuse; - // ibl_specular = ibl_specular * (1 - vxgi_spec.w) + vxgi_spec.xyz; - #endif - - // Blend screen space reflections - #if HAVE_PLUGIN(ssr) - vec4 ssr_spec = textureLod(SSRSpecular, texcoord, 0); - - // Fade out SSR on high roughness values - ssr_spec *= 1.0 - saturate(m.roughness / GET_SETTING(ssr, roughness_fade)); - ssr_spec *= GET_SETTING(ssr, effect_scale); - ibl_specular = ibl_specular * (1 - ssr_spec.w) + ssr_spec.xyz; - #endif - - #if USE_WHITE_ENVIRONMENT - ibl_specular = vec3(1); - ibl_diffuse = vec3(1); - #endif - - vec3 material_f0 = get_material_f0(m); - vec3 specular_ambient = vec3(0); - - // Pre-Integrated environment BRDF - vec3 env_brdf = get_brdf_from_lut(PrefilteredBRDF, NxV, sqrt(roughness), m.specular_ior); - - // Exact metallic brdf lut, unused right now - // vec3 env_brdf_metal = get_brdf_from_lut(PrefilteredMetalBRDF, NxV, roughness); - - // Diffuse and fresnel ambient term - vec3 diffuse_ambient = ibl_diffuse * m.basecolor * (1-m.metallic); - vec3 fresnel = env_brdf.ggg; - diffuse_ambient *= env_brdf.r; - - // Approximate metallic fresnel - vec3 metallic_fresnel = get_metallic_fresnel_approx(m, NxV); - - // Mix between normal and metallic fresnel - fresnel = mix(fresnel, metallic_fresnel, m.metallic); - - if (m.shading_model == SHADING_MODEL_CLEARCOAT) { - vec3 env_brdf_coat = get_brdf_from_lut( - PrefilteredCoatBRDF, NxV, m.linear_roughness * 1.333); - - #if HAVE_PLUGIN(scattering) - vec3 ibl_specular_base = textureLod( - ScatteringIBLSpecular, reflected_dir, - get_specular_mipmap(m)).xyz * sky_ao_factor; - #else - vec3 ibl_specular_base = textureLod( - DefaultEnvmap, fix_cubemap_coord(reflected_dir), - get_mipmap_for_roughness(DefaultEnvmap, m.roughness, NxV)).xyz * - DEFAULT_ENVMAP_BRIGHTNESS; - #endif - - #if REFERENCE_MODE && USE_WHITE_ENVIRONMENT - // ibl_specular_base = vec3(1); - #endif - - specular_ambient = env_brdf_coat.g * ibl_specular; - specular_ambient += env_brdf_coat.r * ibl_specular_base * m.basecolor; - } else { - specular_ambient = fresnel * ibl_specular; - } - - // Sample precomputed occlusion and multiply the ambient term with it - #if HAVE_PLUGIN(ao) - float occlusion = textureLod(AmbientOcclusion, texcoord, 0).x; - #else - float occlusion = 1.0; - #endif - - - float specular_occlusion = compute_specular_occlusion(NxV, occlusion, roughness); - - // Add diffuse and specular ambient term - ambient += diffuse_ambient * occlusion + specular_ambient * specular_occlusion; - - #endif - - // Emissive materials - #if !DEBUG_MODE - if (m.shading_model == SHADING_MODEL_EMISSIVE) { - // TODO: For emissive, use: compute_bloom_luminance() instead of a fixed value - ambient = m.basecolor * 5000.0; - } - #endif - - #if DEBUG_MODE - #if MODE_ACTIVE(OCCLUSION) - result = vec4(occlusion); - result.w = 1; - return; - #endif - - #if MODE_ACTIVE(SKY_AO) - result = vec4(sky_ao_factor); - result.w = 1; - return; - #endif - - #if MODE_ACTIVE(DIFFUSE_AMBIENT) - result = vec4((diffuse_ambient / (1 + diffuse_ambient)) * occlusion, 1); - return; - #endif - - #if MODE_ACTIVE(SPECULAR_AMBIENT) - result = vec4((specular_ambient / (1 + specular_ambient)) * specular_occlusion, 1); - return; - #endif - #endif - - vec4 scene_color = textureLod(ShadedScene, texcoord, 0); - - #if HAVE_PLUGIN(scattering) && !DEBUG_MODE - // Scattering extinction is stored in the w component of the scene texture - ambient *= scene_color.w; - #endif - - #if MODE_ACTIVE(ENVPROBE_COUNT) - // Pass through debug modes - result = texture(EnvmapAmbientDiff, texcoord); - return; - #endif - - result = vec4(scene_color.xyz + ambient, 1); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// This shader applies the ambient term to the shaded scene + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "includes/brdf.inc.glsl" +#pragma include "includes/lights.inc.glsl" +#pragma include "includes/color_spaces.inc.glsl" + +uniform sampler2D ShadedScene; +uniform GBufferData GBuffer; +uniform sampler3D PrefilteredBRDF; +uniform sampler2D PrefilteredMetalBRDF; +uniform sampler2D PrefilteredCoatBRDF; + +uniform samplerCube DefaultEnvmap; + +#if HAVE_PLUGIN(scattering) + uniform samplerCube ScatteringIBLDiffuse; + uniform samplerCube ScatteringIBLSpecular; +#endif + +#if HAVE_PLUGIN(ao) + uniform sampler2D AmbientOcclusion; +#endif + +#if HAVE_PLUGIN(sky_ao) + uniform sampler2D SkyAO; +#endif + +#if HAVE_PLUGIN(vxgi) + // uniform sampler2D VXGISpecular; + uniform sampler2D VXGIDiffuse; +#endif + +#if HAVE_PLUGIN(env_probes) + uniform sampler2D EnvmapAmbientDiff; + uniform sampler2D EnvmapAmbientSpec; +#endif + +#if HAVE_PLUGIN(ssr) + uniform sampler2D SSRSpecular; +#endif + +out vec4 result; + +float compute_specular_occlusion(float NxV, float occlusion, float roughness) { + return saturate(pow(NxV + occlusion, roughness) - 1 + occlusion); +} + +// From: http://www.frostbite.com/wp-content/uploads/2014/11/course_notes_moving_frostbite_to_pbr.pdf +vec3 compute_bloom_luminance(vec3 bloom_color, float bloom_ec, float current_ev) { + float bloom_ev = current_ev + bloom_ec; + return bloom_color * pow(2, bloom_ev - 3); +} + +void main() { + vec2 texcoord = get_texcoord(); + Material m = unpack_material(GBuffer); + vec3 view_vector = normalize(m.position - MainSceneData.camera_pos); + + // Store the accumulated ambient term in a variable + vec3 ambient = vec3(0); + + #if !DEBUG_MODE || MODE_ACTIVE(DIFFUSE_AMBIENT) || MODE_ACTIVE(SPECULAR_AMBIENT) || MODE_ACTIVE(OCCLUSION) || MODE_ACTIVE(SKY_AO) + + // Skip skybox shading + if (is_skybox(m)) { + + #if !REFERENCE_MODE + result = textureLod(ShadedScene, texcoord, 0); + + #if !HAVE_PLUGIN(scattering) + result = textureLod( + DefaultEnvmap, view_vector.yxz * vec3(-1, 1, 1), 0) * + DEFAULT_ENVMAP_BRIGHTNESS; + #endif + #else + + // When in reference mode, display the used environment cubemap as background + #if USE_WHITE_ENVIRONMENT + result = vec4(1); + #else + result = textureLod(DefaultEnvmap, view_vector.yxz * vec3(-1, 1, 1), 0); + #endif + #endif + + return; + } + + + #if HAVE_PLUGIN(sky_ao) + float sky_ao_factor = textureLod(SkyAO, texcoord, 0).x; + sky_ao_factor = pow(sky_ao_factor, 3.0); + + // Prevent total dark ao term + sky_ao_factor = max(0.2, sky_ao_factor); + #else + float sky_ao_factor = 1.0; + #endif + + // Get reflection directory + vec3 reflected_dir = get_reflection_vector(m, view_vector); + float roughness = get_effective_roughness(m); + + // Compute angle between normal and view vector + float NxV = clamp(-dot(m.normal, view_vector), 1e-5, 1.0); + + // Get mipmap offset for the material roughness + float env_mipmap = get_mipmap_for_roughness(DefaultEnvmap, roughness , NxV); + + // Sample default environment map + vec3 ibl_specular = textureLod(DefaultEnvmap, + fix_cubemap_coord(reflected_dir), env_mipmap).xyz * DEFAULT_ENVMAP_BRIGHTNESS; + + // Get cheap irradiance by sampling low levels of the environment map + float ibl_diffuse_mip = get_mipmap_count(DefaultEnvmap) - 3.0; + vec3 ibl_diffuse = textureLod(DefaultEnvmap, fix_cubemap_coord(m.normal), + ibl_diffuse_mip).xyz * DEFAULT_ENVMAP_BRIGHTNESS; + + // Scattering specific code + #if HAVE_PLUGIN(scattering) + float scat_mipmap = m.shading_model == SHADING_MODEL_CLEARCOAT ? + CLEARCOAT_ROUGHNESS : get_specular_mipmap(m); + + ibl_specular = textureLod(ScatteringIBLSpecular, reflected_dir, scat_mipmap).xyz * sky_ao_factor; + ibl_diffuse = textureLod(ScatteringIBLDiffuse, m.normal, 0).xyz * sky_ao_factor; + #endif + + // Blend environment maps + #if HAVE_PLUGIN(env_probes) + vec4 probe_spec = textureLod(EnvmapAmbientSpec, texcoord, 0); + vec4 probe_diff = textureLod(EnvmapAmbientDiff, texcoord, 0); + ibl_diffuse = ibl_diffuse * (1 - probe_diff.w) + probe_diff.xyz; + ibl_specular = ibl_specular * (1 - probe_spec.w) + probe_spec.xyz; + #endif + + // Blend VXGI on top + #if HAVE_PLUGIN(vxgi) + // vec4 vxgi_spec = textureLod(VXGISpecular, texcoord, 0); + ibl_diffuse = textureLod(VXGIDiffuse, texcoord, 0).xyz; + // ibl_specular *= ibl_diffuse; + // ibl_specular = ibl_specular * (1 - vxgi_spec.w) + vxgi_spec.xyz; + #endif + + // Blend screen space reflections + #if HAVE_PLUGIN(ssr) + vec4 ssr_spec = textureLod(SSRSpecular, texcoord, 0); + + // Fade out SSR on high roughness values + ssr_spec *= 1.0 - saturate(m.roughness / GET_SETTING(ssr, roughness_fade)); + ssr_spec *= GET_SETTING(ssr, effect_scale); + ibl_specular = ibl_specular * (1 - ssr_spec.w) + ssr_spec.xyz; + #endif + + #if USE_WHITE_ENVIRONMENT + ibl_specular = vec3(1); + ibl_diffuse = vec3(1); + #endif + + vec3 material_f0 = get_material_f0(m); + vec3 specular_ambient = vec3(0); + + // Pre-Integrated environment BRDF + vec3 env_brdf = get_brdf_from_lut(PrefilteredBRDF, NxV, sqrt(roughness), m.specular_ior); + + // Exact metallic brdf lut, unused right now + // vec3 env_brdf_metal = get_brdf_from_lut(PrefilteredMetalBRDF, NxV, roughness); + + // Diffuse and fresnel ambient term + vec3 diffuse_ambient = ibl_diffuse * m.basecolor * (1-m.metallic); + vec3 fresnel = env_brdf.ggg; + diffuse_ambient *= env_brdf.r; + + // Approximate metallic fresnel + vec3 metallic_fresnel = get_metallic_fresnel_approx(m, NxV); + + // Mix between normal and metallic fresnel + fresnel = mix(fresnel, metallic_fresnel, m.metallic); + + if (m.shading_model == SHADING_MODEL_CLEARCOAT) { + vec3 env_brdf_coat = get_brdf_from_lut( + PrefilteredCoatBRDF, NxV, m.linear_roughness * 1.333); + + #if HAVE_PLUGIN(scattering) + vec3 ibl_specular_base = textureLod( + ScatteringIBLSpecular, reflected_dir, + get_specular_mipmap(m)).xyz * sky_ao_factor; + #else + vec3 ibl_specular_base = textureLod( + DefaultEnvmap, fix_cubemap_coord(reflected_dir), + get_mipmap_for_roughness(DefaultEnvmap, m.roughness, NxV)).xyz * + DEFAULT_ENVMAP_BRIGHTNESS; + #endif + + #if REFERENCE_MODE && USE_WHITE_ENVIRONMENT + // ibl_specular_base = vec3(1); + #endif + + specular_ambient = env_brdf_coat.g * ibl_specular; + specular_ambient += env_brdf_coat.r * ibl_specular_base * m.basecolor; + } else { + specular_ambient = fresnel * ibl_specular; + } + + // Sample precomputed occlusion and multiply the ambient term with it + #if HAVE_PLUGIN(ao) + float occlusion = textureLod(AmbientOcclusion, texcoord, 0).x; + #else + float occlusion = 1.0; + #endif + + + float specular_occlusion = compute_specular_occlusion(NxV, occlusion, roughness); + + // Add diffuse and specular ambient term + ambient += diffuse_ambient * occlusion + specular_ambient * specular_occlusion; + + #endif + + // Emissive materials + #if !DEBUG_MODE + if (m.shading_model == SHADING_MODEL_EMISSIVE) { + // TODO: For emissive, use: compute_bloom_luminance() instead of a fixed value + ambient = m.basecolor * 5000.0; + } + #endif + + #if DEBUG_MODE + #if MODE_ACTIVE(OCCLUSION) + result = vec4(occlusion); + result.w = 1; + return; + #endif + + #if MODE_ACTIVE(SKY_AO) + result = vec4(sky_ao_factor); + result.w = 1; + return; + #endif + + #if MODE_ACTIVE(DIFFUSE_AMBIENT) + result = vec4((diffuse_ambient / (1 + diffuse_ambient)) * occlusion, 1); + return; + #endif + + #if MODE_ACTIVE(SPECULAR_AMBIENT) + result = vec4((specular_ambient / (1 + specular_ambient)) * specular_occlusion, 1); + return; + #endif + #endif + + vec4 scene_color = textureLod(ShadedScene, texcoord, 0); + + #if HAVE_PLUGIN(scattering) && !DEBUG_MODE + // Scattering extinction is stored in the w component of the scene texture + ambient *= scene_color.w; + #endif + + #if MODE_ACTIVE(ENVPROBE_COUNT) + // Pass through debug modes + result = texture(EnvmapAmbientDiff, texcoord); + return; + #endif + + result = vec4(scene_color.xyz + ambient, 1); +} diff --git a/RenderPipelineFile/rpcore/shader/apply_lights.frag.glsl b/RenderPipelineFile/rpcore/shader/apply_lights.frag.glsl index a2c433c8..4acfdca2 100644 --- a/RenderPipelineFile/rpcore/shader/apply_lights.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/apply_lights.frag.glsl @@ -1,121 +1,121 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// This shader performs the screen space shading of all lights - -#pragma include "render_pipeline_base.inc.glsl" - -// Tell the lighting pipeline we are doing this in screen space, so gl_FragCoord -// is available. -#define IS_SCREEN_SPACE 1 - -#pragma include "includes/light_culling.inc.glsl" -#pragma include "includes/transforms.inc.glsl" -#pragma include "includes/lighting_pipeline.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" - -out vec4 result; - -uniform GBufferData GBuffer; - -// Used for velocity rendering mode -#if MODE_ACTIVE(VELOCITY) - uniform sampler2D CombinedVelocity; -#endif - -void main() { - - // Extract material properties - vec2 texcoord = get_texcoord(); - Material m = unpack_material(GBuffer); - ivec3 tile = get_lc_cell_index(ivec2(gl_FragCoord.xy), - distance(MainSceneData.camera_pos, m.position)); - - // Don't shade pixels out of the shading range - #if !DEBUG_MODE - if (tile.z >= LC_TILE_SLICES) { - result = vec4(0, 0, 0, 1); - return; - } - #endif - - // Apply all lights - result = vec4(shade_material_from_tile_buffer(m, tile), 1); - - /* - - Various debugging modes for previewing materials - - */ - - #if MODE_ACTIVE(DIFFUSE) - result.xyz = vec3(m.basecolor); - #endif - - #if MODE_ACTIVE(ROUGHNESS) - result.xyz = vec3(m.roughness); - #endif - - #if MODE_ACTIVE(SPECULAR) - - // Multiply specular on diffuse materials to make it visible - result.xyz = vec3(mix( - m.specular / 0.18, - m.specular, - m.metallic - )); - #endif - - #if MODE_ACTIVE(NORMAL) - result.xyz = vec3(abs(m.normal)); - #endif - - #if MODE_ACTIVE(METALLIC) - result.xyz = vec3(m.metallic); - #endif - - #if MODE_ACTIVE(TRANSLUCENCY) - result.xyz = vec3(m.shading_model == SHADING_MODEL_FOLIAGE ? m.shading_model_param0 : 0.0); - #endif - - #if MODE_ACTIVE(SHADING_MODEL) - result.xyz = vec3(0.1, 0.1, 0.1); - switch (m.shading_model) { - case SHADING_MODEL_FOLIAGE: result.xyz = vec3(0, 1, 0); break; - case SHADING_MODEL_CLEARCOAT: result.xyz = vec3(0, 0, 1); break; - case SHADING_MODEL_SKIN: result.xyz = vec3(1, 0, 0); break; - case SHADING_MODEL_EMISSIVE: result.xyz = vec3(1, 0, 1); break; - case SHADING_MODEL_TRANSPARENT: result.xyz = vec3(0, 1, 1); break; - } - #endif - - #if MODE_ACTIVE(VELOCITY) - result.xyz = abs(textureLod(CombinedVelocity, texcoord, 0).xyz) * 20.0; - #endif - -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// This shader performs the screen space shading of all lights + +#pragma include "render_pipeline_base.inc.glsl" + +// Tell the lighting pipeline we are doing this in screen space, so gl_FragCoord +// is available. +#define IS_SCREEN_SPACE 1 + +#pragma include "includes/light_culling.inc.glsl" +#pragma include "includes/transforms.inc.glsl" +#pragma include "includes/lighting_pipeline.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" + +out vec4 result; + +uniform GBufferData GBuffer; + +// Used for velocity rendering mode +#if MODE_ACTIVE(VELOCITY) + uniform sampler2D CombinedVelocity; +#endif + +void main() { + + // Extract material properties + vec2 texcoord = get_texcoord(); + Material m = unpack_material(GBuffer); + ivec3 tile = get_lc_cell_index(ivec2(gl_FragCoord.xy), + distance(MainSceneData.camera_pos, m.position)); + + // Don't shade pixels out of the shading range + #if !DEBUG_MODE + if (tile.z >= LC_TILE_SLICES) { + result = vec4(0, 0, 0, 1); + return; + } + #endif + + // Apply all lights + result = vec4(shade_material_from_tile_buffer(m, tile), 1); + + /* + + Various debugging modes for previewing materials + + */ + + #if MODE_ACTIVE(DIFFUSE) + result.xyz = vec3(m.basecolor); + #endif + + #if MODE_ACTIVE(ROUGHNESS) + result.xyz = vec3(m.roughness); + #endif + + #if MODE_ACTIVE(SPECULAR) + + // Multiply specular on diffuse materials to make it visible + result.xyz = vec3(mix( + m.specular / 0.18, + m.specular, + m.metallic + )); + #endif + + #if MODE_ACTIVE(NORMAL) + result.xyz = vec3(abs(m.normal)); + #endif + + #if MODE_ACTIVE(METALLIC) + result.xyz = vec3(m.metallic); + #endif + + #if MODE_ACTIVE(TRANSLUCENCY) + result.xyz = vec3(m.shading_model == SHADING_MODEL_FOLIAGE ? m.shading_model_param0 : 0.0); + #endif + + #if MODE_ACTIVE(SHADING_MODEL) + result.xyz = vec3(0.1, 0.1, 0.1); + switch (m.shading_model) { + case SHADING_MODEL_FOLIAGE: result.xyz = vec3(0, 1, 0); break; + case SHADING_MODEL_CLEARCOAT: result.xyz = vec3(0, 0, 1); break; + case SHADING_MODEL_SKIN: result.xyz = vec3(1, 0, 0); break; + case SHADING_MODEL_EMISSIVE: result.xyz = vec3(1, 0, 1); break; + case SHADING_MODEL_TRANSPARENT: result.xyz = vec3(0, 1, 1); break; + } + #endif + + #if MODE_ACTIVE(VELOCITY) + result.xyz = abs(textureLod(CombinedVelocity, texcoord, 0).xyz) * 20.0; + #endif + +} diff --git a/RenderPipelineFile/rpcore/shader/bilateral_blur.frag.glsl b/RenderPipelineFile/rpcore/shader/bilateral_blur.frag.glsl index af90d4b1..336116a8 100644 --- a/RenderPipelineFile/rpcore/shader/bilateral_blur.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/bilateral_blur.frag.glsl @@ -1,92 +1,92 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// This shader provides a generic blur respecting depth and normals - -#pragma optionNV (unroll all) - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "includes/gaussian_weights.inc.glsl" - -uniform ivec2 blur_direction; -uniform sampler2D SourceTex; -uniform sampler2D DownscaledDepth; -uniform GBufferData GBuffer; - -uniform float pixel_stretch; - -// Can be used to only select a specific component. Currently hardcoded and unused. -#define ONLY_RED_COMPONENT 1 -#if ONLY_RED_COMPONENT - #define VALUE_TYPE float - #define SWIZLLE .x -#else - #define VALUE_TYPE vec4 - #define SWIZLLE .xyzw -#endif - -out VALUE_TYPE result; - -void main() { - vec2 texcoord = get_texcoord(); - vec2 pixel_size = 1.0 * pixel_stretch / SCREEN_SIZE; - - // Store accumulated color - VALUE_TYPE accum = VALUE_TYPE(0); - float accum_w = 0.0; - - // Amount of samples, don't forget to change the weights array in case you change this. - const int blur_size = 7; - - // Get the mid pixel normal and depth - vec3 pixel_nrm = get_gbuffer_normal(GBuffer, texcoord); - float pixel_depth = textureLod(DownscaledDepth, texcoord, 0).x; - - // Blur to the right and left - for (int i = -blur_size + 1; i < blur_size; ++i) { - vec2 offcoord = texcoord + pixel_size * i * blur_direction; - VALUE_TYPE sampled = textureLod(SourceTex, offcoord, 0) SWIZLLE; - vec3 nrm = get_gbuffer_normal(GBuffer, offcoord); - float depth = textureLod(DownscaledDepth, offcoord, 0).x; - - float weight = gaussian_weights_7[abs(i)]; // Change this if you modify the blur size - - // Weight by normal and depth - weight *= 1.0 - saturate(GET_SETTING(ao, blur_normal_factor) * - distance(nrm, pixel_nrm) * 1.0); - weight *= 1.0 - saturate(GET_SETTING(ao, blur_depth_factor) * - abs(depth - pixel_depth) * 3); - - accum += sampled * weight; - accum_w += weight; - } - - accum /= max(0.04, accum_w); - result = accum; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// This shader provides a generic blur respecting depth and normals + +#pragma optionNV (unroll all) + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "includes/gaussian_weights.inc.glsl" + +uniform ivec2 blur_direction; +uniform sampler2D SourceTex; +uniform sampler2D DownscaledDepth; +uniform GBufferData GBuffer; + +uniform float pixel_stretch; + +// Can be used to only select a specific component. Currently hardcoded and unused. +#define ONLY_RED_COMPONENT 1 +#if ONLY_RED_COMPONENT + #define VALUE_TYPE float + #define SWIZLLE .x +#else + #define VALUE_TYPE vec4 + #define SWIZLLE .xyzw +#endif + +out VALUE_TYPE result; + +void main() { + vec2 texcoord = get_texcoord(); + vec2 pixel_size = 1.0 * pixel_stretch / SCREEN_SIZE; + + // Store accumulated color + VALUE_TYPE accum = VALUE_TYPE(0); + float accum_w = 0.0; + + // Amount of samples, don't forget to change the weights array in case you change this. + const int blur_size = 7; + + // Get the mid pixel normal and depth + vec3 pixel_nrm = get_gbuffer_normal(GBuffer, texcoord); + float pixel_depth = textureLod(DownscaledDepth, texcoord, 0).x; + + // Blur to the right and left + for (int i = -blur_size + 1; i < blur_size; ++i) { + vec2 offcoord = texcoord + pixel_size * i * blur_direction; + VALUE_TYPE sampled = textureLod(SourceTex, offcoord, 0) SWIZLLE; + vec3 nrm = get_gbuffer_normal(GBuffer, offcoord); + float depth = textureLod(DownscaledDepth, offcoord, 0).x; + + float weight = gaussian_weights_7[abs(i)]; // Change this if you modify the blur size + + // Weight by normal and depth + weight *= 1.0 - saturate(GET_SETTING(ao, blur_normal_factor) * + distance(nrm, pixel_nrm) * 1.0); + weight *= 1.0 - saturate(GET_SETTING(ao, blur_depth_factor) * + abs(depth - pixel_depth) * 3); + + accum += sampled * weight; + accum_w += weight; + } + + accum /= max(0.04, accum_w); + result = accum; +} diff --git a/RenderPipelineFile/rpcore/shader/bilateral_halfres_blur.frag.glsl b/RenderPipelineFile/rpcore/shader/bilateral_halfres_blur.frag.glsl index 4015964b..3ae2f9ce 100644 --- a/RenderPipelineFile/rpcore/shader/bilateral_halfres_blur.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/bilateral_halfres_blur.frag.glsl @@ -1,81 +1,81 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// This shader provides a bilateral blur at half-resolution - -#pragma optionNV (unroll all) - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "includes/gaussian_weights.inc.glsl" - -uniform ivec2 blur_direction; -uniform sampler2D SourceTex; -uniform GBufferData GBuffer; - -out vec4 result; - -float get_lin_z(vec2 ccoord) { - return get_linear_z_from_z(get_gbuffer_depth(GBuffer, ccoord)); -} - -void main() { - vec2 texcoord = get_half_native_texcoord(); - vec2 pixel_size = 2.0 / SCREEN_SIZE; - - // Store accumulated color - vec4 accum = vec4(0); - float accum_w = 0.0; - - // Amount of samples, don't forget to change the weights array in case you change this. - const int blur_size = 4; - - // Get the mid pixel normal and depth - vec3 pixel_nrm = get_gbuffer_normal(GBuffer, texcoord); - float pixel_depth = get_lin_z(texcoord); - - // Blur to the right - for (int i = -blur_size + 1; i < blur_size; ++i) { - vec2 offcoord = texcoord + pixel_size * i * blur_direction; - vec4 sampled = textureLod(SourceTex, offcoord, 0); - vec3 nrm = get_gbuffer_normal(GBuffer, offcoord); - float depth = get_lin_z(offcoord); - - float weight = gaussian_weights_4[abs(i)]; - weight *= 1.0 - saturate(GET_SETTING(ao, blur_normal_factor) * - distance(nrm, pixel_nrm) / 0.01); - weight *= 1.0 - saturate(GET_SETTING(ao, blur_depth_factor) * - abs(depth - pixel_depth) / 0.001); - - accum += sampled * weight; - accum_w += weight; - } - - accum /= max(0.04, accum_w); - result = accum; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// This shader provides a bilateral blur at half-resolution + +#pragma optionNV (unroll all) + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "includes/gaussian_weights.inc.glsl" + +uniform ivec2 blur_direction; +uniform sampler2D SourceTex; +uniform GBufferData GBuffer; + +out vec4 result; + +float get_lin_z(vec2 ccoord) { + return get_linear_z_from_z(get_gbuffer_depth(GBuffer, ccoord)); +} + +void main() { + vec2 texcoord = get_half_native_texcoord(); + vec2 pixel_size = 2.0 / SCREEN_SIZE; + + // Store accumulated color + vec4 accum = vec4(0); + float accum_w = 0.0; + + // Amount of samples, don't forget to change the weights array in case you change this. + const int blur_size = 4; + + // Get the mid pixel normal and depth + vec3 pixel_nrm = get_gbuffer_normal(GBuffer, texcoord); + float pixel_depth = get_lin_z(texcoord); + + // Blur to the right + for (int i = -blur_size + 1; i < blur_size; ++i) { + vec2 offcoord = texcoord + pixel_size * i * blur_direction; + vec4 sampled = textureLod(SourceTex, offcoord, 0); + vec3 nrm = get_gbuffer_normal(GBuffer, offcoord); + float depth = get_lin_z(offcoord); + + float weight = gaussian_weights_4[abs(i)]; + weight *= 1.0 - saturate(GET_SETTING(ao, blur_normal_factor) * + distance(nrm, pixel_nrm) / 0.01); + weight *= 1.0 - saturate(GET_SETTING(ao, blur_depth_factor) * + abs(depth - pixel_depth) / 0.001); + + accum += sampled * weight; + accum_w += weight; + } + + accum /= max(0.04, accum_w); + result = accum; +} diff --git a/RenderPipelineFile/rpcore/shader/bilateral_upscale.frag.glsl b/RenderPipelineFile/rpcore/shader/bilateral_upscale.frag.glsl index 21c65341..201d5750 100644 --- a/RenderPipelineFile/rpcore/shader/bilateral_upscale.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/bilateral_upscale.frag.glsl @@ -1,96 +1,96 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// Shader which upscales a half resolution image to full resolution, -// respecting the normals and depth - -#pragma optionNV (unroll all) - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" - -// x: Max depth difference, y: Max normal difference -uniform vec2 upscaleWeights; -uniform sampler2D SourceTex; -uniform GBufferData GBuffer; - -out vec4 result; - -void main() { - // Get sample coordinates - ivec2 coord = ivec2(gl_FragCoord.xy); - ivec2 bil_start_coord = get_bilateral_coord(coord); - - // Get current pixel data - float mid_depth = get_gbuffer_depth(GBuffer, coord); - vec3 mid_nrm = get_gbuffer_normal(GBuffer, coord); - - const float max_depth_diff = upscaleWeights.x; - const float max_nrm_diff = upscaleWeights.y; - - float weights = 0.0; - vec4 accum = vec4(0); - - // Controls how many other pixels should be taken into account, besides - // of the 4 direct neighbors. - const int search_radius = 0; - - // Accumulate all samples - for (int x = -search_radius; x < 2 + search_radius; ++x) { - for (int y = -search_radius; y < 2 + search_radius; ++y) { - - ivec2 source_coord = bil_start_coord + ivec2(x, y); - vec4 source_sample = texelFetch(SourceTex, source_coord, 0); - - // Check how much information those pixels share, and if it is - // enough, use that sample - float sample_depth = get_gbuffer_depth(GBuffer, source_coord * 2); - vec3 sample_nrm = get_gbuffer_normal(GBuffer, source_coord * 2); - float depth_diff = abs(sample_depth - mid_depth) / max_depth_diff; - float nrm_diff = max(0, dot(sample_nrm, mid_nrm)); - float weight = 1.0 - saturate(depth_diff); - weight *= pow(nrm_diff, 1.0 / max_nrm_diff); - - // Make sure we don't have a null-weight, but instead only a very - // small weight, so that in case no pixel matches, we still have - // data to work with. - // weight = max(1e-5, weight); - - accum += source_sample * weight; - weights += weight; - } - } - - if (weights < 1e-5) { - // When no sample was valid, take the center sample - this is still - // better than invalid pixels - result = texelFetch(SourceTex, coord / 2, 0); - } else { - result = accum / weights; - } -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// Shader which upscales a half resolution image to full resolution, +// respecting the normals and depth + +#pragma optionNV (unroll all) + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" + +// x: Max depth difference, y: Max normal difference +uniform vec2 upscaleWeights; +uniform sampler2D SourceTex; +uniform GBufferData GBuffer; + +out vec4 result; + +void main() { + // Get sample coordinates + ivec2 coord = ivec2(gl_FragCoord.xy); + ivec2 bil_start_coord = get_bilateral_coord(coord); + + // Get current pixel data + float mid_depth = get_gbuffer_depth(GBuffer, coord); + vec3 mid_nrm = get_gbuffer_normal(GBuffer, coord); + + const float max_depth_diff = upscaleWeights.x; + const float max_nrm_diff = upscaleWeights.y; + + float weights = 0.0; + vec4 accum = vec4(0); + + // Controls how many other pixels should be taken into account, besides + // of the 4 direct neighbors. + const int search_radius = 0; + + // Accumulate all samples + for (int x = -search_radius; x < 2 + search_radius; ++x) { + for (int y = -search_radius; y < 2 + search_radius; ++y) { + + ivec2 source_coord = bil_start_coord + ivec2(x, y); + vec4 source_sample = texelFetch(SourceTex, source_coord, 0); + + // Check how much information those pixels share, and if it is + // enough, use that sample + float sample_depth = get_gbuffer_depth(GBuffer, source_coord * 2); + vec3 sample_nrm = get_gbuffer_normal(GBuffer, source_coord * 2); + float depth_diff = abs(sample_depth - mid_depth) / max_depth_diff; + float nrm_diff = max(0, dot(sample_nrm, mid_nrm)); + float weight = 1.0 - saturate(depth_diff); + weight *= pow(nrm_diff, 1.0 / max_nrm_diff); + + // Make sure we don't have a null-weight, but instead only a very + // small weight, so that in case no pixel matches, we still have + // data to work with. + // weight = max(1e-5, weight); + + accum += source_sample * weight; + weights += weight; + } + } + + if (weights < 1e-5) { + // When no sample was valid, take the center sample - this is still + // better than invalid pixels + result = texelFetch(SourceTex, coord / 2, 0); + } else { + result = accum / weights; + } +} diff --git a/RenderPipelineFile/rpcore/shader/collect_used_cells.frag.glsl b/RenderPipelineFile/rpcore/shader/collect_used_cells.frag.glsl index 4e1995ef..ac23608c 100644 --- a/RenderPipelineFile/rpcore/shader/collect_used_cells.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/collect_used_cells.frag.glsl @@ -1,59 +1,59 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// Shader which iterates over the camera-space voxel grid and collects -// all cells which have been flagged into a big atomic driven list. - -#pragma optionNV (unroll all) - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/light_culling.inc.glsl" - -uniform sampler2DArray FlaggedCells; -layout(r32i) uniform iimageBuffer CellListBuffer; -uniform writeonly iimage2DArray RESTRICT CellListIndices; - -void main() { - ivec2 coord = ivec2(gl_FragCoord.xy); - - // Iterate over all slices - for (int i = 0; i < LC_TILE_SLICES; ++i) { - - // Check if the cell is flagged - bool visible = texelFetch(FlaggedCells, ivec3(coord, i), 0).x > 0.5; - if (visible) { - // Append the cell and mark it - // Notice: We add 1 since the first index stores the amount - // of collected cells. - int flag_index = imageAtomicAdd(CellListBuffer, 0, 1) + 1; - int cell_data = coord.x | coord.y << 10 | i << 20; - imageStore(CellListBuffer, flag_index, ivec4(cell_data)); - imageStore(CellListIndices, ivec3(coord, i), ivec4(flag_index)); - } - } -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// Shader which iterates over the camera-space voxel grid and collects +// all cells which have been flagged into a big atomic driven list. + +#pragma optionNV (unroll all) + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/light_culling.inc.glsl" + +uniform sampler2DArray FlaggedCells; +layout(r32i) uniform iimageBuffer CellListBuffer; +uniform writeonly iimage2DArray RESTRICT CellListIndices; + +void main() { + ivec2 coord = ivec2(gl_FragCoord.xy); + + // Iterate over all slices + for (int i = 0; i < LC_TILE_SLICES; ++i) { + + // Check if the cell is flagged + bool visible = texelFetch(FlaggedCells, ivec3(coord, i), 0).x > 0.5; + if (visible) { + // Append the cell and mark it + // Notice: We add 1 since the first index stores the amount + // of collected cells. + int flag_index = imageAtomicAdd(CellListBuffer, 0, 1) + 1; + int cell_data = coord.x | coord.y << 10 | i << 20; + imageStore(CellListBuffer, flag_index, ivec4(cell_data)); + imageStore(CellListIndices, ivec3(coord, i), ivec4(flag_index)); + } + } +} diff --git a/RenderPipelineFile/rpcore/shader/combine_velocity.frag.glsl b/RenderPipelineFile/rpcore/shader/combine_velocity.frag.glsl index 7d4d1e03..e166d555 100644 --- a/RenderPipelineFile/rpcore/shader/combine_velocity.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/combine_velocity.frag.glsl @@ -1,66 +1,66 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// Combines camera and per object velocity by using the velocity of the -// closest fragment - -#pragma optionNV (unroll all) - -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" - -out vec2 result; - -void main() { - vec2 texcoord = get_texcoord(); - ivec2 coord = ivec2(gl_FragCoord.xy); - - vec3 closest = vec3(0, 0, 1); - const int filter_size = 1; - - // Take velocity of closest fragment - for (int i = -filter_size; i <= filter_size; ++i) { - for (int j = -filter_size; j <= filter_size; ++j) { - if ((i == 0 && j == 0) || (abs(i) == 2 && abs(j) == 2)) { - vec2 offcoord = texcoord + vec2(i, j) / SCREEN_SIZE; - float depth = get_depth_at(offcoord); - if (depth < closest.z) { - closest = vec3(offcoord, depth); - } - } - } - } - - // Combine camera and per object velocity. - // XXX: Most likely this is wrong. But since per-object velocity currently - // is disabled, its not an issue. - vec2 camera_velocity = get_camera_velocity(closest.xy); - vec2 per_object_velocity = get_object_velocity_at(closest.xy); - result = camera_velocity + per_object_velocity; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// Combines camera and per object velocity by using the velocity of the +// closest fragment + +#pragma optionNV (unroll all) + +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" + +out vec2 result; + +void main() { + vec2 texcoord = get_texcoord(); + ivec2 coord = ivec2(gl_FragCoord.xy); + + vec3 closest = vec3(0, 0, 1); + const int filter_size = 1; + + // Take velocity of closest fragment + for (int i = -filter_size; i <= filter_size; ++i) { + for (int j = -filter_size; j <= filter_size; ++j) { + if ((i == 0 && j == 0) || (abs(i) == 2 && abs(j) == 2)) { + vec2 offcoord = texcoord + vec2(i, j) / SCREEN_SIZE; + float depth = get_depth_at(offcoord); + if (depth < closest.z) { + closest = vec3(offcoord, depth); + } + } + } + } + + // Combine camera and per object velocity. + // XXX: Most likely this is wrong. But since per-object velocity currently + // is disabled, its not an issue. + vec2 camera_velocity = get_camera_velocity(closest.xy); + vec2 per_object_velocity = get_object_velocity_at(closest.xy); + result = camera_velocity + per_object_velocity; +} diff --git a/RenderPipelineFile/rpcore/shader/cull_lights.frag.glsl b/RenderPipelineFile/rpcore/shader/cull_lights.frag.glsl index 17e8a94a..5861437e 100644 --- a/RenderPipelineFile/rpcore/shader/cull_lights.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/cull_lights.frag.glsl @@ -1,131 +1,131 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// This shader collects the list of all lights for a given cell, sorts them -// and then writes them out as a list. - -layout(early_fragment_tests) in; - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/transforms.inc.glsl" -#pragma include "includes/light_culling.inc.glsl" -#pragma include "includes/light_data.inc.glsl" -#pragma include "includes/light_classification.inc.glsl" - -uniform isamplerBuffer CellListBuffer; -uniform usamplerBuffer FrustumLights; -uniform isamplerBuffer FrustumLightsCount; - -uniform writeonly uimageBuffer PerCellLightsBuffer; -layout(r32i) uniform iimageBuffer PerCellLightCountsBuffer; - -uniform samplerBuffer AllLightsData; - -void main() { - - ivec2 coord = ivec2(gl_FragCoord.xy); - - int thread_id = (coord.x + coord.y * LC_CULLING_SLICE_WIDTH); - int local_offset = thread_id % LC_CULL_THREADS; - - // Find the index of the cell we are about to process - int idx = 1 + thread_id / LC_CULL_THREADS; - int num_total_cells = texelFetch(CellListBuffer, 0).x; - ivec2 precompute_size = MainSceneData.lc_tile_count; - - // If we found no remaining cell, we are done, so just return and hope for - // good coherency. - if (idx > num_total_cells) { - return; - } - - // Fetch the cell data, this contains the cells position - int packed_cell_data = texelFetch(CellListBuffer, idx).x; - int cell_x, cell_y, cell_slice; - unpack_cell_data(packed_cell_data, cell_x, cell_y, cell_slice); - - // Amount to increment the minimum and maximum distance of the slice. This - // avoids false negatives in culling. - const float distance_bias = 0.05; - - // Find the tiles minimum and maximum distance - float min_distance = get_distance_from_slice(cell_slice) - distance_bias; - float max_distance = get_distance_from_slice(cell_slice + 1) + distance_bias; - - // Get the offset in the per-cell light list - int storage_offs = LC_MAX_LIGHTS_PER_CELL * idx; - int num_rendered_lights = 0; - - // Compute sample directions - vec3 local_ray_dirs[num_raydirs] = get_raydirs(cell_x, cell_y, precompute_size); - - // Get amount of visible lights in frustum - int max_frustum_lights = texelFetch(FrustumLightsCount, 0).x; - - // Cull all lights - for (int i = local_offset; - i < max_frustum_lights && num_rendered_lights < LC_MAX_LIGHTS_PER_CELL; - i += LC_CULL_THREADS) { - - int light_index = int(texelFetch(FrustumLights, i).x); - - // Fetch data of current light - LightData light_data = read_light_data(AllLightsData, light_index); - - // Get Light position and project it to view space - vec3 light_pos = get_light_position(light_data); - vec4 light_pos_view = MainSceneData.view_mat_z_up * vec4(light_pos, 1); - - bool visible = false; - - // Get a sphere encapsulating the light and cull against that - Sphere sphere = get_representative_sphere(light_data); - - // XXX: This prevents invalid light culling for very distant small lights. - // Should find a better solution for this! - sphere.radius *= max(1.0, length(light_pos_view.xyz) / 200.0); - - for (int k = 0; k < num_raydirs; ++k) { - visible = visible || viewspace_ray_sphere_distance_intersection( - sphere, local_ray_dirs[k], min_distance, max_distance); - } - - // Uncomment this to see if the culling produces any issues - // visible = true; - - // Write the light to the light buffer - if (visible) { - - // Add one since the first element is the counter storing the total count - int num_lights = imageAtomicAdd(PerCellLightCountsBuffer, idx, 1).x; - imageStore(PerCellLightsBuffer, storage_offs + num_lights, uvec4(light_index)); - - num_rendered_lights = num_lights; - } - } -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// This shader collects the list of all lights for a given cell, sorts them +// and then writes them out as a list. + +layout(early_fragment_tests) in; + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/transforms.inc.glsl" +#pragma include "includes/light_culling.inc.glsl" +#pragma include "includes/light_data.inc.glsl" +#pragma include "includes/light_classification.inc.glsl" + +uniform isamplerBuffer CellListBuffer; +uniform usamplerBuffer FrustumLights; +uniform isamplerBuffer FrustumLightsCount; + +uniform writeonly uimageBuffer PerCellLightsBuffer; +layout(r32i) uniform iimageBuffer PerCellLightCountsBuffer; + +uniform samplerBuffer AllLightsData; + +void main() { + + ivec2 coord = ivec2(gl_FragCoord.xy); + + int thread_id = (coord.x + coord.y * LC_CULLING_SLICE_WIDTH); + int local_offset = thread_id % LC_CULL_THREADS; + + // Find the index of the cell we are about to process + int idx = 1 + thread_id / LC_CULL_THREADS; + int num_total_cells = texelFetch(CellListBuffer, 0).x; + ivec2 precompute_size = MainSceneData.lc_tile_count; + + // If we found no remaining cell, we are done, so just return and hope for + // good coherency. + if (idx > num_total_cells) { + return; + } + + // Fetch the cell data, this contains the cells position + int packed_cell_data = texelFetch(CellListBuffer, idx).x; + int cell_x, cell_y, cell_slice; + unpack_cell_data(packed_cell_data, cell_x, cell_y, cell_slice); + + // Amount to increment the minimum and maximum distance of the slice. This + // avoids false negatives in culling. + const float distance_bias = 0.05; + + // Find the tiles minimum and maximum distance + float min_distance = get_distance_from_slice(cell_slice) - distance_bias; + float max_distance = get_distance_from_slice(cell_slice + 1) + distance_bias; + + // Get the offset in the per-cell light list + int storage_offs = LC_MAX_LIGHTS_PER_CELL * idx; + int num_rendered_lights = 0; + + // Compute sample directions + vec3 local_ray_dirs[num_raydirs] = get_raydirs(cell_x, cell_y, precompute_size); + + // Get amount of visible lights in frustum + int max_frustum_lights = texelFetch(FrustumLightsCount, 0).x; + + // Cull all lights + for (int i = local_offset; + i < max_frustum_lights && num_rendered_lights < LC_MAX_LIGHTS_PER_CELL; + i += LC_CULL_THREADS) { + + int light_index = int(texelFetch(FrustumLights, i).x); + + // Fetch data of current light + LightData light_data = read_light_data(AllLightsData, light_index); + + // Get Light position and project it to view space + vec3 light_pos = get_light_position(light_data); + vec4 light_pos_view = MainSceneData.view_mat_z_up * vec4(light_pos, 1); + + bool visible = false; + + // Get a sphere encapsulating the light and cull against that + Sphere sphere = get_representative_sphere(light_data); + + // XXX: This prevents invalid light culling for very distant small lights. + // Should find a better solution for this! + sphere.radius *= max(1.0, length(light_pos_view.xyz) / 200.0); + + for (int k = 0; k < num_raydirs; ++k) { + visible = visible || viewspace_ray_sphere_distance_intersection( + sphere, local_ray_dirs[k], min_distance, max_distance); + } + + // Uncomment this to see if the culling produces any issues + // visible = true; + + // Write the light to the light buffer + if (visible) { + + // Add one since the first element is the counter storing the total count + int num_lights = imageAtomicAdd(PerCellLightCountsBuffer, idx, 1).x; + imageStore(PerCellLightsBuffer, storage_offs + num_lights, uvec4(light_index)); + + num_rendered_lights = num_lights; + } + } +} diff --git a/RenderPipelineFile/rpcore/shader/default_gui_shader.vert.glsl b/RenderPipelineFile/rpcore/shader/default_gui_shader.vert.glsl index 8d244391..8870fdfb 100644 --- a/RenderPipelineFile/rpcore/shader/default_gui_shader.vert.glsl +++ b/RenderPipelineFile/rpcore/shader/default_gui_shader.vert.glsl @@ -1,38 +1,38 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -in vec4 p3d_Vertex; -in vec2 p3d_MultiTexCoord0; -out vec2 texcoord; - -uniform mat4 p3d_ModelViewProjectionMatrix; - -void main() { - texcoord = p3d_MultiTexCoord0; - gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +in vec4 p3d_Vertex; +in vec2 p3d_MultiTexCoord0; +out vec2 texcoord; + +uniform mat4 p3d_ModelViewProjectionMatrix; + +void main() { + texcoord = p3d_MultiTexCoord0; + gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; +} diff --git a/RenderPipelineFile/rpcore/shader/default_post_process.vert.glsl b/RenderPipelineFile/rpcore/shader/default_post_process.vert.glsl index a820c936..1fc27d97 100644 --- a/RenderPipelineFile/rpcore/shader/default_post_process.vert.glsl +++ b/RenderPipelineFile/rpcore/shader/default_post_process.vert.glsl @@ -1,32 +1,32 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 -in vec4 p3d_Vertex; - -void main() { - gl_Position = vec4(p3d_Vertex.xz, 0, 1); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 +in vec4 p3d_Vertex; + +void main() { + gl_Position = vec4(p3d_Vertex.xz, 0, 1); +} diff --git a/RenderPipelineFile/rpcore/shader/default_post_process_instanced.vert.glsl b/RenderPipelineFile/rpcore/shader/default_post_process_instanced.vert.glsl index 6deecf0e..60dab42f 100644 --- a/RenderPipelineFile/rpcore/shader/default_post_process_instanced.vert.glsl +++ b/RenderPipelineFile/rpcore/shader/default_post_process_instanced.vert.glsl @@ -1,35 +1,35 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -in vec4 p3d_Vertex; -flat out int instance_id; - -void main() { - gl_Position = vec4(p3d_Vertex.xz, 0, 1); - instance_id = gl_InstanceID; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +in vec4 p3d_Vertex; +flat out int instance_id; + +void main() { + gl_Position = vec4(p3d_Vertex.xz, 0, 1); + instance_id = gl_InstanceID; +} diff --git a/RenderPipelineFile/rpcore/shader/downscale_depth.frag.glsl b/RenderPipelineFile/rpcore/shader/downscale_depth.frag.glsl index c1b9b956..15360681 100644 --- a/RenderPipelineFile/rpcore/shader/downscale_depth.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/downscale_depth.frag.glsl @@ -1,39 +1,39 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "includes/transforms.inc.glsl" - -out float result; - -void main() { - ivec2 coord = ivec2(gl_FragCoord.xy); - result = get_linear_z_from_z(get_depth_at(coord)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "includes/transforms.inc.glsl" + +out float result; + +void main() { + ivec2 coord = ivec2(gl_FragCoord.xy); + result = get_linear_z_from_z(get_depth_at(coord)); +} diff --git a/RenderPipelineFile/rpcore/shader/final_present_stage.frag.glsl b/RenderPipelineFile/rpcore/shader/final_present_stage.frag.glsl index 607b9699..ed29c52c 100644 --- a/RenderPipelineFile/rpcore/shader/final_present_stage.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/final_present_stage.frag.glsl @@ -1,61 +1,61 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/color_spaces.inc.glsl" - -uniform sampler2D SourceTex; -out vec4 result; - -// This shader just passes through the input texture - -void main() { - vec2 texcoord = (ivec2(gl_FragCoord.xy) + 0.5) / NATIVE_SCREEN_SIZE; - result = vec4(textureLod(SourceTex, texcoord, 0).xyz, 1); - - #if SPECIAL_MODE_ACTIVE(LUMINANCE) - - // Luminance debug mode, too bright pixels get red, too dark pixels get blue, - // rest stays green - - vec3 color = textureLod(SourceTex, texcoord, 0).xyz; - float luminance = get_luminance(color); - - vec3 color_ok = vec3(0, 1, 0); - vec3 color_too_bright = vec3(1, 0, 0); - vec3 color_too_dark = vec3(0, 0, 1); - - const float max_brightness = 0.7; - const float max_darkness = 0.3; - - color = mix(color_ok, color_too_bright, saturate(5.0 * (luminance - max_brightness))); - color = mix(color, color_too_dark, saturate(5.0 * (max_darkness - luminance))); - - result.xyz = color; - #endif -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/color_spaces.inc.glsl" + +uniform sampler2D SourceTex; +out vec4 result; + +// This shader just passes through the input texture + +void main() { + vec2 texcoord = (ivec2(gl_FragCoord.xy) + 0.5) / NATIVE_SCREEN_SIZE; + result = vec4(textureLod(SourceTex, texcoord, 0).xyz, 1); + + #if SPECIAL_MODE_ACTIVE(LUMINANCE) + + // Luminance debug mode, too bright pixels get red, too dark pixels get blue, + // rest stays green + + vec3 color = textureLod(SourceTex, texcoord, 0).xyz; + float luminance = get_luminance(color); + + vec3 color_ok = vec3(0, 1, 0); + vec3 color_too_bright = vec3(1, 0, 0); + vec3 color_too_dark = vec3(0, 0, 1); + + const float max_brightness = 0.7; + const float max_darkness = 0.3; + + color = mix(color_ok, color_too_bright, saturate(5.0 * (luminance - max_brightness))); + color = mix(color, color_too_dark, saturate(5.0 * (max_darkness - luminance))); + + result.xyz = color; + #endif +} diff --git a/RenderPipelineFile/rpcore/shader/final_stage.frag.glsl b/RenderPipelineFile/rpcore/shader/final_stage.frag.glsl index c916c8ed..0b33b4b8 100644 --- a/RenderPipelineFile/rpcore/shader/final_stage.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/final_stage.frag.glsl @@ -1,55 +1,55 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/noise.inc.glsl" -#pragma include "includes/color_spaces.inc.glsl" - -uniform sampler2D ShadedScene; -out vec4 result; - -void main() { - vec2 texcoord = get_texcoord(); - - // Fetch the current's scene color - vec3 scene_color = textureLod(ShadedScene, texcoord, 0).xyz; - - #if !DEBUG_MODE && !HAVE_PLUGIN(color_correction) - // Do a simple sRGB correction - scene_color = rgb_to_srgb(scene_color); - #endif - - // Apply dithering to prevent banding, since we are converting from 16 bit - // precision to 8 bit precision here - #if !REFERENCE_MODE - vec3 dither = (rand_rgb(texcoord) + rand_rgb(texcoord + 0.5787)) * 0.5 - 0.4; - scene_color += dither / 128.0; - #endif - - result = vec4(scene_color, 1); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/noise.inc.glsl" +#pragma include "includes/color_spaces.inc.glsl" + +uniform sampler2D ShadedScene; +out vec4 result; + +void main() { + vec2 texcoord = get_texcoord(); + + // Fetch the current's scene color + vec3 scene_color = textureLod(ShadedScene, texcoord, 0).xyz; + + #if !DEBUG_MODE && !HAVE_PLUGIN(color_correction) + // Do a simple sRGB correction + scene_color = rgb_to_srgb(scene_color); + #endif + + // Apply dithering to prevent banding, since we are converting from 16 bit + // precision to 8 bit precision here + #if !REFERENCE_MODE + vec3 dither = (rand_rgb(texcoord) + rand_rgb(texcoord + 0.5787)) * 0.5 - 0.4; + scene_color += dither / 128.0; + #endif + + result = vec4(scene_color, 1); +} diff --git a/RenderPipelineFile/rpcore/shader/flag_used_cells.frag.glsl b/RenderPipelineFile/rpcore/shader/flag_used_cells.frag.glsl index 2ebabe42..03b372b3 100644 --- a/RenderPipelineFile/rpcore/shader/flag_used_cells.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/flag_used_cells.frag.glsl @@ -1,50 +1,50 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/light_culling.inc.glsl" -#pragma include "includes/transforms.inc.glsl" - -#define USE_GBUFFER_EXTENSIONS 1 -#pragma include "includes/gbuffer.inc.glsl" - -uniform writeonly image2DArray RESTRICT cellGridFlags; - -void main() { - vec2 texcoord = get_texcoord(); - - // Get the distance to the camera - vec3 surf_pos = get_world_pos_at(texcoord); - float surf_dist = distance(MainSceneData.camera_pos, surf_pos); - - // Find the affected cell - ivec3 tile = get_lc_cell_index(ivec2(gl_FragCoord.xy), surf_dist); - - // Mark the cell as used - imageStore(cellGridFlags, tile, vec4(1)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/light_culling.inc.glsl" +#pragma include "includes/transforms.inc.glsl" + +#define USE_GBUFFER_EXTENSIONS 1 +#pragma include "includes/gbuffer.inc.glsl" + +uniform writeonly image2DArray RESTRICT cellGridFlags; + +void main() { + vec2 texcoord = get_texcoord(); + + // Get the distance to the camera + vec3 surf_pos = get_world_pos_at(texcoord); + float surf_dist = distance(MainSceneData.camera_pos, surf_pos); + + // Find the affected cell + ivec3 tile = get_lc_cell_index(ivec2(gl_FragCoord.xy), surf_dist); + + // Mark the cell as used + imageStore(cellGridFlags, tile, vec4(1)); +} diff --git a/RenderPipelineFile/rpcore/shader/fps_chart.compute.glsl b/RenderPipelineFile/rpcore/shader/fps_chart.compute.glsl index d3a968ad..99e28a30 100644 --- a/RenderPipelineFile/rpcore/shader/fps_chart.compute.glsl +++ b/RenderPipelineFile/rpcore/shader/fps_chart.compute.glsl @@ -1,73 +1,73 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" - -#pragma optionNV (unroll all) - -layout(local_size_x = 10, local_size_y = 4, local_size_z = 1) in; - -uniform writeonly image2D RESTRICT DestTex; -uniform samplerBuffer FPSValues; -uniform int index; -uniform float maxMs; - -void main() { - - // TODO: Might make this an input - const ivec2 widget_size = ivec2(250, 120); - ivec2 coord = ivec2(gl_GlobalInvocationID.xy); - - // Store the current pixels color - vec4 color = vec4(0); - - int buffer_offset = coord.x + index; - - // Make sure the value does not get negative - buffer_offset += 10 * widget_size.x; - buffer_offset = buffer_offset % widget_size.x; - - float ms_value = texelFetch(FPSValues, buffer_offset).x; - float prev_ms_value = texelFetch( - FPSValues, (buffer_offset - 1 + widget_size.x) % widget_size.x).x; - - int ms_pixel = int(ms_value / maxMs * widget_size.y); - int prev_ms_pixel = int(prev_ms_value / maxMs * widget_size.y); - - if (coord.y >= min(ms_pixel, prev_ms_pixel) && - coord.y <= max(ms_pixel, prev_ms_pixel)) { - color = vec4(0, 1, 0, 1); - } - - // Axis - if (coord.x == 0 || coord.y == 0) { - color = vec4(1); - } - - imageStore(DestTex, coord, color); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" + +#pragma optionNV (unroll all) + +layout(local_size_x = 10, local_size_y = 4, local_size_z = 1) in; + +uniform writeonly image2D RESTRICT DestTex; +uniform samplerBuffer FPSValues; +uniform int index; +uniform float maxMs; + +void main() { + + // TODO: Might make this an input + const ivec2 widget_size = ivec2(250, 120); + ivec2 coord = ivec2(gl_GlobalInvocationID.xy); + + // Store the current pixels color + vec4 color = vec4(0); + + int buffer_offset = coord.x + index; + + // Make sure the value does not get negative + buffer_offset += 10 * widget_size.x; + buffer_offset = buffer_offset % widget_size.x; + + float ms_value = texelFetch(FPSValues, buffer_offset).x; + float prev_ms_value = texelFetch( + FPSValues, (buffer_offset - 1 + widget_size.x) % widget_size.x).x; + + int ms_pixel = int(ms_value / maxMs * widget_size.y); + int prev_ms_pixel = int(prev_ms_value / maxMs * widget_size.y); + + if (coord.y >= min(ms_pixel, prev_ms_pixel) && + coord.y <= max(ms_pixel, prev_ms_pixel)) { + color = vec4(0, 1, 0, 1); + } + + // Axis + if (coord.x == 0 || coord.y == 0) { + color = vec4(1); + } + + imageStore(DestTex, coord, color); +} diff --git a/RenderPipelineFile/rpcore/shader/fps_chart_update.compute.glsl b/RenderPipelineFile/rpcore/shader/fps_chart_update.compute.glsl index cdcf86b9..cfa456d1 100644 --- a/RenderPipelineFile/rpcore/shader/fps_chart_update.compute.glsl +++ b/RenderPipelineFile/rpcore/shader/fps_chart_update.compute.glsl @@ -1,43 +1,43 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" - -#pragma optionNV (unroll all) - -layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; - -// Stores the current FPS value in the buffer - -uniform writeonly imageBuffer RESTRICT DestTex; -uniform int index; -uniform float currentData; - -void main() { - imageStore(DestTex, index, vec4(currentData)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" + +#pragma optionNV (unroll all) + +layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; + +// Stores the current FPS value in the buffer + +uniform writeonly imageBuffer RESTRICT DestTex; +uniform int index; +uniform float currentData; + +void main() { + imageStore(DestTex, index, vec4(currentData)); +} diff --git a/RenderPipelineFile/rpcore/shader/group_lights.frag.glsl b/RenderPipelineFile/rpcore/shader/group_lights.frag.glsl index 17669503..226bb958 100644 --- a/RenderPipelineFile/rpcore/shader/group_lights.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/group_lights.frag.glsl @@ -1,105 +1,105 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// This shader collects the list of all lights for a given cell, sorts them -// and then writes them out as a list. - -layout(early_fragment_tests) in; - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/transforms.inc.glsl" -#pragma include "includes/light_culling.inc.glsl" -#pragma include "includes/light_data.inc.glsl" -#pragma include "includes/light_classification.inc.glsl" - -uniform isamplerBuffer CellListBuffer; -uniform usamplerBuffer PerCellLightsBuffer; -layout(r32i) uniform iimageBuffer PerCellLightCountsBuffer; -uniform writeonly uimageBuffer RESTRICT GroupedCellLightsBuffer; -uniform writeonly uimageBuffer RESTRICT GroupedPerCellLightsCountBuffer; - -uniform samplerBuffer AllLightsData; -uniform int maxLightIndex; - -void main() { - ivec2 coord = ivec2(gl_FragCoord.xy); - - // Find the index of the cell we are about to process - int idx = 1 + coord.x + coord.y * LC_CULLING_SLICE_WIDTH; - int num_total_cells = texelFetch(CellListBuffer, 0).x; - - // If we found no remaining cell, we are done, so just return and hope for - // good coherency. - if (idx > num_total_cells) { - return; - } - - // Get the offset in the per-cell culled light list - int data_offset = LC_MAX_LIGHTS_PER_CELL * idx; // Culled lights offset - int dest_offset = LC_MAX_LIGHTS_PER_CELL * idx; // Destination offset - - // Offset into the per-cell culled light counts - int cell_lights_offset = idx * (1 + LIGHT_CLS_COUNT); - - // Get the amount of (unsorted) lights for this cell - int num_culled_lights = imageLoad(PerCellLightCountsBuffer, idx).x; - imageStore(PerCellLightCountsBuffer, idx, ivec4(0)); - - num_culled_lights = min(LC_MAX_LIGHTS_PER_CELL, num_culled_lights); - int num_processed_lights = 0; - - // Fetch the data of all light classes - for (int light_class = 0; light_class < LIGHT_CLS_COUNT; ++light_class) { - int light_count = 0; - - // Iterate over the list of culled lights, and store all lights which - // belong to this class - for (int i = 0; i < num_culled_lights; ++i) { - // XXX: Have to use int, because of stupid OpenGL specification, - // indexes into buffers are ints and not unsigned ints. - int light_index = int(texelFetch(PerCellLightsBuffer, data_offset + i).x); - int light_type = read_light_type(AllLightsData, light_index); - bool casts_shadows = read_casts_shadows(AllLightsData, light_index); - - // In case the culled light is of the same type as the type we are looking for - if (classify_light(light_type, casts_shadows) == light_class) { - imageStore( - GroupedCellLightsBuffer, - dest_offset + num_processed_lights, uvec4(light_index)); - ++light_count; - ++num_processed_lights; - } - } - - // Store the light class count - imageStore(GroupedPerCellLightsCountBuffer, cell_lights_offset + 1 + light_class, uvec4(light_count)); - } - - // Store the total light count to be able to early out - imageStore(GroupedPerCellLightsCountBuffer, cell_lights_offset, uvec4(num_processed_lights)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// This shader collects the list of all lights for a given cell, sorts them +// and then writes them out as a list. + +layout(early_fragment_tests) in; + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/transforms.inc.glsl" +#pragma include "includes/light_culling.inc.glsl" +#pragma include "includes/light_data.inc.glsl" +#pragma include "includes/light_classification.inc.glsl" + +uniform isamplerBuffer CellListBuffer; +uniform usamplerBuffer PerCellLightsBuffer; +layout(r32i) uniform iimageBuffer PerCellLightCountsBuffer; +uniform writeonly uimageBuffer RESTRICT GroupedCellLightsBuffer; +uniform writeonly uimageBuffer RESTRICT GroupedPerCellLightsCountBuffer; + +uniform samplerBuffer AllLightsData; +uniform int maxLightIndex; + +void main() { + ivec2 coord = ivec2(gl_FragCoord.xy); + + // Find the index of the cell we are about to process + int idx = 1 + coord.x + coord.y * LC_CULLING_SLICE_WIDTH; + int num_total_cells = texelFetch(CellListBuffer, 0).x; + + // If we found no remaining cell, we are done, so just return and hope for + // good coherency. + if (idx > num_total_cells) { + return; + } + + // Get the offset in the per-cell culled light list + int data_offset = LC_MAX_LIGHTS_PER_CELL * idx; // Culled lights offset + int dest_offset = LC_MAX_LIGHTS_PER_CELL * idx; // Destination offset + + // Offset into the per-cell culled light counts + int cell_lights_offset = idx * (1 + LIGHT_CLS_COUNT); + + // Get the amount of (unsorted) lights for this cell + int num_culled_lights = imageLoad(PerCellLightCountsBuffer, idx).x; + imageStore(PerCellLightCountsBuffer, idx, ivec4(0)); + + num_culled_lights = min(LC_MAX_LIGHTS_PER_CELL, num_culled_lights); + int num_processed_lights = 0; + + // Fetch the data of all light classes + for (int light_class = 0; light_class < LIGHT_CLS_COUNT; ++light_class) { + int light_count = 0; + + // Iterate over the list of culled lights, and store all lights which + // belong to this class + for (int i = 0; i < num_culled_lights; ++i) { + // XXX: Have to use int, because of stupid OpenGL specification, + // indexes into buffers are ints and not unsigned ints. + int light_index = int(texelFetch(PerCellLightsBuffer, data_offset + i).x); + int light_type = read_light_type(AllLightsData, light_index); + bool casts_shadows = read_casts_shadows(AllLightsData, light_index); + + // In case the culled light is of the same type as the type we are looking for + if (classify_light(light_type, casts_shadows) == light_class) { + imageStore( + GroupedCellLightsBuffer, + dest_offset + num_processed_lights, uvec4(light_index)); + ++light_count; + ++num_processed_lights; + } + } + + // Store the light class count + imageStore(GroupedPerCellLightsCountBuffer, cell_lights_offset + 1 + light_class, uvec4(light_count)); + } + + // Store the total light count to be able to early out + imageStore(GroupedPerCellLightsCountBuffer, cell_lights_offset, uvec4(num_processed_lights)); +} diff --git a/RenderPipelineFile/rpcore/shader/ibl/cubemap_diffuse.frag.glsl b/RenderPipelineFile/rpcore/shader/ibl/cubemap_diffuse.frag.glsl index 72e44623..e38b4052 100644 --- a/RenderPipelineFile/rpcore/shader/ibl/cubemap_diffuse.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/ibl/cubemap_diffuse.frag.glsl @@ -1,67 +1,67 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/importance_sampling.inc.glsl" -#pragma include "includes/poisson_disk.inc.glsl" -#pragma include "includes/noise.inc.glsl" - -#pragma optionNV (unroll all) - -uniform samplerCube SourceCubemap; -uniform writeonly imageCube RESTRICT DestCubemap; -uniform int cubeSize; - -void main() { - const int sample_count = 64; - - // Get cubemap coordinate - ivec2 coord = ivec2(gl_FragCoord.xy); - ivec2 clamped_coord; int face; - vec3 n = texcoord_to_cubemap(cubeSize, coord, clamped_coord, face); - - // Convert normal to spherical coordinates - float theta, phi; - vector_to_spherical(n, theta, phi); - - // Get tangent and binormal - vec3 tangent, binormal; - find_arbitrary_tangent(n, tangent, binormal); - - vec3 accum = vec3(0); - for (int i = 0; i < sample_count; ++i) - { - vec2 xi = hammersley(i, sample_count); - vec3 offset = importance_sample_lambert(xi); - offset = normalize(tangent * offset.x + binormal * offset.y + n * offset.z); - accum += textureLod(SourceCubemap, offset, 0).xyz; - } - - accum /= sample_count; - imageStore(DestCubemap, ivec3(clamped_coord, face), vec4(accum, 1)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/importance_sampling.inc.glsl" +#pragma include "includes/poisson_disk.inc.glsl" +#pragma include "includes/noise.inc.glsl" + +#pragma optionNV (unroll all) + +uniform samplerCube SourceCubemap; +uniform writeonly imageCube RESTRICT DestCubemap; +uniform int cubeSize; + +void main() { + const int sample_count = 64; + + // Get cubemap coordinate + ivec2 coord = ivec2(gl_FragCoord.xy); + ivec2 clamped_coord; int face; + vec3 n = texcoord_to_cubemap(cubeSize, coord, clamped_coord, face); + + // Convert normal to spherical coordinates + float theta, phi; + vector_to_spherical(n, theta, phi); + + // Get tangent and binormal + vec3 tangent, binormal; + find_arbitrary_tangent(n, tangent, binormal); + + vec3 accum = vec3(0); + for (int i = 0; i < sample_count; ++i) + { + vec2 xi = hammersley(i, sample_count); + vec3 offset = importance_sample_lambert(xi); + offset = normalize(tangent * offset.x + binormal * offset.y + n * offset.z); + accum += textureLod(SourceCubemap, offset, 0).xyz; + } + + accum /= sample_count; + imageStore(DestCubemap, ivec3(clamped_coord, face), vec4(accum, 1)); +} diff --git a/RenderPipelineFile/rpcore/shader/ibl/cubemap_diffuse_filter.frag.glsl b/RenderPipelineFile/rpcore/shader/ibl/cubemap_diffuse_filter.frag.glsl index d3c0029a..c6b68189 100644 --- a/RenderPipelineFile/rpcore/shader/ibl/cubemap_diffuse_filter.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/ibl/cubemap_diffuse_filter.frag.glsl @@ -1,64 +1,64 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// Filters the generated noisy diffuse cubemap to make it smooth - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/poisson_disk.inc.glsl" - -#pragma optionNV (unroll all) - -uniform samplerCube SourceCubemap; -uniform writeonly imageCube RESTRICT DestCubemap; -uniform int cubeSize; - -void main() { - const int num_samples = 64; - const float filter_radius = 0.5; - - // Get cubemap coordinate - ivec2 coord = ivec2(gl_FragCoord.xy); - ivec2 clamped_coord; int face; - vec3 n = texcoord_to_cubemap(cubeSize, coord, clamped_coord, face); - - // Get tangent and binormal - vec3 tangent, binormal; - find_arbitrary_tangent(n, tangent, binormal); - - vec3 accum = vec3(0.0); - for (int i = 0; i < num_samples; ++i) { - vec2 offset = poisson_2D_64[i]; - vec3 sample_vec = normalize(n + - filter_radius * offset.x * tangent + - filter_radius * offset.y * binormal); - accum += textureLod(SourceCubemap, sample_vec, 0).xyz; - } - accum /= num_samples; - - imageStore(DestCubemap, ivec3(clamped_coord, face), vec4(accum, 1.0)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// Filters the generated noisy diffuse cubemap to make it smooth + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/poisson_disk.inc.glsl" + +#pragma optionNV (unroll all) + +uniform samplerCube SourceCubemap; +uniform writeonly imageCube RESTRICT DestCubemap; +uniform int cubeSize; + +void main() { + const int num_samples = 64; + const float filter_radius = 0.5; + + // Get cubemap coordinate + ivec2 coord = ivec2(gl_FragCoord.xy); + ivec2 clamped_coord; int face; + vec3 n = texcoord_to_cubemap(cubeSize, coord, clamped_coord, face); + + // Get tangent and binormal + vec3 tangent, binormal; + find_arbitrary_tangent(n, tangent, binormal); + + vec3 accum = vec3(0.0); + for (int i = 0; i < num_samples; ++i) { + vec2 offset = poisson_2D_64[i]; + vec3 sample_vec = normalize(n + + filter_radius * offset.x * tangent + + filter_radius * offset.y * binormal); + accum += textureLod(SourceCubemap, sample_vec, 0).xyz; + } + accum /= num_samples; + + imageStore(DestCubemap, ivec3(clamped_coord, face), vec4(accum, 1.0)); +} diff --git a/RenderPipelineFile/rpcore/shader/ibl/cubemap_specular_filter.frag.glsl b/RenderPipelineFile/rpcore/shader/ibl/cubemap_specular_filter.frag.glsl index 95c6d78c..0e3d48f3 100644 --- a/RenderPipelineFile/rpcore/shader/ibl/cubemap_specular_filter.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/ibl/cubemap_specular_filter.frag.glsl @@ -1,65 +1,65 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/poisson_disk.inc.glsl" - -#pragma optionNV (unroll all) - -uniform samplerCube SourceTex; -uniform writeonly imageCube RESTRICT DestMipmap; -uniform int currentMip; - -void main() { - - const int num_samples = 12; - - // Get cubemap coordinate - int texsize = textureSize(SourceTex, currentMip).x; - ivec2 coord = ivec2(gl_FragCoord.xy); - ivec2 clamped_coord; int face; - vec3 n = texcoord_to_cubemap(texsize, coord, clamped_coord, face); - - // Get tangent and binormal - vec3 tangent, binormal; - find_arbitrary_tangent(n, tangent, binormal); - - const float filter_radius = 0.00 + currentMip * 0.06; - - vec3 accum = vec3(0.0); - for (int i = 0; i < num_samples; ++i) { - vec2 offset = poisson_2D_12[i]; - vec3 sample_vec = normalize(n + - filter_radius * offset.x * tangent + - filter_radius * offset.y * binormal); - accum += textureLod(SourceTex, sample_vec, currentMip).xyz; - } - - accum /= num_samples; - imageStore(DestMipmap, ivec3(clamped_coord, face), vec4(accum, 1.0)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/poisson_disk.inc.glsl" + +#pragma optionNV (unroll all) + +uniform samplerCube SourceTex; +uniform writeonly imageCube RESTRICT DestMipmap; +uniform int currentMip; + +void main() { + + const int num_samples = 12; + + // Get cubemap coordinate + int texsize = textureSize(SourceTex, currentMip).x; + ivec2 coord = ivec2(gl_FragCoord.xy); + ivec2 clamped_coord; int face; + vec3 n = texcoord_to_cubemap(texsize, coord, clamped_coord, face); + + // Get tangent and binormal + vec3 tangent, binormal; + find_arbitrary_tangent(n, tangent, binormal); + + const float filter_radius = 0.00 + currentMip * 0.06; + + vec3 accum = vec3(0.0); + for (int i = 0; i < num_samples; ++i) { + vec2 offset = poisson_2D_12[i]; + vec3 sample_vec = normalize(n + + filter_radius * offset.x * tangent + + filter_radius * offset.y * binormal); + accum += textureLod(SourceTex, sample_vec, currentMip).xyz; + } + + accum /= num_samples; + imageStore(DestMipmap, ivec3(clamped_coord, face), vec4(accum, 1.0)); +} diff --git a/RenderPipelineFile/rpcore/shader/ibl/cubemap_specular_filter_first.frag.glsl b/RenderPipelineFile/rpcore/shader/ibl/cubemap_specular_filter_first.frag.glsl index b6e7400c..b378d2aa 100644 --- a/RenderPipelineFile/rpcore/shader/ibl/cubemap_specular_filter_first.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/ibl/cubemap_specular_filter_first.frag.glsl @@ -1,66 +1,66 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/poisson_disk.inc.glsl" -#pragma include "includes/noise.inc.glsl" - -#pragma optionNV (unroll all) - -uniform samplerCube SourceTex; -uniform writeonly imageCube RESTRICT DestMipmap; -uniform int currentMip; - -void main() { - - const int num_samples = 12; - - // Get cubemap coordinate - int texsize = textureSize(SourceTex, currentMip).x; - ivec2 coord = ivec2(gl_FragCoord.xy); - ivec2 clamped_coord; int face; - vec3 n = texcoord_to_cubemap(texsize, coord, clamped_coord, face); - - // Get tangent and binormal - vec3 tangent, binormal; - find_arbitrary_tangent(n, tangent, binormal); - - const float filter_radius = 0.05; - vec3 accum = vec3(0.0); - for (int i = 0; i < num_samples; ++i) { - vec2 offset = poisson_2D_12[i]; - vec3 sample_vec = normalize(n + - filter_radius * offset.x * tangent + - filter_radius * offset.y * binormal + - (rand(vec2(coord + 23 * i))-0.5) * 0.01); - accum += textureLod(SourceTex, sample_vec, currentMip).xyz; - } - - accum /= num_samples; - imageStore(DestMipmap, ivec3(clamped_coord, face), vec4(accum, 1.0)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/poisson_disk.inc.glsl" +#pragma include "includes/noise.inc.glsl" + +#pragma optionNV (unroll all) + +uniform samplerCube SourceTex; +uniform writeonly imageCube RESTRICT DestMipmap; +uniform int currentMip; + +void main() { + + const int num_samples = 12; + + // Get cubemap coordinate + int texsize = textureSize(SourceTex, currentMip).x; + ivec2 coord = ivec2(gl_FragCoord.xy); + ivec2 clamped_coord; int face; + vec3 n = texcoord_to_cubemap(texsize, coord, clamped_coord, face); + + // Get tangent and binormal + vec3 tangent, binormal; + find_arbitrary_tangent(n, tangent, binormal); + + const float filter_radius = 0.05; + vec3 accum = vec3(0.0); + for (int i = 0; i < num_samples; ++i) { + vec2 offset = poisson_2D_12[i]; + vec3 sample_vec = normalize(n + + filter_radius * offset.x * tangent + + filter_radius * offset.y * binormal + + (rand(vec2(coord + 23 * i))-0.5) * 0.01); + accum += textureLod(SourceTex, sample_vec, currentMip).xyz; + } + + accum /= num_samples; + imageStore(DestMipmap, ivec3(clamped_coord, face), vec4(accum, 1.0)); +} diff --git a/RenderPipelineFile/rpcore/shader/ibl/cubemap_specular_prefilter.frag.glsl b/RenderPipelineFile/rpcore/shader/ibl/cubemap_specular_prefilter.frag.glsl index c5803244..e292d637 100644 --- a/RenderPipelineFile/rpcore/shader/ibl/cubemap_specular_prefilter.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/ibl/cubemap_specular_prefilter.frag.glsl @@ -1,70 +1,70 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/importance_sampling.inc.glsl" -#pragma include "includes/poisson_disk.inc.glsl" -#pragma include "includes/brdf.inc.glsl" -#pragma include "includes/noise.inc.glsl" - -#pragma optionNV (unroll all) - -uniform int currentMip; -uniform samplerCube SourceTex; -layout(r11f_g11f_b10f) uniform imageCube RESTRICT DestMipmap; - -void main() { - const int num_samples = 64; - - // Get cubemap coordinate - int texsize = imageSize(DestMipmap).x; - ivec2 coord = ivec2(gl_FragCoord.xy); - ivec2 clamped_coord; int face; - vec3 n = texcoord_to_cubemap(texsize, coord, clamped_coord, face); - - // Determine target roughenss - float sample_roughness = 1e-10 + (currentMip + 1) / 7.0 - 0.04; - - vec3 tangent, binormal; - find_arbitrary_tangent(n, tangent, binormal); - - vec3 accum = vec3(0); - float accum_weights = 0.0; - - // Importance sampling - for (int i = 0; i < num_samples; ++i) { - vec2 Xi = hammersley(i, num_samples); - vec3 h = importance_sample_ggx(Xi, sample_roughness); - h = normalize(h.x * tangent + h.y * binormal + h.z * n); - accum += textureLod(SourceTex, h, 1).xyz; - } - - // Energy conservation - accum /= num_samples; - imageStore(DestMipmap, ivec3(clamped_coord, face), vec4(accum, 1.0)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/importance_sampling.inc.glsl" +#pragma include "includes/poisson_disk.inc.glsl" +#pragma include "includes/brdf.inc.glsl" +#pragma include "includes/noise.inc.glsl" + +#pragma optionNV (unroll all) + +uniform int currentMip; +uniform samplerCube SourceTex; +layout(r11f_g11f_b10f) uniform imageCube RESTRICT DestMipmap; + +void main() { + const int num_samples = 64; + + // Get cubemap coordinate + int texsize = imageSize(DestMipmap).x; + ivec2 coord = ivec2(gl_FragCoord.xy); + ivec2 clamped_coord; int face; + vec3 n = texcoord_to_cubemap(texsize, coord, clamped_coord, face); + + // Determine target roughenss + float sample_roughness = 1e-10 + (currentMip + 1) / 7.0 - 0.04; + + vec3 tangent, binormal; + find_arbitrary_tangent(n, tangent, binormal); + + vec3 accum = vec3(0); + float accum_weights = 0.0; + + // Importance sampling + for (int i = 0; i < num_samples; ++i) { + vec2 Xi = hammersley(i, num_samples); + vec3 h = importance_sample_ggx(Xi, sample_roughness); + h = normalize(h.x * tangent + h.y * binormal + h.z * n); + accum += textureLod(SourceTex, h, 1).xyz; + } + + // Energy conservation + accum /= num_samples; + imageStore(DestMipmap, ivec3(clamped_coord, face), vec4(accum, 1.0)); +} diff --git a/RenderPipelineFile/rpcore/shader/ibl/cubemap_specular_prefilter_first.frag.glsl b/RenderPipelineFile/rpcore/shader/ibl/cubemap_specular_prefilter_first.frag.glsl index e22ffcaa..bd9692f7 100644 --- a/RenderPipelineFile/rpcore/shader/ibl/cubemap_specular_prefilter_first.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/ibl/cubemap_specular_prefilter_first.frag.glsl @@ -1,50 +1,50 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/importance_sampling.inc.glsl" -#pragma include "includes/poisson_disk.inc.glsl" -#pragma include "includes/brdf.inc.glsl" - -#pragma optionNV (unroll all) - -uniform int currentMip; -uniform samplerCube SourceTex; -layout(r11f_g11f_b10f) uniform imageCube RESTRICT DestMipmap; - -void main() { - const int num_samples = 0; - - // Get cubemap coordinate - int texsize = imageSize(DestMipmap).x; - ivec2 coord = ivec2(gl_FragCoord.xy); - ivec2 clamped_coord; int face; - vec3 n = texcoord_to_cubemap(texsize, coord, clamped_coord, face); - vec3 accum = textureLod(SourceTex, n, 0).xyz; - imageStore(DestMipmap, ivec3(clamped_coord, face), vec4(accum, 1.0)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/importance_sampling.inc.glsl" +#pragma include "includes/poisson_disk.inc.glsl" +#pragma include "includes/brdf.inc.glsl" + +#pragma optionNV (unroll all) + +uniform int currentMip; +uniform samplerCube SourceTex; +layout(r11f_g11f_b10f) uniform imageCube RESTRICT DestMipmap; + +void main() { + const int num_samples = 0; + + // Get cubemap coordinate + int texsize = imageSize(DestMipmap).x; + ivec2 coord = ivec2(gl_FragCoord.xy); + ivec2 clamped_coord; int face; + vec3 n = texcoord_to_cubemap(texsize, coord, clamped_coord, face); + vec3 accum = textureLod(SourceTex, n, 0).xyz; + imageStore(DestMipmap, ivec3(clamped_coord, face), vec4(accum, 1.0)); +} diff --git a/RenderPipelineFile/rpcore/shader/includes/brdf.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/brdf.inc.glsl index ffb469ae..4c721339 100644 --- a/RenderPipelineFile/rpcore/shader/includes/brdf.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/brdf.inc.glsl @@ -1,354 +1,354 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/material.inc.glsl" -#pragma include "includes/color_spaces.inc.glsl" - - -/* - - BRDFs from: - http://www.frostbite.com/wp-content/uploads/2014/11/course_notes_moving_frostbite_to_pbr.pdf - - Also from: - http://www.trentreed.net/blog/physically-based-shading-and-image-based-lighting/ - - Also inspired by the implementation in Disneys BRDF Explorer: - http://www.disneyanimation.com/technology/brdf.html - -*/ - - -// Lambert BRDF -float brdf_lambert() { - return ONE_BY_PI; -} - -// Schlicks approximation to fresnel -vec3 brdf_schlick_fresnel(vec3 specular, float f90, float VxH) -{ - // Fast pow, proposed in - // http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf - return mix(specular, vec3(f90), pow(2, (-5.55473 * VxH - 6.98316) * VxH)); - - // Regular pow - // return mix(specular, vec3(f90), pow( 1.0 - VxH, 5.0)); -} - -vec3 brdf_schlick_fresnel(vec3 specular, float VxH) -{ - return brdf_schlick_fresnel(specular, 1.0, VxH); -} - -// BRDF Proposed by Burley -float brdf_disney_diffuse(float NxV, float NxL, float LxH, float roughness) { - - // In case of squared roughness: - float lin_roughness = roughness; - float energy_bias = 0.5 * lin_roughness; - float energy_factor = mix(1.0, 1.0 / 1.51, lin_roughness); - float fd90 = energy_bias + 2.0 * LxH * LxH * lin_roughness; - vec3 f0 = vec3(1); - float light_scatter = brdf_schlick_fresnel(f0, fd90, NxL).x; - float view_scatter = brdf_schlick_fresnel(f0, fd90, NxV).x; - return light_scatter * view_scatter * energy_factor / M_PI; -} - -/* Distribution functions */ -float brdf_distribution_blinn_phong(float NxH, float roughness) { - float r_sq = roughness * roughness; - float inv_r = 1.0 / r_sq; - return inv_r * ONE_BY_PI * pow(NxH, fma(inv_r, 2.0, -2.0)); -} - -float brdf_distribution_beckmann(float NxH, float roughness) { - float r_sq = roughness * roughness; - float nxh_sq = NxH * NxH; - return exp((nxh_sq - 1.0) / (r_sq * nxh_sq)) / (M_PI * r_sq * nxh_sq * nxh_sq); -} - -float brdf_distribution_ggx(float NxH, float alpha) { - float r_square = alpha * alpha; - float f = (NxH * r_square - NxH) * NxH + 1.0; - return r_square / (f * f); -} - -float brdf_distribution_exponential(float NxH, float roughness) { - float r_sq = roughness * roughness; - float d = exp(-acos(NxH) / roughness); // TODO: optimize - return d * (1.0 + 4.0 * r_sq) / (2.0 * r_sq * - (1.0 + exp(-(M_PI / (2.0 * roughness)))) * M_PI); -} - -float brdf_distribution_gaussian(float NxH, float roughness) { - float theta = acos(NxH); // TODO: optimize - return exp(-theta * theta / (roughness * roughness)); -} - -float brdf_distribution_trowbridge_reitz(float NxH, float roughness) { - float r_sq = roughness * roughness; - float f = r_sq / (NxH * NxH * (r_sq - 1.0) + 1.0); - return f * f * ONE_BY_PI / r_sq; -} - - -/* Visibility functions */ -float brdf_visibility_implicit(float NxL, float NxV) { - return NxL * NxV; -} - -float brdf_visibility_neumann(float NxV, float NxL) { - return NxL * NxV / max(1e-7, max(NxL, NxV)); -} - -float brdf_visibility_cook_torrance(float NxL, float NxV, float NxH, float VxH) { - float nh_by_vh = 2.0 * NxH / VxH; - float eq_nv = NxV * nh_by_vh; - float eq_nl = NxL * nh_by_vh; - return min(1.0, min(eq_nv, eq_nl)); -} - -float brdf_visibility_smith(float NxL, float NxV, float roughness) { - float r_sq = roughness * roughness; - float vis_v = NxL * (NxV * (1 - r_sq) + r_sq); - float vis_l = NxV * (NxL * (1 - r_sq) + r_sq); - return 0.5 / (vis_v + vis_l); -} - -// Helper function for the schlick visibility -float _schlick_g(float NxL, float roughness_sq) { - float k = sqrt(2.0 * roughness_sq * ONE_BY_PI); - return NxL > 0.0 ? NxL / (NxL - k * NxL + k) : 0.0; -} - -float brdf_visibility_schlick(float NxV, float NxL, float roughness) { - float r_sq = roughness * roughness; - return _schlick_g(NxL, r_sq) * _schlick_g(NxV, r_sq); -} - - -float brdf_visibility_smith_ggx(float NxL, float NxV, float roughness) { - float alpha = roughness * roughness; - float lambda_GGXV = NxL * sqrt((-NxV * alpha + NxV) * NxV + alpha); - float lambda_GGXL = NxV * sqrt((-NxL * alpha + NxL) * NxL + alpha); - return 0.5 / (lambda_GGXV + lambda_GGXL) * NxL; -} - - -/* Fresnel functions */ - -float ior_to_specular(float ior) { - float f0 = (ior - AIR_IOR) / (ior + AIR_IOR); - // Clamp between ior of 1 and 2.5 - return clamp(f0 * f0, 0.0, 0.18); -} - -float brdf_fresnel_cook_torrance(float LxH, float roughness, float ior) { - float g = sqrt(max(0, ior * ior + LxH * LxH - 1.0)); - float gpc = g + LxH; - float gmc = g - LxH; - return 0.5 * pow(gmc, 2) / pow(gpc, 2) * (1 + pow(LxH * gpc - 1, 2) / - pow(LxH * gmc + 1, 2)); -} - -float brdf_fresnel_schlick_f0(float LxH, float roughness, float f0) { - return f0 + (1 - f0) * pow(2, (-5.55473 * LxH - 6.98316) * LxH); -} - -float brdf_fresnel_schlick(float LxH, float roughness, float ior) { - float f0 = ior_to_specular(ior); - return brdf_fresnel_schlick_f0(LxH, roughness, f0); -} - -// Exact fresnel, can be slow. -float brdf_fresnel_exact(float cos_theta) { - const float eta = 1.51; - - float scale = 1 / eta; - float cos_theta_t_sqr = 1 - (1 - cos_theta * cos_theta) * (scale * scale); - - if (cos_theta_t_sqr <= 0.0) - return 1.0; - - float cos_theta_i = abs(cos_theta); - float cos_theta_t = sqrt(cos_theta_t_sqr); - - float Rs = (cos_theta_i - eta * cos_theta_t) / (cos_theta_i + eta * cos_theta_t); - float Rp = (eta * cos_theta_i - cos_theta_t) / (eta * cos_theta_i + cos_theta_t); - - return 0.5 * (Rs * Rs + Rp * Rp); -} - -// Exact conductor fresnel, can be slow, also only single channel -float brdf_fresnel_conductor_exact(float cos_theta_i, float eta, float k) { - /* Modified from "Optics" by K.D. Moeller, University Science Books, 1988 */ - float cosThetaI2 = cos_theta_i * cos_theta_i; - float sinThetaI2 = 1 - cosThetaI2; - float sinThetaI4 = sinThetaI2 * sinThetaI2; - - float temp1 = eta * eta - k * k - sinThetaI2; - float a2pb2 = sqrt(temp1 * temp1 + 4 * k * k * eta * eta); - float a = sqrt(0.5f * (a2pb2 + temp1)); - - float term1 = a2pb2 + cosThetaI2; - float term2 = 2 * a * cos_theta_i; - - float Rs2 = (term1 - term2) / (term1 + term2); - - float term3 = a2pb2 * cosThetaI2 + sinThetaI4; - float term4 = term2 * sinThetaI2; - - float Rp2 = Rs2 * (term3 - term4) / (term3 + term4); - return 0.5f * (Rp2 + Rs2); -} - - -// Approximation proposed in -// http://sirkan.iit.bme.hu/~szirmay/fresnel.pdf -vec3 brdf_fresnel_conductor_approx(float cos_theta, vec3 n, vec3 k) { - vec3 k_sq = k * k; - vec3 term0 = square(n - 1.0) + 4 * n * pow(1 - cos_theta, 5.0) + k_sq; - vec3 term1 = square(n + 1.0) + k_sq; - return term0 / term1; -} - -// Diffuse BRDF -float brdf_diffuse(float NxV, float NxL, float LxH, float VxH, float roughness) { - - // Choose one: - return brdf_lambert(); - // return brdf_disney_diffuse(NxV, NxL, LxH, roughness); -} - - -// Distribution -float brdf_distribution(float NxH, float roughness) -{ - NxH = max(1e-5, NxH); - roughness = max(0.0019, roughness); - - // Choose one: - // return brdf_distribution_blinn_phong(NxH, roughness); - // return brdf_distribution_beckmann(NxH, roughness); - // return brdf_distribution_exponential(NxH, roughness); - // return brdf_distribution_gaussian(NxH, roughness); - return brdf_distribution_trowbridge_reitz(NxH, roughness); - // return brdf_distribution_ggx(NxH, roughness); -} - -// Geometric Visibility -float brdf_visibility(float NxL, float NxV, float NxH, float VxH, float roughness) { - - // Choose one: - // float vis = brdf_visibility_neumann(NxV, NxL); - float vis = brdf_visibility_implicit(NxV, NxL); - // float vis = brdf_visibility_smith_ggx(NxV, NxL, roughness); - // float vis = brdf_visibility_schlick(NxV, NxL, roughness); - // float vis = brdf_visibility_cook_torrance(NxL, NxV, NxH, VxH); - // float vis = brdf_visibility_smith(NxL, NxV, roughness); - - - return vis; -} - -// Fresnel -float brdf_fresnel(float LxH, float roughness) { - // Default index of refraction - // TODO: Maybe make this configurable? - const float ior = 1.2; - - return brdf_fresnel_schlick(LxH, roughness, ior); - // return brdf_fresnel_cook_torrance(LxH, roughness, ior); -} - - -/* Material Functions */ -vec3 get_material_f0(Material m) { - // Material specular is already in the 0 .. 0.08 range - return mix(vec3(m.specular), m.basecolor, m.metallic); -} - - -// Returns a reflection vector, bent into the normal direction -vec3 get_reflection_vector(Material m, vec3 view_vector) { - float roughness = m.shading_model == SHADING_MODEL_CLEARCOAT ? - CLEARCOAT_ROUGHNESS : m.roughness; - vec3 reflected_dir = reflect(view_vector, m.normal); - // XXX: Evaluate whats more physically correct - // return normalize(mix(m.normal, reflected_dir, (1 - roughness) * (roughness + sqrt(1 - roughness)))); - return reflected_dir; -} - - -// Returns an approximated mipmap level based on the materials roughness -// level to approximate importance sampled references -float get_specular_mipmap(float roughness) { - - // Approximation to match importance sampled reference, tuned for a - // resolution of 128. - return (roughness * 12.0 - pow(roughness, 6.0) * 1.5); -} - -float get_specular_mipmap(Material m) { - return get_specular_mipmap(m.roughness); -} - -vec3 get_brdf_from_lut(sampler3D lut_texture, float NxV, float roughness, float ior) { - float lookup_slice = (ior - 1.01) / 1.5 + 0.5 / 15.0; - vec3 data = textureLod(lut_texture, vec3(NxV, roughness, lookup_slice), 0).xyz; - - // Unpack packed data - data *= data; - - return data; -} - -vec3 get_brdf_from_lut(sampler2D lut_texture, float NxV, float roughness) { - vec3 data = textureLod(lut_texture, vec2(NxV, roughness), 0).xyz; - // Unpack packed data - data *= data; - return data; -} - -float get_effective_roughness(Material m) { - return m.shading_model == SHADING_MODEL_CLEARCOAT ? CLEARCOAT_ROUGHNESS : m.roughness; -} - -float get_mipmap_for_roughness(samplerCube map, float roughness, float NxV) { - return sqrt(roughness) * 7.0; -} - - -vec3 get_metallic_fresnel_approx(Material m, float NxV) { - vec3 metallic_energy_f0 = vec3(1.0 - 0.7 * m.roughness) * m.basecolor; - vec3 metallic_energy_f90 = mix(vec3(1), 0.5 * m.basecolor, m.linear_roughness); - vec3 metallic_fresnel = mix(metallic_energy_f0, metallic_energy_f90, - pow(1 - NxV, 3.6 - 2.6 * m.linear_roughness)); - return metallic_fresnel; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/material.inc.glsl" +#pragma include "includes/color_spaces.inc.glsl" + + +/* + + BRDFs from: + http://www.frostbite.com/wp-content/uploads/2014/11/course_notes_moving_frostbite_to_pbr.pdf + + Also from: + http://www.trentreed.net/blog/physically-based-shading-and-image-based-lighting/ + + Also inspired by the implementation in Disneys BRDF Explorer: + http://www.disneyanimation.com/technology/brdf.html + +*/ + + +// Lambert BRDF +float brdf_lambert() { + return ONE_BY_PI; +} + +// Schlicks approximation to fresnel +vec3 brdf_schlick_fresnel(vec3 specular, float f90, float VxH) +{ + // Fast pow, proposed in + // http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf + return mix(specular, vec3(f90), pow(2, (-5.55473 * VxH - 6.98316) * VxH)); + + // Regular pow + // return mix(specular, vec3(f90), pow( 1.0 - VxH, 5.0)); +} + +vec3 brdf_schlick_fresnel(vec3 specular, float VxH) +{ + return brdf_schlick_fresnel(specular, 1.0, VxH); +} + +// BRDF Proposed by Burley +float brdf_disney_diffuse(float NxV, float NxL, float LxH, float roughness) { + + // In case of squared roughness: + float lin_roughness = roughness; + float energy_bias = 0.5 * lin_roughness; + float energy_factor = mix(1.0, 1.0 / 1.51, lin_roughness); + float fd90 = energy_bias + 2.0 * LxH * LxH * lin_roughness; + vec3 f0 = vec3(1); + float light_scatter = brdf_schlick_fresnel(f0, fd90, NxL).x; + float view_scatter = brdf_schlick_fresnel(f0, fd90, NxV).x; + return light_scatter * view_scatter * energy_factor / M_PI; +} + +/* Distribution functions */ +float brdf_distribution_blinn_phong(float NxH, float roughness) { + float r_sq = roughness * roughness; + float inv_r = 1.0 / r_sq; + return inv_r * ONE_BY_PI * pow(NxH, fma(inv_r, 2.0, -2.0)); +} + +float brdf_distribution_beckmann(float NxH, float roughness) { + float r_sq = roughness * roughness; + float nxh_sq = NxH * NxH; + return exp((nxh_sq - 1.0) / (r_sq * nxh_sq)) / (M_PI * r_sq * nxh_sq * nxh_sq); +} + +float brdf_distribution_ggx(float NxH, float alpha) { + float r_square = alpha * alpha; + float f = (NxH * r_square - NxH) * NxH + 1.0; + return r_square / (f * f); +} + +float brdf_distribution_exponential(float NxH, float roughness) { + float r_sq = roughness * roughness; + float d = exp(-acos(NxH) / roughness); // TODO: optimize + return d * (1.0 + 4.0 * r_sq) / (2.0 * r_sq * + (1.0 + exp(-(M_PI / (2.0 * roughness)))) * M_PI); +} + +float brdf_distribution_gaussian(float NxH, float roughness) { + float theta = acos(NxH); // TODO: optimize + return exp(-theta * theta / (roughness * roughness)); +} + +float brdf_distribution_trowbridge_reitz(float NxH, float roughness) { + float r_sq = roughness * roughness; + float f = r_sq / (NxH * NxH * (r_sq - 1.0) + 1.0); + return f * f * ONE_BY_PI / r_sq; +} + + +/* Visibility functions */ +float brdf_visibility_implicit(float NxL, float NxV) { + return NxL * NxV; +} + +float brdf_visibility_neumann(float NxV, float NxL) { + return NxL * NxV / max(1e-7, max(NxL, NxV)); +} + +float brdf_visibility_cook_torrance(float NxL, float NxV, float NxH, float VxH) { + float nh_by_vh = 2.0 * NxH / VxH; + float eq_nv = NxV * nh_by_vh; + float eq_nl = NxL * nh_by_vh; + return min(1.0, min(eq_nv, eq_nl)); +} + +float brdf_visibility_smith(float NxL, float NxV, float roughness) { + float r_sq = roughness * roughness; + float vis_v = NxL * (NxV * (1 - r_sq) + r_sq); + float vis_l = NxV * (NxL * (1 - r_sq) + r_sq); + return 0.5 / (vis_v + vis_l); +} + +// Helper function for the schlick visibility +float _schlick_g(float NxL, float roughness_sq) { + float k = sqrt(2.0 * roughness_sq * ONE_BY_PI); + return NxL > 0.0 ? NxL / (NxL - k * NxL + k) : 0.0; +} + +float brdf_visibility_schlick(float NxV, float NxL, float roughness) { + float r_sq = roughness * roughness; + return _schlick_g(NxL, r_sq) * _schlick_g(NxV, r_sq); +} + + +float brdf_visibility_smith_ggx(float NxL, float NxV, float roughness) { + float alpha = roughness * roughness; + float lambda_GGXV = NxL * sqrt((-NxV * alpha + NxV) * NxV + alpha); + float lambda_GGXL = NxV * sqrt((-NxL * alpha + NxL) * NxL + alpha); + return 0.5 / (lambda_GGXV + lambda_GGXL) * NxL; +} + + +/* Fresnel functions */ + +float ior_to_specular(float ior) { + float f0 = (ior - AIR_IOR) / (ior + AIR_IOR); + // Clamp between ior of 1 and 2.5 + return clamp(f0 * f0, 0.0, 0.18); +} + +float brdf_fresnel_cook_torrance(float LxH, float roughness, float ior) { + float g = sqrt(max(0, ior * ior + LxH * LxH - 1.0)); + float gpc = g + LxH; + float gmc = g - LxH; + return 0.5 * pow(gmc, 2) / pow(gpc, 2) * (1 + pow(LxH * gpc - 1, 2) / + pow(LxH * gmc + 1, 2)); +} + +float brdf_fresnel_schlick_f0(float LxH, float roughness, float f0) { + return f0 + (1 - f0) * pow(2, (-5.55473 * LxH - 6.98316) * LxH); +} + +float brdf_fresnel_schlick(float LxH, float roughness, float ior) { + float f0 = ior_to_specular(ior); + return brdf_fresnel_schlick_f0(LxH, roughness, f0); +} + +// Exact fresnel, can be slow. +float brdf_fresnel_exact(float cos_theta) { + const float eta = 1.51; + + float scale = 1 / eta; + float cos_theta_t_sqr = 1 - (1 - cos_theta * cos_theta) * (scale * scale); + + if (cos_theta_t_sqr <= 0.0) + return 1.0; + + float cos_theta_i = abs(cos_theta); + float cos_theta_t = sqrt(cos_theta_t_sqr); + + float Rs = (cos_theta_i - eta * cos_theta_t) / (cos_theta_i + eta * cos_theta_t); + float Rp = (eta * cos_theta_i - cos_theta_t) / (eta * cos_theta_i + cos_theta_t); + + return 0.5 * (Rs * Rs + Rp * Rp); +} + +// Exact conductor fresnel, can be slow, also only single channel +float brdf_fresnel_conductor_exact(float cos_theta_i, float eta, float k) { + /* Modified from "Optics" by K.D. Moeller, University Science Books, 1988 */ + float cosThetaI2 = cos_theta_i * cos_theta_i; + float sinThetaI2 = 1 - cosThetaI2; + float sinThetaI4 = sinThetaI2 * sinThetaI2; + + float temp1 = eta * eta - k * k - sinThetaI2; + float a2pb2 = sqrt(temp1 * temp1 + 4 * k * k * eta * eta); + float a = sqrt(0.5f * (a2pb2 + temp1)); + + float term1 = a2pb2 + cosThetaI2; + float term2 = 2 * a * cos_theta_i; + + float Rs2 = (term1 - term2) / (term1 + term2); + + float term3 = a2pb2 * cosThetaI2 + sinThetaI4; + float term4 = term2 * sinThetaI2; + + float Rp2 = Rs2 * (term3 - term4) / (term3 + term4); + return 0.5f * (Rp2 + Rs2); +} + + +// Approximation proposed in +// http://sirkan.iit.bme.hu/~szirmay/fresnel.pdf +vec3 brdf_fresnel_conductor_approx(float cos_theta, vec3 n, vec3 k) { + vec3 k_sq = k * k; + vec3 term0 = square(n - 1.0) + 4 * n * pow(1 - cos_theta, 5.0) + k_sq; + vec3 term1 = square(n + 1.0) + k_sq; + return term0 / term1; +} + +// Diffuse BRDF +float brdf_diffuse(float NxV, float NxL, float LxH, float VxH, float roughness) { + + // Choose one: + return brdf_lambert(); + // return brdf_disney_diffuse(NxV, NxL, LxH, roughness); +} + + +// Distribution +float brdf_distribution(float NxH, float roughness) +{ + NxH = max(1e-5, NxH); + roughness = max(0.0019, roughness); + + // Choose one: + // return brdf_distribution_blinn_phong(NxH, roughness); + // return brdf_distribution_beckmann(NxH, roughness); + // return brdf_distribution_exponential(NxH, roughness); + // return brdf_distribution_gaussian(NxH, roughness); + return brdf_distribution_trowbridge_reitz(NxH, roughness); + // return brdf_distribution_ggx(NxH, roughness); +} + +// Geometric Visibility +float brdf_visibility(float NxL, float NxV, float NxH, float VxH, float roughness) { + + // Choose one: + // float vis = brdf_visibility_neumann(NxV, NxL); + float vis = brdf_visibility_implicit(NxV, NxL); + // float vis = brdf_visibility_smith_ggx(NxV, NxL, roughness); + // float vis = brdf_visibility_schlick(NxV, NxL, roughness); + // float vis = brdf_visibility_cook_torrance(NxL, NxV, NxH, VxH); + // float vis = brdf_visibility_smith(NxL, NxV, roughness); + + + return vis; +} + +// Fresnel +float brdf_fresnel(float LxH, float roughness) { + // Default index of refraction + // TODO: Maybe make this configurable? + const float ior = 1.2; + + return brdf_fresnel_schlick(LxH, roughness, ior); + // return brdf_fresnel_cook_torrance(LxH, roughness, ior); +} + + +/* Material Functions */ +vec3 get_material_f0(Material m) { + // Material specular is already in the 0 .. 0.08 range + return mix(vec3(m.specular), m.basecolor, m.metallic); +} + + +// Returns a reflection vector, bent into the normal direction +vec3 get_reflection_vector(Material m, vec3 view_vector) { + float roughness = m.shading_model == SHADING_MODEL_CLEARCOAT ? + CLEARCOAT_ROUGHNESS : m.roughness; + vec3 reflected_dir = reflect(view_vector, m.normal); + // XXX: Evaluate whats more physically correct + // return normalize(mix(m.normal, reflected_dir, (1 - roughness) * (roughness + sqrt(1 - roughness)))); + return reflected_dir; +} + + +// Returns an approximated mipmap level based on the materials roughness +// level to approximate importance sampled references +float get_specular_mipmap(float roughness) { + + // Approximation to match importance sampled reference, tuned for a + // resolution of 128. + return (roughness * 12.0 - pow(roughness, 6.0) * 1.5); +} + +float get_specular_mipmap(Material m) { + return get_specular_mipmap(m.roughness); +} + +vec3 get_brdf_from_lut(sampler3D lut_texture, float NxV, float roughness, float ior) { + float lookup_slice = (ior - 1.01) / 1.5 + 0.5 / 15.0; + vec3 data = textureLod(lut_texture, vec3(NxV, roughness, lookup_slice), 0).xyz; + + // Unpack packed data + data *= data; + + return data; +} + +vec3 get_brdf_from_lut(sampler2D lut_texture, float NxV, float roughness) { + vec3 data = textureLod(lut_texture, vec2(NxV, roughness), 0).xyz; + // Unpack packed data + data *= data; + return data; +} + +float get_effective_roughness(Material m) { + return m.shading_model == SHADING_MODEL_CLEARCOAT ? CLEARCOAT_ROUGHNESS : m.roughness; +} + +float get_mipmap_for_roughness(samplerCube map, float roughness, float NxV) { + return sqrt(roughness) * 7.0; +} + + +vec3 get_metallic_fresnel_approx(Material m, float NxV) { + vec3 metallic_energy_f0 = vec3(1.0 - 0.7 * m.roughness) * m.basecolor; + vec3 metallic_energy_f90 = mix(vec3(1), 0.5 * m.basecolor, m.linear_roughness); + vec3 metallic_fresnel = mix(metallic_energy_f0, metallic_energy_f90, + pow(1 - NxV, 3.6 - 2.6 * m.linear_roughness)); + return metallic_fresnel; +} diff --git a/RenderPipelineFile/rpcore/shader/includes/color_spaces.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/color_spaces.inc.glsl index cde2c672..1deefcd3 100644 --- a/RenderPipelineFile/rpcore/shader/includes/color_spaces.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/color_spaces.inc.glsl @@ -1,301 +1,301 @@ -/* - -GLSL Color Space Utility Functions -(c) 2015 tobspr - -------------------------------------------------------------------------------- - -The MIT License (MIT) - -Copyright (c) 2015 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -------------------------------------------------------------------------------- - -Most formulars / matrices are from: -https://en.wikipedia.org/wiki/SRGB - -Some are from: -http://www.chilliant.com/rgb2hsv.html - -*/ - -#pragma once - -// Define saturation macro, if not already user-defined -#ifndef saturate -#define saturate(v) clamp(v, 0, 1) -#endif - -// Constants -const float HCV_EPSILON = 1e-10; -const float HSL_EPSILON = 1e-10; -const float HCY_EPSILON = 1e-10; - -const float SRGB_GAMMA = 1.0 / 2.2; -const float SRGB_INVERSE_GAMMA = 2.2; -const float SRGB_ALPHA = 0.055; - - -// Used to convert from linear RGB to XYZ space -const mat3 RGB_2_XYZ = (mat3( - 0.4124564, 0.3575761, 0.1804375, - 0.2126729, 0.7151522, 0.0721750, - 0.0193339, 0.1191920, 0.9503041 -)); - -// Used to convert from XYZ to linear RGB space -const mat3 XYZ_2_RGB = (mat3( - 3.2404542, -1.5371385, -0.4985314, - -0.9692660, 1.8760108, 0.0415560, - 0.0556434, -0.2040259, 1.0572252 -)); - -const vec3 LUMA_COEFFS = vec3(0.2126, 0.7152, 0.0722); - - -// Returns the luminance of a !! linear !! rgb color -float get_luminance(vec3 rgb) { - return dot(LUMA_COEFFS, rgb); -} - -// Converts a linear rgb color to a srgb color (approximated, but fast) -vec3 rgb_to_srgb_approx(vec3 rgb) { - return pow(rgb, vec3(SRGB_GAMMA)); -} - -// Converts a srgb color to a rgb color (approximated, but fast) -vec3 srgb_to_rgb_approx(vec3 srgb) { - return pow(srgb, vec3(SRGB_INVERSE_GAMMA)); -} - -// Converts a single linear channel to srgb -float linear_to_srgb(float channel) { - if(channel <= 0.0031308) - return 12.92 * channel; - else - return (1.0 + SRGB_ALPHA) * pow(channel, 1.0 / 2.4) - SRGB_ALPHA; -} - -// Converts a single srgb channel to rgb -float srgb_to_linear(float channel) { - if (channel <= 0.04045) - return channel / 12.92; - else - return pow((channel + SRGB_ALPHA) / (1.0 + SRGB_ALPHA), 2.4); -} - -// Converts a linear rgb color to a srgb color (exact, not approximated) -vec3 rgb_to_srgb(vec3 rgb) { - return vec3( - linear_to_srgb(rgb.r), - linear_to_srgb(rgb.g), - linear_to_srgb(rgb.b) - ); -} - -// Converts a srgb color to a linear rgb color (exact, not approximated) -vec3 srgb_to_rgb(vec3 srgb) { - return vec3( - srgb_to_linear(srgb.r), - srgb_to_linear(srgb.g), - srgb_to_linear(srgb.b) - ); -} - -// Converts a color from linear RGB to XYZ space -vec3 rgb_to_xyz(vec3 rgb) { - return RGB_2_XYZ * rgb; -} - -// Converts a color from XYZ to linear RGB space -vec3 xyz_to_rgb(vec3 xyz) { - return XYZ_2_RGB * xyz; -} - -// Converts a color from XYZ to xyY space (Y is luminosity) -vec3 xyz_to_xyY(vec3 xyz) { - float Y = xyz.y; - float x = xyz.x / (xyz.x + xyz.y + xyz.z); - float y = xyz.y / (xyz.x + xyz.y + xyz.z); - return vec3(x, y, Y); -} - -// Converts a color from xyY space to XYZ space -vec3 xyY_to_xyz(vec3 xyY) { - float Y = xyY.z; - float x = Y * xyY.x / xyY.y; - float z = Y * (1.0 - xyY.x - xyY.y) / xyY.y; - return vec3(x, Y, z); -} - -// Converts a color from linear RGB to xyY space -vec3 rgb_to_xyY(vec3 rgb) { - vec3 xyz = rgb_to_xyz(rgb); - return xyz_to_xyY(xyz); -} - -// Converts a color from xyY space to linear RGB -vec3 xyY_to_rgb(vec3 xyY) { - vec3 xyz = xyY_to_xyz(xyY); - return xyz_to_rgb(xyz); -} - -// Converts a value from linear RGB to HCV (Hue, Chroma, Value) -vec3 rgb_to_hcv(vec3 rgb) -{ - // Based on work by Sam Hocevar and Emil Persson - vec4 P = (rgb.g < rgb.b) ? - vec4(rgb.bg, -1.0, 2.0 / 3.0) : vec4(rgb.gb, 0.0, -1.0 / 3.0); - vec4 Q = (rgb.r < P.x) ? vec4(P.xyw, rgb.r) : vec4(rgb.r, P.yzx); - float C = Q.x - min(Q.w, Q.y); - float H = abs((Q.w - Q.y) / (6 * C + HCV_EPSILON) + Q.z); - return vec3(H, C, Q.x); -} - -// Converts from pure Hue to linear RGB -vec3 hue_to_rgb(float hue) -{ - float R = abs(hue * 6 - 3) - 1; - float G = 2 - abs(hue * 6 - 2); - float B = 2 - abs(hue * 6 - 4); - return saturate(vec3(R, G, B)); -} - -// Converts from HSV to linear RGB -vec3 hsv_to_rgb(vec3 hsv) -{ - vec3 rgb = hue_to_rgb(hsv.x); - return ((rgb - 1.0) * hsv.y + 1.0) * hsv.z; -} - -// Converts from HSL to linear RGB -vec3 hsl_to_rgb(vec3 hsl) -{ - vec3 rgb = hue_to_rgb(hsl.x); - float C = (1 - abs(2 * hsl.z - 1)) * hsl.y; - return (rgb - 0.5) * C + hsl.z; -} - -// Converts from HCY to linear RGB -vec3 hcy_to_rgb(vec3 hcy) -{ - const vec3 HCYwts = vec3(0.299, 0.587, 0.114); - vec3 RGB = hue_to_rgb(hcy.x); - float Z = dot(RGB, HCYwts); - if (hcy.z < Z) { - hcy.y *= hcy.z / Z; - } else if (Z < 1) { - hcy.y *= (1 - hcy.z) / (1 - Z); - } - return (RGB - Z) * hcy.y + hcy.z; -} - - -// Converts from linear RGB to HSV -vec3 rgb_to_hsv(vec3 rgb) -{ - vec3 HCV = rgb_to_hcv(rgb); - float S = HCV.y / (HCV.z + HCV_EPSILON); - return vec3(HCV.x, S, HCV.z); -} - -// Converts from linear rgb to HSL -vec3 rgb_to_hsl(vec3 rgb) -{ - vec3 HCV = rgb_to_hcv(rgb); - float L = HCV.z - HCV.y * 0.5; - float S = HCV.y / (1 - abs(L * 2 - 1) + HSL_EPSILON); - return vec3(HCV.x, S, L); -} - -// Converts from rgb to hcy (Hue, Chroma, Luminance) -vec3 rgb_to_hcy(vec3 rgb) -{ - const vec3 HCYwts = vec3(0.299, 0.587, 0.114); - // Corrected by David Schaeffer - vec3 HCV = rgb_to_hcv(rgb); - float Y = dot(rgb, HCYwts); - float Z = dot(hue_to_rgb(HCV.x), HCYwts); - if (Y < Z) { - HCV.y *= Z / (HCY_EPSILON + Y); - } else { - HCV.y *= (1 - Z) / (HCY_EPSILON + 1 - Y); - } - return vec3(HCV.x, HCV.y, Y); -} - -// Additional conversions converting to rgb first and then to the desired -// color space. - -// To srgb -vec3 xyz_to_srgb(vec3 xyz) { return rgb_to_srgb(xyz_to_rgb(xyz)); } -vec3 xyY_to_srgb(vec3 xyY) { return rgb_to_srgb(xyY_to_rgb(xyY)); } -vec3 hue_to_srgb(float hue) { return rgb_to_srgb(hue_to_rgb(hue)); } -vec3 hsv_to_srgb(vec3 hsv) { return rgb_to_srgb(hsv_to_rgb(hsv)); } -vec3 hsl_to_srgb(vec3 hsl) { return rgb_to_srgb(hsl_to_rgb(hsl)); } -vec3 hcy_to_srgb(vec3 hcy) { return rgb_to_srgb(hcy_to_rgb(hcy)); } - -// To xyz -vec3 srgb_to_xyz(vec3 srgb) { return rgb_to_xyz(srgb_to_rgb(srgb)); } -vec3 hue_to_xyz(float hue) { return rgb_to_xyz(hue_to_rgb(hue)); } -vec3 hsv_to_xyz(vec3 hsv) { return rgb_to_xyz(hsv_to_rgb(hsv)); } -vec3 hsl_to_xyz(vec3 hsl) { return rgb_to_xyz(hsl_to_rgb(hsl)); } -vec3 hcy_to_xyz(vec3 hcy) { return rgb_to_xyz(hcy_to_rgb(hcy)); } - -// To xyY -vec3 srgb_to_xyY(vec3 srgb) { return rgb_to_xyY(srgb_to_rgb(srgb)); } -vec3 hue_to_xyY(float hue) { return rgb_to_xyY(hue_to_rgb(hue)); } -vec3 hsv_to_xyY(vec3 hsv) { return rgb_to_xyY(hsv_to_rgb(hsv)); } -vec3 hsl_to_xyY(vec3 hsl) { return rgb_to_xyY(hsl_to_rgb(hsl)); } -vec3 hcy_to_xyY(vec3 hcy) { return rgb_to_xyY(hcy_to_rgb(hcy)); } - -// To HCV -vec3 srgb_to_hcv(vec3 srgb) { return rgb_to_hcv(srgb_to_rgb(srgb)); } -vec3 xyz_to_hcv(vec3 xyz) { return rgb_to_hcv(xyz_to_rgb(xyz)); } -vec3 xyY_to_hcv(vec3 xyY) { return rgb_to_hcv(xyY_to_rgb(xyY)); } -vec3 hue_to_hcv(float hue) { return rgb_to_hcv(hue_to_rgb(hue)); } -vec3 hsv_to_hcv(vec3 hsv) { return rgb_to_hcv(hsv_to_rgb(hsv)); } -vec3 hsl_to_hcv(vec3 hsl) { return rgb_to_hcv(hsl_to_rgb(hsl)); } -vec3 hcy_to_hcv(vec3 hcy) { return rgb_to_hcv(hcy_to_rgb(hcy)); } - -// To HSV -vec3 srgb_to_hsv(vec3 srgb) { return rgb_to_hsv(srgb_to_rgb(srgb)); } -vec3 xyz_to_hsv(vec3 xyz) { return rgb_to_hsv(xyz_to_rgb(xyz)); } -vec3 xyY_to_hsv(vec3 xyY) { return rgb_to_hsv(xyY_to_rgb(xyY)); } -vec3 hue_to_hsv(float hue) { return rgb_to_hsv(hue_to_rgb(hue)); } -vec3 hsl_to_hsv(vec3 hsl) { return rgb_to_hsv(hsl_to_rgb(hsl)); } -vec3 hcy_to_hsv(vec3 hcy) { return rgb_to_hsv(hcy_to_rgb(hcy)); } - -// To HSL -vec3 srgb_to_hsl(vec3 srgb) { return rgb_to_hsl(srgb_to_rgb(srgb)); } -vec3 xyz_to_hsl(vec3 xyz) { return rgb_to_hsl(xyz_to_rgb(xyz)); } -vec3 xyY_to_hsl(vec3 xyY) { return rgb_to_hsl(xyY_to_rgb(xyY)); } -vec3 hue_to_hsl(float hue) { return rgb_to_hsl(hue_to_rgb(hue)); } -vec3 hsv_to_hsl(vec3 hsv) { return rgb_to_hsl(hsv_to_rgb(hsv)); } -vec3 hcy_to_hsl(vec3 hcy) { return rgb_to_hsl(hcy_to_rgb(hcy)); } - -// To HCY -vec3 srgb_to_hcy(vec3 srgb) { return rgb_to_hcy(srgb_to_rgb(srgb)); } -vec3 xyz_to_hcy(vec3 xyz) { return rgb_to_hcy(xyz_to_rgb(xyz)); } -vec3 xyY_to_hcy(vec3 xyY) { return rgb_to_hcy(xyY_to_rgb(xyY)); } -vec3 hue_to_hcy(float hue) { return rgb_to_hcy(hue_to_rgb(hue)); } -vec3 hsv_to_hcy(vec3 hsv) { return rgb_to_hcy(hsv_to_rgb(hsv)); } -vec3 hsl_to_hcy(vec3 hcy) { return rgb_to_hcy(hsl_to_rgb(hcy)); } +/* + +GLSL Color Space Utility Functions +(c) 2015 tobspr + +------------------------------------------------------------------------------- + +The MIT License (MIT) + +Copyright (c) 2015 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------------------- + +Most formulars / matrices are from: +https://en.wikipedia.org/wiki/SRGB + +Some are from: +http://www.chilliant.com/rgb2hsv.html + +*/ + +#pragma once + +// Define saturation macro, if not already user-defined +#ifndef saturate +#define saturate(v) clamp(v, 0, 1) +#endif + +// Constants +const float HCV_EPSILON = 1e-10; +const float HSL_EPSILON = 1e-10; +const float HCY_EPSILON = 1e-10; + +const float SRGB_GAMMA = 1.0 / 2.2; +const float SRGB_INVERSE_GAMMA = 2.2; +const float SRGB_ALPHA = 0.055; + + +// Used to convert from linear RGB to XYZ space +const mat3 RGB_2_XYZ = (mat3( + 0.4124564, 0.3575761, 0.1804375, + 0.2126729, 0.7151522, 0.0721750, + 0.0193339, 0.1191920, 0.9503041 +)); + +// Used to convert from XYZ to linear RGB space +const mat3 XYZ_2_RGB = (mat3( + 3.2404542, -1.5371385, -0.4985314, + -0.9692660, 1.8760108, 0.0415560, + 0.0556434, -0.2040259, 1.0572252 +)); + +const vec3 LUMA_COEFFS = vec3(0.2126, 0.7152, 0.0722); + + +// Returns the luminance of a !! linear !! rgb color +float get_luminance(vec3 rgb) { + return dot(LUMA_COEFFS, rgb); +} + +// Converts a linear rgb color to a srgb color (approximated, but fast) +vec3 rgb_to_srgb_approx(vec3 rgb) { + return pow(rgb, vec3(SRGB_GAMMA)); +} + +// Converts a srgb color to a rgb color (approximated, but fast) +vec3 srgb_to_rgb_approx(vec3 srgb) { + return pow(srgb, vec3(SRGB_INVERSE_GAMMA)); +} + +// Converts a single linear channel to srgb +float linear_to_srgb(float channel) { + if(channel <= 0.0031308) + return 12.92 * channel; + else + return (1.0 + SRGB_ALPHA) * pow(channel, 1.0 / 2.4) - SRGB_ALPHA; +} + +// Converts a single srgb channel to rgb +float srgb_to_linear(float channel) { + if (channel <= 0.04045) + return channel / 12.92; + else + return pow((channel + SRGB_ALPHA) / (1.0 + SRGB_ALPHA), 2.4); +} + +// Converts a linear rgb color to a srgb color (exact, not approximated) +vec3 rgb_to_srgb(vec3 rgb) { + return vec3( + linear_to_srgb(rgb.r), + linear_to_srgb(rgb.g), + linear_to_srgb(rgb.b) + ); +} + +// Converts a srgb color to a linear rgb color (exact, not approximated) +vec3 srgb_to_rgb(vec3 srgb) { + return vec3( + srgb_to_linear(srgb.r), + srgb_to_linear(srgb.g), + srgb_to_linear(srgb.b) + ); +} + +// Converts a color from linear RGB to XYZ space +vec3 rgb_to_xyz(vec3 rgb) { + return RGB_2_XYZ * rgb; +} + +// Converts a color from XYZ to linear RGB space +vec3 xyz_to_rgb(vec3 xyz) { + return XYZ_2_RGB * xyz; +} + +// Converts a color from XYZ to xyY space (Y is luminosity) +vec3 xyz_to_xyY(vec3 xyz) { + float Y = xyz.y; + float x = xyz.x / (xyz.x + xyz.y + xyz.z); + float y = xyz.y / (xyz.x + xyz.y + xyz.z); + return vec3(x, y, Y); +} + +// Converts a color from xyY space to XYZ space +vec3 xyY_to_xyz(vec3 xyY) { + float Y = xyY.z; + float x = Y * xyY.x / xyY.y; + float z = Y * (1.0 - xyY.x - xyY.y) / xyY.y; + return vec3(x, Y, z); +} + +// Converts a color from linear RGB to xyY space +vec3 rgb_to_xyY(vec3 rgb) { + vec3 xyz = rgb_to_xyz(rgb); + return xyz_to_xyY(xyz); +} + +// Converts a color from xyY space to linear RGB +vec3 xyY_to_rgb(vec3 xyY) { + vec3 xyz = xyY_to_xyz(xyY); + return xyz_to_rgb(xyz); +} + +// Converts a value from linear RGB to HCV (Hue, Chroma, Value) +vec3 rgb_to_hcv(vec3 rgb) +{ + // Based on work by Sam Hocevar and Emil Persson + vec4 P = (rgb.g < rgb.b) ? + vec4(rgb.bg, -1.0, 2.0 / 3.0) : vec4(rgb.gb, 0.0, -1.0 / 3.0); + vec4 Q = (rgb.r < P.x) ? vec4(P.xyw, rgb.r) : vec4(rgb.r, P.yzx); + float C = Q.x - min(Q.w, Q.y); + float H = abs((Q.w - Q.y) / (6 * C + HCV_EPSILON) + Q.z); + return vec3(H, C, Q.x); +} + +// Converts from pure Hue to linear RGB +vec3 hue_to_rgb(float hue) +{ + float R = abs(hue * 6 - 3) - 1; + float G = 2 - abs(hue * 6 - 2); + float B = 2 - abs(hue * 6 - 4); + return saturate(vec3(R, G, B)); +} + +// Converts from HSV to linear RGB +vec3 hsv_to_rgb(vec3 hsv) +{ + vec3 rgb = hue_to_rgb(hsv.x); + return ((rgb - 1.0) * hsv.y + 1.0) * hsv.z; +} + +// Converts from HSL to linear RGB +vec3 hsl_to_rgb(vec3 hsl) +{ + vec3 rgb = hue_to_rgb(hsl.x); + float C = (1 - abs(2 * hsl.z - 1)) * hsl.y; + return (rgb - 0.5) * C + hsl.z; +} + +// Converts from HCY to linear RGB +vec3 hcy_to_rgb(vec3 hcy) +{ + const vec3 HCYwts = vec3(0.299, 0.587, 0.114); + vec3 RGB = hue_to_rgb(hcy.x); + float Z = dot(RGB, HCYwts); + if (hcy.z < Z) { + hcy.y *= hcy.z / Z; + } else if (Z < 1) { + hcy.y *= (1 - hcy.z) / (1 - Z); + } + return (RGB - Z) * hcy.y + hcy.z; +} + + +// Converts from linear RGB to HSV +vec3 rgb_to_hsv(vec3 rgb) +{ + vec3 HCV = rgb_to_hcv(rgb); + float S = HCV.y / (HCV.z + HCV_EPSILON); + return vec3(HCV.x, S, HCV.z); +} + +// Converts from linear rgb to HSL +vec3 rgb_to_hsl(vec3 rgb) +{ + vec3 HCV = rgb_to_hcv(rgb); + float L = HCV.z - HCV.y * 0.5; + float S = HCV.y / (1 - abs(L * 2 - 1) + HSL_EPSILON); + return vec3(HCV.x, S, L); +} + +// Converts from rgb to hcy (Hue, Chroma, Luminance) +vec3 rgb_to_hcy(vec3 rgb) +{ + const vec3 HCYwts = vec3(0.299, 0.587, 0.114); + // Corrected by David Schaeffer + vec3 HCV = rgb_to_hcv(rgb); + float Y = dot(rgb, HCYwts); + float Z = dot(hue_to_rgb(HCV.x), HCYwts); + if (Y < Z) { + HCV.y *= Z / (HCY_EPSILON + Y); + } else { + HCV.y *= (1 - Z) / (HCY_EPSILON + 1 - Y); + } + return vec3(HCV.x, HCV.y, Y); +} + +// Additional conversions converting to rgb first and then to the desired +// color space. + +// To srgb +vec3 xyz_to_srgb(vec3 xyz) { return rgb_to_srgb(xyz_to_rgb(xyz)); } +vec3 xyY_to_srgb(vec3 xyY) { return rgb_to_srgb(xyY_to_rgb(xyY)); } +vec3 hue_to_srgb(float hue) { return rgb_to_srgb(hue_to_rgb(hue)); } +vec3 hsv_to_srgb(vec3 hsv) { return rgb_to_srgb(hsv_to_rgb(hsv)); } +vec3 hsl_to_srgb(vec3 hsl) { return rgb_to_srgb(hsl_to_rgb(hsl)); } +vec3 hcy_to_srgb(vec3 hcy) { return rgb_to_srgb(hcy_to_rgb(hcy)); } + +// To xyz +vec3 srgb_to_xyz(vec3 srgb) { return rgb_to_xyz(srgb_to_rgb(srgb)); } +vec3 hue_to_xyz(float hue) { return rgb_to_xyz(hue_to_rgb(hue)); } +vec3 hsv_to_xyz(vec3 hsv) { return rgb_to_xyz(hsv_to_rgb(hsv)); } +vec3 hsl_to_xyz(vec3 hsl) { return rgb_to_xyz(hsl_to_rgb(hsl)); } +vec3 hcy_to_xyz(vec3 hcy) { return rgb_to_xyz(hcy_to_rgb(hcy)); } + +// To xyY +vec3 srgb_to_xyY(vec3 srgb) { return rgb_to_xyY(srgb_to_rgb(srgb)); } +vec3 hue_to_xyY(float hue) { return rgb_to_xyY(hue_to_rgb(hue)); } +vec3 hsv_to_xyY(vec3 hsv) { return rgb_to_xyY(hsv_to_rgb(hsv)); } +vec3 hsl_to_xyY(vec3 hsl) { return rgb_to_xyY(hsl_to_rgb(hsl)); } +vec3 hcy_to_xyY(vec3 hcy) { return rgb_to_xyY(hcy_to_rgb(hcy)); } + +// To HCV +vec3 srgb_to_hcv(vec3 srgb) { return rgb_to_hcv(srgb_to_rgb(srgb)); } +vec3 xyz_to_hcv(vec3 xyz) { return rgb_to_hcv(xyz_to_rgb(xyz)); } +vec3 xyY_to_hcv(vec3 xyY) { return rgb_to_hcv(xyY_to_rgb(xyY)); } +vec3 hue_to_hcv(float hue) { return rgb_to_hcv(hue_to_rgb(hue)); } +vec3 hsv_to_hcv(vec3 hsv) { return rgb_to_hcv(hsv_to_rgb(hsv)); } +vec3 hsl_to_hcv(vec3 hsl) { return rgb_to_hcv(hsl_to_rgb(hsl)); } +vec3 hcy_to_hcv(vec3 hcy) { return rgb_to_hcv(hcy_to_rgb(hcy)); } + +// To HSV +vec3 srgb_to_hsv(vec3 srgb) { return rgb_to_hsv(srgb_to_rgb(srgb)); } +vec3 xyz_to_hsv(vec3 xyz) { return rgb_to_hsv(xyz_to_rgb(xyz)); } +vec3 xyY_to_hsv(vec3 xyY) { return rgb_to_hsv(xyY_to_rgb(xyY)); } +vec3 hue_to_hsv(float hue) { return rgb_to_hsv(hue_to_rgb(hue)); } +vec3 hsl_to_hsv(vec3 hsl) { return rgb_to_hsv(hsl_to_rgb(hsl)); } +vec3 hcy_to_hsv(vec3 hcy) { return rgb_to_hsv(hcy_to_rgb(hcy)); } + +// To HSL +vec3 srgb_to_hsl(vec3 srgb) { return rgb_to_hsl(srgb_to_rgb(srgb)); } +vec3 xyz_to_hsl(vec3 xyz) { return rgb_to_hsl(xyz_to_rgb(xyz)); } +vec3 xyY_to_hsl(vec3 xyY) { return rgb_to_hsl(xyY_to_rgb(xyY)); } +vec3 hue_to_hsl(float hue) { return rgb_to_hsl(hue_to_rgb(hue)); } +vec3 hsv_to_hsl(vec3 hsv) { return rgb_to_hsl(hsv_to_rgb(hsv)); } +vec3 hcy_to_hsl(vec3 hcy) { return rgb_to_hsl(hcy_to_rgb(hcy)); } + +// To HCY +vec3 srgb_to_hcy(vec3 srgb) { return rgb_to_hcy(srgb_to_rgb(srgb)); } +vec3 xyz_to_hcy(vec3 xyz) { return rgb_to_hcy(xyz_to_rgb(xyz)); } +vec3 xyY_to_hcy(vec3 xyY) { return rgb_to_hcy(xyY_to_rgb(xyY)); } +vec3 hue_to_hcy(float hue) { return rgb_to_hcy(hue_to_rgb(hue)); } +vec3 hsv_to_hcy(vec3 hsv) { return rgb_to_hcy(hsv_to_rgb(hsv)); } +vec3 hsl_to_hcy(vec3 hcy) { return rgb_to_hcy(hsl_to_rgb(hcy)); } diff --git a/RenderPipelineFile/rpcore/shader/includes/common_functions.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/common_functions.inc.glsl index 3dcd4e1a..d51777fb 100644 --- a/RenderPipelineFile/rpcore/shader/includes/common_functions.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/common_functions.inc.glsl @@ -1,320 +1,320 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -#define saturate(v) clamp(v, 0, 1) - -#define M_PI 3.1415926535897932384626433 -#define HALF_PI 1.5707963267948966192313216 -#define TWO_PI 6.2831853071795864769252867 -#define FOUR_PI 12.566370614359172953850573 -#define ONE_BY_PI 0.3183098861837906715377675 -#define SQRT_TWO 1.4142135623730950488016887 - -#define AIR_IOR 1.000277 - -// Fixes the cubemap direction -vec3 fix_cubemap_coord(vec3 coord) { - // return normalize(coord.xzy * vec3(1,-1,1)); - return normalize(coord.yxz * vec3(-1, 1, 1)); -} - - -// Converts a texture coodinate and face index to a direction vector -vec3 get_cubemap_coordinate(int face_index, vec2 coord) { - vec3 baseDir = vec3(0); - if (face_index == 0) baseDir = vec3(1.0, -coord.y, -coord.x); - else if (face_index == 1) baseDir = vec3(-1.0, -coord.y, coord.x); - else if (face_index == 2) baseDir = vec3(coord.x, 1.0, coord.y); - else if (face_index == 3) baseDir = vec3(coord.x, -1.0, -coord.y); - else if (face_index == 4) baseDir = vec3(coord.x, -coord.y, 1.0); - else if (face_index == 5) baseDir = vec3(-coord.x, -coord.y, -1.0); - return normalize(baseDir); -} - -// Computes the skydome texcoord based on the pixels view direction -vec2 get_skydome_coord(vec3 view_dir) { - float angle = (atan(view_dir.x, view_dir.y) + M_PI) / (2.0 * M_PI); - return vec2(angle, view_dir.z); -} - -// Converts a coordinate like "gl_FragCoord" to a cubemap direction, with a given -// cubemap size. Assumes the render target has a size of (6 * cubemap_size, cubemap_size). -// clamped_coord will contain a coordinate from (0 .. cubemap_size), this can be -// used for further processing or writing the result. -// face will contain the selected cubemap face. -vec3 texcoord_to_cubemap(int cubemap_size, ivec2 coord, out ivec2 clamped_coord, out int face) { - face = coord.x / cubemap_size; - clamped_coord = coord % cubemap_size; - vec2 local_coord = saturate((clamped_coord + 0.5) / float(cubemap_size)) * 2.0 - 1.0; - return get_cubemap_coordinate(face, local_coord); -} - - -// Version of texcoord_to_cubemap without the out parameters -vec3 texcoord_to_cubemap(int cubemap_size, ivec2 coord) { - ivec2 clamped_coord; int face; - return texcoord_to_cubemap(cubemap_size, coord, clamped_coord, face); -} - - -// Rotate a vector by a given sinus and cosinus of an angle -vec2 rotate(vec2 vector, float cos_alpha, float sin_alpha) { - return vec2( - vector.x * cos_alpha - vector.y * sin_alpha, - vector.x * sin_alpha + vector.y * cos_alpha - ); -} - -// Rotate a vector by an angle -vec2 rotate(vec2 vector, float angle) { - float cos_alpha = cos(angle); - float sin_alpha = sin(angle); - return rotate(vector, cos_alpha, sin_alpha); -} - -// Returns a coordinate which can be used for bilateral upscaling -ivec2 get_bilateral_coord(ivec2 coord) { - return (coord + 1) / 2 - 1; -} - -// Checks if a 2D-coordinate exceeds the [0, 1] range -bool out_of_screen(vec2 tcoord) { - return tcoord.x < 0.0 || tcoord.y < 0.0 || tcoord.x > 1.0 || tcoord.y > 1.0; -} - -// Checks if a 3D-coordinate exceeds the [0, 1] range -bool out_of_unit_box(vec3 coord) { - return coord.x < 0.0 || coord.y < 0.0 || coord.z < 0.0 || - coord.x > 1.0 || coord.y > 1.0 || coord.z > 1.0; -} - - -// Finds a tangent and bitangent vector based on a given normal -void find_arbitrary_tangent(vec3 normal, out vec3 tangent, out vec3 bitangent) { - vec3 v0 = abs(normal.z) < (0.99) ? vec3(0, 0, 1) : vec3(0, 1, 0); - tangent = normalize(cross(v0, normal)); - bitangent = normalize(cross(tangent, normal)); -} - -// Transforms a given vector to tangent space -vec3 tangent_to_world(vec3 normal, vec3 vector) -{ - vec3 tangent, binormal; - find_arbitrary_tangent(normal, tangent, binormal); - return vector.x * tangent + vector.y * binormal + vector.z * normal; -} - - -// Returns the number of mipmaps of a cubemap -int get_mipmap_count(samplerCube cubemap) { - int cubemap_size = textureSize(cubemap, 0).x; - return int(1 + floor(log2(cubemap_size))); -} - -// Returns the number of mipmaps of a 2D Texture -int get_mipmap_count(sampler2D tex) { - int tex_size = textureSize(tex, 0).x; - return int(1 + floor(log2(tex_size))); -} - -// Returns the number of mipmaps of a cubemap arry -int get_mipmap_count(samplerCubeArray cubemap) { - int cubemap_size = textureSize(cubemap, 0).x; - return int(1 + floor(log2(cubemap_size))); -} - -// Converts a normalized spherical coordinate (r = 1) to cartesian coordinates -vec3 spherical_to_vector(float theta, float phi) { - float sin_theta = sin(theta); - return normalize(vec3( - sin_theta * cos(phi), - sin_theta * sin(phi), - cos(theta) - )); -} - -// Converts a cartesian coordinate to spherical coordinates -void vector_to_spherical(vec3 v, out float theta, out float phi, out float radius) { - radius = sqrt(dot(v, v)); - phi = acos(v.z / radius); - theta = atan(v.y, v.x) + M_PI; -} - -// Convertsa normalized vector to spherical coordinates -void vector_to_spherical(vec3 v, out float theta, out float phi) { - phi = acos(v.z); - theta = atan(v.y, v.x) + M_PI; -} - - - -// Converts a given sun azimuth and altitude to a direction vector -vec3 sun_azimuth_to_angle(float azimuth, float altitude) { - float theta = (90 - altitude) / 180.0 * M_PI; - float phi = azimuth / 180.0 * M_PI; - return spherical_to_vector(theta, phi); -} - - -// Blends a given color soft with the base color -// From: https://github.com/mattdesl/glsl-blend-soft-light/blob/master/index.glsl -vec3 blend_soft_light(vec3 base, vec3 blend) { - return mix( - sqrt(base) * (2.0 * blend - 1.0) + 2.0 * base * (1.0 - blend), - 2.0 * base * blend + base * base * (1.0 - 2.0 * blend), - step(base, vec3(0.5)) - ); -} - -// Normalizes v without taking the w component into account -vec4 normalize_without_w(vec4 v) { - return v / length(v.xyz); -} - -// Unpacks a normal from 0 .. 1 range to the -1 .. 1 range -vec3 unpack_texture_normal(vec3 n) { - return fma(n, vec3(2.0), vec3(-1.0)); -} - -// Converts a value from linear to logarithmic distribution, using a given factor -float make_logarithmic(float x, float factor) { - // return (exp(factor * x)-1) / (exp(factor)-1); - return log(factor * x + 1.0 + 1e-6) / log(1.0 + factor); -} - -// Makes a 3 component vector logarithmic -vec3 make_logarithmic(vec3 v, float factor) { - return log(factor * v + 1.0 + 1e-6) / log(1.0 + factor); -} - -// Unpacks an integer from a float, packed by the GPUCommandQueue. -int gpu_cq_unpack_int_from_float(float v) { - #if GPU_CMD_INT_AS_FLOAT - return floatBitsToInt(v); - #else - return int(v); - #endif -} - -// Computes the diffuse antialiasing factor -// From: http://blog.selfshadow.com/sandbox/diffuse_aa.html -float get_diffuse_aa(float w, float NxL) { - float x = sqrt(1.0 - w); - float x0 = 0.373837 * NxL; - float x1 = 0.66874 * x; - float n = x0 + x1; - return w * ((abs(x0) <= x1) ? n * n / x : saturate(NxL)); -} - -// Blends a material -float blend_material(float material_factor, float detailmap, float add_factor, float pow_factor) { - material_factor = max(0, material_factor); - return saturate( - mix(pow(max(0, detailmap + add_factor), pow_factor), 1.0, material_factor) * - material_factor); -} - -// Blends a specular IOR to make sure it never drops below 1.0 -float blend_ior(float material_specular, float sampled_specular) { - return 1.0 + max(0, material_specular - 1.0) * 2.0 * sampled_specular; -} - -// We need to define them as macros instead of functions, since gl_FragCoord is -// not available in compute shaders - -// Regular texcoord -#define get_texcoord() (gl_FragCoord.xy / SCREEN_SIZE) - -// Texcoord for half-res targets sampling full-res targets -#define get_half_texcoord() vec2((ivec2(gl_FragCoord.xy) * 2 + 0.5) / SCREEN_SIZE) - -// Texcoord for half-res targets sampling half-res targets -#define get_half_native_texcoord() ((vec2(gl_FragCoord.xy) + 0.5) / ivec2(SCREEN_SIZE / 2)) - -// Converts degree (0 .. 360) to radians (0 .. 2 PI) -float degree_to_radians(float degree) { - return degree / 180.0 * M_PI; -} - -// Converts radians (0 .. 2 PI) to degree (0 .. 360) -float radians_to_degree(float radians) { - return radians / M_PI * 180.0; -} - -// Simulates a near filter on a fullscreen texture -vec2 truncate_coordinate(vec2 tcoord) { - return (ivec2(tcoord * SCREEN_SIZE) + 0.5) / SCREEN_SIZE; -} - -// Returns the squared length of v -float length_squared(vec2 v) { return dot(v, v); } -float length_squared(vec3 v) { return dot(v, v); } -float length_squared(vec4 v) { return dot(v, v); } - -// Squared distance between a and b -float distance_squared(vec3 a, vec3 b) { return length_squared(a - b); } -float distance_squared(vec4 a, vec4 b) { return length_squared(a - b); } -float distance_squared(vec2 a, vec2 b) { return length_squared(a - b); } - -// Returns x * x -float square(float x) { return x * x; } -vec2 square(vec2 x) { return x * x; } -vec3 square(vec3 x) { return x * x; } -vec4 square(vec4 x) { return x * x; } - -// Minimum and maximum for multiple components -// XXX: There are hardware instructions (at least on AMD) for it: -// https://www.opengl.org/registry/specs/AMD/shader_trinary_minmax.txt -#define min3(a, b, c) min(a, min(b, c)) -#define max3(a, b, c) max(a, max(b, c)) -#define min4(a, b, c, d) min(a, min(b, min(c, d))) -#define max4(a, b, c, d) max(a, max(b, max(c, d))) -#define min5(a, b, c, d, e) min(a, min(b, min(c, min(d, e)))) -#define max5(a, b, c, d, e) max(a, max(b, max(c, max(d, e)))) - -// Convenience function which ensures v dot n is > 0 -vec3 face_forward(vec3 v, vec3 n) { - return dot(v, n) < 0 ? -v : v; -} - -// Creates a rotation mat, rotation should be 0 .. 2 * pi -mat2 make_rotation_mat(float rotation) { - float r_sin = sin(rotation); - float r_cos = cos(rotation); - return mat2(r_cos, -r_sin, r_sin, r_cos); -} - -// Convenience functions for the scattering plugin - probably don't belong here -#define get_sun_vector() sun_azimuth_to_angle(TimeOfDay.scattering.sun_azimuth, TimeOfDay.scattering.sun_altitude) -#define get_sun_color() (TimeOfDay.scattering.sun_color * TimeOfDay.scattering.sun_intensity) -#define get_sun_color_scale(_v) saturate(square((_v.z) / 0.15)) - -#if !HAVE_PLUGIN(color_correction) - #undef get_sun_color - #define get_sun_color() (TimeOfDay.scattering.sun_color * TimeOfDay.scattering.sun_intensity) -#endif +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +#define saturate(v) clamp(v, 0, 1) + +#define M_PI 3.1415926535897932384626433 +#define HALF_PI 1.5707963267948966192313216 +#define TWO_PI 6.2831853071795864769252867 +#define FOUR_PI 12.566370614359172953850573 +#define ONE_BY_PI 0.3183098861837906715377675 +#define SQRT_TWO 1.4142135623730950488016887 + +#define AIR_IOR 1.000277 + +// Fixes the cubemap direction +vec3 fix_cubemap_coord(vec3 coord) { + // return normalize(coord.xzy * vec3(1,-1,1)); + return normalize(coord.yxz * vec3(-1, 1, 1)); +} + + +// Converts a texture coodinate and face index to a direction vector +vec3 get_cubemap_coordinate(int face_index, vec2 coord) { + vec3 baseDir = vec3(0); + if (face_index == 0) baseDir = vec3(1.0, -coord.y, -coord.x); + else if (face_index == 1) baseDir = vec3(-1.0, -coord.y, coord.x); + else if (face_index == 2) baseDir = vec3(coord.x, 1.0, coord.y); + else if (face_index == 3) baseDir = vec3(coord.x, -1.0, -coord.y); + else if (face_index == 4) baseDir = vec3(coord.x, -coord.y, 1.0); + else if (face_index == 5) baseDir = vec3(-coord.x, -coord.y, -1.0); + return normalize(baseDir); +} + +// Computes the skydome texcoord based on the pixels view direction +vec2 get_skydome_coord(vec3 view_dir) { + float angle = (atan(view_dir.x, view_dir.y) + M_PI) / (2.0 * M_PI); + return vec2(angle, view_dir.z); +} + +// Converts a coordinate like "gl_FragCoord" to a cubemap direction, with a given +// cubemap size. Assumes the render target has a size of (6 * cubemap_size, cubemap_size). +// clamped_coord will contain a coordinate from (0 .. cubemap_size), this can be +// used for further processing or writing the result. +// face will contain the selected cubemap face. +vec3 texcoord_to_cubemap(int cubemap_size, ivec2 coord, out ivec2 clamped_coord, out int face) { + face = coord.x / cubemap_size; + clamped_coord = coord % cubemap_size; + vec2 local_coord = saturate((clamped_coord + 0.5) / float(cubemap_size)) * 2.0 - 1.0; + return get_cubemap_coordinate(face, local_coord); +} + + +// Version of texcoord_to_cubemap without the out parameters +vec3 texcoord_to_cubemap(int cubemap_size, ivec2 coord) { + ivec2 clamped_coord; int face; + return texcoord_to_cubemap(cubemap_size, coord, clamped_coord, face); +} + + +// Rotate a vector by a given sinus and cosinus of an angle +vec2 rotate(vec2 vector, float cos_alpha, float sin_alpha) { + return vec2( + vector.x * cos_alpha - vector.y * sin_alpha, + vector.x * sin_alpha + vector.y * cos_alpha + ); +} + +// Rotate a vector by an angle +vec2 rotate(vec2 vector, float angle) { + float cos_alpha = cos(angle); + float sin_alpha = sin(angle); + return rotate(vector, cos_alpha, sin_alpha); +} + +// Returns a coordinate which can be used for bilateral upscaling +ivec2 get_bilateral_coord(ivec2 coord) { + return (coord + 1) / 2 - 1; +} + +// Checks if a 2D-coordinate exceeds the [0, 1] range +bool out_of_screen(vec2 tcoord) { + return tcoord.x < 0.0 || tcoord.y < 0.0 || tcoord.x > 1.0 || tcoord.y > 1.0; +} + +// Checks if a 3D-coordinate exceeds the [0, 1] range +bool out_of_unit_box(vec3 coord) { + return coord.x < 0.0 || coord.y < 0.0 || coord.z < 0.0 || + coord.x > 1.0 || coord.y > 1.0 || coord.z > 1.0; +} + + +// Finds a tangent and bitangent vector based on a given normal +void find_arbitrary_tangent(vec3 normal, out vec3 tangent, out vec3 bitangent) { + vec3 v0 = abs(normal.z) < (0.99) ? vec3(0, 0, 1) : vec3(0, 1, 0); + tangent = normalize(cross(v0, normal)); + bitangent = normalize(cross(tangent, normal)); +} + +// Transforms a given vector to tangent space +vec3 tangent_to_world(vec3 normal, vec3 vector) +{ + vec3 tangent, binormal; + find_arbitrary_tangent(normal, tangent, binormal); + return vector.x * tangent + vector.y * binormal + vector.z * normal; +} + + +// Returns the number of mipmaps of a cubemap +int get_mipmap_count(samplerCube cubemap) { + int cubemap_size = textureSize(cubemap, 0).x; + return int(1 + floor(log2(cubemap_size))); +} + +// Returns the number of mipmaps of a 2D Texture +int get_mipmap_count(sampler2D tex) { + int tex_size = textureSize(tex, 0).x; + return int(1 + floor(log2(tex_size))); +} + +// Returns the number of mipmaps of a cubemap arry +int get_mipmap_count(samplerCubeArray cubemap) { + int cubemap_size = textureSize(cubemap, 0).x; + return int(1 + floor(log2(cubemap_size))); +} + +// Converts a normalized spherical coordinate (r = 1) to cartesian coordinates +vec3 spherical_to_vector(float theta, float phi) { + float sin_theta = sin(theta); + return normalize(vec3( + sin_theta * cos(phi), + sin_theta * sin(phi), + cos(theta) + )); +} + +// Converts a cartesian coordinate to spherical coordinates +void vector_to_spherical(vec3 v, out float theta, out float phi, out float radius) { + radius = sqrt(dot(v, v)); + phi = acos(v.z / radius); + theta = atan(v.y, v.x) + M_PI; +} + +// Convertsa normalized vector to spherical coordinates +void vector_to_spherical(vec3 v, out float theta, out float phi) { + phi = acos(v.z); + theta = atan(v.y, v.x) + M_PI; +} + + + +// Converts a given sun azimuth and altitude to a direction vector +vec3 sun_azimuth_to_angle(float azimuth, float altitude) { + float theta = (90 - altitude) / 180.0 * M_PI; + float phi = azimuth / 180.0 * M_PI; + return spherical_to_vector(theta, phi); +} + + +// Blends a given color soft with the base color +// From: https://github.com/mattdesl/glsl-blend-soft-light/blob/master/index.glsl +vec3 blend_soft_light(vec3 base, vec3 blend) { + return mix( + sqrt(base) * (2.0 * blend - 1.0) + 2.0 * base * (1.0 - blend), + 2.0 * base * blend + base * base * (1.0 - 2.0 * blend), + step(base, vec3(0.5)) + ); +} + +// Normalizes v without taking the w component into account +vec4 normalize_without_w(vec4 v) { + return v / length(v.xyz); +} + +// Unpacks a normal from 0 .. 1 range to the -1 .. 1 range +vec3 unpack_texture_normal(vec3 n) { + return fma(n, vec3(2.0), vec3(-1.0)); +} + +// Converts a value from linear to logarithmic distribution, using a given factor +float make_logarithmic(float x, float factor) { + // return (exp(factor * x)-1) / (exp(factor)-1); + return log(factor * x + 1.0 + 1e-6) / log(1.0 + factor); +} + +// Makes a 3 component vector logarithmic +vec3 make_logarithmic(vec3 v, float factor) { + return log(factor * v + 1.0 + 1e-6) / log(1.0 + factor); +} + +// Unpacks an integer from a float, packed by the GPUCommandQueue. +int gpu_cq_unpack_int_from_float(float v) { + #if GPU_CMD_INT_AS_FLOAT + return floatBitsToInt(v); + #else + return int(v); + #endif +} + +// Computes the diffuse antialiasing factor +// From: http://blog.selfshadow.com/sandbox/diffuse_aa.html +float get_diffuse_aa(float w, float NxL) { + float x = sqrt(1.0 - w); + float x0 = 0.373837 * NxL; + float x1 = 0.66874 * x; + float n = x0 + x1; + return w * ((abs(x0) <= x1) ? n * n / x : saturate(NxL)); +} + +// Blends a material +float blend_material(float material_factor, float detailmap, float add_factor, float pow_factor) { + material_factor = max(0, material_factor); + return saturate( + mix(pow(max(0, detailmap + add_factor), pow_factor), 1.0, material_factor) * + material_factor); +} + +// Blends a specular IOR to make sure it never drops below 1.0 +float blend_ior(float material_specular, float sampled_specular) { + return 1.0 + max(0, material_specular - 1.0) * 2.0 * sampled_specular; +} + +// We need to define them as macros instead of functions, since gl_FragCoord is +// not available in compute shaders + +// Regular texcoord +#define get_texcoord() (gl_FragCoord.xy / SCREEN_SIZE) + +// Texcoord for half-res targets sampling full-res targets +#define get_half_texcoord() vec2((ivec2(gl_FragCoord.xy) * 2 + 0.5) / SCREEN_SIZE) + +// Texcoord for half-res targets sampling half-res targets +#define get_half_native_texcoord() ((vec2(gl_FragCoord.xy) + 0.5) / ivec2(SCREEN_SIZE / 2)) + +// Converts degree (0 .. 360) to radians (0 .. 2 PI) +float degree_to_radians(float degree) { + return degree / 180.0 * M_PI; +} + +// Converts radians (0 .. 2 PI) to degree (0 .. 360) +float radians_to_degree(float radians) { + return radians / M_PI * 180.0; +} + +// Simulates a near filter on a fullscreen texture +vec2 truncate_coordinate(vec2 tcoord) { + return (ivec2(tcoord * SCREEN_SIZE) + 0.5) / SCREEN_SIZE; +} + +// Returns the squared length of v +float length_squared(vec2 v) { return dot(v, v); } +float length_squared(vec3 v) { return dot(v, v); } +float length_squared(vec4 v) { return dot(v, v); } + +// Squared distance between a and b +float distance_squared(vec3 a, vec3 b) { return length_squared(a - b); } +float distance_squared(vec4 a, vec4 b) { return length_squared(a - b); } +float distance_squared(vec2 a, vec2 b) { return length_squared(a - b); } + +// Returns x * x +float square(float x) { return x * x; } +vec2 square(vec2 x) { return x * x; } +vec3 square(vec3 x) { return x * x; } +vec4 square(vec4 x) { return x * x; } + +// Minimum and maximum for multiple components +// XXX: There are hardware instructions (at least on AMD) for it: +// https://www.opengl.org/registry/specs/AMD/shader_trinary_minmax.txt +#define min3(a, b, c) min(a, min(b, c)) +#define max3(a, b, c) max(a, max(b, c)) +#define min4(a, b, c, d) min(a, min(b, min(c, d))) +#define max4(a, b, c, d) max(a, max(b, max(c, d))) +#define min5(a, b, c, d, e) min(a, min(b, min(c, min(d, e)))) +#define max5(a, b, c, d, e) max(a, max(b, max(c, max(d, e)))) + +// Convenience function which ensures v dot n is > 0 +vec3 face_forward(vec3 v, vec3 n) { + return dot(v, n) < 0 ? -v : v; +} + +// Creates a rotation mat, rotation should be 0 .. 2 * pi +mat2 make_rotation_mat(float rotation) { + float r_sin = sin(rotation); + float r_cos = cos(rotation); + return mat2(r_cos, -r_sin, r_sin, r_cos); +} + +// Convenience functions for the scattering plugin - probably don't belong here +#define get_sun_vector() sun_azimuth_to_angle(TimeOfDay.scattering.sun_azimuth, TimeOfDay.scattering.sun_altitude) +#define get_sun_color() (TimeOfDay.scattering.sun_color * TimeOfDay.scattering.sun_intensity) +#define get_sun_color_scale(_v) saturate(square((_v.z) / 0.15)) + +#if !HAVE_PLUGIN(color_correction) + #undef get_sun_color + #define get_sun_color() (TimeOfDay.scattering.sun_color * TimeOfDay.scattering.sun_intensity) +#endif diff --git a/RenderPipelineFile/rpcore/shader/includes/envprobes.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/envprobes.inc.glsl index 7796226a..a1eade2a 100644 --- a/RenderPipelineFile/rpcore/shader/includes/envprobes.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/envprobes.inc.glsl @@ -1,253 +1,253 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -#pragma include "includes/material.inc.glsl" -#pragma include "includes/brdf.inc.glsl" -#pragma include "includes/color_spaces.inc.glsl" -#pragma include "includes/light_culling.inc.glsl" - -// Global probe data -uniform struct { - int num_probes; - samplerCubeArray cubemaps; - samplerCubeArray diffuse_cubemaps; - samplerBuffer dataset; -} EnvProbes; - -// Per probe instance -struct Cubemap { - mat4 transform; - bool use_parallax; - float border_smoothness; - uint index; - - vec3 bounding_sphere_center; - float bounding_sphere_radius; -}; - -// Per cell data -#ifdef APPLY_ENVPROBES_PASS - uniform isampler2DArray CellIndices; - uniform isamplerBuffer PerCellProbes; -#endif - -Cubemap get_cubemap(int index) { - Cubemap result; - int offs = index * 5; - vec4 data0 = texelFetch(EnvProbes.dataset, offs); - vec4 data1 = texelFetch(EnvProbes.dataset, offs + 1); - vec4 data2 = texelFetch(EnvProbes.dataset, offs + 2); - vec4 data3 = texelFetch(EnvProbes.dataset, offs + 3); - vec4 data4 = texelFetch(EnvProbes.dataset, offs + 4); - - // Unpack the packed matrix, we only store a 3x4 matrix to save space, - // sine the last row is always (0, 0, 0, 1) - result.transform = mat4( - data0.x, data0.y, data0.z, 0, - data0.w, data1.x, data1.y, 0, - data1.z, data1.w, data2.x, 0, - data2.y, data2.z, data2.w, 1 - ); - result.index = uint(data3.x + 0.5); - result.use_parallax = data3.y > 0.5; - result.border_smoothness = data3.z; - result.bounding_sphere_center = data4.xyz; - result.bounding_sphere_radius = data4.w; - return result; -} - -// https://seblagarde.wordpress.com/2012/09/29/image-based-lighting-approaches-and-parallax-corrected-cubemap/ -float correct_parallax(Cubemap map, Material m, vec3 vector, out float factor) { - // Intersection with OBB, convert to unit box space - // Transform in local unit parallax cube space (scaled and rotated) - vec3 ray_ls = (map.transform * vec4(vector, 0)).xyz; - vec3 position_ls = (map.transform * vec4(m.position, 1)).xyz; - - // Get fading factor - vec3 local_v = abs(position_ls); - factor = max(local_v.x, max(local_v.y, local_v.z)); - - if (!map.use_parallax) { - return 1e10; - } - - // Intersect with unit box - vec3 first_plane = (1.0 - position_ls) / ray_ls; - vec3 second_plane = (-1.0 - position_ls) / ray_ls; - vec3 furthest_plane = max(first_plane, second_plane); - return min(furthest_plane.x, min(furthest_plane.y, furthest_plane.z)); -} - -vec3 get_cubemap_vector(Cubemap map, Material m, vec3 vector, out float factor, out float dist) { - dist = correct_parallax(map, m, vector, factor); - - // Use distance in world space directly to recover intersection. - // Mix parallax corrected and original vector based on roughness - vec3 intersection_pos = mix( - m.position + vector * dist, - map.bounding_sphere_center + vector, - m.roughness); - return (map.transform * vec4(intersection_pos, 1)).xyz; -} - -vec3 get_reflection_vector(Cubemap map, Material m, out float factor, out float dist) { - vec3 view_vector = normalize(m.position - MainSceneData.camera_pos); - vec3 reflected = get_reflection_vector(m, view_vector); - return get_cubemap_vector(map, m, reflected, factor, dist); -} - -vec3 get_diffuse_vector(Cubemap map, Material m) { - #if 0 - // very expensive, have to think of a better solution - - // maybe we can precompute the matrix and store it. - mat3 tpose_inverse = transpose(inverse(mat3(map.transform))); - return tpose_inverse * (m.normal); - #else - // This is mathematically wrong, but it works fast and reasonable - vec4 transformed = map.transform * vec4(fma(m.normal, vec3(1e5), - map.bounding_sphere_center), 1); - return transformed.xyz; - #endif -} - -void apply_cubemap(int id, Material m, out vec4 diffuse, out vec4 specular, - inout float total_weight, inout float total_blend) { - - const float max_mip = 7.0; - float roughness = get_effective_roughness(m); - - float factor = 0.0; - float mipmap = m.linear_roughness * 10.0; - float intersection_distance = 1.0; - - Cubemap map = get_cubemap(id); - - vec3 direction = get_reflection_vector(map, m, factor, intersection_distance); - vec3 diffuse_direction = get_diffuse_vector(map, m); - - vec3 vector_to_source = normalize(map.bounding_sphere_center - m.position); - - // float normal_blend_factor = saturate(0.1 + 1 * dot(vector_to_source, m.normal)); - float normal_blend_factor = saturate(3.0 * dot(vector_to_source, m.normal)); - float blend = saturate((1 - factor) / max(1e-10, map.border_smoothness)); - blend *= normal_blend_factor; - - // Make sure the gradient looks right after tonemapping - // blend = square(blend); - - float local_distance = intersection_distance / map.bounding_sphere_radius; - - #if 0 - if (map.use_parallax) { - // mipmap *= intersection_distance * 0.012; - } else { - mipmap *= 0.1; - } - #else - // mipmap *= 0.1; - #endif - - - specular = textureLod(EnvProbes.cubemaps, - vec4(direction, map.index), clamp(mipmap, 0.0, max_mip)); - diffuse = textureLod(EnvProbes.diffuse_cubemaps, - vec4(diffuse_direction, map.index), 0); - - // Optional: Correct specular based on diffuse scolor intensity - // specular.xyz = mix(specular.xyz, specular.xyz * get_luminance(diffuse.xyz), diffuse.w); - - // Make sure small probes contribute much more than large ones - float weight = exp(-0.05 * map.bounding_sphere_radius); - weight *= factor >= 1.0 ? 0.0 : 1.0; - - // Apply clip factors - specular *= weight * blend; - diffuse *= weight * blend; - - total_weight += weight * blend; - total_blend += blend; -} - - -#ifdef APPLY_ENVPROBES_PASS - void apply_all_probes(Material m, out vec4 specular, out vec4 diffuse) { - ivec3 tile = get_lc_cell_index( - ivec2(gl_FragCoord.xy), - distance(MainSceneData.camera_pos, m.position)); - - // Don't shade pixels out of the shading range - if (tile.z >= LC_TILE_SLICES) { - specular = vec4(0); - diffuse = vec4(0); - return; - } - - int cell_index = texelFetch(CellIndices, tile, 0).x; - int data_offs = cell_index * MAX_PROBES_PER_CELL; - - vec4 total_diffuse = vec4(0); - vec4 total_specular = vec4(0); - float total_blend = 0; - float total_weight = 0; - - int processed_probes = 0; - for (int i = 0; i < MAX_PROBES_PER_CELL; ++i) { - int cubemap_index = texelFetch(PerCellProbes, data_offs + i).x - 1; - if (cubemap_index < 0) break; - vec4 diff, spec; - - processed_probes += 1; - apply_cubemap(cubemap_index, m, diff, spec, total_weight, total_blend); - total_diffuse += diff; - total_specular += spec; - } - - float scale = 1.0 / max(1e-9, total_weight) * min(1.0, total_blend); - - vec4 result_spec = total_specular * scale; - vec4 result_diff = total_diffuse * scale; - - // Fade out cubemaps as they reach the culling distance - float curr_dist = distance(m.position, MainSceneData.camera_pos); - float fade = saturate(curr_dist / LC_MAX_DISTANCE); - fade = 1 - pow(fade, 5.0); - - result_spec *= fade; - result_diff *= fade; - - // Visualize probe count - #if MODE_ACTIVE(ENVPROBE_COUNT) - float probe_factor = float(processed_probes) / MAX_PROBES_PER_CELL; - result_spec = result_diff = vec4(probe_factor, 1 - probe_factor, 0, 1); - #endif - - specular = result_spec; - diffuse = result_diff; - } - -#endif +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +#pragma include "includes/material.inc.glsl" +#pragma include "includes/brdf.inc.glsl" +#pragma include "includes/color_spaces.inc.glsl" +#pragma include "includes/light_culling.inc.glsl" + +// Global probe data +uniform struct { + int num_probes; + samplerCubeArray cubemaps; + samplerCubeArray diffuse_cubemaps; + samplerBuffer dataset; +} EnvProbes; + +// Per probe instance +struct Cubemap { + mat4 transform; + bool use_parallax; + float border_smoothness; + uint index; + + vec3 bounding_sphere_center; + float bounding_sphere_radius; +}; + +// Per cell data +#ifdef APPLY_ENVPROBES_PASS + uniform isampler2DArray CellIndices; + uniform isamplerBuffer PerCellProbes; +#endif + +Cubemap get_cubemap(int index) { + Cubemap result; + int offs = index * 5; + vec4 data0 = texelFetch(EnvProbes.dataset, offs); + vec4 data1 = texelFetch(EnvProbes.dataset, offs + 1); + vec4 data2 = texelFetch(EnvProbes.dataset, offs + 2); + vec4 data3 = texelFetch(EnvProbes.dataset, offs + 3); + vec4 data4 = texelFetch(EnvProbes.dataset, offs + 4); + + // Unpack the packed matrix, we only store a 3x4 matrix to save space, + // sine the last row is always (0, 0, 0, 1) + result.transform = mat4( + data0.x, data0.y, data0.z, 0, + data0.w, data1.x, data1.y, 0, + data1.z, data1.w, data2.x, 0, + data2.y, data2.z, data2.w, 1 + ); + result.index = uint(data3.x + 0.5); + result.use_parallax = data3.y > 0.5; + result.border_smoothness = data3.z; + result.bounding_sphere_center = data4.xyz; + result.bounding_sphere_radius = data4.w; + return result; +} + +// https://seblagarde.wordpress.com/2012/09/29/image-based-lighting-approaches-and-parallax-corrected-cubemap/ +float correct_parallax(Cubemap map, Material m, vec3 vector, out float factor) { + // Intersection with OBB, convert to unit box space + // Transform in local unit parallax cube space (scaled and rotated) + vec3 ray_ls = (map.transform * vec4(vector, 0)).xyz; + vec3 position_ls = (map.transform * vec4(m.position, 1)).xyz; + + // Get fading factor + vec3 local_v = abs(position_ls); + factor = max(local_v.x, max(local_v.y, local_v.z)); + + if (!map.use_parallax) { + return 1e10; + } + + // Intersect with unit box + vec3 first_plane = (1.0 - position_ls) / ray_ls; + vec3 second_plane = (-1.0 - position_ls) / ray_ls; + vec3 furthest_plane = max(first_plane, second_plane); + return min(furthest_plane.x, min(furthest_plane.y, furthest_plane.z)); +} + +vec3 get_cubemap_vector(Cubemap map, Material m, vec3 vector, out float factor, out float dist) { + dist = correct_parallax(map, m, vector, factor); + + // Use distance in world space directly to recover intersection. + // Mix parallax corrected and original vector based on roughness + vec3 intersection_pos = mix( + m.position + vector * dist, + map.bounding_sphere_center + vector, + m.roughness); + return (map.transform * vec4(intersection_pos, 1)).xyz; +} + +vec3 get_reflection_vector(Cubemap map, Material m, out float factor, out float dist) { + vec3 view_vector = normalize(m.position - MainSceneData.camera_pos); + vec3 reflected = get_reflection_vector(m, view_vector); + return get_cubemap_vector(map, m, reflected, factor, dist); +} + +vec3 get_diffuse_vector(Cubemap map, Material m) { + #if 0 + // very expensive, have to think of a better solution - + // maybe we can precompute the matrix and store it. + mat3 tpose_inverse = transpose(inverse(mat3(map.transform))); + return tpose_inverse * (m.normal); + #else + // This is mathematically wrong, but it works fast and reasonable + vec4 transformed = map.transform * vec4(fma(m.normal, vec3(1e5), + map.bounding_sphere_center), 1); + return transformed.xyz; + #endif +} + +void apply_cubemap(int id, Material m, out vec4 diffuse, out vec4 specular, + inout float total_weight, inout float total_blend) { + + const float max_mip = 7.0; + float roughness = get_effective_roughness(m); + + float factor = 0.0; + float mipmap = m.linear_roughness * 10.0; + float intersection_distance = 1.0; + + Cubemap map = get_cubemap(id); + + vec3 direction = get_reflection_vector(map, m, factor, intersection_distance); + vec3 diffuse_direction = get_diffuse_vector(map, m); + + vec3 vector_to_source = normalize(map.bounding_sphere_center - m.position); + + // float normal_blend_factor = saturate(0.1 + 1 * dot(vector_to_source, m.normal)); + float normal_blend_factor = saturate(3.0 * dot(vector_to_source, m.normal)); + float blend = saturate((1 - factor) / max(1e-10, map.border_smoothness)); + blend *= normal_blend_factor; + + // Make sure the gradient looks right after tonemapping + // blend = square(blend); + + float local_distance = intersection_distance / map.bounding_sphere_radius; + + #if 0 + if (map.use_parallax) { + // mipmap *= intersection_distance * 0.012; + } else { + mipmap *= 0.1; + } + #else + // mipmap *= 0.1; + #endif + + + specular = textureLod(EnvProbes.cubemaps, + vec4(direction, map.index), clamp(mipmap, 0.0, max_mip)); + diffuse = textureLod(EnvProbes.diffuse_cubemaps, + vec4(diffuse_direction, map.index), 0); + + // Optional: Correct specular based on diffuse scolor intensity + // specular.xyz = mix(specular.xyz, specular.xyz * get_luminance(diffuse.xyz), diffuse.w); + + // Make sure small probes contribute much more than large ones + float weight = exp(-0.05 * map.bounding_sphere_radius); + weight *= factor >= 1.0 ? 0.0 : 1.0; + + // Apply clip factors + specular *= weight * blend; + diffuse *= weight * blend; + + total_weight += weight * blend; + total_blend += blend; +} + + +#ifdef APPLY_ENVPROBES_PASS + void apply_all_probes(Material m, out vec4 specular, out vec4 diffuse) { + ivec3 tile = get_lc_cell_index( + ivec2(gl_FragCoord.xy), + distance(MainSceneData.camera_pos, m.position)); + + // Don't shade pixels out of the shading range + if (tile.z >= LC_TILE_SLICES) { + specular = vec4(0); + diffuse = vec4(0); + return; + } + + int cell_index = texelFetch(CellIndices, tile, 0).x; + int data_offs = cell_index * MAX_PROBES_PER_CELL; + + vec4 total_diffuse = vec4(0); + vec4 total_specular = vec4(0); + float total_blend = 0; + float total_weight = 0; + + int processed_probes = 0; + for (int i = 0; i < MAX_PROBES_PER_CELL; ++i) { + int cubemap_index = texelFetch(PerCellProbes, data_offs + i).x - 1; + if (cubemap_index < 0) break; + vec4 diff, spec; + + processed_probes += 1; + apply_cubemap(cubemap_index, m, diff, spec, total_weight, total_blend); + total_diffuse += diff; + total_specular += spec; + } + + float scale = 1.0 / max(1e-9, total_weight) * min(1.0, total_blend); + + vec4 result_spec = total_specular * scale; + vec4 result_diff = total_diffuse * scale; + + // Fade out cubemaps as they reach the culling distance + float curr_dist = distance(m.position, MainSceneData.camera_pos); + float fade = saturate(curr_dist / LC_MAX_DISTANCE); + fade = 1 - pow(fade, 5.0); + + result_spec *= fade; + result_diff *= fade; + + // Visualize probe count + #if MODE_ACTIVE(ENVPROBE_COUNT) + float probe_factor = float(processed_probes) / MAX_PROBES_PER_CELL; + result_spec = result_diff = vec4(probe_factor, 1 - probe_factor, 0, 1); + #endif + + specular = result_spec; + diffuse = result_diff; + } + +#endif diff --git a/RenderPipelineFile/rpcore/shader/includes/forward_shading.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/forward_shading.inc.glsl index bde551f5..6b266747 100644 --- a/RenderPipelineFile/rpcore/shader/includes/forward_shading.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/forward_shading.inc.glsl @@ -1,323 +1,323 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -// Forward shading pipeline - -#pragma include "includes/shadows.inc.glsl" -#pragma include "includes/brdf.inc.glsl" -#pragma include "includes/lights.inc.glsl" -#pragma include "includes/light_data.inc.glsl" -#pragma include "includes/poisson_disk.inc.glsl" - -uniform samplerCube DefaultEnvmap; -uniform samplerBuffer AllLightsData; -uniform int maxLightIndex; - -#if VIEWSPACE_SHADING - uniform sampler3D PrefilteredBRDF; - uniform sampler2D PrefilteredCoatBRDF; -#endif - -// Samplers for the specular cubemaps -#if HAVE_PLUGIN(scattering) - uniform samplerCube ScatteringIBLDiffuse; - uniform samplerCube ScatteringIBLSpecular; -#endif - - -#if HAVE_PLUGIN(pssm) - uniform sampler2DShadow PSSMSceneSunShadowMapPCF; - uniform mat4 PSSMSceneSunShadowMVP; -#endif - -#if HAVE_PLUGIN(sky_ao) - #pragma include "/$$rp/rpplugins/sky_ao/shader/sky_ao.inc.glsl" -#endif - -#if VIEWSPACE_SHADING - - // Also include environment probes - - #if HAVE_PLUGIN(env_probes) - #define APPLY_ENVPROBES_PASS - #pragma include "includes/light_culling.inc.glsl" - #pragma include "includes/envprobes.inc.glsl" - #endif - - struct AmbientResult { - vec3 specular; - vec3 diffuse; - float fresnel; - }; - - // Full forward ambient, expensive, and only available when shading in view-space - AmbientResult get_full_forward_ambient(Material m, vec3 v) { - vec3 reflected_dir = get_reflection_vector(m, v); - float roughness = get_effective_roughness(m); - - // Compute angle between normal and view vector - float NxV = clamp(-dot(m.normal, v), 1e-5, 1.0); - - float env_mipmap = get_mipmap_for_roughness(DefaultEnvmap, roughness , NxV); - - // Sample default environment map - vec3 ibl_specular = textureLod(DefaultEnvmap, fix_cubemap_coord(reflected_dir), - env_mipmap).xyz * DEFAULT_ENVMAP_BRIGHTNESS; - - // Get cheap irradiance by sampling low levels of the environment map - float ibl_diffuse_mip = get_mipmap_count(DefaultEnvmap) - 3.0; - vec3 ibl_diffuse = textureLod(DefaultEnvmap, fix_cubemap_coord(m.normal), - ibl_diffuse_mip).xyz * DEFAULT_ENVMAP_BRIGHTNESS; - - // Scattering specific code - #if HAVE_PLUGIN(scattering) - float scat_mipmap = m.shading_model == SHADING_MODEL_CLEARCOAT ? - CLEARCOAT_ROUGHNESS : get_specular_mipmap(m); - - // Sample prefiltered scattering cubemap - // ibl_specular = textureLod(ScatteringIBLSpecular, reflected_dir, scat_mipmap).xyz; - - // Diffuse IBL - // ibl_diffuse = textureLod(ScatteringIBLDiffuse, m.normal, 0).xyz; - #endif - - #if HAVE_PLUGIN(env_probes) - // vec4 probe_spec, probe_diff; - // apply_all_probes(m, probe_spec, probe_diff); - - // ibl_diffuse = ibl_diffuse * (1 - probe_diff.w) + probe_diff.xyz; - // ibl_specular = ibl_specular * (1 - probe_spec.w) + probe_spec.xyz; - - #endif - - vec3 env_brdf = get_brdf_from_lut(PrefilteredBRDF, NxV, sqrt(roughness), m.specular_ior); - - // Diffuse and fresnel ambient term - vec3 diffuse_ambient = ibl_diffuse * m.basecolor * (1-m.metallic); - vec3 fresnel = env_brdf.ggg; - diffuse_ambient *= env_brdf.r; - - vec3 metallic_fresnel = get_metallic_fresnel_approx(m, NxV); - - // Mix between normal and metallic fresnel - fresnel = mix(fresnel, metallic_fresnel, m.metallic); - - // XXX: Clearcoat not supported here - vec3 specular_ambient = fresnel * ibl_specular; - - AmbientResult ret; - ret.specular = specular_ambient; - ret.diffuse = diffuse_ambient; - ret.fresnel = env_brdf.g; - return ret; - } - -#else - - // cheap forward shaded ambient - vec3 get_forward_ambient(Material m) { - vec3 shading_result = vec3(0); - - #if HAVE_PLUGIN(scattering) - vec3 diff_env = textureLod(ScatteringIBLDiffuse, m.normal, 0).rgb; - #else - int ibl_diffuse_mip = get_mipmap_count(DefaultEnvmap) - 5; - vec3 diff_env = textureLod(DefaultEnvmap, m.normal, ibl_diffuse_mip).rgb * - DEFAULT_ENVMAP_BRIGHTNESS; - #endif - - #if HAVE_PLUGIN(env_probes) - const float sky_ambient_factor = TimeOfDay.env_probes.sky_ambient_scale; - const float ambient_factor = TimeOfDay.env_probes.ambient_scale; - #else - const float sky_ambient_factor = 0.05; - const float ambient_factor = 0.1; - #endif - - #if HAVE_PLUGIN(sky_ao) - float sky_ao = compute_sky_ao(m.position, m.normal); - #else - float sky_ao = 1.0; - #endif - shading_result += (sky_ambient_factor + ambient_factor * sqrt(m.basecolor)) * sky_ao * diff_env; - - - - // Emission - if (m.shading_model == SHADING_MODEL_EMISSIVE) { - shading_result = m.basecolor * (0.005 + diff_env) * 30.0; - } - - return shading_result; - } - -#endif - - -float get_sun_shadow_factor(vec3 position, vec3 normal) { - #if !HAVE_PLUGIN(scattering) || !HAVE_PLUGIN(pssm) - return 1.0; - #else - vec3 sun_vector = get_sun_vector(); - - vec3 biased_position = position; - - // XXX: make this configurable - const float slope_bias = 0.3 * 0.02; - const float normal_bias = 0.0 * 0.005; - const float fixed_bias = 0.5 * 0.001; - vec3 biased_pos = get_biased_position( - position, slope_bias, normal_bias, vOutput.normal, sun_vector); - - vec3 projected = project(PSSMSceneSunShadowMVP, biased_position); - projected.z -= fixed_bias; - - // Fast shadow filtering - float filter_radius = 1.0 / textureSize(PSSMSceneSunShadowMapPCF, 0).y; - float shadow_term = 0; - for(uint i = 0; i < 12; ++i) { - vec3 offset = vec3(poisson_2D_12[i] * filter_radius, 0); - shadow_term += textureLod(PSSMSceneSunShadowMapPCF, projected + offset, 0).x; - } - shadow_term /= 12.0; - return shadow_term; - #endif -} - -#if VIEWSPACE_SHADING - - vec3 get_sun_shading(Material m, vec3 view_vector) { - #if HAVE_PLUGIN(scattering) - - // Compute the sun lighting - vec3 sun_vector = get_sun_vector(); - vec3 l = sun_vector; - vec3 sun_color = get_sun_color() * get_sun_color_scale(sun_vector); - - { - vec3 reflected_dir = reflect(view_vector, m.normal); - const float sun_angular_radius = degree_to_radians(0.54); - const float r = sin(sun_angular_radius); // Disk radius - const float d = cos(sun_angular_radius); // Distance to disk - - // Closest point to a disk (since the radius is small, this is - // a good approximation) - float DdotR = dot(sun_vector, reflected_dir); - vec3 S = reflected_dir - DdotR * sun_vector; - l = DdotR < d ? normalize(d * sun_vector + normalize(S) * r) : reflected_dir; - } - - // XXX: Need a shadow map. Maybe reuse pssm but thats super expensive - // float shadow = get_sun_shadow_factor(m.position, m.normal); - - float shadow = 1.0; - return apply_light(m, view_vector, l, sun_color, 1.0, shadow, vec3(1)); - - - #else - return vec3(0); - #endif - } - - -#else - - // Applies the sun shading, and if the pssm plugin is activated, also the - // sun shadows - vec3 get_sun_shading(Material m) { - #if HAVE_PLUGIN(scattering) - float shadow = 0.0; - vec3 shading_result = vec3(0); - vec3 sun_vector = get_sun_vector(); - vec3 sun_color = get_sun_color() * get_sun_color_scale(sun_vector); - - if (sun_vector.z >= SUN_VECTOR_HORIZON) { - float NxL = saturate(dot(sun_vector, m.normal)); - if (NxL > 0.0) { - shadow = NxL * get_sun_shadow_factor(m.position, m.normal); - } - } - #if HAVE_PLUGIN(env_probes) - const float factor = TimeOfDay.env_probes.sun_ambient_scale; - #else - const float factor = 1.0; - #endif - return shadow * sun_color * factor * m.basecolor; - #else - return vec3(0); - #endif - } -#endif - - - -vec3 get_forward_light_shading(Material m) { - - vec3 shading_result = vec3(0); - - for (int i = 0; i <= maxLightIndex; ++i) { - LightData light_data = read_light_data(AllLightsData, i); - int light_type = get_light_type(light_data); - - // Skip Null-Lights - if (light_type < 1) continue; - - vec3 light_pos = get_light_position(light_data); - vec3 l = light_pos - m.position; - float l_len = length(l); - vec3 light_color = get_light_color(light_data); - - // Shade depending on light type - switch(light_type) { - case LT_POINT_LIGHT: { - float radius = get_pointlight_radius(light_data); - float inner_radius = get_pointlight_inner_radius(light_data); - float att = attenuation_curve(dot(l, l), radius); - float NxL = saturate(dot(m.normal, l) / l_len); - shading_result += saturate(att) * NxL * (m.basecolor * light_color); - break; - } - - case LT_SPOT_LIGHT: { - float radius = get_spotlight_radius(light_data); - float fov = get_spotlight_fov(light_data); - vec3 direction = get_spotlight_direction(light_data); - - float att = get_spotlight_attenuation(l / l_len, direction, - fov, radius, dot(l, l), -1); - float NxL = saturate(dot(m.normal, l) / l_len); - shading_result += saturate(att) * NxL * (m.basecolor * light_color); - break; - } - - } - - } - - return shading_result / M_PI; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +// Forward shading pipeline + +#pragma include "includes/shadows.inc.glsl" +#pragma include "includes/brdf.inc.glsl" +#pragma include "includes/lights.inc.glsl" +#pragma include "includes/light_data.inc.glsl" +#pragma include "includes/poisson_disk.inc.glsl" + +uniform samplerCube DefaultEnvmap; +uniform samplerBuffer AllLightsData; +uniform int maxLightIndex; + +#if VIEWSPACE_SHADING + uniform sampler3D PrefilteredBRDF; + uniform sampler2D PrefilteredCoatBRDF; +#endif + +// Samplers for the specular cubemaps +#if HAVE_PLUGIN(scattering) + uniform samplerCube ScatteringIBLDiffuse; + uniform samplerCube ScatteringIBLSpecular; +#endif + + +#if HAVE_PLUGIN(pssm) + uniform sampler2DShadow PSSMSceneSunShadowMapPCF; + uniform mat4 PSSMSceneSunShadowMVP; +#endif + +#if HAVE_PLUGIN(sky_ao) + #pragma include "/$$rp/rpplugins/sky_ao/shader/sky_ao.inc.glsl" +#endif + +#if VIEWSPACE_SHADING + + // Also include environment probes + + #if HAVE_PLUGIN(env_probes) + #define APPLY_ENVPROBES_PASS + #pragma include "includes/light_culling.inc.glsl" + #pragma include "includes/envprobes.inc.glsl" + #endif + + struct AmbientResult { + vec3 specular; + vec3 diffuse; + float fresnel; + }; + + // Full forward ambient, expensive, and only available when shading in view-space + AmbientResult get_full_forward_ambient(Material m, vec3 v) { + vec3 reflected_dir = get_reflection_vector(m, v); + float roughness = get_effective_roughness(m); + + // Compute angle between normal and view vector + float NxV = clamp(-dot(m.normal, v), 1e-5, 1.0); + + float env_mipmap = get_mipmap_for_roughness(DefaultEnvmap, roughness , NxV); + + // Sample default environment map + vec3 ibl_specular = textureLod(DefaultEnvmap, fix_cubemap_coord(reflected_dir), + env_mipmap).xyz * DEFAULT_ENVMAP_BRIGHTNESS; + + // Get cheap irradiance by sampling low levels of the environment map + float ibl_diffuse_mip = get_mipmap_count(DefaultEnvmap) - 3.0; + vec3 ibl_diffuse = textureLod(DefaultEnvmap, fix_cubemap_coord(m.normal), + ibl_diffuse_mip).xyz * DEFAULT_ENVMAP_BRIGHTNESS; + + // Scattering specific code + #if HAVE_PLUGIN(scattering) + float scat_mipmap = m.shading_model == SHADING_MODEL_CLEARCOAT ? + CLEARCOAT_ROUGHNESS : get_specular_mipmap(m); + + // Sample prefiltered scattering cubemap + // ibl_specular = textureLod(ScatteringIBLSpecular, reflected_dir, scat_mipmap).xyz; + + // Diffuse IBL + // ibl_diffuse = textureLod(ScatteringIBLDiffuse, m.normal, 0).xyz; + #endif + + #if HAVE_PLUGIN(env_probes) + // vec4 probe_spec, probe_diff; + // apply_all_probes(m, probe_spec, probe_diff); + + // ibl_diffuse = ibl_diffuse * (1 - probe_diff.w) + probe_diff.xyz; + // ibl_specular = ibl_specular * (1 - probe_spec.w) + probe_spec.xyz; + + #endif + + vec3 env_brdf = get_brdf_from_lut(PrefilteredBRDF, NxV, sqrt(roughness), m.specular_ior); + + // Diffuse and fresnel ambient term + vec3 diffuse_ambient = ibl_diffuse * m.basecolor * (1-m.metallic); + vec3 fresnel = env_brdf.ggg; + diffuse_ambient *= env_brdf.r; + + vec3 metallic_fresnel = get_metallic_fresnel_approx(m, NxV); + + // Mix between normal and metallic fresnel + fresnel = mix(fresnel, metallic_fresnel, m.metallic); + + // XXX: Clearcoat not supported here + vec3 specular_ambient = fresnel * ibl_specular; + + AmbientResult ret; + ret.specular = specular_ambient; + ret.diffuse = diffuse_ambient; + ret.fresnel = env_brdf.g; + return ret; + } + +#else + + // cheap forward shaded ambient + vec3 get_forward_ambient(Material m) { + vec3 shading_result = vec3(0); + + #if HAVE_PLUGIN(scattering) + vec3 diff_env = textureLod(ScatteringIBLDiffuse, m.normal, 0).rgb; + #else + int ibl_diffuse_mip = get_mipmap_count(DefaultEnvmap) - 5; + vec3 diff_env = textureLod(DefaultEnvmap, m.normal, ibl_diffuse_mip).rgb * + DEFAULT_ENVMAP_BRIGHTNESS; + #endif + + #if HAVE_PLUGIN(env_probes) + const float sky_ambient_factor = TimeOfDay.env_probes.sky_ambient_scale; + const float ambient_factor = TimeOfDay.env_probes.ambient_scale; + #else + const float sky_ambient_factor = 0.05; + const float ambient_factor = 0.1; + #endif + + #if HAVE_PLUGIN(sky_ao) + float sky_ao = compute_sky_ao(m.position, m.normal); + #else + float sky_ao = 1.0; + #endif + shading_result += (sky_ambient_factor + ambient_factor * sqrt(m.basecolor)) * sky_ao * diff_env; + + + + // Emission + if (m.shading_model == SHADING_MODEL_EMISSIVE) { + shading_result = m.basecolor * (0.005 + diff_env) * 30.0; + } + + return shading_result; + } + +#endif + + +float get_sun_shadow_factor(vec3 position, vec3 normal) { + #if !HAVE_PLUGIN(scattering) || !HAVE_PLUGIN(pssm) + return 1.0; + #else + vec3 sun_vector = get_sun_vector(); + + vec3 biased_position = position; + + // XXX: make this configurable + const float slope_bias = 0.3 * 0.02; + const float normal_bias = 0.0 * 0.005; + const float fixed_bias = 0.5 * 0.001; + vec3 biased_pos = get_biased_position( + position, slope_bias, normal_bias, vOutput.normal, sun_vector); + + vec3 projected = project(PSSMSceneSunShadowMVP, biased_position); + projected.z -= fixed_bias; + + // Fast shadow filtering + float filter_radius = 1.0 / textureSize(PSSMSceneSunShadowMapPCF, 0).y; + float shadow_term = 0; + for(uint i = 0; i < 12; ++i) { + vec3 offset = vec3(poisson_2D_12[i] * filter_radius, 0); + shadow_term += textureLod(PSSMSceneSunShadowMapPCF, projected + offset, 0).x; + } + shadow_term /= 12.0; + return shadow_term; + #endif +} + +#if VIEWSPACE_SHADING + + vec3 get_sun_shading(Material m, vec3 view_vector) { + #if HAVE_PLUGIN(scattering) + + // Compute the sun lighting + vec3 sun_vector = get_sun_vector(); + vec3 l = sun_vector; + vec3 sun_color = get_sun_color() * get_sun_color_scale(sun_vector); + + { + vec3 reflected_dir = reflect(view_vector, m.normal); + const float sun_angular_radius = degree_to_radians(0.54); + const float r = sin(sun_angular_radius); // Disk radius + const float d = cos(sun_angular_radius); // Distance to disk + + // Closest point to a disk (since the radius is small, this is + // a good approximation) + float DdotR = dot(sun_vector, reflected_dir); + vec3 S = reflected_dir - DdotR * sun_vector; + l = DdotR < d ? normalize(d * sun_vector + normalize(S) * r) : reflected_dir; + } + + // XXX: Need a shadow map. Maybe reuse pssm but thats super expensive + // float shadow = get_sun_shadow_factor(m.position, m.normal); + + float shadow = 1.0; + return apply_light(m, view_vector, l, sun_color, 1.0, shadow, vec3(1)); + + + #else + return vec3(0); + #endif + } + + +#else + + // Applies the sun shading, and if the pssm plugin is activated, also the + // sun shadows + vec3 get_sun_shading(Material m) { + #if HAVE_PLUGIN(scattering) + float shadow = 0.0; + vec3 shading_result = vec3(0); + vec3 sun_vector = get_sun_vector(); + vec3 sun_color = get_sun_color() * get_sun_color_scale(sun_vector); + + if (sun_vector.z >= SUN_VECTOR_HORIZON) { + float NxL = saturate(dot(sun_vector, m.normal)); + if (NxL > 0.0) { + shadow = NxL * get_sun_shadow_factor(m.position, m.normal); + } + } + #if HAVE_PLUGIN(env_probes) + const float factor = TimeOfDay.env_probes.sun_ambient_scale; + #else + const float factor = 1.0; + #endif + return shadow * sun_color * factor * m.basecolor; + #else + return vec3(0); + #endif + } +#endif + + + +vec3 get_forward_light_shading(Material m) { + + vec3 shading_result = vec3(0); + + for (int i = 0; i <= maxLightIndex; ++i) { + LightData light_data = read_light_data(AllLightsData, i); + int light_type = get_light_type(light_data); + + // Skip Null-Lights + if (light_type < 1) continue; + + vec3 light_pos = get_light_position(light_data); + vec3 l = light_pos - m.position; + float l_len = length(l); + vec3 light_color = get_light_color(light_data); + + // Shade depending on light type + switch(light_type) { + case LT_POINT_LIGHT: { + float radius = get_pointlight_radius(light_data); + float inner_radius = get_pointlight_inner_radius(light_data); + float att = attenuation_curve(dot(l, l), radius); + float NxL = saturate(dot(m.normal, l) / l_len); + shading_result += saturate(att) * NxL * (m.basecolor * light_color); + break; + } + + case LT_SPOT_LIGHT: { + float radius = get_spotlight_radius(light_data); + float fov = get_spotlight_fov(light_data); + vec3 direction = get_spotlight_direction(light_data); + + float att = get_spotlight_attenuation(l / l_len, direction, + fov, radius, dot(l, l), -1); + float NxL = saturate(dot(m.normal, l) / l_len); + shading_result += saturate(att) * NxL * (m.basecolor * light_color); + break; + } + + } + + } + + return shading_result / M_PI; +} diff --git a/RenderPipelineFile/rpcore/shader/includes/gaussian_weights.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/gaussian_weights.inc.glsl index 2900e9e3..c22eabdd 100644 --- a/RenderPipelineFile/rpcore/shader/includes/gaussian_weights.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/gaussian_weights.inc.glsl @@ -1,242 +1,242 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -// lint: skip-file - -#pragma once - - -// Autogenerated, do not edit! - -// python-script to generate the weights: -/* -from __future__ import print_function -SIGMA = 1.0 -file_handle = open("gaussian_weights.txt", "w") -import math -def gauss(x): - return math.exp( -(x**2) / (2.0 * (SIGMA**2) ) ) -for i in range(2, 40): - weights = [gauss(k / float(i-1) * 3.0) for k in range(0, i)] - wsum = weights[0] + 2 * sum(weights[1:]) - weights = [k / wsum for k in weights] - print("CONST_ARRAY float gaussian_weights_{0}[{0}] = float[{0}]({1});".format(i, ', '.join([str(i) for i in weights])), file=file_handle) -file_handle.close() - -*/ - -CONST_ARRAY float gaussian_weights_2[2] = float[2](0.97826491685, 0.0108675415748); -CONST_ARRAY float gaussian_weights_3[3] = float[3](0.598256825182, 0.194225554409, 0.00664603299992); -CONST_ARRAY float gaussian_weights_4[4] = float[4](0.399050279652, 0.242036229376, 0.0540055826224, 0.00443304817524); -CONST_ARRAY float gaussian_weights_5[5] = float[5](0.299372412212, 0.22597815248, 0.0971919922835, 0.0238179220394, 0.0033257270909); -CONST_ARRAY float gaussian_weights_6[6] = float[6](0.239559410861, 0.200096839755, 0.116606083673, 0.0474084957625, 0.0134476107133, 0.00266126466595); -CONST_ARRAY float gaussian_weights_7[7] = float[7](0.199675627498, 0.176213122789, 0.121109390075, 0.0648251851385, 0.0270231576029, 0.00877313479159, 0.00221819585465); -CONST_ARRAY float gaussian_weights_8[8] = float[8](0.171180679737, 0.156160272964, 0.118554487683, 0.0749026292285, 0.0393829054464, 0.0172325706915, 0.00627514853941, 0.00190164557861); -CONST_ARRAY float gaussian_weights_9[9] = float[9](0.149804857814, 0.139633481336, 0.113078639248, 0.0795607594227, 0.0486345167116, 0.0258295968515, 0.0119184008913, 0.00477799498463, 0.00166418164687); -CONST_ARRAY float gaussian_weights_10[10] = float[10](0.133175996016, 0.125979094462, 0.10663900118, 0.0807753247212, 0.0547502887625, 0.0332077287626, 0.0180234111411, 0.00875346226514, 0.00380423901867, 0.00147945167871); -CONST_ARRAY float gaussian_weights_11[11] = float[11](0.119870627342, 0.114596017885, 0.100124364242, 0.0799509287402, 0.0583472982821, 0.0389162949304, 0.0237222412093, 0.013215799633, 0.00672890923663, 0.00313118978625, 0.00133164238418); -CONST_ARRAY float gaussian_weights_12[12] = float[12](0.108982774698, 0.105004137779, 0.0939186994792, 0.0779820830369, 0.0601083349811, 0.043010220493, 0.0285697042449, 0.0176172016263, 0.0100847516981, 0.00535908443022, 0.00264370561491, 0.00121068926685); -CONST_ARRAY float gaussian_weights_13[13] = float[13](0.099908358105, 0.0968345010773, 0.0881688165699, 0.0754147852673, 0.0605974823522, 0.0457413794603, 0.0324354949685, 0.0216066977333, 0.0135211259418, 0.00794865988767, 0.00438966689848, 0.00227732918643, 0.00110988160433); -CONST_ARRAY float gaussian_weights_14[14] = float[14](0.0922291029445, 0.0898057057742, 0.0829109263623, 0.0725757439402, 0.060234195822, 0.0473987215842, 0.0353640528944, 0.0250166600045, 0.0167790942071, 0.0106703727296, 0.0064337259728, 0.00367804823216, 0.00199362821896, 0.00102457278534); -CONST_ARRAY float gaussian_weights_15[15] = float[15](0.0856461968131, 0.0837022313712, 0.0781310922076, 0.0696576247716, 0.0593159286476, 0.0482427341138, 0.0374757556435, 0.0278052491152, 0.0197043035237, 0.0133368508235, 0.00862190840291, 0.00532367362189, 0.00313962187592, 0.00176848417109, 0.00095144330391); -CONST_ARRAY float gaussian_weights_16[16] = float[16](0.0799404796215, 0.0783575520685, 0.0737943634766, 0.0667719013138, 0.0580487023004, 0.0484863518426, 0.0389112087983, 0.0300025492495, 0.0222264351509, 0.0158201169212, 0.0108187674517, 0.00710843674843, 0.0044874398644, 0.00272176986138, 0.00158610663008, 0.000888058511381); -CONST_ARRAY float gaussian_weights_17[17] = float[17](0.0749475595843, 0.0736416335058, 0.0698588070848, 0.0639809599576, 0.0565733860467, 0.0482953623493, 0.0398043484331, 0.0316728718988, 0.0243319101415, 0.0180466584178, 0.0129225799304, 0.0089337437364, 0.00596279102016, 0.00384235530188, 0.00239043692594, 0.00143578327773, 0.000832592179972); -CONST_ARRAY float gaussian_weights_18[18] = float[18](0.0705417098914, 0.069451816911, 0.0662821365674, 0.0613175289906, 0.0549854836326, 0.047795467276, 0.0402717621111, 0.03289196444, 0.0260407956595, 0.0199845303108, 0.0148665056404, 0.0107201068137, 0.00749315271104, 0.00507697836848, 0.00333442766635, 0.00212281695053, 0.0013100233939, 0.000783647610985); -CONST_ARRAY float gaussian_weights_19[19] = float[19](0.0666251318775, 0.0657061792116, 0.0630246743667, 0.0587964725162, 0.0533492350686, 0.0470805347033, 0.0404101851911, 0.0337346772335, 0.0273904105715, 0.0216300134521, 0.0166131237937, 0.0124102942088, 0.00901673109332, 0.006371657499, 0.00437917188719, 0.00292730399695, 0.00190318025692, 0.0012034506512, 0.000740138359388); -CONST_ARRAY float gaussian_weights_20[20] = float[20](0.0631206079092, 0.0623386696286, 0.0600504962028, 0.0564219880656, 0.0517074216853, 0.0462200163444, 0.0402976778174, 0.0342690969121, 0.0284248389542, 0.0229967296489, 0.0181470844488, 0.0139675538565, 0.0104859205472, 0.00767830721424, 0.00548399588371, 0.00382033519528, 0.00259584365953, 0.00172039554551, 0.00111211782068, 0.000701206614755); -CONST_ARRAY float gaussian_weights_21[21] = float[21](0.0599663578227, 0.0592955168529, 0.0573276870732, 0.0541920219987, 0.0500881123762, 0.0452649816716, 0.0399961700994, 0.0345543588197, 0.0291887599519, 0.024107758111, 0.0194682260256, 0.0153717852441, 0.0118672642019, 0.00895788098686, 0.00661132245055, 0.004770893968, 0.00336619727441, 0.00232224340187, 0.00156640581013, 0.00103306870905, 0.000666166061464); -CONST_ARRAY float gaussian_weights_22[22] = float[22](0.057112364552, 0.0565325485975, 0.0548281807346, 0.0521009873998, 0.0485092901538, 0.0442527977069, 0.0395542717215, 0.0346404001495, 0.0297241370428, 0.0249903568146, 0.0205860255043, 0.0166153465249, 0.0131396303393, 0.0101810773885, 0.00772931803295, 0.00574943890287, 0.004190314276, 0.0029922961728, 0.0020936274558, 0.0014352616483, 0.000964050097101, 0.000634461060099); -CONST_ARRAY float gaussian_weights_23[23] = float[23](0.054517701579, 0.0540131711974, 0.0525274224654, 0.0501414375277, 0.0469820267016, 0.0432106689856, 0.0390098705349, 0.034568640811, 0.030068680835, 0.0256726513374, 0.021515495198, 0.0176993063323, 0.0142917503664, 0.0113276252744, 0.00881285454125, 0.00673005489098, 0.00504481084372, 0.00371189295693, 0.00268083618271, 0.00190050661455, 0.0013224911384, 0.000903317516786, 0.000605636958114); -CONST_ARRAY float gaussian_weights_24[24] = float[24](0.0521485573767, 0.0517068310725, 0.050403975608, 0.0483050916261, 0.0455126658702, 0.042158280221, 0.0383923542943, 0.0343730326211, 0.0302553514918, 0.0261816975872, 0.0222743299567, 0.0186304241426, 0.0153197642695, 0.0123849047314, 0.00984338574556, 0.00769143875473, 0.00590856360058, 0.00446238965579, 0.00331332710032, 0.00241864641078, 0.00173576808421, 0.00122467897537, 0.00084950134862, 0.000579318143372); -CONST_ARRAY float gaussian_weights_25[25] = float[25](0.0499767536015, 0.0495878314194, 0.0484391305418, 0.046583456592, 0.0441043302545, 0.0411097561541, 0.0377244327973, 0.0340811848931, 0.0303124333322, 0.0265424534827, 0.0228810251118, 0.0194188737985, 0.0162250763673, 0.0133463837679, 0.0108082309552, 0.00861707301291, 0.0067636181039, 0.00522651897931, 0.00397612596387, 0.00297798074754, 0.0021958253057, 0.00159399822903, 0.00113917916157, 0.000801512644787, 0.000555191582752); -CONST_ARRAY float gaussian_weights_26[26] = float[26](0.0479786188192, 0.0476344133902, 0.0466165426396, 0.0449681953731, 0.0427579639989, 0.0400751110844, 0.0370235971586, 0.033715425763, 0.0302638965903, 0.0267773256187, 0.0233537009481, 0.0200766102918, 0.0170126208304, 0.0142101353977, 0.0116996095611, 0.00949490624815, 0.00759549623064, 0.00598918576442, 0.00465506312071, 0.00356639567308, 0.00269326838853, 0.00200482238421, 0.00147101916547, 0.00106391496797, 0.000758475690917, 0.000532994310372); -CONST_ARRAY float gaussian_weights_27[27] = float[27](0.0461341220677, 0.0458280361329, 0.0449219092487, 0.0434513364077, 0.041473056502, 0.0390613205401, 0.0363032722123, 0.03329374093, 0.0301298793329, 0.0269060634368, 0.0237094185849, 0.0206162474982, 0.0176895305381, 0.0149775565587, 0.0125136384235, 0.0103167809312, 0.00839310755091, 0.00673781900749, 0.00533745056926, 0.00417221196467, 0.00321822819374, 0.00244954419315, 0.00183980458116, 0.00136356575469, 0.000997237148304, 0.000719678922527, 0.000512503802344); -CONST_ARRAY float gaussian_weights_28[28] = float[28](0.0444261989064, 0.0441528077786, 0.0433426874556, 0.0420253835231, 0.0402481455205, 0.0380731143611, 0.0355737191338, 0.0328305743187, 0.0299271960082, 0.0269458517312, 0.0239638252082, 0.021050320394, 0.0182641563909, 0.0156523252517, 0.0132494072776, 0.0110777708249, 0.00914843153174, 0.00746241242714, 0.00601243221213, 0.00478475249866, 0.00376103264156, 0.00292006868614, 0.00223932625116, 0.00169621154636, 0.00126905661972, 0.000937821796457, 0.000684538667784, 0.000493530489859); -CONST_ARRAY float gaussian_weights_29[29] = float[29](0.0428402218449, 0.0425950320572, 0.0418678504625, 0.0406833639509, 0.0390811670302, 0.0371135646233, 0.0348427391927, 0.0323374960065, 0.0296698234919, 0.0269115067302, 0.0241310123361, 0.0213908255587, 0.0187453704346, 0.0162395871695, 0.0139081837241, 0.0117755263835, 0.00985609128793, 0.00815536834958, 0.00667109187858, 0.00539466821111, 0.00431267799915, 0.00340834700336, 0.00266290118508, 0.00205674653671, 0.00157043864968, 0.00118542928527, 0.000884595662595, 0.000652572000374, 0.000475911876172); -CONST_ARRAY float gaussian_weights_30[30] = float[30](0.0413635803064, 0.0411428442631, 0.0404876787473, 0.0394188384355, 0.0379696981566, 0.0361845223744, 0.034116221224, 0.0318237516855, 0.0293693415551, 0.02681571743, 0.0242235064343, 0.02164895686, 0.0191420884567, 0.0167453427128, 0.0144927613074, 0.0124096809627, 0.0105128984936, 0.00881123336238, 0.00730639779605, 0.00599407677732, 0.00486512123495, 0.00390676609731, 0.00310379858835, 0.00243961912893, 0.00189715540038, 0.0014596077497, 0.00111101978377, 0.000836680798136, 0.000623376159936, 0.000459507870433); -CONST_ARRAY float gaussian_weights_31[31] = float[31](0.0399853445765, 0.0397859168385, 0.0391935817056, 0.0382258887254, 0.0369111251945, 0.0352869427376, 0.0333985672178, 0.0312967106632, 0.0290353194613, 0.0266692976067, 0.0242523374248, 0.0218349741136, 0.019462956678, 0.0171759989834, 0.015006943611, 0.0129813407749, 0.0111174172637, 0.00942638810365, 0.00791304767411, 0.00657656782027, 0.00541142793358, 0.00440840524404, 0.00355556151442, 0.00283917353606, 0.00224456783464, 0.00175683343057, 0.00136139920952, 0.00104447357383, 0.000793353010129, 0.000596612772324, 0.000444197054481); -CONST_ARRAY float gaussian_weights_32[32] = float[32](0.0386959947766, 0.038515219631, 0.0379779455684, 0.0370990921929, 0.035902758976, 0.0344211273781, 0.03269302325, 0.0307622290963, 0.0286756484161, 0.0264814290509, 0.0242271492383, 0.0219581595966, 0.0197161578279, 0.017538052295, 0.0154551477556, 0.0134926634575, 0.0116695723433, 0.00999873179996, 0.00848726229109, 0.00713712091343, 0.00594581251708, 0.00490718118467, 0.00401222890115, 0.00324991525467, 0.00260790097568, 0.00207320803451, 0.00163277896075, 0.001273927269, 0.000984678824041, 0.0007540103176, 0.000571995622223, 0.000429873672017); -CONST_ARRAY float gaussian_weights_33[33] = float[33](0.0374872006405, 0.0373228234108, 0.0368340037493, 0.0360334897628, 0.0349419131486, 0.0335869056163, 0.0320019370398, 0.0302249435008, 0.0282968236112, 0.0262598859882, 0.024156329418, 0.0220268304723, 0.0199093019753, 0.0178378709241, 0.0158421076059, 0.0139465201734, 0.0121703121823, 0.0105273857368, 0.00902656082137, 0.00767197268489, 0.00646360400967, 0.0053979069534, 0.00446847163236, 0.00366670165813, 0.00298246326618, 0.00240468165596, 0.00192186570093, 0.00152254956825, 0.00119564651815, 0.000930715882533, 0.000718148744363, 0.000549281085644, 0.000416445182144); -CONST_ARRAY float gaussian_weights_34[34] = float[34](0.0363516413217, 0.036201737859, 0.0357557262315, 0.0350245510305, 0.0340259562937, 0.0327837698447, 0.0313269586531, 0.0296885074803, 0.0279041813374, 0.0260112363621, 0.0240471434912, 0.0220483849925, 0.0200493760572, 0.0180815530273, 0.0161726573904, 0.0143462314376, 0.0126213284443, 0.0110124282972, 0.00952953936303, 0.00817845959286, 0.00696116464401, 0.00587628821513, 0.00491965967439, 0.00408486607427, 0.00336380935028, 0.00274723438686, 0.00222520918402, 0.0017875440918, 0.00142414258053, 0.00112528095772, 0.000881818604268, 0.00068534355663, 0.000528260575697, 0.000403830257603); -CONST_ARRAY float gaussian_weights_35[35] = float[35](0.0352828569912, 0.0351457772102, 0.0347377250654, 0.034068138963, 0.0331523456006, 0.0320109768082, 0.0306691971283, 0.0291557825023, 0.027502097108, 0.025741019002, 0.0239058656123, 0.0220293674457, 0.0201427329382, 0.0182748397359, 0.0164515784956, 0.0146953652717, 0.0130248284398, 0.0114546665776, 0.00999566534574, 0.00865485462404, 0.0074357822241, 0.00633887752729, 0.00536187733782, 0.00450028691203, 0.00374785124891, 0.00309701495804, 0.00253935298702, 0.00206595882394, 0.00166778115643, 0.00133590408651, 0.00106176965372, 0.000837344464398, 0.000655234585795, 0.000508754527564, 0.000391957136174); -CONST_ARRAY float gaussian_weights_36[36] = float[36](0.0342751258684, 0.0341494482206, 0.0337751734808, 0.0331604756171, 0.0323186475622, 0.0312676233073, 0.0300293428847, 0.0286289916303, 0.0270941505287, 0.0254538975676, 0.0237379007598, 0.021975541872, 0.0201951061634, 0.0184230679295, 0.0166834948398, 0.0149975864603, 0.013383354516, 0.0118554448693, 0.0104250943304, 0.00910020962319, 0.0078855513561, 0.00678300282029, 0.00579190187853, 0.0049094140256, 0.00413092573351, 0.00345043921075, 0.0028609524436, 0.00235481157601, 0.00192402606174, 0.00156054035493, 0.00125645900204, 0.00100422471517, 0.000796751251198, 0.000627514647567, 0.000490607571324, 0.00038076225462); -CONST_ARRAY float gaussian_weights_37[37] = float[37](0.0333233617429, 0.0332078562721, 0.0328637365047, 0.0322981096857, 0.0315225495765, 0.030552702685, 0.0294077635218, 0.0281098434571, 0.0266832621385, 0.025153793115, 0.0235478961881, 0.0218919680964, 0.0202116405818, 0.0185311509155, 0.0168728049169, 0.0152565467323, 0.0136996435735, 0.0122164876079, 0.0108185116105, 0.00951421011541, 0.00830925385448, 0.00720668238666, 0.00620715804382, 0.00530926362864, 0.00450982659987, 0.00380425362601, 0.00318686120024, 0.00265119028911, 0.00219029553592, 0.00179700217107, 0.00146412633301, 0.00118465684373, 0.000951898516011, 0.000759578735218, 0.000601920330354, 0.000473684629482, 0.000370189110245); -CONST_ARRAY float gaussian_weights_38[38] = float[38](0.0324230281088, 0.0323166262891, 0.0319995113068, 0.0314778862896, 0.0307618652238, 0.0298651468432, 0.028804578888, 0.02759963209, 0.0262718068088, 0.0248439975284, 0.0233398413379, 0.0217830760655, 0.0201969319937, 0.0186035782268, 0.0170236410318, 0.0154758070976, 0.0139765199563, 0.0125397730562, 0.011176998459, 0.00989704607063, 0.00870624489846, 0.00760853518304, 0.00660565843784, 0.00569739146478, 0.0048818102413, 0.00415557011334, 0.00351418985579, 0.00295232873728, 0.00246404760181, 0.00204304700967, 0.00168287753264, 0.00137711925802, 0.00111952933378, 0.000904157917575, 0.000725434137119, 0.000578224612709, 0.000457867740317, 0.00036018730702); -CONST_ARRAY float gaussian_weights_39[39] = float[39](0.0315700658546, 0.0314718358241, 0.0311789757839, 0.0306969191808, 0.0300345351942, 0.0292038573418, 0.0282197199596, 0.0270993178919, 0.0258617076395, 0.0245272701486, 0.0231171563159, 0.0216527361219, 0.0201550711348, 0.0186444280746, 0.0171398483337, 0.0156587850296, 0.0142168155126, 0.0128274335027, 0.0115019213772, 0.0102492997744, 0.00907634875668, 0.00798769241821, 0.00698593707642, 0.00607185210057, 0.0052445819707, 0.00450187828841, 0.00384034109358, 0.00325565988292, 0.00274284606771, 0.00229645013991, 0.00191075843051, 0.00157996594715, 0.00129832328924, 0.00106025699581, 0.000860463840048, 0.00069398052067, 0.000556230904611, 0.000443053455279, 0.000350711752291); - - - -// Offsets and weights for n = 2, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_2[] = float[2] (0.274068619061, 0.725931380939); -CONST_ARRAY float opt_gaussian_offsets_2[] = float[2] (-1.0, 0.377540668798); - -// Offsets and weights for n = 3, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_3[] = float[3] (0.15246914402, 0.473220416416, 0.374310439564); -CONST_ARRAY float opt_gaussian_offsets_3[] = float[3] (-2.0, -0.468790626626, 1.40733340005); - -// Offsets and weights for n = 4, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_4[] = float[4] (0.106288519406, 0.306091409205, 0.341010215168, 0.24660985622); -CONST_ARRAY float opt_gaussian_offsets_4[] = float[4] (-2.0, -0.458429516783, 1.48611468225, 3.43099866743); - -// Offsets and weights for n = 5, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_5[] = float[5] (0.0816744222584, 0.220481043971, 0.265173712396, 0.249350938326, 0.183319883049); -CONST_ARRAY float opt_gaussian_offsets_5[] = float[5] (-3.0, -1.46101677931, 0.507811864279, 2.47657965106, 4.44552953504); - -// Offsets and weights for n = 6, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_6[] = float[6] (0.0663416657403, 0.170786343946, 0.208182532594, 0.216591983241, 0.192330414616, 0.145767059863); -CONST_ARRAY float opt_gaussian_offsets_6[] = float[6] (-3.0, -1.46505705484, 0.514995501619, 2.49500016666, 4.47502081252, 6.45512110763); - -// Offsets and weights for n = 7, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_7[] = float[7] (0.0558645795815, 0.138837939971, 0.168410194672, 0.182939846093, 0.177962436458, 0.155034498765, 0.12095050446); -CONST_ARRAY float opt_gaussian_offsets_7[] = float[7] (-4.0, -2.46879062663, -0.482645862567, 1.50347216641, 3.48958484011, 5.47571357137, 7.46187967409); - -// Offsets and weights for n = 8, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_8[] = float[8] (0.0482490884887, 0.116731177798, 0.140135626714, 0.155109457758, 0.158290996847, 0.148936931244, 0.12920422877, 0.10334249238); -CONST_ARRAY float opt_gaussian_offsets_8[] = float[8] (-4.0, -2.47196820019, -0.482150445594, 1.50765246363, 3.49744900173, 5.48724766412, 7.47705693918, 9.46688527998); - -// Offsets and weights for n = 9, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_9[] = float[9] (0.042462603473, 0.100587552233, 0.119370554051, 0.133110608252, 0.139473180769, 0.137319230357, 0.127038284604, 0.110433394405, 0.0902045918559); -CONST_ARRAY float opt_gaussian_offsets_9[] = float[9] (-5.0, -3.47463117774, -1.48242911339, 0.509764383426, 2.50195311507, 4.49414089321, 6.48633153138, 8.47852883754, 10.4707366066); - -// Offsets and weights for n = 10, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_10[] = float[10] (0.0379163821476, 0.0883084946127, 0.103630848659, 0.115769751833, 0.12311774987, 0.124642393984, 0.120124182183, 0.110208385676, 0.0962539004871, 0.0800279105479); -CONST_ARRAY float opt_gaussian_offsets_10[] = float[10] (-5.0, -3.47686837586, -1.48303121052, 0.510800788681, 2.50462949733, 4.49845679502, 6.49228456308, 8.48611468225, 10.4799490304, 12.4737894803); - -// Offsets and weights for n = 11, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_11[] = float[11] (0.034250034987, 0.0786680908358, 0.0913650297743, 0.101960743385, 0.109334598331, 0.112655883289, 0.111537726901, 0.106111230043, 0.0970001809665, 0.0852030922382, 0.0719133892488); -CONST_ARRAY float opt_gaussian_offsets_11[] = float[11] (-6.0, -4.47876278503, -2.48375571894, -0.488751898053, 1.5062496745, 3.5012499974, 5.49625007031, 7.49125089312, 9.4862534651, 11.4812587841, 13.4762678459); - -// Offsets and weights for n = 12, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_12[] = float[12] (0.0312305401816, 0.0709054311219, 0.0815774390232, 0.0908099474451, 0.0978069207462, 0.101924493536, 0.10276858031, 0.100257061632, 0.0946329555381, 0.0864256482923, 0.0763687507926, 0.0652922313807); -CONST_ARRAY float opt_gaussian_offsets_12[] = float[12] (-6.0, -4.48038197724, -2.48450909152, -0.488638319781, 1.5072309008, 3.50309913386, 5.49896694362, 7.49483489448, 9.49070355081, 11.4865734766, 13.482445235, 15.4783193884); - -// Offsets and weights for n = 13, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_13[] = float[13] (0.0287005193039, 0.0645245840776, 0.0736085919864, 0.0816749725356, 0.0881468265092, 0.0925297883119, 0.0944742956035, 0.093821628, 0.0906253035588, 0.0851438313916, 0.0778061819627, 0.0691563739381, 0.0597871028205); -CONST_ARRAY float opt_gaussian_offsets_13[] = float[13] (-7.0, -5.48177890584, -3.48524733884, -1.48871719346, 0.507811864279, 2.50434016876, 4.50086805468, 6.49739585688, 8.49392391023, 10.4904525495, 12.4869821093, 14.4835129238, 16.4800453267); - -// Offsets and weights for n = 14, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_14[] = float[14] (0.0265498357372, 0.0591891116629, 0.067008146258, 0.0740882641635, 0.080003184581, 0.084372549444, 0.0869022686967, 0.0874172424738, 0.0858814043683, 0.0824018985422, 0.0772167186324, 0.0706677895493, 0.0631637213247, 0.0551378645661); -CONST_ARRAY float opt_gaussian_offsets_14[] = float[14] (-7.0, -5.482994727, -3.48595044497, -1.48890714597, 0.508135376648, 2.50517732975, 4.50221892034, 6.49926035557, 8.49630184259, 10.4933435885, 12.4903858005, 14.4874286855, 16.4844724502, 18.4815173012); - -// Offsets and weights for n = 15, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_15[] = float[15] (0.0246991001359, 0.0546630307477, 0.0614599000867, 0.0677077117875, 0.0730857269119, 0.0772992275365, 0.0801061552404, 0.0813401134339, 0.0809266963759, 0.0788909166077, 0.0753546997316, 0.0705247954691, 0.0646727718699, 0.0581097839419, 0.0511593701234); -CONST_ARRAY float opt_gaussian_offsets_15[] = float[15] (-8.0, -6.48406152432, -4.48661034495, -2.48915986216, -0.491709943445, 1.5057395438, 3.50318873228, 5.50063775476, 7.49808674403, 9.49553583291, 11.4929851542, 13.4904348406, 15.4878850248, 17.4853358393, 19.4827874166); - -// Offsets and weights for n = 16, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_16[] = float[16] (0.0230896376799, 0.050776085665, 0.0567362408729, 0.0622801542701, 0.0671624585612, 0.0711526823588, 0.074053187201, 0.0757153650032, 0.0760522510849, 0.0750460631044, 0.0727497562674, 0.0692824055002, 0.0648189750606, 0.0595756988869, 0.0537927708459, 0.0477162676376); -CONST_ARRAY float opt_gaussian_offsets_16[] = float[16] (-8.0, -6.48500449838, -4.48722500316, -2.48944601229, -0.491667438186, 1.50611080683, 3.50388881047, 5.50166666049, 7.49944444467, 9.4972222508, 11.4950001667, 13.49277828, 15.4905566786, 17.4883354502, 19.4861146823, 21.4838944625); - -// Offsets and weights for n = 17, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_17[] = float[17] (0.0216771440358, 0.0474024431083, 0.0526696583526, 0.0576157272943, 0.0620500832243, 0.0657906931345, 0.0686763697103, 0.0705782641713, 0.0714094011985, 0.0711312736702, 0.0697568007769, 0.0673493330105, 0.0640178107627, 0.0599085918637, 0.0551948018209, 0.0500642848839, 0.0447073189814); -CONST_ARRAY float opt_gaussian_offsets_17[] = float[17] (-9.0, -7.48584362821, -5.48779539349, -3.489747531, -1.49169998126, 0.506347315253, 2.5043944181, 4.50244138685, 6.50048828109, 8.49853516044, 10.4965820845, 12.4946291128, 14.4926763051, 16.4907237208, 18.4887714194, 20.4868194606, 22.4848679037); - -// Offsets and weights for n = 18, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_18[] = float[18] (0.0204275387753, 0.0444471422165, 0.0491344246582, 0.0535700569458, 0.0576039868414, 0.0610909922542, 0.063899287897, 0.0659187647663, 0.0670681468019, 0.0673004170345, 0.0666060084333, 0.0650134576635, 0.062587459498, 0.0594245067401, 0.0556465251586, 0.0513930883489, 0.0468129040831, 0.0420552918834); -CONST_ARRAY float opt_gaussian_offsets_18[] = float[18] (-9.0, -7.48659490868, -5.48832392241, -3.49005321559, -1.49178274685, 0.506487525174, 2.50475764187, 4.50302764466, 6.50129757494, 8.49956747416, 10.4978373837, 12.4961073451, 14.4943773996, 16.4926475888, 18.490917954, 20.4891885367, 22.487459378, 24.4857305195); - -// Offsets and weights for n = 19, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_19[] = float[19] (0.0193141765234, 0.0418372042117, 0.0460345037418, 0.0500318718292, 0.0537096734822, 0.0569509205974, 0.0596473922603, 0.0617056100684, 0.0630522103997, 0.063638283716, 0.0634423231446, 0.0624715344916, 0.0607613964306, 0.058373508293, 0.0553919075004, 0.0519181635644, 0.0480656477248, 0.0439534278952, 0.0397002441254); -CONST_ARRAY float opt_gaussian_offsets_19[] = float[19] (-10.0, -8.48727126934, -6.48881359538, -4.49035613443, -2.49189885715, -0.493441734166, 1.50501526389, 3.50347216641, 5.50192900278, 7.50038580239, 9.49884259466, 11.497299409, 13.4957562747, 15.4942132214, 17.4926702782, 19.4911274747, 21.4895848401, 23.4880424039, 25.4865001954); - -// Offsets and weights for n = 20, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_20[] = float[20] (0.0183159239212, 0.0395156384342, 0.0432953808504, 0.0469143095749, 0.0502759514807, 0.0532851842709, 0.0558526585709, 0.0578991827119, 0.0593597716547, 0.0601870718495, 0.0603539103928, 0.0598547770814, 0.0587061266098, 0.0569454777934, 0.0546293785999, 0.0518303909461, 0.0486333194666, 0.0451309573637, 0.0414196460388, 0.0375949423883); -CONST_ARRAY float opt_gaussian_offsets_20[] = float[20] (-10.0, -8.48788325916, -6.48926757672, -4.4906520589, -2.4920366845, -0.493421432277, 1.50519371901, 3.50380879059, 5.50242380373, 7.50103877967, 9.49965373967, 11.498268705, 13.4968836969, 15.4954987366, 17.4941138453, 19.4927290444, 21.4913443551, 23.4899597986, 25.488575396, 27.4871911687); - -// Offsets and weights for n = 21, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_21[] = float[21] (0.017415804187, 0.0374373032386, 0.0408584208729, 0.0441487454036, 0.0472296714025, 0.050023174672, 0.0524550548792, 0.0544581909425, 0.0559756111883, 0.0569631828342, 0.0573917437838, 0.0572485330749, 0.0565378220739, 0.0552807028518, 0.0535140483716, 0.0512887160172, 0.0486671165854, 0.0457203108183, 0.0425248216691, 0.0391593610009, 0.0357016641323); -CONST_ARRAY float opt_gaussian_offsets_21[] = float[21] (-11.0, -9.48843956064, -7.48968896203, -5.49093849226, -3.49218813572, -1.49343787681, 0.505312300099, 2.50406241061, 4.50281247034, 6.50156249491, 8.50031249996, 10.4990625011, 12.497812514, 14.4965625542, 16.4953126373, 18.4940627791, 20.492812995, 22.4915633008, 24.490313712, 26.4890642443, 28.4878149131); - -// Offsets and weights for n = 22, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_22[] = float[22] (0.0166000229565, 0.0355659864281, 0.0386768958381, 0.0416803556939, 0.044511709669, 0.047106429585, 0.0494025259225, 0.0513429921806, 0.0528781496068, 0.0539677581387, 0.0545827685096, 0.0547066090713, 0.0543359277732, 0.0534807429267, 0.05216399329, 0.0504205156771, 0.0482955136927, 0.0458426115287, 0.0431216096972, 0.040196073531, 0.0371308894807, 0.0339899188026); -CONST_ARRAY float opt_gaussian_offsets_22[] = float[22] (-11.0, -9.48894737902, -7.49008066671, -5.49121405638, -3.49234753637, -1.49348109503, 0.505385279274, 2.50425159821, 4.50311787342, 6.50198411657, 8.50085033932, 10.4997165533, 12.4985827702, 14.4974490017, 16.4963152595, 18.4951815551, 20.4940479002, 22.4929143066, 24.4917807858, 26.4906473495, 28.4895140094, 30.488380777); - -// Offsets and weights for n = 23, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_23[] = float[23] (0.0158572558637, 0.0338723065065, 0.0367131374703, 0.0394648878577, 0.0420739114357, 0.0444864291153, 0.0466503437274, 0.0485170953621, 0.0500434658737, 0.0511932393217, 0.0519386294298, 0.0522613955547, 0.0521535845943, 0.0516178567066, 0.0506673762143, 0.0493252739403, 0.0476237116354, 0.0456026013273, 0.0433080507681, 0.0407906194304, 0.0381034769055, 0.0353005567664, 0.0324347941928); -CONST_ARRAY float opt_gaussian_offsets_23[] = float[23] (-12.0, -10.4894127398, -8.49044537813, -6.49147809805, -4.49251089071, -2.49354374729, -0.494576658982, 1.50439038303, 3.50335738756, 5.50232436342, 7.50129131944, 9.50025826444, 11.4992252072, 13.4981921566, 15.4971591215, 17.4961261106, 19.4950931327, 21.4940601968, 23.4930273116, 25.4919944858, 27.4909617284, 29.4899290482, 31.4888964539); - -// Offsets and weights for n = 24, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_24[] = float[24] (0.0151781193223, 0.0323321780903, 0.0349364627792, 0.0374662789601, 0.0398767590653, 0.0421227588364, 0.0441602397335, 0.0459476917322, 0.0474475330031, 0.0486274208977, 0.0494614105199, 0.0499309029722, 0.0500253348908, 0.0497425735707, 0.0490889970417, 0.0480792548951, 0.0467357223884, 0.0450876762325, 0.0431702344581, 0.0410231139437, 0.0386892669072, 0.0362134615119, 0.0336408715991, 0.0310157366487); -CONST_ARRAY float opt_gaussian_offsets_24[] = float[24] (-12.0, -10.4898407179, -8.49078554242, -6.49173043279, -4.49267538225, -2.49362038404, -0.494565431416, 1.50448948237, 3.50354436407, 5.50259922044, 7.50165405824, 9.50070888421, 11.4997637051, 13.4988185277, 15.4978733588, 17.496928205, 19.4959830732, 21.4950379701, 23.4940929025, 25.493147877, 27.4922029006, 29.4912579798, 31.4903131215, 33.4893683324); - -// Offsets and weights for n = 25, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_25[] = float[25] (0.0145547717977, 0.0309256724585, 0.0333216402417, 0.0356548788588, 0.0378875858988, 0.0399816099167, 0.0418995150216, 0.0436056826279, 0.0450674056297, 0.0462559283339, 0.0471473860962, 0.047723601823, 0.0479727022219, 0.0478895246213, 0.0474757948889, 0.0467400678857, 0.0456974333241, 0.0443690011569, 0.04278119101, 0.0409648590605, 0.0389543026351, 0.0367861873003, 0.0344984431302, 0.032129176157, 0.0297156379033); -CONST_ARRAY float opt_gaussian_offsets_25[] = float[25] (-13.0, -11.4902356166, -9.49110336963, -7.49197117629, -5.49283903134, -3.49370692955, -1.49457486569, 0.504557165471, 2.50368916916, 4.50282115062, 6.50195311507, 8.50108506774, 10.5002170139, 12.4993489587, 14.4984809075, 16.4976128654, 18.4967448377, 20.4958768296, 22.4950088463, 24.4941408932, 26.4932729754, 28.4924050981, 30.4915372666, 32.4906694861, 34.4898017618); - -// Offsets and weights for n = 26, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_26[] = float[26] (0.0139806095106, 0.0296361596261, 0.0318477401908, 0.0340061091799, 0.0360791996382, 0.0380345665842, 0.0398402151893, 0.0414654617847, 0.0428817957719, 0.0440637088489, 0.0449894579767, 0.0456417302531, 0.0460081813069, 0.0460818238265, 0.0458612491398, 0.0453506720284, 0.0445597967842, 0.0435035104568, 0.0422014168346, 0.0406772315419, 0.0389580643162, 0.0370736187825, 0.0350553426473, 0.0329355621081, 0.0307466334336, 0.028520142239); -CONST_ARRAY float opt_gaussian_offsets_26[] = float[26] (-13.0, -11.4906011073, -9.49140084797, -7.49220063267, -5.4930004573, -3.49380031775, -1.49460020994, 0.504599870223, 2.50379992684, 4.502999964, 6.5021999858, 8.50139999634, 10.5005999997, 12.4998, 14.4990000013, 16.4982000078, 18.4974000234, 20.4966000524, 22.4958000988, 24.4950001667, 26.4942002601, 28.4934003833, 30.4926005403, 32.4918007351, 34.4910009719, 36.4902012547); - -// Offsets and weights for n = 27, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_27[] = float[27] (0.0134500314681, 0.028449654128, 0.0304972623514, 0.0324994402414, 0.0344288143118, 0.0362576299487, 0.0379584017792, 0.0395045923815, 0.040871296279, 0.0420359047813, 0.0429787269811, 0.0436835431173, 0.0441380685956, 0.0443343101221, 0.0442687995259, 0.0439426957131, 0.043361750565, 0.0425361401822, 0.0414801683926, 0.0402118545935, 0.0387524225201, 0.0371257101947, 0.0353575239501, 0.0334749609143, 0.0315057246655, 0.0294774579359, 0.0274171143604); -CONST_ARRAY float opt_gaussian_offsets_27[] = float[27] (-14.0, -12.4909403408, -10.4916797622, -8.49241922001, -6.493158711, -4.49389823193, -2.49463777955, -0.495377350649, 1.50388305803, 3.50314344971, 5.50240382763, 7.50166419504, 9.50092455516, 11.5001849112, 13.4994452665, 15.4987056242, 17.4979659876, 19.4972263598, 21.4964867442, 23.495747144, 25.4950075624, 27.4942680026, 29.4935284679, 31.4927889616, 33.4920494867, 35.4913100467, 37.4905706447); - -// Offsets and weights for n = 28, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_28[] = float[28] (0.0129582560674, 0.0273543103165, 0.0292554660583, 0.0311176025635, 0.0329172158359, 0.0346304326621, 0.0362335258515, 0.0377034536229, 0.0390184062889, 0.0401583422803, 0.0411054951919, 0.0418448339636, 0.0423644595612, 0.0426559235313, 0.0427144565149, 0.0425390980829, 0.0421327229529, 0.0415019625886, 0.0406570251689, 0.0396114207574, 0.0383816020104, 0.0369865337755, 0.0354472073009, 0.0337861164192, 0.0320267139071, 0.030192866258, 0.0283083243587, 0.0263962261095); -CONST_ARRAY float opt_gaussian_offsets_28[] = float[28] (-14.0, -12.4912560356, -10.4919417129, -8.49262742053, -6.4933131559, -4.49399891644, -2.49468469955, -0.495370502671, 1.50394367679, 3.50325784141, 5.50257199378, 7.50188613646, 9.50120027204, 11.5005144031, 13.4998285322, 15.499142662, 17.498456795, 19.4977709338, 21.497085081, 23.4963992392, 25.4957134109, 27.4950275988, 29.4943418053, 31.4936560332, 33.4929702849, 35.4922845631, 37.4915988703, 39.4909132091); - -// Offsets and weights for n = 29, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_29[] = float[29] (0.0125011765317, 0.0263400288238, 0.0281098505852, 0.0298459733379, 0.0315281054522, 0.0331356062206, 0.0346478974098, 0.036044895224, 0.0373074502411, 0.0384177819917, 0.0393598944759, 0.0401199590865, 0.040686652145, 0.0410514355414, 0.0412087707617, 0.0411562588012, 0.0408947010275, 0.0404280788327, 0.039763452797, 0.0389107849351, 0.037882690277, 0.0366941264423, 0.0353620318792, 0.0339049249866, 0.03234247735, 0.030695074782, 0.0289833797506, 0.0272279081406, 0.0254486321699); -CONST_ARRAY float opt_gaussian_offsets_29[] = float[29] (-15.0, -13.4915505493, -11.4921881357, -9.49282574751, -7.49346338263, -5.49410103902, -3.49473871461, -1.49537640731, 0.503985884951, 2.50334816424, 4.50271043263, 6.50207269221, 8.50143494504, 10.5007971932, 12.5001594388, 14.4995216838, 16.4988839304, 18.4982461807, 20.4976084366, 22.4969707003, 24.4963329739, 26.4956952594, 28.4950575589, 30.4944198745, 32.4937822083, 34.4931445623, 36.4925069386, 38.4918693392, 40.4912317663); - -// Offsets and weights for n = 30, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_30[] = float[30] (0.0120752459112, 0.0253981466755, 0.0270497479603, 0.0286720947252, 0.0302475794896, 0.031758269856, 0.0331862397737, 0.0345139179849, 0.0357244443761, 0.0368020242498, 0.0377322701791, 0.038502521139, 0.0391021290372, 0.0395227035884, 0.0397583076624, 0.0398055967488, 0.0396638979611, 0.0393352259681, 0.0388242353293, 0.0381381108078, 0.0372863992743, 0.0362807887003, 0.0351348413894, 0.033863689954, 0.0324837055512, 0.0310121485206, 0.0294668117938, 0.0278656672835, 0.0262265249262, 0.0245667131829); -CONST_ARRAY float opt_gaussian_offsets_30[] = float[30] (-15.0, -13.4918259364, -11.4924203191, -9.49301472323, -7.49360914711, -5.49420358906, -3.49479804739, -1.49539252043, 0.504012993496, 2.50341849608, 4.502823989, 6.50222947393, 8.50163495256, 10.5010404266, 12.5004458976, 14.4998513674, 16.4992568376, 18.49866231, 20.4980677861, 22.4974732676, 24.4968787564, 26.4962842539, 28.4956897619, 30.4950952822, 32.4945008163, 34.4939063659, 36.4933119328, 38.4927175186, 40.492123125, 42.4915287536); - -// Offsets and weights for n = 31, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_31[] = float[31] (0.0116773848279, 0.0245211910437, 0.0260660009978, 0.0275852924479, 0.0290637143118, 0.0304856161901, 0.0318353169301, 0.0330973876259, 0.0342569420705, 0.0352999271142, 0.0362134050695, 0.0369858202625, 0.0376072420704, 0.0380695773066, 0.0383667456063, 0.0384948125006, 0.038452076108, 0.0382391047728, 0.0378587244857, 0.0373159564788, 0.0366179069173, 0.0357736120795, 0.0347938437368, 0.0336908805939, 0.0324782525631, 0.0311704653064, 0.0297827128626, 0.0283305862735, 0.0268297859433, 0.0252958450278, 0.0237438704745); -CONST_ARRAY float opt_gaussian_offsets_31[] = float[31] (-16.0, -14.4920839948, -12.4926394207, -10.4931948647, -8.4937503255, -6.49430580175, -4.49486129205, -2.49541679504, -0.495972309343, 1.50347216641, 3.50291663358, 5.50236109356, 7.50180554771, 9.5012499974, 11.500694444, 13.5001388889, 15.4995833334, 17.499027779, 19.498472227, 21.4979166787, 23.4973611356, 25.496805599, 27.4962500703, 29.4956945509, 31.495139042, 33.4945835452, 35.4940280618, 37.4934725931, 39.4929171405, 41.4923617054, 43.4918062891); - -// Offsets and weights for n = 32, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_32[] = float[32] (0.011304906882, 0.0237026819171, 0.0251507065416, 0.0265763713896, 0.0279662329755, 0.0293065739824, 0.0305836224684, 0.0317837831985, 0.0328938757947, 0.033901373955, 0.0347946397202, 0.0355631466938, 0.0361976862432, 0.0366905510441, 0.0370356908544, 0.0372288361197, 0.037267585878, 0.037151457434, 0.0368818963668, 0.0364622465804, 0.0358976812639, 0.0351950967552, 0.0343629723527, 0.0334112000569, 0.0323508890215, 0.0311941501124, 0.0299538664055, 0.0286434556815, 0.0272766310021, 0.0258671652733, 0.0244286653474, 0.0229743606882); -CONST_ARRAY float opt_gaussian_offsets_32[] = float[32] (-16.0, -14.492326305, -12.4928464819, -10.4933666743, -8.49388688111, -6.49440710113, -4.49492733326, -2.49544757637, -0.495967829348, 1.50351190895, 3.50299163964, 5.50247136385, 7.50195108271, 9.50143079734, 11.5009105089, 13.5003902184, 15.4998699272, 17.4993496362, 19.4988293466, 21.4983090595, 23.4977887761, 25.4972684975, 27.4967482248, 29.4962279592, 31.4957077017, 33.4951874535, 35.4946672158, 37.4941469895, 39.493626776, 41.4931065762, 43.4925863914, 45.4920662227); - -// Offsets and weights for n = 33, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_33[] = float[33] (0.010955457899, 0.022936972726, 0.0242970090863, 0.0256373708328, 0.0269462357952, 0.0282115324444, 0.0294211199518, 0.0305629784405, 0.0316254053597, 0.0325972135674, 0.0334679264771, 0.0342279655323, 0.0348688253347, 0.0353832319562, 0.0357652803168, 0.0360105470026, 0.0361161755112, 0.0360809316269, 0.0359052274243, 0.0355911132491, 0.035142237893, 0.0345637780447, 0.0338623389239, 0.0330458287657, 0.0321233104888, 0.0311048344353, 0.0300012564944, 0.0288240461997, 0.0275850895258, 0.0262964910892, 0.0249703803037, 0.0236187257427, 0.0222531615586); -CONST_ARRAY float opt_gaussian_offsets_33[] = float[33] (-17.0, -15.4925542614, -13.4930424413, -11.4935306345, -9.49401884001, -7.49450705693, -5.49499528434, -3.49548352128, -1.49597176684, 0.503539979913, 2.50305171992, 4.5025634541, 6.5020751834, 8.50158690873, 10.501098631, 12.5006103513, 14.5001220703, 16.4996337891, 18.4991455086, 20.4986572298, 22.4981689535, 24.4976806807, 26.4971924123, 28.4967041493, 30.4962158926, 32.495727643, 34.4952394017, 36.4947511694, 38.4942629471, 40.4937747358, 42.4932865363, 44.4927983496, 46.4923101767); - -// Offsets and weights for n = 34, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_34[] = float[34] (0.0106269661092, 0.0222191206788, 0.0234989336619, 0.0247613662628, 0.0259959812184, 0.0271921148783, 0.0283390259971, 0.029426053101, 0.0304427772918, 0.031379187067, 0.03222584155, 0.03297402843, 0.0336159129307, 0.0341446742532, 0.0345546261735, 0.0348413188147, 0.0350016190478, 0.0350337674884, 0.0349374106417, 0.0347136073722, 0.0343648095349, 0.0338948172612, 0.0333087100411, 0.0326127553496, 0.0318142971136, 0.0309216267959, 0.0299438402574, 0.0288906838477, 0.0277723933565, 0.0265995295291, 0.025382813813, 0.0241329678608, 0.0228605600791, 0.0215758621912); -CONST_ARRAY float opt_gaussian_offsets_34[] = float[34] (-17.0, -15.4927690992, -13.493228146, -11.4936872042, -9.49414627298, -7.49460535168, -5.49506443947, -3.49552353559, -1.49598263925, 0.503558250306, 2.50309913386, 4.5026400122, 6.50218088608, 8.50172175628, 10.5012626236, 12.5008034887, 14.5003443526, 16.4998852158, 18.4994260792, 20.4989669436, 22.4985078098, 24.4980486784, 26.4975895504, 28.4971304264, 30.4966713072, 32.4962121937, 34.4957530865, 36.4952939865, 38.4948348945, 40.4943758111, 42.4939167373, 44.4934576737, 46.4929986211, 48.4925395803); - -// Offsets and weights for n = 35, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_35[] = float[35] (0.0103176010324, 0.0215447805499, 0.0227512495695, 0.0239423078106, 0.025108706565, 0.0262409919812, 0.0273296287144, 0.0283651308048, 0.0293381973431, 0.0302398502594, 0.0310615714112, 0.0317954360651, 0.0324342398575, 0.0329716163978, 0.0334021428322, 0.0337214309223, 0.033926201499, 0.034014340527, 0.0339849354379, 0.0338382908596, 0.0335759233627, 0.0332005353504, 0.0327159687203, 0.0321271394105, 0.0314399543874, 0.0306612130365, 0.0297984952565, 0.0288600388309, 0.027854608845, 0.0267913620359, 0.0256797089935, 0.0245291770894, 0.0233492768839, 0.0221493745731, 0.0209385727842); -CONST_ARRAY float opt_gaussian_offsets_35[] = float[35] (-18.0, -16.4929719162, -14.4934043618, -12.4938368174, -10.4942692821, -8.49470175541, -6.49513423665, -4.49556672518, -2.49599922033, -0.49643172148, 1.50313577204, 3.50270326086, 5.50227074563, 7.50183822701, 9.50140570564, 11.5009731822, 13.5005406572, 15.5001081315, 17.4996756056, 19.4992430802, 21.4988105559, 23.4983780334, 25.4979455133, 27.4975129963, 29.497080483, 31.4966479741, 33.4962154702, 35.495782972, 37.49535048, 39.4949179951, 41.4944855177, 43.4940530486, 45.4936205884, 47.4931881378, 49.4927556973); - -// Offsets and weights for n = 36, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_36[] = float[36] (0.0100257393422, 0.0209101171194, 0.0220493585518, 0.0231748877373, 0.0242784805505, 0.0253517271725, 0.0263861353948, 0.0273732400312, 0.0283047165243, 0.0291724966555, 0.029968884136, 0.0306866677818, 0.0313192299543, 0.0318606479935, 0.0323057864723, 0.0326503782638, 0.0328910926329, 0.0330255888321, 0.0330525539969, 0.0329717244812, 0.0327838901489, 0.0324908815237, 0.0320955400894, 0.0316016724148, 0.031013989138, 0.0303380301802, 0.0295800778452, 0.028747059714, 0.027846443428, 0.0268861255931, 0.0258743171068, 0.0248194272242, 0.0237299486283, 0.02261434567, 0.0214809477837, 0.0203378498874); -CONST_ARRAY float opt_gaussian_offsets_36[] = float[36] (-18.0, -16.4931636913, -14.4935717828, -12.4939798828, -10.4943879908, -8.49479610628, -6.49520422871, -4.49561235753, -2.4960204922, -0.496428632166, 1.5031632231, 3.50275507416, 5.50234692154, 7.50193876579, 9.50153060746, 11.5011224471, 13.5007142852, 15.5003061224, 17.4998979592, 19.4994897961, 21.4990816337, 23.4986734725, 25.4982653131, 27.497857156, 29.4974490017, 31.4970408509, 33.496632704, 35.4962245616, 37.4958164242, 39.4954082924, 41.4950001667, 43.4945920476, 45.4941839358, 47.4937758317, 49.493367736, 51.492959649); - -// Offsets and weights for n = 37, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_37[] = float[37] (0.00974993636734, 0.0203117325559, 0.0213892024529, 0.0224544310942, 0.0235000813023, 0.0245186478884, 0.0255025443957, 0.0264441950142, 0.0273361301596, 0.0281710840642, 0.0289420926191, 0.0296425896425, 0.0302664997198, 0.0308083257886, 0.0312632297024, 0.0316271041272, 0.0318966342731, 0.032069348168, 0.0321436544031, 0.0321188665476, 0.031995213706, 0.0317738369926, 0.0314567719985, 0.0310469176281, 0.0305479919715, 0.0299644761535, 0.0293015473444, 0.0285650023323, 0.0277611732343, 0.0268968370582, 0.0259791209177, 0.0250154047456, 0.0240132233523, 0.0229801696268, 0.0219238005913, 0.0208515478943, 0.0197706341665); -CONST_ARRAY float opt_gaussian_offsets_37[] = float[37] (-19.0, -17.4933453004, -15.4937310384, -13.4941167839, -11.4945025364, -9.49488829538, -7.4952740605, -5.49565983124, -3.49604560714, -1.49643138776, 0.503182827378, 2.50279703872, 4.50241124674, 6.50202545188, 8.50163965462, 10.5012538554, 12.5008680547, 14.5004822529, 16.5000964506, 18.4997106482, 20.4993248461, 22.4989390448, 24.4985532448, 26.4981674465, 28.4977816504, 30.4973958569, 32.4970100665, 34.4966242797, 36.4962384969, 38.4958527186, 40.4954669452, 42.4950811772, 44.4946954151, 46.4943096593, 48.4939239102, 50.4935381684, 52.4931524343); - -// Offsets and weights for n = 38, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_38[] = float[38] (0.00948890217078, 0.0197466058518, 0.0207671865319, 0.0217768050027, 0.0227688949424, 0.0237367380186, 0.0246735370734, 0.0255724936997, 0.0264268890116, 0.0272301662959, 0.0279760141433, 0.0286584485977, 0.0292718928381, 0.0298112529127, 0.030271988092, 0.030650174481, 0.030942560647, 0.0311466141591, 0.0312605581081, 0.0312833968702, 0.0312149305926, 0.0310557581051, 0.0308072681993, 0.0304716194529, 0.0300517090085, 0.0295511309394, 0.028974125038, 0.0283255170485, 0.0276106515206, 0.0268353185907, 0.0260056760908, 0.025128168447, 0.0242094438539, 0.0232562712038, 0.0222754582054, 0.0212737720531, 0.0202578639043, 0.0192341982982); -CONST_ARRAY float opt_gaussian_offsets_38[] = float[38] (-19.0, -17.4935175291, -15.4938827012, -13.4942478798, -11.4946130645, -9.49497825506, -7.49534345092, -5.49570865176, -3.49607385718, -1.49643906678, 0.503195719814, 2.502830503, 4.50246528316, 6.5021000607, 8.50173483599, 10.5013696094, 12.5010043814, 14.5006391523, 16.5002739225, 18.4999086925, 20.4995434625, 22.499178233, 24.4988130044, 26.4984477771, 28.4980825514, 30.4977173278, 32.4973521066, 34.4969868882, 36.496621673, 38.4962564615, 40.4958912539, 42.4955260507, 44.4951608523, 46.4947956591, 48.4944304714, 50.4940652897, 52.4937001143, 54.4933349456); - -// Offsets and weights for n = 39, relative to pixel center -CONST_ARRAY float opt_gaussian_weights_39[] = float[39] (0.00924148137305, 0.0192120420422, 0.0201801154313, 0.0211383429813, 0.0220808308543, 0.023001547634, 0.0238943863517, 0.0247532302506, 0.0255720213328, 0.0263448306384, 0.0270659291345, 0.0277298580395, 0.0283314973845, 0.0288661316105, 0.0293295110303, 0.0297179080354, 0.0300281670083, 0.0302577470078, 0.0304047564176, 0.030467978898, 0.0304468901444, 0.0303416651267, 0.0301531756723, 0.0298829784382, 0.0295332935044, 0.029106974001, 0.0286074673487, 0.0280387688511, 0.027405368511, 0.0267121920605, 0.0259645372878, 0.0251680068087, 0.0243284384733, 0.0234518346107, 0.0225442913003, 0.0216119288229, 0.0206608243792, 0.0196969480813, 0.0187261031213); -CONST_ARRAY float opt_gaussian_offsets_39[] = float[39] (-20.0, -18.4936810844, -16.4940272924, -14.4943735062, -12.4947197254, -10.4950659496, -8.49541217862, -6.495758412, -4.49610464945, -2.49645089063, -0.496797135222, 1.50285661712, 3.50251036672, 5.50216411391, 7.50181785903, 9.5014716024, 11.5011253444, 13.5007790852, 15.5004328254, 17.5000865651, 19.4997403047, 21.4993940446, 23.4990477851, 25.4987015265, 27.4983552691, 29.4980090133, 31.4976627594, 33.4973165078, 35.4969702587, 37.4966240125, 39.4962777696, 41.4959315302, 43.4955852948, 45.4952390636, 47.4948928369, 49.4945466151, 51.4942003986, 53.4938541877, 55.4935079826); +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +// lint: skip-file + +#pragma once + + +// Autogenerated, do not edit! + +// python-script to generate the weights: +/* +from __future__ import print_function +SIGMA = 1.0 +file_handle = open("gaussian_weights.txt", "w") +import math +def gauss(x): + return math.exp( -(x**2) / (2.0 * (SIGMA**2) ) ) +for i in range(2, 40): + weights = [gauss(k / float(i-1) * 3.0) for k in range(0, i)] + wsum = weights[0] + 2 * sum(weights[1:]) + weights = [k / wsum for k in weights] + print("CONST_ARRAY float gaussian_weights_{0}[{0}] = float[{0}]({1});".format(i, ', '.join([str(i) for i in weights])), file=file_handle) +file_handle.close() + +*/ + +CONST_ARRAY float gaussian_weights_2[2] = float[2](0.97826491685, 0.0108675415748); +CONST_ARRAY float gaussian_weights_3[3] = float[3](0.598256825182, 0.194225554409, 0.00664603299992); +CONST_ARRAY float gaussian_weights_4[4] = float[4](0.399050279652, 0.242036229376, 0.0540055826224, 0.00443304817524); +CONST_ARRAY float gaussian_weights_5[5] = float[5](0.299372412212, 0.22597815248, 0.0971919922835, 0.0238179220394, 0.0033257270909); +CONST_ARRAY float gaussian_weights_6[6] = float[6](0.239559410861, 0.200096839755, 0.116606083673, 0.0474084957625, 0.0134476107133, 0.00266126466595); +CONST_ARRAY float gaussian_weights_7[7] = float[7](0.199675627498, 0.176213122789, 0.121109390075, 0.0648251851385, 0.0270231576029, 0.00877313479159, 0.00221819585465); +CONST_ARRAY float gaussian_weights_8[8] = float[8](0.171180679737, 0.156160272964, 0.118554487683, 0.0749026292285, 0.0393829054464, 0.0172325706915, 0.00627514853941, 0.00190164557861); +CONST_ARRAY float gaussian_weights_9[9] = float[9](0.149804857814, 0.139633481336, 0.113078639248, 0.0795607594227, 0.0486345167116, 0.0258295968515, 0.0119184008913, 0.00477799498463, 0.00166418164687); +CONST_ARRAY float gaussian_weights_10[10] = float[10](0.133175996016, 0.125979094462, 0.10663900118, 0.0807753247212, 0.0547502887625, 0.0332077287626, 0.0180234111411, 0.00875346226514, 0.00380423901867, 0.00147945167871); +CONST_ARRAY float gaussian_weights_11[11] = float[11](0.119870627342, 0.114596017885, 0.100124364242, 0.0799509287402, 0.0583472982821, 0.0389162949304, 0.0237222412093, 0.013215799633, 0.00672890923663, 0.00313118978625, 0.00133164238418); +CONST_ARRAY float gaussian_weights_12[12] = float[12](0.108982774698, 0.105004137779, 0.0939186994792, 0.0779820830369, 0.0601083349811, 0.043010220493, 0.0285697042449, 0.0176172016263, 0.0100847516981, 0.00535908443022, 0.00264370561491, 0.00121068926685); +CONST_ARRAY float gaussian_weights_13[13] = float[13](0.099908358105, 0.0968345010773, 0.0881688165699, 0.0754147852673, 0.0605974823522, 0.0457413794603, 0.0324354949685, 0.0216066977333, 0.0135211259418, 0.00794865988767, 0.00438966689848, 0.00227732918643, 0.00110988160433); +CONST_ARRAY float gaussian_weights_14[14] = float[14](0.0922291029445, 0.0898057057742, 0.0829109263623, 0.0725757439402, 0.060234195822, 0.0473987215842, 0.0353640528944, 0.0250166600045, 0.0167790942071, 0.0106703727296, 0.0064337259728, 0.00367804823216, 0.00199362821896, 0.00102457278534); +CONST_ARRAY float gaussian_weights_15[15] = float[15](0.0856461968131, 0.0837022313712, 0.0781310922076, 0.0696576247716, 0.0593159286476, 0.0482427341138, 0.0374757556435, 0.0278052491152, 0.0197043035237, 0.0133368508235, 0.00862190840291, 0.00532367362189, 0.00313962187592, 0.00176848417109, 0.00095144330391); +CONST_ARRAY float gaussian_weights_16[16] = float[16](0.0799404796215, 0.0783575520685, 0.0737943634766, 0.0667719013138, 0.0580487023004, 0.0484863518426, 0.0389112087983, 0.0300025492495, 0.0222264351509, 0.0158201169212, 0.0108187674517, 0.00710843674843, 0.0044874398644, 0.00272176986138, 0.00158610663008, 0.000888058511381); +CONST_ARRAY float gaussian_weights_17[17] = float[17](0.0749475595843, 0.0736416335058, 0.0698588070848, 0.0639809599576, 0.0565733860467, 0.0482953623493, 0.0398043484331, 0.0316728718988, 0.0243319101415, 0.0180466584178, 0.0129225799304, 0.0089337437364, 0.00596279102016, 0.00384235530188, 0.00239043692594, 0.00143578327773, 0.000832592179972); +CONST_ARRAY float gaussian_weights_18[18] = float[18](0.0705417098914, 0.069451816911, 0.0662821365674, 0.0613175289906, 0.0549854836326, 0.047795467276, 0.0402717621111, 0.03289196444, 0.0260407956595, 0.0199845303108, 0.0148665056404, 0.0107201068137, 0.00749315271104, 0.00507697836848, 0.00333442766635, 0.00212281695053, 0.0013100233939, 0.000783647610985); +CONST_ARRAY float gaussian_weights_19[19] = float[19](0.0666251318775, 0.0657061792116, 0.0630246743667, 0.0587964725162, 0.0533492350686, 0.0470805347033, 0.0404101851911, 0.0337346772335, 0.0273904105715, 0.0216300134521, 0.0166131237937, 0.0124102942088, 0.00901673109332, 0.006371657499, 0.00437917188719, 0.00292730399695, 0.00190318025692, 0.0012034506512, 0.000740138359388); +CONST_ARRAY float gaussian_weights_20[20] = float[20](0.0631206079092, 0.0623386696286, 0.0600504962028, 0.0564219880656, 0.0517074216853, 0.0462200163444, 0.0402976778174, 0.0342690969121, 0.0284248389542, 0.0229967296489, 0.0181470844488, 0.0139675538565, 0.0104859205472, 0.00767830721424, 0.00548399588371, 0.00382033519528, 0.00259584365953, 0.00172039554551, 0.00111211782068, 0.000701206614755); +CONST_ARRAY float gaussian_weights_21[21] = float[21](0.0599663578227, 0.0592955168529, 0.0573276870732, 0.0541920219987, 0.0500881123762, 0.0452649816716, 0.0399961700994, 0.0345543588197, 0.0291887599519, 0.024107758111, 0.0194682260256, 0.0153717852441, 0.0118672642019, 0.00895788098686, 0.00661132245055, 0.004770893968, 0.00336619727441, 0.00232224340187, 0.00156640581013, 0.00103306870905, 0.000666166061464); +CONST_ARRAY float gaussian_weights_22[22] = float[22](0.057112364552, 0.0565325485975, 0.0548281807346, 0.0521009873998, 0.0485092901538, 0.0442527977069, 0.0395542717215, 0.0346404001495, 0.0297241370428, 0.0249903568146, 0.0205860255043, 0.0166153465249, 0.0131396303393, 0.0101810773885, 0.00772931803295, 0.00574943890287, 0.004190314276, 0.0029922961728, 0.0020936274558, 0.0014352616483, 0.000964050097101, 0.000634461060099); +CONST_ARRAY float gaussian_weights_23[23] = float[23](0.054517701579, 0.0540131711974, 0.0525274224654, 0.0501414375277, 0.0469820267016, 0.0432106689856, 0.0390098705349, 0.034568640811, 0.030068680835, 0.0256726513374, 0.021515495198, 0.0176993063323, 0.0142917503664, 0.0113276252744, 0.00881285454125, 0.00673005489098, 0.00504481084372, 0.00371189295693, 0.00268083618271, 0.00190050661455, 0.0013224911384, 0.000903317516786, 0.000605636958114); +CONST_ARRAY float gaussian_weights_24[24] = float[24](0.0521485573767, 0.0517068310725, 0.050403975608, 0.0483050916261, 0.0455126658702, 0.042158280221, 0.0383923542943, 0.0343730326211, 0.0302553514918, 0.0261816975872, 0.0222743299567, 0.0186304241426, 0.0153197642695, 0.0123849047314, 0.00984338574556, 0.00769143875473, 0.00590856360058, 0.00446238965579, 0.00331332710032, 0.00241864641078, 0.00173576808421, 0.00122467897537, 0.00084950134862, 0.000579318143372); +CONST_ARRAY float gaussian_weights_25[25] = float[25](0.0499767536015, 0.0495878314194, 0.0484391305418, 0.046583456592, 0.0441043302545, 0.0411097561541, 0.0377244327973, 0.0340811848931, 0.0303124333322, 0.0265424534827, 0.0228810251118, 0.0194188737985, 0.0162250763673, 0.0133463837679, 0.0108082309552, 0.00861707301291, 0.0067636181039, 0.00522651897931, 0.00397612596387, 0.00297798074754, 0.0021958253057, 0.00159399822903, 0.00113917916157, 0.000801512644787, 0.000555191582752); +CONST_ARRAY float gaussian_weights_26[26] = float[26](0.0479786188192, 0.0476344133902, 0.0466165426396, 0.0449681953731, 0.0427579639989, 0.0400751110844, 0.0370235971586, 0.033715425763, 0.0302638965903, 0.0267773256187, 0.0233537009481, 0.0200766102918, 0.0170126208304, 0.0142101353977, 0.0116996095611, 0.00949490624815, 0.00759549623064, 0.00598918576442, 0.00465506312071, 0.00356639567308, 0.00269326838853, 0.00200482238421, 0.00147101916547, 0.00106391496797, 0.000758475690917, 0.000532994310372); +CONST_ARRAY float gaussian_weights_27[27] = float[27](0.0461341220677, 0.0458280361329, 0.0449219092487, 0.0434513364077, 0.041473056502, 0.0390613205401, 0.0363032722123, 0.03329374093, 0.0301298793329, 0.0269060634368, 0.0237094185849, 0.0206162474982, 0.0176895305381, 0.0149775565587, 0.0125136384235, 0.0103167809312, 0.00839310755091, 0.00673781900749, 0.00533745056926, 0.00417221196467, 0.00321822819374, 0.00244954419315, 0.00183980458116, 0.00136356575469, 0.000997237148304, 0.000719678922527, 0.000512503802344); +CONST_ARRAY float gaussian_weights_28[28] = float[28](0.0444261989064, 0.0441528077786, 0.0433426874556, 0.0420253835231, 0.0402481455205, 0.0380731143611, 0.0355737191338, 0.0328305743187, 0.0299271960082, 0.0269458517312, 0.0239638252082, 0.021050320394, 0.0182641563909, 0.0156523252517, 0.0132494072776, 0.0110777708249, 0.00914843153174, 0.00746241242714, 0.00601243221213, 0.00478475249866, 0.00376103264156, 0.00292006868614, 0.00223932625116, 0.00169621154636, 0.00126905661972, 0.000937821796457, 0.000684538667784, 0.000493530489859); +CONST_ARRAY float gaussian_weights_29[29] = float[29](0.0428402218449, 0.0425950320572, 0.0418678504625, 0.0406833639509, 0.0390811670302, 0.0371135646233, 0.0348427391927, 0.0323374960065, 0.0296698234919, 0.0269115067302, 0.0241310123361, 0.0213908255587, 0.0187453704346, 0.0162395871695, 0.0139081837241, 0.0117755263835, 0.00985609128793, 0.00815536834958, 0.00667109187858, 0.00539466821111, 0.00431267799915, 0.00340834700336, 0.00266290118508, 0.00205674653671, 0.00157043864968, 0.00118542928527, 0.000884595662595, 0.000652572000374, 0.000475911876172); +CONST_ARRAY float gaussian_weights_30[30] = float[30](0.0413635803064, 0.0411428442631, 0.0404876787473, 0.0394188384355, 0.0379696981566, 0.0361845223744, 0.034116221224, 0.0318237516855, 0.0293693415551, 0.02681571743, 0.0242235064343, 0.02164895686, 0.0191420884567, 0.0167453427128, 0.0144927613074, 0.0124096809627, 0.0105128984936, 0.00881123336238, 0.00730639779605, 0.00599407677732, 0.00486512123495, 0.00390676609731, 0.00310379858835, 0.00243961912893, 0.00189715540038, 0.0014596077497, 0.00111101978377, 0.000836680798136, 0.000623376159936, 0.000459507870433); +CONST_ARRAY float gaussian_weights_31[31] = float[31](0.0399853445765, 0.0397859168385, 0.0391935817056, 0.0382258887254, 0.0369111251945, 0.0352869427376, 0.0333985672178, 0.0312967106632, 0.0290353194613, 0.0266692976067, 0.0242523374248, 0.0218349741136, 0.019462956678, 0.0171759989834, 0.015006943611, 0.0129813407749, 0.0111174172637, 0.00942638810365, 0.00791304767411, 0.00657656782027, 0.00541142793358, 0.00440840524404, 0.00355556151442, 0.00283917353606, 0.00224456783464, 0.00175683343057, 0.00136139920952, 0.00104447357383, 0.000793353010129, 0.000596612772324, 0.000444197054481); +CONST_ARRAY float gaussian_weights_32[32] = float[32](0.0386959947766, 0.038515219631, 0.0379779455684, 0.0370990921929, 0.035902758976, 0.0344211273781, 0.03269302325, 0.0307622290963, 0.0286756484161, 0.0264814290509, 0.0242271492383, 0.0219581595966, 0.0197161578279, 0.017538052295, 0.0154551477556, 0.0134926634575, 0.0116695723433, 0.00999873179996, 0.00848726229109, 0.00713712091343, 0.00594581251708, 0.00490718118467, 0.00401222890115, 0.00324991525467, 0.00260790097568, 0.00207320803451, 0.00163277896075, 0.001273927269, 0.000984678824041, 0.0007540103176, 0.000571995622223, 0.000429873672017); +CONST_ARRAY float gaussian_weights_33[33] = float[33](0.0374872006405, 0.0373228234108, 0.0368340037493, 0.0360334897628, 0.0349419131486, 0.0335869056163, 0.0320019370398, 0.0302249435008, 0.0282968236112, 0.0262598859882, 0.024156329418, 0.0220268304723, 0.0199093019753, 0.0178378709241, 0.0158421076059, 0.0139465201734, 0.0121703121823, 0.0105273857368, 0.00902656082137, 0.00767197268489, 0.00646360400967, 0.0053979069534, 0.00446847163236, 0.00366670165813, 0.00298246326618, 0.00240468165596, 0.00192186570093, 0.00152254956825, 0.00119564651815, 0.000930715882533, 0.000718148744363, 0.000549281085644, 0.000416445182144); +CONST_ARRAY float gaussian_weights_34[34] = float[34](0.0363516413217, 0.036201737859, 0.0357557262315, 0.0350245510305, 0.0340259562937, 0.0327837698447, 0.0313269586531, 0.0296885074803, 0.0279041813374, 0.0260112363621, 0.0240471434912, 0.0220483849925, 0.0200493760572, 0.0180815530273, 0.0161726573904, 0.0143462314376, 0.0126213284443, 0.0110124282972, 0.00952953936303, 0.00817845959286, 0.00696116464401, 0.00587628821513, 0.00491965967439, 0.00408486607427, 0.00336380935028, 0.00274723438686, 0.00222520918402, 0.0017875440918, 0.00142414258053, 0.00112528095772, 0.000881818604268, 0.00068534355663, 0.000528260575697, 0.000403830257603); +CONST_ARRAY float gaussian_weights_35[35] = float[35](0.0352828569912, 0.0351457772102, 0.0347377250654, 0.034068138963, 0.0331523456006, 0.0320109768082, 0.0306691971283, 0.0291557825023, 0.027502097108, 0.025741019002, 0.0239058656123, 0.0220293674457, 0.0201427329382, 0.0182748397359, 0.0164515784956, 0.0146953652717, 0.0130248284398, 0.0114546665776, 0.00999566534574, 0.00865485462404, 0.0074357822241, 0.00633887752729, 0.00536187733782, 0.00450028691203, 0.00374785124891, 0.00309701495804, 0.00253935298702, 0.00206595882394, 0.00166778115643, 0.00133590408651, 0.00106176965372, 0.000837344464398, 0.000655234585795, 0.000508754527564, 0.000391957136174); +CONST_ARRAY float gaussian_weights_36[36] = float[36](0.0342751258684, 0.0341494482206, 0.0337751734808, 0.0331604756171, 0.0323186475622, 0.0312676233073, 0.0300293428847, 0.0286289916303, 0.0270941505287, 0.0254538975676, 0.0237379007598, 0.021975541872, 0.0201951061634, 0.0184230679295, 0.0166834948398, 0.0149975864603, 0.013383354516, 0.0118554448693, 0.0104250943304, 0.00910020962319, 0.0078855513561, 0.00678300282029, 0.00579190187853, 0.0049094140256, 0.00413092573351, 0.00345043921075, 0.0028609524436, 0.00235481157601, 0.00192402606174, 0.00156054035493, 0.00125645900204, 0.00100422471517, 0.000796751251198, 0.000627514647567, 0.000490607571324, 0.00038076225462); +CONST_ARRAY float gaussian_weights_37[37] = float[37](0.0333233617429, 0.0332078562721, 0.0328637365047, 0.0322981096857, 0.0315225495765, 0.030552702685, 0.0294077635218, 0.0281098434571, 0.0266832621385, 0.025153793115, 0.0235478961881, 0.0218919680964, 0.0202116405818, 0.0185311509155, 0.0168728049169, 0.0152565467323, 0.0136996435735, 0.0122164876079, 0.0108185116105, 0.00951421011541, 0.00830925385448, 0.00720668238666, 0.00620715804382, 0.00530926362864, 0.00450982659987, 0.00380425362601, 0.00318686120024, 0.00265119028911, 0.00219029553592, 0.00179700217107, 0.00146412633301, 0.00118465684373, 0.000951898516011, 0.000759578735218, 0.000601920330354, 0.000473684629482, 0.000370189110245); +CONST_ARRAY float gaussian_weights_38[38] = float[38](0.0324230281088, 0.0323166262891, 0.0319995113068, 0.0314778862896, 0.0307618652238, 0.0298651468432, 0.028804578888, 0.02759963209, 0.0262718068088, 0.0248439975284, 0.0233398413379, 0.0217830760655, 0.0201969319937, 0.0186035782268, 0.0170236410318, 0.0154758070976, 0.0139765199563, 0.0125397730562, 0.011176998459, 0.00989704607063, 0.00870624489846, 0.00760853518304, 0.00660565843784, 0.00569739146478, 0.0048818102413, 0.00415557011334, 0.00351418985579, 0.00295232873728, 0.00246404760181, 0.00204304700967, 0.00168287753264, 0.00137711925802, 0.00111952933378, 0.000904157917575, 0.000725434137119, 0.000578224612709, 0.000457867740317, 0.00036018730702); +CONST_ARRAY float gaussian_weights_39[39] = float[39](0.0315700658546, 0.0314718358241, 0.0311789757839, 0.0306969191808, 0.0300345351942, 0.0292038573418, 0.0282197199596, 0.0270993178919, 0.0258617076395, 0.0245272701486, 0.0231171563159, 0.0216527361219, 0.0201550711348, 0.0186444280746, 0.0171398483337, 0.0156587850296, 0.0142168155126, 0.0128274335027, 0.0115019213772, 0.0102492997744, 0.00907634875668, 0.00798769241821, 0.00698593707642, 0.00607185210057, 0.0052445819707, 0.00450187828841, 0.00384034109358, 0.00325565988292, 0.00274284606771, 0.00229645013991, 0.00191075843051, 0.00157996594715, 0.00129832328924, 0.00106025699581, 0.000860463840048, 0.00069398052067, 0.000556230904611, 0.000443053455279, 0.000350711752291); + + + +// Offsets and weights for n = 2, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_2[] = float[2] (0.274068619061, 0.725931380939); +CONST_ARRAY float opt_gaussian_offsets_2[] = float[2] (-1.0, 0.377540668798); + +// Offsets and weights for n = 3, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_3[] = float[3] (0.15246914402, 0.473220416416, 0.374310439564); +CONST_ARRAY float opt_gaussian_offsets_3[] = float[3] (-2.0, -0.468790626626, 1.40733340005); + +// Offsets and weights for n = 4, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_4[] = float[4] (0.106288519406, 0.306091409205, 0.341010215168, 0.24660985622); +CONST_ARRAY float opt_gaussian_offsets_4[] = float[4] (-2.0, -0.458429516783, 1.48611468225, 3.43099866743); + +// Offsets and weights for n = 5, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_5[] = float[5] (0.0816744222584, 0.220481043971, 0.265173712396, 0.249350938326, 0.183319883049); +CONST_ARRAY float opt_gaussian_offsets_5[] = float[5] (-3.0, -1.46101677931, 0.507811864279, 2.47657965106, 4.44552953504); + +// Offsets and weights for n = 6, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_6[] = float[6] (0.0663416657403, 0.170786343946, 0.208182532594, 0.216591983241, 0.192330414616, 0.145767059863); +CONST_ARRAY float opt_gaussian_offsets_6[] = float[6] (-3.0, -1.46505705484, 0.514995501619, 2.49500016666, 4.47502081252, 6.45512110763); + +// Offsets and weights for n = 7, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_7[] = float[7] (0.0558645795815, 0.138837939971, 0.168410194672, 0.182939846093, 0.177962436458, 0.155034498765, 0.12095050446); +CONST_ARRAY float opt_gaussian_offsets_7[] = float[7] (-4.0, -2.46879062663, -0.482645862567, 1.50347216641, 3.48958484011, 5.47571357137, 7.46187967409); + +// Offsets and weights for n = 8, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_8[] = float[8] (0.0482490884887, 0.116731177798, 0.140135626714, 0.155109457758, 0.158290996847, 0.148936931244, 0.12920422877, 0.10334249238); +CONST_ARRAY float opt_gaussian_offsets_8[] = float[8] (-4.0, -2.47196820019, -0.482150445594, 1.50765246363, 3.49744900173, 5.48724766412, 7.47705693918, 9.46688527998); + +// Offsets and weights for n = 9, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_9[] = float[9] (0.042462603473, 0.100587552233, 0.119370554051, 0.133110608252, 0.139473180769, 0.137319230357, 0.127038284604, 0.110433394405, 0.0902045918559); +CONST_ARRAY float opt_gaussian_offsets_9[] = float[9] (-5.0, -3.47463117774, -1.48242911339, 0.509764383426, 2.50195311507, 4.49414089321, 6.48633153138, 8.47852883754, 10.4707366066); + +// Offsets and weights for n = 10, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_10[] = float[10] (0.0379163821476, 0.0883084946127, 0.103630848659, 0.115769751833, 0.12311774987, 0.124642393984, 0.120124182183, 0.110208385676, 0.0962539004871, 0.0800279105479); +CONST_ARRAY float opt_gaussian_offsets_10[] = float[10] (-5.0, -3.47686837586, -1.48303121052, 0.510800788681, 2.50462949733, 4.49845679502, 6.49228456308, 8.48611468225, 10.4799490304, 12.4737894803); + +// Offsets and weights for n = 11, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_11[] = float[11] (0.034250034987, 0.0786680908358, 0.0913650297743, 0.101960743385, 0.109334598331, 0.112655883289, 0.111537726901, 0.106111230043, 0.0970001809665, 0.0852030922382, 0.0719133892488); +CONST_ARRAY float opt_gaussian_offsets_11[] = float[11] (-6.0, -4.47876278503, -2.48375571894, -0.488751898053, 1.5062496745, 3.5012499974, 5.49625007031, 7.49125089312, 9.4862534651, 11.4812587841, 13.4762678459); + +// Offsets and weights for n = 12, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_12[] = float[12] (0.0312305401816, 0.0709054311219, 0.0815774390232, 0.0908099474451, 0.0978069207462, 0.101924493536, 0.10276858031, 0.100257061632, 0.0946329555381, 0.0864256482923, 0.0763687507926, 0.0652922313807); +CONST_ARRAY float opt_gaussian_offsets_12[] = float[12] (-6.0, -4.48038197724, -2.48450909152, -0.488638319781, 1.5072309008, 3.50309913386, 5.49896694362, 7.49483489448, 9.49070355081, 11.4865734766, 13.482445235, 15.4783193884); + +// Offsets and weights for n = 13, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_13[] = float[13] (0.0287005193039, 0.0645245840776, 0.0736085919864, 0.0816749725356, 0.0881468265092, 0.0925297883119, 0.0944742956035, 0.093821628, 0.0906253035588, 0.0851438313916, 0.0778061819627, 0.0691563739381, 0.0597871028205); +CONST_ARRAY float opt_gaussian_offsets_13[] = float[13] (-7.0, -5.48177890584, -3.48524733884, -1.48871719346, 0.507811864279, 2.50434016876, 4.50086805468, 6.49739585688, 8.49392391023, 10.4904525495, 12.4869821093, 14.4835129238, 16.4800453267); + +// Offsets and weights for n = 14, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_14[] = float[14] (0.0265498357372, 0.0591891116629, 0.067008146258, 0.0740882641635, 0.080003184581, 0.084372549444, 0.0869022686967, 0.0874172424738, 0.0858814043683, 0.0824018985422, 0.0772167186324, 0.0706677895493, 0.0631637213247, 0.0551378645661); +CONST_ARRAY float opt_gaussian_offsets_14[] = float[14] (-7.0, -5.482994727, -3.48595044497, -1.48890714597, 0.508135376648, 2.50517732975, 4.50221892034, 6.49926035557, 8.49630184259, 10.4933435885, 12.4903858005, 14.4874286855, 16.4844724502, 18.4815173012); + +// Offsets and weights for n = 15, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_15[] = float[15] (0.0246991001359, 0.0546630307477, 0.0614599000867, 0.0677077117875, 0.0730857269119, 0.0772992275365, 0.0801061552404, 0.0813401134339, 0.0809266963759, 0.0788909166077, 0.0753546997316, 0.0705247954691, 0.0646727718699, 0.0581097839419, 0.0511593701234); +CONST_ARRAY float opt_gaussian_offsets_15[] = float[15] (-8.0, -6.48406152432, -4.48661034495, -2.48915986216, -0.491709943445, 1.5057395438, 3.50318873228, 5.50063775476, 7.49808674403, 9.49553583291, 11.4929851542, 13.4904348406, 15.4878850248, 17.4853358393, 19.4827874166); + +// Offsets and weights for n = 16, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_16[] = float[16] (0.0230896376799, 0.050776085665, 0.0567362408729, 0.0622801542701, 0.0671624585612, 0.0711526823588, 0.074053187201, 0.0757153650032, 0.0760522510849, 0.0750460631044, 0.0727497562674, 0.0692824055002, 0.0648189750606, 0.0595756988869, 0.0537927708459, 0.0477162676376); +CONST_ARRAY float opt_gaussian_offsets_16[] = float[16] (-8.0, -6.48500449838, -4.48722500316, -2.48944601229, -0.491667438186, 1.50611080683, 3.50388881047, 5.50166666049, 7.49944444467, 9.4972222508, 11.4950001667, 13.49277828, 15.4905566786, 17.4883354502, 19.4861146823, 21.4838944625); + +// Offsets and weights for n = 17, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_17[] = float[17] (0.0216771440358, 0.0474024431083, 0.0526696583526, 0.0576157272943, 0.0620500832243, 0.0657906931345, 0.0686763697103, 0.0705782641713, 0.0714094011985, 0.0711312736702, 0.0697568007769, 0.0673493330105, 0.0640178107627, 0.0599085918637, 0.0551948018209, 0.0500642848839, 0.0447073189814); +CONST_ARRAY float opt_gaussian_offsets_17[] = float[17] (-9.0, -7.48584362821, -5.48779539349, -3.489747531, -1.49169998126, 0.506347315253, 2.5043944181, 4.50244138685, 6.50048828109, 8.49853516044, 10.4965820845, 12.4946291128, 14.4926763051, 16.4907237208, 18.4887714194, 20.4868194606, 22.4848679037); + +// Offsets and weights for n = 18, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_18[] = float[18] (0.0204275387753, 0.0444471422165, 0.0491344246582, 0.0535700569458, 0.0576039868414, 0.0610909922542, 0.063899287897, 0.0659187647663, 0.0670681468019, 0.0673004170345, 0.0666060084333, 0.0650134576635, 0.062587459498, 0.0594245067401, 0.0556465251586, 0.0513930883489, 0.0468129040831, 0.0420552918834); +CONST_ARRAY float opt_gaussian_offsets_18[] = float[18] (-9.0, -7.48659490868, -5.48832392241, -3.49005321559, -1.49178274685, 0.506487525174, 2.50475764187, 4.50302764466, 6.50129757494, 8.49956747416, 10.4978373837, 12.4961073451, 14.4943773996, 16.4926475888, 18.490917954, 20.4891885367, 22.487459378, 24.4857305195); + +// Offsets and weights for n = 19, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_19[] = float[19] (0.0193141765234, 0.0418372042117, 0.0460345037418, 0.0500318718292, 0.0537096734822, 0.0569509205974, 0.0596473922603, 0.0617056100684, 0.0630522103997, 0.063638283716, 0.0634423231446, 0.0624715344916, 0.0607613964306, 0.058373508293, 0.0553919075004, 0.0519181635644, 0.0480656477248, 0.0439534278952, 0.0397002441254); +CONST_ARRAY float opt_gaussian_offsets_19[] = float[19] (-10.0, -8.48727126934, -6.48881359538, -4.49035613443, -2.49189885715, -0.493441734166, 1.50501526389, 3.50347216641, 5.50192900278, 7.50038580239, 9.49884259466, 11.497299409, 13.4957562747, 15.4942132214, 17.4926702782, 19.4911274747, 21.4895848401, 23.4880424039, 25.4865001954); + +// Offsets and weights for n = 20, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_20[] = float[20] (0.0183159239212, 0.0395156384342, 0.0432953808504, 0.0469143095749, 0.0502759514807, 0.0532851842709, 0.0558526585709, 0.0578991827119, 0.0593597716547, 0.0601870718495, 0.0603539103928, 0.0598547770814, 0.0587061266098, 0.0569454777934, 0.0546293785999, 0.0518303909461, 0.0486333194666, 0.0451309573637, 0.0414196460388, 0.0375949423883); +CONST_ARRAY float opt_gaussian_offsets_20[] = float[20] (-10.0, -8.48788325916, -6.48926757672, -4.4906520589, -2.4920366845, -0.493421432277, 1.50519371901, 3.50380879059, 5.50242380373, 7.50103877967, 9.49965373967, 11.498268705, 13.4968836969, 15.4954987366, 17.4941138453, 19.4927290444, 21.4913443551, 23.4899597986, 25.488575396, 27.4871911687); + +// Offsets and weights for n = 21, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_21[] = float[21] (0.017415804187, 0.0374373032386, 0.0408584208729, 0.0441487454036, 0.0472296714025, 0.050023174672, 0.0524550548792, 0.0544581909425, 0.0559756111883, 0.0569631828342, 0.0573917437838, 0.0572485330749, 0.0565378220739, 0.0552807028518, 0.0535140483716, 0.0512887160172, 0.0486671165854, 0.0457203108183, 0.0425248216691, 0.0391593610009, 0.0357016641323); +CONST_ARRAY float opt_gaussian_offsets_21[] = float[21] (-11.0, -9.48843956064, -7.48968896203, -5.49093849226, -3.49218813572, -1.49343787681, 0.505312300099, 2.50406241061, 4.50281247034, 6.50156249491, 8.50031249996, 10.4990625011, 12.497812514, 14.4965625542, 16.4953126373, 18.4940627791, 20.492812995, 22.4915633008, 24.490313712, 26.4890642443, 28.4878149131); + +// Offsets and weights for n = 22, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_22[] = float[22] (0.0166000229565, 0.0355659864281, 0.0386768958381, 0.0416803556939, 0.044511709669, 0.047106429585, 0.0494025259225, 0.0513429921806, 0.0528781496068, 0.0539677581387, 0.0545827685096, 0.0547066090713, 0.0543359277732, 0.0534807429267, 0.05216399329, 0.0504205156771, 0.0482955136927, 0.0458426115287, 0.0431216096972, 0.040196073531, 0.0371308894807, 0.0339899188026); +CONST_ARRAY float opt_gaussian_offsets_22[] = float[22] (-11.0, -9.48894737902, -7.49008066671, -5.49121405638, -3.49234753637, -1.49348109503, 0.505385279274, 2.50425159821, 4.50311787342, 6.50198411657, 8.50085033932, 10.4997165533, 12.4985827702, 14.4974490017, 16.4963152595, 18.4951815551, 20.4940479002, 22.4929143066, 24.4917807858, 26.4906473495, 28.4895140094, 30.488380777); + +// Offsets and weights for n = 23, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_23[] = float[23] (0.0158572558637, 0.0338723065065, 0.0367131374703, 0.0394648878577, 0.0420739114357, 0.0444864291153, 0.0466503437274, 0.0485170953621, 0.0500434658737, 0.0511932393217, 0.0519386294298, 0.0522613955547, 0.0521535845943, 0.0516178567066, 0.0506673762143, 0.0493252739403, 0.0476237116354, 0.0456026013273, 0.0433080507681, 0.0407906194304, 0.0381034769055, 0.0353005567664, 0.0324347941928); +CONST_ARRAY float opt_gaussian_offsets_23[] = float[23] (-12.0, -10.4894127398, -8.49044537813, -6.49147809805, -4.49251089071, -2.49354374729, -0.494576658982, 1.50439038303, 3.50335738756, 5.50232436342, 7.50129131944, 9.50025826444, 11.4992252072, 13.4981921566, 15.4971591215, 17.4961261106, 19.4950931327, 21.4940601968, 23.4930273116, 25.4919944858, 27.4909617284, 29.4899290482, 31.4888964539); + +// Offsets and weights for n = 24, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_24[] = float[24] (0.0151781193223, 0.0323321780903, 0.0349364627792, 0.0374662789601, 0.0398767590653, 0.0421227588364, 0.0441602397335, 0.0459476917322, 0.0474475330031, 0.0486274208977, 0.0494614105199, 0.0499309029722, 0.0500253348908, 0.0497425735707, 0.0490889970417, 0.0480792548951, 0.0467357223884, 0.0450876762325, 0.0431702344581, 0.0410231139437, 0.0386892669072, 0.0362134615119, 0.0336408715991, 0.0310157366487); +CONST_ARRAY float opt_gaussian_offsets_24[] = float[24] (-12.0, -10.4898407179, -8.49078554242, -6.49173043279, -4.49267538225, -2.49362038404, -0.494565431416, 1.50448948237, 3.50354436407, 5.50259922044, 7.50165405824, 9.50070888421, 11.4997637051, 13.4988185277, 15.4978733588, 17.496928205, 19.4959830732, 21.4950379701, 23.4940929025, 25.493147877, 27.4922029006, 29.4912579798, 31.4903131215, 33.4893683324); + +// Offsets and weights for n = 25, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_25[] = float[25] (0.0145547717977, 0.0309256724585, 0.0333216402417, 0.0356548788588, 0.0378875858988, 0.0399816099167, 0.0418995150216, 0.0436056826279, 0.0450674056297, 0.0462559283339, 0.0471473860962, 0.047723601823, 0.0479727022219, 0.0478895246213, 0.0474757948889, 0.0467400678857, 0.0456974333241, 0.0443690011569, 0.04278119101, 0.0409648590605, 0.0389543026351, 0.0367861873003, 0.0344984431302, 0.032129176157, 0.0297156379033); +CONST_ARRAY float opt_gaussian_offsets_25[] = float[25] (-13.0, -11.4902356166, -9.49110336963, -7.49197117629, -5.49283903134, -3.49370692955, -1.49457486569, 0.504557165471, 2.50368916916, 4.50282115062, 6.50195311507, 8.50108506774, 10.5002170139, 12.4993489587, 14.4984809075, 16.4976128654, 18.4967448377, 20.4958768296, 22.4950088463, 24.4941408932, 26.4932729754, 28.4924050981, 30.4915372666, 32.4906694861, 34.4898017618); + +// Offsets and weights for n = 26, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_26[] = float[26] (0.0139806095106, 0.0296361596261, 0.0318477401908, 0.0340061091799, 0.0360791996382, 0.0380345665842, 0.0398402151893, 0.0414654617847, 0.0428817957719, 0.0440637088489, 0.0449894579767, 0.0456417302531, 0.0460081813069, 0.0460818238265, 0.0458612491398, 0.0453506720284, 0.0445597967842, 0.0435035104568, 0.0422014168346, 0.0406772315419, 0.0389580643162, 0.0370736187825, 0.0350553426473, 0.0329355621081, 0.0307466334336, 0.028520142239); +CONST_ARRAY float opt_gaussian_offsets_26[] = float[26] (-13.0, -11.4906011073, -9.49140084797, -7.49220063267, -5.4930004573, -3.49380031775, -1.49460020994, 0.504599870223, 2.50379992684, 4.502999964, 6.5021999858, 8.50139999634, 10.5005999997, 12.4998, 14.4990000013, 16.4982000078, 18.4974000234, 20.4966000524, 22.4958000988, 24.4950001667, 26.4942002601, 28.4934003833, 30.4926005403, 32.4918007351, 34.4910009719, 36.4902012547); + +// Offsets and weights for n = 27, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_27[] = float[27] (0.0134500314681, 0.028449654128, 0.0304972623514, 0.0324994402414, 0.0344288143118, 0.0362576299487, 0.0379584017792, 0.0395045923815, 0.040871296279, 0.0420359047813, 0.0429787269811, 0.0436835431173, 0.0441380685956, 0.0443343101221, 0.0442687995259, 0.0439426957131, 0.043361750565, 0.0425361401822, 0.0414801683926, 0.0402118545935, 0.0387524225201, 0.0371257101947, 0.0353575239501, 0.0334749609143, 0.0315057246655, 0.0294774579359, 0.0274171143604); +CONST_ARRAY float opt_gaussian_offsets_27[] = float[27] (-14.0, -12.4909403408, -10.4916797622, -8.49241922001, -6.493158711, -4.49389823193, -2.49463777955, -0.495377350649, 1.50388305803, 3.50314344971, 5.50240382763, 7.50166419504, 9.50092455516, 11.5001849112, 13.4994452665, 15.4987056242, 17.4979659876, 19.4972263598, 21.4964867442, 23.495747144, 25.4950075624, 27.4942680026, 29.4935284679, 31.4927889616, 33.4920494867, 35.4913100467, 37.4905706447); + +// Offsets and weights for n = 28, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_28[] = float[28] (0.0129582560674, 0.0273543103165, 0.0292554660583, 0.0311176025635, 0.0329172158359, 0.0346304326621, 0.0362335258515, 0.0377034536229, 0.0390184062889, 0.0401583422803, 0.0411054951919, 0.0418448339636, 0.0423644595612, 0.0426559235313, 0.0427144565149, 0.0425390980829, 0.0421327229529, 0.0415019625886, 0.0406570251689, 0.0396114207574, 0.0383816020104, 0.0369865337755, 0.0354472073009, 0.0337861164192, 0.0320267139071, 0.030192866258, 0.0283083243587, 0.0263962261095); +CONST_ARRAY float opt_gaussian_offsets_28[] = float[28] (-14.0, -12.4912560356, -10.4919417129, -8.49262742053, -6.4933131559, -4.49399891644, -2.49468469955, -0.495370502671, 1.50394367679, 3.50325784141, 5.50257199378, 7.50188613646, 9.50120027204, 11.5005144031, 13.4998285322, 15.499142662, 17.498456795, 19.4977709338, 21.497085081, 23.4963992392, 25.4957134109, 27.4950275988, 29.4943418053, 31.4936560332, 33.4929702849, 35.4922845631, 37.4915988703, 39.4909132091); + +// Offsets and weights for n = 29, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_29[] = float[29] (0.0125011765317, 0.0263400288238, 0.0281098505852, 0.0298459733379, 0.0315281054522, 0.0331356062206, 0.0346478974098, 0.036044895224, 0.0373074502411, 0.0384177819917, 0.0393598944759, 0.0401199590865, 0.040686652145, 0.0410514355414, 0.0412087707617, 0.0411562588012, 0.0408947010275, 0.0404280788327, 0.039763452797, 0.0389107849351, 0.037882690277, 0.0366941264423, 0.0353620318792, 0.0339049249866, 0.03234247735, 0.030695074782, 0.0289833797506, 0.0272279081406, 0.0254486321699); +CONST_ARRAY float opt_gaussian_offsets_29[] = float[29] (-15.0, -13.4915505493, -11.4921881357, -9.49282574751, -7.49346338263, -5.49410103902, -3.49473871461, -1.49537640731, 0.503985884951, 2.50334816424, 4.50271043263, 6.50207269221, 8.50143494504, 10.5007971932, 12.5001594388, 14.4995216838, 16.4988839304, 18.4982461807, 20.4976084366, 22.4969707003, 24.4963329739, 26.4956952594, 28.4950575589, 30.4944198745, 32.4937822083, 34.4931445623, 36.4925069386, 38.4918693392, 40.4912317663); + +// Offsets and weights for n = 30, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_30[] = float[30] (0.0120752459112, 0.0253981466755, 0.0270497479603, 0.0286720947252, 0.0302475794896, 0.031758269856, 0.0331862397737, 0.0345139179849, 0.0357244443761, 0.0368020242498, 0.0377322701791, 0.038502521139, 0.0391021290372, 0.0395227035884, 0.0397583076624, 0.0398055967488, 0.0396638979611, 0.0393352259681, 0.0388242353293, 0.0381381108078, 0.0372863992743, 0.0362807887003, 0.0351348413894, 0.033863689954, 0.0324837055512, 0.0310121485206, 0.0294668117938, 0.0278656672835, 0.0262265249262, 0.0245667131829); +CONST_ARRAY float opt_gaussian_offsets_30[] = float[30] (-15.0, -13.4918259364, -11.4924203191, -9.49301472323, -7.49360914711, -5.49420358906, -3.49479804739, -1.49539252043, 0.504012993496, 2.50341849608, 4.502823989, 6.50222947393, 8.50163495256, 10.5010404266, 12.5004458976, 14.4998513674, 16.4992568376, 18.49866231, 20.4980677861, 22.4974732676, 24.4968787564, 26.4962842539, 28.4956897619, 30.4950952822, 32.4945008163, 34.4939063659, 36.4933119328, 38.4927175186, 40.492123125, 42.4915287536); + +// Offsets and weights for n = 31, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_31[] = float[31] (0.0116773848279, 0.0245211910437, 0.0260660009978, 0.0275852924479, 0.0290637143118, 0.0304856161901, 0.0318353169301, 0.0330973876259, 0.0342569420705, 0.0352999271142, 0.0362134050695, 0.0369858202625, 0.0376072420704, 0.0380695773066, 0.0383667456063, 0.0384948125006, 0.038452076108, 0.0382391047728, 0.0378587244857, 0.0373159564788, 0.0366179069173, 0.0357736120795, 0.0347938437368, 0.0336908805939, 0.0324782525631, 0.0311704653064, 0.0297827128626, 0.0283305862735, 0.0268297859433, 0.0252958450278, 0.0237438704745); +CONST_ARRAY float opt_gaussian_offsets_31[] = float[31] (-16.0, -14.4920839948, -12.4926394207, -10.4931948647, -8.4937503255, -6.49430580175, -4.49486129205, -2.49541679504, -0.495972309343, 1.50347216641, 3.50291663358, 5.50236109356, 7.50180554771, 9.5012499974, 11.500694444, 13.5001388889, 15.4995833334, 17.499027779, 19.498472227, 21.4979166787, 23.4973611356, 25.496805599, 27.4962500703, 29.4956945509, 31.495139042, 33.4945835452, 35.4940280618, 37.4934725931, 39.4929171405, 41.4923617054, 43.4918062891); + +// Offsets and weights for n = 32, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_32[] = float[32] (0.011304906882, 0.0237026819171, 0.0251507065416, 0.0265763713896, 0.0279662329755, 0.0293065739824, 0.0305836224684, 0.0317837831985, 0.0328938757947, 0.033901373955, 0.0347946397202, 0.0355631466938, 0.0361976862432, 0.0366905510441, 0.0370356908544, 0.0372288361197, 0.037267585878, 0.037151457434, 0.0368818963668, 0.0364622465804, 0.0358976812639, 0.0351950967552, 0.0343629723527, 0.0334112000569, 0.0323508890215, 0.0311941501124, 0.0299538664055, 0.0286434556815, 0.0272766310021, 0.0258671652733, 0.0244286653474, 0.0229743606882); +CONST_ARRAY float opt_gaussian_offsets_32[] = float[32] (-16.0, -14.492326305, -12.4928464819, -10.4933666743, -8.49388688111, -6.49440710113, -4.49492733326, -2.49544757637, -0.495967829348, 1.50351190895, 3.50299163964, 5.50247136385, 7.50195108271, 9.50143079734, 11.5009105089, 13.5003902184, 15.4998699272, 17.4993496362, 19.4988293466, 21.4983090595, 23.4977887761, 25.4972684975, 27.4967482248, 29.4962279592, 31.4957077017, 33.4951874535, 35.4946672158, 37.4941469895, 39.493626776, 41.4931065762, 43.4925863914, 45.4920662227); + +// Offsets and weights for n = 33, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_33[] = float[33] (0.010955457899, 0.022936972726, 0.0242970090863, 0.0256373708328, 0.0269462357952, 0.0282115324444, 0.0294211199518, 0.0305629784405, 0.0316254053597, 0.0325972135674, 0.0334679264771, 0.0342279655323, 0.0348688253347, 0.0353832319562, 0.0357652803168, 0.0360105470026, 0.0361161755112, 0.0360809316269, 0.0359052274243, 0.0355911132491, 0.035142237893, 0.0345637780447, 0.0338623389239, 0.0330458287657, 0.0321233104888, 0.0311048344353, 0.0300012564944, 0.0288240461997, 0.0275850895258, 0.0262964910892, 0.0249703803037, 0.0236187257427, 0.0222531615586); +CONST_ARRAY float opt_gaussian_offsets_33[] = float[33] (-17.0, -15.4925542614, -13.4930424413, -11.4935306345, -9.49401884001, -7.49450705693, -5.49499528434, -3.49548352128, -1.49597176684, 0.503539979913, 2.50305171992, 4.5025634541, 6.5020751834, 8.50158690873, 10.501098631, 12.5006103513, 14.5001220703, 16.4996337891, 18.4991455086, 20.4986572298, 22.4981689535, 24.4976806807, 26.4971924123, 28.4967041493, 30.4962158926, 32.495727643, 34.4952394017, 36.4947511694, 38.4942629471, 40.4937747358, 42.4932865363, 44.4927983496, 46.4923101767); + +// Offsets and weights for n = 34, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_34[] = float[34] (0.0106269661092, 0.0222191206788, 0.0234989336619, 0.0247613662628, 0.0259959812184, 0.0271921148783, 0.0283390259971, 0.029426053101, 0.0304427772918, 0.031379187067, 0.03222584155, 0.03297402843, 0.0336159129307, 0.0341446742532, 0.0345546261735, 0.0348413188147, 0.0350016190478, 0.0350337674884, 0.0349374106417, 0.0347136073722, 0.0343648095349, 0.0338948172612, 0.0333087100411, 0.0326127553496, 0.0318142971136, 0.0309216267959, 0.0299438402574, 0.0288906838477, 0.0277723933565, 0.0265995295291, 0.025382813813, 0.0241329678608, 0.0228605600791, 0.0215758621912); +CONST_ARRAY float opt_gaussian_offsets_34[] = float[34] (-17.0, -15.4927690992, -13.493228146, -11.4936872042, -9.49414627298, -7.49460535168, -5.49506443947, -3.49552353559, -1.49598263925, 0.503558250306, 2.50309913386, 4.5026400122, 6.50218088608, 8.50172175628, 10.5012626236, 12.5008034887, 14.5003443526, 16.4998852158, 18.4994260792, 20.4989669436, 22.4985078098, 24.4980486784, 26.4975895504, 28.4971304264, 30.4966713072, 32.4962121937, 34.4957530865, 36.4952939865, 38.4948348945, 40.4943758111, 42.4939167373, 44.4934576737, 46.4929986211, 48.4925395803); + +// Offsets and weights for n = 35, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_35[] = float[35] (0.0103176010324, 0.0215447805499, 0.0227512495695, 0.0239423078106, 0.025108706565, 0.0262409919812, 0.0273296287144, 0.0283651308048, 0.0293381973431, 0.0302398502594, 0.0310615714112, 0.0317954360651, 0.0324342398575, 0.0329716163978, 0.0334021428322, 0.0337214309223, 0.033926201499, 0.034014340527, 0.0339849354379, 0.0338382908596, 0.0335759233627, 0.0332005353504, 0.0327159687203, 0.0321271394105, 0.0314399543874, 0.0306612130365, 0.0297984952565, 0.0288600388309, 0.027854608845, 0.0267913620359, 0.0256797089935, 0.0245291770894, 0.0233492768839, 0.0221493745731, 0.0209385727842); +CONST_ARRAY float opt_gaussian_offsets_35[] = float[35] (-18.0, -16.4929719162, -14.4934043618, -12.4938368174, -10.4942692821, -8.49470175541, -6.49513423665, -4.49556672518, -2.49599922033, -0.49643172148, 1.50313577204, 3.50270326086, 5.50227074563, 7.50183822701, 9.50140570564, 11.5009731822, 13.5005406572, 15.5001081315, 17.4996756056, 19.4992430802, 21.4988105559, 23.4983780334, 25.4979455133, 27.4975129963, 29.497080483, 31.4966479741, 33.4962154702, 35.495782972, 37.49535048, 39.4949179951, 41.4944855177, 43.4940530486, 45.4936205884, 47.4931881378, 49.4927556973); + +// Offsets and weights for n = 36, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_36[] = float[36] (0.0100257393422, 0.0209101171194, 0.0220493585518, 0.0231748877373, 0.0242784805505, 0.0253517271725, 0.0263861353948, 0.0273732400312, 0.0283047165243, 0.0291724966555, 0.029968884136, 0.0306866677818, 0.0313192299543, 0.0318606479935, 0.0323057864723, 0.0326503782638, 0.0328910926329, 0.0330255888321, 0.0330525539969, 0.0329717244812, 0.0327838901489, 0.0324908815237, 0.0320955400894, 0.0316016724148, 0.031013989138, 0.0303380301802, 0.0295800778452, 0.028747059714, 0.027846443428, 0.0268861255931, 0.0258743171068, 0.0248194272242, 0.0237299486283, 0.02261434567, 0.0214809477837, 0.0203378498874); +CONST_ARRAY float opt_gaussian_offsets_36[] = float[36] (-18.0, -16.4931636913, -14.4935717828, -12.4939798828, -10.4943879908, -8.49479610628, -6.49520422871, -4.49561235753, -2.4960204922, -0.496428632166, 1.5031632231, 3.50275507416, 5.50234692154, 7.50193876579, 9.50153060746, 11.5011224471, 13.5007142852, 15.5003061224, 17.4998979592, 19.4994897961, 21.4990816337, 23.4986734725, 25.4982653131, 27.497857156, 29.4974490017, 31.4970408509, 33.496632704, 35.4962245616, 37.4958164242, 39.4954082924, 41.4950001667, 43.4945920476, 45.4941839358, 47.4937758317, 49.493367736, 51.492959649); + +// Offsets and weights for n = 37, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_37[] = float[37] (0.00974993636734, 0.0203117325559, 0.0213892024529, 0.0224544310942, 0.0235000813023, 0.0245186478884, 0.0255025443957, 0.0264441950142, 0.0273361301596, 0.0281710840642, 0.0289420926191, 0.0296425896425, 0.0302664997198, 0.0308083257886, 0.0312632297024, 0.0316271041272, 0.0318966342731, 0.032069348168, 0.0321436544031, 0.0321188665476, 0.031995213706, 0.0317738369926, 0.0314567719985, 0.0310469176281, 0.0305479919715, 0.0299644761535, 0.0293015473444, 0.0285650023323, 0.0277611732343, 0.0268968370582, 0.0259791209177, 0.0250154047456, 0.0240132233523, 0.0229801696268, 0.0219238005913, 0.0208515478943, 0.0197706341665); +CONST_ARRAY float opt_gaussian_offsets_37[] = float[37] (-19.0, -17.4933453004, -15.4937310384, -13.4941167839, -11.4945025364, -9.49488829538, -7.4952740605, -5.49565983124, -3.49604560714, -1.49643138776, 0.503182827378, 2.50279703872, 4.50241124674, 6.50202545188, 8.50163965462, 10.5012538554, 12.5008680547, 14.5004822529, 16.5000964506, 18.4997106482, 20.4993248461, 22.4989390448, 24.4985532448, 26.4981674465, 28.4977816504, 30.4973958569, 32.4970100665, 34.4966242797, 36.4962384969, 38.4958527186, 40.4954669452, 42.4950811772, 44.4946954151, 46.4943096593, 48.4939239102, 50.4935381684, 52.4931524343); + +// Offsets and weights for n = 38, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_38[] = float[38] (0.00948890217078, 0.0197466058518, 0.0207671865319, 0.0217768050027, 0.0227688949424, 0.0237367380186, 0.0246735370734, 0.0255724936997, 0.0264268890116, 0.0272301662959, 0.0279760141433, 0.0286584485977, 0.0292718928381, 0.0298112529127, 0.030271988092, 0.030650174481, 0.030942560647, 0.0311466141591, 0.0312605581081, 0.0312833968702, 0.0312149305926, 0.0310557581051, 0.0308072681993, 0.0304716194529, 0.0300517090085, 0.0295511309394, 0.028974125038, 0.0283255170485, 0.0276106515206, 0.0268353185907, 0.0260056760908, 0.025128168447, 0.0242094438539, 0.0232562712038, 0.0222754582054, 0.0212737720531, 0.0202578639043, 0.0192341982982); +CONST_ARRAY float opt_gaussian_offsets_38[] = float[38] (-19.0, -17.4935175291, -15.4938827012, -13.4942478798, -11.4946130645, -9.49497825506, -7.49534345092, -5.49570865176, -3.49607385718, -1.49643906678, 0.503195719814, 2.502830503, 4.50246528316, 6.5021000607, 8.50173483599, 10.5013696094, 12.5010043814, 14.5006391523, 16.5002739225, 18.4999086925, 20.4995434625, 22.499178233, 24.4988130044, 26.4984477771, 28.4980825514, 30.4977173278, 32.4973521066, 34.4969868882, 36.496621673, 38.4962564615, 40.4958912539, 42.4955260507, 44.4951608523, 46.4947956591, 48.4944304714, 50.4940652897, 52.4937001143, 54.4933349456); + +// Offsets and weights for n = 39, relative to pixel center +CONST_ARRAY float opt_gaussian_weights_39[] = float[39] (0.00924148137305, 0.0192120420422, 0.0201801154313, 0.0211383429813, 0.0220808308543, 0.023001547634, 0.0238943863517, 0.0247532302506, 0.0255720213328, 0.0263448306384, 0.0270659291345, 0.0277298580395, 0.0283314973845, 0.0288661316105, 0.0293295110303, 0.0297179080354, 0.0300281670083, 0.0302577470078, 0.0304047564176, 0.030467978898, 0.0304468901444, 0.0303416651267, 0.0301531756723, 0.0298829784382, 0.0295332935044, 0.029106974001, 0.0286074673487, 0.0280387688511, 0.027405368511, 0.0267121920605, 0.0259645372878, 0.0251680068087, 0.0243284384733, 0.0234518346107, 0.0225442913003, 0.0216119288229, 0.0206608243792, 0.0196969480813, 0.0187261031213); +CONST_ARRAY float opt_gaussian_offsets_39[] = float[39] (-20.0, -18.4936810844, -16.4940272924, -14.4943735062, -12.4947197254, -10.4950659496, -8.49541217862, -6.495758412, -4.49610464945, -2.49645089063, -0.496797135222, 1.50285661712, 3.50251036672, 5.50216411391, 7.50181785903, 9.5014716024, 11.5011253444, 13.5007790852, 15.5004328254, 17.5000865651, 19.4997403047, 21.4993940446, 23.4990477851, 25.4987015265, 27.4983552691, 29.4980090133, 31.4976627594, 33.4973165078, 35.4969702587, 37.4966240125, 39.4962777696, 41.4959315302, 43.4955852948, 45.4952390636, 47.4948928369, 49.4945466151, 51.4942003986, 53.4938541877, 55.4935079826); diff --git a/RenderPipelineFile/rpcore/shader/includes/gbuffer.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/gbuffer.inc.glsl index 3b08aeaa..fd29abef 100644 --- a/RenderPipelineFile/rpcore/shader/includes/gbuffer.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/gbuffer.inc.glsl @@ -1,307 +1,307 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -#pragma include "includes/material.inc.glsl" -#pragma include "includes/normal_packing.inc.glsl" -#pragma include "includes/brdf.inc.glsl" - -uniform mat4 p3d_ProjectionMatrix; - -#if IN_GBUFFER_SHADER - - /* - - GBuffer Packing - - */ - - layout(location = 0) out vec4 gbuffer_out_0; - layout(location = 1) out vec4 gbuffer_out_1; - layout(location = 2) out vec4 gbuffer_out_2; - - vec2 compute_velocity() { - // Compute velocity based on this and last frames mvp matrix - vec4 last_proj_pos = vOutput.last_proj_position; - vec2 last_texcoord = fma(last_proj_pos.xy / last_proj_pos.w, vec2(0.5), vec2(0.5)); - vec2 curr_texcoord = gl_FragCoord.xy / SCREEN_SIZE; - return (curr_texcoord - last_texcoord); - } - - // Lean mapping - float adjust_roughness(float roughness, float avg_normal_length) { - // Based on The Order : 1886 SIGGRAPH course notes implementation - if (avg_normal_length < 1.0) - { - float avg_len_sq = avg_normal_length * avg_normal_length; - float kappa = (3 * avg_normal_length - avg_normal_length * avg_len_sq) / - (1 - avg_len_sq); - float variance = 1.0 / (2.0 * kappa) ; - return sqrt(roughness * roughness + variance); - } - return roughness; - } - - - void render_material(MaterialShaderOutput m) { - - // Compute material properties - vec3 normal = normalize(m.normal); - vec2 packed_normal = pack_normal_octahedron(normal); - vec2 velocity = compute_velocity(); - - // Clamp BaseColor, but only for negative values, we allow values > 1.0 - // vec3 basecolor = pow(max(vec3(0), m.basecolor), vec3(2.2)) * 1.0; - vec3 basecolor = max(vec3(0), m.basecolor); - - // Clamp properties like specular and metallic, which have to be in the - // 0 ... 1 range - float specular = clamp(m.specular_ior, 1.0001, 2.51); - float metallic = saturate(m.metallic); - float roughness = clamp(m.roughness, 0.03, 1.0); - - roughness = adjust_roughness(roughness, length(m.normal)); - - // Pack all values to the gbuffer - gbuffer_out_0 = vec4(basecolor.r, basecolor.g, basecolor.b, roughness); - gbuffer_out_1 = vec4(packed_normal.x, packed_normal.y, metallic, specular); - gbuffer_out_2 = vec4(velocity.x, velocity.y, m.shading_model, m.shading_model_param0); - } - - -#else // IN_GBUFFER_SHADER - - - /* - - GBuffer - Unpacking - - */ - - #pragma include "includes/transforms.inc.glsl" - - // Common gbuffer data - struct GBufferData { - sampler2D Depth; - sampler2D Data0; - sampler2D Data1; - sampler2D Data2; - }; - - // Checks whether the given material is the skybox - bool is_skybox(vec3 pos, vec3 camera_pos) { - #if REFERENCE_MODE - return distance(pos, camera_pos) > 1000.0; - #endif - return distance(pos, camera_pos) > 20000.0; - } - - bool is_skybox(Material m, vec3 camera_pos) { - return is_skybox(m.position, camera_pos); - } - - bool is_skybox(Material m) { return is_skybox(m, MainSceneData.camera_pos); } - bool is_skybox(vec3 pos) { return is_skybox(pos, MainSceneData.camera_pos); } - - // Returns the depth at a given texcoord - float get_gbuffer_depth(GBufferData data, vec2 coord) { - return textureLod(data.Depth, coord, 0).x; - } - - // Returns the depth at a given texcoord - float get_gbuffer_depth(GBufferData data, ivec2 coord) { - return texelFetch(data.Depth, coord, 0).x; - } - - // Returns the world space position at a given texcoord - vec3 get_gbuffer_position(GBufferData data, vec2 coord) { - float depth = get_gbuffer_depth(data, coord); - return calculate_surface_pos(depth, coord); - } - - // Returns the world space normal at a given texcoord - vec3 get_gbuffer_normal(GBufferData data, vec2 coord) { - vec2 packed_normal = textureLod(data.Data1, coord, 0).xy; - return unpack_normal_octahedron(packed_normal); - } - // Returns the world space normal at a given texcoord - vec3 get_gbuffer_normal(GBufferData data, ivec2 coord) { - vec2 packed_normal = texelFetch(data.Data1, coord, 0).xy; - return unpack_normal_octahedron(packed_normal); - } - - // Returns the velocity at a given coordinate - vec2 get_gbuffer_object_velocity(GBufferData data, vec2 coord) { - return textureLod(data.Data2, coord, 0).xy; - } - - // Returns the velocity at a given coordinate - vec2 get_gbuffer_object_velocity(GBufferData data, ivec2 coord) { - return texelFetch(data.Data2, coord, 0).xy; - } - - int get_gbuffer_shading_model(GBufferData data, vec2 coord) { - return int(textureLod(data.Data2, coord, 0).z); - } - - float get_gbuffer_roughness(GBufferData data, vec2 coord) { - // XXX: take clearcoat into account - return square(clamp(textureLod(data.Data0, coord, 0).w, MINIMUM_ROUGHNESS, 1.0)); - } - - float get_gbuffer_roughness(GBufferData data, ivec2 coord) { - // XXX: take clearcoat into account - return square(clamp(texelFetch(data.Data0, coord, 0).w, MINIMUM_ROUGHNESS, 1.0)); - } - - // Unpacks a material from the gbuffer - Material unpack_material(GBufferData data, vec2 fcoord) { - - // Fetch data from data-textures - vec4 data0 = textureLod(data.Data0, fcoord, 0); - vec4 data1 = textureLod(data.Data1, fcoord, 0); - vec4 data2 = textureLod(data.Data2, fcoord, 0); - - Material m; - m.position = get_gbuffer_position(data, fcoord); - m.basecolor = data0.xyz; - m.linear_roughness = clamp(data0.w, MINIMUM_ROUGHNESS, 1.0); - m.roughness = m.linear_roughness * m.linear_roughness; - m.normal = unpack_normal_octahedron(data1.xy); - m.metallic = saturate(data1.z * 1.001 - 0.0005); - m.specular_ior = data1.w; - m.specular = ior_to_specular(data1.w); - m.shading_model = int(data2.z); - m.shading_model_param0 = data2.w; - - // Velocity, not stored in the Material struct but stored in the G-Buffer - // vec2 velocity = data2.xy; - return m; - } - - // Unpacks a material from the gbuffer - Material unpack_material(GBufferData data) { - vec2 fcoord = get_texcoord(); - return unpack_material(data, fcoord); - } - - #ifdef USE_GBUFFER_EXTENSIONS - - /* - - GBuffer extensions for reading gbuffer values without having to specify - the gbuffer. - - */ - - uniform GBufferData GBuffer; - - // Returns the depth at a given texcoord - float get_depth_at(vec2 coord) { - return get_gbuffer_depth(GBuffer, coord); - } - // Returns the depth at a given texcoord - float get_depth_at(ivec2 coord) { - return get_gbuffer_depth(GBuffer, coord); - } - - // Returns the view space position at a given texcoord - vec3 get_view_pos_at(vec2 coord) { - return calculate_view_pos(get_depth_at(coord), coord); - } - - - // Returns the world space position at a given texcoord - vec3 get_world_pos_at(vec2 coord) { - return calculate_surface_pos(get_depth_at(coord), coord); - } - - // Returns the velocity given texcoord - vec2 get_object_velocity_at(vec2 coord) { - return get_gbuffer_object_velocity(GBuffer, coord); - } - // Returns the velocity given texcoord - vec2 get_object_velocity_at(ivec2 coord) { - return get_gbuffer_object_velocity(GBuffer, coord); - } - - // Returns the view space normal at a given texcoord. This tries to find - // a good fit normal, but thus is quite expensive. - // It does not include normal mapping, since it uses the depth buffer as source. - vec3 get_view_normal(vec2 coord) { - - // OPTIONAL: Just recover it from the world space normal. - // This has the advantage that it does include normal mapping. - #if 1 - vec3 world_normal = get_gbuffer_normal(GBuffer, coord); - return world_normal_to_view(world_normal); - #endif - - vec2 pixel_size = 1.0 / SCREEN_SIZE; - vec3 view_pos = get_view_pos_at(coord); - - // Do some work to find a good view normal - vec3 dx_px = view_pos - get_view_pos_at(coord + pixel_size * vec2(1, 0)); - vec3 dx_py = view_pos - get_view_pos_at(coord + pixel_size * vec2(0, 1)); - - vec3 dx_nx = get_view_pos_at(coord + pixel_size * vec2(-1, 0)) - view_pos; - vec3 dx_ny = get_view_pos_at(coord + pixel_size * vec2(0, -1)) - view_pos; - - // Find the closest distance in depth - vec3 dx_x = abs(dx_px.z) < abs(dx_nx.z) ? vec3(dx_px) : vec3(dx_nx); - vec3 dx_y = abs(dx_py.z) < abs(dx_ny.z) ? vec3(dx_py) : vec3(dx_ny); - - return normalize(cross(dx_x, dx_y)); - } - - // Returns the view space normal at a given texcoord. This approximates - // the normal instead of calculating it accurately, thus might produce - // smaller artifacts at edges. However you should prefer this method - // wherever possible. It does not include normal mapping, since it uses - // the depth buffer as source. - vec3 get_view_normal_approx(vec2 coord) { - vec3 view_pos = get_view_pos_at(coord); - vec2 pixel_size = 1.0 / SCREEN_SIZE; - vec3 dx_x = view_pos - get_view_pos_at(coord + pixel_size * vec2(1, 0)); - vec3 dx_y = view_pos - get_view_pos_at(coord + pixel_size * vec2(0, 1)); - return normalize(cross(dx_x, dx_y)); - } - - - // Returns the cameras velocity - vec2 get_camera_velocity(vec2 texcoord) { - vec2 film_offset_bias = MainSceneData.current_film_offset * - vec2(1.0, 1.0 / ASPECT_RATIO); - vec3 pos = get_world_pos_at(texcoord - film_offset_bias); - vec4 last_proj = MainSceneData.last_view_proj_mat_no_jitter * vec4(pos, 1); - vec2 last_coord = fma(last_proj.xy / last_proj.w, vec2(0.5), vec2(0.5)); - return last_coord - texcoord; - } - - #endif - -#endif // IN_GBUFFER_SHADER +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +#pragma include "includes/material.inc.glsl" +#pragma include "includes/normal_packing.inc.glsl" +#pragma include "includes/brdf.inc.glsl" + +uniform mat4 p3d_ProjectionMatrix; + +#if IN_GBUFFER_SHADER + + /* + + GBuffer Packing + + */ + + layout(location = 0) out vec4 gbuffer_out_0; + layout(location = 1) out vec4 gbuffer_out_1; + layout(location = 2) out vec4 gbuffer_out_2; + + vec2 compute_velocity() { + // Compute velocity based on this and last frames mvp matrix + vec4 last_proj_pos = vOutput.last_proj_position; + vec2 last_texcoord = fma(last_proj_pos.xy / last_proj_pos.w, vec2(0.5), vec2(0.5)); + vec2 curr_texcoord = gl_FragCoord.xy / SCREEN_SIZE; + return (curr_texcoord - last_texcoord); + } + + // Lean mapping + float adjust_roughness(float roughness, float avg_normal_length) { + // Based on The Order : 1886 SIGGRAPH course notes implementation + if (avg_normal_length < 1.0) + { + float avg_len_sq = avg_normal_length * avg_normal_length; + float kappa = (3 * avg_normal_length - avg_normal_length * avg_len_sq) / + (1 - avg_len_sq); + float variance = 1.0 / (2.0 * kappa) ; + return sqrt(roughness * roughness + variance); + } + return roughness; + } + + + void render_material(MaterialShaderOutput m) { + + // Compute material properties + vec3 normal = normalize(m.normal); + vec2 packed_normal = pack_normal_octahedron(normal); + vec2 velocity = compute_velocity(); + + // Clamp BaseColor, but only for negative values, we allow values > 1.0 + // vec3 basecolor = pow(max(vec3(0), m.basecolor), vec3(2.2)) * 1.0; + vec3 basecolor = max(vec3(0), m.basecolor); + + // Clamp properties like specular and metallic, which have to be in the + // 0 ... 1 range + float specular = clamp(m.specular_ior, 1.0001, 2.51); + float metallic = saturate(m.metallic); + float roughness = clamp(m.roughness, 0.03, 1.0); + + roughness = adjust_roughness(roughness, length(m.normal)); + + // Pack all values to the gbuffer + gbuffer_out_0 = vec4(basecolor.r, basecolor.g, basecolor.b, roughness); + gbuffer_out_1 = vec4(packed_normal.x, packed_normal.y, metallic, specular); + gbuffer_out_2 = vec4(velocity.x, velocity.y, m.shading_model, m.shading_model_param0); + } + + +#else // IN_GBUFFER_SHADER + + + /* + + GBuffer - Unpacking + + */ + + #pragma include "includes/transforms.inc.glsl" + + // Common gbuffer data + struct GBufferData { + sampler2D Depth; + sampler2D Data0; + sampler2D Data1; + sampler2D Data2; + }; + + // Checks whether the given material is the skybox + bool is_skybox(vec3 pos, vec3 camera_pos) { + #if REFERENCE_MODE + return distance(pos, camera_pos) > 1000.0; + #endif + return distance(pos, camera_pos) > 20000.0; + } + + bool is_skybox(Material m, vec3 camera_pos) { + return is_skybox(m.position, camera_pos); + } + + bool is_skybox(Material m) { return is_skybox(m, MainSceneData.camera_pos); } + bool is_skybox(vec3 pos) { return is_skybox(pos, MainSceneData.camera_pos); } + + // Returns the depth at a given texcoord + float get_gbuffer_depth(GBufferData data, vec2 coord) { + return textureLod(data.Depth, coord, 0).x; + } + + // Returns the depth at a given texcoord + float get_gbuffer_depth(GBufferData data, ivec2 coord) { + return texelFetch(data.Depth, coord, 0).x; + } + + // Returns the world space position at a given texcoord + vec3 get_gbuffer_position(GBufferData data, vec2 coord) { + float depth = get_gbuffer_depth(data, coord); + return calculate_surface_pos(depth, coord); + } + + // Returns the world space normal at a given texcoord + vec3 get_gbuffer_normal(GBufferData data, vec2 coord) { + vec2 packed_normal = textureLod(data.Data1, coord, 0).xy; + return unpack_normal_octahedron(packed_normal); + } + // Returns the world space normal at a given texcoord + vec3 get_gbuffer_normal(GBufferData data, ivec2 coord) { + vec2 packed_normal = texelFetch(data.Data1, coord, 0).xy; + return unpack_normal_octahedron(packed_normal); + } + + // Returns the velocity at a given coordinate + vec2 get_gbuffer_object_velocity(GBufferData data, vec2 coord) { + return textureLod(data.Data2, coord, 0).xy; + } + + // Returns the velocity at a given coordinate + vec2 get_gbuffer_object_velocity(GBufferData data, ivec2 coord) { + return texelFetch(data.Data2, coord, 0).xy; + } + + int get_gbuffer_shading_model(GBufferData data, vec2 coord) { + return int(textureLod(data.Data2, coord, 0).z); + } + + float get_gbuffer_roughness(GBufferData data, vec2 coord) { + // XXX: take clearcoat into account + return square(clamp(textureLod(data.Data0, coord, 0).w, MINIMUM_ROUGHNESS, 1.0)); + } + + float get_gbuffer_roughness(GBufferData data, ivec2 coord) { + // XXX: take clearcoat into account + return square(clamp(texelFetch(data.Data0, coord, 0).w, MINIMUM_ROUGHNESS, 1.0)); + } + + // Unpacks a material from the gbuffer + Material unpack_material(GBufferData data, vec2 fcoord) { + + // Fetch data from data-textures + vec4 data0 = textureLod(data.Data0, fcoord, 0); + vec4 data1 = textureLod(data.Data1, fcoord, 0); + vec4 data2 = textureLod(data.Data2, fcoord, 0); + + Material m; + m.position = get_gbuffer_position(data, fcoord); + m.basecolor = data0.xyz; + m.linear_roughness = clamp(data0.w, MINIMUM_ROUGHNESS, 1.0); + m.roughness = m.linear_roughness * m.linear_roughness; + m.normal = unpack_normal_octahedron(data1.xy); + m.metallic = saturate(data1.z * 1.001 - 0.0005); + m.specular_ior = data1.w; + m.specular = ior_to_specular(data1.w); + m.shading_model = int(data2.z); + m.shading_model_param0 = data2.w; + + // Velocity, not stored in the Material struct but stored in the G-Buffer + // vec2 velocity = data2.xy; + return m; + } + + // Unpacks a material from the gbuffer + Material unpack_material(GBufferData data) { + vec2 fcoord = get_texcoord(); + return unpack_material(data, fcoord); + } + + #ifdef USE_GBUFFER_EXTENSIONS + + /* + + GBuffer extensions for reading gbuffer values without having to specify + the gbuffer. + + */ + + uniform GBufferData GBuffer; + + // Returns the depth at a given texcoord + float get_depth_at(vec2 coord) { + return get_gbuffer_depth(GBuffer, coord); + } + // Returns the depth at a given texcoord + float get_depth_at(ivec2 coord) { + return get_gbuffer_depth(GBuffer, coord); + } + + // Returns the view space position at a given texcoord + vec3 get_view_pos_at(vec2 coord) { + return calculate_view_pos(get_depth_at(coord), coord); + } + + + // Returns the world space position at a given texcoord + vec3 get_world_pos_at(vec2 coord) { + return calculate_surface_pos(get_depth_at(coord), coord); + } + + // Returns the velocity given texcoord + vec2 get_object_velocity_at(vec2 coord) { + return get_gbuffer_object_velocity(GBuffer, coord); + } + // Returns the velocity given texcoord + vec2 get_object_velocity_at(ivec2 coord) { + return get_gbuffer_object_velocity(GBuffer, coord); + } + + // Returns the view space normal at a given texcoord. This tries to find + // a good fit normal, but thus is quite expensive. + // It does not include normal mapping, since it uses the depth buffer as source. + vec3 get_view_normal(vec2 coord) { + + // OPTIONAL: Just recover it from the world space normal. + // This has the advantage that it does include normal mapping. + #if 1 + vec3 world_normal = get_gbuffer_normal(GBuffer, coord); + return world_normal_to_view(world_normal); + #endif + + vec2 pixel_size = 1.0 / SCREEN_SIZE; + vec3 view_pos = get_view_pos_at(coord); + + // Do some work to find a good view normal + vec3 dx_px = view_pos - get_view_pos_at(coord + pixel_size * vec2(1, 0)); + vec3 dx_py = view_pos - get_view_pos_at(coord + pixel_size * vec2(0, 1)); + + vec3 dx_nx = get_view_pos_at(coord + pixel_size * vec2(-1, 0)) - view_pos; + vec3 dx_ny = get_view_pos_at(coord + pixel_size * vec2(0, -1)) - view_pos; + + // Find the closest distance in depth + vec3 dx_x = abs(dx_px.z) < abs(dx_nx.z) ? vec3(dx_px) : vec3(dx_nx); + vec3 dx_y = abs(dx_py.z) < abs(dx_ny.z) ? vec3(dx_py) : vec3(dx_ny); + + return normalize(cross(dx_x, dx_y)); + } + + // Returns the view space normal at a given texcoord. This approximates + // the normal instead of calculating it accurately, thus might produce + // smaller artifacts at edges. However you should prefer this method + // wherever possible. It does not include normal mapping, since it uses + // the depth buffer as source. + vec3 get_view_normal_approx(vec2 coord) { + vec3 view_pos = get_view_pos_at(coord); + vec2 pixel_size = 1.0 / SCREEN_SIZE; + vec3 dx_x = view_pos - get_view_pos_at(coord + pixel_size * vec2(1, 0)); + vec3 dx_y = view_pos - get_view_pos_at(coord + pixel_size * vec2(0, 1)); + return normalize(cross(dx_x, dx_y)); + } + + + // Returns the cameras velocity + vec2 get_camera_velocity(vec2 texcoord) { + vec2 film_offset_bias = MainSceneData.current_film_offset * + vec2(1.0, 1.0 / ASPECT_RATIO); + vec3 pos = get_world_pos_at(texcoord - film_offset_bias); + vec4 last_proj = MainSceneData.last_view_proj_mat_no_jitter * vec4(pos, 1); + vec2 last_coord = fma(last_proj.xy / last_proj.w, vec2(0.5), vec2(0.5)); + return last_coord - texcoord; + } + + #endif + +#endif // IN_GBUFFER_SHADER diff --git a/RenderPipelineFile/rpcore/shader/includes/halton_sequences.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/halton_sequences.inc.glsl index f621e288..01529bc8 100644 --- a/RenderPipelineFile/rpcore/shader/includes/halton_sequences.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/halton_sequences.inc.glsl @@ -1,568 +1,568 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -// Halton sequences - - -CONST_ARRAY vec2 halton_2D_4[4] = vec2[]( - vec2(0.00000, -0.33333), - vec2(-0.50000, 0.33333), - vec2(0.50000, -0.77778), - vec2(-0.75000, -0.11111) -); - -CONST_ARRAY vec2 halton_2D_8[8] = vec2[]( - vec2(0.00000, -0.33333), - vec2(-0.50000, 0.33333), - vec2(0.50000, -0.77778), - vec2(-0.75000, -0.11111), - vec2(0.25000, 0.55556), - vec2(-0.25000, -0.55556), - vec2(0.75000, 0.11111), - vec2(-0.87500, 0.77778) -); - -CONST_ARRAY vec2 halton_2D_16[16] = vec2[]( - vec2(0.00000, -0.33333), - vec2(-0.50000, 0.33333), - vec2(0.50000, -0.77778), - vec2(-0.75000, -0.11111), - vec2(0.25000, 0.55556), - vec2(-0.25000, -0.55556), - vec2(0.75000, 0.11111), - vec2(-0.87500, 0.77778), - vec2(0.12500, -0.92593), - vec2(-0.37500, -0.25926), - vec2(0.62500, 0.40741), - vec2(-0.62500, -0.70370), - vec2(0.37500, -0.03704), - vec2(-0.12500, 0.62963), - vec2(0.87500, -0.48148), - vec2(-0.93750, 0.18519) -); - -CONST_ARRAY vec2 halton_2D_32[32] = vec2[]( - vec2(0.00000, -0.33333), - vec2(-0.50000, 0.33333), - vec2(0.50000, -0.77778), - vec2(-0.75000, -0.11111), - vec2(0.25000, 0.55556), - vec2(-0.25000, -0.55556), - vec2(0.75000, 0.11111), - vec2(-0.87500, 0.77778), - vec2(0.12500, -0.92593), - vec2(-0.37500, -0.25926), - vec2(0.62500, 0.40741), - vec2(-0.62500, -0.70370), - vec2(0.37500, -0.03704), - vec2(-0.12500, 0.62963), - vec2(0.87500, -0.48148), - vec2(-0.93750, 0.18519), - vec2(0.06250, 0.85185), - vec2(-0.43750, -0.85185), - vec2(0.56250, -0.18519), - vec2(-0.68750, 0.48148), - vec2(0.31250, -0.62963), - vec2(-0.18750, 0.03704), - vec2(0.81250, 0.70370), - vec2(-0.81250, -0.40741), - vec2(0.18750, 0.25926), - vec2(-0.31250, 0.92593), - vec2(0.68750, -0.97531), - vec2(-0.56250, -0.30864), - vec2(0.43750, 0.35802), - vec2(-0.06250, -0.75309), - vec2(0.93750, -0.08642), - vec2(-0.96875, 0.58025) -); - -CONST_ARRAY vec2 halton_2D_64[64] = vec2[]( - vec2(0.00000, -0.33333), - vec2(-0.50000, 0.33333), - vec2(0.50000, -0.77778), - vec2(-0.75000, -0.11111), - vec2(0.25000, 0.55556), - vec2(-0.25000, -0.55556), - vec2(0.75000, 0.11111), - vec2(-0.87500, 0.77778), - vec2(0.12500, -0.92593), - vec2(-0.37500, -0.25926), - vec2(0.62500, 0.40741), - vec2(-0.62500, -0.70370), - vec2(0.37500, -0.03704), - vec2(-0.12500, 0.62963), - vec2(0.87500, -0.48148), - vec2(-0.93750, 0.18519), - vec2(0.06250, 0.85185), - vec2(-0.43750, -0.85185), - vec2(0.56250, -0.18519), - vec2(-0.68750, 0.48148), - vec2(0.31250, -0.62963), - vec2(-0.18750, 0.03704), - vec2(0.81250, 0.70370), - vec2(-0.81250, -0.40741), - vec2(0.18750, 0.25926), - vec2(-0.31250, 0.92593), - vec2(0.68750, -0.97531), - vec2(-0.56250, -0.30864), - vec2(0.43750, 0.35802), - vec2(-0.06250, -0.75309), - vec2(0.93750, -0.08642), - vec2(-0.96875, 0.58025), - vec2(0.03125, -0.53086), - vec2(-0.46875, 0.13580), - vec2(0.53125, 0.80247), - vec2(-0.71875, -0.90123), - vec2(0.28125, -0.23457), - vec2(-0.21875, 0.43210), - vec2(0.78125, -0.67901), - vec2(-0.84375, -0.01235), - vec2(0.15625, 0.65432), - vec2(-0.34375, -0.45679), - vec2(0.65625, 0.20988), - vec2(-0.59375, 0.87654), - vec2(0.40625, -0.82716), - vec2(-0.09375, -0.16049), - vec2(0.90625, 0.50617), - vec2(-0.90625, -0.60494), - vec2(0.09375, 0.06173), - vec2(-0.40625, 0.72840), - vec2(0.59375, -0.38272), - vec2(-0.65625, 0.28395), - vec2(0.34375, 0.95062), - vec2(-0.15625, -0.95062), - vec2(0.84375, -0.28395), - vec2(-0.78125, 0.38272), - vec2(0.21875, -0.72840), - vec2(-0.28125, -0.06173), - vec2(0.71875, 0.60494), - vec2(-0.53125, -0.50617), - vec2(0.46875, 0.16049), - vec2(-0.03125, 0.82716), - vec2(0.96875, -0.87654), - vec2(-0.98438, -0.20988) -); - -CONST_ARRAY vec2 halton_2D_128[128] = vec2[]( - vec2(0.00000, -0.33333), - vec2(-0.50000, 0.33333), - vec2(0.50000, -0.77778), - vec2(-0.75000, -0.11111), - vec2(0.25000, 0.55556), - vec2(-0.25000, -0.55556), - vec2(0.75000, 0.11111), - vec2(-0.87500, 0.77778), - vec2(0.12500, -0.92593), - vec2(-0.37500, -0.25926), - vec2(0.62500, 0.40741), - vec2(-0.62500, -0.70370), - vec2(0.37500, -0.03704), - vec2(-0.12500, 0.62963), - vec2(0.87500, -0.48148), - vec2(-0.93750, 0.18519), - vec2(0.06250, 0.85185), - vec2(-0.43750, -0.85185), - vec2(0.56250, -0.18519), - vec2(-0.68750, 0.48148), - vec2(0.31250, -0.62963), - vec2(-0.18750, 0.03704), - vec2(0.81250, 0.70370), - vec2(-0.81250, -0.40741), - vec2(0.18750, 0.25926), - vec2(-0.31250, 0.92593), - vec2(0.68750, -0.97531), - vec2(-0.56250, -0.30864), - vec2(0.43750, 0.35802), - vec2(-0.06250, -0.75309), - vec2(0.93750, -0.08642), - vec2(-0.96875, 0.58025), - vec2(0.03125, -0.53086), - vec2(-0.46875, 0.13580), - vec2(0.53125, 0.80247), - vec2(-0.71875, -0.90123), - vec2(0.28125, -0.23457), - vec2(-0.21875, 0.43210), - vec2(0.78125, -0.67901), - vec2(-0.84375, -0.01235), - vec2(0.15625, 0.65432), - vec2(-0.34375, -0.45679), - vec2(0.65625, 0.20988), - vec2(-0.59375, 0.87654), - vec2(0.40625, -0.82716), - vec2(-0.09375, -0.16049), - vec2(0.90625, 0.50617), - vec2(-0.90625, -0.60494), - vec2(0.09375, 0.06173), - vec2(-0.40625, 0.72840), - vec2(0.59375, -0.38272), - vec2(-0.65625, 0.28395), - vec2(0.34375, 0.95062), - vec2(-0.15625, -0.95062), - vec2(0.84375, -0.28395), - vec2(-0.78125, 0.38272), - vec2(0.21875, -0.72840), - vec2(-0.28125, -0.06173), - vec2(0.71875, 0.60494), - vec2(-0.53125, -0.50617), - vec2(0.46875, 0.16049), - vec2(-0.03125, 0.82716), - vec2(0.96875, -0.87654), - vec2(-0.98438, -0.20988), - vec2(0.01562, 0.45679), - vec2(-0.48438, -0.65432), - vec2(0.51562, 0.01235), - vec2(-0.73438, 0.67901), - vec2(0.26562, -0.43210), - vec2(-0.23438, 0.23457), - vec2(0.76562, 0.90123), - vec2(-0.85938, -0.80247), - vec2(0.14062, -0.13580), - vec2(-0.35938, 0.53086), - vec2(0.64062, -0.58025), - vec2(-0.60938, 0.08642), - vec2(0.39062, 0.75309), - vec2(-0.10938, -0.35802), - vec2(0.89062, 0.30864), - vec2(-0.92188, 0.97531), - vec2(0.07812, -0.99177), - vec2(-0.42188, -0.32510), - vec2(0.57812, 0.34156), - vec2(-0.67188, -0.76955), - vec2(0.32812, -0.10288), - vec2(-0.17188, 0.56379), - vec2(0.82812, -0.54733), - vec2(-0.79688, 0.11934), - vec2(0.20312, 0.78601), - vec2(-0.29688, -0.91770), - vec2(0.70312, -0.25103), - vec2(-0.54688, 0.41564), - vec2(0.45312, -0.69547), - vec2(-0.04688, -0.02881), - vec2(0.95312, 0.63786), - vec2(-0.95312, -0.47325), - vec2(0.04688, 0.19342), - vec2(-0.45312, 0.86008), - vec2(0.54688, -0.84362), - vec2(-0.70312, -0.17695), - vec2(0.29688, 0.48971), - vec2(-0.20312, -0.62140), - vec2(0.79688, 0.04527), - vec2(-0.82812, 0.71193), - vec2(0.17188, -0.39918), - vec2(-0.32812, 0.26749), - vec2(0.67188, 0.93416), - vec2(-0.57812, -0.96708), - vec2(0.42188, -0.30041), - vec2(-0.07812, 0.36626), - vec2(0.92188, -0.74486), - vec2(-0.89062, -0.07819), - vec2(0.10938, 0.58848), - vec2(-0.39062, -0.52263), - vec2(0.60938, 0.14403), - vec2(-0.64062, 0.81070), - vec2(0.35938, -0.89300), - vec2(-0.14062, -0.22634), - vec2(0.85938, 0.44033), - vec2(-0.76562, -0.67078), - vec2(0.23438, -0.00412), - vec2(-0.26562, 0.66255), - vec2(0.73438, -0.44856), - vec2(-0.51562, 0.21811), - vec2(0.48438, 0.88477), - vec2(-0.01562, -0.81893), - vec2(0.98438, -0.15226), - vec2(-0.99219, 0.51440) -); - -CONST_ARRAY vec3 halton_3D_4[4] = vec3[]( - vec3(0.00000, -0.33333, -0.60000), - vec3(-0.50000, 0.33333, -0.20000), - vec3(0.50000, -0.77778, 0.20000), - vec3(-0.75000, -0.11111, 0.60000) -); - -CONST_ARRAY vec3 halton_3D_8[8] = vec3[]( - vec3(0.00000, -0.33333, -0.60000), - vec3(-0.50000, 0.33333, -0.20000), - vec3(0.50000, -0.77778, 0.20000), - vec3(-0.75000, -0.11111, 0.60000), - vec3(0.25000, 0.55556, -0.92000), - vec3(-0.25000, -0.55556, -0.52000), - vec3(0.75000, 0.11111, -0.12000), - vec3(-0.87500, 0.77778, 0.28000) -); - -CONST_ARRAY vec3 halton_3D_16[16] = vec3[]( - vec3(0.00000, -0.33333, -0.60000), - vec3(-0.50000, 0.33333, -0.20000), - vec3(0.50000, -0.77778, 0.20000), - vec3(-0.75000, -0.11111, 0.60000), - vec3(0.25000, 0.55556, -0.92000), - vec3(-0.25000, -0.55556, -0.52000), - vec3(0.75000, 0.11111, -0.12000), - vec3(-0.87500, 0.77778, 0.28000), - vec3(0.12500, -0.92593, 0.68000), - vec3(-0.37500, -0.25926, -0.84000), - vec3(0.62500, 0.40741, -0.44000), - vec3(-0.62500, -0.70370, -0.04000), - vec3(0.37500, -0.03704, 0.36000), - vec3(-0.12500, 0.62963, 0.76000), - vec3(0.87500, -0.48148, -0.76000), - vec3(-0.93750, 0.18519, -0.36000) -); - -CONST_ARRAY vec3 halton_3D_32[32] = vec3[]( - vec3(0.00000, -0.33333, -0.60000), - vec3(-0.50000, 0.33333, -0.20000), - vec3(0.50000, -0.77778, 0.20000), - vec3(-0.75000, -0.11111, 0.60000), - vec3(0.25000, 0.55556, -0.92000), - vec3(-0.25000, -0.55556, -0.52000), - vec3(0.75000, 0.11111, -0.12000), - vec3(-0.87500, 0.77778, 0.28000), - vec3(0.12500, -0.92593, 0.68000), - vec3(-0.37500, -0.25926, -0.84000), - vec3(0.62500, 0.40741, -0.44000), - vec3(-0.62500, -0.70370, -0.04000), - vec3(0.37500, -0.03704, 0.36000), - vec3(-0.12500, 0.62963, 0.76000), - vec3(0.87500, -0.48148, -0.76000), - vec3(-0.93750, 0.18519, -0.36000), - vec3(0.06250, 0.85185, 0.04000), - vec3(-0.43750, -0.85185, 0.44000), - vec3(0.56250, -0.18519, 0.84000), - vec3(-0.68750, 0.48148, -0.68000), - vec3(0.31250, -0.62963, -0.28000), - vec3(-0.18750, 0.03704, 0.12000), - vec3(0.81250, 0.70370, 0.52000), - vec3(-0.81250, -0.40741, 0.92000), - vec3(0.18750, 0.25926, -0.98400), - vec3(-0.31250, 0.92593, -0.58400), - vec3(0.68750, -0.97531, -0.18400), - vec3(-0.56250, -0.30864, 0.21600), - vec3(0.43750, 0.35802, 0.61600), - vec3(-0.06250, -0.75309, -0.90400), - vec3(0.93750, -0.08642, -0.50400), - vec3(-0.96875, 0.58025, -0.10400) -); - -CONST_ARRAY vec3 halton_3D_64[64] = vec3[]( - vec3(0.00000, -0.33333, -0.60000), - vec3(-0.50000, 0.33333, -0.20000), - vec3(0.50000, -0.77778, 0.20000), - vec3(-0.75000, -0.11111, 0.60000), - vec3(0.25000, 0.55556, -0.92000), - vec3(-0.25000, -0.55556, -0.52000), - vec3(0.75000, 0.11111, -0.12000), - vec3(-0.87500, 0.77778, 0.28000), - vec3(0.12500, -0.92593, 0.68000), - vec3(-0.37500, -0.25926, -0.84000), - vec3(0.62500, 0.40741, -0.44000), - vec3(-0.62500, -0.70370, -0.04000), - vec3(0.37500, -0.03704, 0.36000), - vec3(-0.12500, 0.62963, 0.76000), - vec3(0.87500, -0.48148, -0.76000), - vec3(-0.93750, 0.18519, -0.36000), - vec3(0.06250, 0.85185, 0.04000), - vec3(-0.43750, -0.85185, 0.44000), - vec3(0.56250, -0.18519, 0.84000), - vec3(-0.68750, 0.48148, -0.68000), - vec3(0.31250, -0.62963, -0.28000), - vec3(-0.18750, 0.03704, 0.12000), - vec3(0.81250, 0.70370, 0.52000), - vec3(-0.81250, -0.40741, 0.92000), - vec3(0.18750, 0.25926, -0.98400), - vec3(-0.31250, 0.92593, -0.58400), - vec3(0.68750, -0.97531, -0.18400), - vec3(-0.56250, -0.30864, 0.21600), - vec3(0.43750, 0.35802, 0.61600), - vec3(-0.06250, -0.75309, -0.90400), - vec3(0.93750, -0.08642, -0.50400), - vec3(-0.96875, 0.58025, -0.10400), - vec3(0.03125, -0.53086, 0.29600), - vec3(-0.46875, 0.13580, 0.69600), - vec3(0.53125, 0.80247, -0.82400), - vec3(-0.71875, -0.90123, -0.42400), - vec3(0.28125, -0.23457, -0.02400), - vec3(-0.21875, 0.43210, 0.37600), - vec3(0.78125, -0.67901, 0.77600), - vec3(-0.84375, -0.01235, -0.74400), - vec3(0.15625, 0.65432, -0.34400), - vec3(-0.34375, -0.45679, 0.05600), - vec3(0.65625, 0.20988, 0.45600), - vec3(-0.59375, 0.87654, 0.85600), - vec3(0.40625, -0.82716, -0.66400), - vec3(-0.09375, -0.16049, -0.26400), - vec3(0.90625, 0.50617, 0.13600), - vec3(-0.90625, -0.60494, 0.53600), - vec3(0.09375, 0.06173, 0.93600), - vec3(-0.40625, 0.72840, -0.96800), - vec3(0.59375, -0.38272, -0.56800), - vec3(-0.65625, 0.28395, -0.16800), - vec3(0.34375, 0.95062, 0.23200), - vec3(-0.15625, -0.95062, 0.63200), - vec3(0.84375, -0.28395, -0.88800), - vec3(-0.78125, 0.38272, -0.48800), - vec3(0.21875, -0.72840, -0.08800), - vec3(-0.28125, -0.06173, 0.31200), - vec3(0.71875, 0.60494, 0.71200), - vec3(-0.53125, -0.50617, -0.80800), - vec3(0.46875, 0.16049, -0.40800), - vec3(-0.03125, 0.82716, -0.00800), - vec3(0.96875, -0.87654, 0.39200), - vec3(-0.98438, -0.20988, 0.79200) -); - -CONST_ARRAY vec3 halton_3D_128[128] = vec3[]( - vec3(0.00000, -0.33333, -0.60000), - vec3(-0.50000, 0.33333, -0.20000), - vec3(0.50000, -0.77778, 0.20000), - vec3(-0.75000, -0.11111, 0.60000), - vec3(0.25000, 0.55556, -0.92000), - vec3(-0.25000, -0.55556, -0.52000), - vec3(0.75000, 0.11111, -0.12000), - vec3(-0.87500, 0.77778, 0.28000), - vec3(0.12500, -0.92593, 0.68000), - vec3(-0.37500, -0.25926, -0.84000), - vec3(0.62500, 0.40741, -0.44000), - vec3(-0.62500, -0.70370, -0.04000), - vec3(0.37500, -0.03704, 0.36000), - vec3(-0.12500, 0.62963, 0.76000), - vec3(0.87500, -0.48148, -0.76000), - vec3(-0.93750, 0.18519, -0.36000), - vec3(0.06250, 0.85185, 0.04000), - vec3(-0.43750, -0.85185, 0.44000), - vec3(0.56250, -0.18519, 0.84000), - vec3(-0.68750, 0.48148, -0.68000), - vec3(0.31250, -0.62963, -0.28000), - vec3(-0.18750, 0.03704, 0.12000), - vec3(0.81250, 0.70370, 0.52000), - vec3(-0.81250, -0.40741, 0.92000), - vec3(0.18750, 0.25926, -0.98400), - vec3(-0.31250, 0.92593, -0.58400), - vec3(0.68750, -0.97531, -0.18400), - vec3(-0.56250, -0.30864, 0.21600), - vec3(0.43750, 0.35802, 0.61600), - vec3(-0.06250, -0.75309, -0.90400), - vec3(0.93750, -0.08642, -0.50400), - vec3(-0.96875, 0.58025, -0.10400), - vec3(0.03125, -0.53086, 0.29600), - vec3(-0.46875, 0.13580, 0.69600), - vec3(0.53125, 0.80247, -0.82400), - vec3(-0.71875, -0.90123, -0.42400), - vec3(0.28125, -0.23457, -0.02400), - vec3(-0.21875, 0.43210, 0.37600), - vec3(0.78125, -0.67901, 0.77600), - vec3(-0.84375, -0.01235, -0.74400), - vec3(0.15625, 0.65432, -0.34400), - vec3(-0.34375, -0.45679, 0.05600), - vec3(0.65625, 0.20988, 0.45600), - vec3(-0.59375, 0.87654, 0.85600), - vec3(0.40625, -0.82716, -0.66400), - vec3(-0.09375, -0.16049, -0.26400), - vec3(0.90625, 0.50617, 0.13600), - vec3(-0.90625, -0.60494, 0.53600), - vec3(0.09375, 0.06173, 0.93600), - vec3(-0.40625, 0.72840, -0.96800), - vec3(0.59375, -0.38272, -0.56800), - vec3(-0.65625, 0.28395, -0.16800), - vec3(0.34375, 0.95062, 0.23200), - vec3(-0.15625, -0.95062, 0.63200), - vec3(0.84375, -0.28395, -0.88800), - vec3(-0.78125, 0.38272, -0.48800), - vec3(0.21875, -0.72840, -0.08800), - vec3(-0.28125, -0.06173, 0.31200), - vec3(0.71875, 0.60494, 0.71200), - vec3(-0.53125, -0.50617, -0.80800), - vec3(0.46875, 0.16049, -0.40800), - vec3(-0.03125, 0.82716, -0.00800), - vec3(0.96875, -0.87654, 0.39200), - vec3(-0.98438, -0.20988, 0.79200), - vec3(0.01562, 0.45679, -0.72800), - vec3(-0.48438, -0.65432, -0.32800), - vec3(0.51562, 0.01235, 0.07200), - vec3(-0.73438, 0.67901, 0.47200), - vec3(0.26562, -0.43210, 0.87200), - vec3(-0.23438, 0.23457, -0.64800), - vec3(0.76562, 0.90123, -0.24800), - vec3(-0.85938, -0.80247, 0.15200), - vec3(0.14062, -0.13580, 0.55200), - vec3(-0.35938, 0.53086, 0.95200), - vec3(0.64062, -0.58025, -0.95200), - vec3(-0.60938, 0.08642, -0.55200), - vec3(0.39062, 0.75309, -0.15200), - vec3(-0.10938, -0.35802, 0.24800), - vec3(0.89062, 0.30864, 0.64800), - vec3(-0.92188, 0.97531, -0.87200), - vec3(0.07812, -0.99177, -0.47200), - vec3(-0.42188, -0.32510, -0.07200), - vec3(0.57812, 0.34156, 0.32800), - vec3(-0.67188, -0.76955, 0.72800), - vec3(0.32812, -0.10288, -0.79200), - vec3(-0.17188, 0.56379, -0.39200), - vec3(0.82812, -0.54733, 0.00800), - vec3(-0.79688, 0.11934, 0.40800), - vec3(0.20312, 0.78601, 0.80800), - vec3(-0.29688, -0.91770, -0.71200), - vec3(0.70312, -0.25103, -0.31200), - vec3(-0.54688, 0.41564, 0.08800), - vec3(0.45312, -0.69547, 0.48800), - vec3(-0.04688, -0.02881, 0.88800), - vec3(0.95312, 0.63786, -0.63200), - vec3(-0.95312, -0.47325, -0.23200), - vec3(0.04688, 0.19342, 0.16800), - vec3(-0.45312, 0.86008, 0.56800), - vec3(0.54688, -0.84362, 0.96800), - vec3(-0.70312, -0.17695, -0.93600), - vec3(0.29688, 0.48971, -0.53600), - vec3(-0.20312, -0.62140, -0.13600), - vec3(0.79688, 0.04527, 0.26400), - vec3(-0.82812, 0.71193, 0.66400), - vec3(0.17188, -0.39918, -0.85600), - vec3(-0.32812, 0.26749, -0.45600), - vec3(0.67188, 0.93416, -0.05600), - vec3(-0.57812, -0.96708, 0.34400), - vec3(0.42188, -0.30041, 0.74400), - vec3(-0.07812, 0.36626, -0.77600), - vec3(0.92188, -0.74486, -0.37600), - vec3(-0.89062, -0.07819, 0.02400), - vec3(0.10938, 0.58848, 0.42400), - vec3(-0.39062, -0.52263, 0.82400), - vec3(0.60938, 0.14403, -0.69600), - vec3(-0.64062, 0.81070, -0.29600), - vec3(0.35938, -0.89300, 0.10400), - vec3(-0.14062, -0.22634, 0.50400), - vec3(0.85938, 0.44033, 0.90400), - vec3(-0.76562, -0.67078, -0.61600), - vec3(0.23438, -0.00412, -0.21600), - vec3(-0.26562, 0.66255, 0.18400), - vec3(0.73438, -0.44856, 0.58400), - vec3(-0.51562, 0.21811, 0.98400), - vec3(0.48438, 0.88477, -0.99680), - vec3(-0.01562, -0.81893, -0.59680), - vec3(0.98438, -0.15226, -0.19680), - vec3(-0.99219, 0.51440, 0.20320) -); +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +// Halton sequences + + +CONST_ARRAY vec2 halton_2D_4[4] = vec2[]( + vec2(0.00000, -0.33333), + vec2(-0.50000, 0.33333), + vec2(0.50000, -0.77778), + vec2(-0.75000, -0.11111) +); + +CONST_ARRAY vec2 halton_2D_8[8] = vec2[]( + vec2(0.00000, -0.33333), + vec2(-0.50000, 0.33333), + vec2(0.50000, -0.77778), + vec2(-0.75000, -0.11111), + vec2(0.25000, 0.55556), + vec2(-0.25000, -0.55556), + vec2(0.75000, 0.11111), + vec2(-0.87500, 0.77778) +); + +CONST_ARRAY vec2 halton_2D_16[16] = vec2[]( + vec2(0.00000, -0.33333), + vec2(-0.50000, 0.33333), + vec2(0.50000, -0.77778), + vec2(-0.75000, -0.11111), + vec2(0.25000, 0.55556), + vec2(-0.25000, -0.55556), + vec2(0.75000, 0.11111), + vec2(-0.87500, 0.77778), + vec2(0.12500, -0.92593), + vec2(-0.37500, -0.25926), + vec2(0.62500, 0.40741), + vec2(-0.62500, -0.70370), + vec2(0.37500, -0.03704), + vec2(-0.12500, 0.62963), + vec2(0.87500, -0.48148), + vec2(-0.93750, 0.18519) +); + +CONST_ARRAY vec2 halton_2D_32[32] = vec2[]( + vec2(0.00000, -0.33333), + vec2(-0.50000, 0.33333), + vec2(0.50000, -0.77778), + vec2(-0.75000, -0.11111), + vec2(0.25000, 0.55556), + vec2(-0.25000, -0.55556), + vec2(0.75000, 0.11111), + vec2(-0.87500, 0.77778), + vec2(0.12500, -0.92593), + vec2(-0.37500, -0.25926), + vec2(0.62500, 0.40741), + vec2(-0.62500, -0.70370), + vec2(0.37500, -0.03704), + vec2(-0.12500, 0.62963), + vec2(0.87500, -0.48148), + vec2(-0.93750, 0.18519), + vec2(0.06250, 0.85185), + vec2(-0.43750, -0.85185), + vec2(0.56250, -0.18519), + vec2(-0.68750, 0.48148), + vec2(0.31250, -0.62963), + vec2(-0.18750, 0.03704), + vec2(0.81250, 0.70370), + vec2(-0.81250, -0.40741), + vec2(0.18750, 0.25926), + vec2(-0.31250, 0.92593), + vec2(0.68750, -0.97531), + vec2(-0.56250, -0.30864), + vec2(0.43750, 0.35802), + vec2(-0.06250, -0.75309), + vec2(0.93750, -0.08642), + vec2(-0.96875, 0.58025) +); + +CONST_ARRAY vec2 halton_2D_64[64] = vec2[]( + vec2(0.00000, -0.33333), + vec2(-0.50000, 0.33333), + vec2(0.50000, -0.77778), + vec2(-0.75000, -0.11111), + vec2(0.25000, 0.55556), + vec2(-0.25000, -0.55556), + vec2(0.75000, 0.11111), + vec2(-0.87500, 0.77778), + vec2(0.12500, -0.92593), + vec2(-0.37500, -0.25926), + vec2(0.62500, 0.40741), + vec2(-0.62500, -0.70370), + vec2(0.37500, -0.03704), + vec2(-0.12500, 0.62963), + vec2(0.87500, -0.48148), + vec2(-0.93750, 0.18519), + vec2(0.06250, 0.85185), + vec2(-0.43750, -0.85185), + vec2(0.56250, -0.18519), + vec2(-0.68750, 0.48148), + vec2(0.31250, -0.62963), + vec2(-0.18750, 0.03704), + vec2(0.81250, 0.70370), + vec2(-0.81250, -0.40741), + vec2(0.18750, 0.25926), + vec2(-0.31250, 0.92593), + vec2(0.68750, -0.97531), + vec2(-0.56250, -0.30864), + vec2(0.43750, 0.35802), + vec2(-0.06250, -0.75309), + vec2(0.93750, -0.08642), + vec2(-0.96875, 0.58025), + vec2(0.03125, -0.53086), + vec2(-0.46875, 0.13580), + vec2(0.53125, 0.80247), + vec2(-0.71875, -0.90123), + vec2(0.28125, -0.23457), + vec2(-0.21875, 0.43210), + vec2(0.78125, -0.67901), + vec2(-0.84375, -0.01235), + vec2(0.15625, 0.65432), + vec2(-0.34375, -0.45679), + vec2(0.65625, 0.20988), + vec2(-0.59375, 0.87654), + vec2(0.40625, -0.82716), + vec2(-0.09375, -0.16049), + vec2(0.90625, 0.50617), + vec2(-0.90625, -0.60494), + vec2(0.09375, 0.06173), + vec2(-0.40625, 0.72840), + vec2(0.59375, -0.38272), + vec2(-0.65625, 0.28395), + vec2(0.34375, 0.95062), + vec2(-0.15625, -0.95062), + vec2(0.84375, -0.28395), + vec2(-0.78125, 0.38272), + vec2(0.21875, -0.72840), + vec2(-0.28125, -0.06173), + vec2(0.71875, 0.60494), + vec2(-0.53125, -0.50617), + vec2(0.46875, 0.16049), + vec2(-0.03125, 0.82716), + vec2(0.96875, -0.87654), + vec2(-0.98438, -0.20988) +); + +CONST_ARRAY vec2 halton_2D_128[128] = vec2[]( + vec2(0.00000, -0.33333), + vec2(-0.50000, 0.33333), + vec2(0.50000, -0.77778), + vec2(-0.75000, -0.11111), + vec2(0.25000, 0.55556), + vec2(-0.25000, -0.55556), + vec2(0.75000, 0.11111), + vec2(-0.87500, 0.77778), + vec2(0.12500, -0.92593), + vec2(-0.37500, -0.25926), + vec2(0.62500, 0.40741), + vec2(-0.62500, -0.70370), + vec2(0.37500, -0.03704), + vec2(-0.12500, 0.62963), + vec2(0.87500, -0.48148), + vec2(-0.93750, 0.18519), + vec2(0.06250, 0.85185), + vec2(-0.43750, -0.85185), + vec2(0.56250, -0.18519), + vec2(-0.68750, 0.48148), + vec2(0.31250, -0.62963), + vec2(-0.18750, 0.03704), + vec2(0.81250, 0.70370), + vec2(-0.81250, -0.40741), + vec2(0.18750, 0.25926), + vec2(-0.31250, 0.92593), + vec2(0.68750, -0.97531), + vec2(-0.56250, -0.30864), + vec2(0.43750, 0.35802), + vec2(-0.06250, -0.75309), + vec2(0.93750, -0.08642), + vec2(-0.96875, 0.58025), + vec2(0.03125, -0.53086), + vec2(-0.46875, 0.13580), + vec2(0.53125, 0.80247), + vec2(-0.71875, -0.90123), + vec2(0.28125, -0.23457), + vec2(-0.21875, 0.43210), + vec2(0.78125, -0.67901), + vec2(-0.84375, -0.01235), + vec2(0.15625, 0.65432), + vec2(-0.34375, -0.45679), + vec2(0.65625, 0.20988), + vec2(-0.59375, 0.87654), + vec2(0.40625, -0.82716), + vec2(-0.09375, -0.16049), + vec2(0.90625, 0.50617), + vec2(-0.90625, -0.60494), + vec2(0.09375, 0.06173), + vec2(-0.40625, 0.72840), + vec2(0.59375, -0.38272), + vec2(-0.65625, 0.28395), + vec2(0.34375, 0.95062), + vec2(-0.15625, -0.95062), + vec2(0.84375, -0.28395), + vec2(-0.78125, 0.38272), + vec2(0.21875, -0.72840), + vec2(-0.28125, -0.06173), + vec2(0.71875, 0.60494), + vec2(-0.53125, -0.50617), + vec2(0.46875, 0.16049), + vec2(-0.03125, 0.82716), + vec2(0.96875, -0.87654), + vec2(-0.98438, -0.20988), + vec2(0.01562, 0.45679), + vec2(-0.48438, -0.65432), + vec2(0.51562, 0.01235), + vec2(-0.73438, 0.67901), + vec2(0.26562, -0.43210), + vec2(-0.23438, 0.23457), + vec2(0.76562, 0.90123), + vec2(-0.85938, -0.80247), + vec2(0.14062, -0.13580), + vec2(-0.35938, 0.53086), + vec2(0.64062, -0.58025), + vec2(-0.60938, 0.08642), + vec2(0.39062, 0.75309), + vec2(-0.10938, -0.35802), + vec2(0.89062, 0.30864), + vec2(-0.92188, 0.97531), + vec2(0.07812, -0.99177), + vec2(-0.42188, -0.32510), + vec2(0.57812, 0.34156), + vec2(-0.67188, -0.76955), + vec2(0.32812, -0.10288), + vec2(-0.17188, 0.56379), + vec2(0.82812, -0.54733), + vec2(-0.79688, 0.11934), + vec2(0.20312, 0.78601), + vec2(-0.29688, -0.91770), + vec2(0.70312, -0.25103), + vec2(-0.54688, 0.41564), + vec2(0.45312, -0.69547), + vec2(-0.04688, -0.02881), + vec2(0.95312, 0.63786), + vec2(-0.95312, -0.47325), + vec2(0.04688, 0.19342), + vec2(-0.45312, 0.86008), + vec2(0.54688, -0.84362), + vec2(-0.70312, -0.17695), + vec2(0.29688, 0.48971), + vec2(-0.20312, -0.62140), + vec2(0.79688, 0.04527), + vec2(-0.82812, 0.71193), + vec2(0.17188, -0.39918), + vec2(-0.32812, 0.26749), + vec2(0.67188, 0.93416), + vec2(-0.57812, -0.96708), + vec2(0.42188, -0.30041), + vec2(-0.07812, 0.36626), + vec2(0.92188, -0.74486), + vec2(-0.89062, -0.07819), + vec2(0.10938, 0.58848), + vec2(-0.39062, -0.52263), + vec2(0.60938, 0.14403), + vec2(-0.64062, 0.81070), + vec2(0.35938, -0.89300), + vec2(-0.14062, -0.22634), + vec2(0.85938, 0.44033), + vec2(-0.76562, -0.67078), + vec2(0.23438, -0.00412), + vec2(-0.26562, 0.66255), + vec2(0.73438, -0.44856), + vec2(-0.51562, 0.21811), + vec2(0.48438, 0.88477), + vec2(-0.01562, -0.81893), + vec2(0.98438, -0.15226), + vec2(-0.99219, 0.51440) +); + +CONST_ARRAY vec3 halton_3D_4[4] = vec3[]( + vec3(0.00000, -0.33333, -0.60000), + vec3(-0.50000, 0.33333, -0.20000), + vec3(0.50000, -0.77778, 0.20000), + vec3(-0.75000, -0.11111, 0.60000) +); + +CONST_ARRAY vec3 halton_3D_8[8] = vec3[]( + vec3(0.00000, -0.33333, -0.60000), + vec3(-0.50000, 0.33333, -0.20000), + vec3(0.50000, -0.77778, 0.20000), + vec3(-0.75000, -0.11111, 0.60000), + vec3(0.25000, 0.55556, -0.92000), + vec3(-0.25000, -0.55556, -0.52000), + vec3(0.75000, 0.11111, -0.12000), + vec3(-0.87500, 0.77778, 0.28000) +); + +CONST_ARRAY vec3 halton_3D_16[16] = vec3[]( + vec3(0.00000, -0.33333, -0.60000), + vec3(-0.50000, 0.33333, -0.20000), + vec3(0.50000, -0.77778, 0.20000), + vec3(-0.75000, -0.11111, 0.60000), + vec3(0.25000, 0.55556, -0.92000), + vec3(-0.25000, -0.55556, -0.52000), + vec3(0.75000, 0.11111, -0.12000), + vec3(-0.87500, 0.77778, 0.28000), + vec3(0.12500, -0.92593, 0.68000), + vec3(-0.37500, -0.25926, -0.84000), + vec3(0.62500, 0.40741, -0.44000), + vec3(-0.62500, -0.70370, -0.04000), + vec3(0.37500, -0.03704, 0.36000), + vec3(-0.12500, 0.62963, 0.76000), + vec3(0.87500, -0.48148, -0.76000), + vec3(-0.93750, 0.18519, -0.36000) +); + +CONST_ARRAY vec3 halton_3D_32[32] = vec3[]( + vec3(0.00000, -0.33333, -0.60000), + vec3(-0.50000, 0.33333, -0.20000), + vec3(0.50000, -0.77778, 0.20000), + vec3(-0.75000, -0.11111, 0.60000), + vec3(0.25000, 0.55556, -0.92000), + vec3(-0.25000, -0.55556, -0.52000), + vec3(0.75000, 0.11111, -0.12000), + vec3(-0.87500, 0.77778, 0.28000), + vec3(0.12500, -0.92593, 0.68000), + vec3(-0.37500, -0.25926, -0.84000), + vec3(0.62500, 0.40741, -0.44000), + vec3(-0.62500, -0.70370, -0.04000), + vec3(0.37500, -0.03704, 0.36000), + vec3(-0.12500, 0.62963, 0.76000), + vec3(0.87500, -0.48148, -0.76000), + vec3(-0.93750, 0.18519, -0.36000), + vec3(0.06250, 0.85185, 0.04000), + vec3(-0.43750, -0.85185, 0.44000), + vec3(0.56250, -0.18519, 0.84000), + vec3(-0.68750, 0.48148, -0.68000), + vec3(0.31250, -0.62963, -0.28000), + vec3(-0.18750, 0.03704, 0.12000), + vec3(0.81250, 0.70370, 0.52000), + vec3(-0.81250, -0.40741, 0.92000), + vec3(0.18750, 0.25926, -0.98400), + vec3(-0.31250, 0.92593, -0.58400), + vec3(0.68750, -0.97531, -0.18400), + vec3(-0.56250, -0.30864, 0.21600), + vec3(0.43750, 0.35802, 0.61600), + vec3(-0.06250, -0.75309, -0.90400), + vec3(0.93750, -0.08642, -0.50400), + vec3(-0.96875, 0.58025, -0.10400) +); + +CONST_ARRAY vec3 halton_3D_64[64] = vec3[]( + vec3(0.00000, -0.33333, -0.60000), + vec3(-0.50000, 0.33333, -0.20000), + vec3(0.50000, -0.77778, 0.20000), + vec3(-0.75000, -0.11111, 0.60000), + vec3(0.25000, 0.55556, -0.92000), + vec3(-0.25000, -0.55556, -0.52000), + vec3(0.75000, 0.11111, -0.12000), + vec3(-0.87500, 0.77778, 0.28000), + vec3(0.12500, -0.92593, 0.68000), + vec3(-0.37500, -0.25926, -0.84000), + vec3(0.62500, 0.40741, -0.44000), + vec3(-0.62500, -0.70370, -0.04000), + vec3(0.37500, -0.03704, 0.36000), + vec3(-0.12500, 0.62963, 0.76000), + vec3(0.87500, -0.48148, -0.76000), + vec3(-0.93750, 0.18519, -0.36000), + vec3(0.06250, 0.85185, 0.04000), + vec3(-0.43750, -0.85185, 0.44000), + vec3(0.56250, -0.18519, 0.84000), + vec3(-0.68750, 0.48148, -0.68000), + vec3(0.31250, -0.62963, -0.28000), + vec3(-0.18750, 0.03704, 0.12000), + vec3(0.81250, 0.70370, 0.52000), + vec3(-0.81250, -0.40741, 0.92000), + vec3(0.18750, 0.25926, -0.98400), + vec3(-0.31250, 0.92593, -0.58400), + vec3(0.68750, -0.97531, -0.18400), + vec3(-0.56250, -0.30864, 0.21600), + vec3(0.43750, 0.35802, 0.61600), + vec3(-0.06250, -0.75309, -0.90400), + vec3(0.93750, -0.08642, -0.50400), + vec3(-0.96875, 0.58025, -0.10400), + vec3(0.03125, -0.53086, 0.29600), + vec3(-0.46875, 0.13580, 0.69600), + vec3(0.53125, 0.80247, -0.82400), + vec3(-0.71875, -0.90123, -0.42400), + vec3(0.28125, -0.23457, -0.02400), + vec3(-0.21875, 0.43210, 0.37600), + vec3(0.78125, -0.67901, 0.77600), + vec3(-0.84375, -0.01235, -0.74400), + vec3(0.15625, 0.65432, -0.34400), + vec3(-0.34375, -0.45679, 0.05600), + vec3(0.65625, 0.20988, 0.45600), + vec3(-0.59375, 0.87654, 0.85600), + vec3(0.40625, -0.82716, -0.66400), + vec3(-0.09375, -0.16049, -0.26400), + vec3(0.90625, 0.50617, 0.13600), + vec3(-0.90625, -0.60494, 0.53600), + vec3(0.09375, 0.06173, 0.93600), + vec3(-0.40625, 0.72840, -0.96800), + vec3(0.59375, -0.38272, -0.56800), + vec3(-0.65625, 0.28395, -0.16800), + vec3(0.34375, 0.95062, 0.23200), + vec3(-0.15625, -0.95062, 0.63200), + vec3(0.84375, -0.28395, -0.88800), + vec3(-0.78125, 0.38272, -0.48800), + vec3(0.21875, -0.72840, -0.08800), + vec3(-0.28125, -0.06173, 0.31200), + vec3(0.71875, 0.60494, 0.71200), + vec3(-0.53125, -0.50617, -0.80800), + vec3(0.46875, 0.16049, -0.40800), + vec3(-0.03125, 0.82716, -0.00800), + vec3(0.96875, -0.87654, 0.39200), + vec3(-0.98438, -0.20988, 0.79200) +); + +CONST_ARRAY vec3 halton_3D_128[128] = vec3[]( + vec3(0.00000, -0.33333, -0.60000), + vec3(-0.50000, 0.33333, -0.20000), + vec3(0.50000, -0.77778, 0.20000), + vec3(-0.75000, -0.11111, 0.60000), + vec3(0.25000, 0.55556, -0.92000), + vec3(-0.25000, -0.55556, -0.52000), + vec3(0.75000, 0.11111, -0.12000), + vec3(-0.87500, 0.77778, 0.28000), + vec3(0.12500, -0.92593, 0.68000), + vec3(-0.37500, -0.25926, -0.84000), + vec3(0.62500, 0.40741, -0.44000), + vec3(-0.62500, -0.70370, -0.04000), + vec3(0.37500, -0.03704, 0.36000), + vec3(-0.12500, 0.62963, 0.76000), + vec3(0.87500, -0.48148, -0.76000), + vec3(-0.93750, 0.18519, -0.36000), + vec3(0.06250, 0.85185, 0.04000), + vec3(-0.43750, -0.85185, 0.44000), + vec3(0.56250, -0.18519, 0.84000), + vec3(-0.68750, 0.48148, -0.68000), + vec3(0.31250, -0.62963, -0.28000), + vec3(-0.18750, 0.03704, 0.12000), + vec3(0.81250, 0.70370, 0.52000), + vec3(-0.81250, -0.40741, 0.92000), + vec3(0.18750, 0.25926, -0.98400), + vec3(-0.31250, 0.92593, -0.58400), + vec3(0.68750, -0.97531, -0.18400), + vec3(-0.56250, -0.30864, 0.21600), + vec3(0.43750, 0.35802, 0.61600), + vec3(-0.06250, -0.75309, -0.90400), + vec3(0.93750, -0.08642, -0.50400), + vec3(-0.96875, 0.58025, -0.10400), + vec3(0.03125, -0.53086, 0.29600), + vec3(-0.46875, 0.13580, 0.69600), + vec3(0.53125, 0.80247, -0.82400), + vec3(-0.71875, -0.90123, -0.42400), + vec3(0.28125, -0.23457, -0.02400), + vec3(-0.21875, 0.43210, 0.37600), + vec3(0.78125, -0.67901, 0.77600), + vec3(-0.84375, -0.01235, -0.74400), + vec3(0.15625, 0.65432, -0.34400), + vec3(-0.34375, -0.45679, 0.05600), + vec3(0.65625, 0.20988, 0.45600), + vec3(-0.59375, 0.87654, 0.85600), + vec3(0.40625, -0.82716, -0.66400), + vec3(-0.09375, -0.16049, -0.26400), + vec3(0.90625, 0.50617, 0.13600), + vec3(-0.90625, -0.60494, 0.53600), + vec3(0.09375, 0.06173, 0.93600), + vec3(-0.40625, 0.72840, -0.96800), + vec3(0.59375, -0.38272, -0.56800), + vec3(-0.65625, 0.28395, -0.16800), + vec3(0.34375, 0.95062, 0.23200), + vec3(-0.15625, -0.95062, 0.63200), + vec3(0.84375, -0.28395, -0.88800), + vec3(-0.78125, 0.38272, -0.48800), + vec3(0.21875, -0.72840, -0.08800), + vec3(-0.28125, -0.06173, 0.31200), + vec3(0.71875, 0.60494, 0.71200), + vec3(-0.53125, -0.50617, -0.80800), + vec3(0.46875, 0.16049, -0.40800), + vec3(-0.03125, 0.82716, -0.00800), + vec3(0.96875, -0.87654, 0.39200), + vec3(-0.98438, -0.20988, 0.79200), + vec3(0.01562, 0.45679, -0.72800), + vec3(-0.48438, -0.65432, -0.32800), + vec3(0.51562, 0.01235, 0.07200), + vec3(-0.73438, 0.67901, 0.47200), + vec3(0.26562, -0.43210, 0.87200), + vec3(-0.23438, 0.23457, -0.64800), + vec3(0.76562, 0.90123, -0.24800), + vec3(-0.85938, -0.80247, 0.15200), + vec3(0.14062, -0.13580, 0.55200), + vec3(-0.35938, 0.53086, 0.95200), + vec3(0.64062, -0.58025, -0.95200), + vec3(-0.60938, 0.08642, -0.55200), + vec3(0.39062, 0.75309, -0.15200), + vec3(-0.10938, -0.35802, 0.24800), + vec3(0.89062, 0.30864, 0.64800), + vec3(-0.92188, 0.97531, -0.87200), + vec3(0.07812, -0.99177, -0.47200), + vec3(-0.42188, -0.32510, -0.07200), + vec3(0.57812, 0.34156, 0.32800), + vec3(-0.67188, -0.76955, 0.72800), + vec3(0.32812, -0.10288, -0.79200), + vec3(-0.17188, 0.56379, -0.39200), + vec3(0.82812, -0.54733, 0.00800), + vec3(-0.79688, 0.11934, 0.40800), + vec3(0.20312, 0.78601, 0.80800), + vec3(-0.29688, -0.91770, -0.71200), + vec3(0.70312, -0.25103, -0.31200), + vec3(-0.54688, 0.41564, 0.08800), + vec3(0.45312, -0.69547, 0.48800), + vec3(-0.04688, -0.02881, 0.88800), + vec3(0.95312, 0.63786, -0.63200), + vec3(-0.95312, -0.47325, -0.23200), + vec3(0.04688, 0.19342, 0.16800), + vec3(-0.45312, 0.86008, 0.56800), + vec3(0.54688, -0.84362, 0.96800), + vec3(-0.70312, -0.17695, -0.93600), + vec3(0.29688, 0.48971, -0.53600), + vec3(-0.20312, -0.62140, -0.13600), + vec3(0.79688, 0.04527, 0.26400), + vec3(-0.82812, 0.71193, 0.66400), + vec3(0.17188, -0.39918, -0.85600), + vec3(-0.32812, 0.26749, -0.45600), + vec3(0.67188, 0.93416, -0.05600), + vec3(-0.57812, -0.96708, 0.34400), + vec3(0.42188, -0.30041, 0.74400), + vec3(-0.07812, 0.36626, -0.77600), + vec3(0.92188, -0.74486, -0.37600), + vec3(-0.89062, -0.07819, 0.02400), + vec3(0.10938, 0.58848, 0.42400), + vec3(-0.39062, -0.52263, 0.82400), + vec3(0.60938, 0.14403, -0.69600), + vec3(-0.64062, 0.81070, -0.29600), + vec3(0.35938, -0.89300, 0.10400), + vec3(-0.14062, -0.22634, 0.50400), + vec3(0.85938, 0.44033, 0.90400), + vec3(-0.76562, -0.67078, -0.61600), + vec3(0.23438, -0.00412, -0.21600), + vec3(-0.26562, 0.66255, 0.18400), + vec3(0.73438, -0.44856, 0.58400), + vec3(-0.51562, 0.21811, 0.98400), + vec3(0.48438, 0.88477, -0.99680), + vec3(-0.01562, -0.81893, -0.59680), + vec3(0.98438, -0.15226, -0.19680), + vec3(-0.99219, 0.51440, 0.20320) +); diff --git a/RenderPipelineFile/rpcore/shader/includes/ies_lighting.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/ies_lighting.inc.glsl index d019c4a0..306c411d 100644 --- a/RenderPipelineFile/rpcore/shader/includes/ies_lighting.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/ies_lighting.inc.glsl @@ -1,55 +1,55 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -uniform sampler3D IESDatasetTex; - -// The ies profiles give us candela values, which we can directly convert to lumens -const float IES_SCALE = 1.0 /* lumens */; - -// Computes the IES factor from a given light vector and profile. -// A profile < 0 means no light profile. -float get_ies_factor(vec3 light_vector, int profile) { - if (profile < 0) - return 1.0; - light_vector = normalize(light_vector); - float horiz_angle = acos(light_vector.z) / M_PI; - float vert_angle = fma(atan(light_vector.x, light_vector.y), 1.0 / TWO_PI, 0.5); - float profile_coord = (profile+0.5) / MAX_IES_PROFILES; - float data = textureLod(IESDatasetTex, vec3(horiz_angle, vert_angle, profile_coord), 0).x; - return data * IES_SCALE; -} - -// Computes the IES factor from a given spherical coordinate and profile. -// theta and phi should range from 0 to 1. -float get_ies_factor(int profile, float theta, float phi) { - if (profile < 0) - return 1.0; - float profile_coord = (profile+0.5) / MAX_IES_PROFILES; - float data = textureLod(IESDatasetTex, vec3(theta * 0.5 + 0.5, phi, profile_coord), 0).x; - return data * IES_SCALE; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +uniform sampler3D IESDatasetTex; + +// The ies profiles give us candela values, which we can directly convert to lumens +const float IES_SCALE = 1.0 /* lumens */; + +// Computes the IES factor from a given light vector and profile. +// A profile < 0 means no light profile. +float get_ies_factor(vec3 light_vector, int profile) { + if (profile < 0) + return 1.0; + light_vector = normalize(light_vector); + float horiz_angle = acos(light_vector.z) / M_PI; + float vert_angle = fma(atan(light_vector.x, light_vector.y), 1.0 / TWO_PI, 0.5); + float profile_coord = (profile+0.5) / MAX_IES_PROFILES; + float data = textureLod(IESDatasetTex, vec3(horiz_angle, vert_angle, profile_coord), 0).x; + return data * IES_SCALE; +} + +// Computes the IES factor from a given spherical coordinate and profile. +// theta and phi should range from 0 to 1. +float get_ies_factor(int profile, float theta, float phi) { + if (profile < 0) + return 1.0; + float profile_coord = (profile+0.5) / MAX_IES_PROFILES; + float data = textureLod(IESDatasetTex, vec3(theta * 0.5 + 0.5, phi, profile_coord), 0).x; + return data * IES_SCALE; +} diff --git a/RenderPipelineFile/rpcore/shader/includes/importance_sampling.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/importance_sampling.inc.glsl index 831e41f8..440453c1 100644 --- a/RenderPipelineFile/rpcore/shader/includes/importance_sampling.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/importance_sampling.inc.glsl @@ -1,55 +1,55 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -// From: -// http://www.trentreed.net/blog/physically-based-shading-and-image-based-lighting/ -vec2 hammersley(uint i, uint N) -{ - return vec2(float(i) / float(N), float(bitfieldReverse(i)) * 2.3283064365386963e-10); -} - -// From: -// http://www.gamedev.net/topic/655431-ibl-problem-with-consistency-using-ggx-anisotropy/ -vec3 importance_sample_ggx(vec2 Xi, float alpha) -{ - // alpha is already squared roughness - float r_square = alpha * alpha; - float phi = TWO_PI * Xi.x; - float cos_theta_sq = (1 - Xi.y) / max(1e-3, 1 + (r_square * r_square - 1) * Xi.y); - float cos_theta = sqrt(cos_theta_sq); - float sin_theta = sqrt(max(0.0, 1.0 - cos_theta_sq)); - return vec3(sin_theta * cos(phi), sin_theta * sin(phi), cos_theta); -} - -vec3 importance_sample_lambert(vec2 Xi) -{ - float phi = TWO_PI * Xi.x; - float cos_theta = sqrt(Xi.y); - float sin_theta = sqrt(1 - Xi.y); - return vec3(sin_theta * cos(phi), sin_theta * sin(phi), cos_theta); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +// From: +// http://www.trentreed.net/blog/physically-based-shading-and-image-based-lighting/ +vec2 hammersley(uint i, uint N) +{ + return vec2(float(i) / float(N), float(bitfieldReverse(i)) * 2.3283064365386963e-10); +} + +// From: +// http://www.gamedev.net/topic/655431-ibl-problem-with-consistency-using-ggx-anisotropy/ +vec3 importance_sample_ggx(vec2 Xi, float alpha) +{ + // alpha is already squared roughness + float r_square = alpha * alpha; + float phi = TWO_PI * Xi.x; + float cos_theta_sq = (1 - Xi.y) / max(1e-3, 1 + (r_square * r_square - 1) * Xi.y); + float cos_theta = sqrt(cos_theta_sq); + float sin_theta = sqrt(max(0.0, 1.0 - cos_theta_sq)); + return vec3(sin_theta * cos(phi), sin_theta * sin(phi), cos_theta); +} + +vec3 importance_sample_lambert(vec2 Xi) +{ + float phi = TWO_PI * Xi.x; + float cos_theta = sqrt(Xi.y); + float sin_theta = sqrt(1 - Xi.y); + return vec3(sin_theta * cos(phi), sin_theta * sin(phi), cos_theta); +} diff --git a/RenderPipelineFile/rpcore/shader/includes/light_classification.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/light_classification.inc.glsl index 91646c69..e5151ed0 100644 --- a/RenderPipelineFile/rpcore/shader/includes/light_classification.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/light_classification.inc.glsl @@ -1,52 +1,52 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -#pragma include "includes/light_data.inc.glsl" - -#define LIGHT_CLS_INVALID -1 -#define LIGHT_CLS_SPOT_NOSHADOW 0 -#define LIGHT_CLS_POINT_NOSHADOW 1 -#define LIGHT_CLS_SPOT_SHADOW 2 -#define LIGHT_CLS_POINT_SHADOW 3 - -#define LIGHT_CLS_COUNT 4 - -#if LIGHT_CLS_COUNT != LC_LIGHT_CLASS_COUNT - #error GLSL and Python lighting system class count do not match up! -#endif - -int classify_light(int light_type, bool casts_shadows) { - switch (light_type) { - case LT_SPOT_LIGHT: - return casts_shadows ? LIGHT_CLS_SPOT_SHADOW : LIGHT_CLS_SPOT_NOSHADOW; - case LT_POINT_LIGHT: - return casts_shadows ? LIGHT_CLS_POINT_SHADOW : LIGHT_CLS_POINT_NOSHADOW; - - }; - return LIGHT_CLS_INVALID; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +#pragma include "includes/light_data.inc.glsl" + +#define LIGHT_CLS_INVALID -1 +#define LIGHT_CLS_SPOT_NOSHADOW 0 +#define LIGHT_CLS_POINT_NOSHADOW 1 +#define LIGHT_CLS_SPOT_SHADOW 2 +#define LIGHT_CLS_POINT_SHADOW 3 + +#define LIGHT_CLS_COUNT 4 + +#if LIGHT_CLS_COUNT != LC_LIGHT_CLASS_COUNT + #error GLSL and Python lighting system class count do not match up! +#endif + +int classify_light(int light_type, bool casts_shadows) { + switch (light_type) { + case LT_SPOT_LIGHT: + return casts_shadows ? LIGHT_CLS_SPOT_SHADOW : LIGHT_CLS_SPOT_NOSHADOW; + case LT_POINT_LIGHT: + return casts_shadows ? LIGHT_CLS_POINT_SHADOW : LIGHT_CLS_POINT_NOSHADOW; + + }; + return LIGHT_CLS_INVALID; +} diff --git a/RenderPipelineFile/rpcore/shader/includes/light_culling.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/light_culling.inc.glsl index 3caf56dc..ab849e23 100644 --- a/RenderPipelineFile/rpcore/shader/includes/light_culling.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/light_culling.inc.glsl @@ -1,186 +1,186 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -#pragma include "includes/transforms.inc.glsl" -#pragma include "includes/light_data.inc.glsl" - -// Controls the exponential factor, values < 1 produce a distribution closer to -// the camera, values > 1 produce a distribution which is further away from the camera. -#define SLICE_EXP_FACTOR 3.0 - -// Cell ray directions -const int num_raydirs = 5; - -// Increase the frustum size by a small bit, because we trace at the corners, -// since using this way we could miss some small parts of the sphere. With this -// bias we should be fine, except for very small spheres, but those will be -// out of the culling range then anyays -const float cull_bias = 1 + 0.01; -vec3 aspect_mul = vec3(1, ASPECT_RATIO, 1); - -CONST_ARRAY vec2 ray_dirs[num_raydirs] = vec2[]( - vec2(0, 0), - vec2(1.0, 1.0) * cull_bias, - vec2(-1.0, 1.0) * cull_bias, - vec2(1.0, -1.0) * cull_bias, - vec2(-1.0, -1.0) * cull_bias -); - -int get_slice_from_distance(float dist) { - float flt_dist = dist / LC_MAX_DISTANCE; - return int(log(flt_dist * SLICE_EXP_FACTOR + 1.0) / - log(1.0 + SLICE_EXP_FACTOR) * LC_TILE_SLICES); -} - -float get_distance_from_slice(int slice) { - float flt_dist = slice / float(LC_TILE_SLICES) * log(1.0 + SLICE_EXP_FACTOR); - float flt_exp = (exp(flt_dist) - 1.0) / SLICE_EXP_FACTOR; - return flt_exp * LC_MAX_DISTANCE; -} - -// Converts a coordinate and distance to the appropriate cell index -ivec3 get_lc_cell_index(ivec2 coord, float surface_distance) { - ivec2 tile = coord / ivec2(LC_TILE_SIZE_X, LC_TILE_SIZE_Y); - return ivec3(tile, get_slice_from_distance(surface_distance)); -} - -void unpack_cell_data(int packed_data, out int cell_x, out int cell_y, out int cell_slice) { - cell_x = packed_data & 0x3FF; - cell_y = (packed_data >> 10) & 0x3FF; - cell_slice = (packed_data >> 20) & 0x3FF; -} - -vec3 transform_raydir(vec2 dir, int cell_x, int cell_y, vec2 precompute_size) { - vec2 cell_pos = (vec2(cell_x, cell_y) + dir * 0.5 + 0.5) / precompute_size; - return normalize(mix( - mix(MainSceneData.vs_frustum_directions[0].xyz, - MainSceneData.vs_frustum_directions[1].xyz, cell_pos.x), - mix(MainSceneData.vs_frustum_directions[2].xyz, - MainSceneData.vs_frustum_directions[3].xyz, cell_pos.x), - cell_pos.y - )); -} - -CONST_ARRAY vec3[num_raydirs] get_raydirs(int cell_x, int cell_y, vec2 precompute_size) { - vec3 local_ray_dirs[num_raydirs]; - - // Generate ray directions - for (int i = 0; i < num_raydirs; ++i) { - local_ray_dirs[i] = transform_raydir(ray_dirs[i], cell_x, cell_y, precompute_size); - } - - return local_ray_dirs; -} - - -struct Sphere { - vec3 pos; - float radius; -}; - -// Interesects a sphere with a ray -// https://en.wikipedia.org/wiki/Line%E2%80%93sphere_intersection -bool ray_sphere_intersection(Sphere sphere, vec3 ray_start, vec3 ray_dir, - out float min_dist, out float max_dist) { - // Get vector from ray to sphere - vec3 o_minus_c = ray_start - sphere.pos; - - // Project that vector onto the ray - float l_dot_o_minus_c = dot(ray_dir, o_minus_c); - - // Compute the distance - float root = l_dot_o_minus_c * l_dot_o_minus_c - dot(o_minus_c, o_minus_c) + - sphere.radius * sphere.radius; - float sqr_root = sqrt(abs(root)); - - min_dist = -l_dot_o_minus_c + sqr_root; - max_dist = -l_dot_o_minus_c - sqr_root; - - return root > 0; // Can be >= 0 to include tangents as well. -} - -// Intersect a sphere with a ray -bool viewspace_ray_sphere_intersection(Sphere sphere, vec3 ray_dir, - out float min_dist, out float max_dist) { - return ray_sphere_intersection(sphere, vec3(0), ray_dir, min_dist, max_dist); -} - -// Intersect a sphere with a ray, given a minimum and maximum ray distance -bool viewspace_ray_sphere_distance_intersection(Sphere sphere, vec3 ray_dir, - float tile_start, float tile_end) { - float r_min, r_max; - bool visible = viewspace_ray_sphere_intersection(sphere, ray_dir, r_min, r_max); - return visible && r_max < tile_end && r_min > tile_start; -} - -// Returns a representative sphere for a light -Sphere get_representative_sphere(LightData data) { - Sphere ret; - - vec3 light_pos = (MainSceneData.view_mat_z_up * - vec4(get_light_position(data), 1)).xyz; - - switch (get_light_type(data)) { - case LT_POINT_LIGHT: { - ret.pos = light_pos; - ret.radius = get_pointlight_radius(data) - + get_pointlight_inner_radius(data); - break; - } - - case LT_SPOT_LIGHT: { - float cone_radius = get_spotlight_radius(data); - vec3 direction = get_spotlight_direction(data); - vec3 direction_view = world_normal_to_view(direction); - float cone_fov = get_spotlight_fov(data); - - // Approximate the cone with a sphere - // See: http://fs5.directupload.net/images/151219/xp2knkre.png - float half_cone_radius = cone_radius * 0.5; - ret.pos = light_pos + direction_view * half_cone_radius; - float hypotenuse = cone_radius / cone_fov; - - // cone_fov is encoded as cos(cone_fov) - // we can get the sin(cone_fov) using basic trigonometry: - // From sin(x)^2 + cos(x)^2 = 1 we can derive: - // sin(cone_fov) = sqrt(1 - cos(cone_fov) * cos(cone_fov)) - #if 0 - // Unoptimized version - float opposite_side = sqrt(1.0 - cone_fov * cone_fov) * hypotenuse; - ret.radius = sqrt(opposite_side * opposite_side + - half_cone_radius * half_cone_radius); - #else - // To optimize this, we don't need the square root any longer: - float opposite_side_sqr = (1.0 - cone_fov * cone_fov) * hypotenuse * hypotenuse; - ret.radius = sqrt(opposite_side_sqr + half_cone_radius * half_cone_radius); - #endif - } - } - - return ret; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +#pragma include "includes/transforms.inc.glsl" +#pragma include "includes/light_data.inc.glsl" + +// Controls the exponential factor, values < 1 produce a distribution closer to +// the camera, values > 1 produce a distribution which is further away from the camera. +#define SLICE_EXP_FACTOR 3.0 + +// Cell ray directions +const int num_raydirs = 5; + +// Increase the frustum size by a small bit, because we trace at the corners, +// since using this way we could miss some small parts of the sphere. With this +// bias we should be fine, except for very small spheres, but those will be +// out of the culling range then anyays +const float cull_bias = 1 + 0.01; +vec3 aspect_mul = vec3(1, ASPECT_RATIO, 1); + +CONST_ARRAY vec2 ray_dirs[num_raydirs] = vec2[]( + vec2(0, 0), + vec2(1.0, 1.0) * cull_bias, + vec2(-1.0, 1.0) * cull_bias, + vec2(1.0, -1.0) * cull_bias, + vec2(-1.0, -1.0) * cull_bias +); + +int get_slice_from_distance(float dist) { + float flt_dist = dist / LC_MAX_DISTANCE; + return int(log(flt_dist * SLICE_EXP_FACTOR + 1.0) / + log(1.0 + SLICE_EXP_FACTOR) * LC_TILE_SLICES); +} + +float get_distance_from_slice(int slice) { + float flt_dist = slice / float(LC_TILE_SLICES) * log(1.0 + SLICE_EXP_FACTOR); + float flt_exp = (exp(flt_dist) - 1.0) / SLICE_EXP_FACTOR; + return flt_exp * LC_MAX_DISTANCE; +} + +// Converts a coordinate and distance to the appropriate cell index +ivec3 get_lc_cell_index(ivec2 coord, float surface_distance) { + ivec2 tile = coord / ivec2(LC_TILE_SIZE_X, LC_TILE_SIZE_Y); + return ivec3(tile, get_slice_from_distance(surface_distance)); +} + +void unpack_cell_data(int packed_data, out int cell_x, out int cell_y, out int cell_slice) { + cell_x = packed_data & 0x3FF; + cell_y = (packed_data >> 10) & 0x3FF; + cell_slice = (packed_data >> 20) & 0x3FF; +} + +vec3 transform_raydir(vec2 dir, int cell_x, int cell_y, vec2 precompute_size) { + vec2 cell_pos = (vec2(cell_x, cell_y) + dir * 0.5 + 0.5) / precompute_size; + return normalize(mix( + mix(MainSceneData.vs_frustum_directions[0].xyz, + MainSceneData.vs_frustum_directions[1].xyz, cell_pos.x), + mix(MainSceneData.vs_frustum_directions[2].xyz, + MainSceneData.vs_frustum_directions[3].xyz, cell_pos.x), + cell_pos.y + )); +} + +CONST_ARRAY vec3[num_raydirs] get_raydirs(int cell_x, int cell_y, vec2 precompute_size) { + vec3 local_ray_dirs[num_raydirs]; + + // Generate ray directions + for (int i = 0; i < num_raydirs; ++i) { + local_ray_dirs[i] = transform_raydir(ray_dirs[i], cell_x, cell_y, precompute_size); + } + + return local_ray_dirs; +} + + +struct Sphere { + vec3 pos; + float radius; +}; + +// Interesects a sphere with a ray +// https://en.wikipedia.org/wiki/Line%E2%80%93sphere_intersection +bool ray_sphere_intersection(Sphere sphere, vec3 ray_start, vec3 ray_dir, + out float min_dist, out float max_dist) { + // Get vector from ray to sphere + vec3 o_minus_c = ray_start - sphere.pos; + + // Project that vector onto the ray + float l_dot_o_minus_c = dot(ray_dir, o_minus_c); + + // Compute the distance + float root = l_dot_o_minus_c * l_dot_o_minus_c - dot(o_minus_c, o_minus_c) + + sphere.radius * sphere.radius; + float sqr_root = sqrt(abs(root)); + + min_dist = -l_dot_o_minus_c + sqr_root; + max_dist = -l_dot_o_minus_c - sqr_root; + + return root > 0; // Can be >= 0 to include tangents as well. +} + +// Intersect a sphere with a ray +bool viewspace_ray_sphere_intersection(Sphere sphere, vec3 ray_dir, + out float min_dist, out float max_dist) { + return ray_sphere_intersection(sphere, vec3(0), ray_dir, min_dist, max_dist); +} + +// Intersect a sphere with a ray, given a minimum and maximum ray distance +bool viewspace_ray_sphere_distance_intersection(Sphere sphere, vec3 ray_dir, + float tile_start, float tile_end) { + float r_min, r_max; + bool visible = viewspace_ray_sphere_intersection(sphere, ray_dir, r_min, r_max); + return visible && r_max < tile_end && r_min > tile_start; +} + +// Returns a representative sphere for a light +Sphere get_representative_sphere(LightData data) { + Sphere ret; + + vec3 light_pos = (MainSceneData.view_mat_z_up * + vec4(get_light_position(data), 1)).xyz; + + switch (get_light_type(data)) { + case LT_POINT_LIGHT: { + ret.pos = light_pos; + ret.radius = get_pointlight_radius(data) + + get_pointlight_inner_radius(data); + break; + } + + case LT_SPOT_LIGHT: { + float cone_radius = get_spotlight_radius(data); + vec3 direction = get_spotlight_direction(data); + vec3 direction_view = world_normal_to_view(direction); + float cone_fov = get_spotlight_fov(data); + + // Approximate the cone with a sphere + // See: http://fs5.directupload.net/images/151219/xp2knkre.png + float half_cone_radius = cone_radius * 0.5; + ret.pos = light_pos + direction_view * half_cone_radius; + float hypotenuse = cone_radius / cone_fov; + + // cone_fov is encoded as cos(cone_fov) + // we can get the sin(cone_fov) using basic trigonometry: + // From sin(x)^2 + cos(x)^2 = 1 we can derive: + // sin(cone_fov) = sqrt(1 - cos(cone_fov) * cos(cone_fov)) + #if 0 + // Unoptimized version + float opposite_side = sqrt(1.0 - cone_fov * cone_fov) * hypotenuse; + ret.radius = sqrt(opposite_side * opposite_side + + half_cone_radius * half_cone_radius); + #else + // To optimize this, we don't need the square root any longer: + float opposite_side_sqr = (1.0 - cone_fov * cone_fov) * hypotenuse * hypotenuse; + ret.radius = sqrt(opposite_side_sqr + half_cone_radius * half_cone_radius); + #endif + } + } + + return ret; +} diff --git a/RenderPipelineFile/rpcore/shader/includes/light_data.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/light_data.inc.glsl index 27f4225b..04ac66e9 100644 --- a/RenderPipelineFile/rpcore/shader/includes/light_data.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/light_data.inc.glsl @@ -1,126 +1,126 @@ -#pragma once - - -#pragma include "includes/light_data.struct.glsl" -#pragma include "includes/source_data.struct.glsl" - -/* - -Description of the packing and the layout can be found here: -https://github.com/tobspr/RenderPipeline/wiki/LightStorage - -*/ - -// Reads the light data from a given buffer and offset -LightData read_light_data(samplerBuffer LightDataBuffer, int offset) { - LightData data; - data.Data0 = texelFetch(LightDataBuffer, offset * 4 + 0); - data.Data1 = texelFetch(LightDataBuffer, offset * 4 + 1); - data.Data2 = texelFetch(LightDataBuffer, offset * 4 + 2); - data.Data3 = texelFetch(LightDataBuffer, offset * 4 + 3); - return data; -} - -// Only reads the light type, in case nothing else is required -int read_light_type(samplerBuffer LightDataBuffer, int offset) { - float data0x = texelFetch(LightDataBuffer, offset * 4 + 0).x; - return gpu_cq_unpack_int_from_float(data0x); -} - -bool read_casts_shadows(samplerBuffer LightDataBuffer, int offset) { - float data0z = texelFetch(LightDataBuffer, offset * 4 + 0).z; - return gpu_cq_unpack_int_from_float(data0z) >= 0; -} - -// Extracts the type of a light -int get_light_type(LightData data) { - return gpu_cq_unpack_int_from_float(data.Data0.x); -} - -// Extracts the ies profile index of a light -int get_ies_profile(LightData data) { - return gpu_cq_unpack_int_from_float(data.Data0.y); -} - -int get_shadow_source_index(LightData data) { - return gpu_cq_unpack_int_from_float(data.Data0.z); -} - -bool get_casts_shadows(LightData data) { - return get_shadow_source_index(data) >= 0; -} - -// Extracts the light world space position -vec3 get_light_position(LightData data) { - return vec3(data.Data0.w, data.Data1.xy); -} - -// Extracts the light color -vec3 get_light_color(LightData data) { - return vec3(data.Data1.zw, data.Data2.x) * 100.0; -} - -/* - -Point Light Dataset - -*/ - -// Extracts the radius of a point light -float get_pointlight_radius(LightData data) { - return data.Data2.y; -} - -// Extracts the inner radius of a point light -float get_pointlight_inner_radius(LightData data) { - return data.Data2.z; -} - -/* - -Spot Light Dataset - -*/ - -// Extracts the radius of a spot light -float get_spotlight_radius(LightData data) { - return data.Data2.y; -} - -// Extracts the fov of a spot light -float get_spotlight_fov(LightData data) { - return data.Data2.z; -} - -// Extracts the direction of a spot light -vec3 get_spotlight_direction(LightData data) { - return vec3(data.Data2.w, data.Data3.xy); -} - - - -/* - -Shadow sources - -*/ - -// Reads the shadow source data from a given buffer and offset -SourceData read_source_data(samplerBuffer SourceDataBuffer, int offset) { - SourceData data; - data.Data0 = texelFetch(SourceDataBuffer, offset + 0); - data.Data1 = texelFetch(SourceDataBuffer, offset + 1); - data.Data2 = texelFetch(SourceDataBuffer, offset + 2); - data.Data3 = texelFetch(SourceDataBuffer, offset + 3); - data.Data4 = texelFetch(SourceDataBuffer, offset + 4); - return data; -} - - -mat4 get_source_mvp(SourceData data) { - return mat4(data.Data0, data.Data1, data.Data2, data.Data3); -} - -vec4 get_source_uv(SourceData data) { - return data.Data4; -} +#pragma once + + +#pragma include "includes/light_data.struct.glsl" +#pragma include "includes/source_data.struct.glsl" + +/* + +Description of the packing and the layout can be found here: +https://github.com/tobspr/RenderPipeline/wiki/LightStorage + +*/ + +// Reads the light data from a given buffer and offset +LightData read_light_data(samplerBuffer LightDataBuffer, int offset) { + LightData data; + data.Data0 = texelFetch(LightDataBuffer, offset * 4 + 0); + data.Data1 = texelFetch(LightDataBuffer, offset * 4 + 1); + data.Data2 = texelFetch(LightDataBuffer, offset * 4 + 2); + data.Data3 = texelFetch(LightDataBuffer, offset * 4 + 3); + return data; +} + +// Only reads the light type, in case nothing else is required +int read_light_type(samplerBuffer LightDataBuffer, int offset) { + float data0x = texelFetch(LightDataBuffer, offset * 4 + 0).x; + return gpu_cq_unpack_int_from_float(data0x); +} + +bool read_casts_shadows(samplerBuffer LightDataBuffer, int offset) { + float data0z = texelFetch(LightDataBuffer, offset * 4 + 0).z; + return gpu_cq_unpack_int_from_float(data0z) >= 0; +} + +// Extracts the type of a light +int get_light_type(LightData data) { + return gpu_cq_unpack_int_from_float(data.Data0.x); +} + +// Extracts the ies profile index of a light +int get_ies_profile(LightData data) { + return gpu_cq_unpack_int_from_float(data.Data0.y); +} + +int get_shadow_source_index(LightData data) { + return gpu_cq_unpack_int_from_float(data.Data0.z); +} + +bool get_casts_shadows(LightData data) { + return get_shadow_source_index(data) >= 0; +} + +// Extracts the light world space position +vec3 get_light_position(LightData data) { + return vec3(data.Data0.w, data.Data1.xy); +} + +// Extracts the light color +vec3 get_light_color(LightData data) { + return vec3(data.Data1.zw, data.Data2.x) * 100.0; +} + +/* + +Point Light Dataset + +*/ + +// Extracts the radius of a point light +float get_pointlight_radius(LightData data) { + return data.Data2.y; +} + +// Extracts the inner radius of a point light +float get_pointlight_inner_radius(LightData data) { + return data.Data2.z; +} + +/* + +Spot Light Dataset + +*/ + +// Extracts the radius of a spot light +float get_spotlight_radius(LightData data) { + return data.Data2.y; +} + +// Extracts the fov of a spot light +float get_spotlight_fov(LightData data) { + return data.Data2.z; +} + +// Extracts the direction of a spot light +vec3 get_spotlight_direction(LightData data) { + return vec3(data.Data2.w, data.Data3.xy); +} + + + +/* + +Shadow sources + +*/ + +// Reads the shadow source data from a given buffer and offset +SourceData read_source_data(samplerBuffer SourceDataBuffer, int offset) { + SourceData data; + data.Data0 = texelFetch(SourceDataBuffer, offset + 0); + data.Data1 = texelFetch(SourceDataBuffer, offset + 1); + data.Data2 = texelFetch(SourceDataBuffer, offset + 2); + data.Data3 = texelFetch(SourceDataBuffer, offset + 3); + data.Data4 = texelFetch(SourceDataBuffer, offset + 4); + return data; +} + + +mat4 get_source_mvp(SourceData data) { + return mat4(data.Data0, data.Data1, data.Data2, data.Data3); +} + +vec4 get_source_uv(SourceData data) { + return data.Data4; +} diff --git a/RenderPipelineFile/rpcore/shader/includes/light_data.struct.glsl b/RenderPipelineFile/rpcore/shader/includes/light_data.struct.glsl index 0ab16385..18d9c14c 100644 --- a/RenderPipelineFile/rpcore/shader/includes/light_data.struct.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/light_data.struct.glsl @@ -1,8 +1,8 @@ -#pragma once - -struct LightData { - vec4 Data0; - vec4 Data1; - vec4 Data2; - vec4 Data3; -}; +#pragma once + +struct LightData { + vec4 Data0; + vec4 Data1; + vec4 Data2; + vec4 Data3; +}; diff --git a/RenderPipelineFile/rpcore/shader/includes/lighting_pipeline.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/lighting_pipeline.inc.glsl index c0c2573e..86ea7e08 100644 --- a/RenderPipelineFile/rpcore/shader/includes/lighting_pipeline.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/lighting_pipeline.inc.glsl @@ -1,267 +1,267 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -#pragma include "includes/material.inc.glsl" -#pragma include "includes/light_culling.inc.glsl" -#pragma include "includes/lights.inc.glsl" -#pragma include "includes/light_data.inc.glsl" -#pragma include "includes/shadows.inc.glsl" -#pragma include "includes/noise.inc.glsl" -#pragma include "includes/light_classification.inc.glsl" -#pragma include "includes/poisson_disk.inc.glsl" - -uniform isampler2DArray CellIndices; -uniform usamplerBuffer PerCellLights; -uniform usamplerBuffer PerCellLightsCounts; -uniform samplerBuffer AllLightsData; -uniform samplerBuffer ShadowSourceData; - -uniform sampler2D ShadowAtlas; - -#if SUPPORT_PCF -uniform sampler2DShadow ShadowAtlasPCF; -#endif - -int get_pointlight_source_offs(vec3 direction) { - vec3 abs_dir = abs(direction); - float max_comp = max3(abs_dir.x, abs_dir.y, abs_dir.z); - if (abs_dir.x >= max_comp - 1e-5) return direction.x >= 0.0 ? 0 : 1; - if (abs_dir.y >= max_comp - 1e-5) return direction.y >= 0.0 ? 2 : 3; - return direction.z >= 0.0 ? 4 : 5; -} - -// Processes a spot light -vec3 process_spotlight(Material m, LightData light_data, vec3 view_vector, float shadow_factor) { - const vec3 transmittance = vec3(1); // <-- TODO - - // Get the lights data - int ies_profile = get_ies_profile(light_data); - vec3 position = get_light_position(light_data); - float radius = get_spotlight_radius(light_data); - float fov = get_spotlight_fov(light_data); - vec3 direction = get_spotlight_direction(light_data); - vec3 l = position - m.position; - vec3 l_norm = normalize(l); - - float attenuation = get_spotlight_attenuation( - l_norm, direction, fov, radius, dot(l, l), ies_profile); - - // Compute the lights influence - return apply_light( - m, view_vector, l_norm, get_light_color(light_data), attenuation, - shadow_factor, transmittance, ONE_BY_PI, ONE_BY_PI, l_norm); -} - -// Processes a point light -vec3 process_pointlight(Material m, LightData light_data, vec3 view_vector, float shadow_factor) { - const vec3 transmittance = vec3(1); // <-- TODO - - // Get the lights data - float radius = get_pointlight_radius(light_data); - float inner_radius = get_pointlight_inner_radius(light_data); - vec3 position = get_light_position(light_data); - int ies_profile = get_ies_profile(light_data); - vec3 l = position - m.position; - float l_len_square = length_squared(l); - - float energy = 1.0 / FOUR_PI; - float dist_sq = l_len_square; - float clearcoat_energy = energy; - vec3 l_diff = l; - - // Spherical area light - if (inner_radius > 0.02) { - l_diff = get_spherical_area_light_horizon(l, m.normal, inner_radius); - l = get_spherical_area_light_vector(m.normal, l, view_vector, inner_radius); - energy = get_spherical_area_light_energy(m.roughness, inner_radius, dist_sq); - dist_sq = max(square(inner_radius) + 0.01, l_len_square - square(inner_radius)); - clearcoat_energy = get_spherical_area_light_energy(CLEARCOAT_ROUGHNESS, inner_radius, dist_sq); - } - - // Get the point light attenuation - float attenuation = attenuation_curve(dist_sq, radius) * get_ies_factor(-l, ies_profile); - - // Compute the lights influence - return apply_light(m, view_vector, normalize(l), get_light_color(light_data), - attenuation, shadow_factor, transmittance, energy, clearcoat_energy, l_diff); -} - - - -// Filters a shadow map -float filter_shadowmap(Material m, SourceData source, vec3 l) { - - // TODO: Examine if this is faster - // if (dot(m.normal, -l) < 0) return 0.0; - - mat4 mvp = get_source_mvp(source); - vec4 uv = get_source_uv(source); - - float rotation = interleaved_gradient_noise( - gl_FragCoord.xy + MainSceneData.frame_index % 32); - mat2 rotation_mat = make_rotation_mat(rotation); - - // TODO: make this configurable - // XXX: Scale by resolution (higher resolution needs smaller bias) - const float slope_bias = 0.001; - const float normal_bias = 0.0001; - const float const_bias = 0.003; - vec3 biased_pos = get_biased_position(m.position, slope_bias, normal_bias, m.normal, -l); - - vec3 projected = project(mvp, biased_pos); - vec2 projected_coord = projected.xy * uv.zw + uv.xy; - - const int num_samples = 8; - const float filter_size = 3.0 / SHADOW_ATLAS_SIZE; - - float accum = 0.0; - - for (int i = 0; i < num_samples; ++i) { - vec2 offs = projected_coord.xy + (rotation_mat * shadow_sample_offsets_8[i]) * filter_size; - #if SUPPORT_PCF - accum += textureLod(ShadowAtlasPCF, vec3(offs, projected.z - const_bias), 0).x; - #else - accum += textureLod(ShadowAtlas, vec2(offs), 0).x > - projected.z - const_bias ? 1.0 : 0.0; - #endif - } - - return accum / num_samples; -} - - - -// Shades the material from the per cell light buffer -vec3 shade_material_from_tile_buffer(Material m, ivec3 tile) { - - #if DEBUG_MODE && !MODE_ACTIVE(LIGHT_COUNT) && !SPECIAL_MODE_ACTIVE(LIGHT_TILES) - return vec3(0); - #endif - - // Skip emissive materials - if (m.shading_model == SHADING_MODEL_EMISSIVE) - return vec3(0); - - vec3 shading_result = vec3(0); - - // Find per tile lights - int cell_index = texelFetch(CellIndices, tile, 0).x; - int count_offs = cell_index * (1 + LIGHT_CLS_COUNT); // 1 for total count, rest for light classes - - - // Get the light counts - uint num_total_lights = texelFetch(PerCellLightsCounts, count_offs).x; - - // Early out when no lights are there - #if !MODE_ACTIVE(LIGHT_COUNT) && !SPECIAL_MODE_ACTIVE(LIGHT_TILES) - if (num_total_lights == 0) { - return vec3(0); - } - #endif - - // Get the per-class counts - uint num_spot_noshadow = texelFetch(PerCellLightsCounts, count_offs + 1 + LIGHT_CLS_SPOT_NOSHADOW).x; - uint num_spot_shadow = texelFetch(PerCellLightsCounts, count_offs + 1 + LIGHT_CLS_SPOT_SHADOW).x; - uint num_point_noshadow = texelFetch(PerCellLightsCounts, count_offs + 1 + LIGHT_CLS_POINT_NOSHADOW).x; - uint num_point_shadow = texelFetch(PerCellLightsCounts, count_offs + 1 + LIGHT_CLS_POINT_SHADOW).x; - - // Compute the index into the culled lights list - int data_offs = cell_index * LC_MAX_LIGHTS_PER_CELL; - int curr_offs = data_offs; - - #if MODE_ACTIVE(LIGHT_COUNT) - float factor = num_total_lights / float(LC_MAX_LIGHTS_PER_CELL); - return vec3(factor, 1 - factor, 0); - #endif - - // Debug mode, show tile bounds - #if SPECIAL_MODE_ACTIVE(LIGHT_TILES) - // Show tiles - #if IS_SCREEN_SPACE - if (int(gl_FragCoord.x) % LC_TILE_SIZE_X == 0 || - int(gl_FragCoord.y) % LC_TILE_SIZE_Y == 0) { - shading_result += 1.0; - } - float light_factor = num_total_lights / float(LC_MAX_LIGHTS_PER_CELL); - shading_result += ((tile.z + 1) % 2) * 0.2; - shading_result += light_factor; - #endif - #endif - - // Compute view vector - vec3 v = normalize(MainSceneData.camera_pos - m.position); - - // Spotlights without shadow - for (int i = 0; i < num_spot_noshadow; ++i) { - int light_offs = int(texelFetch(PerCellLights, curr_offs++).x); - LightData light_data = read_light_data(AllLightsData, light_offs); - shading_result += process_spotlight(m, light_data, v, 1.0); - } - - // Pointlights without shadow - for (int i = 0; i < num_point_noshadow; ++i) { - int light_offs = int(texelFetch(PerCellLights, curr_offs++).x); - LightData light_data = read_light_data(AllLightsData, light_offs); - shading_result += process_pointlight(m, light_data, v, 1.0); - } - - // Spotlights with shadow - for (int i = 0; i < num_spot_shadow; ++i) { - int light_offs = int(texelFetch(PerCellLights, curr_offs++).x); - LightData light_data = read_light_data(AllLightsData, light_offs); - - // Get shadow factor - vec3 v2l = normalize(m.position - get_light_position(light_data)); - int source_index = get_shadow_source_index(light_data); - SourceData source_data = read_source_data(ShadowSourceData, source_index * 5); - float shadow_factor = filter_shadowmap(m, source_data, v2l); - shading_result += process_spotlight(m, light_data, v, shadow_factor); - } - - // Pointlights with shadow - for (int i = 0; i < num_point_shadow; ++i) { - int light_offs = int(texelFetch(PerCellLights, curr_offs++).x); - LightData light_data = read_light_data(AllLightsData, light_offs); - - // Get shadow factor - int source_index = get_shadow_source_index(light_data); - vec3 v2l = normalize(m.position - get_light_position(light_data)); - source_index += get_pointlight_source_offs(v2l); - - SourceData source_data = read_source_data(ShadowSourceData, source_index * 5); - float shadow_factor = filter_shadowmap(m, source_data, v2l); - shading_result += process_pointlight(m, light_data, v, shadow_factor); - } - - // Fade out lights as they reach the culling distance - float curr_dist = distance(m.position, MainSceneData.camera_pos); - float fade = saturate(curr_dist / LC_MAX_DISTANCE); - fade = 1 - pow(fade, 10.0); - - return shading_result * fade; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +#pragma include "includes/material.inc.glsl" +#pragma include "includes/light_culling.inc.glsl" +#pragma include "includes/lights.inc.glsl" +#pragma include "includes/light_data.inc.glsl" +#pragma include "includes/shadows.inc.glsl" +#pragma include "includes/noise.inc.glsl" +#pragma include "includes/light_classification.inc.glsl" +#pragma include "includes/poisson_disk.inc.glsl" + +uniform isampler2DArray CellIndices; +uniform usamplerBuffer PerCellLights; +uniform usamplerBuffer PerCellLightsCounts; +uniform samplerBuffer AllLightsData; +uniform samplerBuffer ShadowSourceData; + +uniform sampler2D ShadowAtlas; + +#if SUPPORT_PCF +uniform sampler2DShadow ShadowAtlasPCF; +#endif + +int get_pointlight_source_offs(vec3 direction) { + vec3 abs_dir = abs(direction); + float max_comp = max3(abs_dir.x, abs_dir.y, abs_dir.z); + if (abs_dir.x >= max_comp - 1e-5) return direction.x >= 0.0 ? 0 : 1; + if (abs_dir.y >= max_comp - 1e-5) return direction.y >= 0.0 ? 2 : 3; + return direction.z >= 0.0 ? 4 : 5; +} + +// Processes a spot light +vec3 process_spotlight(Material m, LightData light_data, vec3 view_vector, float shadow_factor) { + const vec3 transmittance = vec3(1); // <-- TODO + + // Get the lights data + int ies_profile = get_ies_profile(light_data); + vec3 position = get_light_position(light_data); + float radius = get_spotlight_radius(light_data); + float fov = get_spotlight_fov(light_data); + vec3 direction = get_spotlight_direction(light_data); + vec3 l = position - m.position; + vec3 l_norm = normalize(l); + + float attenuation = get_spotlight_attenuation( + l_norm, direction, fov, radius, dot(l, l), ies_profile); + + // Compute the lights influence + return apply_light( + m, view_vector, l_norm, get_light_color(light_data), attenuation, + shadow_factor, transmittance, ONE_BY_PI, ONE_BY_PI, l_norm); +} + +// Processes a point light +vec3 process_pointlight(Material m, LightData light_data, vec3 view_vector, float shadow_factor) { + const vec3 transmittance = vec3(1); // <-- TODO + + // Get the lights data + float radius = get_pointlight_radius(light_data); + float inner_radius = get_pointlight_inner_radius(light_data); + vec3 position = get_light_position(light_data); + int ies_profile = get_ies_profile(light_data); + vec3 l = position - m.position; + float l_len_square = length_squared(l); + + float energy = 1.0 / FOUR_PI; + float dist_sq = l_len_square; + float clearcoat_energy = energy; + vec3 l_diff = l; + + // Spherical area light + if (inner_radius > 0.02) { + l_diff = get_spherical_area_light_horizon(l, m.normal, inner_radius); + l = get_spherical_area_light_vector(m.normal, l, view_vector, inner_radius); + energy = get_spherical_area_light_energy(m.roughness, inner_radius, dist_sq); + dist_sq = max(square(inner_radius) + 0.01, l_len_square - square(inner_radius)); + clearcoat_energy = get_spherical_area_light_energy(CLEARCOAT_ROUGHNESS, inner_radius, dist_sq); + } + + // Get the point light attenuation + float attenuation = attenuation_curve(dist_sq, radius) * get_ies_factor(-l, ies_profile); + + // Compute the lights influence + return apply_light(m, view_vector, normalize(l), get_light_color(light_data), + attenuation, shadow_factor, transmittance, energy, clearcoat_energy, l_diff); +} + + + +// Filters a shadow map +float filter_shadowmap(Material m, SourceData source, vec3 l) { + + // TODO: Examine if this is faster + // if (dot(m.normal, -l) < 0) return 0.0; + + mat4 mvp = get_source_mvp(source); + vec4 uv = get_source_uv(source); + + float rotation = interleaved_gradient_noise( + gl_FragCoord.xy + MainSceneData.frame_index % 32); + mat2 rotation_mat = make_rotation_mat(rotation); + + // TODO: make this configurable + // XXX: Scale by resolution (higher resolution needs smaller bias) + const float slope_bias = 0.001; + const float normal_bias = 0.0001; + const float const_bias = 0.003; + vec3 biased_pos = get_biased_position(m.position, slope_bias, normal_bias, m.normal, -l); + + vec3 projected = project(mvp, biased_pos); + vec2 projected_coord = projected.xy * uv.zw + uv.xy; + + const int num_samples = 8; + const float filter_size = 3.0 / SHADOW_ATLAS_SIZE; + + float accum = 0.0; + + for (int i = 0; i < num_samples; ++i) { + vec2 offs = projected_coord.xy + (rotation_mat * shadow_sample_offsets_8[i]) * filter_size; + #if SUPPORT_PCF + accum += textureLod(ShadowAtlasPCF, vec3(offs, projected.z - const_bias), 0).x; + #else + accum += textureLod(ShadowAtlas, vec2(offs), 0).x > + projected.z - const_bias ? 1.0 : 0.0; + #endif + } + + return accum / num_samples; +} + + + +// Shades the material from the per cell light buffer +vec3 shade_material_from_tile_buffer(Material m, ivec3 tile) { + + #if DEBUG_MODE && !MODE_ACTIVE(LIGHT_COUNT) && !SPECIAL_MODE_ACTIVE(LIGHT_TILES) + return vec3(0); + #endif + + // Skip emissive materials + if (m.shading_model == SHADING_MODEL_EMISSIVE) + return vec3(0); + + vec3 shading_result = vec3(0); + + // Find per tile lights + int cell_index = texelFetch(CellIndices, tile, 0).x; + int count_offs = cell_index * (1 + LIGHT_CLS_COUNT); // 1 for total count, rest for light classes + + + // Get the light counts + uint num_total_lights = texelFetch(PerCellLightsCounts, count_offs).x; + + // Early out when no lights are there + #if !MODE_ACTIVE(LIGHT_COUNT) && !SPECIAL_MODE_ACTIVE(LIGHT_TILES) + if (num_total_lights == 0) { + return vec3(0); + } + #endif + + // Get the per-class counts + uint num_spot_noshadow = texelFetch(PerCellLightsCounts, count_offs + 1 + LIGHT_CLS_SPOT_NOSHADOW).x; + uint num_spot_shadow = texelFetch(PerCellLightsCounts, count_offs + 1 + LIGHT_CLS_SPOT_SHADOW).x; + uint num_point_noshadow = texelFetch(PerCellLightsCounts, count_offs + 1 + LIGHT_CLS_POINT_NOSHADOW).x; + uint num_point_shadow = texelFetch(PerCellLightsCounts, count_offs + 1 + LIGHT_CLS_POINT_SHADOW).x; + + // Compute the index into the culled lights list + int data_offs = cell_index * LC_MAX_LIGHTS_PER_CELL; + int curr_offs = data_offs; + + #if MODE_ACTIVE(LIGHT_COUNT) + float factor = num_total_lights / float(LC_MAX_LIGHTS_PER_CELL); + return vec3(factor, 1 - factor, 0); + #endif + + // Debug mode, show tile bounds + #if SPECIAL_MODE_ACTIVE(LIGHT_TILES) + // Show tiles + #if IS_SCREEN_SPACE + if (int(gl_FragCoord.x) % LC_TILE_SIZE_X == 0 || + int(gl_FragCoord.y) % LC_TILE_SIZE_Y == 0) { + shading_result += 1.0; + } + float light_factor = num_total_lights / float(LC_MAX_LIGHTS_PER_CELL); + shading_result += ((tile.z + 1) % 2) * 0.2; + shading_result += light_factor; + #endif + #endif + + // Compute view vector + vec3 v = normalize(MainSceneData.camera_pos - m.position); + + // Spotlights without shadow + for (int i = 0; i < num_spot_noshadow; ++i) { + int light_offs = int(texelFetch(PerCellLights, curr_offs++).x); + LightData light_data = read_light_data(AllLightsData, light_offs); + shading_result += process_spotlight(m, light_data, v, 1.0); + } + + // Pointlights without shadow + for (int i = 0; i < num_point_noshadow; ++i) { + int light_offs = int(texelFetch(PerCellLights, curr_offs++).x); + LightData light_data = read_light_data(AllLightsData, light_offs); + shading_result += process_pointlight(m, light_data, v, 1.0); + } + + // Spotlights with shadow + for (int i = 0; i < num_spot_shadow; ++i) { + int light_offs = int(texelFetch(PerCellLights, curr_offs++).x); + LightData light_data = read_light_data(AllLightsData, light_offs); + + // Get shadow factor + vec3 v2l = normalize(m.position - get_light_position(light_data)); + int source_index = get_shadow_source_index(light_data); + SourceData source_data = read_source_data(ShadowSourceData, source_index * 5); + float shadow_factor = filter_shadowmap(m, source_data, v2l); + shading_result += process_spotlight(m, light_data, v, shadow_factor); + } + + // Pointlights with shadow + for (int i = 0; i < num_point_shadow; ++i) { + int light_offs = int(texelFetch(PerCellLights, curr_offs++).x); + LightData light_data = read_light_data(AllLightsData, light_offs); + + // Get shadow factor + int source_index = get_shadow_source_index(light_data); + vec3 v2l = normalize(m.position - get_light_position(light_data)); + source_index += get_pointlight_source_offs(v2l); + + SourceData source_data = read_source_data(ShadowSourceData, source_index * 5); + float shadow_factor = filter_shadowmap(m, source_data, v2l); + shading_result += process_pointlight(m, light_data, v, shadow_factor); + } + + // Fade out lights as they reach the culling distance + float curr_dist = distance(m.position, MainSceneData.camera_pos); + float fade = saturate(curr_dist / LC_MAX_DISTANCE); + fade = 1 - pow(fade, 10.0); + + return shading_result * fade; +} diff --git a/RenderPipelineFile/rpcore/shader/includes/lights.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/lights.inc.glsl index a423c504..b9c183e9 100644 --- a/RenderPipelineFile/rpcore/shader/includes/lights.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/lights.inc.glsl @@ -1,151 +1,151 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -#pragma include "includes/material.inc.glsl" -#pragma include "includes/brdf.inc.glsl" -#pragma include "includes/ies_lighting.inc.glsl" - -// Computes the quadratic attenuation curve -float attenuation_curve(float dist_square, float radius) { - #if 0 - return step(dist_square, radius * radius); - #endif - - #if 1 - float factor = dist_square / (radius * radius); - float smooth_factor = saturate(1.0 - factor * factor); - return smooth_factor * smooth_factor / max(0.01 * 0.01, dist_square); - #endif -} - - -// Computes the attenuation for a spot light -float get_spotlight_attenuation(vec3 l, vec3 light_dir, float fov, float radius, - float dist_sq, int ies_profile) { - float dist_attenuation = attenuation_curve(dist_sq, radius); - float cos_angle = dot(l, -light_dir); - - // Rescale angle to fit the full range of the IES profile. We only do this - // for spot lights, for point lights we use the actual angle. - // This is NOT physically correct for spotlights without a FoV of 180deg. - // However, IES profiles might look quite boring when not getting rescaled, - // so the rescaling is performed. - float linear_angle = (cos_angle - fov) / (1 - fov); - float angle_att = saturate(linear_angle); - float ies_factor = get_ies_factor(ies_profile, linear_angle, 0); - return ies_factor * angle_att * angle_att * dist_attenuation; -} - - -// Closest point on spherical area light, also returns energy factor -vec3 get_spherical_area_light_vector(vec3 n, vec3 l_unscaled, vec3 v, float radius) { - vec3 r = reflect(-v, n); - vec3 center_to_ray = dot(l_unscaled, r) * r - l_unscaled; - vec3 closest_point = l_unscaled + center_to_ray * - saturate(radius / max(1e-3, length(center_to_ray))); - return closest_point; -} - -vec3 get_spherical_area_light_horizon(vec3 l_unscaled, vec3 n, float radius) { - return normalize(l_unscaled + n * (0.5 * radius)); -} - -float get_spherical_area_light_energy(float alpha, float radius, float dist_sq) { - return max(0.000005, alpha * alpha) / max(0.01, radius * radius) * 4 * M_PI; -} - -// Computes a lights influence -// TODO: Make this method faster -vec3 apply_light(Material m, vec3 v, vec3 l, vec3 light_color, float attenuation, float shadow, - vec3 transmittance, float energy, float clearcoat_energy, vec3 l_diffuse) { - - // Debugging: Fast rendering path - #if 0 - return light_color * attenuation; - #endif - - float NxL = saturate(dot(m.normal, l_diffuse)); - - if (m.shading_model == SHADING_MODEL_FOLIAGE) { - transmittance = transmittance.xxx; - } else if (m.shading_model == SHADING_MODEL_SKIN) { - NxL = saturate(0.3 + dot(m.normal, l_diffuse)); - } else { - transmittance = vec3(1); - } - - // Compute the dot products - vec3 h = normalize(l + v); - float NxV = max(1e-5, dot(m.normal, v)); - float NxH = max(1e-5, dot(m.normal, h)); - float VxH = clamp(dot(v, h), 1e-5, 1.0); - float LxH = max(0, dot(l, h)); - - vec3 f0 = get_material_f0(m); - - // Diffuse contribution - vec3 shading_result = brdf_diffuse(NxV, NxL, LxH, VxH, m.roughness) - * m.basecolor * (1 - m.metallic); - - // Specular contribution: - // We add some roughness for clearcoat - this is due to the reason that - // light gets scattered and thus a wider highlight is shown. - // This approximates the reference in mitsuba very well. - // float distribution = brdf_distribution(NxH, m.roughness); - float distribution = brdf_distribution(NxH, m.roughness); // xxx - float visibility = brdf_visibility(NxL, NxV, NxH, VxH, m.roughness); - vec3 fresnel = brdf_schlick_fresnel(f0, LxH); - - - // The division by 4 * NxV * NxL is done in the geometric (visibility) term - // already, so to evaluate the complete brdf we just do a multiply - shading_result += (distribution * visibility) * fresnel / (4.0 * NxV * NxL) * energy; - - if (m.shading_model == SHADING_MODEL_CLEARCOAT) { - float distribution_coat = brdf_distribution(NxH, CLEARCOAT_ROUGHNESS); - float visibility_coat = brdf_visibility(NxL, NxV, NxH, VxH, CLEARCOAT_ROUGHNESS); - vec3 fresnel_coat = brdf_schlick_fresnel(vec3(CLEARCOAT_SPECULAR), LxH); - - // Approximation to match reference - shading_result *= (1 - fresnel_coat.x); - shading_result *= 0.4 + 3.0 * m.linear_roughness; - shading_result *= 0.5 + 0.5 * m.basecolor; - - vec3 coat_spec = (distribution_coat * visibility_coat * clearcoat_energy) * fresnel_coat; - shading_result += coat_spec; - } - - return max(vec3(0), - (shading_result * light_color) * (attenuation * shadow * NxL) * transmittance); -} - -vec3 apply_light(Material m, vec3 v, vec3 l, vec3 light_color, float attenuation, - float shadow, vec3 transmittance) { - return apply_light( - m, v, l, light_color, attenuation, shadow, transmittance, 1.0, 1.0, l); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +#pragma include "includes/material.inc.glsl" +#pragma include "includes/brdf.inc.glsl" +#pragma include "includes/ies_lighting.inc.glsl" + +// Computes the quadratic attenuation curve +float attenuation_curve(float dist_square, float radius) { + #if 0 + return step(dist_square, radius * radius); + #endif + + #if 1 + float factor = dist_square / (radius * radius); + float smooth_factor = saturate(1.0 - factor * factor); + return smooth_factor * smooth_factor / max(0.01 * 0.01, dist_square); + #endif +} + + +// Computes the attenuation for a spot light +float get_spotlight_attenuation(vec3 l, vec3 light_dir, float fov, float radius, + float dist_sq, int ies_profile) { + float dist_attenuation = attenuation_curve(dist_sq, radius); + float cos_angle = dot(l, -light_dir); + + // Rescale angle to fit the full range of the IES profile. We only do this + // for spot lights, for point lights we use the actual angle. + // This is NOT physically correct for spotlights without a FoV of 180deg. + // However, IES profiles might look quite boring when not getting rescaled, + // so the rescaling is performed. + float linear_angle = (cos_angle - fov) / (1 - fov); + float angle_att = saturate(linear_angle); + float ies_factor = get_ies_factor(ies_profile, linear_angle, 0); + return ies_factor * angle_att * angle_att * dist_attenuation; +} + + +// Closest point on spherical area light, also returns energy factor +vec3 get_spherical_area_light_vector(vec3 n, vec3 l_unscaled, vec3 v, float radius) { + vec3 r = reflect(-v, n); + vec3 center_to_ray = dot(l_unscaled, r) * r - l_unscaled; + vec3 closest_point = l_unscaled + center_to_ray * + saturate(radius / max(1e-3, length(center_to_ray))); + return closest_point; +} + +vec3 get_spherical_area_light_horizon(vec3 l_unscaled, vec3 n, float radius) { + return normalize(l_unscaled + n * (0.5 * radius)); +} + +float get_spherical_area_light_energy(float alpha, float radius, float dist_sq) { + return max(0.000005, alpha * alpha) / max(0.01, radius * radius) * 4 * M_PI; +} + +// Computes a lights influence +// TODO: Make this method faster +vec3 apply_light(Material m, vec3 v, vec3 l, vec3 light_color, float attenuation, float shadow, + vec3 transmittance, float energy, float clearcoat_energy, vec3 l_diffuse) { + + // Debugging: Fast rendering path + #if 0 + return light_color * attenuation; + #endif + + float NxL = saturate(dot(m.normal, l_diffuse)); + + if (m.shading_model == SHADING_MODEL_FOLIAGE) { + transmittance = transmittance.xxx; + } else if (m.shading_model == SHADING_MODEL_SKIN) { + NxL = saturate(0.3 + dot(m.normal, l_diffuse)); + } else { + transmittance = vec3(1); + } + + // Compute the dot products + vec3 h = normalize(l + v); + float NxV = max(1e-5, dot(m.normal, v)); + float NxH = max(1e-5, dot(m.normal, h)); + float VxH = clamp(dot(v, h), 1e-5, 1.0); + float LxH = max(0, dot(l, h)); + + vec3 f0 = get_material_f0(m); + + // Diffuse contribution + vec3 shading_result = brdf_diffuse(NxV, NxL, LxH, VxH, m.roughness) + * m.basecolor * (1 - m.metallic); + + // Specular contribution: + // We add some roughness for clearcoat - this is due to the reason that + // light gets scattered and thus a wider highlight is shown. + // This approximates the reference in mitsuba very well. + // float distribution = brdf_distribution(NxH, m.roughness); + float distribution = brdf_distribution(NxH, m.roughness); // xxx + float visibility = brdf_visibility(NxL, NxV, NxH, VxH, m.roughness); + vec3 fresnel = brdf_schlick_fresnel(f0, LxH); + + + // The division by 4 * NxV * NxL is done in the geometric (visibility) term + // already, so to evaluate the complete brdf we just do a multiply + shading_result += (distribution * visibility) * fresnel / (4.0 * NxV * NxL) * energy; + + if (m.shading_model == SHADING_MODEL_CLEARCOAT) { + float distribution_coat = brdf_distribution(NxH, CLEARCOAT_ROUGHNESS); + float visibility_coat = brdf_visibility(NxL, NxV, NxH, VxH, CLEARCOAT_ROUGHNESS); + vec3 fresnel_coat = brdf_schlick_fresnel(vec3(CLEARCOAT_SPECULAR), LxH); + + // Approximation to match reference + shading_result *= (1 - fresnel_coat.x); + shading_result *= 0.4 + 3.0 * m.linear_roughness; + shading_result *= 0.5 + 0.5 * m.basecolor; + + vec3 coat_spec = (distribution_coat * visibility_coat * clearcoat_energy) * fresnel_coat; + shading_result += coat_spec; + } + + return max(vec3(0), + (shading_result * light_color) * (attenuation * shadow * NxL) * transmittance); +} + +vec3 apply_light(Material m, vec3 v, vec3 l, vec3 light_color, float attenuation, + float shadow, vec3 transmittance) { + return apply_light( + m, v, l, light_color, attenuation, shadow, transmittance, 1.0, 1.0, l); +} diff --git a/RenderPipelineFile/rpcore/shader/includes/material.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/material.inc.glsl index 3fa42eb6..fc25d871 100644 --- a/RenderPipelineFile/rpcore/shader/includes/material.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/material.inc.glsl @@ -1,146 +1,146 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -#define SHADING_MODEL_DEFAULT 0 -#define SHADING_MODEL_EMISSIVE 1 -#define SHADING_MODEL_CLEARCOAT 2 -#define SHADING_MODEL_TRANSPARENT 3 -#define SHADING_MODEL_SKIN 4 -#define SHADING_MODEL_FOLIAGE 5 - -// Pandas material representation -struct Panda3DMaterial { - vec4 baseColor; - vec4 emission; - float roughness; - float metallic; - float refractiveIndex; -}; - -// Structure passed from the vertex to the fragment shader -struct MaterialBaseInput { - vec3 color; - int shading_model; - float specular_ior; - float metallic; - float roughness; - float normalfactor; - float arbitrary0; - // float arbitrary1; -}; - -// Converts from a Panda3D Material to a render pipeline material -MaterialBaseInput get_input_from_p3d(Panda3DMaterial m) { - MaterialBaseInput mi; - mi.color = m.baseColor.xyz; - mi.specular_ior = m.refractiveIndex; - mi.metallic = m.metallic; - mi.roughness = m.roughness; - mi.shading_model = int(m.emission.x); - mi.normalfactor = m.emission.y; - mi.arbitrary0 = m.emission.z; - // mi.arbitrary1 = m.emission.w; - return mi; -} - -// Structure used in the Material Templates -struct MaterialShaderOutput { - int shading_model; - vec3 basecolor; - vec3 normal; - float roughness; - float specular_ior; - float metallic; - float shading_model_param0; -}; - - -// Structure actually stored in the GBuffer, this *may* differ but not necessarily has to: -struct Material { - int shading_model; - vec3 basecolor; - vec3 normal; - vec3 position; - float roughness; - float specular; - float specular_ior; - float metallic; - float shading_model_param0; - float linear_roughness; -}; - -// Merges two materials, by doing (per component): -// result = current + to_add * factor; -void merge_material_output(inout MaterialShaderOutput current, - MaterialShaderOutput to_add, float factor) { - current.basecolor += to_add.basecolor * factor; - current.roughness += to_add.roughness * factor; - current.specular_ior += to_add.specular_ior * factor; - current.metallic += to_add.metallic * factor; - current.shading_model_param0 += to_add.shading_model_param0 * factor; - current.normal += to_add.normal * factor; -} - -// Creates an empty material -MaterialShaderOutput make_default_material_output() { - MaterialShaderOutput result; - result.shading_model = SHADING_MODEL_DEFAULT; - result.basecolor = vec3(0.8); - result.normal = vec3(0, 0, 1); - result.roughness = 0.3; - result.specular_ior = 1.51; - result.metallic = 0.0; - result.shading_model_param0 = 0.0; - return result; -} - -// Required for ior_to_specular -#pragma include "includes/brdf.inc.glsl" - -#ifdef IN_RENDERING_PASS - - - // Emulates the gbuffer pass - Material emulate_gbuffer_pass(MaterialShaderOutput m_out, vec3 position) { - // Copy properties - Material m; - m.shading_model = m_out.shading_model; - m.basecolor = m_out.basecolor; - m.normal = m_out.normal; - m.specular_ior = m_out.specular_ior; - m.metallic = m_out.metallic; - m.shading_model_param0 = m_out.shading_model_param0; - - // Assign new properties - m.position = position; - m.specular = ior_to_specular(m_out.specular_ior); - m.linear_roughness = m_out.roughness; - m.roughness *= m.roughness; - return m; - } -#endif +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +#define SHADING_MODEL_DEFAULT 0 +#define SHADING_MODEL_EMISSIVE 1 +#define SHADING_MODEL_CLEARCOAT 2 +#define SHADING_MODEL_TRANSPARENT 3 +#define SHADING_MODEL_SKIN 4 +#define SHADING_MODEL_FOLIAGE 5 + +// Pandas material representation +struct Panda3DMaterial { + vec4 baseColor; + vec4 emission; + float roughness; + float metallic; + float refractiveIndex; +}; + +// Structure passed from the vertex to the fragment shader +struct MaterialBaseInput { + vec3 color; + int shading_model; + float specular_ior; + float metallic; + float roughness; + float normalfactor; + float arbitrary0; + // float arbitrary1; +}; + +// Converts from a Panda3D Material to a render pipeline material +MaterialBaseInput get_input_from_p3d(Panda3DMaterial m) { + MaterialBaseInput mi; + mi.color = m.baseColor.xyz; + mi.specular_ior = m.refractiveIndex; + mi.metallic = m.metallic; + mi.roughness = m.roughness; + mi.shading_model = int(m.emission.x); + mi.normalfactor = m.emission.y; + mi.arbitrary0 = m.emission.z; + // mi.arbitrary1 = m.emission.w; + return mi; +} + +// Structure used in the Material Templates +struct MaterialShaderOutput { + int shading_model; + vec3 basecolor; + vec3 normal; + float roughness; + float specular_ior; + float metallic; + float shading_model_param0; +}; + + +// Structure actually stored in the GBuffer, this *may* differ but not necessarily has to: +struct Material { + int shading_model; + vec3 basecolor; + vec3 normal; + vec3 position; + float roughness; + float specular; + float specular_ior; + float metallic; + float shading_model_param0; + float linear_roughness; +}; + +// Merges two materials, by doing (per component): +// result = current + to_add * factor; +void merge_material_output(inout MaterialShaderOutput current, + MaterialShaderOutput to_add, float factor) { + current.basecolor += to_add.basecolor * factor; + current.roughness += to_add.roughness * factor; + current.specular_ior += to_add.specular_ior * factor; + current.metallic += to_add.metallic * factor; + current.shading_model_param0 += to_add.shading_model_param0 * factor; + current.normal += to_add.normal * factor; +} + +// Creates an empty material +MaterialShaderOutput make_default_material_output() { + MaterialShaderOutput result; + result.shading_model = SHADING_MODEL_DEFAULT; + result.basecolor = vec3(0.8); + result.normal = vec3(0, 0, 1); + result.roughness = 0.3; + result.specular_ior = 1.51; + result.metallic = 0.0; + result.shading_model_param0 = 0.0; + return result; +} + +// Required for ior_to_specular +#pragma include "includes/brdf.inc.glsl" + +#ifdef IN_RENDERING_PASS + + + // Emulates the gbuffer pass + Material emulate_gbuffer_pass(MaterialShaderOutput m_out, vec3 position) { + // Copy properties + Material m; + m.shading_model = m_out.shading_model; + m.basecolor = m_out.basecolor; + m.normal = m_out.normal; + m.specular_ior = m_out.specular_ior; + m.metallic = m_out.metallic; + m.shading_model_param0 = m_out.shading_model_param0; + + // Assign new properties + m.position = position; + m.specular = ior_to_specular(m_out.specular_ior); + m.linear_roughness = m_out.roughness; + m.roughness *= m.roughness; + return m; + } +#endif diff --git a/RenderPipelineFile/rpcore/shader/includes/noise.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/noise.inc.glsl index c5bf7d0a..67a84027 100644 --- a/RenderPipelineFile/rpcore/shader/includes/noise.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/noise.inc.glsl @@ -1,328 +1,328 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -/* - -Noise Functions from: -https://github.com/hughsk/glsl-noise - -Check out the repository for a copy of the license: -https://github.com/hughsk/glsl-noise/blob/master/LICENSE - -*/ - -vec3 mod289(vec3 x) { - return x - floor(x * (1.0 / 289.0)) * 289.0; -} - -vec4 mod289(vec4 x) { - return x - floor(x * (1.0 / 289.0)) * 289.0; -} - -vec4 permute(vec4 x) { - return mod289(((x * 34.0) + 1.0) * x); -} - -vec4 taylorInvSqrt(vec4 r) -{ - return 1.79284291400159 - 0.85373472095314 * r; -} - -vec3 fade(vec3 t) { - return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); -} - - -float snoise3D(vec3 v) { - const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0) ; - const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); - - // First corner - vec3 i = floor(v + dot(v, C.yyy)); - vec3 x0 = v - i + dot(i, C.xxx); - - // Other corners - vec3 g = step(x0.yzx, x0.xyz); - vec3 l = 1.0 - g; - vec3 i1 = min(g.xyz, l.zxy); - vec3 i2 = max(g.xyz, l.zxy); - - // x0 = x0 - 0.0 + 0.0 * C.xxx; - // x1 = x0 - i1 + 1.0 * C.xxx; - // x2 = x0 - i2 + 2.0 * C.xxx; - // x3 = x0 - 1.0 + 3.0 * C.xxx; - vec3 x1 = x0 - i1 + C.xxx; - vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y - vec3 x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y - - // Permutations - i = mod289(i); - vec4 p = permute(permute(permute( - i.z + vec4(0.0, i1.z, i2.z, 1.0)) - + i.y + vec4(0.0, i1.y, i2.y, 1.0)) - + i.x + vec4(0.0, i1.x, i2.x, 1.0)); - - // Gradients: 7x7 points over a square, mapped onto an octahedron. - // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294) - float n_ = 0.142857142857; // 1.0/7.0 - vec3 ns = n_ * D.wyz - D.xzx; - - vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7) - - vec4 x_ = floor(j * ns.z); - vec4 y_ = floor(j - 7.0 * x_); // mod(j,N) - - vec4 x = x_ * ns.x + ns.yyyy; - vec4 y = y_ * ns.x + ns.yyyy; - vec4 h = 1.0 - abs(x) - abs(y); - - vec4 b0 = vec4(x.xy, y.xy); - vec4 b1 = vec4(x.zw, y.zw); - - //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0; - //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0; - vec4 s0 = floor(b0) * 2.0 + 1.0; - vec4 s1 = floor(b1) * 2.0 + 1.0; - vec4 sh = -step(h, vec4(0.0)); - - vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy ; - vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww ; - - vec3 p0 = vec3(a0.xy, h.x); - vec3 p1 = vec3(a0.zw, h.y); - vec3 p2 = vec3(a1.xy, h.z); - vec3 p3 = vec3(a1.zw, h.w); - - // Normalise gradients - vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3))); - p0 *= norm.x; - p1 *= norm.y; - p2 *= norm.z; - p3 *= norm.w; - - // Mix final noise value - vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0); - m = m * m; - return 42.0 * dot(m * m, - vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3))); -} - - -// Classic Perlin noise, periodic variant -float pnoise3D(vec3 P, vec3 rep) -{ - vec3 Pi0 = mod(floor(P), rep); // Integer part, modulo period - vec3 Pi1 = mod(Pi0 + vec3(1.0), rep); // Integer part + 1, mod period - Pi0 = mod289(Pi0); - Pi1 = mod289(Pi1); - vec3 Pf0 = fract(P); // Fractional part for interpolation - vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0 - vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); - vec4 iy = vec4(Pi0.yy, Pi1.yy); - vec4 iz0 = Pi0.zzzz; - vec4 iz1 = Pi1.zzzz; - - vec4 ixy = permute(permute(ix) + iy); - vec4 ixy0 = permute(ixy + iz0); - vec4 ixy1 = permute(ixy + iz1); - - vec4 gx0 = ixy0 * (1.0 / 7.0); - vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5; - gx0 = fract(gx0); - vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); - vec4 sz0 = step(gz0, vec4(0.0)); - gx0 -= sz0 * (step(0.0, gx0) - 0.5); - gy0 -= sz0 * (step(0.0, gy0) - 0.5); - - vec4 gx1 = ixy1 * (1.0 / 7.0); - vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5; - gx1 = fract(gx1); - vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); - vec4 sz1 = step(gz1, vec4(0.0)); - gx1 -= sz1 * (step(0.0, gx1) - 0.5); - gy1 -= sz1 * (step(0.0, gy1) - 0.5); - - vec3 g000 = vec3(gx0.x, gy0.x, gz0.x); - vec3 g100 = vec3(gx0.y, gy0.y, gz0.y); - vec3 g010 = vec3(gx0.z, gy0.z, gz0.z); - vec3 g110 = vec3(gx0.w, gy0.w, gz0.w); - vec3 g001 = vec3(gx1.x, gy1.x, gz1.x); - vec3 g101 = vec3(gx1.y, gy1.y, gz1.y); - vec3 g011 = vec3(gx1.z, gy1.z, gz1.z); - vec3 g111 = vec3(gx1.w, gy1.w, gz1.w); - - vec4 norm0 = taylorInvSqrt(vec4( - dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110))); - g000 *= norm0.x; - g010 *= norm0.y; - g100 *= norm0.z; - g110 *= norm0.w; - vec4 norm1 = taylorInvSqrt(vec4( - dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111))); - g001 *= norm1.x; - g011 *= norm1.y; - g101 *= norm1.z; - g111 *= norm1.w; - - float n000 = dot(g000, Pf0); - float n100 = dot(g100, vec3(Pf1.x, Pf0.yz)); - float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z)); - float n110 = dot(g110, vec3(Pf1.xy, Pf0.z)); - float n001 = dot(g001, vec3(Pf0.xy, Pf1.z)); - float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z)); - float n011 = dot(g011, vec3(Pf0.x, Pf1.yz)); - float n111 = dot(g111, Pf1); - - vec3 fade_xyz = fade(Pf0); - vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z); - vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y); - float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); - return 2.2 * n_xyz; -} - - -float rand(vec2 co){ - return abs(fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453)) * 2 - 1; -} - - -vec3 rand_rgb(vec2 co) -{ - return abs(fract(sin(dot(co.xy, vec2(34.4835, 89.6372))) * - vec3(29156.4765, 38273.56393, 47843.75468))) * 2 - 1; -} - - -uniform sampler2D PrecomputedGrain; - -// Computes the film grain using the precomputed grain -float grain(float frame_time) { - float offs = mod(frame_time, 7.2342); - ivec2 offs_coord = ivec2(offs * 28947.0, (offs - 2.5) * 12484.0); - return texelFetch(PrecomputedGrain, (ivec2(gl_FragCoord.xy) + offs_coord) % ivec2(2048), 0).x; -} - - -float fbm(vec3 x, float scale) { - float v = 0.0; - float a = 0.5; - float shift = 0.0; - x *= scale; - for (int i = 0; i < 5; ++i) { - v += a * (pnoise3D(x, vec3(scale)) * 0.5 + 0.5); - x = x * 2.0 + shift; - a *= 0.5; - } - return v; -} - - -// Returns the point in a given cell -vec2 worley_cell_point(ivec2 cell, int num_cells, float drop_rate) { - cell = cell % num_cells; - vec2 cell_base = vec2(cell) / num_cells; - float noise_x = rand(cell_base); - float noise_y = rand(cell_base.yx); - float drop_point = step(rand(cell) + 1e-7, drop_rate); - return cell_base + (0.5 + 1.0 * vec2(noise_x, noise_y)) / num_cells + drop_point * vec2(1e9); -} - - -// Distance accross borders -float distance_border(vec2 a, vec2 b) { - float dx = min(abs(a.x - b.x), min(abs(a.x - 1.0 - b.x), abs(a.x + 1.0 - b.x))); - float dy = min(abs(a.y - b.y), min(abs(a.y - 1.0 - b.y), abs(a.y + 1.0 - b.y))); - return length(vec2(dx, dy)); -} - -// Performs worley noise by checking all adjacent cells -// and comparing the distance to their points -float worley_noise(vec2 coord, int num_cells, float drop_rate) { - coord = fract(coord); - ivec2 cell = ivec2(coord * num_cells); - float dist = 1.0; - - // Search in the surrounding 5x5 cell block - for (int x = 0; x < 5; x++) { - for (int y = 0; y < 5; y++) { - vec2 cell_point = worley_cell_point(cell + ivec2(x-2, y-2), num_cells, drop_rate); - dist = min(dist, distance_border(cell_point, coord)); - } - } - dist /= length(vec2(1.0 / num_cells)); - dist = 1.0 - dist; - return dist; -} - -// Returns the point in a given cell -vec3 worley_cell_point(ivec3 cell, int num_cells, float drop_rate) { - cell = cell % num_cells; - vec3 cell_base = vec3(cell) / num_cells; - float noise_x = rand(cell_base.xy); - float noise_y = rand(cell_base.yx); - float noise_z = rand(cell_base.zx + cell_base.yy); - float drop_point = step(rand(cell.xy + cell.zz) + 1e-7, drop_rate); - return cell_base + (0.5 + 1.0 * vec3(noise_x, noise_y, noise_z)) / - num_cells + drop_point * vec3(1e9); -} - - -// Distance accross borders -float distance_border(vec3 a, vec3 b) { - float dx = min(abs(a.x - b.x), min(abs(a.x - 1.0 - b.x), abs(a.x + 1.0 - b.x))); - float dy = min(abs(a.y - b.y), min(abs(a.y - 1.0 - b.y), abs(a.y + 1.0 - b.y))); - float dz = min(abs(a.z - b.z), min(abs(a.z - 1.0 - b.z), abs(a.z + 1.0 - b.z))); - return length(vec3(dx, dy, dz)); -} - -// Performs worley noise by checking all adjacent cells -// and comparing the distance to their points -float worley_noise(vec3 coord, int num_cells, float drop_rate) { - coord = fract(coord); - ivec3 cell = ivec3(coord * num_cells); - float dist = 1.0; - - // Search in the surrounding 5x5 cell block - for (int x = 0; x < 5; x++) { - for (int y = 0; y < 5; y++) { - for (int z = 0; z < 5; z++) { - vec3 cell_point = worley_cell_point(cell + ivec3(x-2, y-2, z-2), num_cells, drop_rate); - dist = min(dist, distance_border(cell_point, coord)); - } - } - } - dist /= length(vec3(1.0 / num_cells)); - dist = 1.0 - dist; - return dist; -} - -float interleaved_gradient_noise(vec2 seed) { - const float scale = 3.14159; - vec3 magic = vec3(0.06711056, 0.00583715, 52.9829189); - return -scale + 2.0 * scale * fract(magic.z * fract(dot(seed, magic.xy))); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +/* + +Noise Functions from: +https://github.com/hughsk/glsl-noise + +Check out the repository for a copy of the license: +https://github.com/hughsk/glsl-noise/blob/master/LICENSE + +*/ + +vec3 mod289(vec3 x) { + return x - floor(x * (1.0 / 289.0)) * 289.0; +} + +vec4 mod289(vec4 x) { + return x - floor(x * (1.0 / 289.0)) * 289.0; +} + +vec4 permute(vec4 x) { + return mod289(((x * 34.0) + 1.0) * x); +} + +vec4 taylorInvSqrt(vec4 r) +{ + return 1.79284291400159 - 0.85373472095314 * r; +} + +vec3 fade(vec3 t) { + return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); +} + + +float snoise3D(vec3 v) { + const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0) ; + const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); + + // First corner + vec3 i = floor(v + dot(v, C.yyy)); + vec3 x0 = v - i + dot(i, C.xxx); + + // Other corners + vec3 g = step(x0.yzx, x0.xyz); + vec3 l = 1.0 - g; + vec3 i1 = min(g.xyz, l.zxy); + vec3 i2 = max(g.xyz, l.zxy); + + // x0 = x0 - 0.0 + 0.0 * C.xxx; + // x1 = x0 - i1 + 1.0 * C.xxx; + // x2 = x0 - i2 + 2.0 * C.xxx; + // x3 = x0 - 1.0 + 3.0 * C.xxx; + vec3 x1 = x0 - i1 + C.xxx; + vec3 x2 = x0 - i2 + C.yyy; // 2.0*C.x = 1/3 = C.y + vec3 x3 = x0 - D.yyy; // -1.0+3.0*C.x = -0.5 = -D.y + + // Permutations + i = mod289(i); + vec4 p = permute(permute(permute( + i.z + vec4(0.0, i1.z, i2.z, 1.0)) + + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + + i.x + vec4(0.0, i1.x, i2.x, 1.0)); + + // Gradients: 7x7 points over a square, mapped onto an octahedron. + // The ring size 17*17 = 289 is close to a multiple of 49 (49*6 = 294) + float n_ = 0.142857142857; // 1.0/7.0 + vec3 ns = n_ * D.wyz - D.xzx; + + vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,7*7) + + vec4 x_ = floor(j * ns.z); + vec4 y_ = floor(j - 7.0 * x_); // mod(j,N) + + vec4 x = x_ * ns.x + ns.yyyy; + vec4 y = y_ * ns.x + ns.yyyy; + vec4 h = 1.0 - abs(x) - abs(y); + + vec4 b0 = vec4(x.xy, y.xy); + vec4 b1 = vec4(x.zw, y.zw); + + //vec4 s0 = vec4(lessThan(b0,0.0))*2.0 - 1.0; + //vec4 s1 = vec4(lessThan(b1,0.0))*2.0 - 1.0; + vec4 s0 = floor(b0) * 2.0 + 1.0; + vec4 s1 = floor(b1) * 2.0 + 1.0; + vec4 sh = -step(h, vec4(0.0)); + + vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy ; + vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww ; + + vec3 p0 = vec3(a0.xy, h.x); + vec3 p1 = vec3(a0.zw, h.y); + vec3 p2 = vec3(a1.xy, h.z); + vec3 p3 = vec3(a1.zw, h.w); + + // Normalise gradients + vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3))); + p0 *= norm.x; + p1 *= norm.y; + p2 *= norm.z; + p3 *= norm.w; + + // Mix final noise value + vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0); + m = m * m; + return 42.0 * dot(m * m, + vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3))); +} + + +// Classic Perlin noise, periodic variant +float pnoise3D(vec3 P, vec3 rep) +{ + vec3 Pi0 = mod(floor(P), rep); // Integer part, modulo period + vec3 Pi1 = mod(Pi0 + vec3(1.0), rep); // Integer part + 1, mod period + Pi0 = mod289(Pi0); + Pi1 = mod289(Pi1); + vec3 Pf0 = fract(P); // Fractional part for interpolation + vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0 + vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); + vec4 iy = vec4(Pi0.yy, Pi1.yy); + vec4 iz0 = Pi0.zzzz; + vec4 iz1 = Pi1.zzzz; + + vec4 ixy = permute(permute(ix) + iy); + vec4 ixy0 = permute(ixy + iz0); + vec4 ixy1 = permute(ixy + iz1); + + vec4 gx0 = ixy0 * (1.0 / 7.0); + vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5; + gx0 = fract(gx0); + vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); + vec4 sz0 = step(gz0, vec4(0.0)); + gx0 -= sz0 * (step(0.0, gx0) - 0.5); + gy0 -= sz0 * (step(0.0, gy0) - 0.5); + + vec4 gx1 = ixy1 * (1.0 / 7.0); + vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5; + gx1 = fract(gx1); + vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); + vec4 sz1 = step(gz1, vec4(0.0)); + gx1 -= sz1 * (step(0.0, gx1) - 0.5); + gy1 -= sz1 * (step(0.0, gy1) - 0.5); + + vec3 g000 = vec3(gx0.x, gy0.x, gz0.x); + vec3 g100 = vec3(gx0.y, gy0.y, gz0.y); + vec3 g010 = vec3(gx0.z, gy0.z, gz0.z); + vec3 g110 = vec3(gx0.w, gy0.w, gz0.w); + vec3 g001 = vec3(gx1.x, gy1.x, gz1.x); + vec3 g101 = vec3(gx1.y, gy1.y, gz1.y); + vec3 g011 = vec3(gx1.z, gy1.z, gz1.z); + vec3 g111 = vec3(gx1.w, gy1.w, gz1.w); + + vec4 norm0 = taylorInvSqrt(vec4( + dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110))); + g000 *= norm0.x; + g010 *= norm0.y; + g100 *= norm0.z; + g110 *= norm0.w; + vec4 norm1 = taylorInvSqrt(vec4( + dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111))); + g001 *= norm1.x; + g011 *= norm1.y; + g101 *= norm1.z; + g111 *= norm1.w; + + float n000 = dot(g000, Pf0); + float n100 = dot(g100, vec3(Pf1.x, Pf0.yz)); + float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z)); + float n110 = dot(g110, vec3(Pf1.xy, Pf0.z)); + float n001 = dot(g001, vec3(Pf0.xy, Pf1.z)); + float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z)); + float n011 = dot(g011, vec3(Pf0.x, Pf1.yz)); + float n111 = dot(g111, Pf1); + + vec3 fade_xyz = fade(Pf0); + vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z); + vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y); + float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); + return 2.2 * n_xyz; +} + + +float rand(vec2 co){ + return abs(fract(sin(dot(co.xy, vec2(12.9898, 78.233))) * 43758.5453)) * 2 - 1; +} + + +vec3 rand_rgb(vec2 co) +{ + return abs(fract(sin(dot(co.xy, vec2(34.4835, 89.6372))) * + vec3(29156.4765, 38273.56393, 47843.75468))) * 2 - 1; +} + + +uniform sampler2D PrecomputedGrain; + +// Computes the film grain using the precomputed grain +float grain(float frame_time) { + float offs = mod(frame_time, 7.2342); + ivec2 offs_coord = ivec2(offs * 28947.0, (offs - 2.5) * 12484.0); + return texelFetch(PrecomputedGrain, (ivec2(gl_FragCoord.xy) + offs_coord) % ivec2(2048), 0).x; +} + + +float fbm(vec3 x, float scale) { + float v = 0.0; + float a = 0.5; + float shift = 0.0; + x *= scale; + for (int i = 0; i < 5; ++i) { + v += a * (pnoise3D(x, vec3(scale)) * 0.5 + 0.5); + x = x * 2.0 + shift; + a *= 0.5; + } + return v; +} + + +// Returns the point in a given cell +vec2 worley_cell_point(ivec2 cell, int num_cells, float drop_rate) { + cell = cell % num_cells; + vec2 cell_base = vec2(cell) / num_cells; + float noise_x = rand(cell_base); + float noise_y = rand(cell_base.yx); + float drop_point = step(rand(cell) + 1e-7, drop_rate); + return cell_base + (0.5 + 1.0 * vec2(noise_x, noise_y)) / num_cells + drop_point * vec2(1e9); +} + + +// Distance accross borders +float distance_border(vec2 a, vec2 b) { + float dx = min(abs(a.x - b.x), min(abs(a.x - 1.0 - b.x), abs(a.x + 1.0 - b.x))); + float dy = min(abs(a.y - b.y), min(abs(a.y - 1.0 - b.y), abs(a.y + 1.0 - b.y))); + return length(vec2(dx, dy)); +} + +// Performs worley noise by checking all adjacent cells +// and comparing the distance to their points +float worley_noise(vec2 coord, int num_cells, float drop_rate) { + coord = fract(coord); + ivec2 cell = ivec2(coord * num_cells); + float dist = 1.0; + + // Search in the surrounding 5x5 cell block + for (int x = 0; x < 5; x++) { + for (int y = 0; y < 5; y++) { + vec2 cell_point = worley_cell_point(cell + ivec2(x-2, y-2), num_cells, drop_rate); + dist = min(dist, distance_border(cell_point, coord)); + } + } + dist /= length(vec2(1.0 / num_cells)); + dist = 1.0 - dist; + return dist; +} + +// Returns the point in a given cell +vec3 worley_cell_point(ivec3 cell, int num_cells, float drop_rate) { + cell = cell % num_cells; + vec3 cell_base = vec3(cell) / num_cells; + float noise_x = rand(cell_base.xy); + float noise_y = rand(cell_base.yx); + float noise_z = rand(cell_base.zx + cell_base.yy); + float drop_point = step(rand(cell.xy + cell.zz) + 1e-7, drop_rate); + return cell_base + (0.5 + 1.0 * vec3(noise_x, noise_y, noise_z)) / + num_cells + drop_point * vec3(1e9); +} + + +// Distance accross borders +float distance_border(vec3 a, vec3 b) { + float dx = min(abs(a.x - b.x), min(abs(a.x - 1.0 - b.x), abs(a.x + 1.0 - b.x))); + float dy = min(abs(a.y - b.y), min(abs(a.y - 1.0 - b.y), abs(a.y + 1.0 - b.y))); + float dz = min(abs(a.z - b.z), min(abs(a.z - 1.0 - b.z), abs(a.z + 1.0 - b.z))); + return length(vec3(dx, dy, dz)); +} + +// Performs worley noise by checking all adjacent cells +// and comparing the distance to their points +float worley_noise(vec3 coord, int num_cells, float drop_rate) { + coord = fract(coord); + ivec3 cell = ivec3(coord * num_cells); + float dist = 1.0; + + // Search in the surrounding 5x5 cell block + for (int x = 0; x < 5; x++) { + for (int y = 0; y < 5; y++) { + for (int z = 0; z < 5; z++) { + vec3 cell_point = worley_cell_point(cell + ivec3(x-2, y-2, z-2), num_cells, drop_rate); + dist = min(dist, distance_border(cell_point, coord)); + } + } + } + dist /= length(vec3(1.0 / num_cells)); + dist = 1.0 - dist; + return dist; +} + +float interleaved_gradient_noise(vec2 seed) { + const float scale = 3.14159; + vec3 magic = vec3(0.06711056, 0.00583715, 52.9829189); + return -scale + 2.0 * scale * fract(magic.z * fract(dot(seed, magic.xy))); +} diff --git a/RenderPipelineFile/rpcore/shader/includes/nonviewspace_shading_pipeline.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/nonviewspace_shading_pipeline.inc.glsl index 1f3940d4..75094d72 100644 --- a/RenderPipelineFile/rpcore/shader/includes/nonviewspace_shading_pipeline.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/nonviewspace_shading_pipeline.inc.glsl @@ -1,77 +1,77 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -#pragma include "includes/shadows.inc.glsl" -#pragma include "includes/material.inc.glsl" -#pragma include "includes/poisson_disk.inc.glsl" -#pragma include "includes/vertex_output.struct.glsl" - -#if DONT_FETCH_DEFAULT_TEXTURES - // Don't bind any samplers in this case, so the user can do it on his own -#else - uniform sampler2D p3d_Texture0; -#endif - -uniform Panda3DMaterial p3d_Material; - -layout(location = 0) in VertexOutput vOutput; - -#pragma include "includes/normal_mapping.inc.glsl" -#pragma include "includes/forward_shading.inc.glsl" - -MaterialShaderOutput prepare_material(MaterialBaseInput mInput, vec2 texcoord) { - MaterialShaderOutput m; - - #if DONT_SET_MATERIAL_PROPERTIES - // Leave material properties unitialized, and hope the user knows - // what he's doing. - #else - - #if DONT_FETCH_DEFAULT_TEXTURES - vec4 sampled_diffuse = vec4(1); - #else - vec4 sampled_diffuse = texture(p3d_Texture0, texcoord); - #endif - - // XXX: Support for alpha testing - // if (sampled_diffuse.w < 0.5) discard; - - // XXX: *maybe* support for normal mapping - - // Copy default material properties - m.basecolor = sampled_diffuse.xyz * mInput.color; - m.shading_model = mInput.shading_model; - m.normal = vOutput.normal; - m.metallic = mInput.metallic; - m.specular_ior = mInput.specular_ior; - m.roughness = mInput.roughness; - m.shading_model_param0 = mInput.arbitrary0; - #endif - - return m; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +#pragma include "includes/shadows.inc.glsl" +#pragma include "includes/material.inc.glsl" +#pragma include "includes/poisson_disk.inc.glsl" +#pragma include "includes/vertex_output.struct.glsl" + +#if DONT_FETCH_DEFAULT_TEXTURES + // Don't bind any samplers in this case, so the user can do it on his own +#else + uniform sampler2D p3d_Texture0; +#endif + +uniform Panda3DMaterial p3d_Material; + +layout(location = 0) in VertexOutput vOutput; + +#pragma include "includes/normal_mapping.inc.glsl" +#pragma include "includes/forward_shading.inc.glsl" + +MaterialShaderOutput prepare_material(MaterialBaseInput mInput, vec2 texcoord) { + MaterialShaderOutput m; + + #if DONT_SET_MATERIAL_PROPERTIES + // Leave material properties unitialized, and hope the user knows + // what he's doing. + #else + + #if DONT_FETCH_DEFAULT_TEXTURES + vec4 sampled_diffuse = vec4(1); + #else + vec4 sampled_diffuse = texture(p3d_Texture0, texcoord); + #endif + + // XXX: Support for alpha testing + // if (sampled_diffuse.w < 0.5) discard; + + // XXX: *maybe* support for normal mapping + + // Copy default material properties + m.basecolor = sampled_diffuse.xyz * mInput.color; + m.shading_model = mInput.shading_model; + m.normal = vOutput.normal; + m.metallic = mInput.metallic; + m.specular_ior = mInput.specular_ior; + m.roughness = mInput.roughness; + m.shading_model_param0 = mInput.arbitrary0; + #endif + + return m; +} diff --git a/RenderPipelineFile/rpcore/shader/includes/normal_mapping.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/normal_mapping.inc.glsl index 7c6488cb..9b3d6975 100644 --- a/RenderPipelineFile/rpcore/shader/includes/normal_mapping.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/normal_mapping.inc.glsl @@ -1,116 +1,116 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -// Reconstructs the tangent with the deltas of -// the position and texcoord -void reconstruct_tangent(out vec3 tangent, out vec3 binormal) { - vec3 pos_dx = dFdx(vOutput.position); - vec3 pos_dy = dFdy(vOutput.position); - float tcoord_dx = dFdx(vOutput.texcoord.y); - float tcoord_dy = dFdy(vOutput.texcoord.y); - - // Fix issues when the texture coordinate is wrong, this happens when - // two adjacent vertices have the same texture coordinate, as the gradient - // is 0 then. We just assume some hard-coded tangent and binormal then - if (abs(tcoord_dx) < 1e-24 && abs(tcoord_dy) < 1e-24) { - vec3 base = abs(vOutput.normal.z) < 0.999 ? vec3(0, 0, 1) : vec3(0, 1, 0); - tangent = normalize(cross(vOutput.normal, base)); - } else { - tangent = normalize(pos_dx * tcoord_dy - pos_dy * tcoord_dx); - } - - binormal = normalize(cross(tangent, vOutput.normal)); -} - - -// Aplies a normal map with a given base normal and displace normal, weighted by -// the bump factor -vec3 apply_normal_map(vec3 base_normal, vec3 displace_normal, float bump_factor, - vec3 tangent, vec3 binormal) { - // Optional: Make sure the base normal is correct - // base_normal = normalize(base_normal); - displace_normal = mix(vec3(0, 0, 1), displace_normal, saturate(bump_factor)); - return vec3( - tangent * displace_normal.x + - binormal * displace_normal.y + - base_normal * displace_normal.z - ); -} - - -vec3 apply_normal_map(vec3 base_normal, vec3 displace_normal, float bump_factor) { - vec3 tangent, binormal; - reconstruct_tangent(tangent, binormal); - return apply_normal_map(base_normal, displace_normal, bump_factor, tangent, binormal); -} - -// Parallax Mapping -vec2 get_parallax_texcoord(sampler2D displacement_map, float strength) { - // To disable parallax mapping: - // return vOutput.texcoord; - - const float max_dist = 50.0; - vec3 vec_to_cam = vOutput.position - MainSceneData.camera_pos; - - float initial_height = texture(displacement_map, vOutput.texcoord).x; - float pixel_dist = length(vec_to_cam); - - // Early out for materials without parallax mapping - if (initial_height > 0.999 || pixel_dist > max_dist) return vOutput.texcoord; - - float NxV = max(0.0, dot(vOutput.normal, vec_to_cam / pixel_dist)); // xxx merge with pixel dist - - float raymarch_distance = 0.2 * strength; - int num_steps = max(5, int((40 - (pixel_dist / max_dist) * 37.0) * (1 - NxV))); - - vec3 tangent, binormal; - reconstruct_tangent(tangent, binormal); - - vec3 view_vector = normalize(MainSceneData.camera_pos - vOutput.position); - - // Project view vector to tangent space - vec2 tex_offs = vec2(dot(-tangent, view_vector), dot(binormal, view_vector)); - - // Get the ray start and direction - vec3 current_pos = vec3(vOutput.texcoord, 1); - raymarch_distance *= 0.5 / clamp(dot(vOutput.normal, view_vector), 0.3, 1.0); - vec3 offs_step = vec3(tex_offs * raymarch_distance, -1.0) / float(num_steps); - - // Raymarch - vec3 last_hit = current_pos; - for (int i = 0; i < num_steps; ++i) { - float sample_h = texture(displacement_map, current_pos.xy).x; - current_pos += offs_step; - if (sample_h <= current_pos.z) { - last_hit = current_pos; - } - } - - float fade = square(square(square(pixel_dist / max_dist))); - return mix(last_hit.xy, vOutput.texcoord, fade); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +// Reconstructs the tangent with the deltas of +// the position and texcoord +void reconstruct_tangent(out vec3 tangent, out vec3 binormal) { + vec3 pos_dx = dFdx(vOutput.position); + vec3 pos_dy = dFdy(vOutput.position); + float tcoord_dx = dFdx(vOutput.texcoord.y); + float tcoord_dy = dFdy(vOutput.texcoord.y); + + // Fix issues when the texture coordinate is wrong, this happens when + // two adjacent vertices have the same texture coordinate, as the gradient + // is 0 then. We just assume some hard-coded tangent and binormal then + if (abs(tcoord_dx) < 1e-24 && abs(tcoord_dy) < 1e-24) { + vec3 base = abs(vOutput.normal.z) < 0.999 ? vec3(0, 0, 1) : vec3(0, 1, 0); + tangent = normalize(cross(vOutput.normal, base)); + } else { + tangent = normalize(pos_dx * tcoord_dy - pos_dy * tcoord_dx); + } + + binormal = normalize(cross(tangent, vOutput.normal)); +} + + +// Aplies a normal map with a given base normal and displace normal, weighted by +// the bump factor +vec3 apply_normal_map(vec3 base_normal, vec3 displace_normal, float bump_factor, + vec3 tangent, vec3 binormal) { + // Optional: Make sure the base normal is correct + // base_normal = normalize(base_normal); + displace_normal = mix(vec3(0, 0, 1), displace_normal, saturate(bump_factor)); + return vec3( + tangent * displace_normal.x + + binormal * displace_normal.y + + base_normal * displace_normal.z + ); +} + + +vec3 apply_normal_map(vec3 base_normal, vec3 displace_normal, float bump_factor) { + vec3 tangent, binormal; + reconstruct_tangent(tangent, binormal); + return apply_normal_map(base_normal, displace_normal, bump_factor, tangent, binormal); +} + +// Parallax Mapping +vec2 get_parallax_texcoord(sampler2D displacement_map, float strength) { + // To disable parallax mapping: + // return vOutput.texcoord; + + const float max_dist = 50.0; + vec3 vec_to_cam = vOutput.position - MainSceneData.camera_pos; + + float initial_height = texture(displacement_map, vOutput.texcoord).x; + float pixel_dist = length(vec_to_cam); + + // Early out for materials without parallax mapping + if (initial_height > 0.999 || pixel_dist > max_dist) return vOutput.texcoord; + + float NxV = max(0.0, dot(vOutput.normal, vec_to_cam / pixel_dist)); // xxx merge with pixel dist + + float raymarch_distance = 0.2 * strength; + int num_steps = max(5, int((40 - (pixel_dist / max_dist) * 37.0) * (1 - NxV))); + + vec3 tangent, binormal; + reconstruct_tangent(tangent, binormal); + + vec3 view_vector = normalize(MainSceneData.camera_pos - vOutput.position); + + // Project view vector to tangent space + vec2 tex_offs = vec2(dot(-tangent, view_vector), dot(binormal, view_vector)); + + // Get the ray start and direction + vec3 current_pos = vec3(vOutput.texcoord, 1); + raymarch_distance *= 0.5 / clamp(dot(vOutput.normal, view_vector), 0.3, 1.0); + vec3 offs_step = vec3(tex_offs * raymarch_distance, -1.0) / float(num_steps); + + // Raymarch + vec3 last_hit = current_pos; + for (int i = 0; i < num_steps; ++i) { + float sample_h = texture(displacement_map, current_pos.xy).x; + current_pos += offs_step; + if (sample_h <= current_pos.z) { + last_hit = current_pos; + } + } + + float fade = square(square(square(pixel_dist / max_dist))); + return mix(last_hit.xy, vOutput.texcoord, fade); +} diff --git a/RenderPipelineFile/rpcore/shader/includes/normal_packing.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/normal_packing.inc.glsl index 66f9a704..6b27787c 100644 --- a/RenderPipelineFile/rpcore/shader/includes/normal_packing.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/normal_packing.inc.glsl @@ -1,97 +1,97 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -/* - -Normal packing as described in: -A Survey of Efficient Representations for Independent Unit Vectors -Source: http://jcgt.org/published/0003/02/01/paper.pdf - -*/ - -// For each component of v, returns -1 if the component is < 0, else 1 -vec2 sign_not_zero(vec2 v) { - #if 1 - // Branch-Less version - return fma(step(vec2(0.0), v), vec2(2.0), vec2(-1.0)); - #else - // Version with branches (for GLSL < 4.00) - return vec2( - v.x >= 0 ? 1.0 : -1.0, - v.y >= 0 ? 1.0 : -1.0 - ); - #endif -} - -// Packs a 3-component normal to 2 channels using octahedron normals -vec2 pack_normal_octahedron(vec3 v) { - #if 0 - // Version as proposed by the paper - // Project the sphere onto the octahedron, and then onto the xy plane - vec2 p = v.xy * (1.0 / (abs(v.x) + abs(v.y) + abs(v.z))); - // Reflect the folds of the lower hemisphere over the diagonals - return (v.z <= 0.0) ? ((1.0 - abs(p.yx)) * sign_not_zero(p)) : p; - #else - // Faster version using newer GLSL capatibilities - v.xy /= dot(abs(v), vec3(1)); - - #if 0 - // Version with branches - if (v.z <= 0) v.xy = (1.0 - abs(v.yx)) * sign_not_zero(v.xy); - return v.xy; - #else - // Branch-Less version - return mix(v.xy, (1.0 - abs(v.yx)) * sign_not_zero(v.xy), step(v.z, 0.0)); - #endif - #endif -} - - -// Unpacking from octahedron normals, input is the output from pack_normal_octahedron -vec3 unpack_normal_octahedron(vec2 packed_nrm) { - #if 1 - // Version using newer GLSL capatibilities - vec3 v = vec3(packed_nrm.xy, 1.0 - abs(packed_nrm.x) - abs(packed_nrm.y)); - #if 1 - // Version with branches, seems to take less cycles than the - // branch-less version - if (v.z < 0) v.xy = (1.0 - abs(v.yx)) * sign_not_zero(v.xy); - #else - // Branch-Less version - v.xy = mix(v.xy, (1.0 - abs(v.yx)) * sign_not_zero(v.xy), step(v.z, 0)); - #endif - - return normalize(v); - #else - // Version as proposed in the paper. - vec3 v = vec3(packed_nrm, 1.0 - dot(vec2(1), abs(packed_nrm))); - if (v.z < 0) - v.xy = (vec2(1) - abs(v.yx)) * sign_not_zero(v.xy); - return normalize(v); - #endif -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +/* + +Normal packing as described in: +A Survey of Efficient Representations for Independent Unit Vectors +Source: http://jcgt.org/published/0003/02/01/paper.pdf + +*/ + +// For each component of v, returns -1 if the component is < 0, else 1 +vec2 sign_not_zero(vec2 v) { + #if 1 + // Branch-Less version + return fma(step(vec2(0.0), v), vec2(2.0), vec2(-1.0)); + #else + // Version with branches (for GLSL < 4.00) + return vec2( + v.x >= 0 ? 1.0 : -1.0, + v.y >= 0 ? 1.0 : -1.0 + ); + #endif +} + +// Packs a 3-component normal to 2 channels using octahedron normals +vec2 pack_normal_octahedron(vec3 v) { + #if 0 + // Version as proposed by the paper + // Project the sphere onto the octahedron, and then onto the xy plane + vec2 p = v.xy * (1.0 / (abs(v.x) + abs(v.y) + abs(v.z))); + // Reflect the folds of the lower hemisphere over the diagonals + return (v.z <= 0.0) ? ((1.0 - abs(p.yx)) * sign_not_zero(p)) : p; + #else + // Faster version using newer GLSL capatibilities + v.xy /= dot(abs(v), vec3(1)); + + #if 0 + // Version with branches + if (v.z <= 0) v.xy = (1.0 - abs(v.yx)) * sign_not_zero(v.xy); + return v.xy; + #else + // Branch-Less version + return mix(v.xy, (1.0 - abs(v.yx)) * sign_not_zero(v.xy), step(v.z, 0.0)); + #endif + #endif +} + + +// Unpacking from octahedron normals, input is the output from pack_normal_octahedron +vec3 unpack_normal_octahedron(vec2 packed_nrm) { + #if 1 + // Version using newer GLSL capatibilities + vec3 v = vec3(packed_nrm.xy, 1.0 - abs(packed_nrm.x) - abs(packed_nrm.y)); + #if 1 + // Version with branches, seems to take less cycles than the + // branch-less version + if (v.z < 0) v.xy = (1.0 - abs(v.yx)) * sign_not_zero(v.xy); + #else + // Branch-Less version + v.xy = mix(v.xy, (1.0 - abs(v.yx)) * sign_not_zero(v.xy), step(v.z, 0)); + #endif + + return normalize(v); + #else + // Version as proposed in the paper. + vec3 v = vec3(packed_nrm, 1.0 - dot(vec2(1), abs(packed_nrm))); + if (v.z < 0) + v.xy = (vec2(1) - abs(v.yx)) * sign_not_zero(v.xy); + return normalize(v); + #endif +} diff --git a/RenderPipelineFile/rpcore/shader/includes/poisson_disk.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/poisson_disk.inc.glsl index e906d765..643dd20b 100644 --- a/RenderPipelineFile/rpcore/shader/includes/poisson_disk.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/poisson_disk.inc.glsl @@ -1,333 +1,333 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - - -/* - -This are uniformly distributed Poisson Disks. - -All disks are sorted depending on their angle and radius, to make sure texture -chaching works as intended. - -The program to generate the poisson disks can be found in -toolkit/poisson_disk_generator. - -*/ - - -/* - -2D Poisson Disks - -*/ - -CONST_ARRAY vec2 poisson_2D_4[4] = vec2[]( - vec2(-0.953543, -0.300415), - vec2(0.291287, -0.956411), - vec2(0.957769, 0.282242), - vec2(-0.277367, 0.960318) -); - -CONST_ARRAY vec2 poisson_2D_8[8] = vec2[]( - vec2(-0.994272, -0.104946), - vec2(-0.375669, -0.92651), - vec2(0.62611, -0.778924), - vec2(0.910956, 0.412246), - vec2(0.972791, -0.226412), - vec2(0.265636, 0.96113), - vec2(-0.549897, 0.833746), - vec2(0.00219648, 0.00555702) -); - -CONST_ARRAY vec2 poisson_2D_12[12] = vec2[]( - vec2(-0.759347, -0.650607), - vec2(-0.276351, -0.00941298), - vec2(-0.999368, -0.0324209), - vec2(-0.193248, -0.979636), - vec2(0.457157, -0.885871), - vec2(0.968703, -0.246042), - vec2(0.310191, -0.266896), - vec2(0.44068, 0.355746), - vec2(0.448887, 0.885861), - vec2(0.955905, 0.292457), - vec2(-0.79166, 0.605247), - vec2(-0.0714434, 0.980047) -); - - -CONST_ARRAY vec2 poisson_2D_16[16] = vec2[]( - vec2(-0.680088, -0.731923), - vec2(-0.957909, -0.247622), - vec2(0.0948045, -0.992508), - vec2(-0.316418, -0.93561), - vec2(0.508091, -0.270309), - vec2(0.986855, -0.161122), - vec2(-0.0783372, -0.377044), - vec2(0.678299, -0.730012), - vec2(0.264303, 0.150586), - vec2(0.375585, 0.926198), - vec2(0.869216, 0.485342), - vec2(-0.0303609, 0.582547), - vec2(-0.456659, 0.886469), - vec2(-0.0502123, 0.998242), - vec2(-0.522737, 0.0698312), - vec2(-0.857891, 0.512805) -); - -CONST_ARRAY vec2 poisson_2D_32[32] = vec2[]( - vec2(-0.645095, -0.281509), - vec2(-0.491004, -0.535543), - vec2(-0.251013, -0.96746), - vec2(-0.527482, -0.838249), - vec2(-0.940142, -0.338633), - vec2(-0.78887, -0.602182), - vec2(-0.306068, -0.207344), - vec2(0.0236232, -0.763255), - vec2(0.326509, -0.930717), - vec2(0.0188184, -0.427136), - vec2(0.734814, -0.2611), - vec2(0.997828, -0.0467772), - vec2(0.102272, -0.0652116), - vec2(0.800923, -0.596026), - vec2(0.532466, -0.718217), - vec2(0.45988, -0.0147904), - vec2(0.428033, -0.434308), - vec2(0.569939, 0.31757), - vec2(0.847669, 0.530125), - vec2(0.517769, 0.839368), - vec2(0.247533, 0.477887), - vec2(0.952392, 0.24895), - vec2(-0.49404, 0.843787), - vec2(0.071269, 0.98377), - vec2(-0.901791, 0.425404), - vec2(-0.511218, 0.404082), - vec2(-0.166727, 0.573622), - vec2(-0.719383, 0.656148), - vec2(-0.221952, 0.956912), - vec2(-0.639361, 0.0178937), - vec2(-0.192214, 0.149272), - vec2(-0.999393, 0.0286758) -); - -CONST_ARRAY vec2 poisson_2D_64[64] = vec2[]( - vec2(-0.086316, -0.968111), - vec2(-0.279227, -0.959008), - vec2(-0.174659, -0.779219), - vec2(-0.994046, -0.100337), - vec2(-0.830417, -0.258316), - vec2(-0.29098, -0.276363), - vec2(-0.0711791, -0.309748), - vec2(0.127704, -0.414434), - vec2(0.255973, -0.712316), - vec2(-0.472927, -0.394059), - vec2(0.35572, -0.928901), - vec2(-0.658504, -0.535877), - vec2(0.105184, -0.99382), - vec2(-0.414437, -0.0607066), - vec2(-0.366554, -0.691781), - vec2(-0.464703, -0.862405), - vec2(-0.161989, -0.104327), - vec2(-0.174757, -0.515362), - vec2(-0.637325, -0.770203), - vec2(-0.664806, -0.128825), - vec2(-0.879395, -0.465052), - vec2(0.0304411, -0.762748), - vec2(0.443908, -0.577779), - vec2(0.188869, -0.0525923), - vec2(0.988219, -0.136184), - vec2(0.369901, -0.259416), - vec2(0.805865, -0.578815), - vec2(0.628829, -0.776143), - vec2(0.626141, -0.343504), - vec2(0.74722, 0.655944), - vec2(0.234085, 0.806087), - vec2(0.93415, 0.337947), - vec2(0.412135, 0.901209), - vec2(0.676801, 0.206996), - vec2(0.0943238, 0.661738), - vec2(0.118536, 0.984615), - vec2(0.589421, 0.79131), - vec2(0.731047, 0.429766), - vec2(0.478587, 0.439142), - vec2(0.22497, 0.445659), - vec2(0.374495, 0.660695), - vec2(0.906059, 0.0973713), - vec2(0.33495, 0.190513), - vec2(0.757964, -0.136907), - vec2(0.54374, -0.0527806), - vec2(0.908866, -0.36894), - vec2(-0.0776111, 0.477161), - vec2(-0.181392, 0.975743), - vec2(-0.139923, 0.74221), - vec2(-0.78206, 0.102957), - vec2(-0.576849, 0.450708), - vec2(-0.00888789, 0.05214), - vec2(-0.708787, 0.698047), - vec2(-0.84552, 0.531998), - vec2(-0.253359, 0.267802), - vec2(-0.493709, 0.659425), - vec2(-0.992843, 0.11576), - vec2(-0.302095, 0.564714), - vec2(-0.464918, 0.874781), - vec2(-0.711149, 0.302547), - vec2(-0.915964, 0.323287), - vec2(-0.263747, 0.0616327), - vec2(-0.519476, 0.175641), - vec2(0.0307961, 0.26891) -); - - - -/* - - -3D Poisson Disks - - -*/ - -CONST_ARRAY vec3 poisson_3D_16[16] = vec3[]( - vec3(-0.0601252, -0.090253, 0.242585), - vec3(-0.954277, -0.230389, 0.0491814), - vec3(-0.440708, -0.751107, 0.48156), - vec3(0.157285, -0.98681, -0.0113457), - vec3(-0.463958, -0.603355, -0.645049), - vec3(0.352804, -0.600314, 0.705076), - vec3(0.945725, -0.167066, -0.26733), - vec3(0.378653, -0.457091, -0.789674), - vec3(0.175363, 0.326807, -0.923022), - vec3(0.75046, 0.133097, 0.643648), - vec3(0.533623, 0.832552, 0.0769863), - vec3(-0.685037, 0.662173, -0.295431), - vec3(-0.0454946, 0.134477, 0.985295), - vec3(-0.534475, 0.0997254, -0.826768), - vec3(-0.307152, 0.748555, 0.585295), - vec3(-0.709746, 0.0374277, 0.68205) -); - -CONST_ARRAY vec3 poisson_3D_32[32] = vec3[]( - vec3(-0.637433, -0.0795689, 0.749829), - vec3(-0.166216, -0.0315564, -0.983756), - vec3(-0.528491, -0.143633, 0.195646), - vec3(-0.249567, -0.825099, 0.498893), - vec3(-0.558757, -0.434904, -0.667499), - vec3(-0.858404, -0.473501, -0.145915), - vec3(-0.135047, -0.296593, -0.335407), - vec3(0.316311, -0.757367, 0.261108), - vec3(-0.163747, -0.374054, 0.88854), - vec3(0.268538, -0.898278, -0.335799), - vec3(-0.381943, -0.906282, -0.16664), - vec3(0.837301, -0.420993, 0.347183), - vec3(0.36155, -0.12942, 0.913665), - vec3(0.722952, 0.655391, 0.209401), - vec3(0.374059, 0.504317, -0.764699), - vec3(0.466859, 0.811274, -0.270417), - vec3(0.962492, 0.082706, 0.102222), - vec3(0.726909, 0.197392, 0.592712), - vec3(0.726963, -0.519139, -0.26076), - vec3(0.863379, 0.0510047, -0.479442), - vec3(0.39697, -0.392489, -0.778489), - vec3(0.395817, 0.28567, -0.152613), - vec3(0.0115477, 0.0392268, 0.262437), - vec3(-0.124912, 0.314161, 0.906488), - vec3(-0.795609, 0.124298, -0.584104), - vec3(-0.58822, 0.533162, 0.596359), - vec3(-0.465148, 0.867438, 0.068886), - vec3(-0.347131, 0.479682, -0.773511), - vec3(-0.0359631, 0.862508, 0.497219), - vec3(-0.869958, 0.454845, 0.0919322), - vec3(-0.100729, 0.895672, -0.400421), - vec3(-0.312793, 0.316496, -0.184027) -); - -CONST_ARRAY vec3 poisson_3D_64[64] = vec3[]( - vec3(-0.15773, -0.0451896, -0.9623), - vec3(-0.0130272, -0.640955, -0.751804), - vec3(-0.214195, -0.789982, 0.571629), - vec3(-0.22401, -0.127885, -0.479033), - vec3(-0.0381656, -0.400932, 0.389145), - vec3(-0.958604, -0.255706, 0.0608546), - vec3(-0.491959, -0.0240958, 0.867465), - vec3(-0.735088, -0.589627, -0.0952913), - vec3(-0.361071, -0.387626, -0.841852), - vec3(-0.454339, -0.716331, -0.490685), - vec3(-0.464857, -0.00371937, 0.416402), - vec3(-0.252955, -0.952897, -0.0173203), - vec3(-0.310376, -0.450015, 0.834997), - vec3(-0.481379, -0.12181, -0.0713362), - vec3(0.17995, -0.273669, -0.910183), - vec3(0.100674, -0.18972, 0.96892), - vec3(0.229951, -0.770195, -0.427458), - vec3(-0.534464, -0.703867, 0.273916), - vec3(-0.779887, -0.341913, 0.505745), - vec3(-0.793823, -0.290259, -0.406983), - vec3(0.512152, -0.541214, -0.661702), - vec3(0.616316, -0.597375, 0.505634), - vec3(0.798438, -0.464642, -0.225906), - vec3(0.546185, -0.833083, -0.0299397), - vec3(0.38638, -0.311687, 0.154036), - vec3(0.172597, -0.639959, 0.741325), - vec3(0.124675, -0.818478, 0.257105), - vec3(-0.172153, -0.492998, -0.0557574), - vec3(0.318022, -0.24362, -0.38011), - vec3(0.531653, -0.165474, 0.749569), - vec3(0.529882, 0.24116, -0.257057), - vec3(0.466419, 0.215838, 0.25547), - vec3(0.0712114, 0.0329548, 0.548568), - vec3(0.723801, -0.0377743, -0.668303), - vec3(0.814331, 0.205909, 0.504961), - vec3(0.93028, 0.357367, 0.0674278), - vec3(0.921439, -0.213509, 0.314967), - vec3(0.96477, -0.0016533, -0.256952), - vec3(-0.0662509, 0.986109, -0.0656959), - vec3(0.185578, 0.617772, -0.181412), - vec3(0.107275, 0.241256, -0.491317), - vec3(0.164571, 0.247343, 0.917695), - vec3(0.0651426, 0.739647, -0.652712), - vec3(0.456338, 0.504012, 0.706019), - vec3(0.293143, 0.169755, -0.932919), - vec3(0.032654, 0.0818838, 0.00303189), - vec3(0.601786, 0.735747, 0.216412), - vec3(0.151178, 0.940888, 0.300587), - vec3(0.702927, 0.659986, -0.246973), - vec3(0.0395814, 0.691424, 0.686644), - vec3(0.451051, 0.515958, -0.689563), - vec3(-0.891535, 0.111488, 0.301105), - vec3(-0.720135, 0.120472, -0.674127), - vec3(-0.747936, 0.51219, 0.177613), - vec3(-0.16722, 0.372523, -0.887329), - vec3(-0.674427, 0.357484, 0.643218), - vec3(-0.249121, 0.379101, 0.871001), - vec3(-0.929231, 0.219506, -0.255281), - vec3(-0.487606, 0.461665, -0.621111), - vec3(-0.451381, 0.890955, 0.0284355), - vec3(-0.724563, 0.612287, -0.273063), - vec3(-0.228439, 0.555403, 0.34383), - vec3(-0.328195, 0.828738, -0.447974), - vec3(-0.417829, 0.32502, -0.232616) -); +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + + +/* + +This are uniformly distributed Poisson Disks. + +All disks are sorted depending on their angle and radius, to make sure texture +chaching works as intended. + +The program to generate the poisson disks can be found in +toolkit/poisson_disk_generator. + +*/ + + +/* + +2D Poisson Disks + +*/ + +CONST_ARRAY vec2 poisson_2D_4[4] = vec2[]( + vec2(-0.953543, -0.300415), + vec2(0.291287, -0.956411), + vec2(0.957769, 0.282242), + vec2(-0.277367, 0.960318) +); + +CONST_ARRAY vec2 poisson_2D_8[8] = vec2[]( + vec2(-0.994272, -0.104946), + vec2(-0.375669, -0.92651), + vec2(0.62611, -0.778924), + vec2(0.910956, 0.412246), + vec2(0.972791, -0.226412), + vec2(0.265636, 0.96113), + vec2(-0.549897, 0.833746), + vec2(0.00219648, 0.00555702) +); + +CONST_ARRAY vec2 poisson_2D_12[12] = vec2[]( + vec2(-0.759347, -0.650607), + vec2(-0.276351, -0.00941298), + vec2(-0.999368, -0.0324209), + vec2(-0.193248, -0.979636), + vec2(0.457157, -0.885871), + vec2(0.968703, -0.246042), + vec2(0.310191, -0.266896), + vec2(0.44068, 0.355746), + vec2(0.448887, 0.885861), + vec2(0.955905, 0.292457), + vec2(-0.79166, 0.605247), + vec2(-0.0714434, 0.980047) +); + + +CONST_ARRAY vec2 poisson_2D_16[16] = vec2[]( + vec2(-0.680088, -0.731923), + vec2(-0.957909, -0.247622), + vec2(0.0948045, -0.992508), + vec2(-0.316418, -0.93561), + vec2(0.508091, -0.270309), + vec2(0.986855, -0.161122), + vec2(-0.0783372, -0.377044), + vec2(0.678299, -0.730012), + vec2(0.264303, 0.150586), + vec2(0.375585, 0.926198), + vec2(0.869216, 0.485342), + vec2(-0.0303609, 0.582547), + vec2(-0.456659, 0.886469), + vec2(-0.0502123, 0.998242), + vec2(-0.522737, 0.0698312), + vec2(-0.857891, 0.512805) +); + +CONST_ARRAY vec2 poisson_2D_32[32] = vec2[]( + vec2(-0.645095, -0.281509), + vec2(-0.491004, -0.535543), + vec2(-0.251013, -0.96746), + vec2(-0.527482, -0.838249), + vec2(-0.940142, -0.338633), + vec2(-0.78887, -0.602182), + vec2(-0.306068, -0.207344), + vec2(0.0236232, -0.763255), + vec2(0.326509, -0.930717), + vec2(0.0188184, -0.427136), + vec2(0.734814, -0.2611), + vec2(0.997828, -0.0467772), + vec2(0.102272, -0.0652116), + vec2(0.800923, -0.596026), + vec2(0.532466, -0.718217), + vec2(0.45988, -0.0147904), + vec2(0.428033, -0.434308), + vec2(0.569939, 0.31757), + vec2(0.847669, 0.530125), + vec2(0.517769, 0.839368), + vec2(0.247533, 0.477887), + vec2(0.952392, 0.24895), + vec2(-0.49404, 0.843787), + vec2(0.071269, 0.98377), + vec2(-0.901791, 0.425404), + vec2(-0.511218, 0.404082), + vec2(-0.166727, 0.573622), + vec2(-0.719383, 0.656148), + vec2(-0.221952, 0.956912), + vec2(-0.639361, 0.0178937), + vec2(-0.192214, 0.149272), + vec2(-0.999393, 0.0286758) +); + +CONST_ARRAY vec2 poisson_2D_64[64] = vec2[]( + vec2(-0.086316, -0.968111), + vec2(-0.279227, -0.959008), + vec2(-0.174659, -0.779219), + vec2(-0.994046, -0.100337), + vec2(-0.830417, -0.258316), + vec2(-0.29098, -0.276363), + vec2(-0.0711791, -0.309748), + vec2(0.127704, -0.414434), + vec2(0.255973, -0.712316), + vec2(-0.472927, -0.394059), + vec2(0.35572, -0.928901), + vec2(-0.658504, -0.535877), + vec2(0.105184, -0.99382), + vec2(-0.414437, -0.0607066), + vec2(-0.366554, -0.691781), + vec2(-0.464703, -0.862405), + vec2(-0.161989, -0.104327), + vec2(-0.174757, -0.515362), + vec2(-0.637325, -0.770203), + vec2(-0.664806, -0.128825), + vec2(-0.879395, -0.465052), + vec2(0.0304411, -0.762748), + vec2(0.443908, -0.577779), + vec2(0.188869, -0.0525923), + vec2(0.988219, -0.136184), + vec2(0.369901, -0.259416), + vec2(0.805865, -0.578815), + vec2(0.628829, -0.776143), + vec2(0.626141, -0.343504), + vec2(0.74722, 0.655944), + vec2(0.234085, 0.806087), + vec2(0.93415, 0.337947), + vec2(0.412135, 0.901209), + vec2(0.676801, 0.206996), + vec2(0.0943238, 0.661738), + vec2(0.118536, 0.984615), + vec2(0.589421, 0.79131), + vec2(0.731047, 0.429766), + vec2(0.478587, 0.439142), + vec2(0.22497, 0.445659), + vec2(0.374495, 0.660695), + vec2(0.906059, 0.0973713), + vec2(0.33495, 0.190513), + vec2(0.757964, -0.136907), + vec2(0.54374, -0.0527806), + vec2(0.908866, -0.36894), + vec2(-0.0776111, 0.477161), + vec2(-0.181392, 0.975743), + vec2(-0.139923, 0.74221), + vec2(-0.78206, 0.102957), + vec2(-0.576849, 0.450708), + vec2(-0.00888789, 0.05214), + vec2(-0.708787, 0.698047), + vec2(-0.84552, 0.531998), + vec2(-0.253359, 0.267802), + vec2(-0.493709, 0.659425), + vec2(-0.992843, 0.11576), + vec2(-0.302095, 0.564714), + vec2(-0.464918, 0.874781), + vec2(-0.711149, 0.302547), + vec2(-0.915964, 0.323287), + vec2(-0.263747, 0.0616327), + vec2(-0.519476, 0.175641), + vec2(0.0307961, 0.26891) +); + + + +/* + + +3D Poisson Disks + + +*/ + +CONST_ARRAY vec3 poisson_3D_16[16] = vec3[]( + vec3(-0.0601252, -0.090253, 0.242585), + vec3(-0.954277, -0.230389, 0.0491814), + vec3(-0.440708, -0.751107, 0.48156), + vec3(0.157285, -0.98681, -0.0113457), + vec3(-0.463958, -0.603355, -0.645049), + vec3(0.352804, -0.600314, 0.705076), + vec3(0.945725, -0.167066, -0.26733), + vec3(0.378653, -0.457091, -0.789674), + vec3(0.175363, 0.326807, -0.923022), + vec3(0.75046, 0.133097, 0.643648), + vec3(0.533623, 0.832552, 0.0769863), + vec3(-0.685037, 0.662173, -0.295431), + vec3(-0.0454946, 0.134477, 0.985295), + vec3(-0.534475, 0.0997254, -0.826768), + vec3(-0.307152, 0.748555, 0.585295), + vec3(-0.709746, 0.0374277, 0.68205) +); + +CONST_ARRAY vec3 poisson_3D_32[32] = vec3[]( + vec3(-0.637433, -0.0795689, 0.749829), + vec3(-0.166216, -0.0315564, -0.983756), + vec3(-0.528491, -0.143633, 0.195646), + vec3(-0.249567, -0.825099, 0.498893), + vec3(-0.558757, -0.434904, -0.667499), + vec3(-0.858404, -0.473501, -0.145915), + vec3(-0.135047, -0.296593, -0.335407), + vec3(0.316311, -0.757367, 0.261108), + vec3(-0.163747, -0.374054, 0.88854), + vec3(0.268538, -0.898278, -0.335799), + vec3(-0.381943, -0.906282, -0.16664), + vec3(0.837301, -0.420993, 0.347183), + vec3(0.36155, -0.12942, 0.913665), + vec3(0.722952, 0.655391, 0.209401), + vec3(0.374059, 0.504317, -0.764699), + vec3(0.466859, 0.811274, -0.270417), + vec3(0.962492, 0.082706, 0.102222), + vec3(0.726909, 0.197392, 0.592712), + vec3(0.726963, -0.519139, -0.26076), + vec3(0.863379, 0.0510047, -0.479442), + vec3(0.39697, -0.392489, -0.778489), + vec3(0.395817, 0.28567, -0.152613), + vec3(0.0115477, 0.0392268, 0.262437), + vec3(-0.124912, 0.314161, 0.906488), + vec3(-0.795609, 0.124298, -0.584104), + vec3(-0.58822, 0.533162, 0.596359), + vec3(-0.465148, 0.867438, 0.068886), + vec3(-0.347131, 0.479682, -0.773511), + vec3(-0.0359631, 0.862508, 0.497219), + vec3(-0.869958, 0.454845, 0.0919322), + vec3(-0.100729, 0.895672, -0.400421), + vec3(-0.312793, 0.316496, -0.184027) +); + +CONST_ARRAY vec3 poisson_3D_64[64] = vec3[]( + vec3(-0.15773, -0.0451896, -0.9623), + vec3(-0.0130272, -0.640955, -0.751804), + vec3(-0.214195, -0.789982, 0.571629), + vec3(-0.22401, -0.127885, -0.479033), + vec3(-0.0381656, -0.400932, 0.389145), + vec3(-0.958604, -0.255706, 0.0608546), + vec3(-0.491959, -0.0240958, 0.867465), + vec3(-0.735088, -0.589627, -0.0952913), + vec3(-0.361071, -0.387626, -0.841852), + vec3(-0.454339, -0.716331, -0.490685), + vec3(-0.464857, -0.00371937, 0.416402), + vec3(-0.252955, -0.952897, -0.0173203), + vec3(-0.310376, -0.450015, 0.834997), + vec3(-0.481379, -0.12181, -0.0713362), + vec3(0.17995, -0.273669, -0.910183), + vec3(0.100674, -0.18972, 0.96892), + vec3(0.229951, -0.770195, -0.427458), + vec3(-0.534464, -0.703867, 0.273916), + vec3(-0.779887, -0.341913, 0.505745), + vec3(-0.793823, -0.290259, -0.406983), + vec3(0.512152, -0.541214, -0.661702), + vec3(0.616316, -0.597375, 0.505634), + vec3(0.798438, -0.464642, -0.225906), + vec3(0.546185, -0.833083, -0.0299397), + vec3(0.38638, -0.311687, 0.154036), + vec3(0.172597, -0.639959, 0.741325), + vec3(0.124675, -0.818478, 0.257105), + vec3(-0.172153, -0.492998, -0.0557574), + vec3(0.318022, -0.24362, -0.38011), + vec3(0.531653, -0.165474, 0.749569), + vec3(0.529882, 0.24116, -0.257057), + vec3(0.466419, 0.215838, 0.25547), + vec3(0.0712114, 0.0329548, 0.548568), + vec3(0.723801, -0.0377743, -0.668303), + vec3(0.814331, 0.205909, 0.504961), + vec3(0.93028, 0.357367, 0.0674278), + vec3(0.921439, -0.213509, 0.314967), + vec3(0.96477, -0.0016533, -0.256952), + vec3(-0.0662509, 0.986109, -0.0656959), + vec3(0.185578, 0.617772, -0.181412), + vec3(0.107275, 0.241256, -0.491317), + vec3(0.164571, 0.247343, 0.917695), + vec3(0.0651426, 0.739647, -0.652712), + vec3(0.456338, 0.504012, 0.706019), + vec3(0.293143, 0.169755, -0.932919), + vec3(0.032654, 0.0818838, 0.00303189), + vec3(0.601786, 0.735747, 0.216412), + vec3(0.151178, 0.940888, 0.300587), + vec3(0.702927, 0.659986, -0.246973), + vec3(0.0395814, 0.691424, 0.686644), + vec3(0.451051, 0.515958, -0.689563), + vec3(-0.891535, 0.111488, 0.301105), + vec3(-0.720135, 0.120472, -0.674127), + vec3(-0.747936, 0.51219, 0.177613), + vec3(-0.16722, 0.372523, -0.887329), + vec3(-0.674427, 0.357484, 0.643218), + vec3(-0.249121, 0.379101, 0.871001), + vec3(-0.929231, 0.219506, -0.255281), + vec3(-0.487606, 0.461665, -0.621111), + vec3(-0.451381, 0.890955, 0.0284355), + vec3(-0.724563, 0.612287, -0.273063), + vec3(-0.228439, 0.555403, 0.34383), + vec3(-0.328195, 0.828738, -0.447974), + vec3(-0.417829, 0.32502, -0.232616) +); diff --git a/RenderPipelineFile/rpcore/shader/includes/sampling_sequences.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/sampling_sequences.inc.glsl index 39050e40..43e74c53 100644 --- a/RenderPipelineFile/rpcore/shader/includes/sampling_sequences.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/sampling_sequences.inc.glsl @@ -1,46 +1,46 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - - -// Poisson disks -#pragma include "includes/poisson_disk.inc.glsl" -#pragma include "includes/halton_sequences.inc.glsl" - - -// Special getter for plugin sequences - -#define GET_SEQUENCE_SIZE(plugin_id, setting_id) (GET_SETTING(plugin_id, setting_id).length()) -#define GET_SEQUENCE_SAMPLE(plugin_id, setting_id, sample_id) (GET_SETTING(plugin_id, setting_id)[sample_id]) - -#define START_ITERATE_SEQUENCE(plugin_id, setting_id, storage) \ - for (uint _seq_count_000 = 0; _seq_count_000 < GET_SEQUENCE_SIZE(plugin_id, setting_id); ++_seq_count_000) {\ - storage = GET_SEQUENCE_SAMPLE(plugin_id, setting_id, _seq_count_000); - -#define END_ITERATE_SEQUENCE() } - -#define NORMALIZE_SEQUENCE(plugin_id, setting_id, var) var /= float(GET_SEQUENCE_SIZE(plugin_id, setting_id)); +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + + +// Poisson disks +#pragma include "includes/poisson_disk.inc.glsl" +#pragma include "includes/halton_sequences.inc.glsl" + + +// Special getter for plugin sequences + +#define GET_SEQUENCE_SIZE(plugin_id, setting_id) (GET_SETTING(plugin_id, setting_id).length()) +#define GET_SEQUENCE_SAMPLE(plugin_id, setting_id, sample_id) (GET_SETTING(plugin_id, setting_id)[sample_id]) + +#define START_ITERATE_SEQUENCE(plugin_id, setting_id, storage) \ + for (uint _seq_count_000 = 0; _seq_count_000 < GET_SEQUENCE_SIZE(plugin_id, setting_id); ++_seq_count_000) {\ + storage = GET_SEQUENCE_SAMPLE(plugin_id, setting_id, _seq_count_000); + +#define END_ITERATE_SEQUENCE() } + +#define NORMALIZE_SEQUENCE(plugin_id, setting_id, var) var /= float(GET_SEQUENCE_SIZE(plugin_id, setting_id)); diff --git a/RenderPipelineFile/rpcore/shader/includes/shadows.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/shadows.inc.glsl index f162fa43..7eed8007 100644 --- a/RenderPipelineFile/rpcore/shader/includes/shadows.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/shadows.inc.glsl @@ -1,78 +1,78 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - - -CONST_ARRAY vec2[] shadow_sample_offsets_8 = vec2[8]( - vec2(-0.7071, 0.7071), - vec2(-0.0000, -0.8750), - vec2(0.5303, 0.5303), - vec2(-0.6250, -0.0000), - vec2(0.3536, -0.3536), - vec2(-0.0000, 0.3750), - vec2(-0.1768, -0.1768), - vec2(0.1250, 0.0000) -); - -// Projects a point using the given mvp -vec3 project(mat4 mvp, vec3 p) { - vec4 projected = mvp * vec4(p, 1); - return fma(projected.xyz / projected.w, vec3(0.5), vec3(0.5)); -} - -// Given a shadow mvp matrix and the light vector, finds an appropriate filter size -vec2 find_filter_size(mat4 projection, vec3 light, float sample_radius) { - - // Find an arbitrary tangent and bitangent to the given normal - vec3 tangent = vec3(1, 0, 0); - vec3 binormal = vec3(0, 1, 0); - - // Project everything - // TODO: We could actually only use the 3x3 part of the mat and save the - // origin projection. - vec2 proj_origin = project(projection, vec3(0)).xy; - vec2 proj_tangent = abs(project(projection, tangent + binormal).xy - proj_origin); - - return vec2(max(proj_tangent.x, proj_tangent.y) * sample_radius) * 10.0; -} - -// http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/ -// Returns the normal and light dependent bias -vec2 get_shadow_bias(vec3 n, vec3 l) { - float cos_alpha = saturate(dot(n, l)); - float offset_scale_n = sqrt(1 - cos_alpha * cos_alpha); // sin(acos(L·N)) - float offset_scale_l = offset_scale_n / cos_alpha; // tan(acos(L·N)) - return vec2(offset_scale_n, min(2, offset_scale_l)); -} - -// Offsets a position based on slope and normal -vec3 get_biased_position(vec3 pos, float slope_bias, float normal_bias, vec3 normal, vec3 light) { - vec2 offsets = get_shadow_bias(normal, light); - pos += normal * offsets.x * normal_bias; - pos += light * offsets.y * slope_bias; - return pos; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + + +CONST_ARRAY vec2[] shadow_sample_offsets_8 = vec2[8]( + vec2(-0.7071, 0.7071), + vec2(-0.0000, -0.8750), + vec2(0.5303, 0.5303), + vec2(-0.6250, -0.0000), + vec2(0.3536, -0.3536), + vec2(-0.0000, 0.3750), + vec2(-0.1768, -0.1768), + vec2(0.1250, 0.0000) +); + +// Projects a point using the given mvp +vec3 project(mat4 mvp, vec3 p) { + vec4 projected = mvp * vec4(p, 1); + return fma(projected.xyz / projected.w, vec3(0.5), vec3(0.5)); +} + +// Given a shadow mvp matrix and the light vector, finds an appropriate filter size +vec2 find_filter_size(mat4 projection, vec3 light, float sample_radius) { + + // Find an arbitrary tangent and bitangent to the given normal + vec3 tangent = vec3(1, 0, 0); + vec3 binormal = vec3(0, 1, 0); + + // Project everything + // TODO: We could actually only use the 3x3 part of the mat and save the + // origin projection. + vec2 proj_origin = project(projection, vec3(0)).xy; + vec2 proj_tangent = abs(project(projection, tangent + binormal).xy - proj_origin); + + return vec2(max(proj_tangent.x, proj_tangent.y) * sample_radius) * 10.0; +} + +// http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/ +// Returns the normal and light dependent bias +vec2 get_shadow_bias(vec3 n, vec3 l) { + float cos_alpha = saturate(dot(n, l)); + float offset_scale_n = sqrt(1 - cos_alpha * cos_alpha); // sin(acos(L·N)) + float offset_scale_l = offset_scale_n / cos_alpha; // tan(acos(L·N)) + return vec2(offset_scale_n, min(2, offset_scale_l)); +} + +// Offsets a position based on slope and normal +vec3 get_biased_position(vec3 pos, float slope_bias, float normal_bias, vec3 normal, vec3 light) { + vec2 offsets = get_shadow_bias(normal, light); + pos += normal * offsets.x * normal_bias; + pos += light * offsets.y * slope_bias; + return pos; +} diff --git a/RenderPipelineFile/rpcore/shader/includes/skin_shading.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/skin_shading.inc.glsl index c1f18a7e..3bbf87a2 100644 --- a/RenderPipelineFile/rpcore/shader/includes/skin_shading.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/skin_shading.inc.glsl @@ -1,46 +1,46 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -/* -From: -http://www.iryoku.com/translucency/ -*/ - -const float skin_travel_weight = 1.0; - -vec3 skin_transmittance(float distance_through_medium) { - float s = distance_through_medium * skin_travel_weight; - float s_sq = -s * s; - vec3 t = vec3(0.233, 0.455, 0.649) * exp(s_sq / 0.0064) + - vec3(0.1, 0.336, 0.344) * exp(s_sq / 0.0484) + - vec3(0.118, 0.198, 0.0) * exp(s_sq / 0.187) + - vec3(0.113, 0.007, 0.007) * exp(s_sq / 0.567) + - vec3(0.358, 0.004, 0.0) * exp(s_sq / 1.99) + - vec3(0.078, 0.0, 0.0) * exp(s_sq / 7.41); - return t; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +/* +From: +http://www.iryoku.com/translucency/ +*/ + +const float skin_travel_weight = 1.0; + +vec3 skin_transmittance(float distance_through_medium) { + float s = distance_through_medium * skin_travel_weight; + float s_sq = -s * s; + vec3 t = vec3(0.233, 0.455, 0.649) * exp(s_sq / 0.0064) + + vec3(0.1, 0.336, 0.344) * exp(s_sq / 0.0484) + + vec3(0.118, 0.198, 0.0) * exp(s_sq / 0.187) + + vec3(0.113, 0.007, 0.007) * exp(s_sq / 0.567) + + vec3(0.358, 0.004, 0.0) * exp(s_sq / 1.99) + + vec3(0.078, 0.0, 0.0) * exp(s_sq / 7.41); + return t; +} diff --git a/RenderPipelineFile/rpcore/shader/includes/source_data.struct.glsl b/RenderPipelineFile/rpcore/shader/includes/source_data.struct.glsl index 282ae3ad..dec1fcea 100644 --- a/RenderPipelineFile/rpcore/shader/includes/source_data.struct.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/source_data.struct.glsl @@ -1,9 +1,9 @@ -#pragma once - -struct SourceData { - vec4 Data0; - vec4 Data1; - vec4 Data2; - vec4 Data3; - vec4 Data4; -}; +#pragma once + +struct SourceData { + vec4 Data0; + vec4 Data1; + vec4 Data2; + vec4 Data3; + vec4 Data4; +}; diff --git a/RenderPipelineFile/rpcore/shader/includes/temporal_resolve.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/temporal_resolve.inc.glsl index 4611489d..8bf68980 100644 --- a/RenderPipelineFile/rpcore/shader/includes/temporal_resolve.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/temporal_resolve.inc.glsl @@ -1,253 +1,253 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -#define USE_GBUFFER_EXTENSIONS -#pragma include "includes/color_spaces.inc.glsl" -#pragma include "includes/transforms.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" - -// Maximum color distance -#ifndef RS_MAX_CLIP_DIST - #define RS_MAX_CLIP_DIST 0.5 -#endif - -// Blending factor -#ifndef RS_DISTANCE_SCALE - #define RS_DISTANCE_SCALE 10.0 -#endif - -// How long to keep good pixels -#ifndef RS_KEEP_GOOD_DURATION - #define RS_KEEP_GOOD_DURATION 10.0 -#endif - -// How long to keep bad pixels -#ifndef RS_KEEP_BAD_DURATION - #define RS_KEEP_BAD_DURATION 3.0 -#endif - -#ifndef RS_AABB_SIZE - #define RS_AABB_SIZE 1.0 -#endif - -#ifndef RS_USE_SMOOTH_TECHNIQUE - #define RS_USE_SMOOTH_TECHNIQUE 0 -#endif - -/* - -Uses the reprojection suggested in: -http://www.crytek.com/download/Sousa_Graphics_Gems_CryENGINE3.pdf - -Also based on: -https://github.com/playdeadgames/temporal - -*/ - -#define USE_OPTIMIZATIONS 0 -#define USE_CLIPPING 1 - -vec4 clip_aabb(vec3 aabb_min, vec3 aabb_max, vec4 p, vec4 q) -{ -#if USE_OPTIMIZATIONS - // note: only clips towards aabb center (but fast!) - vec3 p_clip = 0.5 * (aabb_max + aabb_min); - vec3 e_clip = 0.5 * (aabb_max - aabb_min); - - vec4 v_clip = q - vec4(p_clip, p.w); - vec3 v_unit = v_clip.xyz / e_clip; - vec3 a_unit = abs(v_unit); - float ma_unit = max(a_unit.x, max(a_unit.y, a_unit.z)); - - if (ma_unit > 1.0) - return vec4(p_clip, p.w) + v_clip / ma_unit; - else - return q; // point inside aabb -#else - vec4 r = q - p; - vec3 rmax = aabb_max - p.xyz; - vec3 rmin = aabb_min - p.xyz; - - const float eps = 1e-8; - - if (r.x > rmax.x + eps) - r *= (rmax.x / r.x); - if (r.y > rmax.y + eps) - r *= (rmax.y / r.y); - if (r.z > rmax.z + eps) - r *= (rmax.z / r.z); - - if (r.x < rmin.x - eps) - r *= (rmin.x / r.x); - if (r.y < rmin.y - eps) - r *= (rmin.y / r.y); - if (r.z < rmin.z - eps) - r *= (rmin.z / r.z); - - return p + r; -#endif -} - - -#if RS_USE_POSITION_TECHNIQUE - uniform sampler2D Previous_SceneDepth; -#endif - - -vec4 resolve_temporal(sampler2D current_tex, sampler2D last_tex, vec2 curr_coord, vec2 last_coord) { - vec2 one_pixel = 1.0 / SCREEN_SIZE; - vec4 curr_m = textureLod(current_tex, curr_coord, 0); - - // Out of screen, can early out - if (out_of_screen(last_coord)) { - return max(vec4(0.0), curr_m); - } - - #if !RS_USE_SMOOTH_TECHNIQUE - - #if RS_USE_POSITION_TECHNIQUE - - float curr_z = get_depth_at(curr_coord); - vec3 curr_pos = calculate_surface_pos(curr_z, curr_coord); - - float last_z = textureLod(Previous_SceneDepth, last_coord, 0).x; - vec3 last_pos = calculate_surface_pos( - last_z, last_coord, MainSceneData.last_inv_view_proj_mat_no_jitter); - - // Weight by distance - float max_distance = RS_DISTANCE_SCALE; - max_distance *= distance(curr_pos, MainSceneData.camera_pos) / 10.0; - - float weight = 1.0 - saturate(distance(curr_pos, last_pos) / max_distance); - weight *= 1 - 1.0 / RS_KEEP_GOOD_DURATION; - - vec4 last_m = textureLod(last_tex, last_coord, 0); - return mix(curr_m, last_m, weight); - - #else - - const float subpixel_threshold = 0.5; - const float gather_base = 0.5; - const float gather_subpixel_motion = 0.1666; - - float curr_depth = get_depth_at(curr_coord); - float vs_dist = get_linear_z_from_z(curr_depth); - - vec2 velocity = last_coord - curr_coord; - - float texel_vel_mag = length(velocity) * vs_dist; - float subpixel_motion = saturate(subpixel_threshold / (1e-8 + texel_vel_mag)); - float min_max_support = gather_base + gather_subpixel_motion * subpixel_motion; - - vec2 ss_offset01 = min_max_support * vec2(1, 0) / SCREEN_SIZE; - vec2 ss_offset11 = min_max_support * vec2(0, 1) / SCREEN_SIZE; - - vec4 c00 = textureLod(current_tex, curr_coord - ss_offset11, 0); - vec4 c10 = textureLod(current_tex, curr_coord - ss_offset01, 0); - vec4 c01 = textureLod(current_tex, curr_coord + ss_offset01, 0); - vec4 c11 = textureLod(current_tex, curr_coord + ss_offset11, 0); - - vec4 cmin = min4(c00, c10, c01, c11); - vec4 cmax = max4(c00, c10, c01, c11); - - vec4 cavg = (c00 + c10 + c01 + c11) / 4.0; - - vec4 last_m = textureLod(last_tex, curr_coord + velocity, 0); - - #if USE_CLIPPING - last_m = clip_aabb(cmin.xyz, cmax.xyz, clamp(cavg, cmin, cmax), last_m); - #else - last_m = clamp(last_m, cmin, cmax); - #endif - - float lum0 = get_luminance(curr_m.rgb); - float lum1 = get_luminance(last_m.rgb); - - const float feedback_min = 1 - 1.0 / RS_KEEP_BAD_DURATION; - const float feedback_max = 1 - 1.0 / RS_KEEP_GOOD_DURATION; - - float unbiased_diff = abs(lum0 - lum1) / max(lum0, max(lum1, 0.2)); - float unbiased_weight = 1.0 - unbiased_diff; - float unbiased_weight_sqr = unbiased_weight * unbiased_weight; - float feedback = mix(feedback_min, feedback_max, unbiased_weight_sqr); - - // output - return mix(curr_m, last_m, feedback); - - #endif - - #else - - // Bounding box size - const float bbs = RS_AABB_SIZE; - - // Get current frame neighbor texels - vec4 curr_tl = textureLod(current_tex, curr_coord + vec2(-bbs, -bbs) * one_pixel, 0); - vec4 curr_tr = textureLod(current_tex, curr_coord + vec2(bbs, -bbs) * one_pixel, 0); - vec4 curr_bl = textureLod(current_tex, curr_coord + vec2(-bbs, bbs) * one_pixel, 0); - vec4 curr_br = textureLod(current_tex, curr_coord + vec2(bbs, bbs) * one_pixel, 0); - - // Get current frame neighbor AABB - vec4 curr_min = min5(curr_m, curr_tl, curr_tr, curr_bl, curr_br); - vec4 curr_max = max5(curr_m, curr_tl, curr_tr, curr_bl, curr_br); - - // Get last frame texels - float blend_weight = 1.0; - vec4 last_m = textureLod(last_tex, last_coord, 0); - vec4 last_tl = textureLod(last_tex, last_coord + vec2(-bbs, -bbs) * one_pixel, 0); - vec4 last_tr = textureLod(last_tex, last_coord + vec2(bbs, -bbs) * one_pixel, 0); - vec4 last_bl = textureLod(last_tex, last_coord + vec2(-bbs, bbs) * one_pixel, 0); - vec4 last_br = textureLod(last_tex, last_coord + vec2(bbs, bbs) * one_pixel, 0); - - float neighbor_diff = length(clamp(last_tl.xyz, curr_min.xyz, curr_max.xyz) - last_tl.xyz) - + length(clamp(last_tr.xyz, curr_min.xyz, curr_max.xyz) - last_tr.xyz) - + length(clamp(last_bl.xyz, curr_min.xyz, curr_max.xyz) - last_bl.xyz) - + length(clamp(last_br.xyz, curr_min.xyz, curr_max.xyz) - last_br.xyz); - - const float tolerance = 0.0; - - float max_difference = clamp(max( - get_luminance(last_m.xyz), - get_luminance(curr_m.xyz)), 0.0001, 15.0) * RS_MAX_CLIP_DIST; - - if (neighbor_diff >= max_difference) - blend_weight = 0.0; - - float blend_amount = saturate(distance(last_m.xyz, curr_m.xyz) * - RS_DISTANCE_SCALE); - - // Merge the sample with the current color, in case we can't pick it - last_m = mix(curr_m, last_m, blend_weight); - - float weight = saturate(1.0 / - mix(RS_KEEP_GOOD_DURATION, RS_KEEP_BAD_DURATION, blend_amount)); - - return max(vec4(0.0), mix(last_m, curr_m, weight)); - #endif - -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +#define USE_GBUFFER_EXTENSIONS +#pragma include "includes/color_spaces.inc.glsl" +#pragma include "includes/transforms.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" + +// Maximum color distance +#ifndef RS_MAX_CLIP_DIST + #define RS_MAX_CLIP_DIST 0.5 +#endif + +// Blending factor +#ifndef RS_DISTANCE_SCALE + #define RS_DISTANCE_SCALE 10.0 +#endif + +// How long to keep good pixels +#ifndef RS_KEEP_GOOD_DURATION + #define RS_KEEP_GOOD_DURATION 10.0 +#endif + +// How long to keep bad pixels +#ifndef RS_KEEP_BAD_DURATION + #define RS_KEEP_BAD_DURATION 3.0 +#endif + +#ifndef RS_AABB_SIZE + #define RS_AABB_SIZE 1.0 +#endif + +#ifndef RS_USE_SMOOTH_TECHNIQUE + #define RS_USE_SMOOTH_TECHNIQUE 0 +#endif + +/* + +Uses the reprojection suggested in: +http://www.crytek.com/download/Sousa_Graphics_Gems_CryENGINE3.pdf + +Also based on: +https://github.com/playdeadgames/temporal + +*/ + +#define USE_OPTIMIZATIONS 0 +#define USE_CLIPPING 1 + +vec4 clip_aabb(vec3 aabb_min, vec3 aabb_max, vec4 p, vec4 q) +{ +#if USE_OPTIMIZATIONS + // note: only clips towards aabb center (but fast!) + vec3 p_clip = 0.5 * (aabb_max + aabb_min); + vec3 e_clip = 0.5 * (aabb_max - aabb_min); + + vec4 v_clip = q - vec4(p_clip, p.w); + vec3 v_unit = v_clip.xyz / e_clip; + vec3 a_unit = abs(v_unit); + float ma_unit = max(a_unit.x, max(a_unit.y, a_unit.z)); + + if (ma_unit > 1.0) + return vec4(p_clip, p.w) + v_clip / ma_unit; + else + return q; // point inside aabb +#else + vec4 r = q - p; + vec3 rmax = aabb_max - p.xyz; + vec3 rmin = aabb_min - p.xyz; + + const float eps = 1e-8; + + if (r.x > rmax.x + eps) + r *= (rmax.x / r.x); + if (r.y > rmax.y + eps) + r *= (rmax.y / r.y); + if (r.z > rmax.z + eps) + r *= (rmax.z / r.z); + + if (r.x < rmin.x - eps) + r *= (rmin.x / r.x); + if (r.y < rmin.y - eps) + r *= (rmin.y / r.y); + if (r.z < rmin.z - eps) + r *= (rmin.z / r.z); + + return p + r; +#endif +} + + +#if RS_USE_POSITION_TECHNIQUE + uniform sampler2D Previous_SceneDepth; +#endif + + +vec4 resolve_temporal(sampler2D current_tex, sampler2D last_tex, vec2 curr_coord, vec2 last_coord) { + vec2 one_pixel = 1.0 / SCREEN_SIZE; + vec4 curr_m = textureLod(current_tex, curr_coord, 0); + + // Out of screen, can early out + if (out_of_screen(last_coord)) { + return max(vec4(0.0), curr_m); + } + + #if !RS_USE_SMOOTH_TECHNIQUE + + #if RS_USE_POSITION_TECHNIQUE + + float curr_z = get_depth_at(curr_coord); + vec3 curr_pos = calculate_surface_pos(curr_z, curr_coord); + + float last_z = textureLod(Previous_SceneDepth, last_coord, 0).x; + vec3 last_pos = calculate_surface_pos( + last_z, last_coord, MainSceneData.last_inv_view_proj_mat_no_jitter); + + // Weight by distance + float max_distance = RS_DISTANCE_SCALE; + max_distance *= distance(curr_pos, MainSceneData.camera_pos) / 10.0; + + float weight = 1.0 - saturate(distance(curr_pos, last_pos) / max_distance); + weight *= 1 - 1.0 / RS_KEEP_GOOD_DURATION; + + vec4 last_m = textureLod(last_tex, last_coord, 0); + return mix(curr_m, last_m, weight); + + #else + + const float subpixel_threshold = 0.5; + const float gather_base = 0.5; + const float gather_subpixel_motion = 0.1666; + + float curr_depth = get_depth_at(curr_coord); + float vs_dist = get_linear_z_from_z(curr_depth); + + vec2 velocity = last_coord - curr_coord; + + float texel_vel_mag = length(velocity) * vs_dist; + float subpixel_motion = saturate(subpixel_threshold / (1e-8 + texel_vel_mag)); + float min_max_support = gather_base + gather_subpixel_motion * subpixel_motion; + + vec2 ss_offset01 = min_max_support * vec2(1, 0) / SCREEN_SIZE; + vec2 ss_offset11 = min_max_support * vec2(0, 1) / SCREEN_SIZE; + + vec4 c00 = textureLod(current_tex, curr_coord - ss_offset11, 0); + vec4 c10 = textureLod(current_tex, curr_coord - ss_offset01, 0); + vec4 c01 = textureLod(current_tex, curr_coord + ss_offset01, 0); + vec4 c11 = textureLod(current_tex, curr_coord + ss_offset11, 0); + + vec4 cmin = min4(c00, c10, c01, c11); + vec4 cmax = max4(c00, c10, c01, c11); + + vec4 cavg = (c00 + c10 + c01 + c11) / 4.0; + + vec4 last_m = textureLod(last_tex, curr_coord + velocity, 0); + + #if USE_CLIPPING + last_m = clip_aabb(cmin.xyz, cmax.xyz, clamp(cavg, cmin, cmax), last_m); + #else + last_m = clamp(last_m, cmin, cmax); + #endif + + float lum0 = get_luminance(curr_m.rgb); + float lum1 = get_luminance(last_m.rgb); + + const float feedback_min = 1 - 1.0 / RS_KEEP_BAD_DURATION; + const float feedback_max = 1 - 1.0 / RS_KEEP_GOOD_DURATION; + + float unbiased_diff = abs(lum0 - lum1) / max(lum0, max(lum1, 0.2)); + float unbiased_weight = 1.0 - unbiased_diff; + float unbiased_weight_sqr = unbiased_weight * unbiased_weight; + float feedback = mix(feedback_min, feedback_max, unbiased_weight_sqr); + + // output + return mix(curr_m, last_m, feedback); + + #endif + + #else + + // Bounding box size + const float bbs = RS_AABB_SIZE; + + // Get current frame neighbor texels + vec4 curr_tl = textureLod(current_tex, curr_coord + vec2(-bbs, -bbs) * one_pixel, 0); + vec4 curr_tr = textureLod(current_tex, curr_coord + vec2(bbs, -bbs) * one_pixel, 0); + vec4 curr_bl = textureLod(current_tex, curr_coord + vec2(-bbs, bbs) * one_pixel, 0); + vec4 curr_br = textureLod(current_tex, curr_coord + vec2(bbs, bbs) * one_pixel, 0); + + // Get current frame neighbor AABB + vec4 curr_min = min5(curr_m, curr_tl, curr_tr, curr_bl, curr_br); + vec4 curr_max = max5(curr_m, curr_tl, curr_tr, curr_bl, curr_br); + + // Get last frame texels + float blend_weight = 1.0; + vec4 last_m = textureLod(last_tex, last_coord, 0); + vec4 last_tl = textureLod(last_tex, last_coord + vec2(-bbs, -bbs) * one_pixel, 0); + vec4 last_tr = textureLod(last_tex, last_coord + vec2(bbs, -bbs) * one_pixel, 0); + vec4 last_bl = textureLod(last_tex, last_coord + vec2(-bbs, bbs) * one_pixel, 0); + vec4 last_br = textureLod(last_tex, last_coord + vec2(bbs, bbs) * one_pixel, 0); + + float neighbor_diff = length(clamp(last_tl.xyz, curr_min.xyz, curr_max.xyz) - last_tl.xyz) + + length(clamp(last_tr.xyz, curr_min.xyz, curr_max.xyz) - last_tr.xyz) + + length(clamp(last_bl.xyz, curr_min.xyz, curr_max.xyz) - last_bl.xyz) + + length(clamp(last_br.xyz, curr_min.xyz, curr_max.xyz) - last_br.xyz); + + const float tolerance = 0.0; + + float max_difference = clamp(max( + get_luminance(last_m.xyz), + get_luminance(curr_m.xyz)), 0.0001, 15.0) * RS_MAX_CLIP_DIST; + + if (neighbor_diff >= max_difference) + blend_weight = 0.0; + + float blend_amount = saturate(distance(last_m.xyz, curr_m.xyz) * + RS_DISTANCE_SCALE); + + // Merge the sample with the current color, in case we can't pick it + last_m = mix(curr_m, last_m, blend_weight); + + float weight = saturate(1.0 / + mix(RS_KEEP_GOOD_DURATION, RS_KEEP_BAD_DURATION, blend_amount)); + + return max(vec4(0.0), mix(last_m, curr_m, weight)); + #endif + +} diff --git a/RenderPipelineFile/rpcore/shader/includes/tonemapping.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/tonemapping.inc.glsl index 37b1d06b..e3425f4f 100644 --- a/RenderPipelineFile/rpcore/shader/includes/tonemapping.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/tonemapping.inc.glsl @@ -1,200 +1,200 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -#pragma include "includes/color_spaces.inc.glsl" - -/* - -Tonemapping Operators, some are from: -http://filmicgames.com/archives/75 - -A good sample using them can be found here: -https://www.shadertoy.com/view/lslGzl - -*/ - - -const float exposure_adjustment = 2.0; - - -// No tonemapping -vec3 tonemap_none(vec3 color) { - color *= exposure_adjustment; - return color; -} - -// Different versions of the reinhard tonemap operators -vec3 tonemap_reinhard(vec3 color) -{ - color *= exposure_adjustment; - - #if ENUM_V_ACTIVE(color_correction, reinhard_version, rgb) - - // Simple reinhard operator in rgb space - vec3 rgb = color / (1.0 + color); - - #elif ENUM_V_ACTIVE(color_correction, reinhard_version, luminance) - - // Simple reinhard operator based on luminance - float luminance = get_luminance(color); - vec3 rgb = color / (1.0 + luminance); - - #elif ENUM_V_ACTIVE(color_correction, reinhard_version, white_preserve) - - // Reinhard operator based on luminance, but white preserving - float white = 5.0; - float luminance = get_luminance(color); - vec3 rgb = color * (1.0 + luminance / (white * white)) / (1.0 + luminance); - - #elif ENUM_V_ACTIVE(color_correction, reinhard_version, luminosity) - - // Reinhard operator in the CIE-Yxy color space - vec3 xyY = rgb_to_xyY(color); - - // This saturates too much? - xyY.z = xyY.z / (1.0 + xyY.z); - - // White preserving approach, doesn't work out well, too - // xyY.z = xyY.z * (1.0 + xyY.z * (white*white)) / (1.0 + xyY.z); - vec3 rgb = xyY_to_rgb(xyY); - - #else - #error Unkown reinhard operator version! - #endif - - return rgb; -} - - -// Optimized version of the Haarm-Peter Duiker’s curve -vec3 tonemap_optimized(vec3 color) -{ - color *= exposure_adjustment; - vec3 x = max(vec3(0.0), color - 0.004); - return (x * (6.2 * x + 0.5)) / (x * (6.2 * x + 1.7) + 0.06); -} - -// Exponential tonemapping -vec3 tonemap_exponential(vec3 color) { - color *= exposure_adjustment; - color = 1.0 - exp(-GET_SETTING(color_correction, exponential_factor) * color); - return color; -} - -// Alternative exponential tonemapping -vec3 tonemap_exponential_2(vec3 color) { - color *= exposure_adjustment; - color = exp(-1.0 / (2.72 * color + 0.15)); - return color; -} - - -// Uncharted 2 tonemapping formula -// See: http://de.slideshare.net/ozlael/hable-john-uncharted2-hdr-lighting -vec3 uncharted_2_tonemap_formula(vec3 x) -{ - const float A = GET_SETTING(color_correction, uc2t_shoulder_strength); - const float B = GET_SETTING(color_correction, uc2t_linear_strength); - const float C = GET_SETTING(color_correction, uc2t_linear_angle); - const float D = GET_SETTING(color_correction, uc2t_toe_strength); - const float E = GET_SETTING(color_correction, uc2t_toe_numerator); - const float F = GET_SETTING(color_correction, uc2t_toe_denumerator); - return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F; -} - -// Uncharted 2 tonemap operator -vec3 tonemap_uncharted2(vec3 color) -{ - const float white_base = GET_SETTING(color_correction, uc2t_reference_white); - color *= exposure_adjustment; - const float exposure_bias = 2.0; - vec3 curr = uncharted_2_tonemap_formula(exposure_bias * color); - vec3 white = uncharted_2_tonemap_formula(vec3(white_base)); - return curr / white; -} - -// Tonemapping selector -vec3 do_tonemapping(vec3 color) { - - // Clamp extreme values of the color - color = clamp(color, 1e-5, 1000.0); - - // Select tonemapping operator - #if ENUM_V_ACTIVE(color_correction, tonemap_operator, none) - color = tonemap_none(color); - #elif ENUM_V_ACTIVE(color_correction, tonemap_operator, optimized) - color = tonemap_optimized(color); - return color; - #elif ENUM_V_ACTIVE(color_correction, tonemap_operator, reinhard) - color = tonemap_reinhard(color); - #elif ENUM_V_ACTIVE(color_correction, tonemap_operator, uncharted2) - color = tonemap_uncharted2(color); - #elif ENUM_V_ACTIVE(color_correction, tonemap_operator, exponential) - color = tonemap_exponential(color); - #elif ENUM_V_ACTIVE(color_correction, tonemap_operator, exponential2) - color = tonemap_exponential_2(color); - #else - #error Unkown tonemapping operator! - #endif - - return rgb_to_srgb(color); -} - - -// From: -// http://www.frostbite.com/wp-content/uploads/2014/11/course_notes_moving_frostbite_to_pbr.pdf - -float computeEV100(float aperture, float shutter_time, float ISO) { - // EV number is defined as: - // 2^ EV_s = N^2 / t and EV_s = EV_100 + log2 (S /100) - // This gives - // EV_s = log2 (N^2 / t) - // EV_100 + log2 (S /100) = log2 (N^2 / t) - // EV_100 = log2 (N^2 / t) - log2 (S /100) - // EV_100 = log2 (N^2 / t . 100 / S) - return log2((aperture * aperture) / shutter_time * 100 / ISO); -} - -float computeEV100FromAvgLuminance(float avg_luminance) { - // We later use the middle gray at 12.7% in order to have - // a middle gray at 18% with a sqrt (2) room for specular highlights - // But here we deal with the spot meter measuring the middle gray - // which is fixed at 12.5 for matching standard camera - // constructor settings (i.e. calibration constant K = 12.5) - return log2(avg_luminance * 100.0 / 12.5); -} - -float convertEV100ToExposure(float EV100) { - // Compute the maximum luminance possible with H_sbs sensitivity - // maxLum = 78 / ( S * q ) * N^2 / t - // = 78 / ( S * q ) * 2^ EV_100 - // = 78 / (100 * 0.65) * 2^ EV_100 - // = 1.2 * 2^ EV - float max_luminance = 1.2 * pow(2.0, EV100); - return 1.0 / max_luminance; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +#pragma include "includes/color_spaces.inc.glsl" + +/* + +Tonemapping Operators, some are from: +http://filmicgames.com/archives/75 + +A good sample using them can be found here: +https://www.shadertoy.com/view/lslGzl + +*/ + + +const float exposure_adjustment = 2.0; + + +// No tonemapping +vec3 tonemap_none(vec3 color) { + color *= exposure_adjustment; + return color; +} + +// Different versions of the reinhard tonemap operators +vec3 tonemap_reinhard(vec3 color) +{ + color *= exposure_adjustment; + + #if ENUM_V_ACTIVE(color_correction, reinhard_version, rgb) + + // Simple reinhard operator in rgb space + vec3 rgb = color / (1.0 + color); + + #elif ENUM_V_ACTIVE(color_correction, reinhard_version, luminance) + + // Simple reinhard operator based on luminance + float luminance = get_luminance(color); + vec3 rgb = color / (1.0 + luminance); + + #elif ENUM_V_ACTIVE(color_correction, reinhard_version, white_preserve) + + // Reinhard operator based on luminance, but white preserving + float white = 5.0; + float luminance = get_luminance(color); + vec3 rgb = color * (1.0 + luminance / (white * white)) / (1.0 + luminance); + + #elif ENUM_V_ACTIVE(color_correction, reinhard_version, luminosity) + + // Reinhard operator in the CIE-Yxy color space + vec3 xyY = rgb_to_xyY(color); + + // This saturates too much? + xyY.z = xyY.z / (1.0 + xyY.z); + + // White preserving approach, doesn't work out well, too + // xyY.z = xyY.z * (1.0 + xyY.z * (white*white)) / (1.0 + xyY.z); + vec3 rgb = xyY_to_rgb(xyY); + + #else + #error Unkown reinhard operator version! + #endif + + return rgb; +} + + +// Optimized version of the Haarm-Peter Duiker’s curve +vec3 tonemap_optimized(vec3 color) +{ + color *= exposure_adjustment; + vec3 x = max(vec3(0.0), color - 0.004); + return (x * (6.2 * x + 0.5)) / (x * (6.2 * x + 1.7) + 0.06); +} + +// Exponential tonemapping +vec3 tonemap_exponential(vec3 color) { + color *= exposure_adjustment; + color = 1.0 - exp(-GET_SETTING(color_correction, exponential_factor) * color); + return color; +} + +// Alternative exponential tonemapping +vec3 tonemap_exponential_2(vec3 color) { + color *= exposure_adjustment; + color = exp(-1.0 / (2.72 * color + 0.15)); + return color; +} + + +// Uncharted 2 tonemapping formula +// See: http://de.slideshare.net/ozlael/hable-john-uncharted2-hdr-lighting +vec3 uncharted_2_tonemap_formula(vec3 x) +{ + const float A = GET_SETTING(color_correction, uc2t_shoulder_strength); + const float B = GET_SETTING(color_correction, uc2t_linear_strength); + const float C = GET_SETTING(color_correction, uc2t_linear_angle); + const float D = GET_SETTING(color_correction, uc2t_toe_strength); + const float E = GET_SETTING(color_correction, uc2t_toe_numerator); + const float F = GET_SETTING(color_correction, uc2t_toe_denumerator); + return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F; +} + +// Uncharted 2 tonemap operator +vec3 tonemap_uncharted2(vec3 color) +{ + const float white_base = GET_SETTING(color_correction, uc2t_reference_white); + color *= exposure_adjustment; + const float exposure_bias = 2.0; + vec3 curr = uncharted_2_tonemap_formula(exposure_bias * color); + vec3 white = uncharted_2_tonemap_formula(vec3(white_base)); + return curr / white; +} + +// Tonemapping selector +vec3 do_tonemapping(vec3 color) { + + // Clamp extreme values of the color + color = clamp(color, 1e-5, 1000.0); + + // Select tonemapping operator + #if ENUM_V_ACTIVE(color_correction, tonemap_operator, none) + color = tonemap_none(color); + #elif ENUM_V_ACTIVE(color_correction, tonemap_operator, optimized) + color = tonemap_optimized(color); + return color; + #elif ENUM_V_ACTIVE(color_correction, tonemap_operator, reinhard) + color = tonemap_reinhard(color); + #elif ENUM_V_ACTIVE(color_correction, tonemap_operator, uncharted2) + color = tonemap_uncharted2(color); + #elif ENUM_V_ACTIVE(color_correction, tonemap_operator, exponential) + color = tonemap_exponential(color); + #elif ENUM_V_ACTIVE(color_correction, tonemap_operator, exponential2) + color = tonemap_exponential_2(color); + #else + #error Unkown tonemapping operator! + #endif + + return rgb_to_srgb(color); +} + + +// From: +// http://www.frostbite.com/wp-content/uploads/2014/11/course_notes_moving_frostbite_to_pbr.pdf + +float computeEV100(float aperture, float shutter_time, float ISO) { + // EV number is defined as: + // 2^ EV_s = N^2 / t and EV_s = EV_100 + log2 (S /100) + // This gives + // EV_s = log2 (N^2 / t) + // EV_100 + log2 (S /100) = log2 (N^2 / t) + // EV_100 = log2 (N^2 / t) - log2 (S /100) + // EV_100 = log2 (N^2 / t . 100 / S) + return log2((aperture * aperture) / shutter_time * 100 / ISO); +} + +float computeEV100FromAvgLuminance(float avg_luminance) { + // We later use the middle gray at 12.7% in order to have + // a middle gray at 18% with a sqrt (2) room for specular highlights + // But here we deal with the spot meter measuring the middle gray + // which is fixed at 12.5 for matching standard camera + // constructor settings (i.e. calibration constant K = 12.5) + return log2(avg_luminance * 100.0 / 12.5); +} + +float convertEV100ToExposure(float EV100) { + // Compute the maximum luminance possible with H_sbs sensitivity + // maxLum = 78 / ( S * q ) * N^2 / t + // = 78 / ( S * q ) * 2^ EV_100 + // = 78 / (100 * 0.65) * 2^ EV_100 + // = 1.2 * 2^ EV + float max_luminance = 1.2 * pow(2.0, EV100); + return 1.0 / max_luminance; +} diff --git a/RenderPipelineFile/rpcore/shader/includes/transforms.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/transforms.inc.glsl index ab0b24e0..b23d8071 100644 --- a/RenderPipelineFile/rpcore/shader/includes/transforms.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/transforms.inc.glsl @@ -1,123 +1,123 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -uniform mat4 trans_clip_of_mainCam_to_mainRender; -uniform mat4 trans_mainRender_to_view_of_mainCam; -uniform mat4 trans_mainRender_to_clip_of_mainCam; - -// Computes linear Z from a given Z value, and near and far plane -float get_linear_z_from_z(float z, float near, float far) { - return 2.0 * near * far / (far + near - (z * 2.0 - 1) * (far - near)); -} - -// Computes linear Z from a given Z value -float get_linear_z_from_z(float z) { - return get_linear_z_from_z(z, CAMERA_NEAR, CAMERA_FAR); -} - -// Computes the Z component from a position in NDC space -float get_z_from_ndc(vec3 ndc_pos) { - return get_linear_z_from_z(ndc_pos.z); -} - -// Computes linear Z from a given Z value, and near and far plane, for orthographic projections -float get_linear_z_from_z_ortographic(float z, float near, float far) { - return 2.0 / (far + near - fma(z, 2.0, -1.0) * (far - near)); -} - -// Computes the surface position based on a given Z, a texcoord, and the Inverse MVP matrix -vec3 calculate_surface_pos(float z, vec2 tcoord, mat4 inverse_mvp) { - vec3 ndc_pos = fma(vec3(tcoord.xy, z), vec3(2.0), vec3(-1.0)); - float clip_w = get_z_from_ndc(ndc_pos); - - vec4 proj = inverse_mvp * vec4(ndc_pos * clip_w, clip_w); - return proj.xyz / proj.w; -} - -// Computes the surface position based on a given Z and a texcoord -vec3 calculate_surface_pos(float z, vec2 tcoord) { - #if 0 - return calculate_surface_pos(z, tcoord, trans_clip_of_mainCam_to_mainRender); - #else - float linz = get_linear_z_from_z(z); - return mix( - mix(MainSceneData.ws_frustum_directions[0], - MainSceneData.ws_frustum_directions[1], tcoord.x), - mix(MainSceneData.ws_frustum_directions[2], - MainSceneData.ws_frustum_directions[3], tcoord.x), - tcoord.y - ).xyz * linz + MainSceneData.camera_pos; - #endif -} - -// Computes the surface position based on a given Z and a texcoord, aswell as a -// custom near and far plane, and the inverse MVP. This is for orthographic projections -vec3 calculate_surface_pos_ortho(float z, vec2 tcoord, float near, float far, mat4 inverse_mvp) { - vec3 ndc_pos = fma(vec3(tcoord.xy, z), vec3(2.0), vec3(-1.0)); - float clip_w = get_linear_z_from_z_ortographic(z, near, far); - vec4 result = inverse_mvp * vec4(ndc_pos * clip_w, clip_w); - return result.xyz / result.w; -} - -// Computes the view position from a given Z value and texcoord -vec3 calculate_view_pos(float z, vec2 tcoord) { - vec4 view_pos = MainSceneData.inv_proj_mat * - vec4(fma(tcoord.xy, vec2(2.0), vec2(-1.0)), z, 1.0); - return view_pos.xyz / view_pos.w; -} - -// Computes the NDC position from a given view position -vec3 view_to_screen(vec3 view_pos) { - vec4 projected = MainSceneData.proj_mat * vec4(view_pos, 1); - projected.xyz /= projected.w; - projected.xy = fma(projected.xy, vec2(0.5), vec2(0.5)); - return projected.xyz; -} - -// Converts a view space normal to world space -vec3 view_normal_to_world(vec3 view_normal) { - // We need to transform the coordinate system, should not be required, - // seems to be some panda bug? - view_normal = view_normal.xzy * vec3(1, -1, 1); - return normalize((vec4(view_normal, 0) * trans_mainRender_to_view_of_mainCam).xyz); -} - -// Converts a world space position to screen space position (NDC) -vec3 world_to_screen(vec3 world_pos) { - vec4 proj = trans_mainRender_to_clip_of_mainCam * vec4(world_pos, 1); - proj.xyz /= proj.w; - proj.xyz = fma(proj.xyz, vec3(0.5), vec3(0.5)); - return proj.xyz; -} - -// Converts a world space normal to view space -vec3 world_normal_to_view(vec3 world_normal) { - vec4 proj = trans_mainRender_to_view_of_mainCam * vec4(world_normal, 0); - proj.xyz *= vec3(1, -1, 1); - return normalize(proj.xzy); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +uniform mat4 trans_clip_of_mainCam_to_mainRender; +uniform mat4 trans_mainRender_to_view_of_mainCam; +uniform mat4 trans_mainRender_to_clip_of_mainCam; + +// Computes linear Z from a given Z value, and near and far plane +float get_linear_z_from_z(float z, float near, float far) { + return 2.0 * near * far / (far + near - (z * 2.0 - 1) * (far - near)); +} + +// Computes linear Z from a given Z value +float get_linear_z_from_z(float z) { + return get_linear_z_from_z(z, CAMERA_NEAR, CAMERA_FAR); +} + +// Computes the Z component from a position in NDC space +float get_z_from_ndc(vec3 ndc_pos) { + return get_linear_z_from_z(ndc_pos.z); +} + +// Computes linear Z from a given Z value, and near and far plane, for orthographic projections +float get_linear_z_from_z_ortographic(float z, float near, float far) { + return 2.0 / (far + near - fma(z, 2.0, -1.0) * (far - near)); +} + +// Computes the surface position based on a given Z, a texcoord, and the Inverse MVP matrix +vec3 calculate_surface_pos(float z, vec2 tcoord, mat4 inverse_mvp) { + vec3 ndc_pos = fma(vec3(tcoord.xy, z), vec3(2.0), vec3(-1.0)); + float clip_w = get_z_from_ndc(ndc_pos); + + vec4 proj = inverse_mvp * vec4(ndc_pos * clip_w, clip_w); + return proj.xyz / proj.w; +} + +// Computes the surface position based on a given Z and a texcoord +vec3 calculate_surface_pos(float z, vec2 tcoord) { + #if 0 + return calculate_surface_pos(z, tcoord, trans_clip_of_mainCam_to_mainRender); + #else + float linz = get_linear_z_from_z(z); + return mix( + mix(MainSceneData.ws_frustum_directions[0], + MainSceneData.ws_frustum_directions[1], tcoord.x), + mix(MainSceneData.ws_frustum_directions[2], + MainSceneData.ws_frustum_directions[3], tcoord.x), + tcoord.y + ).xyz * linz + MainSceneData.camera_pos; + #endif +} + +// Computes the surface position based on a given Z and a texcoord, aswell as a +// custom near and far plane, and the inverse MVP. This is for orthographic projections +vec3 calculate_surface_pos_ortho(float z, vec2 tcoord, float near, float far, mat4 inverse_mvp) { + vec3 ndc_pos = fma(vec3(tcoord.xy, z), vec3(2.0), vec3(-1.0)); + float clip_w = get_linear_z_from_z_ortographic(z, near, far); + vec4 result = inverse_mvp * vec4(ndc_pos * clip_w, clip_w); + return result.xyz / result.w; +} + +// Computes the view position from a given Z value and texcoord +vec3 calculate_view_pos(float z, vec2 tcoord) { + vec4 view_pos = MainSceneData.inv_proj_mat * + vec4(fma(tcoord.xy, vec2(2.0), vec2(-1.0)), z, 1.0); + return view_pos.xyz / view_pos.w; +} + +// Computes the NDC position from a given view position +vec3 view_to_screen(vec3 view_pos) { + vec4 projected = MainSceneData.proj_mat * vec4(view_pos, 1); + projected.xyz /= projected.w; + projected.xy = fma(projected.xy, vec2(0.5), vec2(0.5)); + return projected.xyz; +} + +// Converts a view space normal to world space +vec3 view_normal_to_world(vec3 view_normal) { + // We need to transform the coordinate system, should not be required, + // seems to be some panda bug? + view_normal = view_normal.xzy * vec3(1, -1, 1); + return normalize((vec4(view_normal, 0) * trans_mainRender_to_view_of_mainCam).xyz); +} + +// Converts a world space position to screen space position (NDC) +vec3 world_to_screen(vec3 world_pos) { + vec4 proj = trans_mainRender_to_clip_of_mainCam * vec4(world_pos, 1); + proj.xyz /= proj.w; + proj.xyz = fma(proj.xyz, vec3(0.5), vec3(0.5)); + return proj.xyz; +} + +// Converts a world space normal to view space +vec3 world_normal_to_view(vec3 world_normal) { + vec4 proj = trans_mainRender_to_view_of_mainCam * vec4(world_normal, 0); + proj.xyz *= vec3(1, -1, 1); + return normalize(proj.xzy); +} diff --git a/RenderPipelineFile/rpcore/shader/includes/upsampling.inc.glsl b/RenderPipelineFile/rpcore/shader/includes/upsampling.inc.glsl index bc5c77a9..394aa9a3 100644 --- a/RenderPipelineFile/rpcore/shader/includes/upsampling.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/upsampling.inc.glsl @@ -1,142 +1,142 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -#pragma include "includes/color_spaces.inc.glsl" - -float mitchell_netravali_weights(float x) -{ - // Mitchell Netravali Reconstruction Filter - - // cubic B-spline - // const float b = 1.0, c = 0.0; - - // recommended - // const float b = 1.0 / 3.0, c = 1.0 / 3.0; - - // Catmull-Rom spline - const float b = 0.0, c = 1.0 / 2.0; - - float ax = abs(x); - return (ax < 1.0) ? - ((12.0 - 9.0 * b - 6.0 * c) * ax * ax * ax + - (-18.0 + 12.0 * b + 6.0 * c) * ax * ax + (6.0 - 2.0 * b)) / 6.0 - : ((ax >= 1.0) && (ax < 2.0)) ? - ((-b - 6.0 * c) * ax * ax * ax + (6.0 * b + 30.0 * c) * ax * ax + - (-12.0 * b - 48.0 * c) * ax + (8.0 * b + 24.0 * c)) / 6.0 - : 0.0; -} - -vec4 sample_color(sampler2D tex, vec2 uv) { - return saturate(textureLod(tex, uv, 0)); -} - -// Based on: -// http://glslsandbox.com/e#14615.1 -vec4 bicubic_filter(sampler2D tex, vec2 uv) -{ - - vec2 px = 1.0 / textureSize(tex, 0).xy; - vec2 f = fract(uv / px); - vec2 texel = (uv / px - f + 0.5) * px; - vec4 weights = vec4(mitchell_netravali_weights(1.0 + f.x), - mitchell_netravali_weights(f.x), - mitchell_netravali_weights(1.0 - f.x), - mitchell_netravali_weights(2.0 - f.x)); - vec4 t1 = - sample_color(tex, texel + vec2(-px.x, -px.y)) * weights.x + - sample_color(tex, texel + vec2(0.0, -px.y)) * weights.y + - sample_color(tex, texel + vec2(px.x, -px.y)) * weights.z + - sample_color(tex, texel + vec2(2.0 * px.x, -px.y)) * weights.w; - vec4 t2 = - sample_color(tex, texel + vec2(-px.x, 0.0)) * weights.x + - sample_color(tex, texel + vec2(0.0)) * weights.y + - sample_color(tex, texel + vec2(px.x, 0.0)) * weights.z + - sample_color(tex, texel + vec2(2.0 * px.x, 0.0)) * weights.w; - vec4 t3 = - sample_color(tex, texel + vec2(-px.x, px.y)) * weights.x + - sample_color(tex, texel + vec2(0.0, px.y)) * weights.y + - sample_color(tex, texel + vec2(px.x, px.y)) * weights.z + - sample_color(tex, texel + vec2(2.0 * px.x, px.y)) * weights.w; - vec4 t4 = - sample_color(tex, texel + vec2(-px.x, 2.0 * px.y)) * weights.x + - sample_color(tex, texel + vec2(0.0, 2.0 * px.y)) * weights.y + - sample_color(tex, texel + vec2(px.x, 2.0 * px.y)) * weights.z + - sample_color(tex, texel + vec2(2.0 * px.x, 2.0 * px.y)) * weights.w; - - return mitchell_netravali_weights(1.0 + f.y) * t1 + - mitchell_netravali_weights(f.y) * t2 + - mitchell_netravali_weights(1.0 - f.y) * t3 + - mitchell_netravali_weights(2.0 - f.y) * t4; -} - -// Simple bilinear filter -vec4 bilinear_filter(sampler2D tex, vec2 uv) { - vec2 pixel_size = 1.0 / textureSize(tex, 0).xy; - vec4 color = vec4(0); - float radius = 0.5; - color += sample_color(tex, uv + vec2(-radius, radius) * pixel_size); - color += sample_color(tex, uv + vec2(radius, radius) * pixel_size); - color += sample_color(tex, uv + vec2(-radius, -radius) * pixel_size); - color += sample_color(tex, uv + vec2(radius, -radius) * pixel_size); - return color * 0.25; -} - -// Directional filter with a small sharpen kernel -vec4 directional_filter(sampler2D tex, vec2 uv) { - vec2 pixel_size = 1.0 / textureSize(tex, 0).xy; - float radius = 1.0; - - vec4 color_nw = sample_color(tex, uv + vec2(-radius, -radius) * pixel_size); - vec4 color_ne = sample_color(tex, uv + vec2(radius, -radius) * pixel_size); - vec4 color_sw = sample_color(tex, uv + vec2(-radius, radius) * pixel_size); - vec4 color_se = sample_color(tex, uv + vec2(radius, radius) * pixel_size); - - vec4 color = (color_nw + color_ne + color_sw + color_se) * 0.25; - - float luma_nw = get_luminance(color_nw.xyz); - float luma_ne = get_luminance(color_ne.xyz) + 1e-5; - float luma_sw = get_luminance(color_sw.xyz); - float luma_se = get_luminance(color_se.xyz); - - float center_luminance = get_luminance(color.xyz); - float uv_offs = saturate(center_luminance) * 2 - 1; - - float diag_bl_tr = luma_sw - luma_ne; - float diag_br_tl = luma_se - luma_nw; - - vec2 direction = normalize(vec2(diag_bl_tr + diag_br_tl, diag_bl_tr - diag_br_tl) + 1e-7); - vec2 direction_inv = vec2(-direction.y, direction.x); - uv_offs = 1 - ((1 - uv_offs) * (1 - uv_offs)); - - vec2 uv_offset = uv + direction_inv * pixel_size * abs(uv_offs) * -0.125; - vec4 color_n = sample_color(tex, uv_offset - direction * 0.125 * pixel_size); - vec4 color_p = sample_color(tex, uv_offset + direction * 0.125 * pixel_size); - - const float sharpness = 0.9; - return (color_n + color_p) * ((sharpness + 1.0) * 0.5) - color * sharpness; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +#pragma include "includes/color_spaces.inc.glsl" + +float mitchell_netravali_weights(float x) +{ + // Mitchell Netravali Reconstruction Filter + + // cubic B-spline + // const float b = 1.0, c = 0.0; + + // recommended + // const float b = 1.0 / 3.0, c = 1.0 / 3.0; + + // Catmull-Rom spline + const float b = 0.0, c = 1.0 / 2.0; + + float ax = abs(x); + return (ax < 1.0) ? + ((12.0 - 9.0 * b - 6.0 * c) * ax * ax * ax + + (-18.0 + 12.0 * b + 6.0 * c) * ax * ax + (6.0 - 2.0 * b)) / 6.0 + : ((ax >= 1.0) && (ax < 2.0)) ? + ((-b - 6.0 * c) * ax * ax * ax + (6.0 * b + 30.0 * c) * ax * ax + + (-12.0 * b - 48.0 * c) * ax + (8.0 * b + 24.0 * c)) / 6.0 + : 0.0; +} + +vec4 sample_color(sampler2D tex, vec2 uv) { + return saturate(textureLod(tex, uv, 0)); +} + +// Based on: +// http://glslsandbox.com/e#14615.1 +vec4 bicubic_filter(sampler2D tex, vec2 uv) +{ + + vec2 px = 1.0 / textureSize(tex, 0).xy; + vec2 f = fract(uv / px); + vec2 texel = (uv / px - f + 0.5) * px; + vec4 weights = vec4(mitchell_netravali_weights(1.0 + f.x), + mitchell_netravali_weights(f.x), + mitchell_netravali_weights(1.0 - f.x), + mitchell_netravali_weights(2.0 - f.x)); + vec4 t1 = + sample_color(tex, texel + vec2(-px.x, -px.y)) * weights.x + + sample_color(tex, texel + vec2(0.0, -px.y)) * weights.y + + sample_color(tex, texel + vec2(px.x, -px.y)) * weights.z + + sample_color(tex, texel + vec2(2.0 * px.x, -px.y)) * weights.w; + vec4 t2 = + sample_color(tex, texel + vec2(-px.x, 0.0)) * weights.x + + sample_color(tex, texel + vec2(0.0)) * weights.y + + sample_color(tex, texel + vec2(px.x, 0.0)) * weights.z + + sample_color(tex, texel + vec2(2.0 * px.x, 0.0)) * weights.w; + vec4 t3 = + sample_color(tex, texel + vec2(-px.x, px.y)) * weights.x + + sample_color(tex, texel + vec2(0.0, px.y)) * weights.y + + sample_color(tex, texel + vec2(px.x, px.y)) * weights.z + + sample_color(tex, texel + vec2(2.0 * px.x, px.y)) * weights.w; + vec4 t4 = + sample_color(tex, texel + vec2(-px.x, 2.0 * px.y)) * weights.x + + sample_color(tex, texel + vec2(0.0, 2.0 * px.y)) * weights.y + + sample_color(tex, texel + vec2(px.x, 2.0 * px.y)) * weights.z + + sample_color(tex, texel + vec2(2.0 * px.x, 2.0 * px.y)) * weights.w; + + return mitchell_netravali_weights(1.0 + f.y) * t1 + + mitchell_netravali_weights(f.y) * t2 + + mitchell_netravali_weights(1.0 - f.y) * t3 + + mitchell_netravali_weights(2.0 - f.y) * t4; +} + +// Simple bilinear filter +vec4 bilinear_filter(sampler2D tex, vec2 uv) { + vec2 pixel_size = 1.0 / textureSize(tex, 0).xy; + vec4 color = vec4(0); + float radius = 0.5; + color += sample_color(tex, uv + vec2(-radius, radius) * pixel_size); + color += sample_color(tex, uv + vec2(radius, radius) * pixel_size); + color += sample_color(tex, uv + vec2(-radius, -radius) * pixel_size); + color += sample_color(tex, uv + vec2(radius, -radius) * pixel_size); + return color * 0.25; +} + +// Directional filter with a small sharpen kernel +vec4 directional_filter(sampler2D tex, vec2 uv) { + vec2 pixel_size = 1.0 / textureSize(tex, 0).xy; + float radius = 1.0; + + vec4 color_nw = sample_color(tex, uv + vec2(-radius, -radius) * pixel_size); + vec4 color_ne = sample_color(tex, uv + vec2(radius, -radius) * pixel_size); + vec4 color_sw = sample_color(tex, uv + vec2(-radius, radius) * pixel_size); + vec4 color_se = sample_color(tex, uv + vec2(radius, radius) * pixel_size); + + vec4 color = (color_nw + color_ne + color_sw + color_se) * 0.25; + + float luma_nw = get_luminance(color_nw.xyz); + float luma_ne = get_luminance(color_ne.xyz) + 1e-5; + float luma_sw = get_luminance(color_sw.xyz); + float luma_se = get_luminance(color_se.xyz); + + float center_luminance = get_luminance(color.xyz); + float uv_offs = saturate(center_luminance) * 2 - 1; + + float diag_bl_tr = luma_sw - luma_ne; + float diag_br_tl = luma_se - luma_nw; + + vec2 direction = normalize(vec2(diag_bl_tr + diag_br_tl, diag_bl_tr - diag_br_tl) + 1e-7); + vec2 direction_inv = vec2(-direction.y, direction.x); + uv_offs = 1 - ((1 - uv_offs) * (1 - uv_offs)); + + vec2 uv_offset = uv + direction_inv * pixel_size * abs(uv_offs) * -0.125; + vec4 color_n = sample_color(tex, uv_offset - direction * 0.125 * pixel_size); + vec4 color_p = sample_color(tex, uv_offset + direction * 0.125 * pixel_size); + + const float sharpness = 0.9; + return (color_n + color_p) * ((sharpness + 1.0) * 0.5) - color * sharpness; +} diff --git a/RenderPipelineFile/rpcore/shader/includes/vertex_output.struct.glsl b/RenderPipelineFile/rpcore/shader/includes/vertex_output.struct.glsl index b0555481..a9c9e021 100644 --- a/RenderPipelineFile/rpcore/shader/includes/vertex_output.struct.glsl +++ b/RenderPipelineFile/rpcore/shader/includes/vertex_output.struct.glsl @@ -1,36 +1,36 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -struct VertexOutput { - #if IN_GBUFFER_SHADER - vec4 last_proj_position; - #endif - vec3 normal; - vec2 texcoord; - vec3 position; -}; +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +struct VertexOutput { + #if IN_GBUFFER_SHADER + vec4 last_proj_position; + #endif + vec3 normal; + vec2 texcoord; + vec3 position; +}; diff --git a/RenderPipelineFile/rpcore/shader/pixel_inspector.frag.glsl b/RenderPipelineFile/rpcore/shader/pixel_inspector.frag.glsl index 83c9e764..1ad9cad3 100644 --- a/RenderPipelineFile/rpcore/shader/pixel_inspector.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/pixel_inspector.frag.glsl @@ -1,48 +1,48 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -in vec2 texcoord; -out vec4 color; -uniform sampler2D SceneTex; -uniform vec2 mousePos; - -void main() { - int border = 3; - int zoom = 5; - - ivec2 int_coord = ivec2(texcoord * vec2(400, 300)); - if (int_coord.x < border || int_coord.y < border || - int_coord.x >= 400 - border || int_coord.y >= 300 - border) { - color = vec4(0.05, 0.05, 0.05, 1); - return; - } - - int_coord = (int_coord) / zoom - (ivec2(200, 150)) / zoom + ivec2(mousePos); - color = texelFetch(SceneTex, int_coord, 0); - color.w = 1.0; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +in vec2 texcoord; +out vec4 color; +uniform sampler2D SceneTex; +uniform vec2 mousePos; + +void main() { + int border = 3; + int zoom = 5; + + ivec2 int_coord = ivec2(texcoord * vec2(400, 300)); + if (int_coord.x < border || int_coord.y < border || + int_coord.x >= 400 - border || int_coord.y >= 300 - border) { + color = vec4(0.05, 0.05, 0.05, 1); + return; + } + + int_coord = (int_coord) / zoom - (ivec2(200, 150)) / zoom + ivec2(mousePos); + color = texelFetch(SceneTex, int_coord, 0); + color.w = 1.0; +} diff --git a/RenderPipelineFile/rpcore/shader/process_command_queue.frag.glsl b/RenderPipelineFile/rpcore/shader/process_command_queue.frag.glsl index 775ad001..82a0f2a3 100644 --- a/RenderPipelineFile/rpcore/shader/process_command_queue.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/process_command_queue.frag.glsl @@ -1,140 +1,140 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" - -uniform samplerBuffer CommandQueue; -uniform writeonly imageBuffer RESTRICT LightData; -uniform writeonly imageBuffer RESTRICT SourceData; -uniform int commandCount; - -// Reads a single float from the data stack -float read_float(inout int stack_ptr) { - return texelFetch(CommandQueue, stack_ptr++).x; -} - -// Reads a single int from the data stack -int read_int(inout int stack_ptr) { - return gpu_cq_unpack_int_from_float(read_float(stack_ptr)); -} - -// Reads a 4-component vector from the data stack -vec4 read_vec4(inout int stack_ptr) { - stack_ptr += 4; - return vec4( - texelFetch(CommandQueue, stack_ptr - 4).x, - texelFetch(CommandQueue, stack_ptr - 3).x, - texelFetch(CommandQueue, stack_ptr - 2).x, - texelFetch(CommandQueue, stack_ptr - 1).x - ); -} - -void main() { - - // Store a pointer to the current stack index, its passed as a handle to all - // read functions - int stack_ptr = 0; - - // Process each command - for (int command_index = 0; command_index < commandCount; ++command_index) { - stack_ptr = command_index * 32; - int command_type = read_int(stack_ptr); - - switch(command_type) { - - // Invalid Command Code - case CMD_invalid: break; - - - // Store Light - case CMD_store_light: { - - // Read the destination slot of the light - int slot = read_int(stack_ptr); - int offs = slot * 4; - - // Copy the data over - for (int i = 0; i < 4; ++i) { - imageStore(LightData, offs + i, read_vec4(stack_ptr)); - } - break; - } - - // Remove Light - case CMD_remove_light: { - - // Read the lights slot position - int slot = read_int(stack_ptr); - int offs = slot * 4; - - // Set the data to all zeroes, this indicates a null light - for (int i = 0; i < 4; ++i) { - imageStore(LightData, offs + i, vec4(0)); - } - break; - } - - // Store Source - case CMD_store_source: { - - int slot = read_int(stack_ptr); - int offs = slot * 5; - - // Copy the data over - for (int i = 0; i < 5; ++i) { - imageStore(SourceData, offs + i, read_vec4(stack_ptr)); - } - - break; - } - - // Remove consecutive sources - case CMD_remove_sources: { - int base_slot = read_int(stack_ptr); - int num_slots = read_int(stack_ptr); - - for (int slot = base_slot; slot < base_slot + num_slots; ++slot) { - int offs = slot * 5; - - // Set the data to all zeroes, this indicates an unused source - for (int i = 0; i < 5; ++i) { - imageStore(SourceData, offs + i, vec4(0)); - } - } - break; - } - - - - // .. further commands will follow here - - } - - - } -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" + +uniform samplerBuffer CommandQueue; +uniform writeonly imageBuffer RESTRICT LightData; +uniform writeonly imageBuffer RESTRICT SourceData; +uniform int commandCount; + +// Reads a single float from the data stack +float read_float(inout int stack_ptr) { + return texelFetch(CommandQueue, stack_ptr++).x; +} + +// Reads a single int from the data stack +int read_int(inout int stack_ptr) { + return gpu_cq_unpack_int_from_float(read_float(stack_ptr)); +} + +// Reads a 4-component vector from the data stack +vec4 read_vec4(inout int stack_ptr) { + stack_ptr += 4; + return vec4( + texelFetch(CommandQueue, stack_ptr - 4).x, + texelFetch(CommandQueue, stack_ptr - 3).x, + texelFetch(CommandQueue, stack_ptr - 2).x, + texelFetch(CommandQueue, stack_ptr - 1).x + ); +} + +void main() { + + // Store a pointer to the current stack index, its passed as a handle to all + // read functions + int stack_ptr = 0; + + // Process each command + for (int command_index = 0; command_index < commandCount; ++command_index) { + stack_ptr = command_index * 32; + int command_type = read_int(stack_ptr); + + switch(command_type) { + + // Invalid Command Code + case CMD_invalid: break; + + + // Store Light + case CMD_store_light: { + + // Read the destination slot of the light + int slot = read_int(stack_ptr); + int offs = slot * 4; + + // Copy the data over + for (int i = 0; i < 4; ++i) { + imageStore(LightData, offs + i, read_vec4(stack_ptr)); + } + break; + } + + // Remove Light + case CMD_remove_light: { + + // Read the lights slot position + int slot = read_int(stack_ptr); + int offs = slot * 4; + + // Set the data to all zeroes, this indicates a null light + for (int i = 0; i < 4; ++i) { + imageStore(LightData, offs + i, vec4(0)); + } + break; + } + + // Store Source + case CMD_store_source: { + + int slot = read_int(stack_ptr); + int offs = slot * 5; + + // Copy the data over + for (int i = 0; i < 5; ++i) { + imageStore(SourceData, offs + i, read_vec4(stack_ptr)); + } + + break; + } + + // Remove consecutive sources + case CMD_remove_sources: { + int base_slot = read_int(stack_ptr); + int num_slots = read_int(stack_ptr); + + for (int slot = base_slot; slot < base_slot + num_slots; ++slot) { + int offs = slot * 5; + + // Set the data to all zeroes, this indicates an unused source + for (int i = 0; i < 5; ++i) { + imageStore(SourceData, offs + i, vec4(0)); + } + } + break; + } + + + + // .. further commands will follow here + + } + + + } +} diff --git a/RenderPipelineFile/rpcore/shader/render_pipeline_base.inc.glsl b/RenderPipelineFile/rpcore/shader/render_pipeline_base.inc.glsl index 85b9050b..4191c8f4 100644 --- a/RenderPipelineFile/rpcore/shader/render_pipeline_base.inc.glsl +++ b/RenderPipelineFile/rpcore/shader/render_pipeline_base.inc.glsl @@ -1,134 +1,134 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -// Main configuration file, included by all shaders, provides generic defines -// and functions. - -#pragma include "/$$rptemp/$$pipeline_shader_config.inc.glsl" - -// Optionally unroll *all* loops, this might be faster, but might also be -// slower. Right now, every shader specifies on his own if he wants to unroll -// his loops or not. -// #pragma optionNV (unroll all) - -// Nvidia specific optimizations -#ifdef IS_NVIDIA -#pragma optionNV (fastmath on) -#pragma optionNV (ifcvt none) -#pragma optionNV (inline all) -#pragma optionNV (strict on) -#endif - -#pragma include "/$$rptemp/$$main_scene_data.inc.glsl" - -#ifdef USE_TIME_OF_DAY -#pragma include "/$$rptemp/$$daytime_config.inc.glsl" -#endif - -// Screen size macro -#define WINDOW_WIDTH MainSceneData.screen_size.x -#define WINDOW_HEIGHT MainSceneData.screen_size.y -#define NATIVE_WINDOW_WIDTH MainSceneData.native_screen_size.x -#define NATIVE_WINDOW_HEIGHT MainSceneData.native_screen_size.y -#define SCREEN_SIZE vec2(WINDOW_WIDTH, WINDOW_HEIGHT) -#define SCREEN_SIZE_INT ivec2(WINDOW_WIDTH, WINDOW_HEIGHT) -#define ASPECT_RATIO float(float(WINDOW_HEIGHT) / float(WINDOW_WIDTH)) -#define NATIVE_SCREEN_SIZE vec2(NATIVE_WINDOW_WIDTH, NATIVE_WINDOW_HEIGHT) - -// Plugin functions -#define HAVE_PLUGIN(PLUGIN_NAME) (HAVE_PLUGIN_ ## PLUGIN_NAME) -#define GET_SETTING(PLUGIN_NAME, SETTING_NAME) (PLUGIN_NAME ## _ ## SETTING_NAME) -#define GET_ENUM_VALUE(PLUGIN_NAME, SETTING_NAME, ENUM_KEY) (enum_ ##PLUGIN_NAME ## _ ## SETTING_NAME ## _ ## ENUM_KEY) -#define ENUM_V_ACTIVE(PLUGIN_NAME, SETTING_NAME, ENUM_KEY) (HAVE_PLUGIN(PLUGIN_NAME) && GET_SETTING(PLUGIN_NAME, SETTING_NAME) && GET_SETTING(PLUGIN_NAME, SETTING_NAME) == GET_ENUM_VALUE(PLUGIN_NAME, SETTING_NAME, ENUM_KEY)) - -// Render mode functions -#define DEBUG_MODE ANY_DEBUG_MODE -#define MODE_ACTIVE(MODE_ID) (DEBUG_MODE && (_RM_ ## MODE_ID)) -#define SPECIAL_MODE_ACTIVE(MODE_ID) (_RM_ ## MODE_ID) - -// Branch modes for translucency. -// This serves for the purpose to be enabled or disabled easily. -// Right now, it seems its faster not to branch. It heavily depends on -// the amount of translucent materials on the screen. -#if 0 -#define BRANCH_TRANSLUCENCY(m) if (m.translucency > 0.01) { -#define END_BRANCH_TRANSLUCENCY() } -#else -#define BRANCH_TRANSLUCENCY(m) -#define END_BRANCH_TRANSLUCENCY() -#endif - -// Disable translucency? -#if 1 -#undef BRANCH_TRANSLUCENCY -#undef END_BRANCH_TRANSLUCENCY -#define BRANCH_TRANSLUCENCY(m) if (false) { -#define END_BRANCH_TRANSLUCENCY() } -#endif - -// Restrict qualifier, only on AMD cards, Nvidia can't handle it. See: -// https://devtalk.nvidia.com/default/topic/546817/restrict-keyword-crashes-glsl-compiler/ -// Also, intel seems to expect the keyword (correctly) *before* the image specifier, -// in contrast to AMD, so we disable it on intel gpus, too. -#if IS_AMD - #define RESTRICT restrict -#else - #define RESTRICT -#endif - -// TODO: -#define SUPPORT_PCF 1 - -// Controls the roughness of the clearcoat layer -#define CLEARCOAT_ROUGHNESS 0.001 -#define CLEARCOAT_SPECULAR 0.16 -#define CLEARCOAT_IOR 1.51 - -// Controls the brightness of the fallback cubemap -#if REFERENCE_MODE - #define DEFAULT_ENVMAP_BRIGHTNESS 1.0 -#else - #if HAVE_PLUGIN(color_correction) - #define DEFAULT_ENVMAP_BRIGHTNESS 1.0 - #else - #define DEFAULT_ENVMAP_BRIGHTNESS 1.0 - #endif -#endif - -// Minimum roughness, avoids infinitely bright highlights -#define MINIMUM_ROUGHNESS 0.01 - -// Controls at which point the sun is below the horizon and does not have any -// influence anymore -#define SUN_VECTOR_HORIZON 0.0 - -// Whether to use a completely white environment, only used in reference mode -#define USE_WHITE_ENVIRONMENT 0 - - -#pragma include "includes/common_functions.inc.glsl" +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +// Main configuration file, included by all shaders, provides generic defines +// and functions. + +#pragma include "/$$rptemp/$$pipeline_shader_config.inc.glsl" + +// Optionally unroll *all* loops, this might be faster, but might also be +// slower. Right now, every shader specifies on his own if he wants to unroll +// his loops or not. +// #pragma optionNV (unroll all) + +// Nvidia specific optimizations +#ifdef IS_NVIDIA +#pragma optionNV (fastmath on) +#pragma optionNV (ifcvt none) +#pragma optionNV (inline all) +#pragma optionNV (strict on) +#endif + +#pragma include "/$$rptemp/$$main_scene_data.inc.glsl" + +#ifdef USE_TIME_OF_DAY +#pragma include "/$$rptemp/$$daytime_config.inc.glsl" +#endif + +// Screen size macro +#define WINDOW_WIDTH MainSceneData.screen_size.x +#define WINDOW_HEIGHT MainSceneData.screen_size.y +#define NATIVE_WINDOW_WIDTH MainSceneData.native_screen_size.x +#define NATIVE_WINDOW_HEIGHT MainSceneData.native_screen_size.y +#define SCREEN_SIZE vec2(WINDOW_WIDTH, WINDOW_HEIGHT) +#define SCREEN_SIZE_INT ivec2(WINDOW_WIDTH, WINDOW_HEIGHT) +#define ASPECT_RATIO float(float(WINDOW_HEIGHT) / float(WINDOW_WIDTH)) +#define NATIVE_SCREEN_SIZE vec2(NATIVE_WINDOW_WIDTH, NATIVE_WINDOW_HEIGHT) + +// Plugin functions +#define HAVE_PLUGIN(PLUGIN_NAME) (HAVE_PLUGIN_ ## PLUGIN_NAME) +#define GET_SETTING(PLUGIN_NAME, SETTING_NAME) (PLUGIN_NAME ## _ ## SETTING_NAME) +#define GET_ENUM_VALUE(PLUGIN_NAME, SETTING_NAME, ENUM_KEY) (enum_ ##PLUGIN_NAME ## _ ## SETTING_NAME ## _ ## ENUM_KEY) +#define ENUM_V_ACTIVE(PLUGIN_NAME, SETTING_NAME, ENUM_KEY) (HAVE_PLUGIN(PLUGIN_NAME) && GET_SETTING(PLUGIN_NAME, SETTING_NAME) && GET_SETTING(PLUGIN_NAME, SETTING_NAME) == GET_ENUM_VALUE(PLUGIN_NAME, SETTING_NAME, ENUM_KEY)) + +// Render mode functions +#define DEBUG_MODE ANY_DEBUG_MODE +#define MODE_ACTIVE(MODE_ID) (DEBUG_MODE && (_RM_ ## MODE_ID)) +#define SPECIAL_MODE_ACTIVE(MODE_ID) (_RM_ ## MODE_ID) + +// Branch modes for translucency. +// This serves for the purpose to be enabled or disabled easily. +// Right now, it seems its faster not to branch. It heavily depends on +// the amount of translucent materials on the screen. +#if 0 +#define BRANCH_TRANSLUCENCY(m) if (m.translucency > 0.01) { +#define END_BRANCH_TRANSLUCENCY() } +#else +#define BRANCH_TRANSLUCENCY(m) +#define END_BRANCH_TRANSLUCENCY() +#endif + +// Disable translucency? +#if 1 +#undef BRANCH_TRANSLUCENCY +#undef END_BRANCH_TRANSLUCENCY +#define BRANCH_TRANSLUCENCY(m) if (false) { +#define END_BRANCH_TRANSLUCENCY() } +#endif + +// Restrict qualifier, only on AMD cards, Nvidia can't handle it. See: +// https://devtalk.nvidia.com/default/topic/546817/restrict-keyword-crashes-glsl-compiler/ +// Also, intel seems to expect the keyword (correctly) *before* the image specifier, +// in contrast to AMD, so we disable it on intel gpus, too. +#if IS_AMD + #define RESTRICT restrict +#else + #define RESTRICT +#endif + +// TODO: +#define SUPPORT_PCF 1 + +// Controls the roughness of the clearcoat layer +#define CLEARCOAT_ROUGHNESS 0.001 +#define CLEARCOAT_SPECULAR 0.16 +#define CLEARCOAT_IOR 1.51 + +// Controls the brightness of the fallback cubemap +#if REFERENCE_MODE + #define DEFAULT_ENVMAP_BRIGHTNESS 1.0 +#else + #if HAVE_PLUGIN(color_correction) + #define DEFAULT_ENVMAP_BRIGHTNESS 1.0 + #else + #define DEFAULT_ENVMAP_BRIGHTNESS 1.0 + #endif +#endif + +// Minimum roughness, avoids infinitely bright highlights +#define MINIMUM_ROUGHNESS 0.01 + +// Controls at which point the sun is below the horizon and does not have any +// influence anymore +#define SUN_VECTOR_HORIZON 0.0 + +// Whether to use a completely white environment, only used in reference mode +#define USE_WHITE_ENVIRONMENT 0 + + +#pragma include "includes/common_functions.inc.glsl" diff --git a/RenderPipelineFile/rpcore/shader/templates/envmap.frag.glsl b/RenderPipelineFile/rpcore/shader/templates/envmap.frag.glsl index ea2f6140..c0f2203c 100644 --- a/RenderPipelineFile/rpcore/shader/templates/envmap.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/templates/envmap.frag.glsl @@ -1,62 +1,62 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// Shader used for the environment map - -%defines% - -#define USE_TIME_OF_DAY 1 -#pragma include "render_pipeline_base.inc.glsl" - -%includes% -%inout% - -#pragma include "includes/nonviewspace_shading_pipeline.inc.glsl" - -layout(location = 0) out vec4 result; - -void main() { - vec2 texcoord = vOutput.texcoord; - MaterialBaseInput mInput = get_input_from_p3d(p3d_Material); - - %texcoord% - - MaterialShaderOutput m = prepare_material(mInput, texcoord); - - %material% - - // Actual lighting pass - Material m_out = emulate_gbuffer_pass(m, vOutput.position); - - vec3 ambient = get_forward_ambient(m_out); - vec3 sun_lighting = get_sun_shading(m_out); - vec3 lights = get_forward_light_shading(m_out); - - vec3 combined_lighting = ambient + lights + sun_lighting; - result = vec4(combined_lighting, 1); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// Shader used for the environment map + +%defines% + +#define USE_TIME_OF_DAY 1 +#pragma include "render_pipeline_base.inc.glsl" + +%includes% +%inout% + +#pragma include "includes/nonviewspace_shading_pipeline.inc.glsl" + +layout(location = 0) out vec4 result; + +void main() { + vec2 texcoord = vOutput.texcoord; + MaterialBaseInput mInput = get_input_from_p3d(p3d_Material); + + %texcoord% + + MaterialShaderOutput m = prepare_material(mInput, texcoord); + + %material% + + // Actual lighting pass + Material m_out = emulate_gbuffer_pass(m, vOutput.position); + + vec3 ambient = get_forward_ambient(m_out); + vec3 sun_lighting = get_sun_shading(m_out); + vec3 lights = get_forward_light_shading(m_out); + + vec3 combined_lighting = ambient + lights + sun_lighting; + result = vec4(combined_lighting, 1); +} diff --git a/RenderPipelineFile/rpcore/shader/templates/forward.frag.glsl b/RenderPipelineFile/rpcore/shader/templates/forward.frag.glsl index 68937bbe..0030451f 100644 --- a/RenderPipelineFile/rpcore/shader/templates/forward.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/templates/forward.frag.glsl @@ -1,189 +1,189 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// Forward shading shader - -// Set DONT_FETCH_DEFAULT_TEXTURES to prevent any material textures to get sampled -// Set DONT_SET_MATERIAL_PROPERTIES to prevent any material properties to be set. - -%defines% - -#define VIEWSPACE_SHADING 1 -#define USE_TIME_OF_DAY 1 -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/transforms.inc.glsl" -#pragma include "includes/vertex_output.struct.glsl" -#pragma include "includes/material.inc.glsl" - -%includes% - -layout(location = 0) in VertexOutput vOutput; - -uniform Panda3DMaterial p3d_Material; - -#pragma include "includes/normal_mapping.inc.glsl" -#pragma include "includes/forward_shading.inc.glsl" - -#if DONT_FETCH_DEFAULT_TEXTURES - // Don't bind any samplers in this case, so the user can do it on his own -#else - uniform sampler2D p3d_Texture0; - uniform sampler2D p3d_Texture1; - uniform sampler2D p3d_Texture2; - uniform sampler2D p3d_Texture3; - - // Only use the displacement texture if we actually need it. - #if OPT_PARALLAX_MAPPING - uniform sampler2D p3d_Texture4; - #endif - -#endif - -uniform sampler2D ShadedScene; - -%inout% - -layout(location = 0) out vec4 color_result; - -void main() { - - MaterialBaseInput mInput = get_input_from_p3d(p3d_Material); - - vec2 texcoord = vOutput.texcoord; - - // Get texture coordinate - #if OPT_PARALLAX_MAPPING - texcoord = get_parallax_texcoord(p3d_Texture4, mInput.normalfactor); - #endif - - %texcoord% - - // Fetch texture data - #if DONT_FETCH_DEFAULT_TEXTURES - float sampled_ior = 0.0; - float sampled_roughness = 0.0; - #else - float sampled_ior = texture(p3d_Texture2, texcoord).x; - float sampled_roughness = texture(p3d_Texture3, texcoord).x; - #endif - - #if OPT_ALPHA_TESTING - #if DONT_FETCH_DEFAULT_TEXTURES - // No alpha testing when not using default textures - #else - // Do binary alpha testing, but weight it based on the distance to the - // camera. This prevents alpha tested objects getting too thin when - // viewed from a high distance. - // TODO: Might want to make the alpha testing distance configurable - vec4 sampled_diffuse = texture(p3d_Texture0, texcoord); - float dist_to_camera = distance(MainSceneData.camera_pos, vOutput.position); - float alpha_factor = mix(0.99, 0.1, saturate(dist_to_camera / 15.0)); - if (sampled_diffuse.w < alpha_factor) discard; - #endif - #else - // In case we don't do alpha testing, we don't need the w-component, so - // don't fetch it. In practice, most GPU's will still load the w component - // and discard it, but it surely can't hurt. - #if DONT_FETCH_DEFAULT_TEXTURES - vec3 sampled_diffuse = vec3(0); - #else - vec3 sampled_diffuse = texture(p3d_Texture0, texcoord).xyz; - #endif - #endif - - vec3 material_nrm = vOutput.normal; - - #if OPT_NORMAL_MAPPING - #if DONT_FETCH_DEFAULT_TEXTURES - // No normal mapping when not using default textures - #else - { - // Perform normal mapping if enabled - vec3 sampled_normal = texture(p3d_Texture1, texcoord).xyz; - vec3 detail_normal = unpack_texture_normal(sampled_normal); - material_nrm = apply_normal_map(vOutput.normal, detail_normal, mInput.normalfactor); - } - #endif - #endif - - // Generate the material output - MaterialShaderOutput m; - - #if DONT_SET_MATERIAL_PROPERTIES - // Leave material properties unitialized, and hope the user knows - // what he's doing. - #else - m.shading_model = mInput.shading_model; - - #if DONT_FETCH_DEFAULT_TEXTURES - m.basecolor = mInput.color; - #else - m.basecolor = mInput.color * sampled_diffuse.xyz; - #endif - m.normal = material_nrm; - m.metallic = mInput.metallic; - m.specular_ior = blend_ior(mInput.specular_ior, sampled_ior); - m.roughness = mInput.roughness * sampled_roughness; - m.shading_model_param0 = mInput.arbitrary0; - #endif - - %material% - - // Emulate gbuffer pass - Material m_out = emulate_gbuffer_pass(m, vOutput.position); - - vec3 view_dir = normalize(m_out.position - MainSceneData.camera_pos); - vec3 color = vec3(0); - - float alpha = m_out.shading_model_param0; - AmbientResult ambient = get_full_forward_ambient(m_out, view_dir); - - color += ambient.diffuse; - color += ambient.specular; - color += get_sun_shading(m_out, view_dir); - - // XXX: Apply shading from lights too - - alpha = mix(alpha, 1.0, ambient.fresnel); - - // Refraction (experimental) - #if 0 - vec3 refracted_vector_view = refract(view_dir, m_out.normal, 1.04); - float refraction_dist = 0.5; - vec3 refraction_dest = m_out.position + refracted_vector_view * refraction_dist; - vec2 refraction_screen = saturate(world_to_screen(refraction_dest).xy); - - vec2 scene_coord = get_texcoord(); - vec3 back_color = textureLod(ShadedScene, refraction_screen, 0).xyz; - - color_result.xyz = back_color * (1 - alpha) + color * alpha; - color_result.w = 1.0; - #else - color_result = vec4(color, alpha); - #endif -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// Forward shading shader + +// Set DONT_FETCH_DEFAULT_TEXTURES to prevent any material textures to get sampled +// Set DONT_SET_MATERIAL_PROPERTIES to prevent any material properties to be set. + +%defines% + +#define VIEWSPACE_SHADING 1 +#define USE_TIME_OF_DAY 1 +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/transforms.inc.glsl" +#pragma include "includes/vertex_output.struct.glsl" +#pragma include "includes/material.inc.glsl" + +%includes% + +layout(location = 0) in VertexOutput vOutput; + +uniform Panda3DMaterial p3d_Material; + +#pragma include "includes/normal_mapping.inc.glsl" +#pragma include "includes/forward_shading.inc.glsl" + +#if DONT_FETCH_DEFAULT_TEXTURES + // Don't bind any samplers in this case, so the user can do it on his own +#else + uniform sampler2D p3d_Texture0; + uniform sampler2D p3d_Texture1; + uniform sampler2D p3d_Texture2; + uniform sampler2D p3d_Texture3; + + // Only use the displacement texture if we actually need it. + #if OPT_PARALLAX_MAPPING + uniform sampler2D p3d_Texture4; + #endif + +#endif + +uniform sampler2D ShadedScene; + +%inout% + +layout(location = 0) out vec4 color_result; + +void main() { + + MaterialBaseInput mInput = get_input_from_p3d(p3d_Material); + + vec2 texcoord = vOutput.texcoord; + + // Get texture coordinate + #if OPT_PARALLAX_MAPPING + texcoord = get_parallax_texcoord(p3d_Texture4, mInput.normalfactor); + #endif + + %texcoord% + + // Fetch texture data + #if DONT_FETCH_DEFAULT_TEXTURES + float sampled_ior = 0.0; + float sampled_roughness = 0.0; + #else + float sampled_ior = texture(p3d_Texture2, texcoord).x; + float sampled_roughness = texture(p3d_Texture3, texcoord).x; + #endif + + #if OPT_ALPHA_TESTING + #if DONT_FETCH_DEFAULT_TEXTURES + // No alpha testing when not using default textures + #else + // Do binary alpha testing, but weight it based on the distance to the + // camera. This prevents alpha tested objects getting too thin when + // viewed from a high distance. + // TODO: Might want to make the alpha testing distance configurable + vec4 sampled_diffuse = texture(p3d_Texture0, texcoord); + float dist_to_camera = distance(MainSceneData.camera_pos, vOutput.position); + float alpha_factor = mix(0.99, 0.1, saturate(dist_to_camera / 15.0)); + if (sampled_diffuse.w < alpha_factor) discard; + #endif + #else + // In case we don't do alpha testing, we don't need the w-component, so + // don't fetch it. In practice, most GPU's will still load the w component + // and discard it, but it surely can't hurt. + #if DONT_FETCH_DEFAULT_TEXTURES + vec3 sampled_diffuse = vec3(0); + #else + vec3 sampled_diffuse = texture(p3d_Texture0, texcoord).xyz; + #endif + #endif + + vec3 material_nrm = vOutput.normal; + + #if OPT_NORMAL_MAPPING + #if DONT_FETCH_DEFAULT_TEXTURES + // No normal mapping when not using default textures + #else + { + // Perform normal mapping if enabled + vec3 sampled_normal = texture(p3d_Texture1, texcoord).xyz; + vec3 detail_normal = unpack_texture_normal(sampled_normal); + material_nrm = apply_normal_map(vOutput.normal, detail_normal, mInput.normalfactor); + } + #endif + #endif + + // Generate the material output + MaterialShaderOutput m; + + #if DONT_SET_MATERIAL_PROPERTIES + // Leave material properties unitialized, and hope the user knows + // what he's doing. + #else + m.shading_model = mInput.shading_model; + + #if DONT_FETCH_DEFAULT_TEXTURES + m.basecolor = mInput.color; + #else + m.basecolor = mInput.color * sampled_diffuse.xyz; + #endif + m.normal = material_nrm; + m.metallic = mInput.metallic; + m.specular_ior = blend_ior(mInput.specular_ior, sampled_ior); + m.roughness = mInput.roughness * sampled_roughness; + m.shading_model_param0 = mInput.arbitrary0; + #endif + + %material% + + // Emulate gbuffer pass + Material m_out = emulate_gbuffer_pass(m, vOutput.position); + + vec3 view_dir = normalize(m_out.position - MainSceneData.camera_pos); + vec3 color = vec3(0); + + float alpha = m_out.shading_model_param0; + AmbientResult ambient = get_full_forward_ambient(m_out, view_dir); + + color += ambient.diffuse; + color += ambient.specular; + color += get_sun_shading(m_out, view_dir); + + // XXX: Apply shading from lights too + + alpha = mix(alpha, 1.0, ambient.fresnel); + + // Refraction (experimental) + #if 0 + vec3 refracted_vector_view = refract(view_dir, m_out.normal, 1.04); + float refraction_dist = 0.5; + vec3 refraction_dest = m_out.position + refracted_vector_view * refraction_dist; + vec2 refraction_screen = saturate(world_to_screen(refraction_dest).xy); + + vec2 scene_coord = get_texcoord(); + vec3 back_color = textureLod(ShadedScene, refraction_screen, 0).xyz; + + color_result.xyz = back_color * (1 - alpha) + color * alpha; + color_result.w = 1.0; + #else + color_result = vec4(color, alpha); + #endif +} diff --git a/RenderPipelineFile/rpcore/shader/templates/gbuffer.frag.glsl b/RenderPipelineFile/rpcore/shader/templates/gbuffer.frag.glsl index 8b8c66ca..3a936331 100644 --- a/RenderPipelineFile/rpcore/shader/templates/gbuffer.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/templates/gbuffer.frag.glsl @@ -1,155 +1,155 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// Default GBuffer fragment shader. Supports normal mapping, parallax mapping, -// and sampling default textures. - -// Set DONT_FETCH_DEFAULT_TEXTURES to prevent any material textures to get sampled -// Set DONT_SET_MATERIAL_PROPERTIES to prevent any material properties to be set. - -%defines% - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/vertex_output.struct.glsl" -#pragma include "includes/material.inc.glsl" - -%includes% - -layout(location = 0) in VertexOutput vOutput; - -uniform Panda3DMaterial p3d_Material; - -// Late include of the gbuffer packing since it needs the vOutput -#pragma include "includes/normal_mapping.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" - - -#if DONT_FETCH_DEFAULT_TEXTURES - // Don't bind any samplers in this case, so the user can do it on his own -#else - uniform sampler2D p3d_Texture0; - uniform sampler2D p3d_Texture1; - uniform sampler2D p3d_Texture2; - uniform sampler2D p3d_Texture3; - - // Only use the displacement texture if we actually need it. - #if OPT_PARALLAX_MAPPING - uniform sampler2D p3d_Texture4; - #endif - -#endif - -%inout% - -void main() { - - MaterialBaseInput mInput = get_input_from_p3d(p3d_Material); - - vec2 texcoord = vOutput.texcoord; - - // Get texture coordinate - #if OPT_PARALLAX_MAPPING - texcoord = get_parallax_texcoord(p3d_Texture4, mInput.normalfactor); - #endif - - %texcoord% - - // Fetch texture data - #if DONT_FETCH_DEFAULT_TEXTURES - float sampled_ior = 0.0; - float sampled_roughness = 0.0; - #else - float sampled_ior = texture(p3d_Texture2, texcoord).x; - float sampled_roughness = texture(p3d_Texture3, texcoord).x; - #endif - - #if OPT_ALPHA_TESTING - #if DONT_FETCH_DEFAULT_TEXTURES - // No alpha testing when not using default textures - #else - // Do binary alpha testing, but weight it based on the distance to the - // camera. This prevents alpha tested objects getting too thin when - // viewed from a high distance. - // TODO: Might want to make the alpha testing distance configurable - vec4 sampled_diffuse = texture(p3d_Texture0, texcoord); - float dist_to_camera = distance(MainSceneData.camera_pos, vOutput.position); - float alpha_factor = mix(0.99, 0.1, saturate(dist_to_camera / 15.0)); - if (sampled_diffuse.w < alpha_factor) discard; - #endif - #else - // In case we don't do alpha testing, we don't need the w-component, so - // don't fetch it. In practice, most GPU's will still load the w component - // and discard it, but it surely can't hurt. - #if DONT_FETCH_DEFAULT_TEXTURES - vec3 sampled_diffuse = vec3(0); - #else - vec3 sampled_diffuse = texture(p3d_Texture0, texcoord).xyz; - #endif - #endif - - vec3 material_nrm = vOutput.normal; - - #if OPT_NORMAL_MAPPING - #if DONT_FETCH_DEFAULT_TEXTURES - // No normal mapping when not using default textures - #else - { - // Perform normal mapping if enabled - vec3 sampled_normal = texture(p3d_Texture1, texcoord).xyz; - vec3 detail_normal = unpack_texture_normal(sampled_normal); - material_nrm = apply_normal_map( - vOutput.normal, detail_normal, mInput.normalfactor); - } - #endif - #endif - - // Generate the material output - MaterialShaderOutput m; - - #if DONT_SET_MATERIAL_PROPERTIES - // Leave material properties unitialized, and hope the user knows - // what he's doing. - #else - m.shading_model = mInput.shading_model; - - #if DONT_FETCH_DEFAULT_TEXTURES - m.basecolor = mInput.color; - #else - m.basecolor = mInput.color * sampled_diffuse.xyz; - #endif - m.normal = material_nrm; - m.metallic = mInput.metallic; - m.specular_ior = blend_ior(mInput.specular_ior, sampled_ior); - m.roughness = mInput.roughness * sampled_roughness; - m.shading_model_param0 = mInput.arbitrary0; - #endif - - %material% - - render_material(m); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// Default GBuffer fragment shader. Supports normal mapping, parallax mapping, +// and sampling default textures. + +// Set DONT_FETCH_DEFAULT_TEXTURES to prevent any material textures to get sampled +// Set DONT_SET_MATERIAL_PROPERTIES to prevent any material properties to be set. + +%defines% + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/vertex_output.struct.glsl" +#pragma include "includes/material.inc.glsl" + +%includes% + +layout(location = 0) in VertexOutput vOutput; + +uniform Panda3DMaterial p3d_Material; + +// Late include of the gbuffer packing since it needs the vOutput +#pragma include "includes/normal_mapping.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" + + +#if DONT_FETCH_DEFAULT_TEXTURES + // Don't bind any samplers in this case, so the user can do it on his own +#else + uniform sampler2D p3d_Texture0; + uniform sampler2D p3d_Texture1; + uniform sampler2D p3d_Texture2; + uniform sampler2D p3d_Texture3; + + // Only use the displacement texture if we actually need it. + #if OPT_PARALLAX_MAPPING + uniform sampler2D p3d_Texture4; + #endif + +#endif + +%inout% + +void main() { + + MaterialBaseInput mInput = get_input_from_p3d(p3d_Material); + + vec2 texcoord = vOutput.texcoord; + + // Get texture coordinate + #if OPT_PARALLAX_MAPPING + texcoord = get_parallax_texcoord(p3d_Texture4, mInput.normalfactor); + #endif + + %texcoord% + + // Fetch texture data + #if DONT_FETCH_DEFAULT_TEXTURES + float sampled_ior = 0.0; + float sampled_roughness = 0.0; + #else + float sampled_ior = texture(p3d_Texture2, texcoord).x; + float sampled_roughness = texture(p3d_Texture3, texcoord).x; + #endif + + #if OPT_ALPHA_TESTING + #if DONT_FETCH_DEFAULT_TEXTURES + // No alpha testing when not using default textures + #else + // Do binary alpha testing, but weight it based on the distance to the + // camera. This prevents alpha tested objects getting too thin when + // viewed from a high distance. + // TODO: Might want to make the alpha testing distance configurable + vec4 sampled_diffuse = texture(p3d_Texture0, texcoord); + float dist_to_camera = distance(MainSceneData.camera_pos, vOutput.position); + float alpha_factor = mix(0.99, 0.1, saturate(dist_to_camera / 15.0)); + if (sampled_diffuse.w < alpha_factor) discard; + #endif + #else + // In case we don't do alpha testing, we don't need the w-component, so + // don't fetch it. In practice, most GPU's will still load the w component + // and discard it, but it surely can't hurt. + #if DONT_FETCH_DEFAULT_TEXTURES + vec3 sampled_diffuse = vec3(0); + #else + vec3 sampled_diffuse = texture(p3d_Texture0, texcoord).xyz; + #endif + #endif + + vec3 material_nrm = vOutput.normal; + + #if OPT_NORMAL_MAPPING + #if DONT_FETCH_DEFAULT_TEXTURES + // No normal mapping when not using default textures + #else + { + // Perform normal mapping if enabled + vec3 sampled_normal = texture(p3d_Texture1, texcoord).xyz; + vec3 detail_normal = unpack_texture_normal(sampled_normal); + material_nrm = apply_normal_map( + vOutput.normal, detail_normal, mInput.normalfactor); + } + #endif + #endif + + // Generate the material output + MaterialShaderOutput m; + + #if DONT_SET_MATERIAL_PROPERTIES + // Leave material properties unitialized, and hope the user knows + // what he's doing. + #else + m.shading_model = mInput.shading_model; + + #if DONT_FETCH_DEFAULT_TEXTURES + m.basecolor = mInput.color; + #else + m.basecolor = mInput.color * sampled_diffuse.xyz; + #endif + m.normal = material_nrm; + m.metallic = mInput.metallic; + m.specular_ior = blend_ior(mInput.specular_ior, sampled_ior); + m.roughness = mInput.roughness * sampled_roughness; + m.shading_model_param0 = mInput.arbitrary0; + #endif + + %material% + + render_material(m); +} diff --git a/RenderPipelineFile/rpcore/shader/templates/shadow.frag.glsl b/RenderPipelineFile/rpcore/shader/templates/shadow.frag.glsl index 73a26a14..d73267e7 100644 --- a/RenderPipelineFile/rpcore/shader/templates/shadow.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/templates/shadow.frag.glsl @@ -1,59 +1,59 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -%defines% - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/vertex_output.struct.glsl" - -%includes% -%inout% - -layout(location = 0) in VertexOutput vOutput; - -#if OPT_ALPHA_TESTING -uniform sampler2D p3d_Texture0; -#endif - -void main() { - #if OPT_ALPHA_TESTING - - // Alpha tested shadows. This seems to be quite expensive, so we are - // only doing this for the objects which really need it (like trees) - float sampled_alpha = texture(p3d_Texture0, vOutput.texcoord).w; - if (sampled_alpha < 0.1) discard; - #endif - - %alpha_test% - - #if 0 - // This prevents the effect compiler from emitting a warning about - // a undefined hook. - %material% - #endif -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +%defines% + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/vertex_output.struct.glsl" + +%includes% +%inout% + +layout(location = 0) in VertexOutput vOutput; + +#if OPT_ALPHA_TESTING +uniform sampler2D p3d_Texture0; +#endif + +void main() { + #if OPT_ALPHA_TESTING + + // Alpha tested shadows. This seems to be quite expensive, so we are + // only doing this for the objects which really need it (like trees) + float sampled_alpha = texture(p3d_Texture0, vOutput.texcoord).w; + if (sampled_alpha < 0.1) discard; + #endif + + %alpha_test% + + #if 0 + // This prevents the effect compiler from emitting a warning about + // a undefined hook. + %material% + #endif +} diff --git a/RenderPipelineFile/rpcore/shader/templates/vertex.vert.glsl b/RenderPipelineFile/rpcore/shader/templates/vertex.vert.glsl index 9fe91586..9c648117 100644 --- a/RenderPipelineFile/rpcore/shader/templates/vertex.vert.glsl +++ b/RenderPipelineFile/rpcore/shader/templates/vertex.vert.glsl @@ -1,75 +1,75 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -%defines% - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/vertex_output.struct.glsl" -#pragma include "includes/material.inc.glsl" - -in vec4 p3d_Vertex; -in vec3 p3d_Normal; -in vec2 p3d_MultiTexCoord0; - -uniform mat4 p3d_ViewProjectionMatrix; -uniform mat4 p3d_ModelViewProjectionMatrix; - -#if EXPERIMENTAL_PREV_TRANSFORM - uniform mat4 p3d_PrevModelMatrix; -#endif - -uniform mat4 p3d_ModelMatrix; -uniform mat3 tpose_world_to_model; - -layout(location = 0) out VertexOutput vOutput; - -%includes% -%inout% - -void main() { - vOutput.texcoord = p3d_MultiTexCoord0; - vOutput.normal = normalize(tpose_world_to_model * p3d_Normal).xyz; - vOutput.position = (p3d_ModelMatrix * p3d_Vertex).xyz; - - %transform% - - // TODO: We have to account for skinning, we can maybe use hardware skinning for this. - #if IN_GBUFFER_SHADER - #if EXPERIMENTAL_PREV_TRANSFORM - vOutput.last_proj_position = p3d_ViewProjectionMatrix * - (p3d_PrevModelMatrix * p3d_Vertex); - #else - vOutput.last_proj_position = p3d_ViewProjectionMatrix * - vec4(vOutput.position, 1); - #endif - #endif - - gl_Position = p3d_ViewProjectionMatrix * vec4(vOutput.position, 1); - - %post_transform% -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +%defines% + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/vertex_output.struct.glsl" +#pragma include "includes/material.inc.glsl" + +in vec4 p3d_Vertex; +in vec3 p3d_Normal; +in vec2 p3d_MultiTexCoord0; + +uniform mat4 p3d_ViewProjectionMatrix; +uniform mat4 p3d_ModelViewProjectionMatrix; + +#if EXPERIMENTAL_PREV_TRANSFORM + uniform mat4 p3d_PrevModelMatrix; +#endif + +uniform mat4 p3d_ModelMatrix; +uniform mat3 tpose_world_to_model; + +layout(location = 0) out VertexOutput vOutput; + +%includes% +%inout% + +void main() { + vOutput.texcoord = p3d_MultiTexCoord0; + vOutput.normal = normalize(tpose_world_to_model * p3d_Normal).xyz; + vOutput.position = (p3d_ModelMatrix * p3d_Vertex).xyz; + + %transform% + + // TODO: We have to account for skinning, we can maybe use hardware skinning for this. + #if IN_GBUFFER_SHADER + #if EXPERIMENTAL_PREV_TRANSFORM + vOutput.last_proj_position = p3d_ViewProjectionMatrix * + (p3d_PrevModelMatrix * p3d_Vertex); + #else + vOutput.last_proj_position = p3d_ViewProjectionMatrix * + vec4(vOutput.position, 1); + #endif + #endif + + gl_Position = p3d_ViewProjectionMatrix * vec4(vOutput.position, 1); + + %post_transform% +} diff --git a/RenderPipelineFile/rpcore/shader/templates/voxelize.frag.glsl b/RenderPipelineFile/rpcore/shader/templates/voxelize.frag.glsl index ac52a747..db4b3d9b 100644 --- a/RenderPipelineFile/rpcore/shader/templates/voxelize.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/templates/voxelize.frag.glsl @@ -1,74 +1,74 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// Shader used for voxelization, required for GI - -%defines% - -#define USE_TIME_OF_DAY 1 -#pragma include "render_pipeline_base.inc.glsl" - -%includes% -%inout% - -// Voxel data -uniform vec3 voxelGridPosition; -uniform writeonly image3D RESTRICT VoxelGridDest; - -#pragma include "includes/nonviewspace_shading_pipeline.inc.glsl" - -void main() { - vec2 texcoord = vOutput.texcoord; - MaterialBaseInput mInput = get_input_from_p3d(p3d_Material); - - %texcoord% - - MaterialShaderOutput m = prepare_material(mInput, texcoord); - - %material% - - Material m_out = emulate_gbuffer_pass(m, vOutput.position); - - // Actual lighting pass - vec3 sun_lighting = get_sun_shading(m_out); - vec3 lights = get_forward_light_shading(m_out); - vec3 combined_lighting = lights + sun_lighting; - - // Tonemapping to pack color - combined_lighting = combined_lighting / (1 + combined_lighting); - - // Get destination voxel - const int resolution = GET_SETTING(vxgi, grid_resolution); - const float ws_size = GET_SETTING(vxgi, grid_ws_size); - vec3 vs_coord = (vOutput.position + - vOutput.normal * 0.0 - voxelGridPosition + ws_size) / (2.0 * ws_size); - ivec3 vs_icoord = ivec3(vs_coord * resolution + 1e-5); - - // Write voxel - imageStore(VoxelGridDest, vs_icoord, vec4(combined_lighting, 1.0)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// Shader used for voxelization, required for GI + +%defines% + +#define USE_TIME_OF_DAY 1 +#pragma include "render_pipeline_base.inc.glsl" + +%includes% +%inout% + +// Voxel data +uniform vec3 voxelGridPosition; +uniform writeonly image3D RESTRICT VoxelGridDest; + +#pragma include "includes/nonviewspace_shading_pipeline.inc.glsl" + +void main() { + vec2 texcoord = vOutput.texcoord; + MaterialBaseInput mInput = get_input_from_p3d(p3d_Material); + + %texcoord% + + MaterialShaderOutput m = prepare_material(mInput, texcoord); + + %material% + + Material m_out = emulate_gbuffer_pass(m, vOutput.position); + + // Actual lighting pass + vec3 sun_lighting = get_sun_shading(m_out); + vec3 lights = get_forward_light_shading(m_out); + vec3 combined_lighting = lights + sun_lighting; + + // Tonemapping to pack color + combined_lighting = combined_lighting / (1 + combined_lighting); + + // Get destination voxel + const int resolution = GET_SETTING(vxgi, grid_resolution); + const float ws_size = GET_SETTING(vxgi, grid_ws_size); + vec3 vs_coord = (vOutput.position + + vOutput.normal * 0.0 - voxelGridPosition + ws_size) / (2.0 * ws_size); + ivec3 vs_icoord = ivec3(vs_coord * resolution + 1e-5); + + // Write voxel + imageStore(VoxelGridDest, vs_icoord, vec4(combined_lighting, 1.0)); +} diff --git a/RenderPipelineFile/rpcore/shader/tiled_culling.vert.glsl b/RenderPipelineFile/rpcore/shader/tiled_culling.vert.glsl index dad0c5e7..e90c01f9 100644 --- a/RenderPipelineFile/rpcore/shader/tiled_culling.vert.glsl +++ b/RenderPipelineFile/rpcore/shader/tiled_culling.vert.glsl @@ -1,67 +1,67 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" - -in vec4 p3d_Vertex; -uniform isamplerBuffer CellListBuffer; -uniform int threadCount; - -uint get_slice_count(uint cell_count) { - return uint(ceil(float(cell_count * threadCount) / LC_CULLING_SLICE_WIDTH)); -} - -void main() { - - // XXX: Get rid of all of this unnecessary uint divides. It does not matter - // much for performance (since this is a vertex shader), but the logic - // is still awful and could be much simpler. - - // I don't want to move the logic to the CPU though, as passing uniforms - // is totally slow without UBO's. - - // Find the amount of cells to shade - int num_total_cells = texelFetch(CellListBuffer, 0).x; - - // Compute the amount of pixel lines required to shade all cells - uint num_used_slices = get_slice_count(num_total_cells); - - // Compute the percentage factor of the used slices - uint max_tiles = MainSceneData.lc_tile_count.x * - MainSceneData.lc_tile_count.y * - LC_TILE_SLICES; - uint max_slices = get_slice_count(max_tiles); - - float percentage_height = saturate(num_used_slices / float(max_slices)); - // percentage_height = 1; - - // Store the vertex position. - gl_Position = vec4( - p3d_Vertex.x, - fma(fma(p3d_Vertex.z, 0.5, 0.5) * percentage_height, 2.0, -1.0) , 0, 1); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" + +in vec4 p3d_Vertex; +uniform isamplerBuffer CellListBuffer; +uniform int threadCount; + +uint get_slice_count(uint cell_count) { + return uint(ceil(float(cell_count * threadCount) / LC_CULLING_SLICE_WIDTH)); +} + +void main() { + + // XXX: Get rid of all of this unnecessary uint divides. It does not matter + // much for performance (since this is a vertex shader), but the logic + // is still awful and could be much simpler. + + // I don't want to move the logic to the CPU though, as passing uniforms + // is totally slow without UBO's. + + // Find the amount of cells to shade + int num_total_cells = texelFetch(CellListBuffer, 0).x; + + // Compute the amount of pixel lines required to shade all cells + uint num_used_slices = get_slice_count(num_total_cells); + + // Compute the percentage factor of the used slices + uint max_tiles = MainSceneData.lc_tile_count.x * + MainSceneData.lc_tile_count.y * + LC_TILE_SLICES; + uint max_slices = get_slice_count(max_tiles); + + float percentage_height = saturate(num_used_slices / float(max_slices)); + // percentage_height = 1; + + // Store the vertex position. + gl_Position = vec4( + p3d_Vertex.x, + fma(fma(p3d_Vertex.z, 0.5, 0.5) * percentage_height, 2.0, -1.0) , 0, 1); +} diff --git a/RenderPipelineFile/rpcore/shader/upscale_stage.frag.glsl b/RenderPipelineFile/rpcore/shader/upscale_stage.frag.glsl index 2d94cd94..46364a47 100644 --- a/RenderPipelineFile/rpcore/shader/upscale_stage.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/upscale_stage.frag.glsl @@ -1,48 +1,48 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/noise.inc.glsl" -#pragma include "includes/upsampling.inc.glsl" - -uniform sampler2D ShadedScene; -out vec4 result; - -void main() { - #if 1 - vec2 texcoord = (ivec2(gl_FragCoord.xy) + 0.5) / NATIVE_SCREEN_SIZE; - // vec4 scene_color = bicubic_filter(ShadedScene, texcoord); - // vec4 scene_color = bilinear_filter(ShadedScene, texcoord); - vec4 scene_color = directional_filter(ShadedScene, texcoord); - #else - vec2 texcoord = (ivec2(gl_FragCoord.xy) + 0.5) / NATIVE_SCREEN_SIZE; - vec4 scene_color = textureLod(ShadedScene, texcoord, 0); - #endif - - result = vec4(scene_color.xyz, 1); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/noise.inc.glsl" +#pragma include "includes/upsampling.inc.glsl" + +uniform sampler2D ShadedScene; +out vec4 result; + +void main() { + #if 1 + vec2 texcoord = (ivec2(gl_FragCoord.xy) + 0.5) / NATIVE_SCREEN_SIZE; + // vec4 scene_color = bicubic_filter(ShadedScene, texcoord); + // vec4 scene_color = bilinear_filter(ShadedScene, texcoord); + vec4 scene_color = directional_filter(ShadedScene, texcoord); + #else + vec2 texcoord = (ivec2(gl_FragCoord.xy) + 0.5) / NATIVE_SCREEN_SIZE; + vec4 scene_color = textureLod(ShadedScene, texcoord, 0); + #endif + + result = vec4(scene_color.xyz, 1); +} diff --git a/RenderPipelineFile/rpcore/shader/view_frustum_cull.frag.glsl b/RenderPipelineFile/rpcore/shader/view_frustum_cull.frag.glsl index 64c90d56..8405141f 100644 --- a/RenderPipelineFile/rpcore/shader/view_frustum_cull.frag.glsl +++ b/RenderPipelineFile/rpcore/shader/view_frustum_cull.frag.glsl @@ -1,80 +1,80 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// This shader takes the list of all lights, and evaluates whether they are -// in the camera frustum. This improves the performance of the actual culling -// pass. - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/transforms.inc.glsl" -#pragma include "includes/light_culling.inc.glsl" -#pragma include "includes/light_data.inc.glsl" -#pragma include "includes/light_classification.inc.glsl" - -uniform writeonly uimageBuffer FrustumLights; -layout(r32i) uniform iimageBuffer FrustumLightsCount; - -uniform samplerBuffer AllLightsData; -uniform int maxLightIndex; - -void main() { - - const int tile_size = 16; - ivec2 coord = ivec2(gl_FragCoord.xy); - - float max_light_dist_sq = LC_MAX_DISTANCE * LC_MAX_DISTANCE; - - int start_offset = coord.y * tile_size + coord.x; - - // Iterate over all lights - for (int i = start_offset; i < maxLightIndex + 1; i += tile_size * tile_size) { - - // Fetch data of current light - LightData light_data = read_light_data(AllLightsData, i); - - // XXX: Might first read the type, then skip early - int light_type = get_light_type(light_data); - - // Skip Null-Lights - if (light_type < 1) continue; - - bool visible = true; - - Sphere sphere = get_representative_sphere(light_data); - - // XXX: Do actual culling here - - if (length_squared(sphere.pos) - square(sphere.radius) > max_light_dist_sq) - visible = false; - - if (visible) { - int index = imageAtomicAdd(FrustumLightsCount, 0, 1); - imageStore(FrustumLights, index, uvec4(i)); - } - } -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// This shader takes the list of all lights, and evaluates whether they are +// in the camera frustum. This improves the performance of the actual culling +// pass. + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/transforms.inc.glsl" +#pragma include "includes/light_culling.inc.glsl" +#pragma include "includes/light_data.inc.glsl" +#pragma include "includes/light_classification.inc.glsl" + +uniform writeonly uimageBuffer FrustumLights; +layout(r32i) uniform iimageBuffer FrustumLightsCount; + +uniform samplerBuffer AllLightsData; +uniform int maxLightIndex; + +void main() { + + const int tile_size = 16; + ivec2 coord = ivec2(gl_FragCoord.xy); + + float max_light_dist_sq = LC_MAX_DISTANCE * LC_MAX_DISTANCE; + + int start_offset = coord.y * tile_size + coord.x; + + // Iterate over all lights + for (int i = start_offset; i < maxLightIndex + 1; i += tile_size * tile_size) { + + // Fetch data of current light + LightData light_data = read_light_data(AllLightsData, i); + + // XXX: Might first read the type, then skip early + int light_type = get_light_type(light_data); + + // Skip Null-Lights + if (light_type < 1) continue; + + bool visible = true; + + Sphere sphere = get_representative_sphere(light_data); + + // XXX: Do actual culling here + + if (length_squared(sphere.pos) - square(sphere.radius) > max_light_dist_sq) + visible = false; + + if (visible) { + int index = imageAtomicAdd(FrustumLightsCount, 0, 1); + imageStore(FrustumLights, index, uvec4(i)); + } + } +} diff --git a/RenderPipelineFile/rpcore/shader/visualize_exposure.compute.glsl b/RenderPipelineFile/rpcore/shader/visualize_exposure.compute.glsl index 411837cd..47975408 100644 --- a/RenderPipelineFile/rpcore/shader/visualize_exposure.compute.glsl +++ b/RenderPipelineFile/rpcore/shader/visualize_exposure.compute.glsl @@ -1,75 +1,75 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" - -layout(local_size_x = 10, local_size_y = 4, local_size_z = 1) in; - -uniform writeonly image2D RESTRICT DestTex; -uniform samplerBuffer ExposureTex; - -void main() { - - // TODO: Might make this an input - const ivec2 widget_size = ivec2(140, 20); - ivec2 coord = ivec2(gl_GlobalInvocationID.xy); - - // Store the current pixels color - vec4 color = vec4(0, 0, 0, 0); - vec4 border_color = vec4(0.9, 0.9, 0.9, 1.0); - - // Border - const int border_size = 1; - if (coord.x < border_size || coord.x >= widget_size.x - border_size || - coord.y < border_size || coord.y >= widget_size.y - border_size) { - color += border_color * (1 - color.w); - } - - // Fetch exposure settings - float min_exp = GET_SETTING(color_correction, min_exposure_value); - float max_exp = GET_SETTING(color_correction, max_exposure_value); - - // Fetch current exposure - float curr_exposure = texelFetch(ExposureTex, 0).x; - - // Slider - float slider_pos = saturate((curr_exposure - min_exp) / (max_exp - min_exp)); - - // Make visualization logarithmic - // slider_pos = make_logarithmic(slider_pos, factor); - - const int slider_w = 4; - int slider_pos_int = int(slider_pos * float(widget_size.x - 2 * border_size)) + border_size; - - if (coord.x > slider_pos_int - slider_w && coord.x < slider_pos_int + slider_w) { - // Don't draw the slider over the border - color += vec4(50, 255, 50, 255.0) / 255.0 * (1 - color.w); - } - - imageStore(DestTex, coord, color); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" + +layout(local_size_x = 10, local_size_y = 4, local_size_z = 1) in; + +uniform writeonly image2D RESTRICT DestTex; +uniform samplerBuffer ExposureTex; + +void main() { + + // TODO: Might make this an input + const ivec2 widget_size = ivec2(140, 20); + ivec2 coord = ivec2(gl_GlobalInvocationID.xy); + + // Store the current pixels color + vec4 color = vec4(0, 0, 0, 0); + vec4 border_color = vec4(0.9, 0.9, 0.9, 1.0); + + // Border + const int border_size = 1; + if (coord.x < border_size || coord.x >= widget_size.x - border_size || + coord.y < border_size || coord.y >= widget_size.y - border_size) { + color += border_color * (1 - color.w); + } + + // Fetch exposure settings + float min_exp = GET_SETTING(color_correction, min_exposure_value); + float max_exp = GET_SETTING(color_correction, max_exposure_value); + + // Fetch current exposure + float curr_exposure = texelFetch(ExposureTex, 0).x; + + // Slider + float slider_pos = saturate((curr_exposure - min_exp) / (max_exp - min_exp)); + + // Make visualization logarithmic + // slider_pos = make_logarithmic(slider_pos, factor); + + const int slider_w = 4; + int slider_pos_int = int(slider_pos * float(widget_size.x - 2 * border_size)) + border_size; + + if (coord.x > slider_pos_int - slider_w && coord.x < slider_pos_int + slider_w) { + // Don't draw the slider over the border + color += vec4(50, 255, 50, 255.0) / 255.0 * (1 - color.w); + } + + imageStore(DestTex, coord, color); +} diff --git a/RenderPipelineFile/rpcore/stage_manager.py b/RenderPipelineFile/rpcore/stage_manager.py index 810d4819..4ca51185 100644 --- a/RenderPipelineFile/rpcore/stage_manager.py +++ b/RenderPipelineFile/rpcore/stage_manager.py @@ -1,286 +1,286 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rplibs.six import iteritems -from rplibs.yaml import load_yaml_file - -from direct.stdpy.file import open - -from rpcore.rpobject import RPObject -from rpcore.gui.pipe_viewer import PipeViewer -from rpcore.image import Image -from rpcore.util.shader_input_blocks import SimpleInputBlock, GroupedInputBlock -from rpcore.stages.update_previous_pipes_stage import UpdatePreviousPipesStage - - -class StageManager(RPObject): - - """ This manager takes a list of RenderStages and puts them into an order, - while connecting the different pipes, inputs, ubos and defines. """ - - def __init__(self, pipeline): - """ Constructs the stage manager """ - RPObject.__init__(self) - self.stages = [] - self.inputs = {} - self.pipes = {} - self.input_blocks = [] - self.previous_pipes = {} - self.future_bindings = [] - self.defines = {} - self.pipeline = pipeline - self.created = False - - self._load_stage_order() - - # Register the manager so the pipe viewer can read our data - PipeViewer.register_stage_mgr(self) - - def _load_stage_order(self): - """ Loads the order of all stages from the stages.yaml configuration - file """ - orders = load_yaml_file("/$$rpconfig/stages.yaml") - if "global_stage_order" not in orders: - self.error("Could not load stage order, root key does not exist!") - return - self._stage_order = orders["global_stage_order"] - - def add_stage(self, stage): - """ Adds a new stage """ - if stage.stage_id not in self._stage_order: - self.error("The stage type", stage.debug_name, - "is not registered yet! Please add it to the StageManager!") - return - - if self.created: - self.error("Cannot attach stage, stages are already created!") - return - - self.stages.append(stage) - - def get_stage(self, stage_class): - """ Returns a handle to an instantiated stage """ - for stage in self.stages: - if stage.__class__.__name__ == stage_class: - return stage - - def _prepare_stages(self): - """ Prepares all stages by removing disabled stages and sorting stages - by order """ - self.debug("Preparing stages ..") - - # Remove all disabled stages - to_remove = [] - for stage in self.stages: - if stage.disabled: - to_remove.append(stage) - - for stage in to_remove: - self.stages.remove(stage) - - self.stages.sort(key=lambda stage: self._stage_order.index(stage.stage_id)) - - def _bind_pipes_to_stage(self, stage): - """ Sets all required pipes on a stage """ - for pipe in stage.required_pipes: - - # Check if there is an input block named like the pipe - if pipe in self.input_blocks: - self.input_blocks[pipe].bind_to(stage) - continue - - if pipe.startswith("PreviousFrame::"): - # Special case: Pipes from the previous frame. We assume those - # pipes have the same size as the window and a format of - # F_rgba16. Could be subject to change. - pipe_name = pipe.split("::")[-1] - if pipe_name not in self.previous_pipes: - tex_format = "RGBA16" - - # XXX: Assuming we have a depth texture whenever "depth" - # occurs in the textures name - if "depth" in pipe_name.lower(): - tex_format = "R32" - - pipe_tex = Image.create_2d("Prev-" + pipe_name, 0, 0, tex_format) - pipe_tex.clear_image() - self.previous_pipes[pipe_name] = pipe_tex - stage.set_shader_input("Previous_" + pipe_name, self.previous_pipes[pipe_name]) - continue - - elif pipe.startswith("FuturePipe::"): - # Special case: Future Pipes which are not available yet. - # They will contain the unmodified data from the last - # frame. - pipe_name = pipe.split("::")[-1] - self.debug("Awaiting future pipe", pipe_name) - self.future_bindings.append((pipe_name, stage)) - continue - - if pipe not in self.pipes: - self.fatal("Pipe '" + pipe + "' is missing for", stage) - return False - - pipe_value = self.pipes[pipe] - if isinstance(pipe_value, list) or isinstance(pipe_value, tuple): - stage.set_shader_input(pipe, *pipe_value) - else: - stage.set_shader_input(pipe, pipe_value) - return True - - def _bind_inputs_to_stage(self, stage): - """ Binds all inputs including common inputs to the given stage """ - common_inputs = ["mainCam", "mainRender", "MainSceneData", "TimeOfDay"] - for input_binding in stage.required_inputs + common_inputs: - if input_binding not in self.inputs and \ - input_binding not in self.input_blocks: - self.error("Input", input_binding, "is missing for", stage) - continue - - if input_binding in self.inputs: - stage.set_shader_input(input_binding, self.inputs[input_binding]) - elif input_binding in self.input_blocks: - self.input_blocks[input_binding].bind_to(stage) - else: - assert False, "Input binding not in inputs and not in blocks!" - return True - - def _register_stage_result(self, stage): - """ Registers all produced pipes, inputs and defines from the given - stage, so they can be used by later stages. """ - for pipe_name, pipe_data in (iteritems)(stage.produced_pipes): - if isinstance(pipe_data, (SimpleInputBlock, GroupedInputBlock)): - self.input_blocks[pipe_name] = pipe_data - continue - self.pipes[pipe_name] = pipe_data - - for define_name, data in iteritems(stage.produced_defines): - if define_name in self.defines: - self.warn("Stage", stage, "overrides define", define_name) - self.defines[define_name] = data - - for input_name, data in iteritems(stage.produced_inputs): - if input_name in self.inputs: - self.warn("Stage", stage, "overrides input", input_name) - - if isinstance(data, (SimpleInputBlock, GroupedInputBlock)): - self.input_blocks[input_name] = data - continue - - self.inputs[input_name] = data - - def _create_previous_pipes(self): - """ Creates a target for each last-frame's pipe, any pipe starting - with the prefix 'Previous::' has to be stored and copied each frame. """ - if self.previous_pipes: - self._prev_stage = UpdatePreviousPipesStage(self.pipeline) - for prev_pipe, prev_tex in iteritems(self.previous_pipes): - - if prev_pipe not in self.pipes: - self.error("Attempted to use previous frame data from pipe", - prev_pipe, "- however, that pipe was never created!") - return False - - # Tell the stage to transfer the data from the current pipe to - # the current texture - self._prev_stage.add_transfer(self.pipes[prev_pipe], prev_tex) - self._prev_stage.create() - self._prev_stage.set_dimensions() - self.stages.append(self._prev_stage) - - def _apply_future_bindings(self): - """ Applies all future bindings. At this point all pipes and - inputs should be present """ - for pipe, stage in self.future_bindings: - if pipe not in self.pipes: - self.error("Could not bind future pipe:", pipe, "not present!") - continue - stage.set_shader_input(pipe, self.pipes[pipe]) - self.future_bindings = [] - - def setup(self): - """ Setups the stages """ - self.debug("Setup stages ..") - self.created = True - - # Convert input blocks so we can access them in a better way - self.input_blocks = {block.name: block for block in self.input_blocks} - self._prepare_stages() - - for stage in self.stages: - stage.create() - stage.handle_window_resize() - - # Rely on the methods to print an appropriate error message - if not self._bind_pipes_to_stage(stage): - continue - if not self._bind_inputs_to_stage(stage): - continue - - self._register_stage_result(stage) - self._create_previous_pipes() - self._apply_future_bindings() - - def reload_shaders(self): - """ This pass sets the shaders to all passes and also generates the - shader configuration """ - self.write_autoconfig() - for stage in self.stages: - stage.reload_shaders() - - def update(self): - """ Calls the update method for each registered stage. Inactive stages - are skipped. """ - for stage in self.stages: - if stage.active: - stage.update() - - def handle_window_resize(self): - """ Method to get called when the window got resized. Propagates the - resize event to all registered stages """ - for stage in self.stages: - stage.handle_window_resize() - - def write_autoconfig(self): - """ Writes the shader auto config, based on the defines specified by the - different stages """ - self.debug("Writing shader config") - - # Generate autoconfig as string - output = "#pragma once\n\n" - output += "// Autogenerated by the render pipeline\n" - output += "// Do not edit! Your changes will be lost.\n\n" - - for key, value in sorted(iteritems(self.defines)): - if isinstance(value, bool): - value = 1 if value else 0 - output += "#define " + key + " " + str(value) + "\n" - - try: - with open("/$$rptemp/$$pipeline_shader_config.inc.glsl", "w") as handle: - handle.write(output) - except IOError as msg: - self.error("Error writing shader autoconfig:", msg) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rplibs.six import iteritems +from rplibs.yaml import load_yaml_file + +from direct.stdpy.file import open + +from rpcore.rpobject import RPObject +from rpcore.gui.pipe_viewer import PipeViewer +from rpcore.image import Image +from rpcore.util.shader_input_blocks import SimpleInputBlock, GroupedInputBlock +from rpcore.stages.update_previous_pipes_stage import UpdatePreviousPipesStage + + +class StageManager(RPObject): + + """ This manager takes a list of RenderStages and puts them into an order, + while connecting the different pipes, inputs, ubos and defines. """ + + def __init__(self, pipeline): + """ Constructs the stage manager """ + RPObject.__init__(self) + self.stages = [] + self.inputs = {} + self.pipes = {} + self.input_blocks = [] + self.previous_pipes = {} + self.future_bindings = [] + self.defines = {} + self.pipeline = pipeline + self.created = False + + self._load_stage_order() + + # Register the manager so the pipe viewer can read our data + PipeViewer.register_stage_mgr(self) + + def _load_stage_order(self): + """ Loads the order of all stages from the stages.yaml configuration + file """ + orders = load_yaml_file("/$$rpconfig/stages.yaml") + if "global_stage_order" not in orders: + self.error("Could not load stage order, root key does not exist!") + return + self._stage_order = orders["global_stage_order"] + + def add_stage(self, stage): + """ Adds a new stage """ + if stage.stage_id not in self._stage_order: + self.error("The stage type", stage.debug_name, + "is not registered yet! Please add it to the StageManager!") + return + + if self.created: + self.error("Cannot attach stage, stages are already created!") + return + + self.stages.append(stage) + + def get_stage(self, stage_class): + """ Returns a handle to an instantiated stage """ + for stage in self.stages: + if stage.__class__.__name__ == stage_class: + return stage + + def _prepare_stages(self): + """ Prepares all stages by removing disabled stages and sorting stages + by order """ + self.debug("Preparing stages ..") + + # Remove all disabled stages + to_remove = [] + for stage in self.stages: + if stage.disabled: + to_remove.append(stage) + + for stage in to_remove: + self.stages.remove(stage) + + self.stages.sort(key=lambda stage: self._stage_order.index(stage.stage_id)) + + def _bind_pipes_to_stage(self, stage): + """ Sets all required pipes on a stage """ + for pipe in stage.required_pipes: + + # Check if there is an input block named like the pipe + if pipe in self.input_blocks: + self.input_blocks[pipe].bind_to(stage) + continue + + if pipe.startswith("PreviousFrame::"): + # Special case: Pipes from the previous frame. We assume those + # pipes have the same size as the window and a format of + # F_rgba16. Could be subject to change. + pipe_name = pipe.split("::")[-1] + if pipe_name not in self.previous_pipes: + tex_format = "RGBA16" + + # XXX: Assuming we have a depth texture whenever "depth" + # occurs in the textures name + if "depth" in pipe_name.lower(): + tex_format = "R32" + + pipe_tex = Image.create_2d("Prev-" + pipe_name, 0, 0, tex_format) + pipe_tex.clear_image() + self.previous_pipes[pipe_name] = pipe_tex + stage.set_shader_input("Previous_" + pipe_name, self.previous_pipes[pipe_name]) + continue + + elif pipe.startswith("FuturePipe::"): + # Special case: Future Pipes which are not available yet. + # They will contain the unmodified data from the last + # frame. + pipe_name = pipe.split("::")[-1] + self.debug("Awaiting future pipe", pipe_name) + self.future_bindings.append((pipe_name, stage)) + continue + + if pipe not in self.pipes: + self.fatal("Pipe '" + pipe + "' is missing for", stage) + return False + + pipe_value = self.pipes[pipe] + if isinstance(pipe_value, list) or isinstance(pipe_value, tuple): + stage.set_shader_input(pipe, *pipe_value) + else: + stage.set_shader_input(pipe, pipe_value) + return True + + def _bind_inputs_to_stage(self, stage): + """ Binds all inputs including common inputs to the given stage """ + common_inputs = ["mainCam", "mainRender", "MainSceneData", "TimeOfDay"] + for input_binding in stage.required_inputs + common_inputs: + if input_binding not in self.inputs and \ + input_binding not in self.input_blocks: + self.error("Input", input_binding, "is missing for", stage) + continue + + if input_binding in self.inputs: + stage.set_shader_input(input_binding, self.inputs[input_binding]) + elif input_binding in self.input_blocks: + self.input_blocks[input_binding].bind_to(stage) + else: + assert False, "Input binding not in inputs and not in blocks!" + return True + + def _register_stage_result(self, stage): + """ Registers all produced pipes, inputs and defines from the given + stage, so they can be used by later stages. """ + for pipe_name, pipe_data in (iteritems)(stage.produced_pipes): + if isinstance(pipe_data, (SimpleInputBlock, GroupedInputBlock)): + self.input_blocks[pipe_name] = pipe_data + continue + self.pipes[pipe_name] = pipe_data + + for define_name, data in iteritems(stage.produced_defines): + if define_name in self.defines: + self.warn("Stage", stage, "overrides define", define_name) + self.defines[define_name] = data + + for input_name, data in iteritems(stage.produced_inputs): + if input_name in self.inputs: + self.warn("Stage", stage, "overrides input", input_name) + + if isinstance(data, (SimpleInputBlock, GroupedInputBlock)): + self.input_blocks[input_name] = data + continue + + self.inputs[input_name] = data + + def _create_previous_pipes(self): + """ Creates a target for each last-frame's pipe, any pipe starting + with the prefix 'Previous::' has to be stored and copied each frame. """ + if self.previous_pipes: + self._prev_stage = UpdatePreviousPipesStage(self.pipeline) + for prev_pipe, prev_tex in iteritems(self.previous_pipes): + + if prev_pipe not in self.pipes: + self.error("Attempted to use previous frame data from pipe", + prev_pipe, "- however, that pipe was never created!") + return False + + # Tell the stage to transfer the data from the current pipe to + # the current texture + self._prev_stage.add_transfer(self.pipes[prev_pipe], prev_tex) + self._prev_stage.create() + self._prev_stage.set_dimensions() + self.stages.append(self._prev_stage) + + def _apply_future_bindings(self): + """ Applies all future bindings. At this point all pipes and + inputs should be present """ + for pipe, stage in self.future_bindings: + if pipe not in self.pipes: + self.error("Could not bind future pipe:", pipe, "not present!") + continue + stage.set_shader_input(pipe, self.pipes[pipe]) + self.future_bindings = [] + + def setup(self): + """ Setups the stages """ + self.debug("Setup stages ..") + self.created = True + + # Convert input blocks so we can access them in a better way + self.input_blocks = {block.name: block for block in self.input_blocks} + self._prepare_stages() + + for stage in self.stages: + stage.create() + stage.handle_window_resize() + + # Rely on the methods to print an appropriate error message + if not self._bind_pipes_to_stage(stage): + continue + if not self._bind_inputs_to_stage(stage): + continue + + self._register_stage_result(stage) + self._create_previous_pipes() + self._apply_future_bindings() + + def reload_shaders(self): + """ This pass sets the shaders to all passes and also generates the + shader configuration """ + self.write_autoconfig() + for stage in self.stages: + stage.reload_shaders() + + def update(self): + """ Calls the update method for each registered stage. Inactive stages + are skipped. """ + for stage in self.stages: + if stage.active: + stage.update() + + def handle_window_resize(self): + """ Method to get called when the window got resized. Propagates the + resize event to all registered stages """ + for stage in self.stages: + stage.handle_window_resize() + + def write_autoconfig(self): + """ Writes the shader auto config, based on the defines specified by the + different stages """ + self.debug("Writing shader config") + + # Generate autoconfig as string + output = "#pragma once\n\n" + output += "// Autogenerated by the render pipeline\n" + output += "// Do not edit! Your changes will be lost.\n\n" + + for key, value in sorted(iteritems(self.defines)): + if isinstance(value, bool): + value = 1 if value else 0 + output += "#define " + key + " " + str(value) + "\n" + + try: + with open("/$$rptemp/$$pipeline_shader_config.inc.glsl", "w") as handle: + handle.write(output) + except IOError as msg: + self.error("Error writing shader autoconfig:", msg) diff --git a/RenderPipelineFile/rpcore/stages/ambient_stage.py b/RenderPipelineFile/rpcore/stages/ambient_stage.py index 60772e27..83fc7785 100644 --- a/RenderPipelineFile/rpcore/stages/ambient_stage.py +++ b/RenderPipelineFile/rpcore/stages/ambient_stage.py @@ -1,49 +1,49 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.render_stage import RenderStage - - -class AmbientStage(RenderStage): - - """ This stage computes the ambient term """ - - required_inputs = ["DefaultEnvmap", "PrefilteredBRDF", "PrefilteredMetalBRDF", - "PrefilteredCoatBRDF"] - required_pipes = ["ShadedScene", "GBuffer"] - - @property - def produced_pipes(self): - return {"ShadedScene": self._target.color_tex, - "PostAmbientScene": self._target.color_tex} - - def create(self): - self._target = self.create_target("AmbientStage") - self._target.add_color_attachment(bits=16) - self._target.prepare_buffer() - - def reload_shaders(self): - self._target.shader = self.load_shader("ambient_stage.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.render_stage import RenderStage + + +class AmbientStage(RenderStage): + + """ This stage computes the ambient term """ + + required_inputs = ["DefaultEnvmap", "PrefilteredBRDF", "PrefilteredMetalBRDF", + "PrefilteredCoatBRDF"] + required_pipes = ["ShadedScene", "GBuffer"] + + @property + def produced_pipes(self): + return {"ShadedScene": self._target.color_tex, + "PostAmbientScene": self._target.color_tex} + + def create(self): + self._target = self.create_target("AmbientStage") + self._target.add_color_attachment(bits=16) + self._target.prepare_buffer() + + def reload_shaders(self): + self._target.shader = self.load_shader("ambient_stage.frag.glsl") diff --git a/RenderPipelineFile/rpcore/stages/apply_lights_stage.py b/RenderPipelineFile/rpcore/stages/apply_lights_stage.py index 6d530f4a..572ccb19 100644 --- a/RenderPipelineFile/rpcore/stages/apply_lights_stage.py +++ b/RenderPipelineFile/rpcore/stages/apply_lights_stage.py @@ -1,48 +1,48 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.render_stage import RenderStage - - -class ApplyLightsStage(RenderStage): - - """ This stage applies the lights to the scene using the gbuffer """ - - required_inputs = ["AllLightsData", "IESDatasetTex", "ShadowSourceData"] - required_pipes = ["GBuffer", "CellIndices", "PerCellLights", "ShadowAtlas", - "ShadowAtlasPCF", "CombinedVelocity", "PerCellLightsCounts"] - - @property - def produced_pipes(self): - return {"ShadedScene": self.target.color_tex} - - def create(self): - self.target = self.create_target("ApplyLights") - self.target.add_color_attachment(bits=16) - self.target.prepare_buffer() - - def reload_shaders(self): - self.target.shader = self.load_shader("apply_lights.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.render_stage import RenderStage + + +class ApplyLightsStage(RenderStage): + + """ This stage applies the lights to the scene using the gbuffer """ + + required_inputs = ["AllLightsData", "IESDatasetTex", "ShadowSourceData"] + required_pipes = ["GBuffer", "CellIndices", "PerCellLights", "ShadowAtlas", + "ShadowAtlasPCF", "CombinedVelocity", "PerCellLightsCounts"] + + @property + def produced_pipes(self): + return {"ShadedScene": self.target.color_tex} + + def create(self): + self.target = self.create_target("ApplyLights") + self.target.add_color_attachment(bits=16) + self.target.prepare_buffer() + + def reload_shaders(self): + self.target.shader = self.load_shader("apply_lights.frag.glsl") diff --git a/RenderPipelineFile/rpcore/stages/collect_used_cells_stage.py b/RenderPipelineFile/rpcore/stages/collect_used_cells_stage.py index 1b7956c7..587d255b 100644 --- a/RenderPipelineFile/rpcore/stages/collect_used_cells_stage.py +++ b/RenderPipelineFile/rpcore/stages/collect_used_cells_stage.py @@ -1,77 +1,77 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.image import Image -from rpcore.render_stage import RenderStage - - -class CollectUsedCellsStage(RenderStage): - - """ This stage collects the flagged cells from the FlagUsedCellsStage and - makes a list of them """ - - required_pipes = ["FlaggedCells"] - - @property - def produced_pipes(self): - return { - "CellListBuffer": self.cell_list_buffer, - "CellIndices": self.cell_index_buffer, - } - - def create(self): - self.target = self.create_target("CollectUsedCells") - self.target.size = 0, 0 - self.target.prepare_buffer() - - self.cell_list_buffer = Image.create_buffer("CellList", 0, "R32I") - self.cell_index_buffer = Image.create_2d_array("CellIndices", 0, 0, 0, "R32I") - - self.target.set_shader_inputs( - CellListBuffer=self.cell_list_buffer, - CellListIndices=self.cell_index_buffer) - - def update(self): - self.cell_list_buffer.clear_image() - self.cell_index_buffer.clear_image() - - def set_dimensions(self): - tile_amount = self._pipeline.light_mgr.num_tiles - num_slices = self._pipeline.settings["lighting.culling_grid_slices"] - max_cells = tile_amount.x * tile_amount.y * num_slices - - self.cell_list_buffer.set_x_size(1 + max_cells) - self.cell_index_buffer.set_x_size(tile_amount.x) - self.cell_index_buffer.set_y_size(tile_amount.y) - self.cell_index_buffer.set_z_size(num_slices) - - self.cell_list_buffer.clear_image() - self.cell_index_buffer.clear_image() - - self.target.size = tile_amount.x, tile_amount.y - - def reload_shaders(self): - self.target.shader = self.load_shader("collect_used_cells.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.image import Image +from rpcore.render_stage import RenderStage + + +class CollectUsedCellsStage(RenderStage): + + """ This stage collects the flagged cells from the FlagUsedCellsStage and + makes a list of them """ + + required_pipes = ["FlaggedCells"] + + @property + def produced_pipes(self): + return { + "CellListBuffer": self.cell_list_buffer, + "CellIndices": self.cell_index_buffer, + } + + def create(self): + self.target = self.create_target("CollectUsedCells") + self.target.size = 0, 0 + self.target.prepare_buffer() + + self.cell_list_buffer = Image.create_buffer("CellList", 0, "R32I") + self.cell_index_buffer = Image.create_2d_array("CellIndices", 0, 0, 0, "R32I") + + self.target.set_shader_inputs( + CellListBuffer=self.cell_list_buffer, + CellListIndices=self.cell_index_buffer) + + def update(self): + self.cell_list_buffer.clear_image() + self.cell_index_buffer.clear_image() + + def set_dimensions(self): + tile_amount = self._pipeline.light_mgr.num_tiles + num_slices = self._pipeline.settings["lighting.culling_grid_slices"] + max_cells = tile_amount.x * tile_amount.y * num_slices + + self.cell_list_buffer.set_x_size(1 + max_cells) + self.cell_index_buffer.set_x_size(tile_amount.x) + self.cell_index_buffer.set_y_size(tile_amount.y) + self.cell_index_buffer.set_z_size(num_slices) + + self.cell_list_buffer.clear_image() + self.cell_index_buffer.clear_image() + + self.target.size = tile_amount.x, tile_amount.y + + def reload_shaders(self): + self.target.shader = self.load_shader("collect_used_cells.frag.glsl") diff --git a/RenderPipelineFile/rpcore/stages/combine_velocity_stage.py b/RenderPipelineFile/rpcore/stages/combine_velocity_stage.py index 73df2c6a..8d0b4aaa 100644 --- a/RenderPipelineFile/rpcore/stages/combine_velocity_stage.py +++ b/RenderPipelineFile/rpcore/stages/combine_velocity_stage.py @@ -1,47 +1,47 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.render_stage import RenderStage - - -class CombineVelocityStage(RenderStage): - - """ This stage combines the per-object velocity with the - camera velocity """ - - required_pipes = ["GBuffer"] - - @property - def produced_pipes(self): - return {"CombinedVelocity": self._target.color_tex} - - def create(self): - self._target = self.create_target("CombineVelocity") - self._target.add_color_attachment(bits=(16, 16, 0, 0)) - self._target.prepare_buffer() - - def reload_shaders(self): - self._target.shader = self.load_shader("combine_velocity.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.render_stage import RenderStage + + +class CombineVelocityStage(RenderStage): + + """ This stage combines the per-object velocity with the + camera velocity """ + + required_pipes = ["GBuffer"] + + @property + def produced_pipes(self): + return {"CombinedVelocity": self._target.color_tex} + + def create(self): + self._target = self.create_target("CombineVelocity") + self._target.add_color_attachment(bits=(16, 16, 0, 0)) + self._target.prepare_buffer() + + def reload_shaders(self): + self._target.shader = self.load_shader("combine_velocity.frag.glsl") diff --git a/RenderPipelineFile/rpcore/stages/cull_lights_stage.py b/RenderPipelineFile/rpcore/stages/cull_lights_stage.py index 8c301bd6..98f0fb9c 100644 --- a/RenderPipelineFile/rpcore/stages/cull_lights_stage.py +++ b/RenderPipelineFile/rpcore/stages/cull_lights_stage.py @@ -1,137 +1,137 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -import math - -from rpcore.render_stage import RenderStage -from rpcore.image import Image - - -class CullLightsStage(RenderStage): - - """ This stage takes the list of used cells and creates a list of lights - for each cell """ - - required_pipes = ["CellListBuffer"] - required_inputs = ["AllLightsData", "maxLightIndex"] - - def __init__(self, pipeline): - RenderStage.__init__(self, pipeline) - self.max_lights_per_cell = pipeline.settings["lighting.max_lights_per_cell"] - - if self.max_lights_per_cell > 2**16: - self.fatal("lighting.max_lights_per_cell must be <=", 2**16, "!") - - self.slice_width = pipeline.settings["lighting.culling_slice_width"] - self.cull_threads = 32 - - # Amount of light classes. Has to match the ones in LightClassification.inc.glsl - self.num_light_classes = 4 - - @property - def produced_pipes(self): - return { - "PerCellLights": self.grouped_cell_lights, - "PerCellLightsCounts": self.grouped_cell_lights_counts - } - - @property - def produced_defines(self): - return { - "LC_SLICE_WIDTH": self.slice_width, - "LC_CULL_THREADS": self.cull_threads, - "LC_LIGHT_CLASS_COUNT": self.num_light_classes - } - - def create(self): - self.target_visible = self.create_target("GetVisibleLights") - self.target_visible.size = 16, 16 - self.target_visible.prepare_buffer() - - # TODO: Use no oversized triangle in this stage - self.target_cull = self.create_target("CullLights") - self.target_cull.size = 0, 0 - self.target_cull.prepare_buffer() - - # TODO: Use no oversized triangle in this stage - self.target_group = self.create_target("GroupLightsByClass") - self.target_group.size = 0, 0 - self.target_group.prepare_buffer() - - self.frustum_lights_ctr = Image.create_counter("VisibleLightCount") - self.frustum_lights = Image.create_buffer( - "FrustumLights", self._pipeline.light_mgr.MAX_LIGHTS, "R16UI") - self.per_cell_lights = Image.create_buffer( - "PerCellLights", 0, "R16UI") - self.per_cell_light_counts = Image.create_buffer( - "PerCellLightCounts", 0, "R32I") # Needs to be R32 for atomic add in cull stage - self.grouped_cell_lights = Image.create_buffer( - "GroupedPerCellLights", 0, "R16UI") - self.grouped_cell_lights_counts = Image.create_buffer( - "GroupedPerCellLightsCount", 0, "R16UI") - - self.target_visible.set_shader_inputs( - FrustumLights=self.frustum_lights, - FrustumLightsCount=self.frustum_lights_ctr) - - self.target_cull.set_shader_inputs( - PerCellLightsBuffer=self.per_cell_lights, - PerCellLightCountsBuffer=self.per_cell_light_counts, - FrustumLights=self.frustum_lights, - FrustumLightsCount=self.frustum_lights_ctr, - threadCount=self.cull_threads) - - self.target_group.set_shader_inputs( - PerCellLightsBuffer=self.per_cell_lights, - PerCellLightCountsBuffer=self.per_cell_light_counts, - GroupedCellLightsBuffer=self.grouped_cell_lights, - GroupedPerCellLightsCountBuffer=self.grouped_cell_lights_counts, - threadCount=1) - - def reload_shaders(self): - self.target_cull.shader = self.load_shader( - "tiled_culling.vert.glsl", "cull_lights.frag.glsl") - self.target_group.shader = self.load_shader( - "tiled_culling.vert.glsl", "group_lights.frag.glsl") - self.target_visible.shader = self.load_shader( - "view_frustum_cull.frag.glsl") - - def update(self): - self.frustum_lights_ctr.clear_image() - - def set_dimensions(self): - max_cells = self._pipeline.light_mgr.total_tiles - num_rows_threaded = int( - math.ceil((max_cells * self.cull_threads) / float(self.slice_width))) - num_rows = int(math.ceil(max_cells / float(self.slice_width))) - self.per_cell_lights.set_x_size(max_cells * self.max_lights_per_cell) - self.per_cell_light_counts.set_x_size(max_cells) - self.grouped_cell_lights.set_x_size( - max_cells * (self.max_lights_per_cell + self.num_light_classes)) - self.target_cull.size = self.slice_width, num_rows_threaded - self.target_group.size = self.slice_width, num_rows - - self.grouped_cell_lights_counts.set_x_size(max_cells * (1 + self.num_light_classes)) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +import math + +from rpcore.render_stage import RenderStage +from rpcore.image import Image + + +class CullLightsStage(RenderStage): + + """ This stage takes the list of used cells and creates a list of lights + for each cell """ + + required_pipes = ["CellListBuffer"] + required_inputs = ["AllLightsData", "maxLightIndex"] + + def __init__(self, pipeline): + RenderStage.__init__(self, pipeline) + self.max_lights_per_cell = pipeline.settings["lighting.max_lights_per_cell"] + + if self.max_lights_per_cell > 2**16: + self.fatal("lighting.max_lights_per_cell must be <=", 2**16, "!") + + self.slice_width = pipeline.settings["lighting.culling_slice_width"] + self.cull_threads = 32 + + # Amount of light classes. Has to match the ones in LightClassification.inc.glsl + self.num_light_classes = 4 + + @property + def produced_pipes(self): + return { + "PerCellLights": self.grouped_cell_lights, + "PerCellLightsCounts": self.grouped_cell_lights_counts + } + + @property + def produced_defines(self): + return { + "LC_SLICE_WIDTH": self.slice_width, + "LC_CULL_THREADS": self.cull_threads, + "LC_LIGHT_CLASS_COUNT": self.num_light_classes + } + + def create(self): + self.target_visible = self.create_target("GetVisibleLights") + self.target_visible.size = 16, 16 + self.target_visible.prepare_buffer() + + # TODO: Use no oversized triangle in this stage + self.target_cull = self.create_target("CullLights") + self.target_cull.size = 0, 0 + self.target_cull.prepare_buffer() + + # TODO: Use no oversized triangle in this stage + self.target_group = self.create_target("GroupLightsByClass") + self.target_group.size = 0, 0 + self.target_group.prepare_buffer() + + self.frustum_lights_ctr = Image.create_counter("VisibleLightCount") + self.frustum_lights = Image.create_buffer( + "FrustumLights", self._pipeline.light_mgr.MAX_LIGHTS, "R16UI") + self.per_cell_lights = Image.create_buffer( + "PerCellLights", 0, "R16UI") + self.per_cell_light_counts = Image.create_buffer( + "PerCellLightCounts", 0, "R32I") # Needs to be R32 for atomic add in cull stage + self.grouped_cell_lights = Image.create_buffer( + "GroupedPerCellLights", 0, "R16UI") + self.grouped_cell_lights_counts = Image.create_buffer( + "GroupedPerCellLightsCount", 0, "R16UI") + + self.target_visible.set_shader_inputs( + FrustumLights=self.frustum_lights, + FrustumLightsCount=self.frustum_lights_ctr) + + self.target_cull.set_shader_inputs( + PerCellLightsBuffer=self.per_cell_lights, + PerCellLightCountsBuffer=self.per_cell_light_counts, + FrustumLights=self.frustum_lights, + FrustumLightsCount=self.frustum_lights_ctr, + threadCount=self.cull_threads) + + self.target_group.set_shader_inputs( + PerCellLightsBuffer=self.per_cell_lights, + PerCellLightCountsBuffer=self.per_cell_light_counts, + GroupedCellLightsBuffer=self.grouped_cell_lights, + GroupedPerCellLightsCountBuffer=self.grouped_cell_lights_counts, + threadCount=1) + + def reload_shaders(self): + self.target_cull.shader = self.load_shader( + "tiled_culling.vert.glsl", "cull_lights.frag.glsl") + self.target_group.shader = self.load_shader( + "tiled_culling.vert.glsl", "group_lights.frag.glsl") + self.target_visible.shader = self.load_shader( + "view_frustum_cull.frag.glsl") + + def update(self): + self.frustum_lights_ctr.clear_image() + + def set_dimensions(self): + max_cells = self._pipeline.light_mgr.total_tiles + num_rows_threaded = int( + math.ceil((max_cells * self.cull_threads) / float(self.slice_width))) + num_rows = int(math.ceil(max_cells / float(self.slice_width))) + self.per_cell_lights.set_x_size(max_cells * self.max_lights_per_cell) + self.per_cell_light_counts.set_x_size(max_cells) + self.grouped_cell_lights.set_x_size( + max_cells * (self.max_lights_per_cell + self.num_light_classes)) + self.target_cull.size = self.slice_width, num_rows_threaded + self.target_group.size = self.slice_width, num_rows + + self.grouped_cell_lights_counts.set_x_size(max_cells * (1 + self.num_light_classes)) diff --git a/RenderPipelineFile/rpcore/stages/downscale_z_stage.py b/RenderPipelineFile/rpcore/stages/downscale_z_stage.py index 88e61e97..94be21f1 100644 --- a/RenderPipelineFile/rpcore/stages/downscale_z_stage.py +++ b/RenderPipelineFile/rpcore/stages/downscale_z_stage.py @@ -1,48 +1,48 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import division - -from rpcore.render_stage import RenderStage - - -class DownscaleZStage(RenderStage): - - """ This stage downscales the depth buffer """ - - required_pipes = ["GBuffer"] - - @property - def produced_pipes(self): - return {"DownscaledDepth": self.target.color_tex} - - def create(self): - self.target = self.create_target("DownscaleDepth") - self.target.add_color_attachment(bits=(16, 0, 0, 0)) - self.target.prepare_buffer() - - def reload_shaders(self): - self.target.shader = self.load_shader("downscale_depth.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import division + +from rpcore.render_stage import RenderStage + + +class DownscaleZStage(RenderStage): + + """ This stage downscales the depth buffer """ + + required_pipes = ["GBuffer"] + + @property + def produced_pipes(self): + return {"DownscaledDepth": self.target.color_tex} + + def create(self): + self.target = self.create_target("DownscaleDepth") + self.target.add_color_attachment(bits=(16, 0, 0, 0)) + self.target.prepare_buffer() + + def reload_shaders(self): + self.target.shader = self.load_shader("downscale_depth.frag.glsl") diff --git a/RenderPipelineFile/rpcore/stages/final_stage.py b/RenderPipelineFile/rpcore/stages/final_stage.py index ad6e9612..bed61233 100644 --- a/RenderPipelineFile/rpcore/stages/final_stage.py +++ b/RenderPipelineFile/rpcore/stages/final_stage.py @@ -1,62 +1,62 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.render_stage import RenderStage - - -class FinalStage(RenderStage): - - """ This stage is the final stage and outputs the shaded scene to the - screen """ - - required_pipes = ["ShadedScene"] - - @property - def produced_pipes(self): - return {"ShadedScene": self.target.color_tex} - - def create(self): - - self.target = self.create_target("FinalStage") - self.target.add_color_attachment(bits=16) - self.target.prepare_buffer() - - # XXX: We cannot simply assign the final shader to the window display - # region. This is because of a bug that the last FBO always only gets - # 8 bits, regardles of what was requested - probably because of the - # assumption that no tonemapping/srgb correction will follow afterwards. - # - # This might be a driver bug, or an optimization in Panda3D. However, it - # also has the nice side effect that when taking screenshots (or using - # the pixel inspector), we get the srgb corrected data, so its not too - # much of a disadvantage. - self.present_target = self.create_target("FinalPresentStage") - self.present_target.present_on_screen() - self.present_target.set_shader_input("SourceTex", self.target.color_tex) - - def reload_shaders(self): - self.target.shader = self.load_shader("final_stage.frag.glsl") - self.present_target.shader = self.load_shader("final_present_stage.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.render_stage import RenderStage + + +class FinalStage(RenderStage): + + """ This stage is the final stage and outputs the shaded scene to the + screen """ + + required_pipes = ["ShadedScene"] + + @property + def produced_pipes(self): + return {"ShadedScene": self.target.color_tex} + + def create(self): + + self.target = self.create_target("FinalStage") + self.target.add_color_attachment(bits=16) + self.target.prepare_buffer() + + # XXX: We cannot simply assign the final shader to the window display + # region. This is because of a bug that the last FBO always only gets + # 8 bits, regardles of what was requested - probably because of the + # assumption that no tonemapping/srgb correction will follow afterwards. + # + # This might be a driver bug, or an optimization in Panda3D. However, it + # also has the nice side effect that when taking screenshots (or using + # the pixel inspector), we get the srgb corrected data, so its not too + # much of a disadvantage. + self.present_target = self.create_target("FinalPresentStage") + self.present_target.present_on_screen() + self.present_target.set_shader_input("SourceTex", self.target.color_tex) + + def reload_shaders(self): + self.target.shader = self.load_shader("final_stage.frag.glsl") + self.present_target.shader = self.load_shader("final_present_stage.frag.glsl") diff --git a/RenderPipelineFile/rpcore/stages/flag_used_cells_stage.py b/RenderPipelineFile/rpcore/stages/flag_used_cells_stage.py index ab48f41f..a995b2fd 100644 --- a/RenderPipelineFile/rpcore/stages/flag_used_cells_stage.py +++ b/RenderPipelineFile/rpcore/stages/flag_used_cells_stage.py @@ -1,60 +1,60 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.render_stage import RenderStage -from rpcore.image import Image - - -class FlagUsedCellsStage(RenderStage): - - """ This stage flags all used cells based on the depth buffer """ - - required_pipes = ["GBuffer"] - required_inputs = [] - - @property - def produced_pipes(self): - return {"FlaggedCells": self.cell_grid_flags} - - def create(self): - self.target = self.create_target("FlagUsedCells") - self.target.prepare_buffer() - - self.cell_grid_flags = Image.create_2d_array( - "CellGridFlags", 0, 0, - self._pipeline.settings["lighting.culling_grid_slices"], "R8") - self.target.set_shader_input("cellGridFlags", self.cell_grid_flags) - - def update(self): - self.cell_grid_flags.clear_image() - - def set_dimensions(self): - tile_amount = self._pipeline.light_mgr.num_tiles - self.cell_grid_flags.set_x_size(tile_amount.x) - self.cell_grid_flags.set_y_size(tile_amount.y) - - def reload_shaders(self): - self.target.shader = self.load_shader("flag_used_cells.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.render_stage import RenderStage +from rpcore.image import Image + + +class FlagUsedCellsStage(RenderStage): + + """ This stage flags all used cells based on the depth buffer """ + + required_pipes = ["GBuffer"] + required_inputs = [] + + @property + def produced_pipes(self): + return {"FlaggedCells": self.cell_grid_flags} + + def create(self): + self.target = self.create_target("FlagUsedCells") + self.target.prepare_buffer() + + self.cell_grid_flags = Image.create_2d_array( + "CellGridFlags", 0, 0, + self._pipeline.settings["lighting.culling_grid_slices"], "R8") + self.target.set_shader_input("cellGridFlags", self.cell_grid_flags) + + def update(self): + self.cell_grid_flags.clear_image() + + def set_dimensions(self): + tile_amount = self._pipeline.light_mgr.num_tiles + self.cell_grid_flags.set_x_size(tile_amount.x) + self.cell_grid_flags.set_y_size(tile_amount.y) + + def reload_shaders(self): + self.target.shader = self.load_shader("flag_used_cells.frag.glsl") diff --git a/RenderPipelineFile/rpcore/stages/gbuffer_stage.py b/RenderPipelineFile/rpcore/stages/gbuffer_stage.py index 0b7b6d4b..94ecbf5c 100644 --- a/RenderPipelineFile/rpcore/stages/gbuffer_stage.py +++ b/RenderPipelineFile/rpcore/stages/gbuffer_stage.py @@ -1,66 +1,66 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.globals import Globals -from rpcore.render_stage import RenderStage -from rpcore.util.shader_input_blocks import SimpleInputBlock - - -class GBufferStage(RenderStage): - - """ This is the main pass stage, rendering the objects and creating the - GBuffer which is used in later stages """ - - required_inputs = ["DefaultEnvmap"] - required_pipes = [] - - @property - def produced_pipes(self): - return { - "GBuffer": self.make_gbuffer_ubo(), - "SceneDepth": self.target.depth_tex - } - - def make_gbuffer_ubo(self): - ubo = SimpleInputBlock("GBuffer") - ubo.add_input("Depth", self.target.depth_tex) - ubo.add_input("Data0", self.target.color_tex) - ubo.add_input("Data1", self.target.aux_tex[0]) - ubo.add_input("Data2", self.target.aux_tex[1]) - return ubo - - def create(self): - self.target = self.create_target("GBuffer") - self.target.add_color_attachment(bits=16, alpha=True) - self.target.add_depth_attachment(bits=32) - self.target.add_aux_attachments(bits=16, count=2) - self.target.prepare_render(Globals.base.cam) - - def set_shader_input(self, *args): - Globals.render.set_shader_input(*args) - - def set_shader_inputs(self, **kwargs): - Globals.render.set_shader_inputs(**kwargs) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.globals import Globals +from rpcore.render_stage import RenderStage +from rpcore.util.shader_input_blocks import SimpleInputBlock + + +class GBufferStage(RenderStage): + + """ This is the main pass stage, rendering the objects and creating the + GBuffer which is used in later stages """ + + required_inputs = ["DefaultEnvmap"] + required_pipes = [] + + @property + def produced_pipes(self): + return { + "GBuffer": self.make_gbuffer_ubo(), + "SceneDepth": self.target.depth_tex + } + + def make_gbuffer_ubo(self): + ubo = SimpleInputBlock("GBuffer") + ubo.add_input("Depth", self.target.depth_tex) + ubo.add_input("Data0", self.target.color_tex) + ubo.add_input("Data1", self.target.aux_tex[0]) + ubo.add_input("Data2", self.target.aux_tex[1]) + return ubo + + def create(self): + self.target = self.create_target("GBuffer") + self.target.add_color_attachment(bits=16, alpha=True) + self.target.add_depth_attachment(bits=32) + self.target.add_aux_attachments(bits=16, count=2) + self.target.prepare_render(Globals.base.cam) + + def set_shader_input(self, *args): + Globals.render.set_shader_input(*args) + + def set_shader_inputs(self, **kwargs): + Globals.render.set_shader_inputs(**kwargs) diff --git a/RenderPipelineFile/rpcore/stages/shadow_stage.py b/RenderPipelineFile/rpcore/stages/shadow_stage.py index f0a12c63..a94b3bb0 100644 --- a/RenderPipelineFile/rpcore/stages/shadow_stage.py +++ b/RenderPipelineFile/rpcore/stages/shadow_stage.py @@ -1,78 +1,78 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import SamplerState - -from rpcore.render_stage import RenderStage -from rpcore.globals import Globals - - -class ShadowStage(RenderStage): - - """ This is the stage which renders all shadows """ - required_inputs = [] - - def __init__(self, pipeline): - RenderStage.__init__(self, pipeline) - self.size = 4096 - - @property - def produced_pipes(self): - return { - "ShadowAtlas": self.target.depth_tex, - "ShadowAtlasPCF": (self.target.depth_tex, self.make_pcf_state()), - } - - def make_pcf_state(self): - state = SamplerState() - state.set_minfilter(SamplerState.FT_shadow) - state.set_magfilter(SamplerState.FT_shadow) - return state - - @property - def atlas_buffer(self): - return self.target.internal_buffer - - def create(self): - self.target = self.create_target("ShadowAtlas") - self.target.size = self.size - self.target.add_depth_attachment(bits=16) - self.target.prepare_render(None) - - # Remove all current display regions - self.target.internal_buffer.remove_all_display_regions() - self.target.internal_buffer.get_display_region(0).set_active(False) - - # Disable the target, and also disable depth clear - self.target.active = False - self.target.internal_buffer.set_clear_depth_active(False) - self.target.region.set_clear_depth_active(False) - - def set_shader_input(self, *args): - Globals.render.set_shader_input(*args) - - def set_shader_inputs(self, **kwargs): - Globals.render.set_shader_inputs(**kwargs) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import SamplerState + +from rpcore.render_stage import RenderStage +from rpcore.globals import Globals + + +class ShadowStage(RenderStage): + + """ This is the stage which renders all shadows """ + required_inputs = [] + + def __init__(self, pipeline): + RenderStage.__init__(self, pipeline) + self.size = 4096 + + @property + def produced_pipes(self): + return { + "ShadowAtlas": self.target.depth_tex, + "ShadowAtlasPCF": (self.target.depth_tex, self.make_pcf_state()), + } + + def make_pcf_state(self): + state = SamplerState() + state.set_minfilter(SamplerState.FT_shadow) + state.set_magfilter(SamplerState.FT_shadow) + return state + + @property + def atlas_buffer(self): + return self.target.internal_buffer + + def create(self): + self.target = self.create_target("ShadowAtlas") + self.target.size = self.size + self.target.add_depth_attachment(bits=16) + self.target.prepare_render(None) + + # Remove all current display regions + self.target.internal_buffer.remove_all_display_regions() + self.target.internal_buffer.get_display_region(0).set_active(False) + + # Disable the target, and also disable depth clear + self.target.active = False + self.target.internal_buffer.set_clear_depth_active(False) + self.target.region.set_clear_depth_active(False) + + def set_shader_input(self, *args): + Globals.render.set_shader_input(*args) + + def set_shader_inputs(self, **kwargs): + Globals.render.set_shader_inputs(**kwargs) diff --git a/RenderPipelineFile/rpcore/stages/update_previous_pipes_stage.py b/RenderPipelineFile/rpcore/stages/update_previous_pipes_stage.py index c6dd6557..e4a4bd35 100644 --- a/RenderPipelineFile/rpcore/stages/update_previous_pipes_stage.py +++ b/RenderPipelineFile/rpcore/stages/update_previous_pipes_stage.py @@ -1,126 +1,126 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from direct.stdpy.file import open - -from rpcore.render_stage import RenderStage -from rpcore.globals import Globals - - -class UpdatePreviousPipesStage(RenderStage): - - """ This stage is constructed by the StageManager and stores all the - current pipes in the previous pipe storage. - - This stage is a bit special, and not like the other stages, it does not - specify inputs, since the StageManager passes all required inputs on demand. - Also this stage does not load any shaders, but creates them on the fly. - """ - - def __init__(self, pipeline): - RenderStage.__init__(self, pipeline) - self._transfers = [] - - def add_transfer(self, from_tex, to_tex): - """ Adds a new texture which should be copied from "from_tex" to - "to_tex". This should be called before the stage gets constructed. - The source texture is expected to have the same size as the render - resolution. """ - self._transfers.append((from_tex, to_tex)) - - def create(self): - self.debug("Creating previous pipes stage ..") - self._target = self.create_target("StorePreviousPipes") - self._target.prepare_buffer() - - # Set inputs - for i, (from_tex, to_tex) in enumerate(self._transfers): # pylint: disable=unused-variable - self._target.set_shader_input("SrcTex" + str(i), from_tex) - self._target.set_shader_input("DestTex" + str(i), to_tex) - - def set_dimensions(self): - """ Sets the dimensions on all targets. See RenderTarget::set_dimensions """ - for from_tex, to_tex in self._transfers: # pylint: disable=unused-variable - to_tex.set_x_size(Globals.resolution.x) - to_tex.set_y_size(Globals.resolution.y) - - def reload_shaders(self): - """ This method augo-generates a shader which copies all textures specified - as "from-tex" to the textures specified as "to-tex". """ - uniforms = [] - lines = [] - - # Collect all samplers and generate the required uniforms and copy code - for i, (from_tex, to_tex) in enumerate(self._transfers): - index = str(i) - uniforms.append(self.get_sampler_type(from_tex) + " SrcTex" + index) - uniforms.append(self.get_sampler_type(to_tex, True) + " DestTex" + index) - lines += [ - "\n // Copying " + from_tex.get_name() + " to " + to_tex.get_name(), - self.get_sampler_lookup(from_tex, "data" + index, "SrcTex" + index, "coord_2d_int"), - self.get_store_code(to_tex, "DestTex" + index, "coord_2d_int", "data" + index), - "\n", - ] - - # Actually create the shader - fragment = "#version 430\n" - fragment += "\n// Autogenerated, do not edit! Your changes will be lost.\n\n" - for uniform in uniforms: - fragment += "uniform " + uniform + ";\n" - fragment += "\nvoid main() {\n" - fragment += " ivec2 coord_2d_int = ivec2(gl_FragCoord.xy);\n" - for line in lines: - fragment += " " + line + "\n" - fragment += "}\n" - - # Write the shader - shader_dest = "/$$rptemp/$$update_previous_pipes.frag.glsl" - with open(shader_dest, "w") as handle: - handle.write(fragment) - - # Load it back again - self._target.shader = self.load_shader(shader_dest) - - def get_sampler_type(self, tex, can_write=False): # pylint: disable=unused-argument - """ Returns the matching GLSL sampler type for a Texture, or image type - in case write access is required """ - # TODO: Add more sampler types based on texture type - if not can_write: - return "sampler2D" - else: - return "writeonly image2D" - - def get_sampler_lookup(self, tex, dest_name, sampler_name, coord_var): # noqa # pylint: disable=unused-argument - """ Returns the matching GLSL sampler lookup for a texture, storing the - result in the given glsl variable """ - # TODO: Add more lookups based on texture type - return "vec4 " + dest_name + " = texelFetch(" + sampler_name + ", " + coord_var + ", 0);" - - def get_store_code(self, tex, sampler_name, coord_var, data_var): # noqa# pylint: disable=unused-argument - """ Returns the matching GLSL code to store the given data in a given - texture """ - # TODO: Add more stores based on texture type - return "imageStore(" + sampler_name + ", " + coord_var + ", vec4(" + data_var + "));" +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from direct.stdpy.file import open + +from rpcore.render_stage import RenderStage +from rpcore.globals import Globals + + +class UpdatePreviousPipesStage(RenderStage): + + """ This stage is constructed by the StageManager and stores all the + current pipes in the previous pipe storage. + + This stage is a bit special, and not like the other stages, it does not + specify inputs, since the StageManager passes all required inputs on demand. + Also this stage does not load any shaders, but creates them on the fly. + """ + + def __init__(self, pipeline): + RenderStage.__init__(self, pipeline) + self._transfers = [] + + def add_transfer(self, from_tex, to_tex): + """ Adds a new texture which should be copied from "from_tex" to + "to_tex". This should be called before the stage gets constructed. + The source texture is expected to have the same size as the render + resolution. """ + self._transfers.append((from_tex, to_tex)) + + def create(self): + self.debug("Creating previous pipes stage ..") + self._target = self.create_target("StorePreviousPipes") + self._target.prepare_buffer() + + # Set inputs + for i, (from_tex, to_tex) in enumerate(self._transfers): # pylint: disable=unused-variable + self._target.set_shader_input("SrcTex" + str(i), from_tex) + self._target.set_shader_input("DestTex" + str(i), to_tex) + + def set_dimensions(self): + """ Sets the dimensions on all targets. See RenderTarget::set_dimensions """ + for from_tex, to_tex in self._transfers: # pylint: disable=unused-variable + to_tex.set_x_size(Globals.resolution.x) + to_tex.set_y_size(Globals.resolution.y) + + def reload_shaders(self): + """ This method augo-generates a shader which copies all textures specified + as "from-tex" to the textures specified as "to-tex". """ + uniforms = [] + lines = [] + + # Collect all samplers and generate the required uniforms and copy code + for i, (from_tex, to_tex) in enumerate(self._transfers): + index = str(i) + uniforms.append(self.get_sampler_type(from_tex) + " SrcTex" + index) + uniforms.append(self.get_sampler_type(to_tex, True) + " DestTex" + index) + lines += [ + "\n // Copying " + from_tex.get_name() + " to " + to_tex.get_name(), + self.get_sampler_lookup(from_tex, "data" + index, "SrcTex" + index, "coord_2d_int"), + self.get_store_code(to_tex, "DestTex" + index, "coord_2d_int", "data" + index), + "\n", + ] + + # Actually create the shader + fragment = "#version 430\n" + fragment += "\n// Autogenerated, do not edit! Your changes will be lost.\n\n" + for uniform in uniforms: + fragment += "uniform " + uniform + ";\n" + fragment += "\nvoid main() {\n" + fragment += " ivec2 coord_2d_int = ivec2(gl_FragCoord.xy);\n" + for line in lines: + fragment += " " + line + "\n" + fragment += "}\n" + + # Write the shader + shader_dest = "/$$rptemp/$$update_previous_pipes.frag.glsl" + with open(shader_dest, "w") as handle: + handle.write(fragment) + + # Load it back again + self._target.shader = self.load_shader(shader_dest) + + def get_sampler_type(self, tex, can_write=False): # pylint: disable=unused-argument + """ Returns the matching GLSL sampler type for a Texture, or image type + in case write access is required """ + # TODO: Add more sampler types based on texture type + if not can_write: + return "sampler2D" + else: + return "writeonly image2D" + + def get_sampler_lookup(self, tex, dest_name, sampler_name, coord_var): # noqa # pylint: disable=unused-argument + """ Returns the matching GLSL sampler lookup for a texture, storing the + result in the given glsl variable """ + # TODO: Add more lookups based on texture type + return "vec4 " + dest_name + " = texelFetch(" + sampler_name + ", " + coord_var + ", 0);" + + def get_store_code(self, tex, sampler_name, coord_var, data_var): # noqa# pylint: disable=unused-argument + """ Returns the matching GLSL code to store the given data in a given + texture """ + # TODO: Add more stores based on texture type + return "imageStore(" + sampler_name + ", " + coord_var + ", vec4(" + data_var + "));" diff --git a/RenderPipelineFile/rpcore/stages/upscale_stage.py b/RenderPipelineFile/rpcore/stages/upscale_stage.py index ec1681c5..748329e6 100644 --- a/RenderPipelineFile/rpcore/stages/upscale_stage.py +++ b/RenderPipelineFile/rpcore/stages/upscale_stage.py @@ -1,50 +1,50 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.globals import Globals -from rpcore.render_stage import RenderStage - - -class UpscaleStage(RenderStage): - - """ This stage upscales the scene to native resolution, using a bicubic filter """ - - required_inputs = [] - required_pipes = ["ShadedScene"] - - @property - def produced_pipes(self): - return {"ShadedScene": self._target.color_tex} - - def create(self): - native_size = Globals.native_resolution.x, Globals.native_resolution.y - self._target = self.create_target("Upscale") - self._target.size = native_size - self._target.add_color_attachment(bits=16) - self._target.prepare_buffer() - - def reload_shaders(self): - self._target.shader = self.load_shader("upscale_stage.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.globals import Globals +from rpcore.render_stage import RenderStage + + +class UpscaleStage(RenderStage): + + """ This stage upscales the scene to native resolution, using a bicubic filter """ + + required_inputs = [] + required_pipes = ["ShadedScene"] + + @property + def produced_pipes(self): + return {"ShadedScene": self._target.color_tex} + + def create(self): + native_size = Globals.native_resolution.x, Globals.native_resolution.y + self._target = self.create_target("Upscale") + self._target.size = native_size + self._target.add_color_attachment(bits=16) + self._target.prepare_buffer() + + def reload_shaders(self): + self._target.shader = self.load_shader("upscale_stage.frag.glsl") diff --git a/RenderPipelineFile/rpcore/util/cubemap_filter.py b/RenderPipelineFile/rpcore/util/cubemap_filter.py index 55ffe6f5..bc2cd697 100644 --- a/RenderPipelineFile/rpcore/util/cubemap_filter.py +++ b/RenderPipelineFile/rpcore/util/cubemap_filter.py @@ -1,199 +1,199 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import division - -from panda3d.core import SamplerState, Vec4 - -from rpcore.rpobject import RPObject -from rpcore.image import Image - - -class CubemapFilter(RPObject): - - """ Util class for filtering cubemaps, provides funcionality to generate - a specular and diffuse IBL cubemap. """ - - # Fixed size for the diffuse cubemap, since it does not contain much detail - DIFFUSE_CUBEMAP_SIZE = 10 - PREFILTER_CUBEMAP_SIZE = 32 - - def __init__(self, parent_stage, name="Cubemap", size=128): - """ Inits the filter from a given stage """ - RPObject.__init__(self) - self._stage = parent_stage - self._name = name - self._size = size - self._make_maps() - - @property - def specular_cubemap(self): - """ Returns the generated specular cubemap. The specular cubemap is - mipmapped and provides the specular IBL components of the input cubemap. """ - return self._specular_map - - @property - def diffuse_cubemap(self): - """ Returns the generated diffuse cubemap. The diffuse cubemap has no - mipmaps and contains the filtered diffuse component of the input cubemap. """ - return self._diffuse_map - - @property - def target_cubemap(self): - """ Returns the target where the caller should write the initial cubemap - data to """ - return self._specular_map - - @property - def size(self): - """ Returns the size of the created cubemap, previously passed to the - constructor of the filter """ - return self._size - - def create(self): - """ Creates the filter. The input cubemap should be mipmapped, and will - get reused for the specular cubemap. """ - self._make_specular_targets() - self._make_diffuse_target() - - def _make_maps(self): - """ Internal method to create the cubemap storage """ - - # Create the cubemaps for the diffuse and specular components - self._prefilter_map = Image.create_cube( - self._name + "IBLPrefDiff", CubemapFilter.PREFILTER_CUBEMAP_SIZE, "R11G11B10") - self._diffuse_map = Image.create_cube( - self._name + "IBLDiff", CubemapFilter.DIFFUSE_CUBEMAP_SIZE, "R11G11B10") - self._spec_pref_map = Image.create_cube( - self._name + "IBLPrefSpec", self._size, "R11G11B10") - self._specular_map = Image.create_cube( - self._name + "IBLSpec", self._size, "R11G11B10") - - # Set the correct filtering modes - for tex in [self._diffuse_map, self._specular_map, - self._prefilter_map, self._spec_pref_map]: - tex.set_minfilter(SamplerState.FT_linear) - tex.set_magfilter(SamplerState.FT_linear) - tex.set_clear_color(Vec4(0)) - tex.clear_image() - - # Use mipmaps for the specular cubemap - self._spec_pref_map.set_minfilter(SamplerState.FT_linear_mipmap_linear) - self._specular_map.set_minfilter(SamplerState.FT_linear_mipmap_linear) - - def _make_specular_targets(self): - """ Internal method to create the specular mip chain """ - self._targets_spec = [] - self._targets_spec_filter = [] - mip, mipsize = 0, self._size - while mipsize >= 2: - - # Assuming an initial size of a power of 2, its safe to do this - mipsize = mipsize // 2 - - # Create the target which downsamples the mipmap - target = self._stage.create_target("CF:SpecIBL-PreFilter-" + str(mipsize)) - target.size = mipsize * 6, mipsize - target.prepare_buffer() - - if mip == 0: - target.set_shader_input("SourceTex", self._specular_map) - else: - target.set_shader_input("SourceTex", self._spec_pref_map) - target.set_shader_input( - "DestMipmap", self._spec_pref_map, False, True, -1, - mip + 1, 0) - target.set_shader_input("currentMip", mip) - - mip += 1 - - # Create the target which filters the mipmap and removes the noise - target_filter = self._stage.create_target("CF:SpecIBL-PostFilter-" + str(mipsize)) - target_filter.size = mipsize * 6, mipsize - target_filter.prepare_buffer() - target_filter.set_shader_inputs( - currentMip=mip, - SourceTex=self._spec_pref_map) - target_filter.set_shader_input( - "DestMipmap", self._specular_map, False, True, -1, mip, 0) - - self._targets_spec.append(target) - self._targets_spec_filter.append(target_filter) - - def _make_diffuse_target(self): - """ Internal method to create the diffuse cubemap """ - - # Create the target which integrates the lambert brdf - self._diffuse_target = self._stage.create_target("CF:DiffuseIBL") - self._diffuse_target.size = ( - CubemapFilter.PREFILTER_CUBEMAP_SIZE * 6, - CubemapFilter.PREFILTER_CUBEMAP_SIZE) - self._diffuse_target.prepare_buffer() - - self._diffuse_target.set_shader_inputs( - SourceCubemap=self._specular_map, - DestCubemap=self._prefilter_map, - cubeSize=CubemapFilter.PREFILTER_CUBEMAP_SIZE) - - # Create the target which removes the noise from the previous target, - # which is introduced with importance sampling - self._diff_filter_target = self._stage.create_target("CF:DiffPrefIBL") - self._diff_filter_target.size = ( - CubemapFilter.DIFFUSE_CUBEMAP_SIZE * 6, - CubemapFilter.DIFFUSE_CUBEMAP_SIZE) - self._diff_filter_target.prepare_buffer() - - self._diff_filter_target.set_shader_inputs( - SourceCubemap=self._prefilter_map, - DestCubemap=self._diffuse_map, - cubeSize=CubemapFilter.DIFFUSE_CUBEMAP_SIZE) - - def reload_shaders(self): - """ Sets all required shaders on the filter. """ - - # Set diffuse filter shaders - self._diffuse_target.shader = self._stage.load_shader( - "ibl/cubemap_diffuse.frag.glsl") - self._diff_filter_target.shader = self._stage.load_shader( - "ibl/cubemap_diffuse_filter.frag.glsl") - - # Set specular prefilter shaders - mip_shader = self._stage.load_shader("ibl/cubemap_specular_prefilter.frag.glsl") - for target in self._targets_spec: - target.shader = mip_shader - - # Special shader for the first prefilter target - self._targets_spec[0].shader = self._stage.load_shader( - "ibl/cubemap_specular_prefilter_first.frag.glsl") - - # Set specular filter sampling shaders - mip_filter_shader = self._stage.load_shader("ibl/cubemap_specular_filter.frag.glsl") - for target in self._targets_spec_filter: - target.shader = mip_filter_shader - - # Special shader for the first filter target - self._targets_spec_filter[0].shader = self._stage.load_shader( - "ibl/cubemap_specular_filter_first.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import division + +from panda3d.core import SamplerState, Vec4 + +from rpcore.rpobject import RPObject +from rpcore.image import Image + + +class CubemapFilter(RPObject): + + """ Util class for filtering cubemaps, provides funcionality to generate + a specular and diffuse IBL cubemap. """ + + # Fixed size for the diffuse cubemap, since it does not contain much detail + DIFFUSE_CUBEMAP_SIZE = 10 + PREFILTER_CUBEMAP_SIZE = 32 + + def __init__(self, parent_stage, name="Cubemap", size=128): + """ Inits the filter from a given stage """ + RPObject.__init__(self) + self._stage = parent_stage + self._name = name + self._size = size + self._make_maps() + + @property + def specular_cubemap(self): + """ Returns the generated specular cubemap. The specular cubemap is + mipmapped and provides the specular IBL components of the input cubemap. """ + return self._specular_map + + @property + def diffuse_cubemap(self): + """ Returns the generated diffuse cubemap. The diffuse cubemap has no + mipmaps and contains the filtered diffuse component of the input cubemap. """ + return self._diffuse_map + + @property + def target_cubemap(self): + """ Returns the target where the caller should write the initial cubemap + data to """ + return self._specular_map + + @property + def size(self): + """ Returns the size of the created cubemap, previously passed to the + constructor of the filter """ + return self._size + + def create(self): + """ Creates the filter. The input cubemap should be mipmapped, and will + get reused for the specular cubemap. """ + self._make_specular_targets() + self._make_diffuse_target() + + def _make_maps(self): + """ Internal method to create the cubemap storage """ + + # Create the cubemaps for the diffuse and specular components + self._prefilter_map = Image.create_cube( + self._name + "IBLPrefDiff", CubemapFilter.PREFILTER_CUBEMAP_SIZE, "R11G11B10") + self._diffuse_map = Image.create_cube( + self._name + "IBLDiff", CubemapFilter.DIFFUSE_CUBEMAP_SIZE, "R11G11B10") + self._spec_pref_map = Image.create_cube( + self._name + "IBLPrefSpec", self._size, "R11G11B10") + self._specular_map = Image.create_cube( + self._name + "IBLSpec", self._size, "R11G11B10") + + # Set the correct filtering modes + for tex in [self._diffuse_map, self._specular_map, + self._prefilter_map, self._spec_pref_map]: + tex.set_minfilter(SamplerState.FT_linear) + tex.set_magfilter(SamplerState.FT_linear) + tex.set_clear_color(Vec4(0)) + tex.clear_image() + + # Use mipmaps for the specular cubemap + self._spec_pref_map.set_minfilter(SamplerState.FT_linear_mipmap_linear) + self._specular_map.set_minfilter(SamplerState.FT_linear_mipmap_linear) + + def _make_specular_targets(self): + """ Internal method to create the specular mip chain """ + self._targets_spec = [] + self._targets_spec_filter = [] + mip, mipsize = 0, self._size + while mipsize >= 2: + + # Assuming an initial size of a power of 2, its safe to do this + mipsize = mipsize // 2 + + # Create the target which downsamples the mipmap + target = self._stage.create_target("CF:SpecIBL-PreFilter-" + str(mipsize)) + target.size = mipsize * 6, mipsize + target.prepare_buffer() + + if mip == 0: + target.set_shader_input("SourceTex", self._specular_map) + else: + target.set_shader_input("SourceTex", self._spec_pref_map) + target.set_shader_input( + "DestMipmap", self._spec_pref_map, False, True, -1, + mip + 1, 0) + target.set_shader_input("currentMip", mip) + + mip += 1 + + # Create the target which filters the mipmap and removes the noise + target_filter = self._stage.create_target("CF:SpecIBL-PostFilter-" + str(mipsize)) + target_filter.size = mipsize * 6, mipsize + target_filter.prepare_buffer() + target_filter.set_shader_inputs( + currentMip=mip, + SourceTex=self._spec_pref_map) + target_filter.set_shader_input( + "DestMipmap", self._specular_map, False, True, -1, mip, 0) + + self._targets_spec.append(target) + self._targets_spec_filter.append(target_filter) + + def _make_diffuse_target(self): + """ Internal method to create the diffuse cubemap """ + + # Create the target which integrates the lambert brdf + self._diffuse_target = self._stage.create_target("CF:DiffuseIBL") + self._diffuse_target.size = ( + CubemapFilter.PREFILTER_CUBEMAP_SIZE * 6, + CubemapFilter.PREFILTER_CUBEMAP_SIZE) + self._diffuse_target.prepare_buffer() + + self._diffuse_target.set_shader_inputs( + SourceCubemap=self._specular_map, + DestCubemap=self._prefilter_map, + cubeSize=CubemapFilter.PREFILTER_CUBEMAP_SIZE) + + # Create the target which removes the noise from the previous target, + # which is introduced with importance sampling + self._diff_filter_target = self._stage.create_target("CF:DiffPrefIBL") + self._diff_filter_target.size = ( + CubemapFilter.DIFFUSE_CUBEMAP_SIZE * 6, + CubemapFilter.DIFFUSE_CUBEMAP_SIZE) + self._diff_filter_target.prepare_buffer() + + self._diff_filter_target.set_shader_inputs( + SourceCubemap=self._prefilter_map, + DestCubemap=self._diffuse_map, + cubeSize=CubemapFilter.DIFFUSE_CUBEMAP_SIZE) + + def reload_shaders(self): + """ Sets all required shaders on the filter. """ + + # Set diffuse filter shaders + self._diffuse_target.shader = self._stage.load_shader( + "ibl/cubemap_diffuse.frag.glsl") + self._diff_filter_target.shader = self._stage.load_shader( + "ibl/cubemap_diffuse_filter.frag.glsl") + + # Set specular prefilter shaders + mip_shader = self._stage.load_shader("ibl/cubemap_specular_prefilter.frag.glsl") + for target in self._targets_spec: + target.shader = mip_shader + + # Special shader for the first prefilter target + self._targets_spec[0].shader = self._stage.load_shader( + "ibl/cubemap_specular_prefilter_first.frag.glsl") + + # Set specular filter sampling shaders + mip_filter_shader = self._stage.load_shader("ibl/cubemap_specular_filter.frag.glsl") + for target in self._targets_spec_filter: + target.shader = mip_filter_shader + + # Special shader for the first filter target + self._targets_spec_filter[0].shader = self._stage.load_shader( + "ibl/cubemap_specular_filter_first.frag.glsl") diff --git a/RenderPipelineFile/rpcore/util/display_shader_builder.py b/RenderPipelineFile/rpcore/util/display_shader_builder.py index 4231ee50..8680be25 100644 --- a/RenderPipelineFile/rpcore/util/display_shader_builder.py +++ b/RenderPipelineFile/rpcore/util/display_shader_builder.py @@ -1,176 +1,176 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -# pylint: disable=line-too-long - -from direct.stdpy.file import isfile, open - -from rpcore.image import Image -from rpcore.rpobject import RPObject -from rpcore.loader import RPLoader - - -class DisplayShaderBuilder(object): # pylint: disable=too-few-public-methods - - """ Utility class to generate shaders on the fly to display texture previews - and also buffers """ - - @classmethod - def build(cls, texture, view_width, view_height): - """ Builds a shader to display in a view port with the size - * """ - view_width, view_height = int(view_width), int(view_height) - - cache_key = "/$$rptemp/$$TEXDISPLAY-X{}-Y{}-Z{}-TT{}-CT{}-VW{}-VH{}.frag.glsl".format( - texture.get_x_size(), - texture.get_y_size(), - texture.get_z_size(), - texture.get_texture_type(), - texture.get_component_type(), - view_width, - view_height) - - # Only regenerate the file when there is no cache entry for it - if not isfile(cache_key) or True: - fragment_shader = cls._build_fragment_shader(texture, view_width, view_height) - - with open(cache_key, "w") as handle: - handle.write(fragment_shader) - - return RPLoader.load_shader("/$$rp/shader/default_gui_shader.vert.glsl", cache_key) - - @classmethod - def _build_fragment_shader(cls, texture, view_width, view_height): - """ Internal method to build a fragment shader displaying the texture """ - - sampling_code, sampler_type = cls._generate_sampling_code(texture, view_width, view_height) - - # Build actual shader - built = """ - #version 400 - #pragma include "render_pipeline_base.inc.glsl" - #extension GL_ARB_shading_language_420pack : enable - in vec2 texcoord; - out vec3 result; - uniform int mipmap; - uniform int slice; - uniform float brightness; - uniform bool tonemap; - uniform """ + sampler_type + """ p3d_Texture0; - void main() { - int view_width = """ + str(view_width) + """; - int view_height = """ + str(view_height) + """; - ivec2 display_coord = ivec2(texcoord * vec2(view_width, view_height)); - int int_index = display_coord.x + display_coord.y * view_width; - """ + sampling_code + """ - result *= brightness; - if (tonemap) - result = result / (1 + result); - } - """ - - # Strip trailing spaces - built = '\n'.join([i.strip() for i in built.split("\n")]) - - return built - - @classmethod - def _generate_sampling_code(cls, texture, view_width, view_height): # noqa # pylint: disable=unused-argument,too-many-branches - """ Generates the GLSL code to sample a texture and also returns the - GLSL sampler type """ - - texture_type = texture.get_texture_type() - comp_type = texture.get_component_type() - - # Useful snippets - int_coord = "ivec2 int_coord = ivec2(texcoord * textureSize(p3d_Texture0, mipmap).xy);" - slice_count = "int slice_count = textureSize(p3d_Texture0, 0).z;" - - float_types = [Image.T_float, Image.T_unsigned_byte] - int_types = [Image.T_int, Image.T_unsigned_short, Image.T_unsigned_int_24_8] - - result = "result = vec3(1, 0, 1);", "sampler2D" - - if comp_type not in float_types + int_types: - RPObject.global_warn( - "DisplayShaderBuilder", "Unkown texture component type:", comp_type) - - # 2D Textures - if texture_type == Image.TT_2d_texture: - - if comp_type in float_types: - result = "result = textureLod(p3d_Texture0, texcoord, mipmap).xyz;", "sampler2D" - - elif comp_type in int_types: - result = int_coord + "result = texelFetch(p3d_Texture0, int_coord, mipmap).xyz / 10.0;", "isampler2D" # noqa - - # Buffer Textures - elif texture_type == Image.TT_buffer_texture: - - def range_check(code): - return "if (int_index < textureSize(p3d_Texture0)) {" + code + "} else { result = vec3(1.0, 0.6, 0.2);};" # noqa - - if comp_type in float_types: - result = range_check("result = texelFetch(p3d_Texture0, int_index).xyz;"), "samplerBuffer" # noqa - - elif comp_type in int_types: - result = range_check("result = texelFetch(p3d_Texture0, int_index).xyz / 10.0;"), "isamplerBuffer" # noqa - - # 3D Textures - elif texture_type == Image.TT_3d_texture: - - if comp_type in float_types: - result = slice_count + "result = textureLod(p3d_Texture0, vec3(texcoord, (0.5 + slice) / slice_count), mipmap).xyz;", "sampler3D" # noqa - - elif comp_type in int_types: - result = int_coord + "result = texelFetch(p3d_Texture0, ivec3(int_coord, slice), mipmap).xyz / 10.0;", "isampler3D" # noqa - - # 2D Texture Array - elif texture_type == Image.TT_2d_texture_array: - - if comp_type in float_types: - result = "result = textureLod(p3d_Texture0, vec3(texcoord, slice), mipmap).xyz;", "sampler2DArray" # noqa - - elif comp_type in int_types: - result = int_coord + "result = texelFetch(p3d_Texture0, ivec3(int_coord, slice), mipmap).xyz / 10.0;", "isampler2DArray" # noqa - - # Cubemap - elif texture_type == Image.TT_cube_map: - - code = "vec3 sample_dir = get_cubemap_coordinate(slice, texcoord*2-1);\n" - code += "result = textureLod(p3d_Texture0, sample_dir, mipmap).xyz;" - result = code, "samplerCube" - - # Cubemap array - elif texture_type == Image.TT_cube_map_array: - code = "vec3 sample_dir = get_cubemap_coordinate(slice % 6, texcoord*2-1);\n" - code += "result = textureLod(p3d_Texture0, vec4(sample_dir, slice / 6), mipmap).xyz;" - result = code, "samplerCubeArray" - - else: - print("WARNING: Unhandled texture type", texture_type, "in display shader builder") - - return result +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +# pylint: disable=line-too-long + +from direct.stdpy.file import isfile, open + +from rpcore.image import Image +from rpcore.rpobject import RPObject +from rpcore.loader import RPLoader + + +class DisplayShaderBuilder(object): # pylint: disable=too-few-public-methods + + """ Utility class to generate shaders on the fly to display texture previews + and also buffers """ + + @classmethod + def build(cls, texture, view_width, view_height): + """ Builds a shader to display in a view port with the size + * """ + view_width, view_height = int(view_width), int(view_height) + + cache_key = "/$$rptemp/$$TEXDISPLAY-X{}-Y{}-Z{}-TT{}-CT{}-VW{}-VH{}.frag.glsl".format( + texture.get_x_size(), + texture.get_y_size(), + texture.get_z_size(), + texture.get_texture_type(), + texture.get_component_type(), + view_width, + view_height) + + # Only regenerate the file when there is no cache entry for it + if not isfile(cache_key) or True: + fragment_shader = cls._build_fragment_shader(texture, view_width, view_height) + + with open(cache_key, "w") as handle: + handle.write(fragment_shader) + + return RPLoader.load_shader("/$$rp/shader/default_gui_shader.vert.glsl", cache_key) + + @classmethod + def _build_fragment_shader(cls, texture, view_width, view_height): + """ Internal method to build a fragment shader displaying the texture """ + + sampling_code, sampler_type = cls._generate_sampling_code(texture, view_width, view_height) + + # Build actual shader + built = """ + #version 400 + #pragma include "render_pipeline_base.inc.glsl" + #extension GL_ARB_shading_language_420pack : enable + in vec2 texcoord; + out vec3 result; + uniform int mipmap; + uniform int slice; + uniform float brightness; + uniform bool tonemap; + uniform """ + sampler_type + """ p3d_Texture0; + void main() { + int view_width = """ + str(view_width) + """; + int view_height = """ + str(view_height) + """; + ivec2 display_coord = ivec2(texcoord * vec2(view_width, view_height)); + int int_index = display_coord.x + display_coord.y * view_width; + """ + sampling_code + """ + result *= brightness; + if (tonemap) + result = result / (1 + result); + } + """ + + # Strip trailing spaces + built = '\n'.join([i.strip() for i in built.split("\n")]) + + return built + + @classmethod + def _generate_sampling_code(cls, texture, view_width, view_height): # noqa # pylint: disable=unused-argument,too-many-branches + """ Generates the GLSL code to sample a texture and also returns the + GLSL sampler type """ + + texture_type = texture.get_texture_type() + comp_type = texture.get_component_type() + + # Useful snippets + int_coord = "ivec2 int_coord = ivec2(texcoord * textureSize(p3d_Texture0, mipmap).xy);" + slice_count = "int slice_count = textureSize(p3d_Texture0, 0).z;" + + float_types = [Image.T_float, Image.T_unsigned_byte] + int_types = [Image.T_int, Image.T_unsigned_short, Image.T_unsigned_int_24_8] + + result = "result = vec3(1, 0, 1);", "sampler2D" + + if comp_type not in float_types + int_types: + RPObject.global_warn( + "DisplayShaderBuilder", "Unkown texture component type:", comp_type) + + # 2D Textures + if texture_type == Image.TT_2d_texture: + + if comp_type in float_types: + result = "result = textureLod(p3d_Texture0, texcoord, mipmap).xyz;", "sampler2D" + + elif comp_type in int_types: + result = int_coord + "result = texelFetch(p3d_Texture0, int_coord, mipmap).xyz / 10.0;", "isampler2D" # noqa + + # Buffer Textures + elif texture_type == Image.TT_buffer_texture: + + def range_check(code): + return "if (int_index < textureSize(p3d_Texture0)) {" + code + "} else { result = vec3(1.0, 0.6, 0.2);};" # noqa + + if comp_type in float_types: + result = range_check("result = texelFetch(p3d_Texture0, int_index).xyz;"), "samplerBuffer" # noqa + + elif comp_type in int_types: + result = range_check("result = texelFetch(p3d_Texture0, int_index).xyz / 10.0;"), "isamplerBuffer" # noqa + + # 3D Textures + elif texture_type == Image.TT_3d_texture: + + if comp_type in float_types: + result = slice_count + "result = textureLod(p3d_Texture0, vec3(texcoord, (0.5 + slice) / slice_count), mipmap).xyz;", "sampler3D" # noqa + + elif comp_type in int_types: + result = int_coord + "result = texelFetch(p3d_Texture0, ivec3(int_coord, slice), mipmap).xyz / 10.0;", "isampler3D" # noqa + + # 2D Texture Array + elif texture_type == Image.TT_2d_texture_array: + + if comp_type in float_types: + result = "result = textureLod(p3d_Texture0, vec3(texcoord, slice), mipmap).xyz;", "sampler2DArray" # noqa + + elif comp_type in int_types: + result = int_coord + "result = texelFetch(p3d_Texture0, ivec3(int_coord, slice), mipmap).xyz / 10.0;", "isampler2DArray" # noqa + + # Cubemap + elif texture_type == Image.TT_cube_map: + + code = "vec3 sample_dir = get_cubemap_coordinate(slice, texcoord*2-1);\n" + code += "result = textureLod(p3d_Texture0, sample_dir, mipmap).xyz;" + result = code, "samplerCube" + + # Cubemap array + elif texture_type == Image.TT_cube_map_array: + code = "vec3 sample_dir = get_cubemap_coordinate(slice % 6, texcoord*2-1);\n" + code += "result = textureLod(p3d_Texture0, vec4(sample_dir, slice / 6), mipmap).xyz;" + result = code, "samplerCubeArray" + + else: + print("WARNING: Unhandled texture type", texture_type, "in display shader builder") + + return result diff --git a/RenderPipelineFile/rpcore/util/generic.py b/RenderPipelineFile/rpcore/util/generic.py index d0348892..1a51d66a 100644 --- a/RenderPipelineFile/rpcore/util/generic.py +++ b/RenderPipelineFile/rpcore/util/generic.py @@ -1,115 +1,115 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - - -from __future__ import print_function - -import time -import hashlib - -from panda3d.core import PStatCollector, Mat4, Point4, Vec3 -from rpcore.globals import Globals - - -def rgb_from_string(text, min_brightness=0.6): - """ Creates a rgb color from a given string """ - ohash = hashlib.md5(text[::-1].encode("ascii")).hexdigest() - r, g, b = int(ohash[0:2], 16), int(ohash[2:4], 16), int(ohash[4:6], 16) - neg_inf = 1.0 - min_brightness - return (min_brightness + r / 255.0 * neg_inf, - min_brightness + g / 255.0 * neg_inf, - min_brightness + b / 255.0 * neg_inf) - - -def profile(func): - """ Handy decorator which can be used to profile a function with pstats """ - collector_name = "Debug:%s" % func.__name__ - - global_showbase = Globals.base - - # Insert the collector to a custom dictionary attached to the base - if hasattr(global_showbase, 'custom_collectors'): - if collector_name in global_showbase.custom_collectors.keys(): - pstat = global_showbase.custom_collectors[collector_name] - else: - global_showbase.custom_collectors[collector_name] = \ - PStatCollector(collector_name) - pstat = global_showbase.custom_collectors[collector_name] - else: - pstat = PStatCollector(collector_name) - global_showbase.custom_collectors = {} - global_showbase.custom_collectors[collector_name] = pstat - - def do_pstat(*args, **kargs): - pstat.start() - returned = func(*args, **kargs) - pstat.stop() - return returned - - do_pstat.__name__ = func.__name__ - do_pstat.__dict__ = func.__dict__ - do_pstat.__doc__ = func.__doc__ - return do_pstat - - -class profile_cpu(object): # noqa # pylint: disable=invalid-name,too-few-public-methods - """ - Context manager for profiling CPU duration. This is useful for timing - loading of files or other CPU-heavy operations. Example usage: - - with profile_cpu("Some Task"): - some_slow_operation() - - Duration of the process will be print out on the console later on. - """ - - def __init__(self, name): - self.name = name - - def __enter__(self): - self.start_time = time.process_time() - - def __exit__(self, *args): - duration = (time.process_time() - self.start_time) * 1000.0 - print(self.name, "took", round(duration, 2), "ms ") - - -def snap_shadow_map(mvp, cam_node, resolution): - """ 'Snaps' a shadow map to make sure it always is on full texel centers. - This ensures no flickering occurs while moving the shadow map. - This works by projecting the Point (0,0,0) to light space, compute the - texcoord differences and offset the light world space position by that. """ - mvp = Mat4(mvp) - base_point = mvp.xform(Point4(0, 0, 0, 1)) * 0.5 + 0.5 - texel_size = 1.0 / float(resolution) - offset_x = base_point.x % texel_size - offset_y = base_point.y % texel_size - mvp.invert_in_place() - new_base = mvp.xform(Point4( - (base_point.x - offset_x) * 2.0 - 1.0, - (base_point.y - offset_y) * 2.0 - 1.0, - (base_point.z) * 2.0 - 1.0, 1)) - cam_node.set_pos(cam_node.get_pos() - Vec3(new_base.x, new_base.y, new_base.z)) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + + +from __future__ import print_function + +import time +import hashlib + +from panda3d.core import PStatCollector, Mat4, Point4, Vec3 +from rpcore.globals import Globals + + +def rgb_from_string(text, min_brightness=0.6): + """ Creates a rgb color from a given string """ + ohash = hashlib.md5(text[::-1].encode("ascii")).hexdigest() + r, g, b = int(ohash[0:2], 16), int(ohash[2:4], 16), int(ohash[4:6], 16) + neg_inf = 1.0 - min_brightness + return (min_brightness + r / 255.0 * neg_inf, + min_brightness + g / 255.0 * neg_inf, + min_brightness + b / 255.0 * neg_inf) + + +def profile(func): + """ Handy decorator which can be used to profile a function with pstats """ + collector_name = "Debug:%s" % func.__name__ + + global_showbase = Globals.base + + # Insert the collector to a custom dictionary attached to the base + if hasattr(global_showbase, 'custom_collectors'): + if collector_name in global_showbase.custom_collectors.keys(): + pstat = global_showbase.custom_collectors[collector_name] + else: + global_showbase.custom_collectors[collector_name] = \ + PStatCollector(collector_name) + pstat = global_showbase.custom_collectors[collector_name] + else: + pstat = PStatCollector(collector_name) + global_showbase.custom_collectors = {} + global_showbase.custom_collectors[collector_name] = pstat + + def do_pstat(*args, **kargs): + pstat.start() + returned = func(*args, **kargs) + pstat.stop() + return returned + + do_pstat.__name__ = func.__name__ + do_pstat.__dict__ = func.__dict__ + do_pstat.__doc__ = func.__doc__ + return do_pstat + + +class profile_cpu(object): # noqa # pylint: disable=invalid-name,too-few-public-methods + """ + Context manager for profiling CPU duration. This is useful for timing + loading of files or other CPU-heavy operations. Example usage: + + with profile_cpu("Some Task"): + some_slow_operation() + + Duration of the process will be print out on the console later on. + """ + + def __init__(self, name): + self.name = name + + def __enter__(self): + self.start_time = time.process_time() + + def __exit__(self, *args): + duration = (time.process_time() - self.start_time) * 1000.0 + print(self.name, "took", round(duration, 2), "ms ") + + +def snap_shadow_map(mvp, cam_node, resolution): + """ 'Snaps' a shadow map to make sure it always is on full texel centers. + This ensures no flickering occurs while moving the shadow map. + This works by projecting the Point (0,0,0) to light space, compute the + texcoord differences and offset the light world space position by that. """ + mvp = Mat4(mvp) + base_point = mvp.xform(Point4(0, 0, 0, 1)) * 0.5 + 0.5 + texel_size = 1.0 / float(resolution) + offset_x = base_point.x % texel_size + offset_y = base_point.y % texel_size + mvp.invert_in_place() + new_base = mvp.xform(Point4( + (base_point.x - offset_x) * 2.0 - 1.0, + (base_point.y - offset_y) * 2.0 - 1.0, + (base_point.z) * 2.0 - 1.0, 1)) + cam_node.set_pos(cam_node.get_pos() - Vec3(new_base.x, new_base.y, new_base.z)) diff --git a/RenderPipelineFile/rpcore/util/ies_profile_loader.py b/RenderPipelineFile/rpcore/util/ies_profile_loader.py index caad8ce2..7f305f6e 100644 --- a/RenderPipelineFile/rpcore/util/ies_profile_loader.py +++ b/RenderPipelineFile/rpcore/util/ies_profile_loader.py @@ -1,269 +1,269 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -# Disable the unused variable warning - this occurs since we read a lot more -# properties from the IES file than we actually need -# pylint: disable=unused-variable - -from __future__ import print_function - -import re - -from panda3d.core import PTAFloat, Filename, SamplerState, VirtualFileSystem -from panda3d.core import get_model_path -from direct.stdpy.file import open, join, isfile - -from six.moves import range # pylint: disable=import-error - -from rpcore.native import IESDataset -from rpcore.image import Image -from rpcore.rpobject import RPObject - - -class InvalidIESProfileException(Exception): - """ Exception which is thrown when an error occurs during loading an IES - Profile """ - - -class IESProfileLoader(RPObject): - - """ Loader class to load .IES files and create an IESDataset from it. - It generates a LUT for each loaded ies profile which is used by the lighting - pipeline later on. """ - - # Supported IES Profiles - PROFILES = [ - "IESNA:LM-63-1986", - "IESNA:LM-63-1991", - "IESNA91", - "IESNA:LM-63-1995", - "IESNA:LM-63-2002", - "ERCO Leuchten GmbH BY: ERCO/LUM650/8701", - "ERCO Leuchten GmbH" - ] - - # Regexp for extracting keywords - KEYWORD_REGEX = re.compile(r"\[([A-Za-z0-8_-]+)\](.*)") - - def __init__(self, pipeline): - RPObject.__init__(self) - self._pipeline = pipeline - self._entries = [] - self._max_entries = 32 - self._create_storage() - - def _create_storage(self): - """ Internal method to create the storage for the profile dataset textures """ - self._storage_tex = Image.create_3d("IESDatasets", 512, 512, self._max_entries, "R16") - self._storage_tex.set_minfilter(SamplerState.FT_linear) - self._storage_tex.set_magfilter(SamplerState.FT_linear) - self._storage_tex.set_wrap_u(SamplerState.WM_clamp) - self._storage_tex.set_wrap_v(SamplerState.WM_repeat) - self._storage_tex.set_wrap_w(SamplerState.WM_clamp) - - self._pipeline.stage_mgr.inputs["IESDatasetTex"] = self._storage_tex - self._pipeline.stage_mgr.defines["MAX_IES_PROFILES"] = self._max_entries - - def load(self, filename): - """ Loads a profile from a given filename and returns the internal - used index which can be assigned to a light.""" - - # Make sure the user can load profiles directly from the ies profile folder - data_path = join("/$$rp/data/ies_profiles/", filename) - if isfile(data_path): - filename = data_path - - # Make filename unique - fname = Filename.from_os_specific(filename) - if not VirtualFileSystem.get_global_ptr().resolve_filename( - fname, get_model_path().get_value(), "ies"): - self.error("Could not resolve", filename) - return -1 - fname = fname.get_fullpath() - - # Check for cache entries - if fname in self._entries: - return self._entries.index(fname) - - # Check for out of bounds - if len(self._entries) >= self._max_entries: - # TODO: Could remove unused profiles here or regenerate texture - self.warn("Cannot load IES Profile, too many loaded! (Maximum: 32)") - - # Try loading the dataset, and see what happes - try: - dataset = self._load_and_parse_file(fname) - except InvalidIESProfileException as msg: - self.warn("Failed to load profile from", filename, ":", msg) - return -1 - - if not dataset: - return -1 - - # Dataset was loaded successfully, now copy it - dataset.generate_dataset_texture_into(self._storage_tex, len(self._entries)) - self._entries.append(fname) - - return len(self._entries) - 1 - - def _load_and_parse_file(self, pth): - """ Loads a .IES file from a given filename, returns an IESDataset - which is used by the load function later on. """ - self.debug("Loading ies profile from", pth) - - try: - with open(pth, "r") as handle: - lines = handle.readlines() - except IOError as msg: - self.error("Failed to open", pth, ":", msg) - return None - - lines = [i.strip() for i in lines] - - # Parse version header - self._check_version_header(lines.pop(0)) - - # Parse arbitrary amount of keywords - keywords = self._extract_keywords(lines) # noqa - - # Next line should be TILT=NONE according to the spec - if lines.pop(0) != "TILT=NONE": - raise InvalidIESProfileException("Expected TILT=NONE line, but none found!") - - # From now on, lines do not matter anymore, instead everything is - # space seperated - new_parts = (' '.join(lines)).replace(",", " ").split() - - def read_int(): - return int(new_parts.pop(0)) - - def read_float(): - return float(new_parts.pop(0)) - - # Amount of Lamps - if read_int() != 1: - raise InvalidIESProfileException("Only 1 Lamp supported!") - - # Extract various properties - lumen_per_lamp = read_float() # noqa - candela_multiplier = read_float() # noqa - num_vertical_angles = read_int() - num_horizontal_angles = read_int() - - if num_vertical_angles < 1 or num_horizontal_angles < 1: - raise InvalidIESProfileException("Invalid of vertical/horizontal angles!") - - photometric_type = read_int() # noqa - unit_type = read_int() - - # Check for a correct unit type, should be 1 for meters and 2 for feet - if unit_type not in [1, 2]: - raise InvalidIESProfileException("Invalid unit type") - - width = read_float() # noqa - length = read_float() # noqa - height = read_float() # noqa - ballast_factor = read_float() # noqa - future_use = read_float() # Unused field for future usage # noqa - input_watts = read_float() # noqa - - # Read vertical angles - vertical_angles = [read_float() for i in range(num_vertical_angles)] - horizontal_angles = [read_float() for i in range(num_horizontal_angles)] - - candela_values = [] - candela_scale = 0.0 - - for i in range(num_horizontal_angles): - vertical_data = [read_float() for i in range(num_vertical_angles)] - candela_scale = max(candela_scale, max(vertical_data)) - candela_values += vertical_data - - # Rescale values, divide by maximum - candela_values = [i / candela_scale for i in candela_values] - - if len(new_parts) != 0: - self.warn("Unhandled data at file-end left:", new_parts) - - # Dont abort here, some formats like those from ERCO Leuchten GmbH - # have an END keyword, just ignore everything after the data was - # read in. - - dataset = IESDataset() - dataset.set_vertical_angles(self._list_to_pta(vertical_angles)) - dataset.set_horizontal_angles(self._list_to_pta(horizontal_angles)) - dataset.set_candela_values(self._list_to_pta(candela_values)) - - # Testing code to write out the LUT - # from panda3d.core import Texture - # tex = Texture("temp") - # tex.setup_3d_texture(512, 512, 1, Image.T_float, Image.F_r16) - # dataset.generate_dataset_texture_into(tex, 0) - # tex.write("generated.png") - - return dataset - - def _list_to_pta(self, list_values): - """ Converts a list to a PTAFloat """ - pta = PTAFloat.empty_array(len(list_values)) - for i, val in enumerate(list_values): - pta[i] = val - return pta - - def _check_version_header(self, first_line): - """ Checks if the IES version header is correct and the specified IES - version is supported """ - if first_line not in self.PROFILES: - raise InvalidIESProfileException("Unsupported Profile: " + first_line) - - def _extract_keywords(self, lines): - """ Extracts the keywords from a list of lines, and removes all lines - containing keywords """ - keywords = {} - while lines: - line = lines.pop(0) - if not line.startswith("["): - - # Special format used by some IES files, indicates end of properties - # By just checking for the tilt keyword instead of validating each line, - # we can read even malformed lines, like those from ERCO Leuchten GmbH - if line != "TILT=NONE": - continue - - # No keyword, we popped the line already tho, so append it again - lines.insert(0, line) - return keywords - else: - - # Try matching the keywords - match = self.KEYWORD_REGEX.match(line) - if match: - key, val = match.group(1, 2) - keywords[key.strip()] = val.strip() - else: - raise InvalidIESProfileException("Invalid keyword line: " + line) - - return keywords +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +# Disable the unused variable warning - this occurs since we read a lot more +# properties from the IES file than we actually need +# pylint: disable=unused-variable + +from __future__ import print_function + +import re + +from panda3d.core import PTAFloat, Filename, SamplerState, VirtualFileSystem +from panda3d.core import get_model_path +from direct.stdpy.file import open, join, isfile + +from six.moves import range # pylint: disable=import-error + +from rpcore.native import IESDataset +from rpcore.image import Image +from rpcore.rpobject import RPObject + + +class InvalidIESProfileException(Exception): + """ Exception which is thrown when an error occurs during loading an IES + Profile """ + + +class IESProfileLoader(RPObject): + + """ Loader class to load .IES files and create an IESDataset from it. + It generates a LUT for each loaded ies profile which is used by the lighting + pipeline later on. """ + + # Supported IES Profiles + PROFILES = [ + "IESNA:LM-63-1986", + "IESNA:LM-63-1991", + "IESNA91", + "IESNA:LM-63-1995", + "IESNA:LM-63-2002", + "ERCO Leuchten GmbH BY: ERCO/LUM650/8701", + "ERCO Leuchten GmbH" + ] + + # Regexp for extracting keywords + KEYWORD_REGEX = re.compile(r"\[([A-Za-z0-8_-]+)\](.*)") + + def __init__(self, pipeline): + RPObject.__init__(self) + self._pipeline = pipeline + self._entries = [] + self._max_entries = 32 + self._create_storage() + + def _create_storage(self): + """ Internal method to create the storage for the profile dataset textures """ + self._storage_tex = Image.create_3d("IESDatasets", 512, 512, self._max_entries, "R16") + self._storage_tex.set_minfilter(SamplerState.FT_linear) + self._storage_tex.set_magfilter(SamplerState.FT_linear) + self._storage_tex.set_wrap_u(SamplerState.WM_clamp) + self._storage_tex.set_wrap_v(SamplerState.WM_repeat) + self._storage_tex.set_wrap_w(SamplerState.WM_clamp) + + self._pipeline.stage_mgr.inputs["IESDatasetTex"] = self._storage_tex + self._pipeline.stage_mgr.defines["MAX_IES_PROFILES"] = self._max_entries + + def load(self, filename): + """ Loads a profile from a given filename and returns the internal + used index which can be assigned to a light.""" + + # Make sure the user can load profiles directly from the ies profile folder + data_path = join("/$$rp/data/ies_profiles/", filename) + if isfile(data_path): + filename = data_path + + # Make filename unique + fname = Filename.from_os_specific(filename) + if not VirtualFileSystem.get_global_ptr().resolve_filename( + fname, get_model_path().get_value(), "ies"): + self.error("Could not resolve", filename) + return -1 + fname = fname.get_fullpath() + + # Check for cache entries + if fname in self._entries: + return self._entries.index(fname) + + # Check for out of bounds + if len(self._entries) >= self._max_entries: + # TODO: Could remove unused profiles here or regenerate texture + self.warn("Cannot load IES Profile, too many loaded! (Maximum: 32)") + + # Try loading the dataset, and see what happes + try: + dataset = self._load_and_parse_file(fname) + except InvalidIESProfileException as msg: + self.warn("Failed to load profile from", filename, ":", msg) + return -1 + + if not dataset: + return -1 + + # Dataset was loaded successfully, now copy it + dataset.generate_dataset_texture_into(self._storage_tex, len(self._entries)) + self._entries.append(fname) + + return len(self._entries) - 1 + + def _load_and_parse_file(self, pth): + """ Loads a .IES file from a given filename, returns an IESDataset + which is used by the load function later on. """ + self.debug("Loading ies profile from", pth) + + try: + with open(pth, "r") as handle: + lines = handle.readlines() + except IOError as msg: + self.error("Failed to open", pth, ":", msg) + return None + + lines = [i.strip() for i in lines] + + # Parse version header + self._check_version_header(lines.pop(0)) + + # Parse arbitrary amount of keywords + keywords = self._extract_keywords(lines) # noqa + + # Next line should be TILT=NONE according to the spec + if lines.pop(0) != "TILT=NONE": + raise InvalidIESProfileException("Expected TILT=NONE line, but none found!") + + # From now on, lines do not matter anymore, instead everything is + # space seperated + new_parts = (' '.join(lines)).replace(",", " ").split() + + def read_int(): + return int(new_parts.pop(0)) + + def read_float(): + return float(new_parts.pop(0)) + + # Amount of Lamps + if read_int() != 1: + raise InvalidIESProfileException("Only 1 Lamp supported!") + + # Extract various properties + lumen_per_lamp = read_float() # noqa + candela_multiplier = read_float() # noqa + num_vertical_angles = read_int() + num_horizontal_angles = read_int() + + if num_vertical_angles < 1 or num_horizontal_angles < 1: + raise InvalidIESProfileException("Invalid of vertical/horizontal angles!") + + photometric_type = read_int() # noqa + unit_type = read_int() + + # Check for a correct unit type, should be 1 for meters and 2 for feet + if unit_type not in [1, 2]: + raise InvalidIESProfileException("Invalid unit type") + + width = read_float() # noqa + length = read_float() # noqa + height = read_float() # noqa + ballast_factor = read_float() # noqa + future_use = read_float() # Unused field for future usage # noqa + input_watts = read_float() # noqa + + # Read vertical angles + vertical_angles = [read_float() for i in range(num_vertical_angles)] + horizontal_angles = [read_float() for i in range(num_horizontal_angles)] + + candela_values = [] + candela_scale = 0.0 + + for i in range(num_horizontal_angles): + vertical_data = [read_float() for i in range(num_vertical_angles)] + candela_scale = max(candela_scale, max(vertical_data)) + candela_values += vertical_data + + # Rescale values, divide by maximum + candela_values = [i / candela_scale for i in candela_values] + + if len(new_parts) != 0: + self.warn("Unhandled data at file-end left:", new_parts) + + # Dont abort here, some formats like those from ERCO Leuchten GmbH + # have an END keyword, just ignore everything after the data was + # read in. + + dataset = IESDataset() + dataset.set_vertical_angles(self._list_to_pta(vertical_angles)) + dataset.set_horizontal_angles(self._list_to_pta(horizontal_angles)) + dataset.set_candela_values(self._list_to_pta(candela_values)) + + # Testing code to write out the LUT + # from panda3d.core import Texture + # tex = Texture("temp") + # tex.setup_3d_texture(512, 512, 1, Image.T_float, Image.F_r16) + # dataset.generate_dataset_texture_into(tex, 0) + # tex.write("generated.png") + + return dataset + + def _list_to_pta(self, list_values): + """ Converts a list to a PTAFloat """ + pta = PTAFloat.empty_array(len(list_values)) + for i, val in enumerate(list_values): + pta[i] = val + return pta + + def _check_version_header(self, first_line): + """ Checks if the IES version header is correct and the specified IES + version is supported """ + if first_line not in self.PROFILES: + raise InvalidIESProfileException("Unsupported Profile: " + first_line) + + def _extract_keywords(self, lines): + """ Extracts the keywords from a list of lines, and removes all lines + containing keywords """ + keywords = {} + while lines: + line = lines.pop(0) + if not line.startswith("["): + + # Special format used by some IES files, indicates end of properties + # By just checking for the tilt keyword instead of validating each line, + # we can read even malformed lines, like those from ERCO Leuchten GmbH + if line != "TILT=NONE": + continue + + # No keyword, we popped the line already tho, so append it again + lines.insert(0, line) + return keywords + else: + + # Try matching the keywords + match = self.KEYWORD_REGEX.match(line) + if match: + key, val = match.group(1, 2) + keywords[key.strip()] = val.strip() + else: + raise InvalidIESProfileException("Invalid keyword line: " + line) + + return keywords diff --git a/RenderPipelineFile/rpcore/util/movement_controller.py b/RenderPipelineFile/rpcore/util/movement_controller.py index 6fb2f4b9..763f8e5e 100644 --- a/RenderPipelineFile/rpcore/util/movement_controller.py +++ b/RenderPipelineFile/rpcore/util/movement_controller.py @@ -1,298 +1,298 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - - -# Disable the "exactly one space required after comma" message, for the input -# bindings it looks nicer to insert some spaces (see the setup method) -# pylint: disable=bad-whitespace - -from __future__ import print_function - -import sys -import os -import subprocess - -from panda3d.core import ModifierButtons, Vec3, PStatClient -from panda3d.core import Point3, CurveFitter - - -class MovementController(object): - - """ This is a helper class, used to controll the camera and enable various - debugging features. It is not really part of the pipeline, but included to - view the demo scenes. """ - - def __init__(self, showbase): - self.showbase = showbase - self.movement = [0, 0, 0] - self.velocity = Vec3(0.0) - self.hpr_movement = [0, 0] - self.speed = 0.4 - self.initial_position = Vec3(0) - self.initial_destination = Vec3(0) - self.initial_hpr = Vec3(0) - self.mouse_enabled = False - self.last_mouse_pos = [0, 0] - self.mouse_sensivity = 0.7 - self.keyboard_hpr_speed = 0.4 - self.use_hpr = False - self.smoothness = 6.0 - self.bobbing_amount = 0.0 - self.bobbing_speed = 0.5 - - def set_initial_position(self, pos, target): - """ Sets the initial camera position """ - self.initial_position = pos - self.initial_destination = target - self.use_hpr = False - self.reset_to_initial() - - def set_initial_position_hpr(self, pos, hpr): - """ Sets the initial camera position """ - self.initial_position = pos - self.initial_hpr = hpr - self.use_hpr = True - self.reset_to_initial() - - def reset_to_initial(self): - """ Resets the camera to the initial position """ - self.showbase.camera.set_pos(self.initial_position) - - if self.use_hpr: - self.showbase.camera.set_hpr(self.initial_hpr) - else: - self.showbase.camera.look_at( - self.initial_destination.x, self.initial_destination.y, - self.initial_destination.z) - - def set_movement(self, direction, amount): - self.movement[direction] = amount - - def set_hpr_movement(self, direction, amount): - self.hpr_movement[direction] = amount - - def set_mouse_enabled(self, enabled): - self.mouse_enabled = enabled - - def increase_speed(self): - self.speed *= 1.4 - - def decrease_speed(self): - self.speed *= 0.6 - - def unbind(self): - """ Unbinds the movement controler and restores the previous state """ - raise NotImplementedError() - - @property - def clock_obj(self): - """ Returns the global clock object """ - return self.showbase.taskMgr.globalClock - - def setup(self): - """ Attaches the movement controller and inits the keybindings """ - # x - self.showbase.accept("raw-w", self.set_movement, [0, 1]) - self.showbase.accept("raw-w-up", self.set_movement, [0, 0]) - self.showbase.accept("raw-s", self.set_movement, [0, -1]) - self.showbase.accept("raw-s-up", self.set_movement, [0, 0]) - - # y - self.showbase.accept("raw-a", self.set_movement, [1, -1]) - self.showbase.accept("raw-a-up", self.set_movement, [1, 0]) - self.showbase.accept("raw-d", self.set_movement, [1, 1]) - self.showbase.accept("raw-d-up", self.set_movement, [1, 0]) - - # z - self.showbase.accept("space", self.set_movement, [2, 1]) - self.showbase.accept("space-up", self.set_movement, [2, 0]) - self.showbase.accept("shift", self.set_movement, [2, -1]) - self.showbase.accept("shift-up", self.set_movement, [2, 0]) - - # wireframe + debug + buffer viewer - self.showbase.accept("f3", self.showbase.toggle_wireframe) - self.showbase.accept("f11", lambda: self.showbase.win.save_screenshot("screenshot.png")) - self.showbase.accept("j", self.print_position) - - # mouse - self.showbase.accept("mouse3", self.set_mouse_enabled, [True]) - self.showbase.accept("mouse3-up", self.set_mouse_enabled, [False]) - - # arrow mouse navigation - self.showbase.accept("arrow_up", self.set_hpr_movement, [1, 1]) - self.showbase.accept("arrow_up-up", self.set_hpr_movement, [1, 0]) - self.showbase.accept("arrow_down", self.set_hpr_movement, [1, -1]) - self.showbase.accept("arrow_down-up", self.set_hpr_movement, [1, 0]) - self.showbase.accept("arrow_left", self.set_hpr_movement, [0, 1]) - self.showbase.accept("arrow_left-up", self.set_hpr_movement, [0, 0]) - self.showbase.accept("arrow_right", self.set_hpr_movement, [0, -1]) - self.showbase.accept("arrow_right-up", self.set_hpr_movement, [0, 0]) - - # increase / decrease speed - self.showbase.accept("+", self.increase_speed) - self.showbase.accept("-", self.decrease_speed) - - # disable modifier buttons to be able to move while pressing shift for - # example - self.showbase.mouseWatcherNode.set_modifier_buttons(ModifierButtons()) - self.showbase.buttonThrowers[0].node().set_modifier_buttons(ModifierButtons()) - - # disable pandas builtin mouse control - self.showbase.disableMouse() - - # add ourself as an update task which gets executed very early before - # the rendering - self.update_task = self.showbase.addTask( - self.update, "RP_UpdateMovementController", sort=-40) - - # Hotkeys to connect to pstats and reset the initial position - self.showbase.accept("1", PStatClient.connect) - self.showbase.accept("3", self.reset_to_initial) - - def print_position(self): - """ Prints the camera position and hpr """ - pos = self.showbase.cam.get_pos(self.showbase.render) - hpr = self.showbase.cam.get_hpr(self.showbase.render) - print("(Vec3({}, {}, {}), Vec3({}, {}, {})),".format( - pos.x, pos.y, pos.z, hpr.x, hpr.y, hpr.z)) - - def update(self, task): - """ Internal update method """ - - delta = self.clock_obj.get_dt() - - # Update mouse first - if self.showbase.mouseWatcherNode.has_mouse(): - x = self.showbase.mouseWatcherNode.get_mouse_x() - y = self.showbase.mouseWatcherNode.get_mouse_y() - self.current_mouse_pos = (x * self.showbase.camLens.get_fov().x * self.mouse_sensivity, - y * self.showbase.camLens.get_fov().y * self.mouse_sensivity) - - if self.mouse_enabled: - diffx = self.last_mouse_pos[0] - self.current_mouse_pos[0] - diffy = self.last_mouse_pos[1] - self.current_mouse_pos[1] - - # Don't move in the very beginning - if self.last_mouse_pos[0] == 0 and self.last_mouse_pos[1] == 0: - diffx = 0 - diffy = 0 - - self.showbase.camera.set_h(self.showbase.camera.get_h() + diffx) - self.showbase.camera.set_p(self.showbase.camera.get_p() - diffy) - - self.last_mouse_pos = self.current_mouse_pos[:] - - # Compute movement in render space - movement_direction = (Vec3(self.movement[1], self.movement[0], 0) * - self.speed * delta * 100.0) - - # transform by the camera direction - camera_quaternion = self.showbase.camera.get_quat(self.showbase.render) - translated_direction = camera_quaternion.xform(movement_direction) - - # z-force is inddpendent of camera direction - translated_direction.add_z( - self.movement[2] * delta * 120.0 * self.speed) - - self.velocity += translated_direction * 0.15 - - # apply the new position - self.showbase.camera.set_pos(self.showbase.camera.get_pos() + self.velocity) - - # transform rotation (keyboard keys) - rotation_speed = self.keyboard_hpr_speed * 100.0 - rotation_speed *= delta - self.showbase.camera.set_hpr( - self.showbase.camera.get_hpr() + Vec3( - self.hpr_movement[0], self.hpr_movement[1], 0) * rotation_speed) - - # fade out velocity - self.velocity = self.velocity * max( - 0.0, 1.0 - delta * 60.0 / max(0.01, self.smoothness)) - - # bobbing - ftime = self.clock_obj.get_frame_time() - rotation = (ftime % self.bobbing_speed) / self.bobbing_speed - rotation = (min(rotation, 1.0 - rotation) * 2.0 - 0.5) * 2.0 - if self.velocity.length_squared() > 1e-5 and self.speed > 1e-5: - rotation *= self.bobbing_amount - rotation *= min(1, self.velocity.length()) / self.speed * 0.5 - else: - rotation = 0 - self.showbase.camera.set_r(rotation) - return task.cont - - def play_motion_path(self, points, point_duration=1.2): - """ Plays a motion path from the given set of points """ - fitter = CurveFitter() - for i, (pos, hpr) in enumerate(points): - fitter.add_xyz_hpr(i, pos, hpr) - - fitter.compute_tangents(1.0) - curve = fitter.make_hermite() - print("Starting motion path with", len(points), "CVs") - - self.showbase.render2d.hide() - self.showbase.aspect2d.hide() - - self.curve = curve - self.curve_time_start = self.clock_obj.get_frame_time() - self.curve_time_end = self.clock_obj.get_frame_time() + len(points) * point_duration - self.delta_time_sum = 0.0 - self.delta_time_count = 0 - self.showbase.addTask(self.camera_motion_update, "RP_CameraMotionPath", sort=-50) - self.showbase.taskMgr.remove(self.update_task) - - def camera_motion_update(self, task): - if self.clock_obj.get_frame_time() > self.curve_time_end: - print("Camera motion path finished") - - # Print performance stats - avg_ms = self.delta_time_sum / self.delta_time_count - print("Average frame time (ms): {:4.1f}".format(avg_ms * 1000.0)) - print("Average frame rate: {:4.1f}".format(1.0 / avg_ms)) - - self.update_task = self.showbase.addTask( - self.update, "RP_UpdateMovementController", sort=-50) - self.showbase.render2d.show() - self.showbase.aspect2d.show() - return task.done - - lerp = (self.clock_obj.get_frame_time() - self.curve_time_start) /\ - (self.curve_time_end - self.curve_time_start) - lerp *= self.curve.get_max_t() - - pos, hpr = Point3(0), Vec3(0) - self.curve.evaluate_xyz(lerp, pos) - self.curve.evaluate_hpr(lerp, hpr) - - self.showbase.camera.set_pos(pos) - self.showbase.camera.set_hpr(hpr) - - self.delta_time_sum += self.clock_obj.get_dt() - self.delta_time_count += 1 - - return task.cont +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + + +# Disable the "exactly one space required after comma" message, for the input +# bindings it looks nicer to insert some spaces (see the setup method) +# pylint: disable=bad-whitespace + +from __future__ import print_function + +import sys +import os +import subprocess + +from panda3d.core import ModifierButtons, Vec3, PStatClient +from panda3d.core import Point3, CurveFitter + + +class MovementController(object): + + """ This is a helper class, used to controll the camera and enable various + debugging features. It is not really part of the pipeline, but included to + view the demo scenes. """ + + def __init__(self, showbase): + self.showbase = showbase + self.movement = [0, 0, 0] + self.velocity = Vec3(0.0) + self.hpr_movement = [0, 0] + self.speed = 0.4 + self.initial_position = Vec3(0) + self.initial_destination = Vec3(0) + self.initial_hpr = Vec3(0) + self.mouse_enabled = False + self.last_mouse_pos = [0, 0] + self.mouse_sensivity = 0.7 + self.keyboard_hpr_speed = 0.4 + self.use_hpr = False + self.smoothness = 6.0 + self.bobbing_amount = 0.0 + self.bobbing_speed = 0.5 + + def set_initial_position(self, pos, target): + """ Sets the initial camera position """ + self.initial_position = pos + self.initial_destination = target + self.use_hpr = False + self.reset_to_initial() + + def set_initial_position_hpr(self, pos, hpr): + """ Sets the initial camera position """ + self.initial_position = pos + self.initial_hpr = hpr + self.use_hpr = True + self.reset_to_initial() + + def reset_to_initial(self): + """ Resets the camera to the initial position """ + self.showbase.camera.set_pos(self.initial_position) + + if self.use_hpr: + self.showbase.camera.set_hpr(self.initial_hpr) + else: + self.showbase.camera.look_at( + self.initial_destination.x, self.initial_destination.y, + self.initial_destination.z) + + def set_movement(self, direction, amount): + self.movement[direction] = amount + + def set_hpr_movement(self, direction, amount): + self.hpr_movement[direction] = amount + + def set_mouse_enabled(self, enabled): + self.mouse_enabled = enabled + + def increase_speed(self): + self.speed *= 1.4 + + def decrease_speed(self): + self.speed *= 0.6 + + def unbind(self): + """ Unbinds the movement controler and restores the previous state """ + raise NotImplementedError() + + @property + def clock_obj(self): + """ Returns the global clock object """ + return self.showbase.taskMgr.globalClock + + def setup(self): + """ Attaches the movement controller and inits the keybindings """ + # x + self.showbase.accept("raw-w", self.set_movement, [0, 1]) + self.showbase.accept("raw-w-up", self.set_movement, [0, 0]) + self.showbase.accept("raw-s", self.set_movement, [0, -1]) + self.showbase.accept("raw-s-up", self.set_movement, [0, 0]) + + # y + self.showbase.accept("raw-a", self.set_movement, [1, -1]) + self.showbase.accept("raw-a-up", self.set_movement, [1, 0]) + self.showbase.accept("raw-d", self.set_movement, [1, 1]) + self.showbase.accept("raw-d-up", self.set_movement, [1, 0]) + + # z + self.showbase.accept("space", self.set_movement, [2, 1]) + self.showbase.accept("space-up", self.set_movement, [2, 0]) + self.showbase.accept("shift", self.set_movement, [2, -1]) + self.showbase.accept("shift-up", self.set_movement, [2, 0]) + + # wireframe + debug + buffer viewer + self.showbase.accept("f3", self.showbase.toggle_wireframe) + self.showbase.accept("f11", lambda: self.showbase.win.save_screenshot("screenshot.png")) + self.showbase.accept("j", self.print_position) + + # mouse + self.showbase.accept("mouse3", self.set_mouse_enabled, [True]) + self.showbase.accept("mouse3-up", self.set_mouse_enabled, [False]) + + # arrow mouse navigation + self.showbase.accept("arrow_up", self.set_hpr_movement, [1, 1]) + self.showbase.accept("arrow_up-up", self.set_hpr_movement, [1, 0]) + self.showbase.accept("arrow_down", self.set_hpr_movement, [1, -1]) + self.showbase.accept("arrow_down-up", self.set_hpr_movement, [1, 0]) + self.showbase.accept("arrow_left", self.set_hpr_movement, [0, 1]) + self.showbase.accept("arrow_left-up", self.set_hpr_movement, [0, 0]) + self.showbase.accept("arrow_right", self.set_hpr_movement, [0, -1]) + self.showbase.accept("arrow_right-up", self.set_hpr_movement, [0, 0]) + + # increase / decrease speed + self.showbase.accept("+", self.increase_speed) + self.showbase.accept("-", self.decrease_speed) + + # disable modifier buttons to be able to move while pressing shift for + # example + self.showbase.mouseWatcherNode.set_modifier_buttons(ModifierButtons()) + self.showbase.buttonThrowers[0].node().set_modifier_buttons(ModifierButtons()) + + # disable pandas builtin mouse control + self.showbase.disableMouse() + + # add ourself as an update task which gets executed very early before + # the rendering + self.update_task = self.showbase.addTask( + self.update, "RP_UpdateMovementController", sort=-40) + + # Hotkeys to connect to pstats and reset the initial position + self.showbase.accept("1", PStatClient.connect) + self.showbase.accept("3", self.reset_to_initial) + + def print_position(self): + """ Prints the camera position and hpr """ + pos = self.showbase.cam.get_pos(self.showbase.render) + hpr = self.showbase.cam.get_hpr(self.showbase.render) + print("(Vec3({}, {}, {}), Vec3({}, {}, {})),".format( + pos.x, pos.y, pos.z, hpr.x, hpr.y, hpr.z)) + + def update(self, task): + """ Internal update method """ + + delta = self.clock_obj.get_dt() + + # Update mouse first + if self.showbase.mouseWatcherNode.has_mouse(): + x = self.showbase.mouseWatcherNode.get_mouse_x() + y = self.showbase.mouseWatcherNode.get_mouse_y() + self.current_mouse_pos = (x * self.showbase.camLens.get_fov().x * self.mouse_sensivity, + y * self.showbase.camLens.get_fov().y * self.mouse_sensivity) + + if self.mouse_enabled: + diffx = self.last_mouse_pos[0] - self.current_mouse_pos[0] + diffy = self.last_mouse_pos[1] - self.current_mouse_pos[1] + + # Don't move in the very beginning + if self.last_mouse_pos[0] == 0 and self.last_mouse_pos[1] == 0: + diffx = 0 + diffy = 0 + + self.showbase.camera.set_h(self.showbase.camera.get_h() + diffx) + self.showbase.camera.set_p(self.showbase.camera.get_p() - diffy) + + self.last_mouse_pos = self.current_mouse_pos[:] + + # Compute movement in render space + movement_direction = (Vec3(self.movement[1], self.movement[0], 0) * + self.speed * delta * 100.0) + + # transform by the camera direction + camera_quaternion = self.showbase.camera.get_quat(self.showbase.render) + translated_direction = camera_quaternion.xform(movement_direction) + + # z-force is inddpendent of camera direction + translated_direction.add_z( + self.movement[2] * delta * 120.0 * self.speed) + + self.velocity += translated_direction * 0.15 + + # apply the new position + self.showbase.camera.set_pos(self.showbase.camera.get_pos() + self.velocity) + + # transform rotation (keyboard keys) + rotation_speed = self.keyboard_hpr_speed * 100.0 + rotation_speed *= delta + self.showbase.camera.set_hpr( + self.showbase.camera.get_hpr() + Vec3( + self.hpr_movement[0], self.hpr_movement[1], 0) * rotation_speed) + + # fade out velocity + self.velocity = self.velocity * max( + 0.0, 1.0 - delta * 60.0 / max(0.01, self.smoothness)) + + # bobbing + ftime = self.clock_obj.get_frame_time() + rotation = (ftime % self.bobbing_speed) / self.bobbing_speed + rotation = (min(rotation, 1.0 - rotation) * 2.0 - 0.5) * 2.0 + if self.velocity.length_squared() > 1e-5 and self.speed > 1e-5: + rotation *= self.bobbing_amount + rotation *= min(1, self.velocity.length()) / self.speed * 0.5 + else: + rotation = 0 + self.showbase.camera.set_r(rotation) + return task.cont + + def play_motion_path(self, points, point_duration=1.2): + """ Plays a motion path from the given set of points """ + fitter = CurveFitter() + for i, (pos, hpr) in enumerate(points): + fitter.add_xyz_hpr(i, pos, hpr) + + fitter.compute_tangents(1.0) + curve = fitter.make_hermite() + print("Starting motion path with", len(points), "CVs") + + self.showbase.render2d.hide() + self.showbase.aspect2d.hide() + + self.curve = curve + self.curve_time_start = self.clock_obj.get_frame_time() + self.curve_time_end = self.clock_obj.get_frame_time() + len(points) * point_duration + self.delta_time_sum = 0.0 + self.delta_time_count = 0 + self.showbase.addTask(self.camera_motion_update, "RP_CameraMotionPath", sort=-50) + self.showbase.taskMgr.remove(self.update_task) + + def camera_motion_update(self, task): + if self.clock_obj.get_frame_time() > self.curve_time_end: + print("Camera motion path finished") + + # Print performance stats + avg_ms = self.delta_time_sum / self.delta_time_count + print("Average frame time (ms): {:4.1f}".format(avg_ms * 1000.0)) + print("Average frame rate: {:4.1f}".format(1.0 / avg_ms)) + + self.update_task = self.showbase.addTask( + self.update, "RP_UpdateMovementController", sort=-50) + self.showbase.render2d.show() + self.showbase.aspect2d.show() + return task.done + + lerp = (self.clock_obj.get_frame_time() - self.curve_time_start) /\ + (self.curve_time_end - self.curve_time_start) + lerp *= self.curve.get_max_t() + + pos, hpr = Point3(0), Vec3(0) + self.curve.evaluate_xyz(lerp, pos) + self.curve.evaluate_hpr(lerp, hpr) + + self.showbase.camera.set_pos(pos) + self.showbase.camera.set_hpr(hpr) + + self.delta_time_sum += self.clock_obj.get_dt() + self.delta_time_count += 1 + + return task.cont diff --git a/RenderPipelineFile/rpcore/util/network_communication.py b/RenderPipelineFile/rpcore/util/network_communication.py index cb1b65f1..0d4665de 100644 --- a/RenderPipelineFile/rpcore/util/network_communication.py +++ b/RenderPipelineFile/rpcore/util/network_communication.py @@ -1,161 +1,161 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -import socket -from threading import Thread - -from rpcore.rpobject import RPObject - - -class NetworkCommunication(RPObject): - - """ Listener which accepts messages on several ports to detect incoming updates. - Also provides functionality to send updates. """ - - CONFIG_PORT = 63324 - DAYTIME_PORT = 63325 - MATERIAL_PORT = 63326 - - @classmethod - def send_async(cls, port, message): - """ Starts a new thread which sends a given message to a port """ - thread = Thread(target=cls.__send_message_async, args=(port, message), - name="NC-SendAsync") - thread.setDaemon(True) - thread.start() - return thread - - @classmethod - def listen_threaded(cls, port, callback): - """ Starts a new thread listening to the given port """ - thread = Thread(target=cls.__listen_forever, args=(port, callback), - name="NC-ListenForever") - thread.setDaemon(True) - thread.start() - return thread - - @staticmethod - def __send_message_async(port, message=""): - """ Sends a given message to a given port and immediately returns. """ - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - try: - sock.sendto(message.encode("utf-8"), ("127.0.0.1", port)) - finally: - sock.close() - - @staticmethod - def __listen_forever(port, callback): - """ Listens to a given port, and calls callback in case a message - arrives. This method never returns, except when the connection closed or - could not be established. """ - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - try: - sock.bind(("127.0.0.1", port)) - while True: - data, addr = sock.recvfrom(1024) # pylint: disable=unused-variable - callback(data.decode("utf-8")) - finally: - sock.close() - - def __init__(self, pipeline): - """ Creates the listener service. This also starts listening on the various - ports for updates """ - RPObject.__init__(self) - self._pipeline = pipeline - self._config_updates = set() - self._daytime_updates = set() - self._material_updates = set() - self._config_thread = self.listen_threaded( - self.CONFIG_PORT, self._config_updates.add) - self._daytime_thread = self.listen_threaded( - self.DAYTIME_PORT, self._daytime_updates.add) - self._material_thread = self.listen_threaded( - self.MATERIAL_PORT, self._material_updates.add) - - - def update(self): - """ Update task which gets called every frame and executes the changes. - This takes the incoming scheduled commands and processes one at a time.""" - while self._config_updates: - cmd = self._config_updates.pop() - self._handle_config_command(cmd) - while self._daytime_updates: - cmd = self._daytime_updates.pop() - self._handle_daytime_command(cmd) - while self._material_updates: - cmd = self._material_updates.pop() - self._handle_material_command(cmd) - - def _handle_daytime_command(self, cmd): - """ Handles a daytime command. This could either be a command to set - the time, or a command to reload the time of day configuration. """ - if cmd.startswith("settime "): - daytime = float(cmd.split()[1]) - self._pipeline.daytime_mgr.time = daytime - elif cmd.startswith("loadconf"): - self._pipeline.plugin_mgr.load_daytime_overrides( - "/$$rpconfig/daytime.yaml") - else: - self.warn("Recieved unkown daytime command:", cmd) - - def _handle_config_command(self, cmd): - """ Handles an incomming configuration command. Currently this can only - be an update of a plugin setting """ - if cmd.startswith("setval "): - parts = cmd.split() - if len(parts) < 3: - self.warn("Invalid setval command format. Expected: setval plugin.setting value") - return - - setting_parts = parts[1].split(".") - if len(setting_parts) < 2: - self.warn("Invalid setting format. Expected: plugin.setting") - return - - try: - self._pipeline.plugin_mgr.on_setting_changed( - setting_parts[0], setting_parts[1], parts[2]) - except Exception as e: - self.warn("Error handling setting change:", str(e)) - else: - self.warn("Recieved unkown plugin command:", cmd) - - def _handle_material_command(self, cmd): - """ Handles an incomming material command """ - if cmd.startswith("dump_materials"): - path = cmd[len("dump_materials "):].strip() - self.debug("Writing materials to", path) - self._pipeline.export_materials(path) - - elif cmd.startswith("update_material"): - - data = cmd[len("update_material "):].strip() - parts = data.split() - self._pipeline.update_serialized_material(parts) - - else: - self.warn("Recieved unkown plugin command:", cmd) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +import socket +from threading import Thread + +from rpcore.rpobject import RPObject + + +class NetworkCommunication(RPObject): + + """ Listener which accepts messages on several ports to detect incoming updates. + Also provides functionality to send updates. """ + + CONFIG_PORT = 63324 + DAYTIME_PORT = 63325 + MATERIAL_PORT = 63326 + + @classmethod + def send_async(cls, port, message): + """ Starts a new thread which sends a given message to a port """ + thread = Thread(target=cls.__send_message_async, args=(port, message), + name="NC-SendAsync") + thread.setDaemon(True) + thread.start() + return thread + + @classmethod + def listen_threaded(cls, port, callback): + """ Starts a new thread listening to the given port """ + thread = Thread(target=cls.__listen_forever, args=(port, callback), + name="NC-ListenForever") + thread.setDaemon(True) + thread.start() + return thread + + @staticmethod + def __send_message_async(port, message=""): + """ Sends a given message to a given port and immediately returns. """ + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + try: + sock.sendto(message.encode("utf-8"), ("127.0.0.1", port)) + finally: + sock.close() + + @staticmethod + def __listen_forever(port, callback): + """ Listens to a given port, and calls callback in case a message + arrives. This method never returns, except when the connection closed or + could not be established. """ + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + sock.bind(("127.0.0.1", port)) + while True: + data, addr = sock.recvfrom(1024) # pylint: disable=unused-variable + callback(data.decode("utf-8")) + finally: + sock.close() + + def __init__(self, pipeline): + """ Creates the listener service. This also starts listening on the various + ports for updates """ + RPObject.__init__(self) + self._pipeline = pipeline + self._config_updates = set() + self._daytime_updates = set() + self._material_updates = set() + self._config_thread = self.listen_threaded( + self.CONFIG_PORT, self._config_updates.add) + self._daytime_thread = self.listen_threaded( + self.DAYTIME_PORT, self._daytime_updates.add) + self._material_thread = self.listen_threaded( + self.MATERIAL_PORT, self._material_updates.add) + + + def update(self): + """ Update task which gets called every frame and executes the changes. + This takes the incoming scheduled commands and processes one at a time.""" + while self._config_updates: + cmd = self._config_updates.pop() + self._handle_config_command(cmd) + while self._daytime_updates: + cmd = self._daytime_updates.pop() + self._handle_daytime_command(cmd) + while self._material_updates: + cmd = self._material_updates.pop() + self._handle_material_command(cmd) + + def _handle_daytime_command(self, cmd): + """ Handles a daytime command. This could either be a command to set + the time, or a command to reload the time of day configuration. """ + if cmd.startswith("settime "): + daytime = float(cmd.split()[1]) + self._pipeline.daytime_mgr.time = daytime + elif cmd.startswith("loadconf"): + self._pipeline.plugin_mgr.load_daytime_overrides( + "/$$rpconfig/daytime.yaml") + else: + self.warn("Recieved unkown daytime command:", cmd) + + def _handle_config_command(self, cmd): + """ Handles an incomming configuration command. Currently this can only + be an update of a plugin setting """ + if cmd.startswith("setval "): + parts = cmd.split() + if len(parts) < 3: + self.warn("Invalid setval command format. Expected: setval plugin.setting value") + return + + setting_parts = parts[1].split(".") + if len(setting_parts) < 2: + self.warn("Invalid setting format. Expected: plugin.setting") + return + + try: + self._pipeline.plugin_mgr.on_setting_changed( + setting_parts[0], setting_parts[1], parts[2]) + except Exception as e: + self.warn("Error handling setting change:", str(e)) + else: + self.warn("Recieved unkown plugin command:", cmd) + + def _handle_material_command(self, cmd): + """ Handles an incomming material command """ + if cmd.startswith("dump_materials"): + path = cmd[len("dump_materials "):].strip() + self.debug("Writing materials to", path) + self._pipeline.export_materials(path) + + elif cmd.startswith("update_material"): + + data = cmd[len("update_material "):].strip() + parts = data.split() + self._pipeline.update_serialized_material(parts) + + else: + self.warn("Recieved unkown plugin command:", cmd) diff --git a/RenderPipelineFile/rpcore/util/post_process_region.py b/RenderPipelineFile/rpcore/util/post_process_region.py index f6864881..44a0fab2 100644 --- a/RenderPipelineFile/rpcore/util/post_process_region.py +++ b/RenderPipelineFile/rpcore/util/post_process_region.py @@ -1,110 +1,110 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.rpobject import RPObject - -from panda3d.core import GeomVertexFormat, GeomVertexData, Geom, GeomVertexWriter -from panda3d.core import GeomTriangles, GeomNode, NodePath, Camera, OmniBoundingVolume -from panda3d.core import TransparencyAttrib, OrthographicLens, Vec4 - - -class PostProcessRegion(RPObject): - - """ Simple wrapper class to create fullscreen triangles and quads """ - - @classmethod - def make(cls, internal_buffer, *args): - return cls(internal_buffer, *args) - - def __init__(self, internal_buffer, *args): - RPObject.__init__(self) - self._buffer = internal_buffer - self._region = self._buffer.make_display_region(*args) - self._node = NodePath("RTRoot") - - self._make_fullscreen_tri() - self._make_fullscreen_cam() - self._init_function_pointers() - - def _init_function_pointers(self): - self.set_sort = self._region.set_sort - self.set_instance_count = self._tri.set_instance_count - self.disable_clears = self._region.disable_clears - self.set_active = self._region.set_active - self.set_clear_depth_active = self._region.set_clear_depth_active - self.set_clear_depth = self._region.set_clear_depth - self.set_shader = self._tri.set_shader - self.set_camera = self._region.set_camera - self.set_clear_color_active = self._region.set_clear_color_active - self.set_clear_color = self._region.set_clear_color - self.set_attrib = self._tri.set_attrib - - def _make_fullscreen_tri(self): - """ Creates the oversized triangle used for rendering """ - vformat = GeomVertexFormat.get_v3() - vdata = GeomVertexData("vertices", vformat, Geom.UH_static) - vdata.set_num_rows(3) - vwriter = GeomVertexWriter(vdata, "vertex") - vwriter.add_data3f(-1, 0, -1) - vwriter.add_data3f(3, 0, -1) - vwriter.add_data3f(-1, 0, 3) - gtris = GeomTriangles(Geom.UH_static) - gtris.add_next_vertices(3) - geom = Geom(vdata) - geom.add_primitive(gtris) - geom_node = GeomNode("gn") - geom_node.add_geom(geom) - geom_node.set_final(True) - geom_node.set_bounds(OmniBoundingVolume()) - tri = NodePath(geom_node) - tri.set_depth_test(False) - tri.set_depth_write(False) - tri.set_attrib(TransparencyAttrib.make(TransparencyAttrib.M_none), 10000) - tri.set_color(Vec4(1)) - tri.set_bin("unsorted", 10) - tri.reparent_to(self._node) - self._tri = tri - - def set_shader_input(self, *args, **kwargs): - if kwargs.get("override", False): - self._node.set_shader_input(*args, priority=100000) - else: - self._tri.set_shader_input(*args) - - def set_shader_inputs(self, **kwargs): - self._tri.set_shader_inputs(**kwargs) - - def _make_fullscreen_cam(self): - """ Creates an orthographic camera for the buffer """ - buffer_cam = Camera("BufferCamera") - lens = OrthographicLens() - lens.set_film_size(2, 2) - lens.set_film_offset(0, 0) - lens.set_near_far(-100, 100) - buffer_cam.set_lens(lens) - buffer_cam.set_cull_bounds(OmniBoundingVolume()) - self._camera = self._node.attach_new_node(buffer_cam) - self._region.set_camera(self._camera) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.rpobject import RPObject + +from panda3d.core import GeomVertexFormat, GeomVertexData, Geom, GeomVertexWriter +from panda3d.core import GeomTriangles, GeomNode, NodePath, Camera, OmniBoundingVolume +from panda3d.core import TransparencyAttrib, OrthographicLens, Vec4 + + +class PostProcessRegion(RPObject): + + """ Simple wrapper class to create fullscreen triangles and quads """ + + @classmethod + def make(cls, internal_buffer, *args): + return cls(internal_buffer, *args) + + def __init__(self, internal_buffer, *args): + RPObject.__init__(self) + self._buffer = internal_buffer + self._region = self._buffer.make_display_region(*args) + self._node = NodePath("RTRoot") + + self._make_fullscreen_tri() + self._make_fullscreen_cam() + self._init_function_pointers() + + def _init_function_pointers(self): + self.set_sort = self._region.set_sort + self.set_instance_count = self._tri.set_instance_count + self.disable_clears = self._region.disable_clears + self.set_active = self._region.set_active + self.set_clear_depth_active = self._region.set_clear_depth_active + self.set_clear_depth = self._region.set_clear_depth + self.set_shader = self._tri.set_shader + self.set_camera = self._region.set_camera + self.set_clear_color_active = self._region.set_clear_color_active + self.set_clear_color = self._region.set_clear_color + self.set_attrib = self._tri.set_attrib + + def _make_fullscreen_tri(self): + """ Creates the oversized triangle used for rendering """ + vformat = GeomVertexFormat.get_v3() + vdata = GeomVertexData("vertices", vformat, Geom.UH_static) + vdata.set_num_rows(3) + vwriter = GeomVertexWriter(vdata, "vertex") + vwriter.add_data3f(-1, 0, -1) + vwriter.add_data3f(3, 0, -1) + vwriter.add_data3f(-1, 0, 3) + gtris = GeomTriangles(Geom.UH_static) + gtris.add_next_vertices(3) + geom = Geom(vdata) + geom.add_primitive(gtris) + geom_node = GeomNode("gn") + geom_node.add_geom(geom) + geom_node.set_final(True) + geom_node.set_bounds(OmniBoundingVolume()) + tri = NodePath(geom_node) + tri.set_depth_test(False) + tri.set_depth_write(False) + tri.set_attrib(TransparencyAttrib.make(TransparencyAttrib.M_none), 10000) + tri.set_color(Vec4(1)) + tri.set_bin("unsorted", 10) + tri.reparent_to(self._node) + self._tri = tri + + def set_shader_input(self, *args, **kwargs): + if kwargs.get("override", False): + self._node.set_shader_input(*args, priority=100000) + else: + self._tri.set_shader_input(*args) + + def set_shader_inputs(self, **kwargs): + self._tri.set_shader_inputs(**kwargs) + + def _make_fullscreen_cam(self): + """ Creates an orthographic camera for the buffer """ + buffer_cam = Camera("BufferCamera") + lens = OrthographicLens() + lens.set_film_size(2, 2) + lens.set_film_offset(0, 0) + lens.set_near_far(-100, 100) + buffer_cam.set_lens(lens) + buffer_cam.set_cull_bounds(OmniBoundingVolume()) + self._camera = self._node.attach_new_node(buffer_cam) + self._region.set_camera(self._camera) diff --git a/RenderPipelineFile/rpcore/util/shader_input_blocks.py b/RenderPipelineFile/rpcore/util/shader_input_blocks.py index 44675bf9..20d63cdb 100644 --- a/RenderPipelineFile/rpcore/util/shader_input_blocks.py +++ b/RenderPipelineFile/rpcore/util/shader_input_blocks.py @@ -1,191 +1,191 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" -from rplibs.six import iteritems - -from panda3d.core import PTAFloat, PTALVecBase3f, PTALMatrix4f, PTALVecBase2f -from panda3d.core import PTALVecBase4f, PTALMatrix3f, PTAInt, TypeRegistry, PTALVecBase2i - -from rpcore.rpobject import RPObject - - -class SimpleInputBlock(RPObject): - - """ Simplest possible uniform buffer which just stores a set of values. - This does not use any fancy uniform buffer objects under the hood, and - instead just sets every value as a shader input. """ - - def __init__(self, name): - """ Creates the ubo with the given name """ - RPObject.__init__(self) - self._inputs = {} - self.name = name - - def add_input(self, name, value): - """ Adds a new input to the UBO """ - self._inputs[self.name + "." + name] = value - - def bind_to(self, target): - """ Binds the UBO to a target """ - target.set_shader_inputs(**self._inputs) - - -class GroupedInputBlock(RPObject): - - """ Grouped uniform buffer which either uses PointerToArray's to efficiently - store and update the shader inputs, or in case of uniform buffer object (UBO) - support, uses these to pass the inputs to the shaders. """ - - # Keeps track of the global allocated input blocks to be able to assign - # a unique binding to all of them - UBO_BINDING_INDEX = 0 - - # Mapping of the pta-types to glsl types, and vice versa - PTA_MAPPINGS = { - PTAInt: "int", - PTAFloat: "float", - PTALVecBase2f: "vec2", - PTALVecBase2i: "ivec2", - PTALVecBase3f: "vec3", - PTALVecBase4f: "vec4", - PTALMatrix3f: "mat3", - PTALMatrix4f: "mat4", - } - - def __init__(self, name): - """ Constructs the input block with a given name """ - RPObject.__init__(self) - self.ptas = {} - self._inputs = {} - self.name = name - self.use_ubo = bool(TypeRegistry.ptr().find_type("GLUniformBufferContext")) - - # Acquire a unique index for each UBO to store its binding - self.bind_id = GroupedInputBlock.UBO_BINDING_INDEX - GroupedInputBlock.UBO_BINDING_INDEX += 1 - - if self.bind_id == 0: - # Only output the bind support debug output once (for the first ubo) - self.debug("Native UBO support =", self.use_ubo) - - def register_pta(self, name, input_type): - """ Registers a new input, type should be a glsl type """ - pta = self.glsl_type_to_pta(input_type).empty_array(1) - self.ptas[name] = pta - if self.use_ubo: - self._inputs[self.name + "_UBO." + name] = pta - else: - self._inputs[self.name + "." + name] = pta - - def pta_to_glsl_type(self, pta_handle): - """ Converts a PtaXXX to a glsl type """ - for pta_type, glsl_type in iteritems(GroupedInputBlock.PTA_MAPPINGS): - if isinstance(pta_handle, pta_type): - return glsl_type - self.error("Unrecognized PTA type:", pta_handle) - - def glsl_type_to_pta(self, glsl_type): - """ Converts a glsl type to a PtaXXX type """ - for key, val in iteritems(GroupedInputBlock.PTA_MAPPINGS): - if val == glsl_type: - return key - self.error("Could not resolve GLSL type:", glsl_type) - - def bind_to(self, target): - """ Binds all inputs of this UBO to the given target, which may be - either a RenderTarget or a NodePath """ - - target.set_shader_inputs(**self._inputs) - - def update_input(self, name, value): - """ Updates an existing input """ - self.ptas[name][0] = value - - def get_input(self, name): - """ Returns the value of an existing input """ - return self.ptas[name][0] - - def generate_shader_code(self): # pylint: disable=too-many-branches - """ Generates the GLSL shader code to use the UBO """ - - content = "#pragma once\n\n" - content += "// Autogenerated by the render pipeline\n" - content += "// Do not edit! Your changes will be lost.\n\n" - - structs = {} - inputs = [] - - for input_name, handle in iteritems(self.ptas): - parts = input_name.split(".") - - # Single input, simply add it to the input list - if len(parts) == 1: - inputs.append(self.pta_to_glsl_type(handle) + " " + input_name + ";") - - # Nested input, like scattering.sun_color - elif len(parts) == 2: - struct_name = parts[0] - actual_input_name = parts[1] - if struct_name in structs: - # Struct is already defined, add member definition - structs[struct_name].append( - self.pta_to_glsl_type(handle) + " " + actual_input_name + ";") - else: - # Construct a new struct and add it to the list of inputs - inputs.append(struct_name + "_UBOSTRUCT " + struct_name + ";") - structs[struct_name] = [ - self.pta_to_glsl_type(handle) + " " + actual_input_name + ";" - ] - - # Nested input, like scattering.some_setting.sun_color, not supported yet - else: - self.warn("Structure definition too nested, not supported (yet):", input_name) - - # Add structures - for struct_name, members in iteritems(structs): - content += "struct " + struct_name + "_UBOSTRUCT {\n" - for member in members: - content += " " * 4 + member + "\n" - content += "};\n\n" - - # Add actual inputs - if len(inputs) < 1: - self.debug("No UBO inputs present for", self.name) - else: - if self.use_ubo: - - content += "layout(shared, binding={}) uniform {}_UBO {{\n".format( - self.bind_id, self.name) - for ipt in inputs: - content += " " * 4 + ipt + "\n" - content += "} " + self.name + ";\n" - else: - content += "uniform struct {\n" - for ipt in inputs: - content += " " * 4 + ipt + "\n" - content += "} " + self.name + ";\n" - - content += "\n" - return content +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +from rplibs.six import iteritems + +from panda3d.core import PTAFloat, PTALVecBase3f, PTALMatrix4f, PTALVecBase2f +from panda3d.core import PTALVecBase4f, PTALMatrix3f, PTAInt, TypeRegistry, PTALVecBase2i + +from rpcore.rpobject import RPObject + + +class SimpleInputBlock(RPObject): + + """ Simplest possible uniform buffer which just stores a set of values. + This does not use any fancy uniform buffer objects under the hood, and + instead just sets every value as a shader input. """ + + def __init__(self, name): + """ Creates the ubo with the given name """ + RPObject.__init__(self) + self._inputs = {} + self.name = name + + def add_input(self, name, value): + """ Adds a new input to the UBO """ + self._inputs[self.name + "." + name] = value + + def bind_to(self, target): + """ Binds the UBO to a target """ + target.set_shader_inputs(**self._inputs) + + +class GroupedInputBlock(RPObject): + + """ Grouped uniform buffer which either uses PointerToArray's to efficiently + store and update the shader inputs, or in case of uniform buffer object (UBO) + support, uses these to pass the inputs to the shaders. """ + + # Keeps track of the global allocated input blocks to be able to assign + # a unique binding to all of them + UBO_BINDING_INDEX = 0 + + # Mapping of the pta-types to glsl types, and vice versa + PTA_MAPPINGS = { + PTAInt: "int", + PTAFloat: "float", + PTALVecBase2f: "vec2", + PTALVecBase2i: "ivec2", + PTALVecBase3f: "vec3", + PTALVecBase4f: "vec4", + PTALMatrix3f: "mat3", + PTALMatrix4f: "mat4", + } + + def __init__(self, name): + """ Constructs the input block with a given name """ + RPObject.__init__(self) + self.ptas = {} + self._inputs = {} + self.name = name + self.use_ubo = bool(TypeRegistry.ptr().find_type("GLUniformBufferContext")) + + # Acquire a unique index for each UBO to store its binding + self.bind_id = GroupedInputBlock.UBO_BINDING_INDEX + GroupedInputBlock.UBO_BINDING_INDEX += 1 + + if self.bind_id == 0: + # Only output the bind support debug output once (for the first ubo) + self.debug("Native UBO support =", self.use_ubo) + + def register_pta(self, name, input_type): + """ Registers a new input, type should be a glsl type """ + pta = self.glsl_type_to_pta(input_type).empty_array(1) + self.ptas[name] = pta + if self.use_ubo: + self._inputs[self.name + "_UBO." + name] = pta + else: + self._inputs[self.name + "." + name] = pta + + def pta_to_glsl_type(self, pta_handle): + """ Converts a PtaXXX to a glsl type """ + for pta_type, glsl_type in iteritems(GroupedInputBlock.PTA_MAPPINGS): + if isinstance(pta_handle, pta_type): + return glsl_type + self.error("Unrecognized PTA type:", pta_handle) + + def glsl_type_to_pta(self, glsl_type): + """ Converts a glsl type to a PtaXXX type """ + for key, val in iteritems(GroupedInputBlock.PTA_MAPPINGS): + if val == glsl_type: + return key + self.error("Could not resolve GLSL type:", glsl_type) + + def bind_to(self, target): + """ Binds all inputs of this UBO to the given target, which may be + either a RenderTarget or a NodePath """ + + target.set_shader_inputs(**self._inputs) + + def update_input(self, name, value): + """ Updates an existing input """ + self.ptas[name][0] = value + + def get_input(self, name): + """ Returns the value of an existing input """ + return self.ptas[name][0] + + def generate_shader_code(self): # pylint: disable=too-many-branches + """ Generates the GLSL shader code to use the UBO """ + + content = "#pragma once\n\n" + content += "// Autogenerated by the render pipeline\n" + content += "// Do not edit! Your changes will be lost.\n\n" + + structs = {} + inputs = [] + + for input_name, handle in iteritems(self.ptas): + parts = input_name.split(".") + + # Single input, simply add it to the input list + if len(parts) == 1: + inputs.append(self.pta_to_glsl_type(handle) + " " + input_name + ";") + + # Nested input, like scattering.sun_color + elif len(parts) == 2: + struct_name = parts[0] + actual_input_name = parts[1] + if struct_name in structs: + # Struct is already defined, add member definition + structs[struct_name].append( + self.pta_to_glsl_type(handle) + " " + actual_input_name + ";") + else: + # Construct a new struct and add it to the list of inputs + inputs.append(struct_name + "_UBOSTRUCT " + struct_name + ";") + structs[struct_name] = [ + self.pta_to_glsl_type(handle) + " " + actual_input_name + ";" + ] + + # Nested input, like scattering.some_setting.sun_color, not supported yet + else: + self.warn("Structure definition too nested, not supported (yet):", input_name) + + # Add structures + for struct_name, members in iteritems(structs): + content += "struct " + struct_name + "_UBOSTRUCT {\n" + for member in members: + content += " " * 4 + member + "\n" + content += "};\n\n" + + # Add actual inputs + if len(inputs) < 1: + self.debug("No UBO inputs present for", self.name) + else: + if self.use_ubo: + + content += "layout(shared, binding={}) uniform {}_UBO {{\n".format( + self.bind_id, self.name) + for ipt in inputs: + content += " " * 4 + ipt + "\n" + content += "} " + self.name + ";\n" + else: + content += "uniform struct {\n" + for ipt in inputs: + content += " " * 4 + ipt + "\n" + content += "} " + self.name + ";\n" + + content += "\n" + return content diff --git a/RenderPipelineFile/rpcore/util/smooth_connected_curve.py b/RenderPipelineFile/rpcore/util/smooth_connected_curve.py index 89fe4c14..98f5a0df 100644 --- a/RenderPipelineFile/rpcore/util/smooth_connected_curve.py +++ b/RenderPipelineFile/rpcore/util/smooth_connected_curve.py @@ -1,139 +1,139 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from six.moves import range # pylint: disable=import-error - -from panda3d.core import Vec3, CurveFitter - - -class SmoothConnectedCurve(object): - - """ Interface to a curve which also manages connecting the end of the - curve with the beginning. """ - - def __init__(self): - self._curve = None - self._modified = False - self._border_points = 1 - self._color = (0, 0, 0) - self._cv_points = [(0, 0), (0.5, 0), (1.0, 0)] - self.build_curve() - - @property - def modified(self): - """ Returns whether the curve was modified since the creation """ - return self._modified - - @property - def control_points(self): - """ Returns a list of all controll points """ - return self._cv_points - - @control_points.setter - def control_points(self, points): - """ Sets the cv points to the given list of points """ - self._cv_points = points - self._modified = True - self.build_curve() - - @property - def color(self): - """ Returns the display color of the curve """ - return self._color - - @color.setter - def color(self, rgb): - """ Sets the display color of the curve """ - self._color = rgb - - def set_single_value(self, val): - """ Sets the curve to be linear, and only use a single value """ - self._cv_points = [[0.5, val]] - self._modified = False - self.build_curve() - - def append_cv(self, x, y): - """ Appends a new cv and returns the index of the attached cv """ - self._cv_points.append([x, y]) - self.build_curve() - self._modified = True - return len(self._cv_points) - 1 - - def remove_cv(self, index): - """ Attempts to remove the cv at the given index, does nothing if only - one control point is left """ - if len(self._cv_points) > 1: - del self._cv_points[index] - self._modified = True - self.build_curve() - - def build_curve(self): - """ Rebuilds the curve based on the controll point values """ - sorted_points = sorted(self._cv_points, key=lambda v: v[0]) - first_point = sorted_points[0] - fitter = CurveFitter() - - # Duplicate curve at the beginning - for i in range(self._border_points): - end_point = self._cv_points[(-i + self._border_points - 1) % len(self._cv_points)] - end_point = first_point - fitter.add_xyz(0.0, Vec3(0, end_point[1], 0)) - - # Append the actual points - for point in self._cv_points: - # Clamp point x position to avoid artifacts at the beginning - point_t = max(0.01, point[0]) - fitter.add_xyz(point_t, Vec3(point_t, point[1], 0)) - - # Duplicate curve at the end - for i in range(self._border_points): - start_point = self._cv_points[i % len(self._cv_points)] - start_point = first_point - fitter.add_xyz(1.0, Vec3(1, start_point[1], 0)) - - fitter.sort_points() - fitter.wrap_hpr() - fitter.compute_tangents(1.0) - - self._curve = fitter.make_hermite() - - def set_cv_value(self, index, x_value, y_value): - """ Updates the cv point at the given index """ - self._cv_points[index] = [x_value, y_value] - self._modified = True - - def get_value(self, offset): - """ Returns the value on the curve ranging whereas the offset should be - from 0 to 1 (0 denotes the start of the curve). The returned value will - be a value from 0 to 1 as well. """ - point = Vec3(0) - self._curve.evaluate_xyz(offset, point) - return max(0.0, min(1.0, point.y)) - - def serialize(self): - """ Returns the value of the curve as yaml list """ - points = ["[{:5.10f},{:5.10f}]".format(float(a), float(b)) for a, b in self._cv_points] - return "[" + ','.join(points) + "]" +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from six.moves import range # pylint: disable=import-error + +from panda3d.core import Vec3, CurveFitter + + +class SmoothConnectedCurve(object): + + """ Interface to a curve which also manages connecting the end of the + curve with the beginning. """ + + def __init__(self): + self._curve = None + self._modified = False + self._border_points = 1 + self._color = (0, 0, 0) + self._cv_points = [(0, 0), (0.5, 0), (1.0, 0)] + self.build_curve() + + @property + def modified(self): + """ Returns whether the curve was modified since the creation """ + return self._modified + + @property + def control_points(self): + """ Returns a list of all controll points """ + return self._cv_points + + @control_points.setter + def control_points(self, points): + """ Sets the cv points to the given list of points """ + self._cv_points = points + self._modified = True + self.build_curve() + + @property + def color(self): + """ Returns the display color of the curve """ + return self._color + + @color.setter + def color(self, rgb): + """ Sets the display color of the curve """ + self._color = rgb + + def set_single_value(self, val): + """ Sets the curve to be linear, and only use a single value """ + self._cv_points = [[0.5, val]] + self._modified = False + self.build_curve() + + def append_cv(self, x, y): + """ Appends a new cv and returns the index of the attached cv """ + self._cv_points.append([x, y]) + self.build_curve() + self._modified = True + return len(self._cv_points) - 1 + + def remove_cv(self, index): + """ Attempts to remove the cv at the given index, does nothing if only + one control point is left """ + if len(self._cv_points) > 1: + del self._cv_points[index] + self._modified = True + self.build_curve() + + def build_curve(self): + """ Rebuilds the curve based on the controll point values """ + sorted_points = sorted(self._cv_points, key=lambda v: v[0]) + first_point = sorted_points[0] + fitter = CurveFitter() + + # Duplicate curve at the beginning + for i in range(self._border_points): + end_point = self._cv_points[(-i + self._border_points - 1) % len(self._cv_points)] + end_point = first_point + fitter.add_xyz(0.0, Vec3(0, end_point[1], 0)) + + # Append the actual points + for point in self._cv_points: + # Clamp point x position to avoid artifacts at the beginning + point_t = max(0.01, point[0]) + fitter.add_xyz(point_t, Vec3(point_t, point[1], 0)) + + # Duplicate curve at the end + for i in range(self._border_points): + start_point = self._cv_points[i % len(self._cv_points)] + start_point = first_point + fitter.add_xyz(1.0, Vec3(1, start_point[1], 0)) + + fitter.sort_points() + fitter.wrap_hpr() + fitter.compute_tangents(1.0) + + self._curve = fitter.make_hermite() + + def set_cv_value(self, index, x_value, y_value): + """ Updates the cv point at the given index """ + self._cv_points[index] = [x_value, y_value] + self._modified = True + + def get_value(self, offset): + """ Returns the value on the curve ranging whereas the offset should be + from 0 to 1 (0 denotes the start of the curve). The returned value will + be a value from 0 to 1 as well. """ + point = Vec3(0) + self._curve.evaluate_xyz(offset, point) + return max(0.0, min(1.0, point.y)) + + def serialize(self): + """ Returns the value of the curve as yaml list """ + points = ["[{:5.10f},{:5.10f}]".format(float(a), float(b)) for a, b in self._cv_points] + return "[" + ','.join(points) + "]" diff --git a/RenderPipelineFile/rpcore/util/submodule_downloader.py b/RenderPipelineFile/rpcore/util/submodule_downloader.py index bb15b93a..f0498fed 100644 --- a/RenderPipelineFile/rpcore/util/submodule_downloader.py +++ b/RenderPipelineFile/rpcore/util/submodule_downloader.py @@ -1,147 +1,147 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import print_function - -import os -import sys -import zipfile -import shutil - -from rplibs.six.moves import urllib # pylint: disable=import-error -from rplibs.six import BytesIO, binary_type - - -def download_file(url, chunk_size=100 * 1024): - """ Helper method to download a file displaying a progress bar """ - print("Fetching:", url) - file_content = None - progressbar = None - - if sys.version_info.major <= 2: - - # Import progressbar library - from rplibs.progressbar import FileTransferSpeed, ETA, ProgressBar, Percentage - from rplibs.progressbar import Bar - widgets = ['\tDownloading: ', FileTransferSpeed(), ' ', Bar(), Percentage(), ' ', ETA()] - file_content = [] - bytes_read = 0 - - # Progressively download the file - try: - usock = urllib.request.urlopen(url) - file_size = int(usock.headers.get("Content-Length", 1e10)) - print("File size is", round(file_size / (1024**2), 2), "MB") - progressbar = ProgressBar(widgets=widgets, maxval=file_size).start() - while True: - data = usock.read(chunk_size) - file_content.append(data) - bytes_read += len(data) - progressbar.update(bytes_read) - if not data: - break - usock.close() - except Exception: - print("ERROR: Could not fetch", url, "!", file=sys.stderr) - raise - else: - # Don't use progressbar in python 3 - print("Downloading .. (progressbar disabled due to python 3 build)") - try: - usock = urllib.request.urlopen(url) - file_content = [] - while True: - data = usock.read(chunk_size) - file_content.append(data) - if not data: - break - usock.close() - except Exception: - print("ERROR: Could not fetch", url, "!", file=sys.stderr) - raise - - if progressbar: - progressbar.finish() - - return binary_type().join(file_content) - - -def download_submodule(author, module_name, dest_path, ignore_list): - """ Downloads a submodule from the given author and module name, and extracts - all files which are not on the ignore_list to the dest_path. - - Example: download_submodule("tobspr", "RenderPipeline", ".", ["README.md", "LICENSE"]) - """ - - # Make directory, if it does not exist yet - if not os.path.isdir(dest_path): - os.makedirs(dest_path) - - # Construct download url - source_url = "https://github.com/" + author + "/" + module_name + "/archive/master.zip" - prefix = module_name + "-master" - - # Extract the zip - zip_ptr = BytesIO(download_file(source_url)) - print("Extracting ZIP ...") - - try: - zip_handle = zipfile.ZipFile(zip_ptr) - except zipfile.BadZipfile: - print("ERROR: Invalid zip file!", file=sys.stderr) - sys.exit(3) - - if zip_handle.testzip() is not None: - print("ERROR: Invalid zip file checksums!", file=sys.stderr) - sys.exit(1) - - num_files, num_dirs = 0, 0 - - for fname in zip_handle.namelist(): - rel_name = fname.replace(prefix, "").replace("\\", "/").lstrip("/") - if not rel_name: - continue - - is_file = not rel_name.endswith("/") - rel_name = dest_path.rstrip("/\\") + "/" + rel_name - - # Files - if is_file: - for ignore in ignore_list: - if ignore in rel_name: - break - else: - with zip_handle.open(fname, "r") as source, open(rel_name, "wb") as dest: - shutil.copyfileobj(source, dest) - num_files += 1 - - # Directories - else: - if not os.path.isdir(rel_name): - os.makedirs(rel_name) - num_dirs += 1 - - print("Extracted", num_files, "files and", num_dirs, "directories") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import print_function + +import os +import sys +import zipfile +import shutil + +from rplibs.six.moves import urllib # pylint: disable=import-error +from rplibs.six import BytesIO, binary_type + + +def download_file(url, chunk_size=100 * 1024): + """ Helper method to download a file displaying a progress bar """ + print("Fetching:", url) + file_content = None + progressbar = None + + if sys.version_info.major <= 2: + + # Import progressbar library + from rplibs.progressbar import FileTransferSpeed, ETA, ProgressBar, Percentage + from rplibs.progressbar import Bar + widgets = ['\tDownloading: ', FileTransferSpeed(), ' ', Bar(), Percentage(), ' ', ETA()] + file_content = [] + bytes_read = 0 + + # Progressively download the file + try: + usock = urllib.request.urlopen(url) + file_size = int(usock.headers.get("Content-Length", 1e10)) + print("File size is", round(file_size / (1024**2), 2), "MB") + progressbar = ProgressBar(widgets=widgets, maxval=file_size).start() + while True: + data = usock.read(chunk_size) + file_content.append(data) + bytes_read += len(data) + progressbar.update(bytes_read) + if not data: + break + usock.close() + except Exception: + print("ERROR: Could not fetch", url, "!", file=sys.stderr) + raise + else: + # Don't use progressbar in python 3 + print("Downloading .. (progressbar disabled due to python 3 build)") + try: + usock = urllib.request.urlopen(url) + file_content = [] + while True: + data = usock.read(chunk_size) + file_content.append(data) + if not data: + break + usock.close() + except Exception: + print("ERROR: Could not fetch", url, "!", file=sys.stderr) + raise + + if progressbar: + progressbar.finish() + + return binary_type().join(file_content) + + +def download_submodule(author, module_name, dest_path, ignore_list): + """ Downloads a submodule from the given author and module name, and extracts + all files which are not on the ignore_list to the dest_path. + + Example: download_submodule("tobspr", "RenderPipeline", ".", ["README.md", "LICENSE"]) + """ + + # Make directory, if it does not exist yet + if not os.path.isdir(dest_path): + os.makedirs(dest_path) + + # Construct download url + source_url = "https://github.com/" + author + "/" + module_name + "/archive/master.zip" + prefix = module_name + "-master" + + # Extract the zip + zip_ptr = BytesIO(download_file(source_url)) + print("Extracting ZIP ...") + + try: + zip_handle = zipfile.ZipFile(zip_ptr) + except zipfile.BadZipfile: + print("ERROR: Invalid zip file!", file=sys.stderr) + sys.exit(3) + + if zip_handle.testzip() is not None: + print("ERROR: Invalid zip file checksums!", file=sys.stderr) + sys.exit(1) + + num_files, num_dirs = 0, 0 + + for fname in zip_handle.namelist(): + rel_name = fname.replace(prefix, "").replace("\\", "/").lstrip("/") + if not rel_name: + continue + + is_file = not rel_name.endswith("/") + rel_name = dest_path.rstrip("/\\") + "/" + rel_name + + # Files + if is_file: + for ignore in ignore_list: + if ignore in rel_name: + break + else: + with zip_handle.open(fname, "r") as source, open(rel_name, "wb") as dest: + shutil.copyfileobj(source, dest) + num_files += 1 + + # Directories + else: + if not os.path.isdir(rel_name): + os.makedirs(rel_name) + num_dirs += 1 + + print("Extracted", num_files, "files and", num_dirs, "directories") diff --git a/RenderPipelineFile/rpcore/util/task_scheduler.py b/RenderPipelineFile/rpcore/util/task_scheduler.py index 03d89869..66cbf6c7 100644 --- a/RenderPipelineFile/rpcore/util/task_scheduler.py +++ b/RenderPipelineFile/rpcore/util/task_scheduler.py @@ -1,77 +1,77 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rplibs.yaml import load_yaml_file -from rpcore.rpobject import RPObject - - -class TaskScheduler(RPObject): - - """ This class manages the scheduled tasks and splits them over multiple - frames. Plugins can query whether their subtasks should be executed - or queued for later frames. Also performs analysis on the task configuration - to figure if tasks are distributed uniformly. """ - - def __init__(self, pipeline): - RPObject.__init__(self) - self._pipeline = pipeline - self._tasks = [] - self._frame_index = 0 - self._load_config() - - def _load_config(self): - """ Loads the tasks distribution configuration """ - config = load_yaml_file("/$$rpconfig/task-scheduler.yaml")["frame_cycles"] - for frame_name, tasks in config: # pylint: disable=unused-variable - self._tasks.append(tasks) - - def _check_missing_schedule(self, task_name): - """ Checks whether the given task is scheduled at some point. This can - be used to check whether any task is missing in the task scheduler config. """ - for tasks in self._tasks: - if task_name in tasks: - break - else: - self.error("Task '" + task_name + "' is never scheduled and thus will never run!") - - def is_scheduled(self, task_name): - """ Returns whether a given task is supposed to run this frame """ - self._check_missing_schedule(task_name) - return task_name in self._tasks[self._frame_index] - - def step(self): - """ Advances one frame """ - self._frame_index = (self._frame_index + 1) % len(self._tasks) - - @property - def num_tasks(self): - """ Returns the total amount of tasks """ - return sum((len(i) for i in self._tasks)) - - @property - def num_scheduled_tasks(self): - """ Returns the amount of scheduled tasks this frame """ - return len(self._tasks[self._frame_index]) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rplibs.yaml import load_yaml_file +from rpcore.rpobject import RPObject + + +class TaskScheduler(RPObject): + + """ This class manages the scheduled tasks and splits them over multiple + frames. Plugins can query whether their subtasks should be executed + or queued for later frames. Also performs analysis on the task configuration + to figure if tasks are distributed uniformly. """ + + def __init__(self, pipeline): + RPObject.__init__(self) + self._pipeline = pipeline + self._tasks = [] + self._frame_index = 0 + self._load_config() + + def _load_config(self): + """ Loads the tasks distribution configuration """ + config = load_yaml_file("/$$rpconfig/task-scheduler.yaml")["frame_cycles"] + for frame_name, tasks in config: # pylint: disable=unused-variable + self._tasks.append(tasks) + + def _check_missing_schedule(self, task_name): + """ Checks whether the given task is scheduled at some point. This can + be used to check whether any task is missing in the task scheduler config. """ + for tasks in self._tasks: + if task_name in tasks: + break + else: + self.error("Task '" + task_name + "' is never scheduled and thus will never run!") + + def is_scheduled(self, task_name): + """ Returns whether a given task is supposed to run this frame """ + self._check_missing_schedule(task_name) + return task_name in self._tasks[self._frame_index] + + def step(self): + """ Advances one frame """ + self._frame_index = (self._frame_index + 1) % len(self._tasks) + + @property + def num_tasks(self): + """ Returns the total amount of tasks """ + return sum((len(i) for i in self._tasks)) + + @property + def num_scheduled_tasks(self): + """ Returns the amount of scheduled tasks this frame """ + return len(self._tasks[self._frame_index]) diff --git a/RenderPipelineFile/rplibs/colorama/LICENSE.txt b/RenderPipelineFile/rplibs/colorama/LICENSE.txt index 5f567799..239eaa83 100644 --- a/RenderPipelineFile/rplibs/colorama/LICENSE.txt +++ b/RenderPipelineFile/rplibs/colorama/LICENSE.txt @@ -1,28 +1,28 @@ -Copyright (c) 2010 Jonathan Hartley -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -* Neither the name of the copyright holders, nor those of its contributors - may be used to endorse or promote products derived from this software without - specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - +Copyright (c) 2010 Jonathan Hartley +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holders, nor those of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/RenderPipelineFile/rplibs/colorama/SOURCE.txt b/RenderPipelineFile/rplibs/colorama/SOURCE.txt index c90a8bd5..b33e8989 100644 --- a/RenderPipelineFile/rplibs/colorama/SOURCE.txt +++ b/RenderPipelineFile/rplibs/colorama/SOURCE.txt @@ -1,4 +1,4 @@ -package: colorama -version: 0.3.7 -downloaded: 17.03.2016 -url: https://github.com/tartley/colorama +package: colorama +version: 0.3.7 +downloaded: 17.03.2016 +url: https://github.com/tartley/colorama diff --git a/RenderPipelineFile/rplibs/colorama/__init__.py b/RenderPipelineFile/rplibs/colorama/__init__.py index 670e6b39..c0a2e9fb 100644 --- a/RenderPipelineFile/rplibs/colorama/__init__.py +++ b/RenderPipelineFile/rplibs/colorama/__init__.py @@ -1,7 +1,7 @@ -# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. -from .initialise import init, deinit, reinit, colorama_text -from .ansi import Fore, Back, Style, Cursor -from .ansitowin32 import AnsiToWin32 - -__version__ = '0.3.7' - +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +from .initialise import init, deinit, reinit, colorama_text +from .ansi import Fore, Back, Style, Cursor +from .ansitowin32 import AnsiToWin32 + +__version__ = '0.3.7' + diff --git a/RenderPipelineFile/rplibs/colorama/ansi.py b/RenderPipelineFile/rplibs/colorama/ansi.py index 78776588..8530fd01 100644 --- a/RenderPipelineFile/rplibs/colorama/ansi.py +++ b/RenderPipelineFile/rplibs/colorama/ansi.py @@ -1,102 +1,102 @@ -# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. -''' -This module generates ANSI character codes to printing colors to terminals. -See: http://en.wikipedia.org/wiki/ANSI_escape_code -''' - -CSI = '\033[' -OSC = '\033]' -BEL = '\007' - - -def code_to_chars(code): - return CSI + str(code) + 'm' - -def set_title(title): - return OSC + '2;' + title + BEL - -def clear_screen(mode=2): - return CSI + str(mode) + 'J' - -def clear_line(mode=2): - return CSI + str(mode) + 'K' - - -class AnsiCodes(object): - def __init__(self): - # the subclasses declare class attributes which are numbers. - # Upon instantiation we define instance attributes, which are the same - # as the class attributes but wrapped with the ANSI escape sequence - for name in dir(self): - if not name.startswith('_'): - value = getattr(self, name) - setattr(self, name, code_to_chars(value)) - - -class AnsiCursor(object): - def UP(self, n=1): - return CSI + str(n) + 'A' - def DOWN(self, n=1): - return CSI + str(n) + 'B' - def FORWARD(self, n=1): - return CSI + str(n) + 'C' - def BACK(self, n=1): - return CSI + str(n) + 'D' - def POS(self, x=1, y=1): - return CSI + str(y) + ';' + str(x) + 'H' - - -class AnsiFore(AnsiCodes): - BLACK = 30 - RED = 31 - GREEN = 32 - YELLOW = 33 - BLUE = 34 - MAGENTA = 35 - CYAN = 36 - WHITE = 37 - RESET = 39 - - # These are fairly well supported, but not part of the standard. - LIGHTBLACK_EX = 90 - LIGHTRED_EX = 91 - LIGHTGREEN_EX = 92 - LIGHTYELLOW_EX = 93 - LIGHTBLUE_EX = 94 - LIGHTMAGENTA_EX = 95 - LIGHTCYAN_EX = 96 - LIGHTWHITE_EX = 97 - - -class AnsiBack(AnsiCodes): - BLACK = 40 - RED = 41 - GREEN = 42 - YELLOW = 43 - BLUE = 44 - MAGENTA = 45 - CYAN = 46 - WHITE = 47 - RESET = 49 - - # These are fairly well supported, but not part of the standard. - LIGHTBLACK_EX = 100 - LIGHTRED_EX = 101 - LIGHTGREEN_EX = 102 - LIGHTYELLOW_EX = 103 - LIGHTBLUE_EX = 104 - LIGHTMAGENTA_EX = 105 - LIGHTCYAN_EX = 106 - LIGHTWHITE_EX = 107 - - -class AnsiStyle(AnsiCodes): - BRIGHT = 1 - DIM = 2 - NORMAL = 22 - RESET_ALL = 0 - -Fore = AnsiFore() -Back = AnsiBack() -Style = AnsiStyle() -Cursor = AnsiCursor() +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +''' +This module generates ANSI character codes to printing colors to terminals. +See: http://en.wikipedia.org/wiki/ANSI_escape_code +''' + +CSI = '\033[' +OSC = '\033]' +BEL = '\007' + + +def code_to_chars(code): + return CSI + str(code) + 'm' + +def set_title(title): + return OSC + '2;' + title + BEL + +def clear_screen(mode=2): + return CSI + str(mode) + 'J' + +def clear_line(mode=2): + return CSI + str(mode) + 'K' + + +class AnsiCodes(object): + def __init__(self): + # the subclasses declare class attributes which are numbers. + # Upon instantiation we define instance attributes, which are the same + # as the class attributes but wrapped with the ANSI escape sequence + for name in dir(self): + if not name.startswith('_'): + value = getattr(self, name) + setattr(self, name, code_to_chars(value)) + + +class AnsiCursor(object): + def UP(self, n=1): + return CSI + str(n) + 'A' + def DOWN(self, n=1): + return CSI + str(n) + 'B' + def FORWARD(self, n=1): + return CSI + str(n) + 'C' + def BACK(self, n=1): + return CSI + str(n) + 'D' + def POS(self, x=1, y=1): + return CSI + str(y) + ';' + str(x) + 'H' + + +class AnsiFore(AnsiCodes): + BLACK = 30 + RED = 31 + GREEN = 32 + YELLOW = 33 + BLUE = 34 + MAGENTA = 35 + CYAN = 36 + WHITE = 37 + RESET = 39 + + # These are fairly well supported, but not part of the standard. + LIGHTBLACK_EX = 90 + LIGHTRED_EX = 91 + LIGHTGREEN_EX = 92 + LIGHTYELLOW_EX = 93 + LIGHTBLUE_EX = 94 + LIGHTMAGENTA_EX = 95 + LIGHTCYAN_EX = 96 + LIGHTWHITE_EX = 97 + + +class AnsiBack(AnsiCodes): + BLACK = 40 + RED = 41 + GREEN = 42 + YELLOW = 43 + BLUE = 44 + MAGENTA = 45 + CYAN = 46 + WHITE = 47 + RESET = 49 + + # These are fairly well supported, but not part of the standard. + LIGHTBLACK_EX = 100 + LIGHTRED_EX = 101 + LIGHTGREEN_EX = 102 + LIGHTYELLOW_EX = 103 + LIGHTBLUE_EX = 104 + LIGHTMAGENTA_EX = 105 + LIGHTCYAN_EX = 106 + LIGHTWHITE_EX = 107 + + +class AnsiStyle(AnsiCodes): + BRIGHT = 1 + DIM = 2 + NORMAL = 22 + RESET_ALL = 0 + +Fore = AnsiFore() +Back = AnsiBack() +Style = AnsiStyle() +Cursor = AnsiCursor() diff --git a/RenderPipelineFile/rplibs/colorama/ansitowin32.py b/RenderPipelineFile/rplibs/colorama/ansitowin32.py index b7ff6f21..5cb5a225 100644 --- a/RenderPipelineFile/rplibs/colorama/ansitowin32.py +++ b/RenderPipelineFile/rplibs/colorama/ansitowin32.py @@ -1,236 +1,236 @@ -# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. -import re -import sys -import os - -from .ansi import AnsiFore, AnsiBack, AnsiStyle, Style -from .winterm import WinTerm, WinColor, WinStyle -from .win32 import windll, winapi_test - - -winterm = None -if windll is not None: - winterm = WinTerm() - - -def is_stream_closed(stream): - return not hasattr(stream, 'closed') or stream.closed - - -def is_a_tty(stream): - return hasattr(stream, 'isatty') and stream.isatty() - - -class StreamWrapper(object): - ''' - Wraps a stream (such as stdout), acting as a transparent proxy for all - attribute access apart from method 'write()', which is delegated to our - Converter instance. - ''' - def __init__(self, wrapped, converter): - # double-underscore everything to prevent clashes with names of - # attributes on the wrapped stream object. - self.__wrapped = wrapped - self.__convertor = converter - - def __getattr__(self, name): - return getattr(self.__wrapped, name) - - def write(self, text): - self.__convertor.write(text) - - -class AnsiToWin32(object): - ''' - Implements a 'write()' method which, on Windows, will strip ANSI character - sequences from the text, and if outputting to a tty, will convert them into - win32 function calls. - ''' - ANSI_CSI_RE = re.compile('\001?\033\[((?:\d|;)*)([a-zA-Z])\002?') # Control Sequence Introducer - ANSI_OSC_RE = re.compile('\001?\033\]((?:.|;)*?)(\x07)\002?') # Operating System Command - - def __init__(self, wrapped, convert=None, strip=None, autoreset=False): - # The wrapped stream (normally sys.stdout or sys.stderr) - self.wrapped = wrapped - - # should we reset colors to defaults after every .write() - self.autoreset = autoreset - - # create the proxy wrapping our output stream - self.stream = StreamWrapper(wrapped, self) - - on_windows = os.name == 'nt' - # We test if the WinAPI works, because even if we are on Windows - # we may be using a terminal that doesn't support the WinAPI - # (e.g. Cygwin Terminal). In this case it's up to the terminal - # to support the ANSI codes. - conversion_supported = on_windows and winapi_test() - - # should we strip ANSI sequences from our output? - if strip is None: - strip = conversion_supported or (not is_stream_closed(wrapped) and not is_a_tty(wrapped)) - self.strip = strip - - # should we should convert ANSI sequences into win32 calls? - if convert is None: - convert = conversion_supported and not is_stream_closed(wrapped) and is_a_tty(wrapped) - self.convert = convert - - # dict of ansi codes to win32 functions and parameters - self.win32_calls = self.get_win32_calls() - - # are we wrapping stderr? - self.on_stderr = self.wrapped is sys.stderr - - def should_wrap(self): - ''' - True if this class is actually needed. If false, then the output - stream will not be affected, nor will win32 calls be issued, so - wrapping stdout is not actually required. This will generally be - False on non-Windows platforms, unless optional functionality like - autoreset has been requested using kwargs to init() - ''' - return self.convert or self.strip or self.autoreset - - def get_win32_calls(self): - if self.convert and winterm: - return { - AnsiStyle.RESET_ALL: (winterm.reset_all, ), - AnsiStyle.BRIGHT: (winterm.style, WinStyle.BRIGHT), - AnsiStyle.DIM: (winterm.style, WinStyle.NORMAL), - AnsiStyle.NORMAL: (winterm.style, WinStyle.NORMAL), - AnsiFore.BLACK: (winterm.fore, WinColor.BLACK), - AnsiFore.RED: (winterm.fore, WinColor.RED), - AnsiFore.GREEN: (winterm.fore, WinColor.GREEN), - AnsiFore.YELLOW: (winterm.fore, WinColor.YELLOW), - AnsiFore.BLUE: (winterm.fore, WinColor.BLUE), - AnsiFore.MAGENTA: (winterm.fore, WinColor.MAGENTA), - AnsiFore.CYAN: (winterm.fore, WinColor.CYAN), - AnsiFore.WHITE: (winterm.fore, WinColor.GREY), - AnsiFore.RESET: (winterm.fore, ), - AnsiFore.LIGHTBLACK_EX: (winterm.fore, WinColor.BLACK, True), - AnsiFore.LIGHTRED_EX: (winterm.fore, WinColor.RED, True), - AnsiFore.LIGHTGREEN_EX: (winterm.fore, WinColor.GREEN, True), - AnsiFore.LIGHTYELLOW_EX: (winterm.fore, WinColor.YELLOW, True), - AnsiFore.LIGHTBLUE_EX: (winterm.fore, WinColor.BLUE, True), - AnsiFore.LIGHTMAGENTA_EX: (winterm.fore, WinColor.MAGENTA, True), - AnsiFore.LIGHTCYAN_EX: (winterm.fore, WinColor.CYAN, True), - AnsiFore.LIGHTWHITE_EX: (winterm.fore, WinColor.GREY, True), - AnsiBack.BLACK: (winterm.back, WinColor.BLACK), - AnsiBack.RED: (winterm.back, WinColor.RED), - AnsiBack.GREEN: (winterm.back, WinColor.GREEN), - AnsiBack.YELLOW: (winterm.back, WinColor.YELLOW), - AnsiBack.BLUE: (winterm.back, WinColor.BLUE), - AnsiBack.MAGENTA: (winterm.back, WinColor.MAGENTA), - AnsiBack.CYAN: (winterm.back, WinColor.CYAN), - AnsiBack.WHITE: (winterm.back, WinColor.GREY), - AnsiBack.RESET: (winterm.back, ), - AnsiBack.LIGHTBLACK_EX: (winterm.back, WinColor.BLACK, True), - AnsiBack.LIGHTRED_EX: (winterm.back, WinColor.RED, True), - AnsiBack.LIGHTGREEN_EX: (winterm.back, WinColor.GREEN, True), - AnsiBack.LIGHTYELLOW_EX: (winterm.back, WinColor.YELLOW, True), - AnsiBack.LIGHTBLUE_EX: (winterm.back, WinColor.BLUE, True), - AnsiBack.LIGHTMAGENTA_EX: (winterm.back, WinColor.MAGENTA, True), - AnsiBack.LIGHTCYAN_EX: (winterm.back, WinColor.CYAN, True), - AnsiBack.LIGHTWHITE_EX: (winterm.back, WinColor.GREY, True), - } - return dict() - - def write(self, text): - if self.strip or self.convert: - self.write_and_convert(text) - else: - self.wrapped.write(text) - self.wrapped.flush() - if self.autoreset: - self.reset_all() - - - def reset_all(self): - if self.convert: - self.call_win32('m', (0,)) - elif not self.strip and not is_stream_closed(self.wrapped): - self.wrapped.write(Style.RESET_ALL) - - - def write_and_convert(self, text): - ''' - Write the given text to our wrapped stream, stripping any ANSI - sequences from the text, and optionally converting them into win32 - calls. - ''' - cursor = 0 - text = self.convert_osc(text) - for match in self.ANSI_CSI_RE.finditer(text): - start, end = match.span() - self.write_plain_text(text, cursor, start) - self.convert_ansi(*match.groups()) - cursor = end - self.write_plain_text(text, cursor, len(text)) - - - def write_plain_text(self, text, start, end): - if start < end: - self.wrapped.write(text[start:end]) - self.wrapped.flush() - - - def convert_ansi(self, paramstring, command): - if self.convert: - params = self.extract_params(command, paramstring) - self.call_win32(command, params) - - - def extract_params(self, command, paramstring): - if command in 'Hf': - params = tuple(int(p) if len(p) != 0 else 1 for p in paramstring.split(';')) - while len(params) < 2: - # defaults: - params = params + (1,) - else: - params = tuple(int(p) for p in paramstring.split(';') if len(p) != 0) - if len(params) == 0: - # defaults: - if command in 'JKm': - params = (0,) - elif command in 'ABCD': - params = (1,) - - return params - - - def call_win32(self, command, params): - if command == 'm': - for param in params: - if param in self.win32_calls: - func_args = self.win32_calls[param] - func = func_args[0] - args = func_args[1:] - kwargs = dict(on_stderr=self.on_stderr) - func(*args, **kwargs) - elif command in 'J': - winterm.erase_screen(params[0], on_stderr=self.on_stderr) - elif command in 'K': - winterm.erase_line(params[0], on_stderr=self.on_stderr) - elif command in 'Hf': # cursor position - absolute - winterm.set_cursor_position(params, on_stderr=self.on_stderr) - elif command in 'ABCD': # cursor position - relative - n = params[0] - # A - up, B - down, C - forward, D - back - x, y = {'A': (0, -n), 'B': (0, n), 'C': (n, 0), 'D': (-n, 0)}[command] - winterm.cursor_adjust(x, y, on_stderr=self.on_stderr) - - - def convert_osc(self, text): - for match in self.ANSI_OSC_RE.finditer(text): - start, end = match.span() - text = text[:start] + text[end:] - paramstring, command = match.groups() - if command in '\x07': # \x07 = BEL - params = paramstring.split(";") - # 0 - change title and icon (we will only change title) - # 1 - change icon (we don't support this) - # 2 - change title - if params[0] in '02': - winterm.set_title(params[1]) - return text +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +import re +import sys +import os + +from .ansi import AnsiFore, AnsiBack, AnsiStyle, Style +from .winterm import WinTerm, WinColor, WinStyle +from .win32 import windll, winapi_test + + +winterm = None +if windll is not None: + winterm = WinTerm() + + +def is_stream_closed(stream): + return not hasattr(stream, 'closed') or stream.closed + + +def is_a_tty(stream): + return hasattr(stream, 'isatty') and stream.isatty() + + +class StreamWrapper(object): + ''' + Wraps a stream (such as stdout), acting as a transparent proxy for all + attribute access apart from method 'write()', which is delegated to our + Converter instance. + ''' + def __init__(self, wrapped, converter): + # double-underscore everything to prevent clashes with names of + # attributes on the wrapped stream object. + self.__wrapped = wrapped + self.__convertor = converter + + def __getattr__(self, name): + return getattr(self.__wrapped, name) + + def write(self, text): + self.__convertor.write(text) + + +class AnsiToWin32(object): + ''' + Implements a 'write()' method which, on Windows, will strip ANSI character + sequences from the text, and if outputting to a tty, will convert them into + win32 function calls. + ''' + ANSI_CSI_RE = re.compile('\001?\033\[((?:\d|;)*)([a-zA-Z])\002?') # Control Sequence Introducer + ANSI_OSC_RE = re.compile('\001?\033\]((?:.|;)*?)(\x07)\002?') # Operating System Command + + def __init__(self, wrapped, convert=None, strip=None, autoreset=False): + # The wrapped stream (normally sys.stdout or sys.stderr) + self.wrapped = wrapped + + # should we reset colors to defaults after every .write() + self.autoreset = autoreset + + # create the proxy wrapping our output stream + self.stream = StreamWrapper(wrapped, self) + + on_windows = os.name == 'nt' + # We test if the WinAPI works, because even if we are on Windows + # we may be using a terminal that doesn't support the WinAPI + # (e.g. Cygwin Terminal). In this case it's up to the terminal + # to support the ANSI codes. + conversion_supported = on_windows and winapi_test() + + # should we strip ANSI sequences from our output? + if strip is None: + strip = conversion_supported or (not is_stream_closed(wrapped) and not is_a_tty(wrapped)) + self.strip = strip + + # should we should convert ANSI sequences into win32 calls? + if convert is None: + convert = conversion_supported and not is_stream_closed(wrapped) and is_a_tty(wrapped) + self.convert = convert + + # dict of ansi codes to win32 functions and parameters + self.win32_calls = self.get_win32_calls() + + # are we wrapping stderr? + self.on_stderr = self.wrapped is sys.stderr + + def should_wrap(self): + ''' + True if this class is actually needed. If false, then the output + stream will not be affected, nor will win32 calls be issued, so + wrapping stdout is not actually required. This will generally be + False on non-Windows platforms, unless optional functionality like + autoreset has been requested using kwargs to init() + ''' + return self.convert or self.strip or self.autoreset + + def get_win32_calls(self): + if self.convert and winterm: + return { + AnsiStyle.RESET_ALL: (winterm.reset_all, ), + AnsiStyle.BRIGHT: (winterm.style, WinStyle.BRIGHT), + AnsiStyle.DIM: (winterm.style, WinStyle.NORMAL), + AnsiStyle.NORMAL: (winterm.style, WinStyle.NORMAL), + AnsiFore.BLACK: (winterm.fore, WinColor.BLACK), + AnsiFore.RED: (winterm.fore, WinColor.RED), + AnsiFore.GREEN: (winterm.fore, WinColor.GREEN), + AnsiFore.YELLOW: (winterm.fore, WinColor.YELLOW), + AnsiFore.BLUE: (winterm.fore, WinColor.BLUE), + AnsiFore.MAGENTA: (winterm.fore, WinColor.MAGENTA), + AnsiFore.CYAN: (winterm.fore, WinColor.CYAN), + AnsiFore.WHITE: (winterm.fore, WinColor.GREY), + AnsiFore.RESET: (winterm.fore, ), + AnsiFore.LIGHTBLACK_EX: (winterm.fore, WinColor.BLACK, True), + AnsiFore.LIGHTRED_EX: (winterm.fore, WinColor.RED, True), + AnsiFore.LIGHTGREEN_EX: (winterm.fore, WinColor.GREEN, True), + AnsiFore.LIGHTYELLOW_EX: (winterm.fore, WinColor.YELLOW, True), + AnsiFore.LIGHTBLUE_EX: (winterm.fore, WinColor.BLUE, True), + AnsiFore.LIGHTMAGENTA_EX: (winterm.fore, WinColor.MAGENTA, True), + AnsiFore.LIGHTCYAN_EX: (winterm.fore, WinColor.CYAN, True), + AnsiFore.LIGHTWHITE_EX: (winterm.fore, WinColor.GREY, True), + AnsiBack.BLACK: (winterm.back, WinColor.BLACK), + AnsiBack.RED: (winterm.back, WinColor.RED), + AnsiBack.GREEN: (winterm.back, WinColor.GREEN), + AnsiBack.YELLOW: (winterm.back, WinColor.YELLOW), + AnsiBack.BLUE: (winterm.back, WinColor.BLUE), + AnsiBack.MAGENTA: (winterm.back, WinColor.MAGENTA), + AnsiBack.CYAN: (winterm.back, WinColor.CYAN), + AnsiBack.WHITE: (winterm.back, WinColor.GREY), + AnsiBack.RESET: (winterm.back, ), + AnsiBack.LIGHTBLACK_EX: (winterm.back, WinColor.BLACK, True), + AnsiBack.LIGHTRED_EX: (winterm.back, WinColor.RED, True), + AnsiBack.LIGHTGREEN_EX: (winterm.back, WinColor.GREEN, True), + AnsiBack.LIGHTYELLOW_EX: (winterm.back, WinColor.YELLOW, True), + AnsiBack.LIGHTBLUE_EX: (winterm.back, WinColor.BLUE, True), + AnsiBack.LIGHTMAGENTA_EX: (winterm.back, WinColor.MAGENTA, True), + AnsiBack.LIGHTCYAN_EX: (winterm.back, WinColor.CYAN, True), + AnsiBack.LIGHTWHITE_EX: (winterm.back, WinColor.GREY, True), + } + return dict() + + def write(self, text): + if self.strip or self.convert: + self.write_and_convert(text) + else: + self.wrapped.write(text) + self.wrapped.flush() + if self.autoreset: + self.reset_all() + + + def reset_all(self): + if self.convert: + self.call_win32('m', (0,)) + elif not self.strip and not is_stream_closed(self.wrapped): + self.wrapped.write(Style.RESET_ALL) + + + def write_and_convert(self, text): + ''' + Write the given text to our wrapped stream, stripping any ANSI + sequences from the text, and optionally converting them into win32 + calls. + ''' + cursor = 0 + text = self.convert_osc(text) + for match in self.ANSI_CSI_RE.finditer(text): + start, end = match.span() + self.write_plain_text(text, cursor, start) + self.convert_ansi(*match.groups()) + cursor = end + self.write_plain_text(text, cursor, len(text)) + + + def write_plain_text(self, text, start, end): + if start < end: + self.wrapped.write(text[start:end]) + self.wrapped.flush() + + + def convert_ansi(self, paramstring, command): + if self.convert: + params = self.extract_params(command, paramstring) + self.call_win32(command, params) + + + def extract_params(self, command, paramstring): + if command in 'Hf': + params = tuple(int(p) if len(p) != 0 else 1 for p in paramstring.split(';')) + while len(params) < 2: + # defaults: + params = params + (1,) + else: + params = tuple(int(p) for p in paramstring.split(';') if len(p) != 0) + if len(params) == 0: + # defaults: + if command in 'JKm': + params = (0,) + elif command in 'ABCD': + params = (1,) + + return params + + + def call_win32(self, command, params): + if command == 'm': + for param in params: + if param in self.win32_calls: + func_args = self.win32_calls[param] + func = func_args[0] + args = func_args[1:] + kwargs = dict(on_stderr=self.on_stderr) + func(*args, **kwargs) + elif command in 'J': + winterm.erase_screen(params[0], on_stderr=self.on_stderr) + elif command in 'K': + winterm.erase_line(params[0], on_stderr=self.on_stderr) + elif command in 'Hf': # cursor position - absolute + winterm.set_cursor_position(params, on_stderr=self.on_stderr) + elif command in 'ABCD': # cursor position - relative + n = params[0] + # A - up, B - down, C - forward, D - back + x, y = {'A': (0, -n), 'B': (0, n), 'C': (n, 0), 'D': (-n, 0)}[command] + winterm.cursor_adjust(x, y, on_stderr=self.on_stderr) + + + def convert_osc(self, text): + for match in self.ANSI_OSC_RE.finditer(text): + start, end = match.span() + text = text[:start] + text[end:] + paramstring, command = match.groups() + if command in '\x07': # \x07 = BEL + params = paramstring.split(";") + # 0 - change title and icon (we will only change title) + # 1 - change icon (we don't support this) + # 2 - change title + if params[0] in '02': + winterm.set_title(params[1]) + return text diff --git a/RenderPipelineFile/rplibs/colorama/initialise.py b/RenderPipelineFile/rplibs/colorama/initialise.py index 834962a3..7f031566 100644 --- a/RenderPipelineFile/rplibs/colorama/initialise.py +++ b/RenderPipelineFile/rplibs/colorama/initialise.py @@ -1,82 +1,82 @@ -# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. -import atexit -import contextlib -import sys - -from .ansitowin32 import AnsiToWin32 - - -orig_stdout = None -orig_stderr = None - -wrapped_stdout = None -wrapped_stderr = None - -atexit_done = False - - -def reset_all(): - if AnsiToWin32 is not None: # Issue #74: objects might become None at exit - AnsiToWin32(orig_stdout).reset_all() - - -def init(autoreset=False, convert=None, strip=None, wrap=True): - - if not wrap and any([autoreset, convert, strip]): - raise ValueError('wrap=False conflicts with any other arg=True') - - global wrapped_stdout, wrapped_stderr - global orig_stdout, orig_stderr - - orig_stdout = sys.stdout - orig_stderr = sys.stderr - - if sys.stdout is None: - wrapped_stdout = None - else: - sys.stdout = wrapped_stdout = \ - wrap_stream(orig_stdout, convert, strip, autoreset, wrap) - if sys.stderr is None: - wrapped_stderr = None - else: - sys.stderr = wrapped_stderr = \ - wrap_stream(orig_stderr, convert, strip, autoreset, wrap) - - global atexit_done - if not atexit_done: - atexit.register(reset_all) - atexit_done = True - - -def deinit(): - if orig_stdout is not None: - sys.stdout = orig_stdout - if orig_stderr is not None: - sys.stderr = orig_stderr - - -@contextlib.contextmanager -def colorama_text(*args, **kwargs): - init(*args, **kwargs) - try: - yield - finally: - deinit() - - -def reinit(): - if wrapped_stdout is not None: - sys.stdout = wrapped_stdout - if wrapped_stderr is not None: - sys.stderr = wrapped_stderr - - -def wrap_stream(stream, convert, strip, autoreset, wrap): - if wrap: - wrapper = AnsiToWin32(stream, - convert=convert, strip=strip, autoreset=autoreset) - if wrapper.should_wrap(): - stream = wrapper.stream - return stream - - +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +import atexit +import contextlib +import sys + +from .ansitowin32 import AnsiToWin32 + + +orig_stdout = None +orig_stderr = None + +wrapped_stdout = None +wrapped_stderr = None + +atexit_done = False + + +def reset_all(): + if AnsiToWin32 is not None: # Issue #74: objects might become None at exit + AnsiToWin32(orig_stdout).reset_all() + + +def init(autoreset=False, convert=None, strip=None, wrap=True): + + if not wrap and any([autoreset, convert, strip]): + raise ValueError('wrap=False conflicts with any other arg=True') + + global wrapped_stdout, wrapped_stderr + global orig_stdout, orig_stderr + + orig_stdout = sys.stdout + orig_stderr = sys.stderr + + if sys.stdout is None: + wrapped_stdout = None + else: + sys.stdout = wrapped_stdout = \ + wrap_stream(orig_stdout, convert, strip, autoreset, wrap) + if sys.stderr is None: + wrapped_stderr = None + else: + sys.stderr = wrapped_stderr = \ + wrap_stream(orig_stderr, convert, strip, autoreset, wrap) + + global atexit_done + if not atexit_done: + atexit.register(reset_all) + atexit_done = True + + +def deinit(): + if orig_stdout is not None: + sys.stdout = orig_stdout + if orig_stderr is not None: + sys.stderr = orig_stderr + + +@contextlib.contextmanager +def colorama_text(*args, **kwargs): + init(*args, **kwargs) + try: + yield + finally: + deinit() + + +def reinit(): + if wrapped_stdout is not None: + sys.stdout = wrapped_stdout + if wrapped_stderr is not None: + sys.stderr = wrapped_stderr + + +def wrap_stream(stream, convert, strip, autoreset, wrap): + if wrap: + wrapper = AnsiToWin32(stream, + convert=convert, strip=strip, autoreset=autoreset) + if wrapper.should_wrap(): + stream = wrapper.stream + return stream + + diff --git a/RenderPipelineFile/rplibs/colorama/win32.py b/RenderPipelineFile/rplibs/colorama/win32.py index 3d1d2f2d..1ce75de4 100644 --- a/RenderPipelineFile/rplibs/colorama/win32.py +++ b/RenderPipelineFile/rplibs/colorama/win32.py @@ -1,154 +1,154 @@ -# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. - -# from winbase.h -STDOUT = -11 -STDERR = -12 - -try: - import ctypes - from ctypes import LibraryLoader - windll = LibraryLoader(ctypes.WinDLL) - from ctypes import wintypes -except (AttributeError, ImportError): - windll = None - SetConsoleTextAttribute = lambda *_: None - winapi_test = lambda *_: None -else: - from ctypes import byref, Structure, c_char, POINTER - - COORD = wintypes._COORD - - class CONSOLE_SCREEN_BUFFER_INFO(Structure): - """struct in wincon.h.""" - _fields_ = [ - ("dwSize", COORD), - ("dwCursorPosition", COORD), - ("wAttributes", wintypes.WORD), - ("srWindow", wintypes.SMALL_RECT), - ("dwMaximumWindowSize", COORD), - ] - def __str__(self): - return '(%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d)' % ( - self.dwSize.Y, self.dwSize.X - , self.dwCursorPosition.Y, self.dwCursorPosition.X - , self.wAttributes - , self.srWindow.Top, self.srWindow.Left, self.srWindow.Bottom, self.srWindow.Right - , self.dwMaximumWindowSize.Y, self.dwMaximumWindowSize.X - ) - - _GetStdHandle = windll.kernel32.GetStdHandle - _GetStdHandle.argtypes = [ - wintypes.DWORD, - ] - _GetStdHandle.restype = wintypes.HANDLE - - _GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo - _GetConsoleScreenBufferInfo.argtypes = [ - wintypes.HANDLE, - POINTER(CONSOLE_SCREEN_BUFFER_INFO), - ] - _GetConsoleScreenBufferInfo.restype = wintypes.BOOL - - _SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute - _SetConsoleTextAttribute.argtypes = [ - wintypes.HANDLE, - wintypes.WORD, - ] - _SetConsoleTextAttribute.restype = wintypes.BOOL - - _SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition - _SetConsoleCursorPosition.argtypes = [ - wintypes.HANDLE, - COORD, - ] - _SetConsoleCursorPosition.restype = wintypes.BOOL - - _FillConsoleOutputCharacterA = windll.kernel32.FillConsoleOutputCharacterA - _FillConsoleOutputCharacterA.argtypes = [ - wintypes.HANDLE, - c_char, - wintypes.DWORD, - COORD, - POINTER(wintypes.DWORD), - ] - _FillConsoleOutputCharacterA.restype = wintypes.BOOL - - _FillConsoleOutputAttribute = windll.kernel32.FillConsoleOutputAttribute - _FillConsoleOutputAttribute.argtypes = [ - wintypes.HANDLE, - wintypes.WORD, - wintypes.DWORD, - COORD, - POINTER(wintypes.DWORD), - ] - _FillConsoleOutputAttribute.restype = wintypes.BOOL - - _SetConsoleTitleW = windll.kernel32.SetConsoleTitleA - _SetConsoleTitleW.argtypes = [ - wintypes.LPCSTR - ] - _SetConsoleTitleW.restype = wintypes.BOOL - - handles = { - STDOUT: _GetStdHandle(STDOUT), - STDERR: _GetStdHandle(STDERR), - } - - def winapi_test(): - handle = handles[STDOUT] - csbi = CONSOLE_SCREEN_BUFFER_INFO() - success = _GetConsoleScreenBufferInfo( - handle, byref(csbi)) - return bool(success) - - def GetConsoleScreenBufferInfo(stream_id=STDOUT): - handle = handles[stream_id] - csbi = CONSOLE_SCREEN_BUFFER_INFO() - success = _GetConsoleScreenBufferInfo( - handle, byref(csbi)) - return csbi - - def SetConsoleTextAttribute(stream_id, attrs): - handle = handles[stream_id] - return _SetConsoleTextAttribute(handle, attrs) - - def SetConsoleCursorPosition(stream_id, position, adjust=True): - position = COORD(*position) - # If the position is out of range, do nothing. - if position.Y <= 0 or position.X <= 0: - return - # Adjust for Windows' SetConsoleCursorPosition: - # 1. being 0-based, while ANSI is 1-based. - # 2. expecting (x,y), while ANSI uses (y,x). - adjusted_position = COORD(position.Y - 1, position.X - 1) - if adjust: - # Adjust for viewport's scroll position - sr = GetConsoleScreenBufferInfo(STDOUT).srWindow - adjusted_position.Y += sr.Top - adjusted_position.X += sr.Left - # Resume normal processing - handle = handles[stream_id] - return _SetConsoleCursorPosition(handle, adjusted_position) - - def FillConsoleOutputCharacter(stream_id, char, length, start): - handle = handles[stream_id] - char = c_char(char.encode()) - length = wintypes.DWORD(length) - num_written = wintypes.DWORD(0) - # Note that this is hard-coded for ANSI (vs wide) bytes. - success = _FillConsoleOutputCharacterA( - handle, char, length, start, byref(num_written)) - return num_written.value - - def FillConsoleOutputAttribute(stream_id, attr, length, start): - ''' FillConsoleOutputAttribute( hConsole, csbi.wAttributes, dwConSize, coordScreen, &cCharsWritten )''' - handle = handles[stream_id] - attribute = wintypes.WORD(attr) - length = wintypes.DWORD(length) - num_written = wintypes.DWORD(0) - # Note that this is hard-coded for ANSI (vs wide) bytes. - return _FillConsoleOutputAttribute( - handle, attribute, length, start, byref(num_written)) - - def SetConsoleTitle(title): - return _SetConsoleTitleW(title) +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. + +# from winbase.h +STDOUT = -11 +STDERR = -12 + +try: + import ctypes + from ctypes import LibraryLoader + windll = LibraryLoader(ctypes.WinDLL) + from ctypes import wintypes +except (AttributeError, ImportError): + windll = None + SetConsoleTextAttribute = lambda *_: None + winapi_test = lambda *_: None +else: + from ctypes import byref, Structure, c_char, POINTER + + COORD = wintypes._COORD + + class CONSOLE_SCREEN_BUFFER_INFO(Structure): + """struct in wincon.h.""" + _fields_ = [ + ("dwSize", COORD), + ("dwCursorPosition", COORD), + ("wAttributes", wintypes.WORD), + ("srWindow", wintypes.SMALL_RECT), + ("dwMaximumWindowSize", COORD), + ] + def __str__(self): + return '(%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d)' % ( + self.dwSize.Y, self.dwSize.X + , self.dwCursorPosition.Y, self.dwCursorPosition.X + , self.wAttributes + , self.srWindow.Top, self.srWindow.Left, self.srWindow.Bottom, self.srWindow.Right + , self.dwMaximumWindowSize.Y, self.dwMaximumWindowSize.X + ) + + _GetStdHandle = windll.kernel32.GetStdHandle + _GetStdHandle.argtypes = [ + wintypes.DWORD, + ] + _GetStdHandle.restype = wintypes.HANDLE + + _GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo + _GetConsoleScreenBufferInfo.argtypes = [ + wintypes.HANDLE, + POINTER(CONSOLE_SCREEN_BUFFER_INFO), + ] + _GetConsoleScreenBufferInfo.restype = wintypes.BOOL + + _SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute + _SetConsoleTextAttribute.argtypes = [ + wintypes.HANDLE, + wintypes.WORD, + ] + _SetConsoleTextAttribute.restype = wintypes.BOOL + + _SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition + _SetConsoleCursorPosition.argtypes = [ + wintypes.HANDLE, + COORD, + ] + _SetConsoleCursorPosition.restype = wintypes.BOOL + + _FillConsoleOutputCharacterA = windll.kernel32.FillConsoleOutputCharacterA + _FillConsoleOutputCharacterA.argtypes = [ + wintypes.HANDLE, + c_char, + wintypes.DWORD, + COORD, + POINTER(wintypes.DWORD), + ] + _FillConsoleOutputCharacterA.restype = wintypes.BOOL + + _FillConsoleOutputAttribute = windll.kernel32.FillConsoleOutputAttribute + _FillConsoleOutputAttribute.argtypes = [ + wintypes.HANDLE, + wintypes.WORD, + wintypes.DWORD, + COORD, + POINTER(wintypes.DWORD), + ] + _FillConsoleOutputAttribute.restype = wintypes.BOOL + + _SetConsoleTitleW = windll.kernel32.SetConsoleTitleA + _SetConsoleTitleW.argtypes = [ + wintypes.LPCSTR + ] + _SetConsoleTitleW.restype = wintypes.BOOL + + handles = { + STDOUT: _GetStdHandle(STDOUT), + STDERR: _GetStdHandle(STDERR), + } + + def winapi_test(): + handle = handles[STDOUT] + csbi = CONSOLE_SCREEN_BUFFER_INFO() + success = _GetConsoleScreenBufferInfo( + handle, byref(csbi)) + return bool(success) + + def GetConsoleScreenBufferInfo(stream_id=STDOUT): + handle = handles[stream_id] + csbi = CONSOLE_SCREEN_BUFFER_INFO() + success = _GetConsoleScreenBufferInfo( + handle, byref(csbi)) + return csbi + + def SetConsoleTextAttribute(stream_id, attrs): + handle = handles[stream_id] + return _SetConsoleTextAttribute(handle, attrs) + + def SetConsoleCursorPosition(stream_id, position, adjust=True): + position = COORD(*position) + # If the position is out of range, do nothing. + if position.Y <= 0 or position.X <= 0: + return + # Adjust for Windows' SetConsoleCursorPosition: + # 1. being 0-based, while ANSI is 1-based. + # 2. expecting (x,y), while ANSI uses (y,x). + adjusted_position = COORD(position.Y - 1, position.X - 1) + if adjust: + # Adjust for viewport's scroll position + sr = GetConsoleScreenBufferInfo(STDOUT).srWindow + adjusted_position.Y += sr.Top + adjusted_position.X += sr.Left + # Resume normal processing + handle = handles[stream_id] + return _SetConsoleCursorPosition(handle, adjusted_position) + + def FillConsoleOutputCharacter(stream_id, char, length, start): + handle = handles[stream_id] + char = c_char(char.encode()) + length = wintypes.DWORD(length) + num_written = wintypes.DWORD(0) + # Note that this is hard-coded for ANSI (vs wide) bytes. + success = _FillConsoleOutputCharacterA( + handle, char, length, start, byref(num_written)) + return num_written.value + + def FillConsoleOutputAttribute(stream_id, attr, length, start): + ''' FillConsoleOutputAttribute( hConsole, csbi.wAttributes, dwConSize, coordScreen, &cCharsWritten )''' + handle = handles[stream_id] + attribute = wintypes.WORD(attr) + length = wintypes.DWORD(length) + num_written = wintypes.DWORD(0) + # Note that this is hard-coded for ANSI (vs wide) bytes. + return _FillConsoleOutputAttribute( + handle, attribute, length, start, byref(num_written)) + + def SetConsoleTitle(title): + return _SetConsoleTitleW(title) diff --git a/RenderPipelineFile/rplibs/colorama/winterm.py b/RenderPipelineFile/rplibs/colorama/winterm.py index 60309d3c..385862e1 100644 --- a/RenderPipelineFile/rplibs/colorama/winterm.py +++ b/RenderPipelineFile/rplibs/colorama/winterm.py @@ -1,162 +1,162 @@ -# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. -from . import win32 - - -# from wincon.h -class WinColor(object): - BLACK = 0 - BLUE = 1 - GREEN = 2 - CYAN = 3 - RED = 4 - MAGENTA = 5 - YELLOW = 6 - GREY = 7 - -# from wincon.h -class WinStyle(object): - NORMAL = 0x00 # dim text, dim background - BRIGHT = 0x08 # bright text, dim background - BRIGHT_BACKGROUND = 0x80 # dim text, bright background - -class WinTerm(object): - - def __init__(self): - self._default = win32.GetConsoleScreenBufferInfo(win32.STDOUT).wAttributes - self.set_attrs(self._default) - self._default_fore = self._fore - self._default_back = self._back - self._default_style = self._style - # In order to emulate LIGHT_EX in windows, we borrow the BRIGHT style. - # So that LIGHT_EX colors and BRIGHT style do not clobber each other, - # we track them separately, since LIGHT_EX is overwritten by Fore/Back - # and BRIGHT is overwritten by Style codes. - self._light = 0 - - def get_attrs(self): - return self._fore + self._back * 16 + (self._style | self._light) - - def set_attrs(self, value): - self._fore = value & 7 - self._back = (value >> 4) & 7 - self._style = value & (WinStyle.BRIGHT | WinStyle.BRIGHT_BACKGROUND) - - def reset_all(self, on_stderr=None): - self.set_attrs(self._default) - self.set_console(attrs=self._default) - - def fore(self, fore=None, light=False, on_stderr=False): - if fore is None: - fore = self._default_fore - self._fore = fore - # Emulate LIGHT_EX with BRIGHT Style - if light: - self._light |= WinStyle.BRIGHT - else: - self._light &= ~WinStyle.BRIGHT - self.set_console(on_stderr=on_stderr) - - def back(self, back=None, light=False, on_stderr=False): - if back is None: - back = self._default_back - self._back = back - # Emulate LIGHT_EX with BRIGHT_BACKGROUND Style - if light: - self._light |= WinStyle.BRIGHT_BACKGROUND - else: - self._light &= ~WinStyle.BRIGHT_BACKGROUND - self.set_console(on_stderr=on_stderr) - - def style(self, style=None, on_stderr=False): - if style is None: - style = self._default_style - self._style = style - self.set_console(on_stderr=on_stderr) - - def set_console(self, attrs=None, on_stderr=False): - if attrs is None: - attrs = self.get_attrs() - handle = win32.STDOUT - if on_stderr: - handle = win32.STDERR - win32.SetConsoleTextAttribute(handle, attrs) - - def get_position(self, handle): - position = win32.GetConsoleScreenBufferInfo(handle).dwCursorPosition - # Because Windows coordinates are 0-based, - # and win32.SetConsoleCursorPosition expects 1-based. - position.X += 1 - position.Y += 1 - return position - - def set_cursor_position(self, position=None, on_stderr=False): - if position is None: - # I'm not currently tracking the position, so there is no default. - # position = self.get_position() - return - handle = win32.STDOUT - if on_stderr: - handle = win32.STDERR - win32.SetConsoleCursorPosition(handle, position) - - def cursor_adjust(self, x, y, on_stderr=False): - handle = win32.STDOUT - if on_stderr: - handle = win32.STDERR - position = self.get_position(handle) - adjusted_position = (position.Y + y, position.X + x) - win32.SetConsoleCursorPosition(handle, adjusted_position, adjust=False) - - def erase_screen(self, mode=0, on_stderr=False): - # 0 should clear from the cursor to the end of the screen. - # 1 should clear from the cursor to the beginning of the screen. - # 2 should clear the entire screen, and move cursor to (1,1) - handle = win32.STDOUT - if on_stderr: - handle = win32.STDERR - csbi = win32.GetConsoleScreenBufferInfo(handle) - # get the number of character cells in the current buffer - cells_in_screen = csbi.dwSize.X * csbi.dwSize.Y - # get number of character cells before current cursor position - cells_before_cursor = csbi.dwSize.X * csbi.dwCursorPosition.Y + csbi.dwCursorPosition.X - if mode == 0: - from_coord = csbi.dwCursorPosition - cells_to_erase = cells_in_screen - cells_before_cursor - if mode == 1: - from_coord = win32.COORD(0, 0) - cells_to_erase = cells_before_cursor - elif mode == 2: - from_coord = win32.COORD(0, 0) - cells_to_erase = cells_in_screen - # fill the entire screen with blanks - win32.FillConsoleOutputCharacter(handle, ' ', cells_to_erase, from_coord) - # now set the buffer's attributes accordingly - win32.FillConsoleOutputAttribute(handle, self.get_attrs(), cells_to_erase, from_coord) - if mode == 2: - # put the cursor where needed - win32.SetConsoleCursorPosition(handle, (1, 1)) - - def erase_line(self, mode=0, on_stderr=False): - # 0 should clear from the cursor to the end of the line. - # 1 should clear from the cursor to the beginning of the line. - # 2 should clear the entire line. - handle = win32.STDOUT - if on_stderr: - handle = win32.STDERR - csbi = win32.GetConsoleScreenBufferInfo(handle) - if mode == 0: - from_coord = csbi.dwCursorPosition - cells_to_erase = csbi.dwSize.X - csbi.dwCursorPosition.X - if mode == 1: - from_coord = win32.COORD(0, csbi.dwCursorPosition.Y) - cells_to_erase = csbi.dwCursorPosition.X - elif mode == 2: - from_coord = win32.COORD(0, csbi.dwCursorPosition.Y) - cells_to_erase = csbi.dwSize.X - # fill the entire screen with blanks - win32.FillConsoleOutputCharacter(handle, ' ', cells_to_erase, from_coord) - # now set the buffer's attributes accordingly - win32.FillConsoleOutputAttribute(handle, self.get_attrs(), cells_to_erase, from_coord) - - def set_title(self, title): - win32.SetConsoleTitle(title) +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +from . import win32 + + +# from wincon.h +class WinColor(object): + BLACK = 0 + BLUE = 1 + GREEN = 2 + CYAN = 3 + RED = 4 + MAGENTA = 5 + YELLOW = 6 + GREY = 7 + +# from wincon.h +class WinStyle(object): + NORMAL = 0x00 # dim text, dim background + BRIGHT = 0x08 # bright text, dim background + BRIGHT_BACKGROUND = 0x80 # dim text, bright background + +class WinTerm(object): + + def __init__(self): + self._default = win32.GetConsoleScreenBufferInfo(win32.STDOUT).wAttributes + self.set_attrs(self._default) + self._default_fore = self._fore + self._default_back = self._back + self._default_style = self._style + # In order to emulate LIGHT_EX in windows, we borrow the BRIGHT style. + # So that LIGHT_EX colors and BRIGHT style do not clobber each other, + # we track them separately, since LIGHT_EX is overwritten by Fore/Back + # and BRIGHT is overwritten by Style codes. + self._light = 0 + + def get_attrs(self): + return self._fore + self._back * 16 + (self._style | self._light) + + def set_attrs(self, value): + self._fore = value & 7 + self._back = (value >> 4) & 7 + self._style = value & (WinStyle.BRIGHT | WinStyle.BRIGHT_BACKGROUND) + + def reset_all(self, on_stderr=None): + self.set_attrs(self._default) + self.set_console(attrs=self._default) + + def fore(self, fore=None, light=False, on_stderr=False): + if fore is None: + fore = self._default_fore + self._fore = fore + # Emulate LIGHT_EX with BRIGHT Style + if light: + self._light |= WinStyle.BRIGHT + else: + self._light &= ~WinStyle.BRIGHT + self.set_console(on_stderr=on_stderr) + + def back(self, back=None, light=False, on_stderr=False): + if back is None: + back = self._default_back + self._back = back + # Emulate LIGHT_EX with BRIGHT_BACKGROUND Style + if light: + self._light |= WinStyle.BRIGHT_BACKGROUND + else: + self._light &= ~WinStyle.BRIGHT_BACKGROUND + self.set_console(on_stderr=on_stderr) + + def style(self, style=None, on_stderr=False): + if style is None: + style = self._default_style + self._style = style + self.set_console(on_stderr=on_stderr) + + def set_console(self, attrs=None, on_stderr=False): + if attrs is None: + attrs = self.get_attrs() + handle = win32.STDOUT + if on_stderr: + handle = win32.STDERR + win32.SetConsoleTextAttribute(handle, attrs) + + def get_position(self, handle): + position = win32.GetConsoleScreenBufferInfo(handle).dwCursorPosition + # Because Windows coordinates are 0-based, + # and win32.SetConsoleCursorPosition expects 1-based. + position.X += 1 + position.Y += 1 + return position + + def set_cursor_position(self, position=None, on_stderr=False): + if position is None: + # I'm not currently tracking the position, so there is no default. + # position = self.get_position() + return + handle = win32.STDOUT + if on_stderr: + handle = win32.STDERR + win32.SetConsoleCursorPosition(handle, position) + + def cursor_adjust(self, x, y, on_stderr=False): + handle = win32.STDOUT + if on_stderr: + handle = win32.STDERR + position = self.get_position(handle) + adjusted_position = (position.Y + y, position.X + x) + win32.SetConsoleCursorPosition(handle, adjusted_position, adjust=False) + + def erase_screen(self, mode=0, on_stderr=False): + # 0 should clear from the cursor to the end of the screen. + # 1 should clear from the cursor to the beginning of the screen. + # 2 should clear the entire screen, and move cursor to (1,1) + handle = win32.STDOUT + if on_stderr: + handle = win32.STDERR + csbi = win32.GetConsoleScreenBufferInfo(handle) + # get the number of character cells in the current buffer + cells_in_screen = csbi.dwSize.X * csbi.dwSize.Y + # get number of character cells before current cursor position + cells_before_cursor = csbi.dwSize.X * csbi.dwCursorPosition.Y + csbi.dwCursorPosition.X + if mode == 0: + from_coord = csbi.dwCursorPosition + cells_to_erase = cells_in_screen - cells_before_cursor + if mode == 1: + from_coord = win32.COORD(0, 0) + cells_to_erase = cells_before_cursor + elif mode == 2: + from_coord = win32.COORD(0, 0) + cells_to_erase = cells_in_screen + # fill the entire screen with blanks + win32.FillConsoleOutputCharacter(handle, ' ', cells_to_erase, from_coord) + # now set the buffer's attributes accordingly + win32.FillConsoleOutputAttribute(handle, self.get_attrs(), cells_to_erase, from_coord) + if mode == 2: + # put the cursor where needed + win32.SetConsoleCursorPosition(handle, (1, 1)) + + def erase_line(self, mode=0, on_stderr=False): + # 0 should clear from the cursor to the end of the line. + # 1 should clear from the cursor to the beginning of the line. + # 2 should clear the entire line. + handle = win32.STDOUT + if on_stderr: + handle = win32.STDERR + csbi = win32.GetConsoleScreenBufferInfo(handle) + if mode == 0: + from_coord = csbi.dwCursorPosition + cells_to_erase = csbi.dwSize.X - csbi.dwCursorPosition.X + if mode == 1: + from_coord = win32.COORD(0, csbi.dwCursorPosition.Y) + cells_to_erase = csbi.dwCursorPosition.X + elif mode == 2: + from_coord = win32.COORD(0, csbi.dwCursorPosition.Y) + cells_to_erase = csbi.dwSize.X + # fill the entire screen with blanks + win32.FillConsoleOutputCharacter(handle, ' ', cells_to_erase, from_coord) + # now set the buffer's attributes accordingly + win32.FillConsoleOutputAttribute(handle, self.get_attrs(), cells_to_erase, from_coord) + + def set_title(self, title): + win32.SetConsoleTitle(title) diff --git a/RenderPipelineFile/rplibs/progressbar/LICENSE.txt b/RenderPipelineFile/rplibs/progressbar/LICENSE.txt index fc8ccdc1..8cbe060b 100644 --- a/RenderPipelineFile/rplibs/progressbar/LICENSE.txt +++ b/RenderPipelineFile/rplibs/progressbar/LICENSE.txt @@ -1,52 +1,52 @@ -You can redistribute and/or modify this library under the terms of the -GNU LGPL license or BSD license (or both). - ---- - -progressbar - Text progress bar library for python. -Copyright (C) 2005 Nilton Volpato - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 2.1 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - ---- - -progressbar - Text progress bar library for python -Copyright (c) 2008 Nilton Volpato - -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - a. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - b. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - c. Neither the name of the author nor the names of its contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY -OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH -DAMAGE. +You can redistribute and/or modify this library under the terms of the +GNU LGPL license or BSD license (or both). + +--- + +progressbar - Text progress bar library for python. +Copyright (C) 2005 Nilton Volpato + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +--- + +progressbar - Text progress bar library for python +Copyright (c) 2008 Nilton Volpato + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + a. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + b. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + c. Neither the name of the author nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/RenderPipelineFile/rplibs/progressbar/SOURCE.txt b/RenderPipelineFile/rplibs/progressbar/SOURCE.txt index ee5034c3..e2601bf2 100644 --- a/RenderPipelineFile/rplibs/progressbar/SOURCE.txt +++ b/RenderPipelineFile/rplibs/progressbar/SOURCE.txt @@ -1,6 +1,6 @@ -package: progressbar -version: 2.3 -downloaded: 07.04.2016 -url: https://pypi.python.org/pypi/progressbar - -!! MODIFIED __init__.py to use relative imports !! +package: progressbar +version: 2.3 +downloaded: 07.04.2016 +url: https://pypi.python.org/pypi/progressbar + +!! MODIFIED __init__.py to use relative imports !! diff --git a/RenderPipelineFile/rplibs/progressbar/__init__.py b/RenderPipelineFile/rplibs/progressbar/__init__.py index 70338793..f0361940 100644 --- a/RenderPipelineFile/rplibs/progressbar/__init__.py +++ b/RenderPipelineFile/rplibs/progressbar/__init__.py @@ -1,323 +1,323 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# progressbar - Text progress bar library for Python. -# Copyright (c) 2005 Nilton Volpato -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - - -'''Text progress bar library for Python. - -A text progress bar is typically used to display the progress of a long -running operation, providing a visual cue that processing is underway. - -The ProgressBar class manages the current progress, and the format of the line -is given by a number of widgets. A widget is an object that may display -differently depending on the state of the progress bar. There are three types -of widgets: - - a string, which always shows itself - - - a ProgressBarWidget, which may return a different value every time its - update method is called - - - a ProgressBarWidgetHFill, which is like ProgressBarWidget, except it - expands to fill the remaining width of the line. - -The progressbar module is very easy to use, yet very powerful. It will also -automatically enable features like auto-resizing when the system supports it. -''' - -from __future__ import division - -import math -import os -import signal -import sys -import time - -try: - from fcntl import ioctl - from array import array - import termios -except ImportError: - pass - -from .compat import * -from .widgets import * - - -__author__ = 'Nilton Volpato' -__author_email__ = 'first-name dot last-name @ gmail.com' -__date__ = '2011-05-14' -__version__ = '2.3' - - -class UnknownLength: pass - - -class ProgressBar(object): - '''The ProgressBar class which updates and prints the bar. - - A common way of using it is like: - >>> pbar = ProgressBar().start() - >>> for i in range(100): - ... # do something - ... pbar.update(i+1) - ... - >>> pbar.finish() - - You can also use a ProgressBar as an iterator: - >>> progress = ProgressBar() - >>> for i in progress(some_iterable): - ... # do something - ... - - Since the progress bar is incredibly customizable you can specify - different widgets of any type in any order. You can even write your own - widgets! However, since there are already a good number of widgets you - should probably play around with them before moving on to create your own - widgets. - - The term_width parameter represents the current terminal width. If the - parameter is set to an integer then the progress bar will use that, - otherwise it will attempt to determine the terminal width falling back to - 80 columns if the width cannot be determined. - - When implementing a widget's update method you are passed a reference to - the current progress bar. As a result, you have access to the - ProgressBar's methods and attributes. Although there is nothing preventing - you from changing the ProgressBar you should treat it as read only. - - Useful methods and attributes include (Public API): - - currval: current progress (0 <= currval <= maxval) - - maxval: maximum (and final) value - - finished: True if the bar has finished (reached 100%) - - start_time: the time when start() method of ProgressBar was called - - seconds_elapsed: seconds elapsed since start_time and last call to - update - - percentage(): progress in percent [0..100] - ''' - - __slots__ = ('currval', 'fd', 'finished', 'last_update_time', - 'left_justify', 'maxval', 'next_update', 'num_intervals', - 'poll', 'seconds_elapsed', 'signal_set', 'start_time', - 'term_width', 'update_interval', 'widgets', '_time_sensitive', - '__iterable') - - _DEFAULT_MAXVAL = 100 - _DEFAULT_TERMSIZE = 80 - _DEFAULT_WIDGETS = [Percentage(), ' ', Bar()] - - def __init__(self, maxval=None, widgets=None, term_width=None, poll=1, - left_justify=True, fd=sys.stderr): - '''Initializes a progress bar with sane defaults''' - - # Don't share a reference with any other progress bars - if widgets is None: - widgets = list(self._DEFAULT_WIDGETS) - - self.maxval = maxval - self.widgets = widgets - self.fd = fd - self.left_justify = left_justify - - self.signal_set = False - if term_width is not None: - self.term_width = term_width - else: - try: - self._handle_resize() - signal.signal(signal.SIGWINCH, self._handle_resize) - self.signal_set = True - except (SystemExit, KeyboardInterrupt): raise - except: - self.term_width = self._env_size() - - self.__iterable = None - self._update_widgets() - self.currval = 0 - self.finished = False - self.last_update_time = None - self.poll = poll - self.seconds_elapsed = 0 - self.start_time = None - self.update_interval = 1 - - - def __call__(self, iterable): - 'Use a ProgressBar to iterate through an iterable' - - try: - self.maxval = len(iterable) - except: - if self.maxval is None: - self.maxval = UnknownLength - - self.__iterable = iter(iterable) - return self - - - def __iter__(self): - return self - - - def __next__(self): - try: - value = next(self.__iterable) - if self.start_time is None: self.start() - else: self.update(self.currval + 1) - return value - except StopIteration: - self.finish() - raise - - - # Create an alias so that Python 2.x won't complain about not being - # an iterator. - next = __next__ - - - def _env_size(self): - 'Tries to find the term_width from the environment.' - - return int(os.environ.get('COLUMNS', self._DEFAULT_TERMSIZE)) - 1 - - - def _handle_resize(self, signum=None, frame=None): - 'Tries to catch resize signals sent from the terminal.' - - h, w = array('h', ioctl(self.fd, termios.TIOCGWINSZ, '\0' * 8))[:2] - self.term_width = w - - - def percentage(self): - 'Returns the progress as a percentage.' - return self.currval * 100.0 / self.maxval - - percent = property(percentage) - - - def _format_widgets(self): - result = [] - expanding = [] - width = self.term_width - - for index, widget in enumerate(self.widgets): - if isinstance(widget, WidgetHFill): - result.append(widget) - expanding.insert(0, index) - else: - widget = format_updatable(widget, self) - result.append(widget) - width -= len(widget) - - count = len(expanding) - while count: - portion = max(int(math.ceil(width * 1. / count)), 0) - index = expanding.pop() - count -= 1 - - widget = result[index].update(self, portion) - width -= len(widget) - result[index] = widget - - return result - - - def _format_line(self): - 'Joins the widgets and justifies the line' - - widgets = ''.join(self._format_widgets()) - - if self.left_justify: return widgets.ljust(self.term_width) - else: return widgets.rjust(self.term_width) - - - def _need_update(self): - 'Returns whether the ProgressBar should redraw the line.' - if self.currval >= self.next_update or self.finished: return True - - delta = time.time() - self.last_update_time - return self._time_sensitive and delta > self.poll - - - def _update_widgets(self): - 'Checks all widgets for the time sensitive bit' - - self._time_sensitive = any(getattr(w, 'TIME_SENSITIVE', False) - for w in self.widgets) - - - def update(self, value=None): - 'Updates the ProgressBar to a new value.' - - if value is not None and value is not UnknownLength: - if (self.maxval is not UnknownLength - and not 0 <= value <= self.maxval): - - raise ValueError('Value out of range') - - self.currval = value - - - if not self._need_update(): return - if self.start_time is None: - raise RuntimeError('You must call "start" before calling "update"') - - now = time.time() - self.seconds_elapsed = now - self.start_time - self.next_update = self.currval + self.update_interval - self.fd.write(self._format_line() + '\r') - self.last_update_time = now - - - def start(self): - '''Starts measuring time, and prints the bar at 0%. - - It returns self so you can use it like this: - >>> pbar = ProgressBar().start() - >>> for i in range(100): - ... # do something - ... pbar.update(i+1) - ... - >>> pbar.finish() - ''' - - if self.maxval is None: - self.maxval = self._DEFAULT_MAXVAL - - self.num_intervals = max(100, self.term_width) - self.next_update = 0 - - if self.maxval is not UnknownLength: - if self.maxval < 0: raise ValueError('Value out of range') - self.update_interval = self.maxval / self.num_intervals - - - self.start_time = self.last_update_time = time.time() - self.update(0) - - return self - - - def finish(self): - 'Puts the ProgressBar bar in the finished state.' - - self.finished = True - self.update(self.maxval) - self.fd.write('\n') - if self.signal_set: - signal.signal(signal.SIGWINCH, signal.SIG_DFL) +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# progressbar - Text progress bar library for Python. +# Copyright (c) 2005 Nilton Volpato +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +'''Text progress bar library for Python. + +A text progress bar is typically used to display the progress of a long +running operation, providing a visual cue that processing is underway. + +The ProgressBar class manages the current progress, and the format of the line +is given by a number of widgets. A widget is an object that may display +differently depending on the state of the progress bar. There are three types +of widgets: + - a string, which always shows itself + + - a ProgressBarWidget, which may return a different value every time its + update method is called + + - a ProgressBarWidgetHFill, which is like ProgressBarWidget, except it + expands to fill the remaining width of the line. + +The progressbar module is very easy to use, yet very powerful. It will also +automatically enable features like auto-resizing when the system supports it. +''' + +from __future__ import division + +import math +import os +import signal +import sys +import time + +try: + from fcntl import ioctl + from array import array + import termios +except ImportError: + pass + +from .compat import * +from .widgets import * + + +__author__ = 'Nilton Volpato' +__author_email__ = 'first-name dot last-name @ gmail.com' +__date__ = '2011-05-14' +__version__ = '2.3' + + +class UnknownLength: pass + + +class ProgressBar(object): + '''The ProgressBar class which updates and prints the bar. + + A common way of using it is like: + >>> pbar = ProgressBar().start() + >>> for i in range(100): + ... # do something + ... pbar.update(i+1) + ... + >>> pbar.finish() + + You can also use a ProgressBar as an iterator: + >>> progress = ProgressBar() + >>> for i in progress(some_iterable): + ... # do something + ... + + Since the progress bar is incredibly customizable you can specify + different widgets of any type in any order. You can even write your own + widgets! However, since there are already a good number of widgets you + should probably play around with them before moving on to create your own + widgets. + + The term_width parameter represents the current terminal width. If the + parameter is set to an integer then the progress bar will use that, + otherwise it will attempt to determine the terminal width falling back to + 80 columns if the width cannot be determined. + + When implementing a widget's update method you are passed a reference to + the current progress bar. As a result, you have access to the + ProgressBar's methods and attributes. Although there is nothing preventing + you from changing the ProgressBar you should treat it as read only. + + Useful methods and attributes include (Public API): + - currval: current progress (0 <= currval <= maxval) + - maxval: maximum (and final) value + - finished: True if the bar has finished (reached 100%) + - start_time: the time when start() method of ProgressBar was called + - seconds_elapsed: seconds elapsed since start_time and last call to + update + - percentage(): progress in percent [0..100] + ''' + + __slots__ = ('currval', 'fd', 'finished', 'last_update_time', + 'left_justify', 'maxval', 'next_update', 'num_intervals', + 'poll', 'seconds_elapsed', 'signal_set', 'start_time', + 'term_width', 'update_interval', 'widgets', '_time_sensitive', + '__iterable') + + _DEFAULT_MAXVAL = 100 + _DEFAULT_TERMSIZE = 80 + _DEFAULT_WIDGETS = [Percentage(), ' ', Bar()] + + def __init__(self, maxval=None, widgets=None, term_width=None, poll=1, + left_justify=True, fd=sys.stderr): + '''Initializes a progress bar with sane defaults''' + + # Don't share a reference with any other progress bars + if widgets is None: + widgets = list(self._DEFAULT_WIDGETS) + + self.maxval = maxval + self.widgets = widgets + self.fd = fd + self.left_justify = left_justify + + self.signal_set = False + if term_width is not None: + self.term_width = term_width + else: + try: + self._handle_resize() + signal.signal(signal.SIGWINCH, self._handle_resize) + self.signal_set = True + except (SystemExit, KeyboardInterrupt): raise + except: + self.term_width = self._env_size() + + self.__iterable = None + self._update_widgets() + self.currval = 0 + self.finished = False + self.last_update_time = None + self.poll = poll + self.seconds_elapsed = 0 + self.start_time = None + self.update_interval = 1 + + + def __call__(self, iterable): + 'Use a ProgressBar to iterate through an iterable' + + try: + self.maxval = len(iterable) + except: + if self.maxval is None: + self.maxval = UnknownLength + + self.__iterable = iter(iterable) + return self + + + def __iter__(self): + return self + + + def __next__(self): + try: + value = next(self.__iterable) + if self.start_time is None: self.start() + else: self.update(self.currval + 1) + return value + except StopIteration: + self.finish() + raise + + + # Create an alias so that Python 2.x won't complain about not being + # an iterator. + next = __next__ + + + def _env_size(self): + 'Tries to find the term_width from the environment.' + + return int(os.environ.get('COLUMNS', self._DEFAULT_TERMSIZE)) - 1 + + + def _handle_resize(self, signum=None, frame=None): + 'Tries to catch resize signals sent from the terminal.' + + h, w = array('h', ioctl(self.fd, termios.TIOCGWINSZ, '\0' * 8))[:2] + self.term_width = w + + + def percentage(self): + 'Returns the progress as a percentage.' + return self.currval * 100.0 / self.maxval + + percent = property(percentage) + + + def _format_widgets(self): + result = [] + expanding = [] + width = self.term_width + + for index, widget in enumerate(self.widgets): + if isinstance(widget, WidgetHFill): + result.append(widget) + expanding.insert(0, index) + else: + widget = format_updatable(widget, self) + result.append(widget) + width -= len(widget) + + count = len(expanding) + while count: + portion = max(int(math.ceil(width * 1. / count)), 0) + index = expanding.pop() + count -= 1 + + widget = result[index].update(self, portion) + width -= len(widget) + result[index] = widget + + return result + + + def _format_line(self): + 'Joins the widgets and justifies the line' + + widgets = ''.join(self._format_widgets()) + + if self.left_justify: return widgets.ljust(self.term_width) + else: return widgets.rjust(self.term_width) + + + def _need_update(self): + 'Returns whether the ProgressBar should redraw the line.' + if self.currval >= self.next_update or self.finished: return True + + delta = time.time() - self.last_update_time + return self._time_sensitive and delta > self.poll + + + def _update_widgets(self): + 'Checks all widgets for the time sensitive bit' + + self._time_sensitive = any(getattr(w, 'TIME_SENSITIVE', False) + for w in self.widgets) + + + def update(self, value=None): + 'Updates the ProgressBar to a new value.' + + if value is not None and value is not UnknownLength: + if (self.maxval is not UnknownLength + and not 0 <= value <= self.maxval): + + raise ValueError('Value out of range') + + self.currval = value + + + if not self._need_update(): return + if self.start_time is None: + raise RuntimeError('You must call "start" before calling "update"') + + now = time.time() + self.seconds_elapsed = now - self.start_time + self.next_update = self.currval + self.update_interval + self.fd.write(self._format_line() + '\r') + self.last_update_time = now + + + def start(self): + '''Starts measuring time, and prints the bar at 0%. + + It returns self so you can use it like this: + >>> pbar = ProgressBar().start() + >>> for i in range(100): + ... # do something + ... pbar.update(i+1) + ... + >>> pbar.finish() + ''' + + if self.maxval is None: + self.maxval = self._DEFAULT_MAXVAL + + self.num_intervals = max(100, self.term_width) + self.next_update = 0 + + if self.maxval is not UnknownLength: + if self.maxval < 0: raise ValueError('Value out of range') + self.update_interval = self.maxval / self.num_intervals + + + self.start_time = self.last_update_time = time.time() + self.update(0) + + return self + + + def finish(self): + 'Puts the ProgressBar bar in the finished state.' + + self.finished = True + self.update(self.maxval) + self.fd.write('\n') + if self.signal_set: + signal.signal(signal.SIGWINCH, signal.SIG_DFL) diff --git a/RenderPipelineFile/rplibs/progressbar/compat.py b/RenderPipelineFile/rplibs/progressbar/compat.py index 2514e7a4..f3d6b373 100644 --- a/RenderPipelineFile/rplibs/progressbar/compat.py +++ b/RenderPipelineFile/rplibs/progressbar/compat.py @@ -1,42 +1,42 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# progressbar - Text progress bar library for Python. -# Copyright (c) 2005 Nilton Volpato -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - -'''Compatability methods and classes for the progressbar module''' - - -# Python 3.x (and backports) use a modified iterator syntax -# This will allow 2.x to behave with 3.x iterators -if not hasattr(__builtins__, 'next'): - def next(iter): - try: - # Try new style iterators - return iter.__next__() - except AttributeError: - # Fallback in case of a "native" iterator - return iter.__next__() - - -# Python < 2.5 does not have "any" -if not hasattr(__builtins__, 'any'): - def any(iterator): - for item in iterator: - if item: return True - - return False +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# progressbar - Text progress bar library for Python. +# Copyright (c) 2005 Nilton Volpato +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +'''Compatability methods and classes for the progressbar module''' + + +# Python 3.x (and backports) use a modified iterator syntax +# This will allow 2.x to behave with 3.x iterators +if not hasattr(__builtins__, 'next'): + def next(iter): + try: + # Try new style iterators + return iter.__next__() + except AttributeError: + # Fallback in case of a "native" iterator + return iter.__next__() + + +# Python < 2.5 does not have "any" +if not hasattr(__builtins__, 'any'): + def any(iterator): + for item in iterator: + if item: return True + + return False diff --git a/RenderPipelineFile/rplibs/progressbar/widgets.py b/RenderPipelineFile/rplibs/progressbar/widgets.py index f19713df..06557cb9 100644 --- a/RenderPipelineFile/rplibs/progressbar/widgets.py +++ b/RenderPipelineFile/rplibs/progressbar/widgets.py @@ -1,311 +1,311 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# progressbar - Text progress bar library for Python. -# Copyright (c) 2005 Nilton Volpato -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - -'''Default ProgressBar widgets''' - -from __future__ import division - -import datetime -import math - -try: - from abc import ABCMeta, abstractmethod -except ImportError: - AbstractWidget = object - abstractmethod = lambda fn: fn -else: - AbstractWidget = ABCMeta('AbstractWidget', (object,), {}) - - -def format_updatable(updatable, pbar): - if hasattr(updatable, 'update'): return updatable.update(pbar) - else: return updatable - - -class Widget(AbstractWidget): - '''The base class for all widgets - - The ProgressBar will call the widget's update value when the widget should - be updated. The widget's size may change between calls, but the widget may - display incorrectly if the size changes drastically and repeatedly. - - The boolean TIME_SENSITIVE informs the ProgressBar that it should be - updated more often because it is time sensitive. - ''' - - TIME_SENSITIVE = False - - @abstractmethod - def update(self, pbar): - '''Updates the widget. - - pbar - a reference to the calling ProgressBar - ''' - - -class WidgetHFill(Widget): - '''The base class for all variable width widgets. - - This widget is much like the \\hfill command in TeX, it will expand to - fill the line. You can use more than one in the same line, and they will - all have the same width, and together will fill the line. - ''' - - @abstractmethod - def update(self, pbar, width): - '''Updates the widget providing the total width the widget must fill. - - pbar - a reference to the calling ProgressBar - width - The total width the widget must fill - ''' - - -class Timer(Widget): - 'Widget which displays the elapsed seconds.' - - TIME_SENSITIVE = True - - def __init__(self, format='Elapsed Time: %s'): - self.format = format - - @staticmethod - def format_time(seconds): - 'Formats time as the string "HH:MM:SS".' - - return str(datetime.timedelta(seconds=int(seconds))) - - - def update(self, pbar): - 'Updates the widget to show the elapsed time.' - - return self.format % self.format_time(pbar.seconds_elapsed) - - -class ETA(Timer): - 'Widget which attempts to estimate the time of arrival.' - - TIME_SENSITIVE = True - - def update(self, pbar): - 'Updates the widget to show the ETA or total time when finished.' - - if pbar.currval == 0: - return 'ETA: --:--:--' - elif pbar.finished: - return 'Time: %s' % self.format_time(pbar.seconds_elapsed) - else: - elapsed = pbar.seconds_elapsed - eta = elapsed * pbar.maxval / pbar.currval - elapsed - return 'ETA: %s' % self.format_time(eta) - - -class FileTransferSpeed(Widget): - 'Widget for showing the transfer speed (useful for file transfers).' - - format = '%6.2f %s%s/s' - prefixes = ' kMGTPEZY' - - def __init__(self, unit='B'): - self.unit = unit - - def update(self, pbar): - 'Updates the widget with the current SI prefixed speed.' - - if pbar.seconds_elapsed < 2e-6 or pbar.currval < 2e-6: # =~ 0 - scaled = power = 0 - else: - speed = pbar.currval / pbar.seconds_elapsed - power = int(math.log(speed, 1000)) - scaled = speed / 1000.**power - - return self.format % (scaled, self.prefixes[power], self.unit) - -class Rate(Widget): - 'Widget for showing the rate of entries per second' - - def update(self, pbar): - 'Updates the widget with the current SI prefixed speed.' - - - if pbar.seconds_elapsed < 2e-6 or pbar.currval < 2e-6: # =~ 0 - return "Rate: 0 /s" - else: - speed = pbar.currval / pbar.seconds_elapsed - return "Rate: {:4d} /s".format(int(speed)) - - return self.format % (scaled, self.prefixes[power], self.unit) - - -class AnimatedMarker(Widget): - '''An animated marker for the progress bar which defaults to appear as if - it were rotating. - ''' - - def __init__(self, markers='|/-\\'): - self.markers = markers - self.curmark = -1 - - def update(self, pbar): - '''Updates the widget to show the next marker or the first marker when - finished''' - - if pbar.finished: return self.markers[0] - - self.curmark = (self.curmark + 1) % len(self.markers) - return self.markers[self.curmark] - -# Alias for backwards compatibility -RotatingMarker = AnimatedMarker - - -class Counter(Widget): - 'Displays the current count' - - def __init__(self, format='%d'): - self.format = format - - def update(self, pbar): - return self.format % pbar.currval - - -class Percentage(Widget): - 'Displays the current percentage as a number with a percent sign.' - - def update(self, pbar): - return '%3d%%' % pbar.percentage() - - -class FormatLabel(Timer): - 'Displays a formatted label' - - mapping = { - 'elapsed': ('seconds_elapsed', Timer.format_time), - 'finished': ('finished', None), - 'last_update': ('last_update_time', None), - 'max': ('maxval', None), - 'seconds': ('seconds_elapsed', None), - 'start': ('start_time', None), - 'value': ('currval', None) - } - - def __init__(self, format): - self.format = format - - def update(self, pbar): - context = {} - for name, (key, transform) in self.mapping.items(): - try: - value = getattr(pbar, key) - - if transform is None: - context[name] = value - else: - context[name] = transform(value) - except: pass - - return self.format % context - - -class SimpleProgress(Widget): - 'Returns progress as a count of the total (e.g.: "5 of 47")' - - def __init__(self, sep=' of '): - self.sep = sep - - def update(self, pbar): - return '%d%s%d' % (pbar.currval, self.sep, pbar.maxval) - - -class Bar(WidgetHFill): - 'A progress bar which stretches to fill the line.' - - def __init__(self, marker='#', left='|', right='|', fill=' ', - fill_left=True): - '''Creates a customizable progress bar. - - marker - string or updatable object to use as a marker - left - string or updatable object to use as a left border - right - string or updatable object to use as a right border - fill - character to use for the empty part of the progress bar - fill_left - whether to fill from the left or the right - ''' - self.marker = marker - self.left = left - self.right = right - self.fill = fill - self.fill_left = fill_left - - - def update(self, pbar, width): - 'Updates the progress bar and its subcomponents' - - left, marker, right = (format_updatable(i, pbar) for i in - (self.left, self.marker, self.right)) - - width -= len(left) + len(right) - # Marker must *always* have length of 1 - marker *= int(pbar.currval / pbar.maxval * width) - - if self.fill_left: - return '%s%s%s' % (left, marker.ljust(width, self.fill), right) - else: - return '%s%s%s' % (left, marker.rjust(width, self.fill), right) - - -class ReverseBar(Bar): - 'A bar which has a marker which bounces from side to side.' - - def __init__(self, marker='#', left='|', right='|', fill=' ', - fill_left=False): - '''Creates a customizable progress bar. - - marker - string or updatable object to use as a marker - left - string or updatable object to use as a left border - right - string or updatable object to use as a right border - fill - character to use for the empty part of the progress bar - fill_left - whether to fill from the left or the right - ''' - self.marker = marker - self.left = left - self.right = right - self.fill = fill - self.fill_left = fill_left - - -class BouncingBar(Bar): - def update(self, pbar, width): - 'Updates the progress bar and its subcomponents' - - left, marker, right = (format_updatable(i, pbar) for i in - (self.left, self.marker, self.right)) - - width -= len(left) + len(right) - - if pbar.finished: return '%s%s%s' % (left, width * marker, right) - - position = int(pbar.currval % (width * 2 - 1)) - if position > width: position = width * 2 - position - lpad = self.fill * (position - 1) - rpad = self.fill * (width - len(marker) - len(lpad)) - - # Swap if we want to bounce the other way - if not self.fill_left: rpad, lpad = lpad, rpad - - return '%s%s%s%s%s' % (left, lpad, marker, rpad, right) +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# progressbar - Text progress bar library for Python. +# Copyright (c) 2005 Nilton Volpato +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +'''Default ProgressBar widgets''' + +from __future__ import division + +import datetime +import math + +try: + from abc import ABCMeta, abstractmethod +except ImportError: + AbstractWidget = object + abstractmethod = lambda fn: fn +else: + AbstractWidget = ABCMeta('AbstractWidget', (object,), {}) + + +def format_updatable(updatable, pbar): + if hasattr(updatable, 'update'): return updatable.update(pbar) + else: return updatable + + +class Widget(AbstractWidget): + '''The base class for all widgets + + The ProgressBar will call the widget's update value when the widget should + be updated. The widget's size may change between calls, but the widget may + display incorrectly if the size changes drastically and repeatedly. + + The boolean TIME_SENSITIVE informs the ProgressBar that it should be + updated more often because it is time sensitive. + ''' + + TIME_SENSITIVE = False + + @abstractmethod + def update(self, pbar): + '''Updates the widget. + + pbar - a reference to the calling ProgressBar + ''' + + +class WidgetHFill(Widget): + '''The base class for all variable width widgets. + + This widget is much like the \\hfill command in TeX, it will expand to + fill the line. You can use more than one in the same line, and they will + all have the same width, and together will fill the line. + ''' + + @abstractmethod + def update(self, pbar, width): + '''Updates the widget providing the total width the widget must fill. + + pbar - a reference to the calling ProgressBar + width - The total width the widget must fill + ''' + + +class Timer(Widget): + 'Widget which displays the elapsed seconds.' + + TIME_SENSITIVE = True + + def __init__(self, format='Elapsed Time: %s'): + self.format = format + + @staticmethod + def format_time(seconds): + 'Formats time as the string "HH:MM:SS".' + + return str(datetime.timedelta(seconds=int(seconds))) + + + def update(self, pbar): + 'Updates the widget to show the elapsed time.' + + return self.format % self.format_time(pbar.seconds_elapsed) + + +class ETA(Timer): + 'Widget which attempts to estimate the time of arrival.' + + TIME_SENSITIVE = True + + def update(self, pbar): + 'Updates the widget to show the ETA or total time when finished.' + + if pbar.currval == 0: + return 'ETA: --:--:--' + elif pbar.finished: + return 'Time: %s' % self.format_time(pbar.seconds_elapsed) + else: + elapsed = pbar.seconds_elapsed + eta = elapsed * pbar.maxval / pbar.currval - elapsed + return 'ETA: %s' % self.format_time(eta) + + +class FileTransferSpeed(Widget): + 'Widget for showing the transfer speed (useful for file transfers).' + + format = '%6.2f %s%s/s' + prefixes = ' kMGTPEZY' + + def __init__(self, unit='B'): + self.unit = unit + + def update(self, pbar): + 'Updates the widget with the current SI prefixed speed.' + + if pbar.seconds_elapsed < 2e-6 or pbar.currval < 2e-6: # =~ 0 + scaled = power = 0 + else: + speed = pbar.currval / pbar.seconds_elapsed + power = int(math.log(speed, 1000)) + scaled = speed / 1000.**power + + return self.format % (scaled, self.prefixes[power], self.unit) + +class Rate(Widget): + 'Widget for showing the rate of entries per second' + + def update(self, pbar): + 'Updates the widget with the current SI prefixed speed.' + + + if pbar.seconds_elapsed < 2e-6 or pbar.currval < 2e-6: # =~ 0 + return "Rate: 0 /s" + else: + speed = pbar.currval / pbar.seconds_elapsed + return "Rate: {:4d} /s".format(int(speed)) + + return self.format % (scaled, self.prefixes[power], self.unit) + + +class AnimatedMarker(Widget): + '''An animated marker for the progress bar which defaults to appear as if + it were rotating. + ''' + + def __init__(self, markers='|/-\\'): + self.markers = markers + self.curmark = -1 + + def update(self, pbar): + '''Updates the widget to show the next marker or the first marker when + finished''' + + if pbar.finished: return self.markers[0] + + self.curmark = (self.curmark + 1) % len(self.markers) + return self.markers[self.curmark] + +# Alias for backwards compatibility +RotatingMarker = AnimatedMarker + + +class Counter(Widget): + 'Displays the current count' + + def __init__(self, format='%d'): + self.format = format + + def update(self, pbar): + return self.format % pbar.currval + + +class Percentage(Widget): + 'Displays the current percentage as a number with a percent sign.' + + def update(self, pbar): + return '%3d%%' % pbar.percentage() + + +class FormatLabel(Timer): + 'Displays a formatted label' + + mapping = { + 'elapsed': ('seconds_elapsed', Timer.format_time), + 'finished': ('finished', None), + 'last_update': ('last_update_time', None), + 'max': ('maxval', None), + 'seconds': ('seconds_elapsed', None), + 'start': ('start_time', None), + 'value': ('currval', None) + } + + def __init__(self, format): + self.format = format + + def update(self, pbar): + context = {} + for name, (key, transform) in self.mapping.items(): + try: + value = getattr(pbar, key) + + if transform is None: + context[name] = value + else: + context[name] = transform(value) + except: pass + + return self.format % context + + +class SimpleProgress(Widget): + 'Returns progress as a count of the total (e.g.: "5 of 47")' + + def __init__(self, sep=' of '): + self.sep = sep + + def update(self, pbar): + return '%d%s%d' % (pbar.currval, self.sep, pbar.maxval) + + +class Bar(WidgetHFill): + 'A progress bar which stretches to fill the line.' + + def __init__(self, marker='#', left='|', right='|', fill=' ', + fill_left=True): + '''Creates a customizable progress bar. + + marker - string or updatable object to use as a marker + left - string or updatable object to use as a left border + right - string or updatable object to use as a right border + fill - character to use for the empty part of the progress bar + fill_left - whether to fill from the left or the right + ''' + self.marker = marker + self.left = left + self.right = right + self.fill = fill + self.fill_left = fill_left + + + def update(self, pbar, width): + 'Updates the progress bar and its subcomponents' + + left, marker, right = (format_updatable(i, pbar) for i in + (self.left, self.marker, self.right)) + + width -= len(left) + len(right) + # Marker must *always* have length of 1 + marker *= int(pbar.currval / pbar.maxval * width) + + if self.fill_left: + return '%s%s%s' % (left, marker.ljust(width, self.fill), right) + else: + return '%s%s%s' % (left, marker.rjust(width, self.fill), right) + + +class ReverseBar(Bar): + 'A bar which has a marker which bounces from side to side.' + + def __init__(self, marker='#', left='|', right='|', fill=' ', + fill_left=False): + '''Creates a customizable progress bar. + + marker - string or updatable object to use as a marker + left - string or updatable object to use as a left border + right - string or updatable object to use as a right border + fill - character to use for the empty part of the progress bar + fill_left - whether to fill from the left or the right + ''' + self.marker = marker + self.left = left + self.right = right + self.fill = fill + self.fill_left = fill_left + + +class BouncingBar(Bar): + def update(self, pbar, width): + 'Updates the progress bar and its subcomponents' + + left, marker, right = (format_updatable(i, pbar) for i in + (self.left, self.marker, self.right)) + + width -= len(left) + len(right) + + if pbar.finished: return '%s%s%s' % (left, width * marker, right) + + position = int(pbar.currval % (width * 2 - 1)) + if position > width: position = width * 2 - position + lpad = self.fill * (position - 1) + rpad = self.fill * (width - len(marker) - len(lpad)) + + # Swap if we want to bounce the other way + if not self.fill_left: rpad, lpad = lpad, rpad + + return '%s%s%s%s%s' % (left, lpad, marker, rpad, right) diff --git a/RenderPipelineFile/rplibs/pyqt_imports.py b/RenderPipelineFile/rplibs/pyqt_imports.py index 59031491..614bd6a5 100644 --- a/RenderPipelineFile/rplibs/pyqt_imports.py +++ b/RenderPipelineFile/rplibs/pyqt_imports.py @@ -1,62 +1,62 @@ -""" - -Wrapper script to import all qt classes - -""" - -from __future__ import print_function -import sys - -PYQT_VERSION = 5 - -try: - import PyQt5 -except ImportError: - print("Could not import PyQt5, trying to import PyQt4") - try: - import PyQt4 - PYQT_VERSION = 4 - except ImportError: - print("Failed to import either PyQt4 or PyQt5!") - sys.exit(-1) - -if PYQT_VERSION == 4: - from PyQt4.QtGui import * - from PyQt4.QtCore import * - - def qt_connect(obj, signal_name, handler): - QObject.connect(obj, SIGNAL(signal_name), handler) - -elif PYQT_VERSION == 5: - from PyQt5.QtGui import * - from PyQt5.QtWidgets import * - from PyQt5.QtCore import * - - def qt_connect(obj, signal_name, handler): - if "(" in signal_name: - slot_name = signal_name[:signal_name.index("(")] - handler_type = signal_name[signal_name.index("(") + 1:].rstrip(")") - handler_type = str(handler_type.replace("*", "")).strip() - if not handler_type: - getattr(obj, slot_name).connect(handler) - else: - py_type = { - "int": int, - "QString": str, - "double": float, - "QListWidgetItem": QListWidgetItem - }[handler_type] - getattr(obj, slot_name)[py_type].connect(handler) - else: - getattr(obj, signal_name).connect(handler) - - -def qt_register_fonts(): - import os - this_dir = os.path.realpath(os.path.dirname(__file__)) - font_dir = os.path.join(this_dir, "..", "data", "font") - for f in os.listdir(font_dir): - if f.endswith(".ttf"): - fpath = os.path.relpath(os.path.join(font_dir, f)) - QFontDatabase.addApplicationFont(fpath) - +""" + +Wrapper script to import all qt classes + +""" + +from __future__ import print_function +import sys + +PYQT_VERSION = 5 + +try: + import PyQt5 +except ImportError: + print("Could not import PyQt5, trying to import PyQt4") + try: + import PyQt4 + PYQT_VERSION = 4 + except ImportError: + print("Failed to import either PyQt4 or PyQt5!") + sys.exit(-1) + +if PYQT_VERSION == 4: + from PyQt4.QtGui import * + from PyQt4.QtCore import * + + def qt_connect(obj, signal_name, handler): + QObject.connect(obj, SIGNAL(signal_name), handler) + +elif PYQT_VERSION == 5: + from PyQt5.QtGui import * + from PyQt5.QtWidgets import * + from PyQt5.QtCore import * + + def qt_connect(obj, signal_name, handler): + if "(" in signal_name: + slot_name = signal_name[:signal_name.index("(")] + handler_type = signal_name[signal_name.index("(") + 1:].rstrip(")") + handler_type = str(handler_type.replace("*", "")).strip() + if not handler_type: + getattr(obj, slot_name).connect(handler) + else: + py_type = { + "int": int, + "QString": str, + "double": float, + "QListWidgetItem": QListWidgetItem + }[handler_type] + getattr(obj, slot_name)[py_type].connect(handler) + else: + getattr(obj, signal_name).connect(handler) + + +def qt_register_fonts(): + import os + this_dir = os.path.realpath(os.path.dirname(__file__)) + font_dir = os.path.join(this_dir, "..", "data", "font") + for f in os.listdir(font_dir): + if f.endswith(".ttf"): + fpath = os.path.relpath(os.path.join(font_dir, f)) + QFontDatabase.addApplicationFont(fpath) + diff --git a/RenderPipelineFile/rplibs/six-source.txt b/RenderPipelineFile/rplibs/six-source.txt index b5c5c108..41389c1f 100644 --- a/RenderPipelineFile/rplibs/six-source.txt +++ b/RenderPipelineFile/rplibs/six-source.txt @@ -1,4 +1,4 @@ -package: six -version: 1.10 -downloaded: 01-06-2016 -url: https://pypi.python.org/pypi/six +package: six +version: 1.10 +downloaded: 01-06-2016 +url: https://pypi.python.org/pypi/six diff --git a/RenderPipelineFile/rplibs/six.py b/RenderPipelineFile/rplibs/six.py index 190c0239..7bd92250 100644 --- a/RenderPipelineFile/rplibs/six.py +++ b/RenderPipelineFile/rplibs/six.py @@ -1,868 +1,868 @@ -"""Utilities for writing code that runs on Python 2 and 3""" - -# Copyright (c) 2010-2015 Benjamin Peterson -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from __future__ import absolute_import - -import functools -import itertools -import operator -import sys -import types - -__author__ = "Benjamin Peterson " -__version__ = "1.10.0" - - -# Useful for very coarse version differentiation. -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 -PY34 = sys.version_info[0:2] >= (3, 4) - -if PY3: - string_types = str, - integer_types = int, - class_types = type, - text_type = str - binary_type = bytes - - MAXSIZE = sys.maxsize -else: - string_types = basestring, - integer_types = (int, long) - class_types = (type, types.ClassType) - text_type = unicode - binary_type = str - - if sys.platform.startswith("java"): - # Jython always uses 32 bits. - MAXSIZE = int((1 << 31) - 1) - else: - # It's possible to have sizeof(long) != sizeof(Py_ssize_t). - class X(object): - - def __len__(self): - return 1 << 31 - try: - len(X()) - except OverflowError: - # 32-bit - MAXSIZE = int((1 << 31) - 1) - else: - # 64-bit - MAXSIZE = int((1 << 63) - 1) - del X - - -def _add_doc(func, doc): - """Add documentation to a function.""" - func.__doc__ = doc - - -def _import_module(name): - """Import module, returning the module after the last dot.""" - __import__(name) - return sys.modules[name] - - -class _LazyDescr(object): - - def __init__(self, name): - self.name = name - - def __get__(self, obj, tp): - result = self._resolve() - setattr(obj, self.name, result) # Invokes __set__. - try: - # This is a bit ugly, but it avoids running this again by - # removing this descriptor. - delattr(obj.__class__, self.name) - except AttributeError: - pass - return result - - -class MovedModule(_LazyDescr): - - def __init__(self, name, old, new=None): - super(MovedModule, self).__init__(name) - if PY3: - if new is None: - new = name - self.mod = new - else: - self.mod = old - - def _resolve(self): - return _import_module(self.mod) - - def __getattr__(self, attr): - _module = self._resolve() - value = getattr(_module, attr) - setattr(self, attr, value) - return value - - -class _LazyModule(types.ModuleType): - - def __init__(self, name): - super(_LazyModule, self).__init__(name) - self.__doc__ = self.__class__.__doc__ - - def __dir__(self): - attrs = ["__doc__", "__name__"] - attrs += [attr.name for attr in self._moved_attributes] - return attrs - - # Subclasses should override this - _moved_attributes = [] - - -class MovedAttribute(_LazyDescr): - - def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): - super(MovedAttribute, self).__init__(name) - if PY3: - if new_mod is None: - new_mod = name - self.mod = new_mod - if new_attr is None: - if old_attr is None: - new_attr = name - else: - new_attr = old_attr - self.attr = new_attr - else: - self.mod = old_mod - if old_attr is None: - old_attr = name - self.attr = old_attr - - def _resolve(self): - module = _import_module(self.mod) - return getattr(module, self.attr) - - -class _SixMetaPathImporter(object): - - """ - A meta path importer to import six.moves and its submodules. - - This class implements a PEP302 finder and loader. It should be compatible - with Python 2.5 and all existing versions of Python3 - """ - - def __init__(self, six_module_name): - self.name = six_module_name - self.known_modules = {} - - def _add_module(self, mod, *fullnames): - for fullname in fullnames: - self.known_modules[self.name + "." + fullname] = mod - - def _get_module(self, fullname): - return self.known_modules[self.name + "." + fullname] - - def find_module(self, fullname, path=None): - if fullname in self.known_modules: - return self - return None - - def __get_module(self, fullname): - try: - return self.known_modules[fullname] - except KeyError: - raise ImportError("This loader does not know module " + fullname) - - def load_module(self, fullname): - try: - # in case of a reload - return sys.modules[fullname] - except KeyError: - pass - mod = self.__get_module(fullname) - if isinstance(mod, MovedModule): - mod = mod._resolve() - else: - mod.__loader__ = self - sys.modules[fullname] = mod - return mod - - def is_package(self, fullname): - """ - Return true, if the named module is a package. - - We need this method to get correct spec objects with - Python 3.4 (see PEP451) - """ - return hasattr(self.__get_module(fullname), "__path__") - - def get_code(self, fullname): - """Return None - - Required, if is_package is implemented""" - self.__get_module(fullname) # eventually raises ImportError - return None - get_source = get_code # same as get_code - -_importer = _SixMetaPathImporter(__name__) - - -class _MovedItems(_LazyModule): - - """Lazy loading of moved objects""" - __path__ = [] # mark as package - - -_moved_attributes = [ - MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), - MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), - MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), - MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), - MovedAttribute("intern", "__builtin__", "sys"), - MovedAttribute("map", "itertools", "builtins", "imap", "map"), - MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), - MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), - MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), - MovedAttribute("reduce", "__builtin__", "functools"), - MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), - MovedAttribute("StringIO", "StringIO", "io"), - MovedAttribute("UserDict", "UserDict", "collections"), - MovedAttribute("UserList", "UserList", "collections"), - MovedAttribute("UserString", "UserString", "collections"), - MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), - MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), - MovedModule("builtins", "__builtin__"), - MovedModule("configparser", "ConfigParser"), - MovedModule("copyreg", "copy_reg"), - MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), - MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"), - MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), - MovedModule("http_cookies", "Cookie", "http.cookies"), - MovedModule("html_entities", "htmlentitydefs", "html.entities"), - MovedModule("html_parser", "HTMLParser", "html.parser"), - MovedModule("http_client", "httplib", "http.client"), - MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), - MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), - MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), - MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), - MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), - MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), - MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), - MovedModule("cPickle", "cPickle", "pickle"), - MovedModule("queue", "Queue"), - MovedModule("reprlib", "repr"), - MovedModule("socketserver", "SocketServer"), - MovedModule("_thread", "thread", "_thread"), - MovedModule("tkinter", "Tkinter"), - MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), - MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), - MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), - MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), - MovedModule("tkinter_tix", "Tix", "tkinter.tix"), - MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), - MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), - MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), - MovedModule("tkinter_colorchooser", "tkColorChooser", - "tkinter.colorchooser"), - MovedModule("tkinter_commondialog", "tkCommonDialog", - "tkinter.commondialog"), - MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), - MovedModule("tkinter_font", "tkFont", "tkinter.font"), - MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), - MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", - "tkinter.simpledialog"), - MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), - MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), - MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), - MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), - MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), - MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), -] -# Add windows specific modules. -if sys.platform == "win32": - _moved_attributes += [ - MovedModule("winreg", "_winreg"), - ] - -for attr in _moved_attributes: - setattr(_MovedItems, attr.name, attr) - if isinstance(attr, MovedModule): - _importer._add_module(attr, "moves." + attr.name) -del attr - -_MovedItems._moved_attributes = _moved_attributes - -moves = _MovedItems(__name__ + ".moves") -_importer._add_module(moves, "moves") - - -class Module_six_moves_urllib_parse(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_parse""" - - -_urllib_parse_moved_attributes = [ - MovedAttribute("ParseResult", "urlparse", "urllib.parse"), - MovedAttribute("SplitResult", "urlparse", "urllib.parse"), - MovedAttribute("parse_qs", "urlparse", "urllib.parse"), - MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), - MovedAttribute("urldefrag", "urlparse", "urllib.parse"), - MovedAttribute("urljoin", "urlparse", "urllib.parse"), - MovedAttribute("urlparse", "urlparse", "urllib.parse"), - MovedAttribute("urlsplit", "urlparse", "urllib.parse"), - MovedAttribute("urlunparse", "urlparse", "urllib.parse"), - MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), - MovedAttribute("quote", "urllib", "urllib.parse"), - MovedAttribute("quote_plus", "urllib", "urllib.parse"), - MovedAttribute("unquote", "urllib", "urllib.parse"), - MovedAttribute("unquote_plus", "urllib", "urllib.parse"), - MovedAttribute("urlencode", "urllib", "urllib.parse"), - MovedAttribute("splitquery", "urllib", "urllib.parse"), - MovedAttribute("splittag", "urllib", "urllib.parse"), - MovedAttribute("splituser", "urllib", "urllib.parse"), - MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), - MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), - MovedAttribute("uses_params", "urlparse", "urllib.parse"), - MovedAttribute("uses_query", "urlparse", "urllib.parse"), - MovedAttribute("uses_relative", "urlparse", "urllib.parse"), -] -for attr in _urllib_parse_moved_attributes: - setattr(Module_six_moves_urllib_parse, attr.name, attr) -del attr - -Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes - -_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), - "moves.urllib_parse", "moves.urllib.parse") - - -class Module_six_moves_urllib_error(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_error""" - - -_urllib_error_moved_attributes = [ - MovedAttribute("URLError", "urllib2", "urllib.error"), - MovedAttribute("HTTPError", "urllib2", "urllib.error"), - MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), -] -for attr in _urllib_error_moved_attributes: - setattr(Module_six_moves_urllib_error, attr.name, attr) -del attr - -Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes - -_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), - "moves.urllib_error", "moves.urllib.error") - - -class Module_six_moves_urllib_request(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_request""" - - -_urllib_request_moved_attributes = [ - MovedAttribute("urlopen", "urllib2", "urllib.request"), - MovedAttribute("install_opener", "urllib2", "urllib.request"), - MovedAttribute("build_opener", "urllib2", "urllib.request"), - MovedAttribute("pathname2url", "urllib", "urllib.request"), - MovedAttribute("url2pathname", "urllib", "urllib.request"), - MovedAttribute("getproxies", "urllib", "urllib.request"), - MovedAttribute("Request", "urllib2", "urllib.request"), - MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), - MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), - MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), - MovedAttribute("BaseHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), - MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), - MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), - MovedAttribute("FileHandler", "urllib2", "urllib.request"), - MovedAttribute("FTPHandler", "urllib2", "urllib.request"), - MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), - MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), - MovedAttribute("urlretrieve", "urllib", "urllib.request"), - MovedAttribute("urlcleanup", "urllib", "urllib.request"), - MovedAttribute("URLopener", "urllib", "urllib.request"), - MovedAttribute("FancyURLopener", "urllib", "urllib.request"), - MovedAttribute("proxy_bypass", "urllib", "urllib.request"), -] -for attr in _urllib_request_moved_attributes: - setattr(Module_six_moves_urllib_request, attr.name, attr) -del attr - -Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes - -_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), - "moves.urllib_request", "moves.urllib.request") - - -class Module_six_moves_urllib_response(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_response""" - - -_urllib_response_moved_attributes = [ - MovedAttribute("addbase", "urllib", "urllib.response"), - MovedAttribute("addclosehook", "urllib", "urllib.response"), - MovedAttribute("addinfo", "urllib", "urllib.response"), - MovedAttribute("addinfourl", "urllib", "urllib.response"), -] -for attr in _urllib_response_moved_attributes: - setattr(Module_six_moves_urllib_response, attr.name, attr) -del attr - -Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes - -_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), - "moves.urllib_response", "moves.urllib.response") - - -class Module_six_moves_urllib_robotparser(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_robotparser""" - - -_urllib_robotparser_moved_attributes = [ - MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), -] -for attr in _urllib_robotparser_moved_attributes: - setattr(Module_six_moves_urllib_robotparser, attr.name, attr) -del attr - -Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes - -_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), - "moves.urllib_robotparser", "moves.urllib.robotparser") - - -class Module_six_moves_urllib(types.ModuleType): - - """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" - __path__ = [] # mark as package - parse = _importer._get_module("moves.urllib_parse") - error = _importer._get_module("moves.urllib_error") - request = _importer._get_module("moves.urllib_request") - response = _importer._get_module("moves.urllib_response") - robotparser = _importer._get_module("moves.urllib_robotparser") - - def __dir__(self): - return ['parse', 'error', 'request', 'response', 'robotparser'] - -_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), - "moves.urllib") - - -def add_move(move): - """Add an item to six.moves.""" - setattr(_MovedItems, move.name, move) - - -def remove_move(name): - """Remove item from six.moves.""" - try: - delattr(_MovedItems, name) - except AttributeError: - try: - del moves.__dict__[name] - except KeyError: - raise AttributeError("no such move, %r" % (name,)) - - -if PY3: - _meth_func = "__func__" - _meth_self = "__self__" - - _func_closure = "__closure__" - _func_code = "__code__" - _func_defaults = "__defaults__" - _func_globals = "__globals__" -else: - _meth_func = "im_func" - _meth_self = "im_self" - - _func_closure = "func_closure" - _func_code = "func_code" - _func_defaults = "func_defaults" - _func_globals = "func_globals" - - -try: - advance_iterator = next -except NameError: - def advance_iterator(it): - return it.next() -next = advance_iterator - - -try: - callable = callable -except NameError: - def callable(obj): - return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) - - -if PY3: - def get_unbound_function(unbound): - return unbound - - create_bound_method = types.MethodType - - def create_unbound_method(func, cls): - return func - - Iterator = object -else: - def get_unbound_function(unbound): - return unbound.im_func - - def create_bound_method(func, obj): - return types.MethodType(func, obj, obj.__class__) - - def create_unbound_method(func, cls): - return types.MethodType(func, None, cls) - - class Iterator(object): - - def next(self): - return type(self).__next__(self) - - callable = callable -_add_doc(get_unbound_function, - """Get the function out of a possibly unbound function""") - - -get_method_function = operator.attrgetter(_meth_func) -get_method_self = operator.attrgetter(_meth_self) -get_function_closure = operator.attrgetter(_func_closure) -get_function_code = operator.attrgetter(_func_code) -get_function_defaults = operator.attrgetter(_func_defaults) -get_function_globals = operator.attrgetter(_func_globals) - - -if PY3: - def iterkeys(d, **kw): - return iter(d.keys(**kw)) - - def itervalues(d, **kw): - return iter(d.values(**kw)) - - def iteritems(d, **kw): - return iter(d.items(**kw)) - - def iterlists(d, **kw): - return iter(d.lists(**kw)) - - viewkeys = operator.methodcaller("keys") - - viewvalues = operator.methodcaller("values") - - viewitems = operator.methodcaller("items") -else: - def iterkeys(d, **kw): - return d.iterkeys(**kw) - - def itervalues(d, **kw): - return d.itervalues(**kw) - - def iteritems(d, **kw): - return d.iteritems(**kw) - - def iterlists(d, **kw): - return d.iterlists(**kw) - - viewkeys = operator.methodcaller("viewkeys") - - viewvalues = operator.methodcaller("viewvalues") - - viewitems = operator.methodcaller("viewitems") - -_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") -_add_doc(itervalues, "Return an iterator over the values of a dictionary.") -_add_doc(iteritems, - "Return an iterator over the (key, value) pairs of a dictionary.") -_add_doc(iterlists, - "Return an iterator over the (key, [values]) pairs of a dictionary.") - - -if PY3: - def b(s): - return s.encode("latin-1") - - def u(s): - return s - unichr = chr - import struct - int2byte = struct.Struct(">B").pack - del struct - byte2int = operator.itemgetter(0) - indexbytes = operator.getitem - iterbytes = iter - import io - StringIO = io.StringIO - BytesIO = io.BytesIO - _assertCountEqual = "assertCountEqual" - if sys.version_info[1] <= 1: - _assertRaisesRegex = "assertRaisesRegexp" - _assertRegex = "assertRegexpMatches" - else: - _assertRaisesRegex = "assertRaisesRegex" - _assertRegex = "assertRegex" -else: - def b(s): - return s - # Workaround for standalone backslash - - def u(s): - return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") - unichr = unichr - int2byte = chr - - def byte2int(bs): - return ord(bs[0]) - - def indexbytes(buf, i): - return ord(buf[i]) - iterbytes = functools.partial(itertools.imap, ord) - import StringIO - StringIO = BytesIO = StringIO.StringIO - _assertCountEqual = "assertItemsEqual" - _assertRaisesRegex = "assertRaisesRegexp" - _assertRegex = "assertRegexpMatches" -_add_doc(b, """Byte literal""") -_add_doc(u, """Text literal""") - - -def assertCountEqual(self, *args, **kwargs): - return getattr(self, _assertCountEqual)(*args, **kwargs) - - -def assertRaisesRegex(self, *args, **kwargs): - return getattr(self, _assertRaisesRegex)(*args, **kwargs) - - -def assertRegex(self, *args, **kwargs): - return getattr(self, _assertRegex)(*args, **kwargs) - - -if PY3: - exec_ = getattr(moves.builtins, "exec") - - def reraise(tp, value, tb=None): - if value is None: - value = tp() - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - -else: - def exec_(_code_, _globs_=None, _locs_=None): - """Execute code in a namespace.""" - if _globs_ is None: - frame = sys._getframe(1) - _globs_ = frame.f_globals - if _locs_ is None: - _locs_ = frame.f_locals - del frame - elif _locs_ is None: - _locs_ = _globs_ - exec("""exec _code_ in _globs_, _locs_""") - - exec_("""def reraise(tp, value, tb=None): - raise tp, value, tb -""") - - -if sys.version_info[:2] == (3, 2): - exec_("""def raise_from(value, from_value): - if from_value is None: - raise value - raise value from from_value -""") -elif sys.version_info[:2] > (3, 2): - exec_("""def raise_from(value, from_value): - raise value from from_value -""") -else: - def raise_from(value, from_value): - raise value - - -print_ = getattr(moves.builtins, "print", None) -if print_ is None: - def print_(*args, **kwargs): - """The new-style print function for Python 2.4 and 2.5.""" - fp = kwargs.pop("file", sys.stdout) - if fp is None: - return - - def write(data): - if not isinstance(data, basestring): - data = str(data) - # If the file has an encoding, encode unicode with it. - if (isinstance(fp, file) and - isinstance(data, unicode) and - fp.encoding is not None): - errors = getattr(fp, "errors", None) - if errors is None: - errors = "strict" - data = data.encode(fp.encoding, errors) - fp.write(data) - want_unicode = False - sep = kwargs.pop("sep", None) - if sep is not None: - if isinstance(sep, unicode): - want_unicode = True - elif not isinstance(sep, str): - raise TypeError("sep must be None or a string") - end = kwargs.pop("end", None) - if end is not None: - if isinstance(end, unicode): - want_unicode = True - elif not isinstance(end, str): - raise TypeError("end must be None or a string") - if kwargs: - raise TypeError("invalid keyword arguments to print()") - if not want_unicode: - for arg in args: - if isinstance(arg, unicode): - want_unicode = True - break - if want_unicode: - newline = unicode("\n") - space = unicode(" ") - else: - newline = "\n" - space = " " - if sep is None: - sep = space - if end is None: - end = newline - for i, arg in enumerate(args): - if i: - write(sep) - write(arg) - write(end) -if sys.version_info[:2] < (3, 3): - _print = print_ - - def print_(*args, **kwargs): - fp = kwargs.get("file", sys.stdout) - flush = kwargs.pop("flush", False) - _print(*args, **kwargs) - if flush and fp is not None: - fp.flush() - -_add_doc(reraise, """Reraise an exception.""") - -if sys.version_info[0:2] < (3, 4): - def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, - updated=functools.WRAPPER_UPDATES): - def wrapper(f): - f = functools.wraps(wrapped, assigned, updated)(f) - f.__wrapped__ = wrapped - return f - return wrapper -else: - wraps = functools.wraps - - -def with_metaclass(meta, *bases): - """Create a base class with a metaclass.""" - # This requires a bit of explanation: the basic idea is to make a dummy - # metaclass for one level of class instantiation that replaces itself with - # the actual metaclass. - class metaclass(meta): - - def __new__(cls, name, this_bases, d): - return meta(name, bases, d) - return type.__new__(metaclass, 'temporary_class', (), {}) - - -def add_metaclass(metaclass): - """Class decorator for creating a class with a metaclass.""" - def wrapper(cls): - orig_vars = cls.__dict__.copy() - slots = orig_vars.get('__slots__') - if slots is not None: - if isinstance(slots, str): - slots = [slots] - for slots_var in slots: - orig_vars.pop(slots_var) - orig_vars.pop('__dict__', None) - orig_vars.pop('__weakref__', None) - return metaclass(cls.__name__, cls.__bases__, orig_vars) - return wrapper - - -def python_2_unicode_compatible(klass): - """ - A decorator that defines __unicode__ and __str__ methods under Python 2. - Under Python 3 it does nothing. - - To support Python 2 and 3 with a single code base, define a __str__ method - returning text and apply this decorator to the class. - """ - if PY2: - if '__str__' not in klass.__dict__: - raise ValueError("@python_2_unicode_compatible cannot be applied " - "to %s because it doesn't define __str__()." % - klass.__name__) - klass.__unicode__ = klass.__str__ - klass.__str__ = lambda self: self.__unicode__().encode('utf-8') - return klass - - -# Complete the moves implementation. -# This code is at the end of this module to speed up module loading. -# Turn this module into a package. -__path__ = [] # required for PEP 302 and PEP 451 -__package__ = __name__ # see PEP 366 @ReservedAssignment -if globals().get("__spec__") is not None: - __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable -# Remove other six meta path importers, since they cause problems. This can -# happen if six is removed from sys.modules and then reloaded. (Setuptools does -# this for some reason.) -if sys.meta_path: - for i, importer in enumerate(sys.meta_path): - # Here's some real nastiness: Another "instance" of the six module might - # be floating around. Therefore, we can't use isinstance() to check for - # the six meta path importer, since the other six instance will have - # inserted an importer with different class. - if (type(importer).__name__ == "_SixMetaPathImporter" and - importer.name == __name__): - del sys.meta_path[i] - break - del i, importer -# Finally, add the importer to the meta path import hook. -sys.meta_path.append(_importer) +"""Utilities for writing code that runs on Python 2 and 3""" + +# Copyright (c) 2010-2015 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from __future__ import absolute_import + +import functools +import itertools +import operator +import sys +import types + +__author__ = "Benjamin Peterson " +__version__ = "1.10.0" + + +# Useful for very coarse version differentiation. +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 +PY34 = sys.version_info[0:2] >= (3, 4) + +if PY3: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + + MAXSIZE = sys.maxsize +else: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + + if sys.platform.startswith("java"): + # Jython always uses 32 bits. + MAXSIZE = int((1 << 31) - 1) + else: + # It's possible to have sizeof(long) != sizeof(Py_ssize_t). + class X(object): + + def __len__(self): + return 1 << 31 + try: + len(X()) + except OverflowError: + # 32-bit + MAXSIZE = int((1 << 31) - 1) + else: + # 64-bit + MAXSIZE = int((1 << 63) - 1) + del X + + +def _add_doc(func, doc): + """Add documentation to a function.""" + func.__doc__ = doc + + +def _import_module(name): + """Import module, returning the module after the last dot.""" + __import__(name) + return sys.modules[name] + + +class _LazyDescr(object): + + def __init__(self, name): + self.name = name + + def __get__(self, obj, tp): + result = self._resolve() + setattr(obj, self.name, result) # Invokes __set__. + try: + # This is a bit ugly, but it avoids running this again by + # removing this descriptor. + delattr(obj.__class__, self.name) + except AttributeError: + pass + return result + + +class MovedModule(_LazyDescr): + + def __init__(self, name, old, new=None): + super(MovedModule, self).__init__(name) + if PY3: + if new is None: + new = name + self.mod = new + else: + self.mod = old + + def _resolve(self): + return _import_module(self.mod) + + def __getattr__(self, attr): + _module = self._resolve() + value = getattr(_module, attr) + setattr(self, attr, value) + return value + + +class _LazyModule(types.ModuleType): + + def __init__(self, name): + super(_LazyModule, self).__init__(name) + self.__doc__ = self.__class__.__doc__ + + def __dir__(self): + attrs = ["__doc__", "__name__"] + attrs += [attr.name for attr in self._moved_attributes] + return attrs + + # Subclasses should override this + _moved_attributes = [] + + +class MovedAttribute(_LazyDescr): + + def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): + super(MovedAttribute, self).__init__(name) + if PY3: + if new_mod is None: + new_mod = name + self.mod = new_mod + if new_attr is None: + if old_attr is None: + new_attr = name + else: + new_attr = old_attr + self.attr = new_attr + else: + self.mod = old_mod + if old_attr is None: + old_attr = name + self.attr = old_attr + + def _resolve(self): + module = _import_module(self.mod) + return getattr(module, self.attr) + + +class _SixMetaPathImporter(object): + + """ + A meta path importer to import six.moves and its submodules. + + This class implements a PEP302 finder and loader. It should be compatible + with Python 2.5 and all existing versions of Python3 + """ + + def __init__(self, six_module_name): + self.name = six_module_name + self.known_modules = {} + + def _add_module(self, mod, *fullnames): + for fullname in fullnames: + self.known_modules[self.name + "." + fullname] = mod + + def _get_module(self, fullname): + return self.known_modules[self.name + "." + fullname] + + def find_module(self, fullname, path=None): + if fullname in self.known_modules: + return self + return None + + def __get_module(self, fullname): + try: + return self.known_modules[fullname] + except KeyError: + raise ImportError("This loader does not know module " + fullname) + + def load_module(self, fullname): + try: + # in case of a reload + return sys.modules[fullname] + except KeyError: + pass + mod = self.__get_module(fullname) + if isinstance(mod, MovedModule): + mod = mod._resolve() + else: + mod.__loader__ = self + sys.modules[fullname] = mod + return mod + + def is_package(self, fullname): + """ + Return true, if the named module is a package. + + We need this method to get correct spec objects with + Python 3.4 (see PEP451) + """ + return hasattr(self.__get_module(fullname), "__path__") + + def get_code(self, fullname): + """Return None + + Required, if is_package is implemented""" + self.__get_module(fullname) # eventually raises ImportError + return None + get_source = get_code # same as get_code + +_importer = _SixMetaPathImporter(__name__) + + +class _MovedItems(_LazyModule): + + """Lazy loading of moved objects""" + __path__ = [] # mark as package + + +_moved_attributes = [ + MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), + MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), + MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), + MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), + MovedAttribute("intern", "__builtin__", "sys"), + MovedAttribute("map", "itertools", "builtins", "imap", "map"), + MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), + MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), + MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), + MovedAttribute("reduce", "__builtin__", "functools"), + MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), + MovedAttribute("StringIO", "StringIO", "io"), + MovedAttribute("UserDict", "UserDict", "collections"), + MovedAttribute("UserList", "UserList", "collections"), + MovedAttribute("UserString", "UserString", "collections"), + MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), + MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), + MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), + MovedModule("builtins", "__builtin__"), + MovedModule("configparser", "ConfigParser"), + MovedModule("copyreg", "copy_reg"), + MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), + MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"), + MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), + MovedModule("http_cookies", "Cookie", "http.cookies"), + MovedModule("html_entities", "htmlentitydefs", "html.entities"), + MovedModule("html_parser", "HTMLParser", "html.parser"), + MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), + MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), + MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), + MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), + MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), + MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), + MovedModule("cPickle", "cPickle", "pickle"), + MovedModule("queue", "Queue"), + MovedModule("reprlib", "repr"), + MovedModule("socketserver", "SocketServer"), + MovedModule("_thread", "thread", "_thread"), + MovedModule("tkinter", "Tkinter"), + MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), + MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), + MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), + MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), + MovedModule("tkinter_tix", "Tix", "tkinter.tix"), + MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), + MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), + MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), + MovedModule("tkinter_colorchooser", "tkColorChooser", + "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", + "tkinter.commondialog"), + MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), + MovedModule("tkinter_font", "tkFont", "tkinter.font"), + MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", + "tkinter.simpledialog"), + MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), + MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), + MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), + MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), + MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), + MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), +] +# Add windows specific modules. +if sys.platform == "win32": + _moved_attributes += [ + MovedModule("winreg", "_winreg"), + ] + +for attr in _moved_attributes: + setattr(_MovedItems, attr.name, attr) + if isinstance(attr, MovedModule): + _importer._add_module(attr, "moves." + attr.name) +del attr + +_MovedItems._moved_attributes = _moved_attributes + +moves = _MovedItems(__name__ + ".moves") +_importer._add_module(moves, "moves") + + +class Module_six_moves_urllib_parse(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_parse""" + + +_urllib_parse_moved_attributes = [ + MovedAttribute("ParseResult", "urlparse", "urllib.parse"), + MovedAttribute("SplitResult", "urlparse", "urllib.parse"), + MovedAttribute("parse_qs", "urlparse", "urllib.parse"), + MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), + MovedAttribute("urldefrag", "urlparse", "urllib.parse"), + MovedAttribute("urljoin", "urlparse", "urllib.parse"), + MovedAttribute("urlparse", "urlparse", "urllib.parse"), + MovedAttribute("urlsplit", "urlparse", "urllib.parse"), + MovedAttribute("urlunparse", "urlparse", "urllib.parse"), + MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), + MovedAttribute("quote", "urllib", "urllib.parse"), + MovedAttribute("quote_plus", "urllib", "urllib.parse"), + MovedAttribute("unquote", "urllib", "urllib.parse"), + MovedAttribute("unquote_plus", "urllib", "urllib.parse"), + MovedAttribute("urlencode", "urllib", "urllib.parse"), + MovedAttribute("splitquery", "urllib", "urllib.parse"), + MovedAttribute("splittag", "urllib", "urllib.parse"), + MovedAttribute("splituser", "urllib", "urllib.parse"), + MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), + MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), + MovedAttribute("uses_params", "urlparse", "urllib.parse"), + MovedAttribute("uses_query", "urlparse", "urllib.parse"), + MovedAttribute("uses_relative", "urlparse", "urllib.parse"), +] +for attr in _urllib_parse_moved_attributes: + setattr(Module_six_moves_urllib_parse, attr.name, attr) +del attr + +Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes + +_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), + "moves.urllib_parse", "moves.urllib.parse") + + +class Module_six_moves_urllib_error(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_error""" + + +_urllib_error_moved_attributes = [ + MovedAttribute("URLError", "urllib2", "urllib.error"), + MovedAttribute("HTTPError", "urllib2", "urllib.error"), + MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), +] +for attr in _urllib_error_moved_attributes: + setattr(Module_six_moves_urllib_error, attr.name, attr) +del attr + +Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes + +_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), + "moves.urllib_error", "moves.urllib.error") + + +class Module_six_moves_urllib_request(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_request""" + + +_urllib_request_moved_attributes = [ + MovedAttribute("urlopen", "urllib2", "urllib.request"), + MovedAttribute("install_opener", "urllib2", "urllib.request"), + MovedAttribute("build_opener", "urllib2", "urllib.request"), + MovedAttribute("pathname2url", "urllib", "urllib.request"), + MovedAttribute("url2pathname", "urllib", "urllib.request"), + MovedAttribute("getproxies", "urllib", "urllib.request"), + MovedAttribute("Request", "urllib2", "urllib.request"), + MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), + MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), + MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), + MovedAttribute("BaseHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), + MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), + MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), + MovedAttribute("FileHandler", "urllib2", "urllib.request"), + MovedAttribute("FTPHandler", "urllib2", "urllib.request"), + MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), + MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), + MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), + MovedAttribute("urlretrieve", "urllib", "urllib.request"), + MovedAttribute("urlcleanup", "urllib", "urllib.request"), + MovedAttribute("URLopener", "urllib", "urllib.request"), + MovedAttribute("FancyURLopener", "urllib", "urllib.request"), + MovedAttribute("proxy_bypass", "urllib", "urllib.request"), +] +for attr in _urllib_request_moved_attributes: + setattr(Module_six_moves_urllib_request, attr.name, attr) +del attr + +Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes + +_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), + "moves.urllib_request", "moves.urllib.request") + + +class Module_six_moves_urllib_response(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_response""" + + +_urllib_response_moved_attributes = [ + MovedAttribute("addbase", "urllib", "urllib.response"), + MovedAttribute("addclosehook", "urllib", "urllib.response"), + MovedAttribute("addinfo", "urllib", "urllib.response"), + MovedAttribute("addinfourl", "urllib", "urllib.response"), +] +for attr in _urllib_response_moved_attributes: + setattr(Module_six_moves_urllib_response, attr.name, attr) +del attr + +Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes + +_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), + "moves.urllib_response", "moves.urllib.response") + + +class Module_six_moves_urllib_robotparser(_LazyModule): + + """Lazy loading of moved objects in six.moves.urllib_robotparser""" + + +_urllib_robotparser_moved_attributes = [ + MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), +] +for attr in _urllib_robotparser_moved_attributes: + setattr(Module_six_moves_urllib_robotparser, attr.name, attr) +del attr + +Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes + +_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), + "moves.urllib_robotparser", "moves.urllib.robotparser") + + +class Module_six_moves_urllib(types.ModuleType): + + """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" + __path__ = [] # mark as package + parse = _importer._get_module("moves.urllib_parse") + error = _importer._get_module("moves.urllib_error") + request = _importer._get_module("moves.urllib_request") + response = _importer._get_module("moves.urllib_response") + robotparser = _importer._get_module("moves.urllib_robotparser") + + def __dir__(self): + return ['parse', 'error', 'request', 'response', 'robotparser'] + +_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), + "moves.urllib") + + +def add_move(move): + """Add an item to six.moves.""" + setattr(_MovedItems, move.name, move) + + +def remove_move(name): + """Remove item from six.moves.""" + try: + delattr(_MovedItems, name) + except AttributeError: + try: + del moves.__dict__[name] + except KeyError: + raise AttributeError("no such move, %r" % (name,)) + + +if PY3: + _meth_func = "__func__" + _meth_self = "__self__" + + _func_closure = "__closure__" + _func_code = "__code__" + _func_defaults = "__defaults__" + _func_globals = "__globals__" +else: + _meth_func = "im_func" + _meth_self = "im_self" + + _func_closure = "func_closure" + _func_code = "func_code" + _func_defaults = "func_defaults" + _func_globals = "func_globals" + + +try: + advance_iterator = next +except NameError: + def advance_iterator(it): + return it.next() +next = advance_iterator + + +try: + callable = callable +except NameError: + def callable(obj): + return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + + +if PY3: + def get_unbound_function(unbound): + return unbound + + create_bound_method = types.MethodType + + def create_unbound_method(func, cls): + return func + + Iterator = object +else: + def get_unbound_function(unbound): + return unbound.im_func + + def create_bound_method(func, obj): + return types.MethodType(func, obj, obj.__class__) + + def create_unbound_method(func, cls): + return types.MethodType(func, None, cls) + + class Iterator(object): + + def next(self): + return type(self).__next__(self) + + callable = callable +_add_doc(get_unbound_function, + """Get the function out of a possibly unbound function""") + + +get_method_function = operator.attrgetter(_meth_func) +get_method_self = operator.attrgetter(_meth_self) +get_function_closure = operator.attrgetter(_func_closure) +get_function_code = operator.attrgetter(_func_code) +get_function_defaults = operator.attrgetter(_func_defaults) +get_function_globals = operator.attrgetter(_func_globals) + + +if PY3: + def iterkeys(d, **kw): + return iter(d.keys(**kw)) + + def itervalues(d, **kw): + return iter(d.values(**kw)) + + def iteritems(d, **kw): + return iter(d.items(**kw)) + + def iterlists(d, **kw): + return iter(d.lists(**kw)) + + viewkeys = operator.methodcaller("keys") + + viewvalues = operator.methodcaller("values") + + viewitems = operator.methodcaller("items") +else: + def iterkeys(d, **kw): + return d.iterkeys(**kw) + + def itervalues(d, **kw): + return d.itervalues(**kw) + + def iteritems(d, **kw): + return d.iteritems(**kw) + + def iterlists(d, **kw): + return d.iterlists(**kw) + + viewkeys = operator.methodcaller("viewkeys") + + viewvalues = operator.methodcaller("viewvalues") + + viewitems = operator.methodcaller("viewitems") + +_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") +_add_doc(itervalues, "Return an iterator over the values of a dictionary.") +_add_doc(iteritems, + "Return an iterator over the (key, value) pairs of a dictionary.") +_add_doc(iterlists, + "Return an iterator over the (key, [values]) pairs of a dictionary.") + + +if PY3: + def b(s): + return s.encode("latin-1") + + def u(s): + return s + unichr = chr + import struct + int2byte = struct.Struct(">B").pack + del struct + byte2int = operator.itemgetter(0) + indexbytes = operator.getitem + iterbytes = iter + import io + StringIO = io.StringIO + BytesIO = io.BytesIO + _assertCountEqual = "assertCountEqual" + if sys.version_info[1] <= 1: + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" + else: + _assertRaisesRegex = "assertRaisesRegex" + _assertRegex = "assertRegex" +else: + def b(s): + return s + # Workaround for standalone backslash + + def u(s): + return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") + unichr = unichr + int2byte = chr + + def byte2int(bs): + return ord(bs[0]) + + def indexbytes(buf, i): + return ord(buf[i]) + iterbytes = functools.partial(itertools.imap, ord) + import StringIO + StringIO = BytesIO = StringIO.StringIO + _assertCountEqual = "assertItemsEqual" + _assertRaisesRegex = "assertRaisesRegexp" + _assertRegex = "assertRegexpMatches" +_add_doc(b, """Byte literal""") +_add_doc(u, """Text literal""") + + +def assertCountEqual(self, *args, **kwargs): + return getattr(self, _assertCountEqual)(*args, **kwargs) + + +def assertRaisesRegex(self, *args, **kwargs): + return getattr(self, _assertRaisesRegex)(*args, **kwargs) + + +def assertRegex(self, *args, **kwargs): + return getattr(self, _assertRegex)(*args, **kwargs) + + +if PY3: + exec_ = getattr(moves.builtins, "exec") + + def reraise(tp, value, tb=None): + if value is None: + value = tp() + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + +else: + def exec_(_code_, _globs_=None, _locs_=None): + """Execute code in a namespace.""" + if _globs_ is None: + frame = sys._getframe(1) + _globs_ = frame.f_globals + if _locs_ is None: + _locs_ = frame.f_locals + del frame + elif _locs_ is None: + _locs_ = _globs_ + exec("""exec _code_ in _globs_, _locs_""") + + exec_("""def reraise(tp, value, tb=None): + raise tp, value, tb +""") + + +if sys.version_info[:2] == (3, 2): + exec_("""def raise_from(value, from_value): + if from_value is None: + raise value + raise value from from_value +""") +elif sys.version_info[:2] > (3, 2): + exec_("""def raise_from(value, from_value): + raise value from from_value +""") +else: + def raise_from(value, from_value): + raise value + + +print_ = getattr(moves.builtins, "print", None) +if print_ is None: + def print_(*args, **kwargs): + """The new-style print function for Python 2.4 and 2.5.""" + fp = kwargs.pop("file", sys.stdout) + if fp is None: + return + + def write(data): + if not isinstance(data, basestring): + data = str(data) + # If the file has an encoding, encode unicode with it. + if (isinstance(fp, file) and + isinstance(data, unicode) and + fp.encoding is not None): + errors = getattr(fp, "errors", None) + if errors is None: + errors = "strict" + data = data.encode(fp.encoding, errors) + fp.write(data) + want_unicode = False + sep = kwargs.pop("sep", None) + if sep is not None: + if isinstance(sep, unicode): + want_unicode = True + elif not isinstance(sep, str): + raise TypeError("sep must be None or a string") + end = kwargs.pop("end", None) + if end is not None: + if isinstance(end, unicode): + want_unicode = True + elif not isinstance(end, str): + raise TypeError("end must be None or a string") + if kwargs: + raise TypeError("invalid keyword arguments to print()") + if not want_unicode: + for arg in args: + if isinstance(arg, unicode): + want_unicode = True + break + if want_unicode: + newline = unicode("\n") + space = unicode(" ") + else: + newline = "\n" + space = " " + if sep is None: + sep = space + if end is None: + end = newline + for i, arg in enumerate(args): + if i: + write(sep) + write(arg) + write(end) +if sys.version_info[:2] < (3, 3): + _print = print_ + + def print_(*args, **kwargs): + fp = kwargs.get("file", sys.stdout) + flush = kwargs.pop("flush", False) + _print(*args, **kwargs) + if flush and fp is not None: + fp.flush() + +_add_doc(reraise, """Reraise an exception.""") + +if sys.version_info[0:2] < (3, 4): + def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES): + def wrapper(f): + f = functools.wraps(wrapped, assigned, updated)(f) + f.__wrapped__ = wrapped + return f + return wrapper +else: + wraps = functools.wraps + + +def with_metaclass(meta, *bases): + """Create a base class with a metaclass.""" + # This requires a bit of explanation: the basic idea is to make a dummy + # metaclass for one level of class instantiation that replaces itself with + # the actual metaclass. + class metaclass(meta): + + def __new__(cls, name, this_bases, d): + return meta(name, bases, d) + return type.__new__(metaclass, 'temporary_class', (), {}) + + +def add_metaclass(metaclass): + """Class decorator for creating a class with a metaclass.""" + def wrapper(cls): + orig_vars = cls.__dict__.copy() + slots = orig_vars.get('__slots__') + if slots is not None: + if isinstance(slots, str): + slots = [slots] + for slots_var in slots: + orig_vars.pop(slots_var) + orig_vars.pop('__dict__', None) + orig_vars.pop('__weakref__', None) + return metaclass(cls.__name__, cls.__bases__, orig_vars) + return wrapper + + +def python_2_unicode_compatible(klass): + """ + A decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if PY2: + if '__str__' not in klass.__dict__: + raise ValueError("@python_2_unicode_compatible cannot be applied " + "to %s because it doesn't define __str__()." % + klass.__name__) + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + return klass + + +# Complete the moves implementation. +# This code is at the end of this module to speed up module loading. +# Turn this module into a package. +__path__ = [] # required for PEP 302 and PEP 451 +__package__ = __name__ # see PEP 366 @ReservedAssignment +if globals().get("__spec__") is not None: + __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable +# Remove other six meta path importers, since they cause problems. This can +# happen if six is removed from sys.modules and then reloaded. (Setuptools does +# this for some reason.) +if sys.meta_path: + for i, importer in enumerate(sys.meta_path): + # Here's some real nastiness: Another "instance" of the six module might + # be floating around. Therefore, we can't use isinstance() to check for + # the six meta path importer, since the other six instance will have + # inserted an importer with different class. + if (type(importer).__name__ == "_SixMetaPathImporter" and + importer.name == __name__): + del sys.meta_path[i] + break + del i, importer +# Finally, add the importer to the meta path import hook. +sys.meta_path.append(_importer) diff --git a/RenderPipelineFile/rplibs/yaml/LICENSE b/RenderPipelineFile/rplibs/yaml/LICENSE index 050ced23..79613efa 100644 --- a/RenderPipelineFile/rplibs/yaml/LICENSE +++ b/RenderPipelineFile/rplibs/yaml/LICENSE @@ -1,19 +1,19 @@ -Copyright (c) 2006 Kirill Simonov - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Copyright (c) 2006 Kirill Simonov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/RenderPipelineFile/rplibs/yaml/SOURCE.txt b/RenderPipelineFile/rplibs/yaml/SOURCE.txt index 7a119d75..99049c00 100644 --- a/RenderPipelineFile/rplibs/yaml/SOURCE.txt +++ b/RenderPipelineFile/rplibs/yaml/SOURCE.txt @@ -1,4 +1,4 @@ -package: PyYAML -version: 3.11 -downloaded: 12-23-2015 -url: https://pypi.python.org/pypi/PyYAML +package: PyYAML +version: 3.11 +downloaded: 12-23-2015 +url: https://pypi.python.org/pypi/PyYAML diff --git a/RenderPipelineFile/rplibs/yaml/__init__.py b/RenderPipelineFile/rplibs/yaml/__init__.py index fec84a63..e0e95925 100644 --- a/RenderPipelineFile/rplibs/yaml/__init__.py +++ b/RenderPipelineFile/rplibs/yaml/__init__.py @@ -1,62 +1,62 @@ -""" - -Main PyYAML importer script. Provides functions to load YAML files as dictionaries. - -""" - -from __future__ import print_function - -import sys -import collections -from direct.stdpy.file import open -from rpcore.rpobject import RPObject - -# Import different PyYaml versions depending on the used python version -if sys.version_info < (3, 0): - from .yaml_py2 import load as yaml_load - from .yaml_py2 import YAMLError, SafeLoader -else: - from .yaml_py3 import load as yaml_load - from .yaml_py3 import YAMLError, SafeLoader - -__all__ = ["load_yaml_file", "load_yaml_file_flat"] - -def load_yaml_file(filename): - """ This method is a wrapper arround yaml_load, and provides error checking """ - - import time - start = time.process_time() - - try: - with open(filename, "r") as handle: - parsed_yaml = yaml_load(handle, Loader=SafeLoader) - except IOError as msg: - RPObject.global_error("YAMLLoader", "Could not find or open file:", filename) - RPObject.global_error("YAMLLoader", msg) - raise Exception("Failed to load YAML file: File not found") - except YAMLError as msg: - RPObject.global_error("YAMLLoader", "Invalid yaml-syntax in file:", filename) - RPObject.global_error("YAMLLoader", msg) - raise Exception("Failed to load YAML file: Invalid syntax") - - duration = (time.process_time() - start) * 1000.0 - - # Optionally print out profiling information - # print("Took", round(duration, 2), "ms to load", filename) - - return parsed_yaml - -def __flatten(d, parent_key=''): - """ Internal method to flatten a dictionary """ - items = [] - for k, v in d.items(): - try: - items.extend(__flatten(v, '{}{}.'.format(parent_key, k)).items()) - except AttributeError: - items.append(('{}{}'.format(parent_key, k), v)) - return dict(items) - -def load_yaml_file_flat(filename): - """ Behaves like load_yaml_file, but instead of creating nested dictionaries - it connects keys via '.' """ - return __flatten(load_yaml_file(filename)) +""" + +Main PyYAML importer script. Provides functions to load YAML files as dictionaries. + +""" + +from __future__ import print_function + +import sys +import collections +from direct.stdpy.file import open +from rpcore.rpobject import RPObject + +# Import different PyYaml versions depending on the used python version +if sys.version_info < (3, 0): + from .yaml_py2 import load as yaml_load + from .yaml_py2 import YAMLError, SafeLoader +else: + from .yaml_py3 import load as yaml_load + from .yaml_py3 import YAMLError, SafeLoader + +__all__ = ["load_yaml_file", "load_yaml_file_flat"] + +def load_yaml_file(filename): + """ This method is a wrapper arround yaml_load, and provides error checking """ + + import time + start = time.process_time() + + try: + with open(filename, "r") as handle: + parsed_yaml = yaml_load(handle, Loader=SafeLoader) + except IOError as msg: + RPObject.global_error("YAMLLoader", "Could not find or open file:", filename) + RPObject.global_error("YAMLLoader", msg) + raise Exception("Failed to load YAML file: File not found") + except YAMLError as msg: + RPObject.global_error("YAMLLoader", "Invalid yaml-syntax in file:", filename) + RPObject.global_error("YAMLLoader", msg) + raise Exception("Failed to load YAML file: Invalid syntax") + + duration = (time.process_time() - start) * 1000.0 + + # Optionally print out profiling information + # print("Took", round(duration, 2), "ms to load", filename) + + return parsed_yaml + +def __flatten(d, parent_key=''): + """ Internal method to flatten a dictionary """ + items = [] + for k, v in d.items(): + try: + items.extend(__flatten(v, '{}{}.'.format(parent_key, k)).items()) + except AttributeError: + items.append(('{}{}'.format(parent_key, k), v)) + return dict(items) + +def load_yaml_file_flat(filename): + """ Behaves like load_yaml_file, but instead of creating nested dictionaries + it connects keys via '.' """ + return __flatten(load_yaml_file(filename)) diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py2/__init__.py b/RenderPipelineFile/rplibs/yaml/yaml_py2/__init__.py index 76e19e13..9f3c0ce2 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py2/__init__.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py2/__init__.py @@ -1,315 +1,315 @@ - -from error import * - -from tokens import * -from events import * -from nodes import * - -from loader import * -from dumper import * - -__version__ = '3.11' - -try: - from cyaml import * - __with_libyaml__ = True -except ImportError: - __with_libyaml__ = False - -def scan(stream, Loader=Loader): - """ - Scan a YAML stream and produce scanning tokens. - """ - loader = Loader(stream) - try: - while loader.check_token(): - yield loader.get_token() - finally: - loader.dispose() - -def parse(stream, Loader=Loader): - """ - Parse a YAML stream and produce parsing events. - """ - loader = Loader(stream) - try: - while loader.check_event(): - yield loader.get_event() - finally: - loader.dispose() - -def compose(stream, Loader=Loader): - """ - Parse the first YAML document in a stream - and produce the corresponding representation tree. - """ - loader = Loader(stream) - try: - return loader.get_single_node() - finally: - loader.dispose() - -def compose_all(stream, Loader=Loader): - """ - Parse all YAML documents in a stream - and produce corresponding representation trees. - """ - loader = Loader(stream) - try: - while loader.check_node(): - yield loader.get_node() - finally: - loader.dispose() - -def load(stream, Loader=Loader): - """ - Parse the first YAML document in a stream - and produce the corresponding Python object. - """ - loader = Loader(stream) - try: - return loader.get_single_data() - finally: - loader.dispose() - -def load_all(stream, Loader=Loader): - """ - Parse all YAML documents in a stream - and produce corresponding Python objects. - """ - loader = Loader(stream) - try: - while loader.check_data(): - yield loader.get_data() - finally: - loader.dispose() - -def safe_load(stream): - """ - Parse the first YAML document in a stream - and produce the corresponding Python object. - Resolve only basic YAML tags. - """ - return load(stream, SafeLoader) - -def safe_load_all(stream): - """ - Parse all YAML documents in a stream - and produce corresponding Python objects. - Resolve only basic YAML tags. - """ - return load_all(stream, SafeLoader) - -def emit(events, stream=None, Dumper=Dumper, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None): - """ - Emit YAML parsing events into a stream. - If stream is None, return the produced string instead. - """ - getvalue = None - if stream is None: - from StringIO import StringIO - stream = StringIO() - getvalue = stream.getvalue - dumper = Dumper(stream, canonical=canonical, indent=indent, width=width, - allow_unicode=allow_unicode, line_break=line_break) - try: - for event in events: - dumper.emit(event) - finally: - dumper.dispose() - if getvalue: - return getvalue() - -def serialize_all(nodes, stream=None, Dumper=Dumper, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding='utf-8', explicit_start=None, explicit_end=None, - version=None, tags=None): - """ - Serialize a sequence of representation trees into a YAML stream. - If stream is None, return the produced string instead. - """ - getvalue = None - if stream is None: - if encoding is None: - from StringIO import StringIO - else: - from cStringIO import StringIO - stream = StringIO() - getvalue = stream.getvalue - dumper = Dumper(stream, canonical=canonical, indent=indent, width=width, - allow_unicode=allow_unicode, line_break=line_break, - encoding=encoding, version=version, tags=tags, - explicit_start=explicit_start, explicit_end=explicit_end) - try: - dumper.open() - for node in nodes: - dumper.serialize(node) - dumper.close() - finally: - dumper.dispose() - if getvalue: - return getvalue() - -def serialize(node, stream=None, Dumper=Dumper, **kwds): - """ - Serialize a representation tree into a YAML stream. - If stream is None, return the produced string instead. - """ - return serialize_all([node], stream, Dumper=Dumper, **kwds) - -def dump_all(documents, stream=None, Dumper=Dumper, - default_style=None, default_flow_style=None, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding='utf-8', explicit_start=None, explicit_end=None, - version=None, tags=None): - """ - Serialize a sequence of Python objects into a YAML stream. - If stream is None, return the produced string instead. - """ - getvalue = None - if stream is None: - if encoding is None: - from StringIO import StringIO - else: - from cStringIO import StringIO - stream = StringIO() - getvalue = stream.getvalue - dumper = Dumper(stream, default_style=default_style, - default_flow_style=default_flow_style, - canonical=canonical, indent=indent, width=width, - allow_unicode=allow_unicode, line_break=line_break, - encoding=encoding, version=version, tags=tags, - explicit_start=explicit_start, explicit_end=explicit_end) - try: - dumper.open() - for data in documents: - dumper.represent(data) - dumper.close() - finally: - dumper.dispose() - if getvalue: - return getvalue() - -def dump(data, stream=None, Dumper=Dumper, **kwds): - """ - Serialize a Python object into a YAML stream. - If stream is None, return the produced string instead. - """ - return dump_all([data], stream, Dumper=Dumper, **kwds) - -def safe_dump_all(documents, stream=None, **kwds): - """ - Serialize a sequence of Python objects into a YAML stream. - Produce only basic YAML tags. - If stream is None, return the produced string instead. - """ - return dump_all(documents, stream, Dumper=SafeDumper, **kwds) - -def safe_dump(data, stream=None, **kwds): - """ - Serialize a Python object into a YAML stream. - Produce only basic YAML tags. - If stream is None, return the produced string instead. - """ - return dump_all([data], stream, Dumper=SafeDumper, **kwds) - -def add_implicit_resolver(tag, regexp, first=None, - Loader=Loader, Dumper=Dumper): - """ - Add an implicit scalar detector. - If an implicit scalar value matches the given regexp, - the corresponding tag is assigned to the scalar. - first is a sequence of possible initial characters or None. - """ - Loader.add_implicit_resolver(tag, regexp, first) - Dumper.add_implicit_resolver(tag, regexp, first) - -def add_path_resolver(tag, path, kind=None, Loader=Loader, Dumper=Dumper): - """ - Add a path based resolver for the given tag. - A path is a list of keys that forms a path - to a node in the representation tree. - Keys can be string values, integers, or None. - """ - Loader.add_path_resolver(tag, path, kind) - Dumper.add_path_resolver(tag, path, kind) - -def add_constructor(tag, constructor, Loader=Loader): - """ - Add a constructor for the given tag. - Constructor is a function that accepts a Loader instance - and a node object and produces the corresponding Python object. - """ - Loader.add_constructor(tag, constructor) - -def add_multi_constructor(tag_prefix, multi_constructor, Loader=Loader): - """ - Add a multi-constructor for the given tag prefix. - Multi-constructor is called for a node if its tag starts with tag_prefix. - Multi-constructor accepts a Loader instance, a tag suffix, - and a node object and produces the corresponding Python object. - """ - Loader.add_multi_constructor(tag_prefix, multi_constructor) - -def add_representer(data_type, representer, Dumper=Dumper): - """ - Add a representer for the given type. - Representer is a function accepting a Dumper instance - and an instance of the given data type - and producing the corresponding representation node. - """ - Dumper.add_representer(data_type, representer) - -def add_multi_representer(data_type, multi_representer, Dumper=Dumper): - """ - Add a representer for the given type. - Multi-representer is a function accepting a Dumper instance - and an instance of the given data type or subtype - and producing the corresponding representation node. - """ - Dumper.add_multi_representer(data_type, multi_representer) - -class YAMLObjectMetaclass(type): - """ - The metaclass for YAMLObject. - """ - def __init__(cls, name, bases, kwds): - super(YAMLObjectMetaclass, cls).__init__(name, bases, kwds) - if 'yaml_tag' in kwds and kwds['yaml_tag'] is not None: - cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml) - cls.yaml_dumper.add_representer(cls, cls.to_yaml) - -class YAMLObject(object): - """ - An object that can dump itself to a YAML stream - and load itself from a YAML stream. - """ - - __metaclass__ = YAMLObjectMetaclass - __slots__ = () # no direct instantiation, so allow immutable subclasses - - yaml_loader = Loader - yaml_dumper = Dumper - - yaml_tag = None - yaml_flow_style = None - - def from_yaml(cls, loader, node): - """ - Convert a representation node to a Python object. - """ - return loader.construct_yaml_object(node, cls) - from_yaml = classmethod(from_yaml) - - def to_yaml(cls, dumper, data): - """ - Convert a Python object to a representation node. - """ - return dumper.represent_yaml_object(cls.yaml_tag, data, cls, - flow_style=cls.yaml_flow_style) - to_yaml = classmethod(to_yaml) - + +from error import * + +from tokens import * +from events import * +from nodes import * + +from loader import * +from dumper import * + +__version__ = '3.11' + +try: + from cyaml import * + __with_libyaml__ = True +except ImportError: + __with_libyaml__ = False + +def scan(stream, Loader=Loader): + """ + Scan a YAML stream and produce scanning tokens. + """ + loader = Loader(stream) + try: + while loader.check_token(): + yield loader.get_token() + finally: + loader.dispose() + +def parse(stream, Loader=Loader): + """ + Parse a YAML stream and produce parsing events. + """ + loader = Loader(stream) + try: + while loader.check_event(): + yield loader.get_event() + finally: + loader.dispose() + +def compose(stream, Loader=Loader): + """ + Parse the first YAML document in a stream + and produce the corresponding representation tree. + """ + loader = Loader(stream) + try: + return loader.get_single_node() + finally: + loader.dispose() + +def compose_all(stream, Loader=Loader): + """ + Parse all YAML documents in a stream + and produce corresponding representation trees. + """ + loader = Loader(stream) + try: + while loader.check_node(): + yield loader.get_node() + finally: + loader.dispose() + +def load(stream, Loader=Loader): + """ + Parse the first YAML document in a stream + and produce the corresponding Python object. + """ + loader = Loader(stream) + try: + return loader.get_single_data() + finally: + loader.dispose() + +def load_all(stream, Loader=Loader): + """ + Parse all YAML documents in a stream + and produce corresponding Python objects. + """ + loader = Loader(stream) + try: + while loader.check_data(): + yield loader.get_data() + finally: + loader.dispose() + +def safe_load(stream): + """ + Parse the first YAML document in a stream + and produce the corresponding Python object. + Resolve only basic YAML tags. + """ + return load(stream, SafeLoader) + +def safe_load_all(stream): + """ + Parse all YAML documents in a stream + and produce corresponding Python objects. + Resolve only basic YAML tags. + """ + return load_all(stream, SafeLoader) + +def emit(events, stream=None, Dumper=Dumper, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None): + """ + Emit YAML parsing events into a stream. + If stream is None, return the produced string instead. + """ + getvalue = None + if stream is None: + from StringIO import StringIO + stream = StringIO() + getvalue = stream.getvalue + dumper = Dumper(stream, canonical=canonical, indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break) + try: + for event in events: + dumper.emit(event) + finally: + dumper.dispose() + if getvalue: + return getvalue() + +def serialize_all(nodes, stream=None, Dumper=Dumper, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding='utf-8', explicit_start=None, explicit_end=None, + version=None, tags=None): + """ + Serialize a sequence of representation trees into a YAML stream. + If stream is None, return the produced string instead. + """ + getvalue = None + if stream is None: + if encoding is None: + from StringIO import StringIO + else: + from cStringIO import StringIO + stream = StringIO() + getvalue = stream.getvalue + dumper = Dumper(stream, canonical=canonical, indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break, + encoding=encoding, version=version, tags=tags, + explicit_start=explicit_start, explicit_end=explicit_end) + try: + dumper.open() + for node in nodes: + dumper.serialize(node) + dumper.close() + finally: + dumper.dispose() + if getvalue: + return getvalue() + +def serialize(node, stream=None, Dumper=Dumper, **kwds): + """ + Serialize a representation tree into a YAML stream. + If stream is None, return the produced string instead. + """ + return serialize_all([node], stream, Dumper=Dumper, **kwds) + +def dump_all(documents, stream=None, Dumper=Dumper, + default_style=None, default_flow_style=None, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding='utf-8', explicit_start=None, explicit_end=None, + version=None, tags=None): + """ + Serialize a sequence of Python objects into a YAML stream. + If stream is None, return the produced string instead. + """ + getvalue = None + if stream is None: + if encoding is None: + from StringIO import StringIO + else: + from cStringIO import StringIO + stream = StringIO() + getvalue = stream.getvalue + dumper = Dumper(stream, default_style=default_style, + default_flow_style=default_flow_style, + canonical=canonical, indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break, + encoding=encoding, version=version, tags=tags, + explicit_start=explicit_start, explicit_end=explicit_end) + try: + dumper.open() + for data in documents: + dumper.represent(data) + dumper.close() + finally: + dumper.dispose() + if getvalue: + return getvalue() + +def dump(data, stream=None, Dumper=Dumper, **kwds): + """ + Serialize a Python object into a YAML stream. + If stream is None, return the produced string instead. + """ + return dump_all([data], stream, Dumper=Dumper, **kwds) + +def safe_dump_all(documents, stream=None, **kwds): + """ + Serialize a sequence of Python objects into a YAML stream. + Produce only basic YAML tags. + If stream is None, return the produced string instead. + """ + return dump_all(documents, stream, Dumper=SafeDumper, **kwds) + +def safe_dump(data, stream=None, **kwds): + """ + Serialize a Python object into a YAML stream. + Produce only basic YAML tags. + If stream is None, return the produced string instead. + """ + return dump_all([data], stream, Dumper=SafeDumper, **kwds) + +def add_implicit_resolver(tag, regexp, first=None, + Loader=Loader, Dumper=Dumper): + """ + Add an implicit scalar detector. + If an implicit scalar value matches the given regexp, + the corresponding tag is assigned to the scalar. + first is a sequence of possible initial characters or None. + """ + Loader.add_implicit_resolver(tag, regexp, first) + Dumper.add_implicit_resolver(tag, regexp, first) + +def add_path_resolver(tag, path, kind=None, Loader=Loader, Dumper=Dumper): + """ + Add a path based resolver for the given tag. + A path is a list of keys that forms a path + to a node in the representation tree. + Keys can be string values, integers, or None. + """ + Loader.add_path_resolver(tag, path, kind) + Dumper.add_path_resolver(tag, path, kind) + +def add_constructor(tag, constructor, Loader=Loader): + """ + Add a constructor for the given tag. + Constructor is a function that accepts a Loader instance + and a node object and produces the corresponding Python object. + """ + Loader.add_constructor(tag, constructor) + +def add_multi_constructor(tag_prefix, multi_constructor, Loader=Loader): + """ + Add a multi-constructor for the given tag prefix. + Multi-constructor is called for a node if its tag starts with tag_prefix. + Multi-constructor accepts a Loader instance, a tag suffix, + and a node object and produces the corresponding Python object. + """ + Loader.add_multi_constructor(tag_prefix, multi_constructor) + +def add_representer(data_type, representer, Dumper=Dumper): + """ + Add a representer for the given type. + Representer is a function accepting a Dumper instance + and an instance of the given data type + and producing the corresponding representation node. + """ + Dumper.add_representer(data_type, representer) + +def add_multi_representer(data_type, multi_representer, Dumper=Dumper): + """ + Add a representer for the given type. + Multi-representer is a function accepting a Dumper instance + and an instance of the given data type or subtype + and producing the corresponding representation node. + """ + Dumper.add_multi_representer(data_type, multi_representer) + +class YAMLObjectMetaclass(type): + """ + The metaclass for YAMLObject. + """ + def __init__(cls, name, bases, kwds): + super(YAMLObjectMetaclass, cls).__init__(name, bases, kwds) + if 'yaml_tag' in kwds and kwds['yaml_tag'] is not None: + cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml) + cls.yaml_dumper.add_representer(cls, cls.to_yaml) + +class YAMLObject(object): + """ + An object that can dump itself to a YAML stream + and load itself from a YAML stream. + """ + + __metaclass__ = YAMLObjectMetaclass + __slots__ = () # no direct instantiation, so allow immutable subclasses + + yaml_loader = Loader + yaml_dumper = Dumper + + yaml_tag = None + yaml_flow_style = None + + def from_yaml(cls, loader, node): + """ + Convert a representation node to a Python object. + """ + return loader.construct_yaml_object(node, cls) + from_yaml = classmethod(from_yaml) + + def to_yaml(cls, dumper, data): + """ + Convert a Python object to a representation node. + """ + return dumper.represent_yaml_object(cls.yaml_tag, data, cls, + flow_style=cls.yaml_flow_style) + to_yaml = classmethod(to_yaml) + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py2/composer.py b/RenderPipelineFile/rplibs/yaml/yaml_py2/composer.py index 06e5ac78..67b63f18 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py2/composer.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py2/composer.py @@ -1,139 +1,139 @@ - -__all__ = ['Composer', 'ComposerError'] - -from error import MarkedYAMLError -from events import * -from nodes import * - -class ComposerError(MarkedYAMLError): - pass - -class Composer(object): - - def __init__(self): - self.anchors = {} - - def check_node(self): - # Drop the STREAM-START event. - if self.check_event(StreamStartEvent): - self.get_event() - - # If there are more documents available? - return not self.check_event(StreamEndEvent) - - def get_node(self): - # Get the root node of the next document. - if not self.check_event(StreamEndEvent): - return self.compose_document() - - def get_single_node(self): - # Drop the STREAM-START event. - self.get_event() - - # Compose a document if the stream is not empty. - document = None - if not self.check_event(StreamEndEvent): - document = self.compose_document() - - # Ensure that the stream contains no more documents. - if not self.check_event(StreamEndEvent): - event = self.get_event() - raise ComposerError("expected a single document in the stream", - document.start_mark, "but found another document", - event.start_mark) - - # Drop the STREAM-END event. - self.get_event() - - return document - - def compose_document(self): - # Drop the DOCUMENT-START event. - self.get_event() - - # Compose the root node. - node = self.compose_node(None, None) - - # Drop the DOCUMENT-END event. - self.get_event() - - self.anchors = {} - return node - - def compose_node(self, parent, index): - if self.check_event(AliasEvent): - event = self.get_event() - anchor = event.anchor - if anchor not in self.anchors: - raise ComposerError(None, None, "found undefined alias %r" - % anchor.encode('utf-8'), event.start_mark) - return self.anchors[anchor] - event = self.peek_event() - anchor = event.anchor - if anchor is not None: - if anchor in self.anchors: - raise ComposerError("found duplicate anchor %r; first occurence" - % anchor.encode('utf-8'), self.anchors[anchor].start_mark, - "second occurence", event.start_mark) - self.descend_resolver(parent, index) - if self.check_event(ScalarEvent): - node = self.compose_scalar_node(anchor) - elif self.check_event(SequenceStartEvent): - node = self.compose_sequence_node(anchor) - elif self.check_event(MappingStartEvent): - node = self.compose_mapping_node(anchor) - self.ascend_resolver() - return node - - def compose_scalar_node(self, anchor): - event = self.get_event() - tag = event.tag - if tag is None or tag == u'!': - tag = self.resolve(ScalarNode, event.value, event.implicit) - node = ScalarNode(tag, event.value, - event.start_mark, event.end_mark, style=event.style) - if anchor is not None: - self.anchors[anchor] = node - return node - - def compose_sequence_node(self, anchor): - start_event = self.get_event() - tag = start_event.tag - if tag is None or tag == u'!': - tag = self.resolve(SequenceNode, None, start_event.implicit) - node = SequenceNode(tag, [], - start_event.start_mark, None, - flow_style=start_event.flow_style) - if anchor is not None: - self.anchors[anchor] = node - index = 0 - while not self.check_event(SequenceEndEvent): - node.value.append(self.compose_node(node, index)) - index += 1 - end_event = self.get_event() - node.end_mark = end_event.end_mark - return node - - def compose_mapping_node(self, anchor): - start_event = self.get_event() - tag = start_event.tag - if tag is None or tag == u'!': - tag = self.resolve(MappingNode, None, start_event.implicit) - node = MappingNode(tag, [], - start_event.start_mark, None, - flow_style=start_event.flow_style) - if anchor is not None: - self.anchors[anchor] = node - while not self.check_event(MappingEndEvent): - #key_event = self.peek_event() - item_key = self.compose_node(node, None) - #if item_key in node.value: - # raise ComposerError("while composing a mapping", start_event.start_mark, - # "found duplicate key", key_event.start_mark) - item_value = self.compose_node(node, item_key) - #node.value[item_key] = item_value - node.value.append((item_key, item_value)) - end_event = self.get_event() - node.end_mark = end_event.end_mark - return node - + +__all__ = ['Composer', 'ComposerError'] + +from error import MarkedYAMLError +from events import * +from nodes import * + +class ComposerError(MarkedYAMLError): + pass + +class Composer(object): + + def __init__(self): + self.anchors = {} + + def check_node(self): + # Drop the STREAM-START event. + if self.check_event(StreamStartEvent): + self.get_event() + + # If there are more documents available? + return not self.check_event(StreamEndEvent) + + def get_node(self): + # Get the root node of the next document. + if not self.check_event(StreamEndEvent): + return self.compose_document() + + def get_single_node(self): + # Drop the STREAM-START event. + self.get_event() + + # Compose a document if the stream is not empty. + document = None + if not self.check_event(StreamEndEvent): + document = self.compose_document() + + # Ensure that the stream contains no more documents. + if not self.check_event(StreamEndEvent): + event = self.get_event() + raise ComposerError("expected a single document in the stream", + document.start_mark, "but found another document", + event.start_mark) + + # Drop the STREAM-END event. + self.get_event() + + return document + + def compose_document(self): + # Drop the DOCUMENT-START event. + self.get_event() + + # Compose the root node. + node = self.compose_node(None, None) + + # Drop the DOCUMENT-END event. + self.get_event() + + self.anchors = {} + return node + + def compose_node(self, parent, index): + if self.check_event(AliasEvent): + event = self.get_event() + anchor = event.anchor + if anchor not in self.anchors: + raise ComposerError(None, None, "found undefined alias %r" + % anchor.encode('utf-8'), event.start_mark) + return self.anchors[anchor] + event = self.peek_event() + anchor = event.anchor + if anchor is not None: + if anchor in self.anchors: + raise ComposerError("found duplicate anchor %r; first occurence" + % anchor.encode('utf-8'), self.anchors[anchor].start_mark, + "second occurence", event.start_mark) + self.descend_resolver(parent, index) + if self.check_event(ScalarEvent): + node = self.compose_scalar_node(anchor) + elif self.check_event(SequenceStartEvent): + node = self.compose_sequence_node(anchor) + elif self.check_event(MappingStartEvent): + node = self.compose_mapping_node(anchor) + self.ascend_resolver() + return node + + def compose_scalar_node(self, anchor): + event = self.get_event() + tag = event.tag + if tag is None or tag == u'!': + tag = self.resolve(ScalarNode, event.value, event.implicit) + node = ScalarNode(tag, event.value, + event.start_mark, event.end_mark, style=event.style) + if anchor is not None: + self.anchors[anchor] = node + return node + + def compose_sequence_node(self, anchor): + start_event = self.get_event() + tag = start_event.tag + if tag is None or tag == u'!': + tag = self.resolve(SequenceNode, None, start_event.implicit) + node = SequenceNode(tag, [], + start_event.start_mark, None, + flow_style=start_event.flow_style) + if anchor is not None: + self.anchors[anchor] = node + index = 0 + while not self.check_event(SequenceEndEvent): + node.value.append(self.compose_node(node, index)) + index += 1 + end_event = self.get_event() + node.end_mark = end_event.end_mark + return node + + def compose_mapping_node(self, anchor): + start_event = self.get_event() + tag = start_event.tag + if tag is None or tag == u'!': + tag = self.resolve(MappingNode, None, start_event.implicit) + node = MappingNode(tag, [], + start_event.start_mark, None, + flow_style=start_event.flow_style) + if anchor is not None: + self.anchors[anchor] = node + while not self.check_event(MappingEndEvent): + #key_event = self.peek_event() + item_key = self.compose_node(node, None) + #if item_key in node.value: + # raise ComposerError("while composing a mapping", start_event.start_mark, + # "found duplicate key", key_event.start_mark) + item_value = self.compose_node(node, item_key) + #node.value[item_key] = item_value + node.value.append((item_key, item_value)) + end_event = self.get_event() + node.end_mark = end_event.end_mark + return node + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py2/constructor.py b/RenderPipelineFile/rplibs/yaml/yaml_py2/constructor.py index 635faac3..c6cb9809 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py2/constructor.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py2/constructor.py @@ -1,675 +1,675 @@ - -__all__ = ['BaseConstructor', 'SafeConstructor', 'Constructor', - 'ConstructorError'] - -from error import * -from nodes import * - -import datetime - -import binascii, re, sys, types - -class ConstructorError(MarkedYAMLError): - pass - -class BaseConstructor(object): - - yaml_constructors = {} - yaml_multi_constructors = {} - - def __init__(self): - self.constructed_objects = {} - self.recursive_objects = {} - self.state_generators = [] - self.deep_construct = False - - def check_data(self): - # If there are more documents available? - return self.check_node() - - def get_data(self): - # Construct and return the next document. - if self.check_node(): - return self.construct_document(self.get_node()) - - def get_single_data(self): - # Ensure that the stream contains a single document and construct it. - node = self.get_single_node() - if node is not None: - return self.construct_document(node) - return None - - def construct_document(self, node): - data = self.construct_object(node) - while self.state_generators: - state_generators = self.state_generators - self.state_generators = [] - for generator in state_generators: - for dummy in generator: - pass - self.constructed_objects = {} - self.recursive_objects = {} - self.deep_construct = False - return data - - def construct_object(self, node, deep=False): - if node in self.constructed_objects: - return self.constructed_objects[node] - if deep: - old_deep = self.deep_construct - self.deep_construct = True - if node in self.recursive_objects: - raise ConstructorError(None, None, - "found unconstructable recursive node", node.start_mark) - self.recursive_objects[node] = None - constructor = None - tag_suffix = None - if node.tag in self.yaml_constructors: - constructor = self.yaml_constructors[node.tag] - else: - for tag_prefix in self.yaml_multi_constructors: - if node.tag.startswith(tag_prefix): - tag_suffix = node.tag[len(tag_prefix):] - constructor = self.yaml_multi_constructors[tag_prefix] - break - else: - if None in self.yaml_multi_constructors: - tag_suffix = node.tag - constructor = self.yaml_multi_constructors[None] - elif None in self.yaml_constructors: - constructor = self.yaml_constructors[None] - elif isinstance(node, ScalarNode): - constructor = self.__class__.construct_scalar - elif isinstance(node, SequenceNode): - constructor = self.__class__.construct_sequence - elif isinstance(node, MappingNode): - constructor = self.__class__.construct_mapping - if tag_suffix is None: - data = constructor(self, node) - else: - data = constructor(self, tag_suffix, node) - if isinstance(data, types.GeneratorType): - generator = data - data = generator.next() - if self.deep_construct: - for dummy in generator: - pass - else: - self.state_generators.append(generator) - self.constructed_objects[node] = data - del self.recursive_objects[node] - if deep: - self.deep_construct = old_deep - return data - - def construct_scalar(self, node): - if not isinstance(node, ScalarNode): - raise ConstructorError(None, None, - "expected a scalar node, but found %s" % node.id, - node.start_mark) - return node.value - - def construct_sequence(self, node, deep=False): - if not isinstance(node, SequenceNode): - raise ConstructorError(None, None, - "expected a sequence node, but found %s" % node.id, - node.start_mark) - return [self.construct_object(child, deep=deep) - for child in node.value] - - def construct_mapping(self, node, deep=False): - if not isinstance(node, MappingNode): - raise ConstructorError(None, None, - "expected a mapping node, but found %s" % node.id, - node.start_mark) - mapping = {} - for key_node, value_node in node.value: - key = self.construct_object(key_node, deep=deep) - try: - hash(key) - except TypeError, exc: - raise ConstructorError("while constructing a mapping", node.start_mark, - "found unacceptable key (%s)" % exc, key_node.start_mark) - value = self.construct_object(value_node, deep=deep) - mapping[key] = value - return mapping - - def construct_pairs(self, node, deep=False): - if not isinstance(node, MappingNode): - raise ConstructorError(None, None, - "expected a mapping node, but found %s" % node.id, - node.start_mark) - pairs = [] - for key_node, value_node in node.value: - key = self.construct_object(key_node, deep=deep) - value = self.construct_object(value_node, deep=deep) - pairs.append((key, value)) - return pairs - - def add_constructor(cls, tag, constructor): - if not 'yaml_constructors' in cls.__dict__: - cls.yaml_constructors = cls.yaml_constructors.copy() - cls.yaml_constructors[tag] = constructor - add_constructor = classmethod(add_constructor) - - def add_multi_constructor(cls, tag_prefix, multi_constructor): - if not 'yaml_multi_constructors' in cls.__dict__: - cls.yaml_multi_constructors = cls.yaml_multi_constructors.copy() - cls.yaml_multi_constructors[tag_prefix] = multi_constructor - add_multi_constructor = classmethod(add_multi_constructor) - -class SafeConstructor(BaseConstructor): - - def construct_scalar(self, node): - if isinstance(node, MappingNode): - for key_node, value_node in node.value: - if key_node.tag == u'tag:yaml.org,2002:value': - return self.construct_scalar(value_node) - return BaseConstructor.construct_scalar(self, node) - - def flatten_mapping(self, node): - merge = [] - index = 0 - while index < len(node.value): - key_node, value_node = node.value[index] - if key_node.tag == u'tag:yaml.org,2002:merge': - del node.value[index] - if isinstance(value_node, MappingNode): - self.flatten_mapping(value_node) - merge.extend(value_node.value) - elif isinstance(value_node, SequenceNode): - submerge = [] - for subnode in value_node.value: - if not isinstance(subnode, MappingNode): - raise ConstructorError("while constructing a mapping", - node.start_mark, - "expected a mapping for merging, but found %s" - % subnode.id, subnode.start_mark) - self.flatten_mapping(subnode) - submerge.append(subnode.value) - submerge.reverse() - for value in submerge: - merge.extend(value) - else: - raise ConstructorError("while constructing a mapping", node.start_mark, - "expected a mapping or list of mappings for merging, but found %s" - % value_node.id, value_node.start_mark) - elif key_node.tag == u'tag:yaml.org,2002:value': - key_node.tag = u'tag:yaml.org,2002:str' - index += 1 - else: - index += 1 - if merge: - node.value = merge + node.value - - def construct_mapping(self, node, deep=False): - if isinstance(node, MappingNode): - self.flatten_mapping(node) - return BaseConstructor.construct_mapping(self, node, deep=deep) - - def construct_yaml_null(self, node): - self.construct_scalar(node) - return None - - bool_values = { - u'yes': True, - u'no': False, - u'true': True, - u'false': False, - u'on': True, - u'off': False, - } - - def construct_yaml_bool(self, node): - value = self.construct_scalar(node) - return self.bool_values[value.lower()] - - def construct_yaml_int(self, node): - value = str(self.construct_scalar(node)) - value = value.replace('_', '') - sign = +1 - if value[0] == '-': - sign = -1 - if value[0] in '+-': - value = value[1:] - if value == '0': - return 0 - elif value.startswith('0b'): - return sign*int(value[2:], 2) - elif value.startswith('0x'): - return sign*int(value[2:], 16) - elif value[0] == '0': - return sign*int(value, 8) - elif ':' in value: - digits = [int(part) for part in value.split(':')] - digits.reverse() - base = 1 - value = 0 - for digit in digits: - value += digit*base - base *= 60 - return sign*value - else: - return sign*int(value) - - inf_value = 1e300 - while inf_value != inf_value*inf_value: - inf_value *= inf_value - nan_value = -inf_value/inf_value # Trying to make a quiet NaN (like C99). - - def construct_yaml_float(self, node): - value = str(self.construct_scalar(node)) - value = value.replace('_', '').lower() - sign = +1 - if value[0] == '-': - sign = -1 - if value[0] in '+-': - value = value[1:] - if value == '.inf': - return sign*self.inf_value - elif value == '.nan': - return self.nan_value - elif ':' in value: - digits = [float(part) for part in value.split(':')] - digits.reverse() - base = 1 - value = 0.0 - for digit in digits: - value += digit*base - base *= 60 - return sign*value - else: - return sign*float(value) - - def construct_yaml_binary(self, node): - value = self.construct_scalar(node) - try: - return str(value).decode('base64') - except (binascii.Error, UnicodeEncodeError), exc: - raise ConstructorError(None, None, - "failed to decode base64 data: %s" % exc, node.start_mark) - - timestamp_regexp = re.compile( - ur'''^(?P[0-9][0-9][0-9][0-9]) - -(?P[0-9][0-9]?) - -(?P[0-9][0-9]?) - (?:(?:[Tt]|[ \t]+) - (?P[0-9][0-9]?) - :(?P[0-9][0-9]) - :(?P[0-9][0-9]) - (?:\.(?P[0-9]*))? - (?:[ \t]*(?PZ|(?P[-+])(?P[0-9][0-9]?) - (?::(?P[0-9][0-9]))?))?)?$''', re.X) - - def construct_yaml_timestamp(self, node): - value = self.construct_scalar(node) - match = self.timestamp_regexp.match(node.value) - values = match.groupdict() - year = int(values['year']) - month = int(values['month']) - day = int(values['day']) - if not values['hour']: - return datetime.date(year, month, day) - hour = int(values['hour']) - minute = int(values['minute']) - second = int(values['second']) - fraction = 0 - if values['fraction']: - fraction = values['fraction'][:6] - while len(fraction) < 6: - fraction += '0' - fraction = int(fraction) - delta = None - if values['tz_sign']: - tz_hour = int(values['tz_hour']) - tz_minute = int(values['tz_minute'] or 0) - delta = datetime.timedelta(hours=tz_hour, minutes=tz_minute) - if values['tz_sign'] == '-': - delta = -delta - data = datetime.datetime(year, month, day, hour, minute, second, fraction) - if delta: - data -= delta - return data - - def construct_yaml_omap(self, node): - # Note: we do not check for duplicate keys, because it's too - # CPU-expensive. - omap = [] - yield omap - if not isinstance(node, SequenceNode): - raise ConstructorError("while constructing an ordered map", node.start_mark, - "expected a sequence, but found %s" % node.id, node.start_mark) - for subnode in node.value: - if not isinstance(subnode, MappingNode): - raise ConstructorError("while constructing an ordered map", node.start_mark, - "expected a mapping of length 1, but found %s" % subnode.id, - subnode.start_mark) - if len(subnode.value) != 1: - raise ConstructorError("while constructing an ordered map", node.start_mark, - "expected a single mapping item, but found %d items" % len(subnode.value), - subnode.start_mark) - key_node, value_node = subnode.value[0] - key = self.construct_object(key_node) - value = self.construct_object(value_node) - omap.append((key, value)) - - def construct_yaml_pairs(self, node): - # Note: the same code as `construct_yaml_omap`. - pairs = [] - yield pairs - if not isinstance(node, SequenceNode): - raise ConstructorError("while constructing pairs", node.start_mark, - "expected a sequence, but found %s" % node.id, node.start_mark) - for subnode in node.value: - if not isinstance(subnode, MappingNode): - raise ConstructorError("while constructing pairs", node.start_mark, - "expected a mapping of length 1, but found %s" % subnode.id, - subnode.start_mark) - if len(subnode.value) != 1: - raise ConstructorError("while constructing pairs", node.start_mark, - "expected a single mapping item, but found %d items" % len(subnode.value), - subnode.start_mark) - key_node, value_node = subnode.value[0] - key = self.construct_object(key_node) - value = self.construct_object(value_node) - pairs.append((key, value)) - - def construct_yaml_set(self, node): - data = set() - yield data - value = self.construct_mapping(node) - data.update(value) - - def construct_yaml_str(self, node): - value = self.construct_scalar(node) - try: - return value.encode('ascii') - except UnicodeEncodeError: - return value - - def construct_yaml_seq(self, node): - data = [] - yield data - data.extend(self.construct_sequence(node)) - - def construct_yaml_map(self, node): - data = {} - yield data - value = self.construct_mapping(node) - data.update(value) - - def construct_yaml_object(self, node, cls): - data = cls.__new__(cls) - yield data - if hasattr(data, '__setstate__'): - state = self.construct_mapping(node, deep=True) - data.__setstate__(state) - else: - state = self.construct_mapping(node) - data.__dict__.update(state) - - def construct_undefined(self, node): - raise ConstructorError(None, None, - "could not determine a constructor for the tag %r" % node.tag.encode('utf-8'), - node.start_mark) - -SafeConstructor.add_constructor( - u'tag:yaml.org,2002:null', - SafeConstructor.construct_yaml_null) - -SafeConstructor.add_constructor( - u'tag:yaml.org,2002:bool', - SafeConstructor.construct_yaml_bool) - -SafeConstructor.add_constructor( - u'tag:yaml.org,2002:int', - SafeConstructor.construct_yaml_int) - -SafeConstructor.add_constructor( - u'tag:yaml.org,2002:float', - SafeConstructor.construct_yaml_float) - -SafeConstructor.add_constructor( - u'tag:yaml.org,2002:binary', - SafeConstructor.construct_yaml_binary) - -SafeConstructor.add_constructor( - u'tag:yaml.org,2002:timestamp', - SafeConstructor.construct_yaml_timestamp) - -SafeConstructor.add_constructor( - u'tag:yaml.org,2002:omap', - SafeConstructor.construct_yaml_omap) - -SafeConstructor.add_constructor( - u'tag:yaml.org,2002:pairs', - SafeConstructor.construct_yaml_pairs) - -SafeConstructor.add_constructor( - u'tag:yaml.org,2002:set', - SafeConstructor.construct_yaml_set) - -SafeConstructor.add_constructor( - u'tag:yaml.org,2002:str', - SafeConstructor.construct_yaml_str) - -SafeConstructor.add_constructor( - u'tag:yaml.org,2002:seq', - SafeConstructor.construct_yaml_seq) - -SafeConstructor.add_constructor( - u'tag:yaml.org,2002:map', - SafeConstructor.construct_yaml_map) - -SafeConstructor.add_constructor(None, - SafeConstructor.construct_undefined) - -class Constructor(SafeConstructor): - - def construct_python_str(self, node): - return self.construct_scalar(node).encode('utf-8') - - def construct_python_unicode(self, node): - return self.construct_scalar(node) - - def construct_python_long(self, node): - return long(self.construct_yaml_int(node)) - - def construct_python_complex(self, node): - return complex(self.construct_scalar(node)) - - def construct_python_tuple(self, node): - return tuple(self.construct_sequence(node)) - - def find_python_module(self, name, mark): - if not name: - raise ConstructorError("while constructing a Python module", mark, - "expected non-empty name appended to the tag", mark) - try: - __import__(name) - except ImportError, exc: - raise ConstructorError("while constructing a Python module", mark, - "cannot find module %r (%s)" % (name.encode('utf-8'), exc), mark) - return sys.modules[name] - - def find_python_name(self, name, mark): - if not name: - raise ConstructorError("while constructing a Python object", mark, - "expected non-empty name appended to the tag", mark) - if u'.' in name: - module_name, object_name = name.rsplit('.', 1) - else: - module_name = '__builtin__' - object_name = name - try: - __import__(module_name) - except ImportError, exc: - raise ConstructorError("while constructing a Python object", mark, - "cannot find module %r (%s)" % (module_name.encode('utf-8'), exc), mark) - module = sys.modules[module_name] - if not hasattr(module, object_name): - raise ConstructorError("while constructing a Python object", mark, - "cannot find %r in the module %r" % (object_name.encode('utf-8'), - module.__name__), mark) - return getattr(module, object_name) - - def construct_python_name(self, suffix, node): - value = self.construct_scalar(node) - if value: - raise ConstructorError("while constructing a Python name", node.start_mark, - "expected the empty value, but found %r" % value.encode('utf-8'), - node.start_mark) - return self.find_python_name(suffix, node.start_mark) - - def construct_python_module(self, suffix, node): - value = self.construct_scalar(node) - if value: - raise ConstructorError("while constructing a Python module", node.start_mark, - "expected the empty value, but found %r" % value.encode('utf-8'), - node.start_mark) - return self.find_python_module(suffix, node.start_mark) - - class classobj: pass - - def make_python_instance(self, suffix, node, - args=None, kwds=None, newobj=False): - if not args: - args = [] - if not kwds: - kwds = {} - cls = self.find_python_name(suffix, node.start_mark) - if newobj and isinstance(cls, type(self.classobj)) \ - and not args and not kwds: - instance = self.classobj() - instance.__class__ = cls - return instance - elif newobj and isinstance(cls, type): - return cls.__new__(cls, *args, **kwds) - else: - return cls(*args, **kwds) - - def set_python_instance_state(self, instance, state): - if hasattr(instance, '__setstate__'): - instance.__setstate__(state) - else: - slotstate = {} - if isinstance(state, tuple) and len(state) == 2: - state, slotstate = state - if hasattr(instance, '__dict__'): - instance.__dict__.update(state) - elif state: - slotstate.update(state) - for key, value in slotstate.items(): - setattr(object, key, value) - - def construct_python_object(self, suffix, node): - # Format: - # !!python/object:module.name { ... state ... } - instance = self.make_python_instance(suffix, node, newobj=True) - yield instance - deep = hasattr(instance, '__setstate__') - state = self.construct_mapping(node, deep=deep) - self.set_python_instance_state(instance, state) - - def construct_python_object_apply(self, suffix, node, newobj=False): - # Format: - # !!python/object/apply # (or !!python/object/new) - # args: [ ... arguments ... ] - # kwds: { ... keywords ... } - # state: ... state ... - # listitems: [ ... listitems ... ] - # dictitems: { ... dictitems ... } - # or short format: - # !!python/object/apply [ ... arguments ... ] - # The difference between !!python/object/apply and !!python/object/new - # is how an object is created, check make_python_instance for details. - if isinstance(node, SequenceNode): - args = self.construct_sequence(node, deep=True) - kwds = {} - state = {} - listitems = [] - dictitems = {} - else: - value = self.construct_mapping(node, deep=True) - args = value.get('args', []) - kwds = value.get('kwds', {}) - state = value.get('state', {}) - listitems = value.get('listitems', []) - dictitems = value.get('dictitems', {}) - instance = self.make_python_instance(suffix, node, args, kwds, newobj) - if state: - self.set_python_instance_state(instance, state) - if listitems: - instance.extend(listitems) - if dictitems: - for key in dictitems: - instance[key] = dictitems[key] - return instance - - def construct_python_object_new(self, suffix, node): - return self.construct_python_object_apply(suffix, node, newobj=True) - -Constructor.add_constructor( - u'tag:yaml.org,2002:python/none', - Constructor.construct_yaml_null) - -Constructor.add_constructor( - u'tag:yaml.org,2002:python/bool', - Constructor.construct_yaml_bool) - -Constructor.add_constructor( - u'tag:yaml.org,2002:python/str', - Constructor.construct_python_str) - -Constructor.add_constructor( - u'tag:yaml.org,2002:python/unicode', - Constructor.construct_python_unicode) - -Constructor.add_constructor( - u'tag:yaml.org,2002:python/int', - Constructor.construct_yaml_int) - -Constructor.add_constructor( - u'tag:yaml.org,2002:python/long', - Constructor.construct_python_long) - -Constructor.add_constructor( - u'tag:yaml.org,2002:python/float', - Constructor.construct_yaml_float) - -Constructor.add_constructor( - u'tag:yaml.org,2002:python/complex', - Constructor.construct_python_complex) - -Constructor.add_constructor( - u'tag:yaml.org,2002:python/list', - Constructor.construct_yaml_seq) - -Constructor.add_constructor( - u'tag:yaml.org,2002:python/tuple', - Constructor.construct_python_tuple) - -Constructor.add_constructor( - u'tag:yaml.org,2002:python/dict', - Constructor.construct_yaml_map) - -Constructor.add_multi_constructor( - u'tag:yaml.org,2002:python/name:', - Constructor.construct_python_name) - -Constructor.add_multi_constructor( - u'tag:yaml.org,2002:python/module:', - Constructor.construct_python_module) - -Constructor.add_multi_constructor( - u'tag:yaml.org,2002:python/object:', - Constructor.construct_python_object) - -Constructor.add_multi_constructor( - u'tag:yaml.org,2002:python/object/apply:', - Constructor.construct_python_object_apply) - -Constructor.add_multi_constructor( - u'tag:yaml.org,2002:python/object/new:', - Constructor.construct_python_object_new) - + +__all__ = ['BaseConstructor', 'SafeConstructor', 'Constructor', + 'ConstructorError'] + +from error import * +from nodes import * + +import datetime + +import binascii, re, sys, types + +class ConstructorError(MarkedYAMLError): + pass + +class BaseConstructor(object): + + yaml_constructors = {} + yaml_multi_constructors = {} + + def __init__(self): + self.constructed_objects = {} + self.recursive_objects = {} + self.state_generators = [] + self.deep_construct = False + + def check_data(self): + # If there are more documents available? + return self.check_node() + + def get_data(self): + # Construct and return the next document. + if self.check_node(): + return self.construct_document(self.get_node()) + + def get_single_data(self): + # Ensure that the stream contains a single document and construct it. + node = self.get_single_node() + if node is not None: + return self.construct_document(node) + return None + + def construct_document(self, node): + data = self.construct_object(node) + while self.state_generators: + state_generators = self.state_generators + self.state_generators = [] + for generator in state_generators: + for dummy in generator: + pass + self.constructed_objects = {} + self.recursive_objects = {} + self.deep_construct = False + return data + + def construct_object(self, node, deep=False): + if node in self.constructed_objects: + return self.constructed_objects[node] + if deep: + old_deep = self.deep_construct + self.deep_construct = True + if node in self.recursive_objects: + raise ConstructorError(None, None, + "found unconstructable recursive node", node.start_mark) + self.recursive_objects[node] = None + constructor = None + tag_suffix = None + if node.tag in self.yaml_constructors: + constructor = self.yaml_constructors[node.tag] + else: + for tag_prefix in self.yaml_multi_constructors: + if node.tag.startswith(tag_prefix): + tag_suffix = node.tag[len(tag_prefix):] + constructor = self.yaml_multi_constructors[tag_prefix] + break + else: + if None in self.yaml_multi_constructors: + tag_suffix = node.tag + constructor = self.yaml_multi_constructors[None] + elif None in self.yaml_constructors: + constructor = self.yaml_constructors[None] + elif isinstance(node, ScalarNode): + constructor = self.__class__.construct_scalar + elif isinstance(node, SequenceNode): + constructor = self.__class__.construct_sequence + elif isinstance(node, MappingNode): + constructor = self.__class__.construct_mapping + if tag_suffix is None: + data = constructor(self, node) + else: + data = constructor(self, tag_suffix, node) + if isinstance(data, types.GeneratorType): + generator = data + data = generator.next() + if self.deep_construct: + for dummy in generator: + pass + else: + self.state_generators.append(generator) + self.constructed_objects[node] = data + del self.recursive_objects[node] + if deep: + self.deep_construct = old_deep + return data + + def construct_scalar(self, node): + if not isinstance(node, ScalarNode): + raise ConstructorError(None, None, + "expected a scalar node, but found %s" % node.id, + node.start_mark) + return node.value + + def construct_sequence(self, node, deep=False): + if not isinstance(node, SequenceNode): + raise ConstructorError(None, None, + "expected a sequence node, but found %s" % node.id, + node.start_mark) + return [self.construct_object(child, deep=deep) + for child in node.value] + + def construct_mapping(self, node, deep=False): + if not isinstance(node, MappingNode): + raise ConstructorError(None, None, + "expected a mapping node, but found %s" % node.id, + node.start_mark) + mapping = {} + for key_node, value_node in node.value: + key = self.construct_object(key_node, deep=deep) + try: + hash(key) + except TypeError, exc: + raise ConstructorError("while constructing a mapping", node.start_mark, + "found unacceptable key (%s)" % exc, key_node.start_mark) + value = self.construct_object(value_node, deep=deep) + mapping[key] = value + return mapping + + def construct_pairs(self, node, deep=False): + if not isinstance(node, MappingNode): + raise ConstructorError(None, None, + "expected a mapping node, but found %s" % node.id, + node.start_mark) + pairs = [] + for key_node, value_node in node.value: + key = self.construct_object(key_node, deep=deep) + value = self.construct_object(value_node, deep=deep) + pairs.append((key, value)) + return pairs + + def add_constructor(cls, tag, constructor): + if not 'yaml_constructors' in cls.__dict__: + cls.yaml_constructors = cls.yaml_constructors.copy() + cls.yaml_constructors[tag] = constructor + add_constructor = classmethod(add_constructor) + + def add_multi_constructor(cls, tag_prefix, multi_constructor): + if not 'yaml_multi_constructors' in cls.__dict__: + cls.yaml_multi_constructors = cls.yaml_multi_constructors.copy() + cls.yaml_multi_constructors[tag_prefix] = multi_constructor + add_multi_constructor = classmethod(add_multi_constructor) + +class SafeConstructor(BaseConstructor): + + def construct_scalar(self, node): + if isinstance(node, MappingNode): + for key_node, value_node in node.value: + if key_node.tag == u'tag:yaml.org,2002:value': + return self.construct_scalar(value_node) + return BaseConstructor.construct_scalar(self, node) + + def flatten_mapping(self, node): + merge = [] + index = 0 + while index < len(node.value): + key_node, value_node = node.value[index] + if key_node.tag == u'tag:yaml.org,2002:merge': + del node.value[index] + if isinstance(value_node, MappingNode): + self.flatten_mapping(value_node) + merge.extend(value_node.value) + elif isinstance(value_node, SequenceNode): + submerge = [] + for subnode in value_node.value: + if not isinstance(subnode, MappingNode): + raise ConstructorError("while constructing a mapping", + node.start_mark, + "expected a mapping for merging, but found %s" + % subnode.id, subnode.start_mark) + self.flatten_mapping(subnode) + submerge.append(subnode.value) + submerge.reverse() + for value in submerge: + merge.extend(value) + else: + raise ConstructorError("while constructing a mapping", node.start_mark, + "expected a mapping or list of mappings for merging, but found %s" + % value_node.id, value_node.start_mark) + elif key_node.tag == u'tag:yaml.org,2002:value': + key_node.tag = u'tag:yaml.org,2002:str' + index += 1 + else: + index += 1 + if merge: + node.value = merge + node.value + + def construct_mapping(self, node, deep=False): + if isinstance(node, MappingNode): + self.flatten_mapping(node) + return BaseConstructor.construct_mapping(self, node, deep=deep) + + def construct_yaml_null(self, node): + self.construct_scalar(node) + return None + + bool_values = { + u'yes': True, + u'no': False, + u'true': True, + u'false': False, + u'on': True, + u'off': False, + } + + def construct_yaml_bool(self, node): + value = self.construct_scalar(node) + return self.bool_values[value.lower()] + + def construct_yaml_int(self, node): + value = str(self.construct_scalar(node)) + value = value.replace('_', '') + sign = +1 + if value[0] == '-': + sign = -1 + if value[0] in '+-': + value = value[1:] + if value == '0': + return 0 + elif value.startswith('0b'): + return sign*int(value[2:], 2) + elif value.startswith('0x'): + return sign*int(value[2:], 16) + elif value[0] == '0': + return sign*int(value, 8) + elif ':' in value: + digits = [int(part) for part in value.split(':')] + digits.reverse() + base = 1 + value = 0 + for digit in digits: + value += digit*base + base *= 60 + return sign*value + else: + return sign*int(value) + + inf_value = 1e300 + while inf_value != inf_value*inf_value: + inf_value *= inf_value + nan_value = -inf_value/inf_value # Trying to make a quiet NaN (like C99). + + def construct_yaml_float(self, node): + value = str(self.construct_scalar(node)) + value = value.replace('_', '').lower() + sign = +1 + if value[0] == '-': + sign = -1 + if value[0] in '+-': + value = value[1:] + if value == '.inf': + return sign*self.inf_value + elif value == '.nan': + return self.nan_value + elif ':' in value: + digits = [float(part) for part in value.split(':')] + digits.reverse() + base = 1 + value = 0.0 + for digit in digits: + value += digit*base + base *= 60 + return sign*value + else: + return sign*float(value) + + def construct_yaml_binary(self, node): + value = self.construct_scalar(node) + try: + return str(value).decode('base64') + except (binascii.Error, UnicodeEncodeError), exc: + raise ConstructorError(None, None, + "failed to decode base64 data: %s" % exc, node.start_mark) + + timestamp_regexp = re.compile( + ur'''^(?P[0-9][0-9][0-9][0-9]) + -(?P[0-9][0-9]?) + -(?P[0-9][0-9]?) + (?:(?:[Tt]|[ \t]+) + (?P[0-9][0-9]?) + :(?P[0-9][0-9]) + :(?P[0-9][0-9]) + (?:\.(?P[0-9]*))? + (?:[ \t]*(?PZ|(?P[-+])(?P[0-9][0-9]?) + (?::(?P[0-9][0-9]))?))?)?$''', re.X) + + def construct_yaml_timestamp(self, node): + value = self.construct_scalar(node) + match = self.timestamp_regexp.match(node.value) + values = match.groupdict() + year = int(values['year']) + month = int(values['month']) + day = int(values['day']) + if not values['hour']: + return datetime.date(year, month, day) + hour = int(values['hour']) + minute = int(values['minute']) + second = int(values['second']) + fraction = 0 + if values['fraction']: + fraction = values['fraction'][:6] + while len(fraction) < 6: + fraction += '0' + fraction = int(fraction) + delta = None + if values['tz_sign']: + tz_hour = int(values['tz_hour']) + tz_minute = int(values['tz_minute'] or 0) + delta = datetime.timedelta(hours=tz_hour, minutes=tz_minute) + if values['tz_sign'] == '-': + delta = -delta + data = datetime.datetime(year, month, day, hour, minute, second, fraction) + if delta: + data -= delta + return data + + def construct_yaml_omap(self, node): + # Note: we do not check for duplicate keys, because it's too + # CPU-expensive. + omap = [] + yield omap + if not isinstance(node, SequenceNode): + raise ConstructorError("while constructing an ordered map", node.start_mark, + "expected a sequence, but found %s" % node.id, node.start_mark) + for subnode in node.value: + if not isinstance(subnode, MappingNode): + raise ConstructorError("while constructing an ordered map", node.start_mark, + "expected a mapping of length 1, but found %s" % subnode.id, + subnode.start_mark) + if len(subnode.value) != 1: + raise ConstructorError("while constructing an ordered map", node.start_mark, + "expected a single mapping item, but found %d items" % len(subnode.value), + subnode.start_mark) + key_node, value_node = subnode.value[0] + key = self.construct_object(key_node) + value = self.construct_object(value_node) + omap.append((key, value)) + + def construct_yaml_pairs(self, node): + # Note: the same code as `construct_yaml_omap`. + pairs = [] + yield pairs + if not isinstance(node, SequenceNode): + raise ConstructorError("while constructing pairs", node.start_mark, + "expected a sequence, but found %s" % node.id, node.start_mark) + for subnode in node.value: + if not isinstance(subnode, MappingNode): + raise ConstructorError("while constructing pairs", node.start_mark, + "expected a mapping of length 1, but found %s" % subnode.id, + subnode.start_mark) + if len(subnode.value) != 1: + raise ConstructorError("while constructing pairs", node.start_mark, + "expected a single mapping item, but found %d items" % len(subnode.value), + subnode.start_mark) + key_node, value_node = subnode.value[0] + key = self.construct_object(key_node) + value = self.construct_object(value_node) + pairs.append((key, value)) + + def construct_yaml_set(self, node): + data = set() + yield data + value = self.construct_mapping(node) + data.update(value) + + def construct_yaml_str(self, node): + value = self.construct_scalar(node) + try: + return value.encode('ascii') + except UnicodeEncodeError: + return value + + def construct_yaml_seq(self, node): + data = [] + yield data + data.extend(self.construct_sequence(node)) + + def construct_yaml_map(self, node): + data = {} + yield data + value = self.construct_mapping(node) + data.update(value) + + def construct_yaml_object(self, node, cls): + data = cls.__new__(cls) + yield data + if hasattr(data, '__setstate__'): + state = self.construct_mapping(node, deep=True) + data.__setstate__(state) + else: + state = self.construct_mapping(node) + data.__dict__.update(state) + + def construct_undefined(self, node): + raise ConstructorError(None, None, + "could not determine a constructor for the tag %r" % node.tag.encode('utf-8'), + node.start_mark) + +SafeConstructor.add_constructor( + u'tag:yaml.org,2002:null', + SafeConstructor.construct_yaml_null) + +SafeConstructor.add_constructor( + u'tag:yaml.org,2002:bool', + SafeConstructor.construct_yaml_bool) + +SafeConstructor.add_constructor( + u'tag:yaml.org,2002:int', + SafeConstructor.construct_yaml_int) + +SafeConstructor.add_constructor( + u'tag:yaml.org,2002:float', + SafeConstructor.construct_yaml_float) + +SafeConstructor.add_constructor( + u'tag:yaml.org,2002:binary', + SafeConstructor.construct_yaml_binary) + +SafeConstructor.add_constructor( + u'tag:yaml.org,2002:timestamp', + SafeConstructor.construct_yaml_timestamp) + +SafeConstructor.add_constructor( + u'tag:yaml.org,2002:omap', + SafeConstructor.construct_yaml_omap) + +SafeConstructor.add_constructor( + u'tag:yaml.org,2002:pairs', + SafeConstructor.construct_yaml_pairs) + +SafeConstructor.add_constructor( + u'tag:yaml.org,2002:set', + SafeConstructor.construct_yaml_set) + +SafeConstructor.add_constructor( + u'tag:yaml.org,2002:str', + SafeConstructor.construct_yaml_str) + +SafeConstructor.add_constructor( + u'tag:yaml.org,2002:seq', + SafeConstructor.construct_yaml_seq) + +SafeConstructor.add_constructor( + u'tag:yaml.org,2002:map', + SafeConstructor.construct_yaml_map) + +SafeConstructor.add_constructor(None, + SafeConstructor.construct_undefined) + +class Constructor(SafeConstructor): + + def construct_python_str(self, node): + return self.construct_scalar(node).encode('utf-8') + + def construct_python_unicode(self, node): + return self.construct_scalar(node) + + def construct_python_long(self, node): + return long(self.construct_yaml_int(node)) + + def construct_python_complex(self, node): + return complex(self.construct_scalar(node)) + + def construct_python_tuple(self, node): + return tuple(self.construct_sequence(node)) + + def find_python_module(self, name, mark): + if not name: + raise ConstructorError("while constructing a Python module", mark, + "expected non-empty name appended to the tag", mark) + try: + __import__(name) + except ImportError, exc: + raise ConstructorError("while constructing a Python module", mark, + "cannot find module %r (%s)" % (name.encode('utf-8'), exc), mark) + return sys.modules[name] + + def find_python_name(self, name, mark): + if not name: + raise ConstructorError("while constructing a Python object", mark, + "expected non-empty name appended to the tag", mark) + if u'.' in name: + module_name, object_name = name.rsplit('.', 1) + else: + module_name = '__builtin__' + object_name = name + try: + __import__(module_name) + except ImportError, exc: + raise ConstructorError("while constructing a Python object", mark, + "cannot find module %r (%s)" % (module_name.encode('utf-8'), exc), mark) + module = sys.modules[module_name] + if not hasattr(module, object_name): + raise ConstructorError("while constructing a Python object", mark, + "cannot find %r in the module %r" % (object_name.encode('utf-8'), + module.__name__), mark) + return getattr(module, object_name) + + def construct_python_name(self, suffix, node): + value = self.construct_scalar(node) + if value: + raise ConstructorError("while constructing a Python name", node.start_mark, + "expected the empty value, but found %r" % value.encode('utf-8'), + node.start_mark) + return self.find_python_name(suffix, node.start_mark) + + def construct_python_module(self, suffix, node): + value = self.construct_scalar(node) + if value: + raise ConstructorError("while constructing a Python module", node.start_mark, + "expected the empty value, but found %r" % value.encode('utf-8'), + node.start_mark) + return self.find_python_module(suffix, node.start_mark) + + class classobj: pass + + def make_python_instance(self, suffix, node, + args=None, kwds=None, newobj=False): + if not args: + args = [] + if not kwds: + kwds = {} + cls = self.find_python_name(suffix, node.start_mark) + if newobj and isinstance(cls, type(self.classobj)) \ + and not args and not kwds: + instance = self.classobj() + instance.__class__ = cls + return instance + elif newobj and isinstance(cls, type): + return cls.__new__(cls, *args, **kwds) + else: + return cls(*args, **kwds) + + def set_python_instance_state(self, instance, state): + if hasattr(instance, '__setstate__'): + instance.__setstate__(state) + else: + slotstate = {} + if isinstance(state, tuple) and len(state) == 2: + state, slotstate = state + if hasattr(instance, '__dict__'): + instance.__dict__.update(state) + elif state: + slotstate.update(state) + for key, value in slotstate.items(): + setattr(object, key, value) + + def construct_python_object(self, suffix, node): + # Format: + # !!python/object:module.name { ... state ... } + instance = self.make_python_instance(suffix, node, newobj=True) + yield instance + deep = hasattr(instance, '__setstate__') + state = self.construct_mapping(node, deep=deep) + self.set_python_instance_state(instance, state) + + def construct_python_object_apply(self, suffix, node, newobj=False): + # Format: + # !!python/object/apply # (or !!python/object/new) + # args: [ ... arguments ... ] + # kwds: { ... keywords ... } + # state: ... state ... + # listitems: [ ... listitems ... ] + # dictitems: { ... dictitems ... } + # or short format: + # !!python/object/apply [ ... arguments ... ] + # The difference between !!python/object/apply and !!python/object/new + # is how an object is created, check make_python_instance for details. + if isinstance(node, SequenceNode): + args = self.construct_sequence(node, deep=True) + kwds = {} + state = {} + listitems = [] + dictitems = {} + else: + value = self.construct_mapping(node, deep=True) + args = value.get('args', []) + kwds = value.get('kwds', {}) + state = value.get('state', {}) + listitems = value.get('listitems', []) + dictitems = value.get('dictitems', {}) + instance = self.make_python_instance(suffix, node, args, kwds, newobj) + if state: + self.set_python_instance_state(instance, state) + if listitems: + instance.extend(listitems) + if dictitems: + for key in dictitems: + instance[key] = dictitems[key] + return instance + + def construct_python_object_new(self, suffix, node): + return self.construct_python_object_apply(suffix, node, newobj=True) + +Constructor.add_constructor( + u'tag:yaml.org,2002:python/none', + Constructor.construct_yaml_null) + +Constructor.add_constructor( + u'tag:yaml.org,2002:python/bool', + Constructor.construct_yaml_bool) + +Constructor.add_constructor( + u'tag:yaml.org,2002:python/str', + Constructor.construct_python_str) + +Constructor.add_constructor( + u'tag:yaml.org,2002:python/unicode', + Constructor.construct_python_unicode) + +Constructor.add_constructor( + u'tag:yaml.org,2002:python/int', + Constructor.construct_yaml_int) + +Constructor.add_constructor( + u'tag:yaml.org,2002:python/long', + Constructor.construct_python_long) + +Constructor.add_constructor( + u'tag:yaml.org,2002:python/float', + Constructor.construct_yaml_float) + +Constructor.add_constructor( + u'tag:yaml.org,2002:python/complex', + Constructor.construct_python_complex) + +Constructor.add_constructor( + u'tag:yaml.org,2002:python/list', + Constructor.construct_yaml_seq) + +Constructor.add_constructor( + u'tag:yaml.org,2002:python/tuple', + Constructor.construct_python_tuple) + +Constructor.add_constructor( + u'tag:yaml.org,2002:python/dict', + Constructor.construct_yaml_map) + +Constructor.add_multi_constructor( + u'tag:yaml.org,2002:python/name:', + Constructor.construct_python_name) + +Constructor.add_multi_constructor( + u'tag:yaml.org,2002:python/module:', + Constructor.construct_python_module) + +Constructor.add_multi_constructor( + u'tag:yaml.org,2002:python/object:', + Constructor.construct_python_object) + +Constructor.add_multi_constructor( + u'tag:yaml.org,2002:python/object/apply:', + Constructor.construct_python_object_apply) + +Constructor.add_multi_constructor( + u'tag:yaml.org,2002:python/object/new:', + Constructor.construct_python_object_new) + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py2/cyaml.py b/RenderPipelineFile/rplibs/yaml/yaml_py2/cyaml.py index 68dcd751..e726baf1 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py2/cyaml.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py2/cyaml.py @@ -1,85 +1,85 @@ - -__all__ = ['CBaseLoader', 'CSafeLoader', 'CLoader', - 'CBaseDumper', 'CSafeDumper', 'CDumper'] - -from _yaml import CParser, CEmitter - -from constructor import * - -from serializer import * -from representer import * - -from resolver import * - -class CBaseLoader(CParser, BaseConstructor, BaseResolver): - - def __init__(self, stream): - CParser.__init__(self, stream) - BaseConstructor.__init__(self) - BaseResolver.__init__(self) - -class CSafeLoader(CParser, SafeConstructor, Resolver): - - def __init__(self, stream): - CParser.__init__(self, stream) - SafeConstructor.__init__(self) - Resolver.__init__(self) - -class CLoader(CParser, Constructor, Resolver): - - def __init__(self, stream): - CParser.__init__(self, stream) - Constructor.__init__(self) - Resolver.__init__(self) - -class CBaseDumper(CEmitter, BaseRepresenter, BaseResolver): - - def __init__(self, stream, - default_style=None, default_flow_style=None, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None): - CEmitter.__init__(self, stream, canonical=canonical, - indent=indent, width=width, encoding=encoding, - allow_unicode=allow_unicode, line_break=line_break, - explicit_start=explicit_start, explicit_end=explicit_end, - version=version, tags=tags) - Representer.__init__(self, default_style=default_style, - default_flow_style=default_flow_style) - Resolver.__init__(self) - -class CSafeDumper(CEmitter, SafeRepresenter, Resolver): - - def __init__(self, stream, - default_style=None, default_flow_style=None, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None): - CEmitter.__init__(self, stream, canonical=canonical, - indent=indent, width=width, encoding=encoding, - allow_unicode=allow_unicode, line_break=line_break, - explicit_start=explicit_start, explicit_end=explicit_end, - version=version, tags=tags) - SafeRepresenter.__init__(self, default_style=default_style, - default_flow_style=default_flow_style) - Resolver.__init__(self) - -class CDumper(CEmitter, Serializer, Representer, Resolver): - - def __init__(self, stream, - default_style=None, default_flow_style=None, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None): - CEmitter.__init__(self, stream, canonical=canonical, - indent=indent, width=width, encoding=encoding, - allow_unicode=allow_unicode, line_break=line_break, - explicit_start=explicit_start, explicit_end=explicit_end, - version=version, tags=tags) - Representer.__init__(self, default_style=default_style, - default_flow_style=default_flow_style) - Resolver.__init__(self) - + +__all__ = ['CBaseLoader', 'CSafeLoader', 'CLoader', + 'CBaseDumper', 'CSafeDumper', 'CDumper'] + +from _yaml import CParser, CEmitter + +from constructor import * + +from serializer import * +from representer import * + +from resolver import * + +class CBaseLoader(CParser, BaseConstructor, BaseResolver): + + def __init__(self, stream): + CParser.__init__(self, stream) + BaseConstructor.__init__(self) + BaseResolver.__init__(self) + +class CSafeLoader(CParser, SafeConstructor, Resolver): + + def __init__(self, stream): + CParser.__init__(self, stream) + SafeConstructor.__init__(self) + Resolver.__init__(self) + +class CLoader(CParser, Constructor, Resolver): + + def __init__(self, stream): + CParser.__init__(self, stream) + Constructor.__init__(self) + Resolver.__init__(self) + +class CBaseDumper(CEmitter, BaseRepresenter, BaseResolver): + + def __init__(self, stream, + default_style=None, default_flow_style=None, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None): + CEmitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, encoding=encoding, + allow_unicode=allow_unicode, line_break=line_break, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + Representer.__init__(self, default_style=default_style, + default_flow_style=default_flow_style) + Resolver.__init__(self) + +class CSafeDumper(CEmitter, SafeRepresenter, Resolver): + + def __init__(self, stream, + default_style=None, default_flow_style=None, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None): + CEmitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, encoding=encoding, + allow_unicode=allow_unicode, line_break=line_break, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + SafeRepresenter.__init__(self, default_style=default_style, + default_flow_style=default_flow_style) + Resolver.__init__(self) + +class CDumper(CEmitter, Serializer, Representer, Resolver): + + def __init__(self, stream, + default_style=None, default_flow_style=None, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None): + CEmitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, encoding=encoding, + allow_unicode=allow_unicode, line_break=line_break, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + Representer.__init__(self, default_style=default_style, + default_flow_style=default_flow_style) + Resolver.__init__(self) + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py2/dumper.py b/RenderPipelineFile/rplibs/yaml/yaml_py2/dumper.py index f811d2c9..dea6f367 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py2/dumper.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py2/dumper.py @@ -1,62 +1,62 @@ - -__all__ = ['BaseDumper', 'SafeDumper', 'Dumper'] - -from emitter import * -from serializer import * -from representer import * -from resolver import * - -class BaseDumper(Emitter, Serializer, BaseRepresenter, BaseResolver): - - def __init__(self, stream, - default_style=None, default_flow_style=None, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None): - Emitter.__init__(self, stream, canonical=canonical, - indent=indent, width=width, - allow_unicode=allow_unicode, line_break=line_break) - Serializer.__init__(self, encoding=encoding, - explicit_start=explicit_start, explicit_end=explicit_end, - version=version, tags=tags) - Representer.__init__(self, default_style=default_style, - default_flow_style=default_flow_style) - Resolver.__init__(self) - -class SafeDumper(Emitter, Serializer, SafeRepresenter, Resolver): - - def __init__(self, stream, - default_style=None, default_flow_style=None, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None): - Emitter.__init__(self, stream, canonical=canonical, - indent=indent, width=width, - allow_unicode=allow_unicode, line_break=line_break) - Serializer.__init__(self, encoding=encoding, - explicit_start=explicit_start, explicit_end=explicit_end, - version=version, tags=tags) - SafeRepresenter.__init__(self, default_style=default_style, - default_flow_style=default_flow_style) - Resolver.__init__(self) - -class Dumper(Emitter, Serializer, Representer, Resolver): - - def __init__(self, stream, - default_style=None, default_flow_style=None, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None): - Emitter.__init__(self, stream, canonical=canonical, - indent=indent, width=width, - allow_unicode=allow_unicode, line_break=line_break) - Serializer.__init__(self, encoding=encoding, - explicit_start=explicit_start, explicit_end=explicit_end, - version=version, tags=tags) - Representer.__init__(self, default_style=default_style, - default_flow_style=default_flow_style) - Resolver.__init__(self) - + +__all__ = ['BaseDumper', 'SafeDumper', 'Dumper'] + +from emitter import * +from serializer import * +from representer import * +from resolver import * + +class BaseDumper(Emitter, Serializer, BaseRepresenter, BaseResolver): + + def __init__(self, stream, + default_style=None, default_flow_style=None, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None): + Emitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break) + Serializer.__init__(self, encoding=encoding, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + Representer.__init__(self, default_style=default_style, + default_flow_style=default_flow_style) + Resolver.__init__(self) + +class SafeDumper(Emitter, Serializer, SafeRepresenter, Resolver): + + def __init__(self, stream, + default_style=None, default_flow_style=None, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None): + Emitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break) + Serializer.__init__(self, encoding=encoding, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + SafeRepresenter.__init__(self, default_style=default_style, + default_flow_style=default_flow_style) + Resolver.__init__(self) + +class Dumper(Emitter, Serializer, Representer, Resolver): + + def __init__(self, stream, + default_style=None, default_flow_style=None, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None): + Emitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break) + Serializer.__init__(self, encoding=encoding, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + Representer.__init__(self, default_style=default_style, + default_flow_style=default_flow_style) + Resolver.__init__(self) + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py2/emitter.py b/RenderPipelineFile/rplibs/yaml/yaml_py2/emitter.py index e5bcdccc..cd5fd66a 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py2/emitter.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py2/emitter.py @@ -1,1140 +1,1140 @@ - -# Emitter expects events obeying the following grammar: -# stream ::= STREAM-START document* STREAM-END -# document ::= DOCUMENT-START node DOCUMENT-END -# node ::= SCALAR | sequence | mapping -# sequence ::= SEQUENCE-START node* SEQUENCE-END -# mapping ::= MAPPING-START (node node)* MAPPING-END - -__all__ = ['Emitter', 'EmitterError'] - -from error import YAMLError -from events import * - -class EmitterError(YAMLError): - pass - -class ScalarAnalysis(object): - def __init__(self, scalar, empty, multiline, - allow_flow_plain, allow_block_plain, - allow_single_quoted, allow_double_quoted, - allow_block): - self.scalar = scalar - self.empty = empty - self.multiline = multiline - self.allow_flow_plain = allow_flow_plain - self.allow_block_plain = allow_block_plain - self.allow_single_quoted = allow_single_quoted - self.allow_double_quoted = allow_double_quoted - self.allow_block = allow_block - -class Emitter(object): - - DEFAULT_TAG_PREFIXES = { - u'!' : u'!', - u'tag:yaml.org,2002:' : u'!!', - } - - def __init__(self, stream, canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None): - - # The stream should have the methods `write` and possibly `flush`. - self.stream = stream - - # Encoding can be overriden by STREAM-START. - self.encoding = None - - # Emitter is a state machine with a stack of states to handle nested - # structures. - self.states = [] - self.state = self.expect_stream_start - - # Current event and the event queue. - self.events = [] - self.event = None - - # The current indentation level and the stack of previous indents. - self.indents = [] - self.indent = None - - # Flow level. - self.flow_level = 0 - - # Contexts. - self.root_context = False - self.sequence_context = False - self.mapping_context = False - self.simple_key_context = False - - # Characteristics of the last emitted character: - # - current position. - # - is it a whitespace? - # - is it an indention character - # (indentation space, '-', '?', or ':')? - self.line = 0 - self.column = 0 - self.whitespace = True - self.indention = True - - # Whether the document requires an explicit document indicator - self.open_ended = False - - # Formatting details. - self.canonical = canonical - self.allow_unicode = allow_unicode - self.best_indent = 2 - if indent and 1 < indent < 10: - self.best_indent = indent - self.best_width = 80 - if width and width > self.best_indent*2: - self.best_width = width - self.best_line_break = u'\n' - if line_break in [u'\r', u'\n', u'\r\n']: - self.best_line_break = line_break - - # Tag prefixes. - self.tag_prefixes = None - - # Prepared anchor and tag. - self.prepared_anchor = None - self.prepared_tag = None - - # Scalar analysis and style. - self.analysis = None - self.style = None - - def dispose(self): - # Reset the state attributes (to clear self-references) - self.states = [] - self.state = None - - def emit(self, event): - self.events.append(event) - while not self.need_more_events(): - self.event = self.events.pop(0) - self.state() - self.event = None - - # In some cases, we wait for a few next events before emitting. - - def need_more_events(self): - if not self.events: - return True - event = self.events[0] - if isinstance(event, DocumentStartEvent): - return self.need_events(1) - elif isinstance(event, SequenceStartEvent): - return self.need_events(2) - elif isinstance(event, MappingStartEvent): - return self.need_events(3) - else: - return False - - def need_events(self, count): - level = 0 - for event in self.events[1:]: - if isinstance(event, (DocumentStartEvent, CollectionStartEvent)): - level += 1 - elif isinstance(event, (DocumentEndEvent, CollectionEndEvent)): - level -= 1 - elif isinstance(event, StreamEndEvent): - level = -1 - if level < 0: - return False - return (len(self.events) < count+1) - - def increase_indent(self, flow=False, indentless=False): - self.indents.append(self.indent) - if self.indent is None: - if flow: - self.indent = self.best_indent - else: - self.indent = 0 - elif not indentless: - self.indent += self.best_indent - - # States. - - # Stream handlers. - - def expect_stream_start(self): - if isinstance(self.event, StreamStartEvent): - if self.event.encoding and not getattr(self.stream, 'encoding', None): - self.encoding = self.event.encoding - self.write_stream_start() - self.state = self.expect_first_document_start - else: - raise EmitterError("expected StreamStartEvent, but got %s" - % self.event) - - def expect_nothing(self): - raise EmitterError("expected nothing, but got %s" % self.event) - - # Document handlers. - - def expect_first_document_start(self): - return self.expect_document_start(first=True) - - def expect_document_start(self, first=False): - if isinstance(self.event, DocumentStartEvent): - if (self.event.version or self.event.tags) and self.open_ended: - self.write_indicator(u'...', True) - self.write_indent() - if self.event.version: - version_text = self.prepare_version(self.event.version) - self.write_version_directive(version_text) - self.tag_prefixes = self.DEFAULT_TAG_PREFIXES.copy() - if self.event.tags: - handles = self.event.tags.keys() - handles.sort() - for handle in handles: - prefix = self.event.tags[handle] - self.tag_prefixes[prefix] = handle - handle_text = self.prepare_tag_handle(handle) - prefix_text = self.prepare_tag_prefix(prefix) - self.write_tag_directive(handle_text, prefix_text) - implicit = (first and not self.event.explicit and not self.canonical - and not self.event.version and not self.event.tags - and not self.check_empty_document()) - if not implicit: - self.write_indent() - self.write_indicator(u'---', True) - if self.canonical: - self.write_indent() - self.state = self.expect_document_root - elif isinstance(self.event, StreamEndEvent): - if self.open_ended: - self.write_indicator(u'...', True) - self.write_indent() - self.write_stream_end() - self.state = self.expect_nothing - else: - raise EmitterError("expected DocumentStartEvent, but got %s" - % self.event) - - def expect_document_end(self): - if isinstance(self.event, DocumentEndEvent): - self.write_indent() - if self.event.explicit: - self.write_indicator(u'...', True) - self.write_indent() - self.flush_stream() - self.state = self.expect_document_start - else: - raise EmitterError("expected DocumentEndEvent, but got %s" - % self.event) - - def expect_document_root(self): - self.states.append(self.expect_document_end) - self.expect_node(root=True) - - # Node handlers. - - def expect_node(self, root=False, sequence=False, mapping=False, - simple_key=False): - self.root_context = root - self.sequence_context = sequence - self.mapping_context = mapping - self.simple_key_context = simple_key - if isinstance(self.event, AliasEvent): - self.expect_alias() - elif isinstance(self.event, (ScalarEvent, CollectionStartEvent)): - self.process_anchor(u'&') - self.process_tag() - if isinstance(self.event, ScalarEvent): - self.expect_scalar() - elif isinstance(self.event, SequenceStartEvent): - if self.flow_level or self.canonical or self.event.flow_style \ - or self.check_empty_sequence(): - self.expect_flow_sequence() - else: - self.expect_block_sequence() - elif isinstance(self.event, MappingStartEvent): - if self.flow_level or self.canonical or self.event.flow_style \ - or self.check_empty_mapping(): - self.expect_flow_mapping() - else: - self.expect_block_mapping() - else: - raise EmitterError("expected NodeEvent, but got %s" % self.event) - - def expect_alias(self): - if self.event.anchor is None: - raise EmitterError("anchor is not specified for alias") - self.process_anchor(u'*') - self.state = self.states.pop() - - def expect_scalar(self): - self.increase_indent(flow=True) - self.process_scalar() - self.indent = self.indents.pop() - self.state = self.states.pop() - - # Flow sequence handlers. - - def expect_flow_sequence(self): - self.write_indicator(u'[', True, whitespace=True) - self.flow_level += 1 - self.increase_indent(flow=True) - self.state = self.expect_first_flow_sequence_item - - def expect_first_flow_sequence_item(self): - if isinstance(self.event, SequenceEndEvent): - self.indent = self.indents.pop() - self.flow_level -= 1 - self.write_indicator(u']', False) - self.state = self.states.pop() - else: - if self.canonical or self.column > self.best_width: - self.write_indent() - self.states.append(self.expect_flow_sequence_item) - self.expect_node(sequence=True) - - def expect_flow_sequence_item(self): - if isinstance(self.event, SequenceEndEvent): - self.indent = self.indents.pop() - self.flow_level -= 1 - if self.canonical: - self.write_indicator(u',', False) - self.write_indent() - self.write_indicator(u']', False) - self.state = self.states.pop() - else: - self.write_indicator(u',', False) - if self.canonical or self.column > self.best_width: - self.write_indent() - self.states.append(self.expect_flow_sequence_item) - self.expect_node(sequence=True) - - # Flow mapping handlers. - - def expect_flow_mapping(self): - self.write_indicator(u'{', True, whitespace=True) - self.flow_level += 1 - self.increase_indent(flow=True) - self.state = self.expect_first_flow_mapping_key - - def expect_first_flow_mapping_key(self): - if isinstance(self.event, MappingEndEvent): - self.indent = self.indents.pop() - self.flow_level -= 1 - self.write_indicator(u'}', False) - self.state = self.states.pop() - else: - if self.canonical or self.column > self.best_width: - self.write_indent() - if not self.canonical and self.check_simple_key(): - self.states.append(self.expect_flow_mapping_simple_value) - self.expect_node(mapping=True, simple_key=True) - else: - self.write_indicator(u'?', True) - self.states.append(self.expect_flow_mapping_value) - self.expect_node(mapping=True) - - def expect_flow_mapping_key(self): - if isinstance(self.event, MappingEndEvent): - self.indent = self.indents.pop() - self.flow_level -= 1 - if self.canonical: - self.write_indicator(u',', False) - self.write_indent() - self.write_indicator(u'}', False) - self.state = self.states.pop() - else: - self.write_indicator(u',', False) - if self.canonical or self.column > self.best_width: - self.write_indent() - if not self.canonical and self.check_simple_key(): - self.states.append(self.expect_flow_mapping_simple_value) - self.expect_node(mapping=True, simple_key=True) - else: - self.write_indicator(u'?', True) - self.states.append(self.expect_flow_mapping_value) - self.expect_node(mapping=True) - - def expect_flow_mapping_simple_value(self): - self.write_indicator(u':', False) - self.states.append(self.expect_flow_mapping_key) - self.expect_node(mapping=True) - - def expect_flow_mapping_value(self): - if self.canonical or self.column > self.best_width: - self.write_indent() - self.write_indicator(u':', True) - self.states.append(self.expect_flow_mapping_key) - self.expect_node(mapping=True) - - # Block sequence handlers. - - def expect_block_sequence(self): - indentless = (self.mapping_context and not self.indention) - self.increase_indent(flow=False, indentless=indentless) - self.state = self.expect_first_block_sequence_item - - def expect_first_block_sequence_item(self): - return self.expect_block_sequence_item(first=True) - - def expect_block_sequence_item(self, first=False): - if not first and isinstance(self.event, SequenceEndEvent): - self.indent = self.indents.pop() - self.state = self.states.pop() - else: - self.write_indent() - self.write_indicator(u'-', True, indention=True) - self.states.append(self.expect_block_sequence_item) - self.expect_node(sequence=True) - - # Block mapping handlers. - - def expect_block_mapping(self): - self.increase_indent(flow=False) - self.state = self.expect_first_block_mapping_key - - def expect_first_block_mapping_key(self): - return self.expect_block_mapping_key(first=True) - - def expect_block_mapping_key(self, first=False): - if not first and isinstance(self.event, MappingEndEvent): - self.indent = self.indents.pop() - self.state = self.states.pop() - else: - self.write_indent() - if self.check_simple_key(): - self.states.append(self.expect_block_mapping_simple_value) - self.expect_node(mapping=True, simple_key=True) - else: - self.write_indicator(u'?', True, indention=True) - self.states.append(self.expect_block_mapping_value) - self.expect_node(mapping=True) - - def expect_block_mapping_simple_value(self): - self.write_indicator(u':', False) - self.states.append(self.expect_block_mapping_key) - self.expect_node(mapping=True) - - def expect_block_mapping_value(self): - self.write_indent() - self.write_indicator(u':', True, indention=True) - self.states.append(self.expect_block_mapping_key) - self.expect_node(mapping=True) - - # Checkers. - - def check_empty_sequence(self): - return (isinstance(self.event, SequenceStartEvent) and self.events - and isinstance(self.events[0], SequenceEndEvent)) - - def check_empty_mapping(self): - return (isinstance(self.event, MappingStartEvent) and self.events - and isinstance(self.events[0], MappingEndEvent)) - - def check_empty_document(self): - if not isinstance(self.event, DocumentStartEvent) or not self.events: - return False - event = self.events[0] - return (isinstance(event, ScalarEvent) and event.anchor is None - and event.tag is None and event.implicit and event.value == u'') - - def check_simple_key(self): - length = 0 - if isinstance(self.event, NodeEvent) and self.event.anchor is not None: - if self.prepared_anchor is None: - self.prepared_anchor = self.prepare_anchor(self.event.anchor) - length += len(self.prepared_anchor) - if isinstance(self.event, (ScalarEvent, CollectionStartEvent)) \ - and self.event.tag is not None: - if self.prepared_tag is None: - self.prepared_tag = self.prepare_tag(self.event.tag) - length += len(self.prepared_tag) - if isinstance(self.event, ScalarEvent): - if self.analysis is None: - self.analysis = self.analyze_scalar(self.event.value) - length += len(self.analysis.scalar) - return (length < 128 and (isinstance(self.event, AliasEvent) - or (isinstance(self.event, ScalarEvent) - and not self.analysis.empty and not self.analysis.multiline) - or self.check_empty_sequence() or self.check_empty_mapping())) - - # Anchor, Tag, and Scalar processors. - - def process_anchor(self, indicator): - if self.event.anchor is None: - self.prepared_anchor = None - return - if self.prepared_anchor is None: - self.prepared_anchor = self.prepare_anchor(self.event.anchor) - if self.prepared_anchor: - self.write_indicator(indicator+self.prepared_anchor, True) - self.prepared_anchor = None - - def process_tag(self): - tag = self.event.tag - if isinstance(self.event, ScalarEvent): - if self.style is None: - self.style = self.choose_scalar_style() - if ((not self.canonical or tag is None) and - ((self.style == '' and self.event.implicit[0]) - or (self.style != '' and self.event.implicit[1]))): - self.prepared_tag = None - return - if self.event.implicit[0] and tag is None: - tag = u'!' - self.prepared_tag = None - else: - if (not self.canonical or tag is None) and self.event.implicit: - self.prepared_tag = None - return - if tag is None: - raise EmitterError("tag is not specified") - if self.prepared_tag is None: - self.prepared_tag = self.prepare_tag(tag) - if self.prepared_tag: - self.write_indicator(self.prepared_tag, True) - self.prepared_tag = None - - def choose_scalar_style(self): - if self.analysis is None: - self.analysis = self.analyze_scalar(self.event.value) - if self.event.style == '"' or self.canonical: - return '"' - if not self.event.style and self.event.implicit[0]: - if (not (self.simple_key_context and - (self.analysis.empty or self.analysis.multiline)) - and (self.flow_level and self.analysis.allow_flow_plain - or (not self.flow_level and self.analysis.allow_block_plain))): - return '' - if self.event.style and self.event.style in '|>': - if (not self.flow_level and not self.simple_key_context - and self.analysis.allow_block): - return self.event.style - if not self.event.style or self.event.style == '\'': - if (self.analysis.allow_single_quoted and - not (self.simple_key_context and self.analysis.multiline)): - return '\'' - return '"' - - def process_scalar(self): - if self.analysis is None: - self.analysis = self.analyze_scalar(self.event.value) - if self.style is None: - self.style = self.choose_scalar_style() - split = (not self.simple_key_context) - #if self.analysis.multiline and split \ - # and (not self.style or self.style in '\'\"'): - # self.write_indent() - if self.style == '"': - self.write_double_quoted(self.analysis.scalar, split) - elif self.style == '\'': - self.write_single_quoted(self.analysis.scalar, split) - elif self.style == '>': - self.write_folded(self.analysis.scalar) - elif self.style == '|': - self.write_literal(self.analysis.scalar) - else: - self.write_plain(self.analysis.scalar, split) - self.analysis = None - self.style = None - - # Analyzers. - - def prepare_version(self, version): - major, minor = version - if major != 1: - raise EmitterError("unsupported YAML version: %d.%d" % (major, minor)) - return u'%d.%d' % (major, minor) - - def prepare_tag_handle(self, handle): - if not handle: - raise EmitterError("tag handle must not be empty") - if handle[0] != u'!' or handle[-1] != u'!': - raise EmitterError("tag handle must start and end with '!': %r" - % (handle.encode('utf-8'))) - for ch in handle[1:-1]: - if not (u'0' <= ch <= u'9' or u'A' <= ch <= u'Z' or u'a' <= ch <= u'z' \ - or ch in u'-_'): - raise EmitterError("invalid character %r in the tag handle: %r" - % (ch.encode('utf-8'), handle.encode('utf-8'))) - return handle - - def prepare_tag_prefix(self, prefix): - if not prefix: - raise EmitterError("tag prefix must not be empty") - chunks = [] - start = end = 0 - if prefix[0] == u'!': - end = 1 - while end < len(prefix): - ch = prefix[end] - if u'0' <= ch <= u'9' or u'A' <= ch <= u'Z' or u'a' <= ch <= u'z' \ - or ch in u'-;/?!:@&=+$,_.~*\'()[]': - end += 1 - else: - if start < end: - chunks.append(prefix[start:end]) - start = end = end+1 - data = ch.encode('utf-8') - for ch in data: - chunks.append(u'%%%02X' % ord(ch)) - if start < end: - chunks.append(prefix[start:end]) - return u''.join(chunks) - - def prepare_tag(self, tag): - if not tag: - raise EmitterError("tag must not be empty") - if tag == u'!': - return tag - handle = None - suffix = tag - prefixes = self.tag_prefixes.keys() - prefixes.sort() - for prefix in prefixes: - if tag.startswith(prefix) \ - and (prefix == u'!' or len(prefix) < len(tag)): - handle = self.tag_prefixes[prefix] - suffix = tag[len(prefix):] - chunks = [] - start = end = 0 - while end < len(suffix): - ch = suffix[end] - if u'0' <= ch <= u'9' or u'A' <= ch <= u'Z' or u'a' <= ch <= u'z' \ - or ch in u'-;/?:@&=+$,_.~*\'()[]' \ - or (ch == u'!' and handle != u'!'): - end += 1 - else: - if start < end: - chunks.append(suffix[start:end]) - start = end = end+1 - data = ch.encode('utf-8') - for ch in data: - chunks.append(u'%%%02X' % ord(ch)) - if start < end: - chunks.append(suffix[start:end]) - suffix_text = u''.join(chunks) - if handle: - return u'%s%s' % (handle, suffix_text) - else: - return u'!<%s>' % suffix_text - - def prepare_anchor(self, anchor): - if not anchor: - raise EmitterError("anchor must not be empty") - for ch in anchor: - if not (u'0' <= ch <= u'9' or u'A' <= ch <= u'Z' or u'a' <= ch <= u'z' \ - or ch in u'-_'): - raise EmitterError("invalid character %r in the anchor: %r" - % (ch.encode('utf-8'), anchor.encode('utf-8'))) - return anchor - - def analyze_scalar(self, scalar): - - # Empty scalar is a special case. - if not scalar: - return ScalarAnalysis(scalar=scalar, empty=True, multiline=False, - allow_flow_plain=False, allow_block_plain=True, - allow_single_quoted=True, allow_double_quoted=True, - allow_block=False) - - # Indicators and special characters. - block_indicators = False - flow_indicators = False - line_breaks = False - special_characters = False - - # Important whitespace combinations. - leading_space = False - leading_break = False - trailing_space = False - trailing_break = False - break_space = False - space_break = False - - # Check document indicators. - if scalar.startswith(u'---') or scalar.startswith(u'...'): - block_indicators = True - flow_indicators = True - - # First character or preceded by a whitespace. - preceeded_by_whitespace = True - - # Last character or followed by a whitespace. - followed_by_whitespace = (len(scalar) == 1 or - scalar[1] in u'\0 \t\r\n\x85\u2028\u2029') - - # The previous character is a space. - previous_space = False - - # The previous character is a break. - previous_break = False - - index = 0 - while index < len(scalar): - ch = scalar[index] - - # Check for indicators. - if index == 0: - # Leading indicators are special characters. - if ch in u'#,[]{}&*!|>\'\"%@`': - flow_indicators = True - block_indicators = True - if ch in u'?:': - flow_indicators = True - if followed_by_whitespace: - block_indicators = True - if ch == u'-' and followed_by_whitespace: - flow_indicators = True - block_indicators = True - else: - # Some indicators cannot appear within a scalar as well. - if ch in u',?[]{}': - flow_indicators = True - if ch == u':': - flow_indicators = True - if followed_by_whitespace: - block_indicators = True - if ch == u'#' and preceeded_by_whitespace: - flow_indicators = True - block_indicators = True - - # Check for line breaks, special, and unicode characters. - if ch in u'\n\x85\u2028\u2029': - line_breaks = True - if not (ch == u'\n' or u'\x20' <= ch <= u'\x7E'): - if (ch == u'\x85' or u'\xA0' <= ch <= u'\uD7FF' - or u'\uE000' <= ch <= u'\uFFFD') and ch != u'\uFEFF': - unicode_characters = True - if not self.allow_unicode: - special_characters = True - else: - special_characters = True - - # Detect important whitespace combinations. - if ch == u' ': - if index == 0: - leading_space = True - if index == len(scalar)-1: - trailing_space = True - if previous_break: - break_space = True - previous_space = True - previous_break = False - elif ch in u'\n\x85\u2028\u2029': - if index == 0: - leading_break = True - if index == len(scalar)-1: - trailing_break = True - if previous_space: - space_break = True - previous_space = False - previous_break = True - else: - previous_space = False - previous_break = False - - # Prepare for the next character. - index += 1 - preceeded_by_whitespace = (ch in u'\0 \t\r\n\x85\u2028\u2029') - followed_by_whitespace = (index+1 >= len(scalar) or - scalar[index+1] in u'\0 \t\r\n\x85\u2028\u2029') - - # Let's decide what styles are allowed. - allow_flow_plain = True - allow_block_plain = True - allow_single_quoted = True - allow_double_quoted = True - allow_block = True - - # Leading and trailing whitespaces are bad for plain scalars. - if (leading_space or leading_break - or trailing_space or trailing_break): - allow_flow_plain = allow_block_plain = False - - # We do not permit trailing spaces for block scalars. - if trailing_space: - allow_block = False - - # Spaces at the beginning of a new line are only acceptable for block - # scalars. - if break_space: - allow_flow_plain = allow_block_plain = allow_single_quoted = False - - # Spaces followed by breaks, as well as special character are only - # allowed for double quoted scalars. - if space_break or special_characters: - allow_flow_plain = allow_block_plain = \ - allow_single_quoted = allow_block = False - - # Although the plain scalar writer supports breaks, we never emit - # multiline plain scalars. - if line_breaks: - allow_flow_plain = allow_block_plain = False - - # Flow indicators are forbidden for flow plain scalars. - if flow_indicators: - allow_flow_plain = False - - # Block indicators are forbidden for block plain scalars. - if block_indicators: - allow_block_plain = False - - return ScalarAnalysis(scalar=scalar, - empty=False, multiline=line_breaks, - allow_flow_plain=allow_flow_plain, - allow_block_plain=allow_block_plain, - allow_single_quoted=allow_single_quoted, - allow_double_quoted=allow_double_quoted, - allow_block=allow_block) - - # Writers. - - def flush_stream(self): - if hasattr(self.stream, 'flush'): - self.stream.flush() - - def write_stream_start(self): - # Write BOM if needed. - if self.encoding and self.encoding.startswith('utf-16'): - self.stream.write(u'\uFEFF'.encode(self.encoding)) - - def write_stream_end(self): - self.flush_stream() - - def write_indicator(self, indicator, need_whitespace, - whitespace=False, indention=False): - if self.whitespace or not need_whitespace: - data = indicator - else: - data = u' '+indicator - self.whitespace = whitespace - self.indention = self.indention and indention - self.column += len(data) - self.open_ended = False - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - - def write_indent(self): - indent = self.indent or 0 - if not self.indention or self.column > indent \ - or (self.column == indent and not self.whitespace): - self.write_line_break() - if self.column < indent: - self.whitespace = True - data = u' '*(indent-self.column) - self.column = indent - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - - def write_line_break(self, data=None): - if data is None: - data = self.best_line_break - self.whitespace = True - self.indention = True - self.line += 1 - self.column = 0 - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - - def write_version_directive(self, version_text): - data = u'%%YAML %s' % version_text - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - self.write_line_break() - - def write_tag_directive(self, handle_text, prefix_text): - data = u'%%TAG %s %s' % (handle_text, prefix_text) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - self.write_line_break() - - # Scalar streams. - - def write_single_quoted(self, text, split=True): - self.write_indicator(u'\'', True) - spaces = False - breaks = False - start = end = 0 - while end <= len(text): - ch = None - if end < len(text): - ch = text[end] - if spaces: - if ch is None or ch != u' ': - if start+1 == end and self.column > self.best_width and split \ - and start != 0 and end != len(text): - self.write_indent() - else: - data = text[start:end] - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - start = end - elif breaks: - if ch is None or ch not in u'\n\x85\u2028\u2029': - if text[start] == u'\n': - self.write_line_break() - for br in text[start:end]: - if br == u'\n': - self.write_line_break() - else: - self.write_line_break(br) - self.write_indent() - start = end - else: - if ch is None or ch in u' \n\x85\u2028\u2029' or ch == u'\'': - if start < end: - data = text[start:end] - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - start = end - if ch == u'\'': - data = u'\'\'' - self.column += 2 - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - start = end + 1 - if ch is not None: - spaces = (ch == u' ') - breaks = (ch in u'\n\x85\u2028\u2029') - end += 1 - self.write_indicator(u'\'', False) - - ESCAPE_REPLACEMENTS = { - u'\0': u'0', - u'\x07': u'a', - u'\x08': u'b', - u'\x09': u't', - u'\x0A': u'n', - u'\x0B': u'v', - u'\x0C': u'f', - u'\x0D': u'r', - u'\x1B': u'e', - u'\"': u'\"', - u'\\': u'\\', - u'\x85': u'N', - u'\xA0': u'_', - u'\u2028': u'L', - u'\u2029': u'P', - } - - def write_double_quoted(self, text, split=True): - self.write_indicator(u'"', True) - start = end = 0 - while end <= len(text): - ch = None - if end < len(text): - ch = text[end] - if ch is None or ch in u'"\\\x85\u2028\u2029\uFEFF' \ - or not (u'\x20' <= ch <= u'\x7E' - or (self.allow_unicode - and (u'\xA0' <= ch <= u'\uD7FF' - or u'\uE000' <= ch <= u'\uFFFD'))): - if start < end: - data = text[start:end] - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - start = end - if ch is not None: - if ch in self.ESCAPE_REPLACEMENTS: - data = u'\\'+self.ESCAPE_REPLACEMENTS[ch] - elif ch <= u'\xFF': - data = u'\\x%02X' % ord(ch) - elif ch <= u'\uFFFF': - data = u'\\u%04X' % ord(ch) - else: - data = u'\\U%08X' % ord(ch) - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - start = end+1 - if 0 < end < len(text)-1 and (ch == u' ' or start >= end) \ - and self.column+(end-start) > self.best_width and split: - data = text[start:end]+u'\\' - if start < end: - start = end - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - self.write_indent() - self.whitespace = False - self.indention = False - if text[start] == u' ': - data = u'\\' - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - end += 1 - self.write_indicator(u'"', False) - - def determine_block_hints(self, text): - hints = u'' - if text: - if text[0] in u' \n\x85\u2028\u2029': - hints += unicode(self.best_indent) - if text[-1] not in u'\n\x85\u2028\u2029': - hints += u'-' - elif len(text) == 1 or text[-2] in u'\n\x85\u2028\u2029': - hints += u'+' - return hints - - def write_folded(self, text): - hints = self.determine_block_hints(text) - self.write_indicator(u'>'+hints, True) - if hints[-1:] == u'+': - self.open_ended = True - self.write_line_break() - leading_space = True - spaces = False - breaks = True - start = end = 0 - while end <= len(text): - ch = None - if end < len(text): - ch = text[end] - if breaks: - if ch is None or ch not in u'\n\x85\u2028\u2029': - if not leading_space and ch is not None and ch != u' ' \ - and text[start] == u'\n': - self.write_line_break() - leading_space = (ch == u' ') - for br in text[start:end]: - if br == u'\n': - self.write_line_break() - else: - self.write_line_break(br) - if ch is not None: - self.write_indent() - start = end - elif spaces: - if ch != u' ': - if start+1 == end and self.column > self.best_width: - self.write_indent() - else: - data = text[start:end] - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - start = end - else: - if ch is None or ch in u' \n\x85\u2028\u2029': - data = text[start:end] - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - if ch is None: - self.write_line_break() - start = end - if ch is not None: - breaks = (ch in u'\n\x85\u2028\u2029') - spaces = (ch == u' ') - end += 1 - - def write_literal(self, text): - hints = self.determine_block_hints(text) - self.write_indicator(u'|'+hints, True) - if hints[-1:] == u'+': - self.open_ended = True - self.write_line_break() - breaks = True - start = end = 0 - while end <= len(text): - ch = None - if end < len(text): - ch = text[end] - if breaks: - if ch is None or ch not in u'\n\x85\u2028\u2029': - for br in text[start:end]: - if br == u'\n': - self.write_line_break() - else: - self.write_line_break(br) - if ch is not None: - self.write_indent() - start = end - else: - if ch is None or ch in u'\n\x85\u2028\u2029': - data = text[start:end] - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - if ch is None: - self.write_line_break() - start = end - if ch is not None: - breaks = (ch in u'\n\x85\u2028\u2029') - end += 1 - - def write_plain(self, text, split=True): - if self.root_context: - self.open_ended = True - if not text: - return - if not self.whitespace: - data = u' ' - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - self.whitespace = False - self.indention = False - spaces = False - breaks = False - start = end = 0 - while end <= len(text): - ch = None - if end < len(text): - ch = text[end] - if spaces: - if ch != u' ': - if start+1 == end and self.column > self.best_width and split: - self.write_indent() - self.whitespace = False - self.indention = False - else: - data = text[start:end] - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - start = end - elif breaks: - if ch not in u'\n\x85\u2028\u2029': - if text[start] == u'\n': - self.write_line_break() - for br in text[start:end]: - if br == u'\n': - self.write_line_break() - else: - self.write_line_break(br) - self.write_indent() - self.whitespace = False - self.indention = False - start = end - else: - if ch is None or ch in u' \n\x85\u2028\u2029': - data = text[start:end] - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - start = end - if ch is not None: - spaces = (ch == u' ') - breaks = (ch in u'\n\x85\u2028\u2029') - end += 1 - + +# Emitter expects events obeying the following grammar: +# stream ::= STREAM-START document* STREAM-END +# document ::= DOCUMENT-START node DOCUMENT-END +# node ::= SCALAR | sequence | mapping +# sequence ::= SEQUENCE-START node* SEQUENCE-END +# mapping ::= MAPPING-START (node node)* MAPPING-END + +__all__ = ['Emitter', 'EmitterError'] + +from error import YAMLError +from events import * + +class EmitterError(YAMLError): + pass + +class ScalarAnalysis(object): + def __init__(self, scalar, empty, multiline, + allow_flow_plain, allow_block_plain, + allow_single_quoted, allow_double_quoted, + allow_block): + self.scalar = scalar + self.empty = empty + self.multiline = multiline + self.allow_flow_plain = allow_flow_plain + self.allow_block_plain = allow_block_plain + self.allow_single_quoted = allow_single_quoted + self.allow_double_quoted = allow_double_quoted + self.allow_block = allow_block + +class Emitter(object): + + DEFAULT_TAG_PREFIXES = { + u'!' : u'!', + u'tag:yaml.org,2002:' : u'!!', + } + + def __init__(self, stream, canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None): + + # The stream should have the methods `write` and possibly `flush`. + self.stream = stream + + # Encoding can be overriden by STREAM-START. + self.encoding = None + + # Emitter is a state machine with a stack of states to handle nested + # structures. + self.states = [] + self.state = self.expect_stream_start + + # Current event and the event queue. + self.events = [] + self.event = None + + # The current indentation level and the stack of previous indents. + self.indents = [] + self.indent = None + + # Flow level. + self.flow_level = 0 + + # Contexts. + self.root_context = False + self.sequence_context = False + self.mapping_context = False + self.simple_key_context = False + + # Characteristics of the last emitted character: + # - current position. + # - is it a whitespace? + # - is it an indention character + # (indentation space, '-', '?', or ':')? + self.line = 0 + self.column = 0 + self.whitespace = True + self.indention = True + + # Whether the document requires an explicit document indicator + self.open_ended = False + + # Formatting details. + self.canonical = canonical + self.allow_unicode = allow_unicode + self.best_indent = 2 + if indent and 1 < indent < 10: + self.best_indent = indent + self.best_width = 80 + if width and width > self.best_indent*2: + self.best_width = width + self.best_line_break = u'\n' + if line_break in [u'\r', u'\n', u'\r\n']: + self.best_line_break = line_break + + # Tag prefixes. + self.tag_prefixes = None + + # Prepared anchor and tag. + self.prepared_anchor = None + self.prepared_tag = None + + # Scalar analysis and style. + self.analysis = None + self.style = None + + def dispose(self): + # Reset the state attributes (to clear self-references) + self.states = [] + self.state = None + + def emit(self, event): + self.events.append(event) + while not self.need_more_events(): + self.event = self.events.pop(0) + self.state() + self.event = None + + # In some cases, we wait for a few next events before emitting. + + def need_more_events(self): + if not self.events: + return True + event = self.events[0] + if isinstance(event, DocumentStartEvent): + return self.need_events(1) + elif isinstance(event, SequenceStartEvent): + return self.need_events(2) + elif isinstance(event, MappingStartEvent): + return self.need_events(3) + else: + return False + + def need_events(self, count): + level = 0 + for event in self.events[1:]: + if isinstance(event, (DocumentStartEvent, CollectionStartEvent)): + level += 1 + elif isinstance(event, (DocumentEndEvent, CollectionEndEvent)): + level -= 1 + elif isinstance(event, StreamEndEvent): + level = -1 + if level < 0: + return False + return (len(self.events) < count+1) + + def increase_indent(self, flow=False, indentless=False): + self.indents.append(self.indent) + if self.indent is None: + if flow: + self.indent = self.best_indent + else: + self.indent = 0 + elif not indentless: + self.indent += self.best_indent + + # States. + + # Stream handlers. + + def expect_stream_start(self): + if isinstance(self.event, StreamStartEvent): + if self.event.encoding and not getattr(self.stream, 'encoding', None): + self.encoding = self.event.encoding + self.write_stream_start() + self.state = self.expect_first_document_start + else: + raise EmitterError("expected StreamStartEvent, but got %s" + % self.event) + + def expect_nothing(self): + raise EmitterError("expected nothing, but got %s" % self.event) + + # Document handlers. + + def expect_first_document_start(self): + return self.expect_document_start(first=True) + + def expect_document_start(self, first=False): + if isinstance(self.event, DocumentStartEvent): + if (self.event.version or self.event.tags) and self.open_ended: + self.write_indicator(u'...', True) + self.write_indent() + if self.event.version: + version_text = self.prepare_version(self.event.version) + self.write_version_directive(version_text) + self.tag_prefixes = self.DEFAULT_TAG_PREFIXES.copy() + if self.event.tags: + handles = self.event.tags.keys() + handles.sort() + for handle in handles: + prefix = self.event.tags[handle] + self.tag_prefixes[prefix] = handle + handle_text = self.prepare_tag_handle(handle) + prefix_text = self.prepare_tag_prefix(prefix) + self.write_tag_directive(handle_text, prefix_text) + implicit = (first and not self.event.explicit and not self.canonical + and not self.event.version and not self.event.tags + and not self.check_empty_document()) + if not implicit: + self.write_indent() + self.write_indicator(u'---', True) + if self.canonical: + self.write_indent() + self.state = self.expect_document_root + elif isinstance(self.event, StreamEndEvent): + if self.open_ended: + self.write_indicator(u'...', True) + self.write_indent() + self.write_stream_end() + self.state = self.expect_nothing + else: + raise EmitterError("expected DocumentStartEvent, but got %s" + % self.event) + + def expect_document_end(self): + if isinstance(self.event, DocumentEndEvent): + self.write_indent() + if self.event.explicit: + self.write_indicator(u'...', True) + self.write_indent() + self.flush_stream() + self.state = self.expect_document_start + else: + raise EmitterError("expected DocumentEndEvent, but got %s" + % self.event) + + def expect_document_root(self): + self.states.append(self.expect_document_end) + self.expect_node(root=True) + + # Node handlers. + + def expect_node(self, root=False, sequence=False, mapping=False, + simple_key=False): + self.root_context = root + self.sequence_context = sequence + self.mapping_context = mapping + self.simple_key_context = simple_key + if isinstance(self.event, AliasEvent): + self.expect_alias() + elif isinstance(self.event, (ScalarEvent, CollectionStartEvent)): + self.process_anchor(u'&') + self.process_tag() + if isinstance(self.event, ScalarEvent): + self.expect_scalar() + elif isinstance(self.event, SequenceStartEvent): + if self.flow_level or self.canonical or self.event.flow_style \ + or self.check_empty_sequence(): + self.expect_flow_sequence() + else: + self.expect_block_sequence() + elif isinstance(self.event, MappingStartEvent): + if self.flow_level or self.canonical or self.event.flow_style \ + or self.check_empty_mapping(): + self.expect_flow_mapping() + else: + self.expect_block_mapping() + else: + raise EmitterError("expected NodeEvent, but got %s" % self.event) + + def expect_alias(self): + if self.event.anchor is None: + raise EmitterError("anchor is not specified for alias") + self.process_anchor(u'*') + self.state = self.states.pop() + + def expect_scalar(self): + self.increase_indent(flow=True) + self.process_scalar() + self.indent = self.indents.pop() + self.state = self.states.pop() + + # Flow sequence handlers. + + def expect_flow_sequence(self): + self.write_indicator(u'[', True, whitespace=True) + self.flow_level += 1 + self.increase_indent(flow=True) + self.state = self.expect_first_flow_sequence_item + + def expect_first_flow_sequence_item(self): + if isinstance(self.event, SequenceEndEvent): + self.indent = self.indents.pop() + self.flow_level -= 1 + self.write_indicator(u']', False) + self.state = self.states.pop() + else: + if self.canonical or self.column > self.best_width: + self.write_indent() + self.states.append(self.expect_flow_sequence_item) + self.expect_node(sequence=True) + + def expect_flow_sequence_item(self): + if isinstance(self.event, SequenceEndEvent): + self.indent = self.indents.pop() + self.flow_level -= 1 + if self.canonical: + self.write_indicator(u',', False) + self.write_indent() + self.write_indicator(u']', False) + self.state = self.states.pop() + else: + self.write_indicator(u',', False) + if self.canonical or self.column > self.best_width: + self.write_indent() + self.states.append(self.expect_flow_sequence_item) + self.expect_node(sequence=True) + + # Flow mapping handlers. + + def expect_flow_mapping(self): + self.write_indicator(u'{', True, whitespace=True) + self.flow_level += 1 + self.increase_indent(flow=True) + self.state = self.expect_first_flow_mapping_key + + def expect_first_flow_mapping_key(self): + if isinstance(self.event, MappingEndEvent): + self.indent = self.indents.pop() + self.flow_level -= 1 + self.write_indicator(u'}', False) + self.state = self.states.pop() + else: + if self.canonical or self.column > self.best_width: + self.write_indent() + if not self.canonical and self.check_simple_key(): + self.states.append(self.expect_flow_mapping_simple_value) + self.expect_node(mapping=True, simple_key=True) + else: + self.write_indicator(u'?', True) + self.states.append(self.expect_flow_mapping_value) + self.expect_node(mapping=True) + + def expect_flow_mapping_key(self): + if isinstance(self.event, MappingEndEvent): + self.indent = self.indents.pop() + self.flow_level -= 1 + if self.canonical: + self.write_indicator(u',', False) + self.write_indent() + self.write_indicator(u'}', False) + self.state = self.states.pop() + else: + self.write_indicator(u',', False) + if self.canonical or self.column > self.best_width: + self.write_indent() + if not self.canonical and self.check_simple_key(): + self.states.append(self.expect_flow_mapping_simple_value) + self.expect_node(mapping=True, simple_key=True) + else: + self.write_indicator(u'?', True) + self.states.append(self.expect_flow_mapping_value) + self.expect_node(mapping=True) + + def expect_flow_mapping_simple_value(self): + self.write_indicator(u':', False) + self.states.append(self.expect_flow_mapping_key) + self.expect_node(mapping=True) + + def expect_flow_mapping_value(self): + if self.canonical or self.column > self.best_width: + self.write_indent() + self.write_indicator(u':', True) + self.states.append(self.expect_flow_mapping_key) + self.expect_node(mapping=True) + + # Block sequence handlers. + + def expect_block_sequence(self): + indentless = (self.mapping_context and not self.indention) + self.increase_indent(flow=False, indentless=indentless) + self.state = self.expect_first_block_sequence_item + + def expect_first_block_sequence_item(self): + return self.expect_block_sequence_item(first=True) + + def expect_block_sequence_item(self, first=False): + if not first and isinstance(self.event, SequenceEndEvent): + self.indent = self.indents.pop() + self.state = self.states.pop() + else: + self.write_indent() + self.write_indicator(u'-', True, indention=True) + self.states.append(self.expect_block_sequence_item) + self.expect_node(sequence=True) + + # Block mapping handlers. + + def expect_block_mapping(self): + self.increase_indent(flow=False) + self.state = self.expect_first_block_mapping_key + + def expect_first_block_mapping_key(self): + return self.expect_block_mapping_key(first=True) + + def expect_block_mapping_key(self, first=False): + if not first and isinstance(self.event, MappingEndEvent): + self.indent = self.indents.pop() + self.state = self.states.pop() + else: + self.write_indent() + if self.check_simple_key(): + self.states.append(self.expect_block_mapping_simple_value) + self.expect_node(mapping=True, simple_key=True) + else: + self.write_indicator(u'?', True, indention=True) + self.states.append(self.expect_block_mapping_value) + self.expect_node(mapping=True) + + def expect_block_mapping_simple_value(self): + self.write_indicator(u':', False) + self.states.append(self.expect_block_mapping_key) + self.expect_node(mapping=True) + + def expect_block_mapping_value(self): + self.write_indent() + self.write_indicator(u':', True, indention=True) + self.states.append(self.expect_block_mapping_key) + self.expect_node(mapping=True) + + # Checkers. + + def check_empty_sequence(self): + return (isinstance(self.event, SequenceStartEvent) and self.events + and isinstance(self.events[0], SequenceEndEvent)) + + def check_empty_mapping(self): + return (isinstance(self.event, MappingStartEvent) and self.events + and isinstance(self.events[0], MappingEndEvent)) + + def check_empty_document(self): + if not isinstance(self.event, DocumentStartEvent) or not self.events: + return False + event = self.events[0] + return (isinstance(event, ScalarEvent) and event.anchor is None + and event.tag is None and event.implicit and event.value == u'') + + def check_simple_key(self): + length = 0 + if isinstance(self.event, NodeEvent) and self.event.anchor is not None: + if self.prepared_anchor is None: + self.prepared_anchor = self.prepare_anchor(self.event.anchor) + length += len(self.prepared_anchor) + if isinstance(self.event, (ScalarEvent, CollectionStartEvent)) \ + and self.event.tag is not None: + if self.prepared_tag is None: + self.prepared_tag = self.prepare_tag(self.event.tag) + length += len(self.prepared_tag) + if isinstance(self.event, ScalarEvent): + if self.analysis is None: + self.analysis = self.analyze_scalar(self.event.value) + length += len(self.analysis.scalar) + return (length < 128 and (isinstance(self.event, AliasEvent) + or (isinstance(self.event, ScalarEvent) + and not self.analysis.empty and not self.analysis.multiline) + or self.check_empty_sequence() or self.check_empty_mapping())) + + # Anchor, Tag, and Scalar processors. + + def process_anchor(self, indicator): + if self.event.anchor is None: + self.prepared_anchor = None + return + if self.prepared_anchor is None: + self.prepared_anchor = self.prepare_anchor(self.event.anchor) + if self.prepared_anchor: + self.write_indicator(indicator+self.prepared_anchor, True) + self.prepared_anchor = None + + def process_tag(self): + tag = self.event.tag + if isinstance(self.event, ScalarEvent): + if self.style is None: + self.style = self.choose_scalar_style() + if ((not self.canonical or tag is None) and + ((self.style == '' and self.event.implicit[0]) + or (self.style != '' and self.event.implicit[1]))): + self.prepared_tag = None + return + if self.event.implicit[0] and tag is None: + tag = u'!' + self.prepared_tag = None + else: + if (not self.canonical or tag is None) and self.event.implicit: + self.prepared_tag = None + return + if tag is None: + raise EmitterError("tag is not specified") + if self.prepared_tag is None: + self.prepared_tag = self.prepare_tag(tag) + if self.prepared_tag: + self.write_indicator(self.prepared_tag, True) + self.prepared_tag = None + + def choose_scalar_style(self): + if self.analysis is None: + self.analysis = self.analyze_scalar(self.event.value) + if self.event.style == '"' or self.canonical: + return '"' + if not self.event.style and self.event.implicit[0]: + if (not (self.simple_key_context and + (self.analysis.empty or self.analysis.multiline)) + and (self.flow_level and self.analysis.allow_flow_plain + or (not self.flow_level and self.analysis.allow_block_plain))): + return '' + if self.event.style and self.event.style in '|>': + if (not self.flow_level and not self.simple_key_context + and self.analysis.allow_block): + return self.event.style + if not self.event.style or self.event.style == '\'': + if (self.analysis.allow_single_quoted and + not (self.simple_key_context and self.analysis.multiline)): + return '\'' + return '"' + + def process_scalar(self): + if self.analysis is None: + self.analysis = self.analyze_scalar(self.event.value) + if self.style is None: + self.style = self.choose_scalar_style() + split = (not self.simple_key_context) + #if self.analysis.multiline and split \ + # and (not self.style or self.style in '\'\"'): + # self.write_indent() + if self.style == '"': + self.write_double_quoted(self.analysis.scalar, split) + elif self.style == '\'': + self.write_single_quoted(self.analysis.scalar, split) + elif self.style == '>': + self.write_folded(self.analysis.scalar) + elif self.style == '|': + self.write_literal(self.analysis.scalar) + else: + self.write_plain(self.analysis.scalar, split) + self.analysis = None + self.style = None + + # Analyzers. + + def prepare_version(self, version): + major, minor = version + if major != 1: + raise EmitterError("unsupported YAML version: %d.%d" % (major, minor)) + return u'%d.%d' % (major, minor) + + def prepare_tag_handle(self, handle): + if not handle: + raise EmitterError("tag handle must not be empty") + if handle[0] != u'!' or handle[-1] != u'!': + raise EmitterError("tag handle must start and end with '!': %r" + % (handle.encode('utf-8'))) + for ch in handle[1:-1]: + if not (u'0' <= ch <= u'9' or u'A' <= ch <= u'Z' or u'a' <= ch <= u'z' \ + or ch in u'-_'): + raise EmitterError("invalid character %r in the tag handle: %r" + % (ch.encode('utf-8'), handle.encode('utf-8'))) + return handle + + def prepare_tag_prefix(self, prefix): + if not prefix: + raise EmitterError("tag prefix must not be empty") + chunks = [] + start = end = 0 + if prefix[0] == u'!': + end = 1 + while end < len(prefix): + ch = prefix[end] + if u'0' <= ch <= u'9' or u'A' <= ch <= u'Z' or u'a' <= ch <= u'z' \ + or ch in u'-;/?!:@&=+$,_.~*\'()[]': + end += 1 + else: + if start < end: + chunks.append(prefix[start:end]) + start = end = end+1 + data = ch.encode('utf-8') + for ch in data: + chunks.append(u'%%%02X' % ord(ch)) + if start < end: + chunks.append(prefix[start:end]) + return u''.join(chunks) + + def prepare_tag(self, tag): + if not tag: + raise EmitterError("tag must not be empty") + if tag == u'!': + return tag + handle = None + suffix = tag + prefixes = self.tag_prefixes.keys() + prefixes.sort() + for prefix in prefixes: + if tag.startswith(prefix) \ + and (prefix == u'!' or len(prefix) < len(tag)): + handle = self.tag_prefixes[prefix] + suffix = tag[len(prefix):] + chunks = [] + start = end = 0 + while end < len(suffix): + ch = suffix[end] + if u'0' <= ch <= u'9' or u'A' <= ch <= u'Z' or u'a' <= ch <= u'z' \ + or ch in u'-;/?:@&=+$,_.~*\'()[]' \ + or (ch == u'!' and handle != u'!'): + end += 1 + else: + if start < end: + chunks.append(suffix[start:end]) + start = end = end+1 + data = ch.encode('utf-8') + for ch in data: + chunks.append(u'%%%02X' % ord(ch)) + if start < end: + chunks.append(suffix[start:end]) + suffix_text = u''.join(chunks) + if handle: + return u'%s%s' % (handle, suffix_text) + else: + return u'!<%s>' % suffix_text + + def prepare_anchor(self, anchor): + if not anchor: + raise EmitterError("anchor must not be empty") + for ch in anchor: + if not (u'0' <= ch <= u'9' or u'A' <= ch <= u'Z' or u'a' <= ch <= u'z' \ + or ch in u'-_'): + raise EmitterError("invalid character %r in the anchor: %r" + % (ch.encode('utf-8'), anchor.encode('utf-8'))) + return anchor + + def analyze_scalar(self, scalar): + + # Empty scalar is a special case. + if not scalar: + return ScalarAnalysis(scalar=scalar, empty=True, multiline=False, + allow_flow_plain=False, allow_block_plain=True, + allow_single_quoted=True, allow_double_quoted=True, + allow_block=False) + + # Indicators and special characters. + block_indicators = False + flow_indicators = False + line_breaks = False + special_characters = False + + # Important whitespace combinations. + leading_space = False + leading_break = False + trailing_space = False + trailing_break = False + break_space = False + space_break = False + + # Check document indicators. + if scalar.startswith(u'---') or scalar.startswith(u'...'): + block_indicators = True + flow_indicators = True + + # First character or preceded by a whitespace. + preceeded_by_whitespace = True + + # Last character or followed by a whitespace. + followed_by_whitespace = (len(scalar) == 1 or + scalar[1] in u'\0 \t\r\n\x85\u2028\u2029') + + # The previous character is a space. + previous_space = False + + # The previous character is a break. + previous_break = False + + index = 0 + while index < len(scalar): + ch = scalar[index] + + # Check for indicators. + if index == 0: + # Leading indicators are special characters. + if ch in u'#,[]{}&*!|>\'\"%@`': + flow_indicators = True + block_indicators = True + if ch in u'?:': + flow_indicators = True + if followed_by_whitespace: + block_indicators = True + if ch == u'-' and followed_by_whitespace: + flow_indicators = True + block_indicators = True + else: + # Some indicators cannot appear within a scalar as well. + if ch in u',?[]{}': + flow_indicators = True + if ch == u':': + flow_indicators = True + if followed_by_whitespace: + block_indicators = True + if ch == u'#' and preceeded_by_whitespace: + flow_indicators = True + block_indicators = True + + # Check for line breaks, special, and unicode characters. + if ch in u'\n\x85\u2028\u2029': + line_breaks = True + if not (ch == u'\n' or u'\x20' <= ch <= u'\x7E'): + if (ch == u'\x85' or u'\xA0' <= ch <= u'\uD7FF' + or u'\uE000' <= ch <= u'\uFFFD') and ch != u'\uFEFF': + unicode_characters = True + if not self.allow_unicode: + special_characters = True + else: + special_characters = True + + # Detect important whitespace combinations. + if ch == u' ': + if index == 0: + leading_space = True + if index == len(scalar)-1: + trailing_space = True + if previous_break: + break_space = True + previous_space = True + previous_break = False + elif ch in u'\n\x85\u2028\u2029': + if index == 0: + leading_break = True + if index == len(scalar)-1: + trailing_break = True + if previous_space: + space_break = True + previous_space = False + previous_break = True + else: + previous_space = False + previous_break = False + + # Prepare for the next character. + index += 1 + preceeded_by_whitespace = (ch in u'\0 \t\r\n\x85\u2028\u2029') + followed_by_whitespace = (index+1 >= len(scalar) or + scalar[index+1] in u'\0 \t\r\n\x85\u2028\u2029') + + # Let's decide what styles are allowed. + allow_flow_plain = True + allow_block_plain = True + allow_single_quoted = True + allow_double_quoted = True + allow_block = True + + # Leading and trailing whitespaces are bad for plain scalars. + if (leading_space or leading_break + or trailing_space or trailing_break): + allow_flow_plain = allow_block_plain = False + + # We do not permit trailing spaces for block scalars. + if trailing_space: + allow_block = False + + # Spaces at the beginning of a new line are only acceptable for block + # scalars. + if break_space: + allow_flow_plain = allow_block_plain = allow_single_quoted = False + + # Spaces followed by breaks, as well as special character are only + # allowed for double quoted scalars. + if space_break or special_characters: + allow_flow_plain = allow_block_plain = \ + allow_single_quoted = allow_block = False + + # Although the plain scalar writer supports breaks, we never emit + # multiline plain scalars. + if line_breaks: + allow_flow_plain = allow_block_plain = False + + # Flow indicators are forbidden for flow plain scalars. + if flow_indicators: + allow_flow_plain = False + + # Block indicators are forbidden for block plain scalars. + if block_indicators: + allow_block_plain = False + + return ScalarAnalysis(scalar=scalar, + empty=False, multiline=line_breaks, + allow_flow_plain=allow_flow_plain, + allow_block_plain=allow_block_plain, + allow_single_quoted=allow_single_quoted, + allow_double_quoted=allow_double_quoted, + allow_block=allow_block) + + # Writers. + + def flush_stream(self): + if hasattr(self.stream, 'flush'): + self.stream.flush() + + def write_stream_start(self): + # Write BOM if needed. + if self.encoding and self.encoding.startswith('utf-16'): + self.stream.write(u'\uFEFF'.encode(self.encoding)) + + def write_stream_end(self): + self.flush_stream() + + def write_indicator(self, indicator, need_whitespace, + whitespace=False, indention=False): + if self.whitespace or not need_whitespace: + data = indicator + else: + data = u' '+indicator + self.whitespace = whitespace + self.indention = self.indention and indention + self.column += len(data) + self.open_ended = False + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + + def write_indent(self): + indent = self.indent or 0 + if not self.indention or self.column > indent \ + or (self.column == indent and not self.whitespace): + self.write_line_break() + if self.column < indent: + self.whitespace = True + data = u' '*(indent-self.column) + self.column = indent + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + + def write_line_break(self, data=None): + if data is None: + data = self.best_line_break + self.whitespace = True + self.indention = True + self.line += 1 + self.column = 0 + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + + def write_version_directive(self, version_text): + data = u'%%YAML %s' % version_text + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + self.write_line_break() + + def write_tag_directive(self, handle_text, prefix_text): + data = u'%%TAG %s %s' % (handle_text, prefix_text) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + self.write_line_break() + + # Scalar streams. + + def write_single_quoted(self, text, split=True): + self.write_indicator(u'\'', True) + spaces = False + breaks = False + start = end = 0 + while end <= len(text): + ch = None + if end < len(text): + ch = text[end] + if spaces: + if ch is None or ch != u' ': + if start+1 == end and self.column > self.best_width and split \ + and start != 0 and end != len(text): + self.write_indent() + else: + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + elif breaks: + if ch is None or ch not in u'\n\x85\u2028\u2029': + if text[start] == u'\n': + self.write_line_break() + for br in text[start:end]: + if br == u'\n': + self.write_line_break() + else: + self.write_line_break(br) + self.write_indent() + start = end + else: + if ch is None or ch in u' \n\x85\u2028\u2029' or ch == u'\'': + if start < end: + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + if ch == u'\'': + data = u'\'\'' + self.column += 2 + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + 1 + if ch is not None: + spaces = (ch == u' ') + breaks = (ch in u'\n\x85\u2028\u2029') + end += 1 + self.write_indicator(u'\'', False) + + ESCAPE_REPLACEMENTS = { + u'\0': u'0', + u'\x07': u'a', + u'\x08': u'b', + u'\x09': u't', + u'\x0A': u'n', + u'\x0B': u'v', + u'\x0C': u'f', + u'\x0D': u'r', + u'\x1B': u'e', + u'\"': u'\"', + u'\\': u'\\', + u'\x85': u'N', + u'\xA0': u'_', + u'\u2028': u'L', + u'\u2029': u'P', + } + + def write_double_quoted(self, text, split=True): + self.write_indicator(u'"', True) + start = end = 0 + while end <= len(text): + ch = None + if end < len(text): + ch = text[end] + if ch is None or ch in u'"\\\x85\u2028\u2029\uFEFF' \ + or not (u'\x20' <= ch <= u'\x7E' + or (self.allow_unicode + and (u'\xA0' <= ch <= u'\uD7FF' + or u'\uE000' <= ch <= u'\uFFFD'))): + if start < end: + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + if ch is not None: + if ch in self.ESCAPE_REPLACEMENTS: + data = u'\\'+self.ESCAPE_REPLACEMENTS[ch] + elif ch <= u'\xFF': + data = u'\\x%02X' % ord(ch) + elif ch <= u'\uFFFF': + data = u'\\u%04X' % ord(ch) + else: + data = u'\\U%08X' % ord(ch) + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end+1 + if 0 < end < len(text)-1 and (ch == u' ' or start >= end) \ + and self.column+(end-start) > self.best_width and split: + data = text[start:end]+u'\\' + if start < end: + start = end + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + self.write_indent() + self.whitespace = False + self.indention = False + if text[start] == u' ': + data = u'\\' + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + end += 1 + self.write_indicator(u'"', False) + + def determine_block_hints(self, text): + hints = u'' + if text: + if text[0] in u' \n\x85\u2028\u2029': + hints += unicode(self.best_indent) + if text[-1] not in u'\n\x85\u2028\u2029': + hints += u'-' + elif len(text) == 1 or text[-2] in u'\n\x85\u2028\u2029': + hints += u'+' + return hints + + def write_folded(self, text): + hints = self.determine_block_hints(text) + self.write_indicator(u'>'+hints, True) + if hints[-1:] == u'+': + self.open_ended = True + self.write_line_break() + leading_space = True + spaces = False + breaks = True + start = end = 0 + while end <= len(text): + ch = None + if end < len(text): + ch = text[end] + if breaks: + if ch is None or ch not in u'\n\x85\u2028\u2029': + if not leading_space and ch is not None and ch != u' ' \ + and text[start] == u'\n': + self.write_line_break() + leading_space = (ch == u' ') + for br in text[start:end]: + if br == u'\n': + self.write_line_break() + else: + self.write_line_break(br) + if ch is not None: + self.write_indent() + start = end + elif spaces: + if ch != u' ': + if start+1 == end and self.column > self.best_width: + self.write_indent() + else: + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + else: + if ch is None or ch in u' \n\x85\u2028\u2029': + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + if ch is None: + self.write_line_break() + start = end + if ch is not None: + breaks = (ch in u'\n\x85\u2028\u2029') + spaces = (ch == u' ') + end += 1 + + def write_literal(self, text): + hints = self.determine_block_hints(text) + self.write_indicator(u'|'+hints, True) + if hints[-1:] == u'+': + self.open_ended = True + self.write_line_break() + breaks = True + start = end = 0 + while end <= len(text): + ch = None + if end < len(text): + ch = text[end] + if breaks: + if ch is None or ch not in u'\n\x85\u2028\u2029': + for br in text[start:end]: + if br == u'\n': + self.write_line_break() + else: + self.write_line_break(br) + if ch is not None: + self.write_indent() + start = end + else: + if ch is None or ch in u'\n\x85\u2028\u2029': + data = text[start:end] + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + if ch is None: + self.write_line_break() + start = end + if ch is not None: + breaks = (ch in u'\n\x85\u2028\u2029') + end += 1 + + def write_plain(self, text, split=True): + if self.root_context: + self.open_ended = True + if not text: + return + if not self.whitespace: + data = u' ' + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + self.whitespace = False + self.indention = False + spaces = False + breaks = False + start = end = 0 + while end <= len(text): + ch = None + if end < len(text): + ch = text[end] + if spaces: + if ch != u' ': + if start+1 == end and self.column > self.best_width and split: + self.write_indent() + self.whitespace = False + self.indention = False + else: + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + elif breaks: + if ch not in u'\n\x85\u2028\u2029': + if text[start] == u'\n': + self.write_line_break() + for br in text[start:end]: + if br == u'\n': + self.write_line_break() + else: + self.write_line_break(br) + self.write_indent() + self.whitespace = False + self.indention = False + start = end + else: + if ch is None or ch in u' \n\x85\u2028\u2029': + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + if ch is not None: + spaces = (ch == u' ') + breaks = (ch in u'\n\x85\u2028\u2029') + end += 1 + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py2/error.py b/RenderPipelineFile/rplibs/yaml/yaml_py2/error.py index 577686db..06d4e586 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py2/error.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py2/error.py @@ -1,75 +1,75 @@ - -__all__ = ['Mark', 'YAMLError', 'MarkedYAMLError'] - -class Mark(object): - - def __init__(self, name, index, line, column, buffer, pointer): - self.name = name - self.index = index - self.line = line - self.column = column - self.buffer = buffer - self.pointer = pointer - - def get_snippet(self, indent=4, max_length=75): - if self.buffer is None: - return None - head = '' - start = self.pointer - while start > 0 and self.buffer[start-1] not in u'\0\r\n\x85\u2028\u2029': - start -= 1 - if self.pointer-start > max_length/2-1: - head = ' ... ' - start += 5 - break - tail = '' - end = self.pointer - while end < len(self.buffer) and self.buffer[end] not in u'\0\r\n\x85\u2028\u2029': - end += 1 - if end-self.pointer > max_length/2-1: - tail = ' ... ' - end -= 5 - break - snippet = self.buffer[start:end].encode('utf-8') - return ' '*indent + head + snippet + tail + '\n' \ - + ' '*(indent+self.pointer-start+len(head)) + '^' - - def __str__(self): - snippet = self.get_snippet() - where = " in \"%s\", line %d, column %d" \ - % (self.name, self.line+1, self.column+1) - if snippet is not None: - where += ":\n"+snippet - return where - -class YAMLError(Exception): - pass - -class MarkedYAMLError(YAMLError): - - def __init__(self, context=None, context_mark=None, - problem=None, problem_mark=None, note=None): - self.context = context - self.context_mark = context_mark - self.problem = problem - self.problem_mark = problem_mark - self.note = note - - def __str__(self): - lines = [] - if self.context is not None: - lines.append(self.context) - if self.context_mark is not None \ - and (self.problem is None or self.problem_mark is None - or self.context_mark.name != self.problem_mark.name - or self.context_mark.line != self.problem_mark.line - or self.context_mark.column != self.problem_mark.column): - lines.append(str(self.context_mark)) - if self.problem is not None: - lines.append(self.problem) - if self.problem_mark is not None: - lines.append(str(self.problem_mark)) - if self.note is not None: - lines.append(self.note) - return '\n'.join(lines) - + +__all__ = ['Mark', 'YAMLError', 'MarkedYAMLError'] + +class Mark(object): + + def __init__(self, name, index, line, column, buffer, pointer): + self.name = name + self.index = index + self.line = line + self.column = column + self.buffer = buffer + self.pointer = pointer + + def get_snippet(self, indent=4, max_length=75): + if self.buffer is None: + return None + head = '' + start = self.pointer + while start > 0 and self.buffer[start-1] not in u'\0\r\n\x85\u2028\u2029': + start -= 1 + if self.pointer-start > max_length/2-1: + head = ' ... ' + start += 5 + break + tail = '' + end = self.pointer + while end < len(self.buffer) and self.buffer[end] not in u'\0\r\n\x85\u2028\u2029': + end += 1 + if end-self.pointer > max_length/2-1: + tail = ' ... ' + end -= 5 + break + snippet = self.buffer[start:end].encode('utf-8') + return ' '*indent + head + snippet + tail + '\n' \ + + ' '*(indent+self.pointer-start+len(head)) + '^' + + def __str__(self): + snippet = self.get_snippet() + where = " in \"%s\", line %d, column %d" \ + % (self.name, self.line+1, self.column+1) + if snippet is not None: + where += ":\n"+snippet + return where + +class YAMLError(Exception): + pass + +class MarkedYAMLError(YAMLError): + + def __init__(self, context=None, context_mark=None, + problem=None, problem_mark=None, note=None): + self.context = context + self.context_mark = context_mark + self.problem = problem + self.problem_mark = problem_mark + self.note = note + + def __str__(self): + lines = [] + if self.context is not None: + lines.append(self.context) + if self.context_mark is not None \ + and (self.problem is None or self.problem_mark is None + or self.context_mark.name != self.problem_mark.name + or self.context_mark.line != self.problem_mark.line + or self.context_mark.column != self.problem_mark.column): + lines.append(str(self.context_mark)) + if self.problem is not None: + lines.append(self.problem) + if self.problem_mark is not None: + lines.append(str(self.problem_mark)) + if self.note is not None: + lines.append(self.note) + return '\n'.join(lines) + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py2/events.py b/RenderPipelineFile/rplibs/yaml/yaml_py2/events.py index f79ad389..7ffac1e6 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py2/events.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py2/events.py @@ -1,86 +1,86 @@ - -# Abstract classes. - -class Event(object): - def __init__(self, start_mark=None, end_mark=None): - self.start_mark = start_mark - self.end_mark = end_mark - def __repr__(self): - attributes = [key for key in ['anchor', 'tag', 'implicit', 'value'] - if hasattr(self, key)] - arguments = ', '.join(['%s=%r' % (key, getattr(self, key)) - for key in attributes]) - return '%s(%s)' % (self.__class__.__name__, arguments) - -class NodeEvent(Event): - def __init__(self, anchor, start_mark=None, end_mark=None): - self.anchor = anchor - self.start_mark = start_mark - self.end_mark = end_mark - -class CollectionStartEvent(NodeEvent): - def __init__(self, anchor, tag, implicit, start_mark=None, end_mark=None, - flow_style=None): - self.anchor = anchor - self.tag = tag - self.implicit = implicit - self.start_mark = start_mark - self.end_mark = end_mark - self.flow_style = flow_style - -class CollectionEndEvent(Event): - pass - -# Implementations. - -class StreamStartEvent(Event): - def __init__(self, start_mark=None, end_mark=None, encoding=None): - self.start_mark = start_mark - self.end_mark = end_mark - self.encoding = encoding - -class StreamEndEvent(Event): - pass - -class DocumentStartEvent(Event): - def __init__(self, start_mark=None, end_mark=None, - explicit=None, version=None, tags=None): - self.start_mark = start_mark - self.end_mark = end_mark - self.explicit = explicit - self.version = version - self.tags = tags - -class DocumentEndEvent(Event): - def __init__(self, start_mark=None, end_mark=None, - explicit=None): - self.start_mark = start_mark - self.end_mark = end_mark - self.explicit = explicit - -class AliasEvent(NodeEvent): - pass - -class ScalarEvent(NodeEvent): - def __init__(self, anchor, tag, implicit, value, - start_mark=None, end_mark=None, style=None): - self.anchor = anchor - self.tag = tag - self.implicit = implicit - self.value = value - self.start_mark = start_mark - self.end_mark = end_mark - self.style = style - -class SequenceStartEvent(CollectionStartEvent): - pass - -class SequenceEndEvent(CollectionEndEvent): - pass - -class MappingStartEvent(CollectionStartEvent): - pass - -class MappingEndEvent(CollectionEndEvent): - pass - + +# Abstract classes. + +class Event(object): + def __init__(self, start_mark=None, end_mark=None): + self.start_mark = start_mark + self.end_mark = end_mark + def __repr__(self): + attributes = [key for key in ['anchor', 'tag', 'implicit', 'value'] + if hasattr(self, key)] + arguments = ', '.join(['%s=%r' % (key, getattr(self, key)) + for key in attributes]) + return '%s(%s)' % (self.__class__.__name__, arguments) + +class NodeEvent(Event): + def __init__(self, anchor, start_mark=None, end_mark=None): + self.anchor = anchor + self.start_mark = start_mark + self.end_mark = end_mark + +class CollectionStartEvent(NodeEvent): + def __init__(self, anchor, tag, implicit, start_mark=None, end_mark=None, + flow_style=None): + self.anchor = anchor + self.tag = tag + self.implicit = implicit + self.start_mark = start_mark + self.end_mark = end_mark + self.flow_style = flow_style + +class CollectionEndEvent(Event): + pass + +# Implementations. + +class StreamStartEvent(Event): + def __init__(self, start_mark=None, end_mark=None, encoding=None): + self.start_mark = start_mark + self.end_mark = end_mark + self.encoding = encoding + +class StreamEndEvent(Event): + pass + +class DocumentStartEvent(Event): + def __init__(self, start_mark=None, end_mark=None, + explicit=None, version=None, tags=None): + self.start_mark = start_mark + self.end_mark = end_mark + self.explicit = explicit + self.version = version + self.tags = tags + +class DocumentEndEvent(Event): + def __init__(self, start_mark=None, end_mark=None, + explicit=None): + self.start_mark = start_mark + self.end_mark = end_mark + self.explicit = explicit + +class AliasEvent(NodeEvent): + pass + +class ScalarEvent(NodeEvent): + def __init__(self, anchor, tag, implicit, value, + start_mark=None, end_mark=None, style=None): + self.anchor = anchor + self.tag = tag + self.implicit = implicit + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + self.style = style + +class SequenceStartEvent(CollectionStartEvent): + pass + +class SequenceEndEvent(CollectionEndEvent): + pass + +class MappingStartEvent(CollectionStartEvent): + pass + +class MappingEndEvent(CollectionEndEvent): + pass + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py2/loader.py b/RenderPipelineFile/rplibs/yaml/yaml_py2/loader.py index 293ff467..cfe107f2 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py2/loader.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py2/loader.py @@ -1,40 +1,40 @@ - -__all__ = ['BaseLoader', 'SafeLoader', 'Loader'] - -from reader import * -from scanner import * -from parser import * -from composer import * -from constructor import * -from resolver import * - -class BaseLoader(Reader, Scanner, Parser, Composer, BaseConstructor, BaseResolver): - - def __init__(self, stream): - Reader.__init__(self, stream) - Scanner.__init__(self) - Parser.__init__(self) - Composer.__init__(self) - BaseConstructor.__init__(self) - BaseResolver.__init__(self) - -class SafeLoader(Reader, Scanner, Parser, Composer, SafeConstructor, Resolver): - - def __init__(self, stream): - Reader.__init__(self, stream) - Scanner.__init__(self) - Parser.__init__(self) - Composer.__init__(self) - SafeConstructor.__init__(self) - Resolver.__init__(self) - -class Loader(Reader, Scanner, Parser, Composer, Constructor, Resolver): - - def __init__(self, stream): - Reader.__init__(self, stream) - Scanner.__init__(self) - Parser.__init__(self) - Composer.__init__(self) - Constructor.__init__(self) - Resolver.__init__(self) - + +__all__ = ['BaseLoader', 'SafeLoader', 'Loader'] + +from reader import * +from scanner import * +from parser import * +from composer import * +from constructor import * +from resolver import * + +class BaseLoader(Reader, Scanner, Parser, Composer, BaseConstructor, BaseResolver): + + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + BaseConstructor.__init__(self) + BaseResolver.__init__(self) + +class SafeLoader(Reader, Scanner, Parser, Composer, SafeConstructor, Resolver): + + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + SafeConstructor.__init__(self) + Resolver.__init__(self) + +class Loader(Reader, Scanner, Parser, Composer, Constructor, Resolver): + + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + Constructor.__init__(self) + Resolver.__init__(self) + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py2/nodes.py b/RenderPipelineFile/rplibs/yaml/yaml_py2/nodes.py index c4f070c4..d4a3b0a0 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py2/nodes.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py2/nodes.py @@ -1,49 +1,49 @@ - -class Node(object): - def __init__(self, tag, value, start_mark, end_mark): - self.tag = tag - self.value = value - self.start_mark = start_mark - self.end_mark = end_mark - def __repr__(self): - value = self.value - #if isinstance(value, list): - # if len(value) == 0: - # value = '' - # elif len(value) == 1: - # value = '<1 item>' - # else: - # value = '<%d items>' % len(value) - #else: - # if len(value) > 75: - # value = repr(value[:70]+u' ... ') - # else: - # value = repr(value) - value = repr(value) - return '%s(tag=%r, value=%s)' % (self.__class__.__name__, self.tag, value) - -class ScalarNode(Node): - id = 'scalar' - def __init__(self, tag, value, - start_mark=None, end_mark=None, style=None): - self.tag = tag - self.value = value - self.start_mark = start_mark - self.end_mark = end_mark - self.style = style - -class CollectionNode(Node): - def __init__(self, tag, value, - start_mark=None, end_mark=None, flow_style=None): - self.tag = tag - self.value = value - self.start_mark = start_mark - self.end_mark = end_mark - self.flow_style = flow_style - -class SequenceNode(CollectionNode): - id = 'sequence' - -class MappingNode(CollectionNode): - id = 'mapping' - + +class Node(object): + def __init__(self, tag, value, start_mark, end_mark): + self.tag = tag + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + def __repr__(self): + value = self.value + #if isinstance(value, list): + # if len(value) == 0: + # value = '' + # elif len(value) == 1: + # value = '<1 item>' + # else: + # value = '<%d items>' % len(value) + #else: + # if len(value) > 75: + # value = repr(value[:70]+u' ... ') + # else: + # value = repr(value) + value = repr(value) + return '%s(tag=%r, value=%s)' % (self.__class__.__name__, self.tag, value) + +class ScalarNode(Node): + id = 'scalar' + def __init__(self, tag, value, + start_mark=None, end_mark=None, style=None): + self.tag = tag + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + self.style = style + +class CollectionNode(Node): + def __init__(self, tag, value, + start_mark=None, end_mark=None, flow_style=None): + self.tag = tag + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + self.flow_style = flow_style + +class SequenceNode(CollectionNode): + id = 'sequence' + +class MappingNode(CollectionNode): + id = 'mapping' + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py2/parser.py b/RenderPipelineFile/rplibs/yaml/yaml_py2/parser.py index f9e3057f..8ff0ad70 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py2/parser.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py2/parser.py @@ -1,589 +1,589 @@ - -# The following YAML grammar is LL(1) and is parsed by a recursive descent -# parser. -# -# stream ::= STREAM-START implicit_document? explicit_document* STREAM-END -# implicit_document ::= block_node DOCUMENT-END* -# explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* -# block_node_or_indentless_sequence ::= -# ALIAS -# | properties (block_content | indentless_block_sequence)? -# | block_content -# | indentless_block_sequence -# block_node ::= ALIAS -# | properties block_content? -# | block_content -# flow_node ::= ALIAS -# | properties flow_content? -# | flow_content -# properties ::= TAG ANCHOR? | ANCHOR TAG? -# block_content ::= block_collection | flow_collection | SCALAR -# flow_content ::= flow_collection | SCALAR -# block_collection ::= block_sequence | block_mapping -# flow_collection ::= flow_sequence | flow_mapping -# block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END -# indentless_sequence ::= (BLOCK-ENTRY block_node?)+ -# block_mapping ::= BLOCK-MAPPING_START -# ((KEY block_node_or_indentless_sequence?)? -# (VALUE block_node_or_indentless_sequence?)?)* -# BLOCK-END -# flow_sequence ::= FLOW-SEQUENCE-START -# (flow_sequence_entry FLOW-ENTRY)* -# flow_sequence_entry? -# FLOW-SEQUENCE-END -# flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? -# flow_mapping ::= FLOW-MAPPING-START -# (flow_mapping_entry FLOW-ENTRY)* -# flow_mapping_entry? -# FLOW-MAPPING-END -# flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? -# -# FIRST sets: -# -# stream: { STREAM-START } -# explicit_document: { DIRECTIVE DOCUMENT-START } -# implicit_document: FIRST(block_node) -# block_node: { ALIAS TAG ANCHOR SCALAR BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START } -# flow_node: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START } -# block_content: { BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START SCALAR } -# flow_content: { FLOW-SEQUENCE-START FLOW-MAPPING-START SCALAR } -# block_collection: { BLOCK-SEQUENCE-START BLOCK-MAPPING-START } -# flow_collection: { FLOW-SEQUENCE-START FLOW-MAPPING-START } -# block_sequence: { BLOCK-SEQUENCE-START } -# block_mapping: { BLOCK-MAPPING-START } -# block_node_or_indentless_sequence: { ALIAS ANCHOR TAG SCALAR BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START BLOCK-ENTRY } -# indentless_sequence: { ENTRY } -# flow_collection: { FLOW-SEQUENCE-START FLOW-MAPPING-START } -# flow_sequence: { FLOW-SEQUENCE-START } -# flow_mapping: { FLOW-MAPPING-START } -# flow_sequence_entry: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START KEY } -# flow_mapping_entry: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START KEY } - -__all__ = ['Parser', 'ParserError'] - -from error import MarkedYAMLError -from tokens import * -from events import * -from scanner import * - -class ParserError(MarkedYAMLError): - pass - -class Parser(object): - # Since writing a recursive-descendant parser is a straightforward task, we - # do not give many comments here. - - DEFAULT_TAGS = { - u'!': u'!', - u'!!': u'tag:yaml.org,2002:', - } - - def __init__(self): - self.current_event = None - self.yaml_version = None - self.tag_handles = {} - self.states = [] - self.marks = [] - self.state = self.parse_stream_start - - def dispose(self): - # Reset the state attributes (to clear self-references) - self.states = [] - self.state = None - - def check_event(self, *choices): - # Check the type of the next event. - if self.current_event is None: - if self.state: - self.current_event = self.state() - if self.current_event is not None: - if not choices: - return True - for choice in choices: - if isinstance(self.current_event, choice): - return True - return False - - def peek_event(self): - # Get the next event. - if self.current_event is None: - if self.state: - self.current_event = self.state() - return self.current_event - - def get_event(self): - # Get the next event and proceed further. - if self.current_event is None: - if self.state: - self.current_event = self.state() - value = self.current_event - self.current_event = None - return value - - # stream ::= STREAM-START implicit_document? explicit_document* STREAM-END - # implicit_document ::= block_node DOCUMENT-END* - # explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* - - def parse_stream_start(self): - - # Parse the stream start. - token = self.get_token() - event = StreamStartEvent(token.start_mark, token.end_mark, - encoding=token.encoding) - - # Prepare the next state. - self.state = self.parse_implicit_document_start - - return event - - def parse_implicit_document_start(self): - - # Parse an implicit document. - if not self.check_token(DirectiveToken, DocumentStartToken, - StreamEndToken): - self.tag_handles = self.DEFAULT_TAGS - token = self.peek_token() - start_mark = end_mark = token.start_mark - event = DocumentStartEvent(start_mark, end_mark, - explicit=False) - - # Prepare the next state. - self.states.append(self.parse_document_end) - self.state = self.parse_block_node - - return event - - else: - return self.parse_document_start() - - def parse_document_start(self): - - # Parse any extra document end indicators. - while self.check_token(DocumentEndToken): - self.get_token() - - # Parse an explicit document. - if not self.check_token(StreamEndToken): - token = self.peek_token() - start_mark = token.start_mark - version, tags = self.process_directives() - if not self.check_token(DocumentStartToken): - raise ParserError(None, None, - "expected '', but found %r" - % self.peek_token().id, - self.peek_token().start_mark) - token = self.get_token() - end_mark = token.end_mark - event = DocumentStartEvent(start_mark, end_mark, - explicit=True, version=version, tags=tags) - self.states.append(self.parse_document_end) - self.state = self.parse_document_content - else: - # Parse the end of the stream. - token = self.get_token() - event = StreamEndEvent(token.start_mark, token.end_mark) - assert not self.states - assert not self.marks - self.state = None - return event - - def parse_document_end(self): - - # Parse the document end. - token = self.peek_token() - start_mark = end_mark = token.start_mark - explicit = False - if self.check_token(DocumentEndToken): - token = self.get_token() - end_mark = token.end_mark - explicit = True - event = DocumentEndEvent(start_mark, end_mark, - explicit=explicit) - - # Prepare the next state. - self.state = self.parse_document_start - - return event - - def parse_document_content(self): - if self.check_token(DirectiveToken, - DocumentStartToken, DocumentEndToken, StreamEndToken): - event = self.process_empty_scalar(self.peek_token().start_mark) - self.state = self.states.pop() - return event - else: - return self.parse_block_node() - - def process_directives(self): - self.yaml_version = None - self.tag_handles = {} - while self.check_token(DirectiveToken): - token = self.get_token() - if token.name == u'YAML': - if self.yaml_version is not None: - raise ParserError(None, None, - "found duplicate YAML directive", token.start_mark) - major, minor = token.value - if major != 1: - raise ParserError(None, None, - "found incompatible YAML document (version 1.* is required)", - token.start_mark) - self.yaml_version = token.value - elif token.name == u'TAG': - handle, prefix = token.value - if handle in self.tag_handles: - raise ParserError(None, None, - "duplicate tag handle %r" % handle.encode('utf-8'), - token.start_mark) - self.tag_handles[handle] = prefix - if self.tag_handles: - value = self.yaml_version, self.tag_handles.copy() - else: - value = self.yaml_version, None - for key in self.DEFAULT_TAGS: - if key not in self.tag_handles: - self.tag_handles[key] = self.DEFAULT_TAGS[key] - return value - - # block_node_or_indentless_sequence ::= ALIAS - # | properties (block_content | indentless_block_sequence)? - # | block_content - # | indentless_block_sequence - # block_node ::= ALIAS - # | properties block_content? - # | block_content - # flow_node ::= ALIAS - # | properties flow_content? - # | flow_content - # properties ::= TAG ANCHOR? | ANCHOR TAG? - # block_content ::= block_collection | flow_collection | SCALAR - # flow_content ::= flow_collection | SCALAR - # block_collection ::= block_sequence | block_mapping - # flow_collection ::= flow_sequence | flow_mapping - - def parse_block_node(self): - return self.parse_node(block=True) - - def parse_flow_node(self): - return self.parse_node() - - def parse_block_node_or_indentless_sequence(self): - return self.parse_node(block=True, indentless_sequence=True) - - def parse_node(self, block=False, indentless_sequence=False): - if self.check_token(AliasToken): - token = self.get_token() - event = AliasEvent(token.value, token.start_mark, token.end_mark) - self.state = self.states.pop() - else: - anchor = None - tag = None - start_mark = end_mark = tag_mark = None - if self.check_token(AnchorToken): - token = self.get_token() - start_mark = token.start_mark - end_mark = token.end_mark - anchor = token.value - if self.check_token(TagToken): - token = self.get_token() - tag_mark = token.start_mark - end_mark = token.end_mark - tag = token.value - elif self.check_token(TagToken): - token = self.get_token() - start_mark = tag_mark = token.start_mark - end_mark = token.end_mark - tag = token.value - if self.check_token(AnchorToken): - token = self.get_token() - end_mark = token.end_mark - anchor = token.value - if tag is not None: - handle, suffix = tag - if handle is not None: - if handle not in self.tag_handles: - raise ParserError("while parsing a node", start_mark, - "found undefined tag handle %r" % handle.encode('utf-8'), - tag_mark) - tag = self.tag_handles[handle]+suffix - else: - tag = suffix - #if tag == u'!': - # raise ParserError("while parsing a node", start_mark, - # "found non-specific tag '!'", tag_mark, - # "Please check 'http://pyyaml.org/wiki/YAMLNonSpecificTag' and share your opinion.") - if start_mark is None: - start_mark = end_mark = self.peek_token().start_mark - event = None - implicit = (tag is None or tag == u'!') - if indentless_sequence and self.check_token(BlockEntryToken): - end_mark = self.peek_token().end_mark - event = SequenceStartEvent(anchor, tag, implicit, - start_mark, end_mark) - self.state = self.parse_indentless_sequence_entry - else: - if self.check_token(ScalarToken): - token = self.get_token() - end_mark = token.end_mark - if (token.plain and tag is None) or tag == u'!': - implicit = (True, False) - elif tag is None: - implicit = (False, True) - else: - implicit = (False, False) - event = ScalarEvent(anchor, tag, implicit, token.value, - start_mark, end_mark, style=token.style) - self.state = self.states.pop() - elif self.check_token(FlowSequenceStartToken): - end_mark = self.peek_token().end_mark - event = SequenceStartEvent(anchor, tag, implicit, - start_mark, end_mark, flow_style=True) - self.state = self.parse_flow_sequence_first_entry - elif self.check_token(FlowMappingStartToken): - end_mark = self.peek_token().end_mark - event = MappingStartEvent(anchor, tag, implicit, - start_mark, end_mark, flow_style=True) - self.state = self.parse_flow_mapping_first_key - elif block and self.check_token(BlockSequenceStartToken): - end_mark = self.peek_token().start_mark - event = SequenceStartEvent(anchor, tag, implicit, - start_mark, end_mark, flow_style=False) - self.state = self.parse_block_sequence_first_entry - elif block and self.check_token(BlockMappingStartToken): - end_mark = self.peek_token().start_mark - event = MappingStartEvent(anchor, tag, implicit, - start_mark, end_mark, flow_style=False) - self.state = self.parse_block_mapping_first_key - elif anchor is not None or tag is not None: - # Empty scalars are allowed even if a tag or an anchor is - # specified. - event = ScalarEvent(anchor, tag, (implicit, False), u'', - start_mark, end_mark) - self.state = self.states.pop() - else: - if block: - node = 'block' - else: - node = 'flow' - token = self.peek_token() - raise ParserError("while parsing a %s node" % node, start_mark, - "expected the node content, but found %r" % token.id, - token.start_mark) - return event - - # block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END - - def parse_block_sequence_first_entry(self): - token = self.get_token() - self.marks.append(token.start_mark) - return self.parse_block_sequence_entry() - - def parse_block_sequence_entry(self): - if self.check_token(BlockEntryToken): - token = self.get_token() - if not self.check_token(BlockEntryToken, BlockEndToken): - self.states.append(self.parse_block_sequence_entry) - return self.parse_block_node() - else: - self.state = self.parse_block_sequence_entry - return self.process_empty_scalar(token.end_mark) - if not self.check_token(BlockEndToken): - token = self.peek_token() - raise ParserError("while parsing a block collection", self.marks[-1], - "expected , but found %r" % token.id, token.start_mark) - token = self.get_token() - event = SequenceEndEvent(token.start_mark, token.end_mark) - self.state = self.states.pop() - self.marks.pop() - return event - - # indentless_sequence ::= (BLOCK-ENTRY block_node?)+ - - def parse_indentless_sequence_entry(self): - if self.check_token(BlockEntryToken): - token = self.get_token() - if not self.check_token(BlockEntryToken, - KeyToken, ValueToken, BlockEndToken): - self.states.append(self.parse_indentless_sequence_entry) - return self.parse_block_node() - else: - self.state = self.parse_indentless_sequence_entry - return self.process_empty_scalar(token.end_mark) - token = self.peek_token() - event = SequenceEndEvent(token.start_mark, token.start_mark) - self.state = self.states.pop() - return event - - # block_mapping ::= BLOCK-MAPPING_START - # ((KEY block_node_or_indentless_sequence?)? - # (VALUE block_node_or_indentless_sequence?)?)* - # BLOCK-END - - def parse_block_mapping_first_key(self): - token = self.get_token() - self.marks.append(token.start_mark) - return self.parse_block_mapping_key() - - def parse_block_mapping_key(self): - if self.check_token(KeyToken): - token = self.get_token() - if not self.check_token(KeyToken, ValueToken, BlockEndToken): - self.states.append(self.parse_block_mapping_value) - return self.parse_block_node_or_indentless_sequence() - else: - self.state = self.parse_block_mapping_value - return self.process_empty_scalar(token.end_mark) - if not self.check_token(BlockEndToken): - token = self.peek_token() - raise ParserError("while parsing a block mapping", self.marks[-1], - "expected , but found %r" % token.id, token.start_mark) - token = self.get_token() - event = MappingEndEvent(token.start_mark, token.end_mark) - self.state = self.states.pop() - self.marks.pop() - return event - - def parse_block_mapping_value(self): - if self.check_token(ValueToken): - token = self.get_token() - if not self.check_token(KeyToken, ValueToken, BlockEndToken): - self.states.append(self.parse_block_mapping_key) - return self.parse_block_node_or_indentless_sequence() - else: - self.state = self.parse_block_mapping_key - return self.process_empty_scalar(token.end_mark) - else: - self.state = self.parse_block_mapping_key - token = self.peek_token() - return self.process_empty_scalar(token.start_mark) - - # flow_sequence ::= FLOW-SEQUENCE-START - # (flow_sequence_entry FLOW-ENTRY)* - # flow_sequence_entry? - # FLOW-SEQUENCE-END - # flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? - # - # Note that while production rules for both flow_sequence_entry and - # flow_mapping_entry are equal, their interpretations are different. - # For `flow_sequence_entry`, the part `KEY flow_node? (VALUE flow_node?)?` - # generate an inline mapping (set syntax). - - def parse_flow_sequence_first_entry(self): - token = self.get_token() - self.marks.append(token.start_mark) - return self.parse_flow_sequence_entry(first=True) - - def parse_flow_sequence_entry(self, first=False): - if not self.check_token(FlowSequenceEndToken): - if not first: - if self.check_token(FlowEntryToken): - self.get_token() - else: - token = self.peek_token() - raise ParserError("while parsing a flow sequence", self.marks[-1], - "expected ',' or ']', but got %r" % token.id, token.start_mark) - - if self.check_token(KeyToken): - token = self.peek_token() - event = MappingStartEvent(None, None, True, - token.start_mark, token.end_mark, - flow_style=True) - self.state = self.parse_flow_sequence_entry_mapping_key - return event - elif not self.check_token(FlowSequenceEndToken): - self.states.append(self.parse_flow_sequence_entry) - return self.parse_flow_node() - token = self.get_token() - event = SequenceEndEvent(token.start_mark, token.end_mark) - self.state = self.states.pop() - self.marks.pop() - return event - - def parse_flow_sequence_entry_mapping_key(self): - token = self.get_token() - if not self.check_token(ValueToken, - FlowEntryToken, FlowSequenceEndToken): - self.states.append(self.parse_flow_sequence_entry_mapping_value) - return self.parse_flow_node() - else: - self.state = self.parse_flow_sequence_entry_mapping_value - return self.process_empty_scalar(token.end_mark) - - def parse_flow_sequence_entry_mapping_value(self): - if self.check_token(ValueToken): - token = self.get_token() - if not self.check_token(FlowEntryToken, FlowSequenceEndToken): - self.states.append(self.parse_flow_sequence_entry_mapping_end) - return self.parse_flow_node() - else: - self.state = self.parse_flow_sequence_entry_mapping_end - return self.process_empty_scalar(token.end_mark) - else: - self.state = self.parse_flow_sequence_entry_mapping_end - token = self.peek_token() - return self.process_empty_scalar(token.start_mark) - - def parse_flow_sequence_entry_mapping_end(self): - self.state = self.parse_flow_sequence_entry - token = self.peek_token() - return MappingEndEvent(token.start_mark, token.start_mark) - - # flow_mapping ::= FLOW-MAPPING-START - # (flow_mapping_entry FLOW-ENTRY)* - # flow_mapping_entry? - # FLOW-MAPPING-END - # flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? - - def parse_flow_mapping_first_key(self): - token = self.get_token() - self.marks.append(token.start_mark) - return self.parse_flow_mapping_key(first=True) - - def parse_flow_mapping_key(self, first=False): - if not self.check_token(FlowMappingEndToken): - if not first: - if self.check_token(FlowEntryToken): - self.get_token() - else: - token = self.peek_token() - raise ParserError("while parsing a flow mapping", self.marks[-1], - "expected ',' or '}', but got %r" % token.id, token.start_mark) - if self.check_token(KeyToken): - token = self.get_token() - if not self.check_token(ValueToken, - FlowEntryToken, FlowMappingEndToken): - self.states.append(self.parse_flow_mapping_value) - return self.parse_flow_node() - else: - self.state = self.parse_flow_mapping_value - return self.process_empty_scalar(token.end_mark) - elif not self.check_token(FlowMappingEndToken): - self.states.append(self.parse_flow_mapping_empty_value) - return self.parse_flow_node() - token = self.get_token() - event = MappingEndEvent(token.start_mark, token.end_mark) - self.state = self.states.pop() - self.marks.pop() - return event - - def parse_flow_mapping_value(self): - if self.check_token(ValueToken): - token = self.get_token() - if not self.check_token(FlowEntryToken, FlowMappingEndToken): - self.states.append(self.parse_flow_mapping_key) - return self.parse_flow_node() - else: - self.state = self.parse_flow_mapping_key - return self.process_empty_scalar(token.end_mark) - else: - self.state = self.parse_flow_mapping_key - token = self.peek_token() - return self.process_empty_scalar(token.start_mark) - - def parse_flow_mapping_empty_value(self): - self.state = self.parse_flow_mapping_key - return self.process_empty_scalar(self.peek_token().start_mark) - - def process_empty_scalar(self, mark): - return ScalarEvent(None, None, (True, False), u'', mark, mark) - + +# The following YAML grammar is LL(1) and is parsed by a recursive descent +# parser. +# +# stream ::= STREAM-START implicit_document? explicit_document* STREAM-END +# implicit_document ::= block_node DOCUMENT-END* +# explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* +# block_node_or_indentless_sequence ::= +# ALIAS +# | properties (block_content | indentless_block_sequence)? +# | block_content +# | indentless_block_sequence +# block_node ::= ALIAS +# | properties block_content? +# | block_content +# flow_node ::= ALIAS +# | properties flow_content? +# | flow_content +# properties ::= TAG ANCHOR? | ANCHOR TAG? +# block_content ::= block_collection | flow_collection | SCALAR +# flow_content ::= flow_collection | SCALAR +# block_collection ::= block_sequence | block_mapping +# flow_collection ::= flow_sequence | flow_mapping +# block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END +# indentless_sequence ::= (BLOCK-ENTRY block_node?)+ +# block_mapping ::= BLOCK-MAPPING_START +# ((KEY block_node_or_indentless_sequence?)? +# (VALUE block_node_or_indentless_sequence?)?)* +# BLOCK-END +# flow_sequence ::= FLOW-SEQUENCE-START +# (flow_sequence_entry FLOW-ENTRY)* +# flow_sequence_entry? +# FLOW-SEQUENCE-END +# flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +# flow_mapping ::= FLOW-MAPPING-START +# (flow_mapping_entry FLOW-ENTRY)* +# flow_mapping_entry? +# FLOW-MAPPING-END +# flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +# +# FIRST sets: +# +# stream: { STREAM-START } +# explicit_document: { DIRECTIVE DOCUMENT-START } +# implicit_document: FIRST(block_node) +# block_node: { ALIAS TAG ANCHOR SCALAR BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START } +# flow_node: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START } +# block_content: { BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START SCALAR } +# flow_content: { FLOW-SEQUENCE-START FLOW-MAPPING-START SCALAR } +# block_collection: { BLOCK-SEQUENCE-START BLOCK-MAPPING-START } +# flow_collection: { FLOW-SEQUENCE-START FLOW-MAPPING-START } +# block_sequence: { BLOCK-SEQUENCE-START } +# block_mapping: { BLOCK-MAPPING-START } +# block_node_or_indentless_sequence: { ALIAS ANCHOR TAG SCALAR BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START BLOCK-ENTRY } +# indentless_sequence: { ENTRY } +# flow_collection: { FLOW-SEQUENCE-START FLOW-MAPPING-START } +# flow_sequence: { FLOW-SEQUENCE-START } +# flow_mapping: { FLOW-MAPPING-START } +# flow_sequence_entry: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START KEY } +# flow_mapping_entry: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START KEY } + +__all__ = ['Parser', 'ParserError'] + +from error import MarkedYAMLError +from tokens import * +from events import * +from scanner import * + +class ParserError(MarkedYAMLError): + pass + +class Parser(object): + # Since writing a recursive-descendant parser is a straightforward task, we + # do not give many comments here. + + DEFAULT_TAGS = { + u'!': u'!', + u'!!': u'tag:yaml.org,2002:', + } + + def __init__(self): + self.current_event = None + self.yaml_version = None + self.tag_handles = {} + self.states = [] + self.marks = [] + self.state = self.parse_stream_start + + def dispose(self): + # Reset the state attributes (to clear self-references) + self.states = [] + self.state = None + + def check_event(self, *choices): + # Check the type of the next event. + if self.current_event is None: + if self.state: + self.current_event = self.state() + if self.current_event is not None: + if not choices: + return True + for choice in choices: + if isinstance(self.current_event, choice): + return True + return False + + def peek_event(self): + # Get the next event. + if self.current_event is None: + if self.state: + self.current_event = self.state() + return self.current_event + + def get_event(self): + # Get the next event and proceed further. + if self.current_event is None: + if self.state: + self.current_event = self.state() + value = self.current_event + self.current_event = None + return value + + # stream ::= STREAM-START implicit_document? explicit_document* STREAM-END + # implicit_document ::= block_node DOCUMENT-END* + # explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* + + def parse_stream_start(self): + + # Parse the stream start. + token = self.get_token() + event = StreamStartEvent(token.start_mark, token.end_mark, + encoding=token.encoding) + + # Prepare the next state. + self.state = self.parse_implicit_document_start + + return event + + def parse_implicit_document_start(self): + + # Parse an implicit document. + if not self.check_token(DirectiveToken, DocumentStartToken, + StreamEndToken): + self.tag_handles = self.DEFAULT_TAGS + token = self.peek_token() + start_mark = end_mark = token.start_mark + event = DocumentStartEvent(start_mark, end_mark, + explicit=False) + + # Prepare the next state. + self.states.append(self.parse_document_end) + self.state = self.parse_block_node + + return event + + else: + return self.parse_document_start() + + def parse_document_start(self): + + # Parse any extra document end indicators. + while self.check_token(DocumentEndToken): + self.get_token() + + # Parse an explicit document. + if not self.check_token(StreamEndToken): + token = self.peek_token() + start_mark = token.start_mark + version, tags = self.process_directives() + if not self.check_token(DocumentStartToken): + raise ParserError(None, None, + "expected '', but found %r" + % self.peek_token().id, + self.peek_token().start_mark) + token = self.get_token() + end_mark = token.end_mark + event = DocumentStartEvent(start_mark, end_mark, + explicit=True, version=version, tags=tags) + self.states.append(self.parse_document_end) + self.state = self.parse_document_content + else: + # Parse the end of the stream. + token = self.get_token() + event = StreamEndEvent(token.start_mark, token.end_mark) + assert not self.states + assert not self.marks + self.state = None + return event + + def parse_document_end(self): + + # Parse the document end. + token = self.peek_token() + start_mark = end_mark = token.start_mark + explicit = False + if self.check_token(DocumentEndToken): + token = self.get_token() + end_mark = token.end_mark + explicit = True + event = DocumentEndEvent(start_mark, end_mark, + explicit=explicit) + + # Prepare the next state. + self.state = self.parse_document_start + + return event + + def parse_document_content(self): + if self.check_token(DirectiveToken, + DocumentStartToken, DocumentEndToken, StreamEndToken): + event = self.process_empty_scalar(self.peek_token().start_mark) + self.state = self.states.pop() + return event + else: + return self.parse_block_node() + + def process_directives(self): + self.yaml_version = None + self.tag_handles = {} + while self.check_token(DirectiveToken): + token = self.get_token() + if token.name == u'YAML': + if self.yaml_version is not None: + raise ParserError(None, None, + "found duplicate YAML directive", token.start_mark) + major, minor = token.value + if major != 1: + raise ParserError(None, None, + "found incompatible YAML document (version 1.* is required)", + token.start_mark) + self.yaml_version = token.value + elif token.name == u'TAG': + handle, prefix = token.value + if handle in self.tag_handles: + raise ParserError(None, None, + "duplicate tag handle %r" % handle.encode('utf-8'), + token.start_mark) + self.tag_handles[handle] = prefix + if self.tag_handles: + value = self.yaml_version, self.tag_handles.copy() + else: + value = self.yaml_version, None + for key in self.DEFAULT_TAGS: + if key not in self.tag_handles: + self.tag_handles[key] = self.DEFAULT_TAGS[key] + return value + + # block_node_or_indentless_sequence ::= ALIAS + # | properties (block_content | indentless_block_sequence)? + # | block_content + # | indentless_block_sequence + # block_node ::= ALIAS + # | properties block_content? + # | block_content + # flow_node ::= ALIAS + # | properties flow_content? + # | flow_content + # properties ::= TAG ANCHOR? | ANCHOR TAG? + # block_content ::= block_collection | flow_collection | SCALAR + # flow_content ::= flow_collection | SCALAR + # block_collection ::= block_sequence | block_mapping + # flow_collection ::= flow_sequence | flow_mapping + + def parse_block_node(self): + return self.parse_node(block=True) + + def parse_flow_node(self): + return self.parse_node() + + def parse_block_node_or_indentless_sequence(self): + return self.parse_node(block=True, indentless_sequence=True) + + def parse_node(self, block=False, indentless_sequence=False): + if self.check_token(AliasToken): + token = self.get_token() + event = AliasEvent(token.value, token.start_mark, token.end_mark) + self.state = self.states.pop() + else: + anchor = None + tag = None + start_mark = end_mark = tag_mark = None + if self.check_token(AnchorToken): + token = self.get_token() + start_mark = token.start_mark + end_mark = token.end_mark + anchor = token.value + if self.check_token(TagToken): + token = self.get_token() + tag_mark = token.start_mark + end_mark = token.end_mark + tag = token.value + elif self.check_token(TagToken): + token = self.get_token() + start_mark = tag_mark = token.start_mark + end_mark = token.end_mark + tag = token.value + if self.check_token(AnchorToken): + token = self.get_token() + end_mark = token.end_mark + anchor = token.value + if tag is not None: + handle, suffix = tag + if handle is not None: + if handle not in self.tag_handles: + raise ParserError("while parsing a node", start_mark, + "found undefined tag handle %r" % handle.encode('utf-8'), + tag_mark) + tag = self.tag_handles[handle]+suffix + else: + tag = suffix + #if tag == u'!': + # raise ParserError("while parsing a node", start_mark, + # "found non-specific tag '!'", tag_mark, + # "Please check 'http://pyyaml.org/wiki/YAMLNonSpecificTag' and share your opinion.") + if start_mark is None: + start_mark = end_mark = self.peek_token().start_mark + event = None + implicit = (tag is None or tag == u'!') + if indentless_sequence and self.check_token(BlockEntryToken): + end_mark = self.peek_token().end_mark + event = SequenceStartEvent(anchor, tag, implicit, + start_mark, end_mark) + self.state = self.parse_indentless_sequence_entry + else: + if self.check_token(ScalarToken): + token = self.get_token() + end_mark = token.end_mark + if (token.plain and tag is None) or tag == u'!': + implicit = (True, False) + elif tag is None: + implicit = (False, True) + else: + implicit = (False, False) + event = ScalarEvent(anchor, tag, implicit, token.value, + start_mark, end_mark, style=token.style) + self.state = self.states.pop() + elif self.check_token(FlowSequenceStartToken): + end_mark = self.peek_token().end_mark + event = SequenceStartEvent(anchor, tag, implicit, + start_mark, end_mark, flow_style=True) + self.state = self.parse_flow_sequence_first_entry + elif self.check_token(FlowMappingStartToken): + end_mark = self.peek_token().end_mark + event = MappingStartEvent(anchor, tag, implicit, + start_mark, end_mark, flow_style=True) + self.state = self.parse_flow_mapping_first_key + elif block and self.check_token(BlockSequenceStartToken): + end_mark = self.peek_token().start_mark + event = SequenceStartEvent(anchor, tag, implicit, + start_mark, end_mark, flow_style=False) + self.state = self.parse_block_sequence_first_entry + elif block and self.check_token(BlockMappingStartToken): + end_mark = self.peek_token().start_mark + event = MappingStartEvent(anchor, tag, implicit, + start_mark, end_mark, flow_style=False) + self.state = self.parse_block_mapping_first_key + elif anchor is not None or tag is not None: + # Empty scalars are allowed even if a tag or an anchor is + # specified. + event = ScalarEvent(anchor, tag, (implicit, False), u'', + start_mark, end_mark) + self.state = self.states.pop() + else: + if block: + node = 'block' + else: + node = 'flow' + token = self.peek_token() + raise ParserError("while parsing a %s node" % node, start_mark, + "expected the node content, but found %r" % token.id, + token.start_mark) + return event + + # block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END + + def parse_block_sequence_first_entry(self): + token = self.get_token() + self.marks.append(token.start_mark) + return self.parse_block_sequence_entry() + + def parse_block_sequence_entry(self): + if self.check_token(BlockEntryToken): + token = self.get_token() + if not self.check_token(BlockEntryToken, BlockEndToken): + self.states.append(self.parse_block_sequence_entry) + return self.parse_block_node() + else: + self.state = self.parse_block_sequence_entry + return self.process_empty_scalar(token.end_mark) + if not self.check_token(BlockEndToken): + token = self.peek_token() + raise ParserError("while parsing a block collection", self.marks[-1], + "expected , but found %r" % token.id, token.start_mark) + token = self.get_token() + event = SequenceEndEvent(token.start_mark, token.end_mark) + self.state = self.states.pop() + self.marks.pop() + return event + + # indentless_sequence ::= (BLOCK-ENTRY block_node?)+ + + def parse_indentless_sequence_entry(self): + if self.check_token(BlockEntryToken): + token = self.get_token() + if not self.check_token(BlockEntryToken, + KeyToken, ValueToken, BlockEndToken): + self.states.append(self.parse_indentless_sequence_entry) + return self.parse_block_node() + else: + self.state = self.parse_indentless_sequence_entry + return self.process_empty_scalar(token.end_mark) + token = self.peek_token() + event = SequenceEndEvent(token.start_mark, token.start_mark) + self.state = self.states.pop() + return event + + # block_mapping ::= BLOCK-MAPPING_START + # ((KEY block_node_or_indentless_sequence?)? + # (VALUE block_node_or_indentless_sequence?)?)* + # BLOCK-END + + def parse_block_mapping_first_key(self): + token = self.get_token() + self.marks.append(token.start_mark) + return self.parse_block_mapping_key() + + def parse_block_mapping_key(self): + if self.check_token(KeyToken): + token = self.get_token() + if not self.check_token(KeyToken, ValueToken, BlockEndToken): + self.states.append(self.parse_block_mapping_value) + return self.parse_block_node_or_indentless_sequence() + else: + self.state = self.parse_block_mapping_value + return self.process_empty_scalar(token.end_mark) + if not self.check_token(BlockEndToken): + token = self.peek_token() + raise ParserError("while parsing a block mapping", self.marks[-1], + "expected , but found %r" % token.id, token.start_mark) + token = self.get_token() + event = MappingEndEvent(token.start_mark, token.end_mark) + self.state = self.states.pop() + self.marks.pop() + return event + + def parse_block_mapping_value(self): + if self.check_token(ValueToken): + token = self.get_token() + if not self.check_token(KeyToken, ValueToken, BlockEndToken): + self.states.append(self.parse_block_mapping_key) + return self.parse_block_node_or_indentless_sequence() + else: + self.state = self.parse_block_mapping_key + return self.process_empty_scalar(token.end_mark) + else: + self.state = self.parse_block_mapping_key + token = self.peek_token() + return self.process_empty_scalar(token.start_mark) + + # flow_sequence ::= FLOW-SEQUENCE-START + # (flow_sequence_entry FLOW-ENTRY)* + # flow_sequence_entry? + # FLOW-SEQUENCE-END + # flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? + # + # Note that while production rules for both flow_sequence_entry and + # flow_mapping_entry are equal, their interpretations are different. + # For `flow_sequence_entry`, the part `KEY flow_node? (VALUE flow_node?)?` + # generate an inline mapping (set syntax). + + def parse_flow_sequence_first_entry(self): + token = self.get_token() + self.marks.append(token.start_mark) + return self.parse_flow_sequence_entry(first=True) + + def parse_flow_sequence_entry(self, first=False): + if not self.check_token(FlowSequenceEndToken): + if not first: + if self.check_token(FlowEntryToken): + self.get_token() + else: + token = self.peek_token() + raise ParserError("while parsing a flow sequence", self.marks[-1], + "expected ',' or ']', but got %r" % token.id, token.start_mark) + + if self.check_token(KeyToken): + token = self.peek_token() + event = MappingStartEvent(None, None, True, + token.start_mark, token.end_mark, + flow_style=True) + self.state = self.parse_flow_sequence_entry_mapping_key + return event + elif not self.check_token(FlowSequenceEndToken): + self.states.append(self.parse_flow_sequence_entry) + return self.parse_flow_node() + token = self.get_token() + event = SequenceEndEvent(token.start_mark, token.end_mark) + self.state = self.states.pop() + self.marks.pop() + return event + + def parse_flow_sequence_entry_mapping_key(self): + token = self.get_token() + if not self.check_token(ValueToken, + FlowEntryToken, FlowSequenceEndToken): + self.states.append(self.parse_flow_sequence_entry_mapping_value) + return self.parse_flow_node() + else: + self.state = self.parse_flow_sequence_entry_mapping_value + return self.process_empty_scalar(token.end_mark) + + def parse_flow_sequence_entry_mapping_value(self): + if self.check_token(ValueToken): + token = self.get_token() + if not self.check_token(FlowEntryToken, FlowSequenceEndToken): + self.states.append(self.parse_flow_sequence_entry_mapping_end) + return self.parse_flow_node() + else: + self.state = self.parse_flow_sequence_entry_mapping_end + return self.process_empty_scalar(token.end_mark) + else: + self.state = self.parse_flow_sequence_entry_mapping_end + token = self.peek_token() + return self.process_empty_scalar(token.start_mark) + + def parse_flow_sequence_entry_mapping_end(self): + self.state = self.parse_flow_sequence_entry + token = self.peek_token() + return MappingEndEvent(token.start_mark, token.start_mark) + + # flow_mapping ::= FLOW-MAPPING-START + # (flow_mapping_entry FLOW-ENTRY)* + # flow_mapping_entry? + # FLOW-MAPPING-END + # flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? + + def parse_flow_mapping_first_key(self): + token = self.get_token() + self.marks.append(token.start_mark) + return self.parse_flow_mapping_key(first=True) + + def parse_flow_mapping_key(self, first=False): + if not self.check_token(FlowMappingEndToken): + if not first: + if self.check_token(FlowEntryToken): + self.get_token() + else: + token = self.peek_token() + raise ParserError("while parsing a flow mapping", self.marks[-1], + "expected ',' or '}', but got %r" % token.id, token.start_mark) + if self.check_token(KeyToken): + token = self.get_token() + if not self.check_token(ValueToken, + FlowEntryToken, FlowMappingEndToken): + self.states.append(self.parse_flow_mapping_value) + return self.parse_flow_node() + else: + self.state = self.parse_flow_mapping_value + return self.process_empty_scalar(token.end_mark) + elif not self.check_token(FlowMappingEndToken): + self.states.append(self.parse_flow_mapping_empty_value) + return self.parse_flow_node() + token = self.get_token() + event = MappingEndEvent(token.start_mark, token.end_mark) + self.state = self.states.pop() + self.marks.pop() + return event + + def parse_flow_mapping_value(self): + if self.check_token(ValueToken): + token = self.get_token() + if not self.check_token(FlowEntryToken, FlowMappingEndToken): + self.states.append(self.parse_flow_mapping_key) + return self.parse_flow_node() + else: + self.state = self.parse_flow_mapping_key + return self.process_empty_scalar(token.end_mark) + else: + self.state = self.parse_flow_mapping_key + token = self.peek_token() + return self.process_empty_scalar(token.start_mark) + + def parse_flow_mapping_empty_value(self): + self.state = self.parse_flow_mapping_key + return self.process_empty_scalar(self.peek_token().start_mark) + + def process_empty_scalar(self, mark): + return ScalarEvent(None, None, (True, False), u'', mark, mark) + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py2/reader.py b/RenderPipelineFile/rplibs/yaml/yaml_py2/reader.py index 3249e6b9..41ab7b0c 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py2/reader.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py2/reader.py @@ -1,190 +1,190 @@ -# This module contains abstractions for the input stream. You don't have to -# looks further, there are no pretty code. -# -# We define two classes here. -# -# Mark(source, line, column) -# It's just a record and its only use is producing nice error messages. -# Parser does not use it for any other purposes. -# -# Reader(source, data) -# Reader determines the encoding of `data` and converts it to unicode. -# Reader provides the following methods and attributes: -# reader.peek(length=1) - return the next `length` characters -# reader.forward(length=1) - move the current position to `length` characters. -# reader.index - the number of the current character. -# reader.line, stream.column - the line and the column of the current character. - -__all__ = ['Reader', 'ReaderError'] - -from error import YAMLError, Mark - -import codecs, re - -class ReaderError(YAMLError): - - def __init__(self, name, position, character, encoding, reason): - self.name = name - self.character = character - self.position = position - self.encoding = encoding - self.reason = reason - - def __str__(self): - if isinstance(self.character, str): - return "'%s' codec can't decode byte #x%02x: %s\n" \ - " in \"%s\", position %d" \ - % (self.encoding, ord(self.character), self.reason, - self.name, self.position) - else: - return "unacceptable character #x%04x: %s\n" \ - " in \"%s\", position %d" \ - % (self.character, self.reason, - self.name, self.position) - -class Reader(object): - # Reader: - # - determines the data encoding and converts it to unicode, - # - checks if characters are in allowed range, - # - adds '\0' to the end. - - # Reader accepts - # - a `str` object, - # - a `unicode` object, - # - a file-like object with its `read` method returning `str`, - # - a file-like object with its `read` method returning `unicode`. - - # Yeah, it's ugly and slow. - - def __init__(self, stream): - self.name = None - self.stream = None - self.stream_pointer = 0 - self.eof = True - self.buffer = u'' - self.pointer = 0 - self.raw_buffer = None - self.raw_decode = None - self.encoding = None - self.index = 0 - self.line = 0 - self.column = 0 - if isinstance(stream, unicode): - self.name = "" - self.check_printable(stream) - self.buffer = stream+u'\0' - elif isinstance(stream, str): - self.name = "" - self.raw_buffer = stream - self.determine_encoding() - else: - self.stream = stream - self.name = getattr(stream, 'name', "") - self.eof = False - self.raw_buffer = '' - self.determine_encoding() - - def peek(self, index=0): - try: - return self.buffer[self.pointer+index] - except IndexError: - self.update(index+1) - return self.buffer[self.pointer+index] - - def prefix(self, length=1): - if self.pointer+length >= len(self.buffer): - self.update(length) - return self.buffer[self.pointer:self.pointer+length] - - def forward(self, length=1): - if self.pointer+length+1 >= len(self.buffer): - self.update(length+1) - while length: - ch = self.buffer[self.pointer] - self.pointer += 1 - self.index += 1 - if ch in u'\n\x85\u2028\u2029' \ - or (ch == u'\r' and self.buffer[self.pointer] != u'\n'): - self.line += 1 - self.column = 0 - elif ch != u'\uFEFF': - self.column += 1 - length -= 1 - - def get_mark(self): - if self.stream is None: - return Mark(self.name, self.index, self.line, self.column, - self.buffer, self.pointer) - else: - return Mark(self.name, self.index, self.line, self.column, - None, None) - - def determine_encoding(self): - while not self.eof and len(self.raw_buffer) < 2: - self.update_raw() - if not isinstance(self.raw_buffer, unicode): - if self.raw_buffer.startswith(codecs.BOM_UTF16_LE): - self.raw_decode = codecs.utf_16_le_decode - self.encoding = 'utf-16-le' - elif self.raw_buffer.startswith(codecs.BOM_UTF16_BE): - self.raw_decode = codecs.utf_16_be_decode - self.encoding = 'utf-16-be' - else: - self.raw_decode = codecs.utf_8_decode - self.encoding = 'utf-8' - self.update(1) - - NON_PRINTABLE = re.compile(u'[^\x09\x0A\x0D\x20-\x7E\x85\xA0-\uD7FF\uE000-\uFFFD]') - def check_printable(self, data): - match = self.NON_PRINTABLE.search(data) - if match: - character = match.group() - position = self.index+(len(self.buffer)-self.pointer)+match.start() - raise ReaderError(self.name, position, ord(character), - 'unicode', "special characters are not allowed") - - def update(self, length): - if self.raw_buffer is None: - return - self.buffer = self.buffer[self.pointer:] - self.pointer = 0 - while len(self.buffer) < length: - if not self.eof: - self.update_raw() - if self.raw_decode is not None: - try: - data, converted = self.raw_decode(self.raw_buffer, - 'strict', self.eof) - except UnicodeDecodeError, exc: - character = exc.object[exc.start] - if self.stream is not None: - position = self.stream_pointer-len(self.raw_buffer)+exc.start - else: - position = exc.start - raise ReaderError(self.name, position, character, - exc.encoding, exc.reason) - else: - data = self.raw_buffer - converted = len(data) - self.check_printable(data) - self.buffer += data - self.raw_buffer = self.raw_buffer[converted:] - if self.eof: - self.buffer += u'\0' - self.raw_buffer = None - break - - def update_raw(self, size=1024): - data = self.stream.read(size) - if data: - self.raw_buffer += data - self.stream_pointer += len(data) - else: - self.eof = True - -#try: -# import psyco -# psyco.bind(Reader) -#except ImportError: -# pass - +# This module contains abstractions for the input stream. You don't have to +# looks further, there are no pretty code. +# +# We define two classes here. +# +# Mark(source, line, column) +# It's just a record and its only use is producing nice error messages. +# Parser does not use it for any other purposes. +# +# Reader(source, data) +# Reader determines the encoding of `data` and converts it to unicode. +# Reader provides the following methods and attributes: +# reader.peek(length=1) - return the next `length` characters +# reader.forward(length=1) - move the current position to `length` characters. +# reader.index - the number of the current character. +# reader.line, stream.column - the line and the column of the current character. + +__all__ = ['Reader', 'ReaderError'] + +from error import YAMLError, Mark + +import codecs, re + +class ReaderError(YAMLError): + + def __init__(self, name, position, character, encoding, reason): + self.name = name + self.character = character + self.position = position + self.encoding = encoding + self.reason = reason + + def __str__(self): + if isinstance(self.character, str): + return "'%s' codec can't decode byte #x%02x: %s\n" \ + " in \"%s\", position %d" \ + % (self.encoding, ord(self.character), self.reason, + self.name, self.position) + else: + return "unacceptable character #x%04x: %s\n" \ + " in \"%s\", position %d" \ + % (self.character, self.reason, + self.name, self.position) + +class Reader(object): + # Reader: + # - determines the data encoding and converts it to unicode, + # - checks if characters are in allowed range, + # - adds '\0' to the end. + + # Reader accepts + # - a `str` object, + # - a `unicode` object, + # - a file-like object with its `read` method returning `str`, + # - a file-like object with its `read` method returning `unicode`. + + # Yeah, it's ugly and slow. + + def __init__(self, stream): + self.name = None + self.stream = None + self.stream_pointer = 0 + self.eof = True + self.buffer = u'' + self.pointer = 0 + self.raw_buffer = None + self.raw_decode = None + self.encoding = None + self.index = 0 + self.line = 0 + self.column = 0 + if isinstance(stream, unicode): + self.name = "" + self.check_printable(stream) + self.buffer = stream+u'\0' + elif isinstance(stream, str): + self.name = "" + self.raw_buffer = stream + self.determine_encoding() + else: + self.stream = stream + self.name = getattr(stream, 'name', "") + self.eof = False + self.raw_buffer = '' + self.determine_encoding() + + def peek(self, index=0): + try: + return self.buffer[self.pointer+index] + except IndexError: + self.update(index+1) + return self.buffer[self.pointer+index] + + def prefix(self, length=1): + if self.pointer+length >= len(self.buffer): + self.update(length) + return self.buffer[self.pointer:self.pointer+length] + + def forward(self, length=1): + if self.pointer+length+1 >= len(self.buffer): + self.update(length+1) + while length: + ch = self.buffer[self.pointer] + self.pointer += 1 + self.index += 1 + if ch in u'\n\x85\u2028\u2029' \ + or (ch == u'\r' and self.buffer[self.pointer] != u'\n'): + self.line += 1 + self.column = 0 + elif ch != u'\uFEFF': + self.column += 1 + length -= 1 + + def get_mark(self): + if self.stream is None: + return Mark(self.name, self.index, self.line, self.column, + self.buffer, self.pointer) + else: + return Mark(self.name, self.index, self.line, self.column, + None, None) + + def determine_encoding(self): + while not self.eof and len(self.raw_buffer) < 2: + self.update_raw() + if not isinstance(self.raw_buffer, unicode): + if self.raw_buffer.startswith(codecs.BOM_UTF16_LE): + self.raw_decode = codecs.utf_16_le_decode + self.encoding = 'utf-16-le' + elif self.raw_buffer.startswith(codecs.BOM_UTF16_BE): + self.raw_decode = codecs.utf_16_be_decode + self.encoding = 'utf-16-be' + else: + self.raw_decode = codecs.utf_8_decode + self.encoding = 'utf-8' + self.update(1) + + NON_PRINTABLE = re.compile(u'[^\x09\x0A\x0D\x20-\x7E\x85\xA0-\uD7FF\uE000-\uFFFD]') + def check_printable(self, data): + match = self.NON_PRINTABLE.search(data) + if match: + character = match.group() + position = self.index+(len(self.buffer)-self.pointer)+match.start() + raise ReaderError(self.name, position, ord(character), + 'unicode', "special characters are not allowed") + + def update(self, length): + if self.raw_buffer is None: + return + self.buffer = self.buffer[self.pointer:] + self.pointer = 0 + while len(self.buffer) < length: + if not self.eof: + self.update_raw() + if self.raw_decode is not None: + try: + data, converted = self.raw_decode(self.raw_buffer, + 'strict', self.eof) + except UnicodeDecodeError, exc: + character = exc.object[exc.start] + if self.stream is not None: + position = self.stream_pointer-len(self.raw_buffer)+exc.start + else: + position = exc.start + raise ReaderError(self.name, position, character, + exc.encoding, exc.reason) + else: + data = self.raw_buffer + converted = len(data) + self.check_printable(data) + self.buffer += data + self.raw_buffer = self.raw_buffer[converted:] + if self.eof: + self.buffer += u'\0' + self.raw_buffer = None + break + + def update_raw(self, size=1024): + data = self.stream.read(size) + if data: + self.raw_buffer += data + self.stream_pointer += len(data) + else: + self.eof = True + +#try: +# import psyco +# psyco.bind(Reader) +#except ImportError: +# pass + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py2/representer.py b/RenderPipelineFile/rplibs/yaml/yaml_py2/representer.py index 5f4fc70d..8c3a321c 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py2/representer.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py2/representer.py @@ -1,484 +1,484 @@ - -__all__ = ['BaseRepresenter', 'SafeRepresenter', 'Representer', - 'RepresenterError'] - -from error import * -from nodes import * - -import datetime - -import sys, copy_reg, types - -class RepresenterError(YAMLError): - pass - -class BaseRepresenter(object): - - yaml_representers = {} - yaml_multi_representers = {} - - def __init__(self, default_style=None, default_flow_style=None): - self.default_style = default_style - self.default_flow_style = default_flow_style - self.represented_objects = {} - self.object_keeper = [] - self.alias_key = None - - def represent(self, data): - node = self.represent_data(data) - self.serialize(node) - self.represented_objects = {} - self.object_keeper = [] - self.alias_key = None - - def get_classobj_bases(self, cls): - bases = [cls] - for base in cls.__bases__: - bases.extend(self.get_classobj_bases(base)) - return bases - - def represent_data(self, data): - if self.ignore_aliases(data): - self.alias_key = None - else: - self.alias_key = id(data) - if self.alias_key is not None: - if self.alias_key in self.represented_objects: - node = self.represented_objects[self.alias_key] - #if node is None: - # raise RepresenterError("recursive objects are not allowed: %r" % data) - return node - #self.represented_objects[alias_key] = None - self.object_keeper.append(data) - data_types = type(data).__mro__ - if type(data) is types.InstanceType: - data_types = self.get_classobj_bases(data.__class__)+list(data_types) - if data_types[0] in self.yaml_representers: - node = self.yaml_representers[data_types[0]](self, data) - else: - for data_type in data_types: - if data_type in self.yaml_multi_representers: - node = self.yaml_multi_representers[data_type](self, data) - break - else: - if None in self.yaml_multi_representers: - node = self.yaml_multi_representers[None](self, data) - elif None in self.yaml_representers: - node = self.yaml_representers[None](self, data) - else: - node = ScalarNode(None, unicode(data)) - #if alias_key is not None: - # self.represented_objects[alias_key] = node - return node - - def add_representer(cls, data_type, representer): - if not 'yaml_representers' in cls.__dict__: - cls.yaml_representers = cls.yaml_representers.copy() - cls.yaml_representers[data_type] = representer - add_representer = classmethod(add_representer) - - def add_multi_representer(cls, data_type, representer): - if not 'yaml_multi_representers' in cls.__dict__: - cls.yaml_multi_representers = cls.yaml_multi_representers.copy() - cls.yaml_multi_representers[data_type] = representer - add_multi_representer = classmethod(add_multi_representer) - - def represent_scalar(self, tag, value, style=None): - if style is None: - style = self.default_style - node = ScalarNode(tag, value, style=style) - if self.alias_key is not None: - self.represented_objects[self.alias_key] = node - return node - - def represent_sequence(self, tag, sequence, flow_style=None): - value = [] - node = SequenceNode(tag, value, flow_style=flow_style) - if self.alias_key is not None: - self.represented_objects[self.alias_key] = node - best_style = True - for item in sequence: - node_item = self.represent_data(item) - if not (isinstance(node_item, ScalarNode) and not node_item.style): - best_style = False - value.append(node_item) - if flow_style is None: - if self.default_flow_style is not None: - node.flow_style = self.default_flow_style - else: - node.flow_style = best_style - return node - - def represent_mapping(self, tag, mapping, flow_style=None): - value = [] - node = MappingNode(tag, value, flow_style=flow_style) - if self.alias_key is not None: - self.represented_objects[self.alias_key] = node - best_style = True - if hasattr(mapping, 'items'): - mapping = mapping.items() - mapping.sort() - for item_key, item_value in mapping: - node_key = self.represent_data(item_key) - node_value = self.represent_data(item_value) - if not (isinstance(node_key, ScalarNode) and not node_key.style): - best_style = False - if not (isinstance(node_value, ScalarNode) and not node_value.style): - best_style = False - value.append((node_key, node_value)) - if flow_style is None: - if self.default_flow_style is not None: - node.flow_style = self.default_flow_style - else: - node.flow_style = best_style - return node - - def ignore_aliases(self, data): - return False - -class SafeRepresenter(BaseRepresenter): - - def ignore_aliases(self, data): - if data in [None, ()]: - return True - if isinstance(data, (str, unicode, bool, int, float)): - return True - - def represent_none(self, data): - return self.represent_scalar(u'tag:yaml.org,2002:null', - u'null') - - def represent_str(self, data): - tag = None - style = None - try: - data = unicode(data, 'ascii') - tag = u'tag:yaml.org,2002:str' - except UnicodeDecodeError: - try: - data = unicode(data, 'utf-8') - tag = u'tag:yaml.org,2002:str' - except UnicodeDecodeError: - data = data.encode('base64') - tag = u'tag:yaml.org,2002:binary' - style = '|' - return self.represent_scalar(tag, data, style=style) - - def represent_unicode(self, data): - return self.represent_scalar(u'tag:yaml.org,2002:str', data) - - def represent_bool(self, data): - if data: - value = u'true' - else: - value = u'false' - return self.represent_scalar(u'tag:yaml.org,2002:bool', value) - - def represent_int(self, data): - return self.represent_scalar(u'tag:yaml.org,2002:int', unicode(data)) - - def represent_long(self, data): - return self.represent_scalar(u'tag:yaml.org,2002:int', unicode(data)) - - inf_value = 1e300 - while repr(inf_value) != repr(inf_value*inf_value): - inf_value *= inf_value - - def represent_float(self, data): - if data != data or (data == 0.0 and data == 1.0): - value = u'.nan' - elif data == self.inf_value: - value = u'.inf' - elif data == -self.inf_value: - value = u'-.inf' - else: - value = unicode(repr(data)).lower() - # Note that in some cases `repr(data)` represents a float number - # without the decimal parts. For instance: - # >>> repr(1e17) - # '1e17' - # Unfortunately, this is not a valid float representation according - # to the definition of the `!!float` tag. We fix this by adding - # '.0' before the 'e' symbol. - if u'.' not in value and u'e' in value: - value = value.replace(u'e', u'.0e', 1) - return self.represent_scalar(u'tag:yaml.org,2002:float', value) - - def represent_list(self, data): - #pairs = (len(data) > 0 and isinstance(data, list)) - #if pairs: - # for item in data: - # if not isinstance(item, tuple) or len(item) != 2: - # pairs = False - # break - #if not pairs: - return self.represent_sequence(u'tag:yaml.org,2002:seq', data) - #value = [] - #for item_key, item_value in data: - # value.append(self.represent_mapping(u'tag:yaml.org,2002:map', - # [(item_key, item_value)])) - #return SequenceNode(u'tag:yaml.org,2002:pairs', value) - - def represent_dict(self, data): - return self.represent_mapping(u'tag:yaml.org,2002:map', data) - - def represent_set(self, data): - value = {} - for key in data: - value[key] = None - return self.represent_mapping(u'tag:yaml.org,2002:set', value) - - def represent_date(self, data): - value = unicode(data.isoformat()) - return self.represent_scalar(u'tag:yaml.org,2002:timestamp', value) - - def represent_datetime(self, data): - value = unicode(data.isoformat(' ')) - return self.represent_scalar(u'tag:yaml.org,2002:timestamp', value) - - def represent_yaml_object(self, tag, data, cls, flow_style=None): - if hasattr(data, '__getstate__'): - state = data.__getstate__() - else: - state = data.__dict__.copy() - return self.represent_mapping(tag, state, flow_style=flow_style) - - def represent_undefined(self, data): - raise RepresenterError("cannot represent an object: %s" % data) - -SafeRepresenter.add_representer(type(None), - SafeRepresenter.represent_none) - -SafeRepresenter.add_representer(str, - SafeRepresenter.represent_str) - -SafeRepresenter.add_representer(unicode, - SafeRepresenter.represent_unicode) - -SafeRepresenter.add_representer(bool, - SafeRepresenter.represent_bool) - -SafeRepresenter.add_representer(int, - SafeRepresenter.represent_int) - -SafeRepresenter.add_representer(long, - SafeRepresenter.represent_long) - -SafeRepresenter.add_representer(float, - SafeRepresenter.represent_float) - -SafeRepresenter.add_representer(list, - SafeRepresenter.represent_list) - -SafeRepresenter.add_representer(tuple, - SafeRepresenter.represent_list) - -SafeRepresenter.add_representer(dict, - SafeRepresenter.represent_dict) - -SafeRepresenter.add_representer(set, - SafeRepresenter.represent_set) - -SafeRepresenter.add_representer(datetime.date, - SafeRepresenter.represent_date) - -SafeRepresenter.add_representer(datetime.datetime, - SafeRepresenter.represent_datetime) - -SafeRepresenter.add_representer(None, - SafeRepresenter.represent_undefined) - -class Representer(SafeRepresenter): - - def represent_str(self, data): - tag = None - style = None - try: - data = unicode(data, 'ascii') - tag = u'tag:yaml.org,2002:str' - except UnicodeDecodeError: - try: - data = unicode(data, 'utf-8') - tag = u'tag:yaml.org,2002:python/str' - except UnicodeDecodeError: - data = data.encode('base64') - tag = u'tag:yaml.org,2002:binary' - style = '|' - return self.represent_scalar(tag, data, style=style) - - def represent_unicode(self, data): - tag = None - try: - data.encode('ascii') - tag = u'tag:yaml.org,2002:python/unicode' - except UnicodeEncodeError: - tag = u'tag:yaml.org,2002:str' - return self.represent_scalar(tag, data) - - def represent_long(self, data): - tag = u'tag:yaml.org,2002:int' - if int(data) is not data: - tag = u'tag:yaml.org,2002:python/long' - return self.represent_scalar(tag, unicode(data)) - - def represent_complex(self, data): - if data.imag == 0.0: - data = u'%r' % data.real - elif data.real == 0.0: - data = u'%rj' % data.imag - elif data.imag > 0: - data = u'%r+%rj' % (data.real, data.imag) - else: - data = u'%r%rj' % (data.real, data.imag) - return self.represent_scalar(u'tag:yaml.org,2002:python/complex', data) - - def represent_tuple(self, data): - return self.represent_sequence(u'tag:yaml.org,2002:python/tuple', data) - - def represent_name(self, data): - name = u'%s.%s' % (data.__module__, data.__name__) - return self.represent_scalar(u'tag:yaml.org,2002:python/name:'+name, u'') - - def represent_module(self, data): - return self.represent_scalar( - u'tag:yaml.org,2002:python/module:'+data.__name__, u'') - - def represent_instance(self, data): - # For instances of classic classes, we use __getinitargs__ and - # __getstate__ to serialize the data. - - # If data.__getinitargs__ exists, the object must be reconstructed by - # calling cls(**args), where args is a tuple returned by - # __getinitargs__. Otherwise, the cls.__init__ method should never be - # called and the class instance is created by instantiating a trivial - # class and assigning to the instance's __class__ variable. - - # If data.__getstate__ exists, it returns the state of the object. - # Otherwise, the state of the object is data.__dict__. - - # We produce either a !!python/object or !!python/object/new node. - # If data.__getinitargs__ does not exist and state is a dictionary, we - # produce a !!python/object node . Otherwise we produce a - # !!python/object/new node. - - cls = data.__class__ - class_name = u'%s.%s' % (cls.__module__, cls.__name__) - args = None - state = None - if hasattr(data, '__getinitargs__'): - args = list(data.__getinitargs__()) - if hasattr(data, '__getstate__'): - state = data.__getstate__() - else: - state = data.__dict__ - if args is None and isinstance(state, dict): - return self.represent_mapping( - u'tag:yaml.org,2002:python/object:'+class_name, state) - if isinstance(state, dict) and not state: - return self.represent_sequence( - u'tag:yaml.org,2002:python/object/new:'+class_name, args) - value = {} - if args: - value['args'] = args - value['state'] = state - return self.represent_mapping( - u'tag:yaml.org,2002:python/object/new:'+class_name, value) - - def represent_object(self, data): - # We use __reduce__ API to save the data. data.__reduce__ returns - # a tuple of length 2-5: - # (function, args, state, listitems, dictitems) - - # For reconstructing, we calls function(*args), then set its state, - # listitems, and dictitems if they are not None. - - # A special case is when function.__name__ == '__newobj__'. In this - # case we create the object with args[0].__new__(*args). - - # Another special case is when __reduce__ returns a string - we don't - # support it. - - # We produce a !!python/object, !!python/object/new or - # !!python/object/apply node. - - cls = type(data) - if cls in copy_reg.dispatch_table: - reduce = copy_reg.dispatch_table[cls](data) - elif hasattr(data, '__reduce_ex__'): - reduce = data.__reduce_ex__(2) - elif hasattr(data, '__reduce__'): - reduce = data.__reduce__() - else: - raise RepresenterError("cannot represent object: %r" % data) - reduce = (list(reduce)+[None]*5)[:5] - function, args, state, listitems, dictitems = reduce - args = list(args) - if state is None: - state = {} - if listitems is not None: - listitems = list(listitems) - if dictitems is not None: - dictitems = dict(dictitems) - if function.__name__ == '__newobj__': - function = args[0] - args = args[1:] - tag = u'tag:yaml.org,2002:python/object/new:' - newobj = True - else: - tag = u'tag:yaml.org,2002:python/object/apply:' - newobj = False - function_name = u'%s.%s' % (function.__module__, function.__name__) - if not args and not listitems and not dictitems \ - and isinstance(state, dict) and newobj: - return self.represent_mapping( - u'tag:yaml.org,2002:python/object:'+function_name, state) - if not listitems and not dictitems \ - and isinstance(state, dict) and not state: - return self.represent_sequence(tag+function_name, args) - value = {} - if args: - value['args'] = args - if state or not isinstance(state, dict): - value['state'] = state - if listitems: - value['listitems'] = listitems - if dictitems: - value['dictitems'] = dictitems - return self.represent_mapping(tag+function_name, value) - -Representer.add_representer(str, - Representer.represent_str) - -Representer.add_representer(unicode, - Representer.represent_unicode) - -Representer.add_representer(long, - Representer.represent_long) - -Representer.add_representer(complex, - Representer.represent_complex) - -Representer.add_representer(tuple, - Representer.represent_tuple) - -Representer.add_representer(type, - Representer.represent_name) - -Representer.add_representer(types.ClassType, - Representer.represent_name) - -Representer.add_representer(types.FunctionType, - Representer.represent_name) - -Representer.add_representer(types.BuiltinFunctionType, - Representer.represent_name) - -Representer.add_representer(types.ModuleType, - Representer.represent_module) - -Representer.add_multi_representer(types.InstanceType, - Representer.represent_instance) - -Representer.add_multi_representer(object, - Representer.represent_object) - + +__all__ = ['BaseRepresenter', 'SafeRepresenter', 'Representer', + 'RepresenterError'] + +from error import * +from nodes import * + +import datetime + +import sys, copy_reg, types + +class RepresenterError(YAMLError): + pass + +class BaseRepresenter(object): + + yaml_representers = {} + yaml_multi_representers = {} + + def __init__(self, default_style=None, default_flow_style=None): + self.default_style = default_style + self.default_flow_style = default_flow_style + self.represented_objects = {} + self.object_keeper = [] + self.alias_key = None + + def represent(self, data): + node = self.represent_data(data) + self.serialize(node) + self.represented_objects = {} + self.object_keeper = [] + self.alias_key = None + + def get_classobj_bases(self, cls): + bases = [cls] + for base in cls.__bases__: + bases.extend(self.get_classobj_bases(base)) + return bases + + def represent_data(self, data): + if self.ignore_aliases(data): + self.alias_key = None + else: + self.alias_key = id(data) + if self.alias_key is not None: + if self.alias_key in self.represented_objects: + node = self.represented_objects[self.alias_key] + #if node is None: + # raise RepresenterError("recursive objects are not allowed: %r" % data) + return node + #self.represented_objects[alias_key] = None + self.object_keeper.append(data) + data_types = type(data).__mro__ + if type(data) is types.InstanceType: + data_types = self.get_classobj_bases(data.__class__)+list(data_types) + if data_types[0] in self.yaml_representers: + node = self.yaml_representers[data_types[0]](self, data) + else: + for data_type in data_types: + if data_type in self.yaml_multi_representers: + node = self.yaml_multi_representers[data_type](self, data) + break + else: + if None in self.yaml_multi_representers: + node = self.yaml_multi_representers[None](self, data) + elif None in self.yaml_representers: + node = self.yaml_representers[None](self, data) + else: + node = ScalarNode(None, unicode(data)) + #if alias_key is not None: + # self.represented_objects[alias_key] = node + return node + + def add_representer(cls, data_type, representer): + if not 'yaml_representers' in cls.__dict__: + cls.yaml_representers = cls.yaml_representers.copy() + cls.yaml_representers[data_type] = representer + add_representer = classmethod(add_representer) + + def add_multi_representer(cls, data_type, representer): + if not 'yaml_multi_representers' in cls.__dict__: + cls.yaml_multi_representers = cls.yaml_multi_representers.copy() + cls.yaml_multi_representers[data_type] = representer + add_multi_representer = classmethod(add_multi_representer) + + def represent_scalar(self, tag, value, style=None): + if style is None: + style = self.default_style + node = ScalarNode(tag, value, style=style) + if self.alias_key is not None: + self.represented_objects[self.alias_key] = node + return node + + def represent_sequence(self, tag, sequence, flow_style=None): + value = [] + node = SequenceNode(tag, value, flow_style=flow_style) + if self.alias_key is not None: + self.represented_objects[self.alias_key] = node + best_style = True + for item in sequence: + node_item = self.represent_data(item) + if not (isinstance(node_item, ScalarNode) and not node_item.style): + best_style = False + value.append(node_item) + if flow_style is None: + if self.default_flow_style is not None: + node.flow_style = self.default_flow_style + else: + node.flow_style = best_style + return node + + def represent_mapping(self, tag, mapping, flow_style=None): + value = [] + node = MappingNode(tag, value, flow_style=flow_style) + if self.alias_key is not None: + self.represented_objects[self.alias_key] = node + best_style = True + if hasattr(mapping, 'items'): + mapping = mapping.items() + mapping.sort() + for item_key, item_value in mapping: + node_key = self.represent_data(item_key) + node_value = self.represent_data(item_value) + if not (isinstance(node_key, ScalarNode) and not node_key.style): + best_style = False + if not (isinstance(node_value, ScalarNode) and not node_value.style): + best_style = False + value.append((node_key, node_value)) + if flow_style is None: + if self.default_flow_style is not None: + node.flow_style = self.default_flow_style + else: + node.flow_style = best_style + return node + + def ignore_aliases(self, data): + return False + +class SafeRepresenter(BaseRepresenter): + + def ignore_aliases(self, data): + if data in [None, ()]: + return True + if isinstance(data, (str, unicode, bool, int, float)): + return True + + def represent_none(self, data): + return self.represent_scalar(u'tag:yaml.org,2002:null', + u'null') + + def represent_str(self, data): + tag = None + style = None + try: + data = unicode(data, 'ascii') + tag = u'tag:yaml.org,2002:str' + except UnicodeDecodeError: + try: + data = unicode(data, 'utf-8') + tag = u'tag:yaml.org,2002:str' + except UnicodeDecodeError: + data = data.encode('base64') + tag = u'tag:yaml.org,2002:binary' + style = '|' + return self.represent_scalar(tag, data, style=style) + + def represent_unicode(self, data): + return self.represent_scalar(u'tag:yaml.org,2002:str', data) + + def represent_bool(self, data): + if data: + value = u'true' + else: + value = u'false' + return self.represent_scalar(u'tag:yaml.org,2002:bool', value) + + def represent_int(self, data): + return self.represent_scalar(u'tag:yaml.org,2002:int', unicode(data)) + + def represent_long(self, data): + return self.represent_scalar(u'tag:yaml.org,2002:int', unicode(data)) + + inf_value = 1e300 + while repr(inf_value) != repr(inf_value*inf_value): + inf_value *= inf_value + + def represent_float(self, data): + if data != data or (data == 0.0 and data == 1.0): + value = u'.nan' + elif data == self.inf_value: + value = u'.inf' + elif data == -self.inf_value: + value = u'-.inf' + else: + value = unicode(repr(data)).lower() + # Note that in some cases `repr(data)` represents a float number + # without the decimal parts. For instance: + # >>> repr(1e17) + # '1e17' + # Unfortunately, this is not a valid float representation according + # to the definition of the `!!float` tag. We fix this by adding + # '.0' before the 'e' symbol. + if u'.' not in value and u'e' in value: + value = value.replace(u'e', u'.0e', 1) + return self.represent_scalar(u'tag:yaml.org,2002:float', value) + + def represent_list(self, data): + #pairs = (len(data) > 0 and isinstance(data, list)) + #if pairs: + # for item in data: + # if not isinstance(item, tuple) or len(item) != 2: + # pairs = False + # break + #if not pairs: + return self.represent_sequence(u'tag:yaml.org,2002:seq', data) + #value = [] + #for item_key, item_value in data: + # value.append(self.represent_mapping(u'tag:yaml.org,2002:map', + # [(item_key, item_value)])) + #return SequenceNode(u'tag:yaml.org,2002:pairs', value) + + def represent_dict(self, data): + return self.represent_mapping(u'tag:yaml.org,2002:map', data) + + def represent_set(self, data): + value = {} + for key in data: + value[key] = None + return self.represent_mapping(u'tag:yaml.org,2002:set', value) + + def represent_date(self, data): + value = unicode(data.isoformat()) + return self.represent_scalar(u'tag:yaml.org,2002:timestamp', value) + + def represent_datetime(self, data): + value = unicode(data.isoformat(' ')) + return self.represent_scalar(u'tag:yaml.org,2002:timestamp', value) + + def represent_yaml_object(self, tag, data, cls, flow_style=None): + if hasattr(data, '__getstate__'): + state = data.__getstate__() + else: + state = data.__dict__.copy() + return self.represent_mapping(tag, state, flow_style=flow_style) + + def represent_undefined(self, data): + raise RepresenterError("cannot represent an object: %s" % data) + +SafeRepresenter.add_representer(type(None), + SafeRepresenter.represent_none) + +SafeRepresenter.add_representer(str, + SafeRepresenter.represent_str) + +SafeRepresenter.add_representer(unicode, + SafeRepresenter.represent_unicode) + +SafeRepresenter.add_representer(bool, + SafeRepresenter.represent_bool) + +SafeRepresenter.add_representer(int, + SafeRepresenter.represent_int) + +SafeRepresenter.add_representer(long, + SafeRepresenter.represent_long) + +SafeRepresenter.add_representer(float, + SafeRepresenter.represent_float) + +SafeRepresenter.add_representer(list, + SafeRepresenter.represent_list) + +SafeRepresenter.add_representer(tuple, + SafeRepresenter.represent_list) + +SafeRepresenter.add_representer(dict, + SafeRepresenter.represent_dict) + +SafeRepresenter.add_representer(set, + SafeRepresenter.represent_set) + +SafeRepresenter.add_representer(datetime.date, + SafeRepresenter.represent_date) + +SafeRepresenter.add_representer(datetime.datetime, + SafeRepresenter.represent_datetime) + +SafeRepresenter.add_representer(None, + SafeRepresenter.represent_undefined) + +class Representer(SafeRepresenter): + + def represent_str(self, data): + tag = None + style = None + try: + data = unicode(data, 'ascii') + tag = u'tag:yaml.org,2002:str' + except UnicodeDecodeError: + try: + data = unicode(data, 'utf-8') + tag = u'tag:yaml.org,2002:python/str' + except UnicodeDecodeError: + data = data.encode('base64') + tag = u'tag:yaml.org,2002:binary' + style = '|' + return self.represent_scalar(tag, data, style=style) + + def represent_unicode(self, data): + tag = None + try: + data.encode('ascii') + tag = u'tag:yaml.org,2002:python/unicode' + except UnicodeEncodeError: + tag = u'tag:yaml.org,2002:str' + return self.represent_scalar(tag, data) + + def represent_long(self, data): + tag = u'tag:yaml.org,2002:int' + if int(data) is not data: + tag = u'tag:yaml.org,2002:python/long' + return self.represent_scalar(tag, unicode(data)) + + def represent_complex(self, data): + if data.imag == 0.0: + data = u'%r' % data.real + elif data.real == 0.0: + data = u'%rj' % data.imag + elif data.imag > 0: + data = u'%r+%rj' % (data.real, data.imag) + else: + data = u'%r%rj' % (data.real, data.imag) + return self.represent_scalar(u'tag:yaml.org,2002:python/complex', data) + + def represent_tuple(self, data): + return self.represent_sequence(u'tag:yaml.org,2002:python/tuple', data) + + def represent_name(self, data): + name = u'%s.%s' % (data.__module__, data.__name__) + return self.represent_scalar(u'tag:yaml.org,2002:python/name:'+name, u'') + + def represent_module(self, data): + return self.represent_scalar( + u'tag:yaml.org,2002:python/module:'+data.__name__, u'') + + def represent_instance(self, data): + # For instances of classic classes, we use __getinitargs__ and + # __getstate__ to serialize the data. + + # If data.__getinitargs__ exists, the object must be reconstructed by + # calling cls(**args), where args is a tuple returned by + # __getinitargs__. Otherwise, the cls.__init__ method should never be + # called and the class instance is created by instantiating a trivial + # class and assigning to the instance's __class__ variable. + + # If data.__getstate__ exists, it returns the state of the object. + # Otherwise, the state of the object is data.__dict__. + + # We produce either a !!python/object or !!python/object/new node. + # If data.__getinitargs__ does not exist and state is a dictionary, we + # produce a !!python/object node . Otherwise we produce a + # !!python/object/new node. + + cls = data.__class__ + class_name = u'%s.%s' % (cls.__module__, cls.__name__) + args = None + state = None + if hasattr(data, '__getinitargs__'): + args = list(data.__getinitargs__()) + if hasattr(data, '__getstate__'): + state = data.__getstate__() + else: + state = data.__dict__ + if args is None and isinstance(state, dict): + return self.represent_mapping( + u'tag:yaml.org,2002:python/object:'+class_name, state) + if isinstance(state, dict) and not state: + return self.represent_sequence( + u'tag:yaml.org,2002:python/object/new:'+class_name, args) + value = {} + if args: + value['args'] = args + value['state'] = state + return self.represent_mapping( + u'tag:yaml.org,2002:python/object/new:'+class_name, value) + + def represent_object(self, data): + # We use __reduce__ API to save the data. data.__reduce__ returns + # a tuple of length 2-5: + # (function, args, state, listitems, dictitems) + + # For reconstructing, we calls function(*args), then set its state, + # listitems, and dictitems if they are not None. + + # A special case is when function.__name__ == '__newobj__'. In this + # case we create the object with args[0].__new__(*args). + + # Another special case is when __reduce__ returns a string - we don't + # support it. + + # We produce a !!python/object, !!python/object/new or + # !!python/object/apply node. + + cls = type(data) + if cls in copy_reg.dispatch_table: + reduce = copy_reg.dispatch_table[cls](data) + elif hasattr(data, '__reduce_ex__'): + reduce = data.__reduce_ex__(2) + elif hasattr(data, '__reduce__'): + reduce = data.__reduce__() + else: + raise RepresenterError("cannot represent object: %r" % data) + reduce = (list(reduce)+[None]*5)[:5] + function, args, state, listitems, dictitems = reduce + args = list(args) + if state is None: + state = {} + if listitems is not None: + listitems = list(listitems) + if dictitems is not None: + dictitems = dict(dictitems) + if function.__name__ == '__newobj__': + function = args[0] + args = args[1:] + tag = u'tag:yaml.org,2002:python/object/new:' + newobj = True + else: + tag = u'tag:yaml.org,2002:python/object/apply:' + newobj = False + function_name = u'%s.%s' % (function.__module__, function.__name__) + if not args and not listitems and not dictitems \ + and isinstance(state, dict) and newobj: + return self.represent_mapping( + u'tag:yaml.org,2002:python/object:'+function_name, state) + if not listitems and not dictitems \ + and isinstance(state, dict) and not state: + return self.represent_sequence(tag+function_name, args) + value = {} + if args: + value['args'] = args + if state or not isinstance(state, dict): + value['state'] = state + if listitems: + value['listitems'] = listitems + if dictitems: + value['dictitems'] = dictitems + return self.represent_mapping(tag+function_name, value) + +Representer.add_representer(str, + Representer.represent_str) + +Representer.add_representer(unicode, + Representer.represent_unicode) + +Representer.add_representer(long, + Representer.represent_long) + +Representer.add_representer(complex, + Representer.represent_complex) + +Representer.add_representer(tuple, + Representer.represent_tuple) + +Representer.add_representer(type, + Representer.represent_name) + +Representer.add_representer(types.ClassType, + Representer.represent_name) + +Representer.add_representer(types.FunctionType, + Representer.represent_name) + +Representer.add_representer(types.BuiltinFunctionType, + Representer.represent_name) + +Representer.add_representer(types.ModuleType, + Representer.represent_module) + +Representer.add_multi_representer(types.InstanceType, + Representer.represent_instance) + +Representer.add_multi_representer(object, + Representer.represent_object) + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py2/resolver.py b/RenderPipelineFile/rplibs/yaml/yaml_py2/resolver.py index 6b5ab875..73d81cf5 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py2/resolver.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py2/resolver.py @@ -1,224 +1,224 @@ - -__all__ = ['BaseResolver', 'Resolver'] - -from error import * -from nodes import * - -import re - -class ResolverError(YAMLError): - pass - -class BaseResolver(object): - - DEFAULT_SCALAR_TAG = u'tag:yaml.org,2002:str' - DEFAULT_SEQUENCE_TAG = u'tag:yaml.org,2002:seq' - DEFAULT_MAPPING_TAG = u'tag:yaml.org,2002:map' - - yaml_implicit_resolvers = {} - yaml_path_resolvers = {} - - def __init__(self): - self.resolver_exact_paths = [] - self.resolver_prefix_paths = [] - - def add_implicit_resolver(cls, tag, regexp, first): - if not 'yaml_implicit_resolvers' in cls.__dict__: - cls.yaml_implicit_resolvers = cls.yaml_implicit_resolvers.copy() - if first is None: - first = [None] - for ch in first: - cls.yaml_implicit_resolvers.setdefault(ch, []).append((tag, regexp)) - add_implicit_resolver = classmethod(add_implicit_resolver) - - def add_path_resolver(cls, tag, path, kind=None): - # Note: `add_path_resolver` is experimental. The API could be changed. - # `new_path` is a pattern that is matched against the path from the - # root to the node that is being considered. `node_path` elements are - # tuples `(node_check, index_check)`. `node_check` is a node class: - # `ScalarNode`, `SequenceNode`, `MappingNode` or `None`. `None` - # matches any kind of a node. `index_check` could be `None`, a boolean - # value, a string value, or a number. `None` and `False` match against - # any _value_ of sequence and mapping nodes. `True` matches against - # any _key_ of a mapping node. A string `index_check` matches against - # a mapping value that corresponds to a scalar key which content is - # equal to the `index_check` value. An integer `index_check` matches - # against a sequence value with the index equal to `index_check`. - if not 'yaml_path_resolvers' in cls.__dict__: - cls.yaml_path_resolvers = cls.yaml_path_resolvers.copy() - new_path = [] - for element in path: - if isinstance(element, (list, tuple)): - if len(element) == 2: - node_check, index_check = element - elif len(element) == 1: - node_check = element[0] - index_check = True - else: - raise ResolverError("Invalid path element: %s" % element) - else: - node_check = None - index_check = element - if node_check is str: - node_check = ScalarNode - elif node_check is list: - node_check = SequenceNode - elif node_check is dict: - node_check = MappingNode - elif node_check not in [ScalarNode, SequenceNode, MappingNode] \ - and not isinstance(node_check, basestring) \ - and node_check is not None: - raise ResolverError("Invalid node checker: %s" % node_check) - if not isinstance(index_check, (basestring, int)) \ - and index_check is not None: - raise ResolverError("Invalid index checker: %s" % index_check) - new_path.append((node_check, index_check)) - if kind is str: - kind = ScalarNode - elif kind is list: - kind = SequenceNode - elif kind is dict: - kind = MappingNode - elif kind not in [ScalarNode, SequenceNode, MappingNode] \ - and kind is not None: - raise ResolverError("Invalid node kind: %s" % kind) - cls.yaml_path_resolvers[tuple(new_path), kind] = tag - add_path_resolver = classmethod(add_path_resolver) - - def descend_resolver(self, current_node, current_index): - if not self.yaml_path_resolvers: - return - exact_paths = {} - prefix_paths = [] - if current_node: - depth = len(self.resolver_prefix_paths) - for path, kind in self.resolver_prefix_paths[-1]: - if self.check_resolver_prefix(depth, path, kind, - current_node, current_index): - if len(path) > depth: - prefix_paths.append((path, kind)) - else: - exact_paths[kind] = self.yaml_path_resolvers[path, kind] - else: - for path, kind in self.yaml_path_resolvers: - if not path: - exact_paths[kind] = self.yaml_path_resolvers[path, kind] - else: - prefix_paths.append((path, kind)) - self.resolver_exact_paths.append(exact_paths) - self.resolver_prefix_paths.append(prefix_paths) - - def ascend_resolver(self): - if not self.yaml_path_resolvers: - return - self.resolver_exact_paths.pop() - self.resolver_prefix_paths.pop() - - def check_resolver_prefix(self, depth, path, kind, - current_node, current_index): - node_check, index_check = path[depth-1] - if isinstance(node_check, basestring): - if current_node.tag != node_check: - return - elif node_check is not None: - if not isinstance(current_node, node_check): - return - if index_check is True and current_index is not None: - return - if (index_check is False or index_check is None) \ - and current_index is None: - return - if isinstance(index_check, basestring): - if not (isinstance(current_index, ScalarNode) - and index_check == current_index.value): - return - elif isinstance(index_check, int) and not isinstance(index_check, bool): - if index_check != current_index: - return - return True - - def resolve(self, kind, value, implicit): - if kind is ScalarNode and implicit[0]: - if value == u'': - resolvers = self.yaml_implicit_resolvers.get(u'', []) - else: - resolvers = self.yaml_implicit_resolvers.get(value[0], []) - resolvers += self.yaml_implicit_resolvers.get(None, []) - for tag, regexp in resolvers: - if regexp.match(value): - return tag - implicit = implicit[1] - if self.yaml_path_resolvers: - exact_paths = self.resolver_exact_paths[-1] - if kind in exact_paths: - return exact_paths[kind] - if None in exact_paths: - return exact_paths[None] - if kind is ScalarNode: - return self.DEFAULT_SCALAR_TAG - elif kind is SequenceNode: - return self.DEFAULT_SEQUENCE_TAG - elif kind is MappingNode: - return self.DEFAULT_MAPPING_TAG - -class Resolver(BaseResolver): - pass - -Resolver.add_implicit_resolver( - u'tag:yaml.org,2002:bool', - re.compile(ur'''^(?:yes|Yes|YES|no|No|NO - |true|True|TRUE|false|False|FALSE - |on|On|ON|off|Off|OFF)$''', re.X), - list(u'yYnNtTfFoO')) - -Resolver.add_implicit_resolver( - u'tag:yaml.org,2002:float', - re.compile(ur'''^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+][0-9]+)? - |\.[0-9_]+(?:[eE][-+][0-9]+)? - |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]* - |[-+]?\.(?:inf|Inf|INF) - |\.(?:nan|NaN|NAN))$''', re.X), - list(u'-+0123456789.')) - -Resolver.add_implicit_resolver( - u'tag:yaml.org,2002:int', - re.compile(ur'''^(?:[-+]?0b[0-1_]+ - |[-+]?0[0-7_]+ - |[-+]?(?:0|[1-9][0-9_]*) - |[-+]?0x[0-9a-fA-F_]+ - |[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$''', re.X), - list(u'-+0123456789')) - -Resolver.add_implicit_resolver( - u'tag:yaml.org,2002:merge', - re.compile(ur'^(?:<<)$'), - [u'<']) - -Resolver.add_implicit_resolver( - u'tag:yaml.org,2002:null', - re.compile(ur'''^(?: ~ - |null|Null|NULL - | )$''', re.X), - [u'~', u'n', u'N', u'']) - -Resolver.add_implicit_resolver( - u'tag:yaml.org,2002:timestamp', - re.compile(ur'''^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] - |[0-9][0-9][0-9][0-9] -[0-9][0-9]? -[0-9][0-9]? - (?:[Tt]|[ \t]+)[0-9][0-9]? - :[0-9][0-9] :[0-9][0-9] (?:\.[0-9]*)? - (?:[ \t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$''', re.X), - list(u'0123456789')) - -Resolver.add_implicit_resolver( - u'tag:yaml.org,2002:value', - re.compile(ur'^(?:=)$'), - [u'=']) - -# The following resolver is only for documentation purposes. It cannot work -# because plain scalars cannot start with '!', '&', or '*'. -Resolver.add_implicit_resolver( - u'tag:yaml.org,2002:yaml', - re.compile(ur'^(?:!|&|\*)$'), - list(u'!&*')) - + +__all__ = ['BaseResolver', 'Resolver'] + +from error import * +from nodes import * + +import re + +class ResolverError(YAMLError): + pass + +class BaseResolver(object): + + DEFAULT_SCALAR_TAG = u'tag:yaml.org,2002:str' + DEFAULT_SEQUENCE_TAG = u'tag:yaml.org,2002:seq' + DEFAULT_MAPPING_TAG = u'tag:yaml.org,2002:map' + + yaml_implicit_resolvers = {} + yaml_path_resolvers = {} + + def __init__(self): + self.resolver_exact_paths = [] + self.resolver_prefix_paths = [] + + def add_implicit_resolver(cls, tag, regexp, first): + if not 'yaml_implicit_resolvers' in cls.__dict__: + cls.yaml_implicit_resolvers = cls.yaml_implicit_resolvers.copy() + if first is None: + first = [None] + for ch in first: + cls.yaml_implicit_resolvers.setdefault(ch, []).append((tag, regexp)) + add_implicit_resolver = classmethod(add_implicit_resolver) + + def add_path_resolver(cls, tag, path, kind=None): + # Note: `add_path_resolver` is experimental. The API could be changed. + # `new_path` is a pattern that is matched against the path from the + # root to the node that is being considered. `node_path` elements are + # tuples `(node_check, index_check)`. `node_check` is a node class: + # `ScalarNode`, `SequenceNode`, `MappingNode` or `None`. `None` + # matches any kind of a node. `index_check` could be `None`, a boolean + # value, a string value, or a number. `None` and `False` match against + # any _value_ of sequence and mapping nodes. `True` matches against + # any _key_ of a mapping node. A string `index_check` matches against + # a mapping value that corresponds to a scalar key which content is + # equal to the `index_check` value. An integer `index_check` matches + # against a sequence value with the index equal to `index_check`. + if not 'yaml_path_resolvers' in cls.__dict__: + cls.yaml_path_resolvers = cls.yaml_path_resolvers.copy() + new_path = [] + for element in path: + if isinstance(element, (list, tuple)): + if len(element) == 2: + node_check, index_check = element + elif len(element) == 1: + node_check = element[0] + index_check = True + else: + raise ResolverError("Invalid path element: %s" % element) + else: + node_check = None + index_check = element + if node_check is str: + node_check = ScalarNode + elif node_check is list: + node_check = SequenceNode + elif node_check is dict: + node_check = MappingNode + elif node_check not in [ScalarNode, SequenceNode, MappingNode] \ + and not isinstance(node_check, basestring) \ + and node_check is not None: + raise ResolverError("Invalid node checker: %s" % node_check) + if not isinstance(index_check, (basestring, int)) \ + and index_check is not None: + raise ResolverError("Invalid index checker: %s" % index_check) + new_path.append((node_check, index_check)) + if kind is str: + kind = ScalarNode + elif kind is list: + kind = SequenceNode + elif kind is dict: + kind = MappingNode + elif kind not in [ScalarNode, SequenceNode, MappingNode] \ + and kind is not None: + raise ResolverError("Invalid node kind: %s" % kind) + cls.yaml_path_resolvers[tuple(new_path), kind] = tag + add_path_resolver = classmethod(add_path_resolver) + + def descend_resolver(self, current_node, current_index): + if not self.yaml_path_resolvers: + return + exact_paths = {} + prefix_paths = [] + if current_node: + depth = len(self.resolver_prefix_paths) + for path, kind in self.resolver_prefix_paths[-1]: + if self.check_resolver_prefix(depth, path, kind, + current_node, current_index): + if len(path) > depth: + prefix_paths.append((path, kind)) + else: + exact_paths[kind] = self.yaml_path_resolvers[path, kind] + else: + for path, kind in self.yaml_path_resolvers: + if not path: + exact_paths[kind] = self.yaml_path_resolvers[path, kind] + else: + prefix_paths.append((path, kind)) + self.resolver_exact_paths.append(exact_paths) + self.resolver_prefix_paths.append(prefix_paths) + + def ascend_resolver(self): + if not self.yaml_path_resolvers: + return + self.resolver_exact_paths.pop() + self.resolver_prefix_paths.pop() + + def check_resolver_prefix(self, depth, path, kind, + current_node, current_index): + node_check, index_check = path[depth-1] + if isinstance(node_check, basestring): + if current_node.tag != node_check: + return + elif node_check is not None: + if not isinstance(current_node, node_check): + return + if index_check is True and current_index is not None: + return + if (index_check is False or index_check is None) \ + and current_index is None: + return + if isinstance(index_check, basestring): + if not (isinstance(current_index, ScalarNode) + and index_check == current_index.value): + return + elif isinstance(index_check, int) and not isinstance(index_check, bool): + if index_check != current_index: + return + return True + + def resolve(self, kind, value, implicit): + if kind is ScalarNode and implicit[0]: + if value == u'': + resolvers = self.yaml_implicit_resolvers.get(u'', []) + else: + resolvers = self.yaml_implicit_resolvers.get(value[0], []) + resolvers += self.yaml_implicit_resolvers.get(None, []) + for tag, regexp in resolvers: + if regexp.match(value): + return tag + implicit = implicit[1] + if self.yaml_path_resolvers: + exact_paths = self.resolver_exact_paths[-1] + if kind in exact_paths: + return exact_paths[kind] + if None in exact_paths: + return exact_paths[None] + if kind is ScalarNode: + return self.DEFAULT_SCALAR_TAG + elif kind is SequenceNode: + return self.DEFAULT_SEQUENCE_TAG + elif kind is MappingNode: + return self.DEFAULT_MAPPING_TAG + +class Resolver(BaseResolver): + pass + +Resolver.add_implicit_resolver( + u'tag:yaml.org,2002:bool', + re.compile(ur'''^(?:yes|Yes|YES|no|No|NO + |true|True|TRUE|false|False|FALSE + |on|On|ON|off|Off|OFF)$''', re.X), + list(u'yYnNtTfFoO')) + +Resolver.add_implicit_resolver( + u'tag:yaml.org,2002:float', + re.compile(ur'''^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+][0-9]+)? + |\.[0-9_]+(?:[eE][-+][0-9]+)? + |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]* + |[-+]?\.(?:inf|Inf|INF) + |\.(?:nan|NaN|NAN))$''', re.X), + list(u'-+0123456789.')) + +Resolver.add_implicit_resolver( + u'tag:yaml.org,2002:int', + re.compile(ur'''^(?:[-+]?0b[0-1_]+ + |[-+]?0[0-7_]+ + |[-+]?(?:0|[1-9][0-9_]*) + |[-+]?0x[0-9a-fA-F_]+ + |[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$''', re.X), + list(u'-+0123456789')) + +Resolver.add_implicit_resolver( + u'tag:yaml.org,2002:merge', + re.compile(ur'^(?:<<)$'), + [u'<']) + +Resolver.add_implicit_resolver( + u'tag:yaml.org,2002:null', + re.compile(ur'''^(?: ~ + |null|Null|NULL + | )$''', re.X), + [u'~', u'n', u'N', u'']) + +Resolver.add_implicit_resolver( + u'tag:yaml.org,2002:timestamp', + re.compile(ur'''^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] + |[0-9][0-9][0-9][0-9] -[0-9][0-9]? -[0-9][0-9]? + (?:[Tt]|[ \t]+)[0-9][0-9]? + :[0-9][0-9] :[0-9][0-9] (?:\.[0-9]*)? + (?:[ \t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$''', re.X), + list(u'0123456789')) + +Resolver.add_implicit_resolver( + u'tag:yaml.org,2002:value', + re.compile(ur'^(?:=)$'), + [u'=']) + +# The following resolver is only for documentation purposes. It cannot work +# because plain scalars cannot start with '!', '&', or '*'. +Resolver.add_implicit_resolver( + u'tag:yaml.org,2002:yaml', + re.compile(ur'^(?:!|&|\*)$'), + list(u'!&*')) + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py2/scanner.py b/RenderPipelineFile/rplibs/yaml/yaml_py2/scanner.py index 5228fad6..a5fd8424 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py2/scanner.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py2/scanner.py @@ -1,1457 +1,1457 @@ - -# Scanner produces tokens of the following types: -# STREAM-START -# STREAM-END -# DIRECTIVE(name, value) -# DOCUMENT-START -# DOCUMENT-END -# BLOCK-SEQUENCE-START -# BLOCK-MAPPING-START -# BLOCK-END -# FLOW-SEQUENCE-START -# FLOW-MAPPING-START -# FLOW-SEQUENCE-END -# FLOW-MAPPING-END -# BLOCK-ENTRY -# FLOW-ENTRY -# KEY -# VALUE -# ALIAS(value) -# ANCHOR(value) -# TAG(value) -# SCALAR(value, plain, style) -# -# Read comments in the Scanner code for more details. -# - -__all__ = ['Scanner', 'ScannerError'] - -from error import MarkedYAMLError -from tokens import * - -class ScannerError(MarkedYAMLError): - pass - -class SimpleKey(object): - # See below simple keys treatment. - - def __init__(self, token_number, required, index, line, column, mark): - self.token_number = token_number - self.required = required - self.index = index - self.line = line - self.column = column - self.mark = mark - -class Scanner(object): - - def __init__(self): - """Initialize the scanner.""" - # It is assumed that Scanner and Reader will have a common descendant. - # Reader do the dirty work of checking for BOM and converting the - # input data to Unicode. It also adds NUL to the end. - # - # Reader supports the following methods - # self.peek(i=0) # peek the next i-th character - # self.prefix(l=1) # peek the next l characters - # self.forward(l=1) # read the next l characters and move the pointer. - - # Had we reached the end of the stream? - self.done = False - - # The number of unclosed '{' and '['. `flow_level == 0` means block - # context. - self.flow_level = 0 - - # List of processed tokens that are not yet emitted. - self.tokens = [] - - # Add the STREAM-START token. - self.fetch_stream_start() - - # Number of tokens that were emitted through the `get_token` method. - self.tokens_taken = 0 - - # The current indentation level. - self.indent = -1 - - # Past indentation levels. - self.indents = [] - - # Variables related to simple keys treatment. - - # A simple key is a key that is not denoted by the '?' indicator. - # Example of simple keys: - # --- - # block simple key: value - # ? not a simple key: - # : { flow simple key: value } - # We emit the KEY token before all keys, so when we find a potential - # simple key, we try to locate the corresponding ':' indicator. - # Simple keys should be limited to a single line and 1024 characters. - - # Can a simple key start at the current position? A simple key may - # start: - # - at the beginning of the line, not counting indentation spaces - # (in block context), - # - after '{', '[', ',' (in the flow context), - # - after '?', ':', '-' (in the block context). - # In the block context, this flag also signifies if a block collection - # may start at the current position. - self.allow_simple_key = True - - # Keep track of possible simple keys. This is a dictionary. The key - # is `flow_level`; there can be no more that one possible simple key - # for each level. The value is a SimpleKey record: - # (token_number, required, index, line, column, mark) - # A simple key may start with ALIAS, ANCHOR, TAG, SCALAR(flow), - # '[', or '{' tokens. - self.possible_simple_keys = {} - - # Public methods. - - def check_token(self, *choices): - # Check if the next token is one of the given types. - while self.need_more_tokens(): - self.fetch_more_tokens() - if self.tokens: - if not choices: - return True - for choice in choices: - if isinstance(self.tokens[0], choice): - return True - return False - - def peek_token(self): - # Return the next token, but do not delete if from the queue. - while self.need_more_tokens(): - self.fetch_more_tokens() - if self.tokens: - return self.tokens[0] - - def get_token(self): - # Return the next token. - while self.need_more_tokens(): - self.fetch_more_tokens() - if self.tokens: - self.tokens_taken += 1 - return self.tokens.pop(0) - - # Private methods. - - def need_more_tokens(self): - if self.done: - return False - if not self.tokens: - return True - # The current token may be a potential simple key, so we - # need to look further. - self.stale_possible_simple_keys() - if self.next_possible_simple_key() == self.tokens_taken: - return True - - def fetch_more_tokens(self): - - # Eat whitespaces and comments until we reach the next token. - self.scan_to_next_token() - - # Remove obsolete possible simple keys. - self.stale_possible_simple_keys() - - # Compare the current indentation and column. It may add some tokens - # and decrease the current indentation level. - self.unwind_indent(self.column) - - # Peek the next character. - ch = self.peek() - - # Is it the end of stream? - if ch == u'\0': - return self.fetch_stream_end() - - # Is it a directive? - if ch == u'%' and self.check_directive(): - return self.fetch_directive() - - # Is it the document start? - if ch == u'-' and self.check_document_start(): - return self.fetch_document_start() - - # Is it the document end? - if ch == u'.' and self.check_document_end(): - return self.fetch_document_end() - - # TODO: support for BOM within a stream. - #if ch == u'\uFEFF': - # return self.fetch_bom() <-- issue BOMToken - - # Note: the order of the following checks is NOT significant. - - # Is it the flow sequence start indicator? - if ch == u'[': - return self.fetch_flow_sequence_start() - - # Is it the flow mapping start indicator? - if ch == u'{': - return self.fetch_flow_mapping_start() - - # Is it the flow sequence end indicator? - if ch == u']': - return self.fetch_flow_sequence_end() - - # Is it the flow mapping end indicator? - if ch == u'}': - return self.fetch_flow_mapping_end() - - # Is it the flow entry indicator? - if ch == u',': - return self.fetch_flow_entry() - - # Is it the block entry indicator? - if ch == u'-' and self.check_block_entry(): - return self.fetch_block_entry() - - # Is it the key indicator? - if ch == u'?' and self.check_key(): - return self.fetch_key() - - # Is it the value indicator? - if ch == u':' and self.check_value(): - return self.fetch_value() - - # Is it an alias? - if ch == u'*': - return self.fetch_alias() - - # Is it an anchor? - if ch == u'&': - return self.fetch_anchor() - - # Is it a tag? - if ch == u'!': - return self.fetch_tag() - - # Is it a literal scalar? - if ch == u'|' and not self.flow_level: - return self.fetch_literal() - - # Is it a folded scalar? - if ch == u'>' and not self.flow_level: - return self.fetch_folded() - - # Is it a single quoted scalar? - if ch == u'\'': - return self.fetch_single() - - # Is it a double quoted scalar? - if ch == u'\"': - return self.fetch_double() - - # It must be a plain scalar then. - if self.check_plain(): - return self.fetch_plain() - - # No? It's an error. Let's produce a nice error message. - raise ScannerError("while scanning for the next token", None, - "found character %r that cannot start any token" - % ch.encode('utf-8'), self.get_mark()) - - # Simple keys treatment. - - def next_possible_simple_key(self): - # Return the number of the nearest possible simple key. Actually we - # don't need to loop through the whole dictionary. We may replace it - # with the following code: - # if not self.possible_simple_keys: - # return None - # return self.possible_simple_keys[ - # min(self.possible_simple_keys.keys())].token_number - min_token_number = None - for level in self.possible_simple_keys: - key = self.possible_simple_keys[level] - if min_token_number is None or key.token_number < min_token_number: - min_token_number = key.token_number - return min_token_number - - def stale_possible_simple_keys(self): - # Remove entries that are no longer possible simple keys. According to - # the YAML specification, simple keys - # - should be limited to a single line, - # - should be no longer than 1024 characters. - # Disabling this procedure will allow simple keys of any length and - # height (may cause problems if indentation is broken though). - for level in self.possible_simple_keys.keys(): - key = self.possible_simple_keys[level] - if key.line != self.line \ - or self.index-key.index > 1024: - if key.required: - raise ScannerError("while scanning a simple key", key.mark, - "could not found expected ':'", self.get_mark()) - del self.possible_simple_keys[level] - - def save_possible_simple_key(self): - # The next token may start a simple key. We check if it's possible - # and save its position. This function is called for - # ALIAS, ANCHOR, TAG, SCALAR(flow), '[', and '{'. - - # Check if a simple key is required at the current position. - required = not self.flow_level and self.indent == self.column - - # A simple key is required only if it is the first token in the current - # line. Therefore it is always allowed. - assert self.allow_simple_key or not required - - # The next token might be a simple key. Let's save it's number and - # position. - if self.allow_simple_key: - self.remove_possible_simple_key() - token_number = self.tokens_taken+len(self.tokens) - key = SimpleKey(token_number, required, - self.index, self.line, self.column, self.get_mark()) - self.possible_simple_keys[self.flow_level] = key - - def remove_possible_simple_key(self): - # Remove the saved possible key position at the current flow level. - if self.flow_level in self.possible_simple_keys: - key = self.possible_simple_keys[self.flow_level] - - if key.required: - raise ScannerError("while scanning a simple key", key.mark, - "could not found expected ':'", self.get_mark()) - - del self.possible_simple_keys[self.flow_level] - - # Indentation functions. - - def unwind_indent(self, column): - - ## In flow context, tokens should respect indentation. - ## Actually the condition should be `self.indent >= column` according to - ## the spec. But this condition will prohibit intuitively correct - ## constructions such as - ## key : { - ## } - #if self.flow_level and self.indent > column: - # raise ScannerError(None, None, - # "invalid intendation or unclosed '[' or '{'", - # self.get_mark()) - - # In the flow context, indentation is ignored. We make the scanner less - # restrictive then specification requires. - if self.flow_level: - return - - # In block context, we may need to issue the BLOCK-END tokens. - while self.indent > column: - mark = self.get_mark() - self.indent = self.indents.pop() - self.tokens.append(BlockEndToken(mark, mark)) - - def add_indent(self, column): - # Check if we need to increase indentation. - if self.indent < column: - self.indents.append(self.indent) - self.indent = column - return True - return False - - # Fetchers. - - def fetch_stream_start(self): - # We always add STREAM-START as the first token and STREAM-END as the - # last token. - - # Read the token. - mark = self.get_mark() - - # Add STREAM-START. - self.tokens.append(StreamStartToken(mark, mark, - encoding=self.encoding)) - - - def fetch_stream_end(self): - - # Set the current intendation to -1. - self.unwind_indent(-1) - - # Reset simple keys. - self.remove_possible_simple_key() - self.allow_simple_key = False - self.possible_simple_keys = {} - - # Read the token. - mark = self.get_mark() - - # Add STREAM-END. - self.tokens.append(StreamEndToken(mark, mark)) - - # The steam is finished. - self.done = True - - def fetch_directive(self): - - # Set the current intendation to -1. - self.unwind_indent(-1) - - # Reset simple keys. - self.remove_possible_simple_key() - self.allow_simple_key = False - - # Scan and add DIRECTIVE. - self.tokens.append(self.scan_directive()) - - def fetch_document_start(self): - self.fetch_document_indicator(DocumentStartToken) - - def fetch_document_end(self): - self.fetch_document_indicator(DocumentEndToken) - - def fetch_document_indicator(self, TokenClass): - - # Set the current intendation to -1. - self.unwind_indent(-1) - - # Reset simple keys. Note that there could not be a block collection - # after '---'. - self.remove_possible_simple_key() - self.allow_simple_key = False - - # Add DOCUMENT-START or DOCUMENT-END. - start_mark = self.get_mark() - self.forward(3) - end_mark = self.get_mark() - self.tokens.append(TokenClass(start_mark, end_mark)) - - def fetch_flow_sequence_start(self): - self.fetch_flow_collection_start(FlowSequenceStartToken) - - def fetch_flow_mapping_start(self): - self.fetch_flow_collection_start(FlowMappingStartToken) - - def fetch_flow_collection_start(self, TokenClass): - - # '[' and '{' may start a simple key. - self.save_possible_simple_key() - - # Increase the flow level. - self.flow_level += 1 - - # Simple keys are allowed after '[' and '{'. - self.allow_simple_key = True - - # Add FLOW-SEQUENCE-START or FLOW-MAPPING-START. - start_mark = self.get_mark() - self.forward() - end_mark = self.get_mark() - self.tokens.append(TokenClass(start_mark, end_mark)) - - def fetch_flow_sequence_end(self): - self.fetch_flow_collection_end(FlowSequenceEndToken) - - def fetch_flow_mapping_end(self): - self.fetch_flow_collection_end(FlowMappingEndToken) - - def fetch_flow_collection_end(self, TokenClass): - - # Reset possible simple key on the current level. - self.remove_possible_simple_key() - - # Decrease the flow level. - self.flow_level -= 1 - - # No simple keys after ']' or '}'. - self.allow_simple_key = False - - # Add FLOW-SEQUENCE-END or FLOW-MAPPING-END. - start_mark = self.get_mark() - self.forward() - end_mark = self.get_mark() - self.tokens.append(TokenClass(start_mark, end_mark)) - - def fetch_flow_entry(self): - - # Simple keys are allowed after ','. - self.allow_simple_key = True - - # Reset possible simple key on the current level. - self.remove_possible_simple_key() - - # Add FLOW-ENTRY. - start_mark = self.get_mark() - self.forward() - end_mark = self.get_mark() - self.tokens.append(FlowEntryToken(start_mark, end_mark)) - - def fetch_block_entry(self): - - # Block context needs additional checks. - if not self.flow_level: - - # Are we allowed to start a new entry? - if not self.allow_simple_key: - raise ScannerError(None, None, - "sequence entries are not allowed here", - self.get_mark()) - - # We may need to add BLOCK-SEQUENCE-START. - if self.add_indent(self.column): - mark = self.get_mark() - self.tokens.append(BlockSequenceStartToken(mark, mark)) - - # It's an error for the block entry to occur in the flow context, - # but we let the parser detect this. - else: - pass - - # Simple keys are allowed after '-'. - self.allow_simple_key = True - - # Reset possible simple key on the current level. - self.remove_possible_simple_key() - - # Add BLOCK-ENTRY. - start_mark = self.get_mark() - self.forward() - end_mark = self.get_mark() - self.tokens.append(BlockEntryToken(start_mark, end_mark)) - - def fetch_key(self): - - # Block context needs additional checks. - if not self.flow_level: - - # Are we allowed to start a key (not nessesary a simple)? - if not self.allow_simple_key: - raise ScannerError(None, None, - "mapping keys are not allowed here", - self.get_mark()) - - # We may need to add BLOCK-MAPPING-START. - if self.add_indent(self.column): - mark = self.get_mark() - self.tokens.append(BlockMappingStartToken(mark, mark)) - - # Simple keys are allowed after '?' in the block context. - self.allow_simple_key = not self.flow_level - - # Reset possible simple key on the current level. - self.remove_possible_simple_key() - - # Add KEY. - start_mark = self.get_mark() - self.forward() - end_mark = self.get_mark() - self.tokens.append(KeyToken(start_mark, end_mark)) - - def fetch_value(self): - - # Do we determine a simple key? - if self.flow_level in self.possible_simple_keys: - - # Add KEY. - key = self.possible_simple_keys[self.flow_level] - del self.possible_simple_keys[self.flow_level] - self.tokens.insert(key.token_number-self.tokens_taken, - KeyToken(key.mark, key.mark)) - - # If this key starts a new block mapping, we need to add - # BLOCK-MAPPING-START. - if not self.flow_level: - if self.add_indent(key.column): - self.tokens.insert(key.token_number-self.tokens_taken, - BlockMappingStartToken(key.mark, key.mark)) - - # There cannot be two simple keys one after another. - self.allow_simple_key = False - - # It must be a part of a complex key. - else: - - # Block context needs additional checks. - # (Do we really need them? They will be catched by the parser - # anyway.) - if not self.flow_level: - - # We are allowed to start a complex value if and only if - # we can start a simple key. - if not self.allow_simple_key: - raise ScannerError(None, None, - "mapping values are not allowed here", - self.get_mark()) - - # If this value starts a new block mapping, we need to add - # BLOCK-MAPPING-START. It will be detected as an error later by - # the parser. - if not self.flow_level: - if self.add_indent(self.column): - mark = self.get_mark() - self.tokens.append(BlockMappingStartToken(mark, mark)) - - # Simple keys are allowed after ':' in the block context. - self.allow_simple_key = not self.flow_level - - # Reset possible simple key on the current level. - self.remove_possible_simple_key() - - # Add VALUE. - start_mark = self.get_mark() - self.forward() - end_mark = self.get_mark() - self.tokens.append(ValueToken(start_mark, end_mark)) - - def fetch_alias(self): - - # ALIAS could be a simple key. - self.save_possible_simple_key() - - # No simple keys after ALIAS. - self.allow_simple_key = False - - # Scan and add ALIAS. - self.tokens.append(self.scan_anchor(AliasToken)) - - def fetch_anchor(self): - - # ANCHOR could start a simple key. - self.save_possible_simple_key() - - # No simple keys after ANCHOR. - self.allow_simple_key = False - - # Scan and add ANCHOR. - self.tokens.append(self.scan_anchor(AnchorToken)) - - def fetch_tag(self): - - # TAG could start a simple key. - self.save_possible_simple_key() - - # No simple keys after TAG. - self.allow_simple_key = False - - # Scan and add TAG. - self.tokens.append(self.scan_tag()) - - def fetch_literal(self): - self.fetch_block_scalar(style='|') - - def fetch_folded(self): - self.fetch_block_scalar(style='>') - - def fetch_block_scalar(self, style): - - # A simple key may follow a block scalar. - self.allow_simple_key = True - - # Reset possible simple key on the current level. - self.remove_possible_simple_key() - - # Scan and add SCALAR. - self.tokens.append(self.scan_block_scalar(style)) - - def fetch_single(self): - self.fetch_flow_scalar(style='\'') - - def fetch_double(self): - self.fetch_flow_scalar(style='"') - - def fetch_flow_scalar(self, style): - - # A flow scalar could be a simple key. - self.save_possible_simple_key() - - # No simple keys after flow scalars. - self.allow_simple_key = False - - # Scan and add SCALAR. - self.tokens.append(self.scan_flow_scalar(style)) - - def fetch_plain(self): - - # A plain scalar could be a simple key. - self.save_possible_simple_key() - - # No simple keys after plain scalars. But note that `scan_plain` will - # change this flag if the scan is finished at the beginning of the - # line. - self.allow_simple_key = False - - # Scan and add SCALAR. May change `allow_simple_key`. - self.tokens.append(self.scan_plain()) - - # Checkers. - - def check_directive(self): - - # DIRECTIVE: ^ '%' ... - # The '%' indicator is already checked. - if self.column == 0: - return True - - def check_document_start(self): - - # DOCUMENT-START: ^ '---' (' '|'\n') - if self.column == 0: - if self.prefix(3) == u'---' \ - and self.peek(3) in u'\0 \t\r\n\x85\u2028\u2029': - return True - - def check_document_end(self): - - # DOCUMENT-END: ^ '...' (' '|'\n') - if self.column == 0: - if self.prefix(3) == u'...' \ - and self.peek(3) in u'\0 \t\r\n\x85\u2028\u2029': - return True - - def check_block_entry(self): - - # BLOCK-ENTRY: '-' (' '|'\n') - return self.peek(1) in u'\0 \t\r\n\x85\u2028\u2029' - - def check_key(self): - - # KEY(flow context): '?' - if self.flow_level: - return True - - # KEY(block context): '?' (' '|'\n') - else: - return self.peek(1) in u'\0 \t\r\n\x85\u2028\u2029' - - def check_value(self): - - # VALUE(flow context): ':' - if self.flow_level: - return True - - # VALUE(block context): ':' (' '|'\n') - else: - return self.peek(1) in u'\0 \t\r\n\x85\u2028\u2029' - - def check_plain(self): - - # A plain scalar may start with any non-space character except: - # '-', '?', ':', ',', '[', ']', '{', '}', - # '#', '&', '*', '!', '|', '>', '\'', '\"', - # '%', '@', '`'. - # - # It may also start with - # '-', '?', ':' - # if it is followed by a non-space character. - # - # Note that we limit the last rule to the block context (except the - # '-' character) because we want the flow context to be space - # independent. - ch = self.peek() - return ch not in u'\0 \t\r\n\x85\u2028\u2029-?:,[]{}#&*!|>\'\"%@`' \ - or (self.peek(1) not in u'\0 \t\r\n\x85\u2028\u2029' - and (ch == u'-' or (not self.flow_level and ch in u'?:'))) - - # Scanners. - - def scan_to_next_token(self): - # We ignore spaces, line breaks and comments. - # If we find a line break in the block context, we set the flag - # `allow_simple_key` on. - # The byte order mark is stripped if it's the first character in the - # stream. We do not yet support BOM inside the stream as the - # specification requires. Any such mark will be considered as a part - # of the document. - # - # TODO: We need to make tab handling rules more sane. A good rule is - # Tabs cannot precede tokens - # BLOCK-SEQUENCE-START, BLOCK-MAPPING-START, BLOCK-END, - # KEY(block), VALUE(block), BLOCK-ENTRY - # So the checking code is - # if : - # self.allow_simple_keys = False - # We also need to add the check for `allow_simple_keys == True` to - # `unwind_indent` before issuing BLOCK-END. - # Scanners for block, flow, and plain scalars need to be modified. - - if self.index == 0 and self.peek() == u'\uFEFF': - self.forward() - found = False - while not found: - while self.peek() == u' ': - self.forward() - if self.peek() == u'#': - while self.peek() not in u'\0\r\n\x85\u2028\u2029': - self.forward() - if self.scan_line_break(): - if not self.flow_level: - self.allow_simple_key = True - else: - found = True - - def scan_directive(self): - # See the specification for details. - start_mark = self.get_mark() - self.forward() - name = self.scan_directive_name(start_mark) - value = None - if name == u'YAML': - value = self.scan_yaml_directive_value(start_mark) - end_mark = self.get_mark() - elif name == u'TAG': - value = self.scan_tag_directive_value(start_mark) - end_mark = self.get_mark() - else: - end_mark = self.get_mark() - while self.peek() not in u'\0\r\n\x85\u2028\u2029': - self.forward() - self.scan_directive_ignored_line(start_mark) - return DirectiveToken(name, value, start_mark, end_mark) - - def scan_directive_name(self, start_mark): - # See the specification for details. - length = 0 - ch = self.peek(length) - while u'0' <= ch <= u'9' or u'A' <= ch <= u'Z' or u'a' <= ch <= u'z' \ - or ch in u'-_': - length += 1 - ch = self.peek(length) - if not length: - raise ScannerError("while scanning a directive", start_mark, - "expected alphabetic or numeric character, but found %r" - % ch.encode('utf-8'), self.get_mark()) - value = self.prefix(length) - self.forward(length) - ch = self.peek() - if ch not in u'\0 \r\n\x85\u2028\u2029': - raise ScannerError("while scanning a directive", start_mark, - "expected alphabetic or numeric character, but found %r" - % ch.encode('utf-8'), self.get_mark()) - return value - - def scan_yaml_directive_value(self, start_mark): - # See the specification for details. - while self.peek() == u' ': - self.forward() - major = self.scan_yaml_directive_number(start_mark) - if self.peek() != '.': - raise ScannerError("while scanning a directive", start_mark, - "expected a digit or '.', but found %r" - % self.peek().encode('utf-8'), - self.get_mark()) - self.forward() - minor = self.scan_yaml_directive_number(start_mark) - if self.peek() not in u'\0 \r\n\x85\u2028\u2029': - raise ScannerError("while scanning a directive", start_mark, - "expected a digit or ' ', but found %r" - % self.peek().encode('utf-8'), - self.get_mark()) - return (major, minor) - - def scan_yaml_directive_number(self, start_mark): - # See the specification for details. - ch = self.peek() - if not (u'0' <= ch <= u'9'): - raise ScannerError("while scanning a directive", start_mark, - "expected a digit, but found %r" % ch.encode('utf-8'), - self.get_mark()) - length = 0 - while u'0' <= self.peek(length) <= u'9': - length += 1 - value = int(self.prefix(length)) - self.forward(length) - return value - - def scan_tag_directive_value(self, start_mark): - # See the specification for details. - while self.peek() == u' ': - self.forward() - handle = self.scan_tag_directive_handle(start_mark) - while self.peek() == u' ': - self.forward() - prefix = self.scan_tag_directive_prefix(start_mark) - return (handle, prefix) - - def scan_tag_directive_handle(self, start_mark): - # See the specification for details. - value = self.scan_tag_handle('directive', start_mark) - ch = self.peek() - if ch != u' ': - raise ScannerError("while scanning a directive", start_mark, - "expected ' ', but found %r" % ch.encode('utf-8'), - self.get_mark()) - return value - - def scan_tag_directive_prefix(self, start_mark): - # See the specification for details. - value = self.scan_tag_uri('directive', start_mark) - ch = self.peek() - if ch not in u'\0 \r\n\x85\u2028\u2029': - raise ScannerError("while scanning a directive", start_mark, - "expected ' ', but found %r" % ch.encode('utf-8'), - self.get_mark()) - return value - - def scan_directive_ignored_line(self, start_mark): - # See the specification for details. - while self.peek() == u' ': - self.forward() - if self.peek() == u'#': - while self.peek() not in u'\0\r\n\x85\u2028\u2029': - self.forward() - ch = self.peek() - if ch not in u'\0\r\n\x85\u2028\u2029': - raise ScannerError("while scanning a directive", start_mark, - "expected a comment or a line break, but found %r" - % ch.encode('utf-8'), self.get_mark()) - self.scan_line_break() - - def scan_anchor(self, TokenClass): - # The specification does not restrict characters for anchors and - # aliases. This may lead to problems, for instance, the document: - # [ *alias, value ] - # can be interpteted in two ways, as - # [ "value" ] - # and - # [ *alias , "value" ] - # Therefore we restrict aliases to numbers and ASCII letters. - start_mark = self.get_mark() - indicator = self.peek() - if indicator == u'*': - name = 'alias' - else: - name = 'anchor' - self.forward() - length = 0 - ch = self.peek(length) - while u'0' <= ch <= u'9' or u'A' <= ch <= u'Z' or u'a' <= ch <= u'z' \ - or ch in u'-_': - length += 1 - ch = self.peek(length) - if not length: - raise ScannerError("while scanning an %s" % name, start_mark, - "expected alphabetic or numeric character, but found %r" - % ch.encode('utf-8'), self.get_mark()) - value = self.prefix(length) - self.forward(length) - ch = self.peek() - if ch not in u'\0 \t\r\n\x85\u2028\u2029?:,]}%@`': - raise ScannerError("while scanning an %s" % name, start_mark, - "expected alphabetic or numeric character, but found %r" - % ch.encode('utf-8'), self.get_mark()) - end_mark = self.get_mark() - return TokenClass(value, start_mark, end_mark) - - def scan_tag(self): - # See the specification for details. - start_mark = self.get_mark() - ch = self.peek(1) - if ch == u'<': - handle = None - self.forward(2) - suffix = self.scan_tag_uri('tag', start_mark) - if self.peek() != u'>': - raise ScannerError("while parsing a tag", start_mark, - "expected '>', but found %r" % self.peek().encode('utf-8'), - self.get_mark()) - self.forward() - elif ch in u'\0 \t\r\n\x85\u2028\u2029': - handle = None - suffix = u'!' - self.forward() - else: - length = 1 - use_handle = False - while ch not in u'\0 \r\n\x85\u2028\u2029': - if ch == u'!': - use_handle = True - break - length += 1 - ch = self.peek(length) - handle = u'!' - if use_handle: - handle = self.scan_tag_handle('tag', start_mark) - else: - handle = u'!' - self.forward() - suffix = self.scan_tag_uri('tag', start_mark) - ch = self.peek() - if ch not in u'\0 \r\n\x85\u2028\u2029': - raise ScannerError("while scanning a tag", start_mark, - "expected ' ', but found %r" % ch.encode('utf-8'), - self.get_mark()) - value = (handle, suffix) - end_mark = self.get_mark() - return TagToken(value, start_mark, end_mark) - - def scan_block_scalar(self, style): - # See the specification for details. - - if style == '>': - folded = True - else: - folded = False - - chunks = [] - start_mark = self.get_mark() - - # Scan the header. - self.forward() - chomping, increment = self.scan_block_scalar_indicators(start_mark) - self.scan_block_scalar_ignored_line(start_mark) - - # Determine the indentation level and go to the first non-empty line. - min_indent = self.indent+1 - if min_indent < 1: - min_indent = 1 - if increment is None: - breaks, max_indent, end_mark = self.scan_block_scalar_indentation() - indent = max(min_indent, max_indent) - else: - indent = min_indent+increment-1 - breaks, end_mark = self.scan_block_scalar_breaks(indent) - line_break = u'' - - # Scan the inner part of the block scalar. - while self.column == indent and self.peek() != u'\0': - chunks.extend(breaks) - leading_non_space = self.peek() not in u' \t' - length = 0 - while self.peek(length) not in u'\0\r\n\x85\u2028\u2029': - length += 1 - chunks.append(self.prefix(length)) - self.forward(length) - line_break = self.scan_line_break() - breaks, end_mark = self.scan_block_scalar_breaks(indent) - if self.column == indent and self.peek() != u'\0': - - # Unfortunately, folding rules are ambiguous. - # - # This is the folding according to the specification: - - if folded and line_break == u'\n' \ - and leading_non_space and self.peek() not in u' \t': - if not breaks: - chunks.append(u' ') - else: - chunks.append(line_break) - - # This is Clark Evans's interpretation (also in the spec - # examples): - # - #if folded and line_break == u'\n': - # if not breaks: - # if self.peek() not in ' \t': - # chunks.append(u' ') - # else: - # chunks.append(line_break) - #else: - # chunks.append(line_break) - else: - break - - # Chomp the tail. - if chomping is not False: - chunks.append(line_break) - if chomping is True: - chunks.extend(breaks) - - # We are done. - return ScalarToken(u''.join(chunks), False, start_mark, end_mark, - style) - - def scan_block_scalar_indicators(self, start_mark): - # See the specification for details. - chomping = None - increment = None - ch = self.peek() - if ch in u'+-': - if ch == '+': - chomping = True - else: - chomping = False - self.forward() - ch = self.peek() - if ch in u'0123456789': - increment = int(ch) - if increment == 0: - raise ScannerError("while scanning a block scalar", start_mark, - "expected indentation indicator in the range 1-9, but found 0", - self.get_mark()) - self.forward() - elif ch in u'0123456789': - increment = int(ch) - if increment == 0: - raise ScannerError("while scanning a block scalar", start_mark, - "expected indentation indicator in the range 1-9, but found 0", - self.get_mark()) - self.forward() - ch = self.peek() - if ch in u'+-': - if ch == '+': - chomping = True - else: - chomping = False - self.forward() - ch = self.peek() - if ch not in u'\0 \r\n\x85\u2028\u2029': - raise ScannerError("while scanning a block scalar", start_mark, - "expected chomping or indentation indicators, but found %r" - % ch.encode('utf-8'), self.get_mark()) - return chomping, increment - - def scan_block_scalar_ignored_line(self, start_mark): - # See the specification for details. - while self.peek() == u' ': - self.forward() - if self.peek() == u'#': - while self.peek() not in u'\0\r\n\x85\u2028\u2029': - self.forward() - ch = self.peek() - if ch not in u'\0\r\n\x85\u2028\u2029': - raise ScannerError("while scanning a block scalar", start_mark, - "expected a comment or a line break, but found %r" - % ch.encode('utf-8'), self.get_mark()) - self.scan_line_break() - - def scan_block_scalar_indentation(self): - # See the specification for details. - chunks = [] - max_indent = 0 - end_mark = self.get_mark() - while self.peek() in u' \r\n\x85\u2028\u2029': - if self.peek() != u' ': - chunks.append(self.scan_line_break()) - end_mark = self.get_mark() - else: - self.forward() - if self.column > max_indent: - max_indent = self.column - return chunks, max_indent, end_mark - - def scan_block_scalar_breaks(self, indent): - # See the specification for details. - chunks = [] - end_mark = self.get_mark() - while self.column < indent and self.peek() == u' ': - self.forward() - while self.peek() in u'\r\n\x85\u2028\u2029': - chunks.append(self.scan_line_break()) - end_mark = self.get_mark() - while self.column < indent and self.peek() == u' ': - self.forward() - return chunks, end_mark - - def scan_flow_scalar(self, style): - # See the specification for details. - # Note that we loose indentation rules for quoted scalars. Quoted - # scalars don't need to adhere indentation because " and ' clearly - # mark the beginning and the end of them. Therefore we are less - # restrictive then the specification requires. We only need to check - # that document separators are not included in scalars. - if style == '"': - double = True - else: - double = False - chunks = [] - start_mark = self.get_mark() - quote = self.peek() - self.forward() - chunks.extend(self.scan_flow_scalar_non_spaces(double, start_mark)) - while self.peek() != quote: - chunks.extend(self.scan_flow_scalar_spaces(double, start_mark)) - chunks.extend(self.scan_flow_scalar_non_spaces(double, start_mark)) - self.forward() - end_mark = self.get_mark() - return ScalarToken(u''.join(chunks), False, start_mark, end_mark, - style) - - ESCAPE_REPLACEMENTS = { - u'0': u'\0', - u'a': u'\x07', - u'b': u'\x08', - u't': u'\x09', - u'\t': u'\x09', - u'n': u'\x0A', - u'v': u'\x0B', - u'f': u'\x0C', - u'r': u'\x0D', - u'e': u'\x1B', - u' ': u'\x20', - u'\"': u'\"', - u'\\': u'\\', - u'N': u'\x85', - u'_': u'\xA0', - u'L': u'\u2028', - u'P': u'\u2029', - } - - ESCAPE_CODES = { - u'x': 2, - u'u': 4, - u'U': 8, - } - - def scan_flow_scalar_non_spaces(self, double, start_mark): - # See the specification for details. - chunks = [] - while True: - length = 0 - while self.peek(length) not in u'\'\"\\\0 \t\r\n\x85\u2028\u2029': - length += 1 - if length: - chunks.append(self.prefix(length)) - self.forward(length) - ch = self.peek() - if not double and ch == u'\'' and self.peek(1) == u'\'': - chunks.append(u'\'') - self.forward(2) - elif (double and ch == u'\'') or (not double and ch in u'\"\\'): - chunks.append(ch) - self.forward() - elif double and ch == u'\\': - self.forward() - ch = self.peek() - if ch in self.ESCAPE_REPLACEMENTS: - chunks.append(self.ESCAPE_REPLACEMENTS[ch]) - self.forward() - elif ch in self.ESCAPE_CODES: - length = self.ESCAPE_CODES[ch] - self.forward() - for k in range(length): - if self.peek(k) not in u'0123456789ABCDEFabcdef': - raise ScannerError("while scanning a double-quoted scalar", start_mark, - "expected escape sequence of %d hexdecimal numbers, but found %r" % - (length, self.peek(k).encode('utf-8')), self.get_mark()) - code = int(self.prefix(length), 16) - chunks.append(unichr(code)) - self.forward(length) - elif ch in u'\r\n\x85\u2028\u2029': - self.scan_line_break() - chunks.extend(self.scan_flow_scalar_breaks(double, start_mark)) - else: - raise ScannerError("while scanning a double-quoted scalar", start_mark, - "found unknown escape character %r" % ch.encode('utf-8'), self.get_mark()) - else: - return chunks - - def scan_flow_scalar_spaces(self, double, start_mark): - # See the specification for details. - chunks = [] - length = 0 - while self.peek(length) in u' \t': - length += 1 - whitespaces = self.prefix(length) - self.forward(length) - ch = self.peek() - if ch == u'\0': - raise ScannerError("while scanning a quoted scalar", start_mark, - "found unexpected end of stream", self.get_mark()) - elif ch in u'\r\n\x85\u2028\u2029': - line_break = self.scan_line_break() - breaks = self.scan_flow_scalar_breaks(double, start_mark) - if line_break != u'\n': - chunks.append(line_break) - elif not breaks: - chunks.append(u' ') - chunks.extend(breaks) - else: - chunks.append(whitespaces) - return chunks - - def scan_flow_scalar_breaks(self, double, start_mark): - # See the specification for details. - chunks = [] - while True: - # Instead of checking indentation, we check for document - # separators. - prefix = self.prefix(3) - if (prefix == u'---' or prefix == u'...') \ - and self.peek(3) in u'\0 \t\r\n\x85\u2028\u2029': - raise ScannerError("while scanning a quoted scalar", start_mark, - "found unexpected document separator", self.get_mark()) - while self.peek() in u' \t': - self.forward() - if self.peek() in u'\r\n\x85\u2028\u2029': - chunks.append(self.scan_line_break()) - else: - return chunks - - def scan_plain(self): - # See the specification for details. - # We add an additional restriction for the flow context: - # plain scalars in the flow context cannot contain ',', ':' and '?'. - # We also keep track of the `allow_simple_key` flag here. - # Indentation rules are loosed for the flow context. - chunks = [] - start_mark = self.get_mark() - end_mark = start_mark - indent = self.indent+1 - # We allow zero indentation for scalars, but then we need to check for - # document separators at the beginning of the line. - #if indent == 0: - # indent = 1 - spaces = [] - while True: - length = 0 - if self.peek() == u'#': - break - while True: - ch = self.peek(length) - if ch in u'\0 \t\r\n\x85\u2028\u2029' \ - or (not self.flow_level and ch == u':' and - self.peek(length+1) in u'\0 \t\r\n\x85\u2028\u2029') \ - or (self.flow_level and ch in u',:?[]{}'): - break - length += 1 - # It's not clear what we should do with ':' in the flow context. - if (self.flow_level and ch == u':' - and self.peek(length+1) not in u'\0 \t\r\n\x85\u2028\u2029,[]{}'): - self.forward(length) - raise ScannerError("while scanning a plain scalar", start_mark, - "found unexpected ':'", self.get_mark(), - "Please check http://pyyaml.org/wiki/YAMLColonInFlowContext for details.") - if length == 0: - break - self.allow_simple_key = False - chunks.extend(spaces) - chunks.append(self.prefix(length)) - self.forward(length) - end_mark = self.get_mark() - spaces = self.scan_plain_spaces(indent, start_mark) - if not spaces or self.peek() == u'#' \ - or (not self.flow_level and self.column < indent): - break - return ScalarToken(u''.join(chunks), True, start_mark, end_mark) - - def scan_plain_spaces(self, indent, start_mark): - # See the specification for details. - # The specification is really confusing about tabs in plain scalars. - # We just forbid them completely. Do not use tabs in YAML! - chunks = [] - length = 0 - while self.peek(length) in u' ': - length += 1 - whitespaces = self.prefix(length) - self.forward(length) - ch = self.peek() - if ch in u'\r\n\x85\u2028\u2029': - line_break = self.scan_line_break() - self.allow_simple_key = True - prefix = self.prefix(3) - if (prefix == u'---' or prefix == u'...') \ - and self.peek(3) in u'\0 \t\r\n\x85\u2028\u2029': - return - breaks = [] - while self.peek() in u' \r\n\x85\u2028\u2029': - if self.peek() == ' ': - self.forward() - else: - breaks.append(self.scan_line_break()) - prefix = self.prefix(3) - if (prefix == u'---' or prefix == u'...') \ - and self.peek(3) in u'\0 \t\r\n\x85\u2028\u2029': - return - if line_break != u'\n': - chunks.append(line_break) - elif not breaks: - chunks.append(u' ') - chunks.extend(breaks) - elif whitespaces: - chunks.append(whitespaces) - return chunks - - def scan_tag_handle(self, name, start_mark): - # See the specification for details. - # For some strange reasons, the specification does not allow '_' in - # tag handles. I have allowed it anyway. - ch = self.peek() - if ch != u'!': - raise ScannerError("while scanning a %s" % name, start_mark, - "expected '!', but found %r" % ch.encode('utf-8'), - self.get_mark()) - length = 1 - ch = self.peek(length) - if ch != u' ': - while u'0' <= ch <= u'9' or u'A' <= ch <= u'Z' or u'a' <= ch <= u'z' \ - or ch in u'-_': - length += 1 - ch = self.peek(length) - if ch != u'!': - self.forward(length) - raise ScannerError("while scanning a %s" % name, start_mark, - "expected '!', but found %r" % ch.encode('utf-8'), - self.get_mark()) - length += 1 - value = self.prefix(length) - self.forward(length) - return value - - def scan_tag_uri(self, name, start_mark): - # See the specification for details. - # Note: we do not check if URI is well-formed. - chunks = [] - length = 0 - ch = self.peek(length) - while u'0' <= ch <= u'9' or u'A' <= ch <= u'Z' or u'a' <= ch <= u'z' \ - or ch in u'-;/?:@&=+$,_.!~*\'()[]%': - if ch == u'%': - chunks.append(self.prefix(length)) - self.forward(length) - length = 0 - chunks.append(self.scan_uri_escapes(name, start_mark)) - else: - length += 1 - ch = self.peek(length) - if length: - chunks.append(self.prefix(length)) - self.forward(length) - length = 0 - if not chunks: - raise ScannerError("while parsing a %s" % name, start_mark, - "expected URI, but found %r" % ch.encode('utf-8'), - self.get_mark()) - return u''.join(chunks) - - def scan_uri_escapes(self, name, start_mark): - # See the specification for details. - bytes = [] - mark = self.get_mark() - while self.peek() == u'%': - self.forward() - for k in range(2): - if self.peek(k) not in u'0123456789ABCDEFabcdef': - raise ScannerError("while scanning a %s" % name, start_mark, - "expected URI escape sequence of 2 hexdecimal numbers, but found %r" % - (self.peek(k).encode('utf-8')), self.get_mark()) - bytes.append(chr(int(self.prefix(2), 16))) - self.forward(2) - try: - value = unicode(''.join(bytes), 'utf-8') - except UnicodeDecodeError, exc: - raise ScannerError("while scanning a %s" % name, start_mark, str(exc), mark) - return value - - def scan_line_break(self): - # Transforms: - # '\r\n' : '\n' - # '\r' : '\n' - # '\n' : '\n' - # '\x85' : '\n' - # '\u2028' : '\u2028' - # '\u2029 : '\u2029' - # default : '' - ch = self.peek() - if ch in u'\r\n\x85': - if self.prefix(2) == u'\r\n': - self.forward(2) - else: - self.forward() - return u'\n' - elif ch in u'\u2028\u2029': - self.forward() - return ch - return u'' - -#try: -# import psyco -# psyco.bind(Scanner) -#except ImportError: -# pass - + +# Scanner produces tokens of the following types: +# STREAM-START +# STREAM-END +# DIRECTIVE(name, value) +# DOCUMENT-START +# DOCUMENT-END +# BLOCK-SEQUENCE-START +# BLOCK-MAPPING-START +# BLOCK-END +# FLOW-SEQUENCE-START +# FLOW-MAPPING-START +# FLOW-SEQUENCE-END +# FLOW-MAPPING-END +# BLOCK-ENTRY +# FLOW-ENTRY +# KEY +# VALUE +# ALIAS(value) +# ANCHOR(value) +# TAG(value) +# SCALAR(value, plain, style) +# +# Read comments in the Scanner code for more details. +# + +__all__ = ['Scanner', 'ScannerError'] + +from error import MarkedYAMLError +from tokens import * + +class ScannerError(MarkedYAMLError): + pass + +class SimpleKey(object): + # See below simple keys treatment. + + def __init__(self, token_number, required, index, line, column, mark): + self.token_number = token_number + self.required = required + self.index = index + self.line = line + self.column = column + self.mark = mark + +class Scanner(object): + + def __init__(self): + """Initialize the scanner.""" + # It is assumed that Scanner and Reader will have a common descendant. + # Reader do the dirty work of checking for BOM and converting the + # input data to Unicode. It also adds NUL to the end. + # + # Reader supports the following methods + # self.peek(i=0) # peek the next i-th character + # self.prefix(l=1) # peek the next l characters + # self.forward(l=1) # read the next l characters and move the pointer. + + # Had we reached the end of the stream? + self.done = False + + # The number of unclosed '{' and '['. `flow_level == 0` means block + # context. + self.flow_level = 0 + + # List of processed tokens that are not yet emitted. + self.tokens = [] + + # Add the STREAM-START token. + self.fetch_stream_start() + + # Number of tokens that were emitted through the `get_token` method. + self.tokens_taken = 0 + + # The current indentation level. + self.indent = -1 + + # Past indentation levels. + self.indents = [] + + # Variables related to simple keys treatment. + + # A simple key is a key that is not denoted by the '?' indicator. + # Example of simple keys: + # --- + # block simple key: value + # ? not a simple key: + # : { flow simple key: value } + # We emit the KEY token before all keys, so when we find a potential + # simple key, we try to locate the corresponding ':' indicator. + # Simple keys should be limited to a single line and 1024 characters. + + # Can a simple key start at the current position? A simple key may + # start: + # - at the beginning of the line, not counting indentation spaces + # (in block context), + # - after '{', '[', ',' (in the flow context), + # - after '?', ':', '-' (in the block context). + # In the block context, this flag also signifies if a block collection + # may start at the current position. + self.allow_simple_key = True + + # Keep track of possible simple keys. This is a dictionary. The key + # is `flow_level`; there can be no more that one possible simple key + # for each level. The value is a SimpleKey record: + # (token_number, required, index, line, column, mark) + # A simple key may start with ALIAS, ANCHOR, TAG, SCALAR(flow), + # '[', or '{' tokens. + self.possible_simple_keys = {} + + # Public methods. + + def check_token(self, *choices): + # Check if the next token is one of the given types. + while self.need_more_tokens(): + self.fetch_more_tokens() + if self.tokens: + if not choices: + return True + for choice in choices: + if isinstance(self.tokens[0], choice): + return True + return False + + def peek_token(self): + # Return the next token, but do not delete if from the queue. + while self.need_more_tokens(): + self.fetch_more_tokens() + if self.tokens: + return self.tokens[0] + + def get_token(self): + # Return the next token. + while self.need_more_tokens(): + self.fetch_more_tokens() + if self.tokens: + self.tokens_taken += 1 + return self.tokens.pop(0) + + # Private methods. + + def need_more_tokens(self): + if self.done: + return False + if not self.tokens: + return True + # The current token may be a potential simple key, so we + # need to look further. + self.stale_possible_simple_keys() + if self.next_possible_simple_key() == self.tokens_taken: + return True + + def fetch_more_tokens(self): + + # Eat whitespaces and comments until we reach the next token. + self.scan_to_next_token() + + # Remove obsolete possible simple keys. + self.stale_possible_simple_keys() + + # Compare the current indentation and column. It may add some tokens + # and decrease the current indentation level. + self.unwind_indent(self.column) + + # Peek the next character. + ch = self.peek() + + # Is it the end of stream? + if ch == u'\0': + return self.fetch_stream_end() + + # Is it a directive? + if ch == u'%' and self.check_directive(): + return self.fetch_directive() + + # Is it the document start? + if ch == u'-' and self.check_document_start(): + return self.fetch_document_start() + + # Is it the document end? + if ch == u'.' and self.check_document_end(): + return self.fetch_document_end() + + # TODO: support for BOM within a stream. + #if ch == u'\uFEFF': + # return self.fetch_bom() <-- issue BOMToken + + # Note: the order of the following checks is NOT significant. + + # Is it the flow sequence start indicator? + if ch == u'[': + return self.fetch_flow_sequence_start() + + # Is it the flow mapping start indicator? + if ch == u'{': + return self.fetch_flow_mapping_start() + + # Is it the flow sequence end indicator? + if ch == u']': + return self.fetch_flow_sequence_end() + + # Is it the flow mapping end indicator? + if ch == u'}': + return self.fetch_flow_mapping_end() + + # Is it the flow entry indicator? + if ch == u',': + return self.fetch_flow_entry() + + # Is it the block entry indicator? + if ch == u'-' and self.check_block_entry(): + return self.fetch_block_entry() + + # Is it the key indicator? + if ch == u'?' and self.check_key(): + return self.fetch_key() + + # Is it the value indicator? + if ch == u':' and self.check_value(): + return self.fetch_value() + + # Is it an alias? + if ch == u'*': + return self.fetch_alias() + + # Is it an anchor? + if ch == u'&': + return self.fetch_anchor() + + # Is it a tag? + if ch == u'!': + return self.fetch_tag() + + # Is it a literal scalar? + if ch == u'|' and not self.flow_level: + return self.fetch_literal() + + # Is it a folded scalar? + if ch == u'>' and not self.flow_level: + return self.fetch_folded() + + # Is it a single quoted scalar? + if ch == u'\'': + return self.fetch_single() + + # Is it a double quoted scalar? + if ch == u'\"': + return self.fetch_double() + + # It must be a plain scalar then. + if self.check_plain(): + return self.fetch_plain() + + # No? It's an error. Let's produce a nice error message. + raise ScannerError("while scanning for the next token", None, + "found character %r that cannot start any token" + % ch.encode('utf-8'), self.get_mark()) + + # Simple keys treatment. + + def next_possible_simple_key(self): + # Return the number of the nearest possible simple key. Actually we + # don't need to loop through the whole dictionary. We may replace it + # with the following code: + # if not self.possible_simple_keys: + # return None + # return self.possible_simple_keys[ + # min(self.possible_simple_keys.keys())].token_number + min_token_number = None + for level in self.possible_simple_keys: + key = self.possible_simple_keys[level] + if min_token_number is None or key.token_number < min_token_number: + min_token_number = key.token_number + return min_token_number + + def stale_possible_simple_keys(self): + # Remove entries that are no longer possible simple keys. According to + # the YAML specification, simple keys + # - should be limited to a single line, + # - should be no longer than 1024 characters. + # Disabling this procedure will allow simple keys of any length and + # height (may cause problems if indentation is broken though). + for level in self.possible_simple_keys.keys(): + key = self.possible_simple_keys[level] + if key.line != self.line \ + or self.index-key.index > 1024: + if key.required: + raise ScannerError("while scanning a simple key", key.mark, + "could not found expected ':'", self.get_mark()) + del self.possible_simple_keys[level] + + def save_possible_simple_key(self): + # The next token may start a simple key. We check if it's possible + # and save its position. This function is called for + # ALIAS, ANCHOR, TAG, SCALAR(flow), '[', and '{'. + + # Check if a simple key is required at the current position. + required = not self.flow_level and self.indent == self.column + + # A simple key is required only if it is the first token in the current + # line. Therefore it is always allowed. + assert self.allow_simple_key or not required + + # The next token might be a simple key. Let's save it's number and + # position. + if self.allow_simple_key: + self.remove_possible_simple_key() + token_number = self.tokens_taken+len(self.tokens) + key = SimpleKey(token_number, required, + self.index, self.line, self.column, self.get_mark()) + self.possible_simple_keys[self.flow_level] = key + + def remove_possible_simple_key(self): + # Remove the saved possible key position at the current flow level. + if self.flow_level in self.possible_simple_keys: + key = self.possible_simple_keys[self.flow_level] + + if key.required: + raise ScannerError("while scanning a simple key", key.mark, + "could not found expected ':'", self.get_mark()) + + del self.possible_simple_keys[self.flow_level] + + # Indentation functions. + + def unwind_indent(self, column): + + ## In flow context, tokens should respect indentation. + ## Actually the condition should be `self.indent >= column` according to + ## the spec. But this condition will prohibit intuitively correct + ## constructions such as + ## key : { + ## } + #if self.flow_level and self.indent > column: + # raise ScannerError(None, None, + # "invalid intendation or unclosed '[' or '{'", + # self.get_mark()) + + # In the flow context, indentation is ignored. We make the scanner less + # restrictive then specification requires. + if self.flow_level: + return + + # In block context, we may need to issue the BLOCK-END tokens. + while self.indent > column: + mark = self.get_mark() + self.indent = self.indents.pop() + self.tokens.append(BlockEndToken(mark, mark)) + + def add_indent(self, column): + # Check if we need to increase indentation. + if self.indent < column: + self.indents.append(self.indent) + self.indent = column + return True + return False + + # Fetchers. + + def fetch_stream_start(self): + # We always add STREAM-START as the first token and STREAM-END as the + # last token. + + # Read the token. + mark = self.get_mark() + + # Add STREAM-START. + self.tokens.append(StreamStartToken(mark, mark, + encoding=self.encoding)) + + + def fetch_stream_end(self): + + # Set the current intendation to -1. + self.unwind_indent(-1) + + # Reset simple keys. + self.remove_possible_simple_key() + self.allow_simple_key = False + self.possible_simple_keys = {} + + # Read the token. + mark = self.get_mark() + + # Add STREAM-END. + self.tokens.append(StreamEndToken(mark, mark)) + + # The steam is finished. + self.done = True + + def fetch_directive(self): + + # Set the current intendation to -1. + self.unwind_indent(-1) + + # Reset simple keys. + self.remove_possible_simple_key() + self.allow_simple_key = False + + # Scan and add DIRECTIVE. + self.tokens.append(self.scan_directive()) + + def fetch_document_start(self): + self.fetch_document_indicator(DocumentStartToken) + + def fetch_document_end(self): + self.fetch_document_indicator(DocumentEndToken) + + def fetch_document_indicator(self, TokenClass): + + # Set the current intendation to -1. + self.unwind_indent(-1) + + # Reset simple keys. Note that there could not be a block collection + # after '---'. + self.remove_possible_simple_key() + self.allow_simple_key = False + + # Add DOCUMENT-START or DOCUMENT-END. + start_mark = self.get_mark() + self.forward(3) + end_mark = self.get_mark() + self.tokens.append(TokenClass(start_mark, end_mark)) + + def fetch_flow_sequence_start(self): + self.fetch_flow_collection_start(FlowSequenceStartToken) + + def fetch_flow_mapping_start(self): + self.fetch_flow_collection_start(FlowMappingStartToken) + + def fetch_flow_collection_start(self, TokenClass): + + # '[' and '{' may start a simple key. + self.save_possible_simple_key() + + # Increase the flow level. + self.flow_level += 1 + + # Simple keys are allowed after '[' and '{'. + self.allow_simple_key = True + + # Add FLOW-SEQUENCE-START or FLOW-MAPPING-START. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(TokenClass(start_mark, end_mark)) + + def fetch_flow_sequence_end(self): + self.fetch_flow_collection_end(FlowSequenceEndToken) + + def fetch_flow_mapping_end(self): + self.fetch_flow_collection_end(FlowMappingEndToken) + + def fetch_flow_collection_end(self, TokenClass): + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Decrease the flow level. + self.flow_level -= 1 + + # No simple keys after ']' or '}'. + self.allow_simple_key = False + + # Add FLOW-SEQUENCE-END or FLOW-MAPPING-END. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(TokenClass(start_mark, end_mark)) + + def fetch_flow_entry(self): + + # Simple keys are allowed after ','. + self.allow_simple_key = True + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Add FLOW-ENTRY. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(FlowEntryToken(start_mark, end_mark)) + + def fetch_block_entry(self): + + # Block context needs additional checks. + if not self.flow_level: + + # Are we allowed to start a new entry? + if not self.allow_simple_key: + raise ScannerError(None, None, + "sequence entries are not allowed here", + self.get_mark()) + + # We may need to add BLOCK-SEQUENCE-START. + if self.add_indent(self.column): + mark = self.get_mark() + self.tokens.append(BlockSequenceStartToken(mark, mark)) + + # It's an error for the block entry to occur in the flow context, + # but we let the parser detect this. + else: + pass + + # Simple keys are allowed after '-'. + self.allow_simple_key = True + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Add BLOCK-ENTRY. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(BlockEntryToken(start_mark, end_mark)) + + def fetch_key(self): + + # Block context needs additional checks. + if not self.flow_level: + + # Are we allowed to start a key (not nessesary a simple)? + if not self.allow_simple_key: + raise ScannerError(None, None, + "mapping keys are not allowed here", + self.get_mark()) + + # We may need to add BLOCK-MAPPING-START. + if self.add_indent(self.column): + mark = self.get_mark() + self.tokens.append(BlockMappingStartToken(mark, mark)) + + # Simple keys are allowed after '?' in the block context. + self.allow_simple_key = not self.flow_level + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Add KEY. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(KeyToken(start_mark, end_mark)) + + def fetch_value(self): + + # Do we determine a simple key? + if self.flow_level in self.possible_simple_keys: + + # Add KEY. + key = self.possible_simple_keys[self.flow_level] + del self.possible_simple_keys[self.flow_level] + self.tokens.insert(key.token_number-self.tokens_taken, + KeyToken(key.mark, key.mark)) + + # If this key starts a new block mapping, we need to add + # BLOCK-MAPPING-START. + if not self.flow_level: + if self.add_indent(key.column): + self.tokens.insert(key.token_number-self.tokens_taken, + BlockMappingStartToken(key.mark, key.mark)) + + # There cannot be two simple keys one after another. + self.allow_simple_key = False + + # It must be a part of a complex key. + else: + + # Block context needs additional checks. + # (Do we really need them? They will be catched by the parser + # anyway.) + if not self.flow_level: + + # We are allowed to start a complex value if and only if + # we can start a simple key. + if not self.allow_simple_key: + raise ScannerError(None, None, + "mapping values are not allowed here", + self.get_mark()) + + # If this value starts a new block mapping, we need to add + # BLOCK-MAPPING-START. It will be detected as an error later by + # the parser. + if not self.flow_level: + if self.add_indent(self.column): + mark = self.get_mark() + self.tokens.append(BlockMappingStartToken(mark, mark)) + + # Simple keys are allowed after ':' in the block context. + self.allow_simple_key = not self.flow_level + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Add VALUE. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(ValueToken(start_mark, end_mark)) + + def fetch_alias(self): + + # ALIAS could be a simple key. + self.save_possible_simple_key() + + # No simple keys after ALIAS. + self.allow_simple_key = False + + # Scan and add ALIAS. + self.tokens.append(self.scan_anchor(AliasToken)) + + def fetch_anchor(self): + + # ANCHOR could start a simple key. + self.save_possible_simple_key() + + # No simple keys after ANCHOR. + self.allow_simple_key = False + + # Scan and add ANCHOR. + self.tokens.append(self.scan_anchor(AnchorToken)) + + def fetch_tag(self): + + # TAG could start a simple key. + self.save_possible_simple_key() + + # No simple keys after TAG. + self.allow_simple_key = False + + # Scan and add TAG. + self.tokens.append(self.scan_tag()) + + def fetch_literal(self): + self.fetch_block_scalar(style='|') + + def fetch_folded(self): + self.fetch_block_scalar(style='>') + + def fetch_block_scalar(self, style): + + # A simple key may follow a block scalar. + self.allow_simple_key = True + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Scan and add SCALAR. + self.tokens.append(self.scan_block_scalar(style)) + + def fetch_single(self): + self.fetch_flow_scalar(style='\'') + + def fetch_double(self): + self.fetch_flow_scalar(style='"') + + def fetch_flow_scalar(self, style): + + # A flow scalar could be a simple key. + self.save_possible_simple_key() + + # No simple keys after flow scalars. + self.allow_simple_key = False + + # Scan and add SCALAR. + self.tokens.append(self.scan_flow_scalar(style)) + + def fetch_plain(self): + + # A plain scalar could be a simple key. + self.save_possible_simple_key() + + # No simple keys after plain scalars. But note that `scan_plain` will + # change this flag if the scan is finished at the beginning of the + # line. + self.allow_simple_key = False + + # Scan and add SCALAR. May change `allow_simple_key`. + self.tokens.append(self.scan_plain()) + + # Checkers. + + def check_directive(self): + + # DIRECTIVE: ^ '%' ... + # The '%' indicator is already checked. + if self.column == 0: + return True + + def check_document_start(self): + + # DOCUMENT-START: ^ '---' (' '|'\n') + if self.column == 0: + if self.prefix(3) == u'---' \ + and self.peek(3) in u'\0 \t\r\n\x85\u2028\u2029': + return True + + def check_document_end(self): + + # DOCUMENT-END: ^ '...' (' '|'\n') + if self.column == 0: + if self.prefix(3) == u'...' \ + and self.peek(3) in u'\0 \t\r\n\x85\u2028\u2029': + return True + + def check_block_entry(self): + + # BLOCK-ENTRY: '-' (' '|'\n') + return self.peek(1) in u'\0 \t\r\n\x85\u2028\u2029' + + def check_key(self): + + # KEY(flow context): '?' + if self.flow_level: + return True + + # KEY(block context): '?' (' '|'\n') + else: + return self.peek(1) in u'\0 \t\r\n\x85\u2028\u2029' + + def check_value(self): + + # VALUE(flow context): ':' + if self.flow_level: + return True + + # VALUE(block context): ':' (' '|'\n') + else: + return self.peek(1) in u'\0 \t\r\n\x85\u2028\u2029' + + def check_plain(self): + + # A plain scalar may start with any non-space character except: + # '-', '?', ':', ',', '[', ']', '{', '}', + # '#', '&', '*', '!', '|', '>', '\'', '\"', + # '%', '@', '`'. + # + # It may also start with + # '-', '?', ':' + # if it is followed by a non-space character. + # + # Note that we limit the last rule to the block context (except the + # '-' character) because we want the flow context to be space + # independent. + ch = self.peek() + return ch not in u'\0 \t\r\n\x85\u2028\u2029-?:,[]{}#&*!|>\'\"%@`' \ + or (self.peek(1) not in u'\0 \t\r\n\x85\u2028\u2029' + and (ch == u'-' or (not self.flow_level and ch in u'?:'))) + + # Scanners. + + def scan_to_next_token(self): + # We ignore spaces, line breaks and comments. + # If we find a line break in the block context, we set the flag + # `allow_simple_key` on. + # The byte order mark is stripped if it's the first character in the + # stream. We do not yet support BOM inside the stream as the + # specification requires. Any such mark will be considered as a part + # of the document. + # + # TODO: We need to make tab handling rules more sane. A good rule is + # Tabs cannot precede tokens + # BLOCK-SEQUENCE-START, BLOCK-MAPPING-START, BLOCK-END, + # KEY(block), VALUE(block), BLOCK-ENTRY + # So the checking code is + # if : + # self.allow_simple_keys = False + # We also need to add the check for `allow_simple_keys == True` to + # `unwind_indent` before issuing BLOCK-END. + # Scanners for block, flow, and plain scalars need to be modified. + + if self.index == 0 and self.peek() == u'\uFEFF': + self.forward() + found = False + while not found: + while self.peek() == u' ': + self.forward() + if self.peek() == u'#': + while self.peek() not in u'\0\r\n\x85\u2028\u2029': + self.forward() + if self.scan_line_break(): + if not self.flow_level: + self.allow_simple_key = True + else: + found = True + + def scan_directive(self): + # See the specification for details. + start_mark = self.get_mark() + self.forward() + name = self.scan_directive_name(start_mark) + value = None + if name == u'YAML': + value = self.scan_yaml_directive_value(start_mark) + end_mark = self.get_mark() + elif name == u'TAG': + value = self.scan_tag_directive_value(start_mark) + end_mark = self.get_mark() + else: + end_mark = self.get_mark() + while self.peek() not in u'\0\r\n\x85\u2028\u2029': + self.forward() + self.scan_directive_ignored_line(start_mark) + return DirectiveToken(name, value, start_mark, end_mark) + + def scan_directive_name(self, start_mark): + # See the specification for details. + length = 0 + ch = self.peek(length) + while u'0' <= ch <= u'9' or u'A' <= ch <= u'Z' or u'a' <= ch <= u'z' \ + or ch in u'-_': + length += 1 + ch = self.peek(length) + if not length: + raise ScannerError("while scanning a directive", start_mark, + "expected alphabetic or numeric character, but found %r" + % ch.encode('utf-8'), self.get_mark()) + value = self.prefix(length) + self.forward(length) + ch = self.peek() + if ch not in u'\0 \r\n\x85\u2028\u2029': + raise ScannerError("while scanning a directive", start_mark, + "expected alphabetic or numeric character, but found %r" + % ch.encode('utf-8'), self.get_mark()) + return value + + def scan_yaml_directive_value(self, start_mark): + # See the specification for details. + while self.peek() == u' ': + self.forward() + major = self.scan_yaml_directive_number(start_mark) + if self.peek() != '.': + raise ScannerError("while scanning a directive", start_mark, + "expected a digit or '.', but found %r" + % self.peek().encode('utf-8'), + self.get_mark()) + self.forward() + minor = self.scan_yaml_directive_number(start_mark) + if self.peek() not in u'\0 \r\n\x85\u2028\u2029': + raise ScannerError("while scanning a directive", start_mark, + "expected a digit or ' ', but found %r" + % self.peek().encode('utf-8'), + self.get_mark()) + return (major, minor) + + def scan_yaml_directive_number(self, start_mark): + # See the specification for details. + ch = self.peek() + if not (u'0' <= ch <= u'9'): + raise ScannerError("while scanning a directive", start_mark, + "expected a digit, but found %r" % ch.encode('utf-8'), + self.get_mark()) + length = 0 + while u'0' <= self.peek(length) <= u'9': + length += 1 + value = int(self.prefix(length)) + self.forward(length) + return value + + def scan_tag_directive_value(self, start_mark): + # See the specification for details. + while self.peek() == u' ': + self.forward() + handle = self.scan_tag_directive_handle(start_mark) + while self.peek() == u' ': + self.forward() + prefix = self.scan_tag_directive_prefix(start_mark) + return (handle, prefix) + + def scan_tag_directive_handle(self, start_mark): + # See the specification for details. + value = self.scan_tag_handle('directive', start_mark) + ch = self.peek() + if ch != u' ': + raise ScannerError("while scanning a directive", start_mark, + "expected ' ', but found %r" % ch.encode('utf-8'), + self.get_mark()) + return value + + def scan_tag_directive_prefix(self, start_mark): + # See the specification for details. + value = self.scan_tag_uri('directive', start_mark) + ch = self.peek() + if ch not in u'\0 \r\n\x85\u2028\u2029': + raise ScannerError("while scanning a directive", start_mark, + "expected ' ', but found %r" % ch.encode('utf-8'), + self.get_mark()) + return value + + def scan_directive_ignored_line(self, start_mark): + # See the specification for details. + while self.peek() == u' ': + self.forward() + if self.peek() == u'#': + while self.peek() not in u'\0\r\n\x85\u2028\u2029': + self.forward() + ch = self.peek() + if ch not in u'\0\r\n\x85\u2028\u2029': + raise ScannerError("while scanning a directive", start_mark, + "expected a comment or a line break, but found %r" + % ch.encode('utf-8'), self.get_mark()) + self.scan_line_break() + + def scan_anchor(self, TokenClass): + # The specification does not restrict characters for anchors and + # aliases. This may lead to problems, for instance, the document: + # [ *alias, value ] + # can be interpteted in two ways, as + # [ "value" ] + # and + # [ *alias , "value" ] + # Therefore we restrict aliases to numbers and ASCII letters. + start_mark = self.get_mark() + indicator = self.peek() + if indicator == u'*': + name = 'alias' + else: + name = 'anchor' + self.forward() + length = 0 + ch = self.peek(length) + while u'0' <= ch <= u'9' or u'A' <= ch <= u'Z' or u'a' <= ch <= u'z' \ + or ch in u'-_': + length += 1 + ch = self.peek(length) + if not length: + raise ScannerError("while scanning an %s" % name, start_mark, + "expected alphabetic or numeric character, but found %r" + % ch.encode('utf-8'), self.get_mark()) + value = self.prefix(length) + self.forward(length) + ch = self.peek() + if ch not in u'\0 \t\r\n\x85\u2028\u2029?:,]}%@`': + raise ScannerError("while scanning an %s" % name, start_mark, + "expected alphabetic or numeric character, but found %r" + % ch.encode('utf-8'), self.get_mark()) + end_mark = self.get_mark() + return TokenClass(value, start_mark, end_mark) + + def scan_tag(self): + # See the specification for details. + start_mark = self.get_mark() + ch = self.peek(1) + if ch == u'<': + handle = None + self.forward(2) + suffix = self.scan_tag_uri('tag', start_mark) + if self.peek() != u'>': + raise ScannerError("while parsing a tag", start_mark, + "expected '>', but found %r" % self.peek().encode('utf-8'), + self.get_mark()) + self.forward() + elif ch in u'\0 \t\r\n\x85\u2028\u2029': + handle = None + suffix = u'!' + self.forward() + else: + length = 1 + use_handle = False + while ch not in u'\0 \r\n\x85\u2028\u2029': + if ch == u'!': + use_handle = True + break + length += 1 + ch = self.peek(length) + handle = u'!' + if use_handle: + handle = self.scan_tag_handle('tag', start_mark) + else: + handle = u'!' + self.forward() + suffix = self.scan_tag_uri('tag', start_mark) + ch = self.peek() + if ch not in u'\0 \r\n\x85\u2028\u2029': + raise ScannerError("while scanning a tag", start_mark, + "expected ' ', but found %r" % ch.encode('utf-8'), + self.get_mark()) + value = (handle, suffix) + end_mark = self.get_mark() + return TagToken(value, start_mark, end_mark) + + def scan_block_scalar(self, style): + # See the specification for details. + + if style == '>': + folded = True + else: + folded = False + + chunks = [] + start_mark = self.get_mark() + + # Scan the header. + self.forward() + chomping, increment = self.scan_block_scalar_indicators(start_mark) + self.scan_block_scalar_ignored_line(start_mark) + + # Determine the indentation level and go to the first non-empty line. + min_indent = self.indent+1 + if min_indent < 1: + min_indent = 1 + if increment is None: + breaks, max_indent, end_mark = self.scan_block_scalar_indentation() + indent = max(min_indent, max_indent) + else: + indent = min_indent+increment-1 + breaks, end_mark = self.scan_block_scalar_breaks(indent) + line_break = u'' + + # Scan the inner part of the block scalar. + while self.column == indent and self.peek() != u'\0': + chunks.extend(breaks) + leading_non_space = self.peek() not in u' \t' + length = 0 + while self.peek(length) not in u'\0\r\n\x85\u2028\u2029': + length += 1 + chunks.append(self.prefix(length)) + self.forward(length) + line_break = self.scan_line_break() + breaks, end_mark = self.scan_block_scalar_breaks(indent) + if self.column == indent and self.peek() != u'\0': + + # Unfortunately, folding rules are ambiguous. + # + # This is the folding according to the specification: + + if folded and line_break == u'\n' \ + and leading_non_space and self.peek() not in u' \t': + if not breaks: + chunks.append(u' ') + else: + chunks.append(line_break) + + # This is Clark Evans's interpretation (also in the spec + # examples): + # + #if folded and line_break == u'\n': + # if not breaks: + # if self.peek() not in ' \t': + # chunks.append(u' ') + # else: + # chunks.append(line_break) + #else: + # chunks.append(line_break) + else: + break + + # Chomp the tail. + if chomping is not False: + chunks.append(line_break) + if chomping is True: + chunks.extend(breaks) + + # We are done. + return ScalarToken(u''.join(chunks), False, start_mark, end_mark, + style) + + def scan_block_scalar_indicators(self, start_mark): + # See the specification for details. + chomping = None + increment = None + ch = self.peek() + if ch in u'+-': + if ch == '+': + chomping = True + else: + chomping = False + self.forward() + ch = self.peek() + if ch in u'0123456789': + increment = int(ch) + if increment == 0: + raise ScannerError("while scanning a block scalar", start_mark, + "expected indentation indicator in the range 1-9, but found 0", + self.get_mark()) + self.forward() + elif ch in u'0123456789': + increment = int(ch) + if increment == 0: + raise ScannerError("while scanning a block scalar", start_mark, + "expected indentation indicator in the range 1-9, but found 0", + self.get_mark()) + self.forward() + ch = self.peek() + if ch in u'+-': + if ch == '+': + chomping = True + else: + chomping = False + self.forward() + ch = self.peek() + if ch not in u'\0 \r\n\x85\u2028\u2029': + raise ScannerError("while scanning a block scalar", start_mark, + "expected chomping or indentation indicators, but found %r" + % ch.encode('utf-8'), self.get_mark()) + return chomping, increment + + def scan_block_scalar_ignored_line(self, start_mark): + # See the specification for details. + while self.peek() == u' ': + self.forward() + if self.peek() == u'#': + while self.peek() not in u'\0\r\n\x85\u2028\u2029': + self.forward() + ch = self.peek() + if ch not in u'\0\r\n\x85\u2028\u2029': + raise ScannerError("while scanning a block scalar", start_mark, + "expected a comment or a line break, but found %r" + % ch.encode('utf-8'), self.get_mark()) + self.scan_line_break() + + def scan_block_scalar_indentation(self): + # See the specification for details. + chunks = [] + max_indent = 0 + end_mark = self.get_mark() + while self.peek() in u' \r\n\x85\u2028\u2029': + if self.peek() != u' ': + chunks.append(self.scan_line_break()) + end_mark = self.get_mark() + else: + self.forward() + if self.column > max_indent: + max_indent = self.column + return chunks, max_indent, end_mark + + def scan_block_scalar_breaks(self, indent): + # See the specification for details. + chunks = [] + end_mark = self.get_mark() + while self.column < indent and self.peek() == u' ': + self.forward() + while self.peek() in u'\r\n\x85\u2028\u2029': + chunks.append(self.scan_line_break()) + end_mark = self.get_mark() + while self.column < indent and self.peek() == u' ': + self.forward() + return chunks, end_mark + + def scan_flow_scalar(self, style): + # See the specification for details. + # Note that we loose indentation rules for quoted scalars. Quoted + # scalars don't need to adhere indentation because " and ' clearly + # mark the beginning and the end of them. Therefore we are less + # restrictive then the specification requires. We only need to check + # that document separators are not included in scalars. + if style == '"': + double = True + else: + double = False + chunks = [] + start_mark = self.get_mark() + quote = self.peek() + self.forward() + chunks.extend(self.scan_flow_scalar_non_spaces(double, start_mark)) + while self.peek() != quote: + chunks.extend(self.scan_flow_scalar_spaces(double, start_mark)) + chunks.extend(self.scan_flow_scalar_non_spaces(double, start_mark)) + self.forward() + end_mark = self.get_mark() + return ScalarToken(u''.join(chunks), False, start_mark, end_mark, + style) + + ESCAPE_REPLACEMENTS = { + u'0': u'\0', + u'a': u'\x07', + u'b': u'\x08', + u't': u'\x09', + u'\t': u'\x09', + u'n': u'\x0A', + u'v': u'\x0B', + u'f': u'\x0C', + u'r': u'\x0D', + u'e': u'\x1B', + u' ': u'\x20', + u'\"': u'\"', + u'\\': u'\\', + u'N': u'\x85', + u'_': u'\xA0', + u'L': u'\u2028', + u'P': u'\u2029', + } + + ESCAPE_CODES = { + u'x': 2, + u'u': 4, + u'U': 8, + } + + def scan_flow_scalar_non_spaces(self, double, start_mark): + # See the specification for details. + chunks = [] + while True: + length = 0 + while self.peek(length) not in u'\'\"\\\0 \t\r\n\x85\u2028\u2029': + length += 1 + if length: + chunks.append(self.prefix(length)) + self.forward(length) + ch = self.peek() + if not double and ch == u'\'' and self.peek(1) == u'\'': + chunks.append(u'\'') + self.forward(2) + elif (double and ch == u'\'') or (not double and ch in u'\"\\'): + chunks.append(ch) + self.forward() + elif double and ch == u'\\': + self.forward() + ch = self.peek() + if ch in self.ESCAPE_REPLACEMENTS: + chunks.append(self.ESCAPE_REPLACEMENTS[ch]) + self.forward() + elif ch in self.ESCAPE_CODES: + length = self.ESCAPE_CODES[ch] + self.forward() + for k in range(length): + if self.peek(k) not in u'0123456789ABCDEFabcdef': + raise ScannerError("while scanning a double-quoted scalar", start_mark, + "expected escape sequence of %d hexdecimal numbers, but found %r" % + (length, self.peek(k).encode('utf-8')), self.get_mark()) + code = int(self.prefix(length), 16) + chunks.append(unichr(code)) + self.forward(length) + elif ch in u'\r\n\x85\u2028\u2029': + self.scan_line_break() + chunks.extend(self.scan_flow_scalar_breaks(double, start_mark)) + else: + raise ScannerError("while scanning a double-quoted scalar", start_mark, + "found unknown escape character %r" % ch.encode('utf-8'), self.get_mark()) + else: + return chunks + + def scan_flow_scalar_spaces(self, double, start_mark): + # See the specification for details. + chunks = [] + length = 0 + while self.peek(length) in u' \t': + length += 1 + whitespaces = self.prefix(length) + self.forward(length) + ch = self.peek() + if ch == u'\0': + raise ScannerError("while scanning a quoted scalar", start_mark, + "found unexpected end of stream", self.get_mark()) + elif ch in u'\r\n\x85\u2028\u2029': + line_break = self.scan_line_break() + breaks = self.scan_flow_scalar_breaks(double, start_mark) + if line_break != u'\n': + chunks.append(line_break) + elif not breaks: + chunks.append(u' ') + chunks.extend(breaks) + else: + chunks.append(whitespaces) + return chunks + + def scan_flow_scalar_breaks(self, double, start_mark): + # See the specification for details. + chunks = [] + while True: + # Instead of checking indentation, we check for document + # separators. + prefix = self.prefix(3) + if (prefix == u'---' or prefix == u'...') \ + and self.peek(3) in u'\0 \t\r\n\x85\u2028\u2029': + raise ScannerError("while scanning a quoted scalar", start_mark, + "found unexpected document separator", self.get_mark()) + while self.peek() in u' \t': + self.forward() + if self.peek() in u'\r\n\x85\u2028\u2029': + chunks.append(self.scan_line_break()) + else: + return chunks + + def scan_plain(self): + # See the specification for details. + # We add an additional restriction for the flow context: + # plain scalars in the flow context cannot contain ',', ':' and '?'. + # We also keep track of the `allow_simple_key` flag here. + # Indentation rules are loosed for the flow context. + chunks = [] + start_mark = self.get_mark() + end_mark = start_mark + indent = self.indent+1 + # We allow zero indentation for scalars, but then we need to check for + # document separators at the beginning of the line. + #if indent == 0: + # indent = 1 + spaces = [] + while True: + length = 0 + if self.peek() == u'#': + break + while True: + ch = self.peek(length) + if ch in u'\0 \t\r\n\x85\u2028\u2029' \ + or (not self.flow_level and ch == u':' and + self.peek(length+1) in u'\0 \t\r\n\x85\u2028\u2029') \ + or (self.flow_level and ch in u',:?[]{}'): + break + length += 1 + # It's not clear what we should do with ':' in the flow context. + if (self.flow_level and ch == u':' + and self.peek(length+1) not in u'\0 \t\r\n\x85\u2028\u2029,[]{}'): + self.forward(length) + raise ScannerError("while scanning a plain scalar", start_mark, + "found unexpected ':'", self.get_mark(), + "Please check http://pyyaml.org/wiki/YAMLColonInFlowContext for details.") + if length == 0: + break + self.allow_simple_key = False + chunks.extend(spaces) + chunks.append(self.prefix(length)) + self.forward(length) + end_mark = self.get_mark() + spaces = self.scan_plain_spaces(indent, start_mark) + if not spaces or self.peek() == u'#' \ + or (not self.flow_level and self.column < indent): + break + return ScalarToken(u''.join(chunks), True, start_mark, end_mark) + + def scan_plain_spaces(self, indent, start_mark): + # See the specification for details. + # The specification is really confusing about tabs in plain scalars. + # We just forbid them completely. Do not use tabs in YAML! + chunks = [] + length = 0 + while self.peek(length) in u' ': + length += 1 + whitespaces = self.prefix(length) + self.forward(length) + ch = self.peek() + if ch in u'\r\n\x85\u2028\u2029': + line_break = self.scan_line_break() + self.allow_simple_key = True + prefix = self.prefix(3) + if (prefix == u'---' or prefix == u'...') \ + and self.peek(3) in u'\0 \t\r\n\x85\u2028\u2029': + return + breaks = [] + while self.peek() in u' \r\n\x85\u2028\u2029': + if self.peek() == ' ': + self.forward() + else: + breaks.append(self.scan_line_break()) + prefix = self.prefix(3) + if (prefix == u'---' or prefix == u'...') \ + and self.peek(3) in u'\0 \t\r\n\x85\u2028\u2029': + return + if line_break != u'\n': + chunks.append(line_break) + elif not breaks: + chunks.append(u' ') + chunks.extend(breaks) + elif whitespaces: + chunks.append(whitespaces) + return chunks + + def scan_tag_handle(self, name, start_mark): + # See the specification for details. + # For some strange reasons, the specification does not allow '_' in + # tag handles. I have allowed it anyway. + ch = self.peek() + if ch != u'!': + raise ScannerError("while scanning a %s" % name, start_mark, + "expected '!', but found %r" % ch.encode('utf-8'), + self.get_mark()) + length = 1 + ch = self.peek(length) + if ch != u' ': + while u'0' <= ch <= u'9' or u'A' <= ch <= u'Z' or u'a' <= ch <= u'z' \ + or ch in u'-_': + length += 1 + ch = self.peek(length) + if ch != u'!': + self.forward(length) + raise ScannerError("while scanning a %s" % name, start_mark, + "expected '!', but found %r" % ch.encode('utf-8'), + self.get_mark()) + length += 1 + value = self.prefix(length) + self.forward(length) + return value + + def scan_tag_uri(self, name, start_mark): + # See the specification for details. + # Note: we do not check if URI is well-formed. + chunks = [] + length = 0 + ch = self.peek(length) + while u'0' <= ch <= u'9' or u'A' <= ch <= u'Z' or u'a' <= ch <= u'z' \ + or ch in u'-;/?:@&=+$,_.!~*\'()[]%': + if ch == u'%': + chunks.append(self.prefix(length)) + self.forward(length) + length = 0 + chunks.append(self.scan_uri_escapes(name, start_mark)) + else: + length += 1 + ch = self.peek(length) + if length: + chunks.append(self.prefix(length)) + self.forward(length) + length = 0 + if not chunks: + raise ScannerError("while parsing a %s" % name, start_mark, + "expected URI, but found %r" % ch.encode('utf-8'), + self.get_mark()) + return u''.join(chunks) + + def scan_uri_escapes(self, name, start_mark): + # See the specification for details. + bytes = [] + mark = self.get_mark() + while self.peek() == u'%': + self.forward() + for k in range(2): + if self.peek(k) not in u'0123456789ABCDEFabcdef': + raise ScannerError("while scanning a %s" % name, start_mark, + "expected URI escape sequence of 2 hexdecimal numbers, but found %r" % + (self.peek(k).encode('utf-8')), self.get_mark()) + bytes.append(chr(int(self.prefix(2), 16))) + self.forward(2) + try: + value = unicode(''.join(bytes), 'utf-8') + except UnicodeDecodeError, exc: + raise ScannerError("while scanning a %s" % name, start_mark, str(exc), mark) + return value + + def scan_line_break(self): + # Transforms: + # '\r\n' : '\n' + # '\r' : '\n' + # '\n' : '\n' + # '\x85' : '\n' + # '\u2028' : '\u2028' + # '\u2029 : '\u2029' + # default : '' + ch = self.peek() + if ch in u'\r\n\x85': + if self.prefix(2) == u'\r\n': + self.forward(2) + else: + self.forward() + return u'\n' + elif ch in u'\u2028\u2029': + self.forward() + return ch + return u'' + +#try: +# import psyco +# psyco.bind(Scanner) +#except ImportError: +# pass + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py2/serializer.py b/RenderPipelineFile/rplibs/yaml/yaml_py2/serializer.py index 0bf1e96d..19eea690 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py2/serializer.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py2/serializer.py @@ -1,111 +1,111 @@ - -__all__ = ['Serializer', 'SerializerError'] - -from error import YAMLError -from events import * -from nodes import * - -class SerializerError(YAMLError): - pass - -class Serializer(object): - - ANCHOR_TEMPLATE = u'id%03d' - - def __init__(self, encoding=None, - explicit_start=None, explicit_end=None, version=None, tags=None): - self.use_encoding = encoding - self.use_explicit_start = explicit_start - self.use_explicit_end = explicit_end - self.use_version = version - self.use_tags = tags - self.serialized_nodes = {} - self.anchors = {} - self.last_anchor_id = 0 - self.closed = None - - def open(self): - if self.closed is None: - self.emit(StreamStartEvent(encoding=self.use_encoding)) - self.closed = False - elif self.closed: - raise SerializerError("serializer is closed") - else: - raise SerializerError("serializer is already opened") - - def close(self): - if self.closed is None: - raise SerializerError("serializer is not opened") - elif not self.closed: - self.emit(StreamEndEvent()) - self.closed = True - - #def __del__(self): - # self.close() - - def serialize(self, node): - if self.closed is None: - raise SerializerError("serializer is not opened") - elif self.closed: - raise SerializerError("serializer is closed") - self.emit(DocumentStartEvent(explicit=self.use_explicit_start, - version=self.use_version, tags=self.use_tags)) - self.anchor_node(node) - self.serialize_node(node, None, None) - self.emit(DocumentEndEvent(explicit=self.use_explicit_end)) - self.serialized_nodes = {} - self.anchors = {} - self.last_anchor_id = 0 - - def anchor_node(self, node): - if node in self.anchors: - if self.anchors[node] is None: - self.anchors[node] = self.generate_anchor(node) - else: - self.anchors[node] = None - if isinstance(node, SequenceNode): - for item in node.value: - self.anchor_node(item) - elif isinstance(node, MappingNode): - for key, value in node.value: - self.anchor_node(key) - self.anchor_node(value) - - def generate_anchor(self, node): - self.last_anchor_id += 1 - return self.ANCHOR_TEMPLATE % self.last_anchor_id - - def serialize_node(self, node, parent, index): - alias = self.anchors[node] - if node in self.serialized_nodes: - self.emit(AliasEvent(alias)) - else: - self.serialized_nodes[node] = True - self.descend_resolver(parent, index) - if isinstance(node, ScalarNode): - detected_tag = self.resolve(ScalarNode, node.value, (True, False)) - default_tag = self.resolve(ScalarNode, node.value, (False, True)) - implicit = (node.tag == detected_tag), (node.tag == default_tag) - self.emit(ScalarEvent(alias, node.tag, implicit, node.value, - style=node.style)) - elif isinstance(node, SequenceNode): - implicit = (node.tag - == self.resolve(SequenceNode, node.value, True)) - self.emit(SequenceStartEvent(alias, node.tag, implicit, - flow_style=node.flow_style)) - index = 0 - for item in node.value: - self.serialize_node(item, node, index) - index += 1 - self.emit(SequenceEndEvent()) - elif isinstance(node, MappingNode): - implicit = (node.tag - == self.resolve(MappingNode, node.value, True)) - self.emit(MappingStartEvent(alias, node.tag, implicit, - flow_style=node.flow_style)) - for key, value in node.value: - self.serialize_node(key, node, None) - self.serialize_node(value, node, key) - self.emit(MappingEndEvent()) - self.ascend_resolver() - + +__all__ = ['Serializer', 'SerializerError'] + +from error import YAMLError +from events import * +from nodes import * + +class SerializerError(YAMLError): + pass + +class Serializer(object): + + ANCHOR_TEMPLATE = u'id%03d' + + def __init__(self, encoding=None, + explicit_start=None, explicit_end=None, version=None, tags=None): + self.use_encoding = encoding + self.use_explicit_start = explicit_start + self.use_explicit_end = explicit_end + self.use_version = version + self.use_tags = tags + self.serialized_nodes = {} + self.anchors = {} + self.last_anchor_id = 0 + self.closed = None + + def open(self): + if self.closed is None: + self.emit(StreamStartEvent(encoding=self.use_encoding)) + self.closed = False + elif self.closed: + raise SerializerError("serializer is closed") + else: + raise SerializerError("serializer is already opened") + + def close(self): + if self.closed is None: + raise SerializerError("serializer is not opened") + elif not self.closed: + self.emit(StreamEndEvent()) + self.closed = True + + #def __del__(self): + # self.close() + + def serialize(self, node): + if self.closed is None: + raise SerializerError("serializer is not opened") + elif self.closed: + raise SerializerError("serializer is closed") + self.emit(DocumentStartEvent(explicit=self.use_explicit_start, + version=self.use_version, tags=self.use_tags)) + self.anchor_node(node) + self.serialize_node(node, None, None) + self.emit(DocumentEndEvent(explicit=self.use_explicit_end)) + self.serialized_nodes = {} + self.anchors = {} + self.last_anchor_id = 0 + + def anchor_node(self, node): + if node in self.anchors: + if self.anchors[node] is None: + self.anchors[node] = self.generate_anchor(node) + else: + self.anchors[node] = None + if isinstance(node, SequenceNode): + for item in node.value: + self.anchor_node(item) + elif isinstance(node, MappingNode): + for key, value in node.value: + self.anchor_node(key) + self.anchor_node(value) + + def generate_anchor(self, node): + self.last_anchor_id += 1 + return self.ANCHOR_TEMPLATE % self.last_anchor_id + + def serialize_node(self, node, parent, index): + alias = self.anchors[node] + if node in self.serialized_nodes: + self.emit(AliasEvent(alias)) + else: + self.serialized_nodes[node] = True + self.descend_resolver(parent, index) + if isinstance(node, ScalarNode): + detected_tag = self.resolve(ScalarNode, node.value, (True, False)) + default_tag = self.resolve(ScalarNode, node.value, (False, True)) + implicit = (node.tag == detected_tag), (node.tag == default_tag) + self.emit(ScalarEvent(alias, node.tag, implicit, node.value, + style=node.style)) + elif isinstance(node, SequenceNode): + implicit = (node.tag + == self.resolve(SequenceNode, node.value, True)) + self.emit(SequenceStartEvent(alias, node.tag, implicit, + flow_style=node.flow_style)) + index = 0 + for item in node.value: + self.serialize_node(item, node, index) + index += 1 + self.emit(SequenceEndEvent()) + elif isinstance(node, MappingNode): + implicit = (node.tag + == self.resolve(MappingNode, node.value, True)) + self.emit(MappingStartEvent(alias, node.tag, implicit, + flow_style=node.flow_style)) + for key, value in node.value: + self.serialize_node(key, node, None) + self.serialize_node(value, node, key) + self.emit(MappingEndEvent()) + self.ascend_resolver() + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py2/tokens.py b/RenderPipelineFile/rplibs/yaml/yaml_py2/tokens.py index 4d0b48a3..d06d1cf3 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py2/tokens.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py2/tokens.py @@ -1,104 +1,104 @@ - -class Token(object): - def __init__(self, start_mark, end_mark): - self.start_mark = start_mark - self.end_mark = end_mark - def __repr__(self): - attributes = [key for key in self.__dict__ - if not key.endswith('_mark')] - attributes.sort() - arguments = ', '.join(['%s=%r' % (key, getattr(self, key)) - for key in attributes]) - return '%s(%s)' % (self.__class__.__name__, arguments) - -#class BOMToken(Token): -# id = '' - -class DirectiveToken(Token): - id = '' - def __init__(self, name, value, start_mark, end_mark): - self.name = name - self.value = value - self.start_mark = start_mark - self.end_mark = end_mark - -class DocumentStartToken(Token): - id = '' - -class DocumentEndToken(Token): - id = '' - -class StreamStartToken(Token): - id = '' - def __init__(self, start_mark=None, end_mark=None, - encoding=None): - self.start_mark = start_mark - self.end_mark = end_mark - self.encoding = encoding - -class StreamEndToken(Token): - id = '' - -class BlockSequenceStartToken(Token): - id = '' - -class BlockMappingStartToken(Token): - id = '' - -class BlockEndToken(Token): - id = '' - -class FlowSequenceStartToken(Token): - id = '[' - -class FlowMappingStartToken(Token): - id = '{' - -class FlowSequenceEndToken(Token): - id = ']' - -class FlowMappingEndToken(Token): - id = '}' - -class KeyToken(Token): - id = '?' - -class ValueToken(Token): - id = ':' - -class BlockEntryToken(Token): - id = '-' - -class FlowEntryToken(Token): - id = ',' - -class AliasToken(Token): - id = '' - def __init__(self, value, start_mark, end_mark): - self.value = value - self.start_mark = start_mark - self.end_mark = end_mark - -class AnchorToken(Token): - id = '' - def __init__(self, value, start_mark, end_mark): - self.value = value - self.start_mark = start_mark - self.end_mark = end_mark - -class TagToken(Token): - id = '' - def __init__(self, value, start_mark, end_mark): - self.value = value - self.start_mark = start_mark - self.end_mark = end_mark - -class ScalarToken(Token): - id = '' - def __init__(self, value, plain, start_mark, end_mark, style=None): - self.value = value - self.plain = plain - self.start_mark = start_mark - self.end_mark = end_mark - self.style = style - + +class Token(object): + def __init__(self, start_mark, end_mark): + self.start_mark = start_mark + self.end_mark = end_mark + def __repr__(self): + attributes = [key for key in self.__dict__ + if not key.endswith('_mark')] + attributes.sort() + arguments = ', '.join(['%s=%r' % (key, getattr(self, key)) + for key in attributes]) + return '%s(%s)' % (self.__class__.__name__, arguments) + +#class BOMToken(Token): +# id = '' + +class DirectiveToken(Token): + id = '' + def __init__(self, name, value, start_mark, end_mark): + self.name = name + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + +class DocumentStartToken(Token): + id = '' + +class DocumentEndToken(Token): + id = '' + +class StreamStartToken(Token): + id = '' + def __init__(self, start_mark=None, end_mark=None, + encoding=None): + self.start_mark = start_mark + self.end_mark = end_mark + self.encoding = encoding + +class StreamEndToken(Token): + id = '' + +class BlockSequenceStartToken(Token): + id = '' + +class BlockMappingStartToken(Token): + id = '' + +class BlockEndToken(Token): + id = '' + +class FlowSequenceStartToken(Token): + id = '[' + +class FlowMappingStartToken(Token): + id = '{' + +class FlowSequenceEndToken(Token): + id = ']' + +class FlowMappingEndToken(Token): + id = '}' + +class KeyToken(Token): + id = '?' + +class ValueToken(Token): + id = ':' + +class BlockEntryToken(Token): + id = '-' + +class FlowEntryToken(Token): + id = ',' + +class AliasToken(Token): + id = '' + def __init__(self, value, start_mark, end_mark): + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + +class AnchorToken(Token): + id = '' + def __init__(self, value, start_mark, end_mark): + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + +class TagToken(Token): + id = '' + def __init__(self, value, start_mark, end_mark): + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + +class ScalarToken(Token): + id = '' + def __init__(self, value, plain, start_mark, end_mark, style=None): + self.value = value + self.plain = plain + self.start_mark = start_mark + self.end_mark = end_mark + self.style = style + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py3/__init__.py b/RenderPipelineFile/rplibs/yaml/yaml_py3/__init__.py index a5e20f94..5a17e7ed 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py3/__init__.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py3/__init__.py @@ -1,312 +1,312 @@ - -from .error import * - -from .tokens import * -from .events import * -from .nodes import * - -from .loader import * -from .dumper import * - -__version__ = '3.11' -try: - from .cyaml import * - __with_libyaml__ = True -except ImportError: - __with_libyaml__ = False - -import io - -def scan(stream, Loader=Loader): - """ - Scan a YAML stream and produce scanning tokens. - """ - loader = Loader(stream) - try: - while loader.check_token(): - yield loader.get_token() - finally: - loader.dispose() - -def parse(stream, Loader=Loader): - """ - Parse a YAML stream and produce parsing events. - """ - loader = Loader(stream) - try: - while loader.check_event(): - yield loader.get_event() - finally: - loader.dispose() - -def compose(stream, Loader=Loader): - """ - Parse the first YAML document in a stream - and produce the corresponding representation tree. - """ - loader = Loader(stream) - try: - return loader.get_single_node() - finally: - loader.dispose() - -def compose_all(stream, Loader=Loader): - """ - Parse all YAML documents in a stream - and produce corresponding representation trees. - """ - loader = Loader(stream) - try: - while loader.check_node(): - yield loader.get_node() - finally: - loader.dispose() - -def load(stream, Loader=Loader): - """ - Parse the first YAML document in a stream - and produce the corresponding Python object. - """ - loader = Loader(stream) - try: - return loader.get_single_data() - finally: - loader.dispose() - -def load_all(stream, Loader=Loader): - """ - Parse all YAML documents in a stream - and produce corresponding Python objects. - """ - loader = Loader(stream) - try: - while loader.check_data(): - yield loader.get_data() - finally: - loader.dispose() - -def safe_load(stream): - """ - Parse the first YAML document in a stream - and produce the corresponding Python object. - Resolve only basic YAML tags. - """ - return load(stream, SafeLoader) - -def safe_load_all(stream): - """ - Parse all YAML documents in a stream - and produce corresponding Python objects. - Resolve only basic YAML tags. - """ - return load_all(stream, SafeLoader) - -def emit(events, stream=None, Dumper=Dumper, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None): - """ - Emit YAML parsing events into a stream. - If stream is None, return the produced string instead. - """ - getvalue = None - if stream is None: - stream = io.StringIO() - getvalue = stream.getvalue - dumper = Dumper(stream, canonical=canonical, indent=indent, width=width, - allow_unicode=allow_unicode, line_break=line_break) - try: - for event in events: - dumper.emit(event) - finally: - dumper.dispose() - if getvalue: - return getvalue() - -def serialize_all(nodes, stream=None, Dumper=Dumper, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None): - """ - Serialize a sequence of representation trees into a YAML stream. - If stream is None, return the produced string instead. - """ - getvalue = None - if stream is None: - if encoding is None: - stream = io.StringIO() - else: - stream = io.BytesIO() - getvalue = stream.getvalue - dumper = Dumper(stream, canonical=canonical, indent=indent, width=width, - allow_unicode=allow_unicode, line_break=line_break, - encoding=encoding, version=version, tags=tags, - explicit_start=explicit_start, explicit_end=explicit_end) - try: - dumper.open() - for node in nodes: - dumper.serialize(node) - dumper.close() - finally: - dumper.dispose() - if getvalue: - return getvalue() - -def serialize(node, stream=None, Dumper=Dumper, **kwds): - """ - Serialize a representation tree into a YAML stream. - If stream is None, return the produced string instead. - """ - return serialize_all([node], stream, Dumper=Dumper, **kwds) - -def dump_all(documents, stream=None, Dumper=Dumper, - default_style=None, default_flow_style=None, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None): - """ - Serialize a sequence of Python objects into a YAML stream. - If stream is None, return the produced string instead. - """ - getvalue = None - if stream is None: - if encoding is None: - stream = io.StringIO() - else: - stream = io.BytesIO() - getvalue = stream.getvalue - dumper = Dumper(stream, default_style=default_style, - default_flow_style=default_flow_style, - canonical=canonical, indent=indent, width=width, - allow_unicode=allow_unicode, line_break=line_break, - encoding=encoding, version=version, tags=tags, - explicit_start=explicit_start, explicit_end=explicit_end) - try: - dumper.open() - for data in documents: - dumper.represent(data) - dumper.close() - finally: - dumper.dispose() - if getvalue: - return getvalue() - -def dump(data, stream=None, Dumper=Dumper, **kwds): - """ - Serialize a Python object into a YAML stream. - If stream is None, return the produced string instead. - """ - return dump_all([data], stream, Dumper=Dumper, **kwds) - -def safe_dump_all(documents, stream=None, **kwds): - """ - Serialize a sequence of Python objects into a YAML stream. - Produce only basic YAML tags. - If stream is None, return the produced string instead. - """ - return dump_all(documents, stream, Dumper=SafeDumper, **kwds) - -def safe_dump(data, stream=None, **kwds): - """ - Serialize a Python object into a YAML stream. - Produce only basic YAML tags. - If stream is None, return the produced string instead. - """ - return dump_all([data], stream, Dumper=SafeDumper, **kwds) - -def add_implicit_resolver(tag, regexp, first=None, - Loader=Loader, Dumper=Dumper): - """ - Add an implicit scalar detector. - If an implicit scalar value matches the given regexp, - the corresponding tag is assigned to the scalar. - first is a sequence of possible initial characters or None. - """ - Loader.add_implicit_resolver(tag, regexp, first) - Dumper.add_implicit_resolver(tag, regexp, first) - -def add_path_resolver(tag, path, kind=None, Loader=Loader, Dumper=Dumper): - """ - Add a path based resolver for the given tag. - A path is a list of keys that forms a path - to a node in the representation tree. - Keys can be string values, integers, or None. - """ - Loader.add_path_resolver(tag, path, kind) - Dumper.add_path_resolver(tag, path, kind) - -def add_constructor(tag, constructor, Loader=Loader): - """ - Add a constructor for the given tag. - Constructor is a function that accepts a Loader instance - and a node object and produces the corresponding Python object. - """ - Loader.add_constructor(tag, constructor) - -def add_multi_constructor(tag_prefix, multi_constructor, Loader=Loader): - """ - Add a multi-constructor for the given tag prefix. - Multi-constructor is called for a node if its tag starts with tag_prefix. - Multi-constructor accepts a Loader instance, a tag suffix, - and a node object and produces the corresponding Python object. - """ - Loader.add_multi_constructor(tag_prefix, multi_constructor) - -def add_representer(data_type, representer, Dumper=Dumper): - """ - Add a representer for the given type. - Representer is a function accepting a Dumper instance - and an instance of the given data type - and producing the corresponding representation node. - """ - Dumper.add_representer(data_type, representer) - -def add_multi_representer(data_type, multi_representer, Dumper=Dumper): - """ - Add a representer for the given type. - Multi-representer is a function accepting a Dumper instance - and an instance of the given data type or subtype - and producing the corresponding representation node. - """ - Dumper.add_multi_representer(data_type, multi_representer) - -class YAMLObjectMetaclass(type): - """ - The metaclass for YAMLObject. - """ - def __init__(cls, name, bases, kwds): - super(YAMLObjectMetaclass, cls).__init__(name, bases, kwds) - if 'yaml_tag' in kwds and kwds['yaml_tag'] is not None: - cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml) - cls.yaml_dumper.add_representer(cls, cls.to_yaml) - -class YAMLObject(metaclass=YAMLObjectMetaclass): - """ - An object that can dump itself to a YAML stream - and load itself from a YAML stream. - """ - - __slots__ = () # no direct instantiation, so allow immutable subclasses - - yaml_loader = Loader - yaml_dumper = Dumper - - yaml_tag = None - yaml_flow_style = None - - @classmethod - def from_yaml(cls, loader, node): - """ - Convert a representation node to a Python object. - """ - return loader.construct_yaml_object(node, cls) - - @classmethod - def to_yaml(cls, dumper, data): - """ - Convert a Python object to a representation node. - """ - return dumper.represent_yaml_object(cls.yaml_tag, data, cls, - flow_style=cls.yaml_flow_style) - + +from .error import * + +from .tokens import * +from .events import * +from .nodes import * + +from .loader import * +from .dumper import * + +__version__ = '3.11' +try: + from .cyaml import * + __with_libyaml__ = True +except ImportError: + __with_libyaml__ = False + +import io + +def scan(stream, Loader=Loader): + """ + Scan a YAML stream and produce scanning tokens. + """ + loader = Loader(stream) + try: + while loader.check_token(): + yield loader.get_token() + finally: + loader.dispose() + +def parse(stream, Loader=Loader): + """ + Parse a YAML stream and produce parsing events. + """ + loader = Loader(stream) + try: + while loader.check_event(): + yield loader.get_event() + finally: + loader.dispose() + +def compose(stream, Loader=Loader): + """ + Parse the first YAML document in a stream + and produce the corresponding representation tree. + """ + loader = Loader(stream) + try: + return loader.get_single_node() + finally: + loader.dispose() + +def compose_all(stream, Loader=Loader): + """ + Parse all YAML documents in a stream + and produce corresponding representation trees. + """ + loader = Loader(stream) + try: + while loader.check_node(): + yield loader.get_node() + finally: + loader.dispose() + +def load(stream, Loader=Loader): + """ + Parse the first YAML document in a stream + and produce the corresponding Python object. + """ + loader = Loader(stream) + try: + return loader.get_single_data() + finally: + loader.dispose() + +def load_all(stream, Loader=Loader): + """ + Parse all YAML documents in a stream + and produce corresponding Python objects. + """ + loader = Loader(stream) + try: + while loader.check_data(): + yield loader.get_data() + finally: + loader.dispose() + +def safe_load(stream): + """ + Parse the first YAML document in a stream + and produce the corresponding Python object. + Resolve only basic YAML tags. + """ + return load(stream, SafeLoader) + +def safe_load_all(stream): + """ + Parse all YAML documents in a stream + and produce corresponding Python objects. + Resolve only basic YAML tags. + """ + return load_all(stream, SafeLoader) + +def emit(events, stream=None, Dumper=Dumper, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None): + """ + Emit YAML parsing events into a stream. + If stream is None, return the produced string instead. + """ + getvalue = None + if stream is None: + stream = io.StringIO() + getvalue = stream.getvalue + dumper = Dumper(stream, canonical=canonical, indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break) + try: + for event in events: + dumper.emit(event) + finally: + dumper.dispose() + if getvalue: + return getvalue() + +def serialize_all(nodes, stream=None, Dumper=Dumper, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None): + """ + Serialize a sequence of representation trees into a YAML stream. + If stream is None, return the produced string instead. + """ + getvalue = None + if stream is None: + if encoding is None: + stream = io.StringIO() + else: + stream = io.BytesIO() + getvalue = stream.getvalue + dumper = Dumper(stream, canonical=canonical, indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break, + encoding=encoding, version=version, tags=tags, + explicit_start=explicit_start, explicit_end=explicit_end) + try: + dumper.open() + for node in nodes: + dumper.serialize(node) + dumper.close() + finally: + dumper.dispose() + if getvalue: + return getvalue() + +def serialize(node, stream=None, Dumper=Dumper, **kwds): + """ + Serialize a representation tree into a YAML stream. + If stream is None, return the produced string instead. + """ + return serialize_all([node], stream, Dumper=Dumper, **kwds) + +def dump_all(documents, stream=None, Dumper=Dumper, + default_style=None, default_flow_style=None, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None): + """ + Serialize a sequence of Python objects into a YAML stream. + If stream is None, return the produced string instead. + """ + getvalue = None + if stream is None: + if encoding is None: + stream = io.StringIO() + else: + stream = io.BytesIO() + getvalue = stream.getvalue + dumper = Dumper(stream, default_style=default_style, + default_flow_style=default_flow_style, + canonical=canonical, indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break, + encoding=encoding, version=version, tags=tags, + explicit_start=explicit_start, explicit_end=explicit_end) + try: + dumper.open() + for data in documents: + dumper.represent(data) + dumper.close() + finally: + dumper.dispose() + if getvalue: + return getvalue() + +def dump(data, stream=None, Dumper=Dumper, **kwds): + """ + Serialize a Python object into a YAML stream. + If stream is None, return the produced string instead. + """ + return dump_all([data], stream, Dumper=Dumper, **kwds) + +def safe_dump_all(documents, stream=None, **kwds): + """ + Serialize a sequence of Python objects into a YAML stream. + Produce only basic YAML tags. + If stream is None, return the produced string instead. + """ + return dump_all(documents, stream, Dumper=SafeDumper, **kwds) + +def safe_dump(data, stream=None, **kwds): + """ + Serialize a Python object into a YAML stream. + Produce only basic YAML tags. + If stream is None, return the produced string instead. + """ + return dump_all([data], stream, Dumper=SafeDumper, **kwds) + +def add_implicit_resolver(tag, regexp, first=None, + Loader=Loader, Dumper=Dumper): + """ + Add an implicit scalar detector. + If an implicit scalar value matches the given regexp, + the corresponding tag is assigned to the scalar. + first is a sequence of possible initial characters or None. + """ + Loader.add_implicit_resolver(tag, regexp, first) + Dumper.add_implicit_resolver(tag, regexp, first) + +def add_path_resolver(tag, path, kind=None, Loader=Loader, Dumper=Dumper): + """ + Add a path based resolver for the given tag. + A path is a list of keys that forms a path + to a node in the representation tree. + Keys can be string values, integers, or None. + """ + Loader.add_path_resolver(tag, path, kind) + Dumper.add_path_resolver(tag, path, kind) + +def add_constructor(tag, constructor, Loader=Loader): + """ + Add a constructor for the given tag. + Constructor is a function that accepts a Loader instance + and a node object and produces the corresponding Python object. + """ + Loader.add_constructor(tag, constructor) + +def add_multi_constructor(tag_prefix, multi_constructor, Loader=Loader): + """ + Add a multi-constructor for the given tag prefix. + Multi-constructor is called for a node if its tag starts with tag_prefix. + Multi-constructor accepts a Loader instance, a tag suffix, + and a node object and produces the corresponding Python object. + """ + Loader.add_multi_constructor(tag_prefix, multi_constructor) + +def add_representer(data_type, representer, Dumper=Dumper): + """ + Add a representer for the given type. + Representer is a function accepting a Dumper instance + and an instance of the given data type + and producing the corresponding representation node. + """ + Dumper.add_representer(data_type, representer) + +def add_multi_representer(data_type, multi_representer, Dumper=Dumper): + """ + Add a representer for the given type. + Multi-representer is a function accepting a Dumper instance + and an instance of the given data type or subtype + and producing the corresponding representation node. + """ + Dumper.add_multi_representer(data_type, multi_representer) + +class YAMLObjectMetaclass(type): + """ + The metaclass for YAMLObject. + """ + def __init__(cls, name, bases, kwds): + super(YAMLObjectMetaclass, cls).__init__(name, bases, kwds) + if 'yaml_tag' in kwds and kwds['yaml_tag'] is not None: + cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml) + cls.yaml_dumper.add_representer(cls, cls.to_yaml) + +class YAMLObject(metaclass=YAMLObjectMetaclass): + """ + An object that can dump itself to a YAML stream + and load itself from a YAML stream. + """ + + __slots__ = () # no direct instantiation, so allow immutable subclasses + + yaml_loader = Loader + yaml_dumper = Dumper + + yaml_tag = None + yaml_flow_style = None + + @classmethod + def from_yaml(cls, loader, node): + """ + Convert a representation node to a Python object. + """ + return loader.construct_yaml_object(node, cls) + + @classmethod + def to_yaml(cls, dumper, data): + """ + Convert a Python object to a representation node. + """ + return dumper.represent_yaml_object(cls.yaml_tag, data, cls, + flow_style=cls.yaml_flow_style) + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py3/composer.py b/RenderPipelineFile/rplibs/yaml/yaml_py3/composer.py index d5c6a7ac..c5a2a5e1 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py3/composer.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py3/composer.py @@ -1,139 +1,139 @@ - -__all__ = ['Composer', 'ComposerError'] - -from .error import MarkedYAMLError -from .events import * -from .nodes import * - -class ComposerError(MarkedYAMLError): - pass - -class Composer: - - def __init__(self): - self.anchors = {} - - def check_node(self): - # Drop the STREAM-START event. - if self.check_event(StreamStartEvent): - self.get_event() - - # If there are more documents available? - return not self.check_event(StreamEndEvent) - - def get_node(self): - # Get the root node of the next document. - if not self.check_event(StreamEndEvent): - return self.compose_document() - - def get_single_node(self): - # Drop the STREAM-START event. - self.get_event() - - # Compose a document if the stream is not empty. - document = None - if not self.check_event(StreamEndEvent): - document = self.compose_document() - - # Ensure that the stream contains no more documents. - if not self.check_event(StreamEndEvent): - event = self.get_event() - raise ComposerError("expected a single document in the stream", - document.start_mark, "but found another document", - event.start_mark) - - # Drop the STREAM-END event. - self.get_event() - - return document - - def compose_document(self): - # Drop the DOCUMENT-START event. - self.get_event() - - # Compose the root node. - node = self.compose_node(None, None) - - # Drop the DOCUMENT-END event. - self.get_event() - - self.anchors = {} - return node - - def compose_node(self, parent, index): - if self.check_event(AliasEvent): - event = self.get_event() - anchor = event.anchor - if anchor not in self.anchors: - raise ComposerError(None, None, "found undefined alias %r" - % anchor, event.start_mark) - return self.anchors[anchor] - event = self.peek_event() - anchor = event.anchor - if anchor is not None: - if anchor in self.anchors: - raise ComposerError("found duplicate anchor %r; first occurence" - % anchor, self.anchors[anchor].start_mark, - "second occurence", event.start_mark) - self.descend_resolver(parent, index) - if self.check_event(ScalarEvent): - node = self.compose_scalar_node(anchor) - elif self.check_event(SequenceStartEvent): - node = self.compose_sequence_node(anchor) - elif self.check_event(MappingStartEvent): - node = self.compose_mapping_node(anchor) - self.ascend_resolver() - return node - - def compose_scalar_node(self, anchor): - event = self.get_event() - tag = event.tag - if tag is None or tag == '!': - tag = self.resolve(ScalarNode, event.value, event.implicit) - node = ScalarNode(tag, event.value, - event.start_mark, event.end_mark, style=event.style) - if anchor is not None: - self.anchors[anchor] = node - return node - - def compose_sequence_node(self, anchor): - start_event = self.get_event() - tag = start_event.tag - if tag is None or tag == '!': - tag = self.resolve(SequenceNode, None, start_event.implicit) - node = SequenceNode(tag, [], - start_event.start_mark, None, - flow_style=start_event.flow_style) - if anchor is not None: - self.anchors[anchor] = node - index = 0 - while not self.check_event(SequenceEndEvent): - node.value.append(self.compose_node(node, index)) - index += 1 - end_event = self.get_event() - node.end_mark = end_event.end_mark - return node - - def compose_mapping_node(self, anchor): - start_event = self.get_event() - tag = start_event.tag - if tag is None or tag == '!': - tag = self.resolve(MappingNode, None, start_event.implicit) - node = MappingNode(tag, [], - start_event.start_mark, None, - flow_style=start_event.flow_style) - if anchor is not None: - self.anchors[anchor] = node - while not self.check_event(MappingEndEvent): - #key_event = self.peek_event() - item_key = self.compose_node(node, None) - #if item_key in node.value: - # raise ComposerError("while composing a mapping", start_event.start_mark, - # "found duplicate key", key_event.start_mark) - item_value = self.compose_node(node, item_key) - #node.value[item_key] = item_value - node.value.append((item_key, item_value)) - end_event = self.get_event() - node.end_mark = end_event.end_mark - return node - + +__all__ = ['Composer', 'ComposerError'] + +from .error import MarkedYAMLError +from .events import * +from .nodes import * + +class ComposerError(MarkedYAMLError): + pass + +class Composer: + + def __init__(self): + self.anchors = {} + + def check_node(self): + # Drop the STREAM-START event. + if self.check_event(StreamStartEvent): + self.get_event() + + # If there are more documents available? + return not self.check_event(StreamEndEvent) + + def get_node(self): + # Get the root node of the next document. + if not self.check_event(StreamEndEvent): + return self.compose_document() + + def get_single_node(self): + # Drop the STREAM-START event. + self.get_event() + + # Compose a document if the stream is not empty. + document = None + if not self.check_event(StreamEndEvent): + document = self.compose_document() + + # Ensure that the stream contains no more documents. + if not self.check_event(StreamEndEvent): + event = self.get_event() + raise ComposerError("expected a single document in the stream", + document.start_mark, "but found another document", + event.start_mark) + + # Drop the STREAM-END event. + self.get_event() + + return document + + def compose_document(self): + # Drop the DOCUMENT-START event. + self.get_event() + + # Compose the root node. + node = self.compose_node(None, None) + + # Drop the DOCUMENT-END event. + self.get_event() + + self.anchors = {} + return node + + def compose_node(self, parent, index): + if self.check_event(AliasEvent): + event = self.get_event() + anchor = event.anchor + if anchor not in self.anchors: + raise ComposerError(None, None, "found undefined alias %r" + % anchor, event.start_mark) + return self.anchors[anchor] + event = self.peek_event() + anchor = event.anchor + if anchor is not None: + if anchor in self.anchors: + raise ComposerError("found duplicate anchor %r; first occurence" + % anchor, self.anchors[anchor].start_mark, + "second occurence", event.start_mark) + self.descend_resolver(parent, index) + if self.check_event(ScalarEvent): + node = self.compose_scalar_node(anchor) + elif self.check_event(SequenceStartEvent): + node = self.compose_sequence_node(anchor) + elif self.check_event(MappingStartEvent): + node = self.compose_mapping_node(anchor) + self.ascend_resolver() + return node + + def compose_scalar_node(self, anchor): + event = self.get_event() + tag = event.tag + if tag is None or tag == '!': + tag = self.resolve(ScalarNode, event.value, event.implicit) + node = ScalarNode(tag, event.value, + event.start_mark, event.end_mark, style=event.style) + if anchor is not None: + self.anchors[anchor] = node + return node + + def compose_sequence_node(self, anchor): + start_event = self.get_event() + tag = start_event.tag + if tag is None or tag == '!': + tag = self.resolve(SequenceNode, None, start_event.implicit) + node = SequenceNode(tag, [], + start_event.start_mark, None, + flow_style=start_event.flow_style) + if anchor is not None: + self.anchors[anchor] = node + index = 0 + while not self.check_event(SequenceEndEvent): + node.value.append(self.compose_node(node, index)) + index += 1 + end_event = self.get_event() + node.end_mark = end_event.end_mark + return node + + def compose_mapping_node(self, anchor): + start_event = self.get_event() + tag = start_event.tag + if tag is None or tag == '!': + tag = self.resolve(MappingNode, None, start_event.implicit) + node = MappingNode(tag, [], + start_event.start_mark, None, + flow_style=start_event.flow_style) + if anchor is not None: + self.anchors[anchor] = node + while not self.check_event(MappingEndEvent): + #key_event = self.peek_event() + item_key = self.compose_node(node, None) + #if item_key in node.value: + # raise ComposerError("while composing a mapping", start_event.start_mark, + # "found duplicate key", key_event.start_mark) + item_value = self.compose_node(node, item_key) + #node.value[item_key] = item_value + node.value.append((item_key, item_value)) + end_event = self.get_event() + node.end_mark = end_event.end_mark + return node + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py3/constructor.py b/RenderPipelineFile/rplibs/yaml/yaml_py3/constructor.py index 6222808a..2091f7c2 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py3/constructor.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py3/constructor.py @@ -1,686 +1,686 @@ - -__all__ = ['BaseConstructor', 'SafeConstructor', 'Constructor', - 'ConstructorError'] - -from .error import * -from .nodes import * - -import collections, datetime, base64, binascii, re, sys, types - -class ConstructorError(MarkedYAMLError): - pass - -class BaseConstructor: - - yaml_constructors = {} - yaml_multi_constructors = {} - - def __init__(self): - self.constructed_objects = {} - self.recursive_objects = {} - self.state_generators = [] - self.deep_construct = False - - def check_data(self): - # If there are more documents available? - return self.check_node() - - def get_data(self): - # Construct and return the next document. - if self.check_node(): - return self.construct_document(self.get_node()) - - def get_single_data(self): - # Ensure that the stream contains a single document and construct it. - node = self.get_single_node() - if node is not None: - return self.construct_document(node) - return None - - def construct_document(self, node): - data = self.construct_object(node) - while self.state_generators: - state_generators = self.state_generators - self.state_generators = [] - for generator in state_generators: - for dummy in generator: - pass - self.constructed_objects = {} - self.recursive_objects = {} - self.deep_construct = False - return data - - def construct_object(self, node, deep=False): - if node in self.constructed_objects: - return self.constructed_objects[node] - if deep: - old_deep = self.deep_construct - self.deep_construct = True - if node in self.recursive_objects: - raise ConstructorError(None, None, - "found unconstructable recursive node", node.start_mark) - self.recursive_objects[node] = None - constructor = None - tag_suffix = None - if node.tag in self.yaml_constructors: - constructor = self.yaml_constructors[node.tag] - else: - for tag_prefix in self.yaml_multi_constructors: - if node.tag.startswith(tag_prefix): - tag_suffix = node.tag[len(tag_prefix):] - constructor = self.yaml_multi_constructors[tag_prefix] - break - else: - if None in self.yaml_multi_constructors: - tag_suffix = node.tag - constructor = self.yaml_multi_constructors[None] - elif None in self.yaml_constructors: - constructor = self.yaml_constructors[None] - elif isinstance(node, ScalarNode): - constructor = self.__class__.construct_scalar - elif isinstance(node, SequenceNode): - constructor = self.__class__.construct_sequence - elif isinstance(node, MappingNode): - constructor = self.__class__.construct_mapping - if tag_suffix is None: - data = constructor(self, node) - else: - data = constructor(self, tag_suffix, node) - if isinstance(data, types.GeneratorType): - generator = data - data = next(generator) - if self.deep_construct: - for dummy in generator: - pass - else: - self.state_generators.append(generator) - self.constructed_objects[node] = data - del self.recursive_objects[node] - if deep: - self.deep_construct = old_deep - return data - - def construct_scalar(self, node): - if not isinstance(node, ScalarNode): - raise ConstructorError(None, None, - "expected a scalar node, but found %s" % node.id, - node.start_mark) - return node.value - - def construct_sequence(self, node, deep=False): - if not isinstance(node, SequenceNode): - raise ConstructorError(None, None, - "expected a sequence node, but found %s" % node.id, - node.start_mark) - return [self.construct_object(child, deep=deep) - for child in node.value] - - def construct_mapping(self, node, deep=False): - if not isinstance(node, MappingNode): - raise ConstructorError(None, None, - "expected a mapping node, but found %s" % node.id, - node.start_mark) - mapping = {} - for key_node, value_node in node.value: - key = self.construct_object(key_node, deep=deep) - if not isinstance(key, collections.abc.Hashable): - raise ConstructorError("while constructing a mapping", node.start_mark, - "found unhashable key", key_node.start_mark) - value = self.construct_object(value_node, deep=deep) - mapping[key] = value - return mapping - - def construct_pairs(self, node, deep=False): - if not isinstance(node, MappingNode): - raise ConstructorError(None, None, - "expected a mapping node, but found %s" % node.id, - node.start_mark) - pairs = [] - for key_node, value_node in node.value: - key = self.construct_object(key_node, deep=deep) - value = self.construct_object(value_node, deep=deep) - pairs.append((key, value)) - return pairs - - @classmethod - def add_constructor(cls, tag, constructor): - if not 'yaml_constructors' in cls.__dict__: - cls.yaml_constructors = cls.yaml_constructors.copy() - cls.yaml_constructors[tag] = constructor - - @classmethod - def add_multi_constructor(cls, tag_prefix, multi_constructor): - if not 'yaml_multi_constructors' in cls.__dict__: - cls.yaml_multi_constructors = cls.yaml_multi_constructors.copy() - cls.yaml_multi_constructors[tag_prefix] = multi_constructor - -class SafeConstructor(BaseConstructor): - - def construct_scalar(self, node): - if isinstance(node, MappingNode): - for key_node, value_node in node.value: - if key_node.tag == 'tag:yaml.org,2002:value': - return self.construct_scalar(value_node) - return super().construct_scalar(node) - - def flatten_mapping(self, node): - merge = [] - index = 0 - while index < len(node.value): - key_node, value_node = node.value[index] - if key_node.tag == 'tag:yaml.org,2002:merge': - del node.value[index] - if isinstance(value_node, MappingNode): - self.flatten_mapping(value_node) - merge.extend(value_node.value) - elif isinstance(value_node, SequenceNode): - submerge = [] - for subnode in value_node.value: - if not isinstance(subnode, MappingNode): - raise ConstructorError("while constructing a mapping", - node.start_mark, - "expected a mapping for merging, but found %s" - % subnode.id, subnode.start_mark) - self.flatten_mapping(subnode) - submerge.append(subnode.value) - submerge.reverse() - for value in submerge: - merge.extend(value) - else: - raise ConstructorError("while constructing a mapping", node.start_mark, - "expected a mapping or list of mappings for merging, but found %s" - % value_node.id, value_node.start_mark) - elif key_node.tag == 'tag:yaml.org,2002:value': - key_node.tag = 'tag:yaml.org,2002:str' - index += 1 - else: - index += 1 - if merge: - node.value = merge + node.value - - def construct_mapping(self, node, deep=False): - if isinstance(node, MappingNode): - self.flatten_mapping(node) - return super().construct_mapping(node, deep=deep) - - def construct_yaml_null(self, node): - self.construct_scalar(node) - return None - - bool_values = { - 'yes': True, - 'no': False, - 'true': True, - 'false': False, - 'on': True, - 'off': False, - } - - def construct_yaml_bool(self, node): - value = self.construct_scalar(node) - return self.bool_values[value.lower()] - - def construct_yaml_int(self, node): - value = self.construct_scalar(node) - value = value.replace('_', '') - sign = +1 - if value[0] == '-': - sign = -1 - if value[0] in '+-': - value = value[1:] - if value == '0': - return 0 - elif value.startswith('0b'): - return sign*int(value[2:], 2) - elif value.startswith('0x'): - return sign*int(value[2:], 16) - elif value[0] == '0': - return sign*int(value, 8) - elif ':' in value: - digits = [int(part) for part in value.split(':')] - digits.reverse() - base = 1 - value = 0 - for digit in digits: - value += digit*base - base *= 60 - return sign*value - else: - return sign*int(value) - - inf_value = 1e300 - while inf_value != inf_value*inf_value: - inf_value *= inf_value - nan_value = -inf_value/inf_value # Trying to make a quiet NaN (like C99). - - def construct_yaml_float(self, node): - value = self.construct_scalar(node) - value = value.replace('_', '').lower() - sign = +1 - if value[0] == '-': - sign = -1 - if value[0] in '+-': - value = value[1:] - if value == '.inf': - return sign*self.inf_value - elif value == '.nan': - return self.nan_value - elif ':' in value: - digits = [float(part) for part in value.split(':')] - digits.reverse() - base = 1 - value = 0.0 - for digit in digits: - value += digit*base - base *= 60 - return sign*value - else: - return sign*float(value) - - def construct_yaml_binary(self, node): - try: - value = self.construct_scalar(node).encode('ascii') - except UnicodeEncodeError as exc: - raise ConstructorError(None, None, - "failed to convert base64 data into ascii: %s" % exc, - node.start_mark) - try: - if hasattr(base64, 'decodebytes'): - return base64.decodebytes(value) - else: - return base64.decodestring(value) - except binascii.Error as exc: - raise ConstructorError(None, None, - "failed to decode base64 data: %s" % exc, node.start_mark) - - timestamp_regexp = re.compile( - r'''^(?P[0-9][0-9][0-9][0-9]) - -(?P[0-9][0-9]?) - -(?P[0-9][0-9]?) - (?:(?:[Tt]|[ \t]+) - (?P[0-9][0-9]?) - :(?P[0-9][0-9]) - :(?P[0-9][0-9]) - (?:\.(?P[0-9]*))? - (?:[ \t]*(?PZ|(?P[-+])(?P[0-9][0-9]?) - (?::(?P[0-9][0-9]))?))?)?$''', re.X) - - def construct_yaml_timestamp(self, node): - value = self.construct_scalar(node) - match = self.timestamp_regexp.match(node.value) - values = match.groupdict() - year = int(values['year']) - month = int(values['month']) - day = int(values['day']) - if not values['hour']: - return datetime.date(year, month, day) - hour = int(values['hour']) - minute = int(values['minute']) - second = int(values['second']) - fraction = 0 - if values['fraction']: - fraction = values['fraction'][:6] - while len(fraction) < 6: - fraction += '0' - fraction = int(fraction) - delta = None - if values['tz_sign']: - tz_hour = int(values['tz_hour']) - tz_minute = int(values['tz_minute'] or 0) - delta = datetime.timedelta(hours=tz_hour, minutes=tz_minute) - if values['tz_sign'] == '-': - delta = -delta - data = datetime.datetime(year, month, day, hour, minute, second, fraction) - if delta: - data -= delta - return data - - def construct_yaml_omap(self, node): - # Note: we do not check for duplicate keys, because it's too - # CPU-expensive. - omap = [] - yield omap - if not isinstance(node, SequenceNode): - raise ConstructorError("while constructing an ordered map", node.start_mark, - "expected a sequence, but found %s" % node.id, node.start_mark) - for subnode in node.value: - if not isinstance(subnode, MappingNode): - raise ConstructorError("while constructing an ordered map", node.start_mark, - "expected a mapping of length 1, but found %s" % subnode.id, - subnode.start_mark) - if len(subnode.value) != 1: - raise ConstructorError("while constructing an ordered map", node.start_mark, - "expected a single mapping item, but found %d items" % len(subnode.value), - subnode.start_mark) - key_node, value_node = subnode.value[0] - key = self.construct_object(key_node) - value = self.construct_object(value_node) - omap.append((key, value)) - - def construct_yaml_pairs(self, node): - # Note: the same code as `construct_yaml_omap`. - pairs = [] - yield pairs - if not isinstance(node, SequenceNode): - raise ConstructorError("while constructing pairs", node.start_mark, - "expected a sequence, but found %s" % node.id, node.start_mark) - for subnode in node.value: - if not isinstance(subnode, MappingNode): - raise ConstructorError("while constructing pairs", node.start_mark, - "expected a mapping of length 1, but found %s" % subnode.id, - subnode.start_mark) - if len(subnode.value) != 1: - raise ConstructorError("while constructing pairs", node.start_mark, - "expected a single mapping item, but found %d items" % len(subnode.value), - subnode.start_mark) - key_node, value_node = subnode.value[0] - key = self.construct_object(key_node) - value = self.construct_object(value_node) - pairs.append((key, value)) - - def construct_yaml_set(self, node): - data = set() - yield data - value = self.construct_mapping(node) - data.update(value) - - def construct_yaml_str(self, node): - return self.construct_scalar(node) - - def construct_yaml_seq(self, node): - data = [] - yield data - data.extend(self.construct_sequence(node)) - - def construct_yaml_map(self, node): - data = {} - yield data - value = self.construct_mapping(node) - data.update(value) - - def construct_yaml_object(self, node, cls): - data = cls.__new__(cls) - yield data - if hasattr(data, '__setstate__'): - state = self.construct_mapping(node, deep=True) - data.__setstate__(state) - else: - state = self.construct_mapping(node) - data.__dict__.update(state) - - def construct_undefined(self, node): - raise ConstructorError(None, None, - "could not determine a constructor for the tag %r" % node.tag, - node.start_mark) - -SafeConstructor.add_constructor( - 'tag:yaml.org,2002:null', - SafeConstructor.construct_yaml_null) - -SafeConstructor.add_constructor( - 'tag:yaml.org,2002:bool', - SafeConstructor.construct_yaml_bool) - -SafeConstructor.add_constructor( - 'tag:yaml.org,2002:int', - SafeConstructor.construct_yaml_int) - -SafeConstructor.add_constructor( - 'tag:yaml.org,2002:float', - SafeConstructor.construct_yaml_float) - -SafeConstructor.add_constructor( - 'tag:yaml.org,2002:binary', - SafeConstructor.construct_yaml_binary) - -SafeConstructor.add_constructor( - 'tag:yaml.org,2002:timestamp', - SafeConstructor.construct_yaml_timestamp) - -SafeConstructor.add_constructor( - 'tag:yaml.org,2002:omap', - SafeConstructor.construct_yaml_omap) - -SafeConstructor.add_constructor( - 'tag:yaml.org,2002:pairs', - SafeConstructor.construct_yaml_pairs) - -SafeConstructor.add_constructor( - 'tag:yaml.org,2002:set', - SafeConstructor.construct_yaml_set) - -SafeConstructor.add_constructor( - 'tag:yaml.org,2002:str', - SafeConstructor.construct_yaml_str) - -SafeConstructor.add_constructor( - 'tag:yaml.org,2002:seq', - SafeConstructor.construct_yaml_seq) - -SafeConstructor.add_constructor( - 'tag:yaml.org,2002:map', - SafeConstructor.construct_yaml_map) - -SafeConstructor.add_constructor(None, - SafeConstructor.construct_undefined) - -class Constructor(SafeConstructor): - - def construct_python_str(self, node): - return self.construct_scalar(node) - - def construct_python_unicode(self, node): - return self.construct_scalar(node) - - def construct_python_bytes(self, node): - try: - value = self.construct_scalar(node).encode('ascii') - except UnicodeEncodeError as exc: - raise ConstructorError(None, None, - "failed to convert base64 data into ascii: %s" % exc, - node.start_mark) - try: - if hasattr(base64, 'decodebytes'): - return base64.decodebytes(value) - else: - return base64.decodestring(value) - except binascii.Error as exc: - raise ConstructorError(None, None, - "failed to decode base64 data: %s" % exc, node.start_mark) - - def construct_python_long(self, node): - return self.construct_yaml_int(node) - - def construct_python_complex(self, node): - return complex(self.construct_scalar(node)) - - def construct_python_tuple(self, node): - return tuple(self.construct_sequence(node)) - - def find_python_module(self, name, mark): - if not name: - raise ConstructorError("while constructing a Python module", mark, - "expected non-empty name appended to the tag", mark) - try: - __import__(name) - except ImportError as exc: - raise ConstructorError("while constructing a Python module", mark, - "cannot find module %r (%s)" % (name, exc), mark) - return sys.modules[name] - - def find_python_name(self, name, mark): - if not name: - raise ConstructorError("while constructing a Python object", mark, - "expected non-empty name appended to the tag", mark) - if '.' in name: - module_name, object_name = name.rsplit('.', 1) - else: - module_name = 'builtins' - object_name = name - try: - __import__(module_name) - except ImportError as exc: - raise ConstructorError("while constructing a Python object", mark, - "cannot find module %r (%s)" % (module_name, exc), mark) - module = sys.modules[module_name] - if not hasattr(module, object_name): - raise ConstructorError("while constructing a Python object", mark, - "cannot find %r in the module %r" - % (object_name, module.__name__), mark) - return getattr(module, object_name) - - def construct_python_name(self, suffix, node): - value = self.construct_scalar(node) - if value: - raise ConstructorError("while constructing a Python name", node.start_mark, - "expected the empty value, but found %r" % value, node.start_mark) - return self.find_python_name(suffix, node.start_mark) - - def construct_python_module(self, suffix, node): - value = self.construct_scalar(node) - if value: - raise ConstructorError("while constructing a Python module", node.start_mark, - "expected the empty value, but found %r" % value, node.start_mark) - return self.find_python_module(suffix, node.start_mark) - - def make_python_instance(self, suffix, node, - args=None, kwds=None, newobj=False): - if not args: - args = [] - if not kwds: - kwds = {} - cls = self.find_python_name(suffix, node.start_mark) - if newobj and isinstance(cls, type): - return cls.__new__(cls, *args, **kwds) - else: - return cls(*args, **kwds) - - def set_python_instance_state(self, instance, state): - if hasattr(instance, '__setstate__'): - instance.__setstate__(state) - else: - slotstate = {} - if isinstance(state, tuple) and len(state) == 2: - state, slotstate = state - if hasattr(instance, '__dict__'): - instance.__dict__.update(state) - elif state: - slotstate.update(state) - for key, value in slotstate.items(): - setattr(object, key, value) - - def construct_python_object(self, suffix, node): - # Format: - # !!python/object:module.name { ... state ... } - instance = self.make_python_instance(suffix, node, newobj=True) - yield instance - deep = hasattr(instance, '__setstate__') - state = self.construct_mapping(node, deep=deep) - self.set_python_instance_state(instance, state) - - def construct_python_object_apply(self, suffix, node, newobj=False): - # Format: - # !!python/object/apply # (or !!python/object/new) - # args: [ ... arguments ... ] - # kwds: { ... keywords ... } - # state: ... state ... - # listitems: [ ... listitems ... ] - # dictitems: { ... dictitems ... } - # or short format: - # !!python/object/apply [ ... arguments ... ] - # The difference between !!python/object/apply and !!python/object/new - # is how an object is created, check make_python_instance for details. - if isinstance(node, SequenceNode): - args = self.construct_sequence(node, deep=True) - kwds = {} - state = {} - listitems = [] - dictitems = {} - else: - value = self.construct_mapping(node, deep=True) - args = value.get('args', []) - kwds = value.get('kwds', {}) - state = value.get('state', {}) - listitems = value.get('listitems', []) - dictitems = value.get('dictitems', {}) - instance = self.make_python_instance(suffix, node, args, kwds, newobj) - if state: - self.set_python_instance_state(instance, state) - if listitems: - instance.extend(listitems) - if dictitems: - for key in dictitems: - instance[key] = dictitems[key] - return instance - - def construct_python_object_new(self, suffix, node): - return self.construct_python_object_apply(suffix, node, newobj=True) - -Constructor.add_constructor( - 'tag:yaml.org,2002:python/none', - Constructor.construct_yaml_null) - -Constructor.add_constructor( - 'tag:yaml.org,2002:python/bool', - Constructor.construct_yaml_bool) - -Constructor.add_constructor( - 'tag:yaml.org,2002:python/str', - Constructor.construct_python_str) - -Constructor.add_constructor( - 'tag:yaml.org,2002:python/unicode', - Constructor.construct_python_unicode) - -Constructor.add_constructor( - 'tag:yaml.org,2002:python/bytes', - Constructor.construct_python_bytes) - -Constructor.add_constructor( - 'tag:yaml.org,2002:python/int', - Constructor.construct_yaml_int) - -Constructor.add_constructor( - 'tag:yaml.org,2002:python/long', - Constructor.construct_python_long) - -Constructor.add_constructor( - 'tag:yaml.org,2002:python/float', - Constructor.construct_yaml_float) - -Constructor.add_constructor( - 'tag:yaml.org,2002:python/complex', - Constructor.construct_python_complex) - -Constructor.add_constructor( - 'tag:yaml.org,2002:python/list', - Constructor.construct_yaml_seq) - -Constructor.add_constructor( - 'tag:yaml.org,2002:python/tuple', - Constructor.construct_python_tuple) - -Constructor.add_constructor( - 'tag:yaml.org,2002:python/dict', - Constructor.construct_yaml_map) - -Constructor.add_multi_constructor( - 'tag:yaml.org,2002:python/name:', - Constructor.construct_python_name) - -Constructor.add_multi_constructor( - 'tag:yaml.org,2002:python/module:', - Constructor.construct_python_module) - -Constructor.add_multi_constructor( - 'tag:yaml.org,2002:python/object:', - Constructor.construct_python_object) - -Constructor.add_multi_constructor( - 'tag:yaml.org,2002:python/object/apply:', - Constructor.construct_python_object_apply) - -Constructor.add_multi_constructor( - 'tag:yaml.org,2002:python/object/new:', - Constructor.construct_python_object_new) - + +__all__ = ['BaseConstructor', 'SafeConstructor', 'Constructor', + 'ConstructorError'] + +from .error import * +from .nodes import * + +import collections, datetime, base64, binascii, re, sys, types + +class ConstructorError(MarkedYAMLError): + pass + +class BaseConstructor: + + yaml_constructors = {} + yaml_multi_constructors = {} + + def __init__(self): + self.constructed_objects = {} + self.recursive_objects = {} + self.state_generators = [] + self.deep_construct = False + + def check_data(self): + # If there are more documents available? + return self.check_node() + + def get_data(self): + # Construct and return the next document. + if self.check_node(): + return self.construct_document(self.get_node()) + + def get_single_data(self): + # Ensure that the stream contains a single document and construct it. + node = self.get_single_node() + if node is not None: + return self.construct_document(node) + return None + + def construct_document(self, node): + data = self.construct_object(node) + while self.state_generators: + state_generators = self.state_generators + self.state_generators = [] + for generator in state_generators: + for dummy in generator: + pass + self.constructed_objects = {} + self.recursive_objects = {} + self.deep_construct = False + return data + + def construct_object(self, node, deep=False): + if node in self.constructed_objects: + return self.constructed_objects[node] + if deep: + old_deep = self.deep_construct + self.deep_construct = True + if node in self.recursive_objects: + raise ConstructorError(None, None, + "found unconstructable recursive node", node.start_mark) + self.recursive_objects[node] = None + constructor = None + tag_suffix = None + if node.tag in self.yaml_constructors: + constructor = self.yaml_constructors[node.tag] + else: + for tag_prefix in self.yaml_multi_constructors: + if node.tag.startswith(tag_prefix): + tag_suffix = node.tag[len(tag_prefix):] + constructor = self.yaml_multi_constructors[tag_prefix] + break + else: + if None in self.yaml_multi_constructors: + tag_suffix = node.tag + constructor = self.yaml_multi_constructors[None] + elif None in self.yaml_constructors: + constructor = self.yaml_constructors[None] + elif isinstance(node, ScalarNode): + constructor = self.__class__.construct_scalar + elif isinstance(node, SequenceNode): + constructor = self.__class__.construct_sequence + elif isinstance(node, MappingNode): + constructor = self.__class__.construct_mapping + if tag_suffix is None: + data = constructor(self, node) + else: + data = constructor(self, tag_suffix, node) + if isinstance(data, types.GeneratorType): + generator = data + data = next(generator) + if self.deep_construct: + for dummy in generator: + pass + else: + self.state_generators.append(generator) + self.constructed_objects[node] = data + del self.recursive_objects[node] + if deep: + self.deep_construct = old_deep + return data + + def construct_scalar(self, node): + if not isinstance(node, ScalarNode): + raise ConstructorError(None, None, + "expected a scalar node, but found %s" % node.id, + node.start_mark) + return node.value + + def construct_sequence(self, node, deep=False): + if not isinstance(node, SequenceNode): + raise ConstructorError(None, None, + "expected a sequence node, but found %s" % node.id, + node.start_mark) + return [self.construct_object(child, deep=deep) + for child in node.value] + + def construct_mapping(self, node, deep=False): + if not isinstance(node, MappingNode): + raise ConstructorError(None, None, + "expected a mapping node, but found %s" % node.id, + node.start_mark) + mapping = {} + for key_node, value_node in node.value: + key = self.construct_object(key_node, deep=deep) + if not isinstance(key, collections.abc.Hashable): + raise ConstructorError("while constructing a mapping", node.start_mark, + "found unhashable key", key_node.start_mark) + value = self.construct_object(value_node, deep=deep) + mapping[key] = value + return mapping + + def construct_pairs(self, node, deep=False): + if not isinstance(node, MappingNode): + raise ConstructorError(None, None, + "expected a mapping node, but found %s" % node.id, + node.start_mark) + pairs = [] + for key_node, value_node in node.value: + key = self.construct_object(key_node, deep=deep) + value = self.construct_object(value_node, deep=deep) + pairs.append((key, value)) + return pairs + + @classmethod + def add_constructor(cls, tag, constructor): + if not 'yaml_constructors' in cls.__dict__: + cls.yaml_constructors = cls.yaml_constructors.copy() + cls.yaml_constructors[tag] = constructor + + @classmethod + def add_multi_constructor(cls, tag_prefix, multi_constructor): + if not 'yaml_multi_constructors' in cls.__dict__: + cls.yaml_multi_constructors = cls.yaml_multi_constructors.copy() + cls.yaml_multi_constructors[tag_prefix] = multi_constructor + +class SafeConstructor(BaseConstructor): + + def construct_scalar(self, node): + if isinstance(node, MappingNode): + for key_node, value_node in node.value: + if key_node.tag == 'tag:yaml.org,2002:value': + return self.construct_scalar(value_node) + return super().construct_scalar(node) + + def flatten_mapping(self, node): + merge = [] + index = 0 + while index < len(node.value): + key_node, value_node = node.value[index] + if key_node.tag == 'tag:yaml.org,2002:merge': + del node.value[index] + if isinstance(value_node, MappingNode): + self.flatten_mapping(value_node) + merge.extend(value_node.value) + elif isinstance(value_node, SequenceNode): + submerge = [] + for subnode in value_node.value: + if not isinstance(subnode, MappingNode): + raise ConstructorError("while constructing a mapping", + node.start_mark, + "expected a mapping for merging, but found %s" + % subnode.id, subnode.start_mark) + self.flatten_mapping(subnode) + submerge.append(subnode.value) + submerge.reverse() + for value in submerge: + merge.extend(value) + else: + raise ConstructorError("while constructing a mapping", node.start_mark, + "expected a mapping or list of mappings for merging, but found %s" + % value_node.id, value_node.start_mark) + elif key_node.tag == 'tag:yaml.org,2002:value': + key_node.tag = 'tag:yaml.org,2002:str' + index += 1 + else: + index += 1 + if merge: + node.value = merge + node.value + + def construct_mapping(self, node, deep=False): + if isinstance(node, MappingNode): + self.flatten_mapping(node) + return super().construct_mapping(node, deep=deep) + + def construct_yaml_null(self, node): + self.construct_scalar(node) + return None + + bool_values = { + 'yes': True, + 'no': False, + 'true': True, + 'false': False, + 'on': True, + 'off': False, + } + + def construct_yaml_bool(self, node): + value = self.construct_scalar(node) + return self.bool_values[value.lower()] + + def construct_yaml_int(self, node): + value = self.construct_scalar(node) + value = value.replace('_', '') + sign = +1 + if value[0] == '-': + sign = -1 + if value[0] in '+-': + value = value[1:] + if value == '0': + return 0 + elif value.startswith('0b'): + return sign*int(value[2:], 2) + elif value.startswith('0x'): + return sign*int(value[2:], 16) + elif value[0] == '0': + return sign*int(value, 8) + elif ':' in value: + digits = [int(part) for part in value.split(':')] + digits.reverse() + base = 1 + value = 0 + for digit in digits: + value += digit*base + base *= 60 + return sign*value + else: + return sign*int(value) + + inf_value = 1e300 + while inf_value != inf_value*inf_value: + inf_value *= inf_value + nan_value = -inf_value/inf_value # Trying to make a quiet NaN (like C99). + + def construct_yaml_float(self, node): + value = self.construct_scalar(node) + value = value.replace('_', '').lower() + sign = +1 + if value[0] == '-': + sign = -1 + if value[0] in '+-': + value = value[1:] + if value == '.inf': + return sign*self.inf_value + elif value == '.nan': + return self.nan_value + elif ':' in value: + digits = [float(part) for part in value.split(':')] + digits.reverse() + base = 1 + value = 0.0 + for digit in digits: + value += digit*base + base *= 60 + return sign*value + else: + return sign*float(value) + + def construct_yaml_binary(self, node): + try: + value = self.construct_scalar(node).encode('ascii') + except UnicodeEncodeError as exc: + raise ConstructorError(None, None, + "failed to convert base64 data into ascii: %s" % exc, + node.start_mark) + try: + if hasattr(base64, 'decodebytes'): + return base64.decodebytes(value) + else: + return base64.decodestring(value) + except binascii.Error as exc: + raise ConstructorError(None, None, + "failed to decode base64 data: %s" % exc, node.start_mark) + + timestamp_regexp = re.compile( + r'''^(?P[0-9][0-9][0-9][0-9]) + -(?P[0-9][0-9]?) + -(?P[0-9][0-9]?) + (?:(?:[Tt]|[ \t]+) + (?P[0-9][0-9]?) + :(?P[0-9][0-9]) + :(?P[0-9][0-9]) + (?:\.(?P[0-9]*))? + (?:[ \t]*(?PZ|(?P[-+])(?P[0-9][0-9]?) + (?::(?P[0-9][0-9]))?))?)?$''', re.X) + + def construct_yaml_timestamp(self, node): + value = self.construct_scalar(node) + match = self.timestamp_regexp.match(node.value) + values = match.groupdict() + year = int(values['year']) + month = int(values['month']) + day = int(values['day']) + if not values['hour']: + return datetime.date(year, month, day) + hour = int(values['hour']) + minute = int(values['minute']) + second = int(values['second']) + fraction = 0 + if values['fraction']: + fraction = values['fraction'][:6] + while len(fraction) < 6: + fraction += '0' + fraction = int(fraction) + delta = None + if values['tz_sign']: + tz_hour = int(values['tz_hour']) + tz_minute = int(values['tz_minute'] or 0) + delta = datetime.timedelta(hours=tz_hour, minutes=tz_minute) + if values['tz_sign'] == '-': + delta = -delta + data = datetime.datetime(year, month, day, hour, minute, second, fraction) + if delta: + data -= delta + return data + + def construct_yaml_omap(self, node): + # Note: we do not check for duplicate keys, because it's too + # CPU-expensive. + omap = [] + yield omap + if not isinstance(node, SequenceNode): + raise ConstructorError("while constructing an ordered map", node.start_mark, + "expected a sequence, but found %s" % node.id, node.start_mark) + for subnode in node.value: + if not isinstance(subnode, MappingNode): + raise ConstructorError("while constructing an ordered map", node.start_mark, + "expected a mapping of length 1, but found %s" % subnode.id, + subnode.start_mark) + if len(subnode.value) != 1: + raise ConstructorError("while constructing an ordered map", node.start_mark, + "expected a single mapping item, but found %d items" % len(subnode.value), + subnode.start_mark) + key_node, value_node = subnode.value[0] + key = self.construct_object(key_node) + value = self.construct_object(value_node) + omap.append((key, value)) + + def construct_yaml_pairs(self, node): + # Note: the same code as `construct_yaml_omap`. + pairs = [] + yield pairs + if not isinstance(node, SequenceNode): + raise ConstructorError("while constructing pairs", node.start_mark, + "expected a sequence, but found %s" % node.id, node.start_mark) + for subnode in node.value: + if not isinstance(subnode, MappingNode): + raise ConstructorError("while constructing pairs", node.start_mark, + "expected a mapping of length 1, but found %s" % subnode.id, + subnode.start_mark) + if len(subnode.value) != 1: + raise ConstructorError("while constructing pairs", node.start_mark, + "expected a single mapping item, but found %d items" % len(subnode.value), + subnode.start_mark) + key_node, value_node = subnode.value[0] + key = self.construct_object(key_node) + value = self.construct_object(value_node) + pairs.append((key, value)) + + def construct_yaml_set(self, node): + data = set() + yield data + value = self.construct_mapping(node) + data.update(value) + + def construct_yaml_str(self, node): + return self.construct_scalar(node) + + def construct_yaml_seq(self, node): + data = [] + yield data + data.extend(self.construct_sequence(node)) + + def construct_yaml_map(self, node): + data = {} + yield data + value = self.construct_mapping(node) + data.update(value) + + def construct_yaml_object(self, node, cls): + data = cls.__new__(cls) + yield data + if hasattr(data, '__setstate__'): + state = self.construct_mapping(node, deep=True) + data.__setstate__(state) + else: + state = self.construct_mapping(node) + data.__dict__.update(state) + + def construct_undefined(self, node): + raise ConstructorError(None, None, + "could not determine a constructor for the tag %r" % node.tag, + node.start_mark) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:null', + SafeConstructor.construct_yaml_null) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:bool', + SafeConstructor.construct_yaml_bool) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:int', + SafeConstructor.construct_yaml_int) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:float', + SafeConstructor.construct_yaml_float) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:binary', + SafeConstructor.construct_yaml_binary) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:timestamp', + SafeConstructor.construct_yaml_timestamp) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:omap', + SafeConstructor.construct_yaml_omap) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:pairs', + SafeConstructor.construct_yaml_pairs) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:set', + SafeConstructor.construct_yaml_set) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:str', + SafeConstructor.construct_yaml_str) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:seq', + SafeConstructor.construct_yaml_seq) + +SafeConstructor.add_constructor( + 'tag:yaml.org,2002:map', + SafeConstructor.construct_yaml_map) + +SafeConstructor.add_constructor(None, + SafeConstructor.construct_undefined) + +class Constructor(SafeConstructor): + + def construct_python_str(self, node): + return self.construct_scalar(node) + + def construct_python_unicode(self, node): + return self.construct_scalar(node) + + def construct_python_bytes(self, node): + try: + value = self.construct_scalar(node).encode('ascii') + except UnicodeEncodeError as exc: + raise ConstructorError(None, None, + "failed to convert base64 data into ascii: %s" % exc, + node.start_mark) + try: + if hasattr(base64, 'decodebytes'): + return base64.decodebytes(value) + else: + return base64.decodestring(value) + except binascii.Error as exc: + raise ConstructorError(None, None, + "failed to decode base64 data: %s" % exc, node.start_mark) + + def construct_python_long(self, node): + return self.construct_yaml_int(node) + + def construct_python_complex(self, node): + return complex(self.construct_scalar(node)) + + def construct_python_tuple(self, node): + return tuple(self.construct_sequence(node)) + + def find_python_module(self, name, mark): + if not name: + raise ConstructorError("while constructing a Python module", mark, + "expected non-empty name appended to the tag", mark) + try: + __import__(name) + except ImportError as exc: + raise ConstructorError("while constructing a Python module", mark, + "cannot find module %r (%s)" % (name, exc), mark) + return sys.modules[name] + + def find_python_name(self, name, mark): + if not name: + raise ConstructorError("while constructing a Python object", mark, + "expected non-empty name appended to the tag", mark) + if '.' in name: + module_name, object_name = name.rsplit('.', 1) + else: + module_name = 'builtins' + object_name = name + try: + __import__(module_name) + except ImportError as exc: + raise ConstructorError("while constructing a Python object", mark, + "cannot find module %r (%s)" % (module_name, exc), mark) + module = sys.modules[module_name] + if not hasattr(module, object_name): + raise ConstructorError("while constructing a Python object", mark, + "cannot find %r in the module %r" + % (object_name, module.__name__), mark) + return getattr(module, object_name) + + def construct_python_name(self, suffix, node): + value = self.construct_scalar(node) + if value: + raise ConstructorError("while constructing a Python name", node.start_mark, + "expected the empty value, but found %r" % value, node.start_mark) + return self.find_python_name(suffix, node.start_mark) + + def construct_python_module(self, suffix, node): + value = self.construct_scalar(node) + if value: + raise ConstructorError("while constructing a Python module", node.start_mark, + "expected the empty value, but found %r" % value, node.start_mark) + return self.find_python_module(suffix, node.start_mark) + + def make_python_instance(self, suffix, node, + args=None, kwds=None, newobj=False): + if not args: + args = [] + if not kwds: + kwds = {} + cls = self.find_python_name(suffix, node.start_mark) + if newobj and isinstance(cls, type): + return cls.__new__(cls, *args, **kwds) + else: + return cls(*args, **kwds) + + def set_python_instance_state(self, instance, state): + if hasattr(instance, '__setstate__'): + instance.__setstate__(state) + else: + slotstate = {} + if isinstance(state, tuple) and len(state) == 2: + state, slotstate = state + if hasattr(instance, '__dict__'): + instance.__dict__.update(state) + elif state: + slotstate.update(state) + for key, value in slotstate.items(): + setattr(object, key, value) + + def construct_python_object(self, suffix, node): + # Format: + # !!python/object:module.name { ... state ... } + instance = self.make_python_instance(suffix, node, newobj=True) + yield instance + deep = hasattr(instance, '__setstate__') + state = self.construct_mapping(node, deep=deep) + self.set_python_instance_state(instance, state) + + def construct_python_object_apply(self, suffix, node, newobj=False): + # Format: + # !!python/object/apply # (or !!python/object/new) + # args: [ ... arguments ... ] + # kwds: { ... keywords ... } + # state: ... state ... + # listitems: [ ... listitems ... ] + # dictitems: { ... dictitems ... } + # or short format: + # !!python/object/apply [ ... arguments ... ] + # The difference between !!python/object/apply and !!python/object/new + # is how an object is created, check make_python_instance for details. + if isinstance(node, SequenceNode): + args = self.construct_sequence(node, deep=True) + kwds = {} + state = {} + listitems = [] + dictitems = {} + else: + value = self.construct_mapping(node, deep=True) + args = value.get('args', []) + kwds = value.get('kwds', {}) + state = value.get('state', {}) + listitems = value.get('listitems', []) + dictitems = value.get('dictitems', {}) + instance = self.make_python_instance(suffix, node, args, kwds, newobj) + if state: + self.set_python_instance_state(instance, state) + if listitems: + instance.extend(listitems) + if dictitems: + for key in dictitems: + instance[key] = dictitems[key] + return instance + + def construct_python_object_new(self, suffix, node): + return self.construct_python_object_apply(suffix, node, newobj=True) + +Constructor.add_constructor( + 'tag:yaml.org,2002:python/none', + Constructor.construct_yaml_null) + +Constructor.add_constructor( + 'tag:yaml.org,2002:python/bool', + Constructor.construct_yaml_bool) + +Constructor.add_constructor( + 'tag:yaml.org,2002:python/str', + Constructor.construct_python_str) + +Constructor.add_constructor( + 'tag:yaml.org,2002:python/unicode', + Constructor.construct_python_unicode) + +Constructor.add_constructor( + 'tag:yaml.org,2002:python/bytes', + Constructor.construct_python_bytes) + +Constructor.add_constructor( + 'tag:yaml.org,2002:python/int', + Constructor.construct_yaml_int) + +Constructor.add_constructor( + 'tag:yaml.org,2002:python/long', + Constructor.construct_python_long) + +Constructor.add_constructor( + 'tag:yaml.org,2002:python/float', + Constructor.construct_yaml_float) + +Constructor.add_constructor( + 'tag:yaml.org,2002:python/complex', + Constructor.construct_python_complex) + +Constructor.add_constructor( + 'tag:yaml.org,2002:python/list', + Constructor.construct_yaml_seq) + +Constructor.add_constructor( + 'tag:yaml.org,2002:python/tuple', + Constructor.construct_python_tuple) + +Constructor.add_constructor( + 'tag:yaml.org,2002:python/dict', + Constructor.construct_yaml_map) + +Constructor.add_multi_constructor( + 'tag:yaml.org,2002:python/name:', + Constructor.construct_python_name) + +Constructor.add_multi_constructor( + 'tag:yaml.org,2002:python/module:', + Constructor.construct_python_module) + +Constructor.add_multi_constructor( + 'tag:yaml.org,2002:python/object:', + Constructor.construct_python_object) + +Constructor.add_multi_constructor( + 'tag:yaml.org,2002:python/object/apply:', + Constructor.construct_python_object_apply) + +Constructor.add_multi_constructor( + 'tag:yaml.org,2002:python/object/new:', + Constructor.construct_python_object_new) + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py3/cyaml.py b/RenderPipelineFile/rplibs/yaml/yaml_py3/cyaml.py index d5cb87e9..16ce1598 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py3/cyaml.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py3/cyaml.py @@ -1,85 +1,85 @@ - -__all__ = ['CBaseLoader', 'CSafeLoader', 'CLoader', - 'CBaseDumper', 'CSafeDumper', 'CDumper'] - -from _yaml import CParser, CEmitter - -from .constructor import * - -from .serializer import * -from .representer import * - -from .resolver import * - -class CBaseLoader(CParser, BaseConstructor, BaseResolver): - - def __init__(self, stream): - CParser.__init__(self, stream) - BaseConstructor.__init__(self) - BaseResolver.__init__(self) - -class CSafeLoader(CParser, SafeConstructor, Resolver): - - def __init__(self, stream): - CParser.__init__(self, stream) - SafeConstructor.__init__(self) - Resolver.__init__(self) - -class CLoader(CParser, Constructor, Resolver): - - def __init__(self, stream): - CParser.__init__(self, stream) - Constructor.__init__(self) - Resolver.__init__(self) - -class CBaseDumper(CEmitter, BaseRepresenter, BaseResolver): - - def __init__(self, stream, - default_style=None, default_flow_style=None, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None): - CEmitter.__init__(self, stream, canonical=canonical, - indent=indent, width=width, encoding=encoding, - allow_unicode=allow_unicode, line_break=line_break, - explicit_start=explicit_start, explicit_end=explicit_end, - version=version, tags=tags) - Representer.__init__(self, default_style=default_style, - default_flow_style=default_flow_style) - Resolver.__init__(self) - -class CSafeDumper(CEmitter, SafeRepresenter, Resolver): - - def __init__(self, stream, - default_style=None, default_flow_style=None, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None): - CEmitter.__init__(self, stream, canonical=canonical, - indent=indent, width=width, encoding=encoding, - allow_unicode=allow_unicode, line_break=line_break, - explicit_start=explicit_start, explicit_end=explicit_end, - version=version, tags=tags) - SafeRepresenter.__init__(self, default_style=default_style, - default_flow_style=default_flow_style) - Resolver.__init__(self) - -class CDumper(CEmitter, Serializer, Representer, Resolver): - - def __init__(self, stream, - default_style=None, default_flow_style=None, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None): - CEmitter.__init__(self, stream, canonical=canonical, - indent=indent, width=width, encoding=encoding, - allow_unicode=allow_unicode, line_break=line_break, - explicit_start=explicit_start, explicit_end=explicit_end, - version=version, tags=tags) - Representer.__init__(self, default_style=default_style, - default_flow_style=default_flow_style) - Resolver.__init__(self) - + +__all__ = ['CBaseLoader', 'CSafeLoader', 'CLoader', + 'CBaseDumper', 'CSafeDumper', 'CDumper'] + +from _yaml import CParser, CEmitter + +from .constructor import * + +from .serializer import * +from .representer import * + +from .resolver import * + +class CBaseLoader(CParser, BaseConstructor, BaseResolver): + + def __init__(self, stream): + CParser.__init__(self, stream) + BaseConstructor.__init__(self) + BaseResolver.__init__(self) + +class CSafeLoader(CParser, SafeConstructor, Resolver): + + def __init__(self, stream): + CParser.__init__(self, stream) + SafeConstructor.__init__(self) + Resolver.__init__(self) + +class CLoader(CParser, Constructor, Resolver): + + def __init__(self, stream): + CParser.__init__(self, stream) + Constructor.__init__(self) + Resolver.__init__(self) + +class CBaseDumper(CEmitter, BaseRepresenter, BaseResolver): + + def __init__(self, stream, + default_style=None, default_flow_style=None, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None): + CEmitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, encoding=encoding, + allow_unicode=allow_unicode, line_break=line_break, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + Representer.__init__(self, default_style=default_style, + default_flow_style=default_flow_style) + Resolver.__init__(self) + +class CSafeDumper(CEmitter, SafeRepresenter, Resolver): + + def __init__(self, stream, + default_style=None, default_flow_style=None, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None): + CEmitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, encoding=encoding, + allow_unicode=allow_unicode, line_break=line_break, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + SafeRepresenter.__init__(self, default_style=default_style, + default_flow_style=default_flow_style) + Resolver.__init__(self) + +class CDumper(CEmitter, Serializer, Representer, Resolver): + + def __init__(self, stream, + default_style=None, default_flow_style=None, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None): + CEmitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, encoding=encoding, + allow_unicode=allow_unicode, line_break=line_break, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + Representer.__init__(self, default_style=default_style, + default_flow_style=default_flow_style) + Resolver.__init__(self) + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py3/dumper.py b/RenderPipelineFile/rplibs/yaml/yaml_py3/dumper.py index 0b691287..a2fd61b4 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py3/dumper.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py3/dumper.py @@ -1,62 +1,62 @@ - -__all__ = ['BaseDumper', 'SafeDumper', 'Dumper'] - -from .emitter import * -from .serializer import * -from .representer import * -from .resolver import * - -class BaseDumper(Emitter, Serializer, BaseRepresenter, BaseResolver): - - def __init__(self, stream, - default_style=None, default_flow_style=None, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None): - Emitter.__init__(self, stream, canonical=canonical, - indent=indent, width=width, - allow_unicode=allow_unicode, line_break=line_break) - Serializer.__init__(self, encoding=encoding, - explicit_start=explicit_start, explicit_end=explicit_end, - version=version, tags=tags) - Representer.__init__(self, default_style=default_style, - default_flow_style=default_flow_style) - Resolver.__init__(self) - -class SafeDumper(Emitter, Serializer, SafeRepresenter, Resolver): - - def __init__(self, stream, - default_style=None, default_flow_style=None, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None): - Emitter.__init__(self, stream, canonical=canonical, - indent=indent, width=width, - allow_unicode=allow_unicode, line_break=line_break) - Serializer.__init__(self, encoding=encoding, - explicit_start=explicit_start, explicit_end=explicit_end, - version=version, tags=tags) - SafeRepresenter.__init__(self, default_style=default_style, - default_flow_style=default_flow_style) - Resolver.__init__(self) - -class Dumper(Emitter, Serializer, Representer, Resolver): - - def __init__(self, stream, - default_style=None, default_flow_style=None, - canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None, - encoding=None, explicit_start=None, explicit_end=None, - version=None, tags=None): - Emitter.__init__(self, stream, canonical=canonical, - indent=indent, width=width, - allow_unicode=allow_unicode, line_break=line_break) - Serializer.__init__(self, encoding=encoding, - explicit_start=explicit_start, explicit_end=explicit_end, - version=version, tags=tags) - Representer.__init__(self, default_style=default_style, - default_flow_style=default_flow_style) - Resolver.__init__(self) - + +__all__ = ['BaseDumper', 'SafeDumper', 'Dumper'] + +from .emitter import * +from .serializer import * +from .representer import * +from .resolver import * + +class BaseDumper(Emitter, Serializer, BaseRepresenter, BaseResolver): + + def __init__(self, stream, + default_style=None, default_flow_style=None, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None): + Emitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break) + Serializer.__init__(self, encoding=encoding, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + Representer.__init__(self, default_style=default_style, + default_flow_style=default_flow_style) + Resolver.__init__(self) + +class SafeDumper(Emitter, Serializer, SafeRepresenter, Resolver): + + def __init__(self, stream, + default_style=None, default_flow_style=None, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None): + Emitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break) + Serializer.__init__(self, encoding=encoding, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + SafeRepresenter.__init__(self, default_style=default_style, + default_flow_style=default_flow_style) + Resolver.__init__(self) + +class Dumper(Emitter, Serializer, Representer, Resolver): + + def __init__(self, stream, + default_style=None, default_flow_style=None, + canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None, + encoding=None, explicit_start=None, explicit_end=None, + version=None, tags=None): + Emitter.__init__(self, stream, canonical=canonical, + indent=indent, width=width, + allow_unicode=allow_unicode, line_break=line_break) + Serializer.__init__(self, encoding=encoding, + explicit_start=explicit_start, explicit_end=explicit_end, + version=version, tags=tags) + Representer.__init__(self, default_style=default_style, + default_flow_style=default_flow_style) + Resolver.__init__(self) + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py3/emitter.py b/RenderPipelineFile/rplibs/yaml/yaml_py3/emitter.py index 34cb145a..f1d21f06 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py3/emitter.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py3/emitter.py @@ -1,1137 +1,1137 @@ - -# Emitter expects events obeying the following grammar: -# stream ::= STREAM-START document* STREAM-END -# document ::= DOCUMENT-START node DOCUMENT-END -# node ::= SCALAR | sequence | mapping -# sequence ::= SEQUENCE-START node* SEQUENCE-END -# mapping ::= MAPPING-START (node node)* MAPPING-END - -__all__ = ['Emitter', 'EmitterError'] - -from .error import YAMLError -from .events import * - -class EmitterError(YAMLError): - pass - -class ScalarAnalysis: - def __init__(self, scalar, empty, multiline, - allow_flow_plain, allow_block_plain, - allow_single_quoted, allow_double_quoted, - allow_block): - self.scalar = scalar - self.empty = empty - self.multiline = multiline - self.allow_flow_plain = allow_flow_plain - self.allow_block_plain = allow_block_plain - self.allow_single_quoted = allow_single_quoted - self.allow_double_quoted = allow_double_quoted - self.allow_block = allow_block - -class Emitter: - - DEFAULT_TAG_PREFIXES = { - '!' : '!', - 'tag:yaml.org,2002:' : '!!', - } - - def __init__(self, stream, canonical=None, indent=None, width=None, - allow_unicode=None, line_break=None): - - # The stream should have the methods `write` and possibly `flush`. - self.stream = stream - - # Encoding can be overriden by STREAM-START. - self.encoding = None - - # Emitter is a state machine with a stack of states to handle nested - # structures. - self.states = [] - self.state = self.expect_stream_start - - # Current event and the event queue. - self.events = [] - self.event = None - - # The current indentation level and the stack of previous indents. - self.indents = [] - self.indent = None - - # Flow level. - self.flow_level = 0 - - # Contexts. - self.root_context = False - self.sequence_context = False - self.mapping_context = False - self.simple_key_context = False - - # Characteristics of the last emitted character: - # - current position. - # - is it a whitespace? - # - is it an indention character - # (indentation space, '-', '?', or ':')? - self.line = 0 - self.column = 0 - self.whitespace = True - self.indention = True - - # Whether the document requires an explicit document indicator - self.open_ended = False - - # Formatting details. - self.canonical = canonical - self.allow_unicode = allow_unicode - self.best_indent = 2 - if indent and 1 < indent < 10: - self.best_indent = indent - self.best_width = 80 - if width and width > self.best_indent*2: - self.best_width = width - self.best_line_break = '\n' - if line_break in ['\r', '\n', '\r\n']: - self.best_line_break = line_break - - # Tag prefixes. - self.tag_prefixes = None - - # Prepared anchor and tag. - self.prepared_anchor = None - self.prepared_tag = None - - # Scalar analysis and style. - self.analysis = None - self.style = None - - def dispose(self): - # Reset the state attributes (to clear self-references) - self.states = [] - self.state = None - - def emit(self, event): - self.events.append(event) - while not self.need_more_events(): - self.event = self.events.pop(0) - self.state() - self.event = None - - # In some cases, we wait for a few next events before emitting. - - def need_more_events(self): - if not self.events: - return True - event = self.events[0] - if isinstance(event, DocumentStartEvent): - return self.need_events(1) - elif isinstance(event, SequenceStartEvent): - return self.need_events(2) - elif isinstance(event, MappingStartEvent): - return self.need_events(3) - else: - return False - - def need_events(self, count): - level = 0 - for event in self.events[1:]: - if isinstance(event, (DocumentStartEvent, CollectionStartEvent)): - level += 1 - elif isinstance(event, (DocumentEndEvent, CollectionEndEvent)): - level -= 1 - elif isinstance(event, StreamEndEvent): - level = -1 - if level < 0: - return False - return (len(self.events) < count+1) - - def increase_indent(self, flow=False, indentless=False): - self.indents.append(self.indent) - if self.indent is None: - if flow: - self.indent = self.best_indent - else: - self.indent = 0 - elif not indentless: - self.indent += self.best_indent - - # States. - - # Stream handlers. - - def expect_stream_start(self): - if isinstance(self.event, StreamStartEvent): - if self.event.encoding and not hasattr(self.stream, 'encoding'): - self.encoding = self.event.encoding - self.write_stream_start() - self.state = self.expect_first_document_start - else: - raise EmitterError("expected StreamStartEvent, but got %s" - % self.event) - - def expect_nothing(self): - raise EmitterError("expected nothing, but got %s" % self.event) - - # Document handlers. - - def expect_first_document_start(self): - return self.expect_document_start(first=True) - - def expect_document_start(self, first=False): - if isinstance(self.event, DocumentStartEvent): - if (self.event.version or self.event.tags) and self.open_ended: - self.write_indicator('...', True) - self.write_indent() - if self.event.version: - version_text = self.prepare_version(self.event.version) - self.write_version_directive(version_text) - self.tag_prefixes = self.DEFAULT_TAG_PREFIXES.copy() - if self.event.tags: - handles = sorted(self.event.tags.keys()) - for handle in handles: - prefix = self.event.tags[handle] - self.tag_prefixes[prefix] = handle - handle_text = self.prepare_tag_handle(handle) - prefix_text = self.prepare_tag_prefix(prefix) - self.write_tag_directive(handle_text, prefix_text) - implicit = (first and not self.event.explicit and not self.canonical - and not self.event.version and not self.event.tags - and not self.check_empty_document()) - if not implicit: - self.write_indent() - self.write_indicator('---', True) - if self.canonical: - self.write_indent() - self.state = self.expect_document_root - elif isinstance(self.event, StreamEndEvent): - if self.open_ended: - self.write_indicator('...', True) - self.write_indent() - self.write_stream_end() - self.state = self.expect_nothing - else: - raise EmitterError("expected DocumentStartEvent, but got %s" - % self.event) - - def expect_document_end(self): - if isinstance(self.event, DocumentEndEvent): - self.write_indent() - if self.event.explicit: - self.write_indicator('...', True) - self.write_indent() - self.flush_stream() - self.state = self.expect_document_start - else: - raise EmitterError("expected DocumentEndEvent, but got %s" - % self.event) - - def expect_document_root(self): - self.states.append(self.expect_document_end) - self.expect_node(root=True) - - # Node handlers. - - def expect_node(self, root=False, sequence=False, mapping=False, - simple_key=False): - self.root_context = root - self.sequence_context = sequence - self.mapping_context = mapping - self.simple_key_context = simple_key - if isinstance(self.event, AliasEvent): - self.expect_alias() - elif isinstance(self.event, (ScalarEvent, CollectionStartEvent)): - self.process_anchor('&') - self.process_tag() - if isinstance(self.event, ScalarEvent): - self.expect_scalar() - elif isinstance(self.event, SequenceStartEvent): - if self.flow_level or self.canonical or self.event.flow_style \ - or self.check_empty_sequence(): - self.expect_flow_sequence() - else: - self.expect_block_sequence() - elif isinstance(self.event, MappingStartEvent): - if self.flow_level or self.canonical or self.event.flow_style \ - or self.check_empty_mapping(): - self.expect_flow_mapping() - else: - self.expect_block_mapping() - else: - raise EmitterError("expected NodeEvent, but got %s" % self.event) - - def expect_alias(self): - if self.event.anchor is None: - raise EmitterError("anchor is not specified for alias") - self.process_anchor('*') - self.state = self.states.pop() - - def expect_scalar(self): - self.increase_indent(flow=True) - self.process_scalar() - self.indent = self.indents.pop() - self.state = self.states.pop() - - # Flow sequence handlers. - - def expect_flow_sequence(self): - self.write_indicator('[', True, whitespace=True) - self.flow_level += 1 - self.increase_indent(flow=True) - self.state = self.expect_first_flow_sequence_item - - def expect_first_flow_sequence_item(self): - if isinstance(self.event, SequenceEndEvent): - self.indent = self.indents.pop() - self.flow_level -= 1 - self.write_indicator(']', False) - self.state = self.states.pop() - else: - if self.canonical or self.column > self.best_width: - self.write_indent() - self.states.append(self.expect_flow_sequence_item) - self.expect_node(sequence=True) - - def expect_flow_sequence_item(self): - if isinstance(self.event, SequenceEndEvent): - self.indent = self.indents.pop() - self.flow_level -= 1 - if self.canonical: - self.write_indicator(',', False) - self.write_indent() - self.write_indicator(']', False) - self.state = self.states.pop() - else: - self.write_indicator(',', False) - if self.canonical or self.column > self.best_width: - self.write_indent() - self.states.append(self.expect_flow_sequence_item) - self.expect_node(sequence=True) - - # Flow mapping handlers. - - def expect_flow_mapping(self): - self.write_indicator('{', True, whitespace=True) - self.flow_level += 1 - self.increase_indent(flow=True) - self.state = self.expect_first_flow_mapping_key - - def expect_first_flow_mapping_key(self): - if isinstance(self.event, MappingEndEvent): - self.indent = self.indents.pop() - self.flow_level -= 1 - self.write_indicator('}', False) - self.state = self.states.pop() - else: - if self.canonical or self.column > self.best_width: - self.write_indent() - if not self.canonical and self.check_simple_key(): - self.states.append(self.expect_flow_mapping_simple_value) - self.expect_node(mapping=True, simple_key=True) - else: - self.write_indicator('?', True) - self.states.append(self.expect_flow_mapping_value) - self.expect_node(mapping=True) - - def expect_flow_mapping_key(self): - if isinstance(self.event, MappingEndEvent): - self.indent = self.indents.pop() - self.flow_level -= 1 - if self.canonical: - self.write_indicator(',', False) - self.write_indent() - self.write_indicator('}', False) - self.state = self.states.pop() - else: - self.write_indicator(',', False) - if self.canonical or self.column > self.best_width: - self.write_indent() - if not self.canonical and self.check_simple_key(): - self.states.append(self.expect_flow_mapping_simple_value) - self.expect_node(mapping=True, simple_key=True) - else: - self.write_indicator('?', True) - self.states.append(self.expect_flow_mapping_value) - self.expect_node(mapping=True) - - def expect_flow_mapping_simple_value(self): - self.write_indicator(':', False) - self.states.append(self.expect_flow_mapping_key) - self.expect_node(mapping=True) - - def expect_flow_mapping_value(self): - if self.canonical or self.column > self.best_width: - self.write_indent() - self.write_indicator(':', True) - self.states.append(self.expect_flow_mapping_key) - self.expect_node(mapping=True) - - # Block sequence handlers. - - def expect_block_sequence(self): - indentless = (self.mapping_context and not self.indention) - self.increase_indent(flow=False, indentless=indentless) - self.state = self.expect_first_block_sequence_item - - def expect_first_block_sequence_item(self): - return self.expect_block_sequence_item(first=True) - - def expect_block_sequence_item(self, first=False): - if not first and isinstance(self.event, SequenceEndEvent): - self.indent = self.indents.pop() - self.state = self.states.pop() - else: - self.write_indent() - self.write_indicator('-', True, indention=True) - self.states.append(self.expect_block_sequence_item) - self.expect_node(sequence=True) - - # Block mapping handlers. - - def expect_block_mapping(self): - self.increase_indent(flow=False) - self.state = self.expect_first_block_mapping_key - - def expect_first_block_mapping_key(self): - return self.expect_block_mapping_key(first=True) - - def expect_block_mapping_key(self, first=False): - if not first and isinstance(self.event, MappingEndEvent): - self.indent = self.indents.pop() - self.state = self.states.pop() - else: - self.write_indent() - if self.check_simple_key(): - self.states.append(self.expect_block_mapping_simple_value) - self.expect_node(mapping=True, simple_key=True) - else: - self.write_indicator('?', True, indention=True) - self.states.append(self.expect_block_mapping_value) - self.expect_node(mapping=True) - - def expect_block_mapping_simple_value(self): - self.write_indicator(':', False) - self.states.append(self.expect_block_mapping_key) - self.expect_node(mapping=True) - - def expect_block_mapping_value(self): - self.write_indent() - self.write_indicator(':', True, indention=True) - self.states.append(self.expect_block_mapping_key) - self.expect_node(mapping=True) - - # Checkers. - - def check_empty_sequence(self): - return (isinstance(self.event, SequenceStartEvent) and self.events - and isinstance(self.events[0], SequenceEndEvent)) - - def check_empty_mapping(self): - return (isinstance(self.event, MappingStartEvent) and self.events - and isinstance(self.events[0], MappingEndEvent)) - - def check_empty_document(self): - if not isinstance(self.event, DocumentStartEvent) or not self.events: - return False - event = self.events[0] - return (isinstance(event, ScalarEvent) and event.anchor is None - and event.tag is None and event.implicit and event.value == '') - - def check_simple_key(self): - length = 0 - if isinstance(self.event, NodeEvent) and self.event.anchor is not None: - if self.prepared_anchor is None: - self.prepared_anchor = self.prepare_anchor(self.event.anchor) - length += len(self.prepared_anchor) - if isinstance(self.event, (ScalarEvent, CollectionStartEvent)) \ - and self.event.tag is not None: - if self.prepared_tag is None: - self.prepared_tag = self.prepare_tag(self.event.tag) - length += len(self.prepared_tag) - if isinstance(self.event, ScalarEvent): - if self.analysis is None: - self.analysis = self.analyze_scalar(self.event.value) - length += len(self.analysis.scalar) - return (length < 128 and (isinstance(self.event, AliasEvent) - or (isinstance(self.event, ScalarEvent) - and not self.analysis.empty and not self.analysis.multiline) - or self.check_empty_sequence() or self.check_empty_mapping())) - - # Anchor, Tag, and Scalar processors. - - def process_anchor(self, indicator): - if self.event.anchor is None: - self.prepared_anchor = None - return - if self.prepared_anchor is None: - self.prepared_anchor = self.prepare_anchor(self.event.anchor) - if self.prepared_anchor: - self.write_indicator(indicator+self.prepared_anchor, True) - self.prepared_anchor = None - - def process_tag(self): - tag = self.event.tag - if isinstance(self.event, ScalarEvent): - if self.style is None: - self.style = self.choose_scalar_style() - if ((not self.canonical or tag is None) and - ((self.style == '' and self.event.implicit[0]) - or (self.style != '' and self.event.implicit[1]))): - self.prepared_tag = None - return - if self.event.implicit[0] and tag is None: - tag = '!' - self.prepared_tag = None - else: - if (not self.canonical or tag is None) and self.event.implicit: - self.prepared_tag = None - return - if tag is None: - raise EmitterError("tag is not specified") - if self.prepared_tag is None: - self.prepared_tag = self.prepare_tag(tag) - if self.prepared_tag: - self.write_indicator(self.prepared_tag, True) - self.prepared_tag = None - - def choose_scalar_style(self): - if self.analysis is None: - self.analysis = self.analyze_scalar(self.event.value) - if self.event.style == '"' or self.canonical: - return '"' - if not self.event.style and self.event.implicit[0]: - if (not (self.simple_key_context and - (self.analysis.empty or self.analysis.multiline)) - and (self.flow_level and self.analysis.allow_flow_plain - or (not self.flow_level and self.analysis.allow_block_plain))): - return '' - if self.event.style and self.event.style in '|>': - if (not self.flow_level and not self.simple_key_context - and self.analysis.allow_block): - return self.event.style - if not self.event.style or self.event.style == '\'': - if (self.analysis.allow_single_quoted and - not (self.simple_key_context and self.analysis.multiline)): - return '\'' - return '"' - - def process_scalar(self): - if self.analysis is None: - self.analysis = self.analyze_scalar(self.event.value) - if self.style is None: - self.style = self.choose_scalar_style() - split = (not self.simple_key_context) - #if self.analysis.multiline and split \ - # and (not self.style or self.style in '\'\"'): - # self.write_indent() - if self.style == '"': - self.write_double_quoted(self.analysis.scalar, split) - elif self.style == '\'': - self.write_single_quoted(self.analysis.scalar, split) - elif self.style == '>': - self.write_folded(self.analysis.scalar) - elif self.style == '|': - self.write_literal(self.analysis.scalar) - else: - self.write_plain(self.analysis.scalar, split) - self.analysis = None - self.style = None - - # Analyzers. - - def prepare_version(self, version): - major, minor = version - if major != 1: - raise EmitterError("unsupported YAML version: %d.%d" % (major, minor)) - return '%d.%d' % (major, minor) - - def prepare_tag_handle(self, handle): - if not handle: - raise EmitterError("tag handle must not be empty") - if handle[0] != '!' or handle[-1] != '!': - raise EmitterError("tag handle must start and end with '!': %r" % handle) - for ch in handle[1:-1]: - if not ('0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ - or ch in '-_'): - raise EmitterError("invalid character %r in the tag handle: %r" - % (ch, handle)) - return handle - - def prepare_tag_prefix(self, prefix): - if not prefix: - raise EmitterError("tag prefix must not be empty") - chunks = [] - start = end = 0 - if prefix[0] == '!': - end = 1 - while end < len(prefix): - ch = prefix[end] - if '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ - or ch in '-;/?!:@&=+$,_.~*\'()[]': - end += 1 - else: - if start < end: - chunks.append(prefix[start:end]) - start = end = end+1 - data = ch.encode('utf-8') - for ch in data: - chunks.append('%%%02X' % ord(ch)) - if start < end: - chunks.append(prefix[start:end]) - return ''.join(chunks) - - def prepare_tag(self, tag): - if not tag: - raise EmitterError("tag must not be empty") - if tag == '!': - return tag - handle = None - suffix = tag - prefixes = sorted(self.tag_prefixes.keys()) - for prefix in prefixes: - if tag.startswith(prefix) \ - and (prefix == '!' or len(prefix) < len(tag)): - handle = self.tag_prefixes[prefix] - suffix = tag[len(prefix):] - chunks = [] - start = end = 0 - while end < len(suffix): - ch = suffix[end] - if '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ - or ch in '-;/?:@&=+$,_.~*\'()[]' \ - or (ch == '!' and handle != '!'): - end += 1 - else: - if start < end: - chunks.append(suffix[start:end]) - start = end = end+1 - data = ch.encode('utf-8') - for ch in data: - chunks.append('%%%02X' % ord(ch)) - if start < end: - chunks.append(suffix[start:end]) - suffix_text = ''.join(chunks) - if handle: - return '%s%s' % (handle, suffix_text) - else: - return '!<%s>' % suffix_text - - def prepare_anchor(self, anchor): - if not anchor: - raise EmitterError("anchor must not be empty") - for ch in anchor: - if not ('0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ - or ch in '-_'): - raise EmitterError("invalid character %r in the anchor: %r" - % (ch, anchor)) - return anchor - - def analyze_scalar(self, scalar): - - # Empty scalar is a special case. - if not scalar: - return ScalarAnalysis(scalar=scalar, empty=True, multiline=False, - allow_flow_plain=False, allow_block_plain=True, - allow_single_quoted=True, allow_double_quoted=True, - allow_block=False) - - # Indicators and special characters. - block_indicators = False - flow_indicators = False - line_breaks = False - special_characters = False - - # Important whitespace combinations. - leading_space = False - leading_break = False - trailing_space = False - trailing_break = False - break_space = False - space_break = False - - # Check document indicators. - if scalar.startswith('---') or scalar.startswith('...'): - block_indicators = True - flow_indicators = True - - # First character or preceded by a whitespace. - preceeded_by_whitespace = True - - # Last character or followed by a whitespace. - followed_by_whitespace = (len(scalar) == 1 or - scalar[1] in '\0 \t\r\n\x85\u2028\u2029') - - # The previous character is a space. - previous_space = False - - # The previous character is a break. - previous_break = False - - index = 0 - while index < len(scalar): - ch = scalar[index] - - # Check for indicators. - if index == 0: - # Leading indicators are special characters. - if ch in '#,[]{}&*!|>\'\"%@`': - flow_indicators = True - block_indicators = True - if ch in '?:': - flow_indicators = True - if followed_by_whitespace: - block_indicators = True - if ch == '-' and followed_by_whitespace: - flow_indicators = True - block_indicators = True - else: - # Some indicators cannot appear within a scalar as well. - if ch in ',?[]{}': - flow_indicators = True - if ch == ':': - flow_indicators = True - if followed_by_whitespace: - block_indicators = True - if ch == '#' and preceeded_by_whitespace: - flow_indicators = True - block_indicators = True - - # Check for line breaks, special, and unicode characters. - if ch in '\n\x85\u2028\u2029': - line_breaks = True - if not (ch == '\n' or '\x20' <= ch <= '\x7E'): - if (ch == '\x85' or '\xA0' <= ch <= '\uD7FF' - or '\uE000' <= ch <= '\uFFFD') and ch != '\uFEFF': - unicode_characters = True - if not self.allow_unicode: - special_characters = True - else: - special_characters = True - - # Detect important whitespace combinations. - if ch == ' ': - if index == 0: - leading_space = True - if index == len(scalar)-1: - trailing_space = True - if previous_break: - break_space = True - previous_space = True - previous_break = False - elif ch in '\n\x85\u2028\u2029': - if index == 0: - leading_break = True - if index == len(scalar)-1: - trailing_break = True - if previous_space: - space_break = True - previous_space = False - previous_break = True - else: - previous_space = False - previous_break = False - - # Prepare for the next character. - index += 1 - preceeded_by_whitespace = (ch in '\0 \t\r\n\x85\u2028\u2029') - followed_by_whitespace = (index+1 >= len(scalar) or - scalar[index+1] in '\0 \t\r\n\x85\u2028\u2029') - - # Let's decide what styles are allowed. - allow_flow_plain = True - allow_block_plain = True - allow_single_quoted = True - allow_double_quoted = True - allow_block = True - - # Leading and trailing whitespaces are bad for plain scalars. - if (leading_space or leading_break - or trailing_space or trailing_break): - allow_flow_plain = allow_block_plain = False - - # We do not permit trailing spaces for block scalars. - if trailing_space: - allow_block = False - - # Spaces at the beginning of a new line are only acceptable for block - # scalars. - if break_space: - allow_flow_plain = allow_block_plain = allow_single_quoted = False - - # Spaces followed by breaks, as well as special character are only - # allowed for double quoted scalars. - if space_break or special_characters: - allow_flow_plain = allow_block_plain = \ - allow_single_quoted = allow_block = False - - # Although the plain scalar writer supports breaks, we never emit - # multiline plain scalars. - if line_breaks: - allow_flow_plain = allow_block_plain = False - - # Flow indicators are forbidden for flow plain scalars. - if flow_indicators: - allow_flow_plain = False - - # Block indicators are forbidden for block plain scalars. - if block_indicators: - allow_block_plain = False - - return ScalarAnalysis(scalar=scalar, - empty=False, multiline=line_breaks, - allow_flow_plain=allow_flow_plain, - allow_block_plain=allow_block_plain, - allow_single_quoted=allow_single_quoted, - allow_double_quoted=allow_double_quoted, - allow_block=allow_block) - - # Writers. - - def flush_stream(self): - if hasattr(self.stream, 'flush'): - self.stream.flush() - - def write_stream_start(self): - # Write BOM if needed. - if self.encoding and self.encoding.startswith('utf-16'): - self.stream.write('\uFEFF'.encode(self.encoding)) - - def write_stream_end(self): - self.flush_stream() - - def write_indicator(self, indicator, need_whitespace, - whitespace=False, indention=False): - if self.whitespace or not need_whitespace: - data = indicator - else: - data = ' '+indicator - self.whitespace = whitespace - self.indention = self.indention and indention - self.column += len(data) - self.open_ended = False - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - - def write_indent(self): - indent = self.indent or 0 - if not self.indention or self.column > indent \ - or (self.column == indent and not self.whitespace): - self.write_line_break() - if self.column < indent: - self.whitespace = True - data = ' '*(indent-self.column) - self.column = indent - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - - def write_line_break(self, data=None): - if data is None: - data = self.best_line_break - self.whitespace = True - self.indention = True - self.line += 1 - self.column = 0 - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - - def write_version_directive(self, version_text): - data = '%%YAML %s' % version_text - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - self.write_line_break() - - def write_tag_directive(self, handle_text, prefix_text): - data = '%%TAG %s %s' % (handle_text, prefix_text) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - self.write_line_break() - - # Scalar streams. - - def write_single_quoted(self, text, split=True): - self.write_indicator('\'', True) - spaces = False - breaks = False - start = end = 0 - while end <= len(text): - ch = None - if end < len(text): - ch = text[end] - if spaces: - if ch is None or ch != ' ': - if start+1 == end and self.column > self.best_width and split \ - and start != 0 and end != len(text): - self.write_indent() - else: - data = text[start:end] - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - start = end - elif breaks: - if ch is None or ch not in '\n\x85\u2028\u2029': - if text[start] == '\n': - self.write_line_break() - for br in text[start:end]: - if br == '\n': - self.write_line_break() - else: - self.write_line_break(br) - self.write_indent() - start = end - else: - if ch is None or ch in ' \n\x85\u2028\u2029' or ch == '\'': - if start < end: - data = text[start:end] - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - start = end - if ch == '\'': - data = '\'\'' - self.column += 2 - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - start = end + 1 - if ch is not None: - spaces = (ch == ' ') - breaks = (ch in '\n\x85\u2028\u2029') - end += 1 - self.write_indicator('\'', False) - - ESCAPE_REPLACEMENTS = { - '\0': '0', - '\x07': 'a', - '\x08': 'b', - '\x09': 't', - '\x0A': 'n', - '\x0B': 'v', - '\x0C': 'f', - '\x0D': 'r', - '\x1B': 'e', - '\"': '\"', - '\\': '\\', - '\x85': 'N', - '\xA0': '_', - '\u2028': 'L', - '\u2029': 'P', - } - - def write_double_quoted(self, text, split=True): - self.write_indicator('"', True) - start = end = 0 - while end <= len(text): - ch = None - if end < len(text): - ch = text[end] - if ch is None or ch in '"\\\x85\u2028\u2029\uFEFF' \ - or not ('\x20' <= ch <= '\x7E' - or (self.allow_unicode - and ('\xA0' <= ch <= '\uD7FF' - or '\uE000' <= ch <= '\uFFFD'))): - if start < end: - data = text[start:end] - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - start = end - if ch is not None: - if ch in self.ESCAPE_REPLACEMENTS: - data = '\\'+self.ESCAPE_REPLACEMENTS[ch] - elif ch <= '\xFF': - data = '\\x%02X' % ord(ch) - elif ch <= '\uFFFF': - data = '\\u%04X' % ord(ch) - else: - data = '\\U%08X' % ord(ch) - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - start = end+1 - if 0 < end < len(text)-1 and (ch == ' ' or start >= end) \ - and self.column+(end-start) > self.best_width and split: - data = text[start:end]+'\\' - if start < end: - start = end - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - self.write_indent() - self.whitespace = False - self.indention = False - if text[start] == ' ': - data = '\\' - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - end += 1 - self.write_indicator('"', False) - - def determine_block_hints(self, text): - hints = '' - if text: - if text[0] in ' \n\x85\u2028\u2029': - hints += str(self.best_indent) - if text[-1] not in '\n\x85\u2028\u2029': - hints += '-' - elif len(text) == 1 or text[-2] in '\n\x85\u2028\u2029': - hints += '+' - return hints - - def write_folded(self, text): - hints = self.determine_block_hints(text) - self.write_indicator('>'+hints, True) - if hints[-1:] == '+': - self.open_ended = True - self.write_line_break() - leading_space = True - spaces = False - breaks = True - start = end = 0 - while end <= len(text): - ch = None - if end < len(text): - ch = text[end] - if breaks: - if ch is None or ch not in '\n\x85\u2028\u2029': - if not leading_space and ch is not None and ch != ' ' \ - and text[start] == '\n': - self.write_line_break() - leading_space = (ch == ' ') - for br in text[start:end]: - if br == '\n': - self.write_line_break() - else: - self.write_line_break(br) - if ch is not None: - self.write_indent() - start = end - elif spaces: - if ch != ' ': - if start+1 == end and self.column > self.best_width: - self.write_indent() - else: - data = text[start:end] - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - start = end - else: - if ch is None or ch in ' \n\x85\u2028\u2029': - data = text[start:end] - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - if ch is None: - self.write_line_break() - start = end - if ch is not None: - breaks = (ch in '\n\x85\u2028\u2029') - spaces = (ch == ' ') - end += 1 - - def write_literal(self, text): - hints = self.determine_block_hints(text) - self.write_indicator('|'+hints, True) - if hints[-1:] == '+': - self.open_ended = True - self.write_line_break() - breaks = True - start = end = 0 - while end <= len(text): - ch = None - if end < len(text): - ch = text[end] - if breaks: - if ch is None or ch not in '\n\x85\u2028\u2029': - for br in text[start:end]: - if br == '\n': - self.write_line_break() - else: - self.write_line_break(br) - if ch is not None: - self.write_indent() - start = end - else: - if ch is None or ch in '\n\x85\u2028\u2029': - data = text[start:end] - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - if ch is None: - self.write_line_break() - start = end - if ch is not None: - breaks = (ch in '\n\x85\u2028\u2029') - end += 1 - - def write_plain(self, text, split=True): - if self.root_context: - self.open_ended = True - if not text: - return - if not self.whitespace: - data = ' ' - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - self.whitespace = False - self.indention = False - spaces = False - breaks = False - start = end = 0 - while end <= len(text): - ch = None - if end < len(text): - ch = text[end] - if spaces: - if ch != ' ': - if start+1 == end and self.column > self.best_width and split: - self.write_indent() - self.whitespace = False - self.indention = False - else: - data = text[start:end] - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - start = end - elif breaks: - if ch not in '\n\x85\u2028\u2029': - if text[start] == '\n': - self.write_line_break() - for br in text[start:end]: - if br == '\n': - self.write_line_break() - else: - self.write_line_break(br) - self.write_indent() - self.whitespace = False - self.indention = False - start = end - else: - if ch is None or ch in ' \n\x85\u2028\u2029': - data = text[start:end] - self.column += len(data) - if self.encoding: - data = data.encode(self.encoding) - self.stream.write(data) - start = end - if ch is not None: - spaces = (ch == ' ') - breaks = (ch in '\n\x85\u2028\u2029') - end += 1 - + +# Emitter expects events obeying the following grammar: +# stream ::= STREAM-START document* STREAM-END +# document ::= DOCUMENT-START node DOCUMENT-END +# node ::= SCALAR | sequence | mapping +# sequence ::= SEQUENCE-START node* SEQUENCE-END +# mapping ::= MAPPING-START (node node)* MAPPING-END + +__all__ = ['Emitter', 'EmitterError'] + +from .error import YAMLError +from .events import * + +class EmitterError(YAMLError): + pass + +class ScalarAnalysis: + def __init__(self, scalar, empty, multiline, + allow_flow_plain, allow_block_plain, + allow_single_quoted, allow_double_quoted, + allow_block): + self.scalar = scalar + self.empty = empty + self.multiline = multiline + self.allow_flow_plain = allow_flow_plain + self.allow_block_plain = allow_block_plain + self.allow_single_quoted = allow_single_quoted + self.allow_double_quoted = allow_double_quoted + self.allow_block = allow_block + +class Emitter: + + DEFAULT_TAG_PREFIXES = { + '!' : '!', + 'tag:yaml.org,2002:' : '!!', + } + + def __init__(self, stream, canonical=None, indent=None, width=None, + allow_unicode=None, line_break=None): + + # The stream should have the methods `write` and possibly `flush`. + self.stream = stream + + # Encoding can be overriden by STREAM-START. + self.encoding = None + + # Emitter is a state machine with a stack of states to handle nested + # structures. + self.states = [] + self.state = self.expect_stream_start + + # Current event and the event queue. + self.events = [] + self.event = None + + # The current indentation level and the stack of previous indents. + self.indents = [] + self.indent = None + + # Flow level. + self.flow_level = 0 + + # Contexts. + self.root_context = False + self.sequence_context = False + self.mapping_context = False + self.simple_key_context = False + + # Characteristics of the last emitted character: + # - current position. + # - is it a whitespace? + # - is it an indention character + # (indentation space, '-', '?', or ':')? + self.line = 0 + self.column = 0 + self.whitespace = True + self.indention = True + + # Whether the document requires an explicit document indicator + self.open_ended = False + + # Formatting details. + self.canonical = canonical + self.allow_unicode = allow_unicode + self.best_indent = 2 + if indent and 1 < indent < 10: + self.best_indent = indent + self.best_width = 80 + if width and width > self.best_indent*2: + self.best_width = width + self.best_line_break = '\n' + if line_break in ['\r', '\n', '\r\n']: + self.best_line_break = line_break + + # Tag prefixes. + self.tag_prefixes = None + + # Prepared anchor and tag. + self.prepared_anchor = None + self.prepared_tag = None + + # Scalar analysis and style. + self.analysis = None + self.style = None + + def dispose(self): + # Reset the state attributes (to clear self-references) + self.states = [] + self.state = None + + def emit(self, event): + self.events.append(event) + while not self.need_more_events(): + self.event = self.events.pop(0) + self.state() + self.event = None + + # In some cases, we wait for a few next events before emitting. + + def need_more_events(self): + if not self.events: + return True + event = self.events[0] + if isinstance(event, DocumentStartEvent): + return self.need_events(1) + elif isinstance(event, SequenceStartEvent): + return self.need_events(2) + elif isinstance(event, MappingStartEvent): + return self.need_events(3) + else: + return False + + def need_events(self, count): + level = 0 + for event in self.events[1:]: + if isinstance(event, (DocumentStartEvent, CollectionStartEvent)): + level += 1 + elif isinstance(event, (DocumentEndEvent, CollectionEndEvent)): + level -= 1 + elif isinstance(event, StreamEndEvent): + level = -1 + if level < 0: + return False + return (len(self.events) < count+1) + + def increase_indent(self, flow=False, indentless=False): + self.indents.append(self.indent) + if self.indent is None: + if flow: + self.indent = self.best_indent + else: + self.indent = 0 + elif not indentless: + self.indent += self.best_indent + + # States. + + # Stream handlers. + + def expect_stream_start(self): + if isinstance(self.event, StreamStartEvent): + if self.event.encoding and not hasattr(self.stream, 'encoding'): + self.encoding = self.event.encoding + self.write_stream_start() + self.state = self.expect_first_document_start + else: + raise EmitterError("expected StreamStartEvent, but got %s" + % self.event) + + def expect_nothing(self): + raise EmitterError("expected nothing, but got %s" % self.event) + + # Document handlers. + + def expect_first_document_start(self): + return self.expect_document_start(first=True) + + def expect_document_start(self, first=False): + if isinstance(self.event, DocumentStartEvent): + if (self.event.version or self.event.tags) and self.open_ended: + self.write_indicator('...', True) + self.write_indent() + if self.event.version: + version_text = self.prepare_version(self.event.version) + self.write_version_directive(version_text) + self.tag_prefixes = self.DEFAULT_TAG_PREFIXES.copy() + if self.event.tags: + handles = sorted(self.event.tags.keys()) + for handle in handles: + prefix = self.event.tags[handle] + self.tag_prefixes[prefix] = handle + handle_text = self.prepare_tag_handle(handle) + prefix_text = self.prepare_tag_prefix(prefix) + self.write_tag_directive(handle_text, prefix_text) + implicit = (first and not self.event.explicit and not self.canonical + and not self.event.version and not self.event.tags + and not self.check_empty_document()) + if not implicit: + self.write_indent() + self.write_indicator('---', True) + if self.canonical: + self.write_indent() + self.state = self.expect_document_root + elif isinstance(self.event, StreamEndEvent): + if self.open_ended: + self.write_indicator('...', True) + self.write_indent() + self.write_stream_end() + self.state = self.expect_nothing + else: + raise EmitterError("expected DocumentStartEvent, but got %s" + % self.event) + + def expect_document_end(self): + if isinstance(self.event, DocumentEndEvent): + self.write_indent() + if self.event.explicit: + self.write_indicator('...', True) + self.write_indent() + self.flush_stream() + self.state = self.expect_document_start + else: + raise EmitterError("expected DocumentEndEvent, but got %s" + % self.event) + + def expect_document_root(self): + self.states.append(self.expect_document_end) + self.expect_node(root=True) + + # Node handlers. + + def expect_node(self, root=False, sequence=False, mapping=False, + simple_key=False): + self.root_context = root + self.sequence_context = sequence + self.mapping_context = mapping + self.simple_key_context = simple_key + if isinstance(self.event, AliasEvent): + self.expect_alias() + elif isinstance(self.event, (ScalarEvent, CollectionStartEvent)): + self.process_anchor('&') + self.process_tag() + if isinstance(self.event, ScalarEvent): + self.expect_scalar() + elif isinstance(self.event, SequenceStartEvent): + if self.flow_level or self.canonical or self.event.flow_style \ + or self.check_empty_sequence(): + self.expect_flow_sequence() + else: + self.expect_block_sequence() + elif isinstance(self.event, MappingStartEvent): + if self.flow_level or self.canonical or self.event.flow_style \ + or self.check_empty_mapping(): + self.expect_flow_mapping() + else: + self.expect_block_mapping() + else: + raise EmitterError("expected NodeEvent, but got %s" % self.event) + + def expect_alias(self): + if self.event.anchor is None: + raise EmitterError("anchor is not specified for alias") + self.process_anchor('*') + self.state = self.states.pop() + + def expect_scalar(self): + self.increase_indent(flow=True) + self.process_scalar() + self.indent = self.indents.pop() + self.state = self.states.pop() + + # Flow sequence handlers. + + def expect_flow_sequence(self): + self.write_indicator('[', True, whitespace=True) + self.flow_level += 1 + self.increase_indent(flow=True) + self.state = self.expect_first_flow_sequence_item + + def expect_first_flow_sequence_item(self): + if isinstance(self.event, SequenceEndEvent): + self.indent = self.indents.pop() + self.flow_level -= 1 + self.write_indicator(']', False) + self.state = self.states.pop() + else: + if self.canonical or self.column > self.best_width: + self.write_indent() + self.states.append(self.expect_flow_sequence_item) + self.expect_node(sequence=True) + + def expect_flow_sequence_item(self): + if isinstance(self.event, SequenceEndEvent): + self.indent = self.indents.pop() + self.flow_level -= 1 + if self.canonical: + self.write_indicator(',', False) + self.write_indent() + self.write_indicator(']', False) + self.state = self.states.pop() + else: + self.write_indicator(',', False) + if self.canonical or self.column > self.best_width: + self.write_indent() + self.states.append(self.expect_flow_sequence_item) + self.expect_node(sequence=True) + + # Flow mapping handlers. + + def expect_flow_mapping(self): + self.write_indicator('{', True, whitespace=True) + self.flow_level += 1 + self.increase_indent(flow=True) + self.state = self.expect_first_flow_mapping_key + + def expect_first_flow_mapping_key(self): + if isinstance(self.event, MappingEndEvent): + self.indent = self.indents.pop() + self.flow_level -= 1 + self.write_indicator('}', False) + self.state = self.states.pop() + else: + if self.canonical or self.column > self.best_width: + self.write_indent() + if not self.canonical and self.check_simple_key(): + self.states.append(self.expect_flow_mapping_simple_value) + self.expect_node(mapping=True, simple_key=True) + else: + self.write_indicator('?', True) + self.states.append(self.expect_flow_mapping_value) + self.expect_node(mapping=True) + + def expect_flow_mapping_key(self): + if isinstance(self.event, MappingEndEvent): + self.indent = self.indents.pop() + self.flow_level -= 1 + if self.canonical: + self.write_indicator(',', False) + self.write_indent() + self.write_indicator('}', False) + self.state = self.states.pop() + else: + self.write_indicator(',', False) + if self.canonical or self.column > self.best_width: + self.write_indent() + if not self.canonical and self.check_simple_key(): + self.states.append(self.expect_flow_mapping_simple_value) + self.expect_node(mapping=True, simple_key=True) + else: + self.write_indicator('?', True) + self.states.append(self.expect_flow_mapping_value) + self.expect_node(mapping=True) + + def expect_flow_mapping_simple_value(self): + self.write_indicator(':', False) + self.states.append(self.expect_flow_mapping_key) + self.expect_node(mapping=True) + + def expect_flow_mapping_value(self): + if self.canonical or self.column > self.best_width: + self.write_indent() + self.write_indicator(':', True) + self.states.append(self.expect_flow_mapping_key) + self.expect_node(mapping=True) + + # Block sequence handlers. + + def expect_block_sequence(self): + indentless = (self.mapping_context and not self.indention) + self.increase_indent(flow=False, indentless=indentless) + self.state = self.expect_first_block_sequence_item + + def expect_first_block_sequence_item(self): + return self.expect_block_sequence_item(first=True) + + def expect_block_sequence_item(self, first=False): + if not first and isinstance(self.event, SequenceEndEvent): + self.indent = self.indents.pop() + self.state = self.states.pop() + else: + self.write_indent() + self.write_indicator('-', True, indention=True) + self.states.append(self.expect_block_sequence_item) + self.expect_node(sequence=True) + + # Block mapping handlers. + + def expect_block_mapping(self): + self.increase_indent(flow=False) + self.state = self.expect_first_block_mapping_key + + def expect_first_block_mapping_key(self): + return self.expect_block_mapping_key(first=True) + + def expect_block_mapping_key(self, first=False): + if not first and isinstance(self.event, MappingEndEvent): + self.indent = self.indents.pop() + self.state = self.states.pop() + else: + self.write_indent() + if self.check_simple_key(): + self.states.append(self.expect_block_mapping_simple_value) + self.expect_node(mapping=True, simple_key=True) + else: + self.write_indicator('?', True, indention=True) + self.states.append(self.expect_block_mapping_value) + self.expect_node(mapping=True) + + def expect_block_mapping_simple_value(self): + self.write_indicator(':', False) + self.states.append(self.expect_block_mapping_key) + self.expect_node(mapping=True) + + def expect_block_mapping_value(self): + self.write_indent() + self.write_indicator(':', True, indention=True) + self.states.append(self.expect_block_mapping_key) + self.expect_node(mapping=True) + + # Checkers. + + def check_empty_sequence(self): + return (isinstance(self.event, SequenceStartEvent) and self.events + and isinstance(self.events[0], SequenceEndEvent)) + + def check_empty_mapping(self): + return (isinstance(self.event, MappingStartEvent) and self.events + and isinstance(self.events[0], MappingEndEvent)) + + def check_empty_document(self): + if not isinstance(self.event, DocumentStartEvent) or not self.events: + return False + event = self.events[0] + return (isinstance(event, ScalarEvent) and event.anchor is None + and event.tag is None and event.implicit and event.value == '') + + def check_simple_key(self): + length = 0 + if isinstance(self.event, NodeEvent) and self.event.anchor is not None: + if self.prepared_anchor is None: + self.prepared_anchor = self.prepare_anchor(self.event.anchor) + length += len(self.prepared_anchor) + if isinstance(self.event, (ScalarEvent, CollectionStartEvent)) \ + and self.event.tag is not None: + if self.prepared_tag is None: + self.prepared_tag = self.prepare_tag(self.event.tag) + length += len(self.prepared_tag) + if isinstance(self.event, ScalarEvent): + if self.analysis is None: + self.analysis = self.analyze_scalar(self.event.value) + length += len(self.analysis.scalar) + return (length < 128 and (isinstance(self.event, AliasEvent) + or (isinstance(self.event, ScalarEvent) + and not self.analysis.empty and not self.analysis.multiline) + or self.check_empty_sequence() or self.check_empty_mapping())) + + # Anchor, Tag, and Scalar processors. + + def process_anchor(self, indicator): + if self.event.anchor is None: + self.prepared_anchor = None + return + if self.prepared_anchor is None: + self.prepared_anchor = self.prepare_anchor(self.event.anchor) + if self.prepared_anchor: + self.write_indicator(indicator+self.prepared_anchor, True) + self.prepared_anchor = None + + def process_tag(self): + tag = self.event.tag + if isinstance(self.event, ScalarEvent): + if self.style is None: + self.style = self.choose_scalar_style() + if ((not self.canonical or tag is None) and + ((self.style == '' and self.event.implicit[0]) + or (self.style != '' and self.event.implicit[1]))): + self.prepared_tag = None + return + if self.event.implicit[0] and tag is None: + tag = '!' + self.prepared_tag = None + else: + if (not self.canonical or tag is None) and self.event.implicit: + self.prepared_tag = None + return + if tag is None: + raise EmitterError("tag is not specified") + if self.prepared_tag is None: + self.prepared_tag = self.prepare_tag(tag) + if self.prepared_tag: + self.write_indicator(self.prepared_tag, True) + self.prepared_tag = None + + def choose_scalar_style(self): + if self.analysis is None: + self.analysis = self.analyze_scalar(self.event.value) + if self.event.style == '"' or self.canonical: + return '"' + if not self.event.style and self.event.implicit[0]: + if (not (self.simple_key_context and + (self.analysis.empty or self.analysis.multiline)) + and (self.flow_level and self.analysis.allow_flow_plain + or (not self.flow_level and self.analysis.allow_block_plain))): + return '' + if self.event.style and self.event.style in '|>': + if (not self.flow_level and not self.simple_key_context + and self.analysis.allow_block): + return self.event.style + if not self.event.style or self.event.style == '\'': + if (self.analysis.allow_single_quoted and + not (self.simple_key_context and self.analysis.multiline)): + return '\'' + return '"' + + def process_scalar(self): + if self.analysis is None: + self.analysis = self.analyze_scalar(self.event.value) + if self.style is None: + self.style = self.choose_scalar_style() + split = (not self.simple_key_context) + #if self.analysis.multiline and split \ + # and (not self.style or self.style in '\'\"'): + # self.write_indent() + if self.style == '"': + self.write_double_quoted(self.analysis.scalar, split) + elif self.style == '\'': + self.write_single_quoted(self.analysis.scalar, split) + elif self.style == '>': + self.write_folded(self.analysis.scalar) + elif self.style == '|': + self.write_literal(self.analysis.scalar) + else: + self.write_plain(self.analysis.scalar, split) + self.analysis = None + self.style = None + + # Analyzers. + + def prepare_version(self, version): + major, minor = version + if major != 1: + raise EmitterError("unsupported YAML version: %d.%d" % (major, minor)) + return '%d.%d' % (major, minor) + + def prepare_tag_handle(self, handle): + if not handle: + raise EmitterError("tag handle must not be empty") + if handle[0] != '!' or handle[-1] != '!': + raise EmitterError("tag handle must start and end with '!': %r" % handle) + for ch in handle[1:-1]: + if not ('0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-_'): + raise EmitterError("invalid character %r in the tag handle: %r" + % (ch, handle)) + return handle + + def prepare_tag_prefix(self, prefix): + if not prefix: + raise EmitterError("tag prefix must not be empty") + chunks = [] + start = end = 0 + if prefix[0] == '!': + end = 1 + while end < len(prefix): + ch = prefix[end] + if '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-;/?!:@&=+$,_.~*\'()[]': + end += 1 + else: + if start < end: + chunks.append(prefix[start:end]) + start = end = end+1 + data = ch.encode('utf-8') + for ch in data: + chunks.append('%%%02X' % ord(ch)) + if start < end: + chunks.append(prefix[start:end]) + return ''.join(chunks) + + def prepare_tag(self, tag): + if not tag: + raise EmitterError("tag must not be empty") + if tag == '!': + return tag + handle = None + suffix = tag + prefixes = sorted(self.tag_prefixes.keys()) + for prefix in prefixes: + if tag.startswith(prefix) \ + and (prefix == '!' or len(prefix) < len(tag)): + handle = self.tag_prefixes[prefix] + suffix = tag[len(prefix):] + chunks = [] + start = end = 0 + while end < len(suffix): + ch = suffix[end] + if '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-;/?:@&=+$,_.~*\'()[]' \ + or (ch == '!' and handle != '!'): + end += 1 + else: + if start < end: + chunks.append(suffix[start:end]) + start = end = end+1 + data = ch.encode('utf-8') + for ch in data: + chunks.append('%%%02X' % ord(ch)) + if start < end: + chunks.append(suffix[start:end]) + suffix_text = ''.join(chunks) + if handle: + return '%s%s' % (handle, suffix_text) + else: + return '!<%s>' % suffix_text + + def prepare_anchor(self, anchor): + if not anchor: + raise EmitterError("anchor must not be empty") + for ch in anchor: + if not ('0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-_'): + raise EmitterError("invalid character %r in the anchor: %r" + % (ch, anchor)) + return anchor + + def analyze_scalar(self, scalar): + + # Empty scalar is a special case. + if not scalar: + return ScalarAnalysis(scalar=scalar, empty=True, multiline=False, + allow_flow_plain=False, allow_block_plain=True, + allow_single_quoted=True, allow_double_quoted=True, + allow_block=False) + + # Indicators and special characters. + block_indicators = False + flow_indicators = False + line_breaks = False + special_characters = False + + # Important whitespace combinations. + leading_space = False + leading_break = False + trailing_space = False + trailing_break = False + break_space = False + space_break = False + + # Check document indicators. + if scalar.startswith('---') or scalar.startswith('...'): + block_indicators = True + flow_indicators = True + + # First character or preceded by a whitespace. + preceeded_by_whitespace = True + + # Last character or followed by a whitespace. + followed_by_whitespace = (len(scalar) == 1 or + scalar[1] in '\0 \t\r\n\x85\u2028\u2029') + + # The previous character is a space. + previous_space = False + + # The previous character is a break. + previous_break = False + + index = 0 + while index < len(scalar): + ch = scalar[index] + + # Check for indicators. + if index == 0: + # Leading indicators are special characters. + if ch in '#,[]{}&*!|>\'\"%@`': + flow_indicators = True + block_indicators = True + if ch in '?:': + flow_indicators = True + if followed_by_whitespace: + block_indicators = True + if ch == '-' and followed_by_whitespace: + flow_indicators = True + block_indicators = True + else: + # Some indicators cannot appear within a scalar as well. + if ch in ',?[]{}': + flow_indicators = True + if ch == ':': + flow_indicators = True + if followed_by_whitespace: + block_indicators = True + if ch == '#' and preceeded_by_whitespace: + flow_indicators = True + block_indicators = True + + # Check for line breaks, special, and unicode characters. + if ch in '\n\x85\u2028\u2029': + line_breaks = True + if not (ch == '\n' or '\x20' <= ch <= '\x7E'): + if (ch == '\x85' or '\xA0' <= ch <= '\uD7FF' + or '\uE000' <= ch <= '\uFFFD') and ch != '\uFEFF': + unicode_characters = True + if not self.allow_unicode: + special_characters = True + else: + special_characters = True + + # Detect important whitespace combinations. + if ch == ' ': + if index == 0: + leading_space = True + if index == len(scalar)-1: + trailing_space = True + if previous_break: + break_space = True + previous_space = True + previous_break = False + elif ch in '\n\x85\u2028\u2029': + if index == 0: + leading_break = True + if index == len(scalar)-1: + trailing_break = True + if previous_space: + space_break = True + previous_space = False + previous_break = True + else: + previous_space = False + previous_break = False + + # Prepare for the next character. + index += 1 + preceeded_by_whitespace = (ch in '\0 \t\r\n\x85\u2028\u2029') + followed_by_whitespace = (index+1 >= len(scalar) or + scalar[index+1] in '\0 \t\r\n\x85\u2028\u2029') + + # Let's decide what styles are allowed. + allow_flow_plain = True + allow_block_plain = True + allow_single_quoted = True + allow_double_quoted = True + allow_block = True + + # Leading and trailing whitespaces are bad for plain scalars. + if (leading_space or leading_break + or trailing_space or trailing_break): + allow_flow_plain = allow_block_plain = False + + # We do not permit trailing spaces for block scalars. + if trailing_space: + allow_block = False + + # Spaces at the beginning of a new line are only acceptable for block + # scalars. + if break_space: + allow_flow_plain = allow_block_plain = allow_single_quoted = False + + # Spaces followed by breaks, as well as special character are only + # allowed for double quoted scalars. + if space_break or special_characters: + allow_flow_plain = allow_block_plain = \ + allow_single_quoted = allow_block = False + + # Although the plain scalar writer supports breaks, we never emit + # multiline plain scalars. + if line_breaks: + allow_flow_plain = allow_block_plain = False + + # Flow indicators are forbidden for flow plain scalars. + if flow_indicators: + allow_flow_plain = False + + # Block indicators are forbidden for block plain scalars. + if block_indicators: + allow_block_plain = False + + return ScalarAnalysis(scalar=scalar, + empty=False, multiline=line_breaks, + allow_flow_plain=allow_flow_plain, + allow_block_plain=allow_block_plain, + allow_single_quoted=allow_single_quoted, + allow_double_quoted=allow_double_quoted, + allow_block=allow_block) + + # Writers. + + def flush_stream(self): + if hasattr(self.stream, 'flush'): + self.stream.flush() + + def write_stream_start(self): + # Write BOM if needed. + if self.encoding and self.encoding.startswith('utf-16'): + self.stream.write('\uFEFF'.encode(self.encoding)) + + def write_stream_end(self): + self.flush_stream() + + def write_indicator(self, indicator, need_whitespace, + whitespace=False, indention=False): + if self.whitespace or not need_whitespace: + data = indicator + else: + data = ' '+indicator + self.whitespace = whitespace + self.indention = self.indention and indention + self.column += len(data) + self.open_ended = False + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + + def write_indent(self): + indent = self.indent or 0 + if not self.indention or self.column > indent \ + or (self.column == indent and not self.whitespace): + self.write_line_break() + if self.column < indent: + self.whitespace = True + data = ' '*(indent-self.column) + self.column = indent + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + + def write_line_break(self, data=None): + if data is None: + data = self.best_line_break + self.whitespace = True + self.indention = True + self.line += 1 + self.column = 0 + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + + def write_version_directive(self, version_text): + data = '%%YAML %s' % version_text + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + self.write_line_break() + + def write_tag_directive(self, handle_text, prefix_text): + data = '%%TAG %s %s' % (handle_text, prefix_text) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + self.write_line_break() + + # Scalar streams. + + def write_single_quoted(self, text, split=True): + self.write_indicator('\'', True) + spaces = False + breaks = False + start = end = 0 + while end <= len(text): + ch = None + if end < len(text): + ch = text[end] + if spaces: + if ch is None or ch != ' ': + if start+1 == end and self.column > self.best_width and split \ + and start != 0 and end != len(text): + self.write_indent() + else: + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + elif breaks: + if ch is None or ch not in '\n\x85\u2028\u2029': + if text[start] == '\n': + self.write_line_break() + for br in text[start:end]: + if br == '\n': + self.write_line_break() + else: + self.write_line_break(br) + self.write_indent() + start = end + else: + if ch is None or ch in ' \n\x85\u2028\u2029' or ch == '\'': + if start < end: + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + if ch == '\'': + data = '\'\'' + self.column += 2 + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + 1 + if ch is not None: + spaces = (ch == ' ') + breaks = (ch in '\n\x85\u2028\u2029') + end += 1 + self.write_indicator('\'', False) + + ESCAPE_REPLACEMENTS = { + '\0': '0', + '\x07': 'a', + '\x08': 'b', + '\x09': 't', + '\x0A': 'n', + '\x0B': 'v', + '\x0C': 'f', + '\x0D': 'r', + '\x1B': 'e', + '\"': '\"', + '\\': '\\', + '\x85': 'N', + '\xA0': '_', + '\u2028': 'L', + '\u2029': 'P', + } + + def write_double_quoted(self, text, split=True): + self.write_indicator('"', True) + start = end = 0 + while end <= len(text): + ch = None + if end < len(text): + ch = text[end] + if ch is None or ch in '"\\\x85\u2028\u2029\uFEFF' \ + or not ('\x20' <= ch <= '\x7E' + or (self.allow_unicode + and ('\xA0' <= ch <= '\uD7FF' + or '\uE000' <= ch <= '\uFFFD'))): + if start < end: + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + if ch is not None: + if ch in self.ESCAPE_REPLACEMENTS: + data = '\\'+self.ESCAPE_REPLACEMENTS[ch] + elif ch <= '\xFF': + data = '\\x%02X' % ord(ch) + elif ch <= '\uFFFF': + data = '\\u%04X' % ord(ch) + else: + data = '\\U%08X' % ord(ch) + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end+1 + if 0 < end < len(text)-1 and (ch == ' ' or start >= end) \ + and self.column+(end-start) > self.best_width and split: + data = text[start:end]+'\\' + if start < end: + start = end + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + self.write_indent() + self.whitespace = False + self.indention = False + if text[start] == ' ': + data = '\\' + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + end += 1 + self.write_indicator('"', False) + + def determine_block_hints(self, text): + hints = '' + if text: + if text[0] in ' \n\x85\u2028\u2029': + hints += str(self.best_indent) + if text[-1] not in '\n\x85\u2028\u2029': + hints += '-' + elif len(text) == 1 or text[-2] in '\n\x85\u2028\u2029': + hints += '+' + return hints + + def write_folded(self, text): + hints = self.determine_block_hints(text) + self.write_indicator('>'+hints, True) + if hints[-1:] == '+': + self.open_ended = True + self.write_line_break() + leading_space = True + spaces = False + breaks = True + start = end = 0 + while end <= len(text): + ch = None + if end < len(text): + ch = text[end] + if breaks: + if ch is None or ch not in '\n\x85\u2028\u2029': + if not leading_space and ch is not None and ch != ' ' \ + and text[start] == '\n': + self.write_line_break() + leading_space = (ch == ' ') + for br in text[start:end]: + if br == '\n': + self.write_line_break() + else: + self.write_line_break(br) + if ch is not None: + self.write_indent() + start = end + elif spaces: + if ch != ' ': + if start+1 == end and self.column > self.best_width: + self.write_indent() + else: + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + else: + if ch is None or ch in ' \n\x85\u2028\u2029': + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + if ch is None: + self.write_line_break() + start = end + if ch is not None: + breaks = (ch in '\n\x85\u2028\u2029') + spaces = (ch == ' ') + end += 1 + + def write_literal(self, text): + hints = self.determine_block_hints(text) + self.write_indicator('|'+hints, True) + if hints[-1:] == '+': + self.open_ended = True + self.write_line_break() + breaks = True + start = end = 0 + while end <= len(text): + ch = None + if end < len(text): + ch = text[end] + if breaks: + if ch is None or ch not in '\n\x85\u2028\u2029': + for br in text[start:end]: + if br == '\n': + self.write_line_break() + else: + self.write_line_break(br) + if ch is not None: + self.write_indent() + start = end + else: + if ch is None or ch in '\n\x85\u2028\u2029': + data = text[start:end] + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + if ch is None: + self.write_line_break() + start = end + if ch is not None: + breaks = (ch in '\n\x85\u2028\u2029') + end += 1 + + def write_plain(self, text, split=True): + if self.root_context: + self.open_ended = True + if not text: + return + if not self.whitespace: + data = ' ' + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + self.whitespace = False + self.indention = False + spaces = False + breaks = False + start = end = 0 + while end <= len(text): + ch = None + if end < len(text): + ch = text[end] + if spaces: + if ch != ' ': + if start+1 == end and self.column > self.best_width and split: + self.write_indent() + self.whitespace = False + self.indention = False + else: + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + elif breaks: + if ch not in '\n\x85\u2028\u2029': + if text[start] == '\n': + self.write_line_break() + for br in text[start:end]: + if br == '\n': + self.write_line_break() + else: + self.write_line_break(br) + self.write_indent() + self.whitespace = False + self.indention = False + start = end + else: + if ch is None or ch in ' \n\x85\u2028\u2029': + data = text[start:end] + self.column += len(data) + if self.encoding: + data = data.encode(self.encoding) + self.stream.write(data) + start = end + if ch is not None: + spaces = (ch == ' ') + breaks = (ch in '\n\x85\u2028\u2029') + end += 1 + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py3/error.py b/RenderPipelineFile/rplibs/yaml/yaml_py3/error.py index b796b4dc..fc8a5fa8 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py3/error.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py3/error.py @@ -1,75 +1,75 @@ - -__all__ = ['Mark', 'YAMLError', 'MarkedYAMLError'] - -class Mark: - - def __init__(self, name, index, line, column, buffer, pointer): - self.name = name - self.index = index - self.line = line - self.column = column - self.buffer = buffer - self.pointer = pointer - - def get_snippet(self, indent=4, max_length=75): - if self.buffer is None: - return None - head = '' - start = self.pointer - while start > 0 and self.buffer[start-1] not in '\0\r\n\x85\u2028\u2029': - start -= 1 - if self.pointer-start > max_length/2-1: - head = ' ... ' - start += 5 - break - tail = '' - end = self.pointer - while end < len(self.buffer) and self.buffer[end] not in '\0\r\n\x85\u2028\u2029': - end += 1 - if end-self.pointer > max_length/2-1: - tail = ' ... ' - end -= 5 - break - snippet = self.buffer[start:end] - return ' '*indent + head + snippet + tail + '\n' \ - + ' '*(indent+self.pointer-start+len(head)) + '^' - - def __str__(self): - snippet = self.get_snippet() - where = " in \"%s\", line %d, column %d" \ - % (self.name, self.line+1, self.column+1) - if snippet is not None: - where += ":\n"+snippet - return where - -class YAMLError(Exception): - pass - -class MarkedYAMLError(YAMLError): - - def __init__(self, context=None, context_mark=None, - problem=None, problem_mark=None, note=None): - self.context = context - self.context_mark = context_mark - self.problem = problem - self.problem_mark = problem_mark - self.note = note - - def __str__(self): - lines = [] - if self.context is not None: - lines.append(self.context) - if self.context_mark is not None \ - and (self.problem is None or self.problem_mark is None - or self.context_mark.name != self.problem_mark.name - or self.context_mark.line != self.problem_mark.line - or self.context_mark.column != self.problem_mark.column): - lines.append(str(self.context_mark)) - if self.problem is not None: - lines.append(self.problem) - if self.problem_mark is not None: - lines.append(str(self.problem_mark)) - if self.note is not None: - lines.append(self.note) - return '\n'.join(lines) - + +__all__ = ['Mark', 'YAMLError', 'MarkedYAMLError'] + +class Mark: + + def __init__(self, name, index, line, column, buffer, pointer): + self.name = name + self.index = index + self.line = line + self.column = column + self.buffer = buffer + self.pointer = pointer + + def get_snippet(self, indent=4, max_length=75): + if self.buffer is None: + return None + head = '' + start = self.pointer + while start > 0 and self.buffer[start-1] not in '\0\r\n\x85\u2028\u2029': + start -= 1 + if self.pointer-start > max_length/2-1: + head = ' ... ' + start += 5 + break + tail = '' + end = self.pointer + while end < len(self.buffer) and self.buffer[end] not in '\0\r\n\x85\u2028\u2029': + end += 1 + if end-self.pointer > max_length/2-1: + tail = ' ... ' + end -= 5 + break + snippet = self.buffer[start:end] + return ' '*indent + head + snippet + tail + '\n' \ + + ' '*(indent+self.pointer-start+len(head)) + '^' + + def __str__(self): + snippet = self.get_snippet() + where = " in \"%s\", line %d, column %d" \ + % (self.name, self.line+1, self.column+1) + if snippet is not None: + where += ":\n"+snippet + return where + +class YAMLError(Exception): + pass + +class MarkedYAMLError(YAMLError): + + def __init__(self, context=None, context_mark=None, + problem=None, problem_mark=None, note=None): + self.context = context + self.context_mark = context_mark + self.problem = problem + self.problem_mark = problem_mark + self.note = note + + def __str__(self): + lines = [] + if self.context is not None: + lines.append(self.context) + if self.context_mark is not None \ + and (self.problem is None or self.problem_mark is None + or self.context_mark.name != self.problem_mark.name + or self.context_mark.line != self.problem_mark.line + or self.context_mark.column != self.problem_mark.column): + lines.append(str(self.context_mark)) + if self.problem is not None: + lines.append(self.problem) + if self.problem_mark is not None: + lines.append(str(self.problem_mark)) + if self.note is not None: + lines.append(self.note) + return '\n'.join(lines) + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py3/events.py b/RenderPipelineFile/rplibs/yaml/yaml_py3/events.py index f79ad389..7ffac1e6 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py3/events.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py3/events.py @@ -1,86 +1,86 @@ - -# Abstract classes. - -class Event(object): - def __init__(self, start_mark=None, end_mark=None): - self.start_mark = start_mark - self.end_mark = end_mark - def __repr__(self): - attributes = [key for key in ['anchor', 'tag', 'implicit', 'value'] - if hasattr(self, key)] - arguments = ', '.join(['%s=%r' % (key, getattr(self, key)) - for key in attributes]) - return '%s(%s)' % (self.__class__.__name__, arguments) - -class NodeEvent(Event): - def __init__(self, anchor, start_mark=None, end_mark=None): - self.anchor = anchor - self.start_mark = start_mark - self.end_mark = end_mark - -class CollectionStartEvent(NodeEvent): - def __init__(self, anchor, tag, implicit, start_mark=None, end_mark=None, - flow_style=None): - self.anchor = anchor - self.tag = tag - self.implicit = implicit - self.start_mark = start_mark - self.end_mark = end_mark - self.flow_style = flow_style - -class CollectionEndEvent(Event): - pass - -# Implementations. - -class StreamStartEvent(Event): - def __init__(self, start_mark=None, end_mark=None, encoding=None): - self.start_mark = start_mark - self.end_mark = end_mark - self.encoding = encoding - -class StreamEndEvent(Event): - pass - -class DocumentStartEvent(Event): - def __init__(self, start_mark=None, end_mark=None, - explicit=None, version=None, tags=None): - self.start_mark = start_mark - self.end_mark = end_mark - self.explicit = explicit - self.version = version - self.tags = tags - -class DocumentEndEvent(Event): - def __init__(self, start_mark=None, end_mark=None, - explicit=None): - self.start_mark = start_mark - self.end_mark = end_mark - self.explicit = explicit - -class AliasEvent(NodeEvent): - pass - -class ScalarEvent(NodeEvent): - def __init__(self, anchor, tag, implicit, value, - start_mark=None, end_mark=None, style=None): - self.anchor = anchor - self.tag = tag - self.implicit = implicit - self.value = value - self.start_mark = start_mark - self.end_mark = end_mark - self.style = style - -class SequenceStartEvent(CollectionStartEvent): - pass - -class SequenceEndEvent(CollectionEndEvent): - pass - -class MappingStartEvent(CollectionStartEvent): - pass - -class MappingEndEvent(CollectionEndEvent): - pass - + +# Abstract classes. + +class Event(object): + def __init__(self, start_mark=None, end_mark=None): + self.start_mark = start_mark + self.end_mark = end_mark + def __repr__(self): + attributes = [key for key in ['anchor', 'tag', 'implicit', 'value'] + if hasattr(self, key)] + arguments = ', '.join(['%s=%r' % (key, getattr(self, key)) + for key in attributes]) + return '%s(%s)' % (self.__class__.__name__, arguments) + +class NodeEvent(Event): + def __init__(self, anchor, start_mark=None, end_mark=None): + self.anchor = anchor + self.start_mark = start_mark + self.end_mark = end_mark + +class CollectionStartEvent(NodeEvent): + def __init__(self, anchor, tag, implicit, start_mark=None, end_mark=None, + flow_style=None): + self.anchor = anchor + self.tag = tag + self.implicit = implicit + self.start_mark = start_mark + self.end_mark = end_mark + self.flow_style = flow_style + +class CollectionEndEvent(Event): + pass + +# Implementations. + +class StreamStartEvent(Event): + def __init__(self, start_mark=None, end_mark=None, encoding=None): + self.start_mark = start_mark + self.end_mark = end_mark + self.encoding = encoding + +class StreamEndEvent(Event): + pass + +class DocumentStartEvent(Event): + def __init__(self, start_mark=None, end_mark=None, + explicit=None, version=None, tags=None): + self.start_mark = start_mark + self.end_mark = end_mark + self.explicit = explicit + self.version = version + self.tags = tags + +class DocumentEndEvent(Event): + def __init__(self, start_mark=None, end_mark=None, + explicit=None): + self.start_mark = start_mark + self.end_mark = end_mark + self.explicit = explicit + +class AliasEvent(NodeEvent): + pass + +class ScalarEvent(NodeEvent): + def __init__(self, anchor, tag, implicit, value, + start_mark=None, end_mark=None, style=None): + self.anchor = anchor + self.tag = tag + self.implicit = implicit + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + self.style = style + +class SequenceStartEvent(CollectionStartEvent): + pass + +class SequenceEndEvent(CollectionEndEvent): + pass + +class MappingStartEvent(CollectionStartEvent): + pass + +class MappingEndEvent(CollectionEndEvent): + pass + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py3/loader.py b/RenderPipelineFile/rplibs/yaml/yaml_py3/loader.py index 08c8f01b..db3b5fff 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py3/loader.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py3/loader.py @@ -1,40 +1,40 @@ - -__all__ = ['BaseLoader', 'SafeLoader', 'Loader'] - -from .reader import * -from .scanner import * -from .parser import * -from .composer import * -from .constructor import * -from .resolver import * - -class BaseLoader(Reader, Scanner, Parser, Composer, BaseConstructor, BaseResolver): - - def __init__(self, stream): - Reader.__init__(self, stream) - Scanner.__init__(self) - Parser.__init__(self) - Composer.__init__(self) - BaseConstructor.__init__(self) - BaseResolver.__init__(self) - -class SafeLoader(Reader, Scanner, Parser, Composer, SafeConstructor, Resolver): - - def __init__(self, stream): - Reader.__init__(self, stream) - Scanner.__init__(self) - Parser.__init__(self) - Composer.__init__(self) - SafeConstructor.__init__(self) - Resolver.__init__(self) - -class Loader(Reader, Scanner, Parser, Composer, Constructor, Resolver): - - def __init__(self, stream): - Reader.__init__(self, stream) - Scanner.__init__(self) - Parser.__init__(self) - Composer.__init__(self) - Constructor.__init__(self) - Resolver.__init__(self) - + +__all__ = ['BaseLoader', 'SafeLoader', 'Loader'] + +from .reader import * +from .scanner import * +from .parser import * +from .composer import * +from .constructor import * +from .resolver import * + +class BaseLoader(Reader, Scanner, Parser, Composer, BaseConstructor, BaseResolver): + + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + BaseConstructor.__init__(self) + BaseResolver.__init__(self) + +class SafeLoader(Reader, Scanner, Parser, Composer, SafeConstructor, Resolver): + + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + SafeConstructor.__init__(self) + Resolver.__init__(self) + +class Loader(Reader, Scanner, Parser, Composer, Constructor, Resolver): + + def __init__(self, stream): + Reader.__init__(self, stream) + Scanner.__init__(self) + Parser.__init__(self) + Composer.__init__(self) + Constructor.__init__(self) + Resolver.__init__(self) + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py3/nodes.py b/RenderPipelineFile/rplibs/yaml/yaml_py3/nodes.py index c4f070c4..d4a3b0a0 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py3/nodes.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py3/nodes.py @@ -1,49 +1,49 @@ - -class Node(object): - def __init__(self, tag, value, start_mark, end_mark): - self.tag = tag - self.value = value - self.start_mark = start_mark - self.end_mark = end_mark - def __repr__(self): - value = self.value - #if isinstance(value, list): - # if len(value) == 0: - # value = '' - # elif len(value) == 1: - # value = '<1 item>' - # else: - # value = '<%d items>' % len(value) - #else: - # if len(value) > 75: - # value = repr(value[:70]+u' ... ') - # else: - # value = repr(value) - value = repr(value) - return '%s(tag=%r, value=%s)' % (self.__class__.__name__, self.tag, value) - -class ScalarNode(Node): - id = 'scalar' - def __init__(self, tag, value, - start_mark=None, end_mark=None, style=None): - self.tag = tag - self.value = value - self.start_mark = start_mark - self.end_mark = end_mark - self.style = style - -class CollectionNode(Node): - def __init__(self, tag, value, - start_mark=None, end_mark=None, flow_style=None): - self.tag = tag - self.value = value - self.start_mark = start_mark - self.end_mark = end_mark - self.flow_style = flow_style - -class SequenceNode(CollectionNode): - id = 'sequence' - -class MappingNode(CollectionNode): - id = 'mapping' - + +class Node(object): + def __init__(self, tag, value, start_mark, end_mark): + self.tag = tag + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + def __repr__(self): + value = self.value + #if isinstance(value, list): + # if len(value) == 0: + # value = '' + # elif len(value) == 1: + # value = '<1 item>' + # else: + # value = '<%d items>' % len(value) + #else: + # if len(value) > 75: + # value = repr(value[:70]+u' ... ') + # else: + # value = repr(value) + value = repr(value) + return '%s(tag=%r, value=%s)' % (self.__class__.__name__, self.tag, value) + +class ScalarNode(Node): + id = 'scalar' + def __init__(self, tag, value, + start_mark=None, end_mark=None, style=None): + self.tag = tag + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + self.style = style + +class CollectionNode(Node): + def __init__(self, tag, value, + start_mark=None, end_mark=None, flow_style=None): + self.tag = tag + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + self.flow_style = flow_style + +class SequenceNode(CollectionNode): + id = 'sequence' + +class MappingNode(CollectionNode): + id = 'mapping' + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py3/parser.py b/RenderPipelineFile/rplibs/yaml/yaml_py3/parser.py index 13a5995d..7f32b110 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py3/parser.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py3/parser.py @@ -1,589 +1,589 @@ - -# The following YAML grammar is LL(1) and is parsed by a recursive descent -# parser. -# -# stream ::= STREAM-START implicit_document? explicit_document* STREAM-END -# implicit_document ::= block_node DOCUMENT-END* -# explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* -# block_node_or_indentless_sequence ::= -# ALIAS -# | properties (block_content | indentless_block_sequence)? -# | block_content -# | indentless_block_sequence -# block_node ::= ALIAS -# | properties block_content? -# | block_content -# flow_node ::= ALIAS -# | properties flow_content? -# | flow_content -# properties ::= TAG ANCHOR? | ANCHOR TAG? -# block_content ::= block_collection | flow_collection | SCALAR -# flow_content ::= flow_collection | SCALAR -# block_collection ::= block_sequence | block_mapping -# flow_collection ::= flow_sequence | flow_mapping -# block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END -# indentless_sequence ::= (BLOCK-ENTRY block_node?)+ -# block_mapping ::= BLOCK-MAPPING_START -# ((KEY block_node_or_indentless_sequence?)? -# (VALUE block_node_or_indentless_sequence?)?)* -# BLOCK-END -# flow_sequence ::= FLOW-SEQUENCE-START -# (flow_sequence_entry FLOW-ENTRY)* -# flow_sequence_entry? -# FLOW-SEQUENCE-END -# flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? -# flow_mapping ::= FLOW-MAPPING-START -# (flow_mapping_entry FLOW-ENTRY)* -# flow_mapping_entry? -# FLOW-MAPPING-END -# flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? -# -# FIRST sets: -# -# stream: { STREAM-START } -# explicit_document: { DIRECTIVE DOCUMENT-START } -# implicit_document: FIRST(block_node) -# block_node: { ALIAS TAG ANCHOR SCALAR BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START } -# flow_node: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START } -# block_content: { BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START SCALAR } -# flow_content: { FLOW-SEQUENCE-START FLOW-MAPPING-START SCALAR } -# block_collection: { BLOCK-SEQUENCE-START BLOCK-MAPPING-START } -# flow_collection: { FLOW-SEQUENCE-START FLOW-MAPPING-START } -# block_sequence: { BLOCK-SEQUENCE-START } -# block_mapping: { BLOCK-MAPPING-START } -# block_node_or_indentless_sequence: { ALIAS ANCHOR TAG SCALAR BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START BLOCK-ENTRY } -# indentless_sequence: { ENTRY } -# flow_collection: { FLOW-SEQUENCE-START FLOW-MAPPING-START } -# flow_sequence: { FLOW-SEQUENCE-START } -# flow_mapping: { FLOW-MAPPING-START } -# flow_sequence_entry: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START KEY } -# flow_mapping_entry: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START KEY } - -__all__ = ['Parser', 'ParserError'] - -from .error import MarkedYAMLError -from .tokens import * -from .events import * -from .scanner import * - -class ParserError(MarkedYAMLError): - pass - -class Parser: - # Since writing a recursive-descendant parser is a straightforward task, we - # do not give many comments here. - - DEFAULT_TAGS = { - '!': '!', - '!!': 'tag:yaml.org,2002:', - } - - def __init__(self): - self.current_event = None - self.yaml_version = None - self.tag_handles = {} - self.states = [] - self.marks = [] - self.state = self.parse_stream_start - - def dispose(self): - # Reset the state attributes (to clear self-references) - self.states = [] - self.state = None - - def check_event(self, *choices): - # Check the type of the next event. - if self.current_event is None: - if self.state: - self.current_event = self.state() - if self.current_event is not None: - if not choices: - return True - for choice in choices: - if isinstance(self.current_event, choice): - return True - return False - - def peek_event(self): - # Get the next event. - if self.current_event is None: - if self.state: - self.current_event = self.state() - return self.current_event - - def get_event(self): - # Get the next event and proceed further. - if self.current_event is None: - if self.state: - self.current_event = self.state() - value = self.current_event - self.current_event = None - return value - - # stream ::= STREAM-START implicit_document? explicit_document* STREAM-END - # implicit_document ::= block_node DOCUMENT-END* - # explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* - - def parse_stream_start(self): - - # Parse the stream start. - token = self.get_token() - event = StreamStartEvent(token.start_mark, token.end_mark, - encoding=token.encoding) - - # Prepare the next state. - self.state = self.parse_implicit_document_start - - return event - - def parse_implicit_document_start(self): - - # Parse an implicit document. - if not self.check_token(DirectiveToken, DocumentStartToken, - StreamEndToken): - self.tag_handles = self.DEFAULT_TAGS - token = self.peek_token() - start_mark = end_mark = token.start_mark - event = DocumentStartEvent(start_mark, end_mark, - explicit=False) - - # Prepare the next state. - self.states.append(self.parse_document_end) - self.state = self.parse_block_node - - return event - - else: - return self.parse_document_start() - - def parse_document_start(self): - - # Parse any extra document end indicators. - while self.check_token(DocumentEndToken): - self.get_token() - - # Parse an explicit document. - if not self.check_token(StreamEndToken): - token = self.peek_token() - start_mark = token.start_mark - version, tags = self.process_directives() - if not self.check_token(DocumentStartToken): - raise ParserError(None, None, - "expected '', but found %r" - % self.peek_token().id, - self.peek_token().start_mark) - token = self.get_token() - end_mark = token.end_mark - event = DocumentStartEvent(start_mark, end_mark, - explicit=True, version=version, tags=tags) - self.states.append(self.parse_document_end) - self.state = self.parse_document_content - else: - # Parse the end of the stream. - token = self.get_token() - event = StreamEndEvent(token.start_mark, token.end_mark) - assert not self.states - assert not self.marks - self.state = None - return event - - def parse_document_end(self): - - # Parse the document end. - token = self.peek_token() - start_mark = end_mark = token.start_mark - explicit = False - if self.check_token(DocumentEndToken): - token = self.get_token() - end_mark = token.end_mark - explicit = True - event = DocumentEndEvent(start_mark, end_mark, - explicit=explicit) - - # Prepare the next state. - self.state = self.parse_document_start - - return event - - def parse_document_content(self): - if self.check_token(DirectiveToken, - DocumentStartToken, DocumentEndToken, StreamEndToken): - event = self.process_empty_scalar(self.peek_token().start_mark) - self.state = self.states.pop() - return event - else: - return self.parse_block_node() - - def process_directives(self): - self.yaml_version = None - self.tag_handles = {} - while self.check_token(DirectiveToken): - token = self.get_token() - if token.name == 'YAML': - if self.yaml_version is not None: - raise ParserError(None, None, - "found duplicate YAML directive", token.start_mark) - major, minor = token.value - if major != 1: - raise ParserError(None, None, - "found incompatible YAML document (version 1.* is required)", - token.start_mark) - self.yaml_version = token.value - elif token.name == 'TAG': - handle, prefix = token.value - if handle in self.tag_handles: - raise ParserError(None, None, - "duplicate tag handle %r" % handle, - token.start_mark) - self.tag_handles[handle] = prefix - if self.tag_handles: - value = self.yaml_version, self.tag_handles.copy() - else: - value = self.yaml_version, None - for key in self.DEFAULT_TAGS: - if key not in self.tag_handles: - self.tag_handles[key] = self.DEFAULT_TAGS[key] - return value - - # block_node_or_indentless_sequence ::= ALIAS - # | properties (block_content | indentless_block_sequence)? - # | block_content - # | indentless_block_sequence - # block_node ::= ALIAS - # | properties block_content? - # | block_content - # flow_node ::= ALIAS - # | properties flow_content? - # | flow_content - # properties ::= TAG ANCHOR? | ANCHOR TAG? - # block_content ::= block_collection | flow_collection | SCALAR - # flow_content ::= flow_collection | SCALAR - # block_collection ::= block_sequence | block_mapping - # flow_collection ::= flow_sequence | flow_mapping - - def parse_block_node(self): - return self.parse_node(block=True) - - def parse_flow_node(self): - return self.parse_node() - - def parse_block_node_or_indentless_sequence(self): - return self.parse_node(block=True, indentless_sequence=True) - - def parse_node(self, block=False, indentless_sequence=False): - if self.check_token(AliasToken): - token = self.get_token() - event = AliasEvent(token.value, token.start_mark, token.end_mark) - self.state = self.states.pop() - else: - anchor = None - tag = None - start_mark = end_mark = tag_mark = None - if self.check_token(AnchorToken): - token = self.get_token() - start_mark = token.start_mark - end_mark = token.end_mark - anchor = token.value - if self.check_token(TagToken): - token = self.get_token() - tag_mark = token.start_mark - end_mark = token.end_mark - tag = token.value - elif self.check_token(TagToken): - token = self.get_token() - start_mark = tag_mark = token.start_mark - end_mark = token.end_mark - tag = token.value - if self.check_token(AnchorToken): - token = self.get_token() - end_mark = token.end_mark - anchor = token.value - if tag is not None: - handle, suffix = tag - if handle is not None: - if handle not in self.tag_handles: - raise ParserError("while parsing a node", start_mark, - "found undefined tag handle %r" % handle, - tag_mark) - tag = self.tag_handles[handle]+suffix - else: - tag = suffix - #if tag == '!': - # raise ParserError("while parsing a node", start_mark, - # "found non-specific tag '!'", tag_mark, - # "Please check 'http://pyyaml.org/wiki/YAMLNonSpecificTag' and share your opinion.") - if start_mark is None: - start_mark = end_mark = self.peek_token().start_mark - event = None - implicit = (tag is None or tag == '!') - if indentless_sequence and self.check_token(BlockEntryToken): - end_mark = self.peek_token().end_mark - event = SequenceStartEvent(anchor, tag, implicit, - start_mark, end_mark) - self.state = self.parse_indentless_sequence_entry - else: - if self.check_token(ScalarToken): - token = self.get_token() - end_mark = token.end_mark - if (token.plain and tag is None) or tag == '!': - implicit = (True, False) - elif tag is None: - implicit = (False, True) - else: - implicit = (False, False) - event = ScalarEvent(anchor, tag, implicit, token.value, - start_mark, end_mark, style=token.style) - self.state = self.states.pop() - elif self.check_token(FlowSequenceStartToken): - end_mark = self.peek_token().end_mark - event = SequenceStartEvent(anchor, tag, implicit, - start_mark, end_mark, flow_style=True) - self.state = self.parse_flow_sequence_first_entry - elif self.check_token(FlowMappingStartToken): - end_mark = self.peek_token().end_mark - event = MappingStartEvent(anchor, tag, implicit, - start_mark, end_mark, flow_style=True) - self.state = self.parse_flow_mapping_first_key - elif block and self.check_token(BlockSequenceStartToken): - end_mark = self.peek_token().start_mark - event = SequenceStartEvent(anchor, tag, implicit, - start_mark, end_mark, flow_style=False) - self.state = self.parse_block_sequence_first_entry - elif block and self.check_token(BlockMappingStartToken): - end_mark = self.peek_token().start_mark - event = MappingStartEvent(anchor, tag, implicit, - start_mark, end_mark, flow_style=False) - self.state = self.parse_block_mapping_first_key - elif anchor is not None or tag is not None: - # Empty scalars are allowed even if a tag or an anchor is - # specified. - event = ScalarEvent(anchor, tag, (implicit, False), '', - start_mark, end_mark) - self.state = self.states.pop() - else: - if block: - node = 'block' - else: - node = 'flow' - token = self.peek_token() - raise ParserError("while parsing a %s node" % node, start_mark, - "expected the node content, but found %r" % token.id, - token.start_mark) - return event - - # block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END - - def parse_block_sequence_first_entry(self): - token = self.get_token() - self.marks.append(token.start_mark) - return self.parse_block_sequence_entry() - - def parse_block_sequence_entry(self): - if self.check_token(BlockEntryToken): - token = self.get_token() - if not self.check_token(BlockEntryToken, BlockEndToken): - self.states.append(self.parse_block_sequence_entry) - return self.parse_block_node() - else: - self.state = self.parse_block_sequence_entry - return self.process_empty_scalar(token.end_mark) - if not self.check_token(BlockEndToken): - token = self.peek_token() - raise ParserError("while parsing a block collection", self.marks[-1], - "expected , but found %r" % token.id, token.start_mark) - token = self.get_token() - event = SequenceEndEvent(token.start_mark, token.end_mark) - self.state = self.states.pop() - self.marks.pop() - return event - - # indentless_sequence ::= (BLOCK-ENTRY block_node?)+ - - def parse_indentless_sequence_entry(self): - if self.check_token(BlockEntryToken): - token = self.get_token() - if not self.check_token(BlockEntryToken, - KeyToken, ValueToken, BlockEndToken): - self.states.append(self.parse_indentless_sequence_entry) - return self.parse_block_node() - else: - self.state = self.parse_indentless_sequence_entry - return self.process_empty_scalar(token.end_mark) - token = self.peek_token() - event = SequenceEndEvent(token.start_mark, token.start_mark) - self.state = self.states.pop() - return event - - # block_mapping ::= BLOCK-MAPPING_START - # ((KEY block_node_or_indentless_sequence?)? - # (VALUE block_node_or_indentless_sequence?)?)* - # BLOCK-END - - def parse_block_mapping_first_key(self): - token = self.get_token() - self.marks.append(token.start_mark) - return self.parse_block_mapping_key() - - def parse_block_mapping_key(self): - if self.check_token(KeyToken): - token = self.get_token() - if not self.check_token(KeyToken, ValueToken, BlockEndToken): - self.states.append(self.parse_block_mapping_value) - return self.parse_block_node_or_indentless_sequence() - else: - self.state = self.parse_block_mapping_value - return self.process_empty_scalar(token.end_mark) - if not self.check_token(BlockEndToken): - token = self.peek_token() - raise ParserError("while parsing a block mapping", self.marks[-1], - "expected , but found %r" % token.id, token.start_mark) - token = self.get_token() - event = MappingEndEvent(token.start_mark, token.end_mark) - self.state = self.states.pop() - self.marks.pop() - return event - - def parse_block_mapping_value(self): - if self.check_token(ValueToken): - token = self.get_token() - if not self.check_token(KeyToken, ValueToken, BlockEndToken): - self.states.append(self.parse_block_mapping_key) - return self.parse_block_node_or_indentless_sequence() - else: - self.state = self.parse_block_mapping_key - return self.process_empty_scalar(token.end_mark) - else: - self.state = self.parse_block_mapping_key - token = self.peek_token() - return self.process_empty_scalar(token.start_mark) - - # flow_sequence ::= FLOW-SEQUENCE-START - # (flow_sequence_entry FLOW-ENTRY)* - # flow_sequence_entry? - # FLOW-SEQUENCE-END - # flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? - # - # Note that while production rules for both flow_sequence_entry and - # flow_mapping_entry are equal, their interpretations are different. - # For `flow_sequence_entry`, the part `KEY flow_node? (VALUE flow_node?)?` - # generate an inline mapping (set syntax). - - def parse_flow_sequence_first_entry(self): - token = self.get_token() - self.marks.append(token.start_mark) - return self.parse_flow_sequence_entry(first=True) - - def parse_flow_sequence_entry(self, first=False): - if not self.check_token(FlowSequenceEndToken): - if not first: - if self.check_token(FlowEntryToken): - self.get_token() - else: - token = self.peek_token() - raise ParserError("while parsing a flow sequence", self.marks[-1], - "expected ',' or ']', but got %r" % token.id, token.start_mark) - - if self.check_token(KeyToken): - token = self.peek_token() - event = MappingStartEvent(None, None, True, - token.start_mark, token.end_mark, - flow_style=True) - self.state = self.parse_flow_sequence_entry_mapping_key - return event - elif not self.check_token(FlowSequenceEndToken): - self.states.append(self.parse_flow_sequence_entry) - return self.parse_flow_node() - token = self.get_token() - event = SequenceEndEvent(token.start_mark, token.end_mark) - self.state = self.states.pop() - self.marks.pop() - return event - - def parse_flow_sequence_entry_mapping_key(self): - token = self.get_token() - if not self.check_token(ValueToken, - FlowEntryToken, FlowSequenceEndToken): - self.states.append(self.parse_flow_sequence_entry_mapping_value) - return self.parse_flow_node() - else: - self.state = self.parse_flow_sequence_entry_mapping_value - return self.process_empty_scalar(token.end_mark) - - def parse_flow_sequence_entry_mapping_value(self): - if self.check_token(ValueToken): - token = self.get_token() - if not self.check_token(FlowEntryToken, FlowSequenceEndToken): - self.states.append(self.parse_flow_sequence_entry_mapping_end) - return self.parse_flow_node() - else: - self.state = self.parse_flow_sequence_entry_mapping_end - return self.process_empty_scalar(token.end_mark) - else: - self.state = self.parse_flow_sequence_entry_mapping_end - token = self.peek_token() - return self.process_empty_scalar(token.start_mark) - - def parse_flow_sequence_entry_mapping_end(self): - self.state = self.parse_flow_sequence_entry - token = self.peek_token() - return MappingEndEvent(token.start_mark, token.start_mark) - - # flow_mapping ::= FLOW-MAPPING-START - # (flow_mapping_entry FLOW-ENTRY)* - # flow_mapping_entry? - # FLOW-MAPPING-END - # flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? - - def parse_flow_mapping_first_key(self): - token = self.get_token() - self.marks.append(token.start_mark) - return self.parse_flow_mapping_key(first=True) - - def parse_flow_mapping_key(self, first=False): - if not self.check_token(FlowMappingEndToken): - if not first: - if self.check_token(FlowEntryToken): - self.get_token() - else: - token = self.peek_token() - raise ParserError("while parsing a flow mapping", self.marks[-1], - "expected ',' or '}', but got %r" % token.id, token.start_mark) - if self.check_token(KeyToken): - token = self.get_token() - if not self.check_token(ValueToken, - FlowEntryToken, FlowMappingEndToken): - self.states.append(self.parse_flow_mapping_value) - return self.parse_flow_node() - else: - self.state = self.parse_flow_mapping_value - return self.process_empty_scalar(token.end_mark) - elif not self.check_token(FlowMappingEndToken): - self.states.append(self.parse_flow_mapping_empty_value) - return self.parse_flow_node() - token = self.get_token() - event = MappingEndEvent(token.start_mark, token.end_mark) - self.state = self.states.pop() - self.marks.pop() - return event - - def parse_flow_mapping_value(self): - if self.check_token(ValueToken): - token = self.get_token() - if not self.check_token(FlowEntryToken, FlowMappingEndToken): - self.states.append(self.parse_flow_mapping_key) - return self.parse_flow_node() - else: - self.state = self.parse_flow_mapping_key - return self.process_empty_scalar(token.end_mark) - else: - self.state = self.parse_flow_mapping_key - token = self.peek_token() - return self.process_empty_scalar(token.start_mark) - - def parse_flow_mapping_empty_value(self): - self.state = self.parse_flow_mapping_key - return self.process_empty_scalar(self.peek_token().start_mark) - - def process_empty_scalar(self, mark): - return ScalarEvent(None, None, (True, False), '', mark, mark) - + +# The following YAML grammar is LL(1) and is parsed by a recursive descent +# parser. +# +# stream ::= STREAM-START implicit_document? explicit_document* STREAM-END +# implicit_document ::= block_node DOCUMENT-END* +# explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* +# block_node_or_indentless_sequence ::= +# ALIAS +# | properties (block_content | indentless_block_sequence)? +# | block_content +# | indentless_block_sequence +# block_node ::= ALIAS +# | properties block_content? +# | block_content +# flow_node ::= ALIAS +# | properties flow_content? +# | flow_content +# properties ::= TAG ANCHOR? | ANCHOR TAG? +# block_content ::= block_collection | flow_collection | SCALAR +# flow_content ::= flow_collection | SCALAR +# block_collection ::= block_sequence | block_mapping +# flow_collection ::= flow_sequence | flow_mapping +# block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END +# indentless_sequence ::= (BLOCK-ENTRY block_node?)+ +# block_mapping ::= BLOCK-MAPPING_START +# ((KEY block_node_or_indentless_sequence?)? +# (VALUE block_node_or_indentless_sequence?)?)* +# BLOCK-END +# flow_sequence ::= FLOW-SEQUENCE-START +# (flow_sequence_entry FLOW-ENTRY)* +# flow_sequence_entry? +# FLOW-SEQUENCE-END +# flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +# flow_mapping ::= FLOW-MAPPING-START +# (flow_mapping_entry FLOW-ENTRY)* +# flow_mapping_entry? +# FLOW-MAPPING-END +# flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? +# +# FIRST sets: +# +# stream: { STREAM-START } +# explicit_document: { DIRECTIVE DOCUMENT-START } +# implicit_document: FIRST(block_node) +# block_node: { ALIAS TAG ANCHOR SCALAR BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START } +# flow_node: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START } +# block_content: { BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START SCALAR } +# flow_content: { FLOW-SEQUENCE-START FLOW-MAPPING-START SCALAR } +# block_collection: { BLOCK-SEQUENCE-START BLOCK-MAPPING-START } +# flow_collection: { FLOW-SEQUENCE-START FLOW-MAPPING-START } +# block_sequence: { BLOCK-SEQUENCE-START } +# block_mapping: { BLOCK-MAPPING-START } +# block_node_or_indentless_sequence: { ALIAS ANCHOR TAG SCALAR BLOCK-SEQUENCE-START BLOCK-MAPPING-START FLOW-SEQUENCE-START FLOW-MAPPING-START BLOCK-ENTRY } +# indentless_sequence: { ENTRY } +# flow_collection: { FLOW-SEQUENCE-START FLOW-MAPPING-START } +# flow_sequence: { FLOW-SEQUENCE-START } +# flow_mapping: { FLOW-MAPPING-START } +# flow_sequence_entry: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START KEY } +# flow_mapping_entry: { ALIAS ANCHOR TAG SCALAR FLOW-SEQUENCE-START FLOW-MAPPING-START KEY } + +__all__ = ['Parser', 'ParserError'] + +from .error import MarkedYAMLError +from .tokens import * +from .events import * +from .scanner import * + +class ParserError(MarkedYAMLError): + pass + +class Parser: + # Since writing a recursive-descendant parser is a straightforward task, we + # do not give many comments here. + + DEFAULT_TAGS = { + '!': '!', + '!!': 'tag:yaml.org,2002:', + } + + def __init__(self): + self.current_event = None + self.yaml_version = None + self.tag_handles = {} + self.states = [] + self.marks = [] + self.state = self.parse_stream_start + + def dispose(self): + # Reset the state attributes (to clear self-references) + self.states = [] + self.state = None + + def check_event(self, *choices): + # Check the type of the next event. + if self.current_event is None: + if self.state: + self.current_event = self.state() + if self.current_event is not None: + if not choices: + return True + for choice in choices: + if isinstance(self.current_event, choice): + return True + return False + + def peek_event(self): + # Get the next event. + if self.current_event is None: + if self.state: + self.current_event = self.state() + return self.current_event + + def get_event(self): + # Get the next event and proceed further. + if self.current_event is None: + if self.state: + self.current_event = self.state() + value = self.current_event + self.current_event = None + return value + + # stream ::= STREAM-START implicit_document? explicit_document* STREAM-END + # implicit_document ::= block_node DOCUMENT-END* + # explicit_document ::= DIRECTIVE* DOCUMENT-START block_node? DOCUMENT-END* + + def parse_stream_start(self): + + # Parse the stream start. + token = self.get_token() + event = StreamStartEvent(token.start_mark, token.end_mark, + encoding=token.encoding) + + # Prepare the next state. + self.state = self.parse_implicit_document_start + + return event + + def parse_implicit_document_start(self): + + # Parse an implicit document. + if not self.check_token(DirectiveToken, DocumentStartToken, + StreamEndToken): + self.tag_handles = self.DEFAULT_TAGS + token = self.peek_token() + start_mark = end_mark = token.start_mark + event = DocumentStartEvent(start_mark, end_mark, + explicit=False) + + # Prepare the next state. + self.states.append(self.parse_document_end) + self.state = self.parse_block_node + + return event + + else: + return self.parse_document_start() + + def parse_document_start(self): + + # Parse any extra document end indicators. + while self.check_token(DocumentEndToken): + self.get_token() + + # Parse an explicit document. + if not self.check_token(StreamEndToken): + token = self.peek_token() + start_mark = token.start_mark + version, tags = self.process_directives() + if not self.check_token(DocumentStartToken): + raise ParserError(None, None, + "expected '', but found %r" + % self.peek_token().id, + self.peek_token().start_mark) + token = self.get_token() + end_mark = token.end_mark + event = DocumentStartEvent(start_mark, end_mark, + explicit=True, version=version, tags=tags) + self.states.append(self.parse_document_end) + self.state = self.parse_document_content + else: + # Parse the end of the stream. + token = self.get_token() + event = StreamEndEvent(token.start_mark, token.end_mark) + assert not self.states + assert not self.marks + self.state = None + return event + + def parse_document_end(self): + + # Parse the document end. + token = self.peek_token() + start_mark = end_mark = token.start_mark + explicit = False + if self.check_token(DocumentEndToken): + token = self.get_token() + end_mark = token.end_mark + explicit = True + event = DocumentEndEvent(start_mark, end_mark, + explicit=explicit) + + # Prepare the next state. + self.state = self.parse_document_start + + return event + + def parse_document_content(self): + if self.check_token(DirectiveToken, + DocumentStartToken, DocumentEndToken, StreamEndToken): + event = self.process_empty_scalar(self.peek_token().start_mark) + self.state = self.states.pop() + return event + else: + return self.parse_block_node() + + def process_directives(self): + self.yaml_version = None + self.tag_handles = {} + while self.check_token(DirectiveToken): + token = self.get_token() + if token.name == 'YAML': + if self.yaml_version is not None: + raise ParserError(None, None, + "found duplicate YAML directive", token.start_mark) + major, minor = token.value + if major != 1: + raise ParserError(None, None, + "found incompatible YAML document (version 1.* is required)", + token.start_mark) + self.yaml_version = token.value + elif token.name == 'TAG': + handle, prefix = token.value + if handle in self.tag_handles: + raise ParserError(None, None, + "duplicate tag handle %r" % handle, + token.start_mark) + self.tag_handles[handle] = prefix + if self.tag_handles: + value = self.yaml_version, self.tag_handles.copy() + else: + value = self.yaml_version, None + for key in self.DEFAULT_TAGS: + if key not in self.tag_handles: + self.tag_handles[key] = self.DEFAULT_TAGS[key] + return value + + # block_node_or_indentless_sequence ::= ALIAS + # | properties (block_content | indentless_block_sequence)? + # | block_content + # | indentless_block_sequence + # block_node ::= ALIAS + # | properties block_content? + # | block_content + # flow_node ::= ALIAS + # | properties flow_content? + # | flow_content + # properties ::= TAG ANCHOR? | ANCHOR TAG? + # block_content ::= block_collection | flow_collection | SCALAR + # flow_content ::= flow_collection | SCALAR + # block_collection ::= block_sequence | block_mapping + # flow_collection ::= flow_sequence | flow_mapping + + def parse_block_node(self): + return self.parse_node(block=True) + + def parse_flow_node(self): + return self.parse_node() + + def parse_block_node_or_indentless_sequence(self): + return self.parse_node(block=True, indentless_sequence=True) + + def parse_node(self, block=False, indentless_sequence=False): + if self.check_token(AliasToken): + token = self.get_token() + event = AliasEvent(token.value, token.start_mark, token.end_mark) + self.state = self.states.pop() + else: + anchor = None + tag = None + start_mark = end_mark = tag_mark = None + if self.check_token(AnchorToken): + token = self.get_token() + start_mark = token.start_mark + end_mark = token.end_mark + anchor = token.value + if self.check_token(TagToken): + token = self.get_token() + tag_mark = token.start_mark + end_mark = token.end_mark + tag = token.value + elif self.check_token(TagToken): + token = self.get_token() + start_mark = tag_mark = token.start_mark + end_mark = token.end_mark + tag = token.value + if self.check_token(AnchorToken): + token = self.get_token() + end_mark = token.end_mark + anchor = token.value + if tag is not None: + handle, suffix = tag + if handle is not None: + if handle not in self.tag_handles: + raise ParserError("while parsing a node", start_mark, + "found undefined tag handle %r" % handle, + tag_mark) + tag = self.tag_handles[handle]+suffix + else: + tag = suffix + #if tag == '!': + # raise ParserError("while parsing a node", start_mark, + # "found non-specific tag '!'", tag_mark, + # "Please check 'http://pyyaml.org/wiki/YAMLNonSpecificTag' and share your opinion.") + if start_mark is None: + start_mark = end_mark = self.peek_token().start_mark + event = None + implicit = (tag is None or tag == '!') + if indentless_sequence and self.check_token(BlockEntryToken): + end_mark = self.peek_token().end_mark + event = SequenceStartEvent(anchor, tag, implicit, + start_mark, end_mark) + self.state = self.parse_indentless_sequence_entry + else: + if self.check_token(ScalarToken): + token = self.get_token() + end_mark = token.end_mark + if (token.plain and tag is None) or tag == '!': + implicit = (True, False) + elif tag is None: + implicit = (False, True) + else: + implicit = (False, False) + event = ScalarEvent(anchor, tag, implicit, token.value, + start_mark, end_mark, style=token.style) + self.state = self.states.pop() + elif self.check_token(FlowSequenceStartToken): + end_mark = self.peek_token().end_mark + event = SequenceStartEvent(anchor, tag, implicit, + start_mark, end_mark, flow_style=True) + self.state = self.parse_flow_sequence_first_entry + elif self.check_token(FlowMappingStartToken): + end_mark = self.peek_token().end_mark + event = MappingStartEvent(anchor, tag, implicit, + start_mark, end_mark, flow_style=True) + self.state = self.parse_flow_mapping_first_key + elif block and self.check_token(BlockSequenceStartToken): + end_mark = self.peek_token().start_mark + event = SequenceStartEvent(anchor, tag, implicit, + start_mark, end_mark, flow_style=False) + self.state = self.parse_block_sequence_first_entry + elif block and self.check_token(BlockMappingStartToken): + end_mark = self.peek_token().start_mark + event = MappingStartEvent(anchor, tag, implicit, + start_mark, end_mark, flow_style=False) + self.state = self.parse_block_mapping_first_key + elif anchor is not None or tag is not None: + # Empty scalars are allowed even if a tag or an anchor is + # specified. + event = ScalarEvent(anchor, tag, (implicit, False), '', + start_mark, end_mark) + self.state = self.states.pop() + else: + if block: + node = 'block' + else: + node = 'flow' + token = self.peek_token() + raise ParserError("while parsing a %s node" % node, start_mark, + "expected the node content, but found %r" % token.id, + token.start_mark) + return event + + # block_sequence ::= BLOCK-SEQUENCE-START (BLOCK-ENTRY block_node?)* BLOCK-END + + def parse_block_sequence_first_entry(self): + token = self.get_token() + self.marks.append(token.start_mark) + return self.parse_block_sequence_entry() + + def parse_block_sequence_entry(self): + if self.check_token(BlockEntryToken): + token = self.get_token() + if not self.check_token(BlockEntryToken, BlockEndToken): + self.states.append(self.parse_block_sequence_entry) + return self.parse_block_node() + else: + self.state = self.parse_block_sequence_entry + return self.process_empty_scalar(token.end_mark) + if not self.check_token(BlockEndToken): + token = self.peek_token() + raise ParserError("while parsing a block collection", self.marks[-1], + "expected , but found %r" % token.id, token.start_mark) + token = self.get_token() + event = SequenceEndEvent(token.start_mark, token.end_mark) + self.state = self.states.pop() + self.marks.pop() + return event + + # indentless_sequence ::= (BLOCK-ENTRY block_node?)+ + + def parse_indentless_sequence_entry(self): + if self.check_token(BlockEntryToken): + token = self.get_token() + if not self.check_token(BlockEntryToken, + KeyToken, ValueToken, BlockEndToken): + self.states.append(self.parse_indentless_sequence_entry) + return self.parse_block_node() + else: + self.state = self.parse_indentless_sequence_entry + return self.process_empty_scalar(token.end_mark) + token = self.peek_token() + event = SequenceEndEvent(token.start_mark, token.start_mark) + self.state = self.states.pop() + return event + + # block_mapping ::= BLOCK-MAPPING_START + # ((KEY block_node_or_indentless_sequence?)? + # (VALUE block_node_or_indentless_sequence?)?)* + # BLOCK-END + + def parse_block_mapping_first_key(self): + token = self.get_token() + self.marks.append(token.start_mark) + return self.parse_block_mapping_key() + + def parse_block_mapping_key(self): + if self.check_token(KeyToken): + token = self.get_token() + if not self.check_token(KeyToken, ValueToken, BlockEndToken): + self.states.append(self.parse_block_mapping_value) + return self.parse_block_node_or_indentless_sequence() + else: + self.state = self.parse_block_mapping_value + return self.process_empty_scalar(token.end_mark) + if not self.check_token(BlockEndToken): + token = self.peek_token() + raise ParserError("while parsing a block mapping", self.marks[-1], + "expected , but found %r" % token.id, token.start_mark) + token = self.get_token() + event = MappingEndEvent(token.start_mark, token.end_mark) + self.state = self.states.pop() + self.marks.pop() + return event + + def parse_block_mapping_value(self): + if self.check_token(ValueToken): + token = self.get_token() + if not self.check_token(KeyToken, ValueToken, BlockEndToken): + self.states.append(self.parse_block_mapping_key) + return self.parse_block_node_or_indentless_sequence() + else: + self.state = self.parse_block_mapping_key + return self.process_empty_scalar(token.end_mark) + else: + self.state = self.parse_block_mapping_key + token = self.peek_token() + return self.process_empty_scalar(token.start_mark) + + # flow_sequence ::= FLOW-SEQUENCE-START + # (flow_sequence_entry FLOW-ENTRY)* + # flow_sequence_entry? + # FLOW-SEQUENCE-END + # flow_sequence_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? + # + # Note that while production rules for both flow_sequence_entry and + # flow_mapping_entry are equal, their interpretations are different. + # For `flow_sequence_entry`, the part `KEY flow_node? (VALUE flow_node?)?` + # generate an inline mapping (set syntax). + + def parse_flow_sequence_first_entry(self): + token = self.get_token() + self.marks.append(token.start_mark) + return self.parse_flow_sequence_entry(first=True) + + def parse_flow_sequence_entry(self, first=False): + if not self.check_token(FlowSequenceEndToken): + if not first: + if self.check_token(FlowEntryToken): + self.get_token() + else: + token = self.peek_token() + raise ParserError("while parsing a flow sequence", self.marks[-1], + "expected ',' or ']', but got %r" % token.id, token.start_mark) + + if self.check_token(KeyToken): + token = self.peek_token() + event = MappingStartEvent(None, None, True, + token.start_mark, token.end_mark, + flow_style=True) + self.state = self.parse_flow_sequence_entry_mapping_key + return event + elif not self.check_token(FlowSequenceEndToken): + self.states.append(self.parse_flow_sequence_entry) + return self.parse_flow_node() + token = self.get_token() + event = SequenceEndEvent(token.start_mark, token.end_mark) + self.state = self.states.pop() + self.marks.pop() + return event + + def parse_flow_sequence_entry_mapping_key(self): + token = self.get_token() + if not self.check_token(ValueToken, + FlowEntryToken, FlowSequenceEndToken): + self.states.append(self.parse_flow_sequence_entry_mapping_value) + return self.parse_flow_node() + else: + self.state = self.parse_flow_sequence_entry_mapping_value + return self.process_empty_scalar(token.end_mark) + + def parse_flow_sequence_entry_mapping_value(self): + if self.check_token(ValueToken): + token = self.get_token() + if not self.check_token(FlowEntryToken, FlowSequenceEndToken): + self.states.append(self.parse_flow_sequence_entry_mapping_end) + return self.parse_flow_node() + else: + self.state = self.parse_flow_sequence_entry_mapping_end + return self.process_empty_scalar(token.end_mark) + else: + self.state = self.parse_flow_sequence_entry_mapping_end + token = self.peek_token() + return self.process_empty_scalar(token.start_mark) + + def parse_flow_sequence_entry_mapping_end(self): + self.state = self.parse_flow_sequence_entry + token = self.peek_token() + return MappingEndEvent(token.start_mark, token.start_mark) + + # flow_mapping ::= FLOW-MAPPING-START + # (flow_mapping_entry FLOW-ENTRY)* + # flow_mapping_entry? + # FLOW-MAPPING-END + # flow_mapping_entry ::= flow_node | KEY flow_node? (VALUE flow_node?)? + + def parse_flow_mapping_first_key(self): + token = self.get_token() + self.marks.append(token.start_mark) + return self.parse_flow_mapping_key(first=True) + + def parse_flow_mapping_key(self, first=False): + if not self.check_token(FlowMappingEndToken): + if not first: + if self.check_token(FlowEntryToken): + self.get_token() + else: + token = self.peek_token() + raise ParserError("while parsing a flow mapping", self.marks[-1], + "expected ',' or '}', but got %r" % token.id, token.start_mark) + if self.check_token(KeyToken): + token = self.get_token() + if not self.check_token(ValueToken, + FlowEntryToken, FlowMappingEndToken): + self.states.append(self.parse_flow_mapping_value) + return self.parse_flow_node() + else: + self.state = self.parse_flow_mapping_value + return self.process_empty_scalar(token.end_mark) + elif not self.check_token(FlowMappingEndToken): + self.states.append(self.parse_flow_mapping_empty_value) + return self.parse_flow_node() + token = self.get_token() + event = MappingEndEvent(token.start_mark, token.end_mark) + self.state = self.states.pop() + self.marks.pop() + return event + + def parse_flow_mapping_value(self): + if self.check_token(ValueToken): + token = self.get_token() + if not self.check_token(FlowEntryToken, FlowMappingEndToken): + self.states.append(self.parse_flow_mapping_key) + return self.parse_flow_node() + else: + self.state = self.parse_flow_mapping_key + return self.process_empty_scalar(token.end_mark) + else: + self.state = self.parse_flow_mapping_key + token = self.peek_token() + return self.process_empty_scalar(token.start_mark) + + def parse_flow_mapping_empty_value(self): + self.state = self.parse_flow_mapping_key + return self.process_empty_scalar(self.peek_token().start_mark) + + def process_empty_scalar(self, mark): + return ScalarEvent(None, None, (True, False), '', mark, mark) + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py3/reader.py b/RenderPipelineFile/rplibs/yaml/yaml_py3/reader.py index f70e920f..4419d826 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py3/reader.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py3/reader.py @@ -1,192 +1,192 @@ -# This module contains abstractions for the input stream. You don't have to -# looks further, there are no pretty code. -# -# We define two classes here. -# -# Mark(source, line, column) -# It's just a record and its only use is producing nice error messages. -# Parser does not use it for any other purposes. -# -# Reader(source, data) -# Reader determines the encoding of `data` and converts it to unicode. -# Reader provides the following methods and attributes: -# reader.peek(length=1) - return the next `length` characters -# reader.forward(length=1) - move the current position to `length` characters. -# reader.index - the number of the current character. -# reader.line, stream.column - the line and the column of the current character. - -__all__ = ['Reader', 'ReaderError'] - -from .error import YAMLError, Mark - -import codecs, re - -class ReaderError(YAMLError): - - def __init__(self, name, position, character, encoding, reason): - self.name = name - self.character = character - self.position = position - self.encoding = encoding - self.reason = reason - - def __str__(self): - if isinstance(self.character, bytes): - return "'%s' codec can't decode byte #x%02x: %s\n" \ - " in \"%s\", position %d" \ - % (self.encoding, ord(self.character), self.reason, - self.name, self.position) - else: - return "unacceptable character #x%04x: %s\n" \ - " in \"%s\", position %d" \ - % (self.character, self.reason, - self.name, self.position) - -class Reader(object): - # Reader: - # - determines the data encoding and converts it to a unicode string, - # - checks if characters are in allowed range, - # - adds '\0' to the end. - - # Reader accepts - # - a `bytes` object, - # - a `str` object, - # - a file-like object with its `read` method returning `str`, - # - a file-like object with its `read` method returning `unicode`. - - # Yeah, it's ugly and slow. - - def __init__(self, stream): - self.name = None - self.stream = None - self.stream_pointer = 0 - self.eof = True - self.buffer = '' - self.pointer = 0 - self.raw_buffer = None - self.raw_decode = None - self.encoding = None - self.index = 0 - self.line = 0 - self.column = 0 - if isinstance(stream, str): - self.name = "" - self.check_printable(stream) - self.buffer = stream+'\0' - elif isinstance(stream, bytes): - self.name = "" - self.raw_buffer = stream - self.determine_encoding() - else: - self.stream = stream - self.name = getattr(stream, 'name', "") - self.eof = False - self.raw_buffer = None - self.determine_encoding() - - def peek(self, index=0): - try: - return self.buffer[self.pointer+index] - except IndexError: - self.update(index+1) - return self.buffer[self.pointer+index] - - def prefix(self, length=1): - if self.pointer+length >= len(self.buffer): - self.update(length) - return self.buffer[self.pointer:self.pointer+length] - - def forward(self, length=1): - if self.pointer+length+1 >= len(self.buffer): - self.update(length+1) - while length: - ch = self.buffer[self.pointer] - self.pointer += 1 - self.index += 1 - if ch in '\n\x85\u2028\u2029' \ - or (ch == '\r' and self.buffer[self.pointer] != '\n'): - self.line += 1 - self.column = 0 - elif ch != '\uFEFF': - self.column += 1 - length -= 1 - - def get_mark(self): - if self.stream is None: - return Mark(self.name, self.index, self.line, self.column, - self.buffer, self.pointer) - else: - return Mark(self.name, self.index, self.line, self.column, - None, None) - - def determine_encoding(self): - while not self.eof and (self.raw_buffer is None or len(self.raw_buffer) < 2): - self.update_raw() - if isinstance(self.raw_buffer, bytes): - if self.raw_buffer.startswith(codecs.BOM_UTF16_LE): - self.raw_decode = codecs.utf_16_le_decode - self.encoding = 'utf-16-le' - elif self.raw_buffer.startswith(codecs.BOM_UTF16_BE): - self.raw_decode = codecs.utf_16_be_decode - self.encoding = 'utf-16-be' - else: - self.raw_decode = codecs.utf_8_decode - self.encoding = 'utf-8' - self.update(1) - - NON_PRINTABLE = re.compile('[^\x09\x0A\x0D\x20-\x7E\x85\xA0-\uD7FF\uE000-\uFFFD]') - def check_printable(self, data): - match = self.NON_PRINTABLE.search(data) - if match: - character = match.group() - position = self.index+(len(self.buffer)-self.pointer)+match.start() - raise ReaderError(self.name, position, ord(character), - 'unicode', "special characters are not allowed") - - def update(self, length): - if self.raw_buffer is None: - return - self.buffer = self.buffer[self.pointer:] - self.pointer = 0 - while len(self.buffer) < length: - if not self.eof: - self.update_raw() - if self.raw_decode is not None: - try: - data, converted = self.raw_decode(self.raw_buffer, - 'strict', self.eof) - except UnicodeDecodeError as exc: - character = self.raw_buffer[exc.start] - if self.stream is not None: - position = self.stream_pointer-len(self.raw_buffer)+exc.start - else: - position = exc.start - raise ReaderError(self.name, position, character, - exc.encoding, exc.reason) - else: - data = self.raw_buffer - converted = len(data) - self.check_printable(data) - self.buffer += data - self.raw_buffer = self.raw_buffer[converted:] - if self.eof: - self.buffer += '\0' - self.raw_buffer = None - break - - def update_raw(self, size=4096): - data = self.stream.read(size) - if self.raw_buffer is None: - self.raw_buffer = data - else: - self.raw_buffer += data - self.stream_pointer += len(data) - if not data: - self.eof = True - -#try: -# import psyco -# psyco.bind(Reader) -#except ImportError: -# pass - +# This module contains abstractions for the input stream. You don't have to +# looks further, there are no pretty code. +# +# We define two classes here. +# +# Mark(source, line, column) +# It's just a record and its only use is producing nice error messages. +# Parser does not use it for any other purposes. +# +# Reader(source, data) +# Reader determines the encoding of `data` and converts it to unicode. +# Reader provides the following methods and attributes: +# reader.peek(length=1) - return the next `length` characters +# reader.forward(length=1) - move the current position to `length` characters. +# reader.index - the number of the current character. +# reader.line, stream.column - the line and the column of the current character. + +__all__ = ['Reader', 'ReaderError'] + +from .error import YAMLError, Mark + +import codecs, re + +class ReaderError(YAMLError): + + def __init__(self, name, position, character, encoding, reason): + self.name = name + self.character = character + self.position = position + self.encoding = encoding + self.reason = reason + + def __str__(self): + if isinstance(self.character, bytes): + return "'%s' codec can't decode byte #x%02x: %s\n" \ + " in \"%s\", position %d" \ + % (self.encoding, ord(self.character), self.reason, + self.name, self.position) + else: + return "unacceptable character #x%04x: %s\n" \ + " in \"%s\", position %d" \ + % (self.character, self.reason, + self.name, self.position) + +class Reader(object): + # Reader: + # - determines the data encoding and converts it to a unicode string, + # - checks if characters are in allowed range, + # - adds '\0' to the end. + + # Reader accepts + # - a `bytes` object, + # - a `str` object, + # - a file-like object with its `read` method returning `str`, + # - a file-like object with its `read` method returning `unicode`. + + # Yeah, it's ugly and slow. + + def __init__(self, stream): + self.name = None + self.stream = None + self.stream_pointer = 0 + self.eof = True + self.buffer = '' + self.pointer = 0 + self.raw_buffer = None + self.raw_decode = None + self.encoding = None + self.index = 0 + self.line = 0 + self.column = 0 + if isinstance(stream, str): + self.name = "" + self.check_printable(stream) + self.buffer = stream+'\0' + elif isinstance(stream, bytes): + self.name = "" + self.raw_buffer = stream + self.determine_encoding() + else: + self.stream = stream + self.name = getattr(stream, 'name', "") + self.eof = False + self.raw_buffer = None + self.determine_encoding() + + def peek(self, index=0): + try: + return self.buffer[self.pointer+index] + except IndexError: + self.update(index+1) + return self.buffer[self.pointer+index] + + def prefix(self, length=1): + if self.pointer+length >= len(self.buffer): + self.update(length) + return self.buffer[self.pointer:self.pointer+length] + + def forward(self, length=1): + if self.pointer+length+1 >= len(self.buffer): + self.update(length+1) + while length: + ch = self.buffer[self.pointer] + self.pointer += 1 + self.index += 1 + if ch in '\n\x85\u2028\u2029' \ + or (ch == '\r' and self.buffer[self.pointer] != '\n'): + self.line += 1 + self.column = 0 + elif ch != '\uFEFF': + self.column += 1 + length -= 1 + + def get_mark(self): + if self.stream is None: + return Mark(self.name, self.index, self.line, self.column, + self.buffer, self.pointer) + else: + return Mark(self.name, self.index, self.line, self.column, + None, None) + + def determine_encoding(self): + while not self.eof and (self.raw_buffer is None or len(self.raw_buffer) < 2): + self.update_raw() + if isinstance(self.raw_buffer, bytes): + if self.raw_buffer.startswith(codecs.BOM_UTF16_LE): + self.raw_decode = codecs.utf_16_le_decode + self.encoding = 'utf-16-le' + elif self.raw_buffer.startswith(codecs.BOM_UTF16_BE): + self.raw_decode = codecs.utf_16_be_decode + self.encoding = 'utf-16-be' + else: + self.raw_decode = codecs.utf_8_decode + self.encoding = 'utf-8' + self.update(1) + + NON_PRINTABLE = re.compile('[^\x09\x0A\x0D\x20-\x7E\x85\xA0-\uD7FF\uE000-\uFFFD]') + def check_printable(self, data): + match = self.NON_PRINTABLE.search(data) + if match: + character = match.group() + position = self.index+(len(self.buffer)-self.pointer)+match.start() + raise ReaderError(self.name, position, ord(character), + 'unicode', "special characters are not allowed") + + def update(self, length): + if self.raw_buffer is None: + return + self.buffer = self.buffer[self.pointer:] + self.pointer = 0 + while len(self.buffer) < length: + if not self.eof: + self.update_raw() + if self.raw_decode is not None: + try: + data, converted = self.raw_decode(self.raw_buffer, + 'strict', self.eof) + except UnicodeDecodeError as exc: + character = self.raw_buffer[exc.start] + if self.stream is not None: + position = self.stream_pointer-len(self.raw_buffer)+exc.start + else: + position = exc.start + raise ReaderError(self.name, position, character, + exc.encoding, exc.reason) + else: + data = self.raw_buffer + converted = len(data) + self.check_printable(data) + self.buffer += data + self.raw_buffer = self.raw_buffer[converted:] + if self.eof: + self.buffer += '\0' + self.raw_buffer = None + break + + def update_raw(self, size=4096): + data = self.stream.read(size) + if self.raw_buffer is None: + self.raw_buffer = data + else: + self.raw_buffer += data + self.stream_pointer += len(data) + if not data: + self.eof = True + +#try: +# import psyco +# psyco.bind(Reader) +#except ImportError: +# pass + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py3/representer.py b/RenderPipelineFile/rplibs/yaml/yaml_py3/representer.py index 67cd6fd2..52ab532c 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py3/representer.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py3/representer.py @@ -1,374 +1,374 @@ - -__all__ = ['BaseRepresenter', 'SafeRepresenter', 'Representer', - 'RepresenterError'] - -from .error import * -from .nodes import * - -import datetime, sys, copyreg, types, base64 - -class RepresenterError(YAMLError): - pass - -class BaseRepresenter: - - yaml_representers = {} - yaml_multi_representers = {} - - def __init__(self, default_style=None, default_flow_style=None): - self.default_style = default_style - self.default_flow_style = default_flow_style - self.represented_objects = {} - self.object_keeper = [] - self.alias_key = None - - def represent(self, data): - node = self.represent_data(data) - self.serialize(node) - self.represented_objects = {} - self.object_keeper = [] - self.alias_key = None - - def represent_data(self, data): - if self.ignore_aliases(data): - self.alias_key = None - else: - self.alias_key = id(data) - if self.alias_key is not None: - if self.alias_key in self.represented_objects: - node = self.represented_objects[self.alias_key] - #if node is None: - # raise RepresenterError("recursive objects are not allowed: %r" % data) - return node - #self.represented_objects[alias_key] = None - self.object_keeper.append(data) - data_types = type(data).__mro__ - if data_types[0] in self.yaml_representers: - node = self.yaml_representers[data_types[0]](self, data) - else: - for data_type in data_types: - if data_type in self.yaml_multi_representers: - node = self.yaml_multi_representers[data_type](self, data) - break - else: - if None in self.yaml_multi_representers: - node = self.yaml_multi_representers[None](self, data) - elif None in self.yaml_representers: - node = self.yaml_representers[None](self, data) - else: - node = ScalarNode(None, str(data)) - #if alias_key is not None: - # self.represented_objects[alias_key] = node - return node - - @classmethod - def add_representer(cls, data_type, representer): - if not 'yaml_representers' in cls.__dict__: - cls.yaml_representers = cls.yaml_representers.copy() - cls.yaml_representers[data_type] = representer - - @classmethod - def add_multi_representer(cls, data_type, representer): - if not 'yaml_multi_representers' in cls.__dict__: - cls.yaml_multi_representers = cls.yaml_multi_representers.copy() - cls.yaml_multi_representers[data_type] = representer - - def represent_scalar(self, tag, value, style=None): - if style is None: - style = self.default_style - node = ScalarNode(tag, value, style=style) - if self.alias_key is not None: - self.represented_objects[self.alias_key] = node - return node - - def represent_sequence(self, tag, sequence, flow_style=None): - value = [] - node = SequenceNode(tag, value, flow_style=flow_style) - if self.alias_key is not None: - self.represented_objects[self.alias_key] = node - best_style = True - for item in sequence: - node_item = self.represent_data(item) - if not (isinstance(node_item, ScalarNode) and not node_item.style): - best_style = False - value.append(node_item) - if flow_style is None: - if self.default_flow_style is not None: - node.flow_style = self.default_flow_style - else: - node.flow_style = best_style - return node - - def represent_mapping(self, tag, mapping, flow_style=None): - value = [] - node = MappingNode(tag, value, flow_style=flow_style) - if self.alias_key is not None: - self.represented_objects[self.alias_key] = node - best_style = True - if hasattr(mapping, 'items'): - mapping = list(mapping.items()) - try: - mapping = sorted(mapping) - except TypeError: - pass - for item_key, item_value in mapping: - node_key = self.represent_data(item_key) - node_value = self.represent_data(item_value) - if not (isinstance(node_key, ScalarNode) and not node_key.style): - best_style = False - if not (isinstance(node_value, ScalarNode) and not node_value.style): - best_style = False - value.append((node_key, node_value)) - if flow_style is None: - if self.default_flow_style is not None: - node.flow_style = self.default_flow_style - else: - node.flow_style = best_style - return node - - def ignore_aliases(self, data): - return False - -class SafeRepresenter(BaseRepresenter): - - def ignore_aliases(self, data): - if data in [None, ()]: - return True - if isinstance(data, (str, bytes, bool, int, float)): - return True - - def represent_none(self, data): - return self.represent_scalar('tag:yaml.org,2002:null', 'null') - - def represent_str(self, data): - return self.represent_scalar('tag:yaml.org,2002:str', data) - - def represent_binary(self, data): - if hasattr(base64, 'encodebytes'): - data = base64.encodebytes(data).decode('ascii') - else: - data = base64.encodestring(data).decode('ascii') - return self.represent_scalar('tag:yaml.org,2002:binary', data, style='|') - - def represent_bool(self, data): - if data: - value = 'true' - else: - value = 'false' - return self.represent_scalar('tag:yaml.org,2002:bool', value) - - def represent_int(self, data): - return self.represent_scalar('tag:yaml.org,2002:int', str(data)) - - inf_value = 1e300 - while repr(inf_value) != repr(inf_value*inf_value): - inf_value *= inf_value - - def represent_float(self, data): - if data != data or (data == 0.0 and data == 1.0): - value = '.nan' - elif data == self.inf_value: - value = '.inf' - elif data == -self.inf_value: - value = '-.inf' - else: - value = repr(data).lower() - # Note that in some cases `repr(data)` represents a float number - # without the decimal parts. For instance: - # >>> repr(1e17) - # '1e17' - # Unfortunately, this is not a valid float representation according - # to the definition of the `!!float` tag. We fix this by adding - # '.0' before the 'e' symbol. - if '.' not in value and 'e' in value: - value = value.replace('e', '.0e', 1) - return self.represent_scalar('tag:yaml.org,2002:float', value) - - def represent_list(self, data): - #pairs = (len(data) > 0 and isinstance(data, list)) - #if pairs: - # for item in data: - # if not isinstance(item, tuple) or len(item) != 2: - # pairs = False - # break - #if not pairs: - return self.represent_sequence('tag:yaml.org,2002:seq', data) - #value = [] - #for item_key, item_value in data: - # value.append(self.represent_mapping(u'tag:yaml.org,2002:map', - # [(item_key, item_value)])) - #return SequenceNode(u'tag:yaml.org,2002:pairs', value) - - def represent_dict(self, data): - return self.represent_mapping('tag:yaml.org,2002:map', data) - - def represent_set(self, data): - value = {} - for key in data: - value[key] = None - return self.represent_mapping('tag:yaml.org,2002:set', value) - - def represent_date(self, data): - value = data.isoformat() - return self.represent_scalar('tag:yaml.org,2002:timestamp', value) - - def represent_datetime(self, data): - value = data.isoformat(' ') - return self.represent_scalar('tag:yaml.org,2002:timestamp', value) - - def represent_yaml_object(self, tag, data, cls, flow_style=None): - if hasattr(data, '__getstate__'): - state = data.__getstate__() - else: - state = data.__dict__.copy() - return self.represent_mapping(tag, state, flow_style=flow_style) - - def represent_undefined(self, data): - raise RepresenterError("cannot represent an object: %s" % data) - -SafeRepresenter.add_representer(type(None), - SafeRepresenter.represent_none) - -SafeRepresenter.add_representer(str, - SafeRepresenter.represent_str) - -SafeRepresenter.add_representer(bytes, - SafeRepresenter.represent_binary) - -SafeRepresenter.add_representer(bool, - SafeRepresenter.represent_bool) - -SafeRepresenter.add_representer(int, - SafeRepresenter.represent_int) - -SafeRepresenter.add_representer(float, - SafeRepresenter.represent_float) - -SafeRepresenter.add_representer(list, - SafeRepresenter.represent_list) - -SafeRepresenter.add_representer(tuple, - SafeRepresenter.represent_list) - -SafeRepresenter.add_representer(dict, - SafeRepresenter.represent_dict) - -SafeRepresenter.add_representer(set, - SafeRepresenter.represent_set) - -SafeRepresenter.add_representer(datetime.date, - SafeRepresenter.represent_date) - -SafeRepresenter.add_representer(datetime.datetime, - SafeRepresenter.represent_datetime) - -SafeRepresenter.add_representer(None, - SafeRepresenter.represent_undefined) - -class Representer(SafeRepresenter): - - def represent_complex(self, data): - if data.imag == 0.0: - data = '%r' % data.real - elif data.real == 0.0: - data = '%rj' % data.imag - elif data.imag > 0: - data = '%r+%rj' % (data.real, data.imag) - else: - data = '%r%rj' % (data.real, data.imag) - return self.represent_scalar('tag:yaml.org,2002:python/complex', data) - - def represent_tuple(self, data): - return self.represent_sequence('tag:yaml.org,2002:python/tuple', data) - - def represent_name(self, data): - name = '%s.%s' % (data.__module__, data.__name__) - return self.represent_scalar('tag:yaml.org,2002:python/name:'+name, '') - - def represent_module(self, data): - return self.represent_scalar( - 'tag:yaml.org,2002:python/module:'+data.__name__, '') - - def represent_object(self, data): - # We use __reduce__ API to save the data. data.__reduce__ returns - # a tuple of length 2-5: - # (function, args, state, listitems, dictitems) - - # For reconstructing, we calls function(*args), then set its state, - # listitems, and dictitems if they are not None. - - # A special case is when function.__name__ == '__newobj__'. In this - # case we create the object with args[0].__new__(*args). - - # Another special case is when __reduce__ returns a string - we don't - # support it. - - # We produce a !!python/object, !!python/object/new or - # !!python/object/apply node. - - cls = type(data) - if cls in copyreg.dispatch_table: - reduce = copyreg.dispatch_table[cls](data) - elif hasattr(data, '__reduce_ex__'): - reduce = data.__reduce_ex__(2) - elif hasattr(data, '__reduce__'): - reduce = data.__reduce__() - else: - raise RepresenterError("cannot represent object: %r" % data) - reduce = (list(reduce)+[None]*5)[:5] - function, args, state, listitems, dictitems = reduce - args = list(args) - if state is None: - state = {} - if listitems is not None: - listitems = list(listitems) - if dictitems is not None: - dictitems = dict(dictitems) - if function.__name__ == '__newobj__': - function = args[0] - args = args[1:] - tag = 'tag:yaml.org,2002:python/object/new:' - newobj = True - else: - tag = 'tag:yaml.org,2002:python/object/apply:' - newobj = False - function_name = '%s.%s' % (function.__module__, function.__name__) - if not args and not listitems and not dictitems \ - and isinstance(state, dict) and newobj: - return self.represent_mapping( - 'tag:yaml.org,2002:python/object:'+function_name, state) - if not listitems and not dictitems \ - and isinstance(state, dict) and not state: - return self.represent_sequence(tag+function_name, args) - value = {} - if args: - value['args'] = args - if state or not isinstance(state, dict): - value['state'] = state - if listitems: - value['listitems'] = listitems - if dictitems: - value['dictitems'] = dictitems - return self.represent_mapping(tag+function_name, value) - -Representer.add_representer(complex, - Representer.represent_complex) - -Representer.add_representer(tuple, - Representer.represent_tuple) - -Representer.add_representer(type, - Representer.represent_name) - -Representer.add_representer(types.FunctionType, - Representer.represent_name) - -Representer.add_representer(types.BuiltinFunctionType, - Representer.represent_name) - -Representer.add_representer(types.ModuleType, - Representer.represent_module) - -Representer.add_multi_representer(object, - Representer.represent_object) - + +__all__ = ['BaseRepresenter', 'SafeRepresenter', 'Representer', + 'RepresenterError'] + +from .error import * +from .nodes import * + +import datetime, sys, copyreg, types, base64 + +class RepresenterError(YAMLError): + pass + +class BaseRepresenter: + + yaml_representers = {} + yaml_multi_representers = {} + + def __init__(self, default_style=None, default_flow_style=None): + self.default_style = default_style + self.default_flow_style = default_flow_style + self.represented_objects = {} + self.object_keeper = [] + self.alias_key = None + + def represent(self, data): + node = self.represent_data(data) + self.serialize(node) + self.represented_objects = {} + self.object_keeper = [] + self.alias_key = None + + def represent_data(self, data): + if self.ignore_aliases(data): + self.alias_key = None + else: + self.alias_key = id(data) + if self.alias_key is not None: + if self.alias_key in self.represented_objects: + node = self.represented_objects[self.alias_key] + #if node is None: + # raise RepresenterError("recursive objects are not allowed: %r" % data) + return node + #self.represented_objects[alias_key] = None + self.object_keeper.append(data) + data_types = type(data).__mro__ + if data_types[0] in self.yaml_representers: + node = self.yaml_representers[data_types[0]](self, data) + else: + for data_type in data_types: + if data_type in self.yaml_multi_representers: + node = self.yaml_multi_representers[data_type](self, data) + break + else: + if None in self.yaml_multi_representers: + node = self.yaml_multi_representers[None](self, data) + elif None in self.yaml_representers: + node = self.yaml_representers[None](self, data) + else: + node = ScalarNode(None, str(data)) + #if alias_key is not None: + # self.represented_objects[alias_key] = node + return node + + @classmethod + def add_representer(cls, data_type, representer): + if not 'yaml_representers' in cls.__dict__: + cls.yaml_representers = cls.yaml_representers.copy() + cls.yaml_representers[data_type] = representer + + @classmethod + def add_multi_representer(cls, data_type, representer): + if not 'yaml_multi_representers' in cls.__dict__: + cls.yaml_multi_representers = cls.yaml_multi_representers.copy() + cls.yaml_multi_representers[data_type] = representer + + def represent_scalar(self, tag, value, style=None): + if style is None: + style = self.default_style + node = ScalarNode(tag, value, style=style) + if self.alias_key is not None: + self.represented_objects[self.alias_key] = node + return node + + def represent_sequence(self, tag, sequence, flow_style=None): + value = [] + node = SequenceNode(tag, value, flow_style=flow_style) + if self.alias_key is not None: + self.represented_objects[self.alias_key] = node + best_style = True + for item in sequence: + node_item = self.represent_data(item) + if not (isinstance(node_item, ScalarNode) and not node_item.style): + best_style = False + value.append(node_item) + if flow_style is None: + if self.default_flow_style is not None: + node.flow_style = self.default_flow_style + else: + node.flow_style = best_style + return node + + def represent_mapping(self, tag, mapping, flow_style=None): + value = [] + node = MappingNode(tag, value, flow_style=flow_style) + if self.alias_key is not None: + self.represented_objects[self.alias_key] = node + best_style = True + if hasattr(mapping, 'items'): + mapping = list(mapping.items()) + try: + mapping = sorted(mapping) + except TypeError: + pass + for item_key, item_value in mapping: + node_key = self.represent_data(item_key) + node_value = self.represent_data(item_value) + if not (isinstance(node_key, ScalarNode) and not node_key.style): + best_style = False + if not (isinstance(node_value, ScalarNode) and not node_value.style): + best_style = False + value.append((node_key, node_value)) + if flow_style is None: + if self.default_flow_style is not None: + node.flow_style = self.default_flow_style + else: + node.flow_style = best_style + return node + + def ignore_aliases(self, data): + return False + +class SafeRepresenter(BaseRepresenter): + + def ignore_aliases(self, data): + if data in [None, ()]: + return True + if isinstance(data, (str, bytes, bool, int, float)): + return True + + def represent_none(self, data): + return self.represent_scalar('tag:yaml.org,2002:null', 'null') + + def represent_str(self, data): + return self.represent_scalar('tag:yaml.org,2002:str', data) + + def represent_binary(self, data): + if hasattr(base64, 'encodebytes'): + data = base64.encodebytes(data).decode('ascii') + else: + data = base64.encodestring(data).decode('ascii') + return self.represent_scalar('tag:yaml.org,2002:binary', data, style='|') + + def represent_bool(self, data): + if data: + value = 'true' + else: + value = 'false' + return self.represent_scalar('tag:yaml.org,2002:bool', value) + + def represent_int(self, data): + return self.represent_scalar('tag:yaml.org,2002:int', str(data)) + + inf_value = 1e300 + while repr(inf_value) != repr(inf_value*inf_value): + inf_value *= inf_value + + def represent_float(self, data): + if data != data or (data == 0.0 and data == 1.0): + value = '.nan' + elif data == self.inf_value: + value = '.inf' + elif data == -self.inf_value: + value = '-.inf' + else: + value = repr(data).lower() + # Note that in some cases `repr(data)` represents a float number + # without the decimal parts. For instance: + # >>> repr(1e17) + # '1e17' + # Unfortunately, this is not a valid float representation according + # to the definition of the `!!float` tag. We fix this by adding + # '.0' before the 'e' symbol. + if '.' not in value and 'e' in value: + value = value.replace('e', '.0e', 1) + return self.represent_scalar('tag:yaml.org,2002:float', value) + + def represent_list(self, data): + #pairs = (len(data) > 0 and isinstance(data, list)) + #if pairs: + # for item in data: + # if not isinstance(item, tuple) or len(item) != 2: + # pairs = False + # break + #if not pairs: + return self.represent_sequence('tag:yaml.org,2002:seq', data) + #value = [] + #for item_key, item_value in data: + # value.append(self.represent_mapping(u'tag:yaml.org,2002:map', + # [(item_key, item_value)])) + #return SequenceNode(u'tag:yaml.org,2002:pairs', value) + + def represent_dict(self, data): + return self.represent_mapping('tag:yaml.org,2002:map', data) + + def represent_set(self, data): + value = {} + for key in data: + value[key] = None + return self.represent_mapping('tag:yaml.org,2002:set', value) + + def represent_date(self, data): + value = data.isoformat() + return self.represent_scalar('tag:yaml.org,2002:timestamp', value) + + def represent_datetime(self, data): + value = data.isoformat(' ') + return self.represent_scalar('tag:yaml.org,2002:timestamp', value) + + def represent_yaml_object(self, tag, data, cls, flow_style=None): + if hasattr(data, '__getstate__'): + state = data.__getstate__() + else: + state = data.__dict__.copy() + return self.represent_mapping(tag, state, flow_style=flow_style) + + def represent_undefined(self, data): + raise RepresenterError("cannot represent an object: %s" % data) + +SafeRepresenter.add_representer(type(None), + SafeRepresenter.represent_none) + +SafeRepresenter.add_representer(str, + SafeRepresenter.represent_str) + +SafeRepresenter.add_representer(bytes, + SafeRepresenter.represent_binary) + +SafeRepresenter.add_representer(bool, + SafeRepresenter.represent_bool) + +SafeRepresenter.add_representer(int, + SafeRepresenter.represent_int) + +SafeRepresenter.add_representer(float, + SafeRepresenter.represent_float) + +SafeRepresenter.add_representer(list, + SafeRepresenter.represent_list) + +SafeRepresenter.add_representer(tuple, + SafeRepresenter.represent_list) + +SafeRepresenter.add_representer(dict, + SafeRepresenter.represent_dict) + +SafeRepresenter.add_representer(set, + SafeRepresenter.represent_set) + +SafeRepresenter.add_representer(datetime.date, + SafeRepresenter.represent_date) + +SafeRepresenter.add_representer(datetime.datetime, + SafeRepresenter.represent_datetime) + +SafeRepresenter.add_representer(None, + SafeRepresenter.represent_undefined) + +class Representer(SafeRepresenter): + + def represent_complex(self, data): + if data.imag == 0.0: + data = '%r' % data.real + elif data.real == 0.0: + data = '%rj' % data.imag + elif data.imag > 0: + data = '%r+%rj' % (data.real, data.imag) + else: + data = '%r%rj' % (data.real, data.imag) + return self.represent_scalar('tag:yaml.org,2002:python/complex', data) + + def represent_tuple(self, data): + return self.represent_sequence('tag:yaml.org,2002:python/tuple', data) + + def represent_name(self, data): + name = '%s.%s' % (data.__module__, data.__name__) + return self.represent_scalar('tag:yaml.org,2002:python/name:'+name, '') + + def represent_module(self, data): + return self.represent_scalar( + 'tag:yaml.org,2002:python/module:'+data.__name__, '') + + def represent_object(self, data): + # We use __reduce__ API to save the data. data.__reduce__ returns + # a tuple of length 2-5: + # (function, args, state, listitems, dictitems) + + # For reconstructing, we calls function(*args), then set its state, + # listitems, and dictitems if they are not None. + + # A special case is when function.__name__ == '__newobj__'. In this + # case we create the object with args[0].__new__(*args). + + # Another special case is when __reduce__ returns a string - we don't + # support it. + + # We produce a !!python/object, !!python/object/new or + # !!python/object/apply node. + + cls = type(data) + if cls in copyreg.dispatch_table: + reduce = copyreg.dispatch_table[cls](data) + elif hasattr(data, '__reduce_ex__'): + reduce = data.__reduce_ex__(2) + elif hasattr(data, '__reduce__'): + reduce = data.__reduce__() + else: + raise RepresenterError("cannot represent object: %r" % data) + reduce = (list(reduce)+[None]*5)[:5] + function, args, state, listitems, dictitems = reduce + args = list(args) + if state is None: + state = {} + if listitems is not None: + listitems = list(listitems) + if dictitems is not None: + dictitems = dict(dictitems) + if function.__name__ == '__newobj__': + function = args[0] + args = args[1:] + tag = 'tag:yaml.org,2002:python/object/new:' + newobj = True + else: + tag = 'tag:yaml.org,2002:python/object/apply:' + newobj = False + function_name = '%s.%s' % (function.__module__, function.__name__) + if not args and not listitems and not dictitems \ + and isinstance(state, dict) and newobj: + return self.represent_mapping( + 'tag:yaml.org,2002:python/object:'+function_name, state) + if not listitems and not dictitems \ + and isinstance(state, dict) and not state: + return self.represent_sequence(tag+function_name, args) + value = {} + if args: + value['args'] = args + if state or not isinstance(state, dict): + value['state'] = state + if listitems: + value['listitems'] = listitems + if dictitems: + value['dictitems'] = dictitems + return self.represent_mapping(tag+function_name, value) + +Representer.add_representer(complex, + Representer.represent_complex) + +Representer.add_representer(tuple, + Representer.represent_tuple) + +Representer.add_representer(type, + Representer.represent_name) + +Representer.add_representer(types.FunctionType, + Representer.represent_name) + +Representer.add_representer(types.BuiltinFunctionType, + Representer.represent_name) + +Representer.add_representer(types.ModuleType, + Representer.represent_module) + +Representer.add_multi_representer(object, + Representer.represent_object) + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py3/resolver.py b/RenderPipelineFile/rplibs/yaml/yaml_py3/resolver.py index 0eece258..469684b2 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py3/resolver.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py3/resolver.py @@ -1,224 +1,224 @@ - -__all__ = ['BaseResolver', 'Resolver'] - -from .error import * -from .nodes import * - -import re - -class ResolverError(YAMLError): - pass - -class BaseResolver: - - DEFAULT_SCALAR_TAG = 'tag:yaml.org,2002:str' - DEFAULT_SEQUENCE_TAG = 'tag:yaml.org,2002:seq' - DEFAULT_MAPPING_TAG = 'tag:yaml.org,2002:map' - - yaml_implicit_resolvers = {} - yaml_path_resolvers = {} - - def __init__(self): - self.resolver_exact_paths = [] - self.resolver_prefix_paths = [] - - @classmethod - def add_implicit_resolver(cls, tag, regexp, first): - if not 'yaml_implicit_resolvers' in cls.__dict__: - cls.yaml_implicit_resolvers = cls.yaml_implicit_resolvers.copy() - if first is None: - first = [None] - for ch in first: - cls.yaml_implicit_resolvers.setdefault(ch, []).append((tag, regexp)) - - @classmethod - def add_path_resolver(cls, tag, path, kind=None): - # Note: `add_path_resolver` is experimental. The API could be changed. - # `new_path` is a pattern that is matched against the path from the - # root to the node that is being considered. `node_path` elements are - # tuples `(node_check, index_check)`. `node_check` is a node class: - # `ScalarNode`, `SequenceNode`, `MappingNode` or `None`. `None` - # matches any kind of a node. `index_check` could be `None`, a boolean - # value, a string value, or a number. `None` and `False` match against - # any _value_ of sequence and mapping nodes. `True` matches against - # any _key_ of a mapping node. A string `index_check` matches against - # a mapping value that corresponds to a scalar key which content is - # equal to the `index_check` value. An integer `index_check` matches - # against a sequence value with the index equal to `index_check`. - if not 'yaml_path_resolvers' in cls.__dict__: - cls.yaml_path_resolvers = cls.yaml_path_resolvers.copy() - new_path = [] - for element in path: - if isinstance(element, (list, tuple)): - if len(element) == 2: - node_check, index_check = element - elif len(element) == 1: - node_check = element[0] - index_check = True - else: - raise ResolverError("Invalid path element: %s" % element) - else: - node_check = None - index_check = element - if node_check is str: - node_check = ScalarNode - elif node_check is list: - node_check = SequenceNode - elif node_check is dict: - node_check = MappingNode - elif node_check not in [ScalarNode, SequenceNode, MappingNode] \ - and not isinstance(node_check, str) \ - and node_check is not None: - raise ResolverError("Invalid node checker: %s" % node_check) - if not isinstance(index_check, (str, int)) \ - and index_check is not None: - raise ResolverError("Invalid index checker: %s" % index_check) - new_path.append((node_check, index_check)) - if kind is str: - kind = ScalarNode - elif kind is list: - kind = SequenceNode - elif kind is dict: - kind = MappingNode - elif kind not in [ScalarNode, SequenceNode, MappingNode] \ - and kind is not None: - raise ResolverError("Invalid node kind: %s" % kind) - cls.yaml_path_resolvers[tuple(new_path), kind] = tag - - def descend_resolver(self, current_node, current_index): - if not self.yaml_path_resolvers: - return - exact_paths = {} - prefix_paths = [] - if current_node: - depth = len(self.resolver_prefix_paths) - for path, kind in self.resolver_prefix_paths[-1]: - if self.check_resolver_prefix(depth, path, kind, - current_node, current_index): - if len(path) > depth: - prefix_paths.append((path, kind)) - else: - exact_paths[kind] = self.yaml_path_resolvers[path, kind] - else: - for path, kind in self.yaml_path_resolvers: - if not path: - exact_paths[kind] = self.yaml_path_resolvers[path, kind] - else: - prefix_paths.append((path, kind)) - self.resolver_exact_paths.append(exact_paths) - self.resolver_prefix_paths.append(prefix_paths) - - def ascend_resolver(self): - if not self.yaml_path_resolvers: - return - self.resolver_exact_paths.pop() - self.resolver_prefix_paths.pop() - - def check_resolver_prefix(self, depth, path, kind, - current_node, current_index): - node_check, index_check = path[depth-1] - if isinstance(node_check, str): - if current_node.tag != node_check: - return - elif node_check is not None: - if not isinstance(current_node, node_check): - return - if index_check is True and current_index is not None: - return - if (index_check is False or index_check is None) \ - and current_index is None: - return - if isinstance(index_check, str): - if not (isinstance(current_index, ScalarNode) - and index_check == current_index.value): - return - elif isinstance(index_check, int) and not isinstance(index_check, bool): - if index_check != current_index: - return - return True - - def resolve(self, kind, value, implicit): - if kind is ScalarNode and implicit[0]: - if value == '': - resolvers = self.yaml_implicit_resolvers.get('', []) - else: - resolvers = self.yaml_implicit_resolvers.get(value[0], []) - resolvers += self.yaml_implicit_resolvers.get(None, []) - for tag, regexp in resolvers: - if regexp.match(value): - return tag - implicit = implicit[1] - if self.yaml_path_resolvers: - exact_paths = self.resolver_exact_paths[-1] - if kind in exact_paths: - return exact_paths[kind] - if None in exact_paths: - return exact_paths[None] - if kind is ScalarNode: - return self.DEFAULT_SCALAR_TAG - elif kind is SequenceNode: - return self.DEFAULT_SEQUENCE_TAG - elif kind is MappingNode: - return self.DEFAULT_MAPPING_TAG - -class Resolver(BaseResolver): - pass - -Resolver.add_implicit_resolver( - 'tag:yaml.org,2002:bool', - re.compile(r'''^(?:yes|Yes|YES|no|No|NO - |true|True|TRUE|false|False|FALSE - |on|On|ON|off|Off|OFF)$''', re.X), - list('yYnNtTfFoO')) - -Resolver.add_implicit_resolver( - 'tag:yaml.org,2002:float', - re.compile(r'''^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+][0-9]+)? - |\.[0-9_]+(?:[eE][-+][0-9]+)? - |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]* - |[-+]?\.(?:inf|Inf|INF) - |\.(?:nan|NaN|NAN))$''', re.X), - list('-+0123456789.')) - -Resolver.add_implicit_resolver( - 'tag:yaml.org,2002:int', - re.compile(r'''^(?:[-+]?0b[0-1_]+ - |[-+]?0[0-7_]+ - |[-+]?(?:0|[1-9][0-9_]*) - |[-+]?0x[0-9a-fA-F_]+ - |[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$''', re.X), - list('-+0123456789')) - -Resolver.add_implicit_resolver( - 'tag:yaml.org,2002:merge', - re.compile(r'^(?:<<)$'), - ['<']) - -Resolver.add_implicit_resolver( - 'tag:yaml.org,2002:null', - re.compile(r'''^(?: ~ - |null|Null|NULL - | )$''', re.X), - ['~', 'n', 'N', '']) - -Resolver.add_implicit_resolver( - 'tag:yaml.org,2002:timestamp', - re.compile(r'''^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] - |[0-9][0-9][0-9][0-9] -[0-9][0-9]? -[0-9][0-9]? - (?:[Tt]|[ \t]+)[0-9][0-9]? - :[0-9][0-9] :[0-9][0-9] (?:\.[0-9]*)? - (?:[ \t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$''', re.X), - list('0123456789')) - -Resolver.add_implicit_resolver( - 'tag:yaml.org,2002:value', - re.compile(r'^(?:=)$'), - ['=']) - -# The following resolver is only for documentation purposes. It cannot work -# because plain scalars cannot start with '!', '&', or '*'. -Resolver.add_implicit_resolver( - 'tag:yaml.org,2002:yaml', - re.compile(r'^(?:!|&|\*)$'), - list('!&*')) - + +__all__ = ['BaseResolver', 'Resolver'] + +from .error import * +from .nodes import * + +import re + +class ResolverError(YAMLError): + pass + +class BaseResolver: + + DEFAULT_SCALAR_TAG = 'tag:yaml.org,2002:str' + DEFAULT_SEQUENCE_TAG = 'tag:yaml.org,2002:seq' + DEFAULT_MAPPING_TAG = 'tag:yaml.org,2002:map' + + yaml_implicit_resolvers = {} + yaml_path_resolvers = {} + + def __init__(self): + self.resolver_exact_paths = [] + self.resolver_prefix_paths = [] + + @classmethod + def add_implicit_resolver(cls, tag, regexp, first): + if not 'yaml_implicit_resolvers' in cls.__dict__: + cls.yaml_implicit_resolvers = cls.yaml_implicit_resolvers.copy() + if first is None: + first = [None] + for ch in first: + cls.yaml_implicit_resolvers.setdefault(ch, []).append((tag, regexp)) + + @classmethod + def add_path_resolver(cls, tag, path, kind=None): + # Note: `add_path_resolver` is experimental. The API could be changed. + # `new_path` is a pattern that is matched against the path from the + # root to the node that is being considered. `node_path` elements are + # tuples `(node_check, index_check)`. `node_check` is a node class: + # `ScalarNode`, `SequenceNode`, `MappingNode` or `None`. `None` + # matches any kind of a node. `index_check` could be `None`, a boolean + # value, a string value, or a number. `None` and `False` match against + # any _value_ of sequence and mapping nodes. `True` matches against + # any _key_ of a mapping node. A string `index_check` matches against + # a mapping value that corresponds to a scalar key which content is + # equal to the `index_check` value. An integer `index_check` matches + # against a sequence value with the index equal to `index_check`. + if not 'yaml_path_resolvers' in cls.__dict__: + cls.yaml_path_resolvers = cls.yaml_path_resolvers.copy() + new_path = [] + for element in path: + if isinstance(element, (list, tuple)): + if len(element) == 2: + node_check, index_check = element + elif len(element) == 1: + node_check = element[0] + index_check = True + else: + raise ResolverError("Invalid path element: %s" % element) + else: + node_check = None + index_check = element + if node_check is str: + node_check = ScalarNode + elif node_check is list: + node_check = SequenceNode + elif node_check is dict: + node_check = MappingNode + elif node_check not in [ScalarNode, SequenceNode, MappingNode] \ + and not isinstance(node_check, str) \ + and node_check is not None: + raise ResolverError("Invalid node checker: %s" % node_check) + if not isinstance(index_check, (str, int)) \ + and index_check is not None: + raise ResolverError("Invalid index checker: %s" % index_check) + new_path.append((node_check, index_check)) + if kind is str: + kind = ScalarNode + elif kind is list: + kind = SequenceNode + elif kind is dict: + kind = MappingNode + elif kind not in [ScalarNode, SequenceNode, MappingNode] \ + and kind is not None: + raise ResolverError("Invalid node kind: %s" % kind) + cls.yaml_path_resolvers[tuple(new_path), kind] = tag + + def descend_resolver(self, current_node, current_index): + if not self.yaml_path_resolvers: + return + exact_paths = {} + prefix_paths = [] + if current_node: + depth = len(self.resolver_prefix_paths) + for path, kind in self.resolver_prefix_paths[-1]: + if self.check_resolver_prefix(depth, path, kind, + current_node, current_index): + if len(path) > depth: + prefix_paths.append((path, kind)) + else: + exact_paths[kind] = self.yaml_path_resolvers[path, kind] + else: + for path, kind in self.yaml_path_resolvers: + if not path: + exact_paths[kind] = self.yaml_path_resolvers[path, kind] + else: + prefix_paths.append((path, kind)) + self.resolver_exact_paths.append(exact_paths) + self.resolver_prefix_paths.append(prefix_paths) + + def ascend_resolver(self): + if not self.yaml_path_resolvers: + return + self.resolver_exact_paths.pop() + self.resolver_prefix_paths.pop() + + def check_resolver_prefix(self, depth, path, kind, + current_node, current_index): + node_check, index_check = path[depth-1] + if isinstance(node_check, str): + if current_node.tag != node_check: + return + elif node_check is not None: + if not isinstance(current_node, node_check): + return + if index_check is True and current_index is not None: + return + if (index_check is False or index_check is None) \ + and current_index is None: + return + if isinstance(index_check, str): + if not (isinstance(current_index, ScalarNode) + and index_check == current_index.value): + return + elif isinstance(index_check, int) and not isinstance(index_check, bool): + if index_check != current_index: + return + return True + + def resolve(self, kind, value, implicit): + if kind is ScalarNode and implicit[0]: + if value == '': + resolvers = self.yaml_implicit_resolvers.get('', []) + else: + resolvers = self.yaml_implicit_resolvers.get(value[0], []) + resolvers += self.yaml_implicit_resolvers.get(None, []) + for tag, regexp in resolvers: + if regexp.match(value): + return tag + implicit = implicit[1] + if self.yaml_path_resolvers: + exact_paths = self.resolver_exact_paths[-1] + if kind in exact_paths: + return exact_paths[kind] + if None in exact_paths: + return exact_paths[None] + if kind is ScalarNode: + return self.DEFAULT_SCALAR_TAG + elif kind is SequenceNode: + return self.DEFAULT_SEQUENCE_TAG + elif kind is MappingNode: + return self.DEFAULT_MAPPING_TAG + +class Resolver(BaseResolver): + pass + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:bool', + re.compile(r'''^(?:yes|Yes|YES|no|No|NO + |true|True|TRUE|false|False|FALSE + |on|On|ON|off|Off|OFF)$''', re.X), + list('yYnNtTfFoO')) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:float', + re.compile(r'''^(?:[-+]?(?:[0-9][0-9_]*)\.[0-9_]*(?:[eE][-+][0-9]+)? + |\.[0-9_]+(?:[eE][-+][0-9]+)? + |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\.[0-9_]* + |[-+]?\.(?:inf|Inf|INF) + |\.(?:nan|NaN|NAN))$''', re.X), + list('-+0123456789.')) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:int', + re.compile(r'''^(?:[-+]?0b[0-1_]+ + |[-+]?0[0-7_]+ + |[-+]?(?:0|[1-9][0-9_]*) + |[-+]?0x[0-9a-fA-F_]+ + |[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$''', re.X), + list('-+0123456789')) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:merge', + re.compile(r'^(?:<<)$'), + ['<']) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:null', + re.compile(r'''^(?: ~ + |null|Null|NULL + | )$''', re.X), + ['~', 'n', 'N', '']) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:timestamp', + re.compile(r'''^(?:[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] + |[0-9][0-9][0-9][0-9] -[0-9][0-9]? -[0-9][0-9]? + (?:[Tt]|[ \t]+)[0-9][0-9]? + :[0-9][0-9] :[0-9][0-9] (?:\.[0-9]*)? + (?:[ \t]*(?:Z|[-+][0-9][0-9]?(?::[0-9][0-9])?))?)$''', re.X), + list('0123456789')) + +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:value', + re.compile(r'^(?:=)$'), + ['=']) + +# The following resolver is only for documentation purposes. It cannot work +# because plain scalars cannot start with '!', '&', or '*'. +Resolver.add_implicit_resolver( + 'tag:yaml.org,2002:yaml', + re.compile(r'^(?:!|&|\*)$'), + list('!&*')) + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py3/scanner.py b/RenderPipelineFile/rplibs/yaml/yaml_py3/scanner.py index 494d975b..5261ee72 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py3/scanner.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py3/scanner.py @@ -1,1448 +1,1448 @@ - -# Scanner produces tokens of the following types: -# STREAM-START -# STREAM-END -# DIRECTIVE(name, value) -# DOCUMENT-START -# DOCUMENT-END -# BLOCK-SEQUENCE-START -# BLOCK-MAPPING-START -# BLOCK-END -# FLOW-SEQUENCE-START -# FLOW-MAPPING-START -# FLOW-SEQUENCE-END -# FLOW-MAPPING-END -# BLOCK-ENTRY -# FLOW-ENTRY -# KEY -# VALUE -# ALIAS(value) -# ANCHOR(value) -# TAG(value) -# SCALAR(value, plain, style) -# -# Read comments in the Scanner code for more details. -# - -__all__ = ['Scanner', 'ScannerError'] - -from .error import MarkedYAMLError -from .tokens import * - -class ScannerError(MarkedYAMLError): - pass - -class SimpleKey: - # See below simple keys treatment. - - def __init__(self, token_number, required, index, line, column, mark): - self.token_number = token_number - self.required = required - self.index = index - self.line = line - self.column = column - self.mark = mark - -class Scanner: - - def __init__(self): - """Initialize the scanner.""" - # It is assumed that Scanner and Reader will have a common descendant. - # Reader do the dirty work of checking for BOM and converting the - # input data to Unicode. It also adds NUL to the end. - # - # Reader supports the following methods - # self.peek(i=0) # peek the next i-th character - # self.prefix(l=1) # peek the next l characters - # self.forward(l=1) # read the next l characters and move the pointer. - - # Had we reached the end of the stream? - self.done = False - - # The number of unclosed '{' and '['. `flow_level == 0` means block - # context. - self.flow_level = 0 - - # List of processed tokens that are not yet emitted. - self.tokens = [] - - # Add the STREAM-START token. - self.fetch_stream_start() - - # Number of tokens that were emitted through the `get_token` method. - self.tokens_taken = 0 - - # The current indentation level. - self.indent = -1 - - # Past indentation levels. - self.indents = [] - - # Variables related to simple keys treatment. - - # A simple key is a key that is not denoted by the '?' indicator. - # Example of simple keys: - # --- - # block simple key: value - # ? not a simple key: - # : { flow simple key: value } - # We emit the KEY token before all keys, so when we find a potential - # simple key, we try to locate the corresponding ':' indicator. - # Simple keys should be limited to a single line and 1024 characters. - - # Can a simple key start at the current position? A simple key may - # start: - # - at the beginning of the line, not counting indentation spaces - # (in block context), - # - after '{', '[', ',' (in the flow context), - # - after '?', ':', '-' (in the block context). - # In the block context, this flag also signifies if a block collection - # may start at the current position. - self.allow_simple_key = True - - # Keep track of possible simple keys. This is a dictionary. The key - # is `flow_level`; there can be no more that one possible simple key - # for each level. The value is a SimpleKey record: - # (token_number, required, index, line, column, mark) - # A simple key may start with ALIAS, ANCHOR, TAG, SCALAR(flow), - # '[', or '{' tokens. - self.possible_simple_keys = {} - - # Public methods. - - def check_token(self, *choices): - # Check if the next token is one of the given types. - while self.need_more_tokens(): - self.fetch_more_tokens() - if self.tokens: - if not choices: - return True - for choice in choices: - if isinstance(self.tokens[0], choice): - return True - return False - - def peek_token(self): - # Return the next token, but do not delete if from the queue. - while self.need_more_tokens(): - self.fetch_more_tokens() - if self.tokens: - return self.tokens[0] - - def get_token(self): - # Return the next token. - while self.need_more_tokens(): - self.fetch_more_tokens() - if self.tokens: - self.tokens_taken += 1 - return self.tokens.pop(0) - - # Private methods. - - def need_more_tokens(self): - if self.done: - return False - if not self.tokens: - return True - # The current token may be a potential simple key, so we - # need to look further. - self.stale_possible_simple_keys() - if self.next_possible_simple_key() == self.tokens_taken: - return True - - def fetch_more_tokens(self): - - # Eat whitespaces and comments until we reach the next token. - self.scan_to_next_token() - - # Remove obsolete possible simple keys. - self.stale_possible_simple_keys() - - # Compare the current indentation and column. It may add some tokens - # and decrease the current indentation level. - self.unwind_indent(self.column) - - # Peek the next character. - ch = self.peek() - - # Is it the end of stream? - if ch == '\0': - return self.fetch_stream_end() - - # Is it a directive? - if ch == '%' and self.check_directive(): - return self.fetch_directive() - - # Is it the document start? - if ch == '-' and self.check_document_start(): - return self.fetch_document_start() - - # Is it the document end? - if ch == '.' and self.check_document_end(): - return self.fetch_document_end() - - # TODO: support for BOM within a stream. - #if ch == '\uFEFF': - # return self.fetch_bom() <-- issue BOMToken - - # Note: the order of the following checks is NOT significant. - - # Is it the flow sequence start indicator? - if ch == '[': - return self.fetch_flow_sequence_start() - - # Is it the flow mapping start indicator? - if ch == '{': - return self.fetch_flow_mapping_start() - - # Is it the flow sequence end indicator? - if ch == ']': - return self.fetch_flow_sequence_end() - - # Is it the flow mapping end indicator? - if ch == '}': - return self.fetch_flow_mapping_end() - - # Is it the flow entry indicator? - if ch == ',': - return self.fetch_flow_entry() - - # Is it the block entry indicator? - if ch == '-' and self.check_block_entry(): - return self.fetch_block_entry() - - # Is it the key indicator? - if ch == '?' and self.check_key(): - return self.fetch_key() - - # Is it the value indicator? - if ch == ':' and self.check_value(): - return self.fetch_value() - - # Is it an alias? - if ch == '*': - return self.fetch_alias() - - # Is it an anchor? - if ch == '&': - return self.fetch_anchor() - - # Is it a tag? - if ch == '!': - return self.fetch_tag() - - # Is it a literal scalar? - if ch == '|' and not self.flow_level: - return self.fetch_literal() - - # Is it a folded scalar? - if ch == '>' and not self.flow_level: - return self.fetch_folded() - - # Is it a single quoted scalar? - if ch == '\'': - return self.fetch_single() - - # Is it a double quoted scalar? - if ch == '\"': - return self.fetch_double() - - # It must be a plain scalar then. - if self.check_plain(): - return self.fetch_plain() - - # No? It's an error. Let's produce a nice error message. - raise ScannerError("while scanning for the next token", None, - "found character %r that cannot start any token" % ch, - self.get_mark()) - - # Simple keys treatment. - - def next_possible_simple_key(self): - # Return the number of the nearest possible simple key. Actually we - # don't need to loop through the whole dictionary. We may replace it - # with the following code: - # if not self.possible_simple_keys: - # return None - # return self.possible_simple_keys[ - # min(self.possible_simple_keys.keys())].token_number - min_token_number = None - for level in self.possible_simple_keys: - key = self.possible_simple_keys[level] - if min_token_number is None or key.token_number < min_token_number: - min_token_number = key.token_number - return min_token_number - - def stale_possible_simple_keys(self): - # Remove entries that are no longer possible simple keys. According to - # the YAML specification, simple keys - # - should be limited to a single line, - # - should be no longer than 1024 characters. - # Disabling this procedure will allow simple keys of any length and - # height (may cause problems if indentation is broken though). - for level in list(self.possible_simple_keys): - key = self.possible_simple_keys[level] - if key.line != self.line \ - or self.index-key.index > 1024: - if key.required: - raise ScannerError("while scanning a simple key", key.mark, - "could not found expected ':'", self.get_mark()) - del self.possible_simple_keys[level] - - def save_possible_simple_key(self): - # The next token may start a simple key. We check if it's possible - # and save its position. This function is called for - # ALIAS, ANCHOR, TAG, SCALAR(flow), '[', and '{'. - - # Check if a simple key is required at the current position. - required = not self.flow_level and self.indent == self.column - - # A simple key is required only if it is the first token in the current - # line. Therefore it is always allowed. - assert self.allow_simple_key or not required - - # The next token might be a simple key. Let's save it's number and - # position. - if self.allow_simple_key: - self.remove_possible_simple_key() - token_number = self.tokens_taken+len(self.tokens) - key = SimpleKey(token_number, required, - self.index, self.line, self.column, self.get_mark()) - self.possible_simple_keys[self.flow_level] = key - - def remove_possible_simple_key(self): - # Remove the saved possible key position at the current flow level. - if self.flow_level in self.possible_simple_keys: - key = self.possible_simple_keys[self.flow_level] - - if key.required: - raise ScannerError("while scanning a simple key", key.mark, - "could not found expected ':'", self.get_mark()) - - del self.possible_simple_keys[self.flow_level] - - # Indentation functions. - - def unwind_indent(self, column): - - ## In flow context, tokens should respect indentation. - ## Actually the condition should be `self.indent >= column` according to - ## the spec. But this condition will prohibit intuitively correct - ## constructions such as - ## key : { - ## } - #if self.flow_level and self.indent > column: - # raise ScannerError(None, None, - # "invalid intendation or unclosed '[' or '{'", - # self.get_mark()) - - # In the flow context, indentation is ignored. We make the scanner less - # restrictive then specification requires. - if self.flow_level: - return - - # In block context, we may need to issue the BLOCK-END tokens. - while self.indent > column: - mark = self.get_mark() - self.indent = self.indents.pop() - self.tokens.append(BlockEndToken(mark, mark)) - - def add_indent(self, column): - # Check if we need to increase indentation. - if self.indent < column: - self.indents.append(self.indent) - self.indent = column - return True - return False - - # Fetchers. - - def fetch_stream_start(self): - # We always add STREAM-START as the first token and STREAM-END as the - # last token. - - # Read the token. - mark = self.get_mark() - - # Add STREAM-START. - self.tokens.append(StreamStartToken(mark, mark, - encoding=self.encoding)) - - - def fetch_stream_end(self): - - # Set the current intendation to -1. - self.unwind_indent(-1) - - # Reset simple keys. - self.remove_possible_simple_key() - self.allow_simple_key = False - self.possible_simple_keys = {} - - # Read the token. - mark = self.get_mark() - - # Add STREAM-END. - self.tokens.append(StreamEndToken(mark, mark)) - - # The steam is finished. - self.done = True - - def fetch_directive(self): - - # Set the current intendation to -1. - self.unwind_indent(-1) - - # Reset simple keys. - self.remove_possible_simple_key() - self.allow_simple_key = False - - # Scan and add DIRECTIVE. - self.tokens.append(self.scan_directive()) - - def fetch_document_start(self): - self.fetch_document_indicator(DocumentStartToken) - - def fetch_document_end(self): - self.fetch_document_indicator(DocumentEndToken) - - def fetch_document_indicator(self, TokenClass): - - # Set the current intendation to -1. - self.unwind_indent(-1) - - # Reset simple keys. Note that there could not be a block collection - # after '---'. - self.remove_possible_simple_key() - self.allow_simple_key = False - - # Add DOCUMENT-START or DOCUMENT-END. - start_mark = self.get_mark() - self.forward(3) - end_mark = self.get_mark() - self.tokens.append(TokenClass(start_mark, end_mark)) - - def fetch_flow_sequence_start(self): - self.fetch_flow_collection_start(FlowSequenceStartToken) - - def fetch_flow_mapping_start(self): - self.fetch_flow_collection_start(FlowMappingStartToken) - - def fetch_flow_collection_start(self, TokenClass): - - # '[' and '{' may start a simple key. - self.save_possible_simple_key() - - # Increase the flow level. - self.flow_level += 1 - - # Simple keys are allowed after '[' and '{'. - self.allow_simple_key = True - - # Add FLOW-SEQUENCE-START or FLOW-MAPPING-START. - start_mark = self.get_mark() - self.forward() - end_mark = self.get_mark() - self.tokens.append(TokenClass(start_mark, end_mark)) - - def fetch_flow_sequence_end(self): - self.fetch_flow_collection_end(FlowSequenceEndToken) - - def fetch_flow_mapping_end(self): - self.fetch_flow_collection_end(FlowMappingEndToken) - - def fetch_flow_collection_end(self, TokenClass): - - # Reset possible simple key on the current level. - self.remove_possible_simple_key() - - # Decrease the flow level. - self.flow_level -= 1 - - # No simple keys after ']' or '}'. - self.allow_simple_key = False - - # Add FLOW-SEQUENCE-END or FLOW-MAPPING-END. - start_mark = self.get_mark() - self.forward() - end_mark = self.get_mark() - self.tokens.append(TokenClass(start_mark, end_mark)) - - def fetch_flow_entry(self): - - # Simple keys are allowed after ','. - self.allow_simple_key = True - - # Reset possible simple key on the current level. - self.remove_possible_simple_key() - - # Add FLOW-ENTRY. - start_mark = self.get_mark() - self.forward() - end_mark = self.get_mark() - self.tokens.append(FlowEntryToken(start_mark, end_mark)) - - def fetch_block_entry(self): - - # Block context needs additional checks. - if not self.flow_level: - - # Are we allowed to start a new entry? - if not self.allow_simple_key: - raise ScannerError(None, None, - "sequence entries are not allowed here", - self.get_mark()) - - # We may need to add BLOCK-SEQUENCE-START. - if self.add_indent(self.column): - mark = self.get_mark() - self.tokens.append(BlockSequenceStartToken(mark, mark)) - - # It's an error for the block entry to occur in the flow context, - # but we let the parser detect this. - else: - pass - - # Simple keys are allowed after '-'. - self.allow_simple_key = True - - # Reset possible simple key on the current level. - self.remove_possible_simple_key() - - # Add BLOCK-ENTRY. - start_mark = self.get_mark() - self.forward() - end_mark = self.get_mark() - self.tokens.append(BlockEntryToken(start_mark, end_mark)) - - def fetch_key(self): - - # Block context needs additional checks. - if not self.flow_level: - - # Are we allowed to start a key (not nessesary a simple)? - if not self.allow_simple_key: - raise ScannerError(None, None, - "mapping keys are not allowed here", - self.get_mark()) - - # We may need to add BLOCK-MAPPING-START. - if self.add_indent(self.column): - mark = self.get_mark() - self.tokens.append(BlockMappingStartToken(mark, mark)) - - # Simple keys are allowed after '?' in the block context. - self.allow_simple_key = not self.flow_level - - # Reset possible simple key on the current level. - self.remove_possible_simple_key() - - # Add KEY. - start_mark = self.get_mark() - self.forward() - end_mark = self.get_mark() - self.tokens.append(KeyToken(start_mark, end_mark)) - - def fetch_value(self): - - # Do we determine a simple key? - if self.flow_level in self.possible_simple_keys: - - # Add KEY. - key = self.possible_simple_keys[self.flow_level] - del self.possible_simple_keys[self.flow_level] - self.tokens.insert(key.token_number-self.tokens_taken, - KeyToken(key.mark, key.mark)) - - # If this key starts a new block mapping, we need to add - # BLOCK-MAPPING-START. - if not self.flow_level: - if self.add_indent(key.column): - self.tokens.insert(key.token_number-self.tokens_taken, - BlockMappingStartToken(key.mark, key.mark)) - - # There cannot be two simple keys one after another. - self.allow_simple_key = False - - # It must be a part of a complex key. - else: - - # Block context needs additional checks. - # (Do we really need them? They will be catched by the parser - # anyway.) - if not self.flow_level: - - # We are allowed to start a complex value if and only if - # we can start a simple key. - if not self.allow_simple_key: - raise ScannerError(None, None, - "mapping values are not allowed here", - self.get_mark()) - - # If this value starts a new block mapping, we need to add - # BLOCK-MAPPING-START. It will be detected as an error later by - # the parser. - if not self.flow_level: - if self.add_indent(self.column): - mark = self.get_mark() - self.tokens.append(BlockMappingStartToken(mark, mark)) - - # Simple keys are allowed after ':' in the block context. - self.allow_simple_key = not self.flow_level - - # Reset possible simple key on the current level. - self.remove_possible_simple_key() - - # Add VALUE. - start_mark = self.get_mark() - self.forward() - end_mark = self.get_mark() - self.tokens.append(ValueToken(start_mark, end_mark)) - - def fetch_alias(self): - - # ALIAS could be a simple key. - self.save_possible_simple_key() - - # No simple keys after ALIAS. - self.allow_simple_key = False - - # Scan and add ALIAS. - self.tokens.append(self.scan_anchor(AliasToken)) - - def fetch_anchor(self): - - # ANCHOR could start a simple key. - self.save_possible_simple_key() - - # No simple keys after ANCHOR. - self.allow_simple_key = False - - # Scan and add ANCHOR. - self.tokens.append(self.scan_anchor(AnchorToken)) - - def fetch_tag(self): - - # TAG could start a simple key. - self.save_possible_simple_key() - - # No simple keys after TAG. - self.allow_simple_key = False - - # Scan and add TAG. - self.tokens.append(self.scan_tag()) - - def fetch_literal(self): - self.fetch_block_scalar(style='|') - - def fetch_folded(self): - self.fetch_block_scalar(style='>') - - def fetch_block_scalar(self, style): - - # A simple key may follow a block scalar. - self.allow_simple_key = True - - # Reset possible simple key on the current level. - self.remove_possible_simple_key() - - # Scan and add SCALAR. - self.tokens.append(self.scan_block_scalar(style)) - - def fetch_single(self): - self.fetch_flow_scalar(style='\'') - - def fetch_double(self): - self.fetch_flow_scalar(style='"') - - def fetch_flow_scalar(self, style): - - # A flow scalar could be a simple key. - self.save_possible_simple_key() - - # No simple keys after flow scalars. - self.allow_simple_key = False - - # Scan and add SCALAR. - self.tokens.append(self.scan_flow_scalar(style)) - - def fetch_plain(self): - - # A plain scalar could be a simple key. - self.save_possible_simple_key() - - # No simple keys after plain scalars. But note that `scan_plain` will - # change this flag if the scan is finished at the beginning of the - # line. - self.allow_simple_key = False - - # Scan and add SCALAR. May change `allow_simple_key`. - self.tokens.append(self.scan_plain()) - - # Checkers. - - def check_directive(self): - - # DIRECTIVE: ^ '%' ... - # The '%' indicator is already checked. - if self.column == 0: - return True - - def check_document_start(self): - - # DOCUMENT-START: ^ '---' (' '|'\n') - if self.column == 0: - if self.prefix(3) == '---' \ - and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': - return True - - def check_document_end(self): - - # DOCUMENT-END: ^ '...' (' '|'\n') - if self.column == 0: - if self.prefix(3) == '...' \ - and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': - return True - - def check_block_entry(self): - - # BLOCK-ENTRY: '-' (' '|'\n') - return self.peek(1) in '\0 \t\r\n\x85\u2028\u2029' - - def check_key(self): - - # KEY(flow context): '?' - if self.flow_level: - return True - - # KEY(block context): '?' (' '|'\n') - else: - return self.peek(1) in '\0 \t\r\n\x85\u2028\u2029' - - def check_value(self): - - # VALUE(flow context): ':' - if self.flow_level: - return True - - # VALUE(block context): ':' (' '|'\n') - else: - return self.peek(1) in '\0 \t\r\n\x85\u2028\u2029' - - def check_plain(self): - - # A plain scalar may start with any non-space character except: - # '-', '?', ':', ',', '[', ']', '{', '}', - # '#', '&', '*', '!', '|', '>', '\'', '\"', - # '%', '@', '`'. - # - # It may also start with - # '-', '?', ':' - # if it is followed by a non-space character. - # - # Note that we limit the last rule to the block context (except the - # '-' character) because we want the flow context to be space - # independent. - ch = self.peek() - return ch not in '\0 \t\r\n\x85\u2028\u2029-?:,[]{}#&*!|>\'\"%@`' \ - or (self.peek(1) not in '\0 \t\r\n\x85\u2028\u2029' - and (ch == '-' or (not self.flow_level and ch in '?:'))) - - # Scanners. - - def scan_to_next_token(self): - # We ignore spaces, line breaks and comments. - # If we find a line break in the block context, we set the flag - # `allow_simple_key` on. - # The byte order mark is stripped if it's the first character in the - # stream. We do not yet support BOM inside the stream as the - # specification requires. Any such mark will be considered as a part - # of the document. - # - # TODO: We need to make tab handling rules more sane. A good rule is - # Tabs cannot precede tokens - # BLOCK-SEQUENCE-START, BLOCK-MAPPING-START, BLOCK-END, - # KEY(block), VALUE(block), BLOCK-ENTRY - # So the checking code is - # if : - # self.allow_simple_keys = False - # We also need to add the check for `allow_simple_keys == True` to - # `unwind_indent` before issuing BLOCK-END. - # Scanners for block, flow, and plain scalars need to be modified. - - if self.index == 0 and self.peek() == '\uFEFF': - self.forward() - found = False - while not found: - while self.peek() == ' ': - self.forward() - if self.peek() == '#': - while self.peek() not in '\0\r\n\x85\u2028\u2029': - self.forward() - if self.scan_line_break(): - if not self.flow_level: - self.allow_simple_key = True - else: - found = True - - def scan_directive(self): - # See the specification for details. - start_mark = self.get_mark() - self.forward() - name = self.scan_directive_name(start_mark) - value = None - if name == 'YAML': - value = self.scan_yaml_directive_value(start_mark) - end_mark = self.get_mark() - elif name == 'TAG': - value = self.scan_tag_directive_value(start_mark) - end_mark = self.get_mark() - else: - end_mark = self.get_mark() - while self.peek() not in '\0\r\n\x85\u2028\u2029': - self.forward() - self.scan_directive_ignored_line(start_mark) - return DirectiveToken(name, value, start_mark, end_mark) - - def scan_directive_name(self, start_mark): - # See the specification for details. - length = 0 - ch = self.peek(length) - while '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ - or ch in '-_': - length += 1 - ch = self.peek(length) - if not length: - raise ScannerError("while scanning a directive", start_mark, - "expected alphabetic or numeric character, but found %r" - % ch, self.get_mark()) - value = self.prefix(length) - self.forward(length) - ch = self.peek() - if ch not in '\0 \r\n\x85\u2028\u2029': - raise ScannerError("while scanning a directive", start_mark, - "expected alphabetic or numeric character, but found %r" - % ch, self.get_mark()) - return value - - def scan_yaml_directive_value(self, start_mark): - # See the specification for details. - while self.peek() == ' ': - self.forward() - major = self.scan_yaml_directive_number(start_mark) - if self.peek() != '.': - raise ScannerError("while scanning a directive", start_mark, - "expected a digit or '.', but found %r" % self.peek(), - self.get_mark()) - self.forward() - minor = self.scan_yaml_directive_number(start_mark) - if self.peek() not in '\0 \r\n\x85\u2028\u2029': - raise ScannerError("while scanning a directive", start_mark, - "expected a digit or ' ', but found %r" % self.peek(), - self.get_mark()) - return (major, minor) - - def scan_yaml_directive_number(self, start_mark): - # See the specification for details. - ch = self.peek() - if not ('0' <= ch <= '9'): - raise ScannerError("while scanning a directive", start_mark, - "expected a digit, but found %r" % ch, self.get_mark()) - length = 0 - while '0' <= self.peek(length) <= '9': - length += 1 - value = int(self.prefix(length)) - self.forward(length) - return value - - def scan_tag_directive_value(self, start_mark): - # See the specification for details. - while self.peek() == ' ': - self.forward() - handle = self.scan_tag_directive_handle(start_mark) - while self.peek() == ' ': - self.forward() - prefix = self.scan_tag_directive_prefix(start_mark) - return (handle, prefix) - - def scan_tag_directive_handle(self, start_mark): - # See the specification for details. - value = self.scan_tag_handle('directive', start_mark) - ch = self.peek() - if ch != ' ': - raise ScannerError("while scanning a directive", start_mark, - "expected ' ', but found %r" % ch, self.get_mark()) - return value - - def scan_tag_directive_prefix(self, start_mark): - # See the specification for details. - value = self.scan_tag_uri('directive', start_mark) - ch = self.peek() - if ch not in '\0 \r\n\x85\u2028\u2029': - raise ScannerError("while scanning a directive", start_mark, - "expected ' ', but found %r" % ch, self.get_mark()) - return value - - def scan_directive_ignored_line(self, start_mark): - # See the specification for details. - while self.peek() == ' ': - self.forward() - if self.peek() == '#': - while self.peek() not in '\0\r\n\x85\u2028\u2029': - self.forward() - ch = self.peek() - if ch not in '\0\r\n\x85\u2028\u2029': - raise ScannerError("while scanning a directive", start_mark, - "expected a comment or a line break, but found %r" - % ch, self.get_mark()) - self.scan_line_break() - - def scan_anchor(self, TokenClass): - # The specification does not restrict characters for anchors and - # aliases. This may lead to problems, for instance, the document: - # [ *alias, value ] - # can be interpteted in two ways, as - # [ "value" ] - # and - # [ *alias , "value" ] - # Therefore we restrict aliases to numbers and ASCII letters. - start_mark = self.get_mark() - indicator = self.peek() - if indicator == '*': - name = 'alias' - else: - name = 'anchor' - self.forward() - length = 0 - ch = self.peek(length) - while '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ - or ch in '-_': - length += 1 - ch = self.peek(length) - if not length: - raise ScannerError("while scanning an %s" % name, start_mark, - "expected alphabetic or numeric character, but found %r" - % ch, self.get_mark()) - value = self.prefix(length) - self.forward(length) - ch = self.peek() - if ch not in '\0 \t\r\n\x85\u2028\u2029?:,]}%@`': - raise ScannerError("while scanning an %s" % name, start_mark, - "expected alphabetic or numeric character, but found %r" - % ch, self.get_mark()) - end_mark = self.get_mark() - return TokenClass(value, start_mark, end_mark) - - def scan_tag(self): - # See the specification for details. - start_mark = self.get_mark() - ch = self.peek(1) - if ch == '<': - handle = None - self.forward(2) - suffix = self.scan_tag_uri('tag', start_mark) - if self.peek() != '>': - raise ScannerError("while parsing a tag", start_mark, - "expected '>', but found %r" % self.peek(), - self.get_mark()) - self.forward() - elif ch in '\0 \t\r\n\x85\u2028\u2029': - handle = None - suffix = '!' - self.forward() - else: - length = 1 - use_handle = False - while ch not in '\0 \r\n\x85\u2028\u2029': - if ch == '!': - use_handle = True - break - length += 1 - ch = self.peek(length) - handle = '!' - if use_handle: - handle = self.scan_tag_handle('tag', start_mark) - else: - handle = '!' - self.forward() - suffix = self.scan_tag_uri('tag', start_mark) - ch = self.peek() - if ch not in '\0 \r\n\x85\u2028\u2029': - raise ScannerError("while scanning a tag", start_mark, - "expected ' ', but found %r" % ch, self.get_mark()) - value = (handle, suffix) - end_mark = self.get_mark() - return TagToken(value, start_mark, end_mark) - - def scan_block_scalar(self, style): - # See the specification for details. - - if style == '>': - folded = True - else: - folded = False - - chunks = [] - start_mark = self.get_mark() - - # Scan the header. - self.forward() - chomping, increment = self.scan_block_scalar_indicators(start_mark) - self.scan_block_scalar_ignored_line(start_mark) - - # Determine the indentation level and go to the first non-empty line. - min_indent = self.indent+1 - if min_indent < 1: - min_indent = 1 - if increment is None: - breaks, max_indent, end_mark = self.scan_block_scalar_indentation() - indent = max(min_indent, max_indent) - else: - indent = min_indent+increment-1 - breaks, end_mark = self.scan_block_scalar_breaks(indent) - line_break = '' - - # Scan the inner part of the block scalar. - while self.column == indent and self.peek() != '\0': - chunks.extend(breaks) - leading_non_space = self.peek() not in ' \t' - length = 0 - while self.peek(length) not in '\0\r\n\x85\u2028\u2029': - length += 1 - chunks.append(self.prefix(length)) - self.forward(length) - line_break = self.scan_line_break() - breaks, end_mark = self.scan_block_scalar_breaks(indent) - if self.column == indent and self.peek() != '\0': - - # Unfortunately, folding rules are ambiguous. - # - # This is the folding according to the specification: - - if folded and line_break == '\n' \ - and leading_non_space and self.peek() not in ' \t': - if not breaks: - chunks.append(' ') - else: - chunks.append(line_break) - - # This is Clark Evans's interpretation (also in the spec - # examples): - # - #if folded and line_break == '\n': - # if not breaks: - # if self.peek() not in ' \t': - # chunks.append(' ') - # else: - # chunks.append(line_break) - #else: - # chunks.append(line_break) - else: - break - - # Chomp the tail. - if chomping is not False: - chunks.append(line_break) - if chomping is True: - chunks.extend(breaks) - - # We are done. - return ScalarToken(''.join(chunks), False, start_mark, end_mark, - style) - - def scan_block_scalar_indicators(self, start_mark): - # See the specification for details. - chomping = None - increment = None - ch = self.peek() - if ch in '+-': - if ch == '+': - chomping = True - else: - chomping = False - self.forward() - ch = self.peek() - if ch in '0123456789': - increment = int(ch) - if increment == 0: - raise ScannerError("while scanning a block scalar", start_mark, - "expected indentation indicator in the range 1-9, but found 0", - self.get_mark()) - self.forward() - elif ch in '0123456789': - increment = int(ch) - if increment == 0: - raise ScannerError("while scanning a block scalar", start_mark, - "expected indentation indicator in the range 1-9, but found 0", - self.get_mark()) - self.forward() - ch = self.peek() - if ch in '+-': - if ch == '+': - chomping = True - else: - chomping = False - self.forward() - ch = self.peek() - if ch not in '\0 \r\n\x85\u2028\u2029': - raise ScannerError("while scanning a block scalar", start_mark, - "expected chomping or indentation indicators, but found %r" - % ch, self.get_mark()) - return chomping, increment - - def scan_block_scalar_ignored_line(self, start_mark): - # See the specification for details. - while self.peek() == ' ': - self.forward() - if self.peek() == '#': - while self.peek() not in '\0\r\n\x85\u2028\u2029': - self.forward() - ch = self.peek() - if ch not in '\0\r\n\x85\u2028\u2029': - raise ScannerError("while scanning a block scalar", start_mark, - "expected a comment or a line break, but found %r" % ch, - self.get_mark()) - self.scan_line_break() - - def scan_block_scalar_indentation(self): - # See the specification for details. - chunks = [] - max_indent = 0 - end_mark = self.get_mark() - while self.peek() in ' \r\n\x85\u2028\u2029': - if self.peek() != ' ': - chunks.append(self.scan_line_break()) - end_mark = self.get_mark() - else: - self.forward() - if self.column > max_indent: - max_indent = self.column - return chunks, max_indent, end_mark - - def scan_block_scalar_breaks(self, indent): - # See the specification for details. - chunks = [] - end_mark = self.get_mark() - while self.column < indent and self.peek() == ' ': - self.forward() - while self.peek() in '\r\n\x85\u2028\u2029': - chunks.append(self.scan_line_break()) - end_mark = self.get_mark() - while self.column < indent and self.peek() == ' ': - self.forward() - return chunks, end_mark - - def scan_flow_scalar(self, style): - # See the specification for details. - # Note that we loose indentation rules for quoted scalars. Quoted - # scalars don't need to adhere indentation because " and ' clearly - # mark the beginning and the end of them. Therefore we are less - # restrictive then the specification requires. We only need to check - # that document separators are not included in scalars. - if style == '"': - double = True - else: - double = False - chunks = [] - start_mark = self.get_mark() - quote = self.peek() - self.forward() - chunks.extend(self.scan_flow_scalar_non_spaces(double, start_mark)) - while self.peek() != quote: - chunks.extend(self.scan_flow_scalar_spaces(double, start_mark)) - chunks.extend(self.scan_flow_scalar_non_spaces(double, start_mark)) - self.forward() - end_mark = self.get_mark() - return ScalarToken(''.join(chunks), False, start_mark, end_mark, - style) - - ESCAPE_REPLACEMENTS = { - '0': '\0', - 'a': '\x07', - 'b': '\x08', - 't': '\x09', - '\t': '\x09', - 'n': '\x0A', - 'v': '\x0B', - 'f': '\x0C', - 'r': '\x0D', - 'e': '\x1B', - ' ': '\x20', - '\"': '\"', - '\\': '\\', - 'N': '\x85', - '_': '\xA0', - 'L': '\u2028', - 'P': '\u2029', - } - - ESCAPE_CODES = { - 'x': 2, - 'u': 4, - 'U': 8, - } - - def scan_flow_scalar_non_spaces(self, double, start_mark): - # See the specification for details. - chunks = [] - while True: - length = 0 - while self.peek(length) not in '\'\"\\\0 \t\r\n\x85\u2028\u2029': - length += 1 - if length: - chunks.append(self.prefix(length)) - self.forward(length) - ch = self.peek() - if not double and ch == '\'' and self.peek(1) == '\'': - chunks.append('\'') - self.forward(2) - elif (double and ch == '\'') or (not double and ch in '\"\\'): - chunks.append(ch) - self.forward() - elif double and ch == '\\': - self.forward() - ch = self.peek() - if ch in self.ESCAPE_REPLACEMENTS: - chunks.append(self.ESCAPE_REPLACEMENTS[ch]) - self.forward() - elif ch in self.ESCAPE_CODES: - length = self.ESCAPE_CODES[ch] - self.forward() - for k in range(length): - if self.peek(k) not in '0123456789ABCDEFabcdef': - raise ScannerError("while scanning a double-quoted scalar", start_mark, - "expected escape sequence of %d hexdecimal numbers, but found %r" % - (length, self.peek(k)), self.get_mark()) - code = int(self.prefix(length), 16) - chunks.append(chr(code)) - self.forward(length) - elif ch in '\r\n\x85\u2028\u2029': - self.scan_line_break() - chunks.extend(self.scan_flow_scalar_breaks(double, start_mark)) - else: - raise ScannerError("while scanning a double-quoted scalar", start_mark, - "found unknown escape character %r" % ch, self.get_mark()) - else: - return chunks - - def scan_flow_scalar_spaces(self, double, start_mark): - # See the specification for details. - chunks = [] - length = 0 - while self.peek(length) in ' \t': - length += 1 - whitespaces = self.prefix(length) - self.forward(length) - ch = self.peek() - if ch == '\0': - raise ScannerError("while scanning a quoted scalar", start_mark, - "found unexpected end of stream", self.get_mark()) - elif ch in '\r\n\x85\u2028\u2029': - line_break = self.scan_line_break() - breaks = self.scan_flow_scalar_breaks(double, start_mark) - if line_break != '\n': - chunks.append(line_break) - elif not breaks: - chunks.append(' ') - chunks.extend(breaks) - else: - chunks.append(whitespaces) - return chunks - - def scan_flow_scalar_breaks(self, double, start_mark): - # See the specification for details. - chunks = [] - while True: - # Instead of checking indentation, we check for document - # separators. - prefix = self.prefix(3) - if (prefix == '---' or prefix == '...') \ - and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': - raise ScannerError("while scanning a quoted scalar", start_mark, - "found unexpected document separator", self.get_mark()) - while self.peek() in ' \t': - self.forward() - if self.peek() in '\r\n\x85\u2028\u2029': - chunks.append(self.scan_line_break()) - else: - return chunks - - def scan_plain(self): - # See the specification for details. - # We add an additional restriction for the flow context: - # plain scalars in the flow context cannot contain ',', ':' and '?'. - # We also keep track of the `allow_simple_key` flag here. - # Indentation rules are loosed for the flow context. - chunks = [] - start_mark = self.get_mark() - end_mark = start_mark - indent = self.indent+1 - # We allow zero indentation for scalars, but then we need to check for - # document separators at the beginning of the line. - #if indent == 0: - # indent = 1 - spaces = [] - while True: - length = 0 - if self.peek() == '#': - break - while True: - ch = self.peek(length) - if ch in '\0 \t\r\n\x85\u2028\u2029' \ - or (not self.flow_level and ch == ':' and - self.peek(length+1) in '\0 \t\r\n\x85\u2028\u2029') \ - or (self.flow_level and ch in ',:?[]{}'): - break - length += 1 - # It's not clear what we should do with ':' in the flow context. - if (self.flow_level and ch == ':' - and self.peek(length+1) not in '\0 \t\r\n\x85\u2028\u2029,[]{}'): - self.forward(length) - raise ScannerError("while scanning a plain scalar", start_mark, - "found unexpected ':'", self.get_mark(), - "Please check http://pyyaml.org/wiki/YAMLColonInFlowContext for details.") - if length == 0: - break - self.allow_simple_key = False - chunks.extend(spaces) - chunks.append(self.prefix(length)) - self.forward(length) - end_mark = self.get_mark() - spaces = self.scan_plain_spaces(indent, start_mark) - if not spaces or self.peek() == '#' \ - or (not self.flow_level and self.column < indent): - break - return ScalarToken(''.join(chunks), True, start_mark, end_mark) - - def scan_plain_spaces(self, indent, start_mark): - # See the specification for details. - # The specification is really confusing about tabs in plain scalars. - # We just forbid them completely. Do not use tabs in YAML! - chunks = [] - length = 0 - while self.peek(length) in ' ': - length += 1 - whitespaces = self.prefix(length) - self.forward(length) - ch = self.peek() - if ch in '\r\n\x85\u2028\u2029': - line_break = self.scan_line_break() - self.allow_simple_key = True - prefix = self.prefix(3) - if (prefix == '---' or prefix == '...') \ - and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': - return - breaks = [] - while self.peek() in ' \r\n\x85\u2028\u2029': - if self.peek() == ' ': - self.forward() - else: - breaks.append(self.scan_line_break()) - prefix = self.prefix(3) - if (prefix == '---' or prefix == '...') \ - and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': - return - if line_break != '\n': - chunks.append(line_break) - elif not breaks: - chunks.append(' ') - chunks.extend(breaks) - elif whitespaces: - chunks.append(whitespaces) - return chunks - - def scan_tag_handle(self, name, start_mark): - # See the specification for details. - # For some strange reasons, the specification does not allow '_' in - # tag handles. I have allowed it anyway. - ch = self.peek() - if ch != '!': - raise ScannerError("while scanning a %s" % name, start_mark, - "expected '!', but found %r" % ch, self.get_mark()) - length = 1 - ch = self.peek(length) - if ch != ' ': - while '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ - or ch in '-_': - length += 1 - ch = self.peek(length) - if ch != '!': - self.forward(length) - raise ScannerError("while scanning a %s" % name, start_mark, - "expected '!', but found %r" % ch, self.get_mark()) - length += 1 - value = self.prefix(length) - self.forward(length) - return value - - def scan_tag_uri(self, name, start_mark): - # See the specification for details. - # Note: we do not check if URI is well-formed. - chunks = [] - length = 0 - ch = self.peek(length) - while '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ - or ch in '-;/?:@&=+$,_.!~*\'()[]%': - if ch == '%': - chunks.append(self.prefix(length)) - self.forward(length) - length = 0 - chunks.append(self.scan_uri_escapes(name, start_mark)) - else: - length += 1 - ch = self.peek(length) - if length: - chunks.append(self.prefix(length)) - self.forward(length) - length = 0 - if not chunks: - raise ScannerError("while parsing a %s" % name, start_mark, - "expected URI, but found %r" % ch, self.get_mark()) - return ''.join(chunks) - - def scan_uri_escapes(self, name, start_mark): - # See the specification for details. - codes = [] - mark = self.get_mark() - while self.peek() == '%': - self.forward() - for k in range(2): - if self.peek(k) not in '0123456789ABCDEFabcdef': - raise ScannerError("while scanning a %s" % name, start_mark, - "expected URI escape sequence of 2 hexdecimal numbers, but found %r" - % self.peek(k), self.get_mark()) - codes.append(int(self.prefix(2), 16)) - self.forward(2) - try: - value = bytes(codes).decode('utf-8') - except UnicodeDecodeError as exc: - raise ScannerError("while scanning a %s" % name, start_mark, str(exc), mark) - return value - - def scan_line_break(self): - # Transforms: - # '\r\n' : '\n' - # '\r' : '\n' - # '\n' : '\n' - # '\x85' : '\n' - # '\u2028' : '\u2028' - # '\u2029 : '\u2029' - # default : '' - ch = self.peek() - if ch in '\r\n\x85': - if self.prefix(2) == '\r\n': - self.forward(2) - else: - self.forward() - return '\n' - elif ch in '\u2028\u2029': - self.forward() - return ch - return '' - -#try: -# import psyco -# psyco.bind(Scanner) -#except ImportError: -# pass - + +# Scanner produces tokens of the following types: +# STREAM-START +# STREAM-END +# DIRECTIVE(name, value) +# DOCUMENT-START +# DOCUMENT-END +# BLOCK-SEQUENCE-START +# BLOCK-MAPPING-START +# BLOCK-END +# FLOW-SEQUENCE-START +# FLOW-MAPPING-START +# FLOW-SEQUENCE-END +# FLOW-MAPPING-END +# BLOCK-ENTRY +# FLOW-ENTRY +# KEY +# VALUE +# ALIAS(value) +# ANCHOR(value) +# TAG(value) +# SCALAR(value, plain, style) +# +# Read comments in the Scanner code for more details. +# + +__all__ = ['Scanner', 'ScannerError'] + +from .error import MarkedYAMLError +from .tokens import * + +class ScannerError(MarkedYAMLError): + pass + +class SimpleKey: + # See below simple keys treatment. + + def __init__(self, token_number, required, index, line, column, mark): + self.token_number = token_number + self.required = required + self.index = index + self.line = line + self.column = column + self.mark = mark + +class Scanner: + + def __init__(self): + """Initialize the scanner.""" + # It is assumed that Scanner and Reader will have a common descendant. + # Reader do the dirty work of checking for BOM and converting the + # input data to Unicode. It also adds NUL to the end. + # + # Reader supports the following methods + # self.peek(i=0) # peek the next i-th character + # self.prefix(l=1) # peek the next l characters + # self.forward(l=1) # read the next l characters and move the pointer. + + # Had we reached the end of the stream? + self.done = False + + # The number of unclosed '{' and '['. `flow_level == 0` means block + # context. + self.flow_level = 0 + + # List of processed tokens that are not yet emitted. + self.tokens = [] + + # Add the STREAM-START token. + self.fetch_stream_start() + + # Number of tokens that were emitted through the `get_token` method. + self.tokens_taken = 0 + + # The current indentation level. + self.indent = -1 + + # Past indentation levels. + self.indents = [] + + # Variables related to simple keys treatment. + + # A simple key is a key that is not denoted by the '?' indicator. + # Example of simple keys: + # --- + # block simple key: value + # ? not a simple key: + # : { flow simple key: value } + # We emit the KEY token before all keys, so when we find a potential + # simple key, we try to locate the corresponding ':' indicator. + # Simple keys should be limited to a single line and 1024 characters. + + # Can a simple key start at the current position? A simple key may + # start: + # - at the beginning of the line, not counting indentation spaces + # (in block context), + # - after '{', '[', ',' (in the flow context), + # - after '?', ':', '-' (in the block context). + # In the block context, this flag also signifies if a block collection + # may start at the current position. + self.allow_simple_key = True + + # Keep track of possible simple keys. This is a dictionary. The key + # is `flow_level`; there can be no more that one possible simple key + # for each level. The value is a SimpleKey record: + # (token_number, required, index, line, column, mark) + # A simple key may start with ALIAS, ANCHOR, TAG, SCALAR(flow), + # '[', or '{' tokens. + self.possible_simple_keys = {} + + # Public methods. + + def check_token(self, *choices): + # Check if the next token is one of the given types. + while self.need_more_tokens(): + self.fetch_more_tokens() + if self.tokens: + if not choices: + return True + for choice in choices: + if isinstance(self.tokens[0], choice): + return True + return False + + def peek_token(self): + # Return the next token, but do not delete if from the queue. + while self.need_more_tokens(): + self.fetch_more_tokens() + if self.tokens: + return self.tokens[0] + + def get_token(self): + # Return the next token. + while self.need_more_tokens(): + self.fetch_more_tokens() + if self.tokens: + self.tokens_taken += 1 + return self.tokens.pop(0) + + # Private methods. + + def need_more_tokens(self): + if self.done: + return False + if not self.tokens: + return True + # The current token may be a potential simple key, so we + # need to look further. + self.stale_possible_simple_keys() + if self.next_possible_simple_key() == self.tokens_taken: + return True + + def fetch_more_tokens(self): + + # Eat whitespaces and comments until we reach the next token. + self.scan_to_next_token() + + # Remove obsolete possible simple keys. + self.stale_possible_simple_keys() + + # Compare the current indentation and column. It may add some tokens + # and decrease the current indentation level. + self.unwind_indent(self.column) + + # Peek the next character. + ch = self.peek() + + # Is it the end of stream? + if ch == '\0': + return self.fetch_stream_end() + + # Is it a directive? + if ch == '%' and self.check_directive(): + return self.fetch_directive() + + # Is it the document start? + if ch == '-' and self.check_document_start(): + return self.fetch_document_start() + + # Is it the document end? + if ch == '.' and self.check_document_end(): + return self.fetch_document_end() + + # TODO: support for BOM within a stream. + #if ch == '\uFEFF': + # return self.fetch_bom() <-- issue BOMToken + + # Note: the order of the following checks is NOT significant. + + # Is it the flow sequence start indicator? + if ch == '[': + return self.fetch_flow_sequence_start() + + # Is it the flow mapping start indicator? + if ch == '{': + return self.fetch_flow_mapping_start() + + # Is it the flow sequence end indicator? + if ch == ']': + return self.fetch_flow_sequence_end() + + # Is it the flow mapping end indicator? + if ch == '}': + return self.fetch_flow_mapping_end() + + # Is it the flow entry indicator? + if ch == ',': + return self.fetch_flow_entry() + + # Is it the block entry indicator? + if ch == '-' and self.check_block_entry(): + return self.fetch_block_entry() + + # Is it the key indicator? + if ch == '?' and self.check_key(): + return self.fetch_key() + + # Is it the value indicator? + if ch == ':' and self.check_value(): + return self.fetch_value() + + # Is it an alias? + if ch == '*': + return self.fetch_alias() + + # Is it an anchor? + if ch == '&': + return self.fetch_anchor() + + # Is it a tag? + if ch == '!': + return self.fetch_tag() + + # Is it a literal scalar? + if ch == '|' and not self.flow_level: + return self.fetch_literal() + + # Is it a folded scalar? + if ch == '>' and not self.flow_level: + return self.fetch_folded() + + # Is it a single quoted scalar? + if ch == '\'': + return self.fetch_single() + + # Is it a double quoted scalar? + if ch == '\"': + return self.fetch_double() + + # It must be a plain scalar then. + if self.check_plain(): + return self.fetch_plain() + + # No? It's an error. Let's produce a nice error message. + raise ScannerError("while scanning for the next token", None, + "found character %r that cannot start any token" % ch, + self.get_mark()) + + # Simple keys treatment. + + def next_possible_simple_key(self): + # Return the number of the nearest possible simple key. Actually we + # don't need to loop through the whole dictionary. We may replace it + # with the following code: + # if not self.possible_simple_keys: + # return None + # return self.possible_simple_keys[ + # min(self.possible_simple_keys.keys())].token_number + min_token_number = None + for level in self.possible_simple_keys: + key = self.possible_simple_keys[level] + if min_token_number is None or key.token_number < min_token_number: + min_token_number = key.token_number + return min_token_number + + def stale_possible_simple_keys(self): + # Remove entries that are no longer possible simple keys. According to + # the YAML specification, simple keys + # - should be limited to a single line, + # - should be no longer than 1024 characters. + # Disabling this procedure will allow simple keys of any length and + # height (may cause problems if indentation is broken though). + for level in list(self.possible_simple_keys): + key = self.possible_simple_keys[level] + if key.line != self.line \ + or self.index-key.index > 1024: + if key.required: + raise ScannerError("while scanning a simple key", key.mark, + "could not found expected ':'", self.get_mark()) + del self.possible_simple_keys[level] + + def save_possible_simple_key(self): + # The next token may start a simple key. We check if it's possible + # and save its position. This function is called for + # ALIAS, ANCHOR, TAG, SCALAR(flow), '[', and '{'. + + # Check if a simple key is required at the current position. + required = not self.flow_level and self.indent == self.column + + # A simple key is required only if it is the first token in the current + # line. Therefore it is always allowed. + assert self.allow_simple_key or not required + + # The next token might be a simple key. Let's save it's number and + # position. + if self.allow_simple_key: + self.remove_possible_simple_key() + token_number = self.tokens_taken+len(self.tokens) + key = SimpleKey(token_number, required, + self.index, self.line, self.column, self.get_mark()) + self.possible_simple_keys[self.flow_level] = key + + def remove_possible_simple_key(self): + # Remove the saved possible key position at the current flow level. + if self.flow_level in self.possible_simple_keys: + key = self.possible_simple_keys[self.flow_level] + + if key.required: + raise ScannerError("while scanning a simple key", key.mark, + "could not found expected ':'", self.get_mark()) + + del self.possible_simple_keys[self.flow_level] + + # Indentation functions. + + def unwind_indent(self, column): + + ## In flow context, tokens should respect indentation. + ## Actually the condition should be `self.indent >= column` according to + ## the spec. But this condition will prohibit intuitively correct + ## constructions such as + ## key : { + ## } + #if self.flow_level and self.indent > column: + # raise ScannerError(None, None, + # "invalid intendation or unclosed '[' or '{'", + # self.get_mark()) + + # In the flow context, indentation is ignored. We make the scanner less + # restrictive then specification requires. + if self.flow_level: + return + + # In block context, we may need to issue the BLOCK-END tokens. + while self.indent > column: + mark = self.get_mark() + self.indent = self.indents.pop() + self.tokens.append(BlockEndToken(mark, mark)) + + def add_indent(self, column): + # Check if we need to increase indentation. + if self.indent < column: + self.indents.append(self.indent) + self.indent = column + return True + return False + + # Fetchers. + + def fetch_stream_start(self): + # We always add STREAM-START as the first token and STREAM-END as the + # last token. + + # Read the token. + mark = self.get_mark() + + # Add STREAM-START. + self.tokens.append(StreamStartToken(mark, mark, + encoding=self.encoding)) + + + def fetch_stream_end(self): + + # Set the current intendation to -1. + self.unwind_indent(-1) + + # Reset simple keys. + self.remove_possible_simple_key() + self.allow_simple_key = False + self.possible_simple_keys = {} + + # Read the token. + mark = self.get_mark() + + # Add STREAM-END. + self.tokens.append(StreamEndToken(mark, mark)) + + # The steam is finished. + self.done = True + + def fetch_directive(self): + + # Set the current intendation to -1. + self.unwind_indent(-1) + + # Reset simple keys. + self.remove_possible_simple_key() + self.allow_simple_key = False + + # Scan and add DIRECTIVE. + self.tokens.append(self.scan_directive()) + + def fetch_document_start(self): + self.fetch_document_indicator(DocumentStartToken) + + def fetch_document_end(self): + self.fetch_document_indicator(DocumentEndToken) + + def fetch_document_indicator(self, TokenClass): + + # Set the current intendation to -1. + self.unwind_indent(-1) + + # Reset simple keys. Note that there could not be a block collection + # after '---'. + self.remove_possible_simple_key() + self.allow_simple_key = False + + # Add DOCUMENT-START or DOCUMENT-END. + start_mark = self.get_mark() + self.forward(3) + end_mark = self.get_mark() + self.tokens.append(TokenClass(start_mark, end_mark)) + + def fetch_flow_sequence_start(self): + self.fetch_flow_collection_start(FlowSequenceStartToken) + + def fetch_flow_mapping_start(self): + self.fetch_flow_collection_start(FlowMappingStartToken) + + def fetch_flow_collection_start(self, TokenClass): + + # '[' and '{' may start a simple key. + self.save_possible_simple_key() + + # Increase the flow level. + self.flow_level += 1 + + # Simple keys are allowed after '[' and '{'. + self.allow_simple_key = True + + # Add FLOW-SEQUENCE-START or FLOW-MAPPING-START. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(TokenClass(start_mark, end_mark)) + + def fetch_flow_sequence_end(self): + self.fetch_flow_collection_end(FlowSequenceEndToken) + + def fetch_flow_mapping_end(self): + self.fetch_flow_collection_end(FlowMappingEndToken) + + def fetch_flow_collection_end(self, TokenClass): + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Decrease the flow level. + self.flow_level -= 1 + + # No simple keys after ']' or '}'. + self.allow_simple_key = False + + # Add FLOW-SEQUENCE-END or FLOW-MAPPING-END. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(TokenClass(start_mark, end_mark)) + + def fetch_flow_entry(self): + + # Simple keys are allowed after ','. + self.allow_simple_key = True + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Add FLOW-ENTRY. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(FlowEntryToken(start_mark, end_mark)) + + def fetch_block_entry(self): + + # Block context needs additional checks. + if not self.flow_level: + + # Are we allowed to start a new entry? + if not self.allow_simple_key: + raise ScannerError(None, None, + "sequence entries are not allowed here", + self.get_mark()) + + # We may need to add BLOCK-SEQUENCE-START. + if self.add_indent(self.column): + mark = self.get_mark() + self.tokens.append(BlockSequenceStartToken(mark, mark)) + + # It's an error for the block entry to occur in the flow context, + # but we let the parser detect this. + else: + pass + + # Simple keys are allowed after '-'. + self.allow_simple_key = True + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Add BLOCK-ENTRY. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(BlockEntryToken(start_mark, end_mark)) + + def fetch_key(self): + + # Block context needs additional checks. + if not self.flow_level: + + # Are we allowed to start a key (not nessesary a simple)? + if not self.allow_simple_key: + raise ScannerError(None, None, + "mapping keys are not allowed here", + self.get_mark()) + + # We may need to add BLOCK-MAPPING-START. + if self.add_indent(self.column): + mark = self.get_mark() + self.tokens.append(BlockMappingStartToken(mark, mark)) + + # Simple keys are allowed after '?' in the block context. + self.allow_simple_key = not self.flow_level + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Add KEY. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(KeyToken(start_mark, end_mark)) + + def fetch_value(self): + + # Do we determine a simple key? + if self.flow_level in self.possible_simple_keys: + + # Add KEY. + key = self.possible_simple_keys[self.flow_level] + del self.possible_simple_keys[self.flow_level] + self.tokens.insert(key.token_number-self.tokens_taken, + KeyToken(key.mark, key.mark)) + + # If this key starts a new block mapping, we need to add + # BLOCK-MAPPING-START. + if not self.flow_level: + if self.add_indent(key.column): + self.tokens.insert(key.token_number-self.tokens_taken, + BlockMappingStartToken(key.mark, key.mark)) + + # There cannot be two simple keys one after another. + self.allow_simple_key = False + + # It must be a part of a complex key. + else: + + # Block context needs additional checks. + # (Do we really need them? They will be catched by the parser + # anyway.) + if not self.flow_level: + + # We are allowed to start a complex value if and only if + # we can start a simple key. + if not self.allow_simple_key: + raise ScannerError(None, None, + "mapping values are not allowed here", + self.get_mark()) + + # If this value starts a new block mapping, we need to add + # BLOCK-MAPPING-START. It will be detected as an error later by + # the parser. + if not self.flow_level: + if self.add_indent(self.column): + mark = self.get_mark() + self.tokens.append(BlockMappingStartToken(mark, mark)) + + # Simple keys are allowed after ':' in the block context. + self.allow_simple_key = not self.flow_level + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Add VALUE. + start_mark = self.get_mark() + self.forward() + end_mark = self.get_mark() + self.tokens.append(ValueToken(start_mark, end_mark)) + + def fetch_alias(self): + + # ALIAS could be a simple key. + self.save_possible_simple_key() + + # No simple keys after ALIAS. + self.allow_simple_key = False + + # Scan and add ALIAS. + self.tokens.append(self.scan_anchor(AliasToken)) + + def fetch_anchor(self): + + # ANCHOR could start a simple key. + self.save_possible_simple_key() + + # No simple keys after ANCHOR. + self.allow_simple_key = False + + # Scan and add ANCHOR. + self.tokens.append(self.scan_anchor(AnchorToken)) + + def fetch_tag(self): + + # TAG could start a simple key. + self.save_possible_simple_key() + + # No simple keys after TAG. + self.allow_simple_key = False + + # Scan and add TAG. + self.tokens.append(self.scan_tag()) + + def fetch_literal(self): + self.fetch_block_scalar(style='|') + + def fetch_folded(self): + self.fetch_block_scalar(style='>') + + def fetch_block_scalar(self, style): + + # A simple key may follow a block scalar. + self.allow_simple_key = True + + # Reset possible simple key on the current level. + self.remove_possible_simple_key() + + # Scan and add SCALAR. + self.tokens.append(self.scan_block_scalar(style)) + + def fetch_single(self): + self.fetch_flow_scalar(style='\'') + + def fetch_double(self): + self.fetch_flow_scalar(style='"') + + def fetch_flow_scalar(self, style): + + # A flow scalar could be a simple key. + self.save_possible_simple_key() + + # No simple keys after flow scalars. + self.allow_simple_key = False + + # Scan and add SCALAR. + self.tokens.append(self.scan_flow_scalar(style)) + + def fetch_plain(self): + + # A plain scalar could be a simple key. + self.save_possible_simple_key() + + # No simple keys after plain scalars. But note that `scan_plain` will + # change this flag if the scan is finished at the beginning of the + # line. + self.allow_simple_key = False + + # Scan and add SCALAR. May change `allow_simple_key`. + self.tokens.append(self.scan_plain()) + + # Checkers. + + def check_directive(self): + + # DIRECTIVE: ^ '%' ... + # The '%' indicator is already checked. + if self.column == 0: + return True + + def check_document_start(self): + + # DOCUMENT-START: ^ '---' (' '|'\n') + if self.column == 0: + if self.prefix(3) == '---' \ + and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': + return True + + def check_document_end(self): + + # DOCUMENT-END: ^ '...' (' '|'\n') + if self.column == 0: + if self.prefix(3) == '...' \ + and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': + return True + + def check_block_entry(self): + + # BLOCK-ENTRY: '-' (' '|'\n') + return self.peek(1) in '\0 \t\r\n\x85\u2028\u2029' + + def check_key(self): + + # KEY(flow context): '?' + if self.flow_level: + return True + + # KEY(block context): '?' (' '|'\n') + else: + return self.peek(1) in '\0 \t\r\n\x85\u2028\u2029' + + def check_value(self): + + # VALUE(flow context): ':' + if self.flow_level: + return True + + # VALUE(block context): ':' (' '|'\n') + else: + return self.peek(1) in '\0 \t\r\n\x85\u2028\u2029' + + def check_plain(self): + + # A plain scalar may start with any non-space character except: + # '-', '?', ':', ',', '[', ']', '{', '}', + # '#', '&', '*', '!', '|', '>', '\'', '\"', + # '%', '@', '`'. + # + # It may also start with + # '-', '?', ':' + # if it is followed by a non-space character. + # + # Note that we limit the last rule to the block context (except the + # '-' character) because we want the flow context to be space + # independent. + ch = self.peek() + return ch not in '\0 \t\r\n\x85\u2028\u2029-?:,[]{}#&*!|>\'\"%@`' \ + or (self.peek(1) not in '\0 \t\r\n\x85\u2028\u2029' + and (ch == '-' or (not self.flow_level and ch in '?:'))) + + # Scanners. + + def scan_to_next_token(self): + # We ignore spaces, line breaks and comments. + # If we find a line break in the block context, we set the flag + # `allow_simple_key` on. + # The byte order mark is stripped if it's the first character in the + # stream. We do not yet support BOM inside the stream as the + # specification requires. Any such mark will be considered as a part + # of the document. + # + # TODO: We need to make tab handling rules more sane. A good rule is + # Tabs cannot precede tokens + # BLOCK-SEQUENCE-START, BLOCK-MAPPING-START, BLOCK-END, + # KEY(block), VALUE(block), BLOCK-ENTRY + # So the checking code is + # if : + # self.allow_simple_keys = False + # We also need to add the check for `allow_simple_keys == True` to + # `unwind_indent` before issuing BLOCK-END. + # Scanners for block, flow, and plain scalars need to be modified. + + if self.index == 0 and self.peek() == '\uFEFF': + self.forward() + found = False + while not found: + while self.peek() == ' ': + self.forward() + if self.peek() == '#': + while self.peek() not in '\0\r\n\x85\u2028\u2029': + self.forward() + if self.scan_line_break(): + if not self.flow_level: + self.allow_simple_key = True + else: + found = True + + def scan_directive(self): + # See the specification for details. + start_mark = self.get_mark() + self.forward() + name = self.scan_directive_name(start_mark) + value = None + if name == 'YAML': + value = self.scan_yaml_directive_value(start_mark) + end_mark = self.get_mark() + elif name == 'TAG': + value = self.scan_tag_directive_value(start_mark) + end_mark = self.get_mark() + else: + end_mark = self.get_mark() + while self.peek() not in '\0\r\n\x85\u2028\u2029': + self.forward() + self.scan_directive_ignored_line(start_mark) + return DirectiveToken(name, value, start_mark, end_mark) + + def scan_directive_name(self, start_mark): + # See the specification for details. + length = 0 + ch = self.peek(length) + while '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-_': + length += 1 + ch = self.peek(length) + if not length: + raise ScannerError("while scanning a directive", start_mark, + "expected alphabetic or numeric character, but found %r" + % ch, self.get_mark()) + value = self.prefix(length) + self.forward(length) + ch = self.peek() + if ch not in '\0 \r\n\x85\u2028\u2029': + raise ScannerError("while scanning a directive", start_mark, + "expected alphabetic or numeric character, but found %r" + % ch, self.get_mark()) + return value + + def scan_yaml_directive_value(self, start_mark): + # See the specification for details. + while self.peek() == ' ': + self.forward() + major = self.scan_yaml_directive_number(start_mark) + if self.peek() != '.': + raise ScannerError("while scanning a directive", start_mark, + "expected a digit or '.', but found %r" % self.peek(), + self.get_mark()) + self.forward() + minor = self.scan_yaml_directive_number(start_mark) + if self.peek() not in '\0 \r\n\x85\u2028\u2029': + raise ScannerError("while scanning a directive", start_mark, + "expected a digit or ' ', but found %r" % self.peek(), + self.get_mark()) + return (major, minor) + + def scan_yaml_directive_number(self, start_mark): + # See the specification for details. + ch = self.peek() + if not ('0' <= ch <= '9'): + raise ScannerError("while scanning a directive", start_mark, + "expected a digit, but found %r" % ch, self.get_mark()) + length = 0 + while '0' <= self.peek(length) <= '9': + length += 1 + value = int(self.prefix(length)) + self.forward(length) + return value + + def scan_tag_directive_value(self, start_mark): + # See the specification for details. + while self.peek() == ' ': + self.forward() + handle = self.scan_tag_directive_handle(start_mark) + while self.peek() == ' ': + self.forward() + prefix = self.scan_tag_directive_prefix(start_mark) + return (handle, prefix) + + def scan_tag_directive_handle(self, start_mark): + # See the specification for details. + value = self.scan_tag_handle('directive', start_mark) + ch = self.peek() + if ch != ' ': + raise ScannerError("while scanning a directive", start_mark, + "expected ' ', but found %r" % ch, self.get_mark()) + return value + + def scan_tag_directive_prefix(self, start_mark): + # See the specification for details. + value = self.scan_tag_uri('directive', start_mark) + ch = self.peek() + if ch not in '\0 \r\n\x85\u2028\u2029': + raise ScannerError("while scanning a directive", start_mark, + "expected ' ', but found %r" % ch, self.get_mark()) + return value + + def scan_directive_ignored_line(self, start_mark): + # See the specification for details. + while self.peek() == ' ': + self.forward() + if self.peek() == '#': + while self.peek() not in '\0\r\n\x85\u2028\u2029': + self.forward() + ch = self.peek() + if ch not in '\0\r\n\x85\u2028\u2029': + raise ScannerError("while scanning a directive", start_mark, + "expected a comment or a line break, but found %r" + % ch, self.get_mark()) + self.scan_line_break() + + def scan_anchor(self, TokenClass): + # The specification does not restrict characters for anchors and + # aliases. This may lead to problems, for instance, the document: + # [ *alias, value ] + # can be interpteted in two ways, as + # [ "value" ] + # and + # [ *alias , "value" ] + # Therefore we restrict aliases to numbers and ASCII letters. + start_mark = self.get_mark() + indicator = self.peek() + if indicator == '*': + name = 'alias' + else: + name = 'anchor' + self.forward() + length = 0 + ch = self.peek(length) + while '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-_': + length += 1 + ch = self.peek(length) + if not length: + raise ScannerError("while scanning an %s" % name, start_mark, + "expected alphabetic or numeric character, but found %r" + % ch, self.get_mark()) + value = self.prefix(length) + self.forward(length) + ch = self.peek() + if ch not in '\0 \t\r\n\x85\u2028\u2029?:,]}%@`': + raise ScannerError("while scanning an %s" % name, start_mark, + "expected alphabetic or numeric character, but found %r" + % ch, self.get_mark()) + end_mark = self.get_mark() + return TokenClass(value, start_mark, end_mark) + + def scan_tag(self): + # See the specification for details. + start_mark = self.get_mark() + ch = self.peek(1) + if ch == '<': + handle = None + self.forward(2) + suffix = self.scan_tag_uri('tag', start_mark) + if self.peek() != '>': + raise ScannerError("while parsing a tag", start_mark, + "expected '>', but found %r" % self.peek(), + self.get_mark()) + self.forward() + elif ch in '\0 \t\r\n\x85\u2028\u2029': + handle = None + suffix = '!' + self.forward() + else: + length = 1 + use_handle = False + while ch not in '\0 \r\n\x85\u2028\u2029': + if ch == '!': + use_handle = True + break + length += 1 + ch = self.peek(length) + handle = '!' + if use_handle: + handle = self.scan_tag_handle('tag', start_mark) + else: + handle = '!' + self.forward() + suffix = self.scan_tag_uri('tag', start_mark) + ch = self.peek() + if ch not in '\0 \r\n\x85\u2028\u2029': + raise ScannerError("while scanning a tag", start_mark, + "expected ' ', but found %r" % ch, self.get_mark()) + value = (handle, suffix) + end_mark = self.get_mark() + return TagToken(value, start_mark, end_mark) + + def scan_block_scalar(self, style): + # See the specification for details. + + if style == '>': + folded = True + else: + folded = False + + chunks = [] + start_mark = self.get_mark() + + # Scan the header. + self.forward() + chomping, increment = self.scan_block_scalar_indicators(start_mark) + self.scan_block_scalar_ignored_line(start_mark) + + # Determine the indentation level and go to the first non-empty line. + min_indent = self.indent+1 + if min_indent < 1: + min_indent = 1 + if increment is None: + breaks, max_indent, end_mark = self.scan_block_scalar_indentation() + indent = max(min_indent, max_indent) + else: + indent = min_indent+increment-1 + breaks, end_mark = self.scan_block_scalar_breaks(indent) + line_break = '' + + # Scan the inner part of the block scalar. + while self.column == indent and self.peek() != '\0': + chunks.extend(breaks) + leading_non_space = self.peek() not in ' \t' + length = 0 + while self.peek(length) not in '\0\r\n\x85\u2028\u2029': + length += 1 + chunks.append(self.prefix(length)) + self.forward(length) + line_break = self.scan_line_break() + breaks, end_mark = self.scan_block_scalar_breaks(indent) + if self.column == indent and self.peek() != '\0': + + # Unfortunately, folding rules are ambiguous. + # + # This is the folding according to the specification: + + if folded and line_break == '\n' \ + and leading_non_space and self.peek() not in ' \t': + if not breaks: + chunks.append(' ') + else: + chunks.append(line_break) + + # This is Clark Evans's interpretation (also in the spec + # examples): + # + #if folded and line_break == '\n': + # if not breaks: + # if self.peek() not in ' \t': + # chunks.append(' ') + # else: + # chunks.append(line_break) + #else: + # chunks.append(line_break) + else: + break + + # Chomp the tail. + if chomping is not False: + chunks.append(line_break) + if chomping is True: + chunks.extend(breaks) + + # We are done. + return ScalarToken(''.join(chunks), False, start_mark, end_mark, + style) + + def scan_block_scalar_indicators(self, start_mark): + # See the specification for details. + chomping = None + increment = None + ch = self.peek() + if ch in '+-': + if ch == '+': + chomping = True + else: + chomping = False + self.forward() + ch = self.peek() + if ch in '0123456789': + increment = int(ch) + if increment == 0: + raise ScannerError("while scanning a block scalar", start_mark, + "expected indentation indicator in the range 1-9, but found 0", + self.get_mark()) + self.forward() + elif ch in '0123456789': + increment = int(ch) + if increment == 0: + raise ScannerError("while scanning a block scalar", start_mark, + "expected indentation indicator in the range 1-9, but found 0", + self.get_mark()) + self.forward() + ch = self.peek() + if ch in '+-': + if ch == '+': + chomping = True + else: + chomping = False + self.forward() + ch = self.peek() + if ch not in '\0 \r\n\x85\u2028\u2029': + raise ScannerError("while scanning a block scalar", start_mark, + "expected chomping or indentation indicators, but found %r" + % ch, self.get_mark()) + return chomping, increment + + def scan_block_scalar_ignored_line(self, start_mark): + # See the specification for details. + while self.peek() == ' ': + self.forward() + if self.peek() == '#': + while self.peek() not in '\0\r\n\x85\u2028\u2029': + self.forward() + ch = self.peek() + if ch not in '\0\r\n\x85\u2028\u2029': + raise ScannerError("while scanning a block scalar", start_mark, + "expected a comment or a line break, but found %r" % ch, + self.get_mark()) + self.scan_line_break() + + def scan_block_scalar_indentation(self): + # See the specification for details. + chunks = [] + max_indent = 0 + end_mark = self.get_mark() + while self.peek() in ' \r\n\x85\u2028\u2029': + if self.peek() != ' ': + chunks.append(self.scan_line_break()) + end_mark = self.get_mark() + else: + self.forward() + if self.column > max_indent: + max_indent = self.column + return chunks, max_indent, end_mark + + def scan_block_scalar_breaks(self, indent): + # See the specification for details. + chunks = [] + end_mark = self.get_mark() + while self.column < indent and self.peek() == ' ': + self.forward() + while self.peek() in '\r\n\x85\u2028\u2029': + chunks.append(self.scan_line_break()) + end_mark = self.get_mark() + while self.column < indent and self.peek() == ' ': + self.forward() + return chunks, end_mark + + def scan_flow_scalar(self, style): + # See the specification for details. + # Note that we loose indentation rules for quoted scalars. Quoted + # scalars don't need to adhere indentation because " and ' clearly + # mark the beginning and the end of them. Therefore we are less + # restrictive then the specification requires. We only need to check + # that document separators are not included in scalars. + if style == '"': + double = True + else: + double = False + chunks = [] + start_mark = self.get_mark() + quote = self.peek() + self.forward() + chunks.extend(self.scan_flow_scalar_non_spaces(double, start_mark)) + while self.peek() != quote: + chunks.extend(self.scan_flow_scalar_spaces(double, start_mark)) + chunks.extend(self.scan_flow_scalar_non_spaces(double, start_mark)) + self.forward() + end_mark = self.get_mark() + return ScalarToken(''.join(chunks), False, start_mark, end_mark, + style) + + ESCAPE_REPLACEMENTS = { + '0': '\0', + 'a': '\x07', + 'b': '\x08', + 't': '\x09', + '\t': '\x09', + 'n': '\x0A', + 'v': '\x0B', + 'f': '\x0C', + 'r': '\x0D', + 'e': '\x1B', + ' ': '\x20', + '\"': '\"', + '\\': '\\', + 'N': '\x85', + '_': '\xA0', + 'L': '\u2028', + 'P': '\u2029', + } + + ESCAPE_CODES = { + 'x': 2, + 'u': 4, + 'U': 8, + } + + def scan_flow_scalar_non_spaces(self, double, start_mark): + # See the specification for details. + chunks = [] + while True: + length = 0 + while self.peek(length) not in '\'\"\\\0 \t\r\n\x85\u2028\u2029': + length += 1 + if length: + chunks.append(self.prefix(length)) + self.forward(length) + ch = self.peek() + if not double and ch == '\'' and self.peek(1) == '\'': + chunks.append('\'') + self.forward(2) + elif (double and ch == '\'') or (not double and ch in '\"\\'): + chunks.append(ch) + self.forward() + elif double and ch == '\\': + self.forward() + ch = self.peek() + if ch in self.ESCAPE_REPLACEMENTS: + chunks.append(self.ESCAPE_REPLACEMENTS[ch]) + self.forward() + elif ch in self.ESCAPE_CODES: + length = self.ESCAPE_CODES[ch] + self.forward() + for k in range(length): + if self.peek(k) not in '0123456789ABCDEFabcdef': + raise ScannerError("while scanning a double-quoted scalar", start_mark, + "expected escape sequence of %d hexdecimal numbers, but found %r" % + (length, self.peek(k)), self.get_mark()) + code = int(self.prefix(length), 16) + chunks.append(chr(code)) + self.forward(length) + elif ch in '\r\n\x85\u2028\u2029': + self.scan_line_break() + chunks.extend(self.scan_flow_scalar_breaks(double, start_mark)) + else: + raise ScannerError("while scanning a double-quoted scalar", start_mark, + "found unknown escape character %r" % ch, self.get_mark()) + else: + return chunks + + def scan_flow_scalar_spaces(self, double, start_mark): + # See the specification for details. + chunks = [] + length = 0 + while self.peek(length) in ' \t': + length += 1 + whitespaces = self.prefix(length) + self.forward(length) + ch = self.peek() + if ch == '\0': + raise ScannerError("while scanning a quoted scalar", start_mark, + "found unexpected end of stream", self.get_mark()) + elif ch in '\r\n\x85\u2028\u2029': + line_break = self.scan_line_break() + breaks = self.scan_flow_scalar_breaks(double, start_mark) + if line_break != '\n': + chunks.append(line_break) + elif not breaks: + chunks.append(' ') + chunks.extend(breaks) + else: + chunks.append(whitespaces) + return chunks + + def scan_flow_scalar_breaks(self, double, start_mark): + # See the specification for details. + chunks = [] + while True: + # Instead of checking indentation, we check for document + # separators. + prefix = self.prefix(3) + if (prefix == '---' or prefix == '...') \ + and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': + raise ScannerError("while scanning a quoted scalar", start_mark, + "found unexpected document separator", self.get_mark()) + while self.peek() in ' \t': + self.forward() + if self.peek() in '\r\n\x85\u2028\u2029': + chunks.append(self.scan_line_break()) + else: + return chunks + + def scan_plain(self): + # See the specification for details. + # We add an additional restriction for the flow context: + # plain scalars in the flow context cannot contain ',', ':' and '?'. + # We also keep track of the `allow_simple_key` flag here. + # Indentation rules are loosed for the flow context. + chunks = [] + start_mark = self.get_mark() + end_mark = start_mark + indent = self.indent+1 + # We allow zero indentation for scalars, but then we need to check for + # document separators at the beginning of the line. + #if indent == 0: + # indent = 1 + spaces = [] + while True: + length = 0 + if self.peek() == '#': + break + while True: + ch = self.peek(length) + if ch in '\0 \t\r\n\x85\u2028\u2029' \ + or (not self.flow_level and ch == ':' and + self.peek(length+1) in '\0 \t\r\n\x85\u2028\u2029') \ + or (self.flow_level and ch in ',:?[]{}'): + break + length += 1 + # It's not clear what we should do with ':' in the flow context. + if (self.flow_level and ch == ':' + and self.peek(length+1) not in '\0 \t\r\n\x85\u2028\u2029,[]{}'): + self.forward(length) + raise ScannerError("while scanning a plain scalar", start_mark, + "found unexpected ':'", self.get_mark(), + "Please check http://pyyaml.org/wiki/YAMLColonInFlowContext for details.") + if length == 0: + break + self.allow_simple_key = False + chunks.extend(spaces) + chunks.append(self.prefix(length)) + self.forward(length) + end_mark = self.get_mark() + spaces = self.scan_plain_spaces(indent, start_mark) + if not spaces or self.peek() == '#' \ + or (not self.flow_level and self.column < indent): + break + return ScalarToken(''.join(chunks), True, start_mark, end_mark) + + def scan_plain_spaces(self, indent, start_mark): + # See the specification for details. + # The specification is really confusing about tabs in plain scalars. + # We just forbid them completely. Do not use tabs in YAML! + chunks = [] + length = 0 + while self.peek(length) in ' ': + length += 1 + whitespaces = self.prefix(length) + self.forward(length) + ch = self.peek() + if ch in '\r\n\x85\u2028\u2029': + line_break = self.scan_line_break() + self.allow_simple_key = True + prefix = self.prefix(3) + if (prefix == '---' or prefix == '...') \ + and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': + return + breaks = [] + while self.peek() in ' \r\n\x85\u2028\u2029': + if self.peek() == ' ': + self.forward() + else: + breaks.append(self.scan_line_break()) + prefix = self.prefix(3) + if (prefix == '---' or prefix == '...') \ + and self.peek(3) in '\0 \t\r\n\x85\u2028\u2029': + return + if line_break != '\n': + chunks.append(line_break) + elif not breaks: + chunks.append(' ') + chunks.extend(breaks) + elif whitespaces: + chunks.append(whitespaces) + return chunks + + def scan_tag_handle(self, name, start_mark): + # See the specification for details. + # For some strange reasons, the specification does not allow '_' in + # tag handles. I have allowed it anyway. + ch = self.peek() + if ch != '!': + raise ScannerError("while scanning a %s" % name, start_mark, + "expected '!', but found %r" % ch, self.get_mark()) + length = 1 + ch = self.peek(length) + if ch != ' ': + while '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-_': + length += 1 + ch = self.peek(length) + if ch != '!': + self.forward(length) + raise ScannerError("while scanning a %s" % name, start_mark, + "expected '!', but found %r" % ch, self.get_mark()) + length += 1 + value = self.prefix(length) + self.forward(length) + return value + + def scan_tag_uri(self, name, start_mark): + # See the specification for details. + # Note: we do not check if URI is well-formed. + chunks = [] + length = 0 + ch = self.peek(length) + while '0' <= ch <= '9' or 'A' <= ch <= 'Z' or 'a' <= ch <= 'z' \ + or ch in '-;/?:@&=+$,_.!~*\'()[]%': + if ch == '%': + chunks.append(self.prefix(length)) + self.forward(length) + length = 0 + chunks.append(self.scan_uri_escapes(name, start_mark)) + else: + length += 1 + ch = self.peek(length) + if length: + chunks.append(self.prefix(length)) + self.forward(length) + length = 0 + if not chunks: + raise ScannerError("while parsing a %s" % name, start_mark, + "expected URI, but found %r" % ch, self.get_mark()) + return ''.join(chunks) + + def scan_uri_escapes(self, name, start_mark): + # See the specification for details. + codes = [] + mark = self.get_mark() + while self.peek() == '%': + self.forward() + for k in range(2): + if self.peek(k) not in '0123456789ABCDEFabcdef': + raise ScannerError("while scanning a %s" % name, start_mark, + "expected URI escape sequence of 2 hexdecimal numbers, but found %r" + % self.peek(k), self.get_mark()) + codes.append(int(self.prefix(2), 16)) + self.forward(2) + try: + value = bytes(codes).decode('utf-8') + except UnicodeDecodeError as exc: + raise ScannerError("while scanning a %s" % name, start_mark, str(exc), mark) + return value + + def scan_line_break(self): + # Transforms: + # '\r\n' : '\n' + # '\r' : '\n' + # '\n' : '\n' + # '\x85' : '\n' + # '\u2028' : '\u2028' + # '\u2029 : '\u2029' + # default : '' + ch = self.peek() + if ch in '\r\n\x85': + if self.prefix(2) == '\r\n': + self.forward(2) + else: + self.forward() + return '\n' + elif ch in '\u2028\u2029': + self.forward() + return ch + return '' + +#try: +# import psyco +# psyco.bind(Scanner) +#except ImportError: +# pass + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py3/serializer.py b/RenderPipelineFile/rplibs/yaml/yaml_py3/serializer.py index fe911e67..1a47ba2f 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py3/serializer.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py3/serializer.py @@ -1,111 +1,111 @@ - -__all__ = ['Serializer', 'SerializerError'] - -from .error import YAMLError -from .events import * -from .nodes import * - -class SerializerError(YAMLError): - pass - -class Serializer: - - ANCHOR_TEMPLATE = 'id%03d' - - def __init__(self, encoding=None, - explicit_start=None, explicit_end=None, version=None, tags=None): - self.use_encoding = encoding - self.use_explicit_start = explicit_start - self.use_explicit_end = explicit_end - self.use_version = version - self.use_tags = tags - self.serialized_nodes = {} - self.anchors = {} - self.last_anchor_id = 0 - self.closed = None - - def open(self): - if self.closed is None: - self.emit(StreamStartEvent(encoding=self.use_encoding)) - self.closed = False - elif self.closed: - raise SerializerError("serializer is closed") - else: - raise SerializerError("serializer is already opened") - - def close(self): - if self.closed is None: - raise SerializerError("serializer is not opened") - elif not self.closed: - self.emit(StreamEndEvent()) - self.closed = True - - #def __del__(self): - # self.close() - - def serialize(self, node): - if self.closed is None: - raise SerializerError("serializer is not opened") - elif self.closed: - raise SerializerError("serializer is closed") - self.emit(DocumentStartEvent(explicit=self.use_explicit_start, - version=self.use_version, tags=self.use_tags)) - self.anchor_node(node) - self.serialize_node(node, None, None) - self.emit(DocumentEndEvent(explicit=self.use_explicit_end)) - self.serialized_nodes = {} - self.anchors = {} - self.last_anchor_id = 0 - - def anchor_node(self, node): - if node in self.anchors: - if self.anchors[node] is None: - self.anchors[node] = self.generate_anchor(node) - else: - self.anchors[node] = None - if isinstance(node, SequenceNode): - for item in node.value: - self.anchor_node(item) - elif isinstance(node, MappingNode): - for key, value in node.value: - self.anchor_node(key) - self.anchor_node(value) - - def generate_anchor(self, node): - self.last_anchor_id += 1 - return self.ANCHOR_TEMPLATE % self.last_anchor_id - - def serialize_node(self, node, parent, index): - alias = self.anchors[node] - if node in self.serialized_nodes: - self.emit(AliasEvent(alias)) - else: - self.serialized_nodes[node] = True - self.descend_resolver(parent, index) - if isinstance(node, ScalarNode): - detected_tag = self.resolve(ScalarNode, node.value, (True, False)) - default_tag = self.resolve(ScalarNode, node.value, (False, True)) - implicit = (node.tag == detected_tag), (node.tag == default_tag) - self.emit(ScalarEvent(alias, node.tag, implicit, node.value, - style=node.style)) - elif isinstance(node, SequenceNode): - implicit = (node.tag - == self.resolve(SequenceNode, node.value, True)) - self.emit(SequenceStartEvent(alias, node.tag, implicit, - flow_style=node.flow_style)) - index = 0 - for item in node.value: - self.serialize_node(item, node, index) - index += 1 - self.emit(SequenceEndEvent()) - elif isinstance(node, MappingNode): - implicit = (node.tag - == self.resolve(MappingNode, node.value, True)) - self.emit(MappingStartEvent(alias, node.tag, implicit, - flow_style=node.flow_style)) - for key, value in node.value: - self.serialize_node(key, node, None) - self.serialize_node(value, node, key) - self.emit(MappingEndEvent()) - self.ascend_resolver() - + +__all__ = ['Serializer', 'SerializerError'] + +from .error import YAMLError +from .events import * +from .nodes import * + +class SerializerError(YAMLError): + pass + +class Serializer: + + ANCHOR_TEMPLATE = 'id%03d' + + def __init__(self, encoding=None, + explicit_start=None, explicit_end=None, version=None, tags=None): + self.use_encoding = encoding + self.use_explicit_start = explicit_start + self.use_explicit_end = explicit_end + self.use_version = version + self.use_tags = tags + self.serialized_nodes = {} + self.anchors = {} + self.last_anchor_id = 0 + self.closed = None + + def open(self): + if self.closed is None: + self.emit(StreamStartEvent(encoding=self.use_encoding)) + self.closed = False + elif self.closed: + raise SerializerError("serializer is closed") + else: + raise SerializerError("serializer is already opened") + + def close(self): + if self.closed is None: + raise SerializerError("serializer is not opened") + elif not self.closed: + self.emit(StreamEndEvent()) + self.closed = True + + #def __del__(self): + # self.close() + + def serialize(self, node): + if self.closed is None: + raise SerializerError("serializer is not opened") + elif self.closed: + raise SerializerError("serializer is closed") + self.emit(DocumentStartEvent(explicit=self.use_explicit_start, + version=self.use_version, tags=self.use_tags)) + self.anchor_node(node) + self.serialize_node(node, None, None) + self.emit(DocumentEndEvent(explicit=self.use_explicit_end)) + self.serialized_nodes = {} + self.anchors = {} + self.last_anchor_id = 0 + + def anchor_node(self, node): + if node in self.anchors: + if self.anchors[node] is None: + self.anchors[node] = self.generate_anchor(node) + else: + self.anchors[node] = None + if isinstance(node, SequenceNode): + for item in node.value: + self.anchor_node(item) + elif isinstance(node, MappingNode): + for key, value in node.value: + self.anchor_node(key) + self.anchor_node(value) + + def generate_anchor(self, node): + self.last_anchor_id += 1 + return self.ANCHOR_TEMPLATE % self.last_anchor_id + + def serialize_node(self, node, parent, index): + alias = self.anchors[node] + if node in self.serialized_nodes: + self.emit(AliasEvent(alias)) + else: + self.serialized_nodes[node] = True + self.descend_resolver(parent, index) + if isinstance(node, ScalarNode): + detected_tag = self.resolve(ScalarNode, node.value, (True, False)) + default_tag = self.resolve(ScalarNode, node.value, (False, True)) + implicit = (node.tag == detected_tag), (node.tag == default_tag) + self.emit(ScalarEvent(alias, node.tag, implicit, node.value, + style=node.style)) + elif isinstance(node, SequenceNode): + implicit = (node.tag + == self.resolve(SequenceNode, node.value, True)) + self.emit(SequenceStartEvent(alias, node.tag, implicit, + flow_style=node.flow_style)) + index = 0 + for item in node.value: + self.serialize_node(item, node, index) + index += 1 + self.emit(SequenceEndEvent()) + elif isinstance(node, MappingNode): + implicit = (node.tag + == self.resolve(MappingNode, node.value, True)) + self.emit(MappingStartEvent(alias, node.tag, implicit, + flow_style=node.flow_style)) + for key, value in node.value: + self.serialize_node(key, node, None) + self.serialize_node(value, node, key) + self.emit(MappingEndEvent()) + self.ascend_resolver() + diff --git a/RenderPipelineFile/rplibs/yaml/yaml_py3/tokens.py b/RenderPipelineFile/rplibs/yaml/yaml_py3/tokens.py index 4d0b48a3..d06d1cf3 100644 --- a/RenderPipelineFile/rplibs/yaml/yaml_py3/tokens.py +++ b/RenderPipelineFile/rplibs/yaml/yaml_py3/tokens.py @@ -1,104 +1,104 @@ - -class Token(object): - def __init__(self, start_mark, end_mark): - self.start_mark = start_mark - self.end_mark = end_mark - def __repr__(self): - attributes = [key for key in self.__dict__ - if not key.endswith('_mark')] - attributes.sort() - arguments = ', '.join(['%s=%r' % (key, getattr(self, key)) - for key in attributes]) - return '%s(%s)' % (self.__class__.__name__, arguments) - -#class BOMToken(Token): -# id = '' - -class DirectiveToken(Token): - id = '' - def __init__(self, name, value, start_mark, end_mark): - self.name = name - self.value = value - self.start_mark = start_mark - self.end_mark = end_mark - -class DocumentStartToken(Token): - id = '' - -class DocumentEndToken(Token): - id = '' - -class StreamStartToken(Token): - id = '' - def __init__(self, start_mark=None, end_mark=None, - encoding=None): - self.start_mark = start_mark - self.end_mark = end_mark - self.encoding = encoding - -class StreamEndToken(Token): - id = '' - -class BlockSequenceStartToken(Token): - id = '' - -class BlockMappingStartToken(Token): - id = '' - -class BlockEndToken(Token): - id = '' - -class FlowSequenceStartToken(Token): - id = '[' - -class FlowMappingStartToken(Token): - id = '{' - -class FlowSequenceEndToken(Token): - id = ']' - -class FlowMappingEndToken(Token): - id = '}' - -class KeyToken(Token): - id = '?' - -class ValueToken(Token): - id = ':' - -class BlockEntryToken(Token): - id = '-' - -class FlowEntryToken(Token): - id = ',' - -class AliasToken(Token): - id = '' - def __init__(self, value, start_mark, end_mark): - self.value = value - self.start_mark = start_mark - self.end_mark = end_mark - -class AnchorToken(Token): - id = '' - def __init__(self, value, start_mark, end_mark): - self.value = value - self.start_mark = start_mark - self.end_mark = end_mark - -class TagToken(Token): - id = '' - def __init__(self, value, start_mark, end_mark): - self.value = value - self.start_mark = start_mark - self.end_mark = end_mark - -class ScalarToken(Token): - id = '' - def __init__(self, value, plain, start_mark, end_mark, style=None): - self.value = value - self.plain = plain - self.start_mark = start_mark - self.end_mark = end_mark - self.style = style - + +class Token(object): + def __init__(self, start_mark, end_mark): + self.start_mark = start_mark + self.end_mark = end_mark + def __repr__(self): + attributes = [key for key in self.__dict__ + if not key.endswith('_mark')] + attributes.sort() + arguments = ', '.join(['%s=%r' % (key, getattr(self, key)) + for key in attributes]) + return '%s(%s)' % (self.__class__.__name__, arguments) + +#class BOMToken(Token): +# id = '' + +class DirectiveToken(Token): + id = '' + def __init__(self, name, value, start_mark, end_mark): + self.name = name + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + +class DocumentStartToken(Token): + id = '' + +class DocumentEndToken(Token): + id = '' + +class StreamStartToken(Token): + id = '' + def __init__(self, start_mark=None, end_mark=None, + encoding=None): + self.start_mark = start_mark + self.end_mark = end_mark + self.encoding = encoding + +class StreamEndToken(Token): + id = '' + +class BlockSequenceStartToken(Token): + id = '' + +class BlockMappingStartToken(Token): + id = '' + +class BlockEndToken(Token): + id = '' + +class FlowSequenceStartToken(Token): + id = '[' + +class FlowMappingStartToken(Token): + id = '{' + +class FlowSequenceEndToken(Token): + id = ']' + +class FlowMappingEndToken(Token): + id = '}' + +class KeyToken(Token): + id = '?' + +class ValueToken(Token): + id = ':' + +class BlockEntryToken(Token): + id = '-' + +class FlowEntryToken(Token): + id = ',' + +class AliasToken(Token): + id = '' + def __init__(self, value, start_mark, end_mark): + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + +class AnchorToken(Token): + id = '' + def __init__(self, value, start_mark, end_mark): + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + +class TagToken(Token): + id = '' + def __init__(self, value, start_mark, end_mark): + self.value = value + self.start_mark = start_mark + self.end_mark = end_mark + +class ScalarToken(Token): + id = '' + def __init__(self, value, plain, start_mark, end_mark, style=None): + self.value = value + self.plain = plain + self.start_mark = start_mark + self.end_mark = end_mark + self.style = style + diff --git a/RenderPipelineFile/rpplugins/README.md b/RenderPipelineFile/rpplugins/README.md index bca88809..2af03890 100644 --- a/RenderPipelineFile/rpplugins/README.md +++ b/RenderPipelineFile/rpplugins/README.md @@ -1,8 +1,8 @@ - -## Plugins - -This is the plugin directory of the pipeline. It contains all plugins, including -their source code, resources and shaders. - -In case anybody ever makes plugins for the pipeline, you would want to copy -them into this folder to make them available for the render pipeline. + +## Plugins + +This is the plugin directory of the pipeline. It contains all plugins, including +their source code, resources and shaders. + +In case anybody ever makes plugins for the pipeline, you would want to copy +them into this folder to make them available for the render pipeline. diff --git a/RenderPipelineFile/rpplugins/ao/ao_stage.py b/RenderPipelineFile/rpplugins/ao/ao_stage.py index 9f982a15..ee061f1b 100644 --- a/RenderPipelineFile/rpplugins/ao/ao_stage.py +++ b/RenderPipelineFile/rpplugins/ao/ao_stage.py @@ -1,116 +1,116 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import LVecBase2i, Vec2 -from rpcore.render_stage import RenderStage - - -class AOStage(RenderStage): - - required_inputs = [] - required_pipes = ["GBuffer", "DownscaledDepth", "PreviousFrame::AmbientOcclusion", - "CombinedVelocity", "PreviousFrame::SceneDepth"] - - @property - def produced_pipes(self): - return {"AmbientOcclusion": self.target_resolve.color_tex} - - def create(self): - self.target = self.create_target("Sample") - self.target.size = -2 - self.target.add_color_attachment(bits=(8, 0, 0, 0)) - self.target.prepare_buffer() - - self.target_upscale = self.create_target("Upscale") - self.target_upscale.add_color_attachment(bits=(8, 0, 0, 0)) - self.target_upscale.prepare_buffer() - - self.target_upscale.set_shader_inputs( - SourceTex=self.target.color_tex, - upscaleWeights=Vec2(0.001, 0.001)) - - self.tarrget_detail_ao = self.create_target("DetailAO") - self.tarrget_detail_ao.add_color_attachment(bits=(8, 0, 0, 0)) - self.tarrget_detail_ao.prepare_buffer() - self.tarrget_detail_ao.set_shader_input("AOResult", self.target_upscale.color_tex) - - self.debug("Blur quality is", self.quality) - - # Low - pixel_stretch = 2.0 - blur_passes = 1 - - if self.quality == "MEDIUM": - pixel_stretch = 1.0 - blur_passes = 2 - elif self.quality == "HIGH": - pixel_stretch = 1.0 - blur_passes = 3 - elif self.quality == "ULTRA": - pixel_stretch = 1.0 - blur_passes = 5 - - self.blur_targets = [] - - current_tex = self.tarrget_detail_ao.color_tex - - for i in range(blur_passes): - target_blur_v = self.create_target("BlurV-" + str(i)) - target_blur_v.add_color_attachment(bits=(8, 0, 0, 0)) - target_blur_v.prepare_buffer() - - target_blur_h = self.create_target("BlurH-" + str(i)) - target_blur_h.add_color_attachment(bits=(8, 0, 0, 0)) - target_blur_h.prepare_buffer() - - target_blur_v.set_shader_inputs( - SourceTex=current_tex, - blur_direction=LVecBase2i(0, 1), - pixel_stretch=pixel_stretch) - - target_blur_h.set_shader_inputs( - SourceTex=target_blur_v.color_tex, - blur_direction=LVecBase2i(1, 0), - pixel_stretch=pixel_stretch) - - current_tex = target_blur_h.color_tex - self.blur_targets += [target_blur_v, target_blur_h] - - self.target_resolve = self.create_target("ResolveAO") - self.target_resolve.add_color_attachment(bits=(8, 0, 0, 0)) - self.target_resolve.prepare_buffer() - self.target_resolve.set_shader_input("CurrentTex", current_tex) - - def reload_shaders(self): - self.target.shader = self.load_plugin_shader("ao_sample.frag.glsl") - self.target_upscale.shader = self.load_plugin_shader( - "/$$rp/shader/bilateral_upscale.frag.glsl") - blur_shader = self.load_plugin_shader( - "/$$rp/shader/bilateral_blur.frag.glsl") - for target in self.blur_targets: - target.shader = blur_shader - self.tarrget_detail_ao.shader = self.load_plugin_shader("small_scale_ao.frag.glsl") - self.target_resolve.shader = self.load_plugin_shader("resolve_ao.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import LVecBase2i, Vec2 +from rpcore.render_stage import RenderStage + + +class AOStage(RenderStage): + + required_inputs = [] + required_pipes = ["GBuffer", "DownscaledDepth", "PreviousFrame::AmbientOcclusion", + "CombinedVelocity", "PreviousFrame::SceneDepth"] + + @property + def produced_pipes(self): + return {"AmbientOcclusion": self.target_resolve.color_tex} + + def create(self): + self.target = self.create_target("Sample") + self.target.size = -2 + self.target.add_color_attachment(bits=(8, 0, 0, 0)) + self.target.prepare_buffer() + + self.target_upscale = self.create_target("Upscale") + self.target_upscale.add_color_attachment(bits=(8, 0, 0, 0)) + self.target_upscale.prepare_buffer() + + self.target_upscale.set_shader_inputs( + SourceTex=self.target.color_tex, + upscaleWeights=Vec2(0.001, 0.001)) + + self.tarrget_detail_ao = self.create_target("DetailAO") + self.tarrget_detail_ao.add_color_attachment(bits=(8, 0, 0, 0)) + self.tarrget_detail_ao.prepare_buffer() + self.tarrget_detail_ao.set_shader_input("AOResult", self.target_upscale.color_tex) + + self.debug("Blur quality is", self.quality) + + # Low + pixel_stretch = 2.0 + blur_passes = 1 + + if self.quality == "MEDIUM": + pixel_stretch = 1.0 + blur_passes = 2 + elif self.quality == "HIGH": + pixel_stretch = 1.0 + blur_passes = 3 + elif self.quality == "ULTRA": + pixel_stretch = 1.0 + blur_passes = 5 + + self.blur_targets = [] + + current_tex = self.tarrget_detail_ao.color_tex + + for i in range(blur_passes): + target_blur_v = self.create_target("BlurV-" + str(i)) + target_blur_v.add_color_attachment(bits=(8, 0, 0, 0)) + target_blur_v.prepare_buffer() + + target_blur_h = self.create_target("BlurH-" + str(i)) + target_blur_h.add_color_attachment(bits=(8, 0, 0, 0)) + target_blur_h.prepare_buffer() + + target_blur_v.set_shader_inputs( + SourceTex=current_tex, + blur_direction=LVecBase2i(0, 1), + pixel_stretch=pixel_stretch) + + target_blur_h.set_shader_inputs( + SourceTex=target_blur_v.color_tex, + blur_direction=LVecBase2i(1, 0), + pixel_stretch=pixel_stretch) + + current_tex = target_blur_h.color_tex + self.blur_targets += [target_blur_v, target_blur_h] + + self.target_resolve = self.create_target("ResolveAO") + self.target_resolve.add_color_attachment(bits=(8, 0, 0, 0)) + self.target_resolve.prepare_buffer() + self.target_resolve.set_shader_input("CurrentTex", current_tex) + + def reload_shaders(self): + self.target.shader = self.load_plugin_shader("ao_sample.frag.glsl") + self.target_upscale.shader = self.load_plugin_shader( + "/$$rp/shader/bilateral_upscale.frag.glsl") + blur_shader = self.load_plugin_shader( + "/$$rp/shader/bilateral_blur.frag.glsl") + for target in self.blur_targets: + target.shader = blur_shader + self.tarrget_detail_ao.shader = self.load_plugin_shader("small_scale_ao.frag.glsl") + self.target_resolve.shader = self.load_plugin_shader("resolve_ao.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/ao/config.yaml b/RenderPipelineFile/rpplugins/ao/config.yaml index 0005023a..a254f0fb 100644 --- a/RenderPipelineFile/rpplugins/ao/config.yaml +++ b/RenderPipelineFile/rpplugins/ao/config.yaml @@ -1,280 +1,280 @@ - -settings: !!omap - - - blur_quality: - type: enum - values: ["LOW", "MEDIUM", "HIGH", "ULTRA"] - default: "MEDIUM" - label: Blur quality - description: > - Controls the quality of the post-ao blur, higher values produce smoother - ambient occlusion, but also cost more performance. - - # General settings - - blur_normal_factor: - type: float - range: [0.1, 10.0] - default: 1.2 - shader_runtime: true - label: Bilateral Blur Normal Factor - description: > - This setting controls the normal influence for the bilateral blur. - A value of 0.0 means a normal blur, and a higher value will prevent - blurring over normals. - - - blur_depth_factor: - type: float - range: [0.1, 10.0] - default: 0.9 - shader_runtime: true - label: Biliteral Blur Depth Factor - description: > - This setting controls the depth influence for the bilateral blur. - If a pixel exceeds the distance to the mid pixel by this amount, it - will not get weighted. - - - occlusion_strength: - type: float - range: [0.1, 6.0] - default: 1.6 - shader_runtime: true - label: Occlusion Strength - description: > - This controls the strength of the occlusion. Higher values will make - the occlusion look darker, and lower values will make it less - noticeable. You most likely will have to adjust it differently for - every technique. - - - clip_length: - type: int - range: [1, 16] - default: 2 - shader_runtime: true - label: Clip length - description: > - Controls how long to keep AO samples in the history. Bigger values - will produce a smoother effect but might also produce artifacts, - especially when moving. - - - technique: - type: enum - values: ["SSAO", "HBAO", "SSVO", "ALCHEMY", "UE4AO"] - default: SSAO - shader_runtime: true - label: Technique - description: > - The kernel to use for computing ambient occlusion. The settings below - depend on the kernel, so choose a kernel first! - - - # Settings for SSAO - - - ssao_sample_radius: - display_if: {technique: "SSAO"} - type: float - range: [5, 400] - default: 80 - shader_runtime: true - label: SSAO Sample Radius - description: > - Size of the sampling radius in pixels, higher values might look better, - but also perform worse due to less caching. - - - ssao_sequence: - display_if: {technique: "SSAO"} - type: sample_sequence - dimension: 3 - default: halton_3D_8 - shader_runtime: true - label: SSAO Sample Sequence - description: > - Amount and type of samples to use. A higher amount of samples provides - a better quality but also a decrease in performance. - - - ssao_bias: - display_if: {technique: "SSAO"} - type: float - range: [0.0, 0.5] - default: 0.0333 - shader_runtime: true - label: SSAO Depth Bias - description: > - This is a small bias which prevents self-occlusion artifacts on - curved objects. - - - ssao_max_distance: - display_if: {technique: "SSAO"} - type: float - range: [0.0, 15.0] - default: 6.9 - shader_runtime: true - label: SSAO Max distance - description: > - Prevents halos by specifying the maximum distance between two - pixels to account for ambient occlusion. The distance should be - in world space. - - # Settings for HBAO - - - hbao_sample_radius: - display_if: {technique: "HBAO"} - type: float - range: [5, 400] - default: 255.0 - shader_runtime: true - label: HBAO Sample Radius - description: > - Size of the sampling radius in pixels, higher values might look better, - but also perform worse due to less caching. - - - hbao_ray_count: - display_if: {technique: "HBAO"} - type: int - range: [1, 8] - default: 4 - shader_runtime: true - label: HBAO Ray count - description: > - Amount of rays to march from the pixel position. - - - hbao_ray_steps: - display_if: {technique: "HBAO"} - type: int - range: [1, 16] - default: 3 - shader_runtime: true - label: HBAO Ray steps - description: > - Amount of steps to perform for each ray. - - - hbao_tangent_bias: - display_if: {technique: "HBAO"} - type: float - range: [0.0, 1.0] - default: 0.65 - shader_runtime: true - label: HBAO Tangent bias - description: > - Used to prevent self-occlusion on curved objects, this increases the tangent angle - by a fixed value. - - - hbao_max_distance: - display_if: {technique: "HBAO"} - type: float - range: [0.01, 25.0] - default: 11.5 - shader_runtime: true - label: HBAO Max sample distance - description: > - Maximum distance of a sample to the pixel center in world space - - - # Settings for SSVO - - ssvo_sequence: - display_if: {technique: "SSVO"} - type: sample_sequence - dimension: 2 - default: halton_2D_8 - shader_runtime: true - label: SSVO Sample Sequence - description: > - Amount and type of samples to use. A higher amount of samples - provides a better quality but also a decrease in performance. - - - ssvo_sphere_radius: - display_if: {technique: "SSVO"} - type: float - range: [0.1, 40.0] - default: 18.0 - shader_runtime: true - label: SSVO Sphere sample radius - description: > - Size of the sphere in view space to integrate. High values may produce - artifacts. - - - ssvo_max_distance: - display_if: {technique: "SSVO"} - type: float - range: [0.05, 10.0] - default: 0.82 - shader_runtime: true - label: SSVO Max distance - description: > - Maximum distance until a sample gets rejected, in world space. This prevents halos arround objects. - - - # Settings for ALCHEMY - - - - alchemy_sample_radius: - display_if: {technique: "ALCHEMY"} - type: float - range: [5, 400] - default: 210.0 - shader_runtime: true - label: ALCHEMY Sample Radius - description: > - Size of the sampling radius in pixels, higher values might look better, - but also perform worse due to less caching. - - - alchemy_sequence: - display_if: {technique: "ALCHEMY"} - type: sample_sequence - dimension: 2 - default: halton_2D_8 - shader_runtime: true - label: ALCHEMY Sample Sequence - description: > - Amount and type of samples to use. A higher amount of samples provides - a better quality but also a decrease in performance. - - - alchemy_max_distance: - display_if: {technique: "ALCHEMY"} - type: float - range: [0.1, 20.0] - default: 4.75 - shader_runtime: true - label: ALCHEMY Maximum distance - description: > - Specifies the maximum distance between two samples until one sample - is rejected. This prevents halos arround objects. - - - # Settings for UE4AO - - - ue4ao_sample_radius: - display_if: {technique: "UE4AO"} - type: float - range: [5, 400] - default: 110.5 - shader_runtime: true - label: UE4AO Sample Radius - description: > - Size of the sampling radius in pixels, higher values might look better, - but also perform worse due to less caching. - - - ue4ao_sample_sequence: - display_if: {technique: "UE4AO"} - type: sample_sequence - dimension: 2 - default: halton_2D_8 - shader_runtime: true - label: UE4AO Sample Sequence - description: > - Amount and type of samples to use. A higher amount of samples provides - a better quality but also a decrease in performance. - - - ue4ao_max_distance: - display_if: {technique: "UE4AO"} - type: float - range: [0.1, 15.0] - default: 1.87 - shader_runtime: true - label: UE4AO Maximum distance - description: > - Specifies the maximum distance between two samples until one sample - is rejected. This prevents halos arround objects. - - - -daytime_settings: + +settings: !!omap + + - blur_quality: + type: enum + values: ["LOW", "MEDIUM", "HIGH", "ULTRA"] + default: "MEDIUM" + label: Blur quality + description: > + Controls the quality of the post-ao blur, higher values produce smoother + ambient occlusion, but also cost more performance. + + # General settings + - blur_normal_factor: + type: float + range: [0.1, 10.0] + default: 1.2 + shader_runtime: true + label: Bilateral Blur Normal Factor + description: > + This setting controls the normal influence for the bilateral blur. + A value of 0.0 means a normal blur, and a higher value will prevent + blurring over normals. + + - blur_depth_factor: + type: float + range: [0.1, 10.0] + default: 0.9 + shader_runtime: true + label: Biliteral Blur Depth Factor + description: > + This setting controls the depth influence for the bilateral blur. + If a pixel exceeds the distance to the mid pixel by this amount, it + will not get weighted. + + - occlusion_strength: + type: float + range: [0.1, 6.0] + default: 1.6 + shader_runtime: true + label: Occlusion Strength + description: > + This controls the strength of the occlusion. Higher values will make + the occlusion look darker, and lower values will make it less + noticeable. You most likely will have to adjust it differently for + every technique. + + - clip_length: + type: int + range: [1, 16] + default: 2 + shader_runtime: true + label: Clip length + description: > + Controls how long to keep AO samples in the history. Bigger values + will produce a smoother effect but might also produce artifacts, + especially when moving. + + - technique: + type: enum + values: ["SSAO", "HBAO", "SSVO", "ALCHEMY", "UE4AO"] + default: SSAO + shader_runtime: true + label: Technique + description: > + The kernel to use for computing ambient occlusion. The settings below + depend on the kernel, so choose a kernel first! + + + # Settings for SSAO + + - ssao_sample_radius: + display_if: {technique: "SSAO"} + type: float + range: [5, 400] + default: 80 + shader_runtime: true + label: SSAO Sample Radius + description: > + Size of the sampling radius in pixels, higher values might look better, + but also perform worse due to less caching. + + - ssao_sequence: + display_if: {technique: "SSAO"} + type: sample_sequence + dimension: 3 + default: halton_3D_8 + shader_runtime: true + label: SSAO Sample Sequence + description: > + Amount and type of samples to use. A higher amount of samples provides + a better quality but also a decrease in performance. + + - ssao_bias: + display_if: {technique: "SSAO"} + type: float + range: [0.0, 0.5] + default: 0.0333 + shader_runtime: true + label: SSAO Depth Bias + description: > + This is a small bias which prevents self-occlusion artifacts on + curved objects. + + - ssao_max_distance: + display_if: {technique: "SSAO"} + type: float + range: [0.0, 15.0] + default: 6.9 + shader_runtime: true + label: SSAO Max distance + description: > + Prevents halos by specifying the maximum distance between two + pixels to account for ambient occlusion. The distance should be + in world space. + + # Settings for HBAO + + - hbao_sample_radius: + display_if: {technique: "HBAO"} + type: float + range: [5, 400] + default: 255.0 + shader_runtime: true + label: HBAO Sample Radius + description: > + Size of the sampling radius in pixels, higher values might look better, + but also perform worse due to less caching. + + - hbao_ray_count: + display_if: {technique: "HBAO"} + type: int + range: [1, 8] + default: 4 + shader_runtime: true + label: HBAO Ray count + description: > + Amount of rays to march from the pixel position. + + - hbao_ray_steps: + display_if: {technique: "HBAO"} + type: int + range: [1, 16] + default: 3 + shader_runtime: true + label: HBAO Ray steps + description: > + Amount of steps to perform for each ray. + + - hbao_tangent_bias: + display_if: {technique: "HBAO"} + type: float + range: [0.0, 1.0] + default: 0.65 + shader_runtime: true + label: HBAO Tangent bias + description: > + Used to prevent self-occlusion on curved objects, this increases the tangent angle + by a fixed value. + + - hbao_max_distance: + display_if: {technique: "HBAO"} + type: float + range: [0.01, 25.0] + default: 11.5 + shader_runtime: true + label: HBAO Max sample distance + description: > + Maximum distance of a sample to the pixel center in world space + + + # Settings for SSVO + - ssvo_sequence: + display_if: {technique: "SSVO"} + type: sample_sequence + dimension: 2 + default: halton_2D_8 + shader_runtime: true + label: SSVO Sample Sequence + description: > + Amount and type of samples to use. A higher amount of samples + provides a better quality but also a decrease in performance. + + - ssvo_sphere_radius: + display_if: {technique: "SSVO"} + type: float + range: [0.1, 40.0] + default: 18.0 + shader_runtime: true + label: SSVO Sphere sample radius + description: > + Size of the sphere in view space to integrate. High values may produce + artifacts. + + - ssvo_max_distance: + display_if: {technique: "SSVO"} + type: float + range: [0.05, 10.0] + default: 0.82 + shader_runtime: true + label: SSVO Max distance + description: > + Maximum distance until a sample gets rejected, in world space. This prevents halos arround objects. + + + # Settings for ALCHEMY + + + - alchemy_sample_radius: + display_if: {technique: "ALCHEMY"} + type: float + range: [5, 400] + default: 210.0 + shader_runtime: true + label: ALCHEMY Sample Radius + description: > + Size of the sampling radius in pixels, higher values might look better, + but also perform worse due to less caching. + + - alchemy_sequence: + display_if: {technique: "ALCHEMY"} + type: sample_sequence + dimension: 2 + default: halton_2D_8 + shader_runtime: true + label: ALCHEMY Sample Sequence + description: > + Amount and type of samples to use. A higher amount of samples provides + a better quality but also a decrease in performance. + + - alchemy_max_distance: + display_if: {technique: "ALCHEMY"} + type: float + range: [0.1, 20.0] + default: 4.75 + shader_runtime: true + label: ALCHEMY Maximum distance + description: > + Specifies the maximum distance between two samples until one sample + is rejected. This prevents halos arround objects. + + + # Settings for UE4AO + + - ue4ao_sample_radius: + display_if: {technique: "UE4AO"} + type: float + range: [5, 400] + default: 110.5 + shader_runtime: true + label: UE4AO Sample Radius + description: > + Size of the sampling radius in pixels, higher values might look better, + but also perform worse due to less caching. + + - ue4ao_sample_sequence: + display_if: {technique: "UE4AO"} + type: sample_sequence + dimension: 2 + default: halton_2D_8 + shader_runtime: true + label: UE4AO Sample Sequence + description: > + Amount and type of samples to use. A higher amount of samples provides + a better quality but also a decrease in performance. + + - ue4ao_max_distance: + display_if: {technique: "UE4AO"} + type: float + range: [0.1, 15.0] + default: 1.87 + shader_runtime: true + label: UE4AO Maximum distance + description: > + Specifies the maximum distance between two samples until one sample + is rejected. This prevents halos arround objects. + + + +daytime_settings: diff --git a/RenderPipelineFile/rpplugins/ao/plugin.py b/RenderPipelineFile/rpplugins/ao/plugin.py index 7369d9ab..c046292b 100644 --- a/RenderPipelineFile/rpplugins/ao/plugin.py +++ b/RenderPipelineFile/rpplugins/ao/plugin.py @@ -1,46 +1,46 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.pluginbase.base_plugin import BasePlugin -from rpcore.stages.ambient_stage import AmbientStage - -from .ao_stage import AOStage - - -class Plugin(BasePlugin): - - name = "Ambient Occlusion" - author = "tobspr " - description = ("A plugin to render ambient occlusion supporting multiple" - "techniques such as SSAO, HBAO and much more ") - version = "1.1" - - def on_stage_setup(self): - self.stage = self.create_stage(AOStage) - self.stage.quality = self.get_setting("blur_quality") - - # Make the stages use our output - AmbientStage.required_pipes.append("AmbientOcclusion") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.pluginbase.base_plugin import BasePlugin +from rpcore.stages.ambient_stage import AmbientStage + +from .ao_stage import AOStage + + +class Plugin(BasePlugin): + + name = "Ambient Occlusion" + author = "tobspr " + description = ("A plugin to render ambient occlusion supporting multiple" + "techniques such as SSAO, HBAO and much more ") + version = "1.1" + + def on_stage_setup(self): + self.stage = self.create_stage(AOStage) + self.stage.quality = self.get_setting("blur_quality") + + # Make the stages use our output + AmbientStage.required_pipes.append("AmbientOcclusion") diff --git a/RenderPipelineFile/rpplugins/ao/shader/alchemy.kernel.glsl b/RenderPipelineFile/rpplugins/ao/shader/alchemy.kernel.glsl index c5ad798e..6e1d21ed 100644 --- a/RenderPipelineFile/rpplugins/ao/shader/alchemy.kernel.glsl +++ b/RenderPipelineFile/rpplugins/ao/shader/alchemy.kernel.glsl @@ -1,71 +1,71 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -/* - -ALCHEMY AO - -Projects random points to screen space, and uses the average unoccluded vector -to approximate AO. - -*/ - - -const float sample_radius = GET_SETTING(ao, alchemy_sample_radius); - -float max_dist = GET_SETTING(ao, alchemy_max_distance); -float accum = 0.0; -int accum_count = 0; -vec2 offset_scale = pixel_size * sample_radius * kernel_scale * 0.5; - - -START_ITERATE_SEQUENCE(ao, alchemy_sequence, vec2 offset) - - offset = mix(offset, noise_vec.xy, 0.3); - - vec2 offcoord = texcoord + offset * offset_scale; - - // Get view position at that offset - vec3 off_pos = get_view_pos_at(offcoord); - - // Get the vector s-p to that sample position - vec3 sample_vec = normalize(off_pos - pixel_view_pos); - - // Check if the distance matches, discard matches which are too far away - float dist = distance(off_pos, pixel_view_pos) / max_dist; - if (dist < 1.0) { - // Weight sample by the angle - accum += max(0, dot(pixel_view_normal, sample_vec)) * (1-dist); - } - - ++accum_count; - -END_ITERATE_SEQUENCE(); - -// Normalize values -accum /= max(1.0, float(accum_count)); -result = 1 - accum; +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +/* + +ALCHEMY AO + +Projects random points to screen space, and uses the average unoccluded vector +to approximate AO. + +*/ + + +const float sample_radius = GET_SETTING(ao, alchemy_sample_radius); + +float max_dist = GET_SETTING(ao, alchemy_max_distance); +float accum = 0.0; +int accum_count = 0; +vec2 offset_scale = pixel_size * sample_radius * kernel_scale * 0.5; + + +START_ITERATE_SEQUENCE(ao, alchemy_sequence, vec2 offset) + + offset = mix(offset, noise_vec.xy, 0.3); + + vec2 offcoord = texcoord + offset * offset_scale; + + // Get view position at that offset + vec3 off_pos = get_view_pos_at(offcoord); + + // Get the vector s-p to that sample position + vec3 sample_vec = normalize(off_pos - pixel_view_pos); + + // Check if the distance matches, discard matches which are too far away + float dist = distance(off_pos, pixel_view_pos) / max_dist; + if (dist < 1.0) { + // Weight sample by the angle + accum += max(0, dot(pixel_view_normal, sample_vec)) * (1-dist); + } + + ++accum_count; + +END_ITERATE_SEQUENCE(); + +// Normalize values +accum /= max(1.0, float(accum_count)); +result = 1 - accum; diff --git a/RenderPipelineFile/rpplugins/ao/shader/ao_sample.frag.glsl b/RenderPipelineFile/rpplugins/ao/shader/ao_sample.frag.glsl index d20b9b4d..a85903ad 100644 --- a/RenderPipelineFile/rpplugins/ao/shader/ao_sample.frag.glsl +++ b/RenderPipelineFile/rpplugins/ao/shader/ao_sample.frag.glsl @@ -1,97 +1,97 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma optionNV (unroll all) - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/transforms.inc.glsl" -#pragma include "includes/noise.inc.glsl" -#pragma include "includes/sampling_sequences.inc.glsl" - -out float result; - -uniform sampler2D DownscaledDepth; - -// use the extended gbuffer api -#define USE_GBUFFER_EXTENSIONS 1 -#pragma include "includes/gbuffer.inc.glsl" - -float get_linear_depth_at(vec2 coord) { - return textureLod(DownscaledDepth, coord, 0).x; - // return get_linear_z_from_z(textureLod(GBuffer.Depth, coord, 0).x); -} - -void main() { - - // Provide some variables to the kernel - vec2 screen_size = vec2(WINDOW_WIDTH, WINDOW_HEIGHT); - vec2 pixel_size = vec2(1.0) / screen_size; - - ivec2 coord = ivec2(gl_FragCoord.xy) * 2; - vec2 texcoord = (coord + 0.5) / SCREEN_SIZE; - - // Shader variables - float pixel_depth = get_depth_at(texcoord); - float pixel_distance = get_linear_z_from_z(pixel_depth); - - if (pixel_distance > 1000.0) { - result = 1.0; - return; - } - - // vec3 view_vector = normalize(pixel_world_pos - MainSceneData.camera_pos); - float view_dist = pixel_distance; - - vec3 noise_vec = rand_rgb(coord % 8 + - 0.05 * (MainSceneData.frame_index % (GET_SETTING(ao, clip_length)))); - - vec3 pixel_view_normal = get_view_normal(texcoord); - vec3 pixel_view_pos = get_view_pos_at(texcoord); - vec3 pixel_world_normal = get_gbuffer_normal(GBuffer, texcoord); - - // float kernel_scale = 10.0 / get_linear_z_from_z(pixel_depth); - float kernel_scale = min(5.0, 10.0 / view_dist); - - // Include the appropriate kernel - #if ENUM_V_ACTIVE(ao, technique, SSAO) - #pragma include "ssao.kernel.glsl" - #elif ENUM_V_ACTIVE(ao, technique, HBAO) - #pragma include "hbao.kernel.glsl" - #elif ENUM_V_ACTIVE(ao, technique, SSVO) - #pragma include "ssvo.kernel.glsl" - #elif ENUM_V_ACTIVE(ao, technique, ALCHEMY) - #pragma include "alchemy.kernel.glsl" - #elif ENUM_V_ACTIVE(ao, technique, UE4AO) - #pragma include "ue4ao.kernel.glsl" - #else - #error Unkown AO technique! - #endif - - result = pow(saturate(result), GET_SETTING(ao, occlusion_strength)); - result = pow(result, 3.0); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma optionNV (unroll all) + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/transforms.inc.glsl" +#pragma include "includes/noise.inc.glsl" +#pragma include "includes/sampling_sequences.inc.glsl" + +out float result; + +uniform sampler2D DownscaledDepth; + +// use the extended gbuffer api +#define USE_GBUFFER_EXTENSIONS 1 +#pragma include "includes/gbuffer.inc.glsl" + +float get_linear_depth_at(vec2 coord) { + return textureLod(DownscaledDepth, coord, 0).x; + // return get_linear_z_from_z(textureLod(GBuffer.Depth, coord, 0).x); +} + +void main() { + + // Provide some variables to the kernel + vec2 screen_size = vec2(WINDOW_WIDTH, WINDOW_HEIGHT); + vec2 pixel_size = vec2(1.0) / screen_size; + + ivec2 coord = ivec2(gl_FragCoord.xy) * 2; + vec2 texcoord = (coord + 0.5) / SCREEN_SIZE; + + // Shader variables + float pixel_depth = get_depth_at(texcoord); + float pixel_distance = get_linear_z_from_z(pixel_depth); + + if (pixel_distance > 1000.0) { + result = 1.0; + return; + } + + // vec3 view_vector = normalize(pixel_world_pos - MainSceneData.camera_pos); + float view_dist = pixel_distance; + + vec3 noise_vec = rand_rgb(coord % 8 + + 0.05 * (MainSceneData.frame_index % (GET_SETTING(ao, clip_length)))); + + vec3 pixel_view_normal = get_view_normal(texcoord); + vec3 pixel_view_pos = get_view_pos_at(texcoord); + vec3 pixel_world_normal = get_gbuffer_normal(GBuffer, texcoord); + + // float kernel_scale = 10.0 / get_linear_z_from_z(pixel_depth); + float kernel_scale = min(5.0, 10.0 / view_dist); + + // Include the appropriate kernel + #if ENUM_V_ACTIVE(ao, technique, SSAO) + #pragma include "ssao.kernel.glsl" + #elif ENUM_V_ACTIVE(ao, technique, HBAO) + #pragma include "hbao.kernel.glsl" + #elif ENUM_V_ACTIVE(ao, technique, SSVO) + #pragma include "ssvo.kernel.glsl" + #elif ENUM_V_ACTIVE(ao, technique, ALCHEMY) + #pragma include "alchemy.kernel.glsl" + #elif ENUM_V_ACTIVE(ao, technique, UE4AO) + #pragma include "ue4ao.kernel.glsl" + #else + #error Unkown AO technique! + #endif + + result = pow(saturate(result), GET_SETTING(ao, occlusion_strength)); + result = pow(result, 3.0); +} diff --git a/RenderPipelineFile/rpplugins/ao/shader/hbao.kernel.glsl b/RenderPipelineFile/rpplugins/ao/shader/hbao.kernel.glsl index 8b021436..624370c9 100644 --- a/RenderPipelineFile/rpplugins/ao/shader/hbao.kernel.glsl +++ b/RenderPipelineFile/rpplugins/ao/shader/hbao.kernel.glsl @@ -1,85 +1,85 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - - - -const float sample_radius = GET_SETTING(ao, hbao_sample_radius); -const int num_angles = GET_SETTING(ao, hbao_ray_count); -const int num_ray_steps = GET_SETTING(ao, hbao_ray_steps); -const float tangent_bias = GET_SETTING(ao, hbao_tangent_bias); -const float max_sample_distance = GET_SETTING(ao, hbao_max_distance) * 0.3; - -float accum = 0.0; - -for (int i = 0; i < num_angles; ++i) { - float angle = (i + 2 * noise_vec.x) / float(num_angles) * TWO_PI; - - vec2 sample_dir = vec2(cos(angle), sin(angle)); - - // Find the tangent andle - float tangent_angle = acos(dot(vec3(sample_dir, 0), pixel_view_normal)) - 0.5 * M_PI - + tangent_bias; - - // Assume the horizon angle is the same as the tangent angle at the beginning - // of the ray - float horizon_angle = tangent_angle; - - vec3 last_diff = vec3(0); - - // Raymarch in the sample direction - for (int k = 0; k < num_ray_steps; ++k) { - - // Get new texture coordinate - vec2 texc = texcoord + - sample_dir * (k + 2.0 + 2 * noise_vec.y) / - num_ray_steps * pixel_size * sample_radius * kernel_scale * 0.3; - - // Fetch view pos at that position and compare it - vec3 view_pos = get_view_pos_at(texc); - vec3 diff = view_pos - pixel_view_pos; - - if(length(diff) < max_sample_distance) { - - // Compute actual angle - float sample_angle = atan(diff.z / length(diff.xy)); - - // Correct horizon angle - horizon_angle = max(horizon_angle, sample_angle); - last_diff = diff; - } - } - - // Now that we know the average horizon angle, add it to the result - // For that we simply take the angle-difference - float occlusion = saturate(sin(horizon_angle) - sin(tangent_angle)); - occlusion *= 1.0 / (1 + length(last_diff)); - accum += occlusion; -} - -// Normalize samples -accum /= num_angles; -result = 1 - accum; +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + + + +const float sample_radius = GET_SETTING(ao, hbao_sample_radius); +const int num_angles = GET_SETTING(ao, hbao_ray_count); +const int num_ray_steps = GET_SETTING(ao, hbao_ray_steps); +const float tangent_bias = GET_SETTING(ao, hbao_tangent_bias); +const float max_sample_distance = GET_SETTING(ao, hbao_max_distance) * 0.3; + +float accum = 0.0; + +for (int i = 0; i < num_angles; ++i) { + float angle = (i + 2 * noise_vec.x) / float(num_angles) * TWO_PI; + + vec2 sample_dir = vec2(cos(angle), sin(angle)); + + // Find the tangent andle + float tangent_angle = acos(dot(vec3(sample_dir, 0), pixel_view_normal)) - 0.5 * M_PI + + tangent_bias; + + // Assume the horizon angle is the same as the tangent angle at the beginning + // of the ray + float horizon_angle = tangent_angle; + + vec3 last_diff = vec3(0); + + // Raymarch in the sample direction + for (int k = 0; k < num_ray_steps; ++k) { + + // Get new texture coordinate + vec2 texc = texcoord + + sample_dir * (k + 2.0 + 2 * noise_vec.y) / + num_ray_steps * pixel_size * sample_radius * kernel_scale * 0.3; + + // Fetch view pos at that position and compare it + vec3 view_pos = get_view_pos_at(texc); + vec3 diff = view_pos - pixel_view_pos; + + if(length(diff) < max_sample_distance) { + + // Compute actual angle + float sample_angle = atan(diff.z / length(diff.xy)); + + // Correct horizon angle + horizon_angle = max(horizon_angle, sample_angle); + last_diff = diff; + } + } + + // Now that we know the average horizon angle, add it to the result + // For that we simply take the angle-difference + float occlusion = saturate(sin(horizon_angle) - sin(tangent_angle)); + occlusion *= 1.0 / (1 + length(last_diff)); + accum += occlusion; +} + +// Normalize samples +accum /= num_angles; +result = 1 - accum; diff --git a/RenderPipelineFile/rpplugins/ao/shader/resolve_ao.frag.glsl b/RenderPipelineFile/rpplugins/ao/shader/resolve_ao.frag.glsl index 952967cd..f44f2acc 100644 --- a/RenderPipelineFile/rpplugins/ao/shader/resolve_ao.frag.glsl +++ b/RenderPipelineFile/rpplugins/ao/shader/resolve_ao.frag.glsl @@ -1,56 +1,56 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" - -#define RS_KEEP_GOOD_DURATION float(GET_SETTING(ao, clip_length) * 1.3) -#define RS_USE_POSITION_TECHNIQUE 1 -#define RS_DISTANCE_SCALE 1.0 - -#pragma include "includes/temporal_resolve.inc.glsl" - -uniform sampler2D CurrentTex; -uniform sampler2D CombinedVelocity; -uniform sampler2D Previous_AmbientOcclusion; - -out vec4 result; - -void main() { - vec2 texcoord = get_texcoord(); - - #if GET_SETTING(ao, clip_length) < 1 - // No reprojection needed without temporal ao - result = textureLod(CurrentTex, texcoord, 0).xyz; - #else - vec2 velocity = textureLod(CombinedVelocity, texcoord, 0).xy; - vec2 last_coord = texcoord + velocity; - - result = resolve_temporal( - CurrentTex, Previous_AmbientOcclusion, texcoord, last_coord); - #endif -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" + +#define RS_KEEP_GOOD_DURATION float(GET_SETTING(ao, clip_length) * 1.3) +#define RS_USE_POSITION_TECHNIQUE 1 +#define RS_DISTANCE_SCALE 1.0 + +#pragma include "includes/temporal_resolve.inc.glsl" + +uniform sampler2D CurrentTex; +uniform sampler2D CombinedVelocity; +uniform sampler2D Previous_AmbientOcclusion; + +out vec4 result; + +void main() { + vec2 texcoord = get_texcoord(); + + #if GET_SETTING(ao, clip_length) < 1 + // No reprojection needed without temporal ao + result = textureLod(CurrentTex, texcoord, 0).xyz; + #else + vec2 velocity = textureLod(CombinedVelocity, texcoord, 0).xy; + vec2 last_coord = texcoord + velocity; + + result = resolve_temporal( + CurrentTex, Previous_AmbientOcclusion, texcoord, last_coord); + #endif +} diff --git a/RenderPipelineFile/rpplugins/ao/shader/small_scale_ao.frag.glsl b/RenderPipelineFile/rpplugins/ao/shader/small_scale_ao.frag.glsl index 4ddfd953..e3abcbba 100644 --- a/RenderPipelineFile/rpplugins/ao/shader/small_scale_ao.frag.glsl +++ b/RenderPipelineFile/rpplugins/ao/shader/small_scale_ao.frag.glsl @@ -1,123 +1,123 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma optionNV (unroll all) - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/transforms.inc.glsl" -#pragma include "includes/noise.inc.glsl" -#pragma include "includes/poisson_disk.inc.glsl" - -out float result; - -uniform sampler2D DownscaledDepth; -uniform sampler2D AOResult; - -// use the extended gbuffer api -#define USE_GBUFFER_EXTENSIONS 1 -#pragma include "includes/gbuffer.inc.glsl" - -float get_linear_depth_at(vec2 coord) { - return textureLod(DownscaledDepth, coord, 0).x; -} - -void main() { - - ivec2 coord = ivec2(gl_FragCoord.xy); - - // Provide some variables to the kernel - vec2 screen_size = vec2(WINDOW_WIDTH, WINDOW_HEIGHT); - vec2 pixel_size = vec2(1.0) / screen_size; - vec2 texcoord = get_texcoord(); - float pixel_z = get_linear_depth_at(texcoord); - - // Merge with previous ao result - float prev_result = textureLod(AOResult, texcoord, 0).x; - - // Fade out small scale ao - if (pixel_z > 150.0) { - result = prev_result; - return; - } - - vec3 noise_vec = rand_rgb(coord % 4 + - 0.01 * (MainSceneData.frame_index % (GET_SETTING(ao, clip_length)))); - - vec3 pixel_view_normal = get_view_normal(texcoord); - vec3 pixel_view_pos = get_view_pos_at(texcoord); - - float kernel_scale = min(5.0, 10.0 / pixel_z); - const float sample_radius = 6.0; - - const int num_samples = 4; - const float bias = 0.0005 + 0.01 / kernel_scale; - float max_range = 0.2; - - float sample_offset = sample_radius * pixel_size.x * 30.0; - float range_accum = 0.0; - float accum = 0.0; - - sample_offset /= kernel_scale; - - for (int i = 0; i < num_samples; ++i) { - vec3 offset = poisson_3D_16[4 * i]; - // offset = mix(offset, noise_vec, 0.5); - offset = normalize(offset + noise_vec); - - // Flip offset in case it faces away from the normal - offset = face_forward(offset, pixel_view_normal); - - // Compute offset position in view space - vec3 offset_pos = pixel_view_pos + offset * sample_offset; - - // Project offset position to screen space - vec3 projected = view_to_screen(offset_pos); - - // Fetch the expected depth - float sample_depth = get_linear_depth_at(projected.xy); - // float sample_depth = get_linear_z_from_z(get_depth_at(projected.xy)); - - // Linearize both depths - float linz_a = get_linear_z_from_z(projected.z); - float linz_b = sample_depth; - - // Compare both depths by distance to find the AO factor - float modifier = step(distance(linz_a, linz_b), max_range); - range_accum += fma(modifier, 0.5, 0.5); - modifier *= step(linz_b + bias, linz_a); - accum += modifier; - } - - // Normalize samples - accum /= max(0.1, range_accum); - accum = 1 - accum; - accum = pow(accum, 5.0); - prev_result *= accum; - // prev_result = pow(accum, 9.0); - result = prev_result; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma optionNV (unroll all) + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/transforms.inc.glsl" +#pragma include "includes/noise.inc.glsl" +#pragma include "includes/poisson_disk.inc.glsl" + +out float result; + +uniform sampler2D DownscaledDepth; +uniform sampler2D AOResult; + +// use the extended gbuffer api +#define USE_GBUFFER_EXTENSIONS 1 +#pragma include "includes/gbuffer.inc.glsl" + +float get_linear_depth_at(vec2 coord) { + return textureLod(DownscaledDepth, coord, 0).x; +} + +void main() { + + ivec2 coord = ivec2(gl_FragCoord.xy); + + // Provide some variables to the kernel + vec2 screen_size = vec2(WINDOW_WIDTH, WINDOW_HEIGHT); + vec2 pixel_size = vec2(1.0) / screen_size; + vec2 texcoord = get_texcoord(); + float pixel_z = get_linear_depth_at(texcoord); + + // Merge with previous ao result + float prev_result = textureLod(AOResult, texcoord, 0).x; + + // Fade out small scale ao + if (pixel_z > 150.0) { + result = prev_result; + return; + } + + vec3 noise_vec = rand_rgb(coord % 4 + + 0.01 * (MainSceneData.frame_index % (GET_SETTING(ao, clip_length)))); + + vec3 pixel_view_normal = get_view_normal(texcoord); + vec3 pixel_view_pos = get_view_pos_at(texcoord); + + float kernel_scale = min(5.0, 10.0 / pixel_z); + const float sample_radius = 6.0; + + const int num_samples = 4; + const float bias = 0.0005 + 0.01 / kernel_scale; + float max_range = 0.2; + + float sample_offset = sample_radius * pixel_size.x * 30.0; + float range_accum = 0.0; + float accum = 0.0; + + sample_offset /= kernel_scale; + + for (int i = 0; i < num_samples; ++i) { + vec3 offset = poisson_3D_16[4 * i]; + // offset = mix(offset, noise_vec, 0.5); + offset = normalize(offset + noise_vec); + + // Flip offset in case it faces away from the normal + offset = face_forward(offset, pixel_view_normal); + + // Compute offset position in view space + vec3 offset_pos = pixel_view_pos + offset * sample_offset; + + // Project offset position to screen space + vec3 projected = view_to_screen(offset_pos); + + // Fetch the expected depth + float sample_depth = get_linear_depth_at(projected.xy); + // float sample_depth = get_linear_z_from_z(get_depth_at(projected.xy)); + + // Linearize both depths + float linz_a = get_linear_z_from_z(projected.z); + float linz_b = sample_depth; + + // Compare both depths by distance to find the AO factor + float modifier = step(distance(linz_a, linz_b), max_range); + range_accum += fma(modifier, 0.5, 0.5); + modifier *= step(linz_b + bias, linz_a); + accum += modifier; + } + + // Normalize samples + accum /= max(0.1, range_accum); + accum = 1 - accum; + accum = pow(accum, 5.0); + prev_result *= accum; + // prev_result = pow(accum, 9.0); + result = prev_result; +} diff --git a/RenderPipelineFile/rpplugins/ao/shader/ssao.kernel.glsl b/RenderPipelineFile/rpplugins/ao/shader/ssao.kernel.glsl index e41567b3..d87dc1f0 100644 --- a/RenderPipelineFile/rpplugins/ao/shader/ssao.kernel.glsl +++ b/RenderPipelineFile/rpplugins/ao/shader/ssao.kernel.glsl @@ -1,83 +1,83 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -/* - -CryEngine SSAO - Screen Space Ambient Occlusion - -Samples the depth buffer in a hemisphere arround the current pixel to approximate -AO. - -*/ - -const float sample_radius = GET_SETTING(ao, ssao_sample_radius); -const float bias = GET_SETTING(ao, ssao_bias) * 0.5 / kernel_scale; -float max_range = GET_SETTING(ao, ssao_max_distance); - -float sample_offset = sample_radius * pixel_size.x * 30.0; -float range_accum = 0.0; -float accum = 0.0; - -// Make sure we have nice sampling coherency by scaling the kernel -sample_offset /= 0.8 * kernel_scale; - -START_ITERATE_SEQUENCE(ao, ssao_sequence, vec3 offset) - - offset = mix(offset, noise_vec, 0.5); - offset *= 0.6; - - // Flip offset in case it faces away from the normal - offset = face_forward(offset, pixel_view_normal); - - // Compute offset position in view space - vec3 offset_pos = pixel_view_pos + offset * sample_offset; - - // Project offset position to screen space - vec3 projected = view_to_screen(offset_pos); - - // Fetch the expected depth - float sample_depth = get_linear_depth_at(projected.xy); - - // Linearize both depths - float linz_a = get_linear_z_from_z(projected.z); - float linz_b = sample_depth; - - // Compare both depths by distance to find the AO factor - float modifier = step(distance(linz_a, linz_b), max_range * 0.2); - range_accum += fma(modifier, 0.5, 0.5); - modifier *= step(linz_b + bias, linz_a); - accum += modifier; - -END_ITERATE_SEQUENCE() - - -// normalize samples -accum /= max(0.1, range_accum); - -// Renormalize to match with the other techniques -accum *= 0.4; -result = 1 - accum; +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +/* + +CryEngine SSAO - Screen Space Ambient Occlusion + +Samples the depth buffer in a hemisphere arround the current pixel to approximate +AO. + +*/ + +const float sample_radius = GET_SETTING(ao, ssao_sample_radius); +const float bias = GET_SETTING(ao, ssao_bias) * 0.5 / kernel_scale; +float max_range = GET_SETTING(ao, ssao_max_distance); + +float sample_offset = sample_radius * pixel_size.x * 30.0; +float range_accum = 0.0; +float accum = 0.0; + +// Make sure we have nice sampling coherency by scaling the kernel +sample_offset /= 0.8 * kernel_scale; + +START_ITERATE_SEQUENCE(ao, ssao_sequence, vec3 offset) + + offset = mix(offset, noise_vec, 0.5); + offset *= 0.6; + + // Flip offset in case it faces away from the normal + offset = face_forward(offset, pixel_view_normal); + + // Compute offset position in view space + vec3 offset_pos = pixel_view_pos + offset * sample_offset; + + // Project offset position to screen space + vec3 projected = view_to_screen(offset_pos); + + // Fetch the expected depth + float sample_depth = get_linear_depth_at(projected.xy); + + // Linearize both depths + float linz_a = get_linear_z_from_z(projected.z); + float linz_b = sample_depth; + + // Compare both depths by distance to find the AO factor + float modifier = step(distance(linz_a, linz_b), max_range * 0.2); + range_accum += fma(modifier, 0.5, 0.5); + modifier *= step(linz_b + bias, linz_a); + accum += modifier; + +END_ITERATE_SEQUENCE() + + +// normalize samples +accum /= max(0.1, range_accum); + +// Renormalize to match with the other techniques +accum *= 0.4; +result = 1 - accum; diff --git a/RenderPipelineFile/rpplugins/ao/shader/ssvo.kernel.glsl b/RenderPipelineFile/rpplugins/ao/shader/ssvo.kernel.glsl index ec44af18..fc8d3235 100644 --- a/RenderPipelineFile/rpplugins/ao/shader/ssvo.kernel.glsl +++ b/RenderPipelineFile/rpplugins/ao/shader/ssvo.kernel.glsl @@ -1,100 +1,100 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -/* - -SSVO - Screen Space Volumetric Obscurance - -This algorithm casts rays to a sphere arround the current point in view space, -and approximates the spheres volume by using line intetrals. The percentage -of the spheres volume is then used to compute AO. - -*/ - - - -vec2 sphere_radius = GET_SETTING(ao, ssvo_sphere_radius) * pixel_size; -float max_depth_diff = GET_SETTING(ao, ssvo_max_distance) / kernel_scale; - -float accum = 0.0; -float pixel_linz = get_linear_z_from_z(pixel_depth); - -START_ITERATE_SEQUENCE(ao, ssvo_sequence, vec2 offset) - - offset = mix(offset, noise_vec.xy, 0.3); - - vec2 offc = offset * sphere_radius * 5.0 * kernel_scale; - - // Use paired samples, this enables us to hide depth buffer discontinuities - vec2 offcoord_a = texcoord + offc; - vec2 offcoord_b = texcoord - offc; - - // Compute the sphere height at the sample location - float sphere_height = sqrt(1 - dot(offset, offset)); - - // Get the depth at the sample locations, also - // make the depth linear, this enables us to compare them better - float depth_linz_a = get_linear_depth_at(offcoord_a); - float depth_linz_b = get_linear_depth_at(offcoord_b); - - // Clamp both differences to the maximum depth difference - float diff_a = (pixel_linz - depth_linz_a) / max_depth_diff; - float diff_b = (pixel_linz - depth_linz_b) / max_depth_diff; - - // Compute the line integrals of boths, this is simply the height of the - // line divided by the sphere height. However, we need to substract the - // sphere height once, since we didn't start at the sphere top, but at - // the sphere mid (since we took the depth of point p which is the center - // of the sphere). - float volume_a = (sphere_height - diff_a) / (2.0 * sphere_height); - float volume_b = (sphere_height - diff_b) / (2.0 * sphere_height); - - // Check if the volumes are valid - bool valid_a = diff_a <= sphere_height && diff_a >= -sphere_height; - bool valid_b = diff_b <= sphere_height && diff_b >= -sphere_height; - - // In case either the first or second sample is valid, we can weight them - if (valid_a || valid_b) { - - // Because we use paired samples, we can easily account for discontinuities: - // If a is invalid, we can take the inverse of b as value for a, and vice-versa. - // This works out quite well, even if not mathematically correct. - accum += valid_a ? volume_a : 1 - volume_b; - accum += valid_b ? volume_b : 1 - volume_a; - - // In case both samples are invalid, theres nothing we can do. Just increase - // the integral in this case. - } else { - accum += 1.0; - } - -END_ITERATE_SEQUENCE(); - -NORMALIZE_SEQUENCE(ao, ssvo_sequence, accum); - - -result = accum; +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +/* + +SSVO - Screen Space Volumetric Obscurance + +This algorithm casts rays to a sphere arround the current point in view space, +and approximates the spheres volume by using line intetrals. The percentage +of the spheres volume is then used to compute AO. + +*/ + + + +vec2 sphere_radius = GET_SETTING(ao, ssvo_sphere_radius) * pixel_size; +float max_depth_diff = GET_SETTING(ao, ssvo_max_distance) / kernel_scale; + +float accum = 0.0; +float pixel_linz = get_linear_z_from_z(pixel_depth); + +START_ITERATE_SEQUENCE(ao, ssvo_sequence, vec2 offset) + + offset = mix(offset, noise_vec.xy, 0.3); + + vec2 offc = offset * sphere_radius * 5.0 * kernel_scale; + + // Use paired samples, this enables us to hide depth buffer discontinuities + vec2 offcoord_a = texcoord + offc; + vec2 offcoord_b = texcoord - offc; + + // Compute the sphere height at the sample location + float sphere_height = sqrt(1 - dot(offset, offset)); + + // Get the depth at the sample locations, also + // make the depth linear, this enables us to compare them better + float depth_linz_a = get_linear_depth_at(offcoord_a); + float depth_linz_b = get_linear_depth_at(offcoord_b); + + // Clamp both differences to the maximum depth difference + float diff_a = (pixel_linz - depth_linz_a) / max_depth_diff; + float diff_b = (pixel_linz - depth_linz_b) / max_depth_diff; + + // Compute the line integrals of boths, this is simply the height of the + // line divided by the sphere height. However, we need to substract the + // sphere height once, since we didn't start at the sphere top, but at + // the sphere mid (since we took the depth of point p which is the center + // of the sphere). + float volume_a = (sphere_height - diff_a) / (2.0 * sphere_height); + float volume_b = (sphere_height - diff_b) / (2.0 * sphere_height); + + // Check if the volumes are valid + bool valid_a = diff_a <= sphere_height && diff_a >= -sphere_height; + bool valid_b = diff_b <= sphere_height && diff_b >= -sphere_height; + + // In case either the first or second sample is valid, we can weight them + if (valid_a || valid_b) { + + // Because we use paired samples, we can easily account for discontinuities: + // If a is invalid, we can take the inverse of b as value for a, and vice-versa. + // This works out quite well, even if not mathematically correct. + accum += valid_a ? volume_a : 1 - volume_b; + accum += valid_b ? volume_b : 1 - volume_a; + + // In case both samples are invalid, theres nothing we can do. Just increase + // the integral in this case. + } else { + accum += 1.0; + } + +END_ITERATE_SEQUENCE(); + +NORMALIZE_SEQUENCE(ao, ssvo_sequence, accum); + + +result = accum; diff --git a/RenderPipelineFile/rpplugins/ao/shader/ue4ao.kernel.glsl b/RenderPipelineFile/rpplugins/ao/shader/ue4ao.kernel.glsl index 49f88f37..00f6b5b5 100644 --- a/RenderPipelineFile/rpplugins/ao/shader/ue4ao.kernel.glsl +++ b/RenderPipelineFile/rpplugins/ao/shader/ue4ao.kernel.glsl @@ -1,95 +1,95 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - - -/* - -UE4AO - Unreal Engine 4 AO - -This is an ambient occlusion technique proposed by Unreal Engine. It is very -similar to Alchemy AO, but uses paired samples to approximate the average -unocluded vector. - -*/ - -const float sample_radius = GET_SETTING(ao, ue4ao_sample_radius); -const float max_distance = GET_SETTING(ao, ue4ao_max_distance); - -float accum = 0.0; -float accum_count = 0.0; - -vec2 offset_scale = pixel_size * sample_radius * kernel_scale * 0.4; - -START_ITERATE_SEQUENCE(ao, ue4ao_sample_sequence, vec2 offset) - - offset = mix(offset, noise_vec.xy, 0.3); - vec2 offcoord = offset * offset_scale; - - // Get offset coordinates - vec2 texc_a = texcoord + offcoord; - vec2 texc_b = texcoord - offcoord; - - // Get view position at that offsets - vec3 off_pos_a = get_view_pos_at(texc_a); - vec3 off_pos_b = get_view_pos_at(texc_b); - - // Get the vector s-p to that sample positions - vec3 sample_vec_a = normalize(off_pos_a - pixel_view_pos); - vec3 sample_vec_b = normalize(off_pos_b - pixel_view_pos); - - // Get distances - float dist_a = distance(off_pos_a, pixel_view_pos) / max_distance; - float dist_b = distance(off_pos_b, pixel_view_pos) / max_distance; - - // Check if the samples are valid - float valid_a = step(dist_a - 1, 0.0); - float valid_b = step(dist_b - 1, 0.0); - - float angle_a = max(0, dot(sample_vec_a, pixel_view_normal)); - float angle_b = max(0, dot(sample_vec_b, pixel_view_normal)); - - // TODO: Avoid branching by every means - if (valid_a != valid_b) { - angle_a = mix(-angle_b, angle_a, valid_a); - angle_b = mix(angle_a, -angle_b, valid_b); - dist_a = mix(dist_b, dist_a, valid_a); - dist_b = mix(dist_a, dist_b, valid_b); - } - - // In case any sample is valid - if (valid_a > 0.5 || valid_b > 0.5) { - accum += (angle_a+angle_b) * 0.25 * (2 - (dist_a+dist_b)); - accum_count += 1.0; - } else { - accum_count += 0.5; - - } - -END_ITERATE_SEQUENCE(); - -accum /= max(1.0, accum_count); -result = 1 - accum; +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + + +/* + +UE4AO - Unreal Engine 4 AO + +This is an ambient occlusion technique proposed by Unreal Engine. It is very +similar to Alchemy AO, but uses paired samples to approximate the average +unocluded vector. + +*/ + +const float sample_radius = GET_SETTING(ao, ue4ao_sample_radius); +const float max_distance = GET_SETTING(ao, ue4ao_max_distance); + +float accum = 0.0; +float accum_count = 0.0; + +vec2 offset_scale = pixel_size * sample_radius * kernel_scale * 0.4; + +START_ITERATE_SEQUENCE(ao, ue4ao_sample_sequence, vec2 offset) + + offset = mix(offset, noise_vec.xy, 0.3); + vec2 offcoord = offset * offset_scale; + + // Get offset coordinates + vec2 texc_a = texcoord + offcoord; + vec2 texc_b = texcoord - offcoord; + + // Get view position at that offsets + vec3 off_pos_a = get_view_pos_at(texc_a); + vec3 off_pos_b = get_view_pos_at(texc_b); + + // Get the vector s-p to that sample positions + vec3 sample_vec_a = normalize(off_pos_a - pixel_view_pos); + vec3 sample_vec_b = normalize(off_pos_b - pixel_view_pos); + + // Get distances + float dist_a = distance(off_pos_a, pixel_view_pos) / max_distance; + float dist_b = distance(off_pos_b, pixel_view_pos) / max_distance; + + // Check if the samples are valid + float valid_a = step(dist_a - 1, 0.0); + float valid_b = step(dist_b - 1, 0.0); + + float angle_a = max(0, dot(sample_vec_a, pixel_view_normal)); + float angle_b = max(0, dot(sample_vec_b, pixel_view_normal)); + + // TODO: Avoid branching by every means + if (valid_a != valid_b) { + angle_a = mix(-angle_b, angle_a, valid_a); + angle_b = mix(angle_a, -angle_b, valid_b); + dist_a = mix(dist_b, dist_a, valid_a); + dist_b = mix(dist_a, dist_b, valid_b); + } + + // In case any sample is valid + if (valid_a > 0.5 || valid_b > 0.5) { + accum += (angle_a+angle_b) * 0.25 * (2 - (dist_a+dist_b)); + accum_count += 1.0; + } else { + accum_count += 0.5; + + } + +END_ITERATE_SEQUENCE(); + +accum /= max(1.0, accum_count); +result = 1 - accum; diff --git a/RenderPipelineFile/rpplugins/bloom/bloom_stage.py b/RenderPipelineFile/rpplugins/bloom/bloom_stage.py index 9e8bed35..43edb51b 100644 --- a/RenderPipelineFile/rpplugins/bloom/bloom_stage.py +++ b/RenderPipelineFile/rpplugins/bloom/bloom_stage.py @@ -1,123 +1,123 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from six.moves import range # pylint: disable=import-error - -from panda3d.core import SamplerState, Vec4 - -from rpcore.globals import Globals -from rpcore.render_stage import RenderStage -from rpcore.image import Image - - -class BloomStage(RenderStage): - - required_pipes = ["ShadedScene"] - required_inputs = [] - - def __init__(self, pipeline): - RenderStage.__init__(self, pipeline) - self.num_mips = 6 - self.remove_fireflies = False - - @property - def produced_pipes(self): - return {"ShadedScene": self.target_apply.color_tex} - - def create(self): - - if self.remove_fireflies: - self.target_firefly = self.create_target("RemoveFireflies") - self.target_firefly.add_color_attachment(bits=16) - self.target_firefly.prepare_buffer() - - self.scene_target_img = Image.create_2d("BloomDownsample", 0, 0, "RGBA16") - self.scene_target_img.set_minfilter(SamplerState.FT_linear_mipmap_linear) - self.scene_target_img.set_magfilter(SamplerState.FT_linear) - self.scene_target_img.set_wrap_u(SamplerState.WM_clamp) - self.scene_target_img.set_wrap_v(SamplerState.WM_clamp) - self.scene_target_img.set_clear_color(Vec4(0.1, 0.0, 0.0, 1.0)) - self.scene_target_img.clear_image() - - self.target_extract = self.create_target("ExtractBrightSpots") - self.target_extract.prepare_buffer() - self.target_extract.set_shader_input("DestTex", self.scene_target_img, False, True, -1, 0) - - if self.remove_fireflies: - self.target_extract.set_shader_input("ShadedScene", self.target_firefly.color_tex, 1000) - - self.downsample_targets = [] - self.upsample_targets = [] - - # Downsample passes - for i in range(self.num_mips): - scale_multiplier = 2 ** (1 + i) - target = self.create_target("Downsample:Step-" + str(i)) - target.size = -scale_multiplier, -scale_multiplier - target.prepare_buffer() - target.set_shader_inputs( - sourceMip=i, - SourceTex=self.scene_target_img) - target.set_shader_input("DestTex", self.scene_target_img, False, True, -1, i + 1) - self.downsample_targets.append(target) - - # Upsample passes - for i in range(self.num_mips): - scale_multiplier = 2 ** (self.num_mips - i - 1) - target = self.create_target("Upsample:Step-" + str(i)) - target.size = -scale_multiplier, -scale_multiplier - target.prepare_buffer() - target.set_shader_inputs( - FirstUpsamplePass=(i==0), - sourceMip=(self.num_mips - i), - SourceTex=self.scene_target_img) - target.set_shader_input("DestTex", self.scene_target_img, - False, True, -1, self.num_mips - i - 1) - self.upsample_targets.append(target) - - self.target_apply = self.create_target("ApplyBloom") - self.target_apply.add_color_attachment(bits=16) - self.target_apply.prepare_buffer() - self.target_apply.set_shader_input("BloomTex", self.scene_target_img) - - def set_dimensions(self): - self.scene_target_img.set_x_size(Globals.resolution.x) - self.scene_target_img.set_y_size(Globals.resolution.y) - - def reload_shaders(self): - self.target_extract.shader = self.load_plugin_shader("extract_bright_spots.frag.glsl") - - if self.remove_fireflies: - self.target_firefly.shader = self.load_plugin_shader("remove_fireflies.frag.glsl") - self.target_apply.shader = self.load_plugin_shader("apply_bloom.frag.glsl") - - downsample_shader = self.load_plugin_shader("bloom_downsample.frag.glsl") - for target in self.downsample_targets: - target.shader = downsample_shader - - upsample_shader = self.load_plugin_shader("bloom_upsample.frag.glsl") - for target in self.upsample_targets: - target.shader = upsample_shader +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from six.moves import range # pylint: disable=import-error + +from panda3d.core import SamplerState, Vec4 + +from rpcore.globals import Globals +from rpcore.render_stage import RenderStage +from rpcore.image import Image + + +class BloomStage(RenderStage): + + required_pipes = ["ShadedScene"] + required_inputs = [] + + def __init__(self, pipeline): + RenderStage.__init__(self, pipeline) + self.num_mips = 6 + self.remove_fireflies = False + + @property + def produced_pipes(self): + return {"ShadedScene": self.target_apply.color_tex} + + def create(self): + + if self.remove_fireflies: + self.target_firefly = self.create_target("RemoveFireflies") + self.target_firefly.add_color_attachment(bits=16) + self.target_firefly.prepare_buffer() + + self.scene_target_img = Image.create_2d("BloomDownsample", 0, 0, "RGBA16") + self.scene_target_img.set_minfilter(SamplerState.FT_linear_mipmap_linear) + self.scene_target_img.set_magfilter(SamplerState.FT_linear) + self.scene_target_img.set_wrap_u(SamplerState.WM_clamp) + self.scene_target_img.set_wrap_v(SamplerState.WM_clamp) + self.scene_target_img.set_clear_color(Vec4(0.1, 0.0, 0.0, 1.0)) + self.scene_target_img.clear_image() + + self.target_extract = self.create_target("ExtractBrightSpots") + self.target_extract.prepare_buffer() + self.target_extract.set_shader_input("DestTex", self.scene_target_img, False, True, -1, 0) + + if self.remove_fireflies: + self.target_extract.set_shader_input("ShadedScene", self.target_firefly.color_tex, 1000) + + self.downsample_targets = [] + self.upsample_targets = [] + + # Downsample passes + for i in range(self.num_mips): + scale_multiplier = 2 ** (1 + i) + target = self.create_target("Downsample:Step-" + str(i)) + target.size = -scale_multiplier, -scale_multiplier + target.prepare_buffer() + target.set_shader_inputs( + sourceMip=i, + SourceTex=self.scene_target_img) + target.set_shader_input("DestTex", self.scene_target_img, False, True, -1, i + 1) + self.downsample_targets.append(target) + + # Upsample passes + for i in range(self.num_mips): + scale_multiplier = 2 ** (self.num_mips - i - 1) + target = self.create_target("Upsample:Step-" + str(i)) + target.size = -scale_multiplier, -scale_multiplier + target.prepare_buffer() + target.set_shader_inputs( + FirstUpsamplePass=(i==0), + sourceMip=(self.num_mips - i), + SourceTex=self.scene_target_img) + target.set_shader_input("DestTex", self.scene_target_img, + False, True, -1, self.num_mips - i - 1) + self.upsample_targets.append(target) + + self.target_apply = self.create_target("ApplyBloom") + self.target_apply.add_color_attachment(bits=16) + self.target_apply.prepare_buffer() + self.target_apply.set_shader_input("BloomTex", self.scene_target_img) + + def set_dimensions(self): + self.scene_target_img.set_x_size(Globals.resolution.x) + self.scene_target_img.set_y_size(Globals.resolution.y) + + def reload_shaders(self): + self.target_extract.shader = self.load_plugin_shader("extract_bright_spots.frag.glsl") + + if self.remove_fireflies: + self.target_firefly.shader = self.load_plugin_shader("remove_fireflies.frag.glsl") + self.target_apply.shader = self.load_plugin_shader("apply_bloom.frag.glsl") + + downsample_shader = self.load_plugin_shader("bloom_downsample.frag.glsl") + for target in self.downsample_targets: + target.shader = downsample_shader + + upsample_shader = self.load_plugin_shader("bloom_upsample.frag.glsl") + for target in self.upsample_targets: + target.shader = upsample_shader diff --git a/RenderPipelineFile/rpplugins/bloom/config.yaml b/RenderPipelineFile/rpplugins/bloom/config.yaml index 2968f305..c113cfcc 100644 --- a/RenderPipelineFile/rpplugins/bloom/config.yaml +++ b/RenderPipelineFile/rpplugins/bloom/config.yaml @@ -1,42 +1,42 @@ - -settings: !!omap - - - num_mipmaps: - type: int - range: [2, 10] - default: 8 - label: Mipmap count - description: > - Amount of mipmaps to use for downsampling the scene. A higher - amount of mipmaps will allow capturing wider blur radii. - - - bloom_strength: - type: float - range: [0.01, 10.0] - default: 1.0 - shader_runtime: true - label: Bloom Strength - description: > - Strength of the bloom, higher values will cause more bloom. Make sure - to not overuse this. - - - remove_fireflies: - type: bool - default: false - label: Remove fireflies - description: > - Attempts to reduce flickering by darkening very small bright spots. - This uses the Karis average to reduce often called "fireflies", which are very - bright pixels, by weighting them by their brightness. - - - lens_dirt_factor: - type: float - range: [0, 1] - default: 0.7 - shader_runtime: true - label: Lens dirt factor - description: > - Amount of lens dirt to blend the bloom with. A value of 0 means - no lens dirt, whereas a value of 1 means very intense lens dirt. - -daytime_settings: + +settings: !!omap + + - num_mipmaps: + type: int + range: [2, 10] + default: 8 + label: Mipmap count + description: > + Amount of mipmaps to use for downsampling the scene. A higher + amount of mipmaps will allow capturing wider blur radii. + + - bloom_strength: + type: float + range: [0.01, 10.0] + default: 1.0 + shader_runtime: true + label: Bloom Strength + description: > + Strength of the bloom, higher values will cause more bloom. Make sure + to not overuse this. + + - remove_fireflies: + type: bool + default: false + label: Remove fireflies + description: > + Attempts to reduce flickering by darkening very small bright spots. + This uses the Karis average to reduce often called "fireflies", which are very + bright pixels, by weighting them by their brightness. + + - lens_dirt_factor: + type: float + range: [0, 1] + default: 0.7 + shader_runtime: true + label: Lens dirt factor + description: > + Amount of lens dirt to blend the bloom with. A value of 0 means + no lens dirt, whereas a value of 1 means very intense lens dirt. + +daytime_settings: diff --git a/RenderPipelineFile/rpplugins/bloom/plugin.py b/RenderPipelineFile/rpplugins/bloom/plugin.py index 800195e9..0598ae7b 100644 --- a/RenderPipelineFile/rpplugins/bloom/plugin.py +++ b/RenderPipelineFile/rpplugins/bloom/plugin.py @@ -1,48 +1,48 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.loader import RPLoader -from rpcore.pluginbase.base_plugin import BasePlugin - -from .bloom_stage import BloomStage - - -class Plugin(BasePlugin): - - name = "Bloom" - author = "tobspr " - description = ("This plugin adds support for a technique called Bloom, which " - "makes very bright objects like the sun have a halo.") - version = "1.2" - - def on_stage_setup(self): - self._bloom_stage = self.create_stage(BloomStage) - self._bloom_stage.num_mips = self.get_setting("num_mipmaps") - self._bloom_stage.remove_fireflies = self.get_setting("remove_fireflies") - - def on_pipeline_created(self): - dirt_tex = RPLoader.load_texture(self.get_resource("lens_dirt.txo")) - self._bloom_stage.set_shader_input("LensDirtTex", dirt_tex) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.loader import RPLoader +from rpcore.pluginbase.base_plugin import BasePlugin + +from .bloom_stage import BloomStage + + +class Plugin(BasePlugin): + + name = "Bloom" + author = "tobspr " + description = ("This plugin adds support for a technique called Bloom, which " + "makes very bright objects like the sun have a halo.") + version = "1.2" + + def on_stage_setup(self): + self._bloom_stage = self.create_stage(BloomStage) + self._bloom_stage.num_mips = self.get_setting("num_mipmaps") + self._bloom_stage.remove_fireflies = self.get_setting("remove_fireflies") + + def on_pipeline_created(self): + dirt_tex = RPLoader.load_texture(self.get_resource("lens_dirt.txo")) + self._bloom_stage.set_shader_input("LensDirtTex", dirt_tex) diff --git a/RenderPipelineFile/rpplugins/bloom/resources/SOURCE.txt b/RenderPipelineFile/rpplugins/bloom/resources/SOURCE.txt index 87ba1c25..d729714b 100644 --- a/RenderPipelineFile/rpplugins/bloom/resources/SOURCE.txt +++ b/RenderPipelineFile/rpplugins/bloom/resources/SOURCE.txt @@ -1,2 +1,2 @@ -Filter provided by UNIGINE -http://unigine.com/en/articles/procedural-content-generation2 +Filter provided by UNIGINE +http://unigine.com/en/articles/procedural-content-generation2 diff --git a/RenderPipelineFile/rpplugins/bloom/resources/lens_dirt.ffxml b/RenderPipelineFile/rpplugins/bloom/resources/lens_dirt.ffxml index 6a7beddf..44aba34a 100644 --- a/RenderPipelineFile/rpplugins/bloom/resources/lens_dirt.ffxml +++ b/RenderPipelineFile/rpplugins/bloom/resources/lens_dirt.ffxml @@ -1,702 +1,702 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - -
- - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RenderPipelineFile/rpplugins/bloom/shader/apply_bloom.frag.glsl b/RenderPipelineFile/rpplugins/bloom/shader/apply_bloom.frag.glsl index 8d5382fe..8d0adbea 100644 --- a/RenderPipelineFile/rpplugins/bloom/shader/apply_bloom.frag.glsl +++ b/RenderPipelineFile/rpplugins/bloom/shader/apply_bloom.frag.glsl @@ -1,53 +1,53 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/color_spaces.inc.glsl" - -uniform sampler2D ShadedScene; -uniform sampler2D BloomTex; -uniform sampler2D LensDirtTex; - -out vec3 result; - -void main() { - vec2 texcoord = get_texcoord(); - - vec3 scene_result = textureLod(ShadedScene, texcoord, 0).xyz; - vec3 bloom_result = textureLod(BloomTex, texcoord, 0).xyz; - - // Apply dirt - vec3 lens_dirt = textureLod(LensDirtTex, texcoord, 0).xyz; - float base_dirt_factor = GET_SETTING(bloom, lens_dirt_factor); - vec3 dirt_factor = pow(lens_dirt, vec3(1.0)); - - bloom_result = mix(bloom_result, bloom_result * dirt_factor, base_dirt_factor); - - // Blend scene color and bloom color - result = scene_result + bloom_result; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/color_spaces.inc.glsl" + +uniform sampler2D ShadedScene; +uniform sampler2D BloomTex; +uniform sampler2D LensDirtTex; + +out vec3 result; + +void main() { + vec2 texcoord = get_texcoord(); + + vec3 scene_result = textureLod(ShadedScene, texcoord, 0).xyz; + vec3 bloom_result = textureLod(BloomTex, texcoord, 0).xyz; + + // Apply dirt + vec3 lens_dirt = textureLod(LensDirtTex, texcoord, 0).xyz; + float base_dirt_factor = GET_SETTING(bloom, lens_dirt_factor); + vec3 dirt_factor = pow(lens_dirt, vec3(1.0)); + + bloom_result = mix(bloom_result, bloom_result * dirt_factor, base_dirt_factor); + + // Blend scene color and bloom color + result = scene_result + bloom_result; +} diff --git a/RenderPipelineFile/rpplugins/bloom/shader/bloom_downsample.frag.glsl b/RenderPipelineFile/rpplugins/bloom/shader/bloom_downsample.frag.glsl index 7466fb0a..a48352f9 100644 --- a/RenderPipelineFile/rpplugins/bloom/shader/bloom_downsample.frag.glsl +++ b/RenderPipelineFile/rpplugins/bloom/shader/bloom_downsample.frag.glsl @@ -1,96 +1,96 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" - -uniform int sourceMip; -uniform sampler2D SourceTex; -uniform writeonly image2D RESTRICT DestTex; - -void main() { - ivec2 int_coord = ivec2(gl_FragCoord.xy); - vec2 parent_tex_size = vec2(textureSize(SourceTex, sourceMip).xy); - vec2 texel_size = 1.0 / parent_tex_size; - - // Compute the floating point coordinate pointing to the exact center of the - // parent texel center - vec2 flt_coord = vec2(int_coord + 0.5) / parent_tex_size * 2.0; - - // Filter the image, see: - // http://fs5.directupload.net/images/151213/qfnexcls.png - vec3 center_sample = textureLod(SourceTex, flt_coord, sourceMip).xyz; - - // inner samples (marked red) - vec3 sample_r_tl = textureLod( - SourceTex, flt_coord + vec2(-1, 1) * texel_size, sourceMip).xyz; - vec3 sample_r_tr = textureLod( - SourceTex, flt_coord + vec2(1, 1) * texel_size, sourceMip).xyz; - vec3 sample_r_bl = textureLod( - SourceTex, flt_coord + vec2(-1, -1) * texel_size, sourceMip).xyz; - vec3 sample_r_br = textureLod( - SourceTex, flt_coord + vec2(1, -1) * texel_size, sourceMip).xyz; - - // corner samples - vec3 sample_t = textureLod( - SourceTex, flt_coord + vec2(0, 2) * texel_size, sourceMip).xyz; - vec3 sample_r = textureLod( - SourceTex, flt_coord + vec2(2, 0) * texel_size, sourceMip).xyz; - vec3 sample_b = textureLod( - SourceTex, flt_coord + vec2(0, -2) * texel_size, sourceMip).xyz; - vec3 sample_l = textureLod( - SourceTex, flt_coord + vec2(-2, 0) * texel_size, sourceMip).xyz; - - // edge samples - vec3 sample_tl = textureLod( - SourceTex, flt_coord + vec2(-2, 2) * texel_size, sourceMip).xyz; - vec3 sample_tr = textureLod( - SourceTex, flt_coord + vec2(2, 2) * texel_size, sourceMip).xyz; - vec3 sample_bl = textureLod( - SourceTex, flt_coord + vec2(-2, -2) * texel_size, sourceMip).xyz; - vec3 sample_br = textureLod( - SourceTex, flt_coord + vec2(2, -2) * texel_size, sourceMip).xyz; - - vec3 kernel_sum_red = sample_r_tl + sample_r_tr + sample_r_bl + sample_r_br; - vec3 kernel_sum_yellow = sample_tl + sample_t + sample_l + center_sample; - vec3 kernel_sum_green = sample_tr + sample_t + sample_r + center_sample; - vec3 kernel_sum_purple = sample_bl + sample_b + sample_l + center_sample; - vec3 kernel_sum_blue = sample_br + sample_b + sample_r + center_sample; - - vec3 summed_kernel = kernel_sum_red * 0.5 + kernel_sum_yellow * 0.125 + - kernel_sum_green * 0.125 + kernel_sum_purple * 0.125 + - kernel_sum_blue * 0.125; - - // since every sub-kernel has 4 samples, normalize that - summed_kernel /= 4.0; - - // Decay - // summed_kernel *= 0.92; - summed_kernel *= 1.3; - - imageStore(DestTex, ivec2(gl_FragCoord.xy), vec4(summed_kernel, 0)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" + +uniform int sourceMip; +uniform sampler2D SourceTex; +uniform writeonly image2D RESTRICT DestTex; + +void main() { + ivec2 int_coord = ivec2(gl_FragCoord.xy); + vec2 parent_tex_size = vec2(textureSize(SourceTex, sourceMip).xy); + vec2 texel_size = 1.0 / parent_tex_size; + + // Compute the floating point coordinate pointing to the exact center of the + // parent texel center + vec2 flt_coord = vec2(int_coord + 0.5) / parent_tex_size * 2.0; + + // Filter the image, see: + // http://fs5.directupload.net/images/151213/qfnexcls.png + vec3 center_sample = textureLod(SourceTex, flt_coord, sourceMip).xyz; + + // inner samples (marked red) + vec3 sample_r_tl = textureLod( + SourceTex, flt_coord + vec2(-1, 1) * texel_size, sourceMip).xyz; + vec3 sample_r_tr = textureLod( + SourceTex, flt_coord + vec2(1, 1) * texel_size, sourceMip).xyz; + vec3 sample_r_bl = textureLod( + SourceTex, flt_coord + vec2(-1, -1) * texel_size, sourceMip).xyz; + vec3 sample_r_br = textureLod( + SourceTex, flt_coord + vec2(1, -1) * texel_size, sourceMip).xyz; + + // corner samples + vec3 sample_t = textureLod( + SourceTex, flt_coord + vec2(0, 2) * texel_size, sourceMip).xyz; + vec3 sample_r = textureLod( + SourceTex, flt_coord + vec2(2, 0) * texel_size, sourceMip).xyz; + vec3 sample_b = textureLod( + SourceTex, flt_coord + vec2(0, -2) * texel_size, sourceMip).xyz; + vec3 sample_l = textureLod( + SourceTex, flt_coord + vec2(-2, 0) * texel_size, sourceMip).xyz; + + // edge samples + vec3 sample_tl = textureLod( + SourceTex, flt_coord + vec2(-2, 2) * texel_size, sourceMip).xyz; + vec3 sample_tr = textureLod( + SourceTex, flt_coord + vec2(2, 2) * texel_size, sourceMip).xyz; + vec3 sample_bl = textureLod( + SourceTex, flt_coord + vec2(-2, -2) * texel_size, sourceMip).xyz; + vec3 sample_br = textureLod( + SourceTex, flt_coord + vec2(2, -2) * texel_size, sourceMip).xyz; + + vec3 kernel_sum_red = sample_r_tl + sample_r_tr + sample_r_bl + sample_r_br; + vec3 kernel_sum_yellow = sample_tl + sample_t + sample_l + center_sample; + vec3 kernel_sum_green = sample_tr + sample_t + sample_r + center_sample; + vec3 kernel_sum_purple = sample_bl + sample_b + sample_l + center_sample; + vec3 kernel_sum_blue = sample_br + sample_b + sample_r + center_sample; + + vec3 summed_kernel = kernel_sum_red * 0.5 + kernel_sum_yellow * 0.125 + + kernel_sum_green * 0.125 + kernel_sum_purple * 0.125 + + kernel_sum_blue * 0.125; + + // since every sub-kernel has 4 samples, normalize that + summed_kernel /= 4.0; + + // Decay + // summed_kernel *= 0.92; + summed_kernel *= 1.3; + + imageStore(DestTex, ivec2(gl_FragCoord.xy), vec4(summed_kernel, 0)); +} diff --git a/RenderPipelineFile/rpplugins/bloom/shader/bloom_upsample.frag.glsl b/RenderPipelineFile/rpplugins/bloom/shader/bloom_upsample.frag.glsl index 341b2703..b4f4dd04 100644 --- a/RenderPipelineFile/rpplugins/bloom/shader/bloom_upsample.frag.glsl +++ b/RenderPipelineFile/rpplugins/bloom/shader/bloom_upsample.frag.glsl @@ -1,95 +1,95 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" - -uniform int sourceMip; -uniform sampler2D SourceTex; -uniform writeonly image2D RESTRICT DestTex; - -uniform sampler2D ShadedScene; -uniform sampler2D SumTex; - -uniform bool FirstUpsamplePass; - - -void main() { - vec2 source_size = vec2(textureSize(SourceTex, sourceMip).xy); - vec2 texcoord = (ivec2(gl_FragCoord.xy) + 0.5) / (2.0 * source_size); - vec3 summed = textureLod(SourceTex, texcoord, sourceMip + 1).xyz; - - if (FirstUpsamplePass) { - summed = vec3(0); - } - - vec3 mip_color = vec3(1); - - // Hardcoded mipmap colors for now, should make this adjustable at some point - switch(sourceMip) { - case 0: mip_color = vec3(0.214, 0.429, 0.497); break; - case 1: mip_color = vec3(0.964, 0.947, 0.991); break; - case 2: mip_color = vec3(0.982, 0.542, 0.542); break; - case 3: mip_color = vec3(0.301, 0.493, 1.000); break; - case 4: mip_color = vec3(0.456, 0.209, 0.167); break; - } - - // upsample texture - ivec2 coord = ivec2(gl_FragCoord.xy); - - vec2 flt_coord = vec2(coord) / (2.0 * source_size); - vec2 texel_size = 1.0 / source_size; - - vec3 source_sample = textureLod(SourceTex, flt_coord, sourceMip).xyz * 4; - - source_sample += textureLod( - SourceTex, flt_coord + vec2(0, 1) * texel_size, sourceMip).xyz * 2; - source_sample += textureLod( - SourceTex, flt_coord + vec2(0, -1) * texel_size, sourceMip).xyz * 2; - source_sample += textureLod( - SourceTex, flt_coord + vec2(1, 0) * texel_size, sourceMip).xyz * 2; - source_sample += textureLod( - SourceTex, flt_coord + vec2(-1, 0) * texel_size, sourceMip).xyz * 2; - - source_sample += textureLod( - SourceTex, flt_coord + vec2(-1, -1) * texel_size, sourceMip).xyz; - source_sample += textureLod( - SourceTex, flt_coord + vec2(1, -1) * texel_size, sourceMip).xyz; - source_sample += textureLod( - SourceTex, flt_coord + vec2(-1, 1) * texel_size, sourceMip).xyz; - source_sample += textureLod( - SourceTex, flt_coord + vec2(1, 1) * texel_size, sourceMip).xyz; - - source_sample /= 16.0; - source_sample *= mip_color; - - vec3 pass_result = summed + source_sample; - pass_result *= 0.5; - - vec4 old_data = texelFetch(SourceTex, coord, sourceMip - 1); - imageStore(DestTex, coord, old_data + vec4(pass_result, 0)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" + +uniform int sourceMip; +uniform sampler2D SourceTex; +uniform writeonly image2D RESTRICT DestTex; + +uniform sampler2D ShadedScene; +uniform sampler2D SumTex; + +uniform bool FirstUpsamplePass; + + +void main() { + vec2 source_size = vec2(textureSize(SourceTex, sourceMip).xy); + vec2 texcoord = (ivec2(gl_FragCoord.xy) + 0.5) / (2.0 * source_size); + vec3 summed = textureLod(SourceTex, texcoord, sourceMip + 1).xyz; + + if (FirstUpsamplePass) { + summed = vec3(0); + } + + vec3 mip_color = vec3(1); + + // Hardcoded mipmap colors for now, should make this adjustable at some point + switch(sourceMip) { + case 0: mip_color = vec3(0.214, 0.429, 0.497); break; + case 1: mip_color = vec3(0.964, 0.947, 0.991); break; + case 2: mip_color = vec3(0.982, 0.542, 0.542); break; + case 3: mip_color = vec3(0.301, 0.493, 1.000); break; + case 4: mip_color = vec3(0.456, 0.209, 0.167); break; + } + + // upsample texture + ivec2 coord = ivec2(gl_FragCoord.xy); + + vec2 flt_coord = vec2(coord) / (2.0 * source_size); + vec2 texel_size = 1.0 / source_size; + + vec3 source_sample = textureLod(SourceTex, flt_coord, sourceMip).xyz * 4; + + source_sample += textureLod( + SourceTex, flt_coord + vec2(0, 1) * texel_size, sourceMip).xyz * 2; + source_sample += textureLod( + SourceTex, flt_coord + vec2(0, -1) * texel_size, sourceMip).xyz * 2; + source_sample += textureLod( + SourceTex, flt_coord + vec2(1, 0) * texel_size, sourceMip).xyz * 2; + source_sample += textureLod( + SourceTex, flt_coord + vec2(-1, 0) * texel_size, sourceMip).xyz * 2; + + source_sample += textureLod( + SourceTex, flt_coord + vec2(-1, -1) * texel_size, sourceMip).xyz; + source_sample += textureLod( + SourceTex, flt_coord + vec2(1, -1) * texel_size, sourceMip).xyz; + source_sample += textureLod( + SourceTex, flt_coord + vec2(-1, 1) * texel_size, sourceMip).xyz; + source_sample += textureLod( + SourceTex, flt_coord + vec2(1, 1) * texel_size, sourceMip).xyz; + + source_sample /= 16.0; + source_sample *= mip_color; + + vec3 pass_result = summed + source_sample; + pass_result *= 0.5; + + vec4 old_data = texelFetch(SourceTex, coord, sourceMip - 1); + imageStore(DestTex, coord, old_data + vec4(pass_result, 0)); +} diff --git a/RenderPipelineFile/rpplugins/bloom/shader/extract_bright_spots.frag.glsl b/RenderPipelineFile/rpplugins/bloom/shader/extract_bright_spots.frag.glsl index 1bbb37c2..cac15cfe 100644 --- a/RenderPipelineFile/rpplugins/bloom/shader/extract_bright_spots.frag.glsl +++ b/RenderPipelineFile/rpplugins/bloom/shader/extract_bright_spots.frag.glsl @@ -1,53 +1,53 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/color_spaces.inc.glsl" - -uniform sampler2D ShadedScene; -uniform writeonly image2D RESTRICT DestTex; - -void main() { - vec2 texcoord = get_texcoord(); - ivec2 coord = ivec2(gl_FragCoord.xy); - - vec3 scene_color = textureLod(ShadedScene, texcoord, 0).xyz; - float luma = get_luminance(scene_color); - - // We don't use a threshold for blur, instead we perform the bloom everything, - // which is physically more correct - vec3 bloom_color = scene_color; - bloom_color *= GET_SETTING(bloom, bloom_strength) * 0.005; - bloom_color = clamp(bloom_color, vec3(0), vec3(25000.0)); - - #if DEBUG_MODE - bloom_color *= 0; - #endif - - imageStore(DestTex, coord, vec4(bloom_color, 0)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/color_spaces.inc.glsl" + +uniform sampler2D ShadedScene; +uniform writeonly image2D RESTRICT DestTex; + +void main() { + vec2 texcoord = get_texcoord(); + ivec2 coord = ivec2(gl_FragCoord.xy); + + vec3 scene_color = textureLod(ShadedScene, texcoord, 0).xyz; + float luma = get_luminance(scene_color); + + // We don't use a threshold for blur, instead we perform the bloom everything, + // which is physically more correct + vec3 bloom_color = scene_color; + bloom_color *= GET_SETTING(bloom, bloom_strength) * 0.005; + bloom_color = clamp(bloom_color, vec3(0), vec3(25000.0)); + + #if DEBUG_MODE + bloom_color *= 0; + #endif + + imageStore(DestTex, coord, vec4(bloom_color, 0)); +} diff --git a/RenderPipelineFile/rpplugins/bloom/shader/remove_fireflies.frag.glsl b/RenderPipelineFile/rpplugins/bloom/shader/remove_fireflies.frag.glsl index fdc39be7..df387724 100644 --- a/RenderPipelineFile/rpplugins/bloom/shader/remove_fireflies.frag.glsl +++ b/RenderPipelineFile/rpplugins/bloom/shader/remove_fireflies.frag.glsl @@ -1,70 +1,70 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma optionNV (unroll all) -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gaussian_weights.inc.glsl" -#pragma include "includes/color_spaces.inc.glsl" - -uniform sampler2D ShadedScene; -out vec3 result; - -// Karis average -float get_weight(vec3 color, float weight) { - return weight / (1.0 + get_luminance(color)); -} - -void main() { - - vec2 texcoord = get_texcoord(); - - #if GET_SETTING(bloom, remove_fireflies) - float weights = 0.0; - vec3 accum = vec3(0); - const int filter_size = 2; - - vec2 texel_offs = vec2(1.0) / SCREEN_SIZE; - - // Find all surrounding pixels and weight them - for (int i = -filter_size; i <= filter_size; ++i) { - for (int j = -filter_size; j <= filter_size; ++j) { - vec3 color_sample = textureLod( - ShadedScene, texcoord + vec2(i, j) * texel_offs, 0).xyz; - float weight = get_weight( - color_sample, gaussian_weights_3[abs(i)] * gaussian_weights_3[abs(j)]); - accum += color_sample * weight; - weights += weight; - } - } - - accum /= max(0.01, weights); - result = accum; - #else - result = textureLod(ShadedScene, texcoord, 0).xyz; - #endif -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma optionNV (unroll all) +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gaussian_weights.inc.glsl" +#pragma include "includes/color_spaces.inc.glsl" + +uniform sampler2D ShadedScene; +out vec3 result; + +// Karis average +float get_weight(vec3 color, float weight) { + return weight / (1.0 + get_luminance(color)); +} + +void main() { + + vec2 texcoord = get_texcoord(); + + #if GET_SETTING(bloom, remove_fireflies) + float weights = 0.0; + vec3 accum = vec3(0); + const int filter_size = 2; + + vec2 texel_offs = vec2(1.0) / SCREEN_SIZE; + + // Find all surrounding pixels and weight them + for (int i = -filter_size; i <= filter_size; ++i) { + for (int j = -filter_size; j <= filter_size; ++j) { + vec3 color_sample = textureLod( + ShadedScene, texcoord + vec2(i, j) * texel_offs, 0).xyz; + float weight = get_weight( + color_sample, gaussian_weights_3[abs(i)] * gaussian_weights_3[abs(j)]); + accum += color_sample * weight; + weights += weight; + } + } + + accum /= max(0.01, weights); + result = accum; + #else + result = textureLod(ShadedScene, texcoord, 0).xyz; + #endif +} diff --git a/RenderPipelineFile/rpplugins/clouds/apply_clouds_stage.py b/RenderPipelineFile/rpplugins/clouds/apply_clouds_stage.py index 094d9730..3dcda7ec 100644 --- a/RenderPipelineFile/rpplugins/clouds/apply_clouds_stage.py +++ b/RenderPipelineFile/rpplugins/clouds/apply_clouds_stage.py @@ -1,71 +1,71 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import Vec2 - -from rpcore.render_stage import RenderStage - - -class ApplyCloudsStage(RenderStage): - - """ This stage raymarchs the cloud voxel grid """ - - required_pipes = ["ShadedScene", "GBuffer"] - - def __init__(self, pipeline): - RenderStage.__init__(self, pipeline) - - @property - def produced_pipes(self): - return {"ShadedScene": self.target_apply_clouds.color_tex} - - def create(self): - self.render_target = self.create_target("RaymarchVoxels") - self.render_target.size = -2 - self.render_target.add_color_attachment(bits=16, alpha=True) - self.render_target.prepare_buffer() - - self.upscale_target = self.create_target("UpscaleTarget") - self.upscale_target.add_color_attachment(bits=16, alpha=True) - self.upscale_target.prepare_buffer() - self.upscale_target.set_shader_inputs( - upscaleWeights=Vec2(0.05, 0.2), - SourceTex=self.render_target.color_tex) - - self.target_apply_clouds = self.create_target("MergeWithScene") - self.target_apply_clouds.add_color_attachment(bits=16) - self.target_apply_clouds.prepare_buffer() - - self.target_apply_clouds.set_shader_input( - "CloudsTex", self.upscale_target.color_tex) - - def reload_shaders(self): - self.target_apply_clouds.shader = self.load_plugin_shader( - "apply_clouds.frag.glsl") - self.render_target.shader = self.load_plugin_shader( - "render_clouds.frag.glsl") - self.upscale_target.shader = self.load_plugin_shader( - "/$$rp/shader/bilateral_upscale.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import Vec2 + +from rpcore.render_stage import RenderStage + + +class ApplyCloudsStage(RenderStage): + + """ This stage raymarchs the cloud voxel grid """ + + required_pipes = ["ShadedScene", "GBuffer"] + + def __init__(self, pipeline): + RenderStage.__init__(self, pipeline) + + @property + def produced_pipes(self): + return {"ShadedScene": self.target_apply_clouds.color_tex} + + def create(self): + self.render_target = self.create_target("RaymarchVoxels") + self.render_target.size = -2 + self.render_target.add_color_attachment(bits=16, alpha=True) + self.render_target.prepare_buffer() + + self.upscale_target = self.create_target("UpscaleTarget") + self.upscale_target.add_color_attachment(bits=16, alpha=True) + self.upscale_target.prepare_buffer() + self.upscale_target.set_shader_inputs( + upscaleWeights=Vec2(0.05, 0.2), + SourceTex=self.render_target.color_tex) + + self.target_apply_clouds = self.create_target("MergeWithScene") + self.target_apply_clouds.add_color_attachment(bits=16) + self.target_apply_clouds.prepare_buffer() + + self.target_apply_clouds.set_shader_input( + "CloudsTex", self.upscale_target.color_tex) + + def reload_shaders(self): + self.target_apply_clouds.shader = self.load_plugin_shader( + "apply_clouds.frag.glsl") + self.render_target.shader = self.load_plugin_shader( + "render_clouds.frag.glsl") + self.upscale_target.shader = self.load_plugin_shader( + "/$$rp/shader/bilateral_upscale.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/clouds/cloud_voxel_stage.py b/RenderPipelineFile/rpplugins/clouds/cloud_voxel_stage.py index 2e1dab9d..f9f7a205 100644 --- a/RenderPipelineFile/rpplugins/clouds/cloud_voxel_stage.py +++ b/RenderPipelineFile/rpplugins/clouds/cloud_voxel_stage.py @@ -1,86 +1,86 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import SamplerState, Vec4 - -from rpcore.render_stage import RenderStage -from rpcore.image import Image - - -class CloudVoxelStage(RenderStage): - - """ This stage generates the volumetric cloud voxel grid """ - - required_pipes = ["ScatteringIBLDiffuse"] - - def __init__(self, pipeline): - RenderStage.__init__(self, pipeline) - self._voxel_res_xy = 256 - self._voxel_res_z = 32 - - @property - def produced_pipes(self): - return {"CloudVoxels": self._cloud_voxels} - - @property - def produced_defines(self): - return { - "CLOUD_RES_XY": self._voxel_res_xy, - "CLOUD_RES_Z": self._voxel_res_z - } - - def create(self): - # Construct the voxel texture - self._cloud_voxels = Image.create_3d( - "CloudVoxels", self._voxel_res_xy, self._voxel_res_xy, self._voxel_res_z, "RGBA8") - self._cloud_voxels.set_wrap_u(SamplerState.WM_repeat) - self._cloud_voxels.set_wrap_v(SamplerState.WM_repeat) - self._cloud_voxels.set_wrap_w(SamplerState.WM_border_color) - self._cloud_voxels.set_border_color(Vec4(0, 0, 0, 0)) - - # Construct the target which populates the voxel texture - self._grid_target = self.create_target("CreateVoxels") - self._grid_target.size = self._voxel_res_xy, self._voxel_res_xy - self._grid_target.prepare_buffer() - self._grid_target.quad.set_instance_count(self._voxel_res_z) - self._grid_target.set_shader_input("CloudVoxels", self._cloud_voxels) - - # Construct the target which shades the voxels - self._shade_target = self.create_target("ShadeVoxels") - self._shade_target.size = self._voxel_res_xy, self._voxel_res_xy - self._shade_target.prepare_buffer() - self._shade_target.quad.set_instance_count(self._voxel_res_z) - self._shade_target.set_shader_inputs( - CloudVoxels=self._cloud_voxels, - CloudVoxelsDest=self._cloud_voxels) - - def reload_shaders(self): - self._grid_target.shader = self.load_plugin_shader( - "/$$rp/shader/default_post_process_instanced.vert.glsl", - "generate_clouds.frag.glsl") - self._shade_target.shader = self.load_plugin_shader( - "/$$rp/shader/default_post_process_instanced.vert.glsl", - "shade_clouds.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import SamplerState, Vec4 + +from rpcore.render_stage import RenderStage +from rpcore.image import Image + + +class CloudVoxelStage(RenderStage): + + """ This stage generates the volumetric cloud voxel grid """ + + required_pipes = ["ScatteringIBLDiffuse"] + + def __init__(self, pipeline): + RenderStage.__init__(self, pipeline) + self._voxel_res_xy = 256 + self._voxel_res_z = 32 + + @property + def produced_pipes(self): + return {"CloudVoxels": self._cloud_voxels} + + @property + def produced_defines(self): + return { + "CLOUD_RES_XY": self._voxel_res_xy, + "CLOUD_RES_Z": self._voxel_res_z + } + + def create(self): + # Construct the voxel texture + self._cloud_voxels = Image.create_3d( + "CloudVoxels", self._voxel_res_xy, self._voxel_res_xy, self._voxel_res_z, "RGBA8") + self._cloud_voxels.set_wrap_u(SamplerState.WM_repeat) + self._cloud_voxels.set_wrap_v(SamplerState.WM_repeat) + self._cloud_voxels.set_wrap_w(SamplerState.WM_border_color) + self._cloud_voxels.set_border_color(Vec4(0, 0, 0, 0)) + + # Construct the target which populates the voxel texture + self._grid_target = self.create_target("CreateVoxels") + self._grid_target.size = self._voxel_res_xy, self._voxel_res_xy + self._grid_target.prepare_buffer() + self._grid_target.quad.set_instance_count(self._voxel_res_z) + self._grid_target.set_shader_input("CloudVoxels", self._cloud_voxels) + + # Construct the target which shades the voxels + self._shade_target = self.create_target("ShadeVoxels") + self._shade_target.size = self._voxel_res_xy, self._voxel_res_xy + self._shade_target.prepare_buffer() + self._shade_target.quad.set_instance_count(self._voxel_res_z) + self._shade_target.set_shader_inputs( + CloudVoxels=self._cloud_voxels, + CloudVoxelsDest=self._cloud_voxels) + + def reload_shaders(self): + self._grid_target.shader = self.load_plugin_shader( + "/$$rp/shader/default_post_process_instanced.vert.glsl", + "generate_clouds.frag.glsl") + self._shade_target.shader = self.load_plugin_shader( + "/$$rp/shader/default_post_process_instanced.vert.glsl", + "shade_clouds.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/clouds/config.yaml b/RenderPipelineFile/rpplugins/clouds/config.yaml index 0b26c76a..06f7e97b 100644 --- a/RenderPipelineFile/rpplugins/clouds/config.yaml +++ b/RenderPipelineFile/rpplugins/clouds/config.yaml @@ -1,35 +1,35 @@ - -settings: !!omap - - # - cloud_decay: - # type: float - # range: [0.0, 10.0] - # default: 3.0 - # label: Cloud Decay - # description: > - # Controls how the cloud intensity decays to the top. - # shader_runtime: true - - - raymarch_steps: - type: int - range: [16, 256] - default: 160 - label: Raymarch Steps - shader_runtime: true - description: > - Controls how many steps should be taken through the cloud voxel grid - to integrate the cloud density. Higher values produce more accurate - result but also require more performance. - -daytime_settings: !!omap - - - cloud_brightness: - type: scalar - unit: percent - range: [0, 2] - default: 1.0 - logarithmic_factor: 2.0 - label: Cloud brightness - description: > - Brightness of all cloud layers, controlls how bright the clouds are. - A value of zero causes no clouds. + +settings: !!omap + + # - cloud_decay: + # type: float + # range: [0.0, 10.0] + # default: 3.0 + # label: Cloud Decay + # description: > + # Controls how the cloud intensity decays to the top. + # shader_runtime: true + + - raymarch_steps: + type: int + range: [16, 256] + default: 160 + label: Raymarch Steps + shader_runtime: true + description: > + Controls how many steps should be taken through the cloud voxel grid + to integrate the cloud density. Higher values produce more accurate + result but also require more performance. + +daytime_settings: !!omap + + - cloud_brightness: + type: scalar + unit: percent + range: [0, 2] + default: 1.0 + logarithmic_factor: 2.0 + label: Cloud brightness + description: > + Brightness of all cloud layers, controlls how bright the clouds are. + A value of zero causes no clouds. diff --git a/RenderPipelineFile/rpplugins/clouds/plugin.py b/RenderPipelineFile/rpplugins/clouds/plugin.py index 944ba6c0..c7cd6db8 100644 --- a/RenderPipelineFile/rpplugins/clouds/plugin.py +++ b/RenderPipelineFile/rpplugins/clouds/plugin.py @@ -1,73 +1,73 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import SamplerState - -from rpcore.loader import RPLoader -from rpcore.pluginbase.base_plugin import BasePlugin - -from .apply_clouds_stage import ApplyCloudsStage - -# Dynamic cloud generation is disabled for now -# from .cloud_voxel_stage import CloudVoxelStage - - -class Plugin(BasePlugin): - - name = "Volumetric Clouds" - author = "tobspr " - description = ("This Plugin adds support or volumetric, raytraced clouds. " - "Right now this is pretty unoptimized and may consum a lot " - "of performance.") - version = "alpha (!)" - required_plugins = ("scattering",) - - def on_stage_setup(self): - # self.generation_stage = self.create_stage(CloudVoxelStage) - self.apply_stage = self.create_stage(ApplyCloudsStage) - - def on_pipeline_created(self): - # High-res noise - noise1 = RPLoader.load_texture(self.get_resource("noise1-data.txo")) - noise1.set_wrap_u(SamplerState.WM_repeat) - noise1.set_wrap_v(SamplerState.WM_repeat) - noise1.set_wrap_w(SamplerState.WM_repeat) - noise1.set_minfilter(SamplerState.FT_linear_mipmap_linear) - self.apply_stage.set_shader_input("Noise1", noise1) - - # Low-res noise - noise2 = RPLoader.load_texture(self.get_resource("noise2-data.txo")) - noise2.set_wrap_u(SamplerState.WM_repeat) - noise2.set_wrap_v(SamplerState.WM_repeat) - noise2.set_wrap_w(SamplerState.WM_repeat) - noise2.set_minfilter(SamplerState.FT_linear_mipmap_linear) - self.apply_stage.set_shader_input("Noise2", noise2) - - # Weather tex - weather = RPLoader.load_texture(self.get_resource("weather_tex.png")) - weather.set_wrap_u(SamplerState.WM_repeat) - weather.set_wrap_v(SamplerState.WM_repeat) - self.apply_stage.set_shader_input("WeatherTex", weather) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import SamplerState + +from rpcore.loader import RPLoader +from rpcore.pluginbase.base_plugin import BasePlugin + +from .apply_clouds_stage import ApplyCloudsStage + +# Dynamic cloud generation is disabled for now +# from .cloud_voxel_stage import CloudVoxelStage + + +class Plugin(BasePlugin): + + name = "Volumetric Clouds" + author = "tobspr " + description = ("This Plugin adds support or volumetric, raytraced clouds. " + "Right now this is pretty unoptimized and may consum a lot " + "of performance.") + version = "alpha (!)" + required_plugins = ("scattering",) + + def on_stage_setup(self): + # self.generation_stage = self.create_stage(CloudVoxelStage) + self.apply_stage = self.create_stage(ApplyCloudsStage) + + def on_pipeline_created(self): + # High-res noise + noise1 = RPLoader.load_texture(self.get_resource("noise1-data.txo")) + noise1.set_wrap_u(SamplerState.WM_repeat) + noise1.set_wrap_v(SamplerState.WM_repeat) + noise1.set_wrap_w(SamplerState.WM_repeat) + noise1.set_minfilter(SamplerState.FT_linear_mipmap_linear) + self.apply_stage.set_shader_input("Noise1", noise1) + + # Low-res noise + noise2 = RPLoader.load_texture(self.get_resource("noise2-data.txo")) + noise2.set_wrap_u(SamplerState.WM_repeat) + noise2.set_wrap_v(SamplerState.WM_repeat) + noise2.set_wrap_w(SamplerState.WM_repeat) + noise2.set_minfilter(SamplerState.FT_linear_mipmap_linear) + self.apply_stage.set_shader_input("Noise2", noise2) + + # Weather tex + weather = RPLoader.load_texture(self.get_resource("weather_tex.png")) + weather.set_wrap_u(SamplerState.WM_repeat) + weather.set_wrap_v(SamplerState.WM_repeat) + self.apply_stage.set_shader_input("WeatherTex", weather) diff --git a/RenderPipelineFile/rpplugins/clouds/resources/generate_noise1.compute.glsl b/RenderPipelineFile/rpplugins/clouds/resources/generate_noise1.compute.glsl index 336c4db5..90e8eb0f 100644 --- a/RenderPipelineFile/rpplugins/clouds/resources/generate_noise1.compute.glsl +++ b/RenderPipelineFile/rpplugins/clouds/resources/generate_noise1.compute.glsl @@ -1,58 +1,58 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// Shader to generate the cloud noise textures - -layout (local_size_x = 8, local_size_y = 8, local_size_z = 4) in; -uniform writeonly image3D DestTex; - -const int tex_size = 128; - -#pragma include "noise.inc.glsl" - -void main() { - ivec3 coord = ivec3(gl_GlobalInvocationID.xyz); - vec3 flt_coord = coord / float(tex_size); - float fbm0 = fbm(flt_coord * 5); - - float worley = fbm_worley(flt_coord, 8, 0.7, 2, 0.2723424); - - float OldMin = -1.0; - float OldMax = 1.0; - float NewMin = worley * 0.5; - float NewMax = 1.0; - - float combined = NewMin + (((fbm0 - OldMin) / (OldMax - OldMin)) * (NewMax - NewMin)); - fbm0 = combined; - - float worley1 = sqrt(fbm_worley(flt_coord, 8, 0.6, 4, 0.164354)) * 1.0; - float worley2 = sqrt(fbm_worley(flt_coord, 16, 0.6, 3, 0.19123)) * 1.0; - float worley3 = sqrt(fbm_worley(flt_coord, 32, 0.4, 2, 0.2613)) * 1.0; - - imageStore(DestTex, coord, vec4(fbm0, worley1, worley2, worley3)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// Shader to generate the cloud noise textures + +layout (local_size_x = 8, local_size_y = 8, local_size_z = 4) in; +uniform writeonly image3D DestTex; + +const int tex_size = 128; + +#pragma include "noise.inc.glsl" + +void main() { + ivec3 coord = ivec3(gl_GlobalInvocationID.xyz); + vec3 flt_coord = coord / float(tex_size); + float fbm0 = fbm(flt_coord * 5); + + float worley = fbm_worley(flt_coord, 8, 0.7, 2, 0.2723424); + + float OldMin = -1.0; + float OldMax = 1.0; + float NewMin = worley * 0.5; + float NewMax = 1.0; + + float combined = NewMin + (((fbm0 - OldMin) / (OldMax - OldMin)) * (NewMax - NewMin)); + fbm0 = combined; + + float worley1 = sqrt(fbm_worley(flt_coord, 8, 0.6, 4, 0.164354)) * 1.0; + float worley2 = sqrt(fbm_worley(flt_coord, 16, 0.6, 3, 0.19123)) * 1.0; + float worley3 = sqrt(fbm_worley(flt_coord, 32, 0.4, 2, 0.2613)) * 1.0; + + imageStore(DestTex, coord, vec4(fbm0, worley1, worley2, worley3)); +} diff --git a/RenderPipelineFile/rpplugins/clouds/resources/generate_noise2.compute.glsl b/RenderPipelineFile/rpplugins/clouds/resources/generate_noise2.compute.glsl index db5c1eb9..34feb42b 100644 --- a/RenderPipelineFile/rpplugins/clouds/resources/generate_noise2.compute.glsl +++ b/RenderPipelineFile/rpplugins/clouds/resources/generate_noise2.compute.glsl @@ -1,48 +1,48 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// Shader to generate the cloud noise textures - -layout (local_size_x = 8, local_size_y = 8, local_size_z = 4) in; -uniform writeonly image3D DestTex; - -const int tex_size = 32; - -#pragma include "noise.inc.glsl" - -void main() { - ivec3 coord = ivec3(gl_GlobalInvocationID.xyz); - vec3 flt_coord = coord / float(tex_size); - - float worley1 = sqrt(fbm_worley(flt_coord, 8, 0.6, 3, 0.76632)) * 1.0; - float worley2 = sqrt(fbm_worley(flt_coord, 8, 0.4, 3, 0.26386)) * 1.0; - float worley3 = sqrt(fbm_worley(flt_coord, 16, 0.4, 2, 0.64243)) * 1.0; - - // imageStore(DestTex, coord, vec4(worley1, worley2, worley3, 1)); - imageStore(DestTex, coord, vec4(worley1, worley2, worley3, 1)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// Shader to generate the cloud noise textures + +layout (local_size_x = 8, local_size_y = 8, local_size_z = 4) in; +uniform writeonly image3D DestTex; + +const int tex_size = 32; + +#pragma include "noise.inc.glsl" + +void main() { + ivec3 coord = ivec3(gl_GlobalInvocationID.xyz); + vec3 flt_coord = coord / float(tex_size); + + float worley1 = sqrt(fbm_worley(flt_coord, 8, 0.6, 3, 0.76632)) * 1.0; + float worley2 = sqrt(fbm_worley(flt_coord, 8, 0.4, 3, 0.26386)) * 1.0; + float worley3 = sqrt(fbm_worley(flt_coord, 16, 0.4, 2, 0.64243)) * 1.0; + + // imageStore(DestTex, coord, vec4(worley1, worley2, worley3, 1)); + imageStore(DestTex, coord, vec4(worley1, worley2, worley3, 1)); +} diff --git a/RenderPipelineFile/rpplugins/clouds/resources/noise.inc.glsl b/RenderPipelineFile/rpplugins/clouds/resources/noise.inc.glsl index f5fb255d..b61d648f 100644 --- a/RenderPipelineFile/rpplugins/clouds/resources/noise.inc.glsl +++ b/RenderPipelineFile/rpplugins/clouds/resources/noise.inc.glsl @@ -1,171 +1,171 @@ - -// lint: skip-file - -vec3 mod289(vec3 x) { - return x - floor(x * (1.0 / 289.0)) * 289.0; -} - -vec4 mod289(vec4 x) { - return x - floor(x * (1.0 / 289.0)) * 289.0; -} - -vec4 permute(vec4 x) { - return mod289(((x*34.0)+1.0)*x); -} - -vec4 taylorInvSqrt(vec4 r) -{ - return 1.79284291400159 - 0.85373472095314 * r; -} - -vec3 fade(vec3 t) { - return t*t*t*(t*(t*6.0-15.0)+10.0); -} - - - -// Classic Perlin noise, periodic variant -float pnoise3D(vec3 P, vec3 rep) -{ - vec3 Pi0 = mod(floor(P), rep); // Integer part, modulo period - vec3 Pi1 = mod(Pi0 + vec3(1.0), rep); // Integer part + 1, mod period - Pi0 = mod289(Pi0); - Pi1 = mod289(Pi1); - vec3 Pf0 = fract(P); // Fractional part for interpolation - vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0 - vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); - vec4 iy = vec4(Pi0.yy, Pi1.yy); - vec4 iz0 = Pi0.zzzz; - vec4 iz1 = Pi1.zzzz; - - vec4 ixy = permute(permute(ix) + iy); - vec4 ixy0 = permute(ixy + iz0); - vec4 ixy1 = permute(ixy + iz1); - - vec4 gx0 = ixy0 * (1.0 / 7.0); - vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5; - gx0 = fract(gx0); - vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); - vec4 sz0 = step(gz0, vec4(0.0)); - gx0 -= sz0 * (step(0.0, gx0) - 0.5); - gy0 -= sz0 * (step(0.0, gy0) - 0.5); - - vec4 gx1 = ixy1 * (1.0 / 7.0); - vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5; - gx1 = fract(gx1); - vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); - vec4 sz1 = step(gz1, vec4(0.0)); - gx1 -= sz1 * (step(0.0, gx1) - 0.5); - gy1 -= sz1 * (step(0.0, gy1) - 0.5); - - vec3 g000 = vec3(gx0.x,gy0.x,gz0.x); - vec3 g100 = vec3(gx0.y,gy0.y,gz0.y); - vec3 g010 = vec3(gx0.z,gy0.z,gz0.z); - vec3 g110 = vec3(gx0.w,gy0.w,gz0.w); - vec3 g001 = vec3(gx1.x,gy1.x,gz1.x); - vec3 g101 = vec3(gx1.y,gy1.y,gz1.y); - vec3 g011 = vec3(gx1.z,gy1.z,gz1.z); - vec3 g111 = vec3(gx1.w,gy1.w,gz1.w); - - vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110))); - g000 *= norm0.x; - g010 *= norm0.y; - g100 *= norm0.z; - g110 *= norm0.w; - vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111))); - g001 *= norm1.x; - g011 *= norm1.y; - g101 *= norm1.z; - g111 *= norm1.w; - - float n000 = dot(g000, Pf0); - float n100 = dot(g100, vec3(Pf1.x, Pf0.yz)); - float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z)); - float n110 = dot(g110, vec3(Pf1.xy, Pf0.z)); - float n001 = dot(g001, vec3(Pf0.xy, Pf1.z)); - float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z)); - float n011 = dot(g011, vec3(Pf0.x, Pf1.yz)); - float n111 = dot(g111, Pf1); - - vec3 fade_xyz = fade(Pf0); - vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z); - vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y); - float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); - return 2.2 * n_xyz; -} - - -float fbm(vec3 x) { - float v = 0.0; - float a = 0.5; - vec3 shift = vec3(0.954653); - float tile = 5.0; - for (int i = 0; i < 6; ++i) { - v += a * pnoise3D(x, vec3(tile)); - x = x * 2.0 + shift; - a *= 0.5; - tile *= 2.0; - } - return v; -} - -float rand(vec2 co){ - return abs(fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453)) * 2 - 1; -} - - -// Returns the point in a given cell -vec3 worley_cell_point(ivec3 cell, int num_cells, float drop_rate, float seed) { - cell = cell % num_cells; - vec3 cell_base = vec3(cell) / num_cells; - float noise_x = rand(cell_base.xy + seed); - float noise_y = rand(cell_base.yx + seed); - float noise_z = rand(cell_base.zx + cell_base.yy + seed); - float drop_point = step(rand(cell.xy + cell.yx + cell.zx + cell.zz + seed) + 1e-7, drop_rate); - return cell_base + (0.5 + 1.0 * vec3(noise_x, noise_y, noise_z)) / num_cells + drop_point * vec3(1e9); -} - - -// Distance accross borders -float distance_border(vec3 a, vec3 b) { - float dx = min( abs(a.x - b.x), min(abs( a.x - 1.0 - b.x), abs(a.x + 1.0 - b.x))); - float dy = min( abs(a.y - b.y), min(abs( a.y - 1.0 - b.y), abs(a.y + 1.0 - b.y))); - float dz = min( abs(a.z - b.z), min(abs( a.z - 1.0 - b.z), abs(a.z + 1.0 - b.z))); - return length(vec3(dx, dy, dz)); -} - -// Performs worley noise by checking all adjacent cells -// and comparing the distance to their points -float worley_noise(vec3 coord, int num_cells, float drop_rate, float seed) { - coord = fract(coord); - ivec3 cell = ivec3(coord * num_cells); - float dist = 1.0; - - // Search in the surrounding 5x5 cell block - for (int x = 0; x < 5; x++) { - for (int y = 0; y < 5; y++) { - for (int z = 0; z < 5; z++) { - vec3 cell_point = worley_cell_point(cell + ivec3(x-2, y-2, z-2), num_cells, drop_rate, seed); - dist = min(dist, distance_border(cell_point, coord)); - } - } - } - dist /= length(vec3(1.0 / num_cells)); - dist = clamp(1.0 - dist, 0.0, 1.0); - return dist; -} - - -float fbm_worley(vec3 x, int num_tiles, float drop_rate, int octaves, float seed) { - seed *= 0.05; - float v = 0.0; - float a = 0.5; - vec3 shift = vec3(0.354653); - for (int i = 0; i < octaves; ++i) { - v += a * worley_noise(x, num_tiles, drop_rate, seed); - x = x * 2.0 + shift; - a *= 0.5; - // num_tiles = int(num_tiles * 1.5); - } - return v; -} + +// lint: skip-file + +vec3 mod289(vec3 x) { + return x - floor(x * (1.0 / 289.0)) * 289.0; +} + +vec4 mod289(vec4 x) { + return x - floor(x * (1.0 / 289.0)) * 289.0; +} + +vec4 permute(vec4 x) { + return mod289(((x*34.0)+1.0)*x); +} + +vec4 taylorInvSqrt(vec4 r) +{ + return 1.79284291400159 - 0.85373472095314 * r; +} + +vec3 fade(vec3 t) { + return t*t*t*(t*(t*6.0-15.0)+10.0); +} + + + +// Classic Perlin noise, periodic variant +float pnoise3D(vec3 P, vec3 rep) +{ + vec3 Pi0 = mod(floor(P), rep); // Integer part, modulo period + vec3 Pi1 = mod(Pi0 + vec3(1.0), rep); // Integer part + 1, mod period + Pi0 = mod289(Pi0); + Pi1 = mod289(Pi1); + vec3 Pf0 = fract(P); // Fractional part for interpolation + vec3 Pf1 = Pf0 - vec3(1.0); // Fractional part - 1.0 + vec4 ix = vec4(Pi0.x, Pi1.x, Pi0.x, Pi1.x); + vec4 iy = vec4(Pi0.yy, Pi1.yy); + vec4 iz0 = Pi0.zzzz; + vec4 iz1 = Pi1.zzzz; + + vec4 ixy = permute(permute(ix) + iy); + vec4 ixy0 = permute(ixy + iz0); + vec4 ixy1 = permute(ixy + iz1); + + vec4 gx0 = ixy0 * (1.0 / 7.0); + vec4 gy0 = fract(floor(gx0) * (1.0 / 7.0)) - 0.5; + gx0 = fract(gx0); + vec4 gz0 = vec4(0.5) - abs(gx0) - abs(gy0); + vec4 sz0 = step(gz0, vec4(0.0)); + gx0 -= sz0 * (step(0.0, gx0) - 0.5); + gy0 -= sz0 * (step(0.0, gy0) - 0.5); + + vec4 gx1 = ixy1 * (1.0 / 7.0); + vec4 gy1 = fract(floor(gx1) * (1.0 / 7.0)) - 0.5; + gx1 = fract(gx1); + vec4 gz1 = vec4(0.5) - abs(gx1) - abs(gy1); + vec4 sz1 = step(gz1, vec4(0.0)); + gx1 -= sz1 * (step(0.0, gx1) - 0.5); + gy1 -= sz1 * (step(0.0, gy1) - 0.5); + + vec3 g000 = vec3(gx0.x,gy0.x,gz0.x); + vec3 g100 = vec3(gx0.y,gy0.y,gz0.y); + vec3 g010 = vec3(gx0.z,gy0.z,gz0.z); + vec3 g110 = vec3(gx0.w,gy0.w,gz0.w); + vec3 g001 = vec3(gx1.x,gy1.x,gz1.x); + vec3 g101 = vec3(gx1.y,gy1.y,gz1.y); + vec3 g011 = vec3(gx1.z,gy1.z,gz1.z); + vec3 g111 = vec3(gx1.w,gy1.w,gz1.w); + + vec4 norm0 = taylorInvSqrt(vec4(dot(g000, g000), dot(g010, g010), dot(g100, g100), dot(g110, g110))); + g000 *= norm0.x; + g010 *= norm0.y; + g100 *= norm0.z; + g110 *= norm0.w; + vec4 norm1 = taylorInvSqrt(vec4(dot(g001, g001), dot(g011, g011), dot(g101, g101), dot(g111, g111))); + g001 *= norm1.x; + g011 *= norm1.y; + g101 *= norm1.z; + g111 *= norm1.w; + + float n000 = dot(g000, Pf0); + float n100 = dot(g100, vec3(Pf1.x, Pf0.yz)); + float n010 = dot(g010, vec3(Pf0.x, Pf1.y, Pf0.z)); + float n110 = dot(g110, vec3(Pf1.xy, Pf0.z)); + float n001 = dot(g001, vec3(Pf0.xy, Pf1.z)); + float n101 = dot(g101, vec3(Pf1.x, Pf0.y, Pf1.z)); + float n011 = dot(g011, vec3(Pf0.x, Pf1.yz)); + float n111 = dot(g111, Pf1); + + vec3 fade_xyz = fade(Pf0); + vec4 n_z = mix(vec4(n000, n100, n010, n110), vec4(n001, n101, n011, n111), fade_xyz.z); + vec2 n_yz = mix(n_z.xy, n_z.zw, fade_xyz.y); + float n_xyz = mix(n_yz.x, n_yz.y, fade_xyz.x); + return 2.2 * n_xyz; +} + + +float fbm(vec3 x) { + float v = 0.0; + float a = 0.5; + vec3 shift = vec3(0.954653); + float tile = 5.0; + for (int i = 0; i < 6; ++i) { + v += a * pnoise3D(x, vec3(tile)); + x = x * 2.0 + shift; + a *= 0.5; + tile *= 2.0; + } + return v; +} + +float rand(vec2 co){ + return abs(fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453)) * 2 - 1; +} + + +// Returns the point in a given cell +vec3 worley_cell_point(ivec3 cell, int num_cells, float drop_rate, float seed) { + cell = cell % num_cells; + vec3 cell_base = vec3(cell) / num_cells; + float noise_x = rand(cell_base.xy + seed); + float noise_y = rand(cell_base.yx + seed); + float noise_z = rand(cell_base.zx + cell_base.yy + seed); + float drop_point = step(rand(cell.xy + cell.yx + cell.zx + cell.zz + seed) + 1e-7, drop_rate); + return cell_base + (0.5 + 1.0 * vec3(noise_x, noise_y, noise_z)) / num_cells + drop_point * vec3(1e9); +} + + +// Distance accross borders +float distance_border(vec3 a, vec3 b) { + float dx = min( abs(a.x - b.x), min(abs( a.x - 1.0 - b.x), abs(a.x + 1.0 - b.x))); + float dy = min( abs(a.y - b.y), min(abs( a.y - 1.0 - b.y), abs(a.y + 1.0 - b.y))); + float dz = min( abs(a.z - b.z), min(abs( a.z - 1.0 - b.z), abs(a.z + 1.0 - b.z))); + return length(vec3(dx, dy, dz)); +} + +// Performs worley noise by checking all adjacent cells +// and comparing the distance to their points +float worley_noise(vec3 coord, int num_cells, float drop_rate, float seed) { + coord = fract(coord); + ivec3 cell = ivec3(coord * num_cells); + float dist = 1.0; + + // Search in the surrounding 5x5 cell block + for (int x = 0; x < 5; x++) { + for (int y = 0; y < 5; y++) { + for (int z = 0; z < 5; z++) { + vec3 cell_point = worley_cell_point(cell + ivec3(x-2, y-2, z-2), num_cells, drop_rate, seed); + dist = min(dist, distance_border(cell_point, coord)); + } + } + } + dist /= length(vec3(1.0 / num_cells)); + dist = clamp(1.0 - dist, 0.0, 1.0); + return dist; +} + + +float fbm_worley(vec3 x, int num_tiles, float drop_rate, int octaves, float seed) { + seed *= 0.05; + float v = 0.0; + float a = 0.5; + vec3 shift = vec3(0.354653); + for (int i = 0; i < octaves; ++i) { + v += a * worley_noise(x, num_tiles, drop_rate, seed); + x = x * 2.0 + shift; + a *= 0.5; + // num_tiles = int(num_tiles * 1.5); + } + return v; +} diff --git a/RenderPipelineFile/rpplugins/clouds/resources/precompute.py b/RenderPipelineFile/rpplugins/clouds/resources/precompute.py index afca20f6..ecccae3c 100644 --- a/RenderPipelineFile/rpplugins/clouds/resources/precompute.py +++ b/RenderPipelineFile/rpplugins/clouds/resources/precompute.py @@ -1,89 +1,89 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import print_function, division - -import os -from os.path import dirname, realpath -from panda3d.core import * # noqa -from direct.showbase.ShowBase import ShowBase - - -class Application(ShowBase): - def __init__(self): - load_prc_file_data("", """ - textures-power-2 none - window-type offscreen - win-size 100 100 - gl-coordinate-system default - notify-level-display error - print-pipe-types #f - gl-version 4 3 - """) - - ShowBase.__init__(self) - - base_path = realpath(dirname(__file__)) - os.chdir(base_path) - - node = NodePath("") - w, h, d = 128, 128, 128 - - # High-Res 128^3 texture - print("Computing high-res noise") - voxel_grid = Texture("voxels") - voxel_grid.setup_3d_texture(w, h, d, Texture.T_unsigned_byte, Texture.F_rgba8) - cshader = Shader.load_compute(Shader.SL_GLSL, "generate_noise1.compute.glsl") - node.set_shader(cshader) - node.set_shader_input("DestTex", voxel_grid) - attr = node.get_attrib(ShaderAttrib) - self.graphicsEngine.dispatch_compute( - ((w + 7) // 8, (h + 7) // 8, (d + 3) // 4), attr, self.win.gsg) - self.graphicsEngine.extract_texture_data(voxel_grid, self.win.gsg) - - print("Writing data ..") - # voxel_grid.write(Filename.from_os_specific(join("result/", "#.png")), 0, 0, True, False) - voxel_grid.write("noise1-data.txo.pz") - - # Low-Res 32^3 texture - print("Computing low-res noise") - w, h, d = 32, 32, 32 - voxel_grid = Texture("voxels") - voxel_grid.setup_3d_texture(w, h, d, Texture.T_unsigned_byte, Texture.F_rgba8) - cshader = Shader.load_compute(Shader.SL_GLSL, "generate_noise2.compute.glsl") - node.set_shader(cshader) - node.set_shader_input("DestTex", voxel_grid) - attr = node.get_attrib(ShaderAttrib) - self.graphicsEngine.dispatch_compute( - ((w + 7) // 8, (h + 7) // 8, (d + 3) // 4), attr, self.win.gsg) - self.graphicsEngine.extract_texture_data(voxel_grid, self.win.gsg) - - print("Writing data ..") - # voxel_grid.write(Filename.from_os_specific(join("result2/", "#.png")), 0, 0, True, False) - voxel_grid.write("noise2-data.txo.pz") - - -Application() +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import print_function, division + +import os +from os.path import dirname, realpath +from panda3d.core import * # noqa +from direct.showbase.ShowBase import ShowBase + + +class Application(ShowBase): + def __init__(self): + load_prc_file_data("", """ + textures-power-2 none + window-type offscreen + win-size 100 100 + gl-coordinate-system default + notify-level-display error + print-pipe-types #f + gl-version 4 3 + """) + + ShowBase.__init__(self) + + base_path = realpath(dirname(__file__)) + os.chdir(base_path) + + node = NodePath("") + w, h, d = 128, 128, 128 + + # High-Res 128^3 texture + print("Computing high-res noise") + voxel_grid = Texture("voxels") + voxel_grid.setup_3d_texture(w, h, d, Texture.T_unsigned_byte, Texture.F_rgba8) + cshader = Shader.load_compute(Shader.SL_GLSL, "generate_noise1.compute.glsl") + node.set_shader(cshader) + node.set_shader_input("DestTex", voxel_grid) + attr = node.get_attrib(ShaderAttrib) + self.graphicsEngine.dispatch_compute( + ((w + 7) // 8, (h + 7) // 8, (d + 3) // 4), attr, self.win.gsg) + self.graphicsEngine.extract_texture_data(voxel_grid, self.win.gsg) + + print("Writing data ..") + # voxel_grid.write(Filename.from_os_specific(join("result/", "#.png")), 0, 0, True, False) + voxel_grid.write("noise1-data.txo.pz") + + # Low-Res 32^3 texture + print("Computing low-res noise") + w, h, d = 32, 32, 32 + voxel_grid = Texture("voxels") + voxel_grid.setup_3d_texture(w, h, d, Texture.T_unsigned_byte, Texture.F_rgba8) + cshader = Shader.load_compute(Shader.SL_GLSL, "generate_noise2.compute.glsl") + node.set_shader(cshader) + node.set_shader_input("DestTex", voxel_grid) + attr = node.get_attrib(ShaderAttrib) + self.graphicsEngine.dispatch_compute( + ((w + 7) // 8, (h + 7) // 8, (d + 3) // 4), attr, self.win.gsg) + self.graphicsEngine.extract_texture_data(voxel_grid, self.win.gsg) + + print("Writing data ..") + # voxel_grid.write(Filename.from_os_specific(join("result2/", "#.png")), 0, 0, True, False) + voxel_grid.write("noise2-data.txo.pz") + + +Application() diff --git a/RenderPipelineFile/rpplugins/clouds/shader/apply_clouds.frag.glsl b/RenderPipelineFile/rpplugins/clouds/shader/apply_clouds.frag.glsl index e64983cb..bfd43ada 100644 --- a/RenderPipelineFile/rpplugins/clouds/shader/apply_clouds.frag.glsl +++ b/RenderPipelineFile/rpplugins/clouds/shader/apply_clouds.frag.glsl @@ -1,47 +1,47 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" - -uniform sampler2D CloudsTex; -uniform sampler2D ShadedScene; -out vec4 result; - -void main() { - vec2 texcoord = get_texcoord(); - vec4 scene_color = textureLod(ShadedScene, texcoord, 0); - vec4 cloud_color = textureLod(CloudsTex, texcoord, 0); - - #if !DEBUG_MODE - result = scene_color * (1 - cloud_color.w) + cloud_color; - #else - result = scene_color; - #endif - - result.w = scene_color.w; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" + +uniform sampler2D CloudsTex; +uniform sampler2D ShadedScene; +out vec4 result; + +void main() { + vec2 texcoord = get_texcoord(); + vec4 scene_color = textureLod(ShadedScene, texcoord, 0); + vec4 cloud_color = textureLod(CloudsTex, texcoord, 0); + + #if !DEBUG_MODE + result = scene_color * (1 - cloud_color.w) + cloud_color; + #else + result = scene_color; + #endif + + result.w = scene_color.w; +} diff --git a/RenderPipelineFile/rpplugins/clouds/shader/render_clouds.frag.glsl b/RenderPipelineFile/rpplugins/clouds/shader/render_clouds.frag.glsl index e2e46658..b9791a10 100644 --- a/RenderPipelineFile/rpplugins/clouds/shader/render_clouds.frag.glsl +++ b/RenderPipelineFile/rpplugins/clouds/shader/render_clouds.frag.glsl @@ -1,224 +1,224 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// Renders the clouds -// WORK IN PROGRESS - This shader is unfinished and not cleaned up yet. - -#define USE_TIME_OF_DAY 1 -#pragma include "render_pipeline_base.inc.glsl" - -#define USE_GBUFFER_EXTENSIONS -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "includes/light_culling.inc.glsl" -#pragma include "includes/noise.inc.glsl" - -uniform sampler3D Noise1; -uniform sampler3D Noise2; -uniform sampler2D WeatherTex; - -out vec4 result; - -const float KM = 1000.0; -const float METER = 1.0; - -const float earth_radius = 6371.0 * KM; -const vec3 earth_mid = vec3(0, 0, -earth_radius); -const float cloud_start = earth_radius + 1.3 * KM; -const float cloud_end = earth_radius + 25.0 * KM; - - -float GetHeightFractionForPoint(vec3 inPosition, vec2 inCloudMinMax) -{ - float height_fraction = (inPosition.z - inCloudMinMax.x) / (inCloudMinMax.y - inCloudMinMax.x); - return saturate(height_fraction); -} - -float GetDensityHeightGradientForPoint(vec3 p, vec3 weatherData) { - // return saturate(20.0 * (1.0 - max(0, p.z / 0.3)) ); // XXX - // return p.z > 0.1 ? 0.0 : 1.0; - // return pow(p.z, 0.3); - return 1.0; -} - -float SampleCloudDensity(vec3 p, vec3 weather_data, float mip_level) -{ - - // float3 wind_direction = float3 (1.0 , 0.0 , 0.0) ; - // float cloud_speed = 10.0; - // float cloud_top_offset = 500.0; - // p += height_fraction * wind_direction * cloud_top_offset; - - vec4 low_frequency_noises = textureLod(Noise1, p * 0.6, mip_level); - float low_freq_FBM = (low_frequency_noises.g * 0.625) - + (low_frequency_noises.b * 0.25) - + (low_frequency_noises.a * 0.125); - - float base_cloud = max(0, square(low_frequency_noises.x * low_frequency_noises.y) - 0.07) ; - // base_cloud = pow(base_cloud, 10.0) * 10.0; - - // base_cloud *= max(0, low_frequency_noises.w * 1.4 - 0.2); - // base_cloud *= max(0, low_frequency_noises.z * 1.5 - 0.2); - - float density_height_gradient = GetDensityHeightGradientForPoint(p, weather_data); - base_cloud *= density_height_gradient; - - float cloud_coverage = weather_data.r; - - // base_cloud *= cloud_coverage; - - vec3 high_frequency_noises = textureLod(Noise2, p * 5.63534, mip_level).rgb; - - // float high_freq_FBM = (high_frequency_noises.r * 0.625); - // + (high_frequency_noises.g * 0.25) - // + (high_frequency_noises.b * 0.125); - - // base_cloud = mix(high_frequency_noises.y * base_cloud, 1, base_cloud); - - base_cloud -= high_frequency_noises.y * 0.23 * (1 - base_cloud); - // base_cloud *= 3.0; - base_cloud *= 125.0 * 256.0 / GET_SETTING(clouds, raymarch_steps); - - return saturate(base_cloud); -} - - -vec2 get_cloud_coord(vec3 pos) { - vec2 xy_coord = pos.xy / (cloud_end - cloud_start); - xy_coord.xy /= 1.0 + 0.1 * length(xy_coord); - // xy_coord.xy += 0.5; - // xy_coord *= 0.5; - return xy_coord; -} - -float HenyeyGreenstein(vec3 inLightVector, vec3 inViewVector, float inG) -{ - float cos_angle = dot(normalize(inLightVector), normalize(inViewVector)); - return ((1.0 - inG * inG) / pow((1.0 + inG * inG - 2.0 * inG * cos_angle), - 3.0 / 2.0)) / 4.0 * M_PI; -} - -void main() { - int num_samples = GET_SETTING(clouds, raymarch_steps); - // int num_samples = 256; - - vec2 texcoord = get_half_texcoord(); - vec3 wind_offs = vec3(0.2, 0.3, 0) * 0.052 * MainSceneData.frame_time; - - vec3 pos = get_gbuffer_position(GBuffer, texcoord); - vec3 ray_start = MainSceneData.camera_pos; - vec3 ray_dir = normalize(pos - ray_start); - - vec3 view_vector = normalize(MainSceneData.camera_pos - pos); - - if (!is_skybox(pos) || ray_dir.z < 0.0) { - result = vec4(0); - return; - } - - float t_low, t_high, tmp; - - Sphere earth_sphere; - earth_sphere.pos = earth_mid; - earth_sphere.radius = cloud_start; - bool rb = ray_sphere_intersection(earth_sphere, ray_start, ray_dir, t_low, tmp); - earth_sphere.radius = cloud_end; - bool rt = ray_sphere_intersection(earth_sphere, ray_start, ray_dir, t_high, tmp); - - if (t_low < 0.0) t_low = 0.0; - if (t_high < 0.0 || distance(t_high, t_low) < 0.01) { - result = vec4(0.2, 0, 0, 0); - return; - } - - // Get start and end in cloud space coordinates - vec3 trace_start = vec3(get_cloud_coord(ray_start + t_low * ray_dir), 0.0); - vec3 trace_end = vec3(get_cloud_coord(ray_start + t_high * ray_dir), 1.0); - trace_start += wind_offs; - trace_end += wind_offs; - - // trace_start.xyz += (noise*2.0-1.0) * 0.004; - vec3 trace_step = (trace_end - trace_start) / float(num_samples); - // trace_step.xyz += (noise*2.0-1.0) * 0.015 / num_samples; - - float density = 0.0; - float cloud_test = 0.0; - int zero_density_sample_count = 0; - float mip_level = 0; - - float jitter = abs(rand(ivec2(gl_FragCoord.xy))); - - vec3 p = trace_start + (1 + jitter) * trace_step; - - vec3 accum_color = vec3(0); - - vec3 sun_vector = get_sun_vector(); - - vec3 weather_data = texture(WeatherTex, p.xy).xyz; - for (int i = 0; i < num_samples - 1; ++i) - { - float sampled_density = SampleCloudDensity(p, weather_data, mip_level) * 0.2; - float sampled_sun_density = 0.0; - for (int k = 1; k < 3; ++k) { - sampled_sun_density += SampleCloudDensity( - p + sun_vector * 1.0 / 256.0 * k * k, weather_data, mip_level); - } - sampled_density *= (1 - density); - density += sampled_density; - accum_color += ((0.05 + 0.99 * p.z * p.z) * sampled_density * - (1.0 - sampled_sun_density / 3.0)); - - p += trace_step; - } - - float accum_weight = density; - - float light_samples = density * 1.0; - - float powder_sugar_effect = 1.0 - exp(- light_samples * 2.0); - float beers_law = exp(-light_samples); - float light_energy = 2.0 * beers_law * powder_sugar_effect; - - accum_color *= light_energy * 2.0; - accum_color *= vec3(HenyeyGreenstein(sun_vector, -view_vector, 0.2)) * 1.0; - - float sun_influence = pow(max(0, dot(ray_dir, sun_vector)), 25.0) + 0.0; - vec3 sun_color = sun_influence * 100.0 * TimeOfDay.scattering.sun_color; - - accum_color *= 1.0 + sun_color * max(0, 1 - 0.7 * density); - accum_color *= TimeOfDay.clouds.cloud_brightness * 20.0 * vec3(10, 10, 15); - accum_color *= TimeOfDay.scattering.sun_intensity / 150.0; - accum_color *= TimeOfDay.scattering.sun_color; - - - // Don't render clouds at obligue angles - float horizon = pow(saturate(ray_dir.z * 1.0), 0.1); - accum_color *= horizon; - accum_weight *= horizon; - - result = vec4(accum_color, accum_weight); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// Renders the clouds +// WORK IN PROGRESS - This shader is unfinished and not cleaned up yet. + +#define USE_TIME_OF_DAY 1 +#pragma include "render_pipeline_base.inc.glsl" + +#define USE_GBUFFER_EXTENSIONS +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "includes/light_culling.inc.glsl" +#pragma include "includes/noise.inc.glsl" + +uniform sampler3D Noise1; +uniform sampler3D Noise2; +uniform sampler2D WeatherTex; + +out vec4 result; + +const float KM = 1000.0; +const float METER = 1.0; + +const float earth_radius = 6371.0 * KM; +const vec3 earth_mid = vec3(0, 0, -earth_radius); +const float cloud_start = earth_radius + 1.3 * KM; +const float cloud_end = earth_radius + 25.0 * KM; + + +float GetHeightFractionForPoint(vec3 inPosition, vec2 inCloudMinMax) +{ + float height_fraction = (inPosition.z - inCloudMinMax.x) / (inCloudMinMax.y - inCloudMinMax.x); + return saturate(height_fraction); +} + +float GetDensityHeightGradientForPoint(vec3 p, vec3 weatherData) { + // return saturate(20.0 * (1.0 - max(0, p.z / 0.3)) ); // XXX + // return p.z > 0.1 ? 0.0 : 1.0; + // return pow(p.z, 0.3); + return 1.0; +} + +float SampleCloudDensity(vec3 p, vec3 weather_data, float mip_level) +{ + + // float3 wind_direction = float3 (1.0 , 0.0 , 0.0) ; + // float cloud_speed = 10.0; + // float cloud_top_offset = 500.0; + // p += height_fraction * wind_direction * cloud_top_offset; + + vec4 low_frequency_noises = textureLod(Noise1, p * 0.6, mip_level); + float low_freq_FBM = (low_frequency_noises.g * 0.625) + + (low_frequency_noises.b * 0.25) + + (low_frequency_noises.a * 0.125); + + float base_cloud = max(0, square(low_frequency_noises.x * low_frequency_noises.y) - 0.07) ; + // base_cloud = pow(base_cloud, 10.0) * 10.0; + + // base_cloud *= max(0, low_frequency_noises.w * 1.4 - 0.2); + // base_cloud *= max(0, low_frequency_noises.z * 1.5 - 0.2); + + float density_height_gradient = GetDensityHeightGradientForPoint(p, weather_data); + base_cloud *= density_height_gradient; + + float cloud_coverage = weather_data.r; + + // base_cloud *= cloud_coverage; + + vec3 high_frequency_noises = textureLod(Noise2, p * 5.63534, mip_level).rgb; + + // float high_freq_FBM = (high_frequency_noises.r * 0.625); + // + (high_frequency_noises.g * 0.25) + // + (high_frequency_noises.b * 0.125); + + // base_cloud = mix(high_frequency_noises.y * base_cloud, 1, base_cloud); + + base_cloud -= high_frequency_noises.y * 0.23 * (1 - base_cloud); + // base_cloud *= 3.0; + base_cloud *= 125.0 * 256.0 / GET_SETTING(clouds, raymarch_steps); + + return saturate(base_cloud); +} + + +vec2 get_cloud_coord(vec3 pos) { + vec2 xy_coord = pos.xy / (cloud_end - cloud_start); + xy_coord.xy /= 1.0 + 0.1 * length(xy_coord); + // xy_coord.xy += 0.5; + // xy_coord *= 0.5; + return xy_coord; +} + +float HenyeyGreenstein(vec3 inLightVector, vec3 inViewVector, float inG) +{ + float cos_angle = dot(normalize(inLightVector), normalize(inViewVector)); + return ((1.0 - inG * inG) / pow((1.0 + inG * inG - 2.0 * inG * cos_angle), + 3.0 / 2.0)) / 4.0 * M_PI; +} + +void main() { + int num_samples = GET_SETTING(clouds, raymarch_steps); + // int num_samples = 256; + + vec2 texcoord = get_half_texcoord(); + vec3 wind_offs = vec3(0.2, 0.3, 0) * 0.052 * MainSceneData.frame_time; + + vec3 pos = get_gbuffer_position(GBuffer, texcoord); + vec3 ray_start = MainSceneData.camera_pos; + vec3 ray_dir = normalize(pos - ray_start); + + vec3 view_vector = normalize(MainSceneData.camera_pos - pos); + + if (!is_skybox(pos) || ray_dir.z < 0.0) { + result = vec4(0); + return; + } + + float t_low, t_high, tmp; + + Sphere earth_sphere; + earth_sphere.pos = earth_mid; + earth_sphere.radius = cloud_start; + bool rb = ray_sphere_intersection(earth_sphere, ray_start, ray_dir, t_low, tmp); + earth_sphere.radius = cloud_end; + bool rt = ray_sphere_intersection(earth_sphere, ray_start, ray_dir, t_high, tmp); + + if (t_low < 0.0) t_low = 0.0; + if (t_high < 0.0 || distance(t_high, t_low) < 0.01) { + result = vec4(0.2, 0, 0, 0); + return; + } + + // Get start and end in cloud space coordinates + vec3 trace_start = vec3(get_cloud_coord(ray_start + t_low * ray_dir), 0.0); + vec3 trace_end = vec3(get_cloud_coord(ray_start + t_high * ray_dir), 1.0); + trace_start += wind_offs; + trace_end += wind_offs; + + // trace_start.xyz += (noise*2.0-1.0) * 0.004; + vec3 trace_step = (trace_end - trace_start) / float(num_samples); + // trace_step.xyz += (noise*2.0-1.0) * 0.015 / num_samples; + + float density = 0.0; + float cloud_test = 0.0; + int zero_density_sample_count = 0; + float mip_level = 0; + + float jitter = abs(rand(ivec2(gl_FragCoord.xy))); + + vec3 p = trace_start + (1 + jitter) * trace_step; + + vec3 accum_color = vec3(0); + + vec3 sun_vector = get_sun_vector(); + + vec3 weather_data = texture(WeatherTex, p.xy).xyz; + for (int i = 0; i < num_samples - 1; ++i) + { + float sampled_density = SampleCloudDensity(p, weather_data, mip_level) * 0.2; + float sampled_sun_density = 0.0; + for (int k = 1; k < 3; ++k) { + sampled_sun_density += SampleCloudDensity( + p + sun_vector * 1.0 / 256.0 * k * k, weather_data, mip_level); + } + sampled_density *= (1 - density); + density += sampled_density; + accum_color += ((0.05 + 0.99 * p.z * p.z) * sampled_density * + (1.0 - sampled_sun_density / 3.0)); + + p += trace_step; + } + + float accum_weight = density; + + float light_samples = density * 1.0; + + float powder_sugar_effect = 1.0 - exp(- light_samples * 2.0); + float beers_law = exp(-light_samples); + float light_energy = 2.0 * beers_law * powder_sugar_effect; + + accum_color *= light_energy * 2.0; + accum_color *= vec3(HenyeyGreenstein(sun_vector, -view_vector, 0.2)) * 1.0; + + float sun_influence = pow(max(0, dot(ray_dir, sun_vector)), 25.0) + 0.0; + vec3 sun_color = sun_influence * 100.0 * TimeOfDay.scattering.sun_color; + + accum_color *= 1.0 + sun_color * max(0, 1 - 0.7 * density); + accum_color *= TimeOfDay.clouds.cloud_brightness * 20.0 * vec3(10, 10, 15); + accum_color *= TimeOfDay.scattering.sun_intensity / 150.0; + accum_color *= TimeOfDay.scattering.sun_color; + + + // Don't render clouds at obligue angles + float horizon = pow(saturate(ray_dir.z * 1.0), 0.1); + accum_color *= horizon; + accum_weight *= horizon; + + result = vec4(accum_color, accum_weight); +} diff --git a/RenderPipelineFile/rpplugins/color_correction/auto_exposure_stage.py b/RenderPipelineFile/rpplugins/color_correction/auto_exposure_stage.py index bd116c91..4ad28d6d 100644 --- a/RenderPipelineFile/rpplugins/color_correction/auto_exposure_stage.py +++ b/RenderPipelineFile/rpplugins/color_correction/auto_exposure_stage.py @@ -1,114 +1,114 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import division -from panda3d.core import Vec4 - -from rpcore.render_stage import RenderStage -from rpcore.globals import Globals -from rpcore.image import Image - - -class AutoExposureStage(RenderStage): - - required_pipes = ["ShadedScene"] - required_inputs = [] - - @property - def produced_pipes(self): - return {"ShadedScene": self.target_apply.color_tex, - "Exposure": self.tex_exposure} - - def create(self): - - # Create the target which converts the scene color to a luminance - self.target_lum = self.create_target("GetLuminance") - self.target_lum.size = -4 - self.target_lum.add_color_attachment(bits=(16, 0, 0, 0)) - self.target_lum.prepare_buffer() - - self.mip_targets = [] - - # Create the storage for the exposure, this stores the current and last - # frames exposure - # XXX: We have to use F_r16 instead of F_r32 because of a weird nvidia - # driver bug! However, 16 bits should be enough for sure. - self.tex_exposure = Image.create_buffer("ExposureStorage", 1, "R16") - self.tex_exposure.set_clear_color(Vec4(0.5)) - self.tex_exposure.clear_image() - - # Create the target which extracts the exposure from the average brightness - self.target_analyze = self.create_target("AnalyzeBrightness") - self.target_analyze.size = 1, 1 - self.target_analyze.prepare_buffer() - - self.target_analyze.set_shader_input("ExposureStorage", self.tex_exposure) - - # Create the target which applies the generated exposure to the scene - self.target_apply = self.create_target("ApplyExposure") - self.target_apply.add_color_attachment(bits=16) - self.target_apply.prepare_buffer() - self.target_apply.set_shader_input("Exposure", self.tex_exposure) - - def set_dimensions(self): - for old_target in self.mip_targets: - self.remove_target(old_target) - - wsize_x = (Globals.resolution.x + 3) // 4 - wsize_y = (Globals.resolution.y + 3) // 4 - - # Create the targets which downscale the luminance mipmaps - self.mip_targets = [] - last_tex = self.target_lum.color_tex - while wsize_x >= 4 or wsize_y >= 4: - wsize_x = (wsize_x + 3) // 4 - wsize_y = (wsize_y + 3) // 4 - - mip_target = self.create_target("DScaleLum:S" + str(wsize_x)) - mip_target.add_color_attachment(bits=(16, 0, 0, 0)) - mip_target.size = wsize_x, wsize_y - mip_target.sort = self.target_lum.sort + len(self.mip_targets) - mip_target.prepare_buffer() - mip_target.set_shader_input("SourceTex", last_tex) - self.mip_targets.append(mip_target) - last_tex = mip_target.color_tex - - self.target_analyze.set_shader_input("DownscaledTex", self.mip_targets[-1].color_tex) - - # Shaders might not have been loaded at this point - if hasattr(self, "mip_shader"): - for target in self.mip_targets: - target.shader = self.mip_shader - - def reload_shaders(self): - self.target_lum.shader = self.load_plugin_shader("generate_luminance.frag.glsl") - self.target_analyze.shader = self.load_plugin_shader("analyze_brightness.frag.glsl") - self.target_apply.shader = self.load_plugin_shader("apply_exposure.frag.glsl") - - # Keep shader as reference, required when resizing - self.mip_shader = self.load_plugin_shader("downscale_luminance.frag.glsl") - for target in self.mip_targets: - target.shader = self.mip_shader +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import division +from panda3d.core import Vec4 + +from rpcore.render_stage import RenderStage +from rpcore.globals import Globals +from rpcore.image import Image + + +class AutoExposureStage(RenderStage): + + required_pipes = ["ShadedScene"] + required_inputs = [] + + @property + def produced_pipes(self): + return {"ShadedScene": self.target_apply.color_tex, + "Exposure": self.tex_exposure} + + def create(self): + + # Create the target which converts the scene color to a luminance + self.target_lum = self.create_target("GetLuminance") + self.target_lum.size = -4 + self.target_lum.add_color_attachment(bits=(16, 0, 0, 0)) + self.target_lum.prepare_buffer() + + self.mip_targets = [] + + # Create the storage for the exposure, this stores the current and last + # frames exposure + # XXX: We have to use F_r16 instead of F_r32 because of a weird nvidia + # driver bug! However, 16 bits should be enough for sure. + self.tex_exposure = Image.create_buffer("ExposureStorage", 1, "R16") + self.tex_exposure.set_clear_color(Vec4(0.5)) + self.tex_exposure.clear_image() + + # Create the target which extracts the exposure from the average brightness + self.target_analyze = self.create_target("AnalyzeBrightness") + self.target_analyze.size = 1, 1 + self.target_analyze.prepare_buffer() + + self.target_analyze.set_shader_input("ExposureStorage", self.tex_exposure) + + # Create the target which applies the generated exposure to the scene + self.target_apply = self.create_target("ApplyExposure") + self.target_apply.add_color_attachment(bits=16) + self.target_apply.prepare_buffer() + self.target_apply.set_shader_input("Exposure", self.tex_exposure) + + def set_dimensions(self): + for old_target in self.mip_targets: + self.remove_target(old_target) + + wsize_x = (Globals.resolution.x + 3) // 4 + wsize_y = (Globals.resolution.y + 3) // 4 + + # Create the targets which downscale the luminance mipmaps + self.mip_targets = [] + last_tex = self.target_lum.color_tex + while wsize_x >= 4 or wsize_y >= 4: + wsize_x = (wsize_x + 3) // 4 + wsize_y = (wsize_y + 3) // 4 + + mip_target = self.create_target("DScaleLum:S" + str(wsize_x)) + mip_target.add_color_attachment(bits=(16, 0, 0, 0)) + mip_target.size = wsize_x, wsize_y + mip_target.sort = self.target_lum.sort + len(self.mip_targets) + mip_target.prepare_buffer() + mip_target.set_shader_input("SourceTex", last_tex) + self.mip_targets.append(mip_target) + last_tex = mip_target.color_tex + + self.target_analyze.set_shader_input("DownscaledTex", self.mip_targets[-1].color_tex) + + # Shaders might not have been loaded at this point + if hasattr(self, "mip_shader"): + for target in self.mip_targets: + target.shader = self.mip_shader + + def reload_shaders(self): + self.target_lum.shader = self.load_plugin_shader("generate_luminance.frag.glsl") + self.target_analyze.shader = self.load_plugin_shader("analyze_brightness.frag.glsl") + self.target_apply.shader = self.load_plugin_shader("apply_exposure.frag.glsl") + + # Keep shader as reference, required when resizing + self.mip_shader = self.load_plugin_shader("downscale_luminance.frag.glsl") + for target in self.mip_targets: + target.shader = self.mip_shader diff --git a/RenderPipelineFile/rpplugins/color_correction/color_correction_stage.py b/RenderPipelineFile/rpplugins/color_correction/color_correction_stage.py index 34688431..4b40fca7 100644 --- a/RenderPipelineFile/rpplugins/color_correction/color_correction_stage.py +++ b/RenderPipelineFile/rpplugins/color_correction/color_correction_stage.py @@ -1,45 +1,45 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.render_stage import RenderStage - - -class ColorCorrectionStage(RenderStage): - - required_inputs = [] - required_pipes = ["ShadedScene"] - - @property - def produced_pipes(self): - return {"ShadedScene": self.target.color_tex} - - def create(self): - self.target = self.create_target("PostFX") - self.target.add_color_attachment(bits=16) - self.target.prepare_buffer() - - def reload_shaders(self): - self.target.shader = self.load_plugin_shader("post_fx.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.render_stage import RenderStage + + +class ColorCorrectionStage(RenderStage): + + required_inputs = [] + required_pipes = ["ShadedScene"] + + @property + def produced_pipes(self): + return {"ShadedScene": self.target.color_tex} + + def create(self): + self.target = self.create_target("PostFX") + self.target.add_color_attachment(bits=16) + self.target.prepare_buffer() + + def reload_shaders(self): + self.target.shader = self.load_plugin_shader("post_fx.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/color_correction/config.yaml b/RenderPipelineFile/rpplugins/color_correction/config.yaml index 64aeb7a6..ffec0f91 100644 --- a/RenderPipelineFile/rpplugins/color_correction/config.yaml +++ b/RenderPipelineFile/rpplugins/color_correction/config.yaml @@ -1,333 +1,333 @@ - -name: Color Correction -author: tobspr -version: 1.0 -description: > - This plugin adds support for color correction, vignetting and chromatic - abberation. - -settings: !!omap - - - tonemap_operator: - type: enum - values: [optimized, reinhard, uncharted2, exponential, exponential2, none] - default: uncharted2 - label: Tonemap Operator - shader_runtime: true - description: > - This specifies the tonemapping operator, all operators implement - sRGB color correction as well, except for the none operator which - just passes through the color. None is just for reference, - and should not be used. - - # Reinhard Tonemapping settings - - - reinhard_version: - display_if: {tonemap_operator: "reinhard"} - type: enum - values: [rgb, luminance, white_preserve, luminosity] - default: rgb - shader_runtime: true - label: Reinhard Version - description: > - Choose between different versions of the Reinhard Operator. rgb performs - the operator on the rgb value, luminance on the brightness of the color. - white_preserve also uses the luma value, but tries to preserve the whites. - - # Exponential Tonemapping settings - - - exponential_factor: - display_if: {tonemap_operator: "exponential"} - type: float - range: [0.01, 5.0] - default: 1.0 - shader_runtime: true - label: Exponential factor - description: > - The factor for the exponential mapping. Assuming the factor is named - f, then the output color is computed with 1 - exp(-f*color). - - - # Uncharted 2 Tonemapping settings - - - uc2t_shoulder_strength: - display_if: {tonemap_operator: "uncharted2"} - type: float - range: [0.01, 1.0] - default: 0.22 - shader_runtime: true - label: UC2 Shoulder Strength - description: > - Shoulder Strength, controls the appearance of the bright parts - of the image (the last part of the color curve). - - - uc2t_linear_strength: - display_if: {tonemap_operator: "uncharted2"} - type: float - range: [0.01, 1.0] - default: 0.3 - shader_runtime: true - label: UC2 Linear Strength - description: > - Linear factor, higher values produce a more grayish image, while - lower values produce a more saturated image. - - - uc2t_linear_angle: - display_if: {tonemap_operator: "uncharted2"} - type: float - range: [0.01, 1.0] - default: 0.1 - shader_runtime: true - label: UC2 Linear Angle - description: > - Linear Angle, higher values produce a brighter but more grayish image, - lower values produce a more saturated image. - - - - uc2t_toe_strength: - display_if: {tonemap_operator: "uncharted2"} - type: float - range: [0.01, 1.0] - default: 0.2 - shader_runtime: true - label: UC2 Toe Strength - description: > - Toe Strength, controls the appearance of the dark parts of the image, - (the first part of the color curve). Higher values mean more dark - spots, while lower values will cut-off less dark values. - - - uc2t_toe_numerator: - display_if: {tonemap_operator: "uncharted2"} - type: float - range: [0.0001, 0.1] - default: 0.01 - shader_runtime: true - label: UC2 Toe Numerator - description: > - Toe Numerator, controls the appearance of the dark parts similar to - the toe strength. This is a bias, values below this setting are - completely dark. - - - uc2t_toe_denumerator: - display_if: {tonemap_operator: "uncharted2"} - type: float - range: [0.01, 1.0] - default: 0.3 - shader_runtime: true - label: UC2 Toe Denumerator - description: > - Toe Denumerator, controls the appearance of the dark parts similar to - the toe strength, but instead of cutting of the dark values it - makes them brighter. - - - uc2t_reference_white: - display_if: {tonemap_operator: "uncharted2"} - type: float - range: [0.1, 20.0] - default: 11.2 - shader_runtime: true - label: UC2 Reference White - description: > - Reference White, higher values will produce a darker image, and - vice-versa. This basically just scales the color linear inverse. - - - - vignette_strength: - type: float - range: [0.0, 1.0] - default: 0.35 - shader_runtime: true - label: Vignette Strength - description: > - Controls the strength of the vignette, which darkens the corners - of the image. A strength of 0 means no vignette, a strength of 1 - means a almost completely black vignette. - - - film_grain_strength: - type: float - range: [0.0, 1.0] - default: 0.125 - shader_runtime: true - label: Film Grain Strength - description: > - Controls the strength of the film grain, which simulates the inaccuracies - on a film. This is also useful to add some more details to textures, however - care to not overdo the effect. - - - # Color LUT - - - color_lut: - type: path - default: film_luts/default_lut.png - label: Color LUT - file_type: Color LUTs (*.png) - base_path: film_luts - runtime: true - description: > - Specifies a Color LUT (Look-Up-Table) to use. Have a look at the Render Pipeline - documentation for further information. - - # Chromatic aberration - - - use_chromatic_aberration: - type: bool - default: true - shader_runtime: true - label: Enable Chromatic Aberration - description: > - Enables chromatic aberration, a camera effect caused due to the - different refraction of the wavelengths through a lens. - - - chromatic_aberration_strength: - display_if: {use_chromatic_aberration: true} - type: float - range: [0.009, 2.0] - default: 0.5 - shader_runtime: true - label: Chromatic Aberration Strength - description: > - Strength of the chromatic abberation, you should not overdo this since - it can lead to very unrealistic results fast. - - - chromatic_aberration_samples: - display_if: {use_chromatic_aberration: true} - type: int - range: [1, 5] - default: 2 - shader_runtime: true - label: Chromatic Aberration Samples - description: > - Amount of samples to use for blurring the screen edges when using - chromatic abberation. A higher amount of samples produces a smoother - blur but also increases the performance cost by a lot. - - - # Automatic exposure settings - - - manual_camera_parameters: - type: bool - default: false - label: Manual camera parameters - description: > - When set to true, enables you to set camera parameters such as - ISO, shutter and f-stops. If set to false, the average scene - luminance will be used to compute the exposure. - - - min_exposure_value: - display_if: {manual_camera_parameters: false} - type: float - range: [0.0, 0.1] - default: 0.01 - label: Minimum Exposure - shader_runtime: true - description: > - The minimum exposure value, this prevents the automatic exposure from overfitting - on bright areas - - - max_exposure_value: - display_if: {manual_camera_parameters: false} - type: float - range: [0.0, 1.0] - default: 1.0 - shader_runtime: true - label: Maximum Exposure - description: > - The maximum exposure value, this prevents the automatic exposure from overfitting - on dark areas. - - - exposure_scale: - display_if: {manual_camera_parameters: false} - type: float - range: [0.0, 10.0] - default: 1.0 - label: Exposure Scale - shader_runtime: true - description: > - This is a fixed value which gets added to the average brightness of the - scene. A value greater 1 will increase the scene brightness, while a value - smaller than 1 will increase it. - - - brightness_adaption_rate: - display_if: {manual_camera_parameters: false} - type: float - range: [0.01, 5.0] - default: 3.6 - shader_runtime: true - label: Brightness Adaption Rate - description: > - Adaption rate when moving from dark to bright areas. - Usually faster (= higher) than the rate when moving from bright to dark areas. - - - darkness_adaption_rate: - display_if: {manual_camera_parameters: false} - type: float - range: [0.01, 5.0] - default: 0.7 - shader_runtime: true - label: Darkness Adaption Rate - description: > - Adaption rate when moving from bright to dark areas. - Usually slower (= smaller) than the rate when moving from dark to bright areas. - - - - # Sharpen filter - - use_sharpen: - type: bool - default: true - label: Sharpen Filter - description: > - Enables a sharpen filter, which tries to increase texture detail. - - - sharpen_strength: - display_if: {use_sharpen: true} - type: float - range: [0.0, 5.0] - default: 0.5 - shader_runtime: true - label: Sharpen Strength - description: > - Controls the strength of the sharpen filter. - - - sharpen_twice: - display_if: {use_sharpen: true} - type: bool - default: false - label: Double-Sharpen - description: > - If this setting is true, the sharpen filter will be executed twice, - for an even stronger sharpen effect. - - -daytime_settings: !!omap - - - camera_iso: - type: scalar - unit: none - default: 100 - range: [20, 4000] - label: Camera ISO - logarithmic_factor: 2.0 - description: > - ISO value of the camera. Only has effect with manual camera parameters. - - - camera_shutter: - type: scalar - unit: none - default: 125 - range: [1, 200] - label: Camera Shutter Speed - description: > - The shutter speed of the camera. A value of 125 means a shutter - speed of 1 / 125 seconds. Only has effect with manual camera parameters. - - - camera_aperture: - type: scalar - unit: none - default: 16 - range: [1, 32] - label: Camera Aperture - description: > - Camera aperture, a value of 16 means an aperture of f/16. - Only has effect with manual camera parameters. + +name: Color Correction +author: tobspr +version: 1.0 +description: > + This plugin adds support for color correction, vignetting and chromatic + abberation. + +settings: !!omap + + - tonemap_operator: + type: enum + values: [optimized, reinhard, uncharted2, exponential, exponential2, none] + default: uncharted2 + label: Tonemap Operator + shader_runtime: true + description: > + This specifies the tonemapping operator, all operators implement + sRGB color correction as well, except for the none operator which + just passes through the color. None is just for reference, + and should not be used. + + # Reinhard Tonemapping settings + + - reinhard_version: + display_if: {tonemap_operator: "reinhard"} + type: enum + values: [rgb, luminance, white_preserve, luminosity] + default: rgb + shader_runtime: true + label: Reinhard Version + description: > + Choose between different versions of the Reinhard Operator. rgb performs + the operator on the rgb value, luminance on the brightness of the color. + white_preserve also uses the luma value, but tries to preserve the whites. + + # Exponential Tonemapping settings + + - exponential_factor: + display_if: {tonemap_operator: "exponential"} + type: float + range: [0.01, 5.0] + default: 1.0 + shader_runtime: true + label: Exponential factor + description: > + The factor for the exponential mapping. Assuming the factor is named + f, then the output color is computed with 1 - exp(-f*color). + + + # Uncharted 2 Tonemapping settings + + - uc2t_shoulder_strength: + display_if: {tonemap_operator: "uncharted2"} + type: float + range: [0.01, 1.0] + default: 0.22 + shader_runtime: true + label: UC2 Shoulder Strength + description: > + Shoulder Strength, controls the appearance of the bright parts + of the image (the last part of the color curve). + + - uc2t_linear_strength: + display_if: {tonemap_operator: "uncharted2"} + type: float + range: [0.01, 1.0] + default: 0.3 + shader_runtime: true + label: UC2 Linear Strength + description: > + Linear factor, higher values produce a more grayish image, while + lower values produce a more saturated image. + + - uc2t_linear_angle: + display_if: {tonemap_operator: "uncharted2"} + type: float + range: [0.01, 1.0] + default: 0.1 + shader_runtime: true + label: UC2 Linear Angle + description: > + Linear Angle, higher values produce a brighter but more grayish image, + lower values produce a more saturated image. + + + - uc2t_toe_strength: + display_if: {tonemap_operator: "uncharted2"} + type: float + range: [0.01, 1.0] + default: 0.2 + shader_runtime: true + label: UC2 Toe Strength + description: > + Toe Strength, controls the appearance of the dark parts of the image, + (the first part of the color curve). Higher values mean more dark + spots, while lower values will cut-off less dark values. + + - uc2t_toe_numerator: + display_if: {tonemap_operator: "uncharted2"} + type: float + range: [0.0001, 0.1] + default: 0.01 + shader_runtime: true + label: UC2 Toe Numerator + description: > + Toe Numerator, controls the appearance of the dark parts similar to + the toe strength. This is a bias, values below this setting are + completely dark. + + - uc2t_toe_denumerator: + display_if: {tonemap_operator: "uncharted2"} + type: float + range: [0.01, 1.0] + default: 0.3 + shader_runtime: true + label: UC2 Toe Denumerator + description: > + Toe Denumerator, controls the appearance of the dark parts similar to + the toe strength, but instead of cutting of the dark values it + makes them brighter. + + - uc2t_reference_white: + display_if: {tonemap_operator: "uncharted2"} + type: float + range: [0.1, 20.0] + default: 11.2 + shader_runtime: true + label: UC2 Reference White + description: > + Reference White, higher values will produce a darker image, and + vice-versa. This basically just scales the color linear inverse. + + + - vignette_strength: + type: float + range: [0.0, 1.0] + default: 0.35 + shader_runtime: true + label: Vignette Strength + description: > + Controls the strength of the vignette, which darkens the corners + of the image. A strength of 0 means no vignette, a strength of 1 + means a almost completely black vignette. + + - film_grain_strength: + type: float + range: [0.0, 1.0] + default: 0.125 + shader_runtime: true + label: Film Grain Strength + description: > + Controls the strength of the film grain, which simulates the inaccuracies + on a film. This is also useful to add some more details to textures, however + care to not overdo the effect. + + + # Color LUT + + - color_lut: + type: path + default: film_luts/default_lut.png + label: Color LUT + file_type: Color LUTs (*.png) + base_path: film_luts + runtime: true + description: > + Specifies a Color LUT (Look-Up-Table) to use. Have a look at the Render Pipeline + documentation for further information. + + # Chromatic aberration + + - use_chromatic_aberration: + type: bool + default: true + shader_runtime: true + label: Enable Chromatic Aberration + description: > + Enables chromatic aberration, a camera effect caused due to the + different refraction of the wavelengths through a lens. + + - chromatic_aberration_strength: + display_if: {use_chromatic_aberration: true} + type: float + range: [0.009, 2.0] + default: 0.5 + shader_runtime: true + label: Chromatic Aberration Strength + description: > + Strength of the chromatic abberation, you should not overdo this since + it can lead to very unrealistic results fast. + + - chromatic_aberration_samples: + display_if: {use_chromatic_aberration: true} + type: int + range: [1, 5] + default: 2 + shader_runtime: true + label: Chromatic Aberration Samples + description: > + Amount of samples to use for blurring the screen edges when using + chromatic abberation. A higher amount of samples produces a smoother + blur but also increases the performance cost by a lot. + + + # Automatic exposure settings + + - manual_camera_parameters: + type: bool + default: false + label: Manual camera parameters + description: > + When set to true, enables you to set camera parameters such as + ISO, shutter and f-stops. If set to false, the average scene + luminance will be used to compute the exposure. + + - min_exposure_value: + display_if: {manual_camera_parameters: false} + type: float + range: [0.0, 0.1] + default: 0.01 + label: Minimum Exposure + shader_runtime: true + description: > + The minimum exposure value, this prevents the automatic exposure from overfitting + on bright areas + + - max_exposure_value: + display_if: {manual_camera_parameters: false} + type: float + range: [0.0, 1.0] + default: 1.0 + shader_runtime: true + label: Maximum Exposure + description: > + The maximum exposure value, this prevents the automatic exposure from overfitting + on dark areas. + + - exposure_scale: + display_if: {manual_camera_parameters: false} + type: float + range: [0.0, 10.0] + default: 1.0 + label: Exposure Scale + shader_runtime: true + description: > + This is a fixed value which gets added to the average brightness of the + scene. A value greater 1 will increase the scene brightness, while a value + smaller than 1 will increase it. + + - brightness_adaption_rate: + display_if: {manual_camera_parameters: false} + type: float + range: [0.01, 5.0] + default: 3.6 + shader_runtime: true + label: Brightness Adaption Rate + description: > + Adaption rate when moving from dark to bright areas. + Usually faster (= higher) than the rate when moving from bright to dark areas. + + - darkness_adaption_rate: + display_if: {manual_camera_parameters: false} + type: float + range: [0.01, 5.0] + default: 0.7 + shader_runtime: true + label: Darkness Adaption Rate + description: > + Adaption rate when moving from bright to dark areas. + Usually slower (= smaller) than the rate when moving from dark to bright areas. + + + + # Sharpen filter + - use_sharpen: + type: bool + default: true + label: Sharpen Filter + description: > + Enables a sharpen filter, which tries to increase texture detail. + + - sharpen_strength: + display_if: {use_sharpen: true} + type: float + range: [0.0, 5.0] + default: 0.5 + shader_runtime: true + label: Sharpen Strength + description: > + Controls the strength of the sharpen filter. + + - sharpen_twice: + display_if: {use_sharpen: true} + type: bool + default: false + label: Double-Sharpen + description: > + If this setting is true, the sharpen filter will be executed twice, + for an even stronger sharpen effect. + + +daytime_settings: !!omap + + - camera_iso: + type: scalar + unit: none + default: 100 + range: [20, 4000] + label: Camera ISO + logarithmic_factor: 2.0 + description: > + ISO value of the camera. Only has effect with manual camera parameters. + + - camera_shutter: + type: scalar + unit: none + default: 125 + range: [1, 200] + label: Camera Shutter Speed + description: > + The shutter speed of the camera. A value of 125 means a shutter + speed of 1 / 125 seconds. Only has effect with manual camera parameters. + + - camera_aperture: + type: scalar + unit: none + default: 16 + range: [1, 32] + label: Camera Aperture + description: > + Camera aperture, a value of 16 means an aperture of f/16. + Only has effect with manual camera parameters. diff --git a/RenderPipelineFile/rpplugins/color_correction/manual_exposure_stage.py b/RenderPipelineFile/rpplugins/color_correction/manual_exposure_stage.py index 3859761c..83f67194 100644 --- a/RenderPipelineFile/rpplugins/color_correction/manual_exposure_stage.py +++ b/RenderPipelineFile/rpplugins/color_correction/manual_exposure_stage.py @@ -1,47 +1,47 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import division - -from rpcore.render_stage import RenderStage - - -class ManualExposureStage(RenderStage): - - required_pipes = ["ShadedScene"] - required_inputs = [] - - @property - def produced_pipes(self): - return {"ShadedScene": self.target.color_tex} - - def create(self): - self.target = self.create_target("ManualExposure") - self.target.add_color_attachment(bits=16) - self.target.prepare_buffer() - - def reload_shaders(self): - self.target.shader = self.load_plugin_shader("manual_exposure.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import division + +from rpcore.render_stage import RenderStage + + +class ManualExposureStage(RenderStage): + + required_pipes = ["ShadedScene"] + required_inputs = [] + + @property + def produced_pipes(self): + return {"ShadedScene": self.target.color_tex} + + def create(self): + self.target = self.create_target("ManualExposure") + self.target.add_color_attachment(bits=16) + self.target.prepare_buffer() + + def reload_shaders(self): + self.target.shader = self.load_plugin_shader("manual_exposure.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/color_correction/plugin.py b/RenderPipelineFile/rpplugins/color_correction/plugin.py index 58482a6d..c7b78974 100644 --- a/RenderPipelineFile/rpplugins/color_correction/plugin.py +++ b/RenderPipelineFile/rpplugins/color_correction/plugin.py @@ -1,91 +1,91 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import SamplerState - -# Load the plugin api -from rpcore.pluginbase.base_plugin import BasePlugin -from rpcore.loader import RPLoader - -from .color_correction_stage import ColorCorrectionStage -from .auto_exposure_stage import AutoExposureStage -from .manual_exposure_stage import ManualExposureStage -from .sharpen_stage import SharpenStage -from .tonemapping_stage import TonemappingStage - - -class Plugin(BasePlugin): - - name = "Color Correction" - author = "tobspr " - description = ("This plugin adds support for color correction, vignetting, " - "chromatic abberation and tonemapping. It also controls the " - "camera parameters.") - version = "1.4" - - def on_stage_setup(self): - self.stage = self.create_stage(ColorCorrectionStage) - self.tonemapping_stage = self.create_stage(TonemappingStage) - - if self.get_setting("use_sharpen"): - self.sharpen_stage = self.create_stage(SharpenStage) - self.sharpen_stage.sharpen_twice = self.get_setting("sharpen_twice") - - if not self.get_setting("manual_camera_parameters"): - self.exposure_stage = self.create_stage(AutoExposureStage) - else: - self.exposure_stage = self.create_stage(ManualExposureStage) # noqa # pylint: disable=redefined-variable-type - - def on_pipeline_created(self): - self.load_lut() - self.load_grain() - - def load_lut(self): - """ Loads the color correction lookup table (LUT) """ - lut_path = self.get_resource(self.get_setting("color_lut")) - lut = RPLoader.load_sliced_3d_texture(lut_path, 64) - lut.set_wrap_u(SamplerState.WM_clamp) - lut.set_wrap_v(SamplerState.WM_clamp) - lut.set_wrap_w(SamplerState.WM_clamp) - lut.set_minfilter(SamplerState.FT_linear) - lut.set_magfilter(SamplerState.FT_linear) - lut.set_anisotropic_degree(0) - self.tonemapping_stage.set_shader_input("ColorLUT", lut) - - def load_grain(self): - """ Loads the precomputed film grain """ - grain_tex = RPLoader.load_texture( - "/$$rp/data/film_grain/grain.txo") - grain_tex.set_minfilter(SamplerState.FT_linear) - grain_tex.set_magfilter(SamplerState.FT_linear) - grain_tex.set_wrap_u(SamplerState.WM_repeat) - grain_tex.set_wrap_v(SamplerState.WM_repeat) - grain_tex.set_anisotropic_degree(0) - self.stage.set_shader_input("PrecomputedGrain", grain_tex) - - def update_color_lut(self): - self.debug("Updating color lut ..") - self.load_lut() +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import SamplerState + +# Load the plugin api +from rpcore.pluginbase.base_plugin import BasePlugin +from rpcore.loader import RPLoader + +from .color_correction_stage import ColorCorrectionStage +from .auto_exposure_stage import AutoExposureStage +from .manual_exposure_stage import ManualExposureStage +from .sharpen_stage import SharpenStage +from .tonemapping_stage import TonemappingStage + + +class Plugin(BasePlugin): + + name = "Color Correction" + author = "tobspr " + description = ("This plugin adds support for color correction, vignetting, " + "chromatic abberation and tonemapping. It also controls the " + "camera parameters.") + version = "1.4" + + def on_stage_setup(self): + self.stage = self.create_stage(ColorCorrectionStage) + self.tonemapping_stage = self.create_stage(TonemappingStage) + + if self.get_setting("use_sharpen"): + self.sharpen_stage = self.create_stage(SharpenStage) + self.sharpen_stage.sharpen_twice = self.get_setting("sharpen_twice") + + if not self.get_setting("manual_camera_parameters"): + self.exposure_stage = self.create_stage(AutoExposureStage) + else: + self.exposure_stage = self.create_stage(ManualExposureStage) # noqa # pylint: disable=redefined-variable-type + + def on_pipeline_created(self): + self.load_lut() + self.load_grain() + + def load_lut(self): + """ Loads the color correction lookup table (LUT) """ + lut_path = self.get_resource(self.get_setting("color_lut")) + lut = RPLoader.load_sliced_3d_texture(lut_path, 64) + lut.set_wrap_u(SamplerState.WM_clamp) + lut.set_wrap_v(SamplerState.WM_clamp) + lut.set_wrap_w(SamplerState.WM_clamp) + lut.set_minfilter(SamplerState.FT_linear) + lut.set_magfilter(SamplerState.FT_linear) + lut.set_anisotropic_degree(0) + self.tonemapping_stage.set_shader_input("ColorLUT", lut) + + def load_grain(self): + """ Loads the precomputed film grain """ + grain_tex = RPLoader.load_texture( + "/$$rp/data/film_grain/grain.txo") + grain_tex.set_minfilter(SamplerState.FT_linear) + grain_tex.set_magfilter(SamplerState.FT_linear) + grain_tex.set_wrap_u(SamplerState.WM_repeat) + grain_tex.set_wrap_v(SamplerState.WM_repeat) + grain_tex.set_anisotropic_degree(0) + self.stage.set_shader_input("PrecomputedGrain", grain_tex) + + def update_color_lut(self): + self.debug("Updating color lut ..") + self.load_lut() diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/README.md b/RenderPipelineFile/rpplugins/color_correction/resources/README.md index 89053750..e068837c 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/README.md +++ b/RenderPipelineFile/rpplugins/color_correction/resources/README.md @@ -1,7 +1,7 @@ - - -# Filmic resopnse curves - -The filmic curves were copied from blender and can be found in `datafiles/colormanagement/luts`. -It seems these luts are from http://opencolorio.org. - + + +# Filmic resopnse curves + +The filmic curves were copied from blender and can be found in `datafiles/colormanagement/luts`. +It seems these luts are from http://opencolorio.org. + diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_advantix_100.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_advantix_100.spi1d index 77ae66b3..286f49de 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_advantix_100.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_advantix_100.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0099759204 0.0084230602 0.0063721500 - 0.0195903908 0.0165482908 0.0126159703 - 0.0281873904 0.0239386503 0.0183614101 - 0.0359400399 0.0306491107 0.0236731302 - 0.0429986902 0.0368482284 0.0286820605 - 0.0495699607 0.0426228605 0.0333807915 - 0.0556899793 0.0479997694 0.0378808789 - 0.0614735186 0.0531351306 0.0421144105 - 0.0669660494 0.0580130592 0.0461965613 - 0.0722220466 0.0627681389 0.0501793101 - 0.0772255808 0.0672829002 0.0540068410 - 0.0820826665 0.0715929568 0.0576650202 - 0.0867473707 0.0758115426 0.0612268411 - 0.0911795869 0.0799473077 0.0647046193 - 0.0954854265 0.0839523673 0.0681224987 - 0.0996826664 0.0878431126 0.0714934394 - 0.1037980989 0.0916301981 0.0747776777 - 0.1078191027 0.0953292921 0.0779552832 - 0.1117319018 0.0989396721 0.0810553879 - 0.1155693978 0.1024464965 0.0841333270 - 0.1193092018 0.1058896035 0.0871890634 - 0.1229984984 0.1092770025 0.0902054086 - 0.1266297996 0.1126255989 0.0931699723 - 0.1301828027 0.1159031019 0.0960779637 - 0.1336570978 0.1191100031 0.0989290774 - 0.1370528936 0.1222644001 0.1017358005 - 0.1403681934 0.1253843009 0.1045102030 - 0.1436405927 0.1284482032 0.1072589979 - 0.1468632966 0.1314789057 0.1099923998 - 0.1500498056 0.1344655007 0.1126931980 - 0.1531880051 0.1374220997 0.1153720021 - 0.1562644988 0.1403357983 0.1180130020 - 0.1593036950 0.1431988031 0.1206170022 - 0.1623243988 0.1460403949 0.1231862009 - 0.1653179973 0.1488437951 0.1257254034 - 0.1682834029 0.1516059935 0.1282379031 - 0.1712214053 0.1543526053 0.1307227015 - 0.1741254926 0.1570729017 0.1331889033 - 0.1769879013 0.1597575992 0.1356364042 - 0.1798096001 0.1624283046 0.1380718946 - 0.1825927943 0.1650680006 0.1404998004 - 0.1853370070 0.1676719934 0.1429184973 - 0.1880521029 0.1702331007 0.1453085989 - 0.1907549053 0.1727772951 0.1476781964 - 0.1934437007 0.1753048003 0.1500300020 - 0.1961088926 0.1777949929 0.1523580998 - 0.1987465024 0.1802501976 0.1546709985 - 0.2013590932 0.1826943010 0.1569592059 - 0.2039510012 0.1851214021 0.1592251062 - 0.2065189034 0.1875294000 0.1614681929 - 0.2090530992 0.1899122000 0.1636908054 - 0.2115564942 0.1922917962 0.1659017056 - 0.2140268981 0.1946634054 0.1680952013 - 0.2164652050 0.1970131993 0.1702757031 - 0.2188750952 0.1993432045 0.1724483967 - 0.2212729007 0.2016437054 0.1746093035 - 0.2236527056 0.2039304972 0.1767639965 - 0.2260089964 0.2062091976 0.1789053977 - 0.2283485979 0.2084756047 0.1810386926 - 0.2306748033 0.2107181996 0.1831596941 - 0.2329774946 0.2129396051 0.1852661967 - 0.2352634072 0.2151475996 0.1873624027 - 0.2375380993 0.2173521072 0.1894447953 - 0.2398073971 0.2195435017 0.1915135980 - 0.2420687973 0.2217237949 0.1935653985 - 0.2443214953 0.2238844931 0.1956079006 - 0.2465686053 0.2260320932 0.1976383030 - 0.2488050014 0.2281741947 0.1996645033 - 0.2510258853 0.2303086966 0.2016756982 - 0.2532328963 0.2324285060 0.2036695927 - 0.2554255128 0.2345477939 0.2056519985 - 0.2576034069 0.2366596013 0.2076213062 - 0.2597728968 0.2387540042 0.2095845044 - 0.2619242072 0.2408431023 0.2115366012 - 0.2640654147 0.2429351956 0.2134785950 - 0.2661896050 0.2450205982 0.2154116929 - 0.2682917118 0.2470878959 0.2173403054 - 0.2703784108 0.2491298020 0.2192642987 - 0.2724463940 0.2511504889 0.2211790979 - 0.2745001018 0.2531620860 0.2230885029 - 0.2765473127 0.2551639080 0.2249957025 - 0.2785851955 0.2571550012 0.2268918008 - 0.2806180120 0.2591302097 0.2287832946 - 0.2826406956 0.2611007094 0.2306707054 - 0.2846564949 0.2630642056 0.2325448990 - 0.2866632938 0.2650120854 0.2344118059 - 0.2886619866 0.2669439912 0.2362738997 - 0.2906560004 0.2688710988 0.2381276935 - 0.2926402986 0.2707965970 0.2399802953 - 0.2946187854 0.2727138996 0.2418286055 - 0.2965883017 0.2746171057 0.2436629981 - 0.2985542119 0.2765100896 0.2454881072 - 0.3005166948 0.2783927023 0.2473084927 - 0.3024731874 0.2802628875 0.2491195947 - 0.3044191897 0.2821266055 0.2509351969 - 0.3063541949 0.2839806080 0.2527473867 - 0.3082849979 0.2858333886 0.2545492053 - 0.3102098107 0.2876932919 0.2563391030 - 0.3121250868 0.2895553112 0.2581259906 - 0.3140304089 0.2914139032 0.2599045038 - 0.3159317076 0.2932642996 0.2616701126 - 0.3178148866 0.2951014042 0.2634266019 - 0.3196887970 0.2969211042 0.2651784122 - 0.3215618134 0.2987315953 0.2669197023 - 0.3234333992 0.3005411923 0.2686525881 - 0.3252997994 0.3023456931 0.2703748941 - 0.3271591067 0.3041403890 0.2720899880 - 0.3290081024 0.3059214950 0.2738001049 - 0.3308455050 0.3076925874 0.2755038142 - 0.3326750100 0.3094542027 0.2772029042 - 0.3345032036 0.3112052083 0.2788943052 - 0.3363313079 0.3129465878 0.2805776000 - 0.3381510079 0.3146811128 0.2822565138 - 0.3399673104 0.3164061010 0.2839331031 - 0.3417668045 0.3181275129 0.2856017947 - 0.3435564041 0.3198527992 0.2872670889 - 0.3453372121 0.3215794861 0.2889324129 - 0.3471119106 0.3233039975 0.2905947864 - 0.3488827050 0.3250232041 0.2922506928 - 0.3506490886 0.3267351091 0.2938998938 - 0.3524067998 0.3284330070 0.2955498099 - 0.3541550934 0.3301151097 0.2971960902 - 0.3558937907 0.3317880034 0.2988429070 - 0.3576247096 0.3334558010 0.3004919887 - 0.3593502939 0.3351208866 0.3021433949 - 0.3610683084 0.3367798030 0.3037939072 - 0.3627772033 0.3384372890 0.3054369092 - 0.3644764125 0.3400928974 0.3070757985 - 0.3661648929 0.3417460024 0.3087131977 - 0.3678394854 0.3433952034 0.3103514910 - 0.3695080876 0.3450438976 0.3119843006 - 0.3711709082 0.3466848135 0.3136082888 - 0.3728272021 0.3483184874 0.3152261972 - 0.3744739890 0.3499465883 0.3168402910 - 0.3761177063 0.3515641093 0.3184512854 - 0.3777532876 0.3531782925 0.3200559020 - 0.3793804944 0.3547908962 0.3216531873 - 0.3810012937 0.3564052880 0.3232400119 - 0.3826153874 0.3580116034 0.3248240948 - 0.3842233121 0.3596132994 0.3264040053 - 0.3858231008 0.3612110019 0.3279787004 - 0.3874185085 0.3627916873 0.3295400143 - 0.3890103102 0.3643645048 0.3310950100 - 0.3905987144 0.3659299016 0.3326450884 - 0.3921895027 0.3674801886 0.3341901898 - 0.3937751055 0.3690252006 0.3357275128 - 0.3953565061 0.3705663085 0.3372594118 - 0.3969357014 0.3721058071 0.3387841880 - 0.3985069990 0.3736424148 0.3403007984 - 0.4000718892 0.3751770854 0.3418123126 - 0.4016300142 0.3767110109 0.3433167934 - 0.4031783938 0.3782438040 0.3448176086 - 0.4047181010 0.3797756135 0.3463155031 - 0.4062578976 0.3813019097 0.3478057981 - 0.4077931046 0.3828235865 0.3492931128 - 0.4093224108 0.3843406141 0.3507778943 - 0.4108479917 0.3858481050 0.3522605002 - 0.4123688936 0.3873529136 0.3537409008 - 0.4138790071 0.3888536990 0.3552181125 - 0.4153836071 0.3903481066 0.3566935062 - 0.4168849885 0.3918395936 0.3581683934 - 0.4183782041 0.3933278024 0.3596439064 - 0.4198674858 0.3948136866 0.3611176014 - 0.4213511050 0.3962967098 0.3625896871 - 0.4228254855 0.3977732062 0.3640539050 - 0.4242956936 0.3992463946 0.3655144870 - 0.4257540107 0.4007085860 0.3669649065 - 0.4272057116 0.4021649063 0.3684150875 - 0.4286471903 0.4036136866 0.3698649108 - 0.4300779998 0.4050632119 0.3713107109 - 0.4315038025 0.4065128863 0.3727546930 - 0.4329301119 0.4079527855 0.3741905987 - 0.4343521893 0.4093911052 0.3756245971 - 0.4357595146 0.4108251035 0.3770546913 - 0.4371610880 0.4122549891 0.3784829080 - 0.4385559857 0.4136810899 0.3799099028 - 0.4399372935 0.4150992930 0.3813312054 - 0.4413124025 0.4165146947 0.3827508986 - 0.4426822960 0.4179250002 0.3841583133 - 0.4440504909 0.4193333089 0.3855640888 - 0.4454061091 0.4207299948 0.3869630098 - 0.4467600882 0.4221264124 0.3883576989 - 0.4481090903 0.4235222936 0.3897452950 - 0.4494557977 0.4249134958 0.3911296129 - 0.4507992864 0.4262988865 0.3925110102 - 0.4521391988 0.4276803136 0.3938843906 - 0.4534761906 0.4290586114 0.3952535987 - 0.4548082054 0.4304304123 0.3966163993 - 0.4561375976 0.4317994118 0.3979771137 - 0.4574612081 0.4331589937 0.3993296921 - 0.4587827027 0.4345161915 0.4006809890 - 0.4600965977 0.4358676076 0.4020248055 - 0.4614087045 0.4372180998 0.4033680856 - 0.4627130032 0.4385639131 0.4046998918 - 0.4640161991 0.4399093091 0.4060314000 - 0.4653095007 0.4412535131 0.4073561132 - 0.4666016996 0.4425976872 0.4086807072 - 0.4678846896 0.4439316094 0.4100016057 - 0.4691669047 0.4452649951 0.4113222957 - 0.4704379141 0.4465923905 0.4126363993 - 0.4717079103 0.4479196072 0.4139505923 - 0.4729768038 0.4492313862 0.4152606130 - 0.4742454886 0.4505420923 0.4165706038 - 0.4755127132 0.4518438876 0.4178735912 - 0.4767796993 0.4531444907 0.4191763997 - 0.4780431092 0.4544411004 0.4204703867 - 0.4793055058 0.4557369947 0.4217635989 - 0.4805589020 0.4570290148 0.4230574071 - 0.4818083942 0.4583194852 0.4243515134 - 0.4830555916 0.4596050978 0.4256447852 - 0.4843012094 0.4608877003 0.4269377887 - 0.4855448008 0.4621653855 0.4282276034 - 0.4867860973 0.4634382129 0.4295150936 - 0.4880253077 0.4647068083 0.4307982028 - 0.4892593920 0.4659673870 0.4320754111 - 0.4904916883 0.4672257006 0.4333496094 - 0.4917111993 0.4684731960 0.4346148968 - 0.4929307103 0.4697206914 0.4358800054 - 0.4941438138 0.4709604084 0.4371437132 - 0.4953565001 0.4722000062 0.4384073019 - 0.4965640903 0.4734326899 0.4396674931 - 0.4977697134 0.4746631980 0.4409270883 - 0.4989753962 0.4758923054 0.4421825111 - 0.5001811981 0.4771201015 0.4434351921 - 0.5013839006 0.4783450067 0.4446885884 - 0.5025752783 0.4795615971 0.4459435046 - 0.5037667155 0.4807780981 0.4471982121 - 0.5049498081 0.4819892943 0.4484477043 - 0.5061323047 0.4832003117 0.4496971965 - 0.5073093176 0.4844068885 0.4509449005 - 0.5084829926 0.4856112003 0.4521920085 - 0.5096563101 0.4868122041 0.4534366131 - 0.5108284950 0.4880064130 0.4546774030 - 0.5120006800 0.4892007113 0.4559176862 - 0.5131582022 0.4903838933 0.4571487904 - 0.5143144727 0.4915668964 0.4583798945 - 0.5154706240 0.4927465916 0.4596010149 - 0.5166264176 0.4939242899 0.4608170986 - 0.5177810192 0.4951008856 0.4620308876 - 0.5189288259 0.4962731004 0.4632383883 - 0.5200765729 0.4974452853 0.4644457996 - 0.5212227106 0.4986133873 0.4656499028 - 0.5223680139 0.4997802079 0.4668532908 - 0.5235089064 0.5009468198 0.4680534899 - 0.5246378779 0.5021128058 0.4692485034 - 0.5257669091 0.5032787919 0.4704434872 - 0.5268948078 0.5044375062 0.4716280997 - 0.5280222893 0.5055947900 0.4728116095 - 0.5291473866 0.5067505240 0.4739918113 - 0.5302665830 0.5079032779 0.4751670957 - 0.5313857794 0.5090559721 0.4763424098 - 0.5325024724 0.5101991296 0.4775088131 - 0.5336183906 0.5113397837 0.4786739051 - 0.5347307920 0.5124807954 0.4798345864 - 0.5358300209 0.5136229992 0.4809859991 - 0.5369291902 0.5147653222 0.4821374118 - 0.5380263925 0.5159034729 0.4832834005 - 0.5391222835 0.5170397162 0.4844275117 - 0.5402178764 0.5181757212 0.4855712950 - 0.5412973166 0.5193089843 0.4867131114 - 0.5423766971 0.5204423070 0.4878548086 - 0.5434548855 0.5215684772 0.4889939129 - 0.5445311069 0.5226852298 0.4901303053 - 0.5456073284 0.5238018036 0.4912666976 - 0.5466775894 0.5249150991 0.4923954010 - 0.5477457047 0.5260273814 0.4935226142 - 0.5488138199 0.5271393061 0.4946480095 - 0.5498805046 0.5282471776 0.4957635105 - 0.5509471893 0.5293549895 0.4968788922 - 0.5520086884 0.5304607153 0.4979912043 - 0.5530598164 0.5315629244 0.4990994930 - 0.5541110039 0.5326650143 0.5002077222 - 0.5551608205 0.5337625146 0.5013095140 - 0.5562096238 0.5348572731 0.5024083853 - 0.5572584271 0.5359520912 0.5035073161 - 0.5583010912 0.5370396972 0.5045971274 - 0.5593425035 0.5381262898 0.5056862831 - 0.5603839159 0.5392122269 0.5067744851 - 0.5614247918 0.5402883887 0.5078564286 - 0.5624656081 0.5413644910 0.5089381933 - 0.5635035038 0.5424379706 0.5100173950 - 0.5645298958 0.5435034037 0.5110903978 - 0.5655562878 0.5445687771 0.5121634007 - 0.5665822029 0.5456321836 0.5132340789 - 0.5676072836 0.5466927290 0.5143020749 - 0.5686323047 0.5477532148 0.5153701901 - 0.5696542859 0.5488079190 0.5164343715 - 0.5706726909 0.5498570204 0.5174958110 - 0.5716910958 0.5509061813 0.5185571909 - 0.5727097988 0.5519514084 0.5196129084 - 0.5737286210 0.5529940128 0.5206657052 - 0.5747473836 0.5540364981 0.5217186213 - 0.5757570863 0.5550705791 0.5227680802 - 0.5767611861 0.5561003089 0.5238164067 - 0.5777652264 0.5571299195 0.5248646736 - 0.5787636042 0.5581539273 0.5259051919 - 0.5797588229 0.5591753721 0.5269432068 - 0.5807541013 0.5601968765 0.5279811025 - 0.5817455053 0.5612133145 0.5290176272 - 0.5827348232 0.5622273088 0.5300536156 - 0.5837240219 0.5632413030 0.5310897231 - 0.5847098231 0.5642498732 0.5321198106 - 0.5856931806 0.5652555823 0.5331475139 - 0.5866767168 0.5662612915 0.5341752768 - 0.5876535177 0.5672615767 0.5351998806 - 0.5886247158 0.5682581067 0.5362228155 - 0.5895959735 0.5692545772 0.5372456908 - 0.5905644894 0.5702462792 0.5382614732 - 0.5915299058 0.5712330937 0.5392717719 - 0.5924953222 0.5722199082 0.5402820110 - 0.5934591889 0.5732024908 0.5412889123 - 0.5944200754 0.5741782784 0.5422912836 - 0.5953810215 0.5751540065 0.5432937741 - 0.5963401794 0.5761274099 0.5442951918 - 0.5972915292 0.5770921111 0.5452939868 - 0.5982427001 0.5780568123 0.5462927222 - 0.5991939902 0.5790212750 0.5472911000 - 0.6001344919 0.5799819231 0.5482859015 - 0.6010748744 0.5809425116 0.5492805839 - 0.6020153761 0.5819031000 0.5502753258 - 0.6029484272 0.5828554034 0.5512647033 - 0.6038792729 0.5838056803 0.5522533059 - 0.6048101783 0.5847560167 0.5532417893 - 0.6057401896 0.5857036710 0.5542263985 - 0.6066694856 0.5866494179 0.5552083850 - 0.6075987220 0.5875951052 0.5561903119 - 0.6085264087 0.5885378718 0.5571714044 - 0.6094487906 0.5894731283 0.5581504703 - 0.6103712916 0.5904083252 0.5591297150 - 0.6112936735 0.5913434029 0.5601087213 - 0.6122087836 0.5922747254 0.5610769987 - 0.6131231785 0.5932055712 0.5620452762 - 0.6140375733 0.5941365957 0.5630136132 - 0.6149482131 0.5950686932 0.5639781952 - 0.6158558130 0.5960016251 0.5649409294 - 0.6167634130 0.5969344974 0.5659034848 - 0.6176698804 0.5978664160 0.5668659210 - 0.6185709834 0.5987945795 0.5678272843 - 0.6194720864 0.5997229218 0.5687887073 - 0.6203731894 0.6006512046 0.5697500706 - 0.6212695241 0.6015707254 0.5707088709 - 0.6221640706 0.6024879217 0.5716670752 - 0.6230587959 0.6034051180 0.5726253986 - 0.6239500046 0.6043199897 0.5735797882 - 0.6248329878 0.6052303910 0.5745280981 - 0.6257159710 0.6061406732 0.5754765272 - 0.6265990138 0.6070510149 0.5764248967 - 0.6274775267 0.6079521179 0.5773686171 - 0.6283550262 0.6088513732 0.5783116817 - 0.6292325258 0.6097506881 0.5792549253 - 0.6301056147 0.6106485724 0.5801956058 - 0.6309671998 0.6115432978 0.5811319947 - 0.6318287253 0.6124380231 0.5820685029 - 0.6326903105 0.6133326292 0.5830048919 - 0.6335496902 0.6142238975 0.5839343071 - 0.6344081163 0.6151139736 0.5848619938 - 0.6352664828 0.6160039902 0.5857896805 - 0.6361234188 0.6168932915 0.5867152214 - 0.6369684935 0.6177780032 0.5876327753 - 0.6378136277 0.6186627150 0.5885503292 - 0.6386587024 0.6195474267 0.5894678831 - 0.6395034194 0.6204267144 0.5903825164 - 0.6403477788 0.6213008165 0.5912950039 - 0.6411921978 0.6221749187 0.5922073722 - 0.6420364976 0.6230490208 0.5931199193 - 0.6428722143 0.6239190102 0.5940275788 - 0.6437057257 0.6247882843 0.5949348211 - 0.6445392966 0.6256574988 0.5958420038 - 0.6453729868 0.6265254021 0.5967481732 - 0.6462082863 0.6273844838 0.5976504087 - 0.6470435858 0.6282436848 0.5985525846 - 0.6478788853 0.6291028261 0.5994548202 - 0.6487107873 0.6299602985 0.6003562212 - 0.6495367289 0.6308152080 0.6012567878 - 0.6503624916 0.6316699982 0.6021572948 - 0.6511883736 0.6325249076 0.6030578017 - 0.6520127058 0.6333740950 0.6039531827 - 0.6528357863 0.6342192888 0.6048460007 - 0.6536588073 0.6350646019 0.6057388186 - 0.6544818878 0.6359099150 0.6066315174 - 0.6552997231 0.6367524266 0.6075202823 - 0.6561157107 0.6375942230 0.6084082127 - 0.6569316983 0.6384360194 0.6092960835 - 0.6577478051 0.6392776966 0.6101840734 - 0.6585525870 0.6401113272 0.6110618114 - 0.6593559980 0.6409441233 0.6119391918 - 0.6601594090 0.6417768002 0.6128165722 - 0.6609628201 0.6426095963 0.6136940122 - 0.6617612243 0.6434395909 0.6145706177 - 0.6625593901 0.6442695856 0.6154471040 - 0.6633576155 0.6450996995 0.6163237095 - 0.6641554832 0.6459290981 0.6171987057 - 0.6649441123 0.6467508078 0.6180636287 - 0.6657326818 0.6475725174 0.6189286113 - 0.6665213108 0.6483941078 0.6197934747 - 0.6673098207 0.6492155790 0.6206579804 - 0.6680960059 0.6500353217 0.6215201020 - 0.6688820720 0.6508548856 0.6223822236 - 0.6696683168 0.6516745090 0.6232442856 - 0.6704540849 0.6524934769 0.6241046190 - 0.6712335944 0.6533043981 0.6249533296 - 0.6720131040 0.6541153789 0.6258019209 - 0.6727924943 0.6549263000 0.6266505718 - 0.6735720038 0.6557372212 0.6274989247 - 0.6743516922 0.6565400958 0.6283429265 - 0.6751313806 0.6573429704 0.6291869879 - 0.6759110093 0.6581459045 0.6300309896 - 0.6766906977 0.6589487791 0.6308749914 - 0.6774597764 0.6597450972 0.6317095160 - 0.6782273054 0.6605407000 0.6325436831 - 0.6789947748 0.6613361835 0.6333777905 - 0.6797623038 0.6621317863 0.6342120171 - 0.6805222034 0.6629223824 0.6350417733 - 0.6812793016 0.6637114286 0.6358706951 - 0.6820363998 0.6645004153 0.6366994977 - 0.6827934980 0.6652892828 0.6375284195 - 0.6835458279 0.6660748124 0.6383498907 - 0.6842942834 0.6668576002 0.6391670704 - 0.6850426793 0.6676403880 0.6399844289 - 0.6857910752 0.6684231758 0.6408016086 - 0.6865373254 0.6692041755 0.6416165233 - 0.6872786283 0.6699815989 0.6424279809 - 0.6880199909 0.6707590222 0.6432394981 - 0.6887614131 0.6715363860 0.6440510154 - 0.6895024776 0.6723132730 0.6448624134 - 0.6902387142 0.6730847955 0.6456729770 - 0.6909748912 0.6738561988 0.6464836001 - 0.6917111278 0.6746276021 0.6472942829 - 0.6924473047 0.6753990054 0.6481049061 - 0.6931800246 0.6761628985 0.6489096880 - 0.6939114928 0.6769244075 0.6497132778 - 0.6946430206 0.6776859164 0.6505168080 - 0.6953744888 0.6784474254 0.6513202786 - 0.6961029768 0.6792098284 0.6521201730 - 0.6968262792 0.6799734235 0.6529155970 - 0.6975495815 0.6807370782 0.6537110806 - 0.6982728839 0.6815006733 0.6545065045 - 0.6989961863 0.6822643876 0.6553019285 - 0.6997179985 0.6830213070 0.6560959816 - 0.7004398108 0.6837782264 0.6568900943 - 0.7011616230 0.6845350862 0.6576842070 - 0.7018833756 0.6852920055 0.6584783792 - 0.7026001811 0.6860477924 0.6592714190 - 0.7033123970 0.6868026257 0.6600636244 - 0.7040244937 0.6875573993 0.6608558893 - 0.7047367096 0.6883121729 0.6616480947 - 0.7054488063 0.6890667081 0.6624394059 - 0.7061572075 0.6898154020 0.6632227898 - 0.7068657279 0.6905639768 0.6640061736 - 0.7075741887 0.6913126707 0.6647896171 - 0.7082825899 0.6920614243 0.6655731201 - 0.7089877129 0.6928064823 0.6663510203 - 0.7096893191 0.6935483217 0.6671249866 - 0.7103909254 0.6942901015 0.6678988934 - 0.7110924125 0.6950318813 0.6686729193 - 0.7117940187 0.6957737207 0.6694468260 - 0.7124900222 0.6965091825 0.6702172756 - 0.7131854892 0.6972442269 0.6709877253 - 0.7138808966 0.6979792118 0.6717581749 - 0.7145763040 0.6987141967 0.6725286841 - 0.7152705789 0.6994475722 0.6732978225 - 0.7159615159 0.7001774907 0.6740645170 - 0.7166523933 0.7009072900 0.6748312116 - 0.7173433900 0.7016372085 0.6755979061 - 0.7180343270 0.7023670077 0.6763646007 - 0.7187235951 0.7030938864 0.6771286130 - 0.7194116116 0.7038189769 0.6778911948 - 0.7200996876 0.7045441866 0.6786538959 - 0.7207878232 0.7052692771 0.6794165969 - 0.7214757800 0.7059944272 0.6801792979 - 0.7221612930 0.7067067027 0.6809418201 - 0.7228463292 0.7074177265 0.6817042828 - 0.7235314250 0.7081285715 0.6824668050 - 0.7242164016 0.7088394761 0.6832293868 - 0.7249004245 0.7095499039 0.6839907765 - 0.7255744934 0.7102571726 0.6847469211 - 0.7262486219 0.7109643817 0.6855030060 - 0.7269226909 0.7116715908 0.6862590909 - 0.7275968194 0.7123787999 0.6870152950 - 0.7282711864 0.7130823135 0.6877691150 - 0.7289463878 0.7137774229 0.6885187030 - 0.7296215892 0.7144724131 0.6892682910 - 0.7302967906 0.7151675224 0.6900178790 - 0.7309719920 0.7158625722 0.6907675266 - 0.7316445112 0.7165569067 0.6915168762 - 0.7323129177 0.7172502875 0.6922662854 - 0.7329813838 0.7179437280 0.6930155754 - 0.7336499095 0.7186371088 0.6937649846 - 0.7343183160 0.7193304896 0.6945142746 - 0.7349863052 0.7200188041 0.6952630281 - 0.7356538177 0.7207021713 0.6960111856 - 0.7363212705 0.7213857174 0.6967594028 - 0.7369887829 0.7220690846 0.6975076199 - 0.7376562953 0.7227525711 0.6982558966 - 0.7383213043 0.7234362960 0.6990029812 - 0.7389839888 0.7241200805 0.6997492909 - 0.7396466136 0.7248039842 0.7004957199 - 0.7403091788 0.7254878879 0.7012420297 - 0.7409719229 0.7261717916 0.7019883990 - 0.7416300774 0.7268511057 0.7027307153 - 0.7422838807 0.7275264263 0.7034698725 - 0.7429378033 0.7282016873 0.7042092085 - 0.7435916066 0.7288768888 0.7049484849 - 0.7442454100 0.7295522094 0.7056878209 - 0.7448980212 0.7302259803 0.7064262033 - 0.7455489039 0.7308980823 0.7071638703 - 0.7461997867 0.7315703034 0.7079014778 - 0.7468507886 0.7322424054 0.7086392045 - 0.7475016713 0.7329145074 0.7093768120 - 0.7481493950 0.7335844040 0.7101135850 - 0.7487897873 0.7342495918 0.7108489275 - 0.7494302988 0.7349148989 0.7115842104 - 0.7500706911 0.7355802059 0.7123194933 - 0.7507110834 0.7362453938 0.7130547762 - 0.7513517141 0.7369098067 0.7137894034 - 0.7519931793 0.7375693917 0.7145212889 - 0.7526347041 0.7382289171 0.7152531743 - 0.7532762289 0.7388885021 0.7159851789 - 0.7539176941 0.7395480871 0.7167171240 - 0.7545592189 0.7402076125 0.7174490094 - 0.7551949024 0.7408636212 0.7181727290 - 0.7558298707 0.7415192723 0.7188962102 - 0.7564650178 0.7421751022 0.7196195722 - 0.7570999861 0.7428308129 0.7203431129 - 0.7577350736 0.7434865236 0.7210665941 - 0.7583732009 0.7441366911 0.7217879295 - 0.7590131760 0.7447838783 0.7225083709 - 0.7596532106 0.7454311252 0.7232288122 - 0.7602931261 0.7460783124 0.7239493132 - 0.7609331012 0.7467254996 0.7246696949 - 0.7615718842 0.7473719120 0.7253888845 - 0.7622076869 0.7480165958 0.7261058092 - 0.7628436089 0.7486613989 0.7268226743 - 0.7634794116 0.7493060827 0.7275395989 - 0.7641152740 0.7499508858 0.7282565236 - 0.7647510767 0.7505956888 0.7289733887 - 0.7653750777 0.7512348294 0.7296857238 - 0.7659978271 0.7518734932 0.7303978801 - 0.7666205168 0.7525122166 0.7311100960 - 0.7672433257 0.7531508803 0.7318223119 - 0.7678660154 0.7537896037 0.7325345278 - 0.7684878707 0.7544282079 0.7332416177 - 0.7691087723 0.7550666928 0.7339441776 - 0.7697296739 0.7557051778 0.7346469164 - 0.7703505754 0.7563437223 0.7353495955 - 0.7709714770 0.7569822073 0.7360522151 - 0.7715923786 0.7576206923 0.7367547154 - 0.7722045183 0.7582557797 0.7374516726 - 0.7728161812 0.7588909268 0.7381488085 - 0.7734280229 0.7595258951 0.7388458848 - 0.7740396857 0.7601609230 0.7395429015 - 0.7746515274 0.7607960105 0.7402399778 - 0.7752640843 0.7614319921 0.7409328818 - 0.7758777738 0.7620692253 0.7416213751 - 0.7764915824 0.7627063990 0.7423098087 - 0.7771053910 0.7633436918 0.7429983020 - 0.7777191997 0.7639809251 0.7436866760 - 0.7783328891 0.7646180987 0.7443752289 - 0.7789419293 0.7652487159 0.7450602055 - 0.7795497179 0.7658780217 0.7457447052 - 0.7801573873 0.7665072083 0.7464292049 - 0.7807651758 0.7671365142 0.7471137047 - 0.7813730240 0.7677657008 0.7477982044 - 0.7819799781 0.7683938742 0.7484809160 - 0.7825825810 0.7690163851 0.7491562963 - 0.7831851840 0.7696390152 0.7498317957 - 0.7837877870 0.7702615261 0.7505072951 - 0.7843903899 0.7708839774 0.7511827946 - 0.7849929929 0.7715066075 0.7518582940 - 0.7855939269 0.7721267939 0.7525311708 - 0.7861924767 0.7727441788 0.7532013059 - 0.7867910862 0.7733615041 0.7538713813 - 0.7873896956 0.7739788890 0.7545415163 - 0.7879884243 0.7745962143 0.7552115917 - 0.7885869741 0.7752135992 0.7558817267 - 0.7891780734 0.7758283019 0.7565454841 - 0.7897650003 0.7764415741 0.7572066784 - 0.7903518081 0.7770550251 0.7578678131 - 0.7909386158 0.7776682973 0.7585290074 - 0.7915254831 0.7782816291 0.7591902018 - 0.7921122909 0.7788950205 0.7598512769 - 0.7926985025 0.7795063257 0.7605091929 - 0.7932845950 0.7801172137 0.7611665130 - 0.7938706875 0.7807281017 0.7618237734 - 0.7944567800 0.7813388705 0.7624810934 - 0.7950428724 0.7819498181 0.7631384134 - 0.7956290245 0.7825607061 0.7637956738 - 0.7962080836 0.7831704021 0.7644450068 - 0.7967863083 0.7837799191 0.7650939226 - 0.7973645926 0.7843893766 0.7657428980 - 0.7979428768 0.7849990129 0.7663918138 - 0.7985212207 0.7856084704 0.7670407295 - 0.7990993857 0.7862181067 0.7676897049 - 0.7996805906 0.7868278027 0.7683331966 - 0.8002619147 0.7874376178 0.7689768076 - 0.8008431792 0.7880473733 0.7696204185 - 0.8014245033 0.7886571884 0.7702640295 - 0.8020058274 0.7892670035 0.7709075212 - 0.8025872111 0.7898766994 0.7715510726 - 0.8031632900 0.7904822826 0.7721858025 - 0.8037390113 0.7910876274 0.7728202939 - 0.8043146133 0.7916929126 0.7734547853 - 0.8048902750 0.7922981977 0.7740892768 - 0.8054659963 0.7929034829 0.7747238278 - 0.8060415983 0.7935088277 0.7753583193 - 0.8066141009 0.7941111922 0.7759894729 - 0.8071860075 0.7947131991 0.7766203284 - 0.8077577949 0.7953152061 0.7772511244 - 0.8083297014 0.7959172130 0.7778819203 - 0.8089016080 0.7965192199 0.7785127163 - 0.8094733953 0.7971212268 0.7791435122 - 0.8100445867 0.7977176905 0.7797755003 - 0.8106154203 0.7983120084 0.7804080248 - 0.8111861944 0.7989063859 0.7810403705 - 0.8117570281 0.7995008230 0.7816727757 - 0.8123278022 0.8000950813 0.7823051810 - 0.8128986955 0.8006895185 0.7829377055 - 0.8134648800 0.8012806177 0.7835683227 - 0.8140262961 0.8018686175 0.7841975093 - 0.8145877123 0.8024566174 0.7848265767 - 0.8151491284 0.8030446172 0.7854558229 - 0.8157104254 0.8036326170 0.7860850096 - 0.8162717819 0.8042206168 0.7867141962 - 0.8168334961 0.8048074245 0.7873411179 - 0.8173961043 0.8053904772 0.7879621983 - 0.8179587126 0.8059734702 0.7885832787 - 0.8185213208 0.8065565825 0.7892044187 - 0.8190839291 0.8071395755 0.7898254991 - 0.8196464181 0.8077226877 0.7904465795 - 0.8202090263 0.8083056808 0.7910677195 - 0.8207656741 0.8088853955 0.7916854024 - 0.8213217258 0.8094648123 0.7923029065 - 0.8218777776 0.8100442290 0.7929204702 - 0.8224338293 0.8106235862 0.7935379744 - 0.8229898214 0.8112030029 0.7941555977 - 0.8235458136 0.8117823005 0.7947731018 - 0.8241016269 0.8123605251 0.7953892946 - 0.8246573806 0.8129374981 0.7960044146 - 0.8252130747 0.8135144711 0.7966195941 - 0.8257688284 0.8140915036 0.7972347140 - 0.8263245225 0.8146684766 0.7978497744 - 0.8268802166 0.8152455091 0.7984648943 - 0.8274357915 0.8158224225 0.7990797758 - 0.8279883265 0.8163983822 0.7996929288 - 0.8285408020 0.8169744015 0.8003060222 - 0.8290932178 0.8175503016 0.8009191155 - 0.8296456933 0.8181263208 0.8015322089 - 0.8301981091 0.8187022805 0.8021453023 - 0.8307505846 0.8192781806 0.8027585149 - 0.8312999010 0.8198543191 0.8033677936 - 0.8318461776 0.8204303980 0.8039743900 - 0.8323925138 0.8210065961 0.8045809269 - 0.8329387903 0.8215826750 0.8051874042 - 0.8334850073 0.8221588731 0.8057940006 - 0.8340312839 0.8227350116 0.8064004779 - 0.8345776200 0.8233110905 0.8070070148 - 0.8351238966 0.8238831758 0.8076115251 - 0.8356701732 0.8244549036 0.8082157969 - 0.8362163901 0.8250266910 0.8088201880 - 0.8367627263 0.8255984783 0.8094245195 - 0.8373090029 0.8261703253 0.8100289106 - 0.8378552794 0.8267419934 0.8106333017 - 0.8383994102 0.8273130059 0.8112372160 - 0.8389375210 0.8278818130 0.8118401766 - 0.8394755125 0.8284506202 0.8124431968 - 0.8400135040 0.8290194273 0.8130462170 - 0.8405516148 0.8295882940 0.8136492968 - 0.8410896063 0.8301571012 0.8142523170 - 0.8416277170 0.8307259083 0.8148552775 - 0.8421646953 0.8312906027 0.8154578805 - 0.8427007198 0.8318520188 0.8160601258 - 0.8432368040 0.8324133754 0.8166623712 - 0.8437728882 0.8329747915 0.8172646761 - 0.8443089128 0.8335362077 0.8178669214 - 0.8448449969 0.8340976238 0.8184692264 - 0.8453810811 0.8346589208 0.8190714717 - 0.8459135294 0.8352158070 0.8196697235 - 0.8464447260 0.8357713223 0.8202667832 - 0.8469758034 0.8363267183 0.8208639026 - 0.8475070000 0.8368821740 0.8214610815 - 0.8480380774 0.8374376297 0.8220582008 - 0.8485692739 0.8379930854 0.8226553202 - 0.8491004705 0.8385484815 0.8232524991 - 0.8496297002 0.8390995860 0.8238487840 - 0.8501586914 0.8396500945 0.8244450092 - 0.8506876230 0.8402007222 0.8250411749 - 0.8512166142 0.8407511711 0.8256375194 - 0.8517454863 0.8413017988 0.8262336850 - 0.8522744775 0.8418524265 0.8268299103 - 0.8528034091 0.8424028754 0.8274261951 - 0.8533318043 0.8429502249 0.8280221224 - 0.8538600802 0.8434972763 0.8286179900 - 0.8543884158 0.8440443873 0.8292139173 - 0.8549165726 0.8445914984 0.8298097849 - 0.8554449081 0.8451387286 0.8304055929 - 0.8559731841 0.8456857800 0.8310015202 - 0.8565015197 0.8462328911 0.8315973878 - 0.8570210934 0.8467777967 0.8321936727 - 0.8575400710 0.8473225236 0.8327900171 - 0.8580591083 0.8478673100 0.8333863020 - 0.8585780859 0.8484120965 0.8339825869 - 0.8590971231 0.8489568233 0.8345788717 - 0.8596161008 0.8495016098 0.8351752162 - 0.8601350784 0.8500462770 0.8357715011 - 0.8606511950 0.8505902290 0.8363643289 - 0.8611670136 0.8511340022 0.8369567990 - 0.8616827726 0.8516777754 0.8375492096 - 0.8621984720 0.8522216082 0.8381416798 - 0.8627142906 0.8527653813 0.8387340903 - 0.8632301092 0.8533092141 0.8393266201 - 0.8637458086 0.8538529277 0.8399189711 - 0.8642572165 0.8543968201 0.8405116796 - 0.8647668958 0.8549407721 0.8411043286 - 0.8652765751 0.8554847240 0.8416969776 - 0.8657861948 0.8560286164 0.8422896862 - 0.8662958741 0.8565725088 0.8428823948 - 0.8668056130 0.8571164012 0.8434751034 - 0.8673152924 0.8576604128 0.8440678120 - 0.8678231835 0.8582047224 0.8446561098 - 0.8683292866 0.8587492704 0.8452408910 - 0.8688353896 0.8592939973 0.8458256721 - 0.8693414927 0.8598386049 0.8464105129 - 0.8698475957 0.8603832126 0.8469954133 - 0.8703536987 0.8609278798 0.8475801945 - 0.8708598018 0.8614724874 0.8481649756 - 0.8713654876 0.8620167971 0.8487496972 - 0.8718695045 0.8625596166 0.8493341804 - 0.8723735213 0.8631023765 0.8499186039 - 0.8728774786 0.8636453152 0.8505030870 - 0.8733814955 0.8641880751 0.8510875106 - 0.8738855124 0.8647310138 0.8516719937 - 0.8743895292 0.8652737737 0.8522564173 - 0.8748934865 0.8658165932 0.8528409004 - 0.8753938079 0.8663538098 0.8534224033 - 0.8758934140 0.8668900728 0.8540034890 - 0.8763929009 0.8674263954 0.8545845747 - 0.8768925071 0.8679627180 0.8551657796 - 0.8773921132 0.8684989810 0.8557469249 - 0.8778917193 0.8690353036 0.8563281298 - 0.8783912063 0.8695716262 0.8569092155 - 0.8788912296 0.8701056838 0.8574905992 - 0.8793917894 0.8706367016 0.8580724001 - 0.8798922896 0.8711677194 0.8586542010 - 0.8803929090 0.8716986179 0.8592360020 - 0.8808934093 0.8722295761 0.8598178029 - 0.8813939095 0.8727605939 0.8603996038 - 0.8818945289 0.8732916117 0.8609814048 - 0.8823950291 0.8738226295 0.8615632057 - 0.8828893900 0.8743497729 0.8621373773 - 0.8833830953 0.8748766780 0.8627110124 - 0.8838766813 0.8754035234 0.8632845879 - 0.8843703866 0.8759304285 0.8638582826 - 0.8848639727 0.8764572740 0.8644319177 - 0.8853576779 0.8769841194 0.8650054932 - 0.8858513236 0.8775110245 0.8655791879 - 0.8863434792 0.8780369163 0.8661512136 - 0.8868324161 0.8785607815 0.8667200804 - 0.8873211741 0.8790847063 0.8672890067 - 0.8878101110 0.8796085715 0.8678578734 - 0.8882989287 0.8801324964 0.8684269190 - 0.8887878060 0.8806564212 0.8689957857 - 0.8892766237 0.8811802864 0.8695647120 - 0.8897655010 0.8817042112 0.8701335788 - 0.8902524710 0.8822268844 0.8707010746 - 0.8907384276 0.8827490807 0.8712677956 - 0.8912243247 0.8832712173 0.8718345165 - 0.8917102218 0.8837932944 0.8724011779 - 0.8921961784 0.8843153715 0.8729678988 - 0.8926820755 0.8848375082 0.8735346794 - 0.8931679726 0.8853595853 0.8741014004 - 0.8936539292 0.8858817220 0.8746681213 - 0.8941388726 0.8864020705 0.8752362132 - 0.8946238160 0.8869222999 0.8758044243 - 0.8951086998 0.8874425292 0.8763725758 - 0.8955935836 0.8879626989 0.8769407272 - 0.8960784078 0.8884829283 0.8775088787 - 0.8965632915 0.8890030980 0.8780770898 - 0.8970481753 0.8895233274 0.8786453009 - 0.8975331187 0.8900429010 0.8792127967 - 0.8980188966 0.8905553818 0.8797736764 - 0.8985047936 0.8910679221 0.8803346753 - 0.8989905715 0.8915804029 0.8808956146 - 0.8994764090 0.8920928836 0.8814566135 - 0.8999621868 0.8926054239 0.8820174932 - 0.9004480243 0.8931179047 0.8825784922 - 0.9009338021 0.8936303854 0.8831393719 - 0.9014185071 0.8941423297 0.8836997151 - 0.9018970132 0.8946514130 0.8842570186 - 0.9023755193 0.8951604962 0.8848143816 - 0.9028540850 0.8956695795 0.8853718042 - 0.9033325911 0.8961787224 0.8859291077 - 0.9038110971 0.8966878057 0.8864864707 - 0.9042896032 0.8971968889 0.8870438933 - 0.9047682285 0.8977059722 0.8876011968 - 0.9052457213 0.8982139826 0.8881582022 - 0.9057186246 0.8987172842 0.8887138963 - 0.9061915278 0.8992205858 0.8892694712 - 0.9066643715 0.8997240067 0.8898251057 - 0.9071372747 0.9002273083 0.8903806806 - 0.9076101780 0.9007306099 0.8909363747 - 0.9080830812 0.9012339711 0.8914920092 - 0.9085559845 0.9017372727 0.8920475841 - 0.9090284705 0.9022399783 0.8926029801 - 0.9094979763 0.9027382135 0.8931568265 - 0.9099674821 0.9032363892 0.8937106133 - 0.9104369879 0.9037346244 0.8942644000 - 0.9109064937 0.9042328000 0.8948181868 - 0.9113758802 0.9047309756 0.8953719735 - 0.9118453860 0.9052292109 0.8959258795 - 0.9123148918 0.9057273865 0.8964797258 - 0.9127843976 0.9062256217 0.8970335126 - 0.9132521152 0.9067198038 0.8975815177 - 0.9137197137 0.9072139859 0.8981294036 - 0.9141873121 0.9077082276 0.8986774087 - 0.9146549106 0.9082024097 0.8992254138 - 0.9151225090 0.9086964726 0.8997734189 - 0.9155901074 0.9091907144 0.9003213048 - 0.9160577059 0.9096848965 0.9008693099 - 0.9165253043 0.9101790786 0.9014173150 - 0.9169932008 0.9106711745 0.9019637108 - 0.9174610972 0.9111626744 0.9025096893 - 0.9179289937 0.9116541147 0.9030556083 - 0.9183970094 0.9121456146 0.9036015868 - 0.9188649058 0.9126371145 0.9041476250 - 0.9193328023 0.9131284952 0.9046934843 - 0.9198008180 0.9136199951 0.9052395225 - 0.9202687144 0.9141113758 0.9057855010 - 0.9207351208 0.9146016240 0.9063286781 - 0.9211995006 0.9150900841 0.9068685770 - 0.9216638803 0.9155786037 0.9074085951 - 0.9221282005 0.9160671234 0.9079484940 - 0.9225925803 0.9165555835 0.9084883928 - 0.9230570197 0.9170441031 0.9090284109 - 0.9235213995 0.9175326228 0.9095683098 - 0.9239857793 0.9180210829 0.9101082087 - 0.9244499803 0.9185094237 0.9106479287 - 0.9249091148 0.9189893007 0.9111825824 - 0.9253680706 0.9194691777 0.9117174149 - 0.9258272052 0.9199491143 0.9122521281 - 0.9262862802 0.9204289913 0.9127867818 - 0.9267452955 0.9209089279 0.9133216143 - 0.9272043705 0.9213888049 0.9138563275 - 0.9276635051 0.9218686819 0.9143911004 - 0.9281225204 0.9223486781 0.9149258137 - 0.9285796285 0.9228286147 0.9154574275 - 0.9290350080 0.9233084917 0.9159867167 - 0.9294903278 0.9237884879 0.9165160060 - 0.9299457073 0.9242684245 0.9170452952 - 0.9304010868 0.9247484207 0.9175745845 - 0.9308564067 0.9252282977 0.9181038737 - 0.9313117862 0.9257081747 0.9186331034 - 0.9317672253 0.9261881709 0.9191623926 - 0.9322224855 0.9266681075 0.9196916819 - 0.9326751828 0.9271426201 0.9202163219 - 0.9331278205 0.9276170135 0.9207410216 - 0.9335802794 0.9280914068 0.9212656021 - 0.9340329170 0.9285659194 0.9217903018 - 0.9344854951 0.9290403128 0.9223148823 - 0.9349380732 0.9295147061 0.9228395224 - 0.9353907108 0.9299890995 0.9233642220 - 0.9358432293 0.9304634929 0.9238888025 - 0.9362943769 0.9309374094 0.9244117737 - 0.9367427230 0.9314103723 0.9249315858 - 0.9371910095 0.9318832755 0.9254513979 - 0.9376392961 0.9323561788 0.9259712100 - 0.9380875230 0.9328290820 0.9264910221 - 0.9385358095 0.9333021045 0.9270107746 - 0.9389840961 0.9337750077 0.9275305867 - 0.9394323230 0.9342479110 0.9280503988 - 0.9398806095 0.9347208738 0.9285702109 - 0.9403269887 0.9351912737 0.9290875196 - 0.9407718778 0.9356600046 0.9296032190 - 0.9412168264 0.9361286759 0.9301189184 - 0.9416617155 0.9365974069 0.9306346178 - 0.9421066046 0.9370660186 0.9311503172 - 0.9425514936 0.9375346899 0.9316660762 - 0.9429963827 0.9380034208 0.9321817756 - 0.9434412718 0.9384720922 0.9326974750 - 0.9438862205 0.9389408231 0.9332131743 - 0.9443271160 0.9394043088 0.9337257743 - 0.9447665811 0.9398661256 0.9342374206 - 0.9452061057 0.9403278232 0.9347491264 - 0.9456455708 0.9407895803 0.9352607131 - 0.9460850954 0.9412513971 0.9357724190 - 0.9465246201 0.9417132139 0.9362840056 - 0.9469640851 0.9421749711 0.9367957115 - 0.9474036098 0.9426367879 0.9373072982 - 0.9478430748 0.9430986047 0.9378190041 - 0.9482764006 0.9435576797 0.9383267164 - 0.9487082958 0.9440163970 0.9388335943 - 0.9491403103 0.9444751143 0.9393405914 - 0.9495722055 0.9449337125 0.9398475289 - 0.9500042200 0.9453923702 0.9403545260 - 0.9504361153 0.9458510280 0.9408614039 - 0.9508681297 0.9463096857 0.9413684011 - 0.9513000250 0.9467684031 0.9418752789 - 0.9517319202 0.9472270012 0.9423822761 - 0.9521591067 0.9476765990 0.9428793192 - 0.9525852203 0.9481241703 0.9433743954 - 0.9530112147 0.9485718012 0.9438694119 - 0.9534373283 0.9490193725 0.9443644881 - 0.9538633823 0.9494670033 0.9448595047 - 0.9542894959 0.9499145746 0.9453545809 - 0.9547154903 0.9503622055 0.9458495975 - 0.9551416039 0.9508097768 0.9463446736 - 0.9555677176 0.9512574077 0.9468396902 - 0.9559904933 0.9517037272 0.9473326802 - 0.9564120173 0.9521496296 0.9478248954 - 0.9568334818 0.9525954723 0.9483171105 - 0.9572550058 0.9530413151 0.9488093257 - 0.9576764703 0.9534872174 0.9493014812 - 0.9580979943 0.9539331198 0.9497936964 - 0.9585195184 0.9543790221 0.9502857924 - 0.9589411020 0.9548248053 0.9507780075 - 0.9593626261 0.9552707076 0.9512702227 - 0.9597812295 0.9557142258 0.9517591000 - 0.9601970911 0.9561554790 0.9522449970 - 0.9606130123 0.9565967917 0.9527308941 - 0.9610288739 0.9570381045 0.9532167912 - 0.9614447951 0.9574794769 0.9537026286 - 0.9618607163 0.9579207897 0.9541885257 - 0.9622765779 0.9583621025 0.9546744227 - 0.9626924992 0.9588034153 0.9551603198 - 0.9631084204 0.9592447281 0.9556462169 - 0.9635230899 0.9596858025 0.9561312199 - 0.9639335871 0.9601259232 0.9566131830 - 0.9643440843 0.9605661035 0.9570953250 - 0.9647545815 0.9610062838 0.9575772882 - 0.9651650786 0.9614465237 0.9580593705 - 0.9655756950 0.9618865848 0.9585415125 - 0.9659861922 0.9623268247 0.9590234756 - 0.9663966894 0.9627670050 0.9595056176 - 0.9668071866 0.9632071257 0.9599875808 - 0.9672178030 0.9636473060 0.9604697227 - 0.9676215053 0.9640840888 0.9609463215 - 0.9680240750 0.9645202756 0.9614219069 - 0.9684265852 0.9649565220 0.9618974924 - 0.9688292146 0.9653928280 0.9623731971 - 0.9692317247 0.9658290148 0.9628487825 - 0.9696341753 0.9662652016 0.9633244276 - 0.9700368047 0.9667013884 0.9638000727 - 0.9704393148 0.9671375751 0.9642757177 - 0.9708418846 0.9675738215 0.9647514224 - 0.9712424874 0.9680083990 0.9652252197 - 0.9716396928 0.9684401751 0.9656959176 - 0.9720370173 0.9688720703 0.9661666155 - 0.9724342227 0.9693039060 0.9666373730 - 0.9728314877 0.9697356820 0.9671080709 - 0.9732288122 0.9701675177 0.9675788283 - 0.9736260176 0.9705992937 0.9680495858 - 0.9740232825 0.9710311294 0.9685202837 - 0.9744204879 0.9714629054 0.9689909816 - 0.9748178124 0.9718946815 0.9694617987 - 0.9752097726 0.9723225832 0.9699270725 - 0.9756003022 0.9727494121 0.9703910947 - 0.9759908915 0.9731763005 0.9708551168 - 0.9763814807 0.9736031294 0.9713190794 - 0.9767721295 0.9740298986 0.9717831016 - 0.9771627188 0.9744567275 0.9722471833 - 0.9775531888 0.9748836160 0.9727112055 - 0.9779437780 0.9753103852 0.9731752276 - 0.9783344269 0.9757372141 0.9736391902 - 0.9787241220 0.9761633873 0.9741021991 - 0.9791072011 0.9765844941 0.9745585918 - 0.9794903994 0.9770057201 0.9750149846 - 0.9798734784 0.9774268866 0.9754713178 - 0.9802566767 0.9778481126 0.9759277105 - 0.9806398153 0.9782692194 0.9763841033 - 0.9810230136 0.9786903858 0.9768404961 - 0.9814060926 0.9791116118 0.9772968888 - 0.9817892909 0.9795327783 0.9777532220 - 0.9821724296 0.9799538851 0.9782096148 - 0.9825515747 0.9803730249 0.9786626101 - 0.9829235077 0.9807882905 0.9791095257 - 0.9832953811 0.9812036157 0.9795563221 - 0.9836673141 0.9816188812 0.9800031781 - 0.9840391874 0.9820342064 0.9804500937 - 0.9844111204 0.9824494720 0.9808968902 - 0.9847829938 0.9828647971 0.9813438058 - 0.9851549268 0.9832801223 0.9817907214 - 0.9855268002 0.9836953878 0.9822375178 - 0.9858986735 0.9841107130 0.9826843739 - 0.9862666130 0.9845234156 0.9831274152 - 0.9866302013 0.9849331975 0.9835664034 - 0.9869937897 0.9853429794 0.9840053916 - 0.9873573780 0.9857528210 0.9844443798 - 0.9877210259 0.9861627221 0.9848833084 - 0.9880846143 0.9865725040 0.9853222966 - 0.9884482026 0.9869822860 0.9857612848 - 0.9888117909 0.9873921275 0.9862002730 - 0.9891753793 0.9878020287 0.9866393209 - 0.9895390272 0.9882118106 0.9870783091 - 0.9898961782 0.9886143804 0.9875144958 - 0.9902471900 0.9890102744 0.9879482985 - 0.9905983210 0.9894062281 0.9883821011 - 0.9909493923 0.9898021221 0.9888159037 - 0.9913004041 0.9901980758 0.9892497063 - 0.9916514754 0.9905940294 0.9896833897 - 0.9920024872 0.9909899235 0.9901171923 - 0.9923536181 0.9913858175 0.9905509949 - 0.9927045703 0.9917817116 0.9909847975 - 0.9930557013 0.9921776056 0.9914186001 - 0.9934034944 0.9925646186 0.9918447733 - 0.9937471747 0.9929406047 0.9922618866 - 0.9940909147 0.9933164716 0.9926788807 - 0.9944345951 0.9936925173 0.9930958748 - 0.9947782755 0.9940683842 0.9935129881 - 0.9951220155 0.9944443703 0.9939299822 - 0.9954656959 0.9948204160 0.9943469763 - 0.9958093762 0.9951962829 0.9947639704 - 0.9961531162 0.9955723286 0.9951810837 - 0.9964967966 0.9959481955 0.9955980778 - 0.9968330860 0.9963219166 0.9960101843 - 0.9971498251 0.9966897964 0.9964091182 - 0.9974665046 0.9970576167 0.9968081117 - 0.9977831841 0.9974253774 0.9972071052 - 0.9980999231 0.9977931976 0.9976060987 - 0.9984164834 0.9981610179 0.9980050921 - 0.9987332225 0.9985287786 0.9984040856 - 0.9990499020 0.9988965988 0.9988030195 - 0.9993665814 0.9992644191 0.9992020130 - 0.9996833205 0.9996321797 0.9996010065 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0099759204 0.0084230602 0.0063721500 + 0.0195903908 0.0165482908 0.0126159703 + 0.0281873904 0.0239386503 0.0183614101 + 0.0359400399 0.0306491107 0.0236731302 + 0.0429986902 0.0368482284 0.0286820605 + 0.0495699607 0.0426228605 0.0333807915 + 0.0556899793 0.0479997694 0.0378808789 + 0.0614735186 0.0531351306 0.0421144105 + 0.0669660494 0.0580130592 0.0461965613 + 0.0722220466 0.0627681389 0.0501793101 + 0.0772255808 0.0672829002 0.0540068410 + 0.0820826665 0.0715929568 0.0576650202 + 0.0867473707 0.0758115426 0.0612268411 + 0.0911795869 0.0799473077 0.0647046193 + 0.0954854265 0.0839523673 0.0681224987 + 0.0996826664 0.0878431126 0.0714934394 + 0.1037980989 0.0916301981 0.0747776777 + 0.1078191027 0.0953292921 0.0779552832 + 0.1117319018 0.0989396721 0.0810553879 + 0.1155693978 0.1024464965 0.0841333270 + 0.1193092018 0.1058896035 0.0871890634 + 0.1229984984 0.1092770025 0.0902054086 + 0.1266297996 0.1126255989 0.0931699723 + 0.1301828027 0.1159031019 0.0960779637 + 0.1336570978 0.1191100031 0.0989290774 + 0.1370528936 0.1222644001 0.1017358005 + 0.1403681934 0.1253843009 0.1045102030 + 0.1436405927 0.1284482032 0.1072589979 + 0.1468632966 0.1314789057 0.1099923998 + 0.1500498056 0.1344655007 0.1126931980 + 0.1531880051 0.1374220997 0.1153720021 + 0.1562644988 0.1403357983 0.1180130020 + 0.1593036950 0.1431988031 0.1206170022 + 0.1623243988 0.1460403949 0.1231862009 + 0.1653179973 0.1488437951 0.1257254034 + 0.1682834029 0.1516059935 0.1282379031 + 0.1712214053 0.1543526053 0.1307227015 + 0.1741254926 0.1570729017 0.1331889033 + 0.1769879013 0.1597575992 0.1356364042 + 0.1798096001 0.1624283046 0.1380718946 + 0.1825927943 0.1650680006 0.1404998004 + 0.1853370070 0.1676719934 0.1429184973 + 0.1880521029 0.1702331007 0.1453085989 + 0.1907549053 0.1727772951 0.1476781964 + 0.1934437007 0.1753048003 0.1500300020 + 0.1961088926 0.1777949929 0.1523580998 + 0.1987465024 0.1802501976 0.1546709985 + 0.2013590932 0.1826943010 0.1569592059 + 0.2039510012 0.1851214021 0.1592251062 + 0.2065189034 0.1875294000 0.1614681929 + 0.2090530992 0.1899122000 0.1636908054 + 0.2115564942 0.1922917962 0.1659017056 + 0.2140268981 0.1946634054 0.1680952013 + 0.2164652050 0.1970131993 0.1702757031 + 0.2188750952 0.1993432045 0.1724483967 + 0.2212729007 0.2016437054 0.1746093035 + 0.2236527056 0.2039304972 0.1767639965 + 0.2260089964 0.2062091976 0.1789053977 + 0.2283485979 0.2084756047 0.1810386926 + 0.2306748033 0.2107181996 0.1831596941 + 0.2329774946 0.2129396051 0.1852661967 + 0.2352634072 0.2151475996 0.1873624027 + 0.2375380993 0.2173521072 0.1894447953 + 0.2398073971 0.2195435017 0.1915135980 + 0.2420687973 0.2217237949 0.1935653985 + 0.2443214953 0.2238844931 0.1956079006 + 0.2465686053 0.2260320932 0.1976383030 + 0.2488050014 0.2281741947 0.1996645033 + 0.2510258853 0.2303086966 0.2016756982 + 0.2532328963 0.2324285060 0.2036695927 + 0.2554255128 0.2345477939 0.2056519985 + 0.2576034069 0.2366596013 0.2076213062 + 0.2597728968 0.2387540042 0.2095845044 + 0.2619242072 0.2408431023 0.2115366012 + 0.2640654147 0.2429351956 0.2134785950 + 0.2661896050 0.2450205982 0.2154116929 + 0.2682917118 0.2470878959 0.2173403054 + 0.2703784108 0.2491298020 0.2192642987 + 0.2724463940 0.2511504889 0.2211790979 + 0.2745001018 0.2531620860 0.2230885029 + 0.2765473127 0.2551639080 0.2249957025 + 0.2785851955 0.2571550012 0.2268918008 + 0.2806180120 0.2591302097 0.2287832946 + 0.2826406956 0.2611007094 0.2306707054 + 0.2846564949 0.2630642056 0.2325448990 + 0.2866632938 0.2650120854 0.2344118059 + 0.2886619866 0.2669439912 0.2362738997 + 0.2906560004 0.2688710988 0.2381276935 + 0.2926402986 0.2707965970 0.2399802953 + 0.2946187854 0.2727138996 0.2418286055 + 0.2965883017 0.2746171057 0.2436629981 + 0.2985542119 0.2765100896 0.2454881072 + 0.3005166948 0.2783927023 0.2473084927 + 0.3024731874 0.2802628875 0.2491195947 + 0.3044191897 0.2821266055 0.2509351969 + 0.3063541949 0.2839806080 0.2527473867 + 0.3082849979 0.2858333886 0.2545492053 + 0.3102098107 0.2876932919 0.2563391030 + 0.3121250868 0.2895553112 0.2581259906 + 0.3140304089 0.2914139032 0.2599045038 + 0.3159317076 0.2932642996 0.2616701126 + 0.3178148866 0.2951014042 0.2634266019 + 0.3196887970 0.2969211042 0.2651784122 + 0.3215618134 0.2987315953 0.2669197023 + 0.3234333992 0.3005411923 0.2686525881 + 0.3252997994 0.3023456931 0.2703748941 + 0.3271591067 0.3041403890 0.2720899880 + 0.3290081024 0.3059214950 0.2738001049 + 0.3308455050 0.3076925874 0.2755038142 + 0.3326750100 0.3094542027 0.2772029042 + 0.3345032036 0.3112052083 0.2788943052 + 0.3363313079 0.3129465878 0.2805776000 + 0.3381510079 0.3146811128 0.2822565138 + 0.3399673104 0.3164061010 0.2839331031 + 0.3417668045 0.3181275129 0.2856017947 + 0.3435564041 0.3198527992 0.2872670889 + 0.3453372121 0.3215794861 0.2889324129 + 0.3471119106 0.3233039975 0.2905947864 + 0.3488827050 0.3250232041 0.2922506928 + 0.3506490886 0.3267351091 0.2938998938 + 0.3524067998 0.3284330070 0.2955498099 + 0.3541550934 0.3301151097 0.2971960902 + 0.3558937907 0.3317880034 0.2988429070 + 0.3576247096 0.3334558010 0.3004919887 + 0.3593502939 0.3351208866 0.3021433949 + 0.3610683084 0.3367798030 0.3037939072 + 0.3627772033 0.3384372890 0.3054369092 + 0.3644764125 0.3400928974 0.3070757985 + 0.3661648929 0.3417460024 0.3087131977 + 0.3678394854 0.3433952034 0.3103514910 + 0.3695080876 0.3450438976 0.3119843006 + 0.3711709082 0.3466848135 0.3136082888 + 0.3728272021 0.3483184874 0.3152261972 + 0.3744739890 0.3499465883 0.3168402910 + 0.3761177063 0.3515641093 0.3184512854 + 0.3777532876 0.3531782925 0.3200559020 + 0.3793804944 0.3547908962 0.3216531873 + 0.3810012937 0.3564052880 0.3232400119 + 0.3826153874 0.3580116034 0.3248240948 + 0.3842233121 0.3596132994 0.3264040053 + 0.3858231008 0.3612110019 0.3279787004 + 0.3874185085 0.3627916873 0.3295400143 + 0.3890103102 0.3643645048 0.3310950100 + 0.3905987144 0.3659299016 0.3326450884 + 0.3921895027 0.3674801886 0.3341901898 + 0.3937751055 0.3690252006 0.3357275128 + 0.3953565061 0.3705663085 0.3372594118 + 0.3969357014 0.3721058071 0.3387841880 + 0.3985069990 0.3736424148 0.3403007984 + 0.4000718892 0.3751770854 0.3418123126 + 0.4016300142 0.3767110109 0.3433167934 + 0.4031783938 0.3782438040 0.3448176086 + 0.4047181010 0.3797756135 0.3463155031 + 0.4062578976 0.3813019097 0.3478057981 + 0.4077931046 0.3828235865 0.3492931128 + 0.4093224108 0.3843406141 0.3507778943 + 0.4108479917 0.3858481050 0.3522605002 + 0.4123688936 0.3873529136 0.3537409008 + 0.4138790071 0.3888536990 0.3552181125 + 0.4153836071 0.3903481066 0.3566935062 + 0.4168849885 0.3918395936 0.3581683934 + 0.4183782041 0.3933278024 0.3596439064 + 0.4198674858 0.3948136866 0.3611176014 + 0.4213511050 0.3962967098 0.3625896871 + 0.4228254855 0.3977732062 0.3640539050 + 0.4242956936 0.3992463946 0.3655144870 + 0.4257540107 0.4007085860 0.3669649065 + 0.4272057116 0.4021649063 0.3684150875 + 0.4286471903 0.4036136866 0.3698649108 + 0.4300779998 0.4050632119 0.3713107109 + 0.4315038025 0.4065128863 0.3727546930 + 0.4329301119 0.4079527855 0.3741905987 + 0.4343521893 0.4093911052 0.3756245971 + 0.4357595146 0.4108251035 0.3770546913 + 0.4371610880 0.4122549891 0.3784829080 + 0.4385559857 0.4136810899 0.3799099028 + 0.4399372935 0.4150992930 0.3813312054 + 0.4413124025 0.4165146947 0.3827508986 + 0.4426822960 0.4179250002 0.3841583133 + 0.4440504909 0.4193333089 0.3855640888 + 0.4454061091 0.4207299948 0.3869630098 + 0.4467600882 0.4221264124 0.3883576989 + 0.4481090903 0.4235222936 0.3897452950 + 0.4494557977 0.4249134958 0.3911296129 + 0.4507992864 0.4262988865 0.3925110102 + 0.4521391988 0.4276803136 0.3938843906 + 0.4534761906 0.4290586114 0.3952535987 + 0.4548082054 0.4304304123 0.3966163993 + 0.4561375976 0.4317994118 0.3979771137 + 0.4574612081 0.4331589937 0.3993296921 + 0.4587827027 0.4345161915 0.4006809890 + 0.4600965977 0.4358676076 0.4020248055 + 0.4614087045 0.4372180998 0.4033680856 + 0.4627130032 0.4385639131 0.4046998918 + 0.4640161991 0.4399093091 0.4060314000 + 0.4653095007 0.4412535131 0.4073561132 + 0.4666016996 0.4425976872 0.4086807072 + 0.4678846896 0.4439316094 0.4100016057 + 0.4691669047 0.4452649951 0.4113222957 + 0.4704379141 0.4465923905 0.4126363993 + 0.4717079103 0.4479196072 0.4139505923 + 0.4729768038 0.4492313862 0.4152606130 + 0.4742454886 0.4505420923 0.4165706038 + 0.4755127132 0.4518438876 0.4178735912 + 0.4767796993 0.4531444907 0.4191763997 + 0.4780431092 0.4544411004 0.4204703867 + 0.4793055058 0.4557369947 0.4217635989 + 0.4805589020 0.4570290148 0.4230574071 + 0.4818083942 0.4583194852 0.4243515134 + 0.4830555916 0.4596050978 0.4256447852 + 0.4843012094 0.4608877003 0.4269377887 + 0.4855448008 0.4621653855 0.4282276034 + 0.4867860973 0.4634382129 0.4295150936 + 0.4880253077 0.4647068083 0.4307982028 + 0.4892593920 0.4659673870 0.4320754111 + 0.4904916883 0.4672257006 0.4333496094 + 0.4917111993 0.4684731960 0.4346148968 + 0.4929307103 0.4697206914 0.4358800054 + 0.4941438138 0.4709604084 0.4371437132 + 0.4953565001 0.4722000062 0.4384073019 + 0.4965640903 0.4734326899 0.4396674931 + 0.4977697134 0.4746631980 0.4409270883 + 0.4989753962 0.4758923054 0.4421825111 + 0.5001811981 0.4771201015 0.4434351921 + 0.5013839006 0.4783450067 0.4446885884 + 0.5025752783 0.4795615971 0.4459435046 + 0.5037667155 0.4807780981 0.4471982121 + 0.5049498081 0.4819892943 0.4484477043 + 0.5061323047 0.4832003117 0.4496971965 + 0.5073093176 0.4844068885 0.4509449005 + 0.5084829926 0.4856112003 0.4521920085 + 0.5096563101 0.4868122041 0.4534366131 + 0.5108284950 0.4880064130 0.4546774030 + 0.5120006800 0.4892007113 0.4559176862 + 0.5131582022 0.4903838933 0.4571487904 + 0.5143144727 0.4915668964 0.4583798945 + 0.5154706240 0.4927465916 0.4596010149 + 0.5166264176 0.4939242899 0.4608170986 + 0.5177810192 0.4951008856 0.4620308876 + 0.5189288259 0.4962731004 0.4632383883 + 0.5200765729 0.4974452853 0.4644457996 + 0.5212227106 0.4986133873 0.4656499028 + 0.5223680139 0.4997802079 0.4668532908 + 0.5235089064 0.5009468198 0.4680534899 + 0.5246378779 0.5021128058 0.4692485034 + 0.5257669091 0.5032787919 0.4704434872 + 0.5268948078 0.5044375062 0.4716280997 + 0.5280222893 0.5055947900 0.4728116095 + 0.5291473866 0.5067505240 0.4739918113 + 0.5302665830 0.5079032779 0.4751670957 + 0.5313857794 0.5090559721 0.4763424098 + 0.5325024724 0.5101991296 0.4775088131 + 0.5336183906 0.5113397837 0.4786739051 + 0.5347307920 0.5124807954 0.4798345864 + 0.5358300209 0.5136229992 0.4809859991 + 0.5369291902 0.5147653222 0.4821374118 + 0.5380263925 0.5159034729 0.4832834005 + 0.5391222835 0.5170397162 0.4844275117 + 0.5402178764 0.5181757212 0.4855712950 + 0.5412973166 0.5193089843 0.4867131114 + 0.5423766971 0.5204423070 0.4878548086 + 0.5434548855 0.5215684772 0.4889939129 + 0.5445311069 0.5226852298 0.4901303053 + 0.5456073284 0.5238018036 0.4912666976 + 0.5466775894 0.5249150991 0.4923954010 + 0.5477457047 0.5260273814 0.4935226142 + 0.5488138199 0.5271393061 0.4946480095 + 0.5498805046 0.5282471776 0.4957635105 + 0.5509471893 0.5293549895 0.4968788922 + 0.5520086884 0.5304607153 0.4979912043 + 0.5530598164 0.5315629244 0.4990994930 + 0.5541110039 0.5326650143 0.5002077222 + 0.5551608205 0.5337625146 0.5013095140 + 0.5562096238 0.5348572731 0.5024083853 + 0.5572584271 0.5359520912 0.5035073161 + 0.5583010912 0.5370396972 0.5045971274 + 0.5593425035 0.5381262898 0.5056862831 + 0.5603839159 0.5392122269 0.5067744851 + 0.5614247918 0.5402883887 0.5078564286 + 0.5624656081 0.5413644910 0.5089381933 + 0.5635035038 0.5424379706 0.5100173950 + 0.5645298958 0.5435034037 0.5110903978 + 0.5655562878 0.5445687771 0.5121634007 + 0.5665822029 0.5456321836 0.5132340789 + 0.5676072836 0.5466927290 0.5143020749 + 0.5686323047 0.5477532148 0.5153701901 + 0.5696542859 0.5488079190 0.5164343715 + 0.5706726909 0.5498570204 0.5174958110 + 0.5716910958 0.5509061813 0.5185571909 + 0.5727097988 0.5519514084 0.5196129084 + 0.5737286210 0.5529940128 0.5206657052 + 0.5747473836 0.5540364981 0.5217186213 + 0.5757570863 0.5550705791 0.5227680802 + 0.5767611861 0.5561003089 0.5238164067 + 0.5777652264 0.5571299195 0.5248646736 + 0.5787636042 0.5581539273 0.5259051919 + 0.5797588229 0.5591753721 0.5269432068 + 0.5807541013 0.5601968765 0.5279811025 + 0.5817455053 0.5612133145 0.5290176272 + 0.5827348232 0.5622273088 0.5300536156 + 0.5837240219 0.5632413030 0.5310897231 + 0.5847098231 0.5642498732 0.5321198106 + 0.5856931806 0.5652555823 0.5331475139 + 0.5866767168 0.5662612915 0.5341752768 + 0.5876535177 0.5672615767 0.5351998806 + 0.5886247158 0.5682581067 0.5362228155 + 0.5895959735 0.5692545772 0.5372456908 + 0.5905644894 0.5702462792 0.5382614732 + 0.5915299058 0.5712330937 0.5392717719 + 0.5924953222 0.5722199082 0.5402820110 + 0.5934591889 0.5732024908 0.5412889123 + 0.5944200754 0.5741782784 0.5422912836 + 0.5953810215 0.5751540065 0.5432937741 + 0.5963401794 0.5761274099 0.5442951918 + 0.5972915292 0.5770921111 0.5452939868 + 0.5982427001 0.5780568123 0.5462927222 + 0.5991939902 0.5790212750 0.5472911000 + 0.6001344919 0.5799819231 0.5482859015 + 0.6010748744 0.5809425116 0.5492805839 + 0.6020153761 0.5819031000 0.5502753258 + 0.6029484272 0.5828554034 0.5512647033 + 0.6038792729 0.5838056803 0.5522533059 + 0.6048101783 0.5847560167 0.5532417893 + 0.6057401896 0.5857036710 0.5542263985 + 0.6066694856 0.5866494179 0.5552083850 + 0.6075987220 0.5875951052 0.5561903119 + 0.6085264087 0.5885378718 0.5571714044 + 0.6094487906 0.5894731283 0.5581504703 + 0.6103712916 0.5904083252 0.5591297150 + 0.6112936735 0.5913434029 0.5601087213 + 0.6122087836 0.5922747254 0.5610769987 + 0.6131231785 0.5932055712 0.5620452762 + 0.6140375733 0.5941365957 0.5630136132 + 0.6149482131 0.5950686932 0.5639781952 + 0.6158558130 0.5960016251 0.5649409294 + 0.6167634130 0.5969344974 0.5659034848 + 0.6176698804 0.5978664160 0.5668659210 + 0.6185709834 0.5987945795 0.5678272843 + 0.6194720864 0.5997229218 0.5687887073 + 0.6203731894 0.6006512046 0.5697500706 + 0.6212695241 0.6015707254 0.5707088709 + 0.6221640706 0.6024879217 0.5716670752 + 0.6230587959 0.6034051180 0.5726253986 + 0.6239500046 0.6043199897 0.5735797882 + 0.6248329878 0.6052303910 0.5745280981 + 0.6257159710 0.6061406732 0.5754765272 + 0.6265990138 0.6070510149 0.5764248967 + 0.6274775267 0.6079521179 0.5773686171 + 0.6283550262 0.6088513732 0.5783116817 + 0.6292325258 0.6097506881 0.5792549253 + 0.6301056147 0.6106485724 0.5801956058 + 0.6309671998 0.6115432978 0.5811319947 + 0.6318287253 0.6124380231 0.5820685029 + 0.6326903105 0.6133326292 0.5830048919 + 0.6335496902 0.6142238975 0.5839343071 + 0.6344081163 0.6151139736 0.5848619938 + 0.6352664828 0.6160039902 0.5857896805 + 0.6361234188 0.6168932915 0.5867152214 + 0.6369684935 0.6177780032 0.5876327753 + 0.6378136277 0.6186627150 0.5885503292 + 0.6386587024 0.6195474267 0.5894678831 + 0.6395034194 0.6204267144 0.5903825164 + 0.6403477788 0.6213008165 0.5912950039 + 0.6411921978 0.6221749187 0.5922073722 + 0.6420364976 0.6230490208 0.5931199193 + 0.6428722143 0.6239190102 0.5940275788 + 0.6437057257 0.6247882843 0.5949348211 + 0.6445392966 0.6256574988 0.5958420038 + 0.6453729868 0.6265254021 0.5967481732 + 0.6462082863 0.6273844838 0.5976504087 + 0.6470435858 0.6282436848 0.5985525846 + 0.6478788853 0.6291028261 0.5994548202 + 0.6487107873 0.6299602985 0.6003562212 + 0.6495367289 0.6308152080 0.6012567878 + 0.6503624916 0.6316699982 0.6021572948 + 0.6511883736 0.6325249076 0.6030578017 + 0.6520127058 0.6333740950 0.6039531827 + 0.6528357863 0.6342192888 0.6048460007 + 0.6536588073 0.6350646019 0.6057388186 + 0.6544818878 0.6359099150 0.6066315174 + 0.6552997231 0.6367524266 0.6075202823 + 0.6561157107 0.6375942230 0.6084082127 + 0.6569316983 0.6384360194 0.6092960835 + 0.6577478051 0.6392776966 0.6101840734 + 0.6585525870 0.6401113272 0.6110618114 + 0.6593559980 0.6409441233 0.6119391918 + 0.6601594090 0.6417768002 0.6128165722 + 0.6609628201 0.6426095963 0.6136940122 + 0.6617612243 0.6434395909 0.6145706177 + 0.6625593901 0.6442695856 0.6154471040 + 0.6633576155 0.6450996995 0.6163237095 + 0.6641554832 0.6459290981 0.6171987057 + 0.6649441123 0.6467508078 0.6180636287 + 0.6657326818 0.6475725174 0.6189286113 + 0.6665213108 0.6483941078 0.6197934747 + 0.6673098207 0.6492155790 0.6206579804 + 0.6680960059 0.6500353217 0.6215201020 + 0.6688820720 0.6508548856 0.6223822236 + 0.6696683168 0.6516745090 0.6232442856 + 0.6704540849 0.6524934769 0.6241046190 + 0.6712335944 0.6533043981 0.6249533296 + 0.6720131040 0.6541153789 0.6258019209 + 0.6727924943 0.6549263000 0.6266505718 + 0.6735720038 0.6557372212 0.6274989247 + 0.6743516922 0.6565400958 0.6283429265 + 0.6751313806 0.6573429704 0.6291869879 + 0.6759110093 0.6581459045 0.6300309896 + 0.6766906977 0.6589487791 0.6308749914 + 0.6774597764 0.6597450972 0.6317095160 + 0.6782273054 0.6605407000 0.6325436831 + 0.6789947748 0.6613361835 0.6333777905 + 0.6797623038 0.6621317863 0.6342120171 + 0.6805222034 0.6629223824 0.6350417733 + 0.6812793016 0.6637114286 0.6358706951 + 0.6820363998 0.6645004153 0.6366994977 + 0.6827934980 0.6652892828 0.6375284195 + 0.6835458279 0.6660748124 0.6383498907 + 0.6842942834 0.6668576002 0.6391670704 + 0.6850426793 0.6676403880 0.6399844289 + 0.6857910752 0.6684231758 0.6408016086 + 0.6865373254 0.6692041755 0.6416165233 + 0.6872786283 0.6699815989 0.6424279809 + 0.6880199909 0.6707590222 0.6432394981 + 0.6887614131 0.6715363860 0.6440510154 + 0.6895024776 0.6723132730 0.6448624134 + 0.6902387142 0.6730847955 0.6456729770 + 0.6909748912 0.6738561988 0.6464836001 + 0.6917111278 0.6746276021 0.6472942829 + 0.6924473047 0.6753990054 0.6481049061 + 0.6931800246 0.6761628985 0.6489096880 + 0.6939114928 0.6769244075 0.6497132778 + 0.6946430206 0.6776859164 0.6505168080 + 0.6953744888 0.6784474254 0.6513202786 + 0.6961029768 0.6792098284 0.6521201730 + 0.6968262792 0.6799734235 0.6529155970 + 0.6975495815 0.6807370782 0.6537110806 + 0.6982728839 0.6815006733 0.6545065045 + 0.6989961863 0.6822643876 0.6553019285 + 0.6997179985 0.6830213070 0.6560959816 + 0.7004398108 0.6837782264 0.6568900943 + 0.7011616230 0.6845350862 0.6576842070 + 0.7018833756 0.6852920055 0.6584783792 + 0.7026001811 0.6860477924 0.6592714190 + 0.7033123970 0.6868026257 0.6600636244 + 0.7040244937 0.6875573993 0.6608558893 + 0.7047367096 0.6883121729 0.6616480947 + 0.7054488063 0.6890667081 0.6624394059 + 0.7061572075 0.6898154020 0.6632227898 + 0.7068657279 0.6905639768 0.6640061736 + 0.7075741887 0.6913126707 0.6647896171 + 0.7082825899 0.6920614243 0.6655731201 + 0.7089877129 0.6928064823 0.6663510203 + 0.7096893191 0.6935483217 0.6671249866 + 0.7103909254 0.6942901015 0.6678988934 + 0.7110924125 0.6950318813 0.6686729193 + 0.7117940187 0.6957737207 0.6694468260 + 0.7124900222 0.6965091825 0.6702172756 + 0.7131854892 0.6972442269 0.6709877253 + 0.7138808966 0.6979792118 0.6717581749 + 0.7145763040 0.6987141967 0.6725286841 + 0.7152705789 0.6994475722 0.6732978225 + 0.7159615159 0.7001774907 0.6740645170 + 0.7166523933 0.7009072900 0.6748312116 + 0.7173433900 0.7016372085 0.6755979061 + 0.7180343270 0.7023670077 0.6763646007 + 0.7187235951 0.7030938864 0.6771286130 + 0.7194116116 0.7038189769 0.6778911948 + 0.7200996876 0.7045441866 0.6786538959 + 0.7207878232 0.7052692771 0.6794165969 + 0.7214757800 0.7059944272 0.6801792979 + 0.7221612930 0.7067067027 0.6809418201 + 0.7228463292 0.7074177265 0.6817042828 + 0.7235314250 0.7081285715 0.6824668050 + 0.7242164016 0.7088394761 0.6832293868 + 0.7249004245 0.7095499039 0.6839907765 + 0.7255744934 0.7102571726 0.6847469211 + 0.7262486219 0.7109643817 0.6855030060 + 0.7269226909 0.7116715908 0.6862590909 + 0.7275968194 0.7123787999 0.6870152950 + 0.7282711864 0.7130823135 0.6877691150 + 0.7289463878 0.7137774229 0.6885187030 + 0.7296215892 0.7144724131 0.6892682910 + 0.7302967906 0.7151675224 0.6900178790 + 0.7309719920 0.7158625722 0.6907675266 + 0.7316445112 0.7165569067 0.6915168762 + 0.7323129177 0.7172502875 0.6922662854 + 0.7329813838 0.7179437280 0.6930155754 + 0.7336499095 0.7186371088 0.6937649846 + 0.7343183160 0.7193304896 0.6945142746 + 0.7349863052 0.7200188041 0.6952630281 + 0.7356538177 0.7207021713 0.6960111856 + 0.7363212705 0.7213857174 0.6967594028 + 0.7369887829 0.7220690846 0.6975076199 + 0.7376562953 0.7227525711 0.6982558966 + 0.7383213043 0.7234362960 0.6990029812 + 0.7389839888 0.7241200805 0.6997492909 + 0.7396466136 0.7248039842 0.7004957199 + 0.7403091788 0.7254878879 0.7012420297 + 0.7409719229 0.7261717916 0.7019883990 + 0.7416300774 0.7268511057 0.7027307153 + 0.7422838807 0.7275264263 0.7034698725 + 0.7429378033 0.7282016873 0.7042092085 + 0.7435916066 0.7288768888 0.7049484849 + 0.7442454100 0.7295522094 0.7056878209 + 0.7448980212 0.7302259803 0.7064262033 + 0.7455489039 0.7308980823 0.7071638703 + 0.7461997867 0.7315703034 0.7079014778 + 0.7468507886 0.7322424054 0.7086392045 + 0.7475016713 0.7329145074 0.7093768120 + 0.7481493950 0.7335844040 0.7101135850 + 0.7487897873 0.7342495918 0.7108489275 + 0.7494302988 0.7349148989 0.7115842104 + 0.7500706911 0.7355802059 0.7123194933 + 0.7507110834 0.7362453938 0.7130547762 + 0.7513517141 0.7369098067 0.7137894034 + 0.7519931793 0.7375693917 0.7145212889 + 0.7526347041 0.7382289171 0.7152531743 + 0.7532762289 0.7388885021 0.7159851789 + 0.7539176941 0.7395480871 0.7167171240 + 0.7545592189 0.7402076125 0.7174490094 + 0.7551949024 0.7408636212 0.7181727290 + 0.7558298707 0.7415192723 0.7188962102 + 0.7564650178 0.7421751022 0.7196195722 + 0.7570999861 0.7428308129 0.7203431129 + 0.7577350736 0.7434865236 0.7210665941 + 0.7583732009 0.7441366911 0.7217879295 + 0.7590131760 0.7447838783 0.7225083709 + 0.7596532106 0.7454311252 0.7232288122 + 0.7602931261 0.7460783124 0.7239493132 + 0.7609331012 0.7467254996 0.7246696949 + 0.7615718842 0.7473719120 0.7253888845 + 0.7622076869 0.7480165958 0.7261058092 + 0.7628436089 0.7486613989 0.7268226743 + 0.7634794116 0.7493060827 0.7275395989 + 0.7641152740 0.7499508858 0.7282565236 + 0.7647510767 0.7505956888 0.7289733887 + 0.7653750777 0.7512348294 0.7296857238 + 0.7659978271 0.7518734932 0.7303978801 + 0.7666205168 0.7525122166 0.7311100960 + 0.7672433257 0.7531508803 0.7318223119 + 0.7678660154 0.7537896037 0.7325345278 + 0.7684878707 0.7544282079 0.7332416177 + 0.7691087723 0.7550666928 0.7339441776 + 0.7697296739 0.7557051778 0.7346469164 + 0.7703505754 0.7563437223 0.7353495955 + 0.7709714770 0.7569822073 0.7360522151 + 0.7715923786 0.7576206923 0.7367547154 + 0.7722045183 0.7582557797 0.7374516726 + 0.7728161812 0.7588909268 0.7381488085 + 0.7734280229 0.7595258951 0.7388458848 + 0.7740396857 0.7601609230 0.7395429015 + 0.7746515274 0.7607960105 0.7402399778 + 0.7752640843 0.7614319921 0.7409328818 + 0.7758777738 0.7620692253 0.7416213751 + 0.7764915824 0.7627063990 0.7423098087 + 0.7771053910 0.7633436918 0.7429983020 + 0.7777191997 0.7639809251 0.7436866760 + 0.7783328891 0.7646180987 0.7443752289 + 0.7789419293 0.7652487159 0.7450602055 + 0.7795497179 0.7658780217 0.7457447052 + 0.7801573873 0.7665072083 0.7464292049 + 0.7807651758 0.7671365142 0.7471137047 + 0.7813730240 0.7677657008 0.7477982044 + 0.7819799781 0.7683938742 0.7484809160 + 0.7825825810 0.7690163851 0.7491562963 + 0.7831851840 0.7696390152 0.7498317957 + 0.7837877870 0.7702615261 0.7505072951 + 0.7843903899 0.7708839774 0.7511827946 + 0.7849929929 0.7715066075 0.7518582940 + 0.7855939269 0.7721267939 0.7525311708 + 0.7861924767 0.7727441788 0.7532013059 + 0.7867910862 0.7733615041 0.7538713813 + 0.7873896956 0.7739788890 0.7545415163 + 0.7879884243 0.7745962143 0.7552115917 + 0.7885869741 0.7752135992 0.7558817267 + 0.7891780734 0.7758283019 0.7565454841 + 0.7897650003 0.7764415741 0.7572066784 + 0.7903518081 0.7770550251 0.7578678131 + 0.7909386158 0.7776682973 0.7585290074 + 0.7915254831 0.7782816291 0.7591902018 + 0.7921122909 0.7788950205 0.7598512769 + 0.7926985025 0.7795063257 0.7605091929 + 0.7932845950 0.7801172137 0.7611665130 + 0.7938706875 0.7807281017 0.7618237734 + 0.7944567800 0.7813388705 0.7624810934 + 0.7950428724 0.7819498181 0.7631384134 + 0.7956290245 0.7825607061 0.7637956738 + 0.7962080836 0.7831704021 0.7644450068 + 0.7967863083 0.7837799191 0.7650939226 + 0.7973645926 0.7843893766 0.7657428980 + 0.7979428768 0.7849990129 0.7663918138 + 0.7985212207 0.7856084704 0.7670407295 + 0.7990993857 0.7862181067 0.7676897049 + 0.7996805906 0.7868278027 0.7683331966 + 0.8002619147 0.7874376178 0.7689768076 + 0.8008431792 0.7880473733 0.7696204185 + 0.8014245033 0.7886571884 0.7702640295 + 0.8020058274 0.7892670035 0.7709075212 + 0.8025872111 0.7898766994 0.7715510726 + 0.8031632900 0.7904822826 0.7721858025 + 0.8037390113 0.7910876274 0.7728202939 + 0.8043146133 0.7916929126 0.7734547853 + 0.8048902750 0.7922981977 0.7740892768 + 0.8054659963 0.7929034829 0.7747238278 + 0.8060415983 0.7935088277 0.7753583193 + 0.8066141009 0.7941111922 0.7759894729 + 0.8071860075 0.7947131991 0.7766203284 + 0.8077577949 0.7953152061 0.7772511244 + 0.8083297014 0.7959172130 0.7778819203 + 0.8089016080 0.7965192199 0.7785127163 + 0.8094733953 0.7971212268 0.7791435122 + 0.8100445867 0.7977176905 0.7797755003 + 0.8106154203 0.7983120084 0.7804080248 + 0.8111861944 0.7989063859 0.7810403705 + 0.8117570281 0.7995008230 0.7816727757 + 0.8123278022 0.8000950813 0.7823051810 + 0.8128986955 0.8006895185 0.7829377055 + 0.8134648800 0.8012806177 0.7835683227 + 0.8140262961 0.8018686175 0.7841975093 + 0.8145877123 0.8024566174 0.7848265767 + 0.8151491284 0.8030446172 0.7854558229 + 0.8157104254 0.8036326170 0.7860850096 + 0.8162717819 0.8042206168 0.7867141962 + 0.8168334961 0.8048074245 0.7873411179 + 0.8173961043 0.8053904772 0.7879621983 + 0.8179587126 0.8059734702 0.7885832787 + 0.8185213208 0.8065565825 0.7892044187 + 0.8190839291 0.8071395755 0.7898254991 + 0.8196464181 0.8077226877 0.7904465795 + 0.8202090263 0.8083056808 0.7910677195 + 0.8207656741 0.8088853955 0.7916854024 + 0.8213217258 0.8094648123 0.7923029065 + 0.8218777776 0.8100442290 0.7929204702 + 0.8224338293 0.8106235862 0.7935379744 + 0.8229898214 0.8112030029 0.7941555977 + 0.8235458136 0.8117823005 0.7947731018 + 0.8241016269 0.8123605251 0.7953892946 + 0.8246573806 0.8129374981 0.7960044146 + 0.8252130747 0.8135144711 0.7966195941 + 0.8257688284 0.8140915036 0.7972347140 + 0.8263245225 0.8146684766 0.7978497744 + 0.8268802166 0.8152455091 0.7984648943 + 0.8274357915 0.8158224225 0.7990797758 + 0.8279883265 0.8163983822 0.7996929288 + 0.8285408020 0.8169744015 0.8003060222 + 0.8290932178 0.8175503016 0.8009191155 + 0.8296456933 0.8181263208 0.8015322089 + 0.8301981091 0.8187022805 0.8021453023 + 0.8307505846 0.8192781806 0.8027585149 + 0.8312999010 0.8198543191 0.8033677936 + 0.8318461776 0.8204303980 0.8039743900 + 0.8323925138 0.8210065961 0.8045809269 + 0.8329387903 0.8215826750 0.8051874042 + 0.8334850073 0.8221588731 0.8057940006 + 0.8340312839 0.8227350116 0.8064004779 + 0.8345776200 0.8233110905 0.8070070148 + 0.8351238966 0.8238831758 0.8076115251 + 0.8356701732 0.8244549036 0.8082157969 + 0.8362163901 0.8250266910 0.8088201880 + 0.8367627263 0.8255984783 0.8094245195 + 0.8373090029 0.8261703253 0.8100289106 + 0.8378552794 0.8267419934 0.8106333017 + 0.8383994102 0.8273130059 0.8112372160 + 0.8389375210 0.8278818130 0.8118401766 + 0.8394755125 0.8284506202 0.8124431968 + 0.8400135040 0.8290194273 0.8130462170 + 0.8405516148 0.8295882940 0.8136492968 + 0.8410896063 0.8301571012 0.8142523170 + 0.8416277170 0.8307259083 0.8148552775 + 0.8421646953 0.8312906027 0.8154578805 + 0.8427007198 0.8318520188 0.8160601258 + 0.8432368040 0.8324133754 0.8166623712 + 0.8437728882 0.8329747915 0.8172646761 + 0.8443089128 0.8335362077 0.8178669214 + 0.8448449969 0.8340976238 0.8184692264 + 0.8453810811 0.8346589208 0.8190714717 + 0.8459135294 0.8352158070 0.8196697235 + 0.8464447260 0.8357713223 0.8202667832 + 0.8469758034 0.8363267183 0.8208639026 + 0.8475070000 0.8368821740 0.8214610815 + 0.8480380774 0.8374376297 0.8220582008 + 0.8485692739 0.8379930854 0.8226553202 + 0.8491004705 0.8385484815 0.8232524991 + 0.8496297002 0.8390995860 0.8238487840 + 0.8501586914 0.8396500945 0.8244450092 + 0.8506876230 0.8402007222 0.8250411749 + 0.8512166142 0.8407511711 0.8256375194 + 0.8517454863 0.8413017988 0.8262336850 + 0.8522744775 0.8418524265 0.8268299103 + 0.8528034091 0.8424028754 0.8274261951 + 0.8533318043 0.8429502249 0.8280221224 + 0.8538600802 0.8434972763 0.8286179900 + 0.8543884158 0.8440443873 0.8292139173 + 0.8549165726 0.8445914984 0.8298097849 + 0.8554449081 0.8451387286 0.8304055929 + 0.8559731841 0.8456857800 0.8310015202 + 0.8565015197 0.8462328911 0.8315973878 + 0.8570210934 0.8467777967 0.8321936727 + 0.8575400710 0.8473225236 0.8327900171 + 0.8580591083 0.8478673100 0.8333863020 + 0.8585780859 0.8484120965 0.8339825869 + 0.8590971231 0.8489568233 0.8345788717 + 0.8596161008 0.8495016098 0.8351752162 + 0.8601350784 0.8500462770 0.8357715011 + 0.8606511950 0.8505902290 0.8363643289 + 0.8611670136 0.8511340022 0.8369567990 + 0.8616827726 0.8516777754 0.8375492096 + 0.8621984720 0.8522216082 0.8381416798 + 0.8627142906 0.8527653813 0.8387340903 + 0.8632301092 0.8533092141 0.8393266201 + 0.8637458086 0.8538529277 0.8399189711 + 0.8642572165 0.8543968201 0.8405116796 + 0.8647668958 0.8549407721 0.8411043286 + 0.8652765751 0.8554847240 0.8416969776 + 0.8657861948 0.8560286164 0.8422896862 + 0.8662958741 0.8565725088 0.8428823948 + 0.8668056130 0.8571164012 0.8434751034 + 0.8673152924 0.8576604128 0.8440678120 + 0.8678231835 0.8582047224 0.8446561098 + 0.8683292866 0.8587492704 0.8452408910 + 0.8688353896 0.8592939973 0.8458256721 + 0.8693414927 0.8598386049 0.8464105129 + 0.8698475957 0.8603832126 0.8469954133 + 0.8703536987 0.8609278798 0.8475801945 + 0.8708598018 0.8614724874 0.8481649756 + 0.8713654876 0.8620167971 0.8487496972 + 0.8718695045 0.8625596166 0.8493341804 + 0.8723735213 0.8631023765 0.8499186039 + 0.8728774786 0.8636453152 0.8505030870 + 0.8733814955 0.8641880751 0.8510875106 + 0.8738855124 0.8647310138 0.8516719937 + 0.8743895292 0.8652737737 0.8522564173 + 0.8748934865 0.8658165932 0.8528409004 + 0.8753938079 0.8663538098 0.8534224033 + 0.8758934140 0.8668900728 0.8540034890 + 0.8763929009 0.8674263954 0.8545845747 + 0.8768925071 0.8679627180 0.8551657796 + 0.8773921132 0.8684989810 0.8557469249 + 0.8778917193 0.8690353036 0.8563281298 + 0.8783912063 0.8695716262 0.8569092155 + 0.8788912296 0.8701056838 0.8574905992 + 0.8793917894 0.8706367016 0.8580724001 + 0.8798922896 0.8711677194 0.8586542010 + 0.8803929090 0.8716986179 0.8592360020 + 0.8808934093 0.8722295761 0.8598178029 + 0.8813939095 0.8727605939 0.8603996038 + 0.8818945289 0.8732916117 0.8609814048 + 0.8823950291 0.8738226295 0.8615632057 + 0.8828893900 0.8743497729 0.8621373773 + 0.8833830953 0.8748766780 0.8627110124 + 0.8838766813 0.8754035234 0.8632845879 + 0.8843703866 0.8759304285 0.8638582826 + 0.8848639727 0.8764572740 0.8644319177 + 0.8853576779 0.8769841194 0.8650054932 + 0.8858513236 0.8775110245 0.8655791879 + 0.8863434792 0.8780369163 0.8661512136 + 0.8868324161 0.8785607815 0.8667200804 + 0.8873211741 0.8790847063 0.8672890067 + 0.8878101110 0.8796085715 0.8678578734 + 0.8882989287 0.8801324964 0.8684269190 + 0.8887878060 0.8806564212 0.8689957857 + 0.8892766237 0.8811802864 0.8695647120 + 0.8897655010 0.8817042112 0.8701335788 + 0.8902524710 0.8822268844 0.8707010746 + 0.8907384276 0.8827490807 0.8712677956 + 0.8912243247 0.8832712173 0.8718345165 + 0.8917102218 0.8837932944 0.8724011779 + 0.8921961784 0.8843153715 0.8729678988 + 0.8926820755 0.8848375082 0.8735346794 + 0.8931679726 0.8853595853 0.8741014004 + 0.8936539292 0.8858817220 0.8746681213 + 0.8941388726 0.8864020705 0.8752362132 + 0.8946238160 0.8869222999 0.8758044243 + 0.8951086998 0.8874425292 0.8763725758 + 0.8955935836 0.8879626989 0.8769407272 + 0.8960784078 0.8884829283 0.8775088787 + 0.8965632915 0.8890030980 0.8780770898 + 0.8970481753 0.8895233274 0.8786453009 + 0.8975331187 0.8900429010 0.8792127967 + 0.8980188966 0.8905553818 0.8797736764 + 0.8985047936 0.8910679221 0.8803346753 + 0.8989905715 0.8915804029 0.8808956146 + 0.8994764090 0.8920928836 0.8814566135 + 0.8999621868 0.8926054239 0.8820174932 + 0.9004480243 0.8931179047 0.8825784922 + 0.9009338021 0.8936303854 0.8831393719 + 0.9014185071 0.8941423297 0.8836997151 + 0.9018970132 0.8946514130 0.8842570186 + 0.9023755193 0.8951604962 0.8848143816 + 0.9028540850 0.8956695795 0.8853718042 + 0.9033325911 0.8961787224 0.8859291077 + 0.9038110971 0.8966878057 0.8864864707 + 0.9042896032 0.8971968889 0.8870438933 + 0.9047682285 0.8977059722 0.8876011968 + 0.9052457213 0.8982139826 0.8881582022 + 0.9057186246 0.8987172842 0.8887138963 + 0.9061915278 0.8992205858 0.8892694712 + 0.9066643715 0.8997240067 0.8898251057 + 0.9071372747 0.9002273083 0.8903806806 + 0.9076101780 0.9007306099 0.8909363747 + 0.9080830812 0.9012339711 0.8914920092 + 0.9085559845 0.9017372727 0.8920475841 + 0.9090284705 0.9022399783 0.8926029801 + 0.9094979763 0.9027382135 0.8931568265 + 0.9099674821 0.9032363892 0.8937106133 + 0.9104369879 0.9037346244 0.8942644000 + 0.9109064937 0.9042328000 0.8948181868 + 0.9113758802 0.9047309756 0.8953719735 + 0.9118453860 0.9052292109 0.8959258795 + 0.9123148918 0.9057273865 0.8964797258 + 0.9127843976 0.9062256217 0.8970335126 + 0.9132521152 0.9067198038 0.8975815177 + 0.9137197137 0.9072139859 0.8981294036 + 0.9141873121 0.9077082276 0.8986774087 + 0.9146549106 0.9082024097 0.8992254138 + 0.9151225090 0.9086964726 0.8997734189 + 0.9155901074 0.9091907144 0.9003213048 + 0.9160577059 0.9096848965 0.9008693099 + 0.9165253043 0.9101790786 0.9014173150 + 0.9169932008 0.9106711745 0.9019637108 + 0.9174610972 0.9111626744 0.9025096893 + 0.9179289937 0.9116541147 0.9030556083 + 0.9183970094 0.9121456146 0.9036015868 + 0.9188649058 0.9126371145 0.9041476250 + 0.9193328023 0.9131284952 0.9046934843 + 0.9198008180 0.9136199951 0.9052395225 + 0.9202687144 0.9141113758 0.9057855010 + 0.9207351208 0.9146016240 0.9063286781 + 0.9211995006 0.9150900841 0.9068685770 + 0.9216638803 0.9155786037 0.9074085951 + 0.9221282005 0.9160671234 0.9079484940 + 0.9225925803 0.9165555835 0.9084883928 + 0.9230570197 0.9170441031 0.9090284109 + 0.9235213995 0.9175326228 0.9095683098 + 0.9239857793 0.9180210829 0.9101082087 + 0.9244499803 0.9185094237 0.9106479287 + 0.9249091148 0.9189893007 0.9111825824 + 0.9253680706 0.9194691777 0.9117174149 + 0.9258272052 0.9199491143 0.9122521281 + 0.9262862802 0.9204289913 0.9127867818 + 0.9267452955 0.9209089279 0.9133216143 + 0.9272043705 0.9213888049 0.9138563275 + 0.9276635051 0.9218686819 0.9143911004 + 0.9281225204 0.9223486781 0.9149258137 + 0.9285796285 0.9228286147 0.9154574275 + 0.9290350080 0.9233084917 0.9159867167 + 0.9294903278 0.9237884879 0.9165160060 + 0.9299457073 0.9242684245 0.9170452952 + 0.9304010868 0.9247484207 0.9175745845 + 0.9308564067 0.9252282977 0.9181038737 + 0.9313117862 0.9257081747 0.9186331034 + 0.9317672253 0.9261881709 0.9191623926 + 0.9322224855 0.9266681075 0.9196916819 + 0.9326751828 0.9271426201 0.9202163219 + 0.9331278205 0.9276170135 0.9207410216 + 0.9335802794 0.9280914068 0.9212656021 + 0.9340329170 0.9285659194 0.9217903018 + 0.9344854951 0.9290403128 0.9223148823 + 0.9349380732 0.9295147061 0.9228395224 + 0.9353907108 0.9299890995 0.9233642220 + 0.9358432293 0.9304634929 0.9238888025 + 0.9362943769 0.9309374094 0.9244117737 + 0.9367427230 0.9314103723 0.9249315858 + 0.9371910095 0.9318832755 0.9254513979 + 0.9376392961 0.9323561788 0.9259712100 + 0.9380875230 0.9328290820 0.9264910221 + 0.9385358095 0.9333021045 0.9270107746 + 0.9389840961 0.9337750077 0.9275305867 + 0.9394323230 0.9342479110 0.9280503988 + 0.9398806095 0.9347208738 0.9285702109 + 0.9403269887 0.9351912737 0.9290875196 + 0.9407718778 0.9356600046 0.9296032190 + 0.9412168264 0.9361286759 0.9301189184 + 0.9416617155 0.9365974069 0.9306346178 + 0.9421066046 0.9370660186 0.9311503172 + 0.9425514936 0.9375346899 0.9316660762 + 0.9429963827 0.9380034208 0.9321817756 + 0.9434412718 0.9384720922 0.9326974750 + 0.9438862205 0.9389408231 0.9332131743 + 0.9443271160 0.9394043088 0.9337257743 + 0.9447665811 0.9398661256 0.9342374206 + 0.9452061057 0.9403278232 0.9347491264 + 0.9456455708 0.9407895803 0.9352607131 + 0.9460850954 0.9412513971 0.9357724190 + 0.9465246201 0.9417132139 0.9362840056 + 0.9469640851 0.9421749711 0.9367957115 + 0.9474036098 0.9426367879 0.9373072982 + 0.9478430748 0.9430986047 0.9378190041 + 0.9482764006 0.9435576797 0.9383267164 + 0.9487082958 0.9440163970 0.9388335943 + 0.9491403103 0.9444751143 0.9393405914 + 0.9495722055 0.9449337125 0.9398475289 + 0.9500042200 0.9453923702 0.9403545260 + 0.9504361153 0.9458510280 0.9408614039 + 0.9508681297 0.9463096857 0.9413684011 + 0.9513000250 0.9467684031 0.9418752789 + 0.9517319202 0.9472270012 0.9423822761 + 0.9521591067 0.9476765990 0.9428793192 + 0.9525852203 0.9481241703 0.9433743954 + 0.9530112147 0.9485718012 0.9438694119 + 0.9534373283 0.9490193725 0.9443644881 + 0.9538633823 0.9494670033 0.9448595047 + 0.9542894959 0.9499145746 0.9453545809 + 0.9547154903 0.9503622055 0.9458495975 + 0.9551416039 0.9508097768 0.9463446736 + 0.9555677176 0.9512574077 0.9468396902 + 0.9559904933 0.9517037272 0.9473326802 + 0.9564120173 0.9521496296 0.9478248954 + 0.9568334818 0.9525954723 0.9483171105 + 0.9572550058 0.9530413151 0.9488093257 + 0.9576764703 0.9534872174 0.9493014812 + 0.9580979943 0.9539331198 0.9497936964 + 0.9585195184 0.9543790221 0.9502857924 + 0.9589411020 0.9548248053 0.9507780075 + 0.9593626261 0.9552707076 0.9512702227 + 0.9597812295 0.9557142258 0.9517591000 + 0.9601970911 0.9561554790 0.9522449970 + 0.9606130123 0.9565967917 0.9527308941 + 0.9610288739 0.9570381045 0.9532167912 + 0.9614447951 0.9574794769 0.9537026286 + 0.9618607163 0.9579207897 0.9541885257 + 0.9622765779 0.9583621025 0.9546744227 + 0.9626924992 0.9588034153 0.9551603198 + 0.9631084204 0.9592447281 0.9556462169 + 0.9635230899 0.9596858025 0.9561312199 + 0.9639335871 0.9601259232 0.9566131830 + 0.9643440843 0.9605661035 0.9570953250 + 0.9647545815 0.9610062838 0.9575772882 + 0.9651650786 0.9614465237 0.9580593705 + 0.9655756950 0.9618865848 0.9585415125 + 0.9659861922 0.9623268247 0.9590234756 + 0.9663966894 0.9627670050 0.9595056176 + 0.9668071866 0.9632071257 0.9599875808 + 0.9672178030 0.9636473060 0.9604697227 + 0.9676215053 0.9640840888 0.9609463215 + 0.9680240750 0.9645202756 0.9614219069 + 0.9684265852 0.9649565220 0.9618974924 + 0.9688292146 0.9653928280 0.9623731971 + 0.9692317247 0.9658290148 0.9628487825 + 0.9696341753 0.9662652016 0.9633244276 + 0.9700368047 0.9667013884 0.9638000727 + 0.9704393148 0.9671375751 0.9642757177 + 0.9708418846 0.9675738215 0.9647514224 + 0.9712424874 0.9680083990 0.9652252197 + 0.9716396928 0.9684401751 0.9656959176 + 0.9720370173 0.9688720703 0.9661666155 + 0.9724342227 0.9693039060 0.9666373730 + 0.9728314877 0.9697356820 0.9671080709 + 0.9732288122 0.9701675177 0.9675788283 + 0.9736260176 0.9705992937 0.9680495858 + 0.9740232825 0.9710311294 0.9685202837 + 0.9744204879 0.9714629054 0.9689909816 + 0.9748178124 0.9718946815 0.9694617987 + 0.9752097726 0.9723225832 0.9699270725 + 0.9756003022 0.9727494121 0.9703910947 + 0.9759908915 0.9731763005 0.9708551168 + 0.9763814807 0.9736031294 0.9713190794 + 0.9767721295 0.9740298986 0.9717831016 + 0.9771627188 0.9744567275 0.9722471833 + 0.9775531888 0.9748836160 0.9727112055 + 0.9779437780 0.9753103852 0.9731752276 + 0.9783344269 0.9757372141 0.9736391902 + 0.9787241220 0.9761633873 0.9741021991 + 0.9791072011 0.9765844941 0.9745585918 + 0.9794903994 0.9770057201 0.9750149846 + 0.9798734784 0.9774268866 0.9754713178 + 0.9802566767 0.9778481126 0.9759277105 + 0.9806398153 0.9782692194 0.9763841033 + 0.9810230136 0.9786903858 0.9768404961 + 0.9814060926 0.9791116118 0.9772968888 + 0.9817892909 0.9795327783 0.9777532220 + 0.9821724296 0.9799538851 0.9782096148 + 0.9825515747 0.9803730249 0.9786626101 + 0.9829235077 0.9807882905 0.9791095257 + 0.9832953811 0.9812036157 0.9795563221 + 0.9836673141 0.9816188812 0.9800031781 + 0.9840391874 0.9820342064 0.9804500937 + 0.9844111204 0.9824494720 0.9808968902 + 0.9847829938 0.9828647971 0.9813438058 + 0.9851549268 0.9832801223 0.9817907214 + 0.9855268002 0.9836953878 0.9822375178 + 0.9858986735 0.9841107130 0.9826843739 + 0.9862666130 0.9845234156 0.9831274152 + 0.9866302013 0.9849331975 0.9835664034 + 0.9869937897 0.9853429794 0.9840053916 + 0.9873573780 0.9857528210 0.9844443798 + 0.9877210259 0.9861627221 0.9848833084 + 0.9880846143 0.9865725040 0.9853222966 + 0.9884482026 0.9869822860 0.9857612848 + 0.9888117909 0.9873921275 0.9862002730 + 0.9891753793 0.9878020287 0.9866393209 + 0.9895390272 0.9882118106 0.9870783091 + 0.9898961782 0.9886143804 0.9875144958 + 0.9902471900 0.9890102744 0.9879482985 + 0.9905983210 0.9894062281 0.9883821011 + 0.9909493923 0.9898021221 0.9888159037 + 0.9913004041 0.9901980758 0.9892497063 + 0.9916514754 0.9905940294 0.9896833897 + 0.9920024872 0.9909899235 0.9901171923 + 0.9923536181 0.9913858175 0.9905509949 + 0.9927045703 0.9917817116 0.9909847975 + 0.9930557013 0.9921776056 0.9914186001 + 0.9934034944 0.9925646186 0.9918447733 + 0.9937471747 0.9929406047 0.9922618866 + 0.9940909147 0.9933164716 0.9926788807 + 0.9944345951 0.9936925173 0.9930958748 + 0.9947782755 0.9940683842 0.9935129881 + 0.9951220155 0.9944443703 0.9939299822 + 0.9954656959 0.9948204160 0.9943469763 + 0.9958093762 0.9951962829 0.9947639704 + 0.9961531162 0.9955723286 0.9951810837 + 0.9964967966 0.9959481955 0.9955980778 + 0.9968330860 0.9963219166 0.9960101843 + 0.9971498251 0.9966897964 0.9964091182 + 0.9974665046 0.9970576167 0.9968081117 + 0.9977831841 0.9974253774 0.9972071052 + 0.9980999231 0.9977931976 0.9976060987 + 0.9984164834 0.9981610179 0.9980050921 + 0.9987332225 0.9985287786 0.9984040856 + 0.9990499020 0.9988965988 0.9988030195 + 0.9993665814 0.9992644191 0.9992020130 + 0.9996833205 0.9996321797 0.9996010065 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_advantix_200.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_advantix_200.spi1d index 7492b417..9b15d5c0 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_advantix_200.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_advantix_200.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0109453099 0.0095550315 0.0076129269 - 0.0205393303 0.0180988703 0.0146826003 - 0.0290690009 0.0255930703 0.0210563708 - 0.0365057997 0.0324602798 0.0267342404 - 0.0431397706 0.0389092602 0.0319225714 - 0.0493469499 0.0448722206 0.0368033797 - 0.0553121008 0.0504292287 0.0415878706 - 0.0610060990 0.0556044616 0.0463291109 - 0.0664596632 0.0604789183 0.0508504696 - 0.0717297420 0.0651700869 0.0551753603 - 0.0767887011 0.0697173625 0.0594077297 - 0.0815975219 0.0740136728 0.0635364205 - 0.0862075612 0.0781842917 0.0675580502 - 0.0907116681 0.0822909772 0.0713816136 - 0.0950955972 0.0863237828 0.0743237808 - 0.0993374512 0.0902062133 0.0773941725 - 0.1034374982 0.0940194428 0.0811215192 - 0.1073296964 0.0977483913 0.0852948874 - 0.1110906973 0.1014209017 0.0893627778 - 0.1147643998 0.1050138026 0.0926783979 - 0.1184094027 0.1085262001 0.0958950967 - 0.1220393032 0.1120131016 0.0990428701 - 0.1255874038 0.1154666021 0.1021654978 - 0.1290600002 0.1188692003 0.1052715033 - 0.1324646026 0.1222466975 0.1083573997 - 0.1358134001 0.1256064028 0.1113879010 - 0.1391503960 0.1289401054 0.1143847033 - 0.1424382031 0.1322277933 0.1173340008 - 0.1457035989 0.1354801953 0.1201964989 - 0.1489387006 0.1386919022 0.1230240986 - 0.1521472931 0.1418765932 0.1258170009 - 0.1553370059 0.1450258046 0.1285797060 - 0.1584853977 0.1481391937 0.1313088983 - 0.1615826935 0.1512455940 0.1340118051 - 0.1646398008 0.1543205976 0.1366993934 - 0.1676366031 0.1573432982 0.1393682063 - 0.1705628037 0.1603267044 0.1420211941 - 0.1734506935 0.1632660031 0.1446440071 - 0.1762887985 0.1661476046 0.1472647935 - 0.1791007072 0.1689998060 0.1498612016 - 0.1818982959 0.1718225926 0.1524337977 - 0.1846773028 0.1746128052 0.1549834013 - 0.1874344051 0.1773594022 0.1575088948 - 0.1901603937 0.1800664961 0.1600168049 - 0.1928523034 0.1827678978 0.1625131071 - 0.1955167055 0.1854266971 0.1649871022 - 0.1981182992 0.1880469024 0.1674250066 - 0.2006929070 0.1906183958 0.1698420942 - 0.2032330930 0.1931630969 0.1722377986 - 0.2057420015 0.1956868023 0.1746356040 - 0.2082346976 0.1981967986 0.1770364046 - 0.2106983960 0.2006848007 0.1794321984 - 0.2131627053 0.2031618059 0.1818124950 - 0.2156358063 0.2056335062 0.1841809005 - 0.2180998027 0.2080969065 0.1865282953 - 0.2205528021 0.2105540931 0.1888633072 - 0.2229813933 0.2129819989 0.1911772937 - 0.2253869027 0.2154051960 0.1934747994 - 0.2277887017 0.2178180069 0.1957782060 - 0.2301833928 0.2201987058 0.1980752051 - 0.2325472981 0.2225521058 0.2003640980 - 0.2348767966 0.2248626053 0.2026430070 - 0.2371928990 0.2271340042 0.2049131989 - 0.2394970059 0.2293829024 0.2071806937 - 0.2417781949 0.2316153049 0.2094416022 - 0.2440516949 0.2338470966 0.2117109001 - 0.2463241965 0.2360679954 0.2139746994 - 0.2485778928 0.2382649034 0.2162466943 - 0.2508257926 0.2404524982 0.2185126990 - 0.2530736923 0.2426279038 0.2207739949 - 0.2553015947 0.2447896004 0.2230270952 - 0.2575267851 0.2469552010 0.2252774984 - 0.2597568035 0.2491303980 0.2275131047 - 0.2619835138 0.2513048947 0.2297354937 - 0.2641938031 0.2534686029 0.2319502980 - 0.2663998902 0.2556251884 0.2341517955 - 0.2686052918 0.2577697039 0.2363480031 - 0.2708081007 0.2599022090 0.2385403067 - 0.2730022967 0.2620306909 0.2407180965 - 0.2751887143 0.2641496956 0.2428912967 - 0.2773590982 0.2662425041 0.2450589985 - 0.2795172036 0.2683148086 0.2472172976 - 0.2816726863 0.2703753114 0.2493720949 - 0.2838136852 0.2724193931 0.2515229881 - 0.2859255075 0.2744517922 0.2536627948 - 0.2880168855 0.2764849067 0.2557986081 - 0.2901124954 0.2785145044 0.2579297125 - 0.2922055125 0.2805334926 0.2600534856 - 0.2942928076 0.2825405896 0.2621644139 - 0.2963638008 0.2845480144 0.2642523944 - 0.2984221876 0.2865386903 0.2663209140 - 0.3004606962 0.2885077894 0.2683751881 - 0.3024761081 0.2904664874 0.2704192996 - 0.3044942915 0.2924236953 0.2724545002 - 0.3065181971 0.2943876982 0.2744720876 - 0.3085376918 0.2963528931 0.2764638066 - 0.3105514944 0.2983131111 0.2784410119 - 0.3125489056 0.3002620041 0.2804194987 - 0.3145506084 0.3021939993 0.2823942006 - 0.3165509105 0.3041042089 0.2843523026 - 0.3185442090 0.3060002923 0.2863051891 - 0.3205361962 0.3078896999 0.2882539034 - 0.3225252032 0.3097808063 0.2901889980 - 0.3245142996 0.3116734922 0.2921113074 - 0.3265058100 0.3135590851 0.2940278947 - 0.3284947872 0.3154321909 0.2959429026 - 0.3304775059 0.3173008859 0.2978521883 - 0.3324612975 0.3191635907 0.2997469008 - 0.3344404995 0.3210156858 0.3016290069 - 0.3364037871 0.3228532076 0.3035013080 - 0.3383480906 0.3246806860 0.3053658903 - 0.3402774930 0.3265039027 0.3072257042 - 0.3422003984 0.3283264935 0.3090836108 - 0.3441283107 0.3301435113 0.3109402061 - 0.3460584879 0.3319503963 0.3127999902 - 0.3479807973 0.3337503970 0.3146592081 - 0.3498908877 0.3355464935 0.3165098131 - 0.3517873883 0.3373399973 0.3183467984 - 0.3536739051 0.3391256034 0.3201710880 - 0.3555468917 0.3408975899 0.3219870925 - 0.3574047089 0.3426569104 0.3237960935 - 0.3592503965 0.3444055915 0.3255974948 - 0.3610936105 0.3461483121 0.3273890913 - 0.3629257977 0.3478848040 0.3291662931 - 0.3647486866 0.3496142030 0.3309336901 - 0.3665607870 0.3513360918 0.3326914012 - 0.3683531880 0.3530487120 0.3344373107 - 0.3701289892 0.3547559083 0.3361791968 - 0.3719004095 0.3564561903 0.3379172087 - 0.3736707866 0.3581486046 0.3396506011 - 0.3754306138 0.3598242104 0.3413727880 - 0.3771795928 0.3614909947 0.3430913985 - 0.3789179921 0.3631505966 0.3448042870 - 0.3806397915 0.3648028076 0.3465045989 - 0.3823508024 0.3664542139 0.3481892049 - 0.3840552866 0.3680998981 0.3498643935 - 0.3857525885 0.3697325885 0.3515287936 - 0.3874309957 0.3713547885 0.3531869948 - 0.3890962005 0.3729732037 0.3548409939 - 0.3907422125 0.3745886981 0.3564892113 - 0.3923803866 0.3761973083 0.3581174910 - 0.3940139115 0.3778007925 0.3597348928 - 0.3956401050 0.3793950975 0.3613364995 - 0.3972538114 0.3809765875 0.3629319072 - 0.3988597989 0.3825520873 0.3645235002 - 0.4004507959 0.3841212988 0.3661076128 - 0.4020316899 0.3856911063 0.3676817119 - 0.4036057889 0.3872612119 0.3692494035 - 0.4051836133 0.3888235986 0.3708040118 - 0.4067601860 0.3903797865 0.3723560870 - 0.4083349109 0.3919270039 0.3739052117 - 0.4099012911 0.3934660852 0.3754433095 - 0.4114632905 0.3950020969 0.3769756854 - 0.4130123854 0.3965331018 0.3784922063 - 0.4145576060 0.3980642855 0.3800047934 - 0.4160979986 0.3995954990 0.3815135956 - 0.4176265895 0.4011205137 0.3830150068 - 0.4191510081 0.4026427865 0.3845132887 - 0.4206664860 0.4041537941 0.3859997094 - 0.4221807122 0.4056634009 0.3874785900 - 0.4236926138 0.4071705043 0.3889462948 - 0.4251953065 0.4086725116 0.3904061019 - 0.4266924858 0.4101715982 0.3918620050 - 0.4281755090 0.4116598964 0.3933145106 - 0.4296549857 0.4131458998 0.3947649896 - 0.4311212897 0.4146251082 0.3962041140 - 0.4325863123 0.4161024094 0.3976391852 - 0.4340479076 0.4175755084 0.3990663886 - 0.4355044961 0.4190461934 0.4004910886 - 0.4369558096 0.4205146134 0.4019137025 - 0.4384000897 0.4219822884 0.4033333063 - 0.4398407042 0.4234496057 0.4047515988 - 0.4412648976 0.4249095023 0.4061658084 - 0.4426848888 0.4263676107 0.4075790942 - 0.4440922141 0.4278148115 0.4089758992 - 0.4454984963 0.4292607903 0.4103710055 - 0.4469017088 0.4306946993 0.4117515087 - 0.4483037889 0.4321266115 0.4131309986 - 0.4496976137 0.4335466921 0.4145058095 - 0.4510863125 0.4349648952 0.4158783853 - 0.4524526000 0.4363755882 0.4172433019 - 0.4538140893 0.4377841055 0.4186027944 - 0.4551594853 0.4391863942 0.4199478924 - 0.4565044940 0.4405857027 0.4212876856 - 0.4578481019 0.4419767857 0.4226151109 - 0.4591886997 0.4433653951 0.4239403903 - 0.4605211020 0.4447469115 0.4252606928 - 0.4618496001 0.4461270869 0.4265792966 - 0.4631653130 0.4475035965 0.4278936982 - 0.4644770026 0.4488789141 0.4292060137 - 0.4657723904 0.4502497017 0.4305118024 - 0.4670655131 0.4516187906 0.4318155944 - 0.4683431089 0.4529778957 0.4331100881 - 0.4696202874 0.4543359876 0.4344033897 - 0.4708895981 0.4556787014 0.4356841147 - 0.4721587896 0.4570212960 0.4369646907 - 0.4734266996 0.4583550096 0.4382334054 - 0.4746944010 0.4596883059 0.4395017922 - 0.4759570956 0.4610117078 0.4407595098 - 0.4772185087 0.4623329043 0.4420154095 - 0.4784702957 0.4636422098 0.4432603121 - 0.4797168970 0.4649454951 0.4445005059 - 0.4809587896 0.4662468135 0.4457359910 - 0.4821954966 0.4675458074 0.4469672143 - 0.4834313095 0.4688419104 0.4481967986 - 0.4846647084 0.4701308012 0.4494228959 - 0.4858981073 0.4714190066 0.4506483972 - 0.4871313870 0.4726983011 0.4518673122 - 0.4883646965 0.4739777148 0.4530862868 - 0.4895890951 0.4752514064 0.4542964995 - 0.4908111989 0.4765237868 0.4555051029 - 0.4920256138 0.4777913094 0.4567083120 - 0.4932329953 0.4790548086 0.4579074085 - 0.4944368899 0.4803156853 0.4591037929 - 0.4956291020 0.4815686941 0.4602927864 - 0.4968214035 0.4828216136 0.4614818096 - 0.4980075061 0.4840664864 0.4626607001 - 0.4991930127 0.4853107929 0.4638391137 - 0.5003730059 0.4865506887 0.4650115073 - 0.5015485883 0.4877873063 0.4661796987 - 0.5027217269 0.4890219867 0.4673461020 - 0.5038836002 0.4902490973 0.4685058892 - 0.5050454736 0.4914762080 0.4696657956 - 0.5061964989 0.4927015007 0.4708235860 - 0.5073444247 0.4939264953 0.4719808996 - 0.5084874034 0.4951480031 0.4731355011 - 0.5096216798 0.4963639975 0.4742859006 - 0.5107560754 0.4975799024 0.4754363894 - 0.5118848085 0.4987801015 0.4765751958 - 0.5130128860 0.4999789000 0.4777132869 - 0.5141406059 0.5011754036 0.4788497090 - 0.5152677298 0.5023689270 0.4799841046 - 0.5163947940 0.5035623908 0.4811184108 - 0.5175107121 0.5047491193 0.4822545052 - 0.5186256170 0.5059353709 0.4833906889 - 0.5197319984 0.5071169734 0.4845212996 - 0.5208250284 0.5082917213 0.4856444001 - 0.5219178796 0.5094662905 0.4867675006 - 0.5230032802 0.5106360912 0.4878782928 - 0.5240870118 0.5118049979 0.4889872968 - 0.5251713991 0.5129719973 0.4900952876 - 0.5262575746 0.5141333938 0.4912008941 - 0.5273438096 0.5152949095 0.4923065007 - 0.5284274220 0.5164546967 0.4934087098 - 0.5295094252 0.5176134706 0.4945091903 - 0.5305913091 0.5187724233 0.4956094921 - 0.5316656828 0.5199283957 0.4966976941 - 0.5327398181 0.5210844874 0.4977858961 - 0.5338104963 0.5222364068 0.4988707900 - 0.5348731279 0.5233796239 0.4999495149 - 0.5359356999 0.5245227814 0.5010282993 - 0.5369876027 0.5256589055 0.5021041036 - 0.5380324721 0.5267904997 0.5031781197 - 0.5390774012 0.5279220939 0.5042521954 - 0.5401139259 0.5290479064 0.5053207278 - 0.5411491990 0.5301730037 0.5063884854 - 0.5421838760 0.5312972069 0.5074563026 - 0.5432150960 0.5324152112 0.5085229278 - 0.5442463160 0.5335332751 0.5095896125 - 0.5452744961 0.5346456170 0.5106530786 - 0.5462962985 0.5357465148 0.5117110014 - 0.5473181009 0.5368474126 0.5127688050 - 0.5483338237 0.5379434228 0.5138180852 - 0.5493434072 0.5390350223 0.5148600936 - 0.5503531098 0.5401265025 0.5159021020 - 0.5513597131 0.5412139297 0.5169410706 - 0.5523648262 0.5422992110 0.5179787278 - 0.5533698201 0.5433844924 0.5190163255 - 0.5543758273 0.5444642901 0.5200524926 - 0.5553820729 0.5455427170 0.5210883021 - 0.5563883185 0.5466210246 0.5221241713 - 0.5573911071 0.5476927161 0.5231577754 - 0.5583932996 0.5487633944 0.5241910815 - 0.5593954921 0.5498341918 0.5252243876 - 0.5603914857 0.5508939028 0.5262517929 - 0.5613868237 0.5519527793 0.5272789001 - 0.5623821020 0.5530115962 0.5283061266 - 0.5633684993 0.5540567040 0.5293180943 - 0.5643541813 0.5551009178 0.5303297043 - 0.5653399229 0.5561451912 0.5313413143 - 0.5663121939 0.5571808219 0.5323446989 - 0.5672833920 0.5582157969 0.5333477259 - 0.5682544708 0.5592507124 0.5343508124 - 0.5692139864 0.5602828264 0.5353485942 - 0.5701718926 0.5613144040 0.5363458991 - 0.5711297989 0.5623461008 0.5373433232 - 0.5720816255 0.5633695126 0.5383352041 - 0.5730317831 0.5643910170 0.5393260717 - 0.5739821196 0.5654125810 0.5403169990 - 0.5749322176 0.5664225817 0.5413028002 - 0.5758821964 0.5674279928 0.5422866940 - 0.5768321753 0.5684334040 0.5432707071 - 0.5777807832 0.5694364905 0.5442500114 - 0.5787283182 0.5704377294 0.5452260971 - 0.5796757936 0.5714389086 0.5462023020 - 0.5806186795 0.5724375248 0.5471749902 - 0.5815541744 0.5734320879 0.5481427908 - 0.5824897289 0.5744267106 0.5491107106 - 0.5834230781 0.5754204988 0.5500766039 - 0.5843461156 0.5764102936 0.5510352850 - 0.5852690935 0.5774000883 0.5519940257 - 0.5861920714 0.5783898830 0.5529527068 - 0.5871120095 0.5793784857 0.5539078116 - 0.5880315900 0.5803670287 0.5548627973 - 0.5889511108 0.5813555121 0.5558177829 - 0.5898649096 0.5823364258 0.5567662120 - 0.5907754898 0.5833132267 0.5577116013 - 0.5916861296 0.5842900872 0.5586568713 - 0.5925920010 0.5852631927 0.5595967174 - 0.5934880972 0.5862287879 0.5605269074 - 0.5943841934 0.5871945024 0.5614570975 - 0.5952801704 0.5881602168 0.5623872280 - 0.5961638093 0.5891243815 0.5633127093 - 0.5970469713 0.5900886059 0.5642381907 - 0.5979301929 0.5910527110 0.5651637912 - 0.5988095999 0.5920161009 0.5660883784 - 0.5996860862 0.5929788947 0.5670126081 - 0.6005626917 0.5939416885 0.5679367781 - 0.6014385223 0.5949037075 0.5688589215 - 0.6023102999 0.5958616734 0.5697726011 - 0.6031820774 0.5968198180 0.5706862211 - 0.6040539145 0.5977777839 0.5715999007 - 0.6049264073 0.5987269282 0.5725020766 - 0.6057993174 0.5996723175 0.5734000802 - 0.6066721082 0.6006177068 0.5742980838 - 0.6075447202 0.6015614271 0.5751948953 - 0.6084157825 0.6024988294 0.5760878921 - 0.6092870235 0.6034362912 0.5769808888 - 0.6101580858 0.6043736935 0.5778738856 - 0.6110234261 0.6053066254 0.5787655711 - 0.6118857265 0.6062374115 0.5796566010 - 0.6127480268 0.6071680784 0.5805476904 - 0.6136096716 0.6080988050 0.5814384818 - 0.6144647002 0.6090288162 0.5823276043 - 0.6153196096 0.6099588275 0.5832166076 - 0.6161745191 0.6108887196 0.5841056705 - 0.6170253158 0.6118140221 0.5849937797 - 0.6178714037 0.6127341986 0.5858809948 - 0.6187174916 0.6136543751 0.5867682099 - 0.6195635796 0.6145746112 0.5876554251 - 0.6203994155 0.6154835820 0.5885332227 - 0.6212329268 0.6163904071 0.5894092917 - 0.6220663786 0.6172971725 0.5902854204 - 0.6228989959 0.6182032228 0.5911598206 - 0.6237251759 0.6191043854 0.5920251012 - 0.6245512962 0.6200054884 0.5928903818 - 0.6253774762 0.6209065914 0.5937557220 - 0.6262025833 0.6218063831 0.5946201086 - 0.6270262003 0.6227043867 0.5954833031 - 0.6278496981 0.6236023903 0.5963464975 - 0.6286731958 0.6245002747 0.5972098112 - 0.6294925809 0.6253930926 0.5980716944 - 0.6303092837 0.6262826920 0.5989329815 - 0.6311259866 0.6271722913 0.5997943282 - 0.6319425702 0.6280620098 0.6006556153 - 0.6327558756 0.6289452910 0.6015126705 - 0.6335682273 0.6298272014 0.6023688912 - 0.6343805790 0.6307089925 0.6032251716 - 0.6351928711 0.6315909028 0.6040813923 - 0.6360028982 0.6324610710 0.6049261093 - 0.6368128061 0.6333307028 0.6057704091 - 0.6376225948 0.6342003942 0.6066148281 - 0.6384323835 0.6350694895 0.6074587703 - 0.6392394900 0.6359295249 0.6082984209 - 0.6400464773 0.6367895007 0.6091380715 - 0.6408535838 0.6376495957 0.6099777222 - 0.6416608095 0.6385089159 0.6108170748 - 0.6424685717 0.6393631101 0.6116548777 - 0.6432763934 0.6402173042 0.6124926805 - 0.6440842748 0.6410714984 0.6133304834 - 0.6448920965 0.6419250965 0.6141679883 - 0.6456990838 0.6427741051 0.6150038242 - 0.6465060711 0.6436231136 0.6158394814 - 0.6473131776 0.6444721222 0.6166753173 - 0.6481201053 0.6453201771 0.6175107956 - 0.6489260197 0.6461594105 0.6183446050 - 0.6497318745 0.6469987035 0.6191784739 - 0.6505379081 0.6478378773 0.6200122833 - 0.6513438821 0.6486768723 0.6208459735 - 0.6521543264 0.6495088935 0.6216778159 - 0.6529647112 0.6503409147 0.6225095987 - 0.6537752151 0.6511728764 0.6233413219 - 0.6545855999 0.6520048976 0.6241731048 - 0.6553974152 0.6528308988 0.6250030994 - 0.6562091708 0.6536563039 0.6258329153 - 0.6570211053 0.6544817090 0.6266627908 - 0.6578329206 0.6553071141 0.6274926066 - 0.6586399078 0.6561266780 0.6283208728 - 0.6594451070 0.6569445133 0.6291487813 - 0.6602504253 0.6577622890 0.6299766898 - 0.6610556841 0.6585801244 0.6308044791 - 0.6618577242 0.6593968868 0.6316313744 - 0.6626570225 0.6602129936 0.6324573755 - 0.6634563208 0.6610292196 0.6332834959 - 0.6642554998 0.6618453264 0.6341096163 - 0.6650528908 0.6626601815 0.6349341869 - 0.6658458114 0.6634724140 0.6357557178 - 0.6666386127 0.6642845869 0.6365773082 - 0.6674314737 0.6650968194 0.6373988986 - 0.6682242751 0.6659088731 0.6382200718 - 0.6690135002 0.6667075753 0.6390303969 - 0.6698027849 0.6675063968 0.6398407221 - 0.6705920100 0.6683050990 0.6406509876 - 0.6713812947 0.6691039205 0.6414613128 - 0.6721655726 0.6698979139 0.6422684193 - 0.6729472876 0.6706895828 0.6430739760 - 0.6737290025 0.6714811921 0.6438794732 - 0.6745107174 0.6722728014 0.6446850896 - 0.6752899885 0.6730641127 0.6454901099 - 0.6760625243 0.6738544703 0.6462938786 - 0.6768351197 0.6746450067 0.6470977068 - 0.6776077151 0.6754354239 0.6479014158 - 0.6783801913 0.6762257814 0.6487051845 - 0.6791486740 0.6770142913 0.6495074034 - 0.6799163222 0.6778023839 0.6503092051 - 0.6806839705 0.6785904765 0.6511111259 - 0.6814516187 0.6793786287 0.6519129872 - 0.6822186708 0.6801651120 0.6527122855 - 0.6829848289 0.6809486747 0.6535074115 - 0.6837509274 0.6817322969 0.6543024778 - 0.6845170856 0.6825159192 0.6550976038 - 0.6852831841 0.6832996011 0.6558926105 - 0.6860461235 0.6840777993 0.6566833258 - 0.6868082881 0.6848548055 0.6574732065 - 0.6875705123 0.6856318712 0.6582630873 - 0.6883326769 0.6864088774 0.6590530276 - 0.6890928745 0.6871842146 0.6598421931 - 0.6898465157 0.6879535913 0.6606296897 - 0.6905999780 0.6887230277 0.6614171863 - 0.6913534999 0.6894924045 0.6622046828 - 0.6921070814 0.6902617812 0.6629921794 - 0.6928570271 0.6910268068 0.6637781858 - 0.6936045885 0.6917887926 0.6645631790 - 0.6943520904 0.6925508976 0.6653482914 - 0.6950995922 0.6933128834 0.6661332846 - 0.6958470941 0.6940749884 0.6669183969 - 0.6965919733 0.6948326826 0.6677011847 - 0.6973364949 0.6955900192 0.6684839129 - 0.6980810761 0.6963472962 0.6692665815 - 0.6988255978 0.6971045136 0.6700494289 - 0.6995682716 0.6978610754 0.6708310843 - 0.7003021240 0.6986145973 0.6716089249 - 0.7010359168 0.6993680000 0.6723867059 - 0.7017697096 0.7001215219 0.6731644869 - 0.7025035024 0.7008749247 0.6739423871 - 0.7032322884 0.7016248107 0.6747155190 - 0.7039532065 0.7023693919 0.6754820943 - 0.7046741843 0.7031139731 0.6762487292 - 0.7053951025 0.7038586140 0.6770153046 - 0.7061160207 0.7046031952 0.6777818799 - 0.7068319917 0.7053433061 0.6785455942 - 0.7075439095 0.7060800195 0.6793071032 - 0.7082558870 0.7068166137 0.6800686717 - 0.7089678049 0.7075533271 0.6808302999 - 0.7096797824 0.7082899213 0.6815918088 - 0.7103822827 0.7090213895 0.6823518276 - 0.7110798955 0.7097504139 0.6831110120 - 0.7117773890 0.7104794979 0.6838701963 - 0.7124750018 0.7112085223 0.6846293807 - 0.7131726146 0.7119374871 0.6853885055 - 0.7138615251 0.7126597166 0.6861459017 - 0.7145469785 0.7133795023 0.6869027019 - 0.7152324915 0.7140992880 0.6876593828 - 0.7159180045 0.7148190141 0.6884161830 - 0.7166035175 0.7155387998 0.6891729236 - 0.7172850966 0.7162548900 0.6899223924 - 0.7179651856 0.7169696093 0.6906694770 - 0.7186452150 0.7176843286 0.6914165020 - 0.7193253040 0.7183991075 0.6921635866 - 0.7200053930 0.7191138268 0.6929106116 - 0.7206829190 0.7198268175 0.6936489940 - 0.7213590145 0.7205389738 0.6943836808 - 0.7220352292 0.7212511897 0.6951184273 - 0.7227113843 0.7219635248 0.6958531141 - 0.7233875990 0.7226756811 0.6965876818 - 0.7240635753 0.7233844995 0.6973186731 - 0.7247393727 0.7240908146 0.6980472207 - 0.7254152894 0.7247970104 0.6987757087 - 0.7260912061 0.7255033255 0.6995041966 - 0.7267671227 0.7262095809 0.7002326846 - 0.7274426818 0.7269139290 0.7009603977 - 0.7281178832 0.7276154757 0.7016872168 - 0.7287930846 0.7283170223 0.7024139762 - 0.7294682860 0.7290186286 0.7031407952 - 0.7301434875 0.7297201157 0.7038676143 - 0.7308180928 0.7304204106 0.7045934200 - 0.7314903736 0.7311158776 0.7053160071 - 0.7321627140 0.7318114042 0.7060384750 - 0.7328349948 0.7325068116 0.7067610025 - 0.7335072160 0.7332022786 0.7074834704 - 0.7341794968 0.7338978052 0.7082059979 - 0.7348482013 0.7345873117 0.7089205980 - 0.7355166078 0.7352764010 0.7096347213 - 0.7361850142 0.7359654903 0.7103487849 - 0.7368534207 0.7366545796 0.7110629082 - 0.7375218272 0.7373437285 0.7117769718 - 0.7381889820 0.7380310297 0.7124879956 - 0.7388554215 0.7387169003 0.7131969929 - 0.7395219207 0.7394028902 0.7139061093 - 0.7401883006 0.7400888205 0.7146151066 - 0.7408546805 0.7407748103 0.7153242230 - 0.7415205836 0.7414584756 0.7160325050 - 0.7421839833 0.7421336770 0.7167384028 - 0.7428473830 0.7428088784 0.7174441814 - 0.7435107827 0.7434840798 0.7181500793 - 0.7441741824 0.7441592813 0.7188559771 - 0.7448375821 0.7448344827 0.7195618749 - 0.7454971075 0.7455040216 0.7202640772 - 0.7461552024 0.7461715937 0.7209653258 - 0.7468132973 0.7468392849 0.7216665149 - 0.7474713922 0.7475069761 0.7223677039 - 0.7481294870 0.7481747270 0.7230688930 - 0.7487857938 0.7488409877 0.7237700820 - 0.7494363785 0.7495033741 0.7244712114 - 0.7500869036 0.7501658797 0.7251722813 - 0.7507374883 0.7508283854 0.7258734107 - 0.7513880730 0.7514907718 0.7265745997 - 0.7520385981 0.7521532774 0.7272757292 - 0.7526834011 0.7528085709 0.7279723287 - 0.7533255816 0.7534605861 0.7286670804 - 0.7539677024 0.7541127205 0.7293617725 - 0.7546098828 0.7547647953 0.7300565839 - 0.7552520037 0.7554169297 0.7307513952 - 0.7558938861 0.7560682893 0.7314453721 - 0.7565317750 0.7567108870 0.7321308851 - 0.7571697235 0.7573534250 0.7328165174 - 0.7578076124 0.7579960227 0.7335019708 - 0.7584453821 0.7586385012 0.7341874838 - 0.7590832710 0.7592810988 0.7348731160 - 0.7597208023 0.7599217296 0.7355561256 - 0.7603574991 0.7605590820 0.7362353206 - 0.7609941959 0.7611966133 0.7369145155 - 0.7616308928 0.7618340254 0.7375937104 - 0.7622675896 0.7624713778 0.7382730246 - 0.7629042864 0.7631089091 0.7389522195 - 0.7635387778 0.7637441754 0.7396275997 - 0.7641718984 0.7643783092 0.7403010130 - 0.7648050189 0.7650123835 0.7409744263 - 0.7654381990 0.7656465173 0.7416477799 - 0.7660713196 0.7662805915 0.7423211932 - 0.7667044997 0.7669147253 0.7429946065 - 0.7673382759 0.7675452828 0.7436637878 - 0.7679721713 0.7681750059 0.7443320155 - 0.7686061263 0.7688047290 0.7450003028 - 0.7692400217 0.7694345117 0.7456685901 - 0.7698739767 0.7700641751 0.7463368773 - 0.7705078721 0.7706940174 0.7470052242 - 0.7711389065 0.7713199258 0.7476691008 - 0.7717695832 0.7719454765 0.7483327985 - 0.7724003792 0.7725712061 0.7489963770 - 0.7730311155 0.7731968164 0.7496600747 - 0.7736619115 0.7738224864 0.7503237724 - 0.7742925882 0.7744480968 0.7509874105 - 0.7749173045 0.7750692964 0.7516471148 - 0.7755420208 0.7756903768 0.7523068786 - 0.7761667967 0.7763115764 0.7529665828 - 0.7767915130 0.7769327760 0.7536262870 - 0.7774162292 0.7775539160 0.7542861104 - 0.7780408859 0.7781749964 0.7549456954 - 0.7786604166 0.7787933946 0.7556020021 - 0.7792798281 0.7794117928 0.7562583089 - 0.7798992991 0.7800301909 0.7569146156 - 0.7805187106 0.7806485891 0.7575709224 - 0.7811381817 0.7812669873 0.7582271099 - 0.7817575932 0.7818853855 0.7588834167 - 0.7823737264 0.7824988961 0.7595390081 - 0.7829896212 0.7831121087 0.7601945996 - 0.7836055160 0.7837253213 0.7608501911 - 0.7842214108 0.7843385935 0.7615057826 - 0.7848373055 0.7849518061 0.7621613741 - 0.7854532003 0.7855650187 0.7628170848 - 0.7860642076 0.7861744165 0.7634735703 - 0.7866740227 0.7867829204 0.7641304135 - 0.7872837782 0.7873914242 0.7647870779 - 0.7878935933 0.7879999876 0.7654439211 - 0.7885034084 0.7886084914 0.7661007047 - 0.7891132236 0.7892169952 0.7667574286 - 0.7897166014 0.7898225784 0.7674102187 - 0.7903156877 0.7904263735 0.7680606842 - 0.7909148932 0.7910302281 0.7687112093 - 0.7915139794 0.7916340232 0.7693616748 - 0.7921131849 0.7922376990 0.7700121999 - 0.7927122712 0.7928414941 0.7706627250 - 0.7933096290 0.7934436202 0.7713115215 - 0.7939032912 0.7940425277 0.7719575763 - 0.7944970131 0.7946413755 0.7726035714 - 0.7950906157 0.7952402830 0.7732496262 - 0.7956842780 0.7958391905 0.7738956809 - 0.7962778807 0.7964379787 0.7745416760 - 0.7968716025 0.7970368862 0.7751876712 - 0.7974594831 0.7976313829 0.7758299112 - 0.7980473042 0.7982258797 0.7764720917 - 0.7986351848 0.7988204956 0.7771142721 - 0.7992231250 0.7994149923 0.7777565122 - 0.7998108864 0.8000094891 0.7783988118 - 0.8003988266 0.8006039858 0.7790409923 - 0.8009812832 0.8011956811 0.7796803117 - 0.8015602231 0.8017855287 0.7803177834 - 0.8021391034 0.8023753166 0.7809553146 - 0.8027181029 0.8029651046 0.7815927267 - 0.8032969832 0.8035550117 0.7822301984 - 0.8038759232 0.8041447997 0.7828677297 - 0.8044546247 0.8047344089 0.7835052013 - 0.8050318956 0.8053227067 0.7841426134 - 0.8056092262 0.8059110045 0.7847800851 - 0.8061863780 0.8064993024 0.7854174972 - 0.8067637086 0.8070874810 0.7860549092 - 0.8073409200 0.8076757789 0.7866923213 - 0.8079181910 0.8082640767 0.7873297930 - 0.8084933162 0.8088514805 0.7879632711 - 0.8090668917 0.8094381094 0.7885943055 - 0.8096404076 0.8100247979 0.7892252803 - 0.8102139831 0.8106114864 0.7898563147 - 0.8107876182 0.8111981750 0.7904872894 - 0.8113610744 0.8117849231 0.7911183238 - 0.8119347095 0.8123714924 0.7917492986 - 0.8125054836 0.8129528761 0.7923681736 - 0.8130763173 0.8135340810 0.7929869890 - 0.8136469722 0.8141154051 0.7936058044 - 0.8142176867 0.8146966100 0.7942246199 - 0.8147884011 0.8152778745 0.7948433757 - 0.8153591752 0.8158590794 0.7954621911 - 0.8159289956 0.8164386749 0.7960788012 - 0.8164964914 0.8170142770 0.7966904044 - 0.8170641065 0.8175899982 0.7973020077 - 0.8176316023 0.8181656003 0.7979136109 - 0.8181990981 0.8187412024 0.7985252142 - 0.8187667131 0.8193169236 0.7991368175 - 0.8193342090 0.8198925257 0.7997484207 - 0.8198990226 0.8204675913 0.8003578782 - 0.8204612136 0.8210421801 0.8009656072 - 0.8210235238 0.8216167092 0.8015732765 - 0.8215857744 0.8221912980 0.8021810055 - 0.8221480846 0.8227658868 0.8027886748 - 0.8227102757 0.8233404756 0.8033962846 - 0.8232725859 0.8239151239 0.8040040135 - 0.8238356113 0.8244882822 0.8046082258 - 0.8243988156 0.8250607848 0.8052108884 - 0.8249620795 0.8256334066 0.8058137298 - 0.8255254030 0.8262060285 0.8064163923 - 0.8260886073 0.8267785907 0.8070191145 - 0.8266518712 0.8273512125 0.8076218963 - 0.8272151947 0.8279237747 0.8082246184 - 0.8277786970 0.8284941912 0.8088231087 - 0.8283423185 0.8290640712 0.8094205856 - 0.8289058208 0.8296340108 0.8100181222 - 0.8294693828 0.8302038908 0.8106155992 - 0.8300330043 0.8307737708 0.8112130761 - 0.8305966258 0.8313437104 0.8118106127 - 0.8311601877 0.8319135904 0.8124080896 - 0.8317217827 0.8324813843 0.8130018115 - 0.8322829008 0.8330487013 0.8135946989 - 0.8328440189 0.8336160779 0.8141875863 - 0.8334051967 0.8341833949 0.8147805929 - 0.8339663148 0.8347507119 0.8153734803 - 0.8345274925 0.8353180885 0.8159664273 - 0.8350886106 0.8358854055 0.8165593743 - 0.8356444836 0.8364486098 0.8171482086 - 0.8361988068 0.8370106220 0.8177357912 - 0.8367530704 0.8375725746 0.8183234930 - 0.8373072743 0.8381345868 0.8189110756 - 0.8378615975 0.8386965990 0.8194987178 - 0.8384159207 0.8392586112 0.8200864196 - 0.8389701247 0.8398206234 0.8206740022 - 0.8395189047 0.8403776884 0.8212596774 - 0.8400645256 0.8409320712 0.8218443990 - 0.8406100273 0.8414863944 0.8224290013 - 0.8411555290 0.8420407176 0.8230137229 - 0.8417009711 0.8425949812 0.8235983253 - 0.8422465920 0.8431493044 0.8241829872 - 0.8427920938 0.8437036276 0.8247675896 - 0.8433353901 0.8442538977 0.8253523707 - 0.8438755870 0.8447986245 0.8259375095 - 0.8444157243 0.8453432918 0.8265225291 - 0.8449559212 0.8458880186 0.8271076083 - 0.8454961181 0.8464328051 0.8276926279 - 0.8460361958 0.8469774723 0.8282775879 - 0.8465763927 0.8475221992 0.8288627267 - 0.8471155763 0.8480662107 0.8294473290 - 0.8476483226 0.8486049771 0.8300294876 - 0.8481810093 0.8491439223 0.8306117058 - 0.8487136960 0.8496826887 0.8311939836 - 0.8492463827 0.8502215743 0.8317762017 - 0.8497790098 0.8507604003 0.8323584199 - 0.8503116965 0.8512992859 0.8329406977 - 0.8508443832 0.8518381119 0.8335229158 - 0.8513737917 0.8523707986 0.8341016769 - 0.8519021273 0.8529015183 0.8346794844 - 0.8524304032 0.8534322977 0.8352572918 - 0.8529587984 0.8539630175 0.8358352184 - 0.8534870744 0.8544937968 0.8364130259 - 0.8540154099 0.8550245762 0.8369907737 - 0.8545438051 0.8555552959 0.8375687003 - 0.8550698757 0.8560833931 0.8381444216 - 0.8555914164 0.8566060066 0.8387160897 - 0.8561128974 0.8571285009 0.8392876983 - 0.8566343784 0.8576511145 0.8398594260 - 0.8571557999 0.8581736088 0.8404310942 - 0.8576772809 0.8586962223 0.8410027027 - 0.8581988215 0.8592187166 0.8415743709 - 0.8587203026 0.8597412705 0.8421459794 - 0.8592392802 0.8602582812 0.8427104950 - 0.8597577810 0.8607739806 0.8432731032 - 0.8602762222 0.8612896800 0.8438357711 - 0.8607946038 0.8618053198 0.8443984985 - 0.8613131046 0.8623210192 0.8449612260 - 0.8618314862 0.8628365993 0.8455238938 - 0.8623499274 0.8633522987 0.8460866213 - 0.8628677130 0.8638666272 0.8466476202 - 0.8633821011 0.8643746972 0.8472021222 - 0.8638964891 0.8648828864 0.8477566242 - 0.8644108772 0.8653910756 0.8483110070 - 0.8649253249 0.8658993244 0.8488655090 - 0.8654397130 0.8664075136 0.8494200110 - 0.8659541011 0.8669157028 0.8499745131 - 0.8664684892 0.8674237728 0.8505288959 - 0.8669798970 0.8679283857 0.8510808945 - 0.8674882054 0.8684291840 0.8516303897 - 0.8679965138 0.8689299822 0.8521798849 - 0.8685047030 0.8694307804 0.8527294993 - 0.8690130115 0.8699315786 0.8532789946 - 0.8695213199 0.8704324961 0.8538284898 - 0.8700296283 0.8709332943 0.8543781042 - 0.8705378771 0.8714340925 0.8549275994 - 0.8710486889 0.8719300032 0.8554750085 - 0.8715605140 0.8724238873 0.8560217023 - 0.8720722795 0.8729178905 0.8565683961 - 0.8725841045 0.8734118938 0.8571150899 - 0.8730959892 0.8739058971 0.8576617837 - 0.8736078143 0.8743997812 0.8582084775 - 0.8741195798 0.8748937845 0.8587551713 - 0.8746314049 0.8753877878 0.8593019247 - 0.8751448989 0.8758807182 0.8598421812 - 0.8756586909 0.8763734102 0.8603816032 - 0.8761724830 0.8768661022 0.8609210253 - 0.8766862750 0.8773587942 0.8614603877 - 0.8772001266 0.8778516054 0.8619998097 - 0.8777137995 0.8783442974 0.8625391722 - 0.8782275915 0.8788369894 0.8630785942 - 0.8787413836 0.8793296814 0.8636180162 - 0.8792527914 0.8798214793 0.8641535044 - 0.8797639012 0.8803130984 0.8646888137 - 0.8802750111 0.8808047771 0.8652241230 - 0.8807861209 0.8812963963 0.8657593727 - 0.8812971711 0.8817880154 0.8662946820 - 0.8818082809 0.8822796941 0.8668299913 - 0.8823193908 0.8827713132 0.8673651814 - 0.8828305006 0.8832629919 0.8679004908 - 0.8833349943 0.8837479949 0.8684350252 - 0.8838387132 0.8842322826 0.8689693809 - 0.8843423724 0.8847165108 0.8695037961 - 0.8848459721 0.8852007985 0.8700382113 - 0.8853496909 0.8856850863 0.8705726266 - 0.8858534098 0.8861693144 0.8711069822 - 0.8863570094 0.8866536021 0.8716413975 - 0.8868607283 0.8871377707 0.8721758127 - 0.8873574734 0.8876165152 0.8727089167 - 0.8878520727 0.8880935907 0.8732416034 - 0.8883467913 0.8885707259 0.8737742901 - 0.8888415098 0.8890478015 0.8743069768 - 0.8893362284 0.8895248771 0.8748397231 - 0.8898308873 0.8900020123 0.8753722906 - 0.8903254867 0.8904790878 0.8759049773 - 0.8908202052 0.8909561038 0.8764377236 - 0.8913139105 0.8914325833 0.8769698739 - 0.8918069005 0.8919085860 0.8775017858 - 0.8922998905 0.8923845291 0.8780336976 - 0.8927928805 0.8928604722 0.8785656095 - 0.8932858706 0.8933364749 0.8790975213 - 0.8937789202 0.8938124776 0.8796293736 - 0.8942717910 0.8942884207 0.8801612258 - 0.8947647810 0.8947644234 0.8806930780 - 0.8952565789 0.8952397108 0.8812240958 - 0.8957449198 0.8957130909 0.8817527890 - 0.8962332010 0.8961864710 0.8822814226 - 0.8967214823 0.8966599107 0.8828101158 - 0.8972098231 0.8971332908 0.8833386898 - 0.8976982236 0.8976066709 0.8838673830 - 0.8981865048 0.8980801105 0.8843960762 - 0.8986747861 0.8985534906 0.8849247098 - 0.8991631269 0.8990268707 0.8854534030 - 0.8996483088 0.8994984031 0.8859789968 - 0.9001330137 0.8999695182 0.8865042925 - 0.9006177187 0.9004406929 0.8870295882 - 0.9011024833 0.9009119272 0.8875548244 - 0.9015871882 0.9013829827 0.8880801201 - 0.9020720124 0.9018542171 0.8886054158 - 0.9025567174 0.9023253918 0.8891307116 - 0.9030414224 0.9027965069 0.8896560073 - 0.9035236239 0.9032670856 0.8901805878 - 0.9040020704 0.9037368894 0.8907042146 - 0.9044805169 0.9042065740 0.8912279010 - 0.9049589038 0.9046763182 0.8917515278 - 0.9054374099 0.9051461220 0.8922752142 - 0.9059157968 0.9056158066 0.8927989006 - 0.9063943028 0.9060856104 0.8933225274 - 0.9068726897 0.9065552950 0.8938462138 - 0.9073510766 0.9070249796 0.8943697810 - 0.9078236818 0.9074894190 0.8948928118 - 0.9082950950 0.9079527259 0.8954156041 - 0.9087665081 0.9084160924 0.8959383965 - 0.9092379808 0.9088793993 0.8964611888 - 0.9097093940 0.9093427062 0.8969839811 - 0.9101808071 0.9098060727 0.8975067735 - 0.9106522202 0.9102693796 0.8980296254 - 0.9111235738 0.9107328057 0.8985524178 - 0.9115946293 0.9111946225 0.8990744948 - 0.9120634794 0.9116498828 0.8995931745 - 0.9125325084 0.9121053219 0.9001119137 - 0.9130014181 0.9125605822 0.9006305933 - 0.9134703875 0.9130159020 0.9011492729 - 0.9139392972 0.9134712219 0.9016680717 - 0.9144083261 0.9139264822 0.9021868110 - 0.9148771763 0.9143819213 0.9027054906 - 0.9153460860 0.9148371816 0.9032241702 - 0.9158136249 0.9152905941 0.9037396908 - 0.9162790179 0.9157413840 0.9042512178 - 0.9167444706 0.9161921740 0.9047626257 - 0.9172099233 0.9166430235 0.9052739739 - 0.9176753163 0.9170938134 0.9057853818 - 0.9181408286 0.9175446033 0.9062967896 - 0.9186062217 0.9179953933 0.9068081975 - 0.9190716743 0.9184461832 0.9073196054 - 0.9195371270 0.9188969731 0.9078310132 - 0.9200031161 0.9193475842 0.9083387852 - 0.9204695225 0.9197980165 0.9088441133 - 0.9209359288 0.9202483892 0.9093493223 - 0.9214022756 0.9206988215 0.9098545909 - 0.9218686819 0.9211491942 0.9103597999 - 0.9223350883 0.9215996265 0.9108651280 - 0.9228014946 0.9220499992 0.9113702774 - 0.9232679009 0.9225003719 0.9118756056 - 0.9237343073 0.9229508042 0.9123808146 - 0.9242020249 0.9234021902 0.9128848910 - 0.9246705174 0.9238542914 0.9133883119 - 0.9251390100 0.9243063927 0.9138916731 - 0.9256075025 0.9247584939 0.9143949747 - 0.9260761142 0.9252105951 0.9148983955 - 0.9265446067 0.9256626964 0.9154018164 - 0.9270130992 0.9261147976 0.9159051776 - 0.9274815917 0.9265668988 0.9164085984 - 0.9279500842 0.9270188808 0.9169120193 - 0.9284178019 0.9274703264 0.9174153209 - 0.9288849235 0.9279211760 0.9179183841 - 0.9293519855 0.9283720851 0.9184216261 - 0.9298191071 0.9288229942 0.9189246893 - 0.9302862883 0.9292739034 0.9194278121 - 0.9307534099 0.9297246933 0.9199309945 - 0.9312204719 0.9301756024 0.9204341173 - 0.9316875935 0.9306265116 0.9209372997 - 0.9321547151 0.9310774207 0.9214404225 - 0.9326195121 0.9315286279 0.9219434857 - 0.9330815077 0.9319801927 0.9224464893 - 0.9335433841 0.9324318767 0.9229494929 - 0.9340053797 0.9328835011 0.9234524965 - 0.9344673753 0.9333351254 0.9239555001 - 0.9349293113 0.9337866902 0.9244585037 - 0.9353913069 0.9342383146 0.9249615073 - 0.9358531833 0.9346899986 0.9254645109 - 0.9363151789 0.9351416230 0.9259675145 - 0.9367759228 0.9355927110 0.9264699817 - 0.9372317791 0.9360417128 0.9269704819 - 0.9376876950 0.9364907742 0.9274709225 - 0.9381436706 0.9369398952 0.9279714227 - 0.9385995865 0.9373890162 0.9284719229 - 0.9390555024 0.9378380775 0.9289724231 - 0.9395114183 0.9382871985 0.9294728041 - 0.9399673939 0.9387362003 0.9299733043 - 0.9404233098 0.9391853213 0.9304738045 - 0.9408792257 0.9396343827 0.9309743047 - 0.9413326979 0.9400802255 0.9314733744 - 0.9417858124 0.9405254722 0.9319723248 - 0.9422389865 0.9409707785 0.9324712157 - 0.9426921010 0.9414162040 0.9329702258 - 0.9431452155 0.9418615103 0.9334691167 - 0.9435983896 0.9423068166 0.9339680076 - 0.9440515041 0.9427521229 0.9344670177 - 0.9445046186 0.9431974292 0.9349659085 - 0.9449576735 0.9436427951 0.9354647994 - 0.9454091787 0.9440872073 0.9359622002 - 0.9458584785 0.9445304871 0.9364575744 - 0.9463077784 0.9449738860 0.9369530082 - 0.9467570186 0.9454172254 0.9374483824 - 0.9472063184 0.9458606243 0.9379438162 - 0.9476556182 0.9463039041 0.9384390712 - 0.9481049180 0.9467471838 0.9389345050 - 0.9485542178 0.9471905828 0.9394298792 - 0.9490035176 0.9476339221 0.9399253130 - 0.9494526982 0.9480772018 0.9404206872 - 0.9498931766 0.9485139847 0.9409106970 - 0.9503325224 0.9489501119 0.9413999915 - 0.9507718086 0.9493861198 0.9418894053 - 0.9512112141 0.9498221874 0.9423786998 - 0.9516505003 0.9502581954 0.9428679943 - 0.9520899057 0.9506943226 0.9433574080 - 0.9525291920 0.9511302710 0.9438467026 - 0.9529684782 0.9515663981 0.9443361163 - 0.9534078836 0.9520024061 0.9448254108 - 0.9538437128 0.9524362087 0.9453141093 - 0.9542688131 0.9528629184 0.9458007216 - 0.9546939135 0.9532896280 0.9462872744 - 0.9551190138 0.9537162185 0.9467738867 - 0.9555441141 0.9541429281 0.9472606182 - 0.9559692144 0.9545695782 0.9477471709 - 0.9563943148 0.9549962878 0.9482337832 - 0.9568194747 0.9554229975 0.9487203956 - 0.9572445750 0.9558497071 0.9492071271 - 0.9576696754 0.9562764168 0.9496936798 - 0.9580900073 0.9567002058 0.9501780868 - 0.9585055113 0.9571210742 0.9506604075 - 0.9589208961 0.9575420022 0.9511427283 - 0.9593364000 0.9579628110 0.9516249895 - 0.9597519040 0.9583836794 0.9521073103 - 0.9601672888 0.9588046074 0.9525895715 - 0.9605827928 0.9592254758 0.9530717731 - 0.9609982967 0.9596462846 0.9535540938 - 0.9614136815 0.9600672126 0.9540364146 - 0.9618291855 0.9604880810 0.9545186758 - 0.9622408748 0.9609053731 0.9549967051 - 0.9626505971 0.9613206983 0.9554721713 - 0.9630602002 0.9617360234 0.9559476972 - 0.9634698033 0.9621512294 0.9564232826 - 0.9638794065 0.9625664949 0.9568988085 - 0.9642891288 0.9629818201 0.9573743939 - 0.9646986723 0.9633970857 0.9578499198 - 0.9651082754 0.9638124108 0.9583255053 - 0.9655178785 0.9642276168 0.9588009715 - 0.9659274817 0.9646428823 0.9592764974 - 0.9663329720 0.9650545716 0.9597474933 - 0.9667364955 0.9654644728 0.9602164030 - 0.9671400189 0.9658744931 0.9606851935 - 0.9675434828 0.9662843943 0.9611539841 - 0.9679470062 0.9666942954 0.9616228938 - 0.9683504105 0.9671043158 0.9620916843 - 0.9687538743 0.9675142169 0.9625605941 - 0.9691573977 0.9679241776 0.9630293846 - 0.9695609212 0.9683340788 0.9634981751 - 0.9699643850 0.9687440991 0.9639670849 - 0.9703654051 0.9691501260 0.9644325972 - 0.9707646966 0.9695534706 0.9648959041 - 0.9711639881 0.9699568748 0.9653592706 - 0.9715632796 0.9703602791 0.9658225775 - 0.9719626904 0.9707638025 0.9662858844 - 0.9723619819 0.9711672068 0.9667493105 - 0.9727612734 0.9715706110 0.9672126174 - 0.9731606841 0.9719740748 0.9676759839 - 0.9735599756 0.9723774791 0.9681392908 - 0.9739593267 0.9727808833 0.9686025977 - 0.9743570089 0.9731823802 0.9690620899 - 0.9747526050 0.9735813141 0.9695165157 - 0.9751480818 0.9739801884 0.9699708223 - 0.9755436778 0.9743791223 0.9704251289 - 0.9759392142 0.9747779965 0.9708794951 - 0.9763348103 0.9751768708 0.9713338017 - 0.9767302871 0.9755758047 0.9717881083 - 0.9771258235 0.9759746790 0.9722424746 - 0.9775214195 0.9763736725 0.9726967812 - 0.9779168963 0.9767726064 0.9731512070 - 0.9783114195 0.9771707058 0.9736040235 - 0.9786989093 0.9775642753 0.9740474820 - 0.9790863991 0.9779577851 0.9744911194 - 0.9794738889 0.9783512950 0.9749345779 - 0.9798614979 0.9787448049 0.9753780961 - 0.9802489877 0.9791383147 0.9758216143 - 0.9806364775 0.9795318246 0.9762650728 - 0.9810240269 0.9799252748 0.9767085910 - 0.9814115167 0.9803189039 0.9771521091 - 0.9817990065 0.9807124138 0.9775956273 - 0.9821864963 0.9811059237 0.9780392051 - 0.9825655818 0.9814931750 0.9784761071 - 0.9829416275 0.9818782210 0.9789105058 - 0.9833174944 0.9822633266 0.9793450236 - 0.9836934805 0.9826483130 0.9797794819 - 0.9840694070 0.9830332994 0.9802138805 - 0.9844453931 0.9834184051 0.9806483984 - 0.9848213196 0.9838033915 0.9810829163 - 0.9851973057 0.9841883779 0.9815173149 - 0.9855731726 0.9845734239 0.9819517732 - 0.9859492183 0.9849585295 0.9823862910 - 0.9863222241 0.9853414297 0.9828186035 - 0.9866843820 0.9857168794 0.9832429886 - 0.9870467186 0.9860923290 0.9836674929 - 0.9874088764 0.9864677191 0.9840919971 - 0.9877712131 0.9868432283 0.9845165014 - 0.9881333709 0.9872186184 0.9849410057 - 0.9884957075 0.9875940084 0.9853653908 - 0.9888579249 0.9879695177 0.9857898951 - 0.9892202020 0.9883449078 0.9862143993 - 0.9895824194 0.9887202978 0.9866389036 - 0.9899446964 0.9890956879 0.9870632887 - 0.9902951717 0.9894627929 0.9874807000 - 0.9906374216 0.9898239970 0.9878932238 - 0.9909796119 0.9901852012 0.9883056283 - 0.9913218021 0.9905464053 0.9887179732 - 0.9916639924 0.9909076095 0.9891303778 - 0.9920061827 0.9912688136 0.9895427823 - 0.9923483729 0.9916298985 0.9899551868 - 0.9926906228 0.9919911027 0.9903675914 - 0.9930328131 0.9923523068 0.9907801151 - 0.9933750033 0.9927135110 0.9911925197 - 0.9937171936 0.9930747151 0.9916049242 - 0.9940317869 0.9934129715 0.9920030236 - 0.9943410754 0.9937469959 0.9923985004 - 0.9946504831 0.9940810204 0.9927939177 - 0.9949597716 0.9944151044 0.9931892753 - 0.9952691793 0.9947491288 0.9935848117 - 0.9955785275 0.9950830936 0.9939802289 - 0.9958878160 0.9954171181 0.9943757057 - 0.9961972237 0.9957510829 0.9947711229 - 0.9965065122 0.9960851073 0.9951664805 - 0.9968159199 0.9964190722 0.9955620170 - 0.9971252084 0.9967530966 0.9959573746 - 0.9973875284 0.9970490932 0.9963253736 - 0.9976487160 0.9973441958 0.9966928959 - 0.9979100227 0.9976391792 0.9970602989 - 0.9981712103 0.9979342818 0.9974278212 - 0.9984325171 0.9982293844 0.9977952838 - 0.9986937046 0.9985244870 0.9981626868 - 0.9989550114 0.9988195896 0.9985302091 - 0.9992161989 0.9991146922 0.9988976121 - 0.9994775057 0.9994097948 0.9992650747 - 0.9997386932 0.9997048974 0.9996324778 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0109453099 0.0095550315 0.0076129269 + 0.0205393303 0.0180988703 0.0146826003 + 0.0290690009 0.0255930703 0.0210563708 + 0.0365057997 0.0324602798 0.0267342404 + 0.0431397706 0.0389092602 0.0319225714 + 0.0493469499 0.0448722206 0.0368033797 + 0.0553121008 0.0504292287 0.0415878706 + 0.0610060990 0.0556044616 0.0463291109 + 0.0664596632 0.0604789183 0.0508504696 + 0.0717297420 0.0651700869 0.0551753603 + 0.0767887011 0.0697173625 0.0594077297 + 0.0815975219 0.0740136728 0.0635364205 + 0.0862075612 0.0781842917 0.0675580502 + 0.0907116681 0.0822909772 0.0713816136 + 0.0950955972 0.0863237828 0.0743237808 + 0.0993374512 0.0902062133 0.0773941725 + 0.1034374982 0.0940194428 0.0811215192 + 0.1073296964 0.0977483913 0.0852948874 + 0.1110906973 0.1014209017 0.0893627778 + 0.1147643998 0.1050138026 0.0926783979 + 0.1184094027 0.1085262001 0.0958950967 + 0.1220393032 0.1120131016 0.0990428701 + 0.1255874038 0.1154666021 0.1021654978 + 0.1290600002 0.1188692003 0.1052715033 + 0.1324646026 0.1222466975 0.1083573997 + 0.1358134001 0.1256064028 0.1113879010 + 0.1391503960 0.1289401054 0.1143847033 + 0.1424382031 0.1322277933 0.1173340008 + 0.1457035989 0.1354801953 0.1201964989 + 0.1489387006 0.1386919022 0.1230240986 + 0.1521472931 0.1418765932 0.1258170009 + 0.1553370059 0.1450258046 0.1285797060 + 0.1584853977 0.1481391937 0.1313088983 + 0.1615826935 0.1512455940 0.1340118051 + 0.1646398008 0.1543205976 0.1366993934 + 0.1676366031 0.1573432982 0.1393682063 + 0.1705628037 0.1603267044 0.1420211941 + 0.1734506935 0.1632660031 0.1446440071 + 0.1762887985 0.1661476046 0.1472647935 + 0.1791007072 0.1689998060 0.1498612016 + 0.1818982959 0.1718225926 0.1524337977 + 0.1846773028 0.1746128052 0.1549834013 + 0.1874344051 0.1773594022 0.1575088948 + 0.1901603937 0.1800664961 0.1600168049 + 0.1928523034 0.1827678978 0.1625131071 + 0.1955167055 0.1854266971 0.1649871022 + 0.1981182992 0.1880469024 0.1674250066 + 0.2006929070 0.1906183958 0.1698420942 + 0.2032330930 0.1931630969 0.1722377986 + 0.2057420015 0.1956868023 0.1746356040 + 0.2082346976 0.1981967986 0.1770364046 + 0.2106983960 0.2006848007 0.1794321984 + 0.2131627053 0.2031618059 0.1818124950 + 0.2156358063 0.2056335062 0.1841809005 + 0.2180998027 0.2080969065 0.1865282953 + 0.2205528021 0.2105540931 0.1888633072 + 0.2229813933 0.2129819989 0.1911772937 + 0.2253869027 0.2154051960 0.1934747994 + 0.2277887017 0.2178180069 0.1957782060 + 0.2301833928 0.2201987058 0.1980752051 + 0.2325472981 0.2225521058 0.2003640980 + 0.2348767966 0.2248626053 0.2026430070 + 0.2371928990 0.2271340042 0.2049131989 + 0.2394970059 0.2293829024 0.2071806937 + 0.2417781949 0.2316153049 0.2094416022 + 0.2440516949 0.2338470966 0.2117109001 + 0.2463241965 0.2360679954 0.2139746994 + 0.2485778928 0.2382649034 0.2162466943 + 0.2508257926 0.2404524982 0.2185126990 + 0.2530736923 0.2426279038 0.2207739949 + 0.2553015947 0.2447896004 0.2230270952 + 0.2575267851 0.2469552010 0.2252774984 + 0.2597568035 0.2491303980 0.2275131047 + 0.2619835138 0.2513048947 0.2297354937 + 0.2641938031 0.2534686029 0.2319502980 + 0.2663998902 0.2556251884 0.2341517955 + 0.2686052918 0.2577697039 0.2363480031 + 0.2708081007 0.2599022090 0.2385403067 + 0.2730022967 0.2620306909 0.2407180965 + 0.2751887143 0.2641496956 0.2428912967 + 0.2773590982 0.2662425041 0.2450589985 + 0.2795172036 0.2683148086 0.2472172976 + 0.2816726863 0.2703753114 0.2493720949 + 0.2838136852 0.2724193931 0.2515229881 + 0.2859255075 0.2744517922 0.2536627948 + 0.2880168855 0.2764849067 0.2557986081 + 0.2901124954 0.2785145044 0.2579297125 + 0.2922055125 0.2805334926 0.2600534856 + 0.2942928076 0.2825405896 0.2621644139 + 0.2963638008 0.2845480144 0.2642523944 + 0.2984221876 0.2865386903 0.2663209140 + 0.3004606962 0.2885077894 0.2683751881 + 0.3024761081 0.2904664874 0.2704192996 + 0.3044942915 0.2924236953 0.2724545002 + 0.3065181971 0.2943876982 0.2744720876 + 0.3085376918 0.2963528931 0.2764638066 + 0.3105514944 0.2983131111 0.2784410119 + 0.3125489056 0.3002620041 0.2804194987 + 0.3145506084 0.3021939993 0.2823942006 + 0.3165509105 0.3041042089 0.2843523026 + 0.3185442090 0.3060002923 0.2863051891 + 0.3205361962 0.3078896999 0.2882539034 + 0.3225252032 0.3097808063 0.2901889980 + 0.3245142996 0.3116734922 0.2921113074 + 0.3265058100 0.3135590851 0.2940278947 + 0.3284947872 0.3154321909 0.2959429026 + 0.3304775059 0.3173008859 0.2978521883 + 0.3324612975 0.3191635907 0.2997469008 + 0.3344404995 0.3210156858 0.3016290069 + 0.3364037871 0.3228532076 0.3035013080 + 0.3383480906 0.3246806860 0.3053658903 + 0.3402774930 0.3265039027 0.3072257042 + 0.3422003984 0.3283264935 0.3090836108 + 0.3441283107 0.3301435113 0.3109402061 + 0.3460584879 0.3319503963 0.3127999902 + 0.3479807973 0.3337503970 0.3146592081 + 0.3498908877 0.3355464935 0.3165098131 + 0.3517873883 0.3373399973 0.3183467984 + 0.3536739051 0.3391256034 0.3201710880 + 0.3555468917 0.3408975899 0.3219870925 + 0.3574047089 0.3426569104 0.3237960935 + 0.3592503965 0.3444055915 0.3255974948 + 0.3610936105 0.3461483121 0.3273890913 + 0.3629257977 0.3478848040 0.3291662931 + 0.3647486866 0.3496142030 0.3309336901 + 0.3665607870 0.3513360918 0.3326914012 + 0.3683531880 0.3530487120 0.3344373107 + 0.3701289892 0.3547559083 0.3361791968 + 0.3719004095 0.3564561903 0.3379172087 + 0.3736707866 0.3581486046 0.3396506011 + 0.3754306138 0.3598242104 0.3413727880 + 0.3771795928 0.3614909947 0.3430913985 + 0.3789179921 0.3631505966 0.3448042870 + 0.3806397915 0.3648028076 0.3465045989 + 0.3823508024 0.3664542139 0.3481892049 + 0.3840552866 0.3680998981 0.3498643935 + 0.3857525885 0.3697325885 0.3515287936 + 0.3874309957 0.3713547885 0.3531869948 + 0.3890962005 0.3729732037 0.3548409939 + 0.3907422125 0.3745886981 0.3564892113 + 0.3923803866 0.3761973083 0.3581174910 + 0.3940139115 0.3778007925 0.3597348928 + 0.3956401050 0.3793950975 0.3613364995 + 0.3972538114 0.3809765875 0.3629319072 + 0.3988597989 0.3825520873 0.3645235002 + 0.4004507959 0.3841212988 0.3661076128 + 0.4020316899 0.3856911063 0.3676817119 + 0.4036057889 0.3872612119 0.3692494035 + 0.4051836133 0.3888235986 0.3708040118 + 0.4067601860 0.3903797865 0.3723560870 + 0.4083349109 0.3919270039 0.3739052117 + 0.4099012911 0.3934660852 0.3754433095 + 0.4114632905 0.3950020969 0.3769756854 + 0.4130123854 0.3965331018 0.3784922063 + 0.4145576060 0.3980642855 0.3800047934 + 0.4160979986 0.3995954990 0.3815135956 + 0.4176265895 0.4011205137 0.3830150068 + 0.4191510081 0.4026427865 0.3845132887 + 0.4206664860 0.4041537941 0.3859997094 + 0.4221807122 0.4056634009 0.3874785900 + 0.4236926138 0.4071705043 0.3889462948 + 0.4251953065 0.4086725116 0.3904061019 + 0.4266924858 0.4101715982 0.3918620050 + 0.4281755090 0.4116598964 0.3933145106 + 0.4296549857 0.4131458998 0.3947649896 + 0.4311212897 0.4146251082 0.3962041140 + 0.4325863123 0.4161024094 0.3976391852 + 0.4340479076 0.4175755084 0.3990663886 + 0.4355044961 0.4190461934 0.4004910886 + 0.4369558096 0.4205146134 0.4019137025 + 0.4384000897 0.4219822884 0.4033333063 + 0.4398407042 0.4234496057 0.4047515988 + 0.4412648976 0.4249095023 0.4061658084 + 0.4426848888 0.4263676107 0.4075790942 + 0.4440922141 0.4278148115 0.4089758992 + 0.4454984963 0.4292607903 0.4103710055 + 0.4469017088 0.4306946993 0.4117515087 + 0.4483037889 0.4321266115 0.4131309986 + 0.4496976137 0.4335466921 0.4145058095 + 0.4510863125 0.4349648952 0.4158783853 + 0.4524526000 0.4363755882 0.4172433019 + 0.4538140893 0.4377841055 0.4186027944 + 0.4551594853 0.4391863942 0.4199478924 + 0.4565044940 0.4405857027 0.4212876856 + 0.4578481019 0.4419767857 0.4226151109 + 0.4591886997 0.4433653951 0.4239403903 + 0.4605211020 0.4447469115 0.4252606928 + 0.4618496001 0.4461270869 0.4265792966 + 0.4631653130 0.4475035965 0.4278936982 + 0.4644770026 0.4488789141 0.4292060137 + 0.4657723904 0.4502497017 0.4305118024 + 0.4670655131 0.4516187906 0.4318155944 + 0.4683431089 0.4529778957 0.4331100881 + 0.4696202874 0.4543359876 0.4344033897 + 0.4708895981 0.4556787014 0.4356841147 + 0.4721587896 0.4570212960 0.4369646907 + 0.4734266996 0.4583550096 0.4382334054 + 0.4746944010 0.4596883059 0.4395017922 + 0.4759570956 0.4610117078 0.4407595098 + 0.4772185087 0.4623329043 0.4420154095 + 0.4784702957 0.4636422098 0.4432603121 + 0.4797168970 0.4649454951 0.4445005059 + 0.4809587896 0.4662468135 0.4457359910 + 0.4821954966 0.4675458074 0.4469672143 + 0.4834313095 0.4688419104 0.4481967986 + 0.4846647084 0.4701308012 0.4494228959 + 0.4858981073 0.4714190066 0.4506483972 + 0.4871313870 0.4726983011 0.4518673122 + 0.4883646965 0.4739777148 0.4530862868 + 0.4895890951 0.4752514064 0.4542964995 + 0.4908111989 0.4765237868 0.4555051029 + 0.4920256138 0.4777913094 0.4567083120 + 0.4932329953 0.4790548086 0.4579074085 + 0.4944368899 0.4803156853 0.4591037929 + 0.4956291020 0.4815686941 0.4602927864 + 0.4968214035 0.4828216136 0.4614818096 + 0.4980075061 0.4840664864 0.4626607001 + 0.4991930127 0.4853107929 0.4638391137 + 0.5003730059 0.4865506887 0.4650115073 + 0.5015485883 0.4877873063 0.4661796987 + 0.5027217269 0.4890219867 0.4673461020 + 0.5038836002 0.4902490973 0.4685058892 + 0.5050454736 0.4914762080 0.4696657956 + 0.5061964989 0.4927015007 0.4708235860 + 0.5073444247 0.4939264953 0.4719808996 + 0.5084874034 0.4951480031 0.4731355011 + 0.5096216798 0.4963639975 0.4742859006 + 0.5107560754 0.4975799024 0.4754363894 + 0.5118848085 0.4987801015 0.4765751958 + 0.5130128860 0.4999789000 0.4777132869 + 0.5141406059 0.5011754036 0.4788497090 + 0.5152677298 0.5023689270 0.4799841046 + 0.5163947940 0.5035623908 0.4811184108 + 0.5175107121 0.5047491193 0.4822545052 + 0.5186256170 0.5059353709 0.4833906889 + 0.5197319984 0.5071169734 0.4845212996 + 0.5208250284 0.5082917213 0.4856444001 + 0.5219178796 0.5094662905 0.4867675006 + 0.5230032802 0.5106360912 0.4878782928 + 0.5240870118 0.5118049979 0.4889872968 + 0.5251713991 0.5129719973 0.4900952876 + 0.5262575746 0.5141333938 0.4912008941 + 0.5273438096 0.5152949095 0.4923065007 + 0.5284274220 0.5164546967 0.4934087098 + 0.5295094252 0.5176134706 0.4945091903 + 0.5305913091 0.5187724233 0.4956094921 + 0.5316656828 0.5199283957 0.4966976941 + 0.5327398181 0.5210844874 0.4977858961 + 0.5338104963 0.5222364068 0.4988707900 + 0.5348731279 0.5233796239 0.4999495149 + 0.5359356999 0.5245227814 0.5010282993 + 0.5369876027 0.5256589055 0.5021041036 + 0.5380324721 0.5267904997 0.5031781197 + 0.5390774012 0.5279220939 0.5042521954 + 0.5401139259 0.5290479064 0.5053207278 + 0.5411491990 0.5301730037 0.5063884854 + 0.5421838760 0.5312972069 0.5074563026 + 0.5432150960 0.5324152112 0.5085229278 + 0.5442463160 0.5335332751 0.5095896125 + 0.5452744961 0.5346456170 0.5106530786 + 0.5462962985 0.5357465148 0.5117110014 + 0.5473181009 0.5368474126 0.5127688050 + 0.5483338237 0.5379434228 0.5138180852 + 0.5493434072 0.5390350223 0.5148600936 + 0.5503531098 0.5401265025 0.5159021020 + 0.5513597131 0.5412139297 0.5169410706 + 0.5523648262 0.5422992110 0.5179787278 + 0.5533698201 0.5433844924 0.5190163255 + 0.5543758273 0.5444642901 0.5200524926 + 0.5553820729 0.5455427170 0.5210883021 + 0.5563883185 0.5466210246 0.5221241713 + 0.5573911071 0.5476927161 0.5231577754 + 0.5583932996 0.5487633944 0.5241910815 + 0.5593954921 0.5498341918 0.5252243876 + 0.5603914857 0.5508939028 0.5262517929 + 0.5613868237 0.5519527793 0.5272789001 + 0.5623821020 0.5530115962 0.5283061266 + 0.5633684993 0.5540567040 0.5293180943 + 0.5643541813 0.5551009178 0.5303297043 + 0.5653399229 0.5561451912 0.5313413143 + 0.5663121939 0.5571808219 0.5323446989 + 0.5672833920 0.5582157969 0.5333477259 + 0.5682544708 0.5592507124 0.5343508124 + 0.5692139864 0.5602828264 0.5353485942 + 0.5701718926 0.5613144040 0.5363458991 + 0.5711297989 0.5623461008 0.5373433232 + 0.5720816255 0.5633695126 0.5383352041 + 0.5730317831 0.5643910170 0.5393260717 + 0.5739821196 0.5654125810 0.5403169990 + 0.5749322176 0.5664225817 0.5413028002 + 0.5758821964 0.5674279928 0.5422866940 + 0.5768321753 0.5684334040 0.5432707071 + 0.5777807832 0.5694364905 0.5442500114 + 0.5787283182 0.5704377294 0.5452260971 + 0.5796757936 0.5714389086 0.5462023020 + 0.5806186795 0.5724375248 0.5471749902 + 0.5815541744 0.5734320879 0.5481427908 + 0.5824897289 0.5744267106 0.5491107106 + 0.5834230781 0.5754204988 0.5500766039 + 0.5843461156 0.5764102936 0.5510352850 + 0.5852690935 0.5774000883 0.5519940257 + 0.5861920714 0.5783898830 0.5529527068 + 0.5871120095 0.5793784857 0.5539078116 + 0.5880315900 0.5803670287 0.5548627973 + 0.5889511108 0.5813555121 0.5558177829 + 0.5898649096 0.5823364258 0.5567662120 + 0.5907754898 0.5833132267 0.5577116013 + 0.5916861296 0.5842900872 0.5586568713 + 0.5925920010 0.5852631927 0.5595967174 + 0.5934880972 0.5862287879 0.5605269074 + 0.5943841934 0.5871945024 0.5614570975 + 0.5952801704 0.5881602168 0.5623872280 + 0.5961638093 0.5891243815 0.5633127093 + 0.5970469713 0.5900886059 0.5642381907 + 0.5979301929 0.5910527110 0.5651637912 + 0.5988095999 0.5920161009 0.5660883784 + 0.5996860862 0.5929788947 0.5670126081 + 0.6005626917 0.5939416885 0.5679367781 + 0.6014385223 0.5949037075 0.5688589215 + 0.6023102999 0.5958616734 0.5697726011 + 0.6031820774 0.5968198180 0.5706862211 + 0.6040539145 0.5977777839 0.5715999007 + 0.6049264073 0.5987269282 0.5725020766 + 0.6057993174 0.5996723175 0.5734000802 + 0.6066721082 0.6006177068 0.5742980838 + 0.6075447202 0.6015614271 0.5751948953 + 0.6084157825 0.6024988294 0.5760878921 + 0.6092870235 0.6034362912 0.5769808888 + 0.6101580858 0.6043736935 0.5778738856 + 0.6110234261 0.6053066254 0.5787655711 + 0.6118857265 0.6062374115 0.5796566010 + 0.6127480268 0.6071680784 0.5805476904 + 0.6136096716 0.6080988050 0.5814384818 + 0.6144647002 0.6090288162 0.5823276043 + 0.6153196096 0.6099588275 0.5832166076 + 0.6161745191 0.6108887196 0.5841056705 + 0.6170253158 0.6118140221 0.5849937797 + 0.6178714037 0.6127341986 0.5858809948 + 0.6187174916 0.6136543751 0.5867682099 + 0.6195635796 0.6145746112 0.5876554251 + 0.6203994155 0.6154835820 0.5885332227 + 0.6212329268 0.6163904071 0.5894092917 + 0.6220663786 0.6172971725 0.5902854204 + 0.6228989959 0.6182032228 0.5911598206 + 0.6237251759 0.6191043854 0.5920251012 + 0.6245512962 0.6200054884 0.5928903818 + 0.6253774762 0.6209065914 0.5937557220 + 0.6262025833 0.6218063831 0.5946201086 + 0.6270262003 0.6227043867 0.5954833031 + 0.6278496981 0.6236023903 0.5963464975 + 0.6286731958 0.6245002747 0.5972098112 + 0.6294925809 0.6253930926 0.5980716944 + 0.6303092837 0.6262826920 0.5989329815 + 0.6311259866 0.6271722913 0.5997943282 + 0.6319425702 0.6280620098 0.6006556153 + 0.6327558756 0.6289452910 0.6015126705 + 0.6335682273 0.6298272014 0.6023688912 + 0.6343805790 0.6307089925 0.6032251716 + 0.6351928711 0.6315909028 0.6040813923 + 0.6360028982 0.6324610710 0.6049261093 + 0.6368128061 0.6333307028 0.6057704091 + 0.6376225948 0.6342003942 0.6066148281 + 0.6384323835 0.6350694895 0.6074587703 + 0.6392394900 0.6359295249 0.6082984209 + 0.6400464773 0.6367895007 0.6091380715 + 0.6408535838 0.6376495957 0.6099777222 + 0.6416608095 0.6385089159 0.6108170748 + 0.6424685717 0.6393631101 0.6116548777 + 0.6432763934 0.6402173042 0.6124926805 + 0.6440842748 0.6410714984 0.6133304834 + 0.6448920965 0.6419250965 0.6141679883 + 0.6456990838 0.6427741051 0.6150038242 + 0.6465060711 0.6436231136 0.6158394814 + 0.6473131776 0.6444721222 0.6166753173 + 0.6481201053 0.6453201771 0.6175107956 + 0.6489260197 0.6461594105 0.6183446050 + 0.6497318745 0.6469987035 0.6191784739 + 0.6505379081 0.6478378773 0.6200122833 + 0.6513438821 0.6486768723 0.6208459735 + 0.6521543264 0.6495088935 0.6216778159 + 0.6529647112 0.6503409147 0.6225095987 + 0.6537752151 0.6511728764 0.6233413219 + 0.6545855999 0.6520048976 0.6241731048 + 0.6553974152 0.6528308988 0.6250030994 + 0.6562091708 0.6536563039 0.6258329153 + 0.6570211053 0.6544817090 0.6266627908 + 0.6578329206 0.6553071141 0.6274926066 + 0.6586399078 0.6561266780 0.6283208728 + 0.6594451070 0.6569445133 0.6291487813 + 0.6602504253 0.6577622890 0.6299766898 + 0.6610556841 0.6585801244 0.6308044791 + 0.6618577242 0.6593968868 0.6316313744 + 0.6626570225 0.6602129936 0.6324573755 + 0.6634563208 0.6610292196 0.6332834959 + 0.6642554998 0.6618453264 0.6341096163 + 0.6650528908 0.6626601815 0.6349341869 + 0.6658458114 0.6634724140 0.6357557178 + 0.6666386127 0.6642845869 0.6365773082 + 0.6674314737 0.6650968194 0.6373988986 + 0.6682242751 0.6659088731 0.6382200718 + 0.6690135002 0.6667075753 0.6390303969 + 0.6698027849 0.6675063968 0.6398407221 + 0.6705920100 0.6683050990 0.6406509876 + 0.6713812947 0.6691039205 0.6414613128 + 0.6721655726 0.6698979139 0.6422684193 + 0.6729472876 0.6706895828 0.6430739760 + 0.6737290025 0.6714811921 0.6438794732 + 0.6745107174 0.6722728014 0.6446850896 + 0.6752899885 0.6730641127 0.6454901099 + 0.6760625243 0.6738544703 0.6462938786 + 0.6768351197 0.6746450067 0.6470977068 + 0.6776077151 0.6754354239 0.6479014158 + 0.6783801913 0.6762257814 0.6487051845 + 0.6791486740 0.6770142913 0.6495074034 + 0.6799163222 0.6778023839 0.6503092051 + 0.6806839705 0.6785904765 0.6511111259 + 0.6814516187 0.6793786287 0.6519129872 + 0.6822186708 0.6801651120 0.6527122855 + 0.6829848289 0.6809486747 0.6535074115 + 0.6837509274 0.6817322969 0.6543024778 + 0.6845170856 0.6825159192 0.6550976038 + 0.6852831841 0.6832996011 0.6558926105 + 0.6860461235 0.6840777993 0.6566833258 + 0.6868082881 0.6848548055 0.6574732065 + 0.6875705123 0.6856318712 0.6582630873 + 0.6883326769 0.6864088774 0.6590530276 + 0.6890928745 0.6871842146 0.6598421931 + 0.6898465157 0.6879535913 0.6606296897 + 0.6905999780 0.6887230277 0.6614171863 + 0.6913534999 0.6894924045 0.6622046828 + 0.6921070814 0.6902617812 0.6629921794 + 0.6928570271 0.6910268068 0.6637781858 + 0.6936045885 0.6917887926 0.6645631790 + 0.6943520904 0.6925508976 0.6653482914 + 0.6950995922 0.6933128834 0.6661332846 + 0.6958470941 0.6940749884 0.6669183969 + 0.6965919733 0.6948326826 0.6677011847 + 0.6973364949 0.6955900192 0.6684839129 + 0.6980810761 0.6963472962 0.6692665815 + 0.6988255978 0.6971045136 0.6700494289 + 0.6995682716 0.6978610754 0.6708310843 + 0.7003021240 0.6986145973 0.6716089249 + 0.7010359168 0.6993680000 0.6723867059 + 0.7017697096 0.7001215219 0.6731644869 + 0.7025035024 0.7008749247 0.6739423871 + 0.7032322884 0.7016248107 0.6747155190 + 0.7039532065 0.7023693919 0.6754820943 + 0.7046741843 0.7031139731 0.6762487292 + 0.7053951025 0.7038586140 0.6770153046 + 0.7061160207 0.7046031952 0.6777818799 + 0.7068319917 0.7053433061 0.6785455942 + 0.7075439095 0.7060800195 0.6793071032 + 0.7082558870 0.7068166137 0.6800686717 + 0.7089678049 0.7075533271 0.6808302999 + 0.7096797824 0.7082899213 0.6815918088 + 0.7103822827 0.7090213895 0.6823518276 + 0.7110798955 0.7097504139 0.6831110120 + 0.7117773890 0.7104794979 0.6838701963 + 0.7124750018 0.7112085223 0.6846293807 + 0.7131726146 0.7119374871 0.6853885055 + 0.7138615251 0.7126597166 0.6861459017 + 0.7145469785 0.7133795023 0.6869027019 + 0.7152324915 0.7140992880 0.6876593828 + 0.7159180045 0.7148190141 0.6884161830 + 0.7166035175 0.7155387998 0.6891729236 + 0.7172850966 0.7162548900 0.6899223924 + 0.7179651856 0.7169696093 0.6906694770 + 0.7186452150 0.7176843286 0.6914165020 + 0.7193253040 0.7183991075 0.6921635866 + 0.7200053930 0.7191138268 0.6929106116 + 0.7206829190 0.7198268175 0.6936489940 + 0.7213590145 0.7205389738 0.6943836808 + 0.7220352292 0.7212511897 0.6951184273 + 0.7227113843 0.7219635248 0.6958531141 + 0.7233875990 0.7226756811 0.6965876818 + 0.7240635753 0.7233844995 0.6973186731 + 0.7247393727 0.7240908146 0.6980472207 + 0.7254152894 0.7247970104 0.6987757087 + 0.7260912061 0.7255033255 0.6995041966 + 0.7267671227 0.7262095809 0.7002326846 + 0.7274426818 0.7269139290 0.7009603977 + 0.7281178832 0.7276154757 0.7016872168 + 0.7287930846 0.7283170223 0.7024139762 + 0.7294682860 0.7290186286 0.7031407952 + 0.7301434875 0.7297201157 0.7038676143 + 0.7308180928 0.7304204106 0.7045934200 + 0.7314903736 0.7311158776 0.7053160071 + 0.7321627140 0.7318114042 0.7060384750 + 0.7328349948 0.7325068116 0.7067610025 + 0.7335072160 0.7332022786 0.7074834704 + 0.7341794968 0.7338978052 0.7082059979 + 0.7348482013 0.7345873117 0.7089205980 + 0.7355166078 0.7352764010 0.7096347213 + 0.7361850142 0.7359654903 0.7103487849 + 0.7368534207 0.7366545796 0.7110629082 + 0.7375218272 0.7373437285 0.7117769718 + 0.7381889820 0.7380310297 0.7124879956 + 0.7388554215 0.7387169003 0.7131969929 + 0.7395219207 0.7394028902 0.7139061093 + 0.7401883006 0.7400888205 0.7146151066 + 0.7408546805 0.7407748103 0.7153242230 + 0.7415205836 0.7414584756 0.7160325050 + 0.7421839833 0.7421336770 0.7167384028 + 0.7428473830 0.7428088784 0.7174441814 + 0.7435107827 0.7434840798 0.7181500793 + 0.7441741824 0.7441592813 0.7188559771 + 0.7448375821 0.7448344827 0.7195618749 + 0.7454971075 0.7455040216 0.7202640772 + 0.7461552024 0.7461715937 0.7209653258 + 0.7468132973 0.7468392849 0.7216665149 + 0.7474713922 0.7475069761 0.7223677039 + 0.7481294870 0.7481747270 0.7230688930 + 0.7487857938 0.7488409877 0.7237700820 + 0.7494363785 0.7495033741 0.7244712114 + 0.7500869036 0.7501658797 0.7251722813 + 0.7507374883 0.7508283854 0.7258734107 + 0.7513880730 0.7514907718 0.7265745997 + 0.7520385981 0.7521532774 0.7272757292 + 0.7526834011 0.7528085709 0.7279723287 + 0.7533255816 0.7534605861 0.7286670804 + 0.7539677024 0.7541127205 0.7293617725 + 0.7546098828 0.7547647953 0.7300565839 + 0.7552520037 0.7554169297 0.7307513952 + 0.7558938861 0.7560682893 0.7314453721 + 0.7565317750 0.7567108870 0.7321308851 + 0.7571697235 0.7573534250 0.7328165174 + 0.7578076124 0.7579960227 0.7335019708 + 0.7584453821 0.7586385012 0.7341874838 + 0.7590832710 0.7592810988 0.7348731160 + 0.7597208023 0.7599217296 0.7355561256 + 0.7603574991 0.7605590820 0.7362353206 + 0.7609941959 0.7611966133 0.7369145155 + 0.7616308928 0.7618340254 0.7375937104 + 0.7622675896 0.7624713778 0.7382730246 + 0.7629042864 0.7631089091 0.7389522195 + 0.7635387778 0.7637441754 0.7396275997 + 0.7641718984 0.7643783092 0.7403010130 + 0.7648050189 0.7650123835 0.7409744263 + 0.7654381990 0.7656465173 0.7416477799 + 0.7660713196 0.7662805915 0.7423211932 + 0.7667044997 0.7669147253 0.7429946065 + 0.7673382759 0.7675452828 0.7436637878 + 0.7679721713 0.7681750059 0.7443320155 + 0.7686061263 0.7688047290 0.7450003028 + 0.7692400217 0.7694345117 0.7456685901 + 0.7698739767 0.7700641751 0.7463368773 + 0.7705078721 0.7706940174 0.7470052242 + 0.7711389065 0.7713199258 0.7476691008 + 0.7717695832 0.7719454765 0.7483327985 + 0.7724003792 0.7725712061 0.7489963770 + 0.7730311155 0.7731968164 0.7496600747 + 0.7736619115 0.7738224864 0.7503237724 + 0.7742925882 0.7744480968 0.7509874105 + 0.7749173045 0.7750692964 0.7516471148 + 0.7755420208 0.7756903768 0.7523068786 + 0.7761667967 0.7763115764 0.7529665828 + 0.7767915130 0.7769327760 0.7536262870 + 0.7774162292 0.7775539160 0.7542861104 + 0.7780408859 0.7781749964 0.7549456954 + 0.7786604166 0.7787933946 0.7556020021 + 0.7792798281 0.7794117928 0.7562583089 + 0.7798992991 0.7800301909 0.7569146156 + 0.7805187106 0.7806485891 0.7575709224 + 0.7811381817 0.7812669873 0.7582271099 + 0.7817575932 0.7818853855 0.7588834167 + 0.7823737264 0.7824988961 0.7595390081 + 0.7829896212 0.7831121087 0.7601945996 + 0.7836055160 0.7837253213 0.7608501911 + 0.7842214108 0.7843385935 0.7615057826 + 0.7848373055 0.7849518061 0.7621613741 + 0.7854532003 0.7855650187 0.7628170848 + 0.7860642076 0.7861744165 0.7634735703 + 0.7866740227 0.7867829204 0.7641304135 + 0.7872837782 0.7873914242 0.7647870779 + 0.7878935933 0.7879999876 0.7654439211 + 0.7885034084 0.7886084914 0.7661007047 + 0.7891132236 0.7892169952 0.7667574286 + 0.7897166014 0.7898225784 0.7674102187 + 0.7903156877 0.7904263735 0.7680606842 + 0.7909148932 0.7910302281 0.7687112093 + 0.7915139794 0.7916340232 0.7693616748 + 0.7921131849 0.7922376990 0.7700121999 + 0.7927122712 0.7928414941 0.7706627250 + 0.7933096290 0.7934436202 0.7713115215 + 0.7939032912 0.7940425277 0.7719575763 + 0.7944970131 0.7946413755 0.7726035714 + 0.7950906157 0.7952402830 0.7732496262 + 0.7956842780 0.7958391905 0.7738956809 + 0.7962778807 0.7964379787 0.7745416760 + 0.7968716025 0.7970368862 0.7751876712 + 0.7974594831 0.7976313829 0.7758299112 + 0.7980473042 0.7982258797 0.7764720917 + 0.7986351848 0.7988204956 0.7771142721 + 0.7992231250 0.7994149923 0.7777565122 + 0.7998108864 0.8000094891 0.7783988118 + 0.8003988266 0.8006039858 0.7790409923 + 0.8009812832 0.8011956811 0.7796803117 + 0.8015602231 0.8017855287 0.7803177834 + 0.8021391034 0.8023753166 0.7809553146 + 0.8027181029 0.8029651046 0.7815927267 + 0.8032969832 0.8035550117 0.7822301984 + 0.8038759232 0.8041447997 0.7828677297 + 0.8044546247 0.8047344089 0.7835052013 + 0.8050318956 0.8053227067 0.7841426134 + 0.8056092262 0.8059110045 0.7847800851 + 0.8061863780 0.8064993024 0.7854174972 + 0.8067637086 0.8070874810 0.7860549092 + 0.8073409200 0.8076757789 0.7866923213 + 0.8079181910 0.8082640767 0.7873297930 + 0.8084933162 0.8088514805 0.7879632711 + 0.8090668917 0.8094381094 0.7885943055 + 0.8096404076 0.8100247979 0.7892252803 + 0.8102139831 0.8106114864 0.7898563147 + 0.8107876182 0.8111981750 0.7904872894 + 0.8113610744 0.8117849231 0.7911183238 + 0.8119347095 0.8123714924 0.7917492986 + 0.8125054836 0.8129528761 0.7923681736 + 0.8130763173 0.8135340810 0.7929869890 + 0.8136469722 0.8141154051 0.7936058044 + 0.8142176867 0.8146966100 0.7942246199 + 0.8147884011 0.8152778745 0.7948433757 + 0.8153591752 0.8158590794 0.7954621911 + 0.8159289956 0.8164386749 0.7960788012 + 0.8164964914 0.8170142770 0.7966904044 + 0.8170641065 0.8175899982 0.7973020077 + 0.8176316023 0.8181656003 0.7979136109 + 0.8181990981 0.8187412024 0.7985252142 + 0.8187667131 0.8193169236 0.7991368175 + 0.8193342090 0.8198925257 0.7997484207 + 0.8198990226 0.8204675913 0.8003578782 + 0.8204612136 0.8210421801 0.8009656072 + 0.8210235238 0.8216167092 0.8015732765 + 0.8215857744 0.8221912980 0.8021810055 + 0.8221480846 0.8227658868 0.8027886748 + 0.8227102757 0.8233404756 0.8033962846 + 0.8232725859 0.8239151239 0.8040040135 + 0.8238356113 0.8244882822 0.8046082258 + 0.8243988156 0.8250607848 0.8052108884 + 0.8249620795 0.8256334066 0.8058137298 + 0.8255254030 0.8262060285 0.8064163923 + 0.8260886073 0.8267785907 0.8070191145 + 0.8266518712 0.8273512125 0.8076218963 + 0.8272151947 0.8279237747 0.8082246184 + 0.8277786970 0.8284941912 0.8088231087 + 0.8283423185 0.8290640712 0.8094205856 + 0.8289058208 0.8296340108 0.8100181222 + 0.8294693828 0.8302038908 0.8106155992 + 0.8300330043 0.8307737708 0.8112130761 + 0.8305966258 0.8313437104 0.8118106127 + 0.8311601877 0.8319135904 0.8124080896 + 0.8317217827 0.8324813843 0.8130018115 + 0.8322829008 0.8330487013 0.8135946989 + 0.8328440189 0.8336160779 0.8141875863 + 0.8334051967 0.8341833949 0.8147805929 + 0.8339663148 0.8347507119 0.8153734803 + 0.8345274925 0.8353180885 0.8159664273 + 0.8350886106 0.8358854055 0.8165593743 + 0.8356444836 0.8364486098 0.8171482086 + 0.8361988068 0.8370106220 0.8177357912 + 0.8367530704 0.8375725746 0.8183234930 + 0.8373072743 0.8381345868 0.8189110756 + 0.8378615975 0.8386965990 0.8194987178 + 0.8384159207 0.8392586112 0.8200864196 + 0.8389701247 0.8398206234 0.8206740022 + 0.8395189047 0.8403776884 0.8212596774 + 0.8400645256 0.8409320712 0.8218443990 + 0.8406100273 0.8414863944 0.8224290013 + 0.8411555290 0.8420407176 0.8230137229 + 0.8417009711 0.8425949812 0.8235983253 + 0.8422465920 0.8431493044 0.8241829872 + 0.8427920938 0.8437036276 0.8247675896 + 0.8433353901 0.8442538977 0.8253523707 + 0.8438755870 0.8447986245 0.8259375095 + 0.8444157243 0.8453432918 0.8265225291 + 0.8449559212 0.8458880186 0.8271076083 + 0.8454961181 0.8464328051 0.8276926279 + 0.8460361958 0.8469774723 0.8282775879 + 0.8465763927 0.8475221992 0.8288627267 + 0.8471155763 0.8480662107 0.8294473290 + 0.8476483226 0.8486049771 0.8300294876 + 0.8481810093 0.8491439223 0.8306117058 + 0.8487136960 0.8496826887 0.8311939836 + 0.8492463827 0.8502215743 0.8317762017 + 0.8497790098 0.8507604003 0.8323584199 + 0.8503116965 0.8512992859 0.8329406977 + 0.8508443832 0.8518381119 0.8335229158 + 0.8513737917 0.8523707986 0.8341016769 + 0.8519021273 0.8529015183 0.8346794844 + 0.8524304032 0.8534322977 0.8352572918 + 0.8529587984 0.8539630175 0.8358352184 + 0.8534870744 0.8544937968 0.8364130259 + 0.8540154099 0.8550245762 0.8369907737 + 0.8545438051 0.8555552959 0.8375687003 + 0.8550698757 0.8560833931 0.8381444216 + 0.8555914164 0.8566060066 0.8387160897 + 0.8561128974 0.8571285009 0.8392876983 + 0.8566343784 0.8576511145 0.8398594260 + 0.8571557999 0.8581736088 0.8404310942 + 0.8576772809 0.8586962223 0.8410027027 + 0.8581988215 0.8592187166 0.8415743709 + 0.8587203026 0.8597412705 0.8421459794 + 0.8592392802 0.8602582812 0.8427104950 + 0.8597577810 0.8607739806 0.8432731032 + 0.8602762222 0.8612896800 0.8438357711 + 0.8607946038 0.8618053198 0.8443984985 + 0.8613131046 0.8623210192 0.8449612260 + 0.8618314862 0.8628365993 0.8455238938 + 0.8623499274 0.8633522987 0.8460866213 + 0.8628677130 0.8638666272 0.8466476202 + 0.8633821011 0.8643746972 0.8472021222 + 0.8638964891 0.8648828864 0.8477566242 + 0.8644108772 0.8653910756 0.8483110070 + 0.8649253249 0.8658993244 0.8488655090 + 0.8654397130 0.8664075136 0.8494200110 + 0.8659541011 0.8669157028 0.8499745131 + 0.8664684892 0.8674237728 0.8505288959 + 0.8669798970 0.8679283857 0.8510808945 + 0.8674882054 0.8684291840 0.8516303897 + 0.8679965138 0.8689299822 0.8521798849 + 0.8685047030 0.8694307804 0.8527294993 + 0.8690130115 0.8699315786 0.8532789946 + 0.8695213199 0.8704324961 0.8538284898 + 0.8700296283 0.8709332943 0.8543781042 + 0.8705378771 0.8714340925 0.8549275994 + 0.8710486889 0.8719300032 0.8554750085 + 0.8715605140 0.8724238873 0.8560217023 + 0.8720722795 0.8729178905 0.8565683961 + 0.8725841045 0.8734118938 0.8571150899 + 0.8730959892 0.8739058971 0.8576617837 + 0.8736078143 0.8743997812 0.8582084775 + 0.8741195798 0.8748937845 0.8587551713 + 0.8746314049 0.8753877878 0.8593019247 + 0.8751448989 0.8758807182 0.8598421812 + 0.8756586909 0.8763734102 0.8603816032 + 0.8761724830 0.8768661022 0.8609210253 + 0.8766862750 0.8773587942 0.8614603877 + 0.8772001266 0.8778516054 0.8619998097 + 0.8777137995 0.8783442974 0.8625391722 + 0.8782275915 0.8788369894 0.8630785942 + 0.8787413836 0.8793296814 0.8636180162 + 0.8792527914 0.8798214793 0.8641535044 + 0.8797639012 0.8803130984 0.8646888137 + 0.8802750111 0.8808047771 0.8652241230 + 0.8807861209 0.8812963963 0.8657593727 + 0.8812971711 0.8817880154 0.8662946820 + 0.8818082809 0.8822796941 0.8668299913 + 0.8823193908 0.8827713132 0.8673651814 + 0.8828305006 0.8832629919 0.8679004908 + 0.8833349943 0.8837479949 0.8684350252 + 0.8838387132 0.8842322826 0.8689693809 + 0.8843423724 0.8847165108 0.8695037961 + 0.8848459721 0.8852007985 0.8700382113 + 0.8853496909 0.8856850863 0.8705726266 + 0.8858534098 0.8861693144 0.8711069822 + 0.8863570094 0.8866536021 0.8716413975 + 0.8868607283 0.8871377707 0.8721758127 + 0.8873574734 0.8876165152 0.8727089167 + 0.8878520727 0.8880935907 0.8732416034 + 0.8883467913 0.8885707259 0.8737742901 + 0.8888415098 0.8890478015 0.8743069768 + 0.8893362284 0.8895248771 0.8748397231 + 0.8898308873 0.8900020123 0.8753722906 + 0.8903254867 0.8904790878 0.8759049773 + 0.8908202052 0.8909561038 0.8764377236 + 0.8913139105 0.8914325833 0.8769698739 + 0.8918069005 0.8919085860 0.8775017858 + 0.8922998905 0.8923845291 0.8780336976 + 0.8927928805 0.8928604722 0.8785656095 + 0.8932858706 0.8933364749 0.8790975213 + 0.8937789202 0.8938124776 0.8796293736 + 0.8942717910 0.8942884207 0.8801612258 + 0.8947647810 0.8947644234 0.8806930780 + 0.8952565789 0.8952397108 0.8812240958 + 0.8957449198 0.8957130909 0.8817527890 + 0.8962332010 0.8961864710 0.8822814226 + 0.8967214823 0.8966599107 0.8828101158 + 0.8972098231 0.8971332908 0.8833386898 + 0.8976982236 0.8976066709 0.8838673830 + 0.8981865048 0.8980801105 0.8843960762 + 0.8986747861 0.8985534906 0.8849247098 + 0.8991631269 0.8990268707 0.8854534030 + 0.8996483088 0.8994984031 0.8859789968 + 0.9001330137 0.8999695182 0.8865042925 + 0.9006177187 0.9004406929 0.8870295882 + 0.9011024833 0.9009119272 0.8875548244 + 0.9015871882 0.9013829827 0.8880801201 + 0.9020720124 0.9018542171 0.8886054158 + 0.9025567174 0.9023253918 0.8891307116 + 0.9030414224 0.9027965069 0.8896560073 + 0.9035236239 0.9032670856 0.8901805878 + 0.9040020704 0.9037368894 0.8907042146 + 0.9044805169 0.9042065740 0.8912279010 + 0.9049589038 0.9046763182 0.8917515278 + 0.9054374099 0.9051461220 0.8922752142 + 0.9059157968 0.9056158066 0.8927989006 + 0.9063943028 0.9060856104 0.8933225274 + 0.9068726897 0.9065552950 0.8938462138 + 0.9073510766 0.9070249796 0.8943697810 + 0.9078236818 0.9074894190 0.8948928118 + 0.9082950950 0.9079527259 0.8954156041 + 0.9087665081 0.9084160924 0.8959383965 + 0.9092379808 0.9088793993 0.8964611888 + 0.9097093940 0.9093427062 0.8969839811 + 0.9101808071 0.9098060727 0.8975067735 + 0.9106522202 0.9102693796 0.8980296254 + 0.9111235738 0.9107328057 0.8985524178 + 0.9115946293 0.9111946225 0.8990744948 + 0.9120634794 0.9116498828 0.8995931745 + 0.9125325084 0.9121053219 0.9001119137 + 0.9130014181 0.9125605822 0.9006305933 + 0.9134703875 0.9130159020 0.9011492729 + 0.9139392972 0.9134712219 0.9016680717 + 0.9144083261 0.9139264822 0.9021868110 + 0.9148771763 0.9143819213 0.9027054906 + 0.9153460860 0.9148371816 0.9032241702 + 0.9158136249 0.9152905941 0.9037396908 + 0.9162790179 0.9157413840 0.9042512178 + 0.9167444706 0.9161921740 0.9047626257 + 0.9172099233 0.9166430235 0.9052739739 + 0.9176753163 0.9170938134 0.9057853818 + 0.9181408286 0.9175446033 0.9062967896 + 0.9186062217 0.9179953933 0.9068081975 + 0.9190716743 0.9184461832 0.9073196054 + 0.9195371270 0.9188969731 0.9078310132 + 0.9200031161 0.9193475842 0.9083387852 + 0.9204695225 0.9197980165 0.9088441133 + 0.9209359288 0.9202483892 0.9093493223 + 0.9214022756 0.9206988215 0.9098545909 + 0.9218686819 0.9211491942 0.9103597999 + 0.9223350883 0.9215996265 0.9108651280 + 0.9228014946 0.9220499992 0.9113702774 + 0.9232679009 0.9225003719 0.9118756056 + 0.9237343073 0.9229508042 0.9123808146 + 0.9242020249 0.9234021902 0.9128848910 + 0.9246705174 0.9238542914 0.9133883119 + 0.9251390100 0.9243063927 0.9138916731 + 0.9256075025 0.9247584939 0.9143949747 + 0.9260761142 0.9252105951 0.9148983955 + 0.9265446067 0.9256626964 0.9154018164 + 0.9270130992 0.9261147976 0.9159051776 + 0.9274815917 0.9265668988 0.9164085984 + 0.9279500842 0.9270188808 0.9169120193 + 0.9284178019 0.9274703264 0.9174153209 + 0.9288849235 0.9279211760 0.9179183841 + 0.9293519855 0.9283720851 0.9184216261 + 0.9298191071 0.9288229942 0.9189246893 + 0.9302862883 0.9292739034 0.9194278121 + 0.9307534099 0.9297246933 0.9199309945 + 0.9312204719 0.9301756024 0.9204341173 + 0.9316875935 0.9306265116 0.9209372997 + 0.9321547151 0.9310774207 0.9214404225 + 0.9326195121 0.9315286279 0.9219434857 + 0.9330815077 0.9319801927 0.9224464893 + 0.9335433841 0.9324318767 0.9229494929 + 0.9340053797 0.9328835011 0.9234524965 + 0.9344673753 0.9333351254 0.9239555001 + 0.9349293113 0.9337866902 0.9244585037 + 0.9353913069 0.9342383146 0.9249615073 + 0.9358531833 0.9346899986 0.9254645109 + 0.9363151789 0.9351416230 0.9259675145 + 0.9367759228 0.9355927110 0.9264699817 + 0.9372317791 0.9360417128 0.9269704819 + 0.9376876950 0.9364907742 0.9274709225 + 0.9381436706 0.9369398952 0.9279714227 + 0.9385995865 0.9373890162 0.9284719229 + 0.9390555024 0.9378380775 0.9289724231 + 0.9395114183 0.9382871985 0.9294728041 + 0.9399673939 0.9387362003 0.9299733043 + 0.9404233098 0.9391853213 0.9304738045 + 0.9408792257 0.9396343827 0.9309743047 + 0.9413326979 0.9400802255 0.9314733744 + 0.9417858124 0.9405254722 0.9319723248 + 0.9422389865 0.9409707785 0.9324712157 + 0.9426921010 0.9414162040 0.9329702258 + 0.9431452155 0.9418615103 0.9334691167 + 0.9435983896 0.9423068166 0.9339680076 + 0.9440515041 0.9427521229 0.9344670177 + 0.9445046186 0.9431974292 0.9349659085 + 0.9449576735 0.9436427951 0.9354647994 + 0.9454091787 0.9440872073 0.9359622002 + 0.9458584785 0.9445304871 0.9364575744 + 0.9463077784 0.9449738860 0.9369530082 + 0.9467570186 0.9454172254 0.9374483824 + 0.9472063184 0.9458606243 0.9379438162 + 0.9476556182 0.9463039041 0.9384390712 + 0.9481049180 0.9467471838 0.9389345050 + 0.9485542178 0.9471905828 0.9394298792 + 0.9490035176 0.9476339221 0.9399253130 + 0.9494526982 0.9480772018 0.9404206872 + 0.9498931766 0.9485139847 0.9409106970 + 0.9503325224 0.9489501119 0.9413999915 + 0.9507718086 0.9493861198 0.9418894053 + 0.9512112141 0.9498221874 0.9423786998 + 0.9516505003 0.9502581954 0.9428679943 + 0.9520899057 0.9506943226 0.9433574080 + 0.9525291920 0.9511302710 0.9438467026 + 0.9529684782 0.9515663981 0.9443361163 + 0.9534078836 0.9520024061 0.9448254108 + 0.9538437128 0.9524362087 0.9453141093 + 0.9542688131 0.9528629184 0.9458007216 + 0.9546939135 0.9532896280 0.9462872744 + 0.9551190138 0.9537162185 0.9467738867 + 0.9555441141 0.9541429281 0.9472606182 + 0.9559692144 0.9545695782 0.9477471709 + 0.9563943148 0.9549962878 0.9482337832 + 0.9568194747 0.9554229975 0.9487203956 + 0.9572445750 0.9558497071 0.9492071271 + 0.9576696754 0.9562764168 0.9496936798 + 0.9580900073 0.9567002058 0.9501780868 + 0.9585055113 0.9571210742 0.9506604075 + 0.9589208961 0.9575420022 0.9511427283 + 0.9593364000 0.9579628110 0.9516249895 + 0.9597519040 0.9583836794 0.9521073103 + 0.9601672888 0.9588046074 0.9525895715 + 0.9605827928 0.9592254758 0.9530717731 + 0.9609982967 0.9596462846 0.9535540938 + 0.9614136815 0.9600672126 0.9540364146 + 0.9618291855 0.9604880810 0.9545186758 + 0.9622408748 0.9609053731 0.9549967051 + 0.9626505971 0.9613206983 0.9554721713 + 0.9630602002 0.9617360234 0.9559476972 + 0.9634698033 0.9621512294 0.9564232826 + 0.9638794065 0.9625664949 0.9568988085 + 0.9642891288 0.9629818201 0.9573743939 + 0.9646986723 0.9633970857 0.9578499198 + 0.9651082754 0.9638124108 0.9583255053 + 0.9655178785 0.9642276168 0.9588009715 + 0.9659274817 0.9646428823 0.9592764974 + 0.9663329720 0.9650545716 0.9597474933 + 0.9667364955 0.9654644728 0.9602164030 + 0.9671400189 0.9658744931 0.9606851935 + 0.9675434828 0.9662843943 0.9611539841 + 0.9679470062 0.9666942954 0.9616228938 + 0.9683504105 0.9671043158 0.9620916843 + 0.9687538743 0.9675142169 0.9625605941 + 0.9691573977 0.9679241776 0.9630293846 + 0.9695609212 0.9683340788 0.9634981751 + 0.9699643850 0.9687440991 0.9639670849 + 0.9703654051 0.9691501260 0.9644325972 + 0.9707646966 0.9695534706 0.9648959041 + 0.9711639881 0.9699568748 0.9653592706 + 0.9715632796 0.9703602791 0.9658225775 + 0.9719626904 0.9707638025 0.9662858844 + 0.9723619819 0.9711672068 0.9667493105 + 0.9727612734 0.9715706110 0.9672126174 + 0.9731606841 0.9719740748 0.9676759839 + 0.9735599756 0.9723774791 0.9681392908 + 0.9739593267 0.9727808833 0.9686025977 + 0.9743570089 0.9731823802 0.9690620899 + 0.9747526050 0.9735813141 0.9695165157 + 0.9751480818 0.9739801884 0.9699708223 + 0.9755436778 0.9743791223 0.9704251289 + 0.9759392142 0.9747779965 0.9708794951 + 0.9763348103 0.9751768708 0.9713338017 + 0.9767302871 0.9755758047 0.9717881083 + 0.9771258235 0.9759746790 0.9722424746 + 0.9775214195 0.9763736725 0.9726967812 + 0.9779168963 0.9767726064 0.9731512070 + 0.9783114195 0.9771707058 0.9736040235 + 0.9786989093 0.9775642753 0.9740474820 + 0.9790863991 0.9779577851 0.9744911194 + 0.9794738889 0.9783512950 0.9749345779 + 0.9798614979 0.9787448049 0.9753780961 + 0.9802489877 0.9791383147 0.9758216143 + 0.9806364775 0.9795318246 0.9762650728 + 0.9810240269 0.9799252748 0.9767085910 + 0.9814115167 0.9803189039 0.9771521091 + 0.9817990065 0.9807124138 0.9775956273 + 0.9821864963 0.9811059237 0.9780392051 + 0.9825655818 0.9814931750 0.9784761071 + 0.9829416275 0.9818782210 0.9789105058 + 0.9833174944 0.9822633266 0.9793450236 + 0.9836934805 0.9826483130 0.9797794819 + 0.9840694070 0.9830332994 0.9802138805 + 0.9844453931 0.9834184051 0.9806483984 + 0.9848213196 0.9838033915 0.9810829163 + 0.9851973057 0.9841883779 0.9815173149 + 0.9855731726 0.9845734239 0.9819517732 + 0.9859492183 0.9849585295 0.9823862910 + 0.9863222241 0.9853414297 0.9828186035 + 0.9866843820 0.9857168794 0.9832429886 + 0.9870467186 0.9860923290 0.9836674929 + 0.9874088764 0.9864677191 0.9840919971 + 0.9877712131 0.9868432283 0.9845165014 + 0.9881333709 0.9872186184 0.9849410057 + 0.9884957075 0.9875940084 0.9853653908 + 0.9888579249 0.9879695177 0.9857898951 + 0.9892202020 0.9883449078 0.9862143993 + 0.9895824194 0.9887202978 0.9866389036 + 0.9899446964 0.9890956879 0.9870632887 + 0.9902951717 0.9894627929 0.9874807000 + 0.9906374216 0.9898239970 0.9878932238 + 0.9909796119 0.9901852012 0.9883056283 + 0.9913218021 0.9905464053 0.9887179732 + 0.9916639924 0.9909076095 0.9891303778 + 0.9920061827 0.9912688136 0.9895427823 + 0.9923483729 0.9916298985 0.9899551868 + 0.9926906228 0.9919911027 0.9903675914 + 0.9930328131 0.9923523068 0.9907801151 + 0.9933750033 0.9927135110 0.9911925197 + 0.9937171936 0.9930747151 0.9916049242 + 0.9940317869 0.9934129715 0.9920030236 + 0.9943410754 0.9937469959 0.9923985004 + 0.9946504831 0.9940810204 0.9927939177 + 0.9949597716 0.9944151044 0.9931892753 + 0.9952691793 0.9947491288 0.9935848117 + 0.9955785275 0.9950830936 0.9939802289 + 0.9958878160 0.9954171181 0.9943757057 + 0.9961972237 0.9957510829 0.9947711229 + 0.9965065122 0.9960851073 0.9951664805 + 0.9968159199 0.9964190722 0.9955620170 + 0.9971252084 0.9967530966 0.9959573746 + 0.9973875284 0.9970490932 0.9963253736 + 0.9976487160 0.9973441958 0.9966928959 + 0.9979100227 0.9976391792 0.9970602989 + 0.9981712103 0.9979342818 0.9974278212 + 0.9984325171 0.9982293844 0.9977952838 + 0.9986937046 0.9985244870 0.9981626868 + 0.9989550114 0.9988195896 0.9985302091 + 0.9992161989 0.9991146922 0.9988976121 + 0.9994775057 0.9994097948 0.9992650747 + 0.9997386932 0.9997048974 0.9996324778 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_advantix_400.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_advantix_400.spi1d index 289bffe3..e9e844fb 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_advantix_400.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_advantix_400.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0077106012 0.0070298198 0.0049559241 - 0.0147110997 0.0133244200 0.0095438547 - 0.0209813993 0.0190351103 0.0137957605 - 0.0266668592 0.0242017098 0.0177728795 - 0.0318832099 0.0289859492 0.0215300098 - 0.0367198400 0.0334706604 0.0251365695 - 0.0412060991 0.0377071798 0.0285660401 - 0.0454186983 0.0417603701 0.0318240002 - 0.0495144203 0.0456782803 0.0349723399 - 0.0534897000 0.0494300202 0.0380599014 - 0.0572170094 0.0530248806 0.0410333909 - 0.0607506298 0.0564629287 0.0438579209 - 0.0642050877 0.0597890504 0.0466150604 - 0.0676248968 0.0630782619 0.0493207090 - 0.0709792525 0.0663433895 0.0519826487 - 0.0742666870 0.0695055276 0.0545649193 - 0.0774300173 0.0725333989 0.0570865013 - 0.0805151016 0.0754740685 0.0595324002 - 0.0835210085 0.0783350095 0.0619145297 - 0.0864818692 0.0811579376 0.0642616600 - 0.0894213691 0.0839637592 0.0665879101 - 0.0923196077 0.0867140666 0.0689035431 - 0.0951313078 0.0894301310 0.0712108612 - 0.0978739634 0.0921017230 0.0734841302 - 0.1005603969 0.0947171375 0.0757039636 - 0.1032031998 0.0972908884 0.0778791532 - 0.1058200002 0.0998220220 0.0800394490 - 0.1084190011 0.1023029983 0.0821614563 - 0.1110042036 0.1047497019 0.0842550322 - 0.1135606021 0.1071517989 0.0863265395 - 0.1160767004 0.1095222011 0.0883673579 - 0.1185398027 0.1118747965 0.0903817788 - 0.1209601983 0.1141934991 0.0923686922 - 0.1233263016 0.1164946035 0.0943281874 - 0.1256539971 0.1187560037 0.0962788835 - 0.1279318035 0.1210196018 0.0982189104 - 0.1301839054 0.1232483014 0.1001423001 - 0.1324169934 0.1254474968 0.1020568013 - 0.1346450001 0.1275981963 0.1039576009 - 0.1368439049 0.1297464967 0.1058444977 - 0.1390296966 0.1318780035 0.1077136993 - 0.1412141025 0.1339951009 0.1095665023 - 0.1433850974 0.1361036003 0.1114090011 - 0.1455492973 0.1381817013 0.1132375002 - 0.1476995945 0.1402491927 0.1150472015 - 0.1498260051 0.1422760040 0.1168475971 - 0.1519210935 0.1442877054 0.1186361015 - 0.1539843976 0.1462759972 0.1204114035 - 0.1560284048 0.1482367963 0.1221595034 - 0.1580577046 0.1501837969 0.1238816977 - 0.1600770950 0.1520957053 0.1255871058 - 0.1620800942 0.1539966017 0.1272871047 - 0.1640495956 0.1559009999 0.1289671063 - 0.1659898013 0.1577883065 0.1306212991 - 0.1679138988 0.1596692055 0.1322596073 - 0.1698164046 0.1615532041 0.1338987052 - 0.1716932058 0.1634092033 0.1355329007 - 0.1735651940 0.1652536988 0.1371469051 - 0.1754256934 0.1670915931 0.1387543976 - 0.1772599965 0.1689036936 0.1403618008 - 0.1790851951 0.1707046032 0.1419776976 - 0.1808962971 0.1724977046 0.1435960978 - 0.1826874018 0.1742753983 0.1452053040 - 0.1844758987 0.1760344952 0.1468037963 - 0.1862519979 0.1777956039 0.1483945996 - 0.1880110949 0.1795548946 0.1499821991 - 0.1897681952 0.1812911928 0.1515644044 - 0.1915334016 0.1830188930 0.1531336010 - 0.1933026016 0.1847521067 0.1546926945 - 0.1950711012 0.1864724010 0.1562453955 - 0.1968317926 0.1881736070 0.1577879936 - 0.1985855997 0.1898707002 0.1593212038 - 0.2003341019 0.1915744990 0.1608411968 - 0.2020726055 0.1932611018 0.1623475999 - 0.2038006037 0.1949308962 0.1638498008 - 0.2055211961 0.1965949982 0.1653402001 - 0.2072315067 0.1982464939 0.1668276936 - 0.2089259028 0.1998752952 0.1683100015 - 0.2106073052 0.2014864981 0.1697766036 - 0.2122824937 0.2030902952 0.1712321937 - 0.2139520049 0.2046858966 0.1726851016 - 0.2156112045 0.2062691003 0.1741385013 - 0.2172587067 0.2078413963 0.1755896062 - 0.2189002037 0.2094067931 0.1770340055 - 0.2205414027 0.2109739035 0.1784697026 - 0.2221654058 0.2125506997 0.1799007058 - 0.2237752974 0.2141104043 0.1813267022 - 0.2253734022 0.2156585008 0.1827555001 - 0.2269579023 0.2172029018 0.1841821969 - 0.2285311073 0.2187507004 0.1856049001 - 0.2301028073 0.2203052938 0.1870242953 - 0.2316679060 0.2218398005 0.1884412020 - 0.2332272977 0.2233646065 0.1898476034 - 0.2347781956 0.2248875052 0.1912508011 - 0.2363210022 0.2264139950 0.1926520020 - 0.2378583997 0.2279502004 0.1940511018 - 0.2393900007 0.2294773012 0.1954436004 - 0.2409213930 0.2309951931 0.1968320012 - 0.2424533069 0.2325080037 0.1982160956 - 0.2439856976 0.2340212017 0.1995960027 - 0.2455042005 0.2355341017 0.2009744048 - 0.2470149994 0.2370381951 0.2023515999 - 0.2485187054 0.2385327965 0.2037250996 - 0.2500135005 0.2400144935 0.2050911933 - 0.2515035868 0.2414941937 0.2064488977 - 0.2529886961 0.2429734021 0.2078043073 - 0.2544668913 0.2444527000 0.2091580927 - 0.2559421062 0.2459246963 0.2105102986 - 0.2574181855 0.2473870963 0.2118566930 - 0.2588958144 0.2488387972 0.2131976932 - 0.2603701949 0.2502841949 0.2145289928 - 0.2618409991 0.2517311871 0.2158571929 - 0.2633078098 0.2531799972 0.2171828002 - 0.2647666931 0.2546122968 0.2185021937 - 0.2662197948 0.2560352981 0.2198203057 - 0.2676647902 0.2574462891 0.2211371064 - 0.2690947056 0.2588512003 0.2224553972 - 0.2705186903 0.2602562904 0.2237727940 - 0.2719335854 0.2616656125 0.2250885069 - 0.2733449936 0.2630712092 0.2264019996 - 0.2747541964 0.2644746006 0.2277133018 - 0.2761535943 0.2658649981 0.2290149033 - 0.2775473893 0.2672511041 0.2303117961 - 0.2789311111 0.2686300874 0.2316035926 - 0.2803080976 0.2700101137 0.2328924984 - 0.2816820145 0.2713905871 0.2341804951 - 0.2830528915 0.2727720141 0.2354660928 - 0.2844218910 0.2741498053 0.2367486060 - 0.2857857049 0.2755183876 0.2380257994 - 0.2871482074 0.2768800855 0.2393009067 - 0.2885094881 0.2782357037 0.2405748963 - 0.2898694873 0.2795907855 0.2418427020 - 0.2912288904 0.2809456885 0.2431090027 - 0.2925894856 0.2822988927 0.2443675995 - 0.2939479947 0.2836497128 0.2456243038 - 0.2952952981 0.2849884033 0.2468754947 - 0.2966383994 0.2863219976 0.2481230944 - 0.2979727983 0.2876453102 0.2493657023 - 0.2993055880 0.2889670134 0.2506079078 - 0.3006362915 0.2902868092 0.2518497109 - 0.3019703031 0.2916113138 0.2530942857 - 0.3033066094 0.2929391861 0.2543402910 - 0.3046467900 0.2942663133 0.2555767894 - 0.3059892058 0.2955929935 0.2568103075 - 0.3073258102 0.2969133854 0.2580339909 - 0.3086602092 0.2982315123 0.2592557073 - 0.3099907041 0.2995441854 0.2604722977 - 0.3113198876 0.3008551896 0.2616879940 - 0.3126456141 0.3021624088 0.2629038990 - 0.3139702082 0.3034684956 0.2641198039 - 0.3152804971 0.3047659099 0.2653389871 - 0.3165856004 0.3060601950 0.2665588856 - 0.3178817928 0.3073394001 0.2677814960 - 0.3191738129 0.3086116910 0.2690047026 - 0.3204653859 0.3098776937 0.2702280879 - 0.3217566907 0.3111397028 0.2714515030 - 0.3230504096 0.3124023974 0.2726680934 - 0.3243465126 0.3136656880 0.2738803029 - 0.3256357014 0.3149282932 0.2750887871 - 0.3269129992 0.3161900043 0.2762930095 - 0.3281877041 0.3174473047 0.2774964869 - 0.3294523060 0.3186891079 0.2786985040 - 0.3307167888 0.3199306130 0.2799009085 - 0.3319742084 0.3211612105 0.2811062932 - 0.3332315981 0.3223916888 0.2823117077 - 0.3344798982 0.3236224949 0.2835228145 - 0.3357258141 0.3248533905 0.2847346067 - 0.3369632959 0.3260852993 0.2859402895 - 0.3381935060 0.3273181915 0.2871426940 - 0.3394210935 0.3285504878 0.2883437872 - 0.3406409919 0.3297812939 0.2895427942 - 0.3418608904 0.3310121894 0.2907412052 - 0.3430762887 0.3322376013 0.2919301987 - 0.3442913890 0.3334628046 0.2931191921 - 0.3455013931 0.3346853852 0.2943033874 - 0.3467079103 0.3359062970 0.2954855859 - 0.3479113877 0.3371267021 0.2966673970 - 0.3491044044 0.3383455873 0.2978483140 - 0.3502975106 0.3395645022 0.2990292907 - 0.3514811993 0.3407759070 0.3002046943 - 0.3526628017 0.3419859111 0.3013797998 - 0.3538405001 0.3431887925 0.3025487959 - 0.3550119102 0.3443804085 0.3037114143 - 0.3561832011 0.3455719948 0.3048739135 - 0.3573499024 0.3467567861 0.3060263097 - 0.3585161865 0.3479408920 0.3071787059 - 0.3596819937 0.3491263092 0.3083275855 - 0.3608472049 0.3503136933 0.3094730973 - 0.3620123863 0.3515009880 0.3106186092 - 0.3631744087 0.3526912928 0.3117532134 - 0.3643358946 0.3538821042 0.3128874004 - 0.3654957116 0.3550710082 0.3140192926 - 0.3666515052 0.3562552929 0.3151476085 - 0.3678072095 0.3574396968 0.3162758946 - 0.3689568043 0.3586226106 0.3174037933 - 0.3701032996 0.3598049879 0.3185315132 - 0.3712497056 0.3609873056 0.3196595907 - 0.3723874092 0.3621698916 0.3207899034 - 0.3735251129 0.3633525968 0.3219203055 - 0.3746606112 0.3645330071 0.3230482042 - 0.3757919073 0.3657091856 0.3241730034 - 0.3769231141 0.3668855131 0.3252978027 - 0.3780477941 0.3680585921 0.3264195919 - 0.3791683912 0.3692297041 0.3275401890 - 0.3802889884 0.3704009056 0.3286607862 - 0.3814114928 0.3715662062 0.3297694921 - 0.3825342059 0.3727307022 0.3308778107 - 0.3836567998 0.3738948107 0.3319844902 - 0.3847779036 0.3750543892 0.3330835998 - 0.3858990073 0.3762139976 0.3341825902 - 0.3870202899 0.3773706853 0.3352803886 - 0.3881419003 0.3785190880 0.3363755047 - 0.3892636001 0.3796674013 0.3374705911 - 0.3903855085 0.3808118105 0.3385663033 - 0.3915078044 0.3819499016 0.3396627009 - 0.3926301897 0.3830879927 0.3407590091 - 0.3937473893 0.3842205107 0.3418577909 - 0.3948588073 0.3853465915 0.3429583013 - 0.3959701955 0.3864727020 0.3440589011 - 0.3970786929 0.3875936866 0.3451572061 - 0.3981842995 0.3887099922 0.3462541997 - 0.3992899954 0.3898262978 0.3473511934 - 0.4003950953 0.3909426928 0.3484469056 - 0.4014996886 0.3920593858 0.3495419025 - 0.4026043117 0.3931761086 0.3506368995 - 0.4037061036 0.3942928016 0.3517259955 - 0.4048052132 0.3954096138 0.3528110087 - 0.4059042931 0.3965264857 0.3538959920 - 0.4069995880 0.3976393044 0.3549771905 - 0.4080899060 0.3987472057 0.3560549915 - 0.4091801047 0.3998549879 0.3571327925 - 0.4102692008 0.4009611905 0.3582096994 - 0.4113557935 0.4020644128 0.3592854142 - 0.4124422967 0.4031674862 0.3603610098 - 0.4135288894 0.4042702019 0.3614374101 - 0.4146164060 0.4053708017 0.3625159860 - 0.4157038033 0.4064713120 0.3635945916 - 0.4167912900 0.4075717926 0.3646731079 - 0.4178684056 0.4086636007 0.3657506108 - 0.4189451039 0.4097551107 0.3668282032 - 0.4200218916 0.4108465910 0.3679057062 - 0.4210911095 0.4119307995 0.3689739108 - 0.4221575856 0.4130122960 0.3700402081 - 0.4232240021 0.4140937924 0.3711063862 - 0.4242891967 0.4151681066 0.3721705079 - 0.4253528118 0.4162335992 0.3732328117 - 0.4264163971 0.4172990918 0.3742950857 - 0.4274801910 0.4183638096 0.3753553033 - 0.4285458922 0.4194228053 0.3764078021 - 0.4296115041 0.4204818010 0.3774603903 - 0.4306772053 0.4215407968 0.3785130084 - 0.4317356944 0.4225960970 0.3795596957 - 0.4327917993 0.4236500859 0.3806053102 - 0.4338479042 0.4247041941 0.3816510141 - 0.4349010885 0.4257571995 0.3826957047 - 0.4359481037 0.4268082976 0.3837392926 - 0.4369950891 0.4278593957 0.3847829103 - 0.4380421042 0.4289104939 0.3858264983 - 0.4390830994 0.4299480021 0.3868727982 - 0.4401232004 0.4309833944 0.3879191875 - 0.4411633015 0.4320186973 0.3889656067 - 0.4421998858 0.4330511987 0.3900116980 - 0.4432297945 0.4340781868 0.3910571933 - 0.4442596138 0.4351052940 0.3921026886 - 0.4452894926 0.4361324012 0.3931482136 - 0.4463081956 0.4371604025 0.3941898048 - 0.4473238885 0.4381886125 0.3952310085 - 0.4483394921 0.4392169118 0.3962720931 - 0.4493534863 0.4402456880 0.3973116875 - 0.4503594041 0.4412770867 0.3983471990 - 0.4513652921 0.4423084855 0.3993827105 - 0.4523712099 0.4433399141 0.4004181027 - 0.4533743858 0.4443711936 0.4014500976 - 0.4543752968 0.4454025030 0.4024800062 - 0.4553762078 0.4464336932 0.4035097957 - 0.4563770890 0.4474649131 0.4045395851 - 0.4573712051 0.4484929144 0.4055626094 - 0.4583637118 0.4495202899 0.4065848887 - 0.4593561888 0.4505476058 0.4076071978 - 0.4603483975 0.4515748024 0.4086290896 - 0.4613364041 0.4525992870 0.4096485972 - 0.4623242915 0.4536238015 0.4106681049 - 0.4633122981 0.4546484053 0.4116876125 - 0.4642986953 0.4556727111 0.4127076864 - 0.4652802050 0.4566963017 0.4137291908 - 0.4662617147 0.4577200115 0.4147506952 - 0.4672433138 0.4587436914 0.4157721996 - 0.4682214856 0.4597648084 0.4167939126 - 0.4691945910 0.4607819021 0.4178156853 - 0.4701677859 0.4617989957 0.4188374877 - 0.4711408913 0.4628162086 0.4198592901 - 0.4721117914 0.4638265073 0.4208815098 - 0.4730801880 0.4648295045 0.4219039977 - 0.4740486145 0.4658325016 0.4229264855 - 0.4750168920 0.4668354988 0.4239490926 - 0.4759812951 0.4678359032 0.4249672890 - 0.4769417942 0.4688338935 0.4259825051 - 0.4779022932 0.4698317945 0.4269978106 - 0.4788627923 0.4708298147 0.4280130863 - 0.4798217118 0.4718261063 0.4290243089 - 0.4807789028 0.4728206098 0.4300324917 - 0.4817360938 0.4738149941 0.4310407043 - 0.4826933146 0.4748094976 0.4320487976 - 0.4836510122 0.4758031070 0.4330525994 - 0.4846090972 0.4767954051 0.4340521097 - 0.4855673015 0.4777877033 0.4350515008 - 0.4865255058 0.4787800014 0.4360510111 - 0.4874827862 0.4797681868 0.4370489120 - 0.4884381890 0.4807466865 0.4380441904 - 0.4893935025 0.4817250967 0.4390394986 - 0.4903487861 0.4827035964 0.4400348067 - 0.4913035929 0.4836812913 0.4410301149 - 0.4922530055 0.4846526980 0.4420258105 - 0.4932022989 0.4856241941 0.4430215061 - 0.4941517115 0.4865956008 0.4440171123 - 0.4951010942 0.4875670969 0.4450128078 - 0.4960469902 0.4885401130 0.4460073113 - 0.4969922900 0.4895133078 0.4470018148 - 0.4979375899 0.4904865026 0.4479962885 - 0.4988828897 0.4914596975 0.4489907026 - 0.4998261929 0.4924297035 0.4499832988 - 0.5007678866 0.4933972061 0.4509747922 - 0.5017095804 0.4943645895 0.4519664049 - 0.5026512146 0.4953320920 0.4529578984 - 0.5035929084 0.4962972105 0.4539453983 - 0.5045344830 0.4972535968 0.4549227953 - 0.5054761767 0.4982098937 0.4559001923 - 0.5064178109 0.4991663098 0.4568775892 - 0.5073593855 0.5001226068 0.4578549862 - 0.5082985759 0.5010713935 0.4588257968 - 0.5092371106 0.5020182133 0.4597955048 - 0.5101755261 0.5029649734 0.4607652128 - 0.5111140013 0.5039117932 0.4617348909 - 0.5120512843 0.5048583150 0.4627009034 - 0.5129861832 0.5058041811 0.4636611938 - 0.5139210224 0.5067501068 0.4646215141 - 0.5148558021 0.5076960921 0.4655818045 - 0.5157907009 0.5086420178 0.4665420949 - 0.5167201161 0.5095835924 0.4674983025 - 0.5176479220 0.5105239153 0.4684537053 - 0.5185757279 0.5114641786 0.4694091976 - 0.5195034742 0.5124045014 0.4703646004 - 0.5204296112 0.5133435726 0.4713194966 - 0.5213472843 0.5142772794 0.4722726047 - 0.5222650766 0.5152109265 0.4732257128 - 0.5231828094 0.5161445141 0.4741787910 - 0.5241004825 0.5170782208 0.4751318991 - 0.5250151753 0.5180053115 0.4760791957 - 0.5259271264 0.5189262033 0.4770225883 - 0.5268388987 0.5198469758 0.4779661000 - 0.5277506709 0.5207679272 0.4789094925 - 0.5286625028 0.5216888189 0.4798529148 - 0.5295729041 0.5226063132 0.4807907045 - 0.5304828882 0.5235229135 0.4817276001 - 0.5313928723 0.5244395137 0.4826645851 - 0.5323029160 0.5253561139 0.4836015105 - 0.5332129002 0.5262727141 0.4845381975 - 0.5341181159 0.5271934867 0.4854718149 - 0.5350232720 0.5281143188 0.4864053130 - 0.5359284282 0.5290349722 0.4873388112 - 0.5368335843 0.5299558043 0.4882723987 - 0.5377376080 0.5308768749 0.4892036915 - 0.5386344790 0.5318000913 0.4901269078 - 0.5395315289 0.5327231884 0.4910500944 - 0.5404284000 0.5336464047 0.4919733107 - 0.5413252711 0.5345695019 0.4928965867 - 0.5422213078 0.5354900956 0.4938173890 - 0.5431140065 0.5364019871 0.4947327971 - 0.5440065861 0.5373138785 0.4956482053 - 0.5448992848 0.5382258296 0.4965636134 - 0.5457919240 0.5391377211 0.4974789023 - 0.5466845036 0.5400484800 0.4983932078 - 0.5475770235 0.5409557223 0.4993050992 - 0.5484694839 0.5418627858 0.5002170205 - 0.5493621230 0.5427699089 0.5011289120 - 0.5502545834 0.5436770916 0.5020408034 - 0.5511453748 0.5445848703 0.5029529929 - 0.5520284176 0.5454956889 0.5038657784 - 0.5529114008 0.5464066267 0.5047786236 - 0.5537943244 0.5473175049 0.5056914091 - 0.5546773076 0.5482283235 0.5066043139 - 0.5555598140 0.5491390228 0.5075178146 - 0.5564333797 0.5500462055 0.5084359050 - 0.5573070049 0.5509535074 0.5093541145 - 0.5581805706 0.5518608093 0.5102722049 - 0.5590541959 0.5527679920 0.5111904144 - 0.5599278212 0.5536752939 0.5121085048 - 0.5607947707 0.5545750260 0.5130286813 - 0.5616607070 0.5554735065 0.5139490962 - 0.5625265837 0.5563721061 0.5148693919 - 0.5633925200 0.5572705865 0.5157896876 - 0.5642585158 0.5581691265 0.5167099833 - 0.5651180744 0.5590634942 0.5176302195 - 0.5659735799 0.5599551201 0.5185502172 - 0.5668290257 0.5608466268 0.5194702148 - 0.5676844120 0.5617381930 0.5203902125 - 0.5685399175 0.5626296997 0.5213103294 - 0.5693951845 0.5635216832 0.5222290158 - 0.5702500939 0.5644145012 0.5231453776 - 0.5711050034 0.5653073788 0.5240616798 - 0.5719599128 0.5662003160 0.5249779820 - 0.5728148222 0.5670931935 0.5258942842 - 0.5736696720 0.5679860711 0.5268107057 - 0.5745213032 0.5688735843 0.5277202725 - 0.5753722787 0.5697603226 0.5286294222 - 0.5762233734 0.5706470013 0.5295385122 - 0.5770744085 0.5715336800 0.5304474831 - 0.5779255033 0.5724204183 0.5313565731 - 0.5787749290 0.5733029842 0.5322644114 - 0.5796216726 0.5741786957 0.5331705213 - 0.5804684758 0.5750545263 0.5340765715 - 0.5813153982 0.5759302974 0.5349826813 - 0.5821622014 0.5768060088 0.5358887911 - 0.5830090046 0.5776817799 0.5367949009 - 0.5838500261 0.5785498023 0.5377020240 - 0.5846896768 0.5794162750 0.5386093259 - 0.5855293870 0.5802826881 0.5395165086 - 0.5863690972 0.5811491013 0.5404236913 - 0.5872088075 0.5820155144 0.5413309932 - 0.5880478024 0.5828816295 0.5422391295 - 0.5888836980 0.5837466121 0.5431495905 - 0.5897195935 0.5846114755 0.5440601110 - 0.5905554891 0.5854765177 0.5449705720 - 0.5913913250 0.5863415003 0.5458810925 - 0.5922272205 0.5872064233 0.5467916131 - 0.5930604935 0.5880705118 0.5476971865 - 0.5938909054 0.5889335871 0.5485988855 - 0.5947213173 0.5897967815 0.5495005250 - 0.5955516100 0.5906599164 0.5504022241 - 0.5963820219 0.5915231109 0.5513038039 - 0.5972123742 0.5923861861 0.5522055030 - 0.5980378985 0.5932421088 0.5531039834 - 0.5988610983 0.5940948129 0.5540016294 - 0.5996842980 0.5949473977 0.5548992157 - 0.6005076170 0.5958001018 0.5557969213 - 0.6013308167 0.5966528058 0.5566945076 - 0.6021540761 0.5975055099 0.5575920939 - 0.6029741764 0.5983492136 0.5584818721 - 0.6037936807 0.5991911888 0.5593709946 - 0.6046133041 0.6000332236 0.5602599978 - 0.6054328084 0.6008751988 0.5611490011 - 0.6062523127 0.6017171144 0.5620381236 - 0.6070718765 0.6025590897 0.5629271269 - 0.6078854203 0.6033985019 0.5638099909 - 0.6086981297 0.6042374969 0.5646926761 - 0.6095107794 0.6050766110 0.5655754209 - 0.6103236079 0.6059157252 0.5664582253 - 0.6111363173 0.6067547202 0.5673409104 - 0.6119490266 0.6075937748 0.5682235956 - 0.6127572060 0.6084339023 0.5691041946 - 0.6135646105 0.6092742085 0.5699847937 - 0.6143720150 0.6101143956 0.5708652735 - 0.6151794195 0.6109547019 0.5717458725 - 0.6159867048 0.6117950082 0.5726264119 - 0.6167941093 0.6126351953 0.5735070109 - 0.6175978780 0.6134642959 0.5743842125 - 0.6184005141 0.6142898798 0.5752606988 - 0.6192030907 0.6151155829 0.5761371851 - 0.6200057268 0.6159412265 0.5770136714 - 0.6208083034 0.6167668104 0.5778902173 - 0.6216108799 0.6175925136 0.5787667036 - 0.6224128008 0.6184120178 0.5796405077 - 0.6232140064 0.6192271113 0.5805128217 - 0.6240152717 0.6200422049 0.5813850760 - 0.6248164773 0.6208572984 0.5822573900 - 0.6256178021 0.6216725111 0.5831297040 - 0.6264191270 0.6224876046 0.5840020180 - 0.6272190213 0.6233018041 0.5848723054 - 0.6280159950 0.6241139770 0.5857393146 - 0.6288130879 0.6249262094 0.5866062045 - 0.6296101213 0.6257383823 0.5874732137 - 0.6304070950 0.6265506148 0.5883401036 - 0.6312041283 0.6273627877 0.5892071128 - 0.6320011020 0.6281750202 0.5900738835 - 0.6327958107 0.6289898753 0.5909329057 - 0.6335904002 0.6298049092 0.5917918086 - 0.6343849897 0.6306200027 0.5926507711 - 0.6351795197 0.6314349771 0.5935097933 - 0.6359741092 0.6322500706 0.5943686962 - 0.6367686987 0.6330651045 0.5952277184 - 0.6375622153 0.6338756084 0.5960803032 - 0.6383545995 0.6346815228 0.5969281793 - 0.6391469836 0.6354873776 0.5977761149 - 0.6399394870 0.6362932920 0.5986239910 - 0.6407318711 0.6370993257 0.5994718075 - 0.6415243745 0.6379051805 0.6003196836 - 0.6423168182 0.6387110949 0.6011673212 - 0.6431099176 0.6395062804 0.6020082235 - 0.6439030766 0.6403012276 0.6028491259 - 0.6446962953 0.6410961151 0.6036899090 - 0.6454893947 0.6418910027 0.6045308113 - 0.6462826133 0.6426858902 0.6053715944 - 0.6470757127 0.6434808969 0.6062124968 - 0.6478682160 0.6442744732 0.6070520878 - 0.6486594081 0.6450660229 0.6078900099 - 0.6494506001 0.6458573937 0.6087278128 - 0.6502417922 0.6466488242 0.6095656753 - 0.6510329843 0.6474401951 0.6104035974 - 0.6518241763 0.6482316852 0.6112414002 - 0.6526154280 0.6490231156 0.6120793223 - 0.6534075141 0.6498171091 0.6129179001 - 0.6542003155 0.6506124735 0.6137567759 - 0.6549929976 0.6514077783 0.6145955920 - 0.6557856798 0.6522032022 0.6154345274 - 0.6565784216 0.6529986262 0.6162734032 - 0.6573712230 0.6537939906 0.6171122789 - 0.6581639051 0.6545892954 0.6179510951 - 0.6589608788 0.6553863287 0.6187928915 - 0.6597586274 0.6561836004 0.6196351051 - 0.6605563164 0.6569808125 0.6204771996 - 0.6613541245 0.6577780843 0.6213192940 - 0.6621518135 0.6585754156 0.6221613884 - 0.6629495025 0.6593726873 0.6230034828 - 0.6637473106 0.6601698995 0.6238455772 - 0.6645362973 0.6609578133 0.6246849895 - 0.6653249264 0.6617453098 0.6255244017 - 0.6661134958 0.6625326872 0.6263638139 - 0.6669020057 0.6633201241 0.6272032261 - 0.6676905751 0.6641076207 0.6280425787 - 0.6684792042 0.6648949981 0.6288819909 - 0.6692677736 0.6656824946 0.6297214031 - 0.6700507998 0.6664658785 0.6305593252 - 0.6708337069 0.6672493219 0.6313971877 - 0.6716166139 0.6680325866 0.6322351098 - 0.6723995209 0.6688159108 0.6330729723 - 0.6731824279 0.6695992947 0.6339108944 - 0.6739652157 0.6703826189 0.6347488165 - 0.6747481227 0.6711658835 0.6355866790 - 0.6755290031 0.6719505191 0.6364173889 - 0.6763095856 0.6727352142 0.6372476220 - 0.6770902276 0.6735200286 0.6380779147 - 0.6778709292 0.6743047237 0.6389080882 - 0.6786515117 0.6750894189 0.6397383809 - 0.6794320941 0.6758741140 0.6405686140 - 0.6802126765 0.6766588092 0.6413989067 - 0.6809899211 0.6774402261 0.6422249079 - 0.6817654967 0.6782202721 0.6430497169 - 0.6825411916 0.6790003181 0.6438744068 - 0.6833168864 0.6797804236 0.6446992159 - 0.6840925813 0.6805605292 0.6455240250 - 0.6848682761 0.6813405156 0.6463487148 - 0.6856439710 0.6821206212 0.6471735239 - 0.6864163876 0.6828963161 0.6479974985 - 0.6871845126 0.6836665869 0.6488205791 - 0.6879525781 0.6844369173 0.6496437788 - 0.6887208223 0.6852071881 0.6504669189 - 0.6894888878 0.6859775186 0.6512901187 - 0.6902570128 0.6867477894 0.6521131992 - 0.6910251975 0.6875181198 0.6529363990 - 0.6917930245 0.6882880926 0.6537595987 - 0.6925569773 0.6890543103 0.6545832753 - 0.6933209896 0.6898205280 0.6554070115 - 0.6940848827 0.6905866861 0.6562306881 - 0.6948488951 0.6913527846 0.6570544243 - 0.6956127882 0.6921190023 0.6578781009 - 0.6963768005 0.6928852201 0.6587017775 - 0.6971408129 0.6936513782 0.6595255136 - 0.6979042888 0.6944183111 0.6603466868 - 0.6986675262 0.6951858997 0.6611664891 - 0.6994307041 0.6959534287 0.6619862914 - 0.7001938820 0.6967208982 0.6628060937 - 0.7009570003 0.6974884272 0.6636258960 - 0.7017201781 0.6982558966 0.6644456983 - 0.7024834156 0.6990234852 0.6652655005 - 0.7032464743 0.6997908950 0.6660854220 - 0.7040007114 0.7005546093 0.6669068933 - 0.7047548890 0.7013183832 0.6677284837 - 0.7055091262 0.7020820975 0.6685500145 - 0.7062633038 0.7028458118 0.6693716049 - 0.7070174813 0.7036095262 0.6701930761 - 0.7077717185 0.7043731809 0.6710147262 - 0.7085258961 0.7051368952 0.6718361974 - 0.7092772126 0.7058975101 0.6726554036 - 0.7100226283 0.7066521049 0.6734707952 - 0.7107681036 0.7074066997 0.6742863059 - 0.7115135789 0.7081612945 0.6751016974 - 0.7122589946 0.7089158893 0.6759172082 - 0.7130045295 0.7096704841 0.6767325997 - 0.7137500048 0.7104250789 0.6775481105 - 0.7144954205 0.7111796737 0.6783635020 - 0.7152379155 0.7119320035 0.6791734099 - 0.7159780264 0.7126826048 0.6799798012 - 0.7167180777 0.7134332061 0.6807861924 - 0.7174581885 0.7141839266 0.6815925837 - 0.7181981802 0.7149345279 0.6823989749 - 0.7189382911 0.7156851292 0.6832054257 - 0.7196784019 0.7164356709 0.6840116978 - 0.7204185128 0.7171862721 0.6848180890 - 0.7211552262 0.7179378271 0.6856207252 - 0.7218902111 0.7186897993 0.6864218116 - 0.7226251960 0.7194417119 0.6872228980 - 0.7233601809 0.7201936841 0.6880239844 - 0.7240952253 0.7209457159 0.6888250709 - 0.7248302102 0.7216976285 0.6896262169 - 0.7255653143 0.7224496007 0.6904273033 - 0.7263002992 0.7232015729 0.6912283897 - 0.7270305157 0.7239469290 0.6920284033 - 0.7277582884 0.7246891260 0.6928278804 - 0.7284861803 0.7254313231 0.6936274767 - 0.7292140722 0.7261735201 0.6944270134 - 0.7299419045 0.7269157171 0.6952266097 - 0.7306697965 0.7276579142 0.6960262060 - 0.7313976884 0.7284001708 0.6968256831 - 0.7321255803 0.7291424274 0.6976252794 - 0.7328494787 0.7298778892 0.6984261870 - 0.7335705757 0.7306084037 0.6992279887 - 0.7342916727 0.7313389778 0.7000297904 - 0.7350128293 0.7320696115 0.7008314729 - 0.7357339263 0.7328001857 0.7016332746 - 0.7364550233 0.7335308194 0.7024350762 - 0.7371760011 0.7342612743 0.7032368183 - 0.7378970981 0.7349919081 0.7040386200 - 0.7386168838 0.7357206941 0.7048401237 - 0.7393341064 0.7364463806 0.7056412101 - 0.7400513887 0.7371721268 0.7064422965 - 0.7407686710 0.7378978729 0.7072433829 - 0.7414860129 0.7386236191 0.7080445290 - 0.7422032952 0.7393493056 0.7088456154 - 0.7429205775 0.7400749922 0.7096467018 - 0.7436379194 0.7408006787 0.7104477882 - 0.7443552017 0.7415264249 0.7112485170 - 0.7450708747 0.7422525287 0.7120447159 - 0.7457867265 0.7429785728 0.7128409147 - 0.7465025187 0.7437046766 0.7136371136 - 0.7472183108 0.7444307804 0.7144333720 - 0.7479341030 0.7451568842 0.7152295709 - 0.7486497760 0.7458829880 0.7160258293 - 0.7493656278 0.7466090918 0.7168220282 - 0.7500814199 0.7473351955 0.7176182270 - 0.7507966161 0.7480586171 0.7184088230 - 0.7515113950 0.7487803102 0.7191962004 - 0.7522261739 0.7495018840 0.7199836969 - 0.7529410124 0.7502235770 0.7207711935 - 0.7536557913 0.7509452105 0.7215585709 - 0.7543706298 0.7516667843 0.7223461270 - 0.7550854087 0.7523884773 0.7231336236 - 0.7558001876 0.7531101108 0.7239210010 - 0.7565147281 0.7538313270 0.7247081995 - 0.7572231293 0.7545434237 0.7254921198 - 0.7579314709 0.7552555203 0.7262759805 - 0.7586399913 0.7559676766 0.7270599008 - 0.7593483925 0.7566797733 0.7278438807 - 0.7600567937 0.7573919296 0.7286278009 - 0.7607651949 0.7581040263 0.7294117212 - 0.7614735961 0.7588161826 0.7301957011 - 0.7621819973 0.7595282793 0.7309796214 - 0.7628899217 0.7602388263 0.7317612767 - 0.7635970116 0.7609465122 0.7325400710 - 0.7643041015 0.7616543174 0.7333188057 - 0.7650110722 0.7623620033 0.7340976000 - 0.7657182217 0.7630698085 0.7348762751 - 0.7664251924 0.7637774944 0.7356551290 - 0.7671322823 0.7644852996 0.7364339232 - 0.7678393126 0.7651931047 0.7372125983 - 0.7685464025 0.7659007907 0.7379913926 - 0.7692546248 0.7666099072 0.7387691736 - 0.7699635029 0.7673199773 0.7395464182 - 0.7706725001 0.7680299878 0.7403236032 - 0.7713813782 0.7687399983 0.7411009073 - 0.7720903158 0.7694501281 0.7418780923 - 0.7727993131 0.7701600790 0.7426552773 - 0.7735081911 0.7708700895 0.7434325814 - 0.7742171884 0.7715802193 0.7442098260 - 0.7749261260 0.7722902298 0.7449870110 - 0.7756347060 0.7730041146 0.7457646132 - 0.7763432264 0.7737196088 0.7465423942 - 0.7770516276 0.7744352221 0.7473201156 - 0.7777600884 0.7751507163 0.7480978966 - 0.7784684896 0.7758663297 0.7488756180 - 0.7791770101 0.7765818238 0.7496532798 - 0.7798854709 0.7772973776 0.7504311204 - 0.7805938721 0.7780128717 0.7512087822 - 0.7813023925 0.7787284851 0.7519865036 - 0.7820088267 0.7794386744 0.7527605891 - 0.7827143073 0.7801463008 0.7535331249 - 0.7834197283 0.7808538079 0.7543057203 - 0.7841252089 0.7815613151 0.7550783157 - 0.7848305702 0.7822688818 0.7558507919 - 0.7855361104 0.7829763889 0.7566233873 - 0.7862414718 0.7836840153 0.7573959231 - 0.7869470119 0.7843915224 0.7581685185 - 0.7876523733 0.7850990295 0.7589409947 - 0.7883579135 0.7858042717 0.7597122192 - 0.7890635133 0.7865074277 0.7604821920 - 0.7897689939 0.7872105837 0.7612522244 - 0.7904744744 0.7879136801 0.7620223165 - 0.7911800146 0.7886168957 0.7627922893 - 0.7918856144 0.7893199921 0.7635623813 - 0.7925910950 0.7900232077 0.7643324137 - 0.7932965755 0.7907263041 0.7651025057 - 0.7940021753 0.7914295197 0.7658724785 - 0.7947084904 0.7921326756 0.7666410804 - 0.7954170704 0.7928364277 0.7674059868 - 0.7961258292 0.7935400009 0.7681708932 - 0.7968344092 0.7942435741 0.7689357996 - 0.7975429893 0.7949472070 0.7697008252 - 0.7982516885 0.7956508994 0.7704656720 - 0.7989603281 0.7963544726 0.7712305784 - 0.7996690273 0.7970581055 0.7719954848 - 0.8003776073 0.7977616787 0.7727603912 - 0.8010861874 0.7984653115 0.7735252976 - 0.8017868996 0.7991653085 0.7742831707 - 0.8024861813 0.7998644710 0.7750402093 - 0.8031854033 0.8005638123 0.7757971287 - 0.8038846254 0.8012630939 0.7765541077 - 0.8045839071 0.8019623756 0.7773110271 - 0.8052831292 0.8026615977 0.7780680060 - 0.8059822917 0.8033608794 0.7788249850 - 0.8066815734 0.8040602207 0.7795819044 - 0.8073807955 0.8047593832 0.7803388834 - 0.8080779910 0.8054556251 0.7810941935 - 0.8087708950 0.8061454892 0.7818467021 - 0.8094639182 0.8068354130 0.7825992703 - 0.8101568222 0.8075252175 0.7833517790 - 0.8108497262 0.8082150817 0.7841042876 - 0.8115425706 0.8089050055 0.7848567963 - 0.8122354746 0.8095948100 0.7856093049 - 0.8129284978 0.8102846742 0.7863618135 - 0.8136214018 0.8109744787 0.7871143222 - 0.8143143058 0.8116644025 0.7878667712 - 0.8150073290 0.8123514056 0.7886186838 - 0.8157002926 0.8130369186 0.7893702984 - 0.8163931966 0.8137224913 0.7901219130 - 0.8170862198 0.8144080043 0.7908735275 - 0.8177791834 0.8150935173 0.7916250229 - 0.8184722066 0.8157790899 0.7923765779 - 0.8191652298 0.8164646029 0.7931281924 - 0.8198580742 0.8171501160 0.7938798070 - 0.8205510974 0.8178356886 0.7946314216 - 0.8212441206 0.8185212016 0.7953829765 - 0.8219397068 0.8192058802 0.7961322069 - 0.8226357102 0.8198903799 0.7968811989 - 0.8233315945 0.8205748796 0.7976301908 - 0.8240275979 0.8212594986 0.7983791828 - 0.8247236013 0.8219439983 0.7991281748 - 0.8254194856 0.8226286173 0.7998772264 - 0.8261154890 0.8233131170 0.8006262183 - 0.8268113732 0.8239976168 0.8013753295 - 0.8275073767 0.8246821761 0.8021243215 - 0.8282033801 0.8253666759 0.8028733134 - 0.8288903832 0.8260424733 0.8036262989 - 0.8295770884 0.8267179132 0.8043792844 - 0.8302637935 0.8273934126 0.8051323295 - 0.8309504986 0.8280689120 0.8058853149 - 0.8316372037 0.8287442923 0.8066383004 - 0.8323239088 0.8294197917 0.8073912859 - 0.8330106735 0.8300951719 0.8081442714 - 0.8336973786 0.8307706714 0.8088973165 - 0.8343840837 0.8314461708 0.8096503019 - 0.8350707889 0.8321216106 0.8104032874 - 0.8357518911 0.8327894211 0.8111593127 - 0.8364323974 0.8334565163 0.8119155765 - 0.8371130228 0.8341234922 0.8126717806 - 0.8377935290 0.8347905874 0.8134279847 - 0.8384740949 0.8354576230 0.8141843081 - 0.8391546011 0.8361247182 0.8149405122 - 0.8398352265 0.8367916942 0.8156967759 - 0.8405156732 0.8374587893 0.8164529800 - 0.8411962986 0.8381258249 0.8172091842 - 0.8418768048 0.8387929201 0.8179655075 - 0.8425561786 0.8394573927 0.8187175989 - 0.8432351947 0.8401209712 0.8194684982 - 0.8439140916 0.8407846093 0.8202192783 - 0.8445931077 0.8414481878 0.8209701180 - 0.8452720046 0.8421118259 0.8217208982 - 0.8459510207 0.8427755237 0.8224717975 - 0.8466299176 0.8434391022 0.8232225776 - 0.8473088741 0.8441026807 0.8239734173 - 0.8479878902 0.8447663188 0.8247241974 - 0.8486667871 0.8454298973 0.8254750967 - 0.8493432999 0.8460934162 0.8262230754 - 0.8500158787 0.8467569947 0.8269672990 - 0.8506883979 0.8474205136 0.8277115226 - 0.8513609767 0.8480839729 0.8284556866 - 0.8520336151 0.8487474918 0.8291999102 - 0.8527061939 0.8494110107 0.8299440742 - 0.8533787727 0.8500745296 0.8306882977 - 0.8540514112 0.8507381082 0.8314325809 - 0.8547239900 0.8514016271 0.8321768045 - 0.8553965092 0.8520650864 0.8329210281 - 0.8560690880 0.8527286053 0.8336651921 - 0.8567318916 0.8533846140 0.8344091177 - 0.8573938012 0.8540399075 0.8351529241 - 0.8580557108 0.8546953201 0.8358967900 - 0.8587176204 0.8553506136 0.8366405964 - 0.8593795896 0.8560060263 0.8373845220 - 0.8600414991 0.8566613197 0.8381283879 - 0.8607034087 0.8573166728 0.8388721943 - 0.8613653183 0.8579720259 0.8396161199 - 0.8620272875 0.8586273789 0.8403599262 - 0.8626891971 0.8592826724 0.8411037922 - 0.8633491993 0.8599355817 0.8418468833 - 0.8640050292 0.8605830073 0.8425887227 - 0.8646607995 0.8612304926 0.8433305025 - 0.8653166294 0.8618779778 0.8440722823 - 0.8659723997 0.8625254035 0.8448141217 - 0.8666282296 0.8631728888 0.8455559015 - 0.8672839999 0.8638203740 0.8462976813 - 0.8679397702 0.8644677997 0.8470395207 - 0.8685956001 0.8651152849 0.8477811813 - 0.8692513704 0.8657627106 0.8485230207 - 0.8699072003 0.8664101958 0.8492648005 - 0.8705629706 0.8670558929 0.8500074148 - 0.8712186813 0.8677002788 0.8507503867 - 0.8718745112 0.8683447242 0.8514934778 - 0.8725302219 0.8689891100 0.8522366285 - 0.8731859922 0.8696334958 0.8529797196 - 0.8738417029 0.8702778220 0.8537226915 - 0.8744974732 0.8709222078 0.8544657826 - 0.8751531839 0.8715665936 0.8552088737 - 0.8758090138 0.8722109795 0.8559520245 - 0.8764647841 0.8728554249 0.8566949964 - 0.8771204948 0.8734998107 0.8574380875 - 0.8777772188 0.8741462231 0.8581804037 - 0.8784344792 0.8747938275 0.8589221835 - 0.8790916800 0.8754413128 0.8596640825 - 0.8797490001 0.8760887980 0.8604059219 - 0.8804062009 0.8767364025 0.8611478209 - 0.8810635209 0.8773838878 0.8618897200 - 0.8817207217 0.8780313730 0.8626314998 - 0.8823779225 0.8786789775 0.8633733988 - 0.8830351830 0.8793265224 0.8641152978 - 0.8836923838 0.8799740076 0.8648570776 - 0.8843497038 0.8806216121 0.8655989766 - 0.8850082159 0.8812701106 0.8663362861 - 0.8856676221 0.8819192052 0.8670710921 - 0.8863270283 0.8825684190 0.8678058982 - 0.8869863749 0.8832175136 0.8685407043 - 0.8876457810 0.8838667274 0.8692755103 - 0.8883051276 0.8845158219 0.8700103164 - 0.8889644742 0.8851649165 0.8707451224 - 0.8896238804 0.8858140707 0.8714798093 - 0.8902832866 0.8864632249 0.8722146153 - 0.8909426928 0.8871123791 0.8729494214 - 0.8916019797 0.8877614737 0.8736842275 - 0.8922591805 0.8884071708 0.8744199872 - 0.8929132223 0.8890480995 0.8751569986 - 0.8935672045 0.8896890879 0.8758940101 - 0.8942213058 0.8903300166 0.8766310811 - 0.8948752880 0.8909708858 0.8773680925 - 0.8955293298 0.8916118145 0.8781051040 - 0.8961833119 0.8922526836 0.8788421750 - 0.8968374133 0.8928936124 0.8795791864 - 0.8974913955 0.8935344815 0.8803161979 - 0.8981453776 0.8941755295 0.8810533285 - 0.8987994194 0.8948163986 0.8817902803 - 0.8994534016 0.8954570889 0.8825269938 - 0.9001063704 0.8960943818 0.8832603097 - 0.9007593989 0.8967316747 0.8839936852 - 0.9014124274 0.8973690271 0.8847270012 - 0.9020653963 0.8980063200 0.8854603171 - 0.9027184248 0.8986436129 0.8861935735 - 0.9033715129 0.8992809057 0.8869268894 - 0.9040244818 0.8999181986 0.8876603246 - 0.9046775103 0.9005554914 0.8883935809 - 0.9053304791 0.9011927843 0.8891268969 - 0.9059835076 0.9018300772 0.8898602128 - 0.9066364765 0.9024674296 0.8905935287 - 0.9072905779 0.9031060934 0.8913232088 - 0.9079453945 0.9037460089 0.8920502067 - 0.9086002707 0.9043858051 0.8927773237 - 0.9092550874 0.9050256014 0.8935043216 - 0.9099100232 0.9056653976 0.8942313790 - 0.9105647802 0.9063051939 0.8949583769 - 0.9112197161 0.9069449902 0.8956853747 - 0.9118745923 0.9075847864 0.8964124918 - 0.9125294089 0.9082245827 0.8971394897 - 0.9131842852 0.9088644981 0.8978664875 - 0.9138391018 0.9095042944 0.8985936046 - 0.9144939780 0.9101440907 0.8993206024 - 0.9151384234 0.9107882977 0.9000440240 - 0.9157816768 0.9114329815 0.9007670879 - 0.9164249897 0.9120777249 0.9014902115 - 0.9170683026 0.9127225280 0.9022132754 - 0.9177114964 0.9133672118 0.9029362798 - 0.9183548093 0.9140118957 0.9036594033 - 0.9189981222 0.9146565795 0.9043825269 - 0.9196413755 0.9153013229 0.9051055908 - 0.9202846885 0.9159461260 0.9058287144 - 0.9209278822 0.9165908098 0.9065517783 - 0.9215711951 0.9172354937 0.9072747827 - 0.9222133756 0.9178792834 0.9079968929 - 0.9228429198 0.9185125828 0.9087103009 - 0.9234725237 0.9191460013 0.9094237089 - 0.9241020083 0.9197793007 0.9101369977 - 0.9247316122 0.9204127192 0.9108504057 - 0.9253610969 0.9210460186 0.9115636945 - 0.9259907007 0.9216793776 0.9122771025 - 0.9266201854 0.9223126769 0.9129905105 - 0.9272497892 0.9229460955 0.9137037992 - 0.9278793931 0.9235793948 0.9144172072 - 0.9285088778 0.9242128134 0.9151304960 - 0.9291384816 0.9248461723 0.9158439040 - 0.9297673106 0.9254785180 0.9165560007 - 0.9303902984 0.9261021018 0.9172585011 - 0.9310132265 0.9267256856 0.9179610014 - 0.9316362143 0.9273493290 0.9186636209 - 0.9322590828 0.9279729128 0.9193661213 - 0.9328820705 0.9285964966 0.9200686216 - 0.9335051179 0.9292201996 0.9207711816 - 0.9341279864 0.9298437834 0.9214736819 - 0.9347509742 0.9304674268 0.9221761823 - 0.9353739023 0.9310910106 0.9228788018 - 0.9359968901 0.9317145944 0.9235813022 - 0.9366198182 0.9323381782 0.9242838025 - 0.9372428060 0.9329618216 0.9249864221 - 0.9378637075 0.9335851073 0.9256830812 - 0.9384844899 0.9342085123 0.9263797998 - 0.9391052723 0.9348317981 0.9270763993 - 0.9397261143 0.9354550838 0.9277731180 - 0.9403468966 0.9360784292 0.9284697175 - 0.9409676790 0.9367017150 0.9291663170 - 0.9415885210 0.9373250008 0.9298629761 - 0.9422093034 0.9379482865 0.9305595756 - 0.9428302050 0.9385716915 0.9312562943 - 0.9434509873 0.9391949773 0.9319528937 - 0.9440718293 0.9398183227 0.9326494932 - 0.9446926117 0.9404416084 0.9333462119 - 0.9453073740 0.9410626292 0.9340357780 - 0.9459190965 0.9416825175 0.9347218871 - 0.9465308189 0.9423022866 0.9354081154 - 0.9471424818 0.9429221749 0.9360942245 - 0.9477542043 0.9435420036 0.9367803931 - 0.9483659267 0.9441617727 0.9374665022 - 0.9489775896 0.9447817206 0.9381526709 - 0.9495893121 0.9454014897 0.9388387799 - 0.9502009749 0.9460213780 0.9395250082 - 0.9508126974 0.9466412067 0.9402111173 - 0.9514244199 0.9472610950 0.9408972859 - 0.9520360827 0.9478809237 0.9415833950 - 0.9526458979 0.9484993815 0.9422674179 - 0.9532474279 0.9491121769 0.9429422021 - 0.9538488984 0.9497249126 0.9436171055 - 0.9544504285 0.9503375888 0.9442920089 - 0.9550518990 0.9509503245 0.9449669123 - 0.9556534290 0.9515630007 0.9456418157 - 0.9562548995 0.9521757960 0.9463167191 - 0.9568564296 0.9527884722 0.9469915032 - 0.9574579000 0.9534012079 0.9476664066 - 0.9580594897 0.9540138841 0.9483413100 - 0.9586610198 0.9546266794 0.9490162134 - 0.9592624903 0.9552394152 0.9496911168 - 0.9598640203 0.9558520913 0.9503659010 - 0.9604597092 0.9564607143 0.9510350823 - 0.9610503912 0.9570658207 0.9516996741 - 0.9616411924 0.9576709270 0.9523642063 - 0.9622319937 0.9582759738 0.9530286789 - 0.9628227949 0.9588810802 0.9536932111 - 0.9634134769 0.9594861865 0.9543578029 - 0.9640042782 0.9600912929 0.9550222754 - 0.9645950794 0.9606963992 0.9556868076 - 0.9651858211 0.9613015056 0.9563512802 - 0.9657766223 0.9619066119 0.9570158720 - 0.9663674235 0.9625117183 0.9576804042 - 0.9669582248 0.9631168246 0.9583448768 - 0.9675489068 0.9637219906 0.9590094090 - 0.9681262970 0.9643182158 0.9596667290 - 0.9686986208 0.9649111032 0.9603213072 - 0.9692708254 0.9655039907 0.9609760046 - 0.9698430896 0.9660969973 0.9616305828 - 0.9704154134 0.9666898847 0.9622852206 - 0.9709876776 0.9672827721 0.9629399180 - 0.9715598822 0.9678757191 0.9635944963 - 0.9721322060 0.9684687257 0.9642490745 - 0.9727045298 0.9690616131 0.9649037719 - 0.9732767940 0.9696545005 0.9655584097 - 0.9738491178 0.9702473879 0.9662129879 - 0.9744213223 0.9708403945 0.9668676853 - 0.9749935865 0.9714332819 0.9675223231 - 0.9755455256 0.9720122814 0.9681680202 - 0.9760900736 0.9725863934 0.9688106179 - 0.9766348004 0.9731606245 0.9694532156 - 0.9771794081 0.9737346768 0.9700958729 - 0.9777240753 0.9743087888 0.9707384706 - 0.9782686830 0.9748830199 0.9713811278 - 0.9788134098 0.9754570723 0.9720237255 - 0.9793580174 0.9760311842 0.9726663828 - 0.9799026251 0.9766052961 0.9733089805 - 0.9804472923 0.9771795273 0.9739515781 - 0.9809919000 0.9777535796 0.9745941758 - 0.9815366268 0.9783276916 0.9752368927 - 0.9820811749 0.9789019227 0.9758794904 - 0.9826055765 0.9794644713 0.9765092134 - 0.9831150770 0.9800186753 0.9771293998 - 0.9836245775 0.9805728197 0.9777497053 - 0.9841340184 0.9811270237 0.9783700109 - 0.9846435189 0.9816811085 0.9789903164 - 0.9851529002 0.9822353125 0.9796106219 - 0.9856624007 0.9827893972 0.9802309275 - 0.9861719012 0.9833436012 0.9808511138 - 0.9866812825 0.9838976860 0.9814714193 - 0.9871907830 0.9844518900 0.9820917249 - 0.9877002239 0.9850059748 0.9827119708 - 0.9882097244 0.9855601788 0.9833322763 - 0.9887192249 0.9861143231 0.9839525223 - 0.9892141223 0.9866619110 0.9845647812 - 0.9896609783 0.9871875048 0.9851508737 - 0.9901077747 0.9877132177 0.9857370853 - 0.9905545712 0.9882388115 0.9863231778 - 0.9910014272 0.9887645245 0.9869092703 - 0.9914482832 0.9892901182 0.9874954224 - 0.9918950796 0.9898157716 0.9880815148 - 0.9923418760 0.9903414249 0.9886677265 - 0.9927887917 0.9908670783 0.9892538190 - 0.9932355881 0.9913926721 0.9898399115 - 0.9936823845 0.9919183850 0.9904260039 - 0.9941291809 0.9924439788 0.9910122156 - 0.9945760965 0.9929696918 0.9915983081 - 0.9950228930 0.9934952855 0.9921844006 - 0.9954037070 0.9939768910 0.9927504063 - 0.9957572818 0.9944401979 0.9933080077 - 0.9961109161 0.9949035048 0.9938657284 - 0.9964643717 0.9953668118 0.9944233894 - 0.9968180060 0.9958301187 0.9949809909 - 0.9971715212 0.9962934256 0.9955387115 - 0.9975250959 0.9967567921 0.9960963130 - 0.9978786111 0.9972200990 0.9966539741 - 0.9982321858 0.9976834059 0.9972116947 - 0.9985858202 0.9981467128 0.9977692962 - 0.9989392757 0.9986100197 0.9983270168 - 0.9992929101 0.9990733862 0.9988846779 - 0.9996464252 0.9995366931 0.9994422793 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0077106012 0.0070298198 0.0049559241 + 0.0147110997 0.0133244200 0.0095438547 + 0.0209813993 0.0190351103 0.0137957605 + 0.0266668592 0.0242017098 0.0177728795 + 0.0318832099 0.0289859492 0.0215300098 + 0.0367198400 0.0334706604 0.0251365695 + 0.0412060991 0.0377071798 0.0285660401 + 0.0454186983 0.0417603701 0.0318240002 + 0.0495144203 0.0456782803 0.0349723399 + 0.0534897000 0.0494300202 0.0380599014 + 0.0572170094 0.0530248806 0.0410333909 + 0.0607506298 0.0564629287 0.0438579209 + 0.0642050877 0.0597890504 0.0466150604 + 0.0676248968 0.0630782619 0.0493207090 + 0.0709792525 0.0663433895 0.0519826487 + 0.0742666870 0.0695055276 0.0545649193 + 0.0774300173 0.0725333989 0.0570865013 + 0.0805151016 0.0754740685 0.0595324002 + 0.0835210085 0.0783350095 0.0619145297 + 0.0864818692 0.0811579376 0.0642616600 + 0.0894213691 0.0839637592 0.0665879101 + 0.0923196077 0.0867140666 0.0689035431 + 0.0951313078 0.0894301310 0.0712108612 + 0.0978739634 0.0921017230 0.0734841302 + 0.1005603969 0.0947171375 0.0757039636 + 0.1032031998 0.0972908884 0.0778791532 + 0.1058200002 0.0998220220 0.0800394490 + 0.1084190011 0.1023029983 0.0821614563 + 0.1110042036 0.1047497019 0.0842550322 + 0.1135606021 0.1071517989 0.0863265395 + 0.1160767004 0.1095222011 0.0883673579 + 0.1185398027 0.1118747965 0.0903817788 + 0.1209601983 0.1141934991 0.0923686922 + 0.1233263016 0.1164946035 0.0943281874 + 0.1256539971 0.1187560037 0.0962788835 + 0.1279318035 0.1210196018 0.0982189104 + 0.1301839054 0.1232483014 0.1001423001 + 0.1324169934 0.1254474968 0.1020568013 + 0.1346450001 0.1275981963 0.1039576009 + 0.1368439049 0.1297464967 0.1058444977 + 0.1390296966 0.1318780035 0.1077136993 + 0.1412141025 0.1339951009 0.1095665023 + 0.1433850974 0.1361036003 0.1114090011 + 0.1455492973 0.1381817013 0.1132375002 + 0.1476995945 0.1402491927 0.1150472015 + 0.1498260051 0.1422760040 0.1168475971 + 0.1519210935 0.1442877054 0.1186361015 + 0.1539843976 0.1462759972 0.1204114035 + 0.1560284048 0.1482367963 0.1221595034 + 0.1580577046 0.1501837969 0.1238816977 + 0.1600770950 0.1520957053 0.1255871058 + 0.1620800942 0.1539966017 0.1272871047 + 0.1640495956 0.1559009999 0.1289671063 + 0.1659898013 0.1577883065 0.1306212991 + 0.1679138988 0.1596692055 0.1322596073 + 0.1698164046 0.1615532041 0.1338987052 + 0.1716932058 0.1634092033 0.1355329007 + 0.1735651940 0.1652536988 0.1371469051 + 0.1754256934 0.1670915931 0.1387543976 + 0.1772599965 0.1689036936 0.1403618008 + 0.1790851951 0.1707046032 0.1419776976 + 0.1808962971 0.1724977046 0.1435960978 + 0.1826874018 0.1742753983 0.1452053040 + 0.1844758987 0.1760344952 0.1468037963 + 0.1862519979 0.1777956039 0.1483945996 + 0.1880110949 0.1795548946 0.1499821991 + 0.1897681952 0.1812911928 0.1515644044 + 0.1915334016 0.1830188930 0.1531336010 + 0.1933026016 0.1847521067 0.1546926945 + 0.1950711012 0.1864724010 0.1562453955 + 0.1968317926 0.1881736070 0.1577879936 + 0.1985855997 0.1898707002 0.1593212038 + 0.2003341019 0.1915744990 0.1608411968 + 0.2020726055 0.1932611018 0.1623475999 + 0.2038006037 0.1949308962 0.1638498008 + 0.2055211961 0.1965949982 0.1653402001 + 0.2072315067 0.1982464939 0.1668276936 + 0.2089259028 0.1998752952 0.1683100015 + 0.2106073052 0.2014864981 0.1697766036 + 0.2122824937 0.2030902952 0.1712321937 + 0.2139520049 0.2046858966 0.1726851016 + 0.2156112045 0.2062691003 0.1741385013 + 0.2172587067 0.2078413963 0.1755896062 + 0.2189002037 0.2094067931 0.1770340055 + 0.2205414027 0.2109739035 0.1784697026 + 0.2221654058 0.2125506997 0.1799007058 + 0.2237752974 0.2141104043 0.1813267022 + 0.2253734022 0.2156585008 0.1827555001 + 0.2269579023 0.2172029018 0.1841821969 + 0.2285311073 0.2187507004 0.1856049001 + 0.2301028073 0.2203052938 0.1870242953 + 0.2316679060 0.2218398005 0.1884412020 + 0.2332272977 0.2233646065 0.1898476034 + 0.2347781956 0.2248875052 0.1912508011 + 0.2363210022 0.2264139950 0.1926520020 + 0.2378583997 0.2279502004 0.1940511018 + 0.2393900007 0.2294773012 0.1954436004 + 0.2409213930 0.2309951931 0.1968320012 + 0.2424533069 0.2325080037 0.1982160956 + 0.2439856976 0.2340212017 0.1995960027 + 0.2455042005 0.2355341017 0.2009744048 + 0.2470149994 0.2370381951 0.2023515999 + 0.2485187054 0.2385327965 0.2037250996 + 0.2500135005 0.2400144935 0.2050911933 + 0.2515035868 0.2414941937 0.2064488977 + 0.2529886961 0.2429734021 0.2078043073 + 0.2544668913 0.2444527000 0.2091580927 + 0.2559421062 0.2459246963 0.2105102986 + 0.2574181855 0.2473870963 0.2118566930 + 0.2588958144 0.2488387972 0.2131976932 + 0.2603701949 0.2502841949 0.2145289928 + 0.2618409991 0.2517311871 0.2158571929 + 0.2633078098 0.2531799972 0.2171828002 + 0.2647666931 0.2546122968 0.2185021937 + 0.2662197948 0.2560352981 0.2198203057 + 0.2676647902 0.2574462891 0.2211371064 + 0.2690947056 0.2588512003 0.2224553972 + 0.2705186903 0.2602562904 0.2237727940 + 0.2719335854 0.2616656125 0.2250885069 + 0.2733449936 0.2630712092 0.2264019996 + 0.2747541964 0.2644746006 0.2277133018 + 0.2761535943 0.2658649981 0.2290149033 + 0.2775473893 0.2672511041 0.2303117961 + 0.2789311111 0.2686300874 0.2316035926 + 0.2803080976 0.2700101137 0.2328924984 + 0.2816820145 0.2713905871 0.2341804951 + 0.2830528915 0.2727720141 0.2354660928 + 0.2844218910 0.2741498053 0.2367486060 + 0.2857857049 0.2755183876 0.2380257994 + 0.2871482074 0.2768800855 0.2393009067 + 0.2885094881 0.2782357037 0.2405748963 + 0.2898694873 0.2795907855 0.2418427020 + 0.2912288904 0.2809456885 0.2431090027 + 0.2925894856 0.2822988927 0.2443675995 + 0.2939479947 0.2836497128 0.2456243038 + 0.2952952981 0.2849884033 0.2468754947 + 0.2966383994 0.2863219976 0.2481230944 + 0.2979727983 0.2876453102 0.2493657023 + 0.2993055880 0.2889670134 0.2506079078 + 0.3006362915 0.2902868092 0.2518497109 + 0.3019703031 0.2916113138 0.2530942857 + 0.3033066094 0.2929391861 0.2543402910 + 0.3046467900 0.2942663133 0.2555767894 + 0.3059892058 0.2955929935 0.2568103075 + 0.3073258102 0.2969133854 0.2580339909 + 0.3086602092 0.2982315123 0.2592557073 + 0.3099907041 0.2995441854 0.2604722977 + 0.3113198876 0.3008551896 0.2616879940 + 0.3126456141 0.3021624088 0.2629038990 + 0.3139702082 0.3034684956 0.2641198039 + 0.3152804971 0.3047659099 0.2653389871 + 0.3165856004 0.3060601950 0.2665588856 + 0.3178817928 0.3073394001 0.2677814960 + 0.3191738129 0.3086116910 0.2690047026 + 0.3204653859 0.3098776937 0.2702280879 + 0.3217566907 0.3111397028 0.2714515030 + 0.3230504096 0.3124023974 0.2726680934 + 0.3243465126 0.3136656880 0.2738803029 + 0.3256357014 0.3149282932 0.2750887871 + 0.3269129992 0.3161900043 0.2762930095 + 0.3281877041 0.3174473047 0.2774964869 + 0.3294523060 0.3186891079 0.2786985040 + 0.3307167888 0.3199306130 0.2799009085 + 0.3319742084 0.3211612105 0.2811062932 + 0.3332315981 0.3223916888 0.2823117077 + 0.3344798982 0.3236224949 0.2835228145 + 0.3357258141 0.3248533905 0.2847346067 + 0.3369632959 0.3260852993 0.2859402895 + 0.3381935060 0.3273181915 0.2871426940 + 0.3394210935 0.3285504878 0.2883437872 + 0.3406409919 0.3297812939 0.2895427942 + 0.3418608904 0.3310121894 0.2907412052 + 0.3430762887 0.3322376013 0.2919301987 + 0.3442913890 0.3334628046 0.2931191921 + 0.3455013931 0.3346853852 0.2943033874 + 0.3467079103 0.3359062970 0.2954855859 + 0.3479113877 0.3371267021 0.2966673970 + 0.3491044044 0.3383455873 0.2978483140 + 0.3502975106 0.3395645022 0.2990292907 + 0.3514811993 0.3407759070 0.3002046943 + 0.3526628017 0.3419859111 0.3013797998 + 0.3538405001 0.3431887925 0.3025487959 + 0.3550119102 0.3443804085 0.3037114143 + 0.3561832011 0.3455719948 0.3048739135 + 0.3573499024 0.3467567861 0.3060263097 + 0.3585161865 0.3479408920 0.3071787059 + 0.3596819937 0.3491263092 0.3083275855 + 0.3608472049 0.3503136933 0.3094730973 + 0.3620123863 0.3515009880 0.3106186092 + 0.3631744087 0.3526912928 0.3117532134 + 0.3643358946 0.3538821042 0.3128874004 + 0.3654957116 0.3550710082 0.3140192926 + 0.3666515052 0.3562552929 0.3151476085 + 0.3678072095 0.3574396968 0.3162758946 + 0.3689568043 0.3586226106 0.3174037933 + 0.3701032996 0.3598049879 0.3185315132 + 0.3712497056 0.3609873056 0.3196595907 + 0.3723874092 0.3621698916 0.3207899034 + 0.3735251129 0.3633525968 0.3219203055 + 0.3746606112 0.3645330071 0.3230482042 + 0.3757919073 0.3657091856 0.3241730034 + 0.3769231141 0.3668855131 0.3252978027 + 0.3780477941 0.3680585921 0.3264195919 + 0.3791683912 0.3692297041 0.3275401890 + 0.3802889884 0.3704009056 0.3286607862 + 0.3814114928 0.3715662062 0.3297694921 + 0.3825342059 0.3727307022 0.3308778107 + 0.3836567998 0.3738948107 0.3319844902 + 0.3847779036 0.3750543892 0.3330835998 + 0.3858990073 0.3762139976 0.3341825902 + 0.3870202899 0.3773706853 0.3352803886 + 0.3881419003 0.3785190880 0.3363755047 + 0.3892636001 0.3796674013 0.3374705911 + 0.3903855085 0.3808118105 0.3385663033 + 0.3915078044 0.3819499016 0.3396627009 + 0.3926301897 0.3830879927 0.3407590091 + 0.3937473893 0.3842205107 0.3418577909 + 0.3948588073 0.3853465915 0.3429583013 + 0.3959701955 0.3864727020 0.3440589011 + 0.3970786929 0.3875936866 0.3451572061 + 0.3981842995 0.3887099922 0.3462541997 + 0.3992899954 0.3898262978 0.3473511934 + 0.4003950953 0.3909426928 0.3484469056 + 0.4014996886 0.3920593858 0.3495419025 + 0.4026043117 0.3931761086 0.3506368995 + 0.4037061036 0.3942928016 0.3517259955 + 0.4048052132 0.3954096138 0.3528110087 + 0.4059042931 0.3965264857 0.3538959920 + 0.4069995880 0.3976393044 0.3549771905 + 0.4080899060 0.3987472057 0.3560549915 + 0.4091801047 0.3998549879 0.3571327925 + 0.4102692008 0.4009611905 0.3582096994 + 0.4113557935 0.4020644128 0.3592854142 + 0.4124422967 0.4031674862 0.3603610098 + 0.4135288894 0.4042702019 0.3614374101 + 0.4146164060 0.4053708017 0.3625159860 + 0.4157038033 0.4064713120 0.3635945916 + 0.4167912900 0.4075717926 0.3646731079 + 0.4178684056 0.4086636007 0.3657506108 + 0.4189451039 0.4097551107 0.3668282032 + 0.4200218916 0.4108465910 0.3679057062 + 0.4210911095 0.4119307995 0.3689739108 + 0.4221575856 0.4130122960 0.3700402081 + 0.4232240021 0.4140937924 0.3711063862 + 0.4242891967 0.4151681066 0.3721705079 + 0.4253528118 0.4162335992 0.3732328117 + 0.4264163971 0.4172990918 0.3742950857 + 0.4274801910 0.4183638096 0.3753553033 + 0.4285458922 0.4194228053 0.3764078021 + 0.4296115041 0.4204818010 0.3774603903 + 0.4306772053 0.4215407968 0.3785130084 + 0.4317356944 0.4225960970 0.3795596957 + 0.4327917993 0.4236500859 0.3806053102 + 0.4338479042 0.4247041941 0.3816510141 + 0.4349010885 0.4257571995 0.3826957047 + 0.4359481037 0.4268082976 0.3837392926 + 0.4369950891 0.4278593957 0.3847829103 + 0.4380421042 0.4289104939 0.3858264983 + 0.4390830994 0.4299480021 0.3868727982 + 0.4401232004 0.4309833944 0.3879191875 + 0.4411633015 0.4320186973 0.3889656067 + 0.4421998858 0.4330511987 0.3900116980 + 0.4432297945 0.4340781868 0.3910571933 + 0.4442596138 0.4351052940 0.3921026886 + 0.4452894926 0.4361324012 0.3931482136 + 0.4463081956 0.4371604025 0.3941898048 + 0.4473238885 0.4381886125 0.3952310085 + 0.4483394921 0.4392169118 0.3962720931 + 0.4493534863 0.4402456880 0.3973116875 + 0.4503594041 0.4412770867 0.3983471990 + 0.4513652921 0.4423084855 0.3993827105 + 0.4523712099 0.4433399141 0.4004181027 + 0.4533743858 0.4443711936 0.4014500976 + 0.4543752968 0.4454025030 0.4024800062 + 0.4553762078 0.4464336932 0.4035097957 + 0.4563770890 0.4474649131 0.4045395851 + 0.4573712051 0.4484929144 0.4055626094 + 0.4583637118 0.4495202899 0.4065848887 + 0.4593561888 0.4505476058 0.4076071978 + 0.4603483975 0.4515748024 0.4086290896 + 0.4613364041 0.4525992870 0.4096485972 + 0.4623242915 0.4536238015 0.4106681049 + 0.4633122981 0.4546484053 0.4116876125 + 0.4642986953 0.4556727111 0.4127076864 + 0.4652802050 0.4566963017 0.4137291908 + 0.4662617147 0.4577200115 0.4147506952 + 0.4672433138 0.4587436914 0.4157721996 + 0.4682214856 0.4597648084 0.4167939126 + 0.4691945910 0.4607819021 0.4178156853 + 0.4701677859 0.4617989957 0.4188374877 + 0.4711408913 0.4628162086 0.4198592901 + 0.4721117914 0.4638265073 0.4208815098 + 0.4730801880 0.4648295045 0.4219039977 + 0.4740486145 0.4658325016 0.4229264855 + 0.4750168920 0.4668354988 0.4239490926 + 0.4759812951 0.4678359032 0.4249672890 + 0.4769417942 0.4688338935 0.4259825051 + 0.4779022932 0.4698317945 0.4269978106 + 0.4788627923 0.4708298147 0.4280130863 + 0.4798217118 0.4718261063 0.4290243089 + 0.4807789028 0.4728206098 0.4300324917 + 0.4817360938 0.4738149941 0.4310407043 + 0.4826933146 0.4748094976 0.4320487976 + 0.4836510122 0.4758031070 0.4330525994 + 0.4846090972 0.4767954051 0.4340521097 + 0.4855673015 0.4777877033 0.4350515008 + 0.4865255058 0.4787800014 0.4360510111 + 0.4874827862 0.4797681868 0.4370489120 + 0.4884381890 0.4807466865 0.4380441904 + 0.4893935025 0.4817250967 0.4390394986 + 0.4903487861 0.4827035964 0.4400348067 + 0.4913035929 0.4836812913 0.4410301149 + 0.4922530055 0.4846526980 0.4420258105 + 0.4932022989 0.4856241941 0.4430215061 + 0.4941517115 0.4865956008 0.4440171123 + 0.4951010942 0.4875670969 0.4450128078 + 0.4960469902 0.4885401130 0.4460073113 + 0.4969922900 0.4895133078 0.4470018148 + 0.4979375899 0.4904865026 0.4479962885 + 0.4988828897 0.4914596975 0.4489907026 + 0.4998261929 0.4924297035 0.4499832988 + 0.5007678866 0.4933972061 0.4509747922 + 0.5017095804 0.4943645895 0.4519664049 + 0.5026512146 0.4953320920 0.4529578984 + 0.5035929084 0.4962972105 0.4539453983 + 0.5045344830 0.4972535968 0.4549227953 + 0.5054761767 0.4982098937 0.4559001923 + 0.5064178109 0.4991663098 0.4568775892 + 0.5073593855 0.5001226068 0.4578549862 + 0.5082985759 0.5010713935 0.4588257968 + 0.5092371106 0.5020182133 0.4597955048 + 0.5101755261 0.5029649734 0.4607652128 + 0.5111140013 0.5039117932 0.4617348909 + 0.5120512843 0.5048583150 0.4627009034 + 0.5129861832 0.5058041811 0.4636611938 + 0.5139210224 0.5067501068 0.4646215141 + 0.5148558021 0.5076960921 0.4655818045 + 0.5157907009 0.5086420178 0.4665420949 + 0.5167201161 0.5095835924 0.4674983025 + 0.5176479220 0.5105239153 0.4684537053 + 0.5185757279 0.5114641786 0.4694091976 + 0.5195034742 0.5124045014 0.4703646004 + 0.5204296112 0.5133435726 0.4713194966 + 0.5213472843 0.5142772794 0.4722726047 + 0.5222650766 0.5152109265 0.4732257128 + 0.5231828094 0.5161445141 0.4741787910 + 0.5241004825 0.5170782208 0.4751318991 + 0.5250151753 0.5180053115 0.4760791957 + 0.5259271264 0.5189262033 0.4770225883 + 0.5268388987 0.5198469758 0.4779661000 + 0.5277506709 0.5207679272 0.4789094925 + 0.5286625028 0.5216888189 0.4798529148 + 0.5295729041 0.5226063132 0.4807907045 + 0.5304828882 0.5235229135 0.4817276001 + 0.5313928723 0.5244395137 0.4826645851 + 0.5323029160 0.5253561139 0.4836015105 + 0.5332129002 0.5262727141 0.4845381975 + 0.5341181159 0.5271934867 0.4854718149 + 0.5350232720 0.5281143188 0.4864053130 + 0.5359284282 0.5290349722 0.4873388112 + 0.5368335843 0.5299558043 0.4882723987 + 0.5377376080 0.5308768749 0.4892036915 + 0.5386344790 0.5318000913 0.4901269078 + 0.5395315289 0.5327231884 0.4910500944 + 0.5404284000 0.5336464047 0.4919733107 + 0.5413252711 0.5345695019 0.4928965867 + 0.5422213078 0.5354900956 0.4938173890 + 0.5431140065 0.5364019871 0.4947327971 + 0.5440065861 0.5373138785 0.4956482053 + 0.5448992848 0.5382258296 0.4965636134 + 0.5457919240 0.5391377211 0.4974789023 + 0.5466845036 0.5400484800 0.4983932078 + 0.5475770235 0.5409557223 0.4993050992 + 0.5484694839 0.5418627858 0.5002170205 + 0.5493621230 0.5427699089 0.5011289120 + 0.5502545834 0.5436770916 0.5020408034 + 0.5511453748 0.5445848703 0.5029529929 + 0.5520284176 0.5454956889 0.5038657784 + 0.5529114008 0.5464066267 0.5047786236 + 0.5537943244 0.5473175049 0.5056914091 + 0.5546773076 0.5482283235 0.5066043139 + 0.5555598140 0.5491390228 0.5075178146 + 0.5564333797 0.5500462055 0.5084359050 + 0.5573070049 0.5509535074 0.5093541145 + 0.5581805706 0.5518608093 0.5102722049 + 0.5590541959 0.5527679920 0.5111904144 + 0.5599278212 0.5536752939 0.5121085048 + 0.5607947707 0.5545750260 0.5130286813 + 0.5616607070 0.5554735065 0.5139490962 + 0.5625265837 0.5563721061 0.5148693919 + 0.5633925200 0.5572705865 0.5157896876 + 0.5642585158 0.5581691265 0.5167099833 + 0.5651180744 0.5590634942 0.5176302195 + 0.5659735799 0.5599551201 0.5185502172 + 0.5668290257 0.5608466268 0.5194702148 + 0.5676844120 0.5617381930 0.5203902125 + 0.5685399175 0.5626296997 0.5213103294 + 0.5693951845 0.5635216832 0.5222290158 + 0.5702500939 0.5644145012 0.5231453776 + 0.5711050034 0.5653073788 0.5240616798 + 0.5719599128 0.5662003160 0.5249779820 + 0.5728148222 0.5670931935 0.5258942842 + 0.5736696720 0.5679860711 0.5268107057 + 0.5745213032 0.5688735843 0.5277202725 + 0.5753722787 0.5697603226 0.5286294222 + 0.5762233734 0.5706470013 0.5295385122 + 0.5770744085 0.5715336800 0.5304474831 + 0.5779255033 0.5724204183 0.5313565731 + 0.5787749290 0.5733029842 0.5322644114 + 0.5796216726 0.5741786957 0.5331705213 + 0.5804684758 0.5750545263 0.5340765715 + 0.5813153982 0.5759302974 0.5349826813 + 0.5821622014 0.5768060088 0.5358887911 + 0.5830090046 0.5776817799 0.5367949009 + 0.5838500261 0.5785498023 0.5377020240 + 0.5846896768 0.5794162750 0.5386093259 + 0.5855293870 0.5802826881 0.5395165086 + 0.5863690972 0.5811491013 0.5404236913 + 0.5872088075 0.5820155144 0.5413309932 + 0.5880478024 0.5828816295 0.5422391295 + 0.5888836980 0.5837466121 0.5431495905 + 0.5897195935 0.5846114755 0.5440601110 + 0.5905554891 0.5854765177 0.5449705720 + 0.5913913250 0.5863415003 0.5458810925 + 0.5922272205 0.5872064233 0.5467916131 + 0.5930604935 0.5880705118 0.5476971865 + 0.5938909054 0.5889335871 0.5485988855 + 0.5947213173 0.5897967815 0.5495005250 + 0.5955516100 0.5906599164 0.5504022241 + 0.5963820219 0.5915231109 0.5513038039 + 0.5972123742 0.5923861861 0.5522055030 + 0.5980378985 0.5932421088 0.5531039834 + 0.5988610983 0.5940948129 0.5540016294 + 0.5996842980 0.5949473977 0.5548992157 + 0.6005076170 0.5958001018 0.5557969213 + 0.6013308167 0.5966528058 0.5566945076 + 0.6021540761 0.5975055099 0.5575920939 + 0.6029741764 0.5983492136 0.5584818721 + 0.6037936807 0.5991911888 0.5593709946 + 0.6046133041 0.6000332236 0.5602599978 + 0.6054328084 0.6008751988 0.5611490011 + 0.6062523127 0.6017171144 0.5620381236 + 0.6070718765 0.6025590897 0.5629271269 + 0.6078854203 0.6033985019 0.5638099909 + 0.6086981297 0.6042374969 0.5646926761 + 0.6095107794 0.6050766110 0.5655754209 + 0.6103236079 0.6059157252 0.5664582253 + 0.6111363173 0.6067547202 0.5673409104 + 0.6119490266 0.6075937748 0.5682235956 + 0.6127572060 0.6084339023 0.5691041946 + 0.6135646105 0.6092742085 0.5699847937 + 0.6143720150 0.6101143956 0.5708652735 + 0.6151794195 0.6109547019 0.5717458725 + 0.6159867048 0.6117950082 0.5726264119 + 0.6167941093 0.6126351953 0.5735070109 + 0.6175978780 0.6134642959 0.5743842125 + 0.6184005141 0.6142898798 0.5752606988 + 0.6192030907 0.6151155829 0.5761371851 + 0.6200057268 0.6159412265 0.5770136714 + 0.6208083034 0.6167668104 0.5778902173 + 0.6216108799 0.6175925136 0.5787667036 + 0.6224128008 0.6184120178 0.5796405077 + 0.6232140064 0.6192271113 0.5805128217 + 0.6240152717 0.6200422049 0.5813850760 + 0.6248164773 0.6208572984 0.5822573900 + 0.6256178021 0.6216725111 0.5831297040 + 0.6264191270 0.6224876046 0.5840020180 + 0.6272190213 0.6233018041 0.5848723054 + 0.6280159950 0.6241139770 0.5857393146 + 0.6288130879 0.6249262094 0.5866062045 + 0.6296101213 0.6257383823 0.5874732137 + 0.6304070950 0.6265506148 0.5883401036 + 0.6312041283 0.6273627877 0.5892071128 + 0.6320011020 0.6281750202 0.5900738835 + 0.6327958107 0.6289898753 0.5909329057 + 0.6335904002 0.6298049092 0.5917918086 + 0.6343849897 0.6306200027 0.5926507711 + 0.6351795197 0.6314349771 0.5935097933 + 0.6359741092 0.6322500706 0.5943686962 + 0.6367686987 0.6330651045 0.5952277184 + 0.6375622153 0.6338756084 0.5960803032 + 0.6383545995 0.6346815228 0.5969281793 + 0.6391469836 0.6354873776 0.5977761149 + 0.6399394870 0.6362932920 0.5986239910 + 0.6407318711 0.6370993257 0.5994718075 + 0.6415243745 0.6379051805 0.6003196836 + 0.6423168182 0.6387110949 0.6011673212 + 0.6431099176 0.6395062804 0.6020082235 + 0.6439030766 0.6403012276 0.6028491259 + 0.6446962953 0.6410961151 0.6036899090 + 0.6454893947 0.6418910027 0.6045308113 + 0.6462826133 0.6426858902 0.6053715944 + 0.6470757127 0.6434808969 0.6062124968 + 0.6478682160 0.6442744732 0.6070520878 + 0.6486594081 0.6450660229 0.6078900099 + 0.6494506001 0.6458573937 0.6087278128 + 0.6502417922 0.6466488242 0.6095656753 + 0.6510329843 0.6474401951 0.6104035974 + 0.6518241763 0.6482316852 0.6112414002 + 0.6526154280 0.6490231156 0.6120793223 + 0.6534075141 0.6498171091 0.6129179001 + 0.6542003155 0.6506124735 0.6137567759 + 0.6549929976 0.6514077783 0.6145955920 + 0.6557856798 0.6522032022 0.6154345274 + 0.6565784216 0.6529986262 0.6162734032 + 0.6573712230 0.6537939906 0.6171122789 + 0.6581639051 0.6545892954 0.6179510951 + 0.6589608788 0.6553863287 0.6187928915 + 0.6597586274 0.6561836004 0.6196351051 + 0.6605563164 0.6569808125 0.6204771996 + 0.6613541245 0.6577780843 0.6213192940 + 0.6621518135 0.6585754156 0.6221613884 + 0.6629495025 0.6593726873 0.6230034828 + 0.6637473106 0.6601698995 0.6238455772 + 0.6645362973 0.6609578133 0.6246849895 + 0.6653249264 0.6617453098 0.6255244017 + 0.6661134958 0.6625326872 0.6263638139 + 0.6669020057 0.6633201241 0.6272032261 + 0.6676905751 0.6641076207 0.6280425787 + 0.6684792042 0.6648949981 0.6288819909 + 0.6692677736 0.6656824946 0.6297214031 + 0.6700507998 0.6664658785 0.6305593252 + 0.6708337069 0.6672493219 0.6313971877 + 0.6716166139 0.6680325866 0.6322351098 + 0.6723995209 0.6688159108 0.6330729723 + 0.6731824279 0.6695992947 0.6339108944 + 0.6739652157 0.6703826189 0.6347488165 + 0.6747481227 0.6711658835 0.6355866790 + 0.6755290031 0.6719505191 0.6364173889 + 0.6763095856 0.6727352142 0.6372476220 + 0.6770902276 0.6735200286 0.6380779147 + 0.6778709292 0.6743047237 0.6389080882 + 0.6786515117 0.6750894189 0.6397383809 + 0.6794320941 0.6758741140 0.6405686140 + 0.6802126765 0.6766588092 0.6413989067 + 0.6809899211 0.6774402261 0.6422249079 + 0.6817654967 0.6782202721 0.6430497169 + 0.6825411916 0.6790003181 0.6438744068 + 0.6833168864 0.6797804236 0.6446992159 + 0.6840925813 0.6805605292 0.6455240250 + 0.6848682761 0.6813405156 0.6463487148 + 0.6856439710 0.6821206212 0.6471735239 + 0.6864163876 0.6828963161 0.6479974985 + 0.6871845126 0.6836665869 0.6488205791 + 0.6879525781 0.6844369173 0.6496437788 + 0.6887208223 0.6852071881 0.6504669189 + 0.6894888878 0.6859775186 0.6512901187 + 0.6902570128 0.6867477894 0.6521131992 + 0.6910251975 0.6875181198 0.6529363990 + 0.6917930245 0.6882880926 0.6537595987 + 0.6925569773 0.6890543103 0.6545832753 + 0.6933209896 0.6898205280 0.6554070115 + 0.6940848827 0.6905866861 0.6562306881 + 0.6948488951 0.6913527846 0.6570544243 + 0.6956127882 0.6921190023 0.6578781009 + 0.6963768005 0.6928852201 0.6587017775 + 0.6971408129 0.6936513782 0.6595255136 + 0.6979042888 0.6944183111 0.6603466868 + 0.6986675262 0.6951858997 0.6611664891 + 0.6994307041 0.6959534287 0.6619862914 + 0.7001938820 0.6967208982 0.6628060937 + 0.7009570003 0.6974884272 0.6636258960 + 0.7017201781 0.6982558966 0.6644456983 + 0.7024834156 0.6990234852 0.6652655005 + 0.7032464743 0.6997908950 0.6660854220 + 0.7040007114 0.7005546093 0.6669068933 + 0.7047548890 0.7013183832 0.6677284837 + 0.7055091262 0.7020820975 0.6685500145 + 0.7062633038 0.7028458118 0.6693716049 + 0.7070174813 0.7036095262 0.6701930761 + 0.7077717185 0.7043731809 0.6710147262 + 0.7085258961 0.7051368952 0.6718361974 + 0.7092772126 0.7058975101 0.6726554036 + 0.7100226283 0.7066521049 0.6734707952 + 0.7107681036 0.7074066997 0.6742863059 + 0.7115135789 0.7081612945 0.6751016974 + 0.7122589946 0.7089158893 0.6759172082 + 0.7130045295 0.7096704841 0.6767325997 + 0.7137500048 0.7104250789 0.6775481105 + 0.7144954205 0.7111796737 0.6783635020 + 0.7152379155 0.7119320035 0.6791734099 + 0.7159780264 0.7126826048 0.6799798012 + 0.7167180777 0.7134332061 0.6807861924 + 0.7174581885 0.7141839266 0.6815925837 + 0.7181981802 0.7149345279 0.6823989749 + 0.7189382911 0.7156851292 0.6832054257 + 0.7196784019 0.7164356709 0.6840116978 + 0.7204185128 0.7171862721 0.6848180890 + 0.7211552262 0.7179378271 0.6856207252 + 0.7218902111 0.7186897993 0.6864218116 + 0.7226251960 0.7194417119 0.6872228980 + 0.7233601809 0.7201936841 0.6880239844 + 0.7240952253 0.7209457159 0.6888250709 + 0.7248302102 0.7216976285 0.6896262169 + 0.7255653143 0.7224496007 0.6904273033 + 0.7263002992 0.7232015729 0.6912283897 + 0.7270305157 0.7239469290 0.6920284033 + 0.7277582884 0.7246891260 0.6928278804 + 0.7284861803 0.7254313231 0.6936274767 + 0.7292140722 0.7261735201 0.6944270134 + 0.7299419045 0.7269157171 0.6952266097 + 0.7306697965 0.7276579142 0.6960262060 + 0.7313976884 0.7284001708 0.6968256831 + 0.7321255803 0.7291424274 0.6976252794 + 0.7328494787 0.7298778892 0.6984261870 + 0.7335705757 0.7306084037 0.6992279887 + 0.7342916727 0.7313389778 0.7000297904 + 0.7350128293 0.7320696115 0.7008314729 + 0.7357339263 0.7328001857 0.7016332746 + 0.7364550233 0.7335308194 0.7024350762 + 0.7371760011 0.7342612743 0.7032368183 + 0.7378970981 0.7349919081 0.7040386200 + 0.7386168838 0.7357206941 0.7048401237 + 0.7393341064 0.7364463806 0.7056412101 + 0.7400513887 0.7371721268 0.7064422965 + 0.7407686710 0.7378978729 0.7072433829 + 0.7414860129 0.7386236191 0.7080445290 + 0.7422032952 0.7393493056 0.7088456154 + 0.7429205775 0.7400749922 0.7096467018 + 0.7436379194 0.7408006787 0.7104477882 + 0.7443552017 0.7415264249 0.7112485170 + 0.7450708747 0.7422525287 0.7120447159 + 0.7457867265 0.7429785728 0.7128409147 + 0.7465025187 0.7437046766 0.7136371136 + 0.7472183108 0.7444307804 0.7144333720 + 0.7479341030 0.7451568842 0.7152295709 + 0.7486497760 0.7458829880 0.7160258293 + 0.7493656278 0.7466090918 0.7168220282 + 0.7500814199 0.7473351955 0.7176182270 + 0.7507966161 0.7480586171 0.7184088230 + 0.7515113950 0.7487803102 0.7191962004 + 0.7522261739 0.7495018840 0.7199836969 + 0.7529410124 0.7502235770 0.7207711935 + 0.7536557913 0.7509452105 0.7215585709 + 0.7543706298 0.7516667843 0.7223461270 + 0.7550854087 0.7523884773 0.7231336236 + 0.7558001876 0.7531101108 0.7239210010 + 0.7565147281 0.7538313270 0.7247081995 + 0.7572231293 0.7545434237 0.7254921198 + 0.7579314709 0.7552555203 0.7262759805 + 0.7586399913 0.7559676766 0.7270599008 + 0.7593483925 0.7566797733 0.7278438807 + 0.7600567937 0.7573919296 0.7286278009 + 0.7607651949 0.7581040263 0.7294117212 + 0.7614735961 0.7588161826 0.7301957011 + 0.7621819973 0.7595282793 0.7309796214 + 0.7628899217 0.7602388263 0.7317612767 + 0.7635970116 0.7609465122 0.7325400710 + 0.7643041015 0.7616543174 0.7333188057 + 0.7650110722 0.7623620033 0.7340976000 + 0.7657182217 0.7630698085 0.7348762751 + 0.7664251924 0.7637774944 0.7356551290 + 0.7671322823 0.7644852996 0.7364339232 + 0.7678393126 0.7651931047 0.7372125983 + 0.7685464025 0.7659007907 0.7379913926 + 0.7692546248 0.7666099072 0.7387691736 + 0.7699635029 0.7673199773 0.7395464182 + 0.7706725001 0.7680299878 0.7403236032 + 0.7713813782 0.7687399983 0.7411009073 + 0.7720903158 0.7694501281 0.7418780923 + 0.7727993131 0.7701600790 0.7426552773 + 0.7735081911 0.7708700895 0.7434325814 + 0.7742171884 0.7715802193 0.7442098260 + 0.7749261260 0.7722902298 0.7449870110 + 0.7756347060 0.7730041146 0.7457646132 + 0.7763432264 0.7737196088 0.7465423942 + 0.7770516276 0.7744352221 0.7473201156 + 0.7777600884 0.7751507163 0.7480978966 + 0.7784684896 0.7758663297 0.7488756180 + 0.7791770101 0.7765818238 0.7496532798 + 0.7798854709 0.7772973776 0.7504311204 + 0.7805938721 0.7780128717 0.7512087822 + 0.7813023925 0.7787284851 0.7519865036 + 0.7820088267 0.7794386744 0.7527605891 + 0.7827143073 0.7801463008 0.7535331249 + 0.7834197283 0.7808538079 0.7543057203 + 0.7841252089 0.7815613151 0.7550783157 + 0.7848305702 0.7822688818 0.7558507919 + 0.7855361104 0.7829763889 0.7566233873 + 0.7862414718 0.7836840153 0.7573959231 + 0.7869470119 0.7843915224 0.7581685185 + 0.7876523733 0.7850990295 0.7589409947 + 0.7883579135 0.7858042717 0.7597122192 + 0.7890635133 0.7865074277 0.7604821920 + 0.7897689939 0.7872105837 0.7612522244 + 0.7904744744 0.7879136801 0.7620223165 + 0.7911800146 0.7886168957 0.7627922893 + 0.7918856144 0.7893199921 0.7635623813 + 0.7925910950 0.7900232077 0.7643324137 + 0.7932965755 0.7907263041 0.7651025057 + 0.7940021753 0.7914295197 0.7658724785 + 0.7947084904 0.7921326756 0.7666410804 + 0.7954170704 0.7928364277 0.7674059868 + 0.7961258292 0.7935400009 0.7681708932 + 0.7968344092 0.7942435741 0.7689357996 + 0.7975429893 0.7949472070 0.7697008252 + 0.7982516885 0.7956508994 0.7704656720 + 0.7989603281 0.7963544726 0.7712305784 + 0.7996690273 0.7970581055 0.7719954848 + 0.8003776073 0.7977616787 0.7727603912 + 0.8010861874 0.7984653115 0.7735252976 + 0.8017868996 0.7991653085 0.7742831707 + 0.8024861813 0.7998644710 0.7750402093 + 0.8031854033 0.8005638123 0.7757971287 + 0.8038846254 0.8012630939 0.7765541077 + 0.8045839071 0.8019623756 0.7773110271 + 0.8052831292 0.8026615977 0.7780680060 + 0.8059822917 0.8033608794 0.7788249850 + 0.8066815734 0.8040602207 0.7795819044 + 0.8073807955 0.8047593832 0.7803388834 + 0.8080779910 0.8054556251 0.7810941935 + 0.8087708950 0.8061454892 0.7818467021 + 0.8094639182 0.8068354130 0.7825992703 + 0.8101568222 0.8075252175 0.7833517790 + 0.8108497262 0.8082150817 0.7841042876 + 0.8115425706 0.8089050055 0.7848567963 + 0.8122354746 0.8095948100 0.7856093049 + 0.8129284978 0.8102846742 0.7863618135 + 0.8136214018 0.8109744787 0.7871143222 + 0.8143143058 0.8116644025 0.7878667712 + 0.8150073290 0.8123514056 0.7886186838 + 0.8157002926 0.8130369186 0.7893702984 + 0.8163931966 0.8137224913 0.7901219130 + 0.8170862198 0.8144080043 0.7908735275 + 0.8177791834 0.8150935173 0.7916250229 + 0.8184722066 0.8157790899 0.7923765779 + 0.8191652298 0.8164646029 0.7931281924 + 0.8198580742 0.8171501160 0.7938798070 + 0.8205510974 0.8178356886 0.7946314216 + 0.8212441206 0.8185212016 0.7953829765 + 0.8219397068 0.8192058802 0.7961322069 + 0.8226357102 0.8198903799 0.7968811989 + 0.8233315945 0.8205748796 0.7976301908 + 0.8240275979 0.8212594986 0.7983791828 + 0.8247236013 0.8219439983 0.7991281748 + 0.8254194856 0.8226286173 0.7998772264 + 0.8261154890 0.8233131170 0.8006262183 + 0.8268113732 0.8239976168 0.8013753295 + 0.8275073767 0.8246821761 0.8021243215 + 0.8282033801 0.8253666759 0.8028733134 + 0.8288903832 0.8260424733 0.8036262989 + 0.8295770884 0.8267179132 0.8043792844 + 0.8302637935 0.8273934126 0.8051323295 + 0.8309504986 0.8280689120 0.8058853149 + 0.8316372037 0.8287442923 0.8066383004 + 0.8323239088 0.8294197917 0.8073912859 + 0.8330106735 0.8300951719 0.8081442714 + 0.8336973786 0.8307706714 0.8088973165 + 0.8343840837 0.8314461708 0.8096503019 + 0.8350707889 0.8321216106 0.8104032874 + 0.8357518911 0.8327894211 0.8111593127 + 0.8364323974 0.8334565163 0.8119155765 + 0.8371130228 0.8341234922 0.8126717806 + 0.8377935290 0.8347905874 0.8134279847 + 0.8384740949 0.8354576230 0.8141843081 + 0.8391546011 0.8361247182 0.8149405122 + 0.8398352265 0.8367916942 0.8156967759 + 0.8405156732 0.8374587893 0.8164529800 + 0.8411962986 0.8381258249 0.8172091842 + 0.8418768048 0.8387929201 0.8179655075 + 0.8425561786 0.8394573927 0.8187175989 + 0.8432351947 0.8401209712 0.8194684982 + 0.8439140916 0.8407846093 0.8202192783 + 0.8445931077 0.8414481878 0.8209701180 + 0.8452720046 0.8421118259 0.8217208982 + 0.8459510207 0.8427755237 0.8224717975 + 0.8466299176 0.8434391022 0.8232225776 + 0.8473088741 0.8441026807 0.8239734173 + 0.8479878902 0.8447663188 0.8247241974 + 0.8486667871 0.8454298973 0.8254750967 + 0.8493432999 0.8460934162 0.8262230754 + 0.8500158787 0.8467569947 0.8269672990 + 0.8506883979 0.8474205136 0.8277115226 + 0.8513609767 0.8480839729 0.8284556866 + 0.8520336151 0.8487474918 0.8291999102 + 0.8527061939 0.8494110107 0.8299440742 + 0.8533787727 0.8500745296 0.8306882977 + 0.8540514112 0.8507381082 0.8314325809 + 0.8547239900 0.8514016271 0.8321768045 + 0.8553965092 0.8520650864 0.8329210281 + 0.8560690880 0.8527286053 0.8336651921 + 0.8567318916 0.8533846140 0.8344091177 + 0.8573938012 0.8540399075 0.8351529241 + 0.8580557108 0.8546953201 0.8358967900 + 0.8587176204 0.8553506136 0.8366405964 + 0.8593795896 0.8560060263 0.8373845220 + 0.8600414991 0.8566613197 0.8381283879 + 0.8607034087 0.8573166728 0.8388721943 + 0.8613653183 0.8579720259 0.8396161199 + 0.8620272875 0.8586273789 0.8403599262 + 0.8626891971 0.8592826724 0.8411037922 + 0.8633491993 0.8599355817 0.8418468833 + 0.8640050292 0.8605830073 0.8425887227 + 0.8646607995 0.8612304926 0.8433305025 + 0.8653166294 0.8618779778 0.8440722823 + 0.8659723997 0.8625254035 0.8448141217 + 0.8666282296 0.8631728888 0.8455559015 + 0.8672839999 0.8638203740 0.8462976813 + 0.8679397702 0.8644677997 0.8470395207 + 0.8685956001 0.8651152849 0.8477811813 + 0.8692513704 0.8657627106 0.8485230207 + 0.8699072003 0.8664101958 0.8492648005 + 0.8705629706 0.8670558929 0.8500074148 + 0.8712186813 0.8677002788 0.8507503867 + 0.8718745112 0.8683447242 0.8514934778 + 0.8725302219 0.8689891100 0.8522366285 + 0.8731859922 0.8696334958 0.8529797196 + 0.8738417029 0.8702778220 0.8537226915 + 0.8744974732 0.8709222078 0.8544657826 + 0.8751531839 0.8715665936 0.8552088737 + 0.8758090138 0.8722109795 0.8559520245 + 0.8764647841 0.8728554249 0.8566949964 + 0.8771204948 0.8734998107 0.8574380875 + 0.8777772188 0.8741462231 0.8581804037 + 0.8784344792 0.8747938275 0.8589221835 + 0.8790916800 0.8754413128 0.8596640825 + 0.8797490001 0.8760887980 0.8604059219 + 0.8804062009 0.8767364025 0.8611478209 + 0.8810635209 0.8773838878 0.8618897200 + 0.8817207217 0.8780313730 0.8626314998 + 0.8823779225 0.8786789775 0.8633733988 + 0.8830351830 0.8793265224 0.8641152978 + 0.8836923838 0.8799740076 0.8648570776 + 0.8843497038 0.8806216121 0.8655989766 + 0.8850082159 0.8812701106 0.8663362861 + 0.8856676221 0.8819192052 0.8670710921 + 0.8863270283 0.8825684190 0.8678058982 + 0.8869863749 0.8832175136 0.8685407043 + 0.8876457810 0.8838667274 0.8692755103 + 0.8883051276 0.8845158219 0.8700103164 + 0.8889644742 0.8851649165 0.8707451224 + 0.8896238804 0.8858140707 0.8714798093 + 0.8902832866 0.8864632249 0.8722146153 + 0.8909426928 0.8871123791 0.8729494214 + 0.8916019797 0.8877614737 0.8736842275 + 0.8922591805 0.8884071708 0.8744199872 + 0.8929132223 0.8890480995 0.8751569986 + 0.8935672045 0.8896890879 0.8758940101 + 0.8942213058 0.8903300166 0.8766310811 + 0.8948752880 0.8909708858 0.8773680925 + 0.8955293298 0.8916118145 0.8781051040 + 0.8961833119 0.8922526836 0.8788421750 + 0.8968374133 0.8928936124 0.8795791864 + 0.8974913955 0.8935344815 0.8803161979 + 0.8981453776 0.8941755295 0.8810533285 + 0.8987994194 0.8948163986 0.8817902803 + 0.8994534016 0.8954570889 0.8825269938 + 0.9001063704 0.8960943818 0.8832603097 + 0.9007593989 0.8967316747 0.8839936852 + 0.9014124274 0.8973690271 0.8847270012 + 0.9020653963 0.8980063200 0.8854603171 + 0.9027184248 0.8986436129 0.8861935735 + 0.9033715129 0.8992809057 0.8869268894 + 0.9040244818 0.8999181986 0.8876603246 + 0.9046775103 0.9005554914 0.8883935809 + 0.9053304791 0.9011927843 0.8891268969 + 0.9059835076 0.9018300772 0.8898602128 + 0.9066364765 0.9024674296 0.8905935287 + 0.9072905779 0.9031060934 0.8913232088 + 0.9079453945 0.9037460089 0.8920502067 + 0.9086002707 0.9043858051 0.8927773237 + 0.9092550874 0.9050256014 0.8935043216 + 0.9099100232 0.9056653976 0.8942313790 + 0.9105647802 0.9063051939 0.8949583769 + 0.9112197161 0.9069449902 0.8956853747 + 0.9118745923 0.9075847864 0.8964124918 + 0.9125294089 0.9082245827 0.8971394897 + 0.9131842852 0.9088644981 0.8978664875 + 0.9138391018 0.9095042944 0.8985936046 + 0.9144939780 0.9101440907 0.8993206024 + 0.9151384234 0.9107882977 0.9000440240 + 0.9157816768 0.9114329815 0.9007670879 + 0.9164249897 0.9120777249 0.9014902115 + 0.9170683026 0.9127225280 0.9022132754 + 0.9177114964 0.9133672118 0.9029362798 + 0.9183548093 0.9140118957 0.9036594033 + 0.9189981222 0.9146565795 0.9043825269 + 0.9196413755 0.9153013229 0.9051055908 + 0.9202846885 0.9159461260 0.9058287144 + 0.9209278822 0.9165908098 0.9065517783 + 0.9215711951 0.9172354937 0.9072747827 + 0.9222133756 0.9178792834 0.9079968929 + 0.9228429198 0.9185125828 0.9087103009 + 0.9234725237 0.9191460013 0.9094237089 + 0.9241020083 0.9197793007 0.9101369977 + 0.9247316122 0.9204127192 0.9108504057 + 0.9253610969 0.9210460186 0.9115636945 + 0.9259907007 0.9216793776 0.9122771025 + 0.9266201854 0.9223126769 0.9129905105 + 0.9272497892 0.9229460955 0.9137037992 + 0.9278793931 0.9235793948 0.9144172072 + 0.9285088778 0.9242128134 0.9151304960 + 0.9291384816 0.9248461723 0.9158439040 + 0.9297673106 0.9254785180 0.9165560007 + 0.9303902984 0.9261021018 0.9172585011 + 0.9310132265 0.9267256856 0.9179610014 + 0.9316362143 0.9273493290 0.9186636209 + 0.9322590828 0.9279729128 0.9193661213 + 0.9328820705 0.9285964966 0.9200686216 + 0.9335051179 0.9292201996 0.9207711816 + 0.9341279864 0.9298437834 0.9214736819 + 0.9347509742 0.9304674268 0.9221761823 + 0.9353739023 0.9310910106 0.9228788018 + 0.9359968901 0.9317145944 0.9235813022 + 0.9366198182 0.9323381782 0.9242838025 + 0.9372428060 0.9329618216 0.9249864221 + 0.9378637075 0.9335851073 0.9256830812 + 0.9384844899 0.9342085123 0.9263797998 + 0.9391052723 0.9348317981 0.9270763993 + 0.9397261143 0.9354550838 0.9277731180 + 0.9403468966 0.9360784292 0.9284697175 + 0.9409676790 0.9367017150 0.9291663170 + 0.9415885210 0.9373250008 0.9298629761 + 0.9422093034 0.9379482865 0.9305595756 + 0.9428302050 0.9385716915 0.9312562943 + 0.9434509873 0.9391949773 0.9319528937 + 0.9440718293 0.9398183227 0.9326494932 + 0.9446926117 0.9404416084 0.9333462119 + 0.9453073740 0.9410626292 0.9340357780 + 0.9459190965 0.9416825175 0.9347218871 + 0.9465308189 0.9423022866 0.9354081154 + 0.9471424818 0.9429221749 0.9360942245 + 0.9477542043 0.9435420036 0.9367803931 + 0.9483659267 0.9441617727 0.9374665022 + 0.9489775896 0.9447817206 0.9381526709 + 0.9495893121 0.9454014897 0.9388387799 + 0.9502009749 0.9460213780 0.9395250082 + 0.9508126974 0.9466412067 0.9402111173 + 0.9514244199 0.9472610950 0.9408972859 + 0.9520360827 0.9478809237 0.9415833950 + 0.9526458979 0.9484993815 0.9422674179 + 0.9532474279 0.9491121769 0.9429422021 + 0.9538488984 0.9497249126 0.9436171055 + 0.9544504285 0.9503375888 0.9442920089 + 0.9550518990 0.9509503245 0.9449669123 + 0.9556534290 0.9515630007 0.9456418157 + 0.9562548995 0.9521757960 0.9463167191 + 0.9568564296 0.9527884722 0.9469915032 + 0.9574579000 0.9534012079 0.9476664066 + 0.9580594897 0.9540138841 0.9483413100 + 0.9586610198 0.9546266794 0.9490162134 + 0.9592624903 0.9552394152 0.9496911168 + 0.9598640203 0.9558520913 0.9503659010 + 0.9604597092 0.9564607143 0.9510350823 + 0.9610503912 0.9570658207 0.9516996741 + 0.9616411924 0.9576709270 0.9523642063 + 0.9622319937 0.9582759738 0.9530286789 + 0.9628227949 0.9588810802 0.9536932111 + 0.9634134769 0.9594861865 0.9543578029 + 0.9640042782 0.9600912929 0.9550222754 + 0.9645950794 0.9606963992 0.9556868076 + 0.9651858211 0.9613015056 0.9563512802 + 0.9657766223 0.9619066119 0.9570158720 + 0.9663674235 0.9625117183 0.9576804042 + 0.9669582248 0.9631168246 0.9583448768 + 0.9675489068 0.9637219906 0.9590094090 + 0.9681262970 0.9643182158 0.9596667290 + 0.9686986208 0.9649111032 0.9603213072 + 0.9692708254 0.9655039907 0.9609760046 + 0.9698430896 0.9660969973 0.9616305828 + 0.9704154134 0.9666898847 0.9622852206 + 0.9709876776 0.9672827721 0.9629399180 + 0.9715598822 0.9678757191 0.9635944963 + 0.9721322060 0.9684687257 0.9642490745 + 0.9727045298 0.9690616131 0.9649037719 + 0.9732767940 0.9696545005 0.9655584097 + 0.9738491178 0.9702473879 0.9662129879 + 0.9744213223 0.9708403945 0.9668676853 + 0.9749935865 0.9714332819 0.9675223231 + 0.9755455256 0.9720122814 0.9681680202 + 0.9760900736 0.9725863934 0.9688106179 + 0.9766348004 0.9731606245 0.9694532156 + 0.9771794081 0.9737346768 0.9700958729 + 0.9777240753 0.9743087888 0.9707384706 + 0.9782686830 0.9748830199 0.9713811278 + 0.9788134098 0.9754570723 0.9720237255 + 0.9793580174 0.9760311842 0.9726663828 + 0.9799026251 0.9766052961 0.9733089805 + 0.9804472923 0.9771795273 0.9739515781 + 0.9809919000 0.9777535796 0.9745941758 + 0.9815366268 0.9783276916 0.9752368927 + 0.9820811749 0.9789019227 0.9758794904 + 0.9826055765 0.9794644713 0.9765092134 + 0.9831150770 0.9800186753 0.9771293998 + 0.9836245775 0.9805728197 0.9777497053 + 0.9841340184 0.9811270237 0.9783700109 + 0.9846435189 0.9816811085 0.9789903164 + 0.9851529002 0.9822353125 0.9796106219 + 0.9856624007 0.9827893972 0.9802309275 + 0.9861719012 0.9833436012 0.9808511138 + 0.9866812825 0.9838976860 0.9814714193 + 0.9871907830 0.9844518900 0.9820917249 + 0.9877002239 0.9850059748 0.9827119708 + 0.9882097244 0.9855601788 0.9833322763 + 0.9887192249 0.9861143231 0.9839525223 + 0.9892141223 0.9866619110 0.9845647812 + 0.9896609783 0.9871875048 0.9851508737 + 0.9901077747 0.9877132177 0.9857370853 + 0.9905545712 0.9882388115 0.9863231778 + 0.9910014272 0.9887645245 0.9869092703 + 0.9914482832 0.9892901182 0.9874954224 + 0.9918950796 0.9898157716 0.9880815148 + 0.9923418760 0.9903414249 0.9886677265 + 0.9927887917 0.9908670783 0.9892538190 + 0.9932355881 0.9913926721 0.9898399115 + 0.9936823845 0.9919183850 0.9904260039 + 0.9941291809 0.9924439788 0.9910122156 + 0.9945760965 0.9929696918 0.9915983081 + 0.9950228930 0.9934952855 0.9921844006 + 0.9954037070 0.9939768910 0.9927504063 + 0.9957572818 0.9944401979 0.9933080077 + 0.9961109161 0.9949035048 0.9938657284 + 0.9964643717 0.9953668118 0.9944233894 + 0.9968180060 0.9958301187 0.9949809909 + 0.9971715212 0.9962934256 0.9955387115 + 0.9975250959 0.9967567921 0.9960963130 + 0.9978786111 0.9972200990 0.9966539741 + 0.9982321858 0.9976834059 0.9972116947 + 0.9985858202 0.9981467128 0.9977692962 + 0.9989392757 0.9986100197 0.9983270168 + 0.9992929101 0.9990733862 0.9988846779 + 0.9996464252 0.9995366931 0.9994422793 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfachrome_ct_precisa_100.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfachrome_ct_precisa_100.spi1d index f4f64aeb..a5d15d3e 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfachrome_ct_precisa_100.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfachrome_ct_precisa_100.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0005381406 0.0002651573 0.0005448886 - 0.0011811421 0.0005488236 0.0011236490 - 0.0018776740 0.0008598516 0.0017561211 - 0.0026023439 0.0012074891 0.0024572650 - 0.0033602889 0.0015991570 0.0032185989 - 0.0041644410 0.0020365941 0.0040284242 - 0.0050217710 0.0025163409 0.0048902789 - 0.0059333672 0.0030300061 0.0058214129 - 0.0069048190 0.0035676421 0.0068372628 - 0.0079441704 0.0041191741 0.0079477550 - 0.0090618078 0.0046778312 0.0091583403 - 0.0102658896 0.0052411938 0.0104663204 - 0.0115583995 0.0058102822 0.0118661895 - 0.0129291601 0.0063874712 0.0133533198 - 0.0143661099 0.0069762110 0.0149256596 - 0.0158517305 0.0075797332 0.0165787991 - 0.0173796508 0.0082016559 0.0183057394 - 0.0189530998 0.0088431826 0.0200976301 - 0.0205809996 0.0095067294 0.0219422299 - 0.0222787503 0.0101926001 0.0238327999 - 0.0240615793 0.0109011699 0.0257611796 - 0.0259451196 0.0116325803 0.0277215205 - 0.0279377103 0.0123844100 0.0297172107 - 0.0300485604 0.0131573398 0.0317482613 - 0.0322753713 0.0139512103 0.0338172801 - 0.0346145704 0.0147648398 0.0359307416 - 0.0370622911 0.0155985896 0.0380884185 - 0.0396122485 0.0164521597 0.0402955301 - 0.0422580615 0.0173262991 0.0425595492 - 0.0449894518 0.0182192009 0.0448740013 - 0.0477931984 0.0191300903 0.0472489484 - 0.0506595708 0.0200600196 0.0496853888 - 0.0535767302 0.0210082103 0.0521829911 - 0.0565305017 0.0219749697 0.0547407605 - 0.0595083982 0.0229592193 0.0573574007 - 0.0624992885 0.0239622109 0.0600296818 - 0.0654954314 0.0249828305 0.0627512708 - 0.0684895366 0.0260199904 0.0655123070 - 0.0714772493 0.0270722192 0.0683010966 - 0.0744543299 0.0281406008 0.0711084977 - 0.0774153098 0.0292240307 0.0739231929 - 0.0803605914 0.0303211194 0.0767356381 - 0.0832905099 0.0314316712 0.0795454979 - 0.0862074494 0.0325558111 0.0823479816 - 0.0891113579 0.0336936004 0.0851310268 - 0.0919999331 0.0348446108 0.0878951326 - 0.0948731005 0.0360087790 0.0906417519 - 0.0977329016 0.0371854194 0.0933719203 - 0.1005818024 0.0383735187 0.0960813016 - 0.1034189016 0.0395741388 0.0987711623 - 0.1062425002 0.0407866500 0.1014439017 - 0.1090549007 0.0420108810 0.1041020006 - 0.1118552014 0.0432458110 0.1067482978 - 0.1146425009 0.0444907583 0.1093854010 - 0.1174186990 0.0457457118 0.1120164022 - 0.1201832965 0.0470101312 0.1146423966 - 0.1229387969 0.0482850000 0.1172647029 - 0.1256874949 0.0495695882 0.1198822036 - 0.1284302026 0.0508622415 0.1224958971 - 0.1311666965 0.0521623604 0.1251043975 - 0.1338986009 0.0534698404 0.1277081966 - 0.1366281956 0.0547848493 0.1303080022 - 0.1393581927 0.0561086796 0.1329074055 - 0.1420904994 0.0574397892 0.1355095953 - 0.1448286027 0.0587780997 0.1381137967 - 0.1475744992 0.0601234213 0.1407177001 - 0.1503286064 0.0614760891 0.1433234960 - 0.1530916989 0.0628349632 0.1459300071 - 0.1558624953 0.0642008483 0.1485393941 - 0.1586423963 0.0655714497 0.1511545032 - 0.1614302993 0.0669477507 0.1537750065 - 0.1642321050 0.0683294088 0.1563979983 - 0.1670460999 0.0697180778 0.1590255052 - 0.1698731035 0.0711119920 0.1616581976 - 0.1727122962 0.0725102201 0.1642964929 - 0.1755629927 0.0739143416 0.1669408977 - 0.1784220040 0.0753230676 0.1695916951 - 0.1812902987 0.0767354220 0.1722505987 - 0.1841648072 0.0781524777 0.1749189049 - 0.1870469004 0.0795728117 0.1775977015 - 0.1899344027 0.0809954405 0.1802870035 - 0.1928229034 0.0824212879 0.1829870939 - 0.1957139969 0.0838513300 0.1856992990 - 0.1986058056 0.0852848813 0.1884211004 - 0.2014966011 0.0867218077 0.1911548972 - 0.2043849975 0.0881616324 0.1939021051 - 0.2072687000 0.0896041170 0.1966606975 - 0.2101463974 0.0910492018 0.1994311064 - 0.2130181938 0.0924970210 0.2022143006 - 0.2158842981 0.0939473733 0.2050091028 - 0.2187419981 0.0953999087 0.2078166008 - 0.2215912044 0.0968550667 0.2106339931 - 0.2244296968 0.0983112976 0.2134619057 - 0.2272589952 0.0997681171 0.2162988037 - 0.2300810069 0.1012267023 0.2191399932 - 0.2328950018 0.1026865989 0.2219882011 - 0.2356988937 0.1041480973 0.2248430997 - 0.2384911031 0.1056120992 0.2277006060 - 0.2412708998 0.1070773974 0.2305589020 - 0.2440378964 0.1085444018 0.2334149033 - 0.2467941940 0.1100130007 0.2362655997 - 0.2495391071 0.1114827022 0.2391107976 - 0.2522701919 0.1129537970 0.2419497073 - 0.2549884021 0.1144248024 0.2447791994 - 0.2576946914 0.1158969998 0.2475973070 - 0.2603886127 0.1173700988 0.2504028082 - 0.2630693913 0.1188448966 0.2531929910 - 0.2657378018 0.1203223020 0.2559669018 - 0.2683930099 0.1218004972 0.2587235868 - 0.2710328996 0.1232801974 0.2614609897 - 0.2736578882 0.1247610003 0.2641791999 - 0.2762688994 0.1262425929 0.2668775022 - 0.2788653970 0.1277254969 0.2695545852 - 0.2814469934 0.1292095035 0.2722114921 - 0.2840158045 0.1306941062 0.2748483121 - 0.2865726054 0.1321794987 0.2774635851 - 0.2891159058 0.1336652040 0.2800571024 - 0.2916449904 0.1351507008 0.2826282978 - 0.2941609025 0.1366363019 0.2851763070 - 0.2966657877 0.1381220967 0.2877005935 - 0.2991599143 0.1396085024 0.2902010083 - 0.3016422093 0.1410955936 0.2926791906 - 0.3041135073 0.1425838023 0.2951335907 - 0.3065750003 0.1440730989 0.2975631058 - 0.3090234101 0.1455630064 0.2999700010 - 0.3114587069 0.1470534056 0.3023560941 - 0.3138816059 0.1485445052 0.3047226965 - 0.3162932098 0.1500363946 0.3070712090 - 0.3186948001 0.1515289992 0.3094022870 - 0.3210870028 0.1530221999 0.3117175996 - 0.3234694898 0.1545169055 0.3140189052 - 0.3258418143 0.1560124010 0.3163051009 - 0.3282003999 0.1575089991 0.3185760975 - 0.3305430114 0.1590068042 0.3208332956 - 0.3328739107 0.1605062932 0.3230775893 - 0.3351961970 0.1620068997 0.3253093064 - 0.3375113010 0.1635068059 0.3275283873 - 0.3398194909 0.1650066972 0.3297384977 - 0.3421204984 0.1665070057 0.3319405913 - 0.3444133997 0.1680078954 0.3341340125 - 0.3466998041 0.1695086062 0.3363187015 - 0.3489798009 0.1710103005 0.3384949863 - 0.3512539864 0.1725132018 0.3406594098 - 0.3535234928 0.1740172058 0.3428128064 - 0.3557898104 0.1755214930 0.3449561894 - 0.3580540121 0.1770250052 0.3470906913 - 0.3603161871 0.1785289049 0.3492175043 - 0.3625768125 0.1800335944 0.3513374031 - 0.3648355901 0.1815388054 0.3534527123 - 0.3670929074 0.1830437034 0.3555639982 - 0.3693490922 0.1845479012 0.3576712012 - 0.3716045022 0.1860512048 0.3597705066 - 0.3738591969 0.1875541955 0.3618609011 - 0.3761132061 0.1890579015 0.3639445007 - 0.3783670068 0.1905619055 0.3660219014 - 0.3806206882 0.1920655966 0.3680920899 - 0.3828743994 0.1935691983 0.3701565862 - 0.3851284981 0.1950726956 0.3722172081 - 0.3873830140 0.1965761930 0.3742741942 - 0.3896380961 0.1980797052 0.3763268888 - 0.3918941915 0.1995833069 0.3783758879 - 0.3941515088 0.2010867000 0.3804208040 - 0.3964098990 0.2025887072 0.3824599087 - 0.3986696899 0.2040902972 0.3844920993 - 0.4009316862 0.2055917978 0.3865205944 - 0.4031954110 0.2070932984 0.3885455132 - 0.4054606855 0.2085942030 0.3905645013 - 0.4077295065 0.2100944966 0.3925797045 - 0.4100005031 0.2115938962 0.3945915103 - 0.4122735858 0.2130924016 0.3965987861 - 0.4145509005 0.2145905048 0.3986020982 - 0.4168311954 0.2160879970 0.4006024003 - 0.4191139042 0.2175852060 0.4025978148 - 0.4213995934 0.2190822959 0.4045889080 - 0.4236867130 0.2205785960 0.4065766037 - 0.4259749949 0.2220751941 0.4085580111 - 0.4282661080 0.2235720009 0.4105337858 - 0.4305579960 0.2250701934 0.4125058055 - 0.4328506887 0.2265686989 0.4144718051 - 0.4351426959 0.2280676961 0.4164347947 - 0.4374347031 0.2295663953 0.4183954895 - 0.4397264123 0.2310650051 0.4203527868 - 0.4420182109 0.2325633019 0.4223093092 - 0.4443092942 0.2340615988 0.4242646992 - 0.4465988874 0.2355599999 0.4262180924 - 0.4488858879 0.2370588928 0.4281705916 - 0.4511714876 0.2385578007 0.4301216900 - 0.4534544051 0.2400566041 0.4320701063 - 0.4557343125 0.2415551990 0.4340169132 - 0.4580109119 0.2430536002 0.4359580874 - 0.4602797031 0.2445522994 0.4378975034 - 0.4625451863 0.2460512072 0.4398345947 - 0.4648033977 0.2475509942 0.4417698979 - 0.4670571089 0.2490514964 0.4437046945 - 0.4693068862 0.2505528927 0.4456391931 - 0.4715498090 0.2520549893 0.4475742877 - 0.4737902880 0.2535574138 0.4495103061 - 0.4760245085 0.2550598979 0.4514476955 - 0.4782535136 0.2565625906 0.4533854127 - 0.4804762006 0.2580656111 0.4553222060 - 0.4826883078 0.2595695853 0.4572581053 - 0.4848966897 0.2610749006 0.4591920078 - 0.4870918095 0.2625809908 0.4611235857 - 0.4892837107 0.2640874088 0.4630536139 - 0.4914664030 0.2655943036 0.4649797976 - 0.4936434031 0.2671012878 0.4669052064 - 0.4958145916 0.2686108947 0.4688282013 - 0.4979768991 0.2701208889 0.4707511067 - 0.5001357198 0.2716321051 0.4726733863 - 0.5022798777 0.2731435895 0.4745965004 - 0.5044208765 0.2746556997 0.4765200913 - 0.5065475702 0.2761682868 0.4784458876 - 0.5086696744 0.2776814103 0.4803726077 - 0.5107814074 0.2791965902 0.4822996855 - 0.5128856897 0.2807131112 0.4842267036 - 0.5149818063 0.2822310030 0.4861502945 - 0.5170682073 0.2837496102 0.4880726039 - 0.5191485882 0.2852681875 0.4899899960 - 0.5212162137 0.2867869139 0.4919058979 - 0.5232796073 0.2883065045 0.4938189983 - 0.5253279209 0.2898262143 0.4957309067 - 0.5273739100 0.2913483083 0.4976414144 - 0.5294030905 0.2928704917 0.4995503128 - 0.5314307809 0.2943947911 0.5014581084 - 0.5334423184 0.2959192097 0.5033627748 - 0.5354515910 0.2974444032 0.5052657723 - 0.5374450088 0.2989695966 0.5071659088 - 0.5394350290 0.3004952967 0.5090647936 - 0.5414085984 0.3020209074 0.5109593868 - 0.5433781743 0.3035480082 0.5128529072 - 0.5453327894 0.3050751090 0.5147392750 - 0.5472834110 0.3066017032 0.5166242719 - 0.5492209792 0.3081282973 0.5185018182 - 0.5511540174 0.3096539080 0.5203778148 - 0.5530723929 0.3111793995 0.5222443938 - 0.5549861789 0.3127050102 0.5241091847 - 0.5568844080 0.3142305911 0.5259639025 - 0.5587790012 0.3157570958 0.5278167129 - 0.5606595278 0.3172838092 0.5296614766 - 0.5625373721 0.3188110888 0.5315043926 - 0.5644009113 0.3203384876 0.5333400965 - 0.5662627816 0.3218651116 0.5351735950 - 0.5681079030 0.3233912885 0.5369982123 - 0.5699527264 0.3249165118 0.5388190746 - 0.5717802048 0.3264403939 0.5406309962 - 0.5736076832 0.3279637098 0.5424374938 - 0.5754187703 0.3294858932 0.5442360044 - 0.5772284269 0.3310078979 0.5460273027 - 0.5790212750 0.3325291872 0.5478109121 - 0.5808100104 0.3340505064 0.5495834947 - 0.5825855732 0.3355714083 0.5513505936 - 0.5843548179 0.3370923996 0.5531036854 - 0.5861145854 0.3386130035 0.5548539758 - 0.5878657103 0.3401334882 0.5565863252 - 0.5896102190 0.3416540027 0.5583184958 - 0.5913426280 0.3431744874 0.5600315928 - 0.5930718184 0.3446949124 0.5617440939 - 0.5947852731 0.3462150991 0.5634422898 - 0.5964987874 0.3477351069 0.5651364923 - 0.5981938243 0.3492532969 0.5668197274 - 0.5998880863 0.3507714868 0.5684949160 - 0.6015682220 0.3522872925 0.5701624751 - 0.6032432914 0.3538025916 0.5718169212 - 0.6049085855 0.3553164005 0.5734683275 - 0.6065642238 0.3568288088 0.5750986934 - 0.6082149148 0.3583407104 0.5767289996 - 0.6098505855 0.3598510027 0.5783388019 - 0.6114861965 0.3613612056 0.5799455047 - 0.6131017208 0.3628711998 0.5815384984 - 0.6147162914 0.3643811047 0.5831218958 - 0.6163176894 0.3658905029 0.5846983194 - 0.6179122925 0.3673991859 0.5862581134 - 0.6195003986 0.3689076900 0.5878177881 - 0.6210765243 0.3704144955 0.5893548727 - 0.6226522923 0.3719212115 0.5908914208 - 0.6242104173 0.3734270036 0.5924147964 - 0.6257684827 0.3749324977 0.5939309001 - 0.6273151040 0.3764374852 0.5954414010 - 0.6288558245 0.3779416084 0.5969383121 - 0.6303917170 0.3794457912 0.5984352231 - 0.6319159865 0.3809471130 0.5999152064 - 0.6334401965 0.3824482858 0.6013936996 - 0.6349474788 0.3839477897 0.6028618217 - 0.6364527941 0.3854453862 0.6043204069 - 0.6379473805 0.3869430125 0.6057764888 - 0.6394302845 0.3884387910 0.6072123051 - 0.6409124732 0.3899345100 0.6086481214 - 0.6423766017 0.3914293945 0.6100676060 - 0.6438407898 0.3929235041 0.6114792824 - 0.6452940702 0.3944176137 0.6128858924 - 0.6467397809 0.3959099948 0.6142745018 - 0.6481837034 0.3974024057 0.6156631708 - 0.6496129036 0.3988932073 0.6170371175 - 0.6510421038 0.4003823996 0.6184059978 - 0.6524606943 0.4018715918 0.6197704077 - 0.6538727880 0.4033572972 0.6211217046 - 0.6552829146 0.4048427939 0.6224728823 - 0.6566761136 0.4063270986 0.6238120794 - 0.6580693126 0.4078091085 0.6251469254 - 0.6594510078 0.4092912078 0.6264786720 - 0.6608237028 0.4107702971 0.6277983785 - 0.6621962786 0.4122484028 0.6291180253 - 0.6635506153 0.4137260914 0.6304268837 - 0.6649050117 0.4152004123 0.6317300200 - 0.6662514806 0.4166747928 0.6330316067 - 0.6675873995 0.4181470871 0.6343147755 - 0.6689231992 0.4196172953 0.6355978847 - 0.6702443957 0.4210875034 0.6368724108 - 0.6715623736 0.4225546122 0.6381372809 - 0.6728767753 0.4240212142 0.6394020915 - 0.6741765738 0.4254870117 0.6406546235 - 0.6754763126 0.4269487858 0.6419048905 - 0.6767659187 0.4284105897 0.6431524754 - 0.6780474782 0.4298701882 0.6443892717 - 0.6793289781 0.4313269854 0.6456261277 - 0.6805943847 0.4327838123 0.6468564272 - 0.6818578839 0.4342373013 0.6480810046 - 0.6831179857 0.4356895089 0.6493057013 - 0.6843643785 0.4371415973 0.6505178809 - 0.6856108904 0.4385890961 0.6517279744 - 0.6868489981 0.4400365055 0.6529359818 - 0.6880782843 0.4414829910 0.6541314721 - 0.6893076897 0.4429262877 0.6553270221 - 0.6905233264 0.4443696141 0.6565173864 - 0.6917344928 0.4458107948 0.6577004194 - 0.6929457188 0.4472492039 0.6588833928 - 0.6941388845 0.4486874938 0.6600583196 - 0.6953321099 0.4501225054 0.6612290740 - 0.6965216994 0.4515551925 0.6623998284 - 0.6976982951 0.4529877901 0.6635617018 - 0.6988748908 0.4544155896 0.6647223234 - 0.7000442743 0.4558416903 0.6658821106 - 0.7012037039 0.4572679102 0.6670321822 - 0.7023630142 0.4586881995 0.6681821942 - 0.7035111785 0.4601078033 0.6693289876 - 0.7046517730 0.4615274072 0.6704663038 - 0.7057924271 0.4629419148 0.6716036797 - 0.7069209814 0.4643563032 0.6727368832 - 0.7080454230 0.4657703936 0.6738638878 - 0.7091699243 0.4671806991 0.6749910116 - 0.7102797031 0.4685910046 0.6761143208 - 0.7113872766 0.4700005949 0.6772344112 - 0.7124949098 0.4714061916 0.6783545017 - 0.7135862708 0.4728117883 0.6794689894 - 0.7146772146 0.4742164910 0.6805803180 - 0.7157673240 0.4756168127 0.6816917062 - 0.7168418169 0.4770171046 0.6827973723 - 0.7179161906 0.4784162939 0.6839008927 - 0.7189890146 0.4798108041 0.6850042939 - 0.7200477719 0.4812051952 0.6861020923 - 0.7211065292 0.4825986028 0.6871982217 - 0.7221630812 0.4839865863 0.6882944107 - 0.7232058048 0.4853746891 0.6893844008 - 0.7242485285 0.4867616892 0.6904730201 - 0.7252889872 0.4881420135 0.6915615797 - 0.7263165712 0.4895223975 0.6926428080 - 0.7273442149 0.4909023046 0.6937221885 - 0.7283703089 0.4922760129 0.6948016882 - 0.7293847203 0.4936496913 0.6958737969 - 0.7303991914 0.4950233996 0.6969439983 - 0.7314124703 0.4963918030 0.6980140805 - 0.7324116230 0.4977600873 0.6990771294 - 0.7334107161 0.4991284013 0.7001376152 - 0.7344096899 0.5004916787 0.7011982203 - 0.7353922725 0.5018543005 0.7022535801 - 0.7363749146 0.5032168031 0.7033064961 - 0.7373574972 0.5045748949 0.7043594122 - 0.7383249998 0.5059313178 0.7054070830 - 0.7392910719 0.5072876811 0.7064511180 - 0.7402572036 0.5086405277 0.7074950933 - 0.7412112951 0.5099905133 0.7085348964 - 0.7421622276 0.5113404989 0.7095698714 - 0.7431129813 0.5126873255 0.7106049061 - 0.7440556288 0.5140289068 0.7116366029 - 0.7449935079 0.5153704286 0.7126612067 - 0.7459315062 0.5167104006 0.7136859298 - 0.7468624711 0.5180423856 0.7147092223 - 0.7477856874 0.5193743110 0.7157247066 - 0.7487087846 0.5207061768 0.7167401910 - 0.7496278286 0.5220301747 0.7177556157 - 0.7505353093 0.5233532786 0.7187631726 - 0.7514427900 0.5246765018 0.7197703123 - 0.7523496747 0.5259947777 0.7207775116 - 0.7532436848 0.5273103714 0.7217786908 - 0.7541376948 0.5286259055 0.7227776051 - 0.7550317049 0.5299391747 0.7237765789 - 0.7559167743 0.5312479138 0.7247710824 - 0.7567995787 0.5325565934 0.7257605791 - 0.7576823831 0.5338652730 0.7267500758 - 0.7585588098 0.5351672173 0.7277382016 - 0.7594292164 0.5364691019 0.7287198901 - 0.7602995038 0.5377709866 0.7297015786 - 0.7611671090 0.5390685201 0.7306833863 - 0.7620239258 0.5403637290 0.7316588759 - 0.7628808022 0.5416588783 0.7326335907 - 0.7637376189 0.5429524183 0.7336083055 - 0.7645832896 0.5442410111 0.7345783710 - 0.7654271722 0.5455294847 0.7355446815 - 0.7662711740 0.5468180776 0.7365108728 - 0.7671089768 0.5481011868 0.7374755144 - 0.7679408789 0.5493832231 0.7384330034 - 0.7687727213 0.5506651998 0.7393903732 - 0.7696030140 0.5519440174 0.7403479218 - 0.7704212070 0.5532178879 0.7412980795 - 0.7712393999 0.5544918776 0.7422463894 - 0.7720575929 0.5557658076 0.7431948185 - 0.7728673816 0.5570312142 0.7441394925 - 0.7736734152 0.5582960248 0.7450783849 - 0.7744793892 0.5595608950 0.7460172772 - 0.7752828002 0.5608223081 0.7469562292 - 0.7760779858 0.5620794296 0.7478839755 - 0.7768731713 0.5633366108 0.7488111258 - 0.7776684761 0.5645936728 0.7497382164 - 0.7784557939 0.5658441782 0.7506607175 - 0.7792407274 0.5670942068 0.7515779138 - 0.7800256014 0.5683441162 0.7524952292 - 0.7808077931 0.5695917010 0.7534124255 - 0.7815821767 0.5708349943 0.7543212175 - 0.7823565006 0.5720782280 0.7552297115 - 0.7831308246 0.5733215213 0.7561380863 - 0.7838966846 0.5745599866 0.7570431232 - 0.7846595049 0.5757972002 0.7579433918 - 0.7854222059 0.5770342946 0.7588437200 - 0.7861832976 0.5782706141 0.7597439885 - 0.7869359851 0.5795021057 0.7606366277 - 0.7876886725 0.5807337165 0.7615280747 - 0.7884414196 0.5819652081 0.7624195814 - 0.7891888022 0.5831941962 0.7633082867 - 0.7899320722 0.5844206810 0.7641898990 - 0.7906754017 0.5856472254 0.7650715113 - 0.7914186716 0.5868737102 0.7659531236 - 0.7921544909 0.5880939960 0.7668281794 - 0.7928900123 0.5893129706 0.7676998973 - 0.7936254144 0.5905321240 0.7685714960 - 0.7943581939 0.5917500257 0.7694430947 - 0.7950845957 0.5929607153 0.7703047991 - 0.7958109975 0.5941714048 0.7711665034 - 0.7965373993 0.5953819752 0.7720282078 - 0.7972583771 0.5965896845 0.7728871703 - 0.7979758978 0.5977926850 0.7737402916 - 0.7986934781 0.5989956856 0.7745934725 - 0.7994109988 0.6001986861 0.7754465938 - 0.8001213074 0.6013975143 0.7762938142 - 0.8008307815 0.6025936007 0.7771372199 - 0.8015401959 0.6037898064 0.7779806256 - 0.8022487164 0.6049858928 0.7788239121 - 0.8029516935 0.6061770916 0.7796577811 - 0.8036546707 0.6073670983 0.7804902196 - 0.8043577075 0.6085572243 0.7813224792 - 0.8050580025 0.6097472906 0.7821539044 - 0.8057534099 0.6109319925 0.7829759717 - 0.8064488769 0.6121168137 0.7837980986 - 0.8071444035 0.6133015156 0.7846202254 - 0.8078358769 0.6144853234 0.7854393721 - 0.8085243106 0.6156625152 0.7862510085 - 0.8092126846 0.6168397069 0.7870625854 - 0.8099011183 0.6180168986 0.7878742218 - 0.8105850816 0.6191921234 0.7886819839 - 0.8112673163 0.6203610897 0.7894849777 - 0.8119494915 0.6215299964 0.7902880907 - 0.8126317859 0.6226990223 0.7910910845 - 0.8133102059 0.6238657236 0.7918882966 - 0.8139879704 0.6250272989 0.7926809788 - 0.8146657944 0.6261889935 0.7934737802 - 0.8153436184 0.6273506284 0.7942665815 - 0.8160151243 0.6285101175 0.7950521708 - 0.8166863918 0.6296654940 0.7958343029 - 0.8173575997 0.6308208108 0.7966163754 - 0.8180288076 0.6319761872 0.7973985076 - 0.8186935186 0.6331295967 0.7981728911 - 0.8193582296 0.6342797279 0.7989445925 - 0.8200228810 0.6354296803 0.7997162938 - 0.8206874728 0.6365796924 0.8004879951 - 0.8213469982 0.6377279162 0.8012521267 - 0.8220065236 0.6388722062 0.8020138741 - 0.8226659894 0.6400164962 0.8027758002 - 0.8233252764 0.6411607862 0.8035376072 - 0.8239793777 0.6423032880 0.8042908907 - 0.8246334791 0.6434404850 0.8050416708 - 0.8252875805 0.6445778012 0.8057923913 - 0.8259416819 0.6457151175 0.8065431714 - 0.8265889883 0.6468511820 0.8072862029 - 0.8272364140 0.6479817033 0.8080263734 - 0.8278837204 0.6491122246 0.8087667227 - 0.8285310864 0.6502428055 0.8095068932 - 0.8291730285 0.6513730288 0.8102409840 - 0.8298146129 0.6524980068 0.8109720945 - 0.8304561973 0.6536229849 0.8117030859 - 0.8310977817 0.6547480226 0.8124341965 - 0.8317347765 0.6558730006 0.8131592870 - 0.8323708773 0.6569942236 0.8138797283 - 0.8330070972 0.6581149101 0.8146002293 - 0.8336431980 0.6592354774 0.8153206110 - 0.8342744112 0.6603562236 0.8160365224 - 0.8349034786 0.6614738107 0.8167462945 - 0.8355324864 0.6625900865 0.8174561262 - 0.8361616135 0.6637063026 0.8181658983 - 0.8367878199 0.6648225188 0.8188729882 - 0.8374114037 0.6659368873 0.8195725083 - 0.8380349278 0.6670492291 0.8202719092 - 0.8386585116 0.6681613922 0.8209714293 - 0.8392807245 0.6692736149 0.8216704130 - 0.8399001956 0.6703848839 0.8223608732 - 0.8405197263 0.6714920998 0.8230513930 - 0.8411391973 0.6725993156 0.8237419128 - 0.8417584896 0.6737064719 0.8244323730 - 0.8423752785 0.6748136878 0.8251157999 - 0.8429921865 0.6759157181 0.8257976770 - 0.8436089754 0.6770169139 0.8264794946 - 0.8442258835 0.6781182289 0.8271613717 - 0.8448395133 0.6792194843 0.8278384805 - 0.8454523087 0.6803181767 0.8285114765 - 0.8460649848 0.6814149022 0.8291844726 - 0.8466777802 0.6825116277 0.8298574090 - 0.8472878933 0.6836082935 0.8305279016 - 0.8478950858 0.6847041845 0.8311896920 - 0.8485022783 0.6857963204 0.8318514228 - 0.8491094708 0.6868883967 0.8325132132 - 0.8497161865 0.6879804730 0.8331748843 - 0.8503192067 0.6890726089 0.8338286281 - 0.8509222865 0.6901617050 0.8344811201 - 0.8515254259 0.6912497878 0.8351336718 - 0.8521285057 0.6923378110 0.8357862830 - 0.8527284265 0.6934258938 0.8364341855 - 0.8533272147 0.6945127249 0.8370773792 - 0.8539260030 0.6955966949 0.8377205729 - 0.8545249104 0.6966806054 0.8383638859 - 0.8551219106 0.6977645755 0.8390063047 - 0.8557152152 0.6988484859 0.8396390080 - 0.8563085794 0.6999288797 0.8402717710 - 0.8569018841 0.7010084987 0.8409044743 - 0.8574953079 0.7020881176 0.8415372968 - 0.8580844998 0.7031677961 0.8421642184 - 0.8586729765 0.7042456865 0.8427875042 - 0.8592615724 0.7053195238 0.8434109092 - 0.8598501086 0.7063934207 0.8440341949 - 0.8604366779 0.7074673176 0.8446565270 - 0.8610199094 0.7085412145 0.8452715874 - 0.8616030812 0.7096114159 0.8458867073 - 0.8621863723 0.7106803060 0.8465017080 - 0.8627696037 0.7117491961 0.8471168280 - 0.8633472919 0.7128180265 0.8477255702 - 0.8639240861 0.7138860226 0.8483303189 - 0.8645008206 0.7149490118 0.8489350080 - 0.8650774956 0.7160121202 0.8495395780 - 0.8656529784 0.7170751095 0.8501440287 - 0.8662251234 0.7181380987 0.8507388234 - 0.8667972088 0.7191970944 0.8513334990 - 0.8673692942 0.7202522755 0.8519282937 - 0.8679413795 0.7213075161 0.8525230885 - 0.8685104251 0.7223626971 0.8531140089 - 0.8690779209 0.7234178782 0.8536998034 - 0.8696454167 0.7244672775 0.8542855978 - 0.8702129126 0.7255154848 0.8548712730 - 0.8707802892 0.7265638113 0.8554571271 - 0.8713405132 0.7276120186 0.8560361862 - 0.8719006181 0.7286595702 0.8566132784 - 0.8724607825 0.7297024131 0.8571904898 - 0.8730208874 0.7307453156 0.8577675819 - 0.8735787272 0.7317880988 0.8583437204 - 0.8741317987 0.7328310013 0.8589094877 - 0.8746848106 0.7338718176 0.8594753146 - 0.8752378821 0.7349092960 0.8600410819 - 0.8757908940 0.7359467745 0.8606069088 - 0.8763406873 0.7369843125 0.8611692190 - 0.8768883944 0.7380216718 0.8617258072 - 0.8774359822 0.7390558720 0.8622822762 - 0.8779836893 0.7400876284 0.8628388047 - 0.8785312772 0.7411193848 0.8633952737 - 0.8790736198 0.7421510816 0.8639466763 - 0.8796147704 0.7431827784 0.8644946814 - 0.8801559806 0.7442104220 0.8650428057 - 0.8806971908 0.7452365756 0.8655908108 - 0.8812381029 0.7462627888 0.8661388159 - 0.8817703724 0.7472890019 0.8666791916 - 0.8823025823 0.7483152151 0.8672177196 - 0.8828349113 0.7493349910 0.8677561879 - 0.8833671212 0.7503538132 0.8682947159 - 0.8838980198 0.7513726950 0.8688331842 - 0.8844233751 0.7523915172 0.8693615198 - 0.8849487901 0.7534103990 0.8698894978 - 0.8854740858 0.7544203997 0.8704175949 - 0.8859995008 0.7554299831 0.8709455729 - 0.8865231276 0.7564396262 0.8714727163 - 0.8870428205 0.7574492097 0.8719913960 - 0.8875625134 0.7584586740 0.8725100160 - 0.8880822062 0.7594599128 0.8730286956 - 0.8886018991 0.7604610920 0.8735473752 - 0.8891186714 0.7614622712 0.8740645051 - 0.8896307945 0.7624635100 0.8745744824 - 0.8901429176 0.7634646297 0.8750844002 - 0.8906549811 0.7644580007 0.8755943179 - 0.8911671042 0.7654513717 0.8761042953 - 0.8916761875 0.7664448023 0.8766121268 - 0.8921812177 0.7674381733 0.8771117926 - 0.8926861882 0.7684317231 0.8776115179 - 0.8931912184 0.7694169879 0.8781113029 - 0.8936961889 0.7704021931 0.8786110282 - 0.8941987157 0.7713873982 0.8791092038 - 0.8946977854 0.7723724842 0.8796014190 - 0.8951969147 0.7733576894 0.8800935745 - 0.8956959844 0.7743353844 0.8805857897 - 0.8961951137 0.7753123045 0.8810780048 - 0.8966916800 0.7762892246 0.8815690279 - 0.8971840739 0.7772660851 0.8820533156 - 0.8976764083 0.7782430053 0.8825377226 - 0.8981688023 0.7792145014 0.8830221295 - 0.8986610770 0.7801843882 0.8835064173 - 0.8991513848 0.7811542749 0.8839902282 - 0.8996360898 0.7821242809 0.8844659925 - 0.9001209140 0.7830942273 0.8849416971 - 0.9006056190 0.7840591073 0.8854175210 - 0.9010903835 0.7850208879 0.8858932257 - 0.9015741944 0.7859826088 0.8863689899 - 0.9020525813 0.7869443893 0.8868358731 - 0.9025310278 0.7879061103 0.8873022199 - 0.9030094147 0.7888634801 0.8877685070 - 0.9034878016 0.7898142934 0.8882347941 - 0.9039661884 0.7907652259 0.8887010813 - 0.9044383764 0.7917159796 0.8891620040 - 0.9049103856 0.7926669121 0.8896213174 - 0.9053822756 0.7936159968 0.8900805712 - 0.9058542848 0.7945556045 0.8905398846 - 0.9063262939 0.7954952717 0.8909991980 - 0.9067928195 0.7964348793 0.8914546967 - 0.9072574973 0.7973746061 0.8919070959 - 0.9077221751 0.7983142138 0.8923594952 - 0.9081869125 0.7992444038 0.8928118944 - 0.9086515903 0.8001731038 0.8932642937 - 0.9091123939 0.8011018038 0.8937140107 - 0.9095693231 0.8020305037 0.8941572905 - 0.9100260735 0.8029590845 0.8946006894 - 0.9104830027 0.8038828969 0.8950440288 - 0.9109398127 0.8048021793 0.8954873085 - 0.9113954902 0.8057215214 0.8959305882 - 0.9118455052 0.8066408038 0.8963645101 - 0.9122955799 0.8075600863 0.8967983127 - 0.9127457142 0.8084779978 0.8972321153 - 0.9131957889 0.8093873858 0.8976659179 - 0.9136459231 0.8102967739 0.8980997205 - 0.9140903950 0.8112062812 0.8985280991 - 0.9145337939 0.8121157289 0.8989533782 - 0.9149773121 0.8130251169 0.8993787169 - 0.9154207110 0.8139269948 0.8998039961 - 0.9158641100 0.8148258924 0.9002293944 - 0.9163044095 0.8157246709 0.9006530046 - 0.9167405963 0.8166236281 0.9010717869 - 0.9171767831 0.8175224066 0.9014906287 - 0.9176130295 0.8184180260 0.9019094110 - 0.9180492163 0.8193030953 0.9023281932 - 0.9184855223 0.8201882243 0.9027469754 - 0.9189139009 0.8210734129 0.9031597972 - 0.9193422794 0.8219584823 0.9035711884 - 0.9197707772 0.8228436112 0.9039825797 - 0.9201992154 0.8237193823 0.9043939710 - 0.9206275940 0.8245916963 0.9048054814 - 0.9210522175 0.8254640102 0.9052137733 - 0.9214730859 0.8263362050 0.9056159854 - 0.9218940139 0.8272085190 0.9060181975 - 0.9223148823 0.8280786276 0.9064204097 - 0.9227359295 0.8289378285 0.9068226218 - 0.9231567979 0.8297969103 0.9072247744 - 0.9235706925 0.8306559920 0.9076194763 - 0.9239845872 0.8315150738 0.9080122113 - 0.9243984222 0.8323742747 0.9084050059 - 0.9248123169 0.8332263231 0.9087976813 - 0.9252260923 0.8340724111 0.9091904759 - 0.9256373048 0.8349186182 0.9095817208 - 0.9260444045 0.8357648253 0.9099674821 - 0.9264515042 0.8366109729 0.9103533030 - 0.9268587232 0.8374571204 0.9107391834 - 0.9272658229 0.8382914066 0.9111250043 - 0.9276729822 0.8391243219 0.9115108252 - 0.9280738831 0.8399571776 0.9118928909 - 0.9284729958 0.8407900929 0.9122726917 - 0.9288722277 0.8416228890 0.9126523733 - 0.9292714000 0.8424531221 0.9130321145 - 0.9296705723 0.8432716131 0.9134117961 - 0.9300686717 0.8440901041 0.9137915969 - 0.9304581881 0.8449085951 0.9141638279 - 0.9308475852 0.8457270861 0.9145355821 - 0.9312371016 0.8465456963 0.9149073958 - 0.9316264987 0.8473572731 0.9152792096 - 0.9320160151 0.8481602073 0.9156510234 - 0.9324020743 0.8489630818 0.9160208702 - 0.9327831268 0.8497660160 0.9163845181 - 0.9331640005 0.8505688906 0.9167479873 - 0.9335449934 0.8513718247 0.9171115160 - 0.9339259863 0.8521640897 0.9174751043 - 0.9343069196 0.8529503942 0.9178385735 - 0.9346833229 0.8537368178 0.9181991816 - 0.9350569844 0.8545231819 0.9185562730 - 0.9354307055 0.8553094864 0.9189134836 - 0.9358044267 0.8560959101 0.9192706943 - 0.9361780882 0.8568696976 0.9196279049 - 0.9365519285 0.8576403260 0.9199851155 - 0.9369189143 0.8584108949 0.9203380942 - 0.9372844100 0.8591815233 0.9206891060 - 0.9376500249 0.8599520922 0.9210399985 - 0.9380155802 0.8607226014 0.9213908911 - 0.9383810759 0.8614792228 0.9217417836 - 0.9387466908 0.8622341156 0.9220927954 - 0.9391040802 0.8629890084 0.9224383831 - 0.9394611716 0.8637439013 0.9227827191 - 0.9398183227 0.8644987941 0.9231269956 - 0.9401754141 0.8652538061 0.9234712720 - 0.9405325055 0.8659942150 0.9238156080 - 0.9408891797 0.8667339087 0.9241598845 - 0.9412378073 0.8674736023 0.9244983196 - 0.9415863752 0.8682132959 0.9248359799 - 0.9419351220 0.8689528704 0.9251735806 - 0.9422836900 0.8696926236 0.9255113006 - 0.9426323175 0.8704177737 0.9258490205 - 0.9429801106 0.8711423278 0.9261866808 - 0.9433202147 0.8718668222 0.9265180230 - 0.9436603189 0.8725913167 0.9268490076 - 0.9440004230 0.8733158112 0.9271798730 - 0.9443404078 0.8740403056 0.9275109172 - 0.9446805120 0.8747491241 0.9278417826 - 0.9450199008 0.8754562736 0.9281728268 - 0.9453521967 0.8761636019 0.9284992814 - 0.9456846118 0.8768708110 0.9288254976 - 0.9460169077 0.8775780797 0.9291517138 - 0.9463492036 0.8782854080 0.9294778705 - 0.9466814995 0.8789795041 0.9298040867 - 0.9470136166 0.8796706796 0.9301303029 - 0.9473391175 0.8803619146 0.9304519296 - 0.9476644993 0.8810532093 0.9307729006 - 0.9479898810 0.8817443848 0.9310938716 - 0.9483153224 0.8824356198 0.9314149022 - 0.9486407042 0.8831164241 0.9317358732 - 0.9489660859 0.8837922215 0.9320567846 - 0.9492859244 0.8844680190 0.9323734045 - 0.9496051073 0.8851438165 0.9326885939 - 0.9499244094 0.8858196735 0.9330039024 - 0.9502437115 0.8864954710 0.9333190918 - 0.9505628943 0.8871642947 0.9336344004 - 0.9508821964 0.8878256083 0.9339495897 - 0.9511961937 0.8884869218 0.9342616200 - 0.9515085220 0.8891481161 0.9345716238 - 0.9518209100 0.8898094296 0.9348815084 - 0.9521332979 0.8904706836 0.9351915121 - 0.9524456263 0.8911284804 0.9355015159 - 0.9527580142 0.8917747736 0.9358114004 - 0.9530653954 0.8924211264 0.9361190200 - 0.9533687830 0.8930674195 0.9364230037 - 0.9536721706 0.8937137723 0.9367269874 - 0.9539756179 0.8943601251 0.9370309711 - 0.9542790055 0.8950064182 0.9373350739 - 0.9545822740 0.8956375122 0.9376391172 - 0.9548830986 0.8962674737 0.9379425049 - 0.9551774263 0.8968974948 0.9382423162 - 0.9554716945 0.8975275159 0.9385421276 - 0.9557660222 0.8981574774 0.9388418794 - 0.9560602903 0.8987876177 0.9391416907 - 0.9563546181 0.8994089961 0.9394415021 - 0.9566488862 0.9000241756 0.9397413135 - 0.9569364190 0.9006392956 0.9400386810 - 0.9572235942 0.9012544751 0.9403355718 - 0.9575108886 0.9018695951 0.9406325221 - 0.9577981830 0.9024847746 0.9409294128 - 0.9580854177 0.9030973911 0.9412263036 - 0.9583727121 0.9036974907 0.9415231943 - 0.9586551785 0.9042975903 0.9418178201 - 0.9589346051 0.9048978090 0.9421097040 - 0.9592139721 0.9054979086 0.9424015880 - 0.9594933987 0.9060980082 0.9426935911 - 0.9597727060 0.9066982269 0.9429854751 - 0.9600520730 0.9072868228 0.9432774782 - 0.9603300095 0.9078708291 0.9435690045 - 0.9606015086 0.9084547758 0.9438548088 - 0.9608730078 0.9090389013 0.9441404939 - 0.9611443877 0.9096229076 0.9444261789 - 0.9614158869 0.9102069139 0.9447119236 - 0.9616873264 0.9107877016 0.9449977279 - 0.9619588256 0.9113559127 0.9452834129 - 0.9622257948 0.9119240046 0.9455662966 - 0.9624909163 0.9124920964 0.9458472729 - 0.9627560973 0.9130601883 0.9461283088 - 0.9630212784 0.9136283994 0.9464092255 - 0.9632865191 0.9141964912 0.9466902018 - 0.9635517001 0.9147549272 0.9469711781 - 0.9638155103 0.9153077006 0.9472519755 - 0.9640734196 0.9158604145 0.9475296736 - 0.9643313289 0.9164131880 0.9478074908 - 0.9645891786 0.9169659019 0.9480851889 - 0.9648472071 0.9175186753 0.9483630061 - 0.9651051164 0.9180713296 0.9486407042 - 0.9653630257 0.9186087847 0.9489185214 - 0.9656159282 0.9191462994 0.9491940737 - 0.9658656120 0.9196838140 0.9494674206 - 0.9661152959 0.9202213287 0.9497408271 - 0.9663649201 0.9207587838 0.9500141144 - 0.9666146040 0.9212962985 0.9502874017 - 0.9668642879 0.9218286872 0.9505608082 - 0.9671139121 0.9223511219 0.9508340955 - 0.9673559070 0.9228736162 0.9511033893 - 0.9675977230 0.9233959913 0.9513720870 - 0.9678394198 0.9239184856 0.9516407251 - 0.9680811763 0.9244409204 0.9519094229 - 0.9683228731 0.9249634147 0.9521780014 - 0.9685646892 0.9254769087 0.9524466991 - 0.9688041806 0.9259846210 0.9527146220 - 0.9690378904 0.9264922738 0.9529792070 - 0.9692716002 0.9269999862 0.9532437921 - 0.9695051908 0.9275076985 0.9535083771 - 0.9697389007 0.9280154109 0.9537730217 - 0.9699726105 0.9285231233 0.9540376067 - 0.9702062011 0.9290189743 0.9543021917 - 0.9704347849 0.9295123816 0.9545649290 - 0.9706588984 0.9300057292 0.9548249841 - 0.9708830118 0.9304990172 0.9550849795 - 0.9711070061 0.9309923053 0.9553450942 - 0.9713311195 0.9314855933 0.9556052089 - 0.9715551734 0.9319790006 0.9558652043 - 0.9717792273 0.9324582815 0.9561253190 - 0.9719976187 0.9329372048 0.9563831091 - 0.9722136855 0.9334161282 0.9566395879 - 0.9724298716 0.9338951111 0.9568961263 - 0.9726459980 0.9343739748 0.9571526051 - 0.9728621840 0.9348528981 0.9574090838 - 0.9730783105 0.9353309274 0.9576656222 - 0.9732944965 0.9357969165 0.9579221010 - 0.9735053778 0.9362627864 0.9581764936 - 0.9737156034 0.9367287159 0.9584304094 - 0.9739257097 0.9371945858 0.9586842060 - 0.9741358161 0.9376605749 0.9589381218 - 0.9743459821 0.9381265044 0.9591919184 - 0.9745560884 0.9385910034 0.9594457746 - 0.9747661948 0.9390441179 0.9596995711 - 0.9749692082 0.9394972920 0.9599508047 - 0.9751719236 0.9399505258 0.9602015018 - 0.9753745198 0.9404035807 0.9604521990 - 0.9755771160 0.9408568144 0.9607028961 - 0.9757797718 0.9413099289 0.9609535933 - 0.9759824276 0.9417619109 0.9612042904 - 0.9761850238 0.9422025084 0.9614549875 - 0.9763807058 0.9426429868 0.9617013931 - 0.9765762091 0.9430835843 0.9619473219 - 0.9767717123 0.9435241818 0.9621930718 - 0.9769670963 0.9439647198 0.9624388814 - 0.9771625996 0.9444053173 0.9626846910 - 0.9773581028 0.9448456168 0.9629306197 - 0.9775536060 0.9452726245 0.9631764293 - 0.9777433276 0.9456995726 0.9634190798 - 0.9779326916 0.9461265802 0.9636611938 - 0.9781219959 0.9465535879 0.9639033079 - 0.9783114195 0.9469807148 0.9641454220 - 0.9785007238 0.9474077225 0.9643874764 - 0.9786900878 0.9478346705 0.9646295905 - 0.9788795114 0.9482499957 0.9648717046 - 0.9790633917 0.9486637115 0.9651131034 - 0.9792463183 0.9490774870 0.9653542042 - 0.9794291854 0.9494913220 0.9655953050 - 0.9796121716 0.9499049783 0.9658364058 - 0.9797950983 0.9503188133 0.9660773873 - 0.9799780846 0.9507325888 0.9663184881 - 0.9801610112 0.9511371255 0.9665595889 - 0.9803391099 0.9515374899 0.9667994976 - 0.9805151224 0.9519379735 0.9670386910 - 0.9806911945 0.9523383975 0.9672778249 - 0.9808672071 0.9527388215 0.9675168991 - 0.9810432196 0.9531391859 0.9677560925 - 0.9812191725 0.9535397291 0.9679952264 - 0.9813953042 0.9539344907 0.9682343006 - 0.9815682173 0.9543222785 0.9684708714 - 0.9817377925 0.9547100067 0.9687036872 - 0.9819074273 0.9550976753 0.9689365029 - 0.9820770025 0.9554854035 0.9691691995 - 0.9822465777 0.9558731914 0.9694020152 - 0.9824162722 0.9562609196 0.9696347713 - 0.9825859070 0.9566473961 0.9698675871 - 0.9827542901 0.9570254087 0.9700998068 - 0.9829186797 0.9574033022 0.9703279734 - 0.9830831289 0.9577813148 0.9705563188 - 0.9832473993 0.9581593275 0.9707846045 - 0.9834117889 0.9585372210 0.9710127711 - 0.9835761786 0.9589151740 0.9712411165 - 0.9837405086 0.9592931867 0.9714694023 - 0.9839048982 0.9596645832 0.9716976285 - 0.9840624928 0.9600340128 0.9719249010 - 0.9842191935 0.9604033232 0.9721521139 - 0.9843758941 0.9607725739 0.9723792076 - 0.9845327139 0.9611418843 0.9726063013 - 0.9846894145 0.9615113139 0.9728333950 - 0.9848461747 0.9618806243 0.9730604887 - 0.9850028753 0.9622468948 0.9732875824 - 0.9851561785 0.9626067877 0.9735137820 - 0.9853060246 0.9629666209 0.9737386107 - 0.9854558110 0.9633265138 0.9739633799 - 0.9856055975 0.9636862874 0.9741883278 - 0.9857553840 0.9640461802 0.9744130969 - 0.9859051704 0.9644060135 0.9746379256 - 0.9860550165 0.9647659063 0.9748628139 - 0.9862046242 0.9651184082 0.9750875831 - 0.9863498807 0.9654694200 0.9753091931 - 0.9864953160 0.9658203125 0.9755306840 - 0.9866406918 0.9661713243 0.9757522941 - 0.9867861271 0.9665222168 0.9759737849 - 0.9869315028 0.9668732285 0.9761952758 - 0.9870768785 0.9672241211 0.9764168859 - 0.9872221947 0.9675728083 0.9766383767 - 0.9873657823 0.9679149985 0.9768583179 - 0.9875077009 0.9682573080 0.9770764112 - 0.9876495004 0.9685994983 0.9772945046 - 0.9877914190 0.9689416885 0.9775127172 - 0.9879332781 0.9692839980 0.9777308106 - 0.9880751967 0.9696261883 0.9779489040 - 0.9882171154 0.9699683785 0.9781669974 - 0.9883589745 0.9703047872 0.9783850908 - 0.9884964824 0.9706377983 0.9786008000 - 0.9886338115 0.9709706903 0.9788162708 - 0.9887710214 0.9713037014 0.9790318012 - 0.9889082909 0.9716365933 0.9792472720 - 0.9890456200 0.9719696045 0.9794626832 - 0.9891828895 0.9723024964 0.9796782136 - 0.9893202186 0.9726355076 0.9798936844 - 0.9894558191 0.9729598761 0.9801084995 - 0.9895876050 0.9732838273 0.9803212285 - 0.9897193909 0.9736077189 0.9805338979 - 0.9898512959 0.9739316106 0.9807466865 - 0.9899830818 0.9742555022 0.9809594154 - 0.9901149273 0.9745793939 0.9811722040 - 0.9902467728 0.9749032855 0.9813848734 - 0.9903786182 0.9752253294 0.9815976024 - 0.9905071855 0.9755398035 0.9818084240 - 0.9906337261 0.9758542180 0.9820175767 - 0.9907602072 0.9761685729 0.9822267890 - 0.9908868074 0.9764831066 0.9824360013 - 0.9910132885 0.9767975211 0.9826452732 - 0.9911398292 0.9771118760 0.9828544855 - 0.9912663102 0.9774262905 0.9830636978 - 0.9913927913 0.9777371883 0.9832729101 - 0.9915155768 0.9780421257 0.9834790230 - 0.9916377068 0.9783470035 0.9836843014 - 0.9917597771 0.9786518812 0.9838895798 - 0.9918819070 0.9789568782 0.9840949774 - 0.9920039177 0.9792618155 0.9843003154 - 0.9921259880 0.9795666933 0.9845055938 - 0.9922481179 0.9798716903 0.9847108722 - 0.9923701286 0.9801716208 0.9849162102 - 0.9924876094 0.9804664850 0.9851183891 - 0.9926049709 0.9807614088 0.9853205085 - 0.9927225113 0.9810562730 0.9855225086 - 0.9928399920 0.9813511968 0.9857246280 - 0.9929574132 0.9816461205 0.9859266281 - 0.9930748940 0.9819409847 0.9861286879 - 0.9931923747 0.9822359085 0.9863306880 - 0.9933093190 0.9825251102 0.9865326285 - 0.9934214950 0.9828099012 0.9867315292 - 0.9935336709 0.9830945730 0.9869303703 - 0.9936459064 0.9833794236 0.9871293902 - 0.9937580824 0.9836640954 0.9873282909 - 0.9938703179 0.9839488864 0.9875271916 - 0.9939824939 0.9842336178 0.9877260923 - 0.9940947890 0.9845184088 0.9879251122 - 0.9942061901 0.9847974181 0.9881235957 - 0.9943130016 0.9850715995 0.9883189201 - 0.9944198132 0.9853457808 0.9885141253 - 0.9945265055 0.9856200814 0.9887093902 - 0.9946333170 0.9858943224 0.9889047146 - 0.9947401285 0.9861685038 0.9890999198 - 0.9948468804 0.9864426851 0.9892951846 - 0.9949536920 0.9867169261 0.9894905090 - 0.9950599074 0.9869858027 0.9896854758 - 0.9951617718 0.9872484803 0.9898774028 - 0.9952635765 0.9875112176 0.9900693297 - 0.9953655005 0.9877738953 0.9902613163 - 0.9954673052 0.9880366921 0.9904531837 - 0.9955692291 0.9882994294 0.9906451106 - 0.9956709743 0.9885621071 0.9908369780 - 0.9957728982 0.9888247848 0.9910290241 - 0.9958745837 0.9890835881 0.9912208915 - 0.9959716201 0.9893341064 0.9914097786 - 0.9960685968 0.9895846844 0.9915987253 - 0.9961655736 0.9898352027 0.9917874932 - 0.9962626100 0.9900857210 0.9919763803 - 0.9963595867 0.9903361797 0.9921653271 - 0.9964566231 0.9905868173 0.9923540950 - 0.9965535998 0.9908372760 0.9925429821 - 0.9966505766 0.9910866022 0.9927319288 - 0.9967427254 0.9913256764 0.9929165244 - 0.9968339205 0.9915646911 0.9931002259 - 0.9969251752 0.9918038249 0.9932839274 - 0.9970163703 0.9920428991 0.9934676290 - 0.9971076846 0.9922819138 0.9936512709 - 0.9971988797 0.9925209880 0.9938349724 - 0.9972901940 0.9927601218 0.9940186739 - 0.9973813891 0.9929991961 0.9942023754 - 0.9974687099 0.9932283759 0.9943823218 - 0.9975535870 0.9934555292 0.9945598245 - 0.9976384044 0.9936826825 0.9947373271 - 0.9977232814 0.9939097762 0.9949148297 - 0.9978080988 0.9941369891 0.9950922728 - 0.9978929758 0.9943640828 0.9952697754 - 0.9979777932 0.9945911765 0.9954472780 - 0.9980626702 0.9948183894 0.9956247807 - 0.9981455207 0.9950395823 0.9958003759 - 0.9982240796 0.9952540994 0.9959719181 - 0.9983025789 0.9954686761 0.9961432815 - 0.9983811975 0.9956831932 0.9963148236 - 0.9984596968 0.9958978295 0.9964861870 - 0.9985383153 0.9961122870 0.9966577291 - 0.9986168146 0.9963269234 0.9968290925 - 0.9986953735 0.9965413809 0.9970005751 - 0.9987738729 0.9967551827 0.9971719980 - 0.9988479018 0.9969543219 0.9973402023 - 0.9989216924 0.9971534014 0.9975082278 - 0.9989954233 0.9973524213 0.9976761937 - 0.9990692139 0.9975515008 0.9978441000 - 0.9991430044 0.9977505803 0.9980121255 - 0.9992166758 0.9979497194 0.9981800914 - 0.9992905259 0.9981487989 0.9983481169 - 0.9993643165 0.9983478785 0.9985160232 - 0.9994363785 0.9985377789 0.9986823797 - 0.9995067716 0.9987205863 0.9988471270 - 0.9995772839 0.9989033937 0.9990118146 - 0.9996476769 0.9990860820 0.9991765022 - 0.9997181892 0.9992688894 0.9993411899 - 0.9997885823 0.9994516969 0.9995058775 - 0.9998590946 0.9996345043 0.9996706247 - 0.9999294877 0.9998171926 0.9998353124 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0005381406 0.0002651573 0.0005448886 + 0.0011811421 0.0005488236 0.0011236490 + 0.0018776740 0.0008598516 0.0017561211 + 0.0026023439 0.0012074891 0.0024572650 + 0.0033602889 0.0015991570 0.0032185989 + 0.0041644410 0.0020365941 0.0040284242 + 0.0050217710 0.0025163409 0.0048902789 + 0.0059333672 0.0030300061 0.0058214129 + 0.0069048190 0.0035676421 0.0068372628 + 0.0079441704 0.0041191741 0.0079477550 + 0.0090618078 0.0046778312 0.0091583403 + 0.0102658896 0.0052411938 0.0104663204 + 0.0115583995 0.0058102822 0.0118661895 + 0.0129291601 0.0063874712 0.0133533198 + 0.0143661099 0.0069762110 0.0149256596 + 0.0158517305 0.0075797332 0.0165787991 + 0.0173796508 0.0082016559 0.0183057394 + 0.0189530998 0.0088431826 0.0200976301 + 0.0205809996 0.0095067294 0.0219422299 + 0.0222787503 0.0101926001 0.0238327999 + 0.0240615793 0.0109011699 0.0257611796 + 0.0259451196 0.0116325803 0.0277215205 + 0.0279377103 0.0123844100 0.0297172107 + 0.0300485604 0.0131573398 0.0317482613 + 0.0322753713 0.0139512103 0.0338172801 + 0.0346145704 0.0147648398 0.0359307416 + 0.0370622911 0.0155985896 0.0380884185 + 0.0396122485 0.0164521597 0.0402955301 + 0.0422580615 0.0173262991 0.0425595492 + 0.0449894518 0.0182192009 0.0448740013 + 0.0477931984 0.0191300903 0.0472489484 + 0.0506595708 0.0200600196 0.0496853888 + 0.0535767302 0.0210082103 0.0521829911 + 0.0565305017 0.0219749697 0.0547407605 + 0.0595083982 0.0229592193 0.0573574007 + 0.0624992885 0.0239622109 0.0600296818 + 0.0654954314 0.0249828305 0.0627512708 + 0.0684895366 0.0260199904 0.0655123070 + 0.0714772493 0.0270722192 0.0683010966 + 0.0744543299 0.0281406008 0.0711084977 + 0.0774153098 0.0292240307 0.0739231929 + 0.0803605914 0.0303211194 0.0767356381 + 0.0832905099 0.0314316712 0.0795454979 + 0.0862074494 0.0325558111 0.0823479816 + 0.0891113579 0.0336936004 0.0851310268 + 0.0919999331 0.0348446108 0.0878951326 + 0.0948731005 0.0360087790 0.0906417519 + 0.0977329016 0.0371854194 0.0933719203 + 0.1005818024 0.0383735187 0.0960813016 + 0.1034189016 0.0395741388 0.0987711623 + 0.1062425002 0.0407866500 0.1014439017 + 0.1090549007 0.0420108810 0.1041020006 + 0.1118552014 0.0432458110 0.1067482978 + 0.1146425009 0.0444907583 0.1093854010 + 0.1174186990 0.0457457118 0.1120164022 + 0.1201832965 0.0470101312 0.1146423966 + 0.1229387969 0.0482850000 0.1172647029 + 0.1256874949 0.0495695882 0.1198822036 + 0.1284302026 0.0508622415 0.1224958971 + 0.1311666965 0.0521623604 0.1251043975 + 0.1338986009 0.0534698404 0.1277081966 + 0.1366281956 0.0547848493 0.1303080022 + 0.1393581927 0.0561086796 0.1329074055 + 0.1420904994 0.0574397892 0.1355095953 + 0.1448286027 0.0587780997 0.1381137967 + 0.1475744992 0.0601234213 0.1407177001 + 0.1503286064 0.0614760891 0.1433234960 + 0.1530916989 0.0628349632 0.1459300071 + 0.1558624953 0.0642008483 0.1485393941 + 0.1586423963 0.0655714497 0.1511545032 + 0.1614302993 0.0669477507 0.1537750065 + 0.1642321050 0.0683294088 0.1563979983 + 0.1670460999 0.0697180778 0.1590255052 + 0.1698731035 0.0711119920 0.1616581976 + 0.1727122962 0.0725102201 0.1642964929 + 0.1755629927 0.0739143416 0.1669408977 + 0.1784220040 0.0753230676 0.1695916951 + 0.1812902987 0.0767354220 0.1722505987 + 0.1841648072 0.0781524777 0.1749189049 + 0.1870469004 0.0795728117 0.1775977015 + 0.1899344027 0.0809954405 0.1802870035 + 0.1928229034 0.0824212879 0.1829870939 + 0.1957139969 0.0838513300 0.1856992990 + 0.1986058056 0.0852848813 0.1884211004 + 0.2014966011 0.0867218077 0.1911548972 + 0.2043849975 0.0881616324 0.1939021051 + 0.2072687000 0.0896041170 0.1966606975 + 0.2101463974 0.0910492018 0.1994311064 + 0.2130181938 0.0924970210 0.2022143006 + 0.2158842981 0.0939473733 0.2050091028 + 0.2187419981 0.0953999087 0.2078166008 + 0.2215912044 0.0968550667 0.2106339931 + 0.2244296968 0.0983112976 0.2134619057 + 0.2272589952 0.0997681171 0.2162988037 + 0.2300810069 0.1012267023 0.2191399932 + 0.2328950018 0.1026865989 0.2219882011 + 0.2356988937 0.1041480973 0.2248430997 + 0.2384911031 0.1056120992 0.2277006060 + 0.2412708998 0.1070773974 0.2305589020 + 0.2440378964 0.1085444018 0.2334149033 + 0.2467941940 0.1100130007 0.2362655997 + 0.2495391071 0.1114827022 0.2391107976 + 0.2522701919 0.1129537970 0.2419497073 + 0.2549884021 0.1144248024 0.2447791994 + 0.2576946914 0.1158969998 0.2475973070 + 0.2603886127 0.1173700988 0.2504028082 + 0.2630693913 0.1188448966 0.2531929910 + 0.2657378018 0.1203223020 0.2559669018 + 0.2683930099 0.1218004972 0.2587235868 + 0.2710328996 0.1232801974 0.2614609897 + 0.2736578882 0.1247610003 0.2641791999 + 0.2762688994 0.1262425929 0.2668775022 + 0.2788653970 0.1277254969 0.2695545852 + 0.2814469934 0.1292095035 0.2722114921 + 0.2840158045 0.1306941062 0.2748483121 + 0.2865726054 0.1321794987 0.2774635851 + 0.2891159058 0.1336652040 0.2800571024 + 0.2916449904 0.1351507008 0.2826282978 + 0.2941609025 0.1366363019 0.2851763070 + 0.2966657877 0.1381220967 0.2877005935 + 0.2991599143 0.1396085024 0.2902010083 + 0.3016422093 0.1410955936 0.2926791906 + 0.3041135073 0.1425838023 0.2951335907 + 0.3065750003 0.1440730989 0.2975631058 + 0.3090234101 0.1455630064 0.2999700010 + 0.3114587069 0.1470534056 0.3023560941 + 0.3138816059 0.1485445052 0.3047226965 + 0.3162932098 0.1500363946 0.3070712090 + 0.3186948001 0.1515289992 0.3094022870 + 0.3210870028 0.1530221999 0.3117175996 + 0.3234694898 0.1545169055 0.3140189052 + 0.3258418143 0.1560124010 0.3163051009 + 0.3282003999 0.1575089991 0.3185760975 + 0.3305430114 0.1590068042 0.3208332956 + 0.3328739107 0.1605062932 0.3230775893 + 0.3351961970 0.1620068997 0.3253093064 + 0.3375113010 0.1635068059 0.3275283873 + 0.3398194909 0.1650066972 0.3297384977 + 0.3421204984 0.1665070057 0.3319405913 + 0.3444133997 0.1680078954 0.3341340125 + 0.3466998041 0.1695086062 0.3363187015 + 0.3489798009 0.1710103005 0.3384949863 + 0.3512539864 0.1725132018 0.3406594098 + 0.3535234928 0.1740172058 0.3428128064 + 0.3557898104 0.1755214930 0.3449561894 + 0.3580540121 0.1770250052 0.3470906913 + 0.3603161871 0.1785289049 0.3492175043 + 0.3625768125 0.1800335944 0.3513374031 + 0.3648355901 0.1815388054 0.3534527123 + 0.3670929074 0.1830437034 0.3555639982 + 0.3693490922 0.1845479012 0.3576712012 + 0.3716045022 0.1860512048 0.3597705066 + 0.3738591969 0.1875541955 0.3618609011 + 0.3761132061 0.1890579015 0.3639445007 + 0.3783670068 0.1905619055 0.3660219014 + 0.3806206882 0.1920655966 0.3680920899 + 0.3828743994 0.1935691983 0.3701565862 + 0.3851284981 0.1950726956 0.3722172081 + 0.3873830140 0.1965761930 0.3742741942 + 0.3896380961 0.1980797052 0.3763268888 + 0.3918941915 0.1995833069 0.3783758879 + 0.3941515088 0.2010867000 0.3804208040 + 0.3964098990 0.2025887072 0.3824599087 + 0.3986696899 0.2040902972 0.3844920993 + 0.4009316862 0.2055917978 0.3865205944 + 0.4031954110 0.2070932984 0.3885455132 + 0.4054606855 0.2085942030 0.3905645013 + 0.4077295065 0.2100944966 0.3925797045 + 0.4100005031 0.2115938962 0.3945915103 + 0.4122735858 0.2130924016 0.3965987861 + 0.4145509005 0.2145905048 0.3986020982 + 0.4168311954 0.2160879970 0.4006024003 + 0.4191139042 0.2175852060 0.4025978148 + 0.4213995934 0.2190822959 0.4045889080 + 0.4236867130 0.2205785960 0.4065766037 + 0.4259749949 0.2220751941 0.4085580111 + 0.4282661080 0.2235720009 0.4105337858 + 0.4305579960 0.2250701934 0.4125058055 + 0.4328506887 0.2265686989 0.4144718051 + 0.4351426959 0.2280676961 0.4164347947 + 0.4374347031 0.2295663953 0.4183954895 + 0.4397264123 0.2310650051 0.4203527868 + 0.4420182109 0.2325633019 0.4223093092 + 0.4443092942 0.2340615988 0.4242646992 + 0.4465988874 0.2355599999 0.4262180924 + 0.4488858879 0.2370588928 0.4281705916 + 0.4511714876 0.2385578007 0.4301216900 + 0.4534544051 0.2400566041 0.4320701063 + 0.4557343125 0.2415551990 0.4340169132 + 0.4580109119 0.2430536002 0.4359580874 + 0.4602797031 0.2445522994 0.4378975034 + 0.4625451863 0.2460512072 0.4398345947 + 0.4648033977 0.2475509942 0.4417698979 + 0.4670571089 0.2490514964 0.4437046945 + 0.4693068862 0.2505528927 0.4456391931 + 0.4715498090 0.2520549893 0.4475742877 + 0.4737902880 0.2535574138 0.4495103061 + 0.4760245085 0.2550598979 0.4514476955 + 0.4782535136 0.2565625906 0.4533854127 + 0.4804762006 0.2580656111 0.4553222060 + 0.4826883078 0.2595695853 0.4572581053 + 0.4848966897 0.2610749006 0.4591920078 + 0.4870918095 0.2625809908 0.4611235857 + 0.4892837107 0.2640874088 0.4630536139 + 0.4914664030 0.2655943036 0.4649797976 + 0.4936434031 0.2671012878 0.4669052064 + 0.4958145916 0.2686108947 0.4688282013 + 0.4979768991 0.2701208889 0.4707511067 + 0.5001357198 0.2716321051 0.4726733863 + 0.5022798777 0.2731435895 0.4745965004 + 0.5044208765 0.2746556997 0.4765200913 + 0.5065475702 0.2761682868 0.4784458876 + 0.5086696744 0.2776814103 0.4803726077 + 0.5107814074 0.2791965902 0.4822996855 + 0.5128856897 0.2807131112 0.4842267036 + 0.5149818063 0.2822310030 0.4861502945 + 0.5170682073 0.2837496102 0.4880726039 + 0.5191485882 0.2852681875 0.4899899960 + 0.5212162137 0.2867869139 0.4919058979 + 0.5232796073 0.2883065045 0.4938189983 + 0.5253279209 0.2898262143 0.4957309067 + 0.5273739100 0.2913483083 0.4976414144 + 0.5294030905 0.2928704917 0.4995503128 + 0.5314307809 0.2943947911 0.5014581084 + 0.5334423184 0.2959192097 0.5033627748 + 0.5354515910 0.2974444032 0.5052657723 + 0.5374450088 0.2989695966 0.5071659088 + 0.5394350290 0.3004952967 0.5090647936 + 0.5414085984 0.3020209074 0.5109593868 + 0.5433781743 0.3035480082 0.5128529072 + 0.5453327894 0.3050751090 0.5147392750 + 0.5472834110 0.3066017032 0.5166242719 + 0.5492209792 0.3081282973 0.5185018182 + 0.5511540174 0.3096539080 0.5203778148 + 0.5530723929 0.3111793995 0.5222443938 + 0.5549861789 0.3127050102 0.5241091847 + 0.5568844080 0.3142305911 0.5259639025 + 0.5587790012 0.3157570958 0.5278167129 + 0.5606595278 0.3172838092 0.5296614766 + 0.5625373721 0.3188110888 0.5315043926 + 0.5644009113 0.3203384876 0.5333400965 + 0.5662627816 0.3218651116 0.5351735950 + 0.5681079030 0.3233912885 0.5369982123 + 0.5699527264 0.3249165118 0.5388190746 + 0.5717802048 0.3264403939 0.5406309962 + 0.5736076832 0.3279637098 0.5424374938 + 0.5754187703 0.3294858932 0.5442360044 + 0.5772284269 0.3310078979 0.5460273027 + 0.5790212750 0.3325291872 0.5478109121 + 0.5808100104 0.3340505064 0.5495834947 + 0.5825855732 0.3355714083 0.5513505936 + 0.5843548179 0.3370923996 0.5531036854 + 0.5861145854 0.3386130035 0.5548539758 + 0.5878657103 0.3401334882 0.5565863252 + 0.5896102190 0.3416540027 0.5583184958 + 0.5913426280 0.3431744874 0.5600315928 + 0.5930718184 0.3446949124 0.5617440939 + 0.5947852731 0.3462150991 0.5634422898 + 0.5964987874 0.3477351069 0.5651364923 + 0.5981938243 0.3492532969 0.5668197274 + 0.5998880863 0.3507714868 0.5684949160 + 0.6015682220 0.3522872925 0.5701624751 + 0.6032432914 0.3538025916 0.5718169212 + 0.6049085855 0.3553164005 0.5734683275 + 0.6065642238 0.3568288088 0.5750986934 + 0.6082149148 0.3583407104 0.5767289996 + 0.6098505855 0.3598510027 0.5783388019 + 0.6114861965 0.3613612056 0.5799455047 + 0.6131017208 0.3628711998 0.5815384984 + 0.6147162914 0.3643811047 0.5831218958 + 0.6163176894 0.3658905029 0.5846983194 + 0.6179122925 0.3673991859 0.5862581134 + 0.6195003986 0.3689076900 0.5878177881 + 0.6210765243 0.3704144955 0.5893548727 + 0.6226522923 0.3719212115 0.5908914208 + 0.6242104173 0.3734270036 0.5924147964 + 0.6257684827 0.3749324977 0.5939309001 + 0.6273151040 0.3764374852 0.5954414010 + 0.6288558245 0.3779416084 0.5969383121 + 0.6303917170 0.3794457912 0.5984352231 + 0.6319159865 0.3809471130 0.5999152064 + 0.6334401965 0.3824482858 0.6013936996 + 0.6349474788 0.3839477897 0.6028618217 + 0.6364527941 0.3854453862 0.6043204069 + 0.6379473805 0.3869430125 0.6057764888 + 0.6394302845 0.3884387910 0.6072123051 + 0.6409124732 0.3899345100 0.6086481214 + 0.6423766017 0.3914293945 0.6100676060 + 0.6438407898 0.3929235041 0.6114792824 + 0.6452940702 0.3944176137 0.6128858924 + 0.6467397809 0.3959099948 0.6142745018 + 0.6481837034 0.3974024057 0.6156631708 + 0.6496129036 0.3988932073 0.6170371175 + 0.6510421038 0.4003823996 0.6184059978 + 0.6524606943 0.4018715918 0.6197704077 + 0.6538727880 0.4033572972 0.6211217046 + 0.6552829146 0.4048427939 0.6224728823 + 0.6566761136 0.4063270986 0.6238120794 + 0.6580693126 0.4078091085 0.6251469254 + 0.6594510078 0.4092912078 0.6264786720 + 0.6608237028 0.4107702971 0.6277983785 + 0.6621962786 0.4122484028 0.6291180253 + 0.6635506153 0.4137260914 0.6304268837 + 0.6649050117 0.4152004123 0.6317300200 + 0.6662514806 0.4166747928 0.6330316067 + 0.6675873995 0.4181470871 0.6343147755 + 0.6689231992 0.4196172953 0.6355978847 + 0.6702443957 0.4210875034 0.6368724108 + 0.6715623736 0.4225546122 0.6381372809 + 0.6728767753 0.4240212142 0.6394020915 + 0.6741765738 0.4254870117 0.6406546235 + 0.6754763126 0.4269487858 0.6419048905 + 0.6767659187 0.4284105897 0.6431524754 + 0.6780474782 0.4298701882 0.6443892717 + 0.6793289781 0.4313269854 0.6456261277 + 0.6805943847 0.4327838123 0.6468564272 + 0.6818578839 0.4342373013 0.6480810046 + 0.6831179857 0.4356895089 0.6493057013 + 0.6843643785 0.4371415973 0.6505178809 + 0.6856108904 0.4385890961 0.6517279744 + 0.6868489981 0.4400365055 0.6529359818 + 0.6880782843 0.4414829910 0.6541314721 + 0.6893076897 0.4429262877 0.6553270221 + 0.6905233264 0.4443696141 0.6565173864 + 0.6917344928 0.4458107948 0.6577004194 + 0.6929457188 0.4472492039 0.6588833928 + 0.6941388845 0.4486874938 0.6600583196 + 0.6953321099 0.4501225054 0.6612290740 + 0.6965216994 0.4515551925 0.6623998284 + 0.6976982951 0.4529877901 0.6635617018 + 0.6988748908 0.4544155896 0.6647223234 + 0.7000442743 0.4558416903 0.6658821106 + 0.7012037039 0.4572679102 0.6670321822 + 0.7023630142 0.4586881995 0.6681821942 + 0.7035111785 0.4601078033 0.6693289876 + 0.7046517730 0.4615274072 0.6704663038 + 0.7057924271 0.4629419148 0.6716036797 + 0.7069209814 0.4643563032 0.6727368832 + 0.7080454230 0.4657703936 0.6738638878 + 0.7091699243 0.4671806991 0.6749910116 + 0.7102797031 0.4685910046 0.6761143208 + 0.7113872766 0.4700005949 0.6772344112 + 0.7124949098 0.4714061916 0.6783545017 + 0.7135862708 0.4728117883 0.6794689894 + 0.7146772146 0.4742164910 0.6805803180 + 0.7157673240 0.4756168127 0.6816917062 + 0.7168418169 0.4770171046 0.6827973723 + 0.7179161906 0.4784162939 0.6839008927 + 0.7189890146 0.4798108041 0.6850042939 + 0.7200477719 0.4812051952 0.6861020923 + 0.7211065292 0.4825986028 0.6871982217 + 0.7221630812 0.4839865863 0.6882944107 + 0.7232058048 0.4853746891 0.6893844008 + 0.7242485285 0.4867616892 0.6904730201 + 0.7252889872 0.4881420135 0.6915615797 + 0.7263165712 0.4895223975 0.6926428080 + 0.7273442149 0.4909023046 0.6937221885 + 0.7283703089 0.4922760129 0.6948016882 + 0.7293847203 0.4936496913 0.6958737969 + 0.7303991914 0.4950233996 0.6969439983 + 0.7314124703 0.4963918030 0.6980140805 + 0.7324116230 0.4977600873 0.6990771294 + 0.7334107161 0.4991284013 0.7001376152 + 0.7344096899 0.5004916787 0.7011982203 + 0.7353922725 0.5018543005 0.7022535801 + 0.7363749146 0.5032168031 0.7033064961 + 0.7373574972 0.5045748949 0.7043594122 + 0.7383249998 0.5059313178 0.7054070830 + 0.7392910719 0.5072876811 0.7064511180 + 0.7402572036 0.5086405277 0.7074950933 + 0.7412112951 0.5099905133 0.7085348964 + 0.7421622276 0.5113404989 0.7095698714 + 0.7431129813 0.5126873255 0.7106049061 + 0.7440556288 0.5140289068 0.7116366029 + 0.7449935079 0.5153704286 0.7126612067 + 0.7459315062 0.5167104006 0.7136859298 + 0.7468624711 0.5180423856 0.7147092223 + 0.7477856874 0.5193743110 0.7157247066 + 0.7487087846 0.5207061768 0.7167401910 + 0.7496278286 0.5220301747 0.7177556157 + 0.7505353093 0.5233532786 0.7187631726 + 0.7514427900 0.5246765018 0.7197703123 + 0.7523496747 0.5259947777 0.7207775116 + 0.7532436848 0.5273103714 0.7217786908 + 0.7541376948 0.5286259055 0.7227776051 + 0.7550317049 0.5299391747 0.7237765789 + 0.7559167743 0.5312479138 0.7247710824 + 0.7567995787 0.5325565934 0.7257605791 + 0.7576823831 0.5338652730 0.7267500758 + 0.7585588098 0.5351672173 0.7277382016 + 0.7594292164 0.5364691019 0.7287198901 + 0.7602995038 0.5377709866 0.7297015786 + 0.7611671090 0.5390685201 0.7306833863 + 0.7620239258 0.5403637290 0.7316588759 + 0.7628808022 0.5416588783 0.7326335907 + 0.7637376189 0.5429524183 0.7336083055 + 0.7645832896 0.5442410111 0.7345783710 + 0.7654271722 0.5455294847 0.7355446815 + 0.7662711740 0.5468180776 0.7365108728 + 0.7671089768 0.5481011868 0.7374755144 + 0.7679408789 0.5493832231 0.7384330034 + 0.7687727213 0.5506651998 0.7393903732 + 0.7696030140 0.5519440174 0.7403479218 + 0.7704212070 0.5532178879 0.7412980795 + 0.7712393999 0.5544918776 0.7422463894 + 0.7720575929 0.5557658076 0.7431948185 + 0.7728673816 0.5570312142 0.7441394925 + 0.7736734152 0.5582960248 0.7450783849 + 0.7744793892 0.5595608950 0.7460172772 + 0.7752828002 0.5608223081 0.7469562292 + 0.7760779858 0.5620794296 0.7478839755 + 0.7768731713 0.5633366108 0.7488111258 + 0.7776684761 0.5645936728 0.7497382164 + 0.7784557939 0.5658441782 0.7506607175 + 0.7792407274 0.5670942068 0.7515779138 + 0.7800256014 0.5683441162 0.7524952292 + 0.7808077931 0.5695917010 0.7534124255 + 0.7815821767 0.5708349943 0.7543212175 + 0.7823565006 0.5720782280 0.7552297115 + 0.7831308246 0.5733215213 0.7561380863 + 0.7838966846 0.5745599866 0.7570431232 + 0.7846595049 0.5757972002 0.7579433918 + 0.7854222059 0.5770342946 0.7588437200 + 0.7861832976 0.5782706141 0.7597439885 + 0.7869359851 0.5795021057 0.7606366277 + 0.7876886725 0.5807337165 0.7615280747 + 0.7884414196 0.5819652081 0.7624195814 + 0.7891888022 0.5831941962 0.7633082867 + 0.7899320722 0.5844206810 0.7641898990 + 0.7906754017 0.5856472254 0.7650715113 + 0.7914186716 0.5868737102 0.7659531236 + 0.7921544909 0.5880939960 0.7668281794 + 0.7928900123 0.5893129706 0.7676998973 + 0.7936254144 0.5905321240 0.7685714960 + 0.7943581939 0.5917500257 0.7694430947 + 0.7950845957 0.5929607153 0.7703047991 + 0.7958109975 0.5941714048 0.7711665034 + 0.7965373993 0.5953819752 0.7720282078 + 0.7972583771 0.5965896845 0.7728871703 + 0.7979758978 0.5977926850 0.7737402916 + 0.7986934781 0.5989956856 0.7745934725 + 0.7994109988 0.6001986861 0.7754465938 + 0.8001213074 0.6013975143 0.7762938142 + 0.8008307815 0.6025936007 0.7771372199 + 0.8015401959 0.6037898064 0.7779806256 + 0.8022487164 0.6049858928 0.7788239121 + 0.8029516935 0.6061770916 0.7796577811 + 0.8036546707 0.6073670983 0.7804902196 + 0.8043577075 0.6085572243 0.7813224792 + 0.8050580025 0.6097472906 0.7821539044 + 0.8057534099 0.6109319925 0.7829759717 + 0.8064488769 0.6121168137 0.7837980986 + 0.8071444035 0.6133015156 0.7846202254 + 0.8078358769 0.6144853234 0.7854393721 + 0.8085243106 0.6156625152 0.7862510085 + 0.8092126846 0.6168397069 0.7870625854 + 0.8099011183 0.6180168986 0.7878742218 + 0.8105850816 0.6191921234 0.7886819839 + 0.8112673163 0.6203610897 0.7894849777 + 0.8119494915 0.6215299964 0.7902880907 + 0.8126317859 0.6226990223 0.7910910845 + 0.8133102059 0.6238657236 0.7918882966 + 0.8139879704 0.6250272989 0.7926809788 + 0.8146657944 0.6261889935 0.7934737802 + 0.8153436184 0.6273506284 0.7942665815 + 0.8160151243 0.6285101175 0.7950521708 + 0.8166863918 0.6296654940 0.7958343029 + 0.8173575997 0.6308208108 0.7966163754 + 0.8180288076 0.6319761872 0.7973985076 + 0.8186935186 0.6331295967 0.7981728911 + 0.8193582296 0.6342797279 0.7989445925 + 0.8200228810 0.6354296803 0.7997162938 + 0.8206874728 0.6365796924 0.8004879951 + 0.8213469982 0.6377279162 0.8012521267 + 0.8220065236 0.6388722062 0.8020138741 + 0.8226659894 0.6400164962 0.8027758002 + 0.8233252764 0.6411607862 0.8035376072 + 0.8239793777 0.6423032880 0.8042908907 + 0.8246334791 0.6434404850 0.8050416708 + 0.8252875805 0.6445778012 0.8057923913 + 0.8259416819 0.6457151175 0.8065431714 + 0.8265889883 0.6468511820 0.8072862029 + 0.8272364140 0.6479817033 0.8080263734 + 0.8278837204 0.6491122246 0.8087667227 + 0.8285310864 0.6502428055 0.8095068932 + 0.8291730285 0.6513730288 0.8102409840 + 0.8298146129 0.6524980068 0.8109720945 + 0.8304561973 0.6536229849 0.8117030859 + 0.8310977817 0.6547480226 0.8124341965 + 0.8317347765 0.6558730006 0.8131592870 + 0.8323708773 0.6569942236 0.8138797283 + 0.8330070972 0.6581149101 0.8146002293 + 0.8336431980 0.6592354774 0.8153206110 + 0.8342744112 0.6603562236 0.8160365224 + 0.8349034786 0.6614738107 0.8167462945 + 0.8355324864 0.6625900865 0.8174561262 + 0.8361616135 0.6637063026 0.8181658983 + 0.8367878199 0.6648225188 0.8188729882 + 0.8374114037 0.6659368873 0.8195725083 + 0.8380349278 0.6670492291 0.8202719092 + 0.8386585116 0.6681613922 0.8209714293 + 0.8392807245 0.6692736149 0.8216704130 + 0.8399001956 0.6703848839 0.8223608732 + 0.8405197263 0.6714920998 0.8230513930 + 0.8411391973 0.6725993156 0.8237419128 + 0.8417584896 0.6737064719 0.8244323730 + 0.8423752785 0.6748136878 0.8251157999 + 0.8429921865 0.6759157181 0.8257976770 + 0.8436089754 0.6770169139 0.8264794946 + 0.8442258835 0.6781182289 0.8271613717 + 0.8448395133 0.6792194843 0.8278384805 + 0.8454523087 0.6803181767 0.8285114765 + 0.8460649848 0.6814149022 0.8291844726 + 0.8466777802 0.6825116277 0.8298574090 + 0.8472878933 0.6836082935 0.8305279016 + 0.8478950858 0.6847041845 0.8311896920 + 0.8485022783 0.6857963204 0.8318514228 + 0.8491094708 0.6868883967 0.8325132132 + 0.8497161865 0.6879804730 0.8331748843 + 0.8503192067 0.6890726089 0.8338286281 + 0.8509222865 0.6901617050 0.8344811201 + 0.8515254259 0.6912497878 0.8351336718 + 0.8521285057 0.6923378110 0.8357862830 + 0.8527284265 0.6934258938 0.8364341855 + 0.8533272147 0.6945127249 0.8370773792 + 0.8539260030 0.6955966949 0.8377205729 + 0.8545249104 0.6966806054 0.8383638859 + 0.8551219106 0.6977645755 0.8390063047 + 0.8557152152 0.6988484859 0.8396390080 + 0.8563085794 0.6999288797 0.8402717710 + 0.8569018841 0.7010084987 0.8409044743 + 0.8574953079 0.7020881176 0.8415372968 + 0.8580844998 0.7031677961 0.8421642184 + 0.8586729765 0.7042456865 0.8427875042 + 0.8592615724 0.7053195238 0.8434109092 + 0.8598501086 0.7063934207 0.8440341949 + 0.8604366779 0.7074673176 0.8446565270 + 0.8610199094 0.7085412145 0.8452715874 + 0.8616030812 0.7096114159 0.8458867073 + 0.8621863723 0.7106803060 0.8465017080 + 0.8627696037 0.7117491961 0.8471168280 + 0.8633472919 0.7128180265 0.8477255702 + 0.8639240861 0.7138860226 0.8483303189 + 0.8645008206 0.7149490118 0.8489350080 + 0.8650774956 0.7160121202 0.8495395780 + 0.8656529784 0.7170751095 0.8501440287 + 0.8662251234 0.7181380987 0.8507388234 + 0.8667972088 0.7191970944 0.8513334990 + 0.8673692942 0.7202522755 0.8519282937 + 0.8679413795 0.7213075161 0.8525230885 + 0.8685104251 0.7223626971 0.8531140089 + 0.8690779209 0.7234178782 0.8536998034 + 0.8696454167 0.7244672775 0.8542855978 + 0.8702129126 0.7255154848 0.8548712730 + 0.8707802892 0.7265638113 0.8554571271 + 0.8713405132 0.7276120186 0.8560361862 + 0.8719006181 0.7286595702 0.8566132784 + 0.8724607825 0.7297024131 0.8571904898 + 0.8730208874 0.7307453156 0.8577675819 + 0.8735787272 0.7317880988 0.8583437204 + 0.8741317987 0.7328310013 0.8589094877 + 0.8746848106 0.7338718176 0.8594753146 + 0.8752378821 0.7349092960 0.8600410819 + 0.8757908940 0.7359467745 0.8606069088 + 0.8763406873 0.7369843125 0.8611692190 + 0.8768883944 0.7380216718 0.8617258072 + 0.8774359822 0.7390558720 0.8622822762 + 0.8779836893 0.7400876284 0.8628388047 + 0.8785312772 0.7411193848 0.8633952737 + 0.8790736198 0.7421510816 0.8639466763 + 0.8796147704 0.7431827784 0.8644946814 + 0.8801559806 0.7442104220 0.8650428057 + 0.8806971908 0.7452365756 0.8655908108 + 0.8812381029 0.7462627888 0.8661388159 + 0.8817703724 0.7472890019 0.8666791916 + 0.8823025823 0.7483152151 0.8672177196 + 0.8828349113 0.7493349910 0.8677561879 + 0.8833671212 0.7503538132 0.8682947159 + 0.8838980198 0.7513726950 0.8688331842 + 0.8844233751 0.7523915172 0.8693615198 + 0.8849487901 0.7534103990 0.8698894978 + 0.8854740858 0.7544203997 0.8704175949 + 0.8859995008 0.7554299831 0.8709455729 + 0.8865231276 0.7564396262 0.8714727163 + 0.8870428205 0.7574492097 0.8719913960 + 0.8875625134 0.7584586740 0.8725100160 + 0.8880822062 0.7594599128 0.8730286956 + 0.8886018991 0.7604610920 0.8735473752 + 0.8891186714 0.7614622712 0.8740645051 + 0.8896307945 0.7624635100 0.8745744824 + 0.8901429176 0.7634646297 0.8750844002 + 0.8906549811 0.7644580007 0.8755943179 + 0.8911671042 0.7654513717 0.8761042953 + 0.8916761875 0.7664448023 0.8766121268 + 0.8921812177 0.7674381733 0.8771117926 + 0.8926861882 0.7684317231 0.8776115179 + 0.8931912184 0.7694169879 0.8781113029 + 0.8936961889 0.7704021931 0.8786110282 + 0.8941987157 0.7713873982 0.8791092038 + 0.8946977854 0.7723724842 0.8796014190 + 0.8951969147 0.7733576894 0.8800935745 + 0.8956959844 0.7743353844 0.8805857897 + 0.8961951137 0.7753123045 0.8810780048 + 0.8966916800 0.7762892246 0.8815690279 + 0.8971840739 0.7772660851 0.8820533156 + 0.8976764083 0.7782430053 0.8825377226 + 0.8981688023 0.7792145014 0.8830221295 + 0.8986610770 0.7801843882 0.8835064173 + 0.8991513848 0.7811542749 0.8839902282 + 0.8996360898 0.7821242809 0.8844659925 + 0.9001209140 0.7830942273 0.8849416971 + 0.9006056190 0.7840591073 0.8854175210 + 0.9010903835 0.7850208879 0.8858932257 + 0.9015741944 0.7859826088 0.8863689899 + 0.9020525813 0.7869443893 0.8868358731 + 0.9025310278 0.7879061103 0.8873022199 + 0.9030094147 0.7888634801 0.8877685070 + 0.9034878016 0.7898142934 0.8882347941 + 0.9039661884 0.7907652259 0.8887010813 + 0.9044383764 0.7917159796 0.8891620040 + 0.9049103856 0.7926669121 0.8896213174 + 0.9053822756 0.7936159968 0.8900805712 + 0.9058542848 0.7945556045 0.8905398846 + 0.9063262939 0.7954952717 0.8909991980 + 0.9067928195 0.7964348793 0.8914546967 + 0.9072574973 0.7973746061 0.8919070959 + 0.9077221751 0.7983142138 0.8923594952 + 0.9081869125 0.7992444038 0.8928118944 + 0.9086515903 0.8001731038 0.8932642937 + 0.9091123939 0.8011018038 0.8937140107 + 0.9095693231 0.8020305037 0.8941572905 + 0.9100260735 0.8029590845 0.8946006894 + 0.9104830027 0.8038828969 0.8950440288 + 0.9109398127 0.8048021793 0.8954873085 + 0.9113954902 0.8057215214 0.8959305882 + 0.9118455052 0.8066408038 0.8963645101 + 0.9122955799 0.8075600863 0.8967983127 + 0.9127457142 0.8084779978 0.8972321153 + 0.9131957889 0.8093873858 0.8976659179 + 0.9136459231 0.8102967739 0.8980997205 + 0.9140903950 0.8112062812 0.8985280991 + 0.9145337939 0.8121157289 0.8989533782 + 0.9149773121 0.8130251169 0.8993787169 + 0.9154207110 0.8139269948 0.8998039961 + 0.9158641100 0.8148258924 0.9002293944 + 0.9163044095 0.8157246709 0.9006530046 + 0.9167405963 0.8166236281 0.9010717869 + 0.9171767831 0.8175224066 0.9014906287 + 0.9176130295 0.8184180260 0.9019094110 + 0.9180492163 0.8193030953 0.9023281932 + 0.9184855223 0.8201882243 0.9027469754 + 0.9189139009 0.8210734129 0.9031597972 + 0.9193422794 0.8219584823 0.9035711884 + 0.9197707772 0.8228436112 0.9039825797 + 0.9201992154 0.8237193823 0.9043939710 + 0.9206275940 0.8245916963 0.9048054814 + 0.9210522175 0.8254640102 0.9052137733 + 0.9214730859 0.8263362050 0.9056159854 + 0.9218940139 0.8272085190 0.9060181975 + 0.9223148823 0.8280786276 0.9064204097 + 0.9227359295 0.8289378285 0.9068226218 + 0.9231567979 0.8297969103 0.9072247744 + 0.9235706925 0.8306559920 0.9076194763 + 0.9239845872 0.8315150738 0.9080122113 + 0.9243984222 0.8323742747 0.9084050059 + 0.9248123169 0.8332263231 0.9087976813 + 0.9252260923 0.8340724111 0.9091904759 + 0.9256373048 0.8349186182 0.9095817208 + 0.9260444045 0.8357648253 0.9099674821 + 0.9264515042 0.8366109729 0.9103533030 + 0.9268587232 0.8374571204 0.9107391834 + 0.9272658229 0.8382914066 0.9111250043 + 0.9276729822 0.8391243219 0.9115108252 + 0.9280738831 0.8399571776 0.9118928909 + 0.9284729958 0.8407900929 0.9122726917 + 0.9288722277 0.8416228890 0.9126523733 + 0.9292714000 0.8424531221 0.9130321145 + 0.9296705723 0.8432716131 0.9134117961 + 0.9300686717 0.8440901041 0.9137915969 + 0.9304581881 0.8449085951 0.9141638279 + 0.9308475852 0.8457270861 0.9145355821 + 0.9312371016 0.8465456963 0.9149073958 + 0.9316264987 0.8473572731 0.9152792096 + 0.9320160151 0.8481602073 0.9156510234 + 0.9324020743 0.8489630818 0.9160208702 + 0.9327831268 0.8497660160 0.9163845181 + 0.9331640005 0.8505688906 0.9167479873 + 0.9335449934 0.8513718247 0.9171115160 + 0.9339259863 0.8521640897 0.9174751043 + 0.9343069196 0.8529503942 0.9178385735 + 0.9346833229 0.8537368178 0.9181991816 + 0.9350569844 0.8545231819 0.9185562730 + 0.9354307055 0.8553094864 0.9189134836 + 0.9358044267 0.8560959101 0.9192706943 + 0.9361780882 0.8568696976 0.9196279049 + 0.9365519285 0.8576403260 0.9199851155 + 0.9369189143 0.8584108949 0.9203380942 + 0.9372844100 0.8591815233 0.9206891060 + 0.9376500249 0.8599520922 0.9210399985 + 0.9380155802 0.8607226014 0.9213908911 + 0.9383810759 0.8614792228 0.9217417836 + 0.9387466908 0.8622341156 0.9220927954 + 0.9391040802 0.8629890084 0.9224383831 + 0.9394611716 0.8637439013 0.9227827191 + 0.9398183227 0.8644987941 0.9231269956 + 0.9401754141 0.8652538061 0.9234712720 + 0.9405325055 0.8659942150 0.9238156080 + 0.9408891797 0.8667339087 0.9241598845 + 0.9412378073 0.8674736023 0.9244983196 + 0.9415863752 0.8682132959 0.9248359799 + 0.9419351220 0.8689528704 0.9251735806 + 0.9422836900 0.8696926236 0.9255113006 + 0.9426323175 0.8704177737 0.9258490205 + 0.9429801106 0.8711423278 0.9261866808 + 0.9433202147 0.8718668222 0.9265180230 + 0.9436603189 0.8725913167 0.9268490076 + 0.9440004230 0.8733158112 0.9271798730 + 0.9443404078 0.8740403056 0.9275109172 + 0.9446805120 0.8747491241 0.9278417826 + 0.9450199008 0.8754562736 0.9281728268 + 0.9453521967 0.8761636019 0.9284992814 + 0.9456846118 0.8768708110 0.9288254976 + 0.9460169077 0.8775780797 0.9291517138 + 0.9463492036 0.8782854080 0.9294778705 + 0.9466814995 0.8789795041 0.9298040867 + 0.9470136166 0.8796706796 0.9301303029 + 0.9473391175 0.8803619146 0.9304519296 + 0.9476644993 0.8810532093 0.9307729006 + 0.9479898810 0.8817443848 0.9310938716 + 0.9483153224 0.8824356198 0.9314149022 + 0.9486407042 0.8831164241 0.9317358732 + 0.9489660859 0.8837922215 0.9320567846 + 0.9492859244 0.8844680190 0.9323734045 + 0.9496051073 0.8851438165 0.9326885939 + 0.9499244094 0.8858196735 0.9330039024 + 0.9502437115 0.8864954710 0.9333190918 + 0.9505628943 0.8871642947 0.9336344004 + 0.9508821964 0.8878256083 0.9339495897 + 0.9511961937 0.8884869218 0.9342616200 + 0.9515085220 0.8891481161 0.9345716238 + 0.9518209100 0.8898094296 0.9348815084 + 0.9521332979 0.8904706836 0.9351915121 + 0.9524456263 0.8911284804 0.9355015159 + 0.9527580142 0.8917747736 0.9358114004 + 0.9530653954 0.8924211264 0.9361190200 + 0.9533687830 0.8930674195 0.9364230037 + 0.9536721706 0.8937137723 0.9367269874 + 0.9539756179 0.8943601251 0.9370309711 + 0.9542790055 0.8950064182 0.9373350739 + 0.9545822740 0.8956375122 0.9376391172 + 0.9548830986 0.8962674737 0.9379425049 + 0.9551774263 0.8968974948 0.9382423162 + 0.9554716945 0.8975275159 0.9385421276 + 0.9557660222 0.8981574774 0.9388418794 + 0.9560602903 0.8987876177 0.9391416907 + 0.9563546181 0.8994089961 0.9394415021 + 0.9566488862 0.9000241756 0.9397413135 + 0.9569364190 0.9006392956 0.9400386810 + 0.9572235942 0.9012544751 0.9403355718 + 0.9575108886 0.9018695951 0.9406325221 + 0.9577981830 0.9024847746 0.9409294128 + 0.9580854177 0.9030973911 0.9412263036 + 0.9583727121 0.9036974907 0.9415231943 + 0.9586551785 0.9042975903 0.9418178201 + 0.9589346051 0.9048978090 0.9421097040 + 0.9592139721 0.9054979086 0.9424015880 + 0.9594933987 0.9060980082 0.9426935911 + 0.9597727060 0.9066982269 0.9429854751 + 0.9600520730 0.9072868228 0.9432774782 + 0.9603300095 0.9078708291 0.9435690045 + 0.9606015086 0.9084547758 0.9438548088 + 0.9608730078 0.9090389013 0.9441404939 + 0.9611443877 0.9096229076 0.9444261789 + 0.9614158869 0.9102069139 0.9447119236 + 0.9616873264 0.9107877016 0.9449977279 + 0.9619588256 0.9113559127 0.9452834129 + 0.9622257948 0.9119240046 0.9455662966 + 0.9624909163 0.9124920964 0.9458472729 + 0.9627560973 0.9130601883 0.9461283088 + 0.9630212784 0.9136283994 0.9464092255 + 0.9632865191 0.9141964912 0.9466902018 + 0.9635517001 0.9147549272 0.9469711781 + 0.9638155103 0.9153077006 0.9472519755 + 0.9640734196 0.9158604145 0.9475296736 + 0.9643313289 0.9164131880 0.9478074908 + 0.9645891786 0.9169659019 0.9480851889 + 0.9648472071 0.9175186753 0.9483630061 + 0.9651051164 0.9180713296 0.9486407042 + 0.9653630257 0.9186087847 0.9489185214 + 0.9656159282 0.9191462994 0.9491940737 + 0.9658656120 0.9196838140 0.9494674206 + 0.9661152959 0.9202213287 0.9497408271 + 0.9663649201 0.9207587838 0.9500141144 + 0.9666146040 0.9212962985 0.9502874017 + 0.9668642879 0.9218286872 0.9505608082 + 0.9671139121 0.9223511219 0.9508340955 + 0.9673559070 0.9228736162 0.9511033893 + 0.9675977230 0.9233959913 0.9513720870 + 0.9678394198 0.9239184856 0.9516407251 + 0.9680811763 0.9244409204 0.9519094229 + 0.9683228731 0.9249634147 0.9521780014 + 0.9685646892 0.9254769087 0.9524466991 + 0.9688041806 0.9259846210 0.9527146220 + 0.9690378904 0.9264922738 0.9529792070 + 0.9692716002 0.9269999862 0.9532437921 + 0.9695051908 0.9275076985 0.9535083771 + 0.9697389007 0.9280154109 0.9537730217 + 0.9699726105 0.9285231233 0.9540376067 + 0.9702062011 0.9290189743 0.9543021917 + 0.9704347849 0.9295123816 0.9545649290 + 0.9706588984 0.9300057292 0.9548249841 + 0.9708830118 0.9304990172 0.9550849795 + 0.9711070061 0.9309923053 0.9553450942 + 0.9713311195 0.9314855933 0.9556052089 + 0.9715551734 0.9319790006 0.9558652043 + 0.9717792273 0.9324582815 0.9561253190 + 0.9719976187 0.9329372048 0.9563831091 + 0.9722136855 0.9334161282 0.9566395879 + 0.9724298716 0.9338951111 0.9568961263 + 0.9726459980 0.9343739748 0.9571526051 + 0.9728621840 0.9348528981 0.9574090838 + 0.9730783105 0.9353309274 0.9576656222 + 0.9732944965 0.9357969165 0.9579221010 + 0.9735053778 0.9362627864 0.9581764936 + 0.9737156034 0.9367287159 0.9584304094 + 0.9739257097 0.9371945858 0.9586842060 + 0.9741358161 0.9376605749 0.9589381218 + 0.9743459821 0.9381265044 0.9591919184 + 0.9745560884 0.9385910034 0.9594457746 + 0.9747661948 0.9390441179 0.9596995711 + 0.9749692082 0.9394972920 0.9599508047 + 0.9751719236 0.9399505258 0.9602015018 + 0.9753745198 0.9404035807 0.9604521990 + 0.9755771160 0.9408568144 0.9607028961 + 0.9757797718 0.9413099289 0.9609535933 + 0.9759824276 0.9417619109 0.9612042904 + 0.9761850238 0.9422025084 0.9614549875 + 0.9763807058 0.9426429868 0.9617013931 + 0.9765762091 0.9430835843 0.9619473219 + 0.9767717123 0.9435241818 0.9621930718 + 0.9769670963 0.9439647198 0.9624388814 + 0.9771625996 0.9444053173 0.9626846910 + 0.9773581028 0.9448456168 0.9629306197 + 0.9775536060 0.9452726245 0.9631764293 + 0.9777433276 0.9456995726 0.9634190798 + 0.9779326916 0.9461265802 0.9636611938 + 0.9781219959 0.9465535879 0.9639033079 + 0.9783114195 0.9469807148 0.9641454220 + 0.9785007238 0.9474077225 0.9643874764 + 0.9786900878 0.9478346705 0.9646295905 + 0.9788795114 0.9482499957 0.9648717046 + 0.9790633917 0.9486637115 0.9651131034 + 0.9792463183 0.9490774870 0.9653542042 + 0.9794291854 0.9494913220 0.9655953050 + 0.9796121716 0.9499049783 0.9658364058 + 0.9797950983 0.9503188133 0.9660773873 + 0.9799780846 0.9507325888 0.9663184881 + 0.9801610112 0.9511371255 0.9665595889 + 0.9803391099 0.9515374899 0.9667994976 + 0.9805151224 0.9519379735 0.9670386910 + 0.9806911945 0.9523383975 0.9672778249 + 0.9808672071 0.9527388215 0.9675168991 + 0.9810432196 0.9531391859 0.9677560925 + 0.9812191725 0.9535397291 0.9679952264 + 0.9813953042 0.9539344907 0.9682343006 + 0.9815682173 0.9543222785 0.9684708714 + 0.9817377925 0.9547100067 0.9687036872 + 0.9819074273 0.9550976753 0.9689365029 + 0.9820770025 0.9554854035 0.9691691995 + 0.9822465777 0.9558731914 0.9694020152 + 0.9824162722 0.9562609196 0.9696347713 + 0.9825859070 0.9566473961 0.9698675871 + 0.9827542901 0.9570254087 0.9700998068 + 0.9829186797 0.9574033022 0.9703279734 + 0.9830831289 0.9577813148 0.9705563188 + 0.9832473993 0.9581593275 0.9707846045 + 0.9834117889 0.9585372210 0.9710127711 + 0.9835761786 0.9589151740 0.9712411165 + 0.9837405086 0.9592931867 0.9714694023 + 0.9839048982 0.9596645832 0.9716976285 + 0.9840624928 0.9600340128 0.9719249010 + 0.9842191935 0.9604033232 0.9721521139 + 0.9843758941 0.9607725739 0.9723792076 + 0.9845327139 0.9611418843 0.9726063013 + 0.9846894145 0.9615113139 0.9728333950 + 0.9848461747 0.9618806243 0.9730604887 + 0.9850028753 0.9622468948 0.9732875824 + 0.9851561785 0.9626067877 0.9735137820 + 0.9853060246 0.9629666209 0.9737386107 + 0.9854558110 0.9633265138 0.9739633799 + 0.9856055975 0.9636862874 0.9741883278 + 0.9857553840 0.9640461802 0.9744130969 + 0.9859051704 0.9644060135 0.9746379256 + 0.9860550165 0.9647659063 0.9748628139 + 0.9862046242 0.9651184082 0.9750875831 + 0.9863498807 0.9654694200 0.9753091931 + 0.9864953160 0.9658203125 0.9755306840 + 0.9866406918 0.9661713243 0.9757522941 + 0.9867861271 0.9665222168 0.9759737849 + 0.9869315028 0.9668732285 0.9761952758 + 0.9870768785 0.9672241211 0.9764168859 + 0.9872221947 0.9675728083 0.9766383767 + 0.9873657823 0.9679149985 0.9768583179 + 0.9875077009 0.9682573080 0.9770764112 + 0.9876495004 0.9685994983 0.9772945046 + 0.9877914190 0.9689416885 0.9775127172 + 0.9879332781 0.9692839980 0.9777308106 + 0.9880751967 0.9696261883 0.9779489040 + 0.9882171154 0.9699683785 0.9781669974 + 0.9883589745 0.9703047872 0.9783850908 + 0.9884964824 0.9706377983 0.9786008000 + 0.9886338115 0.9709706903 0.9788162708 + 0.9887710214 0.9713037014 0.9790318012 + 0.9889082909 0.9716365933 0.9792472720 + 0.9890456200 0.9719696045 0.9794626832 + 0.9891828895 0.9723024964 0.9796782136 + 0.9893202186 0.9726355076 0.9798936844 + 0.9894558191 0.9729598761 0.9801084995 + 0.9895876050 0.9732838273 0.9803212285 + 0.9897193909 0.9736077189 0.9805338979 + 0.9898512959 0.9739316106 0.9807466865 + 0.9899830818 0.9742555022 0.9809594154 + 0.9901149273 0.9745793939 0.9811722040 + 0.9902467728 0.9749032855 0.9813848734 + 0.9903786182 0.9752253294 0.9815976024 + 0.9905071855 0.9755398035 0.9818084240 + 0.9906337261 0.9758542180 0.9820175767 + 0.9907602072 0.9761685729 0.9822267890 + 0.9908868074 0.9764831066 0.9824360013 + 0.9910132885 0.9767975211 0.9826452732 + 0.9911398292 0.9771118760 0.9828544855 + 0.9912663102 0.9774262905 0.9830636978 + 0.9913927913 0.9777371883 0.9832729101 + 0.9915155768 0.9780421257 0.9834790230 + 0.9916377068 0.9783470035 0.9836843014 + 0.9917597771 0.9786518812 0.9838895798 + 0.9918819070 0.9789568782 0.9840949774 + 0.9920039177 0.9792618155 0.9843003154 + 0.9921259880 0.9795666933 0.9845055938 + 0.9922481179 0.9798716903 0.9847108722 + 0.9923701286 0.9801716208 0.9849162102 + 0.9924876094 0.9804664850 0.9851183891 + 0.9926049709 0.9807614088 0.9853205085 + 0.9927225113 0.9810562730 0.9855225086 + 0.9928399920 0.9813511968 0.9857246280 + 0.9929574132 0.9816461205 0.9859266281 + 0.9930748940 0.9819409847 0.9861286879 + 0.9931923747 0.9822359085 0.9863306880 + 0.9933093190 0.9825251102 0.9865326285 + 0.9934214950 0.9828099012 0.9867315292 + 0.9935336709 0.9830945730 0.9869303703 + 0.9936459064 0.9833794236 0.9871293902 + 0.9937580824 0.9836640954 0.9873282909 + 0.9938703179 0.9839488864 0.9875271916 + 0.9939824939 0.9842336178 0.9877260923 + 0.9940947890 0.9845184088 0.9879251122 + 0.9942061901 0.9847974181 0.9881235957 + 0.9943130016 0.9850715995 0.9883189201 + 0.9944198132 0.9853457808 0.9885141253 + 0.9945265055 0.9856200814 0.9887093902 + 0.9946333170 0.9858943224 0.9889047146 + 0.9947401285 0.9861685038 0.9890999198 + 0.9948468804 0.9864426851 0.9892951846 + 0.9949536920 0.9867169261 0.9894905090 + 0.9950599074 0.9869858027 0.9896854758 + 0.9951617718 0.9872484803 0.9898774028 + 0.9952635765 0.9875112176 0.9900693297 + 0.9953655005 0.9877738953 0.9902613163 + 0.9954673052 0.9880366921 0.9904531837 + 0.9955692291 0.9882994294 0.9906451106 + 0.9956709743 0.9885621071 0.9908369780 + 0.9957728982 0.9888247848 0.9910290241 + 0.9958745837 0.9890835881 0.9912208915 + 0.9959716201 0.9893341064 0.9914097786 + 0.9960685968 0.9895846844 0.9915987253 + 0.9961655736 0.9898352027 0.9917874932 + 0.9962626100 0.9900857210 0.9919763803 + 0.9963595867 0.9903361797 0.9921653271 + 0.9964566231 0.9905868173 0.9923540950 + 0.9965535998 0.9908372760 0.9925429821 + 0.9966505766 0.9910866022 0.9927319288 + 0.9967427254 0.9913256764 0.9929165244 + 0.9968339205 0.9915646911 0.9931002259 + 0.9969251752 0.9918038249 0.9932839274 + 0.9970163703 0.9920428991 0.9934676290 + 0.9971076846 0.9922819138 0.9936512709 + 0.9971988797 0.9925209880 0.9938349724 + 0.9972901940 0.9927601218 0.9940186739 + 0.9973813891 0.9929991961 0.9942023754 + 0.9974687099 0.9932283759 0.9943823218 + 0.9975535870 0.9934555292 0.9945598245 + 0.9976384044 0.9936826825 0.9947373271 + 0.9977232814 0.9939097762 0.9949148297 + 0.9978080988 0.9941369891 0.9950922728 + 0.9978929758 0.9943640828 0.9952697754 + 0.9979777932 0.9945911765 0.9954472780 + 0.9980626702 0.9948183894 0.9956247807 + 0.9981455207 0.9950395823 0.9958003759 + 0.9982240796 0.9952540994 0.9959719181 + 0.9983025789 0.9954686761 0.9961432815 + 0.9983811975 0.9956831932 0.9963148236 + 0.9984596968 0.9958978295 0.9964861870 + 0.9985383153 0.9961122870 0.9966577291 + 0.9986168146 0.9963269234 0.9968290925 + 0.9986953735 0.9965413809 0.9970005751 + 0.9987738729 0.9967551827 0.9971719980 + 0.9988479018 0.9969543219 0.9973402023 + 0.9989216924 0.9971534014 0.9975082278 + 0.9989954233 0.9973524213 0.9976761937 + 0.9990692139 0.9975515008 0.9978441000 + 0.9991430044 0.9977505803 0.9980121255 + 0.9992166758 0.9979497194 0.9981800914 + 0.9992905259 0.9981487989 0.9983481169 + 0.9993643165 0.9983478785 0.9985160232 + 0.9994363785 0.9985377789 0.9986823797 + 0.9995067716 0.9987205863 0.9988471270 + 0.9995772839 0.9989033937 0.9990118146 + 0.9996476769 0.9990860820 0.9991765022 + 0.9997181892 0.9992688894 0.9993411899 + 0.9997885823 0.9994516969 0.9995058775 + 0.9998590946 0.9996345043 0.9996706247 + 0.9999294877 0.9998171926 0.9998353124 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfachrome_ct_precisa_200.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfachrome_ct_precisa_200.spi1d index 99b9598f..c72c5e3e 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfachrome_ct_precisa_200.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfachrome_ct_precisa_200.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0002699328 0.0001753761 0.0002523783 - 0.0005516693 0.0003570581 0.0005202319 - 0.0008440902 0.0005450998 0.0008039851 - 0.0011470990 0.0007395549 0.0011026140 - 0.0014597330 0.0009404911 0.0014156949 - 0.0017813731 0.0011476850 0.0017421190 - 0.0021126880 0.0013612730 0.0020805011 - 0.0024531030 0.0015815590 0.0024294229 - 0.0028031650 0.0018088090 0.0027884119 - 0.0031638411 0.0020427699 0.0031579121 - 0.0035372649 0.0022836919 0.0035393820 - 0.0039257319 0.0025316810 0.0039346260 - 0.0043312181 0.0027867940 0.0043465500 - 0.0047563538 0.0030489860 0.0047766608 - 0.0052017048 0.0033181130 0.0052262088 - 0.0056667780 0.0035944311 0.0056956508 - 0.0061506722 0.0038782239 0.0061842999 - 0.0066511780 0.0041694939 0.0066920272 - 0.0071674078 0.0044688610 0.0072181849 - 0.0076976968 0.0047760759 0.0077622221 - 0.0082411859 0.0050910199 0.0083242692 - 0.0087973205 0.0054136459 0.0089058857 - 0.0093660019 0.0057440512 0.0095065739 - 0.0099473577 0.0060824021 0.0101279598 - 0.0105416896 0.0064286222 0.0107696000 - 0.0111480197 0.0067824679 0.0114314295 - 0.0117668100 0.0071440842 0.0121141002 - 0.0123977298 0.0075133010 0.0128158899 - 0.0130404802 0.0078901956 0.0135375699 - 0.0136951199 0.0082744109 0.0142785497 - 0.0143617000 0.0086660404 0.0150373597 - 0.0150401201 0.0090650087 0.0158140305 - 0.0157300606 0.0094716744 0.0166073292 - 0.0164318401 0.0098855980 0.0174160805 - 0.0171449091 0.0103072198 0.0182415899 - 0.0178689398 0.0107360203 0.0190821793 - 0.0186050907 0.0111725898 0.0199379697 - 0.0193527602 0.0116162105 0.0208086297 - 0.0201126505 0.0120675396 0.0216945801 - 0.0208839308 0.0125266099 0.0225958191 - 0.0216670800 0.0129928002 0.0235118493 - 0.0224615298 0.0134667596 0.0244435798 - 0.0232677106 0.0139479404 0.0253902208 - 0.0240852199 0.0144363800 0.0263507906 - 0.0249137003 0.0149327302 0.0273268502 - 0.0257528108 0.0154370395 0.0283182394 - 0.0266024694 0.0159486309 0.0293240100 - 0.0274628494 0.0164675806 0.0303431805 - 0.0283344202 0.0169943292 0.0313761793 - 0.0292166006 0.0175288208 0.0324220508 - 0.0301091801 0.0180700701 0.0334794186 - 0.0310120601 0.0186186992 0.0345463790 - 0.0319244787 0.0191750694 0.0356225707 - 0.0328475386 0.0197390206 0.0367073193 - 0.0337808393 0.0203102101 0.0377987213 - 0.0347246900 0.0208885409 0.0388945192 - 0.0356788710 0.0214745607 0.0399941206 - 0.0366426483 0.0220674798 0.0410979390 - 0.0376164615 0.0226682406 0.0422052182 - 0.0386001393 0.0232766997 0.0433141701 - 0.0395945087 0.0238917898 0.0444252789 - 0.0405988395 0.0245153997 0.0455389805 - 0.0416126810 0.0251462702 0.0466558412 - 0.0426358394 0.0257849507 0.0477751605 - 0.0436693914 0.0264302809 0.0488959402 - 0.0447117910 0.0270844605 0.0500185490 - 0.0457648709 0.0277455002 0.0511438698 - 0.0468259901 0.0284139998 0.0522733182 - 0.0478970297 0.0290895496 0.0534073599 - 0.0489763990 0.0297721792 0.0545452908 - 0.0500655286 0.0304632597 0.0556870997 - 0.0511628389 0.0311617199 0.0568346418 - 0.0522699915 0.0318679288 0.0579884015 - 0.0533850715 0.0325823203 0.0591484793 - 0.0545096211 0.0333043113 0.0603141114 - 0.0556424297 0.0340342708 0.0614848994 - 0.0567837097 0.0347718298 0.0626629367 - 0.0579337105 0.0355160087 0.0638478696 - 0.0590906888 0.0362669006 0.0650385097 - 0.0602574088 0.0370258093 0.0662359372 - 0.0614334010 0.0377927385 0.0674390867 - 0.0626170933 0.0385673605 0.0686485395 - 0.0638106465 0.0393497609 0.0698641911 - 0.0650118366 0.0401403792 0.0710861981 - 0.0662213415 0.0409397595 0.0723134726 - 0.0674379617 0.0417471118 0.0735469386 - 0.0686631873 0.0425618887 0.0747859776 - 0.0698976368 0.0433834791 0.0760307387 - 0.0711404011 0.0442118682 0.0772804022 - 0.0723914728 0.0450474396 0.0785348937 - 0.0736518502 0.0458911397 0.0797948688 - 0.0749201477 0.0467427112 0.0810601711 - 0.0761969313 0.0476017408 0.0823324025 - 0.0774813071 0.0484685600 0.0836096331 - 0.0787731186 0.0493434109 0.0848924294 - 0.0800740421 0.0502262115 0.0861801729 - 0.0813831016 0.0511168912 0.0874729678 - 0.0827006921 0.0520157702 0.0887702629 - 0.0840270668 0.0529231317 0.0900717899 - 0.0853607282 0.0538391396 0.0913778096 - 0.0867023394 0.0547635406 0.0926876292 - 0.0880530998 0.0556963496 0.0940016583 - 0.0894121006 0.0566373095 0.0953195170 - 0.0907792076 0.0575862303 0.0966407433 - 0.0921551064 0.0585427284 0.0979649574 - 0.0935385600 0.0595068783 0.0992923602 - 0.0949299335 0.0604788885 0.1006235033 - 0.0963296816 0.0614589490 0.1019582972 - 0.0977364033 0.0624472797 0.1032961980 - 0.0991507098 0.0634440780 0.1046369970 - 0.1005733013 0.0644495711 0.1059795022 - 0.1020032987 0.0654638633 0.1073246971 - 0.1034407988 0.0664867088 0.1086725965 - 0.1048860997 0.0675178468 0.1100225002 - 0.1063387021 0.0685571134 0.1113744006 - 0.1077983007 0.0696041286 0.1127282009 - 0.1092654988 0.0706589893 0.1140839010 - 0.1107404977 0.0717224628 0.1154410988 - 0.1122227013 0.0727944970 0.1168001965 - 0.1137126014 0.0738748014 0.1181621030 - 0.1152106971 0.0749634504 0.1195268035 - 0.1167161986 0.0760605484 0.1208942011 - 0.1182283014 0.0771661699 0.1222644001 - 0.1197486967 0.0782803968 0.1236374006 - 0.1212769002 0.0794033334 0.1250136942 - 0.1228116006 0.0805374980 0.1263937950 - 0.1243539006 0.0816816911 0.1277772933 - 0.1259036064 0.0828360692 0.1291648000 - 0.1274601966 0.0840003714 0.1305564940 - 0.1290239990 0.0851741210 0.1319530010 - 0.1305951029 0.0863568485 0.1333540976 - 0.1321730018 0.0875512436 0.1347596943 - 0.1337587982 0.0887546614 0.1361702979 - 0.1353518963 0.0899670422 0.1375872940 - 0.1369515061 0.0911882967 0.1390096992 - 0.1385575980 0.0924183726 0.1404386014 - 0.1401696056 0.0936605185 0.1418738067 - 0.1417876929 0.0949114934 0.1433147937 - 0.1434136033 0.0961711928 0.1447637975 - 0.1450462937 0.0974395201 0.1462194026 - 0.1466850936 0.0987184718 0.1476815939 - 0.1483296007 0.1000078022 0.1491505057 - 0.1499803066 0.1013054997 0.1506260037 - 0.1516395956 0.1026116982 0.1521109045 - 0.1533043981 0.1039282009 0.1536028981 - 0.1549746990 0.1052552983 0.1551018059 - 0.1566507071 0.1065905988 0.1566075981 - 0.1583354026 0.1079339013 0.1581221074 - 0.1600269973 0.1092884988 0.1596440971 - 0.1617241949 0.1106527001 0.1611728072 - 0.1634269953 0.1120247021 0.1627078950 - 0.1651381999 0.1134048030 0.1642509997 - 0.1668564975 0.1147963032 0.1658020020 - 0.1685802042 0.1161947995 0.1673590988 - 0.1703090966 0.1176000983 0.1689217985 - 0.1720459014 0.1190157980 0.1704941988 - 0.1737886965 0.1204395965 0.1720733941 - 0.1755366027 0.1218703017 0.1736585945 - 0.1772904992 0.1233104020 0.1752517074 - 0.1790520996 0.1247609034 0.1768528074 - 0.1808187962 0.1262181997 0.1784588993 - 0.1825904995 0.1276842058 0.1800702065 - 0.1843689978 0.1291611046 0.1816917062 - 0.1861528009 0.1306443959 0.1833177954 - 0.1879408062 0.1321358979 0.1849482954 - 0.1897339970 0.1336382031 0.1865873039 - 0.1915318966 0.1351462007 0.1882306933 - 0.1933335066 0.1366620958 0.1898781061 - 0.1951407045 0.1381859928 0.1915331036 - 0.1969527006 0.1397145987 0.1931923032 - 0.1987676024 0.1412506998 0.1948550940 - 0.2005867958 0.1427935958 0.1965250969 - 0.2024096996 0.1443406940 0.1981987059 - 0.2042354047 0.1458964050 0.1998753995 - 0.2060655951 0.1474573016 0.2015592009 - 0.2078990936 0.1490225941 0.2032458931 - 0.2097346038 0.1505970955 0.2049356997 - 0.2115736008 0.1521753073 0.2066316009 - 0.2134155929 0.1537598073 0.2083299011 - 0.2152598053 0.1553504020 0.2100317031 - 0.2171059996 0.1569440067 0.2117380053 - 0.2189532965 0.1585451961 0.2134467959 - 0.2208018005 0.1601499021 0.2151606977 - 0.2226525992 0.1617594957 0.2168776989 - 0.2245036960 0.1633763015 0.2185973972 - 0.2263543010 0.1649962068 0.2203236967 - 0.2282046974 0.1666252017 0.2220526040 - 0.2300550044 0.1682582051 0.2237869948 - 0.2319052964 0.1698971987 0.2255263031 - 0.2337547988 0.1715423018 0.2272689044 - 0.2356023043 0.1731901020 0.2290178984 - 0.2374480069 0.1748462021 0.2307692021 - 0.2392929047 0.1765045971 0.2325260937 - 0.2411359996 0.1781698018 0.2342863977 - 0.2429780960 0.1798388958 0.2360495031 - 0.2448175997 0.1815121025 0.2378188968 - 0.2466558963 0.1831911057 0.2395901978 - 0.2484931946 0.1848724037 0.2413679063 - 0.2503274083 0.1865603030 0.2431481034 - 0.2521604002 0.1882493943 0.2449326068 - 0.2539891005 0.1899435967 0.2467211932 - 0.2558167875 0.1916399002 0.2485122979 - 0.2576422989 0.1933400929 0.2503088117 - 0.2594659925 0.1950432062 0.2521066070 - 0.2612882853 0.1967488974 0.2539105117 - 0.2631081939 0.1984582990 0.2557164133 - 0.2649275064 0.2001695931 0.2575275898 - 0.2667450905 0.2018844932 0.2593415976 - 0.2685621977 0.2036001980 0.2611587942 - 0.2703767121 0.2053194046 0.2629804015 - 0.2721900046 0.2070389986 0.2648043931 - 0.2740012109 0.2087621987 0.2666332126 - 0.2758114934 0.2104858011 0.2684633136 - 0.2776210010 0.2122109979 0.2703008056 - 0.2794296145 0.2139367014 0.2721393108 - 0.2812376916 0.2156637013 0.2739846110 - 0.2830449939 0.2173916996 0.2758311927 - 0.2848519981 0.2191213071 0.2776845992 - 0.2866581082 0.2208511978 0.2795400918 - 0.2884640992 0.2225816995 0.2814018130 - 0.2902691066 0.2243124992 0.2832661867 - 0.2920739949 0.2260437012 0.2851360142 - 0.2938778102 0.2277746946 0.2870089114 - 0.2956816852 0.2295051962 0.2888864875 - 0.2974855900 0.2312353998 0.2907676995 - 0.2992897928 0.2329653054 0.2926534116 - 0.3010956943 0.2346948981 0.2945432067 - 0.3029015958 0.2364241928 0.2964377999 - 0.3047079146 0.2381531000 0.2983360887 - 0.3065148890 0.2398813963 0.3002381921 - 0.3083240986 0.2416097969 0.3021444976 - 0.3101342022 0.2433384061 0.3040550947 - 0.3119468987 0.2450670004 0.3059692979 - 0.3137601912 0.2467954010 0.3078877032 - 0.3155752122 0.2485236973 0.3098101914 - 0.3173910081 0.2502520084 0.3117381930 - 0.3192102015 0.2519801855 0.3136689961 - 0.3210298121 0.2537078857 0.3156045079 - 0.3228524923 0.2554357052 0.3175427020 - 0.3246755004 0.2571632862 0.3194876015 - 0.3265027106 0.2588908970 0.3214339018 - 0.3283298910 0.2606168985 0.3233856857 - 0.3301618993 0.2623426914 0.3253385127 - 0.3319939077 0.2640664876 0.3272993863 - 0.3338311017 0.2657895982 0.3292602003 - 0.3356688917 0.2675099969 0.3312292099 - 0.3375113904 0.2692281902 0.3331983089 - 0.3393552005 0.2709451020 0.3351751864 - 0.3412027955 0.2726595998 0.3371534944 - 0.3430523872 0.2743734121 0.3391371071 - 0.3449051976 0.2760832906 0.3411231935 - 0.3467617035 0.2777931094 0.3431136012 - 0.3486200869 0.2795000076 0.3451083899 - 0.3504830897 0.2812066078 0.3471060097 - 0.3523468077 0.2829105854 0.3491097093 - 0.3542161882 0.2846131921 0.3511143029 - 0.3560855985 0.2863143981 0.3531267047 - 0.3579610884 0.2880134881 0.3551391065 - 0.3598372936 0.2897121012 0.3571586907 - 0.3617183864 0.2914075851 0.3591794968 - 0.3636018932 0.2931031883 0.3612056077 - 0.3654884994 0.2947953939 0.3632349968 - 0.3673790991 0.2964870930 0.3652678132 - 0.3692705929 0.2981764078 0.3673068881 - 0.3711664081 0.2998636067 0.3693464100 - 0.3730623126 0.3015499115 0.3713943064 - 0.3749629855 0.3032326996 0.3734421134 - 0.3768644929 0.3049154878 0.3754970133 - 0.3787690997 0.3065958023 0.3775540888 - 0.3806760013 0.3082754910 0.3796147108 - 0.3825837970 0.3099536002 0.3816798031 - 0.3844941854 0.3116297126 0.3837456107 - 0.3864046037 0.3133055866 0.3858197927 - 0.3883185983 0.3149785995 0.3878938854 - 0.3902328908 0.3166514933 0.3899737000 - 0.3921490908 0.3183214962 0.3920556009 - 0.3940671086 0.3199898005 0.3941405118 - 0.3959853053 0.3216570020 0.3962311149 - 0.3979054987 0.3233199120 0.3983218074 - 0.3998256922 0.3249827921 0.4004194140 - 0.4017480910 0.3266434073 0.4025174975 - 0.4036712050 0.3283031881 0.4046190977 - 0.4055953920 0.3299618959 0.4067240059 - 0.4075216949 0.3316178024 0.4088292122 - 0.4094479978 0.3332735896 0.4109373987 - 0.4113749862 0.3349258900 0.4130455852 - 0.4133020043 0.3365769982 0.4151561856 - 0.4152295887 0.3382270932 0.4172683954 - 0.4171578884 0.3398742974 0.4193809032 - 0.4190863073 0.3415214121 0.4214954972 - 0.4210152924 0.3431650996 0.4236100912 - 0.4229443073 0.3448075950 0.4257256985 - 0.4248732030 0.3464492857 0.4278419018 - 0.4268017113 0.3480867147 0.4299578965 - 0.4287301898 0.3497239947 0.4320721030 - 0.4306586087 0.3513588905 0.4341863096 - 0.4325869977 0.3529917896 0.4362998903 - 0.4345157146 0.3546248078 0.4384129047 - 0.4364447892 0.3562546968 0.4405260086 - 0.4383738935 0.3578845859 0.4426352084 - 0.4403015971 0.3595128059 0.4447444081 - 0.4422290921 0.3611381054 0.4468516111 - 0.4441559017 0.3627632856 0.4489555061 - 0.4460807145 0.3643858135 0.4510594904 - 0.4480054975 0.3660072088 0.4531568885 - 0.4499278963 0.3676283956 0.4552522898 - 0.4518488944 0.3692458868 0.4573468864 - 0.4537698925 0.3708634973 0.4594365954 - 0.4556883872 0.3724792004 0.4615261853 - 0.4576067924 0.3740915060 0.4636127055 - 0.4595240057 0.3757036924 0.4656955898 - 0.4614388943 0.3773143888 0.4677784145 - 0.4633539021 0.3789240122 0.4698548019 - 0.4652644098 0.3805336058 0.4719296098 - 0.4671730101 0.3821412027 0.4740034938 - 0.4690816104 0.3837484121 0.4760693014 - 0.4709844887 0.3853549957 0.4781351089 - 0.4728874862 0.3869574070 0.4801965058 - 0.4747886956 0.3885596991 0.4822503924 - 0.4766854942 0.3901604116 0.4843043983 - 0.4785822928 0.3917576969 0.4863505065 - 0.4804754853 0.3933550119 0.4883921146 - 0.4823656976 0.3949500024 0.4904336035 - 0.4842557907 0.3965430856 0.4924621880 - 0.4861414135 0.3981361091 0.4944886863 - 0.4880256951 0.3997269869 0.4965141118 - 0.4899098873 0.4013170898 0.4985252917 - 0.4917871952 0.4029071927 0.5005366206 - 0.4936642945 0.4044938982 0.5025439262 - 0.4955402911 0.4060800970 0.5045402050 - 0.4974110126 0.4076662064 0.5065364838 - 0.4992815852 0.4092473984 0.5085269213 - 0.5011492968 0.4108285904 0.5105090141 - 0.5030111074 0.4124091864 0.5124911070 - 0.5048729181 0.4139845073 0.5144640207 - 0.5067303777 0.4155597985 0.5164294839 - 0.5085831881 0.4171343148 0.5183951259 - 0.5104359984 0.4187046885 0.5203496218 - 0.5122830272 0.4202750921 0.5222988129 - 0.5141261816 0.4218446016 0.5242480040 - 0.5159693956 0.4234111905 0.5261831284 - 0.5178055763 0.4249776900 0.5281137824 - 0.5196387768 0.4265432060 0.5300444961 - 0.5214719176 0.4281054139 0.5319601893 - 0.5232948065 0.4296675026 0.5338729024 - 0.5251145959 0.4312283099 0.5357854962 - 0.5269343853 0.4327852130 0.5376809239 - 0.5287426114 0.4343419969 0.5395737290 - 0.5305480957 0.4358971119 0.5414664745 - 0.5323536992 0.4374468029 0.5433425903 - 0.5341475010 0.4389964044 0.5452167988 - 0.5359390974 0.4405452907 0.5470908880 - 0.5377306938 0.4420902133 0.5489476919 - 0.5395104885 0.4436351061 0.5508022904 - 0.5412878990 0.4451794922 0.5526568890 - 0.5430654287 0.4467185140 0.5544949174 - 0.5448313951 0.4482575059 0.5563300848 - 0.5465946794 0.4497964978 0.5581651926 - 0.5483580828 0.4513302147 0.5599843860 - 0.5501095057 0.4528639913 0.5617994070 - 0.5518571138 0.4543977976 0.5636144876 - 0.5536047220 0.4559254944 0.5654160976 - 0.5553417206 0.4574523866 0.5672122836 - 0.5570737123 0.4589793086 0.5690084100 - 0.5588057041 0.4605017900 0.5707932711 - 0.5605282187 0.4620226920 0.5725706816 - 0.5622438788 0.4635435939 0.5743482113 - 0.5639594793 0.4650601149 0.5761160254 - 0.5656672716 0.4665732086 0.5778731108 - 0.5673655272 0.4680863023 0.5796301961 - 0.5690636039 0.4695965946 0.5813813806 - 0.5707573295 0.4711019993 0.5831196904 - 0.5724400282 0.4726073146 0.5848580003 - 0.5741227269 0.4741115868 0.5865939260 - 0.5758035779 0.4756088853 0.5883123875 - 0.5774692297 0.4771062136 0.5900310278 - 0.5791348219 0.4786033928 0.5917496085 - 0.5808004737 0.4800952971 0.5934519768 - 0.5824471116 0.4815863073 0.5951527953 - 0.5840917230 0.4830772877 0.5968536139 - 0.5857362747 0.4845638871 0.5985416770 - 0.5873677135 0.4860475063 0.6002234817 - 0.5889924765 0.4875310957 0.6019052863 - 0.5906172991 0.4890125990 0.6035785079 - 0.5922335982 0.4904884994 0.6052387953 - 0.5938374996 0.4919644892 0.6068990827 - 0.5954412818 0.4934403896 0.6085569859 - 0.5970427990 0.4949091077 0.6101952791 - 0.5986260176 0.4963772893 0.6118335128 - 0.6002092957 0.4978454113 0.6134716868 - 0.6017925143 0.4993087053 0.6150943041 - 0.6033605933 0.5007681847 0.6167126298 - 0.6049249768 0.5022277236 0.6183308959 - 0.6064894199 0.5036858916 0.6199414134 - 0.6080433726 0.5051380992 0.6215417981 - 0.6095849872 0.5065904260 0.6231423020 - 0.6111266017 0.5080425739 0.6247416735 - 0.6126660705 0.5094881058 0.6263244748 - 0.6141831875 0.5109314919 0.6279072762 - 0.6157003045 0.5123748183 0.6294900775 - 0.6172174215 0.5138152242 0.6310620904 - 0.6187192798 0.5152488947 0.6326283813 - 0.6202139854 0.5166826248 0.6341947913 - 0.6217088103 0.5181162953 0.6357573271 - 0.6231973767 0.5195432901 0.6373063922 - 0.6246680021 0.5209690928 0.6388555169 - 0.6261385083 0.5223947763 0.6404047012 - 0.6276090741 0.5238174796 0.6419379711 - 0.6290594935 0.5252338052 0.6434664130 - 0.6305047870 0.5266500711 0.6449947953 - 0.6319500804 0.5280663967 0.6465178728 - 0.6333882213 0.5294749737 0.6480270028 - 0.6348105073 0.5308814049 0.6495360136 - 0.6362327933 0.5322877169 0.6510450244 - 0.6376551986 0.5336924195 0.6525406241 - 0.6390563250 0.5350908041 0.6540316939 - 0.6404522061 0.5364891887 0.6555228233 - 0.6418480873 0.5378875732 0.6570094824 - 0.6432371140 0.5392802954 0.6584786177 - 0.6446061730 0.5406693816 0.6599478126 - 0.6459752917 0.5420585871 0.6614168882 - 0.6473444104 0.5434476733 0.6628721952 - 0.6486949921 0.5448269844 0.6643189788 - 0.6500368714 0.5462061763 0.6657658219 - 0.6513789296 0.5475854874 0.6672123075 - 0.6527189016 0.5489618182 0.6686396003 - 0.6540355086 0.5503323078 0.6700670123 - 0.6553521156 0.5517029166 0.6714943051 - 0.6566687822 0.5530735254 0.6729140878 - 0.6579744816 0.5544376969 0.6743212938 - 0.6592665911 0.5557985902 0.6757284999 - 0.6605587006 0.5571594238 0.6771355867 - 0.6618508101 0.5585203171 0.6785268188 - 0.6631215811 0.5598711967 0.6799110770 - 0.6643863916 0.5612214804 0.6812955141 - 0.6656513214 0.5625717044 0.6826797724 - 0.6669138074 0.5639197826 0.6840425730 - 0.6681522131 0.5652583241 0.6854054928 - 0.6693906784 0.5665968060 0.6867682934 - 0.6706290841 0.5679352880 0.6881262064 - 0.6718584895 0.5692687035 0.6894711256 - 0.6730719805 0.5705946088 0.6908161044 - 0.6742855906 0.5719205737 0.6921610236 - 0.6754990816 0.5732464790 0.6934955716 - 0.6766976118 0.5745657086 0.6948205233 - 0.6778864264 0.5758802891 0.6961454153 - 0.6790751815 0.5771949291 0.6974701881 - 0.6802639961 0.5785095096 0.6987801194 - 0.6814342141 0.5798168182 0.7000842094 - 0.6826003194 0.5811215043 0.7013882995 - 0.6837664843 0.5824263096 0.7026923895 - 0.6849324703 0.5837309957 0.7039760947 - 0.6860750914 0.5850257874 0.7052575946 - 0.6872177124 0.5863190889 0.7065389752 - 0.6883602142 0.5876123905 0.7078195214 - 0.6894991994 0.5889056921 0.7090789080 - 0.6906188130 0.5901870728 0.7103384137 - 0.6917384267 0.5914680958 0.7115979195 - 0.6928580999 0.5927491188 0.7128540874 - 0.6939715147 0.5940297246 0.7140920162 - 0.6950684190 0.5952988863 0.7153298855 - 0.6961653829 0.5965679884 0.7165678144 - 0.6972624063 0.5978370905 0.7178003192 - 0.6983522773 0.5991057754 0.7190150023 - 0.6994295716 0.6003659964 0.7202296853 - 0.7005069852 0.6016262770 0.7214444280 - 0.7015843987 0.6028864980 0.7226526737 - 0.7026544809 0.6041461825 0.7238447070 - 0.7037143707 0.6053941250 0.7250366807 - 0.7047743201 0.6066418886 0.7262287140 - 0.7058342099 0.6078897119 0.7274144888 - 0.7068858743 0.6091374755 0.7285851836 - 0.7079268098 0.6103718877 0.7297558784 - 0.7089678049 0.6116061807 0.7309266925 - 0.7100088000 0.6128405929 0.7320916057 - 0.7110424042 0.6140750051 0.7332413197 - 0.7120658159 0.6152967215 0.7343909740 - 0.7130892277 0.6165174842 0.7355408072 - 0.7141125798 0.6177383065 0.7366862297 - 0.7151290178 0.6189591289 0.7378168106 - 0.7161335945 0.6201689839 0.7389473915 - 0.7171381712 0.6213768125 0.7400779128 - 0.7181428075 0.6225845218 0.7412056923 - 0.7191423774 0.6237921715 0.7423154712 - 0.7201297879 0.6249904037 0.7434253097 - 0.7211171985 0.6261842847 0.7445352077 - 0.7221046090 0.6273781061 0.7456446886 - 0.7230893970 0.6285719872 0.7467349768 - 0.7240619063 0.6297590733 0.7478252053 - 0.7250344753 0.6309399009 0.7489154935 - 0.7260071039 0.6321207285 0.7500057817 - 0.7269790769 0.6333013773 0.7510781288 - 0.7279344797 0.6344782710 0.7521474957 - 0.7288898826 0.6356462836 0.7532169223 - 0.7298454046 0.6368141770 0.7542862296 - 0.7308008075 0.6379821897 0.7553424835 - 0.7317430973 0.6391494274 0.7563924789 - 0.7326831222 0.6403048038 0.7574424148 - 0.7336230874 0.6414602995 0.7584924102 - 0.7345631123 0.6426156759 0.7595331073 - 0.7354928851 0.6437711120 0.7605624795 - 0.7364168167 0.6449149251 0.7615919113 - 0.7373406291 0.6460554004 0.7626212835 - 0.7382645011 0.6471958160 0.7636470795 - 0.7391830087 0.6483362913 0.7646576166 - 0.7400926948 0.6494706273 0.7656682134 - 0.7410023808 0.6505978703 0.7666786909 - 0.7419121861 0.6517251134 0.7676892877 - 0.7428205013 0.6528524160 0.7686821818 - 0.7437157035 0.6539782882 0.7696729898 - 0.7446109056 0.6550930738 0.7706639171 - 0.7455059886 0.6562079191 0.7716547251 - 0.7464011908 0.6573228240 0.7726355195 - 0.7472869158 0.6584376097 0.7736086249 - 0.7481697798 0.6595439911 0.7745816708 - 0.7490527034 0.6606470942 0.7755547166 - 0.7499356270 0.6617503166 0.7765241265 - 0.7508131862 0.6628534198 0.7774782777 - 0.7516831756 0.6639522910 0.7784324884 - 0.7525532246 0.6650412083 0.7793866992 - 0.7534232140 0.6661300063 0.7803409100 - 0.7542930841 0.6672189236 0.7812796831 - 0.7551512122 0.6683077216 0.7822145224 - 0.7560092807 0.6693845987 0.7831493020 - 0.7568672895 0.6704590917 0.7840840816 - 0.7577254176 0.6715335846 0.7850121260 - 0.7585778236 0.6726080775 0.7859280109 - 0.7594259977 0.6736781001 0.7868438959 - 0.7602741718 0.6747391224 0.7877597809 - 0.7611225247 0.6758000851 0.7886756063 - 0.7619696856 0.6768611073 0.7895761728 - 0.7628092766 0.6779221892 0.7904745936 - 0.7636489272 0.6789721251 0.7913730145 - 0.7644885182 0.6800191998 0.7922713757 - 0.7653279901 0.6810662746 0.7931637764 - 0.7661625743 0.6821134090 0.7940455079 - 0.7669938803 0.6831572056 0.7949272990 - 0.7678251266 0.6841893792 0.7958090901 - 0.7686563730 0.6852216721 0.7966908813 - 0.7694870234 0.6862540245 0.7975584865 - 0.7703074813 0.6872861981 0.7984229922 - 0.7711278796 0.6883111000 0.7992874980 - 0.7719483972 0.6893314123 0.8001520038 - 0.7727687955 0.6903517246 0.8010128736 - 0.7735849023 0.6913719773 0.8018617034 - 0.7743961811 0.6923922896 0.8027104735 - 0.7752075195 0.6934009790 0.8035593033 - 0.7760187984 0.6944093704 0.8044080734 - 0.7768300772 0.6954177022 0.8052453995 - 0.7776347995 0.6964259744 0.8060749173 - 0.7784383893 0.6974307895 0.8069043756 - 0.7792420983 0.6984261274 0.8077338934 - 0.7800456882 0.6994215250 0.8085634708 - 0.7808480263 0.7004168034 0.8093764186 - 0.7816452980 0.7014120817 0.8101881146 - 0.7824425101 0.7024009228 0.8109998107 - 0.7832397819 0.7033842802 0.8118115067 - 0.7840369940 0.7043675780 0.8126195073 - 0.7848305702 0.7053508759 0.8134146929 - 0.7856206894 0.7063342929 0.8142098188 - 0.7864106894 0.7073084712 0.8150050044 - 0.7872008085 0.7082800269 0.8158001900 - 0.7879908085 0.7092515826 0.8165881038 - 0.7887749076 0.7102231979 0.8173683882 - 0.7895570993 0.7111946940 0.8181486726 - 0.7903392911 0.7121539712 0.8189290166 - 0.7911216021 0.7131130099 0.8197093010 - 0.7919037938 0.7140719891 0.8204782009 - 0.7926781774 0.7150310278 0.8212423921 - 0.7934526205 0.7159885168 0.8220065832 - 0.7942268848 0.7169361115 0.8227707148 - 0.7950013280 0.7178837061 0.8235349059 - 0.7957745194 0.7188313007 0.8242856860 - 0.7965422273 0.7197788954 0.8250346184 - 0.7973099947 0.7207238078 0.8257834911 - 0.7980777025 0.7216603756 0.8265324831 - 0.7988454103 0.7225970030 0.8272811770 - 0.7996107936 0.7235335708 0.8280162215 - 0.8003711104 0.7244701982 0.8287512064 - 0.8011314869 0.7254031897 0.8294863105 - 0.8018918037 0.7263281941 0.8302212954 - 0.8026521206 0.7272531986 0.8309546113 - 0.8034093976 0.7281782031 0.8316739798 - 0.8041626811 0.7291032076 0.8323932886 - 0.8049159050 0.7300242186 0.8331127167 - 0.8056691885 0.7309377193 0.8338320851 - 0.8064224124 0.7318512201 0.8345488906 - 0.8071724772 0.7327647209 0.8352528811 - 0.8079192042 0.7336782217 0.8359568715 - 0.8086658716 0.7345883250 0.8366609216 - 0.8094125986 0.7354918122 0.8373649120 - 0.8101593256 0.7363952994 0.8380662799 - 0.8109018803 0.7372987270 0.8387560248 - 0.8116406798 0.7382022142 0.8394458294 - 0.8123794198 0.7391029000 0.8401355147 - 0.8131181002 0.7399963737 0.8408252001 - 0.8138568997 0.7408899069 0.8415125012 - 0.8145921826 0.7417833805 0.8421863914 - 0.8153241277 0.7426769137 0.8428602815 - 0.8160558939 0.7435685992 0.8435341716 - 0.8167877793 0.7444524765 0.8442081809 - 0.8175196052 0.7453362942 0.8448808789 - 0.8182482719 0.7462201118 0.8455417156 - 0.8189728260 0.7471039295 0.8462024927 - 0.8196973205 0.7479872704 0.8468633294 - 0.8204218745 0.7488632202 0.8475239873 - 0.8211464286 0.7497389913 0.8481848240 - 0.8218685985 0.7506148219 0.8488330841 - 0.8225861192 0.7514907122 0.8494811058 - 0.8233035207 0.7523664832 0.8501291275 - 0.8240209222 0.7532342076 0.8507772088 - 0.8247383833 0.7541006804 0.8514251709 - 0.8254544139 0.7549670935 0.8520622849 - 0.8261637092 0.7558336258 0.8526970744 - 0.8268730044 0.7567000985 0.8533319831 - 0.8275822997 0.7575604916 0.8539667726 - 0.8282917142 0.7584174871 0.8546016216 - 0.8290010095 0.7592746019 0.8552286029 - 0.8297007084 0.7601317167 0.8558509946 - 0.8304002881 0.7609887719 0.8564733267 - 0.8310999274 0.7618423104 0.8570957184 - 0.8317995071 0.7626898289 0.8577179909 - 0.8324990869 0.7635374069 0.8583357930 - 0.8331922293 0.7643849850 0.8589463234 - 0.8338832855 0.7652326226 0.8595567942 - 0.8345744014 0.7660794258 0.8601673245 - 0.8352655172 0.7669159770 0.8607777953 - 0.8359565735 0.7677527070 0.8613868952 - 0.8366435170 0.7685893774 0.8619840145 - 0.8373258710 0.7694259882 0.8625810146 - 0.8380082846 0.7702627182 0.8631780744 - 0.8386908174 0.7710928917 0.8637751937 - 0.8393731713 0.7719206214 0.8643721938 - 0.8400542736 0.7727481723 0.8649600148 - 0.8407285213 0.7735759020 0.8655449748 - 0.8414027095 0.7744035721 0.8661299944 - 0.8420767784 0.7752280831 0.8667150140 - 0.8427510262 0.7760459781 0.8672999740 - 0.8434252143 0.7768638730 0.8678801060 - 0.8440936804 0.7776817083 0.8684527278 - 0.8447608948 0.7784996033 0.8690252900 - 0.8454279900 0.7793174982 0.8695979118 - 0.8460950851 0.7801278830 0.8701705933 - 0.8467621803 0.7809370756 0.8707432151 - 0.8474255204 0.7817463875 0.8713021278 - 0.8480827212 0.7825555801 0.8718606830 - 0.8487399817 0.7833648920 0.8724194169 - 0.8493973017 0.7841700912 0.8729780912 - 0.8500545025 0.7849687934 0.8735368252 - 0.8507118225 0.7857673764 0.8740890026 - 0.8513603806 0.7865660787 0.8746353984 - 0.8520085216 0.7873647213 0.8751817942 - 0.8526566029 0.7881634235 0.8757281899 - 0.8533046842 0.7889544964 0.8762745857 - 0.8539528251 0.7897443175 0.8768208027 - 0.8545969129 0.7905340195 0.8773562908 - 0.8552358747 0.7913237810 0.8778917789 - 0.8558748960 0.7921136022 0.8784273267 - 0.8565139174 0.7929015756 0.8789628148 - 0.8571529984 0.7936850786 0.8794983029 - 0.8577920198 0.7944685817 0.8800281882 - 0.8584234118 0.7952520847 0.8805518150 - 0.8590539098 0.7960355878 0.8810753822 - 0.8596845269 0.7968190908 0.8815988898 - 0.8603150845 0.7975977063 0.8821225166 - 0.8609455824 0.7983735204 0.8826460838 - 0.8615739942 0.7991493940 0.8831595778 - 0.8621975183 0.7999252081 0.8836715221 - 0.8628209829 0.8007010818 0.8841834068 - 0.8634445071 0.8014768958 0.8846952915 - 0.8640679121 0.8022441268 0.8852071762 - 0.8646913767 0.8030110002 0.8857167959 - 0.8653084040 0.8037779927 0.8862178922 - 0.8659222722 0.8045449257 0.8867189884 - 0.8665362000 0.8053117990 0.8872202039 - 0.8671500087 0.8060764074 0.8877213001 - 0.8677638769 0.8068339229 0.8882223964 - 0.8683776855 0.8075914979 0.8887180090 - 0.8689824939 0.8083490133 0.8892086744 - 0.8695871234 0.8091065288 0.8896992803 - 0.8701918125 0.8098639846 0.8901900053 - 0.8707963824 0.8106179833 0.8906806111 - 0.8714010715 0.8113679886 0.8911712766 - 0.8720031977 0.8121181130 0.8916537166 - 0.8725978732 0.8128681183 0.8921337724 - 0.8731927276 0.8136181831 0.8926138878 - 0.8737874031 0.8143681884 0.8930941224 - 0.8743820786 0.8151124716 0.8935741782 - 0.8749768734 0.8158538938 0.8940542936 - 0.8755673170 0.8165953159 0.8945251107 - 0.8761531115 0.8173366785 0.8949958086 - 0.8767387867 0.8180781007 0.8954666257 - 0.8773245811 0.8188195229 0.8959373236 - 0.8779103756 0.8195531964 0.8964080215 - 0.8784961104 0.8202847838 0.8968774080 - 0.8790776134 0.8210164905 0.8973398209 - 0.8796569109 0.8217481971 0.8978022933 - 0.8802360892 0.8224797845 0.8982647061 - 0.8808153272 0.8232114911 0.8987271786 - 0.8813946247 0.8239343166 0.8991895914 - 0.8819738030 0.8246560097 0.8996490836 - 0.8825464845 0.8253775835 0.9001011252 - 0.8831173182 0.8260992765 0.9005531073 - 0.8836882114 0.8268209100 0.9010050893 - 0.8842589855 0.8275426030 0.9014570713 - 0.8848298192 0.8282570243 0.9019091725 - 0.8854007125 0.8289706707 0.9023581147 - 0.8859637976 0.8296844959 0.9028012753 - 0.8865255713 0.8303982019 0.9032443762 - 0.8870874047 0.8311120272 0.9036875963 - 0.8876491785 0.8318256736 0.9041308165 - 0.8882110119 0.8325313926 0.9045739174 - 0.8887729049 0.8332362771 0.9050139785 - 0.8893269897 0.8339412212 0.9054486752 - 0.8898801208 0.8346461058 0.9058833718 - 0.8904331923 0.8353509903 0.9063181281 - 0.8909863234 0.8360558152 0.9067528248 - 0.8915393949 0.8367524147 0.9071875215 - 0.8920925260 0.8374475837 0.9076195955 - 0.8926386237 0.8381426930 0.9080467224 - 0.8931834102 0.8388378024 0.9084736705 - 0.8937283158 0.8395329118 0.9089006782 - 0.8942731023 0.8402280211 0.9093278050 - 0.8948178887 0.8409171104 0.9097548127 - 0.8953627944 0.8416041136 0.9101794958 - 0.8958991170 0.8422911167 0.9105970263 - 0.8964328766 0.8429781199 0.9110143781 - 0.8969668150 0.8436651230 0.9114319086 - 0.8975006938 0.8443520069 0.9118493795 - 0.8980345130 0.8450348973 0.9122667909 - 0.8985683918 0.8457146883 0.9126836061 - 0.8990967870 0.8463944197 0.9130951166 - 0.8996223211 0.8470742106 0.9135065079 - 0.9001477957 0.8477538824 0.9139180183 - 0.9006732702 0.8484336734 0.9143294096 - 0.9011988044 0.8491110206 0.9147409201 - 0.9017242789 0.8497840166 0.9151523113 - 0.9022449851 0.8504568934 0.9155570269 - 0.9027605057 0.8511298895 0.9159610868 - 0.9032760859 0.8518028259 0.9163653255 - 0.9037916064 0.8524758220 0.9167693853 - 0.9043071270 0.8531479836 0.9171736240 - 0.9048225880 0.8538120985 0.9175776839 - 0.9053356051 0.8544760942 0.9179764986 - 0.9058414102 0.8551402092 0.9183728099 - 0.9063470960 0.8558042049 0.9187691808 - 0.9068529010 0.8564683199 0.9191656113 - 0.9073585868 0.8571323156 0.9195619226 - 0.9078643918 0.8577905893 0.9199582934 - 0.9083700776 0.8584470749 0.9203512073 - 0.9088681936 0.8591036201 0.9207388759 - 0.9093660712 0.8597599864 0.9211267233 - 0.9098640084 0.8604164720 0.9215145111 - 0.9103618860 0.8610730171 0.9219022989 - 0.9108597040 0.8617268205 0.9222900271 - 0.9113575816 0.8623763919 0.9226773977 - 0.9118500948 0.8630260229 0.9230594039 - 0.9123396277 0.8636757135 0.9234412909 - 0.9128291011 0.8643252850 0.9238231778 - 0.9133185744 0.8649749160 0.9242051840 - 0.9138082266 0.8656246066 0.9245870709 - 0.9142976999 0.8662685156 0.9249690771 - 0.9147847295 0.8669123054 0.9253469110 - 0.9152647257 0.8675560951 0.9257227182 - 0.9157447815 0.8681998849 0.9260985255 - 0.9162247777 0.8688436747 0.9264742732 - 0.9167048931 0.8694875240 0.9268500805 - 0.9171848893 0.8701276183 0.9272258282 - 0.9176650047 0.8707640171 0.9275999069 - 0.9181361198 0.8714004159 0.9279679060 - 0.9186053276 0.8720368147 0.9283359051 - 0.9190744758 0.8726732135 0.9287039042 - 0.9195436835 0.8733096123 0.9290717840 - 0.9200128913 0.8739460111 0.9294397831 - 0.9204820991 0.8745772839 0.9298077822 - 0.9209483862 0.8752083778 0.9301705956 - 0.9214088917 0.8758394122 0.9305313826 - 0.9218693972 0.8764705062 0.9308922291 - 0.9223300219 0.8771016002 0.9312530160 - 0.9227905273 0.8777326941 0.9316139221 - 0.9232509732 0.8783612251 0.9319747090 - 0.9237114787 0.8789854050 0.9323344231 - 0.9241648912 0.8796095848 0.9326891899 - 0.9246163964 0.8802338243 0.9330438972 - 0.9250679016 0.8808580041 0.9333986044 - 0.9255194068 0.8814823031 0.9337533712 - 0.9259709120 0.8821064830 0.9341080785 - 0.9264224172 0.8827261925 0.9344627857 - 0.9268720150 0.8833442926 0.9348145723 - 0.9273136854 0.8839623928 0.9351639152 - 0.9277552962 0.8845806122 0.9355133176 - 0.9281969070 0.8851987123 0.9358627200 - 0.9286385775 0.8858168125 0.9362121224 - 0.9290801883 0.8864346743 0.9365615249 - 0.9295219183 0.8870465755 0.9369109273 - 0.9299585223 0.8876584768 0.9372549057 - 0.9303908944 0.8882703781 0.9375984073 - 0.9308233261 0.8888823986 0.9379419088 - 0.9312556982 0.8894942999 0.9382852912 - 0.9316880703 0.8901062012 0.9386287928 - 0.9321205020 0.8907166123 0.9389722943 - 0.9325528741 0.8913230896 0.9393147826 - 0.9329783916 0.8919296265 0.9396532178 - 0.9334024787 0.8925361037 0.9399914742 - 0.9338266850 0.8931425214 0.9403297901 - 0.9342507720 0.8937489986 0.9406682253 - 0.9346749187 0.8943554759 0.9410064816 - 0.9350991249 0.8949587941 0.9413447976 - 0.9355226159 0.8955584764 0.9416810870 - 0.9359377027 0.8961580992 0.9420145154 - 0.9363527894 0.8967577815 0.9423478842 - 0.9367679954 0.8973575234 0.9426813126 - 0.9371830821 0.8979572058 0.9430146813 - 0.9375982285 0.8985568881 0.9433481097 - 0.9380133748 0.8991538286 0.9436814785 - 0.9384260774 0.8997489214 0.9440121055 - 0.9388316274 0.9003438950 0.9443405271 - 0.9392371178 0.9009389877 0.9446688890 - 0.9396426082 0.9015340805 0.9449973106 - 0.9400480986 0.9021291733 0.9453257918 - 0.9404535890 0.9027243257 0.9456542134 - 0.9408590794 0.9033153057 0.9459825754 - 0.9412612915 0.9039043784 0.9463074803 - 0.9416580200 0.9044935107 0.9466307759 - 0.9420546293 0.9050825834 0.9469540715 - 0.9424512982 0.9056717157 0.9472774267 - 0.9428480268 0.9062607884 0.9476007223 - 0.9432446957 0.9068499207 0.9479240179 - 0.9436413050 0.9074342251 0.9482473135 - 0.9440346956 0.9080166221 0.9485682845 - 0.9444236755 0.9085990191 0.9488884807 - 0.9448127151 0.9091812968 0.9492086768 - 0.9452018142 0.9097636938 0.9495288730 - 0.9455907941 0.9103459716 0.9498491287 - 0.9459797740 0.9109284282 0.9501693249 - 0.9463688135 0.9115049839 0.9504894018 - 0.9467542171 0.9120786190 0.9508066773 - 0.9471350908 0.9126523137 0.9511228800 - 0.9475159049 0.9132260084 0.9514390826 - 0.9478967786 0.9137995839 0.9517552853 - 0.9482775927 0.9143732786 0.9520714879 - 0.9486585259 0.9149469733 0.9523876905 - 0.9490392804 0.9155157208 0.9527038932 - 0.9494168162 0.9160807729 0.9530172944 - 0.9497889876 0.9166458845 0.9533293247 - 0.9501612782 0.9172109962 0.9536414146 - 0.9505335093 0.9177759886 0.9539533854 - 0.9509056807 0.9183411002 0.9542654753 - 0.9512779117 0.9189062119 0.9545776248 - 0.9516502023 0.9194672704 0.9548895955 - 0.9520196915 0.9200227857 0.9551991820 - 0.9523826241 0.9205783010 0.9555068016 - 0.9527454972 0.9211338162 0.9558144808 - 0.9531083703 0.9216892719 0.9561221004 - 0.9534713030 0.9222447872 0.9564297199 - 0.9538341761 0.9228003025 0.9567372799 - 0.9541971087 0.9233545065 0.9570448995 - 0.9545590878 0.9239031076 0.9573509097 - 0.9549140930 0.9244517088 0.9576539993 - 0.9552692175 0.9250003099 0.9579570889 - 0.9556242228 0.9255489111 0.9582601786 - 0.9559792876 0.9260975122 0.9585633278 - 0.9563344121 0.9266461134 0.9588664174 - 0.9566894174 0.9271947145 0.9591695070 - 0.9570444822 0.9277390838 0.9594715834 - 0.9573910236 0.9282829762 0.9597675800 - 0.9577363729 0.9288269877 0.9600635767 - 0.9580819011 0.9293709993 0.9603595734 - 0.9584273100 0.9299150109 0.9606556296 - 0.9587727785 0.9304590225 0.9609516859 - 0.9591181874 0.9310029745 0.9612476826 - 0.9594637156 0.9315435290 0.9615436792 - 0.9598045945 0.9320815802 0.9618365765 - 0.9601426125 0.9326195717 0.9621289968 - 0.9604806900 0.9331576824 0.9624214172 - 0.9608187079 0.9336956739 0.9627137780 - 0.9611567259 0.9342337847 0.9630060792 - 0.9614946842 0.9347717762 0.9632984996 - 0.9618327022 0.9353088737 0.9635909200 - 0.9621691108 0.9358416200 0.9638820887 - 0.9625008702 0.9363743067 0.9641720057 - 0.9628326893 0.9369069934 0.9644619226 - 0.9631645083 0.9374396801 0.9647517800 - 0.9634963274 0.9379724264 0.9650416970 - 0.9638280272 0.9385051131 0.9653316140 - 0.9641597867 0.9390376806 0.9656215906 - 0.9644916058 0.9395660162 0.9659113288 - 0.9648177028 0.9400926828 0.9661973715 - 0.9651427865 0.9406192899 0.9664834738 - 0.9654678702 0.9411458969 0.9667695761 - 0.9657930136 0.9416726232 0.9670556784 - 0.9661180973 0.9421991706 0.9673417807 - 0.9664431810 0.9427258968 0.9676278830 - 0.9667683244 0.9432511926 0.9679139853 - 0.9670909047 0.9437717795 0.9681978226 - 0.9674093723 0.9442923069 0.9684798121 - 0.9677278996 0.9448127747 0.9687616825 - 0.9680464268 0.9453333020 0.9690436721 - 0.9683648944 0.9458538294 0.9693257213 - 0.9686834216 0.9463744164 0.9696077108 - 0.9690018892 0.9468948841 0.9698897004 - 0.9693204165 0.9474115968 0.9701715708 - 0.9696332216 0.9479261041 0.9704506993 - 0.9699448943 0.9484406114 0.9707298279 - 0.9702566266 0.9489551187 0.9710088968 - 0.9705682993 0.9494696259 0.9712880254 - 0.9708799720 0.9499840140 0.9715670943 - 0.9711917043 0.9504985213 0.9718461037 - 0.9715033770 0.9510130286 0.9721251726 - 0.9718133807 0.9515205026 0.9724026918 - 0.9721180201 0.9520277977 0.9726777077 - 0.9724227190 0.9525352120 0.9729527831 - 0.9727274179 0.9530425072 0.9732279181 - 0.9730321169 0.9535499215 0.9735029936 - 0.9733366966 0.9540572166 0.9737780094 - 0.9736413956 0.9545645118 0.9740530849 - 0.9739460945 0.9550700188 0.9743282199 - 0.9742460847 0.9555702209 0.9745998979 - 0.9745427966 0.9560704231 0.9748703837 - 0.9748393893 0.9565706849 0.9751409292 - 0.9751361012 0.9570708871 0.9754114747 - 0.9754328132 0.9575710893 0.9756820202 - 0.9757294059 0.9580712914 0.9759525061 - 0.9760261178 0.9585716128 0.9762231112 - 0.9763227105 0.9590680003 0.9764935970 - 0.9766126275 0.9595606923 0.9767600298 - 0.9769014120 0.9600533843 0.9770264030 - 0.9771901965 0.9605460167 0.9772928953 - 0.9774789214 0.9610387087 0.9775593281 - 0.9777677059 0.9615312815 0.9778257012 - 0.9780563712 0.9620239735 0.9780920744 - 0.9783452153 0.9625167251 0.9783586264 - 0.9786334038 0.9630044103 0.9786242247 - 0.9789144993 0.9634895921 0.9788866043 - 0.9791955948 0.9639747143 0.9791489840 - 0.9794766903 0.9644598961 0.9794114232 - 0.9797577858 0.9649450779 0.9796738029 - 0.9800388813 0.9654303193 0.9799361825 - 0.9803199768 0.9659153819 0.9801986217 - 0.9806010723 0.9664006233 0.9804610014 - 0.9808804989 0.9668791294 0.9807218909 - 0.9811537862 0.9673554897 0.9809799790 - 0.9814270735 0.9678319097 0.9812381268 - 0.9817004204 0.9683082104 0.9814962745 - 0.9819737077 0.9687845707 0.9817544222 - 0.9822469950 0.9692608714 0.9820125103 - 0.9825202823 0.9697372913 0.9822705984 - 0.9827936292 0.9702135921 0.9825286865 - 0.9830644727 0.9706823230 0.9827851057 - 0.9833294749 0.9711489081 0.9830390811 - 0.9835945964 0.9716154933 0.9832929969 - 0.9838597178 0.9720820785 0.9835469723 - 0.9841247201 0.9725487232 0.9838010073 - 0.9843897820 0.9730151892 0.9840549231 - 0.9846549034 0.9734817743 0.9843088984 - 0.9849199057 0.9739484191 0.9845628142 - 0.9851824045 0.9744058847 0.9848148823 - 0.9854390025 0.9748603106 0.9850642085 - 0.9856956005 0.9753146768 0.9853134751 - 0.9859521985 0.9757689834 0.9855628014 - 0.9862087965 0.9762234092 0.9858121276 - 0.9864653945 0.9766777754 0.9860615134 - 0.9867219925 0.9771322012 0.9863107800 - 0.9869785905 0.9775866270 0.9865601063 - 0.9872329831 0.9780325890 0.9868078232 - 0.9874812961 0.9784740806 0.9870523810 - 0.9877294898 0.9789155126 0.9872971177 - 0.9879776835 0.9793568850 0.9875416756 - 0.9882258773 0.9797983170 0.9877864122 - 0.9884740710 0.9802396894 0.9880310297 - 0.9887223244 0.9806811213 0.9882757068 - 0.9889705777 0.9811224937 0.9885203242 - 0.9892175198 0.9815559983 0.9887641072 - 0.9894570708 0.9819805026 0.9890046716 - 0.9896966815 0.9824051261 0.9892451763 - 0.9899362922 0.9828295708 0.9894858003 - 0.9901759028 0.9832541943 0.9897264242 - 0.9904153943 0.9836786985 0.9899669886 - 0.9906550050 0.9841033220 0.9902076125 - 0.9908946157 0.9845278263 0.9904481769 - 0.9911342263 0.9849482775 0.9906886220 - 0.9913660884 0.9853556752 0.9909231067 - 0.9915975928 0.9857630134 0.9911575913 - 0.9918292165 0.9861702919 0.9913920760 - 0.9920607805 0.9865776896 0.9916266203 - 0.9922922850 0.9869850278 0.9918611050 - 0.9925239086 0.9873923063 0.9920955896 - 0.9927554727 0.9877997041 0.9923300743 - 0.9929869771 0.9882069826 0.9925646186 - 0.9932109714 0.9885931015 0.9927943945 - 0.9934319258 0.9889774919 0.9930227995 - 0.9936528206 0.9893618226 0.9932510853 - 0.9938737154 0.9897462130 0.9934794903 - 0.9940946102 0.9901306033 0.9937078953 - 0.9943153858 0.9905149937 0.9939361811 - 0.9945362806 0.9908993840 0.9941645861 - 0.9947571754 0.9912837744 0.9943928719 - 0.9949743152 0.9916560054 0.9946191907 - 0.9951854944 0.9920184016 0.9948425293 - 0.9953966737 0.9923807979 0.9950658083 - 0.9956079125 0.9927430749 0.9952890873 - 0.9958190918 0.9931054711 0.9955124259 - 0.9960302711 0.9934678078 0.9957357049 - 0.9962415099 0.9938302040 0.9959589839 - 0.9964526892 0.9941924810 0.9961823225 - 0.9966639280 0.9945517778 0.9964054823 - 0.9968671203 0.9948847890 0.9966220856 - 0.9970703125 0.9952176809 0.9968386292 - 0.9972733855 0.9955506921 0.9970551729 - 0.9974765778 0.9958835840 0.9972717166 - 0.9976797104 0.9962165952 0.9974883199 - 0.9978829026 0.9965494871 0.9977048039 - 0.9980860949 0.9968824983 0.9979214072 - 0.9982892275 0.9972153902 0.9981380105 - 0.9984850287 0.9975339174 0.9983490109 - 0.9986743927 0.9978420734 0.9985553026 - 0.9988638163 0.9981504083 0.9987617135 - 0.9990531802 0.9984586835 0.9989681244 - 0.9992424846 0.9987668991 0.9991744757 - 0.9994319081 0.9990751743 0.9993808866 - 0.9996212721 0.9993835092 0.9995871782 - 0.9998105764 0.9996917248 0.9997935891 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0002699328 0.0001753761 0.0002523783 + 0.0005516693 0.0003570581 0.0005202319 + 0.0008440902 0.0005450998 0.0008039851 + 0.0011470990 0.0007395549 0.0011026140 + 0.0014597330 0.0009404911 0.0014156949 + 0.0017813731 0.0011476850 0.0017421190 + 0.0021126880 0.0013612730 0.0020805011 + 0.0024531030 0.0015815590 0.0024294229 + 0.0028031650 0.0018088090 0.0027884119 + 0.0031638411 0.0020427699 0.0031579121 + 0.0035372649 0.0022836919 0.0035393820 + 0.0039257319 0.0025316810 0.0039346260 + 0.0043312181 0.0027867940 0.0043465500 + 0.0047563538 0.0030489860 0.0047766608 + 0.0052017048 0.0033181130 0.0052262088 + 0.0056667780 0.0035944311 0.0056956508 + 0.0061506722 0.0038782239 0.0061842999 + 0.0066511780 0.0041694939 0.0066920272 + 0.0071674078 0.0044688610 0.0072181849 + 0.0076976968 0.0047760759 0.0077622221 + 0.0082411859 0.0050910199 0.0083242692 + 0.0087973205 0.0054136459 0.0089058857 + 0.0093660019 0.0057440512 0.0095065739 + 0.0099473577 0.0060824021 0.0101279598 + 0.0105416896 0.0064286222 0.0107696000 + 0.0111480197 0.0067824679 0.0114314295 + 0.0117668100 0.0071440842 0.0121141002 + 0.0123977298 0.0075133010 0.0128158899 + 0.0130404802 0.0078901956 0.0135375699 + 0.0136951199 0.0082744109 0.0142785497 + 0.0143617000 0.0086660404 0.0150373597 + 0.0150401201 0.0090650087 0.0158140305 + 0.0157300606 0.0094716744 0.0166073292 + 0.0164318401 0.0098855980 0.0174160805 + 0.0171449091 0.0103072198 0.0182415899 + 0.0178689398 0.0107360203 0.0190821793 + 0.0186050907 0.0111725898 0.0199379697 + 0.0193527602 0.0116162105 0.0208086297 + 0.0201126505 0.0120675396 0.0216945801 + 0.0208839308 0.0125266099 0.0225958191 + 0.0216670800 0.0129928002 0.0235118493 + 0.0224615298 0.0134667596 0.0244435798 + 0.0232677106 0.0139479404 0.0253902208 + 0.0240852199 0.0144363800 0.0263507906 + 0.0249137003 0.0149327302 0.0273268502 + 0.0257528108 0.0154370395 0.0283182394 + 0.0266024694 0.0159486309 0.0293240100 + 0.0274628494 0.0164675806 0.0303431805 + 0.0283344202 0.0169943292 0.0313761793 + 0.0292166006 0.0175288208 0.0324220508 + 0.0301091801 0.0180700701 0.0334794186 + 0.0310120601 0.0186186992 0.0345463790 + 0.0319244787 0.0191750694 0.0356225707 + 0.0328475386 0.0197390206 0.0367073193 + 0.0337808393 0.0203102101 0.0377987213 + 0.0347246900 0.0208885409 0.0388945192 + 0.0356788710 0.0214745607 0.0399941206 + 0.0366426483 0.0220674798 0.0410979390 + 0.0376164615 0.0226682406 0.0422052182 + 0.0386001393 0.0232766997 0.0433141701 + 0.0395945087 0.0238917898 0.0444252789 + 0.0405988395 0.0245153997 0.0455389805 + 0.0416126810 0.0251462702 0.0466558412 + 0.0426358394 0.0257849507 0.0477751605 + 0.0436693914 0.0264302809 0.0488959402 + 0.0447117910 0.0270844605 0.0500185490 + 0.0457648709 0.0277455002 0.0511438698 + 0.0468259901 0.0284139998 0.0522733182 + 0.0478970297 0.0290895496 0.0534073599 + 0.0489763990 0.0297721792 0.0545452908 + 0.0500655286 0.0304632597 0.0556870997 + 0.0511628389 0.0311617199 0.0568346418 + 0.0522699915 0.0318679288 0.0579884015 + 0.0533850715 0.0325823203 0.0591484793 + 0.0545096211 0.0333043113 0.0603141114 + 0.0556424297 0.0340342708 0.0614848994 + 0.0567837097 0.0347718298 0.0626629367 + 0.0579337105 0.0355160087 0.0638478696 + 0.0590906888 0.0362669006 0.0650385097 + 0.0602574088 0.0370258093 0.0662359372 + 0.0614334010 0.0377927385 0.0674390867 + 0.0626170933 0.0385673605 0.0686485395 + 0.0638106465 0.0393497609 0.0698641911 + 0.0650118366 0.0401403792 0.0710861981 + 0.0662213415 0.0409397595 0.0723134726 + 0.0674379617 0.0417471118 0.0735469386 + 0.0686631873 0.0425618887 0.0747859776 + 0.0698976368 0.0433834791 0.0760307387 + 0.0711404011 0.0442118682 0.0772804022 + 0.0723914728 0.0450474396 0.0785348937 + 0.0736518502 0.0458911397 0.0797948688 + 0.0749201477 0.0467427112 0.0810601711 + 0.0761969313 0.0476017408 0.0823324025 + 0.0774813071 0.0484685600 0.0836096331 + 0.0787731186 0.0493434109 0.0848924294 + 0.0800740421 0.0502262115 0.0861801729 + 0.0813831016 0.0511168912 0.0874729678 + 0.0827006921 0.0520157702 0.0887702629 + 0.0840270668 0.0529231317 0.0900717899 + 0.0853607282 0.0538391396 0.0913778096 + 0.0867023394 0.0547635406 0.0926876292 + 0.0880530998 0.0556963496 0.0940016583 + 0.0894121006 0.0566373095 0.0953195170 + 0.0907792076 0.0575862303 0.0966407433 + 0.0921551064 0.0585427284 0.0979649574 + 0.0935385600 0.0595068783 0.0992923602 + 0.0949299335 0.0604788885 0.1006235033 + 0.0963296816 0.0614589490 0.1019582972 + 0.0977364033 0.0624472797 0.1032961980 + 0.0991507098 0.0634440780 0.1046369970 + 0.1005733013 0.0644495711 0.1059795022 + 0.1020032987 0.0654638633 0.1073246971 + 0.1034407988 0.0664867088 0.1086725965 + 0.1048860997 0.0675178468 0.1100225002 + 0.1063387021 0.0685571134 0.1113744006 + 0.1077983007 0.0696041286 0.1127282009 + 0.1092654988 0.0706589893 0.1140839010 + 0.1107404977 0.0717224628 0.1154410988 + 0.1122227013 0.0727944970 0.1168001965 + 0.1137126014 0.0738748014 0.1181621030 + 0.1152106971 0.0749634504 0.1195268035 + 0.1167161986 0.0760605484 0.1208942011 + 0.1182283014 0.0771661699 0.1222644001 + 0.1197486967 0.0782803968 0.1236374006 + 0.1212769002 0.0794033334 0.1250136942 + 0.1228116006 0.0805374980 0.1263937950 + 0.1243539006 0.0816816911 0.1277772933 + 0.1259036064 0.0828360692 0.1291648000 + 0.1274601966 0.0840003714 0.1305564940 + 0.1290239990 0.0851741210 0.1319530010 + 0.1305951029 0.0863568485 0.1333540976 + 0.1321730018 0.0875512436 0.1347596943 + 0.1337587982 0.0887546614 0.1361702979 + 0.1353518963 0.0899670422 0.1375872940 + 0.1369515061 0.0911882967 0.1390096992 + 0.1385575980 0.0924183726 0.1404386014 + 0.1401696056 0.0936605185 0.1418738067 + 0.1417876929 0.0949114934 0.1433147937 + 0.1434136033 0.0961711928 0.1447637975 + 0.1450462937 0.0974395201 0.1462194026 + 0.1466850936 0.0987184718 0.1476815939 + 0.1483296007 0.1000078022 0.1491505057 + 0.1499803066 0.1013054997 0.1506260037 + 0.1516395956 0.1026116982 0.1521109045 + 0.1533043981 0.1039282009 0.1536028981 + 0.1549746990 0.1052552983 0.1551018059 + 0.1566507071 0.1065905988 0.1566075981 + 0.1583354026 0.1079339013 0.1581221074 + 0.1600269973 0.1092884988 0.1596440971 + 0.1617241949 0.1106527001 0.1611728072 + 0.1634269953 0.1120247021 0.1627078950 + 0.1651381999 0.1134048030 0.1642509997 + 0.1668564975 0.1147963032 0.1658020020 + 0.1685802042 0.1161947995 0.1673590988 + 0.1703090966 0.1176000983 0.1689217985 + 0.1720459014 0.1190157980 0.1704941988 + 0.1737886965 0.1204395965 0.1720733941 + 0.1755366027 0.1218703017 0.1736585945 + 0.1772904992 0.1233104020 0.1752517074 + 0.1790520996 0.1247609034 0.1768528074 + 0.1808187962 0.1262181997 0.1784588993 + 0.1825904995 0.1276842058 0.1800702065 + 0.1843689978 0.1291611046 0.1816917062 + 0.1861528009 0.1306443959 0.1833177954 + 0.1879408062 0.1321358979 0.1849482954 + 0.1897339970 0.1336382031 0.1865873039 + 0.1915318966 0.1351462007 0.1882306933 + 0.1933335066 0.1366620958 0.1898781061 + 0.1951407045 0.1381859928 0.1915331036 + 0.1969527006 0.1397145987 0.1931923032 + 0.1987676024 0.1412506998 0.1948550940 + 0.2005867958 0.1427935958 0.1965250969 + 0.2024096996 0.1443406940 0.1981987059 + 0.2042354047 0.1458964050 0.1998753995 + 0.2060655951 0.1474573016 0.2015592009 + 0.2078990936 0.1490225941 0.2032458931 + 0.2097346038 0.1505970955 0.2049356997 + 0.2115736008 0.1521753073 0.2066316009 + 0.2134155929 0.1537598073 0.2083299011 + 0.2152598053 0.1553504020 0.2100317031 + 0.2171059996 0.1569440067 0.2117380053 + 0.2189532965 0.1585451961 0.2134467959 + 0.2208018005 0.1601499021 0.2151606977 + 0.2226525992 0.1617594957 0.2168776989 + 0.2245036960 0.1633763015 0.2185973972 + 0.2263543010 0.1649962068 0.2203236967 + 0.2282046974 0.1666252017 0.2220526040 + 0.2300550044 0.1682582051 0.2237869948 + 0.2319052964 0.1698971987 0.2255263031 + 0.2337547988 0.1715423018 0.2272689044 + 0.2356023043 0.1731901020 0.2290178984 + 0.2374480069 0.1748462021 0.2307692021 + 0.2392929047 0.1765045971 0.2325260937 + 0.2411359996 0.1781698018 0.2342863977 + 0.2429780960 0.1798388958 0.2360495031 + 0.2448175997 0.1815121025 0.2378188968 + 0.2466558963 0.1831911057 0.2395901978 + 0.2484931946 0.1848724037 0.2413679063 + 0.2503274083 0.1865603030 0.2431481034 + 0.2521604002 0.1882493943 0.2449326068 + 0.2539891005 0.1899435967 0.2467211932 + 0.2558167875 0.1916399002 0.2485122979 + 0.2576422989 0.1933400929 0.2503088117 + 0.2594659925 0.1950432062 0.2521066070 + 0.2612882853 0.1967488974 0.2539105117 + 0.2631081939 0.1984582990 0.2557164133 + 0.2649275064 0.2001695931 0.2575275898 + 0.2667450905 0.2018844932 0.2593415976 + 0.2685621977 0.2036001980 0.2611587942 + 0.2703767121 0.2053194046 0.2629804015 + 0.2721900046 0.2070389986 0.2648043931 + 0.2740012109 0.2087621987 0.2666332126 + 0.2758114934 0.2104858011 0.2684633136 + 0.2776210010 0.2122109979 0.2703008056 + 0.2794296145 0.2139367014 0.2721393108 + 0.2812376916 0.2156637013 0.2739846110 + 0.2830449939 0.2173916996 0.2758311927 + 0.2848519981 0.2191213071 0.2776845992 + 0.2866581082 0.2208511978 0.2795400918 + 0.2884640992 0.2225816995 0.2814018130 + 0.2902691066 0.2243124992 0.2832661867 + 0.2920739949 0.2260437012 0.2851360142 + 0.2938778102 0.2277746946 0.2870089114 + 0.2956816852 0.2295051962 0.2888864875 + 0.2974855900 0.2312353998 0.2907676995 + 0.2992897928 0.2329653054 0.2926534116 + 0.3010956943 0.2346948981 0.2945432067 + 0.3029015958 0.2364241928 0.2964377999 + 0.3047079146 0.2381531000 0.2983360887 + 0.3065148890 0.2398813963 0.3002381921 + 0.3083240986 0.2416097969 0.3021444976 + 0.3101342022 0.2433384061 0.3040550947 + 0.3119468987 0.2450670004 0.3059692979 + 0.3137601912 0.2467954010 0.3078877032 + 0.3155752122 0.2485236973 0.3098101914 + 0.3173910081 0.2502520084 0.3117381930 + 0.3192102015 0.2519801855 0.3136689961 + 0.3210298121 0.2537078857 0.3156045079 + 0.3228524923 0.2554357052 0.3175427020 + 0.3246755004 0.2571632862 0.3194876015 + 0.3265027106 0.2588908970 0.3214339018 + 0.3283298910 0.2606168985 0.3233856857 + 0.3301618993 0.2623426914 0.3253385127 + 0.3319939077 0.2640664876 0.3272993863 + 0.3338311017 0.2657895982 0.3292602003 + 0.3356688917 0.2675099969 0.3312292099 + 0.3375113904 0.2692281902 0.3331983089 + 0.3393552005 0.2709451020 0.3351751864 + 0.3412027955 0.2726595998 0.3371534944 + 0.3430523872 0.2743734121 0.3391371071 + 0.3449051976 0.2760832906 0.3411231935 + 0.3467617035 0.2777931094 0.3431136012 + 0.3486200869 0.2795000076 0.3451083899 + 0.3504830897 0.2812066078 0.3471060097 + 0.3523468077 0.2829105854 0.3491097093 + 0.3542161882 0.2846131921 0.3511143029 + 0.3560855985 0.2863143981 0.3531267047 + 0.3579610884 0.2880134881 0.3551391065 + 0.3598372936 0.2897121012 0.3571586907 + 0.3617183864 0.2914075851 0.3591794968 + 0.3636018932 0.2931031883 0.3612056077 + 0.3654884994 0.2947953939 0.3632349968 + 0.3673790991 0.2964870930 0.3652678132 + 0.3692705929 0.2981764078 0.3673068881 + 0.3711664081 0.2998636067 0.3693464100 + 0.3730623126 0.3015499115 0.3713943064 + 0.3749629855 0.3032326996 0.3734421134 + 0.3768644929 0.3049154878 0.3754970133 + 0.3787690997 0.3065958023 0.3775540888 + 0.3806760013 0.3082754910 0.3796147108 + 0.3825837970 0.3099536002 0.3816798031 + 0.3844941854 0.3116297126 0.3837456107 + 0.3864046037 0.3133055866 0.3858197927 + 0.3883185983 0.3149785995 0.3878938854 + 0.3902328908 0.3166514933 0.3899737000 + 0.3921490908 0.3183214962 0.3920556009 + 0.3940671086 0.3199898005 0.3941405118 + 0.3959853053 0.3216570020 0.3962311149 + 0.3979054987 0.3233199120 0.3983218074 + 0.3998256922 0.3249827921 0.4004194140 + 0.4017480910 0.3266434073 0.4025174975 + 0.4036712050 0.3283031881 0.4046190977 + 0.4055953920 0.3299618959 0.4067240059 + 0.4075216949 0.3316178024 0.4088292122 + 0.4094479978 0.3332735896 0.4109373987 + 0.4113749862 0.3349258900 0.4130455852 + 0.4133020043 0.3365769982 0.4151561856 + 0.4152295887 0.3382270932 0.4172683954 + 0.4171578884 0.3398742974 0.4193809032 + 0.4190863073 0.3415214121 0.4214954972 + 0.4210152924 0.3431650996 0.4236100912 + 0.4229443073 0.3448075950 0.4257256985 + 0.4248732030 0.3464492857 0.4278419018 + 0.4268017113 0.3480867147 0.4299578965 + 0.4287301898 0.3497239947 0.4320721030 + 0.4306586087 0.3513588905 0.4341863096 + 0.4325869977 0.3529917896 0.4362998903 + 0.4345157146 0.3546248078 0.4384129047 + 0.4364447892 0.3562546968 0.4405260086 + 0.4383738935 0.3578845859 0.4426352084 + 0.4403015971 0.3595128059 0.4447444081 + 0.4422290921 0.3611381054 0.4468516111 + 0.4441559017 0.3627632856 0.4489555061 + 0.4460807145 0.3643858135 0.4510594904 + 0.4480054975 0.3660072088 0.4531568885 + 0.4499278963 0.3676283956 0.4552522898 + 0.4518488944 0.3692458868 0.4573468864 + 0.4537698925 0.3708634973 0.4594365954 + 0.4556883872 0.3724792004 0.4615261853 + 0.4576067924 0.3740915060 0.4636127055 + 0.4595240057 0.3757036924 0.4656955898 + 0.4614388943 0.3773143888 0.4677784145 + 0.4633539021 0.3789240122 0.4698548019 + 0.4652644098 0.3805336058 0.4719296098 + 0.4671730101 0.3821412027 0.4740034938 + 0.4690816104 0.3837484121 0.4760693014 + 0.4709844887 0.3853549957 0.4781351089 + 0.4728874862 0.3869574070 0.4801965058 + 0.4747886956 0.3885596991 0.4822503924 + 0.4766854942 0.3901604116 0.4843043983 + 0.4785822928 0.3917576969 0.4863505065 + 0.4804754853 0.3933550119 0.4883921146 + 0.4823656976 0.3949500024 0.4904336035 + 0.4842557907 0.3965430856 0.4924621880 + 0.4861414135 0.3981361091 0.4944886863 + 0.4880256951 0.3997269869 0.4965141118 + 0.4899098873 0.4013170898 0.4985252917 + 0.4917871952 0.4029071927 0.5005366206 + 0.4936642945 0.4044938982 0.5025439262 + 0.4955402911 0.4060800970 0.5045402050 + 0.4974110126 0.4076662064 0.5065364838 + 0.4992815852 0.4092473984 0.5085269213 + 0.5011492968 0.4108285904 0.5105090141 + 0.5030111074 0.4124091864 0.5124911070 + 0.5048729181 0.4139845073 0.5144640207 + 0.5067303777 0.4155597985 0.5164294839 + 0.5085831881 0.4171343148 0.5183951259 + 0.5104359984 0.4187046885 0.5203496218 + 0.5122830272 0.4202750921 0.5222988129 + 0.5141261816 0.4218446016 0.5242480040 + 0.5159693956 0.4234111905 0.5261831284 + 0.5178055763 0.4249776900 0.5281137824 + 0.5196387768 0.4265432060 0.5300444961 + 0.5214719176 0.4281054139 0.5319601893 + 0.5232948065 0.4296675026 0.5338729024 + 0.5251145959 0.4312283099 0.5357854962 + 0.5269343853 0.4327852130 0.5376809239 + 0.5287426114 0.4343419969 0.5395737290 + 0.5305480957 0.4358971119 0.5414664745 + 0.5323536992 0.4374468029 0.5433425903 + 0.5341475010 0.4389964044 0.5452167988 + 0.5359390974 0.4405452907 0.5470908880 + 0.5377306938 0.4420902133 0.5489476919 + 0.5395104885 0.4436351061 0.5508022904 + 0.5412878990 0.4451794922 0.5526568890 + 0.5430654287 0.4467185140 0.5544949174 + 0.5448313951 0.4482575059 0.5563300848 + 0.5465946794 0.4497964978 0.5581651926 + 0.5483580828 0.4513302147 0.5599843860 + 0.5501095057 0.4528639913 0.5617994070 + 0.5518571138 0.4543977976 0.5636144876 + 0.5536047220 0.4559254944 0.5654160976 + 0.5553417206 0.4574523866 0.5672122836 + 0.5570737123 0.4589793086 0.5690084100 + 0.5588057041 0.4605017900 0.5707932711 + 0.5605282187 0.4620226920 0.5725706816 + 0.5622438788 0.4635435939 0.5743482113 + 0.5639594793 0.4650601149 0.5761160254 + 0.5656672716 0.4665732086 0.5778731108 + 0.5673655272 0.4680863023 0.5796301961 + 0.5690636039 0.4695965946 0.5813813806 + 0.5707573295 0.4711019993 0.5831196904 + 0.5724400282 0.4726073146 0.5848580003 + 0.5741227269 0.4741115868 0.5865939260 + 0.5758035779 0.4756088853 0.5883123875 + 0.5774692297 0.4771062136 0.5900310278 + 0.5791348219 0.4786033928 0.5917496085 + 0.5808004737 0.4800952971 0.5934519768 + 0.5824471116 0.4815863073 0.5951527953 + 0.5840917230 0.4830772877 0.5968536139 + 0.5857362747 0.4845638871 0.5985416770 + 0.5873677135 0.4860475063 0.6002234817 + 0.5889924765 0.4875310957 0.6019052863 + 0.5906172991 0.4890125990 0.6035785079 + 0.5922335982 0.4904884994 0.6052387953 + 0.5938374996 0.4919644892 0.6068990827 + 0.5954412818 0.4934403896 0.6085569859 + 0.5970427990 0.4949091077 0.6101952791 + 0.5986260176 0.4963772893 0.6118335128 + 0.6002092957 0.4978454113 0.6134716868 + 0.6017925143 0.4993087053 0.6150943041 + 0.6033605933 0.5007681847 0.6167126298 + 0.6049249768 0.5022277236 0.6183308959 + 0.6064894199 0.5036858916 0.6199414134 + 0.6080433726 0.5051380992 0.6215417981 + 0.6095849872 0.5065904260 0.6231423020 + 0.6111266017 0.5080425739 0.6247416735 + 0.6126660705 0.5094881058 0.6263244748 + 0.6141831875 0.5109314919 0.6279072762 + 0.6157003045 0.5123748183 0.6294900775 + 0.6172174215 0.5138152242 0.6310620904 + 0.6187192798 0.5152488947 0.6326283813 + 0.6202139854 0.5166826248 0.6341947913 + 0.6217088103 0.5181162953 0.6357573271 + 0.6231973767 0.5195432901 0.6373063922 + 0.6246680021 0.5209690928 0.6388555169 + 0.6261385083 0.5223947763 0.6404047012 + 0.6276090741 0.5238174796 0.6419379711 + 0.6290594935 0.5252338052 0.6434664130 + 0.6305047870 0.5266500711 0.6449947953 + 0.6319500804 0.5280663967 0.6465178728 + 0.6333882213 0.5294749737 0.6480270028 + 0.6348105073 0.5308814049 0.6495360136 + 0.6362327933 0.5322877169 0.6510450244 + 0.6376551986 0.5336924195 0.6525406241 + 0.6390563250 0.5350908041 0.6540316939 + 0.6404522061 0.5364891887 0.6555228233 + 0.6418480873 0.5378875732 0.6570094824 + 0.6432371140 0.5392802954 0.6584786177 + 0.6446061730 0.5406693816 0.6599478126 + 0.6459752917 0.5420585871 0.6614168882 + 0.6473444104 0.5434476733 0.6628721952 + 0.6486949921 0.5448269844 0.6643189788 + 0.6500368714 0.5462061763 0.6657658219 + 0.6513789296 0.5475854874 0.6672123075 + 0.6527189016 0.5489618182 0.6686396003 + 0.6540355086 0.5503323078 0.6700670123 + 0.6553521156 0.5517029166 0.6714943051 + 0.6566687822 0.5530735254 0.6729140878 + 0.6579744816 0.5544376969 0.6743212938 + 0.6592665911 0.5557985902 0.6757284999 + 0.6605587006 0.5571594238 0.6771355867 + 0.6618508101 0.5585203171 0.6785268188 + 0.6631215811 0.5598711967 0.6799110770 + 0.6643863916 0.5612214804 0.6812955141 + 0.6656513214 0.5625717044 0.6826797724 + 0.6669138074 0.5639197826 0.6840425730 + 0.6681522131 0.5652583241 0.6854054928 + 0.6693906784 0.5665968060 0.6867682934 + 0.6706290841 0.5679352880 0.6881262064 + 0.6718584895 0.5692687035 0.6894711256 + 0.6730719805 0.5705946088 0.6908161044 + 0.6742855906 0.5719205737 0.6921610236 + 0.6754990816 0.5732464790 0.6934955716 + 0.6766976118 0.5745657086 0.6948205233 + 0.6778864264 0.5758802891 0.6961454153 + 0.6790751815 0.5771949291 0.6974701881 + 0.6802639961 0.5785095096 0.6987801194 + 0.6814342141 0.5798168182 0.7000842094 + 0.6826003194 0.5811215043 0.7013882995 + 0.6837664843 0.5824263096 0.7026923895 + 0.6849324703 0.5837309957 0.7039760947 + 0.6860750914 0.5850257874 0.7052575946 + 0.6872177124 0.5863190889 0.7065389752 + 0.6883602142 0.5876123905 0.7078195214 + 0.6894991994 0.5889056921 0.7090789080 + 0.6906188130 0.5901870728 0.7103384137 + 0.6917384267 0.5914680958 0.7115979195 + 0.6928580999 0.5927491188 0.7128540874 + 0.6939715147 0.5940297246 0.7140920162 + 0.6950684190 0.5952988863 0.7153298855 + 0.6961653829 0.5965679884 0.7165678144 + 0.6972624063 0.5978370905 0.7178003192 + 0.6983522773 0.5991057754 0.7190150023 + 0.6994295716 0.6003659964 0.7202296853 + 0.7005069852 0.6016262770 0.7214444280 + 0.7015843987 0.6028864980 0.7226526737 + 0.7026544809 0.6041461825 0.7238447070 + 0.7037143707 0.6053941250 0.7250366807 + 0.7047743201 0.6066418886 0.7262287140 + 0.7058342099 0.6078897119 0.7274144888 + 0.7068858743 0.6091374755 0.7285851836 + 0.7079268098 0.6103718877 0.7297558784 + 0.7089678049 0.6116061807 0.7309266925 + 0.7100088000 0.6128405929 0.7320916057 + 0.7110424042 0.6140750051 0.7332413197 + 0.7120658159 0.6152967215 0.7343909740 + 0.7130892277 0.6165174842 0.7355408072 + 0.7141125798 0.6177383065 0.7366862297 + 0.7151290178 0.6189591289 0.7378168106 + 0.7161335945 0.6201689839 0.7389473915 + 0.7171381712 0.6213768125 0.7400779128 + 0.7181428075 0.6225845218 0.7412056923 + 0.7191423774 0.6237921715 0.7423154712 + 0.7201297879 0.6249904037 0.7434253097 + 0.7211171985 0.6261842847 0.7445352077 + 0.7221046090 0.6273781061 0.7456446886 + 0.7230893970 0.6285719872 0.7467349768 + 0.7240619063 0.6297590733 0.7478252053 + 0.7250344753 0.6309399009 0.7489154935 + 0.7260071039 0.6321207285 0.7500057817 + 0.7269790769 0.6333013773 0.7510781288 + 0.7279344797 0.6344782710 0.7521474957 + 0.7288898826 0.6356462836 0.7532169223 + 0.7298454046 0.6368141770 0.7542862296 + 0.7308008075 0.6379821897 0.7553424835 + 0.7317430973 0.6391494274 0.7563924789 + 0.7326831222 0.6403048038 0.7574424148 + 0.7336230874 0.6414602995 0.7584924102 + 0.7345631123 0.6426156759 0.7595331073 + 0.7354928851 0.6437711120 0.7605624795 + 0.7364168167 0.6449149251 0.7615919113 + 0.7373406291 0.6460554004 0.7626212835 + 0.7382645011 0.6471958160 0.7636470795 + 0.7391830087 0.6483362913 0.7646576166 + 0.7400926948 0.6494706273 0.7656682134 + 0.7410023808 0.6505978703 0.7666786909 + 0.7419121861 0.6517251134 0.7676892877 + 0.7428205013 0.6528524160 0.7686821818 + 0.7437157035 0.6539782882 0.7696729898 + 0.7446109056 0.6550930738 0.7706639171 + 0.7455059886 0.6562079191 0.7716547251 + 0.7464011908 0.6573228240 0.7726355195 + 0.7472869158 0.6584376097 0.7736086249 + 0.7481697798 0.6595439911 0.7745816708 + 0.7490527034 0.6606470942 0.7755547166 + 0.7499356270 0.6617503166 0.7765241265 + 0.7508131862 0.6628534198 0.7774782777 + 0.7516831756 0.6639522910 0.7784324884 + 0.7525532246 0.6650412083 0.7793866992 + 0.7534232140 0.6661300063 0.7803409100 + 0.7542930841 0.6672189236 0.7812796831 + 0.7551512122 0.6683077216 0.7822145224 + 0.7560092807 0.6693845987 0.7831493020 + 0.7568672895 0.6704590917 0.7840840816 + 0.7577254176 0.6715335846 0.7850121260 + 0.7585778236 0.6726080775 0.7859280109 + 0.7594259977 0.6736781001 0.7868438959 + 0.7602741718 0.6747391224 0.7877597809 + 0.7611225247 0.6758000851 0.7886756063 + 0.7619696856 0.6768611073 0.7895761728 + 0.7628092766 0.6779221892 0.7904745936 + 0.7636489272 0.6789721251 0.7913730145 + 0.7644885182 0.6800191998 0.7922713757 + 0.7653279901 0.6810662746 0.7931637764 + 0.7661625743 0.6821134090 0.7940455079 + 0.7669938803 0.6831572056 0.7949272990 + 0.7678251266 0.6841893792 0.7958090901 + 0.7686563730 0.6852216721 0.7966908813 + 0.7694870234 0.6862540245 0.7975584865 + 0.7703074813 0.6872861981 0.7984229922 + 0.7711278796 0.6883111000 0.7992874980 + 0.7719483972 0.6893314123 0.8001520038 + 0.7727687955 0.6903517246 0.8010128736 + 0.7735849023 0.6913719773 0.8018617034 + 0.7743961811 0.6923922896 0.8027104735 + 0.7752075195 0.6934009790 0.8035593033 + 0.7760187984 0.6944093704 0.8044080734 + 0.7768300772 0.6954177022 0.8052453995 + 0.7776347995 0.6964259744 0.8060749173 + 0.7784383893 0.6974307895 0.8069043756 + 0.7792420983 0.6984261274 0.8077338934 + 0.7800456882 0.6994215250 0.8085634708 + 0.7808480263 0.7004168034 0.8093764186 + 0.7816452980 0.7014120817 0.8101881146 + 0.7824425101 0.7024009228 0.8109998107 + 0.7832397819 0.7033842802 0.8118115067 + 0.7840369940 0.7043675780 0.8126195073 + 0.7848305702 0.7053508759 0.8134146929 + 0.7856206894 0.7063342929 0.8142098188 + 0.7864106894 0.7073084712 0.8150050044 + 0.7872008085 0.7082800269 0.8158001900 + 0.7879908085 0.7092515826 0.8165881038 + 0.7887749076 0.7102231979 0.8173683882 + 0.7895570993 0.7111946940 0.8181486726 + 0.7903392911 0.7121539712 0.8189290166 + 0.7911216021 0.7131130099 0.8197093010 + 0.7919037938 0.7140719891 0.8204782009 + 0.7926781774 0.7150310278 0.8212423921 + 0.7934526205 0.7159885168 0.8220065832 + 0.7942268848 0.7169361115 0.8227707148 + 0.7950013280 0.7178837061 0.8235349059 + 0.7957745194 0.7188313007 0.8242856860 + 0.7965422273 0.7197788954 0.8250346184 + 0.7973099947 0.7207238078 0.8257834911 + 0.7980777025 0.7216603756 0.8265324831 + 0.7988454103 0.7225970030 0.8272811770 + 0.7996107936 0.7235335708 0.8280162215 + 0.8003711104 0.7244701982 0.8287512064 + 0.8011314869 0.7254031897 0.8294863105 + 0.8018918037 0.7263281941 0.8302212954 + 0.8026521206 0.7272531986 0.8309546113 + 0.8034093976 0.7281782031 0.8316739798 + 0.8041626811 0.7291032076 0.8323932886 + 0.8049159050 0.7300242186 0.8331127167 + 0.8056691885 0.7309377193 0.8338320851 + 0.8064224124 0.7318512201 0.8345488906 + 0.8071724772 0.7327647209 0.8352528811 + 0.8079192042 0.7336782217 0.8359568715 + 0.8086658716 0.7345883250 0.8366609216 + 0.8094125986 0.7354918122 0.8373649120 + 0.8101593256 0.7363952994 0.8380662799 + 0.8109018803 0.7372987270 0.8387560248 + 0.8116406798 0.7382022142 0.8394458294 + 0.8123794198 0.7391029000 0.8401355147 + 0.8131181002 0.7399963737 0.8408252001 + 0.8138568997 0.7408899069 0.8415125012 + 0.8145921826 0.7417833805 0.8421863914 + 0.8153241277 0.7426769137 0.8428602815 + 0.8160558939 0.7435685992 0.8435341716 + 0.8167877793 0.7444524765 0.8442081809 + 0.8175196052 0.7453362942 0.8448808789 + 0.8182482719 0.7462201118 0.8455417156 + 0.8189728260 0.7471039295 0.8462024927 + 0.8196973205 0.7479872704 0.8468633294 + 0.8204218745 0.7488632202 0.8475239873 + 0.8211464286 0.7497389913 0.8481848240 + 0.8218685985 0.7506148219 0.8488330841 + 0.8225861192 0.7514907122 0.8494811058 + 0.8233035207 0.7523664832 0.8501291275 + 0.8240209222 0.7532342076 0.8507772088 + 0.8247383833 0.7541006804 0.8514251709 + 0.8254544139 0.7549670935 0.8520622849 + 0.8261637092 0.7558336258 0.8526970744 + 0.8268730044 0.7567000985 0.8533319831 + 0.8275822997 0.7575604916 0.8539667726 + 0.8282917142 0.7584174871 0.8546016216 + 0.8290010095 0.7592746019 0.8552286029 + 0.8297007084 0.7601317167 0.8558509946 + 0.8304002881 0.7609887719 0.8564733267 + 0.8310999274 0.7618423104 0.8570957184 + 0.8317995071 0.7626898289 0.8577179909 + 0.8324990869 0.7635374069 0.8583357930 + 0.8331922293 0.7643849850 0.8589463234 + 0.8338832855 0.7652326226 0.8595567942 + 0.8345744014 0.7660794258 0.8601673245 + 0.8352655172 0.7669159770 0.8607777953 + 0.8359565735 0.7677527070 0.8613868952 + 0.8366435170 0.7685893774 0.8619840145 + 0.8373258710 0.7694259882 0.8625810146 + 0.8380082846 0.7702627182 0.8631780744 + 0.8386908174 0.7710928917 0.8637751937 + 0.8393731713 0.7719206214 0.8643721938 + 0.8400542736 0.7727481723 0.8649600148 + 0.8407285213 0.7735759020 0.8655449748 + 0.8414027095 0.7744035721 0.8661299944 + 0.8420767784 0.7752280831 0.8667150140 + 0.8427510262 0.7760459781 0.8672999740 + 0.8434252143 0.7768638730 0.8678801060 + 0.8440936804 0.7776817083 0.8684527278 + 0.8447608948 0.7784996033 0.8690252900 + 0.8454279900 0.7793174982 0.8695979118 + 0.8460950851 0.7801278830 0.8701705933 + 0.8467621803 0.7809370756 0.8707432151 + 0.8474255204 0.7817463875 0.8713021278 + 0.8480827212 0.7825555801 0.8718606830 + 0.8487399817 0.7833648920 0.8724194169 + 0.8493973017 0.7841700912 0.8729780912 + 0.8500545025 0.7849687934 0.8735368252 + 0.8507118225 0.7857673764 0.8740890026 + 0.8513603806 0.7865660787 0.8746353984 + 0.8520085216 0.7873647213 0.8751817942 + 0.8526566029 0.7881634235 0.8757281899 + 0.8533046842 0.7889544964 0.8762745857 + 0.8539528251 0.7897443175 0.8768208027 + 0.8545969129 0.7905340195 0.8773562908 + 0.8552358747 0.7913237810 0.8778917789 + 0.8558748960 0.7921136022 0.8784273267 + 0.8565139174 0.7929015756 0.8789628148 + 0.8571529984 0.7936850786 0.8794983029 + 0.8577920198 0.7944685817 0.8800281882 + 0.8584234118 0.7952520847 0.8805518150 + 0.8590539098 0.7960355878 0.8810753822 + 0.8596845269 0.7968190908 0.8815988898 + 0.8603150845 0.7975977063 0.8821225166 + 0.8609455824 0.7983735204 0.8826460838 + 0.8615739942 0.7991493940 0.8831595778 + 0.8621975183 0.7999252081 0.8836715221 + 0.8628209829 0.8007010818 0.8841834068 + 0.8634445071 0.8014768958 0.8846952915 + 0.8640679121 0.8022441268 0.8852071762 + 0.8646913767 0.8030110002 0.8857167959 + 0.8653084040 0.8037779927 0.8862178922 + 0.8659222722 0.8045449257 0.8867189884 + 0.8665362000 0.8053117990 0.8872202039 + 0.8671500087 0.8060764074 0.8877213001 + 0.8677638769 0.8068339229 0.8882223964 + 0.8683776855 0.8075914979 0.8887180090 + 0.8689824939 0.8083490133 0.8892086744 + 0.8695871234 0.8091065288 0.8896992803 + 0.8701918125 0.8098639846 0.8901900053 + 0.8707963824 0.8106179833 0.8906806111 + 0.8714010715 0.8113679886 0.8911712766 + 0.8720031977 0.8121181130 0.8916537166 + 0.8725978732 0.8128681183 0.8921337724 + 0.8731927276 0.8136181831 0.8926138878 + 0.8737874031 0.8143681884 0.8930941224 + 0.8743820786 0.8151124716 0.8935741782 + 0.8749768734 0.8158538938 0.8940542936 + 0.8755673170 0.8165953159 0.8945251107 + 0.8761531115 0.8173366785 0.8949958086 + 0.8767387867 0.8180781007 0.8954666257 + 0.8773245811 0.8188195229 0.8959373236 + 0.8779103756 0.8195531964 0.8964080215 + 0.8784961104 0.8202847838 0.8968774080 + 0.8790776134 0.8210164905 0.8973398209 + 0.8796569109 0.8217481971 0.8978022933 + 0.8802360892 0.8224797845 0.8982647061 + 0.8808153272 0.8232114911 0.8987271786 + 0.8813946247 0.8239343166 0.8991895914 + 0.8819738030 0.8246560097 0.8996490836 + 0.8825464845 0.8253775835 0.9001011252 + 0.8831173182 0.8260992765 0.9005531073 + 0.8836882114 0.8268209100 0.9010050893 + 0.8842589855 0.8275426030 0.9014570713 + 0.8848298192 0.8282570243 0.9019091725 + 0.8854007125 0.8289706707 0.9023581147 + 0.8859637976 0.8296844959 0.9028012753 + 0.8865255713 0.8303982019 0.9032443762 + 0.8870874047 0.8311120272 0.9036875963 + 0.8876491785 0.8318256736 0.9041308165 + 0.8882110119 0.8325313926 0.9045739174 + 0.8887729049 0.8332362771 0.9050139785 + 0.8893269897 0.8339412212 0.9054486752 + 0.8898801208 0.8346461058 0.9058833718 + 0.8904331923 0.8353509903 0.9063181281 + 0.8909863234 0.8360558152 0.9067528248 + 0.8915393949 0.8367524147 0.9071875215 + 0.8920925260 0.8374475837 0.9076195955 + 0.8926386237 0.8381426930 0.9080467224 + 0.8931834102 0.8388378024 0.9084736705 + 0.8937283158 0.8395329118 0.9089006782 + 0.8942731023 0.8402280211 0.9093278050 + 0.8948178887 0.8409171104 0.9097548127 + 0.8953627944 0.8416041136 0.9101794958 + 0.8958991170 0.8422911167 0.9105970263 + 0.8964328766 0.8429781199 0.9110143781 + 0.8969668150 0.8436651230 0.9114319086 + 0.8975006938 0.8443520069 0.9118493795 + 0.8980345130 0.8450348973 0.9122667909 + 0.8985683918 0.8457146883 0.9126836061 + 0.8990967870 0.8463944197 0.9130951166 + 0.8996223211 0.8470742106 0.9135065079 + 0.9001477957 0.8477538824 0.9139180183 + 0.9006732702 0.8484336734 0.9143294096 + 0.9011988044 0.8491110206 0.9147409201 + 0.9017242789 0.8497840166 0.9151523113 + 0.9022449851 0.8504568934 0.9155570269 + 0.9027605057 0.8511298895 0.9159610868 + 0.9032760859 0.8518028259 0.9163653255 + 0.9037916064 0.8524758220 0.9167693853 + 0.9043071270 0.8531479836 0.9171736240 + 0.9048225880 0.8538120985 0.9175776839 + 0.9053356051 0.8544760942 0.9179764986 + 0.9058414102 0.8551402092 0.9183728099 + 0.9063470960 0.8558042049 0.9187691808 + 0.9068529010 0.8564683199 0.9191656113 + 0.9073585868 0.8571323156 0.9195619226 + 0.9078643918 0.8577905893 0.9199582934 + 0.9083700776 0.8584470749 0.9203512073 + 0.9088681936 0.8591036201 0.9207388759 + 0.9093660712 0.8597599864 0.9211267233 + 0.9098640084 0.8604164720 0.9215145111 + 0.9103618860 0.8610730171 0.9219022989 + 0.9108597040 0.8617268205 0.9222900271 + 0.9113575816 0.8623763919 0.9226773977 + 0.9118500948 0.8630260229 0.9230594039 + 0.9123396277 0.8636757135 0.9234412909 + 0.9128291011 0.8643252850 0.9238231778 + 0.9133185744 0.8649749160 0.9242051840 + 0.9138082266 0.8656246066 0.9245870709 + 0.9142976999 0.8662685156 0.9249690771 + 0.9147847295 0.8669123054 0.9253469110 + 0.9152647257 0.8675560951 0.9257227182 + 0.9157447815 0.8681998849 0.9260985255 + 0.9162247777 0.8688436747 0.9264742732 + 0.9167048931 0.8694875240 0.9268500805 + 0.9171848893 0.8701276183 0.9272258282 + 0.9176650047 0.8707640171 0.9275999069 + 0.9181361198 0.8714004159 0.9279679060 + 0.9186053276 0.8720368147 0.9283359051 + 0.9190744758 0.8726732135 0.9287039042 + 0.9195436835 0.8733096123 0.9290717840 + 0.9200128913 0.8739460111 0.9294397831 + 0.9204820991 0.8745772839 0.9298077822 + 0.9209483862 0.8752083778 0.9301705956 + 0.9214088917 0.8758394122 0.9305313826 + 0.9218693972 0.8764705062 0.9308922291 + 0.9223300219 0.8771016002 0.9312530160 + 0.9227905273 0.8777326941 0.9316139221 + 0.9232509732 0.8783612251 0.9319747090 + 0.9237114787 0.8789854050 0.9323344231 + 0.9241648912 0.8796095848 0.9326891899 + 0.9246163964 0.8802338243 0.9330438972 + 0.9250679016 0.8808580041 0.9333986044 + 0.9255194068 0.8814823031 0.9337533712 + 0.9259709120 0.8821064830 0.9341080785 + 0.9264224172 0.8827261925 0.9344627857 + 0.9268720150 0.8833442926 0.9348145723 + 0.9273136854 0.8839623928 0.9351639152 + 0.9277552962 0.8845806122 0.9355133176 + 0.9281969070 0.8851987123 0.9358627200 + 0.9286385775 0.8858168125 0.9362121224 + 0.9290801883 0.8864346743 0.9365615249 + 0.9295219183 0.8870465755 0.9369109273 + 0.9299585223 0.8876584768 0.9372549057 + 0.9303908944 0.8882703781 0.9375984073 + 0.9308233261 0.8888823986 0.9379419088 + 0.9312556982 0.8894942999 0.9382852912 + 0.9316880703 0.8901062012 0.9386287928 + 0.9321205020 0.8907166123 0.9389722943 + 0.9325528741 0.8913230896 0.9393147826 + 0.9329783916 0.8919296265 0.9396532178 + 0.9334024787 0.8925361037 0.9399914742 + 0.9338266850 0.8931425214 0.9403297901 + 0.9342507720 0.8937489986 0.9406682253 + 0.9346749187 0.8943554759 0.9410064816 + 0.9350991249 0.8949587941 0.9413447976 + 0.9355226159 0.8955584764 0.9416810870 + 0.9359377027 0.8961580992 0.9420145154 + 0.9363527894 0.8967577815 0.9423478842 + 0.9367679954 0.8973575234 0.9426813126 + 0.9371830821 0.8979572058 0.9430146813 + 0.9375982285 0.8985568881 0.9433481097 + 0.9380133748 0.8991538286 0.9436814785 + 0.9384260774 0.8997489214 0.9440121055 + 0.9388316274 0.9003438950 0.9443405271 + 0.9392371178 0.9009389877 0.9446688890 + 0.9396426082 0.9015340805 0.9449973106 + 0.9400480986 0.9021291733 0.9453257918 + 0.9404535890 0.9027243257 0.9456542134 + 0.9408590794 0.9033153057 0.9459825754 + 0.9412612915 0.9039043784 0.9463074803 + 0.9416580200 0.9044935107 0.9466307759 + 0.9420546293 0.9050825834 0.9469540715 + 0.9424512982 0.9056717157 0.9472774267 + 0.9428480268 0.9062607884 0.9476007223 + 0.9432446957 0.9068499207 0.9479240179 + 0.9436413050 0.9074342251 0.9482473135 + 0.9440346956 0.9080166221 0.9485682845 + 0.9444236755 0.9085990191 0.9488884807 + 0.9448127151 0.9091812968 0.9492086768 + 0.9452018142 0.9097636938 0.9495288730 + 0.9455907941 0.9103459716 0.9498491287 + 0.9459797740 0.9109284282 0.9501693249 + 0.9463688135 0.9115049839 0.9504894018 + 0.9467542171 0.9120786190 0.9508066773 + 0.9471350908 0.9126523137 0.9511228800 + 0.9475159049 0.9132260084 0.9514390826 + 0.9478967786 0.9137995839 0.9517552853 + 0.9482775927 0.9143732786 0.9520714879 + 0.9486585259 0.9149469733 0.9523876905 + 0.9490392804 0.9155157208 0.9527038932 + 0.9494168162 0.9160807729 0.9530172944 + 0.9497889876 0.9166458845 0.9533293247 + 0.9501612782 0.9172109962 0.9536414146 + 0.9505335093 0.9177759886 0.9539533854 + 0.9509056807 0.9183411002 0.9542654753 + 0.9512779117 0.9189062119 0.9545776248 + 0.9516502023 0.9194672704 0.9548895955 + 0.9520196915 0.9200227857 0.9551991820 + 0.9523826241 0.9205783010 0.9555068016 + 0.9527454972 0.9211338162 0.9558144808 + 0.9531083703 0.9216892719 0.9561221004 + 0.9534713030 0.9222447872 0.9564297199 + 0.9538341761 0.9228003025 0.9567372799 + 0.9541971087 0.9233545065 0.9570448995 + 0.9545590878 0.9239031076 0.9573509097 + 0.9549140930 0.9244517088 0.9576539993 + 0.9552692175 0.9250003099 0.9579570889 + 0.9556242228 0.9255489111 0.9582601786 + 0.9559792876 0.9260975122 0.9585633278 + 0.9563344121 0.9266461134 0.9588664174 + 0.9566894174 0.9271947145 0.9591695070 + 0.9570444822 0.9277390838 0.9594715834 + 0.9573910236 0.9282829762 0.9597675800 + 0.9577363729 0.9288269877 0.9600635767 + 0.9580819011 0.9293709993 0.9603595734 + 0.9584273100 0.9299150109 0.9606556296 + 0.9587727785 0.9304590225 0.9609516859 + 0.9591181874 0.9310029745 0.9612476826 + 0.9594637156 0.9315435290 0.9615436792 + 0.9598045945 0.9320815802 0.9618365765 + 0.9601426125 0.9326195717 0.9621289968 + 0.9604806900 0.9331576824 0.9624214172 + 0.9608187079 0.9336956739 0.9627137780 + 0.9611567259 0.9342337847 0.9630060792 + 0.9614946842 0.9347717762 0.9632984996 + 0.9618327022 0.9353088737 0.9635909200 + 0.9621691108 0.9358416200 0.9638820887 + 0.9625008702 0.9363743067 0.9641720057 + 0.9628326893 0.9369069934 0.9644619226 + 0.9631645083 0.9374396801 0.9647517800 + 0.9634963274 0.9379724264 0.9650416970 + 0.9638280272 0.9385051131 0.9653316140 + 0.9641597867 0.9390376806 0.9656215906 + 0.9644916058 0.9395660162 0.9659113288 + 0.9648177028 0.9400926828 0.9661973715 + 0.9651427865 0.9406192899 0.9664834738 + 0.9654678702 0.9411458969 0.9667695761 + 0.9657930136 0.9416726232 0.9670556784 + 0.9661180973 0.9421991706 0.9673417807 + 0.9664431810 0.9427258968 0.9676278830 + 0.9667683244 0.9432511926 0.9679139853 + 0.9670909047 0.9437717795 0.9681978226 + 0.9674093723 0.9442923069 0.9684798121 + 0.9677278996 0.9448127747 0.9687616825 + 0.9680464268 0.9453333020 0.9690436721 + 0.9683648944 0.9458538294 0.9693257213 + 0.9686834216 0.9463744164 0.9696077108 + 0.9690018892 0.9468948841 0.9698897004 + 0.9693204165 0.9474115968 0.9701715708 + 0.9696332216 0.9479261041 0.9704506993 + 0.9699448943 0.9484406114 0.9707298279 + 0.9702566266 0.9489551187 0.9710088968 + 0.9705682993 0.9494696259 0.9712880254 + 0.9708799720 0.9499840140 0.9715670943 + 0.9711917043 0.9504985213 0.9718461037 + 0.9715033770 0.9510130286 0.9721251726 + 0.9718133807 0.9515205026 0.9724026918 + 0.9721180201 0.9520277977 0.9726777077 + 0.9724227190 0.9525352120 0.9729527831 + 0.9727274179 0.9530425072 0.9732279181 + 0.9730321169 0.9535499215 0.9735029936 + 0.9733366966 0.9540572166 0.9737780094 + 0.9736413956 0.9545645118 0.9740530849 + 0.9739460945 0.9550700188 0.9743282199 + 0.9742460847 0.9555702209 0.9745998979 + 0.9745427966 0.9560704231 0.9748703837 + 0.9748393893 0.9565706849 0.9751409292 + 0.9751361012 0.9570708871 0.9754114747 + 0.9754328132 0.9575710893 0.9756820202 + 0.9757294059 0.9580712914 0.9759525061 + 0.9760261178 0.9585716128 0.9762231112 + 0.9763227105 0.9590680003 0.9764935970 + 0.9766126275 0.9595606923 0.9767600298 + 0.9769014120 0.9600533843 0.9770264030 + 0.9771901965 0.9605460167 0.9772928953 + 0.9774789214 0.9610387087 0.9775593281 + 0.9777677059 0.9615312815 0.9778257012 + 0.9780563712 0.9620239735 0.9780920744 + 0.9783452153 0.9625167251 0.9783586264 + 0.9786334038 0.9630044103 0.9786242247 + 0.9789144993 0.9634895921 0.9788866043 + 0.9791955948 0.9639747143 0.9791489840 + 0.9794766903 0.9644598961 0.9794114232 + 0.9797577858 0.9649450779 0.9796738029 + 0.9800388813 0.9654303193 0.9799361825 + 0.9803199768 0.9659153819 0.9801986217 + 0.9806010723 0.9664006233 0.9804610014 + 0.9808804989 0.9668791294 0.9807218909 + 0.9811537862 0.9673554897 0.9809799790 + 0.9814270735 0.9678319097 0.9812381268 + 0.9817004204 0.9683082104 0.9814962745 + 0.9819737077 0.9687845707 0.9817544222 + 0.9822469950 0.9692608714 0.9820125103 + 0.9825202823 0.9697372913 0.9822705984 + 0.9827936292 0.9702135921 0.9825286865 + 0.9830644727 0.9706823230 0.9827851057 + 0.9833294749 0.9711489081 0.9830390811 + 0.9835945964 0.9716154933 0.9832929969 + 0.9838597178 0.9720820785 0.9835469723 + 0.9841247201 0.9725487232 0.9838010073 + 0.9843897820 0.9730151892 0.9840549231 + 0.9846549034 0.9734817743 0.9843088984 + 0.9849199057 0.9739484191 0.9845628142 + 0.9851824045 0.9744058847 0.9848148823 + 0.9854390025 0.9748603106 0.9850642085 + 0.9856956005 0.9753146768 0.9853134751 + 0.9859521985 0.9757689834 0.9855628014 + 0.9862087965 0.9762234092 0.9858121276 + 0.9864653945 0.9766777754 0.9860615134 + 0.9867219925 0.9771322012 0.9863107800 + 0.9869785905 0.9775866270 0.9865601063 + 0.9872329831 0.9780325890 0.9868078232 + 0.9874812961 0.9784740806 0.9870523810 + 0.9877294898 0.9789155126 0.9872971177 + 0.9879776835 0.9793568850 0.9875416756 + 0.9882258773 0.9797983170 0.9877864122 + 0.9884740710 0.9802396894 0.9880310297 + 0.9887223244 0.9806811213 0.9882757068 + 0.9889705777 0.9811224937 0.9885203242 + 0.9892175198 0.9815559983 0.9887641072 + 0.9894570708 0.9819805026 0.9890046716 + 0.9896966815 0.9824051261 0.9892451763 + 0.9899362922 0.9828295708 0.9894858003 + 0.9901759028 0.9832541943 0.9897264242 + 0.9904153943 0.9836786985 0.9899669886 + 0.9906550050 0.9841033220 0.9902076125 + 0.9908946157 0.9845278263 0.9904481769 + 0.9911342263 0.9849482775 0.9906886220 + 0.9913660884 0.9853556752 0.9909231067 + 0.9915975928 0.9857630134 0.9911575913 + 0.9918292165 0.9861702919 0.9913920760 + 0.9920607805 0.9865776896 0.9916266203 + 0.9922922850 0.9869850278 0.9918611050 + 0.9925239086 0.9873923063 0.9920955896 + 0.9927554727 0.9877997041 0.9923300743 + 0.9929869771 0.9882069826 0.9925646186 + 0.9932109714 0.9885931015 0.9927943945 + 0.9934319258 0.9889774919 0.9930227995 + 0.9936528206 0.9893618226 0.9932510853 + 0.9938737154 0.9897462130 0.9934794903 + 0.9940946102 0.9901306033 0.9937078953 + 0.9943153858 0.9905149937 0.9939361811 + 0.9945362806 0.9908993840 0.9941645861 + 0.9947571754 0.9912837744 0.9943928719 + 0.9949743152 0.9916560054 0.9946191907 + 0.9951854944 0.9920184016 0.9948425293 + 0.9953966737 0.9923807979 0.9950658083 + 0.9956079125 0.9927430749 0.9952890873 + 0.9958190918 0.9931054711 0.9955124259 + 0.9960302711 0.9934678078 0.9957357049 + 0.9962415099 0.9938302040 0.9959589839 + 0.9964526892 0.9941924810 0.9961823225 + 0.9966639280 0.9945517778 0.9964054823 + 0.9968671203 0.9948847890 0.9966220856 + 0.9970703125 0.9952176809 0.9968386292 + 0.9972733855 0.9955506921 0.9970551729 + 0.9974765778 0.9958835840 0.9972717166 + 0.9976797104 0.9962165952 0.9974883199 + 0.9978829026 0.9965494871 0.9977048039 + 0.9980860949 0.9968824983 0.9979214072 + 0.9982892275 0.9972153902 0.9981380105 + 0.9984850287 0.9975339174 0.9983490109 + 0.9986743927 0.9978420734 0.9985553026 + 0.9988638163 0.9981504083 0.9987617135 + 0.9990531802 0.9984586835 0.9989681244 + 0.9992424846 0.9987668991 0.9991744757 + 0.9994319081 0.9990751743 0.9993808866 + 0.9996212721 0.9993835092 0.9995871782 + 0.9998105764 0.9996917248 0.9997935891 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfachrome_rsx2_050.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfachrome_rsx2_050.spi1d index 42a32ac9..f15dc09e 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfachrome_rsx2_050.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfachrome_rsx2_050.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0010793300 0.0004914269 0.0009736730 - 0.0023187911 0.0010802930 0.0021354880 - 0.0036931380 0.0018072630 0.0034938811 - 0.0052456409 0.0026662690 0.0050411709 - 0.0070081931 0.0036067569 0.0068641612 - 0.0090551702 0.0045785471 0.0090099405 - 0.0114011699 0.0055691102 0.0114547899 - 0.0139678400 0.0065905452 0.0141683398 - 0.0166869499 0.0076627559 0.0171185602 - 0.0195989702 0.0088004395 0.0202584993 - 0.0228006691 0.0100080501 0.0235486198 - 0.0263756197 0.0112880496 0.0269760005 - 0.0303584095 0.0126401298 0.0305433907 - 0.0347159505 0.0140636303 0.0342755318 - 0.0393988304 0.0155540602 0.0381953716 - 0.0443377309 0.0171071608 0.0423245206 - 0.0494562984 0.0187185593 0.0466708317 - 0.0546695217 0.0203871392 0.0512099490 - 0.0599117801 0.0221128706 0.0559065714 - 0.0651446804 0.0238924101 0.0607012585 - 0.0703425035 0.0257253498 0.0655229613 - 0.0754935667 0.0276135001 0.0703299269 - 0.0806027427 0.0295511000 0.0750854388 - 0.0856716335 0.0315340608 0.0797831416 - 0.0907013789 0.0335622691 0.0844218433 - 0.0956865475 0.0356292501 0.0890035927 - 0.1006257012 0.0377341881 0.0935301632 - 0.1055174991 0.0398772992 0.0980233997 - 0.1103760004 0.0420543700 0.1024931967 - 0.1152203977 0.0442650095 0.1069511026 - 0.1200542971 0.0465072691 0.1114012972 - 0.1248871982 0.0487757288 0.1158443019 - 0.1297339052 0.0510716997 0.1202880964 - 0.1346120983 0.0533953197 0.1247389987 - 0.1395314038 0.0557426214 0.1292016953 - 0.1444949955 0.0581145883 0.1336800009 - 0.1495019048 0.0605066083 0.1381773949 - 0.1545559019 0.0629175901 0.1426994056 - 0.1596433967 0.0653483421 0.1472471952 - 0.1647536010 0.0677959695 0.1518259943 - 0.1698786020 0.0702603534 0.1564404070 - 0.1750122011 0.0727384463 0.1610952020 - 0.1801480949 0.0752333701 0.1657952070 - 0.1852789968 0.0777411908 0.1705430001 - 0.1903969944 0.0802624598 0.1753392965 - 0.1954903007 0.0827964321 0.1801753938 - 0.2005587071 0.0853414088 0.1850488037 - 0.2055945992 0.0878966674 0.1899594069 - 0.2105983049 0.0904616788 0.1949048936 - 0.2155726999 0.0930357873 0.1998749971 - 0.2205152959 0.0956183821 0.2048487961 - 0.2254346013 0.0982099175 0.2098221034 - 0.2303334028 0.1008104980 0.2147811055 - 0.2352052927 0.1034198999 0.2197100967 - 0.2400625050 0.1060379967 0.2246035039 - 0.2449066043 0.1086636037 0.2294571996 - 0.2497355938 0.1112938970 0.2342610061 - 0.2545413971 0.1139284000 0.2390076965 - 0.2593365014 0.1165691987 0.2436912954 - 0.2641316950 0.1192142963 0.2483087927 - 0.2689233124 0.1218615994 0.2528623939 - 0.2737050056 0.1245130002 0.2573607862 - 0.2784807086 0.1271678954 0.2618094981 - 0.2832559049 0.1298251003 0.2662059069 - 0.2880277932 0.1324838996 0.2705568075 - 0.2927958071 0.1351446956 0.2748655975 - 0.2975589931 0.1378072053 0.2791439891 - 0.3023168147 0.1404704005 0.2833940089 - 0.3070589006 0.1431359947 0.2876232862 - 0.3117862940 0.1458034068 0.2918379009 - 0.3165079057 0.1484730989 0.2960408032 - 0.3212226927 0.1511427015 0.3002305925 - 0.3259240985 0.1538105011 0.3044033051 - 0.3306060135 0.1564781964 0.3085659146 - 0.3352772892 0.1591459066 0.3127256930 - 0.3399353027 0.1618131995 0.3168801963 - 0.3445748985 0.1644778997 0.3210279047 - 0.3491897881 0.1671431065 0.3251653910 - 0.3537842929 0.1698081940 0.3292970955 - 0.3583635986 0.1724765003 0.3334251940 - 0.3629280031 0.1751464009 0.3375486135 - 0.3674629927 0.1778157055 0.3416658044 - 0.3719677031 0.1804865003 0.3457753956 - 0.3764384985 0.1831578016 0.3498772979 - 0.3808833957 0.1858278960 0.3539676964 - 0.3853051066 0.1885012984 0.3580411971 - 0.3896934986 0.1911745965 0.3620963991 - 0.3940488100 0.1938498020 0.3661319017 - 0.3983697891 0.1965274960 0.3701558113 - 0.4026510119 0.1992076039 0.3741641045 - 0.4068988860 0.2018887997 0.3781496882 - 0.4111185968 0.2045747042 0.3821138144 - 0.4153113961 0.2072620988 0.3860580921 - 0.4194692075 0.2099498957 0.3899829984 - 0.4235900044 0.2126414031 0.3938888013 - 0.4276776910 0.2153348029 0.3977788091 - 0.4317341149 0.2180286050 0.4016534090 - 0.4357563853 0.2207237929 0.4055137932 - 0.4397394061 0.2234221995 0.4093612134 - 0.4436829090 0.2261236012 0.4131959975 - 0.4475947022 0.2288268059 0.4170176983 - 0.4514735937 0.2315322012 0.4208177924 - 0.4553222954 0.2342423052 0.4245958030 - 0.4591388106 0.2369553000 0.4283536971 - 0.4629189968 0.2396683991 0.4320934117 - 0.4666629136 0.2423831969 0.4358170927 - 0.4703747034 0.2451013029 0.4395225048 - 0.4740515947 0.2478200942 0.4432024956 - 0.4776928127 0.2505406141 0.4468582869 - 0.4813019931 0.2532635927 0.4504924119 - 0.4848793149 0.2559860945 0.4541029930 - 0.4884204865 0.2587074935 0.4576902092 - 0.4919295907 0.2614302039 0.4612514973 - 0.4954057038 0.2641544044 0.4647876918 - 0.4988467097 0.2668786943 0.4682984054 - 0.5022568703 0.2696047127 0.4717861116 - 0.5056318045 0.2723301053 0.4752514064 - 0.5089687109 0.2750515938 0.4786919951 - 0.5122699738 0.2777692974 0.4821071923 - 0.5155354738 0.2804855108 0.4855000973 - 0.5187671781 0.2832008898 0.4888701141 - 0.5219681263 0.2859140933 0.4922114909 - 0.5251352191 0.2886242867 0.4955233037 - 0.5282660127 0.2913315892 0.4988074899 - 0.5313646197 0.2940349877 0.5020601749 - 0.5344318151 0.2967323065 0.5052834749 - 0.5374667048 0.2994225919 0.5084816217 - 0.5404701233 0.3021079004 0.5116459131 - 0.5434368849 0.3047888875 0.5147755146 - 0.5463718176 0.3074640036 0.5178748965 - 0.5492841005 0.3101322055 0.5209411979 - 0.5521711707 0.3127948046 0.5239753127 - 0.5550354123 0.3154520094 0.5269787908 - 0.5578734875 0.3181014955 0.5299521089 - 0.5606822968 0.3207401037 0.5328952074 - 0.5634636283 0.3233641088 0.5358104110 - 0.5662226081 0.3259783983 0.5386914015 - 0.5689582229 0.3285818994 0.5415412784 - 0.5716739893 0.3311730921 0.5443618894 - 0.5743725896 0.3337512016 0.5471563935 - 0.5770525932 0.3363159001 0.5499269962 - 0.5797128081 0.3388671875 0.5526747704 - 0.5823537707 0.3414067030 0.5553982854 - 0.5849738717 0.3439315856 0.5580946207 - 0.5875731707 0.3464401960 0.5607615709 - 0.5901582241 0.3489316106 0.5634018183 - 0.5927311778 0.3514032960 0.5660176277 - 0.5952929258 0.3538616896 0.5686088800 - 0.5978369713 0.3563053906 0.5711727738 - 0.6003630161 0.3587335050 0.5737162828 - 0.6028745770 0.3611412942 0.5762403011 - 0.6053730249 0.3635332882 0.5787448287 - 0.6078591943 0.3659127057 0.5812224150 - 0.6103302240 0.3682799935 0.5836719275 - 0.6127892733 0.3706313074 0.5861017108 - 0.6152378917 0.3729673922 0.5885127783 - 0.6176773906 0.3752928078 0.5909010768 - 0.6201043725 0.3776082993 0.5932654738 - 0.6225174069 0.3799093068 0.5956128240 - 0.6249200106 0.3821944892 0.5979433060 - 0.6273151040 0.3844664991 0.6002511978 - 0.6296957135 0.3867239058 0.6025369167 - 0.6320651174 0.3889622986 0.6048055887 - 0.6344231963 0.3911905885 0.6070576906 - 0.6367667913 0.3934093118 0.6092873216 - 0.6390994787 0.3956130147 0.6115016937 - 0.6414197087 0.3978062868 0.6137034297 - 0.6437255740 0.3999901116 0.6158903241 - 0.6460139155 0.4021590948 0.6180559993 - 0.6482884288 0.4043118060 0.6202107072 - 0.6505498290 0.4064539969 0.6223551035 - 0.6527922750 0.4085829854 0.6244797111 - 0.6550189853 0.4106990099 0.6265919805 - 0.6572331190 0.4128079116 0.6286922097 - 0.6594275832 0.4149076939 0.6307719946 - 0.6616020799 0.4169962108 0.6328415275 - 0.6637635827 0.4190776944 0.6349012256 - 0.6659076214 0.4211486876 0.6369373202 - 0.6680328250 0.4232074916 0.6389623880 - 0.6701455712 0.4252586961 0.6409782171 - 0.6722385287 0.4272964001 0.6429752111 - 0.6743144989 0.4293248057 0.6449599862 - 0.6763777733 0.4313467145 0.6469345093 - 0.6784113050 0.4333539903 0.6488881707 - 0.6804267764 0.4353561103 0.6508327723 - 0.6824293137 0.4373523891 0.6527665854 - 0.6844047904 0.4393377006 0.6546779871 - 0.6863657236 0.4413183928 0.6565799713 - 0.6883090734 0.4432905018 0.6584674120 - 0.6902278066 0.4452567101 0.6603385806 - 0.6921337247 0.4472194016 0.6622003913 - 0.6940143704 0.4491713941 0.6640378833 - 0.6958734989 0.4511190057 0.6658635736 - 0.6977202296 0.4530585110 0.6676772237 - 0.6995369792 0.4549886882 0.6694694161 - 0.7013437748 0.4569143951 0.6712527871 - 0.7031323910 0.4588277936 0.6730164886 - 0.7048978806 0.4607372880 0.6747667193 - 0.7066525817 0.4626379907 0.6765061021 - 0.7083770037 0.4645319879 0.6782280803 - 0.7100898027 0.4664215147 0.6799430251 - 0.7117800713 0.4683004022 0.6816384196 - 0.7134491205 0.4701760113 0.6833207011 - 0.7151061296 0.4720404148 0.6849896908 - 0.7167317271 0.4739001989 0.6866384149 - 0.7183483839 0.4757528007 0.6882799864 - 0.7199388742 0.4775973856 0.6898952723 - 0.7215155959 0.4794372022 0.6915025711 - 0.7230750918 0.4812653959 0.6930922270 - 0.7246130705 0.4830907881 0.6946684122 - 0.7261415124 0.4849025011 0.6962333918 - 0.7276425958 0.4867109954 0.6977772713 - 0.7291371226 0.4885095954 0.6993148923 - 0.7306033969 0.4903028011 0.7008234859 - 0.7320588231 0.4920882881 0.7023267150 - 0.7334914207 0.4938659966 0.7038077712 - 0.7349091768 0.4956378937 0.7052788138 - 0.7363126278 0.4973995984 0.7067326903 - 0.7376976013 0.4991573095 0.7081738710 - 0.7390735745 0.5009049773 0.7096043229 - 0.7404264212 0.5026509166 0.7110185027 - 0.7417743206 0.5043882728 0.7124254107 - 0.7430984974 0.5061246753 0.7138100863 - 0.7444195151 0.5078502297 0.7151901722 - 0.7457147241 0.5095742941 0.7165504098 - 0.7470049858 0.5112900138 0.7179082036 - 0.7482746840 0.5130041242 0.7192432284 - 0.7495390773 0.5147119164 0.7205743194 - 0.7507901192 0.5164154172 0.7218819857 - 0.7520323992 0.5181065202 0.7231845856 - 0.7532587051 0.5197932720 0.7244688272 - 0.7544769049 0.5214695930 0.7257465124 - 0.7556836009 0.5231422782 0.7270067930 - 0.7568811178 0.5248072147 0.7282580733 - 0.7580674291 0.5264676809 0.7294902802 - 0.7592449188 0.5281187296 0.7307134867 - 0.7604126930 0.5297662020 0.7319198847 - 0.7615684271 0.5314055085 0.7331190705 - 0.7627115250 0.5330424905 0.7343053818 - 0.7638474703 0.5346732140 0.7354832292 - 0.7649760246 0.5363014936 0.7366452217 - 0.7660958171 0.5379210711 0.7378004193 - 0.7672052979 0.5395383835 0.7389414907 - 0.7683075070 0.5411432981 0.7400768995 - 0.7693994045 0.5427473187 0.7411974072 - 0.7704840899 0.5443403125 0.7423136234 - 0.7715560198 0.5459334254 0.7434149981 - 0.7726234198 0.5475159883 0.7445133924 - 0.7736793756 0.5490983725 0.7455939054 - 0.7747321129 0.5506709218 0.7466735840 - 0.7757707238 0.5522418022 0.7477390766 - 0.7768080235 0.5538048148 0.7488046288 - 0.7778319120 0.5553649068 0.7498521805 - 0.7788556814 0.5569189787 0.7508987784 - 0.7798684835 0.5584686995 0.7519304156 - 0.7808806896 0.5600128770 0.7529587746 - 0.7818831801 0.5615490079 0.7539767027 - 0.7828834057 0.5630828738 0.7549896240 - 0.7838739753 0.5646088123 0.7559961081 - 0.7848594189 0.5661345124 0.7569965720 - 0.7858390212 0.5676507950 0.7579910159 - 0.7868121266 0.5691671968 0.7589733005 - 0.7877818942 0.5706741810 0.7599534988 - 0.7887430787 0.5721786022 0.7609199286 - 0.7897034883 0.5736780763 0.7618862987 - 0.7906509042 0.5751733780 0.7628408074 - 0.7915983796 0.5766646862 0.7637941837 - 0.7925338149 0.5781458020 0.7647387981 - 0.7934665084 0.5796269774 0.7656794190 - 0.7943943739 0.5810970068 0.7666130066 - 0.7953184843 0.5825669765 0.7675375938 - 0.7962388992 0.5840300918 0.7684599161 - 0.7971503735 0.5854902267 0.7693706155 - 0.7980619073 0.5869455934 0.7702813148 - 0.7989597917 0.5883936286 0.7711808085 - 0.7998577952 0.5898411870 0.7720786929 - 0.8007482290 0.5912801027 0.7729684114 - 0.8016350865 0.5927190185 0.7738516927 - 0.8025193214 0.5941507220 0.7747313976 - 0.8033987880 0.5955792069 0.7755991817 - 0.8042780757 0.5970045924 0.7764670849 - 0.8051458001 0.5984227061 0.7773265839 - 0.8060135841 0.5998408794 0.7781848907 - 0.8068729043 0.6012483835 0.7790384293 - 0.8077275753 0.6026548743 0.7798873782 - 0.8085805774 0.6040555239 0.7807345986 - 0.8094288707 0.6054502726 0.7815697193 - 0.8102772236 0.6068443060 0.7824047208 - 0.8111171126 0.6082274914 0.7832310796 - 0.8119553924 0.6096106768 0.7840542197 - 0.8127890825 0.6109865904 0.7848746181 - 0.8136162758 0.6123577952 0.7856888771 - 0.8144435287 0.6137276292 0.7865030766 - 0.8152586222 0.6150876284 0.7873069048 - 0.8160732985 0.6164476275 0.7881088853 - 0.8168833852 0.6178010106 0.7889056802 - 0.8176890016 0.6191505790 0.7896941900 - 0.8184943795 0.6204990745 0.7904825807 - 0.8192909956 0.6218383908 0.7912638187 - 0.8200876117 0.6231778264 0.7920442224 - 0.8208791018 0.6245108843 0.7928205132 - 0.8216655254 0.6258392930 0.7935903072 - 0.8224520087 0.6271675229 0.7943601012 - 0.8232241869 0.6284862757 0.7951198816 - 0.8239961267 0.6298050880 0.7958778739 - 0.8247646093 0.6311200261 0.7966322899 - 0.8255286217 0.6324295998 0.7973783016 - 0.8262926936 0.6337392926 0.7981241941 - 0.8270503879 0.6350398064 0.7988656759 - 0.8278068900 0.6363382936 0.7996054888 - 0.8285604119 0.6376342773 0.8003445864 - 0.8293051124 0.6389204264 0.8010787964 - 0.8300496936 0.6402065158 0.8018130064 - 0.8307862282 0.6414867043 0.8025422096 - 0.8315181732 0.6427623034 0.8032668829 - 0.8322501779 0.6440379024 0.8039916158 - 0.8329762220 0.6453067064 0.8047072291 - 0.8337022066 0.6465746760 0.8054215908 - 0.8344250917 0.6478404999 0.8061349988 - 0.8351423144 0.6490973830 0.8068444729 - 0.8358594775 0.6503543854 0.8075541258 - 0.8365702033 0.6516067982 0.8082594872 - 0.8372778296 0.6528545022 0.8089613914 - 0.8379853964 0.6541022062 0.8096632957 - 0.8386828899 0.6553429961 0.8103563190 - 0.8393800855 0.6565815210 0.8110477924 - 0.8400756717 0.6578201056 0.8117380738 - 0.8407658935 0.6590459943 0.8124209046 - 0.8414561749 0.6602718234 0.8131036758 - 0.8421434164 0.6614953279 0.8137840033 - 0.8428274989 0.6627100110 0.8144602180 - 0.8435115814 0.6639245749 0.8151363730 - 0.8441904783 0.6651350260 0.8158075213 - 0.8448675871 0.6663392186 0.8164752126 - 0.8455445766 0.6675434113 0.8171430230 - 0.8462108970 0.6687409282 0.8178033233 - 0.8468765020 0.6699336171 0.8184620142 - 0.8475412130 0.6711263061 0.8191205859 - 0.8481993079 0.6723105907 0.8197690845 - 0.8488574028 0.6734917164 0.8204175830 - 0.8495146036 0.6746727824 0.8210650086 - 0.8501691818 0.6758468151 0.8217062950 - 0.8508237004 0.6770195961 0.8223475218 - 0.8514751196 0.6781923771 0.8229870796 - 0.8521217108 0.6793540716 0.8236224055 - 0.8527683020 0.6805154085 0.8242577910 - 0.8534088731 0.6816763878 0.8248903751 - 0.8540437818 0.6828271747 0.8255187869 - 0.8546788096 0.6839780807 0.8261471987 - 0.8553087115 0.6851279736 0.8267704844 - 0.8559352756 0.6862679720 0.8273885846 - 0.8565620184 0.6874079108 0.8280068040 - 0.8571853042 0.6885467172 0.8286191225 - 0.8578070998 0.6896767020 0.8292267919 - 0.8584287763 0.6908066869 0.8298345208 - 0.8590450287 0.6919351220 0.8304392099 - 0.8596587777 0.6930524111 0.8310418725 - 0.8602727056 0.6941698194 0.8316444755 - 0.8608806729 0.6952862144 0.8322427273 - 0.8614864945 0.6963943839 0.8328379989 - 0.8620923162 0.6975026727 0.8334332705 - 0.8626918793 0.6986103058 0.8340228796 - 0.8632891774 0.6997066736 0.8346086740 - 0.8638864756 0.7008031011 0.8351945281 - 0.8644778132 0.7018995285 0.8357760906 - 0.8650662899 0.7029855251 0.8363544941 - 0.8656548858 0.7040715218 0.8369328976 - 0.8662387133 0.7051573992 0.8375086188 - 0.8668196201 0.7062337995 0.8380817771 - 0.8674005866 0.7073091269 0.8386549950 - 0.8679792285 0.7083843946 0.8392254114 - 0.8685559034 0.7094525099 0.8397920728 - 0.8691325784 0.7105184197 0.8403587937 - 0.8697057962 0.7115843296 0.8409234285 - 0.8702744246 0.7126430273 0.8414832950 - 0.8708429933 0.7136971951 0.8420432806 - 0.8714082241 0.7147514820 0.8426018953 - 0.8719649911 0.7158018947 0.8431546092 - 0.8725218773 0.7168475986 0.8437072039 - 0.8730772734 0.7178933024 0.8442596197 - 0.8736236095 0.7189357281 0.8448042274 - 0.8741698265 0.7199681997 0.8453487754 - 0.8747161031 0.7210006714 0.8458935022 - 0.8752585053 0.7220329046 0.8464329243 - 0.8758007288 0.7230560184 0.8469712138 - 0.8763428926 0.7240790725 0.8475095034 - 0.8768798113 0.7251021862 0.8480458260 - 0.8774148822 0.7261161804 0.8485807776 - 0.8779498935 0.7271276116 0.8491157293 - 0.8784800172 0.7281389832 0.8496479988 - 0.8790047765 0.7291454077 0.8501759768 - 0.8795297146 0.7301467061 0.8507040143 - 0.8800523281 0.7311478853 0.8512308002 - 0.8805670738 0.7321473956 0.8517482877 - 0.8810818791 0.7331386805 0.8522657752 - 0.8815966845 0.7341300249 0.8527832031 - 0.8821054101 0.7351213098 0.8532953858 - 0.8826135993 0.7361055017 0.8538061976 - 0.8831217885 0.7370883822 0.8543171287 - 0.8836262226 0.7380713820 0.8548266292 - 0.8841279745 0.7390490174 0.8553346992 - 0.8846297860 0.7400209904 0.8558428288 - 0.8851295710 0.7409930229 0.8563500047 - 0.8856232762 0.7419641018 0.8568516970 - 0.8861169815 0.7429267764 0.8573533893 - 0.8866106868 0.7438895106 0.8578552008 - 0.8870999217 0.7448523045 0.8583500981 - 0.8875883818 0.7458072901 0.8588429093 - 0.8880769014 0.7467586994 0.8593356013 - 0.8885614872 0.7477099895 0.8598263264 - 0.8890416026 0.7486596704 0.8603129983 - 0.8895217776 0.7496036887 0.8607997894 - 0.8900015950 0.7505478263 0.8612865806 - 0.8904702067 0.7514917850 0.8617706895 - 0.8909388185 0.7524288297 0.8622545004 - 0.8914073706 0.7533634901 0.8627383709 - 0.8918703794 0.7542982101 0.8632197976 - 0.8923289776 0.7552309036 0.8636981845 - 0.8927875757 0.7561575770 0.8641765714 - 0.8932459950 0.7570843101 0.8646550179 - 0.8937010765 0.7580109239 0.8651264906 - 0.8941562176 0.7589309812 0.8655977249 - 0.8946112990 0.7598484159 0.8660690188 - 0.8950629830 0.7607657909 0.8665379882 - 0.8955119848 0.7616819739 0.8670043945 - 0.8959609866 0.7625917792 0.8674706817 - 0.8964099884 0.7635017037 0.8679370880 - 0.8968498707 0.7644115090 0.8683987260 - 0.8972896934 0.7653169036 0.8688598871 - 0.8977295756 0.7662187219 0.8693211079 - 0.8981648088 0.7671204805 0.8697806001 - 0.8985937238 0.7680222988 0.8702368736 - 0.8990225792 0.7689167857 0.8706930876 - 0.8994513750 0.7698106766 0.8711493015 - 0.8998739719 0.7707046270 0.8716015220 - 0.9002950788 0.7715961933 0.8720520139 - 0.9007161260 0.7724819183 0.8725025058 - 0.9011359811 0.7733675241 0.8729526997 - 0.9015498161 0.7742530704 0.8733996153 - 0.9019635916 0.7751324177 0.8738464713 - 0.9023774266 0.7760072947 0.8742935061 - 0.9027864933 0.7768821716 0.8747376800 - 0.9031909108 0.7777572274 0.8751780987 - 0.9035953283 0.7786241174 0.8756185174 - 0.9039996862 0.7794898748 0.8760588169 - 0.9043973088 0.7803558111 0.8764961958 - 0.9047932029 0.7812201977 0.8769323230 - 0.9051890969 0.7820760012 0.8773683906 - 0.9055845737 0.7829318047 0.8778045177 - 0.9059756994 0.7837876081 0.8782365918 - 0.9063668251 0.7846404910 0.8786684871 - 0.9067577720 0.7854884267 0.8791003823 - 0.9071465731 0.7863361835 0.8795318007 - 0.9075301886 0.7871841192 0.8799616098 - 0.9079139233 0.7880274057 0.8803914189 - 0.9082974792 0.7888671756 0.8808211088 - 0.9086759090 0.7897068858 0.8812487721 - 0.9090495110 0.7905467153 0.8816735744 - 0.9094231129 0.7913808823 0.8820983171 - 0.9097967148 0.7922129035 0.8825231194 - 0.9101623893 0.7930449247 0.8829430938 - 0.9105246067 0.7938770056 0.8833600283 - 0.9108868241 0.7947024107 0.8837769032 - 0.9112489223 0.7955265045 0.8841937780 - 0.9116070271 0.7963507175 0.8846076131 - 0.9119641185 0.7971748710 0.8850203753 - 0.9123213291 0.7979897261 0.8854331970 - 0.9126784801 0.7988039255 0.8858460188 - 0.9130293131 0.7996181846 0.8862584829 - 0.9133797288 0.8004323244 0.8866708279 - 0.9137300849 0.8012365103 0.8870831728 - 0.9140803218 0.8020406961 0.8874955773 - 0.9144240022 0.8028448224 0.8879050016 - 0.9147678018 0.8036485910 0.8883141279 - 0.9151114821 0.8044434786 0.8887231946 - 0.9154546857 0.8052384853 0.8891323209 - 0.9157909155 0.8060333729 0.8895354867 - 0.9161270857 0.8068280220 0.8899384737 - 0.9164631963 0.8076159954 0.8903415799 - 0.9167991281 0.8084039092 0.8907446265 - 0.9171310067 0.8091918826 0.8911405206 - 0.9174628854 0.8099797964 0.8915362954 - 0.9177948236 0.8107593060 0.8919320703 - 0.9181262851 0.8115388155 0.8923279047 - 0.9184517860 0.8123183250 0.8927236199 - 0.9187772870 0.8130977750 0.8931193948 - 0.9191027880 0.8138713241 0.8935151100 - 0.9194281101 0.8146445751 0.8939108253 - 0.9197450280 0.8154178262 0.8943054080 - 0.9200618863 0.8161910176 0.8946998715 - 0.9203788042 0.8169561028 0.8950942755 - 0.9206957221 0.8177195787 0.8954887986 - 0.9210032225 0.8184831738 0.8958798051 - 0.9213100076 0.8192468286 0.8962702751 - 0.9216167927 0.8200044036 0.8966606855 - 0.9219235778 0.8207594752 0.8970510960 - 0.9222257137 0.8215147257 0.8974366188 - 0.9225267172 0.8222697973 0.8978202939 - 0.9228277206 0.8230199218 0.8982040286 - 0.9231287837 0.8237658143 0.8985875845 - 0.9234266877 0.8245115876 0.8989667892 - 0.9237229824 0.8252574205 0.8993425965 - 0.9240192771 0.8260008097 0.8997182846 - 0.9243155718 0.8267393112 0.9000940919 - 0.9246088266 0.8274778128 0.9004687071 - 0.9248985052 0.8282163143 0.9008412957 - 0.9251881242 0.8289538026 0.9012138844 - 0.9254776835 0.8296815753 0.9015864730 - 0.9257658720 0.8304092884 0.9019587040 - 0.9260491729 0.8311371207 0.9023287296 - 0.9263325930 0.8318648934 0.9026985765 - 0.9266158938 0.8325855732 0.9030684829 - 0.9268993139 0.8333047032 0.9034383893 - 0.9271755219 0.8340237737 0.9038041234 - 0.9274516106 0.8347429037 0.9041693807 - 0.9277275801 0.8354570270 0.9045346975 - 0.9280037284 0.8361662030 0.9048998952 - 0.9282758236 0.8368754983 0.9052616954 - 0.9285460711 0.8375847936 0.9056211710 - 0.9288163185 0.8382927775 0.9059805870 - 0.9290866256 0.8389946818 0.9063401222 - 0.9293550253 0.8396965861 0.9066985846 - 0.9296200871 0.8403984904 0.9070546031 - 0.9298850894 0.8411002755 0.9074106216 - 0.9301502109 0.8417940140 0.9077665210 - 0.9304152131 0.8424854279 0.9081224799 - 0.9306733012 0.8431769013 0.9084752798 - 0.9309315085 0.8438683152 0.9088277817 - 0.9311895967 0.8445565104 0.9091802239 - 0.9314476848 0.8452389836 0.9095327258 - 0.9317021966 0.8459215164 0.9098836184 - 0.9319543242 0.8466041088 0.9102330208 - 0.9322063923 0.8472865820 0.9105823040 - 0.9324585199 0.8479604125 0.9109317064 - 0.9327095151 0.8486334085 0.9112808108 - 0.9329549074 0.8493062854 0.9116259813 - 0.9332001805 0.8499792814 0.9119712710 - 0.9334455132 0.8506484032 0.9123165011 - 0.9336907864 0.8513122201 0.9126616716 - 0.9339326024 0.8519759774 0.9130048156 - 0.9341726899 0.8526396751 0.9133464098 - 0.9344127178 0.8533034921 0.9136881232 - 0.9346528053 0.8539575934 0.9140297771 - 0.9348924756 0.8546106219 0.9143710732 - 0.9351304173 0.8552635908 0.9147090912 - 0.9353682995 0.8559166193 0.9150471091 - 0.9356061816 0.8565670252 0.9153851271 - 0.9358440042 0.8572118878 0.9157230854 - 0.9360798001 0.8578568101 0.9160578847 - 0.9363142848 0.8585017920 0.9163900018 - 0.9365487099 0.8591467142 0.9167221189 - 0.9367831945 0.8597838283 0.9170541763 - 0.9370173812 0.8604179025 0.9173862934 - 0.9372467995 0.8610520959 0.9177129269 - 0.9374760985 0.8616862893 0.9180392027 - 0.9377055168 0.8623197079 0.9183654785 - 0.9379348159 0.8629447818 0.9186918139 - 0.9381616116 0.8635699153 0.9190167785 - 0.9383847117 0.8641949892 0.9193391204 - 0.9386078119 0.8648201227 0.9196614027 - 0.9388309717 0.8654413819 0.9199838042 - 0.9390540719 0.8660572171 0.9203060865 - 0.9392737150 0.8666728735 0.9206275940 - 0.9394921064 0.8672885895 0.9209488034 - 0.9397104979 0.8679043055 0.9212698936 - 0.9399290085 0.8685140014 0.9215909839 - 0.9401472211 0.8691210151 0.9219120741 - 0.9403619170 0.8697279096 0.9222300053 - 0.9405764937 0.8703348041 0.9225476980 - 0.9407910705 0.8709418178 0.9228653908 - 0.9410057068 0.8715392947 0.9231832027 - 0.9412195086 0.8721361756 0.9234995842 - 0.9414315224 0.8727331161 0.9238117933 - 0.9416434169 0.8733298779 0.9241240025 - 0.9418553114 0.8739258051 0.9244362116 - 0.9420672059 0.8745154738 0.9247483015 - 0.9422771931 0.8751052022 0.9250574112 - 0.9424853921 0.8756948113 0.9253627062 - 0.9426935911 0.8762844801 0.9256678820 - 0.9429017901 0.8768712878 0.9259731174 - 0.9431099892 0.8774514198 0.9262782931 - 0.9433146119 0.8780314922 0.9265810847 - 0.9435176253 0.8786115050 0.9268823862 - 0.9437206984 0.8791915774 0.9271836877 - 0.9439237118 0.8797677755 0.9274849892 - 0.9441267252 0.8803384900 0.9277862906 - 0.9443246126 0.8809092045 0.9280866981 - 0.9445216060 0.8814799190 0.9283868074 - 0.9447184801 0.8820505738 0.9286869168 - 0.9449154735 0.8826171756 0.9289870262 - 0.9451124072 0.8831797242 0.9292870760 - 0.9453071952 0.8837422132 0.9295849204 - 0.9455018044 0.8843047023 0.9298822880 - 0.9456964731 0.8848671913 0.9301797152 - 0.9458910823 0.8854259849 0.9304770827 - 0.9460856915 0.8859816194 0.9307745099 - 0.9462794065 0.8865371943 0.9310675263 - 0.9464730024 0.8870928288 0.9313601851 - 0.9466665983 0.8876484036 0.9316527843 - 0.9468601942 0.8881986141 0.9319455028 - 0.9470537901 0.8887439966 0.9322381020 - 0.9472450018 0.8892894983 0.9325249791 - 0.9474362731 0.8898350000 0.9328114986 - 0.9476274848 0.8903805017 0.9330980182 - 0.9478188157 0.8909212947 0.9333845973 - 0.9480100870 0.8914574981 0.9336711168 - 0.9481977224 0.8919935822 0.9339528084 - 0.9483852983 0.8925297856 0.9342340231 - 0.9485728741 0.8930659294 0.9345152974 - 0.9487605095 0.8935987949 0.9347965121 - 0.9489480853 0.8941270709 0.9350777864 - 0.9491310716 0.8946554065 0.9353563786 - 0.9493138194 0.8951836824 0.9356346726 - 0.9494963884 0.8957120180 0.9359129071 - 0.9496790767 0.8962380290 0.9361910820 - 0.9498617053 0.8967580795 0.9364693165 - 0.9500396848 0.8972783089 0.9367458224 - 0.9502165914 0.8977984190 0.9370216727 - 0.9503934979 0.8983185291 0.9372975230 - 0.9505705237 0.8988376260 0.9375733733 - 0.9507473707 0.8993477821 0.9378492832 - 0.9509227872 0.8998581171 0.9381229877 - 0.9510974288 0.9003683925 0.9383950830 - 0.9512720704 0.9008787274 0.9386672974 - 0.9514467120 0.9013890028 0.9389393926 - 0.9516214132 0.9018915892 0.9392116070 - 0.9517948031 0.9023934007 0.9394819140 - 0.9519668818 0.9028952718 0.9397495985 - 0.9521389008 0.9033970833 0.9400172234 - 0.9523109794 0.9038988948 0.9402849078 - 0.9524831176 0.9043961763 0.9405525923 - 0.9526547790 0.9048910141 0.9408199191 - 0.9528251886 0.9053859115 0.9410852790 - 0.9529957175 0.9058806896 0.9413505793 - 0.9531661868 0.9063755274 0.9416159987 - 0.9533365965 0.9068669081 0.9418812990 - 0.9535071254 0.9073519111 0.9421467185 - 0.9536749721 0.9078369141 0.9424085021 - 0.9538426995 0.9083219171 0.9426698089 - 0.9540104866 0.9088069201 0.9429311156 - 0.9541782141 0.9092918038 0.9431924224 - 0.9543460011 0.9097663760 0.9434537292 - 0.9545125961 0.9102410078 0.9437128901 - 0.9546784759 0.9107155800 0.9439705014 - 0.9548444748 0.9111902118 0.9442281127 - 0.9550104141 0.9116647840 0.9444857240 - 0.9551764131 0.9121345878 0.9447432160 - 0.9553416967 0.9126017094 0.9449998140 - 0.9555050135 0.9130687118 0.9452524781 - 0.9556683898 0.9135357141 0.9455052018 - 0.9558317065 0.9140027165 0.9457579255 - 0.9559950233 0.9144682884 0.9460105896 - 0.9561583996 0.9149281979 0.9462631941 - 0.9563187957 0.9153881073 0.9465129972 - 0.9564787745 0.9158478975 0.9467620254 - 0.9566386938 0.9163078070 0.9470111132 - 0.9567986727 0.9167675972 0.9472600818 - 0.9569585919 0.9172213078 0.9475091100 - 0.9571176767 0.9176725745 0.9477577806 - 0.9572752714 0.9181237817 0.9480059147 - 0.9574329257 0.9185751081 0.9482539892 - 0.9575905800 0.9190263152 0.9485020041 - 0.9577481747 0.9194753766 0.9487500787 - 0.9579058290 0.9199153781 0.9489980936 - 0.9580608010 0.9203554988 0.9492439032 - 0.9582154751 0.9207956195 0.9494892955 - 0.9583702087 0.9212356210 0.9497346878 - 0.9585248828 0.9216756821 0.9499800801 - 0.9586796165 0.9221099019 0.9502254725 - 0.9588336945 0.9225407243 0.9504696131 - 0.9589865208 0.9229714274 0.9507107139 - 0.9591394067 0.9234021902 0.9509518147 - 0.9592921734 0.9238328934 0.9511927962 - 0.9594451189 0.9242635965 0.9514338970 - 0.9595978856 0.9246863127 0.9516749978 - 0.9597501755 0.9251089096 0.9519119263 - 0.9599022865 0.9255316257 0.9521470070 - 0.9600545168 0.9259542823 0.9523820877 - 0.9602066278 0.9263768792 0.9526172280 - 0.9603586793 0.9267967939 0.9528523088 - 0.9605106115 0.9272115231 0.9530869722 - 0.9606612921 0.9276261926 0.9533157945 - 0.9608119726 0.9280409217 0.9535444975 - 0.9609627128 0.9284554720 0.9537733197 - 0.9611135125 0.9288702011 0.9540020227 - 0.9612641931 0.9292793870 0.9542307854 - 0.9614138007 0.9296854138 0.9544587135 - 0.9615620971 0.9300913811 0.9546853900 - 0.9617102742 0.9304975271 0.9549120069 - 0.9618585706 0.9309034944 0.9551386237 - 0.9620069265 0.9313095808 0.9553651810 - 0.9621551037 0.9317073822 0.9555919170 - 0.9623023272 0.9321041703 0.9558166265 - 0.9624490738 0.9325010180 0.9560402036 - 0.9625958204 0.9328979254 0.9562637806 - 0.9627425075 0.9332947135 0.9564874768 - 0.9628893137 0.9336907268 0.9567111135 - 0.9630360007 0.9340782762 0.9569346905 - 0.9631810784 0.9344660044 0.9571558833 - 0.9633259177 0.9348536134 0.9573767185 - 0.9634708166 0.9352412820 0.9575974941 - 0.9636157155 0.9356290102 0.9578183293 - 0.9637606144 0.9360147715 0.9580391049 - 0.9639053941 0.9363946915 0.9582598209 - 0.9640493989 0.9367746115 0.9584767222 - 0.9641934037 0.9371545911 0.9586936831 - 0.9643375278 0.9375345111 0.9589105844 - 0.9644814730 0.9379143715 0.9591274858 - 0.9646254778 0.9382917881 0.9593443871 - 0.9647691846 0.9386643171 0.9595605731 - 0.9649112821 0.9390369058 0.9597727060 - 0.9650534987 0.9394093752 0.9599847794 - 0.9651957154 0.9397819042 0.9601967931 - 0.9653378725 0.9401543736 0.9604089260 - 0.9654800296 0.9405236244 0.9606209993 - 0.9656214714 0.9408878088 0.9608321786 - 0.9657613039 0.9412518740 0.9610404968 - 0.9659010172 0.9416161180 0.9612486959 - 0.9660406709 0.9419801831 0.9614568949 - 0.9661805034 0.9423443079 0.9616652131 - 0.9663202167 0.9427043796 0.9618734121 - 0.9664595127 0.9430581927 0.9620807767 - 0.9665976763 0.9434120059 0.9622855186 - 0.9667360187 0.9437658191 0.9624903202 - 0.9668741822 0.9441195726 0.9626950026 - 0.9670125246 0.9444733858 0.9628996849 - 0.9671506882 0.9448243976 0.9631044865 - 0.9672883153 0.9451701045 0.9633080959 - 0.9674242139 0.9455156922 0.9635080099 - 0.9675601125 0.9458613992 0.9637079835 - 0.9676958919 0.9462071061 0.9639078975 - 0.9678317904 0.9465526938 0.9641078115 - 0.9679676890 0.9468963742 0.9643077254 - 0.9681032896 0.9472342134 0.9645069838 - 0.9682378173 0.9475718737 0.9647027254 - 0.9683722854 0.9479095936 0.9648982882 - 0.9685068130 0.9482473135 0.9650939703 - 0.9686412811 0.9485850930 0.9652895927 - 0.9687758088 0.9489220977 0.9654852748 - 0.9689102173 0.9492530823 0.9656807780 - 0.9690427184 0.9495840073 0.9658718705 - 0.9691752791 0.9499149919 0.9660630822 - 0.9693077803 0.9502459764 0.9662541747 - 0.9694404006 0.9505769014 0.9664453864 - 0.9695729017 0.9509078860 0.9666364789 - 0.9697054029 0.9512314796 0.9668276906 - 0.9698356986 0.9515542984 0.9670159221 - 0.9699656963 0.9518771172 0.9672036171 - 0.9700956941 0.9521998763 0.9673913717 - 0.9702258110 0.9525226951 0.9675791264 - 0.9703558087 0.9528455138 0.9677668214 - 0.9704858065 0.9531627893 0.9679545164 - 0.9706159830 0.9534772038 0.9681413770 - 0.9707463980 0.9537916780 0.9683275819 - 0.9708768129 0.9541060925 0.9685139060 - 0.9710071087 0.9544205070 0.9687001705 - 0.9711375237 0.9547349215 0.9688864946 - 0.9712678790 0.9550464153 0.9690728188 - 0.9713981152 0.9553524256 0.9692583084 - 0.9715282917 0.9556583166 0.9694423079 - 0.9716584086 0.9559643269 0.9696264267 - 0.9717885256 0.9562702775 0.9698104262 - 0.9719185829 0.9565762281 0.9699944854 - 0.9720488191 0.9568821788 0.9701784849 - 0.9721788168 0.9571802020 0.9703624845 - 0.9723080993 0.9574782252 0.9705430865 - 0.9724375010 0.9577761292 0.9707236886 - 0.9725667834 0.9580740929 0.9709041715 - 0.9726961255 0.9583721161 0.9710847735 - 0.9728254080 0.9586700201 0.9712653756 - 0.9729546905 0.9589635134 0.9714459181 - 0.9730840921 0.9592537880 0.9716234803 - 0.9732134938 0.9595441222 0.9717993140 - 0.9733428955 0.9598343968 0.9719752073 - 0.9734724164 0.9601246715 0.9721509814 - 0.9736018181 0.9604150057 0.9723268151 - 0.9737312198 0.9607043862 0.9725025892 - 0.9738605022 0.9609869719 0.9726774096 - 0.9739891887 0.9612696767 0.9728478789 - 0.9741178751 0.9615523219 0.9730182886 - 0.9742466211 0.9618350267 0.9731886983 - 0.9743753076 0.9621176124 0.9733592272 - 0.9745039940 0.9624003172 0.9735295773 - 0.9746326804 0.9626781940 0.9737001061 - 0.9747601748 0.9629530907 0.9738674760 - 0.9748873115 0.9632279873 0.9740337133 - 0.9750145078 0.9635028839 0.9742000103 - 0.9751415849 0.9637778997 0.9743661880 - 0.9752687216 0.9640527964 0.9745324254 - 0.9753959179 0.9643273950 0.9746986032 - 0.9755228162 0.9645949006 0.9748641849 - 0.9756492972 0.9648625255 0.9750267267 - 0.9757757187 0.9651299715 0.9751893282 - 0.9759021997 0.9653975964 0.9753518105 - 0.9760286212 0.9656651020 0.9755142927 - 0.9761551023 0.9659326077 0.9756767750 - 0.9762815237 0.9661971927 0.9758393764 - 0.9764071703 0.9664574862 0.9760001898 - 0.9765323997 0.9667177796 0.9761596918 - 0.9766576290 0.9669780731 0.9763193130 - 0.9767829180 0.9672384262 0.9764788151 - 0.9769080877 0.9674987793 0.9766383767 - 0.9770333171 0.9677590728 0.9767978787 - 0.9771584868 0.9680131078 0.9769575000 - 0.9772819877 0.9682648778 0.9771137238 - 0.9774054885 0.9685167074 0.9772697091 - 0.9775289297 0.9687684774 0.9774256945 - 0.9776523709 0.9690203071 0.9775816798 - 0.9777758121 0.9692721963 0.9777377248 - 0.9778991938 0.9695239067 0.9778935909 - 0.9780225158 0.9697673917 0.9780486822 - 0.9781451225 0.9700108767 0.9782000184 - 0.9782677889 0.9702544212 0.9783512950 - 0.9783903956 0.9704979062 0.9785025716 - 0.9785130024 0.9707413912 0.9786539078 - 0.9786357284 0.9709848166 0.9788051844 - 0.9787582755 0.9712265730 0.9789565802 - 0.9788810015 0.9714620709 0.9791055918 - 0.9790037870 0.9716976881 0.9792516828 - 0.9791265130 0.9719331861 0.9793978930 - 0.9792491794 0.9721686840 0.9795439839 - 0.9793720245 0.9724041820 0.9796900749 - 0.9794946909 0.9726397991 0.9798362255 - 0.9796174765 0.9728724957 0.9799823761 - 0.9797399044 0.9731004834 0.9801260829 - 0.9798622727 0.9733284712 0.9802682996 - 0.9799845815 0.9735565186 0.9804103971 - 0.9801068902 0.9737845063 0.9805526137 - 0.9802291989 0.9740126133 0.9806947708 - 0.9803515077 0.9742406011 0.9808369875 - 0.9804738164 0.9744648933 0.9809792042 - 0.9805954099 0.9746851921 0.9811189175 - 0.9807168245 0.9749054909 0.9812576771 - 0.9808381200 0.9751256704 0.9813964963 - 0.9809594750 0.9753460288 0.9815353155 - 0.9810807705 0.9755662084 0.9816740751 - 0.9812021852 0.9757865071 0.9818128943 - 0.9813234806 0.9760035872 0.9819517136 - 0.9814447761 0.9762178063 0.9820870757 - 0.9815660119 0.9764320254 0.9822216034 - 0.9816873074 0.9766461849 0.9823560715 - 0.9818086028 0.9768604040 0.9824907184 - 0.9819297791 0.9770746231 0.9826251864 - 0.9820510745 0.9772887230 0.9827597737 - 0.9821723104 0.9774991870 0.9828943014 - 0.9822927713 0.9777060151 0.9830257893 - 0.9824131727 0.9779127240 0.9831566215 - 0.9825335145 0.9781193733 0.9832873940 - 0.9826539159 0.9783260822 0.9834182262 - 0.9827741981 0.9785327911 0.9835489988 - 0.9828945994 0.9787396193 0.9836797714 - 0.9830150008 0.9789426923 0.9838104844 - 0.9831339717 0.9791409969 0.9839380980 - 0.9832527041 0.9793391824 0.9840646982 - 0.9833713174 0.9795374274 0.9841911793 - 0.9834899902 0.9797356725 0.9843177795 - 0.9836087227 0.9799339175 0.9844443798 - 0.9837273955 0.9801321030 0.9845709205 - 0.9838461280 0.9803280830 0.9846975207 - 0.9839642048 0.9805182219 0.9848210216 - 0.9840821028 0.9807084203 0.9849429727 - 0.9841998816 0.9808984995 0.9850649834 - 0.9843177795 0.9810886979 0.9851869941 - 0.9844356775 0.9812787771 0.9853090048 - 0.9845535755 0.9814689755 0.9854310155 - 0.9846714139 0.9816585183 0.9855529070 - 0.9847885966 0.9818410873 0.9856733084 - 0.9849051237 0.9820237756 0.9857919216 - 0.9850217104 0.9822065234 0.9859105945 - 0.9851381779 0.9823890924 0.9860293269 - 0.9852548242 0.9825717807 0.9861479998 - 0.9853712916 0.9827544093 0.9862666130 - 0.9854878783 0.9829370975 0.9863852859 - 0.9856039286 0.9831125140 0.9865033031 - 0.9857186079 0.9832866192 0.9866192937 - 0.9858332872 0.9834606051 0.9867352843 - 0.9859480262 0.9836345911 0.9868512750 - 0.9860628247 0.9838085771 0.9869673252 - 0.9861775041 0.9839826226 0.9870833158 - 0.9862921834 0.9841566086 0.9871993065 - 0.9864069223 0.9843258858 0.9873152971 - 0.9865176082 0.9844909906 0.9874280095 - 0.9866282940 0.9846559763 0.9875406027 - 0.9867389202 0.9848210812 0.9876531959 - 0.9868496060 0.9849861264 0.9877659082 - 0.9869602919 0.9851511717 0.9878785014 - 0.9870709777 0.9853162169 0.9879910946 - 0.9871817231 0.9854801893 0.9881036878 - 0.9872913957 0.9856383204 0.9882147908 - 0.9874005914 0.9857963920 0.9883249998 - 0.9875099063 0.9859545231 0.9884350896 - 0.9876191020 0.9861125946 0.9885452986 - 0.9877284169 0.9862707257 0.9886553884 - 0.9878376126 0.9864287972 0.9887655973 - 0.9879468083 0.9865869284 0.9888756871 - 0.9880558252 0.9867392778 0.9889851809 - 0.9881638288 0.9868894815 0.9890925288 - 0.9882717133 0.9870396852 0.9891998172 - 0.9883797169 0.9871898890 0.9893071055 - 0.9884877205 0.9873400927 0.9894143939 - 0.9885957241 0.9874902964 0.9895216227 - 0.9887037277 0.9876403809 0.9896289110 - 0.9888116717 0.9877886772 0.9897361994 - 0.9889186025 0.9879299998 0.9898411036 - 0.9890251756 0.9880713820 0.9899451733 - 0.9891318083 0.9882127047 0.9900494218 - 0.9892383814 0.9883540869 0.9901534915 - 0.9893450141 0.9884954095 0.9902576804 - 0.9894515276 0.9886367917 0.9903618097 - 0.9895581007 0.9887781143 0.9904659986 - 0.9896646142 0.9889159203 0.9905695915 - 0.9897707105 0.9890518188 0.9906715155 - 0.9898768067 0.9891877174 0.9907733798 - 0.9899830222 0.9893234968 0.9908754230 - 0.9900891185 0.9894593954 0.9909772873 - 0.9901952147 0.9895952940 0.9910792112 - 0.9903013110 0.9897311926 0.9911811948 - 0.9904074073 0.9898669720 0.9912831187 - 0.9905129075 0.9899997115 0.9913834929 - 0.9906179905 0.9901322722 0.9914830923 - 0.9907230139 0.9902650118 0.9915826917 - 0.9908280969 0.9903976917 0.9916822910 - 0.9909331799 0.9905303121 0.9917818904 - 0.9910383224 0.9906629920 0.9918814898 - 0.9911434054 0.9907956719 0.9919810891 - 0.9912484288 0.9909266829 0.9920806885 - 0.9913520217 0.9910547137 0.9921786189 - 0.9914554954 0.9911826849 0.9922763705 - 0.9915590286 0.9913107157 0.9923741817 - 0.9916625023 0.9914388061 0.9924721122 - 0.9917659760 0.9915667772 0.9925699234 - 0.9918695092 0.9916948080 0.9926676750 - 0.9919729829 0.9918227792 0.9927656054 - 0.9920759201 0.9919472933 0.9928627014 - 0.9921774268 0.9920694232 0.9929581881 - 0.9922788739 0.9921916127 0.9930536151 - 0.9923803806 0.9923136830 0.9931489825 - 0.9924818873 0.9924358726 0.9932444096 - 0.9925833941 0.9925580025 0.9933397770 - 0.9926849008 0.9926801920 0.9934352040 - 0.9927864075 0.9928023219 0.9935305715 - 0.9928867221 0.9929195046 0.9936246276 - 0.9929860234 0.9930353165 0.9937170148 - 0.9930852056 0.9931510091 0.9938094020 - 0.9931843877 0.9932668209 0.9939017892 - 0.9932836294 0.9933825731 0.9939941764 - 0.9933828115 0.9934983850 0.9940866828 - 0.9934821129 0.9936141968 0.9941791296 - 0.9935812950 0.9937298894 0.9942715168 - 0.9936792254 0.9938406944 0.9943618178 - 0.9937763214 0.9939510822 0.9944508076 - 0.9938734174 0.9940614104 0.9945397973 - 0.9939705729 0.9941716790 0.9946289062 - 0.9940677285 0.9942820072 0.9947178960 - 0.9941648245 0.9943923950 0.9948070049 - 0.9942619801 0.9945027232 0.9948959947 - 0.9943590760 0.9946129918 0.9949849844 - 0.9944550991 0.9947180152 0.9950715899 - 0.9945507050 0.9948229194 0.9951571822 - 0.9946463108 0.9949277043 0.9952427745 - 0.9947419167 0.9950326085 0.9953284264 - 0.9948375225 0.9951373935 0.9954140186 - 0.9949331284 0.9952422976 0.9954996109 - 0.9950286150 0.9953472018 0.9955852032 - 0.9951242208 0.9954519868 0.9956707954 - 0.9952185154 0.9955508113 0.9957543015 - 0.9953123927 0.9956492782 0.9958369732 - 0.9954062104 0.9957479239 0.9959197044 - 0.9955000281 0.9958463907 0.9960023761 - 0.9955939054 0.9959449172 0.9960849881 - 0.9956877232 0.9960435033 0.9961677194 - 0.9957814813 0.9961420298 0.9962503910 - 0.9958754182 0.9962404966 0.9963331223 - 0.9959679842 0.9963330030 0.9964141846 - 0.9960600734 0.9964246750 0.9964945912 - 0.9961522818 0.9965164065 0.9965748787 - 0.9962443709 0.9966081977 0.9966552854 - 0.9963365197 0.9966999292 0.9967356920 - 0.9964286089 0.9967916012 0.9968159795 - 0.9965208173 0.9968832731 0.9968963861 - 0.9966129065 0.9969750047 0.9969767928 - 0.9967035055 0.9970617890 0.9970558882 - 0.9967930913 0.9971467853 0.9971340895 - 0.9968826771 0.9972319007 0.9972122908 - 0.9969723821 0.9973170161 0.9972904921 - 0.9970620275 0.9974020123 0.9973688126 - 0.9971516132 0.9974871278 0.9974470139 - 0.9972411990 0.9975721240 0.9975252151 - 0.9973307848 0.9976571798 0.9976034164 - 0.9974188805 0.9977390170 0.9976804852 - 0.9975048900 0.9978179932 0.9977560043 - 0.9975908995 0.9978969097 0.9978315234 - 0.9976769090 0.9979758263 0.9979069233 - 0.9977629185 0.9980546832 0.9979823828 - 0.9978489280 0.9981337190 0.9980579019 - 0.9979348779 0.9982125759 0.9981334209 - 0.9980208874 0.9982914925 0.9982088208 - 0.9981061220 0.9983687997 0.9982838035 - 0.9981874228 0.9984411001 0.9983559251 - 0.9982687831 0.9985135198 0.9984279871 - 0.9983500838 0.9985858202 0.9985002279 - 0.9984313846 0.9986581802 0.9985722899 - 0.9985126853 0.9987304807 0.9986444116 - 0.9985939860 0.9988029003 0.9987164736 - 0.9986754060 0.9988752007 0.9987885952 - 0.9987567067 0.9989476204 0.9988607764 - 0.9988334179 0.9990134835 0.9989299178 - 0.9989092946 0.9990788102 0.9989984035 - 0.9989851117 0.9991440773 0.9990668893 - 0.9990609884 0.9992092848 0.9991354942 - 0.9991368055 0.9992746115 0.9992039800 - 0.9992126822 0.9993398786 0.9992725253 - 0.9992886186 0.9994050860 0.9993410707 - 0.9993643761 0.9994704127 0.9994096160 - 0.9994376898 0.9995324016 0.9994766712 - 0.9995080233 0.9995908141 0.9995421171 - 0.9995782971 0.9996492863 0.9996075034 - 0.9996485710 0.9997076988 0.9996728897 - 0.9997189045 0.9997661710 0.9997382760 - 0.9997891784 0.9998245835 0.9998037815 - 0.9998593926 0.9998831153 0.9998692274 - 0.9999297261 0.9999415278 0.9999346137 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0010793300 0.0004914269 0.0009736730 + 0.0023187911 0.0010802930 0.0021354880 + 0.0036931380 0.0018072630 0.0034938811 + 0.0052456409 0.0026662690 0.0050411709 + 0.0070081931 0.0036067569 0.0068641612 + 0.0090551702 0.0045785471 0.0090099405 + 0.0114011699 0.0055691102 0.0114547899 + 0.0139678400 0.0065905452 0.0141683398 + 0.0166869499 0.0076627559 0.0171185602 + 0.0195989702 0.0088004395 0.0202584993 + 0.0228006691 0.0100080501 0.0235486198 + 0.0263756197 0.0112880496 0.0269760005 + 0.0303584095 0.0126401298 0.0305433907 + 0.0347159505 0.0140636303 0.0342755318 + 0.0393988304 0.0155540602 0.0381953716 + 0.0443377309 0.0171071608 0.0423245206 + 0.0494562984 0.0187185593 0.0466708317 + 0.0546695217 0.0203871392 0.0512099490 + 0.0599117801 0.0221128706 0.0559065714 + 0.0651446804 0.0238924101 0.0607012585 + 0.0703425035 0.0257253498 0.0655229613 + 0.0754935667 0.0276135001 0.0703299269 + 0.0806027427 0.0295511000 0.0750854388 + 0.0856716335 0.0315340608 0.0797831416 + 0.0907013789 0.0335622691 0.0844218433 + 0.0956865475 0.0356292501 0.0890035927 + 0.1006257012 0.0377341881 0.0935301632 + 0.1055174991 0.0398772992 0.0980233997 + 0.1103760004 0.0420543700 0.1024931967 + 0.1152203977 0.0442650095 0.1069511026 + 0.1200542971 0.0465072691 0.1114012972 + 0.1248871982 0.0487757288 0.1158443019 + 0.1297339052 0.0510716997 0.1202880964 + 0.1346120983 0.0533953197 0.1247389987 + 0.1395314038 0.0557426214 0.1292016953 + 0.1444949955 0.0581145883 0.1336800009 + 0.1495019048 0.0605066083 0.1381773949 + 0.1545559019 0.0629175901 0.1426994056 + 0.1596433967 0.0653483421 0.1472471952 + 0.1647536010 0.0677959695 0.1518259943 + 0.1698786020 0.0702603534 0.1564404070 + 0.1750122011 0.0727384463 0.1610952020 + 0.1801480949 0.0752333701 0.1657952070 + 0.1852789968 0.0777411908 0.1705430001 + 0.1903969944 0.0802624598 0.1753392965 + 0.1954903007 0.0827964321 0.1801753938 + 0.2005587071 0.0853414088 0.1850488037 + 0.2055945992 0.0878966674 0.1899594069 + 0.2105983049 0.0904616788 0.1949048936 + 0.2155726999 0.0930357873 0.1998749971 + 0.2205152959 0.0956183821 0.2048487961 + 0.2254346013 0.0982099175 0.2098221034 + 0.2303334028 0.1008104980 0.2147811055 + 0.2352052927 0.1034198999 0.2197100967 + 0.2400625050 0.1060379967 0.2246035039 + 0.2449066043 0.1086636037 0.2294571996 + 0.2497355938 0.1112938970 0.2342610061 + 0.2545413971 0.1139284000 0.2390076965 + 0.2593365014 0.1165691987 0.2436912954 + 0.2641316950 0.1192142963 0.2483087927 + 0.2689233124 0.1218615994 0.2528623939 + 0.2737050056 0.1245130002 0.2573607862 + 0.2784807086 0.1271678954 0.2618094981 + 0.2832559049 0.1298251003 0.2662059069 + 0.2880277932 0.1324838996 0.2705568075 + 0.2927958071 0.1351446956 0.2748655975 + 0.2975589931 0.1378072053 0.2791439891 + 0.3023168147 0.1404704005 0.2833940089 + 0.3070589006 0.1431359947 0.2876232862 + 0.3117862940 0.1458034068 0.2918379009 + 0.3165079057 0.1484730989 0.2960408032 + 0.3212226927 0.1511427015 0.3002305925 + 0.3259240985 0.1538105011 0.3044033051 + 0.3306060135 0.1564781964 0.3085659146 + 0.3352772892 0.1591459066 0.3127256930 + 0.3399353027 0.1618131995 0.3168801963 + 0.3445748985 0.1644778997 0.3210279047 + 0.3491897881 0.1671431065 0.3251653910 + 0.3537842929 0.1698081940 0.3292970955 + 0.3583635986 0.1724765003 0.3334251940 + 0.3629280031 0.1751464009 0.3375486135 + 0.3674629927 0.1778157055 0.3416658044 + 0.3719677031 0.1804865003 0.3457753956 + 0.3764384985 0.1831578016 0.3498772979 + 0.3808833957 0.1858278960 0.3539676964 + 0.3853051066 0.1885012984 0.3580411971 + 0.3896934986 0.1911745965 0.3620963991 + 0.3940488100 0.1938498020 0.3661319017 + 0.3983697891 0.1965274960 0.3701558113 + 0.4026510119 0.1992076039 0.3741641045 + 0.4068988860 0.2018887997 0.3781496882 + 0.4111185968 0.2045747042 0.3821138144 + 0.4153113961 0.2072620988 0.3860580921 + 0.4194692075 0.2099498957 0.3899829984 + 0.4235900044 0.2126414031 0.3938888013 + 0.4276776910 0.2153348029 0.3977788091 + 0.4317341149 0.2180286050 0.4016534090 + 0.4357563853 0.2207237929 0.4055137932 + 0.4397394061 0.2234221995 0.4093612134 + 0.4436829090 0.2261236012 0.4131959975 + 0.4475947022 0.2288268059 0.4170176983 + 0.4514735937 0.2315322012 0.4208177924 + 0.4553222954 0.2342423052 0.4245958030 + 0.4591388106 0.2369553000 0.4283536971 + 0.4629189968 0.2396683991 0.4320934117 + 0.4666629136 0.2423831969 0.4358170927 + 0.4703747034 0.2451013029 0.4395225048 + 0.4740515947 0.2478200942 0.4432024956 + 0.4776928127 0.2505406141 0.4468582869 + 0.4813019931 0.2532635927 0.4504924119 + 0.4848793149 0.2559860945 0.4541029930 + 0.4884204865 0.2587074935 0.4576902092 + 0.4919295907 0.2614302039 0.4612514973 + 0.4954057038 0.2641544044 0.4647876918 + 0.4988467097 0.2668786943 0.4682984054 + 0.5022568703 0.2696047127 0.4717861116 + 0.5056318045 0.2723301053 0.4752514064 + 0.5089687109 0.2750515938 0.4786919951 + 0.5122699738 0.2777692974 0.4821071923 + 0.5155354738 0.2804855108 0.4855000973 + 0.5187671781 0.2832008898 0.4888701141 + 0.5219681263 0.2859140933 0.4922114909 + 0.5251352191 0.2886242867 0.4955233037 + 0.5282660127 0.2913315892 0.4988074899 + 0.5313646197 0.2940349877 0.5020601749 + 0.5344318151 0.2967323065 0.5052834749 + 0.5374667048 0.2994225919 0.5084816217 + 0.5404701233 0.3021079004 0.5116459131 + 0.5434368849 0.3047888875 0.5147755146 + 0.5463718176 0.3074640036 0.5178748965 + 0.5492841005 0.3101322055 0.5209411979 + 0.5521711707 0.3127948046 0.5239753127 + 0.5550354123 0.3154520094 0.5269787908 + 0.5578734875 0.3181014955 0.5299521089 + 0.5606822968 0.3207401037 0.5328952074 + 0.5634636283 0.3233641088 0.5358104110 + 0.5662226081 0.3259783983 0.5386914015 + 0.5689582229 0.3285818994 0.5415412784 + 0.5716739893 0.3311730921 0.5443618894 + 0.5743725896 0.3337512016 0.5471563935 + 0.5770525932 0.3363159001 0.5499269962 + 0.5797128081 0.3388671875 0.5526747704 + 0.5823537707 0.3414067030 0.5553982854 + 0.5849738717 0.3439315856 0.5580946207 + 0.5875731707 0.3464401960 0.5607615709 + 0.5901582241 0.3489316106 0.5634018183 + 0.5927311778 0.3514032960 0.5660176277 + 0.5952929258 0.3538616896 0.5686088800 + 0.5978369713 0.3563053906 0.5711727738 + 0.6003630161 0.3587335050 0.5737162828 + 0.6028745770 0.3611412942 0.5762403011 + 0.6053730249 0.3635332882 0.5787448287 + 0.6078591943 0.3659127057 0.5812224150 + 0.6103302240 0.3682799935 0.5836719275 + 0.6127892733 0.3706313074 0.5861017108 + 0.6152378917 0.3729673922 0.5885127783 + 0.6176773906 0.3752928078 0.5909010768 + 0.6201043725 0.3776082993 0.5932654738 + 0.6225174069 0.3799093068 0.5956128240 + 0.6249200106 0.3821944892 0.5979433060 + 0.6273151040 0.3844664991 0.6002511978 + 0.6296957135 0.3867239058 0.6025369167 + 0.6320651174 0.3889622986 0.6048055887 + 0.6344231963 0.3911905885 0.6070576906 + 0.6367667913 0.3934093118 0.6092873216 + 0.6390994787 0.3956130147 0.6115016937 + 0.6414197087 0.3978062868 0.6137034297 + 0.6437255740 0.3999901116 0.6158903241 + 0.6460139155 0.4021590948 0.6180559993 + 0.6482884288 0.4043118060 0.6202107072 + 0.6505498290 0.4064539969 0.6223551035 + 0.6527922750 0.4085829854 0.6244797111 + 0.6550189853 0.4106990099 0.6265919805 + 0.6572331190 0.4128079116 0.6286922097 + 0.6594275832 0.4149076939 0.6307719946 + 0.6616020799 0.4169962108 0.6328415275 + 0.6637635827 0.4190776944 0.6349012256 + 0.6659076214 0.4211486876 0.6369373202 + 0.6680328250 0.4232074916 0.6389623880 + 0.6701455712 0.4252586961 0.6409782171 + 0.6722385287 0.4272964001 0.6429752111 + 0.6743144989 0.4293248057 0.6449599862 + 0.6763777733 0.4313467145 0.6469345093 + 0.6784113050 0.4333539903 0.6488881707 + 0.6804267764 0.4353561103 0.6508327723 + 0.6824293137 0.4373523891 0.6527665854 + 0.6844047904 0.4393377006 0.6546779871 + 0.6863657236 0.4413183928 0.6565799713 + 0.6883090734 0.4432905018 0.6584674120 + 0.6902278066 0.4452567101 0.6603385806 + 0.6921337247 0.4472194016 0.6622003913 + 0.6940143704 0.4491713941 0.6640378833 + 0.6958734989 0.4511190057 0.6658635736 + 0.6977202296 0.4530585110 0.6676772237 + 0.6995369792 0.4549886882 0.6694694161 + 0.7013437748 0.4569143951 0.6712527871 + 0.7031323910 0.4588277936 0.6730164886 + 0.7048978806 0.4607372880 0.6747667193 + 0.7066525817 0.4626379907 0.6765061021 + 0.7083770037 0.4645319879 0.6782280803 + 0.7100898027 0.4664215147 0.6799430251 + 0.7117800713 0.4683004022 0.6816384196 + 0.7134491205 0.4701760113 0.6833207011 + 0.7151061296 0.4720404148 0.6849896908 + 0.7167317271 0.4739001989 0.6866384149 + 0.7183483839 0.4757528007 0.6882799864 + 0.7199388742 0.4775973856 0.6898952723 + 0.7215155959 0.4794372022 0.6915025711 + 0.7230750918 0.4812653959 0.6930922270 + 0.7246130705 0.4830907881 0.6946684122 + 0.7261415124 0.4849025011 0.6962333918 + 0.7276425958 0.4867109954 0.6977772713 + 0.7291371226 0.4885095954 0.6993148923 + 0.7306033969 0.4903028011 0.7008234859 + 0.7320588231 0.4920882881 0.7023267150 + 0.7334914207 0.4938659966 0.7038077712 + 0.7349091768 0.4956378937 0.7052788138 + 0.7363126278 0.4973995984 0.7067326903 + 0.7376976013 0.4991573095 0.7081738710 + 0.7390735745 0.5009049773 0.7096043229 + 0.7404264212 0.5026509166 0.7110185027 + 0.7417743206 0.5043882728 0.7124254107 + 0.7430984974 0.5061246753 0.7138100863 + 0.7444195151 0.5078502297 0.7151901722 + 0.7457147241 0.5095742941 0.7165504098 + 0.7470049858 0.5112900138 0.7179082036 + 0.7482746840 0.5130041242 0.7192432284 + 0.7495390773 0.5147119164 0.7205743194 + 0.7507901192 0.5164154172 0.7218819857 + 0.7520323992 0.5181065202 0.7231845856 + 0.7532587051 0.5197932720 0.7244688272 + 0.7544769049 0.5214695930 0.7257465124 + 0.7556836009 0.5231422782 0.7270067930 + 0.7568811178 0.5248072147 0.7282580733 + 0.7580674291 0.5264676809 0.7294902802 + 0.7592449188 0.5281187296 0.7307134867 + 0.7604126930 0.5297662020 0.7319198847 + 0.7615684271 0.5314055085 0.7331190705 + 0.7627115250 0.5330424905 0.7343053818 + 0.7638474703 0.5346732140 0.7354832292 + 0.7649760246 0.5363014936 0.7366452217 + 0.7660958171 0.5379210711 0.7378004193 + 0.7672052979 0.5395383835 0.7389414907 + 0.7683075070 0.5411432981 0.7400768995 + 0.7693994045 0.5427473187 0.7411974072 + 0.7704840899 0.5443403125 0.7423136234 + 0.7715560198 0.5459334254 0.7434149981 + 0.7726234198 0.5475159883 0.7445133924 + 0.7736793756 0.5490983725 0.7455939054 + 0.7747321129 0.5506709218 0.7466735840 + 0.7757707238 0.5522418022 0.7477390766 + 0.7768080235 0.5538048148 0.7488046288 + 0.7778319120 0.5553649068 0.7498521805 + 0.7788556814 0.5569189787 0.7508987784 + 0.7798684835 0.5584686995 0.7519304156 + 0.7808806896 0.5600128770 0.7529587746 + 0.7818831801 0.5615490079 0.7539767027 + 0.7828834057 0.5630828738 0.7549896240 + 0.7838739753 0.5646088123 0.7559961081 + 0.7848594189 0.5661345124 0.7569965720 + 0.7858390212 0.5676507950 0.7579910159 + 0.7868121266 0.5691671968 0.7589733005 + 0.7877818942 0.5706741810 0.7599534988 + 0.7887430787 0.5721786022 0.7609199286 + 0.7897034883 0.5736780763 0.7618862987 + 0.7906509042 0.5751733780 0.7628408074 + 0.7915983796 0.5766646862 0.7637941837 + 0.7925338149 0.5781458020 0.7647387981 + 0.7934665084 0.5796269774 0.7656794190 + 0.7943943739 0.5810970068 0.7666130066 + 0.7953184843 0.5825669765 0.7675375938 + 0.7962388992 0.5840300918 0.7684599161 + 0.7971503735 0.5854902267 0.7693706155 + 0.7980619073 0.5869455934 0.7702813148 + 0.7989597917 0.5883936286 0.7711808085 + 0.7998577952 0.5898411870 0.7720786929 + 0.8007482290 0.5912801027 0.7729684114 + 0.8016350865 0.5927190185 0.7738516927 + 0.8025193214 0.5941507220 0.7747313976 + 0.8033987880 0.5955792069 0.7755991817 + 0.8042780757 0.5970045924 0.7764670849 + 0.8051458001 0.5984227061 0.7773265839 + 0.8060135841 0.5998408794 0.7781848907 + 0.8068729043 0.6012483835 0.7790384293 + 0.8077275753 0.6026548743 0.7798873782 + 0.8085805774 0.6040555239 0.7807345986 + 0.8094288707 0.6054502726 0.7815697193 + 0.8102772236 0.6068443060 0.7824047208 + 0.8111171126 0.6082274914 0.7832310796 + 0.8119553924 0.6096106768 0.7840542197 + 0.8127890825 0.6109865904 0.7848746181 + 0.8136162758 0.6123577952 0.7856888771 + 0.8144435287 0.6137276292 0.7865030766 + 0.8152586222 0.6150876284 0.7873069048 + 0.8160732985 0.6164476275 0.7881088853 + 0.8168833852 0.6178010106 0.7889056802 + 0.8176890016 0.6191505790 0.7896941900 + 0.8184943795 0.6204990745 0.7904825807 + 0.8192909956 0.6218383908 0.7912638187 + 0.8200876117 0.6231778264 0.7920442224 + 0.8208791018 0.6245108843 0.7928205132 + 0.8216655254 0.6258392930 0.7935903072 + 0.8224520087 0.6271675229 0.7943601012 + 0.8232241869 0.6284862757 0.7951198816 + 0.8239961267 0.6298050880 0.7958778739 + 0.8247646093 0.6311200261 0.7966322899 + 0.8255286217 0.6324295998 0.7973783016 + 0.8262926936 0.6337392926 0.7981241941 + 0.8270503879 0.6350398064 0.7988656759 + 0.8278068900 0.6363382936 0.7996054888 + 0.8285604119 0.6376342773 0.8003445864 + 0.8293051124 0.6389204264 0.8010787964 + 0.8300496936 0.6402065158 0.8018130064 + 0.8307862282 0.6414867043 0.8025422096 + 0.8315181732 0.6427623034 0.8032668829 + 0.8322501779 0.6440379024 0.8039916158 + 0.8329762220 0.6453067064 0.8047072291 + 0.8337022066 0.6465746760 0.8054215908 + 0.8344250917 0.6478404999 0.8061349988 + 0.8351423144 0.6490973830 0.8068444729 + 0.8358594775 0.6503543854 0.8075541258 + 0.8365702033 0.6516067982 0.8082594872 + 0.8372778296 0.6528545022 0.8089613914 + 0.8379853964 0.6541022062 0.8096632957 + 0.8386828899 0.6553429961 0.8103563190 + 0.8393800855 0.6565815210 0.8110477924 + 0.8400756717 0.6578201056 0.8117380738 + 0.8407658935 0.6590459943 0.8124209046 + 0.8414561749 0.6602718234 0.8131036758 + 0.8421434164 0.6614953279 0.8137840033 + 0.8428274989 0.6627100110 0.8144602180 + 0.8435115814 0.6639245749 0.8151363730 + 0.8441904783 0.6651350260 0.8158075213 + 0.8448675871 0.6663392186 0.8164752126 + 0.8455445766 0.6675434113 0.8171430230 + 0.8462108970 0.6687409282 0.8178033233 + 0.8468765020 0.6699336171 0.8184620142 + 0.8475412130 0.6711263061 0.8191205859 + 0.8481993079 0.6723105907 0.8197690845 + 0.8488574028 0.6734917164 0.8204175830 + 0.8495146036 0.6746727824 0.8210650086 + 0.8501691818 0.6758468151 0.8217062950 + 0.8508237004 0.6770195961 0.8223475218 + 0.8514751196 0.6781923771 0.8229870796 + 0.8521217108 0.6793540716 0.8236224055 + 0.8527683020 0.6805154085 0.8242577910 + 0.8534088731 0.6816763878 0.8248903751 + 0.8540437818 0.6828271747 0.8255187869 + 0.8546788096 0.6839780807 0.8261471987 + 0.8553087115 0.6851279736 0.8267704844 + 0.8559352756 0.6862679720 0.8273885846 + 0.8565620184 0.6874079108 0.8280068040 + 0.8571853042 0.6885467172 0.8286191225 + 0.8578070998 0.6896767020 0.8292267919 + 0.8584287763 0.6908066869 0.8298345208 + 0.8590450287 0.6919351220 0.8304392099 + 0.8596587777 0.6930524111 0.8310418725 + 0.8602727056 0.6941698194 0.8316444755 + 0.8608806729 0.6952862144 0.8322427273 + 0.8614864945 0.6963943839 0.8328379989 + 0.8620923162 0.6975026727 0.8334332705 + 0.8626918793 0.6986103058 0.8340228796 + 0.8632891774 0.6997066736 0.8346086740 + 0.8638864756 0.7008031011 0.8351945281 + 0.8644778132 0.7018995285 0.8357760906 + 0.8650662899 0.7029855251 0.8363544941 + 0.8656548858 0.7040715218 0.8369328976 + 0.8662387133 0.7051573992 0.8375086188 + 0.8668196201 0.7062337995 0.8380817771 + 0.8674005866 0.7073091269 0.8386549950 + 0.8679792285 0.7083843946 0.8392254114 + 0.8685559034 0.7094525099 0.8397920728 + 0.8691325784 0.7105184197 0.8403587937 + 0.8697057962 0.7115843296 0.8409234285 + 0.8702744246 0.7126430273 0.8414832950 + 0.8708429933 0.7136971951 0.8420432806 + 0.8714082241 0.7147514820 0.8426018953 + 0.8719649911 0.7158018947 0.8431546092 + 0.8725218773 0.7168475986 0.8437072039 + 0.8730772734 0.7178933024 0.8442596197 + 0.8736236095 0.7189357281 0.8448042274 + 0.8741698265 0.7199681997 0.8453487754 + 0.8747161031 0.7210006714 0.8458935022 + 0.8752585053 0.7220329046 0.8464329243 + 0.8758007288 0.7230560184 0.8469712138 + 0.8763428926 0.7240790725 0.8475095034 + 0.8768798113 0.7251021862 0.8480458260 + 0.8774148822 0.7261161804 0.8485807776 + 0.8779498935 0.7271276116 0.8491157293 + 0.8784800172 0.7281389832 0.8496479988 + 0.8790047765 0.7291454077 0.8501759768 + 0.8795297146 0.7301467061 0.8507040143 + 0.8800523281 0.7311478853 0.8512308002 + 0.8805670738 0.7321473956 0.8517482877 + 0.8810818791 0.7331386805 0.8522657752 + 0.8815966845 0.7341300249 0.8527832031 + 0.8821054101 0.7351213098 0.8532953858 + 0.8826135993 0.7361055017 0.8538061976 + 0.8831217885 0.7370883822 0.8543171287 + 0.8836262226 0.7380713820 0.8548266292 + 0.8841279745 0.7390490174 0.8553346992 + 0.8846297860 0.7400209904 0.8558428288 + 0.8851295710 0.7409930229 0.8563500047 + 0.8856232762 0.7419641018 0.8568516970 + 0.8861169815 0.7429267764 0.8573533893 + 0.8866106868 0.7438895106 0.8578552008 + 0.8870999217 0.7448523045 0.8583500981 + 0.8875883818 0.7458072901 0.8588429093 + 0.8880769014 0.7467586994 0.8593356013 + 0.8885614872 0.7477099895 0.8598263264 + 0.8890416026 0.7486596704 0.8603129983 + 0.8895217776 0.7496036887 0.8607997894 + 0.8900015950 0.7505478263 0.8612865806 + 0.8904702067 0.7514917850 0.8617706895 + 0.8909388185 0.7524288297 0.8622545004 + 0.8914073706 0.7533634901 0.8627383709 + 0.8918703794 0.7542982101 0.8632197976 + 0.8923289776 0.7552309036 0.8636981845 + 0.8927875757 0.7561575770 0.8641765714 + 0.8932459950 0.7570843101 0.8646550179 + 0.8937010765 0.7580109239 0.8651264906 + 0.8941562176 0.7589309812 0.8655977249 + 0.8946112990 0.7598484159 0.8660690188 + 0.8950629830 0.7607657909 0.8665379882 + 0.8955119848 0.7616819739 0.8670043945 + 0.8959609866 0.7625917792 0.8674706817 + 0.8964099884 0.7635017037 0.8679370880 + 0.8968498707 0.7644115090 0.8683987260 + 0.8972896934 0.7653169036 0.8688598871 + 0.8977295756 0.7662187219 0.8693211079 + 0.8981648088 0.7671204805 0.8697806001 + 0.8985937238 0.7680222988 0.8702368736 + 0.8990225792 0.7689167857 0.8706930876 + 0.8994513750 0.7698106766 0.8711493015 + 0.8998739719 0.7707046270 0.8716015220 + 0.9002950788 0.7715961933 0.8720520139 + 0.9007161260 0.7724819183 0.8725025058 + 0.9011359811 0.7733675241 0.8729526997 + 0.9015498161 0.7742530704 0.8733996153 + 0.9019635916 0.7751324177 0.8738464713 + 0.9023774266 0.7760072947 0.8742935061 + 0.9027864933 0.7768821716 0.8747376800 + 0.9031909108 0.7777572274 0.8751780987 + 0.9035953283 0.7786241174 0.8756185174 + 0.9039996862 0.7794898748 0.8760588169 + 0.9043973088 0.7803558111 0.8764961958 + 0.9047932029 0.7812201977 0.8769323230 + 0.9051890969 0.7820760012 0.8773683906 + 0.9055845737 0.7829318047 0.8778045177 + 0.9059756994 0.7837876081 0.8782365918 + 0.9063668251 0.7846404910 0.8786684871 + 0.9067577720 0.7854884267 0.8791003823 + 0.9071465731 0.7863361835 0.8795318007 + 0.9075301886 0.7871841192 0.8799616098 + 0.9079139233 0.7880274057 0.8803914189 + 0.9082974792 0.7888671756 0.8808211088 + 0.9086759090 0.7897068858 0.8812487721 + 0.9090495110 0.7905467153 0.8816735744 + 0.9094231129 0.7913808823 0.8820983171 + 0.9097967148 0.7922129035 0.8825231194 + 0.9101623893 0.7930449247 0.8829430938 + 0.9105246067 0.7938770056 0.8833600283 + 0.9108868241 0.7947024107 0.8837769032 + 0.9112489223 0.7955265045 0.8841937780 + 0.9116070271 0.7963507175 0.8846076131 + 0.9119641185 0.7971748710 0.8850203753 + 0.9123213291 0.7979897261 0.8854331970 + 0.9126784801 0.7988039255 0.8858460188 + 0.9130293131 0.7996181846 0.8862584829 + 0.9133797288 0.8004323244 0.8866708279 + 0.9137300849 0.8012365103 0.8870831728 + 0.9140803218 0.8020406961 0.8874955773 + 0.9144240022 0.8028448224 0.8879050016 + 0.9147678018 0.8036485910 0.8883141279 + 0.9151114821 0.8044434786 0.8887231946 + 0.9154546857 0.8052384853 0.8891323209 + 0.9157909155 0.8060333729 0.8895354867 + 0.9161270857 0.8068280220 0.8899384737 + 0.9164631963 0.8076159954 0.8903415799 + 0.9167991281 0.8084039092 0.8907446265 + 0.9171310067 0.8091918826 0.8911405206 + 0.9174628854 0.8099797964 0.8915362954 + 0.9177948236 0.8107593060 0.8919320703 + 0.9181262851 0.8115388155 0.8923279047 + 0.9184517860 0.8123183250 0.8927236199 + 0.9187772870 0.8130977750 0.8931193948 + 0.9191027880 0.8138713241 0.8935151100 + 0.9194281101 0.8146445751 0.8939108253 + 0.9197450280 0.8154178262 0.8943054080 + 0.9200618863 0.8161910176 0.8946998715 + 0.9203788042 0.8169561028 0.8950942755 + 0.9206957221 0.8177195787 0.8954887986 + 0.9210032225 0.8184831738 0.8958798051 + 0.9213100076 0.8192468286 0.8962702751 + 0.9216167927 0.8200044036 0.8966606855 + 0.9219235778 0.8207594752 0.8970510960 + 0.9222257137 0.8215147257 0.8974366188 + 0.9225267172 0.8222697973 0.8978202939 + 0.9228277206 0.8230199218 0.8982040286 + 0.9231287837 0.8237658143 0.8985875845 + 0.9234266877 0.8245115876 0.8989667892 + 0.9237229824 0.8252574205 0.8993425965 + 0.9240192771 0.8260008097 0.8997182846 + 0.9243155718 0.8267393112 0.9000940919 + 0.9246088266 0.8274778128 0.9004687071 + 0.9248985052 0.8282163143 0.9008412957 + 0.9251881242 0.8289538026 0.9012138844 + 0.9254776835 0.8296815753 0.9015864730 + 0.9257658720 0.8304092884 0.9019587040 + 0.9260491729 0.8311371207 0.9023287296 + 0.9263325930 0.8318648934 0.9026985765 + 0.9266158938 0.8325855732 0.9030684829 + 0.9268993139 0.8333047032 0.9034383893 + 0.9271755219 0.8340237737 0.9038041234 + 0.9274516106 0.8347429037 0.9041693807 + 0.9277275801 0.8354570270 0.9045346975 + 0.9280037284 0.8361662030 0.9048998952 + 0.9282758236 0.8368754983 0.9052616954 + 0.9285460711 0.8375847936 0.9056211710 + 0.9288163185 0.8382927775 0.9059805870 + 0.9290866256 0.8389946818 0.9063401222 + 0.9293550253 0.8396965861 0.9066985846 + 0.9296200871 0.8403984904 0.9070546031 + 0.9298850894 0.8411002755 0.9074106216 + 0.9301502109 0.8417940140 0.9077665210 + 0.9304152131 0.8424854279 0.9081224799 + 0.9306733012 0.8431769013 0.9084752798 + 0.9309315085 0.8438683152 0.9088277817 + 0.9311895967 0.8445565104 0.9091802239 + 0.9314476848 0.8452389836 0.9095327258 + 0.9317021966 0.8459215164 0.9098836184 + 0.9319543242 0.8466041088 0.9102330208 + 0.9322063923 0.8472865820 0.9105823040 + 0.9324585199 0.8479604125 0.9109317064 + 0.9327095151 0.8486334085 0.9112808108 + 0.9329549074 0.8493062854 0.9116259813 + 0.9332001805 0.8499792814 0.9119712710 + 0.9334455132 0.8506484032 0.9123165011 + 0.9336907864 0.8513122201 0.9126616716 + 0.9339326024 0.8519759774 0.9130048156 + 0.9341726899 0.8526396751 0.9133464098 + 0.9344127178 0.8533034921 0.9136881232 + 0.9346528053 0.8539575934 0.9140297771 + 0.9348924756 0.8546106219 0.9143710732 + 0.9351304173 0.8552635908 0.9147090912 + 0.9353682995 0.8559166193 0.9150471091 + 0.9356061816 0.8565670252 0.9153851271 + 0.9358440042 0.8572118878 0.9157230854 + 0.9360798001 0.8578568101 0.9160578847 + 0.9363142848 0.8585017920 0.9163900018 + 0.9365487099 0.8591467142 0.9167221189 + 0.9367831945 0.8597838283 0.9170541763 + 0.9370173812 0.8604179025 0.9173862934 + 0.9372467995 0.8610520959 0.9177129269 + 0.9374760985 0.8616862893 0.9180392027 + 0.9377055168 0.8623197079 0.9183654785 + 0.9379348159 0.8629447818 0.9186918139 + 0.9381616116 0.8635699153 0.9190167785 + 0.9383847117 0.8641949892 0.9193391204 + 0.9386078119 0.8648201227 0.9196614027 + 0.9388309717 0.8654413819 0.9199838042 + 0.9390540719 0.8660572171 0.9203060865 + 0.9392737150 0.8666728735 0.9206275940 + 0.9394921064 0.8672885895 0.9209488034 + 0.9397104979 0.8679043055 0.9212698936 + 0.9399290085 0.8685140014 0.9215909839 + 0.9401472211 0.8691210151 0.9219120741 + 0.9403619170 0.8697279096 0.9222300053 + 0.9405764937 0.8703348041 0.9225476980 + 0.9407910705 0.8709418178 0.9228653908 + 0.9410057068 0.8715392947 0.9231832027 + 0.9412195086 0.8721361756 0.9234995842 + 0.9414315224 0.8727331161 0.9238117933 + 0.9416434169 0.8733298779 0.9241240025 + 0.9418553114 0.8739258051 0.9244362116 + 0.9420672059 0.8745154738 0.9247483015 + 0.9422771931 0.8751052022 0.9250574112 + 0.9424853921 0.8756948113 0.9253627062 + 0.9426935911 0.8762844801 0.9256678820 + 0.9429017901 0.8768712878 0.9259731174 + 0.9431099892 0.8774514198 0.9262782931 + 0.9433146119 0.8780314922 0.9265810847 + 0.9435176253 0.8786115050 0.9268823862 + 0.9437206984 0.8791915774 0.9271836877 + 0.9439237118 0.8797677755 0.9274849892 + 0.9441267252 0.8803384900 0.9277862906 + 0.9443246126 0.8809092045 0.9280866981 + 0.9445216060 0.8814799190 0.9283868074 + 0.9447184801 0.8820505738 0.9286869168 + 0.9449154735 0.8826171756 0.9289870262 + 0.9451124072 0.8831797242 0.9292870760 + 0.9453071952 0.8837422132 0.9295849204 + 0.9455018044 0.8843047023 0.9298822880 + 0.9456964731 0.8848671913 0.9301797152 + 0.9458910823 0.8854259849 0.9304770827 + 0.9460856915 0.8859816194 0.9307745099 + 0.9462794065 0.8865371943 0.9310675263 + 0.9464730024 0.8870928288 0.9313601851 + 0.9466665983 0.8876484036 0.9316527843 + 0.9468601942 0.8881986141 0.9319455028 + 0.9470537901 0.8887439966 0.9322381020 + 0.9472450018 0.8892894983 0.9325249791 + 0.9474362731 0.8898350000 0.9328114986 + 0.9476274848 0.8903805017 0.9330980182 + 0.9478188157 0.8909212947 0.9333845973 + 0.9480100870 0.8914574981 0.9336711168 + 0.9481977224 0.8919935822 0.9339528084 + 0.9483852983 0.8925297856 0.9342340231 + 0.9485728741 0.8930659294 0.9345152974 + 0.9487605095 0.8935987949 0.9347965121 + 0.9489480853 0.8941270709 0.9350777864 + 0.9491310716 0.8946554065 0.9353563786 + 0.9493138194 0.8951836824 0.9356346726 + 0.9494963884 0.8957120180 0.9359129071 + 0.9496790767 0.8962380290 0.9361910820 + 0.9498617053 0.8967580795 0.9364693165 + 0.9500396848 0.8972783089 0.9367458224 + 0.9502165914 0.8977984190 0.9370216727 + 0.9503934979 0.8983185291 0.9372975230 + 0.9505705237 0.8988376260 0.9375733733 + 0.9507473707 0.8993477821 0.9378492832 + 0.9509227872 0.8998581171 0.9381229877 + 0.9510974288 0.9003683925 0.9383950830 + 0.9512720704 0.9008787274 0.9386672974 + 0.9514467120 0.9013890028 0.9389393926 + 0.9516214132 0.9018915892 0.9392116070 + 0.9517948031 0.9023934007 0.9394819140 + 0.9519668818 0.9028952718 0.9397495985 + 0.9521389008 0.9033970833 0.9400172234 + 0.9523109794 0.9038988948 0.9402849078 + 0.9524831176 0.9043961763 0.9405525923 + 0.9526547790 0.9048910141 0.9408199191 + 0.9528251886 0.9053859115 0.9410852790 + 0.9529957175 0.9058806896 0.9413505793 + 0.9531661868 0.9063755274 0.9416159987 + 0.9533365965 0.9068669081 0.9418812990 + 0.9535071254 0.9073519111 0.9421467185 + 0.9536749721 0.9078369141 0.9424085021 + 0.9538426995 0.9083219171 0.9426698089 + 0.9540104866 0.9088069201 0.9429311156 + 0.9541782141 0.9092918038 0.9431924224 + 0.9543460011 0.9097663760 0.9434537292 + 0.9545125961 0.9102410078 0.9437128901 + 0.9546784759 0.9107155800 0.9439705014 + 0.9548444748 0.9111902118 0.9442281127 + 0.9550104141 0.9116647840 0.9444857240 + 0.9551764131 0.9121345878 0.9447432160 + 0.9553416967 0.9126017094 0.9449998140 + 0.9555050135 0.9130687118 0.9452524781 + 0.9556683898 0.9135357141 0.9455052018 + 0.9558317065 0.9140027165 0.9457579255 + 0.9559950233 0.9144682884 0.9460105896 + 0.9561583996 0.9149281979 0.9462631941 + 0.9563187957 0.9153881073 0.9465129972 + 0.9564787745 0.9158478975 0.9467620254 + 0.9566386938 0.9163078070 0.9470111132 + 0.9567986727 0.9167675972 0.9472600818 + 0.9569585919 0.9172213078 0.9475091100 + 0.9571176767 0.9176725745 0.9477577806 + 0.9572752714 0.9181237817 0.9480059147 + 0.9574329257 0.9185751081 0.9482539892 + 0.9575905800 0.9190263152 0.9485020041 + 0.9577481747 0.9194753766 0.9487500787 + 0.9579058290 0.9199153781 0.9489980936 + 0.9580608010 0.9203554988 0.9492439032 + 0.9582154751 0.9207956195 0.9494892955 + 0.9583702087 0.9212356210 0.9497346878 + 0.9585248828 0.9216756821 0.9499800801 + 0.9586796165 0.9221099019 0.9502254725 + 0.9588336945 0.9225407243 0.9504696131 + 0.9589865208 0.9229714274 0.9507107139 + 0.9591394067 0.9234021902 0.9509518147 + 0.9592921734 0.9238328934 0.9511927962 + 0.9594451189 0.9242635965 0.9514338970 + 0.9595978856 0.9246863127 0.9516749978 + 0.9597501755 0.9251089096 0.9519119263 + 0.9599022865 0.9255316257 0.9521470070 + 0.9600545168 0.9259542823 0.9523820877 + 0.9602066278 0.9263768792 0.9526172280 + 0.9603586793 0.9267967939 0.9528523088 + 0.9605106115 0.9272115231 0.9530869722 + 0.9606612921 0.9276261926 0.9533157945 + 0.9608119726 0.9280409217 0.9535444975 + 0.9609627128 0.9284554720 0.9537733197 + 0.9611135125 0.9288702011 0.9540020227 + 0.9612641931 0.9292793870 0.9542307854 + 0.9614138007 0.9296854138 0.9544587135 + 0.9615620971 0.9300913811 0.9546853900 + 0.9617102742 0.9304975271 0.9549120069 + 0.9618585706 0.9309034944 0.9551386237 + 0.9620069265 0.9313095808 0.9553651810 + 0.9621551037 0.9317073822 0.9555919170 + 0.9623023272 0.9321041703 0.9558166265 + 0.9624490738 0.9325010180 0.9560402036 + 0.9625958204 0.9328979254 0.9562637806 + 0.9627425075 0.9332947135 0.9564874768 + 0.9628893137 0.9336907268 0.9567111135 + 0.9630360007 0.9340782762 0.9569346905 + 0.9631810784 0.9344660044 0.9571558833 + 0.9633259177 0.9348536134 0.9573767185 + 0.9634708166 0.9352412820 0.9575974941 + 0.9636157155 0.9356290102 0.9578183293 + 0.9637606144 0.9360147715 0.9580391049 + 0.9639053941 0.9363946915 0.9582598209 + 0.9640493989 0.9367746115 0.9584767222 + 0.9641934037 0.9371545911 0.9586936831 + 0.9643375278 0.9375345111 0.9589105844 + 0.9644814730 0.9379143715 0.9591274858 + 0.9646254778 0.9382917881 0.9593443871 + 0.9647691846 0.9386643171 0.9595605731 + 0.9649112821 0.9390369058 0.9597727060 + 0.9650534987 0.9394093752 0.9599847794 + 0.9651957154 0.9397819042 0.9601967931 + 0.9653378725 0.9401543736 0.9604089260 + 0.9654800296 0.9405236244 0.9606209993 + 0.9656214714 0.9408878088 0.9608321786 + 0.9657613039 0.9412518740 0.9610404968 + 0.9659010172 0.9416161180 0.9612486959 + 0.9660406709 0.9419801831 0.9614568949 + 0.9661805034 0.9423443079 0.9616652131 + 0.9663202167 0.9427043796 0.9618734121 + 0.9664595127 0.9430581927 0.9620807767 + 0.9665976763 0.9434120059 0.9622855186 + 0.9667360187 0.9437658191 0.9624903202 + 0.9668741822 0.9441195726 0.9626950026 + 0.9670125246 0.9444733858 0.9628996849 + 0.9671506882 0.9448243976 0.9631044865 + 0.9672883153 0.9451701045 0.9633080959 + 0.9674242139 0.9455156922 0.9635080099 + 0.9675601125 0.9458613992 0.9637079835 + 0.9676958919 0.9462071061 0.9639078975 + 0.9678317904 0.9465526938 0.9641078115 + 0.9679676890 0.9468963742 0.9643077254 + 0.9681032896 0.9472342134 0.9645069838 + 0.9682378173 0.9475718737 0.9647027254 + 0.9683722854 0.9479095936 0.9648982882 + 0.9685068130 0.9482473135 0.9650939703 + 0.9686412811 0.9485850930 0.9652895927 + 0.9687758088 0.9489220977 0.9654852748 + 0.9689102173 0.9492530823 0.9656807780 + 0.9690427184 0.9495840073 0.9658718705 + 0.9691752791 0.9499149919 0.9660630822 + 0.9693077803 0.9502459764 0.9662541747 + 0.9694404006 0.9505769014 0.9664453864 + 0.9695729017 0.9509078860 0.9666364789 + 0.9697054029 0.9512314796 0.9668276906 + 0.9698356986 0.9515542984 0.9670159221 + 0.9699656963 0.9518771172 0.9672036171 + 0.9700956941 0.9521998763 0.9673913717 + 0.9702258110 0.9525226951 0.9675791264 + 0.9703558087 0.9528455138 0.9677668214 + 0.9704858065 0.9531627893 0.9679545164 + 0.9706159830 0.9534772038 0.9681413770 + 0.9707463980 0.9537916780 0.9683275819 + 0.9708768129 0.9541060925 0.9685139060 + 0.9710071087 0.9544205070 0.9687001705 + 0.9711375237 0.9547349215 0.9688864946 + 0.9712678790 0.9550464153 0.9690728188 + 0.9713981152 0.9553524256 0.9692583084 + 0.9715282917 0.9556583166 0.9694423079 + 0.9716584086 0.9559643269 0.9696264267 + 0.9717885256 0.9562702775 0.9698104262 + 0.9719185829 0.9565762281 0.9699944854 + 0.9720488191 0.9568821788 0.9701784849 + 0.9721788168 0.9571802020 0.9703624845 + 0.9723080993 0.9574782252 0.9705430865 + 0.9724375010 0.9577761292 0.9707236886 + 0.9725667834 0.9580740929 0.9709041715 + 0.9726961255 0.9583721161 0.9710847735 + 0.9728254080 0.9586700201 0.9712653756 + 0.9729546905 0.9589635134 0.9714459181 + 0.9730840921 0.9592537880 0.9716234803 + 0.9732134938 0.9595441222 0.9717993140 + 0.9733428955 0.9598343968 0.9719752073 + 0.9734724164 0.9601246715 0.9721509814 + 0.9736018181 0.9604150057 0.9723268151 + 0.9737312198 0.9607043862 0.9725025892 + 0.9738605022 0.9609869719 0.9726774096 + 0.9739891887 0.9612696767 0.9728478789 + 0.9741178751 0.9615523219 0.9730182886 + 0.9742466211 0.9618350267 0.9731886983 + 0.9743753076 0.9621176124 0.9733592272 + 0.9745039940 0.9624003172 0.9735295773 + 0.9746326804 0.9626781940 0.9737001061 + 0.9747601748 0.9629530907 0.9738674760 + 0.9748873115 0.9632279873 0.9740337133 + 0.9750145078 0.9635028839 0.9742000103 + 0.9751415849 0.9637778997 0.9743661880 + 0.9752687216 0.9640527964 0.9745324254 + 0.9753959179 0.9643273950 0.9746986032 + 0.9755228162 0.9645949006 0.9748641849 + 0.9756492972 0.9648625255 0.9750267267 + 0.9757757187 0.9651299715 0.9751893282 + 0.9759021997 0.9653975964 0.9753518105 + 0.9760286212 0.9656651020 0.9755142927 + 0.9761551023 0.9659326077 0.9756767750 + 0.9762815237 0.9661971927 0.9758393764 + 0.9764071703 0.9664574862 0.9760001898 + 0.9765323997 0.9667177796 0.9761596918 + 0.9766576290 0.9669780731 0.9763193130 + 0.9767829180 0.9672384262 0.9764788151 + 0.9769080877 0.9674987793 0.9766383767 + 0.9770333171 0.9677590728 0.9767978787 + 0.9771584868 0.9680131078 0.9769575000 + 0.9772819877 0.9682648778 0.9771137238 + 0.9774054885 0.9685167074 0.9772697091 + 0.9775289297 0.9687684774 0.9774256945 + 0.9776523709 0.9690203071 0.9775816798 + 0.9777758121 0.9692721963 0.9777377248 + 0.9778991938 0.9695239067 0.9778935909 + 0.9780225158 0.9697673917 0.9780486822 + 0.9781451225 0.9700108767 0.9782000184 + 0.9782677889 0.9702544212 0.9783512950 + 0.9783903956 0.9704979062 0.9785025716 + 0.9785130024 0.9707413912 0.9786539078 + 0.9786357284 0.9709848166 0.9788051844 + 0.9787582755 0.9712265730 0.9789565802 + 0.9788810015 0.9714620709 0.9791055918 + 0.9790037870 0.9716976881 0.9792516828 + 0.9791265130 0.9719331861 0.9793978930 + 0.9792491794 0.9721686840 0.9795439839 + 0.9793720245 0.9724041820 0.9796900749 + 0.9794946909 0.9726397991 0.9798362255 + 0.9796174765 0.9728724957 0.9799823761 + 0.9797399044 0.9731004834 0.9801260829 + 0.9798622727 0.9733284712 0.9802682996 + 0.9799845815 0.9735565186 0.9804103971 + 0.9801068902 0.9737845063 0.9805526137 + 0.9802291989 0.9740126133 0.9806947708 + 0.9803515077 0.9742406011 0.9808369875 + 0.9804738164 0.9744648933 0.9809792042 + 0.9805954099 0.9746851921 0.9811189175 + 0.9807168245 0.9749054909 0.9812576771 + 0.9808381200 0.9751256704 0.9813964963 + 0.9809594750 0.9753460288 0.9815353155 + 0.9810807705 0.9755662084 0.9816740751 + 0.9812021852 0.9757865071 0.9818128943 + 0.9813234806 0.9760035872 0.9819517136 + 0.9814447761 0.9762178063 0.9820870757 + 0.9815660119 0.9764320254 0.9822216034 + 0.9816873074 0.9766461849 0.9823560715 + 0.9818086028 0.9768604040 0.9824907184 + 0.9819297791 0.9770746231 0.9826251864 + 0.9820510745 0.9772887230 0.9827597737 + 0.9821723104 0.9774991870 0.9828943014 + 0.9822927713 0.9777060151 0.9830257893 + 0.9824131727 0.9779127240 0.9831566215 + 0.9825335145 0.9781193733 0.9832873940 + 0.9826539159 0.9783260822 0.9834182262 + 0.9827741981 0.9785327911 0.9835489988 + 0.9828945994 0.9787396193 0.9836797714 + 0.9830150008 0.9789426923 0.9838104844 + 0.9831339717 0.9791409969 0.9839380980 + 0.9832527041 0.9793391824 0.9840646982 + 0.9833713174 0.9795374274 0.9841911793 + 0.9834899902 0.9797356725 0.9843177795 + 0.9836087227 0.9799339175 0.9844443798 + 0.9837273955 0.9801321030 0.9845709205 + 0.9838461280 0.9803280830 0.9846975207 + 0.9839642048 0.9805182219 0.9848210216 + 0.9840821028 0.9807084203 0.9849429727 + 0.9841998816 0.9808984995 0.9850649834 + 0.9843177795 0.9810886979 0.9851869941 + 0.9844356775 0.9812787771 0.9853090048 + 0.9845535755 0.9814689755 0.9854310155 + 0.9846714139 0.9816585183 0.9855529070 + 0.9847885966 0.9818410873 0.9856733084 + 0.9849051237 0.9820237756 0.9857919216 + 0.9850217104 0.9822065234 0.9859105945 + 0.9851381779 0.9823890924 0.9860293269 + 0.9852548242 0.9825717807 0.9861479998 + 0.9853712916 0.9827544093 0.9862666130 + 0.9854878783 0.9829370975 0.9863852859 + 0.9856039286 0.9831125140 0.9865033031 + 0.9857186079 0.9832866192 0.9866192937 + 0.9858332872 0.9834606051 0.9867352843 + 0.9859480262 0.9836345911 0.9868512750 + 0.9860628247 0.9838085771 0.9869673252 + 0.9861775041 0.9839826226 0.9870833158 + 0.9862921834 0.9841566086 0.9871993065 + 0.9864069223 0.9843258858 0.9873152971 + 0.9865176082 0.9844909906 0.9874280095 + 0.9866282940 0.9846559763 0.9875406027 + 0.9867389202 0.9848210812 0.9876531959 + 0.9868496060 0.9849861264 0.9877659082 + 0.9869602919 0.9851511717 0.9878785014 + 0.9870709777 0.9853162169 0.9879910946 + 0.9871817231 0.9854801893 0.9881036878 + 0.9872913957 0.9856383204 0.9882147908 + 0.9874005914 0.9857963920 0.9883249998 + 0.9875099063 0.9859545231 0.9884350896 + 0.9876191020 0.9861125946 0.9885452986 + 0.9877284169 0.9862707257 0.9886553884 + 0.9878376126 0.9864287972 0.9887655973 + 0.9879468083 0.9865869284 0.9888756871 + 0.9880558252 0.9867392778 0.9889851809 + 0.9881638288 0.9868894815 0.9890925288 + 0.9882717133 0.9870396852 0.9891998172 + 0.9883797169 0.9871898890 0.9893071055 + 0.9884877205 0.9873400927 0.9894143939 + 0.9885957241 0.9874902964 0.9895216227 + 0.9887037277 0.9876403809 0.9896289110 + 0.9888116717 0.9877886772 0.9897361994 + 0.9889186025 0.9879299998 0.9898411036 + 0.9890251756 0.9880713820 0.9899451733 + 0.9891318083 0.9882127047 0.9900494218 + 0.9892383814 0.9883540869 0.9901534915 + 0.9893450141 0.9884954095 0.9902576804 + 0.9894515276 0.9886367917 0.9903618097 + 0.9895581007 0.9887781143 0.9904659986 + 0.9896646142 0.9889159203 0.9905695915 + 0.9897707105 0.9890518188 0.9906715155 + 0.9898768067 0.9891877174 0.9907733798 + 0.9899830222 0.9893234968 0.9908754230 + 0.9900891185 0.9894593954 0.9909772873 + 0.9901952147 0.9895952940 0.9910792112 + 0.9903013110 0.9897311926 0.9911811948 + 0.9904074073 0.9898669720 0.9912831187 + 0.9905129075 0.9899997115 0.9913834929 + 0.9906179905 0.9901322722 0.9914830923 + 0.9907230139 0.9902650118 0.9915826917 + 0.9908280969 0.9903976917 0.9916822910 + 0.9909331799 0.9905303121 0.9917818904 + 0.9910383224 0.9906629920 0.9918814898 + 0.9911434054 0.9907956719 0.9919810891 + 0.9912484288 0.9909266829 0.9920806885 + 0.9913520217 0.9910547137 0.9921786189 + 0.9914554954 0.9911826849 0.9922763705 + 0.9915590286 0.9913107157 0.9923741817 + 0.9916625023 0.9914388061 0.9924721122 + 0.9917659760 0.9915667772 0.9925699234 + 0.9918695092 0.9916948080 0.9926676750 + 0.9919729829 0.9918227792 0.9927656054 + 0.9920759201 0.9919472933 0.9928627014 + 0.9921774268 0.9920694232 0.9929581881 + 0.9922788739 0.9921916127 0.9930536151 + 0.9923803806 0.9923136830 0.9931489825 + 0.9924818873 0.9924358726 0.9932444096 + 0.9925833941 0.9925580025 0.9933397770 + 0.9926849008 0.9926801920 0.9934352040 + 0.9927864075 0.9928023219 0.9935305715 + 0.9928867221 0.9929195046 0.9936246276 + 0.9929860234 0.9930353165 0.9937170148 + 0.9930852056 0.9931510091 0.9938094020 + 0.9931843877 0.9932668209 0.9939017892 + 0.9932836294 0.9933825731 0.9939941764 + 0.9933828115 0.9934983850 0.9940866828 + 0.9934821129 0.9936141968 0.9941791296 + 0.9935812950 0.9937298894 0.9942715168 + 0.9936792254 0.9938406944 0.9943618178 + 0.9937763214 0.9939510822 0.9944508076 + 0.9938734174 0.9940614104 0.9945397973 + 0.9939705729 0.9941716790 0.9946289062 + 0.9940677285 0.9942820072 0.9947178960 + 0.9941648245 0.9943923950 0.9948070049 + 0.9942619801 0.9945027232 0.9948959947 + 0.9943590760 0.9946129918 0.9949849844 + 0.9944550991 0.9947180152 0.9950715899 + 0.9945507050 0.9948229194 0.9951571822 + 0.9946463108 0.9949277043 0.9952427745 + 0.9947419167 0.9950326085 0.9953284264 + 0.9948375225 0.9951373935 0.9954140186 + 0.9949331284 0.9952422976 0.9954996109 + 0.9950286150 0.9953472018 0.9955852032 + 0.9951242208 0.9954519868 0.9956707954 + 0.9952185154 0.9955508113 0.9957543015 + 0.9953123927 0.9956492782 0.9958369732 + 0.9954062104 0.9957479239 0.9959197044 + 0.9955000281 0.9958463907 0.9960023761 + 0.9955939054 0.9959449172 0.9960849881 + 0.9956877232 0.9960435033 0.9961677194 + 0.9957814813 0.9961420298 0.9962503910 + 0.9958754182 0.9962404966 0.9963331223 + 0.9959679842 0.9963330030 0.9964141846 + 0.9960600734 0.9964246750 0.9964945912 + 0.9961522818 0.9965164065 0.9965748787 + 0.9962443709 0.9966081977 0.9966552854 + 0.9963365197 0.9966999292 0.9967356920 + 0.9964286089 0.9967916012 0.9968159795 + 0.9965208173 0.9968832731 0.9968963861 + 0.9966129065 0.9969750047 0.9969767928 + 0.9967035055 0.9970617890 0.9970558882 + 0.9967930913 0.9971467853 0.9971340895 + 0.9968826771 0.9972319007 0.9972122908 + 0.9969723821 0.9973170161 0.9972904921 + 0.9970620275 0.9974020123 0.9973688126 + 0.9971516132 0.9974871278 0.9974470139 + 0.9972411990 0.9975721240 0.9975252151 + 0.9973307848 0.9976571798 0.9976034164 + 0.9974188805 0.9977390170 0.9976804852 + 0.9975048900 0.9978179932 0.9977560043 + 0.9975908995 0.9978969097 0.9978315234 + 0.9976769090 0.9979758263 0.9979069233 + 0.9977629185 0.9980546832 0.9979823828 + 0.9978489280 0.9981337190 0.9980579019 + 0.9979348779 0.9982125759 0.9981334209 + 0.9980208874 0.9982914925 0.9982088208 + 0.9981061220 0.9983687997 0.9982838035 + 0.9981874228 0.9984411001 0.9983559251 + 0.9982687831 0.9985135198 0.9984279871 + 0.9983500838 0.9985858202 0.9985002279 + 0.9984313846 0.9986581802 0.9985722899 + 0.9985126853 0.9987304807 0.9986444116 + 0.9985939860 0.9988029003 0.9987164736 + 0.9986754060 0.9988752007 0.9987885952 + 0.9987567067 0.9989476204 0.9988607764 + 0.9988334179 0.9990134835 0.9989299178 + 0.9989092946 0.9990788102 0.9989984035 + 0.9989851117 0.9991440773 0.9990668893 + 0.9990609884 0.9992092848 0.9991354942 + 0.9991368055 0.9992746115 0.9992039800 + 0.9992126822 0.9993398786 0.9992725253 + 0.9992886186 0.9994050860 0.9993410707 + 0.9993643761 0.9994704127 0.9994096160 + 0.9994376898 0.9995324016 0.9994766712 + 0.9995080233 0.9995908141 0.9995421171 + 0.9995782971 0.9996492863 0.9996075034 + 0.9996485710 0.9997076988 0.9996728897 + 0.9997189045 0.9997661710 0.9997382760 + 0.9997891784 0.9998245835 0.9998037815 + 0.9998593926 0.9998831153 0.9998692274 + 0.9999297261 0.9999415278 0.9999346137 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfachrome_rsx2_100.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfachrome_rsx2_100.spi1d index 545776f1..2fed8c75 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfachrome_rsx2_100.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfachrome_rsx2_100.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0005538157 0.0002576292 0.0005585101 - 0.0012096640 0.0005333599 0.0011604551 - 0.0019140980 0.0008360103 0.0018323910 - 0.0026460199 0.0011740130 0.0025799151 - 0.0034157911 0.0015541241 0.0033877229 - 0.0042393408 0.0019793189 0.0042505912 - 0.0051223529 0.0024463851 0.0051805871 - 0.0060673542 0.0029475561 0.0061999368 - 0.0070817620 0.0034730800 0.0073256488 - 0.0081761600 0.0040138708 0.0085633332 - 0.0093593132 0.0045639272 0.0099106664 - 0.0106341401 0.0051191631 0.0113591403 - 0.0119961500 0.0056801899 0.0129020195 - 0.0134347901 0.0062479940 0.0145361200 - 0.0149379000 0.0068263910 0.0162581895 - 0.0164953694 0.0074195368 0.0180597696 - 0.0181045309 0.0080307238 0.0199336000 - 0.0197760705 0.0086633833 0.0218689796 - 0.0215226207 0.0093178684 0.0238573793 - 0.0233605802 0.0099947192 0.0258926600 - 0.0253009591 0.0106952796 0.0279722493 - 0.0273566395 0.0114182802 0.0300974604 - 0.0295376405 0.0121626100 0.0322676599 - 0.0318443291 0.0129287997 0.0344865993 - 0.0342735499 0.0137156500 0.0367620103 - 0.0368162692 0.0145225199 0.0390981287 - 0.0394631997 0.0153492000 0.0414960496 - 0.0422021896 0.0161942504 0.0439662300 - 0.0450241007 0.0170577895 0.0465124287 - 0.0479182489 0.0179391392 0.0491312891 - 0.0508695506 0.0188368801 0.0518258400 - 0.0538671017 0.0197515395 0.0545928814 - 0.0569038987 0.0206836108 0.0574216582 - 0.0599653386 0.0216330998 0.0603049994 - 0.0630441532 0.0226011295 0.0632309094 - 0.0661331788 0.0235872995 0.0661824495 - 0.0692235529 0.0245907698 0.0691514686 - 0.0723131001 0.0256121606 0.0721308514 - 0.0753963515 0.0266514607 0.0751067922 - 0.0784699991 0.0277084205 0.0780698285 - 0.0815294310 0.0287822001 0.0810127109 - 0.0845746398 0.0298725795 0.0839387029 - 0.0876072496 0.0309803504 0.0868489370 - 0.0906234607 0.0321053714 0.0897342712 - 0.0936240926 0.0332451016 0.0925956666 - 0.0966099873 0.0343989693 0.0954437479 - 0.0995746925 0.0355678201 0.0982764736 - 0.1025210023 0.0367507786 0.1010928974 - 0.1054513976 0.0379455499 0.1038966030 - 0.1083656028 0.0391518585 0.1066884995 - 0.1112613007 0.0403695591 0.1094719023 - 0.1141443029 0.0415982008 0.1122469977 - 0.1170163006 0.0428373106 0.1150157973 - 0.1198785976 0.0440876000 0.1177795008 - 0.1227298006 0.0453483313 0.1205397025 - 0.1255751997 0.0466196500 0.1232986972 - 0.1284184009 0.0479020402 0.1260581017 - 0.1312606931 0.0491929315 0.1288186014 - 0.1341048926 0.0504931509 0.1315809935 - 0.1369509995 0.0518033691 0.1343457997 - 0.1397999972 0.0531228185 0.1371127963 - 0.1426569968 0.0544515885 0.1398842931 - 0.1455242038 0.0557879396 0.1426624060 - 0.1484020054 0.0571304597 0.1454472989 - 0.1512925029 0.0584798306 0.1482387930 - 0.1541953981 0.0598362610 0.1510369033 - 0.1571146995 0.0612012111 0.1538417041 - 0.1600483954 0.0625731573 0.1566565037 - 0.1629952043 0.0639522225 0.1594796926 - 0.1659574062 0.0653370395 0.1623144001 - 0.1689333022 0.0667286515 0.1651574969 - 0.1719211042 0.0681272000 0.1680109054 - 0.1749179959 0.0695317611 0.1708731055 - 0.1779257059 0.0709433332 0.1737474054 - 0.1809400022 0.0723603070 0.1766321063 - 0.1839601994 0.0737829134 0.1795291007 - 0.1869864017 0.0752107576 0.1824384034 - 0.1900130957 0.0766445100 0.1853630990 - 0.1930415034 0.0780834034 0.1883013994 - 0.1960687935 0.0795266330 0.1912554055 - 0.1990925074 0.0809739381 0.1942231059 - 0.2021104991 0.0824264735 0.1972043961 - 0.2051243931 0.0838834122 0.2002002001 - 0.2081328928 0.0853442401 0.2032063007 - 0.2111341059 0.0868088603 0.2062257975 - 0.2141271979 0.0882768705 0.2092570961 - 0.2171119004 0.0897474736 0.2122993022 - 0.2200873941 0.0912210122 0.2153486013 - 0.2230526060 0.0926979184 0.2184022963 - 0.2260069996 0.0941769183 0.2214571983 - 0.2289530039 0.0956579819 0.2245126069 - 0.2318878025 0.0971412882 0.2275668979 - 0.2348102033 0.0986268297 0.2306147963 - 0.2377181053 0.1001135036 0.2336523980 - 0.2406145036 0.1016011015 0.2366762012 - 0.2435014993 0.1030898988 0.2396847010 - 0.2463797927 0.1045792028 0.2426739931 - 0.2492484003 0.1060694978 0.2456423044 - 0.2521043122 0.1075616032 0.2485859990 - 0.2549489141 0.1090551987 0.2515046895 - 0.2577804029 0.1105502024 0.2543984056 - 0.2605997920 0.1120468974 0.2572653890 - 0.2634068131 0.1135445982 0.2601054013 - 0.2662003040 0.1150436029 0.2629188001 - 0.2689799964 0.1165440977 0.2657040954 - 0.2717454135 0.1180453971 0.2684625983 - 0.2744967043 0.1195472032 0.2711946964 - 0.2772313952 0.1210499033 0.2738988996 - 0.2799471915 0.1225532964 0.2765769064 - 0.2826446891 0.1240585968 0.2792299092 - 0.2853254974 0.1255660951 0.2818576992 - 0.2879889011 0.1270741969 0.2844642103 - 0.2906337082 0.1285829991 0.2870489955 - 0.2932612002 0.1300923973 0.2896148860 - 0.2958734035 0.1316034049 0.2921622097 - 0.2984699905 0.1331169009 0.2946906090 - 0.3010497093 0.1346320063 0.2972028852 - 0.3036122024 0.1361477971 0.2996973991 - 0.3061597049 0.1376651973 0.3021731079 - 0.3086943924 0.1391835958 0.3046326041 - 0.3112165928 0.1407034993 0.3070755005 - 0.3137254119 0.1422256976 0.3095063865 - 0.3162207901 0.1437488943 0.3119274080 - 0.3187038004 0.1452739984 0.3143393993 - 0.3211764991 0.1468010992 0.3167423904 - 0.3236395121 0.1483293027 0.3191359937 - 0.3260940909 0.1498585939 0.3215200901 - 0.3285394907 0.1513891965 0.3238947988 - 0.3309769928 0.1529217958 0.3262606859 - 0.3334091902 0.1544554979 0.3286187053 - 0.3358364105 0.1559904069 0.3309695125 - 0.3382585049 0.1575258970 0.3333117962 - 0.3406754136 0.1590621024 0.3356468081 - 0.3430868983 0.1606000960 0.3379752934 - 0.3454923034 0.1621384025 0.3402976096 - 0.3478921950 0.1636777073 0.3426141143 - 0.3502902091 0.1652178019 0.3449253142 - 0.3526883125 0.1667584032 0.3472313881 - 0.3550874889 0.1682990938 0.3495286107 - 0.3574880064 0.1698389053 0.3518162966 - 0.3598900139 0.1713789999 0.3540945053 - 0.3622936904 0.1729196012 0.3563635945 - 0.3646990955 0.1744605005 0.3586243987 - 0.3671062887 0.1760009974 0.3608759940 - 0.3695152998 0.1775424033 0.3631196022 - 0.3719266951 0.1790847033 0.3653568029 - 0.3743407130 0.1806277931 0.3675884008 - 0.3767570853 0.1821713001 0.3698143959 - 0.3791759014 0.1837140024 0.3720313013 - 0.3815971911 0.1852564067 0.3742407858 - 0.3840225041 0.1867990047 0.3764427900 - 0.3864507079 0.1883427054 0.3786371052 - 0.3888813853 0.1898857057 0.3808222115 - 0.3913142085 0.1914276034 0.3829993010 - 0.3937469125 0.1929681003 0.3851706088 - 0.3961795866 0.1945082992 0.3873367906 - 0.3986124098 0.1960480064 0.3894971013 - 0.4010451138 0.1975871027 0.3916527927 - 0.4034779072 0.1991253942 0.3938055933 - 0.4059106112 0.2006641030 0.3959555924 - 0.4083434045 0.2022033036 0.3980999887 - 0.4107761085 0.2037439942 0.4002406001 - 0.4132089019 0.2052841932 0.4023782909 - 0.4156416953 0.2068233937 0.4045133889 - 0.4180744886 0.2083608955 0.4066438079 - 0.4205071926 0.2098989934 0.4087715149 - 0.4229399860 0.2114374042 0.4108966887 - 0.4253725111 0.2129762024 0.4130181968 - 0.4278020859 0.2145155072 0.4151366949 - 0.4302271903 0.2160546035 0.4172526896 - 0.4326483905 0.2175924033 0.4193654954 - 0.4350630045 0.2191300988 0.4214769900 - 0.4374726117 0.2206678987 0.4235875905 - 0.4398784041 0.2222069055 0.4256978929 - 0.4422785044 0.2237454951 0.4278069139 - 0.4446738958 0.2252838016 0.4299151003 - 0.4470661879 0.2268211991 0.4320223033 - 0.4494529963 0.2283589989 0.4341273010 - 0.4518359900 0.2298974991 0.4362305105 - 0.4542163014 0.2314366996 0.4383283854 - 0.4565913081 0.2329763025 0.4404241145 - 0.4589622915 0.2345169932 0.4425179958 - 0.4613293111 0.2360569984 0.4446097016 - 0.4636883140 0.2375967056 0.4467011988 - 0.4660438001 0.2391356975 0.4487924874 - 0.4683940113 0.2406754941 0.4508841932 - 0.4707357883 0.2422166020 0.4529756010 - 0.4730732143 0.2437583953 0.4550662041 - 0.4754005075 0.2453003973 0.4571551979 - 0.4777207971 0.2468425930 0.4592432082 - 0.4800347984 0.2483848035 0.4613265097 - 0.4823366106 0.2499269992 0.4634082913 - 0.4846338928 0.2514702976 0.4654878080 - 0.4869211912 0.2530143857 0.4675660133 - 0.4892016053 0.2545599937 0.4696435928 - 0.4914767146 0.2561056912 0.4717212021 - 0.4937387109 0.2576513886 0.4737991989 - 0.4959957898 0.2591978014 0.4758777916 - 0.4982388020 0.2607448995 0.4779537022 - 0.5004736185 0.2622939944 0.4800283015 - 0.5026996136 0.2638441026 0.4820936918 - 0.5049133897 0.2653951943 0.4841566086 - 0.5071231127 0.2669464946 0.4862132072 - 0.5093185902 0.2684977949 0.4882662892 - 0.5115088224 0.2700498998 0.4903163016 - 0.5136851072 0.2716039121 0.4923604131 - 0.5158513188 0.2731583118 0.4944027960 - 0.5180078149 0.2747133076 0.4964390099 - 0.5201510191 0.2762692869 0.4984740913 - 0.5222893953 0.2778258026 0.5005058050 - 0.5244101286 0.2793847024 0.5025345087 - 0.5265269279 0.2809442878 0.5045595765 - 0.5286250114 0.2825047970 0.5065774918 - 0.5307167768 0.2840653062 0.5085912943 - 0.5327941179 0.2856262922 0.5105962753 - 0.5348628163 0.2871873975 0.5125992894 - 0.5369216204 0.2887484133 0.5145884752 - 0.5389689207 0.2903096974 0.5165752769 - 0.5410090089 0.2918717861 0.5185478926 - 0.5430334806 0.2934342027 0.5205165744 - 0.5450525880 0.2949968874 0.5224738717 - 0.5470547080 0.2965593040 0.5244258046 - 0.5490540862 0.2981210053 0.5263680816 - 0.5510348082 0.2996825874 0.5283023715 - 0.5530139804 0.3012439907 0.5302276015 - 0.5549759865 0.3028059006 0.5321443081 - 0.5569360852 0.3043687046 0.5340541005 - 0.5588794947 0.3059312105 0.5359538794 - 0.5608198047 0.3074932992 0.5378476977 - 0.5627439022 0.3090555072 0.5397273898 - 0.5646646023 0.3106175959 0.5416002274 - 0.5665714145 0.3121803999 0.5434591770 - 0.5684738755 0.3137443066 0.5453127027 - 0.5703607798 0.3153086901 0.5471491814 - 0.5722435713 0.3168740869 0.5489794016 - 0.5741115808 0.3184395134 0.5507919192 - 0.5759764910 0.3200049996 0.5525978208 - 0.5778291225 0.3215703964 0.5543872118 - 0.5796784163 0.3231351078 0.5561701059 - 0.5815119147 0.3246999085 0.5579361916 - 0.5833426714 0.3262651861 0.5596947074 - 0.5851539969 0.3278304935 0.5614392757 - 0.5869635940 0.3293960094 0.5631760955 - 0.5887479186 0.3309614956 0.5649011731 - 0.5905321836 0.3325259984 0.5666179061 - 0.5922917128 0.3340902925 0.5683240891 - 0.5940505266 0.3356544077 0.5700199008 - 0.5957903862 0.3372184038 0.5717071891 - 0.5975276232 0.3387834132 0.5733817816 - 0.5992469788 0.3403491974 0.5750491023 - 0.6009606719 0.3419148922 0.5766983032 - 0.6026608944 0.3434804976 0.5783439875 - 0.6043530107 0.3450458050 0.5799701810 - 0.6060341001 0.3466094136 0.5815963149 - 0.6077030897 0.3481729925 0.5831990242 - 0.6093657017 0.3497354984 0.5848017931 - 0.6110138893 0.3512978852 0.5863847136 - 0.6126598120 0.3528620899 0.5879638195 - 0.6142886281 0.3544270992 0.5895280838 - 0.6159173846 0.3559925854 0.5910843015 - 0.6175265908 0.3575589955 0.5926306248 - 0.6191338897 0.3591251969 0.5941641927 - 0.6207273006 0.3606910110 0.5956931114 - 0.6223142743 0.3622567058 0.5972054005 - 0.6238920093 0.3638209105 0.5987178087 - 0.6254580021 0.3653847873 0.6002131104 - 0.6270201802 0.3669480979 0.6017081141 - 0.6285654902 0.3685106933 0.6031907797 - 0.6301108003 0.3700729907 0.6046687961 - 0.6316359043 0.3716341853 0.6061378717 - 0.6331589222 0.3731954098 0.6075956225 - 0.6346688867 0.3747552931 0.6090502143 - 0.6361703277 0.3763149977 0.6104860902 - 0.6376655102 0.3778744936 0.6119219065 - 0.6391450763 0.3794336915 0.6133409142 - 0.6406247020 0.3809930086 0.6147564054 - 0.6420868039 0.3825530112 0.6161623001 - 0.6435478926 0.3841128945 0.6175583005 - 0.6449993253 0.3856733143 0.6189519763 - 0.6464440227 0.3872339129 0.6203303933 - 0.6478844881 0.3887940943 0.6217089295 - 0.6493095756 0.3903535903 0.6230769157 - 0.6507347226 0.3919129968 0.6244416833 - 0.6521434188 0.3934705853 0.6258000135 - 0.6535484791 0.3950277865 0.6271470785 - 0.6549462080 0.3965846002 0.6284943223 - 0.6563327909 0.3981403112 0.6298246980 - 0.6577193737 0.3996961117 0.6311542988 - 0.6590902805 0.4012506902 0.6324747205 - 0.6604607105 0.4028050900 0.6337866187 - 0.6618214250 0.4043591022 0.6350975037 - 0.6631729007 0.4059123993 0.6363947988 - 0.6645237207 0.4074656069 0.6376919746 - 0.6658543944 0.4090178013 0.6389808059 - 0.6671850085 0.4105696082 0.6402636170 - 0.6685047150 0.4121212065 0.6415452957 - 0.6698150039 0.4136717916 0.6428160071 - 0.6711249948 0.4152224064 0.6440867186 - 0.6724179983 0.4167717099 0.6453511119 - 0.6737111211 0.4183202982 0.6466106772 - 0.6749945283 0.4198687077 0.6478698850 - 0.6762676239 0.4214153886 0.6491171122 - 0.6775407195 0.4229620993 0.6503642201 - 0.6787930131 0.4245072901 0.6516051292 - 0.6800435781 0.4260506034 0.6528393030 - 0.6812877059 0.4275940061 0.6540734768 - 0.6825190187 0.4291338921 0.6552976966 - 0.6837502718 0.4306730926 0.6565207243 - 0.6849684715 0.4322116971 0.6577410102 - 0.6861820817 0.4337471128 0.6589547992 - 0.6873937249 0.4352824986 0.6601685882 - 0.6885895133 0.4368155897 0.6613742113 - 0.6897851825 0.4383459091 0.6625760198 - 0.6909723282 0.4398762882 0.6637775898 - 0.6921495199 0.4414024055 0.6649709940 - 0.6933267117 0.4429270029 0.6661643982 - 0.6944912076 0.4444515109 0.6673536897 - 0.6956524253 0.4459707141 0.6685358882 - 0.6968116164 0.4474897981 0.6697182059 - 0.6979551911 0.4490073919 0.6708937287 - 0.6990987062 0.4505204856 0.6720660925 - 0.7002366185 0.4520334899 0.6732385159 - 0.7013661265 0.4535439909 0.6744014025 - 0.7024955750 0.4550516903 0.6755638719 - 0.7036141753 0.4565593898 0.6767243743 - 0.7047271729 0.4580627978 0.6778780818 - 0.7058401704 0.4595640898 0.6790319085 - 0.7069379091 0.4610654116 0.6801804900 - 0.7080339789 0.4625608921 0.6813232899 - 0.7091283202 0.4640552998 0.6824662089 - 0.7102094293 0.4655497074 0.6836034060 - 0.7112904191 0.4670377970 0.6847382784 - 0.7123653293 0.4685258865 0.6858730912 - 0.7134271860 0.4700129926 0.6869965792 - 0.7144889832 0.4714944065 0.6881188154 - 0.7155439258 0.4729757905 0.6892403960 - 0.7165915966 0.4744558036 0.6903538108 - 0.7176393867 0.4759314060 0.6914672256 - 0.7186779976 0.4774070978 0.6925780177 - 0.7197117209 0.4788806140 0.6936802268 - 0.7207453847 0.4803498089 0.6947823763 - 0.7217658758 0.4818190932 0.6958813071 - 0.7227824926 0.4832853973 0.6969739795 - 0.7237992287 0.4847474098 0.6980667114 - 0.7248027921 0.4862093031 0.6991550922 - 0.7258046269 0.4876680970 0.7002385855 - 0.7268064022 0.4891225100 0.7013220787 - 0.7277902961 0.4905770123 0.7024012804 - 0.7287734151 0.4920283854 0.7034770250 - 0.7297564149 0.4934760034 0.7045525908 - 0.7307261825 0.4949235916 0.7056220770 - 0.7316960096 0.4963681996 0.7066873908 - 0.7326653004 0.4978089035 0.7077527046 - 0.7336220741 0.4992494881 0.7088124752 - 0.7345790267 0.5006874204 0.7098689079 - 0.7355352044 0.5021209121 0.7109252810 - 0.7364767194 0.5035544038 0.7119749784 - 0.7374181151 0.5049852133 0.7130206823 - 0.7383592725 0.5064104795 0.7140665054 - 0.7392876148 0.5078356862 0.7151080966 - 0.7402158976 0.5092589259 0.7161473036 - 0.7411442995 0.5106753111 0.7171863914 - 0.7420564294 0.5120915771 0.7182205915 - 0.7429682016 0.5135068893 0.7192509770 - 0.7438799143 0.5149142742 0.7202814221 - 0.7447810173 0.5163217187 0.7213069797 - 0.7456809878 0.5177291036 0.7223277092 - 0.7465808988 0.5191293955 0.7233483791 - 0.7474709153 0.5205295086 0.7243651152 - 0.7483584285 0.5219296217 0.7253757119 - 0.7492460012 0.5233250260 0.7263864279 - 0.7501245141 0.5247194171 0.7273939848 - 0.7509989738 0.5261136889 0.7283943295 - 0.7518733740 0.5275031924 0.7293944955 - 0.7527413964 0.5288894773 0.7303934097 - 0.7536038756 0.5302758813 0.7313838005 - 0.7544664145 0.5316587090 0.7323741913 - 0.7553241849 0.5330361724 0.7333645821 - 0.7561736703 0.5344135761 0.7343451977 - 0.7570232749 0.5357897282 0.7353253961 - 0.7578713298 0.5371581912 0.7363057137 - 0.7587116957 0.5385265946 0.7372794151 - 0.7595521212 0.5398949981 0.7382512093 - 0.7603924870 0.5412554741 0.7392228842 - 0.7612221837 0.5426148176 0.7401890755 - 0.7620512247 0.5439741015 0.7411506772 - 0.7628800869 0.5453277230 0.7421122789 - 0.7637022138 0.5466768742 0.7430707216 - 0.7645211220 0.5480260849 0.7440214753 - 0.7653400898 0.5493733883 0.7449724078 - 0.7661545873 0.5507144928 0.7459229827 - 0.7669628859 0.5520555973 0.7468630075 - 0.7677711844 0.5533967018 0.7478029132 - 0.7685785890 0.5547322035 0.7487428784 - 0.7693775296 0.5560668111 0.7496752143 - 0.7701764107 0.5574014783 0.7506042719 - 0.7709752917 0.5587329865 0.7515335083 - 0.7717691064 0.5600612760 0.7524588108 - 0.7725613713 0.5613896251 0.7533779740 - 0.7733535767 0.5627173781 0.7542970777 - 0.7741429806 0.5640394092 0.7552161217 - 0.7749279737 0.5653614998 0.7561244965 - 0.7757130265 0.5666834712 0.7570329905 - 0.7764979005 0.5680016279 0.7579414248 - 0.7772737145 0.5693175197 0.7588419914 - 0.7780495286 0.5706332922 0.7597380877 - 0.7788252831 0.5719478130 0.7606341243 - 0.7795972228 0.5732557178 0.7615277767 - 0.7803664207 0.5745636225 0.7624130845 - 0.7811354995 0.5758715272 0.7632983923 - 0.7819035053 0.5771726966 0.7641837001 - 0.7826650143 0.5784705877 0.7650598884 - 0.7834265232 0.5797685981 0.7659333944 - 0.7841879725 0.5810651779 0.7668067813 - 0.7849435806 0.5823537111 0.7676774859 - 0.7856963277 0.5836421847 0.7685415745 - 0.7864488959 0.5849307179 0.7694056034 - 0.7872005105 0.5862150788 0.7702696919 - 0.7879469991 0.5874965787 0.7711244226 - 0.7886934876 0.5887780190 0.7719767094 - 0.7894400954 0.5900595188 0.7728291154 - 0.7901812196 0.5913344026 0.7736784220 - 0.7909191251 0.5926094055 0.7745192051 - 0.7916570902 0.5938844085 0.7753601074 - 0.7923948169 0.5951570272 0.7762010098 - 0.7931283116 0.5964260101 0.7770338058 - 0.7938618064 0.5976949930 0.7778633237 - 0.7945951819 0.5989639759 0.7786927223 - 0.7953253984 0.6002289057 0.7795209885 - 0.7960519195 0.6014925241 0.7803397775 - 0.7967783809 0.6027560830 0.7811586857 - 0.7975050211 0.6040192842 0.7819775939 - 0.7982237935 0.6052780151 0.7827903032 - 0.7989413142 0.6065366864 0.7835965753 - 0.7996588945 0.6077952981 0.7844029069 - 0.8003752232 0.6090512276 0.7852091789 - 0.8010866046 0.6103022099 0.7860053778 - 0.8017979860 0.6115530729 0.7867997289 - 0.8025094271 0.6128041148 0.7875940204 - 0.8032175899 0.6140497923 0.7883868217 - 0.8039227128 0.6152924895 0.7891716957 - 0.8046277761 0.6165351868 0.7899566293 - 0.8053328991 0.6177778244 0.7907415032 - 0.8060317039 0.6190140843 0.7915207148 - 0.8067288995 0.6202492118 0.7922930717 - 0.8074262142 0.6214843988 0.7930656075 - 0.8081231117 0.6227191091 0.7938380241 - 0.8088151813 0.6239476204 0.7946025133 - 0.8095073104 0.6251760125 0.7953637838 - 0.8101993799 0.6264044046 0.7961249948 - 0.8108897805 0.6276314855 0.7968863249 - 0.8115760088 0.6288539171 0.7976366878 - 0.8122622967 0.6300764084 0.7983863950 - 0.8129485250 0.6312987804 0.7991361022 - 0.8136305213 0.6325194240 0.7998843193 - 0.8143079281 0.6337367296 0.8006227016 - 0.8149852753 0.6349539161 0.8013610244 - 0.8156628013 0.6361712217 0.8020994067 - 0.8163372278 0.6373847723 0.8028339744 - 0.8170101047 0.6385943294 0.8035594225 - 0.8176829219 0.6398037076 0.8042848110 - 0.8183556795 0.6410130858 0.8050101995 - 0.8190233111 0.6422181129 0.8057312965 - 0.8196895123 0.6434193850 0.8064466119 - 0.8203555942 0.6446207166 0.8071619272 - 0.8210217953 0.6458219290 0.8078771830 - 0.8216826916 0.6470190883 0.8085857034 - 0.8223428726 0.6482132077 0.8092882037 - 0.8230029941 0.6494073272 0.8099907041 - 0.8236631751 0.6506013870 0.8106932044 - 0.8243181705 0.6517918706 0.8113890290 - 0.8249729872 0.6529797912 0.8120803833 - 0.8256276846 0.6541677117 0.8127716780 - 0.8262825012 0.6553556919 0.8134629726 - 0.8269299865 0.6565406919 0.8141471148 - 0.8275772929 0.6577234864 0.8148270845 - 0.8282247186 0.6589062214 0.8155071139 - 0.8288720250 0.6600890160 0.8161870837 - 0.8295140862 0.6612688899 0.8168601990 - 0.8301560879 0.6624459028 0.8175293803 - 0.8307980895 0.6636229157 0.8181986213 - 0.8314400911 0.6647999287 0.8188679218 - 0.8320763707 0.6659740210 0.8195297122 - 0.8327124119 0.6671438217 0.8201869130 - 0.8333482742 0.6683136821 0.8208439946 - 0.8339843154 0.6694836020 0.8215011954 - 0.8346142769 0.6706516147 0.8221533895 - 0.8352434039 0.6718149185 0.8228014708 - 0.8358724713 0.6729781032 0.8234496117 - 0.8365015984 0.6741414070 0.8240976930 - 0.8371255994 0.6753039956 0.8247407079 - 0.8377479911 0.6764618754 0.8253772855 - 0.8383705020 0.6776198149 0.8260139823 - 0.8389930129 0.6787776947 0.8266506195 - 0.8396112919 0.6799355149 0.8272842765 - 0.8402273059 0.6810882092 0.8279114962 - 0.8408433199 0.6822404265 0.8285385966 - 0.8414592743 0.6833925843 0.8291658163 - 0.8420726061 0.6845448017 0.8297916055 - 0.8426827788 0.6856936216 0.8304092884 - 0.8432930112 0.6868407726 0.8310269713 - 0.8439031839 0.6879879832 0.8316445947 - 0.8445109129 0.6891351938 0.8322622776 - 0.8451119065 0.6902797818 0.8328717947 - 0.8457129002 0.6914209127 0.8334808946 - 0.8463138938 0.6925619245 0.8340898752 - 0.8469148278 0.6937029958 0.8346989155 - 0.8475095034 0.6948431730 0.8353011012 - 0.8481041789 0.6959772110 0.8359006047 - 0.8486989141 0.6971111894 0.8364999890 - 0.8492935896 0.6982452273 0.8370993733 - 0.8498833179 0.6993792057 0.8376960158 - 0.8504714966 0.7005075812 0.8382890224 - 0.8510596156 0.7016344070 0.8388819098 - 0.8516476750 0.7027612925 0.8394749165 - 0.8522318006 0.7038881183 0.8400666118 - 0.8528108001 0.7050123215 0.8406503797 - 0.8533899188 0.7061328888 0.8412342072 - 0.8539689779 0.7072533965 0.8418179154 - 0.8545476198 0.7083739042 0.8424016833 - 0.8551219106 0.7094944119 0.8429775238 - 0.8556962013 0.7106081247 0.8435510993 - 0.8562703729 0.7117217183 0.8441246748 - 0.8568447232 0.7128353715 0.8446984291 - 0.8574144244 0.7139490843 0.8452690244 - 0.8579822183 0.7150586843 0.8458353281 - 0.8585500717 0.7161651254 0.8464015722 - 0.8591179252 0.7172715068 0.8469678760 - 0.8596833944 0.7183778286 0.8475341201 - 0.8602436185 0.7194836736 0.8480933905 - 0.8608037233 0.7205830812 0.8486527205 - 0.8613638282 0.7216826081 0.8492119908 - 0.8619239926 0.7227820158 0.8497712016 - 0.8624796271 0.7238814235 0.8503255248 - 0.8630344272 0.7249767780 0.8508757949 - 0.8635892272 0.7260689139 0.8514261246 - 0.8641440272 0.7271611094 0.8519762754 - 0.8646966219 0.7282531857 0.8525258899 - 0.8652452826 0.7293453217 0.8530645967 - 0.8657941222 0.7304283977 0.8536034226 - 0.8663427830 0.7315114737 0.8541421890 - 0.8668915033 0.7325944901 0.8546810150 - 0.8674352169 0.7336776257 0.8552160859 - 0.8679779172 0.7347571850 0.8557479978 - 0.8685206771 0.7358316183 0.8562799096 - 0.8690633774 0.7369059920 0.8568118811 - 0.8696042895 0.7379804254 0.8573437929 - 0.8701401949 0.7390547991 0.8578673005 - 0.8706761003 0.7401224971 0.8583906293 - 0.8712120056 0.7411879897 0.8589140177 - 0.8717479110 0.7422536016 0.8594372869 - 0.8722802997 0.7433190942 0.8599570990 - 0.8728110194 0.7443835735 0.8604711294 - 0.8733416796 0.7454379797 0.8609851003 - 0.8738722801 0.7464923263 0.8614990711 - 0.8744028211 0.7475466728 0.8620131016 - 0.8749272823 0.7486010790 0.8625208735 - 0.8754518032 0.7496516109 0.8630263805 - 0.8759763241 0.7506958246 0.8635318875 - 0.8765007854 0.7517400980 0.8640373945 - 0.8770225048 0.7527843118 0.8645426035 - 0.8775386810 0.7538285851 0.8650417924 - 0.8780549765 0.7548667789 0.8655411005 - 0.8785712123 0.7559013963 0.8660402894 - 0.8790875077 0.7569360733 0.8665394783 - 0.8796002269 0.7579708099 0.8670362830 - 0.8801108003 0.7590054870 0.8675283790 - 0.8806213737 0.7600315213 0.8680204153 - 0.8811320066 0.7610560060 0.8685125113 - 0.8816425800 0.7620804906 0.8690046072 - 0.8821474910 0.7631049156 0.8694908023 - 0.8826515079 0.7641292214 0.8699728847 - 0.8831555247 0.7651435733 0.8704550266 - 0.8836596012 0.7661579251 0.8709372282 - 0.8841630220 0.7671722770 0.8714193106 - 0.8846591115 0.7681866288 0.8718963265 - 0.8851552010 0.7691996098 0.8723719716 - 0.8856512904 0.7702046037 0.8728476167 - 0.8861473799 0.7712097168 0.8733233213 - 0.8866416812 0.7722147703 0.8737990260 - 0.8871303201 0.7732198238 0.8742687106 - 0.8876188993 0.7742217779 0.8747382164 - 0.8881074786 0.7752138972 0.8752077222 - 0.8885961771 0.7762060761 0.8756772876 - 0.8890824914 0.7771981955 0.8761461973 - 0.8895648718 0.7781903744 0.8766090274 - 0.8900471926 0.7791786194 0.8770719171 - 0.8905295730 0.7801573277 0.8775346875 - 0.8910118937 0.7811360955 0.8779975772 - 0.8914917707 0.7821148038 0.8784590960 - 0.8919684887 0.7830935121 0.8789139986 - 0.8924452066 0.7840679288 0.8793689013 - 0.8929218054 0.7850319743 0.8798238039 - 0.8933985233 0.7859961987 0.8802787066 - 0.8938719034 0.7869604230 0.8807321787 - 0.8943415880 0.7879245877 0.8811805248 - 0.8948113918 0.7888851762 0.8816288114 - 0.8952810764 0.7898358107 0.8820770979 - 0.8957508206 0.7907865047 0.8825255036 - 0.8962174058 0.7917371988 0.8829724789 - 0.8966804147 0.7926878929 0.8834149837 - 0.8971434832 0.7936357856 0.8838574886 - 0.8976066113 0.7945719957 0.8843001127 - 0.8980696201 0.7955083251 0.8847426176 - 0.8985310793 0.7964445949 0.8851838708 - 0.8989905119 0.7973809242 0.8856199980 - 0.8994498253 0.7983161807 0.8860560060 - 0.8999091983 0.7992392778 0.8864920735 - 0.9003685713 0.8001623750 0.8869280815 - 0.9008257985 0.8010855913 0.8873633742 - 0.9012789726 0.8020086884 0.8877922297 - 0.9017320871 0.8029319048 0.8882209063 - 0.9021853209 0.8038424253 0.8886497021 - 0.9026384950 0.8047516942 0.8890783787 - 0.9030898213 0.8056610227 0.8895071149 - 0.9035350084 0.8065704107 0.8899301887 - 0.9039803147 0.8074796796 0.8903532028 - 0.9044255018 0.8083794117 0.8907762170 - 0.9048708081 0.8092753291 0.8911991119 - 0.9053156972 0.8101711869 0.8916221261 - 0.9057556987 0.8110671043 0.8920407891 - 0.9061955810 0.8119630218 0.8924584985 - 0.9066355824 0.8128508925 0.8928763270 - 0.9070755839 0.8137295842 0.8932939768 - 0.9075155854 0.8146083951 0.8937116861 - 0.9079490900 0.8154872060 0.8941258192 - 0.9083815813 0.8163658977 0.8945375085 - 0.9088140726 0.8172419071 0.8949491978 - 0.9092466235 0.8181051016 0.8953608274 - 0.9096791148 0.8189684153 0.8957725167 - 0.9101070166 0.8198316097 0.8961812854 - 0.9105319977 0.8206949234 0.8965846896 - 0.9109569192 0.8215581179 0.8969882131 - 0.9113819003 0.8224077821 0.8973916173 - 0.9118068218 0.8232551813 0.8977950215 - 0.9122298956 0.8241025209 0.8981981874 - 0.9126486778 0.8249498010 0.8985965848 - 0.9130674005 0.8257970810 0.8989951015 - 0.9134861827 0.8266378045 0.8993936181 - 0.9139049053 0.8274710178 0.8997920156 - 0.9143236279 0.8283041716 0.9001904726 - 0.9147337079 0.8291373849 0.9005855918 - 0.9151433706 0.8299707174 0.9009795189 - 0.9155529737 0.8308032155 0.9013733864 - 0.9159626961 0.8316221237 0.9017671943 - 0.9163724184 0.8324409723 0.9021611214 - 0.9167771935 0.8332599998 0.9025527239 - 0.9171783924 0.8340789080 0.9029397964 - 0.9175795913 0.8348978758 0.9033269286 - 0.9179807901 0.8357083797 0.9037140012 - 0.9183819294 0.8365128040 0.9041010737 - 0.9187824130 0.8373172879 0.9044882059 - 0.9191781282 0.8381217718 0.9048687220 - 0.9195739031 0.8389261961 0.9052485824 - 0.9199696779 0.8397294283 0.9056286216 - 0.9203655124 0.8405168056 0.9060084820 - 0.9207612276 0.8413041234 0.9063884020 - 0.9211524725 0.8420915008 0.9067658186 - 0.9215413928 0.8428788185 0.9071398973 - 0.9219303727 0.8436661959 0.9075140953 - 0.9223194122 0.8444454074 0.9078881741 - 0.9227082729 0.8452168703 0.9082623720 - 0.9230962992 0.8459883928 0.9086365104 - 0.9234783053 0.8467599750 0.9090055227 - 0.9238603711 0.8475314975 0.9093741179 - 0.9242423773 0.8483030796 0.9097427130 - 0.9246245027 0.8490607738 0.9101111889 - 0.9250065088 0.8498172760 0.9104797840 - 0.9253851175 0.8505737185 0.9108464122 - 0.9257611036 0.8513301015 0.9112095237 - 0.9261372089 0.8520866036 0.9115725756 - 0.9265131950 0.8528388739 0.9119356871 - 0.9268891811 0.8535797000 0.9122989178 - 0.9272652864 0.8543204069 0.9126620293 - 0.9276344180 0.8550611734 0.9130206108 - 0.9280033708 0.8558018804 0.9133777022 - 0.9283723235 0.8565427065 0.9137349129 - 0.9287412763 0.8572751880 0.9140920043 - 0.9291102290 0.8580012918 0.9144490957 - 0.9294773936 0.8587272763 0.9148054719 - 0.9298406839 0.8594533801 0.9151548147 - 0.9302039742 0.8601794839 0.9155040979 - 0.9305673838 0.8609055877 0.9158533812 - 0.9309306741 0.8616189957 0.9162026048 - 0.9312940240 0.8623296022 0.9165518880 - 0.9316536784 0.8630402088 0.9168993831 - 0.9320111275 0.8637508750 0.9172441959 - 0.9323683977 0.8644614816 0.9175890088 - 0.9327257872 0.8651716709 0.9179338217 - 0.9330831170 0.8658667207 0.9182785153 - 0.9334405065 0.8665615916 0.9186233282 - 0.9337912798 0.8672565818 0.9189655781 - 0.9341409802 0.8679515123 0.9193063974 - 0.9344906211 0.8686465025 0.9196472168 - 0.9348403215 0.8693385124 0.9199880958 - 0.9351899028 0.8700178266 0.9203289151 - 0.9355393052 0.8706970811 0.9206697941 - 0.9358832836 0.8713763952 0.9210057259 - 0.9362273216 0.8720557094 0.9213408232 - 0.9365713000 0.8727350235 0.9216758013 - 0.9369152188 0.8734096289 0.9220108986 - 0.9372591972 0.8740733862 0.9223458767 - 0.9376019835 0.8747370839 0.9226809740 - 0.9379401803 0.8754009008 0.9230101109 - 0.9382783771 0.8760647178 0.9233391881 - 0.9386165738 0.8767284155 0.9236683846 - 0.9389547706 0.8773869276 0.9239975214 - 0.9392930269 0.8780362010 0.9243265986 - 0.9396293163 0.8786854148 0.9246553183 - 0.9399607778 0.8793346882 0.9249799252 - 0.9402924180 0.8799839020 0.9253045917 - 0.9406239986 0.8806331754 0.9256293178 - 0.9409555793 0.8812769055 0.9259539247 - 0.9412871003 0.8819113970 0.9262785912 - 0.9416161776 0.8825458288 0.9266026020 - 0.9419407248 0.8831803203 0.9269232750 - 0.9422652125 0.8838148117 0.9272438884 - 0.9425897002 0.8844493032 0.9275646210 - 0.9429141283 0.8850783110 0.9278851748 - 0.9432386160 0.8856968284 0.9282059073 - 0.9435608983 0.8863152862 0.9285259247 - 0.9438794255 0.8869338036 0.9288429022 - 0.9441977739 0.8875523210 0.9291598797 - 0.9445161819 0.8881707191 0.9294769168 - 0.9448347092 0.8887856007 0.9297938943 - 0.9451531172 0.8893899918 0.9301108718 - 0.9454690218 0.8899943829 0.9304273725 - 0.9457793236 0.8905987740 0.9307404160 - 0.9460895061 0.8912032247 0.9310532808 - 0.9463998079 0.8918076158 0.9313663244 - 0.9467099905 0.8924102783 0.9316793084 - 0.9470202923 0.8930001259 0.9319922924 - 0.9473289847 0.8935899734 0.9323049784 - 0.9476320744 0.8941798210 0.9326118827 - 0.9479352236 0.8947697282 0.9329187870 - 0.9482381940 0.8953595161 0.9332256913 - 0.9485412836 0.8959493041 0.9335324764 - 0.9488443136 0.8965260983 0.9338393807 - 0.9491471052 0.8971018195 0.9341462851 - 0.9494447708 0.8976774812 0.9344497919 - 0.9497426152 0.8982532024 0.9347528815 - 0.9500402808 0.8988289833 0.9350559711 - 0.9503381252 0.8994047046 0.9353591204 - 0.9506357908 0.8999707103 0.9356622100 - 0.9509335756 0.9005324244 0.9359652996 - 0.9512249827 0.9010940790 0.9362673163 - 0.9515153766 0.9016557932 0.9365687966 - 0.9518058896 0.9022175074 0.9368702769 - 0.9520962834 0.9027792215 0.9371718764 - 0.9523866773 0.9033352137 0.9374734163 - 0.9526770711 0.9038825035 0.9377748966 - 0.9529634118 0.9044299126 0.9380748272 - 0.9532471895 0.9049773216 0.9383726716 - 0.9535309076 0.9055246711 0.9386705756 - 0.9538146257 0.9060720801 0.9389684796 - 0.9540982842 0.9066187143 0.9392663836 - 0.9543820024 0.9071521163 0.9395642877 - 0.9546641707 0.9076855183 0.9398614168 - 0.9549428821 0.9082189202 0.9401535988 - 0.9552217126 0.9087523222 0.9404457808 - 0.9555004835 0.9092857242 0.9407380819 - 0.9557793140 0.9098191261 0.9410303235 - 0.9560580850 0.9103447795 0.9413226247 - 0.9563369155 0.9108663797 0.9416148067 - 0.9566090703 0.9113880992 0.9419016838 - 0.9568811059 0.9119098186 0.9421873093 - 0.9571530819 0.9124314785 0.9424728751 - 0.9574251175 0.9129531980 0.9427586198 - 0.9576970935 0.9134721160 0.9430441856 - 0.9579691291 0.9139804244 0.9433298111 - 0.9582366943 0.9144887924 0.9436146021 - 0.9585008025 0.9149972200 0.9438980818 - 0.9587647915 0.9155055285 0.9441816807 - 0.9590288997 0.9160138965 0.9444652200 - 0.9592928886 0.9165222049 0.9447486997 - 0.9595569968 0.9170212150 0.9450322986 - 0.9598206282 0.9175164104 0.9453157783 - 0.9600790739 0.9180114865 0.9455965161 - 0.9603375793 0.9185066223 0.9458770156 - 0.9605960846 0.9190018177 0.9461575150 - 0.9608544707 0.9194968939 0.9464380145 - 0.9611129761 0.9199900031 0.9467185140 - 0.9613714814 0.9204719067 0.9469990134 - 0.9616270065 0.9209538102 0.9472776055 - 0.9618802071 0.9214357138 0.9475535154 - 0.9621335268 0.9219176173 0.9478294253 - 0.9623867273 0.9223995209 0.9481052756 - 0.9626399875 0.9228814244 0.9483813047 - 0.9628931880 0.9233567119 0.9486572146 - 0.9631463885 0.9238265157 0.9489331245 - 0.9633926153 0.9242962003 0.9492051005 - 0.9636389017 0.9247658849 0.9494764805 - 0.9638851285 0.9252356887 0.9497479796 - 0.9641314149 0.9257053733 0.9500194192 - 0.9643775821 0.9261751771 0.9502907991 - 0.9646239281 0.9266350865 0.9505621791 - 0.9648674726 0.9270936847 0.9508329034 - 0.9651070237 0.9275522232 0.9511013031 - 0.9653465152 0.9280108213 0.9513697028 - 0.9655861259 0.9284693003 0.9516382217 - 0.9658256173 0.9289278984 0.9519066215 - 0.9660651088 0.9293845296 0.9521750212 - 0.9663046002 0.9298309088 0.9524434209 - 0.9665402174 0.9302772880 0.9527102113 - 0.9667741060 0.9307237267 0.9529756904 - 0.9670081139 0.9311701059 0.9532412291 - 0.9672421217 0.9316164851 0.9535068274 - 0.9674760103 0.9320629239 0.9537723064 - 0.9677100182 0.9325053096 0.9540377855 - 0.9679439068 0.9329410195 0.9543033838 - 0.9681721926 0.9333767891 0.9545660019 - 0.9684002995 0.9338126183 0.9548282027 - 0.9686284065 0.9342483282 0.9550904036 - 0.9688565135 0.9346840978 0.9553526044 - 0.9690846205 0.9351198077 0.9556146860 - 0.9693127275 0.9355502129 0.9558768868 - 0.9695394039 0.9359760880 0.9561389089 - 0.9697601199 0.9364020228 0.9563990831 - 0.9699807763 0.9368280172 0.9566591978 - 0.9702014923 0.9372538924 0.9569193125 - 0.9704222083 0.9376798272 0.9571794271 - 0.9706429243 0.9381058216 0.9574394822 - 0.9708635807 0.9385251999 0.9576995969 - 0.9710816145 0.9389411807 0.9579588175 - 0.9712945223 0.9393572211 0.9582151175 - 0.9715074897 0.9397732019 0.9584714174 - 0.9717203975 0.9401893020 0.9587277770 - 0.9719334245 0.9406052828 0.9589840770 - 0.9721462727 0.9410213232 0.9592403769 - 0.9723592997 0.9414290190 0.9594966769 - 0.9725701809 0.9418333769 0.9597517252 - 0.9727786779 0.9422376752 0.9600039721 - 0.9729871154 0.9426419735 0.9602563977 - 0.9731954932 0.9430463910 0.9605087042 - 0.9734039903 0.9434506893 0.9607610106 - 0.9736124277 0.9438549876 0.9610133767 - 0.9738209248 0.9442532063 0.9612656832 - 0.9740266800 0.9446488023 0.9615169764 - 0.9742298126 0.9450443983 0.9617664814 - 0.9744328260 0.9454399943 0.9620159864 - 0.9746358991 0.9458355904 0.9622653723 - 0.9748389721 0.9462311864 0.9625148773 - 0.9750419855 0.9466267824 0.9627643824 - 0.9752451181 0.9470162988 0.9630138874 - 0.9754452705 0.9474024177 0.9632629156 - 0.9756422043 0.9477884769 0.9635112882 - 0.9758390784 0.9481745958 0.9637596011 - 0.9760360122 0.9485607743 0.9640079737 - 0.9762328863 0.9489468932 0.9642562866 - 0.9764298201 0.9493330121 0.9645047188 - 0.9766266942 0.9497143030 0.9647529721 - 0.9768214822 0.9500910044 0.9650006294 - 0.9770131111 0.9504678249 0.9652460814 - 0.9772046208 0.9508445859 0.9654915929 - 0.9773961902 0.9512214065 0.9657371044 - 0.9775878191 0.9515982270 0.9659826159 - 0.9777793884 0.9519749284 0.9662281275 - 0.9779708982 0.9523478746 0.9664735794 - 0.9781610966 0.9527137876 0.9667181969 - 0.9783474207 0.9530795813 0.9669579864 - 0.9785336852 0.9534453750 0.9671977758 - 0.9787198901 0.9538112879 0.9674376845 - 0.9789062142 0.9541770816 0.9676774740 - 0.9790924191 0.9545428753 0.9679173231 - 0.9792786837 0.9549077153 0.9681571126 - 0.9794644713 0.9552628994 0.9683969021 - 0.9796441793 0.9556182027 0.9686303735 - 0.9798238873 0.9559733868 0.9688637257 - 0.9800035954 0.9563286901 0.9690970182 - 0.9801833034 0.9566838741 0.9693303108 - 0.9803630114 0.9570391774 0.9695636034 - 0.9805427194 0.9573944807 0.9697968960 - 0.9807224274 0.9577432275 0.9700301886 - 0.9808962941 0.9580901861 0.9702622294 - 0.9810687900 0.9584373236 0.9704936147 - 0.9812414050 0.9587844014 0.9707251191 - 0.9814139009 0.9591314793 0.9709565043 - 0.9815865159 0.9594786167 0.9711880088 - 0.9817590117 0.9598256946 0.9714193940 - 0.9819316268 0.9601699710 0.9716508985 - 0.9821007848 0.9605106115 0.9718813896 - 0.9822667241 0.9608513117 0.9721106887 - 0.9824326038 0.9611920118 0.9723399878 - 0.9825986028 0.9615327120 0.9725692868 - 0.9827644825 0.9618732929 0.9727985859 - 0.9829304218 0.9622139931 0.9730278850 - 0.9830964208 0.9625545144 0.9732571840 - 0.9832617044 0.9628877044 0.9734864235 - 0.9834232926 0.9632207751 0.9737125039 - 0.9835848212 0.9635540247 0.9739385843 - 0.9837464094 0.9638870955 0.9741647243 - 0.9839078784 0.9642202258 0.9743908048 - 0.9840695262 0.9645534158 0.9746168852 - 0.9842309952 0.9648864865 0.9748430252 - 0.9843925834 0.9652161002 0.9750691056 - 0.9845516086 0.9655424953 0.9752932191 - 0.9847096205 0.9658688903 0.9755160809 - 0.9848675132 0.9661952853 0.9757388830 - 0.9850255251 0.9665216804 0.9759618044 - 0.9851834178 0.9668480754 0.9761847258 - 0.9853413105 0.9671744704 0.9764075875 - 0.9854993224 0.9675009251 0.9766303897 - 0.9856563210 0.9678199291 0.9768530726 - 0.9858093262 0.9681388736 0.9770743251 - 0.9859623909 0.9684578776 0.9772955179 - 0.9861153960 0.9687768817 0.9775167704 - 0.9862685204 0.9690958858 0.9777380228 - 0.9864215255 0.9694147706 0.9779592156 - 0.9865745902 0.9697337747 0.9781805277 - 0.9867275953 0.9700502753 0.9784017205 - 0.9868773222 0.9703627229 0.9786214232 - 0.9870250225 0.9706751704 0.9788399935 - 0.9871727824 0.9709876180 0.9790586233 - 0.9873206019 0.9713000059 0.9792770743 - 0.9874683022 0.9716125131 0.9794957042 - 0.9876161218 0.9719249010 0.9797142744 - 0.9877638221 0.9722372890 0.9799329042 - 0.9879115224 0.9725446105 0.9801514149 - 0.9880549908 0.9728497267 0.9803667068 - 0.9881983995 0.9731547832 0.9805818200 - 0.9883419275 0.9734600186 0.9807968736 - 0.9884853959 0.9737650752 0.9810119867 - 0.9886289239 0.9740701914 0.9812270999 - 0.9887723923 0.9743754268 0.9814422131 - 0.9889158010 0.9746804833 0.9816573262 - 0.9890578985 0.9749789238 0.9818714261 - 0.9891973138 0.9752771854 0.9820829034 - 0.9893366098 0.9755755067 0.9822943211 - 0.9894760251 0.9758738279 0.9825057983 - 0.9896153212 0.9761720896 0.9827172160 - 0.9897546768 0.9764702916 0.9829286933 - 0.9898939729 0.9767686129 0.9831401110 - 0.9900333881 0.9770656824 0.9833515882 - 0.9901698232 0.9773573279 0.9835612774 - 0.9903042912 0.9776489139 0.9837695956 - 0.9904388189 0.9779404998 0.9839779139 - 0.9905732870 0.9782320857 0.9841861725 - 0.9907078147 0.9785236716 0.9843944907 - 0.9908422828 0.9788153172 0.9846028090 - 0.9909768105 0.9791069031 0.9848111272 - 0.9911112785 0.9793964028 0.9850193858 - 0.9912419915 0.9796813726 0.9852252007 - 0.9913715720 0.9799662828 0.9854300022 - 0.9915010929 0.9802513123 0.9856348038 - 0.9916306734 0.9805362821 0.9858396053 - 0.9917603135 0.9808213115 0.9860445261 - 0.9918898940 0.9811062813 0.9862493277 - 0.9920194745 0.9813911915 0.9864541292 - 0.9921491146 0.9816734791 0.9866588712 - 0.9922748208 0.9819514751 0.9868602157 - 0.9923998713 0.9822294712 0.9870607853 - 0.9925251007 0.9825075269 0.9872614145 - 0.9926502705 0.9827854037 0.9874619842 - 0.9927754998 0.9830633998 0.9876626730 - 0.9929007292 0.9833413959 0.9878633022 - 0.9930258989 0.9836193919 0.9880638719 - 0.9931511283 0.9838947058 0.9882645011 - 0.9932720065 0.9841657877 0.9884613156 - 0.9933925867 0.9844368100 0.9886577129 - 0.9935132861 0.9847078919 0.9888541102 - 0.9936339855 0.9849789739 0.9890506268 - 0.9937546849 0.9852501154 0.9892470241 - 0.9938753247 0.9855211973 0.9894434214 - 0.9939960241 0.9857922196 0.9896398187 - 0.9941167235 0.9860610962 0.9898362160 - 0.9942330122 0.9863253832 0.9900293946 - 0.9943491220 0.9865897298 0.9902222157 - 0.9944652915 0.9868540168 0.9904149771 - 0.9945814013 0.9871183038 0.9906079173 - 0.9946975112 0.9873827100 0.9908006787 - 0.9948136210 0.9876469970 0.9909934998 - 0.9949297905 0.9879112840 0.9911863804 - 0.9950459003 0.9881742001 0.9913792014 - 0.9951578975 0.9884313941 0.9915676117 - 0.9952692986 0.9886885881 0.9917551875 - 0.9953806996 0.9889457822 0.9919428229 - 0.9954922199 0.9892029762 0.9921303988 - 0.9956036210 0.9894601703 0.9923179746 - 0.9957150221 0.9897174239 0.9925056100 - 0.9958264232 0.9899746180 0.9926931858 - 0.9959378242 0.9902316928 0.9928808212 - 0.9960461259 0.9904808998 0.9930642247 - 0.9961531758 0.9907299876 0.9932457209 - 0.9962602854 0.9909791946 0.9934272766 - 0.9963673949 0.9912282825 0.9936087728 - 0.9964745045 0.9914774895 0.9937903285 - 0.9965816140 0.9917266965 0.9939718843 - 0.9966887236 0.9919757843 0.9941533804 - 0.9967958927 0.9922249913 0.9943349957 - 0.9969006181 0.9924687147 0.9945136905 - 0.9970030785 0.9927104712 0.9946895838 - 0.9971057177 0.9929522276 0.9948654771 - 0.9972081780 0.9931939840 0.9950413704 - 0.9973108172 0.9934358001 0.9952173233 - 0.9974133968 0.9936776161 0.9953930974 - 0.9975159168 0.9939193726 0.9955689907 - 0.9976184964 0.9941611886 0.9957448840 - 0.9977198839 0.9943996072 0.9959198236 - 0.9978176951 0.9946330786 0.9960914254 - 0.9979155064 0.9948667288 0.9962630868 - 0.9980133176 0.9951003194 0.9964346886 - 0.9981111288 0.9953339100 0.9966062903 - 0.9982089996 0.9955673814 0.9967778921 - 0.9983068109 0.9958009720 0.9969494939 - 0.9984046221 0.9960346222 0.9971210957 - 0.9985023737 0.9962679744 0.9972926974 - 0.9985945225 0.9964916110 0.9974582791 - 0.9986860156 0.9967151880 0.9976230860 - 0.9987773895 0.9969388843 0.9977878928 - 0.9988688827 0.9971625209 0.9979526997 - 0.9989603162 0.9973860979 0.9981175065 - 0.9990518093 0.9976096749 0.9982821941 - 0.9991431832 0.9978333116 0.9984470010 - 0.9992346764 0.9980568886 0.9986118078 - 0.9993227720 0.9982761145 0.9987711906 - 0.9994074702 0.9984915853 0.9989247918 - 0.9994921088 0.9987071157 0.9990783930 - 0.9995768070 0.9989225864 0.9992319942 - 0.9996613860 0.9991381168 0.9993855953 - 0.9997460842 0.9993535280 0.9995391965 - 0.9998307228 0.9995689988 0.9996927977 - 0.9999154210 0.9997845292 0.9998463988 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0005538157 0.0002576292 0.0005585101 + 0.0012096640 0.0005333599 0.0011604551 + 0.0019140980 0.0008360103 0.0018323910 + 0.0026460199 0.0011740130 0.0025799151 + 0.0034157911 0.0015541241 0.0033877229 + 0.0042393408 0.0019793189 0.0042505912 + 0.0051223529 0.0024463851 0.0051805871 + 0.0060673542 0.0029475561 0.0061999368 + 0.0070817620 0.0034730800 0.0073256488 + 0.0081761600 0.0040138708 0.0085633332 + 0.0093593132 0.0045639272 0.0099106664 + 0.0106341401 0.0051191631 0.0113591403 + 0.0119961500 0.0056801899 0.0129020195 + 0.0134347901 0.0062479940 0.0145361200 + 0.0149379000 0.0068263910 0.0162581895 + 0.0164953694 0.0074195368 0.0180597696 + 0.0181045309 0.0080307238 0.0199336000 + 0.0197760705 0.0086633833 0.0218689796 + 0.0215226207 0.0093178684 0.0238573793 + 0.0233605802 0.0099947192 0.0258926600 + 0.0253009591 0.0106952796 0.0279722493 + 0.0273566395 0.0114182802 0.0300974604 + 0.0295376405 0.0121626100 0.0322676599 + 0.0318443291 0.0129287997 0.0344865993 + 0.0342735499 0.0137156500 0.0367620103 + 0.0368162692 0.0145225199 0.0390981287 + 0.0394631997 0.0153492000 0.0414960496 + 0.0422021896 0.0161942504 0.0439662300 + 0.0450241007 0.0170577895 0.0465124287 + 0.0479182489 0.0179391392 0.0491312891 + 0.0508695506 0.0188368801 0.0518258400 + 0.0538671017 0.0197515395 0.0545928814 + 0.0569038987 0.0206836108 0.0574216582 + 0.0599653386 0.0216330998 0.0603049994 + 0.0630441532 0.0226011295 0.0632309094 + 0.0661331788 0.0235872995 0.0661824495 + 0.0692235529 0.0245907698 0.0691514686 + 0.0723131001 0.0256121606 0.0721308514 + 0.0753963515 0.0266514607 0.0751067922 + 0.0784699991 0.0277084205 0.0780698285 + 0.0815294310 0.0287822001 0.0810127109 + 0.0845746398 0.0298725795 0.0839387029 + 0.0876072496 0.0309803504 0.0868489370 + 0.0906234607 0.0321053714 0.0897342712 + 0.0936240926 0.0332451016 0.0925956666 + 0.0966099873 0.0343989693 0.0954437479 + 0.0995746925 0.0355678201 0.0982764736 + 0.1025210023 0.0367507786 0.1010928974 + 0.1054513976 0.0379455499 0.1038966030 + 0.1083656028 0.0391518585 0.1066884995 + 0.1112613007 0.0403695591 0.1094719023 + 0.1141443029 0.0415982008 0.1122469977 + 0.1170163006 0.0428373106 0.1150157973 + 0.1198785976 0.0440876000 0.1177795008 + 0.1227298006 0.0453483313 0.1205397025 + 0.1255751997 0.0466196500 0.1232986972 + 0.1284184009 0.0479020402 0.1260581017 + 0.1312606931 0.0491929315 0.1288186014 + 0.1341048926 0.0504931509 0.1315809935 + 0.1369509995 0.0518033691 0.1343457997 + 0.1397999972 0.0531228185 0.1371127963 + 0.1426569968 0.0544515885 0.1398842931 + 0.1455242038 0.0557879396 0.1426624060 + 0.1484020054 0.0571304597 0.1454472989 + 0.1512925029 0.0584798306 0.1482387930 + 0.1541953981 0.0598362610 0.1510369033 + 0.1571146995 0.0612012111 0.1538417041 + 0.1600483954 0.0625731573 0.1566565037 + 0.1629952043 0.0639522225 0.1594796926 + 0.1659574062 0.0653370395 0.1623144001 + 0.1689333022 0.0667286515 0.1651574969 + 0.1719211042 0.0681272000 0.1680109054 + 0.1749179959 0.0695317611 0.1708731055 + 0.1779257059 0.0709433332 0.1737474054 + 0.1809400022 0.0723603070 0.1766321063 + 0.1839601994 0.0737829134 0.1795291007 + 0.1869864017 0.0752107576 0.1824384034 + 0.1900130957 0.0766445100 0.1853630990 + 0.1930415034 0.0780834034 0.1883013994 + 0.1960687935 0.0795266330 0.1912554055 + 0.1990925074 0.0809739381 0.1942231059 + 0.2021104991 0.0824264735 0.1972043961 + 0.2051243931 0.0838834122 0.2002002001 + 0.2081328928 0.0853442401 0.2032063007 + 0.2111341059 0.0868088603 0.2062257975 + 0.2141271979 0.0882768705 0.2092570961 + 0.2171119004 0.0897474736 0.2122993022 + 0.2200873941 0.0912210122 0.2153486013 + 0.2230526060 0.0926979184 0.2184022963 + 0.2260069996 0.0941769183 0.2214571983 + 0.2289530039 0.0956579819 0.2245126069 + 0.2318878025 0.0971412882 0.2275668979 + 0.2348102033 0.0986268297 0.2306147963 + 0.2377181053 0.1001135036 0.2336523980 + 0.2406145036 0.1016011015 0.2366762012 + 0.2435014993 0.1030898988 0.2396847010 + 0.2463797927 0.1045792028 0.2426739931 + 0.2492484003 0.1060694978 0.2456423044 + 0.2521043122 0.1075616032 0.2485859990 + 0.2549489141 0.1090551987 0.2515046895 + 0.2577804029 0.1105502024 0.2543984056 + 0.2605997920 0.1120468974 0.2572653890 + 0.2634068131 0.1135445982 0.2601054013 + 0.2662003040 0.1150436029 0.2629188001 + 0.2689799964 0.1165440977 0.2657040954 + 0.2717454135 0.1180453971 0.2684625983 + 0.2744967043 0.1195472032 0.2711946964 + 0.2772313952 0.1210499033 0.2738988996 + 0.2799471915 0.1225532964 0.2765769064 + 0.2826446891 0.1240585968 0.2792299092 + 0.2853254974 0.1255660951 0.2818576992 + 0.2879889011 0.1270741969 0.2844642103 + 0.2906337082 0.1285829991 0.2870489955 + 0.2932612002 0.1300923973 0.2896148860 + 0.2958734035 0.1316034049 0.2921622097 + 0.2984699905 0.1331169009 0.2946906090 + 0.3010497093 0.1346320063 0.2972028852 + 0.3036122024 0.1361477971 0.2996973991 + 0.3061597049 0.1376651973 0.3021731079 + 0.3086943924 0.1391835958 0.3046326041 + 0.3112165928 0.1407034993 0.3070755005 + 0.3137254119 0.1422256976 0.3095063865 + 0.3162207901 0.1437488943 0.3119274080 + 0.3187038004 0.1452739984 0.3143393993 + 0.3211764991 0.1468010992 0.3167423904 + 0.3236395121 0.1483293027 0.3191359937 + 0.3260940909 0.1498585939 0.3215200901 + 0.3285394907 0.1513891965 0.3238947988 + 0.3309769928 0.1529217958 0.3262606859 + 0.3334091902 0.1544554979 0.3286187053 + 0.3358364105 0.1559904069 0.3309695125 + 0.3382585049 0.1575258970 0.3333117962 + 0.3406754136 0.1590621024 0.3356468081 + 0.3430868983 0.1606000960 0.3379752934 + 0.3454923034 0.1621384025 0.3402976096 + 0.3478921950 0.1636777073 0.3426141143 + 0.3502902091 0.1652178019 0.3449253142 + 0.3526883125 0.1667584032 0.3472313881 + 0.3550874889 0.1682990938 0.3495286107 + 0.3574880064 0.1698389053 0.3518162966 + 0.3598900139 0.1713789999 0.3540945053 + 0.3622936904 0.1729196012 0.3563635945 + 0.3646990955 0.1744605005 0.3586243987 + 0.3671062887 0.1760009974 0.3608759940 + 0.3695152998 0.1775424033 0.3631196022 + 0.3719266951 0.1790847033 0.3653568029 + 0.3743407130 0.1806277931 0.3675884008 + 0.3767570853 0.1821713001 0.3698143959 + 0.3791759014 0.1837140024 0.3720313013 + 0.3815971911 0.1852564067 0.3742407858 + 0.3840225041 0.1867990047 0.3764427900 + 0.3864507079 0.1883427054 0.3786371052 + 0.3888813853 0.1898857057 0.3808222115 + 0.3913142085 0.1914276034 0.3829993010 + 0.3937469125 0.1929681003 0.3851706088 + 0.3961795866 0.1945082992 0.3873367906 + 0.3986124098 0.1960480064 0.3894971013 + 0.4010451138 0.1975871027 0.3916527927 + 0.4034779072 0.1991253942 0.3938055933 + 0.4059106112 0.2006641030 0.3959555924 + 0.4083434045 0.2022033036 0.3980999887 + 0.4107761085 0.2037439942 0.4002406001 + 0.4132089019 0.2052841932 0.4023782909 + 0.4156416953 0.2068233937 0.4045133889 + 0.4180744886 0.2083608955 0.4066438079 + 0.4205071926 0.2098989934 0.4087715149 + 0.4229399860 0.2114374042 0.4108966887 + 0.4253725111 0.2129762024 0.4130181968 + 0.4278020859 0.2145155072 0.4151366949 + 0.4302271903 0.2160546035 0.4172526896 + 0.4326483905 0.2175924033 0.4193654954 + 0.4350630045 0.2191300988 0.4214769900 + 0.4374726117 0.2206678987 0.4235875905 + 0.4398784041 0.2222069055 0.4256978929 + 0.4422785044 0.2237454951 0.4278069139 + 0.4446738958 0.2252838016 0.4299151003 + 0.4470661879 0.2268211991 0.4320223033 + 0.4494529963 0.2283589989 0.4341273010 + 0.4518359900 0.2298974991 0.4362305105 + 0.4542163014 0.2314366996 0.4383283854 + 0.4565913081 0.2329763025 0.4404241145 + 0.4589622915 0.2345169932 0.4425179958 + 0.4613293111 0.2360569984 0.4446097016 + 0.4636883140 0.2375967056 0.4467011988 + 0.4660438001 0.2391356975 0.4487924874 + 0.4683940113 0.2406754941 0.4508841932 + 0.4707357883 0.2422166020 0.4529756010 + 0.4730732143 0.2437583953 0.4550662041 + 0.4754005075 0.2453003973 0.4571551979 + 0.4777207971 0.2468425930 0.4592432082 + 0.4800347984 0.2483848035 0.4613265097 + 0.4823366106 0.2499269992 0.4634082913 + 0.4846338928 0.2514702976 0.4654878080 + 0.4869211912 0.2530143857 0.4675660133 + 0.4892016053 0.2545599937 0.4696435928 + 0.4914767146 0.2561056912 0.4717212021 + 0.4937387109 0.2576513886 0.4737991989 + 0.4959957898 0.2591978014 0.4758777916 + 0.4982388020 0.2607448995 0.4779537022 + 0.5004736185 0.2622939944 0.4800283015 + 0.5026996136 0.2638441026 0.4820936918 + 0.5049133897 0.2653951943 0.4841566086 + 0.5071231127 0.2669464946 0.4862132072 + 0.5093185902 0.2684977949 0.4882662892 + 0.5115088224 0.2700498998 0.4903163016 + 0.5136851072 0.2716039121 0.4923604131 + 0.5158513188 0.2731583118 0.4944027960 + 0.5180078149 0.2747133076 0.4964390099 + 0.5201510191 0.2762692869 0.4984740913 + 0.5222893953 0.2778258026 0.5005058050 + 0.5244101286 0.2793847024 0.5025345087 + 0.5265269279 0.2809442878 0.5045595765 + 0.5286250114 0.2825047970 0.5065774918 + 0.5307167768 0.2840653062 0.5085912943 + 0.5327941179 0.2856262922 0.5105962753 + 0.5348628163 0.2871873975 0.5125992894 + 0.5369216204 0.2887484133 0.5145884752 + 0.5389689207 0.2903096974 0.5165752769 + 0.5410090089 0.2918717861 0.5185478926 + 0.5430334806 0.2934342027 0.5205165744 + 0.5450525880 0.2949968874 0.5224738717 + 0.5470547080 0.2965593040 0.5244258046 + 0.5490540862 0.2981210053 0.5263680816 + 0.5510348082 0.2996825874 0.5283023715 + 0.5530139804 0.3012439907 0.5302276015 + 0.5549759865 0.3028059006 0.5321443081 + 0.5569360852 0.3043687046 0.5340541005 + 0.5588794947 0.3059312105 0.5359538794 + 0.5608198047 0.3074932992 0.5378476977 + 0.5627439022 0.3090555072 0.5397273898 + 0.5646646023 0.3106175959 0.5416002274 + 0.5665714145 0.3121803999 0.5434591770 + 0.5684738755 0.3137443066 0.5453127027 + 0.5703607798 0.3153086901 0.5471491814 + 0.5722435713 0.3168740869 0.5489794016 + 0.5741115808 0.3184395134 0.5507919192 + 0.5759764910 0.3200049996 0.5525978208 + 0.5778291225 0.3215703964 0.5543872118 + 0.5796784163 0.3231351078 0.5561701059 + 0.5815119147 0.3246999085 0.5579361916 + 0.5833426714 0.3262651861 0.5596947074 + 0.5851539969 0.3278304935 0.5614392757 + 0.5869635940 0.3293960094 0.5631760955 + 0.5887479186 0.3309614956 0.5649011731 + 0.5905321836 0.3325259984 0.5666179061 + 0.5922917128 0.3340902925 0.5683240891 + 0.5940505266 0.3356544077 0.5700199008 + 0.5957903862 0.3372184038 0.5717071891 + 0.5975276232 0.3387834132 0.5733817816 + 0.5992469788 0.3403491974 0.5750491023 + 0.6009606719 0.3419148922 0.5766983032 + 0.6026608944 0.3434804976 0.5783439875 + 0.6043530107 0.3450458050 0.5799701810 + 0.6060341001 0.3466094136 0.5815963149 + 0.6077030897 0.3481729925 0.5831990242 + 0.6093657017 0.3497354984 0.5848017931 + 0.6110138893 0.3512978852 0.5863847136 + 0.6126598120 0.3528620899 0.5879638195 + 0.6142886281 0.3544270992 0.5895280838 + 0.6159173846 0.3559925854 0.5910843015 + 0.6175265908 0.3575589955 0.5926306248 + 0.6191338897 0.3591251969 0.5941641927 + 0.6207273006 0.3606910110 0.5956931114 + 0.6223142743 0.3622567058 0.5972054005 + 0.6238920093 0.3638209105 0.5987178087 + 0.6254580021 0.3653847873 0.6002131104 + 0.6270201802 0.3669480979 0.6017081141 + 0.6285654902 0.3685106933 0.6031907797 + 0.6301108003 0.3700729907 0.6046687961 + 0.6316359043 0.3716341853 0.6061378717 + 0.6331589222 0.3731954098 0.6075956225 + 0.6346688867 0.3747552931 0.6090502143 + 0.6361703277 0.3763149977 0.6104860902 + 0.6376655102 0.3778744936 0.6119219065 + 0.6391450763 0.3794336915 0.6133409142 + 0.6406247020 0.3809930086 0.6147564054 + 0.6420868039 0.3825530112 0.6161623001 + 0.6435478926 0.3841128945 0.6175583005 + 0.6449993253 0.3856733143 0.6189519763 + 0.6464440227 0.3872339129 0.6203303933 + 0.6478844881 0.3887940943 0.6217089295 + 0.6493095756 0.3903535903 0.6230769157 + 0.6507347226 0.3919129968 0.6244416833 + 0.6521434188 0.3934705853 0.6258000135 + 0.6535484791 0.3950277865 0.6271470785 + 0.6549462080 0.3965846002 0.6284943223 + 0.6563327909 0.3981403112 0.6298246980 + 0.6577193737 0.3996961117 0.6311542988 + 0.6590902805 0.4012506902 0.6324747205 + 0.6604607105 0.4028050900 0.6337866187 + 0.6618214250 0.4043591022 0.6350975037 + 0.6631729007 0.4059123993 0.6363947988 + 0.6645237207 0.4074656069 0.6376919746 + 0.6658543944 0.4090178013 0.6389808059 + 0.6671850085 0.4105696082 0.6402636170 + 0.6685047150 0.4121212065 0.6415452957 + 0.6698150039 0.4136717916 0.6428160071 + 0.6711249948 0.4152224064 0.6440867186 + 0.6724179983 0.4167717099 0.6453511119 + 0.6737111211 0.4183202982 0.6466106772 + 0.6749945283 0.4198687077 0.6478698850 + 0.6762676239 0.4214153886 0.6491171122 + 0.6775407195 0.4229620993 0.6503642201 + 0.6787930131 0.4245072901 0.6516051292 + 0.6800435781 0.4260506034 0.6528393030 + 0.6812877059 0.4275940061 0.6540734768 + 0.6825190187 0.4291338921 0.6552976966 + 0.6837502718 0.4306730926 0.6565207243 + 0.6849684715 0.4322116971 0.6577410102 + 0.6861820817 0.4337471128 0.6589547992 + 0.6873937249 0.4352824986 0.6601685882 + 0.6885895133 0.4368155897 0.6613742113 + 0.6897851825 0.4383459091 0.6625760198 + 0.6909723282 0.4398762882 0.6637775898 + 0.6921495199 0.4414024055 0.6649709940 + 0.6933267117 0.4429270029 0.6661643982 + 0.6944912076 0.4444515109 0.6673536897 + 0.6956524253 0.4459707141 0.6685358882 + 0.6968116164 0.4474897981 0.6697182059 + 0.6979551911 0.4490073919 0.6708937287 + 0.6990987062 0.4505204856 0.6720660925 + 0.7002366185 0.4520334899 0.6732385159 + 0.7013661265 0.4535439909 0.6744014025 + 0.7024955750 0.4550516903 0.6755638719 + 0.7036141753 0.4565593898 0.6767243743 + 0.7047271729 0.4580627978 0.6778780818 + 0.7058401704 0.4595640898 0.6790319085 + 0.7069379091 0.4610654116 0.6801804900 + 0.7080339789 0.4625608921 0.6813232899 + 0.7091283202 0.4640552998 0.6824662089 + 0.7102094293 0.4655497074 0.6836034060 + 0.7112904191 0.4670377970 0.6847382784 + 0.7123653293 0.4685258865 0.6858730912 + 0.7134271860 0.4700129926 0.6869965792 + 0.7144889832 0.4714944065 0.6881188154 + 0.7155439258 0.4729757905 0.6892403960 + 0.7165915966 0.4744558036 0.6903538108 + 0.7176393867 0.4759314060 0.6914672256 + 0.7186779976 0.4774070978 0.6925780177 + 0.7197117209 0.4788806140 0.6936802268 + 0.7207453847 0.4803498089 0.6947823763 + 0.7217658758 0.4818190932 0.6958813071 + 0.7227824926 0.4832853973 0.6969739795 + 0.7237992287 0.4847474098 0.6980667114 + 0.7248027921 0.4862093031 0.6991550922 + 0.7258046269 0.4876680970 0.7002385855 + 0.7268064022 0.4891225100 0.7013220787 + 0.7277902961 0.4905770123 0.7024012804 + 0.7287734151 0.4920283854 0.7034770250 + 0.7297564149 0.4934760034 0.7045525908 + 0.7307261825 0.4949235916 0.7056220770 + 0.7316960096 0.4963681996 0.7066873908 + 0.7326653004 0.4978089035 0.7077527046 + 0.7336220741 0.4992494881 0.7088124752 + 0.7345790267 0.5006874204 0.7098689079 + 0.7355352044 0.5021209121 0.7109252810 + 0.7364767194 0.5035544038 0.7119749784 + 0.7374181151 0.5049852133 0.7130206823 + 0.7383592725 0.5064104795 0.7140665054 + 0.7392876148 0.5078356862 0.7151080966 + 0.7402158976 0.5092589259 0.7161473036 + 0.7411442995 0.5106753111 0.7171863914 + 0.7420564294 0.5120915771 0.7182205915 + 0.7429682016 0.5135068893 0.7192509770 + 0.7438799143 0.5149142742 0.7202814221 + 0.7447810173 0.5163217187 0.7213069797 + 0.7456809878 0.5177291036 0.7223277092 + 0.7465808988 0.5191293955 0.7233483791 + 0.7474709153 0.5205295086 0.7243651152 + 0.7483584285 0.5219296217 0.7253757119 + 0.7492460012 0.5233250260 0.7263864279 + 0.7501245141 0.5247194171 0.7273939848 + 0.7509989738 0.5261136889 0.7283943295 + 0.7518733740 0.5275031924 0.7293944955 + 0.7527413964 0.5288894773 0.7303934097 + 0.7536038756 0.5302758813 0.7313838005 + 0.7544664145 0.5316587090 0.7323741913 + 0.7553241849 0.5330361724 0.7333645821 + 0.7561736703 0.5344135761 0.7343451977 + 0.7570232749 0.5357897282 0.7353253961 + 0.7578713298 0.5371581912 0.7363057137 + 0.7587116957 0.5385265946 0.7372794151 + 0.7595521212 0.5398949981 0.7382512093 + 0.7603924870 0.5412554741 0.7392228842 + 0.7612221837 0.5426148176 0.7401890755 + 0.7620512247 0.5439741015 0.7411506772 + 0.7628800869 0.5453277230 0.7421122789 + 0.7637022138 0.5466768742 0.7430707216 + 0.7645211220 0.5480260849 0.7440214753 + 0.7653400898 0.5493733883 0.7449724078 + 0.7661545873 0.5507144928 0.7459229827 + 0.7669628859 0.5520555973 0.7468630075 + 0.7677711844 0.5533967018 0.7478029132 + 0.7685785890 0.5547322035 0.7487428784 + 0.7693775296 0.5560668111 0.7496752143 + 0.7701764107 0.5574014783 0.7506042719 + 0.7709752917 0.5587329865 0.7515335083 + 0.7717691064 0.5600612760 0.7524588108 + 0.7725613713 0.5613896251 0.7533779740 + 0.7733535767 0.5627173781 0.7542970777 + 0.7741429806 0.5640394092 0.7552161217 + 0.7749279737 0.5653614998 0.7561244965 + 0.7757130265 0.5666834712 0.7570329905 + 0.7764979005 0.5680016279 0.7579414248 + 0.7772737145 0.5693175197 0.7588419914 + 0.7780495286 0.5706332922 0.7597380877 + 0.7788252831 0.5719478130 0.7606341243 + 0.7795972228 0.5732557178 0.7615277767 + 0.7803664207 0.5745636225 0.7624130845 + 0.7811354995 0.5758715272 0.7632983923 + 0.7819035053 0.5771726966 0.7641837001 + 0.7826650143 0.5784705877 0.7650598884 + 0.7834265232 0.5797685981 0.7659333944 + 0.7841879725 0.5810651779 0.7668067813 + 0.7849435806 0.5823537111 0.7676774859 + 0.7856963277 0.5836421847 0.7685415745 + 0.7864488959 0.5849307179 0.7694056034 + 0.7872005105 0.5862150788 0.7702696919 + 0.7879469991 0.5874965787 0.7711244226 + 0.7886934876 0.5887780190 0.7719767094 + 0.7894400954 0.5900595188 0.7728291154 + 0.7901812196 0.5913344026 0.7736784220 + 0.7909191251 0.5926094055 0.7745192051 + 0.7916570902 0.5938844085 0.7753601074 + 0.7923948169 0.5951570272 0.7762010098 + 0.7931283116 0.5964260101 0.7770338058 + 0.7938618064 0.5976949930 0.7778633237 + 0.7945951819 0.5989639759 0.7786927223 + 0.7953253984 0.6002289057 0.7795209885 + 0.7960519195 0.6014925241 0.7803397775 + 0.7967783809 0.6027560830 0.7811586857 + 0.7975050211 0.6040192842 0.7819775939 + 0.7982237935 0.6052780151 0.7827903032 + 0.7989413142 0.6065366864 0.7835965753 + 0.7996588945 0.6077952981 0.7844029069 + 0.8003752232 0.6090512276 0.7852091789 + 0.8010866046 0.6103022099 0.7860053778 + 0.8017979860 0.6115530729 0.7867997289 + 0.8025094271 0.6128041148 0.7875940204 + 0.8032175899 0.6140497923 0.7883868217 + 0.8039227128 0.6152924895 0.7891716957 + 0.8046277761 0.6165351868 0.7899566293 + 0.8053328991 0.6177778244 0.7907415032 + 0.8060317039 0.6190140843 0.7915207148 + 0.8067288995 0.6202492118 0.7922930717 + 0.8074262142 0.6214843988 0.7930656075 + 0.8081231117 0.6227191091 0.7938380241 + 0.8088151813 0.6239476204 0.7946025133 + 0.8095073104 0.6251760125 0.7953637838 + 0.8101993799 0.6264044046 0.7961249948 + 0.8108897805 0.6276314855 0.7968863249 + 0.8115760088 0.6288539171 0.7976366878 + 0.8122622967 0.6300764084 0.7983863950 + 0.8129485250 0.6312987804 0.7991361022 + 0.8136305213 0.6325194240 0.7998843193 + 0.8143079281 0.6337367296 0.8006227016 + 0.8149852753 0.6349539161 0.8013610244 + 0.8156628013 0.6361712217 0.8020994067 + 0.8163372278 0.6373847723 0.8028339744 + 0.8170101047 0.6385943294 0.8035594225 + 0.8176829219 0.6398037076 0.8042848110 + 0.8183556795 0.6410130858 0.8050101995 + 0.8190233111 0.6422181129 0.8057312965 + 0.8196895123 0.6434193850 0.8064466119 + 0.8203555942 0.6446207166 0.8071619272 + 0.8210217953 0.6458219290 0.8078771830 + 0.8216826916 0.6470190883 0.8085857034 + 0.8223428726 0.6482132077 0.8092882037 + 0.8230029941 0.6494073272 0.8099907041 + 0.8236631751 0.6506013870 0.8106932044 + 0.8243181705 0.6517918706 0.8113890290 + 0.8249729872 0.6529797912 0.8120803833 + 0.8256276846 0.6541677117 0.8127716780 + 0.8262825012 0.6553556919 0.8134629726 + 0.8269299865 0.6565406919 0.8141471148 + 0.8275772929 0.6577234864 0.8148270845 + 0.8282247186 0.6589062214 0.8155071139 + 0.8288720250 0.6600890160 0.8161870837 + 0.8295140862 0.6612688899 0.8168601990 + 0.8301560879 0.6624459028 0.8175293803 + 0.8307980895 0.6636229157 0.8181986213 + 0.8314400911 0.6647999287 0.8188679218 + 0.8320763707 0.6659740210 0.8195297122 + 0.8327124119 0.6671438217 0.8201869130 + 0.8333482742 0.6683136821 0.8208439946 + 0.8339843154 0.6694836020 0.8215011954 + 0.8346142769 0.6706516147 0.8221533895 + 0.8352434039 0.6718149185 0.8228014708 + 0.8358724713 0.6729781032 0.8234496117 + 0.8365015984 0.6741414070 0.8240976930 + 0.8371255994 0.6753039956 0.8247407079 + 0.8377479911 0.6764618754 0.8253772855 + 0.8383705020 0.6776198149 0.8260139823 + 0.8389930129 0.6787776947 0.8266506195 + 0.8396112919 0.6799355149 0.8272842765 + 0.8402273059 0.6810882092 0.8279114962 + 0.8408433199 0.6822404265 0.8285385966 + 0.8414592743 0.6833925843 0.8291658163 + 0.8420726061 0.6845448017 0.8297916055 + 0.8426827788 0.6856936216 0.8304092884 + 0.8432930112 0.6868407726 0.8310269713 + 0.8439031839 0.6879879832 0.8316445947 + 0.8445109129 0.6891351938 0.8322622776 + 0.8451119065 0.6902797818 0.8328717947 + 0.8457129002 0.6914209127 0.8334808946 + 0.8463138938 0.6925619245 0.8340898752 + 0.8469148278 0.6937029958 0.8346989155 + 0.8475095034 0.6948431730 0.8353011012 + 0.8481041789 0.6959772110 0.8359006047 + 0.8486989141 0.6971111894 0.8364999890 + 0.8492935896 0.6982452273 0.8370993733 + 0.8498833179 0.6993792057 0.8376960158 + 0.8504714966 0.7005075812 0.8382890224 + 0.8510596156 0.7016344070 0.8388819098 + 0.8516476750 0.7027612925 0.8394749165 + 0.8522318006 0.7038881183 0.8400666118 + 0.8528108001 0.7050123215 0.8406503797 + 0.8533899188 0.7061328888 0.8412342072 + 0.8539689779 0.7072533965 0.8418179154 + 0.8545476198 0.7083739042 0.8424016833 + 0.8551219106 0.7094944119 0.8429775238 + 0.8556962013 0.7106081247 0.8435510993 + 0.8562703729 0.7117217183 0.8441246748 + 0.8568447232 0.7128353715 0.8446984291 + 0.8574144244 0.7139490843 0.8452690244 + 0.8579822183 0.7150586843 0.8458353281 + 0.8585500717 0.7161651254 0.8464015722 + 0.8591179252 0.7172715068 0.8469678760 + 0.8596833944 0.7183778286 0.8475341201 + 0.8602436185 0.7194836736 0.8480933905 + 0.8608037233 0.7205830812 0.8486527205 + 0.8613638282 0.7216826081 0.8492119908 + 0.8619239926 0.7227820158 0.8497712016 + 0.8624796271 0.7238814235 0.8503255248 + 0.8630344272 0.7249767780 0.8508757949 + 0.8635892272 0.7260689139 0.8514261246 + 0.8641440272 0.7271611094 0.8519762754 + 0.8646966219 0.7282531857 0.8525258899 + 0.8652452826 0.7293453217 0.8530645967 + 0.8657941222 0.7304283977 0.8536034226 + 0.8663427830 0.7315114737 0.8541421890 + 0.8668915033 0.7325944901 0.8546810150 + 0.8674352169 0.7336776257 0.8552160859 + 0.8679779172 0.7347571850 0.8557479978 + 0.8685206771 0.7358316183 0.8562799096 + 0.8690633774 0.7369059920 0.8568118811 + 0.8696042895 0.7379804254 0.8573437929 + 0.8701401949 0.7390547991 0.8578673005 + 0.8706761003 0.7401224971 0.8583906293 + 0.8712120056 0.7411879897 0.8589140177 + 0.8717479110 0.7422536016 0.8594372869 + 0.8722802997 0.7433190942 0.8599570990 + 0.8728110194 0.7443835735 0.8604711294 + 0.8733416796 0.7454379797 0.8609851003 + 0.8738722801 0.7464923263 0.8614990711 + 0.8744028211 0.7475466728 0.8620131016 + 0.8749272823 0.7486010790 0.8625208735 + 0.8754518032 0.7496516109 0.8630263805 + 0.8759763241 0.7506958246 0.8635318875 + 0.8765007854 0.7517400980 0.8640373945 + 0.8770225048 0.7527843118 0.8645426035 + 0.8775386810 0.7538285851 0.8650417924 + 0.8780549765 0.7548667789 0.8655411005 + 0.8785712123 0.7559013963 0.8660402894 + 0.8790875077 0.7569360733 0.8665394783 + 0.8796002269 0.7579708099 0.8670362830 + 0.8801108003 0.7590054870 0.8675283790 + 0.8806213737 0.7600315213 0.8680204153 + 0.8811320066 0.7610560060 0.8685125113 + 0.8816425800 0.7620804906 0.8690046072 + 0.8821474910 0.7631049156 0.8694908023 + 0.8826515079 0.7641292214 0.8699728847 + 0.8831555247 0.7651435733 0.8704550266 + 0.8836596012 0.7661579251 0.8709372282 + 0.8841630220 0.7671722770 0.8714193106 + 0.8846591115 0.7681866288 0.8718963265 + 0.8851552010 0.7691996098 0.8723719716 + 0.8856512904 0.7702046037 0.8728476167 + 0.8861473799 0.7712097168 0.8733233213 + 0.8866416812 0.7722147703 0.8737990260 + 0.8871303201 0.7732198238 0.8742687106 + 0.8876188993 0.7742217779 0.8747382164 + 0.8881074786 0.7752138972 0.8752077222 + 0.8885961771 0.7762060761 0.8756772876 + 0.8890824914 0.7771981955 0.8761461973 + 0.8895648718 0.7781903744 0.8766090274 + 0.8900471926 0.7791786194 0.8770719171 + 0.8905295730 0.7801573277 0.8775346875 + 0.8910118937 0.7811360955 0.8779975772 + 0.8914917707 0.7821148038 0.8784590960 + 0.8919684887 0.7830935121 0.8789139986 + 0.8924452066 0.7840679288 0.8793689013 + 0.8929218054 0.7850319743 0.8798238039 + 0.8933985233 0.7859961987 0.8802787066 + 0.8938719034 0.7869604230 0.8807321787 + 0.8943415880 0.7879245877 0.8811805248 + 0.8948113918 0.7888851762 0.8816288114 + 0.8952810764 0.7898358107 0.8820770979 + 0.8957508206 0.7907865047 0.8825255036 + 0.8962174058 0.7917371988 0.8829724789 + 0.8966804147 0.7926878929 0.8834149837 + 0.8971434832 0.7936357856 0.8838574886 + 0.8976066113 0.7945719957 0.8843001127 + 0.8980696201 0.7955083251 0.8847426176 + 0.8985310793 0.7964445949 0.8851838708 + 0.8989905119 0.7973809242 0.8856199980 + 0.8994498253 0.7983161807 0.8860560060 + 0.8999091983 0.7992392778 0.8864920735 + 0.9003685713 0.8001623750 0.8869280815 + 0.9008257985 0.8010855913 0.8873633742 + 0.9012789726 0.8020086884 0.8877922297 + 0.9017320871 0.8029319048 0.8882209063 + 0.9021853209 0.8038424253 0.8886497021 + 0.9026384950 0.8047516942 0.8890783787 + 0.9030898213 0.8056610227 0.8895071149 + 0.9035350084 0.8065704107 0.8899301887 + 0.9039803147 0.8074796796 0.8903532028 + 0.9044255018 0.8083794117 0.8907762170 + 0.9048708081 0.8092753291 0.8911991119 + 0.9053156972 0.8101711869 0.8916221261 + 0.9057556987 0.8110671043 0.8920407891 + 0.9061955810 0.8119630218 0.8924584985 + 0.9066355824 0.8128508925 0.8928763270 + 0.9070755839 0.8137295842 0.8932939768 + 0.9075155854 0.8146083951 0.8937116861 + 0.9079490900 0.8154872060 0.8941258192 + 0.9083815813 0.8163658977 0.8945375085 + 0.9088140726 0.8172419071 0.8949491978 + 0.9092466235 0.8181051016 0.8953608274 + 0.9096791148 0.8189684153 0.8957725167 + 0.9101070166 0.8198316097 0.8961812854 + 0.9105319977 0.8206949234 0.8965846896 + 0.9109569192 0.8215581179 0.8969882131 + 0.9113819003 0.8224077821 0.8973916173 + 0.9118068218 0.8232551813 0.8977950215 + 0.9122298956 0.8241025209 0.8981981874 + 0.9126486778 0.8249498010 0.8985965848 + 0.9130674005 0.8257970810 0.8989951015 + 0.9134861827 0.8266378045 0.8993936181 + 0.9139049053 0.8274710178 0.8997920156 + 0.9143236279 0.8283041716 0.9001904726 + 0.9147337079 0.8291373849 0.9005855918 + 0.9151433706 0.8299707174 0.9009795189 + 0.9155529737 0.8308032155 0.9013733864 + 0.9159626961 0.8316221237 0.9017671943 + 0.9163724184 0.8324409723 0.9021611214 + 0.9167771935 0.8332599998 0.9025527239 + 0.9171783924 0.8340789080 0.9029397964 + 0.9175795913 0.8348978758 0.9033269286 + 0.9179807901 0.8357083797 0.9037140012 + 0.9183819294 0.8365128040 0.9041010737 + 0.9187824130 0.8373172879 0.9044882059 + 0.9191781282 0.8381217718 0.9048687220 + 0.9195739031 0.8389261961 0.9052485824 + 0.9199696779 0.8397294283 0.9056286216 + 0.9203655124 0.8405168056 0.9060084820 + 0.9207612276 0.8413041234 0.9063884020 + 0.9211524725 0.8420915008 0.9067658186 + 0.9215413928 0.8428788185 0.9071398973 + 0.9219303727 0.8436661959 0.9075140953 + 0.9223194122 0.8444454074 0.9078881741 + 0.9227082729 0.8452168703 0.9082623720 + 0.9230962992 0.8459883928 0.9086365104 + 0.9234783053 0.8467599750 0.9090055227 + 0.9238603711 0.8475314975 0.9093741179 + 0.9242423773 0.8483030796 0.9097427130 + 0.9246245027 0.8490607738 0.9101111889 + 0.9250065088 0.8498172760 0.9104797840 + 0.9253851175 0.8505737185 0.9108464122 + 0.9257611036 0.8513301015 0.9112095237 + 0.9261372089 0.8520866036 0.9115725756 + 0.9265131950 0.8528388739 0.9119356871 + 0.9268891811 0.8535797000 0.9122989178 + 0.9272652864 0.8543204069 0.9126620293 + 0.9276344180 0.8550611734 0.9130206108 + 0.9280033708 0.8558018804 0.9133777022 + 0.9283723235 0.8565427065 0.9137349129 + 0.9287412763 0.8572751880 0.9140920043 + 0.9291102290 0.8580012918 0.9144490957 + 0.9294773936 0.8587272763 0.9148054719 + 0.9298406839 0.8594533801 0.9151548147 + 0.9302039742 0.8601794839 0.9155040979 + 0.9305673838 0.8609055877 0.9158533812 + 0.9309306741 0.8616189957 0.9162026048 + 0.9312940240 0.8623296022 0.9165518880 + 0.9316536784 0.8630402088 0.9168993831 + 0.9320111275 0.8637508750 0.9172441959 + 0.9323683977 0.8644614816 0.9175890088 + 0.9327257872 0.8651716709 0.9179338217 + 0.9330831170 0.8658667207 0.9182785153 + 0.9334405065 0.8665615916 0.9186233282 + 0.9337912798 0.8672565818 0.9189655781 + 0.9341409802 0.8679515123 0.9193063974 + 0.9344906211 0.8686465025 0.9196472168 + 0.9348403215 0.8693385124 0.9199880958 + 0.9351899028 0.8700178266 0.9203289151 + 0.9355393052 0.8706970811 0.9206697941 + 0.9358832836 0.8713763952 0.9210057259 + 0.9362273216 0.8720557094 0.9213408232 + 0.9365713000 0.8727350235 0.9216758013 + 0.9369152188 0.8734096289 0.9220108986 + 0.9372591972 0.8740733862 0.9223458767 + 0.9376019835 0.8747370839 0.9226809740 + 0.9379401803 0.8754009008 0.9230101109 + 0.9382783771 0.8760647178 0.9233391881 + 0.9386165738 0.8767284155 0.9236683846 + 0.9389547706 0.8773869276 0.9239975214 + 0.9392930269 0.8780362010 0.9243265986 + 0.9396293163 0.8786854148 0.9246553183 + 0.9399607778 0.8793346882 0.9249799252 + 0.9402924180 0.8799839020 0.9253045917 + 0.9406239986 0.8806331754 0.9256293178 + 0.9409555793 0.8812769055 0.9259539247 + 0.9412871003 0.8819113970 0.9262785912 + 0.9416161776 0.8825458288 0.9266026020 + 0.9419407248 0.8831803203 0.9269232750 + 0.9422652125 0.8838148117 0.9272438884 + 0.9425897002 0.8844493032 0.9275646210 + 0.9429141283 0.8850783110 0.9278851748 + 0.9432386160 0.8856968284 0.9282059073 + 0.9435608983 0.8863152862 0.9285259247 + 0.9438794255 0.8869338036 0.9288429022 + 0.9441977739 0.8875523210 0.9291598797 + 0.9445161819 0.8881707191 0.9294769168 + 0.9448347092 0.8887856007 0.9297938943 + 0.9451531172 0.8893899918 0.9301108718 + 0.9454690218 0.8899943829 0.9304273725 + 0.9457793236 0.8905987740 0.9307404160 + 0.9460895061 0.8912032247 0.9310532808 + 0.9463998079 0.8918076158 0.9313663244 + 0.9467099905 0.8924102783 0.9316793084 + 0.9470202923 0.8930001259 0.9319922924 + 0.9473289847 0.8935899734 0.9323049784 + 0.9476320744 0.8941798210 0.9326118827 + 0.9479352236 0.8947697282 0.9329187870 + 0.9482381940 0.8953595161 0.9332256913 + 0.9485412836 0.8959493041 0.9335324764 + 0.9488443136 0.8965260983 0.9338393807 + 0.9491471052 0.8971018195 0.9341462851 + 0.9494447708 0.8976774812 0.9344497919 + 0.9497426152 0.8982532024 0.9347528815 + 0.9500402808 0.8988289833 0.9350559711 + 0.9503381252 0.8994047046 0.9353591204 + 0.9506357908 0.8999707103 0.9356622100 + 0.9509335756 0.9005324244 0.9359652996 + 0.9512249827 0.9010940790 0.9362673163 + 0.9515153766 0.9016557932 0.9365687966 + 0.9518058896 0.9022175074 0.9368702769 + 0.9520962834 0.9027792215 0.9371718764 + 0.9523866773 0.9033352137 0.9374734163 + 0.9526770711 0.9038825035 0.9377748966 + 0.9529634118 0.9044299126 0.9380748272 + 0.9532471895 0.9049773216 0.9383726716 + 0.9535309076 0.9055246711 0.9386705756 + 0.9538146257 0.9060720801 0.9389684796 + 0.9540982842 0.9066187143 0.9392663836 + 0.9543820024 0.9071521163 0.9395642877 + 0.9546641707 0.9076855183 0.9398614168 + 0.9549428821 0.9082189202 0.9401535988 + 0.9552217126 0.9087523222 0.9404457808 + 0.9555004835 0.9092857242 0.9407380819 + 0.9557793140 0.9098191261 0.9410303235 + 0.9560580850 0.9103447795 0.9413226247 + 0.9563369155 0.9108663797 0.9416148067 + 0.9566090703 0.9113880992 0.9419016838 + 0.9568811059 0.9119098186 0.9421873093 + 0.9571530819 0.9124314785 0.9424728751 + 0.9574251175 0.9129531980 0.9427586198 + 0.9576970935 0.9134721160 0.9430441856 + 0.9579691291 0.9139804244 0.9433298111 + 0.9582366943 0.9144887924 0.9436146021 + 0.9585008025 0.9149972200 0.9438980818 + 0.9587647915 0.9155055285 0.9441816807 + 0.9590288997 0.9160138965 0.9444652200 + 0.9592928886 0.9165222049 0.9447486997 + 0.9595569968 0.9170212150 0.9450322986 + 0.9598206282 0.9175164104 0.9453157783 + 0.9600790739 0.9180114865 0.9455965161 + 0.9603375793 0.9185066223 0.9458770156 + 0.9605960846 0.9190018177 0.9461575150 + 0.9608544707 0.9194968939 0.9464380145 + 0.9611129761 0.9199900031 0.9467185140 + 0.9613714814 0.9204719067 0.9469990134 + 0.9616270065 0.9209538102 0.9472776055 + 0.9618802071 0.9214357138 0.9475535154 + 0.9621335268 0.9219176173 0.9478294253 + 0.9623867273 0.9223995209 0.9481052756 + 0.9626399875 0.9228814244 0.9483813047 + 0.9628931880 0.9233567119 0.9486572146 + 0.9631463885 0.9238265157 0.9489331245 + 0.9633926153 0.9242962003 0.9492051005 + 0.9636389017 0.9247658849 0.9494764805 + 0.9638851285 0.9252356887 0.9497479796 + 0.9641314149 0.9257053733 0.9500194192 + 0.9643775821 0.9261751771 0.9502907991 + 0.9646239281 0.9266350865 0.9505621791 + 0.9648674726 0.9270936847 0.9508329034 + 0.9651070237 0.9275522232 0.9511013031 + 0.9653465152 0.9280108213 0.9513697028 + 0.9655861259 0.9284693003 0.9516382217 + 0.9658256173 0.9289278984 0.9519066215 + 0.9660651088 0.9293845296 0.9521750212 + 0.9663046002 0.9298309088 0.9524434209 + 0.9665402174 0.9302772880 0.9527102113 + 0.9667741060 0.9307237267 0.9529756904 + 0.9670081139 0.9311701059 0.9532412291 + 0.9672421217 0.9316164851 0.9535068274 + 0.9674760103 0.9320629239 0.9537723064 + 0.9677100182 0.9325053096 0.9540377855 + 0.9679439068 0.9329410195 0.9543033838 + 0.9681721926 0.9333767891 0.9545660019 + 0.9684002995 0.9338126183 0.9548282027 + 0.9686284065 0.9342483282 0.9550904036 + 0.9688565135 0.9346840978 0.9553526044 + 0.9690846205 0.9351198077 0.9556146860 + 0.9693127275 0.9355502129 0.9558768868 + 0.9695394039 0.9359760880 0.9561389089 + 0.9697601199 0.9364020228 0.9563990831 + 0.9699807763 0.9368280172 0.9566591978 + 0.9702014923 0.9372538924 0.9569193125 + 0.9704222083 0.9376798272 0.9571794271 + 0.9706429243 0.9381058216 0.9574394822 + 0.9708635807 0.9385251999 0.9576995969 + 0.9710816145 0.9389411807 0.9579588175 + 0.9712945223 0.9393572211 0.9582151175 + 0.9715074897 0.9397732019 0.9584714174 + 0.9717203975 0.9401893020 0.9587277770 + 0.9719334245 0.9406052828 0.9589840770 + 0.9721462727 0.9410213232 0.9592403769 + 0.9723592997 0.9414290190 0.9594966769 + 0.9725701809 0.9418333769 0.9597517252 + 0.9727786779 0.9422376752 0.9600039721 + 0.9729871154 0.9426419735 0.9602563977 + 0.9731954932 0.9430463910 0.9605087042 + 0.9734039903 0.9434506893 0.9607610106 + 0.9736124277 0.9438549876 0.9610133767 + 0.9738209248 0.9442532063 0.9612656832 + 0.9740266800 0.9446488023 0.9615169764 + 0.9742298126 0.9450443983 0.9617664814 + 0.9744328260 0.9454399943 0.9620159864 + 0.9746358991 0.9458355904 0.9622653723 + 0.9748389721 0.9462311864 0.9625148773 + 0.9750419855 0.9466267824 0.9627643824 + 0.9752451181 0.9470162988 0.9630138874 + 0.9754452705 0.9474024177 0.9632629156 + 0.9756422043 0.9477884769 0.9635112882 + 0.9758390784 0.9481745958 0.9637596011 + 0.9760360122 0.9485607743 0.9640079737 + 0.9762328863 0.9489468932 0.9642562866 + 0.9764298201 0.9493330121 0.9645047188 + 0.9766266942 0.9497143030 0.9647529721 + 0.9768214822 0.9500910044 0.9650006294 + 0.9770131111 0.9504678249 0.9652460814 + 0.9772046208 0.9508445859 0.9654915929 + 0.9773961902 0.9512214065 0.9657371044 + 0.9775878191 0.9515982270 0.9659826159 + 0.9777793884 0.9519749284 0.9662281275 + 0.9779708982 0.9523478746 0.9664735794 + 0.9781610966 0.9527137876 0.9667181969 + 0.9783474207 0.9530795813 0.9669579864 + 0.9785336852 0.9534453750 0.9671977758 + 0.9787198901 0.9538112879 0.9674376845 + 0.9789062142 0.9541770816 0.9676774740 + 0.9790924191 0.9545428753 0.9679173231 + 0.9792786837 0.9549077153 0.9681571126 + 0.9794644713 0.9552628994 0.9683969021 + 0.9796441793 0.9556182027 0.9686303735 + 0.9798238873 0.9559733868 0.9688637257 + 0.9800035954 0.9563286901 0.9690970182 + 0.9801833034 0.9566838741 0.9693303108 + 0.9803630114 0.9570391774 0.9695636034 + 0.9805427194 0.9573944807 0.9697968960 + 0.9807224274 0.9577432275 0.9700301886 + 0.9808962941 0.9580901861 0.9702622294 + 0.9810687900 0.9584373236 0.9704936147 + 0.9812414050 0.9587844014 0.9707251191 + 0.9814139009 0.9591314793 0.9709565043 + 0.9815865159 0.9594786167 0.9711880088 + 0.9817590117 0.9598256946 0.9714193940 + 0.9819316268 0.9601699710 0.9716508985 + 0.9821007848 0.9605106115 0.9718813896 + 0.9822667241 0.9608513117 0.9721106887 + 0.9824326038 0.9611920118 0.9723399878 + 0.9825986028 0.9615327120 0.9725692868 + 0.9827644825 0.9618732929 0.9727985859 + 0.9829304218 0.9622139931 0.9730278850 + 0.9830964208 0.9625545144 0.9732571840 + 0.9832617044 0.9628877044 0.9734864235 + 0.9834232926 0.9632207751 0.9737125039 + 0.9835848212 0.9635540247 0.9739385843 + 0.9837464094 0.9638870955 0.9741647243 + 0.9839078784 0.9642202258 0.9743908048 + 0.9840695262 0.9645534158 0.9746168852 + 0.9842309952 0.9648864865 0.9748430252 + 0.9843925834 0.9652161002 0.9750691056 + 0.9845516086 0.9655424953 0.9752932191 + 0.9847096205 0.9658688903 0.9755160809 + 0.9848675132 0.9661952853 0.9757388830 + 0.9850255251 0.9665216804 0.9759618044 + 0.9851834178 0.9668480754 0.9761847258 + 0.9853413105 0.9671744704 0.9764075875 + 0.9854993224 0.9675009251 0.9766303897 + 0.9856563210 0.9678199291 0.9768530726 + 0.9858093262 0.9681388736 0.9770743251 + 0.9859623909 0.9684578776 0.9772955179 + 0.9861153960 0.9687768817 0.9775167704 + 0.9862685204 0.9690958858 0.9777380228 + 0.9864215255 0.9694147706 0.9779592156 + 0.9865745902 0.9697337747 0.9781805277 + 0.9867275953 0.9700502753 0.9784017205 + 0.9868773222 0.9703627229 0.9786214232 + 0.9870250225 0.9706751704 0.9788399935 + 0.9871727824 0.9709876180 0.9790586233 + 0.9873206019 0.9713000059 0.9792770743 + 0.9874683022 0.9716125131 0.9794957042 + 0.9876161218 0.9719249010 0.9797142744 + 0.9877638221 0.9722372890 0.9799329042 + 0.9879115224 0.9725446105 0.9801514149 + 0.9880549908 0.9728497267 0.9803667068 + 0.9881983995 0.9731547832 0.9805818200 + 0.9883419275 0.9734600186 0.9807968736 + 0.9884853959 0.9737650752 0.9810119867 + 0.9886289239 0.9740701914 0.9812270999 + 0.9887723923 0.9743754268 0.9814422131 + 0.9889158010 0.9746804833 0.9816573262 + 0.9890578985 0.9749789238 0.9818714261 + 0.9891973138 0.9752771854 0.9820829034 + 0.9893366098 0.9755755067 0.9822943211 + 0.9894760251 0.9758738279 0.9825057983 + 0.9896153212 0.9761720896 0.9827172160 + 0.9897546768 0.9764702916 0.9829286933 + 0.9898939729 0.9767686129 0.9831401110 + 0.9900333881 0.9770656824 0.9833515882 + 0.9901698232 0.9773573279 0.9835612774 + 0.9903042912 0.9776489139 0.9837695956 + 0.9904388189 0.9779404998 0.9839779139 + 0.9905732870 0.9782320857 0.9841861725 + 0.9907078147 0.9785236716 0.9843944907 + 0.9908422828 0.9788153172 0.9846028090 + 0.9909768105 0.9791069031 0.9848111272 + 0.9911112785 0.9793964028 0.9850193858 + 0.9912419915 0.9796813726 0.9852252007 + 0.9913715720 0.9799662828 0.9854300022 + 0.9915010929 0.9802513123 0.9856348038 + 0.9916306734 0.9805362821 0.9858396053 + 0.9917603135 0.9808213115 0.9860445261 + 0.9918898940 0.9811062813 0.9862493277 + 0.9920194745 0.9813911915 0.9864541292 + 0.9921491146 0.9816734791 0.9866588712 + 0.9922748208 0.9819514751 0.9868602157 + 0.9923998713 0.9822294712 0.9870607853 + 0.9925251007 0.9825075269 0.9872614145 + 0.9926502705 0.9827854037 0.9874619842 + 0.9927754998 0.9830633998 0.9876626730 + 0.9929007292 0.9833413959 0.9878633022 + 0.9930258989 0.9836193919 0.9880638719 + 0.9931511283 0.9838947058 0.9882645011 + 0.9932720065 0.9841657877 0.9884613156 + 0.9933925867 0.9844368100 0.9886577129 + 0.9935132861 0.9847078919 0.9888541102 + 0.9936339855 0.9849789739 0.9890506268 + 0.9937546849 0.9852501154 0.9892470241 + 0.9938753247 0.9855211973 0.9894434214 + 0.9939960241 0.9857922196 0.9896398187 + 0.9941167235 0.9860610962 0.9898362160 + 0.9942330122 0.9863253832 0.9900293946 + 0.9943491220 0.9865897298 0.9902222157 + 0.9944652915 0.9868540168 0.9904149771 + 0.9945814013 0.9871183038 0.9906079173 + 0.9946975112 0.9873827100 0.9908006787 + 0.9948136210 0.9876469970 0.9909934998 + 0.9949297905 0.9879112840 0.9911863804 + 0.9950459003 0.9881742001 0.9913792014 + 0.9951578975 0.9884313941 0.9915676117 + 0.9952692986 0.9886885881 0.9917551875 + 0.9953806996 0.9889457822 0.9919428229 + 0.9954922199 0.9892029762 0.9921303988 + 0.9956036210 0.9894601703 0.9923179746 + 0.9957150221 0.9897174239 0.9925056100 + 0.9958264232 0.9899746180 0.9926931858 + 0.9959378242 0.9902316928 0.9928808212 + 0.9960461259 0.9904808998 0.9930642247 + 0.9961531758 0.9907299876 0.9932457209 + 0.9962602854 0.9909791946 0.9934272766 + 0.9963673949 0.9912282825 0.9936087728 + 0.9964745045 0.9914774895 0.9937903285 + 0.9965816140 0.9917266965 0.9939718843 + 0.9966887236 0.9919757843 0.9941533804 + 0.9967958927 0.9922249913 0.9943349957 + 0.9969006181 0.9924687147 0.9945136905 + 0.9970030785 0.9927104712 0.9946895838 + 0.9971057177 0.9929522276 0.9948654771 + 0.9972081780 0.9931939840 0.9950413704 + 0.9973108172 0.9934358001 0.9952173233 + 0.9974133968 0.9936776161 0.9953930974 + 0.9975159168 0.9939193726 0.9955689907 + 0.9976184964 0.9941611886 0.9957448840 + 0.9977198839 0.9943996072 0.9959198236 + 0.9978176951 0.9946330786 0.9960914254 + 0.9979155064 0.9948667288 0.9962630868 + 0.9980133176 0.9951003194 0.9964346886 + 0.9981111288 0.9953339100 0.9966062903 + 0.9982089996 0.9955673814 0.9967778921 + 0.9983068109 0.9958009720 0.9969494939 + 0.9984046221 0.9960346222 0.9971210957 + 0.9985023737 0.9962679744 0.9972926974 + 0.9985945225 0.9964916110 0.9974582791 + 0.9986860156 0.9967151880 0.9976230860 + 0.9987773895 0.9969388843 0.9977878928 + 0.9988688827 0.9971625209 0.9979526997 + 0.9989603162 0.9973860979 0.9981175065 + 0.9990518093 0.9976096749 0.9982821941 + 0.9991431832 0.9978333116 0.9984470010 + 0.9992346764 0.9980568886 0.9986118078 + 0.9993227720 0.9982761145 0.9987711906 + 0.9994074702 0.9984915853 0.9989247918 + 0.9994921088 0.9987071157 0.9990783930 + 0.9995768070 0.9989225864 0.9992319942 + 0.9996613860 0.9991381168 0.9993855953 + 0.9997460842 0.9993535280 0.9995391965 + 0.9998307228 0.9995689988 0.9996927977 + 0.9999154210 0.9997845292 0.9998463988 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfachrome_rsx2_200.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfachrome_rsx2_200.spi1d index 5f1a0e14..793d41dc 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfachrome_rsx2_200.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfachrome_rsx2_200.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0002336452 0.0003066142 0.0001923408 - 0.0004746285 0.0006284252 0.0003908366 - 0.0007228691 0.0009574692 0.0005960183 - 0.0009798091 0.0012926980 0.0008084249 - 0.0012458910 0.0016348250 0.0010286780 - 0.0015215250 0.0019854531 0.0012572890 - 0.0018074480 0.0023458831 0.0014948760 - 0.0021044740 0.0027179900 0.0017418840 - 0.0024123720 0.0031044651 0.0019987100 - 0.0027316429 0.0035080169 0.0022662929 - 0.0030628759 0.0039312281 0.0025448580 - 0.0034068460 0.0043767700 0.0028348130 - 0.0037631821 0.0048452052 0.0031369999 - 0.0041319309 0.0053377128 0.0034519250 - 0.0045133741 0.0058530779 0.0037802891 - 0.0049079140 0.0063888212 0.0041218149 - 0.0053149262 0.0069422242 0.0044763028 - 0.0057351599 0.0075108381 0.0048436122 - 0.0061681862 0.0080922469 0.0052233832 - 0.0066139381 0.0086847479 0.0056158421 - 0.0070730280 0.0092877084 0.0060206670 - 0.0075457580 0.0099003958 0.0064372532 - 0.0080318721 0.0105231302 0.0068652900 - 0.0085307229 0.0111563997 0.0073052440 - 0.0090426058 0.0118005900 0.0077564488 - 0.0095678763 0.0124572096 0.0082195234 - 0.0101060299 0.0131261898 0.0086938078 - 0.0106565999 0.0138081899 0.0091797709 - 0.0112196403 0.0145026296 0.0096775433 - 0.0117950998 0.0152096897 0.0101859104 - 0.0123830596 0.0159296691 0.0107057104 - 0.0129827997 0.0166616905 0.0112374704 - 0.0135938600 0.0174061507 0.0117801595 - 0.0142163001 0.0181632899 0.0123338196 - 0.0148505196 0.0189326294 0.0128991101 - 0.0154959997 0.0197145808 0.0134759704 - 0.0161525905 0.0205079298 0.0140651399 - 0.0168197099 0.0213135108 0.0146654900 - 0.0174975600 0.0221296605 0.0152769703 - 0.0181858800 0.0229572095 0.0159003101 - 0.0188853294 0.0237958394 0.0165352002 - 0.0195957199 0.0246469099 0.0171822291 - 0.0203166995 0.0255084708 0.0178419799 - 0.0210479405 0.0263805594 0.0185129102 - 0.0217893906 0.0272626597 0.0191952903 - 0.0225410797 0.0281556007 0.0198900104 - 0.0233031102 0.0290594995 0.0205974299 - 0.0240752809 0.0299743302 0.0213165600 - 0.0248574503 0.0308989193 0.0220477208 - 0.0256489795 0.0318339504 0.0227914508 - 0.0264505297 0.0327800512 0.0235488601 - 0.0272613708 0.0337377489 0.0243192408 - 0.0280819293 0.0347075686 0.0251018200 - 0.0289116092 0.0356889293 0.0258964691 - 0.0297506806 0.0366821997 0.0267038904 - 0.0305987895 0.0376874991 0.0275248494 - 0.0314561911 0.0387053117 0.0283588003 - 0.0323230512 0.0397350118 0.0292057395 - 0.0331984907 0.0407753289 0.0300664101 - 0.0340828784 0.0418274291 0.0309393499 - 0.0349764302 0.0428918488 0.0318249315 - 0.0358787291 0.0439687595 0.0327227190 - 0.0367900617 0.0450573303 0.0336325206 - 0.0377106704 0.0461591706 0.0345553607 - 0.0386387706 0.0472715795 0.0354900099 - 0.0395760499 0.0483947694 0.0364381485 - 0.0405212790 0.0495291613 0.0373984985 - 0.0414749682 0.0506746694 0.0383732207 - 0.0424371511 0.0518323407 0.0393615104 - 0.0434069186 0.0530007891 0.0403636508 - 0.0443847999 0.0541807003 0.0413801111 - 0.0453707203 0.0553707816 0.0424091816 - 0.0463633984 0.0565719493 0.0434532799 - 0.0473654494 0.0577826984 0.0445101783 - 0.0483737215 0.0590048917 0.0455798395 - 0.0493897311 0.0602366105 0.0466651618 - 0.0504129082 0.0614794008 0.0477640294 - 0.0514423698 0.0627331585 0.0488768108 - 0.0524802990 0.0639974698 0.0500062406 - 0.0535254516 0.0652740896 0.0511502102 - 0.0545772910 0.0665622428 0.0523082986 - 0.0556361787 0.0678606406 0.0534805804 - 0.0567024983 0.0691716820 0.0546686798 - 0.0577750802 0.0704934299 0.0558703691 - 0.0588535815 0.0718252510 0.0570854694 - 0.0599400401 0.0731683001 0.0583138093 - 0.0610331818 0.0745219514 0.0595557392 - 0.0621325709 0.0758863464 0.0608114190 - 0.0632396191 0.0772613883 0.0620787703 - 0.0643534884 0.0786459073 0.0633573383 - 0.0654745772 0.0800427720 0.0646468624 - 0.0666032135 0.0814517811 0.0659470782 - 0.0677386299 0.0828704983 0.0672577098 - 0.0688809007 0.0843003020 0.0685787722 - 0.0700303987 0.0857396796 0.0699099898 - 0.0711864233 0.0871883109 0.0712488964 - 0.0723488331 0.0886481106 0.0725946799 - 0.0735196322 0.0901157334 0.0739471167 - 0.0746975020 0.0915923566 0.0753060281 - 0.0758822709 0.0930788666 0.0766710714 - 0.0770741701 0.0945737883 0.0780422911 - 0.0782730505 0.0960773528 0.0794199184 - 0.0794789493 0.0975896269 0.0808036402 - 0.0806913823 0.0991091728 0.0821929798 - 0.0819104165 0.1006359980 0.0835875571 - 0.0831365734 0.1021701992 0.0849873573 - 0.0843702182 0.1037115976 0.0863918066 - 0.0856113136 0.1052597985 0.0878000185 - 0.0868599117 0.1068146974 0.0892115831 - 0.0881158933 0.1083773971 0.0906265825 - 0.0893787891 0.1099461019 0.0920447111 - 0.0906486213 0.1115209982 0.0934653208 - 0.0919253081 0.1131021008 0.0948884189 - 0.0932083800 0.1146880984 0.0963144898 - 0.0944980532 0.1162792966 0.0977434963 - 0.0957952365 0.1178762987 0.0991748795 - 0.0970996320 0.1194792986 0.1006093025 - 0.0984100029 0.1210874990 0.1020475999 - 0.0997272208 0.1226997972 0.1034903973 - 0.1010518000 0.1243164986 0.1049363986 - 0.1023814976 0.1259384006 0.1063854992 - 0.1037181988 0.1275652051 0.1078381985 - 0.1050620005 0.1291960031 0.1092941016 - 0.1064110026 0.1308314055 0.1107527018 - 0.1077655032 0.1324710995 0.1122142971 - 0.1091263965 0.1341142952 0.1136794984 - 0.1104935035 0.1357623935 0.1151480004 - 0.1118659005 0.1374143958 0.1166194007 - 0.1132436991 0.1390711069 0.1180943996 - 0.1146290004 0.1407306939 0.1195726022 - 0.1160207018 0.1423933059 0.1210533977 - 0.1174174026 0.1440594047 0.1225365996 - 0.1188201010 0.1457290053 0.1240229011 - 0.1202287003 0.1474035978 0.1255117059 - 0.1216425970 0.1490814984 0.1270032972 - 0.1230617985 0.1507631987 0.1284980029 - 0.1244878024 0.1524486989 0.1299961060 - 0.1259191930 0.1541378051 0.1314983070 - 0.1273557991 0.1558293998 0.1330049038 - 0.1287976056 0.1575267017 0.1345154941 - 0.1302458048 0.1592278928 0.1360293031 - 0.1317003965 0.1609317958 0.1375454962 - 0.1331606060 0.1626382023 0.1390648037 - 0.1346260011 0.1643474996 0.1405864060 - 0.1360958070 0.1660612971 0.1421104968 - 0.1375721991 0.1677768975 0.1436379999 - 0.1390544027 0.1694948971 0.1451703012 - 0.1405413002 0.1712158024 0.1467055976 - 0.1420325041 0.1729404032 0.1482436061 - 0.1435298026 0.1746671945 0.1497846991 - 0.1450331062 0.1763962060 0.1513286978 - 0.1465408057 0.1781273931 0.1528747976 - 0.1480524987 0.1798599064 0.1544229984 - 0.1495707035 0.1815944016 0.1559745967 - 0.1510937959 0.1833305955 0.1575296074 - 0.1526214033 0.1850683987 0.1590874940 - 0.1541541964 0.1868094057 0.1606484950 - 0.1556929052 0.1885517985 0.1622128040 - 0.1572359949 0.1902959049 0.1637787968 - 0.1587835997 0.1920423061 0.1653465927 - 0.1603381932 0.1937911958 0.1669193059 - 0.1618963927 0.1955417991 0.1684942991 - 0.1634581983 0.1972939074 0.1700717062 - 0.1650260985 0.1990482062 0.1716526002 - 0.1665998995 0.2008040994 0.1732358932 - 0.1681773067 0.2025613040 0.1748213023 - 0.1697590947 0.2043205053 0.1764107943 - 0.1713462025 0.2060810030 0.1780031025 - 0.1729367971 0.2078423053 0.1795973927 - 0.1745319068 0.2096049935 0.1811943948 - 0.1761319935 0.2113689035 0.1827940941 - 0.1777352989 0.2131334990 0.1843959987 - 0.1793437004 0.2148993015 0.1860031039 - 0.1809576005 0.2166654021 0.1876126975 - 0.1825744063 0.2184319943 0.1892254949 - 0.1841952056 0.2201997042 0.1908413023 - 0.1858201027 0.2219685018 0.1924591959 - 0.1874478012 0.2237378955 0.1940806955 - 0.1890821010 0.2255074978 0.1957052946 - 0.1907189935 0.2272779942 0.1973316073 - 0.1923578978 0.2290489972 0.1989613026 - 0.1940028965 0.2308211029 0.2005929053 - 0.1956506073 0.2325938940 0.2022265941 - 0.1973026991 0.2343678027 0.2038643956 - 0.1989585012 0.2361426055 0.2055034935 - 0.2006161064 0.2379177958 0.2071447968 - 0.2022790015 0.2396938056 0.2087884992 - 0.2039445937 0.2414696068 0.2104344964 - 0.2056134939 0.2432450950 0.2120838016 - 0.2072867006 0.2450218946 0.2137342989 - 0.2089617997 0.2467985004 0.2153877020 - 0.2106418014 0.2485740930 0.2170425951 - 0.2123246044 0.2503491044 0.2186993957 - 0.2140112072 0.2521235943 0.2203588933 - 0.2157005966 0.2538985014 0.2220194936 - 0.2173911929 0.2556734085 0.2236841023 - 0.2190874964 0.2574481070 0.2253499031 - 0.2207852006 0.2592217922 0.2270195931 - 0.2224867046 0.2609946132 0.2286913991 - 0.2241905034 0.2627679110 0.2303657979 - 0.2258961946 0.2645412982 0.2320432961 - 0.2276054025 0.2663162053 0.2337222993 - 0.2293156981 0.2680906057 0.2354062945 - 0.2310317010 0.2698636949 0.2370913029 - 0.2327484041 0.2716363966 0.2387810051 - 0.2344674021 0.2734088004 0.2404720038 - 0.2361883968 0.2751807868 0.2421665043 - 0.2379122972 0.2769527137 0.2438628972 - 0.2396381944 0.2787235081 0.2455617040 - 0.2413652986 0.2804942131 0.2472631931 - 0.2430955023 0.2822644114 0.2489665002 - 0.2448267043 0.2840346992 0.2506738007 - 0.2465613037 0.2858053148 0.2523826063 - 0.2482964993 0.2875756919 0.2540957928 - 0.2500359118 0.2893457115 0.2558099031 - 0.2517758012 0.2911148965 0.2575285137 - 0.2535183132 0.2928834856 0.2592475116 - 0.2552621961 0.2946512997 0.2609696984 - 0.2570098937 0.2964186966 0.2626922131 - 0.2587589920 0.2981835902 0.2644180059 - 0.2605102956 0.2999480069 0.2661443055 - 0.2622630894 0.3017111123 0.2678734958 - 0.2640174031 0.3034740984 0.2696034908 - 0.2657730877 0.3052338064 0.2713379860 - 0.2675299048 0.3069935143 0.2730731964 - 0.2692880034 0.3087523878 0.2748118937 - 0.2710470855 0.3105109930 0.2765515149 - 0.2728081942 0.3122670054 0.2782951891 - 0.2745707035 0.3140225112 0.2800396085 - 0.2763350904 0.3157748878 0.2817870975 - 0.2781007886 0.3175269067 0.2835353911 - 0.2798672915 0.3192762136 0.2852882147 - 0.2816343009 0.3210250139 0.2870416045 - 0.2834017873 0.3227711022 0.2887994945 - 0.2851698101 0.3245170116 0.2905577123 - 0.2869389057 0.3262611926 0.2923206985 - 0.2887091935 0.3280051947 0.2940837145 - 0.2904804051 0.3297472894 0.2958520949 - 0.2922525108 0.3314892948 0.2976205945 - 0.2940253913 0.3332268000 0.2993935049 - 0.2957994938 0.3349643052 0.3011670113 - 0.2975736856 0.3366967142 0.3029440939 - 0.2993479967 0.3384287953 0.3047224879 - 0.3011221886 0.3401573896 0.3065038025 - 0.3028964102 0.3418853879 0.3082869947 - 0.3046706915 0.3436118066 0.3100728095 - 0.3064466119 0.3453375995 0.3118613958 - 0.3082225025 0.3470619917 0.3136520088 - 0.3099983037 0.3487856984 0.3154470026 - 0.3117741942 0.3505071998 0.3172427118 - 0.3135501146 0.3522264063 0.3190433085 - 0.3153258860 0.3539440036 0.3208439052 - 0.3171015978 0.3556582034 0.3226490021 - 0.3188770115 0.3573715091 0.3244544864 - 0.3206518888 0.3590804040 0.3262634873 - 0.3224261105 0.3607892990 0.3280740976 - 0.3241997957 0.3624913990 0.3298870027 - 0.3259718120 0.3641934097 0.3317027092 - 0.3277438879 0.3658939898 0.3335191905 - 0.3295131028 0.3675940931 0.3353388011 - 0.3312822878 0.3692927063 0.3371585011 - 0.3330504000 0.3709900975 0.3389824033 - 0.3348180056 0.3726865053 0.3408068120 - 0.3365843892 0.3743805885 0.3426336944 - 0.3383488953 0.3760747015 0.3444621861 - 0.3401131928 0.3777655959 0.3462921977 - 0.3418753147 0.3794564903 0.3481256962 - 0.3436372876 0.3811435997 0.3499591947 - 0.3453963101 0.3828293979 0.3517968059 - 0.3471544981 0.3845128119 0.3536345959 - 0.3489103913 0.3861933053 0.3554756045 - 0.3506639004 0.3878732026 0.3573186994 - 0.3524166942 0.3895485103 0.3591623902 - 0.3541651070 0.3912237883 0.3610084951 - 0.3559136093 0.3928934932 0.3628545105 - 0.3576560915 0.3945614994 0.3647046089 - 0.3593969047 0.3962261975 0.3665556014 - 0.3611347079 0.3978857994 0.3684079945 - 0.3628677130 0.3995453119 0.3702622950 - 0.3646006882 0.4011983871 0.3721165955 - 0.3663268089 0.4028514922 0.3739753067 - 0.3680528998 0.4045008123 0.3758341074 - 0.3697755039 0.4061478078 0.3776954114 - 0.3714954853 0.4077934027 0.3795588911 - 0.3732145131 0.4094338119 0.3814226985 - 0.3749260008 0.4110741019 0.3832913041 - 0.3766376078 0.4127109051 0.3851599097 - 0.3783437908 0.4143466055 0.3870306909 - 0.3800471127 0.4159803092 0.3889032900 - 0.3817487061 0.4176093936 0.3907761872 - 0.3834415078 0.4192385077 0.3926539123 - 0.3851343989 0.4208612144 0.3945316970 - 0.3868206143 0.4224824905 0.3964118063 - 0.3885033131 0.4241017103 0.3982942104 - 0.3901846111 0.4257161915 0.4001766145 - 0.3918552995 0.4273307025 0.4020617008 - 0.3935258985 0.4289397895 0.4039469957 - 0.3951900005 0.4305475056 0.4058333039 - 0.3968490958 0.4321533144 0.4077214003 - 0.3985081911 0.4337531030 0.4096095860 - 0.4001541138 0.4353528917 0.4114991128 - 0.4017998874 0.4369482994 0.4133890867 - 0.4034416974 0.4385415018 0.4152793884 - 0.4050774872 0.4401341975 0.4171707928 - 0.4067131877 0.4417211115 0.4190622866 - 0.4083409905 0.4433079958 0.4209538102 - 0.4099667072 0.4448912144 0.4228455126 - 0.4115909040 0.4464702904 0.4247371852 - 0.4132077098 0.4480494857 0.4266293049 - 0.4148243964 0.4496214092 0.4285216033 - 0.4164358974 0.4511919916 0.4304136932 - 0.4180422127 0.4527612031 0.4323050082 - 0.4196484983 0.4543245137 0.4341962934 - 0.4212470949 0.4558877945 0.4360859990 - 0.4228439927 0.4574449062 0.4379737973 - 0.4244396091 0.4589962959 0.4398615062 - 0.4260253906 0.4605476856 0.4417462945 - 0.4276112020 0.4620890021 0.4436298907 - 0.4291928113 0.4636281133 0.4455133975 - 0.4307680130 0.4651657939 0.4473905861 - 0.4323433042 0.4666922987 0.4492675066 - 0.4339112937 0.4682187140 0.4511435032 - 0.4354755878 0.4697411060 0.4530152977 - 0.4370399117 0.4712564051 0.4548870921 - 0.4385950863 0.4727717042 0.4567565024 - 0.4401490986 0.4742799997 0.4586217105 - 0.4417021871 0.4757837057 0.4604870081 - 0.4432457983 0.4772874117 0.4623486996 - 0.4447894096 0.4787816107 0.4642075896 - 0.4463303983 0.4802734852 0.4660665095 - 0.4478640854 0.4817655087 0.4679197073 - 0.4493978918 0.4832465053 0.4697704017 - 0.4509268105 0.4847275019 0.4716210961 - 0.4524492919 0.4862065911 0.4734647870 - 0.4539718032 0.4876767099 0.4753069878 - 0.4554879069 0.4891467094 0.4771490991 - 0.4569990933 0.4906134903 0.4789814949 - 0.4585103989 0.4920732081 0.4808132052 - 0.4600146115 0.4935329854 0.4826447070 - 0.4615156949 0.4949879050 0.4844656885 - 0.4630168080 0.4964365065 0.4862866998 - 0.4645102024 0.4978851080 0.4881069064 - 0.4660013914 0.4993262887 0.4899150133 - 0.4674926102 0.5007606745 0.4917230010 - 0.4689762890 0.5021951795 0.4935297966 - 0.4704585075 0.5036209226 0.4953246117 - 0.4719407856 0.5050408244 0.4971193969 - 0.4734140933 0.5064607263 0.4989126027 - 0.4748862982 0.5078724027 0.5006924868 - 0.4763585031 0.5092796087 0.5024725199 - 0.4778222144 0.5106868744 0.5042511225 - 0.4792849123 0.5120872855 0.5060155988 - 0.4807476103 0.5134844184 0.5077801943 - 0.4822027981 0.5148816109 0.5095440149 - 0.4836570024 0.5162711740 0.5112923980 - 0.4851112962 0.5176569819 0.5130407214 - 0.4865578115 0.5190427899 0.5147891045 - 0.4880031049 0.5204212070 0.5165216923 - 0.4894483984 0.5217952728 0.5182538033 - 0.4908877909 0.5231695175 0.5199859738 - 0.4923256934 0.5245352983 0.5217025876 - 0.4937636852 0.5258951783 0.5234171748 - 0.4951958060 0.5272551179 0.5251318216 - 0.4966256022 0.5286077261 0.5268319845 - 0.4980554879 0.5299531221 0.5285276771 - 0.4994794130 0.5312983990 0.5302234888 - 0.5008996129 0.5326380134 0.5319082141 - 0.5023198128 0.5339689255 0.5335860848 - 0.5037351847 0.5352998972 0.5352638960 - 0.5051453114 0.5366278887 0.5369331837 - 0.5065554976 0.5379484296 0.5385919213 - 0.5079630017 0.5392689109 0.5402505994 - 0.5093650222 0.5405880809 0.5419046879 - 0.5107669234 0.5418984890 0.5435451269 - 0.5121678710 0.5432088971 0.5451855063 - 0.5135623813 0.5445193052 0.5468252897 - 0.5149568915 0.5458185077 0.5484473109 - 0.5163514018 0.5471168756 0.5500692725 - 0.5177388787 0.5484153032 0.5516912937 - 0.5191257000 0.5497050881 0.5532999039 - 0.5205125213 0.5509917140 0.5549045801 - 0.5218951702 0.5522782803 0.5565093756 - 0.5232759118 0.5535587072 0.5581051707 - 0.5246565938 0.5548328757 0.5596920252 - 0.5260347128 0.5561071038 0.5612788200 - 0.5274087787 0.5573784113 0.5628618002 - 0.5287830234 0.5586404800 0.5644289255 - 0.5301563144 0.5599024892 0.5659958720 - 0.5315235257 0.5611644983 0.5675628781 - 0.5328906178 0.5624132752 0.5691115856 - 0.5342578292 0.5636613965 0.5706571937 - 0.5356180072 0.5649095178 0.5722029209 - 0.5369765162 0.5661481023 0.5737379193 - 0.5383349061 0.5673810840 0.5752621889 - 0.5396882296 0.5686141849 0.5767865181 - 0.5410351157 0.5698431134 0.5783087015 - 0.5423818827 0.5710616112 0.5798140168 - 0.5437278748 0.5722801089 0.5813193917 - 0.5450633168 0.5734986067 0.5828247070 - 0.5463985801 0.5747051239 0.5843166113 - 0.5477339029 0.5759106278 0.5858026743 - 0.5490639210 0.5771160126 0.5872887969 - 0.5503911972 0.5783153772 0.5887703896 - 0.5517184138 0.5795091987 0.5902382731 - 0.5530437827 0.5807030201 0.5917062759 - 0.5543630123 0.5818957090 0.5931742191 - 0.5556821823 0.5830773115 0.5946282744 - 0.5570014119 0.5842589140 0.5960785151 - 0.5583145022 0.5854403973 0.5975285769 - 0.5596256852 0.5866149068 0.5989735723 - 0.5609369278 0.5877853036 0.6004049778 - 0.5622456074 0.5889558196 0.6018365026 - 0.5635476112 0.5901247263 0.6032679081 - 0.5648496151 0.5912854075 0.6046854854 - 0.5661516786 0.5924460888 0.6060982943 - 0.5674464703 0.5936068296 0.6075112224 - 0.5687389970 0.5947598219 0.6089211702 - 0.5700315237 0.5959085226 0.6103187799 - 0.5713216066 0.5970572233 0.6117163897 - 0.5726023912 0.5982046723 0.6131139994 - 0.5738831758 0.5993419886 0.6145017743 - 0.5751640201 0.6004794240 0.6158828735 - 0.5764371753 0.6016166806 0.6172639728 - 0.5777059793 0.6027488708 0.6186450720 - 0.5789747834 0.6038765907 0.6200103164 - 0.5802434087 0.6050041914 0.6213750243 - 0.5815005898 0.6061319113 0.6227397919 - 0.5827577710 0.6072487235 0.6240993142 - 0.5840150714 0.6083645821 0.6254476905 - 0.5852677822 0.6094806194 0.6267961860 - 0.5865132213 0.6105933189 0.6281446218 - 0.5877586007 0.6116985083 0.6294825077 - 0.5890039802 0.6128035784 0.6308146119 - 0.5902428031 0.6139087081 0.6321465969 - 0.5914788842 0.6150081754 0.6334787011 - 0.5927150249 0.6161044836 0.6347957253 - 0.5939509869 0.6172006726 0.6361116171 - 0.5951772928 0.6182969809 0.6374276280 - 0.5964037180 0.6193832159 0.6387404799 - 0.5976300240 0.6204687953 0.6400408745 - 0.5988535881 0.6215543151 0.6413413882 - 0.6000704169 0.6226378083 0.6426417828 - 0.6012871861 0.6237139106 0.6439359188 - 0.6025038958 0.6247900128 0.6452212930 - 0.6037154794 0.6258659959 0.6465067267 - 0.6049222946 0.6269382238 0.6477921009 - 0.6061292291 0.6280059218 0.6490674019 - 0.6073359847 0.6290736794 0.6503365040 - 0.6085358262 0.6301413774 0.6516057253 - 0.6097332239 0.6311997175 0.6528748274 - 0.6109305024 0.6322535872 0.6541312933 - 0.6121277809 0.6333073974 0.6553844810 - 0.6133171916 0.6343613267 0.6566376090 - 0.6145057082 0.6354042292 0.6578906775 - 0.6156942844 0.6364452839 0.6591296792 - 0.6168823242 0.6374865174 0.6603674293 - 0.6180605888 0.6385275126 0.6616051793 - 0.6192389727 0.6395580769 0.6628425717 - 0.6204174161 0.6405887008 0.6640645266 - 0.6215937734 0.6416193247 0.6652863026 - 0.6227611899 0.6426482201 0.6665081978 - 0.6239284873 0.6436666846 0.6677287817 - 0.6250959039 0.6446852088 0.6689363122 - 0.6262605190 0.6457036138 0.6701437831 - 0.6274175048 0.6467195749 0.6713513136 - 0.6285744905 0.6477273107 0.6725571752 - 0.6297314763 0.6487351060 0.6737517118 - 0.6308856010 0.6497427821 0.6749461889 - 0.6320332289 0.6507480741 0.6761407256 - 0.6331807971 0.6517474055 0.6773335934 - 0.6343284249 0.6527466774 0.6785153151 - 0.6354721785 0.6537461281 0.6796969175 - 0.6366078854 0.6547424197 0.6808785796 - 0.6377437115 0.6557319760 0.6820589900 - 0.6388794184 0.6567215919 0.6832273006 - 0.6400116086 0.6577112079 0.6843956113 - 0.6411356926 0.6586982012 0.6855639219 - 0.6422597766 0.6596792936 0.6867318153 - 0.6433839202 0.6606603861 0.6878877878 - 0.6445053220 0.6616414785 0.6890437007 - 0.6456189752 0.6626203060 0.6901996732 - 0.6467326283 0.6635925174 0.6913555861 - 0.6478462815 0.6645647883 0.6925013065 - 0.6489577889 0.6655369997 0.6936463118 - 0.6500586867 0.6665074825 0.6947911978 - 0.6511597037 0.6674702168 0.6959362030 - 0.6522606015 0.6684328914 0.6970707774 - 0.6533609033 0.6693956256 0.6982029080 - 0.6544495821 0.6703575850 0.6993349791 - 0.6555382013 0.6713106036 0.7004671097 - 0.6566268206 0.6722635031 0.7015910149 - 0.6577154994 0.6732165217 0.7027103901 - 0.6587942839 0.6741694212 0.7038298249 - 0.6598721147 0.6751123071 0.7049492002 - 0.6609498858 0.6760544181 0.7060629129 - 0.6620277166 0.6769965291 0.7071694732 - 0.6630986929 0.6779385805 0.7082760930 - 0.6641669869 0.6788738966 0.7093827128 - 0.6652354002 0.6798070073 0.7104861736 - 0.6663036942 0.6807401180 0.7115777731 - 0.6673663259 0.6816732287 0.7126693726 - 0.6684231162 0.6826019287 0.7137609124 - 0.6694800258 0.6835269928 0.7148525119 - 0.6705368161 0.6844519973 0.7159308791 - 0.6715909839 0.6853770018 0.7170084119 - 0.6726369262 0.6862993240 0.7180858254 - 0.6736826897 0.6872149706 0.7191632986 - 0.6747285724 0.6881306171 0.7202311158 - 0.6757745147 0.6890463233 0.7212930918 - 0.6768103838 0.6899619102 0.7223551869 - 0.6778457165 0.6908699870 0.7234172225 - 0.6788811088 0.6917781234 0.7244750261 - 0.6799163818 0.6926862001 0.7255222797 - 0.6809443235 0.6935942769 0.7265697122 - 0.6819676161 0.6944952011 0.7276170254 - 0.6829909086 0.6953927279 0.7286642790 - 0.6840142012 0.6962900758 0.7296993136 - 0.6850343943 0.6971876025 0.7307329774 - 0.6860457063 0.6980822086 0.7317667007 - 0.6870568991 0.6989707947 0.7328004241 - 0.6880680919 0.6998593807 0.7338271141 - 0.6890792847 0.7007479072 0.7348465919 - 0.6900807023 0.7016364932 0.7358661294 - 0.6910803914 0.7025167942 0.7368856072 - 0.6920800805 0.7033964992 0.7379044890 - 0.6930798888 0.7042762041 0.7389112711 - 0.6940743923 0.7051559091 0.7399181128 - 0.6950619221 0.7060313225 0.7409247756 - 0.6960492730 0.7069025040 0.7419316173 - 0.6970368028 0.7077736855 0.7429320216 - 0.6980242133 0.7086449862 0.7439273000 - 0.6990013719 0.7095158100 0.7449225783 - 0.6999779940 0.7103791833 0.7459179163 - 0.7009546757 0.7112424970 0.7469127774 - 0.7019312978 0.7121058106 0.7478963733 - 0.7029024959 0.7129691243 0.7488800287 - 0.7038668990 0.7138271928 0.7498636246 - 0.7048313022 0.7146809101 0.7508472204 - 0.7057957053 0.7155346870 0.7518255115 - 0.7067601085 0.7163884044 0.7527979016 - 0.7077146769 0.7172421217 0.7537702918 - 0.7086681128 0.7180885077 0.7547428012 - 0.7096214890 0.7189350128 0.7557151914 - 0.7105749846 0.7197813988 0.7566773891 - 0.7115257978 0.7206279039 0.7576380968 - 0.7124705911 0.7214711905 0.7585988045 - 0.7134153843 0.7223104835 0.7595595121 - 0.7143602967 0.7231497169 0.7605171204 - 0.7153050900 0.7239890099 0.7614648938 - 0.7162432075 0.7248281837 0.7624126077 - 0.7171782255 0.7256616950 0.7633603811 - 0.7181131840 0.7264938951 0.7643082142 - 0.7190482020 0.7273260951 0.7652475238 - 0.7199829817 0.7281581759 0.7661802173 - 0.7209094167 0.7289890051 0.7671129107 - 0.7218356729 0.7298128009 0.7680456042 - 0.7227619886 0.7306365967 0.7689781785 - 0.7236883044 0.7314602733 0.7698999047 - 0.7246115804 0.7322841287 0.7708194852 - 0.7255285978 0.7331035137 0.7717391849 - 0.7264454961 0.7339180708 0.7726588249 - 0.7273623943 0.7347326875 0.7735773921 - 0.7282792926 0.7355474234 0.7744857073 - 0.7291893959 0.7363619804 0.7753940225 - 0.7300944924 0.7371708155 0.7763022780 - 0.7309995890 0.7379775047 0.7772104740 - 0.7319046855 0.7387843132 0.7781155109 - 0.7328097820 0.7395910025 0.7790126801 - 0.7337058783 0.7403978109 0.7799099088 - 0.7345998287 0.7411972284 0.7808070779 - 0.7354937196 0.7419964075 0.7817043066 - 0.7363876104 0.7427955866 0.7825965285 - 0.7372813821 0.7435948253 0.7834830880 - 0.7381641269 0.7443925738 0.7843697071 - 0.7390465736 0.7451831102 0.7852563262 - 0.7399291992 0.7459737062 0.7861428261 - 0.7408117056 0.7467643023 0.7870228291 - 0.7416930199 0.7475548983 0.7878980041 - 0.7425662875 0.7483425736 0.7887732983 - 0.7434394956 0.7491239905 0.7896485925 - 0.7443128228 0.7499054074 0.7905238867 - 0.7451859713 0.7506868243 0.7913916707 - 0.7460573912 0.7514681220 0.7922559977 - 0.7469223738 0.7522463202 0.7931203246 - 0.7477872968 0.7530199885 0.7939847112 - 0.7486522794 0.7537937164 0.7948489785 - 0.7495173216 0.7545673847 0.7957043052 - 0.7503795028 0.7553411126 0.7965561152 - 0.7512350082 0.7561119795 0.7974079251 - 0.7520905733 0.7568801045 0.7982596755 - 0.7529460788 0.7576481104 0.7991114855 - 0.7538017035 0.7584161758 0.7999551296 - 0.7546544075 0.7591843009 0.8007956147 - 0.7555009127 0.7599483728 0.8016362190 - 0.7563472986 0.7607088089 0.8024767041 - 0.7571938038 0.7614691854 0.8033173084 - 0.7580403090 0.7622296810 0.8041502237 - 0.7588834763 0.7629901171 0.8049796820 - 0.7597190142 0.7637473941 0.8058090806 - 0.7605543733 0.7645015717 0.8066385984 - 0.7613899112 0.7652558088 0.8074679971 - 0.7622252703 0.7660099864 0.8082910180 - 0.7630584836 0.7667642236 0.8091096282 - 0.7638840079 0.7675157189 0.8099281788 - 0.7647094131 0.7682638764 0.8107469082 - 0.7655348778 0.7690120935 0.8115655184 - 0.7663604021 0.7697603106 0.8123788834 - 0.7671846151 0.7705085278 0.8131868243 - 0.7680001259 0.7712538242 0.8139945865 - 0.7688155174 0.7719941139 0.8148024082 - 0.7696310282 0.7727344036 0.8156102896 - 0.7704464197 0.7734745741 0.8164144754 - 0.7712618113 0.7742149234 0.8172109723 - 0.7720686793 0.7749537230 0.8180075288 - 0.7728753090 0.7756870985 0.8188040257 - 0.7736818790 0.7764205933 0.8196005225 - 0.7744885087 0.7771540284 0.8203955889 - 0.7752950788 0.7778875232 0.8211801052 - 0.7760956287 0.7786207199 0.8219646215 - 0.7768943906 0.7793480754 0.8227490783 - 0.7776932120 0.7800753713 0.8235335946 - 0.7784920931 0.7808027864 0.8243181109 - 0.7792909145 0.7815300822 0.8250932097 - 0.7800844908 0.7822574973 0.8258666992 - 0.7808738947 0.7829779983 0.8266401887 - 0.7816631794 0.7836970091 0.8274136782 - 0.7824525833 0.7844160795 0.8281872272 - 0.7832419872 0.7851350904 0.8289542198 - 0.7840290070 0.7858542204 0.8297163248 - 0.7848097086 0.7865694761 0.8304784298 - 0.7855904102 0.7872818112 0.8312404752 - 0.7863711119 0.7879942060 0.8320025802 - 0.7871518135 0.7887064815 0.8327620029 - 0.7879325151 0.7894188166 0.8335124850 - 0.7887042761 0.7901296020 0.8342630267 - 0.7894756794 0.7908356786 0.8350133896 - 0.7902470231 0.7915416956 0.8357638717 - 0.7910184264 0.7922477126 0.8365142941 - 0.7917898297 0.7929537892 0.8372542858 - 0.7925562859 0.7936598063 0.8379924893 - 0.7933195829 0.7943586707 0.8387306929 - 0.7940828800 0.7950568199 0.8394688964 - 0.7948461771 0.7957547903 0.8402070999 - 0.7956094146 0.7964528203 0.8409410715 - 0.7963712811 0.7971509099 0.8416694999 - 0.7971267104 0.7978456020 0.8423978090 - 0.7978820801 0.7985368967 0.8431261182 - 0.7986375093 0.7992283106 0.8438544273 - 0.7993928790 0.7999196053 0.8445827961 - 0.8001483083 0.8006108999 0.8453016281 - 0.8008971810 0.8013020158 0.8460199237 - 0.8016437292 0.8019875288 0.8467382789 - 0.8023902178 0.8026731014 0.8474565744 - 0.8031367064 0.8033586144 0.8481749296 - 0.8038831949 0.8040441275 0.8488876224 - 0.8046274781 0.8047297001 0.8495929837 - 0.8053646088 0.8054109216 0.8502982855 - 0.8061016798 0.8060886264 0.8510035872 - 0.8068388104 0.8067662716 0.8517088890 - 0.8075758815 0.8074440956 0.8524143100 - 0.8083130121 0.8081218004 0.8531094790 - 0.8090445995 0.8087993860 0.8538032770 - 0.8097738028 0.8094710708 0.8544970751 - 0.8105030060 0.8101428747 0.8551908731 - 0.8112320900 0.8108146191 0.8558846712 - 0.8119612932 0.8114864230 0.8565754890 - 0.8126897216 0.8121581078 0.8572577834 - 0.8134108782 0.8128275275 0.8579400778 - 0.8141322136 0.8134937882 0.8586223722 - 0.8148534298 0.8141601086 0.8593047261 - 0.8155747056 0.8148264885 0.8599870801 - 0.8162959218 0.8154928088 0.8606625199 - 0.8170127273 0.8161591887 0.8613334894 - 0.8177244067 0.8168199062 0.8620045781 - 0.8184360266 0.8174793124 0.8626756072 - 0.8191475868 0.8181387782 0.8633465767 - 0.8198593259 0.8187981844 0.8640176058 - 0.8205708861 0.8194577098 0.8646782041 - 0.8212757707 0.8201162815 0.8653377891 - 0.8219788074 0.8207699060 0.8659973145 - 0.8226817846 0.8214235902 0.8666567802 - 0.8233848214 0.8220772147 0.8673163056 - 0.8240877986 0.8227308989 0.8679741025 - 0.8247904181 0.8233845234 0.8686218858 - 0.8254855275 0.8240357041 0.8692697287 - 0.8261805177 0.8246836066 0.8699175119 - 0.8268756270 0.8253313899 0.8705654144 - 0.8275706172 0.8259792924 0.8712131977 - 0.8282657266 0.8266270757 0.8718574047 - 0.8289580941 0.8272749782 0.8724951744 - 0.8296430111 0.8279188275 0.8731331229 - 0.8303279281 0.8285604715 0.8737708926 - 0.8310129046 0.8292021751 0.8744087219 - 0.8316978216 0.8298439980 0.8750464916 - 0.8323826790 0.8304857016 0.8756781816 - 0.8330640197 0.8311274052 0.8763042092 - 0.8337404132 0.8317648768 0.8769301176 - 0.8344169259 0.8324015141 0.8775560260 - 0.8350933194 0.8330382109 0.8781819940 - 0.8357697725 0.8336749077 0.8788079023 - 0.8364462256 0.8343114853 0.8794273734 - 0.8371186256 0.8349481821 0.8800430298 - 0.8377876878 0.8355782032 0.8806586266 - 0.8384569287 0.8362079859 0.8812742233 - 0.8391259909 0.8368378282 0.8818897009 - 0.8397951722 0.8374676704 0.8825052977 - 0.8404642940 0.8380975127 0.8831139207 - 0.8411294222 0.8387269974 0.8837190270 - 0.8417921066 0.8393514752 0.8843241930 - 0.8424547911 0.8399760127 0.8849294186 - 0.8431175947 0.8406004906 0.8855345249 - 0.8437802792 0.8412250876 0.8861396909 - 0.8444430232 0.8418496251 0.8867368102 - 0.8451002240 0.8424736261 0.8873301148 - 0.8457545042 0.8430926204 0.8879234195 - 0.8464089036 0.8437114954 0.8885167241 - 0.8470631838 0.8443304896 0.8891100287 - 0.8477175832 0.8449494839 0.8897033930 - 0.8483719230 0.8455684781 0.8902897239 - 0.8490217924 0.8461869955 0.8908721209 - 0.8496690989 0.8468005061 0.8914545178 - 0.8503165245 0.8474140167 0.8920369148 - 0.8509638906 0.8480275869 0.8926193118 - 0.8516113162 0.8486410975 0.8932017088 - 0.8522586823 0.8492546082 0.8937774897 - 0.8529011011 0.8498679996 0.8943479061 - 0.8535398841 0.8504769206 0.8949182034 - 0.8541787267 0.8510857821 0.8954886198 - 0.8548175097 0.8516947031 0.8960589767 - 0.8554562926 0.8523036242 0.8966292739 - 0.8560950756 0.8529124260 0.8971949220 - 0.8567304015 0.8535212874 0.8977534175 - 0.8573613763 0.8541247249 0.8983119130 - 0.8579922915 0.8547276258 0.8988704085 - 0.8586232066 0.8553304076 0.8994289041 - 0.8592541814 0.8559333086 0.8999875188 - 0.8598850965 0.8565362096 0.9005435705 - 0.8605139852 0.8571389914 0.9010909200 - 0.8611372113 0.8577383757 0.9016382098 - 0.8617604971 0.8583366275 0.9021854997 - 0.8623837829 0.8589348197 0.9027327895 - 0.8630070090 0.8595330119 0.9032800794 - 0.8636302948 0.8601312041 0.9038274288 - 0.8642532229 0.8607293963 0.9043639898 - 0.8648697734 0.8613250852 0.9049003124 - 0.8654863834 0.8619186878 0.9054365158 - 0.8661028743 0.8625122905 0.9059727192 - 0.8667194843 0.8631058931 0.9065089822 - 0.8673360944 0.8636994958 0.9070451856 - 0.8679525852 0.8642930984 0.9075742960 - 0.8685629964 0.8648849130 0.9080997705 - 0.8691716194 0.8654721975 0.9086254239 - 0.8697801828 0.8660594225 0.9091508985 - 0.8703888059 0.8666465878 0.9096763730 - 0.8709974289 0.8672338128 0.9102020264 - 0.8716059923 0.8678210974 0.9107244015 - 0.8722118735 0.8684083223 0.9112399817 - 0.8728144169 0.8689904809 0.9117556214 - 0.8734169006 0.8695725203 0.9122712016 - 0.8740193844 0.8701543808 0.9127867818 - 0.8746219277 0.8707364202 0.9133024216 - 0.8752244115 0.8713182807 0.9138180017 - 0.8758262992 0.8719003201 0.9143245220 - 0.8764201999 0.8724784851 0.9148302078 - 0.8770139813 0.8730542064 0.9153358936 - 0.8776078820 0.8736298084 0.9158415794 - 0.8782017827 0.8742055297 0.9163473248 - 0.8787956834 0.8747810721 0.9168530107 - 0.8793895245 0.8753566742 0.9173536897 - 0.8799785972 0.8759317994 0.9178488255 - 0.8805649877 0.8765037060 0.9183437824 - 0.8811513186 0.8770756721 0.9188389182 - 0.8817375898 0.8776475787 0.9193338752 - 0.8823239207 0.8782194853 0.9198290110 - 0.8829101920 0.8787915111 0.9203240275 - 0.8834956288 0.8793634176 0.9208073020 - 0.8840755820 0.8799321055 0.9212900996 - 0.8846555948 0.8804993033 0.9217727780 - 0.8852356076 0.8810665011 0.9222555757 - 0.8858155012 0.8816336989 0.9227383137 - 0.8863955140 0.8822008967 0.9232211113 - 0.8869755268 0.8827680945 0.9236993790 - 0.8875501156 0.8833345771 0.9241709113 - 0.8881211281 0.8838980198 0.9246425033 - 0.8886920810 0.8844614029 0.9251139760 - 0.8892630935 0.8850247264 0.9255855083 - 0.8898339868 0.8855881095 0.9260569811 - 0.8904049993 0.8861514926 0.9265285134 - 0.8909760118 0.8867148161 0.9269909859 - 0.8915405869 0.8872753978 0.9274508953 - 0.8921051025 0.8878340125 0.9279108047 - 0.8926696777 0.8883925080 0.9283707142 - 0.8932343125 0.8889510036 0.9288306236 - 0.8937988281 0.8895096183 0.9292904735 - 0.8943634033 0.8900681138 0.9297493100 - 0.8949252963 0.8906266093 0.9301987886 - 0.8954825997 0.8911815286 0.9306483269 - 0.8960399032 0.8917360902 0.9310978055 - 0.8965970874 0.8922907114 0.9315472841 - 0.8971543908 0.8928452730 0.9319968224 - 0.8977115750 0.8933998942 0.9324463010 - 0.8982688785 0.8939545751 0.9328917265 - 0.8988218904 0.8945080042 0.9333304167 - 0.8993726969 0.8950576782 0.9337691069 - 0.8999235034 0.8956074119 0.9342077971 - 0.9004741907 0.8961570859 0.9346464872 - 0.9010249972 0.8967068791 0.9350851774 - 0.9015758038 0.8972566128 0.9355239272 - 0.9021266103 0.8978062868 0.9359565973 - 0.9026709199 0.8983541131 0.9363852739 - 0.9032143950 0.8988997936 0.9368138909 - 0.9037579298 0.8994455934 0.9372426271 - 0.9043012857 0.8999912739 0.9376713037 - 0.9048448205 0.9005370736 0.9380999207 - 0.9053882957 0.9010828733 0.9385285974 - 0.9059311748 0.9016286135 0.9389498234 - 0.9064669013 0.9021710157 0.9393684864 - 0.9070025086 0.9027118087 0.9397872090 - 0.9075381160 0.9032527208 0.9402059913 - 0.9080737829 0.9037935138 0.9406247139 - 0.9086093903 0.9043343067 0.9410433769 - 0.9091449976 0.9048752189 0.9414622188 - 0.9096794724 0.9054160118 0.9418721199 - 0.9102091789 0.9059522748 0.9422804713 - 0.9107388258 0.9064875841 0.9426888824 - 0.9112684727 0.9070228934 0.9430974126 - 0.9117981791 0.9075582027 0.9435058236 - 0.9123278856 0.9080935121 0.9439141750 - 0.9128574729 0.9086288214 0.9443227053 - 0.9133856893 0.9091640115 0.9447216988 - 0.9139096141 0.9096950293 0.9451196194 - 0.9144334793 0.9102253914 0.9455175996 - 0.9149574041 0.9107558727 0.9459155798 - 0.9154813290 0.9112864137 0.9463135004 - 0.9160053134 0.9118168950 0.9467114806 - 0.9165291786 0.9123473763 0.9471095204 - 0.9170514941 0.9128779173 0.9474971890 - 0.9175698161 0.9134051800 0.9478837252 - 0.9180880785 0.9139323235 0.9482702017 - 0.9186064005 0.9144592881 0.9486566782 - 0.9191247225 0.9149863720 0.9490432143 - 0.9196429849 0.9155133963 0.9494296908 - 0.9201613069 0.9160404801 0.9498162270 - 0.9206780195 0.9165676236 0.9501945972 - 0.9211896062 0.9170905948 0.9505711794 - 0.9217011929 0.9176132083 0.9509478807 - 0.9222127795 0.9181358814 0.9513245225 - 0.9227244258 0.9186584949 0.9517011046 - 0.9232360721 0.9191811085 0.9520776868 - 0.9237477183 0.9197037220 0.9524543881 - 0.9242582917 0.9202262759 0.9528239965 - 0.9247623086 0.9207435846 0.9531905055 - 0.9252662063 0.9212597013 0.9535570741 - 0.9257702231 0.9217758179 0.9539235830 - 0.9262741804 0.9222918749 0.9542900920 - 0.9267780781 0.9228079915 0.9546567202 - 0.9272820950 0.9233241081 0.9550232291 - 0.9277861118 0.9238402247 0.9553844929 - 0.9282817841 0.9243530035 0.9557405710 - 0.9287772179 0.9248641133 0.9560965896 - 0.9292725921 0.9253752828 0.9564527273 - 0.9297680259 0.9258863926 0.9568088055 - 0.9302634001 0.9263976216 0.9571648836 - 0.9307587743 0.9269086719 0.9575210214 - 0.9312543273 0.9274199009 0.9578741193 - 0.9317451715 0.9279295206 0.9582188725 - 0.9322345853 0.9284372926 0.9585636258 - 0.9327239990 0.9289451241 0.9589083791 - 0.9332134724 0.9294528961 0.9592531919 - 0.9337028861 0.9299607277 0.9595978856 - 0.9341922998 0.9304684997 0.9599426985 - 0.9346817136 0.9309763908 0.9602873921 - 0.9351692200 0.9314835072 0.9606251121 - 0.9356545210 0.9319880009 0.9609624743 - 0.9361398220 0.9324923754 0.9612998962 - 0.9366251230 0.9329968095 0.9616371989 - 0.9371104240 0.9335011840 0.9619746208 - 0.9375957251 0.9340056181 0.9623119235 - 0.9380810261 0.9345099926 0.9626492858 - 0.9385657907 0.9350144267 0.9629824758 - 0.9390466809 0.9355158210 0.9633126855 - 0.9395276904 0.9360166192 0.9636428952 - 0.9400085807 0.9365174174 0.9639731050 - 0.9404894710 0.9370182157 0.9643033147 - 0.9409704804 0.9375190735 0.9646335244 - 0.9414513707 0.9380198717 0.9649637938 - 0.9419323206 0.9385207295 0.9652928710 - 0.9424098134 0.9390196800 0.9656162262 - 0.9428859949 0.9395166039 0.9659395218 - 0.9433621168 0.9400135279 0.9662628770 - 0.9438381791 0.9405103922 0.9665861726 - 0.9443143010 0.9410073757 0.9669095278 - 0.9447904825 0.9415042996 0.9672328234 - 0.9452666044 0.9420012236 0.9675561190 - 0.9457414746 0.9424980879 0.9678742886 - 0.9462127090 0.9429908991 0.9681897759 - 0.9466838837 0.9434838295 0.9685053229 - 0.9471551180 0.9439765811 0.9688206911 - 0.9476262927 0.9444695115 0.9691361785 - 0.9480975270 0.9449623227 0.9694517255 - 0.9485687017 0.9454551935 0.9697672129 - 0.9490398765 0.9459481239 0.9700819254 - 0.9495072961 0.9464387894 0.9703902006 - 0.9499732256 0.9469273090 0.9706984162 - 0.9504390955 0.9474158287 0.9710066915 - 0.9509050846 0.9479044080 0.9713149071 - 0.9513710141 0.9483929276 0.9716231823 - 0.9518368840 0.9488813877 0.9719315171 - 0.9523028135 0.9493700266 0.9722396731 - 0.9527680874 0.9498584867 0.9725440741 - 0.9532285929 0.9503428936 0.9728444219 - 0.9536892176 0.9508270025 0.9731448293 - 0.9541497827 0.9513109922 0.9734451175 - 0.9546102881 0.9517951012 0.9737455249 - 0.9550709128 0.9522790909 0.9740458727 - 0.9555314183 0.9527631998 0.9743462205 - 0.9559919834 0.9532471895 0.9746466279 - 0.9564499259 0.9537299275 0.9749407768 - 0.9569048285 0.9542092085 0.9752337933 - 0.9573596716 0.9546884894 0.9755266905 - 0.9578146935 0.9551678896 0.9758195877 - 0.9582695961 0.9556471705 0.9761124849 - 0.9587244987 0.9561265111 0.9764053822 - 0.9591794014 0.9566057920 0.9766982794 - 0.9596344233 0.9570850730 0.9769905806 - 0.9600849748 0.9575614929 0.9772747159 - 0.9605342150 0.9580358863 0.9775587916 - 0.9609832764 0.9585102797 0.9778429270 - 0.9614325166 0.9589846134 0.9781271219 - 0.9618816972 0.9594590068 0.9784111977 - 0.9623308778 0.9599334002 0.9786952734 - 0.9627801180 0.9604076743 0.9789794087 - 0.9632292986 0.9608821273 0.9792611003 - 0.9636726975 0.9613522291 0.9795362949 - 0.9641159773 0.9618213773 0.9798114896 - 0.9645593166 0.9622905850 0.9800866246 - 0.9650024772 0.9627596736 0.9803618193 - 0.9654458165 0.9632288814 0.9806370139 - 0.9658889771 0.9636980891 0.9809120893 - 0.9663323164 0.9641671777 0.9811872840 - 0.9667748213 0.9646363854 0.9814590812 - 0.9672120214 0.9651002288 0.9817258716 - 0.9676492214 0.9655638933 0.9819926023 - 0.9680864215 0.9660276175 0.9822592735 - 0.9685236216 0.9664912820 0.9825260043 - 0.9689608216 0.9669550061 0.9827927947 - 0.9693980217 0.9674186110 0.9830595255 - 0.9698352218 0.9678822756 0.9833261967 - 0.9702709913 0.9683455825 0.9835889935 - 0.9707019925 0.9688035250 0.9838470817 - 0.9711331129 0.9692615271 0.9841052890 - 0.9715641141 0.9697194099 0.9843633771 - 0.9719951153 0.9701772928 0.9846215248 - 0.9724261165 0.9706352949 0.9848796725 - 0.9728571177 0.9710931778 0.9851378202 - 0.9732881188 0.9715511799 0.9853959084 - 0.9737175703 0.9720084071 0.9856500030 - 0.9741421938 0.9724602103 0.9858992100 - 0.9745668173 0.9729120135 0.9861484170 - 0.9749915004 0.9733638167 0.9863975048 - 0.9754161239 0.9738156199 0.9866467118 - 0.9758406878 0.9742674232 0.9868959188 - 0.9762653112 0.9747192860 0.9871451259 - 0.9766899943 0.9751710892 0.9873942733 - 0.9771133065 0.9756222963 0.9876399040 - 0.9775314927 0.9760677218 0.9878798723 - 0.9779496789 0.9765130877 0.9881199002 - 0.9783678055 0.9769583941 0.9883599281 - 0.9787859917 0.9774038196 0.9885998964 - 0.9792041779 0.9778491855 0.9888399243 - 0.9796224236 0.9782946110 0.9890798926 - 0.9800404906 0.9787399769 0.9893199205 - 0.9804580808 0.9791854024 0.9895578027 - 0.9808694720 0.9796236753 0.9897894263 - 0.9812808037 0.9800621271 0.9900208712 - 0.9816921949 0.9805004001 0.9902524948 - 0.9821035862 0.9809387922 0.9904841185 - 0.9825149179 0.9813771248 0.9907156825 - 0.9829263091 0.9818153977 0.9909471869 - 0.9833377004 0.9822537899 0.9911788106 - 0.9837489724 0.9826921225 0.9914098978 - 0.9841541052 0.9831243157 0.9916322827 - 0.9845585823 0.9835553169 0.9918547273 - 0.9849629998 0.9839861989 0.9920771718 - 0.9853674769 0.9844172001 0.9922996163 - 0.9857718945 0.9848480821 0.9925220013 - 0.9861763716 0.9852790833 0.9927443862 - 0.9865807891 0.9857100248 0.9929667711 - 0.9869853258 0.9861410260 0.9931892157 - 0.9873846173 0.9865669012 0.9934051037 - 0.9877814054 0.9869893789 0.9936192036 - 0.9881781936 0.9874119759 0.9938333035 - 0.9885749817 0.9878345132 0.9940474033 - 0.9889718294 0.9882571101 0.9942615032 - 0.9893686175 0.9886797071 0.9944754839 - 0.9897654057 0.9891021848 0.9946895838 - 0.9901620746 0.9895247817 0.9949036837 - 0.9905560017 0.9899445772 0.9951136112 - 0.9909446836 0.9903581738 0.9953181148 - 0.9913333058 0.9907717705 0.9955226779 - 0.9917219281 0.9911854267 0.9957271814 - 0.9921104908 0.9915990233 0.9959316850 - 0.9924991727 0.9920125008 0.9961361885 - 0.9928877950 0.9924260974 0.9963408113 - 0.9932764173 0.9928396940 0.9965453148 - 0.9936649799 0.9932532907 0.9967495203 - 0.9940438867 0.9936568141 0.9969443083 - 0.9944224954 0.9940596223 0.9971390963 - 0.9948012233 0.9944624901 0.9973340034 - 0.9951797724 0.9948652983 0.9975287914 - 0.9955583811 0.9952681065 0.9977235794 - 0.9959369898 0.9956709146 0.9979184270 - 0.9963155985 0.9960737824 0.9981132150 - 0.9966943264 0.9964765906 0.9983080029 - 0.9970666766 0.9968733788 0.9984989166 - 0.9974333048 0.9972642064 0.9986864924 - 0.9977999926 0.9976549745 0.9988741875 - 0.9981666803 0.9980459213 0.9990618229 - 0.9985333085 0.9984366894 0.9992493987 - 0.9988999963 0.9988275170 0.9994370937 - 0.9992666841 0.9992182851 0.9996247292 - 0.9996333122 0.9996091723 0.9998124242 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0002336452 0.0003066142 0.0001923408 + 0.0004746285 0.0006284252 0.0003908366 + 0.0007228691 0.0009574692 0.0005960183 + 0.0009798091 0.0012926980 0.0008084249 + 0.0012458910 0.0016348250 0.0010286780 + 0.0015215250 0.0019854531 0.0012572890 + 0.0018074480 0.0023458831 0.0014948760 + 0.0021044740 0.0027179900 0.0017418840 + 0.0024123720 0.0031044651 0.0019987100 + 0.0027316429 0.0035080169 0.0022662929 + 0.0030628759 0.0039312281 0.0025448580 + 0.0034068460 0.0043767700 0.0028348130 + 0.0037631821 0.0048452052 0.0031369999 + 0.0041319309 0.0053377128 0.0034519250 + 0.0045133741 0.0058530779 0.0037802891 + 0.0049079140 0.0063888212 0.0041218149 + 0.0053149262 0.0069422242 0.0044763028 + 0.0057351599 0.0075108381 0.0048436122 + 0.0061681862 0.0080922469 0.0052233832 + 0.0066139381 0.0086847479 0.0056158421 + 0.0070730280 0.0092877084 0.0060206670 + 0.0075457580 0.0099003958 0.0064372532 + 0.0080318721 0.0105231302 0.0068652900 + 0.0085307229 0.0111563997 0.0073052440 + 0.0090426058 0.0118005900 0.0077564488 + 0.0095678763 0.0124572096 0.0082195234 + 0.0101060299 0.0131261898 0.0086938078 + 0.0106565999 0.0138081899 0.0091797709 + 0.0112196403 0.0145026296 0.0096775433 + 0.0117950998 0.0152096897 0.0101859104 + 0.0123830596 0.0159296691 0.0107057104 + 0.0129827997 0.0166616905 0.0112374704 + 0.0135938600 0.0174061507 0.0117801595 + 0.0142163001 0.0181632899 0.0123338196 + 0.0148505196 0.0189326294 0.0128991101 + 0.0154959997 0.0197145808 0.0134759704 + 0.0161525905 0.0205079298 0.0140651399 + 0.0168197099 0.0213135108 0.0146654900 + 0.0174975600 0.0221296605 0.0152769703 + 0.0181858800 0.0229572095 0.0159003101 + 0.0188853294 0.0237958394 0.0165352002 + 0.0195957199 0.0246469099 0.0171822291 + 0.0203166995 0.0255084708 0.0178419799 + 0.0210479405 0.0263805594 0.0185129102 + 0.0217893906 0.0272626597 0.0191952903 + 0.0225410797 0.0281556007 0.0198900104 + 0.0233031102 0.0290594995 0.0205974299 + 0.0240752809 0.0299743302 0.0213165600 + 0.0248574503 0.0308989193 0.0220477208 + 0.0256489795 0.0318339504 0.0227914508 + 0.0264505297 0.0327800512 0.0235488601 + 0.0272613708 0.0337377489 0.0243192408 + 0.0280819293 0.0347075686 0.0251018200 + 0.0289116092 0.0356889293 0.0258964691 + 0.0297506806 0.0366821997 0.0267038904 + 0.0305987895 0.0376874991 0.0275248494 + 0.0314561911 0.0387053117 0.0283588003 + 0.0323230512 0.0397350118 0.0292057395 + 0.0331984907 0.0407753289 0.0300664101 + 0.0340828784 0.0418274291 0.0309393499 + 0.0349764302 0.0428918488 0.0318249315 + 0.0358787291 0.0439687595 0.0327227190 + 0.0367900617 0.0450573303 0.0336325206 + 0.0377106704 0.0461591706 0.0345553607 + 0.0386387706 0.0472715795 0.0354900099 + 0.0395760499 0.0483947694 0.0364381485 + 0.0405212790 0.0495291613 0.0373984985 + 0.0414749682 0.0506746694 0.0383732207 + 0.0424371511 0.0518323407 0.0393615104 + 0.0434069186 0.0530007891 0.0403636508 + 0.0443847999 0.0541807003 0.0413801111 + 0.0453707203 0.0553707816 0.0424091816 + 0.0463633984 0.0565719493 0.0434532799 + 0.0473654494 0.0577826984 0.0445101783 + 0.0483737215 0.0590048917 0.0455798395 + 0.0493897311 0.0602366105 0.0466651618 + 0.0504129082 0.0614794008 0.0477640294 + 0.0514423698 0.0627331585 0.0488768108 + 0.0524802990 0.0639974698 0.0500062406 + 0.0535254516 0.0652740896 0.0511502102 + 0.0545772910 0.0665622428 0.0523082986 + 0.0556361787 0.0678606406 0.0534805804 + 0.0567024983 0.0691716820 0.0546686798 + 0.0577750802 0.0704934299 0.0558703691 + 0.0588535815 0.0718252510 0.0570854694 + 0.0599400401 0.0731683001 0.0583138093 + 0.0610331818 0.0745219514 0.0595557392 + 0.0621325709 0.0758863464 0.0608114190 + 0.0632396191 0.0772613883 0.0620787703 + 0.0643534884 0.0786459073 0.0633573383 + 0.0654745772 0.0800427720 0.0646468624 + 0.0666032135 0.0814517811 0.0659470782 + 0.0677386299 0.0828704983 0.0672577098 + 0.0688809007 0.0843003020 0.0685787722 + 0.0700303987 0.0857396796 0.0699099898 + 0.0711864233 0.0871883109 0.0712488964 + 0.0723488331 0.0886481106 0.0725946799 + 0.0735196322 0.0901157334 0.0739471167 + 0.0746975020 0.0915923566 0.0753060281 + 0.0758822709 0.0930788666 0.0766710714 + 0.0770741701 0.0945737883 0.0780422911 + 0.0782730505 0.0960773528 0.0794199184 + 0.0794789493 0.0975896269 0.0808036402 + 0.0806913823 0.0991091728 0.0821929798 + 0.0819104165 0.1006359980 0.0835875571 + 0.0831365734 0.1021701992 0.0849873573 + 0.0843702182 0.1037115976 0.0863918066 + 0.0856113136 0.1052597985 0.0878000185 + 0.0868599117 0.1068146974 0.0892115831 + 0.0881158933 0.1083773971 0.0906265825 + 0.0893787891 0.1099461019 0.0920447111 + 0.0906486213 0.1115209982 0.0934653208 + 0.0919253081 0.1131021008 0.0948884189 + 0.0932083800 0.1146880984 0.0963144898 + 0.0944980532 0.1162792966 0.0977434963 + 0.0957952365 0.1178762987 0.0991748795 + 0.0970996320 0.1194792986 0.1006093025 + 0.0984100029 0.1210874990 0.1020475999 + 0.0997272208 0.1226997972 0.1034903973 + 0.1010518000 0.1243164986 0.1049363986 + 0.1023814976 0.1259384006 0.1063854992 + 0.1037181988 0.1275652051 0.1078381985 + 0.1050620005 0.1291960031 0.1092941016 + 0.1064110026 0.1308314055 0.1107527018 + 0.1077655032 0.1324710995 0.1122142971 + 0.1091263965 0.1341142952 0.1136794984 + 0.1104935035 0.1357623935 0.1151480004 + 0.1118659005 0.1374143958 0.1166194007 + 0.1132436991 0.1390711069 0.1180943996 + 0.1146290004 0.1407306939 0.1195726022 + 0.1160207018 0.1423933059 0.1210533977 + 0.1174174026 0.1440594047 0.1225365996 + 0.1188201010 0.1457290053 0.1240229011 + 0.1202287003 0.1474035978 0.1255117059 + 0.1216425970 0.1490814984 0.1270032972 + 0.1230617985 0.1507631987 0.1284980029 + 0.1244878024 0.1524486989 0.1299961060 + 0.1259191930 0.1541378051 0.1314983070 + 0.1273557991 0.1558293998 0.1330049038 + 0.1287976056 0.1575267017 0.1345154941 + 0.1302458048 0.1592278928 0.1360293031 + 0.1317003965 0.1609317958 0.1375454962 + 0.1331606060 0.1626382023 0.1390648037 + 0.1346260011 0.1643474996 0.1405864060 + 0.1360958070 0.1660612971 0.1421104968 + 0.1375721991 0.1677768975 0.1436379999 + 0.1390544027 0.1694948971 0.1451703012 + 0.1405413002 0.1712158024 0.1467055976 + 0.1420325041 0.1729404032 0.1482436061 + 0.1435298026 0.1746671945 0.1497846991 + 0.1450331062 0.1763962060 0.1513286978 + 0.1465408057 0.1781273931 0.1528747976 + 0.1480524987 0.1798599064 0.1544229984 + 0.1495707035 0.1815944016 0.1559745967 + 0.1510937959 0.1833305955 0.1575296074 + 0.1526214033 0.1850683987 0.1590874940 + 0.1541541964 0.1868094057 0.1606484950 + 0.1556929052 0.1885517985 0.1622128040 + 0.1572359949 0.1902959049 0.1637787968 + 0.1587835997 0.1920423061 0.1653465927 + 0.1603381932 0.1937911958 0.1669193059 + 0.1618963927 0.1955417991 0.1684942991 + 0.1634581983 0.1972939074 0.1700717062 + 0.1650260985 0.1990482062 0.1716526002 + 0.1665998995 0.2008040994 0.1732358932 + 0.1681773067 0.2025613040 0.1748213023 + 0.1697590947 0.2043205053 0.1764107943 + 0.1713462025 0.2060810030 0.1780031025 + 0.1729367971 0.2078423053 0.1795973927 + 0.1745319068 0.2096049935 0.1811943948 + 0.1761319935 0.2113689035 0.1827940941 + 0.1777352989 0.2131334990 0.1843959987 + 0.1793437004 0.2148993015 0.1860031039 + 0.1809576005 0.2166654021 0.1876126975 + 0.1825744063 0.2184319943 0.1892254949 + 0.1841952056 0.2201997042 0.1908413023 + 0.1858201027 0.2219685018 0.1924591959 + 0.1874478012 0.2237378955 0.1940806955 + 0.1890821010 0.2255074978 0.1957052946 + 0.1907189935 0.2272779942 0.1973316073 + 0.1923578978 0.2290489972 0.1989613026 + 0.1940028965 0.2308211029 0.2005929053 + 0.1956506073 0.2325938940 0.2022265941 + 0.1973026991 0.2343678027 0.2038643956 + 0.1989585012 0.2361426055 0.2055034935 + 0.2006161064 0.2379177958 0.2071447968 + 0.2022790015 0.2396938056 0.2087884992 + 0.2039445937 0.2414696068 0.2104344964 + 0.2056134939 0.2432450950 0.2120838016 + 0.2072867006 0.2450218946 0.2137342989 + 0.2089617997 0.2467985004 0.2153877020 + 0.2106418014 0.2485740930 0.2170425951 + 0.2123246044 0.2503491044 0.2186993957 + 0.2140112072 0.2521235943 0.2203588933 + 0.2157005966 0.2538985014 0.2220194936 + 0.2173911929 0.2556734085 0.2236841023 + 0.2190874964 0.2574481070 0.2253499031 + 0.2207852006 0.2592217922 0.2270195931 + 0.2224867046 0.2609946132 0.2286913991 + 0.2241905034 0.2627679110 0.2303657979 + 0.2258961946 0.2645412982 0.2320432961 + 0.2276054025 0.2663162053 0.2337222993 + 0.2293156981 0.2680906057 0.2354062945 + 0.2310317010 0.2698636949 0.2370913029 + 0.2327484041 0.2716363966 0.2387810051 + 0.2344674021 0.2734088004 0.2404720038 + 0.2361883968 0.2751807868 0.2421665043 + 0.2379122972 0.2769527137 0.2438628972 + 0.2396381944 0.2787235081 0.2455617040 + 0.2413652986 0.2804942131 0.2472631931 + 0.2430955023 0.2822644114 0.2489665002 + 0.2448267043 0.2840346992 0.2506738007 + 0.2465613037 0.2858053148 0.2523826063 + 0.2482964993 0.2875756919 0.2540957928 + 0.2500359118 0.2893457115 0.2558099031 + 0.2517758012 0.2911148965 0.2575285137 + 0.2535183132 0.2928834856 0.2592475116 + 0.2552621961 0.2946512997 0.2609696984 + 0.2570098937 0.2964186966 0.2626922131 + 0.2587589920 0.2981835902 0.2644180059 + 0.2605102956 0.2999480069 0.2661443055 + 0.2622630894 0.3017111123 0.2678734958 + 0.2640174031 0.3034740984 0.2696034908 + 0.2657730877 0.3052338064 0.2713379860 + 0.2675299048 0.3069935143 0.2730731964 + 0.2692880034 0.3087523878 0.2748118937 + 0.2710470855 0.3105109930 0.2765515149 + 0.2728081942 0.3122670054 0.2782951891 + 0.2745707035 0.3140225112 0.2800396085 + 0.2763350904 0.3157748878 0.2817870975 + 0.2781007886 0.3175269067 0.2835353911 + 0.2798672915 0.3192762136 0.2852882147 + 0.2816343009 0.3210250139 0.2870416045 + 0.2834017873 0.3227711022 0.2887994945 + 0.2851698101 0.3245170116 0.2905577123 + 0.2869389057 0.3262611926 0.2923206985 + 0.2887091935 0.3280051947 0.2940837145 + 0.2904804051 0.3297472894 0.2958520949 + 0.2922525108 0.3314892948 0.2976205945 + 0.2940253913 0.3332268000 0.2993935049 + 0.2957994938 0.3349643052 0.3011670113 + 0.2975736856 0.3366967142 0.3029440939 + 0.2993479967 0.3384287953 0.3047224879 + 0.3011221886 0.3401573896 0.3065038025 + 0.3028964102 0.3418853879 0.3082869947 + 0.3046706915 0.3436118066 0.3100728095 + 0.3064466119 0.3453375995 0.3118613958 + 0.3082225025 0.3470619917 0.3136520088 + 0.3099983037 0.3487856984 0.3154470026 + 0.3117741942 0.3505071998 0.3172427118 + 0.3135501146 0.3522264063 0.3190433085 + 0.3153258860 0.3539440036 0.3208439052 + 0.3171015978 0.3556582034 0.3226490021 + 0.3188770115 0.3573715091 0.3244544864 + 0.3206518888 0.3590804040 0.3262634873 + 0.3224261105 0.3607892990 0.3280740976 + 0.3241997957 0.3624913990 0.3298870027 + 0.3259718120 0.3641934097 0.3317027092 + 0.3277438879 0.3658939898 0.3335191905 + 0.3295131028 0.3675940931 0.3353388011 + 0.3312822878 0.3692927063 0.3371585011 + 0.3330504000 0.3709900975 0.3389824033 + 0.3348180056 0.3726865053 0.3408068120 + 0.3365843892 0.3743805885 0.3426336944 + 0.3383488953 0.3760747015 0.3444621861 + 0.3401131928 0.3777655959 0.3462921977 + 0.3418753147 0.3794564903 0.3481256962 + 0.3436372876 0.3811435997 0.3499591947 + 0.3453963101 0.3828293979 0.3517968059 + 0.3471544981 0.3845128119 0.3536345959 + 0.3489103913 0.3861933053 0.3554756045 + 0.3506639004 0.3878732026 0.3573186994 + 0.3524166942 0.3895485103 0.3591623902 + 0.3541651070 0.3912237883 0.3610084951 + 0.3559136093 0.3928934932 0.3628545105 + 0.3576560915 0.3945614994 0.3647046089 + 0.3593969047 0.3962261975 0.3665556014 + 0.3611347079 0.3978857994 0.3684079945 + 0.3628677130 0.3995453119 0.3702622950 + 0.3646006882 0.4011983871 0.3721165955 + 0.3663268089 0.4028514922 0.3739753067 + 0.3680528998 0.4045008123 0.3758341074 + 0.3697755039 0.4061478078 0.3776954114 + 0.3714954853 0.4077934027 0.3795588911 + 0.3732145131 0.4094338119 0.3814226985 + 0.3749260008 0.4110741019 0.3832913041 + 0.3766376078 0.4127109051 0.3851599097 + 0.3783437908 0.4143466055 0.3870306909 + 0.3800471127 0.4159803092 0.3889032900 + 0.3817487061 0.4176093936 0.3907761872 + 0.3834415078 0.4192385077 0.3926539123 + 0.3851343989 0.4208612144 0.3945316970 + 0.3868206143 0.4224824905 0.3964118063 + 0.3885033131 0.4241017103 0.3982942104 + 0.3901846111 0.4257161915 0.4001766145 + 0.3918552995 0.4273307025 0.4020617008 + 0.3935258985 0.4289397895 0.4039469957 + 0.3951900005 0.4305475056 0.4058333039 + 0.3968490958 0.4321533144 0.4077214003 + 0.3985081911 0.4337531030 0.4096095860 + 0.4001541138 0.4353528917 0.4114991128 + 0.4017998874 0.4369482994 0.4133890867 + 0.4034416974 0.4385415018 0.4152793884 + 0.4050774872 0.4401341975 0.4171707928 + 0.4067131877 0.4417211115 0.4190622866 + 0.4083409905 0.4433079958 0.4209538102 + 0.4099667072 0.4448912144 0.4228455126 + 0.4115909040 0.4464702904 0.4247371852 + 0.4132077098 0.4480494857 0.4266293049 + 0.4148243964 0.4496214092 0.4285216033 + 0.4164358974 0.4511919916 0.4304136932 + 0.4180422127 0.4527612031 0.4323050082 + 0.4196484983 0.4543245137 0.4341962934 + 0.4212470949 0.4558877945 0.4360859990 + 0.4228439927 0.4574449062 0.4379737973 + 0.4244396091 0.4589962959 0.4398615062 + 0.4260253906 0.4605476856 0.4417462945 + 0.4276112020 0.4620890021 0.4436298907 + 0.4291928113 0.4636281133 0.4455133975 + 0.4307680130 0.4651657939 0.4473905861 + 0.4323433042 0.4666922987 0.4492675066 + 0.4339112937 0.4682187140 0.4511435032 + 0.4354755878 0.4697411060 0.4530152977 + 0.4370399117 0.4712564051 0.4548870921 + 0.4385950863 0.4727717042 0.4567565024 + 0.4401490986 0.4742799997 0.4586217105 + 0.4417021871 0.4757837057 0.4604870081 + 0.4432457983 0.4772874117 0.4623486996 + 0.4447894096 0.4787816107 0.4642075896 + 0.4463303983 0.4802734852 0.4660665095 + 0.4478640854 0.4817655087 0.4679197073 + 0.4493978918 0.4832465053 0.4697704017 + 0.4509268105 0.4847275019 0.4716210961 + 0.4524492919 0.4862065911 0.4734647870 + 0.4539718032 0.4876767099 0.4753069878 + 0.4554879069 0.4891467094 0.4771490991 + 0.4569990933 0.4906134903 0.4789814949 + 0.4585103989 0.4920732081 0.4808132052 + 0.4600146115 0.4935329854 0.4826447070 + 0.4615156949 0.4949879050 0.4844656885 + 0.4630168080 0.4964365065 0.4862866998 + 0.4645102024 0.4978851080 0.4881069064 + 0.4660013914 0.4993262887 0.4899150133 + 0.4674926102 0.5007606745 0.4917230010 + 0.4689762890 0.5021951795 0.4935297966 + 0.4704585075 0.5036209226 0.4953246117 + 0.4719407856 0.5050408244 0.4971193969 + 0.4734140933 0.5064607263 0.4989126027 + 0.4748862982 0.5078724027 0.5006924868 + 0.4763585031 0.5092796087 0.5024725199 + 0.4778222144 0.5106868744 0.5042511225 + 0.4792849123 0.5120872855 0.5060155988 + 0.4807476103 0.5134844184 0.5077801943 + 0.4822027981 0.5148816109 0.5095440149 + 0.4836570024 0.5162711740 0.5112923980 + 0.4851112962 0.5176569819 0.5130407214 + 0.4865578115 0.5190427899 0.5147891045 + 0.4880031049 0.5204212070 0.5165216923 + 0.4894483984 0.5217952728 0.5182538033 + 0.4908877909 0.5231695175 0.5199859738 + 0.4923256934 0.5245352983 0.5217025876 + 0.4937636852 0.5258951783 0.5234171748 + 0.4951958060 0.5272551179 0.5251318216 + 0.4966256022 0.5286077261 0.5268319845 + 0.4980554879 0.5299531221 0.5285276771 + 0.4994794130 0.5312983990 0.5302234888 + 0.5008996129 0.5326380134 0.5319082141 + 0.5023198128 0.5339689255 0.5335860848 + 0.5037351847 0.5352998972 0.5352638960 + 0.5051453114 0.5366278887 0.5369331837 + 0.5065554976 0.5379484296 0.5385919213 + 0.5079630017 0.5392689109 0.5402505994 + 0.5093650222 0.5405880809 0.5419046879 + 0.5107669234 0.5418984890 0.5435451269 + 0.5121678710 0.5432088971 0.5451855063 + 0.5135623813 0.5445193052 0.5468252897 + 0.5149568915 0.5458185077 0.5484473109 + 0.5163514018 0.5471168756 0.5500692725 + 0.5177388787 0.5484153032 0.5516912937 + 0.5191257000 0.5497050881 0.5532999039 + 0.5205125213 0.5509917140 0.5549045801 + 0.5218951702 0.5522782803 0.5565093756 + 0.5232759118 0.5535587072 0.5581051707 + 0.5246565938 0.5548328757 0.5596920252 + 0.5260347128 0.5561071038 0.5612788200 + 0.5274087787 0.5573784113 0.5628618002 + 0.5287830234 0.5586404800 0.5644289255 + 0.5301563144 0.5599024892 0.5659958720 + 0.5315235257 0.5611644983 0.5675628781 + 0.5328906178 0.5624132752 0.5691115856 + 0.5342578292 0.5636613965 0.5706571937 + 0.5356180072 0.5649095178 0.5722029209 + 0.5369765162 0.5661481023 0.5737379193 + 0.5383349061 0.5673810840 0.5752621889 + 0.5396882296 0.5686141849 0.5767865181 + 0.5410351157 0.5698431134 0.5783087015 + 0.5423818827 0.5710616112 0.5798140168 + 0.5437278748 0.5722801089 0.5813193917 + 0.5450633168 0.5734986067 0.5828247070 + 0.5463985801 0.5747051239 0.5843166113 + 0.5477339029 0.5759106278 0.5858026743 + 0.5490639210 0.5771160126 0.5872887969 + 0.5503911972 0.5783153772 0.5887703896 + 0.5517184138 0.5795091987 0.5902382731 + 0.5530437827 0.5807030201 0.5917062759 + 0.5543630123 0.5818957090 0.5931742191 + 0.5556821823 0.5830773115 0.5946282744 + 0.5570014119 0.5842589140 0.5960785151 + 0.5583145022 0.5854403973 0.5975285769 + 0.5596256852 0.5866149068 0.5989735723 + 0.5609369278 0.5877853036 0.6004049778 + 0.5622456074 0.5889558196 0.6018365026 + 0.5635476112 0.5901247263 0.6032679081 + 0.5648496151 0.5912854075 0.6046854854 + 0.5661516786 0.5924460888 0.6060982943 + 0.5674464703 0.5936068296 0.6075112224 + 0.5687389970 0.5947598219 0.6089211702 + 0.5700315237 0.5959085226 0.6103187799 + 0.5713216066 0.5970572233 0.6117163897 + 0.5726023912 0.5982046723 0.6131139994 + 0.5738831758 0.5993419886 0.6145017743 + 0.5751640201 0.6004794240 0.6158828735 + 0.5764371753 0.6016166806 0.6172639728 + 0.5777059793 0.6027488708 0.6186450720 + 0.5789747834 0.6038765907 0.6200103164 + 0.5802434087 0.6050041914 0.6213750243 + 0.5815005898 0.6061319113 0.6227397919 + 0.5827577710 0.6072487235 0.6240993142 + 0.5840150714 0.6083645821 0.6254476905 + 0.5852677822 0.6094806194 0.6267961860 + 0.5865132213 0.6105933189 0.6281446218 + 0.5877586007 0.6116985083 0.6294825077 + 0.5890039802 0.6128035784 0.6308146119 + 0.5902428031 0.6139087081 0.6321465969 + 0.5914788842 0.6150081754 0.6334787011 + 0.5927150249 0.6161044836 0.6347957253 + 0.5939509869 0.6172006726 0.6361116171 + 0.5951772928 0.6182969809 0.6374276280 + 0.5964037180 0.6193832159 0.6387404799 + 0.5976300240 0.6204687953 0.6400408745 + 0.5988535881 0.6215543151 0.6413413882 + 0.6000704169 0.6226378083 0.6426417828 + 0.6012871861 0.6237139106 0.6439359188 + 0.6025038958 0.6247900128 0.6452212930 + 0.6037154794 0.6258659959 0.6465067267 + 0.6049222946 0.6269382238 0.6477921009 + 0.6061292291 0.6280059218 0.6490674019 + 0.6073359847 0.6290736794 0.6503365040 + 0.6085358262 0.6301413774 0.6516057253 + 0.6097332239 0.6311997175 0.6528748274 + 0.6109305024 0.6322535872 0.6541312933 + 0.6121277809 0.6333073974 0.6553844810 + 0.6133171916 0.6343613267 0.6566376090 + 0.6145057082 0.6354042292 0.6578906775 + 0.6156942844 0.6364452839 0.6591296792 + 0.6168823242 0.6374865174 0.6603674293 + 0.6180605888 0.6385275126 0.6616051793 + 0.6192389727 0.6395580769 0.6628425717 + 0.6204174161 0.6405887008 0.6640645266 + 0.6215937734 0.6416193247 0.6652863026 + 0.6227611899 0.6426482201 0.6665081978 + 0.6239284873 0.6436666846 0.6677287817 + 0.6250959039 0.6446852088 0.6689363122 + 0.6262605190 0.6457036138 0.6701437831 + 0.6274175048 0.6467195749 0.6713513136 + 0.6285744905 0.6477273107 0.6725571752 + 0.6297314763 0.6487351060 0.6737517118 + 0.6308856010 0.6497427821 0.6749461889 + 0.6320332289 0.6507480741 0.6761407256 + 0.6331807971 0.6517474055 0.6773335934 + 0.6343284249 0.6527466774 0.6785153151 + 0.6354721785 0.6537461281 0.6796969175 + 0.6366078854 0.6547424197 0.6808785796 + 0.6377437115 0.6557319760 0.6820589900 + 0.6388794184 0.6567215919 0.6832273006 + 0.6400116086 0.6577112079 0.6843956113 + 0.6411356926 0.6586982012 0.6855639219 + 0.6422597766 0.6596792936 0.6867318153 + 0.6433839202 0.6606603861 0.6878877878 + 0.6445053220 0.6616414785 0.6890437007 + 0.6456189752 0.6626203060 0.6901996732 + 0.6467326283 0.6635925174 0.6913555861 + 0.6478462815 0.6645647883 0.6925013065 + 0.6489577889 0.6655369997 0.6936463118 + 0.6500586867 0.6665074825 0.6947911978 + 0.6511597037 0.6674702168 0.6959362030 + 0.6522606015 0.6684328914 0.6970707774 + 0.6533609033 0.6693956256 0.6982029080 + 0.6544495821 0.6703575850 0.6993349791 + 0.6555382013 0.6713106036 0.7004671097 + 0.6566268206 0.6722635031 0.7015910149 + 0.6577154994 0.6732165217 0.7027103901 + 0.6587942839 0.6741694212 0.7038298249 + 0.6598721147 0.6751123071 0.7049492002 + 0.6609498858 0.6760544181 0.7060629129 + 0.6620277166 0.6769965291 0.7071694732 + 0.6630986929 0.6779385805 0.7082760930 + 0.6641669869 0.6788738966 0.7093827128 + 0.6652354002 0.6798070073 0.7104861736 + 0.6663036942 0.6807401180 0.7115777731 + 0.6673663259 0.6816732287 0.7126693726 + 0.6684231162 0.6826019287 0.7137609124 + 0.6694800258 0.6835269928 0.7148525119 + 0.6705368161 0.6844519973 0.7159308791 + 0.6715909839 0.6853770018 0.7170084119 + 0.6726369262 0.6862993240 0.7180858254 + 0.6736826897 0.6872149706 0.7191632986 + 0.6747285724 0.6881306171 0.7202311158 + 0.6757745147 0.6890463233 0.7212930918 + 0.6768103838 0.6899619102 0.7223551869 + 0.6778457165 0.6908699870 0.7234172225 + 0.6788811088 0.6917781234 0.7244750261 + 0.6799163818 0.6926862001 0.7255222797 + 0.6809443235 0.6935942769 0.7265697122 + 0.6819676161 0.6944952011 0.7276170254 + 0.6829909086 0.6953927279 0.7286642790 + 0.6840142012 0.6962900758 0.7296993136 + 0.6850343943 0.6971876025 0.7307329774 + 0.6860457063 0.6980822086 0.7317667007 + 0.6870568991 0.6989707947 0.7328004241 + 0.6880680919 0.6998593807 0.7338271141 + 0.6890792847 0.7007479072 0.7348465919 + 0.6900807023 0.7016364932 0.7358661294 + 0.6910803914 0.7025167942 0.7368856072 + 0.6920800805 0.7033964992 0.7379044890 + 0.6930798888 0.7042762041 0.7389112711 + 0.6940743923 0.7051559091 0.7399181128 + 0.6950619221 0.7060313225 0.7409247756 + 0.6960492730 0.7069025040 0.7419316173 + 0.6970368028 0.7077736855 0.7429320216 + 0.6980242133 0.7086449862 0.7439273000 + 0.6990013719 0.7095158100 0.7449225783 + 0.6999779940 0.7103791833 0.7459179163 + 0.7009546757 0.7112424970 0.7469127774 + 0.7019312978 0.7121058106 0.7478963733 + 0.7029024959 0.7129691243 0.7488800287 + 0.7038668990 0.7138271928 0.7498636246 + 0.7048313022 0.7146809101 0.7508472204 + 0.7057957053 0.7155346870 0.7518255115 + 0.7067601085 0.7163884044 0.7527979016 + 0.7077146769 0.7172421217 0.7537702918 + 0.7086681128 0.7180885077 0.7547428012 + 0.7096214890 0.7189350128 0.7557151914 + 0.7105749846 0.7197813988 0.7566773891 + 0.7115257978 0.7206279039 0.7576380968 + 0.7124705911 0.7214711905 0.7585988045 + 0.7134153843 0.7223104835 0.7595595121 + 0.7143602967 0.7231497169 0.7605171204 + 0.7153050900 0.7239890099 0.7614648938 + 0.7162432075 0.7248281837 0.7624126077 + 0.7171782255 0.7256616950 0.7633603811 + 0.7181131840 0.7264938951 0.7643082142 + 0.7190482020 0.7273260951 0.7652475238 + 0.7199829817 0.7281581759 0.7661802173 + 0.7209094167 0.7289890051 0.7671129107 + 0.7218356729 0.7298128009 0.7680456042 + 0.7227619886 0.7306365967 0.7689781785 + 0.7236883044 0.7314602733 0.7698999047 + 0.7246115804 0.7322841287 0.7708194852 + 0.7255285978 0.7331035137 0.7717391849 + 0.7264454961 0.7339180708 0.7726588249 + 0.7273623943 0.7347326875 0.7735773921 + 0.7282792926 0.7355474234 0.7744857073 + 0.7291893959 0.7363619804 0.7753940225 + 0.7300944924 0.7371708155 0.7763022780 + 0.7309995890 0.7379775047 0.7772104740 + 0.7319046855 0.7387843132 0.7781155109 + 0.7328097820 0.7395910025 0.7790126801 + 0.7337058783 0.7403978109 0.7799099088 + 0.7345998287 0.7411972284 0.7808070779 + 0.7354937196 0.7419964075 0.7817043066 + 0.7363876104 0.7427955866 0.7825965285 + 0.7372813821 0.7435948253 0.7834830880 + 0.7381641269 0.7443925738 0.7843697071 + 0.7390465736 0.7451831102 0.7852563262 + 0.7399291992 0.7459737062 0.7861428261 + 0.7408117056 0.7467643023 0.7870228291 + 0.7416930199 0.7475548983 0.7878980041 + 0.7425662875 0.7483425736 0.7887732983 + 0.7434394956 0.7491239905 0.7896485925 + 0.7443128228 0.7499054074 0.7905238867 + 0.7451859713 0.7506868243 0.7913916707 + 0.7460573912 0.7514681220 0.7922559977 + 0.7469223738 0.7522463202 0.7931203246 + 0.7477872968 0.7530199885 0.7939847112 + 0.7486522794 0.7537937164 0.7948489785 + 0.7495173216 0.7545673847 0.7957043052 + 0.7503795028 0.7553411126 0.7965561152 + 0.7512350082 0.7561119795 0.7974079251 + 0.7520905733 0.7568801045 0.7982596755 + 0.7529460788 0.7576481104 0.7991114855 + 0.7538017035 0.7584161758 0.7999551296 + 0.7546544075 0.7591843009 0.8007956147 + 0.7555009127 0.7599483728 0.8016362190 + 0.7563472986 0.7607088089 0.8024767041 + 0.7571938038 0.7614691854 0.8033173084 + 0.7580403090 0.7622296810 0.8041502237 + 0.7588834763 0.7629901171 0.8049796820 + 0.7597190142 0.7637473941 0.8058090806 + 0.7605543733 0.7645015717 0.8066385984 + 0.7613899112 0.7652558088 0.8074679971 + 0.7622252703 0.7660099864 0.8082910180 + 0.7630584836 0.7667642236 0.8091096282 + 0.7638840079 0.7675157189 0.8099281788 + 0.7647094131 0.7682638764 0.8107469082 + 0.7655348778 0.7690120935 0.8115655184 + 0.7663604021 0.7697603106 0.8123788834 + 0.7671846151 0.7705085278 0.8131868243 + 0.7680001259 0.7712538242 0.8139945865 + 0.7688155174 0.7719941139 0.8148024082 + 0.7696310282 0.7727344036 0.8156102896 + 0.7704464197 0.7734745741 0.8164144754 + 0.7712618113 0.7742149234 0.8172109723 + 0.7720686793 0.7749537230 0.8180075288 + 0.7728753090 0.7756870985 0.8188040257 + 0.7736818790 0.7764205933 0.8196005225 + 0.7744885087 0.7771540284 0.8203955889 + 0.7752950788 0.7778875232 0.8211801052 + 0.7760956287 0.7786207199 0.8219646215 + 0.7768943906 0.7793480754 0.8227490783 + 0.7776932120 0.7800753713 0.8235335946 + 0.7784920931 0.7808027864 0.8243181109 + 0.7792909145 0.7815300822 0.8250932097 + 0.7800844908 0.7822574973 0.8258666992 + 0.7808738947 0.7829779983 0.8266401887 + 0.7816631794 0.7836970091 0.8274136782 + 0.7824525833 0.7844160795 0.8281872272 + 0.7832419872 0.7851350904 0.8289542198 + 0.7840290070 0.7858542204 0.8297163248 + 0.7848097086 0.7865694761 0.8304784298 + 0.7855904102 0.7872818112 0.8312404752 + 0.7863711119 0.7879942060 0.8320025802 + 0.7871518135 0.7887064815 0.8327620029 + 0.7879325151 0.7894188166 0.8335124850 + 0.7887042761 0.7901296020 0.8342630267 + 0.7894756794 0.7908356786 0.8350133896 + 0.7902470231 0.7915416956 0.8357638717 + 0.7910184264 0.7922477126 0.8365142941 + 0.7917898297 0.7929537892 0.8372542858 + 0.7925562859 0.7936598063 0.8379924893 + 0.7933195829 0.7943586707 0.8387306929 + 0.7940828800 0.7950568199 0.8394688964 + 0.7948461771 0.7957547903 0.8402070999 + 0.7956094146 0.7964528203 0.8409410715 + 0.7963712811 0.7971509099 0.8416694999 + 0.7971267104 0.7978456020 0.8423978090 + 0.7978820801 0.7985368967 0.8431261182 + 0.7986375093 0.7992283106 0.8438544273 + 0.7993928790 0.7999196053 0.8445827961 + 0.8001483083 0.8006108999 0.8453016281 + 0.8008971810 0.8013020158 0.8460199237 + 0.8016437292 0.8019875288 0.8467382789 + 0.8023902178 0.8026731014 0.8474565744 + 0.8031367064 0.8033586144 0.8481749296 + 0.8038831949 0.8040441275 0.8488876224 + 0.8046274781 0.8047297001 0.8495929837 + 0.8053646088 0.8054109216 0.8502982855 + 0.8061016798 0.8060886264 0.8510035872 + 0.8068388104 0.8067662716 0.8517088890 + 0.8075758815 0.8074440956 0.8524143100 + 0.8083130121 0.8081218004 0.8531094790 + 0.8090445995 0.8087993860 0.8538032770 + 0.8097738028 0.8094710708 0.8544970751 + 0.8105030060 0.8101428747 0.8551908731 + 0.8112320900 0.8108146191 0.8558846712 + 0.8119612932 0.8114864230 0.8565754890 + 0.8126897216 0.8121581078 0.8572577834 + 0.8134108782 0.8128275275 0.8579400778 + 0.8141322136 0.8134937882 0.8586223722 + 0.8148534298 0.8141601086 0.8593047261 + 0.8155747056 0.8148264885 0.8599870801 + 0.8162959218 0.8154928088 0.8606625199 + 0.8170127273 0.8161591887 0.8613334894 + 0.8177244067 0.8168199062 0.8620045781 + 0.8184360266 0.8174793124 0.8626756072 + 0.8191475868 0.8181387782 0.8633465767 + 0.8198593259 0.8187981844 0.8640176058 + 0.8205708861 0.8194577098 0.8646782041 + 0.8212757707 0.8201162815 0.8653377891 + 0.8219788074 0.8207699060 0.8659973145 + 0.8226817846 0.8214235902 0.8666567802 + 0.8233848214 0.8220772147 0.8673163056 + 0.8240877986 0.8227308989 0.8679741025 + 0.8247904181 0.8233845234 0.8686218858 + 0.8254855275 0.8240357041 0.8692697287 + 0.8261805177 0.8246836066 0.8699175119 + 0.8268756270 0.8253313899 0.8705654144 + 0.8275706172 0.8259792924 0.8712131977 + 0.8282657266 0.8266270757 0.8718574047 + 0.8289580941 0.8272749782 0.8724951744 + 0.8296430111 0.8279188275 0.8731331229 + 0.8303279281 0.8285604715 0.8737708926 + 0.8310129046 0.8292021751 0.8744087219 + 0.8316978216 0.8298439980 0.8750464916 + 0.8323826790 0.8304857016 0.8756781816 + 0.8330640197 0.8311274052 0.8763042092 + 0.8337404132 0.8317648768 0.8769301176 + 0.8344169259 0.8324015141 0.8775560260 + 0.8350933194 0.8330382109 0.8781819940 + 0.8357697725 0.8336749077 0.8788079023 + 0.8364462256 0.8343114853 0.8794273734 + 0.8371186256 0.8349481821 0.8800430298 + 0.8377876878 0.8355782032 0.8806586266 + 0.8384569287 0.8362079859 0.8812742233 + 0.8391259909 0.8368378282 0.8818897009 + 0.8397951722 0.8374676704 0.8825052977 + 0.8404642940 0.8380975127 0.8831139207 + 0.8411294222 0.8387269974 0.8837190270 + 0.8417921066 0.8393514752 0.8843241930 + 0.8424547911 0.8399760127 0.8849294186 + 0.8431175947 0.8406004906 0.8855345249 + 0.8437802792 0.8412250876 0.8861396909 + 0.8444430232 0.8418496251 0.8867368102 + 0.8451002240 0.8424736261 0.8873301148 + 0.8457545042 0.8430926204 0.8879234195 + 0.8464089036 0.8437114954 0.8885167241 + 0.8470631838 0.8443304896 0.8891100287 + 0.8477175832 0.8449494839 0.8897033930 + 0.8483719230 0.8455684781 0.8902897239 + 0.8490217924 0.8461869955 0.8908721209 + 0.8496690989 0.8468005061 0.8914545178 + 0.8503165245 0.8474140167 0.8920369148 + 0.8509638906 0.8480275869 0.8926193118 + 0.8516113162 0.8486410975 0.8932017088 + 0.8522586823 0.8492546082 0.8937774897 + 0.8529011011 0.8498679996 0.8943479061 + 0.8535398841 0.8504769206 0.8949182034 + 0.8541787267 0.8510857821 0.8954886198 + 0.8548175097 0.8516947031 0.8960589767 + 0.8554562926 0.8523036242 0.8966292739 + 0.8560950756 0.8529124260 0.8971949220 + 0.8567304015 0.8535212874 0.8977534175 + 0.8573613763 0.8541247249 0.8983119130 + 0.8579922915 0.8547276258 0.8988704085 + 0.8586232066 0.8553304076 0.8994289041 + 0.8592541814 0.8559333086 0.8999875188 + 0.8598850965 0.8565362096 0.9005435705 + 0.8605139852 0.8571389914 0.9010909200 + 0.8611372113 0.8577383757 0.9016382098 + 0.8617604971 0.8583366275 0.9021854997 + 0.8623837829 0.8589348197 0.9027327895 + 0.8630070090 0.8595330119 0.9032800794 + 0.8636302948 0.8601312041 0.9038274288 + 0.8642532229 0.8607293963 0.9043639898 + 0.8648697734 0.8613250852 0.9049003124 + 0.8654863834 0.8619186878 0.9054365158 + 0.8661028743 0.8625122905 0.9059727192 + 0.8667194843 0.8631058931 0.9065089822 + 0.8673360944 0.8636994958 0.9070451856 + 0.8679525852 0.8642930984 0.9075742960 + 0.8685629964 0.8648849130 0.9080997705 + 0.8691716194 0.8654721975 0.9086254239 + 0.8697801828 0.8660594225 0.9091508985 + 0.8703888059 0.8666465878 0.9096763730 + 0.8709974289 0.8672338128 0.9102020264 + 0.8716059923 0.8678210974 0.9107244015 + 0.8722118735 0.8684083223 0.9112399817 + 0.8728144169 0.8689904809 0.9117556214 + 0.8734169006 0.8695725203 0.9122712016 + 0.8740193844 0.8701543808 0.9127867818 + 0.8746219277 0.8707364202 0.9133024216 + 0.8752244115 0.8713182807 0.9138180017 + 0.8758262992 0.8719003201 0.9143245220 + 0.8764201999 0.8724784851 0.9148302078 + 0.8770139813 0.8730542064 0.9153358936 + 0.8776078820 0.8736298084 0.9158415794 + 0.8782017827 0.8742055297 0.9163473248 + 0.8787956834 0.8747810721 0.9168530107 + 0.8793895245 0.8753566742 0.9173536897 + 0.8799785972 0.8759317994 0.9178488255 + 0.8805649877 0.8765037060 0.9183437824 + 0.8811513186 0.8770756721 0.9188389182 + 0.8817375898 0.8776475787 0.9193338752 + 0.8823239207 0.8782194853 0.9198290110 + 0.8829101920 0.8787915111 0.9203240275 + 0.8834956288 0.8793634176 0.9208073020 + 0.8840755820 0.8799321055 0.9212900996 + 0.8846555948 0.8804993033 0.9217727780 + 0.8852356076 0.8810665011 0.9222555757 + 0.8858155012 0.8816336989 0.9227383137 + 0.8863955140 0.8822008967 0.9232211113 + 0.8869755268 0.8827680945 0.9236993790 + 0.8875501156 0.8833345771 0.9241709113 + 0.8881211281 0.8838980198 0.9246425033 + 0.8886920810 0.8844614029 0.9251139760 + 0.8892630935 0.8850247264 0.9255855083 + 0.8898339868 0.8855881095 0.9260569811 + 0.8904049993 0.8861514926 0.9265285134 + 0.8909760118 0.8867148161 0.9269909859 + 0.8915405869 0.8872753978 0.9274508953 + 0.8921051025 0.8878340125 0.9279108047 + 0.8926696777 0.8883925080 0.9283707142 + 0.8932343125 0.8889510036 0.9288306236 + 0.8937988281 0.8895096183 0.9292904735 + 0.8943634033 0.8900681138 0.9297493100 + 0.8949252963 0.8906266093 0.9301987886 + 0.8954825997 0.8911815286 0.9306483269 + 0.8960399032 0.8917360902 0.9310978055 + 0.8965970874 0.8922907114 0.9315472841 + 0.8971543908 0.8928452730 0.9319968224 + 0.8977115750 0.8933998942 0.9324463010 + 0.8982688785 0.8939545751 0.9328917265 + 0.8988218904 0.8945080042 0.9333304167 + 0.8993726969 0.8950576782 0.9337691069 + 0.8999235034 0.8956074119 0.9342077971 + 0.9004741907 0.8961570859 0.9346464872 + 0.9010249972 0.8967068791 0.9350851774 + 0.9015758038 0.8972566128 0.9355239272 + 0.9021266103 0.8978062868 0.9359565973 + 0.9026709199 0.8983541131 0.9363852739 + 0.9032143950 0.8988997936 0.9368138909 + 0.9037579298 0.8994455934 0.9372426271 + 0.9043012857 0.8999912739 0.9376713037 + 0.9048448205 0.9005370736 0.9380999207 + 0.9053882957 0.9010828733 0.9385285974 + 0.9059311748 0.9016286135 0.9389498234 + 0.9064669013 0.9021710157 0.9393684864 + 0.9070025086 0.9027118087 0.9397872090 + 0.9075381160 0.9032527208 0.9402059913 + 0.9080737829 0.9037935138 0.9406247139 + 0.9086093903 0.9043343067 0.9410433769 + 0.9091449976 0.9048752189 0.9414622188 + 0.9096794724 0.9054160118 0.9418721199 + 0.9102091789 0.9059522748 0.9422804713 + 0.9107388258 0.9064875841 0.9426888824 + 0.9112684727 0.9070228934 0.9430974126 + 0.9117981791 0.9075582027 0.9435058236 + 0.9123278856 0.9080935121 0.9439141750 + 0.9128574729 0.9086288214 0.9443227053 + 0.9133856893 0.9091640115 0.9447216988 + 0.9139096141 0.9096950293 0.9451196194 + 0.9144334793 0.9102253914 0.9455175996 + 0.9149574041 0.9107558727 0.9459155798 + 0.9154813290 0.9112864137 0.9463135004 + 0.9160053134 0.9118168950 0.9467114806 + 0.9165291786 0.9123473763 0.9471095204 + 0.9170514941 0.9128779173 0.9474971890 + 0.9175698161 0.9134051800 0.9478837252 + 0.9180880785 0.9139323235 0.9482702017 + 0.9186064005 0.9144592881 0.9486566782 + 0.9191247225 0.9149863720 0.9490432143 + 0.9196429849 0.9155133963 0.9494296908 + 0.9201613069 0.9160404801 0.9498162270 + 0.9206780195 0.9165676236 0.9501945972 + 0.9211896062 0.9170905948 0.9505711794 + 0.9217011929 0.9176132083 0.9509478807 + 0.9222127795 0.9181358814 0.9513245225 + 0.9227244258 0.9186584949 0.9517011046 + 0.9232360721 0.9191811085 0.9520776868 + 0.9237477183 0.9197037220 0.9524543881 + 0.9242582917 0.9202262759 0.9528239965 + 0.9247623086 0.9207435846 0.9531905055 + 0.9252662063 0.9212597013 0.9535570741 + 0.9257702231 0.9217758179 0.9539235830 + 0.9262741804 0.9222918749 0.9542900920 + 0.9267780781 0.9228079915 0.9546567202 + 0.9272820950 0.9233241081 0.9550232291 + 0.9277861118 0.9238402247 0.9553844929 + 0.9282817841 0.9243530035 0.9557405710 + 0.9287772179 0.9248641133 0.9560965896 + 0.9292725921 0.9253752828 0.9564527273 + 0.9297680259 0.9258863926 0.9568088055 + 0.9302634001 0.9263976216 0.9571648836 + 0.9307587743 0.9269086719 0.9575210214 + 0.9312543273 0.9274199009 0.9578741193 + 0.9317451715 0.9279295206 0.9582188725 + 0.9322345853 0.9284372926 0.9585636258 + 0.9327239990 0.9289451241 0.9589083791 + 0.9332134724 0.9294528961 0.9592531919 + 0.9337028861 0.9299607277 0.9595978856 + 0.9341922998 0.9304684997 0.9599426985 + 0.9346817136 0.9309763908 0.9602873921 + 0.9351692200 0.9314835072 0.9606251121 + 0.9356545210 0.9319880009 0.9609624743 + 0.9361398220 0.9324923754 0.9612998962 + 0.9366251230 0.9329968095 0.9616371989 + 0.9371104240 0.9335011840 0.9619746208 + 0.9375957251 0.9340056181 0.9623119235 + 0.9380810261 0.9345099926 0.9626492858 + 0.9385657907 0.9350144267 0.9629824758 + 0.9390466809 0.9355158210 0.9633126855 + 0.9395276904 0.9360166192 0.9636428952 + 0.9400085807 0.9365174174 0.9639731050 + 0.9404894710 0.9370182157 0.9643033147 + 0.9409704804 0.9375190735 0.9646335244 + 0.9414513707 0.9380198717 0.9649637938 + 0.9419323206 0.9385207295 0.9652928710 + 0.9424098134 0.9390196800 0.9656162262 + 0.9428859949 0.9395166039 0.9659395218 + 0.9433621168 0.9400135279 0.9662628770 + 0.9438381791 0.9405103922 0.9665861726 + 0.9443143010 0.9410073757 0.9669095278 + 0.9447904825 0.9415042996 0.9672328234 + 0.9452666044 0.9420012236 0.9675561190 + 0.9457414746 0.9424980879 0.9678742886 + 0.9462127090 0.9429908991 0.9681897759 + 0.9466838837 0.9434838295 0.9685053229 + 0.9471551180 0.9439765811 0.9688206911 + 0.9476262927 0.9444695115 0.9691361785 + 0.9480975270 0.9449623227 0.9694517255 + 0.9485687017 0.9454551935 0.9697672129 + 0.9490398765 0.9459481239 0.9700819254 + 0.9495072961 0.9464387894 0.9703902006 + 0.9499732256 0.9469273090 0.9706984162 + 0.9504390955 0.9474158287 0.9710066915 + 0.9509050846 0.9479044080 0.9713149071 + 0.9513710141 0.9483929276 0.9716231823 + 0.9518368840 0.9488813877 0.9719315171 + 0.9523028135 0.9493700266 0.9722396731 + 0.9527680874 0.9498584867 0.9725440741 + 0.9532285929 0.9503428936 0.9728444219 + 0.9536892176 0.9508270025 0.9731448293 + 0.9541497827 0.9513109922 0.9734451175 + 0.9546102881 0.9517951012 0.9737455249 + 0.9550709128 0.9522790909 0.9740458727 + 0.9555314183 0.9527631998 0.9743462205 + 0.9559919834 0.9532471895 0.9746466279 + 0.9564499259 0.9537299275 0.9749407768 + 0.9569048285 0.9542092085 0.9752337933 + 0.9573596716 0.9546884894 0.9755266905 + 0.9578146935 0.9551678896 0.9758195877 + 0.9582695961 0.9556471705 0.9761124849 + 0.9587244987 0.9561265111 0.9764053822 + 0.9591794014 0.9566057920 0.9766982794 + 0.9596344233 0.9570850730 0.9769905806 + 0.9600849748 0.9575614929 0.9772747159 + 0.9605342150 0.9580358863 0.9775587916 + 0.9609832764 0.9585102797 0.9778429270 + 0.9614325166 0.9589846134 0.9781271219 + 0.9618816972 0.9594590068 0.9784111977 + 0.9623308778 0.9599334002 0.9786952734 + 0.9627801180 0.9604076743 0.9789794087 + 0.9632292986 0.9608821273 0.9792611003 + 0.9636726975 0.9613522291 0.9795362949 + 0.9641159773 0.9618213773 0.9798114896 + 0.9645593166 0.9622905850 0.9800866246 + 0.9650024772 0.9627596736 0.9803618193 + 0.9654458165 0.9632288814 0.9806370139 + 0.9658889771 0.9636980891 0.9809120893 + 0.9663323164 0.9641671777 0.9811872840 + 0.9667748213 0.9646363854 0.9814590812 + 0.9672120214 0.9651002288 0.9817258716 + 0.9676492214 0.9655638933 0.9819926023 + 0.9680864215 0.9660276175 0.9822592735 + 0.9685236216 0.9664912820 0.9825260043 + 0.9689608216 0.9669550061 0.9827927947 + 0.9693980217 0.9674186110 0.9830595255 + 0.9698352218 0.9678822756 0.9833261967 + 0.9702709913 0.9683455825 0.9835889935 + 0.9707019925 0.9688035250 0.9838470817 + 0.9711331129 0.9692615271 0.9841052890 + 0.9715641141 0.9697194099 0.9843633771 + 0.9719951153 0.9701772928 0.9846215248 + 0.9724261165 0.9706352949 0.9848796725 + 0.9728571177 0.9710931778 0.9851378202 + 0.9732881188 0.9715511799 0.9853959084 + 0.9737175703 0.9720084071 0.9856500030 + 0.9741421938 0.9724602103 0.9858992100 + 0.9745668173 0.9729120135 0.9861484170 + 0.9749915004 0.9733638167 0.9863975048 + 0.9754161239 0.9738156199 0.9866467118 + 0.9758406878 0.9742674232 0.9868959188 + 0.9762653112 0.9747192860 0.9871451259 + 0.9766899943 0.9751710892 0.9873942733 + 0.9771133065 0.9756222963 0.9876399040 + 0.9775314927 0.9760677218 0.9878798723 + 0.9779496789 0.9765130877 0.9881199002 + 0.9783678055 0.9769583941 0.9883599281 + 0.9787859917 0.9774038196 0.9885998964 + 0.9792041779 0.9778491855 0.9888399243 + 0.9796224236 0.9782946110 0.9890798926 + 0.9800404906 0.9787399769 0.9893199205 + 0.9804580808 0.9791854024 0.9895578027 + 0.9808694720 0.9796236753 0.9897894263 + 0.9812808037 0.9800621271 0.9900208712 + 0.9816921949 0.9805004001 0.9902524948 + 0.9821035862 0.9809387922 0.9904841185 + 0.9825149179 0.9813771248 0.9907156825 + 0.9829263091 0.9818153977 0.9909471869 + 0.9833377004 0.9822537899 0.9911788106 + 0.9837489724 0.9826921225 0.9914098978 + 0.9841541052 0.9831243157 0.9916322827 + 0.9845585823 0.9835553169 0.9918547273 + 0.9849629998 0.9839861989 0.9920771718 + 0.9853674769 0.9844172001 0.9922996163 + 0.9857718945 0.9848480821 0.9925220013 + 0.9861763716 0.9852790833 0.9927443862 + 0.9865807891 0.9857100248 0.9929667711 + 0.9869853258 0.9861410260 0.9931892157 + 0.9873846173 0.9865669012 0.9934051037 + 0.9877814054 0.9869893789 0.9936192036 + 0.9881781936 0.9874119759 0.9938333035 + 0.9885749817 0.9878345132 0.9940474033 + 0.9889718294 0.9882571101 0.9942615032 + 0.9893686175 0.9886797071 0.9944754839 + 0.9897654057 0.9891021848 0.9946895838 + 0.9901620746 0.9895247817 0.9949036837 + 0.9905560017 0.9899445772 0.9951136112 + 0.9909446836 0.9903581738 0.9953181148 + 0.9913333058 0.9907717705 0.9955226779 + 0.9917219281 0.9911854267 0.9957271814 + 0.9921104908 0.9915990233 0.9959316850 + 0.9924991727 0.9920125008 0.9961361885 + 0.9928877950 0.9924260974 0.9963408113 + 0.9932764173 0.9928396940 0.9965453148 + 0.9936649799 0.9932532907 0.9967495203 + 0.9940438867 0.9936568141 0.9969443083 + 0.9944224954 0.9940596223 0.9971390963 + 0.9948012233 0.9944624901 0.9973340034 + 0.9951797724 0.9948652983 0.9975287914 + 0.9955583811 0.9952681065 0.9977235794 + 0.9959369898 0.9956709146 0.9979184270 + 0.9963155985 0.9960737824 0.9981132150 + 0.9966943264 0.9964765906 0.9983080029 + 0.9970666766 0.9968733788 0.9984989166 + 0.9974333048 0.9972642064 0.9986864924 + 0.9977999926 0.9976549745 0.9988741875 + 0.9981666803 0.9980459213 0.9990618229 + 0.9985333085 0.9984366894 0.9992493987 + 0.9988999963 0.9988275170 0.9994370937 + 0.9992666841 0.9992182851 0.9996247292 + 0.9996333122 0.9996091723 0.9998124242 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_100.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_100.spi1d index 348d8f1d..9fe8134b 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_100.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_100.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0187414307 0.0199415907 0.0138841104 - 0.0330222212 0.0346524492 0.0255766194 - 0.0449542291 0.0472631715 0.0357697308 - 0.0556400903 0.0585798696 0.0450413786 - 0.0654912964 0.0689996779 0.0536506996 - 0.0747707486 0.0787579194 0.0617258511 - 0.0836306289 0.0879753232 0.0693477094 - 0.0921674594 0.0967618302 0.0766232982 - 0.1004453003 0.1051589996 0.0835988298 - 0.1085427999 0.1132052019 0.0902485028 - 0.1164767966 0.1209279969 0.0966249332 - 0.1242111027 0.1283497959 0.1027311981 - 0.1316560954 0.1354517043 0.1085847020 - 0.1387570053 0.1422473937 0.1142328009 - 0.1455107033 0.1487720013 0.1196969002 - 0.1519546062 0.1550467014 0.1249829978 - 0.1581331939 0.1611181051 0.1301161945 - 0.1640816927 0.1669937074 0.1350999027 - 0.1698216051 0.1726770997 0.1399576068 - 0.1753643006 0.1782052964 0.1447027028 - 0.1807104945 0.1835688949 0.1493282020 - 0.1858936995 0.1887871027 0.1538430005 - 0.1909191012 0.1938627958 0.1582642943 - 0.1958166957 0.1988143027 0.1625947952 - 0.2005814016 0.2036329061 0.1668373942 - 0.2052128017 0.2083510011 0.1709935963 - 0.2097522020 0.2129525989 0.1750802994 - 0.2141969949 0.2174569964 0.1790975928 - 0.2185294926 0.2218813002 0.1830428988 - 0.2227485031 0.2262317985 0.1869215071 - 0.2268646061 0.2304809988 0.1907352060 - 0.2308858931 0.2346275002 0.1944852024 - 0.2348421067 0.2386894971 0.1981807947 - 0.2387396991 0.2426760942 0.2018260062 - 0.2425788045 0.2466064990 0.2054218054 - 0.2463494986 0.2504898012 0.2089662999 - 0.2500706017 0.2543140054 0.2124685943 - 0.2537479997 0.2580659986 0.2159260958 - 0.2573781013 0.2617495954 0.2193453014 - 0.2609572113 0.2653856874 0.2227171957 - 0.2644815147 0.2689718008 0.2260479033 - 0.2679471076 0.2725043893 0.2293380946 - 0.2713502049 0.2759802938 0.2325894982 - 0.2746953070 0.2793953121 0.2358044982 - 0.2779825926 0.2827470899 0.2389748991 - 0.2812089920 0.2860386968 0.2421178967 - 0.2843785882 0.2892819047 0.2452265024 - 0.2875038087 0.2924897075 0.2482994944 - 0.2905904055 0.2956646979 0.2513419092 - 0.2936390042 0.2988075912 0.2543503046 - 0.2966530025 0.3019191027 0.2573288083 - 0.2996253073 0.3050011992 0.2602812052 - 0.3025568128 0.3080531061 0.2632032037 - 0.3054536879 0.3110767007 0.2660993040 - 0.3083193004 0.3140729070 0.2689616978 - 0.3111566007 0.3170422912 0.2717953026 - 0.3139680028 0.3199853003 0.2746086121 - 0.3167569935 0.3229024112 0.2773948908 - 0.3195263147 0.3257946074 0.2801466882 - 0.3222785890 0.3286626935 0.2828755975 - 0.3250167072 0.3315044045 0.2855885029 - 0.3277421892 0.3343142867 0.2882772982 - 0.3304488063 0.3370938897 0.2909384072 - 0.3331305087 0.3398444057 0.2935771048 - 0.3357870877 0.3425664902 0.2961899042 - 0.3384202123 0.3452622890 0.2987827063 - 0.3410323858 0.3479284048 0.3013578057 - 0.3436256051 0.3505629897 0.3039124906 - 0.3462015092 0.3531686962 0.3064478934 - 0.3487611115 0.3557462096 0.3089591861 - 0.3513003886 0.3582991958 0.3114418983 - 0.3538151979 0.3608300984 0.3139019012 - 0.3563049138 0.3633398116 0.3163512051 - 0.3587693870 0.3658294976 0.3187870085 - 0.3612132072 0.3682993948 0.3212068081 - 0.3636367023 0.3707447946 0.3236055970 - 0.3660412133 0.3731693029 0.3259812891 - 0.3684293032 0.3755750954 0.3283393085 - 0.3708001971 0.3779630959 0.3306787014 - 0.3731575906 0.3803366125 0.3330000043 - 0.3754999042 0.3826906979 0.3352980912 - 0.3778310120 0.3850283921 0.3375810087 - 0.3801496029 0.3873493969 0.3398480117 - 0.3824588060 0.3896577060 0.3421055973 - 0.3847584128 0.3919534981 0.3443531990 - 0.3870503008 0.3942336142 0.3465890884 - 0.3893356919 0.3964999914 0.3488120139 - 0.3916150033 0.3987481892 0.3510178924 - 0.3938865066 0.4009808004 0.3532077074 - 0.3961476088 0.4031977952 0.3553779125 - 0.3983986080 0.4054017067 0.3575282991 - 0.4006389976 0.4075945020 0.3596653938 - 0.4028654993 0.4097732902 0.3617888093 - 0.4050788879 0.4119321108 0.3638970852 - 0.4072808027 0.4140745103 0.3659907877 - 0.4094718099 0.4162018001 0.3680689037 - 0.4116508067 0.4183138907 0.3701283038 - 0.4138199985 0.4204131961 0.3721699119 - 0.4159773886 0.4225000143 0.3741934001 - 0.4181219041 0.4245725870 0.3761979938 - 0.4202522039 0.4266324937 0.3781862855 - 0.4223701060 0.4286797941 0.3801617920 - 0.4244759083 0.4307178855 0.3821251094 - 0.4265703857 0.4327476919 0.3840759993 - 0.4286541939 0.4347653985 0.3860141933 - 0.4307268858 0.4367690086 0.3879393041 - 0.4327893853 0.4387587011 0.3898504972 - 0.4348411858 0.4407365918 0.3917472959 - 0.4368841052 0.4427039027 0.3936302960 - 0.4389176071 0.4446617961 0.3954991102 - 0.4409425855 0.4466080964 0.3973535895 - 0.4429590106 0.4485442936 0.3991936147 - 0.4449672103 0.4504688084 0.4010188878 - 0.4469676912 0.4523817897 0.4028322101 - 0.4489580095 0.4542838037 0.4046368897 - 0.4509370923 0.4561766982 0.4064337909 - 0.4529055953 0.4580585063 0.4082230031 - 0.4548639059 0.4599294066 0.4100044966 - 0.4568125904 0.4617907107 0.4117785990 - 0.4587523937 0.4636436999 0.4135451913 - 0.4606837034 0.4654872119 0.4153045118 - 0.4626069963 0.4673207998 0.4170565009 - 0.4645206034 0.4691432118 0.4188013971 - 0.4664235115 0.4709551036 0.4205392003 - 0.4683176875 0.4727576971 0.4222700000 - 0.4702025950 0.4745520949 0.4239938855 - 0.4720798135 0.4763393104 0.4257110059 - 0.4739468098 0.4781185091 0.4274213910 - 0.4758000970 0.4798890054 0.4291251004 - 0.4776405096 0.4816485941 0.4308224022 - 0.4794681072 0.4833956063 0.4325130880 - 0.4812825918 0.4851314127 0.4341974854 - 0.4830833077 0.4868572056 0.4358755946 - 0.4848699868 0.4885736108 0.4375469983 - 0.4866446853 0.4902822077 0.4392102063 - 0.4884060025 0.4919806123 0.4408645928 - 0.4901559949 0.4936690032 0.4425103962 - 0.4918906093 0.4953497946 0.4441474974 - 0.4936125875 0.4970211089 0.4457770884 - 0.4953204989 0.4986830056 0.4473989010 - 0.4970144928 0.5003349185 0.4490132928 - 0.4986953139 0.5019772053 0.4506205916 - 0.5003637075 0.5036125779 0.4522204101 - 0.5020195842 0.5052385926 0.4538131952 - 0.5036631227 0.5068594813 0.4553956985 - 0.5052949190 0.5084728003 0.4569694102 - 0.5069147944 0.5100775957 0.4585345089 - 0.5085229874 0.5116732717 0.4600914121 - 0.5101199746 0.5132597089 0.4616405070 - 0.5117021203 0.5148342848 0.4631803036 - 0.5132743120 0.5164015293 0.4647122920 - 0.5148364902 0.5179616809 0.4662373960 - 0.5163900852 0.5195158124 0.4677560031 - 0.5179367065 0.5210649967 0.4692681134 - 0.5194708109 0.5226017237 0.4707722962 - 0.5209966898 0.5241310000 0.4722703099 - 0.5225142241 0.5256524086 0.4737617075 - 0.5240231752 0.5271654725 0.4752461910 - 0.5255233049 0.5286697149 0.4767219126 - 0.5270152092 0.5301663876 0.4781898856 - 0.5284978151 0.5316576958 0.4796513021 - 0.5299711823 0.5331442952 0.4811064899 - 0.5314347148 0.5346261263 0.4825553000 - 0.5328863859 0.5360969901 0.4839963913 - 0.5343297124 0.5375598073 0.4854325056 - 0.5357651711 0.5390148759 0.4868634939 - 0.5371924043 0.5404617190 0.4882858992 - 0.5386118293 0.5419015884 0.4897007942 - 0.5400264859 0.5433354974 0.4911100864 - 0.5414369702 0.5447630286 0.4925136864 - 0.5428367853 0.5461803079 0.4939090908 - 0.5442283750 0.5475915074 0.4952997863 - 0.5456125736 0.5489966869 0.4966861010 - 0.5469890237 0.5503935814 0.4980660975 - 0.5483577847 0.5517833829 0.4994400144 - 0.5497220755 0.5531684160 0.5008088946 - 0.5510820746 0.5545488000 0.5021709800 - 0.5524330139 0.5559220910 0.5035272837 - 0.5537772775 0.5572903156 0.5048794746 - 0.5551130772 0.5586516261 0.5062263012 - 0.5564405918 0.5600048900 0.5075680017 - 0.5577619076 0.5613521934 0.5089064837 - 0.5590745807 0.5626906157 0.5102406740 - 0.5603765249 0.5640223026 0.5115708113 - 0.5616722703 0.5653495193 0.5128980279 - 0.5629596710 0.5666700006 0.5142201185 - 0.5642386079 0.5679866076 0.5155370831 - 0.5655131936 0.5692998171 0.5168507099 - 0.5667812824 0.5706049204 0.5181574225 - 0.5680419207 0.5719022155 0.5194594264 - 0.5692980289 0.5731946230 0.5207582712 - 0.5705438852 0.5744786859 0.5220487118 - 0.5717846751 0.5757588148 0.5233348012 - 0.5730205774 0.5770344734 0.5246167183 - 0.5742480159 0.5783056021 0.5258914828 - 0.5754718184 0.5795726776 0.5271635056 - 0.5766893029 0.5808308721 0.5284305811 - 0.5778992176 0.5820825100 0.5296934247 - 0.5791049004 0.5833303928 0.5309540033 - 0.5802994967 0.5845699906 0.5322073102 - 0.5814914107 0.5858064890 0.5334576964 - 0.5826792121 0.5870373845 0.5347031951 - 0.5838587284 0.5882629156 0.5359424949 - 0.5850344896 0.5894858837 0.5371791124 - 0.5861983895 0.5906990170 0.5384079218 - 0.5873590708 0.5919092894 0.5396336913 - 0.5885140896 0.5931131840 0.5408535004 - 0.5896620154 0.5943115950 0.5420675278 - 0.5908060074 0.5955064297 0.5432779789 - 0.5919439793 0.5966913104 0.5444819927 - 0.5930796266 0.5978732705 0.5456839800 - 0.5942065120 0.5990452170 0.5468795896 - 0.5953297019 0.6002144217 0.5480728745 - 0.5964481235 0.6013793945 0.5492628813 - 0.5975626111 0.6025397182 0.5504478812 - 0.5986750126 0.6036971211 0.5516301990 - 0.5997776985 0.6048492789 0.5528064966 - 0.6008782983 0.6060000062 0.5539814234 - 0.6019700170 0.6071417928 0.5551493764 - 0.6030591130 0.6082807779 0.5563148260 - 0.6041415930 0.6094110012 0.5574722886 - 0.6052209735 0.6105381846 0.5586267710 - 0.6062965989 0.6116611958 0.5597772002 - 0.6073693037 0.6127799749 0.5609225035 - 0.6084402800 0.6138954759 0.5620642900 - 0.6095039845 0.6150087714 0.5632020831 - 0.6105654836 0.6161211729 0.5643385053 - 0.6116194725 0.6172274947 0.5654675961 - 0.6126726270 0.6183326840 0.5665956140 - 0.6137179732 0.6194270253 0.5677173734 - 0.6147624254 0.6205205917 0.5688384175 - 0.6157985926 0.6216076016 0.5699521899 - 0.6168330908 0.6226931810 0.5710644722 - 0.6178600788 0.6237692833 0.5721685290 - 0.6188859940 0.6248437166 0.5732709169 - 0.6199086905 0.6259111166 0.5743672848 - 0.6209291220 0.6269769073 0.5754616857 - 0.6219440103 0.6280378103 0.5765504241 - 0.6229550838 0.6290960908 0.5776373744 - 0.6239585876 0.6301478148 0.5787205100 - 0.6249598265 0.6311972737 0.5798012018 - 0.6259567142 0.6322420239 0.5808768868 - 0.6269508004 0.6332836747 0.5819506049 - 0.6279395223 0.6343185902 0.5830199718 - 0.6289266944 0.6353514194 0.5840864778 - 0.6299108863 0.6363787055 0.5851466060 - 0.6308935285 0.6374046803 0.5862044096 - 0.6318724751 0.6384266019 0.5872561932 - 0.6328493953 0.6394464970 0.5883064270 - 0.6338198781 0.6404585242 0.5893509984 - 0.6347895861 0.6414691806 0.5903946757 - 0.6357561946 0.6424723268 0.5914332867 - 0.6367220879 0.6434748769 0.5924714208 - 0.6376814246 0.6444696784 0.5935050249 - 0.6386405826 0.6454644799 0.5945385098 - 0.6395891905 0.6464499831 0.5955650806 - 0.6405377984 0.6474351883 0.5965915918 - 0.6414811015 0.6484122276 0.5976113081 - 0.6424236894 0.6493877172 0.5986298919 - 0.6433591843 0.6503596902 0.5996431708 - 0.6442922950 0.6513301134 0.6006544828 - 0.6452214122 0.6522970200 0.6016616821 - 0.6461476088 0.6532608867 0.6026659012 - 0.6470720768 0.6542214155 0.6036679745 - 0.6479939222 0.6551764011 0.6046671867 - 0.6489133239 0.6561300755 0.6056650281 - 0.6498240829 0.6570777893 0.6066576838 - 0.6507347226 0.6580253839 0.6076505184 - 0.6516370177 0.6589658856 0.6086354852 - 0.6525393128 0.6599062085 0.6096205115 - 0.6534368992 0.6608443856 0.6105999947 - 0.6543331146 0.6617819071 0.6115779877 - 0.6552255750 0.6627163291 0.6125519276 - 0.6561149955 0.6636475921 0.6135221124 - 0.6570025086 0.6645767093 0.6144911051 - 0.6578854918 0.6654986143 0.6154568195 - 0.6587684155 0.6664205790 0.6164224744 - 0.6596441269 0.6673378944 0.6173813939 - 0.6605197787 0.6682549715 0.6183400750 - 0.6613910794 0.6691666245 0.6192939878 - 0.6622604132 0.6700754762 0.6202455163 - 0.6631278992 0.6709814072 0.6211943030 - 0.6639922857 0.6718816757 0.6221385002 - 0.6648563147 0.6727818251 0.6230823994 - 0.6657119989 0.6736767888 0.6240196228 - 0.6665678024 0.6745718122 0.6249567270 - 0.6674181819 0.6754602790 0.6258906126 - 0.6682661772 0.6763452888 0.6268228889 - 0.6691122055 0.6772282124 0.6277527809 - 0.6699538231 0.6781055927 0.6286767125 - 0.6707953811 0.6789829731 0.6296005249 - 0.6716330051 0.6798558831 0.6305189133 - 0.6724700928 0.6807280779 0.6314365268 - 0.6733044982 0.6815952063 0.6323506832 - 0.6741359830 0.6824558973 0.6332612038 - 0.6749671102 0.6833165288 0.6341714263 - 0.6757913828 0.6841681004 0.6350759864 - 0.6766155958 0.6850196719 0.6359806061 - 0.6774365902 0.6858676076 0.6368827224 - 0.6782553792 0.6867125034 0.6377828121 - 0.6790735126 0.6875569224 0.6386821270 - 0.6798856854 0.6883959174 0.6395736933 - 0.6806979775 0.6892347932 0.6404653192 - 0.6815052032 0.6900711060 0.6413521171 - 0.6823093295 0.6909055114 0.6422356963 - 0.6831128001 0.6917393804 0.6431186795 - 0.6839104891 0.6925656199 0.6439942718 - 0.6847081780 0.6933919191 0.6448698044 - 0.6855018735 0.6942142844 0.6457418203 - 0.6862921715 0.6950333118 0.6466106772 - 0.6870825291 0.6958522201 0.6474797130 - 0.6878690720 0.6966655254 0.6483433247 - 0.6886556149 0.6974784732 0.6492068768 - 0.6894404888 0.6982899904 0.6500678062 - 0.6902230978 0.6990988851 0.6509246230 - 0.6910058260 0.6999077797 0.6517813802 - 0.6917819977 0.7007126808 0.6526325941 - 0.6925566792 0.7015163898 0.6534823179 - 0.6933307052 0.7023192048 0.6543310285 - 0.6941010952 0.7031159997 0.6551743746 - 0.6948714852 0.7039129138 0.6560177207 - 0.6956394911 0.7047072053 0.6568580866 - 0.6964054704 0.7054989934 0.6576957107 - 0.6971715093 0.7062907815 0.6585332751 - 0.6979302764 0.7070775032 0.6593660116 - 0.6986879110 0.7078630924 0.6601977944 - 0.6994447112 0.7086477280 0.6610285044 - 0.7001975179 0.7094256878 0.6618531942 - 0.7009503245 0.7102035880 0.6626778245 - 0.7017009258 0.7109789848 0.6634991765 - 0.7024487853 0.7117509246 0.6643164158 - 0.7031968236 0.7125229239 0.6651335955 - 0.7039384246 0.7132899761 0.6659477949 - 0.7046775222 0.7140548825 0.6667606831 - 0.7054166198 0.7148197889 0.6675736904 - 0.7061507106 0.7155792713 0.6683797836 - 0.7068846226 0.7163382769 0.6691855788 - 0.7076175213 0.7170966268 0.6699900031 - 0.7083460093 0.7178505063 0.6707876921 - 0.7090746164 0.7186043859 0.6715853810 - 0.7098006010 0.7193557024 0.6723811030 - 0.7105224729 0.7201020122 0.6731733084 - 0.7112444043 0.7208483219 0.6739653945 - 0.7119640112 0.7215924263 0.6747537851 - 0.7126817703 0.7223345041 0.6755387783 - 0.7133995891 0.7230767012 0.6763237715 - 0.7141135931 0.7238159776 0.6771062016 - 0.7148258090 0.7245538831 0.6778873205 - 0.7155380249 0.7252917290 0.6786683798 - 0.7162464857 0.7260233164 0.6794440746 - 0.7169541717 0.7267531157 0.6802182794 - 0.7176619172 0.7274829149 0.6809926033 - 0.7183669209 0.7282087207 0.6817620993 - 0.7190716863 0.7289338708 0.6825309992 - 0.7197765708 0.7296590209 0.6832998991 - 0.7204757929 0.7303797007 0.6840652823 - 0.7211748958 0.7311002016 0.6848304868 - 0.7218738794 0.7318205833 0.6855955124 - 0.7225683928 0.7325310707 0.6863517165 - 0.7232629061 0.7332416177 0.6871078014 - 0.7239571810 0.7339518070 0.6878635883 - 0.7246487141 0.7346547842 0.6886121035 - 0.7253403068 0.7353578210 0.6893606782 - 0.7260314226 0.7360606194 0.6901090145 - 0.7267171741 0.7367588282 0.6908516884 - 0.7274029255 0.7374569178 0.6915943027 - 0.7280883789 0.7381548882 0.6923367977 - 0.7287694812 0.7388454080 0.6930751204 - 0.7294505239 0.7395359278 0.6938133836 - 0.7301315069 0.7402263880 0.6945515871 - 0.7308093905 0.7409111261 0.6952871084 - 0.7314873934 0.7415956259 0.6960226297 - 0.7321652770 0.7422801256 0.6967579722 - 0.7328376770 0.7429602146 0.6974878907 - 0.7335097790 0.7436398268 0.6982172132 - 0.7341818213 0.7443194985 0.6989465952 - 0.7348498106 0.7449948192 0.6996716261 - 0.7355170250 0.7456691861 0.7003955841 - 0.7361841202 0.7463434935 0.7011196017 - 0.7368490100 0.7470148206 0.7018402219 - 0.7375128865 0.7476845980 0.7025592923 - 0.7381768227 0.7483543754 0.7032784224 - 0.7388374209 0.7490217090 0.7039942741 - 0.7394952774 0.7496867180 0.7047075033 - 0.7401531935 0.7503517270 0.7054206133 - 0.7408087254 0.7510150075 0.7061315775 - 0.7414602041 0.7516747713 0.7068389058 - 0.7421118021 0.7523347139 0.7075461149 - 0.7427623868 0.7529938221 0.7082526088 - 0.7434083819 0.7536479831 0.7089549899 - 0.7440543175 0.7543023229 0.7096574903 - 0.7447003126 0.7549564838 0.7103599906 - 0.7453430295 0.7556046247 0.7110564113 - 0.7459853888 0.7562519908 0.7117524743 - 0.7466279268 0.7568994761 0.7124484777 - 0.7472671270 0.7575436831 0.7131392956 - 0.7479048967 0.7581862807 0.7138277888 - 0.7485427260 0.7588288784 0.7145162225 - 0.7491782904 0.7594693899 0.7152029276 - 0.7498108149 0.7601066232 0.7158870101 - 0.7504433990 0.7607436776 0.7165712118 - 0.7510756254 0.7613803744 0.7172548175 - 0.7517048120 0.7620109916 0.7179335952 - 0.7523341179 0.7626414895 0.7186123133 - 0.7529633045 0.7632719874 0.7192909718 - 0.7535892129 0.7638990879 0.7199658751 - 0.7542141080 0.7645248771 0.7206395268 - 0.7548390031 0.7651507258 0.7213131785 - 0.7554618716 0.7657744884 0.7219849825 - 0.7560818791 0.7663950920 0.7226539254 - 0.7567018270 0.7670155764 0.7233228087 - 0.7573217154 0.7676361799 0.7239916921 - 0.7579385042 0.7682504058 0.7246547937 - 0.7585554123 0.7688643932 0.7253178954 - 0.7591722012 0.7694783807 0.7259808779 - 0.7597867250 0.7700899839 0.7266423702 - 0.7603995204 0.7706995010 0.7273027897 - 0.7610123158 0.7713090181 0.7279632092 - 0.7616242766 0.7719178796 0.7286229730 - 0.7622311115 0.7725210786 0.7292783856 - 0.7628380060 0.7731242776 0.7299337983 - 0.7634449005 0.7737274170 0.7305892110 - 0.7640485764 0.7743269205 0.7312399149 - 0.7646508217 0.7749242187 0.7318879962 - 0.7652528882 0.7755215168 0.7325360775 - 0.7658544779 0.7761185169 0.7331836224 - 0.7664529085 0.7767131925 0.7338275909 - 0.7670513988 0.7773079872 0.7344716191 - 0.7676498294 0.7779027224 0.7351155877 - 0.7682449222 0.7784941196 0.7357556820 - 0.7688378096 0.7790830731 0.7363932133 - 0.7694306970 0.7796722054 0.7370308042 - 0.7700235248 0.7802610993 0.7376680970 - 0.7706120014 0.7808436155 0.7383009791 - 0.7712004781 0.7814261913 0.7389338017 - 0.7717890739 0.7820088267 0.7395666242 - 0.7723761201 0.7825897932 0.7401980758 - 0.7729613185 0.7831684947 0.7408279777 - 0.7735465169 0.7837471962 0.7414578199 - 0.7741317749 0.7843258977 0.7420876026 - 0.7747126818 0.7849006057 0.7427113056 - 0.7752926946 0.7854743004 0.7433335781 - 0.7758728266 0.7860481143 0.7439559102 - 0.7764521241 0.7866209745 0.7445772886 - 0.7770282030 0.7871890068 0.7451937795 - 0.7776042223 0.7877569795 0.7458102703 - 0.7781801820 0.7883250117 0.7464267015 - 0.7787547708 0.7888916135 0.7470412850 - 0.7793279290 0.7894564271 0.7476536036 - 0.7799009085 0.7900211811 0.7482659817 - 0.7804740071 0.7905859947 0.7488783002 - 0.7810425162 0.7911475897 0.7494871020 - 0.7816095948 0.7917081714 0.7500947714 - 0.7821766734 0.7922688127 0.7507023811 - 0.7827436924 0.7928293943 0.7513099909 - 0.7833052278 0.7933837771 0.7519133091 - 0.7838668227 0.7939382195 0.7525165081 - 0.7844284177 0.7944926023 0.7531198263 - 0.7849892974 0.7950457931 0.7537218928 - 0.7855485082 0.7955945134 0.7543202043 - 0.7861077189 0.7961432934 0.7549185157 - 0.7866668105 0.7966920733 0.7555168867 - 0.7872235775 0.7972388268 0.7561128139 - 0.7877774239 0.7977824807 0.7567057014 - 0.7883312106 0.7983261943 0.7572985291 - 0.7888849974 0.7988699079 0.7578914165 - 0.7894352078 0.7994105816 0.7584812045 - 0.7899829745 0.7999492288 0.7590687871 - 0.7905306816 0.8004878163 0.7596564889 - 0.7910785079 0.8010262847 0.7602441907 - 0.7916234732 0.8015617132 0.7608277798 - 0.7921676040 0.8020955920 0.7614096999 - 0.7927116752 0.8026295900 0.7619916797 - 0.7932558060 0.8031635284 0.7625735998 - 0.7937961221 0.8036941886 0.7631514072 - 0.7943354845 0.8042240739 0.7637282014 - 0.7948749065 0.8047538996 0.7643049955 - 0.7954143286 0.8052837849 0.7648817897 - 0.7959489822 0.8058102131 0.7654550076 - 0.7964830995 0.8063359261 0.7660276294 - 0.7970170975 0.8068616986 0.7666001916 - 0.7975512147 0.8073874712 0.7671728134 - 0.7980824709 0.8079090118 0.7677428126 - 0.7986134887 0.8084300160 0.7683125138 - 0.7991445065 0.8089510202 0.7688822150 - 0.7996755242 0.8094719052 0.7694519162 - 0.8002029061 0.8099884987 0.7700183988 - 0.8007299900 0.8105044961 0.7705844045 - 0.8012570143 0.8110203743 0.7711504102 - 0.8017840981 0.8115363717 0.7717164755 - 0.8023071289 0.8120499849 0.7722783089 - 0.8028293848 0.8125631213 0.7728394866 - 0.8033518195 0.8130761981 0.7734007239 - 0.8038741946 0.8135892749 0.7739619017 - 0.8043946028 0.8140997291 0.7745202780 - 0.8049145937 0.8146092296 0.7750778794 - 0.8054345250 0.8151187897 0.7756354809 - 0.8059545159 0.8156284094 0.7761932015 - 0.8064721823 0.8161345720 0.7767478228 - 0.8069888949 0.8166393042 0.7773010731 - 0.8075056076 0.8171439767 0.7778543830 - 0.8080223203 0.8176487088 0.7784076929 - 0.8085361123 0.8181504011 0.7789580226 - 0.8090476990 0.8186495900 0.7795059085 - 0.8095592856 0.8191487789 0.7800536752 - 0.8100708723 0.8196480274 0.7806016207 - 0.8105810881 0.8201459050 0.7811478972 - 0.8110890985 0.8206415176 0.7816914916 - 0.8115971088 0.8211370707 0.7822352052 - 0.8121051788 0.8216326833 0.7827789187 - 0.8126127720 0.8221275210 0.7833216786 - 0.8131183982 0.8226181865 0.7838603854 - 0.8136240244 0.8231089115 0.7843990922 - 0.8141295910 0.8235995770 0.7849379182 - 0.8146352172 0.8240903020 0.7854766250 - 0.8151367903 0.8245751858 0.7860131264 - 0.8156381249 0.8250597119 0.7865493894 - 0.8161394000 0.8255441785 0.7870855927 - 0.8166406751 0.8260285854 0.7876219153 - 0.8171395063 0.8265116215 0.7881566286 - 0.8176370263 0.8269937038 0.7886903286 - 0.8181344867 0.8274759054 0.7892240882 - 0.8186320066 0.8279579878 0.7897579074 - 0.8191282749 0.8284389973 0.7902898192 - 0.8196223974 0.8289175034 0.7908182740 - 0.8201165795 0.8293960094 0.7913469076 - 0.8206107020 0.8298745155 0.7918754220 - 0.8211048245 0.8303530216 0.7924038768 - 0.8215956092 0.8308256269 0.7929295897 - 0.8220863938 0.8312981129 0.7934553027 - 0.8225771785 0.8317705989 0.7939808965 - 0.8230679035 0.8322430849 0.7945066094 - 0.8235569000 0.8327139020 0.7950305939 - 0.8240448236 0.8331835866 0.7955533862 - 0.8245326281 0.8336532116 0.7960761786 - 0.8250204921 0.8341228962 0.7965989709 - 0.8255078197 0.8345921040 0.7971209884 - 0.8259931803 0.8350595236 0.7976391912 - 0.8264786005 0.8355268240 0.7981575131 - 0.8269640207 0.8359941840 0.7986757159 - 0.8274493814 0.8364614844 0.7991939783 - 0.8279315829 0.8369246721 0.7997112274 - 0.8284127116 0.8373864293 0.8002278805 - 0.8288937807 0.8378480077 0.8007447124 - 0.8293749094 0.8383095860 0.8012614250 - 0.8298550248 0.8387699723 0.8017773032 - 0.8303318024 0.8392266035 0.8022907972 - 0.8308085799 0.8396831751 0.8028041720 - 0.8312854171 0.8401398063 0.8033176064 - 0.8317621946 0.8405963778 0.8038309813 - 0.8322365880 0.8410515785 0.8043408990 - 0.8327100277 0.8415061831 0.8048493266 - 0.8331834078 0.8419607878 0.8053576946 - 0.8336567879 0.8424153924 0.8058661222 - 0.8341296911 0.8428695202 0.8063743114 - 0.8345996737 0.8433197737 0.8068807125 - 0.8350697160 0.8437700868 0.8073871136 - 0.8355396986 0.8442202806 0.8078935146 - 0.8360096812 0.8446705937 0.8083999157 - 0.8364778757 0.8451184034 0.8089042902 - 0.8369445801 0.8455640078 0.8094068766 - 0.8374112248 0.8460094929 0.8099095225 - 0.8378779292 0.8464550972 0.8104121089 - 0.8383445144 0.8469007015 0.8109146953 - 0.8388080001 0.8473442197 0.8114140034 - 0.8392711878 0.8477873802 0.8119127750 - 0.8397343159 0.8482306004 0.8124117255 - 0.8401975036 0.8486738801 0.8129106164 - 0.8406596780 0.8491165042 0.8134090900 - 0.8411186934 0.8495569229 0.8139066100 - 0.8415778279 0.8499972224 0.8144040108 - 0.8420367837 0.8504375815 0.8149014711 - 0.8424957991 0.8508778811 0.8153988719 - 0.8429536819 0.8513162136 0.8158944249 - 0.8434103727 0.8517522812 0.8163877130 - 0.8438671231 0.8521882892 0.8168810010 - 0.8443238139 0.8526242971 0.8173742890 - 0.8447803855 0.8530603051 0.8178675771 - 0.8452352881 0.8534938097 0.8183590770 - 0.8456894159 0.8539261818 0.8188499212 - 0.8461434841 0.8543586731 0.8193408251 - 0.8465976715 0.8547911048 0.8198316097 - 0.8470517993 0.8552235961 0.8203225136 - 0.8475030065 0.8556535244 0.8208127022 - 0.8479539752 0.8560830951 0.8213027716 - 0.8484050035 0.8565127254 0.8217929006 - 0.8488559723 0.8569422960 0.8222830892 - 0.8493067026 0.8573716283 0.8227729201 - 0.8497545719 0.8577967286 0.8232586980 - 0.8502025008 0.8582218885 0.8237444758 - 0.8506504297 0.8586469889 0.8242303133 - 0.8510982990 0.8590722084 0.8247160912 - 0.8515453935 0.8594967723 0.8252013922 - 0.8519896269 0.8599187732 0.8256844878 - 0.8524339199 0.8603408933 0.8261677027 - 0.8528780937 0.8607628942 0.8266509175 - 0.8533223867 0.8611850142 0.8271340728 - 0.8537651896 0.8616067171 0.8276169896 - 0.8542045951 0.8620272875 0.8280991912 - 0.8546440005 0.8624479175 0.8285813928 - 0.8550834060 0.8628684878 0.8290637136 - 0.8555228114 0.8632891178 0.8295459151 - 0.8559610844 0.8637089133 0.8300266862 - 0.8563969135 0.8641266227 0.8305044770 - 0.8568326831 0.8645442724 0.8309822083 - 0.8572685122 0.8649619818 0.8314598799 - 0.8577042818 0.8653796911 0.8319376707 - 0.8581392765 0.8657960892 0.8324145079 - 0.8585724831 0.8662096262 0.8328894973 - 0.8590056896 0.8666229844 0.8333644271 - 0.8594390154 0.8670365214 0.8338394165 - 0.8598722219 0.8674498796 0.8343142867 - 0.8603044152 0.8678625226 0.8347889185 - 0.8607339263 0.8682723045 0.8352627158 - 0.8611633778 0.8686822057 0.8357365131 - 0.8615928888 0.8690921068 0.8362103105 - 0.8620223999 0.8695020080 0.8366839886 - 0.8624514937 0.8699113727 0.8371570110 - 0.8628783822 0.8703184128 0.8376260996 - 0.8633052707 0.8707253933 0.8380951881 - 0.8637322187 0.8711323142 0.8385642767 - 0.8641591072 0.8715392947 0.8390334249 - 0.8645859957 0.8719462156 0.8395023942 - 0.8650113940 0.8723505139 0.8399685025 - 0.8654367924 0.8727548122 0.8404346704 - 0.8658621907 0.8731591105 0.8409007788 - 0.8662877083 0.8735634089 0.8413670063 - 0.8667131066 0.8739677072 0.8418331146 - 0.8671352863 0.8743693829 0.8422979712 - 0.8675571084 0.8747707009 0.8427627087 - 0.8679788709 0.8751720190 0.8432275057 - 0.8684006929 0.8755732775 0.8436921835 - 0.8688225150 0.8759745955 0.8441569209 - 0.8692411780 0.8763734102 0.8446184993 - 0.8696581721 0.8767709136 0.8450784087 - 0.8700752854 0.8771684170 0.8455383182 - 0.8704923987 0.8775659204 0.8459982276 - 0.8709095120 0.8779633045 0.8464580774 - 0.8713259101 0.8783591986 0.8469166756 - 0.8717415929 0.8787525892 0.8473734260 - 0.8721572757 0.8791459799 0.8478301764 - 0.8725730181 0.8795393705 0.8482869267 - 0.8729887009 0.8799328208 0.8487436175 - 0.8734040856 0.8803259134 0.8492001295 - 0.8738173246 0.8807154894 0.8496552110 - 0.8742303848 0.8811051250 0.8501102924 - 0.8746436238 0.8814948201 0.8505653739 - 0.8750568032 0.8818843961 0.8510203958 - 0.8754699230 0.8822739720 0.8514754772 - 0.8758795857 0.8826612830 0.8519273996 - 0.8762881756 0.8830478191 0.8523783088 - 0.8766967058 0.8834341764 0.8528292179 - 0.8771052957 0.8838207126 0.8532801270 - 0.8775138855 0.8842071891 0.8537309766 - 0.8779212236 0.8845927715 0.8541811705 - 0.8783264160 0.8849766850 0.8546299934 - 0.8787316084 0.8853605986 0.8550788760 - 0.8791368008 0.8857445121 0.8555276990 - 0.8795419931 0.8861284852 0.8559765816 - 0.8799471855 0.8865123987 0.8564254045 - 0.8803501725 0.8868942857 0.8568723798 - 0.8807529807 0.8872759938 0.8573192954 - 0.8811559081 0.8876577020 0.8577662110 - 0.8815587163 0.8880394101 0.8582130075 - 0.8819615841 0.8884211183 0.8586599231 - 0.8823627830 0.8888016939 0.8591055274 - 0.8827620745 0.8891807795 0.8595495224 - 0.8831614256 0.8895599842 0.8599935174 - 0.8835607171 0.8899391294 0.8604375124 - 0.8839600086 0.8903182745 0.8608815074 - 0.8843594193 0.8906974196 0.8613255024 - 0.8847563863 0.8910734057 0.8617683053 - 0.8851532936 0.8914492130 0.8622109294 - 0.8855502009 0.8918249011 0.8626536131 - 0.8859471083 0.8922005892 0.8630962968 - 0.8863440156 0.8925762773 0.8635389805 - 0.8867403865 0.8929507732 0.8639808893 - 0.8871359229 0.8933228850 0.8644214272 - 0.8875315189 0.8936949968 0.8648619056 - 0.8879269958 0.8940669894 0.8653023839 - 0.8883224726 0.8944391012 0.8657429218 - 0.8887181282 0.8948112130 0.8661834002 - 0.8891111016 0.8951818943 0.8666223288 - 0.8895033002 0.8955522180 0.8670606017 - 0.8898953795 0.8959224224 0.8674988747 - 0.8902875781 0.8962926865 0.8679373264 - 0.8906797171 0.8966628909 0.8683755994 - 0.8910716176 0.8970329762 0.8688138723 - 0.8914600015 0.8974004984 0.8692520857 - 0.8918483853 0.8977680802 0.8696902990 - 0.8922368288 0.8981356025 0.8701285124 - 0.8926252127 0.8985031247 0.8705667257 - 0.8930135965 0.8988707066 0.8710048795 - 0.8934013844 0.8992366195 0.8714421988 - 0.8937880993 0.8995997906 0.8718780279 - 0.8941748738 0.8999629021 0.8723136783 - 0.8945615888 0.9003260732 0.8727493882 - 0.8949483037 0.9006891847 0.8731852174 - 0.8953350782 0.9010522962 0.8736209273 - 0.8957204819 0.9014136791 0.8740552068 - 0.8961051106 0.9017738104 0.8744884133 - 0.8964896798 0.9021338820 0.8749216795 - 0.8968743086 0.9024940729 0.8753550053 - 0.8972588778 0.9028542042 0.8757882714 - 0.8976435065 0.9032142758 0.8762215972 - 0.8980247974 0.9035729766 0.8766545057 - 0.8984053731 0.9039313197 0.8770872951 - 0.8987858891 0.9042896032 0.8775202036 - 0.8991664052 0.9046478868 0.8779529929 - 0.8995469809 0.9050061703 0.8783857822 - 0.8999274969 0.9053645134 0.8788185716 - 0.9003065825 0.9057202935 0.8792489171 - 0.9006856084 0.9060758948 0.8796789050 - 0.9010645747 0.9064316154 0.8801090121 - 0.9014436007 0.9067872167 0.8805390000 - 0.9018226862 0.9071428180 0.8809691072 - 0.9022015929 0.9074983001 0.8813989758 - 0.9025796056 0.9078509808 0.8818262815 - 0.9029576182 0.9082037210 0.8822537065 - 0.9033355713 0.9085562825 0.8826810122 - 0.9037135839 0.9089090228 0.8831083179 - 0.9040915966 0.9092617035 0.8835356236 - 0.9044691920 0.9096143246 0.8839628100 - 0.9048432708 0.9099658132 0.8843894005 - 0.9052175283 0.9103173018 0.8848158717 - 0.9055917263 0.9106687903 0.8852424026 - 0.9059658051 0.9110202789 0.8856688738 - 0.9063400030 0.9113718867 0.8860954046 - 0.9067137241 0.9117231965 0.8865216970 - 0.9070836902 0.9120727777 0.8869451284 - 0.9074537754 0.9124222994 0.8873686194 - 0.9078238010 0.9127718210 0.8877919912 - 0.9081938267 0.9131214023 0.8882154822 - 0.9085637927 0.9134709239 0.8886389732 - 0.9089336991 0.9138203263 0.8890622854 - 0.9093015790 0.9141662121 0.8894826770 - 0.9096695185 0.9145120978 0.8899031281 - 0.9100373983 0.9148579836 0.8903235197 - 0.9104052782 0.9152039289 0.8907439113 - 0.9107732177 0.9155496955 0.8911643028 - 0.9111409783 0.9158955812 0.8915846944 - 0.9115064740 0.9162393212 0.8920040727 - 0.9118716717 0.9165828228 0.8924233913 - 0.9122369885 0.9169263244 0.8928427100 - 0.9126023054 0.9172698259 0.8932620287 - 0.9129676223 0.9176133275 0.8936812282 - 0.9133328795 0.9179568291 0.8941004872 - 0.9136958122 0.9182993770 0.8945177794 - 0.9140580297 0.9186418056 0.8949344754 - 0.9144203067 0.9189841151 0.8953511715 - 0.9147825837 0.9193264842 0.8957679868 - 0.9151449203 0.9196687937 0.8961846828 - 0.9155071974 0.9200112224 0.8966013789 - 0.9158688188 0.9203519225 0.8970177174 - 0.9162300825 0.9206913710 0.8974336982 - 0.9165912867 0.9210308790 0.8978497982 - 0.9169526100 0.9213703871 0.8982657790 - 0.9173138142 0.9217098951 0.8986818790 - 0.9176750779 0.9220494032 0.8990979195 - 0.9180358052 0.9223874211 0.8995137215 - 0.9183956981 0.9227225184 0.8999291062 - 0.9187554717 0.9230574965 0.9003444910 - 0.9191153049 0.9233925939 0.9007598758 - 0.9194750786 0.9237275720 0.9011753201 - 0.9198349118 0.9240627289 0.9015907049 - 0.9201946259 0.9243977070 0.9020060897 - 0.9205502868 0.9247310162 0.9024195075 - 0.9209061265 0.9250643253 0.9028329253 - 0.9212617874 0.9253975749 0.9032462835 - 0.9216175079 0.9257308841 0.9036595821 - 0.9219732285 0.9260641932 0.9040730000 - 0.9223288894 0.9263975024 0.9044864178 - 0.9226825833 0.9267300963 0.9049000144 - 0.9230353236 0.9270622730 0.9053136706 - 0.9233880043 0.9273945093 0.9057273865 - 0.9237406850 0.9277268052 0.9061412215 - 0.9240934253 0.9280589819 0.9065548778 - 0.9244461060 0.9283912182 0.9069685936 - 0.9247984290 0.9287226796 0.9073820710 - 0.9251496196 0.9290521741 0.9077947140 - 0.9255006909 0.9293817282 0.9082072973 - 0.9258518219 0.9297111034 0.9086199999 - 0.9262030125 0.9300405979 0.9090325832 - 0.9265540838 0.9303699732 0.9094452858 - 0.9269052744 0.9306995273 0.9098579288 - 0.9272540808 0.9310278296 0.9102687240 - 0.9276024103 0.9313557148 0.9106789231 - 0.9279507995 0.9316835999 0.9110891223 - 0.9282991290 0.9320114851 0.9114993215 - 0.9286473989 0.9323393106 0.9119095206 - 0.9289957285 0.9326671958 0.9123197198 - 0.9293431044 0.9329950809 0.9127299190 - 0.9296882153 0.9333226085 0.9131399989 - 0.9300333858 0.9336500764 0.9135500789 - 0.9303784966 0.9339777231 0.9139602184 - 0.9307237267 0.9343051910 0.9143702984 - 0.9310687780 0.9346327782 0.9147803783 - 0.9314138889 0.9349603057 0.9151905179 - 0.9317582250 0.9352865219 0.9155995846 - 0.9321020246 0.9356120825 0.9160081744 - 0.9324458241 0.9359377027 0.9164168239 - 0.9327896833 0.9362632036 0.9168254733 - 0.9331334829 0.9365888238 0.9172341228 - 0.9334774017 0.9369143844 0.9176427126 - 0.9338210821 0.9372398257 0.9180511236 - 0.9341632128 0.9375618100 0.9184569716 - 0.9345054030 0.9378839135 0.9188628197 - 0.9348474741 0.9382058978 0.9192686081 - 0.9351897240 0.9385280013 0.9196743965 - 0.9355319142 0.9388499856 0.9200801849 - 0.9358739853 0.9391720891 0.9204859734 - 0.9362149835 0.9394931793 0.9208915830 - 0.9365543127 0.9398130178 0.9212968946 - 0.9368935823 0.9401326776 0.9217022061 - 0.9372329116 0.9404523969 0.9221073985 - 0.9375721216 0.9407721162 0.9225127101 - 0.9379113913 0.9410918951 0.9229180217 - 0.9382507205 0.9414116144 0.9233232737 - 0.9385885000 0.9417304993 0.9237279296 - 0.9389253855 0.9420490861 0.9241321087 - 0.9392623901 0.9423676133 0.9245362282 - 0.9395993948 0.9426862001 0.9249404073 - 0.9399363995 0.9430047274 0.9253445864 - 0.9402732849 0.9433231950 0.9257488251 - 0.9406102896 0.9436417818 0.9261528850 - 0.9409458041 0.9439579844 0.9265553951 - 0.9412810802 0.9442737103 0.9269576073 - 0.9416162968 0.9445894957 0.9273598194 - 0.9419515729 0.9449052811 0.9277619720 - 0.9422867894 0.9452211261 0.9281641841 - 0.9426221251 0.9455367923 0.9285663962 - 0.9429572821 0.9458525777 0.9289686084 - 0.9432895780 0.9461655021 0.9293698072 - 0.9436218739 0.9464783072 0.9297710061 - 0.9439542890 0.9467911720 0.9301720858 - 0.9442865849 0.9471039772 0.9305732846 - 0.9446190000 0.9474169016 0.9309744835 - 0.9449512959 0.9477298260 0.9313756824 - 0.9452832937 0.9480425119 0.9317768216 - 0.9456123710 0.9483544827 0.9321771860 - 0.9459415078 0.9486665130 0.9325776100 - 0.9462705851 0.9489784837 0.9329779148 - 0.9465997815 0.9492905140 0.9333782792 - 0.9469289184 0.9496024847 0.9337787032 - 0.9472579956 0.9499145150 0.9341790080 - 0.9475868940 0.9502264261 0.9345791936 - 0.9479146004 0.9505373836 0.9349784255 - 0.9482423067 0.9508484006 0.9353774786 - 0.9485700130 0.9511592984 0.9357767105 - 0.9488977194 0.9514703155 0.9361758232 - 0.9492254257 0.9517812729 0.9365749955 - 0.9495530128 0.9520921707 0.9369741082 - 0.9498804808 0.9524028897 0.9373731017 - 0.9502063990 0.9527112842 0.9377706051 - 0.9505323172 0.9530196786 0.9381682277 - 0.9508581758 0.9533280730 0.9385656714 - 0.9511839747 0.9536365271 0.9389632940 - 0.9515098929 0.9539449215 0.9393607974 - 0.9518358111 0.9542533755 0.9397584200 - 0.9521614909 0.9545617104 0.9401558042 - 0.9524834752 0.9548670053 0.9405503869 - 0.9528055191 0.9551724195 0.9409449100 - 0.9531275034 0.9554777741 0.9413394928 - 0.9534494281 0.9557831883 0.9417340159 - 0.9537714124 0.9560886025 0.9421285987 - 0.9540933967 0.9563940167 0.9425231218 - 0.9544153214 0.9566993117 0.9429177046 - 0.9547337890 0.9570021033 0.9433081746 - 0.9550518990 0.9573044777 0.9436982870 - 0.9553700089 0.9576069117 0.9440883994 - 0.9556879997 0.9579092860 0.9444785118 - 0.9560061097 0.9582117200 0.9448686242 - 0.9563242197 0.9585140944 0.9452586770 - 0.9566423297 0.9588165283 0.9456487894 - 0.9569590092 0.9591178298 0.9460378289 - 0.9572750926 0.9594184756 0.9464262128 - 0.9575912952 0.9597193003 0.9468145967 - 0.9579073787 0.9600200057 0.9472030997 - 0.9582235217 0.9603207707 0.9475914836 - 0.9585396051 0.9606214762 0.9479799271 - 0.9588558078 0.9609223008 0.9483683705 - 0.9591705203 0.9612216949 0.9487560987 - 0.9594833255 0.9615195990 0.9491428137 - 0.9597961903 0.9618173838 0.9495295882 - 0.9601091146 0.9621152878 0.9499164224 - 0.9604219198 0.9624130726 0.9503030777 - 0.9607347846 0.9627109766 0.9506899118 - 0.9610477090 0.9630088210 0.9510766864 - 0.9613602757 0.9633063078 0.9514631033 - 0.9616711140 0.9636015296 0.9518467784 - 0.9619820118 0.9638965726 0.9522305727 - 0.9622929096 0.9641916752 0.9526143074 - 0.9626036882 0.9644867778 0.9529979825 - 0.9629145861 0.9647819996 0.9533817768 - 0.9632254839 0.9650771022 0.9537655115 - 0.9635363817 0.9653722048 0.9541491866 - 0.9638459086 0.9656658769 0.9545314908 - 0.9641550183 0.9659590721 0.9549131989 - 0.9644640088 0.9662523866 0.9552949071 - 0.9647731185 0.9665455818 0.9556766152 - 0.9650822282 0.9668387771 0.9560583234 - 0.9653912783 0.9671319723 0.9564399719 - 0.9657003284 0.9674252272 0.9568217993 - 0.9660084844 0.9677178264 0.9572029114 - 0.9663146734 0.9680086970 0.9575825930 - 0.9666209221 0.9682996869 0.9579622746 - 0.9669271111 0.9685906768 0.9583420157 - 0.9672331810 0.9688816071 0.9587218165 - 0.9675394297 0.9691725969 0.9591014981 - 0.9678456187 0.9694635272 0.9594811797 - 0.9681518078 0.9697545171 0.9598609209 - 0.9684557915 0.9700437784 0.9602388740 - 0.9687592983 0.9703326225 0.9606165290 - 0.9690628052 0.9706214070 0.9609941244 - 0.9693661928 0.9709103107 0.9613717198 - 0.9696696997 0.9711990952 0.9617493153 - 0.9699732065 0.9714879990 0.9621269107 - 0.9702767134 0.9717767835 0.9625045061 - 0.9705796242 0.9720652103 0.9628816247 - 0.9708809853 0.9723523855 0.9632573724 - 0.9711824059 0.9726396203 0.9636331797 - 0.9714838266 0.9729267955 0.9640089273 - 0.9717851281 0.9732139707 0.9643846750 - 0.9720864892 0.9735012054 0.9647604227 - 0.9723879099 0.9737883806 0.9651361704 - 0.9726892710 0.9740756154 0.9655119181 - 0.9729892015 0.9743614793 0.9658862948 - 0.9732881188 0.9746463895 0.9662597179 - 0.9735869765 0.9749314189 0.9666332006 - 0.9738860130 0.9752163291 0.9670066237 - 0.9741848707 0.9755012989 0.9673801064 - 0.9744839072 0.9757863283 0.9677535295 - 0.9747828245 0.9760711789 0.9681270123 - 0.9750816822 0.9763562083 0.9685003757 - 0.9753785133 0.9766384959 0.9688717127 - 0.9756751060 0.9769206047 0.9692428708 - 0.9759716988 0.9772025943 0.9696140289 - 0.9762682915 0.9774847031 0.9699851871 - 0.9765648842 0.9777666926 0.9703562856 - 0.9768614173 0.9780488014 0.9707275033 - 0.9771580100 0.9783307910 0.9710986018 - 0.9774541855 0.9786124229 0.9714694023 - 0.9777483940 0.9788916707 0.9718384147 - 0.9780427217 0.9791710973 0.9722074270 - 0.9783368707 0.9794504046 0.9725763798 - 0.9786310792 0.9797297120 0.9729453921 - 0.9789252877 0.9800090194 0.9733144045 - 0.9792196155 0.9802883267 0.9736832976 - 0.9795138240 0.9805675745 0.9740523100 - 0.9798069000 0.9808459282 0.9744206071 - 0.9800981283 0.9811221957 0.9747872949 - 0.9803891778 0.9813985825 0.9751541018 - 0.9806804061 0.9816750288 0.9755209088 - 0.9809715152 0.9819512963 0.9758877158 - 0.9812626839 0.9822276831 0.9762545228 - 0.9815537930 0.9825041294 0.9766213298 - 0.9818450212 0.9827803969 0.9769880772 - 0.9821346998 0.9830551147 0.9773538113 - 0.9824227095 0.9833278060 0.9777181149 - 0.9827107191 0.9836006165 0.9780824780 - 0.9829987288 0.9838733077 0.9784469008 - 0.9832866788 0.9841461182 0.9788112044 - 0.9835746884 0.9844188094 0.9791756272 - 0.9838626981 0.9846916199 0.9795399904 - 0.9841507077 0.9849643111 0.9799042940 - 0.9844372869 0.9852353930 0.9802669287 - 0.9847224951 0.9855049849 0.9806277156 - 0.9850078225 0.9857745171 0.9809886217 - 0.9852930903 0.9860441089 0.9813495278 - 0.9855784178 0.9863135815 0.9817103148 - 0.9858636856 0.9865831733 0.9820712209 - 0.9861490130 0.9868527055 0.9824320078 - 0.9864342213 0.9871222973 0.9827929139 - 0.9867178798 0.9873905778 0.9831522703 - 0.9869999886 0.9876576066 0.9835103154 - 0.9872819781 0.9879245758 0.9838681817 - 0.9875640869 0.9881917238 0.9842262268 - 0.9878461957 0.9884586930 0.9845842123 - 0.9881281853 0.9887257218 0.9849420786 - 0.9884102941 0.9889926910 0.9853001237 - 0.9886924028 0.9892597795 0.9856579900 - 0.9889730811 0.9895253778 0.9860141873 - 0.9892522097 0.9897891879 0.9863678813 - 0.9895312190 0.9900529981 0.9867215753 - 0.9898102283 0.9903168082 0.9870752096 - 0.9900891781 0.9905806184 0.9874289036 - 0.9903681874 0.9908443093 0.9877825975 - 0.9906473160 0.9911081195 0.9881362915 - 0.9909263253 0.9913719296 0.9884899259 - 0.9912045002 0.9916349053 0.9888418913 - 0.9914805293 0.9918959737 0.9891892076 - 0.9917563796 0.9921571016 0.9895365238 - 0.9920324087 0.9924181104 0.9898838997 - 0.9923083782 0.9926791787 0.9902312160 - 0.9925844073 0.9929403067 0.9905784726 - 0.9928603768 0.9932013154 0.9909257889 - 0.9931364059 0.9934623837 0.9912732244 - 0.9934120178 0.9937232733 0.9916203022 - 0.9936820865 0.9939817786 0.9919636250 - 0.9939520955 0.9942402244 0.9923068881 - 0.9942222238 0.9944987297 0.9926502705 - 0.9944921732 0.9947571754 0.9929935932 - 0.9947623014 0.9950156212 0.9933369756 - 0.9950323105 0.9952741265 0.9936802983 - 0.9953023791 0.9955325723 0.9940236211 - 0.9955723882 0.9957910180 0.9943670034 - 0.9958366156 0.9960443974 0.9947049022 - 0.9960989952 0.9962962866 0.9950411916 - 0.9963614941 0.9965482950 0.9953776002 - 0.9966238737 0.9968001842 0.9957138896 - 0.9968863726 0.9970520735 0.9960501790 - 0.9971488714 0.9973040223 0.9963865876 - 0.9974113107 0.9975559115 0.9967228770 - 0.9976738095 0.9978078008 0.9970592260 - 0.9979344010 0.9980559945 0.9973912239 - 0.9981926084 0.9982990026 0.9977173209 - 0.9984508157 0.9985420108 0.9980434179 - 0.9987090230 0.9987850189 0.9983695149 - 0.9989671707 0.9990280271 0.9986956120 - 0.9992253780 0.9992709756 0.9990217090 - 0.9994835854 0.9995139837 0.9993478060 - 0.9997417927 0.9997569919 0.9996739030 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0187414307 0.0199415907 0.0138841104 + 0.0330222212 0.0346524492 0.0255766194 + 0.0449542291 0.0472631715 0.0357697308 + 0.0556400903 0.0585798696 0.0450413786 + 0.0654912964 0.0689996779 0.0536506996 + 0.0747707486 0.0787579194 0.0617258511 + 0.0836306289 0.0879753232 0.0693477094 + 0.0921674594 0.0967618302 0.0766232982 + 0.1004453003 0.1051589996 0.0835988298 + 0.1085427999 0.1132052019 0.0902485028 + 0.1164767966 0.1209279969 0.0966249332 + 0.1242111027 0.1283497959 0.1027311981 + 0.1316560954 0.1354517043 0.1085847020 + 0.1387570053 0.1422473937 0.1142328009 + 0.1455107033 0.1487720013 0.1196969002 + 0.1519546062 0.1550467014 0.1249829978 + 0.1581331939 0.1611181051 0.1301161945 + 0.1640816927 0.1669937074 0.1350999027 + 0.1698216051 0.1726770997 0.1399576068 + 0.1753643006 0.1782052964 0.1447027028 + 0.1807104945 0.1835688949 0.1493282020 + 0.1858936995 0.1887871027 0.1538430005 + 0.1909191012 0.1938627958 0.1582642943 + 0.1958166957 0.1988143027 0.1625947952 + 0.2005814016 0.2036329061 0.1668373942 + 0.2052128017 0.2083510011 0.1709935963 + 0.2097522020 0.2129525989 0.1750802994 + 0.2141969949 0.2174569964 0.1790975928 + 0.2185294926 0.2218813002 0.1830428988 + 0.2227485031 0.2262317985 0.1869215071 + 0.2268646061 0.2304809988 0.1907352060 + 0.2308858931 0.2346275002 0.1944852024 + 0.2348421067 0.2386894971 0.1981807947 + 0.2387396991 0.2426760942 0.2018260062 + 0.2425788045 0.2466064990 0.2054218054 + 0.2463494986 0.2504898012 0.2089662999 + 0.2500706017 0.2543140054 0.2124685943 + 0.2537479997 0.2580659986 0.2159260958 + 0.2573781013 0.2617495954 0.2193453014 + 0.2609572113 0.2653856874 0.2227171957 + 0.2644815147 0.2689718008 0.2260479033 + 0.2679471076 0.2725043893 0.2293380946 + 0.2713502049 0.2759802938 0.2325894982 + 0.2746953070 0.2793953121 0.2358044982 + 0.2779825926 0.2827470899 0.2389748991 + 0.2812089920 0.2860386968 0.2421178967 + 0.2843785882 0.2892819047 0.2452265024 + 0.2875038087 0.2924897075 0.2482994944 + 0.2905904055 0.2956646979 0.2513419092 + 0.2936390042 0.2988075912 0.2543503046 + 0.2966530025 0.3019191027 0.2573288083 + 0.2996253073 0.3050011992 0.2602812052 + 0.3025568128 0.3080531061 0.2632032037 + 0.3054536879 0.3110767007 0.2660993040 + 0.3083193004 0.3140729070 0.2689616978 + 0.3111566007 0.3170422912 0.2717953026 + 0.3139680028 0.3199853003 0.2746086121 + 0.3167569935 0.3229024112 0.2773948908 + 0.3195263147 0.3257946074 0.2801466882 + 0.3222785890 0.3286626935 0.2828755975 + 0.3250167072 0.3315044045 0.2855885029 + 0.3277421892 0.3343142867 0.2882772982 + 0.3304488063 0.3370938897 0.2909384072 + 0.3331305087 0.3398444057 0.2935771048 + 0.3357870877 0.3425664902 0.2961899042 + 0.3384202123 0.3452622890 0.2987827063 + 0.3410323858 0.3479284048 0.3013578057 + 0.3436256051 0.3505629897 0.3039124906 + 0.3462015092 0.3531686962 0.3064478934 + 0.3487611115 0.3557462096 0.3089591861 + 0.3513003886 0.3582991958 0.3114418983 + 0.3538151979 0.3608300984 0.3139019012 + 0.3563049138 0.3633398116 0.3163512051 + 0.3587693870 0.3658294976 0.3187870085 + 0.3612132072 0.3682993948 0.3212068081 + 0.3636367023 0.3707447946 0.3236055970 + 0.3660412133 0.3731693029 0.3259812891 + 0.3684293032 0.3755750954 0.3283393085 + 0.3708001971 0.3779630959 0.3306787014 + 0.3731575906 0.3803366125 0.3330000043 + 0.3754999042 0.3826906979 0.3352980912 + 0.3778310120 0.3850283921 0.3375810087 + 0.3801496029 0.3873493969 0.3398480117 + 0.3824588060 0.3896577060 0.3421055973 + 0.3847584128 0.3919534981 0.3443531990 + 0.3870503008 0.3942336142 0.3465890884 + 0.3893356919 0.3964999914 0.3488120139 + 0.3916150033 0.3987481892 0.3510178924 + 0.3938865066 0.4009808004 0.3532077074 + 0.3961476088 0.4031977952 0.3553779125 + 0.3983986080 0.4054017067 0.3575282991 + 0.4006389976 0.4075945020 0.3596653938 + 0.4028654993 0.4097732902 0.3617888093 + 0.4050788879 0.4119321108 0.3638970852 + 0.4072808027 0.4140745103 0.3659907877 + 0.4094718099 0.4162018001 0.3680689037 + 0.4116508067 0.4183138907 0.3701283038 + 0.4138199985 0.4204131961 0.3721699119 + 0.4159773886 0.4225000143 0.3741934001 + 0.4181219041 0.4245725870 0.3761979938 + 0.4202522039 0.4266324937 0.3781862855 + 0.4223701060 0.4286797941 0.3801617920 + 0.4244759083 0.4307178855 0.3821251094 + 0.4265703857 0.4327476919 0.3840759993 + 0.4286541939 0.4347653985 0.3860141933 + 0.4307268858 0.4367690086 0.3879393041 + 0.4327893853 0.4387587011 0.3898504972 + 0.4348411858 0.4407365918 0.3917472959 + 0.4368841052 0.4427039027 0.3936302960 + 0.4389176071 0.4446617961 0.3954991102 + 0.4409425855 0.4466080964 0.3973535895 + 0.4429590106 0.4485442936 0.3991936147 + 0.4449672103 0.4504688084 0.4010188878 + 0.4469676912 0.4523817897 0.4028322101 + 0.4489580095 0.4542838037 0.4046368897 + 0.4509370923 0.4561766982 0.4064337909 + 0.4529055953 0.4580585063 0.4082230031 + 0.4548639059 0.4599294066 0.4100044966 + 0.4568125904 0.4617907107 0.4117785990 + 0.4587523937 0.4636436999 0.4135451913 + 0.4606837034 0.4654872119 0.4153045118 + 0.4626069963 0.4673207998 0.4170565009 + 0.4645206034 0.4691432118 0.4188013971 + 0.4664235115 0.4709551036 0.4205392003 + 0.4683176875 0.4727576971 0.4222700000 + 0.4702025950 0.4745520949 0.4239938855 + 0.4720798135 0.4763393104 0.4257110059 + 0.4739468098 0.4781185091 0.4274213910 + 0.4758000970 0.4798890054 0.4291251004 + 0.4776405096 0.4816485941 0.4308224022 + 0.4794681072 0.4833956063 0.4325130880 + 0.4812825918 0.4851314127 0.4341974854 + 0.4830833077 0.4868572056 0.4358755946 + 0.4848699868 0.4885736108 0.4375469983 + 0.4866446853 0.4902822077 0.4392102063 + 0.4884060025 0.4919806123 0.4408645928 + 0.4901559949 0.4936690032 0.4425103962 + 0.4918906093 0.4953497946 0.4441474974 + 0.4936125875 0.4970211089 0.4457770884 + 0.4953204989 0.4986830056 0.4473989010 + 0.4970144928 0.5003349185 0.4490132928 + 0.4986953139 0.5019772053 0.4506205916 + 0.5003637075 0.5036125779 0.4522204101 + 0.5020195842 0.5052385926 0.4538131952 + 0.5036631227 0.5068594813 0.4553956985 + 0.5052949190 0.5084728003 0.4569694102 + 0.5069147944 0.5100775957 0.4585345089 + 0.5085229874 0.5116732717 0.4600914121 + 0.5101199746 0.5132597089 0.4616405070 + 0.5117021203 0.5148342848 0.4631803036 + 0.5132743120 0.5164015293 0.4647122920 + 0.5148364902 0.5179616809 0.4662373960 + 0.5163900852 0.5195158124 0.4677560031 + 0.5179367065 0.5210649967 0.4692681134 + 0.5194708109 0.5226017237 0.4707722962 + 0.5209966898 0.5241310000 0.4722703099 + 0.5225142241 0.5256524086 0.4737617075 + 0.5240231752 0.5271654725 0.4752461910 + 0.5255233049 0.5286697149 0.4767219126 + 0.5270152092 0.5301663876 0.4781898856 + 0.5284978151 0.5316576958 0.4796513021 + 0.5299711823 0.5331442952 0.4811064899 + 0.5314347148 0.5346261263 0.4825553000 + 0.5328863859 0.5360969901 0.4839963913 + 0.5343297124 0.5375598073 0.4854325056 + 0.5357651711 0.5390148759 0.4868634939 + 0.5371924043 0.5404617190 0.4882858992 + 0.5386118293 0.5419015884 0.4897007942 + 0.5400264859 0.5433354974 0.4911100864 + 0.5414369702 0.5447630286 0.4925136864 + 0.5428367853 0.5461803079 0.4939090908 + 0.5442283750 0.5475915074 0.4952997863 + 0.5456125736 0.5489966869 0.4966861010 + 0.5469890237 0.5503935814 0.4980660975 + 0.5483577847 0.5517833829 0.4994400144 + 0.5497220755 0.5531684160 0.5008088946 + 0.5510820746 0.5545488000 0.5021709800 + 0.5524330139 0.5559220910 0.5035272837 + 0.5537772775 0.5572903156 0.5048794746 + 0.5551130772 0.5586516261 0.5062263012 + 0.5564405918 0.5600048900 0.5075680017 + 0.5577619076 0.5613521934 0.5089064837 + 0.5590745807 0.5626906157 0.5102406740 + 0.5603765249 0.5640223026 0.5115708113 + 0.5616722703 0.5653495193 0.5128980279 + 0.5629596710 0.5666700006 0.5142201185 + 0.5642386079 0.5679866076 0.5155370831 + 0.5655131936 0.5692998171 0.5168507099 + 0.5667812824 0.5706049204 0.5181574225 + 0.5680419207 0.5719022155 0.5194594264 + 0.5692980289 0.5731946230 0.5207582712 + 0.5705438852 0.5744786859 0.5220487118 + 0.5717846751 0.5757588148 0.5233348012 + 0.5730205774 0.5770344734 0.5246167183 + 0.5742480159 0.5783056021 0.5258914828 + 0.5754718184 0.5795726776 0.5271635056 + 0.5766893029 0.5808308721 0.5284305811 + 0.5778992176 0.5820825100 0.5296934247 + 0.5791049004 0.5833303928 0.5309540033 + 0.5802994967 0.5845699906 0.5322073102 + 0.5814914107 0.5858064890 0.5334576964 + 0.5826792121 0.5870373845 0.5347031951 + 0.5838587284 0.5882629156 0.5359424949 + 0.5850344896 0.5894858837 0.5371791124 + 0.5861983895 0.5906990170 0.5384079218 + 0.5873590708 0.5919092894 0.5396336913 + 0.5885140896 0.5931131840 0.5408535004 + 0.5896620154 0.5943115950 0.5420675278 + 0.5908060074 0.5955064297 0.5432779789 + 0.5919439793 0.5966913104 0.5444819927 + 0.5930796266 0.5978732705 0.5456839800 + 0.5942065120 0.5990452170 0.5468795896 + 0.5953297019 0.6002144217 0.5480728745 + 0.5964481235 0.6013793945 0.5492628813 + 0.5975626111 0.6025397182 0.5504478812 + 0.5986750126 0.6036971211 0.5516301990 + 0.5997776985 0.6048492789 0.5528064966 + 0.6008782983 0.6060000062 0.5539814234 + 0.6019700170 0.6071417928 0.5551493764 + 0.6030591130 0.6082807779 0.5563148260 + 0.6041415930 0.6094110012 0.5574722886 + 0.6052209735 0.6105381846 0.5586267710 + 0.6062965989 0.6116611958 0.5597772002 + 0.6073693037 0.6127799749 0.5609225035 + 0.6084402800 0.6138954759 0.5620642900 + 0.6095039845 0.6150087714 0.5632020831 + 0.6105654836 0.6161211729 0.5643385053 + 0.6116194725 0.6172274947 0.5654675961 + 0.6126726270 0.6183326840 0.5665956140 + 0.6137179732 0.6194270253 0.5677173734 + 0.6147624254 0.6205205917 0.5688384175 + 0.6157985926 0.6216076016 0.5699521899 + 0.6168330908 0.6226931810 0.5710644722 + 0.6178600788 0.6237692833 0.5721685290 + 0.6188859940 0.6248437166 0.5732709169 + 0.6199086905 0.6259111166 0.5743672848 + 0.6209291220 0.6269769073 0.5754616857 + 0.6219440103 0.6280378103 0.5765504241 + 0.6229550838 0.6290960908 0.5776373744 + 0.6239585876 0.6301478148 0.5787205100 + 0.6249598265 0.6311972737 0.5798012018 + 0.6259567142 0.6322420239 0.5808768868 + 0.6269508004 0.6332836747 0.5819506049 + 0.6279395223 0.6343185902 0.5830199718 + 0.6289266944 0.6353514194 0.5840864778 + 0.6299108863 0.6363787055 0.5851466060 + 0.6308935285 0.6374046803 0.5862044096 + 0.6318724751 0.6384266019 0.5872561932 + 0.6328493953 0.6394464970 0.5883064270 + 0.6338198781 0.6404585242 0.5893509984 + 0.6347895861 0.6414691806 0.5903946757 + 0.6357561946 0.6424723268 0.5914332867 + 0.6367220879 0.6434748769 0.5924714208 + 0.6376814246 0.6444696784 0.5935050249 + 0.6386405826 0.6454644799 0.5945385098 + 0.6395891905 0.6464499831 0.5955650806 + 0.6405377984 0.6474351883 0.5965915918 + 0.6414811015 0.6484122276 0.5976113081 + 0.6424236894 0.6493877172 0.5986298919 + 0.6433591843 0.6503596902 0.5996431708 + 0.6442922950 0.6513301134 0.6006544828 + 0.6452214122 0.6522970200 0.6016616821 + 0.6461476088 0.6532608867 0.6026659012 + 0.6470720768 0.6542214155 0.6036679745 + 0.6479939222 0.6551764011 0.6046671867 + 0.6489133239 0.6561300755 0.6056650281 + 0.6498240829 0.6570777893 0.6066576838 + 0.6507347226 0.6580253839 0.6076505184 + 0.6516370177 0.6589658856 0.6086354852 + 0.6525393128 0.6599062085 0.6096205115 + 0.6534368992 0.6608443856 0.6105999947 + 0.6543331146 0.6617819071 0.6115779877 + 0.6552255750 0.6627163291 0.6125519276 + 0.6561149955 0.6636475921 0.6135221124 + 0.6570025086 0.6645767093 0.6144911051 + 0.6578854918 0.6654986143 0.6154568195 + 0.6587684155 0.6664205790 0.6164224744 + 0.6596441269 0.6673378944 0.6173813939 + 0.6605197787 0.6682549715 0.6183400750 + 0.6613910794 0.6691666245 0.6192939878 + 0.6622604132 0.6700754762 0.6202455163 + 0.6631278992 0.6709814072 0.6211943030 + 0.6639922857 0.6718816757 0.6221385002 + 0.6648563147 0.6727818251 0.6230823994 + 0.6657119989 0.6736767888 0.6240196228 + 0.6665678024 0.6745718122 0.6249567270 + 0.6674181819 0.6754602790 0.6258906126 + 0.6682661772 0.6763452888 0.6268228889 + 0.6691122055 0.6772282124 0.6277527809 + 0.6699538231 0.6781055927 0.6286767125 + 0.6707953811 0.6789829731 0.6296005249 + 0.6716330051 0.6798558831 0.6305189133 + 0.6724700928 0.6807280779 0.6314365268 + 0.6733044982 0.6815952063 0.6323506832 + 0.6741359830 0.6824558973 0.6332612038 + 0.6749671102 0.6833165288 0.6341714263 + 0.6757913828 0.6841681004 0.6350759864 + 0.6766155958 0.6850196719 0.6359806061 + 0.6774365902 0.6858676076 0.6368827224 + 0.6782553792 0.6867125034 0.6377828121 + 0.6790735126 0.6875569224 0.6386821270 + 0.6798856854 0.6883959174 0.6395736933 + 0.6806979775 0.6892347932 0.6404653192 + 0.6815052032 0.6900711060 0.6413521171 + 0.6823093295 0.6909055114 0.6422356963 + 0.6831128001 0.6917393804 0.6431186795 + 0.6839104891 0.6925656199 0.6439942718 + 0.6847081780 0.6933919191 0.6448698044 + 0.6855018735 0.6942142844 0.6457418203 + 0.6862921715 0.6950333118 0.6466106772 + 0.6870825291 0.6958522201 0.6474797130 + 0.6878690720 0.6966655254 0.6483433247 + 0.6886556149 0.6974784732 0.6492068768 + 0.6894404888 0.6982899904 0.6500678062 + 0.6902230978 0.6990988851 0.6509246230 + 0.6910058260 0.6999077797 0.6517813802 + 0.6917819977 0.7007126808 0.6526325941 + 0.6925566792 0.7015163898 0.6534823179 + 0.6933307052 0.7023192048 0.6543310285 + 0.6941010952 0.7031159997 0.6551743746 + 0.6948714852 0.7039129138 0.6560177207 + 0.6956394911 0.7047072053 0.6568580866 + 0.6964054704 0.7054989934 0.6576957107 + 0.6971715093 0.7062907815 0.6585332751 + 0.6979302764 0.7070775032 0.6593660116 + 0.6986879110 0.7078630924 0.6601977944 + 0.6994447112 0.7086477280 0.6610285044 + 0.7001975179 0.7094256878 0.6618531942 + 0.7009503245 0.7102035880 0.6626778245 + 0.7017009258 0.7109789848 0.6634991765 + 0.7024487853 0.7117509246 0.6643164158 + 0.7031968236 0.7125229239 0.6651335955 + 0.7039384246 0.7132899761 0.6659477949 + 0.7046775222 0.7140548825 0.6667606831 + 0.7054166198 0.7148197889 0.6675736904 + 0.7061507106 0.7155792713 0.6683797836 + 0.7068846226 0.7163382769 0.6691855788 + 0.7076175213 0.7170966268 0.6699900031 + 0.7083460093 0.7178505063 0.6707876921 + 0.7090746164 0.7186043859 0.6715853810 + 0.7098006010 0.7193557024 0.6723811030 + 0.7105224729 0.7201020122 0.6731733084 + 0.7112444043 0.7208483219 0.6739653945 + 0.7119640112 0.7215924263 0.6747537851 + 0.7126817703 0.7223345041 0.6755387783 + 0.7133995891 0.7230767012 0.6763237715 + 0.7141135931 0.7238159776 0.6771062016 + 0.7148258090 0.7245538831 0.6778873205 + 0.7155380249 0.7252917290 0.6786683798 + 0.7162464857 0.7260233164 0.6794440746 + 0.7169541717 0.7267531157 0.6802182794 + 0.7176619172 0.7274829149 0.6809926033 + 0.7183669209 0.7282087207 0.6817620993 + 0.7190716863 0.7289338708 0.6825309992 + 0.7197765708 0.7296590209 0.6832998991 + 0.7204757929 0.7303797007 0.6840652823 + 0.7211748958 0.7311002016 0.6848304868 + 0.7218738794 0.7318205833 0.6855955124 + 0.7225683928 0.7325310707 0.6863517165 + 0.7232629061 0.7332416177 0.6871078014 + 0.7239571810 0.7339518070 0.6878635883 + 0.7246487141 0.7346547842 0.6886121035 + 0.7253403068 0.7353578210 0.6893606782 + 0.7260314226 0.7360606194 0.6901090145 + 0.7267171741 0.7367588282 0.6908516884 + 0.7274029255 0.7374569178 0.6915943027 + 0.7280883789 0.7381548882 0.6923367977 + 0.7287694812 0.7388454080 0.6930751204 + 0.7294505239 0.7395359278 0.6938133836 + 0.7301315069 0.7402263880 0.6945515871 + 0.7308093905 0.7409111261 0.6952871084 + 0.7314873934 0.7415956259 0.6960226297 + 0.7321652770 0.7422801256 0.6967579722 + 0.7328376770 0.7429602146 0.6974878907 + 0.7335097790 0.7436398268 0.6982172132 + 0.7341818213 0.7443194985 0.6989465952 + 0.7348498106 0.7449948192 0.6996716261 + 0.7355170250 0.7456691861 0.7003955841 + 0.7361841202 0.7463434935 0.7011196017 + 0.7368490100 0.7470148206 0.7018402219 + 0.7375128865 0.7476845980 0.7025592923 + 0.7381768227 0.7483543754 0.7032784224 + 0.7388374209 0.7490217090 0.7039942741 + 0.7394952774 0.7496867180 0.7047075033 + 0.7401531935 0.7503517270 0.7054206133 + 0.7408087254 0.7510150075 0.7061315775 + 0.7414602041 0.7516747713 0.7068389058 + 0.7421118021 0.7523347139 0.7075461149 + 0.7427623868 0.7529938221 0.7082526088 + 0.7434083819 0.7536479831 0.7089549899 + 0.7440543175 0.7543023229 0.7096574903 + 0.7447003126 0.7549564838 0.7103599906 + 0.7453430295 0.7556046247 0.7110564113 + 0.7459853888 0.7562519908 0.7117524743 + 0.7466279268 0.7568994761 0.7124484777 + 0.7472671270 0.7575436831 0.7131392956 + 0.7479048967 0.7581862807 0.7138277888 + 0.7485427260 0.7588288784 0.7145162225 + 0.7491782904 0.7594693899 0.7152029276 + 0.7498108149 0.7601066232 0.7158870101 + 0.7504433990 0.7607436776 0.7165712118 + 0.7510756254 0.7613803744 0.7172548175 + 0.7517048120 0.7620109916 0.7179335952 + 0.7523341179 0.7626414895 0.7186123133 + 0.7529633045 0.7632719874 0.7192909718 + 0.7535892129 0.7638990879 0.7199658751 + 0.7542141080 0.7645248771 0.7206395268 + 0.7548390031 0.7651507258 0.7213131785 + 0.7554618716 0.7657744884 0.7219849825 + 0.7560818791 0.7663950920 0.7226539254 + 0.7567018270 0.7670155764 0.7233228087 + 0.7573217154 0.7676361799 0.7239916921 + 0.7579385042 0.7682504058 0.7246547937 + 0.7585554123 0.7688643932 0.7253178954 + 0.7591722012 0.7694783807 0.7259808779 + 0.7597867250 0.7700899839 0.7266423702 + 0.7603995204 0.7706995010 0.7273027897 + 0.7610123158 0.7713090181 0.7279632092 + 0.7616242766 0.7719178796 0.7286229730 + 0.7622311115 0.7725210786 0.7292783856 + 0.7628380060 0.7731242776 0.7299337983 + 0.7634449005 0.7737274170 0.7305892110 + 0.7640485764 0.7743269205 0.7312399149 + 0.7646508217 0.7749242187 0.7318879962 + 0.7652528882 0.7755215168 0.7325360775 + 0.7658544779 0.7761185169 0.7331836224 + 0.7664529085 0.7767131925 0.7338275909 + 0.7670513988 0.7773079872 0.7344716191 + 0.7676498294 0.7779027224 0.7351155877 + 0.7682449222 0.7784941196 0.7357556820 + 0.7688378096 0.7790830731 0.7363932133 + 0.7694306970 0.7796722054 0.7370308042 + 0.7700235248 0.7802610993 0.7376680970 + 0.7706120014 0.7808436155 0.7383009791 + 0.7712004781 0.7814261913 0.7389338017 + 0.7717890739 0.7820088267 0.7395666242 + 0.7723761201 0.7825897932 0.7401980758 + 0.7729613185 0.7831684947 0.7408279777 + 0.7735465169 0.7837471962 0.7414578199 + 0.7741317749 0.7843258977 0.7420876026 + 0.7747126818 0.7849006057 0.7427113056 + 0.7752926946 0.7854743004 0.7433335781 + 0.7758728266 0.7860481143 0.7439559102 + 0.7764521241 0.7866209745 0.7445772886 + 0.7770282030 0.7871890068 0.7451937795 + 0.7776042223 0.7877569795 0.7458102703 + 0.7781801820 0.7883250117 0.7464267015 + 0.7787547708 0.7888916135 0.7470412850 + 0.7793279290 0.7894564271 0.7476536036 + 0.7799009085 0.7900211811 0.7482659817 + 0.7804740071 0.7905859947 0.7488783002 + 0.7810425162 0.7911475897 0.7494871020 + 0.7816095948 0.7917081714 0.7500947714 + 0.7821766734 0.7922688127 0.7507023811 + 0.7827436924 0.7928293943 0.7513099909 + 0.7833052278 0.7933837771 0.7519133091 + 0.7838668227 0.7939382195 0.7525165081 + 0.7844284177 0.7944926023 0.7531198263 + 0.7849892974 0.7950457931 0.7537218928 + 0.7855485082 0.7955945134 0.7543202043 + 0.7861077189 0.7961432934 0.7549185157 + 0.7866668105 0.7966920733 0.7555168867 + 0.7872235775 0.7972388268 0.7561128139 + 0.7877774239 0.7977824807 0.7567057014 + 0.7883312106 0.7983261943 0.7572985291 + 0.7888849974 0.7988699079 0.7578914165 + 0.7894352078 0.7994105816 0.7584812045 + 0.7899829745 0.7999492288 0.7590687871 + 0.7905306816 0.8004878163 0.7596564889 + 0.7910785079 0.8010262847 0.7602441907 + 0.7916234732 0.8015617132 0.7608277798 + 0.7921676040 0.8020955920 0.7614096999 + 0.7927116752 0.8026295900 0.7619916797 + 0.7932558060 0.8031635284 0.7625735998 + 0.7937961221 0.8036941886 0.7631514072 + 0.7943354845 0.8042240739 0.7637282014 + 0.7948749065 0.8047538996 0.7643049955 + 0.7954143286 0.8052837849 0.7648817897 + 0.7959489822 0.8058102131 0.7654550076 + 0.7964830995 0.8063359261 0.7660276294 + 0.7970170975 0.8068616986 0.7666001916 + 0.7975512147 0.8073874712 0.7671728134 + 0.7980824709 0.8079090118 0.7677428126 + 0.7986134887 0.8084300160 0.7683125138 + 0.7991445065 0.8089510202 0.7688822150 + 0.7996755242 0.8094719052 0.7694519162 + 0.8002029061 0.8099884987 0.7700183988 + 0.8007299900 0.8105044961 0.7705844045 + 0.8012570143 0.8110203743 0.7711504102 + 0.8017840981 0.8115363717 0.7717164755 + 0.8023071289 0.8120499849 0.7722783089 + 0.8028293848 0.8125631213 0.7728394866 + 0.8033518195 0.8130761981 0.7734007239 + 0.8038741946 0.8135892749 0.7739619017 + 0.8043946028 0.8140997291 0.7745202780 + 0.8049145937 0.8146092296 0.7750778794 + 0.8054345250 0.8151187897 0.7756354809 + 0.8059545159 0.8156284094 0.7761932015 + 0.8064721823 0.8161345720 0.7767478228 + 0.8069888949 0.8166393042 0.7773010731 + 0.8075056076 0.8171439767 0.7778543830 + 0.8080223203 0.8176487088 0.7784076929 + 0.8085361123 0.8181504011 0.7789580226 + 0.8090476990 0.8186495900 0.7795059085 + 0.8095592856 0.8191487789 0.7800536752 + 0.8100708723 0.8196480274 0.7806016207 + 0.8105810881 0.8201459050 0.7811478972 + 0.8110890985 0.8206415176 0.7816914916 + 0.8115971088 0.8211370707 0.7822352052 + 0.8121051788 0.8216326833 0.7827789187 + 0.8126127720 0.8221275210 0.7833216786 + 0.8131183982 0.8226181865 0.7838603854 + 0.8136240244 0.8231089115 0.7843990922 + 0.8141295910 0.8235995770 0.7849379182 + 0.8146352172 0.8240903020 0.7854766250 + 0.8151367903 0.8245751858 0.7860131264 + 0.8156381249 0.8250597119 0.7865493894 + 0.8161394000 0.8255441785 0.7870855927 + 0.8166406751 0.8260285854 0.7876219153 + 0.8171395063 0.8265116215 0.7881566286 + 0.8176370263 0.8269937038 0.7886903286 + 0.8181344867 0.8274759054 0.7892240882 + 0.8186320066 0.8279579878 0.7897579074 + 0.8191282749 0.8284389973 0.7902898192 + 0.8196223974 0.8289175034 0.7908182740 + 0.8201165795 0.8293960094 0.7913469076 + 0.8206107020 0.8298745155 0.7918754220 + 0.8211048245 0.8303530216 0.7924038768 + 0.8215956092 0.8308256269 0.7929295897 + 0.8220863938 0.8312981129 0.7934553027 + 0.8225771785 0.8317705989 0.7939808965 + 0.8230679035 0.8322430849 0.7945066094 + 0.8235569000 0.8327139020 0.7950305939 + 0.8240448236 0.8331835866 0.7955533862 + 0.8245326281 0.8336532116 0.7960761786 + 0.8250204921 0.8341228962 0.7965989709 + 0.8255078197 0.8345921040 0.7971209884 + 0.8259931803 0.8350595236 0.7976391912 + 0.8264786005 0.8355268240 0.7981575131 + 0.8269640207 0.8359941840 0.7986757159 + 0.8274493814 0.8364614844 0.7991939783 + 0.8279315829 0.8369246721 0.7997112274 + 0.8284127116 0.8373864293 0.8002278805 + 0.8288937807 0.8378480077 0.8007447124 + 0.8293749094 0.8383095860 0.8012614250 + 0.8298550248 0.8387699723 0.8017773032 + 0.8303318024 0.8392266035 0.8022907972 + 0.8308085799 0.8396831751 0.8028041720 + 0.8312854171 0.8401398063 0.8033176064 + 0.8317621946 0.8405963778 0.8038309813 + 0.8322365880 0.8410515785 0.8043408990 + 0.8327100277 0.8415061831 0.8048493266 + 0.8331834078 0.8419607878 0.8053576946 + 0.8336567879 0.8424153924 0.8058661222 + 0.8341296911 0.8428695202 0.8063743114 + 0.8345996737 0.8433197737 0.8068807125 + 0.8350697160 0.8437700868 0.8073871136 + 0.8355396986 0.8442202806 0.8078935146 + 0.8360096812 0.8446705937 0.8083999157 + 0.8364778757 0.8451184034 0.8089042902 + 0.8369445801 0.8455640078 0.8094068766 + 0.8374112248 0.8460094929 0.8099095225 + 0.8378779292 0.8464550972 0.8104121089 + 0.8383445144 0.8469007015 0.8109146953 + 0.8388080001 0.8473442197 0.8114140034 + 0.8392711878 0.8477873802 0.8119127750 + 0.8397343159 0.8482306004 0.8124117255 + 0.8401975036 0.8486738801 0.8129106164 + 0.8406596780 0.8491165042 0.8134090900 + 0.8411186934 0.8495569229 0.8139066100 + 0.8415778279 0.8499972224 0.8144040108 + 0.8420367837 0.8504375815 0.8149014711 + 0.8424957991 0.8508778811 0.8153988719 + 0.8429536819 0.8513162136 0.8158944249 + 0.8434103727 0.8517522812 0.8163877130 + 0.8438671231 0.8521882892 0.8168810010 + 0.8443238139 0.8526242971 0.8173742890 + 0.8447803855 0.8530603051 0.8178675771 + 0.8452352881 0.8534938097 0.8183590770 + 0.8456894159 0.8539261818 0.8188499212 + 0.8461434841 0.8543586731 0.8193408251 + 0.8465976715 0.8547911048 0.8198316097 + 0.8470517993 0.8552235961 0.8203225136 + 0.8475030065 0.8556535244 0.8208127022 + 0.8479539752 0.8560830951 0.8213027716 + 0.8484050035 0.8565127254 0.8217929006 + 0.8488559723 0.8569422960 0.8222830892 + 0.8493067026 0.8573716283 0.8227729201 + 0.8497545719 0.8577967286 0.8232586980 + 0.8502025008 0.8582218885 0.8237444758 + 0.8506504297 0.8586469889 0.8242303133 + 0.8510982990 0.8590722084 0.8247160912 + 0.8515453935 0.8594967723 0.8252013922 + 0.8519896269 0.8599187732 0.8256844878 + 0.8524339199 0.8603408933 0.8261677027 + 0.8528780937 0.8607628942 0.8266509175 + 0.8533223867 0.8611850142 0.8271340728 + 0.8537651896 0.8616067171 0.8276169896 + 0.8542045951 0.8620272875 0.8280991912 + 0.8546440005 0.8624479175 0.8285813928 + 0.8550834060 0.8628684878 0.8290637136 + 0.8555228114 0.8632891178 0.8295459151 + 0.8559610844 0.8637089133 0.8300266862 + 0.8563969135 0.8641266227 0.8305044770 + 0.8568326831 0.8645442724 0.8309822083 + 0.8572685122 0.8649619818 0.8314598799 + 0.8577042818 0.8653796911 0.8319376707 + 0.8581392765 0.8657960892 0.8324145079 + 0.8585724831 0.8662096262 0.8328894973 + 0.8590056896 0.8666229844 0.8333644271 + 0.8594390154 0.8670365214 0.8338394165 + 0.8598722219 0.8674498796 0.8343142867 + 0.8603044152 0.8678625226 0.8347889185 + 0.8607339263 0.8682723045 0.8352627158 + 0.8611633778 0.8686822057 0.8357365131 + 0.8615928888 0.8690921068 0.8362103105 + 0.8620223999 0.8695020080 0.8366839886 + 0.8624514937 0.8699113727 0.8371570110 + 0.8628783822 0.8703184128 0.8376260996 + 0.8633052707 0.8707253933 0.8380951881 + 0.8637322187 0.8711323142 0.8385642767 + 0.8641591072 0.8715392947 0.8390334249 + 0.8645859957 0.8719462156 0.8395023942 + 0.8650113940 0.8723505139 0.8399685025 + 0.8654367924 0.8727548122 0.8404346704 + 0.8658621907 0.8731591105 0.8409007788 + 0.8662877083 0.8735634089 0.8413670063 + 0.8667131066 0.8739677072 0.8418331146 + 0.8671352863 0.8743693829 0.8422979712 + 0.8675571084 0.8747707009 0.8427627087 + 0.8679788709 0.8751720190 0.8432275057 + 0.8684006929 0.8755732775 0.8436921835 + 0.8688225150 0.8759745955 0.8441569209 + 0.8692411780 0.8763734102 0.8446184993 + 0.8696581721 0.8767709136 0.8450784087 + 0.8700752854 0.8771684170 0.8455383182 + 0.8704923987 0.8775659204 0.8459982276 + 0.8709095120 0.8779633045 0.8464580774 + 0.8713259101 0.8783591986 0.8469166756 + 0.8717415929 0.8787525892 0.8473734260 + 0.8721572757 0.8791459799 0.8478301764 + 0.8725730181 0.8795393705 0.8482869267 + 0.8729887009 0.8799328208 0.8487436175 + 0.8734040856 0.8803259134 0.8492001295 + 0.8738173246 0.8807154894 0.8496552110 + 0.8742303848 0.8811051250 0.8501102924 + 0.8746436238 0.8814948201 0.8505653739 + 0.8750568032 0.8818843961 0.8510203958 + 0.8754699230 0.8822739720 0.8514754772 + 0.8758795857 0.8826612830 0.8519273996 + 0.8762881756 0.8830478191 0.8523783088 + 0.8766967058 0.8834341764 0.8528292179 + 0.8771052957 0.8838207126 0.8532801270 + 0.8775138855 0.8842071891 0.8537309766 + 0.8779212236 0.8845927715 0.8541811705 + 0.8783264160 0.8849766850 0.8546299934 + 0.8787316084 0.8853605986 0.8550788760 + 0.8791368008 0.8857445121 0.8555276990 + 0.8795419931 0.8861284852 0.8559765816 + 0.8799471855 0.8865123987 0.8564254045 + 0.8803501725 0.8868942857 0.8568723798 + 0.8807529807 0.8872759938 0.8573192954 + 0.8811559081 0.8876577020 0.8577662110 + 0.8815587163 0.8880394101 0.8582130075 + 0.8819615841 0.8884211183 0.8586599231 + 0.8823627830 0.8888016939 0.8591055274 + 0.8827620745 0.8891807795 0.8595495224 + 0.8831614256 0.8895599842 0.8599935174 + 0.8835607171 0.8899391294 0.8604375124 + 0.8839600086 0.8903182745 0.8608815074 + 0.8843594193 0.8906974196 0.8613255024 + 0.8847563863 0.8910734057 0.8617683053 + 0.8851532936 0.8914492130 0.8622109294 + 0.8855502009 0.8918249011 0.8626536131 + 0.8859471083 0.8922005892 0.8630962968 + 0.8863440156 0.8925762773 0.8635389805 + 0.8867403865 0.8929507732 0.8639808893 + 0.8871359229 0.8933228850 0.8644214272 + 0.8875315189 0.8936949968 0.8648619056 + 0.8879269958 0.8940669894 0.8653023839 + 0.8883224726 0.8944391012 0.8657429218 + 0.8887181282 0.8948112130 0.8661834002 + 0.8891111016 0.8951818943 0.8666223288 + 0.8895033002 0.8955522180 0.8670606017 + 0.8898953795 0.8959224224 0.8674988747 + 0.8902875781 0.8962926865 0.8679373264 + 0.8906797171 0.8966628909 0.8683755994 + 0.8910716176 0.8970329762 0.8688138723 + 0.8914600015 0.8974004984 0.8692520857 + 0.8918483853 0.8977680802 0.8696902990 + 0.8922368288 0.8981356025 0.8701285124 + 0.8926252127 0.8985031247 0.8705667257 + 0.8930135965 0.8988707066 0.8710048795 + 0.8934013844 0.8992366195 0.8714421988 + 0.8937880993 0.8995997906 0.8718780279 + 0.8941748738 0.8999629021 0.8723136783 + 0.8945615888 0.9003260732 0.8727493882 + 0.8949483037 0.9006891847 0.8731852174 + 0.8953350782 0.9010522962 0.8736209273 + 0.8957204819 0.9014136791 0.8740552068 + 0.8961051106 0.9017738104 0.8744884133 + 0.8964896798 0.9021338820 0.8749216795 + 0.8968743086 0.9024940729 0.8753550053 + 0.8972588778 0.9028542042 0.8757882714 + 0.8976435065 0.9032142758 0.8762215972 + 0.8980247974 0.9035729766 0.8766545057 + 0.8984053731 0.9039313197 0.8770872951 + 0.8987858891 0.9042896032 0.8775202036 + 0.8991664052 0.9046478868 0.8779529929 + 0.8995469809 0.9050061703 0.8783857822 + 0.8999274969 0.9053645134 0.8788185716 + 0.9003065825 0.9057202935 0.8792489171 + 0.9006856084 0.9060758948 0.8796789050 + 0.9010645747 0.9064316154 0.8801090121 + 0.9014436007 0.9067872167 0.8805390000 + 0.9018226862 0.9071428180 0.8809691072 + 0.9022015929 0.9074983001 0.8813989758 + 0.9025796056 0.9078509808 0.8818262815 + 0.9029576182 0.9082037210 0.8822537065 + 0.9033355713 0.9085562825 0.8826810122 + 0.9037135839 0.9089090228 0.8831083179 + 0.9040915966 0.9092617035 0.8835356236 + 0.9044691920 0.9096143246 0.8839628100 + 0.9048432708 0.9099658132 0.8843894005 + 0.9052175283 0.9103173018 0.8848158717 + 0.9055917263 0.9106687903 0.8852424026 + 0.9059658051 0.9110202789 0.8856688738 + 0.9063400030 0.9113718867 0.8860954046 + 0.9067137241 0.9117231965 0.8865216970 + 0.9070836902 0.9120727777 0.8869451284 + 0.9074537754 0.9124222994 0.8873686194 + 0.9078238010 0.9127718210 0.8877919912 + 0.9081938267 0.9131214023 0.8882154822 + 0.9085637927 0.9134709239 0.8886389732 + 0.9089336991 0.9138203263 0.8890622854 + 0.9093015790 0.9141662121 0.8894826770 + 0.9096695185 0.9145120978 0.8899031281 + 0.9100373983 0.9148579836 0.8903235197 + 0.9104052782 0.9152039289 0.8907439113 + 0.9107732177 0.9155496955 0.8911643028 + 0.9111409783 0.9158955812 0.8915846944 + 0.9115064740 0.9162393212 0.8920040727 + 0.9118716717 0.9165828228 0.8924233913 + 0.9122369885 0.9169263244 0.8928427100 + 0.9126023054 0.9172698259 0.8932620287 + 0.9129676223 0.9176133275 0.8936812282 + 0.9133328795 0.9179568291 0.8941004872 + 0.9136958122 0.9182993770 0.8945177794 + 0.9140580297 0.9186418056 0.8949344754 + 0.9144203067 0.9189841151 0.8953511715 + 0.9147825837 0.9193264842 0.8957679868 + 0.9151449203 0.9196687937 0.8961846828 + 0.9155071974 0.9200112224 0.8966013789 + 0.9158688188 0.9203519225 0.8970177174 + 0.9162300825 0.9206913710 0.8974336982 + 0.9165912867 0.9210308790 0.8978497982 + 0.9169526100 0.9213703871 0.8982657790 + 0.9173138142 0.9217098951 0.8986818790 + 0.9176750779 0.9220494032 0.8990979195 + 0.9180358052 0.9223874211 0.8995137215 + 0.9183956981 0.9227225184 0.8999291062 + 0.9187554717 0.9230574965 0.9003444910 + 0.9191153049 0.9233925939 0.9007598758 + 0.9194750786 0.9237275720 0.9011753201 + 0.9198349118 0.9240627289 0.9015907049 + 0.9201946259 0.9243977070 0.9020060897 + 0.9205502868 0.9247310162 0.9024195075 + 0.9209061265 0.9250643253 0.9028329253 + 0.9212617874 0.9253975749 0.9032462835 + 0.9216175079 0.9257308841 0.9036595821 + 0.9219732285 0.9260641932 0.9040730000 + 0.9223288894 0.9263975024 0.9044864178 + 0.9226825833 0.9267300963 0.9049000144 + 0.9230353236 0.9270622730 0.9053136706 + 0.9233880043 0.9273945093 0.9057273865 + 0.9237406850 0.9277268052 0.9061412215 + 0.9240934253 0.9280589819 0.9065548778 + 0.9244461060 0.9283912182 0.9069685936 + 0.9247984290 0.9287226796 0.9073820710 + 0.9251496196 0.9290521741 0.9077947140 + 0.9255006909 0.9293817282 0.9082072973 + 0.9258518219 0.9297111034 0.9086199999 + 0.9262030125 0.9300405979 0.9090325832 + 0.9265540838 0.9303699732 0.9094452858 + 0.9269052744 0.9306995273 0.9098579288 + 0.9272540808 0.9310278296 0.9102687240 + 0.9276024103 0.9313557148 0.9106789231 + 0.9279507995 0.9316835999 0.9110891223 + 0.9282991290 0.9320114851 0.9114993215 + 0.9286473989 0.9323393106 0.9119095206 + 0.9289957285 0.9326671958 0.9123197198 + 0.9293431044 0.9329950809 0.9127299190 + 0.9296882153 0.9333226085 0.9131399989 + 0.9300333858 0.9336500764 0.9135500789 + 0.9303784966 0.9339777231 0.9139602184 + 0.9307237267 0.9343051910 0.9143702984 + 0.9310687780 0.9346327782 0.9147803783 + 0.9314138889 0.9349603057 0.9151905179 + 0.9317582250 0.9352865219 0.9155995846 + 0.9321020246 0.9356120825 0.9160081744 + 0.9324458241 0.9359377027 0.9164168239 + 0.9327896833 0.9362632036 0.9168254733 + 0.9331334829 0.9365888238 0.9172341228 + 0.9334774017 0.9369143844 0.9176427126 + 0.9338210821 0.9372398257 0.9180511236 + 0.9341632128 0.9375618100 0.9184569716 + 0.9345054030 0.9378839135 0.9188628197 + 0.9348474741 0.9382058978 0.9192686081 + 0.9351897240 0.9385280013 0.9196743965 + 0.9355319142 0.9388499856 0.9200801849 + 0.9358739853 0.9391720891 0.9204859734 + 0.9362149835 0.9394931793 0.9208915830 + 0.9365543127 0.9398130178 0.9212968946 + 0.9368935823 0.9401326776 0.9217022061 + 0.9372329116 0.9404523969 0.9221073985 + 0.9375721216 0.9407721162 0.9225127101 + 0.9379113913 0.9410918951 0.9229180217 + 0.9382507205 0.9414116144 0.9233232737 + 0.9385885000 0.9417304993 0.9237279296 + 0.9389253855 0.9420490861 0.9241321087 + 0.9392623901 0.9423676133 0.9245362282 + 0.9395993948 0.9426862001 0.9249404073 + 0.9399363995 0.9430047274 0.9253445864 + 0.9402732849 0.9433231950 0.9257488251 + 0.9406102896 0.9436417818 0.9261528850 + 0.9409458041 0.9439579844 0.9265553951 + 0.9412810802 0.9442737103 0.9269576073 + 0.9416162968 0.9445894957 0.9273598194 + 0.9419515729 0.9449052811 0.9277619720 + 0.9422867894 0.9452211261 0.9281641841 + 0.9426221251 0.9455367923 0.9285663962 + 0.9429572821 0.9458525777 0.9289686084 + 0.9432895780 0.9461655021 0.9293698072 + 0.9436218739 0.9464783072 0.9297710061 + 0.9439542890 0.9467911720 0.9301720858 + 0.9442865849 0.9471039772 0.9305732846 + 0.9446190000 0.9474169016 0.9309744835 + 0.9449512959 0.9477298260 0.9313756824 + 0.9452832937 0.9480425119 0.9317768216 + 0.9456123710 0.9483544827 0.9321771860 + 0.9459415078 0.9486665130 0.9325776100 + 0.9462705851 0.9489784837 0.9329779148 + 0.9465997815 0.9492905140 0.9333782792 + 0.9469289184 0.9496024847 0.9337787032 + 0.9472579956 0.9499145150 0.9341790080 + 0.9475868940 0.9502264261 0.9345791936 + 0.9479146004 0.9505373836 0.9349784255 + 0.9482423067 0.9508484006 0.9353774786 + 0.9485700130 0.9511592984 0.9357767105 + 0.9488977194 0.9514703155 0.9361758232 + 0.9492254257 0.9517812729 0.9365749955 + 0.9495530128 0.9520921707 0.9369741082 + 0.9498804808 0.9524028897 0.9373731017 + 0.9502063990 0.9527112842 0.9377706051 + 0.9505323172 0.9530196786 0.9381682277 + 0.9508581758 0.9533280730 0.9385656714 + 0.9511839747 0.9536365271 0.9389632940 + 0.9515098929 0.9539449215 0.9393607974 + 0.9518358111 0.9542533755 0.9397584200 + 0.9521614909 0.9545617104 0.9401558042 + 0.9524834752 0.9548670053 0.9405503869 + 0.9528055191 0.9551724195 0.9409449100 + 0.9531275034 0.9554777741 0.9413394928 + 0.9534494281 0.9557831883 0.9417340159 + 0.9537714124 0.9560886025 0.9421285987 + 0.9540933967 0.9563940167 0.9425231218 + 0.9544153214 0.9566993117 0.9429177046 + 0.9547337890 0.9570021033 0.9433081746 + 0.9550518990 0.9573044777 0.9436982870 + 0.9553700089 0.9576069117 0.9440883994 + 0.9556879997 0.9579092860 0.9444785118 + 0.9560061097 0.9582117200 0.9448686242 + 0.9563242197 0.9585140944 0.9452586770 + 0.9566423297 0.9588165283 0.9456487894 + 0.9569590092 0.9591178298 0.9460378289 + 0.9572750926 0.9594184756 0.9464262128 + 0.9575912952 0.9597193003 0.9468145967 + 0.9579073787 0.9600200057 0.9472030997 + 0.9582235217 0.9603207707 0.9475914836 + 0.9585396051 0.9606214762 0.9479799271 + 0.9588558078 0.9609223008 0.9483683705 + 0.9591705203 0.9612216949 0.9487560987 + 0.9594833255 0.9615195990 0.9491428137 + 0.9597961903 0.9618173838 0.9495295882 + 0.9601091146 0.9621152878 0.9499164224 + 0.9604219198 0.9624130726 0.9503030777 + 0.9607347846 0.9627109766 0.9506899118 + 0.9610477090 0.9630088210 0.9510766864 + 0.9613602757 0.9633063078 0.9514631033 + 0.9616711140 0.9636015296 0.9518467784 + 0.9619820118 0.9638965726 0.9522305727 + 0.9622929096 0.9641916752 0.9526143074 + 0.9626036882 0.9644867778 0.9529979825 + 0.9629145861 0.9647819996 0.9533817768 + 0.9632254839 0.9650771022 0.9537655115 + 0.9635363817 0.9653722048 0.9541491866 + 0.9638459086 0.9656658769 0.9545314908 + 0.9641550183 0.9659590721 0.9549131989 + 0.9644640088 0.9662523866 0.9552949071 + 0.9647731185 0.9665455818 0.9556766152 + 0.9650822282 0.9668387771 0.9560583234 + 0.9653912783 0.9671319723 0.9564399719 + 0.9657003284 0.9674252272 0.9568217993 + 0.9660084844 0.9677178264 0.9572029114 + 0.9663146734 0.9680086970 0.9575825930 + 0.9666209221 0.9682996869 0.9579622746 + 0.9669271111 0.9685906768 0.9583420157 + 0.9672331810 0.9688816071 0.9587218165 + 0.9675394297 0.9691725969 0.9591014981 + 0.9678456187 0.9694635272 0.9594811797 + 0.9681518078 0.9697545171 0.9598609209 + 0.9684557915 0.9700437784 0.9602388740 + 0.9687592983 0.9703326225 0.9606165290 + 0.9690628052 0.9706214070 0.9609941244 + 0.9693661928 0.9709103107 0.9613717198 + 0.9696696997 0.9711990952 0.9617493153 + 0.9699732065 0.9714879990 0.9621269107 + 0.9702767134 0.9717767835 0.9625045061 + 0.9705796242 0.9720652103 0.9628816247 + 0.9708809853 0.9723523855 0.9632573724 + 0.9711824059 0.9726396203 0.9636331797 + 0.9714838266 0.9729267955 0.9640089273 + 0.9717851281 0.9732139707 0.9643846750 + 0.9720864892 0.9735012054 0.9647604227 + 0.9723879099 0.9737883806 0.9651361704 + 0.9726892710 0.9740756154 0.9655119181 + 0.9729892015 0.9743614793 0.9658862948 + 0.9732881188 0.9746463895 0.9662597179 + 0.9735869765 0.9749314189 0.9666332006 + 0.9738860130 0.9752163291 0.9670066237 + 0.9741848707 0.9755012989 0.9673801064 + 0.9744839072 0.9757863283 0.9677535295 + 0.9747828245 0.9760711789 0.9681270123 + 0.9750816822 0.9763562083 0.9685003757 + 0.9753785133 0.9766384959 0.9688717127 + 0.9756751060 0.9769206047 0.9692428708 + 0.9759716988 0.9772025943 0.9696140289 + 0.9762682915 0.9774847031 0.9699851871 + 0.9765648842 0.9777666926 0.9703562856 + 0.9768614173 0.9780488014 0.9707275033 + 0.9771580100 0.9783307910 0.9710986018 + 0.9774541855 0.9786124229 0.9714694023 + 0.9777483940 0.9788916707 0.9718384147 + 0.9780427217 0.9791710973 0.9722074270 + 0.9783368707 0.9794504046 0.9725763798 + 0.9786310792 0.9797297120 0.9729453921 + 0.9789252877 0.9800090194 0.9733144045 + 0.9792196155 0.9802883267 0.9736832976 + 0.9795138240 0.9805675745 0.9740523100 + 0.9798069000 0.9808459282 0.9744206071 + 0.9800981283 0.9811221957 0.9747872949 + 0.9803891778 0.9813985825 0.9751541018 + 0.9806804061 0.9816750288 0.9755209088 + 0.9809715152 0.9819512963 0.9758877158 + 0.9812626839 0.9822276831 0.9762545228 + 0.9815537930 0.9825041294 0.9766213298 + 0.9818450212 0.9827803969 0.9769880772 + 0.9821346998 0.9830551147 0.9773538113 + 0.9824227095 0.9833278060 0.9777181149 + 0.9827107191 0.9836006165 0.9780824780 + 0.9829987288 0.9838733077 0.9784469008 + 0.9832866788 0.9841461182 0.9788112044 + 0.9835746884 0.9844188094 0.9791756272 + 0.9838626981 0.9846916199 0.9795399904 + 0.9841507077 0.9849643111 0.9799042940 + 0.9844372869 0.9852353930 0.9802669287 + 0.9847224951 0.9855049849 0.9806277156 + 0.9850078225 0.9857745171 0.9809886217 + 0.9852930903 0.9860441089 0.9813495278 + 0.9855784178 0.9863135815 0.9817103148 + 0.9858636856 0.9865831733 0.9820712209 + 0.9861490130 0.9868527055 0.9824320078 + 0.9864342213 0.9871222973 0.9827929139 + 0.9867178798 0.9873905778 0.9831522703 + 0.9869999886 0.9876576066 0.9835103154 + 0.9872819781 0.9879245758 0.9838681817 + 0.9875640869 0.9881917238 0.9842262268 + 0.9878461957 0.9884586930 0.9845842123 + 0.9881281853 0.9887257218 0.9849420786 + 0.9884102941 0.9889926910 0.9853001237 + 0.9886924028 0.9892597795 0.9856579900 + 0.9889730811 0.9895253778 0.9860141873 + 0.9892522097 0.9897891879 0.9863678813 + 0.9895312190 0.9900529981 0.9867215753 + 0.9898102283 0.9903168082 0.9870752096 + 0.9900891781 0.9905806184 0.9874289036 + 0.9903681874 0.9908443093 0.9877825975 + 0.9906473160 0.9911081195 0.9881362915 + 0.9909263253 0.9913719296 0.9884899259 + 0.9912045002 0.9916349053 0.9888418913 + 0.9914805293 0.9918959737 0.9891892076 + 0.9917563796 0.9921571016 0.9895365238 + 0.9920324087 0.9924181104 0.9898838997 + 0.9923083782 0.9926791787 0.9902312160 + 0.9925844073 0.9929403067 0.9905784726 + 0.9928603768 0.9932013154 0.9909257889 + 0.9931364059 0.9934623837 0.9912732244 + 0.9934120178 0.9937232733 0.9916203022 + 0.9936820865 0.9939817786 0.9919636250 + 0.9939520955 0.9942402244 0.9923068881 + 0.9942222238 0.9944987297 0.9926502705 + 0.9944921732 0.9947571754 0.9929935932 + 0.9947623014 0.9950156212 0.9933369756 + 0.9950323105 0.9952741265 0.9936802983 + 0.9953023791 0.9955325723 0.9940236211 + 0.9955723882 0.9957910180 0.9943670034 + 0.9958366156 0.9960443974 0.9947049022 + 0.9960989952 0.9962962866 0.9950411916 + 0.9963614941 0.9965482950 0.9953776002 + 0.9966238737 0.9968001842 0.9957138896 + 0.9968863726 0.9970520735 0.9960501790 + 0.9971488714 0.9973040223 0.9963865876 + 0.9974113107 0.9975559115 0.9967228770 + 0.9976738095 0.9978078008 0.9970592260 + 0.9979344010 0.9980559945 0.9973912239 + 0.9981926084 0.9982990026 0.9977173209 + 0.9984508157 0.9985420108 0.9980434179 + 0.9987090230 0.9987850189 0.9983695149 + 0.9989671707 0.9990280271 0.9986956120 + 0.9992253780 0.9992709756 0.9990217090 + 0.9994835854 0.9995139837 0.9993478060 + 0.9997417927 0.9997569919 0.9996739030 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_200.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_200.spi1d index 373a91eb..8777c526 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_200.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_200.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0190759301 0.0202765409 0.0141122499 - 0.0335491784 0.0352048501 0.0259871501 - 0.0456284918 0.0479410589 0.0363161191 - 0.0564305596 0.0593791306 0.0456890501 - 0.0664090514 0.0698935464 0.0543796085 - 0.0757837966 0.0797363967 0.0625370890 - 0.0847331807 0.0890476108 0.0702239573 - 0.0933608115 0.0979082435 0.0775574669 - 0.1017175987 0.1063619033 0.0845922306 - 0.1099015996 0.1144784987 0.0912967026 - 0.1179269999 0.1222776026 0.0977132320 - 0.1257234961 0.1297329068 0.1038620025 - 0.1332069039 0.1368501037 0.1097526997 - 0.1403385997 0.1436758041 0.1154337972 - 0.1471385062 0.1502327025 0.1209276021 - 0.1536355019 0.1565580070 0.1262387037 - 0.1598660052 0.1626631021 0.1313958019 - 0.1658526063 0.1685642004 0.1363987029 - 0.1715987027 0.1742689013 0.1412739009 - 0.1771457046 0.1798011959 0.1460293978 - 0.1825121045 0.1851902008 0.1506731957 - 0.1877036989 0.1904155016 0.1552059054 - 0.1927496940 0.1955112070 0.1596464962 - 0.1976590008 0.2004680932 0.1639927030 - 0.2024216950 0.2053215951 0.1682489067 - 0.2070800066 0.2100495994 0.1724233031 - 0.2116314024 0.2146542072 0.1765283942 - 0.2160694003 0.2191715986 0.1805596054 - 0.2203907967 0.2236115038 0.1845162958 - 0.2246263027 0.2279452980 0.1884078979 - 0.2287762016 0.2321790010 0.1922432035 - 0.2328651994 0.2363519073 0.1960130930 - 0.2368912995 0.2404530942 0.1997288018 - 0.2408396006 0.2444870025 0.2033921927 - 0.2446998060 0.2484596968 0.2069972008 - 0.2484990954 0.2523579001 0.2105585039 - 0.2522394061 0.2561706901 0.2140689939 - 0.2559157014 0.2599090934 0.2175384015 - 0.2595242858 0.2635894120 0.2209592015 - 0.2630606890 0.2672078013 0.2243333012 - 0.2665264904 0.2707605064 0.2276653051 - 0.2699350119 0.2742443085 0.2309532017 - 0.2732954025 0.2776700854 0.2342132032 - 0.2766172886 0.2810553908 0.2374293953 - 0.2799009979 0.2844128013 0.2406121045 - 0.2831490934 0.2877435088 0.2437624931 - 0.2863624096 0.2910458148 0.2468758970 - 0.2895404994 0.2943179011 0.2499558926 - 0.2926771045 0.2975485921 0.2530024946 - 0.2957693934 0.3007347882 0.2560170889 - 0.2988215983 0.3038783967 0.2590034902 - 0.3018361032 0.3069826961 0.2619606853 - 0.3048140109 0.3100473881 0.2648896873 - 0.3077591062 0.3130752146 0.2677848041 - 0.3106676936 0.3160668910 0.2706494033 - 0.3135369122 0.3190228045 0.2734929919 - 0.3163697124 0.3219431043 0.2763096094 - 0.3191707134 0.3248273134 0.2790902853 - 0.3219430149 0.3276773989 0.2818470001 - 0.3246892989 0.3305040896 0.2845870852 - 0.3274120092 0.3333109021 0.2873024046 - 0.3301134109 0.3360993862 0.2899892926 - 0.3327957988 0.3388687074 0.2926526964 - 0.3354612887 0.3416137993 0.2952902913 - 0.3381122947 0.3443362117 0.2979075015 - 0.3407450020 0.3470380008 0.3005059063 - 0.3433603942 0.3497211039 0.3030832112 - 0.3459551930 0.3523826003 0.3056401014 - 0.3485282063 0.3550176919 0.3081726134 - 0.3510803878 0.3576203883 0.3106764853 - 0.3536134064 0.3601945937 0.3131647110 - 0.3561300039 0.3627431095 0.3156422973 - 0.3586291075 0.3652668893 0.3181054890 - 0.3611052930 0.3677676916 0.3205488920 - 0.3635632992 0.3702487946 0.3229709864 - 0.3660041094 0.3727082908 0.3253682852 - 0.3684245944 0.3751434982 0.3277472854 - 0.3708260059 0.3775567114 0.3301078975 - 0.3732075989 0.3799484968 0.3324497938 - 0.3755733967 0.3823226094 0.3347747028 - 0.3779214025 0.3846783042 0.3370811045 - 0.3802560866 0.3870177865 0.3393734097 - 0.3825756013 0.3893364966 0.3416540921 - 0.3848834038 0.3916386962 0.3439235091 - 0.3871786892 0.3939253986 0.3461782932 - 0.3894636929 0.3961988091 0.3484173119 - 0.3917393088 0.3984617889 0.3506391943 - 0.3940058053 0.4007127881 0.3528397083 - 0.3962653875 0.4029492140 0.3550203145 - 0.3985182941 0.4051673114 0.3571819961 - 0.4007658064 0.4073692858 0.3593285978 - 0.4030055106 0.4095577896 0.3614608049 - 0.4052354097 0.4117330909 0.3635764122 - 0.4074552953 0.4138962030 0.3656741083 - 0.4096668959 0.4160450995 0.3677546084 - 0.4118706882 0.4181813896 0.3698169887 - 0.4140650928 0.4203011096 0.3718580902 - 0.4162524045 0.4224058986 0.3738800883 - 0.4184325933 0.4244979024 0.3758884072 - 0.4206056893 0.4265781939 0.3778842986 - 0.4227721095 0.4286450148 0.3798668981 - 0.4249303937 0.4306966066 0.3818354011 - 0.4270783961 0.4327369034 0.3837904930 - 0.4292162061 0.4347665906 0.3857319951 - 0.4313440025 0.4367870986 0.3876594007 - 0.4334617853 0.4388001859 0.3895725906 - 0.4355661869 0.4407989085 0.3914704919 - 0.4376563132 0.4427832961 0.3933526874 - 0.4397337139 0.4447549880 0.3952197134 - 0.4417980909 0.4467160106 0.3970730901 - 0.4438503087 0.4486671090 0.3989152014 - 0.4458903074 0.4506083131 0.4007458985 - 0.4479185045 0.4525383115 0.4025672972 - 0.4499352872 0.4544585049 0.4043807089 - 0.4519383013 0.4563668966 0.4061861932 - 0.4539265931 0.4582642913 0.4079838991 - 0.4559009075 0.4601486921 0.4097737968 - 0.4578616917 0.4620200992 0.4115560949 - 0.4598096013 0.4638800919 0.4133307934 - 0.4617452919 0.4657298028 0.4150981009 - 0.4636693895 0.4675683081 0.4168579876 - 0.4655824006 0.4693951011 0.4186106920 - 0.4674822092 0.4712128043 0.4203560948 - 0.4693683088 0.4730215073 0.4220944047 - 0.4712423086 0.4748198092 0.4238258004 - 0.4731037021 0.4766085148 0.4255501926 - 0.4749540985 0.4783886969 0.4272677004 - 0.4767911136 0.4801594913 0.4289785922 - 0.4786145985 0.4819203913 0.4306828082 - 0.4804255068 0.4836716056 0.4323804080 - 0.4822244942 0.4854123890 0.4340715110 - 0.4840112031 0.4871422946 0.4357562065 - 0.4857847989 0.4888612926 0.4374346137 - 0.4875457883 0.4905700088 0.4391067922 - 0.4892955124 0.4922698140 0.4407728910 - 0.4910339117 0.4939587116 0.4424329102 - 0.4927617908 0.4956364036 0.4440839887 - 0.4944767952 0.4973056912 0.4457261860 - 0.4961803854 0.4989677966 0.4473606944 - 0.4978719950 0.5006220937 0.4489873052 - 0.4995515049 0.5022668839 0.4506061971 - 0.5012195706 0.5039017200 0.4522179067 - 0.5028777719 0.5055288076 0.4538218975 - 0.5045251250 0.5071460009 0.4554184973 - 0.5061631799 0.5087574124 0.4570069015 - 0.5077912807 0.5103607774 0.4585888088 - 0.5094099045 0.5119572282 0.4601627886 - 0.5110192895 0.5135468245 0.4617280960 - 0.5126187801 0.5151286721 0.4632849991 - 0.5142037272 0.5166985989 0.4648320079 - 0.5157784224 0.5182607174 0.4663707018 - 0.5173432231 0.5198156238 0.4679018855 - 0.5188995004 0.5213642120 0.4694260061 - 0.5204483271 0.5229077935 0.4709433019 - 0.5219827294 0.5244423747 0.4724520147 - 0.5235059857 0.5259712934 0.4739539921 - 0.5250201821 0.5274928212 0.4754489958 - 0.5265259743 0.5290061235 0.4769366086 - 0.5280231833 0.5305104256 0.4784151018 - 0.5295122862 0.5320072174 0.4798853099 - 0.5309923887 0.5334973931 0.4813486040 - 0.5324633718 0.5349810123 0.4828054905 - 0.5339248180 0.5364567041 0.4842553139 - 0.5353744030 0.5379219055 0.4856970906 - 0.5368158221 0.5393791795 0.4871335924 - 0.5382496715 0.5408291221 0.4885646999 - 0.5396752954 0.5422707796 0.4899865091 - 0.5410915017 0.5437059999 0.4914005101 - 0.5425012708 0.5451353788 0.4928086102 - 0.5439049006 0.5465584993 0.4942106903 - 0.5452982187 0.5479696989 0.4956040084 - 0.5466837883 0.5493730903 0.4969924986 - 0.5480626225 0.5507693291 0.4983761907 - 0.5494338274 0.5521574020 0.4997530878 - 0.5507962704 0.5535387993 0.5011236072 - 0.5521531105 0.5549157858 0.5024887919 - 0.5535033941 0.5562884808 0.5038468242 - 0.5548453927 0.5576546192 0.5051984787 - 0.5561814904 0.5590159297 0.5065459013 - 0.5575097203 0.5603706837 0.5078874826 - 0.5588302016 0.5617178082 0.5092235804 - 0.5601449013 0.5630592704 0.5105562210 - 0.5614513755 0.5643923283 0.5118840933 - 0.5627481937 0.5657188892 0.5132076144 - 0.5640394092 0.5670406818 0.5145276785 - 0.5653225780 0.5683544874 0.5158423781 - 0.5665981770 0.5696623921 0.5171517730 - 0.5678697228 0.5709664226 0.5184581280 - 0.5691354275 0.5722628236 0.5197597146 - 0.5703939795 0.5735521913 0.5210576057 - 0.5716482997 0.5748370290 0.5223528743 - 0.5728936195 0.5761141777 0.5236395001 - 0.5741339922 0.5773866773 0.5249217153 - 0.5753698945 0.5786542892 0.5261995196 - 0.5765981078 0.5799149871 0.5274701118 - 0.5778229833 0.5811718106 0.5287377834 - 0.5790420771 0.5824205279 0.5300002098 - 0.5802537799 0.5836634040 0.5312584043 - 0.5814616084 0.5849028826 0.5325140953 - 0.5826593041 0.5861350894 0.5337622166 - 0.5838543773 0.5873640776 0.5350074172 - 0.5850455165 0.5885868073 0.5362474918 - 0.5862290263 0.5898029804 0.5374835134 - 0.5874090195 0.5910161138 0.5387175083 - 0.5885779858 0.5922204852 0.5399456024 - 0.5897439718 0.5934225917 0.5411707163 - 0.5909047127 0.5946190953 0.5423896909 - 0.5920587778 0.5958110094 0.5436031222 - 0.5932093859 0.5969995856 0.5448129773 - 0.5943543911 0.5981795192 0.5460162163 - 0.5954970121 0.5993567109 0.5472174883 - 0.5966315269 0.6005250812 0.5484123826 - 0.5977625847 0.6016913056 0.5496050715 - 0.5988891721 0.6028537154 0.5507941842 - 0.6000123024 0.6040124297 0.5519784093 - 0.6011335254 0.6051688194 0.5531598926 - 0.6022430062 0.6063209176 0.5543354154 - 0.6033499241 0.6074718833 0.5555096269 - 0.6044468284 0.6086149812 0.5566766262 - 0.6055412292 0.6097556949 0.5578411222 - 0.6066293716 0.6108884811 0.5589979887 - 0.6077128053 0.6120188832 0.5601516962 - 0.6087905169 0.6131458282 0.5613011718 - 0.6098622084 0.6142669916 0.5624457002 - 0.6109300256 0.6153838038 0.5635867715 - 0.6119914055 0.6164928079 0.5647237897 - 0.6130506992 0.6175984740 0.5658593178 - 0.6141037941 0.6186978221 0.5669875741 - 0.6151558757 0.6197956800 0.5681148171 - 0.6162015200 0.6208844185 0.5692356229 - 0.6172462106 0.6219723821 0.5703557730 - 0.6182832122 0.6230551004 0.5714685917 - 0.6193187833 0.6241365075 0.5725799799 - 0.6203479767 0.6252096891 0.5736833215 - 0.6213756204 0.6262813807 0.5747848749 - 0.6223981977 0.6273471713 0.5758804083 - 0.6234180927 0.6284117103 0.5769739151 - 0.6244314909 0.6294723749 0.5780618191 - 0.6254416108 0.6305295229 0.5791479945 - 0.6264446974 0.6315774918 0.5802301764 - 0.6274456978 0.6326224804 0.5813099742 - 0.6284427047 0.6336600184 0.5823848248 - 0.6294364929 0.6346951723 0.5834575295 - 0.6304243207 0.6357244849 0.5845260024 - 0.6314088106 0.6367521286 0.5855916739 - 0.6323868036 0.6377754211 0.5866507888 - 0.6333630085 0.6387977004 0.5877078176 - 0.6343346238 0.6398169994 0.5887587070 - 0.6353039742 0.6408346295 0.5898079872 - 0.6362662911 0.6418454051 0.5908517241 - 0.6372275949 0.6428552270 0.5918945074 - 0.6381838918 0.6438586712 0.5929322243 - 0.6391395926 0.6448618770 0.5939694047 - 0.6400895119 0.6458585262 0.5950021148 - 0.6410393715 0.6468551755 0.5960347056 - 0.6419798732 0.6478437781 0.5970602036 - 0.6429203153 0.6488320827 0.5980855227 - 0.6438559890 0.6498137116 0.5991042852 - 0.6447910070 0.6507939100 0.6001219749 - 0.6457198262 0.6517695189 0.6011344194 - 0.6466463208 0.6527429819 0.6021447182 - 0.6475692987 0.6537116170 0.6031510234 - 0.6484898925 0.6546760798 0.6041542292 - 0.6494089961 0.6556367874 0.6051554084 - 0.6503260732 0.6565908790 0.6061536074 - 0.6512408853 0.6575438976 0.6071500778 - 0.6521478295 0.6584920287 0.6081401706 - 0.6530545950 0.6594399810 0.6091303229 - 0.6539540291 0.6603807807 0.6101105809 - 0.6548532844 0.6613214016 0.6110907793 - 0.6557484865 0.6622567177 0.6120656133 - 0.6566426754 0.6631903052 0.6130387783 - 0.6575334072 0.6641204953 0.6140081286 - 0.6584212780 0.6650474072 0.6149740219 - 0.6593077183 0.6659724116 0.6159386039 - 0.6601902246 0.6668912768 0.6168997884 - 0.6610727906 0.6678102016 0.6178609729 - 0.6619489789 0.6687254906 0.6188141704 - 0.6628252268 0.6696406007 0.6197673082 - 0.6636962295 0.6705511212 0.6207141280 - 0.6645649076 0.6714593172 0.6216580272 - 0.6654312015 0.6723641157 0.6225993037 - 0.6662933826 0.6732622981 0.6235358715 - 0.6671553850 0.6741604805 0.6244724989 - 0.6680101156 0.6750528216 0.6254026294 - 0.6688649058 0.6759452224 0.6263327003 - 0.6697137952 0.6768317819 0.6272596717 - 0.6705600023 0.6777151823 0.6281852722 - 0.6714038253 0.6785969138 0.6291080713 - 0.6722424030 0.6794738173 0.6300241947 - 0.6730809212 0.6803507209 0.6309403181 - 0.6739163995 0.6812239885 0.6318492293 - 0.6747515202 0.6820967197 0.6327571273 - 0.6755837798 0.6829648018 0.6336619854 - 0.6764129996 0.6838271022 0.6345633268 - 0.6772418022 0.6846892834 0.6354644895 - 0.6780627966 0.6855434775 0.6363605261 - 0.6788836718 0.6863977909 0.6372565031 - 0.6797022223 0.6872488260 0.6381502748 - 0.6805189848 0.6880972981 0.6390423775 - 0.6813352108 0.6889454126 0.6399337053 - 0.6821470261 0.6897872090 0.6408177018 - 0.6829589009 0.6906291246 0.6417016983 - 0.6837666035 0.6914675236 0.6425812244 - 0.6845718026 0.6923034191 0.6434577703 - 0.6853765249 0.6931388974 0.6443338990 - 0.6861765981 0.6939678788 0.6452031136 - 0.6869766116 0.6947969794 0.6460722089 - 0.6877734065 0.6956220865 0.6469382048 - 0.6885675192 0.6964433789 0.6478012800 - 0.6893616915 0.6972646713 0.6486644149 - 0.6901482940 0.6980792880 0.6495230198 - 0.6909348965 0.6988936067 0.6503813863 - 0.6917189956 0.6997050047 0.6512373090 - 0.6924995184 0.7005112767 0.6520892978 - 0.6932801008 0.7013177276 0.6529412866 - 0.6940550804 0.7021173239 0.6537883878 - 0.6948289275 0.7029148936 0.6546339989 - 0.6956019998 0.7037113905 0.6554787755 - 0.6963722706 0.7045007944 0.6563187242 - 0.6971424818 0.7052903175 0.6571586132 - 0.6979110241 0.7060778737 0.6579942703 - 0.6986778975 0.7068635225 0.6588261724 - 0.6994447112 0.7076489925 0.6596580148 - 0.7002053261 0.7084293962 0.6604819894 - 0.7009649873 0.7092086077 0.6613044739 - 0.7017238736 0.7099866867 0.6621260047 - 0.7024794817 0.7107571959 0.6629421115 - 0.7032350898 0.7115277052 0.6637582183 - 0.7039889097 0.7122963071 0.6645715833 - 0.7047405839 0.7130622268 0.6653813124 - 0.7054923177 0.7138280869 0.6661911011 - 0.7062383890 0.7145891786 0.6669985056 - 0.7069823742 0.7153481245 0.6678047180 - 0.7077264190 0.7161070108 0.6686109900 - 0.7084659934 0.7168573141 0.6694114208 - 0.7092055082 0.7176067829 0.6702113748 - 0.7099441886 0.7183555961 0.6710103154 - 0.7106792927 0.7190998197 0.6718034148 - 0.7114142776 0.7198439837 0.6725963950 - 0.7121458054 0.7205855846 0.6733878255 - 0.7128710747 0.7213221788 0.6741760969 - 0.7135964036 0.7220587134 0.6749644876 - 0.7143177986 0.7227926850 0.6757488847 - 0.7150359154 0.7235242128 0.6765298843 - 0.7157539725 0.7242556214 0.6773108244 - 0.7164688706 0.7249854207 0.6780883074 - 0.7171823978 0.7257142067 0.6788641214 - 0.7178958058 0.7264429927 0.6796398163 - 0.7186031938 0.7271656990 0.6804109812 - 0.7193090916 0.7278866172 0.6811808944 - 0.7200149894 0.7286074758 0.6819508076 - 0.7207157016 0.7293236852 0.6827170253 - 0.7214158773 0.7300391793 0.6834827065 - 0.7221161127 0.7307546735 0.6842483878 - 0.7228116989 0.7314674258 0.6850115061 - 0.7235072255 0.7321798801 0.6857745051 - 0.7242025733 0.7328923941 0.6865373254 - 0.7248927951 0.7335966825 0.6872922182 - 0.7255830169 0.7343010902 0.6880471110 - 0.7262728810 0.7350053191 0.6888017058 - 0.7269589901 0.7357040048 0.6895499825 - 0.7276450992 0.7364026904 0.6902983785 - 0.7283307910 0.7371011972 0.6910464764 - 0.7290124297 0.7377966046 0.6917898059 - 0.7296938896 0.7384920120 0.6925331950 - 0.7303751707 0.7391871810 0.6932764053 - 0.7310516238 0.7398741841 0.6940135956 - 0.7317278981 0.7405611873 0.6947507858 - 0.7324042916 0.7412481904 0.6954879761 - 0.7330766916 0.7419282198 0.6962195039 - 0.7337490916 0.7426078916 0.6969509721 - 0.7344214916 0.7432876229 0.6976823807 - 0.7350897193 0.7439622879 0.6984075904 - 0.7357577085 0.7446364164 0.6991322041 - 0.7364255786 0.7453106046 0.6998566985 - 0.7370892763 0.7459799051 0.7005770206 - 0.7377521992 0.7466480136 0.7012962103 - 0.7384151220 0.7473161221 0.7020155191 - 0.7390751839 0.7479804158 0.7027320862 - 0.7397342920 0.7486426830 0.7034475207 - 0.7403932810 0.7493051291 0.7041630149 - 0.7410498261 0.7499645948 0.7048757076 - 0.7417044044 0.7506214976 0.7055861950 - 0.7423589826 0.7512784004 0.7062966228 - 0.7430117130 0.7519339919 0.7070053816 - 0.7436612844 0.7525870800 0.7077112794 - 0.7443109155 0.7532402277 0.7084171772 - 0.7449595928 0.7538927794 0.7091221809 - 0.7456040978 0.7545416951 0.7098222971 - 0.7462486029 0.7551906109 0.7105224729 - 0.7468931079 0.7558395267 0.7112227082 - 0.7475314736 0.7564836144 0.7119165063 - 0.7481697202 0.7571272850 0.7126098871 - 0.7488077879 0.7577710152 0.7133032084 - 0.7494426966 0.7584123015 0.7139921784 - 0.7500761151 0.7590525150 0.7146791220 - 0.7507095933 0.7596926093 0.7153660059 - 0.7513409257 0.7603306770 0.7160515785 - 0.7519693971 0.7609655261 0.7167350054 - 0.7525979280 0.7616001964 0.7174183130 - 0.7532256842 0.7622345090 0.7181012034 - 0.7538480163 0.7628622055 0.7187789083 - 0.7544702888 0.7634899020 0.7194564939 - 0.7550926208 0.7641175985 0.7201341987 - 0.7557113767 0.7647429705 0.7208058834 - 0.7563291788 0.7653676867 0.7214756012 - 0.7569469213 0.7659922838 0.7221453786 - 0.7575628161 0.7666150928 0.7228131890 - 0.7581757903 0.7672348022 0.7234779000 - 0.7587887049 0.7678545117 0.7241426110 - 0.7594016790 0.7684742212 0.7248072028 - 0.7600086927 0.7690863013 0.7254648209 - 0.7606157064 0.7696982026 0.7261223197 - 0.7612227201 0.7703102231 0.7267798185 - 0.7618271708 0.7709196210 0.7274342179 - 0.7624298930 0.7715268731 0.7280861735 - 0.7630326152 0.7721343040 0.7287383080 - 0.7636346817 0.7727413177 0.7293899059 - 0.7642328143 0.7733443975 0.7300382853 - 0.7648310065 0.7739474177 0.7306867242 - 0.7654290795 0.7745504975 0.7313351035 - 0.7660248876 0.7751491070 0.7319790125 - 0.7666196227 0.7757449746 0.7326202989 - 0.7672141790 0.7763409019 0.7332617044 - 0.7678084970 0.7769364119 0.7339023948 - 0.7684006095 0.7775282860 0.7345390916 - 0.7689927816 0.7781202197 0.7351757288 - 0.7695848942 0.7787120938 0.7358123064 - 0.7701737285 0.7793018818 0.7364459038 - 0.7707604170 0.7798901200 0.7370774150 - 0.7713471055 0.7804782987 0.7377089858 - 0.7719334960 0.7810664773 0.7383404970 - 0.7725152969 0.7816480994 0.7389686704 - 0.7730969787 0.7822297812 0.7395969033 - 0.7736787200 0.7828114033 0.7402251959 - 0.7742595077 0.7833915949 0.7408527136 - 0.7748392820 0.7839698792 0.7414792776 - 0.7754191160 0.7845481038 0.7421059012 - 0.7759987712 0.7851263285 0.7427325845 - 0.7765746117 0.7857019901 0.7433542013 - 0.7771494985 0.7862771749 0.7439746261 - 0.7777243853 0.7868523002 0.7445951104 - 0.7782984972 0.7874264121 0.7452148199 - 0.7788689733 0.7879943252 0.7458307743 - 0.7794395089 0.7885621190 0.7464467883 - 0.7800099254 0.7891299725 0.7470628023 - 0.7805796862 0.7896950841 0.7476773858 - 0.7811487913 0.7902569175 0.7482905984 - 0.7817177773 0.7908186913 0.7489038110 - 0.7822868824 0.7913805246 0.7495170236 - 0.7828525901 0.7919393182 0.7501246929 - 0.7834174037 0.7924969792 0.7507306933 - 0.7839820981 0.7930546999 0.7513365746 - 0.7845466733 0.7936124206 0.7519425154 - 0.7851064205 0.7941638231 0.7525429130 - 0.7856659889 0.7947151065 0.7531433105 - 0.7862256169 0.7952665091 0.7537437081 - 0.7867845893 0.7958167791 0.7543430924 - 0.7873414755 0.7963629961 0.7549396157 - 0.7878984809 0.7969092727 0.7555360198 - 0.7884554267 0.7974554896 0.7561324835 - 0.7890108228 0.7980003953 0.7567269802 - 0.7895640731 0.7985432744 0.7573187947 - 0.7901175022 0.7990862727 0.7579106092 - 0.7906708121 0.7996292114 0.7585024834 - 0.7912214994 0.8001691103 0.7590919137 - 0.7917703986 0.8007066250 0.7596797943 - 0.7923192978 0.8012441993 0.7602676153 - 0.7928681970 0.8017817140 0.7608553767 - 0.7934157252 0.8023154140 0.7614399195 - 0.7939624786 0.8028475046 0.7620229721 - 0.7945094109 0.8033794761 0.7626060843 - 0.7950562239 0.8039115071 0.7631891966 - 0.7956007123 0.8044400811 0.7637678981 - 0.7961447239 0.8049678206 0.7643455863 - 0.7966886163 0.8054953814 0.7649232745 - 0.7972326279 0.8060231209 0.7655010223 - 0.7977703810 0.8065471053 0.7660744786 - 0.7983074784 0.8070703745 0.7666473985 - 0.7988445759 0.8075938225 0.7672201991 - 0.7993816733 0.8081170917 0.7677931190 - 0.7999145985 0.8086366057 0.7683627009 - 0.8004472256 0.8091554046 0.7689319849 - 0.8009796739 0.8096743226 0.7695012093 - 0.8015123010 0.8101931810 0.7700704932 - 0.8020427823 0.8107082248 0.7706354260 - 0.8025730848 0.8112226129 0.7711998224 - 0.8031035066 0.8117371202 0.7717642188 - 0.8036338091 0.8122515082 0.7723286152 - 0.8041588068 0.8127616048 0.7728883028 - 0.8046830297 0.8132709265 0.7734473944 - 0.8052071929 0.8137801886 0.7740064263 - 0.8057314754 0.8142895103 0.7745653987 - 0.8062517047 0.8147963285 0.7751224041 - 0.8067709208 0.8153023124 0.7756788135 - 0.8072901964 0.8158082962 0.7762352824 - 0.8078094125 0.8163142800 0.7767916918 - 0.8083264232 0.8168172836 0.7773452997 - 0.8088424802 0.8173186779 0.7778974771 - 0.8093585968 0.8178201914 0.7784497142 - 0.8098745942 0.8183215857 0.7790020108 - 0.8103876710 0.8188199997 0.7795510292 - 0.8108984828 0.8193156123 0.7800974250 - 0.8114091754 0.8198112845 0.7806438208 - 0.8119199872 0.8203068972 0.7811902165 - 0.8124291897 0.8208017945 0.7817354202 - 0.8129357100 0.8212953806 0.7822787166 - 0.8134422898 0.8217890263 0.7828220129 - 0.8139489293 0.8222826719 0.7833654284 - 0.8144549727 0.8227757812 0.7839078903 - 0.8149591088 0.8232660890 0.7844464779 - 0.8154631853 0.8237565160 0.7849851251 - 0.8159673810 0.8242468238 0.7855237722 - 0.8164715171 0.8247371912 0.7860624194 - 0.8169714212 0.8252223730 0.7865952253 - 0.8174710274 0.8257070780 0.7871276140 - 0.8179705739 0.8261917830 0.7876598835 - 0.8184702992 0.8266764879 0.7881922722 - 0.8189675808 0.8271585107 0.7887222767 - 0.8194637895 0.8276389241 0.7892510891 - 0.8199599981 0.8281193972 0.7897799015 - 0.8204562068 0.8285998106 0.7903087139 - 0.8209517002 0.8290792108 0.7908356786 - 0.8214461803 0.8295565248 0.7913593054 - 0.8219406009 0.8300337791 0.7918829918 - 0.8224350214 0.8305110931 0.7924066186 - 0.8229293823 0.8309884071 0.7929303050 - 0.8234202266 0.8314602971 0.7934505939 - 0.8239108920 0.8319321275 0.7939708829 - 0.8244016767 0.8324038982 0.7944911718 - 0.8248924017 0.8328756094 0.7950115204 - 0.8253806829 0.8333445787 0.7955307961 - 0.8258677125 0.8338115215 0.7960494757 - 0.8263546228 0.8342785239 0.7965682149 - 0.8268414736 0.8347455263 0.7970868945 - 0.8273276091 0.8352121115 0.7976047993 - 0.8278108239 0.8356773257 0.7981191874 - 0.8282939792 0.8361424804 0.7986335754 - 0.8287771940 0.8366076946 0.7991480827 - 0.8292604089 0.8370729089 0.7996624708 - 0.8297402859 0.8375352025 0.8001754284 - 0.8302192092 0.8379961848 0.8006876707 - 0.8306980133 0.8384572864 0.8012000918 - 0.8311768770 0.8389183879 0.8017123938 - 0.8316546082 0.8393784761 0.8022242785 - 0.8321290016 0.8398349881 0.8027346134 - 0.8326033950 0.8402916193 0.8032448888 - 0.8330777884 0.8407480717 0.8037552834 - 0.8335521817 0.8412047029 0.8042656183 - 0.8340237737 0.8416597843 0.8047726750 - 0.8344941735 0.8421143293 0.8052784204 - 0.8349646926 0.8425686955 0.8057842255 - 0.8354352117 0.8430231810 0.8062899113 - 0.8359050751 0.8434774280 0.8067952991 - 0.8363721967 0.8439291716 0.8072986007 - 0.8368391991 0.8443809748 0.8078019023 - 0.8373062015 0.8448328972 0.8083050847 - 0.8377732038 0.8452847004 0.8088083863 - 0.8382382989 0.8457334042 0.8093103766 - 0.8387017846 0.8461793065 0.8098112941 - 0.8391653895 0.8466250896 0.8103122711 - 0.8396288753 0.8470708728 0.8108131886 - 0.8400924206 0.8475167751 0.8113141060 - 0.8405531049 0.8479586244 0.8118106723 - 0.8410136104 0.8483998775 0.8123067021 - 0.8414739966 0.8488411903 0.8128026724 - 0.8419345021 0.8492823839 0.8132987022 - 0.8423944116 0.8497232199 0.8137940764 - 0.8428521752 0.8501616716 0.8142873049 - 0.8433101177 0.8506001830 0.8147805929 - 0.8437680006 0.8510386944 0.8152738214 - 0.8442258835 0.8514773250 0.8157669902 - 0.8446823955 0.8519136906 0.8162589073 - 0.8451377153 0.8523477912 0.8167495131 - 0.8455929160 0.8527818918 0.8172399998 - 0.8460481763 0.8532159925 0.8177304864 - 0.8465033770 0.8536500931 0.8182210922 - 0.8469561934 0.8540809155 0.8187078834 - 0.8474081755 0.8545101881 0.8191933036 - 0.8478600979 0.8549395800 0.8196786046 - 0.8483120203 0.8553689718 0.8201640248 - 0.8487640023 0.8557983041 0.8206493258 - 0.8492118716 0.8562254906 0.8211318851 - 0.8496595025 0.8566523790 0.8216142058 - 0.8501071930 0.8570793867 0.8220965266 - 0.8505548239 0.8575062752 0.8225787878 - 0.8510020971 0.8579329848 0.8230608106 - 0.8514466286 0.8583570123 0.8235400915 - 0.8518909812 0.8587809801 0.8240194917 - 0.8523355126 0.8592050076 0.8244987726 - 0.8527799249 0.8596289754 0.8249781132 - 0.8532239199 0.8600522280 0.8254566789 - 0.8536661863 0.8604717851 0.8259320855 - 0.8541085124 0.8608914018 0.8264076114 - 0.8545507789 0.8613110185 0.8268830180 - 0.8549929857 0.8617305756 0.8273584843 - 0.8554344177 0.8621492982 0.8278334141 - 0.8558735251 0.8625652790 0.8283066154 - 0.8563125134 0.8629813194 0.8287798166 - 0.8567516208 0.8633973002 0.8292530179 - 0.8571907282 0.8638132811 0.8297262788 - 0.8576285839 0.8642284870 0.8301985264 - 0.8580638766 0.8646416068 0.8306686282 - 0.8584992886 0.8650546074 0.8311386704 - 0.8589347005 0.8654677272 0.8316087127 - 0.8593699932 0.8658807874 0.8320788145 - 0.8598045707 0.8662930727 0.8325483799 - 0.8602377176 0.8667033911 0.8330168724 - 0.8606708050 0.8671137094 0.8334854245 - 0.8611038923 0.8675240874 0.8339539170 - 0.8615369797 0.8679344058 0.8344224095 - 0.8619691730 0.8683441877 0.8348909020 - 0.8623989820 0.8687524796 0.8353595138 - 0.8628287911 0.8691607118 0.8358281255 - 0.8632586002 0.8695690036 0.8362966180 - 0.8636884093 0.8699771762 0.8367652297 - 0.8641173840 0.8703852892 0.8372331858 - 0.8645430207 0.8707920909 0.8376984000 - 0.8649684787 0.8711990118 0.8381634951 - 0.8653941154 0.8716058731 0.8386287093 - 0.8658195734 0.8720127940 0.8390939236 - 0.8662450910 0.8724194765 0.8395590186 - 0.8666694164 0.8728235960 0.8400223255 - 0.8670938015 0.8732277155 0.8404856920 - 0.8675181866 0.8736317754 0.8409489989 - 0.8679425120 0.8740358949 0.8414124250 - 0.8683668971 0.8744400144 0.8418756723 - 0.8687893152 0.8748406172 0.8423389196 - 0.8692114949 0.8752405047 0.8428019881 - 0.8696336746 0.8756405115 0.8432651758 - 0.8700559139 0.8760403991 0.8437283039 - 0.8704780936 0.8764404058 0.8441913724 - 0.8708974123 0.8768380880 0.8446521759 - 0.8713154197 0.8772345781 0.8451117277 - 0.8717334867 0.8776311278 0.8455712199 - 0.8721514940 0.8780276179 0.8460307717 - 0.8725695014 0.8784241080 0.8464903235 - 0.8729861975 0.8788195252 0.8469489813 - 0.8734011054 0.8792132139 0.8474063873 - 0.8738160133 0.8796069026 0.8478639126 - 0.8742309213 0.8800007105 0.8483213782 - 0.8746458292 0.8803943992 0.8487787843 - 0.8750604987 0.8807877898 0.8492361903 - 0.8754733205 0.8811789751 0.8496932983 - 0.8758860230 0.8815701008 0.8501502872 - 0.8762987852 0.8819612265 0.8506072760 - 0.8767116070 0.8823524117 0.8510643244 - 0.8771244287 0.8827434778 0.8515213132 - 0.8775348067 0.8831323981 0.8519753814 - 0.8779444098 0.8835206032 0.8524286151 - 0.8783540726 0.8839088082 0.8528817892 - 0.8787636757 0.8842970133 0.8533350229 - 0.8791733980 0.8846852183 0.8537881970 - 0.8795822263 0.8850721121 0.8542405963 - 0.8799896836 0.8854566216 0.8546915054 - 0.8803973198 0.8858410716 0.8551424742 - 0.8808047771 0.8862255812 0.8555933833 - 0.8812122941 0.8866102099 0.8560442924 - 0.8816198111 0.8869947195 0.8564953208 - 0.8820263147 0.8873755932 0.8569455743 - 0.8824326992 0.8877562284 0.8573958874 - 0.8828390837 0.8881369233 0.8578462005 - 0.8832455277 0.8885174990 0.8582963943 - 0.8836519122 0.8888980746 0.8587467074 - 0.8840569258 0.8892776966 0.8591951728 - 0.8844602108 0.8896561861 0.8596414924 - 0.8848636150 0.8900346160 0.8600878119 - 0.8852669001 0.8904129863 0.8605340719 - 0.8856703043 0.8907914162 0.8609802723 - 0.8860735893 0.8911697865 0.8614265919 - 0.8864722848 0.8915463090 0.8618707061 - 0.8868706226 0.8919227123 0.8623145819 - 0.8872690201 0.8922989964 0.8627585173 - 0.8876674175 0.8926753998 0.8632022738 - 0.8880656958 0.8930518031 0.8636462092 - 0.8884631991 0.8934273124 0.8640897870 - 0.8888590932 0.8938013911 0.8645328283 - 0.8892549872 0.8941754103 0.8649758101 - 0.8896508813 0.8945494294 0.8654187918 - 0.8900468946 0.8949235082 0.8658617735 - 0.8904427886 0.8952975273 0.8663048148 - 0.8908371925 0.8956699967 0.8667446971 - 0.8912311792 0.8960419893 0.8671836257 - 0.8916252255 0.8964139223 0.8676224947 - 0.8920192122 0.8967859149 0.8680614233 - 0.8924131989 0.8971577883 0.8685001731 - 0.8928068876 0.8975296021 0.8689389229 - 0.8931970000 0.8978990912 0.8693752289 - 0.8935871124 0.8982686996 0.8698114753 - 0.8939772844 0.8986381888 0.8702477813 - 0.8943673968 0.8990076780 0.8706840873 - 0.8947576284 0.8993772268 0.8711203933 - 0.8951466084 0.8997455835 0.8715562820 - 0.8955339193 0.9001119733 0.8719915152 - 0.8959211707 0.9004784226 0.8724268079 - 0.8963084817 0.9008446932 0.8728619814 - 0.8966959119 0.9012110829 0.8732972145 - 0.8970832229 0.9015774131 0.8737323880 - 0.8974692822 0.9019417763 0.8741655946 - 0.8978548050 0.9023047090 0.8745973706 - 0.8982402086 0.9026677012 0.8750292063 - 0.8986256123 0.9030305743 0.8754611015 - 0.8990110755 0.9033935070 0.8758928776 - 0.8993964791 0.9037564993 0.8763247132 - 0.8997789025 0.9041181207 0.8767552972 - 0.9001606107 0.9044793248 0.8771855831 - 0.9005421996 0.9048405290 0.8776159286 - 0.9009239078 0.9052016735 0.8780462146 - 0.9013054967 0.9055628777 0.8784765005 - 0.9016872048 0.9059240818 0.8789067864 - 0.9020649195 0.9062829018 0.8793358207 - 0.9024425149 0.9066416025 0.8797646165 - 0.9028201103 0.9070003033 0.8801934719 - 0.9031975865 0.9073588848 0.8806223869 - 0.9035751820 0.9077175856 0.8810513020 - 0.9039525986 0.9080762267 0.8814799786 - 0.9043281078 0.9084306955 0.8819060922 - 0.9047036171 0.9087852836 0.8823322058 - 0.9050791264 0.9091398716 0.8827583194 - 0.9054545760 0.9094944000 0.8831843734 - 0.9058300853 0.9098489881 0.8836104274 - 0.9062054157 0.9102032781 0.8840364814 - 0.9065783024 0.9105550051 0.8844619989 - 0.9069511890 0.9109066725 0.8848875165 - 0.9073241949 0.9112585187 0.8853129148 - 0.9076970816 0.9116101861 0.8857383728 - 0.9080700874 0.9119619131 0.8861638904 - 0.9084427953 0.9123135209 0.8865892291 - 0.9088128209 0.9126635194 0.8870128989 - 0.9091827869 0.9130135179 0.8874365091 - 0.9095528126 0.9133635163 0.8878601789 - 0.9099227786 0.9137135148 0.8882837892 - 0.9102928042 0.9140635133 0.8887075186 - 0.9106627107 0.9144133925 0.8891310096 - 0.9110317826 0.9147610068 0.8895515203 - 0.9114007950 0.9151085019 0.8899719119 - 0.9117698073 0.9154561162 0.8903924227 - 0.9121388793 0.9158036113 0.8908128738 - 0.9125078917 0.9161511064 0.8912333250 - 0.9128769040 0.9164987206 0.8916537762 - 0.9132447243 0.9168428779 0.8920732737 - 0.9136124253 0.9171867967 0.8924927115 - 0.9139801264 0.9175307751 0.8929122090 - 0.9143477082 0.9178748131 0.8933315873 - 0.9147154093 0.9182186723 0.8937510848 - 0.9150831103 0.9185627103 0.8941705227 - 0.9154474735 0.9189047813 0.8945881724 - 0.9158111215 0.9192463756 0.8950055242 - 0.9161747098 0.9195880294 0.8954226971 - 0.9165382981 0.9199296236 0.8958399296 - 0.9169018865 0.9202712178 0.8962571025 - 0.9172654748 0.9206128120 0.8966743946 - 0.9176265001 0.9209533930 0.8970903158 - 0.9179857969 0.9212933183 0.8975052834 - 0.9183449745 0.9216331244 0.8979204297 - 0.9187042117 0.9219729900 0.8983353972 - 0.9190635085 0.9223129153 0.8987504244 - 0.9194226861 0.9226527810 0.8991655111 - 0.9197812080 0.9229915738 0.8995802999 - 0.9201380014 0.9233281016 0.8999949098 - 0.9204949141 0.9236646891 0.9004095197 - 0.9208517075 0.9240012169 0.9008241296 - 0.9212086201 0.9243378043 0.9012386799 - 0.9215654135 0.9246743917 0.9016532898 - 0.9219222069 0.9250109196 0.9020677805 - 0.9222760797 0.9253453016 0.9024798870 - 0.9226300716 0.9256798029 0.9028918743 - 0.9229841232 0.9260141850 0.9033039808 - 0.9233379960 0.9263486862 0.9037160873 - 0.9236919880 0.9266831279 0.9041281939 - 0.9240459800 0.9270175099 0.9045401812 - 0.9243978262 0.9273512959 0.9049506783 - 0.9247484803 0.9276847839 0.9053602219 - 0.9250993133 0.9280182719 0.9057698250 - 0.9254500270 0.9283518195 0.9061794281 - 0.9258008003 0.9286853075 0.9065889716 - 0.9261515141 0.9290186763 0.9069985747 - 0.9265019298 0.9293516278 0.9074079990 - 0.9268512726 0.9296824932 0.9078168273 - 0.9272007942 0.9300134182 0.9082257152 - 0.9275501966 0.9303442836 0.9086344838 - 0.9278995991 0.9306753278 0.9090433121 - 0.9282490015 0.9310061932 0.9094521999 - 0.9285985231 0.9313371181 0.9098610282 - 0.9289464951 0.9316648245 0.9102689028 - 0.9292942286 0.9319915771 0.9106765985 - 0.9296419024 0.9323183894 0.9110842943 - 0.9299895763 0.9326452017 0.9114919901 - 0.9303374290 0.9329720736 0.9118996859 - 0.9306851029 0.9332988858 0.9123073816 - 0.9310320020 0.9336249232 0.9127150178 - 0.9313766956 0.9339488745 0.9131222963 - 0.9317213893 0.9342728257 0.9135295749 - 0.9320660830 0.9345967770 0.9139369130 - 0.9324107766 0.9349207878 0.9143441916 - 0.9327554703 0.9352446795 0.9147515297 - 0.9331002235 0.9355686903 0.9151586890 - 0.9334433079 0.9358913898 0.9155651927 - 0.9337856770 0.9362136126 0.9159712195 - 0.9341281056 0.9365357757 0.9163771868 - 0.9344704747 0.9368579984 0.9167832136 - 0.9348127842 0.9371802211 0.9171893001 - 0.9351552129 0.9375023842 0.9175953269 - 0.9354975224 0.9378244281 0.9180011749 - 0.9358382225 0.9381431937 0.9184055924 - 0.9361788034 0.9384620786 0.9188100100 - 0.9365195036 0.9387809038 0.9192144275 - 0.9368602037 0.9390997291 0.9196187854 - 0.9372009039 0.9394186139 0.9200232029 - 0.9375416040 0.9397373796 0.9204276204 - 0.9378811121 0.9400548935 0.9208316207 - 0.9382188916 0.9403703213 0.9212350845 - 0.9385566115 0.9406856894 0.9216386080 - 0.9388942719 0.9410011172 0.9220420718 - 0.9392319918 0.9413164854 0.9224454761 - 0.9395697117 0.9416319132 0.9228489995 - 0.9399074912 0.9419472814 0.9232525229 - 0.9402431250 0.9422618151 0.9236547947 - 0.9405775070 0.9425758123 0.9240565896 - 0.9409120083 0.9428898096 0.9244583249 - 0.9412463903 0.9432038069 0.9248600006 - 0.9415808916 0.9435179234 0.9252616763 - 0.9419152737 0.9438319206 0.9256634116 - 0.9422497749 0.9441459179 0.9260650873 - 0.9425830245 0.9444586039 0.9264650941 - 0.9429159760 0.9447709918 0.9268646836 - 0.9432489872 0.9450834990 0.9272642732 - 0.9435819983 0.9453958869 0.9276639223 - 0.9439150095 0.9457083941 0.9280635118 - 0.9442480206 0.9460207820 0.9284631014 - 0.9445809722 0.9463332891 0.9288625717 - 0.9449120760 0.9466425776 0.9292607903 - 0.9452432990 0.9469519854 0.9296590090 - 0.9455745220 0.9472612739 0.9300572276 - 0.9459056258 0.9475706816 0.9304553866 - 0.9462367892 0.9478800297 0.9308536053 - 0.9465680122 0.9481893778 0.9312518239 - 0.9468988180 0.9484986067 0.9316499233 - 0.9472268820 0.9488059878 0.9320474863 - 0.9475550056 0.9491134286 0.9324449897 - 0.9478831887 0.9494208097 0.9328426123 - 0.9482113123 0.9497281909 0.9332401156 - 0.9485393763 0.9500355721 0.9336376190 - 0.9488674998 0.9503430128 0.9340351820 - 0.9491953254 0.9506502748 0.9344326854 - 0.9495210052 0.9509568214 0.9348298907 - 0.9498466849 0.9512634277 0.9352269769 - 0.9501724243 0.9515699744 0.9356241822 - 0.9504981041 0.9518765211 0.9360213876 - 0.9508237839 0.9521831274 0.9364185929 - 0.9511495233 0.9524896741 0.9368157983 - 0.9514749050 0.9527959824 0.9372128248 - 0.9517980814 0.9531002045 0.9376080036 - 0.9521211982 0.9534044266 0.9380033016 - 0.9524443150 0.9537085891 0.9383985996 - 0.9527673721 0.9540126920 0.9387937784 - 0.9530906081 0.9543169141 0.9391890764 - 0.9534137249 0.9546210766 0.9395843744 - 0.9537366033 0.9549251795 0.9399794936 - 0.9540542960 0.9552258849 0.9403715134 - 0.9543719888 0.9555265903 0.9407635927 - 0.9546896815 0.9558274150 0.9411556125 - 0.9550073743 0.9561281204 0.9415475726 - 0.9553251266 0.9564288259 0.9419395924 - 0.9556428790 0.9567294717 0.9423316717 - 0.9559605718 0.9570301771 0.9427236915 - 0.9562745094 0.9573286772 0.9431132078 - 0.9565880895 0.9576269984 0.9435023069 - 0.9569017291 0.9579253197 0.9438915253 - 0.9572151899 0.9582235813 0.9442806244 - 0.9575288296 0.9585217834 0.9446697831 - 0.9578424096 0.9588201046 0.9450588822 - 0.9581558704 0.9591184258 0.9454481006 - 0.9584683776 0.9594156742 0.9458364844 - 0.9587804079 0.9597126842 0.9462245703 - 0.9590924978 0.9600095749 0.9466127157 - 0.9594045281 0.9603065252 0.9470008016 - 0.9597164989 0.9606034756 0.9473888874 - 0.9600285888 0.9609004259 0.9477769732 - 0.9603406191 0.9611973166 0.9481651187 - 0.9606512785 0.9614930749 0.9485527873 - 0.9609603882 0.9617875218 0.9489399791 - 0.9612696171 0.9620819092 0.9493271708 - 0.9615787268 0.9623762965 0.9497144818 - 0.9618877769 0.9626706839 0.9501016736 - 0.9621970057 0.9629650712 0.9504889250 - 0.9625061154 0.9632595181 0.9508761168 - 0.9628148079 0.9635536075 0.9512630105 - 0.9631211162 0.9638456106 0.9516479969 - 0.9634273052 0.9641376138 0.9520329833 - 0.9637334943 0.9644294977 0.9524180293 - 0.9640396833 0.9647215009 0.9528030157 - 0.9643458724 0.9650135040 0.9531880021 - 0.9646521211 0.9653053880 0.9535729885 - 0.9649583101 0.9655973911 0.9539579153 - 0.9652628899 0.9658880830 0.9543418884 - 0.9655668736 0.9661784768 0.9547253847 - 0.9658709168 0.9664688110 0.9551090002 - 0.9661749005 0.9667590857 0.9554926157 - 0.9664788246 0.9670494199 0.9558761120 - 0.9667828083 0.9673396945 0.9562597275 - 0.9670867920 0.9676300287 0.9566432834 - 0.9673900008 0.9679197073 0.9570264220 - 0.9676913023 0.9682080150 0.9574084878 - 0.9679926038 0.9684962034 0.9577906132 - 0.9682939053 0.9687843919 0.9581726789 - 0.9685953259 0.9690726995 0.9585548043 - 0.9688966274 0.9693608880 0.9589369297 - 0.9691979289 0.9696490765 0.9593191147 - 0.9694991708 0.9699373841 0.9597011805 - 0.9697983861 0.9702233076 0.9600812197 - 0.9700970054 0.9705085158 0.9604607821 - 0.9703956246 0.9707937837 0.9608402848 - 0.9706941843 0.9710791111 0.9612197876 - 0.9709929228 0.9713643789 0.9615994096 - 0.9712914824 0.9716497064 0.9619789124 - 0.9715901017 0.9719349742 0.9623584151 - 0.9718881845 0.9722197056 0.9627375007 - 0.9721844792 0.9725024104 0.9631149769 - 0.9724807143 0.9727851152 0.9634925127 - 0.9727770090 0.9730678201 0.9638699889 - 0.9730733037 0.9733504057 0.9642475247 - 0.9733694792 0.9736331105 0.9646250010 - 0.9736657739 0.9739158154 0.9650024772 - 0.9739621282 0.9741985202 0.9653798938 - 0.9742565155 0.9744799733 0.9657559991 - 0.9745497704 0.9747607112 0.9661310911 - 0.9748430252 0.9750415087 0.9665063024 - 0.9751362801 0.9753221869 0.9668813944 - 0.9754294753 0.9756029248 0.9672564864 - 0.9757227898 0.9758836031 0.9676316977 - 0.9760159850 0.9761644006 0.9680067897 - 0.9763092995 0.9764450788 0.9683818817 - 0.9765990973 0.9767239094 0.9687542915 - 0.9768885970 0.9770026207 0.9691264033 - 0.9771782160 0.9772812128 0.9694985747 - 0.9774677157 0.9775598049 0.9698706865 - 0.9777572751 0.9778385162 0.9702427983 - 0.9780467749 0.9781171083 0.9706150293 - 0.9783363938 0.9783958197 0.9709870815 - 0.9786252975 0.9786741138 0.9713587761 - 0.9789113998 0.9789509177 0.9717280865 - 0.9791973829 0.9792277217 0.9720973969 - 0.9794834852 0.9795044065 0.9724667072 - 0.9797695279 0.9797812104 0.9728360176 - 0.9800555110 0.9800580144 0.9732053280 - 0.9803416133 0.9803346992 0.9735746980 - 0.9806275964 0.9806115031 0.9739440084 - 0.9809123874 0.9808875918 0.9743121266 - 0.9811949134 0.9811623096 0.9746779799 - 0.9814773798 0.9814370275 0.9750438929 - 0.9817599058 0.9817116857 0.9754099250 - 0.9820423722 0.9819864035 0.9757757783 - 0.9823248982 0.9822611213 0.9761416912 - 0.9826074243 0.9825357795 0.9765076041 - 0.9828898907 0.9828104973 0.9768735170 - 0.9831706285 0.9830839038 0.9772381186 - 0.9834492803 0.9833558202 0.9776011109 - 0.9837279916 0.9836276770 0.9779641032 - 0.9840067029 0.9838995934 0.9783270955 - 0.9842854738 0.9841713905 0.9786900282 - 0.9845641851 0.9844433069 0.9790530205 - 0.9848428965 0.9847152233 0.9794160128 - 0.9851216078 0.9849870801 0.9797790051 - 0.9853982925 0.9852576256 0.9801405072 - 0.9856731296 0.9855268002 0.9805005789 - 0.9859479070 0.9857959747 0.9808607101 - 0.9862228036 0.9860652089 0.9812209010 - 0.9864975810 0.9863345027 0.9815809727 - 0.9867724180 0.9866036773 0.9819411039 - 0.9870471954 0.9868729115 0.9823011756 - 0.9873220921 0.9871420860 0.9826613069 - 0.9875941277 0.9874101877 0.9830198288 - 0.9878634810 0.9876769781 0.9833766818 - 0.9881328940 0.9879438281 0.9837337136 - 0.9884023070 0.9882106781 0.9840906262 - 0.9886716008 0.9884775281 0.9844474792 - 0.9889410138 0.9887443185 0.9848043919 - 0.9892104268 0.9890111089 0.9851613045 - 0.9894797802 0.9892780185 0.9855182171 - 0.9897462130 0.9895430803 0.9858739972 - 0.9900087714 0.9898058772 0.9862282276 - 0.9902712703 0.9900686741 0.9865823984 - 0.9905338287 0.9903314710 0.9869366288 - 0.9907963276 0.9905943274 0.9872906804 - 0.9910588264 0.9908571243 0.9876449108 - 0.9913213849 0.9911199212 0.9879990816 - 0.9915838838 0.9913827181 0.9883533120 - 0.9918448925 0.9916440845 0.9887064099 - 0.9921017885 0.9919016957 0.9890565872 - 0.9923588037 0.9921593070 0.9894067049 - 0.9926156998 0.9924169779 0.9897568822 - 0.9928725958 0.9926745892 0.9901071191 - 0.9931296110 0.9929322004 0.9904572964 - 0.9933865070 0.9931898713 0.9908074141 - 0.9936434031 0.9934474826 0.9911575913 - 0.9939001203 0.9937049747 0.9915075898 - 0.9941518903 0.9939597249 0.9918552041 - 0.9944037795 0.9942144156 0.9922026992 - 0.9946557283 0.9944692254 0.9925501943 - 0.9949074984 0.9947239161 0.9928978086 - 0.9951593876 0.9949786067 0.9932453036 - 0.9954112172 0.9952334166 0.9935927987 - 0.9956631064 0.9954881072 0.9939404130 - 0.9959149957 0.9957427979 0.9942879081 - 0.9961606860 0.9959961772 0.9946306944 - 0.9964045882 0.9962491989 0.9949722290 - 0.9966484904 0.9965021014 0.9953135848 - 0.9968923926 0.9967551231 0.9956551194 - 0.9971364141 0.9970080256 0.9959965944 - 0.9973803163 0.9972609878 0.9963380098 - 0.9976242185 0.9975138903 0.9966794848 - 0.9978681207 0.9977669120 0.9970210195 - 0.9981089234 0.9980177283 0.9973577261 - 0.9983453155 0.9982655048 0.9976879954 - 0.9985817075 0.9985132813 0.9980183244 - 0.9988179803 0.9987609982 0.9983485937 - 0.9990543723 0.9990087748 0.9986789227 - 0.9992908239 0.9992566109 0.9990090728 - 0.9995272160 0.9995043874 0.9993394017 - 0.9997636080 0.9997522235 0.9996696711 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0190759301 0.0202765409 0.0141122499 + 0.0335491784 0.0352048501 0.0259871501 + 0.0456284918 0.0479410589 0.0363161191 + 0.0564305596 0.0593791306 0.0456890501 + 0.0664090514 0.0698935464 0.0543796085 + 0.0757837966 0.0797363967 0.0625370890 + 0.0847331807 0.0890476108 0.0702239573 + 0.0933608115 0.0979082435 0.0775574669 + 0.1017175987 0.1063619033 0.0845922306 + 0.1099015996 0.1144784987 0.0912967026 + 0.1179269999 0.1222776026 0.0977132320 + 0.1257234961 0.1297329068 0.1038620025 + 0.1332069039 0.1368501037 0.1097526997 + 0.1403385997 0.1436758041 0.1154337972 + 0.1471385062 0.1502327025 0.1209276021 + 0.1536355019 0.1565580070 0.1262387037 + 0.1598660052 0.1626631021 0.1313958019 + 0.1658526063 0.1685642004 0.1363987029 + 0.1715987027 0.1742689013 0.1412739009 + 0.1771457046 0.1798011959 0.1460293978 + 0.1825121045 0.1851902008 0.1506731957 + 0.1877036989 0.1904155016 0.1552059054 + 0.1927496940 0.1955112070 0.1596464962 + 0.1976590008 0.2004680932 0.1639927030 + 0.2024216950 0.2053215951 0.1682489067 + 0.2070800066 0.2100495994 0.1724233031 + 0.2116314024 0.2146542072 0.1765283942 + 0.2160694003 0.2191715986 0.1805596054 + 0.2203907967 0.2236115038 0.1845162958 + 0.2246263027 0.2279452980 0.1884078979 + 0.2287762016 0.2321790010 0.1922432035 + 0.2328651994 0.2363519073 0.1960130930 + 0.2368912995 0.2404530942 0.1997288018 + 0.2408396006 0.2444870025 0.2033921927 + 0.2446998060 0.2484596968 0.2069972008 + 0.2484990954 0.2523579001 0.2105585039 + 0.2522394061 0.2561706901 0.2140689939 + 0.2559157014 0.2599090934 0.2175384015 + 0.2595242858 0.2635894120 0.2209592015 + 0.2630606890 0.2672078013 0.2243333012 + 0.2665264904 0.2707605064 0.2276653051 + 0.2699350119 0.2742443085 0.2309532017 + 0.2732954025 0.2776700854 0.2342132032 + 0.2766172886 0.2810553908 0.2374293953 + 0.2799009979 0.2844128013 0.2406121045 + 0.2831490934 0.2877435088 0.2437624931 + 0.2863624096 0.2910458148 0.2468758970 + 0.2895404994 0.2943179011 0.2499558926 + 0.2926771045 0.2975485921 0.2530024946 + 0.2957693934 0.3007347882 0.2560170889 + 0.2988215983 0.3038783967 0.2590034902 + 0.3018361032 0.3069826961 0.2619606853 + 0.3048140109 0.3100473881 0.2648896873 + 0.3077591062 0.3130752146 0.2677848041 + 0.3106676936 0.3160668910 0.2706494033 + 0.3135369122 0.3190228045 0.2734929919 + 0.3163697124 0.3219431043 0.2763096094 + 0.3191707134 0.3248273134 0.2790902853 + 0.3219430149 0.3276773989 0.2818470001 + 0.3246892989 0.3305040896 0.2845870852 + 0.3274120092 0.3333109021 0.2873024046 + 0.3301134109 0.3360993862 0.2899892926 + 0.3327957988 0.3388687074 0.2926526964 + 0.3354612887 0.3416137993 0.2952902913 + 0.3381122947 0.3443362117 0.2979075015 + 0.3407450020 0.3470380008 0.3005059063 + 0.3433603942 0.3497211039 0.3030832112 + 0.3459551930 0.3523826003 0.3056401014 + 0.3485282063 0.3550176919 0.3081726134 + 0.3510803878 0.3576203883 0.3106764853 + 0.3536134064 0.3601945937 0.3131647110 + 0.3561300039 0.3627431095 0.3156422973 + 0.3586291075 0.3652668893 0.3181054890 + 0.3611052930 0.3677676916 0.3205488920 + 0.3635632992 0.3702487946 0.3229709864 + 0.3660041094 0.3727082908 0.3253682852 + 0.3684245944 0.3751434982 0.3277472854 + 0.3708260059 0.3775567114 0.3301078975 + 0.3732075989 0.3799484968 0.3324497938 + 0.3755733967 0.3823226094 0.3347747028 + 0.3779214025 0.3846783042 0.3370811045 + 0.3802560866 0.3870177865 0.3393734097 + 0.3825756013 0.3893364966 0.3416540921 + 0.3848834038 0.3916386962 0.3439235091 + 0.3871786892 0.3939253986 0.3461782932 + 0.3894636929 0.3961988091 0.3484173119 + 0.3917393088 0.3984617889 0.3506391943 + 0.3940058053 0.4007127881 0.3528397083 + 0.3962653875 0.4029492140 0.3550203145 + 0.3985182941 0.4051673114 0.3571819961 + 0.4007658064 0.4073692858 0.3593285978 + 0.4030055106 0.4095577896 0.3614608049 + 0.4052354097 0.4117330909 0.3635764122 + 0.4074552953 0.4138962030 0.3656741083 + 0.4096668959 0.4160450995 0.3677546084 + 0.4118706882 0.4181813896 0.3698169887 + 0.4140650928 0.4203011096 0.3718580902 + 0.4162524045 0.4224058986 0.3738800883 + 0.4184325933 0.4244979024 0.3758884072 + 0.4206056893 0.4265781939 0.3778842986 + 0.4227721095 0.4286450148 0.3798668981 + 0.4249303937 0.4306966066 0.3818354011 + 0.4270783961 0.4327369034 0.3837904930 + 0.4292162061 0.4347665906 0.3857319951 + 0.4313440025 0.4367870986 0.3876594007 + 0.4334617853 0.4388001859 0.3895725906 + 0.4355661869 0.4407989085 0.3914704919 + 0.4376563132 0.4427832961 0.3933526874 + 0.4397337139 0.4447549880 0.3952197134 + 0.4417980909 0.4467160106 0.3970730901 + 0.4438503087 0.4486671090 0.3989152014 + 0.4458903074 0.4506083131 0.4007458985 + 0.4479185045 0.4525383115 0.4025672972 + 0.4499352872 0.4544585049 0.4043807089 + 0.4519383013 0.4563668966 0.4061861932 + 0.4539265931 0.4582642913 0.4079838991 + 0.4559009075 0.4601486921 0.4097737968 + 0.4578616917 0.4620200992 0.4115560949 + 0.4598096013 0.4638800919 0.4133307934 + 0.4617452919 0.4657298028 0.4150981009 + 0.4636693895 0.4675683081 0.4168579876 + 0.4655824006 0.4693951011 0.4186106920 + 0.4674822092 0.4712128043 0.4203560948 + 0.4693683088 0.4730215073 0.4220944047 + 0.4712423086 0.4748198092 0.4238258004 + 0.4731037021 0.4766085148 0.4255501926 + 0.4749540985 0.4783886969 0.4272677004 + 0.4767911136 0.4801594913 0.4289785922 + 0.4786145985 0.4819203913 0.4306828082 + 0.4804255068 0.4836716056 0.4323804080 + 0.4822244942 0.4854123890 0.4340715110 + 0.4840112031 0.4871422946 0.4357562065 + 0.4857847989 0.4888612926 0.4374346137 + 0.4875457883 0.4905700088 0.4391067922 + 0.4892955124 0.4922698140 0.4407728910 + 0.4910339117 0.4939587116 0.4424329102 + 0.4927617908 0.4956364036 0.4440839887 + 0.4944767952 0.4973056912 0.4457261860 + 0.4961803854 0.4989677966 0.4473606944 + 0.4978719950 0.5006220937 0.4489873052 + 0.4995515049 0.5022668839 0.4506061971 + 0.5012195706 0.5039017200 0.4522179067 + 0.5028777719 0.5055288076 0.4538218975 + 0.5045251250 0.5071460009 0.4554184973 + 0.5061631799 0.5087574124 0.4570069015 + 0.5077912807 0.5103607774 0.4585888088 + 0.5094099045 0.5119572282 0.4601627886 + 0.5110192895 0.5135468245 0.4617280960 + 0.5126187801 0.5151286721 0.4632849991 + 0.5142037272 0.5166985989 0.4648320079 + 0.5157784224 0.5182607174 0.4663707018 + 0.5173432231 0.5198156238 0.4679018855 + 0.5188995004 0.5213642120 0.4694260061 + 0.5204483271 0.5229077935 0.4709433019 + 0.5219827294 0.5244423747 0.4724520147 + 0.5235059857 0.5259712934 0.4739539921 + 0.5250201821 0.5274928212 0.4754489958 + 0.5265259743 0.5290061235 0.4769366086 + 0.5280231833 0.5305104256 0.4784151018 + 0.5295122862 0.5320072174 0.4798853099 + 0.5309923887 0.5334973931 0.4813486040 + 0.5324633718 0.5349810123 0.4828054905 + 0.5339248180 0.5364567041 0.4842553139 + 0.5353744030 0.5379219055 0.4856970906 + 0.5368158221 0.5393791795 0.4871335924 + 0.5382496715 0.5408291221 0.4885646999 + 0.5396752954 0.5422707796 0.4899865091 + 0.5410915017 0.5437059999 0.4914005101 + 0.5425012708 0.5451353788 0.4928086102 + 0.5439049006 0.5465584993 0.4942106903 + 0.5452982187 0.5479696989 0.4956040084 + 0.5466837883 0.5493730903 0.4969924986 + 0.5480626225 0.5507693291 0.4983761907 + 0.5494338274 0.5521574020 0.4997530878 + 0.5507962704 0.5535387993 0.5011236072 + 0.5521531105 0.5549157858 0.5024887919 + 0.5535033941 0.5562884808 0.5038468242 + 0.5548453927 0.5576546192 0.5051984787 + 0.5561814904 0.5590159297 0.5065459013 + 0.5575097203 0.5603706837 0.5078874826 + 0.5588302016 0.5617178082 0.5092235804 + 0.5601449013 0.5630592704 0.5105562210 + 0.5614513755 0.5643923283 0.5118840933 + 0.5627481937 0.5657188892 0.5132076144 + 0.5640394092 0.5670406818 0.5145276785 + 0.5653225780 0.5683544874 0.5158423781 + 0.5665981770 0.5696623921 0.5171517730 + 0.5678697228 0.5709664226 0.5184581280 + 0.5691354275 0.5722628236 0.5197597146 + 0.5703939795 0.5735521913 0.5210576057 + 0.5716482997 0.5748370290 0.5223528743 + 0.5728936195 0.5761141777 0.5236395001 + 0.5741339922 0.5773866773 0.5249217153 + 0.5753698945 0.5786542892 0.5261995196 + 0.5765981078 0.5799149871 0.5274701118 + 0.5778229833 0.5811718106 0.5287377834 + 0.5790420771 0.5824205279 0.5300002098 + 0.5802537799 0.5836634040 0.5312584043 + 0.5814616084 0.5849028826 0.5325140953 + 0.5826593041 0.5861350894 0.5337622166 + 0.5838543773 0.5873640776 0.5350074172 + 0.5850455165 0.5885868073 0.5362474918 + 0.5862290263 0.5898029804 0.5374835134 + 0.5874090195 0.5910161138 0.5387175083 + 0.5885779858 0.5922204852 0.5399456024 + 0.5897439718 0.5934225917 0.5411707163 + 0.5909047127 0.5946190953 0.5423896909 + 0.5920587778 0.5958110094 0.5436031222 + 0.5932093859 0.5969995856 0.5448129773 + 0.5943543911 0.5981795192 0.5460162163 + 0.5954970121 0.5993567109 0.5472174883 + 0.5966315269 0.6005250812 0.5484123826 + 0.5977625847 0.6016913056 0.5496050715 + 0.5988891721 0.6028537154 0.5507941842 + 0.6000123024 0.6040124297 0.5519784093 + 0.6011335254 0.6051688194 0.5531598926 + 0.6022430062 0.6063209176 0.5543354154 + 0.6033499241 0.6074718833 0.5555096269 + 0.6044468284 0.6086149812 0.5566766262 + 0.6055412292 0.6097556949 0.5578411222 + 0.6066293716 0.6108884811 0.5589979887 + 0.6077128053 0.6120188832 0.5601516962 + 0.6087905169 0.6131458282 0.5613011718 + 0.6098622084 0.6142669916 0.5624457002 + 0.6109300256 0.6153838038 0.5635867715 + 0.6119914055 0.6164928079 0.5647237897 + 0.6130506992 0.6175984740 0.5658593178 + 0.6141037941 0.6186978221 0.5669875741 + 0.6151558757 0.6197956800 0.5681148171 + 0.6162015200 0.6208844185 0.5692356229 + 0.6172462106 0.6219723821 0.5703557730 + 0.6182832122 0.6230551004 0.5714685917 + 0.6193187833 0.6241365075 0.5725799799 + 0.6203479767 0.6252096891 0.5736833215 + 0.6213756204 0.6262813807 0.5747848749 + 0.6223981977 0.6273471713 0.5758804083 + 0.6234180927 0.6284117103 0.5769739151 + 0.6244314909 0.6294723749 0.5780618191 + 0.6254416108 0.6305295229 0.5791479945 + 0.6264446974 0.6315774918 0.5802301764 + 0.6274456978 0.6326224804 0.5813099742 + 0.6284427047 0.6336600184 0.5823848248 + 0.6294364929 0.6346951723 0.5834575295 + 0.6304243207 0.6357244849 0.5845260024 + 0.6314088106 0.6367521286 0.5855916739 + 0.6323868036 0.6377754211 0.5866507888 + 0.6333630085 0.6387977004 0.5877078176 + 0.6343346238 0.6398169994 0.5887587070 + 0.6353039742 0.6408346295 0.5898079872 + 0.6362662911 0.6418454051 0.5908517241 + 0.6372275949 0.6428552270 0.5918945074 + 0.6381838918 0.6438586712 0.5929322243 + 0.6391395926 0.6448618770 0.5939694047 + 0.6400895119 0.6458585262 0.5950021148 + 0.6410393715 0.6468551755 0.5960347056 + 0.6419798732 0.6478437781 0.5970602036 + 0.6429203153 0.6488320827 0.5980855227 + 0.6438559890 0.6498137116 0.5991042852 + 0.6447910070 0.6507939100 0.6001219749 + 0.6457198262 0.6517695189 0.6011344194 + 0.6466463208 0.6527429819 0.6021447182 + 0.6475692987 0.6537116170 0.6031510234 + 0.6484898925 0.6546760798 0.6041542292 + 0.6494089961 0.6556367874 0.6051554084 + 0.6503260732 0.6565908790 0.6061536074 + 0.6512408853 0.6575438976 0.6071500778 + 0.6521478295 0.6584920287 0.6081401706 + 0.6530545950 0.6594399810 0.6091303229 + 0.6539540291 0.6603807807 0.6101105809 + 0.6548532844 0.6613214016 0.6110907793 + 0.6557484865 0.6622567177 0.6120656133 + 0.6566426754 0.6631903052 0.6130387783 + 0.6575334072 0.6641204953 0.6140081286 + 0.6584212780 0.6650474072 0.6149740219 + 0.6593077183 0.6659724116 0.6159386039 + 0.6601902246 0.6668912768 0.6168997884 + 0.6610727906 0.6678102016 0.6178609729 + 0.6619489789 0.6687254906 0.6188141704 + 0.6628252268 0.6696406007 0.6197673082 + 0.6636962295 0.6705511212 0.6207141280 + 0.6645649076 0.6714593172 0.6216580272 + 0.6654312015 0.6723641157 0.6225993037 + 0.6662933826 0.6732622981 0.6235358715 + 0.6671553850 0.6741604805 0.6244724989 + 0.6680101156 0.6750528216 0.6254026294 + 0.6688649058 0.6759452224 0.6263327003 + 0.6697137952 0.6768317819 0.6272596717 + 0.6705600023 0.6777151823 0.6281852722 + 0.6714038253 0.6785969138 0.6291080713 + 0.6722424030 0.6794738173 0.6300241947 + 0.6730809212 0.6803507209 0.6309403181 + 0.6739163995 0.6812239885 0.6318492293 + 0.6747515202 0.6820967197 0.6327571273 + 0.6755837798 0.6829648018 0.6336619854 + 0.6764129996 0.6838271022 0.6345633268 + 0.6772418022 0.6846892834 0.6354644895 + 0.6780627966 0.6855434775 0.6363605261 + 0.6788836718 0.6863977909 0.6372565031 + 0.6797022223 0.6872488260 0.6381502748 + 0.6805189848 0.6880972981 0.6390423775 + 0.6813352108 0.6889454126 0.6399337053 + 0.6821470261 0.6897872090 0.6408177018 + 0.6829589009 0.6906291246 0.6417016983 + 0.6837666035 0.6914675236 0.6425812244 + 0.6845718026 0.6923034191 0.6434577703 + 0.6853765249 0.6931388974 0.6443338990 + 0.6861765981 0.6939678788 0.6452031136 + 0.6869766116 0.6947969794 0.6460722089 + 0.6877734065 0.6956220865 0.6469382048 + 0.6885675192 0.6964433789 0.6478012800 + 0.6893616915 0.6972646713 0.6486644149 + 0.6901482940 0.6980792880 0.6495230198 + 0.6909348965 0.6988936067 0.6503813863 + 0.6917189956 0.6997050047 0.6512373090 + 0.6924995184 0.7005112767 0.6520892978 + 0.6932801008 0.7013177276 0.6529412866 + 0.6940550804 0.7021173239 0.6537883878 + 0.6948289275 0.7029148936 0.6546339989 + 0.6956019998 0.7037113905 0.6554787755 + 0.6963722706 0.7045007944 0.6563187242 + 0.6971424818 0.7052903175 0.6571586132 + 0.6979110241 0.7060778737 0.6579942703 + 0.6986778975 0.7068635225 0.6588261724 + 0.6994447112 0.7076489925 0.6596580148 + 0.7002053261 0.7084293962 0.6604819894 + 0.7009649873 0.7092086077 0.6613044739 + 0.7017238736 0.7099866867 0.6621260047 + 0.7024794817 0.7107571959 0.6629421115 + 0.7032350898 0.7115277052 0.6637582183 + 0.7039889097 0.7122963071 0.6645715833 + 0.7047405839 0.7130622268 0.6653813124 + 0.7054923177 0.7138280869 0.6661911011 + 0.7062383890 0.7145891786 0.6669985056 + 0.7069823742 0.7153481245 0.6678047180 + 0.7077264190 0.7161070108 0.6686109900 + 0.7084659934 0.7168573141 0.6694114208 + 0.7092055082 0.7176067829 0.6702113748 + 0.7099441886 0.7183555961 0.6710103154 + 0.7106792927 0.7190998197 0.6718034148 + 0.7114142776 0.7198439837 0.6725963950 + 0.7121458054 0.7205855846 0.6733878255 + 0.7128710747 0.7213221788 0.6741760969 + 0.7135964036 0.7220587134 0.6749644876 + 0.7143177986 0.7227926850 0.6757488847 + 0.7150359154 0.7235242128 0.6765298843 + 0.7157539725 0.7242556214 0.6773108244 + 0.7164688706 0.7249854207 0.6780883074 + 0.7171823978 0.7257142067 0.6788641214 + 0.7178958058 0.7264429927 0.6796398163 + 0.7186031938 0.7271656990 0.6804109812 + 0.7193090916 0.7278866172 0.6811808944 + 0.7200149894 0.7286074758 0.6819508076 + 0.7207157016 0.7293236852 0.6827170253 + 0.7214158773 0.7300391793 0.6834827065 + 0.7221161127 0.7307546735 0.6842483878 + 0.7228116989 0.7314674258 0.6850115061 + 0.7235072255 0.7321798801 0.6857745051 + 0.7242025733 0.7328923941 0.6865373254 + 0.7248927951 0.7335966825 0.6872922182 + 0.7255830169 0.7343010902 0.6880471110 + 0.7262728810 0.7350053191 0.6888017058 + 0.7269589901 0.7357040048 0.6895499825 + 0.7276450992 0.7364026904 0.6902983785 + 0.7283307910 0.7371011972 0.6910464764 + 0.7290124297 0.7377966046 0.6917898059 + 0.7296938896 0.7384920120 0.6925331950 + 0.7303751707 0.7391871810 0.6932764053 + 0.7310516238 0.7398741841 0.6940135956 + 0.7317278981 0.7405611873 0.6947507858 + 0.7324042916 0.7412481904 0.6954879761 + 0.7330766916 0.7419282198 0.6962195039 + 0.7337490916 0.7426078916 0.6969509721 + 0.7344214916 0.7432876229 0.6976823807 + 0.7350897193 0.7439622879 0.6984075904 + 0.7357577085 0.7446364164 0.6991322041 + 0.7364255786 0.7453106046 0.6998566985 + 0.7370892763 0.7459799051 0.7005770206 + 0.7377521992 0.7466480136 0.7012962103 + 0.7384151220 0.7473161221 0.7020155191 + 0.7390751839 0.7479804158 0.7027320862 + 0.7397342920 0.7486426830 0.7034475207 + 0.7403932810 0.7493051291 0.7041630149 + 0.7410498261 0.7499645948 0.7048757076 + 0.7417044044 0.7506214976 0.7055861950 + 0.7423589826 0.7512784004 0.7062966228 + 0.7430117130 0.7519339919 0.7070053816 + 0.7436612844 0.7525870800 0.7077112794 + 0.7443109155 0.7532402277 0.7084171772 + 0.7449595928 0.7538927794 0.7091221809 + 0.7456040978 0.7545416951 0.7098222971 + 0.7462486029 0.7551906109 0.7105224729 + 0.7468931079 0.7558395267 0.7112227082 + 0.7475314736 0.7564836144 0.7119165063 + 0.7481697202 0.7571272850 0.7126098871 + 0.7488077879 0.7577710152 0.7133032084 + 0.7494426966 0.7584123015 0.7139921784 + 0.7500761151 0.7590525150 0.7146791220 + 0.7507095933 0.7596926093 0.7153660059 + 0.7513409257 0.7603306770 0.7160515785 + 0.7519693971 0.7609655261 0.7167350054 + 0.7525979280 0.7616001964 0.7174183130 + 0.7532256842 0.7622345090 0.7181012034 + 0.7538480163 0.7628622055 0.7187789083 + 0.7544702888 0.7634899020 0.7194564939 + 0.7550926208 0.7641175985 0.7201341987 + 0.7557113767 0.7647429705 0.7208058834 + 0.7563291788 0.7653676867 0.7214756012 + 0.7569469213 0.7659922838 0.7221453786 + 0.7575628161 0.7666150928 0.7228131890 + 0.7581757903 0.7672348022 0.7234779000 + 0.7587887049 0.7678545117 0.7241426110 + 0.7594016790 0.7684742212 0.7248072028 + 0.7600086927 0.7690863013 0.7254648209 + 0.7606157064 0.7696982026 0.7261223197 + 0.7612227201 0.7703102231 0.7267798185 + 0.7618271708 0.7709196210 0.7274342179 + 0.7624298930 0.7715268731 0.7280861735 + 0.7630326152 0.7721343040 0.7287383080 + 0.7636346817 0.7727413177 0.7293899059 + 0.7642328143 0.7733443975 0.7300382853 + 0.7648310065 0.7739474177 0.7306867242 + 0.7654290795 0.7745504975 0.7313351035 + 0.7660248876 0.7751491070 0.7319790125 + 0.7666196227 0.7757449746 0.7326202989 + 0.7672141790 0.7763409019 0.7332617044 + 0.7678084970 0.7769364119 0.7339023948 + 0.7684006095 0.7775282860 0.7345390916 + 0.7689927816 0.7781202197 0.7351757288 + 0.7695848942 0.7787120938 0.7358123064 + 0.7701737285 0.7793018818 0.7364459038 + 0.7707604170 0.7798901200 0.7370774150 + 0.7713471055 0.7804782987 0.7377089858 + 0.7719334960 0.7810664773 0.7383404970 + 0.7725152969 0.7816480994 0.7389686704 + 0.7730969787 0.7822297812 0.7395969033 + 0.7736787200 0.7828114033 0.7402251959 + 0.7742595077 0.7833915949 0.7408527136 + 0.7748392820 0.7839698792 0.7414792776 + 0.7754191160 0.7845481038 0.7421059012 + 0.7759987712 0.7851263285 0.7427325845 + 0.7765746117 0.7857019901 0.7433542013 + 0.7771494985 0.7862771749 0.7439746261 + 0.7777243853 0.7868523002 0.7445951104 + 0.7782984972 0.7874264121 0.7452148199 + 0.7788689733 0.7879943252 0.7458307743 + 0.7794395089 0.7885621190 0.7464467883 + 0.7800099254 0.7891299725 0.7470628023 + 0.7805796862 0.7896950841 0.7476773858 + 0.7811487913 0.7902569175 0.7482905984 + 0.7817177773 0.7908186913 0.7489038110 + 0.7822868824 0.7913805246 0.7495170236 + 0.7828525901 0.7919393182 0.7501246929 + 0.7834174037 0.7924969792 0.7507306933 + 0.7839820981 0.7930546999 0.7513365746 + 0.7845466733 0.7936124206 0.7519425154 + 0.7851064205 0.7941638231 0.7525429130 + 0.7856659889 0.7947151065 0.7531433105 + 0.7862256169 0.7952665091 0.7537437081 + 0.7867845893 0.7958167791 0.7543430924 + 0.7873414755 0.7963629961 0.7549396157 + 0.7878984809 0.7969092727 0.7555360198 + 0.7884554267 0.7974554896 0.7561324835 + 0.7890108228 0.7980003953 0.7567269802 + 0.7895640731 0.7985432744 0.7573187947 + 0.7901175022 0.7990862727 0.7579106092 + 0.7906708121 0.7996292114 0.7585024834 + 0.7912214994 0.8001691103 0.7590919137 + 0.7917703986 0.8007066250 0.7596797943 + 0.7923192978 0.8012441993 0.7602676153 + 0.7928681970 0.8017817140 0.7608553767 + 0.7934157252 0.8023154140 0.7614399195 + 0.7939624786 0.8028475046 0.7620229721 + 0.7945094109 0.8033794761 0.7626060843 + 0.7950562239 0.8039115071 0.7631891966 + 0.7956007123 0.8044400811 0.7637678981 + 0.7961447239 0.8049678206 0.7643455863 + 0.7966886163 0.8054953814 0.7649232745 + 0.7972326279 0.8060231209 0.7655010223 + 0.7977703810 0.8065471053 0.7660744786 + 0.7983074784 0.8070703745 0.7666473985 + 0.7988445759 0.8075938225 0.7672201991 + 0.7993816733 0.8081170917 0.7677931190 + 0.7999145985 0.8086366057 0.7683627009 + 0.8004472256 0.8091554046 0.7689319849 + 0.8009796739 0.8096743226 0.7695012093 + 0.8015123010 0.8101931810 0.7700704932 + 0.8020427823 0.8107082248 0.7706354260 + 0.8025730848 0.8112226129 0.7711998224 + 0.8031035066 0.8117371202 0.7717642188 + 0.8036338091 0.8122515082 0.7723286152 + 0.8041588068 0.8127616048 0.7728883028 + 0.8046830297 0.8132709265 0.7734473944 + 0.8052071929 0.8137801886 0.7740064263 + 0.8057314754 0.8142895103 0.7745653987 + 0.8062517047 0.8147963285 0.7751224041 + 0.8067709208 0.8153023124 0.7756788135 + 0.8072901964 0.8158082962 0.7762352824 + 0.8078094125 0.8163142800 0.7767916918 + 0.8083264232 0.8168172836 0.7773452997 + 0.8088424802 0.8173186779 0.7778974771 + 0.8093585968 0.8178201914 0.7784497142 + 0.8098745942 0.8183215857 0.7790020108 + 0.8103876710 0.8188199997 0.7795510292 + 0.8108984828 0.8193156123 0.7800974250 + 0.8114091754 0.8198112845 0.7806438208 + 0.8119199872 0.8203068972 0.7811902165 + 0.8124291897 0.8208017945 0.7817354202 + 0.8129357100 0.8212953806 0.7822787166 + 0.8134422898 0.8217890263 0.7828220129 + 0.8139489293 0.8222826719 0.7833654284 + 0.8144549727 0.8227757812 0.7839078903 + 0.8149591088 0.8232660890 0.7844464779 + 0.8154631853 0.8237565160 0.7849851251 + 0.8159673810 0.8242468238 0.7855237722 + 0.8164715171 0.8247371912 0.7860624194 + 0.8169714212 0.8252223730 0.7865952253 + 0.8174710274 0.8257070780 0.7871276140 + 0.8179705739 0.8261917830 0.7876598835 + 0.8184702992 0.8266764879 0.7881922722 + 0.8189675808 0.8271585107 0.7887222767 + 0.8194637895 0.8276389241 0.7892510891 + 0.8199599981 0.8281193972 0.7897799015 + 0.8204562068 0.8285998106 0.7903087139 + 0.8209517002 0.8290792108 0.7908356786 + 0.8214461803 0.8295565248 0.7913593054 + 0.8219406009 0.8300337791 0.7918829918 + 0.8224350214 0.8305110931 0.7924066186 + 0.8229293823 0.8309884071 0.7929303050 + 0.8234202266 0.8314602971 0.7934505939 + 0.8239108920 0.8319321275 0.7939708829 + 0.8244016767 0.8324038982 0.7944911718 + 0.8248924017 0.8328756094 0.7950115204 + 0.8253806829 0.8333445787 0.7955307961 + 0.8258677125 0.8338115215 0.7960494757 + 0.8263546228 0.8342785239 0.7965682149 + 0.8268414736 0.8347455263 0.7970868945 + 0.8273276091 0.8352121115 0.7976047993 + 0.8278108239 0.8356773257 0.7981191874 + 0.8282939792 0.8361424804 0.7986335754 + 0.8287771940 0.8366076946 0.7991480827 + 0.8292604089 0.8370729089 0.7996624708 + 0.8297402859 0.8375352025 0.8001754284 + 0.8302192092 0.8379961848 0.8006876707 + 0.8306980133 0.8384572864 0.8012000918 + 0.8311768770 0.8389183879 0.8017123938 + 0.8316546082 0.8393784761 0.8022242785 + 0.8321290016 0.8398349881 0.8027346134 + 0.8326033950 0.8402916193 0.8032448888 + 0.8330777884 0.8407480717 0.8037552834 + 0.8335521817 0.8412047029 0.8042656183 + 0.8340237737 0.8416597843 0.8047726750 + 0.8344941735 0.8421143293 0.8052784204 + 0.8349646926 0.8425686955 0.8057842255 + 0.8354352117 0.8430231810 0.8062899113 + 0.8359050751 0.8434774280 0.8067952991 + 0.8363721967 0.8439291716 0.8072986007 + 0.8368391991 0.8443809748 0.8078019023 + 0.8373062015 0.8448328972 0.8083050847 + 0.8377732038 0.8452847004 0.8088083863 + 0.8382382989 0.8457334042 0.8093103766 + 0.8387017846 0.8461793065 0.8098112941 + 0.8391653895 0.8466250896 0.8103122711 + 0.8396288753 0.8470708728 0.8108131886 + 0.8400924206 0.8475167751 0.8113141060 + 0.8405531049 0.8479586244 0.8118106723 + 0.8410136104 0.8483998775 0.8123067021 + 0.8414739966 0.8488411903 0.8128026724 + 0.8419345021 0.8492823839 0.8132987022 + 0.8423944116 0.8497232199 0.8137940764 + 0.8428521752 0.8501616716 0.8142873049 + 0.8433101177 0.8506001830 0.8147805929 + 0.8437680006 0.8510386944 0.8152738214 + 0.8442258835 0.8514773250 0.8157669902 + 0.8446823955 0.8519136906 0.8162589073 + 0.8451377153 0.8523477912 0.8167495131 + 0.8455929160 0.8527818918 0.8172399998 + 0.8460481763 0.8532159925 0.8177304864 + 0.8465033770 0.8536500931 0.8182210922 + 0.8469561934 0.8540809155 0.8187078834 + 0.8474081755 0.8545101881 0.8191933036 + 0.8478600979 0.8549395800 0.8196786046 + 0.8483120203 0.8553689718 0.8201640248 + 0.8487640023 0.8557983041 0.8206493258 + 0.8492118716 0.8562254906 0.8211318851 + 0.8496595025 0.8566523790 0.8216142058 + 0.8501071930 0.8570793867 0.8220965266 + 0.8505548239 0.8575062752 0.8225787878 + 0.8510020971 0.8579329848 0.8230608106 + 0.8514466286 0.8583570123 0.8235400915 + 0.8518909812 0.8587809801 0.8240194917 + 0.8523355126 0.8592050076 0.8244987726 + 0.8527799249 0.8596289754 0.8249781132 + 0.8532239199 0.8600522280 0.8254566789 + 0.8536661863 0.8604717851 0.8259320855 + 0.8541085124 0.8608914018 0.8264076114 + 0.8545507789 0.8613110185 0.8268830180 + 0.8549929857 0.8617305756 0.8273584843 + 0.8554344177 0.8621492982 0.8278334141 + 0.8558735251 0.8625652790 0.8283066154 + 0.8563125134 0.8629813194 0.8287798166 + 0.8567516208 0.8633973002 0.8292530179 + 0.8571907282 0.8638132811 0.8297262788 + 0.8576285839 0.8642284870 0.8301985264 + 0.8580638766 0.8646416068 0.8306686282 + 0.8584992886 0.8650546074 0.8311386704 + 0.8589347005 0.8654677272 0.8316087127 + 0.8593699932 0.8658807874 0.8320788145 + 0.8598045707 0.8662930727 0.8325483799 + 0.8602377176 0.8667033911 0.8330168724 + 0.8606708050 0.8671137094 0.8334854245 + 0.8611038923 0.8675240874 0.8339539170 + 0.8615369797 0.8679344058 0.8344224095 + 0.8619691730 0.8683441877 0.8348909020 + 0.8623989820 0.8687524796 0.8353595138 + 0.8628287911 0.8691607118 0.8358281255 + 0.8632586002 0.8695690036 0.8362966180 + 0.8636884093 0.8699771762 0.8367652297 + 0.8641173840 0.8703852892 0.8372331858 + 0.8645430207 0.8707920909 0.8376984000 + 0.8649684787 0.8711990118 0.8381634951 + 0.8653941154 0.8716058731 0.8386287093 + 0.8658195734 0.8720127940 0.8390939236 + 0.8662450910 0.8724194765 0.8395590186 + 0.8666694164 0.8728235960 0.8400223255 + 0.8670938015 0.8732277155 0.8404856920 + 0.8675181866 0.8736317754 0.8409489989 + 0.8679425120 0.8740358949 0.8414124250 + 0.8683668971 0.8744400144 0.8418756723 + 0.8687893152 0.8748406172 0.8423389196 + 0.8692114949 0.8752405047 0.8428019881 + 0.8696336746 0.8756405115 0.8432651758 + 0.8700559139 0.8760403991 0.8437283039 + 0.8704780936 0.8764404058 0.8441913724 + 0.8708974123 0.8768380880 0.8446521759 + 0.8713154197 0.8772345781 0.8451117277 + 0.8717334867 0.8776311278 0.8455712199 + 0.8721514940 0.8780276179 0.8460307717 + 0.8725695014 0.8784241080 0.8464903235 + 0.8729861975 0.8788195252 0.8469489813 + 0.8734011054 0.8792132139 0.8474063873 + 0.8738160133 0.8796069026 0.8478639126 + 0.8742309213 0.8800007105 0.8483213782 + 0.8746458292 0.8803943992 0.8487787843 + 0.8750604987 0.8807877898 0.8492361903 + 0.8754733205 0.8811789751 0.8496932983 + 0.8758860230 0.8815701008 0.8501502872 + 0.8762987852 0.8819612265 0.8506072760 + 0.8767116070 0.8823524117 0.8510643244 + 0.8771244287 0.8827434778 0.8515213132 + 0.8775348067 0.8831323981 0.8519753814 + 0.8779444098 0.8835206032 0.8524286151 + 0.8783540726 0.8839088082 0.8528817892 + 0.8787636757 0.8842970133 0.8533350229 + 0.8791733980 0.8846852183 0.8537881970 + 0.8795822263 0.8850721121 0.8542405963 + 0.8799896836 0.8854566216 0.8546915054 + 0.8803973198 0.8858410716 0.8551424742 + 0.8808047771 0.8862255812 0.8555933833 + 0.8812122941 0.8866102099 0.8560442924 + 0.8816198111 0.8869947195 0.8564953208 + 0.8820263147 0.8873755932 0.8569455743 + 0.8824326992 0.8877562284 0.8573958874 + 0.8828390837 0.8881369233 0.8578462005 + 0.8832455277 0.8885174990 0.8582963943 + 0.8836519122 0.8888980746 0.8587467074 + 0.8840569258 0.8892776966 0.8591951728 + 0.8844602108 0.8896561861 0.8596414924 + 0.8848636150 0.8900346160 0.8600878119 + 0.8852669001 0.8904129863 0.8605340719 + 0.8856703043 0.8907914162 0.8609802723 + 0.8860735893 0.8911697865 0.8614265919 + 0.8864722848 0.8915463090 0.8618707061 + 0.8868706226 0.8919227123 0.8623145819 + 0.8872690201 0.8922989964 0.8627585173 + 0.8876674175 0.8926753998 0.8632022738 + 0.8880656958 0.8930518031 0.8636462092 + 0.8884631991 0.8934273124 0.8640897870 + 0.8888590932 0.8938013911 0.8645328283 + 0.8892549872 0.8941754103 0.8649758101 + 0.8896508813 0.8945494294 0.8654187918 + 0.8900468946 0.8949235082 0.8658617735 + 0.8904427886 0.8952975273 0.8663048148 + 0.8908371925 0.8956699967 0.8667446971 + 0.8912311792 0.8960419893 0.8671836257 + 0.8916252255 0.8964139223 0.8676224947 + 0.8920192122 0.8967859149 0.8680614233 + 0.8924131989 0.8971577883 0.8685001731 + 0.8928068876 0.8975296021 0.8689389229 + 0.8931970000 0.8978990912 0.8693752289 + 0.8935871124 0.8982686996 0.8698114753 + 0.8939772844 0.8986381888 0.8702477813 + 0.8943673968 0.8990076780 0.8706840873 + 0.8947576284 0.8993772268 0.8711203933 + 0.8951466084 0.8997455835 0.8715562820 + 0.8955339193 0.9001119733 0.8719915152 + 0.8959211707 0.9004784226 0.8724268079 + 0.8963084817 0.9008446932 0.8728619814 + 0.8966959119 0.9012110829 0.8732972145 + 0.8970832229 0.9015774131 0.8737323880 + 0.8974692822 0.9019417763 0.8741655946 + 0.8978548050 0.9023047090 0.8745973706 + 0.8982402086 0.9026677012 0.8750292063 + 0.8986256123 0.9030305743 0.8754611015 + 0.8990110755 0.9033935070 0.8758928776 + 0.8993964791 0.9037564993 0.8763247132 + 0.8997789025 0.9041181207 0.8767552972 + 0.9001606107 0.9044793248 0.8771855831 + 0.9005421996 0.9048405290 0.8776159286 + 0.9009239078 0.9052016735 0.8780462146 + 0.9013054967 0.9055628777 0.8784765005 + 0.9016872048 0.9059240818 0.8789067864 + 0.9020649195 0.9062829018 0.8793358207 + 0.9024425149 0.9066416025 0.8797646165 + 0.9028201103 0.9070003033 0.8801934719 + 0.9031975865 0.9073588848 0.8806223869 + 0.9035751820 0.9077175856 0.8810513020 + 0.9039525986 0.9080762267 0.8814799786 + 0.9043281078 0.9084306955 0.8819060922 + 0.9047036171 0.9087852836 0.8823322058 + 0.9050791264 0.9091398716 0.8827583194 + 0.9054545760 0.9094944000 0.8831843734 + 0.9058300853 0.9098489881 0.8836104274 + 0.9062054157 0.9102032781 0.8840364814 + 0.9065783024 0.9105550051 0.8844619989 + 0.9069511890 0.9109066725 0.8848875165 + 0.9073241949 0.9112585187 0.8853129148 + 0.9076970816 0.9116101861 0.8857383728 + 0.9080700874 0.9119619131 0.8861638904 + 0.9084427953 0.9123135209 0.8865892291 + 0.9088128209 0.9126635194 0.8870128989 + 0.9091827869 0.9130135179 0.8874365091 + 0.9095528126 0.9133635163 0.8878601789 + 0.9099227786 0.9137135148 0.8882837892 + 0.9102928042 0.9140635133 0.8887075186 + 0.9106627107 0.9144133925 0.8891310096 + 0.9110317826 0.9147610068 0.8895515203 + 0.9114007950 0.9151085019 0.8899719119 + 0.9117698073 0.9154561162 0.8903924227 + 0.9121388793 0.9158036113 0.8908128738 + 0.9125078917 0.9161511064 0.8912333250 + 0.9128769040 0.9164987206 0.8916537762 + 0.9132447243 0.9168428779 0.8920732737 + 0.9136124253 0.9171867967 0.8924927115 + 0.9139801264 0.9175307751 0.8929122090 + 0.9143477082 0.9178748131 0.8933315873 + 0.9147154093 0.9182186723 0.8937510848 + 0.9150831103 0.9185627103 0.8941705227 + 0.9154474735 0.9189047813 0.8945881724 + 0.9158111215 0.9192463756 0.8950055242 + 0.9161747098 0.9195880294 0.8954226971 + 0.9165382981 0.9199296236 0.8958399296 + 0.9169018865 0.9202712178 0.8962571025 + 0.9172654748 0.9206128120 0.8966743946 + 0.9176265001 0.9209533930 0.8970903158 + 0.9179857969 0.9212933183 0.8975052834 + 0.9183449745 0.9216331244 0.8979204297 + 0.9187042117 0.9219729900 0.8983353972 + 0.9190635085 0.9223129153 0.8987504244 + 0.9194226861 0.9226527810 0.8991655111 + 0.9197812080 0.9229915738 0.8995802999 + 0.9201380014 0.9233281016 0.8999949098 + 0.9204949141 0.9236646891 0.9004095197 + 0.9208517075 0.9240012169 0.9008241296 + 0.9212086201 0.9243378043 0.9012386799 + 0.9215654135 0.9246743917 0.9016532898 + 0.9219222069 0.9250109196 0.9020677805 + 0.9222760797 0.9253453016 0.9024798870 + 0.9226300716 0.9256798029 0.9028918743 + 0.9229841232 0.9260141850 0.9033039808 + 0.9233379960 0.9263486862 0.9037160873 + 0.9236919880 0.9266831279 0.9041281939 + 0.9240459800 0.9270175099 0.9045401812 + 0.9243978262 0.9273512959 0.9049506783 + 0.9247484803 0.9276847839 0.9053602219 + 0.9250993133 0.9280182719 0.9057698250 + 0.9254500270 0.9283518195 0.9061794281 + 0.9258008003 0.9286853075 0.9065889716 + 0.9261515141 0.9290186763 0.9069985747 + 0.9265019298 0.9293516278 0.9074079990 + 0.9268512726 0.9296824932 0.9078168273 + 0.9272007942 0.9300134182 0.9082257152 + 0.9275501966 0.9303442836 0.9086344838 + 0.9278995991 0.9306753278 0.9090433121 + 0.9282490015 0.9310061932 0.9094521999 + 0.9285985231 0.9313371181 0.9098610282 + 0.9289464951 0.9316648245 0.9102689028 + 0.9292942286 0.9319915771 0.9106765985 + 0.9296419024 0.9323183894 0.9110842943 + 0.9299895763 0.9326452017 0.9114919901 + 0.9303374290 0.9329720736 0.9118996859 + 0.9306851029 0.9332988858 0.9123073816 + 0.9310320020 0.9336249232 0.9127150178 + 0.9313766956 0.9339488745 0.9131222963 + 0.9317213893 0.9342728257 0.9135295749 + 0.9320660830 0.9345967770 0.9139369130 + 0.9324107766 0.9349207878 0.9143441916 + 0.9327554703 0.9352446795 0.9147515297 + 0.9331002235 0.9355686903 0.9151586890 + 0.9334433079 0.9358913898 0.9155651927 + 0.9337856770 0.9362136126 0.9159712195 + 0.9341281056 0.9365357757 0.9163771868 + 0.9344704747 0.9368579984 0.9167832136 + 0.9348127842 0.9371802211 0.9171893001 + 0.9351552129 0.9375023842 0.9175953269 + 0.9354975224 0.9378244281 0.9180011749 + 0.9358382225 0.9381431937 0.9184055924 + 0.9361788034 0.9384620786 0.9188100100 + 0.9365195036 0.9387809038 0.9192144275 + 0.9368602037 0.9390997291 0.9196187854 + 0.9372009039 0.9394186139 0.9200232029 + 0.9375416040 0.9397373796 0.9204276204 + 0.9378811121 0.9400548935 0.9208316207 + 0.9382188916 0.9403703213 0.9212350845 + 0.9385566115 0.9406856894 0.9216386080 + 0.9388942719 0.9410011172 0.9220420718 + 0.9392319918 0.9413164854 0.9224454761 + 0.9395697117 0.9416319132 0.9228489995 + 0.9399074912 0.9419472814 0.9232525229 + 0.9402431250 0.9422618151 0.9236547947 + 0.9405775070 0.9425758123 0.9240565896 + 0.9409120083 0.9428898096 0.9244583249 + 0.9412463903 0.9432038069 0.9248600006 + 0.9415808916 0.9435179234 0.9252616763 + 0.9419152737 0.9438319206 0.9256634116 + 0.9422497749 0.9441459179 0.9260650873 + 0.9425830245 0.9444586039 0.9264650941 + 0.9429159760 0.9447709918 0.9268646836 + 0.9432489872 0.9450834990 0.9272642732 + 0.9435819983 0.9453958869 0.9276639223 + 0.9439150095 0.9457083941 0.9280635118 + 0.9442480206 0.9460207820 0.9284631014 + 0.9445809722 0.9463332891 0.9288625717 + 0.9449120760 0.9466425776 0.9292607903 + 0.9452432990 0.9469519854 0.9296590090 + 0.9455745220 0.9472612739 0.9300572276 + 0.9459056258 0.9475706816 0.9304553866 + 0.9462367892 0.9478800297 0.9308536053 + 0.9465680122 0.9481893778 0.9312518239 + 0.9468988180 0.9484986067 0.9316499233 + 0.9472268820 0.9488059878 0.9320474863 + 0.9475550056 0.9491134286 0.9324449897 + 0.9478831887 0.9494208097 0.9328426123 + 0.9482113123 0.9497281909 0.9332401156 + 0.9485393763 0.9500355721 0.9336376190 + 0.9488674998 0.9503430128 0.9340351820 + 0.9491953254 0.9506502748 0.9344326854 + 0.9495210052 0.9509568214 0.9348298907 + 0.9498466849 0.9512634277 0.9352269769 + 0.9501724243 0.9515699744 0.9356241822 + 0.9504981041 0.9518765211 0.9360213876 + 0.9508237839 0.9521831274 0.9364185929 + 0.9511495233 0.9524896741 0.9368157983 + 0.9514749050 0.9527959824 0.9372128248 + 0.9517980814 0.9531002045 0.9376080036 + 0.9521211982 0.9534044266 0.9380033016 + 0.9524443150 0.9537085891 0.9383985996 + 0.9527673721 0.9540126920 0.9387937784 + 0.9530906081 0.9543169141 0.9391890764 + 0.9534137249 0.9546210766 0.9395843744 + 0.9537366033 0.9549251795 0.9399794936 + 0.9540542960 0.9552258849 0.9403715134 + 0.9543719888 0.9555265903 0.9407635927 + 0.9546896815 0.9558274150 0.9411556125 + 0.9550073743 0.9561281204 0.9415475726 + 0.9553251266 0.9564288259 0.9419395924 + 0.9556428790 0.9567294717 0.9423316717 + 0.9559605718 0.9570301771 0.9427236915 + 0.9562745094 0.9573286772 0.9431132078 + 0.9565880895 0.9576269984 0.9435023069 + 0.9569017291 0.9579253197 0.9438915253 + 0.9572151899 0.9582235813 0.9442806244 + 0.9575288296 0.9585217834 0.9446697831 + 0.9578424096 0.9588201046 0.9450588822 + 0.9581558704 0.9591184258 0.9454481006 + 0.9584683776 0.9594156742 0.9458364844 + 0.9587804079 0.9597126842 0.9462245703 + 0.9590924978 0.9600095749 0.9466127157 + 0.9594045281 0.9603065252 0.9470008016 + 0.9597164989 0.9606034756 0.9473888874 + 0.9600285888 0.9609004259 0.9477769732 + 0.9603406191 0.9611973166 0.9481651187 + 0.9606512785 0.9614930749 0.9485527873 + 0.9609603882 0.9617875218 0.9489399791 + 0.9612696171 0.9620819092 0.9493271708 + 0.9615787268 0.9623762965 0.9497144818 + 0.9618877769 0.9626706839 0.9501016736 + 0.9621970057 0.9629650712 0.9504889250 + 0.9625061154 0.9632595181 0.9508761168 + 0.9628148079 0.9635536075 0.9512630105 + 0.9631211162 0.9638456106 0.9516479969 + 0.9634273052 0.9641376138 0.9520329833 + 0.9637334943 0.9644294977 0.9524180293 + 0.9640396833 0.9647215009 0.9528030157 + 0.9643458724 0.9650135040 0.9531880021 + 0.9646521211 0.9653053880 0.9535729885 + 0.9649583101 0.9655973911 0.9539579153 + 0.9652628899 0.9658880830 0.9543418884 + 0.9655668736 0.9661784768 0.9547253847 + 0.9658709168 0.9664688110 0.9551090002 + 0.9661749005 0.9667590857 0.9554926157 + 0.9664788246 0.9670494199 0.9558761120 + 0.9667828083 0.9673396945 0.9562597275 + 0.9670867920 0.9676300287 0.9566432834 + 0.9673900008 0.9679197073 0.9570264220 + 0.9676913023 0.9682080150 0.9574084878 + 0.9679926038 0.9684962034 0.9577906132 + 0.9682939053 0.9687843919 0.9581726789 + 0.9685953259 0.9690726995 0.9585548043 + 0.9688966274 0.9693608880 0.9589369297 + 0.9691979289 0.9696490765 0.9593191147 + 0.9694991708 0.9699373841 0.9597011805 + 0.9697983861 0.9702233076 0.9600812197 + 0.9700970054 0.9705085158 0.9604607821 + 0.9703956246 0.9707937837 0.9608402848 + 0.9706941843 0.9710791111 0.9612197876 + 0.9709929228 0.9713643789 0.9615994096 + 0.9712914824 0.9716497064 0.9619789124 + 0.9715901017 0.9719349742 0.9623584151 + 0.9718881845 0.9722197056 0.9627375007 + 0.9721844792 0.9725024104 0.9631149769 + 0.9724807143 0.9727851152 0.9634925127 + 0.9727770090 0.9730678201 0.9638699889 + 0.9730733037 0.9733504057 0.9642475247 + 0.9733694792 0.9736331105 0.9646250010 + 0.9736657739 0.9739158154 0.9650024772 + 0.9739621282 0.9741985202 0.9653798938 + 0.9742565155 0.9744799733 0.9657559991 + 0.9745497704 0.9747607112 0.9661310911 + 0.9748430252 0.9750415087 0.9665063024 + 0.9751362801 0.9753221869 0.9668813944 + 0.9754294753 0.9756029248 0.9672564864 + 0.9757227898 0.9758836031 0.9676316977 + 0.9760159850 0.9761644006 0.9680067897 + 0.9763092995 0.9764450788 0.9683818817 + 0.9765990973 0.9767239094 0.9687542915 + 0.9768885970 0.9770026207 0.9691264033 + 0.9771782160 0.9772812128 0.9694985747 + 0.9774677157 0.9775598049 0.9698706865 + 0.9777572751 0.9778385162 0.9702427983 + 0.9780467749 0.9781171083 0.9706150293 + 0.9783363938 0.9783958197 0.9709870815 + 0.9786252975 0.9786741138 0.9713587761 + 0.9789113998 0.9789509177 0.9717280865 + 0.9791973829 0.9792277217 0.9720973969 + 0.9794834852 0.9795044065 0.9724667072 + 0.9797695279 0.9797812104 0.9728360176 + 0.9800555110 0.9800580144 0.9732053280 + 0.9803416133 0.9803346992 0.9735746980 + 0.9806275964 0.9806115031 0.9739440084 + 0.9809123874 0.9808875918 0.9743121266 + 0.9811949134 0.9811623096 0.9746779799 + 0.9814773798 0.9814370275 0.9750438929 + 0.9817599058 0.9817116857 0.9754099250 + 0.9820423722 0.9819864035 0.9757757783 + 0.9823248982 0.9822611213 0.9761416912 + 0.9826074243 0.9825357795 0.9765076041 + 0.9828898907 0.9828104973 0.9768735170 + 0.9831706285 0.9830839038 0.9772381186 + 0.9834492803 0.9833558202 0.9776011109 + 0.9837279916 0.9836276770 0.9779641032 + 0.9840067029 0.9838995934 0.9783270955 + 0.9842854738 0.9841713905 0.9786900282 + 0.9845641851 0.9844433069 0.9790530205 + 0.9848428965 0.9847152233 0.9794160128 + 0.9851216078 0.9849870801 0.9797790051 + 0.9853982925 0.9852576256 0.9801405072 + 0.9856731296 0.9855268002 0.9805005789 + 0.9859479070 0.9857959747 0.9808607101 + 0.9862228036 0.9860652089 0.9812209010 + 0.9864975810 0.9863345027 0.9815809727 + 0.9867724180 0.9866036773 0.9819411039 + 0.9870471954 0.9868729115 0.9823011756 + 0.9873220921 0.9871420860 0.9826613069 + 0.9875941277 0.9874101877 0.9830198288 + 0.9878634810 0.9876769781 0.9833766818 + 0.9881328940 0.9879438281 0.9837337136 + 0.9884023070 0.9882106781 0.9840906262 + 0.9886716008 0.9884775281 0.9844474792 + 0.9889410138 0.9887443185 0.9848043919 + 0.9892104268 0.9890111089 0.9851613045 + 0.9894797802 0.9892780185 0.9855182171 + 0.9897462130 0.9895430803 0.9858739972 + 0.9900087714 0.9898058772 0.9862282276 + 0.9902712703 0.9900686741 0.9865823984 + 0.9905338287 0.9903314710 0.9869366288 + 0.9907963276 0.9905943274 0.9872906804 + 0.9910588264 0.9908571243 0.9876449108 + 0.9913213849 0.9911199212 0.9879990816 + 0.9915838838 0.9913827181 0.9883533120 + 0.9918448925 0.9916440845 0.9887064099 + 0.9921017885 0.9919016957 0.9890565872 + 0.9923588037 0.9921593070 0.9894067049 + 0.9926156998 0.9924169779 0.9897568822 + 0.9928725958 0.9926745892 0.9901071191 + 0.9931296110 0.9929322004 0.9904572964 + 0.9933865070 0.9931898713 0.9908074141 + 0.9936434031 0.9934474826 0.9911575913 + 0.9939001203 0.9937049747 0.9915075898 + 0.9941518903 0.9939597249 0.9918552041 + 0.9944037795 0.9942144156 0.9922026992 + 0.9946557283 0.9944692254 0.9925501943 + 0.9949074984 0.9947239161 0.9928978086 + 0.9951593876 0.9949786067 0.9932453036 + 0.9954112172 0.9952334166 0.9935927987 + 0.9956631064 0.9954881072 0.9939404130 + 0.9959149957 0.9957427979 0.9942879081 + 0.9961606860 0.9959961772 0.9946306944 + 0.9964045882 0.9962491989 0.9949722290 + 0.9966484904 0.9965021014 0.9953135848 + 0.9968923926 0.9967551231 0.9956551194 + 0.9971364141 0.9970080256 0.9959965944 + 0.9973803163 0.9972609878 0.9963380098 + 0.9976242185 0.9975138903 0.9966794848 + 0.9978681207 0.9977669120 0.9970210195 + 0.9981089234 0.9980177283 0.9973577261 + 0.9983453155 0.9982655048 0.9976879954 + 0.9985817075 0.9985132813 0.9980183244 + 0.9988179803 0.9987609982 0.9983485937 + 0.9990543723 0.9990087748 0.9986789227 + 0.9992908239 0.9992566109 0.9990090728 + 0.9995272160 0.9995043874 0.9993394017 + 0.9997636080 0.9997522235 0.9996696711 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_400.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_400.spi1d index 030a8fb1..4fc1a2fd 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_400.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_400.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0189804304 0.0202203300 0.0141742602 - 0.0334178284 0.0351021588 0.0260934699 - 0.0454768315 0.0478549302 0.0364666395 - 0.0562601797 0.0592830218 0.0458958298 - 0.0662030578 0.0697968081 0.0546298996 - 0.0755658001 0.0796677694 0.0628222525 - 0.0844966397 0.0889748707 0.0705531687 - 0.0930858031 0.0978165492 0.0779282674 - 0.1014240980 0.1062681973 0.0849699080 - 0.1095660999 0.1143971011 0.0916869491 - 0.1175450981 0.1221864969 0.0981442481 - 0.1253249943 0.1296440959 0.1043419018 - 0.1328340024 0.1367834061 0.1102854982 - 0.1399984062 0.1436239928 0.1160039008 - 0.1468109936 0.1502085030 0.1215291023 - 0.1533181965 0.1565480977 0.1268731952 - 0.1595371962 0.1626639962 0.1320544034 - 0.1655108929 0.1685764045 0.1370812953 - 0.1712646037 0.1742939055 0.1419793069 - 0.1768248975 0.1798332930 0.1467563957 - 0.1822118014 0.1852239966 0.1514313072 - 0.1874231994 0.1904760003 0.1559925973 - 0.1924864948 0.1955907047 0.1604602933 - 0.1974117011 0.2005726993 0.1648356020 - 0.2021995932 0.2054260969 0.1691132933 - 0.2068601996 0.2101702988 0.1733085066 - 0.2114142030 0.2148002982 0.1774341017 - 0.2158668935 0.2193187028 0.1814931035 - 0.2202159017 0.2237484008 0.1854856014 - 0.2244572043 0.2280993015 0.1894098967 - 0.2285912931 0.2323631048 0.1932736933 - 0.2326336950 0.2365342975 0.1970842034 - 0.2366016954 0.2406188995 0.2008334994 - 0.2405177951 0.2446300983 0.2045322061 - 0.2443760037 0.2485861927 0.2081629038 - 0.2481773049 0.2524932921 0.2117401063 - 0.2519231141 0.2563455105 0.2152681053 - 0.2556205094 0.2601292133 0.2187464982 - 0.2592709064 0.2638446987 0.2221813947 - 0.2628698945 0.2674998045 0.2255706042 - 0.2664135993 0.2711037993 0.2289170027 - 0.2698990107 0.2746542096 0.2322203964 - 0.2733238041 0.2781482935 0.2354855984 - 0.2766858041 0.2815822065 0.2387184948 - 0.2799929976 0.2849547863 0.2419216931 - 0.2832500935 0.2882680893 0.2450876981 - 0.2864542007 0.2915301919 0.2482271940 - 0.2896077931 0.2947526872 0.2513332069 - 0.2927064896 0.2979423106 0.2543987930 - 0.2957524955 0.3010992110 0.2574329972 - 0.2987526953 0.3042244911 0.2604371011 - 0.3017115891 0.3073199987 0.2634117007 - 0.3046318889 0.3103852868 0.2663584054 - 0.3075189888 0.3134222031 0.2692733109 - 0.3103761971 0.3164314032 0.2721604109 - 0.3132067025 0.3194135129 0.2750195861 - 0.3160125911 0.3223691881 0.2778514028 - 0.3187975883 0.3252983987 0.2806541920 - 0.3215644956 0.3282026947 0.2834334970 - 0.3243156970 0.3310827017 0.2861855924 - 0.3270539045 0.3339388072 0.2889156938 - 0.3297814131 0.3367714882 0.2916230857 - 0.3325003982 0.3395748138 0.2943058908 - 0.3352132142 0.3423477113 0.2969624102 - 0.3379161954 0.3450841904 0.2995971143 - 0.3405976892 0.3477846980 0.3022120893 - 0.3432567120 0.3504517972 0.3048096895 - 0.3458822072 0.3530882001 0.3073847890 - 0.3484756052 0.3556962013 0.3099358082 - 0.3510380089 0.3582764864 0.3124623895 - 0.3535713851 0.3608322144 0.3149712980 - 0.3560791910 0.3633658886 0.3174606860 - 0.3585632145 0.3658784032 0.3199287057 - 0.3610230982 0.3683710098 0.3223791122 - 0.3634631932 0.3708465993 0.3248122036 - 0.3658835888 0.3732995093 0.3272255063 - 0.3682858050 0.3757315874 0.3296200931 - 0.3706724048 0.3781447113 0.3319934905 - 0.3730427027 0.3805387020 0.3343470097 - 0.3754003942 0.3829126954 0.3366861939 - 0.3777439892 0.3852662146 0.3390057981 - 0.3800773025 0.3876036108 0.3413096070 - 0.3823992014 0.3899244070 0.3435958922 - 0.3847126067 0.3922333121 0.3458688855 - 0.3870173991 0.3945314884 0.3481296897 - 0.3893153071 0.3968203068 0.3503778875 - 0.3916071951 0.3991019130 0.3526121080 - 0.3938935995 0.4013710022 0.3548285067 - 0.3961760998 0.4036257863 0.3570280075 - 0.3984551132 0.4058659971 0.3592115045 - 0.4007292092 0.4080862999 0.3613776863 - 0.4029959142 0.4102860093 0.3635272980 - 0.4052543938 0.4124661088 0.3656620979 - 0.4075042903 0.4146254957 0.3677802980 - 0.4097425938 0.4167692065 0.3698835075 - 0.4119692147 0.4188987911 0.3719711006 - 0.4141839147 0.4210126996 0.3740384877 - 0.4163868129 0.4231149852 0.3760899901 - 0.4185790122 0.4252065122 0.3781270981 - 0.4207611978 0.4272862077 0.3801490963 - 0.4229308069 0.4293535054 0.3821552098 - 0.4250904024 0.4314081967 0.3841435015 - 0.4272392094 0.4334529042 0.3861171901 - 0.4293777943 0.4354852140 0.3880780041 - 0.4315068126 0.4375067949 0.3900259137 - 0.4336226881 0.4395155013 0.3919605017 - 0.4357245862 0.4415121973 0.3938809931 - 0.4378120899 0.4434967041 0.3957869112 - 0.4398863018 0.4454723895 0.3976789117 - 0.4419437051 0.4474394917 0.3995566964 - 0.4439849854 0.4493967891 0.4014219940 - 0.4460108876 0.4513399899 0.4032762945 - 0.4480217993 0.4532704055 0.4051195979 - 0.4500184953 0.4551894069 0.4069514871 - 0.4520016015 0.4570955932 0.4087724090 - 0.4539715946 0.4589887857 0.4105815887 - 0.4559291005 0.4608702958 0.4123832881 - 0.4578745961 0.4627414048 0.4141762853 - 0.4598087966 0.4646035135 0.4159627855 - 0.4617320895 0.4664578140 0.4177407026 - 0.4636450112 0.4683054090 0.4195122123 - 0.4655455053 0.4701451063 0.4212754071 - 0.4674327075 0.4719766080 0.4230324030 - 0.4693084955 0.4737985134 0.4247812033 - 0.4711722136 0.4756115079 0.4265238941 - 0.4730255902 0.4774144888 0.4282588065 - 0.4748682976 0.4792071879 0.4299876094 - 0.4766997099 0.4809884131 0.4317089021 - 0.4785183072 0.4827584922 0.4334239960 - 0.4803259969 0.4845187962 0.4351322055 - 0.4821228087 0.4862686992 0.4368340969 - 0.4839096963 0.4880078137 0.4385294914 - 0.4856866896 0.4897367954 0.4402182996 - 0.4874519110 0.4914573133 0.4419007897 - 0.4892069101 0.4931693077 0.4435743093 - 0.4909515083 0.4948728085 0.4452398121 - 0.4926868081 0.4965651035 0.4468953907 - 0.4944101870 0.4982514977 0.4485433102 - 0.4961231947 0.4999290109 0.4501810968 - 0.4978268147 0.5015965104 0.4518086910 - 0.4995205998 0.5032545924 0.4534263909 - 0.5012040734 0.5049039721 0.4550347030 - 0.5028777719 0.5065457225 0.4566338062 - 0.5045427084 0.5081775188 0.4582231045 - 0.5061982274 0.5097963214 0.4598028064 - 0.5078454018 0.5114061832 0.4613744020 - 0.5094835758 0.5130059123 0.4629381895 - 0.5111126900 0.5145952106 0.4644944072 - 0.5127326846 0.5161756873 0.4660434127 - 0.5143399239 0.5177451968 0.4675836861 - 0.5159363747 0.5193077922 0.4691169858 - 0.5175241828 0.5208638906 0.4706437886 - 0.5191026926 0.5224146247 0.4721643925 - 0.5206707716 0.5239610076 0.4736790955 - 0.5222259164 0.5254994035 0.4751864076 - 0.5237668157 0.5270283818 0.4766879082 - 0.5252965093 0.5285490751 0.4781832099 - 0.5268157721 0.5300623775 0.4796718955 - 0.5283229947 0.5315675139 0.4811522961 - 0.5298194885 0.5330659151 0.4826253057 - 0.5313063860 0.5345565081 0.4840922058 - 0.5327833891 0.5360388160 0.4855529070 - 0.5342487097 0.5375118852 0.4870058000 - 0.5357007980 0.5389770865 0.4884487092 - 0.5371450782 0.5404356718 0.4898858070 - 0.5385817885 0.5418878198 0.4913172126 - 0.5400090218 0.5433310866 0.4927395880 - 0.5414276123 0.5447692871 0.4941565096 - 0.5428383946 0.5462031960 0.4955680966 - 0.5442407131 0.5476328731 0.4969739914 - 0.5456290841 0.5490533710 0.4983738065 - 0.5470101833 0.5504671931 0.4997689128 - 0.5483844876 0.5518745184 0.5011597872 - 0.5497499108 0.5532733202 0.5025461912 - 0.5511074066 0.5546665788 0.5039262772 - 0.5524582863 0.5560551286 0.5053020120 - 0.5538002253 0.5574359298 0.5066723824 - 0.5551335216 0.5588095188 0.5080367923 - 0.5564607978 0.5601773858 0.5093978047 - 0.5577800274 0.5615364909 0.5107548833 - 0.5590916276 0.5628885031 0.5121051073 - 0.5603979826 0.5642359853 0.5134513974 - 0.5616976023 0.5655778050 0.5147926211 - 0.5629879832 0.5669124126 0.5161284208 - 0.5642735958 0.5682426095 0.5174611807 - 0.5655525923 0.5695660710 0.5187898874 - 0.5668237805 0.5708814859 0.5201147199 - 0.5680909753 0.5721923113 0.5214365721 - 0.5693522096 0.5734962225 0.5227527022 - 0.5706079006 0.5747932792 0.5240650773 - 0.5718598962 0.5760861039 0.5253748298 - 0.5731037259 0.5773720741 0.5266771913 - 0.5743418932 0.5786536932 0.5279759169 - 0.5755749941 0.5799306035 0.5292711258 - 0.5768011808 0.5811964869 0.5305575132 - 0.5780242085 0.5824581981 0.5318406820 - 0.5792413950 0.5837122202 0.5331183076 - 0.5804504156 0.5849606991 0.5343890190 - 0.5816553235 0.5862060785 0.5356559753 - 0.5828509927 0.5874449015 0.5369136930 - 0.5840438008 0.5886800289 0.5381681919 - 0.5852320194 0.5899084210 0.5394172072 - 0.5864148736 0.5911297202 0.5406591892 - 0.5875951052 0.5923475027 0.5418981910 - 0.5887641907 0.5935558081 0.5431274176 - 0.5899302959 0.5947611928 0.5443540215 - 0.5910912156 0.5959597826 0.5455750823 - 0.5922474265 0.5971537828 0.5467911959 - 0.5934010148 0.5983446836 0.5480040908 - 0.5945463777 0.5995286107 0.5492113233 - 0.5956892967 0.6007102132 0.5504168272 - 0.5968241096 0.6018840075 0.5516167283 - 0.5979555845 0.6030550003 0.5528140068 - 0.5990828276 0.6042217016 0.5540074110 - 0.6002063751 0.6053854227 0.5551959872 - 0.6013283134 0.6065471768 0.5563817024 - 0.6024429202 0.6077020764 0.5575634837 - 0.6035555005 0.6088551879 0.5587441921 - 0.6046581268 0.6099976897 0.5599179268 - 0.6057583094 0.6111379862 0.5610895753 - 0.6068522930 0.6122714281 0.5622550845 - 0.6079434156 0.6134017706 0.5634173751 - 0.6090310216 0.6145281196 0.5645756125 - 0.6101149917 0.6156514287 0.5657300949 - 0.6111965775 0.6167725921 0.5668820143 - 0.6122707725 0.6178879738 0.5680285096 - 0.6133425236 0.6190009117 0.5691730976 - 0.6144058704 0.6201071143 0.5703091025 - 0.6154682040 0.6212120056 0.5714436173 - 0.6165214777 0.6223102212 0.5725712180 - 0.6175739169 0.6234076023 0.5736979842 - 0.6186174750 0.6244966984 0.5748155117 - 0.6196597815 0.6255844831 0.5759319067 - 0.6206961274 0.6266639233 0.5770416856 - 0.6217305064 0.6277418733 0.5781496763 - 0.6227589250 0.6288138032 0.5792517066 - 0.6237844825 0.6298843026 0.5803521276 - 0.6248034835 0.6309509873 0.5814481974 - 0.6258192062 0.6320158839 0.5825421214 - 0.6268281937 0.6330764294 0.5836308002 - 0.6278346777 0.6341344714 0.5847169161 - 0.6288362145 0.6351872087 0.5857974887 - 0.6298360229 0.6362372041 0.5868756175 - 0.6308323741 0.6372811794 0.5879486203 - 0.6318262815 0.6383233070 0.5890188813 - 0.6328150034 0.6393606067 0.5900835991 - 0.6338009238 0.6403964162 0.5911462903 - 0.6347796917 0.6414275765 0.5922037959 - 0.6357566714 0.6424568295 0.5932598710 - 0.6367279887 0.6434782147 0.5943114161 - 0.6376978755 0.6444982290 0.5953620076 - 0.6386622190 0.6455100775 0.5964084864 - 0.6396257281 0.6465215087 0.5974544287 - 0.6405826807 0.6475260854 0.5984926224 - 0.6415395737 0.6485307217 0.5995308161 - 0.6424885988 0.6495286226 0.6005603075 - 0.6434376836 0.6505261064 0.6015896797 - 0.6443789005 0.6515172720 0.6026117206 - 0.6453189850 0.6525071859 0.6036325097 - 0.6462522745 0.6534914970 0.6046487093 - 0.6471831799 0.6544733047 0.6056631207 - 0.6481100917 0.6554508209 0.6066738963 - 0.6490340829 0.6564248204 0.6076821089 - 0.6499556899 0.6573960781 0.6086879969 - 0.6508737803 0.6583626866 0.6096904278 - 0.6517899036 0.6593278050 0.6106911898 - 0.6526989937 0.6602864265 0.6116858125 - 0.6536079049 0.6612449884 0.6126803160 - 0.6545088291 0.6621980071 0.6136667728 - 0.6554095745 0.6631507874 0.6146532297 - 0.6563045979 0.6640974879 0.6156343222 - 0.6571980119 0.6650422812 0.6166138053 - 0.6580873132 0.6659818292 0.6175904274 - 0.6589732170 0.6669161916 0.6185643077 - 0.6598567963 0.6678487062 0.6195365787 - 0.6607350707 0.6687750220 0.6205043793 - 0.6616132259 0.6697012782 0.6214722991 - 0.6624845862 0.6706203818 0.6224306226 - 0.6633558869 0.6715393066 0.6233887076 - 0.6642240882 0.6724529266 0.6243417859 - 0.6650909781 0.6733639836 0.6252925992 - 0.6659563780 0.6742721200 0.6262406707 - 0.6668195128 0.6751747727 0.6271843910 - 0.6676822901 0.6760774255 0.6281278729 - 0.6685385704 0.6769707799 0.6290643811 - 0.6693950295 0.6778641939 0.6300008297 - 0.6702473760 0.6787514091 0.6309322119 - 0.6710978150 0.6796351075 0.6318609715 - 0.6719467044 0.6805168986 0.6327874064 - 0.6727918983 0.6813933253 0.6337075233 - 0.6736372113 0.6822696924 0.6346276999 - 0.6744775772 0.6831405163 0.6355414987 - 0.6753175259 0.6840105057 0.6364545226 - 0.6761547923 0.6848769188 0.6373646855 - 0.6769893765 0.6857391000 0.6382718086 - 0.6778236032 0.6866012216 0.6391788125 - 0.6786518097 0.6874572039 0.6400815248 - 0.6794800162 0.6883131862 0.6409841180 - 0.6803044081 0.6891654730 0.6418827176 - 0.6811261177 0.6900150180 0.6427782774 - 0.6819474101 0.6908640862 0.6436731815 - 0.6827651858 0.6917092800 0.6445615292 - 0.6835830212 0.6925545931 0.6454498768 - 0.6843972206 0.6933962703 0.6463342905 - 0.6852092743 0.6942355037 0.6472160816 - 0.6860206127 0.6950742006 0.6480973959 - 0.6868252754 0.6959053874 0.6489725709 - 0.6876299977 0.6967365742 0.6498478055 - 0.6884319782 0.6975647807 0.6507201195 - 0.6892318130 0.6983901858 0.6515899897 - 0.6900315285 0.6992155910 0.6524599791 - 0.6908273101 0.7000362873 0.6533246040 - 0.6916229725 0.7008566856 0.6541891098 - 0.6924160719 0.7016751170 0.6550511122 - 0.6932054162 0.7024903297 0.6559094191 - 0.6939945817 0.7033056021 0.6567676067 - 0.6947793961 0.7041168213 0.6576201916 - 0.6955630183 0.7049267292 0.6584712863 - 0.6963458061 0.7057358027 0.6593216062 - 0.6971247196 0.7065396905 0.6601678133 - 0.6979035139 0.7073435187 0.6610140204 - 0.6986786723 0.7081443071 0.6618571877 - 0.6994504929 0.7089418769 0.6626976728 - 0.7002223134 0.7097393870 0.6635380983 - 0.7009885907 0.7105317712 0.6643719077 - 0.7017540932 0.7113230824 0.6652045250 - 0.7025185823 0.7121133804 0.6660360098 - 0.7032787204 0.7128974795 0.6668620110 - 0.7040386796 0.7136815786 0.6676880717 - 0.7047963738 0.7144626975 0.6685119271 - 0.7055510879 0.7152395248 0.6693329811 - 0.7063059211 0.7160161734 0.6701539755 - 0.7070568204 0.7167897224 0.6709715724 - 0.7078062296 0.7175614834 0.6717876792 - 0.7085556984 0.7183333039 0.6726037860 - 0.7092993855 0.7191014290 0.6734123826 - 0.7100430131 0.7198691964 0.6742205024 - 0.7107855082 0.7206360102 0.6750273705 - 0.7115231752 0.7213973999 0.6758279204 - 0.7122610211 0.7221586704 0.6766282916 - 0.7129974961 0.7229182124 0.6774265170 - 0.7137318254 0.7236738801 0.6782206893 - 0.7144662142 0.7244296074 0.6790148020 - 0.7151979804 0.7251828909 0.6798056960 - 0.7159276009 0.7259337902 0.6805936098 - 0.7166572809 0.7266848087 0.6813815832 - 0.7173827887 0.7274302840 0.6821650267 - 0.7181063890 0.7281727791 0.6829462051 - 0.7188299894 0.7289153934 0.6837273836 - 0.7195504904 0.7296513915 0.6845042706 - 0.7202703953 0.7303856015 0.6852802038 - 0.7209903002 0.7311198115 0.6860560775 - 0.7217056751 0.7318493724 0.6868286729 - 0.7224205732 0.7325783968 0.6876009703 - 0.7231354713 0.7333073020 0.6883733273 - 0.7238441706 0.7340301275 0.6891400218 - 0.7245526910 0.7347525954 0.6899065971 - 0.7252612114 0.7354750037 0.6906729937 - 0.7259656787 0.7361903191 0.6914319992 - 0.7266703248 0.7369055152 0.6921910048 - 0.7273744941 0.7376205921 0.6929497123 - 0.7280738950 0.7383303046 0.6937026978 - 0.7287732959 0.7390400171 0.6944558024 - 0.7294721007 0.7397493720 0.6952087283 - 0.7301635146 0.7404522896 0.6959573030 - 0.7308549285 0.7411552072 0.6967059970 - 0.7315459847 0.7418580055 0.6974545121 - 0.7322310805 0.7425541878 0.6981990933 - 0.7329162955 0.7432504296 0.6989436150 - 0.7336012721 0.7439466119 0.6996881962 - 0.7342811823 0.7446365952 0.7004261017 - 0.7349609733 0.7453263998 0.7011638284 - 0.7356408238 0.7460162044 0.7019016147 - 0.7363131046 0.7467001081 0.7026333809 - 0.7369849086 0.7473831773 0.7033646703 - 0.7376567125 0.7480663061 0.7040959001 - 0.7383230925 0.7487441897 0.7048221231 - 0.7389885187 0.7494208217 0.7055472136 - 0.7396538258 0.7500973940 0.7062721848 - 0.7403153777 0.7507699132 0.7069926858 - 0.7409753799 0.7514402866 0.7077111006 - 0.7416353822 0.7521107197 0.7084295154 - 0.7422909141 0.7527776957 0.7091451287 - 0.7429429293 0.7534415126 0.7098584771 - 0.7435948253 0.7541053891 0.7105718851 - 0.7442438006 0.7547671199 0.7112827897 - 0.7448878288 0.7554246187 0.7119894028 - 0.7455319166 0.7560821176 0.7126960754 - 0.7461751103 0.7567387223 0.7134013772 - 0.7468137741 0.7573903203 0.7140998244 - 0.7474524975 0.7580417991 0.7147980928 - 0.7480912209 0.7586933970 0.7154964209 - 0.7487258911 0.7593389750 0.7161883712 - 0.7493603826 0.7599840760 0.7168800235 - 0.7499948740 0.7606291771 0.7175716162 - 0.7506251931 0.7612702250 0.7182598114 - 0.7512537837 0.7619091272 0.7189463973 - 0.7518823147 0.7625480294 0.7196329236 - 0.7525088191 0.7631847262 0.7203181982 - 0.7531324029 0.7638177872 0.7210013866 - 0.7537561059 0.7644509077 0.7216845751 - 0.7543792725 0.7650834918 0.7223674059 - 0.7549992204 0.7657102942 0.7230455279 - 0.7556189895 0.7663369775 0.7237235904 - 0.7562388778 0.7669637203 0.7244017124 - 0.7568544745 0.7675850987 0.7250748277 - 0.7574688792 0.7682043910 0.7257462144 - 0.7580832839 0.7688236833 0.7264177203 - 0.7586959004 0.7694402933 0.7270871997 - 0.7593057752 0.7700520158 0.7277535200 - 0.7599158287 0.7706636190 0.7284198999 - 0.7605257034 0.7712752819 0.7290862799 - 0.7611322999 0.7718833089 0.7297480106 - 0.7617387772 0.7724912167 0.7304096818 - 0.7623453736 0.7730990052 0.7310714126 - 0.7629489899 0.7737038136 0.7317304015 - 0.7635505795 0.7743061781 0.7323871851 - 0.7641521096 0.7749084830 0.7330440283 - 0.7647531033 0.7755101919 0.7337000966 - 0.7653506994 0.7761059999 0.7343516946 - 0.7659481764 0.7767019272 0.7350031734 - 0.7665457726 0.7772976756 0.7356547117 - 0.7671412826 0.7778910995 0.7363024950 - 0.7677358985 0.7784829140 0.7369480133 - 0.7683303952 0.7790747881 0.7375935912 - 0.7689242959 0.7796661258 0.7382383943 - 0.7695142031 0.7802531719 0.7388777137 - 0.7701042295 0.7808402181 0.7395170927 - 0.7706941962 0.7814273238 0.7401564121 - 0.7712818980 0.7820110917 0.7407931089 - 0.7718682289 0.7825924754 0.7414278984 - 0.7724546194 0.7831739187 0.7420626879 - 0.7730407715 0.7837553024 0.7426972985 - 0.7736244798 0.7843334079 0.7433282137 - 0.7742081881 0.7849115729 0.7439591289 - 0.7747918963 0.7854897976 0.7445899844 - 0.7753731012 0.7860661745 0.7452182174 - 0.7759513855 0.7866402864 0.7458429933 - 0.7765296102 0.7872143984 0.7464678288 - 0.7771078944 0.7877886295 0.7470927238 - 0.7776818871 0.7883577943 0.7477130294 - 0.7782551050 0.7889257073 0.7483323216 - 0.7788282037 0.7894936800 0.7489516139 - 0.7794010043 0.7900611162 0.7495700717 - 0.7799720764 0.7906246781 0.7501844764 - 0.7805430889 0.7911882997 0.7507988214 - 0.7811141014 0.7917519212 0.7514132261 - 0.7816827893 0.7923139930 0.7520251870 - 0.7822489142 0.7928743958 0.7526345253 - 0.7828149796 0.7934347987 0.7532438040 - 0.7833812237 0.7939952016 0.7538530827 - 0.7839428782 0.7945516706 0.7544584870 - 0.7845032215 0.7951068282 0.7550626993 - 0.7850636244 0.7956619859 0.7556669116 - 0.7856239080 0.7962170839 0.7562711239 - 0.7861807942 0.7967674732 0.7568696141 - 0.7867377996 0.7973179221 0.7574681044 - 0.7872946858 0.7978683114 0.7580665946 - 0.7878506184 0.7984176874 0.7586637139 - 0.7884032726 0.7989639044 0.7592564225 - 0.7889561057 0.7995100021 0.7598490119 - 0.7895088196 0.8000562191 0.7604417205 - 0.7900593281 0.8006005883 0.7610327005 - 0.7906069756 0.8011425138 0.7616212964 - 0.7911546826 0.8016844988 0.7622098923 - 0.7917025089 0.8022264242 0.7627984881 - 0.7922484875 0.8027654886 0.7633846998 - 0.7927935123 0.8033025861 0.7639691830 - 0.7933384180 0.8038396239 0.7645537257 - 0.7938833833 0.8043767214 0.7651382089 - 0.7944257855 0.8049103022 0.7657201290 - 0.7949671745 0.8054423928 0.7663009763 - 0.7955086827 0.8059744239 0.7668818831 - 0.7960500717 0.8065065145 0.7674626708 - 0.7965880036 0.8070362210 0.7680410743 - 0.7971251011 0.8075652122 0.7686188221 - 0.7976623178 0.8080940843 0.7691965103 - 0.7981994152 0.8086230755 0.7697743177 - 0.7987347245 0.8091489077 0.7703492045 - 0.7992696762 0.8096740842 0.7709236741 - 0.7998046875 0.8101993203 0.7714982033 - 0.8003398180 0.8107244968 0.7720727921 - 0.8008722067 0.8112462759 0.7726444006 - 0.8014042974 0.8117675781 0.7732155919 - 0.8019365072 0.8122889996 0.7737869024 - 0.8024685979 0.8128103018 0.7743582129 - 0.8029965162 0.8133283257 0.7749258876 - 0.8035238981 0.8138456941 0.7754932046 - 0.8040512204 0.8143631816 0.7760605216 - 0.8045786023 0.8148807287 0.7766278982 - 0.8051033020 0.8153948188 0.7771916986 - 0.8056275249 0.8159083128 0.7777550220 - 0.8061518073 0.8164218068 0.7783182859 - 0.8066759706 0.8169354200 0.7788816094 - 0.8071985841 0.8174456954 0.7794430852 - 0.8077207208 0.8179551959 0.7800040841 - 0.8082427979 0.8184646964 0.7805652022 - 0.8087648749 0.8189740777 0.7811262012 - 0.8092843294 0.8194804788 0.7816848159 - 0.8098024726 0.8199853897 0.7822422981 - 0.8103206158 0.8204901814 0.7827997208 - 0.8108388186 0.8209950924 0.7833572030 - 0.8113549948 0.8214983940 0.7839121819 - 0.8118696213 0.8220003843 0.7844650149 - 0.8123841882 0.8225023746 0.7850177884 - 0.8128988147 0.8230044842 0.7855706811 - 0.8134121895 0.8235049844 0.7861223817 - 0.8139237761 0.8240029812 0.7866721749 - 0.8144353032 0.8245009780 0.7872220874 - 0.8149467707 0.8249989152 0.7877719998 - 0.8154575229 0.8254960775 0.7883214951 - 0.8159641027 0.8259888887 0.7888690233 - 0.8164708018 0.8264816999 0.7894164920 - 0.8169775009 0.8269745111 0.7899640203 - 0.8174842000 0.8274673223 0.7905114889 - 0.8179872036 0.8279569149 0.7910552025 - 0.8184900284 0.8284462094 0.7915986776 - 0.8189927936 0.8289356232 0.7921422124 - 0.8194956779 0.8294249177 0.7926856279 - 0.8199970126 0.8299115896 0.7932267785 - 0.8204975128 0.8303967714 0.7937667966 - 0.8209980130 0.8308820128 0.7943068743 - 0.8214985728 0.8313671947 0.7948468924 - 0.8219977021 0.8318507075 0.7953858972 - 0.8224940896 0.8323307037 0.7959229946 - 0.8229905963 0.8328106999 0.7964602113 - 0.8234871030 0.8332906961 0.7969973087 - 0.8239834905 0.8337706923 0.7975344062 - 0.8244758844 0.8342465758 0.7980670929 - 0.8249682188 0.8347225189 0.7985997200 - 0.8254604936 0.8351984024 0.7991324067 - 0.8259528875 0.8356742859 0.7996649742 - 0.8264433146 0.8361485004 0.8001952767 - 0.8269324899 0.8366215229 0.8007240295 - 0.8274217248 0.8370944858 0.8012526035 - 0.8279109001 0.8375675082 0.8017812967 - 0.8283994198 0.8380396962 0.8023093939 - 0.8288854957 0.8385075927 0.8028346896 - 0.8293715119 0.8389754891 0.8033601046 - 0.8298575878 0.8394433856 0.8038854003 - 0.8303436041 0.8399114013 0.8044108152 - 0.8308259249 0.8403760791 0.8049324751 - 0.8313068748 0.8408395052 0.8054528832 - 0.8317878842 0.8413029909 0.8059732914 - 0.8322690129 0.8417664170 0.8064936996 - 0.8327487707 0.8422294855 0.8070130944 - 0.8332253098 0.8426914215 0.8075290918 - 0.8337017894 0.8431531787 0.8080450892 - 0.8341782093 0.8436151147 0.8085610867 - 0.8346546888 0.8440768719 0.8090770841 - 0.8351281881 0.8445361853 0.8095903993 - 0.8356006742 0.8449943066 0.8101025224 - 0.8360731006 0.8454524279 0.8106147051 - 0.8365455270 0.8459104896 0.8111268878 - 0.8370172977 0.8463678956 0.8116385937 - 0.8374856114 0.8468207121 0.8121470809 - 0.8379539847 0.8472735286 0.8126556277 - 0.8384222984 0.8477262855 0.8131639957 - 0.8388906121 0.8481791019 0.8136724830 - 0.8393570185 0.8486301899 0.8141798973 - 0.8398215771 0.8490794897 0.8146862984 - 0.8402863145 0.8495287895 0.8151926994 - 0.8407509923 0.8499780893 0.8156991005 - 0.8412157297 0.8504275084 0.8162055016 - 0.8416776061 0.8508747816 0.8167092800 - 0.8421391249 0.8513218760 0.8172127008 - 0.8426007032 0.8517689705 0.8177161217 - 0.8430622220 0.8522161245 0.8182194829 - 0.8435230851 0.8526622057 0.8187221289 - 0.8439813852 0.8531038761 0.8192216754 - 0.8444396853 0.8535456061 0.8197212219 - 0.8448979855 0.8539872766 0.8202207088 - 0.8453562856 0.8544290066 0.8207201958 - 0.8458127975 0.8548687100 0.8212186098 - 0.8462672830 0.8553062081 0.8217157722 - 0.8467218876 0.8557437062 0.8222129941 - 0.8471763730 0.8561813235 0.8227102160 - 0.8476309180 0.8566188216 0.8232073784 - 0.8480827212 0.8570545912 0.8237022758 - 0.8485333920 0.8574895859 0.8241963983 - 0.8489841223 0.8579246998 0.8246904016 - 0.8494347930 0.8583598137 0.8251844049 - 0.8498855233 0.8587948084 0.8256785274 - 0.8503341079 0.8592264056 0.8261687160 - 0.8507825136 0.8596577048 0.8266587257 - 0.8512309790 0.8600888848 0.8271486163 - 0.8516793847 0.8605201840 0.8276385069 - 0.8521276116 0.8609511256 0.8281282187 - 0.8525732756 0.8613770008 0.8286157250 - 0.8530188799 0.8618029952 0.8291031122 - 0.8534644842 0.8622288704 0.8295904994 - 0.8539102077 0.8626548052 0.8300778866 - 0.8543549776 0.8630800843 0.8305646181 - 0.8547968268 0.8635025024 0.8310487270 - 0.8552384973 0.8639248013 0.8315327764 - 0.8556802869 0.8643472195 0.8320168257 - 0.8561220169 0.8647695184 0.8325008750 - 0.8565629721 0.8651912212 0.8329836726 - 0.8570019007 0.8656111956 0.8334634900 - 0.8574408293 0.8660312295 0.8339433074 - 0.8578798175 0.8664512038 0.8344230056 - 0.8583186865 0.8668712974 0.8349028230 - 0.8587570786 0.8672900796 0.8353816867 - 0.8591942787 0.8677062988 0.8358588219 - 0.8596314788 0.8681225181 0.8363357782 - 0.8600686789 0.8685386777 0.8368129134 - 0.8605058789 0.8689548969 0.8372899294 - 0.8609418869 0.8693702221 0.8377658725 - 0.8613752723 0.8697832823 0.8382393718 - 0.8618087173 0.8701964021 0.8387128711 - 0.8622421026 0.8706095219 0.8391863704 - 0.8626756072 0.8710225224 0.8396599889 - 0.8631076813 0.8714349866 0.8401322961 - 0.8635362983 0.8718457222 0.8406013846 - 0.8639649153 0.8722563982 0.8410704136 - 0.8643935919 0.8726670146 0.8415395021 - 0.8648222089 0.8730776906 0.8420084715 - 0.8652505279 0.8734877110 0.8424770832 - 0.8656774759 0.8738945723 0.8429431915 - 0.8661044836 0.8743014932 0.8434094191 - 0.8665313721 0.8747084141 0.8438755274 - 0.8669583797 0.8751152754 0.8443416953 - 0.8673852086 0.8755220175 0.8448076844 - 0.8678094745 0.8759253025 0.8452700973 - 0.8682336807 0.8763285875 0.8457326293 - 0.8686580062 0.8767318726 0.8461951017 - 0.8690822124 0.8771352172 0.8466575146 - 0.8695064783 0.8775385022 0.8471199870 - 0.8699274063 0.8779397011 0.8475784063 - 0.8703479767 0.8783406019 0.8480362296 - 0.8707684875 0.8787413836 0.8484939933 - 0.8711889982 0.8791422844 0.8489518762 - 0.8716096282 0.8795431852 0.8494096994 - 0.8720285892 0.8799425960 0.8498654962 - 0.8724468946 0.8803411722 0.8503202796 - 0.8728652000 0.8807398081 0.8507750034 - 0.8732835054 0.8811385036 0.8512297869 - 0.8737016916 0.8815370798 0.8516845107 - 0.8741189241 0.8819342852 0.8521381021 - 0.8745344281 0.8823292851 0.8525899053 - 0.8749498725 0.8827242255 0.8530418277 - 0.8753653765 0.8831192255 0.8534936905 - 0.8757808805 0.8835142255 0.8539454937 - 0.8761960268 0.8839088082 0.8543971181 - 0.8766077161 0.8842998147 0.8548464775 - 0.8770195246 0.8846908212 0.8552958965 - 0.8774312735 0.8850818872 0.8557451963 - 0.8778430223 0.8854728937 0.8561946154 - 0.8782547712 0.8858640194 0.8566439748 - 0.8786643744 0.8862522244 0.8570917845 - 0.8790733814 0.8866394758 0.8575391173 - 0.8794823885 0.8870269060 0.8579865098 - 0.8798913956 0.8874142170 0.8584337831 - 0.8803004026 0.8878015280 0.8588811159 - 0.8807088137 0.8881875873 0.8593277931 - 0.8811159730 0.8885713816 0.8597732186 - 0.8815233111 0.8889551759 0.8602187037 - 0.8819304705 0.8893390298 0.8606641293 - 0.8823376894 0.8897228837 0.8611094952 - 0.8827450275 0.8901066780 0.8615549803 - 0.8831487894 0.8904874921 0.8620001078 - 0.8835523725 0.8908680081 0.8624451160 - 0.8839560151 0.8912485242 0.8628901243 - 0.8843594790 0.8916289806 0.8633351922 - 0.8847631216 0.8920094967 0.8637802005 - 0.8851652741 0.8923885822 0.8642246723 - 0.8855656981 0.8927659988 0.8646684289 - 0.8859661818 0.8931434155 0.8651121855 - 0.8863667250 0.8935207725 0.8655558825 - 0.8867670894 0.8938981891 0.8659995794 - 0.8871675730 0.8942756057 0.8664432764 - 0.8875674009 0.8946498036 0.8668851256 - 0.8879672289 0.8950238228 0.8673267961 - 0.8883669972 0.8953977823 0.8677684069 - 0.8887668848 0.8957718015 0.8682100177 - 0.8891667128 0.8961458206 0.8686516881 - 0.8895655274 0.8965191841 0.8690931201 - 0.8899627924 0.8968914151 0.8695340753 - 0.8903599977 0.8972637057 0.8699750900 - 0.8907573223 0.8976359963 0.8704161048 - 0.8911545873 0.8980082870 0.8708571196 - 0.8915517926 0.8983805776 0.8712980747 - 0.8919457793 0.8987510800 0.8717380166 - 0.8923385739 0.8991209269 0.8721774817 - 0.8927314281 0.8994907141 0.8726168871 - 0.8931242824 0.8998606205 0.8730564117 - 0.8935170770 0.9002304077 0.8734958768 - 0.8939098120 0.9006000161 0.8739352226 - 0.8943005800 0.9009667039 0.8743724227 - 0.8946915269 0.9013332725 0.8748096228 - 0.8950822949 0.9017000198 0.8752468228 - 0.8954731822 0.9020665884 0.8756840229 - 0.8958641291 0.9024332762 0.8761211038 - 0.8962544203 0.9027994275 0.8765580058 - 0.8966438174 0.9031645060 0.8769943714 - 0.8970330954 0.9035295844 0.8774306774 - 0.8974224925 0.9038947225 0.8778669834 - 0.8978118896 0.9042598009 0.8783034086 - 0.8982012868 0.9046249986 0.8787397146 - 0.8985881209 0.9049890041 0.8791750073 - 0.8989732862 0.9053522944 0.8796095848 - 0.8993585110 0.9057155848 0.8800442219 - 0.8997436762 0.9060788751 0.8804787993 - 0.9001290202 0.9064422250 0.8809133768 - 0.9005141854 0.9068055153 0.8813480139 - 0.9008963704 0.9071661234 0.8817806840 - 0.9012778997 0.9075260162 0.8822128177 - 0.9016594291 0.9078859091 0.8826448917 - 0.9020408988 0.9082458019 0.8830770850 - 0.9024224281 0.9086055756 0.8835092187 - 0.9028040171 0.9089655280 0.8839412928 - 0.9031841755 0.9093214869 0.8843724132 - 0.9035643935 0.9096773267 0.8848034739 - 0.9039446115 0.9100331068 0.8852344751 - 0.9043248892 0.9103888273 0.8856655955 - 0.9047051072 0.9107446074 0.8860967159 - 0.9050850868 0.9111003280 0.8865275979 - 0.9054622054 0.9114549756 0.8869568110 - 0.9058393240 0.9118096828 0.8873859048 - 0.9062163830 0.9121643901 0.8878151178 - 0.9065935016 0.9125190973 0.8882442117 - 0.9069706202 0.9128738046 0.8886734247 - 0.9073473811 0.9132283926 0.8891022801 - 0.9077209830 0.9135813117 0.8895288110 - 0.9080945849 0.9139341712 0.8899552226 - 0.9084683061 0.9142870903 0.8903816938 - 0.9088419080 0.9146400094 0.8908082247 - 0.9092155099 0.9149929285 0.8912346959 - 0.9095889926 0.9153456092 0.8916611075 - 0.9099609852 0.9156950712 0.8920863271 - 0.9103329778 0.9160447121 0.8925114870 - 0.9107050896 0.9163941741 0.8929367065 - 0.9110770822 0.9167438149 0.8933618069 - 0.9114490747 0.9170932770 0.8937870264 - 0.9118210077 0.9174427986 0.8942121863 - 0.9121910930 0.9177901149 0.8946360946 - 0.9125611782 0.9181374907 0.8950601220 - 0.9129313231 0.9184848070 0.8954840899 - 0.9133014083 0.9188321233 0.8959079981 - 0.9136714935 0.9191794991 0.8963320255 - 0.9140415788 0.9195268154 0.8967558742 - 0.9144073725 0.9198731780 0.8971791863 - 0.9147729278 0.9202194810 0.8976023793 - 0.9151384234 0.9205657840 0.8980255723 - 0.9155039787 0.9209120274 0.8984488845 - 0.9158694744 0.9212582707 0.8988720775 - 0.9162350893 0.9216045737 0.8992952704 - 0.9165984988 0.9219489098 0.8997182250 - 0.9169613123 0.9222924709 0.9001411200 - 0.9173241854 0.9226362109 0.9005638957 - 0.9176871181 0.9229798913 0.9009867907 - 0.9180498719 0.9233235121 0.9014096856 - 0.9184128046 0.9236671925 0.9018325210 - 0.9187752008 0.9240090251 0.9022544026 - 0.9191372991 0.9243493080 0.9026756287 - 0.9194992781 0.9246897101 0.9030967951 - 0.9198613763 0.9250301123 0.9035180211 - 0.9202234149 0.9253705144 0.9039391875 - 0.9205855131 0.9257107973 0.9043604136 - 0.9209464192 0.9260510802 0.9047812223 - 0.9213050008 0.9263911247 0.9052013159 - 0.9216637015 0.9267311096 0.9056212902 - 0.9220224023 0.9270710945 0.9060413241 - 0.9223809838 0.9274110794 0.9064614177 - 0.9227396846 0.9277511239 0.9068813920 - 0.9230982065 0.9280911088 0.9073014259 - 0.9234530926 0.9284300804 0.9077206850 - 0.9238079786 0.9287689924 0.9081398845 - 0.9241629243 0.9291079044 0.9085592031 - 0.9245178103 0.9294468760 0.9089784026 - 0.9248725772 0.9297857881 0.9093977213 - 0.9252275229 0.9301248193 0.9098169804 - 0.9255810976 0.9304615259 0.9102349281 - 0.9259340167 0.9307969809 0.9106521010 - 0.9262868762 0.9311324954 0.9110692739 - 0.9266399145 0.9314680099 0.9114865065 - 0.9269927740 0.9318035841 0.9119036794 - 0.9273456931 0.9321390986 0.9123209119 - 0.9276980758 0.9324735999 0.9127377272 - 0.9280486703 0.9328051209 0.9131532907 - 0.9283993840 0.9331365824 0.9135689139 - 0.9287499785 0.9334679842 0.9139844775 - 0.9291006923 0.9337995052 0.9144001007 - 0.9294514060 0.9341309071 0.9148157835 - 0.9298020005 0.9344624281 0.9152314067 - 0.9301499724 0.9347928762 0.9156460762 - 0.9304972291 0.9351232052 0.9160605073 - 0.9308444262 0.9354534745 0.9164749980 - 0.9311916232 0.9357836843 0.9168894291 - 0.9315388203 0.9361140132 0.9173039198 - 0.9318860173 0.9364442825 0.9177182913 - 0.9322326779 0.9367740154 0.9181321263 - 0.9325779080 0.9371023774 0.9185441136 - 0.9329230189 0.9374307990 0.9189561009 - 0.9332681894 0.9377592206 0.9193680882 - 0.9336133003 0.9380875826 0.9197800756 - 0.9339584708 0.9384160042 0.9201921821 - 0.9343037009 0.9387444258 0.9206042290 - 0.9346480966 0.9390699267 0.9210147858 - 0.9349920750 0.9393941760 0.9214248061 - 0.9353361726 0.9397184253 0.9218348265 - 0.9356803298 0.9400426745 0.9222449064 - 0.9360243082 0.9403669238 0.9226549268 - 0.9363684058 0.9406911135 0.9230648875 - 0.9367122054 0.9410151839 0.9234749079 - 0.9370527267 0.9413359165 0.9238840938 - 0.9373931885 0.9416565895 0.9242933989 - 0.9377337098 0.9419773817 0.9247027040 - 0.9380741715 0.9422981143 0.9251120090 - 0.9384146929 0.9426189065 0.9255211949 - 0.9387552142 0.9429395795 0.9259305000 - 0.9390947223 0.9432600141 0.9263395071 - 0.9394326210 0.9435796142 0.9267479181 - 0.9397705197 0.9438992739 0.9271563888 - 0.9401084185 0.9442188740 0.9275647998 - 0.9404463172 0.9445385933 0.9279732704 - 0.9407842755 0.9448581934 0.9283816814 - 0.9411221743 0.9451779127 0.9287902117 - 0.9414592981 0.9454959035 0.9291977882 - 0.9417958856 0.9458130002 0.9296048880 - 0.9421325922 0.9461300969 0.9300119877 - 0.9424692988 0.9464471936 0.9304192066 - 0.9428058863 0.9467642903 0.9308263063 - 0.9431425929 0.9470815063 0.9312335253 - 0.9434792995 0.9473986030 0.9316406250 - 0.9438139200 0.9477123022 0.9320465922 - 0.9441481829 0.9480254054 0.9324523807 - 0.9444825053 0.9483386278 0.9328582287 - 0.9448167086 0.9486516714 0.9332640171 - 0.9451509714 0.9489647746 0.9336698055 - 0.9454852939 0.9492779970 0.9340755939 - 0.9458196163 0.9495911002 0.9344813228 - 0.9461503029 0.9499025941 0.9348856807 - 0.9464811087 0.9502142072 0.9352900982 - 0.9468119144 0.9505257010 0.9356945157 - 0.9471427202 0.9508371949 0.9360988140 - 0.9474734068 0.9511486888 0.9365031719 - 0.9478042126 0.9514601827 0.9369075894 - 0.9481347203 0.9517716765 0.9373118281 - 0.9484633207 0.9520822167 0.9377142191 - 0.9487919211 0.9523928165 0.9381167293 - 0.9491205215 0.9527034163 0.9385191798 - 0.9494491220 0.9530138969 0.9389216900 - 0.9497776031 0.9533244967 0.9393242002 - 0.9501062036 0.9536349773 0.9397267103 - 0.9504346251 0.9539452195 0.9401286840 - 0.9507619739 0.9542526007 0.9405273795 - 0.9510893226 0.9545599818 0.9409261942 - 0.9514166713 0.9548674226 0.9413250089 - 0.9517440200 0.9551748037 0.9417237043 - 0.9520713091 0.9554821253 0.9421225190 - 0.9523987174 0.9557895064 0.9425212145 - 0.9527255893 0.9560965896 0.9429191947 - 0.9530493021 0.9564006925 0.9433112144 - 0.9533730745 0.9567047954 0.9437032938 - 0.9536967874 0.9570088983 0.9440953135 - 0.9540205002 0.9573130012 0.9444872737 - 0.9543442726 0.9576171041 0.9448792934 - 0.9546679854 0.9579212070 0.9452713132 - 0.9549915195 0.9582251906 0.9456630945 - 0.9553105235 0.9585260153 0.9460486770 - 0.9556294084 0.9588267803 0.9464343190 - 0.9559482932 0.9591276050 0.9468199015 - 0.9562671781 0.9594284296 0.9472054839 - 0.9565861225 0.9597290754 0.9475911260 - 0.9569051266 0.9600299001 0.9479767084 - 0.9572240114 0.9603307247 0.9483622909 - 0.9575372934 0.9606264234 0.9487441778 - 0.9578500986 0.9609215260 0.9491255879 - 0.9581627846 0.9612166286 0.9495071173 - 0.9584754705 0.9615116715 0.9498885870 - 0.9587882161 0.9618067741 0.9502701163 - 0.9591010213 0.9621018767 0.9506515265 - 0.9594137073 0.9623970985 0.9510329962 - 0.9597231150 0.9626886845 0.9514123797 - 0.9600310922 0.9629788995 0.9517909288 - 0.9603391290 0.9632689953 0.9521692991 - 0.9606471062 0.9635592103 0.9525477886 - 0.9609550834 0.9638493061 0.9529262781 - 0.9612631202 0.9641395211 0.9533047080 - 0.9615712166 0.9644296765 0.9536831975 - 0.9618782997 0.9647189975 0.9540600777 - 0.9621844292 0.9650071859 0.9544352293 - 0.9624904990 0.9652954936 0.9548102021 - 0.9627966285 0.9655836821 0.9551852942 - 0.9631026983 0.9658719897 0.9555603266 - 0.9634088278 0.9661601782 0.9559352994 - 0.9637148976 0.9664484859 0.9563103914 - 0.9640207887 0.9667364955 0.9566850066 - 0.9643250704 0.9670230150 0.9570569992 - 0.9646294117 0.9673094153 0.9574289918 - 0.9649336934 0.9675958753 0.9578009844 - 0.9652379751 0.9678822756 0.9581729770 - 0.9655423164 0.9681687951 0.9585450292 - 0.9658465981 0.9684551954 0.9589170218 - 0.9661508799 0.9687417150 0.9592890143 - 0.9664528966 0.9690259099 0.9596589804 - 0.9667541981 0.9693092108 0.9600282907 - 0.9670553803 0.9695925713 0.9603976011 - 0.9673566818 0.9698758721 0.9607669115 - 0.9676579237 0.9701592922 0.9611362219 - 0.9679592252 0.9704425931 0.9615054727 - 0.9682604074 0.9707260132 0.9618747830 - 0.9685606956 0.9710081220 0.9622433186 - 0.9688587785 0.9712873101 0.9626101255 - 0.9691568017 0.9715664983 0.9629768133 - 0.9694548845 0.9718456864 0.9633435011 - 0.9697530270 0.9721248746 0.9637103081 - 0.9700509906 0.9724041224 0.9640769958 - 0.9703490734 0.9726833105 0.9644438028 - 0.9706472158 0.9729624987 0.9648104906 - 0.9709429741 0.9732391238 0.9651746154 - 0.9712383747 0.9735150933 0.9655379057 - 0.9715337157 0.9737911224 0.9659013152 - 0.9718289971 0.9740669727 0.9662647247 - 0.9721243978 0.9743430018 0.9666280746 - 0.9724196792 0.9746189713 0.9669914842 - 0.9727150202 0.9748950005 0.9673548937 - 0.9730095863 0.9751701951 0.9677174091 - 0.9733015895 0.9754428864 0.9680770040 - 0.9735935926 0.9757155776 0.9684365988 - 0.9738855958 0.9759883285 0.9687961936 - 0.9741775990 0.9762610197 0.9691557884 - 0.9744696021 0.9765337110 0.9695153832 - 0.9747616053 0.9768065214 0.9698749185 - 0.9750537276 0.9770792127 0.9702345133 - 0.9753432274 0.9773492813 0.9705916047 - 0.9756311178 0.9776179194 0.9709470868 - 0.9759190083 0.9778863788 0.9713026285 - 0.9762068987 0.9781550169 0.9716581106 - 0.9764947891 0.9784234762 0.9720135927 - 0.9767827988 0.9786919951 0.9723690748 - 0.9770706892 0.9789605737 0.9727246165 - 0.9773585796 0.9792290926 0.9730800986 - 0.9776431918 0.9794937968 0.9734309912 - 0.9779276252 0.9797580838 0.9737815261 - 0.9782118797 0.9800224900 0.9741320014 - 0.9784963131 0.9802867770 0.9744824767 - 0.9787806273 0.9805511236 0.9748330116 - 0.9790650010 0.9808154106 0.9751834869 - 0.9793493152 0.9810798168 0.9755340219 - 0.9796329737 0.9813433886 0.9758836031 - 0.9799132943 0.9816035032 0.9762284756 - 0.9801936150 0.9818636179 0.9765734076 - 0.9804738760 0.9821236730 0.9769182801 - 0.9807541966 0.9823837876 0.9772632122 - 0.9810345769 0.9826439023 0.9776080251 - 0.9813148975 0.9829040170 0.9779528975 - 0.9815952182 0.9831640720 0.9782978296 - 0.9818738103 0.9834225178 0.9786403775 - 0.9821494222 0.9836776853 0.9789785743 - 0.9824249148 0.9839329123 0.9793167710 - 0.9827005267 0.9841880798 0.9796550274 - 0.9829760790 0.9844433069 0.9799932837 - 0.9832515717 0.9846984744 0.9803314805 - 0.9835271835 0.9849537015 0.9806696773 - 0.9838027954 0.9852089286 0.9810078740 - 0.9840760231 0.9854617715 0.9813424945 - 0.9843466878 0.9857121110 0.9816730022 - 0.9846174121 0.9859622717 0.9820033908 - 0.9848880768 0.9862126112 0.9823338985 - 0.9851588011 0.9864627719 0.9826642871 - 0.9854295254 0.9867131114 0.9829946756 - 0.9857001901 0.9869632721 0.9833251834 - 0.9859709144 0.9872136116 0.9836555719 - 0.9862385988 0.9874609709 0.9839816093 - 0.9865036011 0.9877058268 0.9843034744 - 0.9867686033 0.9879505038 0.9846252799 - 0.9870337248 0.9881951809 0.9849470854 - 0.9872987270 0.9884399772 0.9852690101 - 0.9875637293 0.9886847138 0.9855908155 - 0.9878286719 0.9889293909 0.9859126210 - 0.9880936742 0.9891741872 0.9862344265 - 0.9883552790 0.9894152284 0.9865508080 - 0.9886137247 0.9896525741 0.9868618250 - 0.9888721108 0.9898899198 0.9871727824 - 0.9891304970 0.9901272058 0.9874837995 - 0.9893888831 0.9903646111 0.9877948165 - 0.9896472096 0.9906018972 0.9881057143 - 0.9899055958 0.9908393025 0.9884166718 - 0.9901639819 0.9910765886 0.9887276888 - 0.9904199243 0.9913105965 0.9890335202 - 0.9906725883 0.9915400147 0.9893323183 - 0.9909251928 0.9917694926 0.9896311164 - 0.9911777973 0.9919989109 0.9899299145 - 0.9914304018 0.9922283292 0.9902285933 - 0.9916830063 0.9924576879 0.9905273914 - 0.9919356108 0.9926871061 0.9908261895 - 0.9921882153 0.9929165244 0.9911249876 - 0.9924386740 0.9931442142 0.9914202094 - 0.9926838279 0.9933673143 0.9917060137 - 0.9929288030 0.9935904145 0.9919918180 - 0.9931738973 0.9938135147 0.9922776222 - 0.9934189916 0.9940366149 0.9925634265 - 0.9936640263 0.9942597151 0.9928491712 - 0.9939091206 0.9944828153 0.9931349754 - 0.9941540956 0.9947059751 0.9934207201 - 0.9943984747 0.9949285984 0.9937055707 - 0.9946327806 0.9951431751 0.9939746857 - 0.9948670864 0.9953578115 0.9942438006 - 0.9951013923 0.9955723882 0.9945129156 - 0.9953356981 0.9957870841 0.9947819710 - 0.9955700040 0.9960017204 0.9950512052 - 0.9958043098 0.9962162971 0.9953203201 - 0.9960386157 0.9964308739 0.9955893755 - 0.9962729216 0.9966455102 0.9958584905 - 0.9964994192 0.9968507290 0.9961140156 - 0.9967237115 0.9970530868 0.9963654280 - 0.9969478846 0.9972555041 0.9966169000 - 0.9971721768 0.9974579215 0.9968683720 - 0.9973964095 0.9976602793 0.9971199036 - 0.9976207018 0.9978626966 0.9973713756 - 0.9978448749 0.9980651140 0.9976227880 - 0.9980692267 0.9982674718 0.9978743196 - 0.9982891083 0.9984654784 0.9981188774 - 0.9985029101 0.9986572862 0.9983540177 - 0.9987167716 0.9988490939 0.9985892177 - 0.9989306927 0.9990409017 0.9988242984 - 0.9991444945 0.9992327094 0.9990593791 - 0.9993584156 0.9994245768 0.9992945790 - 0.9995722771 0.9996163845 0.9995297194 - 0.9997860789 0.9998081923 0.9997649193 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0189804304 0.0202203300 0.0141742602 + 0.0334178284 0.0351021588 0.0260934699 + 0.0454768315 0.0478549302 0.0364666395 + 0.0562601797 0.0592830218 0.0458958298 + 0.0662030578 0.0697968081 0.0546298996 + 0.0755658001 0.0796677694 0.0628222525 + 0.0844966397 0.0889748707 0.0705531687 + 0.0930858031 0.0978165492 0.0779282674 + 0.1014240980 0.1062681973 0.0849699080 + 0.1095660999 0.1143971011 0.0916869491 + 0.1175450981 0.1221864969 0.0981442481 + 0.1253249943 0.1296440959 0.1043419018 + 0.1328340024 0.1367834061 0.1102854982 + 0.1399984062 0.1436239928 0.1160039008 + 0.1468109936 0.1502085030 0.1215291023 + 0.1533181965 0.1565480977 0.1268731952 + 0.1595371962 0.1626639962 0.1320544034 + 0.1655108929 0.1685764045 0.1370812953 + 0.1712646037 0.1742939055 0.1419793069 + 0.1768248975 0.1798332930 0.1467563957 + 0.1822118014 0.1852239966 0.1514313072 + 0.1874231994 0.1904760003 0.1559925973 + 0.1924864948 0.1955907047 0.1604602933 + 0.1974117011 0.2005726993 0.1648356020 + 0.2021995932 0.2054260969 0.1691132933 + 0.2068601996 0.2101702988 0.1733085066 + 0.2114142030 0.2148002982 0.1774341017 + 0.2158668935 0.2193187028 0.1814931035 + 0.2202159017 0.2237484008 0.1854856014 + 0.2244572043 0.2280993015 0.1894098967 + 0.2285912931 0.2323631048 0.1932736933 + 0.2326336950 0.2365342975 0.1970842034 + 0.2366016954 0.2406188995 0.2008334994 + 0.2405177951 0.2446300983 0.2045322061 + 0.2443760037 0.2485861927 0.2081629038 + 0.2481773049 0.2524932921 0.2117401063 + 0.2519231141 0.2563455105 0.2152681053 + 0.2556205094 0.2601292133 0.2187464982 + 0.2592709064 0.2638446987 0.2221813947 + 0.2628698945 0.2674998045 0.2255706042 + 0.2664135993 0.2711037993 0.2289170027 + 0.2698990107 0.2746542096 0.2322203964 + 0.2733238041 0.2781482935 0.2354855984 + 0.2766858041 0.2815822065 0.2387184948 + 0.2799929976 0.2849547863 0.2419216931 + 0.2832500935 0.2882680893 0.2450876981 + 0.2864542007 0.2915301919 0.2482271940 + 0.2896077931 0.2947526872 0.2513332069 + 0.2927064896 0.2979423106 0.2543987930 + 0.2957524955 0.3010992110 0.2574329972 + 0.2987526953 0.3042244911 0.2604371011 + 0.3017115891 0.3073199987 0.2634117007 + 0.3046318889 0.3103852868 0.2663584054 + 0.3075189888 0.3134222031 0.2692733109 + 0.3103761971 0.3164314032 0.2721604109 + 0.3132067025 0.3194135129 0.2750195861 + 0.3160125911 0.3223691881 0.2778514028 + 0.3187975883 0.3252983987 0.2806541920 + 0.3215644956 0.3282026947 0.2834334970 + 0.3243156970 0.3310827017 0.2861855924 + 0.3270539045 0.3339388072 0.2889156938 + 0.3297814131 0.3367714882 0.2916230857 + 0.3325003982 0.3395748138 0.2943058908 + 0.3352132142 0.3423477113 0.2969624102 + 0.3379161954 0.3450841904 0.2995971143 + 0.3405976892 0.3477846980 0.3022120893 + 0.3432567120 0.3504517972 0.3048096895 + 0.3458822072 0.3530882001 0.3073847890 + 0.3484756052 0.3556962013 0.3099358082 + 0.3510380089 0.3582764864 0.3124623895 + 0.3535713851 0.3608322144 0.3149712980 + 0.3560791910 0.3633658886 0.3174606860 + 0.3585632145 0.3658784032 0.3199287057 + 0.3610230982 0.3683710098 0.3223791122 + 0.3634631932 0.3708465993 0.3248122036 + 0.3658835888 0.3732995093 0.3272255063 + 0.3682858050 0.3757315874 0.3296200931 + 0.3706724048 0.3781447113 0.3319934905 + 0.3730427027 0.3805387020 0.3343470097 + 0.3754003942 0.3829126954 0.3366861939 + 0.3777439892 0.3852662146 0.3390057981 + 0.3800773025 0.3876036108 0.3413096070 + 0.3823992014 0.3899244070 0.3435958922 + 0.3847126067 0.3922333121 0.3458688855 + 0.3870173991 0.3945314884 0.3481296897 + 0.3893153071 0.3968203068 0.3503778875 + 0.3916071951 0.3991019130 0.3526121080 + 0.3938935995 0.4013710022 0.3548285067 + 0.3961760998 0.4036257863 0.3570280075 + 0.3984551132 0.4058659971 0.3592115045 + 0.4007292092 0.4080862999 0.3613776863 + 0.4029959142 0.4102860093 0.3635272980 + 0.4052543938 0.4124661088 0.3656620979 + 0.4075042903 0.4146254957 0.3677802980 + 0.4097425938 0.4167692065 0.3698835075 + 0.4119692147 0.4188987911 0.3719711006 + 0.4141839147 0.4210126996 0.3740384877 + 0.4163868129 0.4231149852 0.3760899901 + 0.4185790122 0.4252065122 0.3781270981 + 0.4207611978 0.4272862077 0.3801490963 + 0.4229308069 0.4293535054 0.3821552098 + 0.4250904024 0.4314081967 0.3841435015 + 0.4272392094 0.4334529042 0.3861171901 + 0.4293777943 0.4354852140 0.3880780041 + 0.4315068126 0.4375067949 0.3900259137 + 0.4336226881 0.4395155013 0.3919605017 + 0.4357245862 0.4415121973 0.3938809931 + 0.4378120899 0.4434967041 0.3957869112 + 0.4398863018 0.4454723895 0.3976789117 + 0.4419437051 0.4474394917 0.3995566964 + 0.4439849854 0.4493967891 0.4014219940 + 0.4460108876 0.4513399899 0.4032762945 + 0.4480217993 0.4532704055 0.4051195979 + 0.4500184953 0.4551894069 0.4069514871 + 0.4520016015 0.4570955932 0.4087724090 + 0.4539715946 0.4589887857 0.4105815887 + 0.4559291005 0.4608702958 0.4123832881 + 0.4578745961 0.4627414048 0.4141762853 + 0.4598087966 0.4646035135 0.4159627855 + 0.4617320895 0.4664578140 0.4177407026 + 0.4636450112 0.4683054090 0.4195122123 + 0.4655455053 0.4701451063 0.4212754071 + 0.4674327075 0.4719766080 0.4230324030 + 0.4693084955 0.4737985134 0.4247812033 + 0.4711722136 0.4756115079 0.4265238941 + 0.4730255902 0.4774144888 0.4282588065 + 0.4748682976 0.4792071879 0.4299876094 + 0.4766997099 0.4809884131 0.4317089021 + 0.4785183072 0.4827584922 0.4334239960 + 0.4803259969 0.4845187962 0.4351322055 + 0.4821228087 0.4862686992 0.4368340969 + 0.4839096963 0.4880078137 0.4385294914 + 0.4856866896 0.4897367954 0.4402182996 + 0.4874519110 0.4914573133 0.4419007897 + 0.4892069101 0.4931693077 0.4435743093 + 0.4909515083 0.4948728085 0.4452398121 + 0.4926868081 0.4965651035 0.4468953907 + 0.4944101870 0.4982514977 0.4485433102 + 0.4961231947 0.4999290109 0.4501810968 + 0.4978268147 0.5015965104 0.4518086910 + 0.4995205998 0.5032545924 0.4534263909 + 0.5012040734 0.5049039721 0.4550347030 + 0.5028777719 0.5065457225 0.4566338062 + 0.5045427084 0.5081775188 0.4582231045 + 0.5061982274 0.5097963214 0.4598028064 + 0.5078454018 0.5114061832 0.4613744020 + 0.5094835758 0.5130059123 0.4629381895 + 0.5111126900 0.5145952106 0.4644944072 + 0.5127326846 0.5161756873 0.4660434127 + 0.5143399239 0.5177451968 0.4675836861 + 0.5159363747 0.5193077922 0.4691169858 + 0.5175241828 0.5208638906 0.4706437886 + 0.5191026926 0.5224146247 0.4721643925 + 0.5206707716 0.5239610076 0.4736790955 + 0.5222259164 0.5254994035 0.4751864076 + 0.5237668157 0.5270283818 0.4766879082 + 0.5252965093 0.5285490751 0.4781832099 + 0.5268157721 0.5300623775 0.4796718955 + 0.5283229947 0.5315675139 0.4811522961 + 0.5298194885 0.5330659151 0.4826253057 + 0.5313063860 0.5345565081 0.4840922058 + 0.5327833891 0.5360388160 0.4855529070 + 0.5342487097 0.5375118852 0.4870058000 + 0.5357007980 0.5389770865 0.4884487092 + 0.5371450782 0.5404356718 0.4898858070 + 0.5385817885 0.5418878198 0.4913172126 + 0.5400090218 0.5433310866 0.4927395880 + 0.5414276123 0.5447692871 0.4941565096 + 0.5428383946 0.5462031960 0.4955680966 + 0.5442407131 0.5476328731 0.4969739914 + 0.5456290841 0.5490533710 0.4983738065 + 0.5470101833 0.5504671931 0.4997689128 + 0.5483844876 0.5518745184 0.5011597872 + 0.5497499108 0.5532733202 0.5025461912 + 0.5511074066 0.5546665788 0.5039262772 + 0.5524582863 0.5560551286 0.5053020120 + 0.5538002253 0.5574359298 0.5066723824 + 0.5551335216 0.5588095188 0.5080367923 + 0.5564607978 0.5601773858 0.5093978047 + 0.5577800274 0.5615364909 0.5107548833 + 0.5590916276 0.5628885031 0.5121051073 + 0.5603979826 0.5642359853 0.5134513974 + 0.5616976023 0.5655778050 0.5147926211 + 0.5629879832 0.5669124126 0.5161284208 + 0.5642735958 0.5682426095 0.5174611807 + 0.5655525923 0.5695660710 0.5187898874 + 0.5668237805 0.5708814859 0.5201147199 + 0.5680909753 0.5721923113 0.5214365721 + 0.5693522096 0.5734962225 0.5227527022 + 0.5706079006 0.5747932792 0.5240650773 + 0.5718598962 0.5760861039 0.5253748298 + 0.5731037259 0.5773720741 0.5266771913 + 0.5743418932 0.5786536932 0.5279759169 + 0.5755749941 0.5799306035 0.5292711258 + 0.5768011808 0.5811964869 0.5305575132 + 0.5780242085 0.5824581981 0.5318406820 + 0.5792413950 0.5837122202 0.5331183076 + 0.5804504156 0.5849606991 0.5343890190 + 0.5816553235 0.5862060785 0.5356559753 + 0.5828509927 0.5874449015 0.5369136930 + 0.5840438008 0.5886800289 0.5381681919 + 0.5852320194 0.5899084210 0.5394172072 + 0.5864148736 0.5911297202 0.5406591892 + 0.5875951052 0.5923475027 0.5418981910 + 0.5887641907 0.5935558081 0.5431274176 + 0.5899302959 0.5947611928 0.5443540215 + 0.5910912156 0.5959597826 0.5455750823 + 0.5922474265 0.5971537828 0.5467911959 + 0.5934010148 0.5983446836 0.5480040908 + 0.5945463777 0.5995286107 0.5492113233 + 0.5956892967 0.6007102132 0.5504168272 + 0.5968241096 0.6018840075 0.5516167283 + 0.5979555845 0.6030550003 0.5528140068 + 0.5990828276 0.6042217016 0.5540074110 + 0.6002063751 0.6053854227 0.5551959872 + 0.6013283134 0.6065471768 0.5563817024 + 0.6024429202 0.6077020764 0.5575634837 + 0.6035555005 0.6088551879 0.5587441921 + 0.6046581268 0.6099976897 0.5599179268 + 0.6057583094 0.6111379862 0.5610895753 + 0.6068522930 0.6122714281 0.5622550845 + 0.6079434156 0.6134017706 0.5634173751 + 0.6090310216 0.6145281196 0.5645756125 + 0.6101149917 0.6156514287 0.5657300949 + 0.6111965775 0.6167725921 0.5668820143 + 0.6122707725 0.6178879738 0.5680285096 + 0.6133425236 0.6190009117 0.5691730976 + 0.6144058704 0.6201071143 0.5703091025 + 0.6154682040 0.6212120056 0.5714436173 + 0.6165214777 0.6223102212 0.5725712180 + 0.6175739169 0.6234076023 0.5736979842 + 0.6186174750 0.6244966984 0.5748155117 + 0.6196597815 0.6255844831 0.5759319067 + 0.6206961274 0.6266639233 0.5770416856 + 0.6217305064 0.6277418733 0.5781496763 + 0.6227589250 0.6288138032 0.5792517066 + 0.6237844825 0.6298843026 0.5803521276 + 0.6248034835 0.6309509873 0.5814481974 + 0.6258192062 0.6320158839 0.5825421214 + 0.6268281937 0.6330764294 0.5836308002 + 0.6278346777 0.6341344714 0.5847169161 + 0.6288362145 0.6351872087 0.5857974887 + 0.6298360229 0.6362372041 0.5868756175 + 0.6308323741 0.6372811794 0.5879486203 + 0.6318262815 0.6383233070 0.5890188813 + 0.6328150034 0.6393606067 0.5900835991 + 0.6338009238 0.6403964162 0.5911462903 + 0.6347796917 0.6414275765 0.5922037959 + 0.6357566714 0.6424568295 0.5932598710 + 0.6367279887 0.6434782147 0.5943114161 + 0.6376978755 0.6444982290 0.5953620076 + 0.6386622190 0.6455100775 0.5964084864 + 0.6396257281 0.6465215087 0.5974544287 + 0.6405826807 0.6475260854 0.5984926224 + 0.6415395737 0.6485307217 0.5995308161 + 0.6424885988 0.6495286226 0.6005603075 + 0.6434376836 0.6505261064 0.6015896797 + 0.6443789005 0.6515172720 0.6026117206 + 0.6453189850 0.6525071859 0.6036325097 + 0.6462522745 0.6534914970 0.6046487093 + 0.6471831799 0.6544733047 0.6056631207 + 0.6481100917 0.6554508209 0.6066738963 + 0.6490340829 0.6564248204 0.6076821089 + 0.6499556899 0.6573960781 0.6086879969 + 0.6508737803 0.6583626866 0.6096904278 + 0.6517899036 0.6593278050 0.6106911898 + 0.6526989937 0.6602864265 0.6116858125 + 0.6536079049 0.6612449884 0.6126803160 + 0.6545088291 0.6621980071 0.6136667728 + 0.6554095745 0.6631507874 0.6146532297 + 0.6563045979 0.6640974879 0.6156343222 + 0.6571980119 0.6650422812 0.6166138053 + 0.6580873132 0.6659818292 0.6175904274 + 0.6589732170 0.6669161916 0.6185643077 + 0.6598567963 0.6678487062 0.6195365787 + 0.6607350707 0.6687750220 0.6205043793 + 0.6616132259 0.6697012782 0.6214722991 + 0.6624845862 0.6706203818 0.6224306226 + 0.6633558869 0.6715393066 0.6233887076 + 0.6642240882 0.6724529266 0.6243417859 + 0.6650909781 0.6733639836 0.6252925992 + 0.6659563780 0.6742721200 0.6262406707 + 0.6668195128 0.6751747727 0.6271843910 + 0.6676822901 0.6760774255 0.6281278729 + 0.6685385704 0.6769707799 0.6290643811 + 0.6693950295 0.6778641939 0.6300008297 + 0.6702473760 0.6787514091 0.6309322119 + 0.6710978150 0.6796351075 0.6318609715 + 0.6719467044 0.6805168986 0.6327874064 + 0.6727918983 0.6813933253 0.6337075233 + 0.6736372113 0.6822696924 0.6346276999 + 0.6744775772 0.6831405163 0.6355414987 + 0.6753175259 0.6840105057 0.6364545226 + 0.6761547923 0.6848769188 0.6373646855 + 0.6769893765 0.6857391000 0.6382718086 + 0.6778236032 0.6866012216 0.6391788125 + 0.6786518097 0.6874572039 0.6400815248 + 0.6794800162 0.6883131862 0.6409841180 + 0.6803044081 0.6891654730 0.6418827176 + 0.6811261177 0.6900150180 0.6427782774 + 0.6819474101 0.6908640862 0.6436731815 + 0.6827651858 0.6917092800 0.6445615292 + 0.6835830212 0.6925545931 0.6454498768 + 0.6843972206 0.6933962703 0.6463342905 + 0.6852092743 0.6942355037 0.6472160816 + 0.6860206127 0.6950742006 0.6480973959 + 0.6868252754 0.6959053874 0.6489725709 + 0.6876299977 0.6967365742 0.6498478055 + 0.6884319782 0.6975647807 0.6507201195 + 0.6892318130 0.6983901858 0.6515899897 + 0.6900315285 0.6992155910 0.6524599791 + 0.6908273101 0.7000362873 0.6533246040 + 0.6916229725 0.7008566856 0.6541891098 + 0.6924160719 0.7016751170 0.6550511122 + 0.6932054162 0.7024903297 0.6559094191 + 0.6939945817 0.7033056021 0.6567676067 + 0.6947793961 0.7041168213 0.6576201916 + 0.6955630183 0.7049267292 0.6584712863 + 0.6963458061 0.7057358027 0.6593216062 + 0.6971247196 0.7065396905 0.6601678133 + 0.6979035139 0.7073435187 0.6610140204 + 0.6986786723 0.7081443071 0.6618571877 + 0.6994504929 0.7089418769 0.6626976728 + 0.7002223134 0.7097393870 0.6635380983 + 0.7009885907 0.7105317712 0.6643719077 + 0.7017540932 0.7113230824 0.6652045250 + 0.7025185823 0.7121133804 0.6660360098 + 0.7032787204 0.7128974795 0.6668620110 + 0.7040386796 0.7136815786 0.6676880717 + 0.7047963738 0.7144626975 0.6685119271 + 0.7055510879 0.7152395248 0.6693329811 + 0.7063059211 0.7160161734 0.6701539755 + 0.7070568204 0.7167897224 0.6709715724 + 0.7078062296 0.7175614834 0.6717876792 + 0.7085556984 0.7183333039 0.6726037860 + 0.7092993855 0.7191014290 0.6734123826 + 0.7100430131 0.7198691964 0.6742205024 + 0.7107855082 0.7206360102 0.6750273705 + 0.7115231752 0.7213973999 0.6758279204 + 0.7122610211 0.7221586704 0.6766282916 + 0.7129974961 0.7229182124 0.6774265170 + 0.7137318254 0.7236738801 0.6782206893 + 0.7144662142 0.7244296074 0.6790148020 + 0.7151979804 0.7251828909 0.6798056960 + 0.7159276009 0.7259337902 0.6805936098 + 0.7166572809 0.7266848087 0.6813815832 + 0.7173827887 0.7274302840 0.6821650267 + 0.7181063890 0.7281727791 0.6829462051 + 0.7188299894 0.7289153934 0.6837273836 + 0.7195504904 0.7296513915 0.6845042706 + 0.7202703953 0.7303856015 0.6852802038 + 0.7209903002 0.7311198115 0.6860560775 + 0.7217056751 0.7318493724 0.6868286729 + 0.7224205732 0.7325783968 0.6876009703 + 0.7231354713 0.7333073020 0.6883733273 + 0.7238441706 0.7340301275 0.6891400218 + 0.7245526910 0.7347525954 0.6899065971 + 0.7252612114 0.7354750037 0.6906729937 + 0.7259656787 0.7361903191 0.6914319992 + 0.7266703248 0.7369055152 0.6921910048 + 0.7273744941 0.7376205921 0.6929497123 + 0.7280738950 0.7383303046 0.6937026978 + 0.7287732959 0.7390400171 0.6944558024 + 0.7294721007 0.7397493720 0.6952087283 + 0.7301635146 0.7404522896 0.6959573030 + 0.7308549285 0.7411552072 0.6967059970 + 0.7315459847 0.7418580055 0.6974545121 + 0.7322310805 0.7425541878 0.6981990933 + 0.7329162955 0.7432504296 0.6989436150 + 0.7336012721 0.7439466119 0.6996881962 + 0.7342811823 0.7446365952 0.7004261017 + 0.7349609733 0.7453263998 0.7011638284 + 0.7356408238 0.7460162044 0.7019016147 + 0.7363131046 0.7467001081 0.7026333809 + 0.7369849086 0.7473831773 0.7033646703 + 0.7376567125 0.7480663061 0.7040959001 + 0.7383230925 0.7487441897 0.7048221231 + 0.7389885187 0.7494208217 0.7055472136 + 0.7396538258 0.7500973940 0.7062721848 + 0.7403153777 0.7507699132 0.7069926858 + 0.7409753799 0.7514402866 0.7077111006 + 0.7416353822 0.7521107197 0.7084295154 + 0.7422909141 0.7527776957 0.7091451287 + 0.7429429293 0.7534415126 0.7098584771 + 0.7435948253 0.7541053891 0.7105718851 + 0.7442438006 0.7547671199 0.7112827897 + 0.7448878288 0.7554246187 0.7119894028 + 0.7455319166 0.7560821176 0.7126960754 + 0.7461751103 0.7567387223 0.7134013772 + 0.7468137741 0.7573903203 0.7140998244 + 0.7474524975 0.7580417991 0.7147980928 + 0.7480912209 0.7586933970 0.7154964209 + 0.7487258911 0.7593389750 0.7161883712 + 0.7493603826 0.7599840760 0.7168800235 + 0.7499948740 0.7606291771 0.7175716162 + 0.7506251931 0.7612702250 0.7182598114 + 0.7512537837 0.7619091272 0.7189463973 + 0.7518823147 0.7625480294 0.7196329236 + 0.7525088191 0.7631847262 0.7203181982 + 0.7531324029 0.7638177872 0.7210013866 + 0.7537561059 0.7644509077 0.7216845751 + 0.7543792725 0.7650834918 0.7223674059 + 0.7549992204 0.7657102942 0.7230455279 + 0.7556189895 0.7663369775 0.7237235904 + 0.7562388778 0.7669637203 0.7244017124 + 0.7568544745 0.7675850987 0.7250748277 + 0.7574688792 0.7682043910 0.7257462144 + 0.7580832839 0.7688236833 0.7264177203 + 0.7586959004 0.7694402933 0.7270871997 + 0.7593057752 0.7700520158 0.7277535200 + 0.7599158287 0.7706636190 0.7284198999 + 0.7605257034 0.7712752819 0.7290862799 + 0.7611322999 0.7718833089 0.7297480106 + 0.7617387772 0.7724912167 0.7304096818 + 0.7623453736 0.7730990052 0.7310714126 + 0.7629489899 0.7737038136 0.7317304015 + 0.7635505795 0.7743061781 0.7323871851 + 0.7641521096 0.7749084830 0.7330440283 + 0.7647531033 0.7755101919 0.7337000966 + 0.7653506994 0.7761059999 0.7343516946 + 0.7659481764 0.7767019272 0.7350031734 + 0.7665457726 0.7772976756 0.7356547117 + 0.7671412826 0.7778910995 0.7363024950 + 0.7677358985 0.7784829140 0.7369480133 + 0.7683303952 0.7790747881 0.7375935912 + 0.7689242959 0.7796661258 0.7382383943 + 0.7695142031 0.7802531719 0.7388777137 + 0.7701042295 0.7808402181 0.7395170927 + 0.7706941962 0.7814273238 0.7401564121 + 0.7712818980 0.7820110917 0.7407931089 + 0.7718682289 0.7825924754 0.7414278984 + 0.7724546194 0.7831739187 0.7420626879 + 0.7730407715 0.7837553024 0.7426972985 + 0.7736244798 0.7843334079 0.7433282137 + 0.7742081881 0.7849115729 0.7439591289 + 0.7747918963 0.7854897976 0.7445899844 + 0.7753731012 0.7860661745 0.7452182174 + 0.7759513855 0.7866402864 0.7458429933 + 0.7765296102 0.7872143984 0.7464678288 + 0.7771078944 0.7877886295 0.7470927238 + 0.7776818871 0.7883577943 0.7477130294 + 0.7782551050 0.7889257073 0.7483323216 + 0.7788282037 0.7894936800 0.7489516139 + 0.7794010043 0.7900611162 0.7495700717 + 0.7799720764 0.7906246781 0.7501844764 + 0.7805430889 0.7911882997 0.7507988214 + 0.7811141014 0.7917519212 0.7514132261 + 0.7816827893 0.7923139930 0.7520251870 + 0.7822489142 0.7928743958 0.7526345253 + 0.7828149796 0.7934347987 0.7532438040 + 0.7833812237 0.7939952016 0.7538530827 + 0.7839428782 0.7945516706 0.7544584870 + 0.7845032215 0.7951068282 0.7550626993 + 0.7850636244 0.7956619859 0.7556669116 + 0.7856239080 0.7962170839 0.7562711239 + 0.7861807942 0.7967674732 0.7568696141 + 0.7867377996 0.7973179221 0.7574681044 + 0.7872946858 0.7978683114 0.7580665946 + 0.7878506184 0.7984176874 0.7586637139 + 0.7884032726 0.7989639044 0.7592564225 + 0.7889561057 0.7995100021 0.7598490119 + 0.7895088196 0.8000562191 0.7604417205 + 0.7900593281 0.8006005883 0.7610327005 + 0.7906069756 0.8011425138 0.7616212964 + 0.7911546826 0.8016844988 0.7622098923 + 0.7917025089 0.8022264242 0.7627984881 + 0.7922484875 0.8027654886 0.7633846998 + 0.7927935123 0.8033025861 0.7639691830 + 0.7933384180 0.8038396239 0.7645537257 + 0.7938833833 0.8043767214 0.7651382089 + 0.7944257855 0.8049103022 0.7657201290 + 0.7949671745 0.8054423928 0.7663009763 + 0.7955086827 0.8059744239 0.7668818831 + 0.7960500717 0.8065065145 0.7674626708 + 0.7965880036 0.8070362210 0.7680410743 + 0.7971251011 0.8075652122 0.7686188221 + 0.7976623178 0.8080940843 0.7691965103 + 0.7981994152 0.8086230755 0.7697743177 + 0.7987347245 0.8091489077 0.7703492045 + 0.7992696762 0.8096740842 0.7709236741 + 0.7998046875 0.8101993203 0.7714982033 + 0.8003398180 0.8107244968 0.7720727921 + 0.8008722067 0.8112462759 0.7726444006 + 0.8014042974 0.8117675781 0.7732155919 + 0.8019365072 0.8122889996 0.7737869024 + 0.8024685979 0.8128103018 0.7743582129 + 0.8029965162 0.8133283257 0.7749258876 + 0.8035238981 0.8138456941 0.7754932046 + 0.8040512204 0.8143631816 0.7760605216 + 0.8045786023 0.8148807287 0.7766278982 + 0.8051033020 0.8153948188 0.7771916986 + 0.8056275249 0.8159083128 0.7777550220 + 0.8061518073 0.8164218068 0.7783182859 + 0.8066759706 0.8169354200 0.7788816094 + 0.8071985841 0.8174456954 0.7794430852 + 0.8077207208 0.8179551959 0.7800040841 + 0.8082427979 0.8184646964 0.7805652022 + 0.8087648749 0.8189740777 0.7811262012 + 0.8092843294 0.8194804788 0.7816848159 + 0.8098024726 0.8199853897 0.7822422981 + 0.8103206158 0.8204901814 0.7827997208 + 0.8108388186 0.8209950924 0.7833572030 + 0.8113549948 0.8214983940 0.7839121819 + 0.8118696213 0.8220003843 0.7844650149 + 0.8123841882 0.8225023746 0.7850177884 + 0.8128988147 0.8230044842 0.7855706811 + 0.8134121895 0.8235049844 0.7861223817 + 0.8139237761 0.8240029812 0.7866721749 + 0.8144353032 0.8245009780 0.7872220874 + 0.8149467707 0.8249989152 0.7877719998 + 0.8154575229 0.8254960775 0.7883214951 + 0.8159641027 0.8259888887 0.7888690233 + 0.8164708018 0.8264816999 0.7894164920 + 0.8169775009 0.8269745111 0.7899640203 + 0.8174842000 0.8274673223 0.7905114889 + 0.8179872036 0.8279569149 0.7910552025 + 0.8184900284 0.8284462094 0.7915986776 + 0.8189927936 0.8289356232 0.7921422124 + 0.8194956779 0.8294249177 0.7926856279 + 0.8199970126 0.8299115896 0.7932267785 + 0.8204975128 0.8303967714 0.7937667966 + 0.8209980130 0.8308820128 0.7943068743 + 0.8214985728 0.8313671947 0.7948468924 + 0.8219977021 0.8318507075 0.7953858972 + 0.8224940896 0.8323307037 0.7959229946 + 0.8229905963 0.8328106999 0.7964602113 + 0.8234871030 0.8332906961 0.7969973087 + 0.8239834905 0.8337706923 0.7975344062 + 0.8244758844 0.8342465758 0.7980670929 + 0.8249682188 0.8347225189 0.7985997200 + 0.8254604936 0.8351984024 0.7991324067 + 0.8259528875 0.8356742859 0.7996649742 + 0.8264433146 0.8361485004 0.8001952767 + 0.8269324899 0.8366215229 0.8007240295 + 0.8274217248 0.8370944858 0.8012526035 + 0.8279109001 0.8375675082 0.8017812967 + 0.8283994198 0.8380396962 0.8023093939 + 0.8288854957 0.8385075927 0.8028346896 + 0.8293715119 0.8389754891 0.8033601046 + 0.8298575878 0.8394433856 0.8038854003 + 0.8303436041 0.8399114013 0.8044108152 + 0.8308259249 0.8403760791 0.8049324751 + 0.8313068748 0.8408395052 0.8054528832 + 0.8317878842 0.8413029909 0.8059732914 + 0.8322690129 0.8417664170 0.8064936996 + 0.8327487707 0.8422294855 0.8070130944 + 0.8332253098 0.8426914215 0.8075290918 + 0.8337017894 0.8431531787 0.8080450892 + 0.8341782093 0.8436151147 0.8085610867 + 0.8346546888 0.8440768719 0.8090770841 + 0.8351281881 0.8445361853 0.8095903993 + 0.8356006742 0.8449943066 0.8101025224 + 0.8360731006 0.8454524279 0.8106147051 + 0.8365455270 0.8459104896 0.8111268878 + 0.8370172977 0.8463678956 0.8116385937 + 0.8374856114 0.8468207121 0.8121470809 + 0.8379539847 0.8472735286 0.8126556277 + 0.8384222984 0.8477262855 0.8131639957 + 0.8388906121 0.8481791019 0.8136724830 + 0.8393570185 0.8486301899 0.8141798973 + 0.8398215771 0.8490794897 0.8146862984 + 0.8402863145 0.8495287895 0.8151926994 + 0.8407509923 0.8499780893 0.8156991005 + 0.8412157297 0.8504275084 0.8162055016 + 0.8416776061 0.8508747816 0.8167092800 + 0.8421391249 0.8513218760 0.8172127008 + 0.8426007032 0.8517689705 0.8177161217 + 0.8430622220 0.8522161245 0.8182194829 + 0.8435230851 0.8526622057 0.8187221289 + 0.8439813852 0.8531038761 0.8192216754 + 0.8444396853 0.8535456061 0.8197212219 + 0.8448979855 0.8539872766 0.8202207088 + 0.8453562856 0.8544290066 0.8207201958 + 0.8458127975 0.8548687100 0.8212186098 + 0.8462672830 0.8553062081 0.8217157722 + 0.8467218876 0.8557437062 0.8222129941 + 0.8471763730 0.8561813235 0.8227102160 + 0.8476309180 0.8566188216 0.8232073784 + 0.8480827212 0.8570545912 0.8237022758 + 0.8485333920 0.8574895859 0.8241963983 + 0.8489841223 0.8579246998 0.8246904016 + 0.8494347930 0.8583598137 0.8251844049 + 0.8498855233 0.8587948084 0.8256785274 + 0.8503341079 0.8592264056 0.8261687160 + 0.8507825136 0.8596577048 0.8266587257 + 0.8512309790 0.8600888848 0.8271486163 + 0.8516793847 0.8605201840 0.8276385069 + 0.8521276116 0.8609511256 0.8281282187 + 0.8525732756 0.8613770008 0.8286157250 + 0.8530188799 0.8618029952 0.8291031122 + 0.8534644842 0.8622288704 0.8295904994 + 0.8539102077 0.8626548052 0.8300778866 + 0.8543549776 0.8630800843 0.8305646181 + 0.8547968268 0.8635025024 0.8310487270 + 0.8552384973 0.8639248013 0.8315327764 + 0.8556802869 0.8643472195 0.8320168257 + 0.8561220169 0.8647695184 0.8325008750 + 0.8565629721 0.8651912212 0.8329836726 + 0.8570019007 0.8656111956 0.8334634900 + 0.8574408293 0.8660312295 0.8339433074 + 0.8578798175 0.8664512038 0.8344230056 + 0.8583186865 0.8668712974 0.8349028230 + 0.8587570786 0.8672900796 0.8353816867 + 0.8591942787 0.8677062988 0.8358588219 + 0.8596314788 0.8681225181 0.8363357782 + 0.8600686789 0.8685386777 0.8368129134 + 0.8605058789 0.8689548969 0.8372899294 + 0.8609418869 0.8693702221 0.8377658725 + 0.8613752723 0.8697832823 0.8382393718 + 0.8618087173 0.8701964021 0.8387128711 + 0.8622421026 0.8706095219 0.8391863704 + 0.8626756072 0.8710225224 0.8396599889 + 0.8631076813 0.8714349866 0.8401322961 + 0.8635362983 0.8718457222 0.8406013846 + 0.8639649153 0.8722563982 0.8410704136 + 0.8643935919 0.8726670146 0.8415395021 + 0.8648222089 0.8730776906 0.8420084715 + 0.8652505279 0.8734877110 0.8424770832 + 0.8656774759 0.8738945723 0.8429431915 + 0.8661044836 0.8743014932 0.8434094191 + 0.8665313721 0.8747084141 0.8438755274 + 0.8669583797 0.8751152754 0.8443416953 + 0.8673852086 0.8755220175 0.8448076844 + 0.8678094745 0.8759253025 0.8452700973 + 0.8682336807 0.8763285875 0.8457326293 + 0.8686580062 0.8767318726 0.8461951017 + 0.8690822124 0.8771352172 0.8466575146 + 0.8695064783 0.8775385022 0.8471199870 + 0.8699274063 0.8779397011 0.8475784063 + 0.8703479767 0.8783406019 0.8480362296 + 0.8707684875 0.8787413836 0.8484939933 + 0.8711889982 0.8791422844 0.8489518762 + 0.8716096282 0.8795431852 0.8494096994 + 0.8720285892 0.8799425960 0.8498654962 + 0.8724468946 0.8803411722 0.8503202796 + 0.8728652000 0.8807398081 0.8507750034 + 0.8732835054 0.8811385036 0.8512297869 + 0.8737016916 0.8815370798 0.8516845107 + 0.8741189241 0.8819342852 0.8521381021 + 0.8745344281 0.8823292851 0.8525899053 + 0.8749498725 0.8827242255 0.8530418277 + 0.8753653765 0.8831192255 0.8534936905 + 0.8757808805 0.8835142255 0.8539454937 + 0.8761960268 0.8839088082 0.8543971181 + 0.8766077161 0.8842998147 0.8548464775 + 0.8770195246 0.8846908212 0.8552958965 + 0.8774312735 0.8850818872 0.8557451963 + 0.8778430223 0.8854728937 0.8561946154 + 0.8782547712 0.8858640194 0.8566439748 + 0.8786643744 0.8862522244 0.8570917845 + 0.8790733814 0.8866394758 0.8575391173 + 0.8794823885 0.8870269060 0.8579865098 + 0.8798913956 0.8874142170 0.8584337831 + 0.8803004026 0.8878015280 0.8588811159 + 0.8807088137 0.8881875873 0.8593277931 + 0.8811159730 0.8885713816 0.8597732186 + 0.8815233111 0.8889551759 0.8602187037 + 0.8819304705 0.8893390298 0.8606641293 + 0.8823376894 0.8897228837 0.8611094952 + 0.8827450275 0.8901066780 0.8615549803 + 0.8831487894 0.8904874921 0.8620001078 + 0.8835523725 0.8908680081 0.8624451160 + 0.8839560151 0.8912485242 0.8628901243 + 0.8843594790 0.8916289806 0.8633351922 + 0.8847631216 0.8920094967 0.8637802005 + 0.8851652741 0.8923885822 0.8642246723 + 0.8855656981 0.8927659988 0.8646684289 + 0.8859661818 0.8931434155 0.8651121855 + 0.8863667250 0.8935207725 0.8655558825 + 0.8867670894 0.8938981891 0.8659995794 + 0.8871675730 0.8942756057 0.8664432764 + 0.8875674009 0.8946498036 0.8668851256 + 0.8879672289 0.8950238228 0.8673267961 + 0.8883669972 0.8953977823 0.8677684069 + 0.8887668848 0.8957718015 0.8682100177 + 0.8891667128 0.8961458206 0.8686516881 + 0.8895655274 0.8965191841 0.8690931201 + 0.8899627924 0.8968914151 0.8695340753 + 0.8903599977 0.8972637057 0.8699750900 + 0.8907573223 0.8976359963 0.8704161048 + 0.8911545873 0.8980082870 0.8708571196 + 0.8915517926 0.8983805776 0.8712980747 + 0.8919457793 0.8987510800 0.8717380166 + 0.8923385739 0.8991209269 0.8721774817 + 0.8927314281 0.8994907141 0.8726168871 + 0.8931242824 0.8998606205 0.8730564117 + 0.8935170770 0.9002304077 0.8734958768 + 0.8939098120 0.9006000161 0.8739352226 + 0.8943005800 0.9009667039 0.8743724227 + 0.8946915269 0.9013332725 0.8748096228 + 0.8950822949 0.9017000198 0.8752468228 + 0.8954731822 0.9020665884 0.8756840229 + 0.8958641291 0.9024332762 0.8761211038 + 0.8962544203 0.9027994275 0.8765580058 + 0.8966438174 0.9031645060 0.8769943714 + 0.8970330954 0.9035295844 0.8774306774 + 0.8974224925 0.9038947225 0.8778669834 + 0.8978118896 0.9042598009 0.8783034086 + 0.8982012868 0.9046249986 0.8787397146 + 0.8985881209 0.9049890041 0.8791750073 + 0.8989732862 0.9053522944 0.8796095848 + 0.8993585110 0.9057155848 0.8800442219 + 0.8997436762 0.9060788751 0.8804787993 + 0.9001290202 0.9064422250 0.8809133768 + 0.9005141854 0.9068055153 0.8813480139 + 0.9008963704 0.9071661234 0.8817806840 + 0.9012778997 0.9075260162 0.8822128177 + 0.9016594291 0.9078859091 0.8826448917 + 0.9020408988 0.9082458019 0.8830770850 + 0.9024224281 0.9086055756 0.8835092187 + 0.9028040171 0.9089655280 0.8839412928 + 0.9031841755 0.9093214869 0.8843724132 + 0.9035643935 0.9096773267 0.8848034739 + 0.9039446115 0.9100331068 0.8852344751 + 0.9043248892 0.9103888273 0.8856655955 + 0.9047051072 0.9107446074 0.8860967159 + 0.9050850868 0.9111003280 0.8865275979 + 0.9054622054 0.9114549756 0.8869568110 + 0.9058393240 0.9118096828 0.8873859048 + 0.9062163830 0.9121643901 0.8878151178 + 0.9065935016 0.9125190973 0.8882442117 + 0.9069706202 0.9128738046 0.8886734247 + 0.9073473811 0.9132283926 0.8891022801 + 0.9077209830 0.9135813117 0.8895288110 + 0.9080945849 0.9139341712 0.8899552226 + 0.9084683061 0.9142870903 0.8903816938 + 0.9088419080 0.9146400094 0.8908082247 + 0.9092155099 0.9149929285 0.8912346959 + 0.9095889926 0.9153456092 0.8916611075 + 0.9099609852 0.9156950712 0.8920863271 + 0.9103329778 0.9160447121 0.8925114870 + 0.9107050896 0.9163941741 0.8929367065 + 0.9110770822 0.9167438149 0.8933618069 + 0.9114490747 0.9170932770 0.8937870264 + 0.9118210077 0.9174427986 0.8942121863 + 0.9121910930 0.9177901149 0.8946360946 + 0.9125611782 0.9181374907 0.8950601220 + 0.9129313231 0.9184848070 0.8954840899 + 0.9133014083 0.9188321233 0.8959079981 + 0.9136714935 0.9191794991 0.8963320255 + 0.9140415788 0.9195268154 0.8967558742 + 0.9144073725 0.9198731780 0.8971791863 + 0.9147729278 0.9202194810 0.8976023793 + 0.9151384234 0.9205657840 0.8980255723 + 0.9155039787 0.9209120274 0.8984488845 + 0.9158694744 0.9212582707 0.8988720775 + 0.9162350893 0.9216045737 0.8992952704 + 0.9165984988 0.9219489098 0.8997182250 + 0.9169613123 0.9222924709 0.9001411200 + 0.9173241854 0.9226362109 0.9005638957 + 0.9176871181 0.9229798913 0.9009867907 + 0.9180498719 0.9233235121 0.9014096856 + 0.9184128046 0.9236671925 0.9018325210 + 0.9187752008 0.9240090251 0.9022544026 + 0.9191372991 0.9243493080 0.9026756287 + 0.9194992781 0.9246897101 0.9030967951 + 0.9198613763 0.9250301123 0.9035180211 + 0.9202234149 0.9253705144 0.9039391875 + 0.9205855131 0.9257107973 0.9043604136 + 0.9209464192 0.9260510802 0.9047812223 + 0.9213050008 0.9263911247 0.9052013159 + 0.9216637015 0.9267311096 0.9056212902 + 0.9220224023 0.9270710945 0.9060413241 + 0.9223809838 0.9274110794 0.9064614177 + 0.9227396846 0.9277511239 0.9068813920 + 0.9230982065 0.9280911088 0.9073014259 + 0.9234530926 0.9284300804 0.9077206850 + 0.9238079786 0.9287689924 0.9081398845 + 0.9241629243 0.9291079044 0.9085592031 + 0.9245178103 0.9294468760 0.9089784026 + 0.9248725772 0.9297857881 0.9093977213 + 0.9252275229 0.9301248193 0.9098169804 + 0.9255810976 0.9304615259 0.9102349281 + 0.9259340167 0.9307969809 0.9106521010 + 0.9262868762 0.9311324954 0.9110692739 + 0.9266399145 0.9314680099 0.9114865065 + 0.9269927740 0.9318035841 0.9119036794 + 0.9273456931 0.9321390986 0.9123209119 + 0.9276980758 0.9324735999 0.9127377272 + 0.9280486703 0.9328051209 0.9131532907 + 0.9283993840 0.9331365824 0.9135689139 + 0.9287499785 0.9334679842 0.9139844775 + 0.9291006923 0.9337995052 0.9144001007 + 0.9294514060 0.9341309071 0.9148157835 + 0.9298020005 0.9344624281 0.9152314067 + 0.9301499724 0.9347928762 0.9156460762 + 0.9304972291 0.9351232052 0.9160605073 + 0.9308444262 0.9354534745 0.9164749980 + 0.9311916232 0.9357836843 0.9168894291 + 0.9315388203 0.9361140132 0.9173039198 + 0.9318860173 0.9364442825 0.9177182913 + 0.9322326779 0.9367740154 0.9181321263 + 0.9325779080 0.9371023774 0.9185441136 + 0.9329230189 0.9374307990 0.9189561009 + 0.9332681894 0.9377592206 0.9193680882 + 0.9336133003 0.9380875826 0.9197800756 + 0.9339584708 0.9384160042 0.9201921821 + 0.9343037009 0.9387444258 0.9206042290 + 0.9346480966 0.9390699267 0.9210147858 + 0.9349920750 0.9393941760 0.9214248061 + 0.9353361726 0.9397184253 0.9218348265 + 0.9356803298 0.9400426745 0.9222449064 + 0.9360243082 0.9403669238 0.9226549268 + 0.9363684058 0.9406911135 0.9230648875 + 0.9367122054 0.9410151839 0.9234749079 + 0.9370527267 0.9413359165 0.9238840938 + 0.9373931885 0.9416565895 0.9242933989 + 0.9377337098 0.9419773817 0.9247027040 + 0.9380741715 0.9422981143 0.9251120090 + 0.9384146929 0.9426189065 0.9255211949 + 0.9387552142 0.9429395795 0.9259305000 + 0.9390947223 0.9432600141 0.9263395071 + 0.9394326210 0.9435796142 0.9267479181 + 0.9397705197 0.9438992739 0.9271563888 + 0.9401084185 0.9442188740 0.9275647998 + 0.9404463172 0.9445385933 0.9279732704 + 0.9407842755 0.9448581934 0.9283816814 + 0.9411221743 0.9451779127 0.9287902117 + 0.9414592981 0.9454959035 0.9291977882 + 0.9417958856 0.9458130002 0.9296048880 + 0.9421325922 0.9461300969 0.9300119877 + 0.9424692988 0.9464471936 0.9304192066 + 0.9428058863 0.9467642903 0.9308263063 + 0.9431425929 0.9470815063 0.9312335253 + 0.9434792995 0.9473986030 0.9316406250 + 0.9438139200 0.9477123022 0.9320465922 + 0.9441481829 0.9480254054 0.9324523807 + 0.9444825053 0.9483386278 0.9328582287 + 0.9448167086 0.9486516714 0.9332640171 + 0.9451509714 0.9489647746 0.9336698055 + 0.9454852939 0.9492779970 0.9340755939 + 0.9458196163 0.9495911002 0.9344813228 + 0.9461503029 0.9499025941 0.9348856807 + 0.9464811087 0.9502142072 0.9352900982 + 0.9468119144 0.9505257010 0.9356945157 + 0.9471427202 0.9508371949 0.9360988140 + 0.9474734068 0.9511486888 0.9365031719 + 0.9478042126 0.9514601827 0.9369075894 + 0.9481347203 0.9517716765 0.9373118281 + 0.9484633207 0.9520822167 0.9377142191 + 0.9487919211 0.9523928165 0.9381167293 + 0.9491205215 0.9527034163 0.9385191798 + 0.9494491220 0.9530138969 0.9389216900 + 0.9497776031 0.9533244967 0.9393242002 + 0.9501062036 0.9536349773 0.9397267103 + 0.9504346251 0.9539452195 0.9401286840 + 0.9507619739 0.9542526007 0.9405273795 + 0.9510893226 0.9545599818 0.9409261942 + 0.9514166713 0.9548674226 0.9413250089 + 0.9517440200 0.9551748037 0.9417237043 + 0.9520713091 0.9554821253 0.9421225190 + 0.9523987174 0.9557895064 0.9425212145 + 0.9527255893 0.9560965896 0.9429191947 + 0.9530493021 0.9564006925 0.9433112144 + 0.9533730745 0.9567047954 0.9437032938 + 0.9536967874 0.9570088983 0.9440953135 + 0.9540205002 0.9573130012 0.9444872737 + 0.9543442726 0.9576171041 0.9448792934 + 0.9546679854 0.9579212070 0.9452713132 + 0.9549915195 0.9582251906 0.9456630945 + 0.9553105235 0.9585260153 0.9460486770 + 0.9556294084 0.9588267803 0.9464343190 + 0.9559482932 0.9591276050 0.9468199015 + 0.9562671781 0.9594284296 0.9472054839 + 0.9565861225 0.9597290754 0.9475911260 + 0.9569051266 0.9600299001 0.9479767084 + 0.9572240114 0.9603307247 0.9483622909 + 0.9575372934 0.9606264234 0.9487441778 + 0.9578500986 0.9609215260 0.9491255879 + 0.9581627846 0.9612166286 0.9495071173 + 0.9584754705 0.9615116715 0.9498885870 + 0.9587882161 0.9618067741 0.9502701163 + 0.9591010213 0.9621018767 0.9506515265 + 0.9594137073 0.9623970985 0.9510329962 + 0.9597231150 0.9626886845 0.9514123797 + 0.9600310922 0.9629788995 0.9517909288 + 0.9603391290 0.9632689953 0.9521692991 + 0.9606471062 0.9635592103 0.9525477886 + 0.9609550834 0.9638493061 0.9529262781 + 0.9612631202 0.9641395211 0.9533047080 + 0.9615712166 0.9644296765 0.9536831975 + 0.9618782997 0.9647189975 0.9540600777 + 0.9621844292 0.9650071859 0.9544352293 + 0.9624904990 0.9652954936 0.9548102021 + 0.9627966285 0.9655836821 0.9551852942 + 0.9631026983 0.9658719897 0.9555603266 + 0.9634088278 0.9661601782 0.9559352994 + 0.9637148976 0.9664484859 0.9563103914 + 0.9640207887 0.9667364955 0.9566850066 + 0.9643250704 0.9670230150 0.9570569992 + 0.9646294117 0.9673094153 0.9574289918 + 0.9649336934 0.9675958753 0.9578009844 + 0.9652379751 0.9678822756 0.9581729770 + 0.9655423164 0.9681687951 0.9585450292 + 0.9658465981 0.9684551954 0.9589170218 + 0.9661508799 0.9687417150 0.9592890143 + 0.9664528966 0.9690259099 0.9596589804 + 0.9667541981 0.9693092108 0.9600282907 + 0.9670553803 0.9695925713 0.9603976011 + 0.9673566818 0.9698758721 0.9607669115 + 0.9676579237 0.9701592922 0.9611362219 + 0.9679592252 0.9704425931 0.9615054727 + 0.9682604074 0.9707260132 0.9618747830 + 0.9685606956 0.9710081220 0.9622433186 + 0.9688587785 0.9712873101 0.9626101255 + 0.9691568017 0.9715664983 0.9629768133 + 0.9694548845 0.9718456864 0.9633435011 + 0.9697530270 0.9721248746 0.9637103081 + 0.9700509906 0.9724041224 0.9640769958 + 0.9703490734 0.9726833105 0.9644438028 + 0.9706472158 0.9729624987 0.9648104906 + 0.9709429741 0.9732391238 0.9651746154 + 0.9712383747 0.9735150933 0.9655379057 + 0.9715337157 0.9737911224 0.9659013152 + 0.9718289971 0.9740669727 0.9662647247 + 0.9721243978 0.9743430018 0.9666280746 + 0.9724196792 0.9746189713 0.9669914842 + 0.9727150202 0.9748950005 0.9673548937 + 0.9730095863 0.9751701951 0.9677174091 + 0.9733015895 0.9754428864 0.9680770040 + 0.9735935926 0.9757155776 0.9684365988 + 0.9738855958 0.9759883285 0.9687961936 + 0.9741775990 0.9762610197 0.9691557884 + 0.9744696021 0.9765337110 0.9695153832 + 0.9747616053 0.9768065214 0.9698749185 + 0.9750537276 0.9770792127 0.9702345133 + 0.9753432274 0.9773492813 0.9705916047 + 0.9756311178 0.9776179194 0.9709470868 + 0.9759190083 0.9778863788 0.9713026285 + 0.9762068987 0.9781550169 0.9716581106 + 0.9764947891 0.9784234762 0.9720135927 + 0.9767827988 0.9786919951 0.9723690748 + 0.9770706892 0.9789605737 0.9727246165 + 0.9773585796 0.9792290926 0.9730800986 + 0.9776431918 0.9794937968 0.9734309912 + 0.9779276252 0.9797580838 0.9737815261 + 0.9782118797 0.9800224900 0.9741320014 + 0.9784963131 0.9802867770 0.9744824767 + 0.9787806273 0.9805511236 0.9748330116 + 0.9790650010 0.9808154106 0.9751834869 + 0.9793493152 0.9810798168 0.9755340219 + 0.9796329737 0.9813433886 0.9758836031 + 0.9799132943 0.9816035032 0.9762284756 + 0.9801936150 0.9818636179 0.9765734076 + 0.9804738760 0.9821236730 0.9769182801 + 0.9807541966 0.9823837876 0.9772632122 + 0.9810345769 0.9826439023 0.9776080251 + 0.9813148975 0.9829040170 0.9779528975 + 0.9815952182 0.9831640720 0.9782978296 + 0.9818738103 0.9834225178 0.9786403775 + 0.9821494222 0.9836776853 0.9789785743 + 0.9824249148 0.9839329123 0.9793167710 + 0.9827005267 0.9841880798 0.9796550274 + 0.9829760790 0.9844433069 0.9799932837 + 0.9832515717 0.9846984744 0.9803314805 + 0.9835271835 0.9849537015 0.9806696773 + 0.9838027954 0.9852089286 0.9810078740 + 0.9840760231 0.9854617715 0.9813424945 + 0.9843466878 0.9857121110 0.9816730022 + 0.9846174121 0.9859622717 0.9820033908 + 0.9848880768 0.9862126112 0.9823338985 + 0.9851588011 0.9864627719 0.9826642871 + 0.9854295254 0.9867131114 0.9829946756 + 0.9857001901 0.9869632721 0.9833251834 + 0.9859709144 0.9872136116 0.9836555719 + 0.9862385988 0.9874609709 0.9839816093 + 0.9865036011 0.9877058268 0.9843034744 + 0.9867686033 0.9879505038 0.9846252799 + 0.9870337248 0.9881951809 0.9849470854 + 0.9872987270 0.9884399772 0.9852690101 + 0.9875637293 0.9886847138 0.9855908155 + 0.9878286719 0.9889293909 0.9859126210 + 0.9880936742 0.9891741872 0.9862344265 + 0.9883552790 0.9894152284 0.9865508080 + 0.9886137247 0.9896525741 0.9868618250 + 0.9888721108 0.9898899198 0.9871727824 + 0.9891304970 0.9901272058 0.9874837995 + 0.9893888831 0.9903646111 0.9877948165 + 0.9896472096 0.9906018972 0.9881057143 + 0.9899055958 0.9908393025 0.9884166718 + 0.9901639819 0.9910765886 0.9887276888 + 0.9904199243 0.9913105965 0.9890335202 + 0.9906725883 0.9915400147 0.9893323183 + 0.9909251928 0.9917694926 0.9896311164 + 0.9911777973 0.9919989109 0.9899299145 + 0.9914304018 0.9922283292 0.9902285933 + 0.9916830063 0.9924576879 0.9905273914 + 0.9919356108 0.9926871061 0.9908261895 + 0.9921882153 0.9929165244 0.9911249876 + 0.9924386740 0.9931442142 0.9914202094 + 0.9926838279 0.9933673143 0.9917060137 + 0.9929288030 0.9935904145 0.9919918180 + 0.9931738973 0.9938135147 0.9922776222 + 0.9934189916 0.9940366149 0.9925634265 + 0.9936640263 0.9942597151 0.9928491712 + 0.9939091206 0.9944828153 0.9931349754 + 0.9941540956 0.9947059751 0.9934207201 + 0.9943984747 0.9949285984 0.9937055707 + 0.9946327806 0.9951431751 0.9939746857 + 0.9948670864 0.9953578115 0.9942438006 + 0.9951013923 0.9955723882 0.9945129156 + 0.9953356981 0.9957870841 0.9947819710 + 0.9955700040 0.9960017204 0.9950512052 + 0.9958043098 0.9962162971 0.9953203201 + 0.9960386157 0.9964308739 0.9955893755 + 0.9962729216 0.9966455102 0.9958584905 + 0.9964994192 0.9968507290 0.9961140156 + 0.9967237115 0.9970530868 0.9963654280 + 0.9969478846 0.9972555041 0.9966169000 + 0.9971721768 0.9974579215 0.9968683720 + 0.9973964095 0.9976602793 0.9971199036 + 0.9976207018 0.9978626966 0.9973713756 + 0.9978448749 0.9980651140 0.9976227880 + 0.9980692267 0.9982674718 0.9978743196 + 0.9982891083 0.9984654784 0.9981188774 + 0.9985029101 0.9986572862 0.9983540177 + 0.9987167716 0.9988490939 0.9985892177 + 0.9989306927 0.9990409017 0.9988242984 + 0.9991444945 0.9992327094 0.9990593791 + 0.9993584156 0.9994245768 0.9992945790 + 0.9995722771 0.9996163845 0.9995297194 + 0.9997860789 0.9998081923 0.9997649193 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_ii_100.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_ii_100.spi1d index 95453678..86fda3a9 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_ii_100.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_ii_100.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0089918738 0.0088112270 0.0067409412 - 0.0171655007 0.0167162009 0.0131348101 - 0.0242981501 0.0235243607 0.0187877901 - 0.0308058802 0.0297358502 0.0239518695 - 0.0368227884 0.0355986990 0.0288143605 - 0.0424588397 0.0412245393 0.0334390104 - 0.0477601402 0.0466648601 0.0378785282 - 0.0527938083 0.0519550890 0.0421676189 - 0.0575832911 0.0571624711 0.0463436693 - 0.0622247718 0.0623087510 0.0504317991 - 0.0668161213 0.0673468485 0.0544279404 - 0.0713887885 0.0722417235 0.0583436601 - 0.0759282932 0.0769878626 0.0621980987 - 0.0804320127 0.0816073194 0.0659957901 - 0.0848805234 0.0860867798 0.0697494522 - 0.0892914683 0.0904396772 0.0734607279 - 0.0936586633 0.0946798995 0.0771252811 - 0.0979755297 0.0988209695 0.0807478875 - 0.1022336036 0.1028660983 0.0843302682 - 0.1064319983 0.1068392023 0.0878701210 - 0.1105559021 0.1107354984 0.0913715884 - 0.1146129966 0.1145422980 0.0948381424 - 0.1185963973 0.1182733998 0.0982583165 - 0.1225240976 0.1219324991 0.1016350016 - 0.1263933927 0.1255291998 0.1049719974 - 0.1301935017 0.1290597022 0.1082687974 - 0.1339209974 0.1325256974 0.1115282029 - 0.1375810951 0.1359412074 0.1147558987 - 0.1411647946 0.1393028945 0.1179407015 - 0.1446768939 0.1426129043 0.1210891977 - 0.1481269002 0.1458766013 0.1242009997 - 0.1515025944 0.1490920037 0.1272723973 - 0.1548155993 0.1522623003 0.1303056926 - 0.1580781043 0.1553830951 0.1333058029 - 0.1612831950 0.1584683955 0.1362698972 - 0.1644275039 0.1615206003 0.1391921043 - 0.1675149053 0.1645296961 0.1420771927 - 0.1705511063 0.1675007045 0.1449283957 - 0.1735346019 0.1704356074 0.1477468014 - 0.1764681935 0.1733424962 0.1505275071 - 0.1793411970 0.1762108952 0.1532687992 - 0.1821642965 0.1790487021 0.1559778005 - 0.1849396974 0.1818661988 0.1586564928 - 0.1876723021 0.1846542954 0.1613045037 - 0.1903574020 0.1874137968 0.1639243066 - 0.1929987073 0.1901445985 0.1665160060 - 0.1956039965 0.1928485036 0.1690804064 - 0.1981741041 0.1955288947 0.1716223955 - 0.2007140964 0.1981831044 0.1741400957 - 0.2032240033 0.2008188963 0.1766290963 - 0.2057044953 0.2034270018 0.1790864021 - 0.2081623971 0.2060122043 0.1815200001 - 0.2106028944 0.2085773945 0.1839299053 - 0.2130292058 0.2111182064 0.1863144040 - 0.2154411972 0.2136391997 0.1886768937 - 0.2178471982 0.2161431015 0.1910151988 - 0.2202418000 0.2186270058 0.1933256984 - 0.2226282060 0.2210904956 0.1956081986 - 0.2250116020 0.2235327959 0.1978694052 - 0.2273910940 0.2259615064 0.2001121938 - 0.2297631949 0.2283712029 0.2023327053 - 0.2321317047 0.2307610959 0.2045315057 - 0.2344924062 0.2331354022 0.2067105025 - 0.2368433028 0.2354861945 0.2088731974 - 0.2391863018 0.2378192991 0.2110181004 - 0.2415205985 0.2401355058 0.2131436020 - 0.2438464016 0.2424366027 0.2152539939 - 0.2461618036 0.2447226048 0.2173456997 - 0.2484730929 0.2469923943 0.2194202989 - 0.2507739067 0.2492519021 0.2214788944 - 0.2530598044 0.2514967918 0.2235199958 - 0.2553376853 0.2537238002 0.2255467027 - 0.2576029897 0.2559401989 0.2275560051 - 0.2598524094 0.2581427991 0.2295472026 - 0.2620919049 0.2603318095 0.2315239012 - 0.2643215060 0.2625105977 0.2334856987 - 0.2665401995 0.2646790147 0.2354328036 - 0.2687489986 0.2668366134 0.2373681068 - 0.2709451020 0.2689833045 0.2392894030 - 0.2731316984 0.2711197138 0.2411931008 - 0.2753098011 0.2732459903 0.2430835962 - 0.2774744034 0.2753610909 0.2449620068 - 0.2796306908 0.2774659097 0.2468270957 - 0.2817795873 0.2795613110 0.2486820072 - 0.2839165926 0.2816452086 0.2505275905 - 0.2860453129 0.2837195992 0.2523604929 - 0.2881659865 0.2857829928 0.2541792989 - 0.2902776897 0.2878372073 0.2559899092 - 0.2923803031 0.2898777127 0.2577917874 - 0.2944732904 0.2919048965 0.2595866024 - 0.2965568006 0.2939237952 0.2613756955 - 0.2986293137 0.2959322035 0.2631560862 - 0.3006938994 0.2979266047 0.2649283111 - 0.3027508855 0.2999104857 0.2666937113 - 0.3047966063 0.3018867075 0.2684526145 - 0.3068324924 0.3038511872 0.2702021897 - 0.3088631034 0.3058032095 0.2719438076 - 0.3108848929 0.3077456951 0.2736783028 - 0.3128944933 0.3096817136 0.2754063010 - 0.3148933947 0.3116078973 0.2771281898 - 0.3168857992 0.3135224879 0.2788479924 - 0.3188689053 0.3154281080 0.2805649042 - 0.3208411932 0.3173280954 0.2822758853 - 0.3228051066 0.3192209005 0.2839803994 - 0.3247616887 0.3211036026 0.2856811881 - 0.3267089128 0.3229758143 0.2873789072 - 0.3286511004 0.3248389959 0.2890717983 - 0.3305900991 0.3266923130 0.2907603085 - 0.3325229883 0.3285365999 0.2924445868 - 0.3344469070 0.3303725123 0.2941254079 - 0.3363626897 0.3322010040 0.2958011925 - 0.3382742107 0.3340212107 0.2974714935 - 0.3401803970 0.3358336091 0.2991386950 - 0.3420785964 0.3376361132 0.3008036017 - 0.3439661860 0.3394295871 0.3024663031 - 0.3458459079 0.3412173986 0.3041245043 - 0.3477211893 0.3429979980 0.3057765961 - 0.3495895863 0.3447695971 0.3074248135 - 0.3514491022 0.3465310931 0.3090695143 - 0.3533000052 0.3482857049 0.3107109964 - 0.3551431000 0.3500339985 0.3123489022 - 0.3569791913 0.3517752886 0.3139818907 - 0.3588086069 0.3535098135 0.3156122863 - 0.3606314957 0.3552359939 0.3172419071 - 0.3624485135 0.3569523096 0.3188686073 - 0.3642578125 0.3586593866 0.3204928041 - 0.3660599887 0.3603613079 0.3221142888 - 0.3678553998 0.3620558083 0.3237330914 - 0.3696443141 0.3637424111 0.3253495991 - 0.3714273870 0.3654215038 0.3269616067 - 0.3732031882 0.3670935929 0.3285692930 - 0.3749721944 0.3687596917 0.3301742971 - 0.3767344952 0.3704194129 0.3317768872 - 0.3784905076 0.3720717132 0.3333773017 - 0.3802404106 0.3737153113 0.3349755108 - 0.3819842935 0.3753502071 0.3365707099 - 0.3837207854 0.3769770861 0.3381618857 - 0.3854480982 0.3785960078 0.3397505879 - 0.3871636093 0.3802081048 0.3413363993 - 0.3888713121 0.3818149865 0.3429175913 - 0.3905732930 0.3834156990 0.3444958925 - 0.3922694027 0.3850092888 0.3460716903 - 0.3939583898 0.3865957856 0.3476448953 - 0.3956395090 0.3881759942 0.3492155969 - 0.3973124027 0.3897528052 0.3507837951 - 0.3989773989 0.3913241029 0.3523463905 - 0.4006344974 0.3928903043 0.3539063931 - 0.4022842050 0.3944503963 0.3554647863 - 0.4039269090 0.3960036933 0.3570212126 - 0.4055627882 0.3975501060 0.3585749865 - 0.4071914852 0.3990882933 0.3601273000 - 0.4088127911 0.4006206095 0.3616780043 - 0.4104264975 0.4021480083 0.3632257879 - 0.4120318890 0.4036706090 0.3647710979 - 0.4136281908 0.4051879048 0.3663145900 - 0.4152132869 0.4066968858 0.3678534031 - 0.4167909920 0.4081991017 0.3693884909 - 0.4183610082 0.4096961021 0.3709208965 - 0.4199230969 0.4111889005 0.3724510074 - 0.4214732051 0.4126752913 0.3739764094 - 0.4230127037 0.4141556919 0.3754990995 - 0.4245432913 0.4156309962 0.3770196140 - 0.4260652065 0.4171015918 0.3785375953 - 0.4275765121 0.4185652137 0.3800503016 - 0.4290784895 0.4200240970 0.3815599978 - 0.4305722117 0.4214783013 0.3830671012 - 0.4320571125 0.4229275882 0.3845714033 - 0.4335275888 0.4243701994 0.3860712945 - 0.4349885881 0.4258084893 0.3875694871 - 0.4364405870 0.4272427857 0.3890654147 - 0.4378808141 0.4286721051 0.3905574977 - 0.4393113852 0.4300973117 0.3920451999 - 0.4407345057 0.4315193892 0.3935315013 - 0.4421485960 0.4329378009 0.3950169981 - 0.4435501993 0.4343496859 0.3964990079 - 0.4449434876 0.4357573986 0.3979789019 - 0.4463286996 0.4371610880 0.3994567990 - 0.4477010071 0.4385577142 0.4009310007 - 0.4490672052 0.4399507046 0.4024015069 - 0.4504272938 0.4413402975 0.4038689137 - 0.4517762065 0.4427213967 0.4053323865 - 0.4531182945 0.4440990090 0.4067929983 - 0.4544540942 0.4454734921 0.4082511961 - 0.4557775855 0.4468427896 0.4097048938 - 0.4570952952 0.4482089877 0.4111567140 - 0.4584074914 0.4495720863 0.4126070142 - 0.4597108960 0.4509283006 0.4140538871 - 0.4610100091 0.4522809982 0.4154989123 - 0.4623034894 0.4536291957 0.4169419110 - 0.4635887146 0.4549719095 0.4183813035 - 0.4648694992 0.4563120902 0.4198183119 - 0.4661431015 0.4576481879 0.4212512076 - 0.4674094021 0.4589804113 0.4226809144 - 0.4686715007 0.4603103101 0.4241088927 - 0.4699237943 0.4616335034 0.4255332053 - 0.4711718857 0.4629541039 0.4269537926 - 0.4724155962 0.4642721117 0.4283713996 - 0.4736534059 0.4655844867 0.4297845066 - 0.4748887122 0.4668945074 0.4311957955 - 0.4761190116 0.4681994021 0.4326032996 - 0.4773451984 0.4694997966 0.4340074062 - 0.4785684049 0.4707967937 0.4354093969 - 0.4797854125 0.4720880985 0.4368061125 - 0.4810002148 0.4733774066 0.4382002950 - 0.4822092950 0.4746614993 0.4395891130 - 0.4834147990 0.4759416878 0.4409742951 - 0.4846169055 0.4772185087 0.4423570931 - 0.4858146906 0.4784907103 0.4437364042 - 0.4870111048 0.4797612131 0.4451141953 - 0.4882034063 0.4810256064 0.4464867115 - 0.4893937111 0.4822880924 0.4478574991 - 0.4905807078 0.4835473895 0.4492256939 - 0.4917649031 0.4848031998 0.4505923092 - 0.4929473102 0.4860568047 0.4519579113 - 0.4941253960 0.4873049855 0.4533194900 - 0.4953022897 0.4885519147 0.4546799958 - 0.4964750111 0.4897922873 0.4560363889 - 0.4976463020 0.4910312891 0.4573906064 - 0.4988138080 0.4922665060 0.4587404132 - 0.4999791980 0.4934988022 0.4600867033 - 0.5011422038 0.4947274923 0.4614296854 - 0.5023019910 0.4959532917 0.4627693892 - 0.5034598708 0.4971773922 0.4641076028 - 0.5046142936 0.4983972907 0.4654398859 - 0.5057677031 0.4996159971 0.4667710960 - 0.5069164038 0.5008293986 0.4680955112 - 0.5080643892 0.5020418763 0.4694189131 - 0.5092083812 0.5032485127 0.4707360864 - 0.5103514194 0.5044537783 0.4720517099 - 0.5114902854 0.5056533813 0.4733619988 - 0.5126280189 0.5068514943 0.4746705890 - 0.5137624741 0.5080453753 0.4759756923 - 0.5148952007 0.5092371702 0.4772787094 - 0.5160247087 0.5104253888 0.4785786867 - 0.5171523094 0.5116108060 0.4798755050 - 0.5182775259 0.5127922893 0.4811685979 - 0.5194002986 0.5139706731 0.4824582040 - 0.5205205083 0.5151454806 0.4837445021 - 0.5216385126 0.5163174272 0.4850262105 - 0.5227543712 0.5174863935 0.4863041043 - 0.5238677263 0.5186526775 0.4875771999 - 0.5249785781 0.5198165774 0.4888466895 - 0.5260874033 0.5209770203 0.4901134968 - 0.5271940231 0.5221341848 0.4913783967 - 0.5282983184 0.5232884884 0.4926396906 - 0.5294001102 0.5244398713 0.4938982129 - 0.5304998755 0.5255879164 0.4951525033 - 0.5315973759 0.5267319083 0.4964028895 - 0.5326929092 0.5278732777 0.4976496994 - 0.5337855816 0.5290107727 0.4988923073 - 0.5348768830 0.5301455259 0.5001319051 - 0.5359653831 0.5312750936 0.5013670921 - 0.5370525718 0.5324026942 0.5025998950 - 0.5381363034 0.5335251093 0.5038275123 - 0.5392193198 0.5346459746 0.5050536990 - 0.5402988195 0.5357596874 0.5062742829 - 0.5413780808 0.5368732214 0.5074942708 - 0.5424528718 0.5379806757 0.5087080002 - 0.5435277224 0.5390880704 0.5099217296 - 0.5445986986 0.5401890874 0.5111290216 - 0.5456693172 0.5412895083 0.5123360753 - 0.5467364192 0.5423848033 0.5135380030 - 0.5478023291 0.5434783101 0.5147387981 - 0.5488657951 0.5445675254 0.5159347057 - 0.5499275923 0.5456534028 0.5171278119 - 0.5509874821 0.5467371941 0.5183176994 - 0.5520446897 0.5478175282 0.5195037723 - 0.5531010032 0.5488967896 0.5206878185 - 0.5541540980 0.5499712825 0.5218651891 - 0.5552070737 0.5510457754 0.5230425000 - 0.5562556982 0.5521146059 0.5242115855 - 0.5573040843 0.5531831980 0.5253806710 - 0.5583497286 0.5542473793 0.5265437961 - 0.5593940020 0.5553098917 0.5277050734 - 0.5604363084 0.5563696027 0.5288618803 - 0.5614761114 0.5574259162 0.5300140977 - 0.5625151992 0.5584810972 0.5311648846 - 0.5635510087 0.5595309734 0.5323101282 - 0.5645866990 0.5605809093 0.5334553123 - 0.5656185150 0.5616251826 0.5345945954 - 0.5666496754 0.5626686215 0.5357334018 - 0.5676786900 0.5637084246 0.5368676782 - 0.5687059760 0.5647451878 0.5379989743 - 0.5697323084 0.5657805800 0.5391280055 - 0.5707550049 0.5668106079 0.5402505994 - 0.5717777014 0.5678406954 0.5413731933 - 0.5727970004 0.5688648820 0.5424870849 - 0.5738158226 0.5698879957 0.5435999036 - 0.5748323798 0.5709078908 0.5447078943 - 0.5758466721 0.5719239712 0.5458114147 - 0.5768607259 0.5729396939 0.5469142199 - 0.5778710842 0.5739501119 0.5480110049 - 0.5788813829 0.5749605894 0.5491077900 - 0.5798888803 0.5759674907 0.5501992106 - 0.5808948278 0.5769727230 0.5512881875 - 0.5818998814 0.5779765844 0.5523753166 - 0.5829018950 0.5789757967 0.5534570813 - 0.5839039087 0.5799751282 0.5545389056 - 0.5849025846 0.5809702873 0.5556156039 - 0.5859003067 0.5819640756 0.5566909909 - 0.5868967772 0.5829563141 0.5577639937 - 0.5878906250 0.5839440823 0.5588322282 - 0.5888844132 0.5849320292 0.5599002838 - 0.5898746848 0.5859153867 0.5609617233 - 0.5908641219 0.5868976116 0.5620216727 - 0.5918524861 0.5878787041 0.5630789995 - 0.5928381085 0.5888563991 0.5641300082 - 0.5938236713 0.5898339748 0.5651810169 - 0.5948061943 0.5908092260 0.5662257075 - 0.5957874060 0.5917832851 0.5672683120 - 0.5967680812 0.5927566886 0.5683094859 - 0.5977455974 0.5937262774 0.5693448782 - 0.5987229943 0.5946959257 0.5703802705 - 0.5996980071 0.5956636071 0.5714107752 - 0.6006711721 0.5966296196 0.5724381804 - 0.6016442776 0.5975956917 0.5734654069 - 0.6026139259 0.5985574722 0.5744841099 - 0.6035832763 0.5995190740 0.5755028725 - 0.6045511961 0.6004796028 0.5765179992 - 0.6055163741 0.6014376879 0.5775272250 - 0.6064814925 0.6023957729 0.5785365105 - 0.6074441075 0.6033508778 0.5795394778 - 0.6084054708 0.6043046117 0.5805402994 - 0.6093667746 0.6052582264 0.5815405846 - 0.6103240252 0.6062085032 0.5825338960 - 0.6112812161 0.6071587205 0.5835272074 - 0.6122370958 0.6081075072 0.5845175982 - 0.6131905913 0.6090533137 0.5855029821 - 0.6141440868 0.6099991798 0.5864884853 - 0.6150950193 0.6109429002 0.5874683857 - 0.6160442829 0.6118853092 0.5884453058 - 0.6169936061 0.6128277183 0.5894222260 - 0.6179397106 0.6137663126 0.5903909206 - 0.6188852787 0.6147043109 0.5913589001 - 0.6198304892 0.6156420112 0.5923253894 - 0.6207720041 0.6165766120 0.5932837725 - 0.6217133999 0.6175112128 0.5942422152 - 0.6226537228 0.6184443831 0.5951976180 - 0.6235914826 0.6193746924 0.5961477160 - 0.6245294213 0.6203050017 0.5970978737 - 0.6254649758 0.6212335825 0.5980439782 - 0.6263986826 0.6221604943 0.5989869237 - 0.6273323894 0.6230874062 0.5999299288 - 0.6282635927 0.6240113974 0.6008661985 - 0.6291937232 0.6249340177 0.6018002033 - 0.6301239133 0.6258566976 0.6027342081 - 0.6310505271 0.6267765164 0.6036599874 - 0.6319764853 0.6276957989 0.6045848131 - 0.6329025030 0.6286150813 0.6055094004 - 0.6338251233 0.6295301914 0.6064245105 - 0.6347475052 0.6304453015 0.6073396206 - 0.6356697083 0.6313601136 0.6082538962 - 0.6365880966 0.6322718263 0.6091607809 - 0.6375064254 0.6331834793 0.6100676060 - 0.6384242773 0.6340945959 0.6109727025 - 0.6393392086 0.6350020766 0.6118695140 - 0.6402540207 0.6359096169 0.6127663255 - 0.6411681175 0.6368162036 0.6136608124 - 0.6420788765 0.6377186179 0.6145474911 - 0.6429896951 0.6386210918 0.6154342294 - 0.6438996792 0.6395224929 0.6163187027 - 0.6448069811 0.6404194832 0.6171966195 - 0.6457142830 0.6413165927 0.6180744767 - 0.6466206908 0.6422126889 0.6189501882 - 0.6475239992 0.6431049109 0.6198194027 - 0.6484273076 0.6439971924 0.6206886172 - 0.6493299007 0.6448885202 0.6215558052 - 0.6502298117 0.6457754970 0.6224160790 - 0.6511296034 0.6466624737 0.6232764721 - 0.6520287991 0.6475486159 0.6241351962 - 0.6529247165 0.6484292150 0.6249868274 - 0.6538205147 0.6493096948 0.6258383989 - 0.6547160745 0.6501898170 0.6266890764 - 0.6556084752 0.6510639787 0.6275333762 - 0.6565008759 0.6519382000 0.6283777952 - 0.6573932767 0.6528124213 0.6292219758 - 0.6582819223 0.6536806226 0.6300593019 - 0.6591703296 0.6545485854 0.6308966279 - 0.6600587964 0.6554166079 0.6317338943 - 0.6609442830 0.6562796235 0.6325649023 - 0.6618294120 0.6571416855 0.6333950758 - 0.6627144217 0.6580038071 0.6342253089 - 0.6635968089 0.6588618159 0.6350504160 - 0.6644778848 0.6597180963 0.6358736753 - 0.6653590202 0.6605743766 0.6366969943 - 0.6662384272 0.6614277959 0.6375163794 - 0.6671161056 0.6622784734 0.6383327842 - 0.6679939032 0.6631292105 0.6391491294 - 0.6688703895 0.6639782190 0.6399635077 - 0.6697443128 0.6648234725 0.6407744288 - 0.6706181765 0.6656687856 0.6415852904 - 0.6714916825 0.6665136218 0.6423954964 - 0.6723623276 0.6673536897 0.6432008743 - 0.6732327938 0.6681938171 0.6440063715 - 0.6741033196 0.6690338850 0.6448118091 - 0.6749706268 0.6698697209 0.6456125975 - 0.6758372784 0.6707047820 0.6464126706 - 0.6767038703 0.6715397835 0.6472128034 - 0.6775687933 0.6723722219 0.6480097771 - 0.6784321070 0.6732022762 0.6488046050 - 0.6792954803 0.6740325093 0.6495993733 - 0.6801578999 0.6748616099 0.6503927112 - 0.6810173988 0.6756870151 0.6511822939 - 0.6818770170 0.6765124798 0.6519718170 - 0.6827365160 0.6773380041 0.6527613997 - 0.6835916042 0.6781579256 0.6535460949 - 0.6844462752 0.6789773107 0.6543303728 - 0.6853010058 0.6797965765 0.6551148295 - 0.6861529946 0.6806129813 0.6558970809 - 0.6870027184 0.6814268231 0.6566780210 - 0.6878523827 0.6822406054 0.6574587822 - 0.6887013912 0.6830536127 0.6582391262 - 0.6895464063 0.6838616729 0.6590166092 - 0.6903914213 0.6846699119 0.6597940922 - 0.6912363768 0.6854779720 0.6605716944 - 0.6920778751 0.6862823963 0.6613466740 - 0.6929180026 0.6870855093 0.6621211171 - 0.6937581897 0.6878886223 0.6628953815 - 0.6945971251 0.6886901855 0.6636688113 - 0.6954327226 0.6894879937 0.6644400954 - 0.6962683201 0.6902858019 0.6652113795 - 0.6971039176 0.6910836101 0.6659826040 - 0.6979358196 0.6918777227 0.6667515039 - 0.6987668276 0.6926708817 0.6675199270 - 0.6995977163 0.6934641004 0.6682882905 - 0.7004268765 0.6942554712 0.6690558195 - 0.7012518048 0.6950421929 0.6698213816 - 0.7020766735 0.6958289742 0.6705870032 - 0.7029017210 0.6966156960 0.6713526845 - 0.7037221789 0.6973984838 0.6721162796 - 0.7045413256 0.6981800199 0.6728792787 - 0.7053604126 0.6989613771 0.6736422777 - 0.7061784267 0.6997417808 0.6744047999 - 0.7069919109 0.7005174756 0.6751654148 - 0.7078052759 0.7012931108 0.6759259105 - 0.7086187005 0.7020686865 0.6766865253 - 0.7094289064 0.7028416991 0.6774455905 - 0.7102369070 0.7036126852 0.6782038808 - 0.7110447884 0.7043836713 0.6789621115 - 0.7118527889 0.7051547170 0.6797204018 - 0.7126556039 0.7059206963 0.6804764867 - 0.7134582996 0.7066864967 0.6812325716 - 0.7142608762 0.7074524164 0.6819887161 - 0.7150620222 0.7082164884 0.6827440858 - 0.7158594728 0.7089766860 0.6834982038 - 0.7166571021 0.7097368836 0.6842523217 - 0.7174546123 0.7104970813 0.6850063801 - 0.7182490826 0.7112538218 0.6857591867 - 0.7190417051 0.7120084167 0.6865115166 - 0.7198343277 0.7127628922 0.6872637272 - 0.7206268907 0.7135174870 0.6880158782 - 0.7214152217 0.7142676115 0.6887664795 - 0.7222030163 0.7150173187 0.6895170808 - 0.7229908109 0.7157669067 0.6902676225 - 0.7237780094 0.7165157795 0.6910178065 - 0.7245612144 0.7172604799 0.6917667985 - 0.7253444791 0.7180052996 0.6925156713 - 0.7261276841 0.7187500000 0.6932647228 - 0.7269088030 0.7194930911 0.6940131187 - 0.7276861072 0.7202336192 0.6947606206 - 0.7284634709 0.7209740877 0.6955081820 - 0.7292407751 0.7217146754 0.6962556839 - 0.7300150990 0.7224528193 0.6970024705 - 0.7307869792 0.7231891155 0.6977487803 - 0.7315589190 0.7239254117 0.6984950900 - 0.7323307991 0.7246617079 0.6992413998 - 0.7330988050 0.7253944278 0.6999868155 - 0.7338652015 0.7261255980 0.7007319927 - 0.7346317172 0.7268568277 0.7014772296 - 0.7353981137 0.7275879979 0.7022224069 - 0.7361603975 0.7283154130 0.7029678822 - 0.7369217873 0.7290419936 0.7037134767 - 0.7376831770 0.7297685742 0.7044590712 - 0.7384446859 0.7304952145 0.7052047849 - 0.7392014265 0.7312179208 0.7059507966 - 0.7399578094 0.7319403291 0.7066968083 - 0.7407141924 0.7326626778 0.7074428201 - 0.7414705753 0.7333850861 0.7081888914 - 0.7422224879 0.7341023088 0.7089354992 - 0.7429742813 0.7348194718 0.7096819878 - 0.7437260747 0.7355365753 0.7104285955 - 0.7444778085 0.7362536788 0.7111750841 - 0.7452235222 0.7369664907 0.7119207978 - 0.7459692955 0.7376793027 0.7126665115 - 0.7467150092 0.7383921146 0.7134122252 - 0.7474606037 0.7391046882 0.7141578197 - 0.7482010126 0.7398132086 0.7149025798 - 0.7489414215 0.7405217290 0.7156472802 - 0.7496817708 0.7412303090 0.7163920999 - 0.7504222989 0.7419388294 0.7171369195 - 0.7511572838 0.7426438928 0.7178807259 - 0.7518922091 0.7433488965 0.7186245918 - 0.7526270747 0.7440540195 0.7193683982 - 0.7533621192 0.7447590828 0.7201123238 - 0.7540926933 0.7454611063 0.7208564281 - 0.7548229098 0.7461627722 0.7216005921 - 0.7555531859 0.7468644977 0.7223448157 - 0.7562834024 0.7475662231 0.7230889201 - 0.7570096850 0.7482659221 0.7238321900 - 0.7577350736 0.7489652038 0.7245752811 - 0.7584605217 0.7496644258 0.7253183722 - 0.7591857910 0.7503635883 0.7260615230 - 0.7599074841 0.7510594130 0.7268037796 - 0.7606273293 0.7517535090 0.7275456786 - 0.7613471150 0.7524476051 0.7282875776 - 0.7620670199 0.7531417012 0.7290294766 - 0.7627841234 0.7538337111 0.7297707796 - 0.7634984851 0.7545235753 0.7305114269 - 0.7642129064 0.7552134991 0.7312520742 - 0.7649273872 0.7559034228 0.7319927216 - 0.7656403184 0.7565919757 0.7327328920 - 0.7663499117 0.7572777867 0.7334721088 - 0.7670595050 0.7579635978 0.7342113853 - 0.7677689791 0.7586494088 0.7349506021 - 0.7684782743 0.7593349814 0.7356896996 - 0.7691832185 0.7600160837 0.7364274859 - 0.7698879838 0.7606971860 0.7371652126 - 0.7705928087 0.7613782883 0.7379028797 - 0.7712976933 0.7620593905 0.7386407256 - 0.7719981074 0.7627363801 0.7393770814 - 0.7726973295 0.7634121776 0.7401133180 - 0.7733964920 0.7640879750 0.7408493757 - 0.7740957141 0.7647637129 0.7415854931 - 0.7747924924 0.7654371262 0.7423207760 - 0.7754864097 0.7661076188 0.7430552840 - 0.7761803865 0.7667781115 0.7437896729 - 0.7768744230 0.7674486041 0.7445241213 - 0.7775678039 0.7681186199 0.7452582717 - 0.7782570124 0.7687847018 0.7459908724 - 0.7789462805 0.7694507241 0.7467234731 - 0.7796354890 0.7701168060 0.7474560738 - 0.7803246975 0.7707828879 0.7481886744 - 0.7810096741 0.7714446783 0.7489197850 - 0.7816932201 0.7721049190 0.7496504784 - 0.7823767066 0.7727652192 0.7503811717 - 0.7830601931 0.7734254003 0.7511119246 - 0.7837420702 0.7740842104 0.7518419027 - 0.7844206095 0.7747399211 0.7525705099 - 0.7850990891 0.7753955722 0.7532991171 - 0.7857775092 0.7760512829 0.7540277839 - 0.7864559889 0.7767069936 0.7547563910 - 0.7871304154 0.7773585916 0.7554832101 - 0.7878041267 0.7780095935 0.7562096715 - 0.7884777784 0.7786604762 0.7569361925 - 0.7891514897 0.7793114185 0.7576625943 - 0.7898237109 0.7799605727 0.7583882809 - 0.7904931903 0.7806066871 0.7591124773 - 0.7911627889 0.7812528014 0.7598366737 - 0.7918323874 0.7818987966 0.7605609298 - 0.7925019264 0.7825449109 0.7612851858 - 0.7931671143 0.7831870914 0.7620074153 - 0.7938314080 0.7838284969 0.7627292275 - 0.7944955826 0.7844697833 0.7634510994 - 0.7951598167 0.7851111889 0.7641729116 - 0.7958226800 0.7857515812 0.7648941278 - 0.7964813113 0.7863892913 0.7656133771 - 0.7971398830 0.7870268822 0.7663328052 - 0.7977985740 0.7876644731 0.7670521140 - 0.7984572053 0.7883021832 0.7677714825 - 0.7991123796 0.7889375091 0.7684891224 - 0.7997658253 0.7895715833 0.7692058086 - 0.8004190922 0.7902057171 0.7699226141 - 0.8010724783 0.7908397913 0.7706393003 - 0.8017258048 0.7914739251 0.7713559866 - 0.8023747206 0.7921038866 0.7720699906 - 0.8030235171 0.7927339077 0.7727841139 - 0.8036723733 0.7933639288 0.7734981179 - 0.8043212295 0.7939940095 0.7742121220 - 0.8049684167 0.7946221232 0.7749251127 - 0.8056119084 0.7952463031 0.7756363153 - 0.8062555194 0.7958704829 0.7763475180 - 0.8068990111 0.7964947224 0.7770587206 - 0.8075426221 0.7971189022 0.7777698040 - 0.8081833124 0.7977399826 0.7784792185 - 0.8088219762 0.7983589172 0.7791873813 - 0.8094608188 0.7989779115 0.7798956037 - 0.8100996017 0.7995969057 0.7806038260 - 0.8107383251 0.8002158999 0.7813121080 - 0.8113725185 0.8008310199 0.7820175886 - 0.8120058179 0.8014454246 0.7827227712 - 0.8126389980 0.8020598292 0.7834278941 - 0.8132722974 0.8026741743 0.7841330767 - 0.8139054179 0.8032884002 0.7848380208 - 0.8145325184 0.8038988113 0.7855399847 - 0.8151596189 0.8045092821 0.7862420082 - 0.8157867193 0.8051196933 0.7869439125 - 0.8164138198 0.8057301044 0.7876458764 - 0.8170399070 0.8063399196 0.7883471251 - 0.8176615238 0.8069468737 0.7890458107 - 0.8182830811 0.8075540066 0.7897444963 - 0.8189046979 0.8081610203 0.7904431224 - 0.8195263743 0.8087679744 0.7911418080 - 0.8201465011 0.8093742728 0.7918393016 - 0.8207632899 0.8099786043 0.7925345898 - 0.8213800788 0.8105828762 0.7932298779 - 0.8219968081 0.8111872077 0.7939251065 - 0.8226135969 0.8117914796 0.7946203947 - 0.8232282996 0.8123940229 0.7953141928 - 0.8238397241 0.8129937053 0.7960060239 - 0.8244510293 0.8135933876 0.7966976762 - 0.8250623941 0.8141930103 0.7973895073 - 0.8256736994 0.8147926927 0.7980812788 - 0.8262823820 0.8153901100 0.7987713814 - 0.8268874884 0.8159844279 0.7994595766 - 0.8274924755 0.8165788054 0.8001477122 - 0.8280975223 0.8171731234 0.8008359075 - 0.8287026286 0.8177673817 0.8015239835 - 0.8293048739 0.8183599710 0.8022106290 - 0.8299033046 0.8189499974 0.8028950095 - 0.8305016160 0.8195400238 0.8035795093 - 0.8310999870 0.8201299906 0.8042638898 - 0.8316984177 0.8207200170 0.8049483895 - 0.8322945237 0.8213084936 0.8056312799 - 0.8328866959 0.8218945265 0.8063120246 - 0.8334789276 0.8224804997 0.8069925904 - 0.8340710998 0.8230664730 0.8076732755 - 0.8346632719 0.8236525059 0.8083540201 - 0.8352540135 0.8242375851 0.8090333939 - 0.8358410001 0.8248205781 0.8097102046 - 0.8364279270 0.8254035711 0.8103870153 - 0.8370147943 0.8259866238 0.8110637069 - 0.8376017213 0.8265694976 0.8117405176 - 0.8381876945 0.8271517754 0.8124166131 - 0.8387684822 0.8277295828 0.8130893707 - 0.8393492103 0.8283075094 0.8137621880 - 0.8399299979 0.8288853765 0.8144350052 - 0.8405107260 0.8294633031 0.8151078224 - 0.8410913944 0.8300412297 0.8157805800 - 0.8416661024 0.8306139112 0.8164492846 - 0.8422405720 0.8311864138 0.8171179891 - 0.8428151011 0.8317589760 0.8177868128 - 0.8433896899 0.8323314786 0.8184555173 - 0.8439642191 0.8329041004 0.8191242218 - 0.8445343971 0.8334730864 0.8197888136 - 0.8451033235 0.8340409994 0.8204522729 - 0.8456721902 0.8346089125 0.8211157918 - 0.8462411165 0.8351768255 0.8217793107 - 0.8468099833 0.8357446790 0.8224428296 - 0.8473765850 0.8363106847 0.8231034875 - 0.8479408026 0.8368747234 0.8237618208 - 0.8485050201 0.8374385834 0.8244199753 - 0.8490692973 0.8380026221 0.8250783086 - 0.8496335149 0.8385666013 0.8257365227 - 0.8501964808 0.8391299844 0.8263936043 - 0.8507544994 0.8396906853 0.8270465136 - 0.8513123989 0.8402515054 0.8276994228 - 0.8518704176 0.8408122063 0.8283522725 - 0.8524283171 0.8413730264 0.8290051818 - 0.8529862761 0.8419337273 0.8296582103 - 0.8535385728 0.8424912095 0.8303064108 - 0.8540899158 0.8430479765 0.8309540153 - 0.8546411991 0.8436049223 0.8316016197 - 0.8551924825 0.8441618085 0.8322492242 - 0.8557438254 0.8447186947 0.8328968287 - 0.8562924862 0.8452731967 0.8335418105 - 0.8568379879 0.8458250165 0.8341839910 - 0.8573834896 0.8463767767 0.8348261714 - 0.8579289913 0.8469285965 0.8354684114 - 0.8584744930 0.8474804163 0.8361105919 - 0.8590198755 0.8480321169 0.8367525935 - 0.8595604897 0.8485792279 0.8373904824 - 0.8601011038 0.8491263986 0.8380283117 - 0.8606415987 0.8496735096 0.8386662006 - 0.8611822128 0.8502206206 0.8393040895 - 0.8617228270 0.8507677913 0.8399419785 - 0.8622605205 0.8513128757 0.8405768275 - 0.8627955914 0.8518565297 0.8412091136 - 0.8633307815 0.8524000049 0.8418415189 - 0.8638659120 0.8529435992 0.8424739242 - 0.8644011021 0.8534870744 0.8431062102 - 0.8649361134 0.8540306091 0.8437383771 - 0.8654658198 0.8545699716 0.8443654180 - 0.8659955263 0.8551093936 0.8449923992 - 0.8665251732 0.8556488156 0.8456193805 - 0.8670548797 0.8561881781 0.8462464213 - 0.8675845861 0.8567274809 0.8468734026 - 0.8681117296 0.8572648168 0.8474978805 - 0.8686357141 0.8577994704 0.8481194973 - 0.8691598177 0.8583341241 0.8487411141 - 0.8696838021 0.8588687778 0.8493627906 - 0.8702077866 0.8594034910 0.8499844074 - 0.8707317710 0.8599380851 0.8506060243 - 0.8712515831 0.8604686856 0.8512223959 - 0.8717706203 0.8609983921 0.8518378139 - 0.8722895980 0.8615282178 0.8524531126 - 0.8728085756 0.8620579243 0.8530684710 - 0.8733276129 0.8625876904 0.8536838889 - 0.8738456964 0.8631166816 0.8542981148 - 0.8743596077 0.8636423945 0.8549072742 - 0.8748735189 0.8641679883 0.8555164933 - 0.8753873110 0.8646935821 0.8561257124 - 0.8759012222 0.8652192950 0.8567348719 - 0.8764150739 0.8657448888 0.8573440909 - 0.8769264817 0.8662691116 0.8579511046 - 0.8774347901 0.8667913079 0.8585553765 - 0.8779432178 0.8673136234 0.8591597080 - 0.8784515858 0.8678358793 0.8597639799 - 0.8789598942 0.8683581948 0.8603683710 - 0.8794683218 0.8688805103 0.8609727025 - 0.8799737096 0.8694002032 0.8615736961 - 0.8804777265 0.8699188232 0.8621733785 - 0.8809818029 0.8704373240 0.8627730012 - 0.8814858198 0.8709558845 0.8633726835 - 0.8819897771 0.8714743853 0.8639724255 - 0.8824939132 0.8719930053 0.8645719886 - 0.8829929233 0.8725072145 0.8651664853 - 0.8834912181 0.8730208278 0.8657605052 - 0.8839895725 0.8735345006 0.8663544059 - 0.8844879270 0.8740481138 0.8669483066 - 0.8849862218 0.8745617270 0.8675422072 - 0.8854843974 0.8750752807 0.8681359291 - 0.8859770298 0.8755844831 0.8687235117 - 0.8864694834 0.8760938048 0.8693109751 - 0.8869621158 0.8766030073 0.8698986173 - 0.8874546289 0.8771122098 0.8704861999 - 0.8879472017 0.8776214719 0.8710737228 - 0.8884391189 0.8781303167 0.8716604114 - 0.8889269233 0.8786363006 0.8722419143 - 0.8894146085 0.8791422844 0.8728232980 - 0.8899024129 0.8796483278 0.8734048009 - 0.8903900981 0.8801543117 0.8739861846 - 0.8908779025 0.8806601763 0.8745676875 - 0.8913648129 0.8811656237 0.8751479983 - 0.8918477297 0.8816679120 0.8757237196 - 0.8923305869 0.8821702003 0.8762993217 - 0.8928135037 0.8826724291 0.8768749833 - 0.8932964206 0.8831747174 0.8774505854 - 0.8937792778 0.8836770058 0.8780263066 - 0.8942611814 0.8841785789 0.8786008954 - 0.8947380781 0.8846768141 0.8791710138 - 0.8952149153 0.8851749897 0.8797410727 - 0.8956918120 0.8856732249 0.8803111911 - 0.8961687088 0.8861714005 0.8808813095 - 0.8966454864 0.8866695762 0.8814514279 - 0.8971219063 0.8871673942 0.8820207119 - 0.8975936174 0.8876612782 0.8825843930 - 0.8980653286 0.8881552815 0.8831480145 - 0.8985369802 0.8886492252 0.8837116957 - 0.8990086913 0.8891432285 0.8842753768 - 0.8994804025 0.8896371126 0.8848391175 - 0.8999521136 0.8901311159 0.8854026198 - 0.9004188776 0.8906217217 0.8859593272 - 0.9008855820 0.8911123276 0.8865159750 - 0.9013522863 0.8916028738 0.8870726228 - 0.9018189907 0.8920934796 0.8876292706 - 0.9022856951 0.8925840855 0.8881859779 - 0.9027525187 0.8930746913 0.8887426853 - 0.9032139182 0.8935613036 0.8892936707 - 0.9036744833 0.8940473199 0.8898438811 - 0.9041351080 0.8945332170 0.8903939724 - 0.9045956731 0.8950191736 0.8909441829 - 0.9050561786 0.8955051899 0.8914942741 - 0.9055168033 0.8959910870 0.8920444846 - 0.9059740901 0.8964738250 0.8925905228 - 0.9064297080 0.8969547153 0.8931344151 - 0.9068853259 0.8974357247 0.8936784267 - 0.9073408842 0.8979166150 0.8942223191 - 0.9077963829 0.8983975053 0.8947662711 - 0.9082520008 0.8988783956 0.8953102231 - 0.9087055922 0.8993577957 0.8958514929 - 0.9091560841 0.8998348713 0.8963887095 - 0.9096066952 0.9003120065 0.8969259858 - 0.9100571871 0.9007890224 0.8974633217 - 0.9105077982 0.9012660980 0.8980004787 - 0.9109582901 0.9017431736 0.8985378146 - 0.9114084840 0.9022198915 0.8990743160 - 0.9118540883 0.9026929140 0.8996040821 - 0.9122998118 0.9031658173 0.9001339078 - 0.9127454162 0.9036387801 0.9006636739 - 0.9131910801 0.9041116834 0.9011934996 - 0.9136366844 0.9045847058 0.9017233253 - 0.9140822887 0.9050576091 0.9022530913 - 0.9145244956 0.9055281878 0.9027777910 - 0.9149652719 0.9059978724 0.9033007026 - 0.9154061079 0.9064676166 0.9038236141 - 0.9158468843 0.9069373012 0.9043465257 - 0.9162877798 0.9074069262 0.9048693776 - 0.9167286158 0.9078766108 0.9053922892 - 0.9171674848 0.9083452225 0.9059128165 - 0.9176024795 0.9088112712 0.9064283967 - 0.9180374146 0.9092773795 0.9069439173 - 0.9184722900 0.9097434878 0.9074594975 - 0.9189072847 0.9102094769 0.9079750180 - 0.9193422198 0.9106755853 0.9084905982 - 0.9197770953 0.9111416936 0.9090061188 - 0.9202072024 0.9116048217 0.9095147848 - 0.9206364155 0.9120672941 0.9100224972 - 0.9210656285 0.9125298262 0.9105300903 - 0.9214947224 0.9129922986 0.9110376835 - 0.9219238758 0.9134547114 0.9115452766 - 0.9223530889 0.9139171839 0.9120529294 - 0.9227808118 0.9143785834 0.9125580192 - 0.9232053757 0.9148374200 0.9130582809 - 0.9236298800 0.9152963161 0.9135584831 - 0.9240545034 0.9157550931 0.9140588045 - 0.9244791269 0.9162139893 0.9145590067 - 0.9249035716 0.9166728854 0.9150593281 - 0.9253281951 0.9171317220 0.9155594707 - 0.9257494211 0.9175868034 0.9160538912 - 0.9261695147 0.9180405736 0.9165465832 - 0.9265896082 0.9184945226 0.9170392156 - 0.9270097017 0.9189482927 0.9175319076 - 0.9274297953 0.9194021821 0.9180244803 - 0.9278498888 0.9198560119 0.9185171127 - 0.9282693267 0.9203091860 0.9190087914 - 0.9286841154 0.9207581282 0.9194943905 - 0.9290989041 0.9212068915 0.9199801087 - 0.9295135736 0.9216557145 0.9204657078 - 0.9299284220 0.9221044779 0.9209513068 - 0.9303432107 0.9225534201 0.9214370251 - 0.9307579994 0.9230021834 0.9219226241 - 0.9311705828 0.9234492183 0.9224053025 - 0.9315806031 0.9238939285 0.9228845239 - 0.9319906235 0.9243385792 0.9233636856 - 0.9324005842 0.9247832894 0.9238429070 - 0.9328106046 0.9252279997 0.9243221283 - 0.9332206249 0.9256727099 0.9248012900 - 0.9336305857 0.9261174202 0.9252805114 - 0.9340372086 0.9265592098 0.9257540703 - 0.9344422817 0.9269996285 0.9262254834 - 0.9348474145 0.9274401069 0.9266968966 - 0.9352524877 0.9278805256 0.9271683097 - 0.9356576204 0.9283210039 0.9276397228 - 0.9360626936 0.9287614226 0.9281110764 - 0.9364678264 0.9292019010 0.9285824895 - 0.9368677139 0.9296385050 0.9290475249 - 0.9372671247 0.9300746918 0.9295117855 - 0.9376664162 0.9305108786 0.9299762249 - 0.9380658269 0.9309471250 0.9304406047 - 0.9384651780 0.9313833117 0.9309049845 - 0.9388645291 0.9318194985 0.9313693047 - 0.9392635822 0.9322556257 0.9318333268 - 0.9396575093 0.9326885939 0.9322912097 - 0.9400513768 0.9331216216 0.9327492118 - 0.9404454231 0.9335545897 0.9332070947 - 0.9408392906 0.9339876175 0.9336650968 - 0.9412332177 0.9344205856 0.9341230989 - 0.9416270852 0.9348536134 0.9345809817 - 0.9420204759 0.9352859855 0.9350377917 - 0.9424101710 0.9357147813 0.9354882240 - 0.9427999854 0.9361435771 0.9359384775 - 0.9431897998 0.9365724921 0.9363889098 - 0.9435796142 0.9370012879 0.9368392229 - 0.9439694285 0.9374300838 0.9372895956 - 0.9443591833 0.9378589988 0.9377400279 - 0.9447481036 0.9382869005 0.9381886721 - 0.9451332092 0.9387105107 0.9386302829 - 0.9455183744 0.9391341209 0.9390718937 - 0.9459034801 0.9395576715 0.9395135045 - 0.9462885857 0.9399812222 0.9399551153 - 0.9466736913 0.9404047728 0.9403967261 - 0.9470589161 0.9408283830 0.9408382773 - 0.9474430084 0.9412512183 0.9412782192 - 0.9478216767 0.9416697025 0.9417101145 - 0.9482004046 0.9420881867 0.9421418905 - 0.9485790730 0.9425066710 0.9425737858 - 0.9489578009 0.9429252148 0.9430056810 - 0.9493365288 0.9433436990 0.9434375167 - 0.9497151971 0.9437621832 0.9438694119 - 0.9500933290 0.9441801906 0.9443004727 - 0.9504646063 0.9445917010 0.9447228909 - 0.9508358240 0.9450030923 0.9451454282 - 0.9512069821 0.9454146028 0.9455677867 - 0.9515783191 0.9458261132 0.9459903240 - 0.9519494772 0.9462375045 0.9464128017 - 0.9523206949 0.9466490149 0.9468352199 - 0.9526919723 0.9470605254 0.9472576976 - 0.9530565739 0.9474642873 0.9476736784 - 0.9534208179 0.9478675127 0.9480893016 - 0.9537848830 0.9482707977 0.9485049844 - 0.9541491270 0.9486740828 0.9489206076 - 0.9545133114 0.9490773082 0.9493361712 - 0.9548773766 0.9494805932 0.9497517943 - 0.9552416205 0.9498838782 0.9501674175 - 0.9556018114 0.9502828717 0.9505789876 - 0.9559605122 0.9506804943 0.9509891272 - 0.9563192129 0.9510779977 0.9513992071 - 0.9566779137 0.9514756203 0.9518092871 - 0.9570366740 0.9518731236 0.9522194266 - 0.9573953748 0.9522706866 0.9526295066 - 0.9577540755 0.9526681900 0.9530395865 - 0.9581105709 0.9530637264 0.9534469247 - 0.9584643841 0.9534568191 0.9538511038 - 0.9588183165 0.9538499117 0.9542552829 - 0.9591721296 0.9542430043 0.9546595812 - 0.9595260024 0.9546362162 0.9550638199 - 0.9598798752 0.9550293088 0.9554679990 - 0.9602336884 0.9554224014 0.9558722973 - 0.9605869055 0.9558148980 0.9562754035 - 0.9609360099 0.9562035203 0.9566727877 - 0.9612851143 0.9565922022 0.9570701122 - 0.9616342187 0.9569808245 0.9574674964 - 0.9619833827 0.9573695064 0.9578648806 - 0.9623324871 0.9577581882 0.9582623243 - 0.9626815915 0.9581468105 0.9586595893 - 0.9630306959 0.9585354924 0.9590569735 - 0.9633762240 0.9589205980 0.9594489932 - 0.9637206793 0.9593046904 0.9598394036 - 0.9640650749 0.9596887827 0.9602298141 - 0.9644095898 0.9600728750 0.9606202841 - 0.9647539854 0.9604570270 0.9610106945 - 0.9650985003 0.9608411193 0.9614012241 - 0.9654428959 0.9612252116 0.9617915750 - 0.9657856226 0.9616078138 0.9621797204 - 0.9661247730 0.9619873166 0.9625632167 - 0.9664641023 0.9623668194 0.9629465938 - 0.9668033123 0.9627463222 0.9633300900 - 0.9671425223 0.9631258249 0.9637135267 - 0.9674816728 0.9635053277 0.9640970230 - 0.9678210020 0.9638847113 0.9644804001 - 0.9681602120 0.9642642140 0.9648638964 - 0.9684944749 0.9646391869 0.9652413726 - 0.9688279033 0.9650132060 0.9656177759 - 0.9691612124 0.9653872252 0.9659941792 - 0.9694945216 0.9657611847 0.9663705826 - 0.9698278904 0.9661352038 0.9667469859 - 0.9701611996 0.9665092230 0.9671233892 - 0.9704945087 0.9668831825 0.9674997926 - 0.9708263874 0.9672555923 0.9678742290 - 0.9711539745 0.9676234126 0.9682434201 - 0.9714816213 0.9679911137 0.9686124921 - 0.9718093276 0.9683588743 0.9689816833 - 0.9721369147 0.9687265754 0.9693508148 - 0.9724645019 0.9690943956 0.9697200060 - 0.9727920890 0.9694622159 0.9700891972 - 0.9731197953 0.9698299170 0.9704583287 - 0.9734438062 0.9701938033 0.9708225727 - 0.9737656713 0.9705553055 0.9711840749 - 0.9740875959 0.9709169269 0.9715456963 - 0.9744095802 0.9712784290 0.9719071984 - 0.9747315049 0.9716399908 0.9722688198 - 0.9750534296 0.9720014930 0.9726303220 - 0.9753754139 0.9723631144 0.9729918838 - 0.9756972790 0.9727246165 0.9733533859 - 0.9760133028 0.9730800986 0.9737070799 - 0.9763289094 0.9734352231 0.9740604162 - 0.9766445756 0.9737904072 0.9744136930 - 0.9769601822 0.9741454720 0.9747670293 - 0.9772757888 0.9745005965 0.9751201868 - 0.9775915146 0.9748557210 0.9754735231 - 0.9779071212 0.9752107859 0.9758267999 - 0.9782214761 0.9755647182 0.9761784077 - 0.9785307050 0.9759128094 0.9765232205 - 0.9788398147 0.9762609005 0.9768680930 - 0.9791489244 0.9766091108 0.9772129059 - 0.9794579744 0.9769572020 0.9775577188 - 0.9797672033 0.9773054123 0.9779024720 - 0.9800763130 0.9776535034 0.9782472849 - 0.9803854227 0.9780015945 0.9785920978 - 0.9806922078 0.9783468843 0.9789335728 - 0.9809947014 0.9786871076 0.9792693257 - 0.9812973142 0.9790272117 0.9796050191 - 0.9815999269 0.9793673754 0.9799407721 - 0.9819024801 0.9797075987 0.9802765250 - 0.9822049737 0.9800478220 0.9806122184 - 0.9825075865 0.9803879857 0.9809479117 - 0.9828101993 0.9807282090 0.9812836051 - 0.9831092954 0.9810643196 0.9816148877 - 0.9834046960 0.9813960195 0.9819414020 - 0.9837002158 0.9817277789 0.9822679162 - 0.9839956164 0.9820595980 0.9825943708 - 0.9842910171 0.9823912978 0.9829208851 - 0.9845864177 0.9827231169 0.9832475185 - 0.9848818183 0.9830548167 0.9835739732 - 0.9851772189 0.9833865762 0.9839004874 - 0.9854683280 0.9837135077 0.9842218161 - 0.9857556820 0.9840362072 0.9845384955 - 0.9860429764 0.9843589067 0.9848552942 - 0.9863303900 0.9846814871 0.9851719737 - 0.9866176844 0.9850041866 0.9854887724 - 0.9869050980 0.9853268862 0.9858055115 - 0.9871923923 0.9856495261 0.9861223102 - 0.9874798059 0.9859722257 0.9864389896 - 0.9877628088 0.9862895012 0.9867501855 - 0.9880418181 0.9866018295 0.9870560765 - 0.9883208275 0.9869140983 0.9873620272 - 0.9885997772 0.9872264266 0.9876679182 - 0.9888787866 0.9875386953 0.9879738092 - 0.9891577959 0.9878510237 0.9882797003 - 0.9894368052 0.9881632924 0.9885855913 - 0.9897158146 0.9884756207 0.9888914824 - 0.9899904728 0.9887828231 0.9891915917 - 0.9902597070 0.9890834093 0.9894841909 - 0.9905288815 0.9893839955 0.9897767901 - 0.9907981157 0.9896845818 0.9900693893 - 0.9910672903 0.9899852276 0.9903619885 - 0.9913365245 0.9902858138 0.9906545877 - 0.9916056991 0.9905864000 0.9909471869 - 0.9918748736 0.9908869863 0.9912399054 - 0.9921408892 0.9911842942 0.9915285110 - 0.9923989177 0.9914734960 0.9918072224 - 0.9926568866 0.9917625785 0.9920858741 - 0.9929149151 0.9920517802 0.9923645854 - 0.9931728840 0.9923409820 0.9926434159 - 0.9934309125 0.9926301241 0.9929221272 - 0.9936888814 0.9929193258 0.9932007790 - 0.9939469099 0.9932084084 0.9934794903 - 0.9942042232 0.9934967756 0.9937573075 - 0.9944509864 0.9937735200 0.9940230846 - 0.9946978092 0.9940503240 0.9942888021 - 0.9949445724 0.9943270087 0.9945545793 - 0.9951913953 0.9946038127 0.9948202968 - 0.9954382181 0.9948804975 0.9950860739 - 0.9956849813 0.9951573014 0.9953517914 - 0.9959318042 0.9954339862 0.9956176281 - 0.9961786866 0.9957107902 0.9958834052 - 0.9964144826 0.9959757924 0.9961376190 - 0.9966472983 0.9962375760 0.9963884950 - 0.9968799949 0.9964993000 0.9966394901 - 0.9971128106 0.9967610240 0.9968904257 - 0.9973455071 0.9970228076 0.9971414208 - 0.9975783229 0.9972844720 0.9973922968 - 0.9978110194 0.9975461960 0.9976432920 - 0.9980437756 0.9978079200 0.9978942275 - 0.9982696176 0.9980614185 0.9981374741 - 0.9984859228 0.9983037710 0.9983702898 - 0.9987022281 0.9985461235 0.9986031055 - 0.9989184737 0.9987884164 0.9988359213 - 0.9991347790 0.9990307093 0.9990686774 - 0.9993510842 0.9992730021 0.9993016124 - 0.9995673895 0.9995154142 0.9995344281 - 0.9997836947 0.9997577071 0.9997671843 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0089918738 0.0088112270 0.0067409412 + 0.0171655007 0.0167162009 0.0131348101 + 0.0242981501 0.0235243607 0.0187877901 + 0.0308058802 0.0297358502 0.0239518695 + 0.0368227884 0.0355986990 0.0288143605 + 0.0424588397 0.0412245393 0.0334390104 + 0.0477601402 0.0466648601 0.0378785282 + 0.0527938083 0.0519550890 0.0421676189 + 0.0575832911 0.0571624711 0.0463436693 + 0.0622247718 0.0623087510 0.0504317991 + 0.0668161213 0.0673468485 0.0544279404 + 0.0713887885 0.0722417235 0.0583436601 + 0.0759282932 0.0769878626 0.0621980987 + 0.0804320127 0.0816073194 0.0659957901 + 0.0848805234 0.0860867798 0.0697494522 + 0.0892914683 0.0904396772 0.0734607279 + 0.0936586633 0.0946798995 0.0771252811 + 0.0979755297 0.0988209695 0.0807478875 + 0.1022336036 0.1028660983 0.0843302682 + 0.1064319983 0.1068392023 0.0878701210 + 0.1105559021 0.1107354984 0.0913715884 + 0.1146129966 0.1145422980 0.0948381424 + 0.1185963973 0.1182733998 0.0982583165 + 0.1225240976 0.1219324991 0.1016350016 + 0.1263933927 0.1255291998 0.1049719974 + 0.1301935017 0.1290597022 0.1082687974 + 0.1339209974 0.1325256974 0.1115282029 + 0.1375810951 0.1359412074 0.1147558987 + 0.1411647946 0.1393028945 0.1179407015 + 0.1446768939 0.1426129043 0.1210891977 + 0.1481269002 0.1458766013 0.1242009997 + 0.1515025944 0.1490920037 0.1272723973 + 0.1548155993 0.1522623003 0.1303056926 + 0.1580781043 0.1553830951 0.1333058029 + 0.1612831950 0.1584683955 0.1362698972 + 0.1644275039 0.1615206003 0.1391921043 + 0.1675149053 0.1645296961 0.1420771927 + 0.1705511063 0.1675007045 0.1449283957 + 0.1735346019 0.1704356074 0.1477468014 + 0.1764681935 0.1733424962 0.1505275071 + 0.1793411970 0.1762108952 0.1532687992 + 0.1821642965 0.1790487021 0.1559778005 + 0.1849396974 0.1818661988 0.1586564928 + 0.1876723021 0.1846542954 0.1613045037 + 0.1903574020 0.1874137968 0.1639243066 + 0.1929987073 0.1901445985 0.1665160060 + 0.1956039965 0.1928485036 0.1690804064 + 0.1981741041 0.1955288947 0.1716223955 + 0.2007140964 0.1981831044 0.1741400957 + 0.2032240033 0.2008188963 0.1766290963 + 0.2057044953 0.2034270018 0.1790864021 + 0.2081623971 0.2060122043 0.1815200001 + 0.2106028944 0.2085773945 0.1839299053 + 0.2130292058 0.2111182064 0.1863144040 + 0.2154411972 0.2136391997 0.1886768937 + 0.2178471982 0.2161431015 0.1910151988 + 0.2202418000 0.2186270058 0.1933256984 + 0.2226282060 0.2210904956 0.1956081986 + 0.2250116020 0.2235327959 0.1978694052 + 0.2273910940 0.2259615064 0.2001121938 + 0.2297631949 0.2283712029 0.2023327053 + 0.2321317047 0.2307610959 0.2045315057 + 0.2344924062 0.2331354022 0.2067105025 + 0.2368433028 0.2354861945 0.2088731974 + 0.2391863018 0.2378192991 0.2110181004 + 0.2415205985 0.2401355058 0.2131436020 + 0.2438464016 0.2424366027 0.2152539939 + 0.2461618036 0.2447226048 0.2173456997 + 0.2484730929 0.2469923943 0.2194202989 + 0.2507739067 0.2492519021 0.2214788944 + 0.2530598044 0.2514967918 0.2235199958 + 0.2553376853 0.2537238002 0.2255467027 + 0.2576029897 0.2559401989 0.2275560051 + 0.2598524094 0.2581427991 0.2295472026 + 0.2620919049 0.2603318095 0.2315239012 + 0.2643215060 0.2625105977 0.2334856987 + 0.2665401995 0.2646790147 0.2354328036 + 0.2687489986 0.2668366134 0.2373681068 + 0.2709451020 0.2689833045 0.2392894030 + 0.2731316984 0.2711197138 0.2411931008 + 0.2753098011 0.2732459903 0.2430835962 + 0.2774744034 0.2753610909 0.2449620068 + 0.2796306908 0.2774659097 0.2468270957 + 0.2817795873 0.2795613110 0.2486820072 + 0.2839165926 0.2816452086 0.2505275905 + 0.2860453129 0.2837195992 0.2523604929 + 0.2881659865 0.2857829928 0.2541792989 + 0.2902776897 0.2878372073 0.2559899092 + 0.2923803031 0.2898777127 0.2577917874 + 0.2944732904 0.2919048965 0.2595866024 + 0.2965568006 0.2939237952 0.2613756955 + 0.2986293137 0.2959322035 0.2631560862 + 0.3006938994 0.2979266047 0.2649283111 + 0.3027508855 0.2999104857 0.2666937113 + 0.3047966063 0.3018867075 0.2684526145 + 0.3068324924 0.3038511872 0.2702021897 + 0.3088631034 0.3058032095 0.2719438076 + 0.3108848929 0.3077456951 0.2736783028 + 0.3128944933 0.3096817136 0.2754063010 + 0.3148933947 0.3116078973 0.2771281898 + 0.3168857992 0.3135224879 0.2788479924 + 0.3188689053 0.3154281080 0.2805649042 + 0.3208411932 0.3173280954 0.2822758853 + 0.3228051066 0.3192209005 0.2839803994 + 0.3247616887 0.3211036026 0.2856811881 + 0.3267089128 0.3229758143 0.2873789072 + 0.3286511004 0.3248389959 0.2890717983 + 0.3305900991 0.3266923130 0.2907603085 + 0.3325229883 0.3285365999 0.2924445868 + 0.3344469070 0.3303725123 0.2941254079 + 0.3363626897 0.3322010040 0.2958011925 + 0.3382742107 0.3340212107 0.2974714935 + 0.3401803970 0.3358336091 0.2991386950 + 0.3420785964 0.3376361132 0.3008036017 + 0.3439661860 0.3394295871 0.3024663031 + 0.3458459079 0.3412173986 0.3041245043 + 0.3477211893 0.3429979980 0.3057765961 + 0.3495895863 0.3447695971 0.3074248135 + 0.3514491022 0.3465310931 0.3090695143 + 0.3533000052 0.3482857049 0.3107109964 + 0.3551431000 0.3500339985 0.3123489022 + 0.3569791913 0.3517752886 0.3139818907 + 0.3588086069 0.3535098135 0.3156122863 + 0.3606314957 0.3552359939 0.3172419071 + 0.3624485135 0.3569523096 0.3188686073 + 0.3642578125 0.3586593866 0.3204928041 + 0.3660599887 0.3603613079 0.3221142888 + 0.3678553998 0.3620558083 0.3237330914 + 0.3696443141 0.3637424111 0.3253495991 + 0.3714273870 0.3654215038 0.3269616067 + 0.3732031882 0.3670935929 0.3285692930 + 0.3749721944 0.3687596917 0.3301742971 + 0.3767344952 0.3704194129 0.3317768872 + 0.3784905076 0.3720717132 0.3333773017 + 0.3802404106 0.3737153113 0.3349755108 + 0.3819842935 0.3753502071 0.3365707099 + 0.3837207854 0.3769770861 0.3381618857 + 0.3854480982 0.3785960078 0.3397505879 + 0.3871636093 0.3802081048 0.3413363993 + 0.3888713121 0.3818149865 0.3429175913 + 0.3905732930 0.3834156990 0.3444958925 + 0.3922694027 0.3850092888 0.3460716903 + 0.3939583898 0.3865957856 0.3476448953 + 0.3956395090 0.3881759942 0.3492155969 + 0.3973124027 0.3897528052 0.3507837951 + 0.3989773989 0.3913241029 0.3523463905 + 0.4006344974 0.3928903043 0.3539063931 + 0.4022842050 0.3944503963 0.3554647863 + 0.4039269090 0.3960036933 0.3570212126 + 0.4055627882 0.3975501060 0.3585749865 + 0.4071914852 0.3990882933 0.3601273000 + 0.4088127911 0.4006206095 0.3616780043 + 0.4104264975 0.4021480083 0.3632257879 + 0.4120318890 0.4036706090 0.3647710979 + 0.4136281908 0.4051879048 0.3663145900 + 0.4152132869 0.4066968858 0.3678534031 + 0.4167909920 0.4081991017 0.3693884909 + 0.4183610082 0.4096961021 0.3709208965 + 0.4199230969 0.4111889005 0.3724510074 + 0.4214732051 0.4126752913 0.3739764094 + 0.4230127037 0.4141556919 0.3754990995 + 0.4245432913 0.4156309962 0.3770196140 + 0.4260652065 0.4171015918 0.3785375953 + 0.4275765121 0.4185652137 0.3800503016 + 0.4290784895 0.4200240970 0.3815599978 + 0.4305722117 0.4214783013 0.3830671012 + 0.4320571125 0.4229275882 0.3845714033 + 0.4335275888 0.4243701994 0.3860712945 + 0.4349885881 0.4258084893 0.3875694871 + 0.4364405870 0.4272427857 0.3890654147 + 0.4378808141 0.4286721051 0.3905574977 + 0.4393113852 0.4300973117 0.3920451999 + 0.4407345057 0.4315193892 0.3935315013 + 0.4421485960 0.4329378009 0.3950169981 + 0.4435501993 0.4343496859 0.3964990079 + 0.4449434876 0.4357573986 0.3979789019 + 0.4463286996 0.4371610880 0.3994567990 + 0.4477010071 0.4385577142 0.4009310007 + 0.4490672052 0.4399507046 0.4024015069 + 0.4504272938 0.4413402975 0.4038689137 + 0.4517762065 0.4427213967 0.4053323865 + 0.4531182945 0.4440990090 0.4067929983 + 0.4544540942 0.4454734921 0.4082511961 + 0.4557775855 0.4468427896 0.4097048938 + 0.4570952952 0.4482089877 0.4111567140 + 0.4584074914 0.4495720863 0.4126070142 + 0.4597108960 0.4509283006 0.4140538871 + 0.4610100091 0.4522809982 0.4154989123 + 0.4623034894 0.4536291957 0.4169419110 + 0.4635887146 0.4549719095 0.4183813035 + 0.4648694992 0.4563120902 0.4198183119 + 0.4661431015 0.4576481879 0.4212512076 + 0.4674094021 0.4589804113 0.4226809144 + 0.4686715007 0.4603103101 0.4241088927 + 0.4699237943 0.4616335034 0.4255332053 + 0.4711718857 0.4629541039 0.4269537926 + 0.4724155962 0.4642721117 0.4283713996 + 0.4736534059 0.4655844867 0.4297845066 + 0.4748887122 0.4668945074 0.4311957955 + 0.4761190116 0.4681994021 0.4326032996 + 0.4773451984 0.4694997966 0.4340074062 + 0.4785684049 0.4707967937 0.4354093969 + 0.4797854125 0.4720880985 0.4368061125 + 0.4810002148 0.4733774066 0.4382002950 + 0.4822092950 0.4746614993 0.4395891130 + 0.4834147990 0.4759416878 0.4409742951 + 0.4846169055 0.4772185087 0.4423570931 + 0.4858146906 0.4784907103 0.4437364042 + 0.4870111048 0.4797612131 0.4451141953 + 0.4882034063 0.4810256064 0.4464867115 + 0.4893937111 0.4822880924 0.4478574991 + 0.4905807078 0.4835473895 0.4492256939 + 0.4917649031 0.4848031998 0.4505923092 + 0.4929473102 0.4860568047 0.4519579113 + 0.4941253960 0.4873049855 0.4533194900 + 0.4953022897 0.4885519147 0.4546799958 + 0.4964750111 0.4897922873 0.4560363889 + 0.4976463020 0.4910312891 0.4573906064 + 0.4988138080 0.4922665060 0.4587404132 + 0.4999791980 0.4934988022 0.4600867033 + 0.5011422038 0.4947274923 0.4614296854 + 0.5023019910 0.4959532917 0.4627693892 + 0.5034598708 0.4971773922 0.4641076028 + 0.5046142936 0.4983972907 0.4654398859 + 0.5057677031 0.4996159971 0.4667710960 + 0.5069164038 0.5008293986 0.4680955112 + 0.5080643892 0.5020418763 0.4694189131 + 0.5092083812 0.5032485127 0.4707360864 + 0.5103514194 0.5044537783 0.4720517099 + 0.5114902854 0.5056533813 0.4733619988 + 0.5126280189 0.5068514943 0.4746705890 + 0.5137624741 0.5080453753 0.4759756923 + 0.5148952007 0.5092371702 0.4772787094 + 0.5160247087 0.5104253888 0.4785786867 + 0.5171523094 0.5116108060 0.4798755050 + 0.5182775259 0.5127922893 0.4811685979 + 0.5194002986 0.5139706731 0.4824582040 + 0.5205205083 0.5151454806 0.4837445021 + 0.5216385126 0.5163174272 0.4850262105 + 0.5227543712 0.5174863935 0.4863041043 + 0.5238677263 0.5186526775 0.4875771999 + 0.5249785781 0.5198165774 0.4888466895 + 0.5260874033 0.5209770203 0.4901134968 + 0.5271940231 0.5221341848 0.4913783967 + 0.5282983184 0.5232884884 0.4926396906 + 0.5294001102 0.5244398713 0.4938982129 + 0.5304998755 0.5255879164 0.4951525033 + 0.5315973759 0.5267319083 0.4964028895 + 0.5326929092 0.5278732777 0.4976496994 + 0.5337855816 0.5290107727 0.4988923073 + 0.5348768830 0.5301455259 0.5001319051 + 0.5359653831 0.5312750936 0.5013670921 + 0.5370525718 0.5324026942 0.5025998950 + 0.5381363034 0.5335251093 0.5038275123 + 0.5392193198 0.5346459746 0.5050536990 + 0.5402988195 0.5357596874 0.5062742829 + 0.5413780808 0.5368732214 0.5074942708 + 0.5424528718 0.5379806757 0.5087080002 + 0.5435277224 0.5390880704 0.5099217296 + 0.5445986986 0.5401890874 0.5111290216 + 0.5456693172 0.5412895083 0.5123360753 + 0.5467364192 0.5423848033 0.5135380030 + 0.5478023291 0.5434783101 0.5147387981 + 0.5488657951 0.5445675254 0.5159347057 + 0.5499275923 0.5456534028 0.5171278119 + 0.5509874821 0.5467371941 0.5183176994 + 0.5520446897 0.5478175282 0.5195037723 + 0.5531010032 0.5488967896 0.5206878185 + 0.5541540980 0.5499712825 0.5218651891 + 0.5552070737 0.5510457754 0.5230425000 + 0.5562556982 0.5521146059 0.5242115855 + 0.5573040843 0.5531831980 0.5253806710 + 0.5583497286 0.5542473793 0.5265437961 + 0.5593940020 0.5553098917 0.5277050734 + 0.5604363084 0.5563696027 0.5288618803 + 0.5614761114 0.5574259162 0.5300140977 + 0.5625151992 0.5584810972 0.5311648846 + 0.5635510087 0.5595309734 0.5323101282 + 0.5645866990 0.5605809093 0.5334553123 + 0.5656185150 0.5616251826 0.5345945954 + 0.5666496754 0.5626686215 0.5357334018 + 0.5676786900 0.5637084246 0.5368676782 + 0.5687059760 0.5647451878 0.5379989743 + 0.5697323084 0.5657805800 0.5391280055 + 0.5707550049 0.5668106079 0.5402505994 + 0.5717777014 0.5678406954 0.5413731933 + 0.5727970004 0.5688648820 0.5424870849 + 0.5738158226 0.5698879957 0.5435999036 + 0.5748323798 0.5709078908 0.5447078943 + 0.5758466721 0.5719239712 0.5458114147 + 0.5768607259 0.5729396939 0.5469142199 + 0.5778710842 0.5739501119 0.5480110049 + 0.5788813829 0.5749605894 0.5491077900 + 0.5798888803 0.5759674907 0.5501992106 + 0.5808948278 0.5769727230 0.5512881875 + 0.5818998814 0.5779765844 0.5523753166 + 0.5829018950 0.5789757967 0.5534570813 + 0.5839039087 0.5799751282 0.5545389056 + 0.5849025846 0.5809702873 0.5556156039 + 0.5859003067 0.5819640756 0.5566909909 + 0.5868967772 0.5829563141 0.5577639937 + 0.5878906250 0.5839440823 0.5588322282 + 0.5888844132 0.5849320292 0.5599002838 + 0.5898746848 0.5859153867 0.5609617233 + 0.5908641219 0.5868976116 0.5620216727 + 0.5918524861 0.5878787041 0.5630789995 + 0.5928381085 0.5888563991 0.5641300082 + 0.5938236713 0.5898339748 0.5651810169 + 0.5948061943 0.5908092260 0.5662257075 + 0.5957874060 0.5917832851 0.5672683120 + 0.5967680812 0.5927566886 0.5683094859 + 0.5977455974 0.5937262774 0.5693448782 + 0.5987229943 0.5946959257 0.5703802705 + 0.5996980071 0.5956636071 0.5714107752 + 0.6006711721 0.5966296196 0.5724381804 + 0.6016442776 0.5975956917 0.5734654069 + 0.6026139259 0.5985574722 0.5744841099 + 0.6035832763 0.5995190740 0.5755028725 + 0.6045511961 0.6004796028 0.5765179992 + 0.6055163741 0.6014376879 0.5775272250 + 0.6064814925 0.6023957729 0.5785365105 + 0.6074441075 0.6033508778 0.5795394778 + 0.6084054708 0.6043046117 0.5805402994 + 0.6093667746 0.6052582264 0.5815405846 + 0.6103240252 0.6062085032 0.5825338960 + 0.6112812161 0.6071587205 0.5835272074 + 0.6122370958 0.6081075072 0.5845175982 + 0.6131905913 0.6090533137 0.5855029821 + 0.6141440868 0.6099991798 0.5864884853 + 0.6150950193 0.6109429002 0.5874683857 + 0.6160442829 0.6118853092 0.5884453058 + 0.6169936061 0.6128277183 0.5894222260 + 0.6179397106 0.6137663126 0.5903909206 + 0.6188852787 0.6147043109 0.5913589001 + 0.6198304892 0.6156420112 0.5923253894 + 0.6207720041 0.6165766120 0.5932837725 + 0.6217133999 0.6175112128 0.5942422152 + 0.6226537228 0.6184443831 0.5951976180 + 0.6235914826 0.6193746924 0.5961477160 + 0.6245294213 0.6203050017 0.5970978737 + 0.6254649758 0.6212335825 0.5980439782 + 0.6263986826 0.6221604943 0.5989869237 + 0.6273323894 0.6230874062 0.5999299288 + 0.6282635927 0.6240113974 0.6008661985 + 0.6291937232 0.6249340177 0.6018002033 + 0.6301239133 0.6258566976 0.6027342081 + 0.6310505271 0.6267765164 0.6036599874 + 0.6319764853 0.6276957989 0.6045848131 + 0.6329025030 0.6286150813 0.6055094004 + 0.6338251233 0.6295301914 0.6064245105 + 0.6347475052 0.6304453015 0.6073396206 + 0.6356697083 0.6313601136 0.6082538962 + 0.6365880966 0.6322718263 0.6091607809 + 0.6375064254 0.6331834793 0.6100676060 + 0.6384242773 0.6340945959 0.6109727025 + 0.6393392086 0.6350020766 0.6118695140 + 0.6402540207 0.6359096169 0.6127663255 + 0.6411681175 0.6368162036 0.6136608124 + 0.6420788765 0.6377186179 0.6145474911 + 0.6429896951 0.6386210918 0.6154342294 + 0.6438996792 0.6395224929 0.6163187027 + 0.6448069811 0.6404194832 0.6171966195 + 0.6457142830 0.6413165927 0.6180744767 + 0.6466206908 0.6422126889 0.6189501882 + 0.6475239992 0.6431049109 0.6198194027 + 0.6484273076 0.6439971924 0.6206886172 + 0.6493299007 0.6448885202 0.6215558052 + 0.6502298117 0.6457754970 0.6224160790 + 0.6511296034 0.6466624737 0.6232764721 + 0.6520287991 0.6475486159 0.6241351962 + 0.6529247165 0.6484292150 0.6249868274 + 0.6538205147 0.6493096948 0.6258383989 + 0.6547160745 0.6501898170 0.6266890764 + 0.6556084752 0.6510639787 0.6275333762 + 0.6565008759 0.6519382000 0.6283777952 + 0.6573932767 0.6528124213 0.6292219758 + 0.6582819223 0.6536806226 0.6300593019 + 0.6591703296 0.6545485854 0.6308966279 + 0.6600587964 0.6554166079 0.6317338943 + 0.6609442830 0.6562796235 0.6325649023 + 0.6618294120 0.6571416855 0.6333950758 + 0.6627144217 0.6580038071 0.6342253089 + 0.6635968089 0.6588618159 0.6350504160 + 0.6644778848 0.6597180963 0.6358736753 + 0.6653590202 0.6605743766 0.6366969943 + 0.6662384272 0.6614277959 0.6375163794 + 0.6671161056 0.6622784734 0.6383327842 + 0.6679939032 0.6631292105 0.6391491294 + 0.6688703895 0.6639782190 0.6399635077 + 0.6697443128 0.6648234725 0.6407744288 + 0.6706181765 0.6656687856 0.6415852904 + 0.6714916825 0.6665136218 0.6423954964 + 0.6723623276 0.6673536897 0.6432008743 + 0.6732327938 0.6681938171 0.6440063715 + 0.6741033196 0.6690338850 0.6448118091 + 0.6749706268 0.6698697209 0.6456125975 + 0.6758372784 0.6707047820 0.6464126706 + 0.6767038703 0.6715397835 0.6472128034 + 0.6775687933 0.6723722219 0.6480097771 + 0.6784321070 0.6732022762 0.6488046050 + 0.6792954803 0.6740325093 0.6495993733 + 0.6801578999 0.6748616099 0.6503927112 + 0.6810173988 0.6756870151 0.6511822939 + 0.6818770170 0.6765124798 0.6519718170 + 0.6827365160 0.6773380041 0.6527613997 + 0.6835916042 0.6781579256 0.6535460949 + 0.6844462752 0.6789773107 0.6543303728 + 0.6853010058 0.6797965765 0.6551148295 + 0.6861529946 0.6806129813 0.6558970809 + 0.6870027184 0.6814268231 0.6566780210 + 0.6878523827 0.6822406054 0.6574587822 + 0.6887013912 0.6830536127 0.6582391262 + 0.6895464063 0.6838616729 0.6590166092 + 0.6903914213 0.6846699119 0.6597940922 + 0.6912363768 0.6854779720 0.6605716944 + 0.6920778751 0.6862823963 0.6613466740 + 0.6929180026 0.6870855093 0.6621211171 + 0.6937581897 0.6878886223 0.6628953815 + 0.6945971251 0.6886901855 0.6636688113 + 0.6954327226 0.6894879937 0.6644400954 + 0.6962683201 0.6902858019 0.6652113795 + 0.6971039176 0.6910836101 0.6659826040 + 0.6979358196 0.6918777227 0.6667515039 + 0.6987668276 0.6926708817 0.6675199270 + 0.6995977163 0.6934641004 0.6682882905 + 0.7004268765 0.6942554712 0.6690558195 + 0.7012518048 0.6950421929 0.6698213816 + 0.7020766735 0.6958289742 0.6705870032 + 0.7029017210 0.6966156960 0.6713526845 + 0.7037221789 0.6973984838 0.6721162796 + 0.7045413256 0.6981800199 0.6728792787 + 0.7053604126 0.6989613771 0.6736422777 + 0.7061784267 0.6997417808 0.6744047999 + 0.7069919109 0.7005174756 0.6751654148 + 0.7078052759 0.7012931108 0.6759259105 + 0.7086187005 0.7020686865 0.6766865253 + 0.7094289064 0.7028416991 0.6774455905 + 0.7102369070 0.7036126852 0.6782038808 + 0.7110447884 0.7043836713 0.6789621115 + 0.7118527889 0.7051547170 0.6797204018 + 0.7126556039 0.7059206963 0.6804764867 + 0.7134582996 0.7066864967 0.6812325716 + 0.7142608762 0.7074524164 0.6819887161 + 0.7150620222 0.7082164884 0.6827440858 + 0.7158594728 0.7089766860 0.6834982038 + 0.7166571021 0.7097368836 0.6842523217 + 0.7174546123 0.7104970813 0.6850063801 + 0.7182490826 0.7112538218 0.6857591867 + 0.7190417051 0.7120084167 0.6865115166 + 0.7198343277 0.7127628922 0.6872637272 + 0.7206268907 0.7135174870 0.6880158782 + 0.7214152217 0.7142676115 0.6887664795 + 0.7222030163 0.7150173187 0.6895170808 + 0.7229908109 0.7157669067 0.6902676225 + 0.7237780094 0.7165157795 0.6910178065 + 0.7245612144 0.7172604799 0.6917667985 + 0.7253444791 0.7180052996 0.6925156713 + 0.7261276841 0.7187500000 0.6932647228 + 0.7269088030 0.7194930911 0.6940131187 + 0.7276861072 0.7202336192 0.6947606206 + 0.7284634709 0.7209740877 0.6955081820 + 0.7292407751 0.7217146754 0.6962556839 + 0.7300150990 0.7224528193 0.6970024705 + 0.7307869792 0.7231891155 0.6977487803 + 0.7315589190 0.7239254117 0.6984950900 + 0.7323307991 0.7246617079 0.6992413998 + 0.7330988050 0.7253944278 0.6999868155 + 0.7338652015 0.7261255980 0.7007319927 + 0.7346317172 0.7268568277 0.7014772296 + 0.7353981137 0.7275879979 0.7022224069 + 0.7361603975 0.7283154130 0.7029678822 + 0.7369217873 0.7290419936 0.7037134767 + 0.7376831770 0.7297685742 0.7044590712 + 0.7384446859 0.7304952145 0.7052047849 + 0.7392014265 0.7312179208 0.7059507966 + 0.7399578094 0.7319403291 0.7066968083 + 0.7407141924 0.7326626778 0.7074428201 + 0.7414705753 0.7333850861 0.7081888914 + 0.7422224879 0.7341023088 0.7089354992 + 0.7429742813 0.7348194718 0.7096819878 + 0.7437260747 0.7355365753 0.7104285955 + 0.7444778085 0.7362536788 0.7111750841 + 0.7452235222 0.7369664907 0.7119207978 + 0.7459692955 0.7376793027 0.7126665115 + 0.7467150092 0.7383921146 0.7134122252 + 0.7474606037 0.7391046882 0.7141578197 + 0.7482010126 0.7398132086 0.7149025798 + 0.7489414215 0.7405217290 0.7156472802 + 0.7496817708 0.7412303090 0.7163920999 + 0.7504222989 0.7419388294 0.7171369195 + 0.7511572838 0.7426438928 0.7178807259 + 0.7518922091 0.7433488965 0.7186245918 + 0.7526270747 0.7440540195 0.7193683982 + 0.7533621192 0.7447590828 0.7201123238 + 0.7540926933 0.7454611063 0.7208564281 + 0.7548229098 0.7461627722 0.7216005921 + 0.7555531859 0.7468644977 0.7223448157 + 0.7562834024 0.7475662231 0.7230889201 + 0.7570096850 0.7482659221 0.7238321900 + 0.7577350736 0.7489652038 0.7245752811 + 0.7584605217 0.7496644258 0.7253183722 + 0.7591857910 0.7503635883 0.7260615230 + 0.7599074841 0.7510594130 0.7268037796 + 0.7606273293 0.7517535090 0.7275456786 + 0.7613471150 0.7524476051 0.7282875776 + 0.7620670199 0.7531417012 0.7290294766 + 0.7627841234 0.7538337111 0.7297707796 + 0.7634984851 0.7545235753 0.7305114269 + 0.7642129064 0.7552134991 0.7312520742 + 0.7649273872 0.7559034228 0.7319927216 + 0.7656403184 0.7565919757 0.7327328920 + 0.7663499117 0.7572777867 0.7334721088 + 0.7670595050 0.7579635978 0.7342113853 + 0.7677689791 0.7586494088 0.7349506021 + 0.7684782743 0.7593349814 0.7356896996 + 0.7691832185 0.7600160837 0.7364274859 + 0.7698879838 0.7606971860 0.7371652126 + 0.7705928087 0.7613782883 0.7379028797 + 0.7712976933 0.7620593905 0.7386407256 + 0.7719981074 0.7627363801 0.7393770814 + 0.7726973295 0.7634121776 0.7401133180 + 0.7733964920 0.7640879750 0.7408493757 + 0.7740957141 0.7647637129 0.7415854931 + 0.7747924924 0.7654371262 0.7423207760 + 0.7754864097 0.7661076188 0.7430552840 + 0.7761803865 0.7667781115 0.7437896729 + 0.7768744230 0.7674486041 0.7445241213 + 0.7775678039 0.7681186199 0.7452582717 + 0.7782570124 0.7687847018 0.7459908724 + 0.7789462805 0.7694507241 0.7467234731 + 0.7796354890 0.7701168060 0.7474560738 + 0.7803246975 0.7707828879 0.7481886744 + 0.7810096741 0.7714446783 0.7489197850 + 0.7816932201 0.7721049190 0.7496504784 + 0.7823767066 0.7727652192 0.7503811717 + 0.7830601931 0.7734254003 0.7511119246 + 0.7837420702 0.7740842104 0.7518419027 + 0.7844206095 0.7747399211 0.7525705099 + 0.7850990891 0.7753955722 0.7532991171 + 0.7857775092 0.7760512829 0.7540277839 + 0.7864559889 0.7767069936 0.7547563910 + 0.7871304154 0.7773585916 0.7554832101 + 0.7878041267 0.7780095935 0.7562096715 + 0.7884777784 0.7786604762 0.7569361925 + 0.7891514897 0.7793114185 0.7576625943 + 0.7898237109 0.7799605727 0.7583882809 + 0.7904931903 0.7806066871 0.7591124773 + 0.7911627889 0.7812528014 0.7598366737 + 0.7918323874 0.7818987966 0.7605609298 + 0.7925019264 0.7825449109 0.7612851858 + 0.7931671143 0.7831870914 0.7620074153 + 0.7938314080 0.7838284969 0.7627292275 + 0.7944955826 0.7844697833 0.7634510994 + 0.7951598167 0.7851111889 0.7641729116 + 0.7958226800 0.7857515812 0.7648941278 + 0.7964813113 0.7863892913 0.7656133771 + 0.7971398830 0.7870268822 0.7663328052 + 0.7977985740 0.7876644731 0.7670521140 + 0.7984572053 0.7883021832 0.7677714825 + 0.7991123796 0.7889375091 0.7684891224 + 0.7997658253 0.7895715833 0.7692058086 + 0.8004190922 0.7902057171 0.7699226141 + 0.8010724783 0.7908397913 0.7706393003 + 0.8017258048 0.7914739251 0.7713559866 + 0.8023747206 0.7921038866 0.7720699906 + 0.8030235171 0.7927339077 0.7727841139 + 0.8036723733 0.7933639288 0.7734981179 + 0.8043212295 0.7939940095 0.7742121220 + 0.8049684167 0.7946221232 0.7749251127 + 0.8056119084 0.7952463031 0.7756363153 + 0.8062555194 0.7958704829 0.7763475180 + 0.8068990111 0.7964947224 0.7770587206 + 0.8075426221 0.7971189022 0.7777698040 + 0.8081833124 0.7977399826 0.7784792185 + 0.8088219762 0.7983589172 0.7791873813 + 0.8094608188 0.7989779115 0.7798956037 + 0.8100996017 0.7995969057 0.7806038260 + 0.8107383251 0.8002158999 0.7813121080 + 0.8113725185 0.8008310199 0.7820175886 + 0.8120058179 0.8014454246 0.7827227712 + 0.8126389980 0.8020598292 0.7834278941 + 0.8132722974 0.8026741743 0.7841330767 + 0.8139054179 0.8032884002 0.7848380208 + 0.8145325184 0.8038988113 0.7855399847 + 0.8151596189 0.8045092821 0.7862420082 + 0.8157867193 0.8051196933 0.7869439125 + 0.8164138198 0.8057301044 0.7876458764 + 0.8170399070 0.8063399196 0.7883471251 + 0.8176615238 0.8069468737 0.7890458107 + 0.8182830811 0.8075540066 0.7897444963 + 0.8189046979 0.8081610203 0.7904431224 + 0.8195263743 0.8087679744 0.7911418080 + 0.8201465011 0.8093742728 0.7918393016 + 0.8207632899 0.8099786043 0.7925345898 + 0.8213800788 0.8105828762 0.7932298779 + 0.8219968081 0.8111872077 0.7939251065 + 0.8226135969 0.8117914796 0.7946203947 + 0.8232282996 0.8123940229 0.7953141928 + 0.8238397241 0.8129937053 0.7960060239 + 0.8244510293 0.8135933876 0.7966976762 + 0.8250623941 0.8141930103 0.7973895073 + 0.8256736994 0.8147926927 0.7980812788 + 0.8262823820 0.8153901100 0.7987713814 + 0.8268874884 0.8159844279 0.7994595766 + 0.8274924755 0.8165788054 0.8001477122 + 0.8280975223 0.8171731234 0.8008359075 + 0.8287026286 0.8177673817 0.8015239835 + 0.8293048739 0.8183599710 0.8022106290 + 0.8299033046 0.8189499974 0.8028950095 + 0.8305016160 0.8195400238 0.8035795093 + 0.8310999870 0.8201299906 0.8042638898 + 0.8316984177 0.8207200170 0.8049483895 + 0.8322945237 0.8213084936 0.8056312799 + 0.8328866959 0.8218945265 0.8063120246 + 0.8334789276 0.8224804997 0.8069925904 + 0.8340710998 0.8230664730 0.8076732755 + 0.8346632719 0.8236525059 0.8083540201 + 0.8352540135 0.8242375851 0.8090333939 + 0.8358410001 0.8248205781 0.8097102046 + 0.8364279270 0.8254035711 0.8103870153 + 0.8370147943 0.8259866238 0.8110637069 + 0.8376017213 0.8265694976 0.8117405176 + 0.8381876945 0.8271517754 0.8124166131 + 0.8387684822 0.8277295828 0.8130893707 + 0.8393492103 0.8283075094 0.8137621880 + 0.8399299979 0.8288853765 0.8144350052 + 0.8405107260 0.8294633031 0.8151078224 + 0.8410913944 0.8300412297 0.8157805800 + 0.8416661024 0.8306139112 0.8164492846 + 0.8422405720 0.8311864138 0.8171179891 + 0.8428151011 0.8317589760 0.8177868128 + 0.8433896899 0.8323314786 0.8184555173 + 0.8439642191 0.8329041004 0.8191242218 + 0.8445343971 0.8334730864 0.8197888136 + 0.8451033235 0.8340409994 0.8204522729 + 0.8456721902 0.8346089125 0.8211157918 + 0.8462411165 0.8351768255 0.8217793107 + 0.8468099833 0.8357446790 0.8224428296 + 0.8473765850 0.8363106847 0.8231034875 + 0.8479408026 0.8368747234 0.8237618208 + 0.8485050201 0.8374385834 0.8244199753 + 0.8490692973 0.8380026221 0.8250783086 + 0.8496335149 0.8385666013 0.8257365227 + 0.8501964808 0.8391299844 0.8263936043 + 0.8507544994 0.8396906853 0.8270465136 + 0.8513123989 0.8402515054 0.8276994228 + 0.8518704176 0.8408122063 0.8283522725 + 0.8524283171 0.8413730264 0.8290051818 + 0.8529862761 0.8419337273 0.8296582103 + 0.8535385728 0.8424912095 0.8303064108 + 0.8540899158 0.8430479765 0.8309540153 + 0.8546411991 0.8436049223 0.8316016197 + 0.8551924825 0.8441618085 0.8322492242 + 0.8557438254 0.8447186947 0.8328968287 + 0.8562924862 0.8452731967 0.8335418105 + 0.8568379879 0.8458250165 0.8341839910 + 0.8573834896 0.8463767767 0.8348261714 + 0.8579289913 0.8469285965 0.8354684114 + 0.8584744930 0.8474804163 0.8361105919 + 0.8590198755 0.8480321169 0.8367525935 + 0.8595604897 0.8485792279 0.8373904824 + 0.8601011038 0.8491263986 0.8380283117 + 0.8606415987 0.8496735096 0.8386662006 + 0.8611822128 0.8502206206 0.8393040895 + 0.8617228270 0.8507677913 0.8399419785 + 0.8622605205 0.8513128757 0.8405768275 + 0.8627955914 0.8518565297 0.8412091136 + 0.8633307815 0.8524000049 0.8418415189 + 0.8638659120 0.8529435992 0.8424739242 + 0.8644011021 0.8534870744 0.8431062102 + 0.8649361134 0.8540306091 0.8437383771 + 0.8654658198 0.8545699716 0.8443654180 + 0.8659955263 0.8551093936 0.8449923992 + 0.8665251732 0.8556488156 0.8456193805 + 0.8670548797 0.8561881781 0.8462464213 + 0.8675845861 0.8567274809 0.8468734026 + 0.8681117296 0.8572648168 0.8474978805 + 0.8686357141 0.8577994704 0.8481194973 + 0.8691598177 0.8583341241 0.8487411141 + 0.8696838021 0.8588687778 0.8493627906 + 0.8702077866 0.8594034910 0.8499844074 + 0.8707317710 0.8599380851 0.8506060243 + 0.8712515831 0.8604686856 0.8512223959 + 0.8717706203 0.8609983921 0.8518378139 + 0.8722895980 0.8615282178 0.8524531126 + 0.8728085756 0.8620579243 0.8530684710 + 0.8733276129 0.8625876904 0.8536838889 + 0.8738456964 0.8631166816 0.8542981148 + 0.8743596077 0.8636423945 0.8549072742 + 0.8748735189 0.8641679883 0.8555164933 + 0.8753873110 0.8646935821 0.8561257124 + 0.8759012222 0.8652192950 0.8567348719 + 0.8764150739 0.8657448888 0.8573440909 + 0.8769264817 0.8662691116 0.8579511046 + 0.8774347901 0.8667913079 0.8585553765 + 0.8779432178 0.8673136234 0.8591597080 + 0.8784515858 0.8678358793 0.8597639799 + 0.8789598942 0.8683581948 0.8603683710 + 0.8794683218 0.8688805103 0.8609727025 + 0.8799737096 0.8694002032 0.8615736961 + 0.8804777265 0.8699188232 0.8621733785 + 0.8809818029 0.8704373240 0.8627730012 + 0.8814858198 0.8709558845 0.8633726835 + 0.8819897771 0.8714743853 0.8639724255 + 0.8824939132 0.8719930053 0.8645719886 + 0.8829929233 0.8725072145 0.8651664853 + 0.8834912181 0.8730208278 0.8657605052 + 0.8839895725 0.8735345006 0.8663544059 + 0.8844879270 0.8740481138 0.8669483066 + 0.8849862218 0.8745617270 0.8675422072 + 0.8854843974 0.8750752807 0.8681359291 + 0.8859770298 0.8755844831 0.8687235117 + 0.8864694834 0.8760938048 0.8693109751 + 0.8869621158 0.8766030073 0.8698986173 + 0.8874546289 0.8771122098 0.8704861999 + 0.8879472017 0.8776214719 0.8710737228 + 0.8884391189 0.8781303167 0.8716604114 + 0.8889269233 0.8786363006 0.8722419143 + 0.8894146085 0.8791422844 0.8728232980 + 0.8899024129 0.8796483278 0.8734048009 + 0.8903900981 0.8801543117 0.8739861846 + 0.8908779025 0.8806601763 0.8745676875 + 0.8913648129 0.8811656237 0.8751479983 + 0.8918477297 0.8816679120 0.8757237196 + 0.8923305869 0.8821702003 0.8762993217 + 0.8928135037 0.8826724291 0.8768749833 + 0.8932964206 0.8831747174 0.8774505854 + 0.8937792778 0.8836770058 0.8780263066 + 0.8942611814 0.8841785789 0.8786008954 + 0.8947380781 0.8846768141 0.8791710138 + 0.8952149153 0.8851749897 0.8797410727 + 0.8956918120 0.8856732249 0.8803111911 + 0.8961687088 0.8861714005 0.8808813095 + 0.8966454864 0.8866695762 0.8814514279 + 0.8971219063 0.8871673942 0.8820207119 + 0.8975936174 0.8876612782 0.8825843930 + 0.8980653286 0.8881552815 0.8831480145 + 0.8985369802 0.8886492252 0.8837116957 + 0.8990086913 0.8891432285 0.8842753768 + 0.8994804025 0.8896371126 0.8848391175 + 0.8999521136 0.8901311159 0.8854026198 + 0.9004188776 0.8906217217 0.8859593272 + 0.9008855820 0.8911123276 0.8865159750 + 0.9013522863 0.8916028738 0.8870726228 + 0.9018189907 0.8920934796 0.8876292706 + 0.9022856951 0.8925840855 0.8881859779 + 0.9027525187 0.8930746913 0.8887426853 + 0.9032139182 0.8935613036 0.8892936707 + 0.9036744833 0.8940473199 0.8898438811 + 0.9041351080 0.8945332170 0.8903939724 + 0.9045956731 0.8950191736 0.8909441829 + 0.9050561786 0.8955051899 0.8914942741 + 0.9055168033 0.8959910870 0.8920444846 + 0.9059740901 0.8964738250 0.8925905228 + 0.9064297080 0.8969547153 0.8931344151 + 0.9068853259 0.8974357247 0.8936784267 + 0.9073408842 0.8979166150 0.8942223191 + 0.9077963829 0.8983975053 0.8947662711 + 0.9082520008 0.8988783956 0.8953102231 + 0.9087055922 0.8993577957 0.8958514929 + 0.9091560841 0.8998348713 0.8963887095 + 0.9096066952 0.9003120065 0.8969259858 + 0.9100571871 0.9007890224 0.8974633217 + 0.9105077982 0.9012660980 0.8980004787 + 0.9109582901 0.9017431736 0.8985378146 + 0.9114084840 0.9022198915 0.8990743160 + 0.9118540883 0.9026929140 0.8996040821 + 0.9122998118 0.9031658173 0.9001339078 + 0.9127454162 0.9036387801 0.9006636739 + 0.9131910801 0.9041116834 0.9011934996 + 0.9136366844 0.9045847058 0.9017233253 + 0.9140822887 0.9050576091 0.9022530913 + 0.9145244956 0.9055281878 0.9027777910 + 0.9149652719 0.9059978724 0.9033007026 + 0.9154061079 0.9064676166 0.9038236141 + 0.9158468843 0.9069373012 0.9043465257 + 0.9162877798 0.9074069262 0.9048693776 + 0.9167286158 0.9078766108 0.9053922892 + 0.9171674848 0.9083452225 0.9059128165 + 0.9176024795 0.9088112712 0.9064283967 + 0.9180374146 0.9092773795 0.9069439173 + 0.9184722900 0.9097434878 0.9074594975 + 0.9189072847 0.9102094769 0.9079750180 + 0.9193422198 0.9106755853 0.9084905982 + 0.9197770953 0.9111416936 0.9090061188 + 0.9202072024 0.9116048217 0.9095147848 + 0.9206364155 0.9120672941 0.9100224972 + 0.9210656285 0.9125298262 0.9105300903 + 0.9214947224 0.9129922986 0.9110376835 + 0.9219238758 0.9134547114 0.9115452766 + 0.9223530889 0.9139171839 0.9120529294 + 0.9227808118 0.9143785834 0.9125580192 + 0.9232053757 0.9148374200 0.9130582809 + 0.9236298800 0.9152963161 0.9135584831 + 0.9240545034 0.9157550931 0.9140588045 + 0.9244791269 0.9162139893 0.9145590067 + 0.9249035716 0.9166728854 0.9150593281 + 0.9253281951 0.9171317220 0.9155594707 + 0.9257494211 0.9175868034 0.9160538912 + 0.9261695147 0.9180405736 0.9165465832 + 0.9265896082 0.9184945226 0.9170392156 + 0.9270097017 0.9189482927 0.9175319076 + 0.9274297953 0.9194021821 0.9180244803 + 0.9278498888 0.9198560119 0.9185171127 + 0.9282693267 0.9203091860 0.9190087914 + 0.9286841154 0.9207581282 0.9194943905 + 0.9290989041 0.9212068915 0.9199801087 + 0.9295135736 0.9216557145 0.9204657078 + 0.9299284220 0.9221044779 0.9209513068 + 0.9303432107 0.9225534201 0.9214370251 + 0.9307579994 0.9230021834 0.9219226241 + 0.9311705828 0.9234492183 0.9224053025 + 0.9315806031 0.9238939285 0.9228845239 + 0.9319906235 0.9243385792 0.9233636856 + 0.9324005842 0.9247832894 0.9238429070 + 0.9328106046 0.9252279997 0.9243221283 + 0.9332206249 0.9256727099 0.9248012900 + 0.9336305857 0.9261174202 0.9252805114 + 0.9340372086 0.9265592098 0.9257540703 + 0.9344422817 0.9269996285 0.9262254834 + 0.9348474145 0.9274401069 0.9266968966 + 0.9352524877 0.9278805256 0.9271683097 + 0.9356576204 0.9283210039 0.9276397228 + 0.9360626936 0.9287614226 0.9281110764 + 0.9364678264 0.9292019010 0.9285824895 + 0.9368677139 0.9296385050 0.9290475249 + 0.9372671247 0.9300746918 0.9295117855 + 0.9376664162 0.9305108786 0.9299762249 + 0.9380658269 0.9309471250 0.9304406047 + 0.9384651780 0.9313833117 0.9309049845 + 0.9388645291 0.9318194985 0.9313693047 + 0.9392635822 0.9322556257 0.9318333268 + 0.9396575093 0.9326885939 0.9322912097 + 0.9400513768 0.9331216216 0.9327492118 + 0.9404454231 0.9335545897 0.9332070947 + 0.9408392906 0.9339876175 0.9336650968 + 0.9412332177 0.9344205856 0.9341230989 + 0.9416270852 0.9348536134 0.9345809817 + 0.9420204759 0.9352859855 0.9350377917 + 0.9424101710 0.9357147813 0.9354882240 + 0.9427999854 0.9361435771 0.9359384775 + 0.9431897998 0.9365724921 0.9363889098 + 0.9435796142 0.9370012879 0.9368392229 + 0.9439694285 0.9374300838 0.9372895956 + 0.9443591833 0.9378589988 0.9377400279 + 0.9447481036 0.9382869005 0.9381886721 + 0.9451332092 0.9387105107 0.9386302829 + 0.9455183744 0.9391341209 0.9390718937 + 0.9459034801 0.9395576715 0.9395135045 + 0.9462885857 0.9399812222 0.9399551153 + 0.9466736913 0.9404047728 0.9403967261 + 0.9470589161 0.9408283830 0.9408382773 + 0.9474430084 0.9412512183 0.9412782192 + 0.9478216767 0.9416697025 0.9417101145 + 0.9482004046 0.9420881867 0.9421418905 + 0.9485790730 0.9425066710 0.9425737858 + 0.9489578009 0.9429252148 0.9430056810 + 0.9493365288 0.9433436990 0.9434375167 + 0.9497151971 0.9437621832 0.9438694119 + 0.9500933290 0.9441801906 0.9443004727 + 0.9504646063 0.9445917010 0.9447228909 + 0.9508358240 0.9450030923 0.9451454282 + 0.9512069821 0.9454146028 0.9455677867 + 0.9515783191 0.9458261132 0.9459903240 + 0.9519494772 0.9462375045 0.9464128017 + 0.9523206949 0.9466490149 0.9468352199 + 0.9526919723 0.9470605254 0.9472576976 + 0.9530565739 0.9474642873 0.9476736784 + 0.9534208179 0.9478675127 0.9480893016 + 0.9537848830 0.9482707977 0.9485049844 + 0.9541491270 0.9486740828 0.9489206076 + 0.9545133114 0.9490773082 0.9493361712 + 0.9548773766 0.9494805932 0.9497517943 + 0.9552416205 0.9498838782 0.9501674175 + 0.9556018114 0.9502828717 0.9505789876 + 0.9559605122 0.9506804943 0.9509891272 + 0.9563192129 0.9510779977 0.9513992071 + 0.9566779137 0.9514756203 0.9518092871 + 0.9570366740 0.9518731236 0.9522194266 + 0.9573953748 0.9522706866 0.9526295066 + 0.9577540755 0.9526681900 0.9530395865 + 0.9581105709 0.9530637264 0.9534469247 + 0.9584643841 0.9534568191 0.9538511038 + 0.9588183165 0.9538499117 0.9542552829 + 0.9591721296 0.9542430043 0.9546595812 + 0.9595260024 0.9546362162 0.9550638199 + 0.9598798752 0.9550293088 0.9554679990 + 0.9602336884 0.9554224014 0.9558722973 + 0.9605869055 0.9558148980 0.9562754035 + 0.9609360099 0.9562035203 0.9566727877 + 0.9612851143 0.9565922022 0.9570701122 + 0.9616342187 0.9569808245 0.9574674964 + 0.9619833827 0.9573695064 0.9578648806 + 0.9623324871 0.9577581882 0.9582623243 + 0.9626815915 0.9581468105 0.9586595893 + 0.9630306959 0.9585354924 0.9590569735 + 0.9633762240 0.9589205980 0.9594489932 + 0.9637206793 0.9593046904 0.9598394036 + 0.9640650749 0.9596887827 0.9602298141 + 0.9644095898 0.9600728750 0.9606202841 + 0.9647539854 0.9604570270 0.9610106945 + 0.9650985003 0.9608411193 0.9614012241 + 0.9654428959 0.9612252116 0.9617915750 + 0.9657856226 0.9616078138 0.9621797204 + 0.9661247730 0.9619873166 0.9625632167 + 0.9664641023 0.9623668194 0.9629465938 + 0.9668033123 0.9627463222 0.9633300900 + 0.9671425223 0.9631258249 0.9637135267 + 0.9674816728 0.9635053277 0.9640970230 + 0.9678210020 0.9638847113 0.9644804001 + 0.9681602120 0.9642642140 0.9648638964 + 0.9684944749 0.9646391869 0.9652413726 + 0.9688279033 0.9650132060 0.9656177759 + 0.9691612124 0.9653872252 0.9659941792 + 0.9694945216 0.9657611847 0.9663705826 + 0.9698278904 0.9661352038 0.9667469859 + 0.9701611996 0.9665092230 0.9671233892 + 0.9704945087 0.9668831825 0.9674997926 + 0.9708263874 0.9672555923 0.9678742290 + 0.9711539745 0.9676234126 0.9682434201 + 0.9714816213 0.9679911137 0.9686124921 + 0.9718093276 0.9683588743 0.9689816833 + 0.9721369147 0.9687265754 0.9693508148 + 0.9724645019 0.9690943956 0.9697200060 + 0.9727920890 0.9694622159 0.9700891972 + 0.9731197953 0.9698299170 0.9704583287 + 0.9734438062 0.9701938033 0.9708225727 + 0.9737656713 0.9705553055 0.9711840749 + 0.9740875959 0.9709169269 0.9715456963 + 0.9744095802 0.9712784290 0.9719071984 + 0.9747315049 0.9716399908 0.9722688198 + 0.9750534296 0.9720014930 0.9726303220 + 0.9753754139 0.9723631144 0.9729918838 + 0.9756972790 0.9727246165 0.9733533859 + 0.9760133028 0.9730800986 0.9737070799 + 0.9763289094 0.9734352231 0.9740604162 + 0.9766445756 0.9737904072 0.9744136930 + 0.9769601822 0.9741454720 0.9747670293 + 0.9772757888 0.9745005965 0.9751201868 + 0.9775915146 0.9748557210 0.9754735231 + 0.9779071212 0.9752107859 0.9758267999 + 0.9782214761 0.9755647182 0.9761784077 + 0.9785307050 0.9759128094 0.9765232205 + 0.9788398147 0.9762609005 0.9768680930 + 0.9791489244 0.9766091108 0.9772129059 + 0.9794579744 0.9769572020 0.9775577188 + 0.9797672033 0.9773054123 0.9779024720 + 0.9800763130 0.9776535034 0.9782472849 + 0.9803854227 0.9780015945 0.9785920978 + 0.9806922078 0.9783468843 0.9789335728 + 0.9809947014 0.9786871076 0.9792693257 + 0.9812973142 0.9790272117 0.9796050191 + 0.9815999269 0.9793673754 0.9799407721 + 0.9819024801 0.9797075987 0.9802765250 + 0.9822049737 0.9800478220 0.9806122184 + 0.9825075865 0.9803879857 0.9809479117 + 0.9828101993 0.9807282090 0.9812836051 + 0.9831092954 0.9810643196 0.9816148877 + 0.9834046960 0.9813960195 0.9819414020 + 0.9837002158 0.9817277789 0.9822679162 + 0.9839956164 0.9820595980 0.9825943708 + 0.9842910171 0.9823912978 0.9829208851 + 0.9845864177 0.9827231169 0.9832475185 + 0.9848818183 0.9830548167 0.9835739732 + 0.9851772189 0.9833865762 0.9839004874 + 0.9854683280 0.9837135077 0.9842218161 + 0.9857556820 0.9840362072 0.9845384955 + 0.9860429764 0.9843589067 0.9848552942 + 0.9863303900 0.9846814871 0.9851719737 + 0.9866176844 0.9850041866 0.9854887724 + 0.9869050980 0.9853268862 0.9858055115 + 0.9871923923 0.9856495261 0.9861223102 + 0.9874798059 0.9859722257 0.9864389896 + 0.9877628088 0.9862895012 0.9867501855 + 0.9880418181 0.9866018295 0.9870560765 + 0.9883208275 0.9869140983 0.9873620272 + 0.9885997772 0.9872264266 0.9876679182 + 0.9888787866 0.9875386953 0.9879738092 + 0.9891577959 0.9878510237 0.9882797003 + 0.9894368052 0.9881632924 0.9885855913 + 0.9897158146 0.9884756207 0.9888914824 + 0.9899904728 0.9887828231 0.9891915917 + 0.9902597070 0.9890834093 0.9894841909 + 0.9905288815 0.9893839955 0.9897767901 + 0.9907981157 0.9896845818 0.9900693893 + 0.9910672903 0.9899852276 0.9903619885 + 0.9913365245 0.9902858138 0.9906545877 + 0.9916056991 0.9905864000 0.9909471869 + 0.9918748736 0.9908869863 0.9912399054 + 0.9921408892 0.9911842942 0.9915285110 + 0.9923989177 0.9914734960 0.9918072224 + 0.9926568866 0.9917625785 0.9920858741 + 0.9929149151 0.9920517802 0.9923645854 + 0.9931728840 0.9923409820 0.9926434159 + 0.9934309125 0.9926301241 0.9929221272 + 0.9936888814 0.9929193258 0.9932007790 + 0.9939469099 0.9932084084 0.9934794903 + 0.9942042232 0.9934967756 0.9937573075 + 0.9944509864 0.9937735200 0.9940230846 + 0.9946978092 0.9940503240 0.9942888021 + 0.9949445724 0.9943270087 0.9945545793 + 0.9951913953 0.9946038127 0.9948202968 + 0.9954382181 0.9948804975 0.9950860739 + 0.9956849813 0.9951573014 0.9953517914 + 0.9959318042 0.9954339862 0.9956176281 + 0.9961786866 0.9957107902 0.9958834052 + 0.9964144826 0.9959757924 0.9961376190 + 0.9966472983 0.9962375760 0.9963884950 + 0.9968799949 0.9964993000 0.9966394901 + 0.9971128106 0.9967610240 0.9968904257 + 0.9973455071 0.9970228076 0.9971414208 + 0.9975783229 0.9972844720 0.9973922968 + 0.9978110194 0.9975461960 0.9976432920 + 0.9980437756 0.9978079200 0.9978942275 + 0.9982696176 0.9980614185 0.9981374741 + 0.9984859228 0.9983037710 0.9983702898 + 0.9987022281 0.9985461235 0.9986031055 + 0.9989184737 0.9987884164 0.9988359213 + 0.9991347790 0.9990307093 0.9990686774 + 0.9993510842 0.9992730021 0.9993016124 + 0.9995673895 0.9995154142 0.9995344281 + 0.9997836947 0.9997577071 0.9997671843 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_ii_200.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_ii_200.spi1d index 66c09889..979afc2d 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_ii_200.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_ii_200.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0175220203 0.0159201808 0.0112829898 - 0.0302315596 0.0280031394 0.0208480097 - 0.0407330506 0.0385310091 0.0291486792 - 0.0501033999 0.0480892099 0.0366973914 - 0.0587283783 0.0569948703 0.0437196717 - 0.0668726563 0.0654532537 0.0503452383 - 0.0746254995 0.0734657720 0.0566753708 - 0.0821137205 0.0811266303 0.0627593473 - 0.0893652514 0.0885340869 0.0686344281 - 0.0964666009 0.0956818983 0.0743253529 - 0.1034414992 0.1026156992 0.0798535123 - 0.1102956012 0.1093306020 0.0852200314 - 0.1170141026 0.1157635972 0.0904216021 - 0.1235916987 0.1219805032 0.0954708308 - 0.1299463063 0.1280032992 0.1003969014 - 0.1360912025 0.1338655055 0.1052050963 - 0.1420111060 0.1395675987 0.1098947003 - 0.1477487981 0.1450989991 0.1144763976 - 0.1533298045 0.1504811943 0.1189825982 - 0.1587764025 0.1557295024 0.1233960986 - 0.1640834063 0.1608278006 0.1277330071 - 0.1692633927 0.1657993942 0.1319945008 - 0.1743295044 0.1706517041 0.1361728013 - 0.1792751998 0.1754034013 0.1402668059 - 0.1841226965 0.1800453961 0.1442912966 - 0.1888726950 0.1845912039 0.1482613981 - 0.1935237944 0.1890432984 0.1521809995 - 0.1980873048 0.1934227943 0.1560502052 - 0.2025565058 0.1977252066 0.1598621011 - 0.2069393992 0.2019387037 0.1636209041 - 0.2112462074 0.2060763985 0.1673309952 - 0.2154687047 0.2101363987 0.1709980965 - 0.2196132988 0.2141312063 0.1746173948 - 0.2236949950 0.2180649042 0.1781916022 - 0.2277055979 0.2219256014 0.1817201972 - 0.2316590995 0.2257197052 0.1852061003 - 0.2355517000 0.2294515073 0.1886554062 - 0.2393918931 0.2331303954 0.1920740008 - 0.2431806028 0.2367579043 0.1954559982 - 0.2469168007 0.2403406054 0.1987928003 - 0.2505995929 0.2438685000 0.2020907998 - 0.2542274892 0.2473488003 0.2053564936 - 0.2578184903 0.2507809997 0.2085936964 - 0.2613571882 0.2541683912 0.2117979974 - 0.2648550868 0.2575106919 0.2149727941 - 0.2683109939 0.2608149052 0.2181185931 - 0.2717253864 0.2640857995 0.2212391943 - 0.2750979960 0.2673158050 0.2243300974 - 0.2784220874 0.2705042958 0.2273901999 - 0.2817071974 0.2736572921 0.2304214984 - 0.2849532962 0.2767716050 0.2334305048 - 0.2881663144 0.2798540890 0.2364110053 - 0.2913444042 0.2829042077 0.2393691987 - 0.2944832146 0.2859222889 0.2423065007 - 0.2975896001 0.2889142036 0.2452152967 - 0.3006660938 0.2918727994 0.2480936944 - 0.3037149906 0.2947992980 0.2509461045 - 0.3067280948 0.2976973951 0.2537800968 - 0.3097102046 0.3005683124 0.2565920949 - 0.3126625121 0.3034102917 0.2593840063 - 0.3155854940 0.3062278926 0.2621518970 - 0.3184789121 0.3090203106 0.2648904026 - 0.3213432133 0.3117856979 0.2676062882 - 0.3241868913 0.3145250082 0.2703030109 - 0.3270075023 0.3172332048 0.2729845047 - 0.3298006058 0.3199186921 0.2756457031 - 0.3325676918 0.3225826025 0.2782875001 - 0.3353078067 0.3252244890 0.2809073031 - 0.3380177915 0.3278487027 0.2835057080 - 0.3406989872 0.3304544091 0.2860867977 - 0.3433553874 0.3330389857 0.2886512876 - 0.3459919989 0.3356043994 0.2911956012 - 0.3486098945 0.3381497860 0.2937203944 - 0.3512117863 0.3406724036 0.2962253094 - 0.3537960052 0.3431720138 0.2987118959 - 0.3563596904 0.3456499875 0.3011856973 - 0.3589065075 0.3481073976 0.3036412895 - 0.3614340127 0.3505432010 0.3060742915 - 0.3639450073 0.3529601097 0.3084880114 - 0.3664397895 0.3553602993 0.3108876050 - 0.3689151108 0.3577440977 0.3132722974 - 0.3713718057 0.3601129949 0.3156406879 - 0.3738071918 0.3624683917 0.3179928064 - 0.3762201071 0.3648113012 0.3203321099 - 0.3786138892 0.3671433032 0.3226552010 - 0.3809883893 0.3694634140 0.3249565065 - 0.3833450079 0.3717713058 0.3272385001 - 0.3856788874 0.3740625978 0.3295018971 - 0.3879950047 0.3763391078 0.3317486942 - 0.3902960122 0.3786000907 0.3339833021 - 0.3925783932 0.3808402121 0.3362008929 - 0.3948448002 0.3830653131 0.3384015858 - 0.3970947862 0.3852767050 0.3405882120 - 0.3993253112 0.3874705136 0.3427591920 - 0.4015383124 0.3896510005 0.3449178934 - 0.4037337899 0.3918201923 0.3470610082 - 0.4059110880 0.3939774036 0.3491885066 - 0.4080676138 0.3961207867 0.3513003886 - 0.4102075100 0.3982515931 0.3533968031 - 0.4123331010 0.4003694952 0.3554781973 - 0.4144445062 0.4024741054 0.3575446904 - 0.4165400863 0.4045642912 0.3595972955 - 0.4186260104 0.4066396058 0.3616338074 - 0.4207012057 0.4087007046 0.3636533022 - 0.4227670133 0.4107474089 0.3656555116 - 0.4248233140 0.4127795100 0.3676426113 - 0.4268693924 0.4147967100 0.3696162105 - 0.4289070964 0.4167988002 0.3715760112 - 0.4309335947 0.4187847972 0.3735184968 - 0.4329519868 0.4207592905 0.3754445016 - 0.4349603057 0.4227212965 0.3773564994 - 0.4369597137 0.4246717989 0.3792560101 - 0.4389463067 0.4266099930 0.3811441064 - 0.4409210086 0.4285362065 0.3830193877 - 0.4428839087 0.4304524064 0.3848828077 - 0.4448337853 0.4323602021 0.3867362142 - 0.4467687905 0.4342601001 0.3885799944 - 0.4486894906 0.4361515045 0.3904125988 - 0.4505963922 0.4380353987 0.3922336102 - 0.4524900913 0.4399108887 0.3940415084 - 0.4543710947 0.4417791069 0.3958359063 - 0.4562399089 0.4436390996 0.3976179957 - 0.4580954909 0.4454919100 0.3993878961 - 0.4599367976 0.4473369122 0.4011451006 - 0.4617645144 0.4491747022 0.4028908908 - 0.4635792077 0.4510050118 0.4046252966 - 0.4653801918 0.4528281987 0.4063490927 - 0.4671661854 0.4546442926 0.4080632031 - 0.4689376950 0.4564531147 0.4097684920 - 0.4706963003 0.4582554102 0.4114660919 - 0.4724414945 0.4600501955 0.4131549895 - 0.4741756022 0.4618388116 0.4148359001 - 0.4758949876 0.4636174142 0.4165103137 - 0.4776011109 0.4653869867 0.4181793034 - 0.4792934954 0.4671463966 0.4198431969 - 0.4809749126 0.4688977003 0.4215008914 - 0.4826433957 0.4706400037 0.4231525958 - 0.4842990041 0.4723741114 0.4247975051 - 0.4859426916 0.4740991890 0.4264369905 - 0.4875749052 0.4758136868 0.4280720055 - 0.4891946912 0.4775179923 0.4297009110 - 0.4908007979 0.4792113900 0.4313251078 - 0.4923956096 0.4808942974 0.4329412878 - 0.4939799905 0.4825680852 0.4345523119 - 0.4955542982 0.4842331111 0.4361563027 - 0.4971171916 0.4858896136 0.4377534986 - 0.4986675084 0.4875380993 0.4393444955 - 0.5002059937 0.4891777039 0.4409264028 - 0.5017321706 0.4908088148 0.4425007105 - 0.5032498837 0.4924311936 0.4440687895 - 0.5047596097 0.4940446913 0.4456312060 - 0.5062615871 0.4956491888 0.4471881092 - 0.5077568889 0.4972448051 0.4487377107 - 0.5092430115 0.4988301992 0.4502801001 - 0.5107212067 0.5004084110 0.4518156946 - 0.5121917129 0.5019794703 0.4533447921 - 0.5136547089 0.5035430789 0.4548679888 - 0.5151066184 0.5050961971 0.4563845992 - 0.5165513754 0.5066397786 0.4578959048 - 0.5179892778 0.5081757903 0.4594022036 - 0.5194200277 0.5097053051 0.4609036148 - 0.5208390951 0.5112237930 0.4623981118 - 0.5222476125 0.5127363205 0.4638865888 - 0.5236480236 0.5142416954 0.4653699994 - 0.5250409842 0.5157390833 0.4668483138 - 0.5264229774 0.5172262788 0.4683186114 - 0.5277984142 0.5187063217 0.4697833955 - 0.5291666985 0.5201799870 0.4712427855 - 0.5305246711 0.5216469765 0.4726960957 - 0.5318741798 0.5231056809 0.4741415083 - 0.5332177877 0.5245596170 0.4755817056 - 0.5345557928 0.5260090232 0.4770165980 - 0.5358840227 0.5274482965 0.4784426987 - 0.5372061729 0.5288820863 0.4798631072 - 0.5385223031 0.5303105712 0.4812782109 - 0.5398281813 0.5317319036 0.4826853871 - 0.5411273837 0.5331465006 0.4840855002 - 0.5424209237 0.5345557928 0.4854811132 - 0.5437052250 0.5359572768 0.4868715107 - 0.5449833870 0.5373530984 0.4882541895 - 0.5462568998 0.5387449265 0.4896318913 - 0.5475227237 0.5401304960 0.4910022914 - 0.5487810969 0.5415118933 0.4923650026 - 0.5500348210 0.5428903103 0.4937227964 - 0.5512812138 0.5442638993 0.4950731993 - 0.5525211096 0.5456309915 0.4964149892 - 0.5537570119 0.5469937921 0.4977517128 - 0.5549864769 0.5483481884 0.4990803897 - 0.5562115908 0.5496984124 0.5004032850 - 0.5574334264 0.5510452986 0.5017220974 - 0.5586463809 0.5523828268 0.5030320883 - 0.5598551035 0.5537164807 0.5043380260 - 0.5610579252 0.5550454259 0.5056391954 - 0.5622550249 0.5563679934 0.5069329143 - 0.5634492040 0.5576872230 0.5082232952 - 0.5646370053 0.5589987040 0.5095074773 - 0.5658180714 0.5603044033 0.5107867122 - 0.5669947863 0.5616064072 0.5120629072 - 0.5681625009 0.5628957748 0.5133314133 - 0.5693277121 0.5641813874 0.5145972967 - 0.5704880953 0.5654600263 0.5158582926 - 0.5716452003 0.5667315722 0.5171144009 - 0.5728005171 0.5679996014 0.5183681846 - 0.5739479065 0.5692597032 0.5196151137 - 0.5750917792 0.5705173016 0.5208598971 - 0.5762277842 0.5717689991 0.5221000910 - 0.5773584247 0.5730172992 0.5233365297 - 0.5784850717 0.5742632151 0.5245702863 - 0.5796056986 0.5755022764 0.5257986784 - 0.5807244778 0.5767388940 0.5270252228 - 0.5818371773 0.5779654980 0.5282452703 - 0.5829467773 0.5791882277 0.5294620991 - 0.5840514898 0.5804045796 0.5306738019 - 0.5851526856 0.5816155076 0.5318812132 - 0.5862517953 0.5828229189 0.5330861211 - 0.5873453021 0.5840238929 0.5342842937 - 0.5884373188 0.5852233768 0.5354809761 - 0.5895215273 0.5864148736 0.5366703868 - 0.5906035900 0.5876041055 0.5378572941 - 0.5916786194 0.5887857080 0.5390371084 - 0.5927507281 0.5899624825 0.5402138233 - 0.5938187242 0.5911325812 0.5413867235 - 0.5948839784 0.5922974944 0.5425544977 - 0.5959472060 0.5934585929 0.5437186956 - 0.5970050097 0.5946133137 0.5448786020 - 0.5980607271 0.5957654715 0.5460368991 - 0.5991076231 0.5969123244 0.5471885204 - 0.6001530290 0.5980582237 0.5483391285 - 0.6011908054 0.5991981030 0.5494847894 - 0.6022279263 0.6003370881 0.5506296754 - 0.6032595038 0.6014683843 0.5517669916 - 0.6042903066 0.6025981903 0.5529031754 - 0.6053165793 0.6037194133 0.5540338755 - 0.6063408852 0.6048387885 0.5551629066 - 0.6073579788 0.6059514880 0.5562863946 - 0.6083726883 0.6070622802 0.5574082732 - 0.6093810201 0.6081678867 0.5585262775 - 0.6103867292 0.6092718244 0.5596421957 - 0.6113867760 0.6103717089 0.5607541203 - 0.6123839021 0.6114696264 0.5618643761 - 0.6133750081 0.6125634909 0.5629718900 - 0.6143637896 0.6136540174 0.5640769005 - 0.6153482199 0.6147373915 0.5651777983 - 0.6163303256 0.6158183813 0.5662764907 - 0.6173073053 0.6168941259 0.5673714280 - 0.6182823181 0.6179677844 0.5684645772 - 0.6192523837 0.6190360785 0.5695542097 - 0.6202210188 0.6201022863 0.5706425905 - 0.6211845875 0.6211615205 0.5717275143 - 0.6221469045 0.6222187877 0.5728116035 - 0.6231033206 0.6232674718 0.5738925934 - 0.6240592003 0.6243153811 0.5749726295 - 0.6250107288 0.6253551245 0.5760461092 - 0.6259621978 0.6263948083 0.5771194100 - 0.6269065142 0.6274272203 0.5781859756 - 0.6278507113 0.6284595728 0.5792527199 - 0.6287866235 0.6294867992 0.5803136826 - 0.6297214031 0.6305134296 0.5813742280 - 0.6306506991 0.6315355897 0.5824303031 - 0.6315780878 0.6325563788 0.5834851861 - 0.6325017214 0.6335713863 0.5845344067 - 0.6334224939 0.6345824003 0.5855802298 - 0.6343404055 0.6355903149 0.5866221189 - 0.6352542043 0.6365941763 0.5876594186 - 0.6361662745 0.6375966072 0.5886945724 - 0.6370725036 0.6385940909 0.5897240043 - 0.6379784942 0.6395912170 0.5907528996 - 0.6388779283 0.6405804753 0.5917738080 - 0.6397773027 0.6415697932 0.5927945971 - 0.6406729221 0.6425516009 0.5938079953 - 0.6415677071 0.6435316801 0.5948200226 - 0.6424599886 0.6445075870 0.5958276987 - 0.6433506012 0.6454802752 0.5968323946 - 0.6442388892 0.6464512944 0.5978347063 - 0.6451222897 0.6474183798 0.5988320112 - 0.6460055113 0.6483852863 0.5998288989 - 0.6468821764 0.6493440866 0.6008189917 - 0.6477587819 0.6503030062 0.6018090248 - 0.6486309767 0.6512538791 0.6027926207 - 0.6495016217 0.6522020102 0.6037741899 - 0.6503694057 0.6531463265 0.6047533154 - 0.6512334943 0.6540852189 0.6057292223 - 0.6520972252 0.6550235748 0.6067042947 - 0.6529569030 0.6559551954 0.6076720953 - 0.6538167000 0.6568868160 0.6086397767 - 0.6546720862 0.6578143239 0.6096029878 - 0.6555258036 0.6587402821 0.6105644703 - 0.6563767791 0.6596642733 0.6115236282 - 0.6572223902 0.6605846882 0.6124786735 - 0.6580681205 0.6615051031 0.6134337783 - 0.6589081883 0.6624196172 0.6143844128 - 0.6597480774 0.6633337140 0.6153349876 - 0.6605852842 0.6642439961 0.6162821054 - 0.6614202261 0.6651508808 0.6172264814 - 0.6622543931 0.6660568714 0.6181699038 - 0.6630814075 0.6669554114 0.6191053987 - 0.6639084816 0.6678538918 0.6200408936 - 0.6647310257 0.6687464714 0.6209712029 - 0.6655508876 0.6696357131 0.6218988895 - 0.6663697958 0.6705237031 0.6228252053 - 0.6671835184 0.6714056134 0.6237459183 - 0.6679971218 0.6722874045 0.6246666908 - 0.6688064933 0.6731655002 0.6255832911 - 0.6696135998 0.6740418077 0.6264979839 - 0.6704198122 0.6749172211 0.6274114847 - 0.6712203026 0.6757869720 0.6283187866 - 0.6720207930 0.6766569018 0.6292260289 - 0.6728178263 0.6775221825 0.6301295757 - 0.6736124158 0.6783844233 0.6310310960 - 0.6744068861 0.6792463064 0.6319320798 - 0.6751977801 0.6801002026 0.6328263879 - 0.6759887934 0.6809539795 0.6337206960 - 0.6767783165 0.6818047762 0.6346113086 - 0.6775661707 0.6826521158 0.6354981065 - 0.6783542037 0.6834993958 0.6363847852 - 0.6791371703 0.6843413115 0.6372650266 - 0.6799193025 0.6851823926 0.6381441951 - 0.6807003021 0.6860218048 0.6390221119 - 0.6814774275 0.6868560910 0.6398959160 - 0.6822544932 0.6876903176 0.6407696009 - 0.6830291152 0.6885216832 0.6416400075 - 0.6838018894 0.6893510818 0.6425083876 - 0.6845747232 0.6901804805 0.6433767080 - 0.6853433847 0.6910036206 0.6442382932 - 0.6861116886 0.6918261051 0.6450995803 - 0.6868789792 0.6926466823 0.6459587216 - 0.6876426935 0.6934604049 0.6468116045 - 0.6884064078 0.6942741871 0.6476644874 - 0.6891673207 0.6950843930 0.6485136151 - 0.6899256110 0.6958913207 0.6493594050 - 0.6906839013 0.6966981888 0.6502050757 - 0.6914379001 0.6975011826 0.6510459781 - 0.6921907067 0.6983032227 0.6518856883 - 0.6929433942 0.6991050243 0.6527251005 - 0.6936904788 0.6999025941 0.6535598040 - 0.6944376230 0.7007001042 0.6543945074 - 0.6951832175 0.7014958858 0.6552277207 - 0.6959239244 0.7022861242 0.6560571194 - 0.6966645718 0.7030763030 0.6568865180 - 0.6974024773 0.7038636804 0.6577119827 - 0.6981366873 0.7046473026 0.6585326791 - 0.6988708973 0.7054308057 0.6593534946 - 0.6996024251 0.7062115073 0.6601701975 - 0.7003322840 0.7069903016 0.6609845161 - 0.7010622025 0.7077690959 0.6617987752 - 0.7017893791 0.7085446119 0.6626083255 - 0.7025159001 0.7093191147 0.6634163260 - 0.7032423019 0.7100936174 0.6642243266 - 0.7039636970 0.7108619809 0.6650285125 - 0.7046843171 0.7116296291 0.6658324003 - 0.7054049969 0.7123970985 0.6666362286 - 0.7061207294 0.7131581903 0.6674346924 - 0.7068364024 0.7139192224 0.6682332158 - 0.7075518966 0.7146798968 0.6690312028 - 0.7082635164 0.7154349089 0.6698222160 - 0.7089750171 0.7161899209 0.6706132889 - 0.7096859217 0.7169442177 0.6714035273 - 0.7103911042 0.7176927924 0.6721884012 - 0.7110964060 0.7184414864 0.6729732752 - 0.7118008137 0.7191895247 0.6737573743 - 0.7125003934 0.7199338078 0.6745371819 - 0.7131999731 0.7206779718 0.6753169894 - 0.7138990164 0.7214214206 0.6760962009 - 0.7145938277 0.7221598029 0.6768721938 - 0.7152886987 0.7228981256 0.6776481867 - 0.7159827948 0.7236354947 0.6784234047 - 0.7166706920 0.7243661880 0.6791931987 - 0.7173587084 0.7250968814 0.6799631119 - 0.7180461884 0.7258272171 0.6807324290 - 0.7187278867 0.7265521288 0.6814960837 - 0.7194095850 0.7272771001 0.6822597980 - 0.7200912833 0.7280020118 0.6830233932 - 0.7207676768 0.7287219763 0.6837813854 - 0.7214440703 0.7294420004 0.6845393777 - 0.7221205235 0.7301620245 0.6852974296 - 0.7227922082 0.7308751941 0.6860501766 - 0.7234632969 0.7315877080 0.6868025064 - 0.7241343856 0.7323002219 0.6875547767 - 0.7248032093 0.7330073118 0.6883025169 - 0.7254711986 0.7337129116 0.6890491247 - 0.7261391878 0.7344185114 0.6897956729 - 0.7268046141 0.7351201773 0.6905381083 - 0.7274683118 0.7358196974 0.6912782192 - 0.7281321287 0.7365190983 0.6920182705 - 0.7287933230 0.7372158170 0.6927558780 - 0.7294514775 0.7379088998 0.6934906840 - 0.7301098108 0.7386021018 0.6942256093 - 0.7307670712 0.7392935753 0.6949588060 - 0.7314220071 0.7399803996 0.6956874728 - 0.7320768237 0.7406671047 0.6964163184 - 0.7327315807 0.7413536906 0.6971448064 - 0.7333819270 0.7420334816 0.6978684068 - 0.7340322137 0.7427132130 0.6985918880 - 0.7346825004 0.7433928847 0.6993154287 - 0.7353283763 0.7440670729 0.7000336051 - 0.7359728813 0.7447395921 0.7007502913 - 0.7366175056 0.7454121113 0.7014670968 - 0.7372602820 0.7460821271 0.7021806240 - 0.7379010916 0.7467494011 0.7028908730 - 0.7385420203 0.7474167943 0.7036011219 - 0.7391821146 0.7480832934 0.7043104172 - 0.7398182154 0.7487462759 0.7050154209 - 0.7404543757 0.7494093180 0.7057204247 - 0.7410904765 0.7500723004 0.7064254284 - 0.7417215705 0.7507299781 0.7071260214 - 0.7423517108 0.7513865829 0.7078258991 - 0.7429817915 0.7520431280 0.7085257769 - 0.7436100841 0.7526969910 0.7092229724 - 0.7442362905 0.7533478141 0.7099171877 - 0.7448624969 0.7539986968 0.7106114030 - 0.7454884052 0.7546491027 0.7113050222 - 0.7461096048 0.7552949786 0.7119926810 - 0.7467309237 0.7559409142 0.7126803994 - 0.7473521233 0.7565869093 0.7133681774 - 0.7479693294 0.7572283149 0.7140517831 - 0.7485842705 0.7578672767 0.7147333026 - 0.7491992712 0.7585062981 0.7154148817 - 0.7498134971 0.7591440082 0.7160953879 - 0.7504243255 0.7597767115 0.7167720199 - 0.7510352135 0.7604094148 0.7174485922 - 0.7516459823 0.7610421181 0.7181252241 - 0.7522531152 0.7616708875 0.7187982202 - 0.7528588176 0.7622982264 0.7194700241 - 0.7534645200 0.7629255056 0.7201418281 - 0.7540687919 0.7635515928 0.7208123207 - 0.7546679974 0.7641733885 0.7214787006 - 0.7552672029 0.7647953033 0.7221450806 - 0.7558664083 0.7654170990 0.7228114009 - 0.7564626932 0.7660366893 0.7234749794 - 0.7570574880 0.7666552067 0.7241373062 - 0.7576522827 0.7672737241 0.7247995734 - 0.7582467794 0.7678917050 0.7254614234 - 0.7588381767 0.7685058117 0.7261199951 - 0.7594295740 0.7691199183 0.7267785072 - 0.7600209713 0.7697339058 0.7274370193 - 0.7606108189 0.7703450918 0.7280923128 - 0.7611988783 0.7709534764 0.7287446260 - 0.7617871165 0.7715619802 0.7293969989 - 0.7623751760 0.7721704245 0.7300493121 - 0.7629606128 0.7727757096 0.7306963205 - 0.7635456920 0.7733805180 0.7313426733 - 0.7641307116 0.7739853263 0.7319890857 - 0.7647150159 0.7745891213 0.7326344252 - 0.7652966976 0.7751892805 0.7332763076 - 0.7658783793 0.7757894993 0.7339181900 - 0.7664601207 0.7763897181 0.7345600724 - 0.7670400143 0.7769867778 0.7352002859 - 0.7676184773 0.7775810957 0.7358390093 - 0.7681970000 0.7781754732 0.7364776731 - 0.7687754035 0.7787699103 0.7371163964 - 0.7693514228 0.7793611288 0.7377526164 - 0.7699269056 0.7799516916 0.7383884192 - 0.7705023289 0.7805421948 0.7390242219 - 0.7710775137 0.7811324000 0.7396594882 - 0.7716495991 0.7817180753 0.7402899265 - 0.7722216845 0.7823038101 0.7409204245 - 0.7727938294 0.7828894854 0.7415509224 - 0.7733650208 0.7834734917 0.7421798110 - 0.7739340067 0.7840530872 0.7428051829 - 0.7745028734 0.7846328020 0.7434306741 - 0.7750719190 0.7852125168 0.7440561056 - 0.7756394148 0.7857902050 0.7446792126 - 0.7762054205 0.7863658071 0.7452998757 - 0.7767714262 0.7869415283 0.7459207177 - 0.7773373127 0.7875170708 0.7465413809 - 0.7779011726 0.7880907059 0.7471587062 - 0.7784639001 0.7886632085 0.7477741838 - 0.7790266275 0.7892357111 0.7483897805 - 0.7795892954 0.7898082137 0.7490053177 - 0.7801496983 0.7903760076 0.7496179938 - 0.7807093263 0.7909423709 0.7502297759 - 0.7812688947 0.7915087938 0.7508416176 - 0.7818285227 0.7920752168 0.7514533997 - 0.7823855877 0.7926365137 0.7520614862 - 0.7829421759 0.7931969762 0.7526692152 - 0.7834988832 0.7937574983 0.7532768846 - 0.7840554714 0.7943180203 0.7538844943 - 0.7846093178 0.7948756218 0.7544875145 - 0.7851626873 0.7954329848 0.7550902963 - 0.7857161760 0.7959904075 0.7556930184 - 0.7862696052 0.7965477705 0.7562956810 - 0.7868201733 0.7971013784 0.7568932176 - 0.7873706222 0.7976546884 0.7574905753 - 0.7879210114 0.7982081175 0.7580878735 - 0.7884714007 0.7987614870 0.7586852908 - 0.7890191078 0.7993094921 0.7592788935 - 0.7895665765 0.7998571992 0.7598723769 - 0.7901141047 0.8004049063 0.7604658008 - 0.7906615734 0.8009526134 0.7610592842 - 0.7912061810 0.8014960885 0.7616505027 - 0.7917506099 0.8020390272 0.7622414231 - 0.7922949195 0.8025820255 0.7628324032 - 0.7928392887 0.8031250238 0.7634233832 - 0.7933812141 0.8036649227 0.7640110850 - 0.7939224839 0.8042042255 0.7645980716 - 0.7944638729 0.8047435880 0.7651851177 - 0.7950052023 0.8052828908 0.7657721043 - 0.7955445051 0.8058183789 0.7663552165 - 0.7960829735 0.8063523769 0.7669368982 - 0.7966215014 0.8068864942 0.7675185800 - 0.7971600294 0.8074206114 0.7681002021 - 0.7976967096 0.8079521060 0.7686796188 - 0.7982320786 0.8084818125 0.7692573071 - 0.7987675071 0.8090115786 0.7698349953 - 0.7993028760 0.8095412850 0.7704126835 - 0.7998371124 0.8100696802 0.7709888816 - 0.8003696203 0.8105961084 0.7715629935 - 0.8009020090 0.8111224771 0.7721372247 - 0.8014343977 0.8116489053 0.7727112770 - 0.8019663095 0.8121743798 0.7732846141 - 0.8024960160 0.8126959801 0.7738549113 - 0.8030256033 0.8132175803 0.7744253278 - 0.8035553098 0.8137391806 0.7749956250 - 0.8040848970 0.8142607212 0.7755659819 - 0.8046116829 0.8147786260 0.7761322856 - 0.8051382899 0.8152961731 0.7766984105 - 0.8056647778 0.8158138990 0.7772645950 - 0.8061913848 0.8163315058 0.7778307796 - 0.8067160845 0.8168472052 0.7783944011 - 0.8072397709 0.8173617721 0.7789568901 - 0.8077635169 0.8178765178 0.7795193195 - 0.8082872033 0.8183912039 0.7800818086 - 0.8088098764 0.8189039230 0.7806432843 - 0.8093308210 0.8194131255 0.7812033892 - 0.8098517060 0.8199223280 0.7817634940 - 0.8103727102 0.8204315901 0.7823235989 - 0.8108935952 0.8209407926 0.7828835845 - 0.8114115000 0.8214446902 0.7834416032 - 0.8119295239 0.8219487071 0.7839996219 - 0.8124474287 0.8224527240 0.7845575809 - 0.8129652739 0.8229566813 0.7851157188 - 0.8134815097 0.8234592080 0.7856711745 - 0.8139966130 0.8239607215 0.7862251997 - 0.8145117164 0.8244621158 0.7867792845 - 0.8150267005 0.8249636292 0.7873333097 - 0.8155413270 0.8254641294 0.7878866196 - 0.8160536885 0.8259606957 0.7884370089 - 0.8165659904 0.8264572024 0.7889872789 - 0.8170784116 0.8269537091 0.7895377278 - 0.8175907731 0.8274502158 0.7900881171 - 0.8181009889 0.8279432058 0.7906360030 - 0.8186104298 0.8284350038 0.7911829948 - 0.8191198111 0.8289268017 0.7917301059 - 0.8196291924 0.8294185996 0.7922770977 - 0.8201377988 0.8299099207 0.7928233147 - 0.8206434250 0.8303995728 0.7933669090 - 0.8211489916 0.8308892846 0.7939103842 - 0.8216546178 0.8313789964 0.7944539785 - 0.8221603036 0.8318687081 0.7949975133 - 0.8226630092 0.8323553205 0.7955383062 - 0.8231644034 0.8328405023 0.7960780263 - 0.8236657977 0.8333258033 0.7966176867 - 0.8241671920 0.8338109851 0.7971572876 - 0.8246682286 0.8342955709 0.7976964116 - 0.8251668215 0.8347756267 0.7982317209 - 0.8256654739 0.8352555037 0.7987669706 - 0.8261641264 0.8357354999 0.7993022203 - 0.8266627192 0.8362154961 0.7998375297 - 0.8271595240 0.8366938233 0.8003703952 - 0.8276544809 0.8371704817 0.8009009957 - 0.8281494975 0.8376471996 0.8014315963 - 0.8286445141 0.8381239176 0.8019623160 - 0.8291395903 0.8386006951 0.8024929166 - 0.8296310902 0.8390740752 0.8030207157 - 0.8301221132 0.8395469785 0.8035480976 - 0.8306130767 0.8400198221 0.8040754199 - 0.8311039805 0.8404927254 0.8046028018 - 0.8315945864 0.8409647942 0.8051295280 - 0.8320829272 0.8414331079 0.8056535125 - 0.8325713277 0.8419014215 0.8061774969 - 0.8330597281 0.8423696160 0.8067014217 - 0.8335480094 0.8428379297 0.8072254062 - 0.8340349197 0.8433049917 0.8077479005 - 0.8345198035 0.8437706828 0.8082687855 - 0.8350048065 0.8442363143 0.8087896109 - 0.8354896903 0.8447020054 0.8093103766 - 0.8359745741 0.8451675773 0.8098313212 - 0.8364562988 0.8456311226 0.8103501797 - 0.8369364142 0.8460934758 0.8108682036 - 0.8374165297 0.8465558887 0.8113862276 - 0.8378965259 0.8470183015 0.8119041920 - 0.8383765817 0.8474807143 0.8124222159 - 0.8388531804 0.8479387760 0.8129376173 - 0.8393293023 0.8483963013 0.8134527206 - 0.8398053050 0.8488537073 0.8139677048 - 0.8402814269 0.8493111730 0.8144826889 - 0.8407574296 0.8497685194 0.8149976730 - 0.8412302732 0.8502224088 0.8155111074 - 0.8417032957 0.8506761789 0.8160244226 - 0.8421761990 0.8511301279 0.8165377975 - 0.8426492214 0.8515838981 0.8170511127 - 0.8431215286 0.8520373702 0.8175640702 - 0.8435900211 0.8524886966 0.8180745840 - 0.8440583944 0.8529400229 0.8185852170 - 0.8445268869 0.8533912897 0.8190956712 - 0.8449953198 0.8538426161 0.8196063042 - 0.8454630971 0.8542929888 0.8201159835 - 0.8459277749 0.8547400832 0.8206226230 - 0.8463925719 0.8551871181 0.8211292028 - 0.8468574286 0.8556342125 0.8216357827 - 0.8473222256 0.8560811877 0.8221424818 - 0.8477864265 0.8565273881 0.8226484060 - 0.8482484221 0.8569706082 0.8231524229 - 0.8487104177 0.8574137092 0.8236563802 - 0.8491724133 0.8578569293 0.8241603971 - 0.8496344090 0.8583000898 0.8246644139 - 0.8500953913 0.8587425947 0.8251677155 - 0.8505532742 0.8591822982 0.8256688714 - 0.8510112166 0.8596221209 0.8261700869 - 0.8514690995 0.8600618243 0.8266713023 - 0.8519269824 0.8605015874 0.8271725178 - 0.8523843288 0.8609408140 0.8276733160 - 0.8528388143 0.8613771200 0.8281720281 - 0.8532934189 0.8618134856 0.8286706805 - 0.8537480235 0.8622499108 0.8291695118 - 0.8542025089 0.8626862764 0.8296682239 - 0.8546568751 0.8631222844 0.8301666975 - 0.8551089764 0.8635547757 0.8306632042 - 0.8555610180 0.8639872074 0.8311597109 - 0.8560131192 0.8644195795 0.8316562176 - 0.8564652205 0.8648520708 0.8321527243 - 0.8569173217 0.8652845025 0.8326491714 - 0.8573657870 0.8657131195 0.8331434131 - 0.8578140736 0.8661414981 0.8336374760 - 0.8582624197 0.8665698767 0.8341315985 - 0.8587107062 0.8669981956 0.8346257806 - 0.8591589928 0.8674265742 0.8351199031 - 0.8596050143 0.8678531051 0.8356130719 - 0.8600503206 0.8682789803 0.8361061215 - 0.8604956269 0.8687049150 0.8365991116 - 0.8609408736 0.8691307902 0.8370921016 - 0.8613861799 0.8695567846 0.8375850916 - 0.8618302941 0.8699809909 0.8380767703 - 0.8622733951 0.8704040051 0.8385676146 - 0.8627164960 0.8708269000 0.8390582800 - 0.8631595969 0.8712497950 0.8395491242 - 0.8636026978 0.8716726899 0.8400399089 - 0.8640447855 0.8720946908 0.8405296803 - 0.8644844890 0.8725144863 0.8410171270 - 0.8649240732 0.8729342222 0.8415045738 - 0.8653637171 0.8733540177 0.8419920206 - 0.8658034205 0.8737736940 0.8424795270 - 0.8662430048 0.8741934896 0.8429669142 - 0.8666800261 0.8746116757 0.8434523940 - 0.8671169877 0.8750299215 0.8439378142 - 0.8675538898 0.8754481077 0.8444231749 - 0.8679907918 0.8758662939 0.8449085951 - 0.8684278131 0.8762844801 0.8453940153 - 0.8688635230 0.8767014146 0.8458780050 - 0.8692985177 0.8771173954 0.8463612199 - 0.8697335124 0.8775334954 0.8468443155 - 0.8701685071 0.8779495955 0.8473274708 - 0.8706035018 0.8783656955 0.8478106856 - 0.8710376024 0.8787810802 0.8482931852 - 0.8714686036 0.8791939020 0.8487735987 - 0.8718994856 0.8796067834 0.8492540121 - 0.8723304272 0.8800197244 0.8497344255 - 0.8727613091 0.8804326057 0.8502147794 - 0.8731923103 0.8808454275 0.8506951928 - 0.8736202717 0.8812555075 0.8511731029 - 0.8740472794 0.8816646934 0.8516504765 - 0.8744744062 0.8820738792 0.8521277905 - 0.8749014735 0.8824831247 0.8526051044 - 0.8753286004 0.8828923106 0.8530824184 - 0.8757551908 0.8833009005 0.8535591960 - 0.8761808276 0.8837080002 0.8540347815 - 0.8766062856 0.8841150999 0.8545104265 - 0.8770318031 0.8845221996 0.8549860120 - 0.8774573207 0.8849292994 0.8554615974 - 0.8778828979 0.8853362799 0.8559371829 - 0.8783056736 0.8857411146 0.8564112782 - 0.8787276149 0.8861449957 0.8568848968 - 0.8791493773 0.8865488768 0.8573585153 - 0.8795713186 0.8869528770 0.8578320742 - 0.8799932003 0.8873568177 0.8583056927 - 0.8804144263 0.8877601027 0.8587788939 - 0.8808318973 0.8881598115 0.8592500091 - 0.8812494874 0.8885595798 0.8597210050 - 0.8816670775 0.8889592886 0.8601921201 - 0.8820847273 0.8893591166 0.8606631160 - 0.8825021982 0.8897588849 0.8611341715 - 0.8829184175 0.8901572227 0.8616037965 - 0.8833333850 0.8905543089 0.8620721102 - 0.8837484121 0.8909513950 0.8625404835 - 0.8841633797 0.8913484216 0.8630087972 - 0.8845784068 0.8917455077 0.8634771705 - 0.8849933743 0.8921425939 0.8639456034 - 0.8854057193 0.8925368786 0.8644126058 - 0.8858175278 0.8929308057 0.8648794293 - 0.8862293959 0.8933246732 0.8653463125 - 0.8866413236 0.8937187195 0.8658130765 - 0.8870530725 0.8941125870 0.8662800193 - 0.8874645233 0.8945059180 0.8667466044 - 0.8878725767 0.8948956132 0.8672116995 - 0.8882806897 0.8952853084 0.8676767945 - 0.8886888027 0.8956750035 0.8681420088 - 0.8890969157 0.8960648179 0.8686071038 - 0.8895050287 0.8964545131 0.8690721989 - 0.8899124265 0.8968430161 0.8695368171 - 0.8903183937 0.8972294927 0.8700006008 - 0.8907244802 0.8976160884 0.8704643250 - 0.8911305070 0.8980026245 0.8709279895 - 0.8915365934 0.8983891010 0.8713917136 - 0.8919426203 0.8987755775 0.8718554974 - 0.8923472166 0.8991609216 0.8723185062 - 0.8927506208 0.8995451927 0.8727809787 - 0.8931540847 0.8999295235 0.8732435703 - 0.8935574889 0.9003137946 0.8737061024 - 0.8939608932 0.9006981850 0.8741686940 - 0.8943642974 0.9010825157 0.8746312261 - 0.8947654963 0.9014645815 0.8750917912 - 0.8951656818 0.9018456936 0.8755515218 - 0.8955659270 0.9022269249 0.8760113120 - 0.8959661126 0.9026079774 0.8764711022 - 0.8963662982 0.9029892087 0.8769308925 - 0.8967664838 0.9033703208 0.8773906231 - 0.8971654773 0.9037498236 0.8778483868 - 0.8975641727 0.9041286707 0.8783056736 - 0.8979628086 0.9045075774 0.8787629008 - 0.8983613849 0.9048866034 0.8792201877 - 0.8987600803 0.9052655101 0.8796774745 - 0.8991587162 0.9056444168 0.8801347017 - 0.8995556235 0.9060223103 0.8805907965 - 0.8999521136 0.9064000249 0.8810467124 - 0.9003486037 0.9067776799 0.8815026283 - 0.9007450938 0.9071552753 0.8819584250 - 0.9011415839 0.9075329900 0.8824142814 - 0.9015380740 0.9079107046 0.8828700781 - 0.9019324183 0.9082859159 0.8833250999 - 0.9023262262 0.9086604714 0.8837798834 - 0.9027199149 0.9090350866 0.8842347264 - 0.9031137228 0.9094097018 0.8846895099 - 0.9035074711 0.9097843170 0.8851444125 - 0.9039012790 0.9101588726 0.8855991960 - 0.9042941928 0.9105305076 0.8860530257 - 0.9046869278 0.9109010100 0.8865063787 - 0.9050796032 0.9112715721 0.8869599104 - 0.9054722786 0.9116421938 0.8874133229 - 0.9058650136 0.9120126963 0.8878667951 - 0.9062576890 0.9123833179 0.8883202076 - 0.9066494107 0.9127531052 0.8887727261 - 0.9070404172 0.9131224751 0.8892245293 - 0.9074314237 0.9134919047 0.8896763921 - 0.9078223705 0.9138612747 0.8901283145 - 0.9082133770 0.9142307043 0.8905801773 - 0.9086043835 0.9146000147 0.8910320997 - 0.9089940190 0.9149683714 0.8914834261 - 0.9093819857 0.9153357148 0.8919339776 - 0.9097698927 0.9157028794 0.8923847079 - 0.9101579189 0.9160702229 0.8928353190 - 0.9105458856 0.9164373875 0.8932858706 - 0.9109339118 0.9168046713 0.8937366009 - 0.9113214016 0.9171708822 0.8941872120 - 0.9117074013 0.9175342917 0.8946375847 - 0.9120935202 0.9178975821 0.8950880170 - 0.9124795794 0.9182608724 0.8955383897 - 0.9128655791 0.9186242819 0.8959888220 - 0.9132516980 0.9189875722 0.8964391947 - 0.9136376977 0.9193509221 0.8968896866 - 0.9140226841 0.9197127223 0.8973388076 - 0.9144076109 0.9200745225 0.8977879286 - 0.9147924781 0.9204362035 0.8982371092 - 0.9151775241 0.9207980037 0.8986861706 - 0.9155623913 0.9211596847 0.8991352916 - 0.9159473181 0.9215214849 0.8995844126 - 0.9163296819 0.9218823910 0.9000319839 - 0.9167106748 0.9222429991 0.9004787207 - 0.9170917869 0.9226036072 0.9009253979 - 0.9174728990 0.9229642153 0.9013720751 - 0.9178540111 0.9233248234 0.9018188119 - 0.9182350039 0.9236853719 0.9022654891 - 0.9186152220 0.9240450263 0.9027118087 - 0.9189931750 0.9244018793 0.9031571150 - 0.9193711877 0.9247587919 0.9036024809 - 0.9197492003 0.9251158237 0.9040477872 - 0.9201272130 0.9254726768 0.9044930935 - 0.9205052257 0.9258295894 0.9049385190 - 0.9208831787 0.9261866212 0.9053838253 - 0.9212595224 0.9265406728 0.9058277011 - 0.9216355085 0.9268943071 0.9062713981 - 0.9220116138 0.9272480011 0.9067149758 - 0.9223875999 0.9276016951 0.9071586728 - 0.9227635860 0.9279553890 0.9076024294 - 0.9231395721 0.9283090830 0.9080460072 - 0.9235144258 0.9286624193 0.9084891081 - 0.9238871932 0.9290151000 0.9089311957 - 0.9242599010 0.9293677807 0.9093734026 - 0.9246327281 0.9297205210 0.9098154902 - 0.9250054955 0.9300732017 0.9102576971 - 0.9253782034 0.9304258823 0.9106997848 - 0.9257509708 0.9307786226 0.9111419916 - 0.9261215925 0.9311289787 0.9115830064 - 0.9264919162 0.9314789772 0.9120237827 - 0.9268621802 0.9318289757 0.9124646187 - 0.9272325039 0.9321790934 0.9129055142 - 0.9276028275 0.9325290918 0.9133462906 - 0.9279729724 0.9328790903 0.9137871265 - 0.9283429980 0.9332283139 0.9142274857 - 0.9287117720 0.9335752726 0.9146667123 - 0.9290807247 0.9339222908 0.9151058793 - 0.9294496179 0.9342694283 0.9155451059 - 0.9298185110 0.9346163869 0.9159842730 - 0.9301874042 0.9349634051 0.9164234996 - 0.9305561781 0.9353104234 0.9168627262 - 0.9309234023 0.9356566072 0.9173014760 - 0.9312896132 0.9360023737 0.9177401066 - 0.9316558242 0.9363480806 0.9181786180 - 0.9320219755 0.9366937876 0.9186171889 - 0.9323881865 0.9370396137 0.9190558195 - 0.9327543974 0.9373853207 0.9194943905 - 0.9331206083 0.9377310872 0.9199329019 - 0.9334837794 0.9380753040 0.9203705788 - 0.9338467121 0.9384195209 0.9208081961 - 0.9342097044 0.9387636185 0.9212458730 - 0.9345725775 0.9391077757 0.9216834903 - 0.9349356294 0.9394518733 0.9221211076 - 0.9352985024 0.9397959709 0.9225587249 - 0.9356611967 0.9401394725 0.9229962230 - 0.9360224009 0.9404795766 0.9234330058 - 0.9363836050 0.9408196807 0.9238699079 - 0.9367448092 0.9411597848 0.9243066907 - 0.9371060133 0.9414998889 0.9247435927 - 0.9374672771 0.9418399930 0.9251803756 - 0.9378284812 0.9421800971 0.9256172776 - 0.9381889105 0.9425194263 0.9260538816 - 0.9385479093 0.9428572059 0.9264901876 - 0.9389069080 0.9431951046 0.9269264936 - 0.9392657876 0.9435328841 0.9273626804 - 0.9396247864 0.9438707232 0.9277989864 - 0.9399837852 0.9442085028 0.9282351732 - 0.9403427839 0.9445464015 0.9286714792 - 0.9407004118 0.9448838830 0.9291065931 - 0.9410564899 0.9452211857 0.9295405746 - 0.9414126873 0.9455584288 0.9299746752 - 0.9417688847 0.9458956718 0.9304087162 - 0.9421250820 0.9462329149 0.9308426976 - 0.9424812794 0.9465702176 0.9312766790 - 0.9428374767 0.9469074011 0.9317107797 - 0.9431930184 0.9472429156 0.9321432114 - 0.9435479045 0.9475771785 0.9325743914 - 0.9439027905 0.9479115009 0.9330056906 - 0.9442576766 0.9482458234 0.9334369898 - 0.9446126223 0.9485800862 0.9338682294 - 0.9449675083 0.9489144087 0.9342995286 - 0.9453223944 0.9492486715 0.9347308278 - 0.9456762075 0.9495813847 0.9351599216 - 0.9460293055 0.9499132037 0.9355877042 - 0.9463824034 0.9502449036 0.9360154867 - 0.9467355013 0.9505766034 0.9364433289 - 0.9470885992 0.9509083033 0.9368711114 - 0.9474416971 0.9512400031 0.9372988939 - 0.9477947950 0.9515718222 0.9377266169 - 0.9481452107 0.9519032240 0.9381518960 - 0.9484937191 0.9522345066 0.9385753274 - 0.9488422275 0.9525657892 0.9389986992 - 0.9491906166 0.9528970718 0.9394220710 - 0.9495391250 0.9532284141 0.9398453832 - 0.9498875141 0.9535596967 0.9402688146 - 0.9502360225 0.9538909793 0.9406921864 - 0.9505816102 0.9542205930 0.9411129951 - 0.9509239197 0.9545481801 0.9415308833 - 0.9512662292 0.9548758268 0.9419488907 - 0.9516084790 0.9552034140 0.9423667789 - 0.9519507885 0.9555310011 0.9427847266 - 0.9522932172 0.9558585882 0.9432026148 - 0.9526355267 0.9561861753 0.9436205029 - 0.9529765248 0.9565111995 0.9440370202 - 0.9533149004 0.9568306208 0.9444504976 - 0.9536532164 0.9571499825 0.9448639154 - 0.9539914727 0.9574694037 0.9452773929 - 0.9543299079 0.9577888250 0.9456908107 - 0.9546682239 0.9581081867 0.9461042881 - 0.9550064802 0.9584276080 0.9465177059 - 0.9553446174 0.9587466121 0.9469308853 - 0.9556806087 0.9590610266 0.9473422170 - 0.9560164809 0.9593753815 0.9477534294 - 0.9563524723 0.9596897960 0.9481647015 - 0.9566884041 0.9600043297 0.9485759735 - 0.9570243955 0.9603186846 0.9489871860 - 0.9573603272 0.9606330991 0.9493985176 - 0.9576963186 0.9609475136 0.9498096704 - 0.9580307007 0.9612603188 0.9502192140 - 0.9583647251 0.9615727067 0.9506282806 - 0.9586988091 0.9618852139 0.9510374069 - 0.9590328932 0.9621976018 0.9514464736 - 0.9593669176 0.9625099897 0.9518555999 - 0.9597010016 0.9628223777 0.9522647262 - 0.9600350857 0.9631348252 0.9526737928 - 0.9603679180 0.9634457231 0.9530814290 - 0.9606994987 0.9637547135 0.9534872770 - 0.9610310793 0.9640638232 0.9538931847 - 0.9613626003 0.9643728733 0.9542990923 - 0.9616941810 0.9646819830 0.9547049999 - 0.9620258212 0.9649910927 0.9551109076 - 0.9623572826 0.9653000832 0.9555168152 - 0.9626886845 0.9656090140 0.9559224248 - 0.9630175829 0.9659156799 0.9563245773 - 0.9633464813 0.9662224054 0.9567267895 - 0.9636753798 0.9665290713 0.9571290016 - 0.9640042782 0.9668357968 0.9575312138 - 0.9643331766 0.9671425223 0.9579333067 - 0.9646620750 0.9674491882 0.9583355188 - 0.9649909735 0.9677559137 0.9587376714 - 0.9653183818 0.9680610895 0.9591376185 - 0.9656450152 0.9683653712 0.9595360160 - 0.9659715295 0.9686697125 0.9599344730 - 0.9662981033 0.9689739943 0.9603329897 - 0.9666246176 0.9692782760 0.9607315063 - 0.9669511914 0.9695824981 0.9611299038 - 0.9672777057 0.9698867798 0.9615284204 - 0.9676039219 0.9701907039 0.9619265199 - 0.9679273963 0.9704914093 0.9623209834 - 0.9682508111 0.9707919955 0.9627156258 - 0.9685742855 0.9710927010 0.9631102085 - 0.9688977003 0.9713934064 0.9635047913 - 0.9692211747 0.9716939926 0.9638993740 - 0.9695447087 0.9719946980 0.9642940164 - 0.9698681235 0.9722954035 0.9646884799 - 0.9701895118 0.9725940824 0.9650804996 - 0.9705088735 0.9728909731 0.9654701948 - 0.9708284140 0.9731879234 0.9658597708 - 0.9711478949 0.9734848142 0.9662495255 - 0.9714673162 0.9737817049 0.9666391015 - 0.9717867970 0.9740785956 0.9670287967 - 0.9721062183 0.9743754864 0.9674183726 - 0.9724256992 0.9746723771 0.9678081274 - 0.9727420211 0.9749661088 0.9681929946 - 0.9730579257 0.9752594233 0.9685772061 - 0.9733737707 0.9755526781 0.9689614773 - 0.9736896157 0.9758459926 0.9693456888 - 0.9740055203 0.9761393070 0.9697300196 - 0.9743214250 0.9764326215 0.9701141715 - 0.9746373296 0.9767258763 0.9704985023 - 0.9749523997 0.9770184755 0.9708817005 - 0.9752640724 0.9773074985 0.9712603092 - 0.9755758047 0.9775965214 0.9716387987 - 0.9758874774 0.9778854847 0.9720172882 - 0.9761992097 0.9781745076 0.9723957777 - 0.9765108824 0.9784634709 0.9727743268 - 0.9768224955 0.9787524939 0.9731528163 - 0.9771342278 0.9790415168 0.9735313058 - 0.9774439931 0.9793285728 0.9739072919 - 0.9777507782 0.9796128273 0.9742792845 - 0.9780576229 0.9798970222 0.9746512175 - 0.9783642888 0.9801810980 0.9750232100 - 0.9786710739 0.9804652929 0.9753952026 - 0.9789779186 0.9807494879 0.9757671952 - 0.9792847037 0.9810336828 0.9761391282 - 0.9795914888 0.9813178778 0.9765111208 - 0.9798954725 0.9815995097 0.9768790007 - 0.9801974297 0.9818789959 0.9772434831 - 0.9804993272 0.9821584225 0.9776080847 - 0.9808012247 0.9824379086 0.9779726267 - 0.9811030030 0.9827173948 0.9783372283 - 0.9814049006 0.9829968810 0.9787017703 - 0.9817067981 0.9832764268 0.9790663123 - 0.9820086956 0.9835559130 0.9794309139 - 0.9823070168 0.9838317037 0.9797903895 - 0.9826034904 0.9841055870 0.9801471829 - 0.9828999043 0.9843794703 0.9805040956 - 0.9831963778 0.9846534133 0.9808608890 - 0.9834929109 0.9849272966 0.9812176824 - 0.9837893248 0.9852011800 0.9815745950 - 0.9840857983 0.9854751229 0.9819313884 - 0.9843822718 0.9857488871 0.9822881818 - 0.9846748710 0.9860187769 0.9826393127 - 0.9849656820 0.9862868190 0.9829875827 - 0.9852566123 0.9865549207 0.9833359718 - 0.9855474234 0.9868229032 0.9836843014 - 0.9858381748 0.9870908856 0.9840325713 - 0.9861291051 0.9873589277 0.9843810201 - 0.9864199162 0.9876269102 0.9847292900 - 0.9867107272 0.9878948927 0.9850776196 - 0.9869980216 0.9881591797 0.9854195118 - 0.9872834086 0.9884213805 0.9857577085 - 0.9875686765 0.9886837006 0.9860960245 - 0.9878541231 0.9889459014 0.9864342213 - 0.9881393909 0.9892082214 0.9867724180 - 0.9884247780 0.9894704223 0.9871106744 - 0.9887101054 0.9897326827 0.9874488711 - 0.9889954925 0.9899948835 0.9877871871 - 0.9892771244 0.9902533889 0.9881182909 - 0.9895554185 0.9905087948 0.9884434938 - 0.9898337722 0.9907642007 0.9887685776 - 0.9901121855 0.9910196066 0.9890937209 - 0.9903905988 0.9912750125 0.9894189239 - 0.9906688929 0.9915304184 0.9897440076 - 0.9909473062 0.9917857051 0.9900690913 - 0.9912257195 0.9920411110 0.9903942943 - 0.9915013909 0.9922938943 0.9907138944 - 0.9917727113 0.9925420880 0.9910243154 - 0.9920439720 0.9927902818 0.9913346767 - 0.9923152924 0.9930384159 0.9916450977 - 0.9925866127 0.9932866096 0.9919555187 - 0.9928578734 0.9935348034 0.9922658801 - 0.9931291938 0.9937829971 0.9925763011 - 0.9934003949 0.9940311909 0.9928867221 - 0.9936708212 0.9942783713 0.9931951165 - 0.9939351082 0.9945191741 0.9934899211 - 0.9941995144 0.9947599173 0.9937847257 - 0.9944638014 0.9950006008 0.9940794110 - 0.9947280884 0.9952414036 0.9943742156 - 0.9949923754 0.9954820871 0.9946690202 - 0.9952567220 0.9957227707 0.9949638247 - 0.9955210090 0.9959635139 0.9952586293 - 0.9957852960 0.9962043166 0.9955533743 - 0.9960408211 0.9964358807 0.9958317876 - 0.9962943792 0.9966655970 0.9961066842 - 0.9965478778 0.9968951941 0.9963817000 - 0.9968014956 0.9971249104 0.9966565967 - 0.9970551133 0.9973546267 0.9969314933 - 0.9973086119 0.9975842237 0.9972063899 - 0.9975622296 0.9978138804 0.9974814057 - 0.9978157878 0.9980435967 0.9977563024 - 0.9980642200 0.9982674122 0.9980189800 - 0.9983060956 0.9984840155 0.9982665777 - 0.9985480905 0.9987004995 0.9985142946 - 0.9987900853 0.9989171028 0.9987618923 - 0.9990320802 0.9991337061 0.9990094900 - 0.9992740750 0.9993503094 0.9992570877 - 0.9995160103 0.9995667934 0.9995048046 - 0.9997580051 0.9997833967 0.9997524023 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0175220203 0.0159201808 0.0112829898 + 0.0302315596 0.0280031394 0.0208480097 + 0.0407330506 0.0385310091 0.0291486792 + 0.0501033999 0.0480892099 0.0366973914 + 0.0587283783 0.0569948703 0.0437196717 + 0.0668726563 0.0654532537 0.0503452383 + 0.0746254995 0.0734657720 0.0566753708 + 0.0821137205 0.0811266303 0.0627593473 + 0.0893652514 0.0885340869 0.0686344281 + 0.0964666009 0.0956818983 0.0743253529 + 0.1034414992 0.1026156992 0.0798535123 + 0.1102956012 0.1093306020 0.0852200314 + 0.1170141026 0.1157635972 0.0904216021 + 0.1235916987 0.1219805032 0.0954708308 + 0.1299463063 0.1280032992 0.1003969014 + 0.1360912025 0.1338655055 0.1052050963 + 0.1420111060 0.1395675987 0.1098947003 + 0.1477487981 0.1450989991 0.1144763976 + 0.1533298045 0.1504811943 0.1189825982 + 0.1587764025 0.1557295024 0.1233960986 + 0.1640834063 0.1608278006 0.1277330071 + 0.1692633927 0.1657993942 0.1319945008 + 0.1743295044 0.1706517041 0.1361728013 + 0.1792751998 0.1754034013 0.1402668059 + 0.1841226965 0.1800453961 0.1442912966 + 0.1888726950 0.1845912039 0.1482613981 + 0.1935237944 0.1890432984 0.1521809995 + 0.1980873048 0.1934227943 0.1560502052 + 0.2025565058 0.1977252066 0.1598621011 + 0.2069393992 0.2019387037 0.1636209041 + 0.2112462074 0.2060763985 0.1673309952 + 0.2154687047 0.2101363987 0.1709980965 + 0.2196132988 0.2141312063 0.1746173948 + 0.2236949950 0.2180649042 0.1781916022 + 0.2277055979 0.2219256014 0.1817201972 + 0.2316590995 0.2257197052 0.1852061003 + 0.2355517000 0.2294515073 0.1886554062 + 0.2393918931 0.2331303954 0.1920740008 + 0.2431806028 0.2367579043 0.1954559982 + 0.2469168007 0.2403406054 0.1987928003 + 0.2505995929 0.2438685000 0.2020907998 + 0.2542274892 0.2473488003 0.2053564936 + 0.2578184903 0.2507809997 0.2085936964 + 0.2613571882 0.2541683912 0.2117979974 + 0.2648550868 0.2575106919 0.2149727941 + 0.2683109939 0.2608149052 0.2181185931 + 0.2717253864 0.2640857995 0.2212391943 + 0.2750979960 0.2673158050 0.2243300974 + 0.2784220874 0.2705042958 0.2273901999 + 0.2817071974 0.2736572921 0.2304214984 + 0.2849532962 0.2767716050 0.2334305048 + 0.2881663144 0.2798540890 0.2364110053 + 0.2913444042 0.2829042077 0.2393691987 + 0.2944832146 0.2859222889 0.2423065007 + 0.2975896001 0.2889142036 0.2452152967 + 0.3006660938 0.2918727994 0.2480936944 + 0.3037149906 0.2947992980 0.2509461045 + 0.3067280948 0.2976973951 0.2537800968 + 0.3097102046 0.3005683124 0.2565920949 + 0.3126625121 0.3034102917 0.2593840063 + 0.3155854940 0.3062278926 0.2621518970 + 0.3184789121 0.3090203106 0.2648904026 + 0.3213432133 0.3117856979 0.2676062882 + 0.3241868913 0.3145250082 0.2703030109 + 0.3270075023 0.3172332048 0.2729845047 + 0.3298006058 0.3199186921 0.2756457031 + 0.3325676918 0.3225826025 0.2782875001 + 0.3353078067 0.3252244890 0.2809073031 + 0.3380177915 0.3278487027 0.2835057080 + 0.3406989872 0.3304544091 0.2860867977 + 0.3433553874 0.3330389857 0.2886512876 + 0.3459919989 0.3356043994 0.2911956012 + 0.3486098945 0.3381497860 0.2937203944 + 0.3512117863 0.3406724036 0.2962253094 + 0.3537960052 0.3431720138 0.2987118959 + 0.3563596904 0.3456499875 0.3011856973 + 0.3589065075 0.3481073976 0.3036412895 + 0.3614340127 0.3505432010 0.3060742915 + 0.3639450073 0.3529601097 0.3084880114 + 0.3664397895 0.3553602993 0.3108876050 + 0.3689151108 0.3577440977 0.3132722974 + 0.3713718057 0.3601129949 0.3156406879 + 0.3738071918 0.3624683917 0.3179928064 + 0.3762201071 0.3648113012 0.3203321099 + 0.3786138892 0.3671433032 0.3226552010 + 0.3809883893 0.3694634140 0.3249565065 + 0.3833450079 0.3717713058 0.3272385001 + 0.3856788874 0.3740625978 0.3295018971 + 0.3879950047 0.3763391078 0.3317486942 + 0.3902960122 0.3786000907 0.3339833021 + 0.3925783932 0.3808402121 0.3362008929 + 0.3948448002 0.3830653131 0.3384015858 + 0.3970947862 0.3852767050 0.3405882120 + 0.3993253112 0.3874705136 0.3427591920 + 0.4015383124 0.3896510005 0.3449178934 + 0.4037337899 0.3918201923 0.3470610082 + 0.4059110880 0.3939774036 0.3491885066 + 0.4080676138 0.3961207867 0.3513003886 + 0.4102075100 0.3982515931 0.3533968031 + 0.4123331010 0.4003694952 0.3554781973 + 0.4144445062 0.4024741054 0.3575446904 + 0.4165400863 0.4045642912 0.3595972955 + 0.4186260104 0.4066396058 0.3616338074 + 0.4207012057 0.4087007046 0.3636533022 + 0.4227670133 0.4107474089 0.3656555116 + 0.4248233140 0.4127795100 0.3676426113 + 0.4268693924 0.4147967100 0.3696162105 + 0.4289070964 0.4167988002 0.3715760112 + 0.4309335947 0.4187847972 0.3735184968 + 0.4329519868 0.4207592905 0.3754445016 + 0.4349603057 0.4227212965 0.3773564994 + 0.4369597137 0.4246717989 0.3792560101 + 0.4389463067 0.4266099930 0.3811441064 + 0.4409210086 0.4285362065 0.3830193877 + 0.4428839087 0.4304524064 0.3848828077 + 0.4448337853 0.4323602021 0.3867362142 + 0.4467687905 0.4342601001 0.3885799944 + 0.4486894906 0.4361515045 0.3904125988 + 0.4505963922 0.4380353987 0.3922336102 + 0.4524900913 0.4399108887 0.3940415084 + 0.4543710947 0.4417791069 0.3958359063 + 0.4562399089 0.4436390996 0.3976179957 + 0.4580954909 0.4454919100 0.3993878961 + 0.4599367976 0.4473369122 0.4011451006 + 0.4617645144 0.4491747022 0.4028908908 + 0.4635792077 0.4510050118 0.4046252966 + 0.4653801918 0.4528281987 0.4063490927 + 0.4671661854 0.4546442926 0.4080632031 + 0.4689376950 0.4564531147 0.4097684920 + 0.4706963003 0.4582554102 0.4114660919 + 0.4724414945 0.4600501955 0.4131549895 + 0.4741756022 0.4618388116 0.4148359001 + 0.4758949876 0.4636174142 0.4165103137 + 0.4776011109 0.4653869867 0.4181793034 + 0.4792934954 0.4671463966 0.4198431969 + 0.4809749126 0.4688977003 0.4215008914 + 0.4826433957 0.4706400037 0.4231525958 + 0.4842990041 0.4723741114 0.4247975051 + 0.4859426916 0.4740991890 0.4264369905 + 0.4875749052 0.4758136868 0.4280720055 + 0.4891946912 0.4775179923 0.4297009110 + 0.4908007979 0.4792113900 0.4313251078 + 0.4923956096 0.4808942974 0.4329412878 + 0.4939799905 0.4825680852 0.4345523119 + 0.4955542982 0.4842331111 0.4361563027 + 0.4971171916 0.4858896136 0.4377534986 + 0.4986675084 0.4875380993 0.4393444955 + 0.5002059937 0.4891777039 0.4409264028 + 0.5017321706 0.4908088148 0.4425007105 + 0.5032498837 0.4924311936 0.4440687895 + 0.5047596097 0.4940446913 0.4456312060 + 0.5062615871 0.4956491888 0.4471881092 + 0.5077568889 0.4972448051 0.4487377107 + 0.5092430115 0.4988301992 0.4502801001 + 0.5107212067 0.5004084110 0.4518156946 + 0.5121917129 0.5019794703 0.4533447921 + 0.5136547089 0.5035430789 0.4548679888 + 0.5151066184 0.5050961971 0.4563845992 + 0.5165513754 0.5066397786 0.4578959048 + 0.5179892778 0.5081757903 0.4594022036 + 0.5194200277 0.5097053051 0.4609036148 + 0.5208390951 0.5112237930 0.4623981118 + 0.5222476125 0.5127363205 0.4638865888 + 0.5236480236 0.5142416954 0.4653699994 + 0.5250409842 0.5157390833 0.4668483138 + 0.5264229774 0.5172262788 0.4683186114 + 0.5277984142 0.5187063217 0.4697833955 + 0.5291666985 0.5201799870 0.4712427855 + 0.5305246711 0.5216469765 0.4726960957 + 0.5318741798 0.5231056809 0.4741415083 + 0.5332177877 0.5245596170 0.4755817056 + 0.5345557928 0.5260090232 0.4770165980 + 0.5358840227 0.5274482965 0.4784426987 + 0.5372061729 0.5288820863 0.4798631072 + 0.5385223031 0.5303105712 0.4812782109 + 0.5398281813 0.5317319036 0.4826853871 + 0.5411273837 0.5331465006 0.4840855002 + 0.5424209237 0.5345557928 0.4854811132 + 0.5437052250 0.5359572768 0.4868715107 + 0.5449833870 0.5373530984 0.4882541895 + 0.5462568998 0.5387449265 0.4896318913 + 0.5475227237 0.5401304960 0.4910022914 + 0.5487810969 0.5415118933 0.4923650026 + 0.5500348210 0.5428903103 0.4937227964 + 0.5512812138 0.5442638993 0.4950731993 + 0.5525211096 0.5456309915 0.4964149892 + 0.5537570119 0.5469937921 0.4977517128 + 0.5549864769 0.5483481884 0.4990803897 + 0.5562115908 0.5496984124 0.5004032850 + 0.5574334264 0.5510452986 0.5017220974 + 0.5586463809 0.5523828268 0.5030320883 + 0.5598551035 0.5537164807 0.5043380260 + 0.5610579252 0.5550454259 0.5056391954 + 0.5622550249 0.5563679934 0.5069329143 + 0.5634492040 0.5576872230 0.5082232952 + 0.5646370053 0.5589987040 0.5095074773 + 0.5658180714 0.5603044033 0.5107867122 + 0.5669947863 0.5616064072 0.5120629072 + 0.5681625009 0.5628957748 0.5133314133 + 0.5693277121 0.5641813874 0.5145972967 + 0.5704880953 0.5654600263 0.5158582926 + 0.5716452003 0.5667315722 0.5171144009 + 0.5728005171 0.5679996014 0.5183681846 + 0.5739479065 0.5692597032 0.5196151137 + 0.5750917792 0.5705173016 0.5208598971 + 0.5762277842 0.5717689991 0.5221000910 + 0.5773584247 0.5730172992 0.5233365297 + 0.5784850717 0.5742632151 0.5245702863 + 0.5796056986 0.5755022764 0.5257986784 + 0.5807244778 0.5767388940 0.5270252228 + 0.5818371773 0.5779654980 0.5282452703 + 0.5829467773 0.5791882277 0.5294620991 + 0.5840514898 0.5804045796 0.5306738019 + 0.5851526856 0.5816155076 0.5318812132 + 0.5862517953 0.5828229189 0.5330861211 + 0.5873453021 0.5840238929 0.5342842937 + 0.5884373188 0.5852233768 0.5354809761 + 0.5895215273 0.5864148736 0.5366703868 + 0.5906035900 0.5876041055 0.5378572941 + 0.5916786194 0.5887857080 0.5390371084 + 0.5927507281 0.5899624825 0.5402138233 + 0.5938187242 0.5911325812 0.5413867235 + 0.5948839784 0.5922974944 0.5425544977 + 0.5959472060 0.5934585929 0.5437186956 + 0.5970050097 0.5946133137 0.5448786020 + 0.5980607271 0.5957654715 0.5460368991 + 0.5991076231 0.5969123244 0.5471885204 + 0.6001530290 0.5980582237 0.5483391285 + 0.6011908054 0.5991981030 0.5494847894 + 0.6022279263 0.6003370881 0.5506296754 + 0.6032595038 0.6014683843 0.5517669916 + 0.6042903066 0.6025981903 0.5529031754 + 0.6053165793 0.6037194133 0.5540338755 + 0.6063408852 0.6048387885 0.5551629066 + 0.6073579788 0.6059514880 0.5562863946 + 0.6083726883 0.6070622802 0.5574082732 + 0.6093810201 0.6081678867 0.5585262775 + 0.6103867292 0.6092718244 0.5596421957 + 0.6113867760 0.6103717089 0.5607541203 + 0.6123839021 0.6114696264 0.5618643761 + 0.6133750081 0.6125634909 0.5629718900 + 0.6143637896 0.6136540174 0.5640769005 + 0.6153482199 0.6147373915 0.5651777983 + 0.6163303256 0.6158183813 0.5662764907 + 0.6173073053 0.6168941259 0.5673714280 + 0.6182823181 0.6179677844 0.5684645772 + 0.6192523837 0.6190360785 0.5695542097 + 0.6202210188 0.6201022863 0.5706425905 + 0.6211845875 0.6211615205 0.5717275143 + 0.6221469045 0.6222187877 0.5728116035 + 0.6231033206 0.6232674718 0.5738925934 + 0.6240592003 0.6243153811 0.5749726295 + 0.6250107288 0.6253551245 0.5760461092 + 0.6259621978 0.6263948083 0.5771194100 + 0.6269065142 0.6274272203 0.5781859756 + 0.6278507113 0.6284595728 0.5792527199 + 0.6287866235 0.6294867992 0.5803136826 + 0.6297214031 0.6305134296 0.5813742280 + 0.6306506991 0.6315355897 0.5824303031 + 0.6315780878 0.6325563788 0.5834851861 + 0.6325017214 0.6335713863 0.5845344067 + 0.6334224939 0.6345824003 0.5855802298 + 0.6343404055 0.6355903149 0.5866221189 + 0.6352542043 0.6365941763 0.5876594186 + 0.6361662745 0.6375966072 0.5886945724 + 0.6370725036 0.6385940909 0.5897240043 + 0.6379784942 0.6395912170 0.5907528996 + 0.6388779283 0.6405804753 0.5917738080 + 0.6397773027 0.6415697932 0.5927945971 + 0.6406729221 0.6425516009 0.5938079953 + 0.6415677071 0.6435316801 0.5948200226 + 0.6424599886 0.6445075870 0.5958276987 + 0.6433506012 0.6454802752 0.5968323946 + 0.6442388892 0.6464512944 0.5978347063 + 0.6451222897 0.6474183798 0.5988320112 + 0.6460055113 0.6483852863 0.5998288989 + 0.6468821764 0.6493440866 0.6008189917 + 0.6477587819 0.6503030062 0.6018090248 + 0.6486309767 0.6512538791 0.6027926207 + 0.6495016217 0.6522020102 0.6037741899 + 0.6503694057 0.6531463265 0.6047533154 + 0.6512334943 0.6540852189 0.6057292223 + 0.6520972252 0.6550235748 0.6067042947 + 0.6529569030 0.6559551954 0.6076720953 + 0.6538167000 0.6568868160 0.6086397767 + 0.6546720862 0.6578143239 0.6096029878 + 0.6555258036 0.6587402821 0.6105644703 + 0.6563767791 0.6596642733 0.6115236282 + 0.6572223902 0.6605846882 0.6124786735 + 0.6580681205 0.6615051031 0.6134337783 + 0.6589081883 0.6624196172 0.6143844128 + 0.6597480774 0.6633337140 0.6153349876 + 0.6605852842 0.6642439961 0.6162821054 + 0.6614202261 0.6651508808 0.6172264814 + 0.6622543931 0.6660568714 0.6181699038 + 0.6630814075 0.6669554114 0.6191053987 + 0.6639084816 0.6678538918 0.6200408936 + 0.6647310257 0.6687464714 0.6209712029 + 0.6655508876 0.6696357131 0.6218988895 + 0.6663697958 0.6705237031 0.6228252053 + 0.6671835184 0.6714056134 0.6237459183 + 0.6679971218 0.6722874045 0.6246666908 + 0.6688064933 0.6731655002 0.6255832911 + 0.6696135998 0.6740418077 0.6264979839 + 0.6704198122 0.6749172211 0.6274114847 + 0.6712203026 0.6757869720 0.6283187866 + 0.6720207930 0.6766569018 0.6292260289 + 0.6728178263 0.6775221825 0.6301295757 + 0.6736124158 0.6783844233 0.6310310960 + 0.6744068861 0.6792463064 0.6319320798 + 0.6751977801 0.6801002026 0.6328263879 + 0.6759887934 0.6809539795 0.6337206960 + 0.6767783165 0.6818047762 0.6346113086 + 0.6775661707 0.6826521158 0.6354981065 + 0.6783542037 0.6834993958 0.6363847852 + 0.6791371703 0.6843413115 0.6372650266 + 0.6799193025 0.6851823926 0.6381441951 + 0.6807003021 0.6860218048 0.6390221119 + 0.6814774275 0.6868560910 0.6398959160 + 0.6822544932 0.6876903176 0.6407696009 + 0.6830291152 0.6885216832 0.6416400075 + 0.6838018894 0.6893510818 0.6425083876 + 0.6845747232 0.6901804805 0.6433767080 + 0.6853433847 0.6910036206 0.6442382932 + 0.6861116886 0.6918261051 0.6450995803 + 0.6868789792 0.6926466823 0.6459587216 + 0.6876426935 0.6934604049 0.6468116045 + 0.6884064078 0.6942741871 0.6476644874 + 0.6891673207 0.6950843930 0.6485136151 + 0.6899256110 0.6958913207 0.6493594050 + 0.6906839013 0.6966981888 0.6502050757 + 0.6914379001 0.6975011826 0.6510459781 + 0.6921907067 0.6983032227 0.6518856883 + 0.6929433942 0.6991050243 0.6527251005 + 0.6936904788 0.6999025941 0.6535598040 + 0.6944376230 0.7007001042 0.6543945074 + 0.6951832175 0.7014958858 0.6552277207 + 0.6959239244 0.7022861242 0.6560571194 + 0.6966645718 0.7030763030 0.6568865180 + 0.6974024773 0.7038636804 0.6577119827 + 0.6981366873 0.7046473026 0.6585326791 + 0.6988708973 0.7054308057 0.6593534946 + 0.6996024251 0.7062115073 0.6601701975 + 0.7003322840 0.7069903016 0.6609845161 + 0.7010622025 0.7077690959 0.6617987752 + 0.7017893791 0.7085446119 0.6626083255 + 0.7025159001 0.7093191147 0.6634163260 + 0.7032423019 0.7100936174 0.6642243266 + 0.7039636970 0.7108619809 0.6650285125 + 0.7046843171 0.7116296291 0.6658324003 + 0.7054049969 0.7123970985 0.6666362286 + 0.7061207294 0.7131581903 0.6674346924 + 0.7068364024 0.7139192224 0.6682332158 + 0.7075518966 0.7146798968 0.6690312028 + 0.7082635164 0.7154349089 0.6698222160 + 0.7089750171 0.7161899209 0.6706132889 + 0.7096859217 0.7169442177 0.6714035273 + 0.7103911042 0.7176927924 0.6721884012 + 0.7110964060 0.7184414864 0.6729732752 + 0.7118008137 0.7191895247 0.6737573743 + 0.7125003934 0.7199338078 0.6745371819 + 0.7131999731 0.7206779718 0.6753169894 + 0.7138990164 0.7214214206 0.6760962009 + 0.7145938277 0.7221598029 0.6768721938 + 0.7152886987 0.7228981256 0.6776481867 + 0.7159827948 0.7236354947 0.6784234047 + 0.7166706920 0.7243661880 0.6791931987 + 0.7173587084 0.7250968814 0.6799631119 + 0.7180461884 0.7258272171 0.6807324290 + 0.7187278867 0.7265521288 0.6814960837 + 0.7194095850 0.7272771001 0.6822597980 + 0.7200912833 0.7280020118 0.6830233932 + 0.7207676768 0.7287219763 0.6837813854 + 0.7214440703 0.7294420004 0.6845393777 + 0.7221205235 0.7301620245 0.6852974296 + 0.7227922082 0.7308751941 0.6860501766 + 0.7234632969 0.7315877080 0.6868025064 + 0.7241343856 0.7323002219 0.6875547767 + 0.7248032093 0.7330073118 0.6883025169 + 0.7254711986 0.7337129116 0.6890491247 + 0.7261391878 0.7344185114 0.6897956729 + 0.7268046141 0.7351201773 0.6905381083 + 0.7274683118 0.7358196974 0.6912782192 + 0.7281321287 0.7365190983 0.6920182705 + 0.7287933230 0.7372158170 0.6927558780 + 0.7294514775 0.7379088998 0.6934906840 + 0.7301098108 0.7386021018 0.6942256093 + 0.7307670712 0.7392935753 0.6949588060 + 0.7314220071 0.7399803996 0.6956874728 + 0.7320768237 0.7406671047 0.6964163184 + 0.7327315807 0.7413536906 0.6971448064 + 0.7333819270 0.7420334816 0.6978684068 + 0.7340322137 0.7427132130 0.6985918880 + 0.7346825004 0.7433928847 0.6993154287 + 0.7353283763 0.7440670729 0.7000336051 + 0.7359728813 0.7447395921 0.7007502913 + 0.7366175056 0.7454121113 0.7014670968 + 0.7372602820 0.7460821271 0.7021806240 + 0.7379010916 0.7467494011 0.7028908730 + 0.7385420203 0.7474167943 0.7036011219 + 0.7391821146 0.7480832934 0.7043104172 + 0.7398182154 0.7487462759 0.7050154209 + 0.7404543757 0.7494093180 0.7057204247 + 0.7410904765 0.7500723004 0.7064254284 + 0.7417215705 0.7507299781 0.7071260214 + 0.7423517108 0.7513865829 0.7078258991 + 0.7429817915 0.7520431280 0.7085257769 + 0.7436100841 0.7526969910 0.7092229724 + 0.7442362905 0.7533478141 0.7099171877 + 0.7448624969 0.7539986968 0.7106114030 + 0.7454884052 0.7546491027 0.7113050222 + 0.7461096048 0.7552949786 0.7119926810 + 0.7467309237 0.7559409142 0.7126803994 + 0.7473521233 0.7565869093 0.7133681774 + 0.7479693294 0.7572283149 0.7140517831 + 0.7485842705 0.7578672767 0.7147333026 + 0.7491992712 0.7585062981 0.7154148817 + 0.7498134971 0.7591440082 0.7160953879 + 0.7504243255 0.7597767115 0.7167720199 + 0.7510352135 0.7604094148 0.7174485922 + 0.7516459823 0.7610421181 0.7181252241 + 0.7522531152 0.7616708875 0.7187982202 + 0.7528588176 0.7622982264 0.7194700241 + 0.7534645200 0.7629255056 0.7201418281 + 0.7540687919 0.7635515928 0.7208123207 + 0.7546679974 0.7641733885 0.7214787006 + 0.7552672029 0.7647953033 0.7221450806 + 0.7558664083 0.7654170990 0.7228114009 + 0.7564626932 0.7660366893 0.7234749794 + 0.7570574880 0.7666552067 0.7241373062 + 0.7576522827 0.7672737241 0.7247995734 + 0.7582467794 0.7678917050 0.7254614234 + 0.7588381767 0.7685058117 0.7261199951 + 0.7594295740 0.7691199183 0.7267785072 + 0.7600209713 0.7697339058 0.7274370193 + 0.7606108189 0.7703450918 0.7280923128 + 0.7611988783 0.7709534764 0.7287446260 + 0.7617871165 0.7715619802 0.7293969989 + 0.7623751760 0.7721704245 0.7300493121 + 0.7629606128 0.7727757096 0.7306963205 + 0.7635456920 0.7733805180 0.7313426733 + 0.7641307116 0.7739853263 0.7319890857 + 0.7647150159 0.7745891213 0.7326344252 + 0.7652966976 0.7751892805 0.7332763076 + 0.7658783793 0.7757894993 0.7339181900 + 0.7664601207 0.7763897181 0.7345600724 + 0.7670400143 0.7769867778 0.7352002859 + 0.7676184773 0.7775810957 0.7358390093 + 0.7681970000 0.7781754732 0.7364776731 + 0.7687754035 0.7787699103 0.7371163964 + 0.7693514228 0.7793611288 0.7377526164 + 0.7699269056 0.7799516916 0.7383884192 + 0.7705023289 0.7805421948 0.7390242219 + 0.7710775137 0.7811324000 0.7396594882 + 0.7716495991 0.7817180753 0.7402899265 + 0.7722216845 0.7823038101 0.7409204245 + 0.7727938294 0.7828894854 0.7415509224 + 0.7733650208 0.7834734917 0.7421798110 + 0.7739340067 0.7840530872 0.7428051829 + 0.7745028734 0.7846328020 0.7434306741 + 0.7750719190 0.7852125168 0.7440561056 + 0.7756394148 0.7857902050 0.7446792126 + 0.7762054205 0.7863658071 0.7452998757 + 0.7767714262 0.7869415283 0.7459207177 + 0.7773373127 0.7875170708 0.7465413809 + 0.7779011726 0.7880907059 0.7471587062 + 0.7784639001 0.7886632085 0.7477741838 + 0.7790266275 0.7892357111 0.7483897805 + 0.7795892954 0.7898082137 0.7490053177 + 0.7801496983 0.7903760076 0.7496179938 + 0.7807093263 0.7909423709 0.7502297759 + 0.7812688947 0.7915087938 0.7508416176 + 0.7818285227 0.7920752168 0.7514533997 + 0.7823855877 0.7926365137 0.7520614862 + 0.7829421759 0.7931969762 0.7526692152 + 0.7834988832 0.7937574983 0.7532768846 + 0.7840554714 0.7943180203 0.7538844943 + 0.7846093178 0.7948756218 0.7544875145 + 0.7851626873 0.7954329848 0.7550902963 + 0.7857161760 0.7959904075 0.7556930184 + 0.7862696052 0.7965477705 0.7562956810 + 0.7868201733 0.7971013784 0.7568932176 + 0.7873706222 0.7976546884 0.7574905753 + 0.7879210114 0.7982081175 0.7580878735 + 0.7884714007 0.7987614870 0.7586852908 + 0.7890191078 0.7993094921 0.7592788935 + 0.7895665765 0.7998571992 0.7598723769 + 0.7901141047 0.8004049063 0.7604658008 + 0.7906615734 0.8009526134 0.7610592842 + 0.7912061810 0.8014960885 0.7616505027 + 0.7917506099 0.8020390272 0.7622414231 + 0.7922949195 0.8025820255 0.7628324032 + 0.7928392887 0.8031250238 0.7634233832 + 0.7933812141 0.8036649227 0.7640110850 + 0.7939224839 0.8042042255 0.7645980716 + 0.7944638729 0.8047435880 0.7651851177 + 0.7950052023 0.8052828908 0.7657721043 + 0.7955445051 0.8058183789 0.7663552165 + 0.7960829735 0.8063523769 0.7669368982 + 0.7966215014 0.8068864942 0.7675185800 + 0.7971600294 0.8074206114 0.7681002021 + 0.7976967096 0.8079521060 0.7686796188 + 0.7982320786 0.8084818125 0.7692573071 + 0.7987675071 0.8090115786 0.7698349953 + 0.7993028760 0.8095412850 0.7704126835 + 0.7998371124 0.8100696802 0.7709888816 + 0.8003696203 0.8105961084 0.7715629935 + 0.8009020090 0.8111224771 0.7721372247 + 0.8014343977 0.8116489053 0.7727112770 + 0.8019663095 0.8121743798 0.7732846141 + 0.8024960160 0.8126959801 0.7738549113 + 0.8030256033 0.8132175803 0.7744253278 + 0.8035553098 0.8137391806 0.7749956250 + 0.8040848970 0.8142607212 0.7755659819 + 0.8046116829 0.8147786260 0.7761322856 + 0.8051382899 0.8152961731 0.7766984105 + 0.8056647778 0.8158138990 0.7772645950 + 0.8061913848 0.8163315058 0.7778307796 + 0.8067160845 0.8168472052 0.7783944011 + 0.8072397709 0.8173617721 0.7789568901 + 0.8077635169 0.8178765178 0.7795193195 + 0.8082872033 0.8183912039 0.7800818086 + 0.8088098764 0.8189039230 0.7806432843 + 0.8093308210 0.8194131255 0.7812033892 + 0.8098517060 0.8199223280 0.7817634940 + 0.8103727102 0.8204315901 0.7823235989 + 0.8108935952 0.8209407926 0.7828835845 + 0.8114115000 0.8214446902 0.7834416032 + 0.8119295239 0.8219487071 0.7839996219 + 0.8124474287 0.8224527240 0.7845575809 + 0.8129652739 0.8229566813 0.7851157188 + 0.8134815097 0.8234592080 0.7856711745 + 0.8139966130 0.8239607215 0.7862251997 + 0.8145117164 0.8244621158 0.7867792845 + 0.8150267005 0.8249636292 0.7873333097 + 0.8155413270 0.8254641294 0.7878866196 + 0.8160536885 0.8259606957 0.7884370089 + 0.8165659904 0.8264572024 0.7889872789 + 0.8170784116 0.8269537091 0.7895377278 + 0.8175907731 0.8274502158 0.7900881171 + 0.8181009889 0.8279432058 0.7906360030 + 0.8186104298 0.8284350038 0.7911829948 + 0.8191198111 0.8289268017 0.7917301059 + 0.8196291924 0.8294185996 0.7922770977 + 0.8201377988 0.8299099207 0.7928233147 + 0.8206434250 0.8303995728 0.7933669090 + 0.8211489916 0.8308892846 0.7939103842 + 0.8216546178 0.8313789964 0.7944539785 + 0.8221603036 0.8318687081 0.7949975133 + 0.8226630092 0.8323553205 0.7955383062 + 0.8231644034 0.8328405023 0.7960780263 + 0.8236657977 0.8333258033 0.7966176867 + 0.8241671920 0.8338109851 0.7971572876 + 0.8246682286 0.8342955709 0.7976964116 + 0.8251668215 0.8347756267 0.7982317209 + 0.8256654739 0.8352555037 0.7987669706 + 0.8261641264 0.8357354999 0.7993022203 + 0.8266627192 0.8362154961 0.7998375297 + 0.8271595240 0.8366938233 0.8003703952 + 0.8276544809 0.8371704817 0.8009009957 + 0.8281494975 0.8376471996 0.8014315963 + 0.8286445141 0.8381239176 0.8019623160 + 0.8291395903 0.8386006951 0.8024929166 + 0.8296310902 0.8390740752 0.8030207157 + 0.8301221132 0.8395469785 0.8035480976 + 0.8306130767 0.8400198221 0.8040754199 + 0.8311039805 0.8404927254 0.8046028018 + 0.8315945864 0.8409647942 0.8051295280 + 0.8320829272 0.8414331079 0.8056535125 + 0.8325713277 0.8419014215 0.8061774969 + 0.8330597281 0.8423696160 0.8067014217 + 0.8335480094 0.8428379297 0.8072254062 + 0.8340349197 0.8433049917 0.8077479005 + 0.8345198035 0.8437706828 0.8082687855 + 0.8350048065 0.8442363143 0.8087896109 + 0.8354896903 0.8447020054 0.8093103766 + 0.8359745741 0.8451675773 0.8098313212 + 0.8364562988 0.8456311226 0.8103501797 + 0.8369364142 0.8460934758 0.8108682036 + 0.8374165297 0.8465558887 0.8113862276 + 0.8378965259 0.8470183015 0.8119041920 + 0.8383765817 0.8474807143 0.8124222159 + 0.8388531804 0.8479387760 0.8129376173 + 0.8393293023 0.8483963013 0.8134527206 + 0.8398053050 0.8488537073 0.8139677048 + 0.8402814269 0.8493111730 0.8144826889 + 0.8407574296 0.8497685194 0.8149976730 + 0.8412302732 0.8502224088 0.8155111074 + 0.8417032957 0.8506761789 0.8160244226 + 0.8421761990 0.8511301279 0.8165377975 + 0.8426492214 0.8515838981 0.8170511127 + 0.8431215286 0.8520373702 0.8175640702 + 0.8435900211 0.8524886966 0.8180745840 + 0.8440583944 0.8529400229 0.8185852170 + 0.8445268869 0.8533912897 0.8190956712 + 0.8449953198 0.8538426161 0.8196063042 + 0.8454630971 0.8542929888 0.8201159835 + 0.8459277749 0.8547400832 0.8206226230 + 0.8463925719 0.8551871181 0.8211292028 + 0.8468574286 0.8556342125 0.8216357827 + 0.8473222256 0.8560811877 0.8221424818 + 0.8477864265 0.8565273881 0.8226484060 + 0.8482484221 0.8569706082 0.8231524229 + 0.8487104177 0.8574137092 0.8236563802 + 0.8491724133 0.8578569293 0.8241603971 + 0.8496344090 0.8583000898 0.8246644139 + 0.8500953913 0.8587425947 0.8251677155 + 0.8505532742 0.8591822982 0.8256688714 + 0.8510112166 0.8596221209 0.8261700869 + 0.8514690995 0.8600618243 0.8266713023 + 0.8519269824 0.8605015874 0.8271725178 + 0.8523843288 0.8609408140 0.8276733160 + 0.8528388143 0.8613771200 0.8281720281 + 0.8532934189 0.8618134856 0.8286706805 + 0.8537480235 0.8622499108 0.8291695118 + 0.8542025089 0.8626862764 0.8296682239 + 0.8546568751 0.8631222844 0.8301666975 + 0.8551089764 0.8635547757 0.8306632042 + 0.8555610180 0.8639872074 0.8311597109 + 0.8560131192 0.8644195795 0.8316562176 + 0.8564652205 0.8648520708 0.8321527243 + 0.8569173217 0.8652845025 0.8326491714 + 0.8573657870 0.8657131195 0.8331434131 + 0.8578140736 0.8661414981 0.8336374760 + 0.8582624197 0.8665698767 0.8341315985 + 0.8587107062 0.8669981956 0.8346257806 + 0.8591589928 0.8674265742 0.8351199031 + 0.8596050143 0.8678531051 0.8356130719 + 0.8600503206 0.8682789803 0.8361061215 + 0.8604956269 0.8687049150 0.8365991116 + 0.8609408736 0.8691307902 0.8370921016 + 0.8613861799 0.8695567846 0.8375850916 + 0.8618302941 0.8699809909 0.8380767703 + 0.8622733951 0.8704040051 0.8385676146 + 0.8627164960 0.8708269000 0.8390582800 + 0.8631595969 0.8712497950 0.8395491242 + 0.8636026978 0.8716726899 0.8400399089 + 0.8640447855 0.8720946908 0.8405296803 + 0.8644844890 0.8725144863 0.8410171270 + 0.8649240732 0.8729342222 0.8415045738 + 0.8653637171 0.8733540177 0.8419920206 + 0.8658034205 0.8737736940 0.8424795270 + 0.8662430048 0.8741934896 0.8429669142 + 0.8666800261 0.8746116757 0.8434523940 + 0.8671169877 0.8750299215 0.8439378142 + 0.8675538898 0.8754481077 0.8444231749 + 0.8679907918 0.8758662939 0.8449085951 + 0.8684278131 0.8762844801 0.8453940153 + 0.8688635230 0.8767014146 0.8458780050 + 0.8692985177 0.8771173954 0.8463612199 + 0.8697335124 0.8775334954 0.8468443155 + 0.8701685071 0.8779495955 0.8473274708 + 0.8706035018 0.8783656955 0.8478106856 + 0.8710376024 0.8787810802 0.8482931852 + 0.8714686036 0.8791939020 0.8487735987 + 0.8718994856 0.8796067834 0.8492540121 + 0.8723304272 0.8800197244 0.8497344255 + 0.8727613091 0.8804326057 0.8502147794 + 0.8731923103 0.8808454275 0.8506951928 + 0.8736202717 0.8812555075 0.8511731029 + 0.8740472794 0.8816646934 0.8516504765 + 0.8744744062 0.8820738792 0.8521277905 + 0.8749014735 0.8824831247 0.8526051044 + 0.8753286004 0.8828923106 0.8530824184 + 0.8757551908 0.8833009005 0.8535591960 + 0.8761808276 0.8837080002 0.8540347815 + 0.8766062856 0.8841150999 0.8545104265 + 0.8770318031 0.8845221996 0.8549860120 + 0.8774573207 0.8849292994 0.8554615974 + 0.8778828979 0.8853362799 0.8559371829 + 0.8783056736 0.8857411146 0.8564112782 + 0.8787276149 0.8861449957 0.8568848968 + 0.8791493773 0.8865488768 0.8573585153 + 0.8795713186 0.8869528770 0.8578320742 + 0.8799932003 0.8873568177 0.8583056927 + 0.8804144263 0.8877601027 0.8587788939 + 0.8808318973 0.8881598115 0.8592500091 + 0.8812494874 0.8885595798 0.8597210050 + 0.8816670775 0.8889592886 0.8601921201 + 0.8820847273 0.8893591166 0.8606631160 + 0.8825021982 0.8897588849 0.8611341715 + 0.8829184175 0.8901572227 0.8616037965 + 0.8833333850 0.8905543089 0.8620721102 + 0.8837484121 0.8909513950 0.8625404835 + 0.8841633797 0.8913484216 0.8630087972 + 0.8845784068 0.8917455077 0.8634771705 + 0.8849933743 0.8921425939 0.8639456034 + 0.8854057193 0.8925368786 0.8644126058 + 0.8858175278 0.8929308057 0.8648794293 + 0.8862293959 0.8933246732 0.8653463125 + 0.8866413236 0.8937187195 0.8658130765 + 0.8870530725 0.8941125870 0.8662800193 + 0.8874645233 0.8945059180 0.8667466044 + 0.8878725767 0.8948956132 0.8672116995 + 0.8882806897 0.8952853084 0.8676767945 + 0.8886888027 0.8956750035 0.8681420088 + 0.8890969157 0.8960648179 0.8686071038 + 0.8895050287 0.8964545131 0.8690721989 + 0.8899124265 0.8968430161 0.8695368171 + 0.8903183937 0.8972294927 0.8700006008 + 0.8907244802 0.8976160884 0.8704643250 + 0.8911305070 0.8980026245 0.8709279895 + 0.8915365934 0.8983891010 0.8713917136 + 0.8919426203 0.8987755775 0.8718554974 + 0.8923472166 0.8991609216 0.8723185062 + 0.8927506208 0.8995451927 0.8727809787 + 0.8931540847 0.8999295235 0.8732435703 + 0.8935574889 0.9003137946 0.8737061024 + 0.8939608932 0.9006981850 0.8741686940 + 0.8943642974 0.9010825157 0.8746312261 + 0.8947654963 0.9014645815 0.8750917912 + 0.8951656818 0.9018456936 0.8755515218 + 0.8955659270 0.9022269249 0.8760113120 + 0.8959661126 0.9026079774 0.8764711022 + 0.8963662982 0.9029892087 0.8769308925 + 0.8967664838 0.9033703208 0.8773906231 + 0.8971654773 0.9037498236 0.8778483868 + 0.8975641727 0.9041286707 0.8783056736 + 0.8979628086 0.9045075774 0.8787629008 + 0.8983613849 0.9048866034 0.8792201877 + 0.8987600803 0.9052655101 0.8796774745 + 0.8991587162 0.9056444168 0.8801347017 + 0.8995556235 0.9060223103 0.8805907965 + 0.8999521136 0.9064000249 0.8810467124 + 0.9003486037 0.9067776799 0.8815026283 + 0.9007450938 0.9071552753 0.8819584250 + 0.9011415839 0.9075329900 0.8824142814 + 0.9015380740 0.9079107046 0.8828700781 + 0.9019324183 0.9082859159 0.8833250999 + 0.9023262262 0.9086604714 0.8837798834 + 0.9027199149 0.9090350866 0.8842347264 + 0.9031137228 0.9094097018 0.8846895099 + 0.9035074711 0.9097843170 0.8851444125 + 0.9039012790 0.9101588726 0.8855991960 + 0.9042941928 0.9105305076 0.8860530257 + 0.9046869278 0.9109010100 0.8865063787 + 0.9050796032 0.9112715721 0.8869599104 + 0.9054722786 0.9116421938 0.8874133229 + 0.9058650136 0.9120126963 0.8878667951 + 0.9062576890 0.9123833179 0.8883202076 + 0.9066494107 0.9127531052 0.8887727261 + 0.9070404172 0.9131224751 0.8892245293 + 0.9074314237 0.9134919047 0.8896763921 + 0.9078223705 0.9138612747 0.8901283145 + 0.9082133770 0.9142307043 0.8905801773 + 0.9086043835 0.9146000147 0.8910320997 + 0.9089940190 0.9149683714 0.8914834261 + 0.9093819857 0.9153357148 0.8919339776 + 0.9097698927 0.9157028794 0.8923847079 + 0.9101579189 0.9160702229 0.8928353190 + 0.9105458856 0.9164373875 0.8932858706 + 0.9109339118 0.9168046713 0.8937366009 + 0.9113214016 0.9171708822 0.8941872120 + 0.9117074013 0.9175342917 0.8946375847 + 0.9120935202 0.9178975821 0.8950880170 + 0.9124795794 0.9182608724 0.8955383897 + 0.9128655791 0.9186242819 0.8959888220 + 0.9132516980 0.9189875722 0.8964391947 + 0.9136376977 0.9193509221 0.8968896866 + 0.9140226841 0.9197127223 0.8973388076 + 0.9144076109 0.9200745225 0.8977879286 + 0.9147924781 0.9204362035 0.8982371092 + 0.9151775241 0.9207980037 0.8986861706 + 0.9155623913 0.9211596847 0.8991352916 + 0.9159473181 0.9215214849 0.8995844126 + 0.9163296819 0.9218823910 0.9000319839 + 0.9167106748 0.9222429991 0.9004787207 + 0.9170917869 0.9226036072 0.9009253979 + 0.9174728990 0.9229642153 0.9013720751 + 0.9178540111 0.9233248234 0.9018188119 + 0.9182350039 0.9236853719 0.9022654891 + 0.9186152220 0.9240450263 0.9027118087 + 0.9189931750 0.9244018793 0.9031571150 + 0.9193711877 0.9247587919 0.9036024809 + 0.9197492003 0.9251158237 0.9040477872 + 0.9201272130 0.9254726768 0.9044930935 + 0.9205052257 0.9258295894 0.9049385190 + 0.9208831787 0.9261866212 0.9053838253 + 0.9212595224 0.9265406728 0.9058277011 + 0.9216355085 0.9268943071 0.9062713981 + 0.9220116138 0.9272480011 0.9067149758 + 0.9223875999 0.9276016951 0.9071586728 + 0.9227635860 0.9279553890 0.9076024294 + 0.9231395721 0.9283090830 0.9080460072 + 0.9235144258 0.9286624193 0.9084891081 + 0.9238871932 0.9290151000 0.9089311957 + 0.9242599010 0.9293677807 0.9093734026 + 0.9246327281 0.9297205210 0.9098154902 + 0.9250054955 0.9300732017 0.9102576971 + 0.9253782034 0.9304258823 0.9106997848 + 0.9257509708 0.9307786226 0.9111419916 + 0.9261215925 0.9311289787 0.9115830064 + 0.9264919162 0.9314789772 0.9120237827 + 0.9268621802 0.9318289757 0.9124646187 + 0.9272325039 0.9321790934 0.9129055142 + 0.9276028275 0.9325290918 0.9133462906 + 0.9279729724 0.9328790903 0.9137871265 + 0.9283429980 0.9332283139 0.9142274857 + 0.9287117720 0.9335752726 0.9146667123 + 0.9290807247 0.9339222908 0.9151058793 + 0.9294496179 0.9342694283 0.9155451059 + 0.9298185110 0.9346163869 0.9159842730 + 0.9301874042 0.9349634051 0.9164234996 + 0.9305561781 0.9353104234 0.9168627262 + 0.9309234023 0.9356566072 0.9173014760 + 0.9312896132 0.9360023737 0.9177401066 + 0.9316558242 0.9363480806 0.9181786180 + 0.9320219755 0.9366937876 0.9186171889 + 0.9323881865 0.9370396137 0.9190558195 + 0.9327543974 0.9373853207 0.9194943905 + 0.9331206083 0.9377310872 0.9199329019 + 0.9334837794 0.9380753040 0.9203705788 + 0.9338467121 0.9384195209 0.9208081961 + 0.9342097044 0.9387636185 0.9212458730 + 0.9345725775 0.9391077757 0.9216834903 + 0.9349356294 0.9394518733 0.9221211076 + 0.9352985024 0.9397959709 0.9225587249 + 0.9356611967 0.9401394725 0.9229962230 + 0.9360224009 0.9404795766 0.9234330058 + 0.9363836050 0.9408196807 0.9238699079 + 0.9367448092 0.9411597848 0.9243066907 + 0.9371060133 0.9414998889 0.9247435927 + 0.9374672771 0.9418399930 0.9251803756 + 0.9378284812 0.9421800971 0.9256172776 + 0.9381889105 0.9425194263 0.9260538816 + 0.9385479093 0.9428572059 0.9264901876 + 0.9389069080 0.9431951046 0.9269264936 + 0.9392657876 0.9435328841 0.9273626804 + 0.9396247864 0.9438707232 0.9277989864 + 0.9399837852 0.9442085028 0.9282351732 + 0.9403427839 0.9445464015 0.9286714792 + 0.9407004118 0.9448838830 0.9291065931 + 0.9410564899 0.9452211857 0.9295405746 + 0.9414126873 0.9455584288 0.9299746752 + 0.9417688847 0.9458956718 0.9304087162 + 0.9421250820 0.9462329149 0.9308426976 + 0.9424812794 0.9465702176 0.9312766790 + 0.9428374767 0.9469074011 0.9317107797 + 0.9431930184 0.9472429156 0.9321432114 + 0.9435479045 0.9475771785 0.9325743914 + 0.9439027905 0.9479115009 0.9330056906 + 0.9442576766 0.9482458234 0.9334369898 + 0.9446126223 0.9485800862 0.9338682294 + 0.9449675083 0.9489144087 0.9342995286 + 0.9453223944 0.9492486715 0.9347308278 + 0.9456762075 0.9495813847 0.9351599216 + 0.9460293055 0.9499132037 0.9355877042 + 0.9463824034 0.9502449036 0.9360154867 + 0.9467355013 0.9505766034 0.9364433289 + 0.9470885992 0.9509083033 0.9368711114 + 0.9474416971 0.9512400031 0.9372988939 + 0.9477947950 0.9515718222 0.9377266169 + 0.9481452107 0.9519032240 0.9381518960 + 0.9484937191 0.9522345066 0.9385753274 + 0.9488422275 0.9525657892 0.9389986992 + 0.9491906166 0.9528970718 0.9394220710 + 0.9495391250 0.9532284141 0.9398453832 + 0.9498875141 0.9535596967 0.9402688146 + 0.9502360225 0.9538909793 0.9406921864 + 0.9505816102 0.9542205930 0.9411129951 + 0.9509239197 0.9545481801 0.9415308833 + 0.9512662292 0.9548758268 0.9419488907 + 0.9516084790 0.9552034140 0.9423667789 + 0.9519507885 0.9555310011 0.9427847266 + 0.9522932172 0.9558585882 0.9432026148 + 0.9526355267 0.9561861753 0.9436205029 + 0.9529765248 0.9565111995 0.9440370202 + 0.9533149004 0.9568306208 0.9444504976 + 0.9536532164 0.9571499825 0.9448639154 + 0.9539914727 0.9574694037 0.9452773929 + 0.9543299079 0.9577888250 0.9456908107 + 0.9546682239 0.9581081867 0.9461042881 + 0.9550064802 0.9584276080 0.9465177059 + 0.9553446174 0.9587466121 0.9469308853 + 0.9556806087 0.9590610266 0.9473422170 + 0.9560164809 0.9593753815 0.9477534294 + 0.9563524723 0.9596897960 0.9481647015 + 0.9566884041 0.9600043297 0.9485759735 + 0.9570243955 0.9603186846 0.9489871860 + 0.9573603272 0.9606330991 0.9493985176 + 0.9576963186 0.9609475136 0.9498096704 + 0.9580307007 0.9612603188 0.9502192140 + 0.9583647251 0.9615727067 0.9506282806 + 0.9586988091 0.9618852139 0.9510374069 + 0.9590328932 0.9621976018 0.9514464736 + 0.9593669176 0.9625099897 0.9518555999 + 0.9597010016 0.9628223777 0.9522647262 + 0.9600350857 0.9631348252 0.9526737928 + 0.9603679180 0.9634457231 0.9530814290 + 0.9606994987 0.9637547135 0.9534872770 + 0.9610310793 0.9640638232 0.9538931847 + 0.9613626003 0.9643728733 0.9542990923 + 0.9616941810 0.9646819830 0.9547049999 + 0.9620258212 0.9649910927 0.9551109076 + 0.9623572826 0.9653000832 0.9555168152 + 0.9626886845 0.9656090140 0.9559224248 + 0.9630175829 0.9659156799 0.9563245773 + 0.9633464813 0.9662224054 0.9567267895 + 0.9636753798 0.9665290713 0.9571290016 + 0.9640042782 0.9668357968 0.9575312138 + 0.9643331766 0.9671425223 0.9579333067 + 0.9646620750 0.9674491882 0.9583355188 + 0.9649909735 0.9677559137 0.9587376714 + 0.9653183818 0.9680610895 0.9591376185 + 0.9656450152 0.9683653712 0.9595360160 + 0.9659715295 0.9686697125 0.9599344730 + 0.9662981033 0.9689739943 0.9603329897 + 0.9666246176 0.9692782760 0.9607315063 + 0.9669511914 0.9695824981 0.9611299038 + 0.9672777057 0.9698867798 0.9615284204 + 0.9676039219 0.9701907039 0.9619265199 + 0.9679273963 0.9704914093 0.9623209834 + 0.9682508111 0.9707919955 0.9627156258 + 0.9685742855 0.9710927010 0.9631102085 + 0.9688977003 0.9713934064 0.9635047913 + 0.9692211747 0.9716939926 0.9638993740 + 0.9695447087 0.9719946980 0.9642940164 + 0.9698681235 0.9722954035 0.9646884799 + 0.9701895118 0.9725940824 0.9650804996 + 0.9705088735 0.9728909731 0.9654701948 + 0.9708284140 0.9731879234 0.9658597708 + 0.9711478949 0.9734848142 0.9662495255 + 0.9714673162 0.9737817049 0.9666391015 + 0.9717867970 0.9740785956 0.9670287967 + 0.9721062183 0.9743754864 0.9674183726 + 0.9724256992 0.9746723771 0.9678081274 + 0.9727420211 0.9749661088 0.9681929946 + 0.9730579257 0.9752594233 0.9685772061 + 0.9733737707 0.9755526781 0.9689614773 + 0.9736896157 0.9758459926 0.9693456888 + 0.9740055203 0.9761393070 0.9697300196 + 0.9743214250 0.9764326215 0.9701141715 + 0.9746373296 0.9767258763 0.9704985023 + 0.9749523997 0.9770184755 0.9708817005 + 0.9752640724 0.9773074985 0.9712603092 + 0.9755758047 0.9775965214 0.9716387987 + 0.9758874774 0.9778854847 0.9720172882 + 0.9761992097 0.9781745076 0.9723957777 + 0.9765108824 0.9784634709 0.9727743268 + 0.9768224955 0.9787524939 0.9731528163 + 0.9771342278 0.9790415168 0.9735313058 + 0.9774439931 0.9793285728 0.9739072919 + 0.9777507782 0.9796128273 0.9742792845 + 0.9780576229 0.9798970222 0.9746512175 + 0.9783642888 0.9801810980 0.9750232100 + 0.9786710739 0.9804652929 0.9753952026 + 0.9789779186 0.9807494879 0.9757671952 + 0.9792847037 0.9810336828 0.9761391282 + 0.9795914888 0.9813178778 0.9765111208 + 0.9798954725 0.9815995097 0.9768790007 + 0.9801974297 0.9818789959 0.9772434831 + 0.9804993272 0.9821584225 0.9776080847 + 0.9808012247 0.9824379086 0.9779726267 + 0.9811030030 0.9827173948 0.9783372283 + 0.9814049006 0.9829968810 0.9787017703 + 0.9817067981 0.9832764268 0.9790663123 + 0.9820086956 0.9835559130 0.9794309139 + 0.9823070168 0.9838317037 0.9797903895 + 0.9826034904 0.9841055870 0.9801471829 + 0.9828999043 0.9843794703 0.9805040956 + 0.9831963778 0.9846534133 0.9808608890 + 0.9834929109 0.9849272966 0.9812176824 + 0.9837893248 0.9852011800 0.9815745950 + 0.9840857983 0.9854751229 0.9819313884 + 0.9843822718 0.9857488871 0.9822881818 + 0.9846748710 0.9860187769 0.9826393127 + 0.9849656820 0.9862868190 0.9829875827 + 0.9852566123 0.9865549207 0.9833359718 + 0.9855474234 0.9868229032 0.9836843014 + 0.9858381748 0.9870908856 0.9840325713 + 0.9861291051 0.9873589277 0.9843810201 + 0.9864199162 0.9876269102 0.9847292900 + 0.9867107272 0.9878948927 0.9850776196 + 0.9869980216 0.9881591797 0.9854195118 + 0.9872834086 0.9884213805 0.9857577085 + 0.9875686765 0.9886837006 0.9860960245 + 0.9878541231 0.9889459014 0.9864342213 + 0.9881393909 0.9892082214 0.9867724180 + 0.9884247780 0.9894704223 0.9871106744 + 0.9887101054 0.9897326827 0.9874488711 + 0.9889954925 0.9899948835 0.9877871871 + 0.9892771244 0.9902533889 0.9881182909 + 0.9895554185 0.9905087948 0.9884434938 + 0.9898337722 0.9907642007 0.9887685776 + 0.9901121855 0.9910196066 0.9890937209 + 0.9903905988 0.9912750125 0.9894189239 + 0.9906688929 0.9915304184 0.9897440076 + 0.9909473062 0.9917857051 0.9900690913 + 0.9912257195 0.9920411110 0.9903942943 + 0.9915013909 0.9922938943 0.9907138944 + 0.9917727113 0.9925420880 0.9910243154 + 0.9920439720 0.9927902818 0.9913346767 + 0.9923152924 0.9930384159 0.9916450977 + 0.9925866127 0.9932866096 0.9919555187 + 0.9928578734 0.9935348034 0.9922658801 + 0.9931291938 0.9937829971 0.9925763011 + 0.9934003949 0.9940311909 0.9928867221 + 0.9936708212 0.9942783713 0.9931951165 + 0.9939351082 0.9945191741 0.9934899211 + 0.9941995144 0.9947599173 0.9937847257 + 0.9944638014 0.9950006008 0.9940794110 + 0.9947280884 0.9952414036 0.9943742156 + 0.9949923754 0.9954820871 0.9946690202 + 0.9952567220 0.9957227707 0.9949638247 + 0.9955210090 0.9959635139 0.9952586293 + 0.9957852960 0.9962043166 0.9955533743 + 0.9960408211 0.9964358807 0.9958317876 + 0.9962943792 0.9966655970 0.9961066842 + 0.9965478778 0.9968951941 0.9963817000 + 0.9968014956 0.9971249104 0.9966565967 + 0.9970551133 0.9973546267 0.9969314933 + 0.9973086119 0.9975842237 0.9972063899 + 0.9975622296 0.9978138804 0.9974814057 + 0.9978157878 0.9980435967 0.9977563024 + 0.9980642200 0.9982674122 0.9980189800 + 0.9983060956 0.9984840155 0.9982665777 + 0.9985480905 0.9987004995 0.9985142946 + 0.9987900853 0.9989171028 0.9987618923 + 0.9990320802 0.9991337061 0.9990094900 + 0.9992740750 0.9993503094 0.9992570877 + 0.9995160103 0.9995667934 0.9995048046 + 0.9997580051 0.9997833967 0.9997524023 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_ii_400.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_ii_400.spi1d index f198692b..5f4ce924 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_ii_400.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_futura_ii_400.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0171004608 0.0156014897 0.0110214697 - 0.0296423100 0.0275058597 0.0204370096 - 0.0400087796 0.0378916785 0.0286195297 - 0.0492478795 0.0473487712 0.0360689983 - 0.0577691793 0.0561984889 0.0429962091 - 0.0658048466 0.0644933432 0.0495528691 - 0.0734695420 0.0724152401 0.0558067188 - 0.0808440894 0.0800366476 0.0618191212 - 0.0879925117 0.0873461664 0.0676262975 - 0.0949557200 0.0944325328 0.0732697919 - 0.1018247008 0.1012879983 0.0787453130 - 0.1086272970 0.1079066992 0.0840507299 - 0.1152980998 0.1143304035 0.0892019123 - 0.1217757985 0.1205366999 0.0942221731 - 0.1280754060 0.1265349984 0.0991079137 - 0.1341703981 0.1323578954 0.1038789004 - 0.1400842071 0.1380200982 0.1085413024 - 0.1458258033 0.1435257047 0.1130928993 - 0.1513952017 0.1488672942 0.1175398976 - 0.1568222046 0.1540704072 0.1219094992 - 0.1621166021 0.1591500938 0.1262044013 - 0.1672838926 0.1641122997 0.1304340065 - 0.1723344028 0.1689572036 0.1346047074 - 0.1772560030 0.1737031043 0.1387116015 - 0.1820691973 0.1783441007 0.1427484006 - 0.1867853999 0.1828867942 0.1467114985 - 0.1914003938 0.1873379946 0.1506099999 - 0.1959276944 0.1916999966 0.1544381976 - 0.2003667951 0.1959740072 0.1582106054 - 0.2047297060 0.2001685053 0.1619343013 - 0.2090197951 0.2042849064 0.1656126976 - 0.2132406980 0.2083213031 0.1692394018 - 0.2173902988 0.2122910023 0.1728218049 - 0.2214687020 0.2161971927 0.1763650030 - 0.2254846990 0.2200367004 0.1798641980 - 0.2294346988 0.2238188982 0.1833273023 - 0.2333287001 0.2275474966 0.1867597997 - 0.2371630073 0.2312255949 0.1901571006 - 0.2409424931 0.2348573953 0.1935154945 - 0.2446697950 0.2384261042 0.1968335062 - 0.2483461946 0.2419455051 0.2001166940 - 0.2519711852 0.2454174012 0.2033713013 - 0.2555448115 0.2488500029 0.2066029012 - 0.2590776980 0.2522298098 0.2098037004 - 0.2625567913 0.2555679977 0.2129680067 - 0.2659949958 0.2588672936 0.2161033005 - 0.2693926096 0.2621265948 0.2192150950 - 0.2727515101 0.2653498054 0.2222974002 - 0.2760717869 0.2685379982 0.2253466994 - 0.2793444991 0.2716934979 0.2283667028 - 0.2825835049 0.2748126090 0.2313646972 - 0.2857859135 0.2778981924 0.2343385071 - 0.2889564931 0.2809517980 0.2372840047 - 0.2920953929 0.2839730978 0.2402049005 - 0.2951973081 0.2869609892 0.2431025058 - 0.2982689142 0.2899155021 0.2459677011 - 0.3013125062 0.2928346097 0.2488068938 - 0.3043302894 0.2957249880 0.2516252100 - 0.3073143959 0.2985872030 0.2544235885 - 0.3102650940 0.3014270067 0.2572003901 - 0.3131817877 0.3042409122 0.2599579096 - 0.3160701096 0.3070298135 0.2626875937 - 0.3189300001 0.3097898066 0.2653909028 - 0.3217678964 0.3125233054 0.2680732906 - 0.3245871961 0.3152284920 0.2707414031 - 0.3273836076 0.3179099858 0.2733910084 - 0.3301528096 0.3205696046 0.2760204077 - 0.3328959048 0.3232055902 0.2786307931 - 0.3356123865 0.3258267045 0.2812187076 - 0.3382978141 0.3284305036 0.2837873995 - 0.3409596980 0.3310135007 0.2863405049 - 0.3436000943 0.3335742950 0.2888756096 - 0.3462189138 0.3361156881 0.2913906872 - 0.3488169909 0.3386341035 0.2938865125 - 0.3513996005 0.3411293030 0.2963629961 - 0.3539629877 0.3436000943 0.2988243103 - 0.3565055132 0.3460493982 0.3012714982 - 0.3590308130 0.3484821916 0.3036991060 - 0.3615351915 0.3508949876 0.3061034977 - 0.3640230000 0.3532888889 0.3084929883 - 0.3664914966 0.3556644917 0.3108676076 - 0.3689433038 0.3580288887 0.3132284880 - 0.3713740110 0.3603788912 0.3155708909 - 0.3737848103 0.3627198935 0.3179005086 - 0.3761711121 0.3650496900 0.3202157021 - 0.3785369992 0.3673666120 0.3225137889 - 0.3808847964 0.3696695864 0.3247897923 - 0.3832105994 0.3719570935 0.3270477951 - 0.3855203986 0.3742293119 0.3292933106 - 0.3878141046 0.3764826059 0.3315246105 - 0.3900910914 0.3787195086 0.3337422907 - 0.3923526108 0.3809430897 0.3359439075 - 0.3945967853 0.3831507862 0.3381271064 - 0.3968229890 0.3853423893 0.3402962089 - 0.3990325034 0.3875181973 0.3424524069 - 0.4012250900 0.3896811008 0.3445951045 - 0.4033975899 0.3918299079 0.3467190862 - 0.4055522084 0.3939658999 0.3488298059 - 0.4076915085 0.3960888982 0.3509294987 - 0.4098165929 0.3981984854 0.3530152142 - 0.4119274020 0.4002932906 0.3550836146 - 0.4140225053 0.4023733139 0.3571367860 - 0.4161036909 0.4044389129 0.3591760099 - 0.4181733131 0.4064899981 0.3611989915 - 0.4202328026 0.4085263014 0.3632034063 - 0.4222835898 0.4105474949 0.3651930988 - 0.4243231118 0.4125525951 0.3671697974 - 0.4263541996 0.4145421982 0.3691323996 - 0.4283750057 0.4165202975 0.3710797131 - 0.4303872883 0.4184862077 0.3730098903 - 0.4323903918 0.4204401076 0.3749246895 - 0.4343844950 0.4223816991 0.3768284917 - 0.4363701046 0.4243108928 0.3787207901 - 0.4383463860 0.4262312949 0.3806005120 - 0.4403150082 0.4281423986 0.3824656010 - 0.4422720969 0.4300462902 0.3843190074 - 0.4442181885 0.4319409132 0.3861612082 - 0.4461520910 0.4338287115 0.3879927993 - 0.4480755031 0.4357072115 0.3898114860 - 0.4499849975 0.4375792146 0.3916176856 - 0.4518805146 0.4394420087 0.3934090137 - 0.4537624121 0.4412986040 0.3951860964 - 0.4556312859 0.4431461096 0.3969503045 - 0.4574877024 0.4449875951 0.3987065852 - 0.4593302011 0.4468202889 0.4004544914 - 0.4611577094 0.4486469924 0.4021933973 - 0.4629718959 0.4504652917 0.4039224088 - 0.4647724032 0.4522775114 0.4056409895 - 0.4665606022 0.4540818930 0.4073483944 - 0.4683364928 0.4558798969 0.4090481102 - 0.4700995982 0.4576708078 0.4107409120 - 0.4718486965 0.4594548941 0.4124262929 - 0.4735842943 0.4612326920 0.4141052067 - 0.4753074944 0.4630033076 0.4157783985 - 0.4770157039 0.4647684097 0.4174447060 - 0.4787110090 0.4665257037 0.4191048145 - 0.4803912044 0.4682776928 0.4207561910 - 0.4820596874 0.4700196981 0.4224033952 - 0.4837144017 0.4717536867 0.4240436852 - 0.4853559136 0.4734789133 0.4256787002 - 0.4869849980 0.4751960039 0.4273071885 - 0.4886021018 0.4769046903 0.4289287925 - 0.4902065098 0.4786028862 0.4305444062 - 0.4917967021 0.4802902043 0.4321511984 - 0.4933750033 0.4819664061 0.4337506890 - 0.4949423969 0.4836333096 0.4353412986 - 0.4964992106 0.4852910936 0.4369243979 - 0.4980452061 0.4869402051 0.4385007024 - 0.4995796978 0.4885810018 0.4400705099 - 0.5011045933 0.4902119935 0.4416334033 - 0.5026171207 0.4918344915 0.4431881905 - 0.5041204095 0.4934495986 0.4447365105 - 0.5056149960 0.4950574934 0.4462780058 - 0.5071011186 0.4966585934 0.4478121996 - 0.5085787773 0.4982511103 0.4493379891 - 0.5100476146 0.4998351932 0.4508579075 - 0.5115081072 0.5014110208 0.4523723125 - 0.5129603744 0.5029789209 0.4538812935 - 0.5144031048 0.5045387745 0.4553841949 - 0.5158382058 0.5060889721 0.4568797052 - 0.5172675848 0.5076315999 0.4583696127 - 0.5186911821 0.5091663003 0.4598538876 - 0.5201076865 0.5106902122 0.4613319039 - 0.5215175152 0.5122056007 0.4628019035 - 0.5229220986 0.5137131810 0.4642662108 - 0.5243213177 0.5152136087 0.4657247961 - 0.5257117152 0.5167068243 0.4671759903 - 0.5270925760 0.5181897879 0.4686197937 - 0.5284671783 0.5196657181 0.4700576961 - 0.5298361778 0.5211349726 0.4714899957 - 0.5311961174 0.5225929022 0.4729127884 - 0.5325514078 0.5240452290 0.4743295908 - 0.5339015126 0.5254917741 0.4757412076 - 0.5352419019 0.5269278884 0.4771471024 - 0.5365741253 0.5283558965 0.4785467982 - 0.5379003286 0.5297778845 0.4799422026 - 0.5392192006 0.5311921835 0.4813325107 - 0.5405315757 0.5325987935 0.4827156961 - 0.5418395996 0.5340006948 0.4840947092 - 0.5431419015 0.5353968143 0.4854688942 - 0.5444355011 0.5367845893 0.4868355095 - 0.5457248092 0.5381672978 0.4881992936 - 0.5470086932 0.5395433903 0.4895603955 - 0.5482844114 0.5409129858 0.4909155071 - 0.5495557189 0.5422785878 0.4922682941 - 0.5508213043 0.5436385870 0.4936183095 - 0.5520780087 0.5449923873 0.4949626923 - 0.5533294082 0.5463421941 0.4963046014 - 0.5545719862 0.5476844907 0.4976435006 - 0.5558081865 0.5490229726 0.4989764988 - 0.5570405126 0.5503587723 0.5003060102 - 0.5582659245 0.5516861081 0.5016294718 - 0.5594872832 0.5530081987 0.5029485226 - 0.5607051253 0.5543255210 0.5042636991 - 0.5619114041 0.5556346178 0.5055726767 - 0.5631139278 0.5569404960 0.5068780780 - 0.5643098950 0.5582408905 0.5081769228 - 0.5655001998 0.5595366955 0.5094692111 - 0.5666872859 0.5608298779 0.5107579827 - 0.5678644180 0.5621141195 0.5120382905 - 0.5690370202 0.5633950233 0.5133149028 - 0.5702024102 0.5646705031 0.5145862103 - 0.5713610053 0.5659371018 0.5158511996 - 0.5725166798 0.5671998858 0.5171135068 - 0.5736652017 0.5684533119 0.5183678865 - 0.5748100877 0.5697032809 0.5196194053 - 0.5759480000 0.5709466934 0.5208657980 - 0.5770794153 0.5721855164 0.5221070051 - 0.5782068968 0.5734214783 0.5233457088 - 0.5793256760 0.5746489167 0.5245773196 - 0.5804418921 0.5758739710 0.5258070230 - 0.5815505981 0.5770922899 0.5270308852 - 0.5826562047 0.5783078074 0.5282518268 - 0.5837575793 0.5795192122 0.5294687152 - 0.5848544240 0.5807257891 0.5306801200 - 0.5859488249 0.5819299221 0.5318887830 - 0.5870361924 0.5831274986 0.5330893993 - 0.5881218910 0.5843237042 0.5342882276 - 0.5891991258 0.5855125785 0.5354802012 - 0.5902739763 0.5866997838 0.5366697907 - 0.5913425088 0.5878822207 0.5378534198 - 0.5924072266 0.5890613794 0.5390337706 - 0.5934671760 0.5902367234 0.5402104855 - 0.5945214033 0.5914065838 0.5413823128 - 0.5955718160 0.5925731063 0.5425512791 - 0.5966168046 0.5937318206 0.5437142253 - 0.5976600051 0.5948877931 0.5448753834 - 0.5986986160 0.5960369110 0.5460278988 - 0.5997365713 0.5971850753 0.5471792221 - 0.6007699966 0.5983254910 0.5483239293 - 0.6018025875 0.5994653106 0.5494676232 - 0.6028274298 0.6005986929 0.5506033897 - 0.6038507819 0.6017305255 0.5517382026 - 0.6048673987 0.6028541923 0.5528683066 - 0.6058822274 0.6039755940 0.5539959073 - 0.6068912745 0.6050894260 0.5551165938 - 0.6078979969 0.6062008739 0.5562350750 - 0.6088986993 0.6073063016 0.5573483109 - 0.6098974943 0.6084094048 0.5584586859 - 0.6108921766 0.6095077991 0.5595636964 - 0.6118845940 0.6106038094 0.5606665015 - 0.6128724813 0.6116948724 0.5617650151 - 0.6138582826 0.6127820015 0.5628610849 - 0.6148399115 0.6138613224 0.5639526248 - 0.6158196926 0.6149377227 0.5650421977 - 0.6167954803 0.6160081029 0.5661283135 - 0.6177694201 0.6170759201 0.5672121048 - 0.6187387109 0.6181375980 0.5682907104 - 0.6197069287 0.6191977859 0.5693675280 - 0.6206719279 0.6202535033 0.5704392195 - 0.6216356754 0.6213079095 0.5715097785 - 0.6225941181 0.6223561764 0.5725752711 - 0.6235517263 0.6234036088 0.5736399889 - 0.6245011091 0.6244416237 0.5746986270 - 0.6254506111 0.6254796982 0.5757570267 - 0.6263933778 0.6265093088 0.5768082142 - 0.6273362041 0.6275388002 0.5778594017 - 0.6282750964 0.6285620928 0.5789057016 - 0.6292135119 0.6295846105 0.5799514055 - 0.6301469803 0.6306030154 0.5809925795 - 0.6310787201 0.6316199899 0.5820323825 - 0.6320052743 0.6326320171 0.5830696821 - 0.6329280138 0.6336405277 0.5841053724 - 0.6338480115 0.6346455216 0.5851392150 - 0.6347640753 0.6356455088 0.5861703753 - 0.6356791258 0.6366437078 0.5871999264 - 0.6365907192 0.6376357079 0.5882244110 - 0.6375023127 0.6386275291 0.5892485976 - 0.6384099126 0.6396116018 0.5902659297 - 0.6393175721 0.6405956149 0.5912833214 - 0.6402198076 0.6415734887 0.5922964215 - 0.6411203146 0.6425495744 0.5933085084 - 0.6420174241 0.6435208917 0.5943167210 - 0.6429113746 0.6444880962 0.5953217745 - 0.6438037157 0.6454532146 0.5963243246 - 0.6446918845 0.6464130878 0.5973206162 - 0.6455799937 0.6473729014 0.5983167887 - 0.6464617848 0.6483271718 0.5993061066 - 0.6473435760 0.6492815018 0.6002953053 - 0.6482205987 0.6502330899 0.6012793183 - 0.6490954757 0.6511834860 0.6022611856 - 0.6499673724 0.6521314979 0.6032401919 - 0.6508346200 0.6530753970 0.6042150855 - 0.6517016292 0.6540189981 0.6051893830 - 0.6525605917 0.6549540162 0.6061556935 - 0.6534196734 0.6558889747 0.6071221232 - 0.6542748213 0.6568189859 0.6080846786 - 0.6551280022 0.6577466130 0.6090456247 - 0.6559792757 0.6586722136 0.6100040078 - 0.6568263173 0.6595928073 0.6109567881 - 0.6576732993 0.6605134010 0.6119096279 - 0.6585139036 0.6614270210 0.6128568053 - 0.6593537927 0.6623396873 0.6138035059 - 0.6601899862 0.6632481217 0.6147469282 - 0.6610223055 0.6641519070 0.6156873107 - 0.6618543863 0.6650553942 0.6166272759 - 0.6626814008 0.6659533978 0.6175631881 - 0.6635084748 0.6668512821 0.6184991002 - 0.6643338203 0.6677461267 0.6194313169 - 0.6651577950 0.6686387062 0.6203612089 - 0.6659812927 0.6695305705 0.6212899089 - 0.6668003798 0.6704167128 0.6222109795 - 0.6676195860 0.6713029146 0.6231321096 - 0.6684346199 0.6721845269 0.6240482926 - 0.6692469716 0.6730633974 0.6249616146 - 0.6700590253 0.6739416718 0.6258742213 - 0.6708669066 0.6748149991 0.6267806888 - 0.6716747284 0.6756882071 0.6276870966 - 0.6724805236 0.6765586734 0.6285902858 - 0.6732844710 0.6774268746 0.6294907928 - 0.6740885973 0.6782950759 0.6303914189 - 0.6748862267 0.6791549921 0.6312856078 - 0.6756836176 0.6800147891 0.6321799159 - 0.6764786839 0.6808714271 0.6330708861 - 0.6772701740 0.6817231774 0.6339576244 - 0.6780617833 0.6825749874 0.6348443031 - 0.6788492799 0.6834219098 0.6357238293 - 0.6796358228 0.6842675805 0.6366016269 - 0.6804214120 0.6851121783 0.6374781132 - 0.6812025905 0.6859521270 0.6383491158 - 0.6819838285 0.6867920160 0.6392201185 - 0.6827620864 0.6876285076 0.6400873065 - 0.6835377216 0.6884618998 0.6409513950 - 0.6843134165 0.6892954111 0.6418154836 - 0.6850838065 0.6901227832 0.6426753998 - 0.6858534217 0.6909493208 0.6435347199 - 0.6866219044 0.6917747259 0.6443930268 - 0.6873850822 0.6925947070 0.6452473998 - 0.6881483197 0.6934146285 0.6461017132 - 0.6889085770 0.6942318082 0.6469531059 - 0.6896653175 0.6950455904 0.6478009820 - 0.6904219985 0.6958593726 0.6486489773 - 0.6911736131 0.6966683865 0.6494913101 - 0.6919230819 0.6974754930 0.6503316164 - 0.6926727295 0.6982825994 0.6511718035 - 0.6934152246 0.6990849972 0.6520056725 - 0.6941574216 0.6998872161 0.6528394222 - 0.6948987842 0.7006883025 0.6536720991 - 0.6956359148 0.7014837265 0.6545003057 - 0.6963729858 0.7022790909 0.6553285122 - 0.6971086264 0.7030712962 0.6561551094 - 0.6978415251 0.7038580179 0.6569787860 - 0.6985744238 0.7046447992 0.6578025818 - 0.6993039250 0.7054278255 0.6586238742 - 0.7000302076 0.7062076926 0.6594430208 - 0.7007566094 0.7069876194 0.6602622271 - 0.7014790177 0.7077631950 0.6610757113 - 0.7021995783 0.7085369229 0.6618868113 - 0.7029200792 0.7093105912 0.6626979113 - 0.7036368847 0.7100790143 0.6635041833 - 0.7043526769 0.7108461261 0.6643093824 - 0.7050685287 0.7116131783 0.6651147008 - 0.7057778239 0.7123737931 0.6659145951 - 0.7064864039 0.7131338120 0.6667140722 - 0.7071951032 0.7138937116 0.6675134897 - 0.7078974843 0.7146461010 0.6683061123 - 0.7085998058 0.7153983116 0.6690986156 - 0.7093018889 0.7161502838 0.6698907018 - 0.7100000978 0.7168943882 0.6706752181 - 0.7106983066 0.7176384926 0.6714596748 - 0.7113963962 0.7183822989 0.6722435951 - 0.7120925784 0.7191202044 0.6730211973 - 0.7127888799 0.7198581100 0.6737989187 - 0.7134850025 0.7205957174 0.6745759845 - 0.7141777277 0.7213283181 0.6753479838 - 0.7148702741 0.7220609188 0.6761199236 - 0.7155628204 0.7227932215 0.6768916249 - 0.7162505984 0.7235177755 0.6776590943 - 0.7169383764 0.7242423296 0.6784266233 - 0.7176260948 0.7249668837 0.6791939735 - 0.7183110714 0.7256841063 0.6799570918 - 0.7189961076 0.7264013886 0.6807202101 - 0.7196810246 0.7271186113 0.6814832091 - 0.7203640938 0.7278298140 0.6822391748 - 0.7210471034 0.7285404801 0.6829947233 - 0.7217299938 0.7292512059 0.6837502122 - 0.7224094272 0.7299574018 0.6845011711 - 0.7230880857 0.7306627035 0.6852512956 - 0.7237666845 0.7313678861 0.6860014200 - 0.7244430184 0.7320697904 0.6867479086 - 0.7251182199 0.7327703238 0.6874929070 - 0.7257934213 0.7334707975 0.6882380247 - 0.7264670730 0.7341670990 0.6889796257 - 0.7271397114 0.7348600030 0.6897184253 - 0.7278121710 0.7355527878 0.6904572845 - 0.7284829021 0.7362431884 0.6911934018 - 0.7291505933 0.7369294763 0.6919252872 - 0.7298182249 0.7376158237 0.6926571131 - 0.7304852009 0.7383013964 0.6933879256 - 0.7311487794 0.7389839888 0.6941142082 - 0.7318122983 0.7396667004 0.6948403716 - 0.7324758768 0.7403492928 0.6955665946 - 0.7331362963 0.7410271168 0.6962887049 - 0.7337965965 0.7417047024 0.6970105767 - 0.7344568968 0.7423822284 0.6977325082 - 0.7351133823 0.7430555820 0.6984499097 - 0.7357681990 0.7437270880 0.6991655231 - 0.7364230156 0.7443987131 0.6998810172 - 0.7370759249 0.7450683713 0.7005940080 - 0.7377259731 0.7457355261 0.7013034821 - 0.7383760810 0.7464025021 0.7020128965 - 0.7390258908 0.7470691204 0.7027217150 - 0.7396721244 0.7477303743 0.7034254074 - 0.7403182983 0.7483916879 0.7041289806 - 0.7409644723 0.7490528822 0.7048326731 - 0.7416077852 0.7497091293 0.7055320740 - 0.7422502041 0.7503637075 0.7062302828 - 0.7428925037 0.7510182858 0.7069284916 - 0.7435333729 0.7516707778 0.7076249719 - 0.7441717982 0.7523202896 0.7083188891 - 0.7448102236 0.7529696822 0.7090129256 - 0.7454485893 0.7536191940 0.7097067833 - 0.7460827231 0.7542620897 0.7103967071 - 0.7467167974 0.7549051046 0.7110865116 - 0.7473508716 0.7555481195 0.7117763758 - 0.7479826212 0.7561879754 0.7124634981 - 0.7486125231 0.7568256259 0.7131487131 - 0.7492423058 0.7574632764 0.7138339281 - 0.7498719096 0.7581006289 0.7145183086 - 0.7504996061 0.7587358952 0.7151979804 - 0.7511271834 0.7593712211 0.7158777118 - 0.7517548203 0.7600064874 0.7165573835 - 0.7523803115 0.7606381178 0.7172330022 - 0.7530044913 0.7612677217 0.7179064155 - 0.7536287904 0.7618973851 0.7185798883 - 0.7542523742 0.7625262141 0.7192522883 - 0.7548720837 0.7631500959 0.7199187875 - 0.7554919124 0.7637740970 0.7205854058 - 0.7561116219 0.7643979788 0.7212520242 - 0.7567297816 0.7650203109 0.7219150066 - 0.7573469877 0.7656415105 0.7225760221 - 0.7579641938 0.7662625909 0.7232369781 - 0.7585812807 0.7668836117 0.7238978148 - 0.7591949105 0.7674986720 0.7245547175 - 0.7598084807 0.7681139112 0.7252116203 - 0.7604219913 0.7687290907 0.7258685231 - 0.7610334754 0.7693409920 0.7265231013 - 0.7616422772 0.7699488997 0.7271751165 - 0.7622511983 0.7705568075 0.7278271914 - 0.7628600001 0.7711647153 0.7284792066 - 0.7634664774 0.7717682719 0.7291265130 - 0.7640724778 0.7723709941 0.7297729254 - 0.7646784782 0.7729737163 0.7304192781 - 0.7652838826 0.7735757232 0.7310644984 - 0.7658861279 0.7741742730 0.7317045927 - 0.7664881945 0.7747728825 0.7323446870 - 0.7670903802 0.7753714919 0.7329847813 - 0.7676901221 0.7759671211 0.7336224914 - 0.7682873011 0.7765594721 0.7342575788 - 0.7688844204 0.7771518230 0.7348927855 - 0.7694815993 0.7777441740 0.7355279922 - 0.7700763941 0.7783325911 0.7361595035 - 0.7706704736 0.7789196968 0.7367901206 - 0.7712646127 0.7795069218 0.7374206781 - 0.7718585730 0.7800940275 0.7380509973 - 0.7724485993 0.7806769013 0.7386764884 - 0.7730386257 0.7812598944 0.7393019795 - 0.7736285925 0.7818428874 0.7399274707 - 0.7742174268 0.7824249268 0.7405520082 - 0.7748020887 0.7830036879 0.7411739230 - 0.7753869295 0.7835826278 0.7417957187 - 0.7759717107 0.7841613889 0.7424175143 - 0.7765550017 0.7847386003 0.7430375218 - 0.7771365047 0.7853134871 0.7436553240 - 0.7777178884 0.7858883739 0.7442730069 - 0.7782993913 0.7864633203 0.7448908091 - 0.7788782716 0.7870357037 0.7455062270 - 0.7794554830 0.7876065969 0.7461202145 - 0.7800326943 0.7881773710 0.7467340827 - 0.7806097865 0.7887482047 0.7473481297 - 0.7811824083 0.7893161178 0.7479594946 - 0.7817530036 0.7898828983 0.7485699058 - 0.7823237181 0.7904496789 0.7491803765 - 0.7828943729 0.7910165191 0.7497909069 - 0.7834607959 0.7915802002 0.7503979802 - 0.7840262055 0.7921432257 0.7510045171 - 0.7845916152 0.7927060723 0.7516109943 - 0.7851570249 0.7932690978 0.7522174716 - 0.7857185006 0.7938284874 0.7528216839 - 0.7862794995 0.7943874002 0.7534257174 - 0.7868404984 0.7949463129 0.7540296912 - 0.7874014974 0.7955052853 0.7546337247 - 0.7879565954 0.7960606217 0.7552338839 - 0.7885110974 0.7966156006 0.7558339238 - 0.7890654802 0.7971706986 0.7564339042 - 0.7896199822 0.7977256775 0.7570338249 - 0.7901695967 0.7982773185 0.7576287985 - 0.7907186747 0.7988284826 0.7582232952 - 0.7912678123 0.7993797064 0.7588177919 - 0.7918167710 0.7999309897 0.7594122887 - 0.7923620939 0.8004786968 0.7600033283 - 0.7929067016 0.8010259271 0.7605937719 - 0.7934514284 0.8015732169 0.7611842752 - 0.7939960957 0.8021203876 0.7617747188 - 0.7945355177 0.8026636839 0.7623630762 - 0.7950735092 0.8032060862 0.7629508972 - 0.7956116199 0.8037484288 0.7635387778 - 0.7961496711 0.8042907715 0.7641267180 - 0.7966831923 0.8048292994 0.7647116780 - 0.7972148061 0.8053662181 0.7652955055 - 0.7977464199 0.8059030771 0.7658793926 - 0.7982779741 0.8064399958 0.7664632797 - 0.7988070846 0.8069747090 0.7670440078 - 0.7993342876 0.8075076938 0.7676222920 - 0.7998613715 0.8080406785 0.7682006955 - 0.8003885746 0.8085737228 0.7687789798 - 0.8009144068 0.8091049194 0.7693557739 - 0.8014380932 0.8096331954 0.7699301243 - 0.8019618988 0.8101614714 0.7705044150 - 0.8024855852 0.8106899261 0.7710787058 - 0.8030084968 0.8112171888 0.7716525197 - 0.8035271764 0.8117402792 0.7722241282 - 0.8040459752 0.8122633100 0.7727957964 - 0.8045647144 0.8127862811 0.7733674049 - 0.8050835133 0.8133093119 0.7739390135 - 0.8055983782 0.8138288856 0.7745084763 - 0.8061131239 0.8143482208 0.7750778198 - 0.8066278100 0.8148674965 0.7756472230 - 0.8071424961 0.8153867722 0.7762165070 - 0.8076552153 0.8159031868 0.7767828703 - 0.8081669211 0.8164179921 0.7773479819 - 0.8086786270 0.8169327974 0.7779129744 - 0.8091902137 0.8174476027 0.7784780264 - 0.8097003102 0.8179603219 0.7790415287 - 0.8102073073 0.8184689879 0.7796025872 - 0.8107144237 0.8189777136 0.7801635861 - 0.8112214804 0.8194864988 0.7807245851 - 0.8117284775 0.8199952245 0.7812855840 - 0.8122318983 0.8205006123 0.7818430066 - 0.8127353191 0.8210061193 0.7824004889 - 0.8132386804 0.8215116262 0.7829579711 - 0.8137421012 0.8220170140 0.7835153937 - 0.8142439127 0.8225206733 0.7840704918 - 0.8147445917 0.8230230808 0.7846242189 - 0.8152452707 0.8235256076 0.7851778269 - 0.8157460093 0.8240280747 0.7857314944 - 0.8162457943 0.8245295286 0.7862842083 - 0.8167423010 0.8250265121 0.7868337035 - 0.8172388077 0.8255233765 0.7873830795 - 0.8177353144 0.8260204196 0.7879325747 - 0.8182318211 0.8265172839 0.7884820104 - 0.8187258244 0.8270121813 0.7890285850 - 0.8192189932 0.8275064230 0.7895743251 - 0.8197121024 0.8280006051 0.7901200056 - 0.8202052712 0.8284947872 0.7906656265 - 0.8206979036 0.8289883137 0.7912104726 - 0.8211886883 0.8294799924 0.7917528152 - 0.8216794729 0.8299716711 0.7922950983 - 0.8221703172 0.8304634094 0.7928373814 - 0.8226609826 0.8309550285 0.7933797240 - 0.8231490850 0.8314424157 0.7939193249 - 0.8236359954 0.8319281936 0.7944579124 - 0.8241229057 0.8324139118 0.7949965000 - 0.8246098161 0.8328995705 0.7955352068 - 0.8250963092 0.8333846927 0.7960731983 - 0.8255802989 0.8338664770 0.7966079116 - 0.8260642290 0.8343483210 0.7971426845 - 0.8265480995 0.8348299861 0.7976773977 - 0.8270320892 0.8353117704 0.7982121110 - 0.8275148869 0.8357924819 0.7987449169 - 0.8279966712 0.8362724185 0.7992761135 - 0.8284785748 0.8367521763 0.7998073101 - 0.8289604187 0.8372321129 0.8003385067 - 0.8294422030 0.8377118707 0.8008695841 - 0.8299208879 0.8381869793 0.8013979197 - 0.8303992152 0.8386614919 0.8019258976 - 0.8308774829 0.8391360044 0.8024538755 - 0.8313558102 0.8396105170 0.8029819131 - 0.8318336010 0.8400840759 0.8035091162 - 0.8323091865 0.8405542970 0.8040336967 - 0.8327847719 0.8410243988 0.8045582771 - 0.8332604170 0.8414946198 0.8050829172 - 0.8337361217 0.8419647813 0.8056074977 - 0.8342108130 0.8424335122 0.8061311245 - 0.8346846104 0.8429005742 0.8066536188 - 0.8351584077 0.8433678150 0.8071761131 - 0.8356322050 0.8438348770 0.8076986074 - 0.8361061215 0.8443019986 0.8082212210 - 0.8365774751 0.8447657824 0.8087412715 - 0.8370481133 0.8452283740 0.8092607260 - 0.8375186920 0.8456910253 0.8097800016 - 0.8379892111 0.8461534977 0.8102992773 - 0.8384597898 0.8466160893 0.8108186722 - 0.8389282227 0.8470754027 0.8113338947 - 0.8393964171 0.8475344181 0.8118488789 - 0.8398646116 0.8479933739 0.8123638034 - 0.8403326869 0.8484523892 0.8128787875 - 0.8408008218 0.8489112258 0.8133934736 - 0.8412675261 0.8493679166 0.8139054179 - 0.8417341113 0.8498246074 0.8144173026 - 0.8422008157 0.8502812982 0.8149293065 - 0.8426674008 0.8507381082 0.8154411912 - 0.8431332707 0.8511940241 0.8159524202 - 0.8435961008 0.8516469002 0.8164612055 - 0.8440588713 0.8520997763 0.8169698715 - 0.8445217013 0.8525527120 0.8174785972 - 0.8449844718 0.8530055881 0.8179873228 - 0.8454462886 0.8534573913 0.8184950948 - 0.8459054232 0.8539065123 0.8190008998 - 0.8463646173 0.8543556929 0.8195067048 - 0.8468238115 0.8548048735 0.8200125098 - 0.8472828865 0.8552539945 0.8205183148 - 0.8477417231 0.8557022214 0.8210232854 - 0.8481997848 0.8561481833 0.8215265274 - 0.8486577868 0.8565942049 0.8220297098 - 0.8491159081 0.8570402265 0.8225328922 - 0.8495739102 0.8574863076 0.8230360746 - 0.8500308990 0.8579313755 0.8235384226 - 0.8504856229 0.8583744168 0.8240389228 - 0.8509402275 0.8588173985 0.8245393038 - 0.8513947725 0.8592604995 0.8250396848 - 0.8518494964 0.8597034812 0.8255401850 - 0.8523032069 0.8601452708 0.8260402083 - 0.8527547121 0.8605837226 0.8265390992 - 0.8532062173 0.8610221148 0.8270381093 - 0.8536576033 0.8614605069 0.8275370002 - 0.8541091084 0.8618988991 0.8280360103 - 0.8545603752 0.8623365164 0.8285344243 - 0.8550112247 0.8627706766 0.8290308118 - 0.8554618955 0.8632047772 0.8295271993 - 0.8559126854 0.8636388779 0.8300235271 - 0.8563634157 0.8640729785 0.8305199146 - 0.8568140268 0.8645070195 0.8310160041 - 0.8572617769 0.8649371862 0.8315085769 - 0.8577095270 0.8653674722 0.8320013285 - 0.8581572771 0.8657976985 0.8324939013 - 0.8586050272 0.8662279844 0.8329865932 - 0.8590527773 0.8666582108 0.8334792256 - 0.8594974279 0.8670850992 0.8339697123 - 0.8599414825 0.8675115705 0.8344600201 - 0.8603855968 0.8679379821 0.8349502087 - 0.8608297110 0.8683645129 0.8354403973 - 0.8612738848 0.8687909245 0.8359307051 - 0.8617168069 0.8692150712 0.8364191055 - 0.8621590734 0.8696380854 0.8369066715 - 0.8626012802 0.8700612187 0.8373944163 - 0.8630436063 0.8704841733 0.8378819823 - 0.8634858727 0.8709071875 0.8383696079 - 0.8639271855 0.8713290095 0.8388562799 - 0.8643670082 0.8717489839 0.8393415809 - 0.8648067713 0.8721688986 0.8398268819 - 0.8652465940 0.8725888729 0.8403123021 - 0.8656864166 0.8730089068 0.8407976031 - 0.8661258817 0.8734285831 0.8412827253 - 0.8665626049 0.8738455176 0.8417659998 - 0.8669993281 0.8742625117 0.8422493935 - 0.8674361110 0.8746795058 0.8427327275 - 0.8678727746 0.8750964999 0.8432160020 - 0.8683094978 0.8755133748 0.8436992764 - 0.8687452078 0.8759278059 0.8441810012 - 0.8691806197 0.8763412237 0.8446620703 - 0.8696159720 0.8767547011 0.8451433182 - 0.8700513840 0.8771681786 0.8456243873 - 0.8704867959 0.8775817156 0.8461055160 - 0.8709213734 0.8779937029 0.8465862870 - 0.8713548183 0.8784034848 0.8470665216 - 0.8717882037 0.8788132071 0.8475466967 - 0.8722217083 0.8792229295 0.8480268717 - 0.8726550937 0.8796325922 0.8485071063 - 0.8730884790 0.8800423741 0.8489872217 - 0.8735179901 0.8804501295 0.8494654894 - 0.8739473224 0.8808577061 0.8499435782 - 0.8743764758 0.8812652826 0.8504217267 - 0.8748058081 0.8816729188 0.8508998752 - 0.8752350211 0.8820804954 0.8513780236 - 0.8756635189 0.8824869990 0.8518546820 - 0.8760908842 0.8828920126 0.8523296714 - 0.8765184283 0.8832970262 0.8528047204 - 0.8769457936 0.8837019801 0.8532798290 - 0.8773732781 0.8841069937 0.8537548184 - 0.8778008223 0.8845118880 0.8542299271 - 0.8782266974 0.8849135041 0.8547031283 - 0.8786525726 0.8853148222 0.8551763296 - 0.8790785074 0.8857160211 0.8556494713 - 0.8795043230 0.8861172795 0.8561226130 - 0.8799301982 0.8865185976 0.8565958142 - 0.8803547025 0.8869189024 0.8570681810 - 0.8807768822 0.8873174787 0.8575392962 - 0.8811990023 0.8877161145 0.8580104113 - 0.8816211820 0.8881146908 0.8584814072 - 0.8820433021 0.8885133266 0.8589525223 - 0.8824654818 0.8889119029 0.8594235778 - 0.8828865886 0.8893092871 0.8598933816 - 0.8833072782 0.8897061944 0.8603627086 - 0.8837280869 0.8901032209 0.8608320951 - 0.8841487765 0.8905001879 0.8613014221 - 0.8845695257 0.8908970952 0.8617706895 - 0.8849902153 0.8912938237 0.8622398973 - 0.8854097128 0.8916872740 0.8627077937 - 0.8858292103 0.8920807242 0.8631755710 - 0.8862488270 0.8924741149 0.8636435270 - 0.8866683245 0.8928675056 0.8641113043 - 0.8870878220 0.8932608962 0.8645792007 - 0.8875058293 0.8936526775 0.8650463820 - 0.8879212141 0.8940414786 0.8655126095 - 0.8883365989 0.8944302797 0.8659787178 - 0.8887521029 0.8948190808 0.8664448857 - 0.8891674876 0.8952078223 0.8669111133 - 0.8895828724 0.8955966234 0.8673772812 - 0.8899958134 0.8959844708 0.8678423166 - 0.8904073238 0.8963717818 0.8683065176 - 0.8908187151 0.8967590928 0.8687707782 - 0.8912302256 0.8971464038 0.8692349792 - 0.8916416168 0.8975337148 0.8696991801 - 0.8920531273 0.8979210258 0.8701635003 - 0.8924632072 0.8983063102 0.8706263900 - 0.8928729892 0.8986909986 0.8710889816 - 0.8932827115 0.8990756869 0.8715515733 - 0.8936924934 0.8994604945 0.8720142245 - 0.8941022754 0.8998451829 0.8724768758 - 0.8945121169 0.9002298713 0.8729395270 - 0.8949183226 0.9006112218 0.8734009266 - 0.8953242898 0.9009922743 0.8738622069 - 0.8957303762 0.9013733268 0.8743236065 - 0.8961364031 0.9017543793 0.8747850060 - 0.8965424895 0.9021354914 0.8752462864 - 0.8969483972 0.9025166035 0.8757076263 - 0.8973509073 0.9028971195 0.8761674166 - 0.8977534175 0.9032775760 0.8766270876 - 0.8981559277 0.9036580920 0.8770868778 - 0.8985583186 0.9040386081 0.8775466084 - 0.8989608288 0.9044191837 0.8780063987 - 0.8993632197 0.9047995210 0.8784658909 - 0.8997644186 0.9051784277 0.8789238930 - 0.9001656175 0.9055572748 0.8793817759 - 0.9005668163 0.9059361219 0.8798397183 - 0.9009678960 0.9063150287 0.8802977204 - 0.9013690948 0.9066938758 0.8807556033 - 0.9017699957 0.9070724249 0.8812134266 - 0.9021677971 0.9074478149 0.8816698194 - 0.9025657177 0.9078232050 0.8821262717 - 0.9029635191 0.9081985950 0.8825827241 - 0.9033613801 0.9085739851 0.8830391765 - 0.9037591815 0.9089493155 0.8834956288 - 0.9041569233 0.9093245864 0.8839520812 - 0.9045507908 0.9096974730 0.8844081163 - 0.9049447179 0.9100704193 0.8848640919 - 0.9053385854 0.9104431868 0.8853201270 - 0.9057325125 0.9108160734 0.8857762218 - 0.9061263800 0.9111890197 0.8862321973 - 0.9065201879 0.9115619063 0.8866881728 - 0.9069120288 0.9119333029 0.8871427774 - 0.9073035121 0.9123045802 0.8875973225 - 0.9076951146 0.9126759768 0.8880518079 - 0.9080867171 0.9130473137 0.8885064125 - 0.9084782004 0.9134187102 0.8889608979 - 0.9088698030 0.9137899876 0.8894153833 - 0.9092592001 0.9141587019 0.8898677826 - 0.9096478820 0.9145267010 0.8903195858 - 0.9100366235 0.9148947001 0.8907713890 - 0.9104254246 0.9152626991 0.8912233114 - 0.9108141065 0.9156306982 0.8916751146 - 0.9112027884 0.9159986973 0.8921269178 - 0.9115895033 0.9163650274 0.8925778270 - 0.9119747877 0.9167301059 0.8930280209 - 0.9123600721 0.9170951843 0.8934782743 - 0.9127454162 0.9174603224 0.8939285278 - 0.9131307006 0.9178254008 0.8943787217 - 0.9135159850 0.9181904793 0.8948289752 - 0.9139006734 0.9185553789 0.8952789903 - 0.9142842293 0.9189199805 0.8957288265 - 0.9146677256 0.9192845225 0.8961784840 - 0.9150511026 0.9196491241 0.8966283202 - 0.9154345989 0.9200137258 0.8970779777 - 0.9158180952 0.9203782082 0.8975276947 - 0.9162014723 0.9207428098 0.8979774117 - 0.9165825844 0.9211050272 0.8984262943 - 0.9169636965 0.9214671850 0.8988751769 - 0.9173448086 0.9218295217 0.8993239999 - 0.9177259207 0.9221916795 0.8997728825 - 0.9181070924 0.9225540161 0.9002218246 - 0.9184882045 0.9229161739 0.9006705880 - 0.9188672900 0.9232757092 0.9011185765 - 0.9192454815 0.9236338735 0.9015660286 - 0.9196236134 0.9239919782 0.9020134807 - 0.9200018048 0.9243502021 0.9024609923 - 0.9203799963 0.9247083068 0.9029083848 - 0.9207581282 0.9250664711 0.9033558965 - 0.9211360216 0.9254242182 0.9038028717 - 0.9215127826 0.9257805943 0.9042485952 - 0.9218896031 0.9261369705 0.9046943188 - 0.9222664237 0.9264934063 0.9051399827 - 0.9226431847 0.9268497825 0.9055857062 - 0.9230201244 0.9272062182 0.9060314298 - 0.9233968854 0.9275625944 0.9064772129 - 0.9237722158 0.9279180169 0.9069222808 - 0.9241471291 0.9282730818 0.9073672891 - 0.9245219827 0.9286280870 0.9078122973 - 0.9248968959 0.9289832115 0.9082573056 - 0.9252718091 0.9293382764 0.9087021947 - 0.9256467223 0.9296934009 0.9091472030 - 0.9260206819 0.9300473928 0.9095919132 - 0.9263923764 0.9303987026 0.9100357890 - 0.9267640710 0.9307498932 0.9104797244 - 0.9271358252 0.9311012030 0.9109236002 - 0.9275075197 0.9314523935 0.9113674760 - 0.9278790951 0.9318037033 0.9118114114 - 0.9282507896 0.9321550131 0.9122552872 - 0.9286209941 0.9325038195 0.9126986265 - 0.9289904833 0.9328516126 0.9131416082 - 0.9293599725 0.9331995249 0.9135845900 - 0.9297295213 0.9335473180 0.9140275717 - 0.9300991297 0.9338951111 0.9144706726 - 0.9304686189 0.9342429042 0.9149137139 - 0.9308379889 0.9345906973 0.9153565764 - 0.9312061071 0.9349374175 0.9157990217 - 0.9315742254 0.9352840781 0.9162414074 - 0.9319424033 0.9356307983 0.9166836739 - 0.9323105216 0.9359775186 0.9171261191 - 0.9326786995 0.9363241792 0.9175685048 - 0.9330468178 0.9366708994 0.9180107713 - 0.9334139228 0.9370164871 0.9184523225 - 0.9337794185 0.9373604059 0.9188922048 - 0.9341449142 0.9377043247 0.9193322062 - 0.9345104098 0.9380481243 0.9197722077 - 0.9348759055 0.9383919835 0.9202122092 - 0.9352414012 0.9387359023 0.9206520915 - 0.9356068969 0.9390798211 0.9210920930 - 0.9359713793 0.9394215941 0.9215306044 - 0.9363352060 0.9397622943 0.9219684005 - 0.9366990924 0.9401029944 0.9224061966 - 0.9370629787 0.9404436946 0.9228439927 - 0.9374268055 0.9407843947 0.9232817888 - 0.9377906919 0.9411249757 0.9237195849 - 0.9381545782 0.9414656758 0.9241573811 - 0.9385176897 0.9418051839 0.9245941043 - 0.9388806820 0.9421443939 0.9250305891 - 0.9392436743 0.9424837232 0.9254671931 - 0.9396067262 0.9428228736 0.9259036779 - 0.9399697781 0.9431622028 0.9263402224 - 0.9403327703 0.9435014129 0.9267767072 - 0.9406958222 0.9438406825 0.9272131920 - 0.9410552979 0.9441781044 0.9276493788 - 0.9414148927 0.9445155859 0.9280856252 - 0.9417744875 0.9448531270 0.9285218120 - 0.9421340823 0.9451906085 0.9289579988 - 0.9424936175 0.9455280900 0.9293941259 - 0.9428532124 0.9458655715 0.9298303127 - 0.9432125092 0.9462026954 0.9302664995 - 0.9435694218 0.9465367198 0.9307025075 - 0.9439262748 0.9468708038 0.9311385751 - 0.9442831874 0.9472048283 0.9315745831 - 0.9446401000 0.9475389123 0.9320107102 - 0.9449970126 0.9478729963 0.9324467182 - 0.9453539848 0.9482070208 0.9328827858 - 0.9457105994 0.9485409260 0.9333183765 - 0.9460659027 0.9488738179 0.9337515235 - 0.9464210868 0.9492065907 0.9341846108 - 0.9467762709 0.9495394826 0.9346178174 - 0.9471315145 0.9498723149 0.9350509048 - 0.9474868178 0.9502052069 0.9354839921 - 0.9478420019 0.9505379796 0.9359171987 - 0.9481968880 0.9508708119 0.9363495708 - 0.9485492110 0.9512028098 0.9367771745 - 0.9489015937 0.9515348077 0.9372048974 - 0.9492539167 0.9518669248 0.9376325011 - 0.9496062994 0.9521989226 0.9380601048 - 0.9499586225 0.9525309205 0.9384877086 - 0.9503110051 0.9528629184 0.9389153719 - 0.9506632090 0.9531947970 0.9393426776 - 0.9510114789 0.9535222054 0.9397646785 - 0.9513596892 0.9538496733 0.9401866794 - 0.9517080188 0.9541772008 0.9406086802 - 0.9520562291 0.9545047283 0.9410306811 - 0.9524044991 0.9548320770 0.9414526224 - 0.9527528286 0.9551596045 0.9418746233 - 0.9531009793 0.9554870725 0.9422966242 - 0.9534440041 0.9558085203 0.9427149892 - 0.9537863135 0.9561291933 0.9431329966 - 0.9541286230 0.9564499259 0.9435510039 - 0.9544708729 0.9567705989 0.9439690113 - 0.9548131824 0.9570913911 0.9443870187 - 0.9551554918 0.9574121237 0.9448050261 - 0.9554979205 0.9577327967 0.9452229738 - 0.9558368921 0.9580510259 0.9456391931 - 0.9561746120 0.9583681226 0.9460545182 - 0.9565122724 0.9586851001 0.9464697838 - 0.9568499923 0.9590021968 0.9468852282 - 0.9571877122 0.9593192935 0.9473004937 - 0.9575253725 0.9596363902 0.9477158785 - 0.9578630924 0.9599534869 0.9481312037 - 0.9581995010 0.9602695107 0.9485455155 - 0.9585341215 0.9605841041 0.9489583969 - 0.9588688016 0.9608986974 0.9493712783 - 0.9592034817 0.9612134099 0.9497842193 - 0.9595381021 0.9615280032 0.9501971006 - 0.9598727822 0.9618425965 0.9506099224 - 0.9602074027 0.9621571898 0.9510228038 - 0.9605417848 0.9624714851 0.9514353871 - 0.9608737826 0.9627835751 0.9518461227 - 0.9612057805 0.9630956054 0.9522566795 - 0.9615378976 0.9634075761 0.9526674151 - 0.9618698955 0.9637196064 0.9530779719 - 0.9622018933 0.9640315771 0.9534885883 - 0.9625340104 0.9643437266 0.9538993239 - 0.9628660083 0.9646556973 0.9543098807 - 0.9631955028 0.9649661779 0.9547188878 - 0.9635242820 0.9652761221 0.9551271796 - 0.9638530016 0.9655861259 0.9555355906 - 0.9641817212 0.9658960104 0.9559440017 - 0.9645103812 0.9662060142 0.9563524127 - 0.9648391008 0.9665158987 0.9567608237 - 0.9651678205 0.9668259025 0.9571691751 - 0.9654955268 0.9671350121 0.9575765729 - 0.9658207297 0.9674422145 0.9579817057 - 0.9661458731 0.9677494168 0.9583867788 - 0.9664710760 0.9680566788 0.9587919116 - 0.9667962790 0.9683638811 0.9591971040 - 0.9671214819 0.9686710835 0.9596021771 - 0.9674468040 0.9689784050 0.9600073099 - 0.9677720070 0.9692856073 0.9604123831 - 0.9680947065 0.9695898294 0.9608144760 - 0.9684168100 0.9698932767 0.9612157941 - 0.9687389135 0.9701967239 0.9616171718 - 0.9690610170 0.9705001712 0.9620184898 - 0.9693831205 0.9708036780 0.9624199271 - 0.9697052836 0.9711071253 0.9628211856 - 0.9700273871 0.9714105725 0.9632225037 - 0.9703485966 0.9717134237 0.9636229277 - 0.9706671238 0.9720138907 0.9640201926 - 0.9709854722 0.9723144770 0.9644175172 - 0.9713038802 0.9726150036 0.9648149014 - 0.9716224074 0.9729155898 0.9652122259 - 0.9719408154 0.9732161760 0.9656094909 - 0.9722592831 0.9735167027 0.9660068154 - 0.9725776911 0.9738172889 0.9664040804 - 0.9728938937 0.9741159081 0.9667984843 - 0.9732084870 0.9744132757 0.9671909213 - 0.9735231996 0.9747105837 0.9675834179 - 0.9738377929 0.9750080109 0.9679757953 - 0.9741525054 0.9753053784 0.9683682919 - 0.9744670987 0.9756028056 0.9687607884 - 0.9747818112 0.9759001136 0.9691532254 - 0.9750964046 0.9761974812 0.9695457220 - 0.9754077196 0.9764910936 0.9699333906 - 0.9757186174 0.9767844081 0.9703207016 - 0.9760295749 0.9770777225 0.9707080126 - 0.9763404727 0.9773709774 0.9710952044 - 0.9766514897 0.9776641726 0.9714825153 - 0.9769623876 0.9779574871 0.9718698263 - 0.9772734046 0.9782508016 0.9722570777 - 0.9775835872 0.9785434008 0.9726434946 - 0.9778903127 0.9788324833 0.9730253816 - 0.9781969786 0.9791216254 0.9734073877 - 0.9785035849 0.9794107080 0.9737892747 - 0.9788103104 0.9796997905 0.9741712213 - 0.9791169763 0.9799888730 0.9745532274 - 0.9794237018 0.9802780151 0.9749351144 - 0.9797304273 0.9805672169 0.9753171206 - 0.9800354242 0.9808546901 0.9756969213 - 0.9803376198 0.9811394811 0.9760727882 - 0.9806396961 0.9814242125 0.9764487147 - 0.9809417725 0.9817090034 0.9768247008 - 0.9812439084 0.9819936752 0.9772006273 - 0.9815461040 0.9822785258 0.9775764942 - 0.9818481803 0.9825631976 0.9779524803 - 0.9821503162 0.9828479886 0.9783284068 - 0.9824500084 0.9831303954 0.9787008166 - 0.9827467799 0.9834101796 0.9790692925 - 0.9830436707 0.9836900234 0.9794378281 - 0.9833405018 0.9839698076 0.9798063040 - 0.9836373925 0.9842494726 0.9801747799 - 0.9839342237 0.9845293164 0.9805433154 - 0.9842311144 0.9848091006 0.9809117913 - 0.9845278859 0.9850888848 0.9812803268 - 0.9848216176 0.9853662848 0.9816443920 - 0.9851121902 0.9856415987 0.9820045233 - 0.9854028225 0.9859169126 0.9823644757 - 0.9856935143 0.9861922264 0.9827246070 - 0.9859840870 0.9864674807 0.9830846786 - 0.9862747192 0.9867427945 0.9834446907 - 0.9865652919 0.9870181084 0.9838048220 - 0.9868559837 0.9872933030 0.9841647744 - 0.9871436954 0.9875661731 0.9845203161 - 0.9874286056 0.9878365993 0.9848713875 - 0.9877135158 0.9881070256 0.9852225184 - 0.9879983068 0.9883773923 0.9855734706 - 0.9882832170 0.9886478186 0.9859246016 - 0.9885681272 0.9889181852 0.9862756729 - 0.9888529778 0.9891886115 0.9866266847 - 0.9891378880 0.9894589782 0.9869778156 - 0.9894198179 0.9897269011 0.9873241782 - 0.9896976948 0.9899914265 0.9876642823 - 0.9899755120 0.9902557731 0.9880045056 - 0.9902533889 0.9905201793 0.9883446097 - 0.9905313253 0.9907847047 0.9886847138 - 0.9908092022 0.9910491109 0.9890248179 - 0.9910870194 0.9913135767 0.9893649817 - 0.9913648963 0.9915779829 0.9897050858 - 0.9916403294 0.9918411970 0.9900411963 - 0.9919093251 0.9921011925 0.9903668165 - 0.9921782017 0.9923611879 0.9906923175 - 0.9924470782 0.9926211834 0.9910178781 - 0.9927160144 0.9928811789 0.9913433790 - 0.9929850101 0.9931411743 0.9916689992 - 0.9932538867 0.9934012294 0.9919946194 - 0.9935228229 0.9936612248 0.9923201203 - 0.9937912822 0.9939208031 0.9926447868 - 0.9940528274 0.9941738844 0.9929555058 - 0.9943143129 0.9944270253 0.9932662249 - 0.9945756793 0.9946801066 0.9935768843 - 0.9948372245 0.9949333072 0.9938876033 - 0.9950987101 0.9951863885 0.9941983223 - 0.9953601956 0.9954395294 0.9945089817 - 0.9956216216 0.9956926107 0.9948197007 - 0.9958831072 0.9959456921 0.9951304793 - 0.9961357713 0.9961907864 0.9954292774 - 0.9963858724 0.9964336157 0.9957246780 - 0.9966359735 0.9966763258 0.9960200787 - 0.9968860745 0.9969190955 0.9963154793 - 0.9971361756 0.9971618056 0.9966108799 - 0.9973862767 0.9974045753 0.9969062805 - 0.9976363778 0.9976472855 0.9972016811 - 0.9978864789 0.9978901148 0.9974970818 - 0.9981297851 0.9981291294 0.9977846742 - 0.9983634949 0.9983630180 0.9980615973 - 0.9985973239 0.9985967875 0.9983385205 - 0.9988310933 0.9988306761 0.9986153841 - 0.9990649223 0.9990645051 0.9988924265 - 0.9992986917 0.9992983937 0.9991692901 - 0.9995324016 0.9995322824 0.9994462132 - 0.9997661710 0.9997661114 0.9997230768 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0171004608 0.0156014897 0.0110214697 + 0.0296423100 0.0275058597 0.0204370096 + 0.0400087796 0.0378916785 0.0286195297 + 0.0492478795 0.0473487712 0.0360689983 + 0.0577691793 0.0561984889 0.0429962091 + 0.0658048466 0.0644933432 0.0495528691 + 0.0734695420 0.0724152401 0.0558067188 + 0.0808440894 0.0800366476 0.0618191212 + 0.0879925117 0.0873461664 0.0676262975 + 0.0949557200 0.0944325328 0.0732697919 + 0.1018247008 0.1012879983 0.0787453130 + 0.1086272970 0.1079066992 0.0840507299 + 0.1152980998 0.1143304035 0.0892019123 + 0.1217757985 0.1205366999 0.0942221731 + 0.1280754060 0.1265349984 0.0991079137 + 0.1341703981 0.1323578954 0.1038789004 + 0.1400842071 0.1380200982 0.1085413024 + 0.1458258033 0.1435257047 0.1130928993 + 0.1513952017 0.1488672942 0.1175398976 + 0.1568222046 0.1540704072 0.1219094992 + 0.1621166021 0.1591500938 0.1262044013 + 0.1672838926 0.1641122997 0.1304340065 + 0.1723344028 0.1689572036 0.1346047074 + 0.1772560030 0.1737031043 0.1387116015 + 0.1820691973 0.1783441007 0.1427484006 + 0.1867853999 0.1828867942 0.1467114985 + 0.1914003938 0.1873379946 0.1506099999 + 0.1959276944 0.1916999966 0.1544381976 + 0.2003667951 0.1959740072 0.1582106054 + 0.2047297060 0.2001685053 0.1619343013 + 0.2090197951 0.2042849064 0.1656126976 + 0.2132406980 0.2083213031 0.1692394018 + 0.2173902988 0.2122910023 0.1728218049 + 0.2214687020 0.2161971927 0.1763650030 + 0.2254846990 0.2200367004 0.1798641980 + 0.2294346988 0.2238188982 0.1833273023 + 0.2333287001 0.2275474966 0.1867597997 + 0.2371630073 0.2312255949 0.1901571006 + 0.2409424931 0.2348573953 0.1935154945 + 0.2446697950 0.2384261042 0.1968335062 + 0.2483461946 0.2419455051 0.2001166940 + 0.2519711852 0.2454174012 0.2033713013 + 0.2555448115 0.2488500029 0.2066029012 + 0.2590776980 0.2522298098 0.2098037004 + 0.2625567913 0.2555679977 0.2129680067 + 0.2659949958 0.2588672936 0.2161033005 + 0.2693926096 0.2621265948 0.2192150950 + 0.2727515101 0.2653498054 0.2222974002 + 0.2760717869 0.2685379982 0.2253466994 + 0.2793444991 0.2716934979 0.2283667028 + 0.2825835049 0.2748126090 0.2313646972 + 0.2857859135 0.2778981924 0.2343385071 + 0.2889564931 0.2809517980 0.2372840047 + 0.2920953929 0.2839730978 0.2402049005 + 0.2951973081 0.2869609892 0.2431025058 + 0.2982689142 0.2899155021 0.2459677011 + 0.3013125062 0.2928346097 0.2488068938 + 0.3043302894 0.2957249880 0.2516252100 + 0.3073143959 0.2985872030 0.2544235885 + 0.3102650940 0.3014270067 0.2572003901 + 0.3131817877 0.3042409122 0.2599579096 + 0.3160701096 0.3070298135 0.2626875937 + 0.3189300001 0.3097898066 0.2653909028 + 0.3217678964 0.3125233054 0.2680732906 + 0.3245871961 0.3152284920 0.2707414031 + 0.3273836076 0.3179099858 0.2733910084 + 0.3301528096 0.3205696046 0.2760204077 + 0.3328959048 0.3232055902 0.2786307931 + 0.3356123865 0.3258267045 0.2812187076 + 0.3382978141 0.3284305036 0.2837873995 + 0.3409596980 0.3310135007 0.2863405049 + 0.3436000943 0.3335742950 0.2888756096 + 0.3462189138 0.3361156881 0.2913906872 + 0.3488169909 0.3386341035 0.2938865125 + 0.3513996005 0.3411293030 0.2963629961 + 0.3539629877 0.3436000943 0.2988243103 + 0.3565055132 0.3460493982 0.3012714982 + 0.3590308130 0.3484821916 0.3036991060 + 0.3615351915 0.3508949876 0.3061034977 + 0.3640230000 0.3532888889 0.3084929883 + 0.3664914966 0.3556644917 0.3108676076 + 0.3689433038 0.3580288887 0.3132284880 + 0.3713740110 0.3603788912 0.3155708909 + 0.3737848103 0.3627198935 0.3179005086 + 0.3761711121 0.3650496900 0.3202157021 + 0.3785369992 0.3673666120 0.3225137889 + 0.3808847964 0.3696695864 0.3247897923 + 0.3832105994 0.3719570935 0.3270477951 + 0.3855203986 0.3742293119 0.3292933106 + 0.3878141046 0.3764826059 0.3315246105 + 0.3900910914 0.3787195086 0.3337422907 + 0.3923526108 0.3809430897 0.3359439075 + 0.3945967853 0.3831507862 0.3381271064 + 0.3968229890 0.3853423893 0.3402962089 + 0.3990325034 0.3875181973 0.3424524069 + 0.4012250900 0.3896811008 0.3445951045 + 0.4033975899 0.3918299079 0.3467190862 + 0.4055522084 0.3939658999 0.3488298059 + 0.4076915085 0.3960888982 0.3509294987 + 0.4098165929 0.3981984854 0.3530152142 + 0.4119274020 0.4002932906 0.3550836146 + 0.4140225053 0.4023733139 0.3571367860 + 0.4161036909 0.4044389129 0.3591760099 + 0.4181733131 0.4064899981 0.3611989915 + 0.4202328026 0.4085263014 0.3632034063 + 0.4222835898 0.4105474949 0.3651930988 + 0.4243231118 0.4125525951 0.3671697974 + 0.4263541996 0.4145421982 0.3691323996 + 0.4283750057 0.4165202975 0.3710797131 + 0.4303872883 0.4184862077 0.3730098903 + 0.4323903918 0.4204401076 0.3749246895 + 0.4343844950 0.4223816991 0.3768284917 + 0.4363701046 0.4243108928 0.3787207901 + 0.4383463860 0.4262312949 0.3806005120 + 0.4403150082 0.4281423986 0.3824656010 + 0.4422720969 0.4300462902 0.3843190074 + 0.4442181885 0.4319409132 0.3861612082 + 0.4461520910 0.4338287115 0.3879927993 + 0.4480755031 0.4357072115 0.3898114860 + 0.4499849975 0.4375792146 0.3916176856 + 0.4518805146 0.4394420087 0.3934090137 + 0.4537624121 0.4412986040 0.3951860964 + 0.4556312859 0.4431461096 0.3969503045 + 0.4574877024 0.4449875951 0.3987065852 + 0.4593302011 0.4468202889 0.4004544914 + 0.4611577094 0.4486469924 0.4021933973 + 0.4629718959 0.4504652917 0.4039224088 + 0.4647724032 0.4522775114 0.4056409895 + 0.4665606022 0.4540818930 0.4073483944 + 0.4683364928 0.4558798969 0.4090481102 + 0.4700995982 0.4576708078 0.4107409120 + 0.4718486965 0.4594548941 0.4124262929 + 0.4735842943 0.4612326920 0.4141052067 + 0.4753074944 0.4630033076 0.4157783985 + 0.4770157039 0.4647684097 0.4174447060 + 0.4787110090 0.4665257037 0.4191048145 + 0.4803912044 0.4682776928 0.4207561910 + 0.4820596874 0.4700196981 0.4224033952 + 0.4837144017 0.4717536867 0.4240436852 + 0.4853559136 0.4734789133 0.4256787002 + 0.4869849980 0.4751960039 0.4273071885 + 0.4886021018 0.4769046903 0.4289287925 + 0.4902065098 0.4786028862 0.4305444062 + 0.4917967021 0.4802902043 0.4321511984 + 0.4933750033 0.4819664061 0.4337506890 + 0.4949423969 0.4836333096 0.4353412986 + 0.4964992106 0.4852910936 0.4369243979 + 0.4980452061 0.4869402051 0.4385007024 + 0.4995796978 0.4885810018 0.4400705099 + 0.5011045933 0.4902119935 0.4416334033 + 0.5026171207 0.4918344915 0.4431881905 + 0.5041204095 0.4934495986 0.4447365105 + 0.5056149960 0.4950574934 0.4462780058 + 0.5071011186 0.4966585934 0.4478121996 + 0.5085787773 0.4982511103 0.4493379891 + 0.5100476146 0.4998351932 0.4508579075 + 0.5115081072 0.5014110208 0.4523723125 + 0.5129603744 0.5029789209 0.4538812935 + 0.5144031048 0.5045387745 0.4553841949 + 0.5158382058 0.5060889721 0.4568797052 + 0.5172675848 0.5076315999 0.4583696127 + 0.5186911821 0.5091663003 0.4598538876 + 0.5201076865 0.5106902122 0.4613319039 + 0.5215175152 0.5122056007 0.4628019035 + 0.5229220986 0.5137131810 0.4642662108 + 0.5243213177 0.5152136087 0.4657247961 + 0.5257117152 0.5167068243 0.4671759903 + 0.5270925760 0.5181897879 0.4686197937 + 0.5284671783 0.5196657181 0.4700576961 + 0.5298361778 0.5211349726 0.4714899957 + 0.5311961174 0.5225929022 0.4729127884 + 0.5325514078 0.5240452290 0.4743295908 + 0.5339015126 0.5254917741 0.4757412076 + 0.5352419019 0.5269278884 0.4771471024 + 0.5365741253 0.5283558965 0.4785467982 + 0.5379003286 0.5297778845 0.4799422026 + 0.5392192006 0.5311921835 0.4813325107 + 0.5405315757 0.5325987935 0.4827156961 + 0.5418395996 0.5340006948 0.4840947092 + 0.5431419015 0.5353968143 0.4854688942 + 0.5444355011 0.5367845893 0.4868355095 + 0.5457248092 0.5381672978 0.4881992936 + 0.5470086932 0.5395433903 0.4895603955 + 0.5482844114 0.5409129858 0.4909155071 + 0.5495557189 0.5422785878 0.4922682941 + 0.5508213043 0.5436385870 0.4936183095 + 0.5520780087 0.5449923873 0.4949626923 + 0.5533294082 0.5463421941 0.4963046014 + 0.5545719862 0.5476844907 0.4976435006 + 0.5558081865 0.5490229726 0.4989764988 + 0.5570405126 0.5503587723 0.5003060102 + 0.5582659245 0.5516861081 0.5016294718 + 0.5594872832 0.5530081987 0.5029485226 + 0.5607051253 0.5543255210 0.5042636991 + 0.5619114041 0.5556346178 0.5055726767 + 0.5631139278 0.5569404960 0.5068780780 + 0.5643098950 0.5582408905 0.5081769228 + 0.5655001998 0.5595366955 0.5094692111 + 0.5666872859 0.5608298779 0.5107579827 + 0.5678644180 0.5621141195 0.5120382905 + 0.5690370202 0.5633950233 0.5133149028 + 0.5702024102 0.5646705031 0.5145862103 + 0.5713610053 0.5659371018 0.5158511996 + 0.5725166798 0.5671998858 0.5171135068 + 0.5736652017 0.5684533119 0.5183678865 + 0.5748100877 0.5697032809 0.5196194053 + 0.5759480000 0.5709466934 0.5208657980 + 0.5770794153 0.5721855164 0.5221070051 + 0.5782068968 0.5734214783 0.5233457088 + 0.5793256760 0.5746489167 0.5245773196 + 0.5804418921 0.5758739710 0.5258070230 + 0.5815505981 0.5770922899 0.5270308852 + 0.5826562047 0.5783078074 0.5282518268 + 0.5837575793 0.5795192122 0.5294687152 + 0.5848544240 0.5807257891 0.5306801200 + 0.5859488249 0.5819299221 0.5318887830 + 0.5870361924 0.5831274986 0.5330893993 + 0.5881218910 0.5843237042 0.5342882276 + 0.5891991258 0.5855125785 0.5354802012 + 0.5902739763 0.5866997838 0.5366697907 + 0.5913425088 0.5878822207 0.5378534198 + 0.5924072266 0.5890613794 0.5390337706 + 0.5934671760 0.5902367234 0.5402104855 + 0.5945214033 0.5914065838 0.5413823128 + 0.5955718160 0.5925731063 0.5425512791 + 0.5966168046 0.5937318206 0.5437142253 + 0.5976600051 0.5948877931 0.5448753834 + 0.5986986160 0.5960369110 0.5460278988 + 0.5997365713 0.5971850753 0.5471792221 + 0.6007699966 0.5983254910 0.5483239293 + 0.6018025875 0.5994653106 0.5494676232 + 0.6028274298 0.6005986929 0.5506033897 + 0.6038507819 0.6017305255 0.5517382026 + 0.6048673987 0.6028541923 0.5528683066 + 0.6058822274 0.6039755940 0.5539959073 + 0.6068912745 0.6050894260 0.5551165938 + 0.6078979969 0.6062008739 0.5562350750 + 0.6088986993 0.6073063016 0.5573483109 + 0.6098974943 0.6084094048 0.5584586859 + 0.6108921766 0.6095077991 0.5595636964 + 0.6118845940 0.6106038094 0.5606665015 + 0.6128724813 0.6116948724 0.5617650151 + 0.6138582826 0.6127820015 0.5628610849 + 0.6148399115 0.6138613224 0.5639526248 + 0.6158196926 0.6149377227 0.5650421977 + 0.6167954803 0.6160081029 0.5661283135 + 0.6177694201 0.6170759201 0.5672121048 + 0.6187387109 0.6181375980 0.5682907104 + 0.6197069287 0.6191977859 0.5693675280 + 0.6206719279 0.6202535033 0.5704392195 + 0.6216356754 0.6213079095 0.5715097785 + 0.6225941181 0.6223561764 0.5725752711 + 0.6235517263 0.6234036088 0.5736399889 + 0.6245011091 0.6244416237 0.5746986270 + 0.6254506111 0.6254796982 0.5757570267 + 0.6263933778 0.6265093088 0.5768082142 + 0.6273362041 0.6275388002 0.5778594017 + 0.6282750964 0.6285620928 0.5789057016 + 0.6292135119 0.6295846105 0.5799514055 + 0.6301469803 0.6306030154 0.5809925795 + 0.6310787201 0.6316199899 0.5820323825 + 0.6320052743 0.6326320171 0.5830696821 + 0.6329280138 0.6336405277 0.5841053724 + 0.6338480115 0.6346455216 0.5851392150 + 0.6347640753 0.6356455088 0.5861703753 + 0.6356791258 0.6366437078 0.5871999264 + 0.6365907192 0.6376357079 0.5882244110 + 0.6375023127 0.6386275291 0.5892485976 + 0.6384099126 0.6396116018 0.5902659297 + 0.6393175721 0.6405956149 0.5912833214 + 0.6402198076 0.6415734887 0.5922964215 + 0.6411203146 0.6425495744 0.5933085084 + 0.6420174241 0.6435208917 0.5943167210 + 0.6429113746 0.6444880962 0.5953217745 + 0.6438037157 0.6454532146 0.5963243246 + 0.6446918845 0.6464130878 0.5973206162 + 0.6455799937 0.6473729014 0.5983167887 + 0.6464617848 0.6483271718 0.5993061066 + 0.6473435760 0.6492815018 0.6002953053 + 0.6482205987 0.6502330899 0.6012793183 + 0.6490954757 0.6511834860 0.6022611856 + 0.6499673724 0.6521314979 0.6032401919 + 0.6508346200 0.6530753970 0.6042150855 + 0.6517016292 0.6540189981 0.6051893830 + 0.6525605917 0.6549540162 0.6061556935 + 0.6534196734 0.6558889747 0.6071221232 + 0.6542748213 0.6568189859 0.6080846786 + 0.6551280022 0.6577466130 0.6090456247 + 0.6559792757 0.6586722136 0.6100040078 + 0.6568263173 0.6595928073 0.6109567881 + 0.6576732993 0.6605134010 0.6119096279 + 0.6585139036 0.6614270210 0.6128568053 + 0.6593537927 0.6623396873 0.6138035059 + 0.6601899862 0.6632481217 0.6147469282 + 0.6610223055 0.6641519070 0.6156873107 + 0.6618543863 0.6650553942 0.6166272759 + 0.6626814008 0.6659533978 0.6175631881 + 0.6635084748 0.6668512821 0.6184991002 + 0.6643338203 0.6677461267 0.6194313169 + 0.6651577950 0.6686387062 0.6203612089 + 0.6659812927 0.6695305705 0.6212899089 + 0.6668003798 0.6704167128 0.6222109795 + 0.6676195860 0.6713029146 0.6231321096 + 0.6684346199 0.6721845269 0.6240482926 + 0.6692469716 0.6730633974 0.6249616146 + 0.6700590253 0.6739416718 0.6258742213 + 0.6708669066 0.6748149991 0.6267806888 + 0.6716747284 0.6756882071 0.6276870966 + 0.6724805236 0.6765586734 0.6285902858 + 0.6732844710 0.6774268746 0.6294907928 + 0.6740885973 0.6782950759 0.6303914189 + 0.6748862267 0.6791549921 0.6312856078 + 0.6756836176 0.6800147891 0.6321799159 + 0.6764786839 0.6808714271 0.6330708861 + 0.6772701740 0.6817231774 0.6339576244 + 0.6780617833 0.6825749874 0.6348443031 + 0.6788492799 0.6834219098 0.6357238293 + 0.6796358228 0.6842675805 0.6366016269 + 0.6804214120 0.6851121783 0.6374781132 + 0.6812025905 0.6859521270 0.6383491158 + 0.6819838285 0.6867920160 0.6392201185 + 0.6827620864 0.6876285076 0.6400873065 + 0.6835377216 0.6884618998 0.6409513950 + 0.6843134165 0.6892954111 0.6418154836 + 0.6850838065 0.6901227832 0.6426753998 + 0.6858534217 0.6909493208 0.6435347199 + 0.6866219044 0.6917747259 0.6443930268 + 0.6873850822 0.6925947070 0.6452473998 + 0.6881483197 0.6934146285 0.6461017132 + 0.6889085770 0.6942318082 0.6469531059 + 0.6896653175 0.6950455904 0.6478009820 + 0.6904219985 0.6958593726 0.6486489773 + 0.6911736131 0.6966683865 0.6494913101 + 0.6919230819 0.6974754930 0.6503316164 + 0.6926727295 0.6982825994 0.6511718035 + 0.6934152246 0.6990849972 0.6520056725 + 0.6941574216 0.6998872161 0.6528394222 + 0.6948987842 0.7006883025 0.6536720991 + 0.6956359148 0.7014837265 0.6545003057 + 0.6963729858 0.7022790909 0.6553285122 + 0.6971086264 0.7030712962 0.6561551094 + 0.6978415251 0.7038580179 0.6569787860 + 0.6985744238 0.7046447992 0.6578025818 + 0.6993039250 0.7054278255 0.6586238742 + 0.7000302076 0.7062076926 0.6594430208 + 0.7007566094 0.7069876194 0.6602622271 + 0.7014790177 0.7077631950 0.6610757113 + 0.7021995783 0.7085369229 0.6618868113 + 0.7029200792 0.7093105912 0.6626979113 + 0.7036368847 0.7100790143 0.6635041833 + 0.7043526769 0.7108461261 0.6643093824 + 0.7050685287 0.7116131783 0.6651147008 + 0.7057778239 0.7123737931 0.6659145951 + 0.7064864039 0.7131338120 0.6667140722 + 0.7071951032 0.7138937116 0.6675134897 + 0.7078974843 0.7146461010 0.6683061123 + 0.7085998058 0.7153983116 0.6690986156 + 0.7093018889 0.7161502838 0.6698907018 + 0.7100000978 0.7168943882 0.6706752181 + 0.7106983066 0.7176384926 0.6714596748 + 0.7113963962 0.7183822989 0.6722435951 + 0.7120925784 0.7191202044 0.6730211973 + 0.7127888799 0.7198581100 0.6737989187 + 0.7134850025 0.7205957174 0.6745759845 + 0.7141777277 0.7213283181 0.6753479838 + 0.7148702741 0.7220609188 0.6761199236 + 0.7155628204 0.7227932215 0.6768916249 + 0.7162505984 0.7235177755 0.6776590943 + 0.7169383764 0.7242423296 0.6784266233 + 0.7176260948 0.7249668837 0.6791939735 + 0.7183110714 0.7256841063 0.6799570918 + 0.7189961076 0.7264013886 0.6807202101 + 0.7196810246 0.7271186113 0.6814832091 + 0.7203640938 0.7278298140 0.6822391748 + 0.7210471034 0.7285404801 0.6829947233 + 0.7217299938 0.7292512059 0.6837502122 + 0.7224094272 0.7299574018 0.6845011711 + 0.7230880857 0.7306627035 0.6852512956 + 0.7237666845 0.7313678861 0.6860014200 + 0.7244430184 0.7320697904 0.6867479086 + 0.7251182199 0.7327703238 0.6874929070 + 0.7257934213 0.7334707975 0.6882380247 + 0.7264670730 0.7341670990 0.6889796257 + 0.7271397114 0.7348600030 0.6897184253 + 0.7278121710 0.7355527878 0.6904572845 + 0.7284829021 0.7362431884 0.6911934018 + 0.7291505933 0.7369294763 0.6919252872 + 0.7298182249 0.7376158237 0.6926571131 + 0.7304852009 0.7383013964 0.6933879256 + 0.7311487794 0.7389839888 0.6941142082 + 0.7318122983 0.7396667004 0.6948403716 + 0.7324758768 0.7403492928 0.6955665946 + 0.7331362963 0.7410271168 0.6962887049 + 0.7337965965 0.7417047024 0.6970105767 + 0.7344568968 0.7423822284 0.6977325082 + 0.7351133823 0.7430555820 0.6984499097 + 0.7357681990 0.7437270880 0.6991655231 + 0.7364230156 0.7443987131 0.6998810172 + 0.7370759249 0.7450683713 0.7005940080 + 0.7377259731 0.7457355261 0.7013034821 + 0.7383760810 0.7464025021 0.7020128965 + 0.7390258908 0.7470691204 0.7027217150 + 0.7396721244 0.7477303743 0.7034254074 + 0.7403182983 0.7483916879 0.7041289806 + 0.7409644723 0.7490528822 0.7048326731 + 0.7416077852 0.7497091293 0.7055320740 + 0.7422502041 0.7503637075 0.7062302828 + 0.7428925037 0.7510182858 0.7069284916 + 0.7435333729 0.7516707778 0.7076249719 + 0.7441717982 0.7523202896 0.7083188891 + 0.7448102236 0.7529696822 0.7090129256 + 0.7454485893 0.7536191940 0.7097067833 + 0.7460827231 0.7542620897 0.7103967071 + 0.7467167974 0.7549051046 0.7110865116 + 0.7473508716 0.7555481195 0.7117763758 + 0.7479826212 0.7561879754 0.7124634981 + 0.7486125231 0.7568256259 0.7131487131 + 0.7492423058 0.7574632764 0.7138339281 + 0.7498719096 0.7581006289 0.7145183086 + 0.7504996061 0.7587358952 0.7151979804 + 0.7511271834 0.7593712211 0.7158777118 + 0.7517548203 0.7600064874 0.7165573835 + 0.7523803115 0.7606381178 0.7172330022 + 0.7530044913 0.7612677217 0.7179064155 + 0.7536287904 0.7618973851 0.7185798883 + 0.7542523742 0.7625262141 0.7192522883 + 0.7548720837 0.7631500959 0.7199187875 + 0.7554919124 0.7637740970 0.7205854058 + 0.7561116219 0.7643979788 0.7212520242 + 0.7567297816 0.7650203109 0.7219150066 + 0.7573469877 0.7656415105 0.7225760221 + 0.7579641938 0.7662625909 0.7232369781 + 0.7585812807 0.7668836117 0.7238978148 + 0.7591949105 0.7674986720 0.7245547175 + 0.7598084807 0.7681139112 0.7252116203 + 0.7604219913 0.7687290907 0.7258685231 + 0.7610334754 0.7693409920 0.7265231013 + 0.7616422772 0.7699488997 0.7271751165 + 0.7622511983 0.7705568075 0.7278271914 + 0.7628600001 0.7711647153 0.7284792066 + 0.7634664774 0.7717682719 0.7291265130 + 0.7640724778 0.7723709941 0.7297729254 + 0.7646784782 0.7729737163 0.7304192781 + 0.7652838826 0.7735757232 0.7310644984 + 0.7658861279 0.7741742730 0.7317045927 + 0.7664881945 0.7747728825 0.7323446870 + 0.7670903802 0.7753714919 0.7329847813 + 0.7676901221 0.7759671211 0.7336224914 + 0.7682873011 0.7765594721 0.7342575788 + 0.7688844204 0.7771518230 0.7348927855 + 0.7694815993 0.7777441740 0.7355279922 + 0.7700763941 0.7783325911 0.7361595035 + 0.7706704736 0.7789196968 0.7367901206 + 0.7712646127 0.7795069218 0.7374206781 + 0.7718585730 0.7800940275 0.7380509973 + 0.7724485993 0.7806769013 0.7386764884 + 0.7730386257 0.7812598944 0.7393019795 + 0.7736285925 0.7818428874 0.7399274707 + 0.7742174268 0.7824249268 0.7405520082 + 0.7748020887 0.7830036879 0.7411739230 + 0.7753869295 0.7835826278 0.7417957187 + 0.7759717107 0.7841613889 0.7424175143 + 0.7765550017 0.7847386003 0.7430375218 + 0.7771365047 0.7853134871 0.7436553240 + 0.7777178884 0.7858883739 0.7442730069 + 0.7782993913 0.7864633203 0.7448908091 + 0.7788782716 0.7870357037 0.7455062270 + 0.7794554830 0.7876065969 0.7461202145 + 0.7800326943 0.7881773710 0.7467340827 + 0.7806097865 0.7887482047 0.7473481297 + 0.7811824083 0.7893161178 0.7479594946 + 0.7817530036 0.7898828983 0.7485699058 + 0.7823237181 0.7904496789 0.7491803765 + 0.7828943729 0.7910165191 0.7497909069 + 0.7834607959 0.7915802002 0.7503979802 + 0.7840262055 0.7921432257 0.7510045171 + 0.7845916152 0.7927060723 0.7516109943 + 0.7851570249 0.7932690978 0.7522174716 + 0.7857185006 0.7938284874 0.7528216839 + 0.7862794995 0.7943874002 0.7534257174 + 0.7868404984 0.7949463129 0.7540296912 + 0.7874014974 0.7955052853 0.7546337247 + 0.7879565954 0.7960606217 0.7552338839 + 0.7885110974 0.7966156006 0.7558339238 + 0.7890654802 0.7971706986 0.7564339042 + 0.7896199822 0.7977256775 0.7570338249 + 0.7901695967 0.7982773185 0.7576287985 + 0.7907186747 0.7988284826 0.7582232952 + 0.7912678123 0.7993797064 0.7588177919 + 0.7918167710 0.7999309897 0.7594122887 + 0.7923620939 0.8004786968 0.7600033283 + 0.7929067016 0.8010259271 0.7605937719 + 0.7934514284 0.8015732169 0.7611842752 + 0.7939960957 0.8021203876 0.7617747188 + 0.7945355177 0.8026636839 0.7623630762 + 0.7950735092 0.8032060862 0.7629508972 + 0.7956116199 0.8037484288 0.7635387778 + 0.7961496711 0.8042907715 0.7641267180 + 0.7966831923 0.8048292994 0.7647116780 + 0.7972148061 0.8053662181 0.7652955055 + 0.7977464199 0.8059030771 0.7658793926 + 0.7982779741 0.8064399958 0.7664632797 + 0.7988070846 0.8069747090 0.7670440078 + 0.7993342876 0.8075076938 0.7676222920 + 0.7998613715 0.8080406785 0.7682006955 + 0.8003885746 0.8085737228 0.7687789798 + 0.8009144068 0.8091049194 0.7693557739 + 0.8014380932 0.8096331954 0.7699301243 + 0.8019618988 0.8101614714 0.7705044150 + 0.8024855852 0.8106899261 0.7710787058 + 0.8030084968 0.8112171888 0.7716525197 + 0.8035271764 0.8117402792 0.7722241282 + 0.8040459752 0.8122633100 0.7727957964 + 0.8045647144 0.8127862811 0.7733674049 + 0.8050835133 0.8133093119 0.7739390135 + 0.8055983782 0.8138288856 0.7745084763 + 0.8061131239 0.8143482208 0.7750778198 + 0.8066278100 0.8148674965 0.7756472230 + 0.8071424961 0.8153867722 0.7762165070 + 0.8076552153 0.8159031868 0.7767828703 + 0.8081669211 0.8164179921 0.7773479819 + 0.8086786270 0.8169327974 0.7779129744 + 0.8091902137 0.8174476027 0.7784780264 + 0.8097003102 0.8179603219 0.7790415287 + 0.8102073073 0.8184689879 0.7796025872 + 0.8107144237 0.8189777136 0.7801635861 + 0.8112214804 0.8194864988 0.7807245851 + 0.8117284775 0.8199952245 0.7812855840 + 0.8122318983 0.8205006123 0.7818430066 + 0.8127353191 0.8210061193 0.7824004889 + 0.8132386804 0.8215116262 0.7829579711 + 0.8137421012 0.8220170140 0.7835153937 + 0.8142439127 0.8225206733 0.7840704918 + 0.8147445917 0.8230230808 0.7846242189 + 0.8152452707 0.8235256076 0.7851778269 + 0.8157460093 0.8240280747 0.7857314944 + 0.8162457943 0.8245295286 0.7862842083 + 0.8167423010 0.8250265121 0.7868337035 + 0.8172388077 0.8255233765 0.7873830795 + 0.8177353144 0.8260204196 0.7879325747 + 0.8182318211 0.8265172839 0.7884820104 + 0.8187258244 0.8270121813 0.7890285850 + 0.8192189932 0.8275064230 0.7895743251 + 0.8197121024 0.8280006051 0.7901200056 + 0.8202052712 0.8284947872 0.7906656265 + 0.8206979036 0.8289883137 0.7912104726 + 0.8211886883 0.8294799924 0.7917528152 + 0.8216794729 0.8299716711 0.7922950983 + 0.8221703172 0.8304634094 0.7928373814 + 0.8226609826 0.8309550285 0.7933797240 + 0.8231490850 0.8314424157 0.7939193249 + 0.8236359954 0.8319281936 0.7944579124 + 0.8241229057 0.8324139118 0.7949965000 + 0.8246098161 0.8328995705 0.7955352068 + 0.8250963092 0.8333846927 0.7960731983 + 0.8255802989 0.8338664770 0.7966079116 + 0.8260642290 0.8343483210 0.7971426845 + 0.8265480995 0.8348299861 0.7976773977 + 0.8270320892 0.8353117704 0.7982121110 + 0.8275148869 0.8357924819 0.7987449169 + 0.8279966712 0.8362724185 0.7992761135 + 0.8284785748 0.8367521763 0.7998073101 + 0.8289604187 0.8372321129 0.8003385067 + 0.8294422030 0.8377118707 0.8008695841 + 0.8299208879 0.8381869793 0.8013979197 + 0.8303992152 0.8386614919 0.8019258976 + 0.8308774829 0.8391360044 0.8024538755 + 0.8313558102 0.8396105170 0.8029819131 + 0.8318336010 0.8400840759 0.8035091162 + 0.8323091865 0.8405542970 0.8040336967 + 0.8327847719 0.8410243988 0.8045582771 + 0.8332604170 0.8414946198 0.8050829172 + 0.8337361217 0.8419647813 0.8056074977 + 0.8342108130 0.8424335122 0.8061311245 + 0.8346846104 0.8429005742 0.8066536188 + 0.8351584077 0.8433678150 0.8071761131 + 0.8356322050 0.8438348770 0.8076986074 + 0.8361061215 0.8443019986 0.8082212210 + 0.8365774751 0.8447657824 0.8087412715 + 0.8370481133 0.8452283740 0.8092607260 + 0.8375186920 0.8456910253 0.8097800016 + 0.8379892111 0.8461534977 0.8102992773 + 0.8384597898 0.8466160893 0.8108186722 + 0.8389282227 0.8470754027 0.8113338947 + 0.8393964171 0.8475344181 0.8118488789 + 0.8398646116 0.8479933739 0.8123638034 + 0.8403326869 0.8484523892 0.8128787875 + 0.8408008218 0.8489112258 0.8133934736 + 0.8412675261 0.8493679166 0.8139054179 + 0.8417341113 0.8498246074 0.8144173026 + 0.8422008157 0.8502812982 0.8149293065 + 0.8426674008 0.8507381082 0.8154411912 + 0.8431332707 0.8511940241 0.8159524202 + 0.8435961008 0.8516469002 0.8164612055 + 0.8440588713 0.8520997763 0.8169698715 + 0.8445217013 0.8525527120 0.8174785972 + 0.8449844718 0.8530055881 0.8179873228 + 0.8454462886 0.8534573913 0.8184950948 + 0.8459054232 0.8539065123 0.8190008998 + 0.8463646173 0.8543556929 0.8195067048 + 0.8468238115 0.8548048735 0.8200125098 + 0.8472828865 0.8552539945 0.8205183148 + 0.8477417231 0.8557022214 0.8210232854 + 0.8481997848 0.8561481833 0.8215265274 + 0.8486577868 0.8565942049 0.8220297098 + 0.8491159081 0.8570402265 0.8225328922 + 0.8495739102 0.8574863076 0.8230360746 + 0.8500308990 0.8579313755 0.8235384226 + 0.8504856229 0.8583744168 0.8240389228 + 0.8509402275 0.8588173985 0.8245393038 + 0.8513947725 0.8592604995 0.8250396848 + 0.8518494964 0.8597034812 0.8255401850 + 0.8523032069 0.8601452708 0.8260402083 + 0.8527547121 0.8605837226 0.8265390992 + 0.8532062173 0.8610221148 0.8270381093 + 0.8536576033 0.8614605069 0.8275370002 + 0.8541091084 0.8618988991 0.8280360103 + 0.8545603752 0.8623365164 0.8285344243 + 0.8550112247 0.8627706766 0.8290308118 + 0.8554618955 0.8632047772 0.8295271993 + 0.8559126854 0.8636388779 0.8300235271 + 0.8563634157 0.8640729785 0.8305199146 + 0.8568140268 0.8645070195 0.8310160041 + 0.8572617769 0.8649371862 0.8315085769 + 0.8577095270 0.8653674722 0.8320013285 + 0.8581572771 0.8657976985 0.8324939013 + 0.8586050272 0.8662279844 0.8329865932 + 0.8590527773 0.8666582108 0.8334792256 + 0.8594974279 0.8670850992 0.8339697123 + 0.8599414825 0.8675115705 0.8344600201 + 0.8603855968 0.8679379821 0.8349502087 + 0.8608297110 0.8683645129 0.8354403973 + 0.8612738848 0.8687909245 0.8359307051 + 0.8617168069 0.8692150712 0.8364191055 + 0.8621590734 0.8696380854 0.8369066715 + 0.8626012802 0.8700612187 0.8373944163 + 0.8630436063 0.8704841733 0.8378819823 + 0.8634858727 0.8709071875 0.8383696079 + 0.8639271855 0.8713290095 0.8388562799 + 0.8643670082 0.8717489839 0.8393415809 + 0.8648067713 0.8721688986 0.8398268819 + 0.8652465940 0.8725888729 0.8403123021 + 0.8656864166 0.8730089068 0.8407976031 + 0.8661258817 0.8734285831 0.8412827253 + 0.8665626049 0.8738455176 0.8417659998 + 0.8669993281 0.8742625117 0.8422493935 + 0.8674361110 0.8746795058 0.8427327275 + 0.8678727746 0.8750964999 0.8432160020 + 0.8683094978 0.8755133748 0.8436992764 + 0.8687452078 0.8759278059 0.8441810012 + 0.8691806197 0.8763412237 0.8446620703 + 0.8696159720 0.8767547011 0.8451433182 + 0.8700513840 0.8771681786 0.8456243873 + 0.8704867959 0.8775817156 0.8461055160 + 0.8709213734 0.8779937029 0.8465862870 + 0.8713548183 0.8784034848 0.8470665216 + 0.8717882037 0.8788132071 0.8475466967 + 0.8722217083 0.8792229295 0.8480268717 + 0.8726550937 0.8796325922 0.8485071063 + 0.8730884790 0.8800423741 0.8489872217 + 0.8735179901 0.8804501295 0.8494654894 + 0.8739473224 0.8808577061 0.8499435782 + 0.8743764758 0.8812652826 0.8504217267 + 0.8748058081 0.8816729188 0.8508998752 + 0.8752350211 0.8820804954 0.8513780236 + 0.8756635189 0.8824869990 0.8518546820 + 0.8760908842 0.8828920126 0.8523296714 + 0.8765184283 0.8832970262 0.8528047204 + 0.8769457936 0.8837019801 0.8532798290 + 0.8773732781 0.8841069937 0.8537548184 + 0.8778008223 0.8845118880 0.8542299271 + 0.8782266974 0.8849135041 0.8547031283 + 0.8786525726 0.8853148222 0.8551763296 + 0.8790785074 0.8857160211 0.8556494713 + 0.8795043230 0.8861172795 0.8561226130 + 0.8799301982 0.8865185976 0.8565958142 + 0.8803547025 0.8869189024 0.8570681810 + 0.8807768822 0.8873174787 0.8575392962 + 0.8811990023 0.8877161145 0.8580104113 + 0.8816211820 0.8881146908 0.8584814072 + 0.8820433021 0.8885133266 0.8589525223 + 0.8824654818 0.8889119029 0.8594235778 + 0.8828865886 0.8893092871 0.8598933816 + 0.8833072782 0.8897061944 0.8603627086 + 0.8837280869 0.8901032209 0.8608320951 + 0.8841487765 0.8905001879 0.8613014221 + 0.8845695257 0.8908970952 0.8617706895 + 0.8849902153 0.8912938237 0.8622398973 + 0.8854097128 0.8916872740 0.8627077937 + 0.8858292103 0.8920807242 0.8631755710 + 0.8862488270 0.8924741149 0.8636435270 + 0.8866683245 0.8928675056 0.8641113043 + 0.8870878220 0.8932608962 0.8645792007 + 0.8875058293 0.8936526775 0.8650463820 + 0.8879212141 0.8940414786 0.8655126095 + 0.8883365989 0.8944302797 0.8659787178 + 0.8887521029 0.8948190808 0.8664448857 + 0.8891674876 0.8952078223 0.8669111133 + 0.8895828724 0.8955966234 0.8673772812 + 0.8899958134 0.8959844708 0.8678423166 + 0.8904073238 0.8963717818 0.8683065176 + 0.8908187151 0.8967590928 0.8687707782 + 0.8912302256 0.8971464038 0.8692349792 + 0.8916416168 0.8975337148 0.8696991801 + 0.8920531273 0.8979210258 0.8701635003 + 0.8924632072 0.8983063102 0.8706263900 + 0.8928729892 0.8986909986 0.8710889816 + 0.8932827115 0.8990756869 0.8715515733 + 0.8936924934 0.8994604945 0.8720142245 + 0.8941022754 0.8998451829 0.8724768758 + 0.8945121169 0.9002298713 0.8729395270 + 0.8949183226 0.9006112218 0.8734009266 + 0.8953242898 0.9009922743 0.8738622069 + 0.8957303762 0.9013733268 0.8743236065 + 0.8961364031 0.9017543793 0.8747850060 + 0.8965424895 0.9021354914 0.8752462864 + 0.8969483972 0.9025166035 0.8757076263 + 0.8973509073 0.9028971195 0.8761674166 + 0.8977534175 0.9032775760 0.8766270876 + 0.8981559277 0.9036580920 0.8770868778 + 0.8985583186 0.9040386081 0.8775466084 + 0.8989608288 0.9044191837 0.8780063987 + 0.8993632197 0.9047995210 0.8784658909 + 0.8997644186 0.9051784277 0.8789238930 + 0.9001656175 0.9055572748 0.8793817759 + 0.9005668163 0.9059361219 0.8798397183 + 0.9009678960 0.9063150287 0.8802977204 + 0.9013690948 0.9066938758 0.8807556033 + 0.9017699957 0.9070724249 0.8812134266 + 0.9021677971 0.9074478149 0.8816698194 + 0.9025657177 0.9078232050 0.8821262717 + 0.9029635191 0.9081985950 0.8825827241 + 0.9033613801 0.9085739851 0.8830391765 + 0.9037591815 0.9089493155 0.8834956288 + 0.9041569233 0.9093245864 0.8839520812 + 0.9045507908 0.9096974730 0.8844081163 + 0.9049447179 0.9100704193 0.8848640919 + 0.9053385854 0.9104431868 0.8853201270 + 0.9057325125 0.9108160734 0.8857762218 + 0.9061263800 0.9111890197 0.8862321973 + 0.9065201879 0.9115619063 0.8866881728 + 0.9069120288 0.9119333029 0.8871427774 + 0.9073035121 0.9123045802 0.8875973225 + 0.9076951146 0.9126759768 0.8880518079 + 0.9080867171 0.9130473137 0.8885064125 + 0.9084782004 0.9134187102 0.8889608979 + 0.9088698030 0.9137899876 0.8894153833 + 0.9092592001 0.9141587019 0.8898677826 + 0.9096478820 0.9145267010 0.8903195858 + 0.9100366235 0.9148947001 0.8907713890 + 0.9104254246 0.9152626991 0.8912233114 + 0.9108141065 0.9156306982 0.8916751146 + 0.9112027884 0.9159986973 0.8921269178 + 0.9115895033 0.9163650274 0.8925778270 + 0.9119747877 0.9167301059 0.8930280209 + 0.9123600721 0.9170951843 0.8934782743 + 0.9127454162 0.9174603224 0.8939285278 + 0.9131307006 0.9178254008 0.8943787217 + 0.9135159850 0.9181904793 0.8948289752 + 0.9139006734 0.9185553789 0.8952789903 + 0.9142842293 0.9189199805 0.8957288265 + 0.9146677256 0.9192845225 0.8961784840 + 0.9150511026 0.9196491241 0.8966283202 + 0.9154345989 0.9200137258 0.8970779777 + 0.9158180952 0.9203782082 0.8975276947 + 0.9162014723 0.9207428098 0.8979774117 + 0.9165825844 0.9211050272 0.8984262943 + 0.9169636965 0.9214671850 0.8988751769 + 0.9173448086 0.9218295217 0.8993239999 + 0.9177259207 0.9221916795 0.8997728825 + 0.9181070924 0.9225540161 0.9002218246 + 0.9184882045 0.9229161739 0.9006705880 + 0.9188672900 0.9232757092 0.9011185765 + 0.9192454815 0.9236338735 0.9015660286 + 0.9196236134 0.9239919782 0.9020134807 + 0.9200018048 0.9243502021 0.9024609923 + 0.9203799963 0.9247083068 0.9029083848 + 0.9207581282 0.9250664711 0.9033558965 + 0.9211360216 0.9254242182 0.9038028717 + 0.9215127826 0.9257805943 0.9042485952 + 0.9218896031 0.9261369705 0.9046943188 + 0.9222664237 0.9264934063 0.9051399827 + 0.9226431847 0.9268497825 0.9055857062 + 0.9230201244 0.9272062182 0.9060314298 + 0.9233968854 0.9275625944 0.9064772129 + 0.9237722158 0.9279180169 0.9069222808 + 0.9241471291 0.9282730818 0.9073672891 + 0.9245219827 0.9286280870 0.9078122973 + 0.9248968959 0.9289832115 0.9082573056 + 0.9252718091 0.9293382764 0.9087021947 + 0.9256467223 0.9296934009 0.9091472030 + 0.9260206819 0.9300473928 0.9095919132 + 0.9263923764 0.9303987026 0.9100357890 + 0.9267640710 0.9307498932 0.9104797244 + 0.9271358252 0.9311012030 0.9109236002 + 0.9275075197 0.9314523935 0.9113674760 + 0.9278790951 0.9318037033 0.9118114114 + 0.9282507896 0.9321550131 0.9122552872 + 0.9286209941 0.9325038195 0.9126986265 + 0.9289904833 0.9328516126 0.9131416082 + 0.9293599725 0.9331995249 0.9135845900 + 0.9297295213 0.9335473180 0.9140275717 + 0.9300991297 0.9338951111 0.9144706726 + 0.9304686189 0.9342429042 0.9149137139 + 0.9308379889 0.9345906973 0.9153565764 + 0.9312061071 0.9349374175 0.9157990217 + 0.9315742254 0.9352840781 0.9162414074 + 0.9319424033 0.9356307983 0.9166836739 + 0.9323105216 0.9359775186 0.9171261191 + 0.9326786995 0.9363241792 0.9175685048 + 0.9330468178 0.9366708994 0.9180107713 + 0.9334139228 0.9370164871 0.9184523225 + 0.9337794185 0.9373604059 0.9188922048 + 0.9341449142 0.9377043247 0.9193322062 + 0.9345104098 0.9380481243 0.9197722077 + 0.9348759055 0.9383919835 0.9202122092 + 0.9352414012 0.9387359023 0.9206520915 + 0.9356068969 0.9390798211 0.9210920930 + 0.9359713793 0.9394215941 0.9215306044 + 0.9363352060 0.9397622943 0.9219684005 + 0.9366990924 0.9401029944 0.9224061966 + 0.9370629787 0.9404436946 0.9228439927 + 0.9374268055 0.9407843947 0.9232817888 + 0.9377906919 0.9411249757 0.9237195849 + 0.9381545782 0.9414656758 0.9241573811 + 0.9385176897 0.9418051839 0.9245941043 + 0.9388806820 0.9421443939 0.9250305891 + 0.9392436743 0.9424837232 0.9254671931 + 0.9396067262 0.9428228736 0.9259036779 + 0.9399697781 0.9431622028 0.9263402224 + 0.9403327703 0.9435014129 0.9267767072 + 0.9406958222 0.9438406825 0.9272131920 + 0.9410552979 0.9441781044 0.9276493788 + 0.9414148927 0.9445155859 0.9280856252 + 0.9417744875 0.9448531270 0.9285218120 + 0.9421340823 0.9451906085 0.9289579988 + 0.9424936175 0.9455280900 0.9293941259 + 0.9428532124 0.9458655715 0.9298303127 + 0.9432125092 0.9462026954 0.9302664995 + 0.9435694218 0.9465367198 0.9307025075 + 0.9439262748 0.9468708038 0.9311385751 + 0.9442831874 0.9472048283 0.9315745831 + 0.9446401000 0.9475389123 0.9320107102 + 0.9449970126 0.9478729963 0.9324467182 + 0.9453539848 0.9482070208 0.9328827858 + 0.9457105994 0.9485409260 0.9333183765 + 0.9460659027 0.9488738179 0.9337515235 + 0.9464210868 0.9492065907 0.9341846108 + 0.9467762709 0.9495394826 0.9346178174 + 0.9471315145 0.9498723149 0.9350509048 + 0.9474868178 0.9502052069 0.9354839921 + 0.9478420019 0.9505379796 0.9359171987 + 0.9481968880 0.9508708119 0.9363495708 + 0.9485492110 0.9512028098 0.9367771745 + 0.9489015937 0.9515348077 0.9372048974 + 0.9492539167 0.9518669248 0.9376325011 + 0.9496062994 0.9521989226 0.9380601048 + 0.9499586225 0.9525309205 0.9384877086 + 0.9503110051 0.9528629184 0.9389153719 + 0.9506632090 0.9531947970 0.9393426776 + 0.9510114789 0.9535222054 0.9397646785 + 0.9513596892 0.9538496733 0.9401866794 + 0.9517080188 0.9541772008 0.9406086802 + 0.9520562291 0.9545047283 0.9410306811 + 0.9524044991 0.9548320770 0.9414526224 + 0.9527528286 0.9551596045 0.9418746233 + 0.9531009793 0.9554870725 0.9422966242 + 0.9534440041 0.9558085203 0.9427149892 + 0.9537863135 0.9561291933 0.9431329966 + 0.9541286230 0.9564499259 0.9435510039 + 0.9544708729 0.9567705989 0.9439690113 + 0.9548131824 0.9570913911 0.9443870187 + 0.9551554918 0.9574121237 0.9448050261 + 0.9554979205 0.9577327967 0.9452229738 + 0.9558368921 0.9580510259 0.9456391931 + 0.9561746120 0.9583681226 0.9460545182 + 0.9565122724 0.9586851001 0.9464697838 + 0.9568499923 0.9590021968 0.9468852282 + 0.9571877122 0.9593192935 0.9473004937 + 0.9575253725 0.9596363902 0.9477158785 + 0.9578630924 0.9599534869 0.9481312037 + 0.9581995010 0.9602695107 0.9485455155 + 0.9585341215 0.9605841041 0.9489583969 + 0.9588688016 0.9608986974 0.9493712783 + 0.9592034817 0.9612134099 0.9497842193 + 0.9595381021 0.9615280032 0.9501971006 + 0.9598727822 0.9618425965 0.9506099224 + 0.9602074027 0.9621571898 0.9510228038 + 0.9605417848 0.9624714851 0.9514353871 + 0.9608737826 0.9627835751 0.9518461227 + 0.9612057805 0.9630956054 0.9522566795 + 0.9615378976 0.9634075761 0.9526674151 + 0.9618698955 0.9637196064 0.9530779719 + 0.9622018933 0.9640315771 0.9534885883 + 0.9625340104 0.9643437266 0.9538993239 + 0.9628660083 0.9646556973 0.9543098807 + 0.9631955028 0.9649661779 0.9547188878 + 0.9635242820 0.9652761221 0.9551271796 + 0.9638530016 0.9655861259 0.9555355906 + 0.9641817212 0.9658960104 0.9559440017 + 0.9645103812 0.9662060142 0.9563524127 + 0.9648391008 0.9665158987 0.9567608237 + 0.9651678205 0.9668259025 0.9571691751 + 0.9654955268 0.9671350121 0.9575765729 + 0.9658207297 0.9674422145 0.9579817057 + 0.9661458731 0.9677494168 0.9583867788 + 0.9664710760 0.9680566788 0.9587919116 + 0.9667962790 0.9683638811 0.9591971040 + 0.9671214819 0.9686710835 0.9596021771 + 0.9674468040 0.9689784050 0.9600073099 + 0.9677720070 0.9692856073 0.9604123831 + 0.9680947065 0.9695898294 0.9608144760 + 0.9684168100 0.9698932767 0.9612157941 + 0.9687389135 0.9701967239 0.9616171718 + 0.9690610170 0.9705001712 0.9620184898 + 0.9693831205 0.9708036780 0.9624199271 + 0.9697052836 0.9711071253 0.9628211856 + 0.9700273871 0.9714105725 0.9632225037 + 0.9703485966 0.9717134237 0.9636229277 + 0.9706671238 0.9720138907 0.9640201926 + 0.9709854722 0.9723144770 0.9644175172 + 0.9713038802 0.9726150036 0.9648149014 + 0.9716224074 0.9729155898 0.9652122259 + 0.9719408154 0.9732161760 0.9656094909 + 0.9722592831 0.9735167027 0.9660068154 + 0.9725776911 0.9738172889 0.9664040804 + 0.9728938937 0.9741159081 0.9667984843 + 0.9732084870 0.9744132757 0.9671909213 + 0.9735231996 0.9747105837 0.9675834179 + 0.9738377929 0.9750080109 0.9679757953 + 0.9741525054 0.9753053784 0.9683682919 + 0.9744670987 0.9756028056 0.9687607884 + 0.9747818112 0.9759001136 0.9691532254 + 0.9750964046 0.9761974812 0.9695457220 + 0.9754077196 0.9764910936 0.9699333906 + 0.9757186174 0.9767844081 0.9703207016 + 0.9760295749 0.9770777225 0.9707080126 + 0.9763404727 0.9773709774 0.9710952044 + 0.9766514897 0.9776641726 0.9714825153 + 0.9769623876 0.9779574871 0.9718698263 + 0.9772734046 0.9782508016 0.9722570777 + 0.9775835872 0.9785434008 0.9726434946 + 0.9778903127 0.9788324833 0.9730253816 + 0.9781969786 0.9791216254 0.9734073877 + 0.9785035849 0.9794107080 0.9737892747 + 0.9788103104 0.9796997905 0.9741712213 + 0.9791169763 0.9799888730 0.9745532274 + 0.9794237018 0.9802780151 0.9749351144 + 0.9797304273 0.9805672169 0.9753171206 + 0.9800354242 0.9808546901 0.9756969213 + 0.9803376198 0.9811394811 0.9760727882 + 0.9806396961 0.9814242125 0.9764487147 + 0.9809417725 0.9817090034 0.9768247008 + 0.9812439084 0.9819936752 0.9772006273 + 0.9815461040 0.9822785258 0.9775764942 + 0.9818481803 0.9825631976 0.9779524803 + 0.9821503162 0.9828479886 0.9783284068 + 0.9824500084 0.9831303954 0.9787008166 + 0.9827467799 0.9834101796 0.9790692925 + 0.9830436707 0.9836900234 0.9794378281 + 0.9833405018 0.9839698076 0.9798063040 + 0.9836373925 0.9842494726 0.9801747799 + 0.9839342237 0.9845293164 0.9805433154 + 0.9842311144 0.9848091006 0.9809117913 + 0.9845278859 0.9850888848 0.9812803268 + 0.9848216176 0.9853662848 0.9816443920 + 0.9851121902 0.9856415987 0.9820045233 + 0.9854028225 0.9859169126 0.9823644757 + 0.9856935143 0.9861922264 0.9827246070 + 0.9859840870 0.9864674807 0.9830846786 + 0.9862747192 0.9867427945 0.9834446907 + 0.9865652919 0.9870181084 0.9838048220 + 0.9868559837 0.9872933030 0.9841647744 + 0.9871436954 0.9875661731 0.9845203161 + 0.9874286056 0.9878365993 0.9848713875 + 0.9877135158 0.9881070256 0.9852225184 + 0.9879983068 0.9883773923 0.9855734706 + 0.9882832170 0.9886478186 0.9859246016 + 0.9885681272 0.9889181852 0.9862756729 + 0.9888529778 0.9891886115 0.9866266847 + 0.9891378880 0.9894589782 0.9869778156 + 0.9894198179 0.9897269011 0.9873241782 + 0.9896976948 0.9899914265 0.9876642823 + 0.9899755120 0.9902557731 0.9880045056 + 0.9902533889 0.9905201793 0.9883446097 + 0.9905313253 0.9907847047 0.9886847138 + 0.9908092022 0.9910491109 0.9890248179 + 0.9910870194 0.9913135767 0.9893649817 + 0.9913648963 0.9915779829 0.9897050858 + 0.9916403294 0.9918411970 0.9900411963 + 0.9919093251 0.9921011925 0.9903668165 + 0.9921782017 0.9923611879 0.9906923175 + 0.9924470782 0.9926211834 0.9910178781 + 0.9927160144 0.9928811789 0.9913433790 + 0.9929850101 0.9931411743 0.9916689992 + 0.9932538867 0.9934012294 0.9919946194 + 0.9935228229 0.9936612248 0.9923201203 + 0.9937912822 0.9939208031 0.9926447868 + 0.9940528274 0.9941738844 0.9929555058 + 0.9943143129 0.9944270253 0.9932662249 + 0.9945756793 0.9946801066 0.9935768843 + 0.9948372245 0.9949333072 0.9938876033 + 0.9950987101 0.9951863885 0.9941983223 + 0.9953601956 0.9954395294 0.9945089817 + 0.9956216216 0.9956926107 0.9948197007 + 0.9958831072 0.9959456921 0.9951304793 + 0.9961357713 0.9961907864 0.9954292774 + 0.9963858724 0.9964336157 0.9957246780 + 0.9966359735 0.9966763258 0.9960200787 + 0.9968860745 0.9969190955 0.9963154793 + 0.9971361756 0.9971618056 0.9966108799 + 0.9973862767 0.9974045753 0.9969062805 + 0.9976363778 0.9976472855 0.9972016811 + 0.9978864789 0.9978901148 0.9974970818 + 0.9981297851 0.9981291294 0.9977846742 + 0.9983634949 0.9983630180 0.9980615973 + 0.9985973239 0.9985967875 0.9983385205 + 0.9988310933 0.9988306761 0.9986153841 + 0.9990649223 0.9990645051 0.9988924265 + 0.9992986917 0.9992983937 0.9991692901 + 0.9995324016 0.9995322824 0.9994462132 + 0.9997661710 0.9997661114 0.9997230768 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_hdc_100_plus.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_hdc_100_plus.spi1d index 41dbb8cc..c3afe1b5 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_hdc_100_plus.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_hdc_100_plus.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0071816309 0.0082409112 0.0071816309 - 0.0151500301 0.0168459192 0.0151500301 - 0.0224017091 0.0247517005 0.0224017091 - 0.0290685501 0.0319880284 0.0290685501 - 0.0352958813 0.0386751406 0.0352958813 - 0.0411669388 0.0448977016 0.0411669388 - 0.0467281416 0.0507521592 0.0467281416 - 0.0520455502 0.0562780313 0.0520455502 - 0.0571411215 0.0615147501 0.0571411215 - 0.0620320588 0.0665091425 0.0620320588 - 0.0667356402 0.0713089630 0.0667356402 - 0.0712965578 0.0759803429 0.0712965578 - 0.0757385418 0.0805422887 0.0757385418 - 0.0800764337 0.0850213692 0.0800764337 - 0.0843147188 0.0894245878 0.0843147188 - 0.0884548426 0.0937559828 0.0884548426 - 0.0925132483 0.0980221704 0.0925132483 - 0.0964898691 0.1022289991 0.0964898691 - 0.1003945023 0.1063620001 0.1003945023 - 0.1042160019 0.1104390025 0.1042160019 - 0.1079747006 0.1144633964 0.1079747006 - 0.1116577014 0.1184230000 0.1116577014 - 0.1152791977 0.1223274022 0.1152791977 - 0.1188424006 0.1261754930 0.1188424006 - 0.1223476008 0.1299650073 0.1223476008 - 0.1257933974 0.1337056011 0.1257933974 - 0.1291857064 0.1373963058 0.1291857064 - 0.1325111985 0.1410335004 0.1325111985 - 0.1357868016 0.1446186006 0.1357868016 - 0.1390141994 0.1481585056 0.1390141994 - 0.1421941966 0.1516450942 0.1421941966 - 0.1453281939 0.1550758928 0.1453281939 - 0.1484290957 0.1584609002 0.1484290957 - 0.1514863074 0.1617927998 0.1514863074 - 0.1545145065 0.1650730968 0.1545145065 - 0.1575087011 0.1683045030 0.1575087011 - 0.1604654938 0.1714911014 0.1604654938 - 0.1633878052 0.1746436059 0.1633878052 - 0.1662711948 0.1777534932 0.1662711948 - 0.1691236049 0.1808203012 0.1691236049 - 0.1719461977 0.1838431954 0.1719461977 - 0.1747328043 0.1868294030 0.1747328043 - 0.1774906069 0.1897847056 0.1774906069 - 0.1802141964 0.1927071065 0.1802141964 - 0.1829040051 0.1955924034 0.1829040051 - 0.1855681986 0.1984508038 0.1855681986 - 0.1881995946 0.2012735009 0.1881995946 - 0.1908040047 0.2040608972 0.1908040047 - 0.1933835000 0.2068151981 0.1933835000 - 0.1959407926 0.2095429003 0.1959407926 - 0.1984723061 0.2122360021 0.1984723061 - 0.2009814978 0.2149036974 0.2009814978 - 0.2034648061 0.2175482064 0.2034648061 - 0.2059251070 0.2201592028 0.2059251070 - 0.2083660066 0.2227438986 0.2083660066 - 0.2107862979 0.2253006995 0.2107862979 - 0.2131928056 0.2278293073 0.2131928056 - 0.2155797929 0.2303356975 0.2155797929 - 0.2179521024 0.2328161001 0.2179521024 - 0.2203088999 0.2352697998 0.2203088999 - 0.2226427943 0.2377053052 0.2226427943 - 0.2249636948 0.2401220948 0.2249636948 - 0.2272658050 0.2425125986 0.2272658050 - 0.2295507044 0.2448851019 0.2295507044 - 0.2318232954 0.2472407967 0.2318232954 - 0.2340773046 0.2495730072 0.2340773046 - 0.2363186032 0.2518830895 0.2363186032 - 0.2385478020 0.2541756034 0.2385478020 - 0.2407571971 0.2564486861 0.2407571971 - 0.2429528981 0.2587057948 0.2429528981 - 0.2451367974 0.2609438896 0.2451367974 - 0.2473030984 0.2631632090 0.2473030984 - 0.2494540960 0.2653681040 0.2494540960 - 0.2515927851 0.2675603926 0.2515927851 - 0.2537161112 0.2697342932 0.2537161112 - 0.2558267117 0.2718896866 0.2558267117 - 0.2579267025 0.2740308046 0.2579267025 - 0.2600139081 0.2761551142 0.2600139081 - 0.2620905042 0.2782635987 0.2620905042 - 0.2641522884 0.2803570926 0.2641522884 - 0.2661997080 0.2824395895 0.2661997080 - 0.2682358921 0.2845089138 0.2682358921 - 0.2702636123 0.2865653038 0.2702636123 - 0.2722778916 0.2886031866 0.2722778916 - 0.2742806971 0.2906292975 0.2742806971 - 0.2762736082 0.2926450074 0.2762736082 - 0.2782565951 0.2946475148 0.2782565951 - 0.2802278996 0.2966375053 0.2802278996 - 0.2821829021 0.2986125946 0.2821829021 - 0.2841286063 0.3005768061 0.2841286063 - 0.2860676050 0.3025309145 0.2860676050 - 0.2879948914 0.3044739962 0.2879948914 - 0.2899109125 0.3064059913 0.2899109125 - 0.2918199003 0.3083246052 0.2918199003 - 0.2937203050 0.3102298975 0.2937203050 - 0.2956117988 0.3121252954 0.2956117988 - 0.2974927127 0.3140129149 0.2974927127 - 0.2993623018 0.3158906996 0.2993623018 - 0.3012244105 0.3177559078 0.3012244105 - 0.3030805886 0.3196083009 0.3030805886 - 0.3049291968 0.3214524984 0.3049291968 - 0.3067674041 0.3232868016 0.3067674041 - 0.3085950911 0.3251119852 0.3085950911 - 0.3104135990 0.3269273937 0.3104135990 - 0.3122237027 0.3287312090 0.3122237027 - 0.3140270114 0.3305267096 0.3140270114 - 0.3158223927 0.3323149085 0.3158223927 - 0.3176110089 0.3340944946 0.3176110089 - 0.3193928003 0.3358643055 0.3193928003 - 0.3211658001 0.3376250863 0.3211658001 - 0.3229303062 0.3393788934 0.3229303062 - 0.3246853054 0.3411253095 0.3246853054 - 0.3264288902 0.3428628147 0.3264288902 - 0.3281643093 0.3445931077 0.3281643093 - 0.3298931122 0.3463181853 0.3298931122 - 0.3316138089 0.3480368853 0.3316138089 - 0.3333249986 0.3497483134 0.3333249986 - 0.3350282013 0.3514511883 0.3350282013 - 0.3367244899 0.3531453907 0.3367244899 - 0.3384152055 0.3548327088 0.3384152055 - 0.3401009142 0.3565143049 0.3401009142 - 0.3417800069 0.3581889868 0.3417800069 - 0.3434512019 0.3598566949 0.3434512019 - 0.3451134861 0.3615188897 0.3451134861 - 0.3467679918 0.3631770015 0.3467679918 - 0.3484159112 0.3648299873 0.3484159112 - 0.3500581980 0.3664768934 0.3500581980 - 0.3516958952 0.3681172132 0.3516958952 - 0.3533272147 0.3697521985 0.3533272147 - 0.3549514115 0.3713811040 0.3549514115 - 0.3565692902 0.3730033040 0.3565692902 - 0.3581799865 0.3746202886 0.3581799865 - 0.3597826958 0.3762314916 0.3597826958 - 0.3613792062 0.3778361976 0.3613792062 - 0.3629701138 0.3794350028 0.3629701138 - 0.3645544946 0.3810291886 0.3645544946 - 0.3661319017 0.3826195002 0.3661319017 - 0.3677032888 0.3842051923 0.3677032888 - 0.3692691922 0.3857854009 0.3692691922 - 0.3708305955 0.3873588145 0.3708305955 - 0.3723872006 0.3889274001 0.3723872006 - 0.3739395142 0.3904910982 0.3739395142 - 0.3754872978 0.3920491040 0.3754872978 - 0.3770299852 0.3936016858 0.3770299852 - 0.3785668015 0.3951497972 0.3785668015 - 0.3800952137 0.3966915905 0.3800952137 - 0.3816176057 0.3982270956 0.3816176057 - 0.3831363916 0.3997577131 0.3831363916 - 0.3846518993 0.4012834132 0.3846518993 - 0.3861635923 0.4028038085 0.3861635923 - 0.3876700103 0.4043180048 0.3876700103 - 0.3891699910 0.4058266878 0.3891699910 - 0.3906654119 0.4073303044 0.3906654119 - 0.3921560049 0.4088295996 0.3921560049 - 0.3936414123 0.4103249907 0.3936414123 - 0.3951213062 0.4118149877 0.3951213062 - 0.3965955079 0.4132992923 0.3965955079 - 0.3980641961 0.4147784114 0.3980641961 - 0.3995279074 0.4162524939 0.3995279074 - 0.4009875953 0.4177218080 0.4009875953 - 0.4024423063 0.4191871881 0.4024423063 - 0.4038924873 0.4206492007 0.4038924873 - 0.4053378999 0.4221076965 0.4053378999 - 0.4067766070 0.4235613942 0.4067766070 - 0.4082114995 0.4250093997 0.4082114995 - 0.4096431136 0.4264526963 0.4096431136 - 0.4110710919 0.4278913140 0.4110710919 - 0.4124928117 0.4293223023 0.4124928117 - 0.4139108062 0.4307503104 0.4139108062 - 0.4153253138 0.4321753085 0.4153253138 - 0.4167349935 0.4335967898 0.4167349935 - 0.4181382954 0.4350129068 0.4181382954 - 0.4195368886 0.4364252090 0.4195368886 - 0.4209308028 0.4378337860 0.4209308028 - 0.4223197103 0.4392373860 0.4223197103 - 0.4237053990 0.4406375885 0.4237053990 - 0.4250878096 0.4420346022 0.4250878096 - 0.4264639914 0.4434260130 0.4264639914 - 0.4278368056 0.4448128045 0.4278368056 - 0.4292069972 0.4461959004 0.4292069972 - 0.4305736125 0.4475724995 0.4305736125 - 0.4319368899 0.4489449859 0.4319368899 - 0.4332974851 0.4503144026 0.4332974851 - 0.4346528947 0.4516780972 0.4346528947 - 0.4360040128 0.4530383050 0.4360040128 - 0.4373517036 0.4543960094 0.4373517036 - 0.4386920035 0.4557482898 0.4386920035 - 0.4400289059 0.4570981860 0.4400289059 - 0.4413625896 0.4584460855 0.4413625896 - 0.4426892102 0.4597887993 0.4426892102 - 0.4440130889 0.4611299038 0.4440130889 - 0.4453332126 0.4624690115 0.4453332126 - 0.4466494024 0.4638045132 0.4466494024 - 0.4479633868 0.4651381075 0.4479633868 - 0.4492729008 0.4664680064 0.4492729008 - 0.4505785108 0.4677945077 0.4505785108 - 0.4518812895 0.4691188931 0.4518812895 - 0.4531790912 0.4704371989 0.4531790912 - 0.4544754028 0.4717530906 0.4544754028 - 0.4557689130 0.4730646908 0.4557689130 - 0.4570592046 0.4743711948 0.4570592046 - 0.4583478868 0.4756751955 0.4583478868 - 0.4596310854 0.4769738019 0.4596310854 - 0.4609121978 0.4782701135 0.4609121978 - 0.4621888995 0.4795624018 0.4621888995 - 0.4634611011 0.4808515906 0.4634611011 - 0.4647307098 0.4821392894 0.4647307098 - 0.4659950137 0.4834218919 0.4659950137 - 0.4672574103 0.4847030044 0.4672574103 - 0.4685145915 0.4859794974 0.4685145915 - 0.4697686136 0.4872528017 0.4697686136 - 0.4710184932 0.4885227978 0.4710184932 - 0.4722628891 0.4897891879 0.4722628891 - 0.4735049009 0.4910542071 0.4735049009 - 0.4747408926 0.4923141003 0.4747408926 - 0.4759756923 0.4935730100 0.4759756923 - 0.4772061110 0.4948287904 0.4772061110 - 0.4784345031 0.4960829020 0.4784345031 - 0.4796591997 0.4973343015 0.4796591997 - 0.4808802903 0.4985828102 0.4808802903 - 0.4820986092 0.4998292029 0.4820986092 - 0.4833118021 0.5010713935 0.4833118021 - 0.4845229983 0.5023121834 0.4845229983 - 0.4857310057 0.5035473704 0.4857310057 - 0.4869383872 0.5047817826 0.4869383872 - 0.4881415069 0.5060117245 0.4881415069 - 0.4893437922 0.5072407722 0.4893437922 - 0.4905417860 0.5084657073 0.4905417860 - 0.4917384088 0.5096889138 0.4917384088 - 0.4929305911 0.5109081864 0.4929305911 - 0.4941214025 0.5121250749 0.4941214025 - 0.4953092933 0.5133383274 0.4953092933 - 0.4964950979 0.5145484209 0.4964950979 - 0.4976781011 0.5157549977 0.4976781011 - 0.4988583922 0.5169578195 0.4988583922 - 0.5000360012 0.5181573033 0.5000360012 - 0.5012102127 0.5193536878 0.5012102127 - 0.5023818016 0.5205478072 0.5023818016 - 0.5035495758 0.5217382908 0.5035495758 - 0.5047146082 0.5229266286 0.5047146082 - 0.5058754086 0.5241109133 0.5058754086 - 0.5070334077 0.5252929926 0.5070334077 - 0.5081868768 0.5264708996 0.5081868768 - 0.5093373060 0.5276464224 0.5093373060 - 0.5104840994 0.5288175941 0.5104840994 - 0.5116283298 0.5299862027 0.5116283298 - 0.5127689242 0.5311505795 0.5127689242 - 0.5139064193 0.5323119164 0.5139064193 - 0.5150403976 0.5334690809 0.5150403976 - 0.5161709189 0.5346227884 0.5161709189 - 0.5172979832 0.5357729197 0.5172979832 - 0.5184208751 0.5369185805 0.5184208751 - 0.5195422769 0.5380612016 0.5195422769 - 0.5206609964 0.5391985774 0.5206609964 - 0.5217785239 0.5403342843 0.5217785239 - 0.5228924751 0.5414649844 0.5228924751 - 0.5240058899 0.5425949097 0.5240058899 - 0.5251148939 0.5437206030 0.5251148939 - 0.5262238979 0.5448461771 0.5262238979 - 0.5273274183 0.5459663868 0.5273274183 - 0.5284308791 0.5470865965 0.5284308791 - 0.5295295119 0.5482017994 0.5295295119 - 0.5306270719 0.5493162274 0.5306270719 - 0.5317205191 0.5504277945 0.5317205191 - 0.5328117013 0.5515381098 0.5328117013 - 0.5339006782 0.5526462197 0.5339006782 - 0.5349869728 0.5537520051 0.5349869728 - 0.5360723734 0.5548564792 0.5360723734 - 0.5371550918 0.5559577942 0.5371550918 - 0.5382375717 0.5570588112 0.5382375717 - 0.5393161774 0.5581569076 0.5393161774 - 0.5403949022 0.5592550039 0.5403949022 - 0.5414698720 0.5603502989 0.5414698720 - 0.5425440073 0.5614449978 0.5425440073 - 0.5436155200 0.5625376105 0.5436155200 - 0.5446845889 0.5636284947 0.5446845889 - 0.5457528830 0.5647183061 0.5457528830 - 0.5468186140 0.5658050179 0.5468186140 - 0.5478842854 0.5668917894 0.5478842854 - 0.5489463806 0.5679742098 0.5489463806 - 0.5500082970 0.5690563917 0.5500082970 - 0.5510675907 0.5701354742 0.5510675907 - 0.5521252751 0.5712130070 0.5521252751 - 0.5531818271 0.5722888112 0.5531818271 - 0.5542349815 0.5733609796 0.5542349815 - 0.5552883148 0.5744333267 0.5552883148 - 0.5563371181 0.5755001903 0.5563371181 - 0.5573856235 0.5765669942 0.5573856235 - 0.5584312081 0.5776314139 0.5584312081 - 0.5594745278 0.5786941051 0.5594745278 - 0.5605170131 0.5797560811 0.5605170131 - 0.5615549088 0.5808146000 0.5615549088 - 0.5625928044 0.5818731189 0.5625928044 - 0.5636265278 0.5829282999 0.5636265278 - 0.5646585822 0.5839822888 0.5646585822 - 0.5656889081 0.5850347877 0.5656889081 - 0.5667148829 0.5860841274 0.5667148829 - 0.5677409768 0.5871334076 0.5677409768 - 0.5687615871 0.5881785154 0.5687615871 - 0.5697811842 0.5892227888 0.5697811842 - 0.5707985163 0.5902652144 0.5707985163 - 0.5718113780 0.5913044214 0.5718113780 - 0.5728244185 0.5923436284 0.5728244185 - 0.5738328099 0.5933796167 0.5738328099 - 0.5748404264 0.5944151878 0.5748404264 - 0.5758461952 0.5954493880 0.5758461952 - 0.5768483877 0.5964813232 0.5768483877 - 0.5778505206 0.5975130796 0.5778505206 - 0.5788481236 0.5985416770 0.5788481236 - 0.5798444748 0.5995696187 0.5798444748 - 0.5808395147 0.6005964875 0.5808395147 - 0.5818299055 0.6016203165 0.5818299055 - 0.5828202963 0.6026440859 0.5828202963 - 0.5838078260 0.6036651134 0.5838078260 - 0.5847936869 0.6046845913 0.5847936869 - 0.5857794881 0.6057038903 0.5857794881 - 0.5867608190 0.6067190170 0.5867608190 - 0.5877420902 0.6077340841 0.5877420902 - 0.5887213945 0.6087473035 0.5887213945 - 0.5896978974 0.6097577810 0.5896978974 - 0.5906744003 0.6107683182 0.5906744003 - 0.5916472077 0.6117751002 0.5916472077 - 0.5926188231 0.6127809286 0.5926188231 - 0.5935899019 0.6137859821 0.5935899019 - 0.5945563912 0.6147853136 0.5945563912 - 0.5955228806 0.6157847047 0.5955228806 - 0.5964874029 0.6167812943 0.5964874029 - 0.5974487066 0.6177741289 0.5974487066 - 0.5984100103 0.6187667847 0.5984100103 - 0.5993676782 0.6197549105 0.5993676782 - 0.6003236771 0.6207411289 0.6003236771 - 0.6012796760 0.6217272878 0.6012796760 - 0.6022304893 0.6227068901 0.6022304893 - 0.6031810045 0.6236863136 0.6031810045 - 0.6041305065 0.6246638894 0.6041305065 - 0.6050754786 0.6256346703 0.6050754786 - 0.6060203910 0.6266055107 0.6060203910 - 0.6069629788 0.6275725961 0.6069629788 - 0.6079022884 0.6285349131 0.6079022884 - 0.6088414788 0.6294971108 0.6088414788 - 0.6097772717 0.6304540038 0.6097772717 - 0.6107106805 0.6314077973 0.6107106805 - 0.6116440892 0.6323614717 0.6116440892 - 0.6125729084 0.6333085895 0.6125729084 - 0.6135004163 0.6342539191 0.6135004163 - 0.6144278049 0.6351991892 0.6144278049 - 0.6153497100 0.6361367702 0.6153497100 - 0.6162710786 0.6370738149 0.6162710786 - 0.6171923280 0.6380103230 0.6171923280 - 0.6181074977 0.6389390826 0.6181074977 - 0.6190227270 0.6398677826 0.6190227270 - 0.6199370027 0.6407952905 0.6199370027 - 0.6208459139 0.6417160034 0.6208459139 - 0.6217548251 0.6426365972 0.6217548251 - 0.6226627231 0.6435554028 0.6226627231 - 0.6235666871 0.6444680095 0.6235666871 - 0.6244707108 0.6453806162 0.6244707108 - 0.6253734231 0.6462908983 0.6253734231 - 0.6262723804 0.6471955180 0.6262723804 - 0.6271713972 0.6481000781 0.6271713972 - 0.6280689836 0.6490027905 0.6280689836 - 0.6289629936 0.6499009728 0.6289629936 - 0.6298570037 0.6507992148 0.6298570037 - 0.6307495236 0.6516954899 0.6307495236 - 0.6316384077 0.6525874138 0.6316384077 - 0.6325272918 0.6534792781 0.6325272918 - 0.6334148049 0.6543694139 0.6334148049 - 0.6342985034 0.6552550793 0.6342985034 - 0.6351823211 0.6561406851 0.6351823211 - 0.6360648274 0.6570249200 0.6360648274 - 0.6369432807 0.6579043865 0.6369432807 - 0.6378217936 0.6587839127 0.6378217936 - 0.6386997104 0.6596624255 0.6386997104 - 0.6395745873 0.6605358124 0.6395745873 - 0.6404494047 0.6614091992 0.6404494047 - 0.6413241029 0.6622822285 0.6413241029 - 0.6421937943 0.6631495953 0.6421937943 - 0.6430636048 0.6640170217 0.6430636048 - 0.6439334154 0.6648843884 0.6439334154 - 0.6447985768 0.6657463908 0.6447985768 - 0.6456633806 0.6666080952 0.6456633806 - 0.6465281844 0.6674697995 0.6465281844 - 0.6473892927 0.6683270931 0.6473892927 - 0.6482492089 0.6691833138 0.6482492089 - 0.6491091251 0.6700394750 0.6491091251 - 0.6499661207 0.6708925962 0.6499661207 - 0.6508212090 0.6717435718 0.6508212090 - 0.6516762972 0.6725946069 0.6516762972 - 0.6525288820 0.6734434962 0.6525288820 - 0.6533777118 0.6742895246 0.6533777118 - 0.6542264819 0.6751354933 0.6542264819 - 0.6550742984 0.6759806871 0.6550742984 - 0.6559169888 0.6768220067 0.6559169888 - 0.6567596793 0.6776633263 0.6567596793 - 0.6576023102 0.6785045862 0.6576023102 - 0.6584395766 0.6793416739 0.6584395766 - 0.6592763066 0.6801785231 0.6592763066 - 0.6601129770 0.6810153127 0.6601129770 - 0.6609460711 0.6818485260 0.6609460711 - 0.6617770791 0.6826794744 0.6617770791 - 0.6626080275 0.6835106015 0.6626080275 - 0.6634371877 0.6843398213 0.6634371877 - 0.6642624736 0.6851654053 0.6642624736 - 0.6650878787 0.6859909892 0.6650878787 - 0.6659132242 0.6868165731 0.6659132242 - 0.6667333245 0.6876372099 0.6667333245 - 0.6675531864 0.6884577274 0.6675531864 - 0.6683731079 0.6892783046 0.6683731079 - 0.6691898704 0.6900957823 0.6691898704 - 0.6700046062 0.6909114718 0.6700046062 - 0.6708192825 0.6917272210 0.6708192825 - 0.6716328859 0.6925418973 0.6716328859 - 0.6724424958 0.6933531761 0.6724424958 - 0.6732522249 0.6941645145 0.6732522249 - 0.6740617752 0.6949757934 0.6740617752 - 0.6748675704 0.6957837939 0.6748675704 - 0.6756724119 0.6965909004 0.6756724119 - 0.6764771938 0.6973981261 0.6764771938 - 0.6772803068 0.6982039213 0.6772803068 - 0.6780803800 0.6990072131 0.6780803800 - 0.6788805127 0.6998106241 0.6788805127 - 0.6796805263 0.7006139159 0.6796805263 - 0.6804767251 0.7014129758 0.6804767251 - 0.6812722087 0.7022113800 0.6812722087 - 0.6820678115 0.7030097842 0.6820678115 - 0.6828618050 0.7038065195 0.6828618050 - 0.6836528778 0.7046002746 0.6836528778 - 0.6844441295 0.7053942084 0.6844441295 - 0.6852353215 0.7061880827 0.6852353215 - 0.6860229969 0.7069783807 0.6860229969 - 0.6868100166 0.7077680826 0.6868100166 - 0.6875969768 0.7085577846 0.6875969768 - 0.6883826256 0.7093464136 0.6883826256 - 0.6891641021 0.7101323009 0.6891641021 - 0.6899456978 0.7109181881 0.6899456978 - 0.6907271743 0.7117040753 0.6907271743 - 0.6915053725 0.7124866843 0.6915053725 - 0.6922817230 0.7132676244 0.6922817230 - 0.6930580139 0.7140485048 0.6930580139 - 0.6938341856 0.7148293257 0.6938341856 - 0.6946055293 0.7156057954 0.6946055293 - 0.6953768134 0.7163822055 0.6953768134 - 0.6961480975 0.7171586752 0.6961480975 - 0.6969177127 0.7179335952 0.6969177127 - 0.6976842284 0.7187060714 0.6976842284 - 0.6984508038 0.7194784880 0.6984508038 - 0.6992173195 0.7202510238 0.6992173195 - 0.6999806762 0.7210199833 0.6999806762 - 0.7007427216 0.7217875719 0.7007427216 - 0.7015047073 0.7225551009 0.7015047073 - 0.7022666931 0.7233226895 0.7022666931 - 0.7030245066 0.7240859270 0.7030245066 - 0.7037820816 0.7248489857 0.7037820816 - 0.7045397162 0.7256121039 0.7045397162 - 0.7052962184 0.7263739109 0.7052962184 - 0.7060483098 0.7271317244 0.7060483098 - 0.7068005204 0.7278894186 0.7068005204 - 0.7075526118 0.7286471128 0.7075526118 - 0.7083024979 0.7294024825 0.7083024979 - 0.7090494037 0.7301552892 0.7090494037 - 0.7097964287 0.7309080958 0.7097964287 - 0.7105432749 0.7316607833 0.7105432749 - 0.7112872005 0.7324107289 0.7112872005 - 0.7120292187 0.7331590056 0.7120292187 - 0.7127712965 0.7339072227 0.7127712965 - 0.7135133147 0.7346554995 0.7135133147 - 0.7142516971 0.7354004979 0.7142516971 - 0.7149891853 0.7361447215 0.7149891853 - 0.7157266140 0.7368888855 0.7157266140 - 0.7164639831 0.7376331091 0.7164639831 - 0.7171975970 0.7383713126 0.7171975970 - 0.7179307938 0.7391089201 0.7179307938 - 0.7186638713 0.7398465872 0.7186638713 - 0.7193971276 0.7405840755 0.7193971276 - 0.7201262116 0.7413142920 0.7201262116 - 0.7208554149 0.7420445085 0.7208554149 - 0.7215844989 0.7427747846 0.7215844989 - 0.7223134041 0.7435045242 0.7223134041 - 0.7230387926 0.7442278266 0.7230387926 - 0.7237643003 0.7449511290 0.7237643003 - 0.7244896889 0.7456744909 0.7244896889 - 0.7252147198 0.7463970780 0.7252147198 - 0.7259353995 0.7471140027 0.7259353995 - 0.7266560793 0.7478309870 0.7266560793 - 0.7273768187 0.7485479116 0.7273768187 - 0.7280970812 0.7492641807 0.7280970812 - 0.7288134098 0.7499753237 0.7288134098 - 0.7295296788 0.7506862879 0.7295296788 - 0.7302460074 0.7513973117 0.7302460074 - 0.7309620976 0.7521079779 0.7309620976 - 0.7316743135 0.7528135777 0.7316743135 - 0.7323865294 0.7535192966 0.7323865294 - 0.7330986857 0.7542248964 0.7330986857 - 0.7338109016 0.7549306154 0.7338109016 - 0.7345194817 0.7556313276 0.7345194817 - 0.7352281213 0.7563319802 0.7352281213 - 0.7359365821 0.7570326924 0.7359365821 - 0.7366451025 0.7577335238 0.7366451025 - 0.7373495102 0.7584289908 0.7373495102 - 0.7380533218 0.7591239214 0.7380533218 - 0.7387570739 0.7598189116 0.7387570739 - 0.7394608855 0.7605137825 0.7394608855 - 0.7401614785 0.7612038255 0.7401614785 - 0.7408609986 0.7618921995 0.7408609986 - 0.7415605187 0.7625805736 0.7415605187 - 0.7422599792 0.7632690072 0.7422599792 - 0.7429572940 0.7639539838 0.7429572940 - 0.7436528802 0.7646365166 0.7436528802 - 0.7443485260 0.7653189898 0.7443485260 - 0.7450441122 0.7660015225 0.7450441122 - 0.7457385063 0.7666820288 0.7457385063 - 0.7464305758 0.7673591971 0.7464305758 - 0.7471227050 0.7680364251 0.7471227050 - 0.7478148937 0.7687135935 0.7478148937 - 0.7485067248 0.7693902850 0.7485067248 - 0.7491956949 0.7700628042 0.7491956949 - 0.7498847842 0.7707353830 0.7498847842 - 0.7505738735 0.7714080215 0.7505738735 - 0.7512630224 0.7720804811 0.7512630224 - 0.7519487739 0.7727484703 0.7519487739 - 0.7526339889 0.7734156251 0.7526339889 - 0.7533192039 0.7740827203 0.7533192039 - 0.7540044188 0.7747498751 0.7540044188 - 0.7546877861 0.7754138112 0.7546877861 - 0.7553694844 0.7760748863 0.7553694844 - 0.7560511827 0.7767360806 0.7560511827 - 0.7567330003 0.7773972154 0.7567330003 - 0.7574139237 0.7780573964 0.7574139237 - 0.7580912709 0.7787132263 0.7580912709 - 0.7587686777 0.7793691158 0.7587686777 - 0.7594460845 0.7800250053 0.7594460845 - 0.7601234913 0.7806808949 0.7601234913 - 0.7607979178 0.7813320756 0.7607979178 - 0.7614712715 0.7819821835 0.7614712715 - 0.7621448040 0.7826321721 0.7621448040 - 0.7628182769 0.7832822800 0.7628182769 - 0.7634900212 0.7839304209 0.7634900212 - 0.7641587257 0.7845754027 0.7641587257 - 0.7648273110 0.7852203250 0.7648273110 - 0.7654958963 0.7858651876 0.7654958963 - 0.7661646008 0.7865102291 0.7661646008 - 0.7668293118 0.7871509790 0.7668293118 - 0.7674936056 0.7877914906 0.7674936056 - 0.7681578994 0.7884320021 0.7681578994 - 0.7688220739 0.7890725136 0.7688220739 - 0.7694842815 0.7897108197 0.7694842815 - 0.7701433897 0.7903463244 0.7701433897 - 0.7708023787 0.7909818292 0.7708023787 - 0.7714614868 0.7916172743 0.7714614868 - 0.7721205950 0.7922527194 0.7721205950 - 0.7727754116 0.7928832173 0.7727754116 - 0.7734296918 0.7935130000 0.7734296918 - 0.7740839720 0.7941429019 0.7740839720 - 0.7747381926 0.7947726846 0.7747381926 - 0.7753911018 0.7954009771 0.7753911018 - 0.7760409713 0.7960259914 0.7760409713 - 0.7766909003 0.7966510057 0.7766909003 - 0.7773407102 0.7972760201 0.7773407102 - 0.7779905796 0.7979009748 0.7779905796 - 0.7786368132 0.7985221744 0.7786368132 - 0.7792813778 0.7991417050 0.7792813778 - 0.7799261212 0.7997612953 0.7799261212 - 0.7805706859 0.8003808856 0.7805706859 - 0.7812150717 0.8010001779 0.7812150717 - 0.7818549275 0.8016149998 0.7818549275 - 0.7824947834 0.8022298813 0.7824947834 - 0.7831345797 0.8028448224 0.7831345797 - 0.7837744951 0.8034597039 0.7837744951 - 0.7844126821 0.8040724993 0.7844126821 - 0.7850481868 0.8046821952 0.7850481868 - 0.7856836915 0.8052918911 0.7856836915 - 0.7863191962 0.8059015274 0.7863191962 - 0.7869547009 0.8065112233 0.7869547009 - 0.7875875831 0.8071171045 0.7875875831 - 0.7882192135 0.8077210188 0.7882192135 - 0.7888507247 0.8083249927 0.7888507247 - 0.7894822955 0.8089289069 0.7894822955 - 0.7901139259 0.8095328808 0.7901139259 - 0.7907413244 0.8101323843 0.7907413244 - 0.7913681269 0.8107314110 0.7913681269 - 0.7919949889 0.8113303781 0.7919949889 - 0.7926219106 0.8119294047 0.7926219106 - 0.7932484150 0.8125280738 0.7932484150 - 0.7938709855 0.8131231070 0.7938709855 - 0.7944936752 0.8137180209 0.7944936752 - 0.7951163054 0.8143129945 0.7951163054 - 0.7957389951 0.8149080276 0.7957389951 - 0.7963607907 0.8155021071 0.7963607907 - 0.7969797254 0.8160938025 0.7969797254 - 0.7975987196 0.8166856170 0.7975987196 - 0.7982177138 0.8172773123 0.7982177138 - 0.7988365889 0.8178690076 0.7988365889 - 0.7994540930 0.8184589148 0.7994540930 - 0.8000686765 0.8190457821 0.8000686765 - 0.8006833196 0.8196325898 0.8006833196 - 0.8012979031 0.8202195168 0.8012979031 - 0.8019124866 0.8208063245 0.8019124866 - 0.8025255203 0.8213908076 0.8025255203 - 0.8031362891 0.8219724298 0.8031362891 - 0.8037471175 0.8225538731 0.8037471175 - 0.8043578863 0.8231354952 0.8043578863 - 0.8049687147 0.8237169981 0.8049687147 - 0.8055779934 0.8242964149 0.8055779934 - 0.8061854839 0.8248735070 0.8061854839 - 0.8067929745 0.8254505992 0.8067929745 - 0.8074005842 0.8260276914 0.8074005842 - 0.8080080748 0.8266047835 0.8080080748 - 0.8086137772 0.8271802068 0.8086137772 - 0.8092173934 0.8277537823 0.8092173934 - 0.8098208904 0.8283274174 0.8098208904 - 0.8104245067 0.8289008737 0.8104245067 - 0.8110281229 0.8294745088 0.8110281229 - 0.8116297722 0.8300458193 0.8116297722 - 0.8122286797 0.8306140900 0.8122286797 - 0.8128275871 0.8311824203 0.8128275871 - 0.8134266138 0.8317506909 0.8134266138 - 0.8140255213 0.8323189020 0.8140255213 - 0.8146228194 0.8328853846 0.8146228194 - 0.8152164817 0.8334481120 0.8152164817 - 0.8158102036 0.8340108991 0.8158102036 - 0.8164039254 0.8345736265 0.8164039254 - 0.8169975877 0.8351362944 0.8169975877 - 0.8175904751 0.8356981277 0.8175904751 - 0.8181796074 0.8362563252 0.8181796074 - 0.8187686801 0.8368144035 0.8187686801 - 0.8193578124 0.8373724818 0.8193578124 - 0.8199468851 0.8379306793 0.8199468851 - 0.8205358982 0.8384886980 0.8205358982 - 0.8211209774 0.8390418887 0.8211209774 - 0.8217059970 0.8395950794 0.8217059970 - 0.8222910762 0.8401483893 0.8222910762 - 0.8228762150 0.8407015800 0.8228762150 - 0.8234611750 0.8412547708 0.8234611750 - 0.8240435719 0.8418037891 0.8240435719 - 0.8246251941 0.8423519135 0.8246251941 - 0.8252068162 0.8428999782 0.8252068162 - 0.8257884979 0.8434481025 0.8257884979 - 0.8263701200 0.8439962268 0.8263701200 - 0.8269495964 0.8445411921 0.8269495964 - 0.8275271058 0.8450837731 0.8275271058 - 0.8281046152 0.8456264138 0.8281046152 - 0.8286821842 0.8461689949 0.8286821842 - 0.8292596936 0.8467115760 0.8292596936 - 0.8298364282 0.8472530246 0.8298364282 - 0.8304104209 0.8477910757 0.8304104209 - 0.8309844136 0.8483291268 0.8309844136 - 0.8315584064 0.8488671780 0.8315584064 - 0.8321323991 0.8494052291 0.8321323991 - 0.8327063918 0.8499432802 0.8327063918 - 0.8332766891 0.8504770994 0.8332766891 - 0.8338465095 0.8510105014 0.8338465095 - 0.8344163895 0.8515439034 0.8344163895 - 0.8349862099 0.8520773053 0.8349862099 - 0.8355559707 0.8526107073 0.8355559707 - 0.8361235261 0.8531416059 0.8361235261 - 0.8366885781 0.8536701202 0.8366885781 - 0.8372538090 0.8541985750 0.8372538090 - 0.8378189206 0.8547270894 0.8378189206 - 0.8383839726 0.8552556038 0.8383839726 - 0.8389489055 0.8557837009 0.8389489055 - 0.8395100236 0.8563070893 0.8395100236 - 0.8400710821 0.8568304777 0.8400710821 - 0.8406322002 0.8573539257 0.8406322002 - 0.8411933184 0.8578773141 0.8411933184 - 0.8417543173 0.8584007025 0.8417543173 - 0.8423134089 0.8589209914 0.8423134089 - 0.8428711295 0.8594391942 0.8428711295 - 0.8434287906 0.8599573970 0.8434287906 - 0.8439865112 0.8604755998 0.8439865112 - 0.8445441127 0.8609936833 0.8445441127 - 0.8451015949 0.8615115881 0.8451015949 - 0.8456565142 0.8620254993 0.8456565142 - 0.8462113738 0.8625394106 0.8462113738 - 0.8467662930 0.8630533218 0.8467662930 - 0.8473210931 0.8635671735 0.8473210931 - 0.8478760123 0.8640810847 0.8478760123 - 0.8484287262 0.8645929098 0.8484287262 - 0.8489791751 0.8651022911 0.8489791751 - 0.8495296240 0.8656117916 0.8495296240 - 0.8500800133 0.8661211729 0.8500800133 - 0.8506304026 0.8666306734 0.8506304026 - 0.8511807919 0.8671401739 0.8511807919 - 0.8517271280 0.8676456213 0.8517271280 - 0.8522726297 0.8681504130 0.8522726297 - 0.8528181911 0.8686553240 0.8528181911 - 0.8533636928 0.8691601157 0.8533636928 - 0.8539093137 0.8696649075 0.8539093137 - 0.8544539809 0.8701689839 0.8544539809 - 0.8549953103 0.8706703186 0.8549953103 - 0.8555366993 0.8711714745 0.8555366993 - 0.8560780883 0.8716726899 0.8560780883 - 0.8566194177 0.8721739054 0.8566194177 - 0.8571608067 0.8726751804 0.8571608067 - 0.8577004075 0.8731746078 0.8577004075 - 0.8582382798 0.8736721873 0.8582382798 - 0.8587760925 0.8741698861 0.8587760925 - 0.8593140244 0.8746675253 0.8593140244 - 0.8598517776 0.8751651049 0.8598517776 - 0.8603895903 0.8756626844 0.8603895903 - 0.8609246016 0.8761566877 0.8609246016 - 0.8614584208 0.8766493797 0.8614584208 - 0.8619921803 0.8771420121 0.8619921803 - 0.8625261188 0.8776347041 0.8625261188 - 0.8630598783 0.8781273961 0.8630598783 - 0.8635936975 0.8786200285 0.8635936975 - 0.8641244173 0.8791080713 0.8641244173 - 0.8646547794 0.8795956969 0.8646547794 - 0.8651853204 0.8800833821 0.8651853204 - 0.8657156825 0.8805711269 0.8657156825 - 0.8662461042 0.8810586929 0.8662461042 - 0.8667764068 0.8815460801 0.8667764068 - 0.8673040271 0.8820288181 0.8673040271 - 0.8678315878 0.8825113773 0.8678315878 - 0.8683593273 0.8829941154 0.8683593273 - 0.8688868880 0.8834767938 0.8688868880 - 0.8694145083 0.8839594722 0.8694145083 - 0.8699415922 0.8844413757 0.8699415922 - 0.8704659939 0.8849189878 0.8704659939 - 0.8709903955 0.8853965998 0.8709903955 - 0.8715146780 0.8858740926 0.8715146780 - 0.8720390797 0.8863517046 0.8720390797 - 0.8725634813 0.8868293166 0.8725634813 - 0.8730871081 0.8873060942 0.8730871081 - 0.8736078143 0.8877797127 0.8736078143 - 0.8741285205 0.8882532716 0.8741285205 - 0.8746492267 0.8887268901 0.8746492267 - 0.8751698136 0.8892006278 0.8751698136 - 0.8756905198 0.8896741867 0.8756905198 - 0.8762103915 0.8901467919 0.8762103915 - 0.8767271042 0.8906152844 0.8767271042 - 0.8772436976 0.8910837770 0.8772436976 - 0.8777604103 0.8915523887 0.8777604103 - 0.8782770038 0.8920208812 0.8782770038 - 0.8787937164 0.8924893737 0.8787937164 - 0.8793097734 0.8929569721 0.8793097734 - 0.8798230886 0.8934193254 0.8798230886 - 0.8803362846 0.8938816190 0.8803362846 - 0.8808495998 0.8943439126 0.8808495998 - 0.8813629150 0.8948062062 0.8813629150 - 0.8818761110 0.8952684999 0.8818761110 - 0.8823893070 0.8957306147 0.8823893070 - 0.8828998208 0.8961877823 0.8828998208 - 0.8834103942 0.8966450095 0.8834103942 - 0.8839209080 0.8971021771 0.8839209080 - 0.8844314218 0.8975594044 0.8844314218 - 0.8849418759 0.8980165720 0.8849418759 - 0.8854525089 0.8984737992 0.8854525089 - 0.8859602213 0.8989266157 0.8859602213 - 0.8864675760 0.8993787766 0.8864675760 - 0.8869749904 0.8998309970 0.8869749904 - 0.8874824047 0.9002832174 0.8874824047 - 0.8879898190 0.9007353783 0.8879898190 - 0.8884971142 0.9011875987 0.8884971142 - 0.8890014291 0.9016364217 0.8890014291 - 0.8895040751 0.9020836949 0.8895040751 - 0.8900067806 0.9025309086 0.8900067806 - 0.8905096054 0.9029781222 0.8905096054 - 0.8910123110 0.9034253955 0.8910123110 - 0.8915150166 0.9038726091 0.8915150166 - 0.8920161128 0.9043182135 0.8920161128 - 0.8925150037 0.9047616124 0.8925150037 - 0.8930137753 0.9052050114 0.8930137753 - 0.8935126066 0.9056484103 0.8935126066 - 0.8940114975 0.9060918093 0.8940114975 - 0.8945103288 0.9065353274 0.8945103288 - 0.8950086832 0.9069780707 0.8950086832 - 0.8955042958 0.9074168801 0.8955042958 - 0.8959999084 0.9078556895 0.8959999084 - 0.8964955211 0.9082944989 0.8964955211 - 0.8969910741 0.9087331891 0.8969910741 - 0.8974866271 0.9091719985 0.8974866271 - 0.8979821801 0.9096108079 0.8979821801 - 0.8984758854 0.9100461006 0.8984758854 - 0.8989689946 0.9104802012 0.8989689946 - 0.8994619846 0.9109144211 0.8994619846 - 0.8999550939 0.9113485217 0.8999550939 - 0.9004480839 0.9117826819 0.9004480839 - 0.9009411931 0.9122167826 0.9009411931 - 0.9014332891 0.9126496911 0.9014332891 - 0.9019234180 0.9130802751 0.9019234180 - 0.9024136066 0.9135109186 0.9024136066 - 0.9029036760 0.9139415026 0.9029036760 - 0.9033939242 0.9143720865 0.9033939242 - 0.9038839936 0.9148026705 0.9038839936 - 0.9043741822 0.9152333140 0.9043741822 - 0.9048606157 0.9156600237 0.9048606157 - 0.9053465128 0.9160860777 0.9053465128 - 0.9058324099 0.9165123105 0.9058324099 - 0.9063184261 0.9169384241 0.9063184261 - 0.9068043232 0.9173644781 0.9068043232 - 0.9072902203 0.9177907109 0.9072902203 - 0.9077749252 0.9182155728 0.9077749252 - 0.9082573056 0.9186385274 0.9082573056 - 0.9087396860 0.9190613031 0.9087396860 - 0.9092221856 0.9194840789 0.9092221856 - 0.9097046256 0.9199069142 0.9097046256 - 0.9101871252 0.9203298092 0.9101871252 - 0.9106695056 0.9207525849 0.9106695056 - 0.9111497998 0.9211729765 0.9111497998 - 0.9116293788 0.9215927124 0.9116293788 - 0.9121090770 0.9220123887 0.9121090770 - 0.9125887156 0.9224321246 0.9125887156 - 0.9130684137 0.9228518009 0.9130684137 - 0.9135479927 0.9232714772 0.9135479927 - 0.9140272141 0.9236903787 0.9140272141 - 0.9145038724 0.9241051078 0.9145038724 - 0.9149804711 0.9245197177 0.9149804711 - 0.9154571891 0.9249343276 0.9154571891 - 0.9159337878 0.9253488779 0.9159337878 - 0.9164105058 0.9257636070 0.9164105058 - 0.9168871045 0.9261782169 0.9168871045 - 0.9173616767 0.9265909791 0.9173616767 - 0.9178339839 0.9270015955 0.9178339839 - 0.9183062911 0.9274122715 0.9183062911 - 0.9187785983 0.9278230071 0.9187785983 - 0.9192509055 0.9282336831 0.9192509055 - 0.9197230935 0.9286444187 0.9197230935 - 0.9201954007 0.9290549755 0.9201954007 - 0.9206650853 0.9294636846 0.9206650853 - 0.9211338162 0.9298716784 0.9211338162 - 0.9216024876 0.9302796125 0.9216024876 - 0.9220712185 0.9306876063 0.9220712185 - 0.9225397706 0.9310954809 0.9225397706 - 0.9230085015 0.9315034151 0.9230085015 - 0.9234771729 0.9319114089 0.9234771729 - 0.9239432812 0.9323160052 0.9239432812 - 0.9244090915 0.9327204227 0.9244090915 - 0.9248750210 0.9331247211 0.9248750210 - 0.9253407717 0.9335290790 0.9253407717 - 0.9258067012 0.9339333773 0.9258067012 - 0.9262725115 0.9343377948 0.9262725115 - 0.9267380834 0.9347419143 0.9267380834 - 0.9272007942 0.9351428747 0.9272007942 - 0.9276636243 0.9355438948 0.9276636243 - 0.9281262755 0.9359449744 0.9281262755 - 0.9285889864 0.9363459945 0.9285889864 - 0.9290516973 0.9367470145 0.9290516973 - 0.9295145273 0.9371480942 0.9295145273 - 0.9299765825 0.9375485778 0.9299765825 - 0.9304360151 0.9379463792 0.9304360151 - 0.9308953881 0.9383442998 0.9308953881 - 0.9313548207 0.9387422204 0.9313548207 - 0.9318141937 0.9391400814 0.9318141937 - 0.9322736263 0.9395378828 0.9322736263 - 0.9327328801 0.9399358034 0.9327328801 - 0.9331911206 0.9403327703 0.9331911206 - 0.9336444736 0.9407266974 0.9336444736 - 0.9340978861 0.9411206245 0.9340978861 - 0.9345511794 0.9415144920 0.9345511794 - 0.9350045919 0.9419082999 0.9350045919 - 0.9354580045 0.9423022270 0.9354580045 - 0.9359112978 0.9426960945 0.9359112978 - 0.9363633990 0.9430891275 0.9363633990 - 0.9368094206 0.9434779882 0.9368094206 - 0.9372553825 0.9438670278 0.9372553825 - 0.9377014041 0.9442558885 0.9377014041 - 0.9381474853 0.9446449280 0.9381474853 - 0.9385935068 0.9450337887 0.9385935068 - 0.9390395284 0.9454228282 0.9390395284 - 0.9394848943 0.9458109736 0.9394848943 - 0.9399245977 0.9461929798 0.9399245977 - 0.9403643012 0.9465749264 0.9403643012 - 0.9408038855 0.9469568729 0.9408038855 - 0.9412435889 0.9473388195 0.9412435889 - 0.9416832924 0.9477208257 0.9416832924 - 0.9421229959 0.9481027126 0.9421229959 - 0.9425626993 0.9484847188 0.9425626993 - 0.9429975748 0.9488611221 0.9429975748 - 0.9434322119 0.9492372274 0.9434322119 - 0.9438669086 0.9496132731 0.9438669086 - 0.9443014860 0.9499893785 0.9443014860 - 0.9447361827 0.9503654838 0.9447361827 - 0.9451708198 0.9507415891 0.9451708198 - 0.9456055164 0.9511176944 0.9456055164 - 0.9460369945 0.9514914751 0.9460369945 - 0.9464673996 0.9518644810 0.9464673996 - 0.9468979239 0.9522374272 0.9468979239 - 0.9473283887 0.9526103735 0.9473283887 - 0.9477587938 0.9529833794 0.9477587938 - 0.9481893182 0.9533563852 0.9481893182 - 0.9486197233 0.9537293911 0.9486197233 - 0.9490481019 0.9541009068 0.9490481019 - 0.9494742751 0.9544708729 0.9494742751 - 0.9499005079 0.9548410177 0.9499005079 - 0.9503266811 0.9552109838 0.9503266811 - 0.9507529140 0.9555810094 0.9507529140 - 0.9511790872 0.9559510946 0.9511790872 - 0.9516053200 0.9563211203 0.9516053200 - 0.9520307779 0.9566904902 0.9520307779 - 0.9524527192 0.9570568800 0.9524527192 - 0.9528746009 0.9574233890 0.9528746009 - 0.9532964826 0.9577897787 0.9532964826 - 0.9537184238 0.9581562877 0.9537184238 - 0.9541404247 0.9585226774 0.9541404247 - 0.9545623064 0.9588891268 0.9545623064 - 0.9549841881 0.9592555761 0.9549841881 - 0.9554026127 0.9596186876 0.9554026127 - 0.9558200836 0.9599809051 0.9558200836 - 0.9562376142 0.9603431821 0.9562376142 - 0.9566550255 0.9607055187 0.9566550255 - 0.9570724964 0.9610676765 0.9570724964 - 0.9574900270 0.9614300132 0.9574900270 - 0.9579074979 0.9617921710 0.9579074979 - 0.9583234191 0.9621530771 0.9583234191 - 0.9587361217 0.9625114202 0.9587361217 - 0.9591488838 0.9628697038 0.9591488838 - 0.9595617056 0.9632279873 0.9595617056 - 0.9599744081 0.9635863900 0.9599744081 - 0.9603871703 0.9639446735 0.9603871703 - 0.9607999921 0.9643030167 0.9607999921 - 0.9612128139 0.9646613002 0.9612128139 - 0.9616206884 0.9650158882 0.9616206884 - 0.9620277286 0.9653698802 0.9620277286 - 0.9624347091 0.9657238126 0.9624347091 - 0.9628416896 0.9660778046 0.9628416896 - 0.9632487297 0.9664316773 0.9632487297 - 0.9636557102 0.9667857289 0.9636557102 - 0.9640626907 0.9671396017 0.9640626907 - 0.9644678831 0.9674922824 0.9644678831 - 0.9648684859 0.9678416252 0.9648684859 - 0.9652690291 0.9681910276 0.9652690291 - 0.9656695127 0.9685403109 0.9656695127 - 0.9660701156 0.9688897133 0.9660701156 - 0.9664705992 0.9692389965 0.9664705992 - 0.9668712020 0.9695882797 0.9668712020 - 0.9672716856 0.9699376822 0.9672716856 - 0.9676678777 0.9702842832 0.9676678777 - 0.9680616856 0.9706295133 0.9680616856 - 0.9684556127 0.9709746242 0.9684556127 - 0.9688494205 0.9713197947 0.9688494205 - 0.9692432284 0.9716649055 0.9692432284 - 0.9696370959 0.9720100760 0.9696370959 - 0.9700309038 0.9723551869 0.9700309038 - 0.9704247713 0.9727004170 0.9704247713 - 0.9708117247 0.9730417132 0.9708117247 - 0.9711983204 0.9733828902 0.9711983204 - 0.9715849161 0.9737240076 0.9715849161 - 0.9719715118 0.9740651846 0.9719715118 - 0.9723581076 0.9744063020 0.9723581076 - 0.9727448225 0.9747474790 0.9727448225 - 0.9731314182 0.9750885963 0.9731314182 - 0.9735164046 0.9754288793 0.9735164046 - 0.9738947749 0.9757658243 0.9738947749 - 0.9742732048 0.9761027098 0.9742732048 - 0.9746515751 0.9764394760 0.9746515751 - 0.9750298858 0.9767764211 0.9750298858 - 0.9754083157 0.9771133065 0.9754083157 - 0.9757866859 0.9774501920 0.9757866859 - 0.9761651158 0.9777870178 0.9761651158 - 0.9765400887 0.9781219959 0.9765400887 - 0.9769093990 0.9784539938 0.9769093990 - 0.9772787094 0.9787858725 0.9772787094 - 0.9776480198 0.9791178107 0.9776480198 - 0.9780173898 0.9794496894 0.9780173898 - 0.9783867002 0.9797816873 0.9783867002 - 0.9787560105 0.9801136255 0.9787560105 - 0.9791253209 0.9804455042 0.9791253209 - 0.9794902205 0.9807748199 0.9794902205 - 0.9798504114 0.9811015129 0.9798504114 - 0.9802107215 0.9814282060 0.9802107215 - 0.9805709124 0.9817547798 0.9805709124 - 0.9809311032 0.9820814729 0.9809311032 - 0.9812914133 0.9824081063 0.9812914133 - 0.9816516042 0.9827347994 0.9816516042 - 0.9820119143 0.9830613732 0.9820119143 - 0.9823670983 0.9833847284 0.9823670983 - 0.9827178717 0.9837049842 0.9827178717 - 0.9830688238 0.9840254188 0.9830688238 - 0.9834197164 0.9843456745 0.9834197164 - 0.9837704897 0.9846661091 0.9837704897 - 0.9841213822 0.9849864244 0.9841213822 - 0.9844722748 0.9853066802 0.9844722748 - 0.9848231077 0.9856271148 0.9848231077 - 0.9851688147 0.9859436154 0.9851688147 - 0.9855098128 0.9862567186 0.9855098128 - 0.9858506918 0.9865697026 0.9858506918 - 0.9861915708 0.9868828058 0.9861915708 - 0.9865326285 0.9871957898 0.9865326285 - 0.9868735075 0.9875088930 0.9868735075 - 0.9872145057 0.9878218770 0.9872145057 - 0.9875553846 0.9881349802 0.9875553846 - 0.9878913760 0.9884446859 0.9878913760 - 0.9882211089 0.9887502193 0.9882211089 - 0.9885507226 0.9890555739 0.9885507226 - 0.9888803959 0.9893611073 0.9888803959 - 0.9892101288 0.9896665215 0.9892101288 - 0.9895396829 0.9899719954 0.9895396829 - 0.9898694158 0.9902774096 0.9898694158 - 0.9901990294 0.9905828834 0.9901990294 - 0.9905248284 0.9908854961 0.9905248284 - 0.9908407927 0.9911810160 0.9908407927 - 0.9911568165 0.9914764166 0.9911568165 - 0.9914727807 0.9917718768 0.9914727807 - 0.9917888045 0.9920672774 0.9917888045 - 0.9921048284 0.9923627973 0.9921048284 - 0.9924207926 0.9926583171 0.9924207926 - 0.9927368164 0.9929537177 0.9927368164 - 0.9930515885 0.9932484031 0.9930515885 - 0.9933490753 0.9935321808 0.9933490753 - 0.9936466813 0.9938160181 0.9936466813 - 0.9939442873 0.9940996766 0.9939442873 - 0.9942418933 0.9943835139 0.9942418933 - 0.9945394993 0.9946672916 0.9945394993 - 0.9948371053 0.9949511290 0.9948371053 - 0.9951347113 0.9952347875 0.9951347113 - 0.9954321980 0.9955186248 0.9954321980 - 0.9957144260 0.9957929850 0.9957144260 - 0.9959921837 0.9960647821 0.9959921837 - 0.9962701201 0.9963365793 0.9962701201 - 0.9965478778 0.9966083765 0.9965478778 - 0.9968256950 0.9968801737 0.9968256950 - 0.9971035719 0.9971519709 0.9971035719 - 0.9973813891 0.9974238276 0.9973813891 - 0.9976593256 0.9976956248 0.9976593256 - 0.9979289770 0.9979603291 0.9979289770 - 0.9981878996 0.9982151985 0.9981878996 - 0.9984468222 0.9984701872 0.9984468222 - 0.9987056255 0.9987251759 0.9987056255 - 0.9989644885 0.9989801049 0.9989644885 - 0.9992234111 0.9992350936 0.9992234111 - 0.9994822741 0.9994900823 0.9994822741 - 0.9997410774 0.9997450113 0.9997410774 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0071816309 0.0082409112 0.0071816309 + 0.0151500301 0.0168459192 0.0151500301 + 0.0224017091 0.0247517005 0.0224017091 + 0.0290685501 0.0319880284 0.0290685501 + 0.0352958813 0.0386751406 0.0352958813 + 0.0411669388 0.0448977016 0.0411669388 + 0.0467281416 0.0507521592 0.0467281416 + 0.0520455502 0.0562780313 0.0520455502 + 0.0571411215 0.0615147501 0.0571411215 + 0.0620320588 0.0665091425 0.0620320588 + 0.0667356402 0.0713089630 0.0667356402 + 0.0712965578 0.0759803429 0.0712965578 + 0.0757385418 0.0805422887 0.0757385418 + 0.0800764337 0.0850213692 0.0800764337 + 0.0843147188 0.0894245878 0.0843147188 + 0.0884548426 0.0937559828 0.0884548426 + 0.0925132483 0.0980221704 0.0925132483 + 0.0964898691 0.1022289991 0.0964898691 + 0.1003945023 0.1063620001 0.1003945023 + 0.1042160019 0.1104390025 0.1042160019 + 0.1079747006 0.1144633964 0.1079747006 + 0.1116577014 0.1184230000 0.1116577014 + 0.1152791977 0.1223274022 0.1152791977 + 0.1188424006 0.1261754930 0.1188424006 + 0.1223476008 0.1299650073 0.1223476008 + 0.1257933974 0.1337056011 0.1257933974 + 0.1291857064 0.1373963058 0.1291857064 + 0.1325111985 0.1410335004 0.1325111985 + 0.1357868016 0.1446186006 0.1357868016 + 0.1390141994 0.1481585056 0.1390141994 + 0.1421941966 0.1516450942 0.1421941966 + 0.1453281939 0.1550758928 0.1453281939 + 0.1484290957 0.1584609002 0.1484290957 + 0.1514863074 0.1617927998 0.1514863074 + 0.1545145065 0.1650730968 0.1545145065 + 0.1575087011 0.1683045030 0.1575087011 + 0.1604654938 0.1714911014 0.1604654938 + 0.1633878052 0.1746436059 0.1633878052 + 0.1662711948 0.1777534932 0.1662711948 + 0.1691236049 0.1808203012 0.1691236049 + 0.1719461977 0.1838431954 0.1719461977 + 0.1747328043 0.1868294030 0.1747328043 + 0.1774906069 0.1897847056 0.1774906069 + 0.1802141964 0.1927071065 0.1802141964 + 0.1829040051 0.1955924034 0.1829040051 + 0.1855681986 0.1984508038 0.1855681986 + 0.1881995946 0.2012735009 0.1881995946 + 0.1908040047 0.2040608972 0.1908040047 + 0.1933835000 0.2068151981 0.1933835000 + 0.1959407926 0.2095429003 0.1959407926 + 0.1984723061 0.2122360021 0.1984723061 + 0.2009814978 0.2149036974 0.2009814978 + 0.2034648061 0.2175482064 0.2034648061 + 0.2059251070 0.2201592028 0.2059251070 + 0.2083660066 0.2227438986 0.2083660066 + 0.2107862979 0.2253006995 0.2107862979 + 0.2131928056 0.2278293073 0.2131928056 + 0.2155797929 0.2303356975 0.2155797929 + 0.2179521024 0.2328161001 0.2179521024 + 0.2203088999 0.2352697998 0.2203088999 + 0.2226427943 0.2377053052 0.2226427943 + 0.2249636948 0.2401220948 0.2249636948 + 0.2272658050 0.2425125986 0.2272658050 + 0.2295507044 0.2448851019 0.2295507044 + 0.2318232954 0.2472407967 0.2318232954 + 0.2340773046 0.2495730072 0.2340773046 + 0.2363186032 0.2518830895 0.2363186032 + 0.2385478020 0.2541756034 0.2385478020 + 0.2407571971 0.2564486861 0.2407571971 + 0.2429528981 0.2587057948 0.2429528981 + 0.2451367974 0.2609438896 0.2451367974 + 0.2473030984 0.2631632090 0.2473030984 + 0.2494540960 0.2653681040 0.2494540960 + 0.2515927851 0.2675603926 0.2515927851 + 0.2537161112 0.2697342932 0.2537161112 + 0.2558267117 0.2718896866 0.2558267117 + 0.2579267025 0.2740308046 0.2579267025 + 0.2600139081 0.2761551142 0.2600139081 + 0.2620905042 0.2782635987 0.2620905042 + 0.2641522884 0.2803570926 0.2641522884 + 0.2661997080 0.2824395895 0.2661997080 + 0.2682358921 0.2845089138 0.2682358921 + 0.2702636123 0.2865653038 0.2702636123 + 0.2722778916 0.2886031866 0.2722778916 + 0.2742806971 0.2906292975 0.2742806971 + 0.2762736082 0.2926450074 0.2762736082 + 0.2782565951 0.2946475148 0.2782565951 + 0.2802278996 0.2966375053 0.2802278996 + 0.2821829021 0.2986125946 0.2821829021 + 0.2841286063 0.3005768061 0.2841286063 + 0.2860676050 0.3025309145 0.2860676050 + 0.2879948914 0.3044739962 0.2879948914 + 0.2899109125 0.3064059913 0.2899109125 + 0.2918199003 0.3083246052 0.2918199003 + 0.2937203050 0.3102298975 0.2937203050 + 0.2956117988 0.3121252954 0.2956117988 + 0.2974927127 0.3140129149 0.2974927127 + 0.2993623018 0.3158906996 0.2993623018 + 0.3012244105 0.3177559078 0.3012244105 + 0.3030805886 0.3196083009 0.3030805886 + 0.3049291968 0.3214524984 0.3049291968 + 0.3067674041 0.3232868016 0.3067674041 + 0.3085950911 0.3251119852 0.3085950911 + 0.3104135990 0.3269273937 0.3104135990 + 0.3122237027 0.3287312090 0.3122237027 + 0.3140270114 0.3305267096 0.3140270114 + 0.3158223927 0.3323149085 0.3158223927 + 0.3176110089 0.3340944946 0.3176110089 + 0.3193928003 0.3358643055 0.3193928003 + 0.3211658001 0.3376250863 0.3211658001 + 0.3229303062 0.3393788934 0.3229303062 + 0.3246853054 0.3411253095 0.3246853054 + 0.3264288902 0.3428628147 0.3264288902 + 0.3281643093 0.3445931077 0.3281643093 + 0.3298931122 0.3463181853 0.3298931122 + 0.3316138089 0.3480368853 0.3316138089 + 0.3333249986 0.3497483134 0.3333249986 + 0.3350282013 0.3514511883 0.3350282013 + 0.3367244899 0.3531453907 0.3367244899 + 0.3384152055 0.3548327088 0.3384152055 + 0.3401009142 0.3565143049 0.3401009142 + 0.3417800069 0.3581889868 0.3417800069 + 0.3434512019 0.3598566949 0.3434512019 + 0.3451134861 0.3615188897 0.3451134861 + 0.3467679918 0.3631770015 0.3467679918 + 0.3484159112 0.3648299873 0.3484159112 + 0.3500581980 0.3664768934 0.3500581980 + 0.3516958952 0.3681172132 0.3516958952 + 0.3533272147 0.3697521985 0.3533272147 + 0.3549514115 0.3713811040 0.3549514115 + 0.3565692902 0.3730033040 0.3565692902 + 0.3581799865 0.3746202886 0.3581799865 + 0.3597826958 0.3762314916 0.3597826958 + 0.3613792062 0.3778361976 0.3613792062 + 0.3629701138 0.3794350028 0.3629701138 + 0.3645544946 0.3810291886 0.3645544946 + 0.3661319017 0.3826195002 0.3661319017 + 0.3677032888 0.3842051923 0.3677032888 + 0.3692691922 0.3857854009 0.3692691922 + 0.3708305955 0.3873588145 0.3708305955 + 0.3723872006 0.3889274001 0.3723872006 + 0.3739395142 0.3904910982 0.3739395142 + 0.3754872978 0.3920491040 0.3754872978 + 0.3770299852 0.3936016858 0.3770299852 + 0.3785668015 0.3951497972 0.3785668015 + 0.3800952137 0.3966915905 0.3800952137 + 0.3816176057 0.3982270956 0.3816176057 + 0.3831363916 0.3997577131 0.3831363916 + 0.3846518993 0.4012834132 0.3846518993 + 0.3861635923 0.4028038085 0.3861635923 + 0.3876700103 0.4043180048 0.3876700103 + 0.3891699910 0.4058266878 0.3891699910 + 0.3906654119 0.4073303044 0.3906654119 + 0.3921560049 0.4088295996 0.3921560049 + 0.3936414123 0.4103249907 0.3936414123 + 0.3951213062 0.4118149877 0.3951213062 + 0.3965955079 0.4132992923 0.3965955079 + 0.3980641961 0.4147784114 0.3980641961 + 0.3995279074 0.4162524939 0.3995279074 + 0.4009875953 0.4177218080 0.4009875953 + 0.4024423063 0.4191871881 0.4024423063 + 0.4038924873 0.4206492007 0.4038924873 + 0.4053378999 0.4221076965 0.4053378999 + 0.4067766070 0.4235613942 0.4067766070 + 0.4082114995 0.4250093997 0.4082114995 + 0.4096431136 0.4264526963 0.4096431136 + 0.4110710919 0.4278913140 0.4110710919 + 0.4124928117 0.4293223023 0.4124928117 + 0.4139108062 0.4307503104 0.4139108062 + 0.4153253138 0.4321753085 0.4153253138 + 0.4167349935 0.4335967898 0.4167349935 + 0.4181382954 0.4350129068 0.4181382954 + 0.4195368886 0.4364252090 0.4195368886 + 0.4209308028 0.4378337860 0.4209308028 + 0.4223197103 0.4392373860 0.4223197103 + 0.4237053990 0.4406375885 0.4237053990 + 0.4250878096 0.4420346022 0.4250878096 + 0.4264639914 0.4434260130 0.4264639914 + 0.4278368056 0.4448128045 0.4278368056 + 0.4292069972 0.4461959004 0.4292069972 + 0.4305736125 0.4475724995 0.4305736125 + 0.4319368899 0.4489449859 0.4319368899 + 0.4332974851 0.4503144026 0.4332974851 + 0.4346528947 0.4516780972 0.4346528947 + 0.4360040128 0.4530383050 0.4360040128 + 0.4373517036 0.4543960094 0.4373517036 + 0.4386920035 0.4557482898 0.4386920035 + 0.4400289059 0.4570981860 0.4400289059 + 0.4413625896 0.4584460855 0.4413625896 + 0.4426892102 0.4597887993 0.4426892102 + 0.4440130889 0.4611299038 0.4440130889 + 0.4453332126 0.4624690115 0.4453332126 + 0.4466494024 0.4638045132 0.4466494024 + 0.4479633868 0.4651381075 0.4479633868 + 0.4492729008 0.4664680064 0.4492729008 + 0.4505785108 0.4677945077 0.4505785108 + 0.4518812895 0.4691188931 0.4518812895 + 0.4531790912 0.4704371989 0.4531790912 + 0.4544754028 0.4717530906 0.4544754028 + 0.4557689130 0.4730646908 0.4557689130 + 0.4570592046 0.4743711948 0.4570592046 + 0.4583478868 0.4756751955 0.4583478868 + 0.4596310854 0.4769738019 0.4596310854 + 0.4609121978 0.4782701135 0.4609121978 + 0.4621888995 0.4795624018 0.4621888995 + 0.4634611011 0.4808515906 0.4634611011 + 0.4647307098 0.4821392894 0.4647307098 + 0.4659950137 0.4834218919 0.4659950137 + 0.4672574103 0.4847030044 0.4672574103 + 0.4685145915 0.4859794974 0.4685145915 + 0.4697686136 0.4872528017 0.4697686136 + 0.4710184932 0.4885227978 0.4710184932 + 0.4722628891 0.4897891879 0.4722628891 + 0.4735049009 0.4910542071 0.4735049009 + 0.4747408926 0.4923141003 0.4747408926 + 0.4759756923 0.4935730100 0.4759756923 + 0.4772061110 0.4948287904 0.4772061110 + 0.4784345031 0.4960829020 0.4784345031 + 0.4796591997 0.4973343015 0.4796591997 + 0.4808802903 0.4985828102 0.4808802903 + 0.4820986092 0.4998292029 0.4820986092 + 0.4833118021 0.5010713935 0.4833118021 + 0.4845229983 0.5023121834 0.4845229983 + 0.4857310057 0.5035473704 0.4857310057 + 0.4869383872 0.5047817826 0.4869383872 + 0.4881415069 0.5060117245 0.4881415069 + 0.4893437922 0.5072407722 0.4893437922 + 0.4905417860 0.5084657073 0.4905417860 + 0.4917384088 0.5096889138 0.4917384088 + 0.4929305911 0.5109081864 0.4929305911 + 0.4941214025 0.5121250749 0.4941214025 + 0.4953092933 0.5133383274 0.4953092933 + 0.4964950979 0.5145484209 0.4964950979 + 0.4976781011 0.5157549977 0.4976781011 + 0.4988583922 0.5169578195 0.4988583922 + 0.5000360012 0.5181573033 0.5000360012 + 0.5012102127 0.5193536878 0.5012102127 + 0.5023818016 0.5205478072 0.5023818016 + 0.5035495758 0.5217382908 0.5035495758 + 0.5047146082 0.5229266286 0.5047146082 + 0.5058754086 0.5241109133 0.5058754086 + 0.5070334077 0.5252929926 0.5070334077 + 0.5081868768 0.5264708996 0.5081868768 + 0.5093373060 0.5276464224 0.5093373060 + 0.5104840994 0.5288175941 0.5104840994 + 0.5116283298 0.5299862027 0.5116283298 + 0.5127689242 0.5311505795 0.5127689242 + 0.5139064193 0.5323119164 0.5139064193 + 0.5150403976 0.5334690809 0.5150403976 + 0.5161709189 0.5346227884 0.5161709189 + 0.5172979832 0.5357729197 0.5172979832 + 0.5184208751 0.5369185805 0.5184208751 + 0.5195422769 0.5380612016 0.5195422769 + 0.5206609964 0.5391985774 0.5206609964 + 0.5217785239 0.5403342843 0.5217785239 + 0.5228924751 0.5414649844 0.5228924751 + 0.5240058899 0.5425949097 0.5240058899 + 0.5251148939 0.5437206030 0.5251148939 + 0.5262238979 0.5448461771 0.5262238979 + 0.5273274183 0.5459663868 0.5273274183 + 0.5284308791 0.5470865965 0.5284308791 + 0.5295295119 0.5482017994 0.5295295119 + 0.5306270719 0.5493162274 0.5306270719 + 0.5317205191 0.5504277945 0.5317205191 + 0.5328117013 0.5515381098 0.5328117013 + 0.5339006782 0.5526462197 0.5339006782 + 0.5349869728 0.5537520051 0.5349869728 + 0.5360723734 0.5548564792 0.5360723734 + 0.5371550918 0.5559577942 0.5371550918 + 0.5382375717 0.5570588112 0.5382375717 + 0.5393161774 0.5581569076 0.5393161774 + 0.5403949022 0.5592550039 0.5403949022 + 0.5414698720 0.5603502989 0.5414698720 + 0.5425440073 0.5614449978 0.5425440073 + 0.5436155200 0.5625376105 0.5436155200 + 0.5446845889 0.5636284947 0.5446845889 + 0.5457528830 0.5647183061 0.5457528830 + 0.5468186140 0.5658050179 0.5468186140 + 0.5478842854 0.5668917894 0.5478842854 + 0.5489463806 0.5679742098 0.5489463806 + 0.5500082970 0.5690563917 0.5500082970 + 0.5510675907 0.5701354742 0.5510675907 + 0.5521252751 0.5712130070 0.5521252751 + 0.5531818271 0.5722888112 0.5531818271 + 0.5542349815 0.5733609796 0.5542349815 + 0.5552883148 0.5744333267 0.5552883148 + 0.5563371181 0.5755001903 0.5563371181 + 0.5573856235 0.5765669942 0.5573856235 + 0.5584312081 0.5776314139 0.5584312081 + 0.5594745278 0.5786941051 0.5594745278 + 0.5605170131 0.5797560811 0.5605170131 + 0.5615549088 0.5808146000 0.5615549088 + 0.5625928044 0.5818731189 0.5625928044 + 0.5636265278 0.5829282999 0.5636265278 + 0.5646585822 0.5839822888 0.5646585822 + 0.5656889081 0.5850347877 0.5656889081 + 0.5667148829 0.5860841274 0.5667148829 + 0.5677409768 0.5871334076 0.5677409768 + 0.5687615871 0.5881785154 0.5687615871 + 0.5697811842 0.5892227888 0.5697811842 + 0.5707985163 0.5902652144 0.5707985163 + 0.5718113780 0.5913044214 0.5718113780 + 0.5728244185 0.5923436284 0.5728244185 + 0.5738328099 0.5933796167 0.5738328099 + 0.5748404264 0.5944151878 0.5748404264 + 0.5758461952 0.5954493880 0.5758461952 + 0.5768483877 0.5964813232 0.5768483877 + 0.5778505206 0.5975130796 0.5778505206 + 0.5788481236 0.5985416770 0.5788481236 + 0.5798444748 0.5995696187 0.5798444748 + 0.5808395147 0.6005964875 0.5808395147 + 0.5818299055 0.6016203165 0.5818299055 + 0.5828202963 0.6026440859 0.5828202963 + 0.5838078260 0.6036651134 0.5838078260 + 0.5847936869 0.6046845913 0.5847936869 + 0.5857794881 0.6057038903 0.5857794881 + 0.5867608190 0.6067190170 0.5867608190 + 0.5877420902 0.6077340841 0.5877420902 + 0.5887213945 0.6087473035 0.5887213945 + 0.5896978974 0.6097577810 0.5896978974 + 0.5906744003 0.6107683182 0.5906744003 + 0.5916472077 0.6117751002 0.5916472077 + 0.5926188231 0.6127809286 0.5926188231 + 0.5935899019 0.6137859821 0.5935899019 + 0.5945563912 0.6147853136 0.5945563912 + 0.5955228806 0.6157847047 0.5955228806 + 0.5964874029 0.6167812943 0.5964874029 + 0.5974487066 0.6177741289 0.5974487066 + 0.5984100103 0.6187667847 0.5984100103 + 0.5993676782 0.6197549105 0.5993676782 + 0.6003236771 0.6207411289 0.6003236771 + 0.6012796760 0.6217272878 0.6012796760 + 0.6022304893 0.6227068901 0.6022304893 + 0.6031810045 0.6236863136 0.6031810045 + 0.6041305065 0.6246638894 0.6041305065 + 0.6050754786 0.6256346703 0.6050754786 + 0.6060203910 0.6266055107 0.6060203910 + 0.6069629788 0.6275725961 0.6069629788 + 0.6079022884 0.6285349131 0.6079022884 + 0.6088414788 0.6294971108 0.6088414788 + 0.6097772717 0.6304540038 0.6097772717 + 0.6107106805 0.6314077973 0.6107106805 + 0.6116440892 0.6323614717 0.6116440892 + 0.6125729084 0.6333085895 0.6125729084 + 0.6135004163 0.6342539191 0.6135004163 + 0.6144278049 0.6351991892 0.6144278049 + 0.6153497100 0.6361367702 0.6153497100 + 0.6162710786 0.6370738149 0.6162710786 + 0.6171923280 0.6380103230 0.6171923280 + 0.6181074977 0.6389390826 0.6181074977 + 0.6190227270 0.6398677826 0.6190227270 + 0.6199370027 0.6407952905 0.6199370027 + 0.6208459139 0.6417160034 0.6208459139 + 0.6217548251 0.6426365972 0.6217548251 + 0.6226627231 0.6435554028 0.6226627231 + 0.6235666871 0.6444680095 0.6235666871 + 0.6244707108 0.6453806162 0.6244707108 + 0.6253734231 0.6462908983 0.6253734231 + 0.6262723804 0.6471955180 0.6262723804 + 0.6271713972 0.6481000781 0.6271713972 + 0.6280689836 0.6490027905 0.6280689836 + 0.6289629936 0.6499009728 0.6289629936 + 0.6298570037 0.6507992148 0.6298570037 + 0.6307495236 0.6516954899 0.6307495236 + 0.6316384077 0.6525874138 0.6316384077 + 0.6325272918 0.6534792781 0.6325272918 + 0.6334148049 0.6543694139 0.6334148049 + 0.6342985034 0.6552550793 0.6342985034 + 0.6351823211 0.6561406851 0.6351823211 + 0.6360648274 0.6570249200 0.6360648274 + 0.6369432807 0.6579043865 0.6369432807 + 0.6378217936 0.6587839127 0.6378217936 + 0.6386997104 0.6596624255 0.6386997104 + 0.6395745873 0.6605358124 0.6395745873 + 0.6404494047 0.6614091992 0.6404494047 + 0.6413241029 0.6622822285 0.6413241029 + 0.6421937943 0.6631495953 0.6421937943 + 0.6430636048 0.6640170217 0.6430636048 + 0.6439334154 0.6648843884 0.6439334154 + 0.6447985768 0.6657463908 0.6447985768 + 0.6456633806 0.6666080952 0.6456633806 + 0.6465281844 0.6674697995 0.6465281844 + 0.6473892927 0.6683270931 0.6473892927 + 0.6482492089 0.6691833138 0.6482492089 + 0.6491091251 0.6700394750 0.6491091251 + 0.6499661207 0.6708925962 0.6499661207 + 0.6508212090 0.6717435718 0.6508212090 + 0.6516762972 0.6725946069 0.6516762972 + 0.6525288820 0.6734434962 0.6525288820 + 0.6533777118 0.6742895246 0.6533777118 + 0.6542264819 0.6751354933 0.6542264819 + 0.6550742984 0.6759806871 0.6550742984 + 0.6559169888 0.6768220067 0.6559169888 + 0.6567596793 0.6776633263 0.6567596793 + 0.6576023102 0.6785045862 0.6576023102 + 0.6584395766 0.6793416739 0.6584395766 + 0.6592763066 0.6801785231 0.6592763066 + 0.6601129770 0.6810153127 0.6601129770 + 0.6609460711 0.6818485260 0.6609460711 + 0.6617770791 0.6826794744 0.6617770791 + 0.6626080275 0.6835106015 0.6626080275 + 0.6634371877 0.6843398213 0.6634371877 + 0.6642624736 0.6851654053 0.6642624736 + 0.6650878787 0.6859909892 0.6650878787 + 0.6659132242 0.6868165731 0.6659132242 + 0.6667333245 0.6876372099 0.6667333245 + 0.6675531864 0.6884577274 0.6675531864 + 0.6683731079 0.6892783046 0.6683731079 + 0.6691898704 0.6900957823 0.6691898704 + 0.6700046062 0.6909114718 0.6700046062 + 0.6708192825 0.6917272210 0.6708192825 + 0.6716328859 0.6925418973 0.6716328859 + 0.6724424958 0.6933531761 0.6724424958 + 0.6732522249 0.6941645145 0.6732522249 + 0.6740617752 0.6949757934 0.6740617752 + 0.6748675704 0.6957837939 0.6748675704 + 0.6756724119 0.6965909004 0.6756724119 + 0.6764771938 0.6973981261 0.6764771938 + 0.6772803068 0.6982039213 0.6772803068 + 0.6780803800 0.6990072131 0.6780803800 + 0.6788805127 0.6998106241 0.6788805127 + 0.6796805263 0.7006139159 0.6796805263 + 0.6804767251 0.7014129758 0.6804767251 + 0.6812722087 0.7022113800 0.6812722087 + 0.6820678115 0.7030097842 0.6820678115 + 0.6828618050 0.7038065195 0.6828618050 + 0.6836528778 0.7046002746 0.6836528778 + 0.6844441295 0.7053942084 0.6844441295 + 0.6852353215 0.7061880827 0.6852353215 + 0.6860229969 0.7069783807 0.6860229969 + 0.6868100166 0.7077680826 0.6868100166 + 0.6875969768 0.7085577846 0.6875969768 + 0.6883826256 0.7093464136 0.6883826256 + 0.6891641021 0.7101323009 0.6891641021 + 0.6899456978 0.7109181881 0.6899456978 + 0.6907271743 0.7117040753 0.6907271743 + 0.6915053725 0.7124866843 0.6915053725 + 0.6922817230 0.7132676244 0.6922817230 + 0.6930580139 0.7140485048 0.6930580139 + 0.6938341856 0.7148293257 0.6938341856 + 0.6946055293 0.7156057954 0.6946055293 + 0.6953768134 0.7163822055 0.6953768134 + 0.6961480975 0.7171586752 0.6961480975 + 0.6969177127 0.7179335952 0.6969177127 + 0.6976842284 0.7187060714 0.6976842284 + 0.6984508038 0.7194784880 0.6984508038 + 0.6992173195 0.7202510238 0.6992173195 + 0.6999806762 0.7210199833 0.6999806762 + 0.7007427216 0.7217875719 0.7007427216 + 0.7015047073 0.7225551009 0.7015047073 + 0.7022666931 0.7233226895 0.7022666931 + 0.7030245066 0.7240859270 0.7030245066 + 0.7037820816 0.7248489857 0.7037820816 + 0.7045397162 0.7256121039 0.7045397162 + 0.7052962184 0.7263739109 0.7052962184 + 0.7060483098 0.7271317244 0.7060483098 + 0.7068005204 0.7278894186 0.7068005204 + 0.7075526118 0.7286471128 0.7075526118 + 0.7083024979 0.7294024825 0.7083024979 + 0.7090494037 0.7301552892 0.7090494037 + 0.7097964287 0.7309080958 0.7097964287 + 0.7105432749 0.7316607833 0.7105432749 + 0.7112872005 0.7324107289 0.7112872005 + 0.7120292187 0.7331590056 0.7120292187 + 0.7127712965 0.7339072227 0.7127712965 + 0.7135133147 0.7346554995 0.7135133147 + 0.7142516971 0.7354004979 0.7142516971 + 0.7149891853 0.7361447215 0.7149891853 + 0.7157266140 0.7368888855 0.7157266140 + 0.7164639831 0.7376331091 0.7164639831 + 0.7171975970 0.7383713126 0.7171975970 + 0.7179307938 0.7391089201 0.7179307938 + 0.7186638713 0.7398465872 0.7186638713 + 0.7193971276 0.7405840755 0.7193971276 + 0.7201262116 0.7413142920 0.7201262116 + 0.7208554149 0.7420445085 0.7208554149 + 0.7215844989 0.7427747846 0.7215844989 + 0.7223134041 0.7435045242 0.7223134041 + 0.7230387926 0.7442278266 0.7230387926 + 0.7237643003 0.7449511290 0.7237643003 + 0.7244896889 0.7456744909 0.7244896889 + 0.7252147198 0.7463970780 0.7252147198 + 0.7259353995 0.7471140027 0.7259353995 + 0.7266560793 0.7478309870 0.7266560793 + 0.7273768187 0.7485479116 0.7273768187 + 0.7280970812 0.7492641807 0.7280970812 + 0.7288134098 0.7499753237 0.7288134098 + 0.7295296788 0.7506862879 0.7295296788 + 0.7302460074 0.7513973117 0.7302460074 + 0.7309620976 0.7521079779 0.7309620976 + 0.7316743135 0.7528135777 0.7316743135 + 0.7323865294 0.7535192966 0.7323865294 + 0.7330986857 0.7542248964 0.7330986857 + 0.7338109016 0.7549306154 0.7338109016 + 0.7345194817 0.7556313276 0.7345194817 + 0.7352281213 0.7563319802 0.7352281213 + 0.7359365821 0.7570326924 0.7359365821 + 0.7366451025 0.7577335238 0.7366451025 + 0.7373495102 0.7584289908 0.7373495102 + 0.7380533218 0.7591239214 0.7380533218 + 0.7387570739 0.7598189116 0.7387570739 + 0.7394608855 0.7605137825 0.7394608855 + 0.7401614785 0.7612038255 0.7401614785 + 0.7408609986 0.7618921995 0.7408609986 + 0.7415605187 0.7625805736 0.7415605187 + 0.7422599792 0.7632690072 0.7422599792 + 0.7429572940 0.7639539838 0.7429572940 + 0.7436528802 0.7646365166 0.7436528802 + 0.7443485260 0.7653189898 0.7443485260 + 0.7450441122 0.7660015225 0.7450441122 + 0.7457385063 0.7666820288 0.7457385063 + 0.7464305758 0.7673591971 0.7464305758 + 0.7471227050 0.7680364251 0.7471227050 + 0.7478148937 0.7687135935 0.7478148937 + 0.7485067248 0.7693902850 0.7485067248 + 0.7491956949 0.7700628042 0.7491956949 + 0.7498847842 0.7707353830 0.7498847842 + 0.7505738735 0.7714080215 0.7505738735 + 0.7512630224 0.7720804811 0.7512630224 + 0.7519487739 0.7727484703 0.7519487739 + 0.7526339889 0.7734156251 0.7526339889 + 0.7533192039 0.7740827203 0.7533192039 + 0.7540044188 0.7747498751 0.7540044188 + 0.7546877861 0.7754138112 0.7546877861 + 0.7553694844 0.7760748863 0.7553694844 + 0.7560511827 0.7767360806 0.7560511827 + 0.7567330003 0.7773972154 0.7567330003 + 0.7574139237 0.7780573964 0.7574139237 + 0.7580912709 0.7787132263 0.7580912709 + 0.7587686777 0.7793691158 0.7587686777 + 0.7594460845 0.7800250053 0.7594460845 + 0.7601234913 0.7806808949 0.7601234913 + 0.7607979178 0.7813320756 0.7607979178 + 0.7614712715 0.7819821835 0.7614712715 + 0.7621448040 0.7826321721 0.7621448040 + 0.7628182769 0.7832822800 0.7628182769 + 0.7634900212 0.7839304209 0.7634900212 + 0.7641587257 0.7845754027 0.7641587257 + 0.7648273110 0.7852203250 0.7648273110 + 0.7654958963 0.7858651876 0.7654958963 + 0.7661646008 0.7865102291 0.7661646008 + 0.7668293118 0.7871509790 0.7668293118 + 0.7674936056 0.7877914906 0.7674936056 + 0.7681578994 0.7884320021 0.7681578994 + 0.7688220739 0.7890725136 0.7688220739 + 0.7694842815 0.7897108197 0.7694842815 + 0.7701433897 0.7903463244 0.7701433897 + 0.7708023787 0.7909818292 0.7708023787 + 0.7714614868 0.7916172743 0.7714614868 + 0.7721205950 0.7922527194 0.7721205950 + 0.7727754116 0.7928832173 0.7727754116 + 0.7734296918 0.7935130000 0.7734296918 + 0.7740839720 0.7941429019 0.7740839720 + 0.7747381926 0.7947726846 0.7747381926 + 0.7753911018 0.7954009771 0.7753911018 + 0.7760409713 0.7960259914 0.7760409713 + 0.7766909003 0.7966510057 0.7766909003 + 0.7773407102 0.7972760201 0.7773407102 + 0.7779905796 0.7979009748 0.7779905796 + 0.7786368132 0.7985221744 0.7786368132 + 0.7792813778 0.7991417050 0.7792813778 + 0.7799261212 0.7997612953 0.7799261212 + 0.7805706859 0.8003808856 0.7805706859 + 0.7812150717 0.8010001779 0.7812150717 + 0.7818549275 0.8016149998 0.7818549275 + 0.7824947834 0.8022298813 0.7824947834 + 0.7831345797 0.8028448224 0.7831345797 + 0.7837744951 0.8034597039 0.7837744951 + 0.7844126821 0.8040724993 0.7844126821 + 0.7850481868 0.8046821952 0.7850481868 + 0.7856836915 0.8052918911 0.7856836915 + 0.7863191962 0.8059015274 0.7863191962 + 0.7869547009 0.8065112233 0.7869547009 + 0.7875875831 0.8071171045 0.7875875831 + 0.7882192135 0.8077210188 0.7882192135 + 0.7888507247 0.8083249927 0.7888507247 + 0.7894822955 0.8089289069 0.7894822955 + 0.7901139259 0.8095328808 0.7901139259 + 0.7907413244 0.8101323843 0.7907413244 + 0.7913681269 0.8107314110 0.7913681269 + 0.7919949889 0.8113303781 0.7919949889 + 0.7926219106 0.8119294047 0.7926219106 + 0.7932484150 0.8125280738 0.7932484150 + 0.7938709855 0.8131231070 0.7938709855 + 0.7944936752 0.8137180209 0.7944936752 + 0.7951163054 0.8143129945 0.7951163054 + 0.7957389951 0.8149080276 0.7957389951 + 0.7963607907 0.8155021071 0.7963607907 + 0.7969797254 0.8160938025 0.7969797254 + 0.7975987196 0.8166856170 0.7975987196 + 0.7982177138 0.8172773123 0.7982177138 + 0.7988365889 0.8178690076 0.7988365889 + 0.7994540930 0.8184589148 0.7994540930 + 0.8000686765 0.8190457821 0.8000686765 + 0.8006833196 0.8196325898 0.8006833196 + 0.8012979031 0.8202195168 0.8012979031 + 0.8019124866 0.8208063245 0.8019124866 + 0.8025255203 0.8213908076 0.8025255203 + 0.8031362891 0.8219724298 0.8031362891 + 0.8037471175 0.8225538731 0.8037471175 + 0.8043578863 0.8231354952 0.8043578863 + 0.8049687147 0.8237169981 0.8049687147 + 0.8055779934 0.8242964149 0.8055779934 + 0.8061854839 0.8248735070 0.8061854839 + 0.8067929745 0.8254505992 0.8067929745 + 0.8074005842 0.8260276914 0.8074005842 + 0.8080080748 0.8266047835 0.8080080748 + 0.8086137772 0.8271802068 0.8086137772 + 0.8092173934 0.8277537823 0.8092173934 + 0.8098208904 0.8283274174 0.8098208904 + 0.8104245067 0.8289008737 0.8104245067 + 0.8110281229 0.8294745088 0.8110281229 + 0.8116297722 0.8300458193 0.8116297722 + 0.8122286797 0.8306140900 0.8122286797 + 0.8128275871 0.8311824203 0.8128275871 + 0.8134266138 0.8317506909 0.8134266138 + 0.8140255213 0.8323189020 0.8140255213 + 0.8146228194 0.8328853846 0.8146228194 + 0.8152164817 0.8334481120 0.8152164817 + 0.8158102036 0.8340108991 0.8158102036 + 0.8164039254 0.8345736265 0.8164039254 + 0.8169975877 0.8351362944 0.8169975877 + 0.8175904751 0.8356981277 0.8175904751 + 0.8181796074 0.8362563252 0.8181796074 + 0.8187686801 0.8368144035 0.8187686801 + 0.8193578124 0.8373724818 0.8193578124 + 0.8199468851 0.8379306793 0.8199468851 + 0.8205358982 0.8384886980 0.8205358982 + 0.8211209774 0.8390418887 0.8211209774 + 0.8217059970 0.8395950794 0.8217059970 + 0.8222910762 0.8401483893 0.8222910762 + 0.8228762150 0.8407015800 0.8228762150 + 0.8234611750 0.8412547708 0.8234611750 + 0.8240435719 0.8418037891 0.8240435719 + 0.8246251941 0.8423519135 0.8246251941 + 0.8252068162 0.8428999782 0.8252068162 + 0.8257884979 0.8434481025 0.8257884979 + 0.8263701200 0.8439962268 0.8263701200 + 0.8269495964 0.8445411921 0.8269495964 + 0.8275271058 0.8450837731 0.8275271058 + 0.8281046152 0.8456264138 0.8281046152 + 0.8286821842 0.8461689949 0.8286821842 + 0.8292596936 0.8467115760 0.8292596936 + 0.8298364282 0.8472530246 0.8298364282 + 0.8304104209 0.8477910757 0.8304104209 + 0.8309844136 0.8483291268 0.8309844136 + 0.8315584064 0.8488671780 0.8315584064 + 0.8321323991 0.8494052291 0.8321323991 + 0.8327063918 0.8499432802 0.8327063918 + 0.8332766891 0.8504770994 0.8332766891 + 0.8338465095 0.8510105014 0.8338465095 + 0.8344163895 0.8515439034 0.8344163895 + 0.8349862099 0.8520773053 0.8349862099 + 0.8355559707 0.8526107073 0.8355559707 + 0.8361235261 0.8531416059 0.8361235261 + 0.8366885781 0.8536701202 0.8366885781 + 0.8372538090 0.8541985750 0.8372538090 + 0.8378189206 0.8547270894 0.8378189206 + 0.8383839726 0.8552556038 0.8383839726 + 0.8389489055 0.8557837009 0.8389489055 + 0.8395100236 0.8563070893 0.8395100236 + 0.8400710821 0.8568304777 0.8400710821 + 0.8406322002 0.8573539257 0.8406322002 + 0.8411933184 0.8578773141 0.8411933184 + 0.8417543173 0.8584007025 0.8417543173 + 0.8423134089 0.8589209914 0.8423134089 + 0.8428711295 0.8594391942 0.8428711295 + 0.8434287906 0.8599573970 0.8434287906 + 0.8439865112 0.8604755998 0.8439865112 + 0.8445441127 0.8609936833 0.8445441127 + 0.8451015949 0.8615115881 0.8451015949 + 0.8456565142 0.8620254993 0.8456565142 + 0.8462113738 0.8625394106 0.8462113738 + 0.8467662930 0.8630533218 0.8467662930 + 0.8473210931 0.8635671735 0.8473210931 + 0.8478760123 0.8640810847 0.8478760123 + 0.8484287262 0.8645929098 0.8484287262 + 0.8489791751 0.8651022911 0.8489791751 + 0.8495296240 0.8656117916 0.8495296240 + 0.8500800133 0.8661211729 0.8500800133 + 0.8506304026 0.8666306734 0.8506304026 + 0.8511807919 0.8671401739 0.8511807919 + 0.8517271280 0.8676456213 0.8517271280 + 0.8522726297 0.8681504130 0.8522726297 + 0.8528181911 0.8686553240 0.8528181911 + 0.8533636928 0.8691601157 0.8533636928 + 0.8539093137 0.8696649075 0.8539093137 + 0.8544539809 0.8701689839 0.8544539809 + 0.8549953103 0.8706703186 0.8549953103 + 0.8555366993 0.8711714745 0.8555366993 + 0.8560780883 0.8716726899 0.8560780883 + 0.8566194177 0.8721739054 0.8566194177 + 0.8571608067 0.8726751804 0.8571608067 + 0.8577004075 0.8731746078 0.8577004075 + 0.8582382798 0.8736721873 0.8582382798 + 0.8587760925 0.8741698861 0.8587760925 + 0.8593140244 0.8746675253 0.8593140244 + 0.8598517776 0.8751651049 0.8598517776 + 0.8603895903 0.8756626844 0.8603895903 + 0.8609246016 0.8761566877 0.8609246016 + 0.8614584208 0.8766493797 0.8614584208 + 0.8619921803 0.8771420121 0.8619921803 + 0.8625261188 0.8776347041 0.8625261188 + 0.8630598783 0.8781273961 0.8630598783 + 0.8635936975 0.8786200285 0.8635936975 + 0.8641244173 0.8791080713 0.8641244173 + 0.8646547794 0.8795956969 0.8646547794 + 0.8651853204 0.8800833821 0.8651853204 + 0.8657156825 0.8805711269 0.8657156825 + 0.8662461042 0.8810586929 0.8662461042 + 0.8667764068 0.8815460801 0.8667764068 + 0.8673040271 0.8820288181 0.8673040271 + 0.8678315878 0.8825113773 0.8678315878 + 0.8683593273 0.8829941154 0.8683593273 + 0.8688868880 0.8834767938 0.8688868880 + 0.8694145083 0.8839594722 0.8694145083 + 0.8699415922 0.8844413757 0.8699415922 + 0.8704659939 0.8849189878 0.8704659939 + 0.8709903955 0.8853965998 0.8709903955 + 0.8715146780 0.8858740926 0.8715146780 + 0.8720390797 0.8863517046 0.8720390797 + 0.8725634813 0.8868293166 0.8725634813 + 0.8730871081 0.8873060942 0.8730871081 + 0.8736078143 0.8877797127 0.8736078143 + 0.8741285205 0.8882532716 0.8741285205 + 0.8746492267 0.8887268901 0.8746492267 + 0.8751698136 0.8892006278 0.8751698136 + 0.8756905198 0.8896741867 0.8756905198 + 0.8762103915 0.8901467919 0.8762103915 + 0.8767271042 0.8906152844 0.8767271042 + 0.8772436976 0.8910837770 0.8772436976 + 0.8777604103 0.8915523887 0.8777604103 + 0.8782770038 0.8920208812 0.8782770038 + 0.8787937164 0.8924893737 0.8787937164 + 0.8793097734 0.8929569721 0.8793097734 + 0.8798230886 0.8934193254 0.8798230886 + 0.8803362846 0.8938816190 0.8803362846 + 0.8808495998 0.8943439126 0.8808495998 + 0.8813629150 0.8948062062 0.8813629150 + 0.8818761110 0.8952684999 0.8818761110 + 0.8823893070 0.8957306147 0.8823893070 + 0.8828998208 0.8961877823 0.8828998208 + 0.8834103942 0.8966450095 0.8834103942 + 0.8839209080 0.8971021771 0.8839209080 + 0.8844314218 0.8975594044 0.8844314218 + 0.8849418759 0.8980165720 0.8849418759 + 0.8854525089 0.8984737992 0.8854525089 + 0.8859602213 0.8989266157 0.8859602213 + 0.8864675760 0.8993787766 0.8864675760 + 0.8869749904 0.8998309970 0.8869749904 + 0.8874824047 0.9002832174 0.8874824047 + 0.8879898190 0.9007353783 0.8879898190 + 0.8884971142 0.9011875987 0.8884971142 + 0.8890014291 0.9016364217 0.8890014291 + 0.8895040751 0.9020836949 0.8895040751 + 0.8900067806 0.9025309086 0.8900067806 + 0.8905096054 0.9029781222 0.8905096054 + 0.8910123110 0.9034253955 0.8910123110 + 0.8915150166 0.9038726091 0.8915150166 + 0.8920161128 0.9043182135 0.8920161128 + 0.8925150037 0.9047616124 0.8925150037 + 0.8930137753 0.9052050114 0.8930137753 + 0.8935126066 0.9056484103 0.8935126066 + 0.8940114975 0.9060918093 0.8940114975 + 0.8945103288 0.9065353274 0.8945103288 + 0.8950086832 0.9069780707 0.8950086832 + 0.8955042958 0.9074168801 0.8955042958 + 0.8959999084 0.9078556895 0.8959999084 + 0.8964955211 0.9082944989 0.8964955211 + 0.8969910741 0.9087331891 0.8969910741 + 0.8974866271 0.9091719985 0.8974866271 + 0.8979821801 0.9096108079 0.8979821801 + 0.8984758854 0.9100461006 0.8984758854 + 0.8989689946 0.9104802012 0.8989689946 + 0.8994619846 0.9109144211 0.8994619846 + 0.8999550939 0.9113485217 0.8999550939 + 0.9004480839 0.9117826819 0.9004480839 + 0.9009411931 0.9122167826 0.9009411931 + 0.9014332891 0.9126496911 0.9014332891 + 0.9019234180 0.9130802751 0.9019234180 + 0.9024136066 0.9135109186 0.9024136066 + 0.9029036760 0.9139415026 0.9029036760 + 0.9033939242 0.9143720865 0.9033939242 + 0.9038839936 0.9148026705 0.9038839936 + 0.9043741822 0.9152333140 0.9043741822 + 0.9048606157 0.9156600237 0.9048606157 + 0.9053465128 0.9160860777 0.9053465128 + 0.9058324099 0.9165123105 0.9058324099 + 0.9063184261 0.9169384241 0.9063184261 + 0.9068043232 0.9173644781 0.9068043232 + 0.9072902203 0.9177907109 0.9072902203 + 0.9077749252 0.9182155728 0.9077749252 + 0.9082573056 0.9186385274 0.9082573056 + 0.9087396860 0.9190613031 0.9087396860 + 0.9092221856 0.9194840789 0.9092221856 + 0.9097046256 0.9199069142 0.9097046256 + 0.9101871252 0.9203298092 0.9101871252 + 0.9106695056 0.9207525849 0.9106695056 + 0.9111497998 0.9211729765 0.9111497998 + 0.9116293788 0.9215927124 0.9116293788 + 0.9121090770 0.9220123887 0.9121090770 + 0.9125887156 0.9224321246 0.9125887156 + 0.9130684137 0.9228518009 0.9130684137 + 0.9135479927 0.9232714772 0.9135479927 + 0.9140272141 0.9236903787 0.9140272141 + 0.9145038724 0.9241051078 0.9145038724 + 0.9149804711 0.9245197177 0.9149804711 + 0.9154571891 0.9249343276 0.9154571891 + 0.9159337878 0.9253488779 0.9159337878 + 0.9164105058 0.9257636070 0.9164105058 + 0.9168871045 0.9261782169 0.9168871045 + 0.9173616767 0.9265909791 0.9173616767 + 0.9178339839 0.9270015955 0.9178339839 + 0.9183062911 0.9274122715 0.9183062911 + 0.9187785983 0.9278230071 0.9187785983 + 0.9192509055 0.9282336831 0.9192509055 + 0.9197230935 0.9286444187 0.9197230935 + 0.9201954007 0.9290549755 0.9201954007 + 0.9206650853 0.9294636846 0.9206650853 + 0.9211338162 0.9298716784 0.9211338162 + 0.9216024876 0.9302796125 0.9216024876 + 0.9220712185 0.9306876063 0.9220712185 + 0.9225397706 0.9310954809 0.9225397706 + 0.9230085015 0.9315034151 0.9230085015 + 0.9234771729 0.9319114089 0.9234771729 + 0.9239432812 0.9323160052 0.9239432812 + 0.9244090915 0.9327204227 0.9244090915 + 0.9248750210 0.9331247211 0.9248750210 + 0.9253407717 0.9335290790 0.9253407717 + 0.9258067012 0.9339333773 0.9258067012 + 0.9262725115 0.9343377948 0.9262725115 + 0.9267380834 0.9347419143 0.9267380834 + 0.9272007942 0.9351428747 0.9272007942 + 0.9276636243 0.9355438948 0.9276636243 + 0.9281262755 0.9359449744 0.9281262755 + 0.9285889864 0.9363459945 0.9285889864 + 0.9290516973 0.9367470145 0.9290516973 + 0.9295145273 0.9371480942 0.9295145273 + 0.9299765825 0.9375485778 0.9299765825 + 0.9304360151 0.9379463792 0.9304360151 + 0.9308953881 0.9383442998 0.9308953881 + 0.9313548207 0.9387422204 0.9313548207 + 0.9318141937 0.9391400814 0.9318141937 + 0.9322736263 0.9395378828 0.9322736263 + 0.9327328801 0.9399358034 0.9327328801 + 0.9331911206 0.9403327703 0.9331911206 + 0.9336444736 0.9407266974 0.9336444736 + 0.9340978861 0.9411206245 0.9340978861 + 0.9345511794 0.9415144920 0.9345511794 + 0.9350045919 0.9419082999 0.9350045919 + 0.9354580045 0.9423022270 0.9354580045 + 0.9359112978 0.9426960945 0.9359112978 + 0.9363633990 0.9430891275 0.9363633990 + 0.9368094206 0.9434779882 0.9368094206 + 0.9372553825 0.9438670278 0.9372553825 + 0.9377014041 0.9442558885 0.9377014041 + 0.9381474853 0.9446449280 0.9381474853 + 0.9385935068 0.9450337887 0.9385935068 + 0.9390395284 0.9454228282 0.9390395284 + 0.9394848943 0.9458109736 0.9394848943 + 0.9399245977 0.9461929798 0.9399245977 + 0.9403643012 0.9465749264 0.9403643012 + 0.9408038855 0.9469568729 0.9408038855 + 0.9412435889 0.9473388195 0.9412435889 + 0.9416832924 0.9477208257 0.9416832924 + 0.9421229959 0.9481027126 0.9421229959 + 0.9425626993 0.9484847188 0.9425626993 + 0.9429975748 0.9488611221 0.9429975748 + 0.9434322119 0.9492372274 0.9434322119 + 0.9438669086 0.9496132731 0.9438669086 + 0.9443014860 0.9499893785 0.9443014860 + 0.9447361827 0.9503654838 0.9447361827 + 0.9451708198 0.9507415891 0.9451708198 + 0.9456055164 0.9511176944 0.9456055164 + 0.9460369945 0.9514914751 0.9460369945 + 0.9464673996 0.9518644810 0.9464673996 + 0.9468979239 0.9522374272 0.9468979239 + 0.9473283887 0.9526103735 0.9473283887 + 0.9477587938 0.9529833794 0.9477587938 + 0.9481893182 0.9533563852 0.9481893182 + 0.9486197233 0.9537293911 0.9486197233 + 0.9490481019 0.9541009068 0.9490481019 + 0.9494742751 0.9544708729 0.9494742751 + 0.9499005079 0.9548410177 0.9499005079 + 0.9503266811 0.9552109838 0.9503266811 + 0.9507529140 0.9555810094 0.9507529140 + 0.9511790872 0.9559510946 0.9511790872 + 0.9516053200 0.9563211203 0.9516053200 + 0.9520307779 0.9566904902 0.9520307779 + 0.9524527192 0.9570568800 0.9524527192 + 0.9528746009 0.9574233890 0.9528746009 + 0.9532964826 0.9577897787 0.9532964826 + 0.9537184238 0.9581562877 0.9537184238 + 0.9541404247 0.9585226774 0.9541404247 + 0.9545623064 0.9588891268 0.9545623064 + 0.9549841881 0.9592555761 0.9549841881 + 0.9554026127 0.9596186876 0.9554026127 + 0.9558200836 0.9599809051 0.9558200836 + 0.9562376142 0.9603431821 0.9562376142 + 0.9566550255 0.9607055187 0.9566550255 + 0.9570724964 0.9610676765 0.9570724964 + 0.9574900270 0.9614300132 0.9574900270 + 0.9579074979 0.9617921710 0.9579074979 + 0.9583234191 0.9621530771 0.9583234191 + 0.9587361217 0.9625114202 0.9587361217 + 0.9591488838 0.9628697038 0.9591488838 + 0.9595617056 0.9632279873 0.9595617056 + 0.9599744081 0.9635863900 0.9599744081 + 0.9603871703 0.9639446735 0.9603871703 + 0.9607999921 0.9643030167 0.9607999921 + 0.9612128139 0.9646613002 0.9612128139 + 0.9616206884 0.9650158882 0.9616206884 + 0.9620277286 0.9653698802 0.9620277286 + 0.9624347091 0.9657238126 0.9624347091 + 0.9628416896 0.9660778046 0.9628416896 + 0.9632487297 0.9664316773 0.9632487297 + 0.9636557102 0.9667857289 0.9636557102 + 0.9640626907 0.9671396017 0.9640626907 + 0.9644678831 0.9674922824 0.9644678831 + 0.9648684859 0.9678416252 0.9648684859 + 0.9652690291 0.9681910276 0.9652690291 + 0.9656695127 0.9685403109 0.9656695127 + 0.9660701156 0.9688897133 0.9660701156 + 0.9664705992 0.9692389965 0.9664705992 + 0.9668712020 0.9695882797 0.9668712020 + 0.9672716856 0.9699376822 0.9672716856 + 0.9676678777 0.9702842832 0.9676678777 + 0.9680616856 0.9706295133 0.9680616856 + 0.9684556127 0.9709746242 0.9684556127 + 0.9688494205 0.9713197947 0.9688494205 + 0.9692432284 0.9716649055 0.9692432284 + 0.9696370959 0.9720100760 0.9696370959 + 0.9700309038 0.9723551869 0.9700309038 + 0.9704247713 0.9727004170 0.9704247713 + 0.9708117247 0.9730417132 0.9708117247 + 0.9711983204 0.9733828902 0.9711983204 + 0.9715849161 0.9737240076 0.9715849161 + 0.9719715118 0.9740651846 0.9719715118 + 0.9723581076 0.9744063020 0.9723581076 + 0.9727448225 0.9747474790 0.9727448225 + 0.9731314182 0.9750885963 0.9731314182 + 0.9735164046 0.9754288793 0.9735164046 + 0.9738947749 0.9757658243 0.9738947749 + 0.9742732048 0.9761027098 0.9742732048 + 0.9746515751 0.9764394760 0.9746515751 + 0.9750298858 0.9767764211 0.9750298858 + 0.9754083157 0.9771133065 0.9754083157 + 0.9757866859 0.9774501920 0.9757866859 + 0.9761651158 0.9777870178 0.9761651158 + 0.9765400887 0.9781219959 0.9765400887 + 0.9769093990 0.9784539938 0.9769093990 + 0.9772787094 0.9787858725 0.9772787094 + 0.9776480198 0.9791178107 0.9776480198 + 0.9780173898 0.9794496894 0.9780173898 + 0.9783867002 0.9797816873 0.9783867002 + 0.9787560105 0.9801136255 0.9787560105 + 0.9791253209 0.9804455042 0.9791253209 + 0.9794902205 0.9807748199 0.9794902205 + 0.9798504114 0.9811015129 0.9798504114 + 0.9802107215 0.9814282060 0.9802107215 + 0.9805709124 0.9817547798 0.9805709124 + 0.9809311032 0.9820814729 0.9809311032 + 0.9812914133 0.9824081063 0.9812914133 + 0.9816516042 0.9827347994 0.9816516042 + 0.9820119143 0.9830613732 0.9820119143 + 0.9823670983 0.9833847284 0.9823670983 + 0.9827178717 0.9837049842 0.9827178717 + 0.9830688238 0.9840254188 0.9830688238 + 0.9834197164 0.9843456745 0.9834197164 + 0.9837704897 0.9846661091 0.9837704897 + 0.9841213822 0.9849864244 0.9841213822 + 0.9844722748 0.9853066802 0.9844722748 + 0.9848231077 0.9856271148 0.9848231077 + 0.9851688147 0.9859436154 0.9851688147 + 0.9855098128 0.9862567186 0.9855098128 + 0.9858506918 0.9865697026 0.9858506918 + 0.9861915708 0.9868828058 0.9861915708 + 0.9865326285 0.9871957898 0.9865326285 + 0.9868735075 0.9875088930 0.9868735075 + 0.9872145057 0.9878218770 0.9872145057 + 0.9875553846 0.9881349802 0.9875553846 + 0.9878913760 0.9884446859 0.9878913760 + 0.9882211089 0.9887502193 0.9882211089 + 0.9885507226 0.9890555739 0.9885507226 + 0.9888803959 0.9893611073 0.9888803959 + 0.9892101288 0.9896665215 0.9892101288 + 0.9895396829 0.9899719954 0.9895396829 + 0.9898694158 0.9902774096 0.9898694158 + 0.9901990294 0.9905828834 0.9901990294 + 0.9905248284 0.9908854961 0.9905248284 + 0.9908407927 0.9911810160 0.9908407927 + 0.9911568165 0.9914764166 0.9911568165 + 0.9914727807 0.9917718768 0.9914727807 + 0.9917888045 0.9920672774 0.9917888045 + 0.9921048284 0.9923627973 0.9921048284 + 0.9924207926 0.9926583171 0.9924207926 + 0.9927368164 0.9929537177 0.9927368164 + 0.9930515885 0.9932484031 0.9930515885 + 0.9933490753 0.9935321808 0.9933490753 + 0.9936466813 0.9938160181 0.9936466813 + 0.9939442873 0.9940996766 0.9939442873 + 0.9942418933 0.9943835139 0.9942418933 + 0.9945394993 0.9946672916 0.9945394993 + 0.9948371053 0.9949511290 0.9948371053 + 0.9951347113 0.9952347875 0.9951347113 + 0.9954321980 0.9955186248 0.9954321980 + 0.9957144260 0.9957929850 0.9957144260 + 0.9959921837 0.9960647821 0.9959921837 + 0.9962701201 0.9963365793 0.9962701201 + 0.9965478778 0.9966083765 0.9965478778 + 0.9968256950 0.9968801737 0.9968256950 + 0.9971035719 0.9971519709 0.9971035719 + 0.9973813891 0.9974238276 0.9973813891 + 0.9976593256 0.9976956248 0.9976593256 + 0.9979289770 0.9979603291 0.9979289770 + 0.9981878996 0.9982151985 0.9981878996 + 0.9984468222 0.9984701872 0.9984468222 + 0.9987056255 0.9987251759 0.9987056255 + 0.9989644885 0.9989801049 0.9989644885 + 0.9992234111 0.9992350936 0.9992234111 + 0.9994822741 0.9994900823 0.9994822741 + 0.9997410774 0.9997450113 0.9997410774 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_hdc_200_plus.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_hdc_200_plus.spi1d index 66e4d7e9..701f12eb 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_hdc_200_plus.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_hdc_200_plus.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0127044199 0.0141023202 0.0096477531 - 0.0238081105 0.0259745792 0.0176070407 - 0.0334607586 0.0364749506 0.0246724300 - 0.0421523489 0.0458938591 0.0311751105 - 0.0501546115 0.0544666387 0.0372429788 - 0.0576152317 0.0623432994 0.0429810397 - 0.0646389574 0.0696527734 0.0484409109 - 0.0713031068 0.0764547214 0.0536791012 - 0.0776453391 0.0828472674 0.0587162301 - 0.0837018490 0.0889110714 0.0635792091 - 0.0895045772 0.0946782306 0.0683003217 - 0.0950731933 0.1001901999 0.0728791431 - 0.1004388034 0.1054987013 0.0773395300 - 0.1056184024 0.1106514037 0.0816663206 - 0.1106417999 0.1156636998 0.0858941898 - 0.1155219972 0.1205495000 0.0900247619 - 0.1202521995 0.1252955049 0.0940808207 - 0.1248437986 0.1299131066 0.0980586410 - 0.1292977929 0.1344144046 0.1019453034 - 0.1336303949 0.1388116032 0.1057268977 - 0.1378560066 0.1431176960 0.1094207019 - 0.1419790983 0.1473421007 0.1130444035 - 0.1460126042 0.1514800042 0.1166006997 - 0.1499602944 0.1555261016 0.1200909987 - 0.1538154036 0.1594948024 0.1235229000 - 0.1575873941 0.1633805931 0.1269004941 - 0.1612810045 0.1671908945 0.1302188039 - 0.1648989022 0.1709319055 0.1334829926 - 0.1684425026 0.1746093035 0.1366928071 - 0.1719215959 0.1782346964 0.1398548931 - 0.1753377020 0.1818004996 0.1429744959 - 0.1787009984 0.1853079051 0.1460445970 - 0.1820088029 0.1887629032 0.1490730941 - 0.1852703989 0.1921591014 0.1520525962 - 0.1884782016 0.1955104023 0.1549959034 - 0.1916348040 0.1988154948 0.1578975022 - 0.1947418004 0.2020708025 0.1607636064 - 0.1978072971 0.2052816004 0.1635923982 - 0.2008365989 0.2084514052 0.1663880944 - 0.2038186044 0.2115817070 0.1691492051 - 0.2067597061 0.2146739066 0.1718768030 - 0.2096651047 0.2177291960 0.1745762974 - 0.2125398964 0.2207520008 0.1772498935 - 0.2153764963 0.2237412930 0.1798918992 - 0.2181748003 0.2266951948 0.1825038940 - 0.2209406942 0.2296195030 0.1850908995 - 0.2236783057 0.2325070053 0.1876516938 - 0.2263896018 0.2353623956 0.1901890934 - 0.2290693074 0.2381868958 0.1927060932 - 0.2317135036 0.2409814000 0.1951995045 - 0.2343312949 0.2437489927 0.1976688951 - 0.2369202971 0.2464884967 0.2001180947 - 0.2394883931 0.2492032051 0.2025482059 - 0.2420337945 0.2518971860 0.2049552947 - 0.2445555031 0.2545694113 0.2073439062 - 0.2470511049 0.2572140098 0.2097100019 - 0.2495238036 0.2598383129 0.2120579928 - 0.2519746125 0.2624385953 0.2143891007 - 0.2544065118 0.2650176883 0.2167063057 - 0.2568238080 0.2675766945 0.2190068066 - 0.2592222989 0.2701159120 0.2212958038 - 0.2615979910 0.2726357877 0.2235656977 - 0.2639487982 0.2751353979 0.2258211970 - 0.2662819922 0.2776151896 0.2280610055 - 0.2685964108 0.2800753117 0.2302847058 - 0.2708936930 0.2825163007 0.2324967980 - 0.2731778920 0.2849400938 0.2346933931 - 0.2754473984 0.2873477042 0.2368790060 - 0.2776994109 0.2897374034 0.2390515059 - 0.2799314857 0.2921099067 0.2412095070 - 0.2821440101 0.2944638133 0.2433556020 - 0.2843374014 0.2968018055 0.2454877943 - 0.2865155935 0.2991257012 0.2476056069 - 0.2886820138 0.3014312983 0.2497113049 - 0.2908358872 0.3037211895 0.2518028915 - 0.2929798961 0.3059999049 0.2538796961 - 0.2951102853 0.3082641065 0.2559436858 - 0.2972272933 0.3105159104 0.2579965889 - 0.2993307114 0.3127523959 0.2600373030 - 0.3014214933 0.3149785101 0.2620668113 - 0.3034971058 0.3171896935 0.2640840113 - 0.3055574000 0.3193879128 0.2660909891 - 0.3076024950 0.3215714097 0.2680850029 - 0.3096376061 0.3237439990 0.2700704932 - 0.3116613030 0.3259027898 0.2720479965 - 0.3136739135 0.3280479014 0.2740156054 - 0.3156760931 0.3301830888 0.2759723961 - 0.3176679015 0.3323085010 0.2779215872 - 0.3196499944 0.3344236016 0.2798638940 - 0.3216224015 0.3365249038 0.2817943096 - 0.3235847950 0.3386119902 0.2837162018 - 0.3255361021 0.3406879008 0.2856321931 - 0.3274742067 0.3427498937 0.2875392139 - 0.3294023871 0.3448010087 0.2894361913 - 0.3313204050 0.3468416929 0.2913256884 - 0.3332281113 0.3488731086 0.2932091951 - 0.3351263106 0.3508965969 0.2950840890 - 0.3370158076 0.3529112935 0.2969497144 - 0.3388954103 0.3549158871 0.2988080978 - 0.3407635093 0.3569113910 0.3006610870 - 0.3426195979 0.3588970900 0.3025059998 - 0.3444670141 0.3608722985 0.3043417931 - 0.3463074863 0.3628363907 0.3061706126 - 0.3481400907 0.3647880852 0.3079940081 - 0.3499644995 0.3667264879 0.3098114133 - 0.3517807126 0.3686558902 0.3116197884 - 0.3535911143 0.3705761135 0.3134202063 - 0.3553932011 0.3724865019 0.3152152002 - 0.3571878970 0.3743883967 0.3170050979 - 0.3589738011 0.3762854040 0.3187876940 - 0.3607504964 0.3781766891 0.3205623031 - 0.3625178039 0.3800612986 0.3223307133 - 0.3642742932 0.3819381893 0.3240945041 - 0.3660207987 0.3838064969 0.3258526027 - 0.3677580953 0.3856652975 0.3276030123 - 0.3694871962 0.3875137866 0.3293435872 - 0.3712058961 0.3893510997 0.3310768008 - 0.3729172051 0.3911783993 0.3328061104 - 0.3746241927 0.3929969072 0.3345299065 - 0.3763248026 0.3948045075 0.3362461030 - 0.3780199885 0.3966018856 0.3379529119 - 0.3797098100 0.3983893096 0.3396534920 - 0.3813967109 0.4001693130 0.3413507938 - 0.3830771148 0.4019418955 0.3430429995 - 0.3847537041 0.4037080109 0.3447282910 - 0.3864229918 0.4054659009 0.3464050889 - 0.3880873919 0.4072161019 0.3480730057 - 0.3897438943 0.4089570045 0.3497362137 - 0.3913944960 0.4106892049 0.3513950109 - 0.3930368125 0.4124135971 0.3530482054 - 0.3946720064 0.4141317010 0.3546941876 - 0.3962987065 0.4158441126 0.3563325107 - 0.3979170918 0.4175505042 0.3579638004 - 0.3995271027 0.4192501009 0.3595893085 - 0.4011279941 0.4209432900 0.3612098098 - 0.4027225077 0.4226306081 0.3628252149 - 0.4043093026 0.4243136942 0.3644339144 - 0.4058884978 0.4259892106 0.3660350144 - 0.4074592888 0.4276596904 0.3676308095 - 0.4090228081 0.4293231964 0.3692201078 - 0.4105781019 0.4309802949 0.3708019853 - 0.4121260941 0.4326306880 0.3723767102 - 0.4136680067 0.4342732131 0.3739450872 - 0.4152038097 0.4359092116 0.3755083978 - 0.4167321920 0.4375346899 0.3770667017 - 0.4182567894 0.4391531050 0.3786197007 - 0.4197764993 0.4407626987 0.3801665008 - 0.4212917984 0.4423646033 0.3817074895 - 0.4228025973 0.4439603090 0.3832431138 - 0.4243074954 0.4455477893 0.3847720027 - 0.4258069098 0.4471279085 0.3862960041 - 0.4273006022 0.4487006068 0.3878161013 - 0.4287903011 0.4502668977 0.3893316984 - 0.4302769899 0.4518280029 0.3908422887 - 0.4317578077 0.4533826113 0.3923459053 - 0.4332346916 0.4549309909 0.3938423991 - 0.4347065091 0.4564737976 0.3953357935 - 0.4361734092 0.4580112100 0.3968257904 - 0.4376364946 0.4595425129 0.3983112872 - 0.4390909076 0.4610661864 0.3997904956 - 0.4405421913 0.4625841975 0.4012655020 - 0.4419898093 0.4640966058 0.4027357101 - 0.4434331954 0.4656026065 0.4042006135 - 0.4448713958 0.4671005905 0.4056580067 - 0.4463051856 0.4685927927 0.4071114957 - 0.4477353096 0.4700792134 0.4085614085 - 0.4491625130 0.4715583026 0.4100078046 - 0.4505859017 0.4730297029 0.4114479125 - 0.4520066977 0.4744954109 0.4128834903 - 0.4534246922 0.4759553075 0.4143148959 - 0.4548385143 0.4774056971 0.4157409966 - 0.4562489986 0.4788500965 0.4171645045 - 0.4576565921 0.4802890122 0.4185852110 - 0.4590603113 0.4817211926 0.4200018048 - 0.4604597092 0.4831478894 0.4214130044 - 0.4618560970 0.4845702946 0.4228211045 - 0.4632489979 0.4859867096 0.4242258966 - 0.4646365941 0.4873968959 0.4256238043 - 0.4660210013 0.4888026118 0.4270183146 - 0.4674021006 0.4902028143 0.4284090996 - 0.4687772095 0.4915963113 0.4297944009 - 0.4701490998 0.4929861128 0.4311766922 - 0.4715175033 0.4943715930 0.4325557947 - 0.4728797078 0.4957531989 0.4339298010 - 0.4742386043 0.4971317947 0.4353007078 - 0.4755935073 0.4985060096 0.4366675913 - 0.4769422114 0.4998764992 0.4380275011 - 0.4782876074 0.5012443066 0.4393840134 - 0.4796282947 0.5026069880 0.4407353997 - 0.4809632003 0.5039666891 0.4420841932 - 0.4822948873 0.5053241849 0.4434312880 - 0.4836206138 0.5066757202 0.4447737932 - 0.4849415123 0.5080248713 0.4461131096 - 0.4862594008 0.5093721151 0.4474501014 - 0.4875693917 0.5107128024 0.4487794936 - 0.4888761938 0.5120517015 0.4501057863 - 0.4901781976 0.5133879185 0.4514274895 - 0.4914737940 0.5147191286 0.4527435899 - 0.4927662909 0.5160481930 0.4540570974 - 0.4940516949 0.5173733234 0.4553644061 - 0.4953328967 0.5186954737 0.4566692114 - 0.4966096878 0.5200139880 0.4579710960 - 0.4978792965 0.5213292241 0.4592679143 - 0.4991464019 0.5226427913 0.4605629146 - 0.5004069805 0.5239503980 0.4618535936 - 0.5016648769 0.5252562761 0.4631414115 - 0.5029193163 0.5265597105 0.4644250870 - 0.5041680932 0.5278586149 0.4657050073 - 0.5054149032 0.5291553736 0.4669832885 - 0.5066552162 0.5304455757 0.4682553113 - 0.5078933835 0.5317338109 0.4695254862 - 0.5091258883 0.5330157876 0.4707902074 - 0.5103549957 0.5342947245 0.4720515907 - 0.5115808845 0.5355700254 0.4733094871 - 0.5128020048 0.5368400216 0.4745627046 - 0.5140215755 0.5381075740 0.4758141935 - 0.5152375102 0.5393667817 0.4770599008 - 0.5164520741 0.5406240225 0.4783042073 - 0.5176615715 0.5418733954 0.4795426130 - 0.5188698173 0.5431200266 0.4807788134 - 0.5200755000 0.5443605781 0.4820106924 - 0.5212779045 0.5455970168 0.4832395017 - 0.5224775076 0.5468291044 0.4844653010 - 0.5236744285 0.5480558872 0.4856874049 - 0.5248702168 0.5492796898 0.4869076908 - 0.5260608196 0.5504972935 0.4881236851 - 0.5272505283 0.5517131090 0.4893389046 - 0.5284340978 0.5529218912 0.4905493855 - 0.5296168923 0.5541298985 0.4917592108 - 0.5307930112 0.5553305745 0.4929626882 - 0.5319678783 0.5565304160 0.4941650927 - 0.5331379771 0.5577239990 0.4953621030 - 0.5343062282 0.5589160919 0.4965574145 - 0.5354691148 0.5601027012 0.4977473915 - 0.5366293192 0.5612874031 0.4989356101 - 0.5377836823 0.5624672771 0.5001201034 - 0.5389350057 0.5636451840 0.5013015866 - 0.5400810838 0.5648186207 0.5024777055 - 0.5412240028 0.5659899116 0.5036516190 - 0.5423620939 0.5671570897 0.5048221946 - 0.5434969068 0.5683212280 0.5059906840 - 0.5446271896 0.5694798827 0.5071563125 - 0.5457543135 0.5706359148 0.5083196163 - 0.5468770862 0.5717874765 0.5094795227 - 0.5479968786 0.5729361773 0.5106369257 - 0.5491123796 0.5740798712 0.5117905140 - 0.5502251983 0.5752214193 0.5129418969 - 0.5513336062 0.5763586164 0.5140898228 - 0.5524396896 0.5774930120 0.5152366757 - 0.5535413027 0.5786203146 0.5163816810 - 0.5546411872 0.5797460079 0.5175253153 - 0.5557361841 0.5808660984 0.5186645985 - 0.5568301082 0.5819849968 0.5198031068 - 0.5579187274 0.5830960274 0.5209382176 - 0.5590069294 0.5842068791 0.5220727921 - 0.5600894094 0.5853117108 0.5232008100 - 0.5611718893 0.5864164233 0.5243288279 - 0.5622487068 0.5875138044 0.5254526734 - 0.5633254051 0.5886104107 0.5265762806 - 0.5643972754 0.5897029042 0.5276951194 - 0.5654681921 0.5907940269 0.5288128257 - 0.5665354133 0.5918807983 0.5299268961 - 0.5676007867 0.5929647088 0.5310389996 - 0.5686635971 0.5940455198 0.5321475863 - 0.5697237849 0.5951223969 0.5332525969 - 0.5707824230 0.5961973071 0.5343558788 - 0.5718374848 0.5972664952 0.5354554057 - 0.5728921294 0.5983355045 0.5365543962 - 0.5739423037 0.5993986130 0.5376480818 - 0.5749924779 0.6004617214 0.5387417078 - 0.5760387182 0.6015192866 0.5398297906 - 0.5770841837 0.6025757790 0.5409170985 - 0.5781269073 0.6036278009 0.5420001149 - 0.5791680217 0.6046767235 0.5430803895 - 0.5802066922 0.6057233810 0.5441582203 - 0.5812410712 0.6067652106 0.5452312827 - 0.5822750926 0.6078069210 0.5463038087 - 0.5833042264 0.6088423729 0.5473703146 - 0.5843334198 0.6098778844 0.5484367013 - 0.5853577852 0.6109092832 0.5494986176 - 0.5863807797 0.6119390130 0.5505589843 - 0.5874018073 0.6129667163 0.5516161919 - 0.5884202123 0.6139913201 0.5526692271 - 0.5894376040 0.6150155067 0.5537214875 - 0.5904482007 0.6160331964 0.5547677279 - 0.5914587975 0.6170510054 0.5558139086 - 0.5924654007 0.6180649996 0.5568549037 - 0.5934706926 0.6190776229 0.5578941703 - 0.5944733024 0.6200882196 0.5589315295 - 0.5954716206 0.6210951805 0.5599654913 - 0.5964698195 0.6221022010 0.5609995127 - 0.5974636078 0.6231055260 0.5620262027 - 0.5984573960 0.6241086125 0.5630527139 - 0.5994476080 0.6251075268 0.5640758872 - 0.6004350781 0.6261023879 0.5650966167 - 0.6014220715 0.6270968914 0.5661162734 - 0.6024059057 0.6280854940 0.5671287775 - 0.6033896208 0.6290740967 0.5681412816 - 0.6043697000 0.6300585866 0.5691496134 - 0.6053478718 0.6310405731 0.5701557994 - 0.6063256264 0.6320214868 0.5711603165 - 0.6073008776 0.6329963207 0.5721578002 - 0.6082761288 0.6339710951 0.5731552839 - 0.6092479825 0.6349408031 0.5741487741 - 0.6102185249 0.6359075904 0.5751404166 - 0.6111885905 0.6368734837 0.5761309266 - 0.6121569276 0.6378319860 0.5771154761 - 0.6131250858 0.6387906075 0.5781000853 - 0.6140908003 0.6397454143 0.5790807009 - 0.6150550246 0.6406973004 0.5800589919 - 0.6160188913 0.6416491270 0.5810368061 - 0.6169794202 0.6425917149 0.5820075274 - 0.6179398894 0.6435343027 0.5829781890 - 0.6188982129 0.6444740891 0.5839462876 - 0.6198542714 0.6454101801 0.5849115252 - 0.6208103895 0.6463462710 0.5858767033 - 0.6217638850 0.6472759247 0.5868369937 - 0.6227170825 0.6482042074 0.5877965093 - 0.6236690283 0.6491310000 0.5887547135 - 0.6246173978 0.6500520706 0.5897085071 - 0.6255657077 0.6509732008 0.5906623006 - 0.6265100241 0.6518902183 0.5916125774 - 0.6274515986 0.6528037786 0.5925605297 - 0.6283932924 0.6537175179 0.5935084224 - 0.6293299794 0.6546260715 0.5944507122 - 0.6302664876 0.6555340886 0.5953925848 - 0.6312015057 0.6564409733 0.5963330865 - 0.6321321726 0.6573432088 0.5972688794 - 0.6330627799 0.6582453847 0.5982046127 - 0.6339905262 0.6591445804 0.5991371274 - 0.6349157095 0.6600404978 0.6000667214 - 0.6358407736 0.6609362960 0.6009961963 - 0.6367601752 0.6618282199 0.6019207239 - 0.6376782060 0.6627187133 0.6028438210 - 0.6385959983 0.6636092067 0.6037667990 - 0.6395090818 0.6644952893 0.6046833992 - 0.6404222250 0.6653813124 0.6056001186 - 0.6413338184 0.6662659049 0.6065152884 - 0.6422404051 0.6671448946 0.6074264050 - 0.6431471109 0.6680238843 0.6083375216 - 0.6440510750 0.6689003706 0.6092464924 - 0.6449515224 0.6697732806 0.6101529002 - 0.6458520293 0.6706461906 0.6110591888 - 0.6467483044 0.6715160012 0.6119620800 - 0.6476420164 0.6723836064 0.6128628254 - 0.6485357285 0.6732510924 0.6137633920 - 0.6494253278 0.6741154790 0.6146603823 - 0.6503136158 0.6749786735 0.6155561805 - 0.6512019038 0.6758419275 0.6164519787 - 0.6520848870 0.6766991019 0.6173437238 - 0.6529672742 0.6775552034 0.6182348728 - 0.6538497210 0.6784113050 0.6191260815 - 0.6547253728 0.6792595983 0.6200146079 - 0.6556010842 0.6801074147 0.6209030151 - 0.6564763784 0.6809551120 0.6217911243 - 0.6573458910 0.6817964911 0.6226750016 - 0.6582155228 0.6826378107 0.6235588193 - 0.6590843201 0.6834787726 0.6244422793 - 0.6599482298 0.6843137145 0.6253234148 - 0.6608120799 0.6851487160 0.6262044907 - 0.6616752744 0.6859831810 0.6270849705 - 0.6625339985 0.6868135929 0.6279615164 - 0.6633927822 0.6876440048 0.6288381219 - 0.6642509103 0.6884738207 0.6297143102 - 0.6651049852 0.6892974973 0.6305881143 - 0.6659591794 0.6901211739 0.6314619184 - 0.6668125987 0.6909444928 0.6323351860 - 0.6676605940 0.6917623281 0.6332045794 - 0.6685085893 0.6925802231 0.6340739131 - 0.6693562865 0.6933978796 0.6349430084 - 0.6701996922 0.6942088008 0.6358096004 - 0.6710430980 0.6950196028 0.6366763115 - 0.6718865037 0.6958305240 0.6375429034 - 0.6727249026 0.6966357827 0.6384050846 - 0.6735633016 0.6974408031 0.6392672062 - 0.6744015813 0.6982458830 0.6401293874 - 0.6752368808 0.6990466118 0.6409891844 - 0.6760717034 0.6998466849 0.6418486238 - 0.6769065857 0.7006468177 0.6427081227 - 0.6777368188 0.7014424205 0.6435642242 - 0.6785656810 0.7022364736 0.6444191933 - 0.6793946028 0.7030305266 0.6452742815 - 0.6802213192 0.7038202882 0.6461277008 - 0.6810467839 0.7046071291 0.6469801068 - 0.6818721890 0.7053937912 0.6478325129 - 0.6826949716 0.7061775923 0.6486828923 - 0.6835147142 0.7069572210 0.6495308876 - 0.6843342781 0.7077367902 0.6503788829 - 0.6851524115 0.7085152864 0.6512262821 - 0.6859658957 0.7092896104 0.6520717144 - 0.6867793798 0.7100639939 0.6529170275 - 0.6875926852 0.7108383775 0.6537624002 - 0.6884006262 0.7116078138 0.6546034217 - 0.6892085075 0.7123771906 0.6554443836 - 0.6900163293 0.7131465077 0.6562854052 - 0.6908203959 0.7139121294 0.6571244001 - 0.6916232705 0.7146762013 0.6579627991 - 0.6924263239 0.7154402733 0.6588011980 - 0.6932265162 0.7162014246 0.6596375704 - 0.6940239072 0.7169584036 0.6604716778 - 0.6948214173 0.7177153230 0.6613057256 - 0.6956177950 0.7184715867 0.6621391177 - 0.6964092851 0.7192226052 0.6629688144 - 0.6972007751 0.7199735045 0.6637985706 - 0.6979923844 0.7207245231 0.6646283269 - 0.6987801790 0.7214716077 0.6654537916 - 0.6995673180 0.7222176194 0.6662783027 - 0.7003543973 0.7229636908 0.6671028733 - 0.7011383772 0.7237073183 0.6679254770 - 0.7019187808 0.7244477272 0.6687458754 - 0.7026991248 0.7251880765 0.6695662141 - 0.7034789920 0.7259283066 0.6703861952 - 0.7042531967 0.7266616821 0.6712015867 - 0.7050274014 0.7273951173 0.6720169187 - 0.7058014870 0.7281284928 0.6728321910 - 0.7065715194 0.7288578749 0.6736438274 - 0.7073392868 0.7295845747 0.6744530797 - 0.7081069946 0.7303113937 0.6752625108 - 0.7088738084 0.7310374975 0.6760708094 - 0.7096366882 0.7317596078 0.6768743992 - 0.7103996277 0.7324817777 0.6776779890 - 0.7111625075 0.7332040071 0.6784815192 - 0.7119218111 0.7339224219 0.6792811155 - 0.7126796246 0.7346392274 0.6800791025 - 0.7134374976 0.7353559136 0.6808769703 - 0.7141941786 0.7360715866 0.6816738248 - 0.7149465084 0.7367821932 0.6824663281 - 0.7156988978 0.7374927998 0.6832587719 - 0.7164512873 0.7382034063 0.6840512753 - 0.7172005773 0.7389100790 0.6848403215 - 0.7179483175 0.7396144867 0.6856274009 - 0.7186959982 0.7403188944 0.6864145994 - 0.7194433212 0.7410228848 0.6872013211 - 0.7201871276 0.7417224050 0.6879833937 - 0.7209308743 0.7424219251 0.6887655258 - 0.7216746807 0.7431213856 0.6895475984 - 0.7224153876 0.7438185811 0.6903271079 - 0.7231531739 0.7445130944 0.6911041737 - 0.7238910198 0.7452077270 0.6918814182 - 0.7246286869 0.7459022999 0.6926584840 - 0.7253618240 0.7465913296 0.6934313774 - 0.7260943055 0.7472792864 0.6942036748 - 0.7268267870 0.7479674220 0.6949760914 - 0.7275583148 0.7486543059 0.6957473755 - 0.7282862067 0.7493367791 0.6965150833 - 0.7290140986 0.7500193715 0.6972827911 - 0.7297419906 0.7507019043 0.6980504990 - 0.7304679155 0.7513812184 0.6988149285 - 0.7311919928 0.7520576119 0.6995764971 - 0.7319161296 0.7527340055 0.7003381252 - 0.7326402068 0.7534102798 0.7010996938 - 0.7333592772 0.7540827990 0.7018569708 - 0.7340772748 0.7547543049 0.7026134133 - 0.7347953916 0.7554258108 0.7033697963 - 0.7355127931 0.7560970187 0.7041258216 - 0.7362236977 0.7567629218 0.7048764825 - 0.7369347215 0.7574288845 0.7056272030 - 0.7376456261 0.7580947876 0.7063779235 - 0.7383548021 0.7587593198 0.7071272135 - 0.7390602827 0.7594197989 0.7078732252 - 0.7397658229 0.7600803971 0.7086191177 - 0.7404711843 0.7607408762 0.7093651295 - 0.7411739230 0.7613990903 0.7101075053 - 0.7418738008 0.7620543838 0.7108464241 - 0.7425737977 0.7627096772 0.7115852237 - 0.7432736754 0.7633650899 0.7123240829 - 0.7439711094 0.7640163898 0.7130596042 - 0.7446671128 0.7646651864 0.7137932181 - 0.7453631163 0.7653139830 0.7145268917 - 0.7460591197 0.7659627795 0.7152605057 - 0.7467520237 0.7666075230 0.7159891725 - 0.7474439740 0.7672507167 0.7167164087 - 0.7481361032 0.7678940296 0.7174435258 - 0.7488281131 0.7685372233 0.7181707025 - 0.7495164871 0.7691755295 0.7188938856 - 0.7502045035 0.7698127031 0.7196164727 - 0.7508924007 0.7704498768 0.7203391194 - 0.7515802979 0.7710872293 0.7210617065 - 0.7522624731 0.7717201710 0.7217788100 - 0.7529442906 0.7723528147 0.7224954963 - 0.7536261082 0.7729853988 0.7232123017 - 0.7543079853 0.7736179829 0.7239289880 - 0.7549846768 0.7742474079 0.7246403098 - 0.7556613088 0.7748765945 0.7253513932 - 0.7563378811 0.7755057812 0.7260624766 - 0.7570145726 0.7761349082 0.7267736197 - 0.7576869130 0.7767605186 0.7274788022 - 0.7583590746 0.7773857713 0.7281838059 - 0.7590312958 0.7780112028 0.7288888097 - 0.7597035170 0.7786365151 0.7295936942 - 0.7603724003 0.7792575955 0.7302941084 - 0.7610409856 0.7798781991 0.7309941053 - 0.7617095709 0.7804989219 0.7316941023 - 0.7623782754 0.7811195254 0.7323940992 - 0.7630437016 0.7817357779 0.7330893874 - 0.7637084723 0.7823513150 0.7337840199 - 0.7643734217 0.7829667926 0.7344784737 - 0.7650381923 0.7835822105 0.7351731062 - 0.7656989098 0.7841936946 0.7358644009 - 0.7663580775 0.7848036885 0.7365546227 - 0.7670173049 0.7854136229 0.7372447848 - 0.7676765919 0.7860234976 0.7379350066 - 0.7683327794 0.7866303921 0.7386223078 - 0.7689871788 0.7872350216 0.7393077016 - 0.7696415782 0.7878395915 0.7399932146 - 0.7702959776 0.7884442210 0.7406786084 - 0.7709481716 0.7890473008 0.7413616776 - 0.7715978026 0.7896479964 0.7420418262 - 0.7722473741 0.7902486920 0.7427219152 - 0.7728970051 0.7908495069 0.7434018850 - 0.7735458016 0.7914491296 0.7440809011 - 0.7741922736 0.7920445800 0.7447561026 - 0.7748388052 0.7926400900 0.7454311848 - 0.7754852772 0.7932356000 0.7461063862 - 0.7761318088 0.7938311100 0.7467815876 - 0.7767726183 0.7944201231 0.7474523187 - 0.7774134874 0.7950087786 0.7481228709 - 0.7780542970 0.7955976129 0.7487936020 - 0.7786952257 0.7961863875 0.7494642138 - 0.7793319225 0.7967715263 0.7501307726 - 0.7799670100 0.7973549962 0.7507957220 - 0.7806020975 0.7979385257 0.7514606714 - 0.7812373042 0.7985219955 0.7521256804 - 0.7818704247 0.7991034985 0.7527887821 - 0.7825008035 0.7996814251 0.7534492016 - 0.7831311226 0.8002592921 0.7541096807 - 0.7837613821 0.8008372784 0.7547701001 - 0.7843915224 0.8014150858 0.7554302812 - 0.7850180268 0.8019878864 0.7560858727 - 0.7856444120 0.8025606275 0.7567415237 - 0.7862709165 0.8031334281 0.7573971152 - 0.7868974209 0.8037062287 0.7580528259 - 0.7875211835 0.8042758703 0.7587059736 - 0.7881436944 0.8048439026 0.7593579888 - 0.7887662053 0.8054118752 0.7600100040 - 0.7893887758 0.8059799075 0.7606620193 - 0.7900099158 0.8065465093 0.7613126040 - 0.7906270027 0.8071078062 0.7619590163 - 0.7912439704 0.8076692224 0.7626054883 - 0.7918611169 0.8082305789 0.7632519007 - 0.7924780846 0.8087918758 0.7638983130 - 0.7930914164 0.8093485236 0.7645419240 - 0.7937036753 0.8099036813 0.7651848197 - 0.7943159938 0.8104587793 0.7658277154 - 0.7949283123 0.8110138774 0.7664706111 - 0.7955389023 0.8115677238 0.7671121955 - 0.7961457968 0.8121181130 0.7677505016 - 0.7967525721 0.8126683831 0.7683889270 - 0.7973595262 0.8132187128 0.7690272927 - 0.7979663014 0.8137689829 0.7696655989 - 0.7985696793 0.8143169284 0.7703021169 - 0.7991721034 0.8148639798 0.7709379196 - 0.7997745275 0.8154109716 0.7715737820 - 0.8003767729 0.8159580827 0.7722095847 - 0.8009781241 0.8165045977 0.7728447914 - 0.8015751839 0.8170487285 0.7734771967 - 0.8021721840 0.8175927997 0.7741094828 - 0.8027691841 0.8181368709 0.7747418880 - 0.8033663034 0.8186810017 0.7753742933 - 0.8039602041 0.8192222714 0.7760047913 - 0.8045521975 0.8197615743 0.7766340971 - 0.8051440716 0.8203008175 0.7772634029 - 0.8057360053 0.8208400011 0.7778927088 - 0.8063278794 0.8213791847 0.7785220146 - 0.8069158792 0.8219143748 0.7791473866 - 0.8075037003 0.8224493265 0.7797725201 - 0.8080914021 0.8229840994 0.7803977132 - 0.8086792231 0.8235189915 0.7810227871 - 0.8092660904 0.8240531087 0.7816470265 - 0.8098508120 0.8245849013 0.7822685838 - 0.8104354739 0.8251166940 0.7828900814 - 0.8110200763 0.8256484866 0.7835115790 - 0.8116047978 0.8261802793 0.7841330767 - 0.8121870160 0.8267101049 0.7847530246 - 0.8127670884 0.8272377849 0.7853714228 - 0.8133472204 0.8277655244 0.7859897017 - 0.8139272928 0.8282932043 0.7866080999 - 0.8145074248 0.8288208842 0.7872264981 - 0.8150827289 0.8293445706 0.7878416181 - 0.8156566024 0.8298670053 0.7884557247 - 0.8162304759 0.8303893805 0.7890698910 - 0.8168044090 0.8309118748 0.7896839976 - 0.8173784018 0.8314343095 0.7902982235 - 0.8179473281 0.8319529295 0.7909085751 - 0.8185161948 0.8324713707 0.7915189266 - 0.8190850019 0.8329898119 0.7921292186 - 0.8196539283 0.8335083127 0.7927395105 - 0.8202221990 0.8340262771 0.7933492064 - 0.8207868934 0.8345406055 0.7939547896 - 0.8213515878 0.8350549936 0.7945603132 - 0.8219162822 0.8355693817 0.7951657772 - 0.8224810958 0.8360837102 0.7957714200 - 0.8230450153 0.8365973234 0.7963758111 - 0.8236066103 0.8371080756 0.7969769835 - 0.8241683245 0.8376188278 0.7975782156 - 0.8247299194 0.8381295800 0.7981793284 - 0.8252915144 0.8386403918 0.7987805009 - 0.8258516192 0.8391501904 0.7993801832 - 0.8264086246 0.8396579027 0.7999768257 - 0.8269655704 0.8401656747 0.8005735278 - 0.8275225759 0.8406733871 0.8011701107 - 0.8280795813 0.8411810994 0.8017666936 - 0.8286346793 0.8416873217 0.8023620844 - 0.8291863799 0.8421903253 0.8029553890 - 0.8297381997 0.8426933289 0.8035488129 - 0.8302899003 0.8431962729 0.8041421175 - 0.8308416009 0.8436993957 0.8047354221 - 0.8313921094 0.8442009091 0.8053274155 - 0.8319404125 0.8446996808 0.8059173226 - 0.8324885964 0.8451983929 0.8065071106 - 0.8330368996 0.8456972241 0.8070968986 - 0.8335850835 0.8461958766 0.8076868057 - 0.8341315985 0.8466938734 0.8082754016 - 0.8346744180 0.8471899033 0.8088614941 - 0.8352171183 0.8476858735 0.8094475269 - 0.8357598186 0.8481817842 0.8100336194 - 0.8363025784 0.8486778140 0.8106197119 - 0.8368440270 0.8491731286 0.8112050295 - 0.8373814225 0.8496652842 0.8117879033 - 0.8379188180 0.8501574993 0.8123707771 - 0.8384562135 0.8506497741 0.8129537106 - 0.8389936090 0.8511419892 0.8135365844 - 0.8395305276 0.8516339064 0.8141192198 - 0.8400635719 0.8521212935 0.8146995902 - 0.8405966759 0.8526086807 0.8152799010 - 0.8411297798 0.8530961275 0.8158602118 - 0.8416628242 0.8535835147 0.8164405227 - 0.8421959281 0.8540710211 0.8170207739 - 0.8427261114 0.8545554280 0.8175972104 - 0.8432559967 0.8550394773 0.8181732893 - 0.8437858820 0.8555235863 0.8187493086 - 0.8443158269 0.8560076952 0.8193253875 - 0.8448457122 0.8564918041 0.8199014068 - 0.8453723192 0.8569735885 0.8204749227 - 0.8458976150 0.8574544191 0.8210471869 - 0.8464229107 0.8579350710 0.8216195703 - 0.8469482064 0.8584159017 0.8221920133 - 0.8474735022 0.8588966727 0.8227642775 - 0.8479961157 0.8593757749 0.8233352900 - 0.8485156298 0.8598529100 0.8239045143 - 0.8490350842 0.8603299856 0.8244736791 - 0.8495544791 0.8608071208 0.8250429034 - 0.8500739932 0.8612841964 0.8256121874 - 0.8505926728 0.8617607951 0.8261809945 - 0.8511074781 0.8622341752 0.8267477155 - 0.8516222239 0.8627076149 0.8273143172 - 0.8521370292 0.8631808758 0.8278809786 - 0.8526517749 0.8636543155 0.8284475803 - 0.8531665206 0.8641276956 0.8290143013 - 0.8536782861 0.8645980954 0.8295775056 - 0.8541892767 0.8650677204 0.8301398158 - 0.8547003865 0.8655372858 0.8307021856 - 0.8552113771 0.8660069108 0.8312644958 - 0.8557224274 0.8664764762 0.8318268061 - 0.8562318087 0.8669446111 0.8323876858 - 0.8567386866 0.8674101233 0.8329463005 - 0.8572456837 0.8678756952 0.8335049152 - 0.8577526808 0.8683413267 0.8340634704 - 0.8582596779 0.8688067794 0.8346220851 - 0.8587666154 0.8692724109 0.8351806998 - 0.8592697978 0.8697341084 0.8357362151 - 0.8597729206 0.8701955080 0.8362916708 - 0.8602761030 0.8706570268 0.8368471265 - 0.8607792258 0.8711184859 0.8374025822 - 0.8612822890 0.8715798855 0.8379580975 - 0.8617836833 0.8720393777 0.8385115266 - 0.8622832894 0.8724967837 0.8390629888 - 0.8627830148 0.8729540706 0.8396145105 - 0.8632826209 0.8734114170 0.8401659131 - 0.8637822270 0.8738687038 0.8407173753 - 0.8642818928 0.8743259907 0.8412687778 - 0.8647763133 0.8747799993 0.8418176770 - 0.8652706742 0.8752337098 0.8423663974 - 0.8657649755 0.8756874800 0.8429151177 - 0.8662593961 0.8761413097 0.8434638977 - 0.8667536974 0.8765950203 0.8440126181 - 0.8672459722 0.8770480156 0.8445599079 - 0.8677352071 0.8774996996 0.8451051116 - 0.8682243824 0.8779513836 0.8456501961 - 0.8687136173 0.8784031272 0.8461953998 - 0.8692027926 0.8788548112 0.8467404842 - 0.8696920276 0.8793064952 0.8472856283 - 0.8701782227 0.8797559738 0.8478285074 - 0.8706634045 0.8802047968 0.8483706117 - 0.8711487055 0.8806536198 0.8489127755 - 0.8716340065 0.8811023831 0.8494548798 - 0.8721193075 0.8815513253 0.8499971032 - 0.8726040721 0.8819996119 0.8505387902 - 0.8730852008 0.8824434280 0.8510770202 - 0.8735662103 0.8828871846 0.8516153097 - 0.8740472794 0.8833310008 0.8521534801 - 0.8745284081 0.8837748170 0.8526918292 - 0.8750094771 0.8842185140 0.8532299995 - 0.8754889965 0.8846611977 0.8537666798 - 0.8759661913 0.8851019740 0.8543009758 - 0.8764433861 0.8855428100 0.8548352718 - 0.8769205809 0.8859835863 0.8553696275 - 0.8773978949 0.8864244223 0.8559039235 - 0.8778750896 0.8868651986 0.8564382195 - 0.8783500195 0.8873041272 0.8569704294 - 0.8788238168 0.8877419829 0.8575013280 - 0.8792976141 0.8881797791 0.8580322862 - 0.8797712922 0.8886176944 0.8585631847 - 0.8802450895 0.8890554905 0.8590942025 - 0.8807188272 0.8894934058 0.8596251011 - 0.8811894059 0.8899270296 0.8601537943 - 0.8816593885 0.8903598189 0.8606820703 - 0.8821293116 0.8907926083 0.8612102866 - 0.8825992942 0.8912252784 0.8617386222 - 0.8830692768 0.8916581273 0.8622667789 - 0.8835391998 0.8920909166 0.8627951145 - 0.8840066195 0.8925206065 0.8633205295 - 0.8844739199 0.8929502964 0.8638458848 - 0.8849412799 0.8933799863 0.8643712997 - 0.8854085803 0.8938096762 0.8648967147 - 0.8858758807 0.8942393064 0.8654221296 - 0.8863430023 0.8946688175 0.8659473062 - 0.8868073225 0.8950955272 0.8664698005 - 0.8872717023 0.8955221772 0.8669921756 - 0.8877360225 0.8959488869 0.8675147295 - 0.8882004023 0.8963754773 0.8680372238 - 0.8886647224 0.8968021870 0.8685597181 - 0.8891286850 0.8972283006 0.8690819740 - 0.8895897269 0.8976498842 0.8696026802 - 0.8900507092 0.8980714083 0.8701235056 - 0.8905116916 0.8984929919 0.8706442714 - 0.8909726739 0.8989145160 0.8711650968 - 0.8914337158 0.8993360996 0.8716859221 - 0.8918942809 0.8997570872 0.8722062111 - 0.8923516273 0.9001740813 0.8727232218 - 0.8928089738 0.9005910754 0.8732401133 - 0.8932663798 0.9010080099 0.8737571239 - 0.8937237859 0.9014250040 0.8742740154 - 0.8941810727 0.9018419981 0.8747909069 - 0.8946381807 0.9022588134 0.8753076196 - 0.8950915933 0.9026719928 0.8758205771 - 0.8955450058 0.9030851722 0.8763335943 - 0.8959984183 0.9034984112 0.8768466711 - 0.8964517713 0.9039115906 0.8773596883 - 0.8969051838 0.9043248296 0.8778727055 - 0.8973585963 0.9047380090 0.8783857226 - 0.8978086114 0.9051464796 0.8788955212 - 0.8982583880 0.9055547118 0.8794052005 - 0.8987082839 0.9059628844 0.8799148798 - 0.8991581202 0.9063711166 0.8804246187 - 0.8996080160 0.9067792892 0.8809341788 - 0.9000577927 0.9071875215 0.8814439178 - 0.9005057216 0.9075921774 0.8819513917 - 0.9009531140 0.9079959989 0.8824583888 - 0.9014005065 0.9083997011 0.8829653859 - 0.9018480182 0.9088035226 0.8834723830 - 0.9022954106 0.9092072845 0.8839793801 - 0.9027428031 0.9096111059 0.8844863176 - 0.9031882286 0.9100126028 0.8849915862 - 0.9036322832 0.9104127884 0.8854957223 - 0.9040763974 0.9108129144 0.8859999180 - 0.9045205116 0.9112131000 0.8865039945 - 0.9049645066 0.9116132259 0.8870080709 - 0.9054086208 0.9120134115 0.8875123262 - 0.9058511853 0.9124118090 0.8880149126 - 0.9062910080 0.9128071070 0.8885148168 - 0.9067307115 0.9132024050 0.8890147209 - 0.9071704745 0.9135975838 0.8895145059 - 0.9076102972 0.9139928818 0.8900144100 - 0.9080501199 0.9143881202 0.8905143142 - 0.9084897041 0.9147831798 0.8910140991 - 0.9089263082 0.9151741862 0.8915116191 - 0.9093629122 0.9155650735 0.8920090795 - 0.9097993970 0.9159560800 0.8925067186 - 0.9102360010 0.9163470268 0.8930041790 - 0.9106726050 0.9167379737 0.8935016990 - 0.9111092091 0.9171289206 0.8939992189 - 0.9115434289 0.9175180197 0.8944947124 - 0.9119766951 0.9179061055 0.8949893117 - 0.9124100208 0.9182943106 0.8954837918 - 0.9128432870 0.9186825156 0.8959783912 - 0.9132764935 0.9190706015 0.8964728713 - 0.9137098193 0.9194588065 0.8969674706 - 0.9141420126 0.9198462963 0.8974612951 - 0.9145715833 0.9202318192 0.8979535103 - 0.9150012136 0.9206172824 0.8984456062 - 0.9154307842 0.9210028052 0.8989377022 - 0.9158604145 0.9213883281 0.8994297981 - 0.9162899256 0.9217737913 0.8999218941 - 0.9167194963 0.9221593142 0.9004141092 - 0.9171459079 0.9225426912 0.9009035826 - 0.9175716043 0.9229255915 0.9013926983 - 0.9179974198 0.9233083725 0.9018816948 - 0.9184231162 0.9236912131 0.9023706913 - 0.9188488126 0.9240741134 0.9028596878 - 0.9192745090 0.9244568944 0.9033486843 - 0.9196990132 0.9248390794 0.9038369060 - 0.9201207757 0.9252194166 0.9043233991 - 0.9205424786 0.9255996943 0.9048098922 - 0.9209643006 0.9259800911 0.9052963853 - 0.9213860035 0.9263604283 0.9057828784 - 0.9218078256 0.9267407060 0.9062693715 - 0.9222295880 0.9271209836 0.9067559242 - 0.9226487279 0.9274995923 0.9072409868 - 0.9230669141 0.9278776050 0.9077255726 - 0.9234851003 0.9282554984 0.9082100987 - 0.9239032865 0.9286333919 0.9086946845 - 0.9243214726 0.9290112853 0.9091792703 - 0.9247395992 0.9293891788 0.9096639156 - 0.9251574874 0.9297668934 0.9101482034 - 0.9255725741 0.9301424026 0.9106302261 - 0.9259877801 0.9305179119 0.9111121297 - 0.9264029264 0.9308934212 0.9115940928 - 0.9268180132 0.9312688708 0.9120761156 - 0.9272332191 0.9316444993 0.9125580192 - 0.9276483059 0.9320200086 0.9130399823 - 0.9280616045 0.9323946238 0.9135211110 - 0.9284722805 0.9327678084 0.9140009880 - 0.9288828969 0.9331409931 0.9144809246 - 0.9292935729 0.9335141778 0.9149606824 - 0.9297043085 0.9338874817 0.9154406190 - 0.9301149845 0.9342607260 0.9159204960 - 0.9305256009 0.9346339107 0.9164003730 - 0.9309334755 0.9350057244 0.9168785214 - 0.9313400984 0.9353767037 0.9173557162 - 0.9317467213 0.9357478023 0.9178329110 - 0.9321532845 0.9361187816 0.9183101058 - 0.9325597882 0.9364898205 0.9187873006 - 0.9329664111 0.9368609190 0.9192644954 - 0.9333729744 0.9372318983 0.9197416902 - 0.9337769747 0.9376000166 0.9202169776 - 0.9341807961 0.9379675984 0.9206920266 - 0.9345844984 0.9383351803 0.9211670756 - 0.9349882007 0.9387028217 0.9216421247 - 0.9353919029 0.9390705228 0.9221171737 - 0.9357956052 0.9394381046 0.9225922227 - 0.9361993074 0.9398056865 0.9230672121 - 0.9366000891 0.9401705861 0.9235394001 - 0.9370008707 0.9405354857 0.9240115881 - 0.9374017119 0.9409003854 0.9244837761 - 0.9378024936 0.9412652850 0.9249560237 - 0.9382032752 0.9416301847 0.9254282117 - 0.9386041760 0.9419950843 0.9259003997 - 0.9390046000 0.9423598051 0.9263724089 - 0.9394022822 0.9427227974 0.9268422723 - 0.9397999048 0.9430859089 0.9273123145 - 0.9401975870 0.9434489012 0.9277822971 - 0.9405953288 0.9438120127 0.9282522202 - 0.9409930110 0.9441750050 0.9287222028 - 0.9413906932 0.9445381165 0.9291921854 - 0.9417877793 0.9449006915 0.9296612740 - 0.9421823025 0.9452607036 0.9301261902 - 0.9425768852 0.9456205964 0.9305911064 - 0.9429714084 0.9459806085 0.9310560226 - 0.9433658719 0.9463406205 0.9315208793 - 0.9437603951 0.9467005730 0.9319856763 - 0.9441549182 0.9470605254 0.9324505925 - 0.9445487261 0.9474198818 0.9329144955 - 0.9449381828 0.9477747083 0.9333720803 - 0.9453276992 0.9481294751 0.9338297248 - 0.9457172155 0.9484843016 0.9342873096 - 0.9461067915 0.9488391280 0.9347448945 - 0.9464963078 0.9491938949 0.9352024794 - 0.9468858242 0.9495487809 0.9356601238 - 0.9472748041 0.9499033093 0.9361174107 - 0.9476565719 0.9502521157 0.9365684986 - 0.9480383992 0.9506009221 0.9370197058 - 0.9484201074 0.9509496093 0.9374709129 - 0.9488018751 0.9512984157 0.9379221201 - 0.9491835833 0.9516472220 0.9383733273 - 0.9495654106 0.9519960284 0.9388244152 - 0.9499471784 0.9523447156 0.9392756224 - 0.9503229260 0.9526900053 0.9397230148 - 0.9506981969 0.9530349970 0.9401701093 - 0.9510734081 0.9533799291 0.9406172037 - 0.9514486194 0.9537248015 0.9410642982 - 0.9518237710 0.9540696740 0.9415112734 - 0.9521991014 0.9544147253 0.9419584274 - 0.9525743127 0.9547595978 0.9424055219 - 0.9529467225 0.9551023841 0.9428507090 - 0.9533181190 0.9554443955 0.9432951808 - 0.9536895156 0.9557864070 0.9437395930 - 0.9540607929 0.9561284184 0.9441841245 - 0.9544321895 0.9564703703 0.9446285963 - 0.9548035860 0.9568123817 0.9450731277 - 0.9551749229 0.9571543932 0.9455174804 - 0.9555448294 0.9574952722 0.9459602833 - 0.9559131265 0.9578347802 0.9464008808 - 0.9562813044 0.9581742883 0.9468415976 - 0.9566496015 0.9585137963 0.9472823143 - 0.9570177794 0.9588533044 0.9477229118 - 0.9573860765 0.9591928124 0.9481636286 - 0.9577543139 0.9595323205 0.9486042857 - 0.9581220150 0.9598714113 0.9490444064 - 0.9584864974 0.9602082968 0.9494813085 - 0.9588509798 0.9605451226 0.9499182105 - 0.9592155218 0.9608818889 0.9503551126 - 0.9595801234 0.9612187743 0.9507918954 - 0.9599446058 0.9615556002 0.9512287974 - 0.9603090882 0.9618924856 0.9516656995 - 0.9606735706 0.9622293115 0.9521026015 - 0.9610344768 0.9625636935 0.9525365829 - 0.9613944292 0.9628973007 0.9529696107 - 0.9617542028 0.9632309079 0.9534025788 - 0.9621140957 0.9635645151 0.9538357258 - 0.9624739289 0.9638981223 0.9542686939 - 0.9628337026 0.9642317295 0.9547017813 - 0.9631935954 0.9645652771 0.9551348090 - 0.9635519981 0.9648981094 0.9555665851 - 0.9639074206 0.9652289152 0.9559956789 - 0.9642627835 0.9655597210 0.9564247727 - 0.9646183252 0.9658905268 0.9568538070 - 0.9649736881 0.9662212729 0.9572829008 - 0.9653291106 0.9665520787 0.9577119946 - 0.9656844735 0.9668828845 0.9581410885 - 0.9660398960 0.9672136903 0.9585701227 - 0.9663916826 0.9675421119 0.9589954019 - 0.9667426944 0.9678698778 0.9594197869 - 0.9670937061 0.9681977034 0.9598442912 - 0.9674447179 0.9685255289 0.9602686763 - 0.9677956104 0.9688534141 0.9606931210 - 0.9681466222 0.9691811800 0.9611175060 - 0.9684975743 0.9695090055 0.9615418911 - 0.9688472748 0.9698359966 0.9619650841 - 0.9691932797 0.9701604247 0.9623844028 - 0.9695392847 0.9704847932 0.9628037214 - 0.9698852897 0.9708092213 0.9632229805 - 0.9702312946 0.9711335897 0.9636424184 - 0.9705772996 0.9714580178 0.9640616775 - 0.9709233046 0.9717823863 0.9644809961 - 0.9712691903 0.9721068144 0.9649003148 - 0.9716116190 0.9724292159 0.9653162956 - 0.9719517827 0.9727504253 0.9657304287 - 0.9722920060 0.9730715156 0.9661443830 - 0.9726322293 0.9733927250 0.9665583968 - 0.9729723930 0.9737138152 0.9669724107 - 0.9733124971 0.9740350246 0.9673864245 - 0.9736527205 0.9743561745 0.9678003788 - 0.9739928842 0.9746773243 0.9682143927 - 0.9743273854 0.9749950767 0.9686228037 - 0.9746615291 0.9753125906 0.9690306783 - 0.9749956727 0.9756301045 0.9694386721 - 0.9753298163 0.9759476185 0.9698466063 - 0.9756639004 0.9762650728 0.9702546000 - 0.9759979844 0.9765825868 0.9706624746 - 0.9763321280 0.9769001007 0.9710705280 - 0.9766650200 0.9772170186 0.9714773297 - 0.9769923091 0.9775304198 0.9718787074 - 0.9773195982 0.9778438807 0.9722800851 - 0.9776468873 0.9781572819 0.9726815820 - 0.9779741764 0.9784708023 0.9730830193 - 0.9783015847 0.9787843227 0.9734843969 - 0.9786288738 0.9790977240 0.9738858938 - 0.9789562225 0.9794111848 0.9742872715 - 0.9792808890 0.9797229767 0.9746860862 - 0.9796009064 0.9800319076 0.9750801921 - 0.9799209237 0.9803407788 0.9754742980 - 0.9802408814 0.9806495905 0.9758684039 - 0.9805610180 0.9809585214 0.9762625098 - 0.9808809757 0.9812673926 0.9766566157 - 0.9812009931 0.9815762043 0.9770507216 - 0.9815210104 0.9818850756 0.9774448276 - 0.9818372726 0.9821913242 0.9778347015 - 0.9821496010 0.9824944139 0.9782199264 - 0.9824618101 0.9827976227 0.9786050916 - 0.9827740788 0.9831007123 0.9789903164 - 0.9830862880 0.9834039211 0.9793756008 - 0.9833986163 0.9837070107 0.9797608256 - 0.9837108254 0.9840102196 0.9801459908 - 0.9840230942 0.9843133092 0.9805312157 - 0.9843308926 0.9846134782 0.9809110165 - 0.9846346974 0.9849109054 0.9812859297 - 0.9849383831 0.9852082729 0.9816609025 - 0.9852421880 0.9855058193 0.9820358157 - 0.9855459929 0.9858031869 0.9824107289 - 0.9858497977 0.9861006141 0.9827855825 - 0.9861536026 0.9863979816 0.9831604958 - 0.9864572883 0.9866954088 0.9835355282 - 0.9867570996 0.9869899154 0.9839053154 - 0.9870529771 0.9872815013 0.9842700958 - 0.9873489141 0.9875730872 0.9846349955 - 0.9876447916 0.9878646731 0.9849997759 - 0.9879406095 0.9881563783 0.9853646755 - 0.9882364869 0.9884480238 0.9857295156 - 0.9885324240 0.9887396097 0.9860944152 - 0.9888283014 0.9890311956 0.9864593148 - 0.9891207814 0.9893190861 0.9868193865 - 0.9894089103 0.9896019101 0.9871731997 - 0.9896969199 0.9898846745 0.9875268936 - 0.9899849892 0.9901676178 0.9878807068 - 0.9902729988 0.9904503822 0.9882345200 - 0.9905611277 0.9907332063 0.9885882735 - 0.9908490777 0.9910160899 0.9889420271 - 0.9911370873 0.9912989140 0.9892957807 - 0.9914227128 0.9915794730 0.9896461964 - 0.9917017817 0.9918540716 0.9899876714 - 0.9919809103 0.9921286702 0.9903292060 - 0.9922599792 0.9924033284 0.9906706810 - 0.9925391078 0.9926779866 0.9910122156 - 0.9928181767 0.9929525852 0.9913538098 - 0.9930973053 0.9932271838 0.9916952848 - 0.9933763742 0.9935019016 0.9920368195 - 0.9936547875 0.9937759042 0.9923774004 - 0.9939225912 0.9940415025 0.9927039146 - 0.9941903949 0.9943069816 0.9930304289 - 0.9944581985 0.9945725799 0.9933570027 - 0.9947260022 0.9948381186 0.9936835170 - 0.9949938059 0.9951037169 0.9940099716 - 0.9952616096 0.9953691959 0.9943366051 - 0.9955294132 0.9956346750 0.9946631193 - 0.9957972765 0.9959002733 0.9949895740 - 0.9960541129 0.9961531162 0.9953014851 - 0.9963079095 0.9964022040 0.9956092238 - 0.9965615869 0.9966512918 0.9959169030 - 0.9968153834 0.9969003797 0.9962245226 - 0.9970691204 0.9971495271 0.9965322018 - 0.9973229170 0.9973986149 0.9968398809 - 0.9975765944 0.9976477027 0.9971476197 - 0.9978303909 0.9978967905 0.9974551797 - 0.9980784059 0.9981389046 0.9977517128 - 0.9983186126 0.9983714819 0.9980326891 - 0.9985588193 0.9986041784 0.9983137250 - 0.9987990260 0.9988368154 0.9985948205 - 0.9990391731 0.9990693927 0.9988757968 - 0.9992793798 0.9993020892 0.9991568923 - 0.9995195866 0.9995347261 0.9994379282 - 0.9997597933 0.9997674227 0.9997190237 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0127044199 0.0141023202 0.0096477531 + 0.0238081105 0.0259745792 0.0176070407 + 0.0334607586 0.0364749506 0.0246724300 + 0.0421523489 0.0458938591 0.0311751105 + 0.0501546115 0.0544666387 0.0372429788 + 0.0576152317 0.0623432994 0.0429810397 + 0.0646389574 0.0696527734 0.0484409109 + 0.0713031068 0.0764547214 0.0536791012 + 0.0776453391 0.0828472674 0.0587162301 + 0.0837018490 0.0889110714 0.0635792091 + 0.0895045772 0.0946782306 0.0683003217 + 0.0950731933 0.1001901999 0.0728791431 + 0.1004388034 0.1054987013 0.0773395300 + 0.1056184024 0.1106514037 0.0816663206 + 0.1106417999 0.1156636998 0.0858941898 + 0.1155219972 0.1205495000 0.0900247619 + 0.1202521995 0.1252955049 0.0940808207 + 0.1248437986 0.1299131066 0.0980586410 + 0.1292977929 0.1344144046 0.1019453034 + 0.1336303949 0.1388116032 0.1057268977 + 0.1378560066 0.1431176960 0.1094207019 + 0.1419790983 0.1473421007 0.1130444035 + 0.1460126042 0.1514800042 0.1166006997 + 0.1499602944 0.1555261016 0.1200909987 + 0.1538154036 0.1594948024 0.1235229000 + 0.1575873941 0.1633805931 0.1269004941 + 0.1612810045 0.1671908945 0.1302188039 + 0.1648989022 0.1709319055 0.1334829926 + 0.1684425026 0.1746093035 0.1366928071 + 0.1719215959 0.1782346964 0.1398548931 + 0.1753377020 0.1818004996 0.1429744959 + 0.1787009984 0.1853079051 0.1460445970 + 0.1820088029 0.1887629032 0.1490730941 + 0.1852703989 0.1921591014 0.1520525962 + 0.1884782016 0.1955104023 0.1549959034 + 0.1916348040 0.1988154948 0.1578975022 + 0.1947418004 0.2020708025 0.1607636064 + 0.1978072971 0.2052816004 0.1635923982 + 0.2008365989 0.2084514052 0.1663880944 + 0.2038186044 0.2115817070 0.1691492051 + 0.2067597061 0.2146739066 0.1718768030 + 0.2096651047 0.2177291960 0.1745762974 + 0.2125398964 0.2207520008 0.1772498935 + 0.2153764963 0.2237412930 0.1798918992 + 0.2181748003 0.2266951948 0.1825038940 + 0.2209406942 0.2296195030 0.1850908995 + 0.2236783057 0.2325070053 0.1876516938 + 0.2263896018 0.2353623956 0.1901890934 + 0.2290693074 0.2381868958 0.1927060932 + 0.2317135036 0.2409814000 0.1951995045 + 0.2343312949 0.2437489927 0.1976688951 + 0.2369202971 0.2464884967 0.2001180947 + 0.2394883931 0.2492032051 0.2025482059 + 0.2420337945 0.2518971860 0.2049552947 + 0.2445555031 0.2545694113 0.2073439062 + 0.2470511049 0.2572140098 0.2097100019 + 0.2495238036 0.2598383129 0.2120579928 + 0.2519746125 0.2624385953 0.2143891007 + 0.2544065118 0.2650176883 0.2167063057 + 0.2568238080 0.2675766945 0.2190068066 + 0.2592222989 0.2701159120 0.2212958038 + 0.2615979910 0.2726357877 0.2235656977 + 0.2639487982 0.2751353979 0.2258211970 + 0.2662819922 0.2776151896 0.2280610055 + 0.2685964108 0.2800753117 0.2302847058 + 0.2708936930 0.2825163007 0.2324967980 + 0.2731778920 0.2849400938 0.2346933931 + 0.2754473984 0.2873477042 0.2368790060 + 0.2776994109 0.2897374034 0.2390515059 + 0.2799314857 0.2921099067 0.2412095070 + 0.2821440101 0.2944638133 0.2433556020 + 0.2843374014 0.2968018055 0.2454877943 + 0.2865155935 0.2991257012 0.2476056069 + 0.2886820138 0.3014312983 0.2497113049 + 0.2908358872 0.3037211895 0.2518028915 + 0.2929798961 0.3059999049 0.2538796961 + 0.2951102853 0.3082641065 0.2559436858 + 0.2972272933 0.3105159104 0.2579965889 + 0.2993307114 0.3127523959 0.2600373030 + 0.3014214933 0.3149785101 0.2620668113 + 0.3034971058 0.3171896935 0.2640840113 + 0.3055574000 0.3193879128 0.2660909891 + 0.3076024950 0.3215714097 0.2680850029 + 0.3096376061 0.3237439990 0.2700704932 + 0.3116613030 0.3259027898 0.2720479965 + 0.3136739135 0.3280479014 0.2740156054 + 0.3156760931 0.3301830888 0.2759723961 + 0.3176679015 0.3323085010 0.2779215872 + 0.3196499944 0.3344236016 0.2798638940 + 0.3216224015 0.3365249038 0.2817943096 + 0.3235847950 0.3386119902 0.2837162018 + 0.3255361021 0.3406879008 0.2856321931 + 0.3274742067 0.3427498937 0.2875392139 + 0.3294023871 0.3448010087 0.2894361913 + 0.3313204050 0.3468416929 0.2913256884 + 0.3332281113 0.3488731086 0.2932091951 + 0.3351263106 0.3508965969 0.2950840890 + 0.3370158076 0.3529112935 0.2969497144 + 0.3388954103 0.3549158871 0.2988080978 + 0.3407635093 0.3569113910 0.3006610870 + 0.3426195979 0.3588970900 0.3025059998 + 0.3444670141 0.3608722985 0.3043417931 + 0.3463074863 0.3628363907 0.3061706126 + 0.3481400907 0.3647880852 0.3079940081 + 0.3499644995 0.3667264879 0.3098114133 + 0.3517807126 0.3686558902 0.3116197884 + 0.3535911143 0.3705761135 0.3134202063 + 0.3553932011 0.3724865019 0.3152152002 + 0.3571878970 0.3743883967 0.3170050979 + 0.3589738011 0.3762854040 0.3187876940 + 0.3607504964 0.3781766891 0.3205623031 + 0.3625178039 0.3800612986 0.3223307133 + 0.3642742932 0.3819381893 0.3240945041 + 0.3660207987 0.3838064969 0.3258526027 + 0.3677580953 0.3856652975 0.3276030123 + 0.3694871962 0.3875137866 0.3293435872 + 0.3712058961 0.3893510997 0.3310768008 + 0.3729172051 0.3911783993 0.3328061104 + 0.3746241927 0.3929969072 0.3345299065 + 0.3763248026 0.3948045075 0.3362461030 + 0.3780199885 0.3966018856 0.3379529119 + 0.3797098100 0.3983893096 0.3396534920 + 0.3813967109 0.4001693130 0.3413507938 + 0.3830771148 0.4019418955 0.3430429995 + 0.3847537041 0.4037080109 0.3447282910 + 0.3864229918 0.4054659009 0.3464050889 + 0.3880873919 0.4072161019 0.3480730057 + 0.3897438943 0.4089570045 0.3497362137 + 0.3913944960 0.4106892049 0.3513950109 + 0.3930368125 0.4124135971 0.3530482054 + 0.3946720064 0.4141317010 0.3546941876 + 0.3962987065 0.4158441126 0.3563325107 + 0.3979170918 0.4175505042 0.3579638004 + 0.3995271027 0.4192501009 0.3595893085 + 0.4011279941 0.4209432900 0.3612098098 + 0.4027225077 0.4226306081 0.3628252149 + 0.4043093026 0.4243136942 0.3644339144 + 0.4058884978 0.4259892106 0.3660350144 + 0.4074592888 0.4276596904 0.3676308095 + 0.4090228081 0.4293231964 0.3692201078 + 0.4105781019 0.4309802949 0.3708019853 + 0.4121260941 0.4326306880 0.3723767102 + 0.4136680067 0.4342732131 0.3739450872 + 0.4152038097 0.4359092116 0.3755083978 + 0.4167321920 0.4375346899 0.3770667017 + 0.4182567894 0.4391531050 0.3786197007 + 0.4197764993 0.4407626987 0.3801665008 + 0.4212917984 0.4423646033 0.3817074895 + 0.4228025973 0.4439603090 0.3832431138 + 0.4243074954 0.4455477893 0.3847720027 + 0.4258069098 0.4471279085 0.3862960041 + 0.4273006022 0.4487006068 0.3878161013 + 0.4287903011 0.4502668977 0.3893316984 + 0.4302769899 0.4518280029 0.3908422887 + 0.4317578077 0.4533826113 0.3923459053 + 0.4332346916 0.4549309909 0.3938423991 + 0.4347065091 0.4564737976 0.3953357935 + 0.4361734092 0.4580112100 0.3968257904 + 0.4376364946 0.4595425129 0.3983112872 + 0.4390909076 0.4610661864 0.3997904956 + 0.4405421913 0.4625841975 0.4012655020 + 0.4419898093 0.4640966058 0.4027357101 + 0.4434331954 0.4656026065 0.4042006135 + 0.4448713958 0.4671005905 0.4056580067 + 0.4463051856 0.4685927927 0.4071114957 + 0.4477353096 0.4700792134 0.4085614085 + 0.4491625130 0.4715583026 0.4100078046 + 0.4505859017 0.4730297029 0.4114479125 + 0.4520066977 0.4744954109 0.4128834903 + 0.4534246922 0.4759553075 0.4143148959 + 0.4548385143 0.4774056971 0.4157409966 + 0.4562489986 0.4788500965 0.4171645045 + 0.4576565921 0.4802890122 0.4185852110 + 0.4590603113 0.4817211926 0.4200018048 + 0.4604597092 0.4831478894 0.4214130044 + 0.4618560970 0.4845702946 0.4228211045 + 0.4632489979 0.4859867096 0.4242258966 + 0.4646365941 0.4873968959 0.4256238043 + 0.4660210013 0.4888026118 0.4270183146 + 0.4674021006 0.4902028143 0.4284090996 + 0.4687772095 0.4915963113 0.4297944009 + 0.4701490998 0.4929861128 0.4311766922 + 0.4715175033 0.4943715930 0.4325557947 + 0.4728797078 0.4957531989 0.4339298010 + 0.4742386043 0.4971317947 0.4353007078 + 0.4755935073 0.4985060096 0.4366675913 + 0.4769422114 0.4998764992 0.4380275011 + 0.4782876074 0.5012443066 0.4393840134 + 0.4796282947 0.5026069880 0.4407353997 + 0.4809632003 0.5039666891 0.4420841932 + 0.4822948873 0.5053241849 0.4434312880 + 0.4836206138 0.5066757202 0.4447737932 + 0.4849415123 0.5080248713 0.4461131096 + 0.4862594008 0.5093721151 0.4474501014 + 0.4875693917 0.5107128024 0.4487794936 + 0.4888761938 0.5120517015 0.4501057863 + 0.4901781976 0.5133879185 0.4514274895 + 0.4914737940 0.5147191286 0.4527435899 + 0.4927662909 0.5160481930 0.4540570974 + 0.4940516949 0.5173733234 0.4553644061 + 0.4953328967 0.5186954737 0.4566692114 + 0.4966096878 0.5200139880 0.4579710960 + 0.4978792965 0.5213292241 0.4592679143 + 0.4991464019 0.5226427913 0.4605629146 + 0.5004069805 0.5239503980 0.4618535936 + 0.5016648769 0.5252562761 0.4631414115 + 0.5029193163 0.5265597105 0.4644250870 + 0.5041680932 0.5278586149 0.4657050073 + 0.5054149032 0.5291553736 0.4669832885 + 0.5066552162 0.5304455757 0.4682553113 + 0.5078933835 0.5317338109 0.4695254862 + 0.5091258883 0.5330157876 0.4707902074 + 0.5103549957 0.5342947245 0.4720515907 + 0.5115808845 0.5355700254 0.4733094871 + 0.5128020048 0.5368400216 0.4745627046 + 0.5140215755 0.5381075740 0.4758141935 + 0.5152375102 0.5393667817 0.4770599008 + 0.5164520741 0.5406240225 0.4783042073 + 0.5176615715 0.5418733954 0.4795426130 + 0.5188698173 0.5431200266 0.4807788134 + 0.5200755000 0.5443605781 0.4820106924 + 0.5212779045 0.5455970168 0.4832395017 + 0.5224775076 0.5468291044 0.4844653010 + 0.5236744285 0.5480558872 0.4856874049 + 0.5248702168 0.5492796898 0.4869076908 + 0.5260608196 0.5504972935 0.4881236851 + 0.5272505283 0.5517131090 0.4893389046 + 0.5284340978 0.5529218912 0.4905493855 + 0.5296168923 0.5541298985 0.4917592108 + 0.5307930112 0.5553305745 0.4929626882 + 0.5319678783 0.5565304160 0.4941650927 + 0.5331379771 0.5577239990 0.4953621030 + 0.5343062282 0.5589160919 0.4965574145 + 0.5354691148 0.5601027012 0.4977473915 + 0.5366293192 0.5612874031 0.4989356101 + 0.5377836823 0.5624672771 0.5001201034 + 0.5389350057 0.5636451840 0.5013015866 + 0.5400810838 0.5648186207 0.5024777055 + 0.5412240028 0.5659899116 0.5036516190 + 0.5423620939 0.5671570897 0.5048221946 + 0.5434969068 0.5683212280 0.5059906840 + 0.5446271896 0.5694798827 0.5071563125 + 0.5457543135 0.5706359148 0.5083196163 + 0.5468770862 0.5717874765 0.5094795227 + 0.5479968786 0.5729361773 0.5106369257 + 0.5491123796 0.5740798712 0.5117905140 + 0.5502251983 0.5752214193 0.5129418969 + 0.5513336062 0.5763586164 0.5140898228 + 0.5524396896 0.5774930120 0.5152366757 + 0.5535413027 0.5786203146 0.5163816810 + 0.5546411872 0.5797460079 0.5175253153 + 0.5557361841 0.5808660984 0.5186645985 + 0.5568301082 0.5819849968 0.5198031068 + 0.5579187274 0.5830960274 0.5209382176 + 0.5590069294 0.5842068791 0.5220727921 + 0.5600894094 0.5853117108 0.5232008100 + 0.5611718893 0.5864164233 0.5243288279 + 0.5622487068 0.5875138044 0.5254526734 + 0.5633254051 0.5886104107 0.5265762806 + 0.5643972754 0.5897029042 0.5276951194 + 0.5654681921 0.5907940269 0.5288128257 + 0.5665354133 0.5918807983 0.5299268961 + 0.5676007867 0.5929647088 0.5310389996 + 0.5686635971 0.5940455198 0.5321475863 + 0.5697237849 0.5951223969 0.5332525969 + 0.5707824230 0.5961973071 0.5343558788 + 0.5718374848 0.5972664952 0.5354554057 + 0.5728921294 0.5983355045 0.5365543962 + 0.5739423037 0.5993986130 0.5376480818 + 0.5749924779 0.6004617214 0.5387417078 + 0.5760387182 0.6015192866 0.5398297906 + 0.5770841837 0.6025757790 0.5409170985 + 0.5781269073 0.6036278009 0.5420001149 + 0.5791680217 0.6046767235 0.5430803895 + 0.5802066922 0.6057233810 0.5441582203 + 0.5812410712 0.6067652106 0.5452312827 + 0.5822750926 0.6078069210 0.5463038087 + 0.5833042264 0.6088423729 0.5473703146 + 0.5843334198 0.6098778844 0.5484367013 + 0.5853577852 0.6109092832 0.5494986176 + 0.5863807797 0.6119390130 0.5505589843 + 0.5874018073 0.6129667163 0.5516161919 + 0.5884202123 0.6139913201 0.5526692271 + 0.5894376040 0.6150155067 0.5537214875 + 0.5904482007 0.6160331964 0.5547677279 + 0.5914587975 0.6170510054 0.5558139086 + 0.5924654007 0.6180649996 0.5568549037 + 0.5934706926 0.6190776229 0.5578941703 + 0.5944733024 0.6200882196 0.5589315295 + 0.5954716206 0.6210951805 0.5599654913 + 0.5964698195 0.6221022010 0.5609995127 + 0.5974636078 0.6231055260 0.5620262027 + 0.5984573960 0.6241086125 0.5630527139 + 0.5994476080 0.6251075268 0.5640758872 + 0.6004350781 0.6261023879 0.5650966167 + 0.6014220715 0.6270968914 0.5661162734 + 0.6024059057 0.6280854940 0.5671287775 + 0.6033896208 0.6290740967 0.5681412816 + 0.6043697000 0.6300585866 0.5691496134 + 0.6053478718 0.6310405731 0.5701557994 + 0.6063256264 0.6320214868 0.5711603165 + 0.6073008776 0.6329963207 0.5721578002 + 0.6082761288 0.6339710951 0.5731552839 + 0.6092479825 0.6349408031 0.5741487741 + 0.6102185249 0.6359075904 0.5751404166 + 0.6111885905 0.6368734837 0.5761309266 + 0.6121569276 0.6378319860 0.5771154761 + 0.6131250858 0.6387906075 0.5781000853 + 0.6140908003 0.6397454143 0.5790807009 + 0.6150550246 0.6406973004 0.5800589919 + 0.6160188913 0.6416491270 0.5810368061 + 0.6169794202 0.6425917149 0.5820075274 + 0.6179398894 0.6435343027 0.5829781890 + 0.6188982129 0.6444740891 0.5839462876 + 0.6198542714 0.6454101801 0.5849115252 + 0.6208103895 0.6463462710 0.5858767033 + 0.6217638850 0.6472759247 0.5868369937 + 0.6227170825 0.6482042074 0.5877965093 + 0.6236690283 0.6491310000 0.5887547135 + 0.6246173978 0.6500520706 0.5897085071 + 0.6255657077 0.6509732008 0.5906623006 + 0.6265100241 0.6518902183 0.5916125774 + 0.6274515986 0.6528037786 0.5925605297 + 0.6283932924 0.6537175179 0.5935084224 + 0.6293299794 0.6546260715 0.5944507122 + 0.6302664876 0.6555340886 0.5953925848 + 0.6312015057 0.6564409733 0.5963330865 + 0.6321321726 0.6573432088 0.5972688794 + 0.6330627799 0.6582453847 0.5982046127 + 0.6339905262 0.6591445804 0.5991371274 + 0.6349157095 0.6600404978 0.6000667214 + 0.6358407736 0.6609362960 0.6009961963 + 0.6367601752 0.6618282199 0.6019207239 + 0.6376782060 0.6627187133 0.6028438210 + 0.6385959983 0.6636092067 0.6037667990 + 0.6395090818 0.6644952893 0.6046833992 + 0.6404222250 0.6653813124 0.6056001186 + 0.6413338184 0.6662659049 0.6065152884 + 0.6422404051 0.6671448946 0.6074264050 + 0.6431471109 0.6680238843 0.6083375216 + 0.6440510750 0.6689003706 0.6092464924 + 0.6449515224 0.6697732806 0.6101529002 + 0.6458520293 0.6706461906 0.6110591888 + 0.6467483044 0.6715160012 0.6119620800 + 0.6476420164 0.6723836064 0.6128628254 + 0.6485357285 0.6732510924 0.6137633920 + 0.6494253278 0.6741154790 0.6146603823 + 0.6503136158 0.6749786735 0.6155561805 + 0.6512019038 0.6758419275 0.6164519787 + 0.6520848870 0.6766991019 0.6173437238 + 0.6529672742 0.6775552034 0.6182348728 + 0.6538497210 0.6784113050 0.6191260815 + 0.6547253728 0.6792595983 0.6200146079 + 0.6556010842 0.6801074147 0.6209030151 + 0.6564763784 0.6809551120 0.6217911243 + 0.6573458910 0.6817964911 0.6226750016 + 0.6582155228 0.6826378107 0.6235588193 + 0.6590843201 0.6834787726 0.6244422793 + 0.6599482298 0.6843137145 0.6253234148 + 0.6608120799 0.6851487160 0.6262044907 + 0.6616752744 0.6859831810 0.6270849705 + 0.6625339985 0.6868135929 0.6279615164 + 0.6633927822 0.6876440048 0.6288381219 + 0.6642509103 0.6884738207 0.6297143102 + 0.6651049852 0.6892974973 0.6305881143 + 0.6659591794 0.6901211739 0.6314619184 + 0.6668125987 0.6909444928 0.6323351860 + 0.6676605940 0.6917623281 0.6332045794 + 0.6685085893 0.6925802231 0.6340739131 + 0.6693562865 0.6933978796 0.6349430084 + 0.6701996922 0.6942088008 0.6358096004 + 0.6710430980 0.6950196028 0.6366763115 + 0.6718865037 0.6958305240 0.6375429034 + 0.6727249026 0.6966357827 0.6384050846 + 0.6735633016 0.6974408031 0.6392672062 + 0.6744015813 0.6982458830 0.6401293874 + 0.6752368808 0.6990466118 0.6409891844 + 0.6760717034 0.6998466849 0.6418486238 + 0.6769065857 0.7006468177 0.6427081227 + 0.6777368188 0.7014424205 0.6435642242 + 0.6785656810 0.7022364736 0.6444191933 + 0.6793946028 0.7030305266 0.6452742815 + 0.6802213192 0.7038202882 0.6461277008 + 0.6810467839 0.7046071291 0.6469801068 + 0.6818721890 0.7053937912 0.6478325129 + 0.6826949716 0.7061775923 0.6486828923 + 0.6835147142 0.7069572210 0.6495308876 + 0.6843342781 0.7077367902 0.6503788829 + 0.6851524115 0.7085152864 0.6512262821 + 0.6859658957 0.7092896104 0.6520717144 + 0.6867793798 0.7100639939 0.6529170275 + 0.6875926852 0.7108383775 0.6537624002 + 0.6884006262 0.7116078138 0.6546034217 + 0.6892085075 0.7123771906 0.6554443836 + 0.6900163293 0.7131465077 0.6562854052 + 0.6908203959 0.7139121294 0.6571244001 + 0.6916232705 0.7146762013 0.6579627991 + 0.6924263239 0.7154402733 0.6588011980 + 0.6932265162 0.7162014246 0.6596375704 + 0.6940239072 0.7169584036 0.6604716778 + 0.6948214173 0.7177153230 0.6613057256 + 0.6956177950 0.7184715867 0.6621391177 + 0.6964092851 0.7192226052 0.6629688144 + 0.6972007751 0.7199735045 0.6637985706 + 0.6979923844 0.7207245231 0.6646283269 + 0.6987801790 0.7214716077 0.6654537916 + 0.6995673180 0.7222176194 0.6662783027 + 0.7003543973 0.7229636908 0.6671028733 + 0.7011383772 0.7237073183 0.6679254770 + 0.7019187808 0.7244477272 0.6687458754 + 0.7026991248 0.7251880765 0.6695662141 + 0.7034789920 0.7259283066 0.6703861952 + 0.7042531967 0.7266616821 0.6712015867 + 0.7050274014 0.7273951173 0.6720169187 + 0.7058014870 0.7281284928 0.6728321910 + 0.7065715194 0.7288578749 0.6736438274 + 0.7073392868 0.7295845747 0.6744530797 + 0.7081069946 0.7303113937 0.6752625108 + 0.7088738084 0.7310374975 0.6760708094 + 0.7096366882 0.7317596078 0.6768743992 + 0.7103996277 0.7324817777 0.6776779890 + 0.7111625075 0.7332040071 0.6784815192 + 0.7119218111 0.7339224219 0.6792811155 + 0.7126796246 0.7346392274 0.6800791025 + 0.7134374976 0.7353559136 0.6808769703 + 0.7141941786 0.7360715866 0.6816738248 + 0.7149465084 0.7367821932 0.6824663281 + 0.7156988978 0.7374927998 0.6832587719 + 0.7164512873 0.7382034063 0.6840512753 + 0.7172005773 0.7389100790 0.6848403215 + 0.7179483175 0.7396144867 0.6856274009 + 0.7186959982 0.7403188944 0.6864145994 + 0.7194433212 0.7410228848 0.6872013211 + 0.7201871276 0.7417224050 0.6879833937 + 0.7209308743 0.7424219251 0.6887655258 + 0.7216746807 0.7431213856 0.6895475984 + 0.7224153876 0.7438185811 0.6903271079 + 0.7231531739 0.7445130944 0.6911041737 + 0.7238910198 0.7452077270 0.6918814182 + 0.7246286869 0.7459022999 0.6926584840 + 0.7253618240 0.7465913296 0.6934313774 + 0.7260943055 0.7472792864 0.6942036748 + 0.7268267870 0.7479674220 0.6949760914 + 0.7275583148 0.7486543059 0.6957473755 + 0.7282862067 0.7493367791 0.6965150833 + 0.7290140986 0.7500193715 0.6972827911 + 0.7297419906 0.7507019043 0.6980504990 + 0.7304679155 0.7513812184 0.6988149285 + 0.7311919928 0.7520576119 0.6995764971 + 0.7319161296 0.7527340055 0.7003381252 + 0.7326402068 0.7534102798 0.7010996938 + 0.7333592772 0.7540827990 0.7018569708 + 0.7340772748 0.7547543049 0.7026134133 + 0.7347953916 0.7554258108 0.7033697963 + 0.7355127931 0.7560970187 0.7041258216 + 0.7362236977 0.7567629218 0.7048764825 + 0.7369347215 0.7574288845 0.7056272030 + 0.7376456261 0.7580947876 0.7063779235 + 0.7383548021 0.7587593198 0.7071272135 + 0.7390602827 0.7594197989 0.7078732252 + 0.7397658229 0.7600803971 0.7086191177 + 0.7404711843 0.7607408762 0.7093651295 + 0.7411739230 0.7613990903 0.7101075053 + 0.7418738008 0.7620543838 0.7108464241 + 0.7425737977 0.7627096772 0.7115852237 + 0.7432736754 0.7633650899 0.7123240829 + 0.7439711094 0.7640163898 0.7130596042 + 0.7446671128 0.7646651864 0.7137932181 + 0.7453631163 0.7653139830 0.7145268917 + 0.7460591197 0.7659627795 0.7152605057 + 0.7467520237 0.7666075230 0.7159891725 + 0.7474439740 0.7672507167 0.7167164087 + 0.7481361032 0.7678940296 0.7174435258 + 0.7488281131 0.7685372233 0.7181707025 + 0.7495164871 0.7691755295 0.7188938856 + 0.7502045035 0.7698127031 0.7196164727 + 0.7508924007 0.7704498768 0.7203391194 + 0.7515802979 0.7710872293 0.7210617065 + 0.7522624731 0.7717201710 0.7217788100 + 0.7529442906 0.7723528147 0.7224954963 + 0.7536261082 0.7729853988 0.7232123017 + 0.7543079853 0.7736179829 0.7239289880 + 0.7549846768 0.7742474079 0.7246403098 + 0.7556613088 0.7748765945 0.7253513932 + 0.7563378811 0.7755057812 0.7260624766 + 0.7570145726 0.7761349082 0.7267736197 + 0.7576869130 0.7767605186 0.7274788022 + 0.7583590746 0.7773857713 0.7281838059 + 0.7590312958 0.7780112028 0.7288888097 + 0.7597035170 0.7786365151 0.7295936942 + 0.7603724003 0.7792575955 0.7302941084 + 0.7610409856 0.7798781991 0.7309941053 + 0.7617095709 0.7804989219 0.7316941023 + 0.7623782754 0.7811195254 0.7323940992 + 0.7630437016 0.7817357779 0.7330893874 + 0.7637084723 0.7823513150 0.7337840199 + 0.7643734217 0.7829667926 0.7344784737 + 0.7650381923 0.7835822105 0.7351731062 + 0.7656989098 0.7841936946 0.7358644009 + 0.7663580775 0.7848036885 0.7365546227 + 0.7670173049 0.7854136229 0.7372447848 + 0.7676765919 0.7860234976 0.7379350066 + 0.7683327794 0.7866303921 0.7386223078 + 0.7689871788 0.7872350216 0.7393077016 + 0.7696415782 0.7878395915 0.7399932146 + 0.7702959776 0.7884442210 0.7406786084 + 0.7709481716 0.7890473008 0.7413616776 + 0.7715978026 0.7896479964 0.7420418262 + 0.7722473741 0.7902486920 0.7427219152 + 0.7728970051 0.7908495069 0.7434018850 + 0.7735458016 0.7914491296 0.7440809011 + 0.7741922736 0.7920445800 0.7447561026 + 0.7748388052 0.7926400900 0.7454311848 + 0.7754852772 0.7932356000 0.7461063862 + 0.7761318088 0.7938311100 0.7467815876 + 0.7767726183 0.7944201231 0.7474523187 + 0.7774134874 0.7950087786 0.7481228709 + 0.7780542970 0.7955976129 0.7487936020 + 0.7786952257 0.7961863875 0.7494642138 + 0.7793319225 0.7967715263 0.7501307726 + 0.7799670100 0.7973549962 0.7507957220 + 0.7806020975 0.7979385257 0.7514606714 + 0.7812373042 0.7985219955 0.7521256804 + 0.7818704247 0.7991034985 0.7527887821 + 0.7825008035 0.7996814251 0.7534492016 + 0.7831311226 0.8002592921 0.7541096807 + 0.7837613821 0.8008372784 0.7547701001 + 0.7843915224 0.8014150858 0.7554302812 + 0.7850180268 0.8019878864 0.7560858727 + 0.7856444120 0.8025606275 0.7567415237 + 0.7862709165 0.8031334281 0.7573971152 + 0.7868974209 0.8037062287 0.7580528259 + 0.7875211835 0.8042758703 0.7587059736 + 0.7881436944 0.8048439026 0.7593579888 + 0.7887662053 0.8054118752 0.7600100040 + 0.7893887758 0.8059799075 0.7606620193 + 0.7900099158 0.8065465093 0.7613126040 + 0.7906270027 0.8071078062 0.7619590163 + 0.7912439704 0.8076692224 0.7626054883 + 0.7918611169 0.8082305789 0.7632519007 + 0.7924780846 0.8087918758 0.7638983130 + 0.7930914164 0.8093485236 0.7645419240 + 0.7937036753 0.8099036813 0.7651848197 + 0.7943159938 0.8104587793 0.7658277154 + 0.7949283123 0.8110138774 0.7664706111 + 0.7955389023 0.8115677238 0.7671121955 + 0.7961457968 0.8121181130 0.7677505016 + 0.7967525721 0.8126683831 0.7683889270 + 0.7973595262 0.8132187128 0.7690272927 + 0.7979663014 0.8137689829 0.7696655989 + 0.7985696793 0.8143169284 0.7703021169 + 0.7991721034 0.8148639798 0.7709379196 + 0.7997745275 0.8154109716 0.7715737820 + 0.8003767729 0.8159580827 0.7722095847 + 0.8009781241 0.8165045977 0.7728447914 + 0.8015751839 0.8170487285 0.7734771967 + 0.8021721840 0.8175927997 0.7741094828 + 0.8027691841 0.8181368709 0.7747418880 + 0.8033663034 0.8186810017 0.7753742933 + 0.8039602041 0.8192222714 0.7760047913 + 0.8045521975 0.8197615743 0.7766340971 + 0.8051440716 0.8203008175 0.7772634029 + 0.8057360053 0.8208400011 0.7778927088 + 0.8063278794 0.8213791847 0.7785220146 + 0.8069158792 0.8219143748 0.7791473866 + 0.8075037003 0.8224493265 0.7797725201 + 0.8080914021 0.8229840994 0.7803977132 + 0.8086792231 0.8235189915 0.7810227871 + 0.8092660904 0.8240531087 0.7816470265 + 0.8098508120 0.8245849013 0.7822685838 + 0.8104354739 0.8251166940 0.7828900814 + 0.8110200763 0.8256484866 0.7835115790 + 0.8116047978 0.8261802793 0.7841330767 + 0.8121870160 0.8267101049 0.7847530246 + 0.8127670884 0.8272377849 0.7853714228 + 0.8133472204 0.8277655244 0.7859897017 + 0.8139272928 0.8282932043 0.7866080999 + 0.8145074248 0.8288208842 0.7872264981 + 0.8150827289 0.8293445706 0.7878416181 + 0.8156566024 0.8298670053 0.7884557247 + 0.8162304759 0.8303893805 0.7890698910 + 0.8168044090 0.8309118748 0.7896839976 + 0.8173784018 0.8314343095 0.7902982235 + 0.8179473281 0.8319529295 0.7909085751 + 0.8185161948 0.8324713707 0.7915189266 + 0.8190850019 0.8329898119 0.7921292186 + 0.8196539283 0.8335083127 0.7927395105 + 0.8202221990 0.8340262771 0.7933492064 + 0.8207868934 0.8345406055 0.7939547896 + 0.8213515878 0.8350549936 0.7945603132 + 0.8219162822 0.8355693817 0.7951657772 + 0.8224810958 0.8360837102 0.7957714200 + 0.8230450153 0.8365973234 0.7963758111 + 0.8236066103 0.8371080756 0.7969769835 + 0.8241683245 0.8376188278 0.7975782156 + 0.8247299194 0.8381295800 0.7981793284 + 0.8252915144 0.8386403918 0.7987805009 + 0.8258516192 0.8391501904 0.7993801832 + 0.8264086246 0.8396579027 0.7999768257 + 0.8269655704 0.8401656747 0.8005735278 + 0.8275225759 0.8406733871 0.8011701107 + 0.8280795813 0.8411810994 0.8017666936 + 0.8286346793 0.8416873217 0.8023620844 + 0.8291863799 0.8421903253 0.8029553890 + 0.8297381997 0.8426933289 0.8035488129 + 0.8302899003 0.8431962729 0.8041421175 + 0.8308416009 0.8436993957 0.8047354221 + 0.8313921094 0.8442009091 0.8053274155 + 0.8319404125 0.8446996808 0.8059173226 + 0.8324885964 0.8451983929 0.8065071106 + 0.8330368996 0.8456972241 0.8070968986 + 0.8335850835 0.8461958766 0.8076868057 + 0.8341315985 0.8466938734 0.8082754016 + 0.8346744180 0.8471899033 0.8088614941 + 0.8352171183 0.8476858735 0.8094475269 + 0.8357598186 0.8481817842 0.8100336194 + 0.8363025784 0.8486778140 0.8106197119 + 0.8368440270 0.8491731286 0.8112050295 + 0.8373814225 0.8496652842 0.8117879033 + 0.8379188180 0.8501574993 0.8123707771 + 0.8384562135 0.8506497741 0.8129537106 + 0.8389936090 0.8511419892 0.8135365844 + 0.8395305276 0.8516339064 0.8141192198 + 0.8400635719 0.8521212935 0.8146995902 + 0.8405966759 0.8526086807 0.8152799010 + 0.8411297798 0.8530961275 0.8158602118 + 0.8416628242 0.8535835147 0.8164405227 + 0.8421959281 0.8540710211 0.8170207739 + 0.8427261114 0.8545554280 0.8175972104 + 0.8432559967 0.8550394773 0.8181732893 + 0.8437858820 0.8555235863 0.8187493086 + 0.8443158269 0.8560076952 0.8193253875 + 0.8448457122 0.8564918041 0.8199014068 + 0.8453723192 0.8569735885 0.8204749227 + 0.8458976150 0.8574544191 0.8210471869 + 0.8464229107 0.8579350710 0.8216195703 + 0.8469482064 0.8584159017 0.8221920133 + 0.8474735022 0.8588966727 0.8227642775 + 0.8479961157 0.8593757749 0.8233352900 + 0.8485156298 0.8598529100 0.8239045143 + 0.8490350842 0.8603299856 0.8244736791 + 0.8495544791 0.8608071208 0.8250429034 + 0.8500739932 0.8612841964 0.8256121874 + 0.8505926728 0.8617607951 0.8261809945 + 0.8511074781 0.8622341752 0.8267477155 + 0.8516222239 0.8627076149 0.8273143172 + 0.8521370292 0.8631808758 0.8278809786 + 0.8526517749 0.8636543155 0.8284475803 + 0.8531665206 0.8641276956 0.8290143013 + 0.8536782861 0.8645980954 0.8295775056 + 0.8541892767 0.8650677204 0.8301398158 + 0.8547003865 0.8655372858 0.8307021856 + 0.8552113771 0.8660069108 0.8312644958 + 0.8557224274 0.8664764762 0.8318268061 + 0.8562318087 0.8669446111 0.8323876858 + 0.8567386866 0.8674101233 0.8329463005 + 0.8572456837 0.8678756952 0.8335049152 + 0.8577526808 0.8683413267 0.8340634704 + 0.8582596779 0.8688067794 0.8346220851 + 0.8587666154 0.8692724109 0.8351806998 + 0.8592697978 0.8697341084 0.8357362151 + 0.8597729206 0.8701955080 0.8362916708 + 0.8602761030 0.8706570268 0.8368471265 + 0.8607792258 0.8711184859 0.8374025822 + 0.8612822890 0.8715798855 0.8379580975 + 0.8617836833 0.8720393777 0.8385115266 + 0.8622832894 0.8724967837 0.8390629888 + 0.8627830148 0.8729540706 0.8396145105 + 0.8632826209 0.8734114170 0.8401659131 + 0.8637822270 0.8738687038 0.8407173753 + 0.8642818928 0.8743259907 0.8412687778 + 0.8647763133 0.8747799993 0.8418176770 + 0.8652706742 0.8752337098 0.8423663974 + 0.8657649755 0.8756874800 0.8429151177 + 0.8662593961 0.8761413097 0.8434638977 + 0.8667536974 0.8765950203 0.8440126181 + 0.8672459722 0.8770480156 0.8445599079 + 0.8677352071 0.8774996996 0.8451051116 + 0.8682243824 0.8779513836 0.8456501961 + 0.8687136173 0.8784031272 0.8461953998 + 0.8692027926 0.8788548112 0.8467404842 + 0.8696920276 0.8793064952 0.8472856283 + 0.8701782227 0.8797559738 0.8478285074 + 0.8706634045 0.8802047968 0.8483706117 + 0.8711487055 0.8806536198 0.8489127755 + 0.8716340065 0.8811023831 0.8494548798 + 0.8721193075 0.8815513253 0.8499971032 + 0.8726040721 0.8819996119 0.8505387902 + 0.8730852008 0.8824434280 0.8510770202 + 0.8735662103 0.8828871846 0.8516153097 + 0.8740472794 0.8833310008 0.8521534801 + 0.8745284081 0.8837748170 0.8526918292 + 0.8750094771 0.8842185140 0.8532299995 + 0.8754889965 0.8846611977 0.8537666798 + 0.8759661913 0.8851019740 0.8543009758 + 0.8764433861 0.8855428100 0.8548352718 + 0.8769205809 0.8859835863 0.8553696275 + 0.8773978949 0.8864244223 0.8559039235 + 0.8778750896 0.8868651986 0.8564382195 + 0.8783500195 0.8873041272 0.8569704294 + 0.8788238168 0.8877419829 0.8575013280 + 0.8792976141 0.8881797791 0.8580322862 + 0.8797712922 0.8886176944 0.8585631847 + 0.8802450895 0.8890554905 0.8590942025 + 0.8807188272 0.8894934058 0.8596251011 + 0.8811894059 0.8899270296 0.8601537943 + 0.8816593885 0.8903598189 0.8606820703 + 0.8821293116 0.8907926083 0.8612102866 + 0.8825992942 0.8912252784 0.8617386222 + 0.8830692768 0.8916581273 0.8622667789 + 0.8835391998 0.8920909166 0.8627951145 + 0.8840066195 0.8925206065 0.8633205295 + 0.8844739199 0.8929502964 0.8638458848 + 0.8849412799 0.8933799863 0.8643712997 + 0.8854085803 0.8938096762 0.8648967147 + 0.8858758807 0.8942393064 0.8654221296 + 0.8863430023 0.8946688175 0.8659473062 + 0.8868073225 0.8950955272 0.8664698005 + 0.8872717023 0.8955221772 0.8669921756 + 0.8877360225 0.8959488869 0.8675147295 + 0.8882004023 0.8963754773 0.8680372238 + 0.8886647224 0.8968021870 0.8685597181 + 0.8891286850 0.8972283006 0.8690819740 + 0.8895897269 0.8976498842 0.8696026802 + 0.8900507092 0.8980714083 0.8701235056 + 0.8905116916 0.8984929919 0.8706442714 + 0.8909726739 0.8989145160 0.8711650968 + 0.8914337158 0.8993360996 0.8716859221 + 0.8918942809 0.8997570872 0.8722062111 + 0.8923516273 0.9001740813 0.8727232218 + 0.8928089738 0.9005910754 0.8732401133 + 0.8932663798 0.9010080099 0.8737571239 + 0.8937237859 0.9014250040 0.8742740154 + 0.8941810727 0.9018419981 0.8747909069 + 0.8946381807 0.9022588134 0.8753076196 + 0.8950915933 0.9026719928 0.8758205771 + 0.8955450058 0.9030851722 0.8763335943 + 0.8959984183 0.9034984112 0.8768466711 + 0.8964517713 0.9039115906 0.8773596883 + 0.8969051838 0.9043248296 0.8778727055 + 0.8973585963 0.9047380090 0.8783857226 + 0.8978086114 0.9051464796 0.8788955212 + 0.8982583880 0.9055547118 0.8794052005 + 0.8987082839 0.9059628844 0.8799148798 + 0.8991581202 0.9063711166 0.8804246187 + 0.8996080160 0.9067792892 0.8809341788 + 0.9000577927 0.9071875215 0.8814439178 + 0.9005057216 0.9075921774 0.8819513917 + 0.9009531140 0.9079959989 0.8824583888 + 0.9014005065 0.9083997011 0.8829653859 + 0.9018480182 0.9088035226 0.8834723830 + 0.9022954106 0.9092072845 0.8839793801 + 0.9027428031 0.9096111059 0.8844863176 + 0.9031882286 0.9100126028 0.8849915862 + 0.9036322832 0.9104127884 0.8854957223 + 0.9040763974 0.9108129144 0.8859999180 + 0.9045205116 0.9112131000 0.8865039945 + 0.9049645066 0.9116132259 0.8870080709 + 0.9054086208 0.9120134115 0.8875123262 + 0.9058511853 0.9124118090 0.8880149126 + 0.9062910080 0.9128071070 0.8885148168 + 0.9067307115 0.9132024050 0.8890147209 + 0.9071704745 0.9135975838 0.8895145059 + 0.9076102972 0.9139928818 0.8900144100 + 0.9080501199 0.9143881202 0.8905143142 + 0.9084897041 0.9147831798 0.8910140991 + 0.9089263082 0.9151741862 0.8915116191 + 0.9093629122 0.9155650735 0.8920090795 + 0.9097993970 0.9159560800 0.8925067186 + 0.9102360010 0.9163470268 0.8930041790 + 0.9106726050 0.9167379737 0.8935016990 + 0.9111092091 0.9171289206 0.8939992189 + 0.9115434289 0.9175180197 0.8944947124 + 0.9119766951 0.9179061055 0.8949893117 + 0.9124100208 0.9182943106 0.8954837918 + 0.9128432870 0.9186825156 0.8959783912 + 0.9132764935 0.9190706015 0.8964728713 + 0.9137098193 0.9194588065 0.8969674706 + 0.9141420126 0.9198462963 0.8974612951 + 0.9145715833 0.9202318192 0.8979535103 + 0.9150012136 0.9206172824 0.8984456062 + 0.9154307842 0.9210028052 0.8989377022 + 0.9158604145 0.9213883281 0.8994297981 + 0.9162899256 0.9217737913 0.8999218941 + 0.9167194963 0.9221593142 0.9004141092 + 0.9171459079 0.9225426912 0.9009035826 + 0.9175716043 0.9229255915 0.9013926983 + 0.9179974198 0.9233083725 0.9018816948 + 0.9184231162 0.9236912131 0.9023706913 + 0.9188488126 0.9240741134 0.9028596878 + 0.9192745090 0.9244568944 0.9033486843 + 0.9196990132 0.9248390794 0.9038369060 + 0.9201207757 0.9252194166 0.9043233991 + 0.9205424786 0.9255996943 0.9048098922 + 0.9209643006 0.9259800911 0.9052963853 + 0.9213860035 0.9263604283 0.9057828784 + 0.9218078256 0.9267407060 0.9062693715 + 0.9222295880 0.9271209836 0.9067559242 + 0.9226487279 0.9274995923 0.9072409868 + 0.9230669141 0.9278776050 0.9077255726 + 0.9234851003 0.9282554984 0.9082100987 + 0.9239032865 0.9286333919 0.9086946845 + 0.9243214726 0.9290112853 0.9091792703 + 0.9247395992 0.9293891788 0.9096639156 + 0.9251574874 0.9297668934 0.9101482034 + 0.9255725741 0.9301424026 0.9106302261 + 0.9259877801 0.9305179119 0.9111121297 + 0.9264029264 0.9308934212 0.9115940928 + 0.9268180132 0.9312688708 0.9120761156 + 0.9272332191 0.9316444993 0.9125580192 + 0.9276483059 0.9320200086 0.9130399823 + 0.9280616045 0.9323946238 0.9135211110 + 0.9284722805 0.9327678084 0.9140009880 + 0.9288828969 0.9331409931 0.9144809246 + 0.9292935729 0.9335141778 0.9149606824 + 0.9297043085 0.9338874817 0.9154406190 + 0.9301149845 0.9342607260 0.9159204960 + 0.9305256009 0.9346339107 0.9164003730 + 0.9309334755 0.9350057244 0.9168785214 + 0.9313400984 0.9353767037 0.9173557162 + 0.9317467213 0.9357478023 0.9178329110 + 0.9321532845 0.9361187816 0.9183101058 + 0.9325597882 0.9364898205 0.9187873006 + 0.9329664111 0.9368609190 0.9192644954 + 0.9333729744 0.9372318983 0.9197416902 + 0.9337769747 0.9376000166 0.9202169776 + 0.9341807961 0.9379675984 0.9206920266 + 0.9345844984 0.9383351803 0.9211670756 + 0.9349882007 0.9387028217 0.9216421247 + 0.9353919029 0.9390705228 0.9221171737 + 0.9357956052 0.9394381046 0.9225922227 + 0.9361993074 0.9398056865 0.9230672121 + 0.9366000891 0.9401705861 0.9235394001 + 0.9370008707 0.9405354857 0.9240115881 + 0.9374017119 0.9409003854 0.9244837761 + 0.9378024936 0.9412652850 0.9249560237 + 0.9382032752 0.9416301847 0.9254282117 + 0.9386041760 0.9419950843 0.9259003997 + 0.9390046000 0.9423598051 0.9263724089 + 0.9394022822 0.9427227974 0.9268422723 + 0.9397999048 0.9430859089 0.9273123145 + 0.9401975870 0.9434489012 0.9277822971 + 0.9405953288 0.9438120127 0.9282522202 + 0.9409930110 0.9441750050 0.9287222028 + 0.9413906932 0.9445381165 0.9291921854 + 0.9417877793 0.9449006915 0.9296612740 + 0.9421823025 0.9452607036 0.9301261902 + 0.9425768852 0.9456205964 0.9305911064 + 0.9429714084 0.9459806085 0.9310560226 + 0.9433658719 0.9463406205 0.9315208793 + 0.9437603951 0.9467005730 0.9319856763 + 0.9441549182 0.9470605254 0.9324505925 + 0.9445487261 0.9474198818 0.9329144955 + 0.9449381828 0.9477747083 0.9333720803 + 0.9453276992 0.9481294751 0.9338297248 + 0.9457172155 0.9484843016 0.9342873096 + 0.9461067915 0.9488391280 0.9347448945 + 0.9464963078 0.9491938949 0.9352024794 + 0.9468858242 0.9495487809 0.9356601238 + 0.9472748041 0.9499033093 0.9361174107 + 0.9476565719 0.9502521157 0.9365684986 + 0.9480383992 0.9506009221 0.9370197058 + 0.9484201074 0.9509496093 0.9374709129 + 0.9488018751 0.9512984157 0.9379221201 + 0.9491835833 0.9516472220 0.9383733273 + 0.9495654106 0.9519960284 0.9388244152 + 0.9499471784 0.9523447156 0.9392756224 + 0.9503229260 0.9526900053 0.9397230148 + 0.9506981969 0.9530349970 0.9401701093 + 0.9510734081 0.9533799291 0.9406172037 + 0.9514486194 0.9537248015 0.9410642982 + 0.9518237710 0.9540696740 0.9415112734 + 0.9521991014 0.9544147253 0.9419584274 + 0.9525743127 0.9547595978 0.9424055219 + 0.9529467225 0.9551023841 0.9428507090 + 0.9533181190 0.9554443955 0.9432951808 + 0.9536895156 0.9557864070 0.9437395930 + 0.9540607929 0.9561284184 0.9441841245 + 0.9544321895 0.9564703703 0.9446285963 + 0.9548035860 0.9568123817 0.9450731277 + 0.9551749229 0.9571543932 0.9455174804 + 0.9555448294 0.9574952722 0.9459602833 + 0.9559131265 0.9578347802 0.9464008808 + 0.9562813044 0.9581742883 0.9468415976 + 0.9566496015 0.9585137963 0.9472823143 + 0.9570177794 0.9588533044 0.9477229118 + 0.9573860765 0.9591928124 0.9481636286 + 0.9577543139 0.9595323205 0.9486042857 + 0.9581220150 0.9598714113 0.9490444064 + 0.9584864974 0.9602082968 0.9494813085 + 0.9588509798 0.9605451226 0.9499182105 + 0.9592155218 0.9608818889 0.9503551126 + 0.9595801234 0.9612187743 0.9507918954 + 0.9599446058 0.9615556002 0.9512287974 + 0.9603090882 0.9618924856 0.9516656995 + 0.9606735706 0.9622293115 0.9521026015 + 0.9610344768 0.9625636935 0.9525365829 + 0.9613944292 0.9628973007 0.9529696107 + 0.9617542028 0.9632309079 0.9534025788 + 0.9621140957 0.9635645151 0.9538357258 + 0.9624739289 0.9638981223 0.9542686939 + 0.9628337026 0.9642317295 0.9547017813 + 0.9631935954 0.9645652771 0.9551348090 + 0.9635519981 0.9648981094 0.9555665851 + 0.9639074206 0.9652289152 0.9559956789 + 0.9642627835 0.9655597210 0.9564247727 + 0.9646183252 0.9658905268 0.9568538070 + 0.9649736881 0.9662212729 0.9572829008 + 0.9653291106 0.9665520787 0.9577119946 + 0.9656844735 0.9668828845 0.9581410885 + 0.9660398960 0.9672136903 0.9585701227 + 0.9663916826 0.9675421119 0.9589954019 + 0.9667426944 0.9678698778 0.9594197869 + 0.9670937061 0.9681977034 0.9598442912 + 0.9674447179 0.9685255289 0.9602686763 + 0.9677956104 0.9688534141 0.9606931210 + 0.9681466222 0.9691811800 0.9611175060 + 0.9684975743 0.9695090055 0.9615418911 + 0.9688472748 0.9698359966 0.9619650841 + 0.9691932797 0.9701604247 0.9623844028 + 0.9695392847 0.9704847932 0.9628037214 + 0.9698852897 0.9708092213 0.9632229805 + 0.9702312946 0.9711335897 0.9636424184 + 0.9705772996 0.9714580178 0.9640616775 + 0.9709233046 0.9717823863 0.9644809961 + 0.9712691903 0.9721068144 0.9649003148 + 0.9716116190 0.9724292159 0.9653162956 + 0.9719517827 0.9727504253 0.9657304287 + 0.9722920060 0.9730715156 0.9661443830 + 0.9726322293 0.9733927250 0.9665583968 + 0.9729723930 0.9737138152 0.9669724107 + 0.9733124971 0.9740350246 0.9673864245 + 0.9736527205 0.9743561745 0.9678003788 + 0.9739928842 0.9746773243 0.9682143927 + 0.9743273854 0.9749950767 0.9686228037 + 0.9746615291 0.9753125906 0.9690306783 + 0.9749956727 0.9756301045 0.9694386721 + 0.9753298163 0.9759476185 0.9698466063 + 0.9756639004 0.9762650728 0.9702546000 + 0.9759979844 0.9765825868 0.9706624746 + 0.9763321280 0.9769001007 0.9710705280 + 0.9766650200 0.9772170186 0.9714773297 + 0.9769923091 0.9775304198 0.9718787074 + 0.9773195982 0.9778438807 0.9722800851 + 0.9776468873 0.9781572819 0.9726815820 + 0.9779741764 0.9784708023 0.9730830193 + 0.9783015847 0.9787843227 0.9734843969 + 0.9786288738 0.9790977240 0.9738858938 + 0.9789562225 0.9794111848 0.9742872715 + 0.9792808890 0.9797229767 0.9746860862 + 0.9796009064 0.9800319076 0.9750801921 + 0.9799209237 0.9803407788 0.9754742980 + 0.9802408814 0.9806495905 0.9758684039 + 0.9805610180 0.9809585214 0.9762625098 + 0.9808809757 0.9812673926 0.9766566157 + 0.9812009931 0.9815762043 0.9770507216 + 0.9815210104 0.9818850756 0.9774448276 + 0.9818372726 0.9821913242 0.9778347015 + 0.9821496010 0.9824944139 0.9782199264 + 0.9824618101 0.9827976227 0.9786050916 + 0.9827740788 0.9831007123 0.9789903164 + 0.9830862880 0.9834039211 0.9793756008 + 0.9833986163 0.9837070107 0.9797608256 + 0.9837108254 0.9840102196 0.9801459908 + 0.9840230942 0.9843133092 0.9805312157 + 0.9843308926 0.9846134782 0.9809110165 + 0.9846346974 0.9849109054 0.9812859297 + 0.9849383831 0.9852082729 0.9816609025 + 0.9852421880 0.9855058193 0.9820358157 + 0.9855459929 0.9858031869 0.9824107289 + 0.9858497977 0.9861006141 0.9827855825 + 0.9861536026 0.9863979816 0.9831604958 + 0.9864572883 0.9866954088 0.9835355282 + 0.9867570996 0.9869899154 0.9839053154 + 0.9870529771 0.9872815013 0.9842700958 + 0.9873489141 0.9875730872 0.9846349955 + 0.9876447916 0.9878646731 0.9849997759 + 0.9879406095 0.9881563783 0.9853646755 + 0.9882364869 0.9884480238 0.9857295156 + 0.9885324240 0.9887396097 0.9860944152 + 0.9888283014 0.9890311956 0.9864593148 + 0.9891207814 0.9893190861 0.9868193865 + 0.9894089103 0.9896019101 0.9871731997 + 0.9896969199 0.9898846745 0.9875268936 + 0.9899849892 0.9901676178 0.9878807068 + 0.9902729988 0.9904503822 0.9882345200 + 0.9905611277 0.9907332063 0.9885882735 + 0.9908490777 0.9910160899 0.9889420271 + 0.9911370873 0.9912989140 0.9892957807 + 0.9914227128 0.9915794730 0.9896461964 + 0.9917017817 0.9918540716 0.9899876714 + 0.9919809103 0.9921286702 0.9903292060 + 0.9922599792 0.9924033284 0.9906706810 + 0.9925391078 0.9926779866 0.9910122156 + 0.9928181767 0.9929525852 0.9913538098 + 0.9930973053 0.9932271838 0.9916952848 + 0.9933763742 0.9935019016 0.9920368195 + 0.9936547875 0.9937759042 0.9923774004 + 0.9939225912 0.9940415025 0.9927039146 + 0.9941903949 0.9943069816 0.9930304289 + 0.9944581985 0.9945725799 0.9933570027 + 0.9947260022 0.9948381186 0.9936835170 + 0.9949938059 0.9951037169 0.9940099716 + 0.9952616096 0.9953691959 0.9943366051 + 0.9955294132 0.9956346750 0.9946631193 + 0.9957972765 0.9959002733 0.9949895740 + 0.9960541129 0.9961531162 0.9953014851 + 0.9963079095 0.9964022040 0.9956092238 + 0.9965615869 0.9966512918 0.9959169030 + 0.9968153834 0.9969003797 0.9962245226 + 0.9970691204 0.9971495271 0.9965322018 + 0.9973229170 0.9973986149 0.9968398809 + 0.9975765944 0.9976477027 0.9971476197 + 0.9978303909 0.9978967905 0.9974551797 + 0.9980784059 0.9981389046 0.9977517128 + 0.9983186126 0.9983714819 0.9980326891 + 0.9985588193 0.9986041784 0.9983137250 + 0.9987990260 0.9988368154 0.9985948205 + 0.9990391731 0.9990693927 0.9988757968 + 0.9992793798 0.9993020892 0.9991568923 + 0.9995195866 0.9995347261 0.9994379282 + 0.9997597933 0.9997674227 0.9997190237 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_hdc_400_plus.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_hdc_400_plus.spi1d index 544bf935..534834da 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_hdc_400_plus.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_hdc_400_plus.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0188545007 0.0200409293 0.0140067302 - 0.0332524888 0.0348740518 0.0258749295 - 0.0452598706 0.0475615300 0.0362151787 - 0.0559787415 0.0589470603 0.0456109904 - 0.0658830702 0.0694492683 0.0543236583 - 0.0752111897 0.0792194605 0.0625015125 - 0.0841012076 0.0884407684 0.0702229664 - 0.0926621929 0.0972259119 0.0775719434 - 0.1009650975 0.1056030989 0.0846038684 - 0.1090589985 0.1136253998 0.0913083479 - 0.1170130968 0.1213366017 0.0977427885 - 0.1247942969 0.1287464052 0.1039230004 - 0.1323173046 0.1358782053 0.1098598987 - 0.1394851059 0.1427361071 0.1155802011 - 0.1463093013 0.1493203044 0.1211000010 - 0.1528204978 0.1556663066 0.1264265031 - 0.1590683013 0.1617784947 0.1315815002 - 0.1650577933 0.1676822007 0.1365823001 - 0.1708182991 0.1734001040 0.1414559036 - 0.1763903052 0.1789600998 0.1462168992 - 0.1817798018 0.1843533963 0.1508775055 - 0.1870086044 0.1895903945 0.1554276943 - 0.1920844018 0.1946922988 0.1598816961 - 0.1970220059 0.1996600926 0.1642390937 - 0.2018329054 0.2045013011 0.1684959978 - 0.2065093070 0.2092238963 0.1726709008 - 0.2110690027 0.2138542980 0.1767773032 - 0.2155348957 0.2183912992 0.1808128059 - 0.2198981047 0.2228228003 0.1847777069 - 0.2241635025 0.2271593064 0.1886793971 - 0.2283228040 0.2314175963 0.1925252974 - 0.2323801965 0.2355985940 0.1963074058 - 0.2363557965 0.2396991998 0.2000353038 - 0.2402614951 0.2437127978 0.2036948055 - 0.2441187054 0.2476392984 0.2072975934 - 0.2479200959 0.2514924109 0.2108584046 - 0.2516668141 0.2552962005 0.2143702954 - 0.2553597987 0.2590639889 0.2178401053 - 0.2590073943 0.2627837062 0.2212640047 - 0.2626096010 0.2664473057 0.2246434987 - 0.2661620975 0.2700594962 0.2279828042 - 0.2696613073 0.2736192942 0.2312768996 - 0.2731044888 0.2771342099 0.2345380038 - 0.2764894962 0.2806042135 0.2377575040 - 0.2798126042 0.2840265930 0.2409393936 - 0.2830832899 0.2873997092 0.2440892011 - 0.2863064110 0.2907201052 0.2471970022 - 0.2894839048 0.2939862907 0.2502729893 - 0.2926194966 0.2971968949 0.2533178031 - 0.2957077026 0.3003590107 0.2563329041 - 0.2987434864 0.3034782112 0.2593177855 - 0.3017309904 0.3065663874 0.2622711957 - 0.3046731055 0.3096238971 0.2652002871 - 0.3075759113 0.3126527965 0.2681050003 - 0.3104431033 0.3156541884 0.2709783912 - 0.3132778108 0.3186286986 0.2738296092 - 0.3160825074 0.3215768933 0.2766565979 - 0.3188611865 0.3244988918 0.2794562876 - 0.3216165900 0.3273960948 0.2822301984 - 0.3243513107 0.3302688003 0.2849779129 - 0.3270677924 0.3331151903 0.2877022028 - 0.3297683895 0.3359293044 0.2903991938 - 0.3324556947 0.3387104869 0.2930696011 - 0.3351315856 0.3414531052 0.2957195044 - 0.3377983868 0.3441590071 0.2983430922 - 0.3404580057 0.3468317986 0.3009459972 - 0.3431122899 0.3494741917 0.3035286963 - 0.3457567990 0.3520886004 0.3060950041 - 0.3483870029 0.3546772897 0.3086442053 - 0.3509952128 0.3572408855 0.3111701012 - 0.3535763919 0.3597824872 0.3136737049 - 0.3561269045 0.3623043895 0.3161568940 - 0.3586477935 0.3648073971 0.3186241984 - 0.3611387014 0.3672929108 0.3210709095 - 0.3636046052 0.3697636127 0.3234997094 - 0.3660457134 0.3722181916 0.3259125948 - 0.3684634864 0.3746534884 0.3283078969 - 0.3708611131 0.3770717978 0.3306862116 - 0.3732374012 0.3794744015 0.3330459893 - 0.3755970895 0.3818623126 0.3353869021 - 0.3779377937 0.3842301071 0.3377087116 - 0.3802644014 0.3865832984 0.3400174975 - 0.3825747967 0.3889225125 0.3423081040 - 0.3848729134 0.3912503123 0.3445838094 - 0.3871577978 0.3935666978 0.3468438089 - 0.3894316852 0.3958694041 0.3490906954 - 0.3916954100 0.3981603980 0.3513231874 - 0.3939490914 0.4004369080 0.3535391092 - 0.3961951137 0.4026972055 0.3557423055 - 0.3984332085 0.4049367011 0.3579295874 - 0.4006646872 0.4071569145 0.3600991964 - 0.4028908908 0.4093607068 0.3622539937 - 0.4051119983 0.4115476012 0.3643927872 - 0.4073289931 0.4137150049 0.3665145040 - 0.4095427990 0.4158644974 0.3686214983 - 0.4117541909 0.4179973006 0.3707144856 - 0.4139570892 0.4201147854 0.3727920055 - 0.4161537886 0.4222174883 0.3748528957 - 0.4183439910 0.4243085980 0.3768979013 - 0.4205237925 0.4263896048 0.3789266050 - 0.4226934910 0.4284594953 0.3809396923 - 0.4248510003 0.4305211902 0.3829380870 - 0.4269989133 0.4325712025 0.3849214911 - 0.4291365147 0.4346114993 0.3868896961 - 0.4312635958 0.4366433918 0.3888421953 - 0.4333817959 0.4386653006 0.3907817900 - 0.4354879856 0.4406721890 0.3927083015 - 0.4375857115 0.4426656961 0.3946214020 - 0.4396727085 0.4446468949 0.3965215981 - 0.4417509139 0.4466181099 0.3984085917 - 0.4438194931 0.4485799074 0.4002822936 - 0.4458774030 0.4505313933 0.4021424949 - 0.4479227960 0.4524719119 0.4039889872 - 0.4499554038 0.4544002116 0.4058243036 - 0.4519762993 0.4563159049 0.4076485038 - 0.4539847076 0.4582194090 0.4094625115 - 0.4559822977 0.4601128101 0.4112651944 - 0.4579656124 0.4619938135 0.4130575061 - 0.4599342942 0.4638626873 0.4148383141 - 0.4618889093 0.4657208920 0.4166125953 - 0.4638301134 0.4675695002 0.4183782041 - 0.4657584131 0.4694097936 0.4201379120 - 0.4676744044 0.4712429941 0.4218888879 - 0.4695785046 0.4730699956 0.4236342013 - 0.4714691937 0.4748862982 0.4253711104 - 0.4733453989 0.4766918123 0.4271022975 - 0.4752089083 0.4784876108 0.4288255870 - 0.4770591855 0.4802748859 0.4305430055 - 0.4788978100 0.4820528924 0.4322530031 - 0.4807246923 0.4838208854 0.4339570105 - 0.4825393856 0.4855782092 0.4356541038 - 0.4843406081 0.4873236120 0.4373449087 - 0.4861288071 0.4890565872 0.4390296042 - 0.4879052043 0.4907774925 0.4407073855 - 0.4896669984 0.4924885035 0.4423801005 - 0.4914163053 0.4941891134 0.4440453053 - 0.4931511879 0.4958809912 0.4457054138 - 0.4948748052 0.4975647032 0.4473578930 - 0.4965868890 0.4992407858 0.4490056038 - 0.4982892871 0.5009080172 0.4506456852 - 0.4999811053 0.5025680065 0.4522778988 - 0.5016608238 0.5042217970 0.4539025128 - 0.5033277869 0.5058667064 0.4555189908 - 0.5049809813 0.5075010061 0.4571281075 - 0.5066220760 0.5091261864 0.4587261975 - 0.5082523823 0.5107440948 0.4603149891 - 0.5098735094 0.5123543739 0.4618948102 - 0.5114861131 0.5139557719 0.4634658098 - 0.5130900741 0.5155460238 0.4650284052 - 0.5146843791 0.5171270967 0.4665809870 - 0.5162659287 0.5186967850 0.4681251943 - 0.5178381205 0.5202578902 0.4696618021 - 0.5194014907 0.5218108296 0.4711911976 - 0.5209562182 0.5233557224 0.4727137089 - 0.5225027204 0.5248928070 0.4742273986 - 0.5240399241 0.5264220834 0.4757342041 - 0.5255666971 0.5279446244 0.4772349000 - 0.5270826221 0.5294610262 0.4787296057 - 0.5285871029 0.5309712887 0.4802174866 - 0.5300801992 0.5324715972 0.4816986024 - 0.5315656066 0.5339645743 0.4831734896 - 0.5330439210 0.5354511142 0.4846417010 - 0.5345147848 0.5369303823 0.4861015081 - 0.5359786153 0.5384036899 0.4875538051 - 0.5374345779 0.5398699045 0.4889999032 - 0.5388823748 0.5413284898 0.4904398918 - 0.5403190255 0.5427761078 0.4918713868 - 0.5417451859 0.5442144275 0.4932934940 - 0.5431644917 0.5456464291 0.4947094917 - 0.5445777774 0.5470724106 0.4961200953 - 0.5459790826 0.5484896898 0.4975199103 - 0.5473731756 0.5499022007 0.4989153147 - 0.5487601161 0.5513098240 0.5003060102 - 0.5501369238 0.5527070761 0.5016881227 - 0.5515047908 0.5540965199 0.5030658245 - 0.5528663993 0.5554798841 0.5044389963 - 0.5542203188 0.5568554997 0.5058041215 - 0.5555655956 0.5582247972 0.5071626902 - 0.5569046140 0.5595903993 0.5085164905 - 0.5582354069 0.5609512925 0.5098640919 - 0.5595571995 0.5623037219 0.5112056732 - 0.5608744025 0.5636506081 0.5125434995 - 0.5621863008 0.5649901032 0.5138760209 - 0.5634893179 0.5663208961 0.5152037144 - 0.5647879243 0.5676466823 0.5165277719 - 0.5660805702 0.5689647198 0.5178461075 - 0.5673637986 0.5702759027 0.5191584826 - 0.5686416030 0.5715829730 0.5204676986 - 0.5699104071 0.5728827119 0.5217714906 - 0.5711724758 0.5741752982 0.5230712891 - 0.5724303126 0.5754632950 0.5243685842 - 0.5736793280 0.5767428279 0.5256611705 - 0.5749223232 0.5780189037 0.5269505978 - 0.5761598945 0.5792912841 0.5282369256 - 0.5773891807 0.5805559158 0.5295171142 - 0.5786157250 0.5818173885 0.5307943821 - 0.5798376203 0.5830736160 0.5320662856 - 0.5810524225 0.5843229294 0.5333334804 - 0.5822634101 0.5855684876 0.5345982909 - 0.5834617019 0.5868036151 0.5358551741 - 0.5846555233 0.5880358219 0.5371090174 - 0.5858426094 0.5892624855 0.5383573771 - 0.5870230794 0.5904846787 0.5395992994 - 0.5882006884 0.5917047858 0.5408381224 - 0.5893698931 0.5929183960 0.5420668125 - 0.5905355811 0.5941283703 0.5432921052 - 0.5916948915 0.5953302979 0.5445101261 - 0.5928494930 0.5965275168 0.5457227826 - 0.5940014720 0.5977213979 0.5469322205 - 0.5951455235 0.5989087224 0.5481334925 - 0.5962870121 0.6000937819 0.5493327975 - 0.5974202752 0.6012709141 0.5505263209 - 0.5985488296 0.6024438739 0.5517168045 - 0.5996711254 0.6036102176 0.5529028773 - 0.6007874012 0.6047713161 0.5540845990 - 0.6019008160 0.6059291959 0.5552638173 - 0.6030085087 0.6070817113 0.5564357042 - 0.6041145921 0.6082327962 0.5576059818 - 0.6052119732 0.6093747020 0.5587717295 - 0.6063060164 0.6105144024 0.5599352717 - 0.6073911786 0.6116470098 0.5610927939 - 0.6084719896 0.6127761006 0.5622478127 - 0.6095473170 0.6139001250 0.5633994937 - 0.6106179953 0.6150193214 0.5645474792 - 0.6116858125 0.6161350012 0.5656930804 - 0.6127483249 0.6172463298 0.5668336153 - 0.6138089895 0.6183556914 0.5679724216 - 0.6148627996 0.6194601059 0.5691046715 - 0.6159157157 0.6205636263 0.5702360272 - 0.6169633269 0.6216604114 0.5713596940 - 0.6180101037 0.6227564216 0.5724828839 - 0.6190497279 0.6238446832 0.5736011267 - 0.6200875044 0.6249315739 0.5747177005 - 0.6211166978 0.6260104179 0.5758256912 - 0.6221439838 0.6270869970 0.5769320130 - 0.6231651902 0.6281551719 0.5780321956 - 0.6241847873 0.6292213202 0.5791301131 - 0.6252005100 0.6302810907 0.5802220702 - 0.6262137294 0.6313390732 0.5813122988 - 0.6272215247 0.6323928237 0.5823984742 - 0.6282256246 0.6334443092 0.5834822059 - 0.6292229891 0.6344907284 0.5845605731 - 0.6302174926 0.6355344057 0.5856364965 - 0.6312069297 0.6365721822 0.5867068172 - 0.6321943998 0.6376072168 0.5877746940 - 0.6331781745 0.6386352777 0.5888370872 - 0.6341605186 0.6396614909 0.5898970962 - 0.6351392865 0.6406822205 0.5909510255 - 0.6361160278 0.6417014003 0.5920032263 - 0.6370865107 0.6427150965 0.5930498242 - 0.6380555034 0.6437277794 0.5940951109 - 0.6390181184 0.6447337866 0.5951337814 - 0.6399800181 0.6457393169 0.5961719155 - 0.6409355998 0.6467369199 0.5972027779 - 0.6418911219 0.6477345228 0.5982336998 - 0.6428390741 0.6487230062 0.5992593169 - 0.6437870860 0.6497110128 0.6002848744 - 0.6447305083 0.6506903768 0.6013044715 - 0.6456733942 0.6516680717 0.6023231745 - 0.6466109157 0.6526402235 0.6033357978 - 0.6475467086 0.6536098123 0.6043460965 - 0.6484770179 0.6545764208 0.6053528786 - 0.6494035721 0.6555402875 0.6063570976 - 0.6503273249 0.6565018892 0.6073589921 - 0.6512470245 0.6574596167 0.6083570719 - 0.6521654129 0.6584159136 0.6093541980 - 0.6530792713 0.6593660116 0.6103470922 - 0.6539931297 0.6603162289 0.6113399863 - 0.6548979282 0.6612582803 0.6123251915 - 0.6558027267 0.6622002125 0.6133103967 - 0.6567010283 0.6631376147 0.6142895222 - 0.6575976014 0.6640735269 0.6152667999 - 0.6584901810 0.6650068164 0.6162406206 - 0.6593794823 0.6659374237 0.6172112823 - 0.6602666974 0.6668658853 0.6181805134 - 0.6611484885 0.6677879095 0.6191458106 - 0.6620302200 0.6687098742 0.6201111078 - 0.6629042029 0.6696240902 0.6210708022 - 0.6637783051 0.6705380082 0.6220304966 - 0.6646463871 0.6714469790 0.6229835153 - 0.6655120850 0.6723536253 0.6239334941 - 0.6663749814 0.6732575297 0.6248810291 - 0.6672335863 0.6741561890 0.6258239746 - 0.6680920124 0.6750550270 0.6267668009 - 0.6689471006 0.6759482026 0.6277027726 - 0.6698023081 0.6768413782 0.6286388040 - 0.6706528068 0.6777299047 0.6295701861 - 0.6715013981 0.6786159277 0.6304991841 - 0.6723480821 0.6794993281 0.6314260960 - 0.6731910110 0.6803758740 0.6323478818 - 0.6740338206 0.6812524199 0.6332696080 - 0.6748722792 0.6821228266 0.6341838837 - 0.6757103205 0.6829922795 0.6350972056 - 0.6765457988 0.6838589907 0.6360073090 - 0.6773787141 0.6847224236 0.6369137168 - 0.6782112718 0.6855856776 0.6378198266 - 0.6790390015 0.6864398122 0.6387205124 - 0.6798666716 0.6872938871 0.6396213174 - 0.6806908250 0.6881433129 0.6405187845 - 0.6815124750 0.6889889836 0.6414139867 - 0.6823334098 0.6898341179 0.6423081756 - 0.6831489205 0.6906722784 0.6431941986 - 0.6839643717 0.6915106177 0.6440802217 - 0.6847774982 0.6923443079 0.6449621916 - 0.6855893135 0.6931747794 0.6458414197 - 0.6864004731 0.6940047741 0.6467201710 - 0.6872066855 0.6948273182 0.6475932002 - 0.6880130172 0.6956499219 0.6484662294 - 0.6888151765 0.6964679956 0.6493358016 - 0.6896142960 0.6972820163 0.6502025723 - 0.6904134154 0.6980959177 0.6510692835 - 0.6912078261 0.6989029050 0.6519299746 - 0.6920022964 0.6997095942 0.6527906060 - 0.6927950978 0.7005147934 0.6536483169 - 0.6935858130 0.7013174891 0.6545016766 - 0.6943765283 0.7021203041 0.6553550959 - 0.6951621175 0.7029188275 0.6562039852 - 0.6959465146 0.7037159801 0.6570516229 - 0.6967297792 0.7045122981 0.6578981876 - 0.6975088120 0.7053027749 0.6587392092 - 0.6982877254 0.7060933113 0.6595801711 - 0.6990643144 0.7068814039 0.6604182720 - 0.6998388171 0.7076671124 0.6612536907 - 0.7006133199 0.7084527016 0.6620889902 - 0.7013818026 0.7092331052 0.6629180908 - 0.7021492720 0.7100123167 0.6637459993 - 0.7029157877 0.7107905746 0.6645727754 - 0.7036771774 0.7115622759 0.6653935909 - 0.7044386268 0.7123339772 0.6662142873 - 0.7051976919 0.7131032944 0.6670325994 - 0.7059540153 0.7138692141 0.6678475738 - 0.7067102790 0.7146351933 0.6686627269 - 0.7074630260 0.7153962851 0.6694738865 - 0.7082142234 0.7161549926 0.6702834964 - 0.7089654803 0.7169137001 0.6710931063 - 0.7097116709 0.7176669836 0.6718968153 - 0.7104576826 0.7184197903 0.6727002263 - 0.7112026811 0.7191722989 0.6735023856 - 0.7119430900 0.7199227214 0.6742987037 - 0.7126836181 0.7206730843 0.6750949025 - 0.7134215236 0.7214214206 0.6758887768 - 0.7141550779 0.7221655250 0.6766782999 - 0.7148886919 0.7229096293 0.6774678230 - 0.7156193852 0.7236497998 0.6782538295 - 0.7163475752 0.7243860960 0.6790366769 - 0.7170758247 0.7251223922 0.6798195839 - 0.7178009748 0.7258551717 0.6805974841 - 0.7185249925 0.7265861034 0.6813730001 - 0.7192488909 0.7273170948 0.6821485162 - 0.7199671268 0.7280430198 0.6829196215 - 0.7206841111 0.7287673950 0.6836894155 - 0.7214009762 0.7294918895 0.6844593287 - 0.7221128941 0.7302092910 0.6852232814 - 0.7228242755 0.7309256792 0.6859866977 - 0.7235357165 0.7316421866 0.6867499948 - 0.7242426872 0.7323533297 0.6875082850 - 0.7249497175 0.7330641747 0.6882663965 - 0.7256563902 0.7337750793 0.6890243888 - 0.7263563275 0.7344793081 0.6897757053 - 0.7270562053 0.7351834774 0.6905270219 - 0.7277556062 0.7358874083 0.6912779808 - 0.7284498811 0.7365834117 0.6920228004 - 0.7291440964 0.7372794151 0.6927676201 - 0.7298380136 0.7379751801 0.6935120821 - 0.7305275798 0.7386654019 0.6942508817 - 0.7312173247 0.7393556833 0.6949896812 - 0.7319065928 0.7400457859 0.6957281828 - 0.7325891256 0.7407314777 0.6964604259 - 0.7332715988 0.7414172292 0.6971926093 - 0.7339540124 0.7421028018 0.6979246736 - 0.7346293926 0.7427842021 0.6986520290 - 0.7353047729 0.7434653044 0.6993792057 - 0.7359800935 0.7441465259 0.7001065016 - 0.7366490960 0.7448238134 0.7008286715 - 0.7373176217 0.7455006838 0.7015504241 - 0.7379860878 0.7461774945 0.7022721171 - 0.7386487722 0.7468507886 0.7029896975 - 0.7393102050 0.7475231886 0.7037063241 - 0.7399716973 0.7481955886 0.7044230103 - 0.7406283021 0.7488650084 0.7051362991 - 0.7412828207 0.7495329976 0.7058482170 - 0.7419373989 0.7502009869 0.7065600157 - 0.7425882220 0.7508667707 0.7072690129 - 0.7432361841 0.7515305877 0.7079756260 - 0.7438840866 0.7521942854 0.7086821795 - 0.7445294261 0.7528566122 0.7093874216 - 0.7451704741 0.7535158992 0.7100903988 - 0.7458115220 0.7541753054 0.7107933760 - 0.7464513183 0.7548338175 0.7114953995 - 0.7470856905 0.7554876804 0.7121930122 - 0.7477200031 0.7561414838 0.7128906250 - 0.7483543158 0.7567954063 0.7135881782 - 0.7489826083 0.7574434876 0.7142792940 - 0.7496104836 0.7580910921 0.7149698138 - 0.7502384186 0.7587388158 0.7156603932 - 0.7508633137 0.7593836188 0.7163473964 - 0.7514868975 0.7600269914 0.7170326114 - 0.7521104217 0.7606704235 0.7177178264 - 0.7527319193 0.7613118291 0.7184010744 - 0.7533503771 0.7619500160 0.7190815210 - 0.7539688945 0.7625880837 0.7197619081 - 0.7545869946 0.7632259130 0.7204418182 - 0.7552015185 0.7638580203 0.7211170197 - 0.7558159828 0.7644901872 0.7217922807 - 0.7564306259 0.7651224136 0.7224674821 - 0.7570430040 0.7657516003 0.7231383920 - 0.7576547265 0.7663797736 0.7238079906 - 0.7582663894 0.7670078874 0.7244775295 - 0.7588763237 0.7676342130 0.7251446843 - 0.7594832778 0.7682573199 0.7258080840 - 0.7600904107 0.7688804269 0.7264714837 - 0.7606974244 0.7695034742 0.7271348238 - 0.7613009214 0.7701197863 0.7277917266 - 0.7619044185 0.7707359791 0.7284486294 - 0.7625079155 0.7713521719 0.7291054130 - 0.7631099820 0.7719650865 0.7297595143 - 0.7637109756 0.7725753188 0.7304114103 - 0.7643120885 0.7731856108 0.7310634255 - 0.7649124265 0.7737953067 0.7317147851 - 0.7655079961 0.7744010091 0.7323616743 - 0.7661035061 0.7750065923 0.7330086827 - 0.7666990757 0.7756121755 0.7336556911 - 0.7672913074 0.7762138247 0.7342997193 - 0.7678816915 0.7768129110 0.7349420190 - 0.7684720755 0.7774121165 0.7355843782 - 0.7690622211 0.7780106068 0.7362260818 - 0.7696506977 0.7786043286 0.7368643284 - 0.7702391148 0.7791979909 0.7375025153 - 0.7708275914 0.7797915936 0.7381407022 - 0.7714130878 0.7803828716 0.7387762070 - 0.7719966769 0.7809723020 0.7394099236 - 0.7725803256 0.7815617919 0.7400435805 - 0.7731636167 0.7821511030 0.7406771779 - 0.7737426162 0.7827333808 0.7413080931 - 0.7743214965 0.7833157182 0.7419390082 - 0.7749004960 0.7838979959 0.7425699234 - 0.7754787803 0.7844783068 0.7431989908 - 0.7760564089 0.7850558758 0.7438256741 - 0.7766339779 0.7856335044 0.7444524765 - 0.7772114873 0.7862110734 0.7450792193 - 0.7777854800 0.7867867947 0.7457013726 - 0.7783586979 0.7873620987 0.7463225126 - 0.7789319158 0.7879372835 0.7469435930 - 0.7795041800 0.7885116935 0.7475640774 - 0.7800722718 0.7890805006 0.7481812835 - 0.7806403041 0.7896494269 0.7487984896 - 0.7812082767 0.7902181745 0.7494155765 - 0.7817749977 0.7907847166 0.7500311732 - 0.7823402286 0.7913483977 0.7506451011 - 0.7829053998 0.7919120193 0.7512589097 - 0.7834705114 0.7924757004 0.7518727779 - 0.7840328813 0.7930366993 0.7524824739 - 0.7845944762 0.7935966849 0.7530907989 - 0.7851560116 0.7941566706 0.7536991239 - 0.7857174277 0.7947167158 0.7543073297 - 0.7862744927 0.7952712178 0.7549107075 - 0.7868316174 0.7958257794 0.7555140853 - 0.7873886824 0.7963802814 0.7561174035 - 0.7879452109 0.7969337106 0.7567198277 - 0.7885001898 0.7974827886 0.7573189139 - 0.7890551090 0.7980319858 0.7579178810 - 0.7896100283 0.7985811234 0.7585170269 - 0.7901636958 0.7991284132 0.7591142058 - 0.7907155752 0.7996730208 0.7597088218 - 0.7912675738 0.8002175093 0.7603033781 - 0.7918195128 0.8007619977 0.7608978748 - 0.7923693061 0.8013039827 0.7614898086 - 0.7929174900 0.8018441200 0.7620797753 - 0.7934656739 0.8023841977 0.7626696825 - 0.7940139174 0.8029242754 0.7632595897 - 0.7945610285 0.8034613729 0.7638459802 - 0.7951077223 0.8039970994 0.7644308209 - 0.7956544161 0.8045328259 0.7650157213 - 0.7962012291 0.8050686121 0.7656006217 - 0.7967458963 0.8056014776 0.7661811709 - 0.7972903252 0.8061336279 0.7667608261 - 0.7978346944 0.8066657186 0.7673404217 - 0.7983791232 0.8071978092 0.7679200172 - 0.7989180088 0.8077269793 0.7684965134 - 0.7994561791 0.8082556725 0.7690725923 - 0.7999945283 0.8087844253 0.7696486115 - 0.8005326986 0.8093131185 0.7702246904 - 0.8010672927 0.8098369837 0.7707965970 - 0.8016015887 0.8103603125 0.7713679075 - 0.8021358848 0.8108835816 0.7719392776 - 0.8026701808 0.8114069104 0.7725105882 - 0.8032029867 0.8119258285 0.7730767131 - 0.8037356138 0.8124440908 0.7736421227 - 0.8042681813 0.8129622936 0.7742075920 - 0.8048008084 0.8134806156 0.7747730017 - 0.8053286076 0.8139951825 0.7753348947 - 0.8058556914 0.8145089746 0.7758960724 - 0.8063827753 0.8150228262 0.7764574289 - 0.8069099188 0.8155366182 0.7770186067 - 0.8074336052 0.8160470128 0.7775772214 - 0.8079562783 0.8165563941 0.7781350017 - 0.8084790707 0.8170658946 0.7786927819 - 0.8090019226 0.8175752759 0.7792506218 - 0.8095226288 0.8180813193 0.7798048258 - 0.8100423813 0.8185856938 0.7803573012 - 0.8105623126 0.8190901279 0.7809098959 - 0.8110821247 0.8195945024 0.7814623713 - 0.8115993142 0.8200960755 0.7820122838 - 0.8121144772 0.8205952048 0.7825602293 - 0.8126296997 0.8210942745 0.7831079960 - 0.8131449819 0.8215934038 0.7836558223 - 0.8136587739 0.8220912814 0.7842022181 - 0.8141704798 0.8225870728 0.7847461104 - 0.8146821260 0.8230829239 0.7852900028 - 0.8151937723 0.8235787153 0.7858338952 - 0.8157051206 0.8240739107 0.7863767743 - 0.8162146807 0.8245658278 0.7869150043 - 0.8167243004 0.8250576854 0.7874531150 - 0.8172338009 0.8255496025 0.7879912853 - 0.8177434206 0.8260415196 0.7885295153 - 0.8182494044 0.8265284896 0.7890629172 - 0.8187550902 0.8270152211 0.7895959020 - 0.8192608953 0.8275017738 0.7901288867 - 0.8197665811 0.8279885054 0.7906618714 - 0.8202685714 0.8284724951 0.7911921144 - 0.8207687140 0.8289551139 0.7917206883 - 0.8212687969 0.8294377923 0.7922493219 - 0.8217688799 0.8299204111 0.7927778959 - 0.8222674727 0.8304020762 0.7933050990 - 0.8227633834 0.8308820724 0.7938293219 - 0.8232592940 0.8313620090 0.7943534851 - 0.8237550855 0.8318418860 0.7948777080 - 0.8242509961 0.8323218822 0.7954018712 - 0.8247438073 0.8327968717 0.7959234715 - 0.8252366185 0.8332718015 0.7964450121 - 0.8257294297 0.8337466717 0.7969664931 - 0.8262221813 0.8342216015 0.7974880934 - 0.8267118931 0.8346939087 0.7980074286 - 0.8271998763 0.8351643085 0.7985252738 - 0.8276877999 0.8356347084 0.7990432978 - 0.8281757832 0.8361051083 0.7995612025 - 0.8286628127 0.8365753293 0.8000785112 - 0.8291462064 0.8370444179 0.8005927205 - 0.8296297193 0.8375133872 0.8011069894 - 0.8301131129 0.8379824162 0.8016213179 - 0.8305966258 0.8384515047 0.8021355867 - 0.8310769796 0.8389179111 0.8026489019 - 0.8315563798 0.8393831849 0.8031619191 - 0.8320357800 0.8398485780 0.8036748767 - 0.8325151801 0.8403139114 0.8041878939 - 0.8329936266 0.8407778740 0.8047001958 - 0.8334690928 0.8412373066 0.8052101731 - 0.8339444995 0.8416966200 0.8057203293 - 0.8344200253 0.8421559930 0.8062303066 - 0.8348953724 0.8426154256 0.8067402840 - 0.8353682756 0.8430727124 0.8072487116 - 0.8358402252 0.8435291052 0.8077564240 - 0.8363121152 0.8439856172 0.8082640767 - 0.8367841244 0.8444420099 0.8087717295 - 0.8372554779 0.8448981047 0.8092792034 - 0.8377242088 0.8453521729 0.8097851276 - 0.8381929994 0.8458061814 0.8102909923 - 0.8386617899 0.8462603092 0.8107969165 - 0.8391305208 0.8467143774 0.8113027811 - 0.8395975828 0.8471655250 0.8118069172 - 0.8400632739 0.8476139903 0.8123093247 - 0.8405290246 0.8480623960 0.8128117919 - 0.8409947157 0.8485109210 0.8133143187 - 0.8414604068 0.8489593863 0.8138167858 - 0.8419228196 0.8494048715 0.8143168092 - 0.8423848748 0.8498501182 0.8148164153 - 0.8428469896 0.8502953053 0.8153160214 - 0.8433091044 0.8507404923 0.8158156872 - 0.8437703252 0.8511855006 0.8163148761 - 0.8442285061 0.8516296744 0.8168122768 - 0.8446866274 0.8520739079 0.8173096180 - 0.8451448083 0.8525180817 0.8178070188 - 0.8456029892 0.8529623151 0.8183044195 - 0.8460600972 0.8534045219 0.8187999129 - 0.8465161920 0.8538445234 0.8192933798 - 0.8469722867 0.8542845249 0.8197870255 - 0.8474283814 0.8547245860 0.8202804923 - 0.8478844166 0.8551645875 0.8207740188 - 0.8483386040 0.8556007147 0.8212651014 - 0.8487920165 0.8560351133 0.8217552900 - 0.8492454290 0.8564695120 0.8222454786 - 0.8496989012 0.8569039702 0.8227357268 - 0.8501523137 0.8573384285 0.8232259154 - 0.8506020904 0.8577691913 0.8237136006 - 0.8510516286 0.8581997156 0.8242009878 - 0.8515011072 0.8586301804 0.8246884942 - 0.8519507051 0.8590605855 0.8251760006 - 0.8523998260 0.8594908714 0.8256632090 - 0.8528448939 0.8599187136 0.8261479735 - 0.8532899022 0.8603464961 0.8266327977 - 0.8537350297 0.8607742786 0.8271176219 - 0.8541800976 0.8612021208 0.8276023865 - 0.8546246290 0.8616291285 0.8280869722 - 0.8550670743 0.8620529175 0.8285707831 - 0.8555095792 0.8624767065 0.8290544748 - 0.8559520245 0.8629004955 0.8295382261 - 0.8563945293 0.8633242249 0.8300219178 - 0.8568360806 0.8637471199 0.8305050731 - 0.8572757244 0.8641673923 0.8309867978 - 0.8577153087 0.8645877838 0.8314685225 - 0.8581547737 0.8650081158 0.8319501877 - 0.8585944176 0.8654283881 0.8324319124 - 0.8590329289 0.8658478856 0.8329126835 - 0.8594689965 0.8662654161 0.8333914876 - 0.8599051237 0.8666828871 0.8338704109 - 0.8603413105 0.8671004176 0.8343492150 - 0.8607773781 0.8675178885 0.8348280191 - 0.8612130284 0.8679342270 0.8353064060 - 0.8616474867 0.8683475852 0.8357838988 - 0.8620818853 0.8687611222 0.8362613916 - 0.8625164032 0.8691744804 0.8367388844 - 0.8629509211 0.8695878983 0.8372163773 - 0.8633844852 0.8700003028 0.8376932740 - 0.8638160825 0.8704100251 0.8381686211 - 0.8642476797 0.8708196878 0.8386437893 - 0.8646792769 0.8712294102 0.8391190767 - 0.8651108742 0.8716390729 0.8395944238 - 0.8655416965 0.8720481992 0.8400691152 - 0.8659693003 0.8724551201 0.8405413032 - 0.8663967848 0.8728619218 0.8410134912 - 0.8668243885 0.8732687235 0.8414856791 - 0.8672518730 0.8736755252 0.8419578075 - 0.8676794171 0.8740822077 0.8424299955 - 0.8681045771 0.8744865060 0.8429005742 - 0.8685297966 0.8748908043 0.8433712125 - 0.8689550161 0.8752951026 0.8438417912 - 0.8693801761 0.8756992817 0.8443124890 - 0.8698053956 0.8761035800 0.8447831273 - 0.8702281117 0.8765046000 0.8452516198 - 0.8706504107 0.8769050241 0.8457198143 - 0.8710728288 0.8773053885 0.8461880088 - 0.8714951277 0.8777058125 0.8466562033 - 0.8719174862 0.8781061769 0.8471243978 - 0.8723371029 0.8785045147 0.8475905061 - 0.8727555275 0.8789016008 0.8480554223 - 0.8731737733 0.8792986870 0.8485202789 - 0.8735920787 0.8796957731 0.8489853144 - 0.8740105033 0.8800929785 0.8494501710 - 0.8744276166 0.8804891109 0.8499143720 - 0.8748430014 0.8808835745 0.8503776193 - 0.8752583861 0.8812779784 0.8508408070 - 0.8756737709 0.8816725016 0.8513038754 - 0.8760892153 0.8820670247 0.8517671227 - 0.8765044212 0.8824610710 0.8522300124 - 0.8769180179 0.8828517199 0.8526905179 - 0.8773316145 0.8832423091 0.8531510830 - 0.8777452111 0.8836328983 0.8536115885 - 0.8781588078 0.8840234280 0.8540722132 - 0.8785724044 0.8844140172 0.8545327187 - 0.8789830208 0.8848019242 0.8549907207 - 0.8793928027 0.8851888776 0.8554478288 - 0.8798025250 0.8855758905 0.8559048772 - 0.8802123070 0.8859630227 0.8563619852 - 0.8806220293 0.8863499761 0.8568190932 - 0.8810304999 0.8867359757 0.8572754264 - 0.8814368844 0.8871203065 0.8577305079 - 0.8818433285 0.8875045180 0.8581857085 - 0.8822497725 0.8878887892 0.8586407900 - 0.8826562166 0.8882731199 0.8590958714 - 0.8830626011 0.8886573911 0.8595510125 - 0.8834682703 0.8890393972 0.8600043058 - 0.8838739991 0.8894212842 0.8604574800 - 0.8842797279 0.8898031712 0.8609105945 - 0.8846853971 0.8901851177 0.8613638282 - 0.8850911260 0.8905668855 0.8618170023 - 0.8854956031 0.8909478784 0.8622695208 - 0.8858984709 0.8913276792 0.8627213240 - 0.8863015175 0.8917074800 0.8631731272 - 0.8867045045 0.8920872808 0.8636248708 - 0.8871074915 0.8924670815 0.8640766740 - 0.8875104785 0.8928470016 0.8645284772 - 0.8879091144 0.8932240009 0.8649795055 - 0.8883073926 0.8936008811 0.8654304147 - 0.8887057900 0.8939777017 0.8658813238 - 0.8891041279 0.8943545818 0.8663321733 - 0.8895024061 0.8947314024 0.8667832017 - 0.8899000287 0.8951069117 0.8672332168 - 0.8902962208 0.8954799175 0.8676818013 - 0.8906924725 0.8958529830 0.8681303263 - 0.8910887241 0.8962259889 0.8685787916 - 0.8914849162 0.8965989947 0.8690273166 - 0.8918812275 0.8969720006 0.8694759011 - 0.8922762275 0.8973435163 0.8699231148 - 0.8926709294 0.8977146149 0.8703697920 - 0.8930655122 0.8980855942 0.8708165288 - 0.8934602141 0.8984566927 0.8712632060 - 0.8938547969 0.8988276720 0.8717098832 - 0.8942492008 0.8991985917 0.8721565008 - 0.8946403265 0.8995674253 0.8726019263 - 0.8950313926 0.8999361992 0.8730471730 - 0.8954225779 0.9003049731 0.8734925985 - 0.8958137035 0.9006738067 0.8739380240 - 0.8962048292 0.9010425806 0.8743833899 - 0.8965950012 0.9014102817 0.8748276830 - 0.8969836235 0.9017760158 0.8752703071 - 0.8973723054 0.9021416903 0.8757128716 - 0.8977609277 0.9025074244 0.8761554956 - 0.8981494904 0.9028730989 0.8765981197 - 0.8985381126 0.9032387733 0.8770406246 - 0.8989257812 0.9036024809 0.8774818182 - 0.8993129730 0.9039648175 0.8779221773 - 0.8997001052 0.9043270946 0.8783624768 - 0.9000871778 0.9046893716 0.8788027763 - 0.9004744291 0.9050518274 0.8792430758 - 0.9008615017 0.9054141045 0.8796833754 - 0.9012458920 0.9057751894 0.8801223040 - 0.9016296864 0.9061359763 0.8805608749 - 0.9020134211 0.9064968824 0.8809995055 - 0.9023972154 0.9068577290 0.8814380169 - 0.9027810097 0.9072185159 0.8818765879 - 0.9031646848 0.9075793028 0.8823152184 - 0.9035456777 0.9079378843 0.8827506900 - 0.9039264917 0.9082962871 0.8831861019 - 0.9043073058 0.9086548090 0.8836215138 - 0.9046881795 0.9090132117 0.8840568066 - 0.9050689936 0.9093716741 0.8844922185 - 0.9054498076 0.9097300172 0.8849275112 - 0.9058302045 0.9100852013 0.8853601813 - 0.9062106013 0.9104403853 0.8857929111 - 0.9065908790 0.9107956290 0.8862255812 - 0.9069712758 0.9111508727 0.8866581917 - 0.9073516726 0.9115061164 0.8870909214 - 0.9077318907 0.9118611813 0.8875234127 - 0.9081099033 0.9122149944 0.8879539967 - 0.9084879756 0.9125688076 0.8883845210 - 0.9088661075 0.9129226208 0.8888149858 - 0.9092441201 0.9132763743 0.8892456293 - 0.9096221924 0.9136301875 0.8896760941 - 0.9099997878 0.9139838219 0.8901063204 - 0.9103738070 0.9143360257 0.8905333281 - 0.9107477069 0.9146881104 0.8909602761 - 0.9111216068 0.9150401950 0.8913872838 - 0.9114955068 0.9153922796 0.8918141723 - 0.9118694067 0.9157444239 0.8922411799 - 0.9122431874 0.9160963893 0.8926680088 - 0.9126154184 0.9164451957 0.8930919170 - 0.9129875898 0.9167940021 0.8935158253 - 0.9133598208 0.9171428084 0.8939396739 - 0.9137319922 0.9174916148 0.8943635821 - 0.9141042233 0.9178404212 0.8947874904 - 0.9144763947 0.9181892276 0.8952113986 - 0.9148473740 0.9185342193 0.8956335783 - 0.9152182937 0.9188787937 0.8960558176 - 0.9155892134 0.9192234874 0.8964778781 - 0.9159600735 0.9195681214 0.8969001174 - 0.9163309932 0.9199128151 0.8973221779 - 0.9167019725 0.9202573895 0.8977444172 - 0.9170697927 0.9206014276 0.8981651068 - 0.9174367785 0.9209452271 0.8985853791 - 0.9178038836 0.9212889075 0.8990057111 - 0.9181709290 0.9216327071 0.8994259834 - 0.9185379148 0.9219765067 0.8998463750 - 0.9189050198 0.9223201871 0.9002667069 - 0.9192699194 0.9226629734 0.9006865025 - 0.9196335077 0.9230049849 0.9011059999 - 0.9199969769 0.9233471155 0.9015253782 - 0.9203606248 0.9236891270 0.9019448757 - 0.9207240939 0.9240310788 0.9023643732 - 0.9210876226 0.9243732095 0.9027838111 - 0.9214507937 0.9247142076 0.9032027721 - 0.9218133092 0.9250528812 0.9036207795 - 0.9221758246 0.9253916144 0.9040387273 - 0.9225382209 0.9257302880 0.9044566751 - 0.9229006767 0.9260690212 0.9048746824 - 0.9232631922 0.9264076948 0.9052925706 - 0.9236255884 0.9267463088 0.9057105780 - 0.9239851832 0.9270828962 0.9061275721 - 0.9243447781 0.9274194837 0.9065446854 - 0.9247043729 0.9277560115 0.9069616795 - 0.9250640273 0.9280925989 0.9073787928 - 0.9254236221 0.9284291863 0.9077957869 - 0.9257832170 0.9287657142 0.9082127810 - 0.9261407256 0.9291017056 0.9086294770 - 0.9264971018 0.9294372797 0.9090461135 - 0.9268534780 0.9297729135 0.9094625711 - 0.9272099137 0.9301084876 0.9098792076 - 0.9275662899 0.9304441214 0.9102957249 - 0.9279227257 0.9307796955 0.9107123017 - 0.9282786846 0.9311146736 0.9111282825 - 0.9286336899 0.9314478040 0.9115430117 - 0.9289886951 0.9317808747 0.9119576216 - 0.9293437004 0.9321138859 0.9123722911 - 0.9296985865 0.9324470162 0.9127869010 - 0.9300535917 0.9327800870 0.9132015705 - 0.9304085970 0.9331132174 0.9136161804 - 0.9307621717 0.9334436059 0.9140297771 - 0.9311155081 0.9337733984 0.9144430757 - 0.9314687252 0.9341030717 0.9148563743 - 0.9318218827 0.9344329238 0.9152696729 - 0.9321752191 0.9347625971 0.9156829715 - 0.9325283766 0.9350923896 0.9160963297 - 0.9328805208 0.9354220033 0.9165095091 - 0.9332295060 0.9357516170 0.9169226885 - 0.9335784912 0.9360811114 0.9173358083 - 0.9339274764 0.9364106059 0.9177489877 - 0.9342765212 0.9367401004 0.9181622267 - 0.9346253872 0.9370697141 0.9185752869 - 0.9349743724 0.9373992085 0.9189885259 - 0.9353216887 0.9377279878 0.9194009900 - 0.9356681705 0.9380565286 0.9198130965 - 0.9360147715 0.9383850098 0.9202253222 - 0.9363613129 0.9387134910 0.9206374884 - 0.9367079139 0.9390419722 0.9210495949 - 0.9370543957 0.9393705130 0.9214618206 - 0.9374008775 0.9396988153 0.9218738079 - 0.9377468228 0.9400240183 0.9222841263 - 0.9380925894 0.9403492212 0.9226943851 - 0.9384384751 0.9406743050 0.9231047034 - 0.9387843013 0.9409995079 0.9235150218 - 0.9391301870 0.9413247108 0.9239252806 - 0.9394760728 0.9416499138 0.9243355989 - 0.9398207068 0.9419735074 0.9247456193 - 0.9401633739 0.9422945976 0.9251552224 - 0.9405061007 0.9426156878 0.9255648851 - 0.9408488274 0.9429367781 0.9259744883 - 0.9411916137 0.9432579875 0.9263840914 - 0.9415342808 0.9435790777 0.9267938137 - 0.9418770075 0.9439002275 0.9272034168 - 0.9422174096 0.9442206025 0.9276124835 - 0.9425566196 0.9445406795 0.9280213118 - 0.9428958297 0.9448608160 0.9284300804 - 0.9432349801 0.9451808929 0.9288390279 - 0.9435741901 0.9455009103 0.9292477965 - 0.9439134002 0.9458209872 0.9296566248 - 0.9442526102 0.9461411238 0.9300655127 - 0.9445902705 0.9464595914 0.9304733276 - 0.9449278116 0.9467778802 0.9308810234 - 0.9452651739 0.9470962286 0.9312887192 - 0.9456027150 0.9474143982 0.9316964149 - 0.9459400773 0.9477326870 0.9321041107 - 0.9462776184 0.9480509758 0.9325118065 - 0.9466149807 0.9483692050 0.9329195023 - 0.9469503760 0.9486842155 0.9333260059 - 0.9472857118 0.9489991069 0.9337323904 - 0.9476211071 0.9493141174 0.9341388941 - 0.9479565024 0.9496291280 0.9345452785 - 0.9482918978 0.9499440193 0.9349517822 - 0.9486272931 0.9502589703 0.9353582263 - 0.9489622712 0.9505736828 0.9357644916 - 0.9492946267 0.9508864880 0.9361696243 - 0.9496268034 0.9511991739 0.9365745783 - 0.9499590993 0.9515119791 0.9369797111 - 0.9502912760 0.9518247843 0.9373847842 - 0.9506235719 0.9521374702 0.9377897978 - 0.9509558082 0.9524502754 0.9381948709 - 0.9512878060 0.9527629018 0.9385995269 - 0.9516180754 0.9530745149 0.9390019178 - 0.9519485235 0.9533861876 0.9394041896 - 0.9522787929 0.9536978006 0.9398065209 - 0.9526091218 0.9540094733 0.9402087927 - 0.9529393911 0.9543210864 0.9406111836 - 0.9532697797 0.9546328187 0.9410135150 - 0.9535998702 0.9549441934 0.9414151907 - 0.9539285898 0.9552533031 0.9418125749 - 0.9542574286 0.9555622935 0.9422100186 - 0.9545860887 0.9558714032 0.9426074028 - 0.9549148083 0.9561805129 0.9430047870 - 0.9552435875 0.9564896226 0.9434021711 - 0.9555723071 0.9567986727 0.9437994957 - 0.9559007883 0.9571076035 0.9441967010 - 0.9562236071 0.9574112892 0.9445887208 - 0.9565463066 0.9577149749 0.9449806213 - 0.9568691254 0.9580187798 0.9453725219 - 0.9571918845 0.9583225250 0.9457644224 - 0.9575145841 0.9586262107 0.9461563230 - 0.9578374028 0.9589298964 0.9465482235 - 0.9581602216 0.9592335820 0.9469401240 - 0.9584769011 0.9595324993 0.9473283291 - 0.9587929249 0.9598308802 0.9477159977 - 0.9591090083 0.9601292014 0.9481037259 - 0.9594249725 0.9604275823 0.9484915137 - 0.9597409964 0.9607260227 0.9488791823 - 0.9600570798 0.9610242844 0.9492670298 - 0.9603731036 0.9613227248 0.9496546984 - 0.9606860280 0.9616187215 0.9500409961 - 0.9609975219 0.9619138241 0.9504266977 - 0.9613090158 0.9622088075 0.9508125186 - 0.9616205096 0.9625037909 0.9511982203 - 0.9619320035 0.9627988935 0.9515839219 - 0.9622434974 0.9630938768 0.9519696236 - 0.9625549912 0.9633889198 0.9523553848 - 0.9628651738 0.9636827707 0.9527397752 - 0.9631738067 0.9639751911 0.9531226158 - 0.9634823203 0.9642676115 0.9535053968 - 0.9637908936 0.9645599127 0.9538881183 - 0.9640994072 0.9648522735 0.9542708993 - 0.9644079804 0.9651446939 0.9546536803 - 0.9647164941 0.9654371142 0.9550365210 - 0.9650247097 0.9657291174 0.9554188848 - 0.9653298855 0.9660187960 0.9557983279 - 0.9656351805 0.9663084149 0.9561777711 - 0.9659404755 0.9665980935 0.9565572739 - 0.9662457705 0.9668877721 0.9569367170 - 0.9665510058 0.9671773911 0.9573162198 - 0.9668563008 0.9674671292 0.9576957226 - 0.9671615958 0.9677568078 0.9580751061 - 0.9674646854 0.9680448771 0.9584522843 - 0.9677671194 0.9683324099 0.9588286877 - 0.9680693746 0.9686200023 0.9592050910 - 0.9683718085 0.9689074755 0.9595814943 - 0.9686741829 0.9691951275 0.9599578977 - 0.9689764977 0.9694827199 0.9603341818 - 0.9692788720 0.9697701931 0.9607105851 - 0.9695801735 0.9700570107 0.9610859156 - 0.9698789120 0.9703419209 0.9614585042 - 0.9701775908 0.9706267715 0.9618310928 - 0.9704763889 0.9709116817 0.9622036815 - 0.9707751274 0.9711965919 0.9625763297 - 0.9710738063 0.9714813828 0.9629489183 - 0.9713726044 0.9717662930 0.9633215070 - 0.9716712832 0.9720512033 0.9636940956 - 0.9719662070 0.9723331928 0.9640635252 - 0.9722601175 0.9726145267 0.9644321799 - 0.9725540876 0.9728958011 0.9648008943 - 0.9728481174 0.9731770754 0.9651694894 - 0.9731420279 0.9734582901 0.9655382037 - 0.9734359980 0.9737396240 0.9659067988 - 0.9737300277 0.9740208983 0.9662755132 - 0.9740229845 0.9743015170 0.9666432738 - 0.9743129015 0.9745799899 0.9670084119 - 0.9746028185 0.9748585820 0.9673733711 - 0.9748927951 0.9751371145 0.9677385092 - 0.9751827121 0.9754155874 0.9681035280 - 0.9754726887 0.9756941199 0.9684686065 - 0.9757626057 0.9759727120 0.9688336253 - 0.9760525823 0.9762511849 0.9691987038 - 0.9763398170 0.9765279293 0.9695615172 - 0.9766253829 0.9768033028 0.9699230194 - 0.9769108891 0.9770787954 0.9702845216 - 0.9771965146 0.9773542285 0.9706460238 - 0.9774820209 0.9776297212 0.9710075259 - 0.9777675867 0.9779050946 0.9713690281 - 0.9780532122 0.9781805873 0.9717304707 - 0.9783387184 0.9784560204 0.9720919132 - 0.9786192775 0.9787276983 0.9724497199 - 0.9788995981 0.9789990783 0.9728072286 - 0.9791797996 0.9792703986 0.9731646776 - 0.9794600010 0.9795417786 0.9735221267 - 0.9797403216 0.9798130989 0.9738795757 - 0.9800205231 0.9800844789 0.9742370844 - 0.9803007245 0.9803557992 0.9745944738 - 0.9805799723 0.9806264043 0.9749512076 - 0.9808548093 0.9808933139 0.9753037095 - 0.9811295867 0.9811602235 0.9756562710 - 0.9814044237 0.9814270735 0.9760088921 - 0.9816792011 0.9816939831 0.9763615131 - 0.9819539785 0.9819608927 0.9767140150 - 0.9822288752 0.9822278023 0.9770665765 - 0.9825037122 0.9824947119 0.9774191976 - 0.9827765226 0.9827601910 0.9777697921 - 0.9830456972 0.9830228090 0.9781169295 - 0.9833148718 0.9832854867 0.9784640074 - 0.9835842252 0.9835482240 0.9788110256 - 0.9838533998 0.9838109016 0.9791581035 - 0.9841225743 0.9840735197 0.9795051813 - 0.9843918085 0.9843361974 0.9798521996 - 0.9846609831 0.9845988750 0.9801992774 - 0.9849274158 0.9848592281 0.9805430770 - 0.9851905704 0.9851167798 0.9808833003 - 0.9854537249 0.9853743911 0.9812235236 - 0.9857168794 0.9856320024 0.9815636277 - 0.9859799743 0.9858896136 0.9819037914 - 0.9862431884 0.9861472249 0.9822440147 - 0.9865062833 0.9864048958 0.9825841188 - 0.9867694974 0.9866625071 0.9829242826 - 0.9870293140 0.9869173169 0.9832602739 - 0.9872860909 0.9871696234 0.9835925102 - 0.9875429273 0.9874218702 0.9839246869 - 0.9877997041 0.9876741767 0.9842569232 - 0.9880564809 0.9879264832 0.9845890999 - 0.9883133173 0.9881787896 0.9849212766 - 0.9885700941 0.9884310961 0.9852535129 - 0.9888268709 0.9886834025 0.9855856895 - 0.9890797734 0.9889330268 0.9859133959 - 0.9893288016 0.9891800284 0.9862366915 - 0.9895778894 0.9894269109 0.9865599871 - 0.9898269176 0.9896739125 0.9868832827 - 0.9900760055 0.9899207950 0.9872065783 - 0.9903249741 0.9901676774 0.9875298738 - 0.9905740023 0.9904146791 0.9878532290 - 0.9908230901 0.9906616211 0.9881765246 - 0.9910683036 0.9909055829 0.9884948134 - 0.9913083911 0.9911454916 0.9888063073 - 0.9915484786 0.9913855195 0.9891179204 - 0.9917885065 0.9916254282 0.9894294143 - 0.9920285940 0.9918652773 0.9897409081 - 0.9922686219 0.9921053052 0.9900524020 - 0.9925087094 0.9923452139 0.9903638959 - 0.9927487969 0.9925851822 0.9906755090 - 0.9929866791 0.9928228855 0.9909834266 - 0.9932188988 0.9930546284 0.9912818074 - 0.9934511185 0.9932863712 0.9915801883 - 0.9936832786 0.9935181141 0.9918786883 - 0.9939156175 0.9937499166 0.9921771288 - 0.9941477776 0.9939817190 0.9924755096 - 0.9943799973 0.9942134023 0.9927738905 - 0.9946122169 0.9944452047 0.9930723906 - 0.9948437810 0.9946765900 0.9933699965 - 0.9950650930 0.9949020743 0.9936543703 - 0.9952864051 0.9951274991 0.9939388037 - 0.9955077171 0.9953529835 0.9942231178 - 0.9957290292 0.9955784082 0.9945074916 - 0.9959502816 0.9958038926 0.9947919250 - 0.9961715937 0.9960293770 0.9950762987 - 0.9963929057 0.9962548018 0.9953606725 - 0.9966142178 0.9964802861 0.9956451058 - 0.9968237877 0.9966976047 0.9959148765 - 0.9970299006 0.9969125986 0.9961804748 - 0.9972360730 0.9971275926 0.9964460731 - 0.9974421859 0.9973425865 0.9967116117 - 0.9976484179 0.9975575209 0.9969772100 - 0.9978544712 0.9977725148 0.9972428083 - 0.9980607033 0.9979875088 0.9975084066 - 0.9982668161 0.9982025027 0.9977738857 - 0.9984667897 0.9984105825 0.9980313182 - 0.9986584783 0.9986093044 0.9982774258 - 0.9988501072 0.9988079071 0.9985234737 - 0.9990417957 0.9990066290 0.9987695813 - 0.9992334247 0.9992052913 0.9990156293 - 0.9994251132 0.9994040132 0.9992616773 - 0.9996166825 0.9996026158 0.9995077848 - 0.9998083711 0.9998012781 0.9997538924 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0188545007 0.0200409293 0.0140067302 + 0.0332524888 0.0348740518 0.0258749295 + 0.0452598706 0.0475615300 0.0362151787 + 0.0559787415 0.0589470603 0.0456109904 + 0.0658830702 0.0694492683 0.0543236583 + 0.0752111897 0.0792194605 0.0625015125 + 0.0841012076 0.0884407684 0.0702229664 + 0.0926621929 0.0972259119 0.0775719434 + 0.1009650975 0.1056030989 0.0846038684 + 0.1090589985 0.1136253998 0.0913083479 + 0.1170130968 0.1213366017 0.0977427885 + 0.1247942969 0.1287464052 0.1039230004 + 0.1323173046 0.1358782053 0.1098598987 + 0.1394851059 0.1427361071 0.1155802011 + 0.1463093013 0.1493203044 0.1211000010 + 0.1528204978 0.1556663066 0.1264265031 + 0.1590683013 0.1617784947 0.1315815002 + 0.1650577933 0.1676822007 0.1365823001 + 0.1708182991 0.1734001040 0.1414559036 + 0.1763903052 0.1789600998 0.1462168992 + 0.1817798018 0.1843533963 0.1508775055 + 0.1870086044 0.1895903945 0.1554276943 + 0.1920844018 0.1946922988 0.1598816961 + 0.1970220059 0.1996600926 0.1642390937 + 0.2018329054 0.2045013011 0.1684959978 + 0.2065093070 0.2092238963 0.1726709008 + 0.2110690027 0.2138542980 0.1767773032 + 0.2155348957 0.2183912992 0.1808128059 + 0.2198981047 0.2228228003 0.1847777069 + 0.2241635025 0.2271593064 0.1886793971 + 0.2283228040 0.2314175963 0.1925252974 + 0.2323801965 0.2355985940 0.1963074058 + 0.2363557965 0.2396991998 0.2000353038 + 0.2402614951 0.2437127978 0.2036948055 + 0.2441187054 0.2476392984 0.2072975934 + 0.2479200959 0.2514924109 0.2108584046 + 0.2516668141 0.2552962005 0.2143702954 + 0.2553597987 0.2590639889 0.2178401053 + 0.2590073943 0.2627837062 0.2212640047 + 0.2626096010 0.2664473057 0.2246434987 + 0.2661620975 0.2700594962 0.2279828042 + 0.2696613073 0.2736192942 0.2312768996 + 0.2731044888 0.2771342099 0.2345380038 + 0.2764894962 0.2806042135 0.2377575040 + 0.2798126042 0.2840265930 0.2409393936 + 0.2830832899 0.2873997092 0.2440892011 + 0.2863064110 0.2907201052 0.2471970022 + 0.2894839048 0.2939862907 0.2502729893 + 0.2926194966 0.2971968949 0.2533178031 + 0.2957077026 0.3003590107 0.2563329041 + 0.2987434864 0.3034782112 0.2593177855 + 0.3017309904 0.3065663874 0.2622711957 + 0.3046731055 0.3096238971 0.2652002871 + 0.3075759113 0.3126527965 0.2681050003 + 0.3104431033 0.3156541884 0.2709783912 + 0.3132778108 0.3186286986 0.2738296092 + 0.3160825074 0.3215768933 0.2766565979 + 0.3188611865 0.3244988918 0.2794562876 + 0.3216165900 0.3273960948 0.2822301984 + 0.3243513107 0.3302688003 0.2849779129 + 0.3270677924 0.3331151903 0.2877022028 + 0.3297683895 0.3359293044 0.2903991938 + 0.3324556947 0.3387104869 0.2930696011 + 0.3351315856 0.3414531052 0.2957195044 + 0.3377983868 0.3441590071 0.2983430922 + 0.3404580057 0.3468317986 0.3009459972 + 0.3431122899 0.3494741917 0.3035286963 + 0.3457567990 0.3520886004 0.3060950041 + 0.3483870029 0.3546772897 0.3086442053 + 0.3509952128 0.3572408855 0.3111701012 + 0.3535763919 0.3597824872 0.3136737049 + 0.3561269045 0.3623043895 0.3161568940 + 0.3586477935 0.3648073971 0.3186241984 + 0.3611387014 0.3672929108 0.3210709095 + 0.3636046052 0.3697636127 0.3234997094 + 0.3660457134 0.3722181916 0.3259125948 + 0.3684634864 0.3746534884 0.3283078969 + 0.3708611131 0.3770717978 0.3306862116 + 0.3732374012 0.3794744015 0.3330459893 + 0.3755970895 0.3818623126 0.3353869021 + 0.3779377937 0.3842301071 0.3377087116 + 0.3802644014 0.3865832984 0.3400174975 + 0.3825747967 0.3889225125 0.3423081040 + 0.3848729134 0.3912503123 0.3445838094 + 0.3871577978 0.3935666978 0.3468438089 + 0.3894316852 0.3958694041 0.3490906954 + 0.3916954100 0.3981603980 0.3513231874 + 0.3939490914 0.4004369080 0.3535391092 + 0.3961951137 0.4026972055 0.3557423055 + 0.3984332085 0.4049367011 0.3579295874 + 0.4006646872 0.4071569145 0.3600991964 + 0.4028908908 0.4093607068 0.3622539937 + 0.4051119983 0.4115476012 0.3643927872 + 0.4073289931 0.4137150049 0.3665145040 + 0.4095427990 0.4158644974 0.3686214983 + 0.4117541909 0.4179973006 0.3707144856 + 0.4139570892 0.4201147854 0.3727920055 + 0.4161537886 0.4222174883 0.3748528957 + 0.4183439910 0.4243085980 0.3768979013 + 0.4205237925 0.4263896048 0.3789266050 + 0.4226934910 0.4284594953 0.3809396923 + 0.4248510003 0.4305211902 0.3829380870 + 0.4269989133 0.4325712025 0.3849214911 + 0.4291365147 0.4346114993 0.3868896961 + 0.4312635958 0.4366433918 0.3888421953 + 0.4333817959 0.4386653006 0.3907817900 + 0.4354879856 0.4406721890 0.3927083015 + 0.4375857115 0.4426656961 0.3946214020 + 0.4396727085 0.4446468949 0.3965215981 + 0.4417509139 0.4466181099 0.3984085917 + 0.4438194931 0.4485799074 0.4002822936 + 0.4458774030 0.4505313933 0.4021424949 + 0.4479227960 0.4524719119 0.4039889872 + 0.4499554038 0.4544002116 0.4058243036 + 0.4519762993 0.4563159049 0.4076485038 + 0.4539847076 0.4582194090 0.4094625115 + 0.4559822977 0.4601128101 0.4112651944 + 0.4579656124 0.4619938135 0.4130575061 + 0.4599342942 0.4638626873 0.4148383141 + 0.4618889093 0.4657208920 0.4166125953 + 0.4638301134 0.4675695002 0.4183782041 + 0.4657584131 0.4694097936 0.4201379120 + 0.4676744044 0.4712429941 0.4218888879 + 0.4695785046 0.4730699956 0.4236342013 + 0.4714691937 0.4748862982 0.4253711104 + 0.4733453989 0.4766918123 0.4271022975 + 0.4752089083 0.4784876108 0.4288255870 + 0.4770591855 0.4802748859 0.4305430055 + 0.4788978100 0.4820528924 0.4322530031 + 0.4807246923 0.4838208854 0.4339570105 + 0.4825393856 0.4855782092 0.4356541038 + 0.4843406081 0.4873236120 0.4373449087 + 0.4861288071 0.4890565872 0.4390296042 + 0.4879052043 0.4907774925 0.4407073855 + 0.4896669984 0.4924885035 0.4423801005 + 0.4914163053 0.4941891134 0.4440453053 + 0.4931511879 0.4958809912 0.4457054138 + 0.4948748052 0.4975647032 0.4473578930 + 0.4965868890 0.4992407858 0.4490056038 + 0.4982892871 0.5009080172 0.4506456852 + 0.4999811053 0.5025680065 0.4522778988 + 0.5016608238 0.5042217970 0.4539025128 + 0.5033277869 0.5058667064 0.4555189908 + 0.5049809813 0.5075010061 0.4571281075 + 0.5066220760 0.5091261864 0.4587261975 + 0.5082523823 0.5107440948 0.4603149891 + 0.5098735094 0.5123543739 0.4618948102 + 0.5114861131 0.5139557719 0.4634658098 + 0.5130900741 0.5155460238 0.4650284052 + 0.5146843791 0.5171270967 0.4665809870 + 0.5162659287 0.5186967850 0.4681251943 + 0.5178381205 0.5202578902 0.4696618021 + 0.5194014907 0.5218108296 0.4711911976 + 0.5209562182 0.5233557224 0.4727137089 + 0.5225027204 0.5248928070 0.4742273986 + 0.5240399241 0.5264220834 0.4757342041 + 0.5255666971 0.5279446244 0.4772349000 + 0.5270826221 0.5294610262 0.4787296057 + 0.5285871029 0.5309712887 0.4802174866 + 0.5300801992 0.5324715972 0.4816986024 + 0.5315656066 0.5339645743 0.4831734896 + 0.5330439210 0.5354511142 0.4846417010 + 0.5345147848 0.5369303823 0.4861015081 + 0.5359786153 0.5384036899 0.4875538051 + 0.5374345779 0.5398699045 0.4889999032 + 0.5388823748 0.5413284898 0.4904398918 + 0.5403190255 0.5427761078 0.4918713868 + 0.5417451859 0.5442144275 0.4932934940 + 0.5431644917 0.5456464291 0.4947094917 + 0.5445777774 0.5470724106 0.4961200953 + 0.5459790826 0.5484896898 0.4975199103 + 0.5473731756 0.5499022007 0.4989153147 + 0.5487601161 0.5513098240 0.5003060102 + 0.5501369238 0.5527070761 0.5016881227 + 0.5515047908 0.5540965199 0.5030658245 + 0.5528663993 0.5554798841 0.5044389963 + 0.5542203188 0.5568554997 0.5058041215 + 0.5555655956 0.5582247972 0.5071626902 + 0.5569046140 0.5595903993 0.5085164905 + 0.5582354069 0.5609512925 0.5098640919 + 0.5595571995 0.5623037219 0.5112056732 + 0.5608744025 0.5636506081 0.5125434995 + 0.5621863008 0.5649901032 0.5138760209 + 0.5634893179 0.5663208961 0.5152037144 + 0.5647879243 0.5676466823 0.5165277719 + 0.5660805702 0.5689647198 0.5178461075 + 0.5673637986 0.5702759027 0.5191584826 + 0.5686416030 0.5715829730 0.5204676986 + 0.5699104071 0.5728827119 0.5217714906 + 0.5711724758 0.5741752982 0.5230712891 + 0.5724303126 0.5754632950 0.5243685842 + 0.5736793280 0.5767428279 0.5256611705 + 0.5749223232 0.5780189037 0.5269505978 + 0.5761598945 0.5792912841 0.5282369256 + 0.5773891807 0.5805559158 0.5295171142 + 0.5786157250 0.5818173885 0.5307943821 + 0.5798376203 0.5830736160 0.5320662856 + 0.5810524225 0.5843229294 0.5333334804 + 0.5822634101 0.5855684876 0.5345982909 + 0.5834617019 0.5868036151 0.5358551741 + 0.5846555233 0.5880358219 0.5371090174 + 0.5858426094 0.5892624855 0.5383573771 + 0.5870230794 0.5904846787 0.5395992994 + 0.5882006884 0.5917047858 0.5408381224 + 0.5893698931 0.5929183960 0.5420668125 + 0.5905355811 0.5941283703 0.5432921052 + 0.5916948915 0.5953302979 0.5445101261 + 0.5928494930 0.5965275168 0.5457227826 + 0.5940014720 0.5977213979 0.5469322205 + 0.5951455235 0.5989087224 0.5481334925 + 0.5962870121 0.6000937819 0.5493327975 + 0.5974202752 0.6012709141 0.5505263209 + 0.5985488296 0.6024438739 0.5517168045 + 0.5996711254 0.6036102176 0.5529028773 + 0.6007874012 0.6047713161 0.5540845990 + 0.6019008160 0.6059291959 0.5552638173 + 0.6030085087 0.6070817113 0.5564357042 + 0.6041145921 0.6082327962 0.5576059818 + 0.6052119732 0.6093747020 0.5587717295 + 0.6063060164 0.6105144024 0.5599352717 + 0.6073911786 0.6116470098 0.5610927939 + 0.6084719896 0.6127761006 0.5622478127 + 0.6095473170 0.6139001250 0.5633994937 + 0.6106179953 0.6150193214 0.5645474792 + 0.6116858125 0.6161350012 0.5656930804 + 0.6127483249 0.6172463298 0.5668336153 + 0.6138089895 0.6183556914 0.5679724216 + 0.6148627996 0.6194601059 0.5691046715 + 0.6159157157 0.6205636263 0.5702360272 + 0.6169633269 0.6216604114 0.5713596940 + 0.6180101037 0.6227564216 0.5724828839 + 0.6190497279 0.6238446832 0.5736011267 + 0.6200875044 0.6249315739 0.5747177005 + 0.6211166978 0.6260104179 0.5758256912 + 0.6221439838 0.6270869970 0.5769320130 + 0.6231651902 0.6281551719 0.5780321956 + 0.6241847873 0.6292213202 0.5791301131 + 0.6252005100 0.6302810907 0.5802220702 + 0.6262137294 0.6313390732 0.5813122988 + 0.6272215247 0.6323928237 0.5823984742 + 0.6282256246 0.6334443092 0.5834822059 + 0.6292229891 0.6344907284 0.5845605731 + 0.6302174926 0.6355344057 0.5856364965 + 0.6312069297 0.6365721822 0.5867068172 + 0.6321943998 0.6376072168 0.5877746940 + 0.6331781745 0.6386352777 0.5888370872 + 0.6341605186 0.6396614909 0.5898970962 + 0.6351392865 0.6406822205 0.5909510255 + 0.6361160278 0.6417014003 0.5920032263 + 0.6370865107 0.6427150965 0.5930498242 + 0.6380555034 0.6437277794 0.5940951109 + 0.6390181184 0.6447337866 0.5951337814 + 0.6399800181 0.6457393169 0.5961719155 + 0.6409355998 0.6467369199 0.5972027779 + 0.6418911219 0.6477345228 0.5982336998 + 0.6428390741 0.6487230062 0.5992593169 + 0.6437870860 0.6497110128 0.6002848744 + 0.6447305083 0.6506903768 0.6013044715 + 0.6456733942 0.6516680717 0.6023231745 + 0.6466109157 0.6526402235 0.6033357978 + 0.6475467086 0.6536098123 0.6043460965 + 0.6484770179 0.6545764208 0.6053528786 + 0.6494035721 0.6555402875 0.6063570976 + 0.6503273249 0.6565018892 0.6073589921 + 0.6512470245 0.6574596167 0.6083570719 + 0.6521654129 0.6584159136 0.6093541980 + 0.6530792713 0.6593660116 0.6103470922 + 0.6539931297 0.6603162289 0.6113399863 + 0.6548979282 0.6612582803 0.6123251915 + 0.6558027267 0.6622002125 0.6133103967 + 0.6567010283 0.6631376147 0.6142895222 + 0.6575976014 0.6640735269 0.6152667999 + 0.6584901810 0.6650068164 0.6162406206 + 0.6593794823 0.6659374237 0.6172112823 + 0.6602666974 0.6668658853 0.6181805134 + 0.6611484885 0.6677879095 0.6191458106 + 0.6620302200 0.6687098742 0.6201111078 + 0.6629042029 0.6696240902 0.6210708022 + 0.6637783051 0.6705380082 0.6220304966 + 0.6646463871 0.6714469790 0.6229835153 + 0.6655120850 0.6723536253 0.6239334941 + 0.6663749814 0.6732575297 0.6248810291 + 0.6672335863 0.6741561890 0.6258239746 + 0.6680920124 0.6750550270 0.6267668009 + 0.6689471006 0.6759482026 0.6277027726 + 0.6698023081 0.6768413782 0.6286388040 + 0.6706528068 0.6777299047 0.6295701861 + 0.6715013981 0.6786159277 0.6304991841 + 0.6723480821 0.6794993281 0.6314260960 + 0.6731910110 0.6803758740 0.6323478818 + 0.6740338206 0.6812524199 0.6332696080 + 0.6748722792 0.6821228266 0.6341838837 + 0.6757103205 0.6829922795 0.6350972056 + 0.6765457988 0.6838589907 0.6360073090 + 0.6773787141 0.6847224236 0.6369137168 + 0.6782112718 0.6855856776 0.6378198266 + 0.6790390015 0.6864398122 0.6387205124 + 0.6798666716 0.6872938871 0.6396213174 + 0.6806908250 0.6881433129 0.6405187845 + 0.6815124750 0.6889889836 0.6414139867 + 0.6823334098 0.6898341179 0.6423081756 + 0.6831489205 0.6906722784 0.6431941986 + 0.6839643717 0.6915106177 0.6440802217 + 0.6847774982 0.6923443079 0.6449621916 + 0.6855893135 0.6931747794 0.6458414197 + 0.6864004731 0.6940047741 0.6467201710 + 0.6872066855 0.6948273182 0.6475932002 + 0.6880130172 0.6956499219 0.6484662294 + 0.6888151765 0.6964679956 0.6493358016 + 0.6896142960 0.6972820163 0.6502025723 + 0.6904134154 0.6980959177 0.6510692835 + 0.6912078261 0.6989029050 0.6519299746 + 0.6920022964 0.6997095942 0.6527906060 + 0.6927950978 0.7005147934 0.6536483169 + 0.6935858130 0.7013174891 0.6545016766 + 0.6943765283 0.7021203041 0.6553550959 + 0.6951621175 0.7029188275 0.6562039852 + 0.6959465146 0.7037159801 0.6570516229 + 0.6967297792 0.7045122981 0.6578981876 + 0.6975088120 0.7053027749 0.6587392092 + 0.6982877254 0.7060933113 0.6595801711 + 0.6990643144 0.7068814039 0.6604182720 + 0.6998388171 0.7076671124 0.6612536907 + 0.7006133199 0.7084527016 0.6620889902 + 0.7013818026 0.7092331052 0.6629180908 + 0.7021492720 0.7100123167 0.6637459993 + 0.7029157877 0.7107905746 0.6645727754 + 0.7036771774 0.7115622759 0.6653935909 + 0.7044386268 0.7123339772 0.6662142873 + 0.7051976919 0.7131032944 0.6670325994 + 0.7059540153 0.7138692141 0.6678475738 + 0.7067102790 0.7146351933 0.6686627269 + 0.7074630260 0.7153962851 0.6694738865 + 0.7082142234 0.7161549926 0.6702834964 + 0.7089654803 0.7169137001 0.6710931063 + 0.7097116709 0.7176669836 0.6718968153 + 0.7104576826 0.7184197903 0.6727002263 + 0.7112026811 0.7191722989 0.6735023856 + 0.7119430900 0.7199227214 0.6742987037 + 0.7126836181 0.7206730843 0.6750949025 + 0.7134215236 0.7214214206 0.6758887768 + 0.7141550779 0.7221655250 0.6766782999 + 0.7148886919 0.7229096293 0.6774678230 + 0.7156193852 0.7236497998 0.6782538295 + 0.7163475752 0.7243860960 0.6790366769 + 0.7170758247 0.7251223922 0.6798195839 + 0.7178009748 0.7258551717 0.6805974841 + 0.7185249925 0.7265861034 0.6813730001 + 0.7192488909 0.7273170948 0.6821485162 + 0.7199671268 0.7280430198 0.6829196215 + 0.7206841111 0.7287673950 0.6836894155 + 0.7214009762 0.7294918895 0.6844593287 + 0.7221128941 0.7302092910 0.6852232814 + 0.7228242755 0.7309256792 0.6859866977 + 0.7235357165 0.7316421866 0.6867499948 + 0.7242426872 0.7323533297 0.6875082850 + 0.7249497175 0.7330641747 0.6882663965 + 0.7256563902 0.7337750793 0.6890243888 + 0.7263563275 0.7344793081 0.6897757053 + 0.7270562053 0.7351834774 0.6905270219 + 0.7277556062 0.7358874083 0.6912779808 + 0.7284498811 0.7365834117 0.6920228004 + 0.7291440964 0.7372794151 0.6927676201 + 0.7298380136 0.7379751801 0.6935120821 + 0.7305275798 0.7386654019 0.6942508817 + 0.7312173247 0.7393556833 0.6949896812 + 0.7319065928 0.7400457859 0.6957281828 + 0.7325891256 0.7407314777 0.6964604259 + 0.7332715988 0.7414172292 0.6971926093 + 0.7339540124 0.7421028018 0.6979246736 + 0.7346293926 0.7427842021 0.6986520290 + 0.7353047729 0.7434653044 0.6993792057 + 0.7359800935 0.7441465259 0.7001065016 + 0.7366490960 0.7448238134 0.7008286715 + 0.7373176217 0.7455006838 0.7015504241 + 0.7379860878 0.7461774945 0.7022721171 + 0.7386487722 0.7468507886 0.7029896975 + 0.7393102050 0.7475231886 0.7037063241 + 0.7399716973 0.7481955886 0.7044230103 + 0.7406283021 0.7488650084 0.7051362991 + 0.7412828207 0.7495329976 0.7058482170 + 0.7419373989 0.7502009869 0.7065600157 + 0.7425882220 0.7508667707 0.7072690129 + 0.7432361841 0.7515305877 0.7079756260 + 0.7438840866 0.7521942854 0.7086821795 + 0.7445294261 0.7528566122 0.7093874216 + 0.7451704741 0.7535158992 0.7100903988 + 0.7458115220 0.7541753054 0.7107933760 + 0.7464513183 0.7548338175 0.7114953995 + 0.7470856905 0.7554876804 0.7121930122 + 0.7477200031 0.7561414838 0.7128906250 + 0.7483543158 0.7567954063 0.7135881782 + 0.7489826083 0.7574434876 0.7142792940 + 0.7496104836 0.7580910921 0.7149698138 + 0.7502384186 0.7587388158 0.7156603932 + 0.7508633137 0.7593836188 0.7163473964 + 0.7514868975 0.7600269914 0.7170326114 + 0.7521104217 0.7606704235 0.7177178264 + 0.7527319193 0.7613118291 0.7184010744 + 0.7533503771 0.7619500160 0.7190815210 + 0.7539688945 0.7625880837 0.7197619081 + 0.7545869946 0.7632259130 0.7204418182 + 0.7552015185 0.7638580203 0.7211170197 + 0.7558159828 0.7644901872 0.7217922807 + 0.7564306259 0.7651224136 0.7224674821 + 0.7570430040 0.7657516003 0.7231383920 + 0.7576547265 0.7663797736 0.7238079906 + 0.7582663894 0.7670078874 0.7244775295 + 0.7588763237 0.7676342130 0.7251446843 + 0.7594832778 0.7682573199 0.7258080840 + 0.7600904107 0.7688804269 0.7264714837 + 0.7606974244 0.7695034742 0.7271348238 + 0.7613009214 0.7701197863 0.7277917266 + 0.7619044185 0.7707359791 0.7284486294 + 0.7625079155 0.7713521719 0.7291054130 + 0.7631099820 0.7719650865 0.7297595143 + 0.7637109756 0.7725753188 0.7304114103 + 0.7643120885 0.7731856108 0.7310634255 + 0.7649124265 0.7737953067 0.7317147851 + 0.7655079961 0.7744010091 0.7323616743 + 0.7661035061 0.7750065923 0.7330086827 + 0.7666990757 0.7756121755 0.7336556911 + 0.7672913074 0.7762138247 0.7342997193 + 0.7678816915 0.7768129110 0.7349420190 + 0.7684720755 0.7774121165 0.7355843782 + 0.7690622211 0.7780106068 0.7362260818 + 0.7696506977 0.7786043286 0.7368643284 + 0.7702391148 0.7791979909 0.7375025153 + 0.7708275914 0.7797915936 0.7381407022 + 0.7714130878 0.7803828716 0.7387762070 + 0.7719966769 0.7809723020 0.7394099236 + 0.7725803256 0.7815617919 0.7400435805 + 0.7731636167 0.7821511030 0.7406771779 + 0.7737426162 0.7827333808 0.7413080931 + 0.7743214965 0.7833157182 0.7419390082 + 0.7749004960 0.7838979959 0.7425699234 + 0.7754787803 0.7844783068 0.7431989908 + 0.7760564089 0.7850558758 0.7438256741 + 0.7766339779 0.7856335044 0.7444524765 + 0.7772114873 0.7862110734 0.7450792193 + 0.7777854800 0.7867867947 0.7457013726 + 0.7783586979 0.7873620987 0.7463225126 + 0.7789319158 0.7879372835 0.7469435930 + 0.7795041800 0.7885116935 0.7475640774 + 0.7800722718 0.7890805006 0.7481812835 + 0.7806403041 0.7896494269 0.7487984896 + 0.7812082767 0.7902181745 0.7494155765 + 0.7817749977 0.7907847166 0.7500311732 + 0.7823402286 0.7913483977 0.7506451011 + 0.7829053998 0.7919120193 0.7512589097 + 0.7834705114 0.7924757004 0.7518727779 + 0.7840328813 0.7930366993 0.7524824739 + 0.7845944762 0.7935966849 0.7530907989 + 0.7851560116 0.7941566706 0.7536991239 + 0.7857174277 0.7947167158 0.7543073297 + 0.7862744927 0.7952712178 0.7549107075 + 0.7868316174 0.7958257794 0.7555140853 + 0.7873886824 0.7963802814 0.7561174035 + 0.7879452109 0.7969337106 0.7567198277 + 0.7885001898 0.7974827886 0.7573189139 + 0.7890551090 0.7980319858 0.7579178810 + 0.7896100283 0.7985811234 0.7585170269 + 0.7901636958 0.7991284132 0.7591142058 + 0.7907155752 0.7996730208 0.7597088218 + 0.7912675738 0.8002175093 0.7603033781 + 0.7918195128 0.8007619977 0.7608978748 + 0.7923693061 0.8013039827 0.7614898086 + 0.7929174900 0.8018441200 0.7620797753 + 0.7934656739 0.8023841977 0.7626696825 + 0.7940139174 0.8029242754 0.7632595897 + 0.7945610285 0.8034613729 0.7638459802 + 0.7951077223 0.8039970994 0.7644308209 + 0.7956544161 0.8045328259 0.7650157213 + 0.7962012291 0.8050686121 0.7656006217 + 0.7967458963 0.8056014776 0.7661811709 + 0.7972903252 0.8061336279 0.7667608261 + 0.7978346944 0.8066657186 0.7673404217 + 0.7983791232 0.8071978092 0.7679200172 + 0.7989180088 0.8077269793 0.7684965134 + 0.7994561791 0.8082556725 0.7690725923 + 0.7999945283 0.8087844253 0.7696486115 + 0.8005326986 0.8093131185 0.7702246904 + 0.8010672927 0.8098369837 0.7707965970 + 0.8016015887 0.8103603125 0.7713679075 + 0.8021358848 0.8108835816 0.7719392776 + 0.8026701808 0.8114069104 0.7725105882 + 0.8032029867 0.8119258285 0.7730767131 + 0.8037356138 0.8124440908 0.7736421227 + 0.8042681813 0.8129622936 0.7742075920 + 0.8048008084 0.8134806156 0.7747730017 + 0.8053286076 0.8139951825 0.7753348947 + 0.8058556914 0.8145089746 0.7758960724 + 0.8063827753 0.8150228262 0.7764574289 + 0.8069099188 0.8155366182 0.7770186067 + 0.8074336052 0.8160470128 0.7775772214 + 0.8079562783 0.8165563941 0.7781350017 + 0.8084790707 0.8170658946 0.7786927819 + 0.8090019226 0.8175752759 0.7792506218 + 0.8095226288 0.8180813193 0.7798048258 + 0.8100423813 0.8185856938 0.7803573012 + 0.8105623126 0.8190901279 0.7809098959 + 0.8110821247 0.8195945024 0.7814623713 + 0.8115993142 0.8200960755 0.7820122838 + 0.8121144772 0.8205952048 0.7825602293 + 0.8126296997 0.8210942745 0.7831079960 + 0.8131449819 0.8215934038 0.7836558223 + 0.8136587739 0.8220912814 0.7842022181 + 0.8141704798 0.8225870728 0.7847461104 + 0.8146821260 0.8230829239 0.7852900028 + 0.8151937723 0.8235787153 0.7858338952 + 0.8157051206 0.8240739107 0.7863767743 + 0.8162146807 0.8245658278 0.7869150043 + 0.8167243004 0.8250576854 0.7874531150 + 0.8172338009 0.8255496025 0.7879912853 + 0.8177434206 0.8260415196 0.7885295153 + 0.8182494044 0.8265284896 0.7890629172 + 0.8187550902 0.8270152211 0.7895959020 + 0.8192608953 0.8275017738 0.7901288867 + 0.8197665811 0.8279885054 0.7906618714 + 0.8202685714 0.8284724951 0.7911921144 + 0.8207687140 0.8289551139 0.7917206883 + 0.8212687969 0.8294377923 0.7922493219 + 0.8217688799 0.8299204111 0.7927778959 + 0.8222674727 0.8304020762 0.7933050990 + 0.8227633834 0.8308820724 0.7938293219 + 0.8232592940 0.8313620090 0.7943534851 + 0.8237550855 0.8318418860 0.7948777080 + 0.8242509961 0.8323218822 0.7954018712 + 0.8247438073 0.8327968717 0.7959234715 + 0.8252366185 0.8332718015 0.7964450121 + 0.8257294297 0.8337466717 0.7969664931 + 0.8262221813 0.8342216015 0.7974880934 + 0.8267118931 0.8346939087 0.7980074286 + 0.8271998763 0.8351643085 0.7985252738 + 0.8276877999 0.8356347084 0.7990432978 + 0.8281757832 0.8361051083 0.7995612025 + 0.8286628127 0.8365753293 0.8000785112 + 0.8291462064 0.8370444179 0.8005927205 + 0.8296297193 0.8375133872 0.8011069894 + 0.8301131129 0.8379824162 0.8016213179 + 0.8305966258 0.8384515047 0.8021355867 + 0.8310769796 0.8389179111 0.8026489019 + 0.8315563798 0.8393831849 0.8031619191 + 0.8320357800 0.8398485780 0.8036748767 + 0.8325151801 0.8403139114 0.8041878939 + 0.8329936266 0.8407778740 0.8047001958 + 0.8334690928 0.8412373066 0.8052101731 + 0.8339444995 0.8416966200 0.8057203293 + 0.8344200253 0.8421559930 0.8062303066 + 0.8348953724 0.8426154256 0.8067402840 + 0.8353682756 0.8430727124 0.8072487116 + 0.8358402252 0.8435291052 0.8077564240 + 0.8363121152 0.8439856172 0.8082640767 + 0.8367841244 0.8444420099 0.8087717295 + 0.8372554779 0.8448981047 0.8092792034 + 0.8377242088 0.8453521729 0.8097851276 + 0.8381929994 0.8458061814 0.8102909923 + 0.8386617899 0.8462603092 0.8107969165 + 0.8391305208 0.8467143774 0.8113027811 + 0.8395975828 0.8471655250 0.8118069172 + 0.8400632739 0.8476139903 0.8123093247 + 0.8405290246 0.8480623960 0.8128117919 + 0.8409947157 0.8485109210 0.8133143187 + 0.8414604068 0.8489593863 0.8138167858 + 0.8419228196 0.8494048715 0.8143168092 + 0.8423848748 0.8498501182 0.8148164153 + 0.8428469896 0.8502953053 0.8153160214 + 0.8433091044 0.8507404923 0.8158156872 + 0.8437703252 0.8511855006 0.8163148761 + 0.8442285061 0.8516296744 0.8168122768 + 0.8446866274 0.8520739079 0.8173096180 + 0.8451448083 0.8525180817 0.8178070188 + 0.8456029892 0.8529623151 0.8183044195 + 0.8460600972 0.8534045219 0.8187999129 + 0.8465161920 0.8538445234 0.8192933798 + 0.8469722867 0.8542845249 0.8197870255 + 0.8474283814 0.8547245860 0.8202804923 + 0.8478844166 0.8551645875 0.8207740188 + 0.8483386040 0.8556007147 0.8212651014 + 0.8487920165 0.8560351133 0.8217552900 + 0.8492454290 0.8564695120 0.8222454786 + 0.8496989012 0.8569039702 0.8227357268 + 0.8501523137 0.8573384285 0.8232259154 + 0.8506020904 0.8577691913 0.8237136006 + 0.8510516286 0.8581997156 0.8242009878 + 0.8515011072 0.8586301804 0.8246884942 + 0.8519507051 0.8590605855 0.8251760006 + 0.8523998260 0.8594908714 0.8256632090 + 0.8528448939 0.8599187136 0.8261479735 + 0.8532899022 0.8603464961 0.8266327977 + 0.8537350297 0.8607742786 0.8271176219 + 0.8541800976 0.8612021208 0.8276023865 + 0.8546246290 0.8616291285 0.8280869722 + 0.8550670743 0.8620529175 0.8285707831 + 0.8555095792 0.8624767065 0.8290544748 + 0.8559520245 0.8629004955 0.8295382261 + 0.8563945293 0.8633242249 0.8300219178 + 0.8568360806 0.8637471199 0.8305050731 + 0.8572757244 0.8641673923 0.8309867978 + 0.8577153087 0.8645877838 0.8314685225 + 0.8581547737 0.8650081158 0.8319501877 + 0.8585944176 0.8654283881 0.8324319124 + 0.8590329289 0.8658478856 0.8329126835 + 0.8594689965 0.8662654161 0.8333914876 + 0.8599051237 0.8666828871 0.8338704109 + 0.8603413105 0.8671004176 0.8343492150 + 0.8607773781 0.8675178885 0.8348280191 + 0.8612130284 0.8679342270 0.8353064060 + 0.8616474867 0.8683475852 0.8357838988 + 0.8620818853 0.8687611222 0.8362613916 + 0.8625164032 0.8691744804 0.8367388844 + 0.8629509211 0.8695878983 0.8372163773 + 0.8633844852 0.8700003028 0.8376932740 + 0.8638160825 0.8704100251 0.8381686211 + 0.8642476797 0.8708196878 0.8386437893 + 0.8646792769 0.8712294102 0.8391190767 + 0.8651108742 0.8716390729 0.8395944238 + 0.8655416965 0.8720481992 0.8400691152 + 0.8659693003 0.8724551201 0.8405413032 + 0.8663967848 0.8728619218 0.8410134912 + 0.8668243885 0.8732687235 0.8414856791 + 0.8672518730 0.8736755252 0.8419578075 + 0.8676794171 0.8740822077 0.8424299955 + 0.8681045771 0.8744865060 0.8429005742 + 0.8685297966 0.8748908043 0.8433712125 + 0.8689550161 0.8752951026 0.8438417912 + 0.8693801761 0.8756992817 0.8443124890 + 0.8698053956 0.8761035800 0.8447831273 + 0.8702281117 0.8765046000 0.8452516198 + 0.8706504107 0.8769050241 0.8457198143 + 0.8710728288 0.8773053885 0.8461880088 + 0.8714951277 0.8777058125 0.8466562033 + 0.8719174862 0.8781061769 0.8471243978 + 0.8723371029 0.8785045147 0.8475905061 + 0.8727555275 0.8789016008 0.8480554223 + 0.8731737733 0.8792986870 0.8485202789 + 0.8735920787 0.8796957731 0.8489853144 + 0.8740105033 0.8800929785 0.8494501710 + 0.8744276166 0.8804891109 0.8499143720 + 0.8748430014 0.8808835745 0.8503776193 + 0.8752583861 0.8812779784 0.8508408070 + 0.8756737709 0.8816725016 0.8513038754 + 0.8760892153 0.8820670247 0.8517671227 + 0.8765044212 0.8824610710 0.8522300124 + 0.8769180179 0.8828517199 0.8526905179 + 0.8773316145 0.8832423091 0.8531510830 + 0.8777452111 0.8836328983 0.8536115885 + 0.8781588078 0.8840234280 0.8540722132 + 0.8785724044 0.8844140172 0.8545327187 + 0.8789830208 0.8848019242 0.8549907207 + 0.8793928027 0.8851888776 0.8554478288 + 0.8798025250 0.8855758905 0.8559048772 + 0.8802123070 0.8859630227 0.8563619852 + 0.8806220293 0.8863499761 0.8568190932 + 0.8810304999 0.8867359757 0.8572754264 + 0.8814368844 0.8871203065 0.8577305079 + 0.8818433285 0.8875045180 0.8581857085 + 0.8822497725 0.8878887892 0.8586407900 + 0.8826562166 0.8882731199 0.8590958714 + 0.8830626011 0.8886573911 0.8595510125 + 0.8834682703 0.8890393972 0.8600043058 + 0.8838739991 0.8894212842 0.8604574800 + 0.8842797279 0.8898031712 0.8609105945 + 0.8846853971 0.8901851177 0.8613638282 + 0.8850911260 0.8905668855 0.8618170023 + 0.8854956031 0.8909478784 0.8622695208 + 0.8858984709 0.8913276792 0.8627213240 + 0.8863015175 0.8917074800 0.8631731272 + 0.8867045045 0.8920872808 0.8636248708 + 0.8871074915 0.8924670815 0.8640766740 + 0.8875104785 0.8928470016 0.8645284772 + 0.8879091144 0.8932240009 0.8649795055 + 0.8883073926 0.8936008811 0.8654304147 + 0.8887057900 0.8939777017 0.8658813238 + 0.8891041279 0.8943545818 0.8663321733 + 0.8895024061 0.8947314024 0.8667832017 + 0.8899000287 0.8951069117 0.8672332168 + 0.8902962208 0.8954799175 0.8676818013 + 0.8906924725 0.8958529830 0.8681303263 + 0.8910887241 0.8962259889 0.8685787916 + 0.8914849162 0.8965989947 0.8690273166 + 0.8918812275 0.8969720006 0.8694759011 + 0.8922762275 0.8973435163 0.8699231148 + 0.8926709294 0.8977146149 0.8703697920 + 0.8930655122 0.8980855942 0.8708165288 + 0.8934602141 0.8984566927 0.8712632060 + 0.8938547969 0.8988276720 0.8717098832 + 0.8942492008 0.8991985917 0.8721565008 + 0.8946403265 0.8995674253 0.8726019263 + 0.8950313926 0.8999361992 0.8730471730 + 0.8954225779 0.9003049731 0.8734925985 + 0.8958137035 0.9006738067 0.8739380240 + 0.8962048292 0.9010425806 0.8743833899 + 0.8965950012 0.9014102817 0.8748276830 + 0.8969836235 0.9017760158 0.8752703071 + 0.8973723054 0.9021416903 0.8757128716 + 0.8977609277 0.9025074244 0.8761554956 + 0.8981494904 0.9028730989 0.8765981197 + 0.8985381126 0.9032387733 0.8770406246 + 0.8989257812 0.9036024809 0.8774818182 + 0.8993129730 0.9039648175 0.8779221773 + 0.8997001052 0.9043270946 0.8783624768 + 0.9000871778 0.9046893716 0.8788027763 + 0.9004744291 0.9050518274 0.8792430758 + 0.9008615017 0.9054141045 0.8796833754 + 0.9012458920 0.9057751894 0.8801223040 + 0.9016296864 0.9061359763 0.8805608749 + 0.9020134211 0.9064968824 0.8809995055 + 0.9023972154 0.9068577290 0.8814380169 + 0.9027810097 0.9072185159 0.8818765879 + 0.9031646848 0.9075793028 0.8823152184 + 0.9035456777 0.9079378843 0.8827506900 + 0.9039264917 0.9082962871 0.8831861019 + 0.9043073058 0.9086548090 0.8836215138 + 0.9046881795 0.9090132117 0.8840568066 + 0.9050689936 0.9093716741 0.8844922185 + 0.9054498076 0.9097300172 0.8849275112 + 0.9058302045 0.9100852013 0.8853601813 + 0.9062106013 0.9104403853 0.8857929111 + 0.9065908790 0.9107956290 0.8862255812 + 0.9069712758 0.9111508727 0.8866581917 + 0.9073516726 0.9115061164 0.8870909214 + 0.9077318907 0.9118611813 0.8875234127 + 0.9081099033 0.9122149944 0.8879539967 + 0.9084879756 0.9125688076 0.8883845210 + 0.9088661075 0.9129226208 0.8888149858 + 0.9092441201 0.9132763743 0.8892456293 + 0.9096221924 0.9136301875 0.8896760941 + 0.9099997878 0.9139838219 0.8901063204 + 0.9103738070 0.9143360257 0.8905333281 + 0.9107477069 0.9146881104 0.8909602761 + 0.9111216068 0.9150401950 0.8913872838 + 0.9114955068 0.9153922796 0.8918141723 + 0.9118694067 0.9157444239 0.8922411799 + 0.9122431874 0.9160963893 0.8926680088 + 0.9126154184 0.9164451957 0.8930919170 + 0.9129875898 0.9167940021 0.8935158253 + 0.9133598208 0.9171428084 0.8939396739 + 0.9137319922 0.9174916148 0.8943635821 + 0.9141042233 0.9178404212 0.8947874904 + 0.9144763947 0.9181892276 0.8952113986 + 0.9148473740 0.9185342193 0.8956335783 + 0.9152182937 0.9188787937 0.8960558176 + 0.9155892134 0.9192234874 0.8964778781 + 0.9159600735 0.9195681214 0.8969001174 + 0.9163309932 0.9199128151 0.8973221779 + 0.9167019725 0.9202573895 0.8977444172 + 0.9170697927 0.9206014276 0.8981651068 + 0.9174367785 0.9209452271 0.8985853791 + 0.9178038836 0.9212889075 0.8990057111 + 0.9181709290 0.9216327071 0.8994259834 + 0.9185379148 0.9219765067 0.8998463750 + 0.9189050198 0.9223201871 0.9002667069 + 0.9192699194 0.9226629734 0.9006865025 + 0.9196335077 0.9230049849 0.9011059999 + 0.9199969769 0.9233471155 0.9015253782 + 0.9203606248 0.9236891270 0.9019448757 + 0.9207240939 0.9240310788 0.9023643732 + 0.9210876226 0.9243732095 0.9027838111 + 0.9214507937 0.9247142076 0.9032027721 + 0.9218133092 0.9250528812 0.9036207795 + 0.9221758246 0.9253916144 0.9040387273 + 0.9225382209 0.9257302880 0.9044566751 + 0.9229006767 0.9260690212 0.9048746824 + 0.9232631922 0.9264076948 0.9052925706 + 0.9236255884 0.9267463088 0.9057105780 + 0.9239851832 0.9270828962 0.9061275721 + 0.9243447781 0.9274194837 0.9065446854 + 0.9247043729 0.9277560115 0.9069616795 + 0.9250640273 0.9280925989 0.9073787928 + 0.9254236221 0.9284291863 0.9077957869 + 0.9257832170 0.9287657142 0.9082127810 + 0.9261407256 0.9291017056 0.9086294770 + 0.9264971018 0.9294372797 0.9090461135 + 0.9268534780 0.9297729135 0.9094625711 + 0.9272099137 0.9301084876 0.9098792076 + 0.9275662899 0.9304441214 0.9102957249 + 0.9279227257 0.9307796955 0.9107123017 + 0.9282786846 0.9311146736 0.9111282825 + 0.9286336899 0.9314478040 0.9115430117 + 0.9289886951 0.9317808747 0.9119576216 + 0.9293437004 0.9321138859 0.9123722911 + 0.9296985865 0.9324470162 0.9127869010 + 0.9300535917 0.9327800870 0.9132015705 + 0.9304085970 0.9331132174 0.9136161804 + 0.9307621717 0.9334436059 0.9140297771 + 0.9311155081 0.9337733984 0.9144430757 + 0.9314687252 0.9341030717 0.9148563743 + 0.9318218827 0.9344329238 0.9152696729 + 0.9321752191 0.9347625971 0.9156829715 + 0.9325283766 0.9350923896 0.9160963297 + 0.9328805208 0.9354220033 0.9165095091 + 0.9332295060 0.9357516170 0.9169226885 + 0.9335784912 0.9360811114 0.9173358083 + 0.9339274764 0.9364106059 0.9177489877 + 0.9342765212 0.9367401004 0.9181622267 + 0.9346253872 0.9370697141 0.9185752869 + 0.9349743724 0.9373992085 0.9189885259 + 0.9353216887 0.9377279878 0.9194009900 + 0.9356681705 0.9380565286 0.9198130965 + 0.9360147715 0.9383850098 0.9202253222 + 0.9363613129 0.9387134910 0.9206374884 + 0.9367079139 0.9390419722 0.9210495949 + 0.9370543957 0.9393705130 0.9214618206 + 0.9374008775 0.9396988153 0.9218738079 + 0.9377468228 0.9400240183 0.9222841263 + 0.9380925894 0.9403492212 0.9226943851 + 0.9384384751 0.9406743050 0.9231047034 + 0.9387843013 0.9409995079 0.9235150218 + 0.9391301870 0.9413247108 0.9239252806 + 0.9394760728 0.9416499138 0.9243355989 + 0.9398207068 0.9419735074 0.9247456193 + 0.9401633739 0.9422945976 0.9251552224 + 0.9405061007 0.9426156878 0.9255648851 + 0.9408488274 0.9429367781 0.9259744883 + 0.9411916137 0.9432579875 0.9263840914 + 0.9415342808 0.9435790777 0.9267938137 + 0.9418770075 0.9439002275 0.9272034168 + 0.9422174096 0.9442206025 0.9276124835 + 0.9425566196 0.9445406795 0.9280213118 + 0.9428958297 0.9448608160 0.9284300804 + 0.9432349801 0.9451808929 0.9288390279 + 0.9435741901 0.9455009103 0.9292477965 + 0.9439134002 0.9458209872 0.9296566248 + 0.9442526102 0.9461411238 0.9300655127 + 0.9445902705 0.9464595914 0.9304733276 + 0.9449278116 0.9467778802 0.9308810234 + 0.9452651739 0.9470962286 0.9312887192 + 0.9456027150 0.9474143982 0.9316964149 + 0.9459400773 0.9477326870 0.9321041107 + 0.9462776184 0.9480509758 0.9325118065 + 0.9466149807 0.9483692050 0.9329195023 + 0.9469503760 0.9486842155 0.9333260059 + 0.9472857118 0.9489991069 0.9337323904 + 0.9476211071 0.9493141174 0.9341388941 + 0.9479565024 0.9496291280 0.9345452785 + 0.9482918978 0.9499440193 0.9349517822 + 0.9486272931 0.9502589703 0.9353582263 + 0.9489622712 0.9505736828 0.9357644916 + 0.9492946267 0.9508864880 0.9361696243 + 0.9496268034 0.9511991739 0.9365745783 + 0.9499590993 0.9515119791 0.9369797111 + 0.9502912760 0.9518247843 0.9373847842 + 0.9506235719 0.9521374702 0.9377897978 + 0.9509558082 0.9524502754 0.9381948709 + 0.9512878060 0.9527629018 0.9385995269 + 0.9516180754 0.9530745149 0.9390019178 + 0.9519485235 0.9533861876 0.9394041896 + 0.9522787929 0.9536978006 0.9398065209 + 0.9526091218 0.9540094733 0.9402087927 + 0.9529393911 0.9543210864 0.9406111836 + 0.9532697797 0.9546328187 0.9410135150 + 0.9535998702 0.9549441934 0.9414151907 + 0.9539285898 0.9552533031 0.9418125749 + 0.9542574286 0.9555622935 0.9422100186 + 0.9545860887 0.9558714032 0.9426074028 + 0.9549148083 0.9561805129 0.9430047870 + 0.9552435875 0.9564896226 0.9434021711 + 0.9555723071 0.9567986727 0.9437994957 + 0.9559007883 0.9571076035 0.9441967010 + 0.9562236071 0.9574112892 0.9445887208 + 0.9565463066 0.9577149749 0.9449806213 + 0.9568691254 0.9580187798 0.9453725219 + 0.9571918845 0.9583225250 0.9457644224 + 0.9575145841 0.9586262107 0.9461563230 + 0.9578374028 0.9589298964 0.9465482235 + 0.9581602216 0.9592335820 0.9469401240 + 0.9584769011 0.9595324993 0.9473283291 + 0.9587929249 0.9598308802 0.9477159977 + 0.9591090083 0.9601292014 0.9481037259 + 0.9594249725 0.9604275823 0.9484915137 + 0.9597409964 0.9607260227 0.9488791823 + 0.9600570798 0.9610242844 0.9492670298 + 0.9603731036 0.9613227248 0.9496546984 + 0.9606860280 0.9616187215 0.9500409961 + 0.9609975219 0.9619138241 0.9504266977 + 0.9613090158 0.9622088075 0.9508125186 + 0.9616205096 0.9625037909 0.9511982203 + 0.9619320035 0.9627988935 0.9515839219 + 0.9622434974 0.9630938768 0.9519696236 + 0.9625549912 0.9633889198 0.9523553848 + 0.9628651738 0.9636827707 0.9527397752 + 0.9631738067 0.9639751911 0.9531226158 + 0.9634823203 0.9642676115 0.9535053968 + 0.9637908936 0.9645599127 0.9538881183 + 0.9640994072 0.9648522735 0.9542708993 + 0.9644079804 0.9651446939 0.9546536803 + 0.9647164941 0.9654371142 0.9550365210 + 0.9650247097 0.9657291174 0.9554188848 + 0.9653298855 0.9660187960 0.9557983279 + 0.9656351805 0.9663084149 0.9561777711 + 0.9659404755 0.9665980935 0.9565572739 + 0.9662457705 0.9668877721 0.9569367170 + 0.9665510058 0.9671773911 0.9573162198 + 0.9668563008 0.9674671292 0.9576957226 + 0.9671615958 0.9677568078 0.9580751061 + 0.9674646854 0.9680448771 0.9584522843 + 0.9677671194 0.9683324099 0.9588286877 + 0.9680693746 0.9686200023 0.9592050910 + 0.9683718085 0.9689074755 0.9595814943 + 0.9686741829 0.9691951275 0.9599578977 + 0.9689764977 0.9694827199 0.9603341818 + 0.9692788720 0.9697701931 0.9607105851 + 0.9695801735 0.9700570107 0.9610859156 + 0.9698789120 0.9703419209 0.9614585042 + 0.9701775908 0.9706267715 0.9618310928 + 0.9704763889 0.9709116817 0.9622036815 + 0.9707751274 0.9711965919 0.9625763297 + 0.9710738063 0.9714813828 0.9629489183 + 0.9713726044 0.9717662930 0.9633215070 + 0.9716712832 0.9720512033 0.9636940956 + 0.9719662070 0.9723331928 0.9640635252 + 0.9722601175 0.9726145267 0.9644321799 + 0.9725540876 0.9728958011 0.9648008943 + 0.9728481174 0.9731770754 0.9651694894 + 0.9731420279 0.9734582901 0.9655382037 + 0.9734359980 0.9737396240 0.9659067988 + 0.9737300277 0.9740208983 0.9662755132 + 0.9740229845 0.9743015170 0.9666432738 + 0.9743129015 0.9745799899 0.9670084119 + 0.9746028185 0.9748585820 0.9673733711 + 0.9748927951 0.9751371145 0.9677385092 + 0.9751827121 0.9754155874 0.9681035280 + 0.9754726887 0.9756941199 0.9684686065 + 0.9757626057 0.9759727120 0.9688336253 + 0.9760525823 0.9762511849 0.9691987038 + 0.9763398170 0.9765279293 0.9695615172 + 0.9766253829 0.9768033028 0.9699230194 + 0.9769108891 0.9770787954 0.9702845216 + 0.9771965146 0.9773542285 0.9706460238 + 0.9774820209 0.9776297212 0.9710075259 + 0.9777675867 0.9779050946 0.9713690281 + 0.9780532122 0.9781805873 0.9717304707 + 0.9783387184 0.9784560204 0.9720919132 + 0.9786192775 0.9787276983 0.9724497199 + 0.9788995981 0.9789990783 0.9728072286 + 0.9791797996 0.9792703986 0.9731646776 + 0.9794600010 0.9795417786 0.9735221267 + 0.9797403216 0.9798130989 0.9738795757 + 0.9800205231 0.9800844789 0.9742370844 + 0.9803007245 0.9803557992 0.9745944738 + 0.9805799723 0.9806264043 0.9749512076 + 0.9808548093 0.9808933139 0.9753037095 + 0.9811295867 0.9811602235 0.9756562710 + 0.9814044237 0.9814270735 0.9760088921 + 0.9816792011 0.9816939831 0.9763615131 + 0.9819539785 0.9819608927 0.9767140150 + 0.9822288752 0.9822278023 0.9770665765 + 0.9825037122 0.9824947119 0.9774191976 + 0.9827765226 0.9827601910 0.9777697921 + 0.9830456972 0.9830228090 0.9781169295 + 0.9833148718 0.9832854867 0.9784640074 + 0.9835842252 0.9835482240 0.9788110256 + 0.9838533998 0.9838109016 0.9791581035 + 0.9841225743 0.9840735197 0.9795051813 + 0.9843918085 0.9843361974 0.9798521996 + 0.9846609831 0.9845988750 0.9801992774 + 0.9849274158 0.9848592281 0.9805430770 + 0.9851905704 0.9851167798 0.9808833003 + 0.9854537249 0.9853743911 0.9812235236 + 0.9857168794 0.9856320024 0.9815636277 + 0.9859799743 0.9858896136 0.9819037914 + 0.9862431884 0.9861472249 0.9822440147 + 0.9865062833 0.9864048958 0.9825841188 + 0.9867694974 0.9866625071 0.9829242826 + 0.9870293140 0.9869173169 0.9832602739 + 0.9872860909 0.9871696234 0.9835925102 + 0.9875429273 0.9874218702 0.9839246869 + 0.9877997041 0.9876741767 0.9842569232 + 0.9880564809 0.9879264832 0.9845890999 + 0.9883133173 0.9881787896 0.9849212766 + 0.9885700941 0.9884310961 0.9852535129 + 0.9888268709 0.9886834025 0.9855856895 + 0.9890797734 0.9889330268 0.9859133959 + 0.9893288016 0.9891800284 0.9862366915 + 0.9895778894 0.9894269109 0.9865599871 + 0.9898269176 0.9896739125 0.9868832827 + 0.9900760055 0.9899207950 0.9872065783 + 0.9903249741 0.9901676774 0.9875298738 + 0.9905740023 0.9904146791 0.9878532290 + 0.9908230901 0.9906616211 0.9881765246 + 0.9910683036 0.9909055829 0.9884948134 + 0.9913083911 0.9911454916 0.9888063073 + 0.9915484786 0.9913855195 0.9891179204 + 0.9917885065 0.9916254282 0.9894294143 + 0.9920285940 0.9918652773 0.9897409081 + 0.9922686219 0.9921053052 0.9900524020 + 0.9925087094 0.9923452139 0.9903638959 + 0.9927487969 0.9925851822 0.9906755090 + 0.9929866791 0.9928228855 0.9909834266 + 0.9932188988 0.9930546284 0.9912818074 + 0.9934511185 0.9932863712 0.9915801883 + 0.9936832786 0.9935181141 0.9918786883 + 0.9939156175 0.9937499166 0.9921771288 + 0.9941477776 0.9939817190 0.9924755096 + 0.9943799973 0.9942134023 0.9927738905 + 0.9946122169 0.9944452047 0.9930723906 + 0.9948437810 0.9946765900 0.9933699965 + 0.9950650930 0.9949020743 0.9936543703 + 0.9952864051 0.9951274991 0.9939388037 + 0.9955077171 0.9953529835 0.9942231178 + 0.9957290292 0.9955784082 0.9945074916 + 0.9959502816 0.9958038926 0.9947919250 + 0.9961715937 0.9960293770 0.9950762987 + 0.9963929057 0.9962548018 0.9953606725 + 0.9966142178 0.9964802861 0.9956451058 + 0.9968237877 0.9966976047 0.9959148765 + 0.9970299006 0.9969125986 0.9961804748 + 0.9972360730 0.9971275926 0.9964460731 + 0.9974421859 0.9973425865 0.9967116117 + 0.9976484179 0.9975575209 0.9969772100 + 0.9978544712 0.9977725148 0.9972428083 + 0.9980607033 0.9979875088 0.9975084066 + 0.9982668161 0.9982025027 0.9977738857 + 0.9984667897 0.9984105825 0.9980313182 + 0.9986584783 0.9986093044 0.9982774258 + 0.9988501072 0.9988079071 0.9985234737 + 0.9990417957 0.9990066290 0.9987695813 + 0.9992334247 0.9992052913 0.9990156293 + 0.9994251132 0.9994040132 0.9992616773 + 0.9996166825 0.9996026158 0.9995077848 + 0.9998083711 0.9998012781 0.9997538924 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_optima_ii_100.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_optima_ii_100.spi1d index 8a56fcf0..57b3929c 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_optima_ii_100.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_optima_ii_100.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0076661762 0.0083268499 0.0068440801 - 0.0158262700 0.0170417801 0.0131509099 - 0.0232411809 0.0250322502 0.0190004501 - 0.0300489496 0.0323281400 0.0244449992 - 0.0363852791 0.0390635394 0.0295583308 - 0.0423460715 0.0453425013 0.0343772098 - 0.0479932986 0.0512349084 0.0389431007 - 0.0533834286 0.0567868613 0.0432897806 - 0.0585362613 0.0620452613 0.0474547893 - 0.0634827465 0.0670664310 0.0514379889 - 0.0682476535 0.0719086230 0.0552622713 - 0.0728643686 0.0766109228 0.0590022616 - 0.0773498490 0.0812185779 0.0626629591 - 0.0817248896 0.0857366771 0.0662265718 - 0.0860022604 0.0901816636 0.0696830973 - 0.0901876912 0.0945459083 0.0730567127 - 0.0942838192 0.0988429263 0.0763788819 - 0.0983011201 0.1030735970 0.0796598792 - 0.1022287980 0.1072389036 0.0828902796 - 0.1060905010 0.1113395020 0.0860634297 - 0.1098807007 0.1153889000 0.0891731530 - 0.1136023998 0.1193716973 0.0922177732 - 0.1172552034 0.1232964024 0.0952064171 - 0.1208416000 0.1271591038 0.0981551483 - 0.1243667975 0.1309625953 0.1010731980 - 0.1278326958 0.1347209960 0.1039592028 - 0.1312370002 0.1384246945 0.1068119034 - 0.1345919073 0.1420657933 0.1096345037 - 0.1378999949 0.1456594020 0.1124313995 - 0.1411581933 0.1492089927 0.1151997000 - 0.1443776041 0.1527009010 0.1179367006 - 0.1475483030 0.1561467052 0.1206402034 - 0.1506752074 0.1595402956 0.1233116984 - 0.1537552029 0.1628801972 0.1259557009 - 0.1567965001 0.1661709994 0.1285720021 - 0.1598031074 0.1694162935 0.1311601996 - 0.1627652049 0.1726270020 0.1337202936 - 0.1656907946 0.1757915020 0.1362521052 - 0.1685829014 0.1789050996 0.1387566030 - 0.1714359969 0.1819735020 0.1412398964 - 0.1742600948 0.1850046962 0.1437025964 - 0.1770537049 0.1879996955 0.1461455971 - 0.1798166037 0.1909551024 0.1485693008 - 0.1825578958 0.1938706934 0.1509743035 - 0.1852674037 0.1967559010 0.1533614993 - 0.1879525036 0.1996030957 0.1557313949 - 0.1906042993 0.2024182975 0.1580837965 - 0.1932311952 0.2052014023 0.1604201049 - 0.1958318949 0.2079581022 0.1627393067 - 0.1984055936 0.2106810957 0.1650432050 - 0.2009532005 0.2133824974 0.1673322022 - 0.2034747005 0.2160581946 0.1696034074 - 0.2059706002 0.2187004983 0.1718567014 - 0.2084430009 0.2213200927 0.1740909964 - 0.2108965069 0.2239086032 0.1763089001 - 0.2133263052 0.2264758050 0.1785116047 - 0.2157393992 0.2290169001 0.1806990951 - 0.2181292027 0.2315327972 0.1828715056 - 0.2205027044 0.2340238988 0.1850311011 - 0.2228585035 0.2364936024 0.1871781945 - 0.2251935005 0.2389385998 0.1893118024 - 0.2275121957 0.2413554937 0.1914346963 - 0.2298126966 0.2437552065 0.1935469061 - 0.2320972979 0.2461318970 0.1956477016 - 0.2343686968 0.2484831065 0.1977396011 - 0.2366248071 0.2508153021 0.1998216063 - 0.2388661057 0.2531288862 0.2018945962 - 0.2410929054 0.2554250956 0.2039602995 - 0.2433024943 0.2577019930 0.2060160935 - 0.2454963028 0.2599579096 0.2080630958 - 0.2476740032 0.2621991932 0.2100999057 - 0.2498372942 0.2644275129 0.2121268064 - 0.2519871891 0.2666364014 0.2141437978 - 0.2541207969 0.2688263953 0.2161484957 - 0.2562352121 0.2710013986 0.2181435972 - 0.2583388984 0.2731583118 0.2201271951 - 0.2604309916 0.2752990127 0.2221024036 - 0.2625060081 0.2774248123 0.2240664065 - 0.2645739019 0.2795382142 0.2260230929 - 0.2666332126 0.2816390097 0.2279690057 - 0.2686808109 0.2837243974 0.2299083024 - 0.2707110047 0.2857916951 0.2318374068 - 0.2727304995 0.2878476977 0.2337605953 - 0.2747409046 0.2898918092 0.2356743068 - 0.2767409086 0.2919228971 0.2375825942 - 0.2787257135 0.2939389050 0.2394821942 - 0.2807008028 0.2959417999 0.2413766980 - 0.2826663852 0.2979333103 0.2432633042 - 0.2846207917 0.2999132872 0.2451453060 - 0.2865644097 0.3018836081 0.2470199019 - 0.2884986103 0.3038400114 0.2488902956 - 0.2904250026 0.3057815135 0.2507540882 - 0.2923448086 0.3077118099 0.2526113987 - 0.2942551076 0.3096336126 0.2544620037 - 0.2961547077 0.3115448952 0.2563053966 - 0.2980456948 0.3134449124 0.2581430972 - 0.2999280095 0.3153319061 0.2599739134 - 0.3018029034 0.3172087073 0.2617988884 - 0.3036679029 0.3190751076 0.2636143863 - 0.3055250049 0.3209312856 0.2654233873 - 0.3073746860 0.3227779865 0.2672255039 - 0.3092148006 0.3246138990 0.2690199912 - 0.3110457957 0.3264397979 0.2708095908 - 0.3128677905 0.3282575011 0.2725906968 - 0.3146800101 0.3300665915 0.2743658125 - 0.3164839149 0.3318657875 0.2761324048 - 0.3182795942 0.3336552978 0.2778904140 - 0.3200652003 0.3354372978 0.2796432972 - 0.3218409121 0.3372116983 0.2813872993 - 0.3236100078 0.3389771879 0.2831256986 - 0.3253748119 0.3407320082 0.2848586142 - 0.3271335959 0.3424785137 0.2865825891 - 0.3288846016 0.3442190886 0.2883006930 - 0.3306263983 0.3459519148 0.2900108993 - 0.3323577940 0.3476763070 0.2917132080 - 0.3340801001 0.3493919075 0.2934105098 - 0.3357945085 0.3511008918 0.2951011062 - 0.3375023901 0.3528040051 0.2967849970 - 0.3392041922 0.3545002043 0.2984645963 - 0.3408981860 0.3561894000 0.3001382947 - 0.3425840139 0.3578724861 0.3018057048 - 0.3442628086 0.3595485985 0.3034670949 - 0.3459351063 0.3612180948 0.3051221967 - 0.3476001024 0.3628816009 0.3067719936 - 0.3492591083 0.3645390868 0.3084169924 - 0.3509126008 0.3661913872 0.3100563884 - 0.3525592089 0.3678377867 0.3116889000 - 0.3541980982 0.3694776893 0.3133162856 - 0.3558301926 0.3711101115 0.3149392903 - 0.3574565947 0.3727348149 0.3165563047 - 0.3590759039 0.3743531108 0.3181675971 - 0.3606899083 0.3759661019 0.3197731972 - 0.3622973859 0.3775736094 0.3213737905 - 0.3638975024 0.3791748881 0.3229694963 - 0.3654907048 0.3807713985 0.3245606124 - 0.3670778871 0.3823625147 0.3261463046 - 0.3686599135 0.3839488924 0.3277261853 - 0.3702372015 0.3855310977 0.3293007910 - 0.3718093038 0.3871083856 0.3308694959 - 0.3733766973 0.3886804879 0.3324309886 - 0.3749383092 0.3902452886 0.3339884877 - 0.3764936924 0.3918035924 0.3355414867 - 0.3780421019 0.3933547139 0.3370898068 - 0.3795835972 0.3948993087 0.3386338055 - 0.3811194897 0.3964386880 0.3401749134 - 0.3826521933 0.3979738951 0.3417122960 - 0.3841798007 0.3995029926 0.3432452977 - 0.3857024908 0.4010264874 0.3447737098 - 0.3872196972 0.4025439918 0.3462958038 - 0.3887315094 0.4040569961 0.3478113115 - 0.3902386129 0.4055672884 0.3493227065 - 0.3917382061 0.4070734084 0.3508295119 - 0.3932329118 0.4085750878 0.3523303866 - 0.3947235942 0.4100722969 0.3538272977 - 0.3962103128 0.4115647078 0.3553214967 - 0.3976925015 0.4130522907 0.3568125963 - 0.3991701007 0.4145320952 0.3582983017 - 0.4006435871 0.4160074890 0.3597793877 - 0.4021126926 0.4174784124 0.3612557054 - 0.4035769105 0.4189445972 0.3627254069 - 0.4050329924 0.4204039872 0.3641889989 - 0.4064843059 0.4218592048 0.3656494021 - 0.4079307020 0.4233103096 0.3671061099 - 0.4093720019 0.4247572124 0.3685575128 - 0.4108068049 0.4261992872 0.3700045943 - 0.4122380912 0.4276376069 0.3714494109 - 0.4136658907 0.4290730059 0.3728919923 - 0.4150888920 0.4305058122 0.3743301034 - 0.4165056050 0.4319339991 0.3757638931 - 0.4179174900 0.4333589077 0.3771938980 - 0.4193247855 0.4347805977 0.3786191940 - 0.4207271934 0.4361965060 0.3800374866 - 0.4221262932 0.4376091063 0.3814533055 - 0.4235219955 0.4390187860 0.3828668892 - 0.4249120951 0.4404245913 0.3842754960 - 0.4262982905 0.4418255091 0.3856791854 - 0.4276815057 0.4432229102 0.3870786130 - 0.4290600121 0.4446159899 0.3884716928 - 0.4304330051 0.4460026920 0.3898597956 - 0.4318026900 0.4473862946 0.3912445009 - 0.4331685901 0.4487664998 0.3926242888 - 0.4345290065 0.4501408041 0.3940002024 - 0.4358857870 0.4515124857 0.3953734040 - 0.4372372925 0.4528811872 0.3967424929 - 0.4385837018 0.4542450011 0.3981088102 - 0.4399270117 0.4556061029 0.3994731903 - 0.4412651956 0.4569629133 0.4008344114 - 0.4425975978 0.4583143890 0.4021924138 - 0.4439268112 0.4596636891 0.4035479128 - 0.4452503920 0.4610106945 0.4048978090 - 0.4465709925 0.4623551965 0.4062455893 - 0.4478893876 0.4636982977 0.4075908959 - 0.4492019117 0.4650372863 0.4089311063 - 0.4505116045 0.4663740098 0.4102692008 - 0.4518170953 0.4677081108 0.4116033912 - 0.4531182945 0.4690375030 0.4129329026 - 0.4544177055 0.4703648090 0.4142597914 - 0.4557136893 0.4716871977 0.4155794978 - 0.4570071995 0.4730058014 0.4168972969 - 0.4582982063 0.4743215144 0.4182122946 - 0.4595845044 0.4756304026 0.4195236862 - 0.4608688056 0.4769366980 0.4208334088 - 0.4621477127 0.4782372117 0.4221383929 - 0.4634240866 0.4795337915 0.4234414995 - 0.4646978974 0.4808278084 0.4247420132 - 0.4659669995 0.4821158051 0.4260379970 - 0.4672344029 0.4834016860 0.4273321033 - 0.4684959054 0.4846813977 0.4286206067 - 0.4697546959 0.4859575927 0.4299075007 - 0.4710089862 0.4872294962 0.4311901927 - 0.4722594917 0.4884983897 0.4324694872 - 0.4735080004 0.4897660017 0.4337458909 - 0.4747509062 0.4910294116 0.4350179136 - 0.4759922028 0.4922915101 0.4362885952 - 0.4772273898 0.4935492873 0.4375523031 - 0.4784607887 0.4948048890 0.4388141930 - 0.4796907008 0.4960573018 0.4400705993 - 0.4809170961 0.4973073006 0.4413250089 - 0.4821406901 0.4985556006 0.4425770938 - 0.4833607972 0.4998002946 0.4438262880 - 0.4845795929 0.5010439754 0.4450738132 - 0.4857935905 0.5022826791 0.4463163912 - 0.4870069027 0.5035204291 0.4475578070 - 0.4882164896 0.5047531128 0.4487918913 - 0.4894252121 0.5059841275 0.4500248134 - 0.4906300902 0.5072104931 0.4512509108 - 0.4918332100 0.5084347725 0.4524757862 - 0.4930329919 0.5096561909 0.4536970854 - 0.4942300022 0.5108749270 0.4549165070 - 0.4954235852 0.5120911002 0.4561327994 - 0.4966148138 0.5133035183 0.4573467076 - 0.4978038967 0.5145136714 0.4585579038 - 0.4989897013 0.5157194734 0.4597668052 - 0.5001736879 0.5169236064 0.4609735906 - 0.5013538003 0.5181236863 0.4621770084 - 0.5025321245 0.5193228126 0.4633781016 - 0.5037057996 0.5205178857 0.4645760059 - 0.5048779845 0.5217121840 0.4657720923 - 0.5060449243 0.5229014754 0.4669648111 - 0.5072103143 0.5240899920 0.4681558907 - 0.5083714128 0.5252736807 0.4693433940 - 0.5095313787 0.5264567137 0.4705292881 - 0.5106868744 0.5276340246 0.4717121124 - 0.5118411779 0.5288106203 0.4728935957 - 0.5129907131 0.5299817920 0.4740710855 - 0.5141386986 0.5311521292 0.4752467871 - 0.5152817965 0.5323163867 0.4764178097 - 0.5164232850 0.5334795713 0.4775865078 - 0.5175597072 0.5346372724 0.4787499011 - 0.5186939240 0.5357934833 0.4799101949 - 0.5198233724 0.5369442105 0.4810669124 - 0.5209500790 0.5380924940 0.4822207987 - 0.5220733881 0.5392372012 0.4833728075 - 0.5231938958 0.5403798223 0.4845224917 - 0.5243116021 0.5415189862 0.4856700897 - 0.5254257917 0.5426552296 0.4868142903 - 0.5265377760 0.5437887907 0.4879572093 - 0.5276451111 0.5449187160 0.4890958965 - 0.5287510753 0.5460466743 0.4902341068 - 0.5298516154 0.5471696258 0.4913682044 - 0.5309517980 0.5482918024 0.4925023019 - 0.5320447087 0.5494078994 0.4936308861 - 0.5331376195 0.5505239964 0.4947592020 - 0.5342239141 0.5516330004 0.4958822131 - 0.5353093743 0.5527415872 0.4970037043 - 0.5363904834 0.5538460016 0.4981215894 - 0.5374696255 0.5549491048 0.4992372096 - 0.5385463834 0.5560489893 0.5003510118 - 0.5396206975 0.5571463108 0.5014619827 - 0.5406937003 0.5582417250 0.5025722980 - 0.5417628884 0.5593330860 0.5036795735 - 0.5428320169 0.5604240894 0.5047869086 - 0.5438956022 0.5615094900 0.5058897138 - 0.5449593067 0.5625947714 0.5069922209 - 0.5460199714 0.5636752248 0.5080906749 - 0.5470796227 0.5647544265 0.5091872215 - 0.5481380224 0.5658313036 0.5102818012 - 0.5491948128 0.5669060946 0.5113732815 - 0.5502511859 0.5679798722 0.5124642849 - 0.5513052940 0.5690498948 0.5135501027 - 0.5523592830 0.5701199174 0.5146359205 - 0.5534104109 0.5711855888 0.5157186985 - 0.5544610023 0.5722507834 0.5168005228 - 0.5555099249 0.5733129978 0.5178806782 - 0.5565569997 0.5743728876 0.5189586282 - 0.5576034784 0.5754317045 0.5200362802 - 0.5586465001 0.5764862895 0.5211094022 - 0.5596894026 0.5775408745 0.5221825242 - 0.5607293248 0.5785906911 0.5232514739 - 0.5617681146 0.5796394944 0.5243185163 - 0.5628052950 0.5806865096 0.5253840089 - 0.5638393164 0.5817313194 0.5264452100 - 0.5648733974 0.5827760100 0.5275064707 - 0.5659030080 0.5838158727 0.5285636187 - 0.5669323206 0.5848557949 0.5296201110 - 0.5679587722 0.5858927965 0.5306740999 - 0.5689827204 0.5869277716 0.5317248702 - 0.5700063109 0.5879620910 0.5327755809 - 0.5710247755 0.5889919996 0.5338218212 - 0.5720431805 0.5900217891 0.5348678231 - 0.5730581880 0.5910484195 0.5359107256 - 0.5740705729 0.5920730829 0.5369504094 - 0.5750827193 0.5930969715 0.5379900932 - 0.5760906935 0.5941162705 0.5390231013 - 0.5770987868 0.5951356888 0.5400561094 - 0.5781041980 0.5961515903 0.5410866141 - 0.5791074038 0.5971654058 0.5421143770 - 0.5801107287 0.5981786847 0.5431421995 - 0.5811091065 0.5991874933 0.5441660285 - 0.5821076035 0.6001963019 0.5451896191 - 0.5831037164 0.6012020707 0.5462111831 - 0.5840969086 0.6022052765 0.5472295880 - 0.5850901008 0.6032084823 0.5482478738 - 0.5860803127 0.6042070985 0.5492616296 - 0.5870699286 0.6052052975 0.5502738953 - 0.5880584717 0.6062015891 0.5512850285 - 0.5890443921 0.6071943045 0.5522903204 - 0.5900301933 0.6081869006 0.5532956719 - 0.5910136104 0.6091759801 0.5542973876 - 0.5919954777 0.6101636887 0.5552961826 - 0.5929774046 0.6111510992 0.5562950969 - 0.5939552784 0.6121332049 0.5572906733 - 0.5949330926 0.6131153107 0.5582858920 - 0.5959095955 0.6140955091 0.5592799187 - 0.5968831778 0.6150727272 0.5602700710 - 0.5978568196 0.6160498857 0.5612602830 - 0.5988277197 0.6170232296 0.5622476935 - 0.5997968912 0.6179949045 0.5632326007 - 0.6007661223 0.6189665794 0.5642176270 - 0.6017311215 0.6199334264 0.5651969910 - 0.6026958227 0.6209002137 0.5661752820 - 0.6036596894 0.6218655705 0.5671530962 - 0.6046196222 0.6228268743 0.5681259036 - 0.6055796146 0.6237881780 0.5690985918 - 0.6065375805 0.6247472167 0.5700702071 - 0.6074926853 0.6257035732 0.5710396171 - 0.6084479094 0.6266599894 0.5720090270 - 0.6093999147 0.6276127100 0.5729761720 - 0.6103500724 0.6285637021 0.5739415884 - 0.6113001704 0.6295146942 0.5749070048 - 0.6122449040 0.6304615140 0.5758687854 - 0.6131883860 0.6314076781 0.5768293738 - 0.6141319871 0.6323534846 0.5777900219 - 0.6150689125 0.6332944036 0.5787457228 - 0.6160058975 0.6342352033 0.5797008276 - 0.6169418097 0.6351751089 0.5806555748 - 0.6178718805 0.6361111999 0.5816044807 - 0.6188020110 0.6370472908 0.5825533867 - 0.6197304130 0.6379815936 0.5835011005 - 0.6206536889 0.6389123797 0.5844430923 - 0.6215770841 0.6398432255 0.5853852034 - 0.6224973202 0.6407719851 0.5863258839 - 0.6234118938 0.6416980028 0.5872634053 - 0.6243265271 0.6426240802 0.5882009268 - 0.6252374053 0.6435475945 0.5891368985 - 0.6261436939 0.6444684267 0.5900704265 - 0.6270499825 0.6453893185 0.5910037756 - 0.6279519796 0.6463075876 0.5919355154 - 0.6288496852 0.6472237706 0.5928648710 - 0.6297472715 0.6481400132 0.5937942266 - 0.6306408048 0.6490532160 0.5947210789 - 0.6315302253 0.6499642134 0.5956451297 - 0.6324197054 0.6508752108 0.5965691209 - 0.6333047748 0.6517835855 0.5974907279 - 0.6341857910 0.6526899934 0.5984091759 - 0.6350668073 0.6535964012 0.5993278027 - 0.6359438896 0.6545000076 0.6002436280 - 0.6368169188 0.6554012895 0.6011555791 - 0.6376898289 0.6563025713 0.6020675898 - 0.6385591030 0.6572015882 0.6029770970 - 0.6394237876 0.6580982804 0.6038823724 - 0.6402884722 0.6589950919 0.6047877073 - 0.6411507130 0.6598895788 0.6056910157 - 0.6420087814 0.6607813239 0.6065897942 - 0.6428670287 0.6616728902 0.6074885130 - 0.6437233090 0.6625624895 0.6083862782 - 0.6445748210 0.6634479761 0.6092801094 - 0.6454262733 0.6643334031 0.6101740003 - 0.6462768912 0.6652175188 0.6110675931 - 0.6471220255 0.6660966277 0.6119573116 - 0.6479671001 0.6669756770 0.6128469110 - 0.6488121748 0.6678544283 0.6137366295 - 0.6496508718 0.6687275171 0.6146218181 - 0.6504895091 0.6696006060 0.6155067086 - 0.6513282061 0.6704736948 0.6163915992 - 0.6521630287 0.6713413000 0.6172724962 - 0.6529970765 0.6722081900 0.6181520224 - 0.6538311243 0.6730750799 0.6190316081 - 0.6546623707 0.6739377975 0.6199085712 - 0.6554920077 0.6747987270 0.6207835078 - 0.6563215852 0.6756594777 0.6216583848 - 0.6571496129 0.6765179038 0.6225318909 - 0.6579748988 0.6773731709 0.6234022975 - 0.6588003039 0.6782284975 0.6242725849 - 0.6596251130 0.6790826917 0.6251426935 - 0.6604462862 0.6799324751 0.6260074973 - 0.6612675786 0.6807821989 0.6268723011 - 0.6620888114 0.6816319823 0.6277372241 - 0.6629067063 0.6824771762 0.6286000013 - 0.6637240052 0.6833220124 0.6294621825 - 0.6645414233 0.6841667295 0.6303244233 - 0.6653565764 0.6850084066 0.6311848760 - 0.6661701798 0.6858481765 0.6320434213 - 0.6669837236 0.6866878867 0.6329019070 - 0.6677964926 0.6875264049 0.6337599754 - 0.6686064005 0.6883615851 0.6346150041 - 0.6694163084 0.6891968250 0.6354699135 - 0.6702262163 0.6900320053 0.6363248825 - 0.6710332036 0.6908645034 0.6371762753 - 0.6718395948 0.6916965842 0.6380265951 - 0.6726461053 0.6925287247 0.6388769746 - 0.6734504104 0.6933588982 0.6397256255 - 0.6742519736 0.6941872239 0.6405714750 - 0.6750535965 0.6950153708 0.6414173245 - 0.6758551002 0.6958433986 0.6422631145 - 0.6766521931 0.6966664791 0.6431037188 - 0.6774494052 0.6974896193 0.6439439058 - 0.6782464981 0.6983126998 0.6447842121 - 0.6790412068 0.6991328001 0.6456223726 - 0.6798340082 0.6999511123 0.6464583874 - 0.6806266904 0.7007693052 0.6472944021 - 0.6814191937 0.7015869021 0.6481302977 - 0.6822078824 0.7024006248 0.6489617229 - 0.6829966903 0.7032142878 0.6497930884 - 0.6837854981 0.7040280104 0.6506243944 - 0.6845712066 0.7048389912 0.6514533162 - 0.6853544712 0.7056484222 0.6522796154 - 0.6861379147 0.7064577937 0.6531059742 - 0.6869211197 0.7072668076 0.6539323926 - 0.6876996756 0.7080708742 0.6547545791 - 0.6884782910 0.7088748813 0.6555765867 - 0.6892567873 0.7096790075 0.6563987136 - 0.6900330782 0.7104793787 0.6572191119 - 0.6908069253 0.7112764716 0.6580371261 - 0.6915807128 0.7120736241 0.6588550806 - 0.6923545003 0.7128707170 0.6596730947 - 0.6931245923 0.7136620879 0.6604871154 - 0.6938942075 0.7144529819 0.6613001823 - 0.6946637034 0.7152439952 0.6621131897 - 0.6954323053 0.7160329819 0.6629254222 - 0.6961976886 0.7168176174 0.6637337804 - 0.6969630122 0.7176023126 0.6645420790 - 0.6977283955 0.7183870077 0.6653504968 - 0.6984916925 0.7191671133 0.6661558747 - 0.6992534995 0.7199447751 0.6669585705 - 0.7000153065 0.7207224965 0.6677613854 - 0.7007771134 0.7215002179 0.6685642004 - 0.7015355825 0.7222713232 0.6693624258 - 0.7022938132 0.7230420709 0.6701598167 - 0.7030519843 0.7238129973 0.6709572077 - 0.7038096786 0.7245823145 0.6717540026 - 0.7045649290 0.7253469229 0.6725463867 - 0.7053201199 0.7261115909 0.6733387113 - 0.7060751915 0.7268761992 0.6741309762 - 0.7068290710 0.7276378274 0.6749218106 - 0.7075812817 0.7283964157 0.6757097840 - 0.7083334923 0.7291548848 0.6764978766 - 0.7090857029 0.7299134731 0.6772860289 - 0.7098354101 0.7306681275 0.6780707836 - 0.7105838060 0.7314211130 0.6788532734 - 0.7113320827 0.7321742177 0.6796358824 - 0.7120804787 0.7329273224 0.6804183722 - 0.7128257751 0.7336754799 0.6811972857 - 0.7135705948 0.7344232798 0.6819751263 - 0.7143152952 0.7351710200 0.6827530265 - 0.7150601149 0.7359182835 0.6835308075 - 0.7158017159 0.7366598248 0.6843031049 - 0.7165433168 0.7374011874 0.6850749850 - 0.7172849178 0.7381426096 0.6858468056 - 0.7180262208 0.7388827205 0.6866185069 - 0.7187647820 0.7396168113 0.6873862147 - 0.7195035219 0.7403510213 0.6881539822 - 0.7202422023 0.7410851717 0.6889218092 - 0.7209799886 0.7418174744 0.6896888018 - 0.7217146754 0.7425451279 0.6904516816 - 0.7224494219 0.7432727218 0.6912146211 - 0.7231841087 0.7440003157 0.6919776201 - 0.7239174843 0.7447258234 0.6927394867 - 0.7246472836 0.7454472184 0.6934980154 - 0.7253772020 0.7461684942 0.6942564845 - 0.7261071205 0.7468898892 0.6950148940 - 0.7268354893 0.7476091981 0.6957721114 - 0.7275608182 0.7483249903 0.6965253949 - 0.7282860279 0.7490409017 0.6972786784 - 0.7290111780 0.7497566938 0.6980319023 - 0.7297351956 0.7504705787 0.6987839937 - 0.7304564118 0.7511811852 0.6995325089 - 0.7311775088 0.7518917918 0.7002809048 - 0.7318987250 0.7526022792 0.7010293007 - 0.7326188087 0.7533113956 0.7017766237 - 0.7333359718 0.7540174723 0.7025194764 - 0.7340533137 0.7547234893 0.7032623887 - 0.7347705960 0.7554296255 0.7040053010 - 0.7354866862 0.7561342120 0.7047476172 - 0.7361978889 0.7568343878 0.7054864168 - 0.7369092107 0.7575346828 0.7062252164 - 0.7376204133 0.7582349181 0.7069640160 - 0.7383310795 0.7589343190 0.7077025771 - 0.7390366197 0.7596284151 0.7084372044 - 0.7397419810 0.7603225112 0.7091717720 - 0.7404474020 0.7610166073 0.7099062800 - 0.7411528230 0.7617105842 0.7106409073 - 0.7418531179 0.7623990178 0.7113716006 - 0.7425531745 0.7630872726 0.7121018767 - 0.7432532907 0.7637757063 0.7128322124 - 0.7439532876 0.7644640207 0.7135624886 - 0.7446492910 0.7651482821 0.7142896056 - 0.7453442216 0.7658317089 0.7150154114 - 0.7460390925 0.7665150762 0.7157412767 - 0.7467340231 0.7671986222 0.7164672017 - 0.7474262714 0.7678791881 0.7171908021 - 0.7481164932 0.7685580850 0.7179121971 - 0.7488067150 0.7692371011 0.7186335921 - 0.7494968772 0.7699159980 0.7193549871 - 0.7501856089 0.7705929279 0.7200750709 - 0.7508713007 0.7712666988 0.7207918763 - 0.7515569925 0.7719404101 0.7215088010 - 0.7522428036 0.7726141810 0.7222256064 - 0.7529281974 0.7732872963 0.7229424119 - 0.7536085248 0.7739552259 0.7236546278 - 0.7542889118 0.7746230960 0.7243667245 - 0.7549691796 0.7752910256 0.7250788808 - 0.7556495070 0.7759588957 0.7257910967 - 0.7563261986 0.7766230106 0.7264999747 - 0.7570015788 0.7772859931 0.7272074223 - 0.7576768994 0.7779489756 0.7279146910 - 0.7583522797 0.7786120772 0.7286220789 - 0.7590258718 0.7792726755 0.7293279171 - 0.7596967220 0.7799301147 0.7300304770 - 0.7603675127 0.7805876136 0.7307329774 - 0.7610384226 0.7812449932 0.7314354777 - 0.7617092133 0.7819021940 0.7321380973 - 0.7623745203 0.7825548053 0.7328367829 - 0.7630398870 0.7832074165 0.7335352898 - 0.7637053132 0.7838600278 0.7342339158 - 0.7643706203 0.7845125198 0.7349324226 - 0.7650331855 0.7851625085 0.7356290221 - 0.7656937242 0.7858108282 0.7363240123 - 0.7663542032 0.7864592075 0.7370188832 - 0.7670148015 0.7871075869 0.7377138734 - 0.7676746845 0.7877553105 0.7384086251 - 0.7683306932 0.7884001732 0.7390993834 - 0.7689867020 0.7890449762 0.7397902012 - 0.7696425915 0.7896898985 0.7404810786 - 0.7702986002 0.7903347015 0.7411718965 - 0.7709521055 0.7909758091 0.7418606281 - 0.7716041803 0.7916151285 0.7425476909 - 0.7722561955 0.7922543287 0.7432348728 - 0.7729082108 0.7928935289 0.7439219952 - 0.7735598087 0.7935318947 0.7446089983 - 0.7742068172 0.7941648960 0.7452918887 - 0.7748538256 0.7947977781 0.7459747791 - 0.7755008936 0.7954307795 0.7466577291 - 0.7761479020 0.7960637808 0.7473406196 - 0.7767928243 0.7966936827 0.7480217814 - 0.7774356008 0.7973213196 0.7487009168 - 0.7780783772 0.7979490161 0.7493799925 - 0.7787212729 0.7985765934 0.7500591278 - 0.7793641090 0.7992041707 0.7507382035 - 0.7800034285 0.7998272181 0.7514144778 - 0.7806422710 0.8004500270 0.7520902753 - 0.7812811732 0.8010727763 0.7527660728 - 0.7819200754 0.8016955256 0.7534418702 - 0.7825579047 0.8023167253 0.7541171908 - 0.7831923962 0.8029342890 0.7547907233 - 0.7838268876 0.8035517931 0.7554641962 - 0.7844614983 0.8041694164 0.7561377287 - 0.7850959897 0.8047869205 0.7568112016 - 0.7857282162 0.8054007888 0.7574830055 - 0.7863587141 0.8060125113 0.7581532001 - 0.7869892716 0.8066241741 0.7588235140 - 0.7876198292 0.8072358966 0.7594937086 - 0.7882503867 0.8078476191 0.7601639032 - 0.7888780832 0.8084548712 0.7608323097 - 0.7895054221 0.8090618253 0.7615002990 - 0.7901325822 0.8096686006 0.7621682286 - 0.7907598019 0.8102754951 0.7628362179 - 0.7913867235 0.8108816743 0.7635040283 - 0.7920100093 0.8114830852 0.7641686797 - 0.7926331758 0.8120846152 0.7648333907 - 0.7932564020 0.8126860857 0.7654981017 - 0.7938796282 0.8132876158 0.7661628127 - 0.7945017815 0.8138875961 0.7668269873 - 0.7951213717 0.8144844770 0.7674893737 - 0.7957410812 0.8150814176 0.7681518197 - 0.7963607907 0.8156782985 0.7688142061 - 0.7969803810 0.8162751794 0.7694767118 - 0.7975987792 0.8168696761 0.7701377869 - 0.7982153893 0.8174614906 0.7707970738 - 0.7988319993 0.8180533051 0.7714563012 - 0.7994486094 0.8186451197 0.7721155286 - 0.8000652790 0.8192368746 0.7727746964 - 0.8006798029 0.8198254108 0.7734327912 - 0.8012925982 0.8204118013 0.7740896940 - 0.8019053936 0.8209980726 0.7747467160 - 0.8025181890 0.8215845227 0.7754036784 - 0.8031309843 0.8221707940 0.7760605812 - 0.8037403226 0.8227540851 0.7767156959 - 0.8043475151 0.8233358264 0.7773694992 - 0.8049547076 0.8239176273 0.7780233026 - 0.8055619001 0.8244994283 0.7786769867 - 0.8061690927 0.8250811100 0.7793307900 - 0.8067730069 0.8256602883 0.7799832225 - 0.8073750138 0.8262382746 0.7806347013 - 0.8079770803 0.8268163800 0.7812862992 - 0.8085792065 0.8273944855 0.7819377780 - 0.8091812730 0.8279724717 0.7825893164 - 0.8097804189 0.8285468221 0.7832390070 - 0.8103780150 0.8291193843 0.7838873267 - 0.8109756112 0.8296921253 0.7845357060 - 0.8115732074 0.8302646875 0.7851840854 - 0.8121708035 0.8308374286 0.7858325243 - 0.8127658963 0.8314061761 0.7864795923 - 0.8133594990 0.8319730759 0.7871257067 - 0.8139529824 0.8325399160 0.7877718806 - 0.8145465851 0.8331068158 0.7884181142 - 0.8151401281 0.8336737156 0.7890642285 - 0.8157312274 0.8342378736 0.7897089124 - 0.8163201213 0.8348001242 0.7903519273 - 0.8169090152 0.8353623152 0.7909948826 - 0.8174979091 0.8359245062 0.7916380167 - 0.8180868030 0.8364866972 0.7922809720 - 0.8186740875 0.8370468020 0.7929232717 - 0.8192589283 0.8376039267 0.7935640812 - 0.8198438287 0.8381611109 0.7942048907 - 0.8204286098 0.8387181759 0.7948457003 - 0.8210135102 0.8392754197 0.7954866290 - 0.8215975761 0.8398311734 0.7961269021 - 0.8221790195 0.8403828740 0.7967646122 - 0.8227605224 0.8409346938 0.7974023223 - 0.8233419061 0.8414865136 0.7980399728 - 0.8239232898 0.8420382142 0.7986776829 - 0.8245046735 0.8425897956 0.7993155122 - 0.8250821829 0.8431358933 0.7999498844 - 0.8256595731 0.8436819911 0.8005841970 - 0.8262370229 0.8442280889 0.8012183905 - 0.8268144131 0.8447741866 0.8018527031 - 0.8273919225 0.8453204036 0.8024870157 - 0.8279669285 0.8458629251 0.8031185865 - 0.8285411000 0.8464043140 0.8037490249 - 0.8291152716 0.8469458222 0.8043794036 - 0.8296893835 0.8474872112 0.8050099015 - 0.8302636147 0.8480286002 0.8056402802 - 0.8308361769 0.8485683799 0.8062694073 - 0.8314064741 0.8491060734 0.8068965077 - 0.8319767714 0.8496438265 0.8075236082 - 0.8325471282 0.8501815200 0.8081507087 - 0.8331174850 0.8507192135 0.8087778091 - 0.8336874843 0.8512564898 0.8094046712 - 0.8342546225 0.8517901897 0.8100280762 - 0.8348217010 0.8523237705 0.8106514215 - 0.8353887796 0.8528574705 0.8112748265 - 0.8359557986 0.8533911109 0.8118981123 - 0.8365228772 0.8539248109 0.8125215173 - 0.8370872736 0.8544545770 0.8131418824 - 0.8376504779 0.8549830914 0.8137606978 - 0.8382136822 0.8555114865 0.8143793941 - 0.8387768269 0.8560398817 0.8149982095 - 0.8393399715 0.8565682769 0.8156170249 - 0.8399021029 0.8570951819 0.8162348866 - 0.8404610157 0.8576182127 0.8168491721 - 0.8410199285 0.8581411242 0.8174635172 - 0.8415787816 0.8586640954 0.8180779219 - 0.8421378136 0.8591871262 0.8186922073 - 0.8426967263 0.8597099781 0.8193064928 - 0.8432520032 0.8602293730 0.8199177980 - 0.8438060880 0.8607478738 0.8205277920 - 0.8443602920 0.8612663150 0.8211377859 - 0.8449144959 0.8617848158 0.8217477798 - 0.8454685807 0.8623031974 0.8223577738 - 0.8460217714 0.8628203869 0.8229669929 - 0.8465716839 0.8633341193 0.8235728145 - 0.8471217155 0.8638477921 0.8241786957 - 0.8476716280 0.8643614054 0.8247845173 - 0.8482214808 0.8648750782 0.8253902793 - 0.8487715125 0.8653888106 0.8259961009 - 0.8493191004 0.8658990860 0.8265994787 - 0.8498654962 0.8664078712 0.8272011876 - 0.8504120111 0.8669167757 0.8278030157 - 0.8509584069 0.8674256206 0.8284047246 - 0.8515049219 0.8679344058 0.8290064931 - 0.8520511985 0.8684428930 0.8296083212 - 0.8525946736 0.8689479232 0.8302060962 - 0.8531380892 0.8694528937 0.8308039904 - 0.8536815047 0.8699579239 0.8314018250 - 0.8542249799 0.8704627752 0.8319995999 - 0.8547683954 0.8709678054 0.8325974941 - 0.8553109765 0.8714712262 0.8331940770 - 0.8558521867 0.8719722033 0.8337880969 - 0.8563933969 0.8724731803 0.8343821168 - 0.8569346070 0.8729742765 0.8349761963 - 0.8574758172 0.8734753132 0.8355702162 - 0.8580170274 0.8739762902 0.8361642957 - 0.8585556149 0.8744739294 0.8367559910 - 0.8590927720 0.8749697804 0.8373463750 - 0.8596299887 0.8754656911 0.8379368186 - 0.8601672053 0.8759617209 0.8385272026 - 0.8607044220 0.8764575720 0.8391175270 - 0.8612416983 0.8769536018 0.8397079110 - 0.8617749810 0.8774446845 0.8402953148 - 0.8623077869 0.8779354095 0.8408821225 - 0.8628407121 0.8784260750 0.8414688706 - 0.8633735776 0.8789168000 0.8420556784 - 0.8639063835 0.8794075251 0.8426426053 - 0.8644390106 0.8798975945 0.8432291746 - 0.8649681211 0.8803830147 0.8438127041 - 0.8654972911 0.8808683753 0.8443961143 - 0.8660264015 0.8813537955 0.8449795246 - 0.8665555716 0.8818392158 0.8455628753 - 0.8670848012 0.8823245764 0.8461462855 - 0.8676131964 0.8828086853 0.8467290998 - 0.8681393862 0.8832886815 0.8473091722 - 0.8686655164 0.8837687969 0.8478893042 - 0.8691915870 0.8842489123 0.8484694958 - 0.8697177172 0.8847289085 0.8490496278 - 0.8702437878 0.8852090240 0.8496297002 - 0.8707692027 0.8856875896 0.8502088785 - 0.8712928891 0.8861634731 0.8507859111 - 0.8718166947 0.8866394162 0.8513628244 - 0.8723403811 0.8871151805 0.8519396782 - 0.8728641272 0.8875911236 0.8525167108 - 0.8733878732 0.8880670071 0.8530936241 - 0.8739106059 0.8885408044 0.8536695838 - 0.8744313717 0.8890113831 0.8542435169 - 0.8749523163 0.8894819021 0.8548172712 - 0.8754730821 0.8899524808 0.8553912044 - 0.8759940267 0.8904231191 0.8559650779 - 0.8765147924 0.8908935785 0.8565390110 - 0.8770341873 0.8913617134 0.8571118712 - 0.8775507808 0.8918259144 0.8576828837 - 0.8780673742 0.8922899961 0.8582537770 - 0.8785840869 0.8927540779 0.8588247895 - 0.8791006804 0.8932182193 0.8593956828 - 0.8796172738 0.8936823010 0.8599665761 - 0.8801329136 0.8941447139 0.8605368137 - 0.8806458712 0.8946034908 0.8611049056 - 0.8811588883 0.8950623274 0.8616731167 - 0.8816719055 0.8955211043 0.8622412086 - 0.8821849227 0.8959800005 0.8628093004 - 0.8826978803 0.8964387774 0.8633773923 - 0.8832104206 0.8968967199 0.8639450073 - 0.8837205172 0.8973513246 0.8645104170 - 0.8842306733 0.8978059292 0.8650758266 - 0.8847408891 0.8982604742 0.8656411767 - 0.8852511048 0.8987150788 0.8662065864 - 0.8857613206 0.8991696835 0.8667719960 - 0.8862714171 0.8996242285 0.8673374057 - 0.8867782950 0.9000756741 0.8679000735 - 0.8872851729 0.9005271792 0.8684629202 - 0.8877921104 0.9009786844 0.8690257072 - 0.8882989883 0.9014301896 0.8695884943 - 0.8888059855 0.9018816948 0.8701512218 - 0.8893129230 0.9023333192 0.8707140088 - 0.8898167014 0.9027820826 0.8712748289 - 0.8903198838 0.9032304287 0.8718351126 - 0.8908231258 0.9036787152 0.8723952770 - 0.8913263083 0.9041271210 0.8729556203 - 0.8918294907 0.9045754075 0.8735159039 - 0.8923326731 0.9050236940 0.8740761280 - 0.8928335905 0.9054703116 0.8746351004 - 0.8933327794 0.9059156179 0.8751929998 - 0.8938320279 0.9063609838 0.8757507801 - 0.8943310976 0.9068062901 0.8763086796 - 0.8948302865 0.9072515965 0.8768665791 - 0.8953294754 0.9076969028 0.8774244785 - 0.8958278894 0.9081414938 0.8779817820 - 0.8963238001 0.9085838795 0.8785374165 - 0.8968197107 0.9090263247 0.8790928721 - 0.8973156810 0.9094687104 0.8796485066 - 0.8978115916 0.9099110961 0.8802040815 - 0.8983076215 0.9103534818 0.8807597160 - 0.8988034725 0.9107959270 0.8813151717 - 0.8992962837 0.9112346172 0.8818688989 - 0.8997886181 0.9116730094 0.8824223280 - 0.9002810121 0.9121112823 0.8829756975 - 0.9007734060 0.9125496745 0.8835291266 - 0.9012656808 0.9129880071 0.8840824962 - 0.9017580748 0.9134262800 0.8846358061 - 0.9022489786 0.9138627052 0.8851882815 - 0.9027383924 0.9142972231 0.8857396245 - 0.9032276869 0.9147316813 0.8862909079 - 0.9037171006 0.9151660800 0.8868421912 - 0.9042065144 0.9156005979 0.8873934746 - 0.9046958089 0.9160349965 0.8879448175 - 0.9051852226 0.9164693952 0.8884961009 - 0.9056711793 0.9169011116 0.8890454173 - 0.9061573148 0.9173327088 0.8895946741 - 0.9066432714 0.9177644253 0.8901439905 - 0.9071292877 0.9181960821 0.8906934261 - 0.9076154232 0.9186277986 0.8912426829 - 0.9081013799 0.9190593958 0.8917919993 - 0.9085853100 0.9194893241 0.8923398852 - 0.9090666771 0.9199172854 0.8928862214 - 0.9095481038 0.9203454256 0.8934323788 - 0.9100295901 0.9207733870 0.8939787149 - 0.9105110168 0.9212014079 0.8945249915 - 0.9109923840 0.9216294885 0.8950712085 - 0.9114738703 0.9220575094 0.8956174850 - 0.9119517803 0.9224802852 0.8961616158 - 0.9124293923 0.9229027033 0.8967053890 - 0.9129070044 0.9233251214 0.8972491026 - 0.9133846164 0.9237474799 0.8977928758 - 0.9138621092 0.9241697788 0.8983367085 - 0.9143397212 0.9245921969 0.8988804817 - 0.9148163795 0.9250133038 0.8994235992 - 0.9152907729 0.9254313111 0.8999646902 - 0.9157652259 0.9258493185 0.9005057216 - 0.9162396193 0.9262673259 0.9010468125 - 0.9167140126 0.9266853929 0.9015879035 - 0.9171885252 0.9271034002 0.9021288753 - 0.9176629186 0.9275214076 0.9026700258 - 0.9181352854 0.9279373884 0.9032099247 - 0.9186062217 0.9283521175 0.9037488103 - 0.9190772176 0.9287667274 0.9042876959 - 0.9195482135 0.9291813970 0.9048265815 - 0.9200190902 0.9295961261 0.9053655267 - 0.9204900861 0.9300107956 0.9059044123 - 0.9209610820 0.9304254055 0.9064432979 - 0.9214289188 0.9308364987 0.9069793224 - 0.9218962193 0.9312469959 0.9075146914 - 0.9223635197 0.9316574931 0.9080500007 - 0.9228308201 0.9320679903 0.9085853100 - 0.9232981205 0.9324784875 0.9091206789 - 0.9237653017 0.9328889847 0.9096559882 - 0.9242324829 0.9332993031 0.9101912975 - 0.9246969223 0.9337061048 0.9107241035 - 0.9251613021 0.9341129065 0.9112569094 - 0.9256256819 0.9345198274 0.9117897153 - 0.9260901213 0.9349266291 0.9123224020 - 0.9265545011 0.9353333712 0.9128552079 - 0.9270188808 0.9357401729 0.9133880138 - 0.9274827838 0.9361463189 0.9139202237 - 0.9279441237 0.9365494251 0.9144498706 - 0.9284055233 0.9369524121 0.9149795175 - 0.9288668036 0.9373555183 0.9155091047 - 0.9293280840 0.9377586246 0.9160386920 - 0.9297894835 0.9381616116 0.9165682793 - 0.9302508235 0.9385647178 0.9170979261 - 0.9307109714 0.9389663935 0.9176266789 - 0.9311680794 0.9393649101 0.9181531072 - 0.9316251278 0.9397633076 0.9186794758 - 0.9320821762 0.9401618242 0.9192059040 - 0.9325392842 0.9405602813 0.9197322726 - 0.9329962730 0.9409586787 0.9202587008 - 0.9334533811 0.9413571954 0.9207851291 - 0.9339085817 0.9417545795 0.9213101268 - 0.9343593717 0.9421498775 0.9218313098 - 0.9348102212 0.9425451756 0.9223526120 - 0.9352610111 0.9429404736 0.9228739142 - 0.9357118011 0.9433357716 0.9233952165 - 0.9361625910 0.9437311292 0.9239163995 - 0.9366133809 0.9441264272 0.9244377017 - 0.9370620251 0.9445207715 0.9249573946 - 0.9375048280 0.9449130297 0.9254726171 - 0.9379475713 0.9453052282 0.9259877801 - 0.9383903742 0.9456974268 0.9265030026 - 0.9388331771 0.9460896254 0.9270182252 - 0.9392759800 0.9464818239 0.9275333881 - 0.9397187829 0.9468740225 0.9280486107 - 0.9401605129 0.9472647905 0.9285627007 - 0.9405969977 0.9476501942 0.9290714860 - 0.9410336018 0.9480355978 0.9295802116 - 0.9414702058 0.9484211206 0.9300889969 - 0.9419068098 0.9488065243 0.9305977225 - 0.9423432946 0.9491919279 0.9311063886 - 0.9427798986 0.9495772719 0.9316151738 - 0.9432163239 0.9499623179 0.9321237803 - 0.9436482191 0.9503390789 0.9326291084 - 0.9440801740 0.9507158995 0.9331343770 - 0.9445121288 0.9510927200 0.9336397052 - 0.9449440837 0.9514694810 0.9341449142 - 0.9453759789 0.9518463016 0.9346501827 - 0.9458078742 0.9522231221 0.9351555109 - 0.9462398887 0.9526000023 0.9356607795 - 0.9466683865 0.9529722929 0.9361640811 - 0.9470962882 0.9533438087 0.9366669059 - 0.9475240707 0.9537153840 0.9371697903 - 0.9479519725 0.9540868998 0.9376726151 - 0.9483798146 0.9544584155 0.9381753802 - 0.9488077164 0.9548299909 0.9386783242 - 0.9492354989 0.9552015066 0.9391810894 - 0.9496610165 0.9555711746 0.9396826029 - 0.9500846863 0.9559394717 0.9401828051 - 0.9505084157 0.9563078284 0.9406831264 - 0.9509320855 0.9566761255 0.9411833882 - 0.9513558149 0.9570444226 0.9416837096 - 0.9517794847 0.9574127197 0.9421839714 - 0.9522032142 0.9577810168 0.9426841736 - 0.9526256919 0.9581483006 0.9431836009 - 0.9530442953 0.9585124254 0.9436798096 - 0.9534628987 0.9588766098 0.9441760778 - 0.9538815022 0.9592407942 0.9446722865 - 0.9543001056 0.9596049190 0.9451684952 - 0.9547187090 0.9599691033 0.9456648231 - 0.9551373124 0.9603332281 0.9461609721 - 0.9555559158 0.9606974125 0.9466571808 - 0.9559701085 0.9610580802 0.9471496940 - 0.9563835859 0.9614182115 0.9476413727 - 0.9567971230 0.9617784023 0.9481331706 - 0.9572104812 0.9621384740 0.9486249089 - 0.9576240182 0.9624987245 0.9491167068 - 0.9580374956 0.9628589153 0.9496085048 - 0.9584509730 0.9632189870 0.9501001835 - 0.9588623047 0.9635775089 0.9505898952 - 0.9592704177 0.9639338851 0.9510766864 - 0.9596785903 0.9642902017 0.9515634179 - 0.9600868225 0.9646465182 0.9520500898 - 0.9604949951 0.9650027752 0.9525368214 - 0.9609032273 0.9653592110 0.9530236125 - 0.9613113999 0.9657155275 0.9535102844 - 0.9617195725 0.9660717845 0.9539970160 - 0.9621226192 0.9664239883 0.9544786215 - 0.9625251889 0.9667757750 0.9549596906 - 0.9629278183 0.9671275020 0.9554407001 - 0.9633303285 0.9674792886 0.9559217095 - 0.9637328982 0.9678310752 0.9564027190 - 0.9641355276 0.9681829214 0.9568837285 - 0.9645379782 0.9685347080 0.9573647976 - 0.9649384022 0.9688847065 0.9578434825 - 0.9653344154 0.9692314267 0.9583176970 - 0.9657304883 0.9695780873 0.9587917924 - 0.9661265016 0.9699246883 0.9592658877 - 0.9665225148 0.9702714086 0.9597399831 - 0.9669185281 0.9706181288 0.9602141976 - 0.9673146009 0.9709647894 0.9606882930 - 0.9677106142 0.9713115096 0.9611623883 - 0.9681016803 0.9716545939 0.9616312981 - 0.9684907198 0.9719963074 0.9620978236 - 0.9688798189 0.9723380208 0.9625642896 - 0.9692687988 0.9726796746 0.9630308151 - 0.9696578979 0.9730213881 0.9634972811 - 0.9700468779 0.9733629823 0.9639638066 - 0.9704359174 0.9737046957 0.9644302726 - 0.9708248973 0.9740462899 0.9648967981 - 0.9712070227 0.9743828177 0.9653550982 - 0.9715890288 0.9747192860 0.9658132792 - 0.9719710946 0.9750556946 0.9662715793 - 0.9723531008 0.9753922224 0.9667298794 - 0.9727351069 0.9757285714 0.9671881199 - 0.9731171727 0.9760650992 0.9676464200 - 0.9734991789 0.9764015079 0.9681046009 - 0.9738795161 0.9767364264 0.9685608149 - 0.9742544293 0.9770668149 0.9690101147 - 0.9746292830 0.9773972034 0.9694594145 - 0.9750041962 0.9777275920 0.9699087143 - 0.9753791094 0.9780579805 0.9703580141 - 0.9757540226 0.9783884883 0.9708073139 - 0.9761288762 0.9787188768 0.9712566137 - 0.9765037894 0.9790493250 0.9717059135 - 0.9768754840 0.9793767929 0.9721511006 - 0.9772428274 0.9797003865 0.9725903869 - 0.9776101112 0.9800240993 0.9730296731 - 0.9779773951 0.9803476930 0.9734690785 - 0.9783446789 0.9806714058 0.9739084244 - 0.9787119031 0.9809949994 0.9743477702 - 0.9790791869 0.9813187122 0.9747871161 - 0.9794464707 0.9816424251 0.9752265215 - 0.9798092246 0.9819620252 0.9756602049 - 0.9801679254 0.9822781086 0.9760888815 - 0.9805266261 0.9825941920 0.9765176177 - 0.9808852077 0.9829102755 0.9769461751 - 0.9812439084 0.9832264185 0.9773749113 - 0.9816026092 0.9835425019 0.9778035879 - 0.9819613099 0.9838585854 0.9782322049 - 0.9823198915 0.9841747284 0.9786608815 - 0.9826725125 0.9844858050 0.9790827036 - 0.9830206037 0.9847931862 0.9794991016 - 0.9833686948 0.9851006866 0.9799156189 - 0.9837167263 0.9854081273 0.9803320765 - 0.9840648174 0.9857155085 0.9807485938 - 0.9844129086 0.9860228896 0.9811649919 - 0.9847608805 0.9863303900 0.9815815091 - 0.9851089716 0.9866377711 0.9819980264 - 0.9854506254 0.9869400859 0.9824073911 - 0.9857869148 0.9872382283 0.9828109741 - 0.9861232042 0.9875363708 0.9832146168 - 0.9864594936 0.9878345132 0.9836181998 - 0.9867957830 0.9881327152 0.9840217233 - 0.9871320724 0.9884307981 0.9844253063 - 0.9874684215 0.9887290001 0.9848288894 - 0.9878047109 0.9890270829 0.9852324724 - 0.9881356955 0.9893202186 0.9856290817 - 0.9884604216 0.9896075130 0.9860175252 - 0.9887850881 0.9898949265 0.9864059091 - 0.9891096950 0.9901822209 0.9867942929 - 0.9894344211 0.9904695153 0.9871826768 - 0.9897590876 0.9907568097 0.9875711203 - 0.9900838137 0.9910441041 0.9879595041 - 0.9904084802 0.9913313985 0.9883478880 - 0.9907290936 0.9916148782 0.9887315035 - 0.9910402894 0.9918898940 0.9891040921 - 0.9913514853 0.9921647906 0.9894766212 - 0.9916628003 0.9924398065 0.9898492098 - 0.9919739962 0.9927147031 0.9902216792 - 0.9922851920 0.9929897189 0.9905943274 - 0.9925965071 0.9932646751 0.9909667969 - 0.9929077029 0.9935395718 0.9913393855 - 0.9932175875 0.9938135147 0.9917104244 - 0.9935122728 0.9940760732 0.9920637012 - 0.9938070774 0.9943386912 0.9924169779 - 0.9941018224 0.9946013093 0.9927703142 - 0.9943966269 0.9948639274 0.9931235909 - 0.9946913123 0.9951264858 0.9934769273 - 0.9949861169 0.9953891039 0.9938303232 - 0.9952808022 0.9956517220 0.9941835999 - 0.9955756068 0.9959142804 0.9945368767 - 0.9958524704 0.9961653948 0.9948744774 - 0.9961246848 0.9964134097 0.9952080250 - 0.9963967800 0.9966614842 0.9955413938 - 0.9966689944 0.9969094992 0.9958748817 - 0.9969412088 0.9971575141 0.9962083101 - 0.9972134233 0.9974055886 0.9965417981 - 0.9974855781 0.9976536036 0.9968752265 - 0.9977576733 0.9979016185 0.9972087145 - 0.9980192780 0.9981427789 0.9975314736 - 0.9982668757 0.9983748794 0.9978399873 - 0.9985144734 0.9986070991 0.9981486201 - 0.9987620711 0.9988391995 0.9984571934 - 0.9990097284 0.9990714192 0.9987657070 - 0.9992572069 0.9993035197 0.9990742803 - 0.9995048046 0.9995356798 0.9993829131 - 0.9997524023 0.9997677803 0.9996914268 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0076661762 0.0083268499 0.0068440801 + 0.0158262700 0.0170417801 0.0131509099 + 0.0232411809 0.0250322502 0.0190004501 + 0.0300489496 0.0323281400 0.0244449992 + 0.0363852791 0.0390635394 0.0295583308 + 0.0423460715 0.0453425013 0.0343772098 + 0.0479932986 0.0512349084 0.0389431007 + 0.0533834286 0.0567868613 0.0432897806 + 0.0585362613 0.0620452613 0.0474547893 + 0.0634827465 0.0670664310 0.0514379889 + 0.0682476535 0.0719086230 0.0552622713 + 0.0728643686 0.0766109228 0.0590022616 + 0.0773498490 0.0812185779 0.0626629591 + 0.0817248896 0.0857366771 0.0662265718 + 0.0860022604 0.0901816636 0.0696830973 + 0.0901876912 0.0945459083 0.0730567127 + 0.0942838192 0.0988429263 0.0763788819 + 0.0983011201 0.1030735970 0.0796598792 + 0.1022287980 0.1072389036 0.0828902796 + 0.1060905010 0.1113395020 0.0860634297 + 0.1098807007 0.1153889000 0.0891731530 + 0.1136023998 0.1193716973 0.0922177732 + 0.1172552034 0.1232964024 0.0952064171 + 0.1208416000 0.1271591038 0.0981551483 + 0.1243667975 0.1309625953 0.1010731980 + 0.1278326958 0.1347209960 0.1039592028 + 0.1312370002 0.1384246945 0.1068119034 + 0.1345919073 0.1420657933 0.1096345037 + 0.1378999949 0.1456594020 0.1124313995 + 0.1411581933 0.1492089927 0.1151997000 + 0.1443776041 0.1527009010 0.1179367006 + 0.1475483030 0.1561467052 0.1206402034 + 0.1506752074 0.1595402956 0.1233116984 + 0.1537552029 0.1628801972 0.1259557009 + 0.1567965001 0.1661709994 0.1285720021 + 0.1598031074 0.1694162935 0.1311601996 + 0.1627652049 0.1726270020 0.1337202936 + 0.1656907946 0.1757915020 0.1362521052 + 0.1685829014 0.1789050996 0.1387566030 + 0.1714359969 0.1819735020 0.1412398964 + 0.1742600948 0.1850046962 0.1437025964 + 0.1770537049 0.1879996955 0.1461455971 + 0.1798166037 0.1909551024 0.1485693008 + 0.1825578958 0.1938706934 0.1509743035 + 0.1852674037 0.1967559010 0.1533614993 + 0.1879525036 0.1996030957 0.1557313949 + 0.1906042993 0.2024182975 0.1580837965 + 0.1932311952 0.2052014023 0.1604201049 + 0.1958318949 0.2079581022 0.1627393067 + 0.1984055936 0.2106810957 0.1650432050 + 0.2009532005 0.2133824974 0.1673322022 + 0.2034747005 0.2160581946 0.1696034074 + 0.2059706002 0.2187004983 0.1718567014 + 0.2084430009 0.2213200927 0.1740909964 + 0.2108965069 0.2239086032 0.1763089001 + 0.2133263052 0.2264758050 0.1785116047 + 0.2157393992 0.2290169001 0.1806990951 + 0.2181292027 0.2315327972 0.1828715056 + 0.2205027044 0.2340238988 0.1850311011 + 0.2228585035 0.2364936024 0.1871781945 + 0.2251935005 0.2389385998 0.1893118024 + 0.2275121957 0.2413554937 0.1914346963 + 0.2298126966 0.2437552065 0.1935469061 + 0.2320972979 0.2461318970 0.1956477016 + 0.2343686968 0.2484831065 0.1977396011 + 0.2366248071 0.2508153021 0.1998216063 + 0.2388661057 0.2531288862 0.2018945962 + 0.2410929054 0.2554250956 0.2039602995 + 0.2433024943 0.2577019930 0.2060160935 + 0.2454963028 0.2599579096 0.2080630958 + 0.2476740032 0.2621991932 0.2100999057 + 0.2498372942 0.2644275129 0.2121268064 + 0.2519871891 0.2666364014 0.2141437978 + 0.2541207969 0.2688263953 0.2161484957 + 0.2562352121 0.2710013986 0.2181435972 + 0.2583388984 0.2731583118 0.2201271951 + 0.2604309916 0.2752990127 0.2221024036 + 0.2625060081 0.2774248123 0.2240664065 + 0.2645739019 0.2795382142 0.2260230929 + 0.2666332126 0.2816390097 0.2279690057 + 0.2686808109 0.2837243974 0.2299083024 + 0.2707110047 0.2857916951 0.2318374068 + 0.2727304995 0.2878476977 0.2337605953 + 0.2747409046 0.2898918092 0.2356743068 + 0.2767409086 0.2919228971 0.2375825942 + 0.2787257135 0.2939389050 0.2394821942 + 0.2807008028 0.2959417999 0.2413766980 + 0.2826663852 0.2979333103 0.2432633042 + 0.2846207917 0.2999132872 0.2451453060 + 0.2865644097 0.3018836081 0.2470199019 + 0.2884986103 0.3038400114 0.2488902956 + 0.2904250026 0.3057815135 0.2507540882 + 0.2923448086 0.3077118099 0.2526113987 + 0.2942551076 0.3096336126 0.2544620037 + 0.2961547077 0.3115448952 0.2563053966 + 0.2980456948 0.3134449124 0.2581430972 + 0.2999280095 0.3153319061 0.2599739134 + 0.3018029034 0.3172087073 0.2617988884 + 0.3036679029 0.3190751076 0.2636143863 + 0.3055250049 0.3209312856 0.2654233873 + 0.3073746860 0.3227779865 0.2672255039 + 0.3092148006 0.3246138990 0.2690199912 + 0.3110457957 0.3264397979 0.2708095908 + 0.3128677905 0.3282575011 0.2725906968 + 0.3146800101 0.3300665915 0.2743658125 + 0.3164839149 0.3318657875 0.2761324048 + 0.3182795942 0.3336552978 0.2778904140 + 0.3200652003 0.3354372978 0.2796432972 + 0.3218409121 0.3372116983 0.2813872993 + 0.3236100078 0.3389771879 0.2831256986 + 0.3253748119 0.3407320082 0.2848586142 + 0.3271335959 0.3424785137 0.2865825891 + 0.3288846016 0.3442190886 0.2883006930 + 0.3306263983 0.3459519148 0.2900108993 + 0.3323577940 0.3476763070 0.2917132080 + 0.3340801001 0.3493919075 0.2934105098 + 0.3357945085 0.3511008918 0.2951011062 + 0.3375023901 0.3528040051 0.2967849970 + 0.3392041922 0.3545002043 0.2984645963 + 0.3408981860 0.3561894000 0.3001382947 + 0.3425840139 0.3578724861 0.3018057048 + 0.3442628086 0.3595485985 0.3034670949 + 0.3459351063 0.3612180948 0.3051221967 + 0.3476001024 0.3628816009 0.3067719936 + 0.3492591083 0.3645390868 0.3084169924 + 0.3509126008 0.3661913872 0.3100563884 + 0.3525592089 0.3678377867 0.3116889000 + 0.3541980982 0.3694776893 0.3133162856 + 0.3558301926 0.3711101115 0.3149392903 + 0.3574565947 0.3727348149 0.3165563047 + 0.3590759039 0.3743531108 0.3181675971 + 0.3606899083 0.3759661019 0.3197731972 + 0.3622973859 0.3775736094 0.3213737905 + 0.3638975024 0.3791748881 0.3229694963 + 0.3654907048 0.3807713985 0.3245606124 + 0.3670778871 0.3823625147 0.3261463046 + 0.3686599135 0.3839488924 0.3277261853 + 0.3702372015 0.3855310977 0.3293007910 + 0.3718093038 0.3871083856 0.3308694959 + 0.3733766973 0.3886804879 0.3324309886 + 0.3749383092 0.3902452886 0.3339884877 + 0.3764936924 0.3918035924 0.3355414867 + 0.3780421019 0.3933547139 0.3370898068 + 0.3795835972 0.3948993087 0.3386338055 + 0.3811194897 0.3964386880 0.3401749134 + 0.3826521933 0.3979738951 0.3417122960 + 0.3841798007 0.3995029926 0.3432452977 + 0.3857024908 0.4010264874 0.3447737098 + 0.3872196972 0.4025439918 0.3462958038 + 0.3887315094 0.4040569961 0.3478113115 + 0.3902386129 0.4055672884 0.3493227065 + 0.3917382061 0.4070734084 0.3508295119 + 0.3932329118 0.4085750878 0.3523303866 + 0.3947235942 0.4100722969 0.3538272977 + 0.3962103128 0.4115647078 0.3553214967 + 0.3976925015 0.4130522907 0.3568125963 + 0.3991701007 0.4145320952 0.3582983017 + 0.4006435871 0.4160074890 0.3597793877 + 0.4021126926 0.4174784124 0.3612557054 + 0.4035769105 0.4189445972 0.3627254069 + 0.4050329924 0.4204039872 0.3641889989 + 0.4064843059 0.4218592048 0.3656494021 + 0.4079307020 0.4233103096 0.3671061099 + 0.4093720019 0.4247572124 0.3685575128 + 0.4108068049 0.4261992872 0.3700045943 + 0.4122380912 0.4276376069 0.3714494109 + 0.4136658907 0.4290730059 0.3728919923 + 0.4150888920 0.4305058122 0.3743301034 + 0.4165056050 0.4319339991 0.3757638931 + 0.4179174900 0.4333589077 0.3771938980 + 0.4193247855 0.4347805977 0.3786191940 + 0.4207271934 0.4361965060 0.3800374866 + 0.4221262932 0.4376091063 0.3814533055 + 0.4235219955 0.4390187860 0.3828668892 + 0.4249120951 0.4404245913 0.3842754960 + 0.4262982905 0.4418255091 0.3856791854 + 0.4276815057 0.4432229102 0.3870786130 + 0.4290600121 0.4446159899 0.3884716928 + 0.4304330051 0.4460026920 0.3898597956 + 0.4318026900 0.4473862946 0.3912445009 + 0.4331685901 0.4487664998 0.3926242888 + 0.4345290065 0.4501408041 0.3940002024 + 0.4358857870 0.4515124857 0.3953734040 + 0.4372372925 0.4528811872 0.3967424929 + 0.4385837018 0.4542450011 0.3981088102 + 0.4399270117 0.4556061029 0.3994731903 + 0.4412651956 0.4569629133 0.4008344114 + 0.4425975978 0.4583143890 0.4021924138 + 0.4439268112 0.4596636891 0.4035479128 + 0.4452503920 0.4610106945 0.4048978090 + 0.4465709925 0.4623551965 0.4062455893 + 0.4478893876 0.4636982977 0.4075908959 + 0.4492019117 0.4650372863 0.4089311063 + 0.4505116045 0.4663740098 0.4102692008 + 0.4518170953 0.4677081108 0.4116033912 + 0.4531182945 0.4690375030 0.4129329026 + 0.4544177055 0.4703648090 0.4142597914 + 0.4557136893 0.4716871977 0.4155794978 + 0.4570071995 0.4730058014 0.4168972969 + 0.4582982063 0.4743215144 0.4182122946 + 0.4595845044 0.4756304026 0.4195236862 + 0.4608688056 0.4769366980 0.4208334088 + 0.4621477127 0.4782372117 0.4221383929 + 0.4634240866 0.4795337915 0.4234414995 + 0.4646978974 0.4808278084 0.4247420132 + 0.4659669995 0.4821158051 0.4260379970 + 0.4672344029 0.4834016860 0.4273321033 + 0.4684959054 0.4846813977 0.4286206067 + 0.4697546959 0.4859575927 0.4299075007 + 0.4710089862 0.4872294962 0.4311901927 + 0.4722594917 0.4884983897 0.4324694872 + 0.4735080004 0.4897660017 0.4337458909 + 0.4747509062 0.4910294116 0.4350179136 + 0.4759922028 0.4922915101 0.4362885952 + 0.4772273898 0.4935492873 0.4375523031 + 0.4784607887 0.4948048890 0.4388141930 + 0.4796907008 0.4960573018 0.4400705993 + 0.4809170961 0.4973073006 0.4413250089 + 0.4821406901 0.4985556006 0.4425770938 + 0.4833607972 0.4998002946 0.4438262880 + 0.4845795929 0.5010439754 0.4450738132 + 0.4857935905 0.5022826791 0.4463163912 + 0.4870069027 0.5035204291 0.4475578070 + 0.4882164896 0.5047531128 0.4487918913 + 0.4894252121 0.5059841275 0.4500248134 + 0.4906300902 0.5072104931 0.4512509108 + 0.4918332100 0.5084347725 0.4524757862 + 0.4930329919 0.5096561909 0.4536970854 + 0.4942300022 0.5108749270 0.4549165070 + 0.4954235852 0.5120911002 0.4561327994 + 0.4966148138 0.5133035183 0.4573467076 + 0.4978038967 0.5145136714 0.4585579038 + 0.4989897013 0.5157194734 0.4597668052 + 0.5001736879 0.5169236064 0.4609735906 + 0.5013538003 0.5181236863 0.4621770084 + 0.5025321245 0.5193228126 0.4633781016 + 0.5037057996 0.5205178857 0.4645760059 + 0.5048779845 0.5217121840 0.4657720923 + 0.5060449243 0.5229014754 0.4669648111 + 0.5072103143 0.5240899920 0.4681558907 + 0.5083714128 0.5252736807 0.4693433940 + 0.5095313787 0.5264567137 0.4705292881 + 0.5106868744 0.5276340246 0.4717121124 + 0.5118411779 0.5288106203 0.4728935957 + 0.5129907131 0.5299817920 0.4740710855 + 0.5141386986 0.5311521292 0.4752467871 + 0.5152817965 0.5323163867 0.4764178097 + 0.5164232850 0.5334795713 0.4775865078 + 0.5175597072 0.5346372724 0.4787499011 + 0.5186939240 0.5357934833 0.4799101949 + 0.5198233724 0.5369442105 0.4810669124 + 0.5209500790 0.5380924940 0.4822207987 + 0.5220733881 0.5392372012 0.4833728075 + 0.5231938958 0.5403798223 0.4845224917 + 0.5243116021 0.5415189862 0.4856700897 + 0.5254257917 0.5426552296 0.4868142903 + 0.5265377760 0.5437887907 0.4879572093 + 0.5276451111 0.5449187160 0.4890958965 + 0.5287510753 0.5460466743 0.4902341068 + 0.5298516154 0.5471696258 0.4913682044 + 0.5309517980 0.5482918024 0.4925023019 + 0.5320447087 0.5494078994 0.4936308861 + 0.5331376195 0.5505239964 0.4947592020 + 0.5342239141 0.5516330004 0.4958822131 + 0.5353093743 0.5527415872 0.4970037043 + 0.5363904834 0.5538460016 0.4981215894 + 0.5374696255 0.5549491048 0.4992372096 + 0.5385463834 0.5560489893 0.5003510118 + 0.5396206975 0.5571463108 0.5014619827 + 0.5406937003 0.5582417250 0.5025722980 + 0.5417628884 0.5593330860 0.5036795735 + 0.5428320169 0.5604240894 0.5047869086 + 0.5438956022 0.5615094900 0.5058897138 + 0.5449593067 0.5625947714 0.5069922209 + 0.5460199714 0.5636752248 0.5080906749 + 0.5470796227 0.5647544265 0.5091872215 + 0.5481380224 0.5658313036 0.5102818012 + 0.5491948128 0.5669060946 0.5113732815 + 0.5502511859 0.5679798722 0.5124642849 + 0.5513052940 0.5690498948 0.5135501027 + 0.5523592830 0.5701199174 0.5146359205 + 0.5534104109 0.5711855888 0.5157186985 + 0.5544610023 0.5722507834 0.5168005228 + 0.5555099249 0.5733129978 0.5178806782 + 0.5565569997 0.5743728876 0.5189586282 + 0.5576034784 0.5754317045 0.5200362802 + 0.5586465001 0.5764862895 0.5211094022 + 0.5596894026 0.5775408745 0.5221825242 + 0.5607293248 0.5785906911 0.5232514739 + 0.5617681146 0.5796394944 0.5243185163 + 0.5628052950 0.5806865096 0.5253840089 + 0.5638393164 0.5817313194 0.5264452100 + 0.5648733974 0.5827760100 0.5275064707 + 0.5659030080 0.5838158727 0.5285636187 + 0.5669323206 0.5848557949 0.5296201110 + 0.5679587722 0.5858927965 0.5306740999 + 0.5689827204 0.5869277716 0.5317248702 + 0.5700063109 0.5879620910 0.5327755809 + 0.5710247755 0.5889919996 0.5338218212 + 0.5720431805 0.5900217891 0.5348678231 + 0.5730581880 0.5910484195 0.5359107256 + 0.5740705729 0.5920730829 0.5369504094 + 0.5750827193 0.5930969715 0.5379900932 + 0.5760906935 0.5941162705 0.5390231013 + 0.5770987868 0.5951356888 0.5400561094 + 0.5781041980 0.5961515903 0.5410866141 + 0.5791074038 0.5971654058 0.5421143770 + 0.5801107287 0.5981786847 0.5431421995 + 0.5811091065 0.5991874933 0.5441660285 + 0.5821076035 0.6001963019 0.5451896191 + 0.5831037164 0.6012020707 0.5462111831 + 0.5840969086 0.6022052765 0.5472295880 + 0.5850901008 0.6032084823 0.5482478738 + 0.5860803127 0.6042070985 0.5492616296 + 0.5870699286 0.6052052975 0.5502738953 + 0.5880584717 0.6062015891 0.5512850285 + 0.5890443921 0.6071943045 0.5522903204 + 0.5900301933 0.6081869006 0.5532956719 + 0.5910136104 0.6091759801 0.5542973876 + 0.5919954777 0.6101636887 0.5552961826 + 0.5929774046 0.6111510992 0.5562950969 + 0.5939552784 0.6121332049 0.5572906733 + 0.5949330926 0.6131153107 0.5582858920 + 0.5959095955 0.6140955091 0.5592799187 + 0.5968831778 0.6150727272 0.5602700710 + 0.5978568196 0.6160498857 0.5612602830 + 0.5988277197 0.6170232296 0.5622476935 + 0.5997968912 0.6179949045 0.5632326007 + 0.6007661223 0.6189665794 0.5642176270 + 0.6017311215 0.6199334264 0.5651969910 + 0.6026958227 0.6209002137 0.5661752820 + 0.6036596894 0.6218655705 0.5671530962 + 0.6046196222 0.6228268743 0.5681259036 + 0.6055796146 0.6237881780 0.5690985918 + 0.6065375805 0.6247472167 0.5700702071 + 0.6074926853 0.6257035732 0.5710396171 + 0.6084479094 0.6266599894 0.5720090270 + 0.6093999147 0.6276127100 0.5729761720 + 0.6103500724 0.6285637021 0.5739415884 + 0.6113001704 0.6295146942 0.5749070048 + 0.6122449040 0.6304615140 0.5758687854 + 0.6131883860 0.6314076781 0.5768293738 + 0.6141319871 0.6323534846 0.5777900219 + 0.6150689125 0.6332944036 0.5787457228 + 0.6160058975 0.6342352033 0.5797008276 + 0.6169418097 0.6351751089 0.5806555748 + 0.6178718805 0.6361111999 0.5816044807 + 0.6188020110 0.6370472908 0.5825533867 + 0.6197304130 0.6379815936 0.5835011005 + 0.6206536889 0.6389123797 0.5844430923 + 0.6215770841 0.6398432255 0.5853852034 + 0.6224973202 0.6407719851 0.5863258839 + 0.6234118938 0.6416980028 0.5872634053 + 0.6243265271 0.6426240802 0.5882009268 + 0.6252374053 0.6435475945 0.5891368985 + 0.6261436939 0.6444684267 0.5900704265 + 0.6270499825 0.6453893185 0.5910037756 + 0.6279519796 0.6463075876 0.5919355154 + 0.6288496852 0.6472237706 0.5928648710 + 0.6297472715 0.6481400132 0.5937942266 + 0.6306408048 0.6490532160 0.5947210789 + 0.6315302253 0.6499642134 0.5956451297 + 0.6324197054 0.6508752108 0.5965691209 + 0.6333047748 0.6517835855 0.5974907279 + 0.6341857910 0.6526899934 0.5984091759 + 0.6350668073 0.6535964012 0.5993278027 + 0.6359438896 0.6545000076 0.6002436280 + 0.6368169188 0.6554012895 0.6011555791 + 0.6376898289 0.6563025713 0.6020675898 + 0.6385591030 0.6572015882 0.6029770970 + 0.6394237876 0.6580982804 0.6038823724 + 0.6402884722 0.6589950919 0.6047877073 + 0.6411507130 0.6598895788 0.6056910157 + 0.6420087814 0.6607813239 0.6065897942 + 0.6428670287 0.6616728902 0.6074885130 + 0.6437233090 0.6625624895 0.6083862782 + 0.6445748210 0.6634479761 0.6092801094 + 0.6454262733 0.6643334031 0.6101740003 + 0.6462768912 0.6652175188 0.6110675931 + 0.6471220255 0.6660966277 0.6119573116 + 0.6479671001 0.6669756770 0.6128469110 + 0.6488121748 0.6678544283 0.6137366295 + 0.6496508718 0.6687275171 0.6146218181 + 0.6504895091 0.6696006060 0.6155067086 + 0.6513282061 0.6704736948 0.6163915992 + 0.6521630287 0.6713413000 0.6172724962 + 0.6529970765 0.6722081900 0.6181520224 + 0.6538311243 0.6730750799 0.6190316081 + 0.6546623707 0.6739377975 0.6199085712 + 0.6554920077 0.6747987270 0.6207835078 + 0.6563215852 0.6756594777 0.6216583848 + 0.6571496129 0.6765179038 0.6225318909 + 0.6579748988 0.6773731709 0.6234022975 + 0.6588003039 0.6782284975 0.6242725849 + 0.6596251130 0.6790826917 0.6251426935 + 0.6604462862 0.6799324751 0.6260074973 + 0.6612675786 0.6807821989 0.6268723011 + 0.6620888114 0.6816319823 0.6277372241 + 0.6629067063 0.6824771762 0.6286000013 + 0.6637240052 0.6833220124 0.6294621825 + 0.6645414233 0.6841667295 0.6303244233 + 0.6653565764 0.6850084066 0.6311848760 + 0.6661701798 0.6858481765 0.6320434213 + 0.6669837236 0.6866878867 0.6329019070 + 0.6677964926 0.6875264049 0.6337599754 + 0.6686064005 0.6883615851 0.6346150041 + 0.6694163084 0.6891968250 0.6354699135 + 0.6702262163 0.6900320053 0.6363248825 + 0.6710332036 0.6908645034 0.6371762753 + 0.6718395948 0.6916965842 0.6380265951 + 0.6726461053 0.6925287247 0.6388769746 + 0.6734504104 0.6933588982 0.6397256255 + 0.6742519736 0.6941872239 0.6405714750 + 0.6750535965 0.6950153708 0.6414173245 + 0.6758551002 0.6958433986 0.6422631145 + 0.6766521931 0.6966664791 0.6431037188 + 0.6774494052 0.6974896193 0.6439439058 + 0.6782464981 0.6983126998 0.6447842121 + 0.6790412068 0.6991328001 0.6456223726 + 0.6798340082 0.6999511123 0.6464583874 + 0.6806266904 0.7007693052 0.6472944021 + 0.6814191937 0.7015869021 0.6481302977 + 0.6822078824 0.7024006248 0.6489617229 + 0.6829966903 0.7032142878 0.6497930884 + 0.6837854981 0.7040280104 0.6506243944 + 0.6845712066 0.7048389912 0.6514533162 + 0.6853544712 0.7056484222 0.6522796154 + 0.6861379147 0.7064577937 0.6531059742 + 0.6869211197 0.7072668076 0.6539323926 + 0.6876996756 0.7080708742 0.6547545791 + 0.6884782910 0.7088748813 0.6555765867 + 0.6892567873 0.7096790075 0.6563987136 + 0.6900330782 0.7104793787 0.6572191119 + 0.6908069253 0.7112764716 0.6580371261 + 0.6915807128 0.7120736241 0.6588550806 + 0.6923545003 0.7128707170 0.6596730947 + 0.6931245923 0.7136620879 0.6604871154 + 0.6938942075 0.7144529819 0.6613001823 + 0.6946637034 0.7152439952 0.6621131897 + 0.6954323053 0.7160329819 0.6629254222 + 0.6961976886 0.7168176174 0.6637337804 + 0.6969630122 0.7176023126 0.6645420790 + 0.6977283955 0.7183870077 0.6653504968 + 0.6984916925 0.7191671133 0.6661558747 + 0.6992534995 0.7199447751 0.6669585705 + 0.7000153065 0.7207224965 0.6677613854 + 0.7007771134 0.7215002179 0.6685642004 + 0.7015355825 0.7222713232 0.6693624258 + 0.7022938132 0.7230420709 0.6701598167 + 0.7030519843 0.7238129973 0.6709572077 + 0.7038096786 0.7245823145 0.6717540026 + 0.7045649290 0.7253469229 0.6725463867 + 0.7053201199 0.7261115909 0.6733387113 + 0.7060751915 0.7268761992 0.6741309762 + 0.7068290710 0.7276378274 0.6749218106 + 0.7075812817 0.7283964157 0.6757097840 + 0.7083334923 0.7291548848 0.6764978766 + 0.7090857029 0.7299134731 0.6772860289 + 0.7098354101 0.7306681275 0.6780707836 + 0.7105838060 0.7314211130 0.6788532734 + 0.7113320827 0.7321742177 0.6796358824 + 0.7120804787 0.7329273224 0.6804183722 + 0.7128257751 0.7336754799 0.6811972857 + 0.7135705948 0.7344232798 0.6819751263 + 0.7143152952 0.7351710200 0.6827530265 + 0.7150601149 0.7359182835 0.6835308075 + 0.7158017159 0.7366598248 0.6843031049 + 0.7165433168 0.7374011874 0.6850749850 + 0.7172849178 0.7381426096 0.6858468056 + 0.7180262208 0.7388827205 0.6866185069 + 0.7187647820 0.7396168113 0.6873862147 + 0.7195035219 0.7403510213 0.6881539822 + 0.7202422023 0.7410851717 0.6889218092 + 0.7209799886 0.7418174744 0.6896888018 + 0.7217146754 0.7425451279 0.6904516816 + 0.7224494219 0.7432727218 0.6912146211 + 0.7231841087 0.7440003157 0.6919776201 + 0.7239174843 0.7447258234 0.6927394867 + 0.7246472836 0.7454472184 0.6934980154 + 0.7253772020 0.7461684942 0.6942564845 + 0.7261071205 0.7468898892 0.6950148940 + 0.7268354893 0.7476091981 0.6957721114 + 0.7275608182 0.7483249903 0.6965253949 + 0.7282860279 0.7490409017 0.6972786784 + 0.7290111780 0.7497566938 0.6980319023 + 0.7297351956 0.7504705787 0.6987839937 + 0.7304564118 0.7511811852 0.6995325089 + 0.7311775088 0.7518917918 0.7002809048 + 0.7318987250 0.7526022792 0.7010293007 + 0.7326188087 0.7533113956 0.7017766237 + 0.7333359718 0.7540174723 0.7025194764 + 0.7340533137 0.7547234893 0.7032623887 + 0.7347705960 0.7554296255 0.7040053010 + 0.7354866862 0.7561342120 0.7047476172 + 0.7361978889 0.7568343878 0.7054864168 + 0.7369092107 0.7575346828 0.7062252164 + 0.7376204133 0.7582349181 0.7069640160 + 0.7383310795 0.7589343190 0.7077025771 + 0.7390366197 0.7596284151 0.7084372044 + 0.7397419810 0.7603225112 0.7091717720 + 0.7404474020 0.7610166073 0.7099062800 + 0.7411528230 0.7617105842 0.7106409073 + 0.7418531179 0.7623990178 0.7113716006 + 0.7425531745 0.7630872726 0.7121018767 + 0.7432532907 0.7637757063 0.7128322124 + 0.7439532876 0.7644640207 0.7135624886 + 0.7446492910 0.7651482821 0.7142896056 + 0.7453442216 0.7658317089 0.7150154114 + 0.7460390925 0.7665150762 0.7157412767 + 0.7467340231 0.7671986222 0.7164672017 + 0.7474262714 0.7678791881 0.7171908021 + 0.7481164932 0.7685580850 0.7179121971 + 0.7488067150 0.7692371011 0.7186335921 + 0.7494968772 0.7699159980 0.7193549871 + 0.7501856089 0.7705929279 0.7200750709 + 0.7508713007 0.7712666988 0.7207918763 + 0.7515569925 0.7719404101 0.7215088010 + 0.7522428036 0.7726141810 0.7222256064 + 0.7529281974 0.7732872963 0.7229424119 + 0.7536085248 0.7739552259 0.7236546278 + 0.7542889118 0.7746230960 0.7243667245 + 0.7549691796 0.7752910256 0.7250788808 + 0.7556495070 0.7759588957 0.7257910967 + 0.7563261986 0.7766230106 0.7264999747 + 0.7570015788 0.7772859931 0.7272074223 + 0.7576768994 0.7779489756 0.7279146910 + 0.7583522797 0.7786120772 0.7286220789 + 0.7590258718 0.7792726755 0.7293279171 + 0.7596967220 0.7799301147 0.7300304770 + 0.7603675127 0.7805876136 0.7307329774 + 0.7610384226 0.7812449932 0.7314354777 + 0.7617092133 0.7819021940 0.7321380973 + 0.7623745203 0.7825548053 0.7328367829 + 0.7630398870 0.7832074165 0.7335352898 + 0.7637053132 0.7838600278 0.7342339158 + 0.7643706203 0.7845125198 0.7349324226 + 0.7650331855 0.7851625085 0.7356290221 + 0.7656937242 0.7858108282 0.7363240123 + 0.7663542032 0.7864592075 0.7370188832 + 0.7670148015 0.7871075869 0.7377138734 + 0.7676746845 0.7877553105 0.7384086251 + 0.7683306932 0.7884001732 0.7390993834 + 0.7689867020 0.7890449762 0.7397902012 + 0.7696425915 0.7896898985 0.7404810786 + 0.7702986002 0.7903347015 0.7411718965 + 0.7709521055 0.7909758091 0.7418606281 + 0.7716041803 0.7916151285 0.7425476909 + 0.7722561955 0.7922543287 0.7432348728 + 0.7729082108 0.7928935289 0.7439219952 + 0.7735598087 0.7935318947 0.7446089983 + 0.7742068172 0.7941648960 0.7452918887 + 0.7748538256 0.7947977781 0.7459747791 + 0.7755008936 0.7954307795 0.7466577291 + 0.7761479020 0.7960637808 0.7473406196 + 0.7767928243 0.7966936827 0.7480217814 + 0.7774356008 0.7973213196 0.7487009168 + 0.7780783772 0.7979490161 0.7493799925 + 0.7787212729 0.7985765934 0.7500591278 + 0.7793641090 0.7992041707 0.7507382035 + 0.7800034285 0.7998272181 0.7514144778 + 0.7806422710 0.8004500270 0.7520902753 + 0.7812811732 0.8010727763 0.7527660728 + 0.7819200754 0.8016955256 0.7534418702 + 0.7825579047 0.8023167253 0.7541171908 + 0.7831923962 0.8029342890 0.7547907233 + 0.7838268876 0.8035517931 0.7554641962 + 0.7844614983 0.8041694164 0.7561377287 + 0.7850959897 0.8047869205 0.7568112016 + 0.7857282162 0.8054007888 0.7574830055 + 0.7863587141 0.8060125113 0.7581532001 + 0.7869892716 0.8066241741 0.7588235140 + 0.7876198292 0.8072358966 0.7594937086 + 0.7882503867 0.8078476191 0.7601639032 + 0.7888780832 0.8084548712 0.7608323097 + 0.7895054221 0.8090618253 0.7615002990 + 0.7901325822 0.8096686006 0.7621682286 + 0.7907598019 0.8102754951 0.7628362179 + 0.7913867235 0.8108816743 0.7635040283 + 0.7920100093 0.8114830852 0.7641686797 + 0.7926331758 0.8120846152 0.7648333907 + 0.7932564020 0.8126860857 0.7654981017 + 0.7938796282 0.8132876158 0.7661628127 + 0.7945017815 0.8138875961 0.7668269873 + 0.7951213717 0.8144844770 0.7674893737 + 0.7957410812 0.8150814176 0.7681518197 + 0.7963607907 0.8156782985 0.7688142061 + 0.7969803810 0.8162751794 0.7694767118 + 0.7975987792 0.8168696761 0.7701377869 + 0.7982153893 0.8174614906 0.7707970738 + 0.7988319993 0.8180533051 0.7714563012 + 0.7994486094 0.8186451197 0.7721155286 + 0.8000652790 0.8192368746 0.7727746964 + 0.8006798029 0.8198254108 0.7734327912 + 0.8012925982 0.8204118013 0.7740896940 + 0.8019053936 0.8209980726 0.7747467160 + 0.8025181890 0.8215845227 0.7754036784 + 0.8031309843 0.8221707940 0.7760605812 + 0.8037403226 0.8227540851 0.7767156959 + 0.8043475151 0.8233358264 0.7773694992 + 0.8049547076 0.8239176273 0.7780233026 + 0.8055619001 0.8244994283 0.7786769867 + 0.8061690927 0.8250811100 0.7793307900 + 0.8067730069 0.8256602883 0.7799832225 + 0.8073750138 0.8262382746 0.7806347013 + 0.8079770803 0.8268163800 0.7812862992 + 0.8085792065 0.8273944855 0.7819377780 + 0.8091812730 0.8279724717 0.7825893164 + 0.8097804189 0.8285468221 0.7832390070 + 0.8103780150 0.8291193843 0.7838873267 + 0.8109756112 0.8296921253 0.7845357060 + 0.8115732074 0.8302646875 0.7851840854 + 0.8121708035 0.8308374286 0.7858325243 + 0.8127658963 0.8314061761 0.7864795923 + 0.8133594990 0.8319730759 0.7871257067 + 0.8139529824 0.8325399160 0.7877718806 + 0.8145465851 0.8331068158 0.7884181142 + 0.8151401281 0.8336737156 0.7890642285 + 0.8157312274 0.8342378736 0.7897089124 + 0.8163201213 0.8348001242 0.7903519273 + 0.8169090152 0.8353623152 0.7909948826 + 0.8174979091 0.8359245062 0.7916380167 + 0.8180868030 0.8364866972 0.7922809720 + 0.8186740875 0.8370468020 0.7929232717 + 0.8192589283 0.8376039267 0.7935640812 + 0.8198438287 0.8381611109 0.7942048907 + 0.8204286098 0.8387181759 0.7948457003 + 0.8210135102 0.8392754197 0.7954866290 + 0.8215975761 0.8398311734 0.7961269021 + 0.8221790195 0.8403828740 0.7967646122 + 0.8227605224 0.8409346938 0.7974023223 + 0.8233419061 0.8414865136 0.7980399728 + 0.8239232898 0.8420382142 0.7986776829 + 0.8245046735 0.8425897956 0.7993155122 + 0.8250821829 0.8431358933 0.7999498844 + 0.8256595731 0.8436819911 0.8005841970 + 0.8262370229 0.8442280889 0.8012183905 + 0.8268144131 0.8447741866 0.8018527031 + 0.8273919225 0.8453204036 0.8024870157 + 0.8279669285 0.8458629251 0.8031185865 + 0.8285411000 0.8464043140 0.8037490249 + 0.8291152716 0.8469458222 0.8043794036 + 0.8296893835 0.8474872112 0.8050099015 + 0.8302636147 0.8480286002 0.8056402802 + 0.8308361769 0.8485683799 0.8062694073 + 0.8314064741 0.8491060734 0.8068965077 + 0.8319767714 0.8496438265 0.8075236082 + 0.8325471282 0.8501815200 0.8081507087 + 0.8331174850 0.8507192135 0.8087778091 + 0.8336874843 0.8512564898 0.8094046712 + 0.8342546225 0.8517901897 0.8100280762 + 0.8348217010 0.8523237705 0.8106514215 + 0.8353887796 0.8528574705 0.8112748265 + 0.8359557986 0.8533911109 0.8118981123 + 0.8365228772 0.8539248109 0.8125215173 + 0.8370872736 0.8544545770 0.8131418824 + 0.8376504779 0.8549830914 0.8137606978 + 0.8382136822 0.8555114865 0.8143793941 + 0.8387768269 0.8560398817 0.8149982095 + 0.8393399715 0.8565682769 0.8156170249 + 0.8399021029 0.8570951819 0.8162348866 + 0.8404610157 0.8576182127 0.8168491721 + 0.8410199285 0.8581411242 0.8174635172 + 0.8415787816 0.8586640954 0.8180779219 + 0.8421378136 0.8591871262 0.8186922073 + 0.8426967263 0.8597099781 0.8193064928 + 0.8432520032 0.8602293730 0.8199177980 + 0.8438060880 0.8607478738 0.8205277920 + 0.8443602920 0.8612663150 0.8211377859 + 0.8449144959 0.8617848158 0.8217477798 + 0.8454685807 0.8623031974 0.8223577738 + 0.8460217714 0.8628203869 0.8229669929 + 0.8465716839 0.8633341193 0.8235728145 + 0.8471217155 0.8638477921 0.8241786957 + 0.8476716280 0.8643614054 0.8247845173 + 0.8482214808 0.8648750782 0.8253902793 + 0.8487715125 0.8653888106 0.8259961009 + 0.8493191004 0.8658990860 0.8265994787 + 0.8498654962 0.8664078712 0.8272011876 + 0.8504120111 0.8669167757 0.8278030157 + 0.8509584069 0.8674256206 0.8284047246 + 0.8515049219 0.8679344058 0.8290064931 + 0.8520511985 0.8684428930 0.8296083212 + 0.8525946736 0.8689479232 0.8302060962 + 0.8531380892 0.8694528937 0.8308039904 + 0.8536815047 0.8699579239 0.8314018250 + 0.8542249799 0.8704627752 0.8319995999 + 0.8547683954 0.8709678054 0.8325974941 + 0.8553109765 0.8714712262 0.8331940770 + 0.8558521867 0.8719722033 0.8337880969 + 0.8563933969 0.8724731803 0.8343821168 + 0.8569346070 0.8729742765 0.8349761963 + 0.8574758172 0.8734753132 0.8355702162 + 0.8580170274 0.8739762902 0.8361642957 + 0.8585556149 0.8744739294 0.8367559910 + 0.8590927720 0.8749697804 0.8373463750 + 0.8596299887 0.8754656911 0.8379368186 + 0.8601672053 0.8759617209 0.8385272026 + 0.8607044220 0.8764575720 0.8391175270 + 0.8612416983 0.8769536018 0.8397079110 + 0.8617749810 0.8774446845 0.8402953148 + 0.8623077869 0.8779354095 0.8408821225 + 0.8628407121 0.8784260750 0.8414688706 + 0.8633735776 0.8789168000 0.8420556784 + 0.8639063835 0.8794075251 0.8426426053 + 0.8644390106 0.8798975945 0.8432291746 + 0.8649681211 0.8803830147 0.8438127041 + 0.8654972911 0.8808683753 0.8443961143 + 0.8660264015 0.8813537955 0.8449795246 + 0.8665555716 0.8818392158 0.8455628753 + 0.8670848012 0.8823245764 0.8461462855 + 0.8676131964 0.8828086853 0.8467290998 + 0.8681393862 0.8832886815 0.8473091722 + 0.8686655164 0.8837687969 0.8478893042 + 0.8691915870 0.8842489123 0.8484694958 + 0.8697177172 0.8847289085 0.8490496278 + 0.8702437878 0.8852090240 0.8496297002 + 0.8707692027 0.8856875896 0.8502088785 + 0.8712928891 0.8861634731 0.8507859111 + 0.8718166947 0.8866394162 0.8513628244 + 0.8723403811 0.8871151805 0.8519396782 + 0.8728641272 0.8875911236 0.8525167108 + 0.8733878732 0.8880670071 0.8530936241 + 0.8739106059 0.8885408044 0.8536695838 + 0.8744313717 0.8890113831 0.8542435169 + 0.8749523163 0.8894819021 0.8548172712 + 0.8754730821 0.8899524808 0.8553912044 + 0.8759940267 0.8904231191 0.8559650779 + 0.8765147924 0.8908935785 0.8565390110 + 0.8770341873 0.8913617134 0.8571118712 + 0.8775507808 0.8918259144 0.8576828837 + 0.8780673742 0.8922899961 0.8582537770 + 0.8785840869 0.8927540779 0.8588247895 + 0.8791006804 0.8932182193 0.8593956828 + 0.8796172738 0.8936823010 0.8599665761 + 0.8801329136 0.8941447139 0.8605368137 + 0.8806458712 0.8946034908 0.8611049056 + 0.8811588883 0.8950623274 0.8616731167 + 0.8816719055 0.8955211043 0.8622412086 + 0.8821849227 0.8959800005 0.8628093004 + 0.8826978803 0.8964387774 0.8633773923 + 0.8832104206 0.8968967199 0.8639450073 + 0.8837205172 0.8973513246 0.8645104170 + 0.8842306733 0.8978059292 0.8650758266 + 0.8847408891 0.8982604742 0.8656411767 + 0.8852511048 0.8987150788 0.8662065864 + 0.8857613206 0.8991696835 0.8667719960 + 0.8862714171 0.8996242285 0.8673374057 + 0.8867782950 0.9000756741 0.8679000735 + 0.8872851729 0.9005271792 0.8684629202 + 0.8877921104 0.9009786844 0.8690257072 + 0.8882989883 0.9014301896 0.8695884943 + 0.8888059855 0.9018816948 0.8701512218 + 0.8893129230 0.9023333192 0.8707140088 + 0.8898167014 0.9027820826 0.8712748289 + 0.8903198838 0.9032304287 0.8718351126 + 0.8908231258 0.9036787152 0.8723952770 + 0.8913263083 0.9041271210 0.8729556203 + 0.8918294907 0.9045754075 0.8735159039 + 0.8923326731 0.9050236940 0.8740761280 + 0.8928335905 0.9054703116 0.8746351004 + 0.8933327794 0.9059156179 0.8751929998 + 0.8938320279 0.9063609838 0.8757507801 + 0.8943310976 0.9068062901 0.8763086796 + 0.8948302865 0.9072515965 0.8768665791 + 0.8953294754 0.9076969028 0.8774244785 + 0.8958278894 0.9081414938 0.8779817820 + 0.8963238001 0.9085838795 0.8785374165 + 0.8968197107 0.9090263247 0.8790928721 + 0.8973156810 0.9094687104 0.8796485066 + 0.8978115916 0.9099110961 0.8802040815 + 0.8983076215 0.9103534818 0.8807597160 + 0.8988034725 0.9107959270 0.8813151717 + 0.8992962837 0.9112346172 0.8818688989 + 0.8997886181 0.9116730094 0.8824223280 + 0.9002810121 0.9121112823 0.8829756975 + 0.9007734060 0.9125496745 0.8835291266 + 0.9012656808 0.9129880071 0.8840824962 + 0.9017580748 0.9134262800 0.8846358061 + 0.9022489786 0.9138627052 0.8851882815 + 0.9027383924 0.9142972231 0.8857396245 + 0.9032276869 0.9147316813 0.8862909079 + 0.9037171006 0.9151660800 0.8868421912 + 0.9042065144 0.9156005979 0.8873934746 + 0.9046958089 0.9160349965 0.8879448175 + 0.9051852226 0.9164693952 0.8884961009 + 0.9056711793 0.9169011116 0.8890454173 + 0.9061573148 0.9173327088 0.8895946741 + 0.9066432714 0.9177644253 0.8901439905 + 0.9071292877 0.9181960821 0.8906934261 + 0.9076154232 0.9186277986 0.8912426829 + 0.9081013799 0.9190593958 0.8917919993 + 0.9085853100 0.9194893241 0.8923398852 + 0.9090666771 0.9199172854 0.8928862214 + 0.9095481038 0.9203454256 0.8934323788 + 0.9100295901 0.9207733870 0.8939787149 + 0.9105110168 0.9212014079 0.8945249915 + 0.9109923840 0.9216294885 0.8950712085 + 0.9114738703 0.9220575094 0.8956174850 + 0.9119517803 0.9224802852 0.8961616158 + 0.9124293923 0.9229027033 0.8967053890 + 0.9129070044 0.9233251214 0.8972491026 + 0.9133846164 0.9237474799 0.8977928758 + 0.9138621092 0.9241697788 0.8983367085 + 0.9143397212 0.9245921969 0.8988804817 + 0.9148163795 0.9250133038 0.8994235992 + 0.9152907729 0.9254313111 0.8999646902 + 0.9157652259 0.9258493185 0.9005057216 + 0.9162396193 0.9262673259 0.9010468125 + 0.9167140126 0.9266853929 0.9015879035 + 0.9171885252 0.9271034002 0.9021288753 + 0.9176629186 0.9275214076 0.9026700258 + 0.9181352854 0.9279373884 0.9032099247 + 0.9186062217 0.9283521175 0.9037488103 + 0.9190772176 0.9287667274 0.9042876959 + 0.9195482135 0.9291813970 0.9048265815 + 0.9200190902 0.9295961261 0.9053655267 + 0.9204900861 0.9300107956 0.9059044123 + 0.9209610820 0.9304254055 0.9064432979 + 0.9214289188 0.9308364987 0.9069793224 + 0.9218962193 0.9312469959 0.9075146914 + 0.9223635197 0.9316574931 0.9080500007 + 0.9228308201 0.9320679903 0.9085853100 + 0.9232981205 0.9324784875 0.9091206789 + 0.9237653017 0.9328889847 0.9096559882 + 0.9242324829 0.9332993031 0.9101912975 + 0.9246969223 0.9337061048 0.9107241035 + 0.9251613021 0.9341129065 0.9112569094 + 0.9256256819 0.9345198274 0.9117897153 + 0.9260901213 0.9349266291 0.9123224020 + 0.9265545011 0.9353333712 0.9128552079 + 0.9270188808 0.9357401729 0.9133880138 + 0.9274827838 0.9361463189 0.9139202237 + 0.9279441237 0.9365494251 0.9144498706 + 0.9284055233 0.9369524121 0.9149795175 + 0.9288668036 0.9373555183 0.9155091047 + 0.9293280840 0.9377586246 0.9160386920 + 0.9297894835 0.9381616116 0.9165682793 + 0.9302508235 0.9385647178 0.9170979261 + 0.9307109714 0.9389663935 0.9176266789 + 0.9311680794 0.9393649101 0.9181531072 + 0.9316251278 0.9397633076 0.9186794758 + 0.9320821762 0.9401618242 0.9192059040 + 0.9325392842 0.9405602813 0.9197322726 + 0.9329962730 0.9409586787 0.9202587008 + 0.9334533811 0.9413571954 0.9207851291 + 0.9339085817 0.9417545795 0.9213101268 + 0.9343593717 0.9421498775 0.9218313098 + 0.9348102212 0.9425451756 0.9223526120 + 0.9352610111 0.9429404736 0.9228739142 + 0.9357118011 0.9433357716 0.9233952165 + 0.9361625910 0.9437311292 0.9239163995 + 0.9366133809 0.9441264272 0.9244377017 + 0.9370620251 0.9445207715 0.9249573946 + 0.9375048280 0.9449130297 0.9254726171 + 0.9379475713 0.9453052282 0.9259877801 + 0.9383903742 0.9456974268 0.9265030026 + 0.9388331771 0.9460896254 0.9270182252 + 0.9392759800 0.9464818239 0.9275333881 + 0.9397187829 0.9468740225 0.9280486107 + 0.9401605129 0.9472647905 0.9285627007 + 0.9405969977 0.9476501942 0.9290714860 + 0.9410336018 0.9480355978 0.9295802116 + 0.9414702058 0.9484211206 0.9300889969 + 0.9419068098 0.9488065243 0.9305977225 + 0.9423432946 0.9491919279 0.9311063886 + 0.9427798986 0.9495772719 0.9316151738 + 0.9432163239 0.9499623179 0.9321237803 + 0.9436482191 0.9503390789 0.9326291084 + 0.9440801740 0.9507158995 0.9331343770 + 0.9445121288 0.9510927200 0.9336397052 + 0.9449440837 0.9514694810 0.9341449142 + 0.9453759789 0.9518463016 0.9346501827 + 0.9458078742 0.9522231221 0.9351555109 + 0.9462398887 0.9526000023 0.9356607795 + 0.9466683865 0.9529722929 0.9361640811 + 0.9470962882 0.9533438087 0.9366669059 + 0.9475240707 0.9537153840 0.9371697903 + 0.9479519725 0.9540868998 0.9376726151 + 0.9483798146 0.9544584155 0.9381753802 + 0.9488077164 0.9548299909 0.9386783242 + 0.9492354989 0.9552015066 0.9391810894 + 0.9496610165 0.9555711746 0.9396826029 + 0.9500846863 0.9559394717 0.9401828051 + 0.9505084157 0.9563078284 0.9406831264 + 0.9509320855 0.9566761255 0.9411833882 + 0.9513558149 0.9570444226 0.9416837096 + 0.9517794847 0.9574127197 0.9421839714 + 0.9522032142 0.9577810168 0.9426841736 + 0.9526256919 0.9581483006 0.9431836009 + 0.9530442953 0.9585124254 0.9436798096 + 0.9534628987 0.9588766098 0.9441760778 + 0.9538815022 0.9592407942 0.9446722865 + 0.9543001056 0.9596049190 0.9451684952 + 0.9547187090 0.9599691033 0.9456648231 + 0.9551373124 0.9603332281 0.9461609721 + 0.9555559158 0.9606974125 0.9466571808 + 0.9559701085 0.9610580802 0.9471496940 + 0.9563835859 0.9614182115 0.9476413727 + 0.9567971230 0.9617784023 0.9481331706 + 0.9572104812 0.9621384740 0.9486249089 + 0.9576240182 0.9624987245 0.9491167068 + 0.9580374956 0.9628589153 0.9496085048 + 0.9584509730 0.9632189870 0.9501001835 + 0.9588623047 0.9635775089 0.9505898952 + 0.9592704177 0.9639338851 0.9510766864 + 0.9596785903 0.9642902017 0.9515634179 + 0.9600868225 0.9646465182 0.9520500898 + 0.9604949951 0.9650027752 0.9525368214 + 0.9609032273 0.9653592110 0.9530236125 + 0.9613113999 0.9657155275 0.9535102844 + 0.9617195725 0.9660717845 0.9539970160 + 0.9621226192 0.9664239883 0.9544786215 + 0.9625251889 0.9667757750 0.9549596906 + 0.9629278183 0.9671275020 0.9554407001 + 0.9633303285 0.9674792886 0.9559217095 + 0.9637328982 0.9678310752 0.9564027190 + 0.9641355276 0.9681829214 0.9568837285 + 0.9645379782 0.9685347080 0.9573647976 + 0.9649384022 0.9688847065 0.9578434825 + 0.9653344154 0.9692314267 0.9583176970 + 0.9657304883 0.9695780873 0.9587917924 + 0.9661265016 0.9699246883 0.9592658877 + 0.9665225148 0.9702714086 0.9597399831 + 0.9669185281 0.9706181288 0.9602141976 + 0.9673146009 0.9709647894 0.9606882930 + 0.9677106142 0.9713115096 0.9611623883 + 0.9681016803 0.9716545939 0.9616312981 + 0.9684907198 0.9719963074 0.9620978236 + 0.9688798189 0.9723380208 0.9625642896 + 0.9692687988 0.9726796746 0.9630308151 + 0.9696578979 0.9730213881 0.9634972811 + 0.9700468779 0.9733629823 0.9639638066 + 0.9704359174 0.9737046957 0.9644302726 + 0.9708248973 0.9740462899 0.9648967981 + 0.9712070227 0.9743828177 0.9653550982 + 0.9715890288 0.9747192860 0.9658132792 + 0.9719710946 0.9750556946 0.9662715793 + 0.9723531008 0.9753922224 0.9667298794 + 0.9727351069 0.9757285714 0.9671881199 + 0.9731171727 0.9760650992 0.9676464200 + 0.9734991789 0.9764015079 0.9681046009 + 0.9738795161 0.9767364264 0.9685608149 + 0.9742544293 0.9770668149 0.9690101147 + 0.9746292830 0.9773972034 0.9694594145 + 0.9750041962 0.9777275920 0.9699087143 + 0.9753791094 0.9780579805 0.9703580141 + 0.9757540226 0.9783884883 0.9708073139 + 0.9761288762 0.9787188768 0.9712566137 + 0.9765037894 0.9790493250 0.9717059135 + 0.9768754840 0.9793767929 0.9721511006 + 0.9772428274 0.9797003865 0.9725903869 + 0.9776101112 0.9800240993 0.9730296731 + 0.9779773951 0.9803476930 0.9734690785 + 0.9783446789 0.9806714058 0.9739084244 + 0.9787119031 0.9809949994 0.9743477702 + 0.9790791869 0.9813187122 0.9747871161 + 0.9794464707 0.9816424251 0.9752265215 + 0.9798092246 0.9819620252 0.9756602049 + 0.9801679254 0.9822781086 0.9760888815 + 0.9805266261 0.9825941920 0.9765176177 + 0.9808852077 0.9829102755 0.9769461751 + 0.9812439084 0.9832264185 0.9773749113 + 0.9816026092 0.9835425019 0.9778035879 + 0.9819613099 0.9838585854 0.9782322049 + 0.9823198915 0.9841747284 0.9786608815 + 0.9826725125 0.9844858050 0.9790827036 + 0.9830206037 0.9847931862 0.9794991016 + 0.9833686948 0.9851006866 0.9799156189 + 0.9837167263 0.9854081273 0.9803320765 + 0.9840648174 0.9857155085 0.9807485938 + 0.9844129086 0.9860228896 0.9811649919 + 0.9847608805 0.9863303900 0.9815815091 + 0.9851089716 0.9866377711 0.9819980264 + 0.9854506254 0.9869400859 0.9824073911 + 0.9857869148 0.9872382283 0.9828109741 + 0.9861232042 0.9875363708 0.9832146168 + 0.9864594936 0.9878345132 0.9836181998 + 0.9867957830 0.9881327152 0.9840217233 + 0.9871320724 0.9884307981 0.9844253063 + 0.9874684215 0.9887290001 0.9848288894 + 0.9878047109 0.9890270829 0.9852324724 + 0.9881356955 0.9893202186 0.9856290817 + 0.9884604216 0.9896075130 0.9860175252 + 0.9887850881 0.9898949265 0.9864059091 + 0.9891096950 0.9901822209 0.9867942929 + 0.9894344211 0.9904695153 0.9871826768 + 0.9897590876 0.9907568097 0.9875711203 + 0.9900838137 0.9910441041 0.9879595041 + 0.9904084802 0.9913313985 0.9883478880 + 0.9907290936 0.9916148782 0.9887315035 + 0.9910402894 0.9918898940 0.9891040921 + 0.9913514853 0.9921647906 0.9894766212 + 0.9916628003 0.9924398065 0.9898492098 + 0.9919739962 0.9927147031 0.9902216792 + 0.9922851920 0.9929897189 0.9905943274 + 0.9925965071 0.9932646751 0.9909667969 + 0.9929077029 0.9935395718 0.9913393855 + 0.9932175875 0.9938135147 0.9917104244 + 0.9935122728 0.9940760732 0.9920637012 + 0.9938070774 0.9943386912 0.9924169779 + 0.9941018224 0.9946013093 0.9927703142 + 0.9943966269 0.9948639274 0.9931235909 + 0.9946913123 0.9951264858 0.9934769273 + 0.9949861169 0.9953891039 0.9938303232 + 0.9952808022 0.9956517220 0.9941835999 + 0.9955756068 0.9959142804 0.9945368767 + 0.9958524704 0.9961653948 0.9948744774 + 0.9961246848 0.9964134097 0.9952080250 + 0.9963967800 0.9966614842 0.9955413938 + 0.9966689944 0.9969094992 0.9958748817 + 0.9969412088 0.9971575141 0.9962083101 + 0.9972134233 0.9974055886 0.9965417981 + 0.9974855781 0.9976536036 0.9968752265 + 0.9977576733 0.9979016185 0.9972087145 + 0.9980192780 0.9981427789 0.9975314736 + 0.9982668757 0.9983748794 0.9978399873 + 0.9985144734 0.9986070991 0.9981486201 + 0.9987620711 0.9988391995 0.9984571934 + 0.9990097284 0.9990714192 0.9987657070 + 0.9992572069 0.9993035197 0.9990742803 + 0.9995048046 0.9995356798 0.9993829131 + 0.9997524023 0.9997677803 0.9996914268 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_optima_ii_200.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_optima_ii_200.spi1d index 11a33289..1e94928a 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_optima_ii_200.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_optima_ii_200.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0111121098 0.0121509498 0.0121509498 - 0.0217311792 0.0234846398 0.0234846398 - 0.0310309306 0.0335913785 0.0335913785 - 0.0394427888 0.0427072011 0.0427072011 - 0.0472172312 0.0510250889 0.0510250889 - 0.0544895791 0.0587030984 0.0587030984 - 0.0613539182 0.0658273399 0.0658273399 - 0.0678610802 0.0724957809 0.0724957809 - 0.0740588829 0.0787753463 0.0787753463 - 0.0799922720 0.0847034305 0.0847034305 - 0.0856926665 0.0903405622 0.0903405622 - 0.0911944881 0.0957725719 0.0957725719 - 0.0965085030 0.1010297015 0.1010297015 - 0.1016504988 0.1061294973 0.1061294973 - 0.1066408977 0.1110759974 0.1110759974 - 0.1114827022 0.1158965006 0.1158965006 - 0.1161810011 0.1205943003 0.1205943003 - 0.1207583994 0.1251748055 0.1251748055 - 0.1252014935 0.1296516955 0.1296516955 - 0.1295239031 0.1340274960 0.1340274960 - 0.1337441057 0.1383039951 0.1383039951 - 0.1378667057 0.1424947977 0.1424947977 - 0.1418835968 0.1466037929 0.1466037929 - 0.1458135992 0.1506410986 0.1506410986 - 0.1496558040 0.1545946002 0.1545946002 - 0.1534211040 0.1584779024 0.1584779024 - 0.1571090072 0.1622924954 0.1622924954 - 0.1607228965 0.1660465002 0.1660465002 - 0.1642647982 0.1697326005 0.1697326005 - 0.1677455008 0.1733641028 0.1733641028 - 0.1711609066 0.1769354939 0.1769354939 - 0.1745131016 0.1804486960 0.1804486960 - 0.1778111011 0.1839037985 0.1839037985 - 0.1810566932 0.1873078942 0.1873078942 - 0.1842480004 0.1906577051 0.1906577051 - 0.1873922944 0.1939612031 0.1939612031 - 0.1904910952 0.1972206980 0.1972206980 - 0.1935475022 0.2004390955 0.2004390955 - 0.1965657026 0.2036167979 0.2036167979 - 0.1995460987 0.2067523003 0.2067523003 - 0.2024825066 0.2098508030 0.2098508030 - 0.2053820044 0.2129165977 0.2129165977 - 0.2082439959 0.2159480006 0.2159480006 - 0.2110687941 0.2189517021 0.2189517021 - 0.2138601989 0.2219190001 0.2219190001 - 0.2166164964 0.2248522043 0.2248522043 - 0.2193437964 0.2277560979 0.2277560979 - 0.2220394015 0.2306237966 0.2306237966 - 0.2247070074 0.2334605008 0.2334605008 - 0.2273449004 0.2362726033 0.2362726033 - 0.2299581021 0.2390560061 0.2390560061 - 0.2325424999 0.2418099940 0.2418099940 - 0.2350990027 0.2445425987 0.2445425987 - 0.2376336008 0.2472528070 0.2472528070 - 0.2401454002 0.2499340028 0.2499340028 - 0.2426387966 0.2525874972 0.2525874972 - 0.2451128960 0.2552196980 0.2552196980 - 0.2475619018 0.2578315139 0.2578315139 - 0.2499863058 0.2604233027 0.2604233027 - 0.2523908913 0.2629944980 0.2629944980 - 0.2547743022 0.2655487061 0.2655487061 - 0.2571432889 0.2680800855 0.2680800855 - 0.2594946027 0.2705903947 0.2705903947 - 0.2618246973 0.2730821967 0.2730821967 - 0.2641352117 0.2755582929 0.2755582929 - 0.2664259076 0.2780191004 0.2780191004 - 0.2686997056 0.2804580927 0.2804580927 - 0.2709574103 0.2828805149 0.2828805149 - 0.2731972933 0.2852869034 0.2852869034 - 0.2754215896 0.2876777947 0.2876777947 - 0.2776325047 0.2900503874 0.2900503874 - 0.2798275948 0.2924054861 0.2924054861 - 0.2820053995 0.2947433889 0.2947433889 - 0.2841629088 0.2970634997 0.2970634997 - 0.2863053083 0.2993713021 0.2993713021 - 0.2884348929 0.3016661108 0.3016661108 - 0.2905501127 0.3039461970 0.3039461970 - 0.2926583886 0.3062120080 0.3062120080 - 0.2947568893 0.3084635139 0.3084635139 - 0.2968437076 0.3106997013 0.3106997013 - 0.2989166081 0.3129228055 0.3129228055 - 0.3009737134 0.3151350021 0.3151350021 - 0.3030146956 0.3173327148 0.3173327148 - 0.3050420880 0.3195173144 0.3195173144 - 0.3070568144 0.3216913044 0.3216913044 - 0.3090623915 0.3238545060 0.3238545060 - 0.3110578060 0.3260023892 0.3260023892 - 0.3130468130 0.3281404078 0.3281404078 - 0.3150278926 0.3302693069 0.3302693069 - 0.3169982135 0.3323845863 0.3323845863 - 0.3189576864 0.3344885111 0.3344885111 - 0.3209047019 0.3365832865 0.3365832865 - 0.3228406012 0.3386682868 0.3386682868 - 0.3247657120 0.3407419920 0.3407419920 - 0.3266789913 0.3428055942 0.3428055942 - 0.3285810053 0.3448581100 0.3448581100 - 0.3304736018 0.3468984962 0.3468984962 - 0.3323583901 0.3489289880 0.3489289880 - 0.3342367113 0.3509478867 0.3509478867 - 0.3361082971 0.3529567122 0.3529567122 - 0.3379718065 0.3549566865 0.3549566865 - 0.3398267031 0.3569482863 0.3569482863 - 0.3416717947 0.3589293957 0.3589293957 - 0.3435060084 0.3609009981 0.3609009981 - 0.3453314900 0.3628633916 0.3628633916 - 0.3471463919 0.3648136854 0.3648136854 - 0.3489513993 0.3667523861 0.3667523861 - 0.3507472873 0.3686820865 0.3686820865 - 0.3525382876 0.3706041873 0.3706041873 - 0.3543233871 0.3725175858 0.3725175858 - 0.3561016917 0.3744210899 0.3744210899 - 0.3578726053 0.3763135970 0.3763135970 - 0.3596351147 0.3781942129 0.3781942129 - 0.3613910973 0.3800665140 0.3800665140 - 0.3631403148 0.3819315135 0.3819315135 - 0.3648819029 0.3837873042 0.3837873042 - 0.3666147888 0.3856337070 0.3856337070 - 0.3683386147 0.3874723017 0.3874723017 - 0.3700559139 0.3893026114 0.3893026114 - 0.3717651963 0.3911237121 0.3911237121 - 0.3734667003 0.3929347992 0.3929347992 - 0.3751592040 0.3947354853 0.3947354853 - 0.3768459857 0.3965282142 0.3965282142 - 0.3785249889 0.3983115852 0.3983115852 - 0.3801974952 0.4000878930 0.4000878930 - 0.3818610907 0.4018566012 0.4018566012 - 0.3835192919 0.4036169052 0.4036169052 - 0.3851707876 0.4053680897 0.4053680897 - 0.3868159950 0.4071125984 0.4071125984 - 0.3884536028 0.4088492990 0.4088492990 - 0.3900853097 0.4105800986 0.4105800986 - 0.3917112947 0.4123049974 0.4123049974 - 0.3933308125 0.4140234888 0.4140234888 - 0.3949447870 0.4157348871 0.4157348871 - 0.3965539038 0.4174388051 0.4174388051 - 0.3981573880 0.4191350937 0.4191350937 - 0.3997552991 0.4208256006 0.4208256006 - 0.4013460875 0.4225083888 0.4225083888 - 0.4029329121 0.4241839945 0.4241839945 - 0.4045143127 0.4258517921 0.4258517921 - 0.4060910046 0.4275138080 0.4275138080 - 0.4076623023 0.4291698933 0.4291698933 - 0.4092277884 0.4308196008 0.4308196008 - 0.4107879102 0.4324634969 0.4324634969 - 0.4123404026 0.4340988100 0.4340988100 - 0.4138863087 0.4357272089 0.4357272089 - 0.4154250026 0.4373478889 0.4373478889 - 0.4169568121 0.4389612079 0.4389612079 - 0.4184818864 0.4405669868 0.4405669868 - 0.4199990034 0.4421640933 0.4421640933 - 0.4215070009 0.4437515140 0.4437515140 - 0.4230087101 0.4453321993 0.4453321993 - 0.4245057106 0.4469063878 0.4469063878 - 0.4259983897 0.4484742880 0.4484742880 - 0.4274863899 0.4500342011 0.4500342011 - 0.4289681017 0.4515857995 0.4515857995 - 0.4304446876 0.4531300068 0.4531300068 - 0.4319162071 0.4546670914 0.4546670914 - 0.4333826005 0.4561972916 0.4561972916 - 0.4348441064 0.4577195942 0.4577195942 - 0.4363026917 0.4592362046 0.4592362046 - 0.4377590120 0.4607473016 0.4607473016 - 0.4392131865 0.4622528851 0.4622528851 - 0.4406625032 0.4637500048 0.4637500048 - 0.4421091080 0.4652411938 0.4652411938 - 0.4435526133 0.4667268991 0.4667268991 - 0.4449920058 0.4682061076 0.4682061076 - 0.4464268088 0.4696769118 0.4696769118 - 0.4478574991 0.4711419940 0.4711419940 - 0.4492841959 0.4726015031 0.4726015031 - 0.4507060051 0.4740524888 0.4740524888 - 0.4521220922 0.4754965901 0.4754965901 - 0.4535340965 0.4769350886 0.4769350886 - 0.4549413919 0.4783662856 0.4783662856 - 0.4563410878 0.4797913134 0.4797913134 - 0.4577364922 0.4812119901 0.4812119901 - 0.4591276050 0.4826276898 0.4826276898 - 0.4605121911 0.4840362966 0.4840362966 - 0.4618934989 0.4854404926 0.4854404926 - 0.4632714987 0.4868401885 0.4868401885 - 0.4646422863 0.4882321954 0.4882321954 - 0.4660094976 0.4896200001 0.4896200001 - 0.4673734009 0.4910030961 0.4910030961 - 0.4687319994 0.4923816025 0.4923816025 - 0.4700880945 0.4937570095 0.4937570095 - 0.4714413881 0.4951288104 0.4951288104 - 0.4727892876 0.4964961112 0.4964961112 - 0.4741345942 0.4978606999 0.4978606999 - 0.4754764140 0.4992212057 0.4992212057 - 0.4768131077 0.5005778074 0.5005778074 - 0.4781472087 0.5019320250 0.5019320250 - 0.4794766903 0.5032812953 0.5032812953 - 0.4808017015 0.5046275854 0.5046275854 - 0.4821242094 0.5059717894 0.5059717894 - 0.4834404886 0.5073099732 0.5073099732 - 0.4847536087 0.5086460710 0.5086460710 - 0.4860630035 0.5099794865 0.5099794865 - 0.4873664975 0.5113078952 0.5113078952 - 0.4886673987 0.5126342773 0.5126342773 - 0.4899624884 0.5139567852 0.5139567852 - 0.4912536144 0.5152760744 0.5152760744 - 0.4925413132 0.5165923238 0.5165923238 - 0.4938223064 0.5179045796 0.5179045796 - 0.4951008856 0.5192151070 0.5192151070 - 0.4963732958 0.5205205083 0.5205205083 - 0.4976417124 0.5218238235 0.5218238235 - 0.4989061058 0.5231245160 0.5231245160 - 0.5001640916 0.5244206190 0.5244206190 - 0.5014199018 0.5257151127 0.5257151127 - 0.5026680231 0.5270051956 0.5270051956 - 0.5039132833 0.5282933712 0.5282933712 - 0.5051530004 0.5295773745 0.5295773745 - 0.5063877106 0.5308588743 0.5308588743 - 0.5076187253 0.5321382880 0.5321382880 - 0.5088446736 0.5334129930 0.5334129930 - 0.5100693107 0.5346865058 0.5346865058 - 0.5112869740 0.5359553099 0.5359553099 - 0.5125030875 0.5372229815 0.5372229815 - 0.5137143135 0.5384858847 0.5384858847 - 0.5149223804 0.5397471189 0.5397471189 - 0.5161263943 0.5410054922 0.5410054922 - 0.5173262954 0.5422605872 0.5422605872 - 0.5185235143 0.5435131192 0.5435131192 - 0.5197150707 0.5447623730 0.5447623730 - 0.5209047794 0.5460104942 0.5460104942 - 0.5220907927 0.5472519994 0.5472519994 - 0.5232760906 0.5484923720 0.5484923720 - 0.5244560838 0.5497261286 0.5497261286 - 0.5256354809 0.5509589911 0.5509589911 - 0.5268111825 0.5521854162 0.5521854162 - 0.5279856920 0.5534102917 0.5534102917 - 0.5291559100 0.5546302199 0.5546302199 - 0.5303248167 0.5558478832 0.5558478832 - 0.5314909220 0.5570605993 0.5570605993 - 0.5326548815 0.5582709908 0.5582709908 - 0.5338155031 0.5594773889 0.5594773889 - 0.5349742770 0.5606803298 0.5606803298 - 0.5361307859 0.5618779063 0.5618779063 - 0.5372846723 0.5630717278 0.5630717278 - 0.5384358168 0.5642607808 0.5642607808 - 0.5395848155 0.5654460788 0.5654460788 - 0.5407317877 0.5666270256 0.5666270256 - 0.5418760777 0.5678040981 0.5678040981 - 0.5430176854 0.5689768791 0.5689768791 - 0.5441573858 0.5701463819 0.5701463819 - 0.5452948809 0.5713114142 0.5713114142 - 0.5464299917 0.5724735260 0.5724735260 - 0.5475621819 0.5736311078 0.5736311078 - 0.5486928225 0.5747861266 0.5747861266 - 0.5498210192 0.5759364963 0.5759364963 - 0.5509474277 0.5770848989 0.5770848989 - 0.5520703197 0.5782284141 0.5782284141 - 0.5531921983 0.5793704987 0.5793704987 - 0.5543109775 0.5805072188 0.5805072188 - 0.5554292202 0.5816432238 0.5816432238 - 0.5565428138 0.5827717781 0.5827717781 - 0.5576562881 0.5839003921 0.5839003921 - 0.5587658882 0.5850207806 0.5850207806 - 0.5598756075 0.5861409903 0.5861409903 - 0.5609806180 0.5872548819 0.5872548819 - 0.5620849729 0.5883675814 0.5883675814 - 0.5631865859 0.5894749165 0.5894749165 - 0.5642871261 0.5905798078 0.5905798078 - 0.5653848052 0.5916802287 0.5916802287 - 0.5664801002 0.5927764773 0.5927764773 - 0.5675740242 0.5938696861 0.5938696861 - 0.5686653852 0.5949565172 0.5949565172 - 0.5697557926 0.5960423946 0.5960423946 - 0.5708405972 0.5971215963 0.5971215963 - 0.5719254017 0.5982007980 0.5982007980 - 0.5730047226 0.5992723107 0.5992723107 - 0.5740836263 0.6003429890 0.6003429890 - 0.5751580000 0.6014091969 0.6014091969 - 0.5762305260 0.6024730802 0.6024730802 - 0.5773004889 0.6035333276 0.6035333276 - 0.5783671737 0.6045879722 0.6045879722 - 0.5794330835 0.6056417823 0.6056417823 - 0.5804936290 0.6066883802 0.6066883802 - 0.5815541148 0.6077349782 0.6077349782 - 0.5826098919 0.6087749004 0.6087749004 - 0.5836648941 0.6098132730 0.6098132730 - 0.5847159028 0.6108481884 0.6108481884 - 0.5857632160 0.6118795872 0.6118795872 - 0.5868092775 0.6129097939 0.6129097939 - 0.5878490806 0.6139339209 0.6139339209 - 0.5888888836 0.6149579883 0.6149579883 - 0.5899229050 0.6159774065 0.6159774065 - 0.5909554958 0.6169953942 0.6169953942 - 0.5919851065 0.6180102229 0.6180102229 - 0.5930107236 0.6190204024 0.6190204024 - 0.5940359831 0.6200305223 0.6200305223 - 0.5950549245 0.6210334897 0.6210334897 - 0.5960736871 0.6220365167 0.6220365167 - 0.5970885158 0.6230354905 0.6230354905 - 0.5981007814 0.6240319014 0.6240319014 - 0.5991119742 0.6250271201 0.6250271201 - 0.6001179814 0.6260160208 0.6260160208 - 0.6011241078 0.6270048022 0.6270048022 - 0.6021257043 0.6279885173 0.6279885173 - 0.6031255722 0.6289699078 0.6289699078 - 0.6041241288 0.6299499273 0.6299499273 - 0.6051182151 0.6309251785 0.6309251785 - 0.6061123013 0.6319004297 0.6319004297 - 0.6071022153 0.6328712106 0.6328712106 - 0.6080906987 0.6338403821 0.6338403821 - 0.6090779901 0.6348081827 0.6348081827 - 0.6100609899 0.6357709169 0.6357709169 - 0.6110439897 0.6367335916 0.6367335916 - 0.6120234728 0.6376916766 0.6376916766 - 0.6130014062 0.6386474967 0.6386474967 - 0.6139786243 0.6396028996 0.6396028996 - 0.6149513721 0.6405537724 0.6405537724 - 0.6159241796 0.6415047050 0.6415047050 - 0.6168935895 0.6424521804 0.6424521804 - 0.6178600788 0.6433963180 0.6433963180 - 0.6188266873 0.6443403959 0.6443403959 - 0.6197869778 0.6452776790 0.6452776790 - 0.6207470298 0.6462143064 0.6462143064 - 0.6217051744 0.6471490860 0.6471490860 - 0.6226593852 0.6480789185 0.6480789185 - 0.6236134768 0.6490086913 0.6490086913 - 0.6245638132 0.6499348283 0.6499348283 - 0.6255118847 0.6508585811 0.6508585811 - 0.6264600754 0.6517825127 0.6517825127 - 0.6274017096 0.6527013183 0.6527013183 - 0.6283431053 0.6536198854 0.6536198854 - 0.6292824149 0.6545372009 0.6545372009 - 0.6302164793 0.6554512978 0.6554512978 - 0.6311504841 0.6563653946 0.6563653946 - 0.6320812106 0.6572765708 0.6572765708 - 0.6330093145 0.6581853032 0.6581853032 - 0.6339374781 0.6590940952 0.6590940952 - 0.6348599792 0.6599957943 0.6599957943 - 0.6357815266 0.6608958244 0.6608958244 - 0.6367025971 0.6617954969 0.6617954969 - 0.6376188993 0.6626883149 0.6626883149 - 0.6385352015 0.6635810733 0.6635810733 - 0.6394497752 0.6644719243 0.6644719243 - 0.6403601766 0.6653575897 0.6653575897 - 0.6412705779 0.6662433147 0.6662433147 - 0.6421788931 0.6671264172 0.6671264172 - 0.6430848837 0.6680064797 0.6680064797 - 0.6439908147 0.6688866019 0.6688866019 - 0.6448925138 0.6697621942 0.6697621942 - 0.6457918882 0.6706354022 0.6706354022 - 0.6466913819 0.6715084910 0.6715084910 - 0.6475862861 0.6723757982 0.6723757982 - 0.6484801769 0.6732414961 0.6732414961 - 0.6493741274 0.6741071939 0.6741071939 - 0.6502627134 0.6749665737 0.6749665737 - 0.6511508822 0.6758252978 0.6758252978 - 0.6520388722 0.6766840219 0.6766840219 - 0.6529223919 0.6775373220 0.6775373220 - 0.6538059115 0.6783906221 0.6783906221 - 0.6546888947 0.6792433858 0.6792433858 - 0.6555675864 0.6800916791 0.6800916791 - 0.6564462185 0.6809399128 0.6809399128 - 0.6573243141 0.6817873716 0.6817873716 - 0.6581990719 0.6826301217 0.6826301217 - 0.6590738893 0.6834728122 0.6834728122 - 0.6599478126 0.6843147278 0.6843147278 - 0.6608172059 0.6851527095 0.6851527095 - 0.6616867185 0.6859906912 0.6859906912 - 0.6625552177 0.6868278980 0.6868278980 - 0.6634197831 0.6876606941 0.6876606941 - 0.6642844081 0.6884933710 0.6884933710 - 0.6651483178 0.6893252730 0.6893252730 - 0.6660085917 0.6901518106 0.6901518106 - 0.6668688059 0.6909784079 0.6909784079 - 0.6677287221 0.6918041706 0.6918041706 - 0.6685851216 0.6926234961 0.6926234961 - 0.6694415808 0.6934428215 0.6934428215 - 0.6702979207 0.6942619085 0.6942619085 - 0.6711509824 0.6950753927 0.6950753927 - 0.6720041037 0.6958888769 0.6958888769 - 0.6728572845 0.6967023015 0.6967023015 - 0.6737046838 0.6975108981 0.6975108981 - 0.6745517850 0.6983190179 0.6983190179 - 0.6753988862 0.6991270781 0.6991270781 - 0.6762413979 0.6999309063 0.6999309063 - 0.6770828962 0.7007337213 0.7007337213 - 0.6779245138 0.7015364766 0.7015364766 - 0.6787626743 0.7023348808 0.7023348808 - 0.6795991063 0.7031309009 0.7031309009 - 0.6804355979 0.7039269209 0.7039269209 - 0.6812698841 0.7047196031 0.7047196031 - 0.6821017861 0.7055087090 0.7055087090 - 0.6829338074 0.7062976956 0.7062976956 - 0.6837641001 0.7070850730 0.7070850730 - 0.6845905781 0.7078680992 0.7078680992 - 0.6854171157 0.7086511850 0.7086511850 - 0.6862432957 0.7094339728 0.7094339728 - 0.6870644093 0.7102119923 0.7102119923 - 0.6878855824 0.7109900117 0.7109900117 - 0.6887066960 0.7117680907 0.7117680907 - 0.6895229220 0.7125418782 0.7125418782 - 0.6903378963 0.7133144736 0.7133144736 - 0.6911528707 0.7140871286 0.7140871286 - 0.6919643283 0.7148566246 0.7148566246 - 0.6927723289 0.7156230807 0.7156230807 - 0.6935803890 0.7163894773 0.7163894773 - 0.6943870187 0.7171549797 0.7171549797 - 0.6951882243 0.7179164886 0.7179164886 - 0.6959893703 0.7186779976 0.7186779976 - 0.6967905760 0.7194393873 0.7194393873 - 0.6975867748 0.7201958895 0.7201958895 - 0.6983821988 0.7209516168 0.7209516168 - 0.6991776228 0.7217072248 0.7217072248 - 0.6999700069 0.7224600911 0.7224600911 - 0.7007597089 0.7232102752 0.7232102752 - 0.7015494108 0.7239603996 0.7239603996 - 0.7023385167 0.7247099280 0.7247099280 - 0.7031233907 0.7254531980 0.7254531980 - 0.7039083838 0.7261965275 0.7261965275 - 0.7046933770 0.7269399166 0.7269399166 - 0.7054750919 0.7276784778 0.7276784778 - 0.7062553763 0.7284148932 0.7284148932 - 0.7070357203 0.7291513085 0.7291513085 - 0.7078148127 0.7298862934 0.7298862934 - 0.7085903287 0.7306169271 0.7306169271 - 0.7093657255 0.7313475013 0.7313475013 - 0.7101411223 0.7320780158 0.7320780158 - 0.7109115124 0.7328050733 0.7328050733 - 0.7116804719 0.7335309982 0.7335309982 - 0.7124493718 0.7342568040 0.7342568040 - 0.7132170200 0.7349814773 0.7349814773 - 0.7139806747 0.7357025146 0.7357025146 - 0.7147443891 0.7364236116 0.7364236116 - 0.7155081034 0.7371445894 0.7371445894 - 0.7162681222 0.7378603816 0.7378603816 - 0.7170267105 0.7385740280 0.7385740280 - 0.7177852988 0.7392876148 0.7392876148 - 0.7185432911 0.7400004268 0.7400004268 - 0.7192980051 0.7407081723 0.7407081723 - 0.7200527191 0.7414160967 0.7414160967 - 0.7208074927 0.7421239018 0.7421239018 - 0.7215601206 0.7428290248 0.7428290248 - 0.7223110795 0.7435317039 0.7435317039 - 0.7230620980 0.7442343831 0.7442343831 - 0.7238131166 0.7449371815 0.7449371815 - 0.7245600820 0.7456340194 0.7456340194 - 0.7253068089 0.7463302016 0.7463302016 - 0.7260534763 0.7470265031 0.7470265031 - 0.7267987728 0.7477208972 0.7477208972 - 0.7275406122 0.7484099865 0.7484099865 - 0.7282822728 0.7490991950 0.7490991950 - 0.7290241122 0.7497884035 0.7497884035 - 0.7297623158 0.7504742146 0.7504742146 - 0.7304981947 0.7511575818 0.7511575818 - 0.7312340140 0.7518410087 0.7518410087 - 0.7319697738 0.7525243759 0.7525243759 - 0.7327011228 0.7532038093 0.7532038093 - 0.7334318161 0.7538825870 0.7538825870 - 0.7341625094 0.7545614839 0.7545614839 - 0.7348924279 0.7552397847 0.7552397847 - 0.7356168032 0.7559139729 0.7559139729 - 0.7363412976 0.7565882802 0.7565882802 - 0.7370656729 0.7572625279 0.7572625279 - 0.7377883792 0.7579351068 0.7579351068 - 0.7385078073 0.7586042881 0.7586042881 - 0.7392271757 0.7592735291 0.7592735291 - 0.7399466038 0.7599428296 0.7599428296 - 0.7406626940 0.7606090903 0.7606090903 - 0.7413759828 0.7612727880 0.7612727880 - 0.7420892715 0.7619364262 0.7619364262 - 0.7428025007 0.7626001239 0.7626001239 - 0.7435113192 0.7632591724 0.7632591724 - 0.7442179918 0.7639161944 0.7639161944 - 0.7449247241 0.7645732164 0.7645732164 - 0.7456315160 0.7652301788 0.7652301788 - 0.7463331223 0.7658814788 0.7658814788 - 0.7470337152 0.7665314078 0.7665314078 - 0.7477343082 0.7671812773 0.7671812773 - 0.7484349012 0.7678310871 0.7678310871 - 0.7491310239 0.7684757710 0.7684757710 - 0.7498267293 0.7691197991 0.7691197991 - 0.7505223751 0.7697638273 0.7697638273 - 0.7512180805 0.7704077959 0.7704077959 - 0.7519093156 0.7710475922 0.7710475922 - 0.7526003718 0.7716872096 0.7716872096 - 0.7532914877 0.7723268270 0.7723268270 - 0.7539826035 0.7729663849 0.7729663849 - 0.7546687722 0.7736009955 0.7736009955 - 0.7553548813 0.7742356062 0.7742356062 - 0.7560411096 0.7748702168 0.7748702168 - 0.7567272782 0.7755048275 0.7755048275 - 0.7574092150 0.7761334777 0.7761334777 - 0.7580910921 0.7767620087 0.7767620087 - 0.7587730885 0.7773905993 0.7773905993 - 0.7594550252 0.7780190706 0.7780190706 - 0.7601323724 0.7786415219 0.7786415219 - 0.7608097196 0.7792634964 0.7792634964 - 0.7614868879 0.7798855901 0.7798855901 - 0.7621641159 0.7805076241 0.7805076241 - 0.7628363967 0.7811251283 0.7811251283 - 0.7635082006 0.7817419171 0.7817419171 - 0.7641798854 0.7823588252 0.7823588252 - 0.7648516297 0.7829756141 0.7829756141 - 0.7655196786 0.7835885286 0.7835885286 - 0.7661867738 0.7842003107 0.7842003107 - 0.7668539286 0.7848120928 0.7848120928 - 0.7675210238 0.7854238749 0.7854238749 - 0.7681856155 0.7860323191 0.7860323191 - 0.7688488960 0.7866386771 0.7866386771 - 0.7695121765 0.7872452140 0.7872452140 - 0.7701753974 0.7878515720 0.7878515720 - 0.7708364725 0.7884562016 0.7884562016 - 0.7714951038 0.7890586853 0.7890586853 - 0.7721537948 0.7896612287 0.7896612287 - 0.7728124261 0.7902637124 0.7902637124 - 0.7734698057 0.7908648252 0.7908648252 - 0.7741237879 0.7914618850 0.7914618850 - 0.7747778296 0.7920590043 0.7920590043 - 0.7754318118 0.7926561832 0.7926561832 - 0.7760856748 0.7932531834 0.7932531834 - 0.7767338753 0.7938435078 0.7938435078 - 0.7773820758 0.7944338918 0.7944338918 - 0.7780303955 0.7950242162 0.7950242162 - 0.7786785960 0.7956146002 0.7956146002 - 0.7793235779 0.7962008715 0.7962008715 - 0.7799674273 0.7967858911 0.7967858911 - 0.7806112766 0.7973707914 0.7973707914 - 0.7812551260 0.7979558110 0.7979558110 - 0.7818971872 0.7985386252 0.7985386252 - 0.7825371027 0.7991185188 0.7991185188 - 0.7831770182 0.7996984124 0.7996984124 - 0.7838168740 0.8002783060 0.8002783060 - 0.7844563127 0.8008577824 0.8008577824 - 0.7850909233 0.8014323711 0.8014323711 - 0.7857254744 0.8020070791 0.8020070791 - 0.7863600850 0.8025817871 0.8025817871 - 0.7869946957 0.8031563759 0.8031563759 - 0.7876250744 0.8037273288 0.8037273288 - 0.7882537842 0.8042966127 0.8042966127 - 0.7888823748 0.8048657775 0.8048657775 - 0.7895110250 0.8054350019 0.8054350019 - 0.7901381850 0.8060027957 0.8060027957 - 0.7907618284 0.8065670133 0.8065670133 - 0.7913854122 0.8071311116 0.8071311116 - 0.7920088768 0.8076952100 0.8076952100 - 0.7926325202 0.8082594275 0.8082594275 - 0.7932527065 0.8088204861 0.8088204861 - 0.7938721776 0.8093808889 0.8093808889 - 0.7944917083 0.8099412918 0.8099412918 - 0.7951111794 0.8105016947 0.8105016947 - 0.7957289815 0.8110610247 0.8110610247 - 0.7963436842 0.8116179109 0.8116179109 - 0.7969583869 0.8121749163 0.8121749163 - 0.7975730896 0.8127318025 0.8127318025 - 0.7981876731 0.8132886887 0.8132886887 - 0.7987986803 0.8138427734 0.8138427734 - 0.7994089127 0.8143960834 0.8143960834 - 0.8000190258 0.8149495125 0.8149495125 - 0.8006290793 0.8155028224 0.8155028224 - 0.8012378812 0.8160552979 0.8160552979 - 0.8018423915 0.8166047931 0.8166047931 - 0.8024469018 0.8171542883 0.8171542883 - 0.8030514717 0.8177037835 0.8177037835 - 0.8036559820 0.8182532787 0.8182532787 - 0.8042569160 0.8187999725 0.8187999725 - 0.8048557043 0.8193448782 0.8193448782 - 0.8054544926 0.8198897839 0.8198897839 - 0.8060532808 0.8204346895 0.8204346895 - 0.8066521287 0.8209795952 0.8209795952 - 0.8072462082 0.8215199113 0.8215199113 - 0.8078402877 0.8220599890 0.8220599890 - 0.8084343076 0.8226001859 0.8226001859 - 0.8090283275 0.8231403232 0.8231403232 - 0.8096209168 0.8236796260 0.8236796260 - 0.8102098703 0.8242164254 0.8242164254 - 0.8107990026 0.8247532248 0.8247532248 - 0.8113880157 0.8252900839 0.8252900839 - 0.8119770885 0.8258268833 0.8258268833 - 0.8125630021 0.8263612986 0.8263612986 - 0.8131465912 0.8268936872 0.8268936872 - 0.8137301803 0.8274261951 0.8274261951 - 0.8143138885 0.8279585838 0.8279585838 - 0.8148974776 0.8284909725 0.8284909725 - 0.8154776096 0.8290188909 0.8290188909 - 0.8160567880 0.8295456767 0.8295456767 - 0.8166360855 0.8300724030 0.8300724030 - 0.8172153234 0.8305991888 0.8305991888 - 0.8177946210 0.8311259151 0.8311259151 - 0.8183705211 0.8316484094 0.8316484094 - 0.8189464211 0.8321709037 0.8321709037 - 0.8195223212 0.8326933980 0.8326933980 - 0.8200982213 0.8332158923 0.8332158923 - 0.8206732869 0.8337377906 0.8337377906 - 0.8212442994 0.8342564106 0.8342564106 - 0.8218153119 0.8347749710 0.8347749710 - 0.8223863244 0.8352935910 0.8352935910 - 0.8229573965 0.8358122706 0.8358122706 - 0.8235266209 0.8363298774 0.8363298774 - 0.8240911961 0.8368446231 0.8368446231 - 0.8246557713 0.8373593092 0.8373593092 - 0.8252205253 0.8378741145 0.8378741145 - 0.8257851005 0.8383888006 0.8383888006 - 0.8263477087 0.8389021754 0.8389021754 - 0.8269069791 0.8394131064 0.8394131064 - 0.8274661899 0.8399239779 0.8399239779 - 0.8280254006 0.8404349089 0.8404349089 - 0.8285846114 0.8409457803 0.8409457803 - 0.8291420937 0.8414549232 0.8414549232 - 0.8296970129 0.8419613242 0.8419613242 - 0.8302518129 0.8424677253 0.8424677253 - 0.8308066726 0.8429740071 0.8429740071 - 0.8313614726 0.8434804082 0.8434804082 - 0.8319147229 0.8439850211 0.8439850211 - 0.8324651122 0.8444867730 0.8444867730 - 0.8330155015 0.8449885845 0.8449885845 - 0.8335660100 0.8454903960 0.8454903960 - 0.8341163993 0.8459923267 0.8459923267 - 0.8346651196 0.8464931250 0.8464931250 - 0.8352108002 0.8469917774 0.8469917774 - 0.8357564807 0.8474906087 0.8474906087 - 0.8363021016 0.8479893804 0.8479893804 - 0.8368477821 0.8484880924 0.8484880924 - 0.8373925090 0.8489859104 0.8489859104 - 0.8379346132 0.8494806290 0.8494806290 - 0.8384767175 0.8499752879 0.8499752879 - 0.8390188217 0.8504700065 0.8504700065 - 0.8395608068 0.8509647250 0.8509647250 - 0.8401023746 0.8514587879 0.8514587879 - 0.8406404257 0.8519484997 0.8519484997 - 0.8411784768 0.8524380922 0.8524380922 - 0.8417165279 0.8529276848 0.8529276848 - 0.8422545791 0.8534172773 0.8534172773 - 0.8427925706 0.8539069295 0.8539069295 - 0.8433259130 0.8543931246 0.8543931246 - 0.8438590169 0.8548790812 0.8548790812 - 0.8443921208 0.8553649783 0.8553649783 - 0.8449252248 0.8558509946 0.8558509946 - 0.8454583287 0.8563370109 0.8563370109 - 0.8459873796 0.8568208218 0.8568208218 - 0.8465151787 0.8573036194 0.8573036194 - 0.8470429182 0.8577864766 0.8577864766 - 0.8475707173 0.8582693934 0.8582693934 - 0.8480983973 0.8587523103 0.8587523103 - 0.8486241102 0.8592330217 0.8592330217 - 0.8491476178 0.8597115278 0.8597115278 - 0.8496711254 0.8601899147 0.8601899147 - 0.8501945734 0.8606684208 0.8606684208 - 0.8507180810 0.8611468077 0.8611468077 - 0.8512408137 0.8616245985 0.8616245985 - 0.8517602086 0.8620989919 0.8620989919 - 0.8522796035 0.8625733852 0.8625733852 - 0.8527989984 0.8630477786 0.8630477786 - 0.8533183932 0.8635221720 0.8635221720 - 0.8538377881 0.8639966249 0.8639966249 - 0.8543522954 0.8644680977 0.8644680977 - 0.8548657894 0.8649389744 0.8649389744 - 0.8553792834 0.8654097915 0.8654097915 - 0.8558928967 0.8658807278 0.8658807278 - 0.8564063907 0.8663516045 0.8663516045 - 0.8569173813 0.8668203950 0.8668203950 - 0.8574250937 0.8672863841 0.8672863841 - 0.8579329252 0.8677524924 0.8677524924 - 0.8584406972 0.8682186007 0.8682186007 - 0.8589485288 0.8686845899 0.8686845899 - 0.8594563007 0.8691506982 0.8691506982 - 0.8599594831 0.8696123958 0.8696123958 - 0.8604626060 0.8700739741 0.8700739741 - 0.8609657884 0.8705356121 0.8705356121 - 0.8614689708 0.8709973097 0.8709973097 - 0.8619722128 0.8714588881 0.8714588881 - 0.8624734879 0.8719184995 0.8719184995 - 0.8629732132 0.8723762035 0.8723762035 - 0.8634728193 0.8728339076 0.8728339076 - 0.8639724255 0.8732916117 0.8732916117 - 0.8644720912 0.8737491965 0.8737491965 - 0.8649716973 0.8742069006 0.8742069006 - 0.8654679060 0.8746609092 0.8746609092 - 0.8659641147 0.8751147985 0.8751147985 - 0.8664603233 0.8755686283 0.8755686283 - 0.8669564128 0.8760225177 0.8760225177 - 0.8674526215 0.8764764071 0.8764764071 - 0.8679472208 0.8769292831 0.8769292831 - 0.8684396744 0.8773809075 0.8773809075 - 0.8689323068 0.8778324723 0.8778324723 - 0.8694248199 0.8782839775 0.8782839775 - 0.8699172735 0.8787356019 0.8787356019 - 0.8704097867 0.8791871071 0.8791871071 - 0.8709002137 0.8796362281 0.8796362281 - 0.8713902235 0.8800846934 0.8800846934 - 0.8718801737 0.8805330992 0.8805330992 - 0.8723701835 0.8809816241 0.8809816241 - 0.8728601933 0.8814300895 0.8814300895 - 0.8733496070 0.8818780184 0.8818780184 - 0.8738359213 0.8823224902 0.8823224902 - 0.8743221760 0.8827670217 0.8827670217 - 0.8748083711 0.8832114935 0.8832114935 - 0.8752946854 0.8836559057 0.8836559057 - 0.8757808805 0.8841003776 0.8841003776 - 0.8762651086 0.8845438957 0.8845438957 - 0.8767464757 0.8849858046 0.8849858046 - 0.8772279024 0.8854277730 0.8854277730 - 0.8777093291 0.8858698010 0.8858698010 - 0.8781906962 0.8863118291 0.8863118291 - 0.8786721230 0.8867536783 0.8867536783 - 0.8791509867 0.8871935010 0.8871935010 - 0.8796287179 0.8876323104 0.8876323104 - 0.8801062703 0.8880710006 0.8880710006 - 0.8805840015 0.8885096908 0.8885096908 - 0.8810616732 0.8889483809 0.8889483809 - 0.8815392852 0.8893870711 0.8893870711 - 0.8820139766 0.8898223042 0.8898223042 - 0.8824881911 0.8902568817 0.8902568817 - 0.8829622865 0.8906915188 0.8906915188 - 0.8834365010 0.8911259770 0.8911259770 - 0.8839107156 0.8915606141 0.8915606141 - 0.8843848109 0.8919951916 0.8919951916 - 0.8848553896 0.8924270868 0.8924270868 - 0.8853260279 0.8928589821 0.8928589821 - 0.8857964873 0.8932908773 0.8932908773 - 0.8862671256 0.8937227726 0.8937227726 - 0.8867377043 0.8941547275 0.8941547275 - 0.8872079253 0.8945863247 0.8945863247 - 0.8876746893 0.8950147033 0.8950147033 - 0.8881413937 0.8954432011 0.8954432011 - 0.8886082172 0.8958716989 0.8958716989 - 0.8890749812 0.8963001966 0.8963001966 - 0.8895418048 0.8967286944 0.8967286944 - 0.8900079131 0.8971564770 0.8971564770 - 0.8904703259 0.8975806832 0.8975806832 - 0.8909326196 0.8980048895 0.8980048895 - 0.8913949728 0.8984290957 0.8984290957 - 0.8918573856 0.8988533020 0.8988533020 - 0.8923196793 0.8992775083 0.8992775083 - 0.8927813768 0.8997012973 0.8997012973 - 0.8932392001 0.9001227021 0.9001227021 - 0.8936970234 0.9005439878 0.9005439878 - 0.8941549063 0.9009653926 0.9009653926 - 0.8946127295 0.9013866782 0.9013866782 - 0.8950704932 0.9018080831 0.9018080831 - 0.8955280185 0.9022290707 0.9022290707 - 0.8959823847 0.9026468992 0.9026468992 - 0.8964369297 0.9030647278 0.9030647278 - 0.8968912959 0.9034824967 0.9034824967 - 0.8973457813 0.9039003253 0.9039003253 - 0.8978002071 0.9043180943 0.9043180943 - 0.8982545733 0.9047358036 0.9047358036 - 0.8987048864 0.9051483870 0.9051483870 - 0.8991550803 0.9055609107 0.9055609107 - 0.8996052742 0.9059733152 0.9059733152 - 0.9000555277 0.9063857794 0.9063857794 - 0.9005057216 0.9067983031 0.9067983031 - 0.9009559155 0.9072108269 0.9072108269 - 0.9014015794 0.9076197743 0.9076197743 - 0.9018465281 0.9080280066 0.9080280066 - 0.9022914171 0.9084362984 0.9084362984 - 0.9027363062 0.9088445902 0.9088445902 - 0.9031810760 0.9092528820 0.9092528820 - 0.9036260247 0.9096611738 0.9096611738 - 0.9040681720 0.9100679755 0.9100679755 - 0.9045090079 0.9104740024 0.9104740024 - 0.9049497247 0.9108799100 0.9108799100 - 0.9053903818 0.9112858772 0.9112858772 - 0.9058312178 0.9116917849 0.9116917849 - 0.9062718749 0.9120978117 0.9120978117 - 0.9067112207 0.9125025868 0.9125025868 - 0.9071481824 0.9129055142 0.9129055142 - 0.9075852036 0.9133083820 0.9133083820 - 0.9080221057 0.9137113094 0.9137113094 - 0.9084591269 0.9141142964 0.9141142964 - 0.9088960886 0.9145172238 0.9145172238 - 0.9093328118 0.9149199128 0.9149199128 - 0.9097660184 0.9153187871 0.9153187871 - 0.9101991057 0.9157177210 0.9157177210 - 0.9106323123 0.9161165953 0.9161165953 - 0.9110653996 0.9165155292 0.9165155292 - 0.9114986062 0.9169144034 0.9169144034 - 0.9119318128 0.9173132777 0.9173132777 - 0.9123622179 0.9177098274 0.9177098274 - 0.9127916098 0.9181053042 0.9181053042 - 0.9132210016 0.9185007811 0.9185007811 - 0.9136503935 0.9188963175 0.9188963175 - 0.9140797853 0.9192917943 0.9192917943 - 0.9145091772 0.9196872711 0.9196872711 - 0.9149373174 0.9200819731 0.9200819731 - 0.9153624177 0.9204747081 0.9204747081 - 0.9157875180 0.9208673835 0.9208673835 - 0.9162126184 0.9212601781 0.9212601781 - 0.9166377187 0.9216529131 0.9216529131 - 0.9170628190 0.9220455885 0.9220455885 - 0.9174879193 0.9224383831 0.9224383831 - 0.9179093242 0.9228280187 0.9228280187 - 0.9183300734 0.9232169986 0.9232169986 - 0.9187508225 0.9236060977 0.9236060977 - 0.9191715717 0.9239950776 0.9239950776 - 0.9195923209 0.9243841171 0.9243841171 - 0.9200131297 0.9247730970 0.9247730970 - 0.9204329252 0.9251611829 0.9251611829 - 0.9208505154 0.9255470037 0.9255470037 - 0.9212682247 0.9259328842 0.9259328842 - 0.9216858745 0.9263188243 0.9263188243 - 0.9221035838 0.9267047048 0.9267047048 - 0.9225212932 0.9270905852 0.9270905852 - 0.9229388833 0.9274765253 0.9274765253 - 0.9233536720 0.9278604984 0.9278604984 - 0.9237673879 0.9282438755 0.9282438755 - 0.9241812229 0.9286273122 0.9286273122 - 0.9245948792 0.9290106893 0.9290106893 - 0.9250087142 0.9293940067 0.9293940067 - 0.9254223704 0.9297773838 0.9297773838 - 0.9258356094 0.9301604033 0.9301604033 - 0.9262446165 0.9305403829 0.9305403829 - 0.9266535044 0.9309204221 0.9309204221 - 0.9270625114 0.9313002825 0.9313002825 - 0.9274715185 0.9316803217 0.9316803217 - 0.9278804064 0.9320603013 0.9320603013 - 0.9282894135 0.9324402213 0.9324402213 - 0.9286968112 0.9328190088 0.9328190088 - 0.9291023016 0.9331961274 0.9331961274 - 0.9295076728 0.9335731864 0.9335731864 - 0.9299131036 0.9339503050 0.9339503050 - 0.9303184748 0.9343274236 0.9343274236 - 0.9307240248 0.9347046018 0.9347046018 - 0.9311293960 0.9350817204 0.9350817204 - 0.9315326810 0.9354572296 0.9354572296 - 0.9319350123 0.9358320832 0.9358320832 - 0.9323372841 0.9362069964 0.9362069964 - 0.9327396154 0.9365817904 0.9365817904 - 0.9331418872 0.9369567037 0.9369567037 - 0.9335442781 0.9373314977 0.9373314977 - 0.9339466095 0.9377064109 0.9377064109 - 0.9343459010 0.9380785227 0.9380785227 - 0.9347448945 0.9384502172 0.9384502172 - 0.9351438880 0.9388219118 0.9388219118 - 0.9355428815 0.9391937256 0.9391937256 - 0.9359418750 0.9395654202 0.9395654202 - 0.9363409281 0.9399371147 0.9399371147 - 0.9367398024 0.9403086901 0.9403086901 - 0.9371359944 0.9406778812 0.9406778812 - 0.9375321865 0.9410470128 0.9410470128 - 0.9379284978 0.9414162040 0.9414162040 - 0.9383246899 0.9417852759 0.9417852759 - 0.9387210011 0.9421545267 0.9421545267 - 0.9391171932 0.9425235987 0.9425235987 - 0.9395130277 0.9428924918 0.9428924918 - 0.9399057031 0.9432595968 0.9432595968 - 0.9402983189 0.9436268210 0.9436268210 - 0.9406909943 0.9439939260 0.9439939260 - 0.9410837293 0.9443609715 0.9443609715 - 0.9414764047 0.9447280765 0.9447280765 - 0.9418690205 0.9450953007 0.9450953007 - 0.9422609806 0.9454616904 0.9454616904 - 0.9426497221 0.9458243847 0.9458243847 - 0.9430382848 0.9461871982 0.9461871982 - 0.9434270263 0.9465500116 0.9465500116 - 0.9438155890 0.9469127059 0.9469127059 - 0.9442042708 0.9472755194 0.9472755194 - 0.9445928931 0.9476382732 0.9476382732 - 0.9449806213 0.9480000734 0.9480000734 - 0.9453629851 0.9483562708 0.9483562708 - 0.9457452893 0.9487125278 0.9487125278 - 0.9461275935 0.9490686059 0.9490686059 - 0.9465098977 0.9494248033 0.9494248033 - 0.9468922019 0.9497810006 0.9497810006 - 0.9472745061 0.9501371980 0.9501371980 - 0.9476562738 0.9504929781 0.9504929781 - 0.9480308890 0.9508436918 0.9508436918 - 0.9484055042 0.9511944056 0.9511944056 - 0.9487801194 0.9515451193 0.9515451193 - 0.9491547942 0.9518957734 0.9518957734 - 0.9495294094 0.9522464871 0.9522464871 - 0.9499040246 0.9525972009 0.9525972009 - 0.9502785802 0.9529479146 0.9529479146 - 0.9506484270 0.9532948732 0.9532948732 - 0.9510179162 0.9536415935 0.9536415935 - 0.9513872862 0.9539883733 0.9539883733 - 0.9517567158 0.9543350935 0.9543350935 - 0.9521262050 0.9546818137 0.9546818137 - 0.9524955750 0.9550285935 0.9550285935 - 0.9528651237 0.9553753138 0.9553753138 - 0.9532322884 0.9557194710 0.9557194710 - 0.9535986781 0.9560627937 0.9560627937 - 0.9539651275 0.9564061165 0.9564061165 - 0.9543313980 0.9567493796 0.9567493796 - 0.9546977878 0.9570927024 0.9570927024 - 0.9550641775 0.9574360251 0.9574360251 - 0.9554306269 0.9577792883 0.9577792883 - 0.9557952285 0.9581211805 0.9581211805 - 0.9561576843 0.9584614038 0.9584614038 - 0.9565201998 0.9588016272 0.9588016272 - 0.9568827152 0.9591417909 0.9591417909 - 0.9572451711 0.9594820142 0.9594820142 - 0.9576076865 0.9598221183 0.9598221183 - 0.9579702020 0.9601622820 0.9601622820 - 0.9583321214 0.9605020285 0.9605020285 - 0.9586902261 0.9608384967 0.9608384967 - 0.9590482712 0.9611749053 0.9611749053 - 0.9594063163 0.9615113735 0.9615113735 - 0.9597644210 0.9618479013 0.9618479013 - 0.9601225257 0.9621844292 0.9621844292 - 0.9604805708 0.9625207782 0.9625207782 - 0.9608386755 0.9628573060 0.9628573060 - 0.9611936212 0.9631906748 0.9631906748 - 0.9615474939 0.9635230899 0.9635230899 - 0.9619014263 0.9638555050 0.9638555050 - 0.9622552991 0.9641879201 0.9641879201 - 0.9626091719 0.9645202756 0.9645202756 - 0.9629632235 0.9648526907 0.9648526907 - 0.9633170962 0.9651851058 0.9651851058 - 0.9636694193 0.9655163288 0.9655163288 - 0.9640185833 0.9658449292 0.9658449292 - 0.9643678069 0.9661735892 0.9661735892 - 0.9647169709 0.9665021896 0.9665021896 - 0.9650661945 0.9668309093 0.9668309093 - 0.9654154181 0.9671595097 0.9671595097 - 0.9657645822 0.9674882293 0.9674882293 - 0.9661138058 0.9678168297 0.9678168297 - 0.9664589763 0.9681419134 0.9681419134 - 0.9668033123 0.9684662819 0.9684662819 - 0.9671475887 0.9687907100 0.9687907100 - 0.9674918056 0.9691150784 0.9691150784 - 0.9678360820 0.9694393873 0.9694393873 - 0.9681804180 0.9697638154 0.9697638154 - 0.9685246944 0.9700881839 0.9700881839 - 0.9688677788 0.9704114199 0.9704114199 - 0.9692075849 0.9707313180 0.9707313180 - 0.9695475101 0.9710512161 0.9710512161 - 0.9698873162 0.9713711143 0.9713711143 - 0.9702271223 0.9716910124 0.9716910124 - 0.9705669284 0.9720107913 0.9720107913 - 0.9709066749 0.9723306894 0.9723306894 - 0.9712464809 0.9726505876 0.9726505876 - 0.9715837240 0.9729676843 0.9729676843 - 0.9719191790 0.9732831717 0.9732831717 - 0.9722548127 0.9735987186 0.9735987186 - 0.9725903869 0.9739142060 0.9739142060 - 0.9729259014 0.9742296934 0.9742296934 - 0.9732614756 0.9745451808 0.9745451808 - 0.9735971093 0.9748606086 0.9748606086 - 0.9739326239 0.9751760960 0.9751760960 - 0.9742640257 0.9754871130 0.9754871130 - 0.9745951295 0.9757977724 0.9757977724 - 0.9749261737 0.9761084914 0.9761084914 - 0.9752572775 0.9764190912 0.9764190912 - 0.9755883813 0.9767298102 0.9767298102 - 0.9759194851 0.9770405293 0.9770405293 - 0.9762505889 0.9773511291 0.9773511291 - 0.9765807986 0.9776608944 0.9776608944 - 0.9769067764 0.9779667258 0.9779667258 - 0.9772328138 0.9782724977 0.9782724977 - 0.9775589108 0.9785783291 0.9785783291 - 0.9778848886 0.9788841009 0.9788841009 - 0.9782109261 0.9791898727 0.9791898727 - 0.9785370231 0.9794957042 0.9794957042 - 0.9788630009 0.9798014164 0.9798014164 - 0.9791870713 0.9801052809 0.9801052809 - 0.9795076847 0.9804059267 0.9804059267 - 0.9798284173 0.9807065129 0.9807065129 - 0.9801490903 0.9810069799 0.9810069799 - 0.9804697037 0.9813076258 0.9813076258 - 0.9807903767 0.9816082120 0.9816082120 - 0.9811111093 0.9819087982 0.9819087982 - 0.9814317226 0.9822093248 0.9822093248 - 0.9817494154 0.9825072885 0.9825072885 - 0.9820637703 0.9828022718 0.9828022718 - 0.9823781848 0.9830973148 0.9830973148 - 0.9826925993 0.9833922982 0.9833922982 - 0.9830070138 0.9836874008 0.9836874008 - 0.9833214283 0.9839823842 0.9839823842 - 0.9836357236 0.9842774272 0.9842774272 - 0.9839500785 0.9845724106 0.9845724106 - 0.9842607975 0.9848645926 0.9848645926 - 0.9845681787 0.9851542115 0.9851542115 - 0.9848756194 0.9854437709 0.9854437709 - 0.9851830006 0.9857333899 0.9857333899 - 0.9854903817 0.9860230088 0.9860230088 - 0.9857978225 0.9863126874 0.9863126874 - 0.9861052036 0.9866023064 0.9866023064 - 0.9864125848 0.9868919253 0.9868919253 - 0.9867162704 0.9871780872 0.9871780872 - 0.9870163798 0.9874610901 0.9874610901 - 0.9873164892 0.9877440929 0.9877440929 - 0.9876165986 0.9880272150 0.9880272150 - 0.9879167080 0.9883102179 0.9883102179 - 0.9882168174 0.9885932207 0.9885932207 - 0.9885169268 0.9888762236 0.9888762236 - 0.9888169765 0.9891592264 0.9891592264 - 0.9891130924 0.9894391894 0.9894391894 - 0.9894037247 0.9897152781 0.9897152781 - 0.9896944165 0.9899914265 0.9899914265 - 0.9899849892 0.9902673960 0.9902673960 - 0.9902756810 0.9905434847 0.9905434847 - 0.9905663133 0.9908195734 0.9908195734 - 0.9908568859 0.9910956025 0.9910956025 - 0.9911475778 0.9913716912 0.9913716912 - 0.9914352894 0.9916458726 0.9916458726 - 0.9917153716 0.9919152260 0.9919152260 - 0.9919955134 0.9921845198 0.9921845198 - 0.9922757149 0.9924538136 0.9924538136 - 0.9925557971 0.9927231073 0.9927231073 - 0.9928358793 0.9929924011 0.9929924011 - 0.9931160808 0.9932618141 0.9932618141 - 0.9933962226 0.9935311079 0.9935311079 - 0.9936755896 0.9937998056 0.9937998056 - 0.9939448833 0.9940592051 0.9940592051 - 0.9942141175 0.9943186045 0.9943186045 - 0.9944832921 0.9945781231 0.9945781231 - 0.9947525263 0.9948375225 0.9948375225 - 0.9950217009 0.9950969815 0.9950969815 - 0.9952909946 0.9953563809 0.9953563809 - 0.9955602288 0.9956158996 0.9956158996 - 0.9958294034 0.9958752990 0.9958752990 - 0.9960865974 0.9961261749 0.9961261749 - 0.9963403940 0.9963746071 0.9963746071 - 0.9965941906 0.9966229796 0.9966229796 - 0.9968479276 0.9968714118 0.9968714118 - 0.9971017241 0.9971197844 0.9971197844 - 0.9973555207 0.9973682165 0.9973682165 - 0.9976093173 0.9976165891 0.9976165891 - 0.9978629947 0.9978650212 0.9978650212 - 0.9981094003 0.9981083274 0.9981083274 - 0.9983456731 0.9983447790 0.9983447790 - 0.9985821247 0.9985812902 0.9985812902 - 0.9988183975 0.9988176823 0.9988176823 - 0.9990546703 0.9990541935 0.9990541935 - 0.9992910028 0.9992905855 0.9992905855 - 0.9995273948 0.9995270967 0.9995270967 - 0.9997637272 0.9997634888 0.9997634888 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0111121098 0.0121509498 0.0121509498 + 0.0217311792 0.0234846398 0.0234846398 + 0.0310309306 0.0335913785 0.0335913785 + 0.0394427888 0.0427072011 0.0427072011 + 0.0472172312 0.0510250889 0.0510250889 + 0.0544895791 0.0587030984 0.0587030984 + 0.0613539182 0.0658273399 0.0658273399 + 0.0678610802 0.0724957809 0.0724957809 + 0.0740588829 0.0787753463 0.0787753463 + 0.0799922720 0.0847034305 0.0847034305 + 0.0856926665 0.0903405622 0.0903405622 + 0.0911944881 0.0957725719 0.0957725719 + 0.0965085030 0.1010297015 0.1010297015 + 0.1016504988 0.1061294973 0.1061294973 + 0.1066408977 0.1110759974 0.1110759974 + 0.1114827022 0.1158965006 0.1158965006 + 0.1161810011 0.1205943003 0.1205943003 + 0.1207583994 0.1251748055 0.1251748055 + 0.1252014935 0.1296516955 0.1296516955 + 0.1295239031 0.1340274960 0.1340274960 + 0.1337441057 0.1383039951 0.1383039951 + 0.1378667057 0.1424947977 0.1424947977 + 0.1418835968 0.1466037929 0.1466037929 + 0.1458135992 0.1506410986 0.1506410986 + 0.1496558040 0.1545946002 0.1545946002 + 0.1534211040 0.1584779024 0.1584779024 + 0.1571090072 0.1622924954 0.1622924954 + 0.1607228965 0.1660465002 0.1660465002 + 0.1642647982 0.1697326005 0.1697326005 + 0.1677455008 0.1733641028 0.1733641028 + 0.1711609066 0.1769354939 0.1769354939 + 0.1745131016 0.1804486960 0.1804486960 + 0.1778111011 0.1839037985 0.1839037985 + 0.1810566932 0.1873078942 0.1873078942 + 0.1842480004 0.1906577051 0.1906577051 + 0.1873922944 0.1939612031 0.1939612031 + 0.1904910952 0.1972206980 0.1972206980 + 0.1935475022 0.2004390955 0.2004390955 + 0.1965657026 0.2036167979 0.2036167979 + 0.1995460987 0.2067523003 0.2067523003 + 0.2024825066 0.2098508030 0.2098508030 + 0.2053820044 0.2129165977 0.2129165977 + 0.2082439959 0.2159480006 0.2159480006 + 0.2110687941 0.2189517021 0.2189517021 + 0.2138601989 0.2219190001 0.2219190001 + 0.2166164964 0.2248522043 0.2248522043 + 0.2193437964 0.2277560979 0.2277560979 + 0.2220394015 0.2306237966 0.2306237966 + 0.2247070074 0.2334605008 0.2334605008 + 0.2273449004 0.2362726033 0.2362726033 + 0.2299581021 0.2390560061 0.2390560061 + 0.2325424999 0.2418099940 0.2418099940 + 0.2350990027 0.2445425987 0.2445425987 + 0.2376336008 0.2472528070 0.2472528070 + 0.2401454002 0.2499340028 0.2499340028 + 0.2426387966 0.2525874972 0.2525874972 + 0.2451128960 0.2552196980 0.2552196980 + 0.2475619018 0.2578315139 0.2578315139 + 0.2499863058 0.2604233027 0.2604233027 + 0.2523908913 0.2629944980 0.2629944980 + 0.2547743022 0.2655487061 0.2655487061 + 0.2571432889 0.2680800855 0.2680800855 + 0.2594946027 0.2705903947 0.2705903947 + 0.2618246973 0.2730821967 0.2730821967 + 0.2641352117 0.2755582929 0.2755582929 + 0.2664259076 0.2780191004 0.2780191004 + 0.2686997056 0.2804580927 0.2804580927 + 0.2709574103 0.2828805149 0.2828805149 + 0.2731972933 0.2852869034 0.2852869034 + 0.2754215896 0.2876777947 0.2876777947 + 0.2776325047 0.2900503874 0.2900503874 + 0.2798275948 0.2924054861 0.2924054861 + 0.2820053995 0.2947433889 0.2947433889 + 0.2841629088 0.2970634997 0.2970634997 + 0.2863053083 0.2993713021 0.2993713021 + 0.2884348929 0.3016661108 0.3016661108 + 0.2905501127 0.3039461970 0.3039461970 + 0.2926583886 0.3062120080 0.3062120080 + 0.2947568893 0.3084635139 0.3084635139 + 0.2968437076 0.3106997013 0.3106997013 + 0.2989166081 0.3129228055 0.3129228055 + 0.3009737134 0.3151350021 0.3151350021 + 0.3030146956 0.3173327148 0.3173327148 + 0.3050420880 0.3195173144 0.3195173144 + 0.3070568144 0.3216913044 0.3216913044 + 0.3090623915 0.3238545060 0.3238545060 + 0.3110578060 0.3260023892 0.3260023892 + 0.3130468130 0.3281404078 0.3281404078 + 0.3150278926 0.3302693069 0.3302693069 + 0.3169982135 0.3323845863 0.3323845863 + 0.3189576864 0.3344885111 0.3344885111 + 0.3209047019 0.3365832865 0.3365832865 + 0.3228406012 0.3386682868 0.3386682868 + 0.3247657120 0.3407419920 0.3407419920 + 0.3266789913 0.3428055942 0.3428055942 + 0.3285810053 0.3448581100 0.3448581100 + 0.3304736018 0.3468984962 0.3468984962 + 0.3323583901 0.3489289880 0.3489289880 + 0.3342367113 0.3509478867 0.3509478867 + 0.3361082971 0.3529567122 0.3529567122 + 0.3379718065 0.3549566865 0.3549566865 + 0.3398267031 0.3569482863 0.3569482863 + 0.3416717947 0.3589293957 0.3589293957 + 0.3435060084 0.3609009981 0.3609009981 + 0.3453314900 0.3628633916 0.3628633916 + 0.3471463919 0.3648136854 0.3648136854 + 0.3489513993 0.3667523861 0.3667523861 + 0.3507472873 0.3686820865 0.3686820865 + 0.3525382876 0.3706041873 0.3706041873 + 0.3543233871 0.3725175858 0.3725175858 + 0.3561016917 0.3744210899 0.3744210899 + 0.3578726053 0.3763135970 0.3763135970 + 0.3596351147 0.3781942129 0.3781942129 + 0.3613910973 0.3800665140 0.3800665140 + 0.3631403148 0.3819315135 0.3819315135 + 0.3648819029 0.3837873042 0.3837873042 + 0.3666147888 0.3856337070 0.3856337070 + 0.3683386147 0.3874723017 0.3874723017 + 0.3700559139 0.3893026114 0.3893026114 + 0.3717651963 0.3911237121 0.3911237121 + 0.3734667003 0.3929347992 0.3929347992 + 0.3751592040 0.3947354853 0.3947354853 + 0.3768459857 0.3965282142 0.3965282142 + 0.3785249889 0.3983115852 0.3983115852 + 0.3801974952 0.4000878930 0.4000878930 + 0.3818610907 0.4018566012 0.4018566012 + 0.3835192919 0.4036169052 0.4036169052 + 0.3851707876 0.4053680897 0.4053680897 + 0.3868159950 0.4071125984 0.4071125984 + 0.3884536028 0.4088492990 0.4088492990 + 0.3900853097 0.4105800986 0.4105800986 + 0.3917112947 0.4123049974 0.4123049974 + 0.3933308125 0.4140234888 0.4140234888 + 0.3949447870 0.4157348871 0.4157348871 + 0.3965539038 0.4174388051 0.4174388051 + 0.3981573880 0.4191350937 0.4191350937 + 0.3997552991 0.4208256006 0.4208256006 + 0.4013460875 0.4225083888 0.4225083888 + 0.4029329121 0.4241839945 0.4241839945 + 0.4045143127 0.4258517921 0.4258517921 + 0.4060910046 0.4275138080 0.4275138080 + 0.4076623023 0.4291698933 0.4291698933 + 0.4092277884 0.4308196008 0.4308196008 + 0.4107879102 0.4324634969 0.4324634969 + 0.4123404026 0.4340988100 0.4340988100 + 0.4138863087 0.4357272089 0.4357272089 + 0.4154250026 0.4373478889 0.4373478889 + 0.4169568121 0.4389612079 0.4389612079 + 0.4184818864 0.4405669868 0.4405669868 + 0.4199990034 0.4421640933 0.4421640933 + 0.4215070009 0.4437515140 0.4437515140 + 0.4230087101 0.4453321993 0.4453321993 + 0.4245057106 0.4469063878 0.4469063878 + 0.4259983897 0.4484742880 0.4484742880 + 0.4274863899 0.4500342011 0.4500342011 + 0.4289681017 0.4515857995 0.4515857995 + 0.4304446876 0.4531300068 0.4531300068 + 0.4319162071 0.4546670914 0.4546670914 + 0.4333826005 0.4561972916 0.4561972916 + 0.4348441064 0.4577195942 0.4577195942 + 0.4363026917 0.4592362046 0.4592362046 + 0.4377590120 0.4607473016 0.4607473016 + 0.4392131865 0.4622528851 0.4622528851 + 0.4406625032 0.4637500048 0.4637500048 + 0.4421091080 0.4652411938 0.4652411938 + 0.4435526133 0.4667268991 0.4667268991 + 0.4449920058 0.4682061076 0.4682061076 + 0.4464268088 0.4696769118 0.4696769118 + 0.4478574991 0.4711419940 0.4711419940 + 0.4492841959 0.4726015031 0.4726015031 + 0.4507060051 0.4740524888 0.4740524888 + 0.4521220922 0.4754965901 0.4754965901 + 0.4535340965 0.4769350886 0.4769350886 + 0.4549413919 0.4783662856 0.4783662856 + 0.4563410878 0.4797913134 0.4797913134 + 0.4577364922 0.4812119901 0.4812119901 + 0.4591276050 0.4826276898 0.4826276898 + 0.4605121911 0.4840362966 0.4840362966 + 0.4618934989 0.4854404926 0.4854404926 + 0.4632714987 0.4868401885 0.4868401885 + 0.4646422863 0.4882321954 0.4882321954 + 0.4660094976 0.4896200001 0.4896200001 + 0.4673734009 0.4910030961 0.4910030961 + 0.4687319994 0.4923816025 0.4923816025 + 0.4700880945 0.4937570095 0.4937570095 + 0.4714413881 0.4951288104 0.4951288104 + 0.4727892876 0.4964961112 0.4964961112 + 0.4741345942 0.4978606999 0.4978606999 + 0.4754764140 0.4992212057 0.4992212057 + 0.4768131077 0.5005778074 0.5005778074 + 0.4781472087 0.5019320250 0.5019320250 + 0.4794766903 0.5032812953 0.5032812953 + 0.4808017015 0.5046275854 0.5046275854 + 0.4821242094 0.5059717894 0.5059717894 + 0.4834404886 0.5073099732 0.5073099732 + 0.4847536087 0.5086460710 0.5086460710 + 0.4860630035 0.5099794865 0.5099794865 + 0.4873664975 0.5113078952 0.5113078952 + 0.4886673987 0.5126342773 0.5126342773 + 0.4899624884 0.5139567852 0.5139567852 + 0.4912536144 0.5152760744 0.5152760744 + 0.4925413132 0.5165923238 0.5165923238 + 0.4938223064 0.5179045796 0.5179045796 + 0.4951008856 0.5192151070 0.5192151070 + 0.4963732958 0.5205205083 0.5205205083 + 0.4976417124 0.5218238235 0.5218238235 + 0.4989061058 0.5231245160 0.5231245160 + 0.5001640916 0.5244206190 0.5244206190 + 0.5014199018 0.5257151127 0.5257151127 + 0.5026680231 0.5270051956 0.5270051956 + 0.5039132833 0.5282933712 0.5282933712 + 0.5051530004 0.5295773745 0.5295773745 + 0.5063877106 0.5308588743 0.5308588743 + 0.5076187253 0.5321382880 0.5321382880 + 0.5088446736 0.5334129930 0.5334129930 + 0.5100693107 0.5346865058 0.5346865058 + 0.5112869740 0.5359553099 0.5359553099 + 0.5125030875 0.5372229815 0.5372229815 + 0.5137143135 0.5384858847 0.5384858847 + 0.5149223804 0.5397471189 0.5397471189 + 0.5161263943 0.5410054922 0.5410054922 + 0.5173262954 0.5422605872 0.5422605872 + 0.5185235143 0.5435131192 0.5435131192 + 0.5197150707 0.5447623730 0.5447623730 + 0.5209047794 0.5460104942 0.5460104942 + 0.5220907927 0.5472519994 0.5472519994 + 0.5232760906 0.5484923720 0.5484923720 + 0.5244560838 0.5497261286 0.5497261286 + 0.5256354809 0.5509589911 0.5509589911 + 0.5268111825 0.5521854162 0.5521854162 + 0.5279856920 0.5534102917 0.5534102917 + 0.5291559100 0.5546302199 0.5546302199 + 0.5303248167 0.5558478832 0.5558478832 + 0.5314909220 0.5570605993 0.5570605993 + 0.5326548815 0.5582709908 0.5582709908 + 0.5338155031 0.5594773889 0.5594773889 + 0.5349742770 0.5606803298 0.5606803298 + 0.5361307859 0.5618779063 0.5618779063 + 0.5372846723 0.5630717278 0.5630717278 + 0.5384358168 0.5642607808 0.5642607808 + 0.5395848155 0.5654460788 0.5654460788 + 0.5407317877 0.5666270256 0.5666270256 + 0.5418760777 0.5678040981 0.5678040981 + 0.5430176854 0.5689768791 0.5689768791 + 0.5441573858 0.5701463819 0.5701463819 + 0.5452948809 0.5713114142 0.5713114142 + 0.5464299917 0.5724735260 0.5724735260 + 0.5475621819 0.5736311078 0.5736311078 + 0.5486928225 0.5747861266 0.5747861266 + 0.5498210192 0.5759364963 0.5759364963 + 0.5509474277 0.5770848989 0.5770848989 + 0.5520703197 0.5782284141 0.5782284141 + 0.5531921983 0.5793704987 0.5793704987 + 0.5543109775 0.5805072188 0.5805072188 + 0.5554292202 0.5816432238 0.5816432238 + 0.5565428138 0.5827717781 0.5827717781 + 0.5576562881 0.5839003921 0.5839003921 + 0.5587658882 0.5850207806 0.5850207806 + 0.5598756075 0.5861409903 0.5861409903 + 0.5609806180 0.5872548819 0.5872548819 + 0.5620849729 0.5883675814 0.5883675814 + 0.5631865859 0.5894749165 0.5894749165 + 0.5642871261 0.5905798078 0.5905798078 + 0.5653848052 0.5916802287 0.5916802287 + 0.5664801002 0.5927764773 0.5927764773 + 0.5675740242 0.5938696861 0.5938696861 + 0.5686653852 0.5949565172 0.5949565172 + 0.5697557926 0.5960423946 0.5960423946 + 0.5708405972 0.5971215963 0.5971215963 + 0.5719254017 0.5982007980 0.5982007980 + 0.5730047226 0.5992723107 0.5992723107 + 0.5740836263 0.6003429890 0.6003429890 + 0.5751580000 0.6014091969 0.6014091969 + 0.5762305260 0.6024730802 0.6024730802 + 0.5773004889 0.6035333276 0.6035333276 + 0.5783671737 0.6045879722 0.6045879722 + 0.5794330835 0.6056417823 0.6056417823 + 0.5804936290 0.6066883802 0.6066883802 + 0.5815541148 0.6077349782 0.6077349782 + 0.5826098919 0.6087749004 0.6087749004 + 0.5836648941 0.6098132730 0.6098132730 + 0.5847159028 0.6108481884 0.6108481884 + 0.5857632160 0.6118795872 0.6118795872 + 0.5868092775 0.6129097939 0.6129097939 + 0.5878490806 0.6139339209 0.6139339209 + 0.5888888836 0.6149579883 0.6149579883 + 0.5899229050 0.6159774065 0.6159774065 + 0.5909554958 0.6169953942 0.6169953942 + 0.5919851065 0.6180102229 0.6180102229 + 0.5930107236 0.6190204024 0.6190204024 + 0.5940359831 0.6200305223 0.6200305223 + 0.5950549245 0.6210334897 0.6210334897 + 0.5960736871 0.6220365167 0.6220365167 + 0.5970885158 0.6230354905 0.6230354905 + 0.5981007814 0.6240319014 0.6240319014 + 0.5991119742 0.6250271201 0.6250271201 + 0.6001179814 0.6260160208 0.6260160208 + 0.6011241078 0.6270048022 0.6270048022 + 0.6021257043 0.6279885173 0.6279885173 + 0.6031255722 0.6289699078 0.6289699078 + 0.6041241288 0.6299499273 0.6299499273 + 0.6051182151 0.6309251785 0.6309251785 + 0.6061123013 0.6319004297 0.6319004297 + 0.6071022153 0.6328712106 0.6328712106 + 0.6080906987 0.6338403821 0.6338403821 + 0.6090779901 0.6348081827 0.6348081827 + 0.6100609899 0.6357709169 0.6357709169 + 0.6110439897 0.6367335916 0.6367335916 + 0.6120234728 0.6376916766 0.6376916766 + 0.6130014062 0.6386474967 0.6386474967 + 0.6139786243 0.6396028996 0.6396028996 + 0.6149513721 0.6405537724 0.6405537724 + 0.6159241796 0.6415047050 0.6415047050 + 0.6168935895 0.6424521804 0.6424521804 + 0.6178600788 0.6433963180 0.6433963180 + 0.6188266873 0.6443403959 0.6443403959 + 0.6197869778 0.6452776790 0.6452776790 + 0.6207470298 0.6462143064 0.6462143064 + 0.6217051744 0.6471490860 0.6471490860 + 0.6226593852 0.6480789185 0.6480789185 + 0.6236134768 0.6490086913 0.6490086913 + 0.6245638132 0.6499348283 0.6499348283 + 0.6255118847 0.6508585811 0.6508585811 + 0.6264600754 0.6517825127 0.6517825127 + 0.6274017096 0.6527013183 0.6527013183 + 0.6283431053 0.6536198854 0.6536198854 + 0.6292824149 0.6545372009 0.6545372009 + 0.6302164793 0.6554512978 0.6554512978 + 0.6311504841 0.6563653946 0.6563653946 + 0.6320812106 0.6572765708 0.6572765708 + 0.6330093145 0.6581853032 0.6581853032 + 0.6339374781 0.6590940952 0.6590940952 + 0.6348599792 0.6599957943 0.6599957943 + 0.6357815266 0.6608958244 0.6608958244 + 0.6367025971 0.6617954969 0.6617954969 + 0.6376188993 0.6626883149 0.6626883149 + 0.6385352015 0.6635810733 0.6635810733 + 0.6394497752 0.6644719243 0.6644719243 + 0.6403601766 0.6653575897 0.6653575897 + 0.6412705779 0.6662433147 0.6662433147 + 0.6421788931 0.6671264172 0.6671264172 + 0.6430848837 0.6680064797 0.6680064797 + 0.6439908147 0.6688866019 0.6688866019 + 0.6448925138 0.6697621942 0.6697621942 + 0.6457918882 0.6706354022 0.6706354022 + 0.6466913819 0.6715084910 0.6715084910 + 0.6475862861 0.6723757982 0.6723757982 + 0.6484801769 0.6732414961 0.6732414961 + 0.6493741274 0.6741071939 0.6741071939 + 0.6502627134 0.6749665737 0.6749665737 + 0.6511508822 0.6758252978 0.6758252978 + 0.6520388722 0.6766840219 0.6766840219 + 0.6529223919 0.6775373220 0.6775373220 + 0.6538059115 0.6783906221 0.6783906221 + 0.6546888947 0.6792433858 0.6792433858 + 0.6555675864 0.6800916791 0.6800916791 + 0.6564462185 0.6809399128 0.6809399128 + 0.6573243141 0.6817873716 0.6817873716 + 0.6581990719 0.6826301217 0.6826301217 + 0.6590738893 0.6834728122 0.6834728122 + 0.6599478126 0.6843147278 0.6843147278 + 0.6608172059 0.6851527095 0.6851527095 + 0.6616867185 0.6859906912 0.6859906912 + 0.6625552177 0.6868278980 0.6868278980 + 0.6634197831 0.6876606941 0.6876606941 + 0.6642844081 0.6884933710 0.6884933710 + 0.6651483178 0.6893252730 0.6893252730 + 0.6660085917 0.6901518106 0.6901518106 + 0.6668688059 0.6909784079 0.6909784079 + 0.6677287221 0.6918041706 0.6918041706 + 0.6685851216 0.6926234961 0.6926234961 + 0.6694415808 0.6934428215 0.6934428215 + 0.6702979207 0.6942619085 0.6942619085 + 0.6711509824 0.6950753927 0.6950753927 + 0.6720041037 0.6958888769 0.6958888769 + 0.6728572845 0.6967023015 0.6967023015 + 0.6737046838 0.6975108981 0.6975108981 + 0.6745517850 0.6983190179 0.6983190179 + 0.6753988862 0.6991270781 0.6991270781 + 0.6762413979 0.6999309063 0.6999309063 + 0.6770828962 0.7007337213 0.7007337213 + 0.6779245138 0.7015364766 0.7015364766 + 0.6787626743 0.7023348808 0.7023348808 + 0.6795991063 0.7031309009 0.7031309009 + 0.6804355979 0.7039269209 0.7039269209 + 0.6812698841 0.7047196031 0.7047196031 + 0.6821017861 0.7055087090 0.7055087090 + 0.6829338074 0.7062976956 0.7062976956 + 0.6837641001 0.7070850730 0.7070850730 + 0.6845905781 0.7078680992 0.7078680992 + 0.6854171157 0.7086511850 0.7086511850 + 0.6862432957 0.7094339728 0.7094339728 + 0.6870644093 0.7102119923 0.7102119923 + 0.6878855824 0.7109900117 0.7109900117 + 0.6887066960 0.7117680907 0.7117680907 + 0.6895229220 0.7125418782 0.7125418782 + 0.6903378963 0.7133144736 0.7133144736 + 0.6911528707 0.7140871286 0.7140871286 + 0.6919643283 0.7148566246 0.7148566246 + 0.6927723289 0.7156230807 0.7156230807 + 0.6935803890 0.7163894773 0.7163894773 + 0.6943870187 0.7171549797 0.7171549797 + 0.6951882243 0.7179164886 0.7179164886 + 0.6959893703 0.7186779976 0.7186779976 + 0.6967905760 0.7194393873 0.7194393873 + 0.6975867748 0.7201958895 0.7201958895 + 0.6983821988 0.7209516168 0.7209516168 + 0.6991776228 0.7217072248 0.7217072248 + 0.6999700069 0.7224600911 0.7224600911 + 0.7007597089 0.7232102752 0.7232102752 + 0.7015494108 0.7239603996 0.7239603996 + 0.7023385167 0.7247099280 0.7247099280 + 0.7031233907 0.7254531980 0.7254531980 + 0.7039083838 0.7261965275 0.7261965275 + 0.7046933770 0.7269399166 0.7269399166 + 0.7054750919 0.7276784778 0.7276784778 + 0.7062553763 0.7284148932 0.7284148932 + 0.7070357203 0.7291513085 0.7291513085 + 0.7078148127 0.7298862934 0.7298862934 + 0.7085903287 0.7306169271 0.7306169271 + 0.7093657255 0.7313475013 0.7313475013 + 0.7101411223 0.7320780158 0.7320780158 + 0.7109115124 0.7328050733 0.7328050733 + 0.7116804719 0.7335309982 0.7335309982 + 0.7124493718 0.7342568040 0.7342568040 + 0.7132170200 0.7349814773 0.7349814773 + 0.7139806747 0.7357025146 0.7357025146 + 0.7147443891 0.7364236116 0.7364236116 + 0.7155081034 0.7371445894 0.7371445894 + 0.7162681222 0.7378603816 0.7378603816 + 0.7170267105 0.7385740280 0.7385740280 + 0.7177852988 0.7392876148 0.7392876148 + 0.7185432911 0.7400004268 0.7400004268 + 0.7192980051 0.7407081723 0.7407081723 + 0.7200527191 0.7414160967 0.7414160967 + 0.7208074927 0.7421239018 0.7421239018 + 0.7215601206 0.7428290248 0.7428290248 + 0.7223110795 0.7435317039 0.7435317039 + 0.7230620980 0.7442343831 0.7442343831 + 0.7238131166 0.7449371815 0.7449371815 + 0.7245600820 0.7456340194 0.7456340194 + 0.7253068089 0.7463302016 0.7463302016 + 0.7260534763 0.7470265031 0.7470265031 + 0.7267987728 0.7477208972 0.7477208972 + 0.7275406122 0.7484099865 0.7484099865 + 0.7282822728 0.7490991950 0.7490991950 + 0.7290241122 0.7497884035 0.7497884035 + 0.7297623158 0.7504742146 0.7504742146 + 0.7304981947 0.7511575818 0.7511575818 + 0.7312340140 0.7518410087 0.7518410087 + 0.7319697738 0.7525243759 0.7525243759 + 0.7327011228 0.7532038093 0.7532038093 + 0.7334318161 0.7538825870 0.7538825870 + 0.7341625094 0.7545614839 0.7545614839 + 0.7348924279 0.7552397847 0.7552397847 + 0.7356168032 0.7559139729 0.7559139729 + 0.7363412976 0.7565882802 0.7565882802 + 0.7370656729 0.7572625279 0.7572625279 + 0.7377883792 0.7579351068 0.7579351068 + 0.7385078073 0.7586042881 0.7586042881 + 0.7392271757 0.7592735291 0.7592735291 + 0.7399466038 0.7599428296 0.7599428296 + 0.7406626940 0.7606090903 0.7606090903 + 0.7413759828 0.7612727880 0.7612727880 + 0.7420892715 0.7619364262 0.7619364262 + 0.7428025007 0.7626001239 0.7626001239 + 0.7435113192 0.7632591724 0.7632591724 + 0.7442179918 0.7639161944 0.7639161944 + 0.7449247241 0.7645732164 0.7645732164 + 0.7456315160 0.7652301788 0.7652301788 + 0.7463331223 0.7658814788 0.7658814788 + 0.7470337152 0.7665314078 0.7665314078 + 0.7477343082 0.7671812773 0.7671812773 + 0.7484349012 0.7678310871 0.7678310871 + 0.7491310239 0.7684757710 0.7684757710 + 0.7498267293 0.7691197991 0.7691197991 + 0.7505223751 0.7697638273 0.7697638273 + 0.7512180805 0.7704077959 0.7704077959 + 0.7519093156 0.7710475922 0.7710475922 + 0.7526003718 0.7716872096 0.7716872096 + 0.7532914877 0.7723268270 0.7723268270 + 0.7539826035 0.7729663849 0.7729663849 + 0.7546687722 0.7736009955 0.7736009955 + 0.7553548813 0.7742356062 0.7742356062 + 0.7560411096 0.7748702168 0.7748702168 + 0.7567272782 0.7755048275 0.7755048275 + 0.7574092150 0.7761334777 0.7761334777 + 0.7580910921 0.7767620087 0.7767620087 + 0.7587730885 0.7773905993 0.7773905993 + 0.7594550252 0.7780190706 0.7780190706 + 0.7601323724 0.7786415219 0.7786415219 + 0.7608097196 0.7792634964 0.7792634964 + 0.7614868879 0.7798855901 0.7798855901 + 0.7621641159 0.7805076241 0.7805076241 + 0.7628363967 0.7811251283 0.7811251283 + 0.7635082006 0.7817419171 0.7817419171 + 0.7641798854 0.7823588252 0.7823588252 + 0.7648516297 0.7829756141 0.7829756141 + 0.7655196786 0.7835885286 0.7835885286 + 0.7661867738 0.7842003107 0.7842003107 + 0.7668539286 0.7848120928 0.7848120928 + 0.7675210238 0.7854238749 0.7854238749 + 0.7681856155 0.7860323191 0.7860323191 + 0.7688488960 0.7866386771 0.7866386771 + 0.7695121765 0.7872452140 0.7872452140 + 0.7701753974 0.7878515720 0.7878515720 + 0.7708364725 0.7884562016 0.7884562016 + 0.7714951038 0.7890586853 0.7890586853 + 0.7721537948 0.7896612287 0.7896612287 + 0.7728124261 0.7902637124 0.7902637124 + 0.7734698057 0.7908648252 0.7908648252 + 0.7741237879 0.7914618850 0.7914618850 + 0.7747778296 0.7920590043 0.7920590043 + 0.7754318118 0.7926561832 0.7926561832 + 0.7760856748 0.7932531834 0.7932531834 + 0.7767338753 0.7938435078 0.7938435078 + 0.7773820758 0.7944338918 0.7944338918 + 0.7780303955 0.7950242162 0.7950242162 + 0.7786785960 0.7956146002 0.7956146002 + 0.7793235779 0.7962008715 0.7962008715 + 0.7799674273 0.7967858911 0.7967858911 + 0.7806112766 0.7973707914 0.7973707914 + 0.7812551260 0.7979558110 0.7979558110 + 0.7818971872 0.7985386252 0.7985386252 + 0.7825371027 0.7991185188 0.7991185188 + 0.7831770182 0.7996984124 0.7996984124 + 0.7838168740 0.8002783060 0.8002783060 + 0.7844563127 0.8008577824 0.8008577824 + 0.7850909233 0.8014323711 0.8014323711 + 0.7857254744 0.8020070791 0.8020070791 + 0.7863600850 0.8025817871 0.8025817871 + 0.7869946957 0.8031563759 0.8031563759 + 0.7876250744 0.8037273288 0.8037273288 + 0.7882537842 0.8042966127 0.8042966127 + 0.7888823748 0.8048657775 0.8048657775 + 0.7895110250 0.8054350019 0.8054350019 + 0.7901381850 0.8060027957 0.8060027957 + 0.7907618284 0.8065670133 0.8065670133 + 0.7913854122 0.8071311116 0.8071311116 + 0.7920088768 0.8076952100 0.8076952100 + 0.7926325202 0.8082594275 0.8082594275 + 0.7932527065 0.8088204861 0.8088204861 + 0.7938721776 0.8093808889 0.8093808889 + 0.7944917083 0.8099412918 0.8099412918 + 0.7951111794 0.8105016947 0.8105016947 + 0.7957289815 0.8110610247 0.8110610247 + 0.7963436842 0.8116179109 0.8116179109 + 0.7969583869 0.8121749163 0.8121749163 + 0.7975730896 0.8127318025 0.8127318025 + 0.7981876731 0.8132886887 0.8132886887 + 0.7987986803 0.8138427734 0.8138427734 + 0.7994089127 0.8143960834 0.8143960834 + 0.8000190258 0.8149495125 0.8149495125 + 0.8006290793 0.8155028224 0.8155028224 + 0.8012378812 0.8160552979 0.8160552979 + 0.8018423915 0.8166047931 0.8166047931 + 0.8024469018 0.8171542883 0.8171542883 + 0.8030514717 0.8177037835 0.8177037835 + 0.8036559820 0.8182532787 0.8182532787 + 0.8042569160 0.8187999725 0.8187999725 + 0.8048557043 0.8193448782 0.8193448782 + 0.8054544926 0.8198897839 0.8198897839 + 0.8060532808 0.8204346895 0.8204346895 + 0.8066521287 0.8209795952 0.8209795952 + 0.8072462082 0.8215199113 0.8215199113 + 0.8078402877 0.8220599890 0.8220599890 + 0.8084343076 0.8226001859 0.8226001859 + 0.8090283275 0.8231403232 0.8231403232 + 0.8096209168 0.8236796260 0.8236796260 + 0.8102098703 0.8242164254 0.8242164254 + 0.8107990026 0.8247532248 0.8247532248 + 0.8113880157 0.8252900839 0.8252900839 + 0.8119770885 0.8258268833 0.8258268833 + 0.8125630021 0.8263612986 0.8263612986 + 0.8131465912 0.8268936872 0.8268936872 + 0.8137301803 0.8274261951 0.8274261951 + 0.8143138885 0.8279585838 0.8279585838 + 0.8148974776 0.8284909725 0.8284909725 + 0.8154776096 0.8290188909 0.8290188909 + 0.8160567880 0.8295456767 0.8295456767 + 0.8166360855 0.8300724030 0.8300724030 + 0.8172153234 0.8305991888 0.8305991888 + 0.8177946210 0.8311259151 0.8311259151 + 0.8183705211 0.8316484094 0.8316484094 + 0.8189464211 0.8321709037 0.8321709037 + 0.8195223212 0.8326933980 0.8326933980 + 0.8200982213 0.8332158923 0.8332158923 + 0.8206732869 0.8337377906 0.8337377906 + 0.8212442994 0.8342564106 0.8342564106 + 0.8218153119 0.8347749710 0.8347749710 + 0.8223863244 0.8352935910 0.8352935910 + 0.8229573965 0.8358122706 0.8358122706 + 0.8235266209 0.8363298774 0.8363298774 + 0.8240911961 0.8368446231 0.8368446231 + 0.8246557713 0.8373593092 0.8373593092 + 0.8252205253 0.8378741145 0.8378741145 + 0.8257851005 0.8383888006 0.8383888006 + 0.8263477087 0.8389021754 0.8389021754 + 0.8269069791 0.8394131064 0.8394131064 + 0.8274661899 0.8399239779 0.8399239779 + 0.8280254006 0.8404349089 0.8404349089 + 0.8285846114 0.8409457803 0.8409457803 + 0.8291420937 0.8414549232 0.8414549232 + 0.8296970129 0.8419613242 0.8419613242 + 0.8302518129 0.8424677253 0.8424677253 + 0.8308066726 0.8429740071 0.8429740071 + 0.8313614726 0.8434804082 0.8434804082 + 0.8319147229 0.8439850211 0.8439850211 + 0.8324651122 0.8444867730 0.8444867730 + 0.8330155015 0.8449885845 0.8449885845 + 0.8335660100 0.8454903960 0.8454903960 + 0.8341163993 0.8459923267 0.8459923267 + 0.8346651196 0.8464931250 0.8464931250 + 0.8352108002 0.8469917774 0.8469917774 + 0.8357564807 0.8474906087 0.8474906087 + 0.8363021016 0.8479893804 0.8479893804 + 0.8368477821 0.8484880924 0.8484880924 + 0.8373925090 0.8489859104 0.8489859104 + 0.8379346132 0.8494806290 0.8494806290 + 0.8384767175 0.8499752879 0.8499752879 + 0.8390188217 0.8504700065 0.8504700065 + 0.8395608068 0.8509647250 0.8509647250 + 0.8401023746 0.8514587879 0.8514587879 + 0.8406404257 0.8519484997 0.8519484997 + 0.8411784768 0.8524380922 0.8524380922 + 0.8417165279 0.8529276848 0.8529276848 + 0.8422545791 0.8534172773 0.8534172773 + 0.8427925706 0.8539069295 0.8539069295 + 0.8433259130 0.8543931246 0.8543931246 + 0.8438590169 0.8548790812 0.8548790812 + 0.8443921208 0.8553649783 0.8553649783 + 0.8449252248 0.8558509946 0.8558509946 + 0.8454583287 0.8563370109 0.8563370109 + 0.8459873796 0.8568208218 0.8568208218 + 0.8465151787 0.8573036194 0.8573036194 + 0.8470429182 0.8577864766 0.8577864766 + 0.8475707173 0.8582693934 0.8582693934 + 0.8480983973 0.8587523103 0.8587523103 + 0.8486241102 0.8592330217 0.8592330217 + 0.8491476178 0.8597115278 0.8597115278 + 0.8496711254 0.8601899147 0.8601899147 + 0.8501945734 0.8606684208 0.8606684208 + 0.8507180810 0.8611468077 0.8611468077 + 0.8512408137 0.8616245985 0.8616245985 + 0.8517602086 0.8620989919 0.8620989919 + 0.8522796035 0.8625733852 0.8625733852 + 0.8527989984 0.8630477786 0.8630477786 + 0.8533183932 0.8635221720 0.8635221720 + 0.8538377881 0.8639966249 0.8639966249 + 0.8543522954 0.8644680977 0.8644680977 + 0.8548657894 0.8649389744 0.8649389744 + 0.8553792834 0.8654097915 0.8654097915 + 0.8558928967 0.8658807278 0.8658807278 + 0.8564063907 0.8663516045 0.8663516045 + 0.8569173813 0.8668203950 0.8668203950 + 0.8574250937 0.8672863841 0.8672863841 + 0.8579329252 0.8677524924 0.8677524924 + 0.8584406972 0.8682186007 0.8682186007 + 0.8589485288 0.8686845899 0.8686845899 + 0.8594563007 0.8691506982 0.8691506982 + 0.8599594831 0.8696123958 0.8696123958 + 0.8604626060 0.8700739741 0.8700739741 + 0.8609657884 0.8705356121 0.8705356121 + 0.8614689708 0.8709973097 0.8709973097 + 0.8619722128 0.8714588881 0.8714588881 + 0.8624734879 0.8719184995 0.8719184995 + 0.8629732132 0.8723762035 0.8723762035 + 0.8634728193 0.8728339076 0.8728339076 + 0.8639724255 0.8732916117 0.8732916117 + 0.8644720912 0.8737491965 0.8737491965 + 0.8649716973 0.8742069006 0.8742069006 + 0.8654679060 0.8746609092 0.8746609092 + 0.8659641147 0.8751147985 0.8751147985 + 0.8664603233 0.8755686283 0.8755686283 + 0.8669564128 0.8760225177 0.8760225177 + 0.8674526215 0.8764764071 0.8764764071 + 0.8679472208 0.8769292831 0.8769292831 + 0.8684396744 0.8773809075 0.8773809075 + 0.8689323068 0.8778324723 0.8778324723 + 0.8694248199 0.8782839775 0.8782839775 + 0.8699172735 0.8787356019 0.8787356019 + 0.8704097867 0.8791871071 0.8791871071 + 0.8709002137 0.8796362281 0.8796362281 + 0.8713902235 0.8800846934 0.8800846934 + 0.8718801737 0.8805330992 0.8805330992 + 0.8723701835 0.8809816241 0.8809816241 + 0.8728601933 0.8814300895 0.8814300895 + 0.8733496070 0.8818780184 0.8818780184 + 0.8738359213 0.8823224902 0.8823224902 + 0.8743221760 0.8827670217 0.8827670217 + 0.8748083711 0.8832114935 0.8832114935 + 0.8752946854 0.8836559057 0.8836559057 + 0.8757808805 0.8841003776 0.8841003776 + 0.8762651086 0.8845438957 0.8845438957 + 0.8767464757 0.8849858046 0.8849858046 + 0.8772279024 0.8854277730 0.8854277730 + 0.8777093291 0.8858698010 0.8858698010 + 0.8781906962 0.8863118291 0.8863118291 + 0.8786721230 0.8867536783 0.8867536783 + 0.8791509867 0.8871935010 0.8871935010 + 0.8796287179 0.8876323104 0.8876323104 + 0.8801062703 0.8880710006 0.8880710006 + 0.8805840015 0.8885096908 0.8885096908 + 0.8810616732 0.8889483809 0.8889483809 + 0.8815392852 0.8893870711 0.8893870711 + 0.8820139766 0.8898223042 0.8898223042 + 0.8824881911 0.8902568817 0.8902568817 + 0.8829622865 0.8906915188 0.8906915188 + 0.8834365010 0.8911259770 0.8911259770 + 0.8839107156 0.8915606141 0.8915606141 + 0.8843848109 0.8919951916 0.8919951916 + 0.8848553896 0.8924270868 0.8924270868 + 0.8853260279 0.8928589821 0.8928589821 + 0.8857964873 0.8932908773 0.8932908773 + 0.8862671256 0.8937227726 0.8937227726 + 0.8867377043 0.8941547275 0.8941547275 + 0.8872079253 0.8945863247 0.8945863247 + 0.8876746893 0.8950147033 0.8950147033 + 0.8881413937 0.8954432011 0.8954432011 + 0.8886082172 0.8958716989 0.8958716989 + 0.8890749812 0.8963001966 0.8963001966 + 0.8895418048 0.8967286944 0.8967286944 + 0.8900079131 0.8971564770 0.8971564770 + 0.8904703259 0.8975806832 0.8975806832 + 0.8909326196 0.8980048895 0.8980048895 + 0.8913949728 0.8984290957 0.8984290957 + 0.8918573856 0.8988533020 0.8988533020 + 0.8923196793 0.8992775083 0.8992775083 + 0.8927813768 0.8997012973 0.8997012973 + 0.8932392001 0.9001227021 0.9001227021 + 0.8936970234 0.9005439878 0.9005439878 + 0.8941549063 0.9009653926 0.9009653926 + 0.8946127295 0.9013866782 0.9013866782 + 0.8950704932 0.9018080831 0.9018080831 + 0.8955280185 0.9022290707 0.9022290707 + 0.8959823847 0.9026468992 0.9026468992 + 0.8964369297 0.9030647278 0.9030647278 + 0.8968912959 0.9034824967 0.9034824967 + 0.8973457813 0.9039003253 0.9039003253 + 0.8978002071 0.9043180943 0.9043180943 + 0.8982545733 0.9047358036 0.9047358036 + 0.8987048864 0.9051483870 0.9051483870 + 0.8991550803 0.9055609107 0.9055609107 + 0.8996052742 0.9059733152 0.9059733152 + 0.9000555277 0.9063857794 0.9063857794 + 0.9005057216 0.9067983031 0.9067983031 + 0.9009559155 0.9072108269 0.9072108269 + 0.9014015794 0.9076197743 0.9076197743 + 0.9018465281 0.9080280066 0.9080280066 + 0.9022914171 0.9084362984 0.9084362984 + 0.9027363062 0.9088445902 0.9088445902 + 0.9031810760 0.9092528820 0.9092528820 + 0.9036260247 0.9096611738 0.9096611738 + 0.9040681720 0.9100679755 0.9100679755 + 0.9045090079 0.9104740024 0.9104740024 + 0.9049497247 0.9108799100 0.9108799100 + 0.9053903818 0.9112858772 0.9112858772 + 0.9058312178 0.9116917849 0.9116917849 + 0.9062718749 0.9120978117 0.9120978117 + 0.9067112207 0.9125025868 0.9125025868 + 0.9071481824 0.9129055142 0.9129055142 + 0.9075852036 0.9133083820 0.9133083820 + 0.9080221057 0.9137113094 0.9137113094 + 0.9084591269 0.9141142964 0.9141142964 + 0.9088960886 0.9145172238 0.9145172238 + 0.9093328118 0.9149199128 0.9149199128 + 0.9097660184 0.9153187871 0.9153187871 + 0.9101991057 0.9157177210 0.9157177210 + 0.9106323123 0.9161165953 0.9161165953 + 0.9110653996 0.9165155292 0.9165155292 + 0.9114986062 0.9169144034 0.9169144034 + 0.9119318128 0.9173132777 0.9173132777 + 0.9123622179 0.9177098274 0.9177098274 + 0.9127916098 0.9181053042 0.9181053042 + 0.9132210016 0.9185007811 0.9185007811 + 0.9136503935 0.9188963175 0.9188963175 + 0.9140797853 0.9192917943 0.9192917943 + 0.9145091772 0.9196872711 0.9196872711 + 0.9149373174 0.9200819731 0.9200819731 + 0.9153624177 0.9204747081 0.9204747081 + 0.9157875180 0.9208673835 0.9208673835 + 0.9162126184 0.9212601781 0.9212601781 + 0.9166377187 0.9216529131 0.9216529131 + 0.9170628190 0.9220455885 0.9220455885 + 0.9174879193 0.9224383831 0.9224383831 + 0.9179093242 0.9228280187 0.9228280187 + 0.9183300734 0.9232169986 0.9232169986 + 0.9187508225 0.9236060977 0.9236060977 + 0.9191715717 0.9239950776 0.9239950776 + 0.9195923209 0.9243841171 0.9243841171 + 0.9200131297 0.9247730970 0.9247730970 + 0.9204329252 0.9251611829 0.9251611829 + 0.9208505154 0.9255470037 0.9255470037 + 0.9212682247 0.9259328842 0.9259328842 + 0.9216858745 0.9263188243 0.9263188243 + 0.9221035838 0.9267047048 0.9267047048 + 0.9225212932 0.9270905852 0.9270905852 + 0.9229388833 0.9274765253 0.9274765253 + 0.9233536720 0.9278604984 0.9278604984 + 0.9237673879 0.9282438755 0.9282438755 + 0.9241812229 0.9286273122 0.9286273122 + 0.9245948792 0.9290106893 0.9290106893 + 0.9250087142 0.9293940067 0.9293940067 + 0.9254223704 0.9297773838 0.9297773838 + 0.9258356094 0.9301604033 0.9301604033 + 0.9262446165 0.9305403829 0.9305403829 + 0.9266535044 0.9309204221 0.9309204221 + 0.9270625114 0.9313002825 0.9313002825 + 0.9274715185 0.9316803217 0.9316803217 + 0.9278804064 0.9320603013 0.9320603013 + 0.9282894135 0.9324402213 0.9324402213 + 0.9286968112 0.9328190088 0.9328190088 + 0.9291023016 0.9331961274 0.9331961274 + 0.9295076728 0.9335731864 0.9335731864 + 0.9299131036 0.9339503050 0.9339503050 + 0.9303184748 0.9343274236 0.9343274236 + 0.9307240248 0.9347046018 0.9347046018 + 0.9311293960 0.9350817204 0.9350817204 + 0.9315326810 0.9354572296 0.9354572296 + 0.9319350123 0.9358320832 0.9358320832 + 0.9323372841 0.9362069964 0.9362069964 + 0.9327396154 0.9365817904 0.9365817904 + 0.9331418872 0.9369567037 0.9369567037 + 0.9335442781 0.9373314977 0.9373314977 + 0.9339466095 0.9377064109 0.9377064109 + 0.9343459010 0.9380785227 0.9380785227 + 0.9347448945 0.9384502172 0.9384502172 + 0.9351438880 0.9388219118 0.9388219118 + 0.9355428815 0.9391937256 0.9391937256 + 0.9359418750 0.9395654202 0.9395654202 + 0.9363409281 0.9399371147 0.9399371147 + 0.9367398024 0.9403086901 0.9403086901 + 0.9371359944 0.9406778812 0.9406778812 + 0.9375321865 0.9410470128 0.9410470128 + 0.9379284978 0.9414162040 0.9414162040 + 0.9383246899 0.9417852759 0.9417852759 + 0.9387210011 0.9421545267 0.9421545267 + 0.9391171932 0.9425235987 0.9425235987 + 0.9395130277 0.9428924918 0.9428924918 + 0.9399057031 0.9432595968 0.9432595968 + 0.9402983189 0.9436268210 0.9436268210 + 0.9406909943 0.9439939260 0.9439939260 + 0.9410837293 0.9443609715 0.9443609715 + 0.9414764047 0.9447280765 0.9447280765 + 0.9418690205 0.9450953007 0.9450953007 + 0.9422609806 0.9454616904 0.9454616904 + 0.9426497221 0.9458243847 0.9458243847 + 0.9430382848 0.9461871982 0.9461871982 + 0.9434270263 0.9465500116 0.9465500116 + 0.9438155890 0.9469127059 0.9469127059 + 0.9442042708 0.9472755194 0.9472755194 + 0.9445928931 0.9476382732 0.9476382732 + 0.9449806213 0.9480000734 0.9480000734 + 0.9453629851 0.9483562708 0.9483562708 + 0.9457452893 0.9487125278 0.9487125278 + 0.9461275935 0.9490686059 0.9490686059 + 0.9465098977 0.9494248033 0.9494248033 + 0.9468922019 0.9497810006 0.9497810006 + 0.9472745061 0.9501371980 0.9501371980 + 0.9476562738 0.9504929781 0.9504929781 + 0.9480308890 0.9508436918 0.9508436918 + 0.9484055042 0.9511944056 0.9511944056 + 0.9487801194 0.9515451193 0.9515451193 + 0.9491547942 0.9518957734 0.9518957734 + 0.9495294094 0.9522464871 0.9522464871 + 0.9499040246 0.9525972009 0.9525972009 + 0.9502785802 0.9529479146 0.9529479146 + 0.9506484270 0.9532948732 0.9532948732 + 0.9510179162 0.9536415935 0.9536415935 + 0.9513872862 0.9539883733 0.9539883733 + 0.9517567158 0.9543350935 0.9543350935 + 0.9521262050 0.9546818137 0.9546818137 + 0.9524955750 0.9550285935 0.9550285935 + 0.9528651237 0.9553753138 0.9553753138 + 0.9532322884 0.9557194710 0.9557194710 + 0.9535986781 0.9560627937 0.9560627937 + 0.9539651275 0.9564061165 0.9564061165 + 0.9543313980 0.9567493796 0.9567493796 + 0.9546977878 0.9570927024 0.9570927024 + 0.9550641775 0.9574360251 0.9574360251 + 0.9554306269 0.9577792883 0.9577792883 + 0.9557952285 0.9581211805 0.9581211805 + 0.9561576843 0.9584614038 0.9584614038 + 0.9565201998 0.9588016272 0.9588016272 + 0.9568827152 0.9591417909 0.9591417909 + 0.9572451711 0.9594820142 0.9594820142 + 0.9576076865 0.9598221183 0.9598221183 + 0.9579702020 0.9601622820 0.9601622820 + 0.9583321214 0.9605020285 0.9605020285 + 0.9586902261 0.9608384967 0.9608384967 + 0.9590482712 0.9611749053 0.9611749053 + 0.9594063163 0.9615113735 0.9615113735 + 0.9597644210 0.9618479013 0.9618479013 + 0.9601225257 0.9621844292 0.9621844292 + 0.9604805708 0.9625207782 0.9625207782 + 0.9608386755 0.9628573060 0.9628573060 + 0.9611936212 0.9631906748 0.9631906748 + 0.9615474939 0.9635230899 0.9635230899 + 0.9619014263 0.9638555050 0.9638555050 + 0.9622552991 0.9641879201 0.9641879201 + 0.9626091719 0.9645202756 0.9645202756 + 0.9629632235 0.9648526907 0.9648526907 + 0.9633170962 0.9651851058 0.9651851058 + 0.9636694193 0.9655163288 0.9655163288 + 0.9640185833 0.9658449292 0.9658449292 + 0.9643678069 0.9661735892 0.9661735892 + 0.9647169709 0.9665021896 0.9665021896 + 0.9650661945 0.9668309093 0.9668309093 + 0.9654154181 0.9671595097 0.9671595097 + 0.9657645822 0.9674882293 0.9674882293 + 0.9661138058 0.9678168297 0.9678168297 + 0.9664589763 0.9681419134 0.9681419134 + 0.9668033123 0.9684662819 0.9684662819 + 0.9671475887 0.9687907100 0.9687907100 + 0.9674918056 0.9691150784 0.9691150784 + 0.9678360820 0.9694393873 0.9694393873 + 0.9681804180 0.9697638154 0.9697638154 + 0.9685246944 0.9700881839 0.9700881839 + 0.9688677788 0.9704114199 0.9704114199 + 0.9692075849 0.9707313180 0.9707313180 + 0.9695475101 0.9710512161 0.9710512161 + 0.9698873162 0.9713711143 0.9713711143 + 0.9702271223 0.9716910124 0.9716910124 + 0.9705669284 0.9720107913 0.9720107913 + 0.9709066749 0.9723306894 0.9723306894 + 0.9712464809 0.9726505876 0.9726505876 + 0.9715837240 0.9729676843 0.9729676843 + 0.9719191790 0.9732831717 0.9732831717 + 0.9722548127 0.9735987186 0.9735987186 + 0.9725903869 0.9739142060 0.9739142060 + 0.9729259014 0.9742296934 0.9742296934 + 0.9732614756 0.9745451808 0.9745451808 + 0.9735971093 0.9748606086 0.9748606086 + 0.9739326239 0.9751760960 0.9751760960 + 0.9742640257 0.9754871130 0.9754871130 + 0.9745951295 0.9757977724 0.9757977724 + 0.9749261737 0.9761084914 0.9761084914 + 0.9752572775 0.9764190912 0.9764190912 + 0.9755883813 0.9767298102 0.9767298102 + 0.9759194851 0.9770405293 0.9770405293 + 0.9762505889 0.9773511291 0.9773511291 + 0.9765807986 0.9776608944 0.9776608944 + 0.9769067764 0.9779667258 0.9779667258 + 0.9772328138 0.9782724977 0.9782724977 + 0.9775589108 0.9785783291 0.9785783291 + 0.9778848886 0.9788841009 0.9788841009 + 0.9782109261 0.9791898727 0.9791898727 + 0.9785370231 0.9794957042 0.9794957042 + 0.9788630009 0.9798014164 0.9798014164 + 0.9791870713 0.9801052809 0.9801052809 + 0.9795076847 0.9804059267 0.9804059267 + 0.9798284173 0.9807065129 0.9807065129 + 0.9801490903 0.9810069799 0.9810069799 + 0.9804697037 0.9813076258 0.9813076258 + 0.9807903767 0.9816082120 0.9816082120 + 0.9811111093 0.9819087982 0.9819087982 + 0.9814317226 0.9822093248 0.9822093248 + 0.9817494154 0.9825072885 0.9825072885 + 0.9820637703 0.9828022718 0.9828022718 + 0.9823781848 0.9830973148 0.9830973148 + 0.9826925993 0.9833922982 0.9833922982 + 0.9830070138 0.9836874008 0.9836874008 + 0.9833214283 0.9839823842 0.9839823842 + 0.9836357236 0.9842774272 0.9842774272 + 0.9839500785 0.9845724106 0.9845724106 + 0.9842607975 0.9848645926 0.9848645926 + 0.9845681787 0.9851542115 0.9851542115 + 0.9848756194 0.9854437709 0.9854437709 + 0.9851830006 0.9857333899 0.9857333899 + 0.9854903817 0.9860230088 0.9860230088 + 0.9857978225 0.9863126874 0.9863126874 + 0.9861052036 0.9866023064 0.9866023064 + 0.9864125848 0.9868919253 0.9868919253 + 0.9867162704 0.9871780872 0.9871780872 + 0.9870163798 0.9874610901 0.9874610901 + 0.9873164892 0.9877440929 0.9877440929 + 0.9876165986 0.9880272150 0.9880272150 + 0.9879167080 0.9883102179 0.9883102179 + 0.9882168174 0.9885932207 0.9885932207 + 0.9885169268 0.9888762236 0.9888762236 + 0.9888169765 0.9891592264 0.9891592264 + 0.9891130924 0.9894391894 0.9894391894 + 0.9894037247 0.9897152781 0.9897152781 + 0.9896944165 0.9899914265 0.9899914265 + 0.9899849892 0.9902673960 0.9902673960 + 0.9902756810 0.9905434847 0.9905434847 + 0.9905663133 0.9908195734 0.9908195734 + 0.9908568859 0.9910956025 0.9910956025 + 0.9911475778 0.9913716912 0.9913716912 + 0.9914352894 0.9916458726 0.9916458726 + 0.9917153716 0.9919152260 0.9919152260 + 0.9919955134 0.9921845198 0.9921845198 + 0.9922757149 0.9924538136 0.9924538136 + 0.9925557971 0.9927231073 0.9927231073 + 0.9928358793 0.9929924011 0.9929924011 + 0.9931160808 0.9932618141 0.9932618141 + 0.9933962226 0.9935311079 0.9935311079 + 0.9936755896 0.9937998056 0.9937998056 + 0.9939448833 0.9940592051 0.9940592051 + 0.9942141175 0.9943186045 0.9943186045 + 0.9944832921 0.9945781231 0.9945781231 + 0.9947525263 0.9948375225 0.9948375225 + 0.9950217009 0.9950969815 0.9950969815 + 0.9952909946 0.9953563809 0.9953563809 + 0.9955602288 0.9956158996 0.9956158996 + 0.9958294034 0.9958752990 0.9958752990 + 0.9960865974 0.9961261749 0.9961261749 + 0.9963403940 0.9963746071 0.9963746071 + 0.9965941906 0.9966229796 0.9966229796 + 0.9968479276 0.9968714118 0.9968714118 + 0.9971017241 0.9971197844 0.9971197844 + 0.9973555207 0.9973682165 0.9973682165 + 0.9976093173 0.9976165891 0.9976165891 + 0.9978629947 0.9978650212 0.9978650212 + 0.9981094003 0.9981083274 0.9981083274 + 0.9983456731 0.9983447790 0.9983447790 + 0.9985821247 0.9985812902 0.9985812902 + 0.9988183975 0.9988176823 0.9988176823 + 0.9990546703 0.9990541935 0.9990541935 + 0.9992910028 0.9992905855 0.9992905855 + 0.9995273948 0.9995270967 0.9995270967 + 0.9997637272 0.9997634888 0.9997634888 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_ultra_050.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_ultra_050.spi1d index bbfcdf7c..8a95948e 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_ultra_050.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_ultra_050.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0261295997 0.0275282897 0.0193712804 - 0.0436814092 0.0463444404 0.0334131084 - 0.0581157207 0.0624009110 0.0459993482 - 0.0707884133 0.0767784268 0.0574880093 - 0.0822683722 0.0900187567 0.0680924430 - 0.0928523093 0.1023626998 0.0779986978 - 0.1027420014 0.1139826998 0.0873259678 - 0.1121006012 0.1249416992 0.0962512568 - 0.1209674031 0.1352714002 0.1049458012 - 0.1293299943 0.1450500935 0.1134937033 - 0.1372125000 0.1543603987 0.1219277978 - 0.1446518004 0.1632550061 0.1302694976 - 0.1517110020 0.1717393994 0.1385125965 - 0.1584471017 0.1798606068 0.1466525048 - 0.1648889929 0.1876538992 0.1546880007 - 0.1710644960 0.1951581985 0.1626233011 - 0.1769893020 0.2023999989 0.1704642028 - 0.1826931983 0.2093926072 0.1781909019 - 0.1881920993 0.2161574960 0.1858115941 - 0.1935151964 0.2226887047 0.1933328062 - 0.1986732930 0.2290194929 0.2007412016 - 0.2036796063 0.2351579070 0.2080101967 - 0.2085503936 0.2411172986 0.2151511014 - 0.2132862955 0.2469058037 0.2221754044 - 0.2178914994 0.2525430918 0.2290547043 - 0.2223861068 0.2580389082 0.2357912064 - 0.2267823070 0.2634046972 0.2423840016 - 0.2310760021 0.2686296105 0.2488331944 - 0.2352629006 0.2737227976 0.2551515996 - 0.2393413931 0.2787044942 0.2613373995 - 0.2433319986 0.2835777998 0.2673675120 - 0.2472561002 0.2883430123 0.2732523978 - 0.2511121929 0.2930105031 0.2790116072 - 0.2549034059 0.2975909114 0.2846449912 - 0.2586241066 0.3020789027 0.2901515961 - 0.2622672915 0.3064748049 0.2955358922 - 0.2658410072 0.3107773066 0.3008053899 - 0.2693549097 0.3150070012 0.3059557974 - 0.2728112042 0.3191769123 0.3109906018 - 0.2762080133 0.3232814074 0.3159134090 - 0.2795439959 0.3273217976 0.3207265139 - 0.2828184962 0.3312951028 0.3254382014 - 0.2860270143 0.3351995945 0.3300522864 - 0.2891789079 0.3390446007 0.3345699012 - 0.2922790051 0.3428247869 0.3389827907 - 0.2953406870 0.3465405107 0.3433066905 - 0.2983652055 0.3501957953 0.3475450873 - 0.3013558090 0.3538022935 0.3517045081 - 0.3043130934 0.3573650122 0.3557837009 - 0.3072336018 0.3608897924 0.3597820997 - 0.3101125956 0.3643819094 0.3637022078 - 0.3129473925 0.3678374887 0.3675518930 - 0.3157351017 0.3712525070 0.3713383079 - 0.3184793890 0.3746286929 0.3750616014 - 0.3211834133 0.3779700100 0.3787159920 - 0.3238503933 0.3812797964 0.3823012114 - 0.3264833987 0.3845624030 0.3858228922 - 0.3290852010 0.3878163993 0.3892802894 - 0.3316572905 0.3910405934 0.3926731944 - 0.3341959119 0.3942339122 0.3960165083 - 0.3367032111 0.3973953128 0.3993082047 - 0.3391802907 0.4005222023 0.4025487006 - 0.3416261971 0.4036155939 0.4057467878 - 0.3440423906 0.4066751897 0.4088996053 - 0.3464320898 0.4097003937 0.4120030999 - 0.3487968147 0.4126878977 0.4150581956 - 0.3511416912 0.4156408012 0.4180706143 - 0.3534660041 0.4185580909 0.4210394919 - 0.3557670116 0.4214420021 0.4239597917 - 0.3580501974 0.4243034124 0.4268440902 - 0.3603110015 0.4271419048 0.4296903014 - 0.3625530899 0.4299592078 0.4325061142 - 0.3647739887 0.4327593148 0.4352906942 - 0.3669759929 0.4355489016 0.4380438924 - 0.3691574037 0.4383288026 0.4407683015 - 0.3713135123 0.4410988092 0.4434654117 - 0.3734453022 0.4438571036 0.4461325109 - 0.3755545914 0.4466049075 0.4487735927 - 0.3776372969 0.4493390918 0.4513854980 - 0.3797005117 0.4520614147 0.4539698958 - 0.3817447126 0.4547702968 0.4565226138 - 0.3837673068 0.4574646056 0.4590449929 - 0.3857727945 0.4601452947 0.4615412056 - 0.3877620995 0.4628106952 0.4640046060 - 0.3897323012 0.4654597938 0.4664371908 - 0.3916867077 0.4680936038 0.4688445032 - 0.3936245143 0.4707117081 0.4712260067 - 0.3955442011 0.4733099043 0.4735803902 - 0.3974477053 0.4758862853 0.4759103060 - 0.3993369937 0.4784410894 0.4782153070 - 0.4012120068 0.4809744954 0.4804973900 - 0.4030661881 0.4834777117 0.4827564061 - 0.4049043059 0.4859539866 0.4849967062 - 0.4067262113 0.4884032905 0.4872167110 - 0.4085322917 0.4908267856 0.4894092977 - 0.4103249907 0.4932255149 0.4915755093 - 0.4121057093 0.4955982864 0.4937163889 - 0.4138723910 0.4979467094 0.4958330989 - 0.4156245887 0.5002682805 0.4979225099 - 0.4173628986 0.5025631189 0.4999859035 - 0.4190871119 0.5048316121 0.5020250082 - 0.4207980931 0.5070779920 0.5040426254 - 0.4224953055 0.5093029141 0.5060408115 - 0.4241782129 0.5115067959 0.5080217719 - 0.4258452058 0.5136871934 0.5099846721 - 0.4274975955 0.5158441067 0.5119276047 - 0.4291372001 0.5179803967 0.5138525963 - 0.4307664931 0.5200970173 0.5157613158 - 0.4323846996 0.5221933722 0.5176572204 - 0.4339928925 0.5242717862 0.5195353031 - 0.4355897009 0.5263323784 0.5213940144 - 0.4371742010 0.5283774137 0.5232350826 - 0.4387458861 0.5304070115 0.5250611901 - 0.4403051138 0.5324196219 0.5268712044 - 0.4418540895 0.5344129205 0.5286645889 - 0.4433917105 0.5363882184 0.5304409266 - 0.4449174106 0.5383464098 0.5322008729 - 0.4464308918 0.5402891040 0.5339469910 - 0.4479325116 0.5422176123 0.5356794000 - 0.4494239092 0.5441330075 0.5373972058 - 0.4509055912 0.5460358262 0.5391005874 - 0.4523760974 0.5479230881 0.5407894254 - 0.4538336098 0.5497955084 0.5424653888 - 0.4552780986 0.5516521931 0.5441265702 - 0.4567125142 0.5534964204 0.5457723737 - 0.4581381083 0.5553289056 0.5474044085 - 0.4595550001 0.5571491718 0.5490232706 - 0.4609611034 0.5589544773 0.5506296158 - 0.4623560905 0.5607442260 0.5522220135 - 0.4637416899 0.5625196099 0.5537992120 - 0.4651190937 0.5642817020 0.5553634167 - 0.4664886892 0.5660303235 0.5569143295 - 0.4678488970 0.5677667856 0.5584530830 - 0.4691990018 0.5694890022 0.5599805117 - 0.4705401957 0.5711963773 0.5614979267 - 0.4718737006 0.5728896260 0.5630066991 - 0.4731974900 0.5745667219 0.5645061731 - 0.4745135009 0.5762310028 0.5659909844 - 0.4758217931 0.5778843760 0.5674638748 - 0.4771224856 0.5795271993 0.5689262748 - 0.4784156084 0.5811589956 0.5703796744 - 0.4796993136 0.5827751756 0.5718243718 - 0.4809752107 0.5843808055 0.5732576847 - 0.4822460115 0.5859761834 0.5746794939 - 0.4835121036 0.5875607729 0.5760902762 - 0.4847720861 0.5891314745 0.5774900913 - 0.4860242903 0.5906872153 0.5788798928 - 0.4872705042 0.5922328234 0.5802617073 - 0.4885112047 0.5937690735 0.5816357136 - 0.4897463918 0.5952950716 0.5830007195 - 0.4909757972 0.5968127251 0.5843585134 - 0.4922001958 0.5983219147 0.5857095718 - 0.4934197068 0.5998219848 0.5870532990 - 0.4946334958 0.6013069153 0.5883836150 - 0.4958420992 0.6027836800 0.5897060037 - 0.4970462918 0.6042528749 0.5910214186 - 0.4982447922 0.6057118773 0.5923281908 - 0.4994386137 0.6071583033 0.5936253071 - 0.5006300211 0.6085960865 0.5949164033 - 0.5018193126 0.6100246906 0.5962014794 - 0.5030052066 0.6114413142 0.5974758863 - 0.5041871071 0.6128522754 0.5987439752 - 0.5053647161 0.6142578721 0.6000061035 - 0.5065345168 0.6156520247 0.6012579203 - 0.5077012181 0.6170390844 0.6025043726 - 0.5088649988 0.6184194088 0.6037459970 - 0.5100249052 0.6197851896 0.6049779058 - 0.5111827254 0.6211447716 0.6062039733 - 0.5123385787 0.6224983931 0.6074249148 - 0.5134863853 0.6238390207 0.6086357832 - 0.5146304965 0.6251735091 0.6098436117 - 0.5157698989 0.6265006065 0.6110476255 - 0.5169038177 0.6278185248 0.6122431159 - 0.5180357099 0.6291317940 0.6134337187 - 0.5191645026 0.6304373145 0.6146162748 - 0.5202901959 0.6317332983 0.6157922149 - 0.5214136243 0.6330240965 0.6169643998 - 0.5225296021 0.6343023181 0.6181287766 - 0.5236412883 0.6355733275 0.6192877889 - 0.5247489214 0.6368368864 0.6204416752 - 0.5258507133 0.6380882263 0.6215860248 - 0.5269507170 0.6393350959 0.6227265000 - 0.5280470252 0.6405732036 0.6238588095 - 0.5291407108 0.6418051720 0.6249852777 - 0.5302323103 0.6430323720 0.6261076927 - 0.5313162208 0.6442492008 0.6272221208 - 0.5323973894 0.6454623938 0.6283330917 - 0.5334717035 0.6466655731 0.6294345856 - 0.5345426798 0.6478630900 0.6305302978 - 0.5356101990 0.6490542889 0.6316205263 - 0.5366739035 0.6502388120 0.6327043772 - 0.5377362967 0.6514207721 0.6337860823 - 0.5387936831 0.6525934935 0.6348581910 - 0.5398488045 0.6537632942 0.6359266043 - 0.5408974290 0.6549249887 0.6369863153 - 0.5419415236 0.6560810804 0.6380411983 - 0.5429813266 0.6572313905 0.6390917897 - 0.5440160036 0.6583732963 0.6401355863 - 0.5450487137 0.6595116258 0.6411771178 - 0.5460764766 0.6606398225 0.6422092915 - 0.5471035242 0.6617661715 0.6432397962 - 0.5481255054 0.6628851295 0.6442614794 - 0.5491452217 0.6640012264 0.6452804804 - 0.5501583219 0.6651095748 0.6462923288 - 0.5511676073 0.6662127972 0.6473004222 - 0.5521715879 0.6673082113 0.6483036280 - 0.5531716943 0.6683982015 0.6493026018 - 0.5541684031 0.6694834828 0.6502984166 - 0.5551614761 0.6705622077 0.6512870789 - 0.5561527014 0.6716372967 0.6522722840 - 0.5571410060 0.6727038026 0.6532505155 - 0.5581284165 0.6737673879 0.6542266011 - 0.5591105223 0.6748213768 0.6551975012 - 0.5600913763 0.6758732796 0.6561676264 - 0.5610644817 0.6769148111 0.6571305990 - 0.5620365143 0.6779549718 0.6580929756 - 0.5630018115 0.6789849997 0.6590462923 - 0.5639665723 0.6800143123 0.6599991918 - 0.5649260283 0.6810361147 0.6609457731 - 0.5658853054 0.6820576191 0.6618922949 - 0.5668407083 0.6830697060 0.6628310084 - 0.5677961111 0.6840814948 0.6637696028 - 0.5687469244 0.6850817800 0.6646987200 - 0.5696974993 0.6860815883 0.6656274796 - 0.5706422925 0.6870731711 0.6665499806 - 0.5715867281 0.6880642176 0.6674721837 - 0.5725247860 0.6890473962 0.6683868766 - 0.5734621882 0.6900296211 0.6693009138 - 0.5743951201 0.6910039783 0.6702064872 - 0.5753269792 0.6919766068 0.6711105704 - 0.5762560964 0.6929423809 0.6720092893 - 0.5771843195 0.6939054728 0.6729062796 - 0.5781095028 0.6948626041 0.6737989187 - 0.5790330768 0.6958160996 0.6746891737 - 0.5799536109 0.6967654824 0.6755756736 - 0.5808709860 0.6977106929 0.6764587164 - 0.5817859769 0.6986532807 0.6773390174 - 0.5826966763 0.6995912194 0.6782149076 - 0.5836061835 0.7005271912 0.6790891886 - 0.5845115781 0.7014548779 0.6799582839 - 0.5854169130 0.7023826241 0.6808270812 - 0.5863186121 0.7033014894 0.6816880107 - 0.5872203112 0.7042204142 0.6825488806 - 0.5881202817 0.7051343918 0.6834049821 - 0.5890197754 0.7060474157 0.6842601895 - 0.5899165869 0.7069560289 0.6851115227 - 0.5908117294 0.7078614831 0.6859601736 - 0.5917043090 0.7087628841 0.6868060231 - 0.5925924778 0.7096565962 0.6876469851 - 0.5934802294 0.7105495930 0.6884872913 - 0.5943630934 0.7114354968 0.6893228889 - 0.5952460170 0.7123214006 0.6901584268 - 0.5961257815 0.7132012248 0.6909888983 - 0.5970050097 0.7140796185 0.6918182969 - 0.5978829861 0.7149519920 0.6926438212 - 0.5987598896 0.7158185244 0.6934657097 - 0.5996358991 0.7166832089 0.6942861080 - 0.6005082130 0.7175390124 0.6951001883 - 0.6013805866 0.7183946967 0.6959143281 - 0.6022471786 0.7192428708 0.6967222095 - 0.6031126976 0.7200896144 0.6975290179 - 0.6039757133 0.7209308743 0.6983324289 - 0.6048358083 0.7217659950 0.6991323233 - 0.6056957245 0.7226004004 0.6999315023 - 0.6065520048 0.7234249711 0.7007243037 - 0.6074082851 0.7242496014 0.7015169859 - 0.6082630754 0.7250695229 0.7023050785 - 0.6091169715 0.7258870006 0.7030910850 - 0.6099703908 0.7267029285 0.7038756013 - 0.6108219028 0.7275134921 0.7046552896 - 0.6116734743 0.7283241749 0.7054349184 - 0.6125208139 0.7291293740 0.7062094212 - 0.6133667827 0.7299330831 0.7069824934 - 0.6142112017 0.7307348251 0.7077537179 - 0.6150516868 0.7315317988 0.7085208297 - 0.6158921719 0.7323288918 0.7092878222 - 0.6167293787 0.7331209779 0.7100501060 - 0.6175658107 0.7339118123 0.7108113170 - 0.6184015274 0.7347002029 0.7115700245 - 0.6192352176 0.7354820967 0.7123227715 - 0.6200687885 0.7362639904 0.7130755782 - 0.6209005713 0.7370405197 0.7138231993 - 0.6217315793 0.7378150225 0.7145689726 - 0.6225616932 0.7385885119 0.7153139710 - 0.6233869195 0.7393574119 0.7160546780 - 0.6242122054 0.7401263118 0.7167953849 - 0.6250342131 0.7408906817 0.7175316215 - 0.6258537173 0.7416514754 0.7182646990 - 0.6266732216 0.7424122095 0.7189977765 - 0.6274886131 0.7431641817 0.7197247744 - 0.6283037066 0.7439156771 0.7204515934 - 0.6291179061 0.7446653247 0.7211769223 - 0.6299300790 0.7454105020 0.7218992710 - 0.6307423115 0.7461556792 0.7226216197 - 0.6315525770 0.7468963265 0.7233393788 - 0.6323618293 0.7476344109 0.7240548134 - 0.6331710219 0.7483724952 0.7247703075 - 0.6339744925 0.7491027117 0.7254779935 - 0.6347777843 0.7498325706 0.7261853814 - 0.6355797052 0.7505611777 0.7268915772 - 0.6363769770 0.7512856722 0.7275938988 - 0.6371744275 0.7520102859 0.7282962799 - 0.6379693747 0.7527318001 0.7289955020 - 0.6387621760 0.7534502149 0.7296919227 - 0.6395549774 0.7541685104 0.7303882241 - 0.6403455138 0.7548798919 0.7310792208 - 0.6411350965 0.7555885911 0.7317686081 - 0.6419246793 0.7562974095 0.7324578762 - 0.6427112222 0.7570006847 0.7331432104 - 0.6434975863 0.7577037215 0.7338283062 - 0.6442831755 0.7584059834 0.7345126271 - 0.6450635195 0.7591038942 0.7351925969 - 0.6458438039 0.7598018050 0.7358726263 - 0.6466221809 0.7604972720 0.7365499139 - 0.6473965049 0.7611867189 0.7372214794 - 0.6481707096 0.7618761063 0.7378931046 - 0.6489428878 0.7625625134 0.7385625839 - 0.6497123837 0.7632448077 0.7392295003 - 0.6504818797 0.7639272213 0.7398964167 - 0.6512495279 0.7646067142 0.7405610085 - 0.6520156264 0.7652835846 0.7412236929 - 0.6527817249 0.7659605145 0.7418864965 - 0.6535456181 0.7666330934 0.7425445914 - 0.6543081999 0.7673028111 0.7432000041 - 0.6550709009 0.7679725885 0.7438554168 - 0.6558291912 0.7686370015 0.7445067167 - 0.6565855742 0.7692987919 0.7451562285 - 0.6573420167 0.7699605227 0.7458056808 - 0.6580939889 0.7706184983 0.7464519739 - 0.6588441133 0.7712748051 0.7470970154 - 0.6595942974 0.7719311118 0.7477421165 - 0.6603410244 0.7725834250 0.7483826876 - 0.6610863209 0.7732340097 0.7490215898 - 0.6618314981 0.7738845944 0.7496606112 - 0.6625742912 0.7745307088 0.7502961755 - 0.6633158922 0.7751746774 0.7509304881 - 0.6640574932 0.7758187056 0.7515646815 - 0.6647968292 0.7764587998 0.7521967888 - 0.6655347943 0.7770963907 0.7528278232 - 0.6662726998 0.7777339816 0.7534586787 - 0.6670070887 0.7783685923 0.7540869117 - 0.6677386165 0.7790005803 0.7547128797 - 0.6684702039 0.7796326280 0.7553389072 - 0.6691988707 0.7802624106 0.7559623122 - 0.6699240804 0.7808893919 0.7565826774 - 0.6706492901 0.7815164924 0.7572029829 - 0.6713730097 0.7821415067 0.7578222752 - 0.6720930934 0.7827615142 0.7584391832 - 0.6728131771 0.7833815813 0.7590560913 - 0.6735327840 0.7840009928 0.7596722841 - 0.6742489934 0.7846158147 0.7602841854 - 0.6749653220 0.7852305174 0.7608960867 - 0.6756814718 0.7858452797 0.7615079880 - 0.6763954163 0.7864570022 0.7621135712 - 0.6771090031 0.7870684266 0.7627189159 - 0.6778227091 0.7876799107 0.7633240819 - 0.6785330772 0.7882875800 0.7639262080 - 0.6792423129 0.7888938189 0.7645270228 - 0.6799514294 0.7894999981 0.7651277781 - 0.6806576848 0.7901020050 0.7657262087 - 0.6813606024 0.7906988859 0.7663218975 - 0.6820635200 0.7912957072 0.7669175863 - 0.6827654839 0.7918915749 0.7675123215 - 0.6834632158 0.7924820781 0.7681027055 - 0.6841610074 0.7930727005 0.7686930895 - 0.6848587990 0.7936632037 0.7692834735 - 0.6855531931 0.7942507863 0.7698693871 - 0.6862471104 0.7948378921 0.7704545856 - 0.6869410276 0.7954249978 0.7710397840 - 0.6876335144 0.7960084081 0.7716224790 - 0.6883248091 0.7965881228 0.7722026706 - 0.6890159845 0.7971677780 0.7727829218 - 0.6897066236 0.7977464795 0.7733623981 - 0.6903933287 0.7983182073 0.7739374042 - 0.6910800934 0.7988898158 0.7745124102 - 0.6917667985 0.7994614840 0.7750874162 - 0.6924489737 0.8000293970 0.7756578922 - 0.6931294799 0.8005958796 0.7762268782 - 0.6938099861 0.8011624217 0.7767959237 - 0.6944890022 0.8017272949 0.7773637772 - 0.6951643825 0.8022881746 0.7779290080 - 0.6958398223 0.8028491139 0.7784942985 - 0.6965152025 0.8034101129 0.7790595293 - 0.6971874237 0.8039643764 0.7796214223 - 0.6978589892 0.8045170903 0.7801826000 - 0.6985304952 0.8050699234 0.7807437778 - 0.6992011070 0.8056213260 0.7813032866 - 0.6998698711 0.8061696291 0.7818598151 - 0.7005386949 0.8067178130 0.7824162841 - 0.7012075186 0.8072661161 0.7829726934 - 0.7018737197 0.8078114986 0.7835255265 - 0.7025392056 0.8083561063 0.7840774059 - 0.7032046914 0.8089007139 0.7846292853 - 0.7038692236 0.8094440103 0.7851802707 - 0.7045302987 0.8099824190 0.7857282162 - 0.7051913738 0.8105208278 0.7862762213 - 0.7058525085 0.8110591769 0.7868241072 - 0.7065104842 0.8115934730 0.7873699069 - 0.7071666718 0.8121252060 0.7879143953 - 0.7078229189 0.8126569986 0.7884590030 - 0.7084791064 0.8131887913 0.7890034914 - 0.7091314793 0.8137164712 0.7895429134 - 0.7097837925 0.8142442107 0.7900823951 - 0.7104362249 0.8147718906 0.7906218171 - 0.7110877037 0.8152979016 0.7911595106 - 0.7117375135 0.8158202767 0.7916939855 - 0.7123872042 0.8163427114 0.7922285199 - 0.7130370140 0.8168652058 0.7927629948 - 0.7136846781 0.8173835278 0.7932949066 - 0.7143312097 0.8178995252 0.7938255072 - 0.7149778008 0.8184155226 0.7943559885 - 0.7156242728 0.8189315200 0.7948865891 - 0.7162669897 0.8194420934 0.7954136133 - 0.7169092894 0.8199520707 0.7959402800 - 0.7175515890 0.8204621077 0.7964671254 - 0.7181931138 0.8209713101 0.7969928980 - 0.7188305855 0.8214758039 0.7975142002 - 0.7194681168 0.8219804168 0.7980353832 - 0.7201055884 0.8224849105 0.7985566258 - 0.7207418084 0.8229876161 0.7990764976 - 0.7213757038 0.8234868050 0.7995939851 - 0.7220094800 0.8239858747 0.8001114726 - 0.7226433158 0.8244851232 0.8006290197 - 0.7232757807 0.8249812126 0.8011451960 - 0.7239069939 0.8254746199 0.8016602993 - 0.7245383263 0.8259679079 0.8021754026 - 0.7251694798 0.8264611959 0.8026905060 - 0.7257997990 0.8269500136 0.8032016754 - 0.7264295220 0.8274360895 0.8037108779 - 0.7270591855 0.8279222846 0.8042200804 - 0.7276890278 0.8284084797 0.8047292829 - 0.7283164263 0.8288906813 0.8052350879 - 0.7289428711 0.8293713927 0.8057398200 - 0.7295694947 0.8298521042 0.8062443733 - 0.7301961184 0.8303328753 0.8067491055 - 0.7308188081 0.8308109045 0.8072515130 - 0.7314406037 0.8312882185 0.8077533245 - 0.7320623994 0.8317655921 0.8082550764 - 0.7326841950 0.8322429061 0.8087568879 - 0.7333028913 0.8327162862 0.8092550039 - 0.7339209914 0.8331888914 0.8097522855 - 0.7345389724 0.8336614966 0.8102496862 - 0.7351570725 0.8341341019 0.8107470274 - 0.7357730865 0.8346002102 0.8112403154 - 0.7363885045 0.8350648880 0.8117327094 - 0.7370039821 0.8355295062 0.8122249842 - 0.7376194000 0.8359941244 0.8127173781 - 0.7382336855 0.8364549279 0.8132079244 - 0.7388474941 0.8369144201 0.8136978149 - 0.7394614220 0.8373739719 0.8141875863 - 0.7400751710 0.8378335238 0.8146774769 - 0.7406880260 0.8382915258 0.8151652813 - 0.7413002849 0.8387488127 0.8156523108 - 0.7419126034 0.8392060995 0.8161392212 - 0.7425248027 0.8396633863 0.8166260719 - 0.7431349754 0.8401176929 0.8171094060 - 0.7437434196 0.8405694962 0.8175901175 - 0.7443519235 0.8410212994 0.8180707097 - 0.7449604273 0.8414732218 0.8185513020 - 0.7455673218 0.8419228196 0.8190304041 - 0.7461718917 0.8423690200 0.8195071220 - 0.7467764020 0.8428152204 0.8199838996 - 0.7473809719 0.8432614207 0.8204606771 - 0.7479851246 0.8437070847 0.8209370971 - 0.7485867739 0.8441503048 0.8214123249 - 0.7491886020 0.8445935845 0.8218876123 - 0.7497903705 0.8450369239 0.8223627806 - 0.7503920794 0.8454802036 0.8228381276 - 0.7509921193 0.8459206223 0.8233087063 - 0.7515920997 0.8463609219 0.8237791061 - 0.7521920204 0.8468011022 0.8242496252 - 0.7527918816 0.8472414017 0.8247200251 - 0.7533906102 0.8476775289 0.8251867890 - 0.7539888024 0.8481116891 0.8256520033 - 0.7545868754 0.8485457897 0.8261172175 - 0.7551851273 0.8489800096 0.8265823722 - 0.7557817101 0.8494123220 0.8270465136 - 0.7563759089 0.8498412967 0.8275086880 - 0.7569699883 0.8502702713 0.8279709816 - 0.7575641274 0.8506993055 0.8284332156 - 0.7581582069 0.8511282802 0.8288952708 - 0.7587482929 0.8515555859 0.8293542862 - 0.7593383789 0.8519830108 0.8298131824 - 0.7599285841 0.8524103165 0.8302720785 - 0.7605187297 0.8528376222 0.8307309747 - 0.7611069083 0.8532627225 0.8311864138 - 0.7616940141 0.8536863923 0.8316398859 - 0.7622811198 0.8541101217 0.8320934176 - 0.7628682852 0.8545337915 0.8325468898 - 0.7634549737 0.8549563885 0.8329998851 - 0.7640401125 0.8553745747 0.8334509134 - 0.7646251917 0.8557928205 0.8339020014 - 0.7652102113 0.8562110066 0.8343530297 - 0.7657952905 0.8566291928 0.8348039985 - 0.7663789988 0.8570433855 0.8352537155 - 0.7669621110 0.8574560881 0.8357028961 - 0.7675452232 0.8578686714 0.8361520767 - 0.7681282163 0.8582813144 0.8366013169 - 0.7687104940 0.8586933017 0.8370494843 - 0.7692893744 0.8591027856 0.8374937177 - 0.7698683143 0.8595123291 0.8379380107 - 0.7704473138 0.8599218130 0.8383821845 - 0.7710261941 0.8603312969 0.8388264775 - 0.7716023922 0.8607382774 0.8392679095 - 0.7721772194 0.8611438274 0.8397077918 - 0.7727519870 0.8615493774 0.8401476741 - 0.7733268142 0.8619549870 0.8405876756 - 0.7739014030 0.8623604178 0.8410274982 - 0.7744730115 0.8627613187 0.8414660096 - 0.7750446200 0.8631622195 0.8419045210 - 0.7756162882 0.8635631800 0.8423430920 - 0.7761878967 0.8639640808 0.8427816033 - 0.7767586112 0.8643633723 0.8432186246 - 0.7773280144 0.8647603989 0.8436536789 - 0.7778974175 0.8651574254 0.8440887928 - 0.7784668207 0.8655543923 0.8445239067 - 0.7790362835 0.8659514189 0.8449590206 - 0.7796040773 0.8663460016 0.8453900218 - 0.7801712751 0.8667395711 0.8458197117 - 0.7807385921 0.8671333194 0.8462492824 - 0.7813057899 0.8675270081 0.8466789722 - 0.7818729877 0.8679206967 0.8471086025 - 0.7824358940 0.8683102727 0.8475356102 - 0.7829989195 0.8686999083 0.8479626775 - 0.7835618258 0.8690894842 0.8483896852 - 0.7841247916 0.8694791198 0.8488166928 - 0.7846866846 0.8698676229 0.8492432833 - 0.7852454185 0.8702524900 0.8496683240 - 0.7858040929 0.8706374168 0.8500934243 - 0.7863628268 0.8710222244 0.8505184054 - 0.7869215012 0.8714070916 0.8509433866 - 0.7874788046 0.8717902899 0.8513662815 - 0.7880342007 0.8721709847 0.8517863750 - 0.7885895967 0.8725517988 0.8522065282 - 0.7891449928 0.8729326129 0.8526266217 - 0.7897003293 0.8733134270 0.8530467153 - 0.7902544141 0.8736922145 0.8534634113 - 0.7908074260 0.8740695715 0.8538776040 - 0.7913604975 0.8744469285 0.8542917967 - 0.7919135094 0.8748242259 0.8547059894 - 0.7924665213 0.8752015829 0.8551203012 - 0.7930179834 0.8755767941 0.8555328250 - 0.7935686111 0.8759508729 0.8559445739 - 0.7941192985 0.8763250709 0.8563563824 - 0.7946699858 0.8766993284 0.8567680717 - 0.7952206731 0.8770734072 0.8571798801 - 0.7957680821 0.8774452806 0.8575891852 - 0.7963144183 0.8778162003 0.8579975963 - 0.7968606949 0.8781871796 0.8584060073 - 0.7974069715 0.8785582185 0.8588144183 - 0.7979531884 0.8789290786 0.8592228293 - 0.7984963059 0.8792970777 0.8596280217 - 0.7990381718 0.8796638846 0.8600320816 - 0.7995802164 0.8800308108 0.8604362011 - 0.8001220822 0.8803976774 0.8608403206 - 0.8006640077 0.8807644844 0.8612444997 - 0.8012034893 0.8811290264 0.8616467714 - 0.8017420173 0.8814923167 0.8620483875 - 0.8022804856 0.8818557262 0.8624501228 - 0.8028190136 0.8822190762 0.8628516793 - 0.8033574820 0.8825824857 0.8632534146 - 0.8038944006 0.8829445243 0.8636533022 - 0.8044304252 0.8833057880 0.8640521765 - 0.8049663901 0.8836669922 0.8644511104 - 0.8055024147 0.8840281963 0.8648499846 - 0.8060383797 0.8843894005 0.8652489185 - 0.8065730929 0.8847498894 0.8656460047 - 0.8071064949 0.8851096034 0.8660413027 - 0.8076400161 0.8854691982 0.8664366007 - 0.8081734180 0.8858289123 0.8668318987 - 0.8087068796 0.8861885071 0.8672271967 - 0.8092389107 0.8865470290 0.8676213026 - 0.8097679019 0.8869025707 0.8680127263 - 0.8102967739 0.8872582912 0.8684040904 - 0.8108258247 0.8876140118 0.8687955141 - 0.8113546968 0.8879696727 0.8691868782 - 0.8118832111 0.8883249760 0.8695781231 - 0.8124076724 0.8886772990 0.8699674010 - 0.8129323125 0.8890296221 0.8703566790 - 0.8134567738 0.8893818855 0.8707460165 - 0.8139812946 0.8897342086 0.8711354136 - 0.8145058155 0.8900865912 0.8715246916 - 0.8150272965 0.8904371858 0.8719115257 - 0.8155483007 0.8907874227 0.8722978830 - 0.8160691857 0.8911375999 0.8726842999 - 0.8165901899 0.8914877772 0.8730705976 - 0.8171110749 0.8918380141 0.8734570146 - 0.8176307082 0.8921874762 0.8738412857 - 0.8181489706 0.8925362229 0.8742238879 - 0.8186672926 0.8928849101 0.8746063709 - 0.8191856146 0.8932335973 0.8749889135 - 0.8197038770 0.8935822845 0.8753713965 - 0.8202217817 0.8939304948 0.8757534027 - 0.8207373023 0.8942753077 0.8761324286 - 0.8212528825 0.8946201801 0.8765113950 - 0.8217685223 0.8949649930 0.8768904209 - 0.8222841024 0.8953098059 0.8772693872 - 0.8227996826 0.8956546187 0.8776484132 - 0.8233128786 0.8959960938 0.8780251145 - 0.8238251805 0.8963361979 0.8784009814 - 0.8243374825 0.8966763020 0.8787769079 - 0.8248497844 0.8970165253 0.8791527152 - 0.8253620863 0.8973566294 0.8795285821 - 0.8258733749 0.8976960182 0.8799036145 - 0.8263819218 0.8980333209 0.8802763820 - 0.8268905282 0.8983706236 0.8806490898 - 0.8273990154 0.8987079263 0.8810219169 - 0.8279076219 0.8990452886 0.8813946843 - 0.8284161091 0.8993825912 0.8817673922 - 0.8289219141 0.8997188807 0.8821375966 - 0.8294265866 0.9000548124 0.8825070262 - 0.8299313784 0.9003908038 0.8828763962 - 0.8304362297 0.9007266760 0.8832458258 - 0.8309410214 0.9010627270 0.8836151958 - 0.8314452171 0.9013980031 0.8839840293 - 0.8319470882 0.9017302990 0.8843502998 - 0.8324490190 0.9020625949 0.8847165704 - 0.8329507709 0.9023948908 0.8850830197 - 0.8334527016 0.9027271867 0.8854492903 - 0.8339545727 0.9030594826 0.8858156800 - 0.8344553113 0.9033885002 0.8861801028 - 0.8349552155 0.9037148952 0.8865432143 - 0.8354550004 0.9040412903 0.8869063258 - 0.8359549046 0.9043676853 0.8872693777 - 0.8364546895 0.9046940804 0.8876324296 - 0.8369545937 0.9050204754 0.8879954815 - 0.8374524713 0.9053437710 0.8883556724 - 0.8379502892 0.9056668282 0.8887156248 - 0.8384481072 0.9059898257 0.8890755177 - 0.8389458060 0.9063128233 0.8894352913 - 0.8394436240 0.9066357017 0.8897951841 - 0.8399409056 0.9069585204 0.8901546001 - 0.8404362202 0.9072805047 0.8905115724 - 0.8409314156 0.9076024294 0.8908686042 - 0.8414266706 0.9079242945 0.8912255764 - 0.8419219255 0.9082462192 0.8915826082 - 0.8424171209 0.9085682034 0.8919395804 - 0.8429111242 0.9088888168 0.8922954798 - 0.8434032798 0.9092074037 0.8926500082 - 0.8438953757 0.9095259905 0.8930044770 - 0.8443875909 0.9098446965 0.8933590055 - 0.8448796868 0.9101632833 0.8937134147 - 0.8453719020 0.9104819894 0.8940678835 - 0.8458622098 0.9107974172 0.8944209218 - 0.8463512063 0.9111105800 0.8947728276 - 0.8468402028 0.9114238024 0.8951247931 - 0.8473293185 0.9117369056 0.8954768181 - 0.8478183150 0.9120501280 0.8958287239 - 0.8483073115 0.9123632908 0.8961806893 - 0.8487946987 0.9126743078 0.8965306878 - 0.8492814898 0.9129843712 0.8968799114 - 0.8497682214 0.9132944942 0.8972290158 - 0.8502548933 0.9136046171 0.8975781798 - 0.8507416844 0.9139146805 0.8979274035 - 0.8512284160 0.9142248034 0.8982766271 - 0.8517140150 0.9145342708 0.8986228108 - 0.8521993160 0.9148436785 0.8989682794 - 0.8526844978 0.9151530266 0.8993136883 - 0.8531697989 0.9154623747 0.8996590972 - 0.8536550999 0.9157717228 0.9000046253 - 0.8541402817 0.9160810113 0.9003499746 - 0.8546243906 0.9163880944 0.9006927013 - 0.8551082015 0.9166945219 0.9010347128 - 0.8555920720 0.9170008898 0.9013767242 - 0.8560758829 0.9173073173 0.9017186761 - 0.8565596938 0.9176136851 0.9020606875 - 0.8570435047 0.9179201126 0.9024026990 - 0.8575248718 0.9182233214 0.9027428031 - 0.8580054045 0.9185255170 0.9030823112 - 0.8584859967 0.9188277125 0.9034218192 - 0.8589665890 0.9191299081 0.9037613273 - 0.8594471216 0.9194321036 0.9041007757 - 0.8599277139 0.9197342992 0.9044402838 - 0.8604061007 0.9200347066 0.9047783017 - 0.8608834743 0.9203342199 0.9051156044 - 0.8613607883 0.9206337929 0.9054529071 - 0.8618381023 0.9209333062 0.9057902098 - 0.8623154163 0.9212328196 0.9061275125 - 0.8627927899 0.9215323925 0.9064648151 - 0.8632687926 0.9218313098 0.9068005085 - 0.8637437224 0.9221296906 0.9071345925 - 0.8642185926 0.9224280119 0.9074686766 - 0.8646935225 0.9227263927 0.9078028202 - 0.8651683927 0.9230247736 0.9081369042 - 0.8656433225 0.9233232141 0.9084709883 - 0.8661177158 0.9236208200 0.9088042974 - 0.8665909171 0.9239168167 0.9091358781 - 0.8670641780 0.9242126942 0.9094673991 - 0.8675373793 0.9245086908 0.9097989202 - 0.8680106997 0.9248046279 0.9101303816 - 0.8684839010 0.9251006246 0.9104620218 - 0.8689571023 0.9253963828 0.9107934237 - 0.8694286942 0.9256886244 0.9111229181 - 0.8699004054 0.9259809256 0.9114524126 - 0.8703719974 0.9262732267 0.9117819071 - 0.8708435893 0.9265655279 0.9121115208 - 0.8713153005 0.9268577099 0.9124410152 - 0.8717868924 0.9271500111 0.9127705097 - 0.8722561002 0.9274398088 0.9130979180 - 0.8727244139 0.9277285933 0.9134243727 - 0.8731926084 0.9280173182 0.9137510061 - 0.8736609221 0.9283061028 0.9140775204 - 0.8741291761 0.9285948873 0.9144040942 - 0.8745974898 0.9288836718 0.9147307277 - 0.8750646114 0.9291719198 0.9150562882 - 0.8755295277 0.9294592738 0.9153801799 - 0.8759943843 0.9297466278 0.9157040715 - 0.8764593005 0.9300339222 0.9160280824 - 0.8769242167 0.9303212166 0.9163519740 - 0.8773891926 0.9306085110 0.9166759253 - 0.8778541088 0.9308958054 0.9169998169 - 0.8783167005 0.9311814904 0.9173220992 - 0.8787789941 0.9314669967 0.9176442027 - 0.8792414069 0.9317525029 0.9179663062 - 0.8797037005 0.9320380092 0.9182885289 - 0.8801661134 0.9323235154 0.9186105728 - 0.8806284070 0.9326090217 0.9189326763 - 0.8810901046 0.9328929186 0.9192541838 - 0.8815506101 0.9331744909 0.9195749164 - 0.8820111752 0.9334561825 0.9198955297 - 0.8824717999 0.9337378144 0.9202160835 - 0.8829324245 0.9340193868 0.9205368161 - 0.8833928704 0.9343010783 0.9208574295 - 0.8838534951 0.9345827103 0.9211781025 - 0.8843126297 0.9348607063 0.9214954972 - 0.8847714067 0.9351381063 0.9218124747 - 0.8852301836 0.9354155064 0.9221295118 - 0.8856890202 0.9356929064 0.9224464297 - 0.8861477971 0.9359703064 0.9227634072 - 0.8866065741 0.9362477064 0.9230803847 - 0.8870645761 0.9365242720 0.9233967066 - 0.8875198960 0.9367982149 0.9237111211 - 0.8879752755 0.9370720983 0.9240254760 - 0.8884305954 0.9373459816 0.9243398905 - 0.8888859153 0.9376199245 0.9246544242 - 0.8893412948 0.9378938079 0.9249687791 - 0.8897966146 0.9381676912 0.9252831936 - 0.8902500272 0.9384400249 0.9255968928 - 0.8907018900 0.9387111068 0.9259101152 - 0.8911538124 0.9389823079 0.9262232780 - 0.8916056752 0.9392533898 0.9265365005 - 0.8920575976 0.9395244718 0.9268497229 - 0.8925095201 0.9397956133 0.9271628857 - 0.8929613829 0.9400668144 0.9274761081 - 0.8934110999 0.9403352737 0.9277874827 - 0.8938602805 0.9406033158 0.9280986190 - 0.8943095207 0.9408712983 0.9284096956 - 0.8947588205 0.9411392808 0.9287207127 - 0.8952080011 0.9414073229 0.9290317893 - 0.8956571817 0.9416753054 0.9293429255 - 0.8961064219 0.9419432282 0.9296538234 - 0.8965536952 0.9422078133 0.9299619198 - 0.8970010281 0.9424722791 0.9302698970 - 0.8974484205 0.9427369237 0.9305778742 - 0.8978956938 0.9430013895 0.9308859110 - 0.8983430266 0.9432659149 0.9311940074 - 0.8987902999 0.9435304999 0.9315019846 - 0.8992372751 0.9437944889 0.9318097234 - 0.8996827006 0.9440562725 0.9321159720 - 0.9001281261 0.9443179965 0.9324222803 - 0.9005734921 0.9445797801 0.9327287078 - 0.9010189772 0.9448416233 0.9330350161 - 0.9014644027 0.9451032877 0.9333413243 - 0.9019098282 0.9453650713 0.9336475730 - 0.9023545980 0.9456260204 0.9339538217 - 0.9027975798 0.9458845258 0.9342594147 - 0.9032405019 0.9461429715 0.9345651269 - 0.9036834836 0.9464014769 0.9348707795 - 0.9041265249 0.9466599822 0.9351763725 - 0.9045695066 0.9469184875 0.9354820848 - 0.9050124884 0.9471769929 0.9357876778 - 0.9054549932 0.9474347234 0.9360926151 - 0.9058961868 0.9476901293 0.9363955855 - 0.9063374996 0.9479454756 0.9366984963 - 0.9067788124 0.9482008815 0.9370014071 - 0.9072200060 0.9484562874 0.9373043180 - 0.9076613188 0.9487115741 0.9376072288 - 0.9081025720 0.9489669800 0.9379101992 - 0.9085432887 0.9492217898 0.9382125139 - 0.9089823961 0.9494745135 0.9385129809 - 0.9094215035 0.9497271776 0.9388133883 - 0.9098604918 0.9499800205 0.9391139150 - 0.9102995992 0.9502326846 0.9394143820 - 0.9107387066 0.9504854083 0.9397147894 - 0.9111778140 0.9507380724 0.9400153160 - 0.9116166234 0.9509906173 0.9403156042 - 0.9120534062 0.9512413144 0.9406144023 - 0.9124903083 0.9514918923 0.9409132004 - 0.9129270911 0.9517425895 0.9412119985 - 0.9133639932 0.9519932866 0.9415107965 - 0.9138007760 0.9522439837 0.9418095946 - 0.9142376781 0.9524946809 0.9421083927 - 0.9146745205 0.9527453780 0.9424071908 - 0.9151092768 0.9529926777 0.9427046776 - 0.9155439138 0.9532396197 0.9430022240 - 0.9159784913 0.9534866214 0.9432995915 - 0.9164131284 0.9537336230 0.9435970187 - 0.9168477058 0.9539806247 0.9438943863 - 0.9172822833 0.9542276263 0.9441918135 - 0.9177169800 0.9544746280 0.9444893003 - 0.9181497097 0.9547194839 0.9447842836 - 0.9185816050 0.9549633861 0.9450783730 - 0.9190135002 0.9552074075 0.9453724027 - 0.9194452763 0.9554513097 0.9456663728 - 0.9198771715 0.9556952715 0.9459604025 - 0.9203091264 0.9559391737 0.9462544918 - 0.9207410216 0.9561831951 0.9465485215 - 0.9211720824 0.9564263821 0.9468415976 - 0.9216020107 0.9566686153 0.9471331835 - 0.9220318198 0.9569107294 0.9474248886 - 0.9224616289 0.9571527839 0.9477164745 - 0.9228914976 0.9573948979 0.9480081797 - 0.9233213067 0.9576370120 0.9482998252 - 0.9237511754 0.9578791857 0.9485914707 - 0.9241809845 0.9581212997 0.9488831162 - 0.9246084094 0.9583622217 0.9491736293 - 0.9250357151 0.9586032033 0.9494642019 - 0.9254630804 0.9588441253 0.9497547150 - 0.9258903861 0.9590849876 0.9500452280 - 0.9263178110 0.9593259096 0.9503356814 - 0.9267451167 0.9595668912 0.9506263137 - 0.9271724820 0.9598078132 0.9509168267 - 0.9275984168 0.9600471854 0.9512062073 - 0.9280232787 0.9602853060 0.9514948130 - 0.9284480810 0.9605233073 0.9517832994 - 0.9288728833 0.9607614279 0.9520719051 - 0.9292978048 0.9609994888 0.9523603916 - 0.9297226071 0.9612376094 0.9526489973 - 0.9301474094 0.9614756703 0.9529374838 - 0.9305720925 0.9617136121 0.9532259107 - 0.9309926033 0.9619483948 0.9535105228 - 0.9314129949 0.9621831179 0.9537950754 - 0.9318335056 0.9624179006 0.9540796876 - 0.9322540164 0.9626526237 0.9543642998 - 0.9326745272 0.9628874063 0.9546489120 - 0.9330949187 0.9631221294 0.9549335837 - 0.9335154295 0.9633569121 0.9552181959 - 0.9339327216 0.9635908008 0.9555017948 - 0.9343463778 0.9638239145 0.9557842016 - 0.9347600937 0.9640570283 0.9560666084 - 0.9351738095 0.9642900229 0.9563490152 - 0.9355874062 0.9645230770 0.9566314220 - 0.9360011220 0.9647560716 0.9569138288 - 0.9364147782 0.9649891853 0.9571961761 - 0.9368284941 0.9652221799 0.9574785829 - 0.9372366071 0.9654536247 0.9577592015 - 0.9376435280 0.9656844735 0.9580392838 - 0.9380503893 0.9659153223 0.9583194256 - 0.9384573102 0.9661461711 0.9585996270 - 0.9388641119 0.9663770795 0.9588797092 - 0.9392709732 0.9666079879 0.9591597915 - 0.9396778941 0.9668388963 0.9594399929 - 0.9400843978 0.9670693874 0.9597195983 - 0.9404869080 0.9672951102 0.9599938989 - 0.9408894777 0.9675207734 0.9602683187 - 0.9412919879 0.9677464962 0.9605426192 - 0.9416946173 0.9679722190 0.9608169794 - 0.9420971870 0.9681978822 0.9610913992 - 0.9424996972 0.9684236050 0.9613656998 - 0.9429023266 0.9686493278 0.9616401196 - 0.9433040023 0.9688737988 0.9619129896 - 0.9437037706 0.9690952897 0.9621827006 - 0.9441037178 0.9693168998 0.9624524713 - 0.9445034862 0.9695385098 0.9627221823 - 0.9449033141 0.9697600007 0.9629918933 - 0.9453032017 0.9699816108 0.9632616043 - 0.9457030296 0.9702031016 0.9635313153 - 0.9461029172 0.9704247117 0.9638010263 - 0.9465014935 0.9706450105 0.9640696049 - 0.9468985796 0.9708636999 0.9643365741 - 0.9472957253 0.9710823298 0.9646036029 - 0.9476928711 0.9713010192 0.9648706913 - 0.9480900168 0.9715197086 0.9651377201 - 0.9484871030 0.9717382789 0.9654048085 - 0.9488843083 0.9719570279 0.9656717777 - 0.9492813945 0.9721757174 0.9659388065 - 0.9496768117 0.9723932743 0.9662050009 - 0.9500706792 0.9726098180 0.9664703012 - 0.9504644871 0.9728264213 0.9667354822 - 0.9508582950 0.9730429053 0.9670007825 - 0.9512521029 0.9732595086 0.9672660828 - 0.9516459107 0.9734759927 0.9675313830 - 0.9520397782 0.9736925960 0.9677966237 - 0.9524335861 0.9739090800 0.9680619240 - 0.9528253078 0.9741246104 0.9683256745 - 0.9532151222 0.9743391275 0.9685881138 - 0.9536048174 0.9745535254 0.9688504934 - 0.9539945126 0.9747679830 0.9691128731 - 0.9543843269 0.9749823809 0.9693753123 - 0.9547740221 0.9751967788 0.9696376920 - 0.9551637173 0.9754112959 0.9699000716 - 0.9555534720 0.9756256938 0.9701625109 - 0.9559414983 0.9758388996 0.9704235196 - 0.9563273191 0.9760503173 0.9706826210 - 0.9567130208 0.9762617946 0.9709417224 - 0.9570987821 0.9764732122 0.9712008238 - 0.9574846029 0.9766845703 0.9714599848 - 0.9578703046 0.9768959880 0.9717190862 - 0.9582561255 0.9771075249 0.9719781876 - 0.9586418867 0.9773188829 0.9722372890 - 0.9590265751 0.9775292873 0.9724956751 - 0.9594085217 0.9777370095 0.9727519751 - 0.9597904086 0.9779447913 0.9730082750 - 0.9601722956 0.9781525135 0.9732645750 - 0.9605541825 0.9783601761 0.9735208750 - 0.9609361291 0.9785678983 0.9737771749 - 0.9613180161 0.9787756801 0.9740334749 - 0.9616999030 0.9789834023 0.9742897749 - 0.9620816708 0.9791910052 0.9745460153 - 0.9624596238 0.9793955088 0.9747992754 - 0.9628375769 0.9796000719 0.9750527143 - 0.9632155895 0.9798046947 0.9753059745 - 0.9635934830 0.9800091982 0.9755592942 - 0.9639714956 0.9802138209 0.9758126140 - 0.9643493891 0.9804183841 0.9760659933 - 0.9647274017 0.9806228876 0.9763193130 - 0.9651054144 0.9808275104 0.9765725732 - 0.9654802084 0.9810302258 0.9768230915 - 0.9658539295 0.9812322855 0.9770727158 - 0.9662274718 0.9814344049 0.9773222208 - 0.9666011930 0.9816365242 0.9775717854 - 0.9669749141 0.9818385839 0.9778214097 - 0.9673485756 0.9820407033 0.9780709147 - 0.9677222967 0.9822428226 0.9783204794 - 0.9680960178 0.9824448824 0.9785699844 - 0.9684677720 0.9826459289 0.9788182974 - 0.9688364863 0.9828448296 0.9790642262 - 0.9692050815 0.9830436707 0.9793102145 - 0.9695736766 0.9832426906 0.9795560837 - 0.9699423909 0.9834415913 0.9798020720 - 0.9703109860 0.9836406112 0.9800480008 - 0.9706795812 0.9838395119 0.9802939892 - 0.9710482955 0.9840384126 0.9805399179 - 0.9714168906 0.9842373729 0.9807859063 - 0.9717804790 0.9844332933 0.9810283780 - 0.9721437097 0.9846289158 0.9812706113 - 0.9725068212 0.9848244786 0.9815129042 - 0.9728699923 0.9850201011 0.9817550778 - 0.9732331038 0.9852156043 0.9819973707 - 0.9735962152 0.9854112267 0.9822396040 - 0.9739593863 0.9856067896 0.9824817777 - 0.9743224978 0.9858024120 0.9827240705 - 0.9746835232 0.9859969020 0.9829645157 - 0.9750407934 0.9861896038 0.9832020998 - 0.9753981233 0.9863823056 0.9834396839 - 0.9757553935 0.9865750074 0.9836772084 - 0.9761127234 0.9867678285 0.9839147925 - 0.9764701128 0.9869604707 0.9841523767 - 0.9768273830 0.9871531725 0.9843899012 - 0.9771847129 0.9873458743 0.9846274853 - 0.9775419831 0.9875385761 0.9848651290 - 0.9778943062 0.9877287149 0.9850988984 - 0.9782450199 0.9879180193 0.9853315949 - 0.9785957932 0.9881073236 0.9855642915 - 0.9789465070 0.9882966280 0.9857969284 - 0.9792972803 0.9884859920 0.9860296249 - 0.9796479940 0.9886752963 0.9862623215 - 0.9799987078 0.9888646007 0.9864950180 - 0.9803494811 0.9890539050 0.9867277145 - 0.9806995988 0.9892429113 0.9869599938 - 0.9810422063 0.9894282222 0.9871879816 - 0.9813848138 0.9896134138 0.9874159098 - 0.9817274213 0.9897987247 0.9876437783 - 0.9820700288 0.9899839163 0.9878718257 - 0.9824125767 0.9901692271 0.9880996943 - 0.9827551246 0.9903544188 0.9883276820 - 0.9830976725 0.9905397296 0.9885556102 - 0.9834402800 0.9907249212 0.9887835979 - 0.9837800264 0.9909089208 0.9890096188 - 0.9841130972 0.9910902977 0.9892314076 - 0.9844462276 0.9912716150 0.9894531965 - 0.9847792983 0.9914529920 0.9896751046 - 0.9851124287 0.9916343093 0.9898968935 - 0.9854454994 0.9918156266 0.9901186824 - 0.9857786298 0.9919970036 0.9903404713 - 0.9861117005 0.9921783209 0.9905623198 - 0.9864447713 0.9923596978 0.9907841086 - 0.9867734909 0.9925395250 0.9910029769 - 0.9870970845 0.9927175045 0.9912183881 - 0.9874207973 0.9928954840 0.9914339185 - 0.9877445102 0.9930735230 0.9916493297 - 0.9880681038 0.9932515025 0.9918648005 - 0.9883918166 0.9934294820 0.9920802116 - 0.9887155294 0.9936075211 0.9922956824 - 0.9890391231 0.9937855005 0.9925110936 - 0.9893627763 0.9939634800 0.9927266240 - 0.9896805286 0.9941388965 0.9929388165 - 0.9899929762 0.9943121076 0.9931482077 - 0.9903056026 0.9944853187 0.9933575988 - 0.9906181097 0.9946585298 0.9935669899 - 0.9909306765 0.9948316813 0.9937765002 - 0.9912433028 0.9950047731 0.9939858913 - 0.9915558100 0.9951779842 0.9941952825 - 0.9918683767 0.9953511953 0.9944046736 - 0.9921808839 0.9955244064 0.9946141243 - 0.9924861789 0.9956938028 0.9948195219 - 0.9927850962 0.9958598018 0.9950211048 - 0.9930838943 0.9960258007 0.9952228069 - 0.9933826923 0.9961917996 0.9954243898 - 0.9936814904 0.9963577986 0.9956260920 - 0.9939802885 0.9965237975 0.9958277941 - 0.9942790866 0.9966899157 0.9960293770 - 0.9945780039 0.9968559146 0.9962310791 - 0.9948768020 0.9970219135 0.9964327216 - 0.9951673746 0.9971848130 0.9966300130 - 0.9954472184 0.9973435998 0.9968215227 - 0.9957271218 0.9975023866 0.9970129728 - 0.9960069060 0.9976611733 0.9972046018 - 0.9962868094 0.9978200197 0.9973961115 - 0.9965665936 0.9979788065 0.9975876212 - 0.9968464971 0.9981375933 0.9977790713 - 0.9971262813 0.9982963800 0.9979707003 - 0.9974061847 0.9984552264 0.9981622100 - 0.9976803064 0.9986127019 0.9983515143 - 0.9979380965 0.9987668991 0.9985346794 - 0.9981958270 0.9989209771 0.9987179041 - 0.9984536171 0.9990751743 0.9989010096 - 0.9987112880 0.9992293119 0.9990841746 - 0.9989690185 0.9993833899 0.9992673993 - 0.9992268085 0.9995375872 0.9994505048 - 0.9994844794 0.9996917248 0.9996337295 - 0.9997423291 0.9998459220 0.9998167753 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0261295997 0.0275282897 0.0193712804 + 0.0436814092 0.0463444404 0.0334131084 + 0.0581157207 0.0624009110 0.0459993482 + 0.0707884133 0.0767784268 0.0574880093 + 0.0822683722 0.0900187567 0.0680924430 + 0.0928523093 0.1023626998 0.0779986978 + 0.1027420014 0.1139826998 0.0873259678 + 0.1121006012 0.1249416992 0.0962512568 + 0.1209674031 0.1352714002 0.1049458012 + 0.1293299943 0.1450500935 0.1134937033 + 0.1372125000 0.1543603987 0.1219277978 + 0.1446518004 0.1632550061 0.1302694976 + 0.1517110020 0.1717393994 0.1385125965 + 0.1584471017 0.1798606068 0.1466525048 + 0.1648889929 0.1876538992 0.1546880007 + 0.1710644960 0.1951581985 0.1626233011 + 0.1769893020 0.2023999989 0.1704642028 + 0.1826931983 0.2093926072 0.1781909019 + 0.1881920993 0.2161574960 0.1858115941 + 0.1935151964 0.2226887047 0.1933328062 + 0.1986732930 0.2290194929 0.2007412016 + 0.2036796063 0.2351579070 0.2080101967 + 0.2085503936 0.2411172986 0.2151511014 + 0.2132862955 0.2469058037 0.2221754044 + 0.2178914994 0.2525430918 0.2290547043 + 0.2223861068 0.2580389082 0.2357912064 + 0.2267823070 0.2634046972 0.2423840016 + 0.2310760021 0.2686296105 0.2488331944 + 0.2352629006 0.2737227976 0.2551515996 + 0.2393413931 0.2787044942 0.2613373995 + 0.2433319986 0.2835777998 0.2673675120 + 0.2472561002 0.2883430123 0.2732523978 + 0.2511121929 0.2930105031 0.2790116072 + 0.2549034059 0.2975909114 0.2846449912 + 0.2586241066 0.3020789027 0.2901515961 + 0.2622672915 0.3064748049 0.2955358922 + 0.2658410072 0.3107773066 0.3008053899 + 0.2693549097 0.3150070012 0.3059557974 + 0.2728112042 0.3191769123 0.3109906018 + 0.2762080133 0.3232814074 0.3159134090 + 0.2795439959 0.3273217976 0.3207265139 + 0.2828184962 0.3312951028 0.3254382014 + 0.2860270143 0.3351995945 0.3300522864 + 0.2891789079 0.3390446007 0.3345699012 + 0.2922790051 0.3428247869 0.3389827907 + 0.2953406870 0.3465405107 0.3433066905 + 0.2983652055 0.3501957953 0.3475450873 + 0.3013558090 0.3538022935 0.3517045081 + 0.3043130934 0.3573650122 0.3557837009 + 0.3072336018 0.3608897924 0.3597820997 + 0.3101125956 0.3643819094 0.3637022078 + 0.3129473925 0.3678374887 0.3675518930 + 0.3157351017 0.3712525070 0.3713383079 + 0.3184793890 0.3746286929 0.3750616014 + 0.3211834133 0.3779700100 0.3787159920 + 0.3238503933 0.3812797964 0.3823012114 + 0.3264833987 0.3845624030 0.3858228922 + 0.3290852010 0.3878163993 0.3892802894 + 0.3316572905 0.3910405934 0.3926731944 + 0.3341959119 0.3942339122 0.3960165083 + 0.3367032111 0.3973953128 0.3993082047 + 0.3391802907 0.4005222023 0.4025487006 + 0.3416261971 0.4036155939 0.4057467878 + 0.3440423906 0.4066751897 0.4088996053 + 0.3464320898 0.4097003937 0.4120030999 + 0.3487968147 0.4126878977 0.4150581956 + 0.3511416912 0.4156408012 0.4180706143 + 0.3534660041 0.4185580909 0.4210394919 + 0.3557670116 0.4214420021 0.4239597917 + 0.3580501974 0.4243034124 0.4268440902 + 0.3603110015 0.4271419048 0.4296903014 + 0.3625530899 0.4299592078 0.4325061142 + 0.3647739887 0.4327593148 0.4352906942 + 0.3669759929 0.4355489016 0.4380438924 + 0.3691574037 0.4383288026 0.4407683015 + 0.3713135123 0.4410988092 0.4434654117 + 0.3734453022 0.4438571036 0.4461325109 + 0.3755545914 0.4466049075 0.4487735927 + 0.3776372969 0.4493390918 0.4513854980 + 0.3797005117 0.4520614147 0.4539698958 + 0.3817447126 0.4547702968 0.4565226138 + 0.3837673068 0.4574646056 0.4590449929 + 0.3857727945 0.4601452947 0.4615412056 + 0.3877620995 0.4628106952 0.4640046060 + 0.3897323012 0.4654597938 0.4664371908 + 0.3916867077 0.4680936038 0.4688445032 + 0.3936245143 0.4707117081 0.4712260067 + 0.3955442011 0.4733099043 0.4735803902 + 0.3974477053 0.4758862853 0.4759103060 + 0.3993369937 0.4784410894 0.4782153070 + 0.4012120068 0.4809744954 0.4804973900 + 0.4030661881 0.4834777117 0.4827564061 + 0.4049043059 0.4859539866 0.4849967062 + 0.4067262113 0.4884032905 0.4872167110 + 0.4085322917 0.4908267856 0.4894092977 + 0.4103249907 0.4932255149 0.4915755093 + 0.4121057093 0.4955982864 0.4937163889 + 0.4138723910 0.4979467094 0.4958330989 + 0.4156245887 0.5002682805 0.4979225099 + 0.4173628986 0.5025631189 0.4999859035 + 0.4190871119 0.5048316121 0.5020250082 + 0.4207980931 0.5070779920 0.5040426254 + 0.4224953055 0.5093029141 0.5060408115 + 0.4241782129 0.5115067959 0.5080217719 + 0.4258452058 0.5136871934 0.5099846721 + 0.4274975955 0.5158441067 0.5119276047 + 0.4291372001 0.5179803967 0.5138525963 + 0.4307664931 0.5200970173 0.5157613158 + 0.4323846996 0.5221933722 0.5176572204 + 0.4339928925 0.5242717862 0.5195353031 + 0.4355897009 0.5263323784 0.5213940144 + 0.4371742010 0.5283774137 0.5232350826 + 0.4387458861 0.5304070115 0.5250611901 + 0.4403051138 0.5324196219 0.5268712044 + 0.4418540895 0.5344129205 0.5286645889 + 0.4433917105 0.5363882184 0.5304409266 + 0.4449174106 0.5383464098 0.5322008729 + 0.4464308918 0.5402891040 0.5339469910 + 0.4479325116 0.5422176123 0.5356794000 + 0.4494239092 0.5441330075 0.5373972058 + 0.4509055912 0.5460358262 0.5391005874 + 0.4523760974 0.5479230881 0.5407894254 + 0.4538336098 0.5497955084 0.5424653888 + 0.4552780986 0.5516521931 0.5441265702 + 0.4567125142 0.5534964204 0.5457723737 + 0.4581381083 0.5553289056 0.5474044085 + 0.4595550001 0.5571491718 0.5490232706 + 0.4609611034 0.5589544773 0.5506296158 + 0.4623560905 0.5607442260 0.5522220135 + 0.4637416899 0.5625196099 0.5537992120 + 0.4651190937 0.5642817020 0.5553634167 + 0.4664886892 0.5660303235 0.5569143295 + 0.4678488970 0.5677667856 0.5584530830 + 0.4691990018 0.5694890022 0.5599805117 + 0.4705401957 0.5711963773 0.5614979267 + 0.4718737006 0.5728896260 0.5630066991 + 0.4731974900 0.5745667219 0.5645061731 + 0.4745135009 0.5762310028 0.5659909844 + 0.4758217931 0.5778843760 0.5674638748 + 0.4771224856 0.5795271993 0.5689262748 + 0.4784156084 0.5811589956 0.5703796744 + 0.4796993136 0.5827751756 0.5718243718 + 0.4809752107 0.5843808055 0.5732576847 + 0.4822460115 0.5859761834 0.5746794939 + 0.4835121036 0.5875607729 0.5760902762 + 0.4847720861 0.5891314745 0.5774900913 + 0.4860242903 0.5906872153 0.5788798928 + 0.4872705042 0.5922328234 0.5802617073 + 0.4885112047 0.5937690735 0.5816357136 + 0.4897463918 0.5952950716 0.5830007195 + 0.4909757972 0.5968127251 0.5843585134 + 0.4922001958 0.5983219147 0.5857095718 + 0.4934197068 0.5998219848 0.5870532990 + 0.4946334958 0.6013069153 0.5883836150 + 0.4958420992 0.6027836800 0.5897060037 + 0.4970462918 0.6042528749 0.5910214186 + 0.4982447922 0.6057118773 0.5923281908 + 0.4994386137 0.6071583033 0.5936253071 + 0.5006300211 0.6085960865 0.5949164033 + 0.5018193126 0.6100246906 0.5962014794 + 0.5030052066 0.6114413142 0.5974758863 + 0.5041871071 0.6128522754 0.5987439752 + 0.5053647161 0.6142578721 0.6000061035 + 0.5065345168 0.6156520247 0.6012579203 + 0.5077012181 0.6170390844 0.6025043726 + 0.5088649988 0.6184194088 0.6037459970 + 0.5100249052 0.6197851896 0.6049779058 + 0.5111827254 0.6211447716 0.6062039733 + 0.5123385787 0.6224983931 0.6074249148 + 0.5134863853 0.6238390207 0.6086357832 + 0.5146304965 0.6251735091 0.6098436117 + 0.5157698989 0.6265006065 0.6110476255 + 0.5169038177 0.6278185248 0.6122431159 + 0.5180357099 0.6291317940 0.6134337187 + 0.5191645026 0.6304373145 0.6146162748 + 0.5202901959 0.6317332983 0.6157922149 + 0.5214136243 0.6330240965 0.6169643998 + 0.5225296021 0.6343023181 0.6181287766 + 0.5236412883 0.6355733275 0.6192877889 + 0.5247489214 0.6368368864 0.6204416752 + 0.5258507133 0.6380882263 0.6215860248 + 0.5269507170 0.6393350959 0.6227265000 + 0.5280470252 0.6405732036 0.6238588095 + 0.5291407108 0.6418051720 0.6249852777 + 0.5302323103 0.6430323720 0.6261076927 + 0.5313162208 0.6442492008 0.6272221208 + 0.5323973894 0.6454623938 0.6283330917 + 0.5334717035 0.6466655731 0.6294345856 + 0.5345426798 0.6478630900 0.6305302978 + 0.5356101990 0.6490542889 0.6316205263 + 0.5366739035 0.6502388120 0.6327043772 + 0.5377362967 0.6514207721 0.6337860823 + 0.5387936831 0.6525934935 0.6348581910 + 0.5398488045 0.6537632942 0.6359266043 + 0.5408974290 0.6549249887 0.6369863153 + 0.5419415236 0.6560810804 0.6380411983 + 0.5429813266 0.6572313905 0.6390917897 + 0.5440160036 0.6583732963 0.6401355863 + 0.5450487137 0.6595116258 0.6411771178 + 0.5460764766 0.6606398225 0.6422092915 + 0.5471035242 0.6617661715 0.6432397962 + 0.5481255054 0.6628851295 0.6442614794 + 0.5491452217 0.6640012264 0.6452804804 + 0.5501583219 0.6651095748 0.6462923288 + 0.5511676073 0.6662127972 0.6473004222 + 0.5521715879 0.6673082113 0.6483036280 + 0.5531716943 0.6683982015 0.6493026018 + 0.5541684031 0.6694834828 0.6502984166 + 0.5551614761 0.6705622077 0.6512870789 + 0.5561527014 0.6716372967 0.6522722840 + 0.5571410060 0.6727038026 0.6532505155 + 0.5581284165 0.6737673879 0.6542266011 + 0.5591105223 0.6748213768 0.6551975012 + 0.5600913763 0.6758732796 0.6561676264 + 0.5610644817 0.6769148111 0.6571305990 + 0.5620365143 0.6779549718 0.6580929756 + 0.5630018115 0.6789849997 0.6590462923 + 0.5639665723 0.6800143123 0.6599991918 + 0.5649260283 0.6810361147 0.6609457731 + 0.5658853054 0.6820576191 0.6618922949 + 0.5668407083 0.6830697060 0.6628310084 + 0.5677961111 0.6840814948 0.6637696028 + 0.5687469244 0.6850817800 0.6646987200 + 0.5696974993 0.6860815883 0.6656274796 + 0.5706422925 0.6870731711 0.6665499806 + 0.5715867281 0.6880642176 0.6674721837 + 0.5725247860 0.6890473962 0.6683868766 + 0.5734621882 0.6900296211 0.6693009138 + 0.5743951201 0.6910039783 0.6702064872 + 0.5753269792 0.6919766068 0.6711105704 + 0.5762560964 0.6929423809 0.6720092893 + 0.5771843195 0.6939054728 0.6729062796 + 0.5781095028 0.6948626041 0.6737989187 + 0.5790330768 0.6958160996 0.6746891737 + 0.5799536109 0.6967654824 0.6755756736 + 0.5808709860 0.6977106929 0.6764587164 + 0.5817859769 0.6986532807 0.6773390174 + 0.5826966763 0.6995912194 0.6782149076 + 0.5836061835 0.7005271912 0.6790891886 + 0.5845115781 0.7014548779 0.6799582839 + 0.5854169130 0.7023826241 0.6808270812 + 0.5863186121 0.7033014894 0.6816880107 + 0.5872203112 0.7042204142 0.6825488806 + 0.5881202817 0.7051343918 0.6834049821 + 0.5890197754 0.7060474157 0.6842601895 + 0.5899165869 0.7069560289 0.6851115227 + 0.5908117294 0.7078614831 0.6859601736 + 0.5917043090 0.7087628841 0.6868060231 + 0.5925924778 0.7096565962 0.6876469851 + 0.5934802294 0.7105495930 0.6884872913 + 0.5943630934 0.7114354968 0.6893228889 + 0.5952460170 0.7123214006 0.6901584268 + 0.5961257815 0.7132012248 0.6909888983 + 0.5970050097 0.7140796185 0.6918182969 + 0.5978829861 0.7149519920 0.6926438212 + 0.5987598896 0.7158185244 0.6934657097 + 0.5996358991 0.7166832089 0.6942861080 + 0.6005082130 0.7175390124 0.6951001883 + 0.6013805866 0.7183946967 0.6959143281 + 0.6022471786 0.7192428708 0.6967222095 + 0.6031126976 0.7200896144 0.6975290179 + 0.6039757133 0.7209308743 0.6983324289 + 0.6048358083 0.7217659950 0.6991323233 + 0.6056957245 0.7226004004 0.6999315023 + 0.6065520048 0.7234249711 0.7007243037 + 0.6074082851 0.7242496014 0.7015169859 + 0.6082630754 0.7250695229 0.7023050785 + 0.6091169715 0.7258870006 0.7030910850 + 0.6099703908 0.7267029285 0.7038756013 + 0.6108219028 0.7275134921 0.7046552896 + 0.6116734743 0.7283241749 0.7054349184 + 0.6125208139 0.7291293740 0.7062094212 + 0.6133667827 0.7299330831 0.7069824934 + 0.6142112017 0.7307348251 0.7077537179 + 0.6150516868 0.7315317988 0.7085208297 + 0.6158921719 0.7323288918 0.7092878222 + 0.6167293787 0.7331209779 0.7100501060 + 0.6175658107 0.7339118123 0.7108113170 + 0.6184015274 0.7347002029 0.7115700245 + 0.6192352176 0.7354820967 0.7123227715 + 0.6200687885 0.7362639904 0.7130755782 + 0.6209005713 0.7370405197 0.7138231993 + 0.6217315793 0.7378150225 0.7145689726 + 0.6225616932 0.7385885119 0.7153139710 + 0.6233869195 0.7393574119 0.7160546780 + 0.6242122054 0.7401263118 0.7167953849 + 0.6250342131 0.7408906817 0.7175316215 + 0.6258537173 0.7416514754 0.7182646990 + 0.6266732216 0.7424122095 0.7189977765 + 0.6274886131 0.7431641817 0.7197247744 + 0.6283037066 0.7439156771 0.7204515934 + 0.6291179061 0.7446653247 0.7211769223 + 0.6299300790 0.7454105020 0.7218992710 + 0.6307423115 0.7461556792 0.7226216197 + 0.6315525770 0.7468963265 0.7233393788 + 0.6323618293 0.7476344109 0.7240548134 + 0.6331710219 0.7483724952 0.7247703075 + 0.6339744925 0.7491027117 0.7254779935 + 0.6347777843 0.7498325706 0.7261853814 + 0.6355797052 0.7505611777 0.7268915772 + 0.6363769770 0.7512856722 0.7275938988 + 0.6371744275 0.7520102859 0.7282962799 + 0.6379693747 0.7527318001 0.7289955020 + 0.6387621760 0.7534502149 0.7296919227 + 0.6395549774 0.7541685104 0.7303882241 + 0.6403455138 0.7548798919 0.7310792208 + 0.6411350965 0.7555885911 0.7317686081 + 0.6419246793 0.7562974095 0.7324578762 + 0.6427112222 0.7570006847 0.7331432104 + 0.6434975863 0.7577037215 0.7338283062 + 0.6442831755 0.7584059834 0.7345126271 + 0.6450635195 0.7591038942 0.7351925969 + 0.6458438039 0.7598018050 0.7358726263 + 0.6466221809 0.7604972720 0.7365499139 + 0.6473965049 0.7611867189 0.7372214794 + 0.6481707096 0.7618761063 0.7378931046 + 0.6489428878 0.7625625134 0.7385625839 + 0.6497123837 0.7632448077 0.7392295003 + 0.6504818797 0.7639272213 0.7398964167 + 0.6512495279 0.7646067142 0.7405610085 + 0.6520156264 0.7652835846 0.7412236929 + 0.6527817249 0.7659605145 0.7418864965 + 0.6535456181 0.7666330934 0.7425445914 + 0.6543081999 0.7673028111 0.7432000041 + 0.6550709009 0.7679725885 0.7438554168 + 0.6558291912 0.7686370015 0.7445067167 + 0.6565855742 0.7692987919 0.7451562285 + 0.6573420167 0.7699605227 0.7458056808 + 0.6580939889 0.7706184983 0.7464519739 + 0.6588441133 0.7712748051 0.7470970154 + 0.6595942974 0.7719311118 0.7477421165 + 0.6603410244 0.7725834250 0.7483826876 + 0.6610863209 0.7732340097 0.7490215898 + 0.6618314981 0.7738845944 0.7496606112 + 0.6625742912 0.7745307088 0.7502961755 + 0.6633158922 0.7751746774 0.7509304881 + 0.6640574932 0.7758187056 0.7515646815 + 0.6647968292 0.7764587998 0.7521967888 + 0.6655347943 0.7770963907 0.7528278232 + 0.6662726998 0.7777339816 0.7534586787 + 0.6670070887 0.7783685923 0.7540869117 + 0.6677386165 0.7790005803 0.7547128797 + 0.6684702039 0.7796326280 0.7553389072 + 0.6691988707 0.7802624106 0.7559623122 + 0.6699240804 0.7808893919 0.7565826774 + 0.6706492901 0.7815164924 0.7572029829 + 0.6713730097 0.7821415067 0.7578222752 + 0.6720930934 0.7827615142 0.7584391832 + 0.6728131771 0.7833815813 0.7590560913 + 0.6735327840 0.7840009928 0.7596722841 + 0.6742489934 0.7846158147 0.7602841854 + 0.6749653220 0.7852305174 0.7608960867 + 0.6756814718 0.7858452797 0.7615079880 + 0.6763954163 0.7864570022 0.7621135712 + 0.6771090031 0.7870684266 0.7627189159 + 0.6778227091 0.7876799107 0.7633240819 + 0.6785330772 0.7882875800 0.7639262080 + 0.6792423129 0.7888938189 0.7645270228 + 0.6799514294 0.7894999981 0.7651277781 + 0.6806576848 0.7901020050 0.7657262087 + 0.6813606024 0.7906988859 0.7663218975 + 0.6820635200 0.7912957072 0.7669175863 + 0.6827654839 0.7918915749 0.7675123215 + 0.6834632158 0.7924820781 0.7681027055 + 0.6841610074 0.7930727005 0.7686930895 + 0.6848587990 0.7936632037 0.7692834735 + 0.6855531931 0.7942507863 0.7698693871 + 0.6862471104 0.7948378921 0.7704545856 + 0.6869410276 0.7954249978 0.7710397840 + 0.6876335144 0.7960084081 0.7716224790 + 0.6883248091 0.7965881228 0.7722026706 + 0.6890159845 0.7971677780 0.7727829218 + 0.6897066236 0.7977464795 0.7733623981 + 0.6903933287 0.7983182073 0.7739374042 + 0.6910800934 0.7988898158 0.7745124102 + 0.6917667985 0.7994614840 0.7750874162 + 0.6924489737 0.8000293970 0.7756578922 + 0.6931294799 0.8005958796 0.7762268782 + 0.6938099861 0.8011624217 0.7767959237 + 0.6944890022 0.8017272949 0.7773637772 + 0.6951643825 0.8022881746 0.7779290080 + 0.6958398223 0.8028491139 0.7784942985 + 0.6965152025 0.8034101129 0.7790595293 + 0.6971874237 0.8039643764 0.7796214223 + 0.6978589892 0.8045170903 0.7801826000 + 0.6985304952 0.8050699234 0.7807437778 + 0.6992011070 0.8056213260 0.7813032866 + 0.6998698711 0.8061696291 0.7818598151 + 0.7005386949 0.8067178130 0.7824162841 + 0.7012075186 0.8072661161 0.7829726934 + 0.7018737197 0.8078114986 0.7835255265 + 0.7025392056 0.8083561063 0.7840774059 + 0.7032046914 0.8089007139 0.7846292853 + 0.7038692236 0.8094440103 0.7851802707 + 0.7045302987 0.8099824190 0.7857282162 + 0.7051913738 0.8105208278 0.7862762213 + 0.7058525085 0.8110591769 0.7868241072 + 0.7065104842 0.8115934730 0.7873699069 + 0.7071666718 0.8121252060 0.7879143953 + 0.7078229189 0.8126569986 0.7884590030 + 0.7084791064 0.8131887913 0.7890034914 + 0.7091314793 0.8137164712 0.7895429134 + 0.7097837925 0.8142442107 0.7900823951 + 0.7104362249 0.8147718906 0.7906218171 + 0.7110877037 0.8152979016 0.7911595106 + 0.7117375135 0.8158202767 0.7916939855 + 0.7123872042 0.8163427114 0.7922285199 + 0.7130370140 0.8168652058 0.7927629948 + 0.7136846781 0.8173835278 0.7932949066 + 0.7143312097 0.8178995252 0.7938255072 + 0.7149778008 0.8184155226 0.7943559885 + 0.7156242728 0.8189315200 0.7948865891 + 0.7162669897 0.8194420934 0.7954136133 + 0.7169092894 0.8199520707 0.7959402800 + 0.7175515890 0.8204621077 0.7964671254 + 0.7181931138 0.8209713101 0.7969928980 + 0.7188305855 0.8214758039 0.7975142002 + 0.7194681168 0.8219804168 0.7980353832 + 0.7201055884 0.8224849105 0.7985566258 + 0.7207418084 0.8229876161 0.7990764976 + 0.7213757038 0.8234868050 0.7995939851 + 0.7220094800 0.8239858747 0.8001114726 + 0.7226433158 0.8244851232 0.8006290197 + 0.7232757807 0.8249812126 0.8011451960 + 0.7239069939 0.8254746199 0.8016602993 + 0.7245383263 0.8259679079 0.8021754026 + 0.7251694798 0.8264611959 0.8026905060 + 0.7257997990 0.8269500136 0.8032016754 + 0.7264295220 0.8274360895 0.8037108779 + 0.7270591855 0.8279222846 0.8042200804 + 0.7276890278 0.8284084797 0.8047292829 + 0.7283164263 0.8288906813 0.8052350879 + 0.7289428711 0.8293713927 0.8057398200 + 0.7295694947 0.8298521042 0.8062443733 + 0.7301961184 0.8303328753 0.8067491055 + 0.7308188081 0.8308109045 0.8072515130 + 0.7314406037 0.8312882185 0.8077533245 + 0.7320623994 0.8317655921 0.8082550764 + 0.7326841950 0.8322429061 0.8087568879 + 0.7333028913 0.8327162862 0.8092550039 + 0.7339209914 0.8331888914 0.8097522855 + 0.7345389724 0.8336614966 0.8102496862 + 0.7351570725 0.8341341019 0.8107470274 + 0.7357730865 0.8346002102 0.8112403154 + 0.7363885045 0.8350648880 0.8117327094 + 0.7370039821 0.8355295062 0.8122249842 + 0.7376194000 0.8359941244 0.8127173781 + 0.7382336855 0.8364549279 0.8132079244 + 0.7388474941 0.8369144201 0.8136978149 + 0.7394614220 0.8373739719 0.8141875863 + 0.7400751710 0.8378335238 0.8146774769 + 0.7406880260 0.8382915258 0.8151652813 + 0.7413002849 0.8387488127 0.8156523108 + 0.7419126034 0.8392060995 0.8161392212 + 0.7425248027 0.8396633863 0.8166260719 + 0.7431349754 0.8401176929 0.8171094060 + 0.7437434196 0.8405694962 0.8175901175 + 0.7443519235 0.8410212994 0.8180707097 + 0.7449604273 0.8414732218 0.8185513020 + 0.7455673218 0.8419228196 0.8190304041 + 0.7461718917 0.8423690200 0.8195071220 + 0.7467764020 0.8428152204 0.8199838996 + 0.7473809719 0.8432614207 0.8204606771 + 0.7479851246 0.8437070847 0.8209370971 + 0.7485867739 0.8441503048 0.8214123249 + 0.7491886020 0.8445935845 0.8218876123 + 0.7497903705 0.8450369239 0.8223627806 + 0.7503920794 0.8454802036 0.8228381276 + 0.7509921193 0.8459206223 0.8233087063 + 0.7515920997 0.8463609219 0.8237791061 + 0.7521920204 0.8468011022 0.8242496252 + 0.7527918816 0.8472414017 0.8247200251 + 0.7533906102 0.8476775289 0.8251867890 + 0.7539888024 0.8481116891 0.8256520033 + 0.7545868754 0.8485457897 0.8261172175 + 0.7551851273 0.8489800096 0.8265823722 + 0.7557817101 0.8494123220 0.8270465136 + 0.7563759089 0.8498412967 0.8275086880 + 0.7569699883 0.8502702713 0.8279709816 + 0.7575641274 0.8506993055 0.8284332156 + 0.7581582069 0.8511282802 0.8288952708 + 0.7587482929 0.8515555859 0.8293542862 + 0.7593383789 0.8519830108 0.8298131824 + 0.7599285841 0.8524103165 0.8302720785 + 0.7605187297 0.8528376222 0.8307309747 + 0.7611069083 0.8532627225 0.8311864138 + 0.7616940141 0.8536863923 0.8316398859 + 0.7622811198 0.8541101217 0.8320934176 + 0.7628682852 0.8545337915 0.8325468898 + 0.7634549737 0.8549563885 0.8329998851 + 0.7640401125 0.8553745747 0.8334509134 + 0.7646251917 0.8557928205 0.8339020014 + 0.7652102113 0.8562110066 0.8343530297 + 0.7657952905 0.8566291928 0.8348039985 + 0.7663789988 0.8570433855 0.8352537155 + 0.7669621110 0.8574560881 0.8357028961 + 0.7675452232 0.8578686714 0.8361520767 + 0.7681282163 0.8582813144 0.8366013169 + 0.7687104940 0.8586933017 0.8370494843 + 0.7692893744 0.8591027856 0.8374937177 + 0.7698683143 0.8595123291 0.8379380107 + 0.7704473138 0.8599218130 0.8383821845 + 0.7710261941 0.8603312969 0.8388264775 + 0.7716023922 0.8607382774 0.8392679095 + 0.7721772194 0.8611438274 0.8397077918 + 0.7727519870 0.8615493774 0.8401476741 + 0.7733268142 0.8619549870 0.8405876756 + 0.7739014030 0.8623604178 0.8410274982 + 0.7744730115 0.8627613187 0.8414660096 + 0.7750446200 0.8631622195 0.8419045210 + 0.7756162882 0.8635631800 0.8423430920 + 0.7761878967 0.8639640808 0.8427816033 + 0.7767586112 0.8643633723 0.8432186246 + 0.7773280144 0.8647603989 0.8436536789 + 0.7778974175 0.8651574254 0.8440887928 + 0.7784668207 0.8655543923 0.8445239067 + 0.7790362835 0.8659514189 0.8449590206 + 0.7796040773 0.8663460016 0.8453900218 + 0.7801712751 0.8667395711 0.8458197117 + 0.7807385921 0.8671333194 0.8462492824 + 0.7813057899 0.8675270081 0.8466789722 + 0.7818729877 0.8679206967 0.8471086025 + 0.7824358940 0.8683102727 0.8475356102 + 0.7829989195 0.8686999083 0.8479626775 + 0.7835618258 0.8690894842 0.8483896852 + 0.7841247916 0.8694791198 0.8488166928 + 0.7846866846 0.8698676229 0.8492432833 + 0.7852454185 0.8702524900 0.8496683240 + 0.7858040929 0.8706374168 0.8500934243 + 0.7863628268 0.8710222244 0.8505184054 + 0.7869215012 0.8714070916 0.8509433866 + 0.7874788046 0.8717902899 0.8513662815 + 0.7880342007 0.8721709847 0.8517863750 + 0.7885895967 0.8725517988 0.8522065282 + 0.7891449928 0.8729326129 0.8526266217 + 0.7897003293 0.8733134270 0.8530467153 + 0.7902544141 0.8736922145 0.8534634113 + 0.7908074260 0.8740695715 0.8538776040 + 0.7913604975 0.8744469285 0.8542917967 + 0.7919135094 0.8748242259 0.8547059894 + 0.7924665213 0.8752015829 0.8551203012 + 0.7930179834 0.8755767941 0.8555328250 + 0.7935686111 0.8759508729 0.8559445739 + 0.7941192985 0.8763250709 0.8563563824 + 0.7946699858 0.8766993284 0.8567680717 + 0.7952206731 0.8770734072 0.8571798801 + 0.7957680821 0.8774452806 0.8575891852 + 0.7963144183 0.8778162003 0.8579975963 + 0.7968606949 0.8781871796 0.8584060073 + 0.7974069715 0.8785582185 0.8588144183 + 0.7979531884 0.8789290786 0.8592228293 + 0.7984963059 0.8792970777 0.8596280217 + 0.7990381718 0.8796638846 0.8600320816 + 0.7995802164 0.8800308108 0.8604362011 + 0.8001220822 0.8803976774 0.8608403206 + 0.8006640077 0.8807644844 0.8612444997 + 0.8012034893 0.8811290264 0.8616467714 + 0.8017420173 0.8814923167 0.8620483875 + 0.8022804856 0.8818557262 0.8624501228 + 0.8028190136 0.8822190762 0.8628516793 + 0.8033574820 0.8825824857 0.8632534146 + 0.8038944006 0.8829445243 0.8636533022 + 0.8044304252 0.8833057880 0.8640521765 + 0.8049663901 0.8836669922 0.8644511104 + 0.8055024147 0.8840281963 0.8648499846 + 0.8060383797 0.8843894005 0.8652489185 + 0.8065730929 0.8847498894 0.8656460047 + 0.8071064949 0.8851096034 0.8660413027 + 0.8076400161 0.8854691982 0.8664366007 + 0.8081734180 0.8858289123 0.8668318987 + 0.8087068796 0.8861885071 0.8672271967 + 0.8092389107 0.8865470290 0.8676213026 + 0.8097679019 0.8869025707 0.8680127263 + 0.8102967739 0.8872582912 0.8684040904 + 0.8108258247 0.8876140118 0.8687955141 + 0.8113546968 0.8879696727 0.8691868782 + 0.8118832111 0.8883249760 0.8695781231 + 0.8124076724 0.8886772990 0.8699674010 + 0.8129323125 0.8890296221 0.8703566790 + 0.8134567738 0.8893818855 0.8707460165 + 0.8139812946 0.8897342086 0.8711354136 + 0.8145058155 0.8900865912 0.8715246916 + 0.8150272965 0.8904371858 0.8719115257 + 0.8155483007 0.8907874227 0.8722978830 + 0.8160691857 0.8911375999 0.8726842999 + 0.8165901899 0.8914877772 0.8730705976 + 0.8171110749 0.8918380141 0.8734570146 + 0.8176307082 0.8921874762 0.8738412857 + 0.8181489706 0.8925362229 0.8742238879 + 0.8186672926 0.8928849101 0.8746063709 + 0.8191856146 0.8932335973 0.8749889135 + 0.8197038770 0.8935822845 0.8753713965 + 0.8202217817 0.8939304948 0.8757534027 + 0.8207373023 0.8942753077 0.8761324286 + 0.8212528825 0.8946201801 0.8765113950 + 0.8217685223 0.8949649930 0.8768904209 + 0.8222841024 0.8953098059 0.8772693872 + 0.8227996826 0.8956546187 0.8776484132 + 0.8233128786 0.8959960938 0.8780251145 + 0.8238251805 0.8963361979 0.8784009814 + 0.8243374825 0.8966763020 0.8787769079 + 0.8248497844 0.8970165253 0.8791527152 + 0.8253620863 0.8973566294 0.8795285821 + 0.8258733749 0.8976960182 0.8799036145 + 0.8263819218 0.8980333209 0.8802763820 + 0.8268905282 0.8983706236 0.8806490898 + 0.8273990154 0.8987079263 0.8810219169 + 0.8279076219 0.8990452886 0.8813946843 + 0.8284161091 0.8993825912 0.8817673922 + 0.8289219141 0.8997188807 0.8821375966 + 0.8294265866 0.9000548124 0.8825070262 + 0.8299313784 0.9003908038 0.8828763962 + 0.8304362297 0.9007266760 0.8832458258 + 0.8309410214 0.9010627270 0.8836151958 + 0.8314452171 0.9013980031 0.8839840293 + 0.8319470882 0.9017302990 0.8843502998 + 0.8324490190 0.9020625949 0.8847165704 + 0.8329507709 0.9023948908 0.8850830197 + 0.8334527016 0.9027271867 0.8854492903 + 0.8339545727 0.9030594826 0.8858156800 + 0.8344553113 0.9033885002 0.8861801028 + 0.8349552155 0.9037148952 0.8865432143 + 0.8354550004 0.9040412903 0.8869063258 + 0.8359549046 0.9043676853 0.8872693777 + 0.8364546895 0.9046940804 0.8876324296 + 0.8369545937 0.9050204754 0.8879954815 + 0.8374524713 0.9053437710 0.8883556724 + 0.8379502892 0.9056668282 0.8887156248 + 0.8384481072 0.9059898257 0.8890755177 + 0.8389458060 0.9063128233 0.8894352913 + 0.8394436240 0.9066357017 0.8897951841 + 0.8399409056 0.9069585204 0.8901546001 + 0.8404362202 0.9072805047 0.8905115724 + 0.8409314156 0.9076024294 0.8908686042 + 0.8414266706 0.9079242945 0.8912255764 + 0.8419219255 0.9082462192 0.8915826082 + 0.8424171209 0.9085682034 0.8919395804 + 0.8429111242 0.9088888168 0.8922954798 + 0.8434032798 0.9092074037 0.8926500082 + 0.8438953757 0.9095259905 0.8930044770 + 0.8443875909 0.9098446965 0.8933590055 + 0.8448796868 0.9101632833 0.8937134147 + 0.8453719020 0.9104819894 0.8940678835 + 0.8458622098 0.9107974172 0.8944209218 + 0.8463512063 0.9111105800 0.8947728276 + 0.8468402028 0.9114238024 0.8951247931 + 0.8473293185 0.9117369056 0.8954768181 + 0.8478183150 0.9120501280 0.8958287239 + 0.8483073115 0.9123632908 0.8961806893 + 0.8487946987 0.9126743078 0.8965306878 + 0.8492814898 0.9129843712 0.8968799114 + 0.8497682214 0.9132944942 0.8972290158 + 0.8502548933 0.9136046171 0.8975781798 + 0.8507416844 0.9139146805 0.8979274035 + 0.8512284160 0.9142248034 0.8982766271 + 0.8517140150 0.9145342708 0.8986228108 + 0.8521993160 0.9148436785 0.8989682794 + 0.8526844978 0.9151530266 0.8993136883 + 0.8531697989 0.9154623747 0.8996590972 + 0.8536550999 0.9157717228 0.9000046253 + 0.8541402817 0.9160810113 0.9003499746 + 0.8546243906 0.9163880944 0.9006927013 + 0.8551082015 0.9166945219 0.9010347128 + 0.8555920720 0.9170008898 0.9013767242 + 0.8560758829 0.9173073173 0.9017186761 + 0.8565596938 0.9176136851 0.9020606875 + 0.8570435047 0.9179201126 0.9024026990 + 0.8575248718 0.9182233214 0.9027428031 + 0.8580054045 0.9185255170 0.9030823112 + 0.8584859967 0.9188277125 0.9034218192 + 0.8589665890 0.9191299081 0.9037613273 + 0.8594471216 0.9194321036 0.9041007757 + 0.8599277139 0.9197342992 0.9044402838 + 0.8604061007 0.9200347066 0.9047783017 + 0.8608834743 0.9203342199 0.9051156044 + 0.8613607883 0.9206337929 0.9054529071 + 0.8618381023 0.9209333062 0.9057902098 + 0.8623154163 0.9212328196 0.9061275125 + 0.8627927899 0.9215323925 0.9064648151 + 0.8632687926 0.9218313098 0.9068005085 + 0.8637437224 0.9221296906 0.9071345925 + 0.8642185926 0.9224280119 0.9074686766 + 0.8646935225 0.9227263927 0.9078028202 + 0.8651683927 0.9230247736 0.9081369042 + 0.8656433225 0.9233232141 0.9084709883 + 0.8661177158 0.9236208200 0.9088042974 + 0.8665909171 0.9239168167 0.9091358781 + 0.8670641780 0.9242126942 0.9094673991 + 0.8675373793 0.9245086908 0.9097989202 + 0.8680106997 0.9248046279 0.9101303816 + 0.8684839010 0.9251006246 0.9104620218 + 0.8689571023 0.9253963828 0.9107934237 + 0.8694286942 0.9256886244 0.9111229181 + 0.8699004054 0.9259809256 0.9114524126 + 0.8703719974 0.9262732267 0.9117819071 + 0.8708435893 0.9265655279 0.9121115208 + 0.8713153005 0.9268577099 0.9124410152 + 0.8717868924 0.9271500111 0.9127705097 + 0.8722561002 0.9274398088 0.9130979180 + 0.8727244139 0.9277285933 0.9134243727 + 0.8731926084 0.9280173182 0.9137510061 + 0.8736609221 0.9283061028 0.9140775204 + 0.8741291761 0.9285948873 0.9144040942 + 0.8745974898 0.9288836718 0.9147307277 + 0.8750646114 0.9291719198 0.9150562882 + 0.8755295277 0.9294592738 0.9153801799 + 0.8759943843 0.9297466278 0.9157040715 + 0.8764593005 0.9300339222 0.9160280824 + 0.8769242167 0.9303212166 0.9163519740 + 0.8773891926 0.9306085110 0.9166759253 + 0.8778541088 0.9308958054 0.9169998169 + 0.8783167005 0.9311814904 0.9173220992 + 0.8787789941 0.9314669967 0.9176442027 + 0.8792414069 0.9317525029 0.9179663062 + 0.8797037005 0.9320380092 0.9182885289 + 0.8801661134 0.9323235154 0.9186105728 + 0.8806284070 0.9326090217 0.9189326763 + 0.8810901046 0.9328929186 0.9192541838 + 0.8815506101 0.9331744909 0.9195749164 + 0.8820111752 0.9334561825 0.9198955297 + 0.8824717999 0.9337378144 0.9202160835 + 0.8829324245 0.9340193868 0.9205368161 + 0.8833928704 0.9343010783 0.9208574295 + 0.8838534951 0.9345827103 0.9211781025 + 0.8843126297 0.9348607063 0.9214954972 + 0.8847714067 0.9351381063 0.9218124747 + 0.8852301836 0.9354155064 0.9221295118 + 0.8856890202 0.9356929064 0.9224464297 + 0.8861477971 0.9359703064 0.9227634072 + 0.8866065741 0.9362477064 0.9230803847 + 0.8870645761 0.9365242720 0.9233967066 + 0.8875198960 0.9367982149 0.9237111211 + 0.8879752755 0.9370720983 0.9240254760 + 0.8884305954 0.9373459816 0.9243398905 + 0.8888859153 0.9376199245 0.9246544242 + 0.8893412948 0.9378938079 0.9249687791 + 0.8897966146 0.9381676912 0.9252831936 + 0.8902500272 0.9384400249 0.9255968928 + 0.8907018900 0.9387111068 0.9259101152 + 0.8911538124 0.9389823079 0.9262232780 + 0.8916056752 0.9392533898 0.9265365005 + 0.8920575976 0.9395244718 0.9268497229 + 0.8925095201 0.9397956133 0.9271628857 + 0.8929613829 0.9400668144 0.9274761081 + 0.8934110999 0.9403352737 0.9277874827 + 0.8938602805 0.9406033158 0.9280986190 + 0.8943095207 0.9408712983 0.9284096956 + 0.8947588205 0.9411392808 0.9287207127 + 0.8952080011 0.9414073229 0.9290317893 + 0.8956571817 0.9416753054 0.9293429255 + 0.8961064219 0.9419432282 0.9296538234 + 0.8965536952 0.9422078133 0.9299619198 + 0.8970010281 0.9424722791 0.9302698970 + 0.8974484205 0.9427369237 0.9305778742 + 0.8978956938 0.9430013895 0.9308859110 + 0.8983430266 0.9432659149 0.9311940074 + 0.8987902999 0.9435304999 0.9315019846 + 0.8992372751 0.9437944889 0.9318097234 + 0.8996827006 0.9440562725 0.9321159720 + 0.9001281261 0.9443179965 0.9324222803 + 0.9005734921 0.9445797801 0.9327287078 + 0.9010189772 0.9448416233 0.9330350161 + 0.9014644027 0.9451032877 0.9333413243 + 0.9019098282 0.9453650713 0.9336475730 + 0.9023545980 0.9456260204 0.9339538217 + 0.9027975798 0.9458845258 0.9342594147 + 0.9032405019 0.9461429715 0.9345651269 + 0.9036834836 0.9464014769 0.9348707795 + 0.9041265249 0.9466599822 0.9351763725 + 0.9045695066 0.9469184875 0.9354820848 + 0.9050124884 0.9471769929 0.9357876778 + 0.9054549932 0.9474347234 0.9360926151 + 0.9058961868 0.9476901293 0.9363955855 + 0.9063374996 0.9479454756 0.9366984963 + 0.9067788124 0.9482008815 0.9370014071 + 0.9072200060 0.9484562874 0.9373043180 + 0.9076613188 0.9487115741 0.9376072288 + 0.9081025720 0.9489669800 0.9379101992 + 0.9085432887 0.9492217898 0.9382125139 + 0.9089823961 0.9494745135 0.9385129809 + 0.9094215035 0.9497271776 0.9388133883 + 0.9098604918 0.9499800205 0.9391139150 + 0.9102995992 0.9502326846 0.9394143820 + 0.9107387066 0.9504854083 0.9397147894 + 0.9111778140 0.9507380724 0.9400153160 + 0.9116166234 0.9509906173 0.9403156042 + 0.9120534062 0.9512413144 0.9406144023 + 0.9124903083 0.9514918923 0.9409132004 + 0.9129270911 0.9517425895 0.9412119985 + 0.9133639932 0.9519932866 0.9415107965 + 0.9138007760 0.9522439837 0.9418095946 + 0.9142376781 0.9524946809 0.9421083927 + 0.9146745205 0.9527453780 0.9424071908 + 0.9151092768 0.9529926777 0.9427046776 + 0.9155439138 0.9532396197 0.9430022240 + 0.9159784913 0.9534866214 0.9432995915 + 0.9164131284 0.9537336230 0.9435970187 + 0.9168477058 0.9539806247 0.9438943863 + 0.9172822833 0.9542276263 0.9441918135 + 0.9177169800 0.9544746280 0.9444893003 + 0.9181497097 0.9547194839 0.9447842836 + 0.9185816050 0.9549633861 0.9450783730 + 0.9190135002 0.9552074075 0.9453724027 + 0.9194452763 0.9554513097 0.9456663728 + 0.9198771715 0.9556952715 0.9459604025 + 0.9203091264 0.9559391737 0.9462544918 + 0.9207410216 0.9561831951 0.9465485215 + 0.9211720824 0.9564263821 0.9468415976 + 0.9216020107 0.9566686153 0.9471331835 + 0.9220318198 0.9569107294 0.9474248886 + 0.9224616289 0.9571527839 0.9477164745 + 0.9228914976 0.9573948979 0.9480081797 + 0.9233213067 0.9576370120 0.9482998252 + 0.9237511754 0.9578791857 0.9485914707 + 0.9241809845 0.9581212997 0.9488831162 + 0.9246084094 0.9583622217 0.9491736293 + 0.9250357151 0.9586032033 0.9494642019 + 0.9254630804 0.9588441253 0.9497547150 + 0.9258903861 0.9590849876 0.9500452280 + 0.9263178110 0.9593259096 0.9503356814 + 0.9267451167 0.9595668912 0.9506263137 + 0.9271724820 0.9598078132 0.9509168267 + 0.9275984168 0.9600471854 0.9512062073 + 0.9280232787 0.9602853060 0.9514948130 + 0.9284480810 0.9605233073 0.9517832994 + 0.9288728833 0.9607614279 0.9520719051 + 0.9292978048 0.9609994888 0.9523603916 + 0.9297226071 0.9612376094 0.9526489973 + 0.9301474094 0.9614756703 0.9529374838 + 0.9305720925 0.9617136121 0.9532259107 + 0.9309926033 0.9619483948 0.9535105228 + 0.9314129949 0.9621831179 0.9537950754 + 0.9318335056 0.9624179006 0.9540796876 + 0.9322540164 0.9626526237 0.9543642998 + 0.9326745272 0.9628874063 0.9546489120 + 0.9330949187 0.9631221294 0.9549335837 + 0.9335154295 0.9633569121 0.9552181959 + 0.9339327216 0.9635908008 0.9555017948 + 0.9343463778 0.9638239145 0.9557842016 + 0.9347600937 0.9640570283 0.9560666084 + 0.9351738095 0.9642900229 0.9563490152 + 0.9355874062 0.9645230770 0.9566314220 + 0.9360011220 0.9647560716 0.9569138288 + 0.9364147782 0.9649891853 0.9571961761 + 0.9368284941 0.9652221799 0.9574785829 + 0.9372366071 0.9654536247 0.9577592015 + 0.9376435280 0.9656844735 0.9580392838 + 0.9380503893 0.9659153223 0.9583194256 + 0.9384573102 0.9661461711 0.9585996270 + 0.9388641119 0.9663770795 0.9588797092 + 0.9392709732 0.9666079879 0.9591597915 + 0.9396778941 0.9668388963 0.9594399929 + 0.9400843978 0.9670693874 0.9597195983 + 0.9404869080 0.9672951102 0.9599938989 + 0.9408894777 0.9675207734 0.9602683187 + 0.9412919879 0.9677464962 0.9605426192 + 0.9416946173 0.9679722190 0.9608169794 + 0.9420971870 0.9681978822 0.9610913992 + 0.9424996972 0.9684236050 0.9613656998 + 0.9429023266 0.9686493278 0.9616401196 + 0.9433040023 0.9688737988 0.9619129896 + 0.9437037706 0.9690952897 0.9621827006 + 0.9441037178 0.9693168998 0.9624524713 + 0.9445034862 0.9695385098 0.9627221823 + 0.9449033141 0.9697600007 0.9629918933 + 0.9453032017 0.9699816108 0.9632616043 + 0.9457030296 0.9702031016 0.9635313153 + 0.9461029172 0.9704247117 0.9638010263 + 0.9465014935 0.9706450105 0.9640696049 + 0.9468985796 0.9708636999 0.9643365741 + 0.9472957253 0.9710823298 0.9646036029 + 0.9476928711 0.9713010192 0.9648706913 + 0.9480900168 0.9715197086 0.9651377201 + 0.9484871030 0.9717382789 0.9654048085 + 0.9488843083 0.9719570279 0.9656717777 + 0.9492813945 0.9721757174 0.9659388065 + 0.9496768117 0.9723932743 0.9662050009 + 0.9500706792 0.9726098180 0.9664703012 + 0.9504644871 0.9728264213 0.9667354822 + 0.9508582950 0.9730429053 0.9670007825 + 0.9512521029 0.9732595086 0.9672660828 + 0.9516459107 0.9734759927 0.9675313830 + 0.9520397782 0.9736925960 0.9677966237 + 0.9524335861 0.9739090800 0.9680619240 + 0.9528253078 0.9741246104 0.9683256745 + 0.9532151222 0.9743391275 0.9685881138 + 0.9536048174 0.9745535254 0.9688504934 + 0.9539945126 0.9747679830 0.9691128731 + 0.9543843269 0.9749823809 0.9693753123 + 0.9547740221 0.9751967788 0.9696376920 + 0.9551637173 0.9754112959 0.9699000716 + 0.9555534720 0.9756256938 0.9701625109 + 0.9559414983 0.9758388996 0.9704235196 + 0.9563273191 0.9760503173 0.9706826210 + 0.9567130208 0.9762617946 0.9709417224 + 0.9570987821 0.9764732122 0.9712008238 + 0.9574846029 0.9766845703 0.9714599848 + 0.9578703046 0.9768959880 0.9717190862 + 0.9582561255 0.9771075249 0.9719781876 + 0.9586418867 0.9773188829 0.9722372890 + 0.9590265751 0.9775292873 0.9724956751 + 0.9594085217 0.9777370095 0.9727519751 + 0.9597904086 0.9779447913 0.9730082750 + 0.9601722956 0.9781525135 0.9732645750 + 0.9605541825 0.9783601761 0.9735208750 + 0.9609361291 0.9785678983 0.9737771749 + 0.9613180161 0.9787756801 0.9740334749 + 0.9616999030 0.9789834023 0.9742897749 + 0.9620816708 0.9791910052 0.9745460153 + 0.9624596238 0.9793955088 0.9747992754 + 0.9628375769 0.9796000719 0.9750527143 + 0.9632155895 0.9798046947 0.9753059745 + 0.9635934830 0.9800091982 0.9755592942 + 0.9639714956 0.9802138209 0.9758126140 + 0.9643493891 0.9804183841 0.9760659933 + 0.9647274017 0.9806228876 0.9763193130 + 0.9651054144 0.9808275104 0.9765725732 + 0.9654802084 0.9810302258 0.9768230915 + 0.9658539295 0.9812322855 0.9770727158 + 0.9662274718 0.9814344049 0.9773222208 + 0.9666011930 0.9816365242 0.9775717854 + 0.9669749141 0.9818385839 0.9778214097 + 0.9673485756 0.9820407033 0.9780709147 + 0.9677222967 0.9822428226 0.9783204794 + 0.9680960178 0.9824448824 0.9785699844 + 0.9684677720 0.9826459289 0.9788182974 + 0.9688364863 0.9828448296 0.9790642262 + 0.9692050815 0.9830436707 0.9793102145 + 0.9695736766 0.9832426906 0.9795560837 + 0.9699423909 0.9834415913 0.9798020720 + 0.9703109860 0.9836406112 0.9800480008 + 0.9706795812 0.9838395119 0.9802939892 + 0.9710482955 0.9840384126 0.9805399179 + 0.9714168906 0.9842373729 0.9807859063 + 0.9717804790 0.9844332933 0.9810283780 + 0.9721437097 0.9846289158 0.9812706113 + 0.9725068212 0.9848244786 0.9815129042 + 0.9728699923 0.9850201011 0.9817550778 + 0.9732331038 0.9852156043 0.9819973707 + 0.9735962152 0.9854112267 0.9822396040 + 0.9739593863 0.9856067896 0.9824817777 + 0.9743224978 0.9858024120 0.9827240705 + 0.9746835232 0.9859969020 0.9829645157 + 0.9750407934 0.9861896038 0.9832020998 + 0.9753981233 0.9863823056 0.9834396839 + 0.9757553935 0.9865750074 0.9836772084 + 0.9761127234 0.9867678285 0.9839147925 + 0.9764701128 0.9869604707 0.9841523767 + 0.9768273830 0.9871531725 0.9843899012 + 0.9771847129 0.9873458743 0.9846274853 + 0.9775419831 0.9875385761 0.9848651290 + 0.9778943062 0.9877287149 0.9850988984 + 0.9782450199 0.9879180193 0.9853315949 + 0.9785957932 0.9881073236 0.9855642915 + 0.9789465070 0.9882966280 0.9857969284 + 0.9792972803 0.9884859920 0.9860296249 + 0.9796479940 0.9886752963 0.9862623215 + 0.9799987078 0.9888646007 0.9864950180 + 0.9803494811 0.9890539050 0.9867277145 + 0.9806995988 0.9892429113 0.9869599938 + 0.9810422063 0.9894282222 0.9871879816 + 0.9813848138 0.9896134138 0.9874159098 + 0.9817274213 0.9897987247 0.9876437783 + 0.9820700288 0.9899839163 0.9878718257 + 0.9824125767 0.9901692271 0.9880996943 + 0.9827551246 0.9903544188 0.9883276820 + 0.9830976725 0.9905397296 0.9885556102 + 0.9834402800 0.9907249212 0.9887835979 + 0.9837800264 0.9909089208 0.9890096188 + 0.9841130972 0.9910902977 0.9892314076 + 0.9844462276 0.9912716150 0.9894531965 + 0.9847792983 0.9914529920 0.9896751046 + 0.9851124287 0.9916343093 0.9898968935 + 0.9854454994 0.9918156266 0.9901186824 + 0.9857786298 0.9919970036 0.9903404713 + 0.9861117005 0.9921783209 0.9905623198 + 0.9864447713 0.9923596978 0.9907841086 + 0.9867734909 0.9925395250 0.9910029769 + 0.9870970845 0.9927175045 0.9912183881 + 0.9874207973 0.9928954840 0.9914339185 + 0.9877445102 0.9930735230 0.9916493297 + 0.9880681038 0.9932515025 0.9918648005 + 0.9883918166 0.9934294820 0.9920802116 + 0.9887155294 0.9936075211 0.9922956824 + 0.9890391231 0.9937855005 0.9925110936 + 0.9893627763 0.9939634800 0.9927266240 + 0.9896805286 0.9941388965 0.9929388165 + 0.9899929762 0.9943121076 0.9931482077 + 0.9903056026 0.9944853187 0.9933575988 + 0.9906181097 0.9946585298 0.9935669899 + 0.9909306765 0.9948316813 0.9937765002 + 0.9912433028 0.9950047731 0.9939858913 + 0.9915558100 0.9951779842 0.9941952825 + 0.9918683767 0.9953511953 0.9944046736 + 0.9921808839 0.9955244064 0.9946141243 + 0.9924861789 0.9956938028 0.9948195219 + 0.9927850962 0.9958598018 0.9950211048 + 0.9930838943 0.9960258007 0.9952228069 + 0.9933826923 0.9961917996 0.9954243898 + 0.9936814904 0.9963577986 0.9956260920 + 0.9939802885 0.9965237975 0.9958277941 + 0.9942790866 0.9966899157 0.9960293770 + 0.9945780039 0.9968559146 0.9962310791 + 0.9948768020 0.9970219135 0.9964327216 + 0.9951673746 0.9971848130 0.9966300130 + 0.9954472184 0.9973435998 0.9968215227 + 0.9957271218 0.9975023866 0.9970129728 + 0.9960069060 0.9976611733 0.9972046018 + 0.9962868094 0.9978200197 0.9973961115 + 0.9965665936 0.9979788065 0.9975876212 + 0.9968464971 0.9981375933 0.9977790713 + 0.9971262813 0.9982963800 0.9979707003 + 0.9974061847 0.9984552264 0.9981622100 + 0.9976803064 0.9986127019 0.9983515143 + 0.9979380965 0.9987668991 0.9985346794 + 0.9981958270 0.9989209771 0.9987179041 + 0.9984536171 0.9990751743 0.9989010096 + 0.9987112880 0.9992293119 0.9990841746 + 0.9989690185 0.9993833899 0.9992673993 + 0.9992268085 0.9995375872 0.9994505048 + 0.9994844794 0.9996917248 0.9996337295 + 0.9997423291 0.9998459220 0.9998167753 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_vista_100.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_vista_100.spi1d index 18c00d1d..b61f55b4 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_vista_100.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_vista_100.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0090248538 0.0088125290 0.0067258808 - 0.0172150191 0.0167176109 0.0131098796 - 0.0243747793 0.0235208403 0.0187523402 - 0.0308876392 0.0297362003 0.0239069704 - 0.0369180292 0.0355890803 0.0287567694 - 0.0425458290 0.0411948487 0.0333772190 - 0.0478529185 0.0466255695 0.0378036089 - 0.0528910905 0.0519192591 0.0420932695 - 0.0576865897 0.0571291298 0.0462520085 - 0.0623513199 0.0622628704 0.0503120609 - 0.0669768602 0.0672920421 0.0542953908 - 0.0715567619 0.0721824616 0.0582090616 - 0.0760855973 0.0769187585 0.0620570593 - 0.0805741325 0.0815202296 0.0658472329 - 0.0850307494 0.0859836414 0.0695805177 - 0.0894547775 0.0903253108 0.0732729286 - 0.0938297287 0.0945612267 0.0769176036 - 0.0981539637 0.0986936092 0.0805180594 - 0.1024200022 0.1027287021 0.0840807334 - 0.1066194996 0.1066882983 0.0876046494 - 0.1107435003 0.1105668023 0.0910888687 - 0.1148013026 0.1143577993 0.0945325121 - 0.1187836006 0.1180787012 0.0979359895 - 0.1227072999 0.1217278987 0.1012983993 - 0.1265697032 0.1253149062 0.1046219021 - 0.1303700954 0.1288353056 0.1078997031 - 0.1340970993 0.1322914958 0.1111387014 - 0.1377590001 0.1356990933 0.1143449992 - 0.1413495988 0.1390592009 0.1175180003 - 0.1448680013 0.1423660964 0.1206526011 - 0.1483224928 0.1456252038 0.1237441003 - 0.1516993940 0.1488344073 0.1267928034 - 0.1550139934 0.1519985944 0.1298023015 - 0.1582762003 0.1551181972 0.1327774972 - 0.1614779979 0.1581996977 0.1357243061 - 0.1646181941 0.1612505019 0.1386376023 - 0.1676993966 0.1642563939 0.1415113956 - 0.1707275957 0.1672241986 0.1443507969 - 0.1737034023 0.1701561958 0.1471655965 - 0.1766317040 0.1730598062 0.1499509066 - 0.1795026064 0.1759244055 0.1527041048 - 0.1823239028 0.1787561029 0.1554242969 - 0.1850987971 0.1815645993 0.1581097990 - 0.1878297031 0.1843390018 0.1607618928 - 0.1905121952 0.1870823056 0.1633782983 - 0.1931522936 0.1898010969 0.1659625024 - 0.1957568973 0.1924955994 0.1685173064 - 0.1983253062 0.1951646954 0.1710429937 - 0.2008654028 0.1978061944 0.1735426039 - 0.2033737004 0.2004275024 0.1760165989 - 0.2058539987 0.2030244023 0.1784628034 - 0.2083114982 0.2055975944 0.1808815002 - 0.2107529044 0.2081506997 0.1832714975 - 0.2131794989 0.2106831074 0.1856442988 - 0.2155988961 0.2131962031 0.1879992038 - 0.2180117071 0.2156914026 0.1903316975 - 0.2204127014 0.2181655020 0.1926410943 - 0.2228053063 0.2206185013 0.1949304938 - 0.2251946926 0.2230494022 0.1971997023 - 0.2275789976 0.2254669964 0.1994490027 - 0.2299547046 0.2278635949 0.2016751021 - 0.2323268950 0.2302404940 0.2038796991 - 0.2346891016 0.2326005995 0.2060652971 - 0.2370416969 0.2349402010 0.2082331926 - 0.2393849939 0.2372657061 0.2103780955 - 0.2417193949 0.2395751029 0.2125052065 - 0.2440440953 0.2418743074 0.2146157026 - 0.2463590056 0.2441594005 0.2167119980 - 0.2486687005 0.2464269996 0.2187922001 - 0.2509658039 0.2486840934 0.2208545059 - 0.2532486022 0.2509253025 0.2229007035 - 0.2555230856 0.2531493902 0.2249286026 - 0.2577824891 0.2553615868 0.2269378006 - 0.2600266933 0.2575596869 0.2289337069 - 0.2622607946 0.2597441971 0.2309127003 - 0.2644844949 0.2619175017 0.2328774929 - 0.2667022049 0.2640810013 0.2348311990 - 0.2689101100 0.2662318051 0.2367679030 - 0.2711063027 0.2683725059 0.2386890054 - 0.2732926905 0.2705014944 0.2405951023 - 0.2754707932 0.2726207078 0.2424882054 - 0.2776342928 0.2747271955 0.2443652004 - 0.2797912061 0.2768242061 0.2462334931 - 0.2819392085 0.2789101899 0.2480895966 - 0.2840760946 0.2809852958 0.2499316037 - 0.2862043977 0.2830494046 0.2517591119 - 0.2883251011 0.2851034105 0.2535771132 - 0.2904368043 0.2871474028 0.2553840876 - 0.2925382853 0.2891792059 0.2571800053 - 0.2946313024 0.2912011147 0.2589674890 - 0.2967143953 0.2932142019 0.2607443035 - 0.2987858057 0.2952139080 0.2625100017 - 0.3008505106 0.2972015142 0.2642692924 - 0.3029071987 0.2991822064 0.2660228014 - 0.3049511909 0.3011547029 0.2677705884 - 0.3069872856 0.3031156063 0.2695144117 - 0.3090176880 0.3050638139 0.2712531090 - 0.3110390007 0.3070029020 0.2729856968 - 0.3130499125 0.3089343011 0.2747122049 - 0.3150539994 0.3108555079 0.2764343023 - 0.3170512021 0.3127650023 0.2781510949 - 0.3190402091 0.3146660924 0.2798615098 - 0.3210211992 0.3165611923 0.2815661132 - 0.3229936957 0.3184478879 0.2832651138 - 0.3249579072 0.3203234971 0.2849591076 - 0.3269129992 0.3221895099 0.2866472900 - 0.3288631141 0.3240458071 0.2883332074 - 0.3308089077 0.3258920908 0.2900170982 - 0.3327476978 0.3277291059 0.2916966081 - 0.3346762955 0.3295575976 0.2933689058 - 0.3365967870 0.3313786089 0.2950361967 - 0.3385129869 0.3331907094 0.2967014909 - 0.3404223025 0.3349941075 0.2983629107 - 0.3423224092 0.3367873132 0.3000209033 - 0.3442116082 0.3385719061 0.3016754985 - 0.3460933864 0.3403505981 0.3033275902 - 0.3479698002 0.3421216011 0.3049786985 - 0.3498382866 0.3438830078 0.3066262007 - 0.3516973853 0.3456346095 0.3082687855 - 0.3535484076 0.3473792970 0.3099066913 - 0.3553937972 0.3491171896 0.3115408123 - 0.3572331965 0.3508478105 0.3131729960 - 0.3590649962 0.3525711000 0.3148024082 - 0.3608897924 0.3542861938 0.3164277971 - 0.3627074957 0.3559936881 0.3180496991 - 0.3645170927 0.3576950133 0.3196698129 - 0.3663190007 0.3593909144 0.3212881088 - 0.3681136072 0.3610796034 0.3229044080 - 0.3699010015 0.3627603948 0.3245167136 - 0.3716816008 0.3644332886 0.3261250854 - 0.3734537065 0.3660990894 0.3277314901 - 0.3752183020 0.3677594066 0.3293341994 - 0.3769755960 0.3694128990 0.3309333026 - 0.3787257969 0.3710587025 0.3325302899 - 0.3804695010 0.3726957142 0.3341256976 - 0.3822059929 0.3743239939 0.3357195854 - 0.3839339912 0.3759444058 0.3373118043 - 0.3856526017 0.3775574863 0.3389019072 - 0.3873623013 0.3791646957 0.3404884040 - 0.3890652955 0.3807657957 0.3420729041 - 0.3907626867 0.3823603094 0.3436552882 - 0.3924534023 0.3839474916 0.3452340066 - 0.3941374123 0.3855277002 0.3468089998 - 0.3958151042 0.3871026039 0.3483793139 - 0.3974870145 0.3886739910 0.3499442935 - 0.3991512060 0.3902403116 0.3515053093 - 0.4008058012 0.3918004930 0.3530634046 - 0.4024530053 0.3933542967 0.3546175063 - 0.4040930867 0.3949011862 0.3561685979 - 0.4057264924 0.3964413106 0.3577178121 - 0.4073539078 0.3979749084 0.3592661023 - 0.4089717865 0.3995010853 0.3608109057 - 0.4105815887 0.4010227919 0.3623529971 - 0.4121825993 0.4025394022 0.3638921082 - 0.4137746990 0.4040510952 0.3654282987 - 0.4153572917 0.4055579901 0.3669610918 - 0.4169301987 0.4070582092 0.3684917986 - 0.4184952080 0.4085538983 0.3700191975 - 0.4200516045 0.4100449979 0.3715431988 - 0.4215987027 0.4115310907 0.3730638027 - 0.4231320918 0.4130091071 0.3745814860 - 0.4246563911 0.4144822061 0.3760955930 - 0.4261722863 0.4159503877 0.3776068985 - 0.4276798964 0.4174129963 0.3791159987 - 0.4291754067 0.4188691974 0.3806217909 - 0.4306622148 0.4203203917 0.3821237981 - 0.4321402907 0.4217669070 0.3836225867 - 0.4336062968 0.4232077003 0.3851186037 - 0.4350613952 0.4246425927 0.3866108060 - 0.4365088940 0.4260734916 0.3881003857 - 0.4379490912 0.4275006056 0.3895873129 - 0.4393770993 0.4289224148 0.3910715878 - 0.4407972097 0.4303407967 0.3925527930 - 0.4422096014 0.4317556918 0.3940311074 - 0.4436098039 0.4331642091 0.3955051005 - 0.4449998140 0.4345675111 0.3969767988 - 0.4463821948 0.4359666109 0.3984459937 - 0.4477544129 0.4373601079 0.3999111950 - 0.4491179883 0.4387483895 0.4013735950 - 0.4504755139 0.4401332140 0.4028339982 - 0.4518243968 0.4415132105 0.4042919874 - 0.4531635940 0.4428895116 0.4057449996 - 0.4544965029 0.4442631900 0.4071955085 - 0.4558199048 0.4456324875 0.4086431861 - 0.4571351111 0.4469971061 0.4100872874 - 0.4584454000 0.4483583868 0.4115292132 - 0.4597485065 0.4497137070 0.4129675031 - 0.4610452950 0.4510636032 0.4144037068 - 0.4623377919 0.4524100125 0.4158385098 - 0.4636217058 0.4537501931 0.4172705114 - 0.4649001062 0.4550873041 0.4187003970 - 0.4661735892 0.4564214945 0.4201287925 - 0.4674373865 0.4577501118 0.4215534031 - 0.4686970115 0.4590756893 0.4229756892 - 0.4699491858 0.4603962004 0.4243943989 - 0.4711951911 0.4617125988 0.4258098900 - 0.4724380970 0.4630267918 0.4272238910 - 0.4736748040 0.4643347859 0.4286344051 - 0.4749085009 0.4656395018 0.4300416112 - 0.4761387110 0.4669404924 0.4314455986 - 0.4773631096 0.4682346880 0.4328455925 - 0.4785852134 0.4695264101 0.4342440069 - 0.4798015058 0.4708124995 0.4356383979 - 0.4810144007 0.4720954001 0.4370301068 - 0.4822235107 0.4733743072 0.4384193122 - 0.4834271073 0.4746474028 0.4398039877 - 0.4846287966 0.4759184122 0.4411869943 - 0.4858255088 0.4771836102 0.4425632954 - 0.4870207012 0.4784463942 0.4439373910 - 0.4882127941 0.4797044098 0.4453079998 - 0.4894016981 0.4809592068 0.4466761053 - 0.4905884862 0.4822115898 0.4480428994 - 0.4917714894 0.4834589958 0.4494043887 - 0.4929533005 0.4847050011 0.4507643878 - 0.4941304028 0.4859468937 0.4521192908 - 0.4953059852 0.4871870875 0.4534727931 - 0.4964784980 0.4884232879 0.4548236132 - 0.4976483881 0.4896576107 0.4561721087 - 0.4988156855 0.4908897877 0.4575186968 - 0.4999799132 0.4921190143 0.4588620961 - 0.5011426806 0.4933466911 0.4602043927 - 0.5023010969 0.4945699871 0.4615409970 - 0.5034586191 0.4957925081 0.4628765881 - 0.5046117902 0.4970094860 0.4642063081 - 0.5057641864 0.4982253909 0.4655351043 - 0.5069122910 0.4994361997 0.4668604136 - 0.5080590844 0.5006449223 0.4681833088 - 0.5092027783 0.5018489957 0.4695020914 - 0.5103443861 0.5030499101 0.4708178043 - 0.5114830136 0.5042467117 0.4721299112 - 0.5126193762 0.5054402947 0.4734382927 - 0.5137537122 0.5066308975 0.4747439027 - 0.5148848891 0.5078179240 0.4760439992 - 0.5160142183 0.5090023875 0.4773412049 - 0.5171405077 0.5101822019 0.4786339104 - 0.5182654262 0.5113596916 0.4799247086 - 0.5193868279 0.5125321746 0.4812121093 - 0.5205066800 0.5137025714 0.4824982882 - 0.5216233730 0.5148683786 0.4837796092 - 0.5227389932 0.5160326958 0.4850594997 - 0.5238509178 0.5171926022 0.4863342941 - 0.5249615908 0.5183511972 0.4876079857 - 0.5260689855 0.5195046067 0.4888750911 - 0.5271754265 0.5206564069 0.4901408851 - 0.5282781124 0.5218034983 0.4914003909 - 0.5293796062 0.5229489207 0.4926582873 - 0.5304781199 0.5240889788 0.4939117134 - 0.5315753818 0.5252268910 0.4951637089 - 0.5326694250 0.5263600945 0.4964101911 - 0.5337619185 0.5274910927 0.4976545870 - 0.5348516703 0.5286170840 0.4988938868 - 0.5359399915 0.5297397971 0.5001304746 - 0.5370256901 0.5308594704 0.5013629794 - 0.5381091237 0.5319762826 0.5025923252 - 0.5391908288 0.5330902934 0.5038173795 - 0.5402699709 0.5342003703 0.5050377846 - 0.5413478017 0.5353085995 0.5062550902 - 0.5424222946 0.5364122987 0.5074666739 - 0.5434960723 0.5375146866 0.5086768866 - 0.5445663929 0.5386111736 0.5098820925 - 0.5456367135 0.5397076011 0.5110868812 - 0.5467023253 0.5407969952 0.5122858882 - 0.5477678180 0.5418863893 0.5134847760 - 0.5488299727 0.5429692864 0.5146782994 - 0.5498914719 0.5440509915 0.5158712268 - 0.5509498715 0.5451288819 0.5170599818 - 0.5520067215 0.5462048054 0.5182471871 - 0.5530617237 0.5472781062 0.5194309950 - 0.5541145205 0.5483480096 0.5206112266 - 0.5551661849 0.5494166017 0.5217891932 - 0.5562142730 0.5504804254 0.5229610801 - 0.5572623014 0.5515441895 0.5241327286 - 0.5583065152 0.5526019931 0.5252950191 - 0.5593506098 0.5536596179 0.5264571905 - 0.5603913069 0.5547125936 0.5276136994 - 0.5614306927 0.5557637811 0.5287684202 - 0.5624685287 0.5568122864 0.5299198031 - 0.5635039210 0.5578569174 0.5310674906 - 0.5645387173 0.5589007735 0.5322136879 - 0.5655695796 0.5599387884 0.5333532095 - 0.5666003823 0.5609768033 0.5344926119 - 0.5676280856 0.5620093942 0.5356252789 - 0.5686550140 0.5630406141 0.5367568731 - 0.5696796775 0.5640686154 0.5378847122 - 0.5707020164 0.5650928020 0.5390092134 - 0.5717239976 0.5661163926 0.5401322842 - 0.5727424026 0.5671336055 0.5412485003 - 0.5737608075 0.5681508183 0.5423645973 - 0.5747758746 0.5691627264 0.5434734821 - 0.5757898092 0.5701727271 0.5445804000 - 0.5768023729 0.5711808205 0.5456839204 - 0.5778123736 0.5721849203 0.5467820168 - 0.5788223743 0.5731890202 0.5478801131 - 0.5798283219 0.5741881132 0.5489701033 - 0.5808337927 0.5751867294 0.5500597954 - 0.5818374753 0.5761824846 0.5511454940 - 0.5828391910 0.5771750808 0.5522276759 - 0.5838407278 0.5781676173 0.5533092022 - 0.5848379731 0.5791544914 0.5543829203 - 0.5858352184 0.5801414251 0.5554565787 - 0.5868303776 0.5811259151 0.5565248132 - 0.5878236890 0.5821083188 0.5575889945 - 0.5888170004 0.5830906034 0.5586525202 - 0.5898060203 0.5840693712 0.5597082973 - 0.5907949805 0.5850481987 0.5607640147 - 0.5917819738 0.5860245824 0.5618162751 - 0.5927671790 0.5869988799 0.5628659725 - 0.5937523842 0.5879732966 0.5639154911 - 0.5947332978 0.5889440179 0.5649582148 - 0.5957140923 0.5899146795 0.5660009980 - 0.5966933966 0.5908836126 0.5670400858 - 0.5976703763 0.5918499231 0.5680750012 - 0.5986474752 0.5928161144 0.5691099763 - 0.5996209979 0.5937793851 0.5701385736 - 0.6005936861 0.5947421193 0.5711663961 - 0.6015655994 0.5957037807 0.5721924901 - 0.6025345922 0.5966621041 0.5732141137 - 0.6035035849 0.5976204276 0.5742357969 - 0.6044700146 0.5985764861 0.5752530098 - 0.6054347157 0.5995312929 0.5762677789 - 0.6063994169 0.6004859805 0.5772826076 - 0.6073606014 0.6014366150 0.5782899261 - 0.6083216071 0.6023870111 0.5792971849 - 0.6092813015 0.6033363938 0.5803018212 - 0.6102380753 0.6042832136 0.5813007951 - 0.6111947894 0.6052300930 0.5822998285 - 0.6121494174 0.6061744094 0.5832931995 - 0.6131024957 0.6071168780 0.5842832923 - 0.6140555143 0.6080595255 0.5852733850 - 0.6150048971 0.6089988947 0.5862563252 - 0.6159536839 0.6099380255 0.5872384906 - 0.6169021130 0.6108763814 0.5882192254 - 0.6178473234 0.6118112206 0.5891932249 - 0.6187924743 0.6127460003 0.5901671052 - 0.6197360754 0.6136794090 0.5911372900 - 0.6206771135 0.6146106720 0.5921025276 - 0.6216180921 0.6155419946 0.5930677056 - 0.6225569844 0.6164706945 0.5940269232 - 0.6234943867 0.6173977852 0.5949828029 - 0.6244317889 0.6183248162 0.5959386826 - 0.6253659129 0.6192492247 0.5968865156 - 0.6262990832 0.6201726794 0.5978326797 - 0.6272323728 0.6210963130 0.5987787843 - 0.6281622052 0.6220158935 0.5997148156 - 0.6290917993 0.6229352951 0.6006507277 - 0.6300209761 0.6238542795 0.6015853286 - 0.6309465766 0.6247702837 0.6025123000 - 0.6318721175 0.6256862879 0.6034393907 - 0.6327968240 0.6266012788 0.6043639183 - 0.6337187886 0.6275131106 0.6052818894 - 0.6346408129 0.6284248829 0.6061998010 - 0.6355614066 0.6293355823 0.6071143150 - 0.6364793181 0.6302440166 0.6080229282 - 0.6373972297 0.6311523914 0.6089316010 - 0.6383137107 0.6320592165 0.6098359823 - 0.6392281055 0.6329634190 0.6107351780 - 0.6401425004 0.6338676810 0.6116343737 - 0.6410549879 0.6347705126 0.6125288010 - 0.6419653296 0.6356713772 0.6134184003 - 0.6428756118 0.6365723014 0.6143079996 - 0.6437842846 0.6374711990 0.6151924133 - 0.6446911097 0.6383680105 0.6160721779 - 0.6455978751 0.6392648816 0.6169518828 - 0.6465027928 0.6401593089 0.6178272963 - 0.6474056244 0.6410511136 0.6186987758 - 0.6483083963 0.6419429183 0.6195703149 - 0.6492096186 0.6428322792 0.6204382777 - 0.6501089931 0.6437187791 0.6213029027 - 0.6510083079 0.6446053982 0.6221675277 - 0.6519060135 0.6454898715 0.6230288744 - 0.6528013945 0.6463716030 0.6238865852 - 0.6536967158 0.6472532749 0.6247442961 - 0.6545909047 0.6481332779 0.6255992055 - 0.6554828286 0.6490098834 0.6264498830 - 0.6563746929 0.6498864293 0.6273005009 - 0.6572657228 0.6507614851 0.6281492114 - 0.6581537127 0.6516315937 0.6289927959 - 0.6590415835 0.6525018215 0.6298364997 - 0.6599292159 0.6533712149 0.6306790113 - 0.6608136892 0.6542350054 0.6315155029 - 0.6616982818 0.6550989151 0.6323519945 - 0.6625828743 0.6559628248 0.6331884265 - 0.6634635925 0.6568207741 0.6340178251 - 0.6643443108 0.6576786041 0.6348472238 - 0.6652249098 0.6585363150 0.6356766224 - 0.6661028862 0.6593894958 0.6365011930 - 0.6669800878 0.6602413058 0.6373249888 - 0.6678574085 0.6610931754 0.6381487846 - 0.6687322855 0.6619417071 0.6389691234 - 0.6696056724 0.6627879143 0.6397873759 - 0.6704791188 0.6636340022 0.6406056881 - 0.6713513136 0.6644781828 0.6414217949 - 0.6722213030 0.6653187871 0.6422346234 - 0.6730912924 0.6661595106 0.6430473924 - 0.6739609241 0.6669995189 0.6438593864 - 0.6748270988 0.6678348184 0.6446667910 - 0.6756932139 0.6686701179 0.6454741955 - 0.6765593886 0.6695052981 0.6462816000 - 0.6774228215 0.6703364253 0.6470842957 - 0.6782855988 0.6711664796 0.6478862166 - 0.6791484952 0.6719965935 0.6486880779 - 0.6800094247 0.6728242040 0.6494870782 - 0.6808683872 0.6736493111 0.6502835751 - 0.6817274094 0.6744744182 0.6510801911 - 0.6825857759 0.6752986908 0.6518756151 - 0.6834415793 0.6761190295 0.6526666880 - 0.6842973232 0.6769393086 0.6534578204 - 0.6851530075 0.6777595878 0.6542488933 - 0.6860055923 0.6785761118 0.6550366282 - 0.6868575811 0.6793918014 0.6558237076 - 0.6877095103 0.6802073717 0.6566109061 - 0.6885594130 0.6810206175 0.6573961973 - 0.6894065738 0.6818302274 0.6581792831 - 0.6902536750 0.6826397777 0.6589624286 - 0.6911007762 0.6834495068 0.6597452760 - 0.6919429898 0.6842535734 0.6605246067 - 0.6927852035 0.6850578189 0.6613038182 - 0.6936272979 0.6858618855 0.6620830894 - 0.6944670081 0.6866629720 0.6628600955 - 0.6953045130 0.6874614954 0.6636353731 - 0.6961420178 0.6882600188 0.6644108295 - 0.6969792247 0.6890581846 0.6651858091 - 0.6978119016 0.6898517013 0.6659575105 - 0.6986445785 0.6906450987 0.6667290926 - 0.6994773149 0.6914386153 0.6675007939 - 0.7003065944 0.6922283173 0.6682701707 - 0.7011330724 0.6930149794 0.6690381765 - 0.7019597292 0.6938015819 0.6698061824 - 0.7027862072 0.6945881844 0.6705740094 - 0.7036067843 0.6953691244 0.6713399291 - 0.7044274211 0.6961500049 0.6721059084 - 0.7052479982 0.6969308257 0.6728718281 - 0.7060660124 0.6977090240 0.6736367941 - 0.7068808079 0.6984837055 0.6744008064 - 0.7076956034 0.6992583871 0.6751648188 - 0.7085103989 0.7000331879 0.6759287715 - 0.7093204856 0.7008035183 0.6766912937 - 0.7101296782 0.7015730739 0.6774535775 - 0.7109388709 0.7023426890 0.6782159209 - 0.7117468715 0.7031109929 0.6789777875 - 0.7125505805 0.7038751245 0.6797384024 - 0.7133544087 0.7046391964 0.6804990768 - 0.7141581178 0.7054032087 0.6812598109 - 0.7149589062 0.7061647773 0.6820197105 - 0.7157574296 0.7069243193 0.6827788949 - 0.7165558934 0.7076838017 0.6835381985 - 0.7173544168 0.7084434032 0.6842973828 - 0.7181484103 0.7091985941 0.6850554943 - 0.7189418077 0.7099533081 0.6858134866 - 0.7197352052 0.7107080221 0.6865714788 - 0.7205278277 0.7114617229 0.6873291731 - 0.7213162780 0.7122108936 0.6880860925 - 0.7221047878 0.7129601240 0.6888428926 - 0.7228932977 0.7137092948 0.6895998120 - 0.7236797810 0.7144563198 0.6903561950 - 0.7244635820 0.7152001262 0.6911119819 - 0.7252473235 0.7159438729 0.6918678880 - 0.7260311246 0.7166876197 0.6926236749 - 0.7268118858 0.7174283862 0.6933789849 - 0.7275910974 0.7181674242 0.6941339970 - 0.7283703089 0.7189065218 0.6948890090 - 0.7291494012 0.7196455002 0.6956440210 - 0.7299249768 0.7203806043 0.6963983774 - 0.7306997180 0.7211148739 0.6971526146 - 0.7314745188 0.7218492031 0.6979069114 - 0.7322493196 0.7225834727 0.6986610889 - 0.7330183983 0.7233123183 0.6994134188 - 0.7337874770 0.7240409851 0.7001656890 - 0.7345564961 0.7247697115 0.7009178996 - 0.7353249788 0.7254979014 0.7016698718 - 0.7360888124 0.7262215018 0.7024201751 - 0.7368525863 0.7269449830 0.7031704187 - 0.7376164198 0.7276685834 0.7039207220 - 0.7383791208 0.7283911705 0.7046704888 - 0.7391375899 0.7291100025 0.7054188251 - 0.7398961186 0.7298288941 0.7061672211 - 0.7406545877 0.7305477262 0.7069156170 - 0.7414119244 0.7312654257 0.7076634169 - 0.7421655059 0.7319800258 0.7084097862 - 0.7429192066 0.7326945066 0.7091562748 - 0.7436727881 0.7334091067 0.7099028230 - 0.7444245815 0.7341225147 0.7106482983 - 0.7451719046 0.7348331809 0.7113916874 - 0.7459192276 0.7355439067 0.7121351957 - 0.7466664910 0.7362545729 0.7128785849 - 0.7474122047 0.7369639277 0.7136210203 - 0.7481539249 0.7376697063 0.7143614292 - 0.7488955855 0.7383756042 0.7151017189 - 0.7496371865 0.7390813828 0.7158420086 - 0.7503774762 0.7397862077 0.7165814042 - 0.7511134148 0.7404878736 0.7173187733 - 0.7518494129 0.7411895990 0.7180560827 - 0.7525852919 0.7418913245 0.7187933922 - 0.7533203959 0.7425922751 0.7195299864 - 0.7540513277 0.7432901263 0.7202641964 - 0.7547823191 0.7439879179 0.7209984064 - 0.7555131912 0.7446855903 0.7217326164 - 0.7562438250 0.7453832030 0.7224665284 - 0.7569695711 0.7460777760 0.7231976986 - 0.7576953769 0.7467724085 0.7239289880 - 0.7584211826 0.7474669814 0.7246602178 - 0.7591469884 0.7481616139 0.7253913879 - 0.7598674297 0.7488520145 0.7261208892 - 0.7605875134 0.7495421171 0.7268503904 - 0.7613074780 0.7502321005 0.7275798917 - 0.7620275021 0.7509222031 0.7283093929 - 0.7627431750 0.7516098022 0.7290375829 - 0.7634574771 0.7522965074 0.7297652960 - 0.7641718984 0.7529832125 0.7304931283 - 0.7648862004 0.7536699772 0.7312207818 - 0.7655977011 0.7543541789 0.7319474816 - 0.7663068771 0.7550362945 0.7326734066 - 0.7670161724 0.7557184100 0.7333992720 - 0.7677254081 0.7564005256 0.7341251969 - 0.7684330940 0.7570810914 0.7348505259 - 0.7691373229 0.7577580214 0.7355744839 - 0.7698414922 0.7584350109 0.7362985015 - 0.7705457211 0.7591118813 0.7370225787 - 0.7712497711 0.7597886920 0.7377464771 - 0.7719480991 0.7604609728 0.7384685278 - 0.7726464272 0.7611331940 0.7391905189 - 0.7733446956 0.7618054748 0.7399126291 - 0.7740430236 0.7624776959 0.7406346202 - 0.7747374177 0.7631471157 0.7413551211 - 0.7754303217 0.7638152242 0.7420752048 - 0.7761231065 0.7644832730 0.7427952290 - 0.7768160105 0.7651513815 0.7435151935 - 0.7775070071 0.7658178210 0.7442349195 - 0.7781947851 0.7664811015 0.7449539900 - 0.7788826227 0.7671443820 0.7456731200 - 0.7795704007 0.7678076029 0.7463921905 - 0.7802581787 0.7684708834 0.7471113205 - 0.7809401155 0.7691282034 0.7478293777 - 0.7816219926 0.7697854042 0.7485474944 - 0.7823038101 0.7704424858 0.7492656112 - 0.7829856873 0.7710996866 0.7499837279 - 0.7836645842 0.7717540264 0.7507011294 - 0.7843410969 0.7724062204 0.7514179945 - 0.7850177288 0.7730582952 0.7521349788 - 0.7856941819 0.7737104893 0.7528519034 - 0.7863702774 0.7743622065 0.7535687089 - 0.7870417833 0.7750092745 0.7542843819 - 0.7877132893 0.7756564021 0.7550001144 - 0.7883847952 0.7763035297 0.7557157874 - 0.7890563011 0.7769505978 0.7564315200 - 0.7897251844 0.7775946259 0.7571462989 - 0.7903923988 0.7782363892 0.7578606009 - 0.7910594940 0.7788782716 0.7585750222 - 0.7917265892 0.7795200944 0.7592893243 - 0.7923933864 0.7801616192 0.7600035071 - 0.7930548787 0.7807983756 0.7607163787 - 0.7937164903 0.7814353108 0.7614293098 - 0.7943781018 0.7820721269 0.7621421814 - 0.7950395942 0.7827090025 0.7628551126 - 0.7956982851 0.7833436728 0.7635670900 - 0.7963539958 0.7839763761 0.7642784715 - 0.7970097065 0.7846090794 0.7649897933 - 0.7976654172 0.7852417827 0.7657011151 - 0.7983211279 0.7858744860 0.7664124966 - 0.7989718914 0.7865037918 0.7671223283 - 0.7996219993 0.7871326208 0.7678319812 - 0.8002722263 0.7877613902 0.7685415745 - 0.8009223938 0.7883902192 0.7692512870 - 0.8015714288 0.7890179753 0.7699605227 - 0.8022168875 0.7896423936 0.7706683874 - 0.8028622866 0.7902668118 0.7713763118 - 0.8035078049 0.7908911705 0.7720841765 - 0.8041532040 0.7915155888 0.7727922201 - 0.8047962189 0.7921379209 0.7734990120 - 0.8054372072 0.7927585840 0.7742050290 - 0.8060783148 0.7933791876 0.7749111056 - 0.8067193031 0.7939997911 0.7756171227 - 0.8073604107 0.7946205139 0.7763231993 - 0.8079984188 0.7952384949 0.7770274878 - 0.8086358905 0.7958559990 0.7777315974 - 0.8092733026 0.7964735031 0.7784357071 - 0.8099107146 0.7970910072 0.7791396976 - 0.8105478287 0.7977082133 0.7798436284 - 0.8111795783 0.7983207703 0.7805455923 - 0.8118113279 0.7989333272 0.7812476158 - 0.8124430776 0.7995458841 0.7819495797 - 0.8130748868 0.8001585007 0.7826516032 - 0.8137050271 0.8007699251 0.7833529115 - 0.8143305779 0.8013781905 0.7840527296 - 0.8149561286 0.8019865155 0.7847524881 - 0.8155816197 0.8025947809 0.7854523063 - 0.8162071109 0.8032031059 0.7861521244 - 0.8168302774 0.8038098216 0.7868509293 - 0.8174502254 0.8044143915 0.7875484228 - 0.8180701137 0.8050190210 0.7882459164 - 0.8186900020 0.8056234717 0.7889434099 - 0.8193098903 0.8062281013 0.7896409035 - 0.8199270964 0.8068311214 0.7903370261 - 0.8205420971 0.8074325919 0.7910320759 - 0.8211570978 0.8080341220 0.7917271852 - 0.8217719793 0.8086355925 0.7924222946 - 0.8223869801 0.8092371225 0.7931174040 - 0.8229985237 0.8098356128 0.7938109040 - 0.8236079812 0.8104321957 0.7945033908 - 0.8242173791 0.8110287189 0.7951959968 - 0.8248267770 0.8116251826 0.7958884835 - 0.8254361749 0.8122218251 0.7965810895 - 0.8260415196 0.8128147125 0.7972719073 - 0.8266444802 0.8134055734 0.7979617715 - 0.8272475004 0.8139964938 0.7986516953 - 0.8278505206 0.8145874739 0.7993416190 - 0.8284534812 0.8151783943 0.8000316024 - 0.8290522099 0.8157662749 0.8007196188 - 0.8296483755 0.8163524866 0.8014068007 - 0.8302447200 0.8169388175 0.8020939827 - 0.8308408856 0.8175250292 0.8027811050 - 0.8314371109 0.8181111813 0.8034682870 - 0.8320297003 0.8186948895 0.8041536808 - 0.8326196074 0.8192769289 0.8048380017 - 0.8332095742 0.8198587894 0.8055223227 - 0.8337996006 0.8204407096 0.8062065840 - 0.8343895078 0.8210226893 0.8068909049 - 0.8349767923 0.8216028810 0.8075736761 - 0.8355613947 0.8221815228 0.8082550168 - 0.8361459970 0.8227599859 0.8089362979 - 0.8367305994 0.8233386278 0.8096176982 - 0.8373152018 0.8239170909 0.8102989793 - 0.8378980160 0.8244941235 0.8109787107 - 0.8384776711 0.8250682950 0.8116558790 - 0.8390573859 0.8256425858 0.8123331070 - 0.8396371007 0.8262168169 0.8130102158 - 0.8402168155 0.8267911077 0.8136873841 - 0.8407955170 0.8273645043 0.8143637180 - 0.8413699865 0.8279343843 0.8150367141 - 0.8419443965 0.8285043836 0.8157097101 - 0.8425189257 0.8290743232 0.8163827062 - 0.8430932760 0.8296442032 0.8170557022 - 0.8436678052 0.8302142024 0.8177286983 - 0.8442364931 0.8307793736 0.8183974028 - 0.8448051214 0.8313444853 0.8190662265 - 0.8453736901 0.8319095969 0.8197348714 - 0.8459423184 0.8324747086 0.8204035759 - 0.8465108275 0.8330397010 0.8210722804 - 0.8470758796 0.8336018920 0.8217378855 - 0.8476396799 0.8341627717 0.8224022985 - 0.8482034206 0.8347237706 0.8230667114 - 0.8487671018 0.8352848291 0.8237311244 - 0.8493307829 0.8358457088 0.8243954778 - 0.8498923182 0.8364052773 0.8250575066 - 0.8504505157 0.8369628191 0.8257161975 - 0.8510087132 0.8375203013 0.8263750076 - 0.8515669703 0.8380777836 0.8270336986 - 0.8521252275 0.8386353254 0.8276925087 - 0.8526831269 0.8391926289 0.8283507228 - 0.8532357216 0.8397459984 0.8290038705 - 0.8537883759 0.8402994871 0.8296570778 - 0.8543409705 0.8408529162 0.8303104043 - 0.8548936248 0.8414064050 0.8309636116 - 0.8554462194 0.8419598937 0.8316168189 - 0.8559948802 0.8425098062 0.8322660923 - 0.8565415144 0.8430578709 0.8329136968 - 0.8570882082 0.8436061144 0.8335613012 - 0.8576349020 0.8441541791 0.8342089057 - 0.8581814766 0.8447024226 0.8348565102 - 0.8587269783 0.8452494144 0.8355026841 - 0.8592686057 0.8457927704 0.8361449242 - 0.8598102927 0.8463361263 0.8367869854 - 0.8603519201 0.8468794227 0.8374292254 - 0.8608934879 0.8474227190 0.8380714059 - 0.8614351153 0.8479660153 0.8387135267 - 0.8619735241 0.8485065103 0.8393514156 - 0.8625109196 0.8490459919 0.8399881124 - 0.8630483150 0.8495854735 0.8406246901 - 0.8635855913 0.8501250148 0.8412613869 - 0.8641229868 0.8506644964 0.8418980837 - 0.8646594286 0.8512032032 0.8425334096 - 0.8651921749 0.8517394066 0.8431646824 - 0.8657249808 0.8522754908 0.8437960744 - 0.8662577868 0.8528116941 0.8444274068 - 0.8667905927 0.8533477783 0.8450586796 - 0.8673233986 0.8538839817 0.8456900120 - 0.8678523898 0.8544178009 0.8463178277 - 0.8683791757 0.8549503088 0.8469436765 - 0.8689060211 0.8554828167 0.8475697041 - 0.8694328070 0.8560153246 0.8481956124 - 0.8699597120 0.8565478921 0.8488215804 - 0.8704863787 0.8570802808 0.8494471908 - 0.8710079193 0.8576077819 0.8500669003 - 0.8715295196 0.8581352830 0.8506866097 - 0.8720510006 0.8586627841 0.8513063192 - 0.8725726008 0.8591902852 0.8519260287 - 0.8730940819 0.8597177863 0.8525456786 - 0.8736137748 0.8602437973 0.8531630039 - 0.8741300106 0.8607671261 0.8537763953 - 0.8746461868 0.8612903953 0.8543899059 - 0.8751623034 0.8618137240 0.8550032973 - 0.8756784797 0.8623369932 0.8556168079 - 0.8761947155 0.8628603220 0.8562303185 - 0.8767071962 0.8633813858 0.8568397164 - 0.8772177100 0.8639011979 0.8574470282 - 0.8777281046 0.8644211292 0.8580543995 - 0.8782384992 0.8649408817 0.8586617112 - 0.8787490129 0.8654606938 0.8592690825 - 0.8792594075 0.8659806252 0.8598763943 - 0.8797658086 0.8664970994 0.8604782820 - 0.8802716732 0.8670130968 0.8610795736 - 0.8807774782 0.8675292134 0.8616809249 - 0.8812834024 0.8680452108 0.8622822165 - 0.8817893267 0.8685612082 0.8628833890 - 0.8822947145 0.8690769076 0.8634842038 - 0.8827946782 0.8695878983 0.8640792966 - 0.8832945824 0.8700988889 0.8646745086 - 0.8837944865 0.8706098795 0.8652696013 - 0.8842945099 0.8711209297 0.8658648133 - 0.8847944140 0.8716319203 0.8664600253 - 0.8852931261 0.8721420169 0.8670535088 - 0.8857870102 0.8726484776 0.8676418066 - 0.8862808943 0.8731549978 0.8682301044 - 0.8867747784 0.8736615181 0.8688184023 - 0.8872687221 0.8741679788 0.8694065809 - 0.8877626061 0.8746744990 0.8699948788 - 0.8882550001 0.8751800060 0.8705809712 - 0.8887438774 0.8756831288 0.8711625934 - 0.8892328143 0.8761863112 0.8717442155 - 0.8897215724 0.8766893744 0.8723257780 - 0.8902105093 0.8771924973 0.8729074001 - 0.8906993866 0.8776956201 0.8734890223 - 0.8911864758 0.8781973720 0.8740683198 - 0.8916702867 0.8786967993 0.8746433854 - 0.8921542168 0.8791961074 0.8752185106 - 0.8926380277 0.8796954155 0.8757935762 - 0.8931217790 0.8801947236 0.8763688207 - 0.8936055899 0.8806940913 0.8769438863 - 0.8940873146 0.8811920285 0.8775168061 - 0.8945649266 0.8816872239 0.8780856133 - 0.8950424790 0.8821823001 0.8786544800 - 0.8955200911 0.8826774955 0.8792232871 - 0.8959977031 0.8831726909 0.8797922134 - 0.8964753151 0.8836678863 0.8803610802 - 0.8969513178 0.8841618896 0.8809276819 - 0.8974236250 0.8846526742 0.8814892769 - 0.8978958130 0.8851435781 0.8820508718 - 0.8983680010 0.8856344223 0.8826125264 - 0.8988403082 0.8861253262 0.8831741810 - 0.8993124962 0.8866161108 0.8837357759 - 0.8997837901 0.8871064186 0.8842961192 - 0.9002509117 0.8875938058 0.8848509789 - 0.9007179141 0.8880811930 0.8854060173 - 0.9011849761 0.8885685802 0.8859608769 - 0.9016519785 0.8890560269 0.8865159154 - 0.9021189809 0.8895434141 0.8870707750 - 0.9025859833 0.8900306821 0.8876255155 - 0.9030467272 0.8905134201 0.8881742954 - 0.9035072923 0.8909960985 0.8887230158 - 0.9039679766 0.8914787173 0.8892716765 - 0.9044287205 0.8919613957 0.8898203969 - 0.9048894048 0.8924440742 0.8903691769 - 0.9053500891 0.8929268122 0.8909178972 - 0.9058066010 0.8934053183 0.8914616704 - 0.9062620997 0.8938828707 0.8920044899 - 0.9067175984 0.8943604827 0.8925474286 - 0.9071730971 0.8948379755 0.8930901885 - 0.9076285958 0.8953155875 0.8936330080 - 0.9080840945 0.8957931995 0.8941758871 - 0.9085366726 0.8962686062 0.8947150111 - 0.9089869857 0.8967422247 0.8952515125 - 0.9094372988 0.8972157836 0.8957878947 - 0.9098876119 0.8976894021 0.8963243961 - 0.9103379250 0.8981630206 0.8968607783 - 0.9107881784 0.8986366987 0.8973972201 - 0.9112372994 0.8991093040 0.8979318142 - 0.9116824865 0.8995786905 0.8984611034 - 0.9121276736 0.9000480771 0.8989903927 - 0.9125729203 0.9005175233 0.8995196819 - 0.9130182266 0.9009870291 0.9000489712 - 0.9134634137 0.9014564157 0.9005783200 - 0.9139086008 0.9019258022 0.9011074901 - 0.9143493772 0.9023923278 0.9016308784 - 0.9147896171 0.9028583765 0.9021536112 - 0.9152297974 0.9033244848 0.9026762843 - 0.9156699777 0.9037905931 0.9031988978 - 0.9161102176 0.9042567015 0.9037215710 - 0.9165503979 0.9047226906 0.9042443037 - 0.9169877172 0.9051871896 0.9047639966 - 0.9174218178 0.9056496024 0.9052804708 - 0.9178559780 0.9061120749 0.9057971239 - 0.9182900786 0.9065744877 0.9063135982 - 0.9187241793 0.9070370197 0.9068300724 - 0.9191582799 0.9074993730 0.9073466063 - 0.9195923805 0.9079619050 0.9078630805 - 0.9200205803 0.9084206820 0.9083728194 - 0.9204487801 0.9088795185 0.9088826180 - 0.9208769798 0.9093382955 0.9093922973 - 0.9213051796 0.9097970724 0.9099020958 - 0.9217333794 0.9102559090 0.9104117751 - 0.9221615195 0.9107146263 0.9109215140 - 0.9225875139 0.9111716747 0.9114277959 - 0.9230108261 0.9116266966 0.9119303823 - 0.9234341979 0.9120817780 0.9124330878 - 0.9238576293 0.9125369191 0.9129356742 - 0.9242808819 0.9129918814 0.9134383202 - 0.9247043133 0.9134470224 0.9139409065 - 0.9251276851 0.9139019847 0.9144436121 - 0.9255468845 0.9143533707 0.9149402976 - 0.9259656072 0.9148042798 0.9154363871 - 0.9263843298 0.9152551889 0.9159325957 - 0.9268029928 0.9157060981 0.9164286852 - 0.9272217155 0.9161570072 0.9169248939 - 0.9276403785 0.9166079164 0.9174209833 - 0.9280576706 0.9170578122 0.9179155231 - 0.9284709096 0.9175047278 0.9184057117 - 0.9288840890 0.9179517031 0.9188959002 - 0.9292973280 0.9183986187 0.9193860888 - 0.9297105074 0.9188455939 0.9198762774 - 0.9301236868 0.9192925096 0.9203665257 - 0.9305369258 0.9197394848 0.9208567142 - 0.9309471846 0.9201840758 0.9213432074 - 0.9313554764 0.9206268787 0.9218271971 - 0.9317637086 0.9210698009 0.9223111272 - 0.9321720004 0.9215126038 0.9227951169 - 0.9325801730 0.9219554067 0.9232789874 - 0.9329885244 0.9223983288 0.9237629771 - 0.9333966970 0.9228410721 0.9242469072 - 0.9338006973 0.9232804775 0.9247245193 - 0.9342038035 0.9237191081 0.9252008200 - 0.9346070290 0.9241576791 0.9256771207 - 0.9350100756 0.9245964289 0.9261534810 - 0.9354133010 0.9250349998 0.9266297817 - 0.9358164072 0.9254735708 0.9271062016 - 0.9362192750 0.9259120822 0.9275822043 - 0.9366164804 0.9263464808 0.9280517101 - 0.9370136857 0.9267808795 0.9285210967 - 0.9374108911 0.9272152781 0.9289906025 - 0.9378080964 0.9276496768 0.9294601083 - 0.9382053018 0.9280840755 0.9299296141 - 0.9386025071 0.9285184741 0.9303991199 - 0.9389988780 0.9289522767 0.9308673143 - 0.9393916130 0.9293835163 0.9313306212 - 0.9397842884 0.9298146963 0.9317938089 - 0.9401770830 0.9302458763 0.9322569966 - 0.9405698180 0.9306771159 0.9327201843 - 0.9409624934 0.9311082959 0.9331833720 - 0.9413552284 0.9315395951 0.9336466193 - 0.9417470098 0.9319697022 0.9341077209 - 0.9421362281 0.9323967099 0.9345635176 - 0.9425255060 0.9328237176 0.9350193143 - 0.9429147243 0.9332507849 0.9354749918 - 0.9433040023 0.9336777925 0.9359307885 - 0.9436932206 0.9341048002 0.9363865256 - 0.9440824986 0.9345319271 0.9368423223 - 0.9444702864 0.9349573851 0.9372959733 - 0.9448544979 0.9353792071 0.9377450943 - 0.9452388287 0.9358010292 0.9381942153 - 0.9456230998 0.9362229109 0.9386432171 - 0.9460073113 0.9366446733 0.9390922785 - 0.9463915825 0.9370664954 0.9395413995 - 0.9467759132 0.9374883175 0.9399905205 - 0.9471582770 0.9379090071 0.9404376745 - 0.9475358129 0.9383267164 0.9408801198 - 0.9479132891 0.9387443066 0.9413226247 - 0.9482908249 0.9391620159 0.9417650104 - 0.9486683011 0.9395796061 0.9422073960 - 0.9490457773 0.9399973154 0.9426497817 - 0.9494233131 0.9404149055 0.9430922866 - 0.9497994184 0.9408314824 0.9435328841 - 0.9501690865 0.9412431121 0.9439659715 - 0.9505386949 0.9416546822 0.9443989992 - 0.9509084225 0.9420663118 0.9448320866 - 0.9512780905 0.9424778819 0.9452651739 - 0.9516478181 0.9428895116 0.9456983209 - 0.9520174861 0.9433010817 0.9461312890 - 0.9523869157 0.9437124729 0.9465638995 - 0.9527491927 0.9441193938 0.9469869137 - 0.9531115294 0.9445263147 0.9474099278 - 0.9534738064 0.9449331760 0.9478328824 - 0.9538360834 0.9453400970 0.9482558966 - 0.9541984200 0.9457470179 0.9486789107 - 0.9545606971 0.9461538792 0.9491018057 - 0.9549229741 0.9465608001 0.9495248199 - 0.9552813768 0.9469650984 0.9499412179 - 0.9556391239 0.9473688006 0.9503563046 - 0.9559966922 0.9477726221 0.9507713914 - 0.9563543200 0.9481763244 0.9511865973 - 0.9567118883 0.9485800862 0.9516016841 - 0.9570695162 0.9489837885 0.9520167708 - 0.9574272037 0.9493876100 0.9524319768 - 0.9577826262 0.9497889876 0.9528437853 - 0.9581363201 0.9501885772 0.9532533288 - 0.9584900737 0.9505882263 0.9536628127 - 0.9588438272 0.9509878159 0.9540722966 - 0.9591975808 0.9513874054 0.9544817805 - 0.9595512748 0.9517869949 0.9548913240 - 0.9599050879 0.9521865845 0.9553006887 - 0.9602577090 0.9525852203 0.9557086825 - 0.9606065750 0.9529808164 0.9561120272 - 0.9609556198 0.9533764124 0.9565153718 - 0.9613046050 0.9537721276 0.9569187164 - 0.9616534710 0.9541677237 0.9573221207 - 0.9620025158 0.9545633197 0.9577255249 - 0.9623513818 0.9549589753 0.9581288099 - 0.9627004266 0.9553545713 0.9585322142 - 0.9630452991 0.9557468891 0.9589298964 - 0.9633895755 0.9561386704 0.9593268037 - 0.9637339115 0.9565305114 0.9597237110 - 0.9640781879 0.9569222927 0.9601204991 - 0.9644225240 0.9573140740 0.9605174065 - 0.9647668004 0.9577057958 0.9609143138 - 0.9651110172 0.9580975771 0.9613112211 - 0.9654532075 0.9584879279 0.9617047906 - 0.9657922983 0.9588758945 0.9620940089 - 0.9661315084 0.9592639804 0.9624832273 - 0.9664705992 0.9596521258 0.9628723860 - 0.9668096900 0.9600400925 0.9632616043 - 0.9671489000 0.9604281783 0.9636508822 - 0.9674879909 0.9608163238 0.9640401006 - 0.9678270817 0.9612042904 0.9644293189 - 0.9681611061 0.9615883827 0.9648113251 - 0.9684945941 0.9619721174 0.9651926756 - 0.9688280225 0.9623557925 0.9655740857 - 0.9691613913 0.9627394080 0.9659554958 - 0.9694948792 0.9631230831 0.9663369060 - 0.9698283076 0.9635068178 0.9667183161 - 0.9701616764 0.9638903737 0.9670996070 - 0.9704933763 0.9642724991 0.9674782753 - 0.9708213806 0.9646512270 0.9678518176 - 0.9711495042 0.9650298953 0.9682253003 - 0.9714776278 0.9654086828 0.9685987830 - 0.9718056917 0.9657874107 0.9689723253 - 0.9721336961 0.9661661983 0.9693458080 - 0.9724618196 0.9665449262 0.9697191715 - 0.9727898836 0.9669237137 0.9700927138 - 0.9731143117 0.9672991037 0.9704605937 - 0.9734373093 0.9676731825 0.9708260894 - 0.9737601876 0.9680472016 0.9711915851 - 0.9740831852 0.9684212804 0.9715570807 - 0.9744061232 0.9687952995 0.9719226956 - 0.9747291207 0.9691693783 0.9722881913 - 0.9750519991 0.9695433974 0.9726536870 - 0.9753749967 0.9699174762 0.9730191231 - 0.9756926894 0.9702869058 0.9733765125 - 0.9760103822 0.9706562757 0.9737339020 - 0.9763280749 0.9710255861 0.9740912914 - 0.9766458273 0.9713950157 0.9744486809 - 0.9769635201 0.9717643857 0.9748060703 - 0.9772812128 0.9721338153 0.9751635194 - 0.9775989056 0.9725031853 0.9755209088 - 0.9779152274 0.9728714228 0.9758762121 - 0.9782270789 0.9732357860 0.9762253165 - 0.9785388708 0.9736002088 0.9765743017 - 0.9788507819 0.9739645720 0.9769234061 - 0.9791625738 0.9743289948 0.9772723913 - 0.9794744253 0.9746934772 0.9776214957 - 0.9797862768 0.9750579000 0.9779704809 - 0.9800981283 0.9754223228 0.9783195853 - 0.9804072976 0.9757844210 0.9786648750 - 0.9807128906 0.9761431813 0.9790049791 - 0.9810184240 0.9765020013 0.9793450832 - 0.9813238978 0.9768608212 0.9796851873 - 0.9816294909 0.9772195816 0.9800254107 - 0.9819350243 0.9775784016 0.9803655148 - 0.9822404981 0.9779372215 0.9807056189 - 0.9825460911 0.9782959819 0.9810457230 - 0.9828479290 0.9786517024 0.9813808799 - 0.9831464887 0.9790048003 0.9817116857 - 0.9834451079 0.9793578982 0.9820424914 - 0.9837437272 0.9797109962 0.9823732972 - 0.9840422869 0.9800640941 0.9827041030 - 0.9843410254 0.9804171920 0.9830349088 - 0.9846395850 0.9807702899 0.9833657146 - 0.9849382043 0.9811233878 0.9836965203 - 0.9852322936 0.9814730883 0.9840217233 - 0.9855231047 0.9818202257 0.9843425751 - 0.9858139157 0.9821674228 0.9846634865 - 0.9861047268 0.9825145006 0.9849843979 - 0.9863954782 0.9828615785 0.9853054285 - 0.9866862893 0.9832087755 0.9856262803 - 0.9869771004 0.9835559130 0.9859471917 - 0.9872679114 0.9839029908 0.9862681031 - 0.9875541925 0.9842463732 0.9865829945 - 0.9878368974 0.9845867157 0.9868929982 - 0.9881196022 0.9849269986 0.9872030020 - 0.9884023070 0.9852672815 0.9875130057 - 0.9886850119 0.9856076241 0.9878228903 - 0.9889677167 0.9859479070 0.9881328940 - 0.9892504215 0.9862881899 0.9884428978 - 0.9895331264 0.9866284728 0.9887529016 - 0.9898114204 0.9869654179 0.9890568256 - 0.9900845885 0.9872981906 0.9893537164 - 0.9903576970 0.9876310825 0.9896506071 - 0.9906309247 0.9879639149 0.9899474978 - 0.9909039736 0.9882966876 0.9902443886 - 0.9911772013 0.9886295795 0.9905412793 - 0.9914503098 0.9889624119 0.9908382297 - 0.9917234778 0.9892953038 0.9911351204 - 0.9919933081 0.9896262288 0.9914277196 - 0.9922555089 0.9899526834 0.9917106032 - 0.9925177097 0.9902790785 0.9919934273 - 0.9927799106 0.9906055927 0.9922763109 - 0.9930421114 0.9909321070 0.9925591946 - 0.9933043122 0.9912585020 0.9928420782 - 0.9935665131 0.9915850163 0.9931250215 - 0.9938287139 0.9919114709 0.9934079051 - 0.9940900207 0.9922373891 0.9936895967 - 0.9943411946 0.9925572872 0.9939587712 - 0.9945924878 0.9928773046 0.9942278862 - 0.9948437214 0.9931972027 0.9944971204 - 0.9950950146 0.9935171008 0.9947661757 - 0.9953461885 0.9938369989 0.9950354099 - 0.9955974221 0.9941570163 0.9953045845 - 0.9958487153 0.9944769144 0.9955736995 - 0.9960998893 0.9947968125 0.9958428741 - 0.9963402152 0.9951102734 0.9960997105 - 0.9965775013 0.9954220057 0.9963533282 - 0.9968149066 0.9957337976 0.9966068864 - 0.9970521927 0.9960455894 0.9968605042 - 0.9972895980 0.9963573217 0.9971140027 - 0.9975268841 0.9966691136 0.9973676205 - 0.9977642894 0.9969809055 0.9976211786 - 0.9980015755 0.9972925782 0.9978747964 - 0.9982318878 0.9975994229 0.9981204271 - 0.9984529018 0.9978994727 0.9983553290 - 0.9986739755 0.9981995225 0.9985902905 - 0.9988949895 0.9984995723 0.9988251925 - 0.9991160035 0.9987996817 0.9990602136 - 0.9993370175 0.9990997910 0.9992951155 - 0.9995579720 0.9993997812 0.9995300770 - 0.9997789860 0.9996998906 0.9997649789 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0090248538 0.0088125290 0.0067258808 + 0.0172150191 0.0167176109 0.0131098796 + 0.0243747793 0.0235208403 0.0187523402 + 0.0308876392 0.0297362003 0.0239069704 + 0.0369180292 0.0355890803 0.0287567694 + 0.0425458290 0.0411948487 0.0333772190 + 0.0478529185 0.0466255695 0.0378036089 + 0.0528910905 0.0519192591 0.0420932695 + 0.0576865897 0.0571291298 0.0462520085 + 0.0623513199 0.0622628704 0.0503120609 + 0.0669768602 0.0672920421 0.0542953908 + 0.0715567619 0.0721824616 0.0582090616 + 0.0760855973 0.0769187585 0.0620570593 + 0.0805741325 0.0815202296 0.0658472329 + 0.0850307494 0.0859836414 0.0695805177 + 0.0894547775 0.0903253108 0.0732729286 + 0.0938297287 0.0945612267 0.0769176036 + 0.0981539637 0.0986936092 0.0805180594 + 0.1024200022 0.1027287021 0.0840807334 + 0.1066194996 0.1066882983 0.0876046494 + 0.1107435003 0.1105668023 0.0910888687 + 0.1148013026 0.1143577993 0.0945325121 + 0.1187836006 0.1180787012 0.0979359895 + 0.1227072999 0.1217278987 0.1012983993 + 0.1265697032 0.1253149062 0.1046219021 + 0.1303700954 0.1288353056 0.1078997031 + 0.1340970993 0.1322914958 0.1111387014 + 0.1377590001 0.1356990933 0.1143449992 + 0.1413495988 0.1390592009 0.1175180003 + 0.1448680013 0.1423660964 0.1206526011 + 0.1483224928 0.1456252038 0.1237441003 + 0.1516993940 0.1488344073 0.1267928034 + 0.1550139934 0.1519985944 0.1298023015 + 0.1582762003 0.1551181972 0.1327774972 + 0.1614779979 0.1581996977 0.1357243061 + 0.1646181941 0.1612505019 0.1386376023 + 0.1676993966 0.1642563939 0.1415113956 + 0.1707275957 0.1672241986 0.1443507969 + 0.1737034023 0.1701561958 0.1471655965 + 0.1766317040 0.1730598062 0.1499509066 + 0.1795026064 0.1759244055 0.1527041048 + 0.1823239028 0.1787561029 0.1554242969 + 0.1850987971 0.1815645993 0.1581097990 + 0.1878297031 0.1843390018 0.1607618928 + 0.1905121952 0.1870823056 0.1633782983 + 0.1931522936 0.1898010969 0.1659625024 + 0.1957568973 0.1924955994 0.1685173064 + 0.1983253062 0.1951646954 0.1710429937 + 0.2008654028 0.1978061944 0.1735426039 + 0.2033737004 0.2004275024 0.1760165989 + 0.2058539987 0.2030244023 0.1784628034 + 0.2083114982 0.2055975944 0.1808815002 + 0.2107529044 0.2081506997 0.1832714975 + 0.2131794989 0.2106831074 0.1856442988 + 0.2155988961 0.2131962031 0.1879992038 + 0.2180117071 0.2156914026 0.1903316975 + 0.2204127014 0.2181655020 0.1926410943 + 0.2228053063 0.2206185013 0.1949304938 + 0.2251946926 0.2230494022 0.1971997023 + 0.2275789976 0.2254669964 0.1994490027 + 0.2299547046 0.2278635949 0.2016751021 + 0.2323268950 0.2302404940 0.2038796991 + 0.2346891016 0.2326005995 0.2060652971 + 0.2370416969 0.2349402010 0.2082331926 + 0.2393849939 0.2372657061 0.2103780955 + 0.2417193949 0.2395751029 0.2125052065 + 0.2440440953 0.2418743074 0.2146157026 + 0.2463590056 0.2441594005 0.2167119980 + 0.2486687005 0.2464269996 0.2187922001 + 0.2509658039 0.2486840934 0.2208545059 + 0.2532486022 0.2509253025 0.2229007035 + 0.2555230856 0.2531493902 0.2249286026 + 0.2577824891 0.2553615868 0.2269378006 + 0.2600266933 0.2575596869 0.2289337069 + 0.2622607946 0.2597441971 0.2309127003 + 0.2644844949 0.2619175017 0.2328774929 + 0.2667022049 0.2640810013 0.2348311990 + 0.2689101100 0.2662318051 0.2367679030 + 0.2711063027 0.2683725059 0.2386890054 + 0.2732926905 0.2705014944 0.2405951023 + 0.2754707932 0.2726207078 0.2424882054 + 0.2776342928 0.2747271955 0.2443652004 + 0.2797912061 0.2768242061 0.2462334931 + 0.2819392085 0.2789101899 0.2480895966 + 0.2840760946 0.2809852958 0.2499316037 + 0.2862043977 0.2830494046 0.2517591119 + 0.2883251011 0.2851034105 0.2535771132 + 0.2904368043 0.2871474028 0.2553840876 + 0.2925382853 0.2891792059 0.2571800053 + 0.2946313024 0.2912011147 0.2589674890 + 0.2967143953 0.2932142019 0.2607443035 + 0.2987858057 0.2952139080 0.2625100017 + 0.3008505106 0.2972015142 0.2642692924 + 0.3029071987 0.2991822064 0.2660228014 + 0.3049511909 0.3011547029 0.2677705884 + 0.3069872856 0.3031156063 0.2695144117 + 0.3090176880 0.3050638139 0.2712531090 + 0.3110390007 0.3070029020 0.2729856968 + 0.3130499125 0.3089343011 0.2747122049 + 0.3150539994 0.3108555079 0.2764343023 + 0.3170512021 0.3127650023 0.2781510949 + 0.3190402091 0.3146660924 0.2798615098 + 0.3210211992 0.3165611923 0.2815661132 + 0.3229936957 0.3184478879 0.2832651138 + 0.3249579072 0.3203234971 0.2849591076 + 0.3269129992 0.3221895099 0.2866472900 + 0.3288631141 0.3240458071 0.2883332074 + 0.3308089077 0.3258920908 0.2900170982 + 0.3327476978 0.3277291059 0.2916966081 + 0.3346762955 0.3295575976 0.2933689058 + 0.3365967870 0.3313786089 0.2950361967 + 0.3385129869 0.3331907094 0.2967014909 + 0.3404223025 0.3349941075 0.2983629107 + 0.3423224092 0.3367873132 0.3000209033 + 0.3442116082 0.3385719061 0.3016754985 + 0.3460933864 0.3403505981 0.3033275902 + 0.3479698002 0.3421216011 0.3049786985 + 0.3498382866 0.3438830078 0.3066262007 + 0.3516973853 0.3456346095 0.3082687855 + 0.3535484076 0.3473792970 0.3099066913 + 0.3553937972 0.3491171896 0.3115408123 + 0.3572331965 0.3508478105 0.3131729960 + 0.3590649962 0.3525711000 0.3148024082 + 0.3608897924 0.3542861938 0.3164277971 + 0.3627074957 0.3559936881 0.3180496991 + 0.3645170927 0.3576950133 0.3196698129 + 0.3663190007 0.3593909144 0.3212881088 + 0.3681136072 0.3610796034 0.3229044080 + 0.3699010015 0.3627603948 0.3245167136 + 0.3716816008 0.3644332886 0.3261250854 + 0.3734537065 0.3660990894 0.3277314901 + 0.3752183020 0.3677594066 0.3293341994 + 0.3769755960 0.3694128990 0.3309333026 + 0.3787257969 0.3710587025 0.3325302899 + 0.3804695010 0.3726957142 0.3341256976 + 0.3822059929 0.3743239939 0.3357195854 + 0.3839339912 0.3759444058 0.3373118043 + 0.3856526017 0.3775574863 0.3389019072 + 0.3873623013 0.3791646957 0.3404884040 + 0.3890652955 0.3807657957 0.3420729041 + 0.3907626867 0.3823603094 0.3436552882 + 0.3924534023 0.3839474916 0.3452340066 + 0.3941374123 0.3855277002 0.3468089998 + 0.3958151042 0.3871026039 0.3483793139 + 0.3974870145 0.3886739910 0.3499442935 + 0.3991512060 0.3902403116 0.3515053093 + 0.4008058012 0.3918004930 0.3530634046 + 0.4024530053 0.3933542967 0.3546175063 + 0.4040930867 0.3949011862 0.3561685979 + 0.4057264924 0.3964413106 0.3577178121 + 0.4073539078 0.3979749084 0.3592661023 + 0.4089717865 0.3995010853 0.3608109057 + 0.4105815887 0.4010227919 0.3623529971 + 0.4121825993 0.4025394022 0.3638921082 + 0.4137746990 0.4040510952 0.3654282987 + 0.4153572917 0.4055579901 0.3669610918 + 0.4169301987 0.4070582092 0.3684917986 + 0.4184952080 0.4085538983 0.3700191975 + 0.4200516045 0.4100449979 0.3715431988 + 0.4215987027 0.4115310907 0.3730638027 + 0.4231320918 0.4130091071 0.3745814860 + 0.4246563911 0.4144822061 0.3760955930 + 0.4261722863 0.4159503877 0.3776068985 + 0.4276798964 0.4174129963 0.3791159987 + 0.4291754067 0.4188691974 0.3806217909 + 0.4306622148 0.4203203917 0.3821237981 + 0.4321402907 0.4217669070 0.3836225867 + 0.4336062968 0.4232077003 0.3851186037 + 0.4350613952 0.4246425927 0.3866108060 + 0.4365088940 0.4260734916 0.3881003857 + 0.4379490912 0.4275006056 0.3895873129 + 0.4393770993 0.4289224148 0.3910715878 + 0.4407972097 0.4303407967 0.3925527930 + 0.4422096014 0.4317556918 0.3940311074 + 0.4436098039 0.4331642091 0.3955051005 + 0.4449998140 0.4345675111 0.3969767988 + 0.4463821948 0.4359666109 0.3984459937 + 0.4477544129 0.4373601079 0.3999111950 + 0.4491179883 0.4387483895 0.4013735950 + 0.4504755139 0.4401332140 0.4028339982 + 0.4518243968 0.4415132105 0.4042919874 + 0.4531635940 0.4428895116 0.4057449996 + 0.4544965029 0.4442631900 0.4071955085 + 0.4558199048 0.4456324875 0.4086431861 + 0.4571351111 0.4469971061 0.4100872874 + 0.4584454000 0.4483583868 0.4115292132 + 0.4597485065 0.4497137070 0.4129675031 + 0.4610452950 0.4510636032 0.4144037068 + 0.4623377919 0.4524100125 0.4158385098 + 0.4636217058 0.4537501931 0.4172705114 + 0.4649001062 0.4550873041 0.4187003970 + 0.4661735892 0.4564214945 0.4201287925 + 0.4674373865 0.4577501118 0.4215534031 + 0.4686970115 0.4590756893 0.4229756892 + 0.4699491858 0.4603962004 0.4243943989 + 0.4711951911 0.4617125988 0.4258098900 + 0.4724380970 0.4630267918 0.4272238910 + 0.4736748040 0.4643347859 0.4286344051 + 0.4749085009 0.4656395018 0.4300416112 + 0.4761387110 0.4669404924 0.4314455986 + 0.4773631096 0.4682346880 0.4328455925 + 0.4785852134 0.4695264101 0.4342440069 + 0.4798015058 0.4708124995 0.4356383979 + 0.4810144007 0.4720954001 0.4370301068 + 0.4822235107 0.4733743072 0.4384193122 + 0.4834271073 0.4746474028 0.4398039877 + 0.4846287966 0.4759184122 0.4411869943 + 0.4858255088 0.4771836102 0.4425632954 + 0.4870207012 0.4784463942 0.4439373910 + 0.4882127941 0.4797044098 0.4453079998 + 0.4894016981 0.4809592068 0.4466761053 + 0.4905884862 0.4822115898 0.4480428994 + 0.4917714894 0.4834589958 0.4494043887 + 0.4929533005 0.4847050011 0.4507643878 + 0.4941304028 0.4859468937 0.4521192908 + 0.4953059852 0.4871870875 0.4534727931 + 0.4964784980 0.4884232879 0.4548236132 + 0.4976483881 0.4896576107 0.4561721087 + 0.4988156855 0.4908897877 0.4575186968 + 0.4999799132 0.4921190143 0.4588620961 + 0.5011426806 0.4933466911 0.4602043927 + 0.5023010969 0.4945699871 0.4615409970 + 0.5034586191 0.4957925081 0.4628765881 + 0.5046117902 0.4970094860 0.4642063081 + 0.5057641864 0.4982253909 0.4655351043 + 0.5069122910 0.4994361997 0.4668604136 + 0.5080590844 0.5006449223 0.4681833088 + 0.5092027783 0.5018489957 0.4695020914 + 0.5103443861 0.5030499101 0.4708178043 + 0.5114830136 0.5042467117 0.4721299112 + 0.5126193762 0.5054402947 0.4734382927 + 0.5137537122 0.5066308975 0.4747439027 + 0.5148848891 0.5078179240 0.4760439992 + 0.5160142183 0.5090023875 0.4773412049 + 0.5171405077 0.5101822019 0.4786339104 + 0.5182654262 0.5113596916 0.4799247086 + 0.5193868279 0.5125321746 0.4812121093 + 0.5205066800 0.5137025714 0.4824982882 + 0.5216233730 0.5148683786 0.4837796092 + 0.5227389932 0.5160326958 0.4850594997 + 0.5238509178 0.5171926022 0.4863342941 + 0.5249615908 0.5183511972 0.4876079857 + 0.5260689855 0.5195046067 0.4888750911 + 0.5271754265 0.5206564069 0.4901408851 + 0.5282781124 0.5218034983 0.4914003909 + 0.5293796062 0.5229489207 0.4926582873 + 0.5304781199 0.5240889788 0.4939117134 + 0.5315753818 0.5252268910 0.4951637089 + 0.5326694250 0.5263600945 0.4964101911 + 0.5337619185 0.5274910927 0.4976545870 + 0.5348516703 0.5286170840 0.4988938868 + 0.5359399915 0.5297397971 0.5001304746 + 0.5370256901 0.5308594704 0.5013629794 + 0.5381091237 0.5319762826 0.5025923252 + 0.5391908288 0.5330902934 0.5038173795 + 0.5402699709 0.5342003703 0.5050377846 + 0.5413478017 0.5353085995 0.5062550902 + 0.5424222946 0.5364122987 0.5074666739 + 0.5434960723 0.5375146866 0.5086768866 + 0.5445663929 0.5386111736 0.5098820925 + 0.5456367135 0.5397076011 0.5110868812 + 0.5467023253 0.5407969952 0.5122858882 + 0.5477678180 0.5418863893 0.5134847760 + 0.5488299727 0.5429692864 0.5146782994 + 0.5498914719 0.5440509915 0.5158712268 + 0.5509498715 0.5451288819 0.5170599818 + 0.5520067215 0.5462048054 0.5182471871 + 0.5530617237 0.5472781062 0.5194309950 + 0.5541145205 0.5483480096 0.5206112266 + 0.5551661849 0.5494166017 0.5217891932 + 0.5562142730 0.5504804254 0.5229610801 + 0.5572623014 0.5515441895 0.5241327286 + 0.5583065152 0.5526019931 0.5252950191 + 0.5593506098 0.5536596179 0.5264571905 + 0.5603913069 0.5547125936 0.5276136994 + 0.5614306927 0.5557637811 0.5287684202 + 0.5624685287 0.5568122864 0.5299198031 + 0.5635039210 0.5578569174 0.5310674906 + 0.5645387173 0.5589007735 0.5322136879 + 0.5655695796 0.5599387884 0.5333532095 + 0.5666003823 0.5609768033 0.5344926119 + 0.5676280856 0.5620093942 0.5356252789 + 0.5686550140 0.5630406141 0.5367568731 + 0.5696796775 0.5640686154 0.5378847122 + 0.5707020164 0.5650928020 0.5390092134 + 0.5717239976 0.5661163926 0.5401322842 + 0.5727424026 0.5671336055 0.5412485003 + 0.5737608075 0.5681508183 0.5423645973 + 0.5747758746 0.5691627264 0.5434734821 + 0.5757898092 0.5701727271 0.5445804000 + 0.5768023729 0.5711808205 0.5456839204 + 0.5778123736 0.5721849203 0.5467820168 + 0.5788223743 0.5731890202 0.5478801131 + 0.5798283219 0.5741881132 0.5489701033 + 0.5808337927 0.5751867294 0.5500597954 + 0.5818374753 0.5761824846 0.5511454940 + 0.5828391910 0.5771750808 0.5522276759 + 0.5838407278 0.5781676173 0.5533092022 + 0.5848379731 0.5791544914 0.5543829203 + 0.5858352184 0.5801414251 0.5554565787 + 0.5868303776 0.5811259151 0.5565248132 + 0.5878236890 0.5821083188 0.5575889945 + 0.5888170004 0.5830906034 0.5586525202 + 0.5898060203 0.5840693712 0.5597082973 + 0.5907949805 0.5850481987 0.5607640147 + 0.5917819738 0.5860245824 0.5618162751 + 0.5927671790 0.5869988799 0.5628659725 + 0.5937523842 0.5879732966 0.5639154911 + 0.5947332978 0.5889440179 0.5649582148 + 0.5957140923 0.5899146795 0.5660009980 + 0.5966933966 0.5908836126 0.5670400858 + 0.5976703763 0.5918499231 0.5680750012 + 0.5986474752 0.5928161144 0.5691099763 + 0.5996209979 0.5937793851 0.5701385736 + 0.6005936861 0.5947421193 0.5711663961 + 0.6015655994 0.5957037807 0.5721924901 + 0.6025345922 0.5966621041 0.5732141137 + 0.6035035849 0.5976204276 0.5742357969 + 0.6044700146 0.5985764861 0.5752530098 + 0.6054347157 0.5995312929 0.5762677789 + 0.6063994169 0.6004859805 0.5772826076 + 0.6073606014 0.6014366150 0.5782899261 + 0.6083216071 0.6023870111 0.5792971849 + 0.6092813015 0.6033363938 0.5803018212 + 0.6102380753 0.6042832136 0.5813007951 + 0.6111947894 0.6052300930 0.5822998285 + 0.6121494174 0.6061744094 0.5832931995 + 0.6131024957 0.6071168780 0.5842832923 + 0.6140555143 0.6080595255 0.5852733850 + 0.6150048971 0.6089988947 0.5862563252 + 0.6159536839 0.6099380255 0.5872384906 + 0.6169021130 0.6108763814 0.5882192254 + 0.6178473234 0.6118112206 0.5891932249 + 0.6187924743 0.6127460003 0.5901671052 + 0.6197360754 0.6136794090 0.5911372900 + 0.6206771135 0.6146106720 0.5921025276 + 0.6216180921 0.6155419946 0.5930677056 + 0.6225569844 0.6164706945 0.5940269232 + 0.6234943867 0.6173977852 0.5949828029 + 0.6244317889 0.6183248162 0.5959386826 + 0.6253659129 0.6192492247 0.5968865156 + 0.6262990832 0.6201726794 0.5978326797 + 0.6272323728 0.6210963130 0.5987787843 + 0.6281622052 0.6220158935 0.5997148156 + 0.6290917993 0.6229352951 0.6006507277 + 0.6300209761 0.6238542795 0.6015853286 + 0.6309465766 0.6247702837 0.6025123000 + 0.6318721175 0.6256862879 0.6034393907 + 0.6327968240 0.6266012788 0.6043639183 + 0.6337187886 0.6275131106 0.6052818894 + 0.6346408129 0.6284248829 0.6061998010 + 0.6355614066 0.6293355823 0.6071143150 + 0.6364793181 0.6302440166 0.6080229282 + 0.6373972297 0.6311523914 0.6089316010 + 0.6383137107 0.6320592165 0.6098359823 + 0.6392281055 0.6329634190 0.6107351780 + 0.6401425004 0.6338676810 0.6116343737 + 0.6410549879 0.6347705126 0.6125288010 + 0.6419653296 0.6356713772 0.6134184003 + 0.6428756118 0.6365723014 0.6143079996 + 0.6437842846 0.6374711990 0.6151924133 + 0.6446911097 0.6383680105 0.6160721779 + 0.6455978751 0.6392648816 0.6169518828 + 0.6465027928 0.6401593089 0.6178272963 + 0.6474056244 0.6410511136 0.6186987758 + 0.6483083963 0.6419429183 0.6195703149 + 0.6492096186 0.6428322792 0.6204382777 + 0.6501089931 0.6437187791 0.6213029027 + 0.6510083079 0.6446053982 0.6221675277 + 0.6519060135 0.6454898715 0.6230288744 + 0.6528013945 0.6463716030 0.6238865852 + 0.6536967158 0.6472532749 0.6247442961 + 0.6545909047 0.6481332779 0.6255992055 + 0.6554828286 0.6490098834 0.6264498830 + 0.6563746929 0.6498864293 0.6273005009 + 0.6572657228 0.6507614851 0.6281492114 + 0.6581537127 0.6516315937 0.6289927959 + 0.6590415835 0.6525018215 0.6298364997 + 0.6599292159 0.6533712149 0.6306790113 + 0.6608136892 0.6542350054 0.6315155029 + 0.6616982818 0.6550989151 0.6323519945 + 0.6625828743 0.6559628248 0.6331884265 + 0.6634635925 0.6568207741 0.6340178251 + 0.6643443108 0.6576786041 0.6348472238 + 0.6652249098 0.6585363150 0.6356766224 + 0.6661028862 0.6593894958 0.6365011930 + 0.6669800878 0.6602413058 0.6373249888 + 0.6678574085 0.6610931754 0.6381487846 + 0.6687322855 0.6619417071 0.6389691234 + 0.6696056724 0.6627879143 0.6397873759 + 0.6704791188 0.6636340022 0.6406056881 + 0.6713513136 0.6644781828 0.6414217949 + 0.6722213030 0.6653187871 0.6422346234 + 0.6730912924 0.6661595106 0.6430473924 + 0.6739609241 0.6669995189 0.6438593864 + 0.6748270988 0.6678348184 0.6446667910 + 0.6756932139 0.6686701179 0.6454741955 + 0.6765593886 0.6695052981 0.6462816000 + 0.6774228215 0.6703364253 0.6470842957 + 0.6782855988 0.6711664796 0.6478862166 + 0.6791484952 0.6719965935 0.6486880779 + 0.6800094247 0.6728242040 0.6494870782 + 0.6808683872 0.6736493111 0.6502835751 + 0.6817274094 0.6744744182 0.6510801911 + 0.6825857759 0.6752986908 0.6518756151 + 0.6834415793 0.6761190295 0.6526666880 + 0.6842973232 0.6769393086 0.6534578204 + 0.6851530075 0.6777595878 0.6542488933 + 0.6860055923 0.6785761118 0.6550366282 + 0.6868575811 0.6793918014 0.6558237076 + 0.6877095103 0.6802073717 0.6566109061 + 0.6885594130 0.6810206175 0.6573961973 + 0.6894065738 0.6818302274 0.6581792831 + 0.6902536750 0.6826397777 0.6589624286 + 0.6911007762 0.6834495068 0.6597452760 + 0.6919429898 0.6842535734 0.6605246067 + 0.6927852035 0.6850578189 0.6613038182 + 0.6936272979 0.6858618855 0.6620830894 + 0.6944670081 0.6866629720 0.6628600955 + 0.6953045130 0.6874614954 0.6636353731 + 0.6961420178 0.6882600188 0.6644108295 + 0.6969792247 0.6890581846 0.6651858091 + 0.6978119016 0.6898517013 0.6659575105 + 0.6986445785 0.6906450987 0.6667290926 + 0.6994773149 0.6914386153 0.6675007939 + 0.7003065944 0.6922283173 0.6682701707 + 0.7011330724 0.6930149794 0.6690381765 + 0.7019597292 0.6938015819 0.6698061824 + 0.7027862072 0.6945881844 0.6705740094 + 0.7036067843 0.6953691244 0.6713399291 + 0.7044274211 0.6961500049 0.6721059084 + 0.7052479982 0.6969308257 0.6728718281 + 0.7060660124 0.6977090240 0.6736367941 + 0.7068808079 0.6984837055 0.6744008064 + 0.7076956034 0.6992583871 0.6751648188 + 0.7085103989 0.7000331879 0.6759287715 + 0.7093204856 0.7008035183 0.6766912937 + 0.7101296782 0.7015730739 0.6774535775 + 0.7109388709 0.7023426890 0.6782159209 + 0.7117468715 0.7031109929 0.6789777875 + 0.7125505805 0.7038751245 0.6797384024 + 0.7133544087 0.7046391964 0.6804990768 + 0.7141581178 0.7054032087 0.6812598109 + 0.7149589062 0.7061647773 0.6820197105 + 0.7157574296 0.7069243193 0.6827788949 + 0.7165558934 0.7076838017 0.6835381985 + 0.7173544168 0.7084434032 0.6842973828 + 0.7181484103 0.7091985941 0.6850554943 + 0.7189418077 0.7099533081 0.6858134866 + 0.7197352052 0.7107080221 0.6865714788 + 0.7205278277 0.7114617229 0.6873291731 + 0.7213162780 0.7122108936 0.6880860925 + 0.7221047878 0.7129601240 0.6888428926 + 0.7228932977 0.7137092948 0.6895998120 + 0.7236797810 0.7144563198 0.6903561950 + 0.7244635820 0.7152001262 0.6911119819 + 0.7252473235 0.7159438729 0.6918678880 + 0.7260311246 0.7166876197 0.6926236749 + 0.7268118858 0.7174283862 0.6933789849 + 0.7275910974 0.7181674242 0.6941339970 + 0.7283703089 0.7189065218 0.6948890090 + 0.7291494012 0.7196455002 0.6956440210 + 0.7299249768 0.7203806043 0.6963983774 + 0.7306997180 0.7211148739 0.6971526146 + 0.7314745188 0.7218492031 0.6979069114 + 0.7322493196 0.7225834727 0.6986610889 + 0.7330183983 0.7233123183 0.6994134188 + 0.7337874770 0.7240409851 0.7001656890 + 0.7345564961 0.7247697115 0.7009178996 + 0.7353249788 0.7254979014 0.7016698718 + 0.7360888124 0.7262215018 0.7024201751 + 0.7368525863 0.7269449830 0.7031704187 + 0.7376164198 0.7276685834 0.7039207220 + 0.7383791208 0.7283911705 0.7046704888 + 0.7391375899 0.7291100025 0.7054188251 + 0.7398961186 0.7298288941 0.7061672211 + 0.7406545877 0.7305477262 0.7069156170 + 0.7414119244 0.7312654257 0.7076634169 + 0.7421655059 0.7319800258 0.7084097862 + 0.7429192066 0.7326945066 0.7091562748 + 0.7436727881 0.7334091067 0.7099028230 + 0.7444245815 0.7341225147 0.7106482983 + 0.7451719046 0.7348331809 0.7113916874 + 0.7459192276 0.7355439067 0.7121351957 + 0.7466664910 0.7362545729 0.7128785849 + 0.7474122047 0.7369639277 0.7136210203 + 0.7481539249 0.7376697063 0.7143614292 + 0.7488955855 0.7383756042 0.7151017189 + 0.7496371865 0.7390813828 0.7158420086 + 0.7503774762 0.7397862077 0.7165814042 + 0.7511134148 0.7404878736 0.7173187733 + 0.7518494129 0.7411895990 0.7180560827 + 0.7525852919 0.7418913245 0.7187933922 + 0.7533203959 0.7425922751 0.7195299864 + 0.7540513277 0.7432901263 0.7202641964 + 0.7547823191 0.7439879179 0.7209984064 + 0.7555131912 0.7446855903 0.7217326164 + 0.7562438250 0.7453832030 0.7224665284 + 0.7569695711 0.7460777760 0.7231976986 + 0.7576953769 0.7467724085 0.7239289880 + 0.7584211826 0.7474669814 0.7246602178 + 0.7591469884 0.7481616139 0.7253913879 + 0.7598674297 0.7488520145 0.7261208892 + 0.7605875134 0.7495421171 0.7268503904 + 0.7613074780 0.7502321005 0.7275798917 + 0.7620275021 0.7509222031 0.7283093929 + 0.7627431750 0.7516098022 0.7290375829 + 0.7634574771 0.7522965074 0.7297652960 + 0.7641718984 0.7529832125 0.7304931283 + 0.7648862004 0.7536699772 0.7312207818 + 0.7655977011 0.7543541789 0.7319474816 + 0.7663068771 0.7550362945 0.7326734066 + 0.7670161724 0.7557184100 0.7333992720 + 0.7677254081 0.7564005256 0.7341251969 + 0.7684330940 0.7570810914 0.7348505259 + 0.7691373229 0.7577580214 0.7355744839 + 0.7698414922 0.7584350109 0.7362985015 + 0.7705457211 0.7591118813 0.7370225787 + 0.7712497711 0.7597886920 0.7377464771 + 0.7719480991 0.7604609728 0.7384685278 + 0.7726464272 0.7611331940 0.7391905189 + 0.7733446956 0.7618054748 0.7399126291 + 0.7740430236 0.7624776959 0.7406346202 + 0.7747374177 0.7631471157 0.7413551211 + 0.7754303217 0.7638152242 0.7420752048 + 0.7761231065 0.7644832730 0.7427952290 + 0.7768160105 0.7651513815 0.7435151935 + 0.7775070071 0.7658178210 0.7442349195 + 0.7781947851 0.7664811015 0.7449539900 + 0.7788826227 0.7671443820 0.7456731200 + 0.7795704007 0.7678076029 0.7463921905 + 0.7802581787 0.7684708834 0.7471113205 + 0.7809401155 0.7691282034 0.7478293777 + 0.7816219926 0.7697854042 0.7485474944 + 0.7823038101 0.7704424858 0.7492656112 + 0.7829856873 0.7710996866 0.7499837279 + 0.7836645842 0.7717540264 0.7507011294 + 0.7843410969 0.7724062204 0.7514179945 + 0.7850177288 0.7730582952 0.7521349788 + 0.7856941819 0.7737104893 0.7528519034 + 0.7863702774 0.7743622065 0.7535687089 + 0.7870417833 0.7750092745 0.7542843819 + 0.7877132893 0.7756564021 0.7550001144 + 0.7883847952 0.7763035297 0.7557157874 + 0.7890563011 0.7769505978 0.7564315200 + 0.7897251844 0.7775946259 0.7571462989 + 0.7903923988 0.7782363892 0.7578606009 + 0.7910594940 0.7788782716 0.7585750222 + 0.7917265892 0.7795200944 0.7592893243 + 0.7923933864 0.7801616192 0.7600035071 + 0.7930548787 0.7807983756 0.7607163787 + 0.7937164903 0.7814353108 0.7614293098 + 0.7943781018 0.7820721269 0.7621421814 + 0.7950395942 0.7827090025 0.7628551126 + 0.7956982851 0.7833436728 0.7635670900 + 0.7963539958 0.7839763761 0.7642784715 + 0.7970097065 0.7846090794 0.7649897933 + 0.7976654172 0.7852417827 0.7657011151 + 0.7983211279 0.7858744860 0.7664124966 + 0.7989718914 0.7865037918 0.7671223283 + 0.7996219993 0.7871326208 0.7678319812 + 0.8002722263 0.7877613902 0.7685415745 + 0.8009223938 0.7883902192 0.7692512870 + 0.8015714288 0.7890179753 0.7699605227 + 0.8022168875 0.7896423936 0.7706683874 + 0.8028622866 0.7902668118 0.7713763118 + 0.8035078049 0.7908911705 0.7720841765 + 0.8041532040 0.7915155888 0.7727922201 + 0.8047962189 0.7921379209 0.7734990120 + 0.8054372072 0.7927585840 0.7742050290 + 0.8060783148 0.7933791876 0.7749111056 + 0.8067193031 0.7939997911 0.7756171227 + 0.8073604107 0.7946205139 0.7763231993 + 0.8079984188 0.7952384949 0.7770274878 + 0.8086358905 0.7958559990 0.7777315974 + 0.8092733026 0.7964735031 0.7784357071 + 0.8099107146 0.7970910072 0.7791396976 + 0.8105478287 0.7977082133 0.7798436284 + 0.8111795783 0.7983207703 0.7805455923 + 0.8118113279 0.7989333272 0.7812476158 + 0.8124430776 0.7995458841 0.7819495797 + 0.8130748868 0.8001585007 0.7826516032 + 0.8137050271 0.8007699251 0.7833529115 + 0.8143305779 0.8013781905 0.7840527296 + 0.8149561286 0.8019865155 0.7847524881 + 0.8155816197 0.8025947809 0.7854523063 + 0.8162071109 0.8032031059 0.7861521244 + 0.8168302774 0.8038098216 0.7868509293 + 0.8174502254 0.8044143915 0.7875484228 + 0.8180701137 0.8050190210 0.7882459164 + 0.8186900020 0.8056234717 0.7889434099 + 0.8193098903 0.8062281013 0.7896409035 + 0.8199270964 0.8068311214 0.7903370261 + 0.8205420971 0.8074325919 0.7910320759 + 0.8211570978 0.8080341220 0.7917271852 + 0.8217719793 0.8086355925 0.7924222946 + 0.8223869801 0.8092371225 0.7931174040 + 0.8229985237 0.8098356128 0.7938109040 + 0.8236079812 0.8104321957 0.7945033908 + 0.8242173791 0.8110287189 0.7951959968 + 0.8248267770 0.8116251826 0.7958884835 + 0.8254361749 0.8122218251 0.7965810895 + 0.8260415196 0.8128147125 0.7972719073 + 0.8266444802 0.8134055734 0.7979617715 + 0.8272475004 0.8139964938 0.7986516953 + 0.8278505206 0.8145874739 0.7993416190 + 0.8284534812 0.8151783943 0.8000316024 + 0.8290522099 0.8157662749 0.8007196188 + 0.8296483755 0.8163524866 0.8014068007 + 0.8302447200 0.8169388175 0.8020939827 + 0.8308408856 0.8175250292 0.8027811050 + 0.8314371109 0.8181111813 0.8034682870 + 0.8320297003 0.8186948895 0.8041536808 + 0.8326196074 0.8192769289 0.8048380017 + 0.8332095742 0.8198587894 0.8055223227 + 0.8337996006 0.8204407096 0.8062065840 + 0.8343895078 0.8210226893 0.8068909049 + 0.8349767923 0.8216028810 0.8075736761 + 0.8355613947 0.8221815228 0.8082550168 + 0.8361459970 0.8227599859 0.8089362979 + 0.8367305994 0.8233386278 0.8096176982 + 0.8373152018 0.8239170909 0.8102989793 + 0.8378980160 0.8244941235 0.8109787107 + 0.8384776711 0.8250682950 0.8116558790 + 0.8390573859 0.8256425858 0.8123331070 + 0.8396371007 0.8262168169 0.8130102158 + 0.8402168155 0.8267911077 0.8136873841 + 0.8407955170 0.8273645043 0.8143637180 + 0.8413699865 0.8279343843 0.8150367141 + 0.8419443965 0.8285043836 0.8157097101 + 0.8425189257 0.8290743232 0.8163827062 + 0.8430932760 0.8296442032 0.8170557022 + 0.8436678052 0.8302142024 0.8177286983 + 0.8442364931 0.8307793736 0.8183974028 + 0.8448051214 0.8313444853 0.8190662265 + 0.8453736901 0.8319095969 0.8197348714 + 0.8459423184 0.8324747086 0.8204035759 + 0.8465108275 0.8330397010 0.8210722804 + 0.8470758796 0.8336018920 0.8217378855 + 0.8476396799 0.8341627717 0.8224022985 + 0.8482034206 0.8347237706 0.8230667114 + 0.8487671018 0.8352848291 0.8237311244 + 0.8493307829 0.8358457088 0.8243954778 + 0.8498923182 0.8364052773 0.8250575066 + 0.8504505157 0.8369628191 0.8257161975 + 0.8510087132 0.8375203013 0.8263750076 + 0.8515669703 0.8380777836 0.8270336986 + 0.8521252275 0.8386353254 0.8276925087 + 0.8526831269 0.8391926289 0.8283507228 + 0.8532357216 0.8397459984 0.8290038705 + 0.8537883759 0.8402994871 0.8296570778 + 0.8543409705 0.8408529162 0.8303104043 + 0.8548936248 0.8414064050 0.8309636116 + 0.8554462194 0.8419598937 0.8316168189 + 0.8559948802 0.8425098062 0.8322660923 + 0.8565415144 0.8430578709 0.8329136968 + 0.8570882082 0.8436061144 0.8335613012 + 0.8576349020 0.8441541791 0.8342089057 + 0.8581814766 0.8447024226 0.8348565102 + 0.8587269783 0.8452494144 0.8355026841 + 0.8592686057 0.8457927704 0.8361449242 + 0.8598102927 0.8463361263 0.8367869854 + 0.8603519201 0.8468794227 0.8374292254 + 0.8608934879 0.8474227190 0.8380714059 + 0.8614351153 0.8479660153 0.8387135267 + 0.8619735241 0.8485065103 0.8393514156 + 0.8625109196 0.8490459919 0.8399881124 + 0.8630483150 0.8495854735 0.8406246901 + 0.8635855913 0.8501250148 0.8412613869 + 0.8641229868 0.8506644964 0.8418980837 + 0.8646594286 0.8512032032 0.8425334096 + 0.8651921749 0.8517394066 0.8431646824 + 0.8657249808 0.8522754908 0.8437960744 + 0.8662577868 0.8528116941 0.8444274068 + 0.8667905927 0.8533477783 0.8450586796 + 0.8673233986 0.8538839817 0.8456900120 + 0.8678523898 0.8544178009 0.8463178277 + 0.8683791757 0.8549503088 0.8469436765 + 0.8689060211 0.8554828167 0.8475697041 + 0.8694328070 0.8560153246 0.8481956124 + 0.8699597120 0.8565478921 0.8488215804 + 0.8704863787 0.8570802808 0.8494471908 + 0.8710079193 0.8576077819 0.8500669003 + 0.8715295196 0.8581352830 0.8506866097 + 0.8720510006 0.8586627841 0.8513063192 + 0.8725726008 0.8591902852 0.8519260287 + 0.8730940819 0.8597177863 0.8525456786 + 0.8736137748 0.8602437973 0.8531630039 + 0.8741300106 0.8607671261 0.8537763953 + 0.8746461868 0.8612903953 0.8543899059 + 0.8751623034 0.8618137240 0.8550032973 + 0.8756784797 0.8623369932 0.8556168079 + 0.8761947155 0.8628603220 0.8562303185 + 0.8767071962 0.8633813858 0.8568397164 + 0.8772177100 0.8639011979 0.8574470282 + 0.8777281046 0.8644211292 0.8580543995 + 0.8782384992 0.8649408817 0.8586617112 + 0.8787490129 0.8654606938 0.8592690825 + 0.8792594075 0.8659806252 0.8598763943 + 0.8797658086 0.8664970994 0.8604782820 + 0.8802716732 0.8670130968 0.8610795736 + 0.8807774782 0.8675292134 0.8616809249 + 0.8812834024 0.8680452108 0.8622822165 + 0.8817893267 0.8685612082 0.8628833890 + 0.8822947145 0.8690769076 0.8634842038 + 0.8827946782 0.8695878983 0.8640792966 + 0.8832945824 0.8700988889 0.8646745086 + 0.8837944865 0.8706098795 0.8652696013 + 0.8842945099 0.8711209297 0.8658648133 + 0.8847944140 0.8716319203 0.8664600253 + 0.8852931261 0.8721420169 0.8670535088 + 0.8857870102 0.8726484776 0.8676418066 + 0.8862808943 0.8731549978 0.8682301044 + 0.8867747784 0.8736615181 0.8688184023 + 0.8872687221 0.8741679788 0.8694065809 + 0.8877626061 0.8746744990 0.8699948788 + 0.8882550001 0.8751800060 0.8705809712 + 0.8887438774 0.8756831288 0.8711625934 + 0.8892328143 0.8761863112 0.8717442155 + 0.8897215724 0.8766893744 0.8723257780 + 0.8902105093 0.8771924973 0.8729074001 + 0.8906993866 0.8776956201 0.8734890223 + 0.8911864758 0.8781973720 0.8740683198 + 0.8916702867 0.8786967993 0.8746433854 + 0.8921542168 0.8791961074 0.8752185106 + 0.8926380277 0.8796954155 0.8757935762 + 0.8931217790 0.8801947236 0.8763688207 + 0.8936055899 0.8806940913 0.8769438863 + 0.8940873146 0.8811920285 0.8775168061 + 0.8945649266 0.8816872239 0.8780856133 + 0.8950424790 0.8821823001 0.8786544800 + 0.8955200911 0.8826774955 0.8792232871 + 0.8959977031 0.8831726909 0.8797922134 + 0.8964753151 0.8836678863 0.8803610802 + 0.8969513178 0.8841618896 0.8809276819 + 0.8974236250 0.8846526742 0.8814892769 + 0.8978958130 0.8851435781 0.8820508718 + 0.8983680010 0.8856344223 0.8826125264 + 0.8988403082 0.8861253262 0.8831741810 + 0.8993124962 0.8866161108 0.8837357759 + 0.8997837901 0.8871064186 0.8842961192 + 0.9002509117 0.8875938058 0.8848509789 + 0.9007179141 0.8880811930 0.8854060173 + 0.9011849761 0.8885685802 0.8859608769 + 0.9016519785 0.8890560269 0.8865159154 + 0.9021189809 0.8895434141 0.8870707750 + 0.9025859833 0.8900306821 0.8876255155 + 0.9030467272 0.8905134201 0.8881742954 + 0.9035072923 0.8909960985 0.8887230158 + 0.9039679766 0.8914787173 0.8892716765 + 0.9044287205 0.8919613957 0.8898203969 + 0.9048894048 0.8924440742 0.8903691769 + 0.9053500891 0.8929268122 0.8909178972 + 0.9058066010 0.8934053183 0.8914616704 + 0.9062620997 0.8938828707 0.8920044899 + 0.9067175984 0.8943604827 0.8925474286 + 0.9071730971 0.8948379755 0.8930901885 + 0.9076285958 0.8953155875 0.8936330080 + 0.9080840945 0.8957931995 0.8941758871 + 0.9085366726 0.8962686062 0.8947150111 + 0.9089869857 0.8967422247 0.8952515125 + 0.9094372988 0.8972157836 0.8957878947 + 0.9098876119 0.8976894021 0.8963243961 + 0.9103379250 0.8981630206 0.8968607783 + 0.9107881784 0.8986366987 0.8973972201 + 0.9112372994 0.8991093040 0.8979318142 + 0.9116824865 0.8995786905 0.8984611034 + 0.9121276736 0.9000480771 0.8989903927 + 0.9125729203 0.9005175233 0.8995196819 + 0.9130182266 0.9009870291 0.9000489712 + 0.9134634137 0.9014564157 0.9005783200 + 0.9139086008 0.9019258022 0.9011074901 + 0.9143493772 0.9023923278 0.9016308784 + 0.9147896171 0.9028583765 0.9021536112 + 0.9152297974 0.9033244848 0.9026762843 + 0.9156699777 0.9037905931 0.9031988978 + 0.9161102176 0.9042567015 0.9037215710 + 0.9165503979 0.9047226906 0.9042443037 + 0.9169877172 0.9051871896 0.9047639966 + 0.9174218178 0.9056496024 0.9052804708 + 0.9178559780 0.9061120749 0.9057971239 + 0.9182900786 0.9065744877 0.9063135982 + 0.9187241793 0.9070370197 0.9068300724 + 0.9191582799 0.9074993730 0.9073466063 + 0.9195923805 0.9079619050 0.9078630805 + 0.9200205803 0.9084206820 0.9083728194 + 0.9204487801 0.9088795185 0.9088826180 + 0.9208769798 0.9093382955 0.9093922973 + 0.9213051796 0.9097970724 0.9099020958 + 0.9217333794 0.9102559090 0.9104117751 + 0.9221615195 0.9107146263 0.9109215140 + 0.9225875139 0.9111716747 0.9114277959 + 0.9230108261 0.9116266966 0.9119303823 + 0.9234341979 0.9120817780 0.9124330878 + 0.9238576293 0.9125369191 0.9129356742 + 0.9242808819 0.9129918814 0.9134383202 + 0.9247043133 0.9134470224 0.9139409065 + 0.9251276851 0.9139019847 0.9144436121 + 0.9255468845 0.9143533707 0.9149402976 + 0.9259656072 0.9148042798 0.9154363871 + 0.9263843298 0.9152551889 0.9159325957 + 0.9268029928 0.9157060981 0.9164286852 + 0.9272217155 0.9161570072 0.9169248939 + 0.9276403785 0.9166079164 0.9174209833 + 0.9280576706 0.9170578122 0.9179155231 + 0.9284709096 0.9175047278 0.9184057117 + 0.9288840890 0.9179517031 0.9188959002 + 0.9292973280 0.9183986187 0.9193860888 + 0.9297105074 0.9188455939 0.9198762774 + 0.9301236868 0.9192925096 0.9203665257 + 0.9305369258 0.9197394848 0.9208567142 + 0.9309471846 0.9201840758 0.9213432074 + 0.9313554764 0.9206268787 0.9218271971 + 0.9317637086 0.9210698009 0.9223111272 + 0.9321720004 0.9215126038 0.9227951169 + 0.9325801730 0.9219554067 0.9232789874 + 0.9329885244 0.9223983288 0.9237629771 + 0.9333966970 0.9228410721 0.9242469072 + 0.9338006973 0.9232804775 0.9247245193 + 0.9342038035 0.9237191081 0.9252008200 + 0.9346070290 0.9241576791 0.9256771207 + 0.9350100756 0.9245964289 0.9261534810 + 0.9354133010 0.9250349998 0.9266297817 + 0.9358164072 0.9254735708 0.9271062016 + 0.9362192750 0.9259120822 0.9275822043 + 0.9366164804 0.9263464808 0.9280517101 + 0.9370136857 0.9267808795 0.9285210967 + 0.9374108911 0.9272152781 0.9289906025 + 0.9378080964 0.9276496768 0.9294601083 + 0.9382053018 0.9280840755 0.9299296141 + 0.9386025071 0.9285184741 0.9303991199 + 0.9389988780 0.9289522767 0.9308673143 + 0.9393916130 0.9293835163 0.9313306212 + 0.9397842884 0.9298146963 0.9317938089 + 0.9401770830 0.9302458763 0.9322569966 + 0.9405698180 0.9306771159 0.9327201843 + 0.9409624934 0.9311082959 0.9331833720 + 0.9413552284 0.9315395951 0.9336466193 + 0.9417470098 0.9319697022 0.9341077209 + 0.9421362281 0.9323967099 0.9345635176 + 0.9425255060 0.9328237176 0.9350193143 + 0.9429147243 0.9332507849 0.9354749918 + 0.9433040023 0.9336777925 0.9359307885 + 0.9436932206 0.9341048002 0.9363865256 + 0.9440824986 0.9345319271 0.9368423223 + 0.9444702864 0.9349573851 0.9372959733 + 0.9448544979 0.9353792071 0.9377450943 + 0.9452388287 0.9358010292 0.9381942153 + 0.9456230998 0.9362229109 0.9386432171 + 0.9460073113 0.9366446733 0.9390922785 + 0.9463915825 0.9370664954 0.9395413995 + 0.9467759132 0.9374883175 0.9399905205 + 0.9471582770 0.9379090071 0.9404376745 + 0.9475358129 0.9383267164 0.9408801198 + 0.9479132891 0.9387443066 0.9413226247 + 0.9482908249 0.9391620159 0.9417650104 + 0.9486683011 0.9395796061 0.9422073960 + 0.9490457773 0.9399973154 0.9426497817 + 0.9494233131 0.9404149055 0.9430922866 + 0.9497994184 0.9408314824 0.9435328841 + 0.9501690865 0.9412431121 0.9439659715 + 0.9505386949 0.9416546822 0.9443989992 + 0.9509084225 0.9420663118 0.9448320866 + 0.9512780905 0.9424778819 0.9452651739 + 0.9516478181 0.9428895116 0.9456983209 + 0.9520174861 0.9433010817 0.9461312890 + 0.9523869157 0.9437124729 0.9465638995 + 0.9527491927 0.9441193938 0.9469869137 + 0.9531115294 0.9445263147 0.9474099278 + 0.9534738064 0.9449331760 0.9478328824 + 0.9538360834 0.9453400970 0.9482558966 + 0.9541984200 0.9457470179 0.9486789107 + 0.9545606971 0.9461538792 0.9491018057 + 0.9549229741 0.9465608001 0.9495248199 + 0.9552813768 0.9469650984 0.9499412179 + 0.9556391239 0.9473688006 0.9503563046 + 0.9559966922 0.9477726221 0.9507713914 + 0.9563543200 0.9481763244 0.9511865973 + 0.9567118883 0.9485800862 0.9516016841 + 0.9570695162 0.9489837885 0.9520167708 + 0.9574272037 0.9493876100 0.9524319768 + 0.9577826262 0.9497889876 0.9528437853 + 0.9581363201 0.9501885772 0.9532533288 + 0.9584900737 0.9505882263 0.9536628127 + 0.9588438272 0.9509878159 0.9540722966 + 0.9591975808 0.9513874054 0.9544817805 + 0.9595512748 0.9517869949 0.9548913240 + 0.9599050879 0.9521865845 0.9553006887 + 0.9602577090 0.9525852203 0.9557086825 + 0.9606065750 0.9529808164 0.9561120272 + 0.9609556198 0.9533764124 0.9565153718 + 0.9613046050 0.9537721276 0.9569187164 + 0.9616534710 0.9541677237 0.9573221207 + 0.9620025158 0.9545633197 0.9577255249 + 0.9623513818 0.9549589753 0.9581288099 + 0.9627004266 0.9553545713 0.9585322142 + 0.9630452991 0.9557468891 0.9589298964 + 0.9633895755 0.9561386704 0.9593268037 + 0.9637339115 0.9565305114 0.9597237110 + 0.9640781879 0.9569222927 0.9601204991 + 0.9644225240 0.9573140740 0.9605174065 + 0.9647668004 0.9577057958 0.9609143138 + 0.9651110172 0.9580975771 0.9613112211 + 0.9654532075 0.9584879279 0.9617047906 + 0.9657922983 0.9588758945 0.9620940089 + 0.9661315084 0.9592639804 0.9624832273 + 0.9664705992 0.9596521258 0.9628723860 + 0.9668096900 0.9600400925 0.9632616043 + 0.9671489000 0.9604281783 0.9636508822 + 0.9674879909 0.9608163238 0.9640401006 + 0.9678270817 0.9612042904 0.9644293189 + 0.9681611061 0.9615883827 0.9648113251 + 0.9684945941 0.9619721174 0.9651926756 + 0.9688280225 0.9623557925 0.9655740857 + 0.9691613913 0.9627394080 0.9659554958 + 0.9694948792 0.9631230831 0.9663369060 + 0.9698283076 0.9635068178 0.9667183161 + 0.9701616764 0.9638903737 0.9670996070 + 0.9704933763 0.9642724991 0.9674782753 + 0.9708213806 0.9646512270 0.9678518176 + 0.9711495042 0.9650298953 0.9682253003 + 0.9714776278 0.9654086828 0.9685987830 + 0.9718056917 0.9657874107 0.9689723253 + 0.9721336961 0.9661661983 0.9693458080 + 0.9724618196 0.9665449262 0.9697191715 + 0.9727898836 0.9669237137 0.9700927138 + 0.9731143117 0.9672991037 0.9704605937 + 0.9734373093 0.9676731825 0.9708260894 + 0.9737601876 0.9680472016 0.9711915851 + 0.9740831852 0.9684212804 0.9715570807 + 0.9744061232 0.9687952995 0.9719226956 + 0.9747291207 0.9691693783 0.9722881913 + 0.9750519991 0.9695433974 0.9726536870 + 0.9753749967 0.9699174762 0.9730191231 + 0.9756926894 0.9702869058 0.9733765125 + 0.9760103822 0.9706562757 0.9737339020 + 0.9763280749 0.9710255861 0.9740912914 + 0.9766458273 0.9713950157 0.9744486809 + 0.9769635201 0.9717643857 0.9748060703 + 0.9772812128 0.9721338153 0.9751635194 + 0.9775989056 0.9725031853 0.9755209088 + 0.9779152274 0.9728714228 0.9758762121 + 0.9782270789 0.9732357860 0.9762253165 + 0.9785388708 0.9736002088 0.9765743017 + 0.9788507819 0.9739645720 0.9769234061 + 0.9791625738 0.9743289948 0.9772723913 + 0.9794744253 0.9746934772 0.9776214957 + 0.9797862768 0.9750579000 0.9779704809 + 0.9800981283 0.9754223228 0.9783195853 + 0.9804072976 0.9757844210 0.9786648750 + 0.9807128906 0.9761431813 0.9790049791 + 0.9810184240 0.9765020013 0.9793450832 + 0.9813238978 0.9768608212 0.9796851873 + 0.9816294909 0.9772195816 0.9800254107 + 0.9819350243 0.9775784016 0.9803655148 + 0.9822404981 0.9779372215 0.9807056189 + 0.9825460911 0.9782959819 0.9810457230 + 0.9828479290 0.9786517024 0.9813808799 + 0.9831464887 0.9790048003 0.9817116857 + 0.9834451079 0.9793578982 0.9820424914 + 0.9837437272 0.9797109962 0.9823732972 + 0.9840422869 0.9800640941 0.9827041030 + 0.9843410254 0.9804171920 0.9830349088 + 0.9846395850 0.9807702899 0.9833657146 + 0.9849382043 0.9811233878 0.9836965203 + 0.9852322936 0.9814730883 0.9840217233 + 0.9855231047 0.9818202257 0.9843425751 + 0.9858139157 0.9821674228 0.9846634865 + 0.9861047268 0.9825145006 0.9849843979 + 0.9863954782 0.9828615785 0.9853054285 + 0.9866862893 0.9832087755 0.9856262803 + 0.9869771004 0.9835559130 0.9859471917 + 0.9872679114 0.9839029908 0.9862681031 + 0.9875541925 0.9842463732 0.9865829945 + 0.9878368974 0.9845867157 0.9868929982 + 0.9881196022 0.9849269986 0.9872030020 + 0.9884023070 0.9852672815 0.9875130057 + 0.9886850119 0.9856076241 0.9878228903 + 0.9889677167 0.9859479070 0.9881328940 + 0.9892504215 0.9862881899 0.9884428978 + 0.9895331264 0.9866284728 0.9887529016 + 0.9898114204 0.9869654179 0.9890568256 + 0.9900845885 0.9872981906 0.9893537164 + 0.9903576970 0.9876310825 0.9896506071 + 0.9906309247 0.9879639149 0.9899474978 + 0.9909039736 0.9882966876 0.9902443886 + 0.9911772013 0.9886295795 0.9905412793 + 0.9914503098 0.9889624119 0.9908382297 + 0.9917234778 0.9892953038 0.9911351204 + 0.9919933081 0.9896262288 0.9914277196 + 0.9922555089 0.9899526834 0.9917106032 + 0.9925177097 0.9902790785 0.9919934273 + 0.9927799106 0.9906055927 0.9922763109 + 0.9930421114 0.9909321070 0.9925591946 + 0.9933043122 0.9912585020 0.9928420782 + 0.9935665131 0.9915850163 0.9931250215 + 0.9938287139 0.9919114709 0.9934079051 + 0.9940900207 0.9922373891 0.9936895967 + 0.9943411946 0.9925572872 0.9939587712 + 0.9945924878 0.9928773046 0.9942278862 + 0.9948437214 0.9931972027 0.9944971204 + 0.9950950146 0.9935171008 0.9947661757 + 0.9953461885 0.9938369989 0.9950354099 + 0.9955974221 0.9941570163 0.9953045845 + 0.9958487153 0.9944769144 0.9955736995 + 0.9960998893 0.9947968125 0.9958428741 + 0.9963402152 0.9951102734 0.9960997105 + 0.9965775013 0.9954220057 0.9963533282 + 0.9968149066 0.9957337976 0.9966068864 + 0.9970521927 0.9960455894 0.9968605042 + 0.9972895980 0.9963573217 0.9971140027 + 0.9975268841 0.9966691136 0.9973676205 + 0.9977642894 0.9969809055 0.9976211786 + 0.9980015755 0.9972925782 0.9978747964 + 0.9982318878 0.9975994229 0.9981204271 + 0.9984529018 0.9978994727 0.9983553290 + 0.9986739755 0.9981995225 0.9985902905 + 0.9988949895 0.9984995723 0.9988251925 + 0.9991160035 0.9987996817 0.9990602136 + 0.9993370175 0.9990997910 0.9992951155 + 0.9995579720 0.9993997812 0.9995300770 + 0.9997789860 0.9996998906 0.9997649789 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_vista_200.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_vista_200.spi1d index 4ef1e35a..ec4f1420 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_vista_200.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_vista_200.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0182101503 0.0179095902 0.0133477096 - 0.0321649686 0.0326713808 0.0245428905 - 0.0435327813 0.0445400514 0.0339762792 - 0.0535616502 0.0549106896 0.0424365215 - 0.0627619624 0.0643109381 0.0502561592 - 0.0713807195 0.0730527714 0.0576005317 - 0.0795889422 0.0812662989 0.0646079034 - 0.0874749497 0.0890629813 0.0713189989 - 0.0951398313 0.0965353027 0.0778023675 - 0.1026329994 0.1037214994 0.0840878412 - 0.1098783985 0.1106367037 0.0901622772 - 0.1168456972 0.1172993034 0.0960228518 - 0.1235098019 0.1237140000 0.1016969979 - 0.1299058944 0.1299183965 0.1071913987 - 0.1360470951 0.1359218955 0.1125032008 - 0.1419679970 0.1417497993 0.1176472977 - 0.1476961970 0.1474111974 0.1226311028 - 0.1532527953 0.1529219002 0.1274778992 - 0.1586499959 0.1582891047 0.1321873963 - 0.1639094949 0.1635248065 0.1367731988 - 0.1690364927 0.1686291993 0.1412428021 - 0.1740324050 0.1736201942 0.1455994993 - 0.1789164990 0.1784920990 0.1498488039 - 0.1836923957 0.1832599044 0.1539963931 - 0.1883690059 0.1879281998 0.1580594927 - 0.1929446012 0.1924999058 0.1620312035 - 0.1974374950 0.1969808936 0.1659148932 - 0.2018467933 0.2013725042 0.1697210968 - 0.2061869949 0.2056811005 0.1734540015 - 0.2104454935 0.2099131942 0.1771208942 - 0.2146379054 0.2140665054 0.1807264984 - 0.2187689990 0.2181411982 0.1842699945 - 0.2228457928 0.2221457958 0.1877449006 - 0.2268676013 0.2260929942 0.1911672056 - 0.2308367044 0.2299744040 0.1945340037 - 0.2347573936 0.2337978929 0.1978531927 - 0.2386282980 0.2375611067 0.2011176944 - 0.2424549013 0.2412583977 0.2043330073 - 0.2462375015 0.2449042946 0.2075064927 - 0.2499749959 0.2485097051 0.2106339037 - 0.2536737919 0.2520638108 0.2137185931 - 0.2573330998 0.2555652857 0.2167686969 - 0.2609579861 0.2590112984 0.2197788060 - 0.2645516098 0.2624117136 0.2227548957 - 0.2681075931 0.2657625973 0.2257010043 - 0.2716293931 0.2690761089 0.2286102027 - 0.2751240134 0.2723546028 0.2314918935 - 0.2785834074 0.2755965889 0.2343432009 - 0.2820093036 0.2787989080 0.2371643037 - 0.2854062021 0.2819635868 0.2399580032 - 0.2887754142 0.2850886881 0.2427307069 - 0.2921054959 0.2881690860 0.2454746962 - 0.2953915000 0.2912029028 0.2481936961 - 0.2986359894 0.2941966951 0.2508893013 - 0.3018450141 0.2971538901 0.2535605133 - 0.3050135970 0.3000777960 0.2562063038 - 0.3081485033 0.3029662073 0.2588256896 - 0.3112491965 0.3058190048 0.2614240944 - 0.3143157065 0.3086350858 0.2640033066 - 0.3173457086 0.3114137053 0.2665627897 - 0.3203401864 0.3141573966 0.2691006958 - 0.3232952952 0.3168714046 0.2716197073 - 0.3262135088 0.3195553124 0.2741247118 - 0.3290931880 0.3222120106 0.2766116858 - 0.3319376111 0.3248479962 0.2790769935 - 0.3347463012 0.3274633884 0.2815260887 - 0.3375192881 0.3300592899 0.2839562893 - 0.3402611911 0.3326362073 0.2863681912 - 0.3429714143 0.3351945877 0.2887654006 - 0.3456538916 0.3377344012 0.2911457121 - 0.3483096957 0.3402554095 0.2935140133 - 0.3509393930 0.3427593112 0.2958686948 - 0.3535420001 0.3452464938 0.2982082963 - 0.3561150134 0.3477157056 0.3005363941 - 0.3586607873 0.3501690924 0.3028526008 - 0.3611787856 0.3526060879 0.3051568866 - 0.3636738956 0.3550266922 0.3074469864 - 0.3661471903 0.3574325144 0.3097249866 - 0.3686017990 0.3598220050 0.3119905889 - 0.3710421920 0.3621949852 0.3142440021 - 0.3734661043 0.3645452857 0.3164834976 - 0.3758763969 0.3668780029 0.3187099993 - 0.3782708049 0.3691841960 0.3209230006 - 0.3806518912 0.3714686930 0.3231270015 - 0.3830181956 0.3737291098 0.3253178000 - 0.3853709102 0.3759689927 0.3274958134 - 0.3877105117 0.3781890869 0.3296644986 - 0.3900356889 0.3803892136 0.3318220973 - 0.3923485875 0.3825727105 0.3339678943 - 0.3946483135 0.3847385943 0.3360984921 - 0.3969348967 0.3868887126 0.3382169902 - 0.3992097080 0.3890219927 0.3403255939 - 0.4014719129 0.3911359906 0.3424212933 - 0.4037215114 0.3932318985 0.3445019126 - 0.4059596956 0.3953123093 0.3465686142 - 0.4081867933 0.3973782957 0.3486247063 - 0.4103969038 0.3994235098 0.3506675959 - 0.4125916958 0.4014526010 0.3526985049 - 0.4147717953 0.4034663141 0.3547205925 - 0.4169346094 0.4054656029 0.3567343056 - 0.4190779030 0.4074513912 0.3587369919 - 0.4212022126 0.4094235003 0.3607279956 - 0.4233098924 0.4113812149 0.3627093136 - 0.4254015088 0.4133256972 0.3646796942 - 0.4274776876 0.4152582884 0.3666377068 - 0.4295392036 0.4171769023 0.3685849011 - 0.4315859079 0.4190813899 0.3705236912 - 0.4336147010 0.4209738970 0.3724532127 - 0.4356271923 0.4228563905 0.3743726909 - 0.4376241863 0.4247288108 0.3762832880 - 0.4396047890 0.4265901148 0.3781884909 - 0.4415675104 0.4284402132 0.3800868988 - 0.4435133040 0.4302783012 0.3819774091 - 0.4454411864 0.4321058095 0.3838588893 - 0.4473508894 0.4339239895 0.3857302964 - 0.4492434859 0.4357284009 0.3875930905 - 0.4511201978 0.4375185072 0.3894470930 - 0.4529820085 0.4392957091 0.3912915885 - 0.4548301101 0.4410578907 0.3931255937 - 0.4566622972 0.4428066909 0.3949485123 - 0.4584765136 0.4445427954 0.3967649043 - 0.4602738023 0.4462682009 0.3985747099 - 0.4620555043 0.4479798973 0.4003768861 - 0.4638229907 0.4496806860 0.4021708071 - 0.4655745029 0.4513705969 0.4039582014 - 0.4673084915 0.4530492127 0.4057385027 - 0.4690262079 0.4547170997 0.4075132012 - 0.4707291126 0.4563761950 0.4092822969 - 0.4724188149 0.4580253959 0.4110448956 - 0.4740945101 0.4596652091 0.4128004014 - 0.4757550955 0.4612967968 0.4145481884 - 0.4774011075 0.4629195929 0.4162878096 - 0.4790340960 0.4645322859 0.4180184007 - 0.4806557000 0.4661347866 0.4197404087 - 0.4822664857 0.4677279890 0.4214550853 - 0.4838629961 0.4693109989 0.4231620133 - 0.4854427874 0.4708836973 0.4248597026 - 0.4870088995 0.4724482000 0.4265482128 - 0.4885635972 0.4740057886 0.4282296002 - 0.4901061058 0.4755555987 0.4299038947 - 0.4916355014 0.4770967066 0.4315708876 - 0.4931511879 0.4786292911 0.4332301915 - 0.4946528077 0.4801545143 0.4348819852 - 0.4961408973 0.4816705883 0.4365274012 - 0.4976170063 0.4831782877 0.4381654859 - 0.4990840852 0.4846773148 0.4397968054 - 0.5005418062 0.4861674011 0.4414210916 - 0.5019897819 0.4876480103 0.4430381060 - 0.5034281015 0.4891189933 0.4446476102 - 0.5048564076 0.4905793965 0.4462502003 - 0.5062754154 0.4920318127 0.4478481114 - 0.5076872110 0.4934777915 0.4494414032 - 0.5090919137 0.4949184060 0.4510299861 - 0.5104892850 0.4963541031 0.4526137114 - 0.5118765831 0.4977797866 0.4541929066 - 0.5132548809 0.4991959929 0.4557681084 - 0.5146262050 0.5006046891 0.4573394060 - 0.5159909725 0.5020067096 0.4589067101 - 0.5173475742 0.5034021735 0.4604690969 - 0.5186926126 0.5047916770 0.4620254934 - 0.5200299025 0.5061759949 0.4635772109 - 0.5213602781 0.5075541139 0.4651241004 - 0.5226839185 0.5089235902 0.4666649997 - 0.5240020156 0.5102841854 0.4681988955 - 0.5253158808 0.5116395950 0.4697273076 - 0.5266252756 0.5129901171 0.4712502062 - 0.5279287100 0.5143331885 0.4727657139 - 0.5292261243 0.5156680942 0.4742735922 - 0.5305185914 0.5169963837 0.4757766128 - 0.5318061113 0.5183182955 0.4772751927 - 0.5330879092 0.5196315050 0.4787661135 - 0.5343660116 0.5209389925 0.4802523851 - 0.5356402993 0.5222411752 0.4817340076 - 0.5369089246 0.5235363245 0.4832083881 - 0.5381723046 0.5248249173 0.4846785069 - 0.5394316912 0.5261074901 0.4861451089 - 0.5406854153 0.5273811817 0.4876070917 - 0.5419325829 0.5286462903 0.4890637100 - 0.5431752801 0.5299065113 0.4905168116 - 0.5444123149 0.5311611295 0.4919654131 - 0.5456442237 0.5324099064 0.4934079051 - 0.5468729138 0.5336548090 0.4948467910 - 0.5480973125 0.5348948240 0.4962810874 - 0.5493161082 0.5361279249 0.4977090061 - 0.5505313873 0.5373569727 0.4991332889 - 0.5517416000 0.5385804772 0.5005524158 - 0.5529457927 0.5397964120 0.5019652843 - 0.5541468859 0.5410084128 0.5033745766 - 0.5553439856 0.5422148705 0.5047777891 - 0.5565372705 0.5434154272 0.5061753988 - 0.5577278137 0.5446121097 0.5075693727 - 0.5589103103 0.5458009839 0.5089558959 - 0.5600882173 0.5469862819 0.5103380084 - 0.5612615943 0.5481684208 0.5117158890 - 0.5624287128 0.5493460298 0.5130857825 - 0.5635939837 0.5505207777 0.5144522190 - 0.5647562146 0.5516902208 0.5158123970 - 0.5659151077 0.5528519750 0.5171663761 - 0.5670722127 0.5540099740 0.5185170174 - 0.5682216287 0.5551596284 0.5198565125 - 0.5693680048 0.5563063025 0.5211923718 - 0.5705093145 0.5574489832 0.5225226283 - 0.5716463923 0.5585880280 0.5238460898 - 0.5727816820 0.5597252250 0.5251665711 - 0.5739135742 0.5608549118 0.5264781117 - 0.5750433803 0.5619807243 0.5277863741 - 0.5761687756 0.5630995035 0.5290886760 - 0.5772883892 0.5642123818 0.5303848982 - 0.5784044862 0.5653222799 0.5316777825 - 0.5795143247 0.5664256811 0.5329598784 - 0.5806226730 0.5675271749 0.5342394114 - 0.5817270875 0.5686224103 0.5355114937 - 0.5828294754 0.5697141886 0.5367797017 - 0.5839291215 0.5708016157 0.5380430222 - 0.5850237012 0.5718852878 0.5393006206 - 0.5861157179 0.5729672909 0.5405554175 - 0.5872008204 0.5740441084 0.5418028235 - 0.5882844925 0.5751196146 0.5430486798 - 0.5893629789 0.5761870146 0.5442872047 - 0.5904405713 0.5772519112 0.5455237031 - 0.5915157199 0.5783103704 0.5467544794 - 0.5925881267 0.5793656707 0.5479820967 - 0.5936570168 0.5804172754 0.5492056012 - 0.5947207212 0.5814660788 0.5504246950 - 0.5957809091 0.5825130939 0.5516411066 - 0.5968360901 0.5835543871 0.5528522134 - 0.5978894830 0.5845938921 0.5540617108 - 0.5989391208 0.5856245756 0.5552654266 - 0.5999881029 0.5866541862 0.5564681888 - 0.6010323763 0.5876761079 0.5576648116 - 0.6020759940 0.5886973739 0.5588607192 - 0.6031129956 0.5897135735 0.5600510836 - 0.6041488051 0.5907285810 0.5612400770 - 0.6051782966 0.5917387009 0.5624223948 - 0.6062065959 0.5927467942 0.5636029243 - 0.6072307825 0.5937488079 0.5647776127 - 0.6082534790 0.5947481990 0.5659500957 - 0.6092721820 0.5957416296 0.5671173930 - 0.6102892160 0.5967329144 0.5682824850 - 0.6113023162 0.5977199078 0.5694428086 - 0.6123135090 0.5987045169 0.5706002116 - 0.6133208871 0.5996844769 0.5717515945 - 0.6143262982 0.6006621718 0.5729001760 - 0.6153277755 0.6016355753 0.5740433931 - 0.6163274050 0.6026067138 0.5751839876 - 0.6173228025 0.6035735011 0.5763192177 - 0.6183164120 0.6045382023 0.5774521828 - 0.6193056703 0.6054980159 0.5785800815 - 0.6202933788 0.6064559221 0.5797063112 - 0.6212763786 0.6074082851 0.5808271170 - 0.6222583055 0.6083593965 0.5819467902 - 0.6232348084 0.6093046069 0.5830612183 - 0.6242107153 0.6102492809 0.5841749907 - 0.6251806021 0.6111894846 0.5852832198 - 0.6261504889 0.6121296883 0.5863913894 - 0.6271138787 0.6130650043 0.5874921083 - 0.6280770898 0.6140003204 0.5885927081 - 0.6290364861 0.6149287820 0.5896871090 - 0.6299952865 0.6158562899 0.5907804966 - 0.6309506893 0.6167783141 0.5918691754 - 0.6319050193 0.6176983714 0.5929561257 - 0.6328564286 0.6186152101 0.5940394998 - 0.6338058114 0.6195300221 0.5951203108 - 0.6347532272 0.6204434037 0.5961987972 - 0.6356977224 0.6213548779 0.5972741246 - 0.6366410255 0.6222651005 0.5983482003 - 0.6375805140 0.6231712103 0.5994184017 - 0.6385198832 0.6240770817 0.6004886031 - 0.6394541860 0.6249756217 0.6015521884 - 0.6403884888 0.6258741021 0.6026158929 - 0.6413171291 0.6267682910 0.6036748886 - 0.6422441006 0.6276612878 0.6047325730 - 0.6431670189 0.6285513043 0.6057875156 - 0.6440864801 0.6294386983 0.6068398952 - 0.6450045109 0.6303241253 0.6078910232 - 0.6459187269 0.6312044263 0.6089388132 - 0.6468328238 0.6320846081 0.6099866033 - 0.6477437019 0.6329575181 0.6110302806 - 0.6486546993 0.6338304281 0.6120740771 - 0.6495618224 0.6346995831 0.6131126285 - 0.6504672766 0.6355671883 0.6141489744 - 0.6513699889 0.6364325881 0.6151828170 - 0.6522679925 0.6372947097 0.6162124276 - 0.6531658173 0.6381564736 0.6172416806 - 0.6540582776 0.6390134096 0.6182652116 - 0.6549509168 0.6398702264 0.6192886233 - 0.6558411717 0.6407238245 0.6203082800 - 0.6567304134 0.6415758729 0.6213262081 - 0.6576179862 0.6424263716 0.6223418713 - 0.6585016847 0.6432734728 0.6233528256 - 0.6593855023 0.6441205740 0.6243637204 - 0.6602624059 0.6449629068 0.6253690124 - 0.6611385942 0.6458048224 0.6263738275 - 0.6620121002 0.6466442943 0.6273748279 - 0.6628826857 0.6474813819 0.6283720732 - 0.6637530923 0.6483181715 0.6293689013 - 0.6646183133 0.6491507292 0.6303573847 - 0.6654834747 0.6499832869 0.6313458085 - 0.6663444042 0.6508116722 0.6323295832 - 0.6672021747 0.6516373754 0.6333100796 - 0.6680591702 0.6524623036 0.6342896819 - 0.6689094901 0.6532815099 0.6352627873 - 0.6697599292 0.6541007161 0.6362358928 - 0.6706069708 0.6549171805 0.6372048855 - 0.6714519858 0.6557319760 0.6381713152 - 0.6722967029 0.6565461755 0.6391372085 - 0.6731383204 0.6573548913 0.6400974989 - 0.6739798784 0.6581636071 0.6410579085 - 0.6748195887 0.6589679122 0.6420152187 - 0.6756578088 0.6597688198 0.6429703236 - 0.6764960289 0.6605696082 0.6439253092 - 0.6773291230 0.6613646746 0.6448736787 - 0.6781619787 0.6621596813 0.6458221078 - 0.6789928079 0.6629531980 0.6467680931 - 0.6798204780 0.6637446284 0.6477108002 - 0.6806482077 0.6645358801 0.6486533880 - 0.6814717054 0.6653245091 0.6495919824 - 0.6822941899 0.6661124825 0.6505295038 - 0.6831157207 0.6668993831 0.6514660120 - 0.6839330792 0.6676816940 0.6523975730 - 0.6847504973 0.6684640050 0.6533290744 - 0.6855651736 0.6692435145 0.6542578936 - 0.6863775253 0.6700205207 0.6551842093 - 0.6871898174 0.6707975864 0.6561105847 - 0.6879963279 0.6715716720 0.6570330858 - 0.6888018250 0.6723455191 0.6579551101 - 0.6896061897 0.6731184125 0.6588761210 - 0.6904050112 0.6738871932 0.6597927213 - 0.6912037730 0.6746560931 0.6607092023 - 0.6920003891 0.6754217744 0.6616228223 - 0.6927945018 0.6761841178 0.6625328064 - 0.6935886145 0.6769462824 0.6634427905 - 0.6943795085 0.6777051091 0.6643487811 - 0.6951690912 0.6784626842 0.6652531028 - 0.6959587932 0.6792201996 0.6661574841 - 0.6967442036 0.6799734235 0.6670572758 - 0.6975293159 0.6807262897 0.6679568291 - 0.6983137727 0.6814782023 0.6688550711 - 0.6990944743 0.6822254062 0.6697469950 - 0.6998751163 0.6829724908 0.6706389785 - 0.7006543279 0.6837174892 0.6715285182 - 0.7014306784 0.6844587922 0.6724137068 - 0.7022070885 0.6851999760 0.6732988954 - 0.7029811740 0.6859381795 0.6741808057 - 0.7037534118 0.6866738200 0.6750599742 - 0.7045254707 0.6874092817 0.6759392023 - 0.7052934766 0.6881412268 0.6768147945 - 0.7060595751 0.6888713837 0.6776887774 - 0.7068256140 0.6896017194 0.6785628200 - 0.7075870037 0.6903280020 0.6794317961 - 0.7083472013 0.6910533905 0.6802995801 - 0.7091073990 0.6917787790 0.6811674237 - 0.7098640800 0.6924982071 0.6820288897 - 0.7106204033 0.6932169795 0.6828898191 - 0.7113767266 0.6939358115 0.6837506890 - 0.7121276259 0.6946483254 0.6846057177 - 0.7128784060 0.6953607798 0.6854606867 - 0.7136288881 0.6960731149 0.6863154173 - 0.7143725157 0.6967815161 0.6871635914 - 0.7151160836 0.6974899173 0.6880118847 - 0.7158594131 0.6981980801 0.6888597012 - 0.7165976167 0.6989027858 0.6897020936 - 0.7173358798 0.6996076107 0.6905444860 - 0.7180739045 0.7003120780 0.6913864017 - 0.7188072801 0.7010132074 0.6922219992 - 0.7195405960 0.7017142773 0.6930575967 - 0.7202737927 0.7024151087 0.6938928962 - 0.7210023999 0.7031126022 0.6947227716 - 0.7217310071 0.7038099766 0.6955527067 - 0.7224596143 0.7045074105 0.6963825822 - 0.7231836915 0.7052016854 0.6972076297 - 0.7239078283 0.7058959007 0.6980326772 - 0.7246319056 0.7065901160 0.6988577247 - 0.7253506184 0.7072818279 0.6996777058 - 0.7260687947 0.7079733014 0.7004973292 - 0.7267869711 0.7086647749 0.7013168931 - 0.7275004983 0.7093524933 0.7021313906 - 0.7282128930 0.7100393772 0.7029448152 - 0.7289254069 0.7107263207 0.7037581801 - 0.7296341062 0.7114102840 0.7045679092 - 0.7303411961 0.7120929956 0.7053759098 - 0.7310482860 0.7127758265 0.7061840296 - 0.7317516804 0.7134565115 0.7069895864 - 0.7324519753 0.7141355872 0.7077932954 - 0.7331522107 0.7148147225 0.7085970044 - 0.7338507175 0.7154917121 0.7093988061 - 0.7345461249 0.7161653042 0.7101973295 - 0.7352414727 0.7168390155 0.7109956741 - 0.7359364033 0.7175115943 0.7117931843 - 0.7366288900 0.7181801796 0.7125861049 - 0.7373213172 0.7188488245 0.7133789062 - 0.7380138040 0.7195172906 0.7141718268 - 0.7387021184 0.7201828957 0.7149586082 - 0.7393900752 0.7208483815 0.7157452106 - 0.7400780916 0.7215139270 0.7165318131 - 0.7407631874 0.7221763134 0.7173146009 - 0.7414469719 0.7228373885 0.7180958986 - 0.7421306968 0.7234985232 0.7188770771 - 0.7428128123 0.7241570950 0.7196565866 - 0.7434924841 0.7248123884 0.7204334736 - 0.7441722155 0.7254676819 0.7212104201 - 0.7448515892 0.7261224985 0.7219868898 - 0.7455275059 0.7267739773 0.7227588296 - 0.7462034225 0.7274255157 0.7235307097 - 0.7468793988 0.7280768752 0.7243025899 - 0.7475525737 0.7287256122 0.7250691056 - 0.7482250929 0.7293733954 0.7258337736 - 0.7488974929 0.7300211787 0.7265986204 - 0.7495679855 0.7306671739 0.7273609042 - 0.7502354980 0.7313104868 0.7281196117 - 0.7509030104 0.7319536805 0.7288782001 - 0.7515705228 0.7325968742 0.7296367884 - 0.7522333264 0.7332357764 0.7303904295 - 0.7528960109 0.7338746786 0.7311440110 - 0.7535588145 0.7345135808 0.7318975925 - 0.7542191148 0.7351499796 0.7326478958 - 0.7548775077 0.7357847095 0.7333958745 - 0.7555359006 0.7364193797 0.7341439128 - 0.7561935782 0.7370532751 0.7348909974 - 0.7568460703 0.7376828790 0.7356327176 - 0.7574986815 0.7383124828 0.7363743782 - 0.7581512928 0.7389420867 0.7371160984 - 0.7588003278 0.7395684719 0.7378535271 - 0.7594472766 0.7401931286 0.7385885119 - 0.7600942850 0.7408177853 0.7393234968 - 0.7607408166 0.7414417863 0.7400577068 - 0.7613841295 0.7420616746 0.7407870889 - 0.7620275021 0.7426816225 0.7415165901 - 0.7626708746 0.7433015108 0.7422459722 - 0.7633122802 0.7439180017 0.7429726720 - 0.7639523745 0.7445324063 0.7436977029 - 0.7645925283 0.7451466918 0.7444226146 - 0.7652325034 0.7457607985 0.7451472878 - 0.7658681273 0.7463697195 0.7458673120 - 0.7665035725 0.7469785810 0.7465873957 - 0.7671391964 0.7475875020 0.7473074198 - 0.7677729130 0.7481939793 0.7480244040 - 0.7684043050 0.7487975955 0.7487375736 - 0.7690356970 0.7494012117 0.7494506836 - 0.7696670890 0.7500048876 0.7501639128 - 0.7702953815 0.7506046295 0.7508723140 - 0.7709228992 0.7512034774 0.7515797019 - 0.7715504766 0.7518025041 0.7522869706 - 0.7721772194 0.7524008751 0.7529935837 - 0.7727996707 0.7529972196 0.7536963820 - 0.7734221816 0.7535933852 0.7543991804 - 0.7740446925 0.7541896105 0.7551020980 - 0.7746649981 0.7547839284 0.7558023930 - 0.7752826810 0.7553762794 0.7565001845 - 0.7759004831 0.7559686899 0.7571979761 - 0.7765182853 0.7565611005 0.7578958273 - 0.7771326900 0.7571496964 0.7585895061 - 0.7777460814 0.7577372789 0.7592819929 - 0.7783594728 0.7583248019 0.7599744201 - 0.7789728045 0.7589122057 0.7606667280 - 0.7795792222 0.7594966888 0.7613533735 - 0.7801855803 0.7600811720 0.7620400190 - 0.7807919979 0.7606657147 0.7627267241 - 0.7813968062 0.7612491846 0.7634122968 - 0.7819966078 0.7618296742 0.7640944123 - 0.7825964093 0.7624102235 0.7647765279 - 0.7831960917 0.7629907131 0.7654587030 - 0.7837939262 0.7635689974 0.7661380172 - 0.7843887806 0.7641444206 0.7668138146 - 0.7849838138 0.7647197843 0.7674896717 - 0.7855787873 0.7652952075 0.7681655288 - 0.7861704826 0.7658686042 0.7688372731 - 0.7867596745 0.7664408088 0.7695063949 - 0.7873489261 0.7670128942 0.7701755166 - 0.7879381776 0.7675850987 0.7708445787 - 0.7885246873 0.7681542039 0.7715098858 - 0.7891101837 0.7687221169 0.7721735835 - 0.7896956205 0.7692900896 0.7728372812 - 0.7902809978 0.7698580027 0.7735010982 - 0.7908638120 0.7704228759 0.7741602063 - 0.7914459109 0.7709870934 0.7748184204 - 0.7920280099 0.7715513706 0.7754765153 - 0.7926099896 0.7721155882 0.7761346102 - 0.7931870222 0.7726777196 0.7767888904 - 0.7937632799 0.7732396722 0.7774425745 - 0.7943394780 0.7738016248 0.7780963182 - 0.7949157953 0.7743635178 0.7787501216 - 0.7954884768 0.7749227285 0.7793995142 - 0.7960608006 0.7754815817 0.7800486088 - 0.7966331244 0.7760406137 0.7806975842 - 0.7972053885 0.7765995860 0.7813466787 - 0.7977746129 0.7771558762 0.7819913030 - 0.7983434796 0.7777119875 0.7826355100 - 0.7989124060 0.7782680988 0.7832797170 - 0.7994812727 0.7788242102 0.7839238048 - 0.8000450134 0.7793778181 0.7845636010 - 0.8006079793 0.7799311280 0.7852026820 - 0.8011708856 0.7804843187 0.7858418226 - 0.8017339110 0.7810376287 0.7864809036 - 0.8022924066 0.7815880775 0.7871146202 - 0.8028498292 0.7821378708 0.7877470851 - 0.8034073114 0.7826877236 0.7883794904 - 0.8039646745 0.7832375765 0.7890118957 - 0.8045186996 0.7837849855 0.7896395922 - 0.8050711751 0.7843313813 0.7902652025 - 0.8056237102 0.7848778963 0.7908908129 - 0.8061761856 0.7854242921 0.7915164232 - 0.8067255020 0.7859687805 0.7921388745 - 0.8072721958 0.7865118980 0.7927588820 - 0.8078188896 0.7870550156 0.7933788896 - 0.8083655834 0.7875980735 0.7939988971 - 0.8089101911 0.7881401777 0.7946166992 - 0.8094515800 0.7886806130 0.7952311039 - 0.8099929094 0.7892209888 0.7958455086 - 0.8105341792 0.7897614241 0.7964599133 - 0.8110746741 0.7903016806 0.7970730066 - 0.8116112947 0.7908411026 0.7976801991 - 0.8121479154 0.7913805842 0.7982873917 - 0.8126844764 0.7919200063 0.7988945842 - 0.8132210970 0.7924594879 0.7995017767 - 0.8137524128 0.7929970026 0.8001034260 - 0.8142833114 0.7935343981 0.8007047176 - 0.8148142099 0.7940719128 0.8013060093 - 0.8153451085 0.7946093082 0.8019073009 - 0.8158726096 0.7951447964 0.8025054932 - 0.8163983226 0.7956792712 0.8031023145 - 0.8169239759 0.7962138057 0.8036990762 - 0.8174496889 0.7967482209 0.8042958975 - 0.8179737926 0.7972822785 0.8048911095 - 0.8184949160 0.7978156209 0.8054832816 - 0.8190159798 0.7983487844 0.8060755134 - 0.8195371032 0.7988821268 0.8066676855 - 0.8200582862 0.7994152904 0.8072599173 - 0.8205738068 0.7999464273 0.8078473806 - 0.8210893273 0.8004773855 0.8084350228 - 0.8216049075 0.8010084033 0.8090224862 - 0.8221204281 0.8015394211 0.8096100092 - 0.8226320148 0.8020684123 0.8101946115 - 0.8231410980 0.8025962710 0.8107774258 - 0.8236501217 0.8031241298 0.8113601804 - 0.8241592050 0.8036519289 0.8119429946 - 0.8246672153 0.8041794896 0.8125250936 - 0.8251705170 0.8047057986 0.8131045103 - 0.8256738186 0.8052322268 0.8136839271 - 0.8261770010 0.8057584763 0.8142634034 - 0.8266803026 0.8062849045 0.8148428202 - 0.8271808028 0.8068094254 0.8154187202 - 0.8276802897 0.8073332906 0.8159933090 - 0.8281797171 0.8078572154 0.8165680170 - 0.8286792040 0.8083810806 0.8171426058 - 0.8291776180 0.8089041114 0.8177158237 - 0.8296723962 0.8094245791 0.8182842731 - 0.8301671743 0.8099449873 0.8188527822 - 0.8306620121 0.8104655147 0.8194214106 - 0.8311567903 0.8109859824 0.8199899197 - 0.8316488266 0.8115053177 0.8205549717 - 0.8321394920 0.8120239973 0.8211187720 - 0.8326302767 0.8125427961 0.8216825724 - 0.8331211209 0.8130615950 0.8222463727 - 0.8336114883 0.8135799170 0.8228093982 - 0.8341001868 0.8140959740 0.8233684897 - 0.8345888853 0.8146120906 0.8239275217 - 0.8350775242 0.8151282072 0.8244866133 - 0.8355662227 0.8156443238 0.8250455856 - 0.8360525966 0.8161584139 0.8256021738 - 0.8365371227 0.8166708946 0.8261566162 - 0.8370215297 0.8171833158 0.8267109990 - 0.8375059962 0.8176957965 0.8272655010 - 0.8379905224 0.8182082772 0.8278198838 - 0.8384705782 0.8187190294 0.8283702135 - 0.8389502168 0.8192296028 0.8289201260 - 0.8394296765 0.8197401166 0.8294699192 - 0.8399093151 0.8202505708 0.8300197124 - 0.8403881788 0.8207604885 0.8305683136 - 0.8408648968 0.8212682009 0.8311123848 - 0.8413414955 0.8217757940 0.8316563964 - 0.8418180943 0.8222835064 0.8322004080 - 0.8422946930 0.8227912188 0.8327444792 - 0.8427696228 0.8232969046 0.8332856894 - 0.8432425857 0.8238008022 0.8338239789 - 0.8437157273 0.8243045807 0.8343623281 - 0.8441886902 0.8248084784 0.8349006772 - 0.8446617723 0.8253123760 0.8354390264 - 0.8451316953 0.8258146048 0.8359742165 - 0.8456004858 0.8263162971 0.8365082145 - 0.8460692167 0.8268179893 0.8370422125 - 0.8465380073 0.8273196816 0.8375762105 - 0.8470066786 0.8278213739 0.8381102085 - 0.8474733233 0.8283202052 0.8386403918 - 0.8479397297 0.8288187981 0.8391703963 - 0.8484060764 0.8293173909 0.8397002816 - 0.8488724828 0.8298161030 0.8402302861 - 0.8493387103 0.8303142786 0.8407598138 - 0.8498020768 0.8308088779 0.8412843943 - 0.8502655029 0.8313035965 0.8418090940 - 0.8507289290 0.8317981958 0.8423337936 - 0.8511922956 0.8322929144 0.8428584933 - 0.8516547084 0.8327869773 0.8433821201 - 0.8521131873 0.8332791924 0.8439015746 - 0.8525716066 0.8337715268 0.8444212079 - 0.8530300856 0.8342636824 0.8449407220 - 0.8534886241 0.8347560167 0.8454602957 - 0.8539462090 0.8352472782 0.8459783792 - 0.8544015884 0.8357362747 0.8464931250 - 0.8548570871 0.8362252712 0.8470076919 - 0.8553125858 0.8367143273 0.8475223184 - 0.8557680249 0.8372033238 0.8480370045 - 0.8562228084 0.8376908898 0.8485500813 - 0.8566759229 0.8381757140 0.8490599990 - 0.8571289778 0.8386605978 0.8495699167 - 0.8575822115 0.8391454220 0.8500797749 - 0.8580353260 0.8396301866 0.8505896926 - 0.8584871292 0.8401141763 0.8510981798 - 0.8589360118 0.8405964971 0.8516036868 - 0.8593848944 0.8410786986 0.8521090746 - 0.8598337770 0.8415609002 0.8526145816 - 0.8602827191 0.8420432210 0.8531200290 - 0.8607307076 0.8425244093 0.8536239862 - 0.8611760139 0.8430032134 0.8541238904 - 0.8616213202 0.8434820175 0.8546237946 - 0.8620666862 0.8439608812 0.8551236987 - 0.8625119925 0.8444396853 0.8556236029 - 0.8629568219 0.8449175954 0.8561224937 - 0.8633992076 0.8453921080 0.8566170931 - 0.8638417125 0.8458666205 0.8571115732 - 0.8642840981 0.8463410139 0.8576061130 - 0.8647266030 0.8468155265 0.8581007123 - 0.8651688099 0.8472898006 0.8585950136 - 0.8656079173 0.8477615118 0.8590856194 - 0.8660469055 0.8482331038 0.8595762849 - 0.8664858937 0.8487048149 0.8600668907 - 0.8669250011 0.8491765261 0.8605576158 - 0.8673639894 0.8496481180 0.8610482216 - 0.8677995801 0.8501166105 0.8615357280 - 0.8682346940 0.8505846262 0.8620226979 - 0.8686698079 0.8510527015 0.8625097871 - 0.8691048026 0.8515207767 0.8629968762 - 0.8695399165 0.8519887924 0.8634839058 - 0.8699728847 0.8524538279 0.8639678955 - 0.8704047799 0.8529173732 0.8644505143 - 0.8708366156 0.8533809185 0.8649330735 - 0.8712685108 0.8538445234 0.8654156923 - 0.8717002869 0.8543081284 0.8658981919 - 0.8721306920 0.8547704220 0.8663790226 - 0.8725587130 0.8552309275 0.8668572903 - 0.8729867935 0.8556914926 0.8673356175 - 0.8734148741 0.8561519980 0.8678138852 - 0.8738428950 0.8566126227 0.8682922125 - 0.8742704988 0.8570727110 0.8687700033 - 0.8746942878 0.8575295210 0.8692442775 - 0.8751180172 0.8579862714 0.8697186112 - 0.8755416870 0.8584430814 0.8701928854 - 0.8759654760 0.8588998914 0.8706671000 - 0.8763892055 0.8593567014 0.8711413741 - 0.8768100739 0.8598098755 0.8716111183 - 0.8772302270 0.8602619767 0.8720793724 - 0.8776502013 0.8607140779 0.8725476861 - 0.8780702949 0.8611662984 0.8730159998 - 0.8784903288 0.8616183996 0.8734843135 - 0.8789088130 0.8620693088 0.8739504814 - 0.8793246746 0.8625183105 0.8744130135 - 0.8797405958 0.8629671931 0.8748756051 - 0.8801565170 0.8634161949 0.8753381968 - 0.8805723190 0.8638650775 0.8758007884 - 0.8809881806 0.8643140793 0.8762633801 - 0.8814008832 0.8647593260 0.8767219186 - 0.8818134069 0.8652043939 0.8771802187 - 0.8822258711 0.8656494021 0.8776385188 - 0.8826382756 0.8660945296 0.8780966997 - 0.8830507994 0.8665394783 0.8785549998 - 0.8834620118 0.8669828176 0.8790109158 - 0.8838717937 0.8674241900 0.8794640899 - 0.8842815161 0.8678655028 0.8799173236 - 0.8846911788 0.8683068752 0.8803703785 - 0.8851009011 0.8687481880 0.8808236122 - 0.8855106831 0.8691896200 0.8812767863 - 0.8859174252 0.8696290851 0.8817254901 - 0.8863239288 0.8700686097 0.8821738958 - 0.8867303729 0.8705080152 0.8826223016 - 0.8871369958 0.8709474802 0.8830707073 - 0.8875434995 0.8713868856 0.8835191131 - 0.8879491091 0.8718252778 0.8839659095 - 0.8883531094 0.8722618222 0.8844100833 - 0.8887569904 0.8726983070 0.8848543167 - 0.8891609907 0.8731347919 0.8852983713 - 0.8895649910 0.8735713959 0.8857426047 - 0.8899689913 0.8740078807 0.8861867189 - 0.8903707862 0.8744425774 0.8866271973 - 0.8907716870 0.8748766780 0.8870663047 - 0.8911727071 0.8753107786 0.8875054717 - 0.8915737271 0.8757449985 0.8879445791 - 0.8919746876 0.8761790991 0.8883838058 - 0.8923754096 0.8766130209 0.8888226151 - 0.8927729726 0.8770452738 0.8892570734 - 0.8931705952 0.8774775267 0.8896915913 - 0.8935680985 0.8779097199 0.8901261091 - 0.8939657211 0.8783419728 0.8905606270 - 0.8943632245 0.8787742257 0.8909950852 - 0.8947597742 0.8792054057 0.8914281130 - 0.8951544762 0.8796346784 0.8918585181 - 0.8955491781 0.8800640702 0.8922889233 - 0.8959439993 0.8804934025 0.8927193284 - 0.8963387012 0.8809227943 0.8931496739 - 0.8967334032 0.8813521862 0.8935800791 - 0.8971260786 0.8817800879 0.8940076232 - 0.8975175023 0.8822070956 0.8944330812 - 0.8979089856 0.8826341033 0.8948585987 - 0.8983004093 0.8830611110 0.8952841163 - 0.8986917734 0.8834881186 0.8957095742 - 0.8990831971 0.8839151263 0.8961350918 - 0.8994715810 0.8843405843 0.8965569735 - 0.8998593092 0.8847656846 0.8969780207 - 0.9002469778 0.8851909041 0.8973988891 - 0.9006347060 0.8856160045 0.8978198767 - 0.9010223746 0.8860412240 0.8982409239 - 0.9014101028 0.8864663243 0.8986619115 - 0.9017949104 0.8868888021 0.8990790844 - 0.9021794796 0.8873111010 0.8994960785 - 0.9025642276 0.8877333999 0.8999131918 - 0.9029487967 0.8881556988 0.9003301859 - 0.9033334851 0.8885779977 0.9007471800 - 0.9037179947 0.8890001774 0.9011639953 - 0.9041002989 0.8894202113 0.9015762210 - 0.9044826031 0.8898401856 0.9019883871 - 0.9048647881 0.8902602196 0.9024006128 - 0.9052470922 0.8906801939 0.9028128982 - 0.9056293964 0.8911002278 0.9032251239 - 0.9060112834 0.8915200233 0.9036368132 - 0.9063897133 0.8919382095 0.9040434957 - 0.9067682028 0.8923563957 0.9044501781 - 0.9071465731 0.8927745819 0.9048569202 - 0.9075250030 0.8931927085 0.9052636027 - 0.9079034925 0.8936108947 0.9056702852 - 0.9082816243 0.8940287828 0.9060766101 - 0.9086568952 0.8944442272 0.9064794779 - 0.9090322256 0.8948596120 0.9068824053 - 0.9094074965 0.8952749968 0.9072852731 - 0.9097827077 0.8956903815 0.9076882005 - 0.9101579785 0.8961058259 0.9080911279 - 0.9105333090 0.8965210915 0.9084938765 - 0.9109072089 0.8969342113 0.9088944793 - 0.9112812281 0.8973472714 0.9092950225 - 0.9116551280 0.8977603912 0.9096956253 - 0.9120290875 0.8981735110 0.9100962281 - 0.9124029875 0.8985865712 0.9104967713 - 0.9127770066 0.8989996910 0.9108973742 - 0.9131482840 0.8994112015 0.9112942219 - 0.9135193825 0.8998224735 0.9116908908 - 0.9138906002 0.9002338052 0.9120875001 - 0.9142616987 0.9006450772 0.9124842286 - 0.9146327972 0.9010564089 0.9128807783 - 0.9150040150 0.9014678001 0.9132773876 - 0.9153717160 0.9018769264 0.9136704803 - 0.9157385230 0.9022855163 0.9140626192 - 0.9161052704 0.9026941061 0.9144545794 - 0.9164720774 0.9031026959 0.9148467183 - 0.9168388844 0.9035112262 0.9152387977 - 0.9172058105 0.9039198160 0.9156308770 - 0.9175711274 0.9043270946 0.9160212278 - 0.9179353714 0.9047334194 0.9164103270 - 0.9182996750 0.9051396847 0.9167994261 - 0.9186639786 0.9055461287 0.9171885848 - 0.9190282822 0.9059523940 0.9175776839 - 0.9193925858 0.9063587785 0.9179667830 - 0.9197562933 0.9067645073 0.9183548093 - 0.9201188087 0.9071691036 0.9187405109 - 0.9204813242 0.9075737000 0.9191262126 - 0.9208437800 0.9079781771 0.9195119143 - 0.9212062955 0.9083827734 0.9198976159 - 0.9215688109 0.9087874293 0.9202833176 - 0.9219312072 0.9091919065 0.9206687808 - 0.9222893715 0.9095938206 0.9210494161 - 0.9226475954 0.9099956155 0.9214301109 - 0.9230058193 0.9103975296 0.9218106866 - 0.9233639836 0.9107993841 0.9221913218 - 0.9237222075 0.9112012982 0.9225718975 - 0.9240803123 0.9116032124 0.9229525924 - 0.9244369268 0.9120035768 0.9233309031 - 0.9247925282 0.9124032259 0.9237080812 - 0.9251481891 0.9128028750 0.9240853786 - 0.9255039096 0.9132025838 0.9244626164 - 0.9258595705 0.9136022925 0.9248397946 - 0.9262152910 0.9140018821 0.9252169728 - 0.9265707731 0.9144011736 0.9255934954 - 0.9269257188 0.9147990942 0.9259678721 - 0.9272807240 0.9151970148 0.9263421893 - 0.9276356101 0.9155949950 0.9267166257 - 0.9279906154 0.9159929156 0.9270910025 - 0.9283455014 0.9163908958 0.9274653792 - 0.9287005067 0.9167888165 0.9278396964 - 0.9290528893 0.9171856046 0.9282106161 - 0.9294046760 0.9175820947 0.9285805225 - 0.9297564030 0.9179785252 0.9289504290 - 0.9301081896 0.9183750153 0.9293202758 - 0.9304599166 0.9187715054 0.9296901822 - 0.9308117032 0.9191678762 0.9300600886 - 0.9311624765 0.9195644259 0.9304289222 - 0.9315106869 0.9199606180 0.9307948947 - 0.9318587780 0.9203569293 0.9311608076 - 0.9322069883 0.9207531810 0.9315267801 - 0.9325551987 0.9211494923 0.9318928123 - 0.9329034090 0.9215456843 0.9322587848 - 0.9332516193 0.9219419956 0.9326246977 - 0.9335985184 0.9223375916 0.9329884052 - 0.9339448810 0.9227328897 0.9333512187 - 0.9342911839 0.9231281281 0.9337139130 - 0.9346376061 0.9235234261 0.9340766072 - 0.9349840283 0.9239187241 0.9344394207 - 0.9353303909 0.9243140221 0.9348021150 - 0.9356765747 0.9247090816 0.9351645112 - 0.9360207915 0.9251027107 0.9355235100 - 0.9363651276 0.9254962802 0.9358825088 - 0.9367092848 0.9258899093 0.9362415075 - 0.9370536208 0.9262835979 0.9366003871 - 0.9373977780 0.9266772270 0.9369593859 - 0.9377421141 0.9270707965 0.9373183846 - 0.9380850196 0.9274641871 0.9376757145 - 0.9384257793 0.9278573990 0.9380304813 - 0.9387664795 0.9282506704 0.9383851886 - 0.9391072989 0.9286438823 0.9387400150 - 0.9394481182 0.9290370941 0.9390947223 - 0.9397888780 0.9294303060 0.9394494891 - 0.9401296973 0.9298235178 0.9398041964 - 0.9404693842 0.9302158952 0.9401566982 - 0.9408084750 0.9306079149 0.9405080080 - 0.9411476254 0.9309998751 0.9408593178 - 0.9414865971 0.9313920140 0.9412106276 - 0.9418256879 0.9317839742 0.9415618777 - 0.9421647787 0.9321759939 0.9419131875 - 0.9425039291 0.9325680137 0.9422644973 - 0.9428421259 0.9329584837 0.9426131845 - 0.9431802034 0.9333485961 0.9429615140 - 0.9435182810 0.9337388277 0.9433097839 - 0.9438564181 0.9341289997 0.9436581135 - 0.9441944957 0.9345191121 0.9440063834 - 0.9445325732 0.9349092841 0.9443547130 - 0.9448707104 0.9352995157 0.9447029829 - 0.9452053905 0.9356890917 0.9450477958 - 0.9455401897 0.9360786080 0.9453927279 - 0.9458749294 0.9364681840 0.9457374811 - 0.9462096095 0.9368578196 0.9460824132 - 0.9465444088 0.9372473955 0.9464272261 - 0.9468790889 0.9376369119 0.9467720985 - 0.9472135901 0.9380264282 0.9471164942 - 0.9475457072 0.9384145737 0.9474576116 - 0.9478777051 0.9388027787 0.9477987289 - 0.9482098222 0.9391909838 0.9481397867 - 0.9485418797 0.9395791888 0.9484809041 - 0.9488739967 0.9399673939 0.9488220215 - 0.9492059946 0.9403555989 0.9491630793 - 0.9495379925 0.9407432079 0.9495037794 - 0.9498690963 0.9411267042 0.9498416781 - 0.9502003193 0.9415103197 0.9501795769 - 0.9505314231 0.9418938756 0.9505175948 - 0.9508625865 0.9422774911 0.9508554935 - 0.9511936903 0.9426611066 0.9511933923 - 0.9515247941 0.9430447221 0.9515312910 - 0.9518557191 0.9434276223 0.9518690109 - 0.9521846175 0.9438055754 0.9522042871 - 0.9525135756 0.9441835880 0.9525396228 - 0.9528424740 0.9445616007 0.9528750181 - 0.9531713724 0.9449396133 0.9532102942 - 0.9535002708 0.9453176260 0.9535456896 - 0.9538292289 0.9456955194 0.9538810253 - 0.9541580081 0.9460734129 0.9542161226 - 0.9544816017 0.9464468956 0.9545456171 - 0.9548051953 0.9468203783 0.9548751116 - 0.9551287889 0.9471939802 0.9552046061 - 0.9554523826 0.9475675225 0.9555341005 - 0.9557759166 0.9479411244 0.9558635950 - 0.9560995102 0.9483146071 0.9561930895 - 0.9564231038 0.9486880898 0.9565225840 - 0.9567415714 0.9490590096 0.9568458200 - 0.9570595026 0.9494295716 0.9571682215 - 0.9573773146 0.9498001933 0.9574906230 - 0.9576951861 0.9501708150 0.9578130245 - 0.9580131173 0.9505413771 0.9581354260 - 0.9583309889 0.9509119987 0.9584578276 - 0.9586488008 0.9512825012 0.9587802291 - 0.9589642882 0.9516518712 0.9591000080 - 0.9592787027 0.9520208240 0.9594187140 - 0.9595931172 0.9523895979 0.9597374201 - 0.9599075913 0.9527583718 0.9600561261 - 0.9602220058 0.9531273246 0.9603748918 - 0.9605364203 0.9534960985 0.9606935978 - 0.9608507752 0.9538649917 0.9610123038 - 0.9611636996 0.9542328715 0.9613298178 - 0.9614747763 0.9545994997 0.9616457224 - 0.9617857933 0.9549661875 0.9619616866 - 0.9620968103 0.9553328753 0.9622775912 - 0.9624078274 0.9556996226 0.9625936151 - 0.9627187848 0.9560663104 0.9629095197 - 0.9630298018 0.9564329982 0.9632254839 - 0.9633405209 0.9567993283 0.9635409713 - 0.9636486173 0.9571633935 0.9638535976 - 0.9639567733 0.9575273991 0.9641662836 - 0.9642649889 0.9578914046 0.9644789100 - 0.9645730853 0.9582554102 0.9647914767 - 0.9648813009 0.9586194158 0.9651042223 - 0.9651893973 0.9589834809 0.9654167891 - 0.9654976130 0.9593474865 0.9657294154 - 0.9658039808 0.9597097039 0.9660398960 - 0.9661098123 0.9600713849 0.9663494825 - 0.9664155245 0.9604331255 0.9666591883 - 0.9667212963 0.9607948065 0.9669688940 - 0.9670271277 0.9611564875 0.9672784805 - 0.9673328996 0.9615182281 0.9675881863 - 0.9676386118 0.9618799090 0.9678978920 - 0.9679433107 0.9622406960 0.9682065248 - 0.9682455063 0.9625995755 0.9685127139 - 0.9685477018 0.9629585743 0.9688190222 - 0.9688498974 0.9633175135 0.9691252112 - 0.9691520929 0.9636763930 0.9694314003 - 0.9694542885 0.9640353918 0.9697375894 - 0.9697564840 0.9643942714 0.9700438976 - 0.9700586796 0.9647533298 0.9703500867 - 0.9703577757 0.9651095867 0.9706525803 - 0.9706562161 0.9654654264 0.9709542990 - 0.9709545970 0.9658210874 0.9712558985 - 0.9712529182 0.9661768079 0.9715574980 - 0.9715512991 0.9665325880 0.9718590975 - 0.9718496799 0.9668883085 0.9721608162 - 0.9721480012 0.9672440290 0.9724624157 - 0.9724456072 0.9675989747 0.9727630019 - 0.9727405906 0.9679515958 0.9730604291 - 0.9730355740 0.9683042169 0.9733577967 - 0.9733306766 0.9686567783 0.9736552238 - 0.9736257195 0.9690095186 0.9739525914 - 0.9739207029 0.9693620801 0.9742500186 - 0.9742156863 0.9697147012 0.9745473862 - 0.9745107293 0.9700673223 0.9748446941 - 0.9748036265 0.9704176188 0.9751394987 - 0.9750949740 0.9707666039 0.9754326940 - 0.9753863811 0.9711155295 0.9757258296 - 0.9756777883 0.9714645147 0.9760190248 - 0.9759691954 0.9718133807 0.9763122201 - 0.9762606025 0.9721624255 0.9766052961 - 0.9765521288 0.9725112915 0.9768984914 - 0.9768434763 0.9728602171 0.9771916270 - 0.9771314263 0.9732053876 0.9774799943 - 0.9774190784 0.9735503197 0.9777678847 - 0.9777066708 0.9738951921 0.9780558944 - 0.9779943228 0.9742401242 0.9783437848 - 0.9782819748 0.9745849967 0.9786317945 - 0.9785696268 0.9749299288 0.9789196849 - 0.9788572788 0.9752748013 0.9792076945 - 0.9791442752 0.9756188989 0.9794946909 - 0.9794281721 0.9759591818 0.9797772765 - 0.9797121286 0.9762995839 0.9800599217 - 0.9799959064 0.9766399860 0.9803425074 - 0.9802798033 0.9769803882 0.9806250930 - 0.9805637002 0.9773206711 0.9809077978 - 0.9808475971 0.9776610732 0.9811903834 - 0.9811314940 0.9780014753 0.9814730287 - 0.9814137816 0.9783400297 0.9817537069 - 0.9816933274 0.9786750078 0.9820309877 - 0.9819728136 0.9790101051 0.9823082089 - 0.9822522998 0.9793450832 0.9825854897 - 0.9825317860 0.9796801805 0.9828627110 - 0.9828112721 0.9800152183 0.9831399918 - 0.9830908179 0.9803503156 0.9834172726 - 0.9833703041 0.9806852937 0.9836944938 - 0.9836477041 0.9810177088 0.9839690924 - 0.9839227200 0.9813470840 0.9842407703 - 0.9841976762 0.9816765189 0.9845123887 - 0.9844726920 0.9820057750 0.9847840071 - 0.9847477078 0.9823352098 0.9850556254 - 0.9850227237 0.9826645255 0.9853271842 - 0.9852976203 0.9829939008 0.9855988026 - 0.9855725765 0.9833232760 0.9858704209 - 0.9858452082 0.9836488962 0.9861391783 - 0.9861155152 0.9839711785 0.9864053130 - 0.9863858223 0.9842935205 0.9866713285 - 0.9866561890 0.9846158028 0.9869374037 - 0.9869264960 0.9849380851 0.9872034788 - 0.9871968031 0.9852603078 0.9874696136 - 0.9874671102 0.9855825901 0.9877356291 - 0.9877374172 0.9859048724 0.9880017042 - 0.9880046248 0.9862229228 0.9882646799 - 0.9882686138 0.9865366220 0.9885246158 - 0.9885327220 0.9868503809 0.9887844920 - 0.9887967110 0.9871640801 0.9890444875 - 0.9890608191 0.9874778986 0.9893044233 - 0.9893248081 0.9877915978 0.9895642996 - 0.9895889163 0.9881054163 0.9898241758 - 0.9898529053 0.9884191155 0.9900841713 - 0.9901142120 0.9887291193 0.9903411865 - 0.9903718829 0.9890338778 0.9905943871 - 0.9906294942 0.9893386960 0.9908475280 - 0.9908872247 0.9896435142 0.9911007285 - 0.9911447763 0.9899483919 0.9913538098 - 0.9914023876 0.9902532101 0.9916070104 - 0.9916601181 0.9905580282 0.9918600917 - 0.9919177294 0.9908627868 0.9921131730 - 0.9921736121 0.9911643267 0.9923645854 - 0.9924249053 0.9914569855 0.9926109910 - 0.9926763177 0.9917497039 0.9928573966 - 0.9929276109 0.9920424223 0.9931039214 - 0.9931789041 0.9923352003 0.9933503270 - 0.9934303164 0.9926279187 0.9935967922 - 0.9936816096 0.9929205775 0.9938431978 - 0.9939329028 0.9932134151 0.9940897226 - 0.9941837192 0.9935051799 0.9943355918 - 0.9944264293 0.9937829971 0.9945728183 - 0.9946690202 0.9940606952 0.9948101044 - 0.9949116111 0.9943385124 0.9950473905 - 0.9951542020 0.9946163297 0.9952846169 - 0.9953969121 0.9948940873 0.9955219030 - 0.9956395030 0.9951717854 0.9957591891 - 0.9958820939 0.9954496026 0.9959964752 - 0.9961246848 0.9957274199 0.9962337017 - 0.9963588715 0.9959930182 0.9964621067 - 0.9965906739 0.9962550998 0.9966878295 - 0.9968224764 0.9965171814 0.9969136119 - 0.9970542789 0.9967793226 0.9971392751 - 0.9972860217 0.9970414042 0.9973651171 - 0.9975178242 0.9973034859 0.9975907803 - 0.9977496266 0.9975656271 0.9978166223 - 0.9979814291 0.9978277087 0.9980422854 - 0.9982097745 0.9980804920 0.9982643723 - 0.9984334707 0.9983204007 0.9984812737 - 0.9986572862 0.9985604286 0.9986982942 - 0.9988811016 0.9988002777 0.9989151955 - 0.9991049170 0.9990401864 0.9991322160 - 0.9993286729 0.9992802143 0.9993491173 - 0.9995524287 0.9995201230 0.9995660782 - 0.9997761846 0.9997600913 0.9997829795 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0182101503 0.0179095902 0.0133477096 + 0.0321649686 0.0326713808 0.0245428905 + 0.0435327813 0.0445400514 0.0339762792 + 0.0535616502 0.0549106896 0.0424365215 + 0.0627619624 0.0643109381 0.0502561592 + 0.0713807195 0.0730527714 0.0576005317 + 0.0795889422 0.0812662989 0.0646079034 + 0.0874749497 0.0890629813 0.0713189989 + 0.0951398313 0.0965353027 0.0778023675 + 0.1026329994 0.1037214994 0.0840878412 + 0.1098783985 0.1106367037 0.0901622772 + 0.1168456972 0.1172993034 0.0960228518 + 0.1235098019 0.1237140000 0.1016969979 + 0.1299058944 0.1299183965 0.1071913987 + 0.1360470951 0.1359218955 0.1125032008 + 0.1419679970 0.1417497993 0.1176472977 + 0.1476961970 0.1474111974 0.1226311028 + 0.1532527953 0.1529219002 0.1274778992 + 0.1586499959 0.1582891047 0.1321873963 + 0.1639094949 0.1635248065 0.1367731988 + 0.1690364927 0.1686291993 0.1412428021 + 0.1740324050 0.1736201942 0.1455994993 + 0.1789164990 0.1784920990 0.1498488039 + 0.1836923957 0.1832599044 0.1539963931 + 0.1883690059 0.1879281998 0.1580594927 + 0.1929446012 0.1924999058 0.1620312035 + 0.1974374950 0.1969808936 0.1659148932 + 0.2018467933 0.2013725042 0.1697210968 + 0.2061869949 0.2056811005 0.1734540015 + 0.2104454935 0.2099131942 0.1771208942 + 0.2146379054 0.2140665054 0.1807264984 + 0.2187689990 0.2181411982 0.1842699945 + 0.2228457928 0.2221457958 0.1877449006 + 0.2268676013 0.2260929942 0.1911672056 + 0.2308367044 0.2299744040 0.1945340037 + 0.2347573936 0.2337978929 0.1978531927 + 0.2386282980 0.2375611067 0.2011176944 + 0.2424549013 0.2412583977 0.2043330073 + 0.2462375015 0.2449042946 0.2075064927 + 0.2499749959 0.2485097051 0.2106339037 + 0.2536737919 0.2520638108 0.2137185931 + 0.2573330998 0.2555652857 0.2167686969 + 0.2609579861 0.2590112984 0.2197788060 + 0.2645516098 0.2624117136 0.2227548957 + 0.2681075931 0.2657625973 0.2257010043 + 0.2716293931 0.2690761089 0.2286102027 + 0.2751240134 0.2723546028 0.2314918935 + 0.2785834074 0.2755965889 0.2343432009 + 0.2820093036 0.2787989080 0.2371643037 + 0.2854062021 0.2819635868 0.2399580032 + 0.2887754142 0.2850886881 0.2427307069 + 0.2921054959 0.2881690860 0.2454746962 + 0.2953915000 0.2912029028 0.2481936961 + 0.2986359894 0.2941966951 0.2508893013 + 0.3018450141 0.2971538901 0.2535605133 + 0.3050135970 0.3000777960 0.2562063038 + 0.3081485033 0.3029662073 0.2588256896 + 0.3112491965 0.3058190048 0.2614240944 + 0.3143157065 0.3086350858 0.2640033066 + 0.3173457086 0.3114137053 0.2665627897 + 0.3203401864 0.3141573966 0.2691006958 + 0.3232952952 0.3168714046 0.2716197073 + 0.3262135088 0.3195553124 0.2741247118 + 0.3290931880 0.3222120106 0.2766116858 + 0.3319376111 0.3248479962 0.2790769935 + 0.3347463012 0.3274633884 0.2815260887 + 0.3375192881 0.3300592899 0.2839562893 + 0.3402611911 0.3326362073 0.2863681912 + 0.3429714143 0.3351945877 0.2887654006 + 0.3456538916 0.3377344012 0.2911457121 + 0.3483096957 0.3402554095 0.2935140133 + 0.3509393930 0.3427593112 0.2958686948 + 0.3535420001 0.3452464938 0.2982082963 + 0.3561150134 0.3477157056 0.3005363941 + 0.3586607873 0.3501690924 0.3028526008 + 0.3611787856 0.3526060879 0.3051568866 + 0.3636738956 0.3550266922 0.3074469864 + 0.3661471903 0.3574325144 0.3097249866 + 0.3686017990 0.3598220050 0.3119905889 + 0.3710421920 0.3621949852 0.3142440021 + 0.3734661043 0.3645452857 0.3164834976 + 0.3758763969 0.3668780029 0.3187099993 + 0.3782708049 0.3691841960 0.3209230006 + 0.3806518912 0.3714686930 0.3231270015 + 0.3830181956 0.3737291098 0.3253178000 + 0.3853709102 0.3759689927 0.3274958134 + 0.3877105117 0.3781890869 0.3296644986 + 0.3900356889 0.3803892136 0.3318220973 + 0.3923485875 0.3825727105 0.3339678943 + 0.3946483135 0.3847385943 0.3360984921 + 0.3969348967 0.3868887126 0.3382169902 + 0.3992097080 0.3890219927 0.3403255939 + 0.4014719129 0.3911359906 0.3424212933 + 0.4037215114 0.3932318985 0.3445019126 + 0.4059596956 0.3953123093 0.3465686142 + 0.4081867933 0.3973782957 0.3486247063 + 0.4103969038 0.3994235098 0.3506675959 + 0.4125916958 0.4014526010 0.3526985049 + 0.4147717953 0.4034663141 0.3547205925 + 0.4169346094 0.4054656029 0.3567343056 + 0.4190779030 0.4074513912 0.3587369919 + 0.4212022126 0.4094235003 0.3607279956 + 0.4233098924 0.4113812149 0.3627093136 + 0.4254015088 0.4133256972 0.3646796942 + 0.4274776876 0.4152582884 0.3666377068 + 0.4295392036 0.4171769023 0.3685849011 + 0.4315859079 0.4190813899 0.3705236912 + 0.4336147010 0.4209738970 0.3724532127 + 0.4356271923 0.4228563905 0.3743726909 + 0.4376241863 0.4247288108 0.3762832880 + 0.4396047890 0.4265901148 0.3781884909 + 0.4415675104 0.4284402132 0.3800868988 + 0.4435133040 0.4302783012 0.3819774091 + 0.4454411864 0.4321058095 0.3838588893 + 0.4473508894 0.4339239895 0.3857302964 + 0.4492434859 0.4357284009 0.3875930905 + 0.4511201978 0.4375185072 0.3894470930 + 0.4529820085 0.4392957091 0.3912915885 + 0.4548301101 0.4410578907 0.3931255937 + 0.4566622972 0.4428066909 0.3949485123 + 0.4584765136 0.4445427954 0.3967649043 + 0.4602738023 0.4462682009 0.3985747099 + 0.4620555043 0.4479798973 0.4003768861 + 0.4638229907 0.4496806860 0.4021708071 + 0.4655745029 0.4513705969 0.4039582014 + 0.4673084915 0.4530492127 0.4057385027 + 0.4690262079 0.4547170997 0.4075132012 + 0.4707291126 0.4563761950 0.4092822969 + 0.4724188149 0.4580253959 0.4110448956 + 0.4740945101 0.4596652091 0.4128004014 + 0.4757550955 0.4612967968 0.4145481884 + 0.4774011075 0.4629195929 0.4162878096 + 0.4790340960 0.4645322859 0.4180184007 + 0.4806557000 0.4661347866 0.4197404087 + 0.4822664857 0.4677279890 0.4214550853 + 0.4838629961 0.4693109989 0.4231620133 + 0.4854427874 0.4708836973 0.4248597026 + 0.4870088995 0.4724482000 0.4265482128 + 0.4885635972 0.4740057886 0.4282296002 + 0.4901061058 0.4755555987 0.4299038947 + 0.4916355014 0.4770967066 0.4315708876 + 0.4931511879 0.4786292911 0.4332301915 + 0.4946528077 0.4801545143 0.4348819852 + 0.4961408973 0.4816705883 0.4365274012 + 0.4976170063 0.4831782877 0.4381654859 + 0.4990840852 0.4846773148 0.4397968054 + 0.5005418062 0.4861674011 0.4414210916 + 0.5019897819 0.4876480103 0.4430381060 + 0.5034281015 0.4891189933 0.4446476102 + 0.5048564076 0.4905793965 0.4462502003 + 0.5062754154 0.4920318127 0.4478481114 + 0.5076872110 0.4934777915 0.4494414032 + 0.5090919137 0.4949184060 0.4510299861 + 0.5104892850 0.4963541031 0.4526137114 + 0.5118765831 0.4977797866 0.4541929066 + 0.5132548809 0.4991959929 0.4557681084 + 0.5146262050 0.5006046891 0.4573394060 + 0.5159909725 0.5020067096 0.4589067101 + 0.5173475742 0.5034021735 0.4604690969 + 0.5186926126 0.5047916770 0.4620254934 + 0.5200299025 0.5061759949 0.4635772109 + 0.5213602781 0.5075541139 0.4651241004 + 0.5226839185 0.5089235902 0.4666649997 + 0.5240020156 0.5102841854 0.4681988955 + 0.5253158808 0.5116395950 0.4697273076 + 0.5266252756 0.5129901171 0.4712502062 + 0.5279287100 0.5143331885 0.4727657139 + 0.5292261243 0.5156680942 0.4742735922 + 0.5305185914 0.5169963837 0.4757766128 + 0.5318061113 0.5183182955 0.4772751927 + 0.5330879092 0.5196315050 0.4787661135 + 0.5343660116 0.5209389925 0.4802523851 + 0.5356402993 0.5222411752 0.4817340076 + 0.5369089246 0.5235363245 0.4832083881 + 0.5381723046 0.5248249173 0.4846785069 + 0.5394316912 0.5261074901 0.4861451089 + 0.5406854153 0.5273811817 0.4876070917 + 0.5419325829 0.5286462903 0.4890637100 + 0.5431752801 0.5299065113 0.4905168116 + 0.5444123149 0.5311611295 0.4919654131 + 0.5456442237 0.5324099064 0.4934079051 + 0.5468729138 0.5336548090 0.4948467910 + 0.5480973125 0.5348948240 0.4962810874 + 0.5493161082 0.5361279249 0.4977090061 + 0.5505313873 0.5373569727 0.4991332889 + 0.5517416000 0.5385804772 0.5005524158 + 0.5529457927 0.5397964120 0.5019652843 + 0.5541468859 0.5410084128 0.5033745766 + 0.5553439856 0.5422148705 0.5047777891 + 0.5565372705 0.5434154272 0.5061753988 + 0.5577278137 0.5446121097 0.5075693727 + 0.5589103103 0.5458009839 0.5089558959 + 0.5600882173 0.5469862819 0.5103380084 + 0.5612615943 0.5481684208 0.5117158890 + 0.5624287128 0.5493460298 0.5130857825 + 0.5635939837 0.5505207777 0.5144522190 + 0.5647562146 0.5516902208 0.5158123970 + 0.5659151077 0.5528519750 0.5171663761 + 0.5670722127 0.5540099740 0.5185170174 + 0.5682216287 0.5551596284 0.5198565125 + 0.5693680048 0.5563063025 0.5211923718 + 0.5705093145 0.5574489832 0.5225226283 + 0.5716463923 0.5585880280 0.5238460898 + 0.5727816820 0.5597252250 0.5251665711 + 0.5739135742 0.5608549118 0.5264781117 + 0.5750433803 0.5619807243 0.5277863741 + 0.5761687756 0.5630995035 0.5290886760 + 0.5772883892 0.5642123818 0.5303848982 + 0.5784044862 0.5653222799 0.5316777825 + 0.5795143247 0.5664256811 0.5329598784 + 0.5806226730 0.5675271749 0.5342394114 + 0.5817270875 0.5686224103 0.5355114937 + 0.5828294754 0.5697141886 0.5367797017 + 0.5839291215 0.5708016157 0.5380430222 + 0.5850237012 0.5718852878 0.5393006206 + 0.5861157179 0.5729672909 0.5405554175 + 0.5872008204 0.5740441084 0.5418028235 + 0.5882844925 0.5751196146 0.5430486798 + 0.5893629789 0.5761870146 0.5442872047 + 0.5904405713 0.5772519112 0.5455237031 + 0.5915157199 0.5783103704 0.5467544794 + 0.5925881267 0.5793656707 0.5479820967 + 0.5936570168 0.5804172754 0.5492056012 + 0.5947207212 0.5814660788 0.5504246950 + 0.5957809091 0.5825130939 0.5516411066 + 0.5968360901 0.5835543871 0.5528522134 + 0.5978894830 0.5845938921 0.5540617108 + 0.5989391208 0.5856245756 0.5552654266 + 0.5999881029 0.5866541862 0.5564681888 + 0.6010323763 0.5876761079 0.5576648116 + 0.6020759940 0.5886973739 0.5588607192 + 0.6031129956 0.5897135735 0.5600510836 + 0.6041488051 0.5907285810 0.5612400770 + 0.6051782966 0.5917387009 0.5624223948 + 0.6062065959 0.5927467942 0.5636029243 + 0.6072307825 0.5937488079 0.5647776127 + 0.6082534790 0.5947481990 0.5659500957 + 0.6092721820 0.5957416296 0.5671173930 + 0.6102892160 0.5967329144 0.5682824850 + 0.6113023162 0.5977199078 0.5694428086 + 0.6123135090 0.5987045169 0.5706002116 + 0.6133208871 0.5996844769 0.5717515945 + 0.6143262982 0.6006621718 0.5729001760 + 0.6153277755 0.6016355753 0.5740433931 + 0.6163274050 0.6026067138 0.5751839876 + 0.6173228025 0.6035735011 0.5763192177 + 0.6183164120 0.6045382023 0.5774521828 + 0.6193056703 0.6054980159 0.5785800815 + 0.6202933788 0.6064559221 0.5797063112 + 0.6212763786 0.6074082851 0.5808271170 + 0.6222583055 0.6083593965 0.5819467902 + 0.6232348084 0.6093046069 0.5830612183 + 0.6242107153 0.6102492809 0.5841749907 + 0.6251806021 0.6111894846 0.5852832198 + 0.6261504889 0.6121296883 0.5863913894 + 0.6271138787 0.6130650043 0.5874921083 + 0.6280770898 0.6140003204 0.5885927081 + 0.6290364861 0.6149287820 0.5896871090 + 0.6299952865 0.6158562899 0.5907804966 + 0.6309506893 0.6167783141 0.5918691754 + 0.6319050193 0.6176983714 0.5929561257 + 0.6328564286 0.6186152101 0.5940394998 + 0.6338058114 0.6195300221 0.5951203108 + 0.6347532272 0.6204434037 0.5961987972 + 0.6356977224 0.6213548779 0.5972741246 + 0.6366410255 0.6222651005 0.5983482003 + 0.6375805140 0.6231712103 0.5994184017 + 0.6385198832 0.6240770817 0.6004886031 + 0.6394541860 0.6249756217 0.6015521884 + 0.6403884888 0.6258741021 0.6026158929 + 0.6413171291 0.6267682910 0.6036748886 + 0.6422441006 0.6276612878 0.6047325730 + 0.6431670189 0.6285513043 0.6057875156 + 0.6440864801 0.6294386983 0.6068398952 + 0.6450045109 0.6303241253 0.6078910232 + 0.6459187269 0.6312044263 0.6089388132 + 0.6468328238 0.6320846081 0.6099866033 + 0.6477437019 0.6329575181 0.6110302806 + 0.6486546993 0.6338304281 0.6120740771 + 0.6495618224 0.6346995831 0.6131126285 + 0.6504672766 0.6355671883 0.6141489744 + 0.6513699889 0.6364325881 0.6151828170 + 0.6522679925 0.6372947097 0.6162124276 + 0.6531658173 0.6381564736 0.6172416806 + 0.6540582776 0.6390134096 0.6182652116 + 0.6549509168 0.6398702264 0.6192886233 + 0.6558411717 0.6407238245 0.6203082800 + 0.6567304134 0.6415758729 0.6213262081 + 0.6576179862 0.6424263716 0.6223418713 + 0.6585016847 0.6432734728 0.6233528256 + 0.6593855023 0.6441205740 0.6243637204 + 0.6602624059 0.6449629068 0.6253690124 + 0.6611385942 0.6458048224 0.6263738275 + 0.6620121002 0.6466442943 0.6273748279 + 0.6628826857 0.6474813819 0.6283720732 + 0.6637530923 0.6483181715 0.6293689013 + 0.6646183133 0.6491507292 0.6303573847 + 0.6654834747 0.6499832869 0.6313458085 + 0.6663444042 0.6508116722 0.6323295832 + 0.6672021747 0.6516373754 0.6333100796 + 0.6680591702 0.6524623036 0.6342896819 + 0.6689094901 0.6532815099 0.6352627873 + 0.6697599292 0.6541007161 0.6362358928 + 0.6706069708 0.6549171805 0.6372048855 + 0.6714519858 0.6557319760 0.6381713152 + 0.6722967029 0.6565461755 0.6391372085 + 0.6731383204 0.6573548913 0.6400974989 + 0.6739798784 0.6581636071 0.6410579085 + 0.6748195887 0.6589679122 0.6420152187 + 0.6756578088 0.6597688198 0.6429703236 + 0.6764960289 0.6605696082 0.6439253092 + 0.6773291230 0.6613646746 0.6448736787 + 0.6781619787 0.6621596813 0.6458221078 + 0.6789928079 0.6629531980 0.6467680931 + 0.6798204780 0.6637446284 0.6477108002 + 0.6806482077 0.6645358801 0.6486533880 + 0.6814717054 0.6653245091 0.6495919824 + 0.6822941899 0.6661124825 0.6505295038 + 0.6831157207 0.6668993831 0.6514660120 + 0.6839330792 0.6676816940 0.6523975730 + 0.6847504973 0.6684640050 0.6533290744 + 0.6855651736 0.6692435145 0.6542578936 + 0.6863775253 0.6700205207 0.6551842093 + 0.6871898174 0.6707975864 0.6561105847 + 0.6879963279 0.6715716720 0.6570330858 + 0.6888018250 0.6723455191 0.6579551101 + 0.6896061897 0.6731184125 0.6588761210 + 0.6904050112 0.6738871932 0.6597927213 + 0.6912037730 0.6746560931 0.6607092023 + 0.6920003891 0.6754217744 0.6616228223 + 0.6927945018 0.6761841178 0.6625328064 + 0.6935886145 0.6769462824 0.6634427905 + 0.6943795085 0.6777051091 0.6643487811 + 0.6951690912 0.6784626842 0.6652531028 + 0.6959587932 0.6792201996 0.6661574841 + 0.6967442036 0.6799734235 0.6670572758 + 0.6975293159 0.6807262897 0.6679568291 + 0.6983137727 0.6814782023 0.6688550711 + 0.6990944743 0.6822254062 0.6697469950 + 0.6998751163 0.6829724908 0.6706389785 + 0.7006543279 0.6837174892 0.6715285182 + 0.7014306784 0.6844587922 0.6724137068 + 0.7022070885 0.6851999760 0.6732988954 + 0.7029811740 0.6859381795 0.6741808057 + 0.7037534118 0.6866738200 0.6750599742 + 0.7045254707 0.6874092817 0.6759392023 + 0.7052934766 0.6881412268 0.6768147945 + 0.7060595751 0.6888713837 0.6776887774 + 0.7068256140 0.6896017194 0.6785628200 + 0.7075870037 0.6903280020 0.6794317961 + 0.7083472013 0.6910533905 0.6802995801 + 0.7091073990 0.6917787790 0.6811674237 + 0.7098640800 0.6924982071 0.6820288897 + 0.7106204033 0.6932169795 0.6828898191 + 0.7113767266 0.6939358115 0.6837506890 + 0.7121276259 0.6946483254 0.6846057177 + 0.7128784060 0.6953607798 0.6854606867 + 0.7136288881 0.6960731149 0.6863154173 + 0.7143725157 0.6967815161 0.6871635914 + 0.7151160836 0.6974899173 0.6880118847 + 0.7158594131 0.6981980801 0.6888597012 + 0.7165976167 0.6989027858 0.6897020936 + 0.7173358798 0.6996076107 0.6905444860 + 0.7180739045 0.7003120780 0.6913864017 + 0.7188072801 0.7010132074 0.6922219992 + 0.7195405960 0.7017142773 0.6930575967 + 0.7202737927 0.7024151087 0.6938928962 + 0.7210023999 0.7031126022 0.6947227716 + 0.7217310071 0.7038099766 0.6955527067 + 0.7224596143 0.7045074105 0.6963825822 + 0.7231836915 0.7052016854 0.6972076297 + 0.7239078283 0.7058959007 0.6980326772 + 0.7246319056 0.7065901160 0.6988577247 + 0.7253506184 0.7072818279 0.6996777058 + 0.7260687947 0.7079733014 0.7004973292 + 0.7267869711 0.7086647749 0.7013168931 + 0.7275004983 0.7093524933 0.7021313906 + 0.7282128930 0.7100393772 0.7029448152 + 0.7289254069 0.7107263207 0.7037581801 + 0.7296341062 0.7114102840 0.7045679092 + 0.7303411961 0.7120929956 0.7053759098 + 0.7310482860 0.7127758265 0.7061840296 + 0.7317516804 0.7134565115 0.7069895864 + 0.7324519753 0.7141355872 0.7077932954 + 0.7331522107 0.7148147225 0.7085970044 + 0.7338507175 0.7154917121 0.7093988061 + 0.7345461249 0.7161653042 0.7101973295 + 0.7352414727 0.7168390155 0.7109956741 + 0.7359364033 0.7175115943 0.7117931843 + 0.7366288900 0.7181801796 0.7125861049 + 0.7373213172 0.7188488245 0.7133789062 + 0.7380138040 0.7195172906 0.7141718268 + 0.7387021184 0.7201828957 0.7149586082 + 0.7393900752 0.7208483815 0.7157452106 + 0.7400780916 0.7215139270 0.7165318131 + 0.7407631874 0.7221763134 0.7173146009 + 0.7414469719 0.7228373885 0.7180958986 + 0.7421306968 0.7234985232 0.7188770771 + 0.7428128123 0.7241570950 0.7196565866 + 0.7434924841 0.7248123884 0.7204334736 + 0.7441722155 0.7254676819 0.7212104201 + 0.7448515892 0.7261224985 0.7219868898 + 0.7455275059 0.7267739773 0.7227588296 + 0.7462034225 0.7274255157 0.7235307097 + 0.7468793988 0.7280768752 0.7243025899 + 0.7475525737 0.7287256122 0.7250691056 + 0.7482250929 0.7293733954 0.7258337736 + 0.7488974929 0.7300211787 0.7265986204 + 0.7495679855 0.7306671739 0.7273609042 + 0.7502354980 0.7313104868 0.7281196117 + 0.7509030104 0.7319536805 0.7288782001 + 0.7515705228 0.7325968742 0.7296367884 + 0.7522333264 0.7332357764 0.7303904295 + 0.7528960109 0.7338746786 0.7311440110 + 0.7535588145 0.7345135808 0.7318975925 + 0.7542191148 0.7351499796 0.7326478958 + 0.7548775077 0.7357847095 0.7333958745 + 0.7555359006 0.7364193797 0.7341439128 + 0.7561935782 0.7370532751 0.7348909974 + 0.7568460703 0.7376828790 0.7356327176 + 0.7574986815 0.7383124828 0.7363743782 + 0.7581512928 0.7389420867 0.7371160984 + 0.7588003278 0.7395684719 0.7378535271 + 0.7594472766 0.7401931286 0.7385885119 + 0.7600942850 0.7408177853 0.7393234968 + 0.7607408166 0.7414417863 0.7400577068 + 0.7613841295 0.7420616746 0.7407870889 + 0.7620275021 0.7426816225 0.7415165901 + 0.7626708746 0.7433015108 0.7422459722 + 0.7633122802 0.7439180017 0.7429726720 + 0.7639523745 0.7445324063 0.7436977029 + 0.7645925283 0.7451466918 0.7444226146 + 0.7652325034 0.7457607985 0.7451472878 + 0.7658681273 0.7463697195 0.7458673120 + 0.7665035725 0.7469785810 0.7465873957 + 0.7671391964 0.7475875020 0.7473074198 + 0.7677729130 0.7481939793 0.7480244040 + 0.7684043050 0.7487975955 0.7487375736 + 0.7690356970 0.7494012117 0.7494506836 + 0.7696670890 0.7500048876 0.7501639128 + 0.7702953815 0.7506046295 0.7508723140 + 0.7709228992 0.7512034774 0.7515797019 + 0.7715504766 0.7518025041 0.7522869706 + 0.7721772194 0.7524008751 0.7529935837 + 0.7727996707 0.7529972196 0.7536963820 + 0.7734221816 0.7535933852 0.7543991804 + 0.7740446925 0.7541896105 0.7551020980 + 0.7746649981 0.7547839284 0.7558023930 + 0.7752826810 0.7553762794 0.7565001845 + 0.7759004831 0.7559686899 0.7571979761 + 0.7765182853 0.7565611005 0.7578958273 + 0.7771326900 0.7571496964 0.7585895061 + 0.7777460814 0.7577372789 0.7592819929 + 0.7783594728 0.7583248019 0.7599744201 + 0.7789728045 0.7589122057 0.7606667280 + 0.7795792222 0.7594966888 0.7613533735 + 0.7801855803 0.7600811720 0.7620400190 + 0.7807919979 0.7606657147 0.7627267241 + 0.7813968062 0.7612491846 0.7634122968 + 0.7819966078 0.7618296742 0.7640944123 + 0.7825964093 0.7624102235 0.7647765279 + 0.7831960917 0.7629907131 0.7654587030 + 0.7837939262 0.7635689974 0.7661380172 + 0.7843887806 0.7641444206 0.7668138146 + 0.7849838138 0.7647197843 0.7674896717 + 0.7855787873 0.7652952075 0.7681655288 + 0.7861704826 0.7658686042 0.7688372731 + 0.7867596745 0.7664408088 0.7695063949 + 0.7873489261 0.7670128942 0.7701755166 + 0.7879381776 0.7675850987 0.7708445787 + 0.7885246873 0.7681542039 0.7715098858 + 0.7891101837 0.7687221169 0.7721735835 + 0.7896956205 0.7692900896 0.7728372812 + 0.7902809978 0.7698580027 0.7735010982 + 0.7908638120 0.7704228759 0.7741602063 + 0.7914459109 0.7709870934 0.7748184204 + 0.7920280099 0.7715513706 0.7754765153 + 0.7926099896 0.7721155882 0.7761346102 + 0.7931870222 0.7726777196 0.7767888904 + 0.7937632799 0.7732396722 0.7774425745 + 0.7943394780 0.7738016248 0.7780963182 + 0.7949157953 0.7743635178 0.7787501216 + 0.7954884768 0.7749227285 0.7793995142 + 0.7960608006 0.7754815817 0.7800486088 + 0.7966331244 0.7760406137 0.7806975842 + 0.7972053885 0.7765995860 0.7813466787 + 0.7977746129 0.7771558762 0.7819913030 + 0.7983434796 0.7777119875 0.7826355100 + 0.7989124060 0.7782680988 0.7832797170 + 0.7994812727 0.7788242102 0.7839238048 + 0.8000450134 0.7793778181 0.7845636010 + 0.8006079793 0.7799311280 0.7852026820 + 0.8011708856 0.7804843187 0.7858418226 + 0.8017339110 0.7810376287 0.7864809036 + 0.8022924066 0.7815880775 0.7871146202 + 0.8028498292 0.7821378708 0.7877470851 + 0.8034073114 0.7826877236 0.7883794904 + 0.8039646745 0.7832375765 0.7890118957 + 0.8045186996 0.7837849855 0.7896395922 + 0.8050711751 0.7843313813 0.7902652025 + 0.8056237102 0.7848778963 0.7908908129 + 0.8061761856 0.7854242921 0.7915164232 + 0.8067255020 0.7859687805 0.7921388745 + 0.8072721958 0.7865118980 0.7927588820 + 0.8078188896 0.7870550156 0.7933788896 + 0.8083655834 0.7875980735 0.7939988971 + 0.8089101911 0.7881401777 0.7946166992 + 0.8094515800 0.7886806130 0.7952311039 + 0.8099929094 0.7892209888 0.7958455086 + 0.8105341792 0.7897614241 0.7964599133 + 0.8110746741 0.7903016806 0.7970730066 + 0.8116112947 0.7908411026 0.7976801991 + 0.8121479154 0.7913805842 0.7982873917 + 0.8126844764 0.7919200063 0.7988945842 + 0.8132210970 0.7924594879 0.7995017767 + 0.8137524128 0.7929970026 0.8001034260 + 0.8142833114 0.7935343981 0.8007047176 + 0.8148142099 0.7940719128 0.8013060093 + 0.8153451085 0.7946093082 0.8019073009 + 0.8158726096 0.7951447964 0.8025054932 + 0.8163983226 0.7956792712 0.8031023145 + 0.8169239759 0.7962138057 0.8036990762 + 0.8174496889 0.7967482209 0.8042958975 + 0.8179737926 0.7972822785 0.8048911095 + 0.8184949160 0.7978156209 0.8054832816 + 0.8190159798 0.7983487844 0.8060755134 + 0.8195371032 0.7988821268 0.8066676855 + 0.8200582862 0.7994152904 0.8072599173 + 0.8205738068 0.7999464273 0.8078473806 + 0.8210893273 0.8004773855 0.8084350228 + 0.8216049075 0.8010084033 0.8090224862 + 0.8221204281 0.8015394211 0.8096100092 + 0.8226320148 0.8020684123 0.8101946115 + 0.8231410980 0.8025962710 0.8107774258 + 0.8236501217 0.8031241298 0.8113601804 + 0.8241592050 0.8036519289 0.8119429946 + 0.8246672153 0.8041794896 0.8125250936 + 0.8251705170 0.8047057986 0.8131045103 + 0.8256738186 0.8052322268 0.8136839271 + 0.8261770010 0.8057584763 0.8142634034 + 0.8266803026 0.8062849045 0.8148428202 + 0.8271808028 0.8068094254 0.8154187202 + 0.8276802897 0.8073332906 0.8159933090 + 0.8281797171 0.8078572154 0.8165680170 + 0.8286792040 0.8083810806 0.8171426058 + 0.8291776180 0.8089041114 0.8177158237 + 0.8296723962 0.8094245791 0.8182842731 + 0.8301671743 0.8099449873 0.8188527822 + 0.8306620121 0.8104655147 0.8194214106 + 0.8311567903 0.8109859824 0.8199899197 + 0.8316488266 0.8115053177 0.8205549717 + 0.8321394920 0.8120239973 0.8211187720 + 0.8326302767 0.8125427961 0.8216825724 + 0.8331211209 0.8130615950 0.8222463727 + 0.8336114883 0.8135799170 0.8228093982 + 0.8341001868 0.8140959740 0.8233684897 + 0.8345888853 0.8146120906 0.8239275217 + 0.8350775242 0.8151282072 0.8244866133 + 0.8355662227 0.8156443238 0.8250455856 + 0.8360525966 0.8161584139 0.8256021738 + 0.8365371227 0.8166708946 0.8261566162 + 0.8370215297 0.8171833158 0.8267109990 + 0.8375059962 0.8176957965 0.8272655010 + 0.8379905224 0.8182082772 0.8278198838 + 0.8384705782 0.8187190294 0.8283702135 + 0.8389502168 0.8192296028 0.8289201260 + 0.8394296765 0.8197401166 0.8294699192 + 0.8399093151 0.8202505708 0.8300197124 + 0.8403881788 0.8207604885 0.8305683136 + 0.8408648968 0.8212682009 0.8311123848 + 0.8413414955 0.8217757940 0.8316563964 + 0.8418180943 0.8222835064 0.8322004080 + 0.8422946930 0.8227912188 0.8327444792 + 0.8427696228 0.8232969046 0.8332856894 + 0.8432425857 0.8238008022 0.8338239789 + 0.8437157273 0.8243045807 0.8343623281 + 0.8441886902 0.8248084784 0.8349006772 + 0.8446617723 0.8253123760 0.8354390264 + 0.8451316953 0.8258146048 0.8359742165 + 0.8456004858 0.8263162971 0.8365082145 + 0.8460692167 0.8268179893 0.8370422125 + 0.8465380073 0.8273196816 0.8375762105 + 0.8470066786 0.8278213739 0.8381102085 + 0.8474733233 0.8283202052 0.8386403918 + 0.8479397297 0.8288187981 0.8391703963 + 0.8484060764 0.8293173909 0.8397002816 + 0.8488724828 0.8298161030 0.8402302861 + 0.8493387103 0.8303142786 0.8407598138 + 0.8498020768 0.8308088779 0.8412843943 + 0.8502655029 0.8313035965 0.8418090940 + 0.8507289290 0.8317981958 0.8423337936 + 0.8511922956 0.8322929144 0.8428584933 + 0.8516547084 0.8327869773 0.8433821201 + 0.8521131873 0.8332791924 0.8439015746 + 0.8525716066 0.8337715268 0.8444212079 + 0.8530300856 0.8342636824 0.8449407220 + 0.8534886241 0.8347560167 0.8454602957 + 0.8539462090 0.8352472782 0.8459783792 + 0.8544015884 0.8357362747 0.8464931250 + 0.8548570871 0.8362252712 0.8470076919 + 0.8553125858 0.8367143273 0.8475223184 + 0.8557680249 0.8372033238 0.8480370045 + 0.8562228084 0.8376908898 0.8485500813 + 0.8566759229 0.8381757140 0.8490599990 + 0.8571289778 0.8386605978 0.8495699167 + 0.8575822115 0.8391454220 0.8500797749 + 0.8580353260 0.8396301866 0.8505896926 + 0.8584871292 0.8401141763 0.8510981798 + 0.8589360118 0.8405964971 0.8516036868 + 0.8593848944 0.8410786986 0.8521090746 + 0.8598337770 0.8415609002 0.8526145816 + 0.8602827191 0.8420432210 0.8531200290 + 0.8607307076 0.8425244093 0.8536239862 + 0.8611760139 0.8430032134 0.8541238904 + 0.8616213202 0.8434820175 0.8546237946 + 0.8620666862 0.8439608812 0.8551236987 + 0.8625119925 0.8444396853 0.8556236029 + 0.8629568219 0.8449175954 0.8561224937 + 0.8633992076 0.8453921080 0.8566170931 + 0.8638417125 0.8458666205 0.8571115732 + 0.8642840981 0.8463410139 0.8576061130 + 0.8647266030 0.8468155265 0.8581007123 + 0.8651688099 0.8472898006 0.8585950136 + 0.8656079173 0.8477615118 0.8590856194 + 0.8660469055 0.8482331038 0.8595762849 + 0.8664858937 0.8487048149 0.8600668907 + 0.8669250011 0.8491765261 0.8605576158 + 0.8673639894 0.8496481180 0.8610482216 + 0.8677995801 0.8501166105 0.8615357280 + 0.8682346940 0.8505846262 0.8620226979 + 0.8686698079 0.8510527015 0.8625097871 + 0.8691048026 0.8515207767 0.8629968762 + 0.8695399165 0.8519887924 0.8634839058 + 0.8699728847 0.8524538279 0.8639678955 + 0.8704047799 0.8529173732 0.8644505143 + 0.8708366156 0.8533809185 0.8649330735 + 0.8712685108 0.8538445234 0.8654156923 + 0.8717002869 0.8543081284 0.8658981919 + 0.8721306920 0.8547704220 0.8663790226 + 0.8725587130 0.8552309275 0.8668572903 + 0.8729867935 0.8556914926 0.8673356175 + 0.8734148741 0.8561519980 0.8678138852 + 0.8738428950 0.8566126227 0.8682922125 + 0.8742704988 0.8570727110 0.8687700033 + 0.8746942878 0.8575295210 0.8692442775 + 0.8751180172 0.8579862714 0.8697186112 + 0.8755416870 0.8584430814 0.8701928854 + 0.8759654760 0.8588998914 0.8706671000 + 0.8763892055 0.8593567014 0.8711413741 + 0.8768100739 0.8598098755 0.8716111183 + 0.8772302270 0.8602619767 0.8720793724 + 0.8776502013 0.8607140779 0.8725476861 + 0.8780702949 0.8611662984 0.8730159998 + 0.8784903288 0.8616183996 0.8734843135 + 0.8789088130 0.8620693088 0.8739504814 + 0.8793246746 0.8625183105 0.8744130135 + 0.8797405958 0.8629671931 0.8748756051 + 0.8801565170 0.8634161949 0.8753381968 + 0.8805723190 0.8638650775 0.8758007884 + 0.8809881806 0.8643140793 0.8762633801 + 0.8814008832 0.8647593260 0.8767219186 + 0.8818134069 0.8652043939 0.8771802187 + 0.8822258711 0.8656494021 0.8776385188 + 0.8826382756 0.8660945296 0.8780966997 + 0.8830507994 0.8665394783 0.8785549998 + 0.8834620118 0.8669828176 0.8790109158 + 0.8838717937 0.8674241900 0.8794640899 + 0.8842815161 0.8678655028 0.8799173236 + 0.8846911788 0.8683068752 0.8803703785 + 0.8851009011 0.8687481880 0.8808236122 + 0.8855106831 0.8691896200 0.8812767863 + 0.8859174252 0.8696290851 0.8817254901 + 0.8863239288 0.8700686097 0.8821738958 + 0.8867303729 0.8705080152 0.8826223016 + 0.8871369958 0.8709474802 0.8830707073 + 0.8875434995 0.8713868856 0.8835191131 + 0.8879491091 0.8718252778 0.8839659095 + 0.8883531094 0.8722618222 0.8844100833 + 0.8887569904 0.8726983070 0.8848543167 + 0.8891609907 0.8731347919 0.8852983713 + 0.8895649910 0.8735713959 0.8857426047 + 0.8899689913 0.8740078807 0.8861867189 + 0.8903707862 0.8744425774 0.8866271973 + 0.8907716870 0.8748766780 0.8870663047 + 0.8911727071 0.8753107786 0.8875054717 + 0.8915737271 0.8757449985 0.8879445791 + 0.8919746876 0.8761790991 0.8883838058 + 0.8923754096 0.8766130209 0.8888226151 + 0.8927729726 0.8770452738 0.8892570734 + 0.8931705952 0.8774775267 0.8896915913 + 0.8935680985 0.8779097199 0.8901261091 + 0.8939657211 0.8783419728 0.8905606270 + 0.8943632245 0.8787742257 0.8909950852 + 0.8947597742 0.8792054057 0.8914281130 + 0.8951544762 0.8796346784 0.8918585181 + 0.8955491781 0.8800640702 0.8922889233 + 0.8959439993 0.8804934025 0.8927193284 + 0.8963387012 0.8809227943 0.8931496739 + 0.8967334032 0.8813521862 0.8935800791 + 0.8971260786 0.8817800879 0.8940076232 + 0.8975175023 0.8822070956 0.8944330812 + 0.8979089856 0.8826341033 0.8948585987 + 0.8983004093 0.8830611110 0.8952841163 + 0.8986917734 0.8834881186 0.8957095742 + 0.8990831971 0.8839151263 0.8961350918 + 0.8994715810 0.8843405843 0.8965569735 + 0.8998593092 0.8847656846 0.8969780207 + 0.9002469778 0.8851909041 0.8973988891 + 0.9006347060 0.8856160045 0.8978198767 + 0.9010223746 0.8860412240 0.8982409239 + 0.9014101028 0.8864663243 0.8986619115 + 0.9017949104 0.8868888021 0.8990790844 + 0.9021794796 0.8873111010 0.8994960785 + 0.9025642276 0.8877333999 0.8999131918 + 0.9029487967 0.8881556988 0.9003301859 + 0.9033334851 0.8885779977 0.9007471800 + 0.9037179947 0.8890001774 0.9011639953 + 0.9041002989 0.8894202113 0.9015762210 + 0.9044826031 0.8898401856 0.9019883871 + 0.9048647881 0.8902602196 0.9024006128 + 0.9052470922 0.8906801939 0.9028128982 + 0.9056293964 0.8911002278 0.9032251239 + 0.9060112834 0.8915200233 0.9036368132 + 0.9063897133 0.8919382095 0.9040434957 + 0.9067682028 0.8923563957 0.9044501781 + 0.9071465731 0.8927745819 0.9048569202 + 0.9075250030 0.8931927085 0.9052636027 + 0.9079034925 0.8936108947 0.9056702852 + 0.9082816243 0.8940287828 0.9060766101 + 0.9086568952 0.8944442272 0.9064794779 + 0.9090322256 0.8948596120 0.9068824053 + 0.9094074965 0.8952749968 0.9072852731 + 0.9097827077 0.8956903815 0.9076882005 + 0.9101579785 0.8961058259 0.9080911279 + 0.9105333090 0.8965210915 0.9084938765 + 0.9109072089 0.8969342113 0.9088944793 + 0.9112812281 0.8973472714 0.9092950225 + 0.9116551280 0.8977603912 0.9096956253 + 0.9120290875 0.8981735110 0.9100962281 + 0.9124029875 0.8985865712 0.9104967713 + 0.9127770066 0.8989996910 0.9108973742 + 0.9131482840 0.8994112015 0.9112942219 + 0.9135193825 0.8998224735 0.9116908908 + 0.9138906002 0.9002338052 0.9120875001 + 0.9142616987 0.9006450772 0.9124842286 + 0.9146327972 0.9010564089 0.9128807783 + 0.9150040150 0.9014678001 0.9132773876 + 0.9153717160 0.9018769264 0.9136704803 + 0.9157385230 0.9022855163 0.9140626192 + 0.9161052704 0.9026941061 0.9144545794 + 0.9164720774 0.9031026959 0.9148467183 + 0.9168388844 0.9035112262 0.9152387977 + 0.9172058105 0.9039198160 0.9156308770 + 0.9175711274 0.9043270946 0.9160212278 + 0.9179353714 0.9047334194 0.9164103270 + 0.9182996750 0.9051396847 0.9167994261 + 0.9186639786 0.9055461287 0.9171885848 + 0.9190282822 0.9059523940 0.9175776839 + 0.9193925858 0.9063587785 0.9179667830 + 0.9197562933 0.9067645073 0.9183548093 + 0.9201188087 0.9071691036 0.9187405109 + 0.9204813242 0.9075737000 0.9191262126 + 0.9208437800 0.9079781771 0.9195119143 + 0.9212062955 0.9083827734 0.9198976159 + 0.9215688109 0.9087874293 0.9202833176 + 0.9219312072 0.9091919065 0.9206687808 + 0.9222893715 0.9095938206 0.9210494161 + 0.9226475954 0.9099956155 0.9214301109 + 0.9230058193 0.9103975296 0.9218106866 + 0.9233639836 0.9107993841 0.9221913218 + 0.9237222075 0.9112012982 0.9225718975 + 0.9240803123 0.9116032124 0.9229525924 + 0.9244369268 0.9120035768 0.9233309031 + 0.9247925282 0.9124032259 0.9237080812 + 0.9251481891 0.9128028750 0.9240853786 + 0.9255039096 0.9132025838 0.9244626164 + 0.9258595705 0.9136022925 0.9248397946 + 0.9262152910 0.9140018821 0.9252169728 + 0.9265707731 0.9144011736 0.9255934954 + 0.9269257188 0.9147990942 0.9259678721 + 0.9272807240 0.9151970148 0.9263421893 + 0.9276356101 0.9155949950 0.9267166257 + 0.9279906154 0.9159929156 0.9270910025 + 0.9283455014 0.9163908958 0.9274653792 + 0.9287005067 0.9167888165 0.9278396964 + 0.9290528893 0.9171856046 0.9282106161 + 0.9294046760 0.9175820947 0.9285805225 + 0.9297564030 0.9179785252 0.9289504290 + 0.9301081896 0.9183750153 0.9293202758 + 0.9304599166 0.9187715054 0.9296901822 + 0.9308117032 0.9191678762 0.9300600886 + 0.9311624765 0.9195644259 0.9304289222 + 0.9315106869 0.9199606180 0.9307948947 + 0.9318587780 0.9203569293 0.9311608076 + 0.9322069883 0.9207531810 0.9315267801 + 0.9325551987 0.9211494923 0.9318928123 + 0.9329034090 0.9215456843 0.9322587848 + 0.9332516193 0.9219419956 0.9326246977 + 0.9335985184 0.9223375916 0.9329884052 + 0.9339448810 0.9227328897 0.9333512187 + 0.9342911839 0.9231281281 0.9337139130 + 0.9346376061 0.9235234261 0.9340766072 + 0.9349840283 0.9239187241 0.9344394207 + 0.9353303909 0.9243140221 0.9348021150 + 0.9356765747 0.9247090816 0.9351645112 + 0.9360207915 0.9251027107 0.9355235100 + 0.9363651276 0.9254962802 0.9358825088 + 0.9367092848 0.9258899093 0.9362415075 + 0.9370536208 0.9262835979 0.9366003871 + 0.9373977780 0.9266772270 0.9369593859 + 0.9377421141 0.9270707965 0.9373183846 + 0.9380850196 0.9274641871 0.9376757145 + 0.9384257793 0.9278573990 0.9380304813 + 0.9387664795 0.9282506704 0.9383851886 + 0.9391072989 0.9286438823 0.9387400150 + 0.9394481182 0.9290370941 0.9390947223 + 0.9397888780 0.9294303060 0.9394494891 + 0.9401296973 0.9298235178 0.9398041964 + 0.9404693842 0.9302158952 0.9401566982 + 0.9408084750 0.9306079149 0.9405080080 + 0.9411476254 0.9309998751 0.9408593178 + 0.9414865971 0.9313920140 0.9412106276 + 0.9418256879 0.9317839742 0.9415618777 + 0.9421647787 0.9321759939 0.9419131875 + 0.9425039291 0.9325680137 0.9422644973 + 0.9428421259 0.9329584837 0.9426131845 + 0.9431802034 0.9333485961 0.9429615140 + 0.9435182810 0.9337388277 0.9433097839 + 0.9438564181 0.9341289997 0.9436581135 + 0.9441944957 0.9345191121 0.9440063834 + 0.9445325732 0.9349092841 0.9443547130 + 0.9448707104 0.9352995157 0.9447029829 + 0.9452053905 0.9356890917 0.9450477958 + 0.9455401897 0.9360786080 0.9453927279 + 0.9458749294 0.9364681840 0.9457374811 + 0.9462096095 0.9368578196 0.9460824132 + 0.9465444088 0.9372473955 0.9464272261 + 0.9468790889 0.9376369119 0.9467720985 + 0.9472135901 0.9380264282 0.9471164942 + 0.9475457072 0.9384145737 0.9474576116 + 0.9478777051 0.9388027787 0.9477987289 + 0.9482098222 0.9391909838 0.9481397867 + 0.9485418797 0.9395791888 0.9484809041 + 0.9488739967 0.9399673939 0.9488220215 + 0.9492059946 0.9403555989 0.9491630793 + 0.9495379925 0.9407432079 0.9495037794 + 0.9498690963 0.9411267042 0.9498416781 + 0.9502003193 0.9415103197 0.9501795769 + 0.9505314231 0.9418938756 0.9505175948 + 0.9508625865 0.9422774911 0.9508554935 + 0.9511936903 0.9426611066 0.9511933923 + 0.9515247941 0.9430447221 0.9515312910 + 0.9518557191 0.9434276223 0.9518690109 + 0.9521846175 0.9438055754 0.9522042871 + 0.9525135756 0.9441835880 0.9525396228 + 0.9528424740 0.9445616007 0.9528750181 + 0.9531713724 0.9449396133 0.9532102942 + 0.9535002708 0.9453176260 0.9535456896 + 0.9538292289 0.9456955194 0.9538810253 + 0.9541580081 0.9460734129 0.9542161226 + 0.9544816017 0.9464468956 0.9545456171 + 0.9548051953 0.9468203783 0.9548751116 + 0.9551287889 0.9471939802 0.9552046061 + 0.9554523826 0.9475675225 0.9555341005 + 0.9557759166 0.9479411244 0.9558635950 + 0.9560995102 0.9483146071 0.9561930895 + 0.9564231038 0.9486880898 0.9565225840 + 0.9567415714 0.9490590096 0.9568458200 + 0.9570595026 0.9494295716 0.9571682215 + 0.9573773146 0.9498001933 0.9574906230 + 0.9576951861 0.9501708150 0.9578130245 + 0.9580131173 0.9505413771 0.9581354260 + 0.9583309889 0.9509119987 0.9584578276 + 0.9586488008 0.9512825012 0.9587802291 + 0.9589642882 0.9516518712 0.9591000080 + 0.9592787027 0.9520208240 0.9594187140 + 0.9595931172 0.9523895979 0.9597374201 + 0.9599075913 0.9527583718 0.9600561261 + 0.9602220058 0.9531273246 0.9603748918 + 0.9605364203 0.9534960985 0.9606935978 + 0.9608507752 0.9538649917 0.9610123038 + 0.9611636996 0.9542328715 0.9613298178 + 0.9614747763 0.9545994997 0.9616457224 + 0.9617857933 0.9549661875 0.9619616866 + 0.9620968103 0.9553328753 0.9622775912 + 0.9624078274 0.9556996226 0.9625936151 + 0.9627187848 0.9560663104 0.9629095197 + 0.9630298018 0.9564329982 0.9632254839 + 0.9633405209 0.9567993283 0.9635409713 + 0.9636486173 0.9571633935 0.9638535976 + 0.9639567733 0.9575273991 0.9641662836 + 0.9642649889 0.9578914046 0.9644789100 + 0.9645730853 0.9582554102 0.9647914767 + 0.9648813009 0.9586194158 0.9651042223 + 0.9651893973 0.9589834809 0.9654167891 + 0.9654976130 0.9593474865 0.9657294154 + 0.9658039808 0.9597097039 0.9660398960 + 0.9661098123 0.9600713849 0.9663494825 + 0.9664155245 0.9604331255 0.9666591883 + 0.9667212963 0.9607948065 0.9669688940 + 0.9670271277 0.9611564875 0.9672784805 + 0.9673328996 0.9615182281 0.9675881863 + 0.9676386118 0.9618799090 0.9678978920 + 0.9679433107 0.9622406960 0.9682065248 + 0.9682455063 0.9625995755 0.9685127139 + 0.9685477018 0.9629585743 0.9688190222 + 0.9688498974 0.9633175135 0.9691252112 + 0.9691520929 0.9636763930 0.9694314003 + 0.9694542885 0.9640353918 0.9697375894 + 0.9697564840 0.9643942714 0.9700438976 + 0.9700586796 0.9647533298 0.9703500867 + 0.9703577757 0.9651095867 0.9706525803 + 0.9706562161 0.9654654264 0.9709542990 + 0.9709545970 0.9658210874 0.9712558985 + 0.9712529182 0.9661768079 0.9715574980 + 0.9715512991 0.9665325880 0.9718590975 + 0.9718496799 0.9668883085 0.9721608162 + 0.9721480012 0.9672440290 0.9724624157 + 0.9724456072 0.9675989747 0.9727630019 + 0.9727405906 0.9679515958 0.9730604291 + 0.9730355740 0.9683042169 0.9733577967 + 0.9733306766 0.9686567783 0.9736552238 + 0.9736257195 0.9690095186 0.9739525914 + 0.9739207029 0.9693620801 0.9742500186 + 0.9742156863 0.9697147012 0.9745473862 + 0.9745107293 0.9700673223 0.9748446941 + 0.9748036265 0.9704176188 0.9751394987 + 0.9750949740 0.9707666039 0.9754326940 + 0.9753863811 0.9711155295 0.9757258296 + 0.9756777883 0.9714645147 0.9760190248 + 0.9759691954 0.9718133807 0.9763122201 + 0.9762606025 0.9721624255 0.9766052961 + 0.9765521288 0.9725112915 0.9768984914 + 0.9768434763 0.9728602171 0.9771916270 + 0.9771314263 0.9732053876 0.9774799943 + 0.9774190784 0.9735503197 0.9777678847 + 0.9777066708 0.9738951921 0.9780558944 + 0.9779943228 0.9742401242 0.9783437848 + 0.9782819748 0.9745849967 0.9786317945 + 0.9785696268 0.9749299288 0.9789196849 + 0.9788572788 0.9752748013 0.9792076945 + 0.9791442752 0.9756188989 0.9794946909 + 0.9794281721 0.9759591818 0.9797772765 + 0.9797121286 0.9762995839 0.9800599217 + 0.9799959064 0.9766399860 0.9803425074 + 0.9802798033 0.9769803882 0.9806250930 + 0.9805637002 0.9773206711 0.9809077978 + 0.9808475971 0.9776610732 0.9811903834 + 0.9811314940 0.9780014753 0.9814730287 + 0.9814137816 0.9783400297 0.9817537069 + 0.9816933274 0.9786750078 0.9820309877 + 0.9819728136 0.9790101051 0.9823082089 + 0.9822522998 0.9793450832 0.9825854897 + 0.9825317860 0.9796801805 0.9828627110 + 0.9828112721 0.9800152183 0.9831399918 + 0.9830908179 0.9803503156 0.9834172726 + 0.9833703041 0.9806852937 0.9836944938 + 0.9836477041 0.9810177088 0.9839690924 + 0.9839227200 0.9813470840 0.9842407703 + 0.9841976762 0.9816765189 0.9845123887 + 0.9844726920 0.9820057750 0.9847840071 + 0.9847477078 0.9823352098 0.9850556254 + 0.9850227237 0.9826645255 0.9853271842 + 0.9852976203 0.9829939008 0.9855988026 + 0.9855725765 0.9833232760 0.9858704209 + 0.9858452082 0.9836488962 0.9861391783 + 0.9861155152 0.9839711785 0.9864053130 + 0.9863858223 0.9842935205 0.9866713285 + 0.9866561890 0.9846158028 0.9869374037 + 0.9869264960 0.9849380851 0.9872034788 + 0.9871968031 0.9852603078 0.9874696136 + 0.9874671102 0.9855825901 0.9877356291 + 0.9877374172 0.9859048724 0.9880017042 + 0.9880046248 0.9862229228 0.9882646799 + 0.9882686138 0.9865366220 0.9885246158 + 0.9885327220 0.9868503809 0.9887844920 + 0.9887967110 0.9871640801 0.9890444875 + 0.9890608191 0.9874778986 0.9893044233 + 0.9893248081 0.9877915978 0.9895642996 + 0.9895889163 0.9881054163 0.9898241758 + 0.9898529053 0.9884191155 0.9900841713 + 0.9901142120 0.9887291193 0.9903411865 + 0.9903718829 0.9890338778 0.9905943871 + 0.9906294942 0.9893386960 0.9908475280 + 0.9908872247 0.9896435142 0.9911007285 + 0.9911447763 0.9899483919 0.9913538098 + 0.9914023876 0.9902532101 0.9916070104 + 0.9916601181 0.9905580282 0.9918600917 + 0.9919177294 0.9908627868 0.9921131730 + 0.9921736121 0.9911643267 0.9923645854 + 0.9924249053 0.9914569855 0.9926109910 + 0.9926763177 0.9917497039 0.9928573966 + 0.9929276109 0.9920424223 0.9931039214 + 0.9931789041 0.9923352003 0.9933503270 + 0.9934303164 0.9926279187 0.9935967922 + 0.9936816096 0.9929205775 0.9938431978 + 0.9939329028 0.9932134151 0.9940897226 + 0.9941837192 0.9935051799 0.9943355918 + 0.9944264293 0.9937829971 0.9945728183 + 0.9946690202 0.9940606952 0.9948101044 + 0.9949116111 0.9943385124 0.9950473905 + 0.9951542020 0.9946163297 0.9952846169 + 0.9953969121 0.9948940873 0.9955219030 + 0.9956395030 0.9951717854 0.9957591891 + 0.9958820939 0.9954496026 0.9959964752 + 0.9961246848 0.9957274199 0.9962337017 + 0.9963588715 0.9959930182 0.9964621067 + 0.9965906739 0.9962550998 0.9966878295 + 0.9968224764 0.9965171814 0.9969136119 + 0.9970542789 0.9967793226 0.9971392751 + 0.9972860217 0.9970414042 0.9973651171 + 0.9975178242 0.9973034859 0.9975907803 + 0.9977496266 0.9975656271 0.9978166223 + 0.9979814291 0.9978277087 0.9980422854 + 0.9982097745 0.9980804920 0.9982643723 + 0.9984334707 0.9983204007 0.9984812737 + 0.9986572862 0.9985604286 0.9986982942 + 0.9988811016 0.9988002777 0.9989151955 + 0.9991049170 0.9990401864 0.9991322160 + 0.9993286729 0.9992802143 0.9993491173 + 0.9995524287 0.9995201230 0.9995660782 + 0.9997761846 0.9997600913 0.9997829795 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_vista_400.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_vista_400.spi1d index 33463884..fbfb6f1c 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_vista_400.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_vista_400.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0176717807 0.0159793403 0.0112875197 - 0.0304853506 0.0281650797 0.0208594296 - 0.0410733782 0.0387531407 0.0291500594 - 0.0505116396 0.0483908802 0.0366822407 - 0.0592069514 0.0573300086 0.0436884314 - 0.0673851594 0.0657815933 0.0503007919 - 0.0751756504 0.0738594905 0.0565973185 - 0.0826697499 0.0815291628 0.0626588985 - 0.0899700671 0.0889572278 0.0685112923 - 0.0971127078 0.0961195603 0.0741866305 - 0.1040614992 0.1030588970 0.0796945021 - 0.1108867973 0.1097960994 0.0850348696 - 0.1175592989 0.1162500009 0.0902234167 - 0.1240769997 0.1224389970 0.0952606276 - 0.1304063946 0.1284389049 0.1001622975 - 0.1365357041 0.1342712045 0.1049377993 - 0.1425068974 0.1399544030 0.1096132994 - 0.1483051926 0.1454842985 0.1141887009 - 0.1539328992 0.1508464068 0.1186479032 - 0.1594130993 0.1560681015 0.1230128035 - 0.1647385061 0.1611503065 0.1272864938 - 0.1699229032 0.1660998017 0.1315000951 - 0.1749854982 0.1709350944 0.1356704980 - 0.1799377054 0.1756564975 0.1397946030 - 0.1847759038 0.1802780032 0.1438630074 - 0.1895146966 0.1848101020 0.1478590965 - 0.1941531003 0.1892485023 0.1517767012 - 0.1987002045 0.1936012954 0.1556075960 - 0.2031593025 0.1978708953 0.1593558043 - 0.2075425982 0.2020536959 0.1630499065 - 0.2118435949 0.2061593980 0.1667018980 - 0.2160740942 0.2101961970 0.1703169942 - 0.2202394009 0.2141588926 0.1739034057 - 0.2243397981 0.2180612981 0.1774580032 - 0.2283758074 0.2218959928 0.1809788048 - 0.2323485017 0.2256778032 0.1844571978 - 0.2362571955 0.2294041961 0.1878990978 - 0.2401061058 0.2330839932 0.1913042963 - 0.2438924015 0.2367096990 0.1946662962 - 0.2476287931 0.2402735949 0.1979884058 - 0.2513082922 0.2437945008 0.2012688965 - 0.2549471855 0.2472652048 0.2045135945 - 0.2585380971 0.2506807148 0.2077264041 - 0.2620711029 0.2540544868 0.2109075934 - 0.2655648887 0.2573910058 0.2140623927 - 0.2690165043 0.2606757879 0.2171906978 - 0.2724193931 0.2639169097 0.2202925980 - 0.2757751048 0.2671265006 0.2233645022 - 0.2791010141 0.2702954113 0.2264122963 - 0.2823888063 0.2734270096 0.2294276059 - 0.2856281996 0.2765175104 0.2324116975 - 0.2888259888 0.2795749009 0.2353696972 - 0.2919923067 0.2826111019 0.2383048981 - 0.2951227129 0.2856203914 0.2412146926 - 0.2982215881 0.2885940969 0.2441015989 - 0.3012860119 0.2915267050 0.2469657958 - 0.3043090105 0.2944279015 0.2498013973 - 0.3073101938 0.2973093092 0.2526110113 - 0.3102934957 0.3001666963 0.2553977966 - 0.3132512867 0.3029935062 0.2581652105 - 0.3161765933 0.3057951033 0.2609097958 - 0.3190675080 0.3085728884 0.2636330128 - 0.3219313025 0.3113223910 0.2663359046 - 0.3247652054 0.3140394092 0.2690128982 - 0.3275650144 0.3167183995 0.2716695964 - 0.3303397894 0.3193723857 0.2743076086 - 0.3330946863 0.3220146000 0.2769277096 - 0.3358342946 0.3246417940 0.2795312107 - 0.3385555148 0.3272505105 0.2821188867 - 0.3412531912 0.3298355937 0.2846879959 - 0.3439249098 0.3323946893 0.2872357965 - 0.3465682864 0.3349305987 0.2897650003 - 0.3491815031 0.3374486864 0.2922779918 - 0.3517724872 0.3399476111 0.2947722971 - 0.3543426991 0.3424271047 0.2972482145 - 0.3568890095 0.3448829055 0.2997061908 - 0.3594169915 0.3473187089 0.3021450043 - 0.3619238138 0.3497349918 0.3045690060 - 0.3644065857 0.3521293998 0.3069755137 - 0.3668681085 0.3545056880 0.3093619943 - 0.3693178892 0.3568738103 0.3117316961 - 0.3717587888 0.3592340052 0.3140853941 - 0.3741885126 0.3615838885 0.3164219856 - 0.3766059875 0.3639225960 0.3187406957 - 0.3790099025 0.3662486970 0.3210408986 - 0.3813976049 0.3685598075 0.3233255148 - 0.3837701082 0.3708567023 0.3255949020 - 0.3861232102 0.3731352985 0.3278453052 - 0.3884578049 0.3753966093 0.3300752938 - 0.3907732964 0.3776400983 0.3322862089 - 0.3930662870 0.3798610866 0.3344799876 - 0.3953461945 0.3820647001 0.3366613984 - 0.3976148069 0.3842560053 0.3388288021 - 0.3998697102 0.3864341080 0.3409801126 - 0.4021120965 0.3886001110 0.3431150019 - 0.4043419063 0.3907540143 0.3452382982 - 0.4065585136 0.3928948939 0.3473502100 - 0.4087597132 0.3950211108 0.3494505882 - 0.4109471142 0.3971337974 0.3515371084 - 0.4131202102 0.3992326856 0.3536098003 - 0.4152786136 0.4013175070 0.3556677103 - 0.4174210131 0.4033867121 0.3577109873 - 0.4195466042 0.4054397941 0.3597368896 - 0.4216561913 0.4074774981 0.3617466092 - 0.4237481952 0.4094994962 0.3637399971 - 0.4258198142 0.4115054905 0.3657152951 - 0.4278750122 0.4134953022 0.3676727116 - 0.4299133122 0.4154728055 0.3696179092 - 0.4319362044 0.4174365997 0.3715522885 - 0.4339421093 0.4193887115 0.3734749854 - 0.4359337986 0.4213280082 0.3753845096 - 0.4379098117 0.4232569933 0.3772805035 - 0.4398722053 0.4251773953 0.3791654110 - 0.4418199062 0.4270893037 0.3810392916 - 0.4437533915 0.4289928973 0.3829011917 - 0.4456734061 0.4308882058 0.3847497106 - 0.4475804865 0.4327754080 0.3865840137 - 0.4494752884 0.4346545041 0.3884027004 - 0.4513584077 0.4365256131 0.3902072906 - 0.4532304108 0.4383887947 0.3919999897 - 0.4550901055 0.4402442873 0.3937804997 - 0.4569362104 0.4420920908 0.3955484033 - 0.4587698877 0.4439322948 0.3973042071 - 0.4605911970 0.4457649887 0.3990490139 - 0.4624013007 0.4475904107 0.4007813931 - 0.4641987979 0.4494084120 0.4025042057 - 0.4659827948 0.4512192011 0.4042207897 - 0.4677540958 0.4530228972 0.4059314132 - 0.4695135057 0.4548195899 0.4076353014 - 0.4712609053 0.4566093981 0.4093317986 - 0.4729954898 0.4583922923 0.4110203981 - 0.4747171104 0.4601685107 0.4127005041 - 0.4764277041 0.4619381130 0.4143716097 - 0.4781259000 0.4637005925 0.4160360992 - 0.4798142016 0.4654561877 0.4176951051 - 0.4814904928 0.4672054946 0.4193486869 - 0.4831553996 0.4689483941 0.4209963977 - 0.4848085046 0.4706839025 0.4226379097 - 0.4864495993 0.4724108875 0.4242731035 - 0.4880791903 0.4741292000 0.4259015024 - 0.4896988869 0.4758399129 0.4275228083 - 0.4913074970 0.4775409997 0.4291365147 - 0.4929072857 0.4792341888 0.4307413101 - 0.4944972992 0.4809193015 0.4323380888 - 0.4960778058 0.4825971127 0.4339267015 - 0.4976491034 0.4842678010 0.4355069101 - 0.4992105067 0.4859308898 0.4370785952 - 0.5007578135 0.4875870049 0.4386403859 - 0.5022947788 0.4892325103 0.4401915073 - 0.5038217902 0.4908691049 0.4417355955 - 0.5053405166 0.4924972057 0.4432739019 - 0.5068516731 0.4941171110 0.4448068142 - 0.5083519220 0.4957279861 0.4463337958 - 0.5098429918 0.4973297119 0.4478540123 - 0.5113254189 0.4989241958 0.4493685067 - 0.5127993822 0.5005111098 0.4508773983 - 0.5142660141 0.5020899177 0.4523806870 - 0.5157235265 0.5036565065 0.4538762867 - 0.5171709061 0.5052154064 0.4553655088 - 0.5186092257 0.5067667961 0.4568488002 - 0.5200390816 0.5083103776 0.4583263099 - 0.5214567780 0.5098450780 0.4597958028 - 0.5228651762 0.5113719106 0.4612582922 - 0.5242664218 0.5128924847 0.4627147019 - 0.5256614089 0.5144073963 0.4641650915 - 0.5270467997 0.5159106255 0.4656063020 - 0.5284280181 0.5174064040 0.4670408070 - 0.5298041105 0.5188953876 0.4684692025 - 0.5311722159 0.5203765035 0.4698903859 - 0.5325285792 0.5218489766 0.4713025093 - 0.5338789225 0.5233148932 0.4727083147 - 0.5352239013 0.5247743726 0.4741083086 - 0.5365588069 0.5262218714 0.4755015075 - 0.5378903151 0.5276635885 0.4768893123 - 0.5392178297 0.5290997028 0.4782724977 - 0.5405359268 0.5305265784 0.4796496034 - 0.5418452024 0.5319476128 0.4810225964 - 0.5431485176 0.5333635211 0.4823926091 - 0.5444443226 0.5347704887 0.4837583005 - 0.5457333922 0.5361695886 0.4851197004 - 0.5470187068 0.5375636816 0.4864782095 - 0.5482994914 0.5389505029 0.4878326058 - 0.5495707989 0.5403308272 0.4891827106 - 0.5508363843 0.5417066813 0.4905301034 - 0.5520933270 0.5430750251 0.4918732047 - 0.5533440113 0.5444362164 0.4932124019 - 0.5545908809 0.5457931161 0.4945487976 - 0.5558316112 0.5471432805 0.4958792031 - 0.5570693016 0.5484884977 0.4972046018 - 0.5583047271 0.5498301983 0.4985263944 - 0.5595293045 0.5511658192 0.4998416901 - 0.5607494712 0.5524978042 0.5011532903 - 0.5619639754 0.5538250208 0.5024604797 - 0.5631700754 0.5551431775 0.5037620068 - 0.5643717051 0.5564572215 0.5050604939 - 0.5655639768 0.5577635169 0.5063533187 - 0.5667486787 0.5590627193 0.5076419711 - 0.5679283738 0.5603570938 0.5089282990 - 0.5691004992 0.5616438985 0.5102069974 - 0.5702697039 0.5629271865 0.5114836097 - 0.5714333057 0.5642039180 0.5127562284 - 0.5725921988 0.5654767156 0.5140225887 - 0.5737485886 0.5667474866 0.5152860284 - 0.5748950243 0.5680088997 0.5165405869 - 0.5760377049 0.5692681074 0.5177925825 - 0.5771713853 0.5705221891 0.5190386772 - 0.5783004761 0.5717713237 0.5202798247 - 0.5794264078 0.5730165839 0.5215175748 - 0.5805451870 0.5742524266 0.5227484703 - 0.5816622972 0.5754857063 0.5239772797 - 0.5827761292 0.5767114758 0.5251995921 - 0.5838869214 0.5779340267 0.5264189839 - 0.5849927068 0.5791509748 0.5276340246 - 0.5860930085 0.5803642869 0.5288445950 - 0.5871897936 0.5815749764 0.5300526023 - 0.5882819295 0.5827775002 0.5312535167 - 0.5893725753 0.5839778781 0.5324525833 - 0.5904521942 0.5851687789 0.5336431861 - 0.5915297270 0.5863571763 0.5348318219 - 0.5925999880 0.5875378251 0.5360146761 - 0.5936675072 0.5887145996 0.5371940136 - 0.5947307944 0.5898860097 0.5383685827 - 0.5957894921 0.5910528898 0.5395389199 - 0.5968446136 0.5922164917 0.5407065153 - 0.5978962779 0.5933740139 0.5418683290 - 0.5989465714 0.5945292115 0.5430281758 - 0.5999879837 0.5956771970 0.5441824794 - 0.6010277867 0.5968238711 0.5453361273 - 0.6020594835 0.5979635715 0.5464810133 - 0.6030905247 0.5991023779 0.5476251245 - 0.6041157842 0.6002333760 0.5487617254 - 0.6051399112 0.6013631821 0.5498968959 - 0.6061576009 0.6024864912 0.5510259271 - 0.6071736217 0.6036077142 0.5521529913 - 0.6081838012 0.6047217250 0.5532745719 - 0.6091915965 0.6058334112 0.5543937087 - 0.6101928949 0.6069393754 0.5555071235 - 0.6111915708 0.6080427766 0.5566176176 - 0.6121845841 0.6091406941 0.5577220917 - 0.6131750941 0.6102350950 0.5588241220 - 0.6141607165 0.6113222241 0.5599215031 - 0.6151443720 0.6124070883 0.5610160828 - 0.6161242723 0.6134874821 0.5621057153 - 0.6171023250 0.6145648956 0.5631932020 - 0.6180763841 0.6156355739 0.5642765760 - 0.6190484166 0.6167044044 0.5653579831 - 0.6200155020 0.6177682281 0.5664346814 - 0.6209805012 0.6188302040 0.5675101280 - 0.6219391823 0.6198859811 0.5685818195 - 0.6228963733 0.6209400296 0.5696520209 - 0.6238467097 0.6219862103 0.5707165003 - 0.6247966886 0.6230316758 0.5717800856 - 0.6257433295 0.6240708828 0.5728380084 - 0.6266897917 0.6251100898 0.5738958120 - 0.6276292801 0.6261410713 0.5749465227 - 0.6285687089 0.6271719933 0.5759971142 - 0.6294990182 0.6281968951 0.5770409107 - 0.6304280162 0.6292210221 0.5780838728 - 0.6313526034 0.6302379966 0.5791224837 - 0.6322757006 0.6312527061 0.5801597238 - 0.6331943870 0.6322625875 0.5811929703 - 0.6341099739 0.6332694292 0.5822238922 - 0.6350240111 0.6342734098 0.5832517147 - 0.6359356046 0.6352738142 0.5842757225 - 0.6368461251 0.6362721920 0.5852975249 - 0.6377531290 0.6372647285 0.5863136053 - 0.6386598945 0.6382569075 0.5873292089 - 0.6395592093 0.6392408013 0.5883370042 - 0.6404585242 0.6402246952 0.5893447995 - 0.6413550973 0.6412026286 0.5903472900 - 0.6422510147 0.6421790719 0.5913487077 - 0.6431431770 0.6431517005 0.5923461914 - 0.6440324783 0.6441212296 0.5933409929 - 0.6449189186 0.6450884938 0.5943337083 - 0.6457985044 0.6460511088 0.5953221917 - 0.6466779113 0.6470134854 0.5963103771 - 0.6475521922 0.6479676962 0.5972914100 - 0.6484264135 0.6489219069 0.5982723832 - 0.6492943764 0.6498695016 0.5992498994 - 0.6501600742 0.6508147120 0.6002262235 - 0.6510232091 0.6517578959 0.6011999249 - 0.6518825889 0.6526980996 0.6021702886 - 0.6527417898 0.6536375880 0.6031401157 - 0.6535974145 0.6545693278 0.6041048169 - 0.6544529200 0.6555008888 0.6050693989 - 0.6553012729 0.6564255953 0.6060274839 - 0.6561468244 0.6573476791 0.6069834232 - 0.6569896936 0.6582674980 0.6079360247 - 0.6578274965 0.6591829062 0.6088829041 - 0.6586652994 0.6600983739 0.6098297834 - 0.6594979167 0.6610056758 0.6107699275 - 0.6603302956 0.6619125009 0.6117097735 - 0.6611601710 0.6628168225 0.6126456857 - 0.6619879007 0.6637189984 0.6135783195 - 0.6628152728 0.6646203995 0.6145102978 - 0.6636397243 0.6655144095 0.6154376268 - 0.6644641161 0.6664084196 0.6163650155 - 0.6652842164 0.6672983170 0.6172876954 - 0.6661018729 0.6681858897 0.6182081103 - 0.6669185758 0.6690726876 0.6191273928 - 0.6677302122 0.6699557900 0.6200417876 - 0.6685419083 0.6708388925 0.6209561825 - 0.6693503857 0.6717147827 0.6218646765 - 0.6701574922 0.6725872159 0.6227704883 - 0.6709632874 0.6734582186 0.6236749887 - 0.6717622876 0.6743215919 0.6245731711 - 0.6725612283 0.6751849055 0.6254714131 - 0.6733565927 0.6760439873 0.6263654232 - 0.6741496921 0.6769003272 0.6272568703 - 0.6749426126 0.6777564287 0.6281480193 - 0.6757320762 0.6786072850 0.6290338039 - 0.6765215993 0.6794583201 0.6299195290 - 0.6773090959 0.6803076863 0.6308029294 - 0.6780942082 0.6811552048 0.6316838264 - 0.6788793206 0.6820027232 0.6325647235 - 0.6796609163 0.6828439832 0.6334397793 - 0.6804419756 0.6836842895 0.6343142986 - 0.6812220216 0.6845229864 0.6351867914 - 0.6819983721 0.6853569150 0.6360538006 - 0.6827747822 0.6861907244 0.6369209290 - 0.6835479140 0.6870210767 0.6377838254 - 0.6843187809 0.6878491044 0.6386439800 - 0.6850897074 0.6886770129 0.6395043135 - 0.6858559847 0.6894986033 0.6403582096 - 0.6866217852 0.6903195977 0.6412116885 - 0.6873862743 0.6911389828 0.6420640945 - 0.6881462932 0.6919528246 0.6429128051 - 0.6889063120 0.6927667260 0.6437615156 - 0.6896629930 0.6935775876 0.6446070075 - 0.6904165149 0.6943858266 0.6454496980 - 0.6911699772 0.6951940060 0.6462923288 - 0.6919187903 0.6959962845 0.6471291780 - 0.6926661134 0.6967967749 0.6479645967 - 0.6934133768 0.6975972056 0.6487997770 - 0.6941565275 0.6983886957 0.6496288180 - 0.6948996186 0.6991801262 0.6504579186 - 0.6956412792 0.6999704242 0.6512849927 - 0.6963785291 0.7007569075 0.6521068215 - 0.6971156001 0.7015432715 0.6529284716 - 0.6978498101 0.7023270130 0.6537482738 - 0.6985800862 0.7031068802 0.6545658112 - 0.6993104219 0.7038868070 0.6553832293 - 0.7000390291 0.7046630979 0.6561965942 - 0.7007665038 0.7054370046 0.6570075750 - 0.7014939785 0.7062109113 0.6578186154 - 0.7022175789 0.7069805264 0.6586261988 - 0.7029399872 0.7077487707 0.6594328284 - 0.7036622763 0.7085170746 0.6602393985 - 0.7043780088 0.7092791796 0.6610412002 - 0.7050929070 0.7100403905 0.6618425250 - 0.7058076859 0.7108017206 0.6626436710 - 0.7065191269 0.7115598917 0.6634386182 - 0.7072305083 0.7123181224 0.6642333865 - 0.7079415917 0.7130758762 0.6650279164 - 0.7086470723 0.7138280869 0.6658183932 - 0.7093526721 0.7145804167 0.6666089296 - 0.7100573778 0.7153319120 0.6673982143 - 0.7107548714 0.7160776854 0.6681799293 - 0.7114523053 0.7168235183 0.6689617038 - 0.7121492028 0.7175688744 0.6697422266 - 0.7128425837 0.7183113098 0.6705163717 - 0.7135359049 0.7190536857 0.6712905765 - 0.7142282724 0.7197949886 0.6720638275 - 0.7149152756 0.7205299139 0.6728317142 - 0.7156022191 0.7212648988 0.6735996008 - 0.7162883878 0.7219992280 0.6743667722 - 0.7169690132 0.7227286100 0.6751294732 - 0.7176495790 0.7234581113 0.6758921742 - 0.7183300257 0.7241873145 0.6766545177 - 0.7190073729 0.7249132991 0.6774132252 - 0.7196847796 0.7256392837 0.6781718731 - 0.7203623056 0.7263653278 0.6789305210 - 0.7210342288 0.7270826101 0.6796817780 - 0.7217060924 0.7277998924 0.6804329753 - 0.7223780155 0.7285171747 0.6811842918 - 0.7230476737 0.7292283773 0.6819289923 - 0.7237172127 0.7299389243 0.6826732159 - 0.7243866920 0.7306494117 0.6834173203 - 0.7250542045 0.7313563228 0.6841576099 - 0.7257212996 0.7320619822 0.6848968267 - 0.7263882756 0.7327678204 0.6856359839 - 0.7270516157 0.7334702015 0.6863722205 - 0.7277126908 0.7341706157 0.6871067882 - 0.7283738256 0.7348709702 0.6878414154 - 0.7290335298 0.7355691195 0.6885737777 - 0.7296916246 0.7362642884 0.6893038154 - 0.7303497791 0.7369595170 0.6900337934 - 0.7310072184 0.7376533151 0.6907622814 - 0.7316623926 0.7383428216 0.6914864779 - 0.7323176265 0.7390323281 0.6922107935 - 0.7329726815 0.7397217155 0.6929348111 - 0.7336215973 0.7404049039 0.6936529279 - 0.7342703938 0.7410882115 0.6943709254 - 0.7349191904 0.7417714000 0.6950889826 - 0.7355654240 0.7424499989 0.6958044767 - 0.7362108827 0.7431272268 0.6965193748 - 0.7368562818 0.7438042760 0.6972342730 - 0.7375000715 0.7444782853 0.6979461908 - 0.7381421924 0.7451488972 0.6986551881 - 0.7387843132 0.7458193898 0.6993641257 - 0.7394251823 0.7464888096 0.7000715137 - 0.7400605083 0.7471522093 0.7007719278 - 0.7406957746 0.7478156090 0.7014722228 - 0.7413311005 0.7484790087 0.7021725774 - 0.7419633269 0.7491366267 0.7028682828 - 0.7425947785 0.7497931123 0.7035630941 - 0.7432262897 0.7504494786 0.7042579055 - 0.7438560724 0.7511044741 0.7049499154 - 0.7444838881 0.7517579198 0.7056387067 - 0.7451115847 0.7524113059 0.7063274980 - 0.7457391024 0.7530642748 0.7070159912 - 0.7463619113 0.7537124157 0.7077018023 - 0.7469847202 0.7543604970 0.7083876133 - 0.7476075292 0.7550085783 0.7090734243 - 0.7482284904 0.7556517720 0.7097554207 - 0.7488484979 0.7562924027 0.7104355097 - 0.7494685054 0.7569329739 0.7111154795 - 0.7500876784 0.7575728297 0.7117941976 - 0.7507038116 0.7582098246 0.7124679089 - 0.7513198256 0.7588467002 0.7131415009 - 0.7519357800 0.7594835758 0.7138150930 - 0.7525480986 0.7601163983 0.7144855857 - 0.7531589270 0.7607474923 0.7151550055 - 0.7537698150 0.7613785863 0.7158244252 - 0.7543799877 0.7620080113 0.7164921761 - 0.7549877763 0.7626309991 0.7171549201 - 0.7555956244 0.7632541060 0.7178174853 - 0.7562034130 0.7638770938 0.7184802294 - 0.7568084002 0.7644972801 0.7191396952 - 0.7574118972 0.7651162148 0.7197976708 - 0.7580155134 0.7657349706 0.7204558253 - 0.7586185932 0.7663530707 0.7211132050 - 0.7592173815 0.7669656277 0.7217658758 - 0.7598162889 0.7675780058 0.7224187255 - 0.7604150772 0.7681905031 0.7230715156 - 0.7610118985 0.7688006759 0.7237222195 - 0.7616066933 0.7694088817 0.7243710160 - 0.7622014880 0.7700170875 0.7250198722 - 0.7627962828 0.7706251740 0.7256687284 - 0.7633875012 0.7712314725 0.7263153195 - 0.7639781237 0.7718374133 0.7269614935 - 0.7645688057 0.7724434137 0.7276077867 - 0.7651584148 0.7730479240 0.7282524109 - 0.7657443881 0.7736473083 0.7288914919 - 0.7663304806 0.7742465734 0.7295305729 - 0.7669165730 0.7748460174 0.7301695943 - 0.7675005794 0.7754428983 0.7308055162 - 0.7680826783 0.7760378122 0.7314386964 - 0.7686648965 0.7766327262 0.7320718765 - 0.7692471147 0.7772275209 0.7327051759 - 0.7698258758 0.7778202891 0.7333346009 - 0.7704041004 0.7784126997 0.7339633703 - 0.7709822059 0.7790051103 0.7345920801 - 0.7715600729 0.7795969248 0.7352203727 - 0.7721338272 0.7801824808 0.7358438969 - 0.7727075219 0.7807679772 0.7364673018 - 0.7732812762 0.7813535929 0.7370908260 - 0.7738538980 0.7819374800 0.7377125025 - 0.7744238973 0.7825171947 0.7383304834 - 0.7749938965 0.7830970287 0.7389485836 - 0.7755638957 0.7836768031 0.7395666242 - 0.7761321068 0.7842543721 0.7401825190 - 0.7766981125 0.7848296165 0.7407962084 - 0.7772641778 0.7854048014 0.7414098978 - 0.7778303027 0.7859801054 0.7420237064 - 0.7783936262 0.7865508795 0.7426350117 - 0.7789554000 0.7871193290 0.7432450056 - 0.7795171738 0.7876875997 0.7438551188 - 0.7800790071 0.7882559896 0.7444651127 - 0.7806380987 0.7888197899 0.7450724840 - 0.7811962962 0.7893822789 0.7456791997 - 0.7817544937 0.7899448276 0.7462859154 - 0.7823126912 0.7905073166 0.7468925714 - 0.7828676105 0.7910656929 0.7474957108 - 0.7834218740 0.7916235924 0.7480981946 - 0.7839761972 0.7921813726 0.7487007976 - 0.7845305204 0.7927392125 0.7493032813 - 0.7850810289 0.7932925820 0.7499018908 - 0.7856311202 0.7938457131 0.7505000830 - 0.7861812711 0.7943987250 0.7510983944 - 0.7867314219 0.7949517965 0.7516967058 - 0.7872781754 0.7955003977 0.7522919178 - 0.7878248096 0.7960488200 0.7528868914 - 0.7883713841 0.7965971828 0.7534819841 - 0.7889180183 0.7971456051 0.7540770769 - 0.7894610167 0.7976896763 0.7546678185 - 0.7900037169 0.7982336283 0.7552582026 - 0.7905464172 0.7987774014 0.7558485866 - 0.7910891175 0.7993212938 0.7564390898 - 0.7916281223 0.7998626232 0.7570242286 - 0.7921667099 0.8004035950 0.7576088905 - 0.7927052975 0.8009446263 0.7581936121 - 0.7932438850 0.8014857173 0.7587782741 - 0.7937796116 0.8020228744 0.7593606710 - 0.7943146825 0.8025593758 0.7599425912 - 0.7948498130 0.8030958772 0.7605245113 - 0.7953848839 0.8036323190 0.7611063719 - 0.7959170938 0.8041642904 0.7616853714 - 0.7964482903 0.8046945930 0.7622634172 - 0.7969794869 0.8052248955 0.7628415227 - 0.7975106239 0.8057551980 0.7634195089 - 0.7980393767 0.8062841296 0.7639942765 - 0.7985665202 0.8068119287 0.7645670176 - 0.7990936041 0.8073397875 0.7651396990 - 0.7996206284 0.8078677058 0.7657123804 - 0.8001462817 0.8083937764 0.7662832737 - 0.8006699085 0.8089175224 0.7668517828 - 0.8011934161 0.8094412088 0.7674202919 - 0.8017169833 0.8099648952 0.7679886818 - 0.8022397757 0.8104875088 0.7685561776 - 0.8027595282 0.8110054135 0.7691202760 - 0.8032791018 0.8115233779 0.7696843743 - 0.8037986755 0.8120412827 0.7702484727 - 0.8043184280 0.8125593066 0.7708125114 - 0.8048341870 0.8130753040 0.7713751793 - 0.8053497076 0.8135911822 0.7719377279 - 0.8058652878 0.8141071200 0.7725002766 - 0.8063809276 0.8146229982 0.7730628848 - 0.8068935871 0.8151363730 0.7736229897 - 0.8074046969 0.8156484962 0.7741820216 - 0.8079158068 0.8161606789 0.7747409940 - 0.8084269762 0.8166728020 0.7753000259 - 0.8089368939 0.8171827197 0.7758569121 - 0.8094444871 0.8176885247 0.7764102221 - 0.8099520802 0.8181943893 0.7769634724 - 0.8104596734 0.8187001944 0.7775169015 - 0.8109673858 0.8192059994 0.7780702114 - 0.8114724159 0.8197085261 0.7786207199 - 0.8119773865 0.8202108741 0.7791712880 - 0.8124824762 0.8207132816 0.7797217965 - 0.8129875064 0.8212158084 0.7802724242 - 0.8134900928 0.8217161894 0.7808198929 - 0.8139910102 0.8222153783 0.7813655734 - 0.8144919276 0.8227146268 0.7819113135 - 0.8149927855 0.8232136965 0.7824569941 - 0.8154932261 0.8237118125 0.7830020189 - 0.8159909248 0.8242052197 0.7835443020 - 0.8164886236 0.8246986270 0.7840867043 - 0.8169863820 0.8251919150 0.7846289873 - 0.8174840808 0.8256853223 0.7851713896 - 0.8179801702 0.8261765242 0.7857123017 - 0.8184756041 0.8266668916 0.7862526774 - 0.8189710975 0.8271573782 0.7867931128 - 0.8194664717 0.8276478052 0.7873336077 - 0.8199610710 0.8281375170 0.7878730297 - 0.8204528093 0.8286252022 0.7884094715 - 0.8209444284 0.8291128278 0.7889459729 - 0.8214361072 0.8296005130 0.7894824743 - 0.8219277859 0.8300880790 0.7900189757 - 0.8224173784 0.8305714130 0.7905547023 - 0.8229061961 0.8310527205 0.7910900116 - 0.8233950138 0.8315340281 0.7916253805 - 0.8238837719 0.8320153952 0.7921606898 - 0.8243722916 0.8324962258 0.7926955819 - 0.8248590827 0.8329733014 0.7932277918 - 0.8253458738 0.8334503770 0.7937598825 - 0.8258327246 0.8339275122 0.7942920923 - 0.8263195157 0.8344045877 0.7948243022 - 0.8268045187 0.8348805904 0.7953537107 - 0.8272879124 0.8353555799 0.7958806753 - 0.8277711868 0.8358306289 0.7964078188 - 0.8282545209 0.8363054991 0.7969347835 - 0.8287379146 0.8367804885 0.7974618077 - 0.8292189837 0.8372507095 0.7979866862 - 0.8296996951 0.8377199769 0.7985112071 - 0.8301805258 0.8381894231 0.7990357876 - 0.8306612968 0.8386586905 0.7995604277 - 0.8311417103 0.8391273022 0.8000841737 - 0.8316208124 0.8395920992 0.8006044030 - 0.8320997953 0.8400568962 0.8011245728 - 0.8325788975 0.8405218124 0.8016448021 - 0.8330578804 0.8409866095 0.8021649718 - 0.8335351944 0.8414500952 0.8026837707 - 0.8340101242 0.8419117928 0.8032010794 - 0.8344849944 0.8423734903 0.8037183285 - 0.8349598050 0.8428351879 0.8042355180 - 0.8354346752 0.8432968855 0.8047528267 - 0.8359069824 0.8437553048 0.8052688837 - 0.8363779783 0.8442121744 0.8057845831 - 0.8368489742 0.8446689844 0.8063002229 - 0.8373200297 0.8451257944 0.8068159223 - 0.8377910256 0.8455827236 0.8073315024 - 0.8382608294 0.8460361958 0.8078433275 - 0.8387305140 0.8464891911 0.8083544970 - 0.8392001987 0.8469423056 0.8088657260 - 0.8396698833 0.8473953009 0.8093768954 - 0.8401396275 0.8478482962 0.8098881245 - 0.8406054974 0.8482989073 0.8103969097 - 0.8410714865 0.8487493992 0.8109058142 - 0.8415374756 0.8492000103 0.8114145994 - 0.8420035243 0.8496506214 0.8119235039 - 0.8424689770 0.8501005769 0.8124318719 - 0.8429316282 0.8505470157 0.8129380941 - 0.8433941007 0.8509933949 0.8134443164 - 0.8438566923 0.8514398932 0.8139504194 - 0.8443192244 0.8518862724 0.8144565821 - 0.8447815776 0.8523321152 0.8149620891 - 0.8452432752 0.8527753949 0.8154653907 - 0.8457049727 0.8532186747 0.8159685731 - 0.8461666107 0.8536620140 0.8164718747 - 0.8466283083 0.8541052938 0.8169751167 - 0.8470892906 0.8545482159 0.8174781203 - 0.8475477099 0.8549897075 0.8179804087 - 0.8480061889 0.8554311991 0.8184826970 - 0.8484646082 0.8558726907 0.8189849854 - 0.8489230871 0.8563141823 0.8194872737 - 0.8493804932 0.8567548990 0.8199887276 - 0.8498343825 0.8571929932 0.8204873204 - 0.8502882719 0.8576310277 0.8209857941 - 0.8507422209 0.8580691218 0.8214843869 - 0.8511961102 0.8585072160 0.8219829798 - 0.8516497016 0.8589447141 0.8224810958 - 0.8521021008 0.8593794107 0.8229773045 - 0.8525546193 0.8598141074 0.8234735131 - 0.8530070186 0.8602489233 0.8239697218 - 0.8534594178 0.8606836200 0.8244658709 - 0.8539115787 0.8611180186 0.8249620199 - 0.8543611169 0.8615491986 0.8254572153 - 0.8548105955 0.8619803190 0.8259524107 - 0.8552600741 0.8624114990 0.8264474869 - 0.8557096124 0.8628426194 0.8269426823 - 0.8561590910 0.8632737994 0.8274378777 - 0.8566038013 0.8637009859 0.8279286027 - 0.8570482135 0.8641281128 0.8284192085 - 0.8574925065 0.8645551205 0.8289096951 - 0.8579369187 0.8649821281 0.8294003010 - 0.8583812118 0.8654091954 0.8298907876 - 0.8588234782 0.8658344150 0.8303787708 - 0.8592652082 0.8662589788 0.8308660984 - 0.8597068191 0.8666837215 0.8313534260 - 0.8601484895 0.8671082854 0.8318405747 - 0.8605902195 0.8675329089 0.8323279023 - 0.8610305190 0.8679561019 0.8328140974 - 0.8614696860 0.8683781028 0.8332993984 - 0.8619089127 0.8687999845 0.8337846994 - 0.8623480797 0.8692219853 0.8342700005 - 0.8627873063 0.8696439862 0.8347553015 - 0.8632252216 0.8700649738 0.8352395296 - 0.8636596203 0.8704838157 0.8357210755 - 0.8640940785 0.8709024787 0.8362026215 - 0.8645285964 0.8713212013 0.8366841078 - 0.8649629951 0.8717399836 0.8371655941 - 0.8653975129 0.8721587062 0.8376470804 - 0.8658297062 0.8725745082 0.8381273746 - 0.8662618995 0.8729901910 0.8386076093 - 0.8666940928 0.8734058738 0.8390877843 - 0.8671262264 0.8738214970 0.8395680785 - 0.8675584197 0.8742371798 0.8400483131 - 0.8679893017 0.8746507168 0.8405265808 - 0.8684194088 0.8750627041 0.8410037756 - 0.8688495159 0.8754748106 0.8414809108 - 0.8692796230 0.8758867979 0.8419579864 - 0.8697096705 0.8762987852 0.8424351811 - 0.8701388836 0.8767099977 0.8429118991 - 0.8705646992 0.8771181107 0.8433871865 - 0.8709903955 0.8775262237 0.8438625932 - 0.8714162111 0.8779342771 0.8443378806 - 0.8718420267 0.8783423901 0.8448132277 - 0.8722677827 0.8787505031 0.8452885151 - 0.8726921082 0.8791568875 0.8457635045 - 0.8731160164 0.8795629144 0.8462383151 - 0.8735398054 0.8799688220 0.8467131257 - 0.8739637136 0.8803747892 0.8471878767 - 0.8743875027 0.8807808161 0.8476626873 - 0.8748108745 0.8811860085 0.8481363058 - 0.8752331138 0.8815891743 0.8486071229 - 0.8756552935 0.8819925189 0.8490779996 - 0.8760774732 0.8823956847 0.8495488167 - 0.8764997125 0.8827990294 0.8500195742 - 0.8769218922 0.8832021952 0.8504905105 - 0.8773404956 0.8836026192 0.8509593010 - 0.8777579069 0.8840019703 0.8514273763 - 0.8781753182 0.8844013810 0.8518955112 - 0.8785927296 0.8848007917 0.8523637056 - 0.8790100813 0.8852002025 0.8528317809 - 0.8794270754 0.8855990767 0.8532996774 - 0.8798415065 0.8859958053 0.8537663817 - 0.8802559972 0.8863924146 0.8542332053 - 0.8806704283 0.8867890835 0.8546999097 - 0.8810849190 0.8871858120 0.8551666141 - 0.8814992905 0.8875824213 0.8556333184 - 0.8819131851 0.8879781961 0.8560982943 - 0.8823264837 0.8883730769 0.8565617800 - 0.8827397227 0.8887680769 0.8570253253 - 0.8831530213 0.8891630173 0.8574888110 - 0.8835663199 0.8895580173 0.8579522967 - 0.8839796185 0.8899530172 0.8584157825 - 0.8843892813 0.8903455138 0.8588786721 - 0.8847984076 0.8907375932 0.8593413830 - 0.8852074146 0.8911296725 0.8598040938 - 0.8856164813 0.8915218115 0.8602668047 - 0.8860256076 0.8919138908 0.8607295752 - 0.8864343166 0.8923054934 0.8611919284 - 0.8868409991 0.8926942945 0.8616520762 - 0.8872476220 0.8930829763 0.8621122837 - 0.8876541853 0.8934717774 0.8625724912 - 0.8880609274 0.8938605189 0.8630326986 - 0.8884674907 0.8942492008 0.8634929061 - 0.8888738751 0.8946374059 0.8639521003 - 0.8892799020 0.8950244188 0.8644095063 - 0.8896859288 0.8954114914 0.8648669124 - 0.8900917768 0.8957985044 0.8653243780 - 0.8904978037 0.8961855769 0.8657817841 - 0.8909037709 0.8965725899 0.8662391901 - 0.8913077116 0.8969581127 0.8666961193 - 0.8917099833 0.8973423243 0.8671525121 - 0.8921123147 0.8977264762 0.8676090240 - 0.8925145864 0.8981108069 0.8680654168 - 0.8929169178 0.8984950185 0.8685219288 - 0.8933190703 0.8988791704 0.8689783216 - 0.8937194943 0.8992601037 0.8694334030 - 0.8941190839 0.8996396065 0.8698878884 - 0.8945186138 0.9000191092 0.8703424931 - 0.8949180841 0.9003986120 0.8707969785 - 0.8953176737 0.9007781148 0.8712515235 - 0.8957172036 0.9011576176 0.8717060089 - 0.8961154819 0.9015353918 0.8721609116 - 0.8965135217 0.9019128084 0.8726158142 - 0.8969115019 0.9022902250 0.8730707765 - 0.8973094821 0.9026675820 0.8735256791 - 0.8977075219 0.9030449986 0.8739807010 - 0.8981055021 0.9034224153 0.8744356036 - 0.8985009789 0.9037982821 0.8748894930 - 0.8988959789 0.9041739702 0.8753430843 - 0.8992909193 0.9045495987 0.8757967949 - 0.8996858001 0.9049252868 0.8762505054 - 0.9000808001 0.9053009152 0.8767040968 - 0.9004756808 0.9056766033 0.8771578074 - 0.9008677006 0.9060493112 0.8776090145 - 0.9012590051 0.9064213037 0.8780596852 - 0.9016503096 0.9067932963 0.8785104156 - 0.9020416141 0.9071652889 0.8789610863 - 0.9024329782 0.9075372815 0.8794118166 - 0.9028242826 0.9079092741 0.8798624873 - 0.9032142162 0.9082794189 0.8803126812 - 0.9036036134 0.9086487889 0.8807626963 - 0.9039930105 0.9090182185 0.8812127113 - 0.9043824077 0.9093875885 0.8816627264 - 0.9047718048 0.9097570181 0.8821126819 - 0.9051612020 0.9101263881 0.8825628161 - 0.9055489898 0.9104949236 0.8830112815 - 0.9059358239 0.9108626842 0.8834589720 - 0.9063225985 0.9112305045 0.8839067221 - 0.9067094922 0.9115983844 0.8843544126 - 0.9070963264 0.9119662046 0.8848021030 - 0.9074832201 0.9123340845 0.8852497935 - 0.9078686237 0.9127002954 0.8856955767 - 0.9082522988 0.9130645990 0.8861392736 - 0.9086359739 0.9134290218 0.8865829706 - 0.9090197086 0.9137933254 0.8870267272 - 0.9094033837 0.9141576886 0.8874704242 - 0.9097871184 0.9145219922 0.8879141212 - 0.9101704955 0.9148855805 0.8883572817 - 0.9105526805 0.9152469039 0.8887993097 - 0.9109349847 0.9156082273 0.8892412782 - 0.9113172293 0.9159694910 0.8896833062 - 0.9116994739 0.9163308144 0.8901252747 - 0.9120817184 0.9166920185 0.8905673027 - 0.9124640226 0.9170532823 0.8910092711 - 0.9128441811 0.9174141288 0.8914495111 - 0.9132243991 0.9177749157 0.8918895721 - 0.9136046171 0.9181355834 0.8923298120 - 0.9139847755 0.9184963703 0.8927698731 - 0.9143649936 0.9188570976 0.8932099938 - 0.9147452116 0.9192178845 0.8936501741 - 0.9151231050 0.9195770025 0.8940905929 - 0.9154999256 0.9199352860 0.8945311904 - 0.9158766270 0.9202936888 0.8949717283 - 0.9162533283 0.9206519723 0.8954123259 - 0.9166300893 0.9210103154 0.8958528042 - 0.9170067906 0.9213687181 0.8962934017 - 0.9173828959 0.9217258096 0.8967335224 - 0.9177572727 0.9220798016 0.8971729279 - 0.9181317091 0.9224339128 0.8976122141 - 0.9185060859 0.9227880239 0.8980515003 - 0.9188805223 0.9231420159 0.8984909058 - 0.9192548990 0.9234961271 0.8989301920 - 0.9196293950 0.9238501787 0.8993694782 - 0.9200025201 0.9242026806 0.8998063803 - 0.9203755260 0.9245550036 0.9002429247 - 0.9207484126 0.9249072075 0.9006794095 - 0.9211214185 0.9252594709 0.9011157751 - 0.9214943051 0.9256117940 0.9015523195 - 0.9218673110 0.9259641171 0.9019888043 - 0.9222391248 0.9263157845 0.9024249911 - 0.9226092100 0.9266666770 0.9028608799 - 0.9229792953 0.9270175099 0.9032967091 - 0.9233493209 0.9273684025 0.9037325978 - 0.9237194061 0.9277192950 0.9041684866 - 0.9240894914 0.9280701280 0.9046043158 - 0.9244595766 0.9284210205 0.9050402045 - 0.9248281121 0.9287691116 0.9054753184 - 0.9251965284 0.9291167259 0.9059103727 - 0.9255648255 0.9294642806 0.9063454866 - 0.9259331226 0.9298120141 0.9067806005 - 0.9263014793 0.9301596284 0.9072157145 - 0.9266697764 0.9305071831 0.9076508284 - 0.9270378947 0.9308542013 0.9080855250 - 0.9274052978 0.9311996102 0.9085192084 - 0.9277727008 0.9315450191 0.9089530110 - 0.9281402230 0.9318904281 0.9093868136 - 0.9285076261 0.9322357178 0.9098204970 - 0.9288750291 0.9325811267 0.9102542996 - 0.9292423725 0.9329264760 0.9106879830 - 0.9296076298 0.9332711101 0.9111205935 - 0.9299712777 0.9336152077 0.9115526080 - 0.9303351045 0.9339594245 0.9119845033 - 0.9306988120 0.9343035221 0.9124163985 - 0.9310625792 0.9346476197 0.9128482938 - 0.9314264059 0.9349917769 0.9132803082 - 0.9317901134 0.9353358746 0.9137122035 - 0.9321511984 0.9356771708 0.9141423106 - 0.9325122237 0.9360182285 0.9145721793 - 0.9328730702 0.9363592863 0.9150022268 - 0.9332339764 0.9367004037 0.9154322147 - 0.9335948825 0.9370414019 0.9158620834 - 0.9339559078 0.9373825192 0.9162920713 - 0.9343165159 0.9377229810 0.9167218208 - 0.9346756935 0.9380605817 0.9171506166 - 0.9350348711 0.9383983016 0.9175792933 - 0.9353941083 0.9387360215 0.9180080891 - 0.9357532859 0.9390736222 0.9184368849 - 0.9361125231 0.9394112825 0.9188656211 - 0.9364717007 0.9397490025 0.9192944169 - 0.9368298054 0.9400861263 0.9197227955 - 0.9371858835 0.9404221773 0.9201506972 - 0.9375420213 0.9407582879 0.9205787182 - 0.9378980994 0.9410943985 0.9210066199 - 0.9382542968 0.9414305091 0.9214345813 - 0.9386103749 0.9417666197 0.9218624830 - 0.9389665127 0.9421027899 0.9222903848 - 0.9393215179 0.9424378872 0.9227181077 - 0.9396752715 0.9427719712 0.9231454134 - 0.9400290847 0.9431061745 0.9235727191 - 0.9403830171 0.9434403181 0.9240000844 - 0.9407367706 0.9437745214 0.9244273901 - 0.9410905838 0.9441086054 0.9248546958 - 0.9414445162 0.9444426894 0.9252821207 - 0.9417976141 0.9447755218 0.9257091880 - 0.9421502948 0.9451072812 0.9261360765 - 0.9425029159 0.9454391003 0.9265630841 - 0.9428554773 0.9457709193 0.9269899726 - 0.9432082176 0.9461026788 0.9274169803 - 0.9435607791 0.9464343786 0.9278439283 - 0.9439134002 0.9467661977 0.9282708764 - 0.9442645907 0.9470977187 0.9286978245 - 0.9446147084 0.9474288821 0.9291247725 - 0.9449648261 0.9477601051 0.9295517802 - 0.9453148842 0.9480913281 0.9299787283 - 0.9456650019 0.9484224916 0.9304056764 - 0.9460151196 0.9487537146 0.9308326840 - 0.9463651776 0.9490848780 0.9312595725 - 0.9467138052 0.9494156241 0.9316855073 - 0.9470613003 0.9497458935 0.9321106076 - 0.9474087954 0.9500762224 0.9325357080 - 0.9477561712 0.9504064918 0.9329606891 - 0.9481037259 0.9507368207 0.9333857894 - 0.9484512210 0.9510670900 0.9338108897 - 0.9487985969 0.9513974190 0.9342358708 - 0.9491446018 0.9517257214 0.9346597791 - 0.9494888783 0.9520516992 0.9350821972 - 0.9498332143 0.9523777962 0.9355046749 - 0.9501774907 0.9527037740 0.9359272122 - 0.9505218267 0.9530298114 0.9363496900 - 0.9508661032 0.9533559084 0.9367721081 - 0.9512103200 0.9536818862 0.9371945858 - 0.9515535235 0.9540063143 0.9376161098 - 0.9518942237 0.9543272853 0.9380356073 - 0.9522349238 0.9546483159 0.9384549856 - 0.9525756836 0.9549692869 0.9388744831 - 0.9529163837 0.9552901983 0.9392939806 - 0.9532570839 0.9556112289 0.9397134185 - 0.9535977840 0.9559322000 0.9401329160 - 0.9539381862 0.9562529922 0.9405519962 - 0.9542763233 0.9565721750 0.9409685731 - 0.9546142817 0.9568914175 0.9413852096 - 0.9549522996 0.9572107196 0.9418017864 - 0.9552903771 0.9575299025 0.9422184229 - 0.9556283951 0.9578490853 0.9426349998 - 0.9559664726 0.9581683874 0.9430515766 - 0.9563044906 0.9584875703 0.9434682131 - 0.9566408992 0.9588049054 0.9438832998 - 0.9569768906 0.9591218233 0.9442980289 - 0.9573128819 0.9594386816 0.9447128177 - 0.9576488733 0.9597554803 0.9451274872 - 0.9579848051 0.9600723982 0.9455422759 - 0.9583207965 0.9603893161 0.9459570050 - 0.9586567879 0.9607061744 0.9463717937 - 0.9589915872 0.9610219002 0.9467856288 - 0.9593248963 0.9613364935 0.9471985102 - 0.9596583247 0.9616510868 0.9476113915 - 0.9599916935 0.9619656205 0.9480242729 - 0.9603250027 0.9622802138 0.9484372139 - 0.9606583714 0.9625946879 0.9488500953 - 0.9609917998 0.9629092813 0.9492629170 - 0.9613248706 0.9632236958 0.9496756792 - 0.9616556168 0.9635366201 0.9500876069 - 0.9619864225 0.9638494849 0.9504995942 - 0.9623171091 0.9641624093 0.9509115219 - 0.9626479149 0.9644752741 0.9513233900 - 0.9629786015 0.9647881985 0.9517353177 - 0.9633094072 0.9651011825 0.9521471858 - 0.9636400938 0.9654141068 0.9525591135 - 0.9639695287 0.9657256007 0.9529703259 - 0.9642981291 0.9660363197 0.9533810019 - 0.9646267891 0.9663469791 0.9537916780 - 0.9649553895 0.9666576982 0.9542024136 - 0.9652839899 0.9669682980 0.9546130896 - 0.9656125903 0.9672790170 0.9550238252 - 0.9659413099 0.9675896764 0.9554345012 - 0.9662696123 0.9679000974 0.9558449984 - 0.9665955901 0.9682075977 0.9562532902 - 0.9669216275 0.9685152173 0.9566615820 - 0.9672476053 0.9688227177 0.9570698738 - 0.9675735831 0.9691302180 0.9574782252 - 0.9678996801 0.9694377780 0.9578865170 - 0.9682257175 0.9697452784 0.9582948089 - 0.9685516953 0.9700528979 0.9587031007 - 0.9688763022 0.9703593254 0.9591103792 - 0.9691995978 0.9706646800 0.9595170021 - 0.9695230126 0.9709700942 0.9599235058 - 0.9698463082 0.9712755084 0.9603301287 - 0.9701697230 0.9715809226 0.9607365727 - 0.9704930186 0.9718862772 0.9611431956 - 0.9708163738 0.9721916914 0.9615496993 - 0.9711397290 0.9724971056 0.9619563222 - 0.9714608192 0.9728004932 0.9623609781 - 0.9717814922 0.9731037021 0.9627655149 - 0.9721022844 0.9734067917 0.9631699920 - 0.9724230170 0.9737098813 0.9635745883 - 0.9727438092 0.9740129709 0.9639791250 - 0.9730644822 0.9743161798 0.9643836021 - 0.9733852744 0.9746193290 0.9647880793 - 0.9737054706 0.9749218822 0.9651920795 - 0.9740229845 0.9752221107 0.9655939937 - 0.9743404984 0.9755222797 0.9659960270 - 0.9746580720 0.9758223891 0.9663978815 - 0.9749755859 0.9761226177 0.9667997956 - 0.9752930999 0.9764227271 0.9672017097 - 0.9756106138 0.9767228961 0.9676036239 - 0.9759281278 0.9770230055 0.9680054784 - 0.9762442708 0.9773221016 0.9684066772 - 0.9765583873 0.9776195288 0.9688066840 - 0.9768725038 0.9779168963 0.9692066908 - 0.9771866798 0.9782143235 0.9696066976 - 0.9775007963 0.9785116911 0.9700067043 - 0.9778149128 0.9788091779 0.9704067111 - 0.9781290293 0.9791066051 0.9708067775 - 0.9784432054 0.9794039726 0.9712067842 - 0.9787554741 0.9796999097 0.9716057777 - 0.9790663123 0.9799945951 0.9720041752 - 0.9793770909 0.9802892804 0.9724025130 - 0.9796879888 0.9805839062 0.9728007913 - 0.9799988270 0.9808785915 0.9731991291 - 0.9803097248 0.9811732769 0.9735975266 - 0.9806205034 0.9814680219 0.9739958048 - 0.9809312820 0.9817627072 0.9743940830 - 0.9812394977 0.9820554852 0.9747912884 - 0.9815462828 0.9823471904 0.9751877785 - 0.9818530083 0.9826390147 0.9755843282 - 0.9821597934 0.9829307199 0.9759808183 - 0.9824665785 0.9832224846 0.9763773084 - 0.9827733040 0.9835141897 0.9767737985 - 0.9830800891 0.9838058949 0.9771704078 - 0.9833868742 0.9840977192 0.9775668979 - 0.9836912751 0.9843873978 0.9779617190 - 0.9839947224 0.9846761227 0.9783557057 - 0.9842982292 0.9849647880 0.9787496924 - 0.9846016169 0.9852535129 0.9791436791 - 0.9849050045 0.9855421782 0.9795376062 - 0.9852083921 0.9858309031 0.9799315929 - 0.9855117798 0.9861196280 0.9803255796 - 0.9858152270 0.9864082932 0.9807196259 - 0.9861156940 0.9866940975 0.9811127782 - 0.9864144921 0.9869784117 0.9815055132 - 0.9867134094 0.9872627258 0.9818981886 - 0.9870122075 0.9875469804 0.9822909236 - 0.9873110056 0.9878312945 0.9826835990 - 0.9876099229 0.9881156087 0.9830763936 - 0.9879087210 0.9883999228 0.9834691286 - 0.9882075787 0.9886841774 0.9838618040 - 0.9885028005 0.9889653921 0.9842537045 - 0.9887949228 0.9892442226 0.9846450090 - 0.9890869856 0.9895228744 0.9850361943 - 0.9893791080 0.9898015857 0.9854274988 - 0.9896711111 0.9900802970 0.9858188033 - 0.9899631739 0.9903590083 0.9862099886 - 0.9902552962 0.9906377196 0.9866012931 - 0.9905474186 0.9909163713 0.9869924784 - 0.9908375740 0.9911934733 0.9873834252 - 0.9911243916 0.9914677739 0.9877734184 - 0.9914113283 0.9917420745 0.9881634712 - 0.9916980863 0.9920163751 0.9885535240 - 0.9919849038 0.9922906756 0.9889435768 - 0.9922717810 0.9925649762 0.9893336296 - 0.9925585985 0.9928393960 0.9897236824 - 0.9928454161 0.9931136966 0.9901136756 - 0.9931316972 0.9933875203 0.9905033112 - 0.9934136868 0.9936578870 0.9908893108 - 0.9936957955 0.9939283133 0.9912753105 - 0.9939777851 0.9941986799 0.9916613102 - 0.9942598939 0.9944691062 0.9920473099 - 0.9945418835 0.9947394729 0.9924331903 - 0.9948239923 0.9950098991 0.9928191900 - 0.9951061010 0.9952803850 0.9932051897 - 0.9953880906 0.9955508113 0.9935911894 - 0.9956638217 0.9958161116 0.9939712286 - 0.9959380031 0.9960802794 0.9943498969 - 0.9962123036 0.9963445067 0.9947286248 - 0.9964866042 0.9966086745 0.9951072931 - 0.9967609048 0.9968729019 0.9954860210 - 0.9970350862 0.9971370101 0.9958646894 - 0.9973093867 0.9974011779 0.9962432981 - 0.9975836873 0.9976654053 0.9966220260 - 0.9978551865 0.9979274273 0.9969990849 - 0.9981232882 0.9981864095 0.9973741770 - 0.9983913898 0.9984455109 0.9977493286 - 0.9986594915 0.9987046123 0.9981244206 - 0.9989275932 0.9989637136 0.9984995723 - 0.9991956949 0.9992228150 0.9988747239 - 0.9994637966 0.9994817972 0.9992498159 - 0.9997318983 0.9997408986 0.9996249080 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0176717807 0.0159793403 0.0112875197 + 0.0304853506 0.0281650797 0.0208594296 + 0.0410733782 0.0387531407 0.0291500594 + 0.0505116396 0.0483908802 0.0366822407 + 0.0592069514 0.0573300086 0.0436884314 + 0.0673851594 0.0657815933 0.0503007919 + 0.0751756504 0.0738594905 0.0565973185 + 0.0826697499 0.0815291628 0.0626588985 + 0.0899700671 0.0889572278 0.0685112923 + 0.0971127078 0.0961195603 0.0741866305 + 0.1040614992 0.1030588970 0.0796945021 + 0.1108867973 0.1097960994 0.0850348696 + 0.1175592989 0.1162500009 0.0902234167 + 0.1240769997 0.1224389970 0.0952606276 + 0.1304063946 0.1284389049 0.1001622975 + 0.1365357041 0.1342712045 0.1049377993 + 0.1425068974 0.1399544030 0.1096132994 + 0.1483051926 0.1454842985 0.1141887009 + 0.1539328992 0.1508464068 0.1186479032 + 0.1594130993 0.1560681015 0.1230128035 + 0.1647385061 0.1611503065 0.1272864938 + 0.1699229032 0.1660998017 0.1315000951 + 0.1749854982 0.1709350944 0.1356704980 + 0.1799377054 0.1756564975 0.1397946030 + 0.1847759038 0.1802780032 0.1438630074 + 0.1895146966 0.1848101020 0.1478590965 + 0.1941531003 0.1892485023 0.1517767012 + 0.1987002045 0.1936012954 0.1556075960 + 0.2031593025 0.1978708953 0.1593558043 + 0.2075425982 0.2020536959 0.1630499065 + 0.2118435949 0.2061593980 0.1667018980 + 0.2160740942 0.2101961970 0.1703169942 + 0.2202394009 0.2141588926 0.1739034057 + 0.2243397981 0.2180612981 0.1774580032 + 0.2283758074 0.2218959928 0.1809788048 + 0.2323485017 0.2256778032 0.1844571978 + 0.2362571955 0.2294041961 0.1878990978 + 0.2401061058 0.2330839932 0.1913042963 + 0.2438924015 0.2367096990 0.1946662962 + 0.2476287931 0.2402735949 0.1979884058 + 0.2513082922 0.2437945008 0.2012688965 + 0.2549471855 0.2472652048 0.2045135945 + 0.2585380971 0.2506807148 0.2077264041 + 0.2620711029 0.2540544868 0.2109075934 + 0.2655648887 0.2573910058 0.2140623927 + 0.2690165043 0.2606757879 0.2171906978 + 0.2724193931 0.2639169097 0.2202925980 + 0.2757751048 0.2671265006 0.2233645022 + 0.2791010141 0.2702954113 0.2264122963 + 0.2823888063 0.2734270096 0.2294276059 + 0.2856281996 0.2765175104 0.2324116975 + 0.2888259888 0.2795749009 0.2353696972 + 0.2919923067 0.2826111019 0.2383048981 + 0.2951227129 0.2856203914 0.2412146926 + 0.2982215881 0.2885940969 0.2441015989 + 0.3012860119 0.2915267050 0.2469657958 + 0.3043090105 0.2944279015 0.2498013973 + 0.3073101938 0.2973093092 0.2526110113 + 0.3102934957 0.3001666963 0.2553977966 + 0.3132512867 0.3029935062 0.2581652105 + 0.3161765933 0.3057951033 0.2609097958 + 0.3190675080 0.3085728884 0.2636330128 + 0.3219313025 0.3113223910 0.2663359046 + 0.3247652054 0.3140394092 0.2690128982 + 0.3275650144 0.3167183995 0.2716695964 + 0.3303397894 0.3193723857 0.2743076086 + 0.3330946863 0.3220146000 0.2769277096 + 0.3358342946 0.3246417940 0.2795312107 + 0.3385555148 0.3272505105 0.2821188867 + 0.3412531912 0.3298355937 0.2846879959 + 0.3439249098 0.3323946893 0.2872357965 + 0.3465682864 0.3349305987 0.2897650003 + 0.3491815031 0.3374486864 0.2922779918 + 0.3517724872 0.3399476111 0.2947722971 + 0.3543426991 0.3424271047 0.2972482145 + 0.3568890095 0.3448829055 0.2997061908 + 0.3594169915 0.3473187089 0.3021450043 + 0.3619238138 0.3497349918 0.3045690060 + 0.3644065857 0.3521293998 0.3069755137 + 0.3668681085 0.3545056880 0.3093619943 + 0.3693178892 0.3568738103 0.3117316961 + 0.3717587888 0.3592340052 0.3140853941 + 0.3741885126 0.3615838885 0.3164219856 + 0.3766059875 0.3639225960 0.3187406957 + 0.3790099025 0.3662486970 0.3210408986 + 0.3813976049 0.3685598075 0.3233255148 + 0.3837701082 0.3708567023 0.3255949020 + 0.3861232102 0.3731352985 0.3278453052 + 0.3884578049 0.3753966093 0.3300752938 + 0.3907732964 0.3776400983 0.3322862089 + 0.3930662870 0.3798610866 0.3344799876 + 0.3953461945 0.3820647001 0.3366613984 + 0.3976148069 0.3842560053 0.3388288021 + 0.3998697102 0.3864341080 0.3409801126 + 0.4021120965 0.3886001110 0.3431150019 + 0.4043419063 0.3907540143 0.3452382982 + 0.4065585136 0.3928948939 0.3473502100 + 0.4087597132 0.3950211108 0.3494505882 + 0.4109471142 0.3971337974 0.3515371084 + 0.4131202102 0.3992326856 0.3536098003 + 0.4152786136 0.4013175070 0.3556677103 + 0.4174210131 0.4033867121 0.3577109873 + 0.4195466042 0.4054397941 0.3597368896 + 0.4216561913 0.4074774981 0.3617466092 + 0.4237481952 0.4094994962 0.3637399971 + 0.4258198142 0.4115054905 0.3657152951 + 0.4278750122 0.4134953022 0.3676727116 + 0.4299133122 0.4154728055 0.3696179092 + 0.4319362044 0.4174365997 0.3715522885 + 0.4339421093 0.4193887115 0.3734749854 + 0.4359337986 0.4213280082 0.3753845096 + 0.4379098117 0.4232569933 0.3772805035 + 0.4398722053 0.4251773953 0.3791654110 + 0.4418199062 0.4270893037 0.3810392916 + 0.4437533915 0.4289928973 0.3829011917 + 0.4456734061 0.4308882058 0.3847497106 + 0.4475804865 0.4327754080 0.3865840137 + 0.4494752884 0.4346545041 0.3884027004 + 0.4513584077 0.4365256131 0.3902072906 + 0.4532304108 0.4383887947 0.3919999897 + 0.4550901055 0.4402442873 0.3937804997 + 0.4569362104 0.4420920908 0.3955484033 + 0.4587698877 0.4439322948 0.3973042071 + 0.4605911970 0.4457649887 0.3990490139 + 0.4624013007 0.4475904107 0.4007813931 + 0.4641987979 0.4494084120 0.4025042057 + 0.4659827948 0.4512192011 0.4042207897 + 0.4677540958 0.4530228972 0.4059314132 + 0.4695135057 0.4548195899 0.4076353014 + 0.4712609053 0.4566093981 0.4093317986 + 0.4729954898 0.4583922923 0.4110203981 + 0.4747171104 0.4601685107 0.4127005041 + 0.4764277041 0.4619381130 0.4143716097 + 0.4781259000 0.4637005925 0.4160360992 + 0.4798142016 0.4654561877 0.4176951051 + 0.4814904928 0.4672054946 0.4193486869 + 0.4831553996 0.4689483941 0.4209963977 + 0.4848085046 0.4706839025 0.4226379097 + 0.4864495993 0.4724108875 0.4242731035 + 0.4880791903 0.4741292000 0.4259015024 + 0.4896988869 0.4758399129 0.4275228083 + 0.4913074970 0.4775409997 0.4291365147 + 0.4929072857 0.4792341888 0.4307413101 + 0.4944972992 0.4809193015 0.4323380888 + 0.4960778058 0.4825971127 0.4339267015 + 0.4976491034 0.4842678010 0.4355069101 + 0.4992105067 0.4859308898 0.4370785952 + 0.5007578135 0.4875870049 0.4386403859 + 0.5022947788 0.4892325103 0.4401915073 + 0.5038217902 0.4908691049 0.4417355955 + 0.5053405166 0.4924972057 0.4432739019 + 0.5068516731 0.4941171110 0.4448068142 + 0.5083519220 0.4957279861 0.4463337958 + 0.5098429918 0.4973297119 0.4478540123 + 0.5113254189 0.4989241958 0.4493685067 + 0.5127993822 0.5005111098 0.4508773983 + 0.5142660141 0.5020899177 0.4523806870 + 0.5157235265 0.5036565065 0.4538762867 + 0.5171709061 0.5052154064 0.4553655088 + 0.5186092257 0.5067667961 0.4568488002 + 0.5200390816 0.5083103776 0.4583263099 + 0.5214567780 0.5098450780 0.4597958028 + 0.5228651762 0.5113719106 0.4612582922 + 0.5242664218 0.5128924847 0.4627147019 + 0.5256614089 0.5144073963 0.4641650915 + 0.5270467997 0.5159106255 0.4656063020 + 0.5284280181 0.5174064040 0.4670408070 + 0.5298041105 0.5188953876 0.4684692025 + 0.5311722159 0.5203765035 0.4698903859 + 0.5325285792 0.5218489766 0.4713025093 + 0.5338789225 0.5233148932 0.4727083147 + 0.5352239013 0.5247743726 0.4741083086 + 0.5365588069 0.5262218714 0.4755015075 + 0.5378903151 0.5276635885 0.4768893123 + 0.5392178297 0.5290997028 0.4782724977 + 0.5405359268 0.5305265784 0.4796496034 + 0.5418452024 0.5319476128 0.4810225964 + 0.5431485176 0.5333635211 0.4823926091 + 0.5444443226 0.5347704887 0.4837583005 + 0.5457333922 0.5361695886 0.4851197004 + 0.5470187068 0.5375636816 0.4864782095 + 0.5482994914 0.5389505029 0.4878326058 + 0.5495707989 0.5403308272 0.4891827106 + 0.5508363843 0.5417066813 0.4905301034 + 0.5520933270 0.5430750251 0.4918732047 + 0.5533440113 0.5444362164 0.4932124019 + 0.5545908809 0.5457931161 0.4945487976 + 0.5558316112 0.5471432805 0.4958792031 + 0.5570693016 0.5484884977 0.4972046018 + 0.5583047271 0.5498301983 0.4985263944 + 0.5595293045 0.5511658192 0.4998416901 + 0.5607494712 0.5524978042 0.5011532903 + 0.5619639754 0.5538250208 0.5024604797 + 0.5631700754 0.5551431775 0.5037620068 + 0.5643717051 0.5564572215 0.5050604939 + 0.5655639768 0.5577635169 0.5063533187 + 0.5667486787 0.5590627193 0.5076419711 + 0.5679283738 0.5603570938 0.5089282990 + 0.5691004992 0.5616438985 0.5102069974 + 0.5702697039 0.5629271865 0.5114836097 + 0.5714333057 0.5642039180 0.5127562284 + 0.5725921988 0.5654767156 0.5140225887 + 0.5737485886 0.5667474866 0.5152860284 + 0.5748950243 0.5680088997 0.5165405869 + 0.5760377049 0.5692681074 0.5177925825 + 0.5771713853 0.5705221891 0.5190386772 + 0.5783004761 0.5717713237 0.5202798247 + 0.5794264078 0.5730165839 0.5215175748 + 0.5805451870 0.5742524266 0.5227484703 + 0.5816622972 0.5754857063 0.5239772797 + 0.5827761292 0.5767114758 0.5251995921 + 0.5838869214 0.5779340267 0.5264189839 + 0.5849927068 0.5791509748 0.5276340246 + 0.5860930085 0.5803642869 0.5288445950 + 0.5871897936 0.5815749764 0.5300526023 + 0.5882819295 0.5827775002 0.5312535167 + 0.5893725753 0.5839778781 0.5324525833 + 0.5904521942 0.5851687789 0.5336431861 + 0.5915297270 0.5863571763 0.5348318219 + 0.5925999880 0.5875378251 0.5360146761 + 0.5936675072 0.5887145996 0.5371940136 + 0.5947307944 0.5898860097 0.5383685827 + 0.5957894921 0.5910528898 0.5395389199 + 0.5968446136 0.5922164917 0.5407065153 + 0.5978962779 0.5933740139 0.5418683290 + 0.5989465714 0.5945292115 0.5430281758 + 0.5999879837 0.5956771970 0.5441824794 + 0.6010277867 0.5968238711 0.5453361273 + 0.6020594835 0.5979635715 0.5464810133 + 0.6030905247 0.5991023779 0.5476251245 + 0.6041157842 0.6002333760 0.5487617254 + 0.6051399112 0.6013631821 0.5498968959 + 0.6061576009 0.6024864912 0.5510259271 + 0.6071736217 0.6036077142 0.5521529913 + 0.6081838012 0.6047217250 0.5532745719 + 0.6091915965 0.6058334112 0.5543937087 + 0.6101928949 0.6069393754 0.5555071235 + 0.6111915708 0.6080427766 0.5566176176 + 0.6121845841 0.6091406941 0.5577220917 + 0.6131750941 0.6102350950 0.5588241220 + 0.6141607165 0.6113222241 0.5599215031 + 0.6151443720 0.6124070883 0.5610160828 + 0.6161242723 0.6134874821 0.5621057153 + 0.6171023250 0.6145648956 0.5631932020 + 0.6180763841 0.6156355739 0.5642765760 + 0.6190484166 0.6167044044 0.5653579831 + 0.6200155020 0.6177682281 0.5664346814 + 0.6209805012 0.6188302040 0.5675101280 + 0.6219391823 0.6198859811 0.5685818195 + 0.6228963733 0.6209400296 0.5696520209 + 0.6238467097 0.6219862103 0.5707165003 + 0.6247966886 0.6230316758 0.5717800856 + 0.6257433295 0.6240708828 0.5728380084 + 0.6266897917 0.6251100898 0.5738958120 + 0.6276292801 0.6261410713 0.5749465227 + 0.6285687089 0.6271719933 0.5759971142 + 0.6294990182 0.6281968951 0.5770409107 + 0.6304280162 0.6292210221 0.5780838728 + 0.6313526034 0.6302379966 0.5791224837 + 0.6322757006 0.6312527061 0.5801597238 + 0.6331943870 0.6322625875 0.5811929703 + 0.6341099739 0.6332694292 0.5822238922 + 0.6350240111 0.6342734098 0.5832517147 + 0.6359356046 0.6352738142 0.5842757225 + 0.6368461251 0.6362721920 0.5852975249 + 0.6377531290 0.6372647285 0.5863136053 + 0.6386598945 0.6382569075 0.5873292089 + 0.6395592093 0.6392408013 0.5883370042 + 0.6404585242 0.6402246952 0.5893447995 + 0.6413550973 0.6412026286 0.5903472900 + 0.6422510147 0.6421790719 0.5913487077 + 0.6431431770 0.6431517005 0.5923461914 + 0.6440324783 0.6441212296 0.5933409929 + 0.6449189186 0.6450884938 0.5943337083 + 0.6457985044 0.6460511088 0.5953221917 + 0.6466779113 0.6470134854 0.5963103771 + 0.6475521922 0.6479676962 0.5972914100 + 0.6484264135 0.6489219069 0.5982723832 + 0.6492943764 0.6498695016 0.5992498994 + 0.6501600742 0.6508147120 0.6002262235 + 0.6510232091 0.6517578959 0.6011999249 + 0.6518825889 0.6526980996 0.6021702886 + 0.6527417898 0.6536375880 0.6031401157 + 0.6535974145 0.6545693278 0.6041048169 + 0.6544529200 0.6555008888 0.6050693989 + 0.6553012729 0.6564255953 0.6060274839 + 0.6561468244 0.6573476791 0.6069834232 + 0.6569896936 0.6582674980 0.6079360247 + 0.6578274965 0.6591829062 0.6088829041 + 0.6586652994 0.6600983739 0.6098297834 + 0.6594979167 0.6610056758 0.6107699275 + 0.6603302956 0.6619125009 0.6117097735 + 0.6611601710 0.6628168225 0.6126456857 + 0.6619879007 0.6637189984 0.6135783195 + 0.6628152728 0.6646203995 0.6145102978 + 0.6636397243 0.6655144095 0.6154376268 + 0.6644641161 0.6664084196 0.6163650155 + 0.6652842164 0.6672983170 0.6172876954 + 0.6661018729 0.6681858897 0.6182081103 + 0.6669185758 0.6690726876 0.6191273928 + 0.6677302122 0.6699557900 0.6200417876 + 0.6685419083 0.6708388925 0.6209561825 + 0.6693503857 0.6717147827 0.6218646765 + 0.6701574922 0.6725872159 0.6227704883 + 0.6709632874 0.6734582186 0.6236749887 + 0.6717622876 0.6743215919 0.6245731711 + 0.6725612283 0.6751849055 0.6254714131 + 0.6733565927 0.6760439873 0.6263654232 + 0.6741496921 0.6769003272 0.6272568703 + 0.6749426126 0.6777564287 0.6281480193 + 0.6757320762 0.6786072850 0.6290338039 + 0.6765215993 0.6794583201 0.6299195290 + 0.6773090959 0.6803076863 0.6308029294 + 0.6780942082 0.6811552048 0.6316838264 + 0.6788793206 0.6820027232 0.6325647235 + 0.6796609163 0.6828439832 0.6334397793 + 0.6804419756 0.6836842895 0.6343142986 + 0.6812220216 0.6845229864 0.6351867914 + 0.6819983721 0.6853569150 0.6360538006 + 0.6827747822 0.6861907244 0.6369209290 + 0.6835479140 0.6870210767 0.6377838254 + 0.6843187809 0.6878491044 0.6386439800 + 0.6850897074 0.6886770129 0.6395043135 + 0.6858559847 0.6894986033 0.6403582096 + 0.6866217852 0.6903195977 0.6412116885 + 0.6873862743 0.6911389828 0.6420640945 + 0.6881462932 0.6919528246 0.6429128051 + 0.6889063120 0.6927667260 0.6437615156 + 0.6896629930 0.6935775876 0.6446070075 + 0.6904165149 0.6943858266 0.6454496980 + 0.6911699772 0.6951940060 0.6462923288 + 0.6919187903 0.6959962845 0.6471291780 + 0.6926661134 0.6967967749 0.6479645967 + 0.6934133768 0.6975972056 0.6487997770 + 0.6941565275 0.6983886957 0.6496288180 + 0.6948996186 0.6991801262 0.6504579186 + 0.6956412792 0.6999704242 0.6512849927 + 0.6963785291 0.7007569075 0.6521068215 + 0.6971156001 0.7015432715 0.6529284716 + 0.6978498101 0.7023270130 0.6537482738 + 0.6985800862 0.7031068802 0.6545658112 + 0.6993104219 0.7038868070 0.6553832293 + 0.7000390291 0.7046630979 0.6561965942 + 0.7007665038 0.7054370046 0.6570075750 + 0.7014939785 0.7062109113 0.6578186154 + 0.7022175789 0.7069805264 0.6586261988 + 0.7029399872 0.7077487707 0.6594328284 + 0.7036622763 0.7085170746 0.6602393985 + 0.7043780088 0.7092791796 0.6610412002 + 0.7050929070 0.7100403905 0.6618425250 + 0.7058076859 0.7108017206 0.6626436710 + 0.7065191269 0.7115598917 0.6634386182 + 0.7072305083 0.7123181224 0.6642333865 + 0.7079415917 0.7130758762 0.6650279164 + 0.7086470723 0.7138280869 0.6658183932 + 0.7093526721 0.7145804167 0.6666089296 + 0.7100573778 0.7153319120 0.6673982143 + 0.7107548714 0.7160776854 0.6681799293 + 0.7114523053 0.7168235183 0.6689617038 + 0.7121492028 0.7175688744 0.6697422266 + 0.7128425837 0.7183113098 0.6705163717 + 0.7135359049 0.7190536857 0.6712905765 + 0.7142282724 0.7197949886 0.6720638275 + 0.7149152756 0.7205299139 0.6728317142 + 0.7156022191 0.7212648988 0.6735996008 + 0.7162883878 0.7219992280 0.6743667722 + 0.7169690132 0.7227286100 0.6751294732 + 0.7176495790 0.7234581113 0.6758921742 + 0.7183300257 0.7241873145 0.6766545177 + 0.7190073729 0.7249132991 0.6774132252 + 0.7196847796 0.7256392837 0.6781718731 + 0.7203623056 0.7263653278 0.6789305210 + 0.7210342288 0.7270826101 0.6796817780 + 0.7217060924 0.7277998924 0.6804329753 + 0.7223780155 0.7285171747 0.6811842918 + 0.7230476737 0.7292283773 0.6819289923 + 0.7237172127 0.7299389243 0.6826732159 + 0.7243866920 0.7306494117 0.6834173203 + 0.7250542045 0.7313563228 0.6841576099 + 0.7257212996 0.7320619822 0.6848968267 + 0.7263882756 0.7327678204 0.6856359839 + 0.7270516157 0.7334702015 0.6863722205 + 0.7277126908 0.7341706157 0.6871067882 + 0.7283738256 0.7348709702 0.6878414154 + 0.7290335298 0.7355691195 0.6885737777 + 0.7296916246 0.7362642884 0.6893038154 + 0.7303497791 0.7369595170 0.6900337934 + 0.7310072184 0.7376533151 0.6907622814 + 0.7316623926 0.7383428216 0.6914864779 + 0.7323176265 0.7390323281 0.6922107935 + 0.7329726815 0.7397217155 0.6929348111 + 0.7336215973 0.7404049039 0.6936529279 + 0.7342703938 0.7410882115 0.6943709254 + 0.7349191904 0.7417714000 0.6950889826 + 0.7355654240 0.7424499989 0.6958044767 + 0.7362108827 0.7431272268 0.6965193748 + 0.7368562818 0.7438042760 0.6972342730 + 0.7375000715 0.7444782853 0.6979461908 + 0.7381421924 0.7451488972 0.6986551881 + 0.7387843132 0.7458193898 0.6993641257 + 0.7394251823 0.7464888096 0.7000715137 + 0.7400605083 0.7471522093 0.7007719278 + 0.7406957746 0.7478156090 0.7014722228 + 0.7413311005 0.7484790087 0.7021725774 + 0.7419633269 0.7491366267 0.7028682828 + 0.7425947785 0.7497931123 0.7035630941 + 0.7432262897 0.7504494786 0.7042579055 + 0.7438560724 0.7511044741 0.7049499154 + 0.7444838881 0.7517579198 0.7056387067 + 0.7451115847 0.7524113059 0.7063274980 + 0.7457391024 0.7530642748 0.7070159912 + 0.7463619113 0.7537124157 0.7077018023 + 0.7469847202 0.7543604970 0.7083876133 + 0.7476075292 0.7550085783 0.7090734243 + 0.7482284904 0.7556517720 0.7097554207 + 0.7488484979 0.7562924027 0.7104355097 + 0.7494685054 0.7569329739 0.7111154795 + 0.7500876784 0.7575728297 0.7117941976 + 0.7507038116 0.7582098246 0.7124679089 + 0.7513198256 0.7588467002 0.7131415009 + 0.7519357800 0.7594835758 0.7138150930 + 0.7525480986 0.7601163983 0.7144855857 + 0.7531589270 0.7607474923 0.7151550055 + 0.7537698150 0.7613785863 0.7158244252 + 0.7543799877 0.7620080113 0.7164921761 + 0.7549877763 0.7626309991 0.7171549201 + 0.7555956244 0.7632541060 0.7178174853 + 0.7562034130 0.7638770938 0.7184802294 + 0.7568084002 0.7644972801 0.7191396952 + 0.7574118972 0.7651162148 0.7197976708 + 0.7580155134 0.7657349706 0.7204558253 + 0.7586185932 0.7663530707 0.7211132050 + 0.7592173815 0.7669656277 0.7217658758 + 0.7598162889 0.7675780058 0.7224187255 + 0.7604150772 0.7681905031 0.7230715156 + 0.7610118985 0.7688006759 0.7237222195 + 0.7616066933 0.7694088817 0.7243710160 + 0.7622014880 0.7700170875 0.7250198722 + 0.7627962828 0.7706251740 0.7256687284 + 0.7633875012 0.7712314725 0.7263153195 + 0.7639781237 0.7718374133 0.7269614935 + 0.7645688057 0.7724434137 0.7276077867 + 0.7651584148 0.7730479240 0.7282524109 + 0.7657443881 0.7736473083 0.7288914919 + 0.7663304806 0.7742465734 0.7295305729 + 0.7669165730 0.7748460174 0.7301695943 + 0.7675005794 0.7754428983 0.7308055162 + 0.7680826783 0.7760378122 0.7314386964 + 0.7686648965 0.7766327262 0.7320718765 + 0.7692471147 0.7772275209 0.7327051759 + 0.7698258758 0.7778202891 0.7333346009 + 0.7704041004 0.7784126997 0.7339633703 + 0.7709822059 0.7790051103 0.7345920801 + 0.7715600729 0.7795969248 0.7352203727 + 0.7721338272 0.7801824808 0.7358438969 + 0.7727075219 0.7807679772 0.7364673018 + 0.7732812762 0.7813535929 0.7370908260 + 0.7738538980 0.7819374800 0.7377125025 + 0.7744238973 0.7825171947 0.7383304834 + 0.7749938965 0.7830970287 0.7389485836 + 0.7755638957 0.7836768031 0.7395666242 + 0.7761321068 0.7842543721 0.7401825190 + 0.7766981125 0.7848296165 0.7407962084 + 0.7772641778 0.7854048014 0.7414098978 + 0.7778303027 0.7859801054 0.7420237064 + 0.7783936262 0.7865508795 0.7426350117 + 0.7789554000 0.7871193290 0.7432450056 + 0.7795171738 0.7876875997 0.7438551188 + 0.7800790071 0.7882559896 0.7444651127 + 0.7806380987 0.7888197899 0.7450724840 + 0.7811962962 0.7893822789 0.7456791997 + 0.7817544937 0.7899448276 0.7462859154 + 0.7823126912 0.7905073166 0.7468925714 + 0.7828676105 0.7910656929 0.7474957108 + 0.7834218740 0.7916235924 0.7480981946 + 0.7839761972 0.7921813726 0.7487007976 + 0.7845305204 0.7927392125 0.7493032813 + 0.7850810289 0.7932925820 0.7499018908 + 0.7856311202 0.7938457131 0.7505000830 + 0.7861812711 0.7943987250 0.7510983944 + 0.7867314219 0.7949517965 0.7516967058 + 0.7872781754 0.7955003977 0.7522919178 + 0.7878248096 0.7960488200 0.7528868914 + 0.7883713841 0.7965971828 0.7534819841 + 0.7889180183 0.7971456051 0.7540770769 + 0.7894610167 0.7976896763 0.7546678185 + 0.7900037169 0.7982336283 0.7552582026 + 0.7905464172 0.7987774014 0.7558485866 + 0.7910891175 0.7993212938 0.7564390898 + 0.7916281223 0.7998626232 0.7570242286 + 0.7921667099 0.8004035950 0.7576088905 + 0.7927052975 0.8009446263 0.7581936121 + 0.7932438850 0.8014857173 0.7587782741 + 0.7937796116 0.8020228744 0.7593606710 + 0.7943146825 0.8025593758 0.7599425912 + 0.7948498130 0.8030958772 0.7605245113 + 0.7953848839 0.8036323190 0.7611063719 + 0.7959170938 0.8041642904 0.7616853714 + 0.7964482903 0.8046945930 0.7622634172 + 0.7969794869 0.8052248955 0.7628415227 + 0.7975106239 0.8057551980 0.7634195089 + 0.7980393767 0.8062841296 0.7639942765 + 0.7985665202 0.8068119287 0.7645670176 + 0.7990936041 0.8073397875 0.7651396990 + 0.7996206284 0.8078677058 0.7657123804 + 0.8001462817 0.8083937764 0.7662832737 + 0.8006699085 0.8089175224 0.7668517828 + 0.8011934161 0.8094412088 0.7674202919 + 0.8017169833 0.8099648952 0.7679886818 + 0.8022397757 0.8104875088 0.7685561776 + 0.8027595282 0.8110054135 0.7691202760 + 0.8032791018 0.8115233779 0.7696843743 + 0.8037986755 0.8120412827 0.7702484727 + 0.8043184280 0.8125593066 0.7708125114 + 0.8048341870 0.8130753040 0.7713751793 + 0.8053497076 0.8135911822 0.7719377279 + 0.8058652878 0.8141071200 0.7725002766 + 0.8063809276 0.8146229982 0.7730628848 + 0.8068935871 0.8151363730 0.7736229897 + 0.8074046969 0.8156484962 0.7741820216 + 0.8079158068 0.8161606789 0.7747409940 + 0.8084269762 0.8166728020 0.7753000259 + 0.8089368939 0.8171827197 0.7758569121 + 0.8094444871 0.8176885247 0.7764102221 + 0.8099520802 0.8181943893 0.7769634724 + 0.8104596734 0.8187001944 0.7775169015 + 0.8109673858 0.8192059994 0.7780702114 + 0.8114724159 0.8197085261 0.7786207199 + 0.8119773865 0.8202108741 0.7791712880 + 0.8124824762 0.8207132816 0.7797217965 + 0.8129875064 0.8212158084 0.7802724242 + 0.8134900928 0.8217161894 0.7808198929 + 0.8139910102 0.8222153783 0.7813655734 + 0.8144919276 0.8227146268 0.7819113135 + 0.8149927855 0.8232136965 0.7824569941 + 0.8154932261 0.8237118125 0.7830020189 + 0.8159909248 0.8242052197 0.7835443020 + 0.8164886236 0.8246986270 0.7840867043 + 0.8169863820 0.8251919150 0.7846289873 + 0.8174840808 0.8256853223 0.7851713896 + 0.8179801702 0.8261765242 0.7857123017 + 0.8184756041 0.8266668916 0.7862526774 + 0.8189710975 0.8271573782 0.7867931128 + 0.8194664717 0.8276478052 0.7873336077 + 0.8199610710 0.8281375170 0.7878730297 + 0.8204528093 0.8286252022 0.7884094715 + 0.8209444284 0.8291128278 0.7889459729 + 0.8214361072 0.8296005130 0.7894824743 + 0.8219277859 0.8300880790 0.7900189757 + 0.8224173784 0.8305714130 0.7905547023 + 0.8229061961 0.8310527205 0.7910900116 + 0.8233950138 0.8315340281 0.7916253805 + 0.8238837719 0.8320153952 0.7921606898 + 0.8243722916 0.8324962258 0.7926955819 + 0.8248590827 0.8329733014 0.7932277918 + 0.8253458738 0.8334503770 0.7937598825 + 0.8258327246 0.8339275122 0.7942920923 + 0.8263195157 0.8344045877 0.7948243022 + 0.8268045187 0.8348805904 0.7953537107 + 0.8272879124 0.8353555799 0.7958806753 + 0.8277711868 0.8358306289 0.7964078188 + 0.8282545209 0.8363054991 0.7969347835 + 0.8287379146 0.8367804885 0.7974618077 + 0.8292189837 0.8372507095 0.7979866862 + 0.8296996951 0.8377199769 0.7985112071 + 0.8301805258 0.8381894231 0.7990357876 + 0.8306612968 0.8386586905 0.7995604277 + 0.8311417103 0.8391273022 0.8000841737 + 0.8316208124 0.8395920992 0.8006044030 + 0.8320997953 0.8400568962 0.8011245728 + 0.8325788975 0.8405218124 0.8016448021 + 0.8330578804 0.8409866095 0.8021649718 + 0.8335351944 0.8414500952 0.8026837707 + 0.8340101242 0.8419117928 0.8032010794 + 0.8344849944 0.8423734903 0.8037183285 + 0.8349598050 0.8428351879 0.8042355180 + 0.8354346752 0.8432968855 0.8047528267 + 0.8359069824 0.8437553048 0.8052688837 + 0.8363779783 0.8442121744 0.8057845831 + 0.8368489742 0.8446689844 0.8063002229 + 0.8373200297 0.8451257944 0.8068159223 + 0.8377910256 0.8455827236 0.8073315024 + 0.8382608294 0.8460361958 0.8078433275 + 0.8387305140 0.8464891911 0.8083544970 + 0.8392001987 0.8469423056 0.8088657260 + 0.8396698833 0.8473953009 0.8093768954 + 0.8401396275 0.8478482962 0.8098881245 + 0.8406054974 0.8482989073 0.8103969097 + 0.8410714865 0.8487493992 0.8109058142 + 0.8415374756 0.8492000103 0.8114145994 + 0.8420035243 0.8496506214 0.8119235039 + 0.8424689770 0.8501005769 0.8124318719 + 0.8429316282 0.8505470157 0.8129380941 + 0.8433941007 0.8509933949 0.8134443164 + 0.8438566923 0.8514398932 0.8139504194 + 0.8443192244 0.8518862724 0.8144565821 + 0.8447815776 0.8523321152 0.8149620891 + 0.8452432752 0.8527753949 0.8154653907 + 0.8457049727 0.8532186747 0.8159685731 + 0.8461666107 0.8536620140 0.8164718747 + 0.8466283083 0.8541052938 0.8169751167 + 0.8470892906 0.8545482159 0.8174781203 + 0.8475477099 0.8549897075 0.8179804087 + 0.8480061889 0.8554311991 0.8184826970 + 0.8484646082 0.8558726907 0.8189849854 + 0.8489230871 0.8563141823 0.8194872737 + 0.8493804932 0.8567548990 0.8199887276 + 0.8498343825 0.8571929932 0.8204873204 + 0.8502882719 0.8576310277 0.8209857941 + 0.8507422209 0.8580691218 0.8214843869 + 0.8511961102 0.8585072160 0.8219829798 + 0.8516497016 0.8589447141 0.8224810958 + 0.8521021008 0.8593794107 0.8229773045 + 0.8525546193 0.8598141074 0.8234735131 + 0.8530070186 0.8602489233 0.8239697218 + 0.8534594178 0.8606836200 0.8244658709 + 0.8539115787 0.8611180186 0.8249620199 + 0.8543611169 0.8615491986 0.8254572153 + 0.8548105955 0.8619803190 0.8259524107 + 0.8552600741 0.8624114990 0.8264474869 + 0.8557096124 0.8628426194 0.8269426823 + 0.8561590910 0.8632737994 0.8274378777 + 0.8566038013 0.8637009859 0.8279286027 + 0.8570482135 0.8641281128 0.8284192085 + 0.8574925065 0.8645551205 0.8289096951 + 0.8579369187 0.8649821281 0.8294003010 + 0.8583812118 0.8654091954 0.8298907876 + 0.8588234782 0.8658344150 0.8303787708 + 0.8592652082 0.8662589788 0.8308660984 + 0.8597068191 0.8666837215 0.8313534260 + 0.8601484895 0.8671082854 0.8318405747 + 0.8605902195 0.8675329089 0.8323279023 + 0.8610305190 0.8679561019 0.8328140974 + 0.8614696860 0.8683781028 0.8332993984 + 0.8619089127 0.8687999845 0.8337846994 + 0.8623480797 0.8692219853 0.8342700005 + 0.8627873063 0.8696439862 0.8347553015 + 0.8632252216 0.8700649738 0.8352395296 + 0.8636596203 0.8704838157 0.8357210755 + 0.8640940785 0.8709024787 0.8362026215 + 0.8645285964 0.8713212013 0.8366841078 + 0.8649629951 0.8717399836 0.8371655941 + 0.8653975129 0.8721587062 0.8376470804 + 0.8658297062 0.8725745082 0.8381273746 + 0.8662618995 0.8729901910 0.8386076093 + 0.8666940928 0.8734058738 0.8390877843 + 0.8671262264 0.8738214970 0.8395680785 + 0.8675584197 0.8742371798 0.8400483131 + 0.8679893017 0.8746507168 0.8405265808 + 0.8684194088 0.8750627041 0.8410037756 + 0.8688495159 0.8754748106 0.8414809108 + 0.8692796230 0.8758867979 0.8419579864 + 0.8697096705 0.8762987852 0.8424351811 + 0.8701388836 0.8767099977 0.8429118991 + 0.8705646992 0.8771181107 0.8433871865 + 0.8709903955 0.8775262237 0.8438625932 + 0.8714162111 0.8779342771 0.8443378806 + 0.8718420267 0.8783423901 0.8448132277 + 0.8722677827 0.8787505031 0.8452885151 + 0.8726921082 0.8791568875 0.8457635045 + 0.8731160164 0.8795629144 0.8462383151 + 0.8735398054 0.8799688220 0.8467131257 + 0.8739637136 0.8803747892 0.8471878767 + 0.8743875027 0.8807808161 0.8476626873 + 0.8748108745 0.8811860085 0.8481363058 + 0.8752331138 0.8815891743 0.8486071229 + 0.8756552935 0.8819925189 0.8490779996 + 0.8760774732 0.8823956847 0.8495488167 + 0.8764997125 0.8827990294 0.8500195742 + 0.8769218922 0.8832021952 0.8504905105 + 0.8773404956 0.8836026192 0.8509593010 + 0.8777579069 0.8840019703 0.8514273763 + 0.8781753182 0.8844013810 0.8518955112 + 0.8785927296 0.8848007917 0.8523637056 + 0.8790100813 0.8852002025 0.8528317809 + 0.8794270754 0.8855990767 0.8532996774 + 0.8798415065 0.8859958053 0.8537663817 + 0.8802559972 0.8863924146 0.8542332053 + 0.8806704283 0.8867890835 0.8546999097 + 0.8810849190 0.8871858120 0.8551666141 + 0.8814992905 0.8875824213 0.8556333184 + 0.8819131851 0.8879781961 0.8560982943 + 0.8823264837 0.8883730769 0.8565617800 + 0.8827397227 0.8887680769 0.8570253253 + 0.8831530213 0.8891630173 0.8574888110 + 0.8835663199 0.8895580173 0.8579522967 + 0.8839796185 0.8899530172 0.8584157825 + 0.8843892813 0.8903455138 0.8588786721 + 0.8847984076 0.8907375932 0.8593413830 + 0.8852074146 0.8911296725 0.8598040938 + 0.8856164813 0.8915218115 0.8602668047 + 0.8860256076 0.8919138908 0.8607295752 + 0.8864343166 0.8923054934 0.8611919284 + 0.8868409991 0.8926942945 0.8616520762 + 0.8872476220 0.8930829763 0.8621122837 + 0.8876541853 0.8934717774 0.8625724912 + 0.8880609274 0.8938605189 0.8630326986 + 0.8884674907 0.8942492008 0.8634929061 + 0.8888738751 0.8946374059 0.8639521003 + 0.8892799020 0.8950244188 0.8644095063 + 0.8896859288 0.8954114914 0.8648669124 + 0.8900917768 0.8957985044 0.8653243780 + 0.8904978037 0.8961855769 0.8657817841 + 0.8909037709 0.8965725899 0.8662391901 + 0.8913077116 0.8969581127 0.8666961193 + 0.8917099833 0.8973423243 0.8671525121 + 0.8921123147 0.8977264762 0.8676090240 + 0.8925145864 0.8981108069 0.8680654168 + 0.8929169178 0.8984950185 0.8685219288 + 0.8933190703 0.8988791704 0.8689783216 + 0.8937194943 0.8992601037 0.8694334030 + 0.8941190839 0.8996396065 0.8698878884 + 0.8945186138 0.9000191092 0.8703424931 + 0.8949180841 0.9003986120 0.8707969785 + 0.8953176737 0.9007781148 0.8712515235 + 0.8957172036 0.9011576176 0.8717060089 + 0.8961154819 0.9015353918 0.8721609116 + 0.8965135217 0.9019128084 0.8726158142 + 0.8969115019 0.9022902250 0.8730707765 + 0.8973094821 0.9026675820 0.8735256791 + 0.8977075219 0.9030449986 0.8739807010 + 0.8981055021 0.9034224153 0.8744356036 + 0.8985009789 0.9037982821 0.8748894930 + 0.8988959789 0.9041739702 0.8753430843 + 0.8992909193 0.9045495987 0.8757967949 + 0.8996858001 0.9049252868 0.8762505054 + 0.9000808001 0.9053009152 0.8767040968 + 0.9004756808 0.9056766033 0.8771578074 + 0.9008677006 0.9060493112 0.8776090145 + 0.9012590051 0.9064213037 0.8780596852 + 0.9016503096 0.9067932963 0.8785104156 + 0.9020416141 0.9071652889 0.8789610863 + 0.9024329782 0.9075372815 0.8794118166 + 0.9028242826 0.9079092741 0.8798624873 + 0.9032142162 0.9082794189 0.8803126812 + 0.9036036134 0.9086487889 0.8807626963 + 0.9039930105 0.9090182185 0.8812127113 + 0.9043824077 0.9093875885 0.8816627264 + 0.9047718048 0.9097570181 0.8821126819 + 0.9051612020 0.9101263881 0.8825628161 + 0.9055489898 0.9104949236 0.8830112815 + 0.9059358239 0.9108626842 0.8834589720 + 0.9063225985 0.9112305045 0.8839067221 + 0.9067094922 0.9115983844 0.8843544126 + 0.9070963264 0.9119662046 0.8848021030 + 0.9074832201 0.9123340845 0.8852497935 + 0.9078686237 0.9127002954 0.8856955767 + 0.9082522988 0.9130645990 0.8861392736 + 0.9086359739 0.9134290218 0.8865829706 + 0.9090197086 0.9137933254 0.8870267272 + 0.9094033837 0.9141576886 0.8874704242 + 0.9097871184 0.9145219922 0.8879141212 + 0.9101704955 0.9148855805 0.8883572817 + 0.9105526805 0.9152469039 0.8887993097 + 0.9109349847 0.9156082273 0.8892412782 + 0.9113172293 0.9159694910 0.8896833062 + 0.9116994739 0.9163308144 0.8901252747 + 0.9120817184 0.9166920185 0.8905673027 + 0.9124640226 0.9170532823 0.8910092711 + 0.9128441811 0.9174141288 0.8914495111 + 0.9132243991 0.9177749157 0.8918895721 + 0.9136046171 0.9181355834 0.8923298120 + 0.9139847755 0.9184963703 0.8927698731 + 0.9143649936 0.9188570976 0.8932099938 + 0.9147452116 0.9192178845 0.8936501741 + 0.9151231050 0.9195770025 0.8940905929 + 0.9154999256 0.9199352860 0.8945311904 + 0.9158766270 0.9202936888 0.8949717283 + 0.9162533283 0.9206519723 0.8954123259 + 0.9166300893 0.9210103154 0.8958528042 + 0.9170067906 0.9213687181 0.8962934017 + 0.9173828959 0.9217258096 0.8967335224 + 0.9177572727 0.9220798016 0.8971729279 + 0.9181317091 0.9224339128 0.8976122141 + 0.9185060859 0.9227880239 0.8980515003 + 0.9188805223 0.9231420159 0.8984909058 + 0.9192548990 0.9234961271 0.8989301920 + 0.9196293950 0.9238501787 0.8993694782 + 0.9200025201 0.9242026806 0.8998063803 + 0.9203755260 0.9245550036 0.9002429247 + 0.9207484126 0.9249072075 0.9006794095 + 0.9211214185 0.9252594709 0.9011157751 + 0.9214943051 0.9256117940 0.9015523195 + 0.9218673110 0.9259641171 0.9019888043 + 0.9222391248 0.9263157845 0.9024249911 + 0.9226092100 0.9266666770 0.9028608799 + 0.9229792953 0.9270175099 0.9032967091 + 0.9233493209 0.9273684025 0.9037325978 + 0.9237194061 0.9277192950 0.9041684866 + 0.9240894914 0.9280701280 0.9046043158 + 0.9244595766 0.9284210205 0.9050402045 + 0.9248281121 0.9287691116 0.9054753184 + 0.9251965284 0.9291167259 0.9059103727 + 0.9255648255 0.9294642806 0.9063454866 + 0.9259331226 0.9298120141 0.9067806005 + 0.9263014793 0.9301596284 0.9072157145 + 0.9266697764 0.9305071831 0.9076508284 + 0.9270378947 0.9308542013 0.9080855250 + 0.9274052978 0.9311996102 0.9085192084 + 0.9277727008 0.9315450191 0.9089530110 + 0.9281402230 0.9318904281 0.9093868136 + 0.9285076261 0.9322357178 0.9098204970 + 0.9288750291 0.9325811267 0.9102542996 + 0.9292423725 0.9329264760 0.9106879830 + 0.9296076298 0.9332711101 0.9111205935 + 0.9299712777 0.9336152077 0.9115526080 + 0.9303351045 0.9339594245 0.9119845033 + 0.9306988120 0.9343035221 0.9124163985 + 0.9310625792 0.9346476197 0.9128482938 + 0.9314264059 0.9349917769 0.9132803082 + 0.9317901134 0.9353358746 0.9137122035 + 0.9321511984 0.9356771708 0.9141423106 + 0.9325122237 0.9360182285 0.9145721793 + 0.9328730702 0.9363592863 0.9150022268 + 0.9332339764 0.9367004037 0.9154322147 + 0.9335948825 0.9370414019 0.9158620834 + 0.9339559078 0.9373825192 0.9162920713 + 0.9343165159 0.9377229810 0.9167218208 + 0.9346756935 0.9380605817 0.9171506166 + 0.9350348711 0.9383983016 0.9175792933 + 0.9353941083 0.9387360215 0.9180080891 + 0.9357532859 0.9390736222 0.9184368849 + 0.9361125231 0.9394112825 0.9188656211 + 0.9364717007 0.9397490025 0.9192944169 + 0.9368298054 0.9400861263 0.9197227955 + 0.9371858835 0.9404221773 0.9201506972 + 0.9375420213 0.9407582879 0.9205787182 + 0.9378980994 0.9410943985 0.9210066199 + 0.9382542968 0.9414305091 0.9214345813 + 0.9386103749 0.9417666197 0.9218624830 + 0.9389665127 0.9421027899 0.9222903848 + 0.9393215179 0.9424378872 0.9227181077 + 0.9396752715 0.9427719712 0.9231454134 + 0.9400290847 0.9431061745 0.9235727191 + 0.9403830171 0.9434403181 0.9240000844 + 0.9407367706 0.9437745214 0.9244273901 + 0.9410905838 0.9441086054 0.9248546958 + 0.9414445162 0.9444426894 0.9252821207 + 0.9417976141 0.9447755218 0.9257091880 + 0.9421502948 0.9451072812 0.9261360765 + 0.9425029159 0.9454391003 0.9265630841 + 0.9428554773 0.9457709193 0.9269899726 + 0.9432082176 0.9461026788 0.9274169803 + 0.9435607791 0.9464343786 0.9278439283 + 0.9439134002 0.9467661977 0.9282708764 + 0.9442645907 0.9470977187 0.9286978245 + 0.9446147084 0.9474288821 0.9291247725 + 0.9449648261 0.9477601051 0.9295517802 + 0.9453148842 0.9480913281 0.9299787283 + 0.9456650019 0.9484224916 0.9304056764 + 0.9460151196 0.9487537146 0.9308326840 + 0.9463651776 0.9490848780 0.9312595725 + 0.9467138052 0.9494156241 0.9316855073 + 0.9470613003 0.9497458935 0.9321106076 + 0.9474087954 0.9500762224 0.9325357080 + 0.9477561712 0.9504064918 0.9329606891 + 0.9481037259 0.9507368207 0.9333857894 + 0.9484512210 0.9510670900 0.9338108897 + 0.9487985969 0.9513974190 0.9342358708 + 0.9491446018 0.9517257214 0.9346597791 + 0.9494888783 0.9520516992 0.9350821972 + 0.9498332143 0.9523777962 0.9355046749 + 0.9501774907 0.9527037740 0.9359272122 + 0.9505218267 0.9530298114 0.9363496900 + 0.9508661032 0.9533559084 0.9367721081 + 0.9512103200 0.9536818862 0.9371945858 + 0.9515535235 0.9540063143 0.9376161098 + 0.9518942237 0.9543272853 0.9380356073 + 0.9522349238 0.9546483159 0.9384549856 + 0.9525756836 0.9549692869 0.9388744831 + 0.9529163837 0.9552901983 0.9392939806 + 0.9532570839 0.9556112289 0.9397134185 + 0.9535977840 0.9559322000 0.9401329160 + 0.9539381862 0.9562529922 0.9405519962 + 0.9542763233 0.9565721750 0.9409685731 + 0.9546142817 0.9568914175 0.9413852096 + 0.9549522996 0.9572107196 0.9418017864 + 0.9552903771 0.9575299025 0.9422184229 + 0.9556283951 0.9578490853 0.9426349998 + 0.9559664726 0.9581683874 0.9430515766 + 0.9563044906 0.9584875703 0.9434682131 + 0.9566408992 0.9588049054 0.9438832998 + 0.9569768906 0.9591218233 0.9442980289 + 0.9573128819 0.9594386816 0.9447128177 + 0.9576488733 0.9597554803 0.9451274872 + 0.9579848051 0.9600723982 0.9455422759 + 0.9583207965 0.9603893161 0.9459570050 + 0.9586567879 0.9607061744 0.9463717937 + 0.9589915872 0.9610219002 0.9467856288 + 0.9593248963 0.9613364935 0.9471985102 + 0.9596583247 0.9616510868 0.9476113915 + 0.9599916935 0.9619656205 0.9480242729 + 0.9603250027 0.9622802138 0.9484372139 + 0.9606583714 0.9625946879 0.9488500953 + 0.9609917998 0.9629092813 0.9492629170 + 0.9613248706 0.9632236958 0.9496756792 + 0.9616556168 0.9635366201 0.9500876069 + 0.9619864225 0.9638494849 0.9504995942 + 0.9623171091 0.9641624093 0.9509115219 + 0.9626479149 0.9644752741 0.9513233900 + 0.9629786015 0.9647881985 0.9517353177 + 0.9633094072 0.9651011825 0.9521471858 + 0.9636400938 0.9654141068 0.9525591135 + 0.9639695287 0.9657256007 0.9529703259 + 0.9642981291 0.9660363197 0.9533810019 + 0.9646267891 0.9663469791 0.9537916780 + 0.9649553895 0.9666576982 0.9542024136 + 0.9652839899 0.9669682980 0.9546130896 + 0.9656125903 0.9672790170 0.9550238252 + 0.9659413099 0.9675896764 0.9554345012 + 0.9662696123 0.9679000974 0.9558449984 + 0.9665955901 0.9682075977 0.9562532902 + 0.9669216275 0.9685152173 0.9566615820 + 0.9672476053 0.9688227177 0.9570698738 + 0.9675735831 0.9691302180 0.9574782252 + 0.9678996801 0.9694377780 0.9578865170 + 0.9682257175 0.9697452784 0.9582948089 + 0.9685516953 0.9700528979 0.9587031007 + 0.9688763022 0.9703593254 0.9591103792 + 0.9691995978 0.9706646800 0.9595170021 + 0.9695230126 0.9709700942 0.9599235058 + 0.9698463082 0.9712755084 0.9603301287 + 0.9701697230 0.9715809226 0.9607365727 + 0.9704930186 0.9718862772 0.9611431956 + 0.9708163738 0.9721916914 0.9615496993 + 0.9711397290 0.9724971056 0.9619563222 + 0.9714608192 0.9728004932 0.9623609781 + 0.9717814922 0.9731037021 0.9627655149 + 0.9721022844 0.9734067917 0.9631699920 + 0.9724230170 0.9737098813 0.9635745883 + 0.9727438092 0.9740129709 0.9639791250 + 0.9730644822 0.9743161798 0.9643836021 + 0.9733852744 0.9746193290 0.9647880793 + 0.9737054706 0.9749218822 0.9651920795 + 0.9740229845 0.9752221107 0.9655939937 + 0.9743404984 0.9755222797 0.9659960270 + 0.9746580720 0.9758223891 0.9663978815 + 0.9749755859 0.9761226177 0.9667997956 + 0.9752930999 0.9764227271 0.9672017097 + 0.9756106138 0.9767228961 0.9676036239 + 0.9759281278 0.9770230055 0.9680054784 + 0.9762442708 0.9773221016 0.9684066772 + 0.9765583873 0.9776195288 0.9688066840 + 0.9768725038 0.9779168963 0.9692066908 + 0.9771866798 0.9782143235 0.9696066976 + 0.9775007963 0.9785116911 0.9700067043 + 0.9778149128 0.9788091779 0.9704067111 + 0.9781290293 0.9791066051 0.9708067775 + 0.9784432054 0.9794039726 0.9712067842 + 0.9787554741 0.9796999097 0.9716057777 + 0.9790663123 0.9799945951 0.9720041752 + 0.9793770909 0.9802892804 0.9724025130 + 0.9796879888 0.9805839062 0.9728007913 + 0.9799988270 0.9808785915 0.9731991291 + 0.9803097248 0.9811732769 0.9735975266 + 0.9806205034 0.9814680219 0.9739958048 + 0.9809312820 0.9817627072 0.9743940830 + 0.9812394977 0.9820554852 0.9747912884 + 0.9815462828 0.9823471904 0.9751877785 + 0.9818530083 0.9826390147 0.9755843282 + 0.9821597934 0.9829307199 0.9759808183 + 0.9824665785 0.9832224846 0.9763773084 + 0.9827733040 0.9835141897 0.9767737985 + 0.9830800891 0.9838058949 0.9771704078 + 0.9833868742 0.9840977192 0.9775668979 + 0.9836912751 0.9843873978 0.9779617190 + 0.9839947224 0.9846761227 0.9783557057 + 0.9842982292 0.9849647880 0.9787496924 + 0.9846016169 0.9852535129 0.9791436791 + 0.9849050045 0.9855421782 0.9795376062 + 0.9852083921 0.9858309031 0.9799315929 + 0.9855117798 0.9861196280 0.9803255796 + 0.9858152270 0.9864082932 0.9807196259 + 0.9861156940 0.9866940975 0.9811127782 + 0.9864144921 0.9869784117 0.9815055132 + 0.9867134094 0.9872627258 0.9818981886 + 0.9870122075 0.9875469804 0.9822909236 + 0.9873110056 0.9878312945 0.9826835990 + 0.9876099229 0.9881156087 0.9830763936 + 0.9879087210 0.9883999228 0.9834691286 + 0.9882075787 0.9886841774 0.9838618040 + 0.9885028005 0.9889653921 0.9842537045 + 0.9887949228 0.9892442226 0.9846450090 + 0.9890869856 0.9895228744 0.9850361943 + 0.9893791080 0.9898015857 0.9854274988 + 0.9896711111 0.9900802970 0.9858188033 + 0.9899631739 0.9903590083 0.9862099886 + 0.9902552962 0.9906377196 0.9866012931 + 0.9905474186 0.9909163713 0.9869924784 + 0.9908375740 0.9911934733 0.9873834252 + 0.9911243916 0.9914677739 0.9877734184 + 0.9914113283 0.9917420745 0.9881634712 + 0.9916980863 0.9920163751 0.9885535240 + 0.9919849038 0.9922906756 0.9889435768 + 0.9922717810 0.9925649762 0.9893336296 + 0.9925585985 0.9928393960 0.9897236824 + 0.9928454161 0.9931136966 0.9901136756 + 0.9931316972 0.9933875203 0.9905033112 + 0.9934136868 0.9936578870 0.9908893108 + 0.9936957955 0.9939283133 0.9912753105 + 0.9939777851 0.9941986799 0.9916613102 + 0.9942598939 0.9944691062 0.9920473099 + 0.9945418835 0.9947394729 0.9924331903 + 0.9948239923 0.9950098991 0.9928191900 + 0.9951061010 0.9952803850 0.9932051897 + 0.9953880906 0.9955508113 0.9935911894 + 0.9956638217 0.9958161116 0.9939712286 + 0.9959380031 0.9960802794 0.9943498969 + 0.9962123036 0.9963445067 0.9947286248 + 0.9964866042 0.9966086745 0.9951072931 + 0.9967609048 0.9968729019 0.9954860210 + 0.9970350862 0.9971370101 0.9958646894 + 0.9973093867 0.9974011779 0.9962432981 + 0.9975836873 0.9976654053 0.9966220260 + 0.9978551865 0.9979274273 0.9969990849 + 0.9981232882 0.9981864095 0.9973741770 + 0.9983913898 0.9984455109 0.9977493286 + 0.9986594915 0.9987046123 0.9981244206 + 0.9989275932 0.9989637136 0.9984995723 + 0.9991956949 0.9992228150 0.9988747239 + 0.9994637966 0.9994817972 0.9992498159 + 0.9997318983 0.9997408986 0.9996249080 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_vista_800.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_vista_800.spi1d index 2289f6b2..0e809829 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_vista_800.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/agfa_agfacolor_vista_800.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0185100008 0.0164319202 0.0126006696 - 0.0311950799 0.0276685897 0.0221115705 - 0.0417797714 0.0372233018 0.0303079691 - 0.0512531996 0.0458445400 0.0377487317 - 0.0600418709 0.0538478307 0.0446756296 - 0.0683360174 0.0614236407 0.0512225516 - 0.0762501881 0.0686839372 0.0574930906 - 0.0838661790 0.0756844804 0.0635228232 - 0.0913166925 0.0824975967 0.0693578273 - 0.0986084193 0.0892031565 0.0750138834 - 0.1057450026 0.0958264992 0.0805220678 - 0.1127896011 0.1024202034 0.0859134421 - 0.1197260022 0.1089596003 0.0911877826 - 0.1265023947 0.1154130027 0.0963427573 - 0.1330948025 0.1217321977 0.1013797969 - 0.1394809932 0.1279036999 0.1063025966 - 0.1456708014 0.1339247972 0.1111140996 - 0.1516747028 0.1397916973 0.1158170998 - 0.1575195938 0.1455152035 0.1204319000 - 0.1632243991 0.1510861963 0.1249561980 - 0.1688113064 0.1565372944 0.1294070035 - 0.1742866039 0.1618628055 0.1337791979 - 0.1796312928 0.1670635045 0.1380856037 - 0.1848537028 0.1721491963 0.1423334032 - 0.1899529994 0.1771177948 0.1465034038 - 0.1949339062 0.1819808930 0.1506060064 - 0.1998192966 0.1867358983 0.1546511948 - 0.2046055943 0.1913896054 0.1586488932 - 0.2093106955 0.1959585994 0.1626013964 - 0.2139423043 0.2004473954 0.1665008068 - 0.2184966058 0.2048527002 0.1703405976 - 0.2229668945 0.2091723979 0.1741351932 - 0.2273671031 0.2134197950 0.1778849959 - 0.2316877991 0.2175989002 0.1815871000 - 0.2359403968 0.2217054963 0.1852401942 - 0.2401207983 0.2257481962 0.1888412982 - 0.2442404926 0.2297320962 0.1923972070 - 0.2482959926 0.2336588949 0.1959189028 - 0.2522906065 0.2375231981 0.1994097978 - 0.2562406063 0.2413284928 0.2028710991 - 0.2601419091 0.2450881004 0.2062990963 - 0.2639907897 0.2487992048 0.2096917033 - 0.2677909136 0.2524616122 0.2130511999 - 0.2715432942 0.2560823858 0.2163791060 - 0.2752490938 0.2596504092 0.2196782976 - 0.2789067924 0.2631703019 0.2229489982 - 0.2825185061 0.2666490972 0.2261860967 - 0.2860870957 0.2700851858 0.2293913960 - 0.2896127105 0.2734856904 0.2325628996 - 0.2930983901 0.2768476009 0.2357040942 - 0.2965483069 0.2801684141 0.2388191074 - 0.2999581099 0.2834582031 0.2419067025 - 0.3033216894 0.2867042124 0.2449615002 - 0.3066503108 0.2899151146 0.2479871958 - 0.3099426925 0.2930943966 0.2509889007 - 0.3132070005 0.2962363064 0.2539705932 - 0.3164420128 0.2993449867 0.2569276094 - 0.3196372092 0.3024170995 0.2598538995 - 0.3228017092 0.3054631948 0.2627541125 - 0.3259347975 0.3084770143 0.2656317949 - 0.3290332854 0.3114668131 0.2684907913 - 0.3321030140 0.3144359887 0.2713276148 - 0.3351433873 0.3173773885 0.2741436064 - 0.3381536007 0.3202837110 0.2769415081 - 0.3411369026 0.3231653869 0.2797215879 - 0.3440904915 0.3260202110 0.2824831903 - 0.3470189869 0.3288542926 0.2852219939 - 0.3499220014 0.3316698968 0.2879402041 - 0.3528007865 0.3344615102 0.2906357050 - 0.3556517959 0.3372254968 0.2933070958 - 0.3584778011 0.3399674892 0.2959615886 - 0.3612776995 0.3426899016 0.2985951900 - 0.3640502095 0.3453918099 0.3012051880 - 0.3667984903 0.3480693996 0.3037928045 - 0.3695279062 0.3507238925 0.3063583076 - 0.3722384870 0.3533582091 0.3089059889 - 0.3749296069 0.3559763134 0.3114354908 - 0.3776003122 0.3585732877 0.3139464855 - 0.3802514076 0.3611460924 0.3164429963 - 0.3828811944 0.3637025952 0.3189218938 - 0.3854868114 0.3662396967 0.3213779926 - 0.3880729079 0.3687601089 0.3238114119 - 0.3906360865 0.3712593019 0.3262237906 - 0.3931770027 0.3737432957 0.3286206126 - 0.3956986070 0.3762117028 0.3310020864 - 0.3982034028 0.3786646128 0.3333649933 - 0.4006921947 0.3811041117 0.3357118070 - 0.4031597972 0.3835237026 0.3380421996 - 0.4056158066 0.3859252036 0.3403576910 - 0.4080604017 0.3883082867 0.3426569998 - 0.4104911089 0.3906767964 0.3449401855 - 0.4129078090 0.3930304050 0.3472093046 - 0.4153085053 0.3953728974 0.3494597971 - 0.4176962078 0.3977056146 0.3516952097 - 0.4200707078 0.4000267088 0.3539173901 - 0.4224311113 0.4023346007 0.3561232984 - 0.4247764051 0.4046272933 0.3583106995 - 0.4271036088 0.4069069028 0.3604839146 - 0.4294143915 0.4091728926 0.3626427054 - 0.4317080081 0.4114241898 0.3647843897 - 0.4339860082 0.4136590064 0.3669061959 - 0.4362483025 0.4158793986 0.3690074980 - 0.4384947121 0.4180856943 0.3710916936 - 0.4407278895 0.4202806056 0.3731572032 - 0.4429492950 0.4224627912 0.3752071857 - 0.4451586008 0.4246312082 0.3772436082 - 0.4473553896 0.4267886877 0.3792651892 - 0.4495382905 0.4289326072 0.3812715113 - 0.4517078996 0.4310675859 0.3832643926 - 0.4538640976 0.4331924021 0.3852454126 - 0.4560067058 0.4353078902 0.3872154057 - 0.4581354856 0.4374152124 0.3891724944 - 0.4602501988 0.4395132959 0.3911151886 - 0.4623506069 0.4416010976 0.3930422068 - 0.4644348025 0.4436782002 0.3949519098 - 0.4665004909 0.4457429945 0.3968465030 - 0.4685483873 0.4477992058 0.3987269998 - 0.4705809951 0.4498446882 0.4005940855 - 0.4725970030 0.4518814087 0.4024456143 - 0.4745942950 0.4539059103 0.4042817056 - 0.4765745997 0.4559206069 0.4061039090 - 0.4785369933 0.4579218924 0.4079152048 - 0.4804831147 0.4599123001 0.4097140133 - 0.4824160933 0.4618884921 0.4115009010 - 0.4843369126 0.4638526142 0.4132741988 - 0.4862442017 0.4658051133 0.4150344133 - 0.4881368876 0.4677473903 0.4167799056 - 0.4900161922 0.4696780145 0.4185154140 - 0.4918822050 0.4715974033 0.4202412963 - 0.4937359989 0.4735072851 0.4219577014 - 0.4955778122 0.4754084051 0.4236656129 - 0.4974071980 0.4772998095 0.4253664017 - 0.4992232919 0.4791795909 0.4270611107 - 0.5010253191 0.4810472131 0.4287497997 - 0.5028162003 0.4829035103 0.4304314852 - 0.5045911074 0.4847469032 0.4321100116 - 0.5063520074 0.4865756035 0.4337812066 - 0.5080993772 0.4883880019 0.4354482889 - 0.5098353028 0.4901869893 0.4371086061 - 0.5115584135 0.4919714034 0.4387632906 - 0.5132681727 0.4937416911 0.4404115975 - 0.5149654746 0.4954991937 0.4420526028 - 0.5166510940 0.4972419143 0.4436877966 - 0.5183221102 0.4989711940 0.4453130066 - 0.5199794769 0.5006883740 0.4469313920 - 0.5216261744 0.5023959875 0.4485433996 - 0.5232623219 0.5040932894 0.4501496851 - 0.5248879194 0.5057805181 0.4517503083 - 0.5265035033 0.5074588060 0.4533442855 - 0.5281016231 0.5091217756 0.4549309015 - 0.5296890140 0.5107734203 0.4565131068 - 0.5312656760 0.5124161243 0.4580908120 - 0.5328325033 0.5140507221 0.4596638978 - 0.5343909860 0.5156767964 0.4612325132 - 0.5359356999 0.5172935128 0.4627946019 - 0.5374701023 0.5189024210 0.4643518031 - 0.5389943719 0.5205038190 0.4659039974 - 0.5405091047 0.5220975280 0.4674513042 - 0.5420141220 0.5236791968 0.4689922929 - 0.5435104966 0.5252534747 0.4705272019 - 0.5449982285 0.5268201232 0.4720568061 - 0.5464764833 0.5283790827 0.4735811949 - 0.5479413867 0.5299316049 0.4750989974 - 0.5493953824 0.5314735174 0.4766100943 - 0.5508413911 0.5330083966 0.4781157076 - 0.5522797704 0.5345366001 0.4796159863 - 0.5537052155 0.5360544920 0.4811086059 - 0.5551214218 0.5375661254 0.4825947881 - 0.5565286875 0.5390713811 0.4840748906 - 0.5579245090 0.5405685902 0.4855479002 - 0.5593087077 0.5420556068 0.4870094061 - 0.5606855154 0.5435367227 0.4884645045 - 0.5620552897 0.5450118780 0.4899129868 - 0.5634167790 0.5464770794 0.4913502932 - 0.5647714138 0.5479345918 0.4927789867 - 0.5661193728 0.5493863225 0.4941999018 - 0.5674555898 0.5508307815 0.4956082106 - 0.5687829256 0.5522680879 0.4970068932 - 0.5701043010 0.5537011027 0.4983989000 - 0.5714179873 0.5551288724 0.4997817874 - 0.5727239251 0.5565506816 0.5011560917 - 0.5740242004 0.5579689145 0.5025244951 - 0.5753160715 0.5593820214 0.5038843155 - 0.5766007900 0.5607866049 0.5052357912 - 0.5778805017 0.5621855855 0.5065827966 - 0.5791519880 0.5635759830 0.5079236031 - 0.5804169178 0.5649588108 0.5092585087 - 0.5816770792 0.5663369298 0.5105897188 - 0.5829272270 0.5677073002 0.5119146109 - 0.5841702223 0.5690726042 0.5132346749 - 0.5854083896 0.5704342723 0.5145516992 - 0.5866373777 0.5717855096 0.5158618093 - 0.5878623724 0.5731319785 0.5171685815 - 0.5890823007 0.5744730830 0.5184715986 - 0.5902937055 0.5758020282 0.5197688937 - 0.5915008187 0.5771260858 0.5210636258 - 0.5926988721 0.5784417987 0.5223534703 - 0.5938925147 0.5797501802 0.5236395001 - 0.5950831771 0.5810542107 0.5249232054 - 0.5962653160 0.5823497176 0.5262010098 - 0.5974434018 0.5836412907 0.5274763107 - 0.5986136198 0.5849254727 0.5287467241 - 0.5997778177 0.5862020850 0.5300114751 - 0.6009387970 0.5874748230 0.5312736034 - 0.6020932198 0.5887362957 0.5325291157 - 0.6032456160 0.5899950862 0.5337823033 - 0.6043936014 0.5912477970 0.5350301862 - 0.6055356264 0.5924956203 0.5362746119 - 0.6066730022 0.5937396884 0.5375164747 - 0.6078019142 0.5949761868 0.5387526155 - 0.6089282036 0.5962103009 0.5399869084 - 0.6100460291 0.5974360704 0.5412151814 - 0.6111602783 0.5986577868 0.5424402952 - 0.6122679114 0.5998724103 0.5436596870 - 0.6133708954 0.6010810733 0.5448756218 - 0.6144707203 0.6022859216 0.5460891724 - 0.6155622005 0.6034839749 0.5472962856 - 0.6166514754 0.6046805978 0.5485017896 - 0.6177281141 0.6058706045 0.5497025847 - 0.6188026071 0.6070585251 0.5509018898 - 0.6198686957 0.6082382798 0.5520957708 - 0.6209319830 0.6094141006 0.5532882214 - 0.6219903827 0.6105834842 0.5544781089 - 0.6230461001 0.6117473245 0.5556638837 - 0.6240993738 0.6129062772 0.5568463802 - 0.6251472831 0.6140604019 0.5580248833 - 0.6261926293 0.6152123213 0.5592017174 - 0.6272296906 0.6163583994 0.5603731871 - 0.6282647848 0.6175032258 0.5615437031 - 0.6292926073 0.6186398864 0.5627098083 - 0.6303197145 0.6197757125 0.5638754964 - 0.6313399076 0.6209014058 0.5650361180 - 0.6323593259 0.6220262051 0.5661963820 - 0.6333720088 0.6231431961 0.5673534870 - 0.6343832016 0.6242589951 0.5685095787 - 0.6353875995 0.6253690720 0.5696610212 - 0.6363899708 0.6264768839 0.5708104968 - 0.6373851895 0.6275768280 0.5719538927 - 0.6383780837 0.6286739707 0.5730956197 - 0.6393638253 0.6297637224 0.5742328763 - 0.6403476000 0.6308506727 0.5753682256 - 0.6413263083 0.6319305897 0.5764986873 - 0.6423032880 0.6330078840 0.5776277781 - 0.6432760954 0.6340783238 0.5787534118 - 0.6442463994 0.6351463199 0.5798763037 - 0.6452097297 0.6362072825 0.5809919834 - 0.6461709738 0.6372658014 0.5821056962 - 0.6471257210 0.6383160949 0.5832133293 - 0.6480792165 0.6393647790 0.5843191743 - 0.6490263939 0.6404061913 0.5854179859 - 0.6499727964 0.6414468288 0.5865159035 - 0.6509122849 0.6424813271 0.5876076818 - 0.6518514752 0.6435155869 0.5886991024 - 0.6527829170 0.6445450187 0.5897815824 - 0.6537142992 0.6455743909 0.5908641219 - 0.6546376944 0.6465963721 0.5919384956 - 0.6555603743 0.6476178765 0.5930123925 - 0.6564759016 0.6486306787 0.5940799117 - 0.6573897004 0.6496415138 0.5951458812 - 0.6582992077 0.6506463289 0.5962070227 - 0.6592066884 0.6516482234 0.5972657800 - 0.6601099968 0.6526457071 0.5983211994 - 0.6610093117 0.6536391973 0.5993734002 - 0.6619052887 0.6546294093 0.6004229784 - 0.6627947092 0.6556134224 0.6014679074 - 0.6636828780 0.6565961838 0.6025115252 - 0.6645650268 0.6575726271 0.6035487056 - 0.6654471159 0.6585490704 0.6045858264 - 0.6663234830 0.6595203876 0.6056172848 - 0.6671994925 0.6604915261 0.6066485047 - 0.6680691242 0.6614578962 0.6076750755 - 0.6689361930 0.6624224782 0.6086999774 - 0.6697993875 0.6633834243 0.6097220182 - 0.6706585288 0.6643403172 0.6107410192 - 0.6715161204 0.6652954221 0.6117582917 - 0.6723682880 0.6662437916 0.6127699018 - 0.6732205749 0.6671922207 0.6137815118 - 0.6740664840 0.6681342721 0.6147891283 - 0.6749119163 0.6690760255 0.6157965064 - 0.6757528782 0.6700139046 0.6167991757 - 0.6765912771 0.6709495783 0.6177995205 - 0.6774272919 0.6718820930 0.6187973022 - 0.6782582998 0.6728078723 0.6197900176 - 0.6790893078 0.6737337112 0.6207826734 - 0.6799132228 0.6746503115 0.6217687130 - 0.6807367802 0.6755667925 0.6227545738 - 0.6815574765 0.6764777899 0.6237366796 - 0.6823765039 0.6773855090 0.6247165203 - 0.6831939816 0.6782909036 0.6256946921 - 0.6840069294 0.6791892052 0.6266679764 - 0.6848198175 0.6800873876 0.6276412010 - 0.6856262088 0.6809793711 0.6286085844 - 0.6864315271 0.6818702221 0.6295750141 - 0.6872345805 0.6827588081 0.6305391788 - 0.6880347729 0.6836444139 0.6315007806 - 0.6888350248 0.6845300198 0.6324623823 - 0.6896290183 0.6854097843 0.6334151030 - 0.6904228926 0.6862894893 0.6343677044 - 0.6912125945 0.6871656179 0.6353166103 - 0.6919984818 0.6880385280 0.6362622976 - 0.6927841902 0.6889110208 0.6372076273 - 0.6935647726 0.6897763014 0.6381461024 - 0.6943454146 0.6906415820 0.6390845776 - 0.6951224208 0.6915038824 0.6400200725 - 0.6958963871 0.6923636794 0.6409534216 - 0.6966701746 0.6932234168 0.6418864131 - 0.6974359155 0.6940783858 0.6428129077 - 0.6982017159 0.6949334145 0.6437392831 - 0.6989647150 0.6957843900 0.6446622014 - 0.6997249126 0.6966313720 0.6455814838 - 0.7004849911 0.6974784732 0.6465008855 - 0.7012411952 0.6983184814 0.6474134922 - 0.7019971013 0.6991580725 0.6483256817 - 0.7027515173 0.6999952793 0.6492353082 - 0.7035031915 0.7008281946 0.6501401067 - 0.7042549253 0.7016611099 0.6510447860 - 0.7050036788 0.7024886012 0.6519446969 - 0.7057514191 0.7033143044 0.6528429985 - 0.7064986229 0.7041394114 0.6537404060 - 0.7072420716 0.7049599886 0.6546317935 - 0.7079855204 0.7057806253 0.6555231810 - 0.7087271214 0.7065981030 0.6564105749 - 0.7094665170 0.7074120045 0.6572937965 - 0.7102057934 0.7082259059 0.6581770182 - 0.7109420896 0.7090328932 0.6590545774 - 0.7116774917 0.7098382115 0.6599308252 - 0.7124124765 0.7106426954 0.6608062983 - 0.7131438255 0.7114415169 0.6616767049 - 0.7138749957 0.7122402787 0.6625469923 - 0.7146041989 0.7130368948 0.6634144187 - 0.7153301239 0.7138301134 0.6642774940 - 0.7160559893 0.7146233916 0.6651405096 - 0.7167779207 0.7154110074 0.6660001278 - 0.7174975872 0.7161955833 0.6668580174 - 0.7182173133 0.7169802785 0.6677159071 - 0.7189335227 0.7177579999 0.6685678959 - 0.7196493149 0.7185348868 0.6694192886 - 0.7203645110 0.7193109989 0.6702697873 - 0.7210751772 0.7200810909 0.6711146235 - 0.7217859030 0.7208511829 0.6719594002 - 0.7224943042 0.7216190100 0.6728019714 - 0.7231976986 0.7223820090 0.6736403108 - 0.7239010930 0.7231450081 0.6744784713 - 0.7246015072 0.7239043713 0.6753134131 - 0.7252988219 0.7246599793 0.6761450768 - 0.7259960771 0.7254155874 0.6769768000 - 0.7266904712 0.7261663079 0.6778048873 - 0.7273831964 0.7269141078 0.6786310077 - 0.7280758023 0.7276619077 0.6794571280 - 0.7287634015 0.7284036279 0.6802788973 - 0.7294492126 0.7291433215 0.6810994148 - 0.7301350832 0.7298830152 0.6819198132 - 0.7308161855 0.7306177020 0.6827344894 - 0.7314965725 0.7313514948 0.6835482121 - 0.7321768999 0.7320852876 0.6843619943 - 0.7328532934 0.7328147292 0.6851683259 - 0.7335293889 0.7335436940 0.6859741211 - 0.7342054844 0.7342727184 0.6867799163 - 0.7348755002 0.7349944711 0.6875799894 - 0.7355453968 0.7357162237 0.6883801222 - 0.7362151742 0.7364377975 0.6891800761 - 0.7368800044 0.7371528745 0.6899746060 - 0.7375447750 0.7378680706 0.6907690763 - 0.7382096052 0.7385832071 0.6915634871 - 0.7388706207 0.7392929196 0.6923534274 - 0.7395316958 0.7400025725 0.6931431890 - 0.7401927114 0.7407122850 0.6939331293 - 0.7408468127 0.7414141893 0.6947162747 - 0.7415009141 0.7421159744 0.6954995990 - 0.7421548963 0.7428178191 0.6962828040 - 0.7428026199 0.7435129285 0.6970598102 - 0.7434499264 0.7442076206 0.6978365183 - 0.7440971732 0.7449023128 0.6986131072 - 0.7447416782 0.7455918193 0.6993851066 - 0.7453858256 0.7462803721 0.7001565099 - 0.7460299134 0.7469691038 0.7009277940 - 0.7466691136 0.7476530075 0.7016950250 - 0.7473068833 0.7483354807 0.7024608850 - 0.7479445934 0.7490180135 0.7032269239 - 0.7485784292 0.7496981025 0.7039886713 - 0.7492100000 0.7503767014 0.7047483921 - 0.7498415709 0.7510554194 0.7055081129 - 0.7504718900 0.7517315149 0.7062641978 - 0.7511010170 0.7524052262 0.7070168853 - 0.7517300248 0.7530788183 0.7077695727 - 0.7523571253 0.7537503839 0.7085205913 - 0.7529804111 0.7544177175 0.7092683911 - 0.7536036968 0.7550848722 0.7100160718 - 0.7542262077 0.7557517290 0.7107632756 - 0.7548426986 0.7564147711 0.7115064859 - 0.7554591894 0.7570779920 0.7122498155 - 0.7560756803 0.7577410936 0.7129930258 - 0.7566884756 0.7583994865 0.7137312293 - 0.7573007941 0.7590574026 0.7144687772 - 0.7579131722 0.7597153187 0.7152063251 - 0.7585223913 0.7603682280 0.7159397006 - 0.7591298223 0.7610182762 0.7166706920 - 0.7597371936 0.7616683841 0.7174018025 - 0.7603425980 0.7623162270 0.7181308866 - 0.7609441280 0.7629598975 0.7188568115 - 0.7615455985 0.7636035085 0.7195826769 - 0.7621468902 0.7642468810 0.7203084230 - 0.7627450824 0.7648851871 0.7210304737 - 0.7633432150 0.7655234933 0.7217525244 - 0.7639414072 0.7661617994 0.7224745750 - 0.7645366192 0.7667945027 0.7231923938 - 0.7651305795 0.7674250007 0.7239086032 - 0.7657245994 0.7680554986 0.7246248126 - 0.7663164139 0.7686836720 0.7253388762 - 0.7669041157 0.7693076730 0.7260494232 - 0.7674916983 0.7699316144 0.7267599106 - 0.7680794001 0.7705556154 0.7274703979 - 0.7686625719 0.7711765170 0.7281758785 - 0.7692456245 0.7717971802 0.7288811207 - 0.7698286176 0.7724180222 0.7295863032 - 0.7704098821 0.7730364203 0.7302888036 - 0.7709895968 0.7736529112 0.7309889793 - 0.7715693116 0.7742694020 0.7316892147 - 0.7721486092 0.7748854160 0.7323889732 - 0.7727230191 0.7754955888 0.7330843210 - 0.7732973099 0.7761058211 0.7337794900 - 0.7738716006 0.7767159939 0.7344747782 - 0.7744429708 0.7773231864 0.7351666093 - 0.7750123739 0.7779282928 0.7358562946 - 0.7755817175 0.7785333991 0.7365459800 - 0.7761507034 0.7791380882 0.7372353077 - 0.7767155766 0.7797393203 0.7379208803 - 0.7772805095 0.7803406119 0.7386065722 - 0.7778455019 0.7809417844 0.7392922044 - 0.7784079909 0.7815397978 0.7399750948 - 0.7789685726 0.7821353078 0.7406558990 - 0.7795292139 0.7827306986 0.7413367033 - 0.7800899148 0.7833262086 0.7420175076 - 0.7806466818 0.7839170098 0.7426925898 - 0.7812033892 0.7845076919 0.7433677912 - 0.7817602158 0.7850984931 0.7440428734 - 0.7823156118 0.7856879234 0.7447155118 - 0.7828689814 0.7862753272 0.7453842759 - 0.7834224105 0.7868626118 0.7460532188 - 0.7839757204 0.7874500155 0.7467219830 - 0.7845265865 0.7880331874 0.7473868728 - 0.7850766778 0.7886152267 0.7480508089 - 0.7856268287 0.7891972065 0.7487146258 - 0.7861765027 0.7897787094 0.7493779063 - 0.7867226005 0.7903565168 0.7500371933 - 0.7872688174 0.7909343243 0.7506964803 - 0.7878149152 0.7915120721 0.7513558269 - 0.7883592248 0.7920885086 0.7520118952 - 0.7889010906 0.7926632762 0.7526640296 - 0.7894430161 0.7932379842 0.7533161044 - 0.7899848223 0.7938128114 0.7539681792 - 0.7905251980 0.7943832278 0.7546153069 - 0.7910649180 0.7949519753 0.7552605867 - 0.7916046977 0.7955207229 0.7559058070 - 0.7921444178 0.7960894704 0.7565510869 - 0.7926815152 0.7966526747 0.7571910024 - 0.7932183743 0.7972157001 0.7578307986 - 0.7937554121 0.7977787256 0.7584705949 - 0.7942914963 0.7983412147 0.7591093183 - 0.7948241234 0.7989016175 0.7597439289 - 0.7953568101 0.7994620800 0.7603784800 - 0.7958894968 0.8000226021 0.7610130906 - 0.7964202166 0.8005809188 0.7616454959 - 0.7969478965 0.8011360168 0.7622743845 - 0.7974755764 0.8016909957 0.7629033923 - 0.7980033159 0.8022460938 0.7635322809 - 0.7985295057 0.8027976751 0.7641590238 - 0.7990543842 0.8033463955 0.7647839189 - 0.7995793819 0.8038951755 0.7654088140 - 0.8001043200 0.8044440150 0.7660337090 - 0.8006263971 0.8049899936 0.7666553855 - 0.8011472225 0.8055347204 0.7672758102 - 0.8016679287 0.8060795069 0.7678961754 - 0.8021886945 0.8066241741 0.7685164809 - 0.8027051091 0.8071653843 0.7691326737 - 0.8032202721 0.8077054024 0.7697477937 - 0.8037353754 0.8082454801 0.7703629136 - 0.8042505980 0.8087856174 0.7709779143 - 0.8047627807 0.8093202114 0.7715882063 - 0.8052744269 0.8098536730 0.7721974850 - 0.8057860136 0.8103871942 0.7728068233 - 0.8062974811 0.8109207153 0.7734162211 - 0.8068054914 0.8114497066 0.7740216851 - 0.8073129058 0.8119781017 0.7746266723 - 0.8078203201 0.8125064969 0.7752317190 - 0.8083277941 0.8130347729 0.7758368254 - 0.8088303804 0.8135598898 0.7764384151 - 0.8093321919 0.8140844107 0.7770395875 - 0.8098341227 0.8146088719 0.7776407003 - 0.8103358746 0.8151333928 0.7782418728 - 0.8108342886 0.8156532049 0.7788380980 - 0.8113318086 0.8161720037 0.7794333100 - 0.8118293881 0.8166908026 0.7800284028 - 0.8123269081 0.8172097206 0.7806236148 - 0.8128222823 0.8177251816 0.7812144756 - 0.8133168817 0.8182396889 0.7818040252 - 0.8138114810 0.8187540770 0.7823935151 - 0.8143060803 0.8192685843 0.7829831243 - 0.8147979975 0.8197811246 0.7835692167 - 0.8152884245 0.8202924728 0.7841535807 - 0.8157786727 0.8208039999 0.7847380042 - 0.8162690997 0.8213154078 0.7853223085 - 0.8167579174 0.8218243122 0.7859038711 - 0.8172453046 0.8223307729 0.7864828110 - 0.8177325726 0.8228374124 0.7870616913 - 0.8182200193 0.8233438730 0.7876405716 - 0.8187068105 0.8238489032 0.7882180810 - 0.8191924095 0.8243508935 0.7887930274 - 0.8196780086 0.8248528838 0.7893679738 - 0.8201637268 0.8253548741 0.7899429202 - 0.8206487894 0.8258563876 0.7905169725 - 0.8211302757 0.8263542056 0.7910869122 - 0.8216118813 0.8268520832 0.7916566730 - 0.8220934272 0.8273500204 0.7922264934 - 0.8225749731 0.8278478980 0.7927963734 - 0.8230516911 0.8283423185 0.7933602929 - 0.8235278726 0.8288363218 0.7939236760 - 0.8240039945 0.8293302059 0.7944871187 - 0.8244801164 0.8298242092 0.7950503826 - 0.8249552846 0.8303152919 0.7956107259 - 0.8254299164 0.8308048248 0.7961691022 - 0.8259044886 0.8312941790 0.7967274785 - 0.8263791203 0.8317837119 0.7972859144 - 0.8268527985 0.8322715759 0.7978432178 - 0.8273245096 0.8327561021 0.7983981967 - 0.8277962208 0.8332406282 0.7989532948 - 0.8282678723 0.8337250948 0.7995082736 - 0.8287395835 0.8342096210 0.8000633717 - 0.8292062879 0.8346912861 0.8006147146 - 0.8296728134 0.8351728916 0.8011659980 - 0.8301392794 0.8356546164 0.8017172813 - 0.8306058049 0.8361362219 0.8022685051 - 0.8310710788 0.8366158009 0.8028172851 - 0.8315353990 0.8370940089 0.8033642769 - 0.8319997787 0.8375722170 0.8039113879 - 0.8324642181 0.8380503058 0.8044583797 - 0.8329282999 0.8385276794 0.8050045967 - 0.8333910704 0.8390009999 0.8055471182 - 0.8338539004 0.8394743204 0.8060895801 - 0.8343166113 0.8399475813 0.8066321015 - 0.8347793818 0.8404209018 0.8071746230 - 0.8352388144 0.8408915997 0.8077148795 - 0.8356966972 0.8413612843 0.8082543015 - 0.8361546993 0.8418310285 0.8087937832 - 0.8366125822 0.8423007131 0.8093332052 - 0.8370695710 0.8427699208 0.8098720908 - 0.8375232220 0.8432372808 0.8104090095 - 0.8379768133 0.8437047005 0.8109459281 - 0.8384304047 0.8441720009 0.8114827871 - 0.8388838768 0.8446394205 0.8120197058 - 0.8393365741 0.8451036811 0.8125535250 - 0.8397887945 0.8455665112 0.8130859137 - 0.8402410746 0.8460292220 0.8136183023 - 0.8406932950 0.8464919925 0.8141508102 - 0.8411452174 0.8469541073 0.8146827221 - 0.8415938020 0.8474109769 0.8152111769 - 0.8420425057 0.8478677869 0.8157396913 - 0.8424912095 0.8483247161 0.8162680864 - 0.8429399133 0.8487815261 0.8167966008 - 0.8433864713 0.8492373228 0.8173233867 - 0.8438311815 0.8496921062 0.8178485036 - 0.8442757726 0.8501468897 0.8183735013 - 0.8447204828 0.8506016731 0.8188986182 - 0.8451651931 0.8510563970 0.8194236755 - 0.8456082940 0.8515083194 0.8199456930 - 0.8460512161 0.8519595861 0.8204671741 - 0.8464940786 0.8524109125 0.8209887743 - 0.8469370008 0.8528622985 0.8215103149 - 0.8473795056 0.8533126116 0.8220310807 - 0.8478202820 0.8537582755 0.8225489855 - 0.8482609987 0.8542038798 0.8230667710 - 0.8487017155 0.8546496034 0.8235846162 - 0.8491423726 0.8550952077 0.8241025209 - 0.8495807052 0.8555396795 0.8246183991 - 0.8500161767 0.8559827209 0.8251321912 - 0.8504515886 0.8564257026 0.8256459236 - 0.8508870006 0.8568686247 0.8261595964 - 0.8513224125 0.8573116064 0.8266733885 - 0.8517556787 0.8577530980 0.8271840811 - 0.8521881104 0.8581939936 0.8276935816 - 0.8526204824 0.8586348295 0.8282030821 - 0.8530527949 0.8590756059 0.8287125826 - 0.8534852266 0.8595165014 0.8292220831 - 0.8539168239 0.8599526286 0.8297290802 - 0.8543483019 0.8603882194 0.8302358985 - 0.8547797799 0.8608238101 0.8307427168 - 0.8552113175 0.8612592816 0.8312494755 - 0.8556426764 0.8616946936 0.8317559958 - 0.8560702801 0.8621253967 0.8322589993 - 0.8564978242 0.8625560999 0.8327620029 - 0.8569254279 0.8629868031 0.8332650065 - 0.8573529720 0.8634175062 0.8337680101 - 0.8577799201 0.8638479114 0.8342701197 - 0.8582031131 0.8642767072 0.8347682953 - 0.8586263061 0.8647056222 0.8352664709 - 0.8590494990 0.8651344180 0.8357647061 - 0.8594726920 0.8655632138 0.8362628818 - 0.8598954082 0.8659911156 0.8367602229 - 0.8603163958 0.8664159179 0.8372551203 - 0.8607373834 0.8668407202 0.8377498984 - 0.8611584902 0.8672655225 0.8382447958 - 0.8615794778 0.8676903248 0.8387396932 - 0.8619998097 0.8681135774 0.8392338157 - 0.8624181747 0.8685330153 0.8397259712 - 0.8628365993 0.8689525127 0.8402181864 - 0.8632550836 0.8693720102 0.8407104015 - 0.8636735082 0.8697913885 0.8412026763 - 0.8640908003 0.8702098131 0.8416935802 - 0.8645054102 0.8706253171 0.8421815038 - 0.8649199009 0.8710408807 0.8426694274 - 0.8653343916 0.8714563847 0.8431572914 - 0.8657490015 0.8718720078 0.8436452150 - 0.8661630154 0.8722869754 0.8441321850 - 0.8665751219 0.8726999760 0.8446164131 - 0.8669872284 0.8731129766 0.8451005220 - 0.8673992753 0.8735259771 0.8455846906 - 0.8678115010 0.8739389777 0.8460687995 - 0.8682234287 0.8743513823 0.8465524912 - 0.8686344028 0.8747605085 0.8470336199 - 0.8690453172 0.8751696944 0.8475146890 - 0.8694562912 0.8755788207 0.8479956985 - 0.8698672056 0.8759880066 0.8484768271 - 0.8702781200 0.8763970137 0.8489577174 - 0.8706849217 0.8768029213 0.8494340777 - 0.8710917830 0.8772087097 0.8499104977 - 0.8714985847 0.8776144981 0.8503869176 - 0.8719053864 0.8780202866 0.8508632183 - 0.8723123074 0.8784261942 0.8513395786 - 0.8727166057 0.8788295984 0.8518124223 - 0.8731204867 0.8792327046 0.8522846103 - 0.8735244274 0.8796356916 0.8527569175 - 0.8739284277 0.8800387979 0.8532291055 - 0.8743323088 0.8804419041 0.8537012935 - 0.8747352958 0.8808423877 0.8541712761 - 0.8751378059 0.8812415004 0.8546400070 - 0.8755401969 0.8816406131 0.8551086783 - 0.8759427071 0.8820397258 0.8555774093 - 0.8763452172 0.8824387789 0.8560460806 - 0.8767465949 0.8828365803 0.8565135002 - 0.8771464229 0.8832321167 0.8569787145 - 0.8775460720 0.8836277127 0.8574439287 - 0.8779459000 0.8840233088 0.8579090834 - 0.8783457279 0.8844187856 0.8583742976 - 0.8787450790 0.8848142028 0.8588392138 - 0.8791409135 0.8852068782 0.8593007922 - 0.8795366287 0.8855996132 0.8597623706 - 0.8799324036 0.8859922886 0.8602240086 - 0.8803281784 0.8863850236 0.8606855869 - 0.8807238936 0.8867778182 0.8611472249 - 0.8811178207 0.8871687055 0.8616060019 - 0.8815109730 0.8875589967 0.8620637059 - 0.8819041848 0.8879492879 0.8625215292 - 0.8822973967 0.8883395791 0.8629792929 - 0.8826907277 0.8887298703 0.8634371161 - 0.8830834031 0.8891189098 0.8638935089 - 0.8834751844 0.8895053864 0.8643475175 - 0.8838670850 0.8898919821 0.8648015261 - 0.8842589855 0.8902785778 0.8652554154 - 0.8846508265 0.8906651735 0.8657094240 - 0.8850427270 0.8910517097 0.8661633730 - 0.8854306936 0.8914347291 0.8666151762 - 0.8858184218 0.8918172717 0.8670668006 - 0.8862060905 0.8921998739 0.8675184846 - 0.8865936995 0.8925824761 0.8679701090 - 0.8869814277 0.8929650784 0.8684217930 - 0.8873677850 0.8933458924 0.8688719869 - 0.8877524734 0.8937243819 0.8693205714 - 0.8881371021 0.8941028714 0.8697690964 - 0.8885217905 0.8944814205 0.8702176213 - 0.8889064789 0.8948599100 0.8706660867 - 0.8892911077 0.8952383995 0.8711146712 - 0.8896743059 0.8956134915 0.8715596199 - 0.8900573850 0.8959884048 0.8720040917 - 0.8904405236 0.8963631988 0.8724486828 - 0.8908236027 0.8967379928 0.8728932142 - 0.8912066221 0.8971129060 0.8733376861 - 0.8915888071 0.8974866271 0.8737813830 - 0.8919690251 0.8978582025 0.8742234111 - 0.8923493028 0.8982297778 0.8746654987 - 0.8927295208 0.8986012936 0.8751075268 - 0.8931097984 0.8989729285 0.8755494952 - 0.8934900165 0.8993445039 0.8759915829 - 0.8938673735 0.8997135758 0.8764312267 - 0.8942434788 0.9000818133 0.8768699765 - 0.8946197033 0.9004498720 0.8773087263 - 0.8949959278 0.9008181095 0.8777474761 - 0.8953720927 0.9011862874 0.8781862855 - 0.8957480788 0.9015542269 0.8786247969 - 0.8961216211 0.9019187093 0.8790593743 - 0.8964951038 0.9022831917 0.8794940114 - 0.8968685865 0.9026476741 0.8799285889 - 0.8972421288 0.9030122161 0.8803632259 - 0.8976156116 0.9033766985 0.8807978034 - 0.8979886174 0.9037405849 0.8812314868 - 0.8983604908 0.9041035175 0.8816633821 - 0.8987324834 0.9044662714 0.8820952773 - 0.8991044760 0.9048290849 0.8825271726 - 0.8994765282 0.9051918983 0.8829591274 - 0.8998485208 0.9055547714 0.8833910823 - 0.9002189040 0.9059160948 0.8838210106 - 0.9005880952 0.9062762856 0.8842495084 - 0.9009574056 0.9066364765 0.8846780062 - 0.9013267159 0.9069967270 0.8851063848 - 0.9016959071 0.9073569179 0.8855348825 - 0.9020652175 0.9077171087 0.8859633803 - 0.9024326801 0.9080746174 0.8863885999 - 0.9027997851 0.9084312916 0.8868126869 - 0.9031668901 0.9087879062 0.8872368932 - 0.9035339952 0.9091445804 0.8876610994 - 0.9039011002 0.9095013142 0.8880853057 - 0.9042682052 0.9098579884 0.8885095119 - 0.9046338201 0.9102131128 0.8889309764 - 0.9049991965 0.9105681777 0.8893523812 - 0.9053646922 0.9109231830 0.8897737265 - 0.9057301283 0.9112783074 0.8901950717 - 0.9060956240 0.9116333723 0.8906164169 - 0.9064608812 0.9119883776 0.8910375834 - 0.9068235755 0.9123414755 0.8914554119 - 0.9071862102 0.9126946926 0.8918731809 - 0.9075487852 0.9130477905 0.8922908902 - 0.9079114795 0.9134010077 0.8927087188 - 0.9082741141 0.9137542248 0.8931264281 - 0.9086365104 0.9141070843 0.8935438991 - 0.9089967012 0.9144567251 0.8939571977 - 0.9093567729 0.9148061872 0.8943704963 - 0.9097169042 0.9151558280 0.8947837949 - 0.9100769758 0.9155054092 0.8951970935 - 0.9104372263 0.9158549905 0.8956103921 - 0.9107971787 0.9162042737 0.8960235119 - 0.9111555219 0.9165493846 0.8964337707 - 0.9115139246 0.9168946147 0.8968440890 - 0.9118723273 0.9172397256 0.8972544074 - 0.9122306108 0.9175848961 0.8976647258 - 0.9125890136 0.9179300070 0.8980749846 - 0.9129472971 0.9182751179 0.8984851837 - 0.9133043289 0.9186192155 0.8988931775 - 0.9136611819 0.9189633131 0.8993011713 - 0.9140180945 0.9193074107 0.8997092247 - 0.9143750072 0.9196515083 0.9001170993 - 0.9147319198 0.9199956059 0.9005250931 - 0.9150887728 0.9203397036 0.9009330869 - 0.9154434204 0.9206820130 0.9013389945 - 0.9157977104 0.9210242033 0.9017447233 - 0.9161520004 0.9213662744 0.9021503925 - 0.9165062904 0.9217085242 0.9025561810 - 0.9168605804 0.9220507145 0.9029619098 - 0.9172148705 0.9223927855 0.9033675790 - 0.9175676107 0.9227315784 0.9037716985 - 0.9179198146 0.9230694175 0.9041752815 - 0.9182720184 0.9234071970 0.9045788050 - 0.9186242223 0.9237450957 0.9049823880 - 0.9189764857 0.9240828753 0.9053859711 - 0.9193286896 0.9244207144 0.9057894945 - 0.9196799994 0.9247564077 0.9061918259 - 0.9200307131 0.9250903726 0.9065930843 - 0.9203813076 0.9254245162 0.9069944024 - 0.9207320213 0.9257584810 0.9073956013 - 0.9210826159 0.9260926247 0.9077969193 - 0.9214332104 0.9264267087 0.9081981778 - 0.9217830896 0.9267603755 0.9085987806 - 0.9221310019 0.9270933867 0.9089978933 - 0.9224789143 0.9274263978 0.9093968868 - 0.9228268862 0.9277595282 0.9097959995 - 0.9231747985 0.9280924797 0.9101951122 - 0.9235227704 0.9284254909 0.9105942249 - 0.9238706827 0.9287585020 0.9109932184 - 0.9242162108 0.9290888906 0.9113901854 - 0.9245617986 0.9294192791 0.9117870927 - 0.9249073267 0.9297497272 0.9121840000 - 0.9252527952 0.9300799966 0.9125810266 - 0.9255983829 0.9304103851 0.9129778743 - 0.9259439111 0.9307407737 0.9133749008 - 0.9262884259 0.9310684204 0.9137703776 - 0.9266322255 0.9313945174 0.9141650796 - 0.9269760847 0.9317206740 0.9145597816 - 0.9273198843 0.9320467710 0.9149544835 - 0.9276638031 0.9323729277 0.9153491855 - 0.9280076027 0.9326990247 0.9157438874 - 0.9283512235 0.9330248237 0.9161379933 - 0.9286935925 0.9333493114 0.9165306091 - 0.9290360808 0.9336737990 0.9169231057 - 0.9293785095 0.9339984059 0.9173156023 - 0.9297209978 0.9343228936 0.9177080989 - 0.9300634265 0.9346475005 0.9181007147 - 0.9304059148 0.9349719882 0.9184932113 - 0.9307463765 0.9352958202 0.9188840985 - 0.9310864210 0.9356192946 0.9192745090 - 0.9314262867 0.9359428883 0.9196649790 - 0.9317662716 0.9362664223 0.9200553894 - 0.9321063161 0.9365898967 0.9204457998 - 0.9324461818 0.9369134903 0.9208363295 - 0.9327856898 0.9372361898 0.9212262034 - 0.9331235886 0.9375563860 0.9216144085 - 0.9334616065 0.9378765821 0.9220026135 - 0.9337995052 0.9381968975 0.9223908186 - 0.9341375232 0.9385170937 0.9227790236 - 0.9344754219 0.9388374090 0.9231672883 - 0.9348133802 0.9391576052 0.9235554934 - 0.9351503253 0.9394760132 0.9239422083 - 0.9354866743 0.9397935271 0.9243283272 - 0.9358232021 0.9401109815 0.9247143865 - 0.9361596107 0.9404284954 0.9251005054 - 0.9364960790 0.9407460093 0.9254866242 - 0.9368324876 0.9410635233 0.9258726835 - 0.9371687770 0.9413809776 0.9262586236 - 0.9375026822 0.9416983128 0.9266427159 - 0.9378365278 0.9420157075 0.9270266891 - 0.9381703734 0.9423331022 0.9274107218 - 0.9385042191 0.9426503778 0.9277948141 - 0.9388381243 0.9429677725 0.9281787872 - 0.9391719103 0.9432851076 0.9285628200 - 0.9395049214 0.9436019063 0.9289460182 - 0.9398363829 0.9439175129 0.9293279052 - 0.9401680231 0.9442331791 0.9297096729 - 0.9404994845 0.9445489049 0.9300916195 - 0.9408311248 0.9448645115 0.9304735065 - 0.9411625862 0.9451801777 0.9308552742 - 0.9414941072 0.9454957843 0.9312372208 - 0.9418246746 0.9458090067 0.9316176772 - 0.9421545863 0.9461206794 0.9319974184 - 0.9424844980 0.9464324117 0.9323772192 - 0.9428144097 0.9467440844 0.9327569008 - 0.9431443810 0.9470558167 0.9331367016 - 0.9434742928 0.9473674893 0.9335163832 - 0.9438042045 0.9476792812 0.9338961840 - 0.9441329837 0.9479892850 0.9342741966 - 0.9444615841 0.9482989907 0.9346519709 - 0.9447901845 0.9486086965 0.9350296855 - 0.9451187849 0.9489185214 0.9354074001 - 0.9454473853 0.9492282271 0.9357851744 - 0.9457759261 0.9495378733 0.9361628890 - 0.9461045265 0.9498476982 0.9365406036 - 0.9464306831 0.9501572847 0.9369168878 - 0.9467568994 0.9504669905 0.9372932911 - 0.9470831156 0.9507766962 0.9376695752 - 0.9474092722 0.9510864019 0.9380459189 - 0.9477354884 0.9513961077 0.9384223223 - 0.9480617046 0.9517058134 0.9387986064 - 0.9483876824 0.9520152807 0.9391748905 - 0.9487118721 0.9523227215 0.9395511150 - 0.9490361810 0.9526301026 0.9399273992 - 0.9493604898 0.9529374838 0.9403036237 - 0.9496846795 0.9532449245 0.9406797886 - 0.9500089884 0.9535524249 0.9410560131 - 0.9503332973 0.9538598061 0.9414321780 - 0.9506573081 0.9541665912 0.9418082833 - 0.9509801865 0.9544696212 0.9421828985 - 0.9513030052 0.9547725916 0.9425575733 - 0.9516258240 0.9550755024 0.9429323077 - 0.9519485831 0.9553784728 0.9433069229 - 0.9522714019 0.9556815028 0.9436815977 - 0.9525942206 0.9559844136 0.9440562725 - 0.9529168010 0.9562870860 0.9444304109 - 0.9532371163 0.9565876126 0.9448000789 - 0.9535574913 0.9568880200 0.9451699257 - 0.9538778067 0.9571884871 0.9455395937 - 0.9541981220 0.9574888945 0.9459093809 - 0.9545183778 0.9577893019 0.9462791085 - 0.9548388124 0.9580898285 0.9466487765 - 0.9551590085 0.9583901763 0.9470183849 - 0.9554758072 0.9586884975 0.9473823905 - 0.9557926059 0.9589868784 0.9477463961 - 0.9561094046 0.9592853189 0.9481102824 - 0.9564262033 0.9595835805 0.9484742880 - 0.9567428827 0.9598820210 0.9488382936 - 0.9570596814 0.9601804018 0.9492021799 - 0.9573764801 0.9604787827 0.9495661855 - 0.9576882720 0.9607712030 0.9499266744 - 0.9579992890 0.9610629082 0.9502866864 - 0.9583104253 0.9613546133 0.9506466985 - 0.9586215019 0.9616461992 0.9510068297 - 0.9589325786 0.9619379044 0.9513667822 - 0.9592437148 0.9622296095 0.9517267942 - 0.9595547915 0.9625213146 0.9520869255 - 0.9598625898 0.9628093243 0.9524452090 - 0.9601690769 0.9630957246 0.9528027773 - 0.9604755044 0.9633821249 0.9531604052 - 0.9607818723 0.9636685252 0.9535179734 - 0.9610882998 0.9639549255 0.9538757205 - 0.9613947272 0.9642413259 0.9542332888 - 0.9617010951 0.9645277262 0.9545909166 - 0.9620065093 0.9648134112 0.9549475908 - 0.9623104930 0.9650980830 0.9553031921 - 0.9626144767 0.9653828144 0.9556587934 - 0.9629185200 0.9656674862 0.9560143948 - 0.9632223845 0.9659522176 0.9563699961 - 0.9635264277 0.9662368894 0.9567255974 - 0.9638304114 0.9665215015 0.9570811987 - 0.9641342163 0.9668059945 0.9574365020 - 0.9644362926 0.9670882821 0.9577893019 - 0.9647383094 0.9673706293 0.9581422210 - 0.9650403857 0.9676529169 0.9584950805 - 0.9653425217 0.9679353237 0.9588479996 - 0.9656445980 0.9682176113 0.9592007995 - 0.9659466743 0.9684998989 0.9595537186 - 0.9662488103 0.9687821865 0.9599065781 - 0.9665489793 0.9690619707 0.9602572918 - 0.9668484926 0.9693408012 0.9606072903 - 0.9671481252 0.9696195722 0.9609572887 - 0.9674475789 0.9698984027 0.9613072872 - 0.9677472115 0.9701771140 0.9616572261 - 0.9680467248 0.9704558849 0.9620072246 - 0.9683462977 0.9707347155 0.9623572230 - 0.9686446786 0.9710127711 0.9627063870 - 0.9689406157 0.9712892175 0.9630538225 - 0.9692364931 0.9715657234 0.9634011984 - 0.9695323706 0.9718421102 0.9637485743 - 0.9698283076 0.9721184969 0.9640960097 - 0.9701241851 0.9723948836 0.9644433856 - 0.9704201818 0.9726712704 0.9647908211 - 0.9707161188 0.9729477167 0.9651381969 - 0.9710099101 0.9732221961 0.9654831290 - 0.9713031054 0.9734960794 0.9658272862 - 0.9715964198 0.9737700224 0.9661715031 - 0.9718896151 0.9740439057 0.9665157199 - 0.9721829295 0.9743177891 0.9668598771 - 0.9724761248 0.9745916724 0.9672040939 - 0.9727693796 0.9748656750 0.9675483108 - 0.9730619788 0.9751387239 0.9678918123 - 0.9733523726 0.9754089713 0.9682325721 - 0.9736428857 0.9756792784 0.9685735106 - 0.9739332795 0.9759495854 0.9689142704 - 0.9742236733 0.9762198925 0.9692552090 - 0.9745141864 0.9764901996 0.9695960879 - 0.9748045802 0.9767605066 0.9699369073 - 0.9750950933 0.9770308137 0.9702777863 - 0.9753832817 0.9772984982 0.9706165791 - 0.9756699204 0.9775645137 0.9709541798 - 0.9759566188 0.9778305292 0.9712917209 - 0.9762431979 0.9780964851 0.9716293216 - 0.9765298963 0.9783625007 0.9719668031 - 0.9768165946 0.9786285162 0.9723044038 - 0.9771031737 0.9788944721 0.9726418853 - 0.9773898721 0.9791604877 0.9729794860 - 0.9776729941 0.9794232845 0.9733135104 - 0.9779558182 0.9796857238 0.9736471772 - 0.9782385826 0.9799482226 0.9739809036 - 0.9785215259 0.9802107215 0.9743145704 - 0.9788042903 0.9804731011 0.9746482968 - 0.9790871143 0.9807356000 0.9749820232 - 0.9793698788 0.9809979796 0.9753156900 - 0.9796521068 0.9812598825 0.9756488204 - 0.9799309969 0.9815183878 0.9759784937 - 0.9802098870 0.9817768931 0.9763082266 - 0.9804887772 0.9820353985 0.9766378999 - 0.9807677269 0.9822939038 0.9769675732 - 0.9810466766 0.9825525284 0.9772973061 - 0.9813256264 0.9828109741 0.9776269794 - 0.9816045165 0.9830694795 0.9779567122 - 0.9818817973 0.9833264947 0.9782847762 - 0.9821562171 0.9835805297 0.9786099792 - 0.9824305177 0.9838346243 0.9789351821 - 0.9827048182 0.9840885997 0.9792603850 - 0.9829791784 0.9843426943 0.9795857072 - 0.9832534790 0.9845967293 0.9799109101 - 0.9835278988 0.9848508239 0.9802361131 - 0.9838021994 0.9851047993 0.9805613160 - 0.9840744138 0.9853568077 0.9808841944 - 0.9843440056 0.9856063724 0.9812043905 - 0.9846137166 0.9858559966 0.9815247059 - 0.9848834276 0.9861056209 0.9818449020 - 0.9851530790 0.9863551855 0.9821652174 - 0.9854227901 0.9866049290 0.9824854136 - 0.9856923819 0.9868544936 0.9828057289 - 0.9859620929 0.9871041179 0.9831259251 - 0.9862291813 0.9873511195 0.9834434986 - 0.9864938259 0.9875954986 0.9837586880 - 0.9867584109 0.9878399968 0.9840738177 - 0.9870229959 0.9880844951 0.9843888879 - 0.9872875810 0.9883289933 0.9847040176 - 0.9875522256 0.9885734916 0.9850190878 - 0.9878168106 0.9888179898 0.9853342175 - 0.9880813956 0.9890624881 0.9856492877 - 0.9883427024 0.9893040061 0.9859611988 - 0.9886007905 0.9895424843 0.9862697124 - 0.9888588786 0.9897810817 0.9865782261 - 0.9891169071 0.9900196195 0.9868866801 - 0.9893749952 0.9902582169 0.9871953130 - 0.9896330833 0.9904968143 0.9875038266 - 0.9898911119 0.9907352924 0.9878122807 - 0.9901492000 0.9909738898 0.9881207943 - 0.9904044867 0.9912102818 0.9884263873 - 0.9906560183 0.9914438128 0.9887279868 - 0.9909074903 0.9916772246 0.9890295267 - 0.9911590219 0.9919106960 0.9893310070 - 0.9914104939 0.9921442270 0.9896326065 - 0.9916620851 0.9923775792 0.9899340868 - 0.9919136167 0.9926111102 0.9902356267 - 0.9921650887 0.9928445816 0.9905372262 - 0.9924147129 0.9930760264 0.9908366799 - 0.9926593900 0.9933021069 0.9911310077 - 0.9929041266 0.9935281873 0.9914252162 - 0.9931489229 0.9937543273 0.9917194843 - 0.9933936000 0.9939802885 0.9920136929 - 0.9936382771 0.9942064285 0.9923080206 - 0.9938830137 0.9944325089 0.9926022291 - 0.9941276908 0.9946585894 0.9928964972 - 0.9943717718 0.9948840141 0.9931902289 - 0.9946069717 0.9950991869 0.9934738874 - 0.9948421717 0.9953144193 0.9937577248 - 0.9950773716 0.9955294728 0.9940413833 - 0.9953125715 0.9957447052 0.9943252206 - 0.9955477715 0.9959598184 0.9946088791 - 0.9957829714 0.9961749911 0.9948927164 - 0.9960181713 0.9963901043 0.9951763749 - 0.9962533712 0.9966052771 0.9954602122 - 0.9964799881 0.9968116283 0.9957352877 - 0.9967042208 0.9970152974 0.9960079789 - 0.9969282746 0.9972190857 0.9962807298 - 0.9971525073 0.9974228144 0.9965534210 - 0.9973766208 0.9976264834 0.9968259931 - 0.9976006746 0.9978302121 0.9970986843 - 0.9978249073 0.9980338812 0.9973713756 - 0.9980490208 0.9982377291 0.9976441264 - 0.9982699156 0.9984378219 0.9979118109 - 0.9984861016 0.9986330867 0.9981728196 - 0.9987024069 0.9988284111 0.9984338880 - 0.9989187121 0.9990236163 0.9986948967 - 0.9991348982 0.9992188811 0.9989559054 - 0.9993512034 0.9994142056 0.9992169142 - 0.9995675087 0.9996094704 0.9994779825 - 0.9997836947 0.9998046756 0.9997389913 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0185100008 0.0164319202 0.0126006696 + 0.0311950799 0.0276685897 0.0221115705 + 0.0417797714 0.0372233018 0.0303079691 + 0.0512531996 0.0458445400 0.0377487317 + 0.0600418709 0.0538478307 0.0446756296 + 0.0683360174 0.0614236407 0.0512225516 + 0.0762501881 0.0686839372 0.0574930906 + 0.0838661790 0.0756844804 0.0635228232 + 0.0913166925 0.0824975967 0.0693578273 + 0.0986084193 0.0892031565 0.0750138834 + 0.1057450026 0.0958264992 0.0805220678 + 0.1127896011 0.1024202034 0.0859134421 + 0.1197260022 0.1089596003 0.0911877826 + 0.1265023947 0.1154130027 0.0963427573 + 0.1330948025 0.1217321977 0.1013797969 + 0.1394809932 0.1279036999 0.1063025966 + 0.1456708014 0.1339247972 0.1111140996 + 0.1516747028 0.1397916973 0.1158170998 + 0.1575195938 0.1455152035 0.1204319000 + 0.1632243991 0.1510861963 0.1249561980 + 0.1688113064 0.1565372944 0.1294070035 + 0.1742866039 0.1618628055 0.1337791979 + 0.1796312928 0.1670635045 0.1380856037 + 0.1848537028 0.1721491963 0.1423334032 + 0.1899529994 0.1771177948 0.1465034038 + 0.1949339062 0.1819808930 0.1506060064 + 0.1998192966 0.1867358983 0.1546511948 + 0.2046055943 0.1913896054 0.1586488932 + 0.2093106955 0.1959585994 0.1626013964 + 0.2139423043 0.2004473954 0.1665008068 + 0.2184966058 0.2048527002 0.1703405976 + 0.2229668945 0.2091723979 0.1741351932 + 0.2273671031 0.2134197950 0.1778849959 + 0.2316877991 0.2175989002 0.1815871000 + 0.2359403968 0.2217054963 0.1852401942 + 0.2401207983 0.2257481962 0.1888412982 + 0.2442404926 0.2297320962 0.1923972070 + 0.2482959926 0.2336588949 0.1959189028 + 0.2522906065 0.2375231981 0.1994097978 + 0.2562406063 0.2413284928 0.2028710991 + 0.2601419091 0.2450881004 0.2062990963 + 0.2639907897 0.2487992048 0.2096917033 + 0.2677909136 0.2524616122 0.2130511999 + 0.2715432942 0.2560823858 0.2163791060 + 0.2752490938 0.2596504092 0.2196782976 + 0.2789067924 0.2631703019 0.2229489982 + 0.2825185061 0.2666490972 0.2261860967 + 0.2860870957 0.2700851858 0.2293913960 + 0.2896127105 0.2734856904 0.2325628996 + 0.2930983901 0.2768476009 0.2357040942 + 0.2965483069 0.2801684141 0.2388191074 + 0.2999581099 0.2834582031 0.2419067025 + 0.3033216894 0.2867042124 0.2449615002 + 0.3066503108 0.2899151146 0.2479871958 + 0.3099426925 0.2930943966 0.2509889007 + 0.3132070005 0.2962363064 0.2539705932 + 0.3164420128 0.2993449867 0.2569276094 + 0.3196372092 0.3024170995 0.2598538995 + 0.3228017092 0.3054631948 0.2627541125 + 0.3259347975 0.3084770143 0.2656317949 + 0.3290332854 0.3114668131 0.2684907913 + 0.3321030140 0.3144359887 0.2713276148 + 0.3351433873 0.3173773885 0.2741436064 + 0.3381536007 0.3202837110 0.2769415081 + 0.3411369026 0.3231653869 0.2797215879 + 0.3440904915 0.3260202110 0.2824831903 + 0.3470189869 0.3288542926 0.2852219939 + 0.3499220014 0.3316698968 0.2879402041 + 0.3528007865 0.3344615102 0.2906357050 + 0.3556517959 0.3372254968 0.2933070958 + 0.3584778011 0.3399674892 0.2959615886 + 0.3612776995 0.3426899016 0.2985951900 + 0.3640502095 0.3453918099 0.3012051880 + 0.3667984903 0.3480693996 0.3037928045 + 0.3695279062 0.3507238925 0.3063583076 + 0.3722384870 0.3533582091 0.3089059889 + 0.3749296069 0.3559763134 0.3114354908 + 0.3776003122 0.3585732877 0.3139464855 + 0.3802514076 0.3611460924 0.3164429963 + 0.3828811944 0.3637025952 0.3189218938 + 0.3854868114 0.3662396967 0.3213779926 + 0.3880729079 0.3687601089 0.3238114119 + 0.3906360865 0.3712593019 0.3262237906 + 0.3931770027 0.3737432957 0.3286206126 + 0.3956986070 0.3762117028 0.3310020864 + 0.3982034028 0.3786646128 0.3333649933 + 0.4006921947 0.3811041117 0.3357118070 + 0.4031597972 0.3835237026 0.3380421996 + 0.4056158066 0.3859252036 0.3403576910 + 0.4080604017 0.3883082867 0.3426569998 + 0.4104911089 0.3906767964 0.3449401855 + 0.4129078090 0.3930304050 0.3472093046 + 0.4153085053 0.3953728974 0.3494597971 + 0.4176962078 0.3977056146 0.3516952097 + 0.4200707078 0.4000267088 0.3539173901 + 0.4224311113 0.4023346007 0.3561232984 + 0.4247764051 0.4046272933 0.3583106995 + 0.4271036088 0.4069069028 0.3604839146 + 0.4294143915 0.4091728926 0.3626427054 + 0.4317080081 0.4114241898 0.3647843897 + 0.4339860082 0.4136590064 0.3669061959 + 0.4362483025 0.4158793986 0.3690074980 + 0.4384947121 0.4180856943 0.3710916936 + 0.4407278895 0.4202806056 0.3731572032 + 0.4429492950 0.4224627912 0.3752071857 + 0.4451586008 0.4246312082 0.3772436082 + 0.4473553896 0.4267886877 0.3792651892 + 0.4495382905 0.4289326072 0.3812715113 + 0.4517078996 0.4310675859 0.3832643926 + 0.4538640976 0.4331924021 0.3852454126 + 0.4560067058 0.4353078902 0.3872154057 + 0.4581354856 0.4374152124 0.3891724944 + 0.4602501988 0.4395132959 0.3911151886 + 0.4623506069 0.4416010976 0.3930422068 + 0.4644348025 0.4436782002 0.3949519098 + 0.4665004909 0.4457429945 0.3968465030 + 0.4685483873 0.4477992058 0.3987269998 + 0.4705809951 0.4498446882 0.4005940855 + 0.4725970030 0.4518814087 0.4024456143 + 0.4745942950 0.4539059103 0.4042817056 + 0.4765745997 0.4559206069 0.4061039090 + 0.4785369933 0.4579218924 0.4079152048 + 0.4804831147 0.4599123001 0.4097140133 + 0.4824160933 0.4618884921 0.4115009010 + 0.4843369126 0.4638526142 0.4132741988 + 0.4862442017 0.4658051133 0.4150344133 + 0.4881368876 0.4677473903 0.4167799056 + 0.4900161922 0.4696780145 0.4185154140 + 0.4918822050 0.4715974033 0.4202412963 + 0.4937359989 0.4735072851 0.4219577014 + 0.4955778122 0.4754084051 0.4236656129 + 0.4974071980 0.4772998095 0.4253664017 + 0.4992232919 0.4791795909 0.4270611107 + 0.5010253191 0.4810472131 0.4287497997 + 0.5028162003 0.4829035103 0.4304314852 + 0.5045911074 0.4847469032 0.4321100116 + 0.5063520074 0.4865756035 0.4337812066 + 0.5080993772 0.4883880019 0.4354482889 + 0.5098353028 0.4901869893 0.4371086061 + 0.5115584135 0.4919714034 0.4387632906 + 0.5132681727 0.4937416911 0.4404115975 + 0.5149654746 0.4954991937 0.4420526028 + 0.5166510940 0.4972419143 0.4436877966 + 0.5183221102 0.4989711940 0.4453130066 + 0.5199794769 0.5006883740 0.4469313920 + 0.5216261744 0.5023959875 0.4485433996 + 0.5232623219 0.5040932894 0.4501496851 + 0.5248879194 0.5057805181 0.4517503083 + 0.5265035033 0.5074588060 0.4533442855 + 0.5281016231 0.5091217756 0.4549309015 + 0.5296890140 0.5107734203 0.4565131068 + 0.5312656760 0.5124161243 0.4580908120 + 0.5328325033 0.5140507221 0.4596638978 + 0.5343909860 0.5156767964 0.4612325132 + 0.5359356999 0.5172935128 0.4627946019 + 0.5374701023 0.5189024210 0.4643518031 + 0.5389943719 0.5205038190 0.4659039974 + 0.5405091047 0.5220975280 0.4674513042 + 0.5420141220 0.5236791968 0.4689922929 + 0.5435104966 0.5252534747 0.4705272019 + 0.5449982285 0.5268201232 0.4720568061 + 0.5464764833 0.5283790827 0.4735811949 + 0.5479413867 0.5299316049 0.4750989974 + 0.5493953824 0.5314735174 0.4766100943 + 0.5508413911 0.5330083966 0.4781157076 + 0.5522797704 0.5345366001 0.4796159863 + 0.5537052155 0.5360544920 0.4811086059 + 0.5551214218 0.5375661254 0.4825947881 + 0.5565286875 0.5390713811 0.4840748906 + 0.5579245090 0.5405685902 0.4855479002 + 0.5593087077 0.5420556068 0.4870094061 + 0.5606855154 0.5435367227 0.4884645045 + 0.5620552897 0.5450118780 0.4899129868 + 0.5634167790 0.5464770794 0.4913502932 + 0.5647714138 0.5479345918 0.4927789867 + 0.5661193728 0.5493863225 0.4941999018 + 0.5674555898 0.5508307815 0.4956082106 + 0.5687829256 0.5522680879 0.4970068932 + 0.5701043010 0.5537011027 0.4983989000 + 0.5714179873 0.5551288724 0.4997817874 + 0.5727239251 0.5565506816 0.5011560917 + 0.5740242004 0.5579689145 0.5025244951 + 0.5753160715 0.5593820214 0.5038843155 + 0.5766007900 0.5607866049 0.5052357912 + 0.5778805017 0.5621855855 0.5065827966 + 0.5791519880 0.5635759830 0.5079236031 + 0.5804169178 0.5649588108 0.5092585087 + 0.5816770792 0.5663369298 0.5105897188 + 0.5829272270 0.5677073002 0.5119146109 + 0.5841702223 0.5690726042 0.5132346749 + 0.5854083896 0.5704342723 0.5145516992 + 0.5866373777 0.5717855096 0.5158618093 + 0.5878623724 0.5731319785 0.5171685815 + 0.5890823007 0.5744730830 0.5184715986 + 0.5902937055 0.5758020282 0.5197688937 + 0.5915008187 0.5771260858 0.5210636258 + 0.5926988721 0.5784417987 0.5223534703 + 0.5938925147 0.5797501802 0.5236395001 + 0.5950831771 0.5810542107 0.5249232054 + 0.5962653160 0.5823497176 0.5262010098 + 0.5974434018 0.5836412907 0.5274763107 + 0.5986136198 0.5849254727 0.5287467241 + 0.5997778177 0.5862020850 0.5300114751 + 0.6009387970 0.5874748230 0.5312736034 + 0.6020932198 0.5887362957 0.5325291157 + 0.6032456160 0.5899950862 0.5337823033 + 0.6043936014 0.5912477970 0.5350301862 + 0.6055356264 0.5924956203 0.5362746119 + 0.6066730022 0.5937396884 0.5375164747 + 0.6078019142 0.5949761868 0.5387526155 + 0.6089282036 0.5962103009 0.5399869084 + 0.6100460291 0.5974360704 0.5412151814 + 0.6111602783 0.5986577868 0.5424402952 + 0.6122679114 0.5998724103 0.5436596870 + 0.6133708954 0.6010810733 0.5448756218 + 0.6144707203 0.6022859216 0.5460891724 + 0.6155622005 0.6034839749 0.5472962856 + 0.6166514754 0.6046805978 0.5485017896 + 0.6177281141 0.6058706045 0.5497025847 + 0.6188026071 0.6070585251 0.5509018898 + 0.6198686957 0.6082382798 0.5520957708 + 0.6209319830 0.6094141006 0.5532882214 + 0.6219903827 0.6105834842 0.5544781089 + 0.6230461001 0.6117473245 0.5556638837 + 0.6240993738 0.6129062772 0.5568463802 + 0.6251472831 0.6140604019 0.5580248833 + 0.6261926293 0.6152123213 0.5592017174 + 0.6272296906 0.6163583994 0.5603731871 + 0.6282647848 0.6175032258 0.5615437031 + 0.6292926073 0.6186398864 0.5627098083 + 0.6303197145 0.6197757125 0.5638754964 + 0.6313399076 0.6209014058 0.5650361180 + 0.6323593259 0.6220262051 0.5661963820 + 0.6333720088 0.6231431961 0.5673534870 + 0.6343832016 0.6242589951 0.5685095787 + 0.6353875995 0.6253690720 0.5696610212 + 0.6363899708 0.6264768839 0.5708104968 + 0.6373851895 0.6275768280 0.5719538927 + 0.6383780837 0.6286739707 0.5730956197 + 0.6393638253 0.6297637224 0.5742328763 + 0.6403476000 0.6308506727 0.5753682256 + 0.6413263083 0.6319305897 0.5764986873 + 0.6423032880 0.6330078840 0.5776277781 + 0.6432760954 0.6340783238 0.5787534118 + 0.6442463994 0.6351463199 0.5798763037 + 0.6452097297 0.6362072825 0.5809919834 + 0.6461709738 0.6372658014 0.5821056962 + 0.6471257210 0.6383160949 0.5832133293 + 0.6480792165 0.6393647790 0.5843191743 + 0.6490263939 0.6404061913 0.5854179859 + 0.6499727964 0.6414468288 0.5865159035 + 0.6509122849 0.6424813271 0.5876076818 + 0.6518514752 0.6435155869 0.5886991024 + 0.6527829170 0.6445450187 0.5897815824 + 0.6537142992 0.6455743909 0.5908641219 + 0.6546376944 0.6465963721 0.5919384956 + 0.6555603743 0.6476178765 0.5930123925 + 0.6564759016 0.6486306787 0.5940799117 + 0.6573897004 0.6496415138 0.5951458812 + 0.6582992077 0.6506463289 0.5962070227 + 0.6592066884 0.6516482234 0.5972657800 + 0.6601099968 0.6526457071 0.5983211994 + 0.6610093117 0.6536391973 0.5993734002 + 0.6619052887 0.6546294093 0.6004229784 + 0.6627947092 0.6556134224 0.6014679074 + 0.6636828780 0.6565961838 0.6025115252 + 0.6645650268 0.6575726271 0.6035487056 + 0.6654471159 0.6585490704 0.6045858264 + 0.6663234830 0.6595203876 0.6056172848 + 0.6671994925 0.6604915261 0.6066485047 + 0.6680691242 0.6614578962 0.6076750755 + 0.6689361930 0.6624224782 0.6086999774 + 0.6697993875 0.6633834243 0.6097220182 + 0.6706585288 0.6643403172 0.6107410192 + 0.6715161204 0.6652954221 0.6117582917 + 0.6723682880 0.6662437916 0.6127699018 + 0.6732205749 0.6671922207 0.6137815118 + 0.6740664840 0.6681342721 0.6147891283 + 0.6749119163 0.6690760255 0.6157965064 + 0.6757528782 0.6700139046 0.6167991757 + 0.6765912771 0.6709495783 0.6177995205 + 0.6774272919 0.6718820930 0.6187973022 + 0.6782582998 0.6728078723 0.6197900176 + 0.6790893078 0.6737337112 0.6207826734 + 0.6799132228 0.6746503115 0.6217687130 + 0.6807367802 0.6755667925 0.6227545738 + 0.6815574765 0.6764777899 0.6237366796 + 0.6823765039 0.6773855090 0.6247165203 + 0.6831939816 0.6782909036 0.6256946921 + 0.6840069294 0.6791892052 0.6266679764 + 0.6848198175 0.6800873876 0.6276412010 + 0.6856262088 0.6809793711 0.6286085844 + 0.6864315271 0.6818702221 0.6295750141 + 0.6872345805 0.6827588081 0.6305391788 + 0.6880347729 0.6836444139 0.6315007806 + 0.6888350248 0.6845300198 0.6324623823 + 0.6896290183 0.6854097843 0.6334151030 + 0.6904228926 0.6862894893 0.6343677044 + 0.6912125945 0.6871656179 0.6353166103 + 0.6919984818 0.6880385280 0.6362622976 + 0.6927841902 0.6889110208 0.6372076273 + 0.6935647726 0.6897763014 0.6381461024 + 0.6943454146 0.6906415820 0.6390845776 + 0.6951224208 0.6915038824 0.6400200725 + 0.6958963871 0.6923636794 0.6409534216 + 0.6966701746 0.6932234168 0.6418864131 + 0.6974359155 0.6940783858 0.6428129077 + 0.6982017159 0.6949334145 0.6437392831 + 0.6989647150 0.6957843900 0.6446622014 + 0.6997249126 0.6966313720 0.6455814838 + 0.7004849911 0.6974784732 0.6465008855 + 0.7012411952 0.6983184814 0.6474134922 + 0.7019971013 0.6991580725 0.6483256817 + 0.7027515173 0.6999952793 0.6492353082 + 0.7035031915 0.7008281946 0.6501401067 + 0.7042549253 0.7016611099 0.6510447860 + 0.7050036788 0.7024886012 0.6519446969 + 0.7057514191 0.7033143044 0.6528429985 + 0.7064986229 0.7041394114 0.6537404060 + 0.7072420716 0.7049599886 0.6546317935 + 0.7079855204 0.7057806253 0.6555231810 + 0.7087271214 0.7065981030 0.6564105749 + 0.7094665170 0.7074120045 0.6572937965 + 0.7102057934 0.7082259059 0.6581770182 + 0.7109420896 0.7090328932 0.6590545774 + 0.7116774917 0.7098382115 0.6599308252 + 0.7124124765 0.7106426954 0.6608062983 + 0.7131438255 0.7114415169 0.6616767049 + 0.7138749957 0.7122402787 0.6625469923 + 0.7146041989 0.7130368948 0.6634144187 + 0.7153301239 0.7138301134 0.6642774940 + 0.7160559893 0.7146233916 0.6651405096 + 0.7167779207 0.7154110074 0.6660001278 + 0.7174975872 0.7161955833 0.6668580174 + 0.7182173133 0.7169802785 0.6677159071 + 0.7189335227 0.7177579999 0.6685678959 + 0.7196493149 0.7185348868 0.6694192886 + 0.7203645110 0.7193109989 0.6702697873 + 0.7210751772 0.7200810909 0.6711146235 + 0.7217859030 0.7208511829 0.6719594002 + 0.7224943042 0.7216190100 0.6728019714 + 0.7231976986 0.7223820090 0.6736403108 + 0.7239010930 0.7231450081 0.6744784713 + 0.7246015072 0.7239043713 0.6753134131 + 0.7252988219 0.7246599793 0.6761450768 + 0.7259960771 0.7254155874 0.6769768000 + 0.7266904712 0.7261663079 0.6778048873 + 0.7273831964 0.7269141078 0.6786310077 + 0.7280758023 0.7276619077 0.6794571280 + 0.7287634015 0.7284036279 0.6802788973 + 0.7294492126 0.7291433215 0.6810994148 + 0.7301350832 0.7298830152 0.6819198132 + 0.7308161855 0.7306177020 0.6827344894 + 0.7314965725 0.7313514948 0.6835482121 + 0.7321768999 0.7320852876 0.6843619943 + 0.7328532934 0.7328147292 0.6851683259 + 0.7335293889 0.7335436940 0.6859741211 + 0.7342054844 0.7342727184 0.6867799163 + 0.7348755002 0.7349944711 0.6875799894 + 0.7355453968 0.7357162237 0.6883801222 + 0.7362151742 0.7364377975 0.6891800761 + 0.7368800044 0.7371528745 0.6899746060 + 0.7375447750 0.7378680706 0.6907690763 + 0.7382096052 0.7385832071 0.6915634871 + 0.7388706207 0.7392929196 0.6923534274 + 0.7395316958 0.7400025725 0.6931431890 + 0.7401927114 0.7407122850 0.6939331293 + 0.7408468127 0.7414141893 0.6947162747 + 0.7415009141 0.7421159744 0.6954995990 + 0.7421548963 0.7428178191 0.6962828040 + 0.7428026199 0.7435129285 0.6970598102 + 0.7434499264 0.7442076206 0.6978365183 + 0.7440971732 0.7449023128 0.6986131072 + 0.7447416782 0.7455918193 0.6993851066 + 0.7453858256 0.7462803721 0.7001565099 + 0.7460299134 0.7469691038 0.7009277940 + 0.7466691136 0.7476530075 0.7016950250 + 0.7473068833 0.7483354807 0.7024608850 + 0.7479445934 0.7490180135 0.7032269239 + 0.7485784292 0.7496981025 0.7039886713 + 0.7492100000 0.7503767014 0.7047483921 + 0.7498415709 0.7510554194 0.7055081129 + 0.7504718900 0.7517315149 0.7062641978 + 0.7511010170 0.7524052262 0.7070168853 + 0.7517300248 0.7530788183 0.7077695727 + 0.7523571253 0.7537503839 0.7085205913 + 0.7529804111 0.7544177175 0.7092683911 + 0.7536036968 0.7550848722 0.7100160718 + 0.7542262077 0.7557517290 0.7107632756 + 0.7548426986 0.7564147711 0.7115064859 + 0.7554591894 0.7570779920 0.7122498155 + 0.7560756803 0.7577410936 0.7129930258 + 0.7566884756 0.7583994865 0.7137312293 + 0.7573007941 0.7590574026 0.7144687772 + 0.7579131722 0.7597153187 0.7152063251 + 0.7585223913 0.7603682280 0.7159397006 + 0.7591298223 0.7610182762 0.7166706920 + 0.7597371936 0.7616683841 0.7174018025 + 0.7603425980 0.7623162270 0.7181308866 + 0.7609441280 0.7629598975 0.7188568115 + 0.7615455985 0.7636035085 0.7195826769 + 0.7621468902 0.7642468810 0.7203084230 + 0.7627450824 0.7648851871 0.7210304737 + 0.7633432150 0.7655234933 0.7217525244 + 0.7639414072 0.7661617994 0.7224745750 + 0.7645366192 0.7667945027 0.7231923938 + 0.7651305795 0.7674250007 0.7239086032 + 0.7657245994 0.7680554986 0.7246248126 + 0.7663164139 0.7686836720 0.7253388762 + 0.7669041157 0.7693076730 0.7260494232 + 0.7674916983 0.7699316144 0.7267599106 + 0.7680794001 0.7705556154 0.7274703979 + 0.7686625719 0.7711765170 0.7281758785 + 0.7692456245 0.7717971802 0.7288811207 + 0.7698286176 0.7724180222 0.7295863032 + 0.7704098821 0.7730364203 0.7302888036 + 0.7709895968 0.7736529112 0.7309889793 + 0.7715693116 0.7742694020 0.7316892147 + 0.7721486092 0.7748854160 0.7323889732 + 0.7727230191 0.7754955888 0.7330843210 + 0.7732973099 0.7761058211 0.7337794900 + 0.7738716006 0.7767159939 0.7344747782 + 0.7744429708 0.7773231864 0.7351666093 + 0.7750123739 0.7779282928 0.7358562946 + 0.7755817175 0.7785333991 0.7365459800 + 0.7761507034 0.7791380882 0.7372353077 + 0.7767155766 0.7797393203 0.7379208803 + 0.7772805095 0.7803406119 0.7386065722 + 0.7778455019 0.7809417844 0.7392922044 + 0.7784079909 0.7815397978 0.7399750948 + 0.7789685726 0.7821353078 0.7406558990 + 0.7795292139 0.7827306986 0.7413367033 + 0.7800899148 0.7833262086 0.7420175076 + 0.7806466818 0.7839170098 0.7426925898 + 0.7812033892 0.7845076919 0.7433677912 + 0.7817602158 0.7850984931 0.7440428734 + 0.7823156118 0.7856879234 0.7447155118 + 0.7828689814 0.7862753272 0.7453842759 + 0.7834224105 0.7868626118 0.7460532188 + 0.7839757204 0.7874500155 0.7467219830 + 0.7845265865 0.7880331874 0.7473868728 + 0.7850766778 0.7886152267 0.7480508089 + 0.7856268287 0.7891972065 0.7487146258 + 0.7861765027 0.7897787094 0.7493779063 + 0.7867226005 0.7903565168 0.7500371933 + 0.7872688174 0.7909343243 0.7506964803 + 0.7878149152 0.7915120721 0.7513558269 + 0.7883592248 0.7920885086 0.7520118952 + 0.7889010906 0.7926632762 0.7526640296 + 0.7894430161 0.7932379842 0.7533161044 + 0.7899848223 0.7938128114 0.7539681792 + 0.7905251980 0.7943832278 0.7546153069 + 0.7910649180 0.7949519753 0.7552605867 + 0.7916046977 0.7955207229 0.7559058070 + 0.7921444178 0.7960894704 0.7565510869 + 0.7926815152 0.7966526747 0.7571910024 + 0.7932183743 0.7972157001 0.7578307986 + 0.7937554121 0.7977787256 0.7584705949 + 0.7942914963 0.7983412147 0.7591093183 + 0.7948241234 0.7989016175 0.7597439289 + 0.7953568101 0.7994620800 0.7603784800 + 0.7958894968 0.8000226021 0.7610130906 + 0.7964202166 0.8005809188 0.7616454959 + 0.7969478965 0.8011360168 0.7622743845 + 0.7974755764 0.8016909957 0.7629033923 + 0.7980033159 0.8022460938 0.7635322809 + 0.7985295057 0.8027976751 0.7641590238 + 0.7990543842 0.8033463955 0.7647839189 + 0.7995793819 0.8038951755 0.7654088140 + 0.8001043200 0.8044440150 0.7660337090 + 0.8006263971 0.8049899936 0.7666553855 + 0.8011472225 0.8055347204 0.7672758102 + 0.8016679287 0.8060795069 0.7678961754 + 0.8021886945 0.8066241741 0.7685164809 + 0.8027051091 0.8071653843 0.7691326737 + 0.8032202721 0.8077054024 0.7697477937 + 0.8037353754 0.8082454801 0.7703629136 + 0.8042505980 0.8087856174 0.7709779143 + 0.8047627807 0.8093202114 0.7715882063 + 0.8052744269 0.8098536730 0.7721974850 + 0.8057860136 0.8103871942 0.7728068233 + 0.8062974811 0.8109207153 0.7734162211 + 0.8068054914 0.8114497066 0.7740216851 + 0.8073129058 0.8119781017 0.7746266723 + 0.8078203201 0.8125064969 0.7752317190 + 0.8083277941 0.8130347729 0.7758368254 + 0.8088303804 0.8135598898 0.7764384151 + 0.8093321919 0.8140844107 0.7770395875 + 0.8098341227 0.8146088719 0.7776407003 + 0.8103358746 0.8151333928 0.7782418728 + 0.8108342886 0.8156532049 0.7788380980 + 0.8113318086 0.8161720037 0.7794333100 + 0.8118293881 0.8166908026 0.7800284028 + 0.8123269081 0.8172097206 0.7806236148 + 0.8128222823 0.8177251816 0.7812144756 + 0.8133168817 0.8182396889 0.7818040252 + 0.8138114810 0.8187540770 0.7823935151 + 0.8143060803 0.8192685843 0.7829831243 + 0.8147979975 0.8197811246 0.7835692167 + 0.8152884245 0.8202924728 0.7841535807 + 0.8157786727 0.8208039999 0.7847380042 + 0.8162690997 0.8213154078 0.7853223085 + 0.8167579174 0.8218243122 0.7859038711 + 0.8172453046 0.8223307729 0.7864828110 + 0.8177325726 0.8228374124 0.7870616913 + 0.8182200193 0.8233438730 0.7876405716 + 0.8187068105 0.8238489032 0.7882180810 + 0.8191924095 0.8243508935 0.7887930274 + 0.8196780086 0.8248528838 0.7893679738 + 0.8201637268 0.8253548741 0.7899429202 + 0.8206487894 0.8258563876 0.7905169725 + 0.8211302757 0.8263542056 0.7910869122 + 0.8216118813 0.8268520832 0.7916566730 + 0.8220934272 0.8273500204 0.7922264934 + 0.8225749731 0.8278478980 0.7927963734 + 0.8230516911 0.8283423185 0.7933602929 + 0.8235278726 0.8288363218 0.7939236760 + 0.8240039945 0.8293302059 0.7944871187 + 0.8244801164 0.8298242092 0.7950503826 + 0.8249552846 0.8303152919 0.7956107259 + 0.8254299164 0.8308048248 0.7961691022 + 0.8259044886 0.8312941790 0.7967274785 + 0.8263791203 0.8317837119 0.7972859144 + 0.8268527985 0.8322715759 0.7978432178 + 0.8273245096 0.8327561021 0.7983981967 + 0.8277962208 0.8332406282 0.7989532948 + 0.8282678723 0.8337250948 0.7995082736 + 0.8287395835 0.8342096210 0.8000633717 + 0.8292062879 0.8346912861 0.8006147146 + 0.8296728134 0.8351728916 0.8011659980 + 0.8301392794 0.8356546164 0.8017172813 + 0.8306058049 0.8361362219 0.8022685051 + 0.8310710788 0.8366158009 0.8028172851 + 0.8315353990 0.8370940089 0.8033642769 + 0.8319997787 0.8375722170 0.8039113879 + 0.8324642181 0.8380503058 0.8044583797 + 0.8329282999 0.8385276794 0.8050045967 + 0.8333910704 0.8390009999 0.8055471182 + 0.8338539004 0.8394743204 0.8060895801 + 0.8343166113 0.8399475813 0.8066321015 + 0.8347793818 0.8404209018 0.8071746230 + 0.8352388144 0.8408915997 0.8077148795 + 0.8356966972 0.8413612843 0.8082543015 + 0.8361546993 0.8418310285 0.8087937832 + 0.8366125822 0.8423007131 0.8093332052 + 0.8370695710 0.8427699208 0.8098720908 + 0.8375232220 0.8432372808 0.8104090095 + 0.8379768133 0.8437047005 0.8109459281 + 0.8384304047 0.8441720009 0.8114827871 + 0.8388838768 0.8446394205 0.8120197058 + 0.8393365741 0.8451036811 0.8125535250 + 0.8397887945 0.8455665112 0.8130859137 + 0.8402410746 0.8460292220 0.8136183023 + 0.8406932950 0.8464919925 0.8141508102 + 0.8411452174 0.8469541073 0.8146827221 + 0.8415938020 0.8474109769 0.8152111769 + 0.8420425057 0.8478677869 0.8157396913 + 0.8424912095 0.8483247161 0.8162680864 + 0.8429399133 0.8487815261 0.8167966008 + 0.8433864713 0.8492373228 0.8173233867 + 0.8438311815 0.8496921062 0.8178485036 + 0.8442757726 0.8501468897 0.8183735013 + 0.8447204828 0.8506016731 0.8188986182 + 0.8451651931 0.8510563970 0.8194236755 + 0.8456082940 0.8515083194 0.8199456930 + 0.8460512161 0.8519595861 0.8204671741 + 0.8464940786 0.8524109125 0.8209887743 + 0.8469370008 0.8528622985 0.8215103149 + 0.8473795056 0.8533126116 0.8220310807 + 0.8478202820 0.8537582755 0.8225489855 + 0.8482609987 0.8542038798 0.8230667710 + 0.8487017155 0.8546496034 0.8235846162 + 0.8491423726 0.8550952077 0.8241025209 + 0.8495807052 0.8555396795 0.8246183991 + 0.8500161767 0.8559827209 0.8251321912 + 0.8504515886 0.8564257026 0.8256459236 + 0.8508870006 0.8568686247 0.8261595964 + 0.8513224125 0.8573116064 0.8266733885 + 0.8517556787 0.8577530980 0.8271840811 + 0.8521881104 0.8581939936 0.8276935816 + 0.8526204824 0.8586348295 0.8282030821 + 0.8530527949 0.8590756059 0.8287125826 + 0.8534852266 0.8595165014 0.8292220831 + 0.8539168239 0.8599526286 0.8297290802 + 0.8543483019 0.8603882194 0.8302358985 + 0.8547797799 0.8608238101 0.8307427168 + 0.8552113175 0.8612592816 0.8312494755 + 0.8556426764 0.8616946936 0.8317559958 + 0.8560702801 0.8621253967 0.8322589993 + 0.8564978242 0.8625560999 0.8327620029 + 0.8569254279 0.8629868031 0.8332650065 + 0.8573529720 0.8634175062 0.8337680101 + 0.8577799201 0.8638479114 0.8342701197 + 0.8582031131 0.8642767072 0.8347682953 + 0.8586263061 0.8647056222 0.8352664709 + 0.8590494990 0.8651344180 0.8357647061 + 0.8594726920 0.8655632138 0.8362628818 + 0.8598954082 0.8659911156 0.8367602229 + 0.8603163958 0.8664159179 0.8372551203 + 0.8607373834 0.8668407202 0.8377498984 + 0.8611584902 0.8672655225 0.8382447958 + 0.8615794778 0.8676903248 0.8387396932 + 0.8619998097 0.8681135774 0.8392338157 + 0.8624181747 0.8685330153 0.8397259712 + 0.8628365993 0.8689525127 0.8402181864 + 0.8632550836 0.8693720102 0.8407104015 + 0.8636735082 0.8697913885 0.8412026763 + 0.8640908003 0.8702098131 0.8416935802 + 0.8645054102 0.8706253171 0.8421815038 + 0.8649199009 0.8710408807 0.8426694274 + 0.8653343916 0.8714563847 0.8431572914 + 0.8657490015 0.8718720078 0.8436452150 + 0.8661630154 0.8722869754 0.8441321850 + 0.8665751219 0.8726999760 0.8446164131 + 0.8669872284 0.8731129766 0.8451005220 + 0.8673992753 0.8735259771 0.8455846906 + 0.8678115010 0.8739389777 0.8460687995 + 0.8682234287 0.8743513823 0.8465524912 + 0.8686344028 0.8747605085 0.8470336199 + 0.8690453172 0.8751696944 0.8475146890 + 0.8694562912 0.8755788207 0.8479956985 + 0.8698672056 0.8759880066 0.8484768271 + 0.8702781200 0.8763970137 0.8489577174 + 0.8706849217 0.8768029213 0.8494340777 + 0.8710917830 0.8772087097 0.8499104977 + 0.8714985847 0.8776144981 0.8503869176 + 0.8719053864 0.8780202866 0.8508632183 + 0.8723123074 0.8784261942 0.8513395786 + 0.8727166057 0.8788295984 0.8518124223 + 0.8731204867 0.8792327046 0.8522846103 + 0.8735244274 0.8796356916 0.8527569175 + 0.8739284277 0.8800387979 0.8532291055 + 0.8743323088 0.8804419041 0.8537012935 + 0.8747352958 0.8808423877 0.8541712761 + 0.8751378059 0.8812415004 0.8546400070 + 0.8755401969 0.8816406131 0.8551086783 + 0.8759427071 0.8820397258 0.8555774093 + 0.8763452172 0.8824387789 0.8560460806 + 0.8767465949 0.8828365803 0.8565135002 + 0.8771464229 0.8832321167 0.8569787145 + 0.8775460720 0.8836277127 0.8574439287 + 0.8779459000 0.8840233088 0.8579090834 + 0.8783457279 0.8844187856 0.8583742976 + 0.8787450790 0.8848142028 0.8588392138 + 0.8791409135 0.8852068782 0.8593007922 + 0.8795366287 0.8855996132 0.8597623706 + 0.8799324036 0.8859922886 0.8602240086 + 0.8803281784 0.8863850236 0.8606855869 + 0.8807238936 0.8867778182 0.8611472249 + 0.8811178207 0.8871687055 0.8616060019 + 0.8815109730 0.8875589967 0.8620637059 + 0.8819041848 0.8879492879 0.8625215292 + 0.8822973967 0.8883395791 0.8629792929 + 0.8826907277 0.8887298703 0.8634371161 + 0.8830834031 0.8891189098 0.8638935089 + 0.8834751844 0.8895053864 0.8643475175 + 0.8838670850 0.8898919821 0.8648015261 + 0.8842589855 0.8902785778 0.8652554154 + 0.8846508265 0.8906651735 0.8657094240 + 0.8850427270 0.8910517097 0.8661633730 + 0.8854306936 0.8914347291 0.8666151762 + 0.8858184218 0.8918172717 0.8670668006 + 0.8862060905 0.8921998739 0.8675184846 + 0.8865936995 0.8925824761 0.8679701090 + 0.8869814277 0.8929650784 0.8684217930 + 0.8873677850 0.8933458924 0.8688719869 + 0.8877524734 0.8937243819 0.8693205714 + 0.8881371021 0.8941028714 0.8697690964 + 0.8885217905 0.8944814205 0.8702176213 + 0.8889064789 0.8948599100 0.8706660867 + 0.8892911077 0.8952383995 0.8711146712 + 0.8896743059 0.8956134915 0.8715596199 + 0.8900573850 0.8959884048 0.8720040917 + 0.8904405236 0.8963631988 0.8724486828 + 0.8908236027 0.8967379928 0.8728932142 + 0.8912066221 0.8971129060 0.8733376861 + 0.8915888071 0.8974866271 0.8737813830 + 0.8919690251 0.8978582025 0.8742234111 + 0.8923493028 0.8982297778 0.8746654987 + 0.8927295208 0.8986012936 0.8751075268 + 0.8931097984 0.8989729285 0.8755494952 + 0.8934900165 0.8993445039 0.8759915829 + 0.8938673735 0.8997135758 0.8764312267 + 0.8942434788 0.9000818133 0.8768699765 + 0.8946197033 0.9004498720 0.8773087263 + 0.8949959278 0.9008181095 0.8777474761 + 0.8953720927 0.9011862874 0.8781862855 + 0.8957480788 0.9015542269 0.8786247969 + 0.8961216211 0.9019187093 0.8790593743 + 0.8964951038 0.9022831917 0.8794940114 + 0.8968685865 0.9026476741 0.8799285889 + 0.8972421288 0.9030122161 0.8803632259 + 0.8976156116 0.9033766985 0.8807978034 + 0.8979886174 0.9037405849 0.8812314868 + 0.8983604908 0.9041035175 0.8816633821 + 0.8987324834 0.9044662714 0.8820952773 + 0.8991044760 0.9048290849 0.8825271726 + 0.8994765282 0.9051918983 0.8829591274 + 0.8998485208 0.9055547714 0.8833910823 + 0.9002189040 0.9059160948 0.8838210106 + 0.9005880952 0.9062762856 0.8842495084 + 0.9009574056 0.9066364765 0.8846780062 + 0.9013267159 0.9069967270 0.8851063848 + 0.9016959071 0.9073569179 0.8855348825 + 0.9020652175 0.9077171087 0.8859633803 + 0.9024326801 0.9080746174 0.8863885999 + 0.9027997851 0.9084312916 0.8868126869 + 0.9031668901 0.9087879062 0.8872368932 + 0.9035339952 0.9091445804 0.8876610994 + 0.9039011002 0.9095013142 0.8880853057 + 0.9042682052 0.9098579884 0.8885095119 + 0.9046338201 0.9102131128 0.8889309764 + 0.9049991965 0.9105681777 0.8893523812 + 0.9053646922 0.9109231830 0.8897737265 + 0.9057301283 0.9112783074 0.8901950717 + 0.9060956240 0.9116333723 0.8906164169 + 0.9064608812 0.9119883776 0.8910375834 + 0.9068235755 0.9123414755 0.8914554119 + 0.9071862102 0.9126946926 0.8918731809 + 0.9075487852 0.9130477905 0.8922908902 + 0.9079114795 0.9134010077 0.8927087188 + 0.9082741141 0.9137542248 0.8931264281 + 0.9086365104 0.9141070843 0.8935438991 + 0.9089967012 0.9144567251 0.8939571977 + 0.9093567729 0.9148061872 0.8943704963 + 0.9097169042 0.9151558280 0.8947837949 + 0.9100769758 0.9155054092 0.8951970935 + 0.9104372263 0.9158549905 0.8956103921 + 0.9107971787 0.9162042737 0.8960235119 + 0.9111555219 0.9165493846 0.8964337707 + 0.9115139246 0.9168946147 0.8968440890 + 0.9118723273 0.9172397256 0.8972544074 + 0.9122306108 0.9175848961 0.8976647258 + 0.9125890136 0.9179300070 0.8980749846 + 0.9129472971 0.9182751179 0.8984851837 + 0.9133043289 0.9186192155 0.8988931775 + 0.9136611819 0.9189633131 0.8993011713 + 0.9140180945 0.9193074107 0.8997092247 + 0.9143750072 0.9196515083 0.9001170993 + 0.9147319198 0.9199956059 0.9005250931 + 0.9150887728 0.9203397036 0.9009330869 + 0.9154434204 0.9206820130 0.9013389945 + 0.9157977104 0.9210242033 0.9017447233 + 0.9161520004 0.9213662744 0.9021503925 + 0.9165062904 0.9217085242 0.9025561810 + 0.9168605804 0.9220507145 0.9029619098 + 0.9172148705 0.9223927855 0.9033675790 + 0.9175676107 0.9227315784 0.9037716985 + 0.9179198146 0.9230694175 0.9041752815 + 0.9182720184 0.9234071970 0.9045788050 + 0.9186242223 0.9237450957 0.9049823880 + 0.9189764857 0.9240828753 0.9053859711 + 0.9193286896 0.9244207144 0.9057894945 + 0.9196799994 0.9247564077 0.9061918259 + 0.9200307131 0.9250903726 0.9065930843 + 0.9203813076 0.9254245162 0.9069944024 + 0.9207320213 0.9257584810 0.9073956013 + 0.9210826159 0.9260926247 0.9077969193 + 0.9214332104 0.9264267087 0.9081981778 + 0.9217830896 0.9267603755 0.9085987806 + 0.9221310019 0.9270933867 0.9089978933 + 0.9224789143 0.9274263978 0.9093968868 + 0.9228268862 0.9277595282 0.9097959995 + 0.9231747985 0.9280924797 0.9101951122 + 0.9235227704 0.9284254909 0.9105942249 + 0.9238706827 0.9287585020 0.9109932184 + 0.9242162108 0.9290888906 0.9113901854 + 0.9245617986 0.9294192791 0.9117870927 + 0.9249073267 0.9297497272 0.9121840000 + 0.9252527952 0.9300799966 0.9125810266 + 0.9255983829 0.9304103851 0.9129778743 + 0.9259439111 0.9307407737 0.9133749008 + 0.9262884259 0.9310684204 0.9137703776 + 0.9266322255 0.9313945174 0.9141650796 + 0.9269760847 0.9317206740 0.9145597816 + 0.9273198843 0.9320467710 0.9149544835 + 0.9276638031 0.9323729277 0.9153491855 + 0.9280076027 0.9326990247 0.9157438874 + 0.9283512235 0.9330248237 0.9161379933 + 0.9286935925 0.9333493114 0.9165306091 + 0.9290360808 0.9336737990 0.9169231057 + 0.9293785095 0.9339984059 0.9173156023 + 0.9297209978 0.9343228936 0.9177080989 + 0.9300634265 0.9346475005 0.9181007147 + 0.9304059148 0.9349719882 0.9184932113 + 0.9307463765 0.9352958202 0.9188840985 + 0.9310864210 0.9356192946 0.9192745090 + 0.9314262867 0.9359428883 0.9196649790 + 0.9317662716 0.9362664223 0.9200553894 + 0.9321063161 0.9365898967 0.9204457998 + 0.9324461818 0.9369134903 0.9208363295 + 0.9327856898 0.9372361898 0.9212262034 + 0.9331235886 0.9375563860 0.9216144085 + 0.9334616065 0.9378765821 0.9220026135 + 0.9337995052 0.9381968975 0.9223908186 + 0.9341375232 0.9385170937 0.9227790236 + 0.9344754219 0.9388374090 0.9231672883 + 0.9348133802 0.9391576052 0.9235554934 + 0.9351503253 0.9394760132 0.9239422083 + 0.9354866743 0.9397935271 0.9243283272 + 0.9358232021 0.9401109815 0.9247143865 + 0.9361596107 0.9404284954 0.9251005054 + 0.9364960790 0.9407460093 0.9254866242 + 0.9368324876 0.9410635233 0.9258726835 + 0.9371687770 0.9413809776 0.9262586236 + 0.9375026822 0.9416983128 0.9266427159 + 0.9378365278 0.9420157075 0.9270266891 + 0.9381703734 0.9423331022 0.9274107218 + 0.9385042191 0.9426503778 0.9277948141 + 0.9388381243 0.9429677725 0.9281787872 + 0.9391719103 0.9432851076 0.9285628200 + 0.9395049214 0.9436019063 0.9289460182 + 0.9398363829 0.9439175129 0.9293279052 + 0.9401680231 0.9442331791 0.9297096729 + 0.9404994845 0.9445489049 0.9300916195 + 0.9408311248 0.9448645115 0.9304735065 + 0.9411625862 0.9451801777 0.9308552742 + 0.9414941072 0.9454957843 0.9312372208 + 0.9418246746 0.9458090067 0.9316176772 + 0.9421545863 0.9461206794 0.9319974184 + 0.9424844980 0.9464324117 0.9323772192 + 0.9428144097 0.9467440844 0.9327569008 + 0.9431443810 0.9470558167 0.9331367016 + 0.9434742928 0.9473674893 0.9335163832 + 0.9438042045 0.9476792812 0.9338961840 + 0.9441329837 0.9479892850 0.9342741966 + 0.9444615841 0.9482989907 0.9346519709 + 0.9447901845 0.9486086965 0.9350296855 + 0.9451187849 0.9489185214 0.9354074001 + 0.9454473853 0.9492282271 0.9357851744 + 0.9457759261 0.9495378733 0.9361628890 + 0.9461045265 0.9498476982 0.9365406036 + 0.9464306831 0.9501572847 0.9369168878 + 0.9467568994 0.9504669905 0.9372932911 + 0.9470831156 0.9507766962 0.9376695752 + 0.9474092722 0.9510864019 0.9380459189 + 0.9477354884 0.9513961077 0.9384223223 + 0.9480617046 0.9517058134 0.9387986064 + 0.9483876824 0.9520152807 0.9391748905 + 0.9487118721 0.9523227215 0.9395511150 + 0.9490361810 0.9526301026 0.9399273992 + 0.9493604898 0.9529374838 0.9403036237 + 0.9496846795 0.9532449245 0.9406797886 + 0.9500089884 0.9535524249 0.9410560131 + 0.9503332973 0.9538598061 0.9414321780 + 0.9506573081 0.9541665912 0.9418082833 + 0.9509801865 0.9544696212 0.9421828985 + 0.9513030052 0.9547725916 0.9425575733 + 0.9516258240 0.9550755024 0.9429323077 + 0.9519485831 0.9553784728 0.9433069229 + 0.9522714019 0.9556815028 0.9436815977 + 0.9525942206 0.9559844136 0.9440562725 + 0.9529168010 0.9562870860 0.9444304109 + 0.9532371163 0.9565876126 0.9448000789 + 0.9535574913 0.9568880200 0.9451699257 + 0.9538778067 0.9571884871 0.9455395937 + 0.9541981220 0.9574888945 0.9459093809 + 0.9545183778 0.9577893019 0.9462791085 + 0.9548388124 0.9580898285 0.9466487765 + 0.9551590085 0.9583901763 0.9470183849 + 0.9554758072 0.9586884975 0.9473823905 + 0.9557926059 0.9589868784 0.9477463961 + 0.9561094046 0.9592853189 0.9481102824 + 0.9564262033 0.9595835805 0.9484742880 + 0.9567428827 0.9598820210 0.9488382936 + 0.9570596814 0.9601804018 0.9492021799 + 0.9573764801 0.9604787827 0.9495661855 + 0.9576882720 0.9607712030 0.9499266744 + 0.9579992890 0.9610629082 0.9502866864 + 0.9583104253 0.9613546133 0.9506466985 + 0.9586215019 0.9616461992 0.9510068297 + 0.9589325786 0.9619379044 0.9513667822 + 0.9592437148 0.9622296095 0.9517267942 + 0.9595547915 0.9625213146 0.9520869255 + 0.9598625898 0.9628093243 0.9524452090 + 0.9601690769 0.9630957246 0.9528027773 + 0.9604755044 0.9633821249 0.9531604052 + 0.9607818723 0.9636685252 0.9535179734 + 0.9610882998 0.9639549255 0.9538757205 + 0.9613947272 0.9642413259 0.9542332888 + 0.9617010951 0.9645277262 0.9545909166 + 0.9620065093 0.9648134112 0.9549475908 + 0.9623104930 0.9650980830 0.9553031921 + 0.9626144767 0.9653828144 0.9556587934 + 0.9629185200 0.9656674862 0.9560143948 + 0.9632223845 0.9659522176 0.9563699961 + 0.9635264277 0.9662368894 0.9567255974 + 0.9638304114 0.9665215015 0.9570811987 + 0.9641342163 0.9668059945 0.9574365020 + 0.9644362926 0.9670882821 0.9577893019 + 0.9647383094 0.9673706293 0.9581422210 + 0.9650403857 0.9676529169 0.9584950805 + 0.9653425217 0.9679353237 0.9588479996 + 0.9656445980 0.9682176113 0.9592007995 + 0.9659466743 0.9684998989 0.9595537186 + 0.9662488103 0.9687821865 0.9599065781 + 0.9665489793 0.9690619707 0.9602572918 + 0.9668484926 0.9693408012 0.9606072903 + 0.9671481252 0.9696195722 0.9609572887 + 0.9674475789 0.9698984027 0.9613072872 + 0.9677472115 0.9701771140 0.9616572261 + 0.9680467248 0.9704558849 0.9620072246 + 0.9683462977 0.9707347155 0.9623572230 + 0.9686446786 0.9710127711 0.9627063870 + 0.9689406157 0.9712892175 0.9630538225 + 0.9692364931 0.9715657234 0.9634011984 + 0.9695323706 0.9718421102 0.9637485743 + 0.9698283076 0.9721184969 0.9640960097 + 0.9701241851 0.9723948836 0.9644433856 + 0.9704201818 0.9726712704 0.9647908211 + 0.9707161188 0.9729477167 0.9651381969 + 0.9710099101 0.9732221961 0.9654831290 + 0.9713031054 0.9734960794 0.9658272862 + 0.9715964198 0.9737700224 0.9661715031 + 0.9718896151 0.9740439057 0.9665157199 + 0.9721829295 0.9743177891 0.9668598771 + 0.9724761248 0.9745916724 0.9672040939 + 0.9727693796 0.9748656750 0.9675483108 + 0.9730619788 0.9751387239 0.9678918123 + 0.9733523726 0.9754089713 0.9682325721 + 0.9736428857 0.9756792784 0.9685735106 + 0.9739332795 0.9759495854 0.9689142704 + 0.9742236733 0.9762198925 0.9692552090 + 0.9745141864 0.9764901996 0.9695960879 + 0.9748045802 0.9767605066 0.9699369073 + 0.9750950933 0.9770308137 0.9702777863 + 0.9753832817 0.9772984982 0.9706165791 + 0.9756699204 0.9775645137 0.9709541798 + 0.9759566188 0.9778305292 0.9712917209 + 0.9762431979 0.9780964851 0.9716293216 + 0.9765298963 0.9783625007 0.9719668031 + 0.9768165946 0.9786285162 0.9723044038 + 0.9771031737 0.9788944721 0.9726418853 + 0.9773898721 0.9791604877 0.9729794860 + 0.9776729941 0.9794232845 0.9733135104 + 0.9779558182 0.9796857238 0.9736471772 + 0.9782385826 0.9799482226 0.9739809036 + 0.9785215259 0.9802107215 0.9743145704 + 0.9788042903 0.9804731011 0.9746482968 + 0.9790871143 0.9807356000 0.9749820232 + 0.9793698788 0.9809979796 0.9753156900 + 0.9796521068 0.9812598825 0.9756488204 + 0.9799309969 0.9815183878 0.9759784937 + 0.9802098870 0.9817768931 0.9763082266 + 0.9804887772 0.9820353985 0.9766378999 + 0.9807677269 0.9822939038 0.9769675732 + 0.9810466766 0.9825525284 0.9772973061 + 0.9813256264 0.9828109741 0.9776269794 + 0.9816045165 0.9830694795 0.9779567122 + 0.9818817973 0.9833264947 0.9782847762 + 0.9821562171 0.9835805297 0.9786099792 + 0.9824305177 0.9838346243 0.9789351821 + 0.9827048182 0.9840885997 0.9792603850 + 0.9829791784 0.9843426943 0.9795857072 + 0.9832534790 0.9845967293 0.9799109101 + 0.9835278988 0.9848508239 0.9802361131 + 0.9838021994 0.9851047993 0.9805613160 + 0.9840744138 0.9853568077 0.9808841944 + 0.9843440056 0.9856063724 0.9812043905 + 0.9846137166 0.9858559966 0.9815247059 + 0.9848834276 0.9861056209 0.9818449020 + 0.9851530790 0.9863551855 0.9821652174 + 0.9854227901 0.9866049290 0.9824854136 + 0.9856923819 0.9868544936 0.9828057289 + 0.9859620929 0.9871041179 0.9831259251 + 0.9862291813 0.9873511195 0.9834434986 + 0.9864938259 0.9875954986 0.9837586880 + 0.9867584109 0.9878399968 0.9840738177 + 0.9870229959 0.9880844951 0.9843888879 + 0.9872875810 0.9883289933 0.9847040176 + 0.9875522256 0.9885734916 0.9850190878 + 0.9878168106 0.9888179898 0.9853342175 + 0.9880813956 0.9890624881 0.9856492877 + 0.9883427024 0.9893040061 0.9859611988 + 0.9886007905 0.9895424843 0.9862697124 + 0.9888588786 0.9897810817 0.9865782261 + 0.9891169071 0.9900196195 0.9868866801 + 0.9893749952 0.9902582169 0.9871953130 + 0.9896330833 0.9904968143 0.9875038266 + 0.9898911119 0.9907352924 0.9878122807 + 0.9901492000 0.9909738898 0.9881207943 + 0.9904044867 0.9912102818 0.9884263873 + 0.9906560183 0.9914438128 0.9887279868 + 0.9909074903 0.9916772246 0.9890295267 + 0.9911590219 0.9919106960 0.9893310070 + 0.9914104939 0.9921442270 0.9896326065 + 0.9916620851 0.9923775792 0.9899340868 + 0.9919136167 0.9926111102 0.9902356267 + 0.9921650887 0.9928445816 0.9905372262 + 0.9924147129 0.9930760264 0.9908366799 + 0.9926593900 0.9933021069 0.9911310077 + 0.9929041266 0.9935281873 0.9914252162 + 0.9931489229 0.9937543273 0.9917194843 + 0.9933936000 0.9939802885 0.9920136929 + 0.9936382771 0.9942064285 0.9923080206 + 0.9938830137 0.9944325089 0.9926022291 + 0.9941276908 0.9946585894 0.9928964972 + 0.9943717718 0.9948840141 0.9931902289 + 0.9946069717 0.9950991869 0.9934738874 + 0.9948421717 0.9953144193 0.9937577248 + 0.9950773716 0.9955294728 0.9940413833 + 0.9953125715 0.9957447052 0.9943252206 + 0.9955477715 0.9959598184 0.9946088791 + 0.9957829714 0.9961749911 0.9948927164 + 0.9960181713 0.9963901043 0.9951763749 + 0.9962533712 0.9966052771 0.9954602122 + 0.9964799881 0.9968116283 0.9957352877 + 0.9967042208 0.9970152974 0.9960079789 + 0.9969282746 0.9972190857 0.9962807298 + 0.9971525073 0.9974228144 0.9965534210 + 0.9973766208 0.9976264834 0.9968259931 + 0.9976006746 0.9978302121 0.9970986843 + 0.9978249073 0.9980338812 0.9973713756 + 0.9980490208 0.9982377291 0.9976441264 + 0.9982699156 0.9984378219 0.9979118109 + 0.9984861016 0.9986330867 0.9981728196 + 0.9987024069 0.9988284111 0.9984338880 + 0.9989187121 0.9990236163 0.9986948967 + 0.9991348982 0.9992188811 0.9989559054 + 0.9993512034 0.9994142056 0.9992169142 + 0.9995675087 0.9996094704 0.9994779825 + 0.9997836947 0.9998046756 0.9997389913 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/canon_optura_981111.slrr.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/canon_optura_981111.slrr.spi1d index 2081fd8d..8798ebfc 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/canon_optura_981111.slrr.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/canon_optura_981111.slrr.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 1 -{ - 0.0000000000 - 0.0014726890 - 0.0029453780 - 0.0044180658 - 0.0058907550 - 0.0073634442 - 0.0088361334 - 0.0103471801 - 0.0120028201 - 0.0136584500 - 0.0153140901 - 0.0169697199 - 0.0186253507 - 0.0203142408 - 0.0221658293 - 0.0240174104 - 0.0258690007 - 0.0277205799 - 0.0295721702 - 0.0315769389 - 0.0336277485 - 0.0356785506 - 0.0377293602 - 0.0397801585 - 0.0420004018 - 0.0442409813 - 0.0464815497 - 0.0487221293 - 0.0510343090 - 0.0534415208 - 0.0558487400 - 0.0582559519 - 0.0606994517 - 0.0632383898 - 0.0657773167 - 0.0683162585 - 0.0708854869 - 0.0735143498 - 0.0761432126 - 0.0787720680 - 0.0814261734 - 0.0841023996 - 0.0867786184 - 0.0894548520 - 0.0921387821 - 0.0948246717 - 0.0975105688 - 0.1001949981 - 0.1028613001 - 0.1055274978 - 0.1081937999 - 0.1108473018 - 0.1134739965 - 0.1161006987 - 0.1187274009 - 0.1213281006 - 0.1239041984 - 0.1264802963 - 0.1290563941 - 0.1315979064 - 0.1341194063 - 0.1366408020 - 0.1391623020 - 0.1416479945 - 0.1441158056 - 0.1465836018 - 0.1490515023 - 0.1514890045 - 0.1539075971 - 0.1563262939 - 0.1587449014 - 0.1611427963 - 0.1635183990 - 0.1658940017 - 0.1682696044 - 0.1706354022 - 0.1729750037 - 0.1753146052 - 0.1776542068 - 0.1799937934 - 0.1823045015 - 0.1846151054 - 0.1869256943 - 0.1892361939 - 0.1915317029 - 0.1938195974 - 0.1961075068 - 0.1983955055 - 0.2006783038 - 0.2029488981 - 0.2052195966 - 0.2074902058 - 0.2097609043 - 0.2120196968 - 0.2142771035 - 0.2165344954 - 0.2187919021 - 0.2210443020 - 0.2232909948 - 0.2255375981 - 0.2277843058 - 0.2300308049 - 0.2322676033 - 0.2345044017 - 0.2367410958 - 0.2389778942 - 0.2412088960 - 0.2434349060 - 0.2456609011 - 0.2478868961 - 0.2501122952 - 0.2523249984 - 0.2545377910 - 0.2567506135 - 0.2589632869 - 0.2611669004 - 0.2633624077 - 0.2655578852 - 0.2677533925 - 0.2699488997 - 0.2721224129 - 0.2742953897 - 0.2764684856 - 0.2786414921 - 0.2808037996 - 0.2829482853 - 0.2850928009 - 0.2872372866 - 0.2893818021 - 0.2915014923 - 0.2936109900 - 0.2957204878 - 0.2978301048 - 0.2999396026 - 0.3020089865 - 0.3040772080 - 0.3061453998 - 0.3082135916 - 0.3102754056 - 0.3122963905 - 0.3143174946 - 0.3163385093 - 0.3183594942 - 0.3203707039 - 0.3223395944 - 0.3243085146 - 0.3262774050 - 0.3282462955 - 0.3302091062 - 0.3321220875 - 0.3340350986 - 0.3359481096 - 0.3378610909 - 0.3397741020 - 0.3416356146 - 0.3434903026 - 0.3453449905 - 0.3471997082 - 0.3490543962 - 0.3508799970 - 0.3526752889 - 0.3544706106 - 0.3562659025 - 0.3580613136 - 0.3598566055 - 0.3615975976 - 0.3633337915 - 0.3650701046 - 0.3668062985 - 0.3685424924 - 0.3702695072 - 0.3719482124 - 0.3736267984 - 0.3753054142 - 0.3769840896 - 0.3786627054 - 0.3803300858 - 0.3819536865 - 0.3835771978 - 0.3852007091 - 0.3868241906 - 0.3884477019 - 0.3900690079 - 0.3916406929 - 0.3932124078 - 0.3947840929 - 0.3963558972 - 0.3979276121 - 0.3994992971 - 0.4010384083 - 0.4025622904 - 0.4040861130 - 0.4056099951 - 0.4071337879 - 0.4086576998 - 0.4101763964 - 0.4116567969 - 0.4131371975 - 0.4146175981 - 0.4160979986 - 0.4175783992 - 0.4190588892 - 0.4205251932 - 0.4219667912 - 0.4234085083 - 0.4248501956 - 0.4262919128 - 0.4277335107 - 0.4291751981 - 0.4306024015 - 0.4320102930 - 0.4334180951 - 0.4348258972 - 0.4362337887 - 0.4376415908 - 0.4390495121 - 0.4404478967 - 0.4418269098 - 0.4432058930 - 0.4445849061 - 0.4459638894 - 0.4473429024 - 0.4487217963 - 0.4500991106 - 0.4514541924 - 0.4528093040 - 0.4541643858 - 0.4555194974 - 0.4568746090 - 0.4582296908 - 0.4595848024 - 0.4609268010 - 0.4622628987 - 0.4635990858 - 0.4649352133 - 0.4662713110 - 0.4676074982 - 0.4689435959 - 0.4702768028 - 0.4715988040 - 0.4729208052 - 0.4742426872 - 0.4755646884 - 0.4768866897 - 0.4782086909 - 0.4795306027 - 0.4808464944 - 0.4821588993 - 0.4834713042 - 0.4847837985 - 0.4860962033 - 0.4874086976 - 0.4887211025 - 0.4900333881 - 0.4913406968 - 0.4926480055 - 0.4939553142 - 0.4952625930 - 0.4965699017 - 0.4978772104 - 0.4991844893 - 0.5004913807 - 0.5017976761 - 0.5031039119 - 0.5044102073 - 0.5057165027 - 0.5070226789 - 0.5083289742 - 0.5096353292 - 0.5109434724 - 0.5122525096 - 0.5135614872 - 0.5148705244 - 0.5161795020 - 0.5174884796 - 0.5187975168 - 0.5201069117 - 0.5214219093 - 0.5227369070 - 0.5240519047 - 0.5253669024 - 0.5266819000 - 0.5279968977 - 0.5293118954 - 0.5306311250 - 0.5319548249 - 0.5332785249 - 0.5346022248 - 0.5359259844 - 0.5372496843 - 0.5385733843 - 0.5398970842 - 0.5412306786 - 0.5425651073 - 0.5438994765 - 0.5452339053 - 0.5465683937 - 0.5479028225 - 0.5492371917 - 0.5505766869 - 0.5519229174 - 0.5532690883 - 0.5546153188 - 0.5559614897 - 0.5573077202 - 0.5586538911 - 0.5600001812 - 0.5613582134 - 0.5627163053 - 0.5640742779 - 0.5654324293 - 0.5667904019 - 0.5681484938 - 0.5695065260 - 0.5708712935 - 0.5722399950 - 0.5736086965 - 0.5749773979 - 0.5763460994 - 0.5777148008 - 0.5790835023 - 0.5804548860 - 0.5818316936 - 0.5832085013 - 0.5845853090 - 0.5859621763 - 0.5873389840 - 0.5887157917 - 0.5900928974 - 0.5914738178 - 0.5928547978 - 0.5942357779 - 0.5956166983 - 0.5969976783 - 0.5983787179 - 0.5997595787 - 0.6011396050 - 0.6025193930 - 0.6038991809 - 0.6052790284 - 0.6066588163 - 0.6080386043 - 0.6094183922 - 0.6107937098 - 0.6121658087 - 0.6135379076 - 0.6149100065 - 0.6162821054 - 0.6176542044 - 0.6190263033 - 0.6203941107 - 0.6217511296 - 0.6231080890 - 0.6244651079 - 0.6258221269 - 0.6271792054 - 0.6285362244 - 0.6298931837 - 0.6312292218 - 0.6325632930 - 0.6338974833 - 0.6352316141 - 0.6365658045 - 0.6378998756 - 0.6392341256 - 0.6405552030 - 0.6418588161 - 0.6431624293 - 0.6444659829 - 0.6457697153 - 0.6470733285 - 0.6483768821 - 0.6496804953 - 0.6509557962 - 0.6522219181 - 0.6534879804 - 0.6547541022 - 0.6560202241 - 0.6572862864 - 0.6585524082 - 0.6598184109 - 0.6610473990 - 0.6622701883 - 0.6634929776 - 0.6647157073 - 0.6659384966 - 0.6671612859 - 0.6683840156 - 0.6696068048 - 0.6707972884 - 0.6719723940 - 0.6731474996 - 0.6743227243 - 0.6754977703 - 0.6766729951 - 0.6778481007 - 0.6790233254 - 0.6801899076 - 0.6813148260 - 0.6824398041 - 0.6835647225 - 0.6846895814 - 0.6858144999 - 0.6869394779 - 0.6880643964 - 0.6891893148 - 0.6902999878 - 0.6913738251 - 0.6924476027 - 0.6935213804 - 0.6945952773 - 0.6956691146 - 0.6967428923 - 0.6978167295 - 0.6988906264 - 0.6999644041 - 0.7009894848 - 0.7020128965 - 0.7030364275 - 0.7040597796 - 0.7050833106 - 0.7061067224 - 0.7071300745 - 0.7081536055 - 0.7091770172 - 0.7101910114 - 0.7111660838 - 0.7121412754 - 0.7131164074 - 0.7140914798 - 0.7150666714 - 0.7160418034 - 0.7170169950 - 0.7179921269 - 0.7189671993 - 0.7199423909 - 0.7208750844 - 0.7218052149 - 0.7227352262 - 0.7236652970 - 0.7245953083 - 0.7255253792 - 0.7264555097 - 0.7273855209 - 0.7283155918 - 0.7292456031 - 0.7301679254 - 0.7310569882 - 0.7319461107 - 0.7328351140 - 0.7337241769 - 0.7346132994 - 0.7355023026 - 0.7363914251 - 0.7372804880 - 0.7381694913 - 0.7390586138 - 0.7399476767 - 0.7408025861 - 0.7416555285 - 0.7425082922 - 0.7433611155 - 0.7442138791 - 0.7450668216 - 0.7459195852 - 0.7467724085 - 0.7476251721 - 0.7484781146 - 0.7493308783 - 0.7501770258 - 0.7509987950 - 0.7518206835 - 0.7526425123 - 0.7534642816 - 0.7542861104 - 0.7551078796 - 0.7559298277 - 0.7567515969 - 0.7575734258 - 0.7583951950 - 0.7592170238 - 0.7600377202 - 0.7608340979 - 0.7616304755 - 0.7624269128 - 0.7632232904 - 0.7640197873 - 0.7648162246 - 0.7656126022 - 0.7664089799 - 0.7672054768 - 0.7680019140 - 0.7687982917 - 0.7695947289 - 0.7703816295 - 0.7711585760 - 0.7719355226 - 0.7727125287 - 0.7734894156 - 0.7742664218 - 0.7750433087 - 0.7758203149 - 0.7765973210 - 0.7773742080 - 0.7781512141 - 0.7789281011 - 0.7797051072 - 0.7804738283 - 0.7812374830 - 0.7820013165 - 0.7827649713 - 0.7835286856 - 0.7842923999 - 0.7850561142 - 0.7858198881 - 0.7865836024 - 0.7873473167 - 0.7881109715 - 0.7888746858 - 0.7896384001 - 0.7903987169 - 0.7911556959 - 0.7919127941 - 0.7926697731 - 0.7934268713 - 0.7941839099 - 0.7949410081 - 0.7956979871 - 0.7964550853 - 0.7972121239 - 0.7979692221 - 0.7987263203 - 0.7994832993 - 0.8002405167 - 0.8009977937 - 0.8017551899 - 0.8025125861 - 0.8032699227 - 0.8040273190 - 0.8047845960 - 0.8055419922 - 0.8062993884 - 0.8070567250 - 0.8078141212 - 0.8085715175 - 0.8093287945 - 0.8100870848 - 0.8108522296 - 0.8116173148 - 0.8123824000 - 0.8131474853 - 0.8139125705 - 0.8146777749 - 0.8154429197 - 0.8162080050 - 0.8169730902 - 0.8177381754 - 0.8185033202 - 0.8192685246 - 0.8200343251 - 0.8208150864 - 0.8215959072 - 0.8223767281 - 0.8231574893 - 0.8239383101 - 0.8247190714 - 0.8255000114 - 0.8262807727 - 0.8270615935 - 0.8278424144 - 0.8286231756 - 0.8294039965 - 0.8301904798 - 0.8309953809 - 0.8318002820 - 0.8326051831 - 0.8334100246 - 0.8342149258 - 0.8350198269 - 0.8358247280 - 0.8366295099 - 0.8374344110 - 0.8382393122 - 0.8390442133 - 0.8398489952 - 0.8406803012 - 0.8415176272 - 0.8423550129 - 0.8431922793 - 0.8440296054 - 0.8448669910 - 0.8457043171 - 0.8465415835 - 0.8473790288 - 0.8482162952 - 0.8490536213 - 0.8498910069 - 0.8507630229 - 0.8516402841 - 0.8525176048 - 0.8533949256 - 0.8542721868 - 0.8551493883 - 0.8560267091 - 0.8569039702 - 0.8577812910 - 0.8586584926 - 0.8595358133 - 0.8604339957 - 0.8613557816 - 0.8622775078 - 0.8631992936 - 0.8641210198 - 0.8650426865 - 0.8659644723 - 0.8668861985 - 0.8678079844 - 0.8687297106 - 0.8696514964 - 0.8705996275 - 0.8715636730 - 0.8725277781 - 0.8734918833 - 0.8744559884 - 0.8754202127 - 0.8763843179 - 0.8773484230 - 0.8783125281 - 0.8792766929 - 0.8802478909 - 0.8812404275 - 0.8822329044 - 0.8832253814 - 0.8842179179 - 0.8852105141 - 0.8862029910 - 0.8871955276 - 0.8881880045 - 0.8891804814 - 0.8901726007 - 0.8911628127 - 0.8921530247 - 0.8931431174 - 0.8941333294 - 0.8951234818 - 0.8961136937 - 0.8971037865 - 0.8980939984 - 0.8990842104 - 0.9000707269 - 0.9010120034 - 0.9019532800 - 0.9028946161 - 0.9038360119 - 0.9047772884 - 0.9057186246 - 0.9066599011 - 0.9076012969 - 0.9085425735 - 0.9094839096 - 0.9103804827 - 0.9112226963 - 0.9120650291 - 0.9129071832 - 0.9137495160 - 0.9145917892 - 0.9154340029 - 0.9162762761 - 0.9171184897 - 0.9179608226 - 0.9188029766 - 0.9196453094 - 0.9204097986 - 0.9211177230 - 0.9218255877 - 0.9225335121 - 0.9232413769 - 0.9239493012 - 0.9246572256 - 0.9253650904 - 0.9260730147 - 0.9267808795 - 0.9274888039 - 0.9281967282 - 0.9289045930 - 0.9296125174 - 0.9302552938 - 0.9308195710 - 0.9313837886 - 0.9319481254 - 0.9325122833 - 0.9330765009 - 0.9336407781 - 0.9342049956 - 0.9347692132 - 0.9353334904 - 0.9358977079 - 0.9364619255 - 0.9370262027 - 0.9375904202 - 0.9381545782 - 0.9387189150 - 0.9392830729 - 0.9398472905 - 0.9403160810 - 0.9407495856 - 0.9411829710 - 0.9416164160 - 0.9420498013 - 0.9424831867 - 0.9429165721 - 0.9433500171 - 0.9437834024 - 0.9442167878 - 0.9446501732 - 0.9450836182 - 0.9455170035 - 0.9459503889 - 0.9463838935 - 0.9468172789 - 0.9472507238 - 0.9476841092 - 0.9481174946 - 0.9485508800 - 0.9489843249 - 0.9494177103 - 0.9498510957 - 0.9502140880 - 0.9505401850 - 0.9508662820 - 0.9511923790 - 0.9515184760 - 0.9518445730 - 0.9521707296 - 0.9524968266 - 0.9528229237 - 0.9531490207 - 0.9534751177 - 0.9538012147 - 0.9541273117 - 0.9544534087 - 0.9547795057 - 0.9551056027 - 0.9554316998 - 0.9557577968 - 0.9560838938 - 0.9564099908 - 0.9567360878 - 0.9570621848 - 0.9573882818 - 0.9577143788 - 0.9580404758 - 0.9583665729 - 0.9586927295 - 0.9590188265 - 0.9593449235 - 0.9596710205 - 0.9599971175 - 0.9602413177 - 0.9604846835 - 0.9607281089 - 0.9609714746 - 0.9612150192 - 0.9614583850 - 0.9617018104 - 0.9619451761 - 0.9621887207 - 0.9624320865 - 0.9626755118 - 0.9629188776 - 0.9631623030 - 0.9634057879 - 0.9636492133 - 0.9638925791 - 0.9641360044 - 0.9643794894 - 0.9646229148 - 0.9648662806 - 0.9651097059 - 0.9653531909 - 0.9655966163 - 0.9658399820 - 0.9660834074 - 0.9663268924 - 0.9665703177 - 0.9668136835 - 0.9670571089 - 0.9673005939 - 0.9675440192 - 0.9677873850 - 0.9680308104 - 0.9682741761 - 0.9685177207 - 0.9687610865 - 0.9690045118 - 0.9692478776 - 0.9694914222 - 0.9697347879 - 0.9699782133 - 0.9701654911 - 0.9703474045 - 0.9705291986 - 0.9707109928 - 0.9708927870 - 0.9710745811 - 0.9712563753 - 0.9714382291 - 0.9716200233 - 0.9718018770 - 0.9719836712 - 0.9721655250 - 0.9723473191 - 0.9725291133 - 0.9727109075 - 0.9728927016 - 0.9730746150 - 0.9732564092 - 0.9734382033 - 0.9736199975 - 0.9738017917 - 0.9739835858 - 0.9741653800 - 0.9743471742 - 0.9745290875 - 0.9747108817 - 0.9748926759 - 0.9750745296 - 0.9752563238 - 0.9754381180 - 0.9756199121 - 0.9758018255 - 0.9759836197 - 0.9761654139 - 0.9763472080 - 0.9765290022 - 0.9767107964 - 0.9768925905 - 0.9770743847 - 0.9772562981 - 0.9774380922 - 0.9776198864 - 0.9778016806 - 0.9779834747 - 0.9781653285 - 0.9783471227 - 0.9785289168 - 0.9787107706 - 0.9788926244 - 0.9790744185 - 0.9792562127 - 0.9794380069 - 0.9796198010 - 0.9798015952 - 0.9799835086 - 0.9801241159 - 0.9802607298 - 0.9803972840 - 0.9805337787 - 0.9806703925 - 0.9808070064 - 0.9809435010 - 0.9810801148 - 0.9812166095 - 0.9813532233 - 0.9814897776 - 0.9816262722 - 0.9817628860 - 0.9818994999 - 0.9820359945 - 0.9821726084 - 0.9823091030 - 0.9824457169 - 0.9825822711 - 0.9827188253 - 0.9828553796 - 0.9829919934 - 0.9831284881 - 0.9832651019 - 0.9834015965 - 0.9835382104 - 0.9836748242 - 0.9838113189 - 0.9839478731 - 0.9840844870 - 0.9842209816 - 0.9843575954 - 0.9844942093 - 0.9846307039 - 0.9847673178 - 0.9849038124 - 0.9850404263 - 0.9851769805 - 0.9853134751 - 0.9854500890 - 0.9855867028 - 0.9857231975 - 0.9858598113 - 0.9859963059 - 0.9861329198 - 0.9862694740 - 0.9864060283 - 0.9865425825 - 0.9866791964 - 0.9868156910 - 0.9869523048 - 0.9870887995 - 0.9872254133 - 0.9873620272 - 0.9874985218 - 0.9876350760 - 0.9877716899 - 0.9879081845 - 0.9880447984 - 0.9881814122 - 0.9883179069 - 0.9884545207 - 0.9885910153 - 0.9887276292 - 0.9888641834 - 0.9890006781 - 0.9891372919 - 0.9892739058 - 0.9894104004 - 0.9895470142 - 0.9896835089 - 0.9898201227 - 0.9899566770 - 0.9900705814 - 0.9901739955 - 0.9902775288 - 0.9903808832 - 0.9904842973 - 0.9905877709 - 0.9906911850 - 0.9907945991 - 0.9908980727 - 0.9910014868 - 0.9911049008 - 0.9912083745 - 0.9913117886 - 0.9914152026 - 0.9915186167 - 0.9916220903 - 0.9917255044 - 0.9918289185 - 0.9919323921 - 0.9920358062 - 0.9921392202 - 0.9922426939 - 0.9923461080 - 0.9924495220 - 0.9925529957 - 0.9926564097 - 0.9927598238 - 0.9928632975 - 0.9929667115 - 0.9930701256 - 0.9931734800 - 0.9932770133 - 0.9933804274 - 0.9934837818 - 0.9935873151 - 0.9936907291 - 0.9937940836 - 0.9938976169 - 0.9940009713 - 0.9941043854 - 0.9942079186 - 0.9943112731 - 0.9944146872 - 0.9945182204 - 0.9946215749 - 0.9947249889 - 0.9948284030 - 0.9949318767 - 0.9950352907 - 0.9951387048 - 0.9952421784 - 0.9953455925 - 0.9954490066 - 0.9955524802 - 0.9956558943 - 0.9957593083 - 0.9958627820 - 0.9959661961 - 0.9960696101 - 0.9961730242 - 0.9962764978 - 0.9963799119 - 0.9964833260 - 0.9965867996 - 0.9966902137 - 0.9967936277 - 0.9968971014 - 0.9970005155 - 0.9971039295 - 0.9972074032 - 0.9973108172 - 0.9974141717 - 0.9975177050 - 0.9976211190 - 0.9977244735 - 0.9978278875 - 0.9979314208 - 0.9980347753 - 0.9981381893 - 0.9982417226 - 0.9983450770 - 0.9984484911 - 0.9985520244 - 0.9986553788 - 0.9987587929 - 0.9988623261 - 0.9989656806 - 0.9990690947 - 0.9991726279 - 0.9992759824 - 0.9993793964 - 0.9994828105 - 0.9995862842 - 0.9996896982 - 0.9997931123 - 0.9998965859 - 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 1 +{ + 0.0000000000 + 0.0014726890 + 0.0029453780 + 0.0044180658 + 0.0058907550 + 0.0073634442 + 0.0088361334 + 0.0103471801 + 0.0120028201 + 0.0136584500 + 0.0153140901 + 0.0169697199 + 0.0186253507 + 0.0203142408 + 0.0221658293 + 0.0240174104 + 0.0258690007 + 0.0277205799 + 0.0295721702 + 0.0315769389 + 0.0336277485 + 0.0356785506 + 0.0377293602 + 0.0397801585 + 0.0420004018 + 0.0442409813 + 0.0464815497 + 0.0487221293 + 0.0510343090 + 0.0534415208 + 0.0558487400 + 0.0582559519 + 0.0606994517 + 0.0632383898 + 0.0657773167 + 0.0683162585 + 0.0708854869 + 0.0735143498 + 0.0761432126 + 0.0787720680 + 0.0814261734 + 0.0841023996 + 0.0867786184 + 0.0894548520 + 0.0921387821 + 0.0948246717 + 0.0975105688 + 0.1001949981 + 0.1028613001 + 0.1055274978 + 0.1081937999 + 0.1108473018 + 0.1134739965 + 0.1161006987 + 0.1187274009 + 0.1213281006 + 0.1239041984 + 0.1264802963 + 0.1290563941 + 0.1315979064 + 0.1341194063 + 0.1366408020 + 0.1391623020 + 0.1416479945 + 0.1441158056 + 0.1465836018 + 0.1490515023 + 0.1514890045 + 0.1539075971 + 0.1563262939 + 0.1587449014 + 0.1611427963 + 0.1635183990 + 0.1658940017 + 0.1682696044 + 0.1706354022 + 0.1729750037 + 0.1753146052 + 0.1776542068 + 0.1799937934 + 0.1823045015 + 0.1846151054 + 0.1869256943 + 0.1892361939 + 0.1915317029 + 0.1938195974 + 0.1961075068 + 0.1983955055 + 0.2006783038 + 0.2029488981 + 0.2052195966 + 0.2074902058 + 0.2097609043 + 0.2120196968 + 0.2142771035 + 0.2165344954 + 0.2187919021 + 0.2210443020 + 0.2232909948 + 0.2255375981 + 0.2277843058 + 0.2300308049 + 0.2322676033 + 0.2345044017 + 0.2367410958 + 0.2389778942 + 0.2412088960 + 0.2434349060 + 0.2456609011 + 0.2478868961 + 0.2501122952 + 0.2523249984 + 0.2545377910 + 0.2567506135 + 0.2589632869 + 0.2611669004 + 0.2633624077 + 0.2655578852 + 0.2677533925 + 0.2699488997 + 0.2721224129 + 0.2742953897 + 0.2764684856 + 0.2786414921 + 0.2808037996 + 0.2829482853 + 0.2850928009 + 0.2872372866 + 0.2893818021 + 0.2915014923 + 0.2936109900 + 0.2957204878 + 0.2978301048 + 0.2999396026 + 0.3020089865 + 0.3040772080 + 0.3061453998 + 0.3082135916 + 0.3102754056 + 0.3122963905 + 0.3143174946 + 0.3163385093 + 0.3183594942 + 0.3203707039 + 0.3223395944 + 0.3243085146 + 0.3262774050 + 0.3282462955 + 0.3302091062 + 0.3321220875 + 0.3340350986 + 0.3359481096 + 0.3378610909 + 0.3397741020 + 0.3416356146 + 0.3434903026 + 0.3453449905 + 0.3471997082 + 0.3490543962 + 0.3508799970 + 0.3526752889 + 0.3544706106 + 0.3562659025 + 0.3580613136 + 0.3598566055 + 0.3615975976 + 0.3633337915 + 0.3650701046 + 0.3668062985 + 0.3685424924 + 0.3702695072 + 0.3719482124 + 0.3736267984 + 0.3753054142 + 0.3769840896 + 0.3786627054 + 0.3803300858 + 0.3819536865 + 0.3835771978 + 0.3852007091 + 0.3868241906 + 0.3884477019 + 0.3900690079 + 0.3916406929 + 0.3932124078 + 0.3947840929 + 0.3963558972 + 0.3979276121 + 0.3994992971 + 0.4010384083 + 0.4025622904 + 0.4040861130 + 0.4056099951 + 0.4071337879 + 0.4086576998 + 0.4101763964 + 0.4116567969 + 0.4131371975 + 0.4146175981 + 0.4160979986 + 0.4175783992 + 0.4190588892 + 0.4205251932 + 0.4219667912 + 0.4234085083 + 0.4248501956 + 0.4262919128 + 0.4277335107 + 0.4291751981 + 0.4306024015 + 0.4320102930 + 0.4334180951 + 0.4348258972 + 0.4362337887 + 0.4376415908 + 0.4390495121 + 0.4404478967 + 0.4418269098 + 0.4432058930 + 0.4445849061 + 0.4459638894 + 0.4473429024 + 0.4487217963 + 0.4500991106 + 0.4514541924 + 0.4528093040 + 0.4541643858 + 0.4555194974 + 0.4568746090 + 0.4582296908 + 0.4595848024 + 0.4609268010 + 0.4622628987 + 0.4635990858 + 0.4649352133 + 0.4662713110 + 0.4676074982 + 0.4689435959 + 0.4702768028 + 0.4715988040 + 0.4729208052 + 0.4742426872 + 0.4755646884 + 0.4768866897 + 0.4782086909 + 0.4795306027 + 0.4808464944 + 0.4821588993 + 0.4834713042 + 0.4847837985 + 0.4860962033 + 0.4874086976 + 0.4887211025 + 0.4900333881 + 0.4913406968 + 0.4926480055 + 0.4939553142 + 0.4952625930 + 0.4965699017 + 0.4978772104 + 0.4991844893 + 0.5004913807 + 0.5017976761 + 0.5031039119 + 0.5044102073 + 0.5057165027 + 0.5070226789 + 0.5083289742 + 0.5096353292 + 0.5109434724 + 0.5122525096 + 0.5135614872 + 0.5148705244 + 0.5161795020 + 0.5174884796 + 0.5187975168 + 0.5201069117 + 0.5214219093 + 0.5227369070 + 0.5240519047 + 0.5253669024 + 0.5266819000 + 0.5279968977 + 0.5293118954 + 0.5306311250 + 0.5319548249 + 0.5332785249 + 0.5346022248 + 0.5359259844 + 0.5372496843 + 0.5385733843 + 0.5398970842 + 0.5412306786 + 0.5425651073 + 0.5438994765 + 0.5452339053 + 0.5465683937 + 0.5479028225 + 0.5492371917 + 0.5505766869 + 0.5519229174 + 0.5532690883 + 0.5546153188 + 0.5559614897 + 0.5573077202 + 0.5586538911 + 0.5600001812 + 0.5613582134 + 0.5627163053 + 0.5640742779 + 0.5654324293 + 0.5667904019 + 0.5681484938 + 0.5695065260 + 0.5708712935 + 0.5722399950 + 0.5736086965 + 0.5749773979 + 0.5763460994 + 0.5777148008 + 0.5790835023 + 0.5804548860 + 0.5818316936 + 0.5832085013 + 0.5845853090 + 0.5859621763 + 0.5873389840 + 0.5887157917 + 0.5900928974 + 0.5914738178 + 0.5928547978 + 0.5942357779 + 0.5956166983 + 0.5969976783 + 0.5983787179 + 0.5997595787 + 0.6011396050 + 0.6025193930 + 0.6038991809 + 0.6052790284 + 0.6066588163 + 0.6080386043 + 0.6094183922 + 0.6107937098 + 0.6121658087 + 0.6135379076 + 0.6149100065 + 0.6162821054 + 0.6176542044 + 0.6190263033 + 0.6203941107 + 0.6217511296 + 0.6231080890 + 0.6244651079 + 0.6258221269 + 0.6271792054 + 0.6285362244 + 0.6298931837 + 0.6312292218 + 0.6325632930 + 0.6338974833 + 0.6352316141 + 0.6365658045 + 0.6378998756 + 0.6392341256 + 0.6405552030 + 0.6418588161 + 0.6431624293 + 0.6444659829 + 0.6457697153 + 0.6470733285 + 0.6483768821 + 0.6496804953 + 0.6509557962 + 0.6522219181 + 0.6534879804 + 0.6547541022 + 0.6560202241 + 0.6572862864 + 0.6585524082 + 0.6598184109 + 0.6610473990 + 0.6622701883 + 0.6634929776 + 0.6647157073 + 0.6659384966 + 0.6671612859 + 0.6683840156 + 0.6696068048 + 0.6707972884 + 0.6719723940 + 0.6731474996 + 0.6743227243 + 0.6754977703 + 0.6766729951 + 0.6778481007 + 0.6790233254 + 0.6801899076 + 0.6813148260 + 0.6824398041 + 0.6835647225 + 0.6846895814 + 0.6858144999 + 0.6869394779 + 0.6880643964 + 0.6891893148 + 0.6902999878 + 0.6913738251 + 0.6924476027 + 0.6935213804 + 0.6945952773 + 0.6956691146 + 0.6967428923 + 0.6978167295 + 0.6988906264 + 0.6999644041 + 0.7009894848 + 0.7020128965 + 0.7030364275 + 0.7040597796 + 0.7050833106 + 0.7061067224 + 0.7071300745 + 0.7081536055 + 0.7091770172 + 0.7101910114 + 0.7111660838 + 0.7121412754 + 0.7131164074 + 0.7140914798 + 0.7150666714 + 0.7160418034 + 0.7170169950 + 0.7179921269 + 0.7189671993 + 0.7199423909 + 0.7208750844 + 0.7218052149 + 0.7227352262 + 0.7236652970 + 0.7245953083 + 0.7255253792 + 0.7264555097 + 0.7273855209 + 0.7283155918 + 0.7292456031 + 0.7301679254 + 0.7310569882 + 0.7319461107 + 0.7328351140 + 0.7337241769 + 0.7346132994 + 0.7355023026 + 0.7363914251 + 0.7372804880 + 0.7381694913 + 0.7390586138 + 0.7399476767 + 0.7408025861 + 0.7416555285 + 0.7425082922 + 0.7433611155 + 0.7442138791 + 0.7450668216 + 0.7459195852 + 0.7467724085 + 0.7476251721 + 0.7484781146 + 0.7493308783 + 0.7501770258 + 0.7509987950 + 0.7518206835 + 0.7526425123 + 0.7534642816 + 0.7542861104 + 0.7551078796 + 0.7559298277 + 0.7567515969 + 0.7575734258 + 0.7583951950 + 0.7592170238 + 0.7600377202 + 0.7608340979 + 0.7616304755 + 0.7624269128 + 0.7632232904 + 0.7640197873 + 0.7648162246 + 0.7656126022 + 0.7664089799 + 0.7672054768 + 0.7680019140 + 0.7687982917 + 0.7695947289 + 0.7703816295 + 0.7711585760 + 0.7719355226 + 0.7727125287 + 0.7734894156 + 0.7742664218 + 0.7750433087 + 0.7758203149 + 0.7765973210 + 0.7773742080 + 0.7781512141 + 0.7789281011 + 0.7797051072 + 0.7804738283 + 0.7812374830 + 0.7820013165 + 0.7827649713 + 0.7835286856 + 0.7842923999 + 0.7850561142 + 0.7858198881 + 0.7865836024 + 0.7873473167 + 0.7881109715 + 0.7888746858 + 0.7896384001 + 0.7903987169 + 0.7911556959 + 0.7919127941 + 0.7926697731 + 0.7934268713 + 0.7941839099 + 0.7949410081 + 0.7956979871 + 0.7964550853 + 0.7972121239 + 0.7979692221 + 0.7987263203 + 0.7994832993 + 0.8002405167 + 0.8009977937 + 0.8017551899 + 0.8025125861 + 0.8032699227 + 0.8040273190 + 0.8047845960 + 0.8055419922 + 0.8062993884 + 0.8070567250 + 0.8078141212 + 0.8085715175 + 0.8093287945 + 0.8100870848 + 0.8108522296 + 0.8116173148 + 0.8123824000 + 0.8131474853 + 0.8139125705 + 0.8146777749 + 0.8154429197 + 0.8162080050 + 0.8169730902 + 0.8177381754 + 0.8185033202 + 0.8192685246 + 0.8200343251 + 0.8208150864 + 0.8215959072 + 0.8223767281 + 0.8231574893 + 0.8239383101 + 0.8247190714 + 0.8255000114 + 0.8262807727 + 0.8270615935 + 0.8278424144 + 0.8286231756 + 0.8294039965 + 0.8301904798 + 0.8309953809 + 0.8318002820 + 0.8326051831 + 0.8334100246 + 0.8342149258 + 0.8350198269 + 0.8358247280 + 0.8366295099 + 0.8374344110 + 0.8382393122 + 0.8390442133 + 0.8398489952 + 0.8406803012 + 0.8415176272 + 0.8423550129 + 0.8431922793 + 0.8440296054 + 0.8448669910 + 0.8457043171 + 0.8465415835 + 0.8473790288 + 0.8482162952 + 0.8490536213 + 0.8498910069 + 0.8507630229 + 0.8516402841 + 0.8525176048 + 0.8533949256 + 0.8542721868 + 0.8551493883 + 0.8560267091 + 0.8569039702 + 0.8577812910 + 0.8586584926 + 0.8595358133 + 0.8604339957 + 0.8613557816 + 0.8622775078 + 0.8631992936 + 0.8641210198 + 0.8650426865 + 0.8659644723 + 0.8668861985 + 0.8678079844 + 0.8687297106 + 0.8696514964 + 0.8705996275 + 0.8715636730 + 0.8725277781 + 0.8734918833 + 0.8744559884 + 0.8754202127 + 0.8763843179 + 0.8773484230 + 0.8783125281 + 0.8792766929 + 0.8802478909 + 0.8812404275 + 0.8822329044 + 0.8832253814 + 0.8842179179 + 0.8852105141 + 0.8862029910 + 0.8871955276 + 0.8881880045 + 0.8891804814 + 0.8901726007 + 0.8911628127 + 0.8921530247 + 0.8931431174 + 0.8941333294 + 0.8951234818 + 0.8961136937 + 0.8971037865 + 0.8980939984 + 0.8990842104 + 0.9000707269 + 0.9010120034 + 0.9019532800 + 0.9028946161 + 0.9038360119 + 0.9047772884 + 0.9057186246 + 0.9066599011 + 0.9076012969 + 0.9085425735 + 0.9094839096 + 0.9103804827 + 0.9112226963 + 0.9120650291 + 0.9129071832 + 0.9137495160 + 0.9145917892 + 0.9154340029 + 0.9162762761 + 0.9171184897 + 0.9179608226 + 0.9188029766 + 0.9196453094 + 0.9204097986 + 0.9211177230 + 0.9218255877 + 0.9225335121 + 0.9232413769 + 0.9239493012 + 0.9246572256 + 0.9253650904 + 0.9260730147 + 0.9267808795 + 0.9274888039 + 0.9281967282 + 0.9289045930 + 0.9296125174 + 0.9302552938 + 0.9308195710 + 0.9313837886 + 0.9319481254 + 0.9325122833 + 0.9330765009 + 0.9336407781 + 0.9342049956 + 0.9347692132 + 0.9353334904 + 0.9358977079 + 0.9364619255 + 0.9370262027 + 0.9375904202 + 0.9381545782 + 0.9387189150 + 0.9392830729 + 0.9398472905 + 0.9403160810 + 0.9407495856 + 0.9411829710 + 0.9416164160 + 0.9420498013 + 0.9424831867 + 0.9429165721 + 0.9433500171 + 0.9437834024 + 0.9442167878 + 0.9446501732 + 0.9450836182 + 0.9455170035 + 0.9459503889 + 0.9463838935 + 0.9468172789 + 0.9472507238 + 0.9476841092 + 0.9481174946 + 0.9485508800 + 0.9489843249 + 0.9494177103 + 0.9498510957 + 0.9502140880 + 0.9505401850 + 0.9508662820 + 0.9511923790 + 0.9515184760 + 0.9518445730 + 0.9521707296 + 0.9524968266 + 0.9528229237 + 0.9531490207 + 0.9534751177 + 0.9538012147 + 0.9541273117 + 0.9544534087 + 0.9547795057 + 0.9551056027 + 0.9554316998 + 0.9557577968 + 0.9560838938 + 0.9564099908 + 0.9567360878 + 0.9570621848 + 0.9573882818 + 0.9577143788 + 0.9580404758 + 0.9583665729 + 0.9586927295 + 0.9590188265 + 0.9593449235 + 0.9596710205 + 0.9599971175 + 0.9602413177 + 0.9604846835 + 0.9607281089 + 0.9609714746 + 0.9612150192 + 0.9614583850 + 0.9617018104 + 0.9619451761 + 0.9621887207 + 0.9624320865 + 0.9626755118 + 0.9629188776 + 0.9631623030 + 0.9634057879 + 0.9636492133 + 0.9638925791 + 0.9641360044 + 0.9643794894 + 0.9646229148 + 0.9648662806 + 0.9651097059 + 0.9653531909 + 0.9655966163 + 0.9658399820 + 0.9660834074 + 0.9663268924 + 0.9665703177 + 0.9668136835 + 0.9670571089 + 0.9673005939 + 0.9675440192 + 0.9677873850 + 0.9680308104 + 0.9682741761 + 0.9685177207 + 0.9687610865 + 0.9690045118 + 0.9692478776 + 0.9694914222 + 0.9697347879 + 0.9699782133 + 0.9701654911 + 0.9703474045 + 0.9705291986 + 0.9707109928 + 0.9708927870 + 0.9710745811 + 0.9712563753 + 0.9714382291 + 0.9716200233 + 0.9718018770 + 0.9719836712 + 0.9721655250 + 0.9723473191 + 0.9725291133 + 0.9727109075 + 0.9728927016 + 0.9730746150 + 0.9732564092 + 0.9734382033 + 0.9736199975 + 0.9738017917 + 0.9739835858 + 0.9741653800 + 0.9743471742 + 0.9745290875 + 0.9747108817 + 0.9748926759 + 0.9750745296 + 0.9752563238 + 0.9754381180 + 0.9756199121 + 0.9758018255 + 0.9759836197 + 0.9761654139 + 0.9763472080 + 0.9765290022 + 0.9767107964 + 0.9768925905 + 0.9770743847 + 0.9772562981 + 0.9774380922 + 0.9776198864 + 0.9778016806 + 0.9779834747 + 0.9781653285 + 0.9783471227 + 0.9785289168 + 0.9787107706 + 0.9788926244 + 0.9790744185 + 0.9792562127 + 0.9794380069 + 0.9796198010 + 0.9798015952 + 0.9799835086 + 0.9801241159 + 0.9802607298 + 0.9803972840 + 0.9805337787 + 0.9806703925 + 0.9808070064 + 0.9809435010 + 0.9810801148 + 0.9812166095 + 0.9813532233 + 0.9814897776 + 0.9816262722 + 0.9817628860 + 0.9818994999 + 0.9820359945 + 0.9821726084 + 0.9823091030 + 0.9824457169 + 0.9825822711 + 0.9827188253 + 0.9828553796 + 0.9829919934 + 0.9831284881 + 0.9832651019 + 0.9834015965 + 0.9835382104 + 0.9836748242 + 0.9838113189 + 0.9839478731 + 0.9840844870 + 0.9842209816 + 0.9843575954 + 0.9844942093 + 0.9846307039 + 0.9847673178 + 0.9849038124 + 0.9850404263 + 0.9851769805 + 0.9853134751 + 0.9854500890 + 0.9855867028 + 0.9857231975 + 0.9858598113 + 0.9859963059 + 0.9861329198 + 0.9862694740 + 0.9864060283 + 0.9865425825 + 0.9866791964 + 0.9868156910 + 0.9869523048 + 0.9870887995 + 0.9872254133 + 0.9873620272 + 0.9874985218 + 0.9876350760 + 0.9877716899 + 0.9879081845 + 0.9880447984 + 0.9881814122 + 0.9883179069 + 0.9884545207 + 0.9885910153 + 0.9887276292 + 0.9888641834 + 0.9890006781 + 0.9891372919 + 0.9892739058 + 0.9894104004 + 0.9895470142 + 0.9896835089 + 0.9898201227 + 0.9899566770 + 0.9900705814 + 0.9901739955 + 0.9902775288 + 0.9903808832 + 0.9904842973 + 0.9905877709 + 0.9906911850 + 0.9907945991 + 0.9908980727 + 0.9910014868 + 0.9911049008 + 0.9912083745 + 0.9913117886 + 0.9914152026 + 0.9915186167 + 0.9916220903 + 0.9917255044 + 0.9918289185 + 0.9919323921 + 0.9920358062 + 0.9921392202 + 0.9922426939 + 0.9923461080 + 0.9924495220 + 0.9925529957 + 0.9926564097 + 0.9927598238 + 0.9928632975 + 0.9929667115 + 0.9930701256 + 0.9931734800 + 0.9932770133 + 0.9933804274 + 0.9934837818 + 0.9935873151 + 0.9936907291 + 0.9937940836 + 0.9938976169 + 0.9940009713 + 0.9941043854 + 0.9942079186 + 0.9943112731 + 0.9944146872 + 0.9945182204 + 0.9946215749 + 0.9947249889 + 0.9948284030 + 0.9949318767 + 0.9950352907 + 0.9951387048 + 0.9952421784 + 0.9953455925 + 0.9954490066 + 0.9955524802 + 0.9956558943 + 0.9957593083 + 0.9958627820 + 0.9959661961 + 0.9960696101 + 0.9961730242 + 0.9962764978 + 0.9963799119 + 0.9964833260 + 0.9965867996 + 0.9966902137 + 0.9967936277 + 0.9968971014 + 0.9970005155 + 0.9971039295 + 0.9972074032 + 0.9973108172 + 0.9974141717 + 0.9975177050 + 0.9976211190 + 0.9977244735 + 0.9978278875 + 0.9979314208 + 0.9980347753 + 0.9981381893 + 0.9982417226 + 0.9983450770 + 0.9984484911 + 0.9985520244 + 0.9986553788 + 0.9987587929 + 0.9988623261 + 0.9989656806 + 0.9990690947 + 0.9991726279 + 0.9992759824 + 0.9993793964 + 0.9994828105 + 0.9995862842 + 0.9996896982 + 0.9997931123 + 0.9998965859 + 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/canon_optura_981111.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/canon_optura_981111.spi1d index 23ca3fa4..35dc7380 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/canon_optura_981111.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/canon_optura_981111.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 1 -{ - 0.0000000000 - 0.0025450480 - 0.0050900960 - 0.0076351431 - 0.0101884697 - 0.0128504699 - 0.0155124702 - 0.0181744602 - 0.0208844505 - 0.0236991905 - 0.0265139192 - 0.0293286499 - 0.0322732218 - 0.0352584682 - 0.0382437110 - 0.0412991717 - 0.0444549806 - 0.0476107895 - 0.0508038513 - 0.0541130006 - 0.0574221388 - 0.0607581101 - 0.0641886517 - 0.0676191822 - 0.0710741282 - 0.0745844394 - 0.0780947506 - 0.0816211328 - 0.0851665810 - 0.0887120292 - 0.0922535583 - 0.0957928598 - 0.0993321612 - 0.1028394997 - 0.1063392982 - 0.1098392010 - 0.1132792011 - 0.1167163998 - 0.1201502010 - 0.1235113963 - 0.1268725991 - 0.1302281022 - 0.1335085034 - 0.1367888004 - 0.1400675029 - 0.1432684064 - 0.1464692056 - 0.1496700943 - 0.1528047025 - 0.1559316963 - 0.1590587050 - 0.1621396989 - 0.1652007997 - 0.1682619005 - 0.1712982953 - 0.1743023992 - 0.1773066074 - 0.1803057045 - 0.1832617074 - 0.1862176061 - 0.1891735941 - 0.1921004951 - 0.1950162053 - 0.1979319006 - 0.2008378059 - 0.2037197053 - 0.2066015005 - 0.2094834000 - 0.2123412043 - 0.2151937932 - 0.2180463970 - 0.2208905071 - 0.2237161994 - 0.2265419066 - 0.2293675989 - 0.2321725041 - 0.2349714041 - 0.2377703935 - 0.2405633926 - 0.2433333993 - 0.2461033016 - 0.2488732934 - 0.2516236007 - 0.2543602884 - 0.2570970953 - 0.2598339021 - 0.2625338137 - 0.2652313113 - 0.2679288983 - 0.2706156075 - 0.2732666135 - 0.2759177089 - 0.2785687149 - 0.2811945975 - 0.2837912142 - 0.2863877118 - 0.2889842987 - 0.2915427089 - 0.2940768003 - 0.2966108024 - 0.2991448939 - 0.3016327024 - 0.3040969968 - 0.3065612912 - 0.3090254962 - 0.3114438951 - 0.3138321042 - 0.3162203133 - 0.3186084926 - 0.3209632039 - 0.3232708871 - 0.3255786002 - 0.3278863132 - 0.3301869929 - 0.3324112892 - 0.3346355855 - 0.3368599117 - 0.3390842080 - 0.3412589133 - 0.3433989882 - 0.3455390036 - 0.3476791084 - 0.3498190939 - 0.3518827856 - 0.3539395034 - 0.3559961915 - 0.3580527902 - 0.3601051867 - 0.3620809913 - 0.3640567958 - 0.3660326004 - 0.3680082858 - 0.3699840903 - 0.3718833923 - 0.3737820983 - 0.3756807148 - 0.3775793910 - 0.3794780076 - 0.3813242912 - 0.3831506073 - 0.3849770129 - 0.3868033886 - 0.3886297941 - 0.3904395103 - 0.3921990991 - 0.3939588070 - 0.3957183957 - 0.3974781036 - 0.3992376924 - 0.4009630084 - 0.4026620984 - 0.4043611884 - 0.4060603082 - 0.4077593982 - 0.4094584882 - 0.4111205935 - 0.4127655029 - 0.4144104123 - 0.4160552919 - 0.4177002013 - 0.4193451107 - 0.4209614098 - 0.4225586951 - 0.4241560102 - 0.4257532954 - 0.4273506105 - 0.4289478958 - 0.4305312037 - 0.4320875108 - 0.4336437881 - 0.4352000058 - 0.4367563128 - 0.4383125007 - 0.4398688078 - 0.4413934946 - 0.4429152012 - 0.4444369972 - 0.4459587932 - 0.4474805892 - 0.4490022957 - 0.4505144060 - 0.4520080984 - 0.4535017014 - 0.4549953938 - 0.4564889967 - 0.4579826891 - 0.4594762921 - 0.4609557092 - 0.4624272883 - 0.4638989866 - 0.4653705955 - 0.4668422043 - 0.4683139026 - 0.4697855115 - 0.4712435007 - 0.4726991057 - 0.4741545916 - 0.4756101966 - 0.4770658016 - 0.4785214067 - 0.4799770117 - 0.4814223051 - 0.4828675091 - 0.4843125939 - 0.4857577980 - 0.4872029126 - 0.4886480868 - 0.4900929034 - 0.4915328026 - 0.4929727912 - 0.4944128096 - 0.4958527088 - 0.4972926974 - 0.4987326860 - 0.5001726151 - 0.5016121864 - 0.5030518770 - 0.5044915080 - 0.5059311986 - 0.5073708296 - 0.5088104010 - 0.5102508068 - 0.5116943717 - 0.5131379962 - 0.5145816207 - 0.5160253048 - 0.5174689293 - 0.5189124942 - 0.5203580260 - 0.5218092799 - 0.5232605934 - 0.5247119069 - 0.5261632800 - 0.5276145935 - 0.5290659070 - 0.5305209160 - 0.5319827199 - 0.5334445238 - 0.5349063277 - 0.5363680720 - 0.5378299952 - 0.5392917991 - 0.5407599807 - 0.5422341824 - 0.5437083840 - 0.5451825857 - 0.5466567874 - 0.5481309891 - 0.5496053100 - 0.5510889888 - 0.5525763035 - 0.5540636182 - 0.5555508137 - 0.5570381284 - 0.5585253239 - 0.5600126982 - 0.5615121722 - 0.5630118251 - 0.5645112991 - 0.5660107732 - 0.5675103068 - 0.5690099001 - 0.5705127120 - 0.5720220804 - 0.5735315084 - 0.5750408769 - 0.5765503049 - 0.5780596733 - 0.5795689821 - 0.5810825229 - 0.5825976133 - 0.5841127038 - 0.5856277943 - 0.5871428251 - 0.5886579156 - 0.5901730061 - 0.5916877985 - 0.5932027102 - 0.5947175026 - 0.5962324142 - 0.5977472067 - 0.5992619991 - 0.6007729173 - 0.6022800207 - 0.6037871242 - 0.6052942872 - 0.6068013906 - 0.6083084941 - 0.6098155975 - 0.6113082170 - 0.6127988100 - 0.6142895222 - 0.6157801151 - 0.6172707081 - 0.6187613010 - 0.6202476025 - 0.6217123270 - 0.6231769919 - 0.6246417165 - 0.6261065006 - 0.6275712252 - 0.6290358901 - 0.6304885149 - 0.6319180131 - 0.6333473921 - 0.6347767711 - 0.6362062097 - 0.6376355886 - 0.6390650272 - 0.6404792070 - 0.6418644786 - 0.6432499290 - 0.6446353197 - 0.6460205913 - 0.6474059820 - 0.6487913728 - 0.6501702070 - 0.6515042186 - 0.6528382897 - 0.6541723013 - 0.6555063725 - 0.6568403840 - 0.6581745148 - 0.6595085263 - 0.6608067155 - 0.6620839238 - 0.6633611917 - 0.6646384001 - 0.6659157276 - 0.6671929955 - 0.6684702039 - 0.6697474718 - 0.6709765196 - 0.6721935868 - 0.6734107137 - 0.6746279001 - 0.6758450270 - 0.6770620942 - 0.6782792807 - 0.6794964075 - 0.6806775928 - 0.6818333864 - 0.6829891801 - 0.6841449738 - 0.6853008270 - 0.6864566207 - 0.6876124144 - 0.6887682080 - 0.6899240017 - 0.6910231709 - 0.6921185255 - 0.6932137012 - 0.6943089962 - 0.6954041719 - 0.6964995265 - 0.6975947022 - 0.6986898780 - 0.6997851729 - 0.7008336782 - 0.7018709183 - 0.7029079795 - 0.7039451003 - 0.7049822807 - 0.7060194016 - 0.7070565820 - 0.7080937028 - 0.7091308236 - 0.7101591825 - 0.7111420035 - 0.7121248245 - 0.7131077051 - 0.7140905261 - 0.7150732875 - 0.7160561085 - 0.7170389891 - 0.7180218101 - 0.7190045714 - 0.7199873924 - 0.7209213972 - 0.7218546867 - 0.7227879763 - 0.7237213254 - 0.7246546149 - 0.7255879045 - 0.7265211940 - 0.7274544835 - 0.7283877730 - 0.7293211222 - 0.7302423716 - 0.7311316729 - 0.7320209742 - 0.7329102755 - 0.7337995768 - 0.7346888781 - 0.7355781198 - 0.7364674211 - 0.7373567224 - 0.7382460237 - 0.7391353250 - 0.7400234938 - 0.7408747077 - 0.7417259812 - 0.7425771952 - 0.7434284091 - 0.7442796826 - 0.7451308966 - 0.7459822297 - 0.7468333840 - 0.7476845980 - 0.7485358715 - 0.7493870854 - 0.7502294183 - 0.7510489225 - 0.7518684268 - 0.7526878715 - 0.7535073757 - 0.7543268800 - 0.7551463842 - 0.7559658885 - 0.7567853928 - 0.7576048970 - 0.7584244013 - 0.7592439055 - 0.7600615025 - 0.7608557940 - 0.7616500854 - 0.7624443769 - 0.7632387877 - 0.7640330791 - 0.7648273706 - 0.7656217217 - 0.7664160132 - 0.7672104239 - 0.7680047154 - 0.7687990069 - 0.7695932984 - 0.7703787088 - 0.7711545825 - 0.7719305158 - 0.7727065086 - 0.7734823823 - 0.7742583156 - 0.7750343084 - 0.7758101821 - 0.7765861154 - 0.7773621082 - 0.7781379819 - 0.7789139152 - 0.7796899080 - 0.7804589868 - 0.7812235951 - 0.7819883227 - 0.7827528715 - 0.7835174799 - 0.7842820883 - 0.7850466967 - 0.7858114243 - 0.7865759730 - 0.7873405814 - 0.7881051898 - 0.7888697982 - 0.7896345258 - 0.7903971076 - 0.7911577821 - 0.7919185162 - 0.7926793098 - 0.7934399843 - 0.7942007184 - 0.7949615121 - 0.7957221866 - 0.7964829803 - 0.7972437143 - 0.7980043888 - 0.7987651825 - 0.7995259166 - 0.8002882004 - 0.8010529876 - 0.8018177152 - 0.8025825024 - 0.8033472896 - 0.8041120768 - 0.8048768044 - 0.8056415915 - 0.8064063787 - 0.8071712255 - 0.8079360127 - 0.8087006807 - 0.8094655275 - 0.8102341294 - 0.8110113740 - 0.8117886782 - 0.8125661016 - 0.8133434057 - 0.8141207099 - 0.8148980737 - 0.8156753778 - 0.8164526820 - 0.8172299862 - 0.8180074096 - 0.8187847137 - 0.8195620179 - 0.8203489184 - 0.8211479187 - 0.8219469786 - 0.8227460980 - 0.8235452175 - 0.8243442774 - 0.8251432776 - 0.8259423971 - 0.8267415166 - 0.8275405765 - 0.8283395767 - 0.8291386962 - 0.8299378157 - 0.8307659030 - 0.8315964937 - 0.8324270844 - 0.8332576752 - 0.8340882063 - 0.8349187970 - 0.8357493877 - 0.8365799785 - 0.8374106288 - 0.8382411003 - 0.8390716910 - 0.8399022818 - 0.8407692909 - 0.8416411877 - 0.8425130248 - 0.8433849216 - 0.8442568183 - 0.8451285958 - 0.8460004926 - 0.8468723893 - 0.8477442265 - 0.8486161232 - 0.8494880199 - 0.8503803015 - 0.8513019085 - 0.8522233963 - 0.8531450033 - 0.8540664911 - 0.8549880981 - 0.8559095860 - 0.8568311930 - 0.8577526808 - 0.8586742282 - 0.8595957756 - 0.8605474234 - 0.8615226150 - 0.8624976873 - 0.8634728789 - 0.8644480705 - 0.8654232025 - 0.8663983941 - 0.8673735261 - 0.8683487177 - 0.8693237901 - 0.8703135848 - 0.8713365197 - 0.8723592758 - 0.8733820915 - 0.8744049072 - 0.8754277825 - 0.8764505982 - 0.8774734139 - 0.8784962893 - 0.8795191050 - 0.8805549145 - 0.8816024065 - 0.8826497793 - 0.8836972117 - 0.8847445846 - 0.8857920170 - 0.8868393898 - 0.8878868222 - 0.8889341950 - 0.8899816275 - 0.8910092115 - 0.8920364976 - 0.8930637240 - 0.8940910101 - 0.8951181769 - 0.8961455226 - 0.8971728086 - 0.8981999755 - 0.8992273211 - 0.9002348185 - 0.9011824131 - 0.9021301270 - 0.9030777216 - 0.9040253758 - 0.9049729705 - 0.9059206247 - 0.9068682790 - 0.9078158736 - 0.9087635279 - 0.9097111821 - 0.9105666876 - 0.9113819003 - 0.9121969938 - 0.9130122066 - 0.9138273001 - 0.9146425128 - 0.9154576063 - 0.9162728190 - 0.9170879126 - 0.9179031253 - 0.9187182188 - 0.9195333719 - 0.9202811718 - 0.9209386706 - 0.9215962887 - 0.9222537875 - 0.9229114056 - 0.9235690236 - 0.9242265224 - 0.9248840809 - 0.9255415797 - 0.9261991978 - 0.9268566966 - 0.9275143147 - 0.9281718135 - 0.9288293719 - 0.9294869900 - 0.9301112294 - 0.9306169152 - 0.9311227202 - 0.9316285253 - 0.9321342111 - 0.9326400161 - 0.9331458211 - 0.9336515069 - 0.9341573119 - 0.9346631169 - 0.9351688027 - 0.9356746078 - 0.9361804128 - 0.9366860986 - 0.9371919036 - 0.9376977086 - 0.9382033944 - 0.9387091994 - 0.9392150044 - 0.9397206903 - 0.9401693940 - 0.9405475855 - 0.9409257770 - 0.9413040876 - 0.9416822791 - 0.9420604706 - 0.9424387813 - 0.9428169727 - 0.9431952238 - 0.9435734153 - 0.9439517260 - 0.9443299174 - 0.9447081089 - 0.9450864196 - 0.9454646111 - 0.9458428025 - 0.9462211132 - 0.9465993047 - 0.9469774961 - 0.9473558068 - 0.9477339983 - 0.9481121898 - 0.9484903812 - 0.9488686919 - 0.9492468834 - 0.9496250749 - 0.9500024915 - 0.9502819777 - 0.9505615234 - 0.9508408904 - 0.9511203766 - 0.9513999224 - 0.9516794086 - 0.9519588947 - 0.9522383809 - 0.9525179267 - 0.9527974129 - 0.9530767798 - 0.9533563256 - 0.9536358118 - 0.9539152980 - 0.9541947842 - 0.9544742703 - 0.9547538161 - 0.9550331831 - 0.9553127289 - 0.9555922151 - 0.9558717012 - 0.9561511874 - 0.9564306736 - 0.9567102194 - 0.9569895864 - 0.9572690725 - 0.9575486183 - 0.9578281045 - 0.9581075907 - 0.9583870769 - 0.9586666226 - 0.9589459896 - 0.9592254758 - 0.9595050216 - 0.9597845078 - 0.9600471854 - 0.9602534771 - 0.9604598880 - 0.9606661797 - 0.9608724713 - 0.9610788226 - 0.9612851143 - 0.9614914060 - 0.9616976976 - 0.9619039893 - 0.9621102810 - 0.9623166919 - 0.9625229836 - 0.9627292752 - 0.9629356265 - 0.9631419182 - 0.9633482099 - 0.9635545015 - 0.9637607932 - 0.9639670849 - 0.9641734958 - 0.9643797874 - 0.9645860791 - 0.9647923708 - 0.9649987221 - 0.9652050138 - 0.9654113054 - 0.9656175971 - 0.9658238888 - 0.9660302997 - 0.9662365913 - 0.9664428830 - 0.9666491747 - 0.9668555260 - 0.9670618176 - 0.9672681093 - 0.9674744010 - 0.9676806927 - 0.9678869843 - 0.9680933952 - 0.9682996869 - 0.9685059786 - 0.9687122703 - 0.9689186215 - 0.9691249132 - 0.9693312049 - 0.9695374966 - 0.9697437882 - 0.9699501991 - 0.9701160789 - 0.9702693224 - 0.9704223871 - 0.9705755115 - 0.9707286954 - 0.9708818197 - 0.9710348845 - 0.9711880088 - 0.9713411927 - 0.9714943171 - 0.9716473818 - 0.9718006253 - 0.9719536901 - 0.9721068144 - 0.9722599983 - 0.9724131227 - 0.9725661874 - 0.9727193713 - 0.9728724957 - 0.9730256200 - 0.9731786847 - 0.9733319283 - 0.9734849930 - 0.9736381173 - 0.9737913013 - 0.9739444256 - 0.9740974903 - 0.9742506742 - 0.9744037986 - 0.9745569229 - 0.9747101068 - 0.9748631716 - 0.9750162959 - 0.9751694202 - 0.9753226042 - 0.9754757285 - 0.9756287932 - 0.9757819772 - 0.9759351015 - 0.9760882258 - 0.9762414098 - 0.9763944745 - 0.9765475988 - 0.9767007232 - 0.9768539071 - 0.9770069718 - 0.9771600962 - 0.9773132801 - 0.9774664044 - 0.9776195288 - 0.9777727127 - 0.9779257774 - 0.9780789018 - 0.9782320857 - 0.9783852100 - 0.9785382748 - 0.9786913991 - 0.9788445830 - 0.9789977074 - 0.9791507721 - 0.9793040156 - 0.9794570804 - 0.9796102047 - 0.9797633886 - 0.9799165130 - 0.9800521135 - 0.9801667929 - 0.9802814722 - 0.9803960919 - 0.9805107713 - 0.9806255102 - 0.9807401896 - 0.9808548093 - 0.9809694886 - 0.9810842276 - 0.9811989069 - 0.9813135266 - 0.9814282060 - 0.9815428853 - 0.9816575050 - 0.9817721844 - 0.9818869233 - 0.9820016026 - 0.9821162224 - 0.9822309017 - 0.9823455811 - 0.9824602008 - 0.9825748801 - 0.9826896191 - 0.9828042984 - 0.9829189181 - 0.9830335975 - 0.9831482768 - 0.9832628965 - 0.9833775759 - 0.9834923148 - 0.9836069942 - 0.9837216139 - 0.9838362932 - 0.9839509726 - 0.9840655923 - 0.9841802716 - 0.9842950106 - 0.9844096899 - 0.9845243096 - 0.9846389890 - 0.9847537279 - 0.9848682880 - 0.9849830270 - 0.9850977063 - 0.9852123857 - 0.9853270054 - 0.9854416847 - 0.9855564237 - 0.9856711030 - 0.9857857227 - 0.9859004021 - 0.9860150814 - 0.9861297011 - 0.9862443805 - 0.9863591194 - 0.9864737988 - 0.9865884185 - 0.9867030978 - 0.9868177772 - 0.9869323969 - 0.9870470762 - 0.9871618152 - 0.9872764945 - 0.9873911142 - 0.9875057936 - 0.9876204729 - 0.9877350926 - 0.9878497720 - 0.9879645109 - 0.9880791903 - 0.9881938100 - 0.9883084893 - 0.9884232283 - 0.9885377884 - 0.9886525273 - 0.9887672067 - 0.9888818860 - 0.9889965057 - 0.9891111851 - 0.9892259240 - 0.9893404841 - 0.9894552231 - 0.9895699024 - 0.9896845818 - 0.9897992015 - 0.9899138808 - 0.9900215864 - 0.9901083708 - 0.9901952147 - 0.9902818799 - 0.9903687239 - 0.9904555082 - 0.9905421734 - 0.9906290174 - 0.9907158017 - 0.9908025265 - 0.9908893108 - 0.9909760952 - 0.9910628200 - 0.9911496043 - 0.9912363887 - 0.9913231730 - 0.9914098978 - 0.9914966822 - 0.9915835261 - 0.9916701913 - 0.9917569757 - 0.9918438196 - 0.9919304848 - 0.9920173287 - 0.9921041131 - 0.9921907783 - 0.9922776222 - 0.9923644066 - 0.9924510717 - 0.9925379157 - 0.9926247001 - 0.9927114248 - 0.9927982092 - 0.9928849936 - 0.9929717779 - 0.9930585027 - 0.9931452870 - 0.9932320714 - 0.9933187962 - 0.9934055805 - 0.9934924245 - 0.9935790896 - 0.9936658740 - 0.9937527180 - 0.9938393831 - 0.9939262271 - 0.9940130115 - 0.9940996766 - 0.9941865206 - 0.9942733049 - 0.9943600297 - 0.9944468141 - 0.9945335984 - 0.9946203828 - 0.9947071075 - 0.9947938919 - 0.9948806763 - 0.9949674010 - 0.9950541854 - 0.9951410294 - 0.9952276945 - 0.9953144789 - 0.9954013228 - 0.9954879880 - 0.9955747724 - 0.9956616163 - 0.9957482815 - 0.9958351254 - 0.9959219098 - 0.9960085750 - 0.9960954189 - 0.9961822033 - 0.9962689877 - 0.9963557124 - 0.9964424968 - 0.9965292811 - 0.9966160059 - 0.9967027903 - 0.9967895746 - 0.9968762994 - 0.9969630837 - 0.9970499277 - 0.9971365929 - 0.9972233772 - 0.9973102212 - 0.9973968863 - 0.9974836707 - 0.9975705147 - 0.9976572990 - 0.9977440238 - 0.9978308082 - 0.9979175925 - 0.9980043173 - 0.9980911016 - 0.9981778860 - 0.9982646108 - 0.9983513951 - 0.9984381795 - 0.9985249043 - 0.9986116886 - 0.9986984730 - 0.9987851977 - 0.9988719821 - 0.9989588261 - 0.9990454912 - 0.9991322756 - 0.9992191195 - 0.9993059039 - 0.9993926287 - 0.9994794130 - 0.9995661974 - 0.9996529222 - 0.9997397065 - 0.9998264909 - 0.9999132156 - 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 1 +{ + 0.0000000000 + 0.0025450480 + 0.0050900960 + 0.0076351431 + 0.0101884697 + 0.0128504699 + 0.0155124702 + 0.0181744602 + 0.0208844505 + 0.0236991905 + 0.0265139192 + 0.0293286499 + 0.0322732218 + 0.0352584682 + 0.0382437110 + 0.0412991717 + 0.0444549806 + 0.0476107895 + 0.0508038513 + 0.0541130006 + 0.0574221388 + 0.0607581101 + 0.0641886517 + 0.0676191822 + 0.0710741282 + 0.0745844394 + 0.0780947506 + 0.0816211328 + 0.0851665810 + 0.0887120292 + 0.0922535583 + 0.0957928598 + 0.0993321612 + 0.1028394997 + 0.1063392982 + 0.1098392010 + 0.1132792011 + 0.1167163998 + 0.1201502010 + 0.1235113963 + 0.1268725991 + 0.1302281022 + 0.1335085034 + 0.1367888004 + 0.1400675029 + 0.1432684064 + 0.1464692056 + 0.1496700943 + 0.1528047025 + 0.1559316963 + 0.1590587050 + 0.1621396989 + 0.1652007997 + 0.1682619005 + 0.1712982953 + 0.1743023992 + 0.1773066074 + 0.1803057045 + 0.1832617074 + 0.1862176061 + 0.1891735941 + 0.1921004951 + 0.1950162053 + 0.1979319006 + 0.2008378059 + 0.2037197053 + 0.2066015005 + 0.2094834000 + 0.2123412043 + 0.2151937932 + 0.2180463970 + 0.2208905071 + 0.2237161994 + 0.2265419066 + 0.2293675989 + 0.2321725041 + 0.2349714041 + 0.2377703935 + 0.2405633926 + 0.2433333993 + 0.2461033016 + 0.2488732934 + 0.2516236007 + 0.2543602884 + 0.2570970953 + 0.2598339021 + 0.2625338137 + 0.2652313113 + 0.2679288983 + 0.2706156075 + 0.2732666135 + 0.2759177089 + 0.2785687149 + 0.2811945975 + 0.2837912142 + 0.2863877118 + 0.2889842987 + 0.2915427089 + 0.2940768003 + 0.2966108024 + 0.2991448939 + 0.3016327024 + 0.3040969968 + 0.3065612912 + 0.3090254962 + 0.3114438951 + 0.3138321042 + 0.3162203133 + 0.3186084926 + 0.3209632039 + 0.3232708871 + 0.3255786002 + 0.3278863132 + 0.3301869929 + 0.3324112892 + 0.3346355855 + 0.3368599117 + 0.3390842080 + 0.3412589133 + 0.3433989882 + 0.3455390036 + 0.3476791084 + 0.3498190939 + 0.3518827856 + 0.3539395034 + 0.3559961915 + 0.3580527902 + 0.3601051867 + 0.3620809913 + 0.3640567958 + 0.3660326004 + 0.3680082858 + 0.3699840903 + 0.3718833923 + 0.3737820983 + 0.3756807148 + 0.3775793910 + 0.3794780076 + 0.3813242912 + 0.3831506073 + 0.3849770129 + 0.3868033886 + 0.3886297941 + 0.3904395103 + 0.3921990991 + 0.3939588070 + 0.3957183957 + 0.3974781036 + 0.3992376924 + 0.4009630084 + 0.4026620984 + 0.4043611884 + 0.4060603082 + 0.4077593982 + 0.4094584882 + 0.4111205935 + 0.4127655029 + 0.4144104123 + 0.4160552919 + 0.4177002013 + 0.4193451107 + 0.4209614098 + 0.4225586951 + 0.4241560102 + 0.4257532954 + 0.4273506105 + 0.4289478958 + 0.4305312037 + 0.4320875108 + 0.4336437881 + 0.4352000058 + 0.4367563128 + 0.4383125007 + 0.4398688078 + 0.4413934946 + 0.4429152012 + 0.4444369972 + 0.4459587932 + 0.4474805892 + 0.4490022957 + 0.4505144060 + 0.4520080984 + 0.4535017014 + 0.4549953938 + 0.4564889967 + 0.4579826891 + 0.4594762921 + 0.4609557092 + 0.4624272883 + 0.4638989866 + 0.4653705955 + 0.4668422043 + 0.4683139026 + 0.4697855115 + 0.4712435007 + 0.4726991057 + 0.4741545916 + 0.4756101966 + 0.4770658016 + 0.4785214067 + 0.4799770117 + 0.4814223051 + 0.4828675091 + 0.4843125939 + 0.4857577980 + 0.4872029126 + 0.4886480868 + 0.4900929034 + 0.4915328026 + 0.4929727912 + 0.4944128096 + 0.4958527088 + 0.4972926974 + 0.4987326860 + 0.5001726151 + 0.5016121864 + 0.5030518770 + 0.5044915080 + 0.5059311986 + 0.5073708296 + 0.5088104010 + 0.5102508068 + 0.5116943717 + 0.5131379962 + 0.5145816207 + 0.5160253048 + 0.5174689293 + 0.5189124942 + 0.5203580260 + 0.5218092799 + 0.5232605934 + 0.5247119069 + 0.5261632800 + 0.5276145935 + 0.5290659070 + 0.5305209160 + 0.5319827199 + 0.5334445238 + 0.5349063277 + 0.5363680720 + 0.5378299952 + 0.5392917991 + 0.5407599807 + 0.5422341824 + 0.5437083840 + 0.5451825857 + 0.5466567874 + 0.5481309891 + 0.5496053100 + 0.5510889888 + 0.5525763035 + 0.5540636182 + 0.5555508137 + 0.5570381284 + 0.5585253239 + 0.5600126982 + 0.5615121722 + 0.5630118251 + 0.5645112991 + 0.5660107732 + 0.5675103068 + 0.5690099001 + 0.5705127120 + 0.5720220804 + 0.5735315084 + 0.5750408769 + 0.5765503049 + 0.5780596733 + 0.5795689821 + 0.5810825229 + 0.5825976133 + 0.5841127038 + 0.5856277943 + 0.5871428251 + 0.5886579156 + 0.5901730061 + 0.5916877985 + 0.5932027102 + 0.5947175026 + 0.5962324142 + 0.5977472067 + 0.5992619991 + 0.6007729173 + 0.6022800207 + 0.6037871242 + 0.6052942872 + 0.6068013906 + 0.6083084941 + 0.6098155975 + 0.6113082170 + 0.6127988100 + 0.6142895222 + 0.6157801151 + 0.6172707081 + 0.6187613010 + 0.6202476025 + 0.6217123270 + 0.6231769919 + 0.6246417165 + 0.6261065006 + 0.6275712252 + 0.6290358901 + 0.6304885149 + 0.6319180131 + 0.6333473921 + 0.6347767711 + 0.6362062097 + 0.6376355886 + 0.6390650272 + 0.6404792070 + 0.6418644786 + 0.6432499290 + 0.6446353197 + 0.6460205913 + 0.6474059820 + 0.6487913728 + 0.6501702070 + 0.6515042186 + 0.6528382897 + 0.6541723013 + 0.6555063725 + 0.6568403840 + 0.6581745148 + 0.6595085263 + 0.6608067155 + 0.6620839238 + 0.6633611917 + 0.6646384001 + 0.6659157276 + 0.6671929955 + 0.6684702039 + 0.6697474718 + 0.6709765196 + 0.6721935868 + 0.6734107137 + 0.6746279001 + 0.6758450270 + 0.6770620942 + 0.6782792807 + 0.6794964075 + 0.6806775928 + 0.6818333864 + 0.6829891801 + 0.6841449738 + 0.6853008270 + 0.6864566207 + 0.6876124144 + 0.6887682080 + 0.6899240017 + 0.6910231709 + 0.6921185255 + 0.6932137012 + 0.6943089962 + 0.6954041719 + 0.6964995265 + 0.6975947022 + 0.6986898780 + 0.6997851729 + 0.7008336782 + 0.7018709183 + 0.7029079795 + 0.7039451003 + 0.7049822807 + 0.7060194016 + 0.7070565820 + 0.7080937028 + 0.7091308236 + 0.7101591825 + 0.7111420035 + 0.7121248245 + 0.7131077051 + 0.7140905261 + 0.7150732875 + 0.7160561085 + 0.7170389891 + 0.7180218101 + 0.7190045714 + 0.7199873924 + 0.7209213972 + 0.7218546867 + 0.7227879763 + 0.7237213254 + 0.7246546149 + 0.7255879045 + 0.7265211940 + 0.7274544835 + 0.7283877730 + 0.7293211222 + 0.7302423716 + 0.7311316729 + 0.7320209742 + 0.7329102755 + 0.7337995768 + 0.7346888781 + 0.7355781198 + 0.7364674211 + 0.7373567224 + 0.7382460237 + 0.7391353250 + 0.7400234938 + 0.7408747077 + 0.7417259812 + 0.7425771952 + 0.7434284091 + 0.7442796826 + 0.7451308966 + 0.7459822297 + 0.7468333840 + 0.7476845980 + 0.7485358715 + 0.7493870854 + 0.7502294183 + 0.7510489225 + 0.7518684268 + 0.7526878715 + 0.7535073757 + 0.7543268800 + 0.7551463842 + 0.7559658885 + 0.7567853928 + 0.7576048970 + 0.7584244013 + 0.7592439055 + 0.7600615025 + 0.7608557940 + 0.7616500854 + 0.7624443769 + 0.7632387877 + 0.7640330791 + 0.7648273706 + 0.7656217217 + 0.7664160132 + 0.7672104239 + 0.7680047154 + 0.7687990069 + 0.7695932984 + 0.7703787088 + 0.7711545825 + 0.7719305158 + 0.7727065086 + 0.7734823823 + 0.7742583156 + 0.7750343084 + 0.7758101821 + 0.7765861154 + 0.7773621082 + 0.7781379819 + 0.7789139152 + 0.7796899080 + 0.7804589868 + 0.7812235951 + 0.7819883227 + 0.7827528715 + 0.7835174799 + 0.7842820883 + 0.7850466967 + 0.7858114243 + 0.7865759730 + 0.7873405814 + 0.7881051898 + 0.7888697982 + 0.7896345258 + 0.7903971076 + 0.7911577821 + 0.7919185162 + 0.7926793098 + 0.7934399843 + 0.7942007184 + 0.7949615121 + 0.7957221866 + 0.7964829803 + 0.7972437143 + 0.7980043888 + 0.7987651825 + 0.7995259166 + 0.8002882004 + 0.8010529876 + 0.8018177152 + 0.8025825024 + 0.8033472896 + 0.8041120768 + 0.8048768044 + 0.8056415915 + 0.8064063787 + 0.8071712255 + 0.8079360127 + 0.8087006807 + 0.8094655275 + 0.8102341294 + 0.8110113740 + 0.8117886782 + 0.8125661016 + 0.8133434057 + 0.8141207099 + 0.8148980737 + 0.8156753778 + 0.8164526820 + 0.8172299862 + 0.8180074096 + 0.8187847137 + 0.8195620179 + 0.8203489184 + 0.8211479187 + 0.8219469786 + 0.8227460980 + 0.8235452175 + 0.8243442774 + 0.8251432776 + 0.8259423971 + 0.8267415166 + 0.8275405765 + 0.8283395767 + 0.8291386962 + 0.8299378157 + 0.8307659030 + 0.8315964937 + 0.8324270844 + 0.8332576752 + 0.8340882063 + 0.8349187970 + 0.8357493877 + 0.8365799785 + 0.8374106288 + 0.8382411003 + 0.8390716910 + 0.8399022818 + 0.8407692909 + 0.8416411877 + 0.8425130248 + 0.8433849216 + 0.8442568183 + 0.8451285958 + 0.8460004926 + 0.8468723893 + 0.8477442265 + 0.8486161232 + 0.8494880199 + 0.8503803015 + 0.8513019085 + 0.8522233963 + 0.8531450033 + 0.8540664911 + 0.8549880981 + 0.8559095860 + 0.8568311930 + 0.8577526808 + 0.8586742282 + 0.8595957756 + 0.8605474234 + 0.8615226150 + 0.8624976873 + 0.8634728789 + 0.8644480705 + 0.8654232025 + 0.8663983941 + 0.8673735261 + 0.8683487177 + 0.8693237901 + 0.8703135848 + 0.8713365197 + 0.8723592758 + 0.8733820915 + 0.8744049072 + 0.8754277825 + 0.8764505982 + 0.8774734139 + 0.8784962893 + 0.8795191050 + 0.8805549145 + 0.8816024065 + 0.8826497793 + 0.8836972117 + 0.8847445846 + 0.8857920170 + 0.8868393898 + 0.8878868222 + 0.8889341950 + 0.8899816275 + 0.8910092115 + 0.8920364976 + 0.8930637240 + 0.8940910101 + 0.8951181769 + 0.8961455226 + 0.8971728086 + 0.8981999755 + 0.8992273211 + 0.9002348185 + 0.9011824131 + 0.9021301270 + 0.9030777216 + 0.9040253758 + 0.9049729705 + 0.9059206247 + 0.9068682790 + 0.9078158736 + 0.9087635279 + 0.9097111821 + 0.9105666876 + 0.9113819003 + 0.9121969938 + 0.9130122066 + 0.9138273001 + 0.9146425128 + 0.9154576063 + 0.9162728190 + 0.9170879126 + 0.9179031253 + 0.9187182188 + 0.9195333719 + 0.9202811718 + 0.9209386706 + 0.9215962887 + 0.9222537875 + 0.9229114056 + 0.9235690236 + 0.9242265224 + 0.9248840809 + 0.9255415797 + 0.9261991978 + 0.9268566966 + 0.9275143147 + 0.9281718135 + 0.9288293719 + 0.9294869900 + 0.9301112294 + 0.9306169152 + 0.9311227202 + 0.9316285253 + 0.9321342111 + 0.9326400161 + 0.9331458211 + 0.9336515069 + 0.9341573119 + 0.9346631169 + 0.9351688027 + 0.9356746078 + 0.9361804128 + 0.9366860986 + 0.9371919036 + 0.9376977086 + 0.9382033944 + 0.9387091994 + 0.9392150044 + 0.9397206903 + 0.9401693940 + 0.9405475855 + 0.9409257770 + 0.9413040876 + 0.9416822791 + 0.9420604706 + 0.9424387813 + 0.9428169727 + 0.9431952238 + 0.9435734153 + 0.9439517260 + 0.9443299174 + 0.9447081089 + 0.9450864196 + 0.9454646111 + 0.9458428025 + 0.9462211132 + 0.9465993047 + 0.9469774961 + 0.9473558068 + 0.9477339983 + 0.9481121898 + 0.9484903812 + 0.9488686919 + 0.9492468834 + 0.9496250749 + 0.9500024915 + 0.9502819777 + 0.9505615234 + 0.9508408904 + 0.9511203766 + 0.9513999224 + 0.9516794086 + 0.9519588947 + 0.9522383809 + 0.9525179267 + 0.9527974129 + 0.9530767798 + 0.9533563256 + 0.9536358118 + 0.9539152980 + 0.9541947842 + 0.9544742703 + 0.9547538161 + 0.9550331831 + 0.9553127289 + 0.9555922151 + 0.9558717012 + 0.9561511874 + 0.9564306736 + 0.9567102194 + 0.9569895864 + 0.9572690725 + 0.9575486183 + 0.9578281045 + 0.9581075907 + 0.9583870769 + 0.9586666226 + 0.9589459896 + 0.9592254758 + 0.9595050216 + 0.9597845078 + 0.9600471854 + 0.9602534771 + 0.9604598880 + 0.9606661797 + 0.9608724713 + 0.9610788226 + 0.9612851143 + 0.9614914060 + 0.9616976976 + 0.9619039893 + 0.9621102810 + 0.9623166919 + 0.9625229836 + 0.9627292752 + 0.9629356265 + 0.9631419182 + 0.9633482099 + 0.9635545015 + 0.9637607932 + 0.9639670849 + 0.9641734958 + 0.9643797874 + 0.9645860791 + 0.9647923708 + 0.9649987221 + 0.9652050138 + 0.9654113054 + 0.9656175971 + 0.9658238888 + 0.9660302997 + 0.9662365913 + 0.9664428830 + 0.9666491747 + 0.9668555260 + 0.9670618176 + 0.9672681093 + 0.9674744010 + 0.9676806927 + 0.9678869843 + 0.9680933952 + 0.9682996869 + 0.9685059786 + 0.9687122703 + 0.9689186215 + 0.9691249132 + 0.9693312049 + 0.9695374966 + 0.9697437882 + 0.9699501991 + 0.9701160789 + 0.9702693224 + 0.9704223871 + 0.9705755115 + 0.9707286954 + 0.9708818197 + 0.9710348845 + 0.9711880088 + 0.9713411927 + 0.9714943171 + 0.9716473818 + 0.9718006253 + 0.9719536901 + 0.9721068144 + 0.9722599983 + 0.9724131227 + 0.9725661874 + 0.9727193713 + 0.9728724957 + 0.9730256200 + 0.9731786847 + 0.9733319283 + 0.9734849930 + 0.9736381173 + 0.9737913013 + 0.9739444256 + 0.9740974903 + 0.9742506742 + 0.9744037986 + 0.9745569229 + 0.9747101068 + 0.9748631716 + 0.9750162959 + 0.9751694202 + 0.9753226042 + 0.9754757285 + 0.9756287932 + 0.9757819772 + 0.9759351015 + 0.9760882258 + 0.9762414098 + 0.9763944745 + 0.9765475988 + 0.9767007232 + 0.9768539071 + 0.9770069718 + 0.9771600962 + 0.9773132801 + 0.9774664044 + 0.9776195288 + 0.9777727127 + 0.9779257774 + 0.9780789018 + 0.9782320857 + 0.9783852100 + 0.9785382748 + 0.9786913991 + 0.9788445830 + 0.9789977074 + 0.9791507721 + 0.9793040156 + 0.9794570804 + 0.9796102047 + 0.9797633886 + 0.9799165130 + 0.9800521135 + 0.9801667929 + 0.9802814722 + 0.9803960919 + 0.9805107713 + 0.9806255102 + 0.9807401896 + 0.9808548093 + 0.9809694886 + 0.9810842276 + 0.9811989069 + 0.9813135266 + 0.9814282060 + 0.9815428853 + 0.9816575050 + 0.9817721844 + 0.9818869233 + 0.9820016026 + 0.9821162224 + 0.9822309017 + 0.9823455811 + 0.9824602008 + 0.9825748801 + 0.9826896191 + 0.9828042984 + 0.9829189181 + 0.9830335975 + 0.9831482768 + 0.9832628965 + 0.9833775759 + 0.9834923148 + 0.9836069942 + 0.9837216139 + 0.9838362932 + 0.9839509726 + 0.9840655923 + 0.9841802716 + 0.9842950106 + 0.9844096899 + 0.9845243096 + 0.9846389890 + 0.9847537279 + 0.9848682880 + 0.9849830270 + 0.9850977063 + 0.9852123857 + 0.9853270054 + 0.9854416847 + 0.9855564237 + 0.9856711030 + 0.9857857227 + 0.9859004021 + 0.9860150814 + 0.9861297011 + 0.9862443805 + 0.9863591194 + 0.9864737988 + 0.9865884185 + 0.9867030978 + 0.9868177772 + 0.9869323969 + 0.9870470762 + 0.9871618152 + 0.9872764945 + 0.9873911142 + 0.9875057936 + 0.9876204729 + 0.9877350926 + 0.9878497720 + 0.9879645109 + 0.9880791903 + 0.9881938100 + 0.9883084893 + 0.9884232283 + 0.9885377884 + 0.9886525273 + 0.9887672067 + 0.9888818860 + 0.9889965057 + 0.9891111851 + 0.9892259240 + 0.9893404841 + 0.9894552231 + 0.9895699024 + 0.9896845818 + 0.9897992015 + 0.9899138808 + 0.9900215864 + 0.9901083708 + 0.9901952147 + 0.9902818799 + 0.9903687239 + 0.9904555082 + 0.9905421734 + 0.9906290174 + 0.9907158017 + 0.9908025265 + 0.9908893108 + 0.9909760952 + 0.9910628200 + 0.9911496043 + 0.9912363887 + 0.9913231730 + 0.9914098978 + 0.9914966822 + 0.9915835261 + 0.9916701913 + 0.9917569757 + 0.9918438196 + 0.9919304848 + 0.9920173287 + 0.9921041131 + 0.9921907783 + 0.9922776222 + 0.9923644066 + 0.9924510717 + 0.9925379157 + 0.9926247001 + 0.9927114248 + 0.9927982092 + 0.9928849936 + 0.9929717779 + 0.9930585027 + 0.9931452870 + 0.9932320714 + 0.9933187962 + 0.9934055805 + 0.9934924245 + 0.9935790896 + 0.9936658740 + 0.9937527180 + 0.9938393831 + 0.9939262271 + 0.9940130115 + 0.9940996766 + 0.9941865206 + 0.9942733049 + 0.9943600297 + 0.9944468141 + 0.9945335984 + 0.9946203828 + 0.9947071075 + 0.9947938919 + 0.9948806763 + 0.9949674010 + 0.9950541854 + 0.9951410294 + 0.9952276945 + 0.9953144789 + 0.9954013228 + 0.9954879880 + 0.9955747724 + 0.9956616163 + 0.9957482815 + 0.9958351254 + 0.9959219098 + 0.9960085750 + 0.9960954189 + 0.9961822033 + 0.9962689877 + 0.9963557124 + 0.9964424968 + 0.9965292811 + 0.9966160059 + 0.9967027903 + 0.9967895746 + 0.9968762994 + 0.9969630837 + 0.9970499277 + 0.9971365929 + 0.9972233772 + 0.9973102212 + 0.9973968863 + 0.9974836707 + 0.9975705147 + 0.9976572990 + 0.9977440238 + 0.9978308082 + 0.9979175925 + 0.9980043173 + 0.9980911016 + 0.9981778860 + 0.9982646108 + 0.9983513951 + 0.9984381795 + 0.9985249043 + 0.9986116886 + 0.9986984730 + 0.9987851977 + 0.9988719821 + 0.9989588261 + 0.9990454912 + 0.9991322756 + 0.9992191195 + 0.9993059039 + 0.9993926287 + 0.9994794130 + 0.9995661974 + 0.9996529222 + 0.9997397065 + 0.9998264909 + 0.9999132156 + 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/canon_optura_981113.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/canon_optura_981113.spi1d index b3ee4a4b..ff2cbaea 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/canon_optura_981113.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/canon_optura_981113.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 1 -{ - 0.0000000000 - 0.0135446005 - 0.0202735607 - 0.0249911100 - 0.0297086593 - 0.0336879417 - 0.0376186185 - 0.0414095595 - 0.0449856818 - 0.0485617891 - 0.0520423800 - 0.0554586984 - 0.0588750206 - 0.0622536913 - 0.0656138808 - 0.0689740628 - 0.0723363981 - 0.0756996870 - 0.0790629685 - 0.0824523792 - 0.0858518705 - 0.0892513692 - 0.0926911235 - 0.0961422473 - 0.0995933712 - 0.1030920967 - 0.1065972000 - 0.1101036966 - 0.1136552989 - 0.1172067970 - 0.1207652017 - 0.1243487000 - 0.1279322952 - 0.1315212995 - 0.1351177990 - 0.1387142986 - 0.1423058063 - 0.1458943933 - 0.1494830996 - 0.1530472934 - 0.1566074044 - 0.1601652950 - 0.1636777967 - 0.1671904027 - 0.1706901938 - 0.1741389930 - 0.1775878072 - 0.1810134947 - 0.1843854934 - 0.1877575070 - 0.1911004037 - 0.1943856031 - 0.1976709068 - 0.2009288967 - 0.2041203976 - 0.2073118985 - 0.2104879022 - 0.2135811001 - 0.2166742980 - 0.2197676003 - 0.2227675021 - 0.2257598937 - 0.2287521958 - 0.2316852063 - 0.2345757037 - 0.2374662012 - 0.2403441072 - 0.2431329042 - 0.2459215969 - 0.2487103939 - 0.2514449954 - 0.2541329861 - 0.2568210065 - 0.2595090866 - 0.2621161044 - 0.2647050917 - 0.2672941089 - 0.2698830068 - 0.2723794878 - 0.2748715878 - 0.2773636878 - 0.2798557878 - 0.2822589874 - 0.2846567929 - 0.2870545983 - 0.2894524038 - 0.2917796075 - 0.2940860093 - 0.2963924110 - 0.2986988127 - 0.3009667993 - 0.3031851053 - 0.3054032922 - 0.3076215982 - 0.3098397851 - 0.3119795024 - 0.3141129911 - 0.3162465096 - 0.3183799982 - 0.3204940856 - 0.3225466013 - 0.3245992064 - 0.3266518116 - 0.3287042975 - 0.3307285011 - 0.3327040076 - 0.3346796036 - 0.3366551101 - 0.3386305869 - 0.3405838013 - 0.3424864113 - 0.3443889916 - 0.3462916017 - 0.3481942117 - 0.3500933051 - 0.3519271910 - 0.3537611067 - 0.3555949926 - 0.3574289083 - 0.3592627943 - 0.3610582054 - 0.3628278077 - 0.3645974100 - 0.3663670123 - 0.3681367040 - 0.3699063063 - 0.3716191053 - 0.3733288050 - 0.3750385046 - 0.3767482042 - 0.3784579039 - 0.3801622093 - 0.3818163872 - 0.3834707141 - 0.3851250112 - 0.3867791891 - 0.3884334862 - 0.3900850117 - 0.3916882873 - 0.3932915032 - 0.3948946893 - 0.3964979947 - 0.3981012106 - 0.3997044861 - 0.4012697041 - 0.4028263986 - 0.4043830931 - 0.4059396982 - 0.4074963927 - 0.4090530872 - 0.4105932117 - 0.4121075869 - 0.4136219919 - 0.4151363969 - 0.4166508019 - 0.4181652069 - 0.4196796119 - 0.4211640060 - 0.4226402938 - 0.4241167009 - 0.4255929887 - 0.4270693958 - 0.4285457134 - 0.4300214946 - 0.4314638972 - 0.4329062998 - 0.4343487024 - 0.4357911050 - 0.4372335076 - 0.4386759102 - 0.4401158094 - 0.4415282011 - 0.4429405034 - 0.4443528950 - 0.4457651973 - 0.4471774995 - 0.4485898912 - 0.4500021935 - 0.4513882101 - 0.4527741969 - 0.4541603029 - 0.4555462897 - 0.4569323957 - 0.4583184123 - 0.4597044885 - 0.4610725939 - 0.4624359906 - 0.4637992978 - 0.4651626050 - 0.4665259123 - 0.4678891897 - 0.4692525864 - 0.4706071019 - 0.4719510078 - 0.4732949138 - 0.4746387899 - 0.4759826958 - 0.4773266017 - 0.4786705077 - 0.4800142050 - 0.4813418090 - 0.4826692939 - 0.4839968979 - 0.4853244126 - 0.4866518974 - 0.4879795015 - 0.4893069863 - 0.4906280935 - 0.4919421077 - 0.4932560027 - 0.4945699871 - 0.4958840013 - 0.4971979856 - 0.4985119104 - 0.4998258948 - 0.5011302829 - 0.5024331212 - 0.5037360191 - 0.5050389171 - 0.5063418150 - 0.5076445937 - 0.5089474916 - 0.5102487206 - 0.5115424991 - 0.5128363967 - 0.5141302943 - 0.5154240727 - 0.5167179704 - 0.5180119276 - 0.5193057060 - 0.5205962062 - 0.5218827128 - 0.5231692791 - 0.5244557858 - 0.5257424116 - 0.5270289779 - 0.5283154845 - 0.5296021104 - 0.5308843851 - 0.5321649909 - 0.5334454775 - 0.5347260237 - 0.5360065103 - 0.5372869968 - 0.5385676026 - 0.5398480892 - 0.5411239266 - 0.5423991084 - 0.5436742902 - 0.5449494720 - 0.5462247133 - 0.5474998951 - 0.5487750769 - 0.5500500798 - 0.5513203144 - 0.5525904298 - 0.5538604856 - 0.5551306009 - 0.5564007759 - 0.5576708913 - 0.5589410067 - 0.5602102280 - 0.5614749193 - 0.5627396107 - 0.5640043020 - 0.5652689934 - 0.5665336847 - 0.5677983761 - 0.5690631270 - 0.5703262091 - 0.5715845227 - 0.5728427768 - 0.5741010904 - 0.5753594041 - 0.5766177177 - 0.5778759718 - 0.5791342854 - 0.5803902149 - 0.5816406012 - 0.5828909874 - 0.5841413736 - 0.5853918195 - 0.5866422057 - 0.5878925920 - 0.5891429782 - 0.5903903246 - 0.5916306973 - 0.5928711295 - 0.5941116214 - 0.5953519940 - 0.5965924263 - 0.5978329182 - 0.5990732908 - 0.6003105044 - 0.6015384197 - 0.6027662754 - 0.6039940715 - 0.6052219868 - 0.6064499021 - 0.6076776981 - 0.6089056134 - 0.6101318002 - 0.6113442183 - 0.6125565171 - 0.6137688756 - 0.6149812937 - 0.6161937118 - 0.6174060702 - 0.6186184883 - 0.6198309064 - 0.6210271716 - 0.6222209930 - 0.6234146953 - 0.6246085167 - 0.6258022189 - 0.6269959807 - 0.6281896830 - 0.6293835044 - 0.6305665970 - 0.6317384839 - 0.6329103708 - 0.6340823174 - 0.6352540851 - 0.6364259720 - 0.6375979185 - 0.6387698054 - 0.6399416924 - 0.6410897970 - 0.6422367096 - 0.6433836222 - 0.6445304751 - 0.6456773281 - 0.6468241811 - 0.6479710937 - 0.6491180062 - 0.6502584815 - 0.6513776183 - 0.6524966955 - 0.6536157131 - 0.6547347903 - 0.6558539271 - 0.6569730043 - 0.6580920815 - 0.6592112184 - 0.6603214145 - 0.6614102721 - 0.6624991894 - 0.6635881066 - 0.6646770239 - 0.6657658815 - 0.6668547988 - 0.6679437160 - 0.6690325737 - 0.6701179743 - 0.6711750031 - 0.6722319126 - 0.6732888818 - 0.6743459105 - 0.6754028797 - 0.6764597893 - 0.6775168180 - 0.6785737872 - 0.6796308160 - 0.6806663275 - 0.6816902161 - 0.6827141047 - 0.6837379932 - 0.6847618818 - 0.6857857704 - 0.6868097186 - 0.6878336072 - 0.6888574958 - 0.6898815036 - 0.6908758283 - 0.6918662190 - 0.6928567290 - 0.6938471198 - 0.6948375106 - 0.6958280206 - 0.6968184114 - 0.6978089213 - 0.6987993121 - 0.6997897029 - 0.7007541060 - 0.7017112970 - 0.7026686072 - 0.7036259174 - 0.7045832276 - 0.7055404782 - 0.7064977884 - 0.7074550986 - 0.7084122896 - 0.7093695998 - 0.7103158832 - 0.7112410069 - 0.7121660709 - 0.7130911946 - 0.7140163183 - 0.7149413824 - 0.7158665061 - 0.7167915106 - 0.7177165747 - 0.7186416984 - 0.7195668221 - 0.7204756141 - 0.7213701010 - 0.7222645879 - 0.7231590748 - 0.7240536213 - 0.7249481082 - 0.7258424759 - 0.7267370224 - 0.7276315093 - 0.7285259962 - 0.7294204831 - 0.7303048968 - 0.7311708927 - 0.7320368886 - 0.7329028845 - 0.7337688208 - 0.7346348166 - 0.7355008125 - 0.7363668084 - 0.7372328043 - 0.7380986810 - 0.7389646769 - 0.7398306727 - 0.7406758070 - 0.7415158749 - 0.7423558831 - 0.7431960106 - 0.7440360188 - 0.7448760867 - 0.7457160950 - 0.7465562224 - 0.7473961711 - 0.7482362986 - 0.7490763068 - 0.7499163747 - 0.7507357001 - 0.7515528202 - 0.7523698211 - 0.7531868219 - 0.7540038824 - 0.7548208833 - 0.7556380033 - 0.7564550042 - 0.7572721243 - 0.7580891252 - 0.7589061260 - 0.7597231865 - 0.7605271935 - 0.7613244057 - 0.7621216774 - 0.7629190087 - 0.7637162805 - 0.7645134926 - 0.7653108239 - 0.7661080956 - 0.7669054270 - 0.7677025795 - 0.7684999108 - 0.7692971826 - 0.7700924873 - 0.7708734870 - 0.7716544271 - 0.7724353075 - 0.7732163072 - 0.7739971876 - 0.7747781873 - 0.7755591273 - 0.7763401270 - 0.7771210074 - 0.7779018879 - 0.7786828876 - 0.7794638276 - 0.7802407742 - 0.7810090184 - 0.7817772031 - 0.7825455070 - 0.7833136916 - 0.7840818763 - 0.7848501801 - 0.7856184244 - 0.7863866091 - 0.7871549129 - 0.7879230976 - 0.7886912823 - 0.7894595265 - 0.7902250886 - 0.7909842730 - 0.7917435169 - 0.7925027013 - 0.7932620049 - 0.7940211892 - 0.7947803736 - 0.7955396175 - 0.7962988019 - 0.7970579863 - 0.7978171706 - 0.7985764742 - 0.7993357182 - 0.8000941873 - 0.8008481264 - 0.8016020060 - 0.8023558855 - 0.8031098247 - 0.8038637042 - 0.8046175838 - 0.8053714037 - 0.8061252832 - 0.8068792224 - 0.8076331019 - 0.8083869815 - 0.8091409206 - 0.8098948002 - 0.8106470704 - 0.8113992214 - 0.8121513724 - 0.8129035234 - 0.8136556149 - 0.8144077063 - 0.8151597977 - 0.8159118891 - 0.8166639805 - 0.8174160719 - 0.8181682825 - 0.8189203739 - 0.8196725249 - 0.8204253912 - 0.8211789131 - 0.8219323754 - 0.8226860166 - 0.8234394789 - 0.8241930008 - 0.8249465227 - 0.8257001042 - 0.8264536262 - 0.8272070885 - 0.8279606104 - 0.8287141919 - 0.8294677138 - 0.8302224278 - 0.8309798241 - 0.8317372799 - 0.8324947953 - 0.8332523108 - 0.8340097070 - 0.8347672224 - 0.8355246782 - 0.8362820745 - 0.8370395899 - 0.8377971053 - 0.8385546207 - 0.8393120170 - 0.8400700092 - 0.8408328295 - 0.8415954709 - 0.8423582911 - 0.8431211114 - 0.8438838720 - 0.8446465731 - 0.8454093933 - 0.8461722136 - 0.8469349146 - 0.8476976752 - 0.8484604955 - 0.8492233157 - 0.8499860168 - 0.8507534862 - 0.8515210152 - 0.8522886038 - 0.8530561924 - 0.8538237214 - 0.8545913100 - 0.8553587794 - 0.8561264277 - 0.8568938971 - 0.8576614857 - 0.8584290147 - 0.8591966033 - 0.8599640727 - 0.8607330918 - 0.8615021706 - 0.8622711897 - 0.8630402088 - 0.8638092875 - 0.8645783067 - 0.8653473854 - 0.8661164045 - 0.8668854833 - 0.8676545024 - 0.8684235215 - 0.8691926003 - 0.8699616194 - 0.8707253933 - 0.8714889288 - 0.8722524047 - 0.8730158806 - 0.8737794161 - 0.8745428920 - 0.8753064871 - 0.8760700226 - 0.8768334985 - 0.8775969744 - 0.8783605099 - 0.8791239858 - 0.8798875213 - 0.8806365728 - 0.8813831210 - 0.8821297288 - 0.8828762770 - 0.8836228848 - 0.8843694925 - 0.8851159811 - 0.8858625889 - 0.8866091967 - 0.8873558044 - 0.8881024122 - 0.8888489008 - 0.8895955086 - 0.8903272748 - 0.8910415769 - 0.8917558193 - 0.8924701214 - 0.8931844234 - 0.8938987255 - 0.8946129084 - 0.8953272104 - 0.8960415125 - 0.8967556953 - 0.8974699974 - 0.8981842995 - 0.8988986015 - 0.8996127844 - 0.9003043175 - 0.9009687901 - 0.9016332030 - 0.9022976756 - 0.9029622078 - 0.9036266804 - 0.9042910933 - 0.9049556255 - 0.9056200981 - 0.9062845707 - 0.9069489837 - 0.9076135159 - 0.9082779884 - 0.9089424014 - 0.9096068740 - 0.9102444053 - 0.9108428955 - 0.9114415050 - 0.9120399952 - 0.9126384854 - 0.9132369757 - 0.9138355255 - 0.9144340158 - 0.9150325060 - 0.9156309962 - 0.9162294865 - 0.9168279767 - 0.9174265265 - 0.9180250168 - 0.9186235070 - 0.9192219973 - 0.9198204875 - 0.9203649759 - 0.9208863974 - 0.9214078188 - 0.9219291210 - 0.9224504828 - 0.9229717851 - 0.9234932065 - 0.9240146279 - 0.9245358706 - 0.9250572920 - 0.9255785942 - 0.9261000156 - 0.9266213775 - 0.9271426797 - 0.9276641011 - 0.9281855226 - 0.9287068248 - 0.9292281866 - 0.9297494888 - 0.9302288294 - 0.9306690097 - 0.9311093092 - 0.9315496087 - 0.9319899082 - 0.9324302077 - 0.9328703880 - 0.9333106875 - 0.9337509871 - 0.9341912866 - 0.9346315861 - 0.9350718260 - 0.9355121255 - 0.9359524250 - 0.9363927245 - 0.9368330240 - 0.9372732043 - 0.9377135038 - 0.9381538033 - 0.9385941029 - 0.9390344024 - 0.9394745827 - 0.9399148822 - 0.9402922988 - 0.9406545758 - 0.9410169125 - 0.9413791895 - 0.9417415261 - 0.9421038032 - 0.9424660802 - 0.9428284168 - 0.9431906939 - 0.9435529709 - 0.9439153075 - 0.9442775846 - 0.9446399212 - 0.9450021982 - 0.9453645945 - 0.9457268715 - 0.9460892081 - 0.9464514852 - 0.9468138218 - 0.9471760988 - 0.9475383759 - 0.9479007125 - 0.9482629895 - 0.9486253262 - 0.9489876032 - 0.9493498802 - 0.9497122169 - 0.9500601292 - 0.9503526092 - 0.9506450295 - 0.9509373903 - 0.9512298107 - 0.9515222907 - 0.9518147111 - 0.9521070719 - 0.9523994923 - 0.9526919723 - 0.9529843926 - 0.9532768130 - 0.9535691738 - 0.9538617134 - 0.9541540742 - 0.9544464946 - 0.9547389150 - 0.9550313950 - 0.9553238153 - 0.9556161761 - 0.9559085965 - 0.9562010765 - 0.9564934969 - 0.9567859173 - 0.9570782781 - 0.9573706985 - 0.9576631784 - 0.9579555988 - 0.9582480192 - 0.9585403800 - 0.9588329196 - 0.9591252804 - 0.9594177008 - 0.9597101212 - 0.9600020051 - 0.9602351189 - 0.9604681134 - 0.9607011080 - 0.9609341025 - 0.9611670971 - 0.9614000916 - 0.9616330862 - 0.9618662000 - 0.9620991945 - 0.9623321891 - 0.9625651836 - 0.9627981782 - 0.9630311728 - 0.9632642269 - 0.9634972811 - 0.9637302756 - 0.9639633298 - 0.9641963243 - 0.9644293189 - 0.9646623135 - 0.9648953080 - 0.9651284218 - 0.9653614163 - 0.9655944109 - 0.9658274055 - 0.9660604000 - 0.9662933946 - 0.9665263891 - 0.9667595029 - 0.9669924974 - 0.9672254920 - 0.9674584866 - 0.9676914811 - 0.9679244757 - 0.9681574702 - 0.9683905840 - 0.9686235785 - 0.9688565731 - 0.9690896273 - 0.9693226218 - 0.9695556164 - 0.9697886109 - 0.9700170755 - 0.9702014923 - 0.9703857899 - 0.9705702066 - 0.9707545042 - 0.9709389210 - 0.9711232185 - 0.9713075757 - 0.9714918733 - 0.9716762900 - 0.9718605876 - 0.9720450044 - 0.9722293019 - 0.9724137187 - 0.9725980163 - 0.9727823734 - 0.9729666710 - 0.9731510878 - 0.9733353853 - 0.9735198021 - 0.9737040997 - 0.9738885164 - 0.9740728140 - 0.9742571712 - 0.9744415283 - 0.9746258855 - 0.9748101830 - 0.9749945998 - 0.9751788974 - 0.9753633142 - 0.9755476117 - 0.9757320285 - 0.9759163260 - 0.9761006236 - 0.9762849808 - 0.9764692783 - 0.9766536951 - 0.9768379927 - 0.9770224094 - 0.9772067070 - 0.9773911238 - 0.9775754213 - 0.9777597785 - 0.9779440761 - 0.9781284928 - 0.9783127904 - 0.9784972072 - 0.9786815047 - 0.9788659215 - 0.9790502191 - 0.9792345762 - 0.9794188738 - 0.9796032906 - 0.9797875881 - 0.9799720049 - 0.9801232815 - 0.9802687764 - 0.9804142118 - 0.9805597067 - 0.9807050824 - 0.9808505774 - 0.9809960127 - 0.9811415076 - 0.9812868834 - 0.9814323783 - 0.9815778136 - 0.9817233086 - 0.9818686843 - 0.9820141792 - 0.9821596146 - 0.9823051095 - 0.9824504852 - 0.9825959802 - 0.9827414155 - 0.9828869104 - 0.9830322862 - 0.9831777811 - 0.9833232164 - 0.9834687114 - 0.9836140871 - 0.9837595820 - 0.9839050174 - 0.9840505123 - 0.9841958880 - 0.9843413830 - 0.9844868183 - 0.9846323133 - 0.9847776890 - 0.9849231839 - 0.9850686193 - 0.9852141142 - 0.9853594899 - 0.9855049849 - 0.9856504202 - 0.9857959151 - 0.9859412909 - 0.9860867858 - 0.9862322211 - 0.9863777161 - 0.9865230918 - 0.9866685867 - 0.9868140221 - 0.9869595170 - 0.9871048927 - 0.9872503877 - 0.9873958230 - 0.9875413179 - 0.9876866937 - 0.9878321886 - 0.9879776239 - 0.9881231189 - 0.9882684946 - 0.9884139895 - 0.9885594249 - 0.9887049198 - 0.9888502955 - 0.9889957905 - 0.9891412258 - 0.9892867208 - 0.9894320965 - 0.9895775914 - 0.9897230864 - 0.9898685217 - 0.9900109768 - 0.9901257753 - 0.9902405739 - 0.9903554916 - 0.9904702902 - 0.9905850887 - 0.9906998873 - 0.9908146858 - 0.9909294844 - 0.9910444021 - 0.9911592007 - 0.9912739992 - 0.9913887978 - 0.9915035963 - 0.9916183949 - 0.9917333126 - 0.9918481112 - 0.9919629097 - 0.9920777082 - 0.9921925068 - 0.9923073053 - 0.9924221039 - 0.9925370216 - 0.9926518202 - 0.9927666187 - 0.9928814173 - 0.9929962158 - 0.9931110144 - 0.9932258725 - 0.9933406711 - 0.9934555292 - 0.9935703278 - 0.9936851263 - 0.9937999249 - 0.9939147830 - 0.9940295815 - 0.9941443801 - 0.9942591786 - 0.9943739772 - 0.9944887757 - 0.9946036935 - 0.9947184920 - 0.9948332906 - 0.9949480891 - 0.9950628877 - 0.9951776862 - 0.9952924848 - 0.9954074025 - 0.9955222011 - 0.9956369996 - 0.9957517982 - 0.9958665967 - 0.9959813952 - 0.9960963130 - 0.9962111115 - 0.9963259101 - 0.9964407086 - 0.9965555072 - 0.9966703057 - 0.9967852235 - 0.9969000220 - 0.9970148206 - 0.9971296191 - 0.9972444177 - 0.9973592162 - 0.9974740148 - 0.9975888729 - 0.9977036715 - 0.9978185296 - 0.9979333282 - 0.9980481267 - 0.9981629252 - 0.9982777834 - 0.9983925819 - 0.9985073805 - 0.9986221790 - 0.9987369776 - 0.9988517761 - 0.9989666939 - 0.9990814924 - 0.9991962910 - 0.9993110895 - 0.9994258881 - 0.9995406866 - 0.9996556044 - 0.9997704029 - 0.9998852015 - 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 1 +{ + 0.0000000000 + 0.0135446005 + 0.0202735607 + 0.0249911100 + 0.0297086593 + 0.0336879417 + 0.0376186185 + 0.0414095595 + 0.0449856818 + 0.0485617891 + 0.0520423800 + 0.0554586984 + 0.0588750206 + 0.0622536913 + 0.0656138808 + 0.0689740628 + 0.0723363981 + 0.0756996870 + 0.0790629685 + 0.0824523792 + 0.0858518705 + 0.0892513692 + 0.0926911235 + 0.0961422473 + 0.0995933712 + 0.1030920967 + 0.1065972000 + 0.1101036966 + 0.1136552989 + 0.1172067970 + 0.1207652017 + 0.1243487000 + 0.1279322952 + 0.1315212995 + 0.1351177990 + 0.1387142986 + 0.1423058063 + 0.1458943933 + 0.1494830996 + 0.1530472934 + 0.1566074044 + 0.1601652950 + 0.1636777967 + 0.1671904027 + 0.1706901938 + 0.1741389930 + 0.1775878072 + 0.1810134947 + 0.1843854934 + 0.1877575070 + 0.1911004037 + 0.1943856031 + 0.1976709068 + 0.2009288967 + 0.2041203976 + 0.2073118985 + 0.2104879022 + 0.2135811001 + 0.2166742980 + 0.2197676003 + 0.2227675021 + 0.2257598937 + 0.2287521958 + 0.2316852063 + 0.2345757037 + 0.2374662012 + 0.2403441072 + 0.2431329042 + 0.2459215969 + 0.2487103939 + 0.2514449954 + 0.2541329861 + 0.2568210065 + 0.2595090866 + 0.2621161044 + 0.2647050917 + 0.2672941089 + 0.2698830068 + 0.2723794878 + 0.2748715878 + 0.2773636878 + 0.2798557878 + 0.2822589874 + 0.2846567929 + 0.2870545983 + 0.2894524038 + 0.2917796075 + 0.2940860093 + 0.2963924110 + 0.2986988127 + 0.3009667993 + 0.3031851053 + 0.3054032922 + 0.3076215982 + 0.3098397851 + 0.3119795024 + 0.3141129911 + 0.3162465096 + 0.3183799982 + 0.3204940856 + 0.3225466013 + 0.3245992064 + 0.3266518116 + 0.3287042975 + 0.3307285011 + 0.3327040076 + 0.3346796036 + 0.3366551101 + 0.3386305869 + 0.3405838013 + 0.3424864113 + 0.3443889916 + 0.3462916017 + 0.3481942117 + 0.3500933051 + 0.3519271910 + 0.3537611067 + 0.3555949926 + 0.3574289083 + 0.3592627943 + 0.3610582054 + 0.3628278077 + 0.3645974100 + 0.3663670123 + 0.3681367040 + 0.3699063063 + 0.3716191053 + 0.3733288050 + 0.3750385046 + 0.3767482042 + 0.3784579039 + 0.3801622093 + 0.3818163872 + 0.3834707141 + 0.3851250112 + 0.3867791891 + 0.3884334862 + 0.3900850117 + 0.3916882873 + 0.3932915032 + 0.3948946893 + 0.3964979947 + 0.3981012106 + 0.3997044861 + 0.4012697041 + 0.4028263986 + 0.4043830931 + 0.4059396982 + 0.4074963927 + 0.4090530872 + 0.4105932117 + 0.4121075869 + 0.4136219919 + 0.4151363969 + 0.4166508019 + 0.4181652069 + 0.4196796119 + 0.4211640060 + 0.4226402938 + 0.4241167009 + 0.4255929887 + 0.4270693958 + 0.4285457134 + 0.4300214946 + 0.4314638972 + 0.4329062998 + 0.4343487024 + 0.4357911050 + 0.4372335076 + 0.4386759102 + 0.4401158094 + 0.4415282011 + 0.4429405034 + 0.4443528950 + 0.4457651973 + 0.4471774995 + 0.4485898912 + 0.4500021935 + 0.4513882101 + 0.4527741969 + 0.4541603029 + 0.4555462897 + 0.4569323957 + 0.4583184123 + 0.4597044885 + 0.4610725939 + 0.4624359906 + 0.4637992978 + 0.4651626050 + 0.4665259123 + 0.4678891897 + 0.4692525864 + 0.4706071019 + 0.4719510078 + 0.4732949138 + 0.4746387899 + 0.4759826958 + 0.4773266017 + 0.4786705077 + 0.4800142050 + 0.4813418090 + 0.4826692939 + 0.4839968979 + 0.4853244126 + 0.4866518974 + 0.4879795015 + 0.4893069863 + 0.4906280935 + 0.4919421077 + 0.4932560027 + 0.4945699871 + 0.4958840013 + 0.4971979856 + 0.4985119104 + 0.4998258948 + 0.5011302829 + 0.5024331212 + 0.5037360191 + 0.5050389171 + 0.5063418150 + 0.5076445937 + 0.5089474916 + 0.5102487206 + 0.5115424991 + 0.5128363967 + 0.5141302943 + 0.5154240727 + 0.5167179704 + 0.5180119276 + 0.5193057060 + 0.5205962062 + 0.5218827128 + 0.5231692791 + 0.5244557858 + 0.5257424116 + 0.5270289779 + 0.5283154845 + 0.5296021104 + 0.5308843851 + 0.5321649909 + 0.5334454775 + 0.5347260237 + 0.5360065103 + 0.5372869968 + 0.5385676026 + 0.5398480892 + 0.5411239266 + 0.5423991084 + 0.5436742902 + 0.5449494720 + 0.5462247133 + 0.5474998951 + 0.5487750769 + 0.5500500798 + 0.5513203144 + 0.5525904298 + 0.5538604856 + 0.5551306009 + 0.5564007759 + 0.5576708913 + 0.5589410067 + 0.5602102280 + 0.5614749193 + 0.5627396107 + 0.5640043020 + 0.5652689934 + 0.5665336847 + 0.5677983761 + 0.5690631270 + 0.5703262091 + 0.5715845227 + 0.5728427768 + 0.5741010904 + 0.5753594041 + 0.5766177177 + 0.5778759718 + 0.5791342854 + 0.5803902149 + 0.5816406012 + 0.5828909874 + 0.5841413736 + 0.5853918195 + 0.5866422057 + 0.5878925920 + 0.5891429782 + 0.5903903246 + 0.5916306973 + 0.5928711295 + 0.5941116214 + 0.5953519940 + 0.5965924263 + 0.5978329182 + 0.5990732908 + 0.6003105044 + 0.6015384197 + 0.6027662754 + 0.6039940715 + 0.6052219868 + 0.6064499021 + 0.6076776981 + 0.6089056134 + 0.6101318002 + 0.6113442183 + 0.6125565171 + 0.6137688756 + 0.6149812937 + 0.6161937118 + 0.6174060702 + 0.6186184883 + 0.6198309064 + 0.6210271716 + 0.6222209930 + 0.6234146953 + 0.6246085167 + 0.6258022189 + 0.6269959807 + 0.6281896830 + 0.6293835044 + 0.6305665970 + 0.6317384839 + 0.6329103708 + 0.6340823174 + 0.6352540851 + 0.6364259720 + 0.6375979185 + 0.6387698054 + 0.6399416924 + 0.6410897970 + 0.6422367096 + 0.6433836222 + 0.6445304751 + 0.6456773281 + 0.6468241811 + 0.6479710937 + 0.6491180062 + 0.6502584815 + 0.6513776183 + 0.6524966955 + 0.6536157131 + 0.6547347903 + 0.6558539271 + 0.6569730043 + 0.6580920815 + 0.6592112184 + 0.6603214145 + 0.6614102721 + 0.6624991894 + 0.6635881066 + 0.6646770239 + 0.6657658815 + 0.6668547988 + 0.6679437160 + 0.6690325737 + 0.6701179743 + 0.6711750031 + 0.6722319126 + 0.6732888818 + 0.6743459105 + 0.6754028797 + 0.6764597893 + 0.6775168180 + 0.6785737872 + 0.6796308160 + 0.6806663275 + 0.6816902161 + 0.6827141047 + 0.6837379932 + 0.6847618818 + 0.6857857704 + 0.6868097186 + 0.6878336072 + 0.6888574958 + 0.6898815036 + 0.6908758283 + 0.6918662190 + 0.6928567290 + 0.6938471198 + 0.6948375106 + 0.6958280206 + 0.6968184114 + 0.6978089213 + 0.6987993121 + 0.6997897029 + 0.7007541060 + 0.7017112970 + 0.7026686072 + 0.7036259174 + 0.7045832276 + 0.7055404782 + 0.7064977884 + 0.7074550986 + 0.7084122896 + 0.7093695998 + 0.7103158832 + 0.7112410069 + 0.7121660709 + 0.7130911946 + 0.7140163183 + 0.7149413824 + 0.7158665061 + 0.7167915106 + 0.7177165747 + 0.7186416984 + 0.7195668221 + 0.7204756141 + 0.7213701010 + 0.7222645879 + 0.7231590748 + 0.7240536213 + 0.7249481082 + 0.7258424759 + 0.7267370224 + 0.7276315093 + 0.7285259962 + 0.7294204831 + 0.7303048968 + 0.7311708927 + 0.7320368886 + 0.7329028845 + 0.7337688208 + 0.7346348166 + 0.7355008125 + 0.7363668084 + 0.7372328043 + 0.7380986810 + 0.7389646769 + 0.7398306727 + 0.7406758070 + 0.7415158749 + 0.7423558831 + 0.7431960106 + 0.7440360188 + 0.7448760867 + 0.7457160950 + 0.7465562224 + 0.7473961711 + 0.7482362986 + 0.7490763068 + 0.7499163747 + 0.7507357001 + 0.7515528202 + 0.7523698211 + 0.7531868219 + 0.7540038824 + 0.7548208833 + 0.7556380033 + 0.7564550042 + 0.7572721243 + 0.7580891252 + 0.7589061260 + 0.7597231865 + 0.7605271935 + 0.7613244057 + 0.7621216774 + 0.7629190087 + 0.7637162805 + 0.7645134926 + 0.7653108239 + 0.7661080956 + 0.7669054270 + 0.7677025795 + 0.7684999108 + 0.7692971826 + 0.7700924873 + 0.7708734870 + 0.7716544271 + 0.7724353075 + 0.7732163072 + 0.7739971876 + 0.7747781873 + 0.7755591273 + 0.7763401270 + 0.7771210074 + 0.7779018879 + 0.7786828876 + 0.7794638276 + 0.7802407742 + 0.7810090184 + 0.7817772031 + 0.7825455070 + 0.7833136916 + 0.7840818763 + 0.7848501801 + 0.7856184244 + 0.7863866091 + 0.7871549129 + 0.7879230976 + 0.7886912823 + 0.7894595265 + 0.7902250886 + 0.7909842730 + 0.7917435169 + 0.7925027013 + 0.7932620049 + 0.7940211892 + 0.7947803736 + 0.7955396175 + 0.7962988019 + 0.7970579863 + 0.7978171706 + 0.7985764742 + 0.7993357182 + 0.8000941873 + 0.8008481264 + 0.8016020060 + 0.8023558855 + 0.8031098247 + 0.8038637042 + 0.8046175838 + 0.8053714037 + 0.8061252832 + 0.8068792224 + 0.8076331019 + 0.8083869815 + 0.8091409206 + 0.8098948002 + 0.8106470704 + 0.8113992214 + 0.8121513724 + 0.8129035234 + 0.8136556149 + 0.8144077063 + 0.8151597977 + 0.8159118891 + 0.8166639805 + 0.8174160719 + 0.8181682825 + 0.8189203739 + 0.8196725249 + 0.8204253912 + 0.8211789131 + 0.8219323754 + 0.8226860166 + 0.8234394789 + 0.8241930008 + 0.8249465227 + 0.8257001042 + 0.8264536262 + 0.8272070885 + 0.8279606104 + 0.8287141919 + 0.8294677138 + 0.8302224278 + 0.8309798241 + 0.8317372799 + 0.8324947953 + 0.8332523108 + 0.8340097070 + 0.8347672224 + 0.8355246782 + 0.8362820745 + 0.8370395899 + 0.8377971053 + 0.8385546207 + 0.8393120170 + 0.8400700092 + 0.8408328295 + 0.8415954709 + 0.8423582911 + 0.8431211114 + 0.8438838720 + 0.8446465731 + 0.8454093933 + 0.8461722136 + 0.8469349146 + 0.8476976752 + 0.8484604955 + 0.8492233157 + 0.8499860168 + 0.8507534862 + 0.8515210152 + 0.8522886038 + 0.8530561924 + 0.8538237214 + 0.8545913100 + 0.8553587794 + 0.8561264277 + 0.8568938971 + 0.8576614857 + 0.8584290147 + 0.8591966033 + 0.8599640727 + 0.8607330918 + 0.8615021706 + 0.8622711897 + 0.8630402088 + 0.8638092875 + 0.8645783067 + 0.8653473854 + 0.8661164045 + 0.8668854833 + 0.8676545024 + 0.8684235215 + 0.8691926003 + 0.8699616194 + 0.8707253933 + 0.8714889288 + 0.8722524047 + 0.8730158806 + 0.8737794161 + 0.8745428920 + 0.8753064871 + 0.8760700226 + 0.8768334985 + 0.8775969744 + 0.8783605099 + 0.8791239858 + 0.8798875213 + 0.8806365728 + 0.8813831210 + 0.8821297288 + 0.8828762770 + 0.8836228848 + 0.8843694925 + 0.8851159811 + 0.8858625889 + 0.8866091967 + 0.8873558044 + 0.8881024122 + 0.8888489008 + 0.8895955086 + 0.8903272748 + 0.8910415769 + 0.8917558193 + 0.8924701214 + 0.8931844234 + 0.8938987255 + 0.8946129084 + 0.8953272104 + 0.8960415125 + 0.8967556953 + 0.8974699974 + 0.8981842995 + 0.8988986015 + 0.8996127844 + 0.9003043175 + 0.9009687901 + 0.9016332030 + 0.9022976756 + 0.9029622078 + 0.9036266804 + 0.9042910933 + 0.9049556255 + 0.9056200981 + 0.9062845707 + 0.9069489837 + 0.9076135159 + 0.9082779884 + 0.9089424014 + 0.9096068740 + 0.9102444053 + 0.9108428955 + 0.9114415050 + 0.9120399952 + 0.9126384854 + 0.9132369757 + 0.9138355255 + 0.9144340158 + 0.9150325060 + 0.9156309962 + 0.9162294865 + 0.9168279767 + 0.9174265265 + 0.9180250168 + 0.9186235070 + 0.9192219973 + 0.9198204875 + 0.9203649759 + 0.9208863974 + 0.9214078188 + 0.9219291210 + 0.9224504828 + 0.9229717851 + 0.9234932065 + 0.9240146279 + 0.9245358706 + 0.9250572920 + 0.9255785942 + 0.9261000156 + 0.9266213775 + 0.9271426797 + 0.9276641011 + 0.9281855226 + 0.9287068248 + 0.9292281866 + 0.9297494888 + 0.9302288294 + 0.9306690097 + 0.9311093092 + 0.9315496087 + 0.9319899082 + 0.9324302077 + 0.9328703880 + 0.9333106875 + 0.9337509871 + 0.9341912866 + 0.9346315861 + 0.9350718260 + 0.9355121255 + 0.9359524250 + 0.9363927245 + 0.9368330240 + 0.9372732043 + 0.9377135038 + 0.9381538033 + 0.9385941029 + 0.9390344024 + 0.9394745827 + 0.9399148822 + 0.9402922988 + 0.9406545758 + 0.9410169125 + 0.9413791895 + 0.9417415261 + 0.9421038032 + 0.9424660802 + 0.9428284168 + 0.9431906939 + 0.9435529709 + 0.9439153075 + 0.9442775846 + 0.9446399212 + 0.9450021982 + 0.9453645945 + 0.9457268715 + 0.9460892081 + 0.9464514852 + 0.9468138218 + 0.9471760988 + 0.9475383759 + 0.9479007125 + 0.9482629895 + 0.9486253262 + 0.9489876032 + 0.9493498802 + 0.9497122169 + 0.9500601292 + 0.9503526092 + 0.9506450295 + 0.9509373903 + 0.9512298107 + 0.9515222907 + 0.9518147111 + 0.9521070719 + 0.9523994923 + 0.9526919723 + 0.9529843926 + 0.9532768130 + 0.9535691738 + 0.9538617134 + 0.9541540742 + 0.9544464946 + 0.9547389150 + 0.9550313950 + 0.9553238153 + 0.9556161761 + 0.9559085965 + 0.9562010765 + 0.9564934969 + 0.9567859173 + 0.9570782781 + 0.9573706985 + 0.9576631784 + 0.9579555988 + 0.9582480192 + 0.9585403800 + 0.9588329196 + 0.9591252804 + 0.9594177008 + 0.9597101212 + 0.9600020051 + 0.9602351189 + 0.9604681134 + 0.9607011080 + 0.9609341025 + 0.9611670971 + 0.9614000916 + 0.9616330862 + 0.9618662000 + 0.9620991945 + 0.9623321891 + 0.9625651836 + 0.9627981782 + 0.9630311728 + 0.9632642269 + 0.9634972811 + 0.9637302756 + 0.9639633298 + 0.9641963243 + 0.9644293189 + 0.9646623135 + 0.9648953080 + 0.9651284218 + 0.9653614163 + 0.9655944109 + 0.9658274055 + 0.9660604000 + 0.9662933946 + 0.9665263891 + 0.9667595029 + 0.9669924974 + 0.9672254920 + 0.9674584866 + 0.9676914811 + 0.9679244757 + 0.9681574702 + 0.9683905840 + 0.9686235785 + 0.9688565731 + 0.9690896273 + 0.9693226218 + 0.9695556164 + 0.9697886109 + 0.9700170755 + 0.9702014923 + 0.9703857899 + 0.9705702066 + 0.9707545042 + 0.9709389210 + 0.9711232185 + 0.9713075757 + 0.9714918733 + 0.9716762900 + 0.9718605876 + 0.9720450044 + 0.9722293019 + 0.9724137187 + 0.9725980163 + 0.9727823734 + 0.9729666710 + 0.9731510878 + 0.9733353853 + 0.9735198021 + 0.9737040997 + 0.9738885164 + 0.9740728140 + 0.9742571712 + 0.9744415283 + 0.9746258855 + 0.9748101830 + 0.9749945998 + 0.9751788974 + 0.9753633142 + 0.9755476117 + 0.9757320285 + 0.9759163260 + 0.9761006236 + 0.9762849808 + 0.9764692783 + 0.9766536951 + 0.9768379927 + 0.9770224094 + 0.9772067070 + 0.9773911238 + 0.9775754213 + 0.9777597785 + 0.9779440761 + 0.9781284928 + 0.9783127904 + 0.9784972072 + 0.9786815047 + 0.9788659215 + 0.9790502191 + 0.9792345762 + 0.9794188738 + 0.9796032906 + 0.9797875881 + 0.9799720049 + 0.9801232815 + 0.9802687764 + 0.9804142118 + 0.9805597067 + 0.9807050824 + 0.9808505774 + 0.9809960127 + 0.9811415076 + 0.9812868834 + 0.9814323783 + 0.9815778136 + 0.9817233086 + 0.9818686843 + 0.9820141792 + 0.9821596146 + 0.9823051095 + 0.9824504852 + 0.9825959802 + 0.9827414155 + 0.9828869104 + 0.9830322862 + 0.9831777811 + 0.9833232164 + 0.9834687114 + 0.9836140871 + 0.9837595820 + 0.9839050174 + 0.9840505123 + 0.9841958880 + 0.9843413830 + 0.9844868183 + 0.9846323133 + 0.9847776890 + 0.9849231839 + 0.9850686193 + 0.9852141142 + 0.9853594899 + 0.9855049849 + 0.9856504202 + 0.9857959151 + 0.9859412909 + 0.9860867858 + 0.9862322211 + 0.9863777161 + 0.9865230918 + 0.9866685867 + 0.9868140221 + 0.9869595170 + 0.9871048927 + 0.9872503877 + 0.9873958230 + 0.9875413179 + 0.9876866937 + 0.9878321886 + 0.9879776239 + 0.9881231189 + 0.9882684946 + 0.9884139895 + 0.9885594249 + 0.9887049198 + 0.9888502955 + 0.9889957905 + 0.9891412258 + 0.9892867208 + 0.9894320965 + 0.9895775914 + 0.9897230864 + 0.9898685217 + 0.9900109768 + 0.9901257753 + 0.9902405739 + 0.9903554916 + 0.9904702902 + 0.9905850887 + 0.9906998873 + 0.9908146858 + 0.9909294844 + 0.9910444021 + 0.9911592007 + 0.9912739992 + 0.9913887978 + 0.9915035963 + 0.9916183949 + 0.9917333126 + 0.9918481112 + 0.9919629097 + 0.9920777082 + 0.9921925068 + 0.9923073053 + 0.9924221039 + 0.9925370216 + 0.9926518202 + 0.9927666187 + 0.9928814173 + 0.9929962158 + 0.9931110144 + 0.9932258725 + 0.9933406711 + 0.9934555292 + 0.9935703278 + 0.9936851263 + 0.9937999249 + 0.9939147830 + 0.9940295815 + 0.9941443801 + 0.9942591786 + 0.9943739772 + 0.9944887757 + 0.9946036935 + 0.9947184920 + 0.9948332906 + 0.9949480891 + 0.9950628877 + 0.9951776862 + 0.9952924848 + 0.9954074025 + 0.9955222011 + 0.9956369996 + 0.9957517982 + 0.9958665967 + 0.9959813952 + 0.9960963130 + 0.9962111115 + 0.9963259101 + 0.9964407086 + 0.9965555072 + 0.9966703057 + 0.9967852235 + 0.9969000220 + 0.9970148206 + 0.9971296191 + 0.9972444177 + 0.9973592162 + 0.9974740148 + 0.9975888729 + 0.9977036715 + 0.9978185296 + 0.9979333282 + 0.9980481267 + 0.9981629252 + 0.9982777834 + 0.9983925819 + 0.9985073805 + 0.9986221790 + 0.9987369776 + 0.9988517761 + 0.9989666939 + 0.9990814924 + 0.9991962910 + 0.9993110895 + 0.9994258881 + 0.9995406866 + 0.9996556044 + 0.9997704029 + 0.9998852015 + 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/canon_optura_981114.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/canon_optura_981114.spi1d index 477d237a..2109b75a 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/canon_optura_981114.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/canon_optura_981114.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 1 -{ - 0.0000000000 - 0.0011549980 - 0.0023099950 - 0.0034649931 - 0.0046199900 - 0.0057749879 - 0.0069299862 - 0.0080849826 - 0.0092399810 - 0.0104988897 - 0.0119577302 - 0.0134165697 - 0.0148754101 - 0.0163342599 - 0.0177931003 - 0.0192519408 - 0.0208822303 - 0.0226929598 - 0.0245036893 - 0.0263144094 - 0.0281251408 - 0.0299358703 - 0.0321105719 - 0.0342986286 - 0.0364867002 - 0.0386747718 - 0.0410060883 - 0.0435574502 - 0.0461088084 - 0.0486601703 - 0.0513551086 - 0.0542088412 - 0.0570625700 - 0.0599162988 - 0.0629694685 - 0.0660286620 - 0.0690878481 - 0.0722158924 - 0.0753731802 - 0.0785304680 - 0.0816907808 - 0.0848537087 - 0.0880166367 - 0.0911577269 - 0.0942621008 - 0.0973664671 - 0.1004566029 - 0.1034670025 - 0.1064774022 - 0.1094878018 - 0.1124091968 - 0.1153123975 - 0.1182155982 - 0.1210779995 - 0.1238754019 - 0.1266727000 - 0.1294700056 - 0.1321890950 - 0.1348897964 - 0.1375904977 - 0.1402821988 - 0.1428993046 - 0.1455163062 - 0.1481333971 - 0.1507305056 - 0.1532779038 - 0.1558253020 - 0.1583727002 - 0.1608998030 - 0.1633909047 - 0.1658820063 - 0.1683731973 - 0.1708489954 - 0.1732959002 - 0.1757428944 - 0.1781897992 - 0.1806280017 - 0.1830410063 - 0.1854541004 - 0.1878671944 - 0.1902772933 - 0.1926648021 - 0.1950524002 - 0.1974398941 - 0.1998274028 - 0.2021971047 - 0.2045654058 - 0.2069337070 - 0.2093019933 - 0.2116595954 - 0.2140129060 - 0.2163662016 - 0.2187193930 - 0.2210668027 - 0.2234072983 - 0.2257477045 - 0.2280880958 - 0.2304262072 - 0.2327540070 - 0.2350818068 - 0.2374096066 - 0.2397374958 - 0.2420527041 - 0.2443664074 - 0.2466800958 - 0.2489937991 - 0.2512977123 - 0.2535941005 - 0.2558904886 - 0.2581869066 - 0.2604787052 - 0.2627533972 - 0.2650280893 - 0.2673028111 - 0.2695775032 - 0.2718302011 - 0.2740780115 - 0.2763257027 - 0.2785733938 - 0.2808091044 - 0.2830238938 - 0.2852387130 - 0.2874535024 - 0.2896682024 - 0.2918500006 - 0.2940258980 - 0.2962017953 - 0.2983776927 - 0.3005422950 - 0.3026734889 - 0.3048047125 - 0.3069359958 - 0.3090671897 - 0.3111704886 - 0.3132519126 - 0.3153333068 - 0.3174147904 - 0.3194962144 - 0.3215366006 - 0.3235639036 - 0.3255912066 - 0.3276185095 - 0.3296456933 - 0.3316256106 - 0.3335955143 - 0.3355652988 - 0.3375352025 - 0.3395051062 - 0.3414301872 - 0.3433403969 - 0.3452506065 - 0.3471607864 - 0.3490709960 - 0.3509500921 - 0.3527995944 - 0.3546490967 - 0.3564985991 - 0.3583481908 - 0.3601911962 - 0.3619799018 - 0.3637686074 - 0.3655573130 - 0.3673459888 - 0.3691348135 - 0.3708924949 - 0.3726212978 - 0.3743501008 - 0.3760789037 - 0.3778077066 - 0.3795365095 - 0.3812226951 - 0.3828932047 - 0.3845637143 - 0.3862341940 - 0.3879047036 - 0.3895752132 - 0.3912039101 - 0.3928183019 - 0.3944328129 - 0.3960472047 - 0.3976615965 - 0.3992759883 - 0.4008611143 - 0.4024221897 - 0.4039832950 - 0.4055444002 - 0.4071055949 - 0.4086667001 - 0.4102205038 - 0.4117313921 - 0.4132421911 - 0.4147531092 - 0.4162639976 - 0.4177747965 - 0.4192857146 - 0.4207718074 - 0.4222356975 - 0.4236997068 - 0.4251635969 - 0.4266274869 - 0.4280914068 - 0.4295552969 - 0.4309889078 - 0.4324092865 - 0.4338296950 - 0.4352501035 - 0.4366703928 - 0.4380908012 - 0.4395112097 - 0.4409053028 - 0.4422855079 - 0.4436658025 - 0.4450460970 - 0.4464263916 - 0.4478066862 - 0.4491870105 - 0.4505521953 - 0.4518958032 - 0.4532394111 - 0.4545829892 - 0.4559265971 - 0.4572702050 - 0.4586139023 - 0.4599575102 - 0.4612688124 - 0.4625791907 - 0.4638895094 - 0.4651997983 - 0.4665100873 - 0.4678204060 - 0.4691306949 - 0.4704309106 - 0.4717111886 - 0.4729914069 - 0.4742717147 - 0.4755519032 - 0.4768322110 - 0.4781123996 - 0.4793927073 - 0.4806587100 - 0.4819119871 - 0.4831652939 - 0.4844186008 - 0.4856719077 - 0.4869252145 - 0.4881784916 - 0.4894317091 - 0.4906719029 - 0.4919010997 - 0.4931303859 - 0.4943596125 - 0.4955888987 - 0.4968182147 - 0.4980474114 - 0.4992766976 - 0.5004972219 - 0.5017052293 - 0.5029131770 - 0.5041211247 - 0.5053290725 - 0.5065370798 - 0.5077450871 - 0.5089530945 - 0.5101585984 - 0.5113480091 - 0.5125373006 - 0.5137265921 - 0.5149158835 - 0.5161052942 - 0.5172945857 - 0.5184838772 - 0.5196732879 - 0.5208507776 - 0.5220239162 - 0.5231968760 - 0.5243700147 - 0.5255430937 - 0.5267161727 - 0.5278891921 - 0.5290622711 - 0.5302324891 - 0.5313915014 - 0.5325505733 - 0.5337095857 - 0.5348685980 - 0.5360276103 - 0.5371866226 - 0.5383455753 - 0.5395045877 - 0.5406568050 - 0.5418037772 - 0.5429506898 - 0.5440977216 - 0.5452446938 - 0.5463917255 - 0.5475386977 - 0.5486857295 - 0.5498327017 - 0.5509709120 - 0.5521076918 - 0.5532444715 - 0.5543813109 - 0.5555180907 - 0.5566548705 - 0.5577917099 - 0.5589284897 - 0.5600647926 - 0.5611929893 - 0.5623211861 - 0.5634493828 - 0.5645775795 - 0.5657057762 - 0.5668339729 - 0.5679622293 - 0.5690904260 - 0.5702171922 - 0.5713382959 - 0.5724593997 - 0.5735803843 - 0.5747014880 - 0.5758225918 - 0.5769436955 - 0.5780646801 - 0.5791857839 - 0.5803052783 - 0.5814204812 - 0.5825356245 - 0.5836508274 - 0.5847659707 - 0.5858811736 - 0.5869963765 - 0.5881115794 - 0.5892267823 - 0.5903404951 - 0.5914508104 - 0.5925611258 - 0.5936715007 - 0.5947818160 - 0.5958921909 - 0.5970025063 - 0.5981128812 - 0.5992231965 - 0.6003323793 - 0.6014387012 - 0.6025450826 - 0.6036515236 - 0.6047579050 - 0.6058642268 - 0.6069706082 - 0.6080769897 - 0.6091833115 - 0.6102889180 - 0.6113919020 - 0.6124950051 - 0.6135979891 - 0.6147010922 - 0.6158041954 - 0.6169071794 - 0.6180102825 - 0.6191133857 - 0.6202158928 - 0.6213160753 - 0.6224163771 - 0.6235166788 - 0.6246169209 - 0.6257172227 - 0.6268175244 - 0.6279177070 - 0.6290180087 - 0.6301180124 - 0.6312158108 - 0.6323136091 - 0.6334114075 - 0.6345092058 - 0.6356070042 - 0.6367048025 - 0.6378026009 - 0.6389003992 - 0.6399981976 - 0.6410936117 - 0.6421890855 - 0.6432846189 - 0.6443799734 - 0.6454755068 - 0.6465709805 - 0.6476663947 - 0.6487619281 - 0.6498574018 - 0.6509507895 - 0.6520439982 - 0.6531370878 - 0.6542302966 - 0.6553233862 - 0.6564165950 - 0.6575096846 - 0.6586028934 - 0.6596959829 - 0.6607874036 - 0.6618781090 - 0.6629688740 - 0.6640595794 - 0.6651502848 - 0.6662409902 - 0.6673316956 - 0.6684224010 - 0.6695131063 - 0.6706023216 - 0.6716902852 - 0.6727783084 - 0.6738662720 - 0.6749542952 - 0.6760423779 - 0.6771304011 - 0.6782184243 - 0.6793063879 - 0.6803932190 - 0.6814782023 - 0.6825631261 - 0.6836479902 - 0.6847329140 - 0.6858178973 - 0.6869028211 - 0.6879876852 - 0.6890726089 - 0.6901569963 - 0.6912384033 - 0.6923196912 - 0.6934009790 - 0.6944823861 - 0.6955636740 - 0.6966450214 - 0.6977263093 - 0.6988077164 - 0.6998890042 - 0.7009665966 - 0.7020437121 - 0.7031208873 - 0.7041980028 - 0.7052751780 - 0.7063522935 - 0.7074295282 - 0.7085065842 - 0.7095838189 - 0.7106578946 - 0.7117301822 - 0.7128025293 - 0.7138748169 - 0.7149469852 - 0.7160192728 - 0.7170916200 - 0.7181639075 - 0.7192360759 - 0.7203068137 - 0.7213733792 - 0.7224400043 - 0.7235066295 - 0.7245730758 - 0.7256397009 - 0.7267063260 - 0.7277728915 - 0.7288395166 - 0.7299060822 - 0.7309666276 - 0.7320265770 - 0.7330865860 - 0.7341464758 - 0.7352064848 - 0.7362664938 - 0.7373265028 - 0.7383863926 - 0.7394464016 - 0.7405027151 - 0.7415549159 - 0.7426071763 - 0.7436593771 - 0.7447115779 - 0.7457638979 - 0.7468160987 - 0.7478684187 - 0.7489206195 - 0.7499728203 - 0.7510163188 - 0.7520595193 - 0.7531027794 - 0.7541459799 - 0.7551891804 - 0.7562325001 - 0.7572757006 - 0.7583189011 - 0.7593622208 - 0.7604013085 - 0.7614340186 - 0.7624667287 - 0.7634993792 - 0.7645320892 - 0.7655647993 - 0.7665975094 - 0.7676302195 - 0.7686629295 - 0.7696955800 - 0.7707195878 - 0.7717399001 - 0.7727602124 - 0.7737805247 - 0.7748007774 - 0.7758212090 - 0.7768415213 - 0.7778617740 - 0.7788820863 - 0.7799023986 - 0.7809095979 - 0.7819153070 - 0.7829210758 - 0.7839267850 - 0.7849326134 - 0.7859383225 - 0.7869439721 - 0.7879498005 - 0.7889555097 - 0.7899612784 - 0.7909504771 - 0.7919390202 - 0.7929275036 - 0.7939161062 - 0.7949045897 - 0.7958930731 - 0.7968816757 - 0.7978702188 - 0.7988587022 - 0.7998473048 - 0.8008186221 - 0.8017867804 - 0.8027549982 - 0.8037232161 - 0.8046914935 - 0.8056597114 - 0.8066279292 - 0.8075960875 - 0.8085643053 - 0.8095325232 - 0.8104882836 - 0.8114324808 - 0.8123767972 - 0.8133209944 - 0.8142651916 - 0.8152093887 - 0.8161537051 - 0.8170979023 - 0.8180420995 - 0.8189862967 - 0.8199306130 - 0.8208488226 - 0.8217648864 - 0.8226810098 - 0.8235970736 - 0.8245131969 - 0.8254293203 - 0.8263453841 - 0.8272615075 - 0.8281775713 - 0.8290936947 - 0.8300095201 - 0.8308929205 - 0.8317763209 - 0.8326597214 - 0.8335431218 - 0.8344264030 - 0.8353098035 - 0.8361932039 - 0.8370766044 - 0.8379600048 - 0.8388434052 - 0.8397268057 - 0.8405842185 - 0.8414300084 - 0.8422756791 - 0.8431215286 - 0.8439673185 - 0.8448129892 - 0.8456587791 - 0.8465046287 - 0.8473502994 - 0.8481960893 - 0.8490418792 - 0.8498876095 - 0.8506965041 - 0.8514996767 - 0.8523029089 - 0.8531060815 - 0.8539093137 - 0.8547124267 - 0.8555155993 - 0.8563187718 - 0.8571220040 - 0.8579251766 - 0.8587284088 - 0.8595315814 - 0.8603150249 - 0.8610708714 - 0.8618267775 - 0.8625826240 - 0.8633384705 - 0.8640943170 - 0.8648502231 - 0.8656061292 - 0.8663619161 - 0.8671178222 - 0.8678736091 - 0.8686295152 - 0.8693854213 - 0.8701316118 - 0.8708360195 - 0.8715404272 - 0.8722447157 - 0.8729491234 - 0.8736534715 - 0.8743578196 - 0.8750622272 - 0.8757665753 - 0.8764709830 - 0.8771752715 - 0.8778796792 - 0.8785840869 - 0.8792883754 - 0.8799927831 - 0.8806430101 - 0.8812925816 - 0.8819422126 - 0.8825919032 - 0.8832414746 - 0.8838911057 - 0.8845406771 - 0.8851904273 - 0.8858399987 - 0.8864896297 - 0.8871392012 - 0.8877888918 - 0.8884385228 - 0.8890880942 - 0.8897377253 - 0.8903535008 - 0.8909463286 - 0.8915392160 - 0.8921319842 - 0.8927248120 - 0.8933175802 - 0.8939105272 - 0.8945032954 - 0.8950961232 - 0.8956890106 - 0.8962817788 - 0.8968746066 - 0.8974674940 - 0.8980603218 - 0.8986530900 - 0.8992459178 - 0.8998388052 - 0.9003897905 - 0.9009250998 - 0.9014605284 - 0.9019957781 - 0.9025312066 - 0.9030665755 - 0.9036018848 - 0.9041373134 - 0.9046726227 - 0.9052079916 - 0.9057434201 - 0.9062787294 - 0.9068140984 - 0.9073494077 - 0.9078847766 - 0.9084202051 - 0.9089555144 - 0.9094908834 - 0.9100235105 - 0.9105020761 - 0.9109807014 - 0.9114593863 - 0.9119380116 - 0.9124165773 - 0.9128953218 - 0.9133738875 - 0.9138525128 - 0.9143311977 - 0.9148098230 - 0.9152883887 - 0.9157670736 - 0.9162456989 - 0.9167243242 - 0.9172030091 - 0.9176815748 - 0.9181602001 - 0.9186388850 - 0.9191175103 - 0.9195960760 - 0.9200661778 - 0.9204902053 - 0.9209141135 - 0.9213380814 - 0.9217619896 - 0.9221860170 - 0.9226099253 - 0.9230338931 - 0.9234578013 - 0.9238818288 - 0.9243056774 - 0.9247297049 - 0.9251536131 - 0.9255775213 - 0.9260014892 - 0.9264253974 - 0.9268494248 - 0.9272732735 - 0.9276973009 - 0.9281212091 - 0.9285451770 - 0.9289690852 - 0.9293931127 - 0.9298170209 - 0.9302117229 - 0.9305840731 - 0.9309564829 - 0.9313287735 - 0.9317011833 - 0.9320735931 - 0.9324460030 - 0.9328184128 - 0.9331908226 - 0.9335631728 - 0.9339355826 - 0.9343079925 - 0.9346802831 - 0.9350526929 - 0.9354251027 - 0.9357975125 - 0.9361699224 - 0.9365422726 - 0.9369146824 - 0.9372870922 - 0.9376595020 - 0.9380317926 - 0.9384042025 - 0.9387766123 - 0.9391490221 - 0.9395213723 - 0.9398937821 - 0.9402320981 - 0.9405568838 - 0.9408816099 - 0.9412062764 - 0.9415311217 - 0.9418557882 - 0.9421805739 - 0.9425053000 - 0.9428300858 - 0.9431548119 - 0.9434794784 - 0.9438043237 - 0.9441289902 - 0.9444537759 - 0.9447785020 - 0.9451032877 - 0.9454280138 - 0.9457526803 - 0.9460775256 - 0.9464021921 - 0.9467269778 - 0.9470517039 - 0.9473763704 - 0.9477012157 - 0.9480258822 - 0.9483507276 - 0.9486753941 - 0.9490001798 - 0.9493249059 - 0.9496495724 - 0.9499744177 - 0.9502593279 - 0.9505407810 - 0.9508221745 - 0.9511036873 - 0.9513852000 - 0.9516667128 - 0.9519482255 - 0.9522296190 - 0.9525110722 - 0.9527925849 - 0.9530740976 - 0.9533556104 - 0.9536370039 - 0.9539185166 - 0.9542000294 - 0.9544814825 - 0.9547629952 - 0.9550443888 - 0.9553259015 - 0.9556074142 - 0.9558889270 - 0.9561703801 - 0.9564517736 - 0.9567332864 - 0.9570147991 - 0.9572963119 - 0.9575778246 - 0.9578592181 - 0.9581406713 - 0.9584221840 - 0.9587036967 - 0.9589852095 - 0.9592666030 - 0.9595481157 - 0.9598296285 - 0.9600958228 - 0.9603385925 - 0.9605814219 - 0.9608241916 - 0.9610670209 - 0.9613097906 - 0.9615526199 - 0.9617953897 - 0.9620382190 - 0.9622809887 - 0.9625238180 - 0.9627665877 - 0.9630094171 - 0.9632521868 - 0.9634950161 - 0.9637377858 - 0.9639806151 - 0.9642233849 - 0.9644662142 - 0.9647089839 - 0.9649518132 - 0.9651945829 - 0.9654374123 - 0.9656801820 - 0.9659230113 - 0.9661657810 - 0.9664086103 - 0.9666513801 - 0.9668942094 - 0.9671369791 - 0.9673798084 - 0.9676225781 - 0.9678654075 - 0.9681081772 - 0.9683510065 - 0.9685937762 - 0.9688366055 - 0.9690793753 - 0.9693222046 - 0.9695649743 - 0.9698078036 - 0.9700434208 - 0.9702520967 - 0.9704607725 - 0.9706693888 - 0.9708781242 - 0.9710866809 - 0.9712954164 - 0.9715039730 - 0.9717127085 - 0.9719213247 - 0.9721300006 - 0.9723386168 - 0.9725472927 - 0.9727559090 - 0.9729645848 - 0.9731732011 - 0.9733818769 - 0.9735904932 - 0.9737992287 - 0.9740077853 - 0.9742165208 - 0.9744251966 - 0.9746338129 - 0.9748424888 - 0.9750511050 - 0.9752597809 - 0.9754683971 - 0.9756770730 - 0.9758856893 - 0.9760944247 - 0.9763029814 - 0.9765117168 - 0.9767202735 - 0.9769290090 - 0.9771376252 - 0.9773463011 - 0.9775549173 - 0.9777635932 - 0.9779723287 - 0.9781808853 - 0.9783896208 - 0.9785981774 - 0.9788069129 - 0.9790155292 - 0.9792242050 - 0.9794328213 - 0.9796414971 - 0.9798501134 - 0.9800503850 - 0.9802291989 - 0.9804080129 - 0.9805868864 - 0.9807657003 - 0.9809445143 - 0.9811233282 - 0.9813022017 - 0.9814810157 - 0.9816598296 - 0.9818385839 - 0.9820175171 - 0.9821962714 - 0.9823750854 - 0.9825540185 - 0.9827327728 - 0.9829115868 - 0.9830904007 - 0.9832692742 - 0.9834480882 - 0.9836269021 - 0.9838057160 - 0.9839845896 - 0.9841634035 - 0.9843422174 - 0.9845209718 - 0.9846999049 - 0.9848787189 - 0.9850574732 - 0.9852364063 - 0.9854152203 - 0.9855939746 - 0.9857727885 - 0.9859517217 - 0.9861304760 - 0.9863092899 - 0.9864881039 - 0.9866669774 - 0.9868457913 - 0.9870246053 - 0.9872034192 - 0.9873822927 - 0.9875611067 - 0.9877399206 - 0.9879187942 - 0.9880976081 - 0.9882764220 - 0.9884551764 - 0.9886341095 - 0.9888129234 - 0.9889916778 - 0.9891704917 - 0.9893494248 - 0.9895281792 - 0.9897069931 - 0.9898858070 - 0.9900553226 - 0.9902083278 - 0.9903612733 - 0.9905142784 - 0.9906672835 - 0.9908202887 - 0.9909732938 - 0.9911262989 - 0.9912793040 - 0.9914323092 - 0.9915853143 - 0.9917383194 - 0.9918913245 - 0.9920443296 - 0.9921972752 - 0.9923502803 - 0.9925032854 - 0.9926561713 - 0.9928091764 - 0.9929621816 - 0.9931151867 - 0.9932681918 - 0.9934211969 - 0.9935742021 - 0.9937272072 - 0.9938802123 - 0.9940332174 - 0.9941862226 - 0.9943392277 - 0.9944921732 - 0.9946451783 - 0.9947981834 - 0.9949511886 - 0.9951041937 - 0.9952571988 - 0.9954102039 - 0.9955630898 - 0.9957160950 - 0.9958691001 - 0.9960221052 - 0.9961751103 - 0.9963281155 - 0.9964811206 - 0.9966341257 - 0.9967870712 - 0.9969400764 - 0.9970930815 - 0.9972460866 - 0.9973990917 - 0.9975520968 - 0.9977051020 - 0.9978581071 - 0.9980111122 - 0.9981641173 - 0.9983171225 - 0.9984701276 - 0.9986230135 - 0.9987760186 - 0.9989290237 - 0.9990820289 - 0.9992349744 - 0.9993879795 - 0.9995409846 - 0.9996939898 - 0.9998469949 - 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 1 +{ + 0.0000000000 + 0.0011549980 + 0.0023099950 + 0.0034649931 + 0.0046199900 + 0.0057749879 + 0.0069299862 + 0.0080849826 + 0.0092399810 + 0.0104988897 + 0.0119577302 + 0.0134165697 + 0.0148754101 + 0.0163342599 + 0.0177931003 + 0.0192519408 + 0.0208822303 + 0.0226929598 + 0.0245036893 + 0.0263144094 + 0.0281251408 + 0.0299358703 + 0.0321105719 + 0.0342986286 + 0.0364867002 + 0.0386747718 + 0.0410060883 + 0.0435574502 + 0.0461088084 + 0.0486601703 + 0.0513551086 + 0.0542088412 + 0.0570625700 + 0.0599162988 + 0.0629694685 + 0.0660286620 + 0.0690878481 + 0.0722158924 + 0.0753731802 + 0.0785304680 + 0.0816907808 + 0.0848537087 + 0.0880166367 + 0.0911577269 + 0.0942621008 + 0.0973664671 + 0.1004566029 + 0.1034670025 + 0.1064774022 + 0.1094878018 + 0.1124091968 + 0.1153123975 + 0.1182155982 + 0.1210779995 + 0.1238754019 + 0.1266727000 + 0.1294700056 + 0.1321890950 + 0.1348897964 + 0.1375904977 + 0.1402821988 + 0.1428993046 + 0.1455163062 + 0.1481333971 + 0.1507305056 + 0.1532779038 + 0.1558253020 + 0.1583727002 + 0.1608998030 + 0.1633909047 + 0.1658820063 + 0.1683731973 + 0.1708489954 + 0.1732959002 + 0.1757428944 + 0.1781897992 + 0.1806280017 + 0.1830410063 + 0.1854541004 + 0.1878671944 + 0.1902772933 + 0.1926648021 + 0.1950524002 + 0.1974398941 + 0.1998274028 + 0.2021971047 + 0.2045654058 + 0.2069337070 + 0.2093019933 + 0.2116595954 + 0.2140129060 + 0.2163662016 + 0.2187193930 + 0.2210668027 + 0.2234072983 + 0.2257477045 + 0.2280880958 + 0.2304262072 + 0.2327540070 + 0.2350818068 + 0.2374096066 + 0.2397374958 + 0.2420527041 + 0.2443664074 + 0.2466800958 + 0.2489937991 + 0.2512977123 + 0.2535941005 + 0.2558904886 + 0.2581869066 + 0.2604787052 + 0.2627533972 + 0.2650280893 + 0.2673028111 + 0.2695775032 + 0.2718302011 + 0.2740780115 + 0.2763257027 + 0.2785733938 + 0.2808091044 + 0.2830238938 + 0.2852387130 + 0.2874535024 + 0.2896682024 + 0.2918500006 + 0.2940258980 + 0.2962017953 + 0.2983776927 + 0.3005422950 + 0.3026734889 + 0.3048047125 + 0.3069359958 + 0.3090671897 + 0.3111704886 + 0.3132519126 + 0.3153333068 + 0.3174147904 + 0.3194962144 + 0.3215366006 + 0.3235639036 + 0.3255912066 + 0.3276185095 + 0.3296456933 + 0.3316256106 + 0.3335955143 + 0.3355652988 + 0.3375352025 + 0.3395051062 + 0.3414301872 + 0.3433403969 + 0.3452506065 + 0.3471607864 + 0.3490709960 + 0.3509500921 + 0.3527995944 + 0.3546490967 + 0.3564985991 + 0.3583481908 + 0.3601911962 + 0.3619799018 + 0.3637686074 + 0.3655573130 + 0.3673459888 + 0.3691348135 + 0.3708924949 + 0.3726212978 + 0.3743501008 + 0.3760789037 + 0.3778077066 + 0.3795365095 + 0.3812226951 + 0.3828932047 + 0.3845637143 + 0.3862341940 + 0.3879047036 + 0.3895752132 + 0.3912039101 + 0.3928183019 + 0.3944328129 + 0.3960472047 + 0.3976615965 + 0.3992759883 + 0.4008611143 + 0.4024221897 + 0.4039832950 + 0.4055444002 + 0.4071055949 + 0.4086667001 + 0.4102205038 + 0.4117313921 + 0.4132421911 + 0.4147531092 + 0.4162639976 + 0.4177747965 + 0.4192857146 + 0.4207718074 + 0.4222356975 + 0.4236997068 + 0.4251635969 + 0.4266274869 + 0.4280914068 + 0.4295552969 + 0.4309889078 + 0.4324092865 + 0.4338296950 + 0.4352501035 + 0.4366703928 + 0.4380908012 + 0.4395112097 + 0.4409053028 + 0.4422855079 + 0.4436658025 + 0.4450460970 + 0.4464263916 + 0.4478066862 + 0.4491870105 + 0.4505521953 + 0.4518958032 + 0.4532394111 + 0.4545829892 + 0.4559265971 + 0.4572702050 + 0.4586139023 + 0.4599575102 + 0.4612688124 + 0.4625791907 + 0.4638895094 + 0.4651997983 + 0.4665100873 + 0.4678204060 + 0.4691306949 + 0.4704309106 + 0.4717111886 + 0.4729914069 + 0.4742717147 + 0.4755519032 + 0.4768322110 + 0.4781123996 + 0.4793927073 + 0.4806587100 + 0.4819119871 + 0.4831652939 + 0.4844186008 + 0.4856719077 + 0.4869252145 + 0.4881784916 + 0.4894317091 + 0.4906719029 + 0.4919010997 + 0.4931303859 + 0.4943596125 + 0.4955888987 + 0.4968182147 + 0.4980474114 + 0.4992766976 + 0.5004972219 + 0.5017052293 + 0.5029131770 + 0.5041211247 + 0.5053290725 + 0.5065370798 + 0.5077450871 + 0.5089530945 + 0.5101585984 + 0.5113480091 + 0.5125373006 + 0.5137265921 + 0.5149158835 + 0.5161052942 + 0.5172945857 + 0.5184838772 + 0.5196732879 + 0.5208507776 + 0.5220239162 + 0.5231968760 + 0.5243700147 + 0.5255430937 + 0.5267161727 + 0.5278891921 + 0.5290622711 + 0.5302324891 + 0.5313915014 + 0.5325505733 + 0.5337095857 + 0.5348685980 + 0.5360276103 + 0.5371866226 + 0.5383455753 + 0.5395045877 + 0.5406568050 + 0.5418037772 + 0.5429506898 + 0.5440977216 + 0.5452446938 + 0.5463917255 + 0.5475386977 + 0.5486857295 + 0.5498327017 + 0.5509709120 + 0.5521076918 + 0.5532444715 + 0.5543813109 + 0.5555180907 + 0.5566548705 + 0.5577917099 + 0.5589284897 + 0.5600647926 + 0.5611929893 + 0.5623211861 + 0.5634493828 + 0.5645775795 + 0.5657057762 + 0.5668339729 + 0.5679622293 + 0.5690904260 + 0.5702171922 + 0.5713382959 + 0.5724593997 + 0.5735803843 + 0.5747014880 + 0.5758225918 + 0.5769436955 + 0.5780646801 + 0.5791857839 + 0.5803052783 + 0.5814204812 + 0.5825356245 + 0.5836508274 + 0.5847659707 + 0.5858811736 + 0.5869963765 + 0.5881115794 + 0.5892267823 + 0.5903404951 + 0.5914508104 + 0.5925611258 + 0.5936715007 + 0.5947818160 + 0.5958921909 + 0.5970025063 + 0.5981128812 + 0.5992231965 + 0.6003323793 + 0.6014387012 + 0.6025450826 + 0.6036515236 + 0.6047579050 + 0.6058642268 + 0.6069706082 + 0.6080769897 + 0.6091833115 + 0.6102889180 + 0.6113919020 + 0.6124950051 + 0.6135979891 + 0.6147010922 + 0.6158041954 + 0.6169071794 + 0.6180102825 + 0.6191133857 + 0.6202158928 + 0.6213160753 + 0.6224163771 + 0.6235166788 + 0.6246169209 + 0.6257172227 + 0.6268175244 + 0.6279177070 + 0.6290180087 + 0.6301180124 + 0.6312158108 + 0.6323136091 + 0.6334114075 + 0.6345092058 + 0.6356070042 + 0.6367048025 + 0.6378026009 + 0.6389003992 + 0.6399981976 + 0.6410936117 + 0.6421890855 + 0.6432846189 + 0.6443799734 + 0.6454755068 + 0.6465709805 + 0.6476663947 + 0.6487619281 + 0.6498574018 + 0.6509507895 + 0.6520439982 + 0.6531370878 + 0.6542302966 + 0.6553233862 + 0.6564165950 + 0.6575096846 + 0.6586028934 + 0.6596959829 + 0.6607874036 + 0.6618781090 + 0.6629688740 + 0.6640595794 + 0.6651502848 + 0.6662409902 + 0.6673316956 + 0.6684224010 + 0.6695131063 + 0.6706023216 + 0.6716902852 + 0.6727783084 + 0.6738662720 + 0.6749542952 + 0.6760423779 + 0.6771304011 + 0.6782184243 + 0.6793063879 + 0.6803932190 + 0.6814782023 + 0.6825631261 + 0.6836479902 + 0.6847329140 + 0.6858178973 + 0.6869028211 + 0.6879876852 + 0.6890726089 + 0.6901569963 + 0.6912384033 + 0.6923196912 + 0.6934009790 + 0.6944823861 + 0.6955636740 + 0.6966450214 + 0.6977263093 + 0.6988077164 + 0.6998890042 + 0.7009665966 + 0.7020437121 + 0.7031208873 + 0.7041980028 + 0.7052751780 + 0.7063522935 + 0.7074295282 + 0.7085065842 + 0.7095838189 + 0.7106578946 + 0.7117301822 + 0.7128025293 + 0.7138748169 + 0.7149469852 + 0.7160192728 + 0.7170916200 + 0.7181639075 + 0.7192360759 + 0.7203068137 + 0.7213733792 + 0.7224400043 + 0.7235066295 + 0.7245730758 + 0.7256397009 + 0.7267063260 + 0.7277728915 + 0.7288395166 + 0.7299060822 + 0.7309666276 + 0.7320265770 + 0.7330865860 + 0.7341464758 + 0.7352064848 + 0.7362664938 + 0.7373265028 + 0.7383863926 + 0.7394464016 + 0.7405027151 + 0.7415549159 + 0.7426071763 + 0.7436593771 + 0.7447115779 + 0.7457638979 + 0.7468160987 + 0.7478684187 + 0.7489206195 + 0.7499728203 + 0.7510163188 + 0.7520595193 + 0.7531027794 + 0.7541459799 + 0.7551891804 + 0.7562325001 + 0.7572757006 + 0.7583189011 + 0.7593622208 + 0.7604013085 + 0.7614340186 + 0.7624667287 + 0.7634993792 + 0.7645320892 + 0.7655647993 + 0.7665975094 + 0.7676302195 + 0.7686629295 + 0.7696955800 + 0.7707195878 + 0.7717399001 + 0.7727602124 + 0.7737805247 + 0.7748007774 + 0.7758212090 + 0.7768415213 + 0.7778617740 + 0.7788820863 + 0.7799023986 + 0.7809095979 + 0.7819153070 + 0.7829210758 + 0.7839267850 + 0.7849326134 + 0.7859383225 + 0.7869439721 + 0.7879498005 + 0.7889555097 + 0.7899612784 + 0.7909504771 + 0.7919390202 + 0.7929275036 + 0.7939161062 + 0.7949045897 + 0.7958930731 + 0.7968816757 + 0.7978702188 + 0.7988587022 + 0.7998473048 + 0.8008186221 + 0.8017867804 + 0.8027549982 + 0.8037232161 + 0.8046914935 + 0.8056597114 + 0.8066279292 + 0.8075960875 + 0.8085643053 + 0.8095325232 + 0.8104882836 + 0.8114324808 + 0.8123767972 + 0.8133209944 + 0.8142651916 + 0.8152093887 + 0.8161537051 + 0.8170979023 + 0.8180420995 + 0.8189862967 + 0.8199306130 + 0.8208488226 + 0.8217648864 + 0.8226810098 + 0.8235970736 + 0.8245131969 + 0.8254293203 + 0.8263453841 + 0.8272615075 + 0.8281775713 + 0.8290936947 + 0.8300095201 + 0.8308929205 + 0.8317763209 + 0.8326597214 + 0.8335431218 + 0.8344264030 + 0.8353098035 + 0.8361932039 + 0.8370766044 + 0.8379600048 + 0.8388434052 + 0.8397268057 + 0.8405842185 + 0.8414300084 + 0.8422756791 + 0.8431215286 + 0.8439673185 + 0.8448129892 + 0.8456587791 + 0.8465046287 + 0.8473502994 + 0.8481960893 + 0.8490418792 + 0.8498876095 + 0.8506965041 + 0.8514996767 + 0.8523029089 + 0.8531060815 + 0.8539093137 + 0.8547124267 + 0.8555155993 + 0.8563187718 + 0.8571220040 + 0.8579251766 + 0.8587284088 + 0.8595315814 + 0.8603150249 + 0.8610708714 + 0.8618267775 + 0.8625826240 + 0.8633384705 + 0.8640943170 + 0.8648502231 + 0.8656061292 + 0.8663619161 + 0.8671178222 + 0.8678736091 + 0.8686295152 + 0.8693854213 + 0.8701316118 + 0.8708360195 + 0.8715404272 + 0.8722447157 + 0.8729491234 + 0.8736534715 + 0.8743578196 + 0.8750622272 + 0.8757665753 + 0.8764709830 + 0.8771752715 + 0.8778796792 + 0.8785840869 + 0.8792883754 + 0.8799927831 + 0.8806430101 + 0.8812925816 + 0.8819422126 + 0.8825919032 + 0.8832414746 + 0.8838911057 + 0.8845406771 + 0.8851904273 + 0.8858399987 + 0.8864896297 + 0.8871392012 + 0.8877888918 + 0.8884385228 + 0.8890880942 + 0.8897377253 + 0.8903535008 + 0.8909463286 + 0.8915392160 + 0.8921319842 + 0.8927248120 + 0.8933175802 + 0.8939105272 + 0.8945032954 + 0.8950961232 + 0.8956890106 + 0.8962817788 + 0.8968746066 + 0.8974674940 + 0.8980603218 + 0.8986530900 + 0.8992459178 + 0.8998388052 + 0.9003897905 + 0.9009250998 + 0.9014605284 + 0.9019957781 + 0.9025312066 + 0.9030665755 + 0.9036018848 + 0.9041373134 + 0.9046726227 + 0.9052079916 + 0.9057434201 + 0.9062787294 + 0.9068140984 + 0.9073494077 + 0.9078847766 + 0.9084202051 + 0.9089555144 + 0.9094908834 + 0.9100235105 + 0.9105020761 + 0.9109807014 + 0.9114593863 + 0.9119380116 + 0.9124165773 + 0.9128953218 + 0.9133738875 + 0.9138525128 + 0.9143311977 + 0.9148098230 + 0.9152883887 + 0.9157670736 + 0.9162456989 + 0.9167243242 + 0.9172030091 + 0.9176815748 + 0.9181602001 + 0.9186388850 + 0.9191175103 + 0.9195960760 + 0.9200661778 + 0.9204902053 + 0.9209141135 + 0.9213380814 + 0.9217619896 + 0.9221860170 + 0.9226099253 + 0.9230338931 + 0.9234578013 + 0.9238818288 + 0.9243056774 + 0.9247297049 + 0.9251536131 + 0.9255775213 + 0.9260014892 + 0.9264253974 + 0.9268494248 + 0.9272732735 + 0.9276973009 + 0.9281212091 + 0.9285451770 + 0.9289690852 + 0.9293931127 + 0.9298170209 + 0.9302117229 + 0.9305840731 + 0.9309564829 + 0.9313287735 + 0.9317011833 + 0.9320735931 + 0.9324460030 + 0.9328184128 + 0.9331908226 + 0.9335631728 + 0.9339355826 + 0.9343079925 + 0.9346802831 + 0.9350526929 + 0.9354251027 + 0.9357975125 + 0.9361699224 + 0.9365422726 + 0.9369146824 + 0.9372870922 + 0.9376595020 + 0.9380317926 + 0.9384042025 + 0.9387766123 + 0.9391490221 + 0.9395213723 + 0.9398937821 + 0.9402320981 + 0.9405568838 + 0.9408816099 + 0.9412062764 + 0.9415311217 + 0.9418557882 + 0.9421805739 + 0.9425053000 + 0.9428300858 + 0.9431548119 + 0.9434794784 + 0.9438043237 + 0.9441289902 + 0.9444537759 + 0.9447785020 + 0.9451032877 + 0.9454280138 + 0.9457526803 + 0.9460775256 + 0.9464021921 + 0.9467269778 + 0.9470517039 + 0.9473763704 + 0.9477012157 + 0.9480258822 + 0.9483507276 + 0.9486753941 + 0.9490001798 + 0.9493249059 + 0.9496495724 + 0.9499744177 + 0.9502593279 + 0.9505407810 + 0.9508221745 + 0.9511036873 + 0.9513852000 + 0.9516667128 + 0.9519482255 + 0.9522296190 + 0.9525110722 + 0.9527925849 + 0.9530740976 + 0.9533556104 + 0.9536370039 + 0.9539185166 + 0.9542000294 + 0.9544814825 + 0.9547629952 + 0.9550443888 + 0.9553259015 + 0.9556074142 + 0.9558889270 + 0.9561703801 + 0.9564517736 + 0.9567332864 + 0.9570147991 + 0.9572963119 + 0.9575778246 + 0.9578592181 + 0.9581406713 + 0.9584221840 + 0.9587036967 + 0.9589852095 + 0.9592666030 + 0.9595481157 + 0.9598296285 + 0.9600958228 + 0.9603385925 + 0.9605814219 + 0.9608241916 + 0.9610670209 + 0.9613097906 + 0.9615526199 + 0.9617953897 + 0.9620382190 + 0.9622809887 + 0.9625238180 + 0.9627665877 + 0.9630094171 + 0.9632521868 + 0.9634950161 + 0.9637377858 + 0.9639806151 + 0.9642233849 + 0.9644662142 + 0.9647089839 + 0.9649518132 + 0.9651945829 + 0.9654374123 + 0.9656801820 + 0.9659230113 + 0.9661657810 + 0.9664086103 + 0.9666513801 + 0.9668942094 + 0.9671369791 + 0.9673798084 + 0.9676225781 + 0.9678654075 + 0.9681081772 + 0.9683510065 + 0.9685937762 + 0.9688366055 + 0.9690793753 + 0.9693222046 + 0.9695649743 + 0.9698078036 + 0.9700434208 + 0.9702520967 + 0.9704607725 + 0.9706693888 + 0.9708781242 + 0.9710866809 + 0.9712954164 + 0.9715039730 + 0.9717127085 + 0.9719213247 + 0.9721300006 + 0.9723386168 + 0.9725472927 + 0.9727559090 + 0.9729645848 + 0.9731732011 + 0.9733818769 + 0.9735904932 + 0.9737992287 + 0.9740077853 + 0.9742165208 + 0.9744251966 + 0.9746338129 + 0.9748424888 + 0.9750511050 + 0.9752597809 + 0.9754683971 + 0.9756770730 + 0.9758856893 + 0.9760944247 + 0.9763029814 + 0.9765117168 + 0.9767202735 + 0.9769290090 + 0.9771376252 + 0.9773463011 + 0.9775549173 + 0.9777635932 + 0.9779723287 + 0.9781808853 + 0.9783896208 + 0.9785981774 + 0.9788069129 + 0.9790155292 + 0.9792242050 + 0.9794328213 + 0.9796414971 + 0.9798501134 + 0.9800503850 + 0.9802291989 + 0.9804080129 + 0.9805868864 + 0.9807657003 + 0.9809445143 + 0.9811233282 + 0.9813022017 + 0.9814810157 + 0.9816598296 + 0.9818385839 + 0.9820175171 + 0.9821962714 + 0.9823750854 + 0.9825540185 + 0.9827327728 + 0.9829115868 + 0.9830904007 + 0.9832692742 + 0.9834480882 + 0.9836269021 + 0.9838057160 + 0.9839845896 + 0.9841634035 + 0.9843422174 + 0.9845209718 + 0.9846999049 + 0.9848787189 + 0.9850574732 + 0.9852364063 + 0.9854152203 + 0.9855939746 + 0.9857727885 + 0.9859517217 + 0.9861304760 + 0.9863092899 + 0.9864881039 + 0.9866669774 + 0.9868457913 + 0.9870246053 + 0.9872034192 + 0.9873822927 + 0.9875611067 + 0.9877399206 + 0.9879187942 + 0.9880976081 + 0.9882764220 + 0.9884551764 + 0.9886341095 + 0.9888129234 + 0.9889916778 + 0.9891704917 + 0.9893494248 + 0.9895281792 + 0.9897069931 + 0.9898858070 + 0.9900553226 + 0.9902083278 + 0.9903612733 + 0.9905142784 + 0.9906672835 + 0.9908202887 + 0.9909732938 + 0.9911262989 + 0.9912793040 + 0.9914323092 + 0.9915853143 + 0.9917383194 + 0.9918913245 + 0.9920443296 + 0.9921972752 + 0.9923502803 + 0.9925032854 + 0.9926561713 + 0.9928091764 + 0.9929621816 + 0.9931151867 + 0.9932681918 + 0.9934211969 + 0.9935742021 + 0.9937272072 + 0.9938802123 + 0.9940332174 + 0.9941862226 + 0.9943392277 + 0.9944921732 + 0.9946451783 + 0.9947981834 + 0.9949511886 + 0.9951041937 + 0.9952571988 + 0.9954102039 + 0.9955630898 + 0.9957160950 + 0.9958691001 + 0.9960221052 + 0.9961751103 + 0.9963281155 + 0.9964811206 + 0.9966341257 + 0.9967870712 + 0.9969400764 + 0.9970930815 + 0.9972460866 + 0.9973990917 + 0.9975520968 + 0.9977051020 + 0.9978581071 + 0.9980111122 + 0.9981641173 + 0.9983171225 + 0.9984701276 + 0.9986230135 + 0.9987760186 + 0.9989290237 + 0.9990820289 + 0.9992349744 + 0.9993879795 + 0.9995409846 + 0.9996939898 + 0.9998469949 + 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/eastman_double_x_neg_12min.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/eastman_double_x_neg_12min.spi1d index d5e4ea9d..5b365d46 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/eastman_double_x_neg_12min.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/eastman_double_x_neg_12min.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 1 -{ - 0.0000000000 - 0.0007793432 - 0.0016043870 - 0.0025048309 - 0.0034564850 - 0.0044531059 - 0.0054927212 - 0.0065743062 - 0.0076966030 - 0.0088652465 - 0.0100406297 - 0.0112482598 - 0.0124893002 - 0.0137513904 - 0.0150318900 - 0.0163053796 - 0.0175968707 - 0.0188959707 - 0.0202209800 - 0.0215417892 - 0.0228415392 - 0.0241345204 - 0.0254166108 - 0.0267051402 - 0.0279715508 - 0.0292299706 - 0.0305107906 - 0.0318148397 - 0.0331244394 - 0.0344182886 - 0.0356710814 - 0.0369000100 - 0.0381264500 - 0.0393604413 - 0.0405932292 - 0.0418275297 - 0.0430539995 - 0.0442756005 - 0.0454925410 - 0.0467148200 - 0.0479568392 - 0.0492201187 - 0.0505055487 - 0.0518039688 - 0.0530999005 - 0.0543935411 - 0.0556728095 - 0.0569286086 - 0.0581700616 - 0.0593975298 - 0.0606211387 - 0.0618490316 - 0.0630754977 - 0.0642991737 - 0.0655274168 - 0.0667622983 - 0.0679954067 - 0.0692288280 - 0.0704668984 - 0.0717051104 - 0.0729388893 - 0.0741742104 - 0.0754114315 - 0.0766490623 - 0.0779046863 - 0.0791758820 - 0.0804568231 - 0.0817457736 - 0.0830455273 - 0.0843512416 - 0.0856669694 - 0.0869878605 - 0.0883099586 - 0.0896294117 - 0.0909416676 - 0.0922398567 - 0.0935223699 - 0.0948002487 - 0.0960806310 - 0.0973561779 - 0.0986227468 - 0.0998791903 - 0.1011310965 - 0.1023767963 - 0.1036218032 - 0.1048732996 - 0.1061237976 - 0.1073772013 - 0.1086406037 - 0.1099131033 - 0.1111890003 - 0.1124633029 - 0.1137432009 - 0.1150280014 - 0.1163161024 - 0.1176021993 - 0.1188817993 - 0.1201585010 - 0.1214374006 - 0.1227219999 - 0.1240141019 - 0.1253069043 - 0.1266023964 - 0.1279031932 - 0.1292033046 - 0.1305052042 - 0.1318120956 - 0.1331252009 - 0.1344417036 - 0.1357488036 - 0.1370531023 - 0.1383568048 - 0.1396605074 - 0.1409630030 - 0.1422598958 - 0.1435438991 - 0.1448162943 - 0.1460811943 - 0.1473405957 - 0.1486002952 - 0.1498605013 - 0.1511213034 - 0.1523707062 - 0.1536183953 - 0.1548663974 - 0.1561181992 - 0.1573731005 - 0.1586305052 - 0.1598910987 - 0.1611555964 - 0.1624190062 - 0.1636800021 - 0.1649398953 - 0.1662012041 - 0.1674640030 - 0.1687299013 - 0.1699973047 - 0.1712664962 - 0.1725308001 - 0.1737935990 - 0.1750548929 - 0.1763173938 - 0.1775805950 - 0.1788450032 - 0.1801103055 - 0.1813762039 - 0.1826315969 - 0.1838831007 - 0.1851277947 - 0.1863732934 - 0.1876191944 - 0.1888664067 - 0.1901142001 - 0.1913629025 - 0.1926099062 - 0.1938558966 - 0.1950968057 - 0.1963375956 - 0.1975809038 - 0.1988252997 - 0.2000714988 - 0.2013193965 - 0.2025685012 - 0.2038163990 - 0.2050641030 - 0.2063076943 - 0.2075518072 - 0.2087996006 - 0.2100487053 - 0.2113008052 - 0.2125546932 - 0.2138112932 - 0.2150679976 - 0.2163249999 - 0.2175804973 - 0.2188349068 - 0.2200877964 - 0.2213395983 - 0.2225918025 - 0.2238440067 - 0.2250992060 - 0.2263558954 - 0.2276154011 - 0.2288765013 - 0.2301405072 - 0.2314060032 - 0.2326692939 - 0.2339307964 - 0.2351918072 - 0.2364521027 - 0.2377136946 - 0.2389771044 - 0.2402402014 - 0.2415028960 - 0.2427645028 - 0.2440197021 - 0.2452747971 - 0.2465146035 - 0.2477540970 - 0.2489840984 - 0.2502116859 - 0.2514351010 - 0.2526553869 - 0.2538737059 - 0.2550880015 - 0.2563014030 - 0.2575060129 - 0.2587105036 - 0.2599005997 - 0.2610872090 - 0.2622725964 - 0.2634567022 - 0.2646411955 - 0.2658275068 - 0.2670137882 - 0.2682031095 - 0.2693929970 - 0.2705830038 - 0.2717730105 - 0.2729629874 - 0.2741507888 - 0.2753385901 - 0.2765288949 - 0.2777211964 - 0.2789132893 - 0.2801041007 - 0.2812949121 - 0.2824903131 - 0.2836886942 - 0.2848882973 - 0.2861017883 - 0.2873153090 - 0.2885307968 - 0.2897481024 - 0.2909654081 - 0.2921859026 - 0.2934064865 - 0.2946256995 - 0.2958427072 - 0.2970597148 - 0.2982782125 - 0.2994971871 - 0.3007166982 - 0.3019422889 - 0.3031679094 - 0.3043949902 - 0.3056245148 - 0.3068540096 - 0.3080824018 - 0.3093104064 - 0.3105382919 - 0.3117617965 - 0.3129849136 - 0.3142082095 - 0.3154324889 - 0.3166567981 - 0.3178820908 - 0.3191089034 - 0.3203358054 - 0.3215647936 - 0.3227958977 - 0.3240269125 - 0.3252558112 - 0.3264834881 - 0.3277111948 - 0.3289369941 - 0.3301621974 - 0.3313874006 - 0.3326055110 - 0.3338221014 - 0.3350386918 - 0.3362470865 - 0.3374541104 - 0.3386611044 - 0.3398616016 - 0.3410609961 - 0.3422603905 - 0.3434514999 - 0.3446409106 - 0.3458304107 - 0.3470165133 - 0.3482016921 - 0.3493868113 - 0.3505715132 - 0.3517560065 - 0.3529404998 - 0.3541254103 - 0.3553107083 - 0.3564960063 - 0.3576815128 - 0.3588675857 - 0.3600535989 - 0.3612394929 - 0.3624249995 - 0.3636103868 - 0.3647958934 - 0.3659862876 - 0.3671767116 - 0.3683671057 - 0.3695602119 - 0.3707543015 - 0.3719483912 - 0.3731409013 - 0.3743312061 - 0.3755215108 - 0.3767108917 - 0.3778904974 - 0.3790701926 - 0.3802497983 - 0.3814249933 - 0.3825984001 - 0.3837718070 - 0.3849433959 - 0.3861109912 - 0.3872785866 - 0.3884460926 - 0.3896096051 - 0.3907724023 - 0.3919351995 - 0.3930951953 - 0.3942511082 - 0.3954069018 - 0.3965628147 - 0.3977113962 - 0.3988589942 - 0.4000065923 - 0.4011510909 - 0.4022890925 - 0.4034270048 - 0.4045650065 - 0.4056946933 - 0.4068213105 - 0.4079478085 - 0.4090737104 - 0.4101923108 - 0.4113109112 - 0.4124295115 - 0.4135465026 - 0.4146609008 - 0.4157753885 - 0.4168899059 - 0.4180073142 - 0.4191260040 - 0.4202446938 - 0.4213634133 - 0.4224787951 - 0.4235939085 - 0.4247091115 - 0.4258244038 - 0.4269405007 - 0.4280565977 - 0.4291726947 - 0.4302915931 - 0.4314163029 - 0.4325410128 - 0.4336656928 - 0.4347921908 - 0.4359208047 - 0.4370495081 - 0.4381780922 - 0.4393090010 - 0.4404419065 - 0.4415748119 - 0.4427076876 - 0.4438371062 - 0.4449638128 - 0.4460904002 - 0.4472169876 - 0.4483405054 - 0.4494614005 - 0.4505822957 - 0.4517031908 - 0.4528234005 - 0.4539428949 - 0.4550623894 - 0.4561819136 - 0.4572989047 - 0.4584122002 - 0.4595254064 - 0.4606387019 - 0.4617524147 - 0.4628674090 - 0.4639823139 - 0.4650971889 - 0.4662118852 - 0.4673239887 - 0.4684360921 - 0.4695481956 - 0.4706602991 - 0.4717625082 - 0.4728631973 - 0.4739638865 - 0.4750646055 - 0.4761615098 - 0.4772560000 - 0.4783504903 - 0.4794450104 - 0.4805378914 - 0.4816266894 - 0.4827154875 - 0.4838042855 - 0.4848932028 - 0.4859696925 - 0.4870451093 - 0.4881204069 - 0.4891957939 - 0.4902659059 - 0.4913307130 - 0.4923956096 - 0.4934605062 - 0.4945251942 - 0.4955861866 - 0.4966472089 - 0.4977082014 - 0.4987691939 - 0.4998275936 - 0.5008835793 - 0.5019395947 - 0.5029956102 - 0.5040516257 - 0.5051162243 - 0.5061810017 - 0.5072458982 - 0.5083106756 - 0.5093780756 - 0.5104498863 - 0.5115215778 - 0.5125933886 - 0.5136651993 - 0.5147346854 - 0.5158032179 - 0.5168716908 - 0.5179402232 - 0.5190086961 - 0.5200641155 - 0.5211194158 - 0.5221747160 - 0.5232300758 - 0.5242825747 - 0.5253263116 - 0.5263699889 - 0.5274137259 - 0.5284574032 - 0.5294985771 - 0.5305367112 - 0.5315749049 - 0.5326129794 - 0.5336511135 - 0.5346894860 - 0.5357280970 - 0.5367668271 - 0.5378053784 - 0.5388441086 - 0.5398868918 - 0.5409319997 - 0.5419769883 - 0.5430220962 - 0.5440670848 - 0.5451143980 - 0.5461627245 - 0.5472109914 - 0.5482593179 - 0.5493075848 - 0.5503467917 - 0.5513814092 - 0.5524160266 - 0.5534505844 - 0.5544852018 - 0.5555140972 - 0.5565391183 - 0.5575640798 - 0.5585891008 - 0.5596141219 - 0.5606383085 - 0.5616616011 - 0.5626850128 - 0.5637083054 - 0.5647315979 - 0.5657541752 - 0.5667750239 - 0.5677956939 - 0.5688164234 - 0.5698372126 - 0.5708575845 - 0.5718625784 - 0.5728675723 - 0.5738726258 - 0.5748776197 - 0.5758826137 - 0.5768746138 - 0.5778616071 - 0.5788486004 - 0.5798354745 - 0.5808225274 - 0.5818051100 - 0.5827798247 - 0.5837545991 - 0.5847293139 - 0.5857040882 - 0.5866789222 - 0.5876461267 - 0.5886127949 - 0.5895795822 - 0.5905464292 - 0.5915130973 - 0.5924791098 - 0.5934441090 - 0.5944091082 - 0.5953741074 - 0.5963391066 - 0.5973042250 - 0.5982726812 - 0.5992414951 - 0.6002103090 - 0.6011791825 - 0.6021479964 - 0.6031122804 - 0.6040681005 - 0.6050239205 - 0.6059796810 - 0.6069355011 - 0.6078913212 - 0.6088355184 - 0.6097744703 - 0.6107134223 - 0.6116523743 - 0.6125913858 - 0.6135302782 - 0.6144611835 - 0.6153919101 - 0.6163225174 - 0.6172531843 - 0.6181837916 - 0.6191145778 - 0.6200459003 - 0.6209772229 - 0.6219084859 - 0.6228398085 - 0.6237710118 - 0.6247057915 - 0.6256468892 - 0.6265881062 - 0.6275292039 - 0.6284703016 - 0.6294115186 - 0.6303575039 - 0.6313095093 - 0.6322615147 - 0.6332135797 - 0.6341655850 - 0.6351175904 - 0.6360645294 - 0.6370056272 - 0.6379467249 - 0.6388878822 - 0.6398289800 - 0.6407700777 - 0.6417067051 - 0.6426376104 - 0.6435685158 - 0.6444994211 - 0.6454303265 - 0.6463611126 - 0.6472914815 - 0.6482210159 - 0.6491504908 - 0.6500799060 - 0.6510093808 - 0.6519389153 - 0.6528680921 - 0.6537961960 - 0.6547242999 - 0.6556524038 - 0.6565805078 - 0.6575086713 - 0.6584367752 - 0.6593579054 - 0.6602786779 - 0.6611993909 - 0.6621202230 - 0.6630408764 - 0.6639617085 - 0.6648803949 - 0.6657980084 - 0.6667156219 - 0.6676332951 - 0.6685509086 - 0.6694685221 - 0.6703848243 - 0.6712977886 - 0.6722108126 - 0.6731237769 - 0.6740368009 - 0.6749498248 - 0.6758627892 - 0.6767727733 - 0.6776821017 - 0.6785914898 - 0.6795008779 - 0.6804103255 - 0.6813195944 - 0.6822264791 - 0.6831287146 - 0.6840307713 - 0.6849328876 - 0.6858351231 - 0.6867371798 - 0.6876392961 - 0.6885449290 - 0.6894515753 - 0.6903582811 - 0.6912649274 - 0.6921715736 - 0.6930782795 - 0.6939843893 - 0.6948862076 - 0.6957879066 - 0.6966897249 - 0.6975914836 - 0.6984933019 - 0.6993951201 - 0.7002941966 - 0.7011892796 - 0.7020843029 - 0.7029793859 - 0.7038745284 - 0.7047696114 - 0.7056646943 - 0.7065535784 - 0.7074385285 - 0.7083234191 - 0.7092083097 - 0.7100932002 - 0.7109780908 - 0.7118629813 - 0.7127363086 - 0.7136055827 - 0.7144749165 - 0.7153440714 - 0.7162134051 - 0.7170826197 - 0.7179518938 - 0.7188181877 - 0.7196837068 - 0.7205492854 - 0.7214148045 - 0.7222803831 - 0.7231459022 - 0.7240114212 - 0.7248805165 - 0.7257506251 - 0.7266206741 - 0.7274907827 - 0.7283608913 - 0.7292309999 - 0.7301011086 - 0.7309634089 - 0.7318221927 - 0.7326809168 - 0.7335395813 - 0.7343984246 - 0.7352570891 - 0.7361158133 - 0.7369657159 - 0.7378072739 - 0.7386490107 - 0.7394906282 - 0.7403321862 - 0.7411738038 - 0.7420154214 - 0.7428557277 - 0.7436921000 - 0.7445284724 - 0.7453650236 - 0.7462013960 - 0.7470378876 - 0.7478743196 - 0.7487106919 - 0.7495496869 - 0.7503886819 - 0.7512277961 - 0.7520667911 - 0.7529059052 - 0.7537449002 - 0.7545840144 - 0.7554156780 - 0.7562410831 - 0.7570664287 - 0.7578917742 - 0.7587171793 - 0.7595425844 - 0.7603678703 - 0.7611923218 - 0.7619991899 - 0.7628061175 - 0.7636129856 - 0.7644199133 - 0.7652267814 - 0.7660337090 - 0.7668405175 - 0.7676423192 - 0.7684385180 - 0.7692347169 - 0.7700309753 - 0.7708271742 - 0.7716233730 - 0.7724196911 - 0.7732158899 - 0.7740070820 - 0.7747973204 - 0.7755873799 - 0.7763776183 - 0.7771677971 - 0.7779579759 - 0.7787482142 - 0.7795379162 - 0.7803233862 - 0.7811089158 - 0.7818945050 - 0.7826799750 - 0.7834655046 - 0.7842510939 - 0.7850366235 - 0.7858232856 - 0.7866128087 - 0.7874022722 - 0.7881917953 - 0.7889813185 - 0.7897707820 - 0.7905603051 - 0.7913498282 - 0.7921416163 - 0.7929369807 - 0.7937325239 - 0.7945278883 - 0.7953233719 - 0.7961187959 - 0.7969142795 - 0.7977098227 - 0.7985072732 - 0.7993078828 - 0.8001083732 - 0.8009089828 - 0.8017095923 - 0.8025102019 - 0.8033106923 - 0.8041113019 - 0.8049136996 - 0.8057196736 - 0.8065257072 - 0.8073316813 - 0.8081377149 - 0.8089436889 - 0.8097497225 - 0.8105556965 - 0.8113610148 - 0.8121625185 - 0.8129640818 - 0.8137655854 - 0.8145672083 - 0.8153687119 - 0.8161702752 - 0.8169717789 - 0.8177734017 - 0.8185741901 - 0.8193749189 - 0.8201755881 - 0.8209763169 - 0.8217771053 - 0.8225777745 - 0.8233785033 - 0.8241791725 - 0.8249862790 - 0.8257976770 - 0.8266091943 - 0.8274207115 - 0.8282322288 - 0.8290436864 - 0.8298552036 - 0.8306667209 - 0.8314790130 - 0.8322961926 - 0.8331133723 - 0.8339306116 - 0.8347477913 - 0.8355649710 - 0.8363820910 - 0.8371992707 - 0.8380165100 - 0.8388273120 - 0.8396344185 - 0.8404415846 - 0.8412486911 - 0.8420559168 - 0.8428630233 - 0.8436701894 - 0.8444772959 - 0.8452842832 - 0.8460859060 - 0.8468874097 - 0.8476889729 - 0.8484904766 - 0.8492919803 - 0.8500936031 - 0.8508951068 - 0.8516967297 - 0.8524969816 - 0.8532947898 - 0.8540925980 - 0.8548902869 - 0.8556880951 - 0.8564857841 - 0.8572835922 - 0.8580814004 - 0.8588790894 - 0.8596730232 - 0.8604632020 - 0.8612535000 - 0.8620436788 - 0.8628339767 - 0.8636242151 - 0.8644145131 - 0.8652046919 - 0.8659949899 - 0.8667806983 - 0.8675637841 - 0.8683468103 - 0.8691297770 - 0.8699129224 - 0.8706958890 - 0.8714789152 - 0.8722620010 - 0.8730450273 - 0.8738256097 - 0.8746048808 - 0.8753840923 - 0.8761633039 - 0.8769425750 - 0.8777217865 - 0.8785009980 - 0.8792803288 - 0.8800594807 - 0.8808330894 - 0.8816019893 - 0.8823708296 - 0.8831397295 - 0.8839085102 - 0.8846774101 - 0.8854461908 - 0.8862150908 - 0.8869838715 - 0.8877493739 - 0.8885086775 - 0.8892679811 - 0.8900274038 - 0.8907867074 - 0.8915460706 - 0.8923053741 - 0.8930646777 - 0.8938241005 - 0.8945831060 - 0.8953374028 - 0.8960916996 - 0.8968459964 - 0.8976002932 - 0.8983545899 - 0.8991088867 - 0.8998631835 - 0.9006174803 - 0.9013717771 - 0.9021221995 - 0.9028707147 - 0.9036192298 - 0.9043676853 - 0.9051162004 - 0.9058647156 - 0.9066131711 - 0.9073616862 - 0.9081102014 - 0.9088572264 - 0.9095978141 - 0.9103382826 - 0.9110788703 - 0.9118193984 - 0.9125599265 - 0.9133005142 - 0.9140409827 - 0.9147815704 - 0.9155220985 - 0.9162616730 - 0.9170007110 - 0.9177396894 - 0.9184786081 - 0.9192175865 - 0.9199566245 - 0.9206956029 - 0.9214345813 - 0.9221735001 - 0.9229124784 - 0.9236471057 - 0.9243810773 - 0.9251152277 - 0.9258491993 - 0.9265832901 - 0.9273173809 - 0.9280514121 - 0.9287855029 - 0.9295194745 - 0.9302524924 - 0.9309753180 - 0.9316980839 - 0.9324210286 - 0.9331437945 - 0.9338666201 - 0.9345893860 - 0.9353122711 - 0.9360350966 - 0.9367579222 - 0.9374781847 - 0.9381874204 - 0.9388967156 - 0.9396058917 - 0.9403151274 - 0.9410244226 - 0.9417335987 - 0.9424427748 - 0.9431521297 - 0.9438613057 - 0.9445694089 - 0.9452716708 - 0.9459741116 - 0.9466764927 - 0.9473788738 - 0.9480813146 - 0.9487836957 - 0.9494860768 - 0.9501885176 - 0.9508908987 - 0.9515926838 - 0.9522830844 - 0.9529734850 - 0.9536638856 - 0.9543542862 - 0.9550446868 - 0.9557350874 - 0.9564254880 - 0.9571158886 - 0.9578062892 - 0.9584966898 - 0.9591748118 - 0.9598501921 - 0.9605255723 - 0.9612010121 - 0.9618763924 - 0.9625517726 - 0.9632272124 - 0.9639025927 - 0.9645779729 - 0.9652534127 - 0.9659177065 - 0.9665694833 - 0.9672212005 - 0.9678729773 - 0.9685248137 - 0.9691764712 - 0.9698283076 - 0.9704800248 - 0.9711318016 - 0.9717835784 - 0.9724348783 - 0.9730530977 - 0.9736713171 - 0.9742894769 - 0.9749076962 - 0.9755259752 - 0.9761441946 - 0.9767624140 - 0.9773805737 - 0.9779987931 - 0.9786170125 - 0.9792193770 - 0.9798017740 - 0.9803841710 - 0.9809666872 - 0.9815490842 - 0.9821314812 - 0.9827138782 - 0.9832963943 - 0.9838787913 - 0.9844611883 - 0.9850435853 - 0.9855896831 - 0.9861239791 - 0.9866582751 - 0.9871926904 - 0.9877269864 - 0.9882612824 - 0.9887955785 - 0.9893299937 - 0.9898642898 - 0.9903985858 - 0.9909328818 - 0.9913933873 - 0.9918504953 - 0.9923074841 - 0.9927644730 - 0.9932215214 - 0.9936786294 - 0.9941356182 - 0.9945926070 - 0.9950495958 - 0.9955067039 - 0.9959604144 - 0.9963275790 - 0.9966949224 - 0.9970620871 - 0.9974293113 - 0.9977965951 - 0.9981638193 - 0.9985311031 - 0.9988983274 - 0.9992654920 - 0.9996327758 - 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 1 +{ + 0.0000000000 + 0.0007793432 + 0.0016043870 + 0.0025048309 + 0.0034564850 + 0.0044531059 + 0.0054927212 + 0.0065743062 + 0.0076966030 + 0.0088652465 + 0.0100406297 + 0.0112482598 + 0.0124893002 + 0.0137513904 + 0.0150318900 + 0.0163053796 + 0.0175968707 + 0.0188959707 + 0.0202209800 + 0.0215417892 + 0.0228415392 + 0.0241345204 + 0.0254166108 + 0.0267051402 + 0.0279715508 + 0.0292299706 + 0.0305107906 + 0.0318148397 + 0.0331244394 + 0.0344182886 + 0.0356710814 + 0.0369000100 + 0.0381264500 + 0.0393604413 + 0.0405932292 + 0.0418275297 + 0.0430539995 + 0.0442756005 + 0.0454925410 + 0.0467148200 + 0.0479568392 + 0.0492201187 + 0.0505055487 + 0.0518039688 + 0.0530999005 + 0.0543935411 + 0.0556728095 + 0.0569286086 + 0.0581700616 + 0.0593975298 + 0.0606211387 + 0.0618490316 + 0.0630754977 + 0.0642991737 + 0.0655274168 + 0.0667622983 + 0.0679954067 + 0.0692288280 + 0.0704668984 + 0.0717051104 + 0.0729388893 + 0.0741742104 + 0.0754114315 + 0.0766490623 + 0.0779046863 + 0.0791758820 + 0.0804568231 + 0.0817457736 + 0.0830455273 + 0.0843512416 + 0.0856669694 + 0.0869878605 + 0.0883099586 + 0.0896294117 + 0.0909416676 + 0.0922398567 + 0.0935223699 + 0.0948002487 + 0.0960806310 + 0.0973561779 + 0.0986227468 + 0.0998791903 + 0.1011310965 + 0.1023767963 + 0.1036218032 + 0.1048732996 + 0.1061237976 + 0.1073772013 + 0.1086406037 + 0.1099131033 + 0.1111890003 + 0.1124633029 + 0.1137432009 + 0.1150280014 + 0.1163161024 + 0.1176021993 + 0.1188817993 + 0.1201585010 + 0.1214374006 + 0.1227219999 + 0.1240141019 + 0.1253069043 + 0.1266023964 + 0.1279031932 + 0.1292033046 + 0.1305052042 + 0.1318120956 + 0.1331252009 + 0.1344417036 + 0.1357488036 + 0.1370531023 + 0.1383568048 + 0.1396605074 + 0.1409630030 + 0.1422598958 + 0.1435438991 + 0.1448162943 + 0.1460811943 + 0.1473405957 + 0.1486002952 + 0.1498605013 + 0.1511213034 + 0.1523707062 + 0.1536183953 + 0.1548663974 + 0.1561181992 + 0.1573731005 + 0.1586305052 + 0.1598910987 + 0.1611555964 + 0.1624190062 + 0.1636800021 + 0.1649398953 + 0.1662012041 + 0.1674640030 + 0.1687299013 + 0.1699973047 + 0.1712664962 + 0.1725308001 + 0.1737935990 + 0.1750548929 + 0.1763173938 + 0.1775805950 + 0.1788450032 + 0.1801103055 + 0.1813762039 + 0.1826315969 + 0.1838831007 + 0.1851277947 + 0.1863732934 + 0.1876191944 + 0.1888664067 + 0.1901142001 + 0.1913629025 + 0.1926099062 + 0.1938558966 + 0.1950968057 + 0.1963375956 + 0.1975809038 + 0.1988252997 + 0.2000714988 + 0.2013193965 + 0.2025685012 + 0.2038163990 + 0.2050641030 + 0.2063076943 + 0.2075518072 + 0.2087996006 + 0.2100487053 + 0.2113008052 + 0.2125546932 + 0.2138112932 + 0.2150679976 + 0.2163249999 + 0.2175804973 + 0.2188349068 + 0.2200877964 + 0.2213395983 + 0.2225918025 + 0.2238440067 + 0.2250992060 + 0.2263558954 + 0.2276154011 + 0.2288765013 + 0.2301405072 + 0.2314060032 + 0.2326692939 + 0.2339307964 + 0.2351918072 + 0.2364521027 + 0.2377136946 + 0.2389771044 + 0.2402402014 + 0.2415028960 + 0.2427645028 + 0.2440197021 + 0.2452747971 + 0.2465146035 + 0.2477540970 + 0.2489840984 + 0.2502116859 + 0.2514351010 + 0.2526553869 + 0.2538737059 + 0.2550880015 + 0.2563014030 + 0.2575060129 + 0.2587105036 + 0.2599005997 + 0.2610872090 + 0.2622725964 + 0.2634567022 + 0.2646411955 + 0.2658275068 + 0.2670137882 + 0.2682031095 + 0.2693929970 + 0.2705830038 + 0.2717730105 + 0.2729629874 + 0.2741507888 + 0.2753385901 + 0.2765288949 + 0.2777211964 + 0.2789132893 + 0.2801041007 + 0.2812949121 + 0.2824903131 + 0.2836886942 + 0.2848882973 + 0.2861017883 + 0.2873153090 + 0.2885307968 + 0.2897481024 + 0.2909654081 + 0.2921859026 + 0.2934064865 + 0.2946256995 + 0.2958427072 + 0.2970597148 + 0.2982782125 + 0.2994971871 + 0.3007166982 + 0.3019422889 + 0.3031679094 + 0.3043949902 + 0.3056245148 + 0.3068540096 + 0.3080824018 + 0.3093104064 + 0.3105382919 + 0.3117617965 + 0.3129849136 + 0.3142082095 + 0.3154324889 + 0.3166567981 + 0.3178820908 + 0.3191089034 + 0.3203358054 + 0.3215647936 + 0.3227958977 + 0.3240269125 + 0.3252558112 + 0.3264834881 + 0.3277111948 + 0.3289369941 + 0.3301621974 + 0.3313874006 + 0.3326055110 + 0.3338221014 + 0.3350386918 + 0.3362470865 + 0.3374541104 + 0.3386611044 + 0.3398616016 + 0.3410609961 + 0.3422603905 + 0.3434514999 + 0.3446409106 + 0.3458304107 + 0.3470165133 + 0.3482016921 + 0.3493868113 + 0.3505715132 + 0.3517560065 + 0.3529404998 + 0.3541254103 + 0.3553107083 + 0.3564960063 + 0.3576815128 + 0.3588675857 + 0.3600535989 + 0.3612394929 + 0.3624249995 + 0.3636103868 + 0.3647958934 + 0.3659862876 + 0.3671767116 + 0.3683671057 + 0.3695602119 + 0.3707543015 + 0.3719483912 + 0.3731409013 + 0.3743312061 + 0.3755215108 + 0.3767108917 + 0.3778904974 + 0.3790701926 + 0.3802497983 + 0.3814249933 + 0.3825984001 + 0.3837718070 + 0.3849433959 + 0.3861109912 + 0.3872785866 + 0.3884460926 + 0.3896096051 + 0.3907724023 + 0.3919351995 + 0.3930951953 + 0.3942511082 + 0.3954069018 + 0.3965628147 + 0.3977113962 + 0.3988589942 + 0.4000065923 + 0.4011510909 + 0.4022890925 + 0.4034270048 + 0.4045650065 + 0.4056946933 + 0.4068213105 + 0.4079478085 + 0.4090737104 + 0.4101923108 + 0.4113109112 + 0.4124295115 + 0.4135465026 + 0.4146609008 + 0.4157753885 + 0.4168899059 + 0.4180073142 + 0.4191260040 + 0.4202446938 + 0.4213634133 + 0.4224787951 + 0.4235939085 + 0.4247091115 + 0.4258244038 + 0.4269405007 + 0.4280565977 + 0.4291726947 + 0.4302915931 + 0.4314163029 + 0.4325410128 + 0.4336656928 + 0.4347921908 + 0.4359208047 + 0.4370495081 + 0.4381780922 + 0.4393090010 + 0.4404419065 + 0.4415748119 + 0.4427076876 + 0.4438371062 + 0.4449638128 + 0.4460904002 + 0.4472169876 + 0.4483405054 + 0.4494614005 + 0.4505822957 + 0.4517031908 + 0.4528234005 + 0.4539428949 + 0.4550623894 + 0.4561819136 + 0.4572989047 + 0.4584122002 + 0.4595254064 + 0.4606387019 + 0.4617524147 + 0.4628674090 + 0.4639823139 + 0.4650971889 + 0.4662118852 + 0.4673239887 + 0.4684360921 + 0.4695481956 + 0.4706602991 + 0.4717625082 + 0.4728631973 + 0.4739638865 + 0.4750646055 + 0.4761615098 + 0.4772560000 + 0.4783504903 + 0.4794450104 + 0.4805378914 + 0.4816266894 + 0.4827154875 + 0.4838042855 + 0.4848932028 + 0.4859696925 + 0.4870451093 + 0.4881204069 + 0.4891957939 + 0.4902659059 + 0.4913307130 + 0.4923956096 + 0.4934605062 + 0.4945251942 + 0.4955861866 + 0.4966472089 + 0.4977082014 + 0.4987691939 + 0.4998275936 + 0.5008835793 + 0.5019395947 + 0.5029956102 + 0.5040516257 + 0.5051162243 + 0.5061810017 + 0.5072458982 + 0.5083106756 + 0.5093780756 + 0.5104498863 + 0.5115215778 + 0.5125933886 + 0.5136651993 + 0.5147346854 + 0.5158032179 + 0.5168716908 + 0.5179402232 + 0.5190086961 + 0.5200641155 + 0.5211194158 + 0.5221747160 + 0.5232300758 + 0.5242825747 + 0.5253263116 + 0.5263699889 + 0.5274137259 + 0.5284574032 + 0.5294985771 + 0.5305367112 + 0.5315749049 + 0.5326129794 + 0.5336511135 + 0.5346894860 + 0.5357280970 + 0.5367668271 + 0.5378053784 + 0.5388441086 + 0.5398868918 + 0.5409319997 + 0.5419769883 + 0.5430220962 + 0.5440670848 + 0.5451143980 + 0.5461627245 + 0.5472109914 + 0.5482593179 + 0.5493075848 + 0.5503467917 + 0.5513814092 + 0.5524160266 + 0.5534505844 + 0.5544852018 + 0.5555140972 + 0.5565391183 + 0.5575640798 + 0.5585891008 + 0.5596141219 + 0.5606383085 + 0.5616616011 + 0.5626850128 + 0.5637083054 + 0.5647315979 + 0.5657541752 + 0.5667750239 + 0.5677956939 + 0.5688164234 + 0.5698372126 + 0.5708575845 + 0.5718625784 + 0.5728675723 + 0.5738726258 + 0.5748776197 + 0.5758826137 + 0.5768746138 + 0.5778616071 + 0.5788486004 + 0.5798354745 + 0.5808225274 + 0.5818051100 + 0.5827798247 + 0.5837545991 + 0.5847293139 + 0.5857040882 + 0.5866789222 + 0.5876461267 + 0.5886127949 + 0.5895795822 + 0.5905464292 + 0.5915130973 + 0.5924791098 + 0.5934441090 + 0.5944091082 + 0.5953741074 + 0.5963391066 + 0.5973042250 + 0.5982726812 + 0.5992414951 + 0.6002103090 + 0.6011791825 + 0.6021479964 + 0.6031122804 + 0.6040681005 + 0.6050239205 + 0.6059796810 + 0.6069355011 + 0.6078913212 + 0.6088355184 + 0.6097744703 + 0.6107134223 + 0.6116523743 + 0.6125913858 + 0.6135302782 + 0.6144611835 + 0.6153919101 + 0.6163225174 + 0.6172531843 + 0.6181837916 + 0.6191145778 + 0.6200459003 + 0.6209772229 + 0.6219084859 + 0.6228398085 + 0.6237710118 + 0.6247057915 + 0.6256468892 + 0.6265881062 + 0.6275292039 + 0.6284703016 + 0.6294115186 + 0.6303575039 + 0.6313095093 + 0.6322615147 + 0.6332135797 + 0.6341655850 + 0.6351175904 + 0.6360645294 + 0.6370056272 + 0.6379467249 + 0.6388878822 + 0.6398289800 + 0.6407700777 + 0.6417067051 + 0.6426376104 + 0.6435685158 + 0.6444994211 + 0.6454303265 + 0.6463611126 + 0.6472914815 + 0.6482210159 + 0.6491504908 + 0.6500799060 + 0.6510093808 + 0.6519389153 + 0.6528680921 + 0.6537961960 + 0.6547242999 + 0.6556524038 + 0.6565805078 + 0.6575086713 + 0.6584367752 + 0.6593579054 + 0.6602786779 + 0.6611993909 + 0.6621202230 + 0.6630408764 + 0.6639617085 + 0.6648803949 + 0.6657980084 + 0.6667156219 + 0.6676332951 + 0.6685509086 + 0.6694685221 + 0.6703848243 + 0.6712977886 + 0.6722108126 + 0.6731237769 + 0.6740368009 + 0.6749498248 + 0.6758627892 + 0.6767727733 + 0.6776821017 + 0.6785914898 + 0.6795008779 + 0.6804103255 + 0.6813195944 + 0.6822264791 + 0.6831287146 + 0.6840307713 + 0.6849328876 + 0.6858351231 + 0.6867371798 + 0.6876392961 + 0.6885449290 + 0.6894515753 + 0.6903582811 + 0.6912649274 + 0.6921715736 + 0.6930782795 + 0.6939843893 + 0.6948862076 + 0.6957879066 + 0.6966897249 + 0.6975914836 + 0.6984933019 + 0.6993951201 + 0.7002941966 + 0.7011892796 + 0.7020843029 + 0.7029793859 + 0.7038745284 + 0.7047696114 + 0.7056646943 + 0.7065535784 + 0.7074385285 + 0.7083234191 + 0.7092083097 + 0.7100932002 + 0.7109780908 + 0.7118629813 + 0.7127363086 + 0.7136055827 + 0.7144749165 + 0.7153440714 + 0.7162134051 + 0.7170826197 + 0.7179518938 + 0.7188181877 + 0.7196837068 + 0.7205492854 + 0.7214148045 + 0.7222803831 + 0.7231459022 + 0.7240114212 + 0.7248805165 + 0.7257506251 + 0.7266206741 + 0.7274907827 + 0.7283608913 + 0.7292309999 + 0.7301011086 + 0.7309634089 + 0.7318221927 + 0.7326809168 + 0.7335395813 + 0.7343984246 + 0.7352570891 + 0.7361158133 + 0.7369657159 + 0.7378072739 + 0.7386490107 + 0.7394906282 + 0.7403321862 + 0.7411738038 + 0.7420154214 + 0.7428557277 + 0.7436921000 + 0.7445284724 + 0.7453650236 + 0.7462013960 + 0.7470378876 + 0.7478743196 + 0.7487106919 + 0.7495496869 + 0.7503886819 + 0.7512277961 + 0.7520667911 + 0.7529059052 + 0.7537449002 + 0.7545840144 + 0.7554156780 + 0.7562410831 + 0.7570664287 + 0.7578917742 + 0.7587171793 + 0.7595425844 + 0.7603678703 + 0.7611923218 + 0.7619991899 + 0.7628061175 + 0.7636129856 + 0.7644199133 + 0.7652267814 + 0.7660337090 + 0.7668405175 + 0.7676423192 + 0.7684385180 + 0.7692347169 + 0.7700309753 + 0.7708271742 + 0.7716233730 + 0.7724196911 + 0.7732158899 + 0.7740070820 + 0.7747973204 + 0.7755873799 + 0.7763776183 + 0.7771677971 + 0.7779579759 + 0.7787482142 + 0.7795379162 + 0.7803233862 + 0.7811089158 + 0.7818945050 + 0.7826799750 + 0.7834655046 + 0.7842510939 + 0.7850366235 + 0.7858232856 + 0.7866128087 + 0.7874022722 + 0.7881917953 + 0.7889813185 + 0.7897707820 + 0.7905603051 + 0.7913498282 + 0.7921416163 + 0.7929369807 + 0.7937325239 + 0.7945278883 + 0.7953233719 + 0.7961187959 + 0.7969142795 + 0.7977098227 + 0.7985072732 + 0.7993078828 + 0.8001083732 + 0.8009089828 + 0.8017095923 + 0.8025102019 + 0.8033106923 + 0.8041113019 + 0.8049136996 + 0.8057196736 + 0.8065257072 + 0.8073316813 + 0.8081377149 + 0.8089436889 + 0.8097497225 + 0.8105556965 + 0.8113610148 + 0.8121625185 + 0.8129640818 + 0.8137655854 + 0.8145672083 + 0.8153687119 + 0.8161702752 + 0.8169717789 + 0.8177734017 + 0.8185741901 + 0.8193749189 + 0.8201755881 + 0.8209763169 + 0.8217771053 + 0.8225777745 + 0.8233785033 + 0.8241791725 + 0.8249862790 + 0.8257976770 + 0.8266091943 + 0.8274207115 + 0.8282322288 + 0.8290436864 + 0.8298552036 + 0.8306667209 + 0.8314790130 + 0.8322961926 + 0.8331133723 + 0.8339306116 + 0.8347477913 + 0.8355649710 + 0.8363820910 + 0.8371992707 + 0.8380165100 + 0.8388273120 + 0.8396344185 + 0.8404415846 + 0.8412486911 + 0.8420559168 + 0.8428630233 + 0.8436701894 + 0.8444772959 + 0.8452842832 + 0.8460859060 + 0.8468874097 + 0.8476889729 + 0.8484904766 + 0.8492919803 + 0.8500936031 + 0.8508951068 + 0.8516967297 + 0.8524969816 + 0.8532947898 + 0.8540925980 + 0.8548902869 + 0.8556880951 + 0.8564857841 + 0.8572835922 + 0.8580814004 + 0.8588790894 + 0.8596730232 + 0.8604632020 + 0.8612535000 + 0.8620436788 + 0.8628339767 + 0.8636242151 + 0.8644145131 + 0.8652046919 + 0.8659949899 + 0.8667806983 + 0.8675637841 + 0.8683468103 + 0.8691297770 + 0.8699129224 + 0.8706958890 + 0.8714789152 + 0.8722620010 + 0.8730450273 + 0.8738256097 + 0.8746048808 + 0.8753840923 + 0.8761633039 + 0.8769425750 + 0.8777217865 + 0.8785009980 + 0.8792803288 + 0.8800594807 + 0.8808330894 + 0.8816019893 + 0.8823708296 + 0.8831397295 + 0.8839085102 + 0.8846774101 + 0.8854461908 + 0.8862150908 + 0.8869838715 + 0.8877493739 + 0.8885086775 + 0.8892679811 + 0.8900274038 + 0.8907867074 + 0.8915460706 + 0.8923053741 + 0.8930646777 + 0.8938241005 + 0.8945831060 + 0.8953374028 + 0.8960916996 + 0.8968459964 + 0.8976002932 + 0.8983545899 + 0.8991088867 + 0.8998631835 + 0.9006174803 + 0.9013717771 + 0.9021221995 + 0.9028707147 + 0.9036192298 + 0.9043676853 + 0.9051162004 + 0.9058647156 + 0.9066131711 + 0.9073616862 + 0.9081102014 + 0.9088572264 + 0.9095978141 + 0.9103382826 + 0.9110788703 + 0.9118193984 + 0.9125599265 + 0.9133005142 + 0.9140409827 + 0.9147815704 + 0.9155220985 + 0.9162616730 + 0.9170007110 + 0.9177396894 + 0.9184786081 + 0.9192175865 + 0.9199566245 + 0.9206956029 + 0.9214345813 + 0.9221735001 + 0.9229124784 + 0.9236471057 + 0.9243810773 + 0.9251152277 + 0.9258491993 + 0.9265832901 + 0.9273173809 + 0.9280514121 + 0.9287855029 + 0.9295194745 + 0.9302524924 + 0.9309753180 + 0.9316980839 + 0.9324210286 + 0.9331437945 + 0.9338666201 + 0.9345893860 + 0.9353122711 + 0.9360350966 + 0.9367579222 + 0.9374781847 + 0.9381874204 + 0.9388967156 + 0.9396058917 + 0.9403151274 + 0.9410244226 + 0.9417335987 + 0.9424427748 + 0.9431521297 + 0.9438613057 + 0.9445694089 + 0.9452716708 + 0.9459741116 + 0.9466764927 + 0.9473788738 + 0.9480813146 + 0.9487836957 + 0.9494860768 + 0.9501885176 + 0.9508908987 + 0.9515926838 + 0.9522830844 + 0.9529734850 + 0.9536638856 + 0.9543542862 + 0.9550446868 + 0.9557350874 + 0.9564254880 + 0.9571158886 + 0.9578062892 + 0.9584966898 + 0.9591748118 + 0.9598501921 + 0.9605255723 + 0.9612010121 + 0.9618763924 + 0.9625517726 + 0.9632272124 + 0.9639025927 + 0.9645779729 + 0.9652534127 + 0.9659177065 + 0.9665694833 + 0.9672212005 + 0.9678729773 + 0.9685248137 + 0.9691764712 + 0.9698283076 + 0.9704800248 + 0.9711318016 + 0.9717835784 + 0.9724348783 + 0.9730530977 + 0.9736713171 + 0.9742894769 + 0.9749076962 + 0.9755259752 + 0.9761441946 + 0.9767624140 + 0.9773805737 + 0.9779987931 + 0.9786170125 + 0.9792193770 + 0.9798017740 + 0.9803841710 + 0.9809666872 + 0.9815490842 + 0.9821314812 + 0.9827138782 + 0.9832963943 + 0.9838787913 + 0.9844611883 + 0.9850435853 + 0.9855896831 + 0.9861239791 + 0.9866582751 + 0.9871926904 + 0.9877269864 + 0.9882612824 + 0.9887955785 + 0.9893299937 + 0.9898642898 + 0.9903985858 + 0.9909328818 + 0.9913933873 + 0.9918504953 + 0.9923074841 + 0.9927644730 + 0.9932215214 + 0.9936786294 + 0.9941356182 + 0.9945926070 + 0.9950495958 + 0.9955067039 + 0.9959604144 + 0.9963275790 + 0.9966949224 + 0.9970620871 + 0.9974293113 + 0.9977965951 + 0.9981638193 + 0.9985311031 + 0.9988983274 + 0.9992654920 + 0.9996327758 + 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/eastman_double_x_neg_4min.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/eastman_double_x_neg_4min.spi1d index 4835f842..8a43a49f 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/eastman_double_x_neg_4min.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/eastman_double_x_neg_4min.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 1 -{ - 0.0000000000 - 0.0023687680 - 0.0054706829 - 0.0086651789 - 0.0120176496 - 0.0158339497 - 0.0195765607 - 0.0236159302 - 0.0276366994 - 0.0318052210 - 0.0358720496 - 0.0399059989 - 0.0439607613 - 0.0478296690 - 0.0517273210 - 0.0555948503 - 0.0593124293 - 0.0629053935 - 0.0664692596 - 0.0699850023 - 0.0734276623 - 0.0768507421 - 0.0801614299 - 0.0833590478 - 0.0865080431 - 0.0895957723 - 0.0926453620 - 0.0956536904 - 0.0985890403 - 0.1014889032 - 0.1043139026 - 0.1071280017 - 0.1098439023 - 0.1126044020 - 0.1154047996 - 0.1181370020 - 0.1207929030 - 0.1234567985 - 0.1260991991 - 0.1286775023 - 0.1312310994 - 0.1338002980 - 0.1363786012 - 0.1389155984 - 0.1414254010 - 0.1439868957 - 0.1465484947 - 0.1490968019 - 0.1516236961 - 0.1540950984 - 0.1565376967 - 0.1589566022 - 0.1613540947 - 0.1637308002 - 0.1660850048 - 0.1684197038 - 0.1707345992 - 0.1730296016 - 0.1753066033 - 0.1775436997 - 0.1797461957 - 0.1819386929 - 0.1841226071 - 0.1862974018 - 0.1884626001 - 0.1906190962 - 0.1927487999 - 0.1948511004 - 0.1969520003 - 0.1990513057 - 0.2011491954 - 0.2032300979 - 0.2052827030 - 0.2073220015 - 0.2093351036 - 0.2113271952 - 0.2133180052 - 0.2153075039 - 0.2172959000 - 0.2192790955 - 0.2212336957 - 0.2231718004 - 0.2251033932 - 0.2270285040 - 0.2289548069 - 0.2309035063 - 0.2328732014 - 0.2348369956 - 0.2367957979 - 0.2387501001 - 0.2406919003 - 0.2426078022 - 0.2445061952 - 0.2464056015 - 0.2483084053 - 0.2502099872 - 0.2521064878 - 0.2539938092 - 0.2558684051 - 0.2577266097 - 0.2595650852 - 0.2613835931 - 0.2631973922 - 0.2650325000 - 0.2668823004 - 0.2687312961 - 0.2705838084 - 0.2724444866 - 0.2743074894 - 0.2761450112 - 0.2779580951 - 0.2797524035 - 0.2815274894 - 0.2832885981 - 0.2850483060 - 0.2868123949 - 0.2885822058 - 0.2903468013 - 0.2920975983 - 0.2938277125 - 0.2955364883 - 0.2972253859 - 0.2988938093 - 0.3005304933 - 0.3021554053 - 0.3037792146 - 0.3054072857 - 0.3070620894 - 0.3087309003 - 0.3104070127 - 0.3120703995 - 0.3137196898 - 0.3153668940 - 0.3170216084 - 0.3186618090 - 0.3202942014 - 0.3219175935 - 0.3235363066 - 0.3251608014 - 0.3267996013 - 0.3284519017 - 0.3301031888 - 0.3317410052 - 0.3333635926 - 0.3349764943 - 0.3365670145 - 0.3381485939 - 0.3397214115 - 0.3412905037 - 0.3428558111 - 0.3444117010 - 0.3459688127 - 0.3475267887 - 0.3490850031 - 0.3506405950 - 0.3521885872 - 0.3537277877 - 0.3552600145 - 0.3567726910 - 0.3582783937 - 0.3597534001 - 0.3612213135 - 0.3626750112 - 0.3641248941 - 0.3655717969 - 0.3670117855 - 0.3684495986 - 0.3698936105 - 0.3713377118 - 0.3727797866 - 0.3742178977 - 0.3756431937 - 0.3770616055 - 0.3784677088 - 0.3798623085 - 0.3812437057 - 0.3826156855 - 0.3839798868 - 0.3853352964 - 0.3866848946 - 0.3880395889 - 0.3893971145 - 0.3907648027 - 0.3921375871 - 0.3935118020 - 0.3948867023 - 0.3962557912 - 0.3976216912 - 0.3989877999 - 0.4003542066 - 0.4017100036 - 0.4030559957 - 0.4043887854 - 0.4057038128 - 0.4070141017 - 0.4083139896 - 0.4096128941 - 0.4109064043 - 0.4121997952 - 0.4134868085 - 0.4147739112 - 0.4160555899 - 0.4173362851 - 0.4186184108 - 0.4199014008 - 0.4211877882 - 0.4224792123 - 0.4237694144 - 0.4250515997 - 0.4263338149 - 0.4276081026 - 0.4288811088 - 0.4301379919 - 0.4313819110 - 0.4326207042 - 0.4338417947 - 0.4350628853 - 0.4362733960 - 0.4374822080 - 0.4386931062 - 0.4399060011 - 0.4411188960 - 0.4423305988 - 0.4435423911 - 0.4447481036 - 0.4459505081 - 0.4471527040 - 0.4483538866 - 0.4495550990 - 0.4507592022 - 0.4519644976 - 0.4531674981 - 0.4543609023 - 0.4555541873 - 0.4567396045 - 0.4579207897 - 0.4591009021 - 0.4602701068 - 0.4614393115 - 0.4626025856 - 0.4637596905 - 0.4649167955 - 0.4660637975 - 0.4672091901 - 0.4683555067 - 0.4695048034 - 0.4706541896 - 0.4718030095 - 0.4729512036 - 0.4740993977 - 0.4752402902 - 0.4763796926 - 0.4775182903 - 0.4786491096 - 0.4797798991 - 0.4809078872 - 0.4820297956 - 0.4831517935 - 0.4842692018 - 0.4853824079 - 0.4864956141 - 0.4876025915 - 0.4887068868 - 0.4898110926 - 0.4909138083 - 0.4920162857 - 0.4931187034 - 0.4942246974 - 0.4953309894 - 0.4964371026 - 0.4975334108 - 0.4986295998 - 0.4997260869 - 0.5008261800 - 0.5019261837 - 0.5030261278 - 0.5041244030 - 0.5052226782 - 0.5063204765 - 0.5074101090 - 0.5084996819 - 0.5095893145 - 0.5106704831 - 0.5117517710 - 0.5128329992 - 0.5139064789 - 0.5149794221 - 0.5160521865 - 0.5171185732 - 0.5181834102 - 0.5192481875 - 0.5203077197 - 0.5213642716 - 0.5224208832 - 0.5234770179 - 0.5245326161 - 0.5255880952 - 0.5266445875 - 0.5277037024 - 0.5287628174 - 0.5298218727 - 0.5308706760 - 0.5319193006 - 0.5329679847 - 0.5340090990 - 0.5350469947 - 0.5360848904 - 0.5371162891 - 0.5381368995 - 0.5391575098 - 0.5401778221 - 0.5411760211 - 0.5421741009 - 0.5431722999 - 0.5441654921 - 0.5451557040 - 0.5461459160 - 0.5471357107 - 0.5481235981 - 0.5491116047 - 0.5500994921 - 0.5510740876 - 0.5520431995 - 0.5530123115 - 0.5539786220 - 0.5549340248 - 0.5558893085 - 0.5568445921 - 0.5578002930 - 0.5587561727 - 0.5597121119 - 0.5606679916 - 0.5616241097 - 0.5625802875 - 0.5635365248 - 0.5644925237 - 0.5654482245 - 0.5664039850 - 0.5673596859 - 0.5683150887 - 0.5692703724 - 0.5702255964 - 0.5711804032 - 0.5721278787 - 0.5730754733 - 0.5740230083 - 0.5749645829 - 0.5758937001 - 0.5768228769 - 0.5777519941 - 0.5786722898 - 0.5795848966 - 0.5804976225 - 0.5814102888 - 0.5823227167 - 0.5832350850 - 0.5841473937 - 0.5850597024 - 0.5859693885 - 0.5868784785 - 0.5877876878 - 0.5886967778 - 0.5896003246 - 0.5905032754 - 0.5914062858 - 0.5923094153 - 0.5932080746 - 0.5941067934 - 0.5950053930 - 0.5959041119 - 0.5967944860 - 0.5976848006 - 0.5985751748 - 0.5994654894 - 0.6003587246 - 0.6012520790 - 0.6021453738 - 0.6030387282 - 0.6039313078 - 0.6048238277 - 0.6057162285 - 0.6066086888 - 0.6074911952 - 0.6083701849 - 0.6092491746 - 0.6101281047 - 0.6109964848 - 0.6118568778 - 0.6127172112 - 0.6135776043 - 0.6144360900 - 0.6152914166 - 0.6161466241 - 0.6170017719 - 0.6178573966 - 0.6187161803 - 0.6195750237 - 0.6204338074 - 0.6212925911 - 0.6221556067 - 0.6230198741 - 0.6238840818 - 0.6247482896 - 0.6256136894 - 0.6264808178 - 0.6273478866 - 0.6282148957 - 0.6290820241 - 0.6299490929 - 0.6308162212 - 0.6316832900 - 0.6325504780 - 0.6334146857 - 0.6342768073 - 0.6351389885 - 0.6360011101 - 0.6368619800 - 0.6377084851 - 0.6385549903 - 0.6394014955 - 0.6402480006 - 0.6410933137 - 0.6419374943 - 0.6427816749 - 0.6436259747 - 0.6444702148 - 0.6453065276 - 0.6461421847 - 0.6469779015 - 0.6478136182 - 0.6486436725 - 0.6494596004 - 0.6502755880 - 0.6510915160 - 0.6519075036 - 0.6527132988 - 0.6535121202 - 0.6543108821 - 0.6551098228 - 0.6559085846 - 0.6566948891 - 0.6574789286 - 0.6582627892 - 0.6590468287 - 0.6598308086 - 0.6606147885 - 0.6613988280 - 0.6621828079 - 0.6629667878 - 0.6637505293 - 0.6645327210 - 0.6653149128 - 0.6660971045 - 0.6668794155 - 0.6676582098 - 0.6684288979 - 0.6691997051 - 0.6699703932 - 0.6707410812 - 0.6715074182 - 0.6722648144 - 0.6730222106 - 0.6737796068 - 0.6745370030 - 0.6752945185 - 0.6760522127 - 0.6768100262 - 0.6775677204 - 0.6783254743 - 0.6790840030 - 0.6798446774 - 0.6806052923 - 0.6813660264 - 0.6821267009 - 0.6828873158 - 0.6836469769 - 0.6844068170 - 0.6851665974 - 0.6859263182 - 0.6866860986 - 0.6874372959 - 0.6881881952 - 0.6889392138 - 0.6896901131 - 0.6904410124 - 0.6911798120 - 0.6919142008 - 0.6926484704 - 0.6933829188 - 0.6941173077 - 0.6948506236 - 0.6955826283 - 0.6963145733 - 0.6970465183 - 0.6977785230 - 0.6985105276 - 0.6992424726 - 0.6999745965 - 0.7007066011 - 0.7014386058 - 0.7021707296 - 0.7028998137 - 0.7036275864 - 0.7043554187 - 0.7050831914 - 0.7058110237 - 0.7065402269 - 0.7072740197 - 0.7080078125 - 0.7087416053 - 0.7094753981 - 0.7102091908 - 0.7109380960 - 0.7116650939 - 0.7123920918 - 0.7131190896 - 0.7138460875 - 0.7145711780 - 0.7152836919 - 0.7159962058 - 0.7167087197 - 0.7174212933 - 0.7181338072 - 0.7188457847 - 0.7195572257 - 0.7202687263 - 0.7209801078 - 0.7216914892 - 0.7224029899 - 0.7231106758 - 0.7238175273 - 0.7245242000 - 0.7252308726 - 0.7259377241 - 0.7266442180 - 0.7273383141 - 0.7280324101 - 0.7287266254 - 0.7294207215 - 0.7301148772 - 0.7308074832 - 0.7314937115 - 0.7321799994 - 0.7328662276 - 0.7335523963 - 0.7342386246 - 0.7349203229 - 0.7355914712 - 0.7362626791 - 0.7369338870 - 0.7376050949 - 0.7382763028 - 0.7389466763 - 0.7396153808 - 0.7402840853 - 0.7409527898 - 0.7416214943 - 0.7422903180 - 0.7429624796 - 0.7436416745 - 0.7443209887 - 0.7450001836 - 0.7456793785 - 0.7463586926 - 0.7470390201 - 0.7477223873 - 0.7484058738 - 0.7490894198 - 0.7497729063 - 0.7504563928 - 0.7511395812 - 0.7518200874 - 0.7525004745 - 0.7531809807 - 0.7538614273 - 0.7545418739 - 0.7552223802 - 0.7558920979 - 0.7565605044 - 0.7572287917 - 0.7578970790 - 0.7585654855 - 0.7592337728 - 0.7598943114 - 0.7605494261 - 0.7612044811 - 0.7618595958 - 0.7625148296 - 0.7631698847 - 0.7638273835 - 0.7644920945 - 0.7651568055 - 0.7658215165 - 0.7664862275 - 0.7671508193 - 0.7678154707 - 0.7684820890 - 0.7691490054 - 0.7698159814 - 0.7704828978 - 0.7711498737 - 0.7718167901 - 0.7724804878 - 0.7731373906 - 0.7737944126 - 0.7744513750 - 0.7751083970 - 0.7757654190 - 0.7764223814 - 0.7770789862 - 0.7777354121 - 0.7783918977 - 0.7790483832 - 0.7797049284 - 0.7803612947 - 0.7810173035 - 0.7816702127 - 0.7823230028 - 0.7829759121 - 0.7836287022 - 0.7842816114 - 0.7849345207 - 0.7855864167 - 0.7862372994 - 0.7868881822 - 0.7875390053 - 0.7881898880 - 0.7888407707 - 0.7894915938 - 0.7901493907 - 0.7908108234 - 0.7914720774 - 0.7921335101 - 0.7927948833 - 0.7934563160 - 0.7941176295 - 0.7947735190 - 0.7954280972 - 0.7960826755 - 0.7967371941 - 0.7973917723 - 0.7980462909 - 0.7987009287 - 0.7993466258 - 0.7999910712 - 0.8006355762 - 0.8012802005 - 0.8019247055 - 0.8025692105 - 0.8032137156 - 0.8038467169 - 0.8044781089 - 0.8051096201 - 0.8057410121 - 0.8063725233 - 0.8070039153 - 0.8076354265 - 0.8082551956 - 0.8088722229 - 0.8094893098 - 0.8101062775 - 0.8107234240 - 0.8113403916 - 0.8119574785 - 0.8125758171 - 0.8131948113 - 0.8138139248 - 0.8144329190 - 0.8150519133 - 0.8156709075 - 0.8162900209 - 0.8169054985 - 0.8175162077 - 0.8181269169 - 0.8187375069 - 0.8193482161 - 0.8199589252 - 0.8205695748 - 0.8211792707 - 0.8217806220 - 0.8223819733 - 0.8229833841 - 0.8235847950 - 0.8241862059 - 0.8247876167 - 0.8253890276 - 0.8259866834 - 0.8265830278 - 0.8271793127 - 0.8277755976 - 0.8283720016 - 0.8289682865 - 0.8295645714 - 0.8301581144 - 0.8307436705 - 0.8313292861 - 0.8319149017 - 0.8325005174 - 0.8330860734 - 0.8336716890 - 0.8342573047 - 0.8348429799 - 0.8354287148 - 0.8360143900 - 0.8366001844 - 0.8371859193 - 0.8377715945 - 0.8383573294 - 0.8389438987 - 0.8395407200 - 0.8401374817 - 0.8407343030 - 0.8413311243 - 0.8419278860 - 0.8425247073 - 0.8431215286 - 0.8437163830 - 0.8443077803 - 0.8448992968 - 0.8454906940 - 0.8460822105 - 0.8466736078 - 0.8472651243 - 0.8478565216 - 0.8484436274 - 0.8490272760 - 0.8496111035 - 0.8501948714 - 0.8507785797 - 0.8513624072 - 0.8519461155 - 0.8525298834 - 0.8531131744 - 0.8536962271 - 0.8542792201 - 0.8548622131 - 0.8554453254 - 0.8560283184 - 0.8566113114 - 0.8571943045 - 0.8577736020 - 0.8583512902 - 0.8589290977 - 0.8595069051 - 0.8600847125 - 0.8606625199 - 0.8612403274 - 0.8618180752 - 0.8624026775 - 0.8629906178 - 0.8635786176 - 0.8641664982 - 0.8647544980 - 0.8653423786 - 0.8659303784 - 0.8665183187 - 0.8671081066 - 0.8676996231 - 0.8682910800 - 0.8688824773 - 0.8694739938 - 0.8700655103 - 0.8706569076 - 0.8712484241 - 0.8718379140 - 0.8724226952 - 0.8730074763 - 0.8735923171 - 0.8741770983 - 0.8747618794 - 0.8753467202 - 0.8759315014 - 0.8765162826 - 0.8770982027 - 0.8776800036 - 0.8782619238 - 0.8788437843 - 0.8794255853 - 0.8800075054 - 0.8805894256 - 0.8811712265 - 0.8817477226 - 0.8823202848 - 0.8828929067 - 0.8834654093 - 0.8840379715 - 0.8846104741 - 0.8851830959 - 0.8857555985 - 0.8863282800 - 0.8869022727 - 0.8874762058 - 0.8880501986 - 0.8886241913 - 0.8891981244 - 0.8897721171 - 0.8903459907 - 0.8909199834 - 0.8914952874 - 0.8920720220 - 0.8926486969 - 0.8932253718 - 0.8938022256 - 0.8943789005 - 0.8949555755 - 0.8955323100 - 0.8961089849 - 0.8966785073 - 0.8972461224 - 0.8978137970 - 0.8983814716 - 0.8989492059 - 0.8995168209 - 0.9000844955 - 0.9006522298 - 0.9012197256 - 0.9017801881 - 0.9023407102 - 0.9029011726 - 0.9034616947 - 0.9040222168 - 0.9045826793 - 0.9051432014 - 0.9057037234 - 0.9062626958 - 0.9068132043 - 0.9073637128 - 0.9079142213 - 0.9084647894 - 0.9090152979 - 0.9095658064 - 0.9101163149 - 0.9106668234 - 0.9112154245 - 0.9117550254 - 0.9122946858 - 0.9128342867 - 0.9133738875 - 0.9139136076 - 0.9144532084 - 0.9149928093 - 0.9155325294 - 0.9160724878 - 0.9166154861 - 0.9171584845 - 0.9177014828 - 0.9182446003 - 0.9187875986 - 0.9193305969 - 0.9198735952 - 0.9204165936 - 0.9209595919 - 0.9214972854 - 0.9220345020 - 0.9225717783 - 0.9231091142 - 0.9236463904 - 0.9241837263 - 0.9247208834 - 0.9252582192 - 0.9257954955 - 0.9263281822 - 0.9268587828 - 0.9273893833 - 0.9279199839 - 0.9284505248 - 0.9289811254 - 0.9295117259 - 0.9300423265 - 0.9305728078 - 0.9311010838 - 0.9316242933 - 0.9321475029 - 0.9326707125 - 0.9331939220 - 0.9337170720 - 0.9342402816 - 0.9347636104 - 0.9352868199 - 0.9358100295 - 0.9363203049 - 0.9368283749 - 0.9373363853 - 0.9378443956 - 0.9383524060 - 0.9388604164 - 0.9393684268 - 0.9398764968 - 0.9403845072 - 0.9408923984 - 0.9413998723 - 0.9419075251 - 0.9424151182 - 0.9429225922 - 0.9434301853 - 0.9439377785 - 0.9444453120 - 0.9449529052 - 0.9454603791 - 0.9459722042 - 0.9464867711 - 0.9470013976 - 0.9475159049 - 0.9480304718 - 0.9485449791 - 0.9490596056 - 0.9495741725 - 0.9500886798 - 0.9506033063 - 0.9511142969 - 0.9516245127 - 0.9521347880 - 0.9526450038 - 0.9531552196 - 0.9536654949 - 0.9541757107 - 0.9546859264 - 0.9551960826 - 0.9557064176 - 0.9562019110 - 0.9566963911 - 0.9571908712 - 0.9576854110 - 0.9581798911 - 0.9586743712 - 0.9591689110 - 0.9596635103 - 0.9601579905 - 0.9606524706 - 0.9611294270 - 0.9616053104 - 0.9620811939 - 0.9625570774 - 0.9630330205 - 0.9635089040 - 0.9639847875 - 0.9644606709 - 0.9649366140 - 0.9654123783 - 0.9658750892 - 0.9663354158 - 0.9667956829 - 0.9672560096 - 0.9677162170 - 0.9681764841 - 0.9686368108 - 0.9690970778 - 0.9695574045 - 0.9700176120 - 0.9704771042 - 0.9709361196 - 0.9713950753 - 0.9718540907 - 0.9723131061 - 0.9727721214 - 0.9732310772 - 0.9736900926 - 0.9741491079 - 0.9746081829 - 0.9750655293 - 0.9755191803 - 0.9759730101 - 0.9764267206 - 0.9768803716 - 0.9773342013 - 0.9777879119 - 0.9782416821 - 0.9786953926 - 0.9791491032 - 0.9796028733 - 0.9800477028 - 0.9804913998 - 0.9809350967 - 0.9813787937 - 0.9818224907 - 0.9822661877 - 0.9827098846 - 0.9831535816 - 0.9835972786 - 0.9840409756 - 0.9844785929 - 0.9849042296 - 0.9853296876 - 0.9857553244 - 0.9861809015 - 0.9866064787 - 0.9870321155 - 0.9874576926 - 0.9878833294 - 0.9883089066 - 0.9887344837 - 0.9891439080 - 0.9895455837 - 0.9899473190 - 0.9903489947 - 0.9907506704 - 0.9911524057 - 0.9915542006 - 0.9919558764 - 0.9923576117 - 0.9927592874 - 0.9931610227 - 0.9935230017 - 0.9938808084 - 0.9942384958 - 0.9945963025 - 0.9949539900 - 0.9953116775 - 0.9956694841 - 0.9960271716 - 0.9963849783 - 0.9967427254 - 0.9970995188 - 0.9973632097 - 0.9976267815 - 0.9978904724 - 0.9981542230 - 0.9984179139 - 0.9986816049 - 0.9989452958 - 0.9992089272 - 0.9994726181 - 0.9997363091 - 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 1 +{ + 0.0000000000 + 0.0023687680 + 0.0054706829 + 0.0086651789 + 0.0120176496 + 0.0158339497 + 0.0195765607 + 0.0236159302 + 0.0276366994 + 0.0318052210 + 0.0358720496 + 0.0399059989 + 0.0439607613 + 0.0478296690 + 0.0517273210 + 0.0555948503 + 0.0593124293 + 0.0629053935 + 0.0664692596 + 0.0699850023 + 0.0734276623 + 0.0768507421 + 0.0801614299 + 0.0833590478 + 0.0865080431 + 0.0895957723 + 0.0926453620 + 0.0956536904 + 0.0985890403 + 0.1014889032 + 0.1043139026 + 0.1071280017 + 0.1098439023 + 0.1126044020 + 0.1154047996 + 0.1181370020 + 0.1207929030 + 0.1234567985 + 0.1260991991 + 0.1286775023 + 0.1312310994 + 0.1338002980 + 0.1363786012 + 0.1389155984 + 0.1414254010 + 0.1439868957 + 0.1465484947 + 0.1490968019 + 0.1516236961 + 0.1540950984 + 0.1565376967 + 0.1589566022 + 0.1613540947 + 0.1637308002 + 0.1660850048 + 0.1684197038 + 0.1707345992 + 0.1730296016 + 0.1753066033 + 0.1775436997 + 0.1797461957 + 0.1819386929 + 0.1841226071 + 0.1862974018 + 0.1884626001 + 0.1906190962 + 0.1927487999 + 0.1948511004 + 0.1969520003 + 0.1990513057 + 0.2011491954 + 0.2032300979 + 0.2052827030 + 0.2073220015 + 0.2093351036 + 0.2113271952 + 0.2133180052 + 0.2153075039 + 0.2172959000 + 0.2192790955 + 0.2212336957 + 0.2231718004 + 0.2251033932 + 0.2270285040 + 0.2289548069 + 0.2309035063 + 0.2328732014 + 0.2348369956 + 0.2367957979 + 0.2387501001 + 0.2406919003 + 0.2426078022 + 0.2445061952 + 0.2464056015 + 0.2483084053 + 0.2502099872 + 0.2521064878 + 0.2539938092 + 0.2558684051 + 0.2577266097 + 0.2595650852 + 0.2613835931 + 0.2631973922 + 0.2650325000 + 0.2668823004 + 0.2687312961 + 0.2705838084 + 0.2724444866 + 0.2743074894 + 0.2761450112 + 0.2779580951 + 0.2797524035 + 0.2815274894 + 0.2832885981 + 0.2850483060 + 0.2868123949 + 0.2885822058 + 0.2903468013 + 0.2920975983 + 0.2938277125 + 0.2955364883 + 0.2972253859 + 0.2988938093 + 0.3005304933 + 0.3021554053 + 0.3037792146 + 0.3054072857 + 0.3070620894 + 0.3087309003 + 0.3104070127 + 0.3120703995 + 0.3137196898 + 0.3153668940 + 0.3170216084 + 0.3186618090 + 0.3202942014 + 0.3219175935 + 0.3235363066 + 0.3251608014 + 0.3267996013 + 0.3284519017 + 0.3301031888 + 0.3317410052 + 0.3333635926 + 0.3349764943 + 0.3365670145 + 0.3381485939 + 0.3397214115 + 0.3412905037 + 0.3428558111 + 0.3444117010 + 0.3459688127 + 0.3475267887 + 0.3490850031 + 0.3506405950 + 0.3521885872 + 0.3537277877 + 0.3552600145 + 0.3567726910 + 0.3582783937 + 0.3597534001 + 0.3612213135 + 0.3626750112 + 0.3641248941 + 0.3655717969 + 0.3670117855 + 0.3684495986 + 0.3698936105 + 0.3713377118 + 0.3727797866 + 0.3742178977 + 0.3756431937 + 0.3770616055 + 0.3784677088 + 0.3798623085 + 0.3812437057 + 0.3826156855 + 0.3839798868 + 0.3853352964 + 0.3866848946 + 0.3880395889 + 0.3893971145 + 0.3907648027 + 0.3921375871 + 0.3935118020 + 0.3948867023 + 0.3962557912 + 0.3976216912 + 0.3989877999 + 0.4003542066 + 0.4017100036 + 0.4030559957 + 0.4043887854 + 0.4057038128 + 0.4070141017 + 0.4083139896 + 0.4096128941 + 0.4109064043 + 0.4121997952 + 0.4134868085 + 0.4147739112 + 0.4160555899 + 0.4173362851 + 0.4186184108 + 0.4199014008 + 0.4211877882 + 0.4224792123 + 0.4237694144 + 0.4250515997 + 0.4263338149 + 0.4276081026 + 0.4288811088 + 0.4301379919 + 0.4313819110 + 0.4326207042 + 0.4338417947 + 0.4350628853 + 0.4362733960 + 0.4374822080 + 0.4386931062 + 0.4399060011 + 0.4411188960 + 0.4423305988 + 0.4435423911 + 0.4447481036 + 0.4459505081 + 0.4471527040 + 0.4483538866 + 0.4495550990 + 0.4507592022 + 0.4519644976 + 0.4531674981 + 0.4543609023 + 0.4555541873 + 0.4567396045 + 0.4579207897 + 0.4591009021 + 0.4602701068 + 0.4614393115 + 0.4626025856 + 0.4637596905 + 0.4649167955 + 0.4660637975 + 0.4672091901 + 0.4683555067 + 0.4695048034 + 0.4706541896 + 0.4718030095 + 0.4729512036 + 0.4740993977 + 0.4752402902 + 0.4763796926 + 0.4775182903 + 0.4786491096 + 0.4797798991 + 0.4809078872 + 0.4820297956 + 0.4831517935 + 0.4842692018 + 0.4853824079 + 0.4864956141 + 0.4876025915 + 0.4887068868 + 0.4898110926 + 0.4909138083 + 0.4920162857 + 0.4931187034 + 0.4942246974 + 0.4953309894 + 0.4964371026 + 0.4975334108 + 0.4986295998 + 0.4997260869 + 0.5008261800 + 0.5019261837 + 0.5030261278 + 0.5041244030 + 0.5052226782 + 0.5063204765 + 0.5074101090 + 0.5084996819 + 0.5095893145 + 0.5106704831 + 0.5117517710 + 0.5128329992 + 0.5139064789 + 0.5149794221 + 0.5160521865 + 0.5171185732 + 0.5181834102 + 0.5192481875 + 0.5203077197 + 0.5213642716 + 0.5224208832 + 0.5234770179 + 0.5245326161 + 0.5255880952 + 0.5266445875 + 0.5277037024 + 0.5287628174 + 0.5298218727 + 0.5308706760 + 0.5319193006 + 0.5329679847 + 0.5340090990 + 0.5350469947 + 0.5360848904 + 0.5371162891 + 0.5381368995 + 0.5391575098 + 0.5401778221 + 0.5411760211 + 0.5421741009 + 0.5431722999 + 0.5441654921 + 0.5451557040 + 0.5461459160 + 0.5471357107 + 0.5481235981 + 0.5491116047 + 0.5500994921 + 0.5510740876 + 0.5520431995 + 0.5530123115 + 0.5539786220 + 0.5549340248 + 0.5558893085 + 0.5568445921 + 0.5578002930 + 0.5587561727 + 0.5597121119 + 0.5606679916 + 0.5616241097 + 0.5625802875 + 0.5635365248 + 0.5644925237 + 0.5654482245 + 0.5664039850 + 0.5673596859 + 0.5683150887 + 0.5692703724 + 0.5702255964 + 0.5711804032 + 0.5721278787 + 0.5730754733 + 0.5740230083 + 0.5749645829 + 0.5758937001 + 0.5768228769 + 0.5777519941 + 0.5786722898 + 0.5795848966 + 0.5804976225 + 0.5814102888 + 0.5823227167 + 0.5832350850 + 0.5841473937 + 0.5850597024 + 0.5859693885 + 0.5868784785 + 0.5877876878 + 0.5886967778 + 0.5896003246 + 0.5905032754 + 0.5914062858 + 0.5923094153 + 0.5932080746 + 0.5941067934 + 0.5950053930 + 0.5959041119 + 0.5967944860 + 0.5976848006 + 0.5985751748 + 0.5994654894 + 0.6003587246 + 0.6012520790 + 0.6021453738 + 0.6030387282 + 0.6039313078 + 0.6048238277 + 0.6057162285 + 0.6066086888 + 0.6074911952 + 0.6083701849 + 0.6092491746 + 0.6101281047 + 0.6109964848 + 0.6118568778 + 0.6127172112 + 0.6135776043 + 0.6144360900 + 0.6152914166 + 0.6161466241 + 0.6170017719 + 0.6178573966 + 0.6187161803 + 0.6195750237 + 0.6204338074 + 0.6212925911 + 0.6221556067 + 0.6230198741 + 0.6238840818 + 0.6247482896 + 0.6256136894 + 0.6264808178 + 0.6273478866 + 0.6282148957 + 0.6290820241 + 0.6299490929 + 0.6308162212 + 0.6316832900 + 0.6325504780 + 0.6334146857 + 0.6342768073 + 0.6351389885 + 0.6360011101 + 0.6368619800 + 0.6377084851 + 0.6385549903 + 0.6394014955 + 0.6402480006 + 0.6410933137 + 0.6419374943 + 0.6427816749 + 0.6436259747 + 0.6444702148 + 0.6453065276 + 0.6461421847 + 0.6469779015 + 0.6478136182 + 0.6486436725 + 0.6494596004 + 0.6502755880 + 0.6510915160 + 0.6519075036 + 0.6527132988 + 0.6535121202 + 0.6543108821 + 0.6551098228 + 0.6559085846 + 0.6566948891 + 0.6574789286 + 0.6582627892 + 0.6590468287 + 0.6598308086 + 0.6606147885 + 0.6613988280 + 0.6621828079 + 0.6629667878 + 0.6637505293 + 0.6645327210 + 0.6653149128 + 0.6660971045 + 0.6668794155 + 0.6676582098 + 0.6684288979 + 0.6691997051 + 0.6699703932 + 0.6707410812 + 0.6715074182 + 0.6722648144 + 0.6730222106 + 0.6737796068 + 0.6745370030 + 0.6752945185 + 0.6760522127 + 0.6768100262 + 0.6775677204 + 0.6783254743 + 0.6790840030 + 0.6798446774 + 0.6806052923 + 0.6813660264 + 0.6821267009 + 0.6828873158 + 0.6836469769 + 0.6844068170 + 0.6851665974 + 0.6859263182 + 0.6866860986 + 0.6874372959 + 0.6881881952 + 0.6889392138 + 0.6896901131 + 0.6904410124 + 0.6911798120 + 0.6919142008 + 0.6926484704 + 0.6933829188 + 0.6941173077 + 0.6948506236 + 0.6955826283 + 0.6963145733 + 0.6970465183 + 0.6977785230 + 0.6985105276 + 0.6992424726 + 0.6999745965 + 0.7007066011 + 0.7014386058 + 0.7021707296 + 0.7028998137 + 0.7036275864 + 0.7043554187 + 0.7050831914 + 0.7058110237 + 0.7065402269 + 0.7072740197 + 0.7080078125 + 0.7087416053 + 0.7094753981 + 0.7102091908 + 0.7109380960 + 0.7116650939 + 0.7123920918 + 0.7131190896 + 0.7138460875 + 0.7145711780 + 0.7152836919 + 0.7159962058 + 0.7167087197 + 0.7174212933 + 0.7181338072 + 0.7188457847 + 0.7195572257 + 0.7202687263 + 0.7209801078 + 0.7216914892 + 0.7224029899 + 0.7231106758 + 0.7238175273 + 0.7245242000 + 0.7252308726 + 0.7259377241 + 0.7266442180 + 0.7273383141 + 0.7280324101 + 0.7287266254 + 0.7294207215 + 0.7301148772 + 0.7308074832 + 0.7314937115 + 0.7321799994 + 0.7328662276 + 0.7335523963 + 0.7342386246 + 0.7349203229 + 0.7355914712 + 0.7362626791 + 0.7369338870 + 0.7376050949 + 0.7382763028 + 0.7389466763 + 0.7396153808 + 0.7402840853 + 0.7409527898 + 0.7416214943 + 0.7422903180 + 0.7429624796 + 0.7436416745 + 0.7443209887 + 0.7450001836 + 0.7456793785 + 0.7463586926 + 0.7470390201 + 0.7477223873 + 0.7484058738 + 0.7490894198 + 0.7497729063 + 0.7504563928 + 0.7511395812 + 0.7518200874 + 0.7525004745 + 0.7531809807 + 0.7538614273 + 0.7545418739 + 0.7552223802 + 0.7558920979 + 0.7565605044 + 0.7572287917 + 0.7578970790 + 0.7585654855 + 0.7592337728 + 0.7598943114 + 0.7605494261 + 0.7612044811 + 0.7618595958 + 0.7625148296 + 0.7631698847 + 0.7638273835 + 0.7644920945 + 0.7651568055 + 0.7658215165 + 0.7664862275 + 0.7671508193 + 0.7678154707 + 0.7684820890 + 0.7691490054 + 0.7698159814 + 0.7704828978 + 0.7711498737 + 0.7718167901 + 0.7724804878 + 0.7731373906 + 0.7737944126 + 0.7744513750 + 0.7751083970 + 0.7757654190 + 0.7764223814 + 0.7770789862 + 0.7777354121 + 0.7783918977 + 0.7790483832 + 0.7797049284 + 0.7803612947 + 0.7810173035 + 0.7816702127 + 0.7823230028 + 0.7829759121 + 0.7836287022 + 0.7842816114 + 0.7849345207 + 0.7855864167 + 0.7862372994 + 0.7868881822 + 0.7875390053 + 0.7881898880 + 0.7888407707 + 0.7894915938 + 0.7901493907 + 0.7908108234 + 0.7914720774 + 0.7921335101 + 0.7927948833 + 0.7934563160 + 0.7941176295 + 0.7947735190 + 0.7954280972 + 0.7960826755 + 0.7967371941 + 0.7973917723 + 0.7980462909 + 0.7987009287 + 0.7993466258 + 0.7999910712 + 0.8006355762 + 0.8012802005 + 0.8019247055 + 0.8025692105 + 0.8032137156 + 0.8038467169 + 0.8044781089 + 0.8051096201 + 0.8057410121 + 0.8063725233 + 0.8070039153 + 0.8076354265 + 0.8082551956 + 0.8088722229 + 0.8094893098 + 0.8101062775 + 0.8107234240 + 0.8113403916 + 0.8119574785 + 0.8125758171 + 0.8131948113 + 0.8138139248 + 0.8144329190 + 0.8150519133 + 0.8156709075 + 0.8162900209 + 0.8169054985 + 0.8175162077 + 0.8181269169 + 0.8187375069 + 0.8193482161 + 0.8199589252 + 0.8205695748 + 0.8211792707 + 0.8217806220 + 0.8223819733 + 0.8229833841 + 0.8235847950 + 0.8241862059 + 0.8247876167 + 0.8253890276 + 0.8259866834 + 0.8265830278 + 0.8271793127 + 0.8277755976 + 0.8283720016 + 0.8289682865 + 0.8295645714 + 0.8301581144 + 0.8307436705 + 0.8313292861 + 0.8319149017 + 0.8325005174 + 0.8330860734 + 0.8336716890 + 0.8342573047 + 0.8348429799 + 0.8354287148 + 0.8360143900 + 0.8366001844 + 0.8371859193 + 0.8377715945 + 0.8383573294 + 0.8389438987 + 0.8395407200 + 0.8401374817 + 0.8407343030 + 0.8413311243 + 0.8419278860 + 0.8425247073 + 0.8431215286 + 0.8437163830 + 0.8443077803 + 0.8448992968 + 0.8454906940 + 0.8460822105 + 0.8466736078 + 0.8472651243 + 0.8478565216 + 0.8484436274 + 0.8490272760 + 0.8496111035 + 0.8501948714 + 0.8507785797 + 0.8513624072 + 0.8519461155 + 0.8525298834 + 0.8531131744 + 0.8536962271 + 0.8542792201 + 0.8548622131 + 0.8554453254 + 0.8560283184 + 0.8566113114 + 0.8571943045 + 0.8577736020 + 0.8583512902 + 0.8589290977 + 0.8595069051 + 0.8600847125 + 0.8606625199 + 0.8612403274 + 0.8618180752 + 0.8624026775 + 0.8629906178 + 0.8635786176 + 0.8641664982 + 0.8647544980 + 0.8653423786 + 0.8659303784 + 0.8665183187 + 0.8671081066 + 0.8676996231 + 0.8682910800 + 0.8688824773 + 0.8694739938 + 0.8700655103 + 0.8706569076 + 0.8712484241 + 0.8718379140 + 0.8724226952 + 0.8730074763 + 0.8735923171 + 0.8741770983 + 0.8747618794 + 0.8753467202 + 0.8759315014 + 0.8765162826 + 0.8770982027 + 0.8776800036 + 0.8782619238 + 0.8788437843 + 0.8794255853 + 0.8800075054 + 0.8805894256 + 0.8811712265 + 0.8817477226 + 0.8823202848 + 0.8828929067 + 0.8834654093 + 0.8840379715 + 0.8846104741 + 0.8851830959 + 0.8857555985 + 0.8863282800 + 0.8869022727 + 0.8874762058 + 0.8880501986 + 0.8886241913 + 0.8891981244 + 0.8897721171 + 0.8903459907 + 0.8909199834 + 0.8914952874 + 0.8920720220 + 0.8926486969 + 0.8932253718 + 0.8938022256 + 0.8943789005 + 0.8949555755 + 0.8955323100 + 0.8961089849 + 0.8966785073 + 0.8972461224 + 0.8978137970 + 0.8983814716 + 0.8989492059 + 0.8995168209 + 0.9000844955 + 0.9006522298 + 0.9012197256 + 0.9017801881 + 0.9023407102 + 0.9029011726 + 0.9034616947 + 0.9040222168 + 0.9045826793 + 0.9051432014 + 0.9057037234 + 0.9062626958 + 0.9068132043 + 0.9073637128 + 0.9079142213 + 0.9084647894 + 0.9090152979 + 0.9095658064 + 0.9101163149 + 0.9106668234 + 0.9112154245 + 0.9117550254 + 0.9122946858 + 0.9128342867 + 0.9133738875 + 0.9139136076 + 0.9144532084 + 0.9149928093 + 0.9155325294 + 0.9160724878 + 0.9166154861 + 0.9171584845 + 0.9177014828 + 0.9182446003 + 0.9187875986 + 0.9193305969 + 0.9198735952 + 0.9204165936 + 0.9209595919 + 0.9214972854 + 0.9220345020 + 0.9225717783 + 0.9231091142 + 0.9236463904 + 0.9241837263 + 0.9247208834 + 0.9252582192 + 0.9257954955 + 0.9263281822 + 0.9268587828 + 0.9273893833 + 0.9279199839 + 0.9284505248 + 0.9289811254 + 0.9295117259 + 0.9300423265 + 0.9305728078 + 0.9311010838 + 0.9316242933 + 0.9321475029 + 0.9326707125 + 0.9331939220 + 0.9337170720 + 0.9342402816 + 0.9347636104 + 0.9352868199 + 0.9358100295 + 0.9363203049 + 0.9368283749 + 0.9373363853 + 0.9378443956 + 0.9383524060 + 0.9388604164 + 0.9393684268 + 0.9398764968 + 0.9403845072 + 0.9408923984 + 0.9413998723 + 0.9419075251 + 0.9424151182 + 0.9429225922 + 0.9434301853 + 0.9439377785 + 0.9444453120 + 0.9449529052 + 0.9454603791 + 0.9459722042 + 0.9464867711 + 0.9470013976 + 0.9475159049 + 0.9480304718 + 0.9485449791 + 0.9490596056 + 0.9495741725 + 0.9500886798 + 0.9506033063 + 0.9511142969 + 0.9516245127 + 0.9521347880 + 0.9526450038 + 0.9531552196 + 0.9536654949 + 0.9541757107 + 0.9546859264 + 0.9551960826 + 0.9557064176 + 0.9562019110 + 0.9566963911 + 0.9571908712 + 0.9576854110 + 0.9581798911 + 0.9586743712 + 0.9591689110 + 0.9596635103 + 0.9601579905 + 0.9606524706 + 0.9611294270 + 0.9616053104 + 0.9620811939 + 0.9625570774 + 0.9630330205 + 0.9635089040 + 0.9639847875 + 0.9644606709 + 0.9649366140 + 0.9654123783 + 0.9658750892 + 0.9663354158 + 0.9667956829 + 0.9672560096 + 0.9677162170 + 0.9681764841 + 0.9686368108 + 0.9690970778 + 0.9695574045 + 0.9700176120 + 0.9704771042 + 0.9709361196 + 0.9713950753 + 0.9718540907 + 0.9723131061 + 0.9727721214 + 0.9732310772 + 0.9736900926 + 0.9741491079 + 0.9746081829 + 0.9750655293 + 0.9755191803 + 0.9759730101 + 0.9764267206 + 0.9768803716 + 0.9773342013 + 0.9777879119 + 0.9782416821 + 0.9786953926 + 0.9791491032 + 0.9796028733 + 0.9800477028 + 0.9804913998 + 0.9809350967 + 0.9813787937 + 0.9818224907 + 0.9822661877 + 0.9827098846 + 0.9831535816 + 0.9835972786 + 0.9840409756 + 0.9844785929 + 0.9849042296 + 0.9853296876 + 0.9857553244 + 0.9861809015 + 0.9866064787 + 0.9870321155 + 0.9874576926 + 0.9878833294 + 0.9883089066 + 0.9887344837 + 0.9891439080 + 0.9895455837 + 0.9899473190 + 0.9903489947 + 0.9907506704 + 0.9911524057 + 0.9915542006 + 0.9919558764 + 0.9923576117 + 0.9927592874 + 0.9931610227 + 0.9935230017 + 0.9938808084 + 0.9942384958 + 0.9945963025 + 0.9949539900 + 0.9953116775 + 0.9956694841 + 0.9960271716 + 0.9963849783 + 0.9967427254 + 0.9970995188 + 0.9973632097 + 0.9976267815 + 0.9978904724 + 0.9981542230 + 0.9984179139 + 0.9986816049 + 0.9989452958 + 0.9992089272 + 0.9994726181 + 0.9997363091 + 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/eastman_double_x_neg_5min.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/eastman_double_x_neg_5min.spi1d index d199eba7..6aef05c7 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/eastman_double_x_neg_5min.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/eastman_double_x_neg_5min.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 1 -{ - 0.0000000000 - 0.0030272121 - 0.0063094199 - 0.0098175881 - 0.0132390903 - 0.0165068507 - 0.0197617300 - 0.0230012909 - 0.0261860900 - 0.0293740109 - 0.0325979404 - 0.0358636007 - 0.0391314887 - 0.0424252301 - 0.0457398184 - 0.0491124503 - 0.0525345095 - 0.0558606908 - 0.0591593795 - 0.0623592213 - 0.0656063929 - 0.0688082799 - 0.0719619915 - 0.0750681385 - 0.0781241581 - 0.0811320320 - 0.0840930864 - 0.0869851932 - 0.0898269489 - 0.0926580206 - 0.0954112411 - 0.0981268808 - 0.1008647010 - 0.1035747007 - 0.1062444001 - 0.1088567972 - 0.1114612967 - 0.1140118018 - 0.1165103987 - 0.1189728975 - 0.1214018017 - 0.1238031015 - 0.1261426061 - 0.1285005063 - 0.1308861971 - 0.1332633942 - 0.1356558949 - 0.1380044073 - 0.1403446943 - 0.1426548958 - 0.1449654996 - 0.1472640932 - 0.1495569050 - 0.1518307030 - 0.1540544927 - 0.1562412977 - 0.1584434956 - 0.1606242061 - 0.1627897024 - 0.1649412066 - 0.1670477986 - 0.1691658944 - 0.1713186055 - 0.1734748930 - 0.1756159067 - 0.1777509004 - 0.1798588932 - 0.1819704026 - 0.1841046959 - 0.1862291992 - 0.1883134991 - 0.1904103011 - 0.1924743056 - 0.1944988072 - 0.1965171993 - 0.1985401064 - 0.2005351931 - 0.2025036067 - 0.2044758946 - 0.2064372003 - 0.2083602995 - 0.2102862000 - 0.2122135013 - 0.2141225934 - 0.2159958929 - 0.2178680003 - 0.2197511047 - 0.2216199934 - 0.2234732956 - 0.2253248990 - 0.2271879017 - 0.2290568948 - 0.2309205979 - 0.2327875942 - 0.2346781045 - 0.2365760058 - 0.2384558022 - 0.2403216064 - 0.2421821058 - 0.2440275997 - 0.2458492070 - 0.2476378977 - 0.2494101971 - 0.2511883080 - 0.2529624104 - 0.2547251880 - 0.2564688921 - 0.2581931055 - 0.2599115074 - 0.2616336942 - 0.2633337080 - 0.2650091052 - 0.2666670084 - 0.2683193982 - 0.2699855864 - 0.2716380954 - 0.2732735872 - 0.2748895884 - 0.2764844894 - 0.2780745029 - 0.2796624005 - 0.2812372148 - 0.2827902138 - 0.2843335867 - 0.2858856916 - 0.2874445021 - 0.2890088856 - 0.2905732989 - 0.2921339869 - 0.2936922908 - 0.2952659130 - 0.2968513072 - 0.2984412909 - 0.3000228107 - 0.3015750945 - 0.3031128943 - 0.3046396971 - 0.3061710894 - 0.3077040017 - 0.3092370927 - 0.3107506931 - 0.3122516870 - 0.3137294054 - 0.3152104020 - 0.3166939020 - 0.3181836009 - 0.3196648061 - 0.3211343884 - 0.3225882053 - 0.3240387142 - 0.3254922032 - 0.3269436955 - 0.3283936083 - 0.3298347890 - 0.3312718868 - 0.3326953948 - 0.3341206014 - 0.3355478048 - 0.3369952142 - 0.3384498060 - 0.3399038017 - 0.3413537145 - 0.3427903950 - 0.3442232013 - 0.3456513882 - 0.3470813930 - 0.3485122919 - 0.3499529958 - 0.3513954878 - 0.3528392911 - 0.3542819917 - 0.3557189107 - 0.3571557105 - 0.3585920930 - 0.3600325882 - 0.3614800870 - 0.3629246950 - 0.3643657863 - 0.3657987118 - 0.3672237992 - 0.3686499894 - 0.3700770140 - 0.3714995086 - 0.3729186952 - 0.3743284941 - 0.3757312894 - 0.3771198094 - 0.3784962893 - 0.3798680007 - 0.3812347949 - 0.3826048970 - 0.3839792907 - 0.3853527904 - 0.3867248893 - 0.3880946934 - 0.3894568980 - 0.3908174038 - 0.3921599090 - 0.3935025036 - 0.3948425949 - 0.3961825073 - 0.3975262940 - 0.3988716006 - 0.4002110064 - 0.4015452862 - 0.4028792977 - 0.4042125046 - 0.4055452049 - 0.4068675935 - 0.4081901014 - 0.4095175862 - 0.4108465910 - 0.4121741951 - 0.4135001898 - 0.4148246050 - 0.4161387980 - 0.4174529910 - 0.4187706113 - 0.4200890958 - 0.4214057028 - 0.4227195084 - 0.4240333140 - 0.4253349006 - 0.4266364872 - 0.4279260933 - 0.4292064011 - 0.4304861128 - 0.4317606986 - 0.4330351949 - 0.4343157113 - 0.4355998933 - 0.4368841946 - 0.4381690919 - 0.4394541085 - 0.4407306015 - 0.4420005977 - 0.4432705939 - 0.4445107877 - 0.4457508922 - 0.4469895065 - 0.4482258856 - 0.4494622052 - 0.4506838918 - 0.4519014955 - 0.4531166852 - 0.4543156028 - 0.4555146098 - 0.4567095041 - 0.4578996897 - 0.4590897858 - 0.4602785110 - 0.4614667892 - 0.4626550078 - 0.4638502896 - 0.4650456905 - 0.4662390947 - 0.4674263895 - 0.4686135948 - 0.4697979987 - 0.4709784091 - 0.4721589088 - 0.4733403027 - 0.4745222926 - 0.4757043123 - 0.4768933952 - 0.4780850112 - 0.4792765081 - 0.4804633856 - 0.4816494882 - 0.4828355014 - 0.4839955866 - 0.4851537943 - 0.4863120019 - 0.4874627888 - 0.4886133969 - 0.4897640944 - 0.4909220040 - 0.4920799136 - 0.4932379127 - 0.4943912029 - 0.4955444932 - 0.4966978133 - 0.4978221059 - 0.4989440143 - 0.5000659823 - 0.5011757016 - 0.5022830963 - 0.5033904910 - 0.5044888854 - 0.5055838823 - 0.5066789985 - 0.5077672005 - 0.5088503957 - 0.5099335909 - 0.5110126734 - 0.5120856166 - 0.5131585002 - 0.5142297745 - 0.5152938962 - 0.5163578987 - 0.5174219012 - 0.5184922814 - 0.5195631981 - 0.5206341147 - 0.5217061043 - 0.5227789283 - 0.5238515735 - 0.5249236822 - 0.5259944201 - 0.5270652175 - 0.5281358957 - 0.5291953087 - 0.5302540064 - 0.5313128233 - 0.5323665738 - 0.5334162712 - 0.5344660282 - 0.5355166197 - 0.5365738273 - 0.5376309752 - 0.5386883020 - 0.5397393703 - 0.5407869816 - 0.5418345928 - 0.5428798199 - 0.5439077020 - 0.5449357033 - 0.5459637046 - 0.5469853878 - 0.5480024219 - 0.5490195155 - 0.5500364900 - 0.5510535836 - 0.5520707965 - 0.5530878901 - 0.5541033745 - 0.5551161170 - 0.5561287999 - 0.5571414828 - 0.5581471920 - 0.5591498017 - 0.5601524115 - 0.5611549020 - 0.5621367097 - 0.5631183982 - 0.5641000271 - 0.5650776029 - 0.5660427809 - 0.5670080781 - 0.5679733753 - 0.5689417124 - 0.5699135065 - 0.5708853006 - 0.5718570948 - 0.5728263855 - 0.5737943053 - 0.5747622252 - 0.5757302046 - 0.5766975880 - 0.5776649117 - 0.5786321759 - 0.5795994997 - 0.5805698037 - 0.5815405250 - 0.5825111270 - 0.5834817886 - 0.5844538808 - 0.5854260921 - 0.5863983035 - 0.5873705745 - 0.5883414149 - 0.5893121958 - 0.5902829766 - 0.5912536979 - 0.5922099948 - 0.5931643248 - 0.5941184759 - 0.5950728059 - 0.5960159898 - 0.5969563723 - 0.5978966951 - 0.5988370776 - 0.5997706056 - 0.6007006764 - 0.6016308069 - 0.6025608182 - 0.6034846902 - 0.6044021845 - 0.6053197980 - 0.6062374115 - 0.6071522832 - 0.6080605984 - 0.6089687943 - 0.6098771095 - 0.6107851863 - 0.6116871238 - 0.6125890017 - 0.6134908795 - 0.6143928170 - 0.6152886152 - 0.6161820889 - 0.6170755029 - 0.6179689765 - 0.6188614964 - 0.6197524071 - 0.6206433773 - 0.6215342879 - 0.6224251986 - 0.6233009100 - 0.6241760254 - 0.6250510812 - 0.6259261966 - 0.6267927885 - 0.6276513934 - 0.6285099983 - 0.6293684840 - 0.6302270889 - 0.6310849786 - 0.6319429278 - 0.6328008175 - 0.6336587071 - 0.6345123053 - 0.6353611946 - 0.6362100244 - 0.6370587945 - 0.6379076838 - 0.6387491822 - 0.6395896077 - 0.6404299736 - 0.6412703991 - 0.6421101093 - 0.6429476142 - 0.6437851191 - 0.6446226835 - 0.6454601884 - 0.6463015079 - 0.6471462250 - 0.6479908824 - 0.6488355994 - 0.6496803164 - 0.6505240798 - 0.6513677835 - 0.6522114873 - 0.6530550718 - 0.6538987756 - 0.6547306180 - 0.6555622816 - 0.6563938856 - 0.6572256088 - 0.6580554843 - 0.6588743925 - 0.6596933007 - 0.6605123281 - 0.6613311768 - 0.6621484160 - 0.6629601717 - 0.6637719870 - 0.6645838022 - 0.6653956175 - 0.6662080288 - 0.6670222878 - 0.6678364873 - 0.6686508060 - 0.6694651246 - 0.6702783108 - 0.6710888147 - 0.6718993187 - 0.6727098227 - 0.6735203266 - 0.6743295789 - 0.6751338840 - 0.6759383082 - 0.6767426133 - 0.6775469780 - 0.6783509851 - 0.6791514158 - 0.6799517870 - 0.6807522774 - 0.6815527081 - 0.6823530793 - 0.6831542850 - 0.6839554906 - 0.6847568154 - 0.6855580211 - 0.6863592863 - 0.6871563792 - 0.6879516244 - 0.6887468100 - 0.6895419955 - 0.6903371215 - 0.6911249757 - 0.6919013262 - 0.6926776171 - 0.6934539080 - 0.6942301989 - 0.6950061917 - 0.6957769990 - 0.6965478063 - 0.6973184943 - 0.6980893016 - 0.6988599896 - 0.6996240020 - 0.7003840208 - 0.7011440992 - 0.7019041181 - 0.7026641965 - 0.7034214735 - 0.7041661143 - 0.7049108148 - 0.7056553960 - 0.7063999772 - 0.7071446776 - 0.7078785896 - 0.7086068988 - 0.7093350887 - 0.7100633979 - 0.7107917070 - 0.7115197182 - 0.7122437954 - 0.7129678726 - 0.7136920094 - 0.7144160867 - 0.7151402235 - 0.7158653140 - 0.7165917754 - 0.7173182964 - 0.7180448174 - 0.7187712789 - 0.7194977999 - 0.7202196121 - 0.7209396958 - 0.7216597199 - 0.7223798037 - 0.7230998874 - 0.7238199711 - 0.7245283127 - 0.7252361774 - 0.7259441018 - 0.7266520858 - 0.7273600101 - 0.7280663252 - 0.7287629247 - 0.7294594049 - 0.7301560044 - 0.7308526039 - 0.7315490842 - 0.7322456241 - 0.7329418063 - 0.7336379886 - 0.7343341708 - 0.7350304127 - 0.7357265949 - 0.7364248037 - 0.7371274829 - 0.7378302813 - 0.7385330200 - 0.7392356992 - 0.7399383783 - 0.7406404018 - 0.7413403988 - 0.7420405149 - 0.7427405119 - 0.7434405088 - 0.7441406250 - 0.7448390722 - 0.7455317974 - 0.7462245226 - 0.7469171882 - 0.7476099133 - 0.7483025789 - 0.7489954233 - 0.7496907115 - 0.7503858805 - 0.7510811090 - 0.7517763972 - 0.7524716258 - 0.7531669140 - 0.7538704872 - 0.7545759082 - 0.7552812099 - 0.7559865713 - 0.7566918731 - 0.7573972940 - 0.7581034899 - 0.7588102818 - 0.7595170736 - 0.7602239251 - 0.7609307170 - 0.7616376281 - 0.7623426914 - 0.7630413175 - 0.7637398839 - 0.7644385099 - 0.7651370764 - 0.7658355832 - 0.7665342093 - 0.7672234178 - 0.7679098845 - 0.7685962915 - 0.7692828178 - 0.7699692845 - 0.7706558108 - 0.7713416219 - 0.7720258832 - 0.7727100849 - 0.7733944058 - 0.7740787268 - 0.7747629285 - 0.7754471898 - 0.7761262059 - 0.7768033743 - 0.7774804831 - 0.7781575918 - 0.7788347006 - 0.7795119286 - 0.7801874876 - 0.7808486819 - 0.7815099955 - 0.7821711898 - 0.7828325033 - 0.7834938169 - 0.7841550112 - 0.7848129869 - 0.7854657769 - 0.7861186266 - 0.7867714167 - 0.7874242067 - 0.7880769968 - 0.7887297869 - 0.7893806100 - 0.7900301814 - 0.7906798124 - 0.7913293839 - 0.7919788957 - 0.7926285267 - 0.7932780981 - 0.7939221859 - 0.7945647836 - 0.7952073216 - 0.7958499193 - 0.7964923978 - 0.7971348763 - 0.7977774739 - 0.7984188199 - 0.7990599275 - 0.7997009754 - 0.8003420234 - 0.8009830713 - 0.8016241789 - 0.8022652864 - 0.8028982282 - 0.8035295010 - 0.8041607738 - 0.8047922254 - 0.8054234982 - 0.8060547709 - 0.8066862226 - 0.8073112965 - 0.8079345822 - 0.8085579276 - 0.8091812134 - 0.8098044991 - 0.8104277253 - 0.8110510111 - 0.8116706014 - 0.8122878075 - 0.8129050136 - 0.8135222197 - 0.8141394854 - 0.8147566915 - 0.8153738976 - 0.8159893751 - 0.8166022897 - 0.8172150850 - 0.8178278804 - 0.8184407949 - 0.8190535903 - 0.8196663857 - 0.8202791214 - 0.8208892941 - 0.8214995861 - 0.8221098781 - 0.8227202296 - 0.8233304024 - 0.8239406943 - 0.8245509863 - 0.8251606226 - 0.8257699013 - 0.8263791800 - 0.8269885182 - 0.8275977969 - 0.8282071948 - 0.8288164735 - 0.8294249773 - 0.8300309777 - 0.8306369781 - 0.8312429786 - 0.8318489790 - 0.8324549794 - 0.8330609798 - 0.8336669207 - 0.8342692852 - 0.8348699808 - 0.8354706764 - 0.8360713720 - 0.8366721272 - 0.8372727036 - 0.8378733993 - 0.8384736776 - 0.8390662074 - 0.8396586180 - 0.8402510881 - 0.8408436179 - 0.8414360881 - 0.8420284986 - 0.8426210284 - 0.8432112932 - 0.8437972069 - 0.8443831801 - 0.8449690938 - 0.8455551267 - 0.8461410999 - 0.8467270136 - 0.8473129869 - 0.8479040265 - 0.8484994769 - 0.8490949273 - 0.8496903181 - 0.8502858281 - 0.8508812189 - 0.8514766097 - 0.8520721197 - 0.8526653051 - 0.8532572985 - 0.8538494110 - 0.8544414043 - 0.8550335169 - 0.8556255102 - 0.8562176228 - 0.8568096161 - 0.8573905230 - 0.8579664230 - 0.8585423827 - 0.8591184020 - 0.8596943021 - 0.8602703214 - 0.8608462811 - 0.8614221811 - 0.8619980216 - 0.8625736833 - 0.8631494045 - 0.8637250066 - 0.8643007278 - 0.8648763895 - 0.8654521108 - 0.8660277128 - 0.8666040897 - 0.8671810031 - 0.8677579761 - 0.8683350086 - 0.8689119220 - 0.8694888949 - 0.8700659275 - 0.8706427813 - 0.8712193966 - 0.8717948198 - 0.8723701835 - 0.8729457259 - 0.8735210896 - 0.8740965128 - 0.8746719956 - 0.8752474189 - 0.8758227825 - 0.8763949871 - 0.8769670129 - 0.8775389791 - 0.8781110048 - 0.8786829710 - 0.8792549968 - 0.8798270226 - 0.8803989887 - 0.8809679747 - 0.8815342784 - 0.8821007013 - 0.8826671243 - 0.8832334876 - 0.8837999105 - 0.8843662143 - 0.8849325776 - 0.8854988217 - 0.8860610127 - 0.8866233230 - 0.8871855140 - 0.8877478242 - 0.8883100748 - 0.8888723254 - 0.8894345760 - 0.8899968266 - 0.8905580044 - 0.8911178112 - 0.8916776180 - 0.8922374249 - 0.8927971721 - 0.8933569789 - 0.8939167857 - 0.8944765925 - 0.8950363994 - 0.8955954909 - 0.8961544037 - 0.8967131972 - 0.8972721100 - 0.8978309035 - 0.8983898163 - 0.8989487290 - 0.8995075226 - 0.9000663757 - 0.9006261230 - 0.9011856914 - 0.9017453790 - 0.9023050070 - 0.9028646946 - 0.9034243226 - 0.9039840102 - 0.9045435786 - 0.9051036239 - 0.9056655169 - 0.9062274098 - 0.9067893028 - 0.9073511958 - 0.9079130888 - 0.9084749818 - 0.9090368748 - 0.9095988274 - 0.9101592898 - 0.9107123017 - 0.9112653136 - 0.9118183255 - 0.9123712182 - 0.9129241705 - 0.9134771824 - 0.9140301943 - 0.9145832062 - 0.9151350856 - 0.9156765938 - 0.9162181020 - 0.9167596102 - 0.9173009992 - 0.9178425074 - 0.9183840156 - 0.9189255238 - 0.9194669724 - 0.9200084805 - 0.9205374122 - 0.9210653901 - 0.9215933084 - 0.9221212864 - 0.9226492047 - 0.9231771827 - 0.9237051010 - 0.9242330790 - 0.9247609973 - 0.9252843857 - 0.9258053899 - 0.9263263941 - 0.9268475175 - 0.9273685217 - 0.9278895855 - 0.9284105897 - 0.9289315939 - 0.9294527173 - 0.9299737811 - 0.9304950833 - 0.9310163260 - 0.9315376282 - 0.9320588708 - 0.9325801730 - 0.9331014752 - 0.9336227179 - 0.9341440201 - 0.9346653223 - 0.9351785183 - 0.9356899858 - 0.9362015128 - 0.9367129803 - 0.9372245073 - 0.9377359748 - 0.9382475019 - 0.9387590289 - 0.9392704964 - 0.9397779703 - 0.9402741790 - 0.9407703876 - 0.9412665963 - 0.9417628050 - 0.9422590733 - 0.9427552819 - 0.9432514906 - 0.9437476993 - 0.9442439079 - 0.9447419047 - 0.9452412128 - 0.9457404017 - 0.9462395906 - 0.9467388988 - 0.9472380877 - 0.9477373958 - 0.9482365847 - 0.9487358928 - 0.9492350817 - 0.9497361183 - 0.9502375722 - 0.9507390261 - 0.9512404203 - 0.9517418742 - 0.9522433281 - 0.9527447820 - 0.9532461762 - 0.9537475705 - 0.9542490840 - 0.9547458887 - 0.9552422762 - 0.9557387233 - 0.9562351704 - 0.9567316175 - 0.9572281241 - 0.9577245116 - 0.9582208991 - 0.9587174058 - 0.9592137933 - 0.9596955180 - 0.9601761103 - 0.9606567025 - 0.9611372948 - 0.9616180062 - 0.9620985985 - 0.9625791907 - 0.9630599022 - 0.9635404944 - 0.9640210867 - 0.9644860029 - 0.9649478793 - 0.9654096961 - 0.9658715725 - 0.9663333893 - 0.9667953253 - 0.9672570825 - 0.9677190185 - 0.9681807756 - 0.9686427116 - 0.9691007733 - 0.9695566893 - 0.9700127244 - 0.9704685807 - 0.9709244967 - 0.9713804722 - 0.9718363881 - 0.9722923040 - 0.9727482200 - 0.9732041955 - 0.9736583829 - 0.9741089940 - 0.9745596051 - 0.9750102162 - 0.9754608274 - 0.9759113789 - 0.9763619900 - 0.9768124819 - 0.9772630930 - 0.9777137041 - 0.9781643152 - 0.9786108732 - 0.9790568948 - 0.9795029163 - 0.9799489975 - 0.9803950191 - 0.9808409810 - 0.9812871218 - 0.9817330837 - 0.9821791053 - 0.9826251268 - 0.9830691218 - 0.9835088253 - 0.9839485288 - 0.9843881726 - 0.9848278761 - 0.9852675796 - 0.9857072830 - 0.9861469865 - 0.9865866899 - 0.9870265126 - 0.9874662161 - 0.9878935218 - 0.9883149266 - 0.9887362719 - 0.9891576767 - 0.9895790815 - 0.9900006056 - 0.9904220104 - 0.9908434153 - 0.9912648201 - 0.9916862249 - 0.9921076298 - 0.9924955964 - 0.9928798079 - 0.9932640791 - 0.9936484098 - 0.9940326810 - 0.9944170117 - 0.9948012233 - 0.9951854944 - 0.9955698252 - 0.9959540963 - 0.9963378906 - 0.9966707826 - 0.9970036745 - 0.9973366857 - 0.9976695776 - 0.9980025291 - 0.9983354211 - 0.9986683130 - 0.9990012050 - 0.9993342161 - 0.9996671081 - 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 1 +{ + 0.0000000000 + 0.0030272121 + 0.0063094199 + 0.0098175881 + 0.0132390903 + 0.0165068507 + 0.0197617300 + 0.0230012909 + 0.0261860900 + 0.0293740109 + 0.0325979404 + 0.0358636007 + 0.0391314887 + 0.0424252301 + 0.0457398184 + 0.0491124503 + 0.0525345095 + 0.0558606908 + 0.0591593795 + 0.0623592213 + 0.0656063929 + 0.0688082799 + 0.0719619915 + 0.0750681385 + 0.0781241581 + 0.0811320320 + 0.0840930864 + 0.0869851932 + 0.0898269489 + 0.0926580206 + 0.0954112411 + 0.0981268808 + 0.1008647010 + 0.1035747007 + 0.1062444001 + 0.1088567972 + 0.1114612967 + 0.1140118018 + 0.1165103987 + 0.1189728975 + 0.1214018017 + 0.1238031015 + 0.1261426061 + 0.1285005063 + 0.1308861971 + 0.1332633942 + 0.1356558949 + 0.1380044073 + 0.1403446943 + 0.1426548958 + 0.1449654996 + 0.1472640932 + 0.1495569050 + 0.1518307030 + 0.1540544927 + 0.1562412977 + 0.1584434956 + 0.1606242061 + 0.1627897024 + 0.1649412066 + 0.1670477986 + 0.1691658944 + 0.1713186055 + 0.1734748930 + 0.1756159067 + 0.1777509004 + 0.1798588932 + 0.1819704026 + 0.1841046959 + 0.1862291992 + 0.1883134991 + 0.1904103011 + 0.1924743056 + 0.1944988072 + 0.1965171993 + 0.1985401064 + 0.2005351931 + 0.2025036067 + 0.2044758946 + 0.2064372003 + 0.2083602995 + 0.2102862000 + 0.2122135013 + 0.2141225934 + 0.2159958929 + 0.2178680003 + 0.2197511047 + 0.2216199934 + 0.2234732956 + 0.2253248990 + 0.2271879017 + 0.2290568948 + 0.2309205979 + 0.2327875942 + 0.2346781045 + 0.2365760058 + 0.2384558022 + 0.2403216064 + 0.2421821058 + 0.2440275997 + 0.2458492070 + 0.2476378977 + 0.2494101971 + 0.2511883080 + 0.2529624104 + 0.2547251880 + 0.2564688921 + 0.2581931055 + 0.2599115074 + 0.2616336942 + 0.2633337080 + 0.2650091052 + 0.2666670084 + 0.2683193982 + 0.2699855864 + 0.2716380954 + 0.2732735872 + 0.2748895884 + 0.2764844894 + 0.2780745029 + 0.2796624005 + 0.2812372148 + 0.2827902138 + 0.2843335867 + 0.2858856916 + 0.2874445021 + 0.2890088856 + 0.2905732989 + 0.2921339869 + 0.2936922908 + 0.2952659130 + 0.2968513072 + 0.2984412909 + 0.3000228107 + 0.3015750945 + 0.3031128943 + 0.3046396971 + 0.3061710894 + 0.3077040017 + 0.3092370927 + 0.3107506931 + 0.3122516870 + 0.3137294054 + 0.3152104020 + 0.3166939020 + 0.3181836009 + 0.3196648061 + 0.3211343884 + 0.3225882053 + 0.3240387142 + 0.3254922032 + 0.3269436955 + 0.3283936083 + 0.3298347890 + 0.3312718868 + 0.3326953948 + 0.3341206014 + 0.3355478048 + 0.3369952142 + 0.3384498060 + 0.3399038017 + 0.3413537145 + 0.3427903950 + 0.3442232013 + 0.3456513882 + 0.3470813930 + 0.3485122919 + 0.3499529958 + 0.3513954878 + 0.3528392911 + 0.3542819917 + 0.3557189107 + 0.3571557105 + 0.3585920930 + 0.3600325882 + 0.3614800870 + 0.3629246950 + 0.3643657863 + 0.3657987118 + 0.3672237992 + 0.3686499894 + 0.3700770140 + 0.3714995086 + 0.3729186952 + 0.3743284941 + 0.3757312894 + 0.3771198094 + 0.3784962893 + 0.3798680007 + 0.3812347949 + 0.3826048970 + 0.3839792907 + 0.3853527904 + 0.3867248893 + 0.3880946934 + 0.3894568980 + 0.3908174038 + 0.3921599090 + 0.3935025036 + 0.3948425949 + 0.3961825073 + 0.3975262940 + 0.3988716006 + 0.4002110064 + 0.4015452862 + 0.4028792977 + 0.4042125046 + 0.4055452049 + 0.4068675935 + 0.4081901014 + 0.4095175862 + 0.4108465910 + 0.4121741951 + 0.4135001898 + 0.4148246050 + 0.4161387980 + 0.4174529910 + 0.4187706113 + 0.4200890958 + 0.4214057028 + 0.4227195084 + 0.4240333140 + 0.4253349006 + 0.4266364872 + 0.4279260933 + 0.4292064011 + 0.4304861128 + 0.4317606986 + 0.4330351949 + 0.4343157113 + 0.4355998933 + 0.4368841946 + 0.4381690919 + 0.4394541085 + 0.4407306015 + 0.4420005977 + 0.4432705939 + 0.4445107877 + 0.4457508922 + 0.4469895065 + 0.4482258856 + 0.4494622052 + 0.4506838918 + 0.4519014955 + 0.4531166852 + 0.4543156028 + 0.4555146098 + 0.4567095041 + 0.4578996897 + 0.4590897858 + 0.4602785110 + 0.4614667892 + 0.4626550078 + 0.4638502896 + 0.4650456905 + 0.4662390947 + 0.4674263895 + 0.4686135948 + 0.4697979987 + 0.4709784091 + 0.4721589088 + 0.4733403027 + 0.4745222926 + 0.4757043123 + 0.4768933952 + 0.4780850112 + 0.4792765081 + 0.4804633856 + 0.4816494882 + 0.4828355014 + 0.4839955866 + 0.4851537943 + 0.4863120019 + 0.4874627888 + 0.4886133969 + 0.4897640944 + 0.4909220040 + 0.4920799136 + 0.4932379127 + 0.4943912029 + 0.4955444932 + 0.4966978133 + 0.4978221059 + 0.4989440143 + 0.5000659823 + 0.5011757016 + 0.5022830963 + 0.5033904910 + 0.5044888854 + 0.5055838823 + 0.5066789985 + 0.5077672005 + 0.5088503957 + 0.5099335909 + 0.5110126734 + 0.5120856166 + 0.5131585002 + 0.5142297745 + 0.5152938962 + 0.5163578987 + 0.5174219012 + 0.5184922814 + 0.5195631981 + 0.5206341147 + 0.5217061043 + 0.5227789283 + 0.5238515735 + 0.5249236822 + 0.5259944201 + 0.5270652175 + 0.5281358957 + 0.5291953087 + 0.5302540064 + 0.5313128233 + 0.5323665738 + 0.5334162712 + 0.5344660282 + 0.5355166197 + 0.5365738273 + 0.5376309752 + 0.5386883020 + 0.5397393703 + 0.5407869816 + 0.5418345928 + 0.5428798199 + 0.5439077020 + 0.5449357033 + 0.5459637046 + 0.5469853878 + 0.5480024219 + 0.5490195155 + 0.5500364900 + 0.5510535836 + 0.5520707965 + 0.5530878901 + 0.5541033745 + 0.5551161170 + 0.5561287999 + 0.5571414828 + 0.5581471920 + 0.5591498017 + 0.5601524115 + 0.5611549020 + 0.5621367097 + 0.5631183982 + 0.5641000271 + 0.5650776029 + 0.5660427809 + 0.5670080781 + 0.5679733753 + 0.5689417124 + 0.5699135065 + 0.5708853006 + 0.5718570948 + 0.5728263855 + 0.5737943053 + 0.5747622252 + 0.5757302046 + 0.5766975880 + 0.5776649117 + 0.5786321759 + 0.5795994997 + 0.5805698037 + 0.5815405250 + 0.5825111270 + 0.5834817886 + 0.5844538808 + 0.5854260921 + 0.5863983035 + 0.5873705745 + 0.5883414149 + 0.5893121958 + 0.5902829766 + 0.5912536979 + 0.5922099948 + 0.5931643248 + 0.5941184759 + 0.5950728059 + 0.5960159898 + 0.5969563723 + 0.5978966951 + 0.5988370776 + 0.5997706056 + 0.6007006764 + 0.6016308069 + 0.6025608182 + 0.6034846902 + 0.6044021845 + 0.6053197980 + 0.6062374115 + 0.6071522832 + 0.6080605984 + 0.6089687943 + 0.6098771095 + 0.6107851863 + 0.6116871238 + 0.6125890017 + 0.6134908795 + 0.6143928170 + 0.6152886152 + 0.6161820889 + 0.6170755029 + 0.6179689765 + 0.6188614964 + 0.6197524071 + 0.6206433773 + 0.6215342879 + 0.6224251986 + 0.6233009100 + 0.6241760254 + 0.6250510812 + 0.6259261966 + 0.6267927885 + 0.6276513934 + 0.6285099983 + 0.6293684840 + 0.6302270889 + 0.6310849786 + 0.6319429278 + 0.6328008175 + 0.6336587071 + 0.6345123053 + 0.6353611946 + 0.6362100244 + 0.6370587945 + 0.6379076838 + 0.6387491822 + 0.6395896077 + 0.6404299736 + 0.6412703991 + 0.6421101093 + 0.6429476142 + 0.6437851191 + 0.6446226835 + 0.6454601884 + 0.6463015079 + 0.6471462250 + 0.6479908824 + 0.6488355994 + 0.6496803164 + 0.6505240798 + 0.6513677835 + 0.6522114873 + 0.6530550718 + 0.6538987756 + 0.6547306180 + 0.6555622816 + 0.6563938856 + 0.6572256088 + 0.6580554843 + 0.6588743925 + 0.6596933007 + 0.6605123281 + 0.6613311768 + 0.6621484160 + 0.6629601717 + 0.6637719870 + 0.6645838022 + 0.6653956175 + 0.6662080288 + 0.6670222878 + 0.6678364873 + 0.6686508060 + 0.6694651246 + 0.6702783108 + 0.6710888147 + 0.6718993187 + 0.6727098227 + 0.6735203266 + 0.6743295789 + 0.6751338840 + 0.6759383082 + 0.6767426133 + 0.6775469780 + 0.6783509851 + 0.6791514158 + 0.6799517870 + 0.6807522774 + 0.6815527081 + 0.6823530793 + 0.6831542850 + 0.6839554906 + 0.6847568154 + 0.6855580211 + 0.6863592863 + 0.6871563792 + 0.6879516244 + 0.6887468100 + 0.6895419955 + 0.6903371215 + 0.6911249757 + 0.6919013262 + 0.6926776171 + 0.6934539080 + 0.6942301989 + 0.6950061917 + 0.6957769990 + 0.6965478063 + 0.6973184943 + 0.6980893016 + 0.6988599896 + 0.6996240020 + 0.7003840208 + 0.7011440992 + 0.7019041181 + 0.7026641965 + 0.7034214735 + 0.7041661143 + 0.7049108148 + 0.7056553960 + 0.7063999772 + 0.7071446776 + 0.7078785896 + 0.7086068988 + 0.7093350887 + 0.7100633979 + 0.7107917070 + 0.7115197182 + 0.7122437954 + 0.7129678726 + 0.7136920094 + 0.7144160867 + 0.7151402235 + 0.7158653140 + 0.7165917754 + 0.7173182964 + 0.7180448174 + 0.7187712789 + 0.7194977999 + 0.7202196121 + 0.7209396958 + 0.7216597199 + 0.7223798037 + 0.7230998874 + 0.7238199711 + 0.7245283127 + 0.7252361774 + 0.7259441018 + 0.7266520858 + 0.7273600101 + 0.7280663252 + 0.7287629247 + 0.7294594049 + 0.7301560044 + 0.7308526039 + 0.7315490842 + 0.7322456241 + 0.7329418063 + 0.7336379886 + 0.7343341708 + 0.7350304127 + 0.7357265949 + 0.7364248037 + 0.7371274829 + 0.7378302813 + 0.7385330200 + 0.7392356992 + 0.7399383783 + 0.7406404018 + 0.7413403988 + 0.7420405149 + 0.7427405119 + 0.7434405088 + 0.7441406250 + 0.7448390722 + 0.7455317974 + 0.7462245226 + 0.7469171882 + 0.7476099133 + 0.7483025789 + 0.7489954233 + 0.7496907115 + 0.7503858805 + 0.7510811090 + 0.7517763972 + 0.7524716258 + 0.7531669140 + 0.7538704872 + 0.7545759082 + 0.7552812099 + 0.7559865713 + 0.7566918731 + 0.7573972940 + 0.7581034899 + 0.7588102818 + 0.7595170736 + 0.7602239251 + 0.7609307170 + 0.7616376281 + 0.7623426914 + 0.7630413175 + 0.7637398839 + 0.7644385099 + 0.7651370764 + 0.7658355832 + 0.7665342093 + 0.7672234178 + 0.7679098845 + 0.7685962915 + 0.7692828178 + 0.7699692845 + 0.7706558108 + 0.7713416219 + 0.7720258832 + 0.7727100849 + 0.7733944058 + 0.7740787268 + 0.7747629285 + 0.7754471898 + 0.7761262059 + 0.7768033743 + 0.7774804831 + 0.7781575918 + 0.7788347006 + 0.7795119286 + 0.7801874876 + 0.7808486819 + 0.7815099955 + 0.7821711898 + 0.7828325033 + 0.7834938169 + 0.7841550112 + 0.7848129869 + 0.7854657769 + 0.7861186266 + 0.7867714167 + 0.7874242067 + 0.7880769968 + 0.7887297869 + 0.7893806100 + 0.7900301814 + 0.7906798124 + 0.7913293839 + 0.7919788957 + 0.7926285267 + 0.7932780981 + 0.7939221859 + 0.7945647836 + 0.7952073216 + 0.7958499193 + 0.7964923978 + 0.7971348763 + 0.7977774739 + 0.7984188199 + 0.7990599275 + 0.7997009754 + 0.8003420234 + 0.8009830713 + 0.8016241789 + 0.8022652864 + 0.8028982282 + 0.8035295010 + 0.8041607738 + 0.8047922254 + 0.8054234982 + 0.8060547709 + 0.8066862226 + 0.8073112965 + 0.8079345822 + 0.8085579276 + 0.8091812134 + 0.8098044991 + 0.8104277253 + 0.8110510111 + 0.8116706014 + 0.8122878075 + 0.8129050136 + 0.8135222197 + 0.8141394854 + 0.8147566915 + 0.8153738976 + 0.8159893751 + 0.8166022897 + 0.8172150850 + 0.8178278804 + 0.8184407949 + 0.8190535903 + 0.8196663857 + 0.8202791214 + 0.8208892941 + 0.8214995861 + 0.8221098781 + 0.8227202296 + 0.8233304024 + 0.8239406943 + 0.8245509863 + 0.8251606226 + 0.8257699013 + 0.8263791800 + 0.8269885182 + 0.8275977969 + 0.8282071948 + 0.8288164735 + 0.8294249773 + 0.8300309777 + 0.8306369781 + 0.8312429786 + 0.8318489790 + 0.8324549794 + 0.8330609798 + 0.8336669207 + 0.8342692852 + 0.8348699808 + 0.8354706764 + 0.8360713720 + 0.8366721272 + 0.8372727036 + 0.8378733993 + 0.8384736776 + 0.8390662074 + 0.8396586180 + 0.8402510881 + 0.8408436179 + 0.8414360881 + 0.8420284986 + 0.8426210284 + 0.8432112932 + 0.8437972069 + 0.8443831801 + 0.8449690938 + 0.8455551267 + 0.8461410999 + 0.8467270136 + 0.8473129869 + 0.8479040265 + 0.8484994769 + 0.8490949273 + 0.8496903181 + 0.8502858281 + 0.8508812189 + 0.8514766097 + 0.8520721197 + 0.8526653051 + 0.8532572985 + 0.8538494110 + 0.8544414043 + 0.8550335169 + 0.8556255102 + 0.8562176228 + 0.8568096161 + 0.8573905230 + 0.8579664230 + 0.8585423827 + 0.8591184020 + 0.8596943021 + 0.8602703214 + 0.8608462811 + 0.8614221811 + 0.8619980216 + 0.8625736833 + 0.8631494045 + 0.8637250066 + 0.8643007278 + 0.8648763895 + 0.8654521108 + 0.8660277128 + 0.8666040897 + 0.8671810031 + 0.8677579761 + 0.8683350086 + 0.8689119220 + 0.8694888949 + 0.8700659275 + 0.8706427813 + 0.8712193966 + 0.8717948198 + 0.8723701835 + 0.8729457259 + 0.8735210896 + 0.8740965128 + 0.8746719956 + 0.8752474189 + 0.8758227825 + 0.8763949871 + 0.8769670129 + 0.8775389791 + 0.8781110048 + 0.8786829710 + 0.8792549968 + 0.8798270226 + 0.8803989887 + 0.8809679747 + 0.8815342784 + 0.8821007013 + 0.8826671243 + 0.8832334876 + 0.8837999105 + 0.8843662143 + 0.8849325776 + 0.8854988217 + 0.8860610127 + 0.8866233230 + 0.8871855140 + 0.8877478242 + 0.8883100748 + 0.8888723254 + 0.8894345760 + 0.8899968266 + 0.8905580044 + 0.8911178112 + 0.8916776180 + 0.8922374249 + 0.8927971721 + 0.8933569789 + 0.8939167857 + 0.8944765925 + 0.8950363994 + 0.8955954909 + 0.8961544037 + 0.8967131972 + 0.8972721100 + 0.8978309035 + 0.8983898163 + 0.8989487290 + 0.8995075226 + 0.9000663757 + 0.9006261230 + 0.9011856914 + 0.9017453790 + 0.9023050070 + 0.9028646946 + 0.9034243226 + 0.9039840102 + 0.9045435786 + 0.9051036239 + 0.9056655169 + 0.9062274098 + 0.9067893028 + 0.9073511958 + 0.9079130888 + 0.9084749818 + 0.9090368748 + 0.9095988274 + 0.9101592898 + 0.9107123017 + 0.9112653136 + 0.9118183255 + 0.9123712182 + 0.9129241705 + 0.9134771824 + 0.9140301943 + 0.9145832062 + 0.9151350856 + 0.9156765938 + 0.9162181020 + 0.9167596102 + 0.9173009992 + 0.9178425074 + 0.9183840156 + 0.9189255238 + 0.9194669724 + 0.9200084805 + 0.9205374122 + 0.9210653901 + 0.9215933084 + 0.9221212864 + 0.9226492047 + 0.9231771827 + 0.9237051010 + 0.9242330790 + 0.9247609973 + 0.9252843857 + 0.9258053899 + 0.9263263941 + 0.9268475175 + 0.9273685217 + 0.9278895855 + 0.9284105897 + 0.9289315939 + 0.9294527173 + 0.9299737811 + 0.9304950833 + 0.9310163260 + 0.9315376282 + 0.9320588708 + 0.9325801730 + 0.9331014752 + 0.9336227179 + 0.9341440201 + 0.9346653223 + 0.9351785183 + 0.9356899858 + 0.9362015128 + 0.9367129803 + 0.9372245073 + 0.9377359748 + 0.9382475019 + 0.9387590289 + 0.9392704964 + 0.9397779703 + 0.9402741790 + 0.9407703876 + 0.9412665963 + 0.9417628050 + 0.9422590733 + 0.9427552819 + 0.9432514906 + 0.9437476993 + 0.9442439079 + 0.9447419047 + 0.9452412128 + 0.9457404017 + 0.9462395906 + 0.9467388988 + 0.9472380877 + 0.9477373958 + 0.9482365847 + 0.9487358928 + 0.9492350817 + 0.9497361183 + 0.9502375722 + 0.9507390261 + 0.9512404203 + 0.9517418742 + 0.9522433281 + 0.9527447820 + 0.9532461762 + 0.9537475705 + 0.9542490840 + 0.9547458887 + 0.9552422762 + 0.9557387233 + 0.9562351704 + 0.9567316175 + 0.9572281241 + 0.9577245116 + 0.9582208991 + 0.9587174058 + 0.9592137933 + 0.9596955180 + 0.9601761103 + 0.9606567025 + 0.9611372948 + 0.9616180062 + 0.9620985985 + 0.9625791907 + 0.9630599022 + 0.9635404944 + 0.9640210867 + 0.9644860029 + 0.9649478793 + 0.9654096961 + 0.9658715725 + 0.9663333893 + 0.9667953253 + 0.9672570825 + 0.9677190185 + 0.9681807756 + 0.9686427116 + 0.9691007733 + 0.9695566893 + 0.9700127244 + 0.9704685807 + 0.9709244967 + 0.9713804722 + 0.9718363881 + 0.9722923040 + 0.9727482200 + 0.9732041955 + 0.9736583829 + 0.9741089940 + 0.9745596051 + 0.9750102162 + 0.9754608274 + 0.9759113789 + 0.9763619900 + 0.9768124819 + 0.9772630930 + 0.9777137041 + 0.9781643152 + 0.9786108732 + 0.9790568948 + 0.9795029163 + 0.9799489975 + 0.9803950191 + 0.9808409810 + 0.9812871218 + 0.9817330837 + 0.9821791053 + 0.9826251268 + 0.9830691218 + 0.9835088253 + 0.9839485288 + 0.9843881726 + 0.9848278761 + 0.9852675796 + 0.9857072830 + 0.9861469865 + 0.9865866899 + 0.9870265126 + 0.9874662161 + 0.9878935218 + 0.9883149266 + 0.9887362719 + 0.9891576767 + 0.9895790815 + 0.9900006056 + 0.9904220104 + 0.9908434153 + 0.9912648201 + 0.9916862249 + 0.9921076298 + 0.9924955964 + 0.9928798079 + 0.9932640791 + 0.9936484098 + 0.9940326810 + 0.9944170117 + 0.9948012233 + 0.9951854944 + 0.9955698252 + 0.9959540963 + 0.9963378906 + 0.9966707826 + 0.9970036745 + 0.9973366857 + 0.9976695776 + 0.9980025291 + 0.9983354211 + 0.9986683130 + 0.9990012050 + 0.9993342161 + 0.9996671081 + 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/eastman_double_x_neg_6min.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/eastman_double_x_neg_6min.spi1d index 1eb80666..93ef9ebd 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/eastman_double_x_neg_6min.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/eastman_double_x_neg_6min.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 1 -{ - 0.0000000000 - 0.0022076759 - 0.0048505990 - 0.0077307681 - 0.0106430203 - 0.0134941395 - 0.0164271407 - 0.0193642899 - 0.0222814996 - 0.0253338106 - 0.0283298604 - 0.0312295891 - 0.0340677910 - 0.0368127897 - 0.0395192690 - 0.0422245711 - 0.0448633991 - 0.0473990701 - 0.0498608798 - 0.0522968508 - 0.0547638610 - 0.0572266914 - 0.0596168712 - 0.0619427115 - 0.0641859174 - 0.0663840473 - 0.0685592070 - 0.0707125664 - 0.0728932172 - 0.0750729963 - 0.0772423595 - 0.0793742388 - 0.0814804435 - 0.0835459232 - 0.0855793729 - 0.0876187608 - 0.0896756127 - 0.0917201489 - 0.0937488973 - 0.0957670510 - 0.0977691785 - 0.0997611210 - 0.1017474979 - 0.1037304029 - 0.1057140008 - 0.1076987982 - 0.1096796989 - 0.1116433963 - 0.1136050969 - 0.1155518964 - 0.1174954027 - 0.1194273010 - 0.1213321984 - 0.1232244000 - 0.1250977069 - 0.1269564033 - 0.1288059056 - 0.1306264997 - 0.1324591935 - 0.1342981011 - 0.1361421049 - 0.1379898936 - 0.1398337930 - 0.1416724026 - 0.1435085982 - 0.1453595012 - 0.1472201943 - 0.1490903944 - 0.1509464979 - 0.1527885944 - 0.1546380967 - 0.1564874053 - 0.1583337933 - 0.1601832062 - 0.1620080024 - 0.1638301015 - 0.1656454057 - 0.1674109995 - 0.1691544950 - 0.1708903015 - 0.1726087034 - 0.1743192971 - 0.1760134995 - 0.1776891053 - 0.1793549061 - 0.1810144931 - 0.1826763004 - 0.1843471974 - 0.1860086024 - 0.1876676977 - 0.1893319935 - 0.1910057068 - 0.1926884055 - 0.1943672001 - 0.1960387975 - 0.1977010965 - 0.1993733943 - 0.2010557055 - 0.2027229965 - 0.2043714970 - 0.2060067058 - 0.2076272964 - 0.2092293054 - 0.2108173966 - 0.2123903036 - 0.2139514983 - 0.2155003995 - 0.2170374990 - 0.2185630947 - 0.2200783044 - 0.2215853930 - 0.2230909020 - 0.2245920002 - 0.2260849029 - 0.2275708020 - 0.2290592045 - 0.2305610031 - 0.2320587039 - 0.2335456014 - 0.2350171953 - 0.2364920974 - 0.2379685044 - 0.2394436002 - 0.2409150004 - 0.2423750013 - 0.2438220978 - 0.2452608943 - 0.2466982007 - 0.2481335998 - 0.2495692968 - 0.2510060966 - 0.2524541020 - 0.2539057136 - 0.2553601861 - 0.2568137050 - 0.2582660913 - 0.2597176135 - 0.2611657083 - 0.2626079023 - 0.2640449107 - 0.2654654086 - 0.2668786049 - 0.2682839036 - 0.2696810961 - 0.2710821927 - 0.2724924982 - 0.2739025056 - 0.2753120065 - 0.2767190933 - 0.2781172097 - 0.2795062065 - 0.2808851004 - 0.2822608054 - 0.2836303115 - 0.2849979103 - 0.2863635123 - 0.2877384126 - 0.2891165912 - 0.2905051112 - 0.2918922901 - 0.2932763994 - 0.2946614027 - 0.2960470021 - 0.2974393070 - 0.2988334000 - 0.3002207875 - 0.3016053140 - 0.3029761910 - 0.3043459952 - 0.3057133853 - 0.3070785999 - 0.3084413111 - 0.3098073900 - 0.3111760020 - 0.3125430048 - 0.3139094114 - 0.3152734935 - 0.3166370094 - 0.3179858923 - 0.3193317950 - 0.3206684887 - 0.3220038116 - 0.3233413100 - 0.3246791065 - 0.3260106146 - 0.3273411989 - 0.3286638856 - 0.3299851120 - 0.3313086033 - 0.3326326013 - 0.3339475095 - 0.3352591991 - 0.3365674019 - 0.3378736973 - 0.3391835093 - 0.3404963911 - 0.3418085873 - 0.3431195915 - 0.3444280922 - 0.3457283974 - 0.3470287025 - 0.3483307958 - 0.3496328890 - 0.3509300053 - 0.3522259891 - 0.3535211980 - 0.3548158109 - 0.3561145067 - 0.3574211001 - 0.3587279022 - 0.3600377142 - 0.3613474965 - 0.3626418114 - 0.3639313877 - 0.3652200997 - 0.3665077984 - 0.3677951992 - 0.3690789044 - 0.3703626096 - 0.3716369867 - 0.3729074895 - 0.3741776943 - 0.3754476011 - 0.3767175078 - 0.3779971004 - 0.3792783916 - 0.3805550933 - 0.3818249106 - 0.3830947876 - 0.3843517900 - 0.3856075108 - 0.3868649006 - 0.3881247044 - 0.3893845975 - 0.3906409144 - 0.3918966949 - 0.3931471109 - 0.3943845928 - 0.3956221938 - 0.3968513906 - 0.3980770111 - 0.3993020058 - 0.4005202949 - 0.4017386138 - 0.4029510021 - 0.4041553140 - 0.4053595066 - 0.4065566957 - 0.4077515006 - 0.4089460075 - 0.4101307988 - 0.4113157094 - 0.4124985933 - 0.4136764109 - 0.4148542881 - 0.4160273969 - 0.4171960950 - 0.4183647931 - 0.4195275903 - 0.4206880033 - 0.4218483865 - 0.4230014086 - 0.4241535068 - 0.4253054857 - 0.4264521897 - 0.4275988936 - 0.4287458956 - 0.4298943877 - 0.4310429096 - 0.4321908951 - 0.4333373010 - 0.4344835877 - 0.4356265068 - 0.4367592037 - 0.4378919899 - 0.4390229881 - 0.4401493967 - 0.4412758946 - 0.4424026012 - 0.4435302019 - 0.4446578026 - 0.4457832873 - 0.4469003975 - 0.4480175972 - 0.4491330087 - 0.4502356052 - 0.4513381124 - 0.4524405897 - 0.4535480142 - 0.4546554983 - 0.4557628930 - 0.4568733871 - 0.4579845965 - 0.4590956867 - 0.4602054060 - 0.4613144100 - 0.4624234140 - 0.4635319114 - 0.4646396935 - 0.4657475054 - 0.4668546915 - 0.4679591954 - 0.4690637887 - 0.4701682925 - 0.4712668061 - 0.4723646045 - 0.4734624028 - 0.4745511115 - 0.4756326973 - 0.4767143130 - 0.4777936935 - 0.4788640141 - 0.4799341857 - 0.4810045063 - 0.4820767939 - 0.4831495881 - 0.4842225015 - 0.4852963984 - 0.4863724113 - 0.4874483049 - 0.4885242879 - 0.4895890057 - 0.4906522930 - 0.4917156994 - 0.4927777052 - 0.4938377142 - 0.4948976934 - 0.4959576130 - 0.4970098138 - 0.4980604947 - 0.4991110861 - 0.5001580119 - 0.5011935830 - 0.5022290945 - 0.5032647252 - 0.5042961836 - 0.5053253174 - 0.5063543916 - 0.5073835254 - 0.5083957911 - 0.5094074011 - 0.5104188919 - 0.5114263892 - 0.5124220848 - 0.5134177208 - 0.5144132972 - 0.5154060721 - 0.5163959265 - 0.5173857808 - 0.5183756948 - 0.5193517208 - 0.5203220844 - 0.5212926269 - 0.5222629905 - 0.5232200027 - 0.5241754055 - 0.5251306891 - 0.5260860920 - 0.5270460844 - 0.5280060768 - 0.5289661288 - 0.5299252272 - 0.5308769941 - 0.5318287015 - 0.5327805281 - 0.5337300897 - 0.5346677899 - 0.5356054902 - 0.5365431905 - 0.5374808908 - 0.5384188890 - 0.5393568873 - 0.5402948856 - 0.5412318110 - 0.5421609879 - 0.5430902243 - 0.5440192819 - 0.5449479818 - 0.5458660722 - 0.5467841029 - 0.5477020741 - 0.5486202240 - 0.5495285988 - 0.5504361987 - 0.5513436794 - 0.5522512794 - 0.5531492829 - 0.5540440083 - 0.5549386740 - 0.5558333993 - 0.5567305088 - 0.5576295853 - 0.5585287809 - 0.5594279766 - 0.5603266954 - 0.5612239838 - 0.5621213913 - 0.5630186796 - 0.5639160275 - 0.5648062229 - 0.5656958818 - 0.5665857196 - 0.5674753785 - 0.5683673024 - 0.5692604780 - 0.5701537132 - 0.5710470080 - 0.5719397068 - 0.5728303194 - 0.5737208724 - 0.5746114850 - 0.5755020976 - 0.5763865113 - 0.5772687793 - 0.5781511068 - 0.5790333748 - 0.5799121857 - 0.5807796121 - 0.5816469789 - 0.5825144053 - 0.5833817720 - 0.5842388868 - 0.5850911736 - 0.5859435201 - 0.5867958069 - 0.5876480937 - 0.5885003805 - 0.5893527269 - 0.5902050734 - 0.5910574198 - 0.5919092298 - 0.5927603841 - 0.5936114788 - 0.5944626927 - 0.5953139067 - 0.5961607099 - 0.5970060825 - 0.5978515148 - 0.5986968279 - 0.5995422006 - 0.6003857255 - 0.6012291908 - 0.6020725965 - 0.6029161215 - 0.6037604809 - 0.6046084166 - 0.6054564118 - 0.6063044071 - 0.6071524024 - 0.6080002189 - 0.6088476777 - 0.6096950769 - 0.6105425954 - 0.6113901138 - 0.6122344732 - 0.6130748987 - 0.6139153838 - 0.6147558093 - 0.6155961752 - 0.6164314151 - 0.6172606945 - 0.6180900931 - 0.6189194918 - 0.6197488904 - 0.6205750704 - 0.6213976741 - 0.6222202778 - 0.6230428815 - 0.6238654852 - 0.6246876121 - 0.6255089045 - 0.6263303161 - 0.6271517277 - 0.6279730201 - 0.6287912726 - 0.6296015978 - 0.6304119825 - 0.6312224269 - 0.6320326924 - 0.6328420043 - 0.6336416006 - 0.6344411969 - 0.6352407932 - 0.6360403895 - 0.6368399858 - 0.6376360059 - 0.6384313703 - 0.6392267942 - 0.6400222182 - 0.6408177018 - 0.6416134238 - 0.6424095035 - 0.6432055831 - 0.6440016031 - 0.6447976828 - 0.6455929279 - 0.6463851929 - 0.6471775770 - 0.6479700208 - 0.6487624049 - 0.6495547891 - 0.6503387094 - 0.6511209011 - 0.6519030929 - 0.6526852250 - 0.6534674168 - 0.6542478204 - 0.6550251245 - 0.6558023095 - 0.6565796137 - 0.6573567986 - 0.6581341028 - 0.6589120030 - 0.6596900225 - 0.6604681015 - 0.6612461209 - 0.6620242000 - 0.6628038287 - 0.6635876894 - 0.6643714905 - 0.6651554108 - 0.6659392715 - 0.6667230725 - 0.6675077081 - 0.6682925820 - 0.6690775156 - 0.6698623896 - 0.6706473827 - 0.6714323163 - 0.6722124815 - 0.6729924083 - 0.6737722158 - 0.6745520830 - 0.6753318906 - 0.6761127114 - 0.6768987179 - 0.6776847839 - 0.6784707904 - 0.6792567968 - 0.6800428033 - 0.6808322072 - 0.6816287041 - 0.6824250817 - 0.6832215786 - 0.6840180159 - 0.6848145127 - 0.6856052279 - 0.6863880754 - 0.6871711016 - 0.6879541278 - 0.6887369752 - 0.6895200014 - 0.6902992129 - 0.6910743713 - 0.6918495297 - 0.6926246881 - 0.6933997869 - 0.6941748857 - 0.6949504018 - 0.6957262158 - 0.6965019703 - 0.6972777843 - 0.6980537176 - 0.6988294721 - 0.6996037960 - 0.7003753781 - 0.7011470795 - 0.7019187212 - 0.7026903033 - 0.7034620047 - 0.7042315006 - 0.7049934268 - 0.7057551742 - 0.7065169811 - 0.7072789073 - 0.7080407143 - 0.7088025808 - 0.7095500827 - 0.7102975249 - 0.7110450268 - 0.7117924094 - 0.7125399113 - 0.7132872939 - 0.7140312195 - 0.7147734761 - 0.7155159116 - 0.7162582874 - 0.7170007229 - 0.7177429795 - 0.7184851170 - 0.7192267179 - 0.7199681997 - 0.7207098007 - 0.7214512825 - 0.7221928835 - 0.7229344249 - 0.7236632705 - 0.7243912816 - 0.7251192927 - 0.7258473039 - 0.7265753150 - 0.7273033261 - 0.7280253768 - 0.7287402749 - 0.7294551134 - 0.7301700115 - 0.7308847904 - 0.7315996289 - 0.7323145270 - 0.7330235839 - 0.7337319255 - 0.7344402075 - 0.7351484895 - 0.7358567715 - 0.7365651131 - 0.7372738123 - 0.7379835248 - 0.7386932969 - 0.7394030094 - 0.7401127219 - 0.7408224940 - 0.7415322065 - 0.7422450781 - 0.7429605126 - 0.7436760068 - 0.7443913817 - 0.7451068759 - 0.7458223104 - 0.7465378046 - 0.7472481728 - 0.7479571104 - 0.7486659884 - 0.7493749261 - 0.7500838041 - 0.7507926822 - 0.7515016198 - 0.7522053123 - 0.7529084086 - 0.7536116242 - 0.7543147206 - 0.7550178766 - 0.7557209730 - 0.7564241886 - 0.7571367025 - 0.7578495145 - 0.7585622072 - 0.7592749000 - 0.7599875927 - 0.7607002854 - 0.7614130974 - 0.7621260881 - 0.7628390789 - 0.7635521293 - 0.7642651200 - 0.7649781108 - 0.7656911016 - 0.7664040923 - 0.7671093941 - 0.7678139210 - 0.7685185075 - 0.7692229748 - 0.7699275017 - 0.7706320286 - 0.7713364959 - 0.7720378041 - 0.7727380991 - 0.7734382749 - 0.7741385102 - 0.7748388052 - 0.7755389810 - 0.7762392163 - 0.7769402266 - 0.7776417136 - 0.7783433199 - 0.7790448070 - 0.7797464132 - 0.7804480195 - 0.7811495066 - 0.7818514109 - 0.7825546861 - 0.7832580209 - 0.7839611769 - 0.7846645117 - 0.7853677869 - 0.7860711217 - 0.7867742777 - 0.7874720097 - 0.7881684899 - 0.7888649702 - 0.7895615101 - 0.7902579904 - 0.7909544706 - 0.7916510105 - 0.7923467755 - 0.7930412889 - 0.7937358022 - 0.7944303155 - 0.7951248288 - 0.7958192229 - 0.7965136766 - 0.7972081900 - 0.7979041934 - 0.7986006141 - 0.7992969751 - 0.7999933958 - 0.8006898165 - 0.8013861775 - 0.8020825982 - 0.8027787209 - 0.8034735918 - 0.8041685224 - 0.8048635125 - 0.8055583835 - 0.8062533140 - 0.8069481850 - 0.8076431155 - 0.8083345890 - 0.8090218902 - 0.8097093105 - 0.8103966117 - 0.8110839128 - 0.8117712736 - 0.8124585748 - 0.8131459951 - 0.8138281703 - 0.8145077229 - 0.8151872754 - 0.8158668280 - 0.8165463209 - 0.8172258139 - 0.8179054260 - 0.8185849190 - 0.8192651868 - 0.8199456930 - 0.8206261992 - 0.8213067055 - 0.8219872117 - 0.8226677179 - 0.8233482242 - 0.8240286708 - 0.8247126937 - 0.8253976107 - 0.8260825872 - 0.8267675042 - 0.8274524808 - 0.8281373978 - 0.8288223147 - 0.8295072913 - 0.8301901221 - 0.8308722973 - 0.8315544724 - 0.8322365880 - 0.8329188228 - 0.8336008787 - 0.8342831135 - 0.8349652290 - 0.8356407285 - 0.8363116980 - 0.8369827867 - 0.8376538157 - 0.8383247852 - 0.8389958143 - 0.8396669030 - 0.8403378725 - 0.8410078287 - 0.8416758180 - 0.8423439264 - 0.8430119157 - 0.8436799049 - 0.8443478942 - 0.8450158834 - 0.8456839919 - 0.8463522196 - 0.8470252752 - 0.8476983905 - 0.8483713865 - 0.8490445018 - 0.8497176170 - 0.8503906727 - 0.8510637283 - 0.8517367840 - 0.8524100780 - 0.8530836105 - 0.8537570834 - 0.8544304967 - 0.8551040292 - 0.8557773829 - 0.8564509153 - 0.8571243882 - 0.8577973843 - 0.8584672809 - 0.8591372967 - 0.8598073125 - 0.8604773283 - 0.8611472249 - 0.8618171811 - 0.8624871969 - 0.8631570935 - 0.8638226986 - 0.8644846082 - 0.8651465774 - 0.8658084869 - 0.8664703965 - 0.8671323061 - 0.8677942157 - 0.8684561253 - 0.8691179752 - 0.8697800040 - 0.8704419136 - 0.8711038828 - 0.8717659116 - 0.8724278212 - 0.8730897903 - 0.8737516999 - 0.8744137287 - 0.8750752807 - 0.8757330179 - 0.8763905764 - 0.8770483136 - 0.8777058721 - 0.8783634901 - 0.8790212274 - 0.8796787858 - 0.8803364038 - 0.8809922934 - 0.8816409111 - 0.8822894096 - 0.8829380274 - 0.8835865855 - 0.8842352033 - 0.8848837018 - 0.8855323195 - 0.8861808777 - 0.8868293166 - 0.8874773979 - 0.8881254196 - 0.8887733817 - 0.8894214034 - 0.8900694847 - 0.8907175064 - 0.8913655281 - 0.8920136094 - 0.8926612735 - 0.8933075070 - 0.8939536810 - 0.8945999742 - 0.8952462077 - 0.8958923817 - 0.8965386152 - 0.8971849084 - 0.8978310823 - 0.8984773159 - 0.8991330266 - 0.8997886777 - 0.9004443884 - 0.9011002183 - 0.9017559290 - 0.9024116993 - 0.9030674100 - 0.9037231803 - 0.9043788910 - 0.9050328135 - 0.9056859016 - 0.9063389897 - 0.9069920778 - 0.9076452851 - 0.9082983732 - 0.9089515209 - 0.9096046090 - 0.9102578163 - 0.9109084010 - 0.9115543962 - 0.9122003913 - 0.9128465056 - 0.9134925008 - 0.9141384959 - 0.9147844911 - 0.9154306054 - 0.9160766006 - 0.9167225957 - 0.9173694253 - 0.9180163145 - 0.9186632037 - 0.9193100929 - 0.9199569821 - 0.9206038713 - 0.9212508202 - 0.9218977094 - 0.9225445986 - 0.9231904149 - 0.9238339067 - 0.9244773984 - 0.9251207709 - 0.9257643223 - 0.9264076948 - 0.9270511866 - 0.9276946187 - 0.9283381104 - 0.9289816022 - 0.9296203852 - 0.9302564859 - 0.9308925867 - 0.9315286875 - 0.9321647882 - 0.9328008890 - 0.9334369898 - 0.9340732098 - 0.9347093105 - 0.9353454113 - 0.9359716177 - 0.9365960956 - 0.9372205138 - 0.9378449917 - 0.9384695292 - 0.9390940070 - 0.9397184849 - 0.9403430223 - 0.9409673810 - 0.9415919185 - 0.9422088861 - 0.9428256154 - 0.9434422851 - 0.9440590143 - 0.9446756840 - 0.9452924132 - 0.9459090829 - 0.9465258121 - 0.9471424818 - 0.9477592111 - 0.9483684897 - 0.9489774704 - 0.9495865703 - 0.9501956105 - 0.9508047104 - 0.9514136910 - 0.9520227909 - 0.9526317716 - 0.9532408714 - 0.9538499117 - 0.9544495940 - 0.9550477862 - 0.9556459785 - 0.9562441707 - 0.9568424225 - 0.9574406147 - 0.9580388069 - 0.9586369991 - 0.9592351913 - 0.9598333836 - 0.9604253769 - 0.9610139728 - 0.9616026878 - 0.9621914029 - 0.9627799988 - 0.9633687139 - 0.9639573097 - 0.9645460248 - 0.9651346803 - 0.9657232761 - 0.9663069248 - 0.9668803215 - 0.9674537182 - 0.9680271149 - 0.9686005116 - 0.9691739082 - 0.9697473049 - 0.9703207016 - 0.9708940983 - 0.9714674950 - 0.9720408916 - 0.9725995064 - 0.9731563926 - 0.9737132192 - 0.9742701054 - 0.9748269916 - 0.9753838181 - 0.9759407043 - 0.9764975905 - 0.9770544171 - 0.9776113033 - 0.9781597257 - 0.9786922932 - 0.9792249203 - 0.9797576070 - 0.9802901745 - 0.9808228016 - 0.9813554287 - 0.9818881154 - 0.9824206829 - 0.9829533100 - 0.9834858775 - 0.9840049744 - 0.9845178723 - 0.9850307703 - 0.9855437279 - 0.9860566854 - 0.9865695834 - 0.9870824814 - 0.9875953794 - 0.9881082773 - 0.9886211753 - 0.9891340733 - 0.9896370173 - 0.9901388884 - 0.9906408191 - 0.9911426902 - 0.9916446209 - 0.9921464920 - 0.9926484227 - 0.9931502938 - 0.9936522245 - 0.9941540956 - 0.9946557879 - 0.9951416850 - 0.9956275225 - 0.9961133003 - 0.9965991974 - 0.9970849752 - 0.9975708127 - 0.9980567098 - 0.9985424876 - 0.9990283251 - 0.9995142221 - 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 1 +{ + 0.0000000000 + 0.0022076759 + 0.0048505990 + 0.0077307681 + 0.0106430203 + 0.0134941395 + 0.0164271407 + 0.0193642899 + 0.0222814996 + 0.0253338106 + 0.0283298604 + 0.0312295891 + 0.0340677910 + 0.0368127897 + 0.0395192690 + 0.0422245711 + 0.0448633991 + 0.0473990701 + 0.0498608798 + 0.0522968508 + 0.0547638610 + 0.0572266914 + 0.0596168712 + 0.0619427115 + 0.0641859174 + 0.0663840473 + 0.0685592070 + 0.0707125664 + 0.0728932172 + 0.0750729963 + 0.0772423595 + 0.0793742388 + 0.0814804435 + 0.0835459232 + 0.0855793729 + 0.0876187608 + 0.0896756127 + 0.0917201489 + 0.0937488973 + 0.0957670510 + 0.0977691785 + 0.0997611210 + 0.1017474979 + 0.1037304029 + 0.1057140008 + 0.1076987982 + 0.1096796989 + 0.1116433963 + 0.1136050969 + 0.1155518964 + 0.1174954027 + 0.1194273010 + 0.1213321984 + 0.1232244000 + 0.1250977069 + 0.1269564033 + 0.1288059056 + 0.1306264997 + 0.1324591935 + 0.1342981011 + 0.1361421049 + 0.1379898936 + 0.1398337930 + 0.1416724026 + 0.1435085982 + 0.1453595012 + 0.1472201943 + 0.1490903944 + 0.1509464979 + 0.1527885944 + 0.1546380967 + 0.1564874053 + 0.1583337933 + 0.1601832062 + 0.1620080024 + 0.1638301015 + 0.1656454057 + 0.1674109995 + 0.1691544950 + 0.1708903015 + 0.1726087034 + 0.1743192971 + 0.1760134995 + 0.1776891053 + 0.1793549061 + 0.1810144931 + 0.1826763004 + 0.1843471974 + 0.1860086024 + 0.1876676977 + 0.1893319935 + 0.1910057068 + 0.1926884055 + 0.1943672001 + 0.1960387975 + 0.1977010965 + 0.1993733943 + 0.2010557055 + 0.2027229965 + 0.2043714970 + 0.2060067058 + 0.2076272964 + 0.2092293054 + 0.2108173966 + 0.2123903036 + 0.2139514983 + 0.2155003995 + 0.2170374990 + 0.2185630947 + 0.2200783044 + 0.2215853930 + 0.2230909020 + 0.2245920002 + 0.2260849029 + 0.2275708020 + 0.2290592045 + 0.2305610031 + 0.2320587039 + 0.2335456014 + 0.2350171953 + 0.2364920974 + 0.2379685044 + 0.2394436002 + 0.2409150004 + 0.2423750013 + 0.2438220978 + 0.2452608943 + 0.2466982007 + 0.2481335998 + 0.2495692968 + 0.2510060966 + 0.2524541020 + 0.2539057136 + 0.2553601861 + 0.2568137050 + 0.2582660913 + 0.2597176135 + 0.2611657083 + 0.2626079023 + 0.2640449107 + 0.2654654086 + 0.2668786049 + 0.2682839036 + 0.2696810961 + 0.2710821927 + 0.2724924982 + 0.2739025056 + 0.2753120065 + 0.2767190933 + 0.2781172097 + 0.2795062065 + 0.2808851004 + 0.2822608054 + 0.2836303115 + 0.2849979103 + 0.2863635123 + 0.2877384126 + 0.2891165912 + 0.2905051112 + 0.2918922901 + 0.2932763994 + 0.2946614027 + 0.2960470021 + 0.2974393070 + 0.2988334000 + 0.3002207875 + 0.3016053140 + 0.3029761910 + 0.3043459952 + 0.3057133853 + 0.3070785999 + 0.3084413111 + 0.3098073900 + 0.3111760020 + 0.3125430048 + 0.3139094114 + 0.3152734935 + 0.3166370094 + 0.3179858923 + 0.3193317950 + 0.3206684887 + 0.3220038116 + 0.3233413100 + 0.3246791065 + 0.3260106146 + 0.3273411989 + 0.3286638856 + 0.3299851120 + 0.3313086033 + 0.3326326013 + 0.3339475095 + 0.3352591991 + 0.3365674019 + 0.3378736973 + 0.3391835093 + 0.3404963911 + 0.3418085873 + 0.3431195915 + 0.3444280922 + 0.3457283974 + 0.3470287025 + 0.3483307958 + 0.3496328890 + 0.3509300053 + 0.3522259891 + 0.3535211980 + 0.3548158109 + 0.3561145067 + 0.3574211001 + 0.3587279022 + 0.3600377142 + 0.3613474965 + 0.3626418114 + 0.3639313877 + 0.3652200997 + 0.3665077984 + 0.3677951992 + 0.3690789044 + 0.3703626096 + 0.3716369867 + 0.3729074895 + 0.3741776943 + 0.3754476011 + 0.3767175078 + 0.3779971004 + 0.3792783916 + 0.3805550933 + 0.3818249106 + 0.3830947876 + 0.3843517900 + 0.3856075108 + 0.3868649006 + 0.3881247044 + 0.3893845975 + 0.3906409144 + 0.3918966949 + 0.3931471109 + 0.3943845928 + 0.3956221938 + 0.3968513906 + 0.3980770111 + 0.3993020058 + 0.4005202949 + 0.4017386138 + 0.4029510021 + 0.4041553140 + 0.4053595066 + 0.4065566957 + 0.4077515006 + 0.4089460075 + 0.4101307988 + 0.4113157094 + 0.4124985933 + 0.4136764109 + 0.4148542881 + 0.4160273969 + 0.4171960950 + 0.4183647931 + 0.4195275903 + 0.4206880033 + 0.4218483865 + 0.4230014086 + 0.4241535068 + 0.4253054857 + 0.4264521897 + 0.4275988936 + 0.4287458956 + 0.4298943877 + 0.4310429096 + 0.4321908951 + 0.4333373010 + 0.4344835877 + 0.4356265068 + 0.4367592037 + 0.4378919899 + 0.4390229881 + 0.4401493967 + 0.4412758946 + 0.4424026012 + 0.4435302019 + 0.4446578026 + 0.4457832873 + 0.4469003975 + 0.4480175972 + 0.4491330087 + 0.4502356052 + 0.4513381124 + 0.4524405897 + 0.4535480142 + 0.4546554983 + 0.4557628930 + 0.4568733871 + 0.4579845965 + 0.4590956867 + 0.4602054060 + 0.4613144100 + 0.4624234140 + 0.4635319114 + 0.4646396935 + 0.4657475054 + 0.4668546915 + 0.4679591954 + 0.4690637887 + 0.4701682925 + 0.4712668061 + 0.4723646045 + 0.4734624028 + 0.4745511115 + 0.4756326973 + 0.4767143130 + 0.4777936935 + 0.4788640141 + 0.4799341857 + 0.4810045063 + 0.4820767939 + 0.4831495881 + 0.4842225015 + 0.4852963984 + 0.4863724113 + 0.4874483049 + 0.4885242879 + 0.4895890057 + 0.4906522930 + 0.4917156994 + 0.4927777052 + 0.4938377142 + 0.4948976934 + 0.4959576130 + 0.4970098138 + 0.4980604947 + 0.4991110861 + 0.5001580119 + 0.5011935830 + 0.5022290945 + 0.5032647252 + 0.5042961836 + 0.5053253174 + 0.5063543916 + 0.5073835254 + 0.5083957911 + 0.5094074011 + 0.5104188919 + 0.5114263892 + 0.5124220848 + 0.5134177208 + 0.5144132972 + 0.5154060721 + 0.5163959265 + 0.5173857808 + 0.5183756948 + 0.5193517208 + 0.5203220844 + 0.5212926269 + 0.5222629905 + 0.5232200027 + 0.5241754055 + 0.5251306891 + 0.5260860920 + 0.5270460844 + 0.5280060768 + 0.5289661288 + 0.5299252272 + 0.5308769941 + 0.5318287015 + 0.5327805281 + 0.5337300897 + 0.5346677899 + 0.5356054902 + 0.5365431905 + 0.5374808908 + 0.5384188890 + 0.5393568873 + 0.5402948856 + 0.5412318110 + 0.5421609879 + 0.5430902243 + 0.5440192819 + 0.5449479818 + 0.5458660722 + 0.5467841029 + 0.5477020741 + 0.5486202240 + 0.5495285988 + 0.5504361987 + 0.5513436794 + 0.5522512794 + 0.5531492829 + 0.5540440083 + 0.5549386740 + 0.5558333993 + 0.5567305088 + 0.5576295853 + 0.5585287809 + 0.5594279766 + 0.5603266954 + 0.5612239838 + 0.5621213913 + 0.5630186796 + 0.5639160275 + 0.5648062229 + 0.5656958818 + 0.5665857196 + 0.5674753785 + 0.5683673024 + 0.5692604780 + 0.5701537132 + 0.5710470080 + 0.5719397068 + 0.5728303194 + 0.5737208724 + 0.5746114850 + 0.5755020976 + 0.5763865113 + 0.5772687793 + 0.5781511068 + 0.5790333748 + 0.5799121857 + 0.5807796121 + 0.5816469789 + 0.5825144053 + 0.5833817720 + 0.5842388868 + 0.5850911736 + 0.5859435201 + 0.5867958069 + 0.5876480937 + 0.5885003805 + 0.5893527269 + 0.5902050734 + 0.5910574198 + 0.5919092298 + 0.5927603841 + 0.5936114788 + 0.5944626927 + 0.5953139067 + 0.5961607099 + 0.5970060825 + 0.5978515148 + 0.5986968279 + 0.5995422006 + 0.6003857255 + 0.6012291908 + 0.6020725965 + 0.6029161215 + 0.6037604809 + 0.6046084166 + 0.6054564118 + 0.6063044071 + 0.6071524024 + 0.6080002189 + 0.6088476777 + 0.6096950769 + 0.6105425954 + 0.6113901138 + 0.6122344732 + 0.6130748987 + 0.6139153838 + 0.6147558093 + 0.6155961752 + 0.6164314151 + 0.6172606945 + 0.6180900931 + 0.6189194918 + 0.6197488904 + 0.6205750704 + 0.6213976741 + 0.6222202778 + 0.6230428815 + 0.6238654852 + 0.6246876121 + 0.6255089045 + 0.6263303161 + 0.6271517277 + 0.6279730201 + 0.6287912726 + 0.6296015978 + 0.6304119825 + 0.6312224269 + 0.6320326924 + 0.6328420043 + 0.6336416006 + 0.6344411969 + 0.6352407932 + 0.6360403895 + 0.6368399858 + 0.6376360059 + 0.6384313703 + 0.6392267942 + 0.6400222182 + 0.6408177018 + 0.6416134238 + 0.6424095035 + 0.6432055831 + 0.6440016031 + 0.6447976828 + 0.6455929279 + 0.6463851929 + 0.6471775770 + 0.6479700208 + 0.6487624049 + 0.6495547891 + 0.6503387094 + 0.6511209011 + 0.6519030929 + 0.6526852250 + 0.6534674168 + 0.6542478204 + 0.6550251245 + 0.6558023095 + 0.6565796137 + 0.6573567986 + 0.6581341028 + 0.6589120030 + 0.6596900225 + 0.6604681015 + 0.6612461209 + 0.6620242000 + 0.6628038287 + 0.6635876894 + 0.6643714905 + 0.6651554108 + 0.6659392715 + 0.6667230725 + 0.6675077081 + 0.6682925820 + 0.6690775156 + 0.6698623896 + 0.6706473827 + 0.6714323163 + 0.6722124815 + 0.6729924083 + 0.6737722158 + 0.6745520830 + 0.6753318906 + 0.6761127114 + 0.6768987179 + 0.6776847839 + 0.6784707904 + 0.6792567968 + 0.6800428033 + 0.6808322072 + 0.6816287041 + 0.6824250817 + 0.6832215786 + 0.6840180159 + 0.6848145127 + 0.6856052279 + 0.6863880754 + 0.6871711016 + 0.6879541278 + 0.6887369752 + 0.6895200014 + 0.6902992129 + 0.6910743713 + 0.6918495297 + 0.6926246881 + 0.6933997869 + 0.6941748857 + 0.6949504018 + 0.6957262158 + 0.6965019703 + 0.6972777843 + 0.6980537176 + 0.6988294721 + 0.6996037960 + 0.7003753781 + 0.7011470795 + 0.7019187212 + 0.7026903033 + 0.7034620047 + 0.7042315006 + 0.7049934268 + 0.7057551742 + 0.7065169811 + 0.7072789073 + 0.7080407143 + 0.7088025808 + 0.7095500827 + 0.7102975249 + 0.7110450268 + 0.7117924094 + 0.7125399113 + 0.7132872939 + 0.7140312195 + 0.7147734761 + 0.7155159116 + 0.7162582874 + 0.7170007229 + 0.7177429795 + 0.7184851170 + 0.7192267179 + 0.7199681997 + 0.7207098007 + 0.7214512825 + 0.7221928835 + 0.7229344249 + 0.7236632705 + 0.7243912816 + 0.7251192927 + 0.7258473039 + 0.7265753150 + 0.7273033261 + 0.7280253768 + 0.7287402749 + 0.7294551134 + 0.7301700115 + 0.7308847904 + 0.7315996289 + 0.7323145270 + 0.7330235839 + 0.7337319255 + 0.7344402075 + 0.7351484895 + 0.7358567715 + 0.7365651131 + 0.7372738123 + 0.7379835248 + 0.7386932969 + 0.7394030094 + 0.7401127219 + 0.7408224940 + 0.7415322065 + 0.7422450781 + 0.7429605126 + 0.7436760068 + 0.7443913817 + 0.7451068759 + 0.7458223104 + 0.7465378046 + 0.7472481728 + 0.7479571104 + 0.7486659884 + 0.7493749261 + 0.7500838041 + 0.7507926822 + 0.7515016198 + 0.7522053123 + 0.7529084086 + 0.7536116242 + 0.7543147206 + 0.7550178766 + 0.7557209730 + 0.7564241886 + 0.7571367025 + 0.7578495145 + 0.7585622072 + 0.7592749000 + 0.7599875927 + 0.7607002854 + 0.7614130974 + 0.7621260881 + 0.7628390789 + 0.7635521293 + 0.7642651200 + 0.7649781108 + 0.7656911016 + 0.7664040923 + 0.7671093941 + 0.7678139210 + 0.7685185075 + 0.7692229748 + 0.7699275017 + 0.7706320286 + 0.7713364959 + 0.7720378041 + 0.7727380991 + 0.7734382749 + 0.7741385102 + 0.7748388052 + 0.7755389810 + 0.7762392163 + 0.7769402266 + 0.7776417136 + 0.7783433199 + 0.7790448070 + 0.7797464132 + 0.7804480195 + 0.7811495066 + 0.7818514109 + 0.7825546861 + 0.7832580209 + 0.7839611769 + 0.7846645117 + 0.7853677869 + 0.7860711217 + 0.7867742777 + 0.7874720097 + 0.7881684899 + 0.7888649702 + 0.7895615101 + 0.7902579904 + 0.7909544706 + 0.7916510105 + 0.7923467755 + 0.7930412889 + 0.7937358022 + 0.7944303155 + 0.7951248288 + 0.7958192229 + 0.7965136766 + 0.7972081900 + 0.7979041934 + 0.7986006141 + 0.7992969751 + 0.7999933958 + 0.8006898165 + 0.8013861775 + 0.8020825982 + 0.8027787209 + 0.8034735918 + 0.8041685224 + 0.8048635125 + 0.8055583835 + 0.8062533140 + 0.8069481850 + 0.8076431155 + 0.8083345890 + 0.8090218902 + 0.8097093105 + 0.8103966117 + 0.8110839128 + 0.8117712736 + 0.8124585748 + 0.8131459951 + 0.8138281703 + 0.8145077229 + 0.8151872754 + 0.8158668280 + 0.8165463209 + 0.8172258139 + 0.8179054260 + 0.8185849190 + 0.8192651868 + 0.8199456930 + 0.8206261992 + 0.8213067055 + 0.8219872117 + 0.8226677179 + 0.8233482242 + 0.8240286708 + 0.8247126937 + 0.8253976107 + 0.8260825872 + 0.8267675042 + 0.8274524808 + 0.8281373978 + 0.8288223147 + 0.8295072913 + 0.8301901221 + 0.8308722973 + 0.8315544724 + 0.8322365880 + 0.8329188228 + 0.8336008787 + 0.8342831135 + 0.8349652290 + 0.8356407285 + 0.8363116980 + 0.8369827867 + 0.8376538157 + 0.8383247852 + 0.8389958143 + 0.8396669030 + 0.8403378725 + 0.8410078287 + 0.8416758180 + 0.8423439264 + 0.8430119157 + 0.8436799049 + 0.8443478942 + 0.8450158834 + 0.8456839919 + 0.8463522196 + 0.8470252752 + 0.8476983905 + 0.8483713865 + 0.8490445018 + 0.8497176170 + 0.8503906727 + 0.8510637283 + 0.8517367840 + 0.8524100780 + 0.8530836105 + 0.8537570834 + 0.8544304967 + 0.8551040292 + 0.8557773829 + 0.8564509153 + 0.8571243882 + 0.8577973843 + 0.8584672809 + 0.8591372967 + 0.8598073125 + 0.8604773283 + 0.8611472249 + 0.8618171811 + 0.8624871969 + 0.8631570935 + 0.8638226986 + 0.8644846082 + 0.8651465774 + 0.8658084869 + 0.8664703965 + 0.8671323061 + 0.8677942157 + 0.8684561253 + 0.8691179752 + 0.8697800040 + 0.8704419136 + 0.8711038828 + 0.8717659116 + 0.8724278212 + 0.8730897903 + 0.8737516999 + 0.8744137287 + 0.8750752807 + 0.8757330179 + 0.8763905764 + 0.8770483136 + 0.8777058721 + 0.8783634901 + 0.8790212274 + 0.8796787858 + 0.8803364038 + 0.8809922934 + 0.8816409111 + 0.8822894096 + 0.8829380274 + 0.8835865855 + 0.8842352033 + 0.8848837018 + 0.8855323195 + 0.8861808777 + 0.8868293166 + 0.8874773979 + 0.8881254196 + 0.8887733817 + 0.8894214034 + 0.8900694847 + 0.8907175064 + 0.8913655281 + 0.8920136094 + 0.8926612735 + 0.8933075070 + 0.8939536810 + 0.8945999742 + 0.8952462077 + 0.8958923817 + 0.8965386152 + 0.8971849084 + 0.8978310823 + 0.8984773159 + 0.8991330266 + 0.8997886777 + 0.9004443884 + 0.9011002183 + 0.9017559290 + 0.9024116993 + 0.9030674100 + 0.9037231803 + 0.9043788910 + 0.9050328135 + 0.9056859016 + 0.9063389897 + 0.9069920778 + 0.9076452851 + 0.9082983732 + 0.9089515209 + 0.9096046090 + 0.9102578163 + 0.9109084010 + 0.9115543962 + 0.9122003913 + 0.9128465056 + 0.9134925008 + 0.9141384959 + 0.9147844911 + 0.9154306054 + 0.9160766006 + 0.9167225957 + 0.9173694253 + 0.9180163145 + 0.9186632037 + 0.9193100929 + 0.9199569821 + 0.9206038713 + 0.9212508202 + 0.9218977094 + 0.9225445986 + 0.9231904149 + 0.9238339067 + 0.9244773984 + 0.9251207709 + 0.9257643223 + 0.9264076948 + 0.9270511866 + 0.9276946187 + 0.9283381104 + 0.9289816022 + 0.9296203852 + 0.9302564859 + 0.9308925867 + 0.9315286875 + 0.9321647882 + 0.9328008890 + 0.9334369898 + 0.9340732098 + 0.9347093105 + 0.9353454113 + 0.9359716177 + 0.9365960956 + 0.9372205138 + 0.9378449917 + 0.9384695292 + 0.9390940070 + 0.9397184849 + 0.9403430223 + 0.9409673810 + 0.9415919185 + 0.9422088861 + 0.9428256154 + 0.9434422851 + 0.9440590143 + 0.9446756840 + 0.9452924132 + 0.9459090829 + 0.9465258121 + 0.9471424818 + 0.9477592111 + 0.9483684897 + 0.9489774704 + 0.9495865703 + 0.9501956105 + 0.9508047104 + 0.9514136910 + 0.9520227909 + 0.9526317716 + 0.9532408714 + 0.9538499117 + 0.9544495940 + 0.9550477862 + 0.9556459785 + 0.9562441707 + 0.9568424225 + 0.9574406147 + 0.9580388069 + 0.9586369991 + 0.9592351913 + 0.9598333836 + 0.9604253769 + 0.9610139728 + 0.9616026878 + 0.9621914029 + 0.9627799988 + 0.9633687139 + 0.9639573097 + 0.9645460248 + 0.9651346803 + 0.9657232761 + 0.9663069248 + 0.9668803215 + 0.9674537182 + 0.9680271149 + 0.9686005116 + 0.9691739082 + 0.9697473049 + 0.9703207016 + 0.9708940983 + 0.9714674950 + 0.9720408916 + 0.9725995064 + 0.9731563926 + 0.9737132192 + 0.9742701054 + 0.9748269916 + 0.9753838181 + 0.9759407043 + 0.9764975905 + 0.9770544171 + 0.9776113033 + 0.9781597257 + 0.9786922932 + 0.9792249203 + 0.9797576070 + 0.9802901745 + 0.9808228016 + 0.9813554287 + 0.9818881154 + 0.9824206829 + 0.9829533100 + 0.9834858775 + 0.9840049744 + 0.9845178723 + 0.9850307703 + 0.9855437279 + 0.9860566854 + 0.9865695834 + 0.9870824814 + 0.9875953794 + 0.9881082773 + 0.9886211753 + 0.9891340733 + 0.9896370173 + 0.9901388884 + 0.9906408191 + 0.9911426902 + 0.9916446209 + 0.9921464920 + 0.9926484227 + 0.9931502938 + 0.9936522245 + 0.9941540956 + 0.9946557879 + 0.9951416850 + 0.9956275225 + 0.9961133003 + 0.9965991974 + 0.9970849752 + 0.9975708127 + 0.9980567098 + 0.9985424876 + 0.9990283251 + 0.9995142221 + 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/fujifilm_f-125.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/fujifilm_f-125.spi1d index eee3fc33..b20846fb 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/fujifilm_f-125.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/fujifilm_f-125.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0174498409 0.0158984791 0.0114764702 - 0.0361074992 0.0300892591 0.0227251798 - 0.0508534499 0.0413388014 0.0331778601 - 0.0630743131 0.0511820987 0.0424059182 - 0.0737055317 0.0595689416 0.0507076010 - 0.0831899792 0.0670762509 0.0583669208 - 0.0918336809 0.0739420429 0.0653621703 - 0.0999075696 0.0803586915 0.0719582215 - 0.1073999032 0.0863586813 0.0781143606 - 0.1143994033 0.0919513777 0.0840181410 - 0.1210075021 0.0972131789 0.0896857306 - 0.1272652000 0.1023328975 0.0950955227 - 0.1332022995 0.1073141024 0.1002841964 - 0.1389860064 0.1120809987 0.1052708998 - 0.1446685940 0.1166312024 0.1100749969 - 0.1502207965 0.1210201979 0.1147160009 - 0.1556082070 0.1252893060 0.1191812009 - 0.1608344018 0.1294762045 0.1235382035 - 0.1659597009 0.1336127967 0.1277814955 - 0.1709610969 0.1376406997 0.1319573969 - 0.1758396029 0.1415354013 0.1360505968 - 0.1805783063 0.1453579962 0.1400354058 - 0.1851565987 0.1491020024 0.1439131051 - 0.1896200031 0.1527583003 0.1477226019 - 0.1940072030 0.1563414037 0.1514364034 - 0.1983578950 0.1599265933 0.1550768018 - 0.2026146054 0.1634611934 0.1586581022 - 0.2068161964 0.1669549942 0.1621633023 - 0.2109510005 0.1704287976 0.1656109989 - 0.2149749994 0.1738846004 0.1689939946 - 0.2188791931 0.1773294955 0.1723017991 - 0.2226788998 0.1807734072 0.1755644977 - 0.2264375985 0.1841789037 0.1787340939 - 0.2301400006 0.1875509024 0.1818515062 - 0.2337515056 0.1908915937 0.1848967969 - 0.2373262048 0.1941868067 0.1878945976 - 0.2408698052 0.1974609047 0.1908354014 - 0.2443857044 0.2007011026 0.1937474012 - 0.2478293926 0.2038740963 0.1966456026 - 0.2511906028 0.2069949955 0.1995196939 - 0.2545030117 0.2100716978 0.2023606002 - 0.2577551007 0.2130948007 0.2052040994 - 0.2609705925 0.2161156982 0.2080169022 - 0.2641761005 0.2191050053 0.2107931972 - 0.2673386037 0.2220419943 0.2135623991 - 0.2704797983 0.2249206007 0.2162752002 - 0.2735692859 0.2277640998 0.2189542055 - 0.2766304910 0.2305577993 0.2216016054 - 0.2796724141 0.2333119065 0.2241947949 - 0.2826831937 0.2360485047 0.2267569005 - 0.2856605947 0.2387613952 0.2292927057 - 0.2885996997 0.2414368987 0.2317842990 - 0.2914834917 0.2441027015 0.2342378944 - 0.2943134904 0.2467696965 0.2366532981 - 0.2971065044 0.2494162023 0.2390290052 - 0.2998841107 0.2520292997 0.2413756996 - 0.3026126027 0.2546142042 0.2437071949 - 0.3053067923 0.2571905851 0.2460273951 - 0.3079755008 0.2597542107 0.2483306974 - 0.3106071949 0.2622892857 0.2506220937 - 0.3132098019 0.2647835910 0.2528899908 - 0.3157896101 0.2672505081 0.2551301122 - 0.3183388114 0.2697114050 0.2573640943 - 0.3208617866 0.2721551955 0.2595869899 - 0.3233729005 0.2745685875 0.2617990971 - 0.3258588910 0.2769567966 0.2639977038 - 0.3283306956 0.2793405056 0.2661921084 - 0.3308002055 0.2817173004 0.2683886886 - 0.3332616091 0.2840746939 0.2705698907 - 0.3356887102 0.2864069939 0.2727307081 - 0.3380911052 0.2887319922 0.2748725116 - 0.3404774070 0.2910597026 0.2769970894 - 0.3428531885 0.2933830023 0.2791089118 - 0.3452278078 0.2956911922 0.2812050879 - 0.3475916088 0.2979753911 0.2832851112 - 0.3499327898 0.3002283871 0.2853564918 - 0.3522517085 0.3024463058 0.2874124944 - 0.3545554876 0.3046588004 0.2894454896 - 0.3568528891 0.3068628013 0.2914513052 - 0.3591203094 0.3090505004 0.2934390903 - 0.3613556027 0.3112153113 0.2954114974 - 0.3635644913 0.3133526146 0.2973645031 - 0.3657538891 0.3154605031 0.2992906868 - 0.3679293990 0.3175630867 0.3012037873 - 0.3700731993 0.3196572065 0.3031010032 - 0.3722012937 0.3217284083 0.3049888909 - 0.3743169010 0.3237737119 0.3068724871 - 0.3764213026 0.3257966936 0.3087452054 - 0.3785130084 0.3278082013 0.3106024861 - 0.3805955052 0.3298071027 0.3124460876 - 0.3826780915 0.3317958117 0.3142769933 - 0.3847546875 0.3337770998 0.3160952926 - 0.3867942095 0.3357304037 0.3179017007 - 0.3888104856 0.3376702964 0.3196955025 - 0.3908145130 0.3396002054 0.3214753866 - 0.3928089142 0.3415268064 0.3232490122 - 0.3947995007 0.3434461057 0.3250178993 - 0.3967804909 0.3453572989 0.3267830908 - 0.3987523913 0.3472534120 0.3285475075 - 0.4007174075 0.3491292894 0.3303103149 - 0.4026708007 0.3509947956 0.3320707083 - 0.4046143889 0.3528488874 0.3338232934 - 0.4065484107 0.3546791077 0.3355642855 - 0.4084819853 0.3565005064 0.3372963965 - 0.4104149938 0.3583168089 0.3390068114 - 0.4123412967 0.3601278961 0.3407028019 - 0.4142560065 0.3619349003 0.3423874974 - 0.4161522090 0.3637281060 0.3440541029 - 0.4180339873 0.3655182123 0.3457126915 - 0.4199067950 0.3673050106 0.3473584950 - 0.4217523932 0.3690750003 0.3489893973 - 0.4235874116 0.3708429039 0.3506138027 - 0.4254117906 0.3726145029 0.3522236049 - 0.4272232056 0.3743841052 0.3538290858 - 0.4290306866 0.3761518002 0.3554284871 - 0.4308294058 0.3779003918 0.3570241034 - 0.4326210022 0.3796432018 0.3586181998 - 0.4344018102 0.3813720942 0.3602060080 - 0.4361707866 0.3830938041 0.3617906868 - 0.4379333854 0.3848012090 0.3633658886 - 0.4396809936 0.3864955008 0.3649328053 - 0.4414246082 0.3881793916 0.3664895892 - 0.4431430995 0.3898509145 0.3680340052 - 0.4448564947 0.3915182054 0.3695713878 - 0.4465528131 0.3931697011 0.3710972071 - 0.4482432008 0.3948189020 0.3726201952 - 0.4499239028 0.3964514136 0.3741200864 - 0.4515936971 0.3980818987 0.3756181896 - 0.4532519877 0.3997015953 0.3771077991 - 0.4548968971 0.4013178945 0.3785969913 - 0.4565317035 0.4029234946 0.3800840974 - 0.4581527114 0.4045226872 0.3815703094 - 0.4597654939 0.4061067104 0.3830527067 - 0.4613632858 0.4076885879 0.3845353127 - 0.4629530907 0.4092662036 0.3860189021 - 0.4645408094 0.4108397961 0.3874995112 - 0.4661273062 0.4124050140 0.3889681995 - 0.4677076936 0.4139649868 0.3904353082 - 0.4692842960 0.4155119956 0.3918946981 - 0.4708591998 0.4170548022 0.3933512866 - 0.4724324942 0.4185824096 0.3947859108 - 0.4739956856 0.4201084077 0.3962202072 - 0.4755468965 0.4216217101 0.3976345956 - 0.4770910144 0.4231351018 0.3990488946 - 0.4786213040 0.4246394038 0.4004501104 - 0.4801476002 0.4261435866 0.4018495977 - 0.4816561043 0.4276433885 0.4032416940 - 0.4831644893 0.4291422963 0.4046311080 - 0.4846526980 0.4306376874 0.4060145020 - 0.4861406982 0.4321312904 0.4073927999 - 0.4876179993 0.4336220026 0.4087674022 - 0.4890919924 0.4351086915 0.4101338089 - 0.4905653000 0.4365926087 0.4114997089 - 0.4920378029 0.4380624890 0.4128586948 - 0.4935061932 0.4395323992 0.4142177105 - 0.4949601889 0.4409978986 0.4155749083 - 0.4964140952 0.4424628019 0.4169315994 - 0.4978365004 0.4439246953 0.4182890952 - 0.4992552996 0.4453840852 0.4196475148 - 0.5006620884 0.4468388855 0.4210050106 - 0.5020588040 0.4482730925 0.4223509133 - 0.5034533739 0.4497072101 0.4236967862 - 0.5048360825 0.4511240125 0.4250375032 - 0.5062186718 0.4525361061 0.4263758063 - 0.5075957179 0.4539420903 0.4277085066 - 0.5089703798 0.4553368986 0.4290247858 - 0.5103421211 0.4567317963 0.4303410053 - 0.5117046833 0.4581159055 0.4316442013 - 0.5130673051 0.4594987035 0.4329437912 - 0.5144175887 0.4608711898 0.4342381954 - 0.5157638192 0.4622273147 0.4355193973 - 0.5171049237 0.4635834098 0.4368005097 - 0.5184270144 0.4649255872 0.4380722940 - 0.5197492242 0.4662652016 0.4393407106 - 0.5210537910 0.4676026106 0.4406091869 - 0.5223482847 0.4689337909 0.4418779016 - 0.5236423016 0.4702650905 0.4431464970 - 0.5249115229 0.4715861082 0.4444108009 - 0.5261808038 0.4729016125 0.4456712902 - 0.5274490118 0.4742170870 0.4469316900 - 0.5287154913 0.4755303860 0.4481886029 - 0.5299819708 0.4768437147 0.4494448900 - 0.5312492251 0.4781504869 0.4506979883 - 0.5325167775 0.4794408083 0.4519374073 - 0.5337843895 0.4807311893 0.4531767964 - 0.5350342989 0.4820165038 0.4544104934 - 0.5362828970 0.4832974970 0.4556373060 - 0.5375289917 0.4845784903 0.4568639994 - 0.5387613177 0.4858368039 0.4580819011 - 0.5399935842 0.4870882928 0.4592955112 - 0.5412209034 0.4883398116 0.4605090916 - 0.5424383879 0.4895803928 0.4617215991 - 0.5436558723 0.4908204079 0.4629341066 - 0.5448678136 0.4920594990 0.4641464949 - 0.5460734963 0.4932895899 0.4653517008 - 0.5472792983 0.4945197105 0.4665569961 - 0.5484790802 0.4957481027 0.4677614868 - 0.5496746898 0.4969693124 0.4689593911 - 0.5508701801 0.4981904924 0.4701572061 - 0.5520610809 0.4994137883 0.4713543952 - 0.5532492995 0.5006437898 0.4725480974 - 0.5544373989 0.5018737912 0.4737417102 - 0.5556218028 0.5030999184 0.4749346972 - 0.5568041205 0.5043138862 0.4761235118 - 0.5579863191 0.5055277944 0.4773122966 - 0.5591588020 0.5067374706 0.4785006046 - 0.5603247285 0.5079305172 0.4796842933 - 0.5614907146 0.5091236234 0.4808681011 - 0.5626431108 0.5103173256 0.4820516109 - 0.5637829900 0.5115153193 0.4832226932 - 0.5649229884 0.5127133131 0.4843936861 - 0.5660511851 0.5139113069 0.4855647981 - 0.5671615005 0.5151088834 0.4867219925 - 0.5682718158 0.5163065195 0.4878776968 - 0.5693790913 0.5175040960 0.4890333116 - 0.5704758167 0.5186815858 0.4901837111 - 0.5715726018 0.5198547244 0.4913321137 - 0.5726693273 0.5210276842 0.4924804866 - 0.5737720132 0.5221964717 0.4936192930 - 0.5748748183 0.5233623981 0.4947485924 - 0.5759775043 0.5245283246 0.4958778918 - 0.5770760775 0.5256924033 0.4970034957 - 0.5781732202 0.5268526077 0.4981164932 - 0.5792703032 0.5280128121 0.4992294014 - 0.5803601146 0.5291728973 0.5003424287 - 0.5814397931 0.5303236246 0.5014445186 - 0.5825194120 0.5314744115 0.5025454760 - 0.5835987926 0.5326251984 0.5036464930 - 0.5846728086 0.5337677002 0.5047429800 - 0.5857468843 0.5349056125 0.5058357120 - 0.5868209004 0.5360434055 0.5069283843 - 0.5878757238 0.5371788144 0.5080201030 - 0.5889192820 0.5383052826 0.5091047883 - 0.5899627805 0.5394318104 0.5101894736 - 0.5910035968 0.5405583978 0.5112742186 - 0.5920301080 0.5416883826 0.5123528838 - 0.5930566788 0.5428196192 0.5134283900 - 0.5940833092 0.5439507961 0.5145038962 - 0.5950983167 0.5450789928 0.5155786872 - 0.5961067080 0.5461968184 0.5166484118 - 0.5971152186 0.5473145843 0.5177180767 - 0.5981239080 0.5484325290 0.5187878013 - 0.5991356969 0.5495414734 0.5198484063 - 0.6001476049 0.5506458879 0.5209016204 - 0.6011593938 0.5517503023 0.5219547153 - 0.6021643281 0.5528541207 0.5230079293 - 0.6031603217 0.5539363027 0.5240486860 - 0.6041563749 0.5550184250 0.5250884295 - 0.6051524282 0.5561006069 0.5261281729 - 0.6061363816 0.5571805835 0.5271657705 - 0.6071165204 0.5582563877 0.5281966925 - 0.6080965996 0.5593320727 0.5292276144 - 0.6090764999 0.5604078770 0.5302584767 - 0.6100435257 0.5614696145 0.5312880278 - 0.6110104918 0.5625219941 0.5323162079 - 0.6119775176 0.5635743737 0.5333443880 - 0.6129397154 0.5646268129 0.5343726277 - 0.6138859987 0.5656726956 0.5353969932 - 0.6148322821 0.5667169094 0.5364199877 - 0.6157786250 0.5677611828 0.5374429226 - 0.6167212725 0.5688053966 0.5384659171 - 0.6176583171 0.5698326230 0.5394816995 - 0.6185953021 0.5708590150 0.5404965281 - 0.6195322871 0.5718854070 0.5415111780 - 0.6204634905 0.5729113817 0.5425258875 - 0.6213884950 0.5739294291 0.5435299873 - 0.6223133802 0.5749472976 0.5445337296 - 0.6232383251 0.5759652853 0.5455372930 - 0.6241629124 0.5769823194 0.5465409160 - 0.6250872016 0.5779896975 0.5475350022 - 0.6260113716 0.5789970756 0.5485289097 - 0.6269357204 0.5800045133 0.5495228171 - 0.6278557777 0.5810111165 0.5505167842 - 0.6287717223 0.5820034742 0.5515152216 - 0.6296874881 0.5829960108 0.5525137782 - 0.6306033731 0.5839884281 0.5535123944 - 0.6315113902 0.5849809051 0.5545110106 - 0.6324080229 0.5859525204 0.5554962158 - 0.6333045959 0.5869235992 0.5564795732 - 0.6342011094 0.5878946185 0.5574629903 - 0.6350932121 0.5888655782 0.5584464073 - 0.6359726787 0.5898336172 0.5594201088 - 0.6368522048 0.5908008814 0.5603901744 - 0.6377316713 0.5917682052 0.5613601804 - 0.6386104822 0.5927355289 0.5623303056 - 0.6394773126 0.5936999917 0.5632957816 - 0.6403442025 0.5946627259 0.5642570257 - 0.6412109733 0.5956255198 0.5652183294 - 0.6420779228 0.5965881944 0.5661795735 - 0.6429408789 0.5975462794 0.5671393871 - 0.6438030005 0.5984956026 0.5680949092 - 0.6446650028 0.5994448066 0.5690503120 - 0.6455270052 0.6003941298 0.5700057745 - 0.6463868022 0.6013432741 0.5709612966 - 0.6472437978 0.6022868752 0.5719025135 - 0.6481007934 0.6032304764 0.5728424788 - 0.6489577889 0.6041740775 0.5737825036 - 0.6498141289 0.6051176786 0.5747225285 - 0.6506562233 0.6060569286 0.5756577849 - 0.6514983177 0.6069930792 0.5765889287 - 0.6523404717 0.6079294086 0.5775198936 - 0.6531826258 0.6088656783 0.5784509182 - 0.6540184021 0.6098006964 0.5793818235 - 0.6548498273 0.6107280850 0.5803099871 - 0.6556811929 0.6116555929 0.5812382102 - 0.6565124989 0.6125831008 0.5821664929 - 0.6573433876 0.6135106087 0.5830947161 - 0.6581645012 0.6144319773 0.5840185881 - 0.6589856148 0.6153494716 0.5849387050 - 0.6598067284 0.6162670255 0.5858588219 - 0.6606277823 0.6171844006 0.5867788792 - 0.6614431739 0.6181018949 0.5876989961 - 0.6622517705 0.6190115213 0.5886114240 - 0.6630604267 0.6199212074 0.5895230770 - 0.6638690233 0.6208307743 0.5904347897 - 0.6646776199 0.6217405200 0.5913465023 - 0.6654757261 0.6226496100 0.5922542214 - 0.6662709117 0.6235576868 0.5931503177 - 0.6670660973 0.6244658232 0.5940464139 - 0.6678612828 0.6253738999 0.5949425101 - 0.6686561704 0.6262819767 0.5958386064 - 0.6694442034 0.6271823049 0.5967332721 - 0.6702321768 0.6280776262 0.5976269245 - 0.6710202098 0.6289728880 0.5985203981 - 0.6718081832 0.6298682094 0.5994138718 - 0.6725955009 0.6307634711 0.6003074050 - 0.6733806133 0.6316475272 0.6011890769 - 0.6741657257 0.6325287223 0.6020658016 - 0.6749507189 0.6334097981 0.6029425859 - 0.6757357717 0.6342908740 0.6038193703 - 0.6765164137 0.6351721287 0.6046960950 - 0.6772890091 0.6360508800 0.6055694818 - 0.6780614853 0.6369295716 0.6064420938 - 0.6788340807 0.6378083229 0.6073147058 - 0.6796066165 0.6386870146 0.6081873775 - 0.6803761721 0.6395655870 0.6090599895 - 0.6811413169 0.6404411793 0.6099300981 - 0.6819062829 0.6413165927 0.6107999086 - 0.6826714277 0.6421920061 0.6116697192 - 0.6834365129 0.6430674195 0.6125395298 - 0.6841984987 0.6439427733 0.6134092808 - 0.6849551797 0.6448048949 0.6142743230 - 0.6857119203 0.6456660032 0.6151384115 - 0.6864687204 0.6465271115 0.6160026193 - 0.6872254014 0.6473882198 0.6168667078 - 0.6879792213 0.6482493281 0.6177309155 - 0.6887251735 0.6491037011 0.6185951233 - 0.6894711256 0.6499567032 0.6194592714 - 0.6902170777 0.6508097053 0.6203234792 - 0.6909629703 0.6516628265 0.6211876869 - 0.6917080283 0.6525158286 0.6220518947 - 0.6924436092 0.6533647776 0.6229112148 - 0.6931793094 0.6542117000 0.6237667799 - 0.6939148903 0.6550586224 0.6246222854 - 0.6946504712 0.6559054852 0.6254779100 - 0.6953861117 0.6567524076 0.6263334155 - 0.6961168051 0.6575965285 0.6271870732 - 0.6968464851 0.6584361792 0.6280364990 - 0.6975762844 0.6592760086 0.6288859248 - 0.6983060241 0.6601157188 0.6297354102 - 0.6990358233 0.6609554887 0.6305847764 - 0.6997650862 0.6617947817 0.6314343214 - 0.7004939914 0.6626260877 0.6322728992 - 0.7012230158 0.6634575129 0.6331112981 - 0.7019519210 0.6642888784 0.6339496970 - 0.7026808858 0.6651203036 0.6347880960 - 0.7034088969 0.6659517288 0.6356264949 - 0.7041271925 0.6667770743 0.6364635825 - 0.7048454881 0.6675990224 0.6372997165 - 0.7055637836 0.6684209108 0.6381359100 - 0.7062821984 0.6692429185 0.6389719844 - 0.7070004940 0.6700648069 0.6398081183 - 0.7077152729 0.6708840132 0.6406437755 - 0.7084274888 0.6716861129 0.6414728165 - 0.7091398239 0.6724882722 0.6423017979 - 0.7098520994 0.6732904911 0.6431307793 - 0.7105643749 0.6740925908 0.6439598203 - 0.7112765908 0.6748948097 0.6447888017 - 0.7119773030 0.6756910086 0.6456167102 - 0.7126780152 0.6764826775 0.6464434862 - 0.7133787870 0.6772742867 0.6472703218 - 0.7140794992 0.6780658960 0.6480970979 - 0.7147802711 0.6788576245 0.6489239931 - 0.7154778838 0.6796491742 0.6497508287 - 0.7161695957 0.6804391742 0.6505677104 - 0.7168614268 0.6812291145 0.6513826251 - 0.7175530791 0.6820189953 0.6521974802 - 0.7182449102 0.6828088164 0.6530123949 - 0.7189366221 0.6835986972 0.6538271904 - 0.7196236253 0.6843873262 0.6546415091 - 0.7203075290 0.6851689219 0.6554480195 - 0.7209913731 0.6859505177 0.6562544703 - 0.7216752768 0.6867321730 0.6570609808 - 0.7223592997 0.6875138283 0.6578674912 - 0.7230432034 0.6882954240 0.6586740017 - 0.7237169147 0.6890760064 0.6594784856 - 0.7243875265 0.6898545027 0.6602771878 - 0.7250581980 0.6906331182 0.6610758901 - 0.7257289290 0.6914116144 0.6618745923 - 0.7263996005 0.6921901107 0.6626732945 - 0.7270702124 0.6929686069 0.6634719968 - 0.7277340293 0.6937432885 0.6642683744 - 0.7283964753 0.6945124269 0.6650602221 - 0.7290591002 0.6952815056 0.6658521295 - 0.7297216058 0.6960507035 0.6666439176 - 0.7303841710 0.6968197823 0.6674357057 - 0.7310466766 0.6975889206 0.6682276130 - 0.7317048907 0.6983528733 0.6690183282 - 0.7323622108 0.6991090178 0.6698067784 - 0.7330195904 0.6998651028 0.6705952883 - 0.7336769104 0.7006211877 0.6713837981 - 0.7343342900 0.7013772726 0.6721723080 - 0.7349916101 0.7021334171 0.6729608178 - 0.7356424928 0.7028859258 0.6737465262 - 0.7362912297 0.7036299109 0.6745222211 - 0.7369397879 0.7043738961 0.6752979755 - 0.7375885248 0.7051178217 0.6760736704 - 0.7382372022 0.7058618069 0.6768494844 - 0.7388858795 0.7066057920 0.6776251793 - 0.7395280004 0.7073493004 0.6784006953 - 0.7401649952 0.7080891132 0.6791678071 - 0.7408019900 0.7088289857 0.6799349785 - 0.7414389849 0.7095689178 0.6807020903 - 0.7420759201 0.7103086710 0.6814693213 - 0.7427129149 0.7110486031 0.6822363734 - 0.7433465123 0.7117884159 0.6830036044 - 0.7439708710 0.7125198841 0.6837611794 - 0.7445952892 0.7132496834 0.6845157146 - 0.7452197075 0.7139794230 0.6852701902 - 0.7458441854 0.7147092223 0.6860247254 - 0.7464686036 0.7154390216 0.6867792010 - 0.7470930219 0.7161687016 0.6875336766 - 0.7477155924 0.7168959975 0.6882864833 - 0.7483379841 0.7176201940 0.6890364885 - 0.7489603758 0.7183443904 0.6897866130 - 0.7495828271 0.7190685868 0.6905366182 - 0.7502052188 0.7197927833 0.6912866831 - 0.7508276105 0.7205169797 0.6920368075 - 0.7514470816 0.7212411761 0.6927868128 - 0.7520624995 0.7219575047 0.6935216784 - 0.7526779771 0.7226737142 0.6942551136 - 0.7532935143 0.7233898044 0.6949886084 - 0.7539088726 0.7241060138 0.6957219839 - 0.7545244098 0.7248222232 0.6964554191 - 0.7551398873 0.7255383730 0.6971887946 - 0.7557541132 0.7262521982 0.6979203224 - 0.7563681006 0.7269626856 0.6986479163 - 0.7569820881 0.7276732922 0.6993755102 - 0.7575961947 0.7283838987 0.7001032233 - 0.7582101822 0.7290943861 0.7008308172 - 0.7588242292 0.7298049927 0.7015584707 - 0.7594367266 0.7305154800 0.7022861242 - 0.7600427866 0.7312164903 0.7030051947 - 0.7606487870 0.7319142818 0.7037203908 - 0.7612549067 0.7326120138 0.7044355273 - 0.7618610263 0.7333098054 0.7051507235 - 0.7624670267 0.7340075970 0.7058659196 - 0.7630730867 0.7347053885 0.7065809965 - 0.7636762857 0.7354031801 0.7072961926 - 0.7642757297 0.7360877991 0.7080059052 - 0.7648751140 0.7367721796 0.7087152004 - 0.7654743791 0.7374566197 0.7094244957 - 0.7660738230 0.7381410003 0.7101337910 - 0.7666732073 0.7388253808 0.7108430266 - 0.7672724724 0.7395098805 0.7115523219 - 0.7678692937 0.7401931286 0.7122616172 - 0.7684640288 0.7408679724 0.7129713297 - 0.7690587044 0.7415428758 0.7136811018 - 0.7696534991 0.7422178984 0.7143908143 - 0.7702481747 0.7428928018 0.7151005268 - 0.7708429098 0.7435677052 0.7158101797 - 0.7714375854 0.7442426085 0.7165200114 - 0.7720239162 0.7449169755 0.7172291875 - 0.7726042271 0.7455884814 0.7179328203 - 0.7731844783 0.7462599874 0.7186363935 - 0.7737647891 0.7469314933 0.7193400264 - 0.7743450999 0.7476029992 0.7200435996 - 0.7749254107 0.7482745051 0.7207471728 - 0.7755057216 0.7489460111 0.7214508057 - 0.7760874033 0.7496169209 0.7221543193 - 0.7766703963 0.7502800822 0.7228549123 - 0.7772535086 0.7509431839 0.7235553861 - 0.7778365016 0.7516064048 0.7242559195 - 0.7784196138 0.7522696257 0.7249565125 - 0.7790026069 0.7529327869 0.7256569862 - 0.7795857191 0.7535960078 0.7263575196 - 0.7801663876 0.7542592287 0.7270581126 - 0.7807425261 0.7549132109 0.7277507782 - 0.7813186049 0.7555662990 0.7284420729 - 0.7818946838 0.7562193871 0.7291333079 - 0.7824708223 0.7568724751 0.7298246026 - 0.7830467820 0.7575256228 0.7305157781 - 0.7836229205 0.7581787109 0.7312070727 - 0.7841988206 0.7588317990 0.7318983078 - 0.7847698927 0.7594774961 0.7325916290 - 0.7853410840 0.7601187229 0.7332865000 - 0.7859122157 0.7607598901 0.7339814901 - 0.7864832878 0.7614011168 0.7346764207 - 0.7870544791 0.7620422840 0.7353712916 - 0.7876256108 0.7626835108 0.7360662818 - 0.7881966829 0.7633246779 0.7367612123 - 0.7887653708 0.7639619708 0.7374557853 - 0.7893326879 0.7645862103 0.7381486297 - 0.7899000049 0.7652103901 0.7388414741 - 0.7904673219 0.7658346295 0.7395343781 - 0.7910345793 0.7664586902 0.7402272224 - 0.7916018963 0.7670829296 0.7409201264 - 0.7921692133 0.7677071095 0.7416129708 - 0.7927350998 0.7683312893 0.7423058748 - 0.7932925820 0.7689499259 0.7429925799 - 0.7938501239 0.7695664763 0.7436763048 - 0.7944076061 0.7701830268 0.7443599105 - 0.7949650884 0.7707995772 0.7450435162 - 0.7955226898 0.7714161277 0.7457271814 - 0.7960801721 0.7720326185 0.7464107871 - 0.7966377139 0.7726492286 0.7470945120 - 0.7971891761 0.7732650042 0.7477774024 - 0.7977350950 0.7738748193 0.7484462857 - 0.7982810736 0.7744845152 0.7491152287 - 0.7988271117 0.7750942707 0.7497841120 - 0.7993730903 0.7757040262 0.7504529953 - 0.7999191284 0.7763137817 0.7511218786 - 0.8004651070 0.7769234776 0.7517908216 - 0.8010110259 0.7775332928 0.7524597049 - 0.8015481234 0.7781410813 0.7531278729 - 0.8020827770 0.7787457705 0.7537946105 - 0.8026174903 0.7793504000 0.7544614077 - 0.8031522036 0.7799550295 0.7551280856 - 0.8036869168 0.7805595994 0.7557948232 - 0.8042215705 0.7811642885 0.7564616203 - 0.8047562242 0.7817689180 0.7571282983 - 0.8052908778 0.7823734879 0.7577949762 - 0.8058195114 0.7829757929 0.7584580183 - 0.8063477278 0.7835758924 0.7591165900 - 0.8068758845 0.7841761112 0.7597752810 - 0.8074041009 0.7847762704 0.7604339719 - 0.8079321980 0.7853764296 0.7610926032 - 0.8084604144 0.7859765887 0.7617512941 - 0.8089885712 0.7865768075 0.7624099851 - 0.8095167875 0.7871769071 0.7630686164 - 0.8100429177 0.7877739072 0.7637246847 - 0.8105689287 0.7883684039 0.7643780708 - 0.8110948801 0.7889629006 0.7650315762 - 0.8116210103 0.7895573974 0.7656850815 - 0.8121470213 0.7901518941 0.7663385868 - 0.8126729727 0.7907463908 0.7669919729 - 0.8131991029 0.7913408279 0.7676454782 - 0.8137251139 0.7919353247 0.7682989836 - 0.8142498136 0.7925245762 0.7689477205 - 0.8147743940 0.7931082845 0.7695900202 - 0.8152989745 0.7936919928 0.7702323198 - 0.8158236146 0.7942757010 0.7708745003 - 0.8163480759 0.7948594093 0.7715167999 - 0.8168727160 0.7954431772 0.7721590996 - 0.8173972964 0.7960268855 0.7728013992 - 0.8179218769 0.7966105938 0.7734436989 - 0.8184441924 0.7971937060 0.7740848064 - 0.8189654946 0.7977755070 0.7747216821 - 0.8194869161 0.7983571887 0.7753584981 - 0.8200082183 0.7989389896 0.7759953737 - 0.8205295205 0.7995206714 0.7766323090 - 0.8210508227 0.8001024127 0.7772691846 - 0.8215721846 0.8006842136 0.7779061198 - 0.8220934868 0.8012658954 0.7785429955 - 0.8226118088 0.8018476963 0.7791798711 - 0.8231247067 0.8024280071 0.7798110843 - 0.8236376047 0.8030081987 0.7804415822 - 0.8241505027 0.8035883904 0.7810720801 - 0.8246634007 0.8041685820 0.7817025781 - 0.8251761794 0.8047488928 0.7823330760 - 0.8256890774 0.8053290844 0.7829635143 - 0.8262019753 0.8059092760 0.7835940123 - 0.8267148733 0.8064895868 0.7842245102 - 0.8272213936 0.8070695996 0.7848517895 - 0.8277270794 0.8076496124 0.7854750752 - 0.8282328844 0.8082295060 0.7860983014 - 0.8287385702 0.8088093996 0.7867215872 - 0.8292443752 0.8093892932 0.7873448730 - 0.8297501802 0.8099691868 0.7879682183 - 0.8302559257 0.8105490804 0.7885915041 - 0.8307616711 0.8111289740 0.7892147899 - 0.8312665224 0.8117089868 0.7898380160 - 0.8317692280 0.8122891188 0.7904539704 - 0.8322719932 0.8128693104 0.7910689116 - 0.8327746987 0.8134495020 0.7916837931 - 0.8332775235 0.8140296936 0.7922986746 - 0.8337802291 0.8146098852 0.7929136157 - 0.8342829943 0.8151900768 0.7935284972 - 0.8347858191 0.8157703280 0.7941433787 - 0.8352885246 0.8163505197 0.7947583199 - 0.8357869983 0.8169273734 0.7953717113 - 0.8362827897 0.8174930811 0.7959778905 - 0.8367785215 0.8180589080 0.7965841293 - 0.8372743130 0.8186246753 0.7971904278 - 0.8377701044 0.8191903830 0.7977966070 - 0.8382657766 0.8197562099 0.7984027863 - 0.8387616277 0.8203219175 0.7990090847 - 0.8392574191 0.8208876848 0.7996153235 - 0.8397530913 0.8214535117 0.8002215028 - 0.8402497172 0.8220161200 0.8008267879 - 0.8407465219 0.8225740194 0.8014301062 - 0.8412433267 0.8231317997 0.8020334840 - 0.8417400718 0.8236895800 0.8026369214 - 0.8422368765 0.8242474794 0.8032401800 - 0.8427336812 0.8248053193 0.8038436174 - 0.8432304263 0.8253630996 0.8044469953 - 0.8437271714 0.8259209991 0.8050503135 - 0.8442239761 0.8264787793 0.8056536913 - 0.8447151184 0.8270347118 0.8062539101 - 0.8452048898 0.8275880218 0.8068488240 - 0.8456946015 0.8281413913 0.8074436784 - 0.8461843133 0.8286947012 0.8080385923 - 0.8466740251 0.8292480111 0.8086336255 - 0.8471636772 0.8298013210 0.8092284799 - 0.8476533890 0.8303546906 0.8098233938 - 0.8481432199 0.8309080005 0.8104183078 - 0.8486328721 0.8314613104 0.8110132217 - 0.8491188288 0.8320134878 0.8116052747 - 0.8496029973 0.8325635791 0.8121902943 - 0.8500872850 0.8331136107 0.8127751946 - 0.8505715132 0.8336635828 0.8133600950 - 0.8510558009 0.8342136741 0.8139451146 - 0.8515400290 0.8347637057 0.8145300150 - 0.8520243168 0.8353136778 0.8151149154 - 0.8525084853 0.8358637094 0.8156998754 - 0.8529927731 0.8364138007 0.8162847757 - 0.8534749746 0.8369629979 0.8168694973 - 0.8539546728 0.8375040889 0.8174484968 - 0.8544343710 0.8380451798 0.8180274963 - 0.8549141288 0.8385862708 0.8186064959 - 0.8553938270 0.8391274214 0.8191856146 - 0.8558735251 0.8396685123 0.8197646141 - 0.8563532233 0.8402096033 0.8203436136 - 0.8568329215 0.8407506943 0.8209226131 - 0.8573125005 0.8412917852 0.8215016723 - 0.8577917814 0.8418329954 0.8220806718 - 0.8582649827 0.8423717022 0.8226525784 - 0.8587383032 0.8429095149 0.8232210875 - 0.8592115045 0.8434472084 0.8237894773 - 0.8596848249 0.8439850211 0.8243579865 - 0.8601580262 0.8445227742 0.8249264956 - 0.8606312871 0.8450605869 0.8254948854 - 0.8611046076 0.8455983996 0.8260633945 - 0.8615778089 0.8461362123 0.8266319036 - 0.8620511293 0.8466740251 0.8272002935 - 0.8625226021 0.8472099900 0.8277677298 - 0.8629927039 0.8477393985 0.8283293247 - 0.8634628057 0.8482688069 0.8288909197 - 0.8639330268 0.8487980962 0.8294523954 - 0.8644031286 0.8493275046 0.8300139904 - 0.8648731709 0.8498567939 0.8305755854 - 0.8653432727 0.8503862023 0.8311371207 - 0.8658133745 0.8509156108 0.8316987157 - 0.8662835956 0.8514449000 0.8322603106 - 0.8667536974 0.8519743085 0.8328217864 - 0.8672150970 0.8524994254 0.8333820105 - 0.8676754832 0.8530210257 0.8339408040 - 0.8681359291 0.8535426259 0.8344995975 - 0.8685963154 0.8540641069 0.8350583911 - 0.8690567017 0.8545857072 0.8356171846 - 0.8695170879 0.8551073074 0.8361760974 - 0.8699774742 0.8556289077 0.8367348909 - 0.8704379201 0.8561505079 0.8372936845 - 0.8708983064 0.8566719890 0.8378524780 - 0.8713576198 0.8571935892 0.8384112716 - 0.8718056083 0.8577103019 0.8389707208 - 0.8722537160 0.8582252860 0.8395304084 - 0.8727017045 0.8587402105 0.8400899768 - 0.8731498122 0.8592551947 0.8406497240 - 0.8735978007 0.8597701788 0.8412094116 - 0.8740459085 0.8602852225 0.8417689800 - 0.8744938970 0.8608002067 0.8423287272 - 0.8749418855 0.8613150716 0.8428884149 - 0.8753899932 0.8618301153 0.8434481025 - 0.8758366704 0.8623450994 0.8440076709 - 0.8762751818 0.8628504872 0.8445582986 - 0.8767135739 0.8633533716 0.8451061249 - 0.8771520853 0.8638563752 0.8456537724 - 0.8775904775 0.8643593192 0.8462015986 - 0.8780289888 0.8648623228 0.8467493057 - 0.8784673810 0.8653652072 0.8472970724 - 0.8789058924 0.8658682108 0.8478447795 - 0.8793442845 0.8663712144 0.8483924866 - 0.8797827959 0.8668740988 0.8489403129 - 0.8802210093 0.8673771024 0.8494880199 - 0.8806512952 0.8678756952 0.8500298858 - 0.8810817003 0.8683726192 0.8505687714 - 0.8815121055 0.8688694835 0.8511077166 - 0.8819423914 0.8693662882 0.8516464829 - 0.8823727965 0.8698632121 0.8521854281 - 0.8828032017 0.8703600764 0.8527243137 - 0.8832334876 0.8708568811 0.8532631993 - 0.8836638927 0.8713538051 0.8538020849 - 0.8840942979 0.8718507290 0.8543409705 - 0.8845247030 0.8723474741 0.8548799157 - 0.8849477172 0.8728402853 0.8554143906 - 0.8853685856 0.8733280897 0.8559429049 - 0.8857895732 0.8738158941 0.8564714193 - 0.8862105012 0.8743038177 0.8569998741 - 0.8866314888 0.8747916222 0.8575283289 - 0.8870524168 0.8752794862 0.8580567837 - 0.8874734044 0.8757672906 0.8585852981 - 0.8878942728 0.8762552142 0.8591138124 - 0.8883153200 0.8767430186 0.8596423268 - 0.8887361884 0.8772308826 0.8601707816 - 0.8891549706 0.8777185082 0.8606991768 - 0.8895698786 0.8782029152 0.8612251878 - 0.8899849057 0.8786873817 0.8617511988 - 0.8903998733 0.8791717887 0.8622772098 - 0.8908147812 0.8796561956 0.8628032804 - 0.8912298083 0.8801407218 0.8633292913 - 0.8916447759 0.8806251287 0.8638553023 - 0.8920598030 0.8811094761 0.8643813133 - 0.8924747109 0.8815940022 0.8649073243 - 0.8928896785 0.8820784092 0.8654333949 - 0.8933047056 0.8825628161 0.8659594059 - 0.8937178850 0.8830412030 0.8664832711 - 0.8941305876 0.8835136890 0.8670049906 - 0.8945432901 0.8839861751 0.8675267100 - 0.8949561119 0.8844587207 0.8680483103 - 0.8953688145 0.8849312067 0.8685700297 - 0.8957815766 0.8854036927 0.8690915704 - 0.8961942792 0.8858762980 0.8696132898 - 0.8966071010 0.8863487840 0.8701350093 - 0.8970198035 0.8868213296 0.8706566095 - 0.8974326253 0.8872938156 0.8711783290 - 0.8978449106 0.8877663016 0.8716999292 - 0.8982529044 0.8882371187 0.8722208142 - 0.8986608982 0.8887075186 0.8727415204 - 0.8990687728 0.8891779184 0.8732621074 - 0.8994768262 0.8896481991 0.8737828135 - 0.8998848200 0.8901185989 0.8743034005 - 0.9002928138 0.8905889988 0.8748241067 - 0.9007008076 0.8910593987 0.8753446937 - 0.9011088014 0.8915297985 0.8758653998 - 0.9015167952 0.8920001984 0.8763859868 - 0.9019247293 0.8924705982 0.8769065738 - 0.9023321867 0.8929408789 0.8774272799 - 0.9027374983 0.8934035897 0.8779382706 - 0.9031428099 0.8938655853 0.8784481287 - 0.9035481215 0.8943277001 0.8789578080 - 0.9039533734 0.8947896957 0.8794676065 - 0.9043586850 0.8952516913 0.8799774051 - 0.9047639966 0.8957138062 0.8804872036 - 0.9051693082 0.8961758018 0.8809968829 - 0.9055746198 0.8966377974 0.8815066814 - 0.9059798717 0.8970997930 0.8820164800 - 0.9063851833 0.8975619078 0.8825262785 - 0.9067893028 0.8980239034 0.8830360174 - 0.9071872830 0.8984869719 0.8835430741 - 0.9075853229 0.8989502788 0.8840497136 - 0.9079833031 0.8994135857 0.8845561743 - 0.9083812833 0.8998768926 0.8850628138 - 0.9087793231 0.9003401995 0.8855693936 - 0.9091774225 0.9008035064 0.8860759735 - 0.9095754027 0.9012668133 0.8865826130 - 0.9099733829 0.9017301202 0.8870890737 - 0.9103714228 0.9021934271 0.8875957131 - 0.9107694030 0.9026566744 0.8881022930 - 0.9111673832 0.9031199813 0.8886088729 - 0.9115555286 0.9035797715 0.8891084790 - 0.9119430780 0.9040381908 0.8896042705 - 0.9123308063 0.9044964910 0.8901001811 - 0.9127184153 0.9049547911 0.8905960917 - 0.9131060839 0.9054132104 0.8910920024 - 0.9134936929 0.9058715105 0.8915879130 - 0.9138813019 0.9063298106 0.8920838237 - 0.9142689705 0.9067881703 0.8925796747 - 0.9146565795 0.9072464705 0.8930755854 - 0.9150443077 0.9077047706 0.8935714960 - 0.9154319167 0.9081631899 0.8940674067 - 0.9158164263 0.9086192250 0.8945624232 - 0.9161984921 0.9090703130 0.8950551748 - 0.9165806770 0.9095214009 0.8955479264 - 0.9169628024 0.9099724889 0.8960406780 - 0.9173449874 0.9104235768 0.8965334892 - 0.9177271724 0.9108747244 0.8970261812 - 0.9181092978 0.9113258123 0.8975189924 - 0.9184914827 0.9117770195 0.8980118036 - 0.9188736081 0.9122281075 0.8985044956 - 0.9192557931 0.9126791954 0.8989973068 - 0.9196379781 0.9131302834 0.8994901180 - 0.9200199842 0.9135813713 0.8999828100 - 0.9203976989 0.9140260220 0.9004676938 - 0.9207754731 0.9144688249 0.9009500146 - 0.9211533070 0.9149116278 0.9014322758 - 0.9215310812 0.9153543711 0.9019145966 - 0.9219087958 0.9157971740 0.9023969173 - 0.9222866297 0.9162399769 0.9028791785 - 0.9226644039 0.9166827798 0.9033614993 - 0.9230421185 0.9171255827 0.9038438201 - 0.9234198928 0.9175683856 0.9043260813 - 0.9237977266 0.9180111885 0.9048084021 - 0.9241755009 0.9184539914 0.9052907228 - 0.9245520234 0.9188957214 0.9057729244 - 0.9249256849 0.9193243980 0.9062542915 - 0.9252995253 0.9197530150 0.9067355990 - 0.9256731868 0.9201816916 0.9072170258 - 0.9260470271 0.9206103086 0.9076983929 - 0.9264206886 0.9210389256 0.9081798196 - 0.9267945290 0.9214676023 0.9086611271 - 0.9271683097 0.9218962193 0.9091424942 - 0.9275419712 0.9223248959 0.9096239209 - 0.9279158115 0.9227535129 0.9101052284 - 0.9282894731 0.9231821895 0.9105865955 - 0.9286633134 0.9236108065 0.9110680223 - 0.9290362000 0.9240378141 0.9115486741 - 0.9294080734 0.9244576097 0.9120259881 - 0.9297800064 0.9248775244 0.9125033021 - 0.9301518798 0.9252973199 0.9129806757 - 0.9305236936 0.9257171154 0.9134579897 - 0.9308956265 0.9261369109 0.9139353037 - 0.9312674999 0.9265568256 0.9144126773 - 0.9316393733 0.9269766212 0.9148899913 - 0.9320111871 0.9273964167 0.9153673053 - 0.9323831201 0.9278162718 0.9158446193 - 0.9327549934 0.9282361269 0.9163219929 - 0.9331269264 0.9286559224 0.9167993069 - 0.9334965944 0.9290750027 0.9172763824 - 0.9338625073 0.9294890165 0.9177514911 - 0.9342283010 0.9299029708 0.9182265997 - 0.9345940948 0.9303171039 0.9187017083 - 0.9349600077 0.9307311177 0.9191766977 - 0.9353258014 0.9311450720 0.9196518064 - 0.9356915951 0.9315592051 0.9201269150 - 0.9360575080 0.9319732189 0.9206020236 - 0.9364233017 0.9323871732 0.9210770726 - 0.9367890954 0.9328011870 0.9215521812 - 0.9371550083 0.9332153201 0.9220272899 - 0.9375208020 0.9336292744 0.9225023985 - 0.9378861189 0.9340432882 0.9229773879 - 0.9382457137 0.9344478846 0.9234445095 - 0.9386053085 0.9348514080 0.9239102006 - 0.9389649034 0.9352548122 0.9243760109 - 0.9393244982 0.9356582165 0.9248417020 - 0.9396840930 0.9360616207 0.9253073931 - 0.9400436878 0.9364650846 0.9257732034 - 0.9404032826 0.9368684888 0.9262388945 - 0.9407628775 0.9372718930 0.9267047048 - 0.9411224723 0.9376752973 0.9271703959 - 0.9414821267 0.9380788207 0.9276362062 - 0.9418417215 0.9384822249 0.9281018972 - 0.9422013164 0.9388856292 0.9285677075 - 0.9425578713 0.9392852187 0.9290300012 - 0.9429129958 0.9396802187 0.9294880033 - 0.9432681203 0.9400752783 0.9299458861 - 0.9436231852 0.9404702783 0.9304038882 - 0.9439783096 0.9408653975 0.9308617711 - 0.9443333149 0.9412603974 0.9313197732 - 0.9446883798 0.9416555166 0.9317777753 - 0.9450435042 0.9420505762 0.9322357178 - 0.9453986287 0.9424455762 0.9326937199 - 0.9457536936 0.9428406954 0.9331517220 - 0.9461086988 0.9432356954 0.9336096048 - 0.9464638233 0.9436308146 0.9340676069 - 0.9468188286 0.9440258145 0.9345256090 - 0.9471713901 0.9444158077 0.9349750280 - 0.9475240111 0.9448049068 0.9354225993 - 0.9478765726 0.9451941252 0.9358702898 - 0.9482290745 0.9455832243 0.9363179803 - 0.9485816956 0.9459723234 0.9367656112 - 0.9489343166 0.9463614225 0.9372133017 - 0.9492868781 0.9467505217 0.9376609921 - 0.9496394992 0.9471396804 0.9381086826 - 0.9499921203 0.9475287795 0.9385563135 - 0.9503446817 0.9479178786 0.9390040040 - 0.9506971836 0.9483069777 0.9394516945 - 0.9510498047 0.9486961961 0.9398993254 - 0.9514018297 0.9490848184 0.9403468966 - 0.9517515898 0.9494650960 0.9407904744 - 0.9521014094 0.9498453736 0.9412341118 - 0.9524511099 0.9502257109 0.9416778088 - 0.9528009295 0.9506059885 0.9421213865 - 0.9531506896 0.9509863257 0.9425650239 - 0.9535005093 0.9513666034 0.9430087209 - 0.9538503289 0.9517468810 0.9434522986 - 0.9542000890 0.9521272182 0.9438959956 - 0.9545499086 0.9525074959 0.9443395734 - 0.9548997283 0.9528877735 0.9447832108 - 0.9552494287 0.9532681704 0.9452269077 - 0.9555991888 0.9536485076 0.9456704855 - 0.9559475183 0.9540281892 0.9461137056 - 0.9562901855 0.9543992281 0.9465476871 - 0.9566329122 0.9547703266 0.9469817877 - 0.9569756985 0.9551414251 0.9474158287 - 0.9573184252 0.9555124044 0.9478498101 - 0.9576610923 0.9558835030 0.9482839108 - 0.9580038786 0.9562546015 0.9487178922 - 0.9583466053 0.9566255808 0.9491519928 - 0.9586892724 0.9569966793 0.9495859742 - 0.9590321183 0.9573677778 0.9500200152 - 0.9593747854 0.9577388167 0.9504541159 - 0.9597175121 0.9581099153 0.9508880973 - 0.9600601792 0.9584810138 0.9513220787 - 0.9604027271 0.9588519931 0.9517561793 - 0.9607350230 0.9592133760 0.9521834254 - 0.9610671997 0.9595736265 0.9526095986 - 0.9613994956 0.9599336982 0.9530358911 - 0.9617316723 0.9602938294 0.9534621239 - 0.9620639086 0.9606539011 0.9538882971 - 0.9623962045 0.9610139728 0.9543144703 - 0.9627283812 0.9613741040 0.9547407031 - 0.9630606174 0.9617341757 0.9551668763 - 0.9633929133 0.9620944262 0.9555931091 - 0.9637250900 0.9624544978 0.9560192823 - 0.9640573859 0.9628146291 0.9564455152 - 0.9643896222 0.9631747007 0.9568716884 - 0.9647217989 0.9635347724 0.9572979212 - 0.9650493860 0.9638900161 0.9577190876 - 0.9653742909 0.9642401934 0.9581351280 - 0.9656991959 0.9645903707 0.9585509896 - 0.9660241008 0.9649404883 0.9589669704 - 0.9663490057 0.9652907252 0.9593828917 - 0.9666739106 0.9656409025 0.9597988725 - 0.9669988155 0.9659910798 0.9602147937 - 0.9673237205 0.9663413167 0.9606307745 - 0.9676485062 0.9666914940 0.9610466957 - 0.9679734111 0.9670416713 0.9614626765 - 0.9682983160 0.9673919082 0.9618785977 - 0.9686232209 0.9677420855 0.9622945786 - 0.9689481258 0.9680922031 0.9627104998 - 0.9692727923 0.9684423804 0.9631264806 - 0.9695920944 0.9687845111 0.9635347724 - 0.9699112773 0.9691258073 0.9639422894 - 0.9702305794 0.9694671035 0.9643498063 - 0.9705498815 0.9698083997 0.9647573233 - 0.9708691835 0.9701496959 0.9651647806 - 0.9711884260 0.9704909921 0.9655722976 - 0.9715077281 0.9708322883 0.9659798741 - 0.9718269706 0.9711735845 0.9663873911 - 0.9721462131 0.9715148807 0.9667949080 - 0.9724655151 0.9718561769 0.9672024250 - 0.9727848172 0.9721974730 0.9676098824 - 0.9731040001 0.9725388288 0.9680173993 - 0.9734233022 0.9728801250 0.9684249163 - 0.9737418890 0.9732202291 0.9688311219 - 0.9740585089 0.9735528827 0.9692280293 - 0.9743750095 0.9738854766 0.9696248770 - 0.9746916294 0.9742181897 0.9700219035 - 0.9750080705 0.9745507836 0.9704188108 - 0.9753245711 0.9748834968 0.9708157182 - 0.9756411910 0.9752160907 0.9712126255 - 0.9759576917 0.9755488038 0.9716095924 - 0.9762743115 0.9758813977 0.9720064998 - 0.9765908122 0.9762141109 0.9724034071 - 0.9769073129 0.9765467048 0.9728003144 - 0.9772238731 0.9768794179 0.9731972814 - 0.9775403738 0.9772120118 0.9735941887 - 0.9778568745 0.9775447249 0.9739910960 - 0.9781730771 0.9778758287 0.9743868709 - 0.9784877896 0.9781979918 0.9747753739 - 0.9788025022 0.9785202742 0.9751638770 - 0.9791172147 0.9788424969 0.9755523801 - 0.9794319868 0.9791647196 0.9759408832 - 0.9797466993 0.9794870019 0.9763293862 - 0.9800614119 0.9798092246 0.9767180085 - 0.9803761244 0.9801315069 0.9771065116 - 0.9806908965 0.9804537296 0.9774950147 - 0.9810056090 0.9807758927 0.9778835177 - 0.9813203216 0.9810981750 0.9782720208 - 0.9816349745 0.9814203978 0.9786605239 - 0.9819498062 0.9817426801 0.9790490866 - 0.9822645187 0.9820649028 0.9794375896 - 0.9825791121 0.9823871255 0.9798260927 - 0.9828888774 0.9827020764 0.9802024961 - 0.9831985831 0.9830166101 0.9805781245 - 0.9835082889 0.9833310843 0.9809536934 - 0.9838181138 0.9836456776 0.9813293219 - 0.9841278195 0.9839602113 0.9817048907 - 0.9844375253 0.9842746854 0.9820805192 - 0.9847471714 0.9845892191 0.9824560881 - 0.9850569963 0.9849038124 0.9828317761 - 0.9853667021 0.9852182865 0.9832074046 - 0.9856764078 0.9855328202 0.9835829735 - 0.9859861732 0.9858472943 0.9839586020 - 0.9862958789 0.9861618876 0.9843341708 - 0.9866055846 0.9864764214 0.9847097993 - 0.9869154096 0.9867908955 0.9850854278 - 0.9872199893 0.9871007800 0.9854502082 - 0.9875209928 0.9874067903 0.9858055115 - 0.9878221154 0.9877126813 0.9861608148 - 0.9881231189 0.9880186915 0.9865159988 - 0.9884241223 0.9883245826 0.9868713021 - 0.9887251854 0.9886305928 0.9872264862 - 0.9890261889 0.9889364839 0.9875817895 - 0.9893273115 0.9892424941 0.9879369736 - 0.9896283150 0.9895483851 0.9882922769 - 0.9899293780 0.9898543954 0.9886475801 - 0.9902303815 0.9901602864 0.9890028238 - 0.9905313849 0.9904662967 0.9893581271 - 0.9908325076 0.9907721877 0.9897133112 - 0.9911335111 0.9910781980 0.9900686145 - 0.9914345741 0.9913840890 0.9904237986 - 0.9917227030 0.9916865230 0.9907621741 - 0.9920099974 0.9919885993 0.9910988212 - 0.9922972918 0.9922906160 0.9914355278 - 0.9925847054 0.9925926924 0.9917721152 - 0.9928719997 0.9928947091 0.9921088219 - 0.9931594133 0.9931967854 0.9924454093 - 0.9934467077 0.9934988022 0.9927819967 - 0.9937341213 0.9938008785 0.9931187034 - 0.9940214157 0.9941028953 0.9934552908 - 0.9943088293 0.9944049716 0.9937918782 - 0.9945961237 0.9947069883 0.9941285849 - 0.9948834777 0.9950091243 0.9944651723 - 0.9951707721 0.9953110814 0.9948018789 - 0.9954581857 0.9956132174 0.9951385260 - 0.9957451820 0.9959133267 0.9954729080 - 0.9960289001 0.9961857200 0.9957746863 - 0.9963124990 0.9964581728 0.9960765243 - 0.9965962172 0.9967306256 0.9963783026 - 0.9968798161 0.9970030785 0.9966800809 - 0.9971634746 0.9972754717 0.9969819188 - 0.9974470735 0.9975479841 0.9972836971 - 0.9977307916 0.9978203773 0.9975854754 - 0.9980143905 0.9980928898 0.9978873134 - 0.9982981086 0.9983652830 0.9981892109 - 0.9985817075 0.9986377954 0.9984909892 - 0.9988654256 0.9989101887 0.9987928271 - 0.9991490245 0.9991827011 0.9990946054 - 0.9994326830 0.9994550943 0.9993963838 - 0.9997162819 0.9997276068 0.9996982217 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0174498409 0.0158984791 0.0114764702 + 0.0361074992 0.0300892591 0.0227251798 + 0.0508534499 0.0413388014 0.0331778601 + 0.0630743131 0.0511820987 0.0424059182 + 0.0737055317 0.0595689416 0.0507076010 + 0.0831899792 0.0670762509 0.0583669208 + 0.0918336809 0.0739420429 0.0653621703 + 0.0999075696 0.0803586915 0.0719582215 + 0.1073999032 0.0863586813 0.0781143606 + 0.1143994033 0.0919513777 0.0840181410 + 0.1210075021 0.0972131789 0.0896857306 + 0.1272652000 0.1023328975 0.0950955227 + 0.1332022995 0.1073141024 0.1002841964 + 0.1389860064 0.1120809987 0.1052708998 + 0.1446685940 0.1166312024 0.1100749969 + 0.1502207965 0.1210201979 0.1147160009 + 0.1556082070 0.1252893060 0.1191812009 + 0.1608344018 0.1294762045 0.1235382035 + 0.1659597009 0.1336127967 0.1277814955 + 0.1709610969 0.1376406997 0.1319573969 + 0.1758396029 0.1415354013 0.1360505968 + 0.1805783063 0.1453579962 0.1400354058 + 0.1851565987 0.1491020024 0.1439131051 + 0.1896200031 0.1527583003 0.1477226019 + 0.1940072030 0.1563414037 0.1514364034 + 0.1983578950 0.1599265933 0.1550768018 + 0.2026146054 0.1634611934 0.1586581022 + 0.2068161964 0.1669549942 0.1621633023 + 0.2109510005 0.1704287976 0.1656109989 + 0.2149749994 0.1738846004 0.1689939946 + 0.2188791931 0.1773294955 0.1723017991 + 0.2226788998 0.1807734072 0.1755644977 + 0.2264375985 0.1841789037 0.1787340939 + 0.2301400006 0.1875509024 0.1818515062 + 0.2337515056 0.1908915937 0.1848967969 + 0.2373262048 0.1941868067 0.1878945976 + 0.2408698052 0.1974609047 0.1908354014 + 0.2443857044 0.2007011026 0.1937474012 + 0.2478293926 0.2038740963 0.1966456026 + 0.2511906028 0.2069949955 0.1995196939 + 0.2545030117 0.2100716978 0.2023606002 + 0.2577551007 0.2130948007 0.2052040994 + 0.2609705925 0.2161156982 0.2080169022 + 0.2641761005 0.2191050053 0.2107931972 + 0.2673386037 0.2220419943 0.2135623991 + 0.2704797983 0.2249206007 0.2162752002 + 0.2735692859 0.2277640998 0.2189542055 + 0.2766304910 0.2305577993 0.2216016054 + 0.2796724141 0.2333119065 0.2241947949 + 0.2826831937 0.2360485047 0.2267569005 + 0.2856605947 0.2387613952 0.2292927057 + 0.2885996997 0.2414368987 0.2317842990 + 0.2914834917 0.2441027015 0.2342378944 + 0.2943134904 0.2467696965 0.2366532981 + 0.2971065044 0.2494162023 0.2390290052 + 0.2998841107 0.2520292997 0.2413756996 + 0.3026126027 0.2546142042 0.2437071949 + 0.3053067923 0.2571905851 0.2460273951 + 0.3079755008 0.2597542107 0.2483306974 + 0.3106071949 0.2622892857 0.2506220937 + 0.3132098019 0.2647835910 0.2528899908 + 0.3157896101 0.2672505081 0.2551301122 + 0.3183388114 0.2697114050 0.2573640943 + 0.3208617866 0.2721551955 0.2595869899 + 0.3233729005 0.2745685875 0.2617990971 + 0.3258588910 0.2769567966 0.2639977038 + 0.3283306956 0.2793405056 0.2661921084 + 0.3308002055 0.2817173004 0.2683886886 + 0.3332616091 0.2840746939 0.2705698907 + 0.3356887102 0.2864069939 0.2727307081 + 0.3380911052 0.2887319922 0.2748725116 + 0.3404774070 0.2910597026 0.2769970894 + 0.3428531885 0.2933830023 0.2791089118 + 0.3452278078 0.2956911922 0.2812050879 + 0.3475916088 0.2979753911 0.2832851112 + 0.3499327898 0.3002283871 0.2853564918 + 0.3522517085 0.3024463058 0.2874124944 + 0.3545554876 0.3046588004 0.2894454896 + 0.3568528891 0.3068628013 0.2914513052 + 0.3591203094 0.3090505004 0.2934390903 + 0.3613556027 0.3112153113 0.2954114974 + 0.3635644913 0.3133526146 0.2973645031 + 0.3657538891 0.3154605031 0.2992906868 + 0.3679293990 0.3175630867 0.3012037873 + 0.3700731993 0.3196572065 0.3031010032 + 0.3722012937 0.3217284083 0.3049888909 + 0.3743169010 0.3237737119 0.3068724871 + 0.3764213026 0.3257966936 0.3087452054 + 0.3785130084 0.3278082013 0.3106024861 + 0.3805955052 0.3298071027 0.3124460876 + 0.3826780915 0.3317958117 0.3142769933 + 0.3847546875 0.3337770998 0.3160952926 + 0.3867942095 0.3357304037 0.3179017007 + 0.3888104856 0.3376702964 0.3196955025 + 0.3908145130 0.3396002054 0.3214753866 + 0.3928089142 0.3415268064 0.3232490122 + 0.3947995007 0.3434461057 0.3250178993 + 0.3967804909 0.3453572989 0.3267830908 + 0.3987523913 0.3472534120 0.3285475075 + 0.4007174075 0.3491292894 0.3303103149 + 0.4026708007 0.3509947956 0.3320707083 + 0.4046143889 0.3528488874 0.3338232934 + 0.4065484107 0.3546791077 0.3355642855 + 0.4084819853 0.3565005064 0.3372963965 + 0.4104149938 0.3583168089 0.3390068114 + 0.4123412967 0.3601278961 0.3407028019 + 0.4142560065 0.3619349003 0.3423874974 + 0.4161522090 0.3637281060 0.3440541029 + 0.4180339873 0.3655182123 0.3457126915 + 0.4199067950 0.3673050106 0.3473584950 + 0.4217523932 0.3690750003 0.3489893973 + 0.4235874116 0.3708429039 0.3506138027 + 0.4254117906 0.3726145029 0.3522236049 + 0.4272232056 0.3743841052 0.3538290858 + 0.4290306866 0.3761518002 0.3554284871 + 0.4308294058 0.3779003918 0.3570241034 + 0.4326210022 0.3796432018 0.3586181998 + 0.4344018102 0.3813720942 0.3602060080 + 0.4361707866 0.3830938041 0.3617906868 + 0.4379333854 0.3848012090 0.3633658886 + 0.4396809936 0.3864955008 0.3649328053 + 0.4414246082 0.3881793916 0.3664895892 + 0.4431430995 0.3898509145 0.3680340052 + 0.4448564947 0.3915182054 0.3695713878 + 0.4465528131 0.3931697011 0.3710972071 + 0.4482432008 0.3948189020 0.3726201952 + 0.4499239028 0.3964514136 0.3741200864 + 0.4515936971 0.3980818987 0.3756181896 + 0.4532519877 0.3997015953 0.3771077991 + 0.4548968971 0.4013178945 0.3785969913 + 0.4565317035 0.4029234946 0.3800840974 + 0.4581527114 0.4045226872 0.3815703094 + 0.4597654939 0.4061067104 0.3830527067 + 0.4613632858 0.4076885879 0.3845353127 + 0.4629530907 0.4092662036 0.3860189021 + 0.4645408094 0.4108397961 0.3874995112 + 0.4661273062 0.4124050140 0.3889681995 + 0.4677076936 0.4139649868 0.3904353082 + 0.4692842960 0.4155119956 0.3918946981 + 0.4708591998 0.4170548022 0.3933512866 + 0.4724324942 0.4185824096 0.3947859108 + 0.4739956856 0.4201084077 0.3962202072 + 0.4755468965 0.4216217101 0.3976345956 + 0.4770910144 0.4231351018 0.3990488946 + 0.4786213040 0.4246394038 0.4004501104 + 0.4801476002 0.4261435866 0.4018495977 + 0.4816561043 0.4276433885 0.4032416940 + 0.4831644893 0.4291422963 0.4046311080 + 0.4846526980 0.4306376874 0.4060145020 + 0.4861406982 0.4321312904 0.4073927999 + 0.4876179993 0.4336220026 0.4087674022 + 0.4890919924 0.4351086915 0.4101338089 + 0.4905653000 0.4365926087 0.4114997089 + 0.4920378029 0.4380624890 0.4128586948 + 0.4935061932 0.4395323992 0.4142177105 + 0.4949601889 0.4409978986 0.4155749083 + 0.4964140952 0.4424628019 0.4169315994 + 0.4978365004 0.4439246953 0.4182890952 + 0.4992552996 0.4453840852 0.4196475148 + 0.5006620884 0.4468388855 0.4210050106 + 0.5020588040 0.4482730925 0.4223509133 + 0.5034533739 0.4497072101 0.4236967862 + 0.5048360825 0.4511240125 0.4250375032 + 0.5062186718 0.4525361061 0.4263758063 + 0.5075957179 0.4539420903 0.4277085066 + 0.5089703798 0.4553368986 0.4290247858 + 0.5103421211 0.4567317963 0.4303410053 + 0.5117046833 0.4581159055 0.4316442013 + 0.5130673051 0.4594987035 0.4329437912 + 0.5144175887 0.4608711898 0.4342381954 + 0.5157638192 0.4622273147 0.4355193973 + 0.5171049237 0.4635834098 0.4368005097 + 0.5184270144 0.4649255872 0.4380722940 + 0.5197492242 0.4662652016 0.4393407106 + 0.5210537910 0.4676026106 0.4406091869 + 0.5223482847 0.4689337909 0.4418779016 + 0.5236423016 0.4702650905 0.4431464970 + 0.5249115229 0.4715861082 0.4444108009 + 0.5261808038 0.4729016125 0.4456712902 + 0.5274490118 0.4742170870 0.4469316900 + 0.5287154913 0.4755303860 0.4481886029 + 0.5299819708 0.4768437147 0.4494448900 + 0.5312492251 0.4781504869 0.4506979883 + 0.5325167775 0.4794408083 0.4519374073 + 0.5337843895 0.4807311893 0.4531767964 + 0.5350342989 0.4820165038 0.4544104934 + 0.5362828970 0.4832974970 0.4556373060 + 0.5375289917 0.4845784903 0.4568639994 + 0.5387613177 0.4858368039 0.4580819011 + 0.5399935842 0.4870882928 0.4592955112 + 0.5412209034 0.4883398116 0.4605090916 + 0.5424383879 0.4895803928 0.4617215991 + 0.5436558723 0.4908204079 0.4629341066 + 0.5448678136 0.4920594990 0.4641464949 + 0.5460734963 0.4932895899 0.4653517008 + 0.5472792983 0.4945197105 0.4665569961 + 0.5484790802 0.4957481027 0.4677614868 + 0.5496746898 0.4969693124 0.4689593911 + 0.5508701801 0.4981904924 0.4701572061 + 0.5520610809 0.4994137883 0.4713543952 + 0.5532492995 0.5006437898 0.4725480974 + 0.5544373989 0.5018737912 0.4737417102 + 0.5556218028 0.5030999184 0.4749346972 + 0.5568041205 0.5043138862 0.4761235118 + 0.5579863191 0.5055277944 0.4773122966 + 0.5591588020 0.5067374706 0.4785006046 + 0.5603247285 0.5079305172 0.4796842933 + 0.5614907146 0.5091236234 0.4808681011 + 0.5626431108 0.5103173256 0.4820516109 + 0.5637829900 0.5115153193 0.4832226932 + 0.5649229884 0.5127133131 0.4843936861 + 0.5660511851 0.5139113069 0.4855647981 + 0.5671615005 0.5151088834 0.4867219925 + 0.5682718158 0.5163065195 0.4878776968 + 0.5693790913 0.5175040960 0.4890333116 + 0.5704758167 0.5186815858 0.4901837111 + 0.5715726018 0.5198547244 0.4913321137 + 0.5726693273 0.5210276842 0.4924804866 + 0.5737720132 0.5221964717 0.4936192930 + 0.5748748183 0.5233623981 0.4947485924 + 0.5759775043 0.5245283246 0.4958778918 + 0.5770760775 0.5256924033 0.4970034957 + 0.5781732202 0.5268526077 0.4981164932 + 0.5792703032 0.5280128121 0.4992294014 + 0.5803601146 0.5291728973 0.5003424287 + 0.5814397931 0.5303236246 0.5014445186 + 0.5825194120 0.5314744115 0.5025454760 + 0.5835987926 0.5326251984 0.5036464930 + 0.5846728086 0.5337677002 0.5047429800 + 0.5857468843 0.5349056125 0.5058357120 + 0.5868209004 0.5360434055 0.5069283843 + 0.5878757238 0.5371788144 0.5080201030 + 0.5889192820 0.5383052826 0.5091047883 + 0.5899627805 0.5394318104 0.5101894736 + 0.5910035968 0.5405583978 0.5112742186 + 0.5920301080 0.5416883826 0.5123528838 + 0.5930566788 0.5428196192 0.5134283900 + 0.5940833092 0.5439507961 0.5145038962 + 0.5950983167 0.5450789928 0.5155786872 + 0.5961067080 0.5461968184 0.5166484118 + 0.5971152186 0.5473145843 0.5177180767 + 0.5981239080 0.5484325290 0.5187878013 + 0.5991356969 0.5495414734 0.5198484063 + 0.6001476049 0.5506458879 0.5209016204 + 0.6011593938 0.5517503023 0.5219547153 + 0.6021643281 0.5528541207 0.5230079293 + 0.6031603217 0.5539363027 0.5240486860 + 0.6041563749 0.5550184250 0.5250884295 + 0.6051524282 0.5561006069 0.5261281729 + 0.6061363816 0.5571805835 0.5271657705 + 0.6071165204 0.5582563877 0.5281966925 + 0.6080965996 0.5593320727 0.5292276144 + 0.6090764999 0.5604078770 0.5302584767 + 0.6100435257 0.5614696145 0.5312880278 + 0.6110104918 0.5625219941 0.5323162079 + 0.6119775176 0.5635743737 0.5333443880 + 0.6129397154 0.5646268129 0.5343726277 + 0.6138859987 0.5656726956 0.5353969932 + 0.6148322821 0.5667169094 0.5364199877 + 0.6157786250 0.5677611828 0.5374429226 + 0.6167212725 0.5688053966 0.5384659171 + 0.6176583171 0.5698326230 0.5394816995 + 0.6185953021 0.5708590150 0.5404965281 + 0.6195322871 0.5718854070 0.5415111780 + 0.6204634905 0.5729113817 0.5425258875 + 0.6213884950 0.5739294291 0.5435299873 + 0.6223133802 0.5749472976 0.5445337296 + 0.6232383251 0.5759652853 0.5455372930 + 0.6241629124 0.5769823194 0.5465409160 + 0.6250872016 0.5779896975 0.5475350022 + 0.6260113716 0.5789970756 0.5485289097 + 0.6269357204 0.5800045133 0.5495228171 + 0.6278557777 0.5810111165 0.5505167842 + 0.6287717223 0.5820034742 0.5515152216 + 0.6296874881 0.5829960108 0.5525137782 + 0.6306033731 0.5839884281 0.5535123944 + 0.6315113902 0.5849809051 0.5545110106 + 0.6324080229 0.5859525204 0.5554962158 + 0.6333045959 0.5869235992 0.5564795732 + 0.6342011094 0.5878946185 0.5574629903 + 0.6350932121 0.5888655782 0.5584464073 + 0.6359726787 0.5898336172 0.5594201088 + 0.6368522048 0.5908008814 0.5603901744 + 0.6377316713 0.5917682052 0.5613601804 + 0.6386104822 0.5927355289 0.5623303056 + 0.6394773126 0.5936999917 0.5632957816 + 0.6403442025 0.5946627259 0.5642570257 + 0.6412109733 0.5956255198 0.5652183294 + 0.6420779228 0.5965881944 0.5661795735 + 0.6429408789 0.5975462794 0.5671393871 + 0.6438030005 0.5984956026 0.5680949092 + 0.6446650028 0.5994448066 0.5690503120 + 0.6455270052 0.6003941298 0.5700057745 + 0.6463868022 0.6013432741 0.5709612966 + 0.6472437978 0.6022868752 0.5719025135 + 0.6481007934 0.6032304764 0.5728424788 + 0.6489577889 0.6041740775 0.5737825036 + 0.6498141289 0.6051176786 0.5747225285 + 0.6506562233 0.6060569286 0.5756577849 + 0.6514983177 0.6069930792 0.5765889287 + 0.6523404717 0.6079294086 0.5775198936 + 0.6531826258 0.6088656783 0.5784509182 + 0.6540184021 0.6098006964 0.5793818235 + 0.6548498273 0.6107280850 0.5803099871 + 0.6556811929 0.6116555929 0.5812382102 + 0.6565124989 0.6125831008 0.5821664929 + 0.6573433876 0.6135106087 0.5830947161 + 0.6581645012 0.6144319773 0.5840185881 + 0.6589856148 0.6153494716 0.5849387050 + 0.6598067284 0.6162670255 0.5858588219 + 0.6606277823 0.6171844006 0.5867788792 + 0.6614431739 0.6181018949 0.5876989961 + 0.6622517705 0.6190115213 0.5886114240 + 0.6630604267 0.6199212074 0.5895230770 + 0.6638690233 0.6208307743 0.5904347897 + 0.6646776199 0.6217405200 0.5913465023 + 0.6654757261 0.6226496100 0.5922542214 + 0.6662709117 0.6235576868 0.5931503177 + 0.6670660973 0.6244658232 0.5940464139 + 0.6678612828 0.6253738999 0.5949425101 + 0.6686561704 0.6262819767 0.5958386064 + 0.6694442034 0.6271823049 0.5967332721 + 0.6702321768 0.6280776262 0.5976269245 + 0.6710202098 0.6289728880 0.5985203981 + 0.6718081832 0.6298682094 0.5994138718 + 0.6725955009 0.6307634711 0.6003074050 + 0.6733806133 0.6316475272 0.6011890769 + 0.6741657257 0.6325287223 0.6020658016 + 0.6749507189 0.6334097981 0.6029425859 + 0.6757357717 0.6342908740 0.6038193703 + 0.6765164137 0.6351721287 0.6046960950 + 0.6772890091 0.6360508800 0.6055694818 + 0.6780614853 0.6369295716 0.6064420938 + 0.6788340807 0.6378083229 0.6073147058 + 0.6796066165 0.6386870146 0.6081873775 + 0.6803761721 0.6395655870 0.6090599895 + 0.6811413169 0.6404411793 0.6099300981 + 0.6819062829 0.6413165927 0.6107999086 + 0.6826714277 0.6421920061 0.6116697192 + 0.6834365129 0.6430674195 0.6125395298 + 0.6841984987 0.6439427733 0.6134092808 + 0.6849551797 0.6448048949 0.6142743230 + 0.6857119203 0.6456660032 0.6151384115 + 0.6864687204 0.6465271115 0.6160026193 + 0.6872254014 0.6473882198 0.6168667078 + 0.6879792213 0.6482493281 0.6177309155 + 0.6887251735 0.6491037011 0.6185951233 + 0.6894711256 0.6499567032 0.6194592714 + 0.6902170777 0.6508097053 0.6203234792 + 0.6909629703 0.6516628265 0.6211876869 + 0.6917080283 0.6525158286 0.6220518947 + 0.6924436092 0.6533647776 0.6229112148 + 0.6931793094 0.6542117000 0.6237667799 + 0.6939148903 0.6550586224 0.6246222854 + 0.6946504712 0.6559054852 0.6254779100 + 0.6953861117 0.6567524076 0.6263334155 + 0.6961168051 0.6575965285 0.6271870732 + 0.6968464851 0.6584361792 0.6280364990 + 0.6975762844 0.6592760086 0.6288859248 + 0.6983060241 0.6601157188 0.6297354102 + 0.6990358233 0.6609554887 0.6305847764 + 0.6997650862 0.6617947817 0.6314343214 + 0.7004939914 0.6626260877 0.6322728992 + 0.7012230158 0.6634575129 0.6331112981 + 0.7019519210 0.6642888784 0.6339496970 + 0.7026808858 0.6651203036 0.6347880960 + 0.7034088969 0.6659517288 0.6356264949 + 0.7041271925 0.6667770743 0.6364635825 + 0.7048454881 0.6675990224 0.6372997165 + 0.7055637836 0.6684209108 0.6381359100 + 0.7062821984 0.6692429185 0.6389719844 + 0.7070004940 0.6700648069 0.6398081183 + 0.7077152729 0.6708840132 0.6406437755 + 0.7084274888 0.6716861129 0.6414728165 + 0.7091398239 0.6724882722 0.6423017979 + 0.7098520994 0.6732904911 0.6431307793 + 0.7105643749 0.6740925908 0.6439598203 + 0.7112765908 0.6748948097 0.6447888017 + 0.7119773030 0.6756910086 0.6456167102 + 0.7126780152 0.6764826775 0.6464434862 + 0.7133787870 0.6772742867 0.6472703218 + 0.7140794992 0.6780658960 0.6480970979 + 0.7147802711 0.6788576245 0.6489239931 + 0.7154778838 0.6796491742 0.6497508287 + 0.7161695957 0.6804391742 0.6505677104 + 0.7168614268 0.6812291145 0.6513826251 + 0.7175530791 0.6820189953 0.6521974802 + 0.7182449102 0.6828088164 0.6530123949 + 0.7189366221 0.6835986972 0.6538271904 + 0.7196236253 0.6843873262 0.6546415091 + 0.7203075290 0.6851689219 0.6554480195 + 0.7209913731 0.6859505177 0.6562544703 + 0.7216752768 0.6867321730 0.6570609808 + 0.7223592997 0.6875138283 0.6578674912 + 0.7230432034 0.6882954240 0.6586740017 + 0.7237169147 0.6890760064 0.6594784856 + 0.7243875265 0.6898545027 0.6602771878 + 0.7250581980 0.6906331182 0.6610758901 + 0.7257289290 0.6914116144 0.6618745923 + 0.7263996005 0.6921901107 0.6626732945 + 0.7270702124 0.6929686069 0.6634719968 + 0.7277340293 0.6937432885 0.6642683744 + 0.7283964753 0.6945124269 0.6650602221 + 0.7290591002 0.6952815056 0.6658521295 + 0.7297216058 0.6960507035 0.6666439176 + 0.7303841710 0.6968197823 0.6674357057 + 0.7310466766 0.6975889206 0.6682276130 + 0.7317048907 0.6983528733 0.6690183282 + 0.7323622108 0.6991090178 0.6698067784 + 0.7330195904 0.6998651028 0.6705952883 + 0.7336769104 0.7006211877 0.6713837981 + 0.7343342900 0.7013772726 0.6721723080 + 0.7349916101 0.7021334171 0.6729608178 + 0.7356424928 0.7028859258 0.6737465262 + 0.7362912297 0.7036299109 0.6745222211 + 0.7369397879 0.7043738961 0.6752979755 + 0.7375885248 0.7051178217 0.6760736704 + 0.7382372022 0.7058618069 0.6768494844 + 0.7388858795 0.7066057920 0.6776251793 + 0.7395280004 0.7073493004 0.6784006953 + 0.7401649952 0.7080891132 0.6791678071 + 0.7408019900 0.7088289857 0.6799349785 + 0.7414389849 0.7095689178 0.6807020903 + 0.7420759201 0.7103086710 0.6814693213 + 0.7427129149 0.7110486031 0.6822363734 + 0.7433465123 0.7117884159 0.6830036044 + 0.7439708710 0.7125198841 0.6837611794 + 0.7445952892 0.7132496834 0.6845157146 + 0.7452197075 0.7139794230 0.6852701902 + 0.7458441854 0.7147092223 0.6860247254 + 0.7464686036 0.7154390216 0.6867792010 + 0.7470930219 0.7161687016 0.6875336766 + 0.7477155924 0.7168959975 0.6882864833 + 0.7483379841 0.7176201940 0.6890364885 + 0.7489603758 0.7183443904 0.6897866130 + 0.7495828271 0.7190685868 0.6905366182 + 0.7502052188 0.7197927833 0.6912866831 + 0.7508276105 0.7205169797 0.6920368075 + 0.7514470816 0.7212411761 0.6927868128 + 0.7520624995 0.7219575047 0.6935216784 + 0.7526779771 0.7226737142 0.6942551136 + 0.7532935143 0.7233898044 0.6949886084 + 0.7539088726 0.7241060138 0.6957219839 + 0.7545244098 0.7248222232 0.6964554191 + 0.7551398873 0.7255383730 0.6971887946 + 0.7557541132 0.7262521982 0.6979203224 + 0.7563681006 0.7269626856 0.6986479163 + 0.7569820881 0.7276732922 0.6993755102 + 0.7575961947 0.7283838987 0.7001032233 + 0.7582101822 0.7290943861 0.7008308172 + 0.7588242292 0.7298049927 0.7015584707 + 0.7594367266 0.7305154800 0.7022861242 + 0.7600427866 0.7312164903 0.7030051947 + 0.7606487870 0.7319142818 0.7037203908 + 0.7612549067 0.7326120138 0.7044355273 + 0.7618610263 0.7333098054 0.7051507235 + 0.7624670267 0.7340075970 0.7058659196 + 0.7630730867 0.7347053885 0.7065809965 + 0.7636762857 0.7354031801 0.7072961926 + 0.7642757297 0.7360877991 0.7080059052 + 0.7648751140 0.7367721796 0.7087152004 + 0.7654743791 0.7374566197 0.7094244957 + 0.7660738230 0.7381410003 0.7101337910 + 0.7666732073 0.7388253808 0.7108430266 + 0.7672724724 0.7395098805 0.7115523219 + 0.7678692937 0.7401931286 0.7122616172 + 0.7684640288 0.7408679724 0.7129713297 + 0.7690587044 0.7415428758 0.7136811018 + 0.7696534991 0.7422178984 0.7143908143 + 0.7702481747 0.7428928018 0.7151005268 + 0.7708429098 0.7435677052 0.7158101797 + 0.7714375854 0.7442426085 0.7165200114 + 0.7720239162 0.7449169755 0.7172291875 + 0.7726042271 0.7455884814 0.7179328203 + 0.7731844783 0.7462599874 0.7186363935 + 0.7737647891 0.7469314933 0.7193400264 + 0.7743450999 0.7476029992 0.7200435996 + 0.7749254107 0.7482745051 0.7207471728 + 0.7755057216 0.7489460111 0.7214508057 + 0.7760874033 0.7496169209 0.7221543193 + 0.7766703963 0.7502800822 0.7228549123 + 0.7772535086 0.7509431839 0.7235553861 + 0.7778365016 0.7516064048 0.7242559195 + 0.7784196138 0.7522696257 0.7249565125 + 0.7790026069 0.7529327869 0.7256569862 + 0.7795857191 0.7535960078 0.7263575196 + 0.7801663876 0.7542592287 0.7270581126 + 0.7807425261 0.7549132109 0.7277507782 + 0.7813186049 0.7555662990 0.7284420729 + 0.7818946838 0.7562193871 0.7291333079 + 0.7824708223 0.7568724751 0.7298246026 + 0.7830467820 0.7575256228 0.7305157781 + 0.7836229205 0.7581787109 0.7312070727 + 0.7841988206 0.7588317990 0.7318983078 + 0.7847698927 0.7594774961 0.7325916290 + 0.7853410840 0.7601187229 0.7332865000 + 0.7859122157 0.7607598901 0.7339814901 + 0.7864832878 0.7614011168 0.7346764207 + 0.7870544791 0.7620422840 0.7353712916 + 0.7876256108 0.7626835108 0.7360662818 + 0.7881966829 0.7633246779 0.7367612123 + 0.7887653708 0.7639619708 0.7374557853 + 0.7893326879 0.7645862103 0.7381486297 + 0.7899000049 0.7652103901 0.7388414741 + 0.7904673219 0.7658346295 0.7395343781 + 0.7910345793 0.7664586902 0.7402272224 + 0.7916018963 0.7670829296 0.7409201264 + 0.7921692133 0.7677071095 0.7416129708 + 0.7927350998 0.7683312893 0.7423058748 + 0.7932925820 0.7689499259 0.7429925799 + 0.7938501239 0.7695664763 0.7436763048 + 0.7944076061 0.7701830268 0.7443599105 + 0.7949650884 0.7707995772 0.7450435162 + 0.7955226898 0.7714161277 0.7457271814 + 0.7960801721 0.7720326185 0.7464107871 + 0.7966377139 0.7726492286 0.7470945120 + 0.7971891761 0.7732650042 0.7477774024 + 0.7977350950 0.7738748193 0.7484462857 + 0.7982810736 0.7744845152 0.7491152287 + 0.7988271117 0.7750942707 0.7497841120 + 0.7993730903 0.7757040262 0.7504529953 + 0.7999191284 0.7763137817 0.7511218786 + 0.8004651070 0.7769234776 0.7517908216 + 0.8010110259 0.7775332928 0.7524597049 + 0.8015481234 0.7781410813 0.7531278729 + 0.8020827770 0.7787457705 0.7537946105 + 0.8026174903 0.7793504000 0.7544614077 + 0.8031522036 0.7799550295 0.7551280856 + 0.8036869168 0.7805595994 0.7557948232 + 0.8042215705 0.7811642885 0.7564616203 + 0.8047562242 0.7817689180 0.7571282983 + 0.8052908778 0.7823734879 0.7577949762 + 0.8058195114 0.7829757929 0.7584580183 + 0.8063477278 0.7835758924 0.7591165900 + 0.8068758845 0.7841761112 0.7597752810 + 0.8074041009 0.7847762704 0.7604339719 + 0.8079321980 0.7853764296 0.7610926032 + 0.8084604144 0.7859765887 0.7617512941 + 0.8089885712 0.7865768075 0.7624099851 + 0.8095167875 0.7871769071 0.7630686164 + 0.8100429177 0.7877739072 0.7637246847 + 0.8105689287 0.7883684039 0.7643780708 + 0.8110948801 0.7889629006 0.7650315762 + 0.8116210103 0.7895573974 0.7656850815 + 0.8121470213 0.7901518941 0.7663385868 + 0.8126729727 0.7907463908 0.7669919729 + 0.8131991029 0.7913408279 0.7676454782 + 0.8137251139 0.7919353247 0.7682989836 + 0.8142498136 0.7925245762 0.7689477205 + 0.8147743940 0.7931082845 0.7695900202 + 0.8152989745 0.7936919928 0.7702323198 + 0.8158236146 0.7942757010 0.7708745003 + 0.8163480759 0.7948594093 0.7715167999 + 0.8168727160 0.7954431772 0.7721590996 + 0.8173972964 0.7960268855 0.7728013992 + 0.8179218769 0.7966105938 0.7734436989 + 0.8184441924 0.7971937060 0.7740848064 + 0.8189654946 0.7977755070 0.7747216821 + 0.8194869161 0.7983571887 0.7753584981 + 0.8200082183 0.7989389896 0.7759953737 + 0.8205295205 0.7995206714 0.7766323090 + 0.8210508227 0.8001024127 0.7772691846 + 0.8215721846 0.8006842136 0.7779061198 + 0.8220934868 0.8012658954 0.7785429955 + 0.8226118088 0.8018476963 0.7791798711 + 0.8231247067 0.8024280071 0.7798110843 + 0.8236376047 0.8030081987 0.7804415822 + 0.8241505027 0.8035883904 0.7810720801 + 0.8246634007 0.8041685820 0.7817025781 + 0.8251761794 0.8047488928 0.7823330760 + 0.8256890774 0.8053290844 0.7829635143 + 0.8262019753 0.8059092760 0.7835940123 + 0.8267148733 0.8064895868 0.7842245102 + 0.8272213936 0.8070695996 0.7848517895 + 0.8277270794 0.8076496124 0.7854750752 + 0.8282328844 0.8082295060 0.7860983014 + 0.8287385702 0.8088093996 0.7867215872 + 0.8292443752 0.8093892932 0.7873448730 + 0.8297501802 0.8099691868 0.7879682183 + 0.8302559257 0.8105490804 0.7885915041 + 0.8307616711 0.8111289740 0.7892147899 + 0.8312665224 0.8117089868 0.7898380160 + 0.8317692280 0.8122891188 0.7904539704 + 0.8322719932 0.8128693104 0.7910689116 + 0.8327746987 0.8134495020 0.7916837931 + 0.8332775235 0.8140296936 0.7922986746 + 0.8337802291 0.8146098852 0.7929136157 + 0.8342829943 0.8151900768 0.7935284972 + 0.8347858191 0.8157703280 0.7941433787 + 0.8352885246 0.8163505197 0.7947583199 + 0.8357869983 0.8169273734 0.7953717113 + 0.8362827897 0.8174930811 0.7959778905 + 0.8367785215 0.8180589080 0.7965841293 + 0.8372743130 0.8186246753 0.7971904278 + 0.8377701044 0.8191903830 0.7977966070 + 0.8382657766 0.8197562099 0.7984027863 + 0.8387616277 0.8203219175 0.7990090847 + 0.8392574191 0.8208876848 0.7996153235 + 0.8397530913 0.8214535117 0.8002215028 + 0.8402497172 0.8220161200 0.8008267879 + 0.8407465219 0.8225740194 0.8014301062 + 0.8412433267 0.8231317997 0.8020334840 + 0.8417400718 0.8236895800 0.8026369214 + 0.8422368765 0.8242474794 0.8032401800 + 0.8427336812 0.8248053193 0.8038436174 + 0.8432304263 0.8253630996 0.8044469953 + 0.8437271714 0.8259209991 0.8050503135 + 0.8442239761 0.8264787793 0.8056536913 + 0.8447151184 0.8270347118 0.8062539101 + 0.8452048898 0.8275880218 0.8068488240 + 0.8456946015 0.8281413913 0.8074436784 + 0.8461843133 0.8286947012 0.8080385923 + 0.8466740251 0.8292480111 0.8086336255 + 0.8471636772 0.8298013210 0.8092284799 + 0.8476533890 0.8303546906 0.8098233938 + 0.8481432199 0.8309080005 0.8104183078 + 0.8486328721 0.8314613104 0.8110132217 + 0.8491188288 0.8320134878 0.8116052747 + 0.8496029973 0.8325635791 0.8121902943 + 0.8500872850 0.8331136107 0.8127751946 + 0.8505715132 0.8336635828 0.8133600950 + 0.8510558009 0.8342136741 0.8139451146 + 0.8515400290 0.8347637057 0.8145300150 + 0.8520243168 0.8353136778 0.8151149154 + 0.8525084853 0.8358637094 0.8156998754 + 0.8529927731 0.8364138007 0.8162847757 + 0.8534749746 0.8369629979 0.8168694973 + 0.8539546728 0.8375040889 0.8174484968 + 0.8544343710 0.8380451798 0.8180274963 + 0.8549141288 0.8385862708 0.8186064959 + 0.8553938270 0.8391274214 0.8191856146 + 0.8558735251 0.8396685123 0.8197646141 + 0.8563532233 0.8402096033 0.8203436136 + 0.8568329215 0.8407506943 0.8209226131 + 0.8573125005 0.8412917852 0.8215016723 + 0.8577917814 0.8418329954 0.8220806718 + 0.8582649827 0.8423717022 0.8226525784 + 0.8587383032 0.8429095149 0.8232210875 + 0.8592115045 0.8434472084 0.8237894773 + 0.8596848249 0.8439850211 0.8243579865 + 0.8601580262 0.8445227742 0.8249264956 + 0.8606312871 0.8450605869 0.8254948854 + 0.8611046076 0.8455983996 0.8260633945 + 0.8615778089 0.8461362123 0.8266319036 + 0.8620511293 0.8466740251 0.8272002935 + 0.8625226021 0.8472099900 0.8277677298 + 0.8629927039 0.8477393985 0.8283293247 + 0.8634628057 0.8482688069 0.8288909197 + 0.8639330268 0.8487980962 0.8294523954 + 0.8644031286 0.8493275046 0.8300139904 + 0.8648731709 0.8498567939 0.8305755854 + 0.8653432727 0.8503862023 0.8311371207 + 0.8658133745 0.8509156108 0.8316987157 + 0.8662835956 0.8514449000 0.8322603106 + 0.8667536974 0.8519743085 0.8328217864 + 0.8672150970 0.8524994254 0.8333820105 + 0.8676754832 0.8530210257 0.8339408040 + 0.8681359291 0.8535426259 0.8344995975 + 0.8685963154 0.8540641069 0.8350583911 + 0.8690567017 0.8545857072 0.8356171846 + 0.8695170879 0.8551073074 0.8361760974 + 0.8699774742 0.8556289077 0.8367348909 + 0.8704379201 0.8561505079 0.8372936845 + 0.8708983064 0.8566719890 0.8378524780 + 0.8713576198 0.8571935892 0.8384112716 + 0.8718056083 0.8577103019 0.8389707208 + 0.8722537160 0.8582252860 0.8395304084 + 0.8727017045 0.8587402105 0.8400899768 + 0.8731498122 0.8592551947 0.8406497240 + 0.8735978007 0.8597701788 0.8412094116 + 0.8740459085 0.8602852225 0.8417689800 + 0.8744938970 0.8608002067 0.8423287272 + 0.8749418855 0.8613150716 0.8428884149 + 0.8753899932 0.8618301153 0.8434481025 + 0.8758366704 0.8623450994 0.8440076709 + 0.8762751818 0.8628504872 0.8445582986 + 0.8767135739 0.8633533716 0.8451061249 + 0.8771520853 0.8638563752 0.8456537724 + 0.8775904775 0.8643593192 0.8462015986 + 0.8780289888 0.8648623228 0.8467493057 + 0.8784673810 0.8653652072 0.8472970724 + 0.8789058924 0.8658682108 0.8478447795 + 0.8793442845 0.8663712144 0.8483924866 + 0.8797827959 0.8668740988 0.8489403129 + 0.8802210093 0.8673771024 0.8494880199 + 0.8806512952 0.8678756952 0.8500298858 + 0.8810817003 0.8683726192 0.8505687714 + 0.8815121055 0.8688694835 0.8511077166 + 0.8819423914 0.8693662882 0.8516464829 + 0.8823727965 0.8698632121 0.8521854281 + 0.8828032017 0.8703600764 0.8527243137 + 0.8832334876 0.8708568811 0.8532631993 + 0.8836638927 0.8713538051 0.8538020849 + 0.8840942979 0.8718507290 0.8543409705 + 0.8845247030 0.8723474741 0.8548799157 + 0.8849477172 0.8728402853 0.8554143906 + 0.8853685856 0.8733280897 0.8559429049 + 0.8857895732 0.8738158941 0.8564714193 + 0.8862105012 0.8743038177 0.8569998741 + 0.8866314888 0.8747916222 0.8575283289 + 0.8870524168 0.8752794862 0.8580567837 + 0.8874734044 0.8757672906 0.8585852981 + 0.8878942728 0.8762552142 0.8591138124 + 0.8883153200 0.8767430186 0.8596423268 + 0.8887361884 0.8772308826 0.8601707816 + 0.8891549706 0.8777185082 0.8606991768 + 0.8895698786 0.8782029152 0.8612251878 + 0.8899849057 0.8786873817 0.8617511988 + 0.8903998733 0.8791717887 0.8622772098 + 0.8908147812 0.8796561956 0.8628032804 + 0.8912298083 0.8801407218 0.8633292913 + 0.8916447759 0.8806251287 0.8638553023 + 0.8920598030 0.8811094761 0.8643813133 + 0.8924747109 0.8815940022 0.8649073243 + 0.8928896785 0.8820784092 0.8654333949 + 0.8933047056 0.8825628161 0.8659594059 + 0.8937178850 0.8830412030 0.8664832711 + 0.8941305876 0.8835136890 0.8670049906 + 0.8945432901 0.8839861751 0.8675267100 + 0.8949561119 0.8844587207 0.8680483103 + 0.8953688145 0.8849312067 0.8685700297 + 0.8957815766 0.8854036927 0.8690915704 + 0.8961942792 0.8858762980 0.8696132898 + 0.8966071010 0.8863487840 0.8701350093 + 0.8970198035 0.8868213296 0.8706566095 + 0.8974326253 0.8872938156 0.8711783290 + 0.8978449106 0.8877663016 0.8716999292 + 0.8982529044 0.8882371187 0.8722208142 + 0.8986608982 0.8887075186 0.8727415204 + 0.8990687728 0.8891779184 0.8732621074 + 0.8994768262 0.8896481991 0.8737828135 + 0.8998848200 0.8901185989 0.8743034005 + 0.9002928138 0.8905889988 0.8748241067 + 0.9007008076 0.8910593987 0.8753446937 + 0.9011088014 0.8915297985 0.8758653998 + 0.9015167952 0.8920001984 0.8763859868 + 0.9019247293 0.8924705982 0.8769065738 + 0.9023321867 0.8929408789 0.8774272799 + 0.9027374983 0.8934035897 0.8779382706 + 0.9031428099 0.8938655853 0.8784481287 + 0.9035481215 0.8943277001 0.8789578080 + 0.9039533734 0.8947896957 0.8794676065 + 0.9043586850 0.8952516913 0.8799774051 + 0.9047639966 0.8957138062 0.8804872036 + 0.9051693082 0.8961758018 0.8809968829 + 0.9055746198 0.8966377974 0.8815066814 + 0.9059798717 0.8970997930 0.8820164800 + 0.9063851833 0.8975619078 0.8825262785 + 0.9067893028 0.8980239034 0.8830360174 + 0.9071872830 0.8984869719 0.8835430741 + 0.9075853229 0.8989502788 0.8840497136 + 0.9079833031 0.8994135857 0.8845561743 + 0.9083812833 0.8998768926 0.8850628138 + 0.9087793231 0.9003401995 0.8855693936 + 0.9091774225 0.9008035064 0.8860759735 + 0.9095754027 0.9012668133 0.8865826130 + 0.9099733829 0.9017301202 0.8870890737 + 0.9103714228 0.9021934271 0.8875957131 + 0.9107694030 0.9026566744 0.8881022930 + 0.9111673832 0.9031199813 0.8886088729 + 0.9115555286 0.9035797715 0.8891084790 + 0.9119430780 0.9040381908 0.8896042705 + 0.9123308063 0.9044964910 0.8901001811 + 0.9127184153 0.9049547911 0.8905960917 + 0.9131060839 0.9054132104 0.8910920024 + 0.9134936929 0.9058715105 0.8915879130 + 0.9138813019 0.9063298106 0.8920838237 + 0.9142689705 0.9067881703 0.8925796747 + 0.9146565795 0.9072464705 0.8930755854 + 0.9150443077 0.9077047706 0.8935714960 + 0.9154319167 0.9081631899 0.8940674067 + 0.9158164263 0.9086192250 0.8945624232 + 0.9161984921 0.9090703130 0.8950551748 + 0.9165806770 0.9095214009 0.8955479264 + 0.9169628024 0.9099724889 0.8960406780 + 0.9173449874 0.9104235768 0.8965334892 + 0.9177271724 0.9108747244 0.8970261812 + 0.9181092978 0.9113258123 0.8975189924 + 0.9184914827 0.9117770195 0.8980118036 + 0.9188736081 0.9122281075 0.8985044956 + 0.9192557931 0.9126791954 0.8989973068 + 0.9196379781 0.9131302834 0.8994901180 + 0.9200199842 0.9135813713 0.8999828100 + 0.9203976989 0.9140260220 0.9004676938 + 0.9207754731 0.9144688249 0.9009500146 + 0.9211533070 0.9149116278 0.9014322758 + 0.9215310812 0.9153543711 0.9019145966 + 0.9219087958 0.9157971740 0.9023969173 + 0.9222866297 0.9162399769 0.9028791785 + 0.9226644039 0.9166827798 0.9033614993 + 0.9230421185 0.9171255827 0.9038438201 + 0.9234198928 0.9175683856 0.9043260813 + 0.9237977266 0.9180111885 0.9048084021 + 0.9241755009 0.9184539914 0.9052907228 + 0.9245520234 0.9188957214 0.9057729244 + 0.9249256849 0.9193243980 0.9062542915 + 0.9252995253 0.9197530150 0.9067355990 + 0.9256731868 0.9201816916 0.9072170258 + 0.9260470271 0.9206103086 0.9076983929 + 0.9264206886 0.9210389256 0.9081798196 + 0.9267945290 0.9214676023 0.9086611271 + 0.9271683097 0.9218962193 0.9091424942 + 0.9275419712 0.9223248959 0.9096239209 + 0.9279158115 0.9227535129 0.9101052284 + 0.9282894731 0.9231821895 0.9105865955 + 0.9286633134 0.9236108065 0.9110680223 + 0.9290362000 0.9240378141 0.9115486741 + 0.9294080734 0.9244576097 0.9120259881 + 0.9297800064 0.9248775244 0.9125033021 + 0.9301518798 0.9252973199 0.9129806757 + 0.9305236936 0.9257171154 0.9134579897 + 0.9308956265 0.9261369109 0.9139353037 + 0.9312674999 0.9265568256 0.9144126773 + 0.9316393733 0.9269766212 0.9148899913 + 0.9320111871 0.9273964167 0.9153673053 + 0.9323831201 0.9278162718 0.9158446193 + 0.9327549934 0.9282361269 0.9163219929 + 0.9331269264 0.9286559224 0.9167993069 + 0.9334965944 0.9290750027 0.9172763824 + 0.9338625073 0.9294890165 0.9177514911 + 0.9342283010 0.9299029708 0.9182265997 + 0.9345940948 0.9303171039 0.9187017083 + 0.9349600077 0.9307311177 0.9191766977 + 0.9353258014 0.9311450720 0.9196518064 + 0.9356915951 0.9315592051 0.9201269150 + 0.9360575080 0.9319732189 0.9206020236 + 0.9364233017 0.9323871732 0.9210770726 + 0.9367890954 0.9328011870 0.9215521812 + 0.9371550083 0.9332153201 0.9220272899 + 0.9375208020 0.9336292744 0.9225023985 + 0.9378861189 0.9340432882 0.9229773879 + 0.9382457137 0.9344478846 0.9234445095 + 0.9386053085 0.9348514080 0.9239102006 + 0.9389649034 0.9352548122 0.9243760109 + 0.9393244982 0.9356582165 0.9248417020 + 0.9396840930 0.9360616207 0.9253073931 + 0.9400436878 0.9364650846 0.9257732034 + 0.9404032826 0.9368684888 0.9262388945 + 0.9407628775 0.9372718930 0.9267047048 + 0.9411224723 0.9376752973 0.9271703959 + 0.9414821267 0.9380788207 0.9276362062 + 0.9418417215 0.9384822249 0.9281018972 + 0.9422013164 0.9388856292 0.9285677075 + 0.9425578713 0.9392852187 0.9290300012 + 0.9429129958 0.9396802187 0.9294880033 + 0.9432681203 0.9400752783 0.9299458861 + 0.9436231852 0.9404702783 0.9304038882 + 0.9439783096 0.9408653975 0.9308617711 + 0.9443333149 0.9412603974 0.9313197732 + 0.9446883798 0.9416555166 0.9317777753 + 0.9450435042 0.9420505762 0.9322357178 + 0.9453986287 0.9424455762 0.9326937199 + 0.9457536936 0.9428406954 0.9331517220 + 0.9461086988 0.9432356954 0.9336096048 + 0.9464638233 0.9436308146 0.9340676069 + 0.9468188286 0.9440258145 0.9345256090 + 0.9471713901 0.9444158077 0.9349750280 + 0.9475240111 0.9448049068 0.9354225993 + 0.9478765726 0.9451941252 0.9358702898 + 0.9482290745 0.9455832243 0.9363179803 + 0.9485816956 0.9459723234 0.9367656112 + 0.9489343166 0.9463614225 0.9372133017 + 0.9492868781 0.9467505217 0.9376609921 + 0.9496394992 0.9471396804 0.9381086826 + 0.9499921203 0.9475287795 0.9385563135 + 0.9503446817 0.9479178786 0.9390040040 + 0.9506971836 0.9483069777 0.9394516945 + 0.9510498047 0.9486961961 0.9398993254 + 0.9514018297 0.9490848184 0.9403468966 + 0.9517515898 0.9494650960 0.9407904744 + 0.9521014094 0.9498453736 0.9412341118 + 0.9524511099 0.9502257109 0.9416778088 + 0.9528009295 0.9506059885 0.9421213865 + 0.9531506896 0.9509863257 0.9425650239 + 0.9535005093 0.9513666034 0.9430087209 + 0.9538503289 0.9517468810 0.9434522986 + 0.9542000890 0.9521272182 0.9438959956 + 0.9545499086 0.9525074959 0.9443395734 + 0.9548997283 0.9528877735 0.9447832108 + 0.9552494287 0.9532681704 0.9452269077 + 0.9555991888 0.9536485076 0.9456704855 + 0.9559475183 0.9540281892 0.9461137056 + 0.9562901855 0.9543992281 0.9465476871 + 0.9566329122 0.9547703266 0.9469817877 + 0.9569756985 0.9551414251 0.9474158287 + 0.9573184252 0.9555124044 0.9478498101 + 0.9576610923 0.9558835030 0.9482839108 + 0.9580038786 0.9562546015 0.9487178922 + 0.9583466053 0.9566255808 0.9491519928 + 0.9586892724 0.9569966793 0.9495859742 + 0.9590321183 0.9573677778 0.9500200152 + 0.9593747854 0.9577388167 0.9504541159 + 0.9597175121 0.9581099153 0.9508880973 + 0.9600601792 0.9584810138 0.9513220787 + 0.9604027271 0.9588519931 0.9517561793 + 0.9607350230 0.9592133760 0.9521834254 + 0.9610671997 0.9595736265 0.9526095986 + 0.9613994956 0.9599336982 0.9530358911 + 0.9617316723 0.9602938294 0.9534621239 + 0.9620639086 0.9606539011 0.9538882971 + 0.9623962045 0.9610139728 0.9543144703 + 0.9627283812 0.9613741040 0.9547407031 + 0.9630606174 0.9617341757 0.9551668763 + 0.9633929133 0.9620944262 0.9555931091 + 0.9637250900 0.9624544978 0.9560192823 + 0.9640573859 0.9628146291 0.9564455152 + 0.9643896222 0.9631747007 0.9568716884 + 0.9647217989 0.9635347724 0.9572979212 + 0.9650493860 0.9638900161 0.9577190876 + 0.9653742909 0.9642401934 0.9581351280 + 0.9656991959 0.9645903707 0.9585509896 + 0.9660241008 0.9649404883 0.9589669704 + 0.9663490057 0.9652907252 0.9593828917 + 0.9666739106 0.9656409025 0.9597988725 + 0.9669988155 0.9659910798 0.9602147937 + 0.9673237205 0.9663413167 0.9606307745 + 0.9676485062 0.9666914940 0.9610466957 + 0.9679734111 0.9670416713 0.9614626765 + 0.9682983160 0.9673919082 0.9618785977 + 0.9686232209 0.9677420855 0.9622945786 + 0.9689481258 0.9680922031 0.9627104998 + 0.9692727923 0.9684423804 0.9631264806 + 0.9695920944 0.9687845111 0.9635347724 + 0.9699112773 0.9691258073 0.9639422894 + 0.9702305794 0.9694671035 0.9643498063 + 0.9705498815 0.9698083997 0.9647573233 + 0.9708691835 0.9701496959 0.9651647806 + 0.9711884260 0.9704909921 0.9655722976 + 0.9715077281 0.9708322883 0.9659798741 + 0.9718269706 0.9711735845 0.9663873911 + 0.9721462131 0.9715148807 0.9667949080 + 0.9724655151 0.9718561769 0.9672024250 + 0.9727848172 0.9721974730 0.9676098824 + 0.9731040001 0.9725388288 0.9680173993 + 0.9734233022 0.9728801250 0.9684249163 + 0.9737418890 0.9732202291 0.9688311219 + 0.9740585089 0.9735528827 0.9692280293 + 0.9743750095 0.9738854766 0.9696248770 + 0.9746916294 0.9742181897 0.9700219035 + 0.9750080705 0.9745507836 0.9704188108 + 0.9753245711 0.9748834968 0.9708157182 + 0.9756411910 0.9752160907 0.9712126255 + 0.9759576917 0.9755488038 0.9716095924 + 0.9762743115 0.9758813977 0.9720064998 + 0.9765908122 0.9762141109 0.9724034071 + 0.9769073129 0.9765467048 0.9728003144 + 0.9772238731 0.9768794179 0.9731972814 + 0.9775403738 0.9772120118 0.9735941887 + 0.9778568745 0.9775447249 0.9739910960 + 0.9781730771 0.9778758287 0.9743868709 + 0.9784877896 0.9781979918 0.9747753739 + 0.9788025022 0.9785202742 0.9751638770 + 0.9791172147 0.9788424969 0.9755523801 + 0.9794319868 0.9791647196 0.9759408832 + 0.9797466993 0.9794870019 0.9763293862 + 0.9800614119 0.9798092246 0.9767180085 + 0.9803761244 0.9801315069 0.9771065116 + 0.9806908965 0.9804537296 0.9774950147 + 0.9810056090 0.9807758927 0.9778835177 + 0.9813203216 0.9810981750 0.9782720208 + 0.9816349745 0.9814203978 0.9786605239 + 0.9819498062 0.9817426801 0.9790490866 + 0.9822645187 0.9820649028 0.9794375896 + 0.9825791121 0.9823871255 0.9798260927 + 0.9828888774 0.9827020764 0.9802024961 + 0.9831985831 0.9830166101 0.9805781245 + 0.9835082889 0.9833310843 0.9809536934 + 0.9838181138 0.9836456776 0.9813293219 + 0.9841278195 0.9839602113 0.9817048907 + 0.9844375253 0.9842746854 0.9820805192 + 0.9847471714 0.9845892191 0.9824560881 + 0.9850569963 0.9849038124 0.9828317761 + 0.9853667021 0.9852182865 0.9832074046 + 0.9856764078 0.9855328202 0.9835829735 + 0.9859861732 0.9858472943 0.9839586020 + 0.9862958789 0.9861618876 0.9843341708 + 0.9866055846 0.9864764214 0.9847097993 + 0.9869154096 0.9867908955 0.9850854278 + 0.9872199893 0.9871007800 0.9854502082 + 0.9875209928 0.9874067903 0.9858055115 + 0.9878221154 0.9877126813 0.9861608148 + 0.9881231189 0.9880186915 0.9865159988 + 0.9884241223 0.9883245826 0.9868713021 + 0.9887251854 0.9886305928 0.9872264862 + 0.9890261889 0.9889364839 0.9875817895 + 0.9893273115 0.9892424941 0.9879369736 + 0.9896283150 0.9895483851 0.9882922769 + 0.9899293780 0.9898543954 0.9886475801 + 0.9902303815 0.9901602864 0.9890028238 + 0.9905313849 0.9904662967 0.9893581271 + 0.9908325076 0.9907721877 0.9897133112 + 0.9911335111 0.9910781980 0.9900686145 + 0.9914345741 0.9913840890 0.9904237986 + 0.9917227030 0.9916865230 0.9907621741 + 0.9920099974 0.9919885993 0.9910988212 + 0.9922972918 0.9922906160 0.9914355278 + 0.9925847054 0.9925926924 0.9917721152 + 0.9928719997 0.9928947091 0.9921088219 + 0.9931594133 0.9931967854 0.9924454093 + 0.9934467077 0.9934988022 0.9927819967 + 0.9937341213 0.9938008785 0.9931187034 + 0.9940214157 0.9941028953 0.9934552908 + 0.9943088293 0.9944049716 0.9937918782 + 0.9945961237 0.9947069883 0.9941285849 + 0.9948834777 0.9950091243 0.9944651723 + 0.9951707721 0.9953110814 0.9948018789 + 0.9954581857 0.9956132174 0.9951385260 + 0.9957451820 0.9959133267 0.9954729080 + 0.9960289001 0.9961857200 0.9957746863 + 0.9963124990 0.9964581728 0.9960765243 + 0.9965962172 0.9967306256 0.9963783026 + 0.9968798161 0.9970030785 0.9966800809 + 0.9971634746 0.9972754717 0.9969819188 + 0.9974470735 0.9975479841 0.9972836971 + 0.9977307916 0.9978203773 0.9975854754 + 0.9980143905 0.9980928898 0.9978873134 + 0.9982981086 0.9983652830 0.9981892109 + 0.9985817075 0.9986377954 0.9984909892 + 0.9988654256 0.9989101887 0.9987928271 + 0.9991490245 0.9991827011 0.9990946054 + 0.9994326830 0.9994550943 0.9993963838 + 0.9997162819 0.9997276068 0.9996982217 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/fujifilm_f-250.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/fujifilm_f-250.spi1d index cf2405a3..8f8016e5 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/fujifilm_f-250.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/fujifilm_f-250.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0340563506 0.0275065694 0.0159309693 - 0.0581281595 0.0446017683 0.0286259800 - 0.0766732469 0.0585205704 0.0393032692 - 0.0912864283 0.0693459511 0.0486846492 - 0.1037112027 0.0783905834 0.0571168698 - 0.1149740964 0.0866255537 0.0647848472 - 0.1255443990 0.0943730623 0.0718255714 - 0.1358117014 0.1018529013 0.0783746019 - 0.1458894014 0.1092692986 0.0845360085 - 0.1554104984 0.1165430993 0.0902841464 - 0.1642753929 0.1235136986 0.0956311375 - 0.1725185066 0.1300638020 0.1007089019 - 0.1802159995 0.1362310052 0.1055677980 - 0.1874677986 0.1420270056 0.1101925969 - 0.1943995059 0.1475103050 0.1146183014 - 0.2010439038 0.1526764929 0.1188452989 - 0.2075248957 0.1576137990 0.1229263991 - 0.2138666064 0.1623017043 0.1268835962 - 0.2200817019 0.1667637974 0.1307622045 - 0.2262571007 0.1710768938 0.1345600933 - 0.2322959006 0.1752613038 0.1383388937 - 0.2381743044 0.1792901009 0.1420553029 - 0.2439599037 0.1831696928 0.1457192004 - 0.2495301962 0.1869384944 0.1493068933 - 0.2549104095 0.1905891001 0.1528359056 - 0.2600974143 0.1941210032 0.1562989056 - 0.2651267946 0.1975311041 0.1596667022 - 0.2699950933 0.2007963061 0.1629731953 - 0.2746872902 0.2039687932 0.1662473977 - 0.2792500854 0.2070634067 0.1694674939 - 0.2837044001 0.2100957930 0.1726872027 - 0.2880541086 0.2130752951 0.1758597940 - 0.2922781110 0.2160121948 0.1789810956 - 0.2963983119 0.2189164013 0.1820618957 - 0.3004223108 0.2217878997 0.1850834042 - 0.3043056130 0.2246080041 0.1880794019 - 0.3080697954 0.2273644060 0.1910150051 - 0.3117355108 0.2300619930 0.1939160973 - 0.3153220117 0.2327065021 0.1967920959 - 0.3188439906 0.2353048027 0.1996531039 - 0.3223091066 0.2378443033 0.2024717033 - 0.3257097900 0.2403286994 0.2052704990 - 0.3290207982 0.2427732944 0.2080609947 - 0.3322731853 0.2452045977 0.2108310014 - 0.3354865909 0.2476018071 0.2135528028 - 0.3386512101 0.2499758005 0.2162420005 - 0.3417851031 0.2523289025 0.2188850939 - 0.3448846936 0.2546671927 0.2214843929 - 0.3479135036 0.2569605112 0.2240575999 - 0.3508971930 0.2591898143 0.2266031057 - 0.3538430929 0.2613660097 0.2291243970 - 0.3567287922 0.2634985149 0.2316229939 - 0.3595845103 0.2655822933 0.2340888977 - 0.3624258935 0.2676230967 0.2365391999 - 0.3652456105 0.2696104944 0.2389807999 - 0.3680748940 0.2715514004 0.2413962036 - 0.3708611131 0.2734310031 0.2437824011 - 0.3735972047 0.2752597928 0.2461562008 - 0.3762691021 0.2770377994 0.2485167980 - 0.3789086044 0.2787886858 0.2508699000 - 0.3814966083 0.2805095911 0.2532047033 - 0.3840735853 0.2822039127 0.2555142939 - 0.3866173923 0.2838774920 0.2578059137 - 0.3891361058 0.2855109870 0.2600739002 - 0.3916285038 0.2871398032 0.2623229921 - 0.3941093087 0.2887691855 0.2645553052 - 0.3965596855 0.2904030979 0.2667697966 - 0.3989767134 0.2920188010 0.2689580023 - 0.4013713896 0.2936233878 0.2711285055 - 0.4037455022 0.2952230871 0.2732937932 - 0.4060991108 0.2968241870 0.2754479051 - 0.4084275067 0.2984308898 0.2775818110 - 0.4107517004 0.3000445962 0.2796877921 - 0.4130510986 0.3016583025 0.2817656994 - 0.4153209031 0.3032869101 0.2838352919 - 0.4175666869 0.3049050868 0.2858906984 - 0.4197983146 0.3065397143 0.2879244983 - 0.4220117033 0.3081744909 0.2899451852 - 0.4241906106 0.3098129034 0.2919526100 - 0.4263457954 0.3114565909 0.2939467132 - 0.4284875989 0.3130967915 0.2959286869 - 0.4306012988 0.3147349954 0.2978995144 - 0.4326950014 0.3163720965 0.2998577058 - 0.4347662032 0.3180099130 0.3018037975 - 0.4368287027 0.3196572065 0.3037435114 - 0.4388831854 0.3213165104 0.3056769073 - 0.4409214854 0.3229748011 0.3075988889 - 0.4429436028 0.3246040940 0.3095020056 - 0.4449625909 0.3262285888 0.3113960922 - 0.4469791055 0.3278538883 0.3132843077 - 0.4489935935 0.3294833004 0.3151693940 - 0.4509986937 0.3311119974 0.3170453012 - 0.4529815018 0.3327403069 0.3189097047 - 0.4549526870 0.3343681991 0.3207617104 - 0.4569247961 0.3359952867 0.3225966990 - 0.4588770866 0.3376198113 0.3244307935 - 0.4608316123 0.3392421007 0.3262627125 - 0.4627926052 0.3408617973 0.3280918002 - 0.4647417963 0.3424758017 0.3298951089 - 0.4666925967 0.3440926969 0.3316844106 - 0.4686441123 0.3457145989 0.3334603012 - 0.4705764949 0.3473288119 0.3352111876 - 0.4724830091 0.3489370942 0.3369530141 - 0.4743657112 0.3505378962 0.3386858106 - 0.4762322009 0.3521256149 0.3404065073 - 0.4780816138 0.3537108004 0.3421171010 - 0.4799089134 0.3552944958 0.3438138068 - 0.4817309082 0.3568659127 0.3454996943 - 0.4835452139 0.3584319055 0.3471781015 - 0.4853380024 0.3599914014 0.3488380909 - 0.4871332943 0.3615401089 0.3504877090 - 0.4889298975 0.3630783856 0.3521300852 - 0.4907036126 0.3646185100 0.3537611961 - 0.4924693108 0.3661572933 0.3553892970 - 0.4942173064 0.3676883876 0.3570109904 - 0.4959532917 0.3692195117 0.3586300910 - 0.4976806045 0.3707506955 0.3602473140 - 0.4994089901 0.3722806871 0.3618556857 - 0.5011367202 0.3738102019 0.3634608984 - 0.5028569102 0.3753306866 0.3650502861 - 0.5045688748 0.3768486083 0.3666361868 - 0.5062633157 0.3783572018 0.3682140112 - 0.5079504251 0.3798607886 0.3697811067 - 0.5096306801 0.3813571036 0.3713372052 - 0.5112873912 0.3828429878 0.3728885055 - 0.5129328966 0.3843210936 0.3744373918 - 0.5145742297 0.3857963085 0.3759793937 - 0.5162146091 0.3872702122 0.3775196970 - 0.5178315043 0.3887482882 0.3790464103 - 0.5194466114 0.3902272880 0.3805716932 - 0.5210654736 0.3916977048 0.3820922077 - 0.5226833224 0.3931671977 0.3836125135 - 0.5242862105 0.3946341872 0.3851282001 - 0.5258901119 0.3961010873 0.3866432905 - 0.5275049806 0.3975507915 0.3881508112 - 0.5291187763 0.3990002871 0.3896571100 - 0.5307204127 0.4004558921 0.3911463916 - 0.5323221087 0.4019115865 0.3926354945 - 0.5339249969 0.4033721089 0.3941121101 - 0.5355278850 0.4048328102 0.3955886960 - 0.5371186137 0.4062801898 0.3970466852 - 0.5387089849 0.4077258110 0.3985038996 - 0.5402762294 0.4091561139 0.3999438882 - 0.5418404937 0.4105820954 0.4013813138 - 0.5433958173 0.4120019972 0.4028140903 - 0.5449482799 0.4134188890 0.4042451978 - 0.5464795232 0.4148291945 0.4056676030 - 0.5479968190 0.4162329137 0.4070835114 - 0.5495104194 0.4176310897 0.4084953070 - 0.5510190725 0.4190171063 0.4099004865 - 0.5525225997 0.4204013050 0.4113028049 - 0.5540046096 0.4217655063 0.4126909971 - 0.5554866195 0.4231297076 0.4140791893 - 0.5569679737 0.4244849980 0.4154512882 - 0.5584493279 0.4258384109 0.4168216884 - 0.5599154234 0.4271919131 0.4181849957 - 0.5613729954 0.4285452962 0.4195438027 - 0.5628271103 0.4298977852 0.4209015071 - 0.5642738938 0.4312470853 0.4222564995 - 0.5657206774 0.4325963855 0.4236116111 - 0.5671554208 0.4339382946 0.4249607027 - 0.5685896277 0.4352790117 0.4263094068 - 0.5700142980 0.4366140068 0.4276516140 - 0.5714316964 0.4379422963 0.4289883971 - 0.5728462934 0.4392705858 0.4303236902 - 0.5742422938 0.4405961931 0.4316473901 - 0.5756382942 0.4419218898 0.4329712093 - 0.5770087838 0.4432437122 0.4342881143 - 0.5783665776 0.4445624053 0.4356010854 - 0.5797222257 0.4458796084 0.4369125068 - 0.5810663104 0.4471733868 0.4382139146 - 0.5824103951 0.4484671950 0.4395152032 - 0.5837360024 0.4497536123 0.4408144057 - 0.5850501060 0.4510329962 0.4421119988 - 0.5863640904 0.4523124993 0.4434092939 - 0.5876724124 0.4535883069 0.4446893036 - 0.5889807940 0.4548639953 0.4459693134 - 0.5902798176 0.4561336040 0.4472444057 - 0.5915662050 0.4573903978 0.4485121965 - 0.5928525925 0.4586473107 0.4497799873 - 0.5941187143 0.4598957002 0.4510425925 - 0.5953786969 0.4611397982 0.4523034990 - 0.5966371894 0.4623838067 0.4535635114 - 0.5978767872 0.4636138082 0.4548071027 - 0.5991163850 0.4648432136 0.4560508132 - 0.6003507972 0.4660690129 0.4572936893 - 0.6015748978 0.4672825933 0.4585348964 - 0.6027989984 0.4684962034 0.4597761929 - 0.6040027738 0.4697115123 0.4610106051 - 0.6051900983 0.4709289968 0.4622390866 - 0.6063774228 0.4721465111 0.4634675980 - 0.6075572968 0.4733569920 0.4646885097 - 0.6087344885 0.4745635092 0.4659062028 - 0.6099116802 0.4757699072 0.4671239853 - 0.6110656857 0.4769654870 0.4683316052 - 0.6122164130 0.4781579971 0.4695374966 - 0.6133670807 0.4793503881 0.4707433879 - 0.6145061851 0.4805291891 0.4719372094 - 0.6156448722 0.4817059934 0.4731302857 - 0.6167832017 0.4828827083 0.4743235111 - 0.6179025769 0.4840502143 0.4755181074 - 0.6190220714 0.4852168858 0.4767127037 - 0.6201403141 0.4863835871 0.4779073000 - 0.6212201118 0.4875405133 0.4790931046 - 0.6222999096 0.4886966050 0.4802789986 - 0.6233797073 0.4898526967 0.4814648926 - 0.6244452000 0.4909954071 0.4826442897 - 0.6255105734 0.4921365082 0.4838235974 - 0.6265758872 0.4932776093 0.4850027859 - 0.6276177168 0.4943977892 0.4861753881 - 0.6286571026 0.4955132902 0.4873471856 - 0.6296964884 0.4966287911 0.4885190129 - 0.6307348013 0.4977380931 0.4896813035 - 0.6317728162 0.4988445044 0.4908406138 - 0.6328107715 0.4999508858 0.4919998944 - 0.6338409781 0.5010526180 0.4931561053 - 0.6348659992 0.5021498799 0.4943102002 - 0.6358910799 0.5032472014 0.4954642057 - 0.6369110942 0.5043411851 0.4966160059 - 0.6379228830 0.5054274201 0.4977641106 - 0.6389346719 0.5065134764 0.4989121854 - 0.6399447918 0.5075992942 0.5000599027 - 0.6409429908 0.5086715221 0.5012037158 - 0.6419411898 0.5097436905 0.5023475885 - 0.6429393291 0.5108159184 0.5034914017 - 0.6439177990 0.5118789077 0.5046312809 - 0.6448913813 0.5129379034 0.5057700872 - 0.6458649039 0.5139970183 0.5069088936 - 0.6468328834 0.5150539875 0.5080435872 - 0.6477934122 0.5161070824 0.5091723204 - 0.6487538815 0.5171602964 0.5103009939 - 0.6497141719 0.5182133913 0.5114297271 - 0.6506603956 0.5192496181 0.5125468969 - 0.6516065001 0.5202844143 0.5136641264 - 0.6525526047 0.5213190913 0.5147812963 - 0.6534926891 0.5223445892 0.5158960819 - 0.6544277072 0.5233588815 0.5170087814 - 0.6553627253 0.5243731141 0.5181214809 - 0.6562976837 0.5253874063 0.5192341805 - 0.6572293043 0.5263924003 0.5203433037 - 0.6581609249 0.5273966789 0.5214524269 - 0.6590924859 0.5284010172 0.5225614905 - 0.6600255966 0.5294005275 0.5236659050 - 0.6609606147 0.5303912163 0.5247640014 - 0.6618955731 0.5313820243 0.5258620977 - 0.6628304720 0.5323727131 0.5269600749 - 0.6637492776 0.5333474278 0.5280525088 - 0.6646643877 0.5343161225 0.5291435122 - 0.6655796170 0.5352846980 0.5302343965 - 0.6664931774 0.5362530947 0.5313249230 - 0.6673964858 0.5372167230 0.5324121714 - 0.6682997942 0.5381802917 0.5334994793 - 0.6692031026 0.5391438007 0.5345867276 - 0.6701043844 0.5401027799 0.5356699824 - 0.6710026860 0.5410515070 0.5367466807 - 0.6719009876 0.5420001745 0.5378234982 - 0.6727992892 0.5429489017 0.5389001966 - 0.6736878753 0.5438944101 0.5399749279 - 0.6745703220 0.5448371172 0.5410481095 - 0.6754528284 0.5457797050 0.5421212912 - 0.6763352156 0.5467222929 0.5431944728 - 0.6772043705 0.5476604104 0.5442599058 - 0.6780694127 0.5485960841 0.5453224778 - 0.6789343953 0.5495318174 0.5463851094 - 0.6797993779 0.5504674911 0.5474476814 - 0.6806520224 0.5513973236 0.5485045910 - 0.6815025806 0.5523251891 0.5495604277 - 0.6823530793 0.5532531142 0.5506160855 - 0.6832036972 0.5541810989 0.5516719222 - 0.6840479970 0.5551018715 0.5527248979 - 0.6848915219 0.5560209155 0.5537775159 - 0.6857351065 0.5569398999 0.5548300743 - 0.6865785718 0.5578588843 0.5558826923 - 0.6874216795 0.5587705970 0.5569267869 - 0.6882647872 0.5596801043 0.5579692721 - 0.6891077757 0.5605894923 0.5590118766 - 0.6899508238 0.5614989996 0.5600544214 - 0.6907768250 0.5624088049 0.5610944033 - 0.6915978789 0.5633186102 0.5621334910 - 0.6924189925 0.5642284751 0.5631725788 - 0.6932401061 0.5651383996 0.5642117262 - 0.6940519214 0.5660479069 0.5652444959 - 0.6948580742 0.5669571161 0.5662732720 - 0.6956644058 0.5678663254 0.5673021078 - 0.6964706779 0.5687754750 0.5683308840 - 0.6972733736 0.5696833730 0.5693569779 - 0.6980714798 0.5705887079 0.5703790784 - 0.6988695264 0.5714939833 0.5714011788 - 0.6996675134 0.5723993182 0.5724232793 - 0.7004622221 0.5733036995 0.5734450221 - 0.7012382150 0.5741962790 0.5744643211 - 0.7020143270 0.5750889182 0.5754836798 - 0.7027903795 0.5759814978 0.5765029788 - 0.7035663724 0.5768741965 0.5775223970 - 0.7043431997 0.5777689219 0.5785338283 - 0.7051200867 0.5786644220 0.5795432925 - 0.7058969736 0.5795599818 0.5805526972 - 0.7066739202 0.5804554820 0.5815622211 - 0.7074453831 0.5813494921 0.5825701952 - 0.7082093954 0.5822402835 0.5835762024 - 0.7089732885 0.5831311941 0.5845820904 - 0.7097373009 0.5840221047 0.5855880976 - 0.7105011940 0.5849128962 0.5865939856 - 0.7112401128 0.5857965946 0.5875905156 - 0.7119778991 0.5866792202 0.5885863900 - 0.7127156258 0.5875617862 0.5895823240 - 0.7134532928 0.5884444714 0.5905781984 - 0.7141891718 0.5893244147 0.5915713906 - 0.7149226069 0.5901988745 0.5925608873 - 0.7156559825 0.5910735130 0.5935503244 - 0.7163894176 0.5919480920 0.5945397019 - 0.7171227932 0.5928226709 0.5955291986 - 0.7178444862 0.5936961174 0.5965164900 - 0.7185636759 0.5945690870 0.5975033045 - 0.7192828059 0.5954421759 0.5984901190 - 0.7200019956 0.5963152051 0.5994768739 - 0.7207183242 0.5971875191 0.6004620194 - 0.7214186788 0.5980507731 0.6014366150 - 0.7221192122 0.5989142060 0.6024110913 - 0.7228196263 0.5997775197 0.6033856869 - 0.7235201001 0.6006407738 0.6043602824 - 0.7242172956 0.6015027165 0.6053301096 - 0.7249100804 0.6023617983 0.6062927842 - 0.7256028056 0.6032208800 0.6072555184 - 0.7262955904 0.6040800214 0.6082183123 - 0.7269883156 0.6049389839 0.6091809869 - 0.7276725173 0.6057918072 0.6101375222 - 0.7283511758 0.6066387892 0.6110895872 - 0.7290298939 0.6074858904 0.6120417714 - 0.7297086120 0.6083328724 0.6129938960 - 0.7303872705 0.6091799736 0.6139460802 - 0.7310575843 0.6100224853 0.6148930788 - 0.7317247987 0.6108626127 0.6158381104 - 0.7323920727 0.6117026210 0.6167830825 - 0.7330592871 0.6125426888 0.6177281737 - 0.7337265015 0.6133828163 0.6186732054 - 0.7343794703 0.6142179966 0.6196153164 - 0.7350285053 0.6150509715 0.6205565929 - 0.7356774211 0.6158841252 0.6214978099 - 0.7363263965 0.6167172194 0.6224390864 - 0.7369753718 0.6175503135 0.6233803034 - 0.7376106977 0.6183786988 0.6243141890 - 0.7382416129 0.6192048192 0.6252453923 - 0.7388725281 0.6200309992 0.6261765957 - 0.7395033836 0.6208571196 0.6271077991 - 0.7401342988 0.6216832995 0.6280390024 - 0.7407568097 0.6225084066 0.6289677024 - 0.7413746715 0.6233326197 0.6298949122 - 0.7419925928 0.6241568923 0.6308221221 - 0.7426105142 0.6249812245 0.6317493916 - 0.7432283759 0.6258054972 0.6326766014 - 0.7438343167 0.6266260743 0.6335960031 - 0.7444267869 0.6274409294 0.6345061064 - 0.7450193167 0.6282557249 0.6354162097 - 0.7456117868 0.6290705204 0.6363263130 - 0.7462043166 0.6298853159 0.6372364163 - 0.7467948198 0.6306993961 0.6381446719 - 0.7473781109 0.6315096021 0.6390461922 - 0.7479612827 0.6323196888 0.6399477124 - 0.7485445142 0.6331298947 0.6408491731 - 0.7491278052 0.6339399815 0.6417506933 - 0.7497109771 0.6347501874 0.6426522136 - 0.7502849102 0.6355507970 0.6435450912 - 0.7508574128 0.6363490820 0.6444365978 - 0.7514299750 0.6371474862 0.6453281045 - 0.7520025969 0.6379457712 0.6462196112 - 0.7525750995 0.6387441158 0.6471111178 - 0.7531384230 0.6395425200 0.6479977965 - 0.7536910176 0.6403411031 0.6488788128 - 0.7542436123 0.6411396861 0.6497598290 - 0.7547962070 0.6419383287 0.6506407857 - 0.7553488016 0.6427369118 0.6515218019 - 0.7559013963 0.6435353756 0.6524028182 - 0.7564553022 0.6443219781 0.6532747746 - 0.7570090890 0.6451072097 0.6541464925 - 0.7575628757 0.6458923221 0.6550180912 - 0.7581167817 0.6466773748 0.6558898091 - 0.7586706281 0.6474626064 0.6567614079 - 0.7592213154 0.6482473016 0.6576269865 - 0.7597671151 0.6490309238 0.6584824920 - 0.7603129745 0.6498146057 0.6593379974 - 0.7608588934 0.6505982876 0.6601936221 - 0.7614046931 0.6513819098 0.6610491276 - 0.7619506121 0.6521655917 0.6619045734 - 0.7624893785 0.6529437900 0.6627510786 - 0.7630257010 0.6537193060 0.6635940075 - 0.7635619044 0.6544947028 0.6644368768 - 0.7640982270 0.6552700996 0.6652798057 - 0.7646344900 0.6560454965 0.6661226749 - 0.7651708126 0.6568210125 0.6669656038 - 0.7657119036 0.6575928926 0.6677941084 - 0.7662529945 0.6583644748 0.6686221957 - 0.7667940855 0.6591361761 0.6694504023 - 0.7673352957 0.6599078178 0.6702784896 - 0.7678763866 0.6606794000 0.6711065769 - 0.7684165239 0.6614500880 0.6719334722 - 0.7689508796 0.6622111201 0.6727535725 - 0.7694852948 0.6629719734 0.6735736132 - 0.7700197101 0.6637328863 0.6743935943 - 0.7705541849 0.6644939184 0.6752135754 - 0.7710886002 0.6652547717 0.6760336161 - 0.7716192007 0.6660150290 0.6768512130 - 0.7721378803 0.6667712927 0.6776608229 - 0.7726566792 0.6675274968 0.6784703732 - 0.7731754184 0.6682838202 0.6792799830 - 0.7736940980 0.6690400839 0.6800895929 - 0.7742128968 0.6697964072 0.6808992028 - 0.7747325897 0.6705529094 0.6817058921 - 0.7752555013 0.6713106036 0.6825023890 - 0.7757782936 0.6720684171 0.6832988858 - 0.7763012052 0.6728261113 0.6840953827 - 0.7768241167 0.6735839248 0.6848918796 - 0.7773470283 0.6743416190 0.6856883764 - 0.7778692245 0.6750988960 0.6864832044 - 0.7783874273 0.6758496761 0.6872665882 - 0.7789055705 0.6766005158 0.6880499721 - 0.7794237733 0.6773511767 0.6888334155 - 0.7799419165 0.6781020164 0.6896167994 - 0.7804601192 0.6788527966 0.6904001832 - 0.7809783220 0.6796035767 0.6911836267 - 0.7814986110 0.6803464890 0.6919562221 - 0.7820190191 0.6810883880 0.6927281022 - 0.7825394273 0.6818302870 0.6934999824 - 0.7830597758 0.6825721860 0.6942719221 - 0.7835801840 0.6833140850 0.6950438023 - 0.7841004729 0.6840559840 0.6958156824 - 0.7846269011 0.6847928166 0.6965779066 - 0.7851561904 0.6855260730 0.6973350048 - 0.7856854796 0.6862595081 0.6980922222 - 0.7862148285 0.6869928837 0.6988493204 - 0.7867441177 0.6877261996 0.6996064186 - 0.7872734070 0.6884595752 0.7003635764 - 0.7878013253 0.6891936064 0.7011185288 - 0.7883257270 0.6899294853 0.7018678784 - 0.7888501287 0.6906654835 0.7026172876 - 0.7893745899 0.6914014816 0.7033666968 - 0.7898989916 0.6921374202 0.7041161060 - 0.7904235125 0.6928734183 0.7048655152 - 0.7909479141 0.6936092973 0.7056149244 - 0.7914677858 0.6943377256 0.7063497901 - 0.7919868231 0.6950635910 0.7070816159 - 0.7925058007 0.6957895756 0.7078133821 - 0.7930247784 0.6965156198 0.7085452080 - 0.7935438156 0.6972414851 0.7092769742 - 0.7940626740 0.6979675293 0.7100086808 - 0.7945836782 0.6986932755 0.7107384205 - 0.7951102257 0.6994187832 0.7114617229 - 0.7956367135 0.7001444101 0.7121850252 - 0.7961632013 0.7008699179 0.7129083276 - 0.7966896892 0.7015954256 0.7136315703 - 0.7972161770 0.7023208737 0.7143548727 - 0.7977427244 0.7030463815 0.7150781751 - 0.7982670069 0.7037687898 0.7157930732 - 0.7987899780 0.7044885755 0.7165021896 - 0.7993130088 0.7052084208 0.7172113061 - 0.7998359799 0.7059280872 0.7179204226 - 0.8003590107 0.7066478729 0.7186294198 - 0.8008819818 0.7073677182 0.7193384767 - 0.8014048934 0.7080873847 0.7200475931 - 0.8019158244 0.7088059783 0.7207496762 - 0.8024243116 0.7095242143 0.7214503884 - 0.8029329181 0.7102422714 0.7221511006 - 0.8034414053 0.7109605074 0.7228518128 - 0.8039500117 0.7116786838 0.7235525250 - 0.8044584990 0.7123969197 0.7242532969 - 0.8049671054 0.7131149769 0.7249540091 - 0.8054732084 0.7138308883 0.7256454825 - 0.8059793115 0.7145465016 0.7263367176 - 0.8064854145 0.7152621150 0.7270278931 - 0.8069915175 0.7159777284 0.7277191877 - 0.8074976206 0.7166932821 0.7284104228 - 0.8080037236 0.7174088955 0.7291015983 - 0.8085097075 0.7181245089 0.7297927737 - 0.8090131879 0.7188358903 0.7304673791 - 0.8095167279 0.7195469737 0.7311419249 - 0.8100202084 0.7202581167 0.7318164706 - 0.8105236888 0.7209693193 0.7324910164 - 0.8110272288 0.7216804028 0.7331656218 - 0.8115307093 0.7223916054 0.7338401079 - 0.8120341897 0.7231026888 0.7345147133 - 0.8125367761 0.7238088846 0.7351782918 - 0.8130393028 0.7245143056 0.7358410954 - 0.8135418892 0.7252197862 0.7365038991 - 0.8140444160 0.7259252071 0.7371665835 - 0.8145468831 0.7266306877 0.7378293872 - 0.8150494099 0.7273361087 0.7384921908 - 0.8155519962 0.7280415893 0.7391549945 - 0.8160499930 0.7287421227 0.7398017049 - 0.8165466189 0.7294405103 0.7404432297 - 0.8170431852 0.7301387787 0.7410846949 - 0.8175398111 0.7308372259 0.7417262197 - 0.8180363774 0.7315356135 0.7423676848 - 0.8185330033 0.7322340012 0.7430092096 - 0.8190296888 0.7329323888 0.7436506748 - 0.8195242286 0.7336298227 0.7442870736 - 0.8200165033 0.7343257070 0.7449178100 - 0.8205088973 0.7350217104 0.7455484867 - 0.8210011721 0.7357177138 0.7461792231 - 0.8214935064 0.7364135981 0.7468098998 - 0.8219857812 0.7371096015 0.7474406958 - 0.8224781752 0.7378056049 0.7480713725 - 0.8229702115 0.7385014892 0.7487009168 - 0.8234596252 0.7391961217 0.7493168712 - 0.8239489794 0.7398905754 0.7499328852 - 0.8244382739 0.7405852079 0.7505488992 - 0.8249276876 0.7412797809 0.7511649132 - 0.8254171014 0.7419744134 0.7517809272 - 0.8259065151 0.7426689267 0.7523968816 - 0.8263958097 0.7433634996 0.7530128956 - 0.8268806934 0.7440590262 0.7536231279 - 0.8273624778 0.7447553277 0.7542291880 - 0.8278442025 0.7454516888 0.7548351884 - 0.8283259869 0.7461479902 0.7554411888 - 0.8288077116 0.7468442917 0.7560473084 - 0.8292894959 0.7475407124 0.7566533089 - 0.8297712207 0.7482370138 0.7572593093 - 0.8302530050 0.7489333153 0.7578654289 - 0.8307291269 0.7496249080 0.7584576011 - 0.8312050104 0.7503160834 0.7590492964 - 0.8316810131 0.7510071993 0.7596409917 - 0.8321568966 0.7516983747 0.7602326870 - 0.8326328993 0.7523894906 0.7608243823 - 0.8331087828 0.7530807257 0.7614160776 - 0.8335847259 0.7537717819 0.7620077729 - 0.8340600729 0.7544625998 0.7625957727 - 0.8345336914 0.7551519275 0.7631717920 - 0.8350073099 0.7558411956 0.7637478113 - 0.8354809284 0.7565305233 0.7643237710 - 0.8359544873 0.7572197914 0.7648997903 - 0.8364281058 0.7579091191 0.7654758096 - 0.8369017243 0.7585983872 0.7660518289 - 0.8373752832 0.7592877150 0.7666277885 - 0.8378481865 0.7599760294 0.7671977878 - 0.8383200169 0.7606624961 0.7677584291 - 0.8387917876 0.7613489032 0.7683190107 - 0.8392636776 0.7620353103 0.7688795924 - 0.8397355080 0.7627217770 0.7694401741 - 0.8402072787 0.7634081841 0.7700008154 - 0.8406791091 0.7640945911 0.7705613971 - 0.8411508799 0.7647809982 0.7711219788 - 0.8416199088 0.7654656768 0.7716754079 - 0.8420853019 0.7661474943 0.7722191811 - 0.8425505757 0.7668293118 0.7727630734 - 0.8430159092 0.7675111294 0.7733069062 - 0.8434813023 0.7681930065 0.7738506794 - 0.8439465761 0.7688748240 0.7743945122 - 0.8444120288 0.7695565820 0.7749384046 - 0.8448773026 0.7702383995 0.7754821777 - 0.8453407288 0.7709181905 0.7760233283 - 0.8458008170 0.7715933919 0.7765594125 - 0.8462607861 0.7722684741 0.7770954967 - 0.8467208147 0.7729436755 0.7776315808 - 0.8471809030 0.7736188173 0.7781677246 - 0.8476408720 0.7742940187 0.7787038088 - 0.8481009007 0.7749692202 0.7792398930 - 0.8485609889 0.7756443024 0.7797759771 - 0.8490195274 0.7763193250 0.7803099751 - 0.8494699001 0.7769930959 0.7808322906 - 0.8499203920 0.7776669264 0.7813544869 - 0.8503708243 0.7783406973 0.7818766832 - 0.8508211970 0.7790145278 0.7823989987 - 0.8512716889 0.7796882987 0.7829211950 - 0.8517221212 0.7803621292 0.7834433913 - 0.8521726131 0.7810359001 0.7839657068 - 0.8526229858 0.7817096710 0.7844879031 - 0.8530669808 0.7823808193 0.7850015163 - 0.8535096049 0.7830513120 0.7855132818 - 0.8539522886 0.7837216854 0.7860249877 - 0.8543949723 0.7843921781 0.7865368128 - 0.8548377156 0.7850626111 0.7870485187 - 0.8552803993 0.7857329845 0.7875602841 - 0.8557230234 0.7864034772 0.7880721092 - 0.8561657071 0.7870739102 0.7885838151 - 0.8566074967 0.7877444029 0.7890909910 - 0.8570477962 0.7884148955 0.7895907760 - 0.8574879766 0.7890855074 0.7900906205 - 0.8579282761 0.7897560000 0.7905902863 - 0.8583685756 0.7904264927 0.7910900712 - 0.8588088751 0.7910971045 0.7915899158 - 0.8592491746 0.7917675972 0.7920895815 - 0.8596894741 0.7924380898 0.7925894260 - 0.8601297736 0.7931085825 0.7930892110 - 0.8605728149 0.7937740088 0.7935796976 - 0.8610163927 0.7944378257 0.7940679789 - 0.8614599705 0.7951015830 0.7945562005 - 0.8619036078 0.7957653999 0.7950444818 - 0.8623471856 0.7964292169 0.7955328226 - 0.8627907038 0.7970929742 0.7960211039 - 0.8632342815 0.7977567911 0.7965093255 - 0.8636779189 0.7984206080 0.7969976068 - 0.8641206026 0.7990841866 0.7974848747 - 0.8645566106 0.7997450233 0.7979636192 - 0.8649924994 0.8004058003 0.7984423041 - 0.8654283881 0.8010665774 0.7989211082 - 0.8658643961 0.8017274141 0.7993997931 - 0.8663002849 0.8023881912 0.7998784781 - 0.8667361736 0.8030490279 0.8003572226 - 0.8671721816 0.8037098050 0.8008359075 - 0.8676080704 0.8043705821 0.8013145924 - 0.8680428267 0.8050302267 0.8017914295 - 0.8684741855 0.8056855798 0.8022627234 - 0.8689056039 0.8063411117 0.8027340770 - 0.8693370223 0.8069965243 0.8032054901 - 0.8697683811 0.8076519966 0.8036769032 - 0.8701997995 0.8083074093 0.8041483164 - 0.8706312180 0.8089628220 0.8046196103 - 0.8710625768 0.8096182942 0.8050910234 - 0.8714941144 0.8102737069 0.8055623770 - 0.8719242811 0.8109272718 0.8060321212 - 0.8723520041 0.8115758896 0.8064982295 - 0.8727797270 0.8122243881 0.8069642782 - 0.8732073903 0.8128730059 0.8074303865 - 0.8736351132 0.8135215044 0.8078963757 - 0.8740627766 0.8141701221 0.8083624840 - 0.8744904995 0.8148186207 0.8088285923 - 0.8749181032 0.8154671788 0.8092945814 - 0.8753458261 0.8161156774 0.8097606897 - 0.8757717013 0.8167629242 0.8102260828 - 0.8761911988 0.8174033165 0.8106889725 - 0.8766106963 0.8180438280 0.8111518025 - 0.8770301938 0.8186842203 0.8116146922 - 0.8774496913 0.8193246126 0.8120775819 - 0.8778691888 0.8199651241 0.8125404119 - 0.8782886863 0.8206055164 0.8130033016 - 0.8787081838 0.8212460279 0.8134661913 - 0.8791276813 0.8218864202 0.8139290214 - 0.8795471787 0.8225268722 0.8143919110 - 0.8799598813 0.8231630921 0.8148487806 - 0.8803725243 0.8237990737 0.8153055906 - 0.8807851076 0.8244349957 0.8157624006 - 0.8811976910 0.8250709772 0.8162190914 - 0.8816102743 0.8257070184 0.8166759014 - 0.8820229173 0.8263429999 0.8171327114 - 0.8824356198 0.8269789219 0.8175895214 - 0.8828482032 0.8276149035 0.8180462718 - 0.8832607865 0.8282508850 0.8185030818 - 0.8836668730 0.8288804889 0.8189595938 - 0.8840687871 0.8295053840 0.8194159269 - 0.8844708204 0.8301302791 0.8198722005 - 0.8848727942 0.8307551146 0.8203284740 - 0.8852748275 0.8313800097 0.8207848072 - 0.8856766820 0.8320049047 0.8212410808 - 0.8860787153 0.8326296806 0.8216974139 - 0.8864806890 0.8332545757 0.8221536875 - 0.8868827224 0.8338794708 0.8226100206 - 0.8872836828 0.8345038891 0.8230658770 - 0.8876764178 0.8351209760 0.8235191107 - 0.8880689740 0.8357381225 0.8239722848 - 0.8884617090 0.8363550901 0.8244253993 - 0.8888543844 0.8369721770 0.8248785734 - 0.8892471194 0.8375893235 0.8253316879 - 0.8896397948 0.8382064104 0.8257849216 - 0.8900324702 0.8388234973 0.8262379766 - 0.8904250860 0.8394405842 0.8266912103 - 0.8908178210 0.8400576711 0.8271443248 - 0.8912085891 0.8406690955 0.8275967836 - 0.8915970922 0.8412722945 0.8280482888 - 0.8919855952 0.8418754935 0.8284997940 - 0.8923739791 0.8424786925 0.8289512992 - 0.8927624822 0.8430818915 0.8294028044 - 0.8931509852 0.8436850905 0.8298543096 - 0.8935394287 0.8442882895 0.8303058147 - 0.8939278722 0.8448914886 0.8307573199 - 0.8943163753 0.8454946876 0.8312088251 - 0.8947048783 0.8460978866 0.8316602707 - 0.8950937986 0.8466901183 0.8321077824 - 0.8954830170 0.8472754955 0.8325530887 - 0.8958721757 0.8478608131 0.8329983950 - 0.8962613940 0.8484461904 0.8334437013 - 0.8966506124 0.8490315080 0.8338890076 - 0.8970397711 0.8496168852 0.8343343139 - 0.8974289894 0.8502022028 0.8347796202 - 0.8978182077 0.8507875204 0.8352249265 - 0.8982074261 0.8513728976 0.8356701732 - 0.8985965848 0.8519582152 0.8361154795 - 0.8989794254 0.8525332212 0.8365576863 - 0.8993595839 0.8531031013 0.8369985819 - 0.8997398019 0.8536729217 0.8374395967 - 0.9001200199 0.8542428017 0.8378804922 - 0.9005001783 0.8548126817 0.8383213878 - 0.9008803964 0.8553825021 0.8387622833 - 0.9012606144 0.8559523821 0.8392031789 - 0.9016407728 0.8565223217 0.8396441936 - 0.9020209908 0.8570920825 0.8400850892 - 0.9024012089 0.8576620221 0.8405259848 - 0.9027789235 0.8582289219 0.8409655094 - 0.9031550288 0.8587936759 0.8414040208 - 0.9035310745 0.8593583703 0.8418424726 - 0.9039072990 0.8599231839 0.8422811031 - 0.9042834044 0.8604879975 0.8427196145 - 0.9046595097 0.8610526919 0.8431581855 - 0.9050356150 0.8616175056 0.8435966969 - 0.9054117799 0.8621823192 0.8440352082 - 0.9057878852 0.8627470136 0.8444737792 - 0.9061639905 0.8633118272 0.8449122906 - 0.9065403938 0.8638728857 0.8453506231 - 0.9069172144 0.8644266129 0.8457884789 - 0.9072939754 0.8649802804 0.8462263942 - 0.9076709151 0.8655340075 0.8466644287 - 0.9080476761 0.8660876751 0.8471022844 - 0.9084244967 0.8666414022 0.8475401998 - 0.9088013172 0.8671951294 0.8479781151 - 0.9091781974 0.8677487969 0.8484159708 - 0.9095550179 0.8683025241 0.8488538861 - 0.9099317789 0.8688561916 0.8492919207 - 0.9103087187 0.8694099188 0.8497297764 - 0.9106770158 0.8699470162 0.8501740098 - 0.9110450745 0.8704831004 0.8506183028 - 0.9114131927 0.8710191846 0.8510627151 - 0.9117813110 0.8715553284 0.8515071273 - 0.9121494293 0.8720914125 0.8519514799 - 0.9125174880 0.8726274967 0.8523958921 - 0.9128857255 0.8731635213 0.8528403044 - 0.9132537842 0.8736996055 0.8532845974 - 0.9136219025 0.8742356896 0.8537290096 - 0.9139900208 0.8747717738 0.8541734219 - 0.9143564105 0.8753021955 0.8546187878 - 0.9147205949 0.8758236766 0.8550655246 - 0.9150847793 0.8763452768 0.8555122018 - 0.9154489040 0.8768668175 0.8559588790 - 0.9158130884 0.8773882985 0.8564056158 - 0.9161772728 0.8779098988 0.8568522930 - 0.9165413976 0.8784313798 0.8572989702 - 0.9169055820 0.8789529204 0.8577457070 - 0.9172698259 0.8794744015 0.8581923842 - 0.9176338911 0.8799960017 0.8586391807 - 0.9179980755 0.8805174828 0.8590859175 - 0.9183627963 0.8810274005 0.8595321178 - 0.9187275767 0.8815318942 0.8599780798 - 0.9190924168 0.8820363283 0.8604242206 - 0.9194573164 0.8825408220 0.8608701825 - 0.9198220968 0.8830453157 0.8613162041 - 0.9201869965 0.8835498095 0.8617622852 - 0.9205517769 0.8840541840 0.8622083068 - 0.9209166765 0.8845586777 0.8626543283 - 0.9212815166 0.8850631714 0.8631004095 - 0.9216464162 0.8855676055 0.8635463715 - 0.9220111966 0.8860720992 0.8639923930 - 0.9223688841 0.8865699172 0.8644400239 - 0.9227253199 0.8870661259 0.8648878932 - 0.9230818152 0.8875622749 0.8653358221 - 0.9234381914 0.8880584836 0.8657836914 - 0.9237946272 0.8885546923 0.8662316203 - 0.9241510034 0.8890509009 0.8666793704 - 0.9245073795 0.8895472288 0.8671272993 - 0.9248638153 0.8900433779 0.8675752282 - 0.9252201915 0.8905395865 0.8680230975 - 0.9255766869 0.8910357952 0.8684710264 - 0.9259331226 0.8915320039 0.8689187765 - 0.9262825251 0.8920152187 0.8693681955 - 0.9266303778 0.8924947977 0.8698179722 - 0.9269781709 0.8929743767 0.8702676892 - 0.9273260832 0.8934540153 0.8707175255 - 0.9276738763 0.8939337134 0.8711671829 - 0.9280217886 0.8944132924 0.8716170192 - 0.9283695817 0.8948928714 0.8720666766 - 0.9287174940 0.8953725100 0.8725165129 - 0.9290654063 0.8958520889 0.8729661703 - 0.9294131994 0.8963317275 0.8734158874 - 0.9297611117 0.8968113065 0.8738657236 - 0.9301087260 0.8972867727 0.8743184805 - 0.9304562211 0.8977593184 0.8747732043 - 0.9308035970 0.8982318044 0.8752279282 - 0.9311510921 0.8987044096 0.8756825924 - 0.9314985871 0.8991768956 0.8761373162 - 0.9318460226 0.8996493816 0.8765919805 - 0.9321935177 0.9001219869 0.8770467043 - 0.9325410128 0.9005944729 0.8775014281 - 0.9328885078 0.9010670781 0.8779560924 - 0.9332358837 0.9015396237 0.8784108758 - 0.9335833788 0.9020121098 0.8788655996 - 0.9339289069 0.9024819732 0.8793206811 - 0.9342687130 0.9029427171 0.8797768950 - 0.9346085787 0.9034032822 0.8802332282 - 0.9349483848 0.9038640261 0.8806893826 - 0.9352881908 0.9043247104 0.8811457157 - 0.9356281161 0.9047853947 0.8816019893 - 0.9359679222 0.9052460194 0.8820582032 - 0.9363077283 0.9057067037 0.8825144768 - 0.9366474748 0.9061673880 0.8829706907 - 0.9369874001 0.9066280723 0.8834270239 - 0.9373272061 0.9070886970 0.8838832974 - 0.9376670122 0.9075493813 0.8843395114 - 0.9379975200 0.9080064297 0.8847972751 - 0.9383242726 0.9084616899 0.8852556944 - 0.9386510849 0.9089168906 0.8857141137 - 0.9389780164 0.9093722105 0.8861724734 - 0.9393048286 0.9098274708 0.8866308928 - 0.9396315813 0.9102827907 0.8870893121 - 0.9399585128 0.9107381105 0.8875476718 - 0.9402853251 0.9111933708 0.8880060911 - 0.9406121969 0.9116486907 0.8884645104 - 0.9409390092 0.9121040106 0.8889229298 - 0.9412658215 0.9125592709 0.8893812895 - 0.9415925741 0.9130145907 0.8898397088 - 0.9419143200 0.9134653211 0.8903028965 - 0.9422358871 0.9139158130 0.8907660246 - 0.9425575733 0.9143664241 0.8912292123 - 0.9428793192 0.9148169756 0.8916922808 - 0.9432010055 0.9152675271 0.8921554089 - 0.9435225725 0.9157180786 0.8926185966 - 0.9438443184 0.9161686897 0.8930817246 - 0.9441660047 0.9166191816 0.8935449123 - 0.9444875717 0.9170697927 0.8940079808 - 0.9448093176 0.9175204039 0.8944711089 - 0.9451310039 0.9179708958 0.8949342966 - 0.9454516768 0.9184200168 0.8953979015 - 0.9457663298 0.9188566804 0.8958650827 - 0.9460809231 0.9192935228 0.8963323236 - 0.9463955164 0.9197301865 0.8967995048 - 0.9467101097 0.9201670289 0.8972666860 - 0.9470247030 0.9206036925 0.8977338076 - 0.9473394156 0.9210404158 0.8982009888 - 0.9476540089 0.9214771986 0.8986682296 - 0.9479686022 0.9219139218 0.8991354108 - 0.9482831955 0.9223507047 0.8996024728 - 0.9485977888 0.9227874279 0.9000697136 - 0.9489123821 0.9232240915 0.9005368948 - 0.9492262006 0.9236606956 0.9010049105 - 0.9495301247 0.9240928888 0.9014831781 - 0.9498339891 0.9245252013 0.9019615054 - 0.9501379132 0.9249575138 0.9024397731 - 0.9504417777 0.9253897071 0.9029181004 - 0.9507457018 0.9258220196 0.9033964276 - 0.9510496259 0.9262542725 0.9038746953 - 0.9513534904 0.9266865253 0.9043530226 - 0.9516574144 0.9271187782 0.9048312902 - 0.9519612789 0.9275510907 0.9053096175 - 0.9522652030 0.9279832840 0.9057878852 - 0.9525691271 0.9284155965 0.9062662125 - 0.9528729916 0.9288479090 0.9067444801 - 0.9531697035 0.9292709231 0.9072285891 - 0.9534651041 0.9296919107 0.9077138901 - 0.9537603855 0.9301127791 0.9081990719 - 0.9540557265 0.9305338264 0.9086843729 - 0.9543510079 0.9309548140 0.9091696739 - 0.9546462893 0.9313756824 0.9096549749 - 0.9549415708 0.9317967296 0.9101402760 - 0.9552369118 0.9322175980 0.9106255770 - 0.9555321932 0.9326385856 0.9111108780 - 0.9558275938 0.9330595732 0.9115961194 - 0.9561228752 0.9334805012 0.9120814204 - 0.9564182162 0.9339014888 0.9125667214 - 0.9567111731 0.9343215823 0.9130539894 - 0.9570007920 0.9347404242 0.9135441184 - 0.9572904706 0.9351593256 0.9140341878 - 0.9575802088 0.9355781078 0.9145243168 - 0.9578698874 0.9359970093 0.9150145054 - 0.9581596255 0.9364159107 0.9155045748 - 0.9584493041 0.9368346930 0.9159947038 - 0.9587389827 0.9372535944 0.9164847732 - 0.9590287209 0.9376723766 0.9169749022 - 0.9593183994 0.9380912781 0.9174650908 - 0.9596080780 0.9385101199 0.9179552197 - 0.9598978162 0.9389290214 0.9184452891 - 0.9601874948 0.9393478036 0.9189354181 - 0.9604712129 0.9397596717 0.9194250703 - 0.9607533813 0.9401696920 0.9199147224 - 0.9610357285 0.9405797124 0.9204041958 - 0.9613178968 0.9409896135 0.9208937883 - 0.9616001844 0.9413995743 0.9213833809 - 0.9618824124 0.9418095946 0.9218729138 - 0.9621647000 0.9422196150 0.9223625064 - 0.9624469280 0.9426295757 0.9228520989 - 0.9627290964 0.9430395961 0.9233415723 - 0.9630113840 0.9434496164 0.9238312244 - 0.9632936120 0.9438595772 0.9243208170 - 0.9635758996 0.9442695975 0.9248104095 - 0.9638580084 0.9446796179 0.9252998829 - 0.9641314745 0.9450818896 0.9257894158 - 0.9644048810 0.9454842806 0.9262788892 - 0.9646784067 0.9458866119 0.9267684221 - 0.9649518728 0.9462888837 0.9272578955 - 0.9652252793 0.9466912150 0.9277474284 - 0.9654988050 0.9470934868 0.9282369018 - 0.9657722712 0.9474958181 0.9287263751 - 0.9660456777 0.9478980899 0.9292159081 - 0.9663192034 0.9483004808 0.9297053814 - 0.9665926099 0.9487028122 0.9301949143 - 0.9668660760 0.9491050839 0.9306843281 - 0.9671396017 0.9495074153 0.9311738014 - 0.9674128294 0.9499096274 0.9316633940 - 0.9676774144 0.9503046274 0.9321550131 - 0.9679421186 0.9506996274 0.9326465726 - 0.9682067037 0.9510946274 0.9331381917 - 0.9684712887 0.9514895082 0.9336298108 - 0.9687359929 0.9518845081 0.9341213703 - 0.9690005779 0.9522795081 0.9346129894 - 0.9692652225 0.9526745081 0.9351046085 - 0.9695299268 0.9530693889 0.9355962276 - 0.9697945118 0.9534643888 0.9360877872 - 0.9700590968 0.9538593888 0.9365794063 - 0.9703238010 0.9542543888 0.9370710254 - 0.9705883861 0.9546493292 0.9375625849 - 0.9708530903 0.9550443292 0.9380542040 - 0.9711099863 0.9554365277 0.9385489821 - 0.9713655114 0.9558281898 0.9390444160 - 0.9716209769 0.9562199116 0.9395396709 - 0.9718766212 0.9566115141 0.9400351048 - 0.9721320868 0.9570031762 0.9405304790 - 0.9723876119 0.9573948979 0.9410259128 - 0.9726430774 0.9577865005 0.9415212870 - 0.9728987217 0.9581782222 0.9420167208 - 0.9731541872 0.9585698247 0.9425120950 - 0.9734097123 0.9589614868 0.9430075288 - 0.9736652970 0.9593532085 0.9435027838 - 0.9739208221 0.9597448111 0.9439982176 - 0.9741762877 0.9601364732 0.9444935918 - 0.9744294882 0.9605246186 0.9449895024 - 0.9746801257 0.9609082937 0.9454861283 - 0.9749307036 0.9612920880 0.9459825754 - 0.9751814008 0.9616758823 0.9464792013 - 0.9754319787 0.9620596170 0.9469757080 - 0.9756826162 0.9624434114 0.9474722147 - 0.9759331942 0.9628272057 0.9479687810 - 0.9761837721 0.9632108808 0.9484652877 - 0.9764344096 0.9635946751 0.9489619136 - 0.9766849875 0.9639785290 0.9494584203 - 0.9769356251 0.9643622041 0.9499549270 - 0.9771862030 0.9647459984 0.9504514933 - 0.9774367809 0.9651297927 0.9509480000 - 0.9776874185 0.9655135274 0.9514446259 - 0.9779301882 0.9658905268 0.9519404769 - 0.9781720042 0.9662665129 0.9524363279 - 0.9784138203 0.9666424990 0.9529321790 - 0.9786555767 0.9670186043 0.9534279704 - 0.9788973927 0.9673945904 0.9539238811 - 0.9791392088 0.9677705765 0.9544196725 - 0.9793810248 0.9681466222 0.9549155831 - 0.9796229005 0.9685227275 0.9554113746 - 0.9798647165 0.9688987136 0.9559072852 - 0.9801064730 0.9692746997 0.9564030766 - 0.9803482890 0.9696506858 0.9568989873 - 0.9805901051 0.9700267911 0.9573947787 - 0.9808319211 0.9704027772 0.9578906894 - 0.9810727835 0.9707782269 0.9583864808 - 0.9813064933 0.9711478949 0.9588822722 - 0.9815400839 0.9715176225 0.9593781233 - 0.9817737937 0.9718871713 0.9598739147 - 0.9820073843 0.9722568989 0.9603695869 - 0.9822410941 0.9726266265 0.9608653784 - 0.9824746847 0.9729962945 0.9613612294 - 0.9827083945 0.9733660221 0.9618570209 - 0.9829419851 0.9737356901 0.9623526931 - 0.9831756949 0.9741054177 0.9628484845 - 0.9834094048 0.9744750857 0.9633442760 - 0.9836429954 0.9748448133 0.9638401270 - 0.9838767052 0.9752144814 0.9643357992 - 0.9841102958 0.9755842090 0.9648315907 - 0.9843431711 0.9759528041 0.9653275013 - 0.9845708013 0.9763129950 0.9658238292 - 0.9847983718 0.9766731858 0.9663200974 - 0.9850260019 0.9770334959 0.9668164253 - 0.9852535725 0.9773936868 0.9673126936 - 0.9854812026 0.9777538776 0.9678090215 - 0.9857087731 0.9781141877 0.9683052897 - 0.9859364033 0.9784743786 0.9688016176 - 0.9861639738 0.9788346291 0.9692978859 - 0.9863917232 0.9791948795 0.9697942138 - 0.9866192937 0.9795550704 0.9702904820 - 0.9868469238 0.9799153209 0.9707868099 - 0.9870744944 0.9802755713 0.9712830782 - 0.9873021245 0.9806358218 0.9717794061 - 0.9875296950 0.9809960127 0.9722756743 - 0.9877505898 0.9813417196 0.9727702737 - 0.9879710078 0.9816861153 0.9732648134 - 0.9881914258 0.9820305109 0.9737592936 - 0.9884117246 0.9823750257 0.9742537141 - 0.9886320829 0.9827194214 0.9747481942 - 0.9888525009 0.9830638170 0.9752426147 - 0.9890729189 0.9834082723 0.9757370949 - 0.9892932773 0.9837527275 0.9762315750 - 0.9895136952 0.9840971231 0.9767259955 - 0.9897341132 0.9844415188 0.9772204757 - 0.9899544716 0.9847859740 0.9777150154 - 0.9901748896 0.9851304293 0.9782093763 - 0.9903953075 0.9854748249 0.9787039161 - 0.9906157255 0.9858192801 0.9791983962 - 0.9908351898 0.9861568809 0.9796867967 - 0.9910538197 0.9864884019 0.9801697135 - 0.9912725091 0.9868198037 0.9806526899 - 0.9914911985 0.9871512055 0.9811356068 - 0.9917098284 0.9874826074 0.9816185832 - 0.9919285178 0.9878140092 0.9821015000 - 0.9921472073 0.9881454110 0.9825844765 - 0.9923658967 0.9884768724 0.9830673933 - 0.9925845265 0.9888082743 0.9835504293 - 0.9928032160 0.9891396761 0.9840332866 - 0.9930219054 0.9894710779 0.9845163226 - 0.9932404757 0.9898024797 0.9849991798 - 0.9934592247 0.9901340008 0.9854822159 - 0.9936779141 0.9904654026 0.9859650731 - 0.9938964844 0.9907968044 0.9864481091 - 0.9941064715 0.9911096096 0.9869107008 - 0.9943155050 0.9914203286 0.9873710275 - 0.9945245981 0.9917309880 0.9878314137 - 0.9947335720 0.9920417070 0.9882918000 - 0.9949426055 0.9923524261 0.9887521863 - 0.9951516986 0.9926630855 0.9892125726 - 0.9953606725 0.9929738045 0.9896730185 - 0.9955697060 0.9932845235 0.9901334047 - 0.9957787991 0.9935951829 0.9905936718 - 0.9959877729 0.9939059019 0.9910541177 - 0.9961968064 0.9942166209 0.9915145040 - 0.9964057803 0.9945272803 0.9919748902 - 0.9966148734 0.9948379993 0.9924352765 - 0.9968239069 0.9951487184 0.9928957224 - 0.9970322251 0.9954589009 0.9933549762 - 0.9972301126 0.9957615733 0.9937980175 - 0.9974278808 0.9960644245 0.9942409992 - 0.9976258278 0.9963670969 0.9946839809 - 0.9978235960 0.9966698885 0.9951270223 - 0.9980214834 0.9969726205 0.9955700040 - 0.9982193112 0.9972752929 0.9960129857 - 0.9984171987 0.9975780845 0.9964560270 - 0.9986150265 0.9978808165 0.9968990088 - 0.9988129139 0.9981836081 0.9973419905 - 0.9990106821 0.9984862804 0.9977849722 - 0.9992086291 0.9987890124 0.9982280135 - 0.9994063973 0.9990918040 0.9986709952 - 0.9996042848 0.9993944764 0.9991139770 - 0.9998021126 0.9996973276 0.9995570183 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0340563506 0.0275065694 0.0159309693 + 0.0581281595 0.0446017683 0.0286259800 + 0.0766732469 0.0585205704 0.0393032692 + 0.0912864283 0.0693459511 0.0486846492 + 0.1037112027 0.0783905834 0.0571168698 + 0.1149740964 0.0866255537 0.0647848472 + 0.1255443990 0.0943730623 0.0718255714 + 0.1358117014 0.1018529013 0.0783746019 + 0.1458894014 0.1092692986 0.0845360085 + 0.1554104984 0.1165430993 0.0902841464 + 0.1642753929 0.1235136986 0.0956311375 + 0.1725185066 0.1300638020 0.1007089019 + 0.1802159995 0.1362310052 0.1055677980 + 0.1874677986 0.1420270056 0.1101925969 + 0.1943995059 0.1475103050 0.1146183014 + 0.2010439038 0.1526764929 0.1188452989 + 0.2075248957 0.1576137990 0.1229263991 + 0.2138666064 0.1623017043 0.1268835962 + 0.2200817019 0.1667637974 0.1307622045 + 0.2262571007 0.1710768938 0.1345600933 + 0.2322959006 0.1752613038 0.1383388937 + 0.2381743044 0.1792901009 0.1420553029 + 0.2439599037 0.1831696928 0.1457192004 + 0.2495301962 0.1869384944 0.1493068933 + 0.2549104095 0.1905891001 0.1528359056 + 0.2600974143 0.1941210032 0.1562989056 + 0.2651267946 0.1975311041 0.1596667022 + 0.2699950933 0.2007963061 0.1629731953 + 0.2746872902 0.2039687932 0.1662473977 + 0.2792500854 0.2070634067 0.1694674939 + 0.2837044001 0.2100957930 0.1726872027 + 0.2880541086 0.2130752951 0.1758597940 + 0.2922781110 0.2160121948 0.1789810956 + 0.2963983119 0.2189164013 0.1820618957 + 0.3004223108 0.2217878997 0.1850834042 + 0.3043056130 0.2246080041 0.1880794019 + 0.3080697954 0.2273644060 0.1910150051 + 0.3117355108 0.2300619930 0.1939160973 + 0.3153220117 0.2327065021 0.1967920959 + 0.3188439906 0.2353048027 0.1996531039 + 0.3223091066 0.2378443033 0.2024717033 + 0.3257097900 0.2403286994 0.2052704990 + 0.3290207982 0.2427732944 0.2080609947 + 0.3322731853 0.2452045977 0.2108310014 + 0.3354865909 0.2476018071 0.2135528028 + 0.3386512101 0.2499758005 0.2162420005 + 0.3417851031 0.2523289025 0.2188850939 + 0.3448846936 0.2546671927 0.2214843929 + 0.3479135036 0.2569605112 0.2240575999 + 0.3508971930 0.2591898143 0.2266031057 + 0.3538430929 0.2613660097 0.2291243970 + 0.3567287922 0.2634985149 0.2316229939 + 0.3595845103 0.2655822933 0.2340888977 + 0.3624258935 0.2676230967 0.2365391999 + 0.3652456105 0.2696104944 0.2389807999 + 0.3680748940 0.2715514004 0.2413962036 + 0.3708611131 0.2734310031 0.2437824011 + 0.3735972047 0.2752597928 0.2461562008 + 0.3762691021 0.2770377994 0.2485167980 + 0.3789086044 0.2787886858 0.2508699000 + 0.3814966083 0.2805095911 0.2532047033 + 0.3840735853 0.2822039127 0.2555142939 + 0.3866173923 0.2838774920 0.2578059137 + 0.3891361058 0.2855109870 0.2600739002 + 0.3916285038 0.2871398032 0.2623229921 + 0.3941093087 0.2887691855 0.2645553052 + 0.3965596855 0.2904030979 0.2667697966 + 0.3989767134 0.2920188010 0.2689580023 + 0.4013713896 0.2936233878 0.2711285055 + 0.4037455022 0.2952230871 0.2732937932 + 0.4060991108 0.2968241870 0.2754479051 + 0.4084275067 0.2984308898 0.2775818110 + 0.4107517004 0.3000445962 0.2796877921 + 0.4130510986 0.3016583025 0.2817656994 + 0.4153209031 0.3032869101 0.2838352919 + 0.4175666869 0.3049050868 0.2858906984 + 0.4197983146 0.3065397143 0.2879244983 + 0.4220117033 0.3081744909 0.2899451852 + 0.4241906106 0.3098129034 0.2919526100 + 0.4263457954 0.3114565909 0.2939467132 + 0.4284875989 0.3130967915 0.2959286869 + 0.4306012988 0.3147349954 0.2978995144 + 0.4326950014 0.3163720965 0.2998577058 + 0.4347662032 0.3180099130 0.3018037975 + 0.4368287027 0.3196572065 0.3037435114 + 0.4388831854 0.3213165104 0.3056769073 + 0.4409214854 0.3229748011 0.3075988889 + 0.4429436028 0.3246040940 0.3095020056 + 0.4449625909 0.3262285888 0.3113960922 + 0.4469791055 0.3278538883 0.3132843077 + 0.4489935935 0.3294833004 0.3151693940 + 0.4509986937 0.3311119974 0.3170453012 + 0.4529815018 0.3327403069 0.3189097047 + 0.4549526870 0.3343681991 0.3207617104 + 0.4569247961 0.3359952867 0.3225966990 + 0.4588770866 0.3376198113 0.3244307935 + 0.4608316123 0.3392421007 0.3262627125 + 0.4627926052 0.3408617973 0.3280918002 + 0.4647417963 0.3424758017 0.3298951089 + 0.4666925967 0.3440926969 0.3316844106 + 0.4686441123 0.3457145989 0.3334603012 + 0.4705764949 0.3473288119 0.3352111876 + 0.4724830091 0.3489370942 0.3369530141 + 0.4743657112 0.3505378962 0.3386858106 + 0.4762322009 0.3521256149 0.3404065073 + 0.4780816138 0.3537108004 0.3421171010 + 0.4799089134 0.3552944958 0.3438138068 + 0.4817309082 0.3568659127 0.3454996943 + 0.4835452139 0.3584319055 0.3471781015 + 0.4853380024 0.3599914014 0.3488380909 + 0.4871332943 0.3615401089 0.3504877090 + 0.4889298975 0.3630783856 0.3521300852 + 0.4907036126 0.3646185100 0.3537611961 + 0.4924693108 0.3661572933 0.3553892970 + 0.4942173064 0.3676883876 0.3570109904 + 0.4959532917 0.3692195117 0.3586300910 + 0.4976806045 0.3707506955 0.3602473140 + 0.4994089901 0.3722806871 0.3618556857 + 0.5011367202 0.3738102019 0.3634608984 + 0.5028569102 0.3753306866 0.3650502861 + 0.5045688748 0.3768486083 0.3666361868 + 0.5062633157 0.3783572018 0.3682140112 + 0.5079504251 0.3798607886 0.3697811067 + 0.5096306801 0.3813571036 0.3713372052 + 0.5112873912 0.3828429878 0.3728885055 + 0.5129328966 0.3843210936 0.3744373918 + 0.5145742297 0.3857963085 0.3759793937 + 0.5162146091 0.3872702122 0.3775196970 + 0.5178315043 0.3887482882 0.3790464103 + 0.5194466114 0.3902272880 0.3805716932 + 0.5210654736 0.3916977048 0.3820922077 + 0.5226833224 0.3931671977 0.3836125135 + 0.5242862105 0.3946341872 0.3851282001 + 0.5258901119 0.3961010873 0.3866432905 + 0.5275049806 0.3975507915 0.3881508112 + 0.5291187763 0.3990002871 0.3896571100 + 0.5307204127 0.4004558921 0.3911463916 + 0.5323221087 0.4019115865 0.3926354945 + 0.5339249969 0.4033721089 0.3941121101 + 0.5355278850 0.4048328102 0.3955886960 + 0.5371186137 0.4062801898 0.3970466852 + 0.5387089849 0.4077258110 0.3985038996 + 0.5402762294 0.4091561139 0.3999438882 + 0.5418404937 0.4105820954 0.4013813138 + 0.5433958173 0.4120019972 0.4028140903 + 0.5449482799 0.4134188890 0.4042451978 + 0.5464795232 0.4148291945 0.4056676030 + 0.5479968190 0.4162329137 0.4070835114 + 0.5495104194 0.4176310897 0.4084953070 + 0.5510190725 0.4190171063 0.4099004865 + 0.5525225997 0.4204013050 0.4113028049 + 0.5540046096 0.4217655063 0.4126909971 + 0.5554866195 0.4231297076 0.4140791893 + 0.5569679737 0.4244849980 0.4154512882 + 0.5584493279 0.4258384109 0.4168216884 + 0.5599154234 0.4271919131 0.4181849957 + 0.5613729954 0.4285452962 0.4195438027 + 0.5628271103 0.4298977852 0.4209015071 + 0.5642738938 0.4312470853 0.4222564995 + 0.5657206774 0.4325963855 0.4236116111 + 0.5671554208 0.4339382946 0.4249607027 + 0.5685896277 0.4352790117 0.4263094068 + 0.5700142980 0.4366140068 0.4276516140 + 0.5714316964 0.4379422963 0.4289883971 + 0.5728462934 0.4392705858 0.4303236902 + 0.5742422938 0.4405961931 0.4316473901 + 0.5756382942 0.4419218898 0.4329712093 + 0.5770087838 0.4432437122 0.4342881143 + 0.5783665776 0.4445624053 0.4356010854 + 0.5797222257 0.4458796084 0.4369125068 + 0.5810663104 0.4471733868 0.4382139146 + 0.5824103951 0.4484671950 0.4395152032 + 0.5837360024 0.4497536123 0.4408144057 + 0.5850501060 0.4510329962 0.4421119988 + 0.5863640904 0.4523124993 0.4434092939 + 0.5876724124 0.4535883069 0.4446893036 + 0.5889807940 0.4548639953 0.4459693134 + 0.5902798176 0.4561336040 0.4472444057 + 0.5915662050 0.4573903978 0.4485121965 + 0.5928525925 0.4586473107 0.4497799873 + 0.5941187143 0.4598957002 0.4510425925 + 0.5953786969 0.4611397982 0.4523034990 + 0.5966371894 0.4623838067 0.4535635114 + 0.5978767872 0.4636138082 0.4548071027 + 0.5991163850 0.4648432136 0.4560508132 + 0.6003507972 0.4660690129 0.4572936893 + 0.6015748978 0.4672825933 0.4585348964 + 0.6027989984 0.4684962034 0.4597761929 + 0.6040027738 0.4697115123 0.4610106051 + 0.6051900983 0.4709289968 0.4622390866 + 0.6063774228 0.4721465111 0.4634675980 + 0.6075572968 0.4733569920 0.4646885097 + 0.6087344885 0.4745635092 0.4659062028 + 0.6099116802 0.4757699072 0.4671239853 + 0.6110656857 0.4769654870 0.4683316052 + 0.6122164130 0.4781579971 0.4695374966 + 0.6133670807 0.4793503881 0.4707433879 + 0.6145061851 0.4805291891 0.4719372094 + 0.6156448722 0.4817059934 0.4731302857 + 0.6167832017 0.4828827083 0.4743235111 + 0.6179025769 0.4840502143 0.4755181074 + 0.6190220714 0.4852168858 0.4767127037 + 0.6201403141 0.4863835871 0.4779073000 + 0.6212201118 0.4875405133 0.4790931046 + 0.6222999096 0.4886966050 0.4802789986 + 0.6233797073 0.4898526967 0.4814648926 + 0.6244452000 0.4909954071 0.4826442897 + 0.6255105734 0.4921365082 0.4838235974 + 0.6265758872 0.4932776093 0.4850027859 + 0.6276177168 0.4943977892 0.4861753881 + 0.6286571026 0.4955132902 0.4873471856 + 0.6296964884 0.4966287911 0.4885190129 + 0.6307348013 0.4977380931 0.4896813035 + 0.6317728162 0.4988445044 0.4908406138 + 0.6328107715 0.4999508858 0.4919998944 + 0.6338409781 0.5010526180 0.4931561053 + 0.6348659992 0.5021498799 0.4943102002 + 0.6358910799 0.5032472014 0.4954642057 + 0.6369110942 0.5043411851 0.4966160059 + 0.6379228830 0.5054274201 0.4977641106 + 0.6389346719 0.5065134764 0.4989121854 + 0.6399447918 0.5075992942 0.5000599027 + 0.6409429908 0.5086715221 0.5012037158 + 0.6419411898 0.5097436905 0.5023475885 + 0.6429393291 0.5108159184 0.5034914017 + 0.6439177990 0.5118789077 0.5046312809 + 0.6448913813 0.5129379034 0.5057700872 + 0.6458649039 0.5139970183 0.5069088936 + 0.6468328834 0.5150539875 0.5080435872 + 0.6477934122 0.5161070824 0.5091723204 + 0.6487538815 0.5171602964 0.5103009939 + 0.6497141719 0.5182133913 0.5114297271 + 0.6506603956 0.5192496181 0.5125468969 + 0.6516065001 0.5202844143 0.5136641264 + 0.6525526047 0.5213190913 0.5147812963 + 0.6534926891 0.5223445892 0.5158960819 + 0.6544277072 0.5233588815 0.5170087814 + 0.6553627253 0.5243731141 0.5181214809 + 0.6562976837 0.5253874063 0.5192341805 + 0.6572293043 0.5263924003 0.5203433037 + 0.6581609249 0.5273966789 0.5214524269 + 0.6590924859 0.5284010172 0.5225614905 + 0.6600255966 0.5294005275 0.5236659050 + 0.6609606147 0.5303912163 0.5247640014 + 0.6618955731 0.5313820243 0.5258620977 + 0.6628304720 0.5323727131 0.5269600749 + 0.6637492776 0.5333474278 0.5280525088 + 0.6646643877 0.5343161225 0.5291435122 + 0.6655796170 0.5352846980 0.5302343965 + 0.6664931774 0.5362530947 0.5313249230 + 0.6673964858 0.5372167230 0.5324121714 + 0.6682997942 0.5381802917 0.5334994793 + 0.6692031026 0.5391438007 0.5345867276 + 0.6701043844 0.5401027799 0.5356699824 + 0.6710026860 0.5410515070 0.5367466807 + 0.6719009876 0.5420001745 0.5378234982 + 0.6727992892 0.5429489017 0.5389001966 + 0.6736878753 0.5438944101 0.5399749279 + 0.6745703220 0.5448371172 0.5410481095 + 0.6754528284 0.5457797050 0.5421212912 + 0.6763352156 0.5467222929 0.5431944728 + 0.6772043705 0.5476604104 0.5442599058 + 0.6780694127 0.5485960841 0.5453224778 + 0.6789343953 0.5495318174 0.5463851094 + 0.6797993779 0.5504674911 0.5474476814 + 0.6806520224 0.5513973236 0.5485045910 + 0.6815025806 0.5523251891 0.5495604277 + 0.6823530793 0.5532531142 0.5506160855 + 0.6832036972 0.5541810989 0.5516719222 + 0.6840479970 0.5551018715 0.5527248979 + 0.6848915219 0.5560209155 0.5537775159 + 0.6857351065 0.5569398999 0.5548300743 + 0.6865785718 0.5578588843 0.5558826923 + 0.6874216795 0.5587705970 0.5569267869 + 0.6882647872 0.5596801043 0.5579692721 + 0.6891077757 0.5605894923 0.5590118766 + 0.6899508238 0.5614989996 0.5600544214 + 0.6907768250 0.5624088049 0.5610944033 + 0.6915978789 0.5633186102 0.5621334910 + 0.6924189925 0.5642284751 0.5631725788 + 0.6932401061 0.5651383996 0.5642117262 + 0.6940519214 0.5660479069 0.5652444959 + 0.6948580742 0.5669571161 0.5662732720 + 0.6956644058 0.5678663254 0.5673021078 + 0.6964706779 0.5687754750 0.5683308840 + 0.6972733736 0.5696833730 0.5693569779 + 0.6980714798 0.5705887079 0.5703790784 + 0.6988695264 0.5714939833 0.5714011788 + 0.6996675134 0.5723993182 0.5724232793 + 0.7004622221 0.5733036995 0.5734450221 + 0.7012382150 0.5741962790 0.5744643211 + 0.7020143270 0.5750889182 0.5754836798 + 0.7027903795 0.5759814978 0.5765029788 + 0.7035663724 0.5768741965 0.5775223970 + 0.7043431997 0.5777689219 0.5785338283 + 0.7051200867 0.5786644220 0.5795432925 + 0.7058969736 0.5795599818 0.5805526972 + 0.7066739202 0.5804554820 0.5815622211 + 0.7074453831 0.5813494921 0.5825701952 + 0.7082093954 0.5822402835 0.5835762024 + 0.7089732885 0.5831311941 0.5845820904 + 0.7097373009 0.5840221047 0.5855880976 + 0.7105011940 0.5849128962 0.5865939856 + 0.7112401128 0.5857965946 0.5875905156 + 0.7119778991 0.5866792202 0.5885863900 + 0.7127156258 0.5875617862 0.5895823240 + 0.7134532928 0.5884444714 0.5905781984 + 0.7141891718 0.5893244147 0.5915713906 + 0.7149226069 0.5901988745 0.5925608873 + 0.7156559825 0.5910735130 0.5935503244 + 0.7163894176 0.5919480920 0.5945397019 + 0.7171227932 0.5928226709 0.5955291986 + 0.7178444862 0.5936961174 0.5965164900 + 0.7185636759 0.5945690870 0.5975033045 + 0.7192828059 0.5954421759 0.5984901190 + 0.7200019956 0.5963152051 0.5994768739 + 0.7207183242 0.5971875191 0.6004620194 + 0.7214186788 0.5980507731 0.6014366150 + 0.7221192122 0.5989142060 0.6024110913 + 0.7228196263 0.5997775197 0.6033856869 + 0.7235201001 0.6006407738 0.6043602824 + 0.7242172956 0.6015027165 0.6053301096 + 0.7249100804 0.6023617983 0.6062927842 + 0.7256028056 0.6032208800 0.6072555184 + 0.7262955904 0.6040800214 0.6082183123 + 0.7269883156 0.6049389839 0.6091809869 + 0.7276725173 0.6057918072 0.6101375222 + 0.7283511758 0.6066387892 0.6110895872 + 0.7290298939 0.6074858904 0.6120417714 + 0.7297086120 0.6083328724 0.6129938960 + 0.7303872705 0.6091799736 0.6139460802 + 0.7310575843 0.6100224853 0.6148930788 + 0.7317247987 0.6108626127 0.6158381104 + 0.7323920727 0.6117026210 0.6167830825 + 0.7330592871 0.6125426888 0.6177281737 + 0.7337265015 0.6133828163 0.6186732054 + 0.7343794703 0.6142179966 0.6196153164 + 0.7350285053 0.6150509715 0.6205565929 + 0.7356774211 0.6158841252 0.6214978099 + 0.7363263965 0.6167172194 0.6224390864 + 0.7369753718 0.6175503135 0.6233803034 + 0.7376106977 0.6183786988 0.6243141890 + 0.7382416129 0.6192048192 0.6252453923 + 0.7388725281 0.6200309992 0.6261765957 + 0.7395033836 0.6208571196 0.6271077991 + 0.7401342988 0.6216832995 0.6280390024 + 0.7407568097 0.6225084066 0.6289677024 + 0.7413746715 0.6233326197 0.6298949122 + 0.7419925928 0.6241568923 0.6308221221 + 0.7426105142 0.6249812245 0.6317493916 + 0.7432283759 0.6258054972 0.6326766014 + 0.7438343167 0.6266260743 0.6335960031 + 0.7444267869 0.6274409294 0.6345061064 + 0.7450193167 0.6282557249 0.6354162097 + 0.7456117868 0.6290705204 0.6363263130 + 0.7462043166 0.6298853159 0.6372364163 + 0.7467948198 0.6306993961 0.6381446719 + 0.7473781109 0.6315096021 0.6390461922 + 0.7479612827 0.6323196888 0.6399477124 + 0.7485445142 0.6331298947 0.6408491731 + 0.7491278052 0.6339399815 0.6417506933 + 0.7497109771 0.6347501874 0.6426522136 + 0.7502849102 0.6355507970 0.6435450912 + 0.7508574128 0.6363490820 0.6444365978 + 0.7514299750 0.6371474862 0.6453281045 + 0.7520025969 0.6379457712 0.6462196112 + 0.7525750995 0.6387441158 0.6471111178 + 0.7531384230 0.6395425200 0.6479977965 + 0.7536910176 0.6403411031 0.6488788128 + 0.7542436123 0.6411396861 0.6497598290 + 0.7547962070 0.6419383287 0.6506407857 + 0.7553488016 0.6427369118 0.6515218019 + 0.7559013963 0.6435353756 0.6524028182 + 0.7564553022 0.6443219781 0.6532747746 + 0.7570090890 0.6451072097 0.6541464925 + 0.7575628757 0.6458923221 0.6550180912 + 0.7581167817 0.6466773748 0.6558898091 + 0.7586706281 0.6474626064 0.6567614079 + 0.7592213154 0.6482473016 0.6576269865 + 0.7597671151 0.6490309238 0.6584824920 + 0.7603129745 0.6498146057 0.6593379974 + 0.7608588934 0.6505982876 0.6601936221 + 0.7614046931 0.6513819098 0.6610491276 + 0.7619506121 0.6521655917 0.6619045734 + 0.7624893785 0.6529437900 0.6627510786 + 0.7630257010 0.6537193060 0.6635940075 + 0.7635619044 0.6544947028 0.6644368768 + 0.7640982270 0.6552700996 0.6652798057 + 0.7646344900 0.6560454965 0.6661226749 + 0.7651708126 0.6568210125 0.6669656038 + 0.7657119036 0.6575928926 0.6677941084 + 0.7662529945 0.6583644748 0.6686221957 + 0.7667940855 0.6591361761 0.6694504023 + 0.7673352957 0.6599078178 0.6702784896 + 0.7678763866 0.6606794000 0.6711065769 + 0.7684165239 0.6614500880 0.6719334722 + 0.7689508796 0.6622111201 0.6727535725 + 0.7694852948 0.6629719734 0.6735736132 + 0.7700197101 0.6637328863 0.6743935943 + 0.7705541849 0.6644939184 0.6752135754 + 0.7710886002 0.6652547717 0.6760336161 + 0.7716192007 0.6660150290 0.6768512130 + 0.7721378803 0.6667712927 0.6776608229 + 0.7726566792 0.6675274968 0.6784703732 + 0.7731754184 0.6682838202 0.6792799830 + 0.7736940980 0.6690400839 0.6800895929 + 0.7742128968 0.6697964072 0.6808992028 + 0.7747325897 0.6705529094 0.6817058921 + 0.7752555013 0.6713106036 0.6825023890 + 0.7757782936 0.6720684171 0.6832988858 + 0.7763012052 0.6728261113 0.6840953827 + 0.7768241167 0.6735839248 0.6848918796 + 0.7773470283 0.6743416190 0.6856883764 + 0.7778692245 0.6750988960 0.6864832044 + 0.7783874273 0.6758496761 0.6872665882 + 0.7789055705 0.6766005158 0.6880499721 + 0.7794237733 0.6773511767 0.6888334155 + 0.7799419165 0.6781020164 0.6896167994 + 0.7804601192 0.6788527966 0.6904001832 + 0.7809783220 0.6796035767 0.6911836267 + 0.7814986110 0.6803464890 0.6919562221 + 0.7820190191 0.6810883880 0.6927281022 + 0.7825394273 0.6818302870 0.6934999824 + 0.7830597758 0.6825721860 0.6942719221 + 0.7835801840 0.6833140850 0.6950438023 + 0.7841004729 0.6840559840 0.6958156824 + 0.7846269011 0.6847928166 0.6965779066 + 0.7851561904 0.6855260730 0.6973350048 + 0.7856854796 0.6862595081 0.6980922222 + 0.7862148285 0.6869928837 0.6988493204 + 0.7867441177 0.6877261996 0.6996064186 + 0.7872734070 0.6884595752 0.7003635764 + 0.7878013253 0.6891936064 0.7011185288 + 0.7883257270 0.6899294853 0.7018678784 + 0.7888501287 0.6906654835 0.7026172876 + 0.7893745899 0.6914014816 0.7033666968 + 0.7898989916 0.6921374202 0.7041161060 + 0.7904235125 0.6928734183 0.7048655152 + 0.7909479141 0.6936092973 0.7056149244 + 0.7914677858 0.6943377256 0.7063497901 + 0.7919868231 0.6950635910 0.7070816159 + 0.7925058007 0.6957895756 0.7078133821 + 0.7930247784 0.6965156198 0.7085452080 + 0.7935438156 0.6972414851 0.7092769742 + 0.7940626740 0.6979675293 0.7100086808 + 0.7945836782 0.6986932755 0.7107384205 + 0.7951102257 0.6994187832 0.7114617229 + 0.7956367135 0.7001444101 0.7121850252 + 0.7961632013 0.7008699179 0.7129083276 + 0.7966896892 0.7015954256 0.7136315703 + 0.7972161770 0.7023208737 0.7143548727 + 0.7977427244 0.7030463815 0.7150781751 + 0.7982670069 0.7037687898 0.7157930732 + 0.7987899780 0.7044885755 0.7165021896 + 0.7993130088 0.7052084208 0.7172113061 + 0.7998359799 0.7059280872 0.7179204226 + 0.8003590107 0.7066478729 0.7186294198 + 0.8008819818 0.7073677182 0.7193384767 + 0.8014048934 0.7080873847 0.7200475931 + 0.8019158244 0.7088059783 0.7207496762 + 0.8024243116 0.7095242143 0.7214503884 + 0.8029329181 0.7102422714 0.7221511006 + 0.8034414053 0.7109605074 0.7228518128 + 0.8039500117 0.7116786838 0.7235525250 + 0.8044584990 0.7123969197 0.7242532969 + 0.8049671054 0.7131149769 0.7249540091 + 0.8054732084 0.7138308883 0.7256454825 + 0.8059793115 0.7145465016 0.7263367176 + 0.8064854145 0.7152621150 0.7270278931 + 0.8069915175 0.7159777284 0.7277191877 + 0.8074976206 0.7166932821 0.7284104228 + 0.8080037236 0.7174088955 0.7291015983 + 0.8085097075 0.7181245089 0.7297927737 + 0.8090131879 0.7188358903 0.7304673791 + 0.8095167279 0.7195469737 0.7311419249 + 0.8100202084 0.7202581167 0.7318164706 + 0.8105236888 0.7209693193 0.7324910164 + 0.8110272288 0.7216804028 0.7331656218 + 0.8115307093 0.7223916054 0.7338401079 + 0.8120341897 0.7231026888 0.7345147133 + 0.8125367761 0.7238088846 0.7351782918 + 0.8130393028 0.7245143056 0.7358410954 + 0.8135418892 0.7252197862 0.7365038991 + 0.8140444160 0.7259252071 0.7371665835 + 0.8145468831 0.7266306877 0.7378293872 + 0.8150494099 0.7273361087 0.7384921908 + 0.8155519962 0.7280415893 0.7391549945 + 0.8160499930 0.7287421227 0.7398017049 + 0.8165466189 0.7294405103 0.7404432297 + 0.8170431852 0.7301387787 0.7410846949 + 0.8175398111 0.7308372259 0.7417262197 + 0.8180363774 0.7315356135 0.7423676848 + 0.8185330033 0.7322340012 0.7430092096 + 0.8190296888 0.7329323888 0.7436506748 + 0.8195242286 0.7336298227 0.7442870736 + 0.8200165033 0.7343257070 0.7449178100 + 0.8205088973 0.7350217104 0.7455484867 + 0.8210011721 0.7357177138 0.7461792231 + 0.8214935064 0.7364135981 0.7468098998 + 0.8219857812 0.7371096015 0.7474406958 + 0.8224781752 0.7378056049 0.7480713725 + 0.8229702115 0.7385014892 0.7487009168 + 0.8234596252 0.7391961217 0.7493168712 + 0.8239489794 0.7398905754 0.7499328852 + 0.8244382739 0.7405852079 0.7505488992 + 0.8249276876 0.7412797809 0.7511649132 + 0.8254171014 0.7419744134 0.7517809272 + 0.8259065151 0.7426689267 0.7523968816 + 0.8263958097 0.7433634996 0.7530128956 + 0.8268806934 0.7440590262 0.7536231279 + 0.8273624778 0.7447553277 0.7542291880 + 0.8278442025 0.7454516888 0.7548351884 + 0.8283259869 0.7461479902 0.7554411888 + 0.8288077116 0.7468442917 0.7560473084 + 0.8292894959 0.7475407124 0.7566533089 + 0.8297712207 0.7482370138 0.7572593093 + 0.8302530050 0.7489333153 0.7578654289 + 0.8307291269 0.7496249080 0.7584576011 + 0.8312050104 0.7503160834 0.7590492964 + 0.8316810131 0.7510071993 0.7596409917 + 0.8321568966 0.7516983747 0.7602326870 + 0.8326328993 0.7523894906 0.7608243823 + 0.8331087828 0.7530807257 0.7614160776 + 0.8335847259 0.7537717819 0.7620077729 + 0.8340600729 0.7544625998 0.7625957727 + 0.8345336914 0.7551519275 0.7631717920 + 0.8350073099 0.7558411956 0.7637478113 + 0.8354809284 0.7565305233 0.7643237710 + 0.8359544873 0.7572197914 0.7648997903 + 0.8364281058 0.7579091191 0.7654758096 + 0.8369017243 0.7585983872 0.7660518289 + 0.8373752832 0.7592877150 0.7666277885 + 0.8378481865 0.7599760294 0.7671977878 + 0.8383200169 0.7606624961 0.7677584291 + 0.8387917876 0.7613489032 0.7683190107 + 0.8392636776 0.7620353103 0.7688795924 + 0.8397355080 0.7627217770 0.7694401741 + 0.8402072787 0.7634081841 0.7700008154 + 0.8406791091 0.7640945911 0.7705613971 + 0.8411508799 0.7647809982 0.7711219788 + 0.8416199088 0.7654656768 0.7716754079 + 0.8420853019 0.7661474943 0.7722191811 + 0.8425505757 0.7668293118 0.7727630734 + 0.8430159092 0.7675111294 0.7733069062 + 0.8434813023 0.7681930065 0.7738506794 + 0.8439465761 0.7688748240 0.7743945122 + 0.8444120288 0.7695565820 0.7749384046 + 0.8448773026 0.7702383995 0.7754821777 + 0.8453407288 0.7709181905 0.7760233283 + 0.8458008170 0.7715933919 0.7765594125 + 0.8462607861 0.7722684741 0.7770954967 + 0.8467208147 0.7729436755 0.7776315808 + 0.8471809030 0.7736188173 0.7781677246 + 0.8476408720 0.7742940187 0.7787038088 + 0.8481009007 0.7749692202 0.7792398930 + 0.8485609889 0.7756443024 0.7797759771 + 0.8490195274 0.7763193250 0.7803099751 + 0.8494699001 0.7769930959 0.7808322906 + 0.8499203920 0.7776669264 0.7813544869 + 0.8503708243 0.7783406973 0.7818766832 + 0.8508211970 0.7790145278 0.7823989987 + 0.8512716889 0.7796882987 0.7829211950 + 0.8517221212 0.7803621292 0.7834433913 + 0.8521726131 0.7810359001 0.7839657068 + 0.8526229858 0.7817096710 0.7844879031 + 0.8530669808 0.7823808193 0.7850015163 + 0.8535096049 0.7830513120 0.7855132818 + 0.8539522886 0.7837216854 0.7860249877 + 0.8543949723 0.7843921781 0.7865368128 + 0.8548377156 0.7850626111 0.7870485187 + 0.8552803993 0.7857329845 0.7875602841 + 0.8557230234 0.7864034772 0.7880721092 + 0.8561657071 0.7870739102 0.7885838151 + 0.8566074967 0.7877444029 0.7890909910 + 0.8570477962 0.7884148955 0.7895907760 + 0.8574879766 0.7890855074 0.7900906205 + 0.8579282761 0.7897560000 0.7905902863 + 0.8583685756 0.7904264927 0.7910900712 + 0.8588088751 0.7910971045 0.7915899158 + 0.8592491746 0.7917675972 0.7920895815 + 0.8596894741 0.7924380898 0.7925894260 + 0.8601297736 0.7931085825 0.7930892110 + 0.8605728149 0.7937740088 0.7935796976 + 0.8610163927 0.7944378257 0.7940679789 + 0.8614599705 0.7951015830 0.7945562005 + 0.8619036078 0.7957653999 0.7950444818 + 0.8623471856 0.7964292169 0.7955328226 + 0.8627907038 0.7970929742 0.7960211039 + 0.8632342815 0.7977567911 0.7965093255 + 0.8636779189 0.7984206080 0.7969976068 + 0.8641206026 0.7990841866 0.7974848747 + 0.8645566106 0.7997450233 0.7979636192 + 0.8649924994 0.8004058003 0.7984423041 + 0.8654283881 0.8010665774 0.7989211082 + 0.8658643961 0.8017274141 0.7993997931 + 0.8663002849 0.8023881912 0.7998784781 + 0.8667361736 0.8030490279 0.8003572226 + 0.8671721816 0.8037098050 0.8008359075 + 0.8676080704 0.8043705821 0.8013145924 + 0.8680428267 0.8050302267 0.8017914295 + 0.8684741855 0.8056855798 0.8022627234 + 0.8689056039 0.8063411117 0.8027340770 + 0.8693370223 0.8069965243 0.8032054901 + 0.8697683811 0.8076519966 0.8036769032 + 0.8701997995 0.8083074093 0.8041483164 + 0.8706312180 0.8089628220 0.8046196103 + 0.8710625768 0.8096182942 0.8050910234 + 0.8714941144 0.8102737069 0.8055623770 + 0.8719242811 0.8109272718 0.8060321212 + 0.8723520041 0.8115758896 0.8064982295 + 0.8727797270 0.8122243881 0.8069642782 + 0.8732073903 0.8128730059 0.8074303865 + 0.8736351132 0.8135215044 0.8078963757 + 0.8740627766 0.8141701221 0.8083624840 + 0.8744904995 0.8148186207 0.8088285923 + 0.8749181032 0.8154671788 0.8092945814 + 0.8753458261 0.8161156774 0.8097606897 + 0.8757717013 0.8167629242 0.8102260828 + 0.8761911988 0.8174033165 0.8106889725 + 0.8766106963 0.8180438280 0.8111518025 + 0.8770301938 0.8186842203 0.8116146922 + 0.8774496913 0.8193246126 0.8120775819 + 0.8778691888 0.8199651241 0.8125404119 + 0.8782886863 0.8206055164 0.8130033016 + 0.8787081838 0.8212460279 0.8134661913 + 0.8791276813 0.8218864202 0.8139290214 + 0.8795471787 0.8225268722 0.8143919110 + 0.8799598813 0.8231630921 0.8148487806 + 0.8803725243 0.8237990737 0.8153055906 + 0.8807851076 0.8244349957 0.8157624006 + 0.8811976910 0.8250709772 0.8162190914 + 0.8816102743 0.8257070184 0.8166759014 + 0.8820229173 0.8263429999 0.8171327114 + 0.8824356198 0.8269789219 0.8175895214 + 0.8828482032 0.8276149035 0.8180462718 + 0.8832607865 0.8282508850 0.8185030818 + 0.8836668730 0.8288804889 0.8189595938 + 0.8840687871 0.8295053840 0.8194159269 + 0.8844708204 0.8301302791 0.8198722005 + 0.8848727942 0.8307551146 0.8203284740 + 0.8852748275 0.8313800097 0.8207848072 + 0.8856766820 0.8320049047 0.8212410808 + 0.8860787153 0.8326296806 0.8216974139 + 0.8864806890 0.8332545757 0.8221536875 + 0.8868827224 0.8338794708 0.8226100206 + 0.8872836828 0.8345038891 0.8230658770 + 0.8876764178 0.8351209760 0.8235191107 + 0.8880689740 0.8357381225 0.8239722848 + 0.8884617090 0.8363550901 0.8244253993 + 0.8888543844 0.8369721770 0.8248785734 + 0.8892471194 0.8375893235 0.8253316879 + 0.8896397948 0.8382064104 0.8257849216 + 0.8900324702 0.8388234973 0.8262379766 + 0.8904250860 0.8394405842 0.8266912103 + 0.8908178210 0.8400576711 0.8271443248 + 0.8912085891 0.8406690955 0.8275967836 + 0.8915970922 0.8412722945 0.8280482888 + 0.8919855952 0.8418754935 0.8284997940 + 0.8923739791 0.8424786925 0.8289512992 + 0.8927624822 0.8430818915 0.8294028044 + 0.8931509852 0.8436850905 0.8298543096 + 0.8935394287 0.8442882895 0.8303058147 + 0.8939278722 0.8448914886 0.8307573199 + 0.8943163753 0.8454946876 0.8312088251 + 0.8947048783 0.8460978866 0.8316602707 + 0.8950937986 0.8466901183 0.8321077824 + 0.8954830170 0.8472754955 0.8325530887 + 0.8958721757 0.8478608131 0.8329983950 + 0.8962613940 0.8484461904 0.8334437013 + 0.8966506124 0.8490315080 0.8338890076 + 0.8970397711 0.8496168852 0.8343343139 + 0.8974289894 0.8502022028 0.8347796202 + 0.8978182077 0.8507875204 0.8352249265 + 0.8982074261 0.8513728976 0.8356701732 + 0.8985965848 0.8519582152 0.8361154795 + 0.8989794254 0.8525332212 0.8365576863 + 0.8993595839 0.8531031013 0.8369985819 + 0.8997398019 0.8536729217 0.8374395967 + 0.9001200199 0.8542428017 0.8378804922 + 0.9005001783 0.8548126817 0.8383213878 + 0.9008803964 0.8553825021 0.8387622833 + 0.9012606144 0.8559523821 0.8392031789 + 0.9016407728 0.8565223217 0.8396441936 + 0.9020209908 0.8570920825 0.8400850892 + 0.9024012089 0.8576620221 0.8405259848 + 0.9027789235 0.8582289219 0.8409655094 + 0.9031550288 0.8587936759 0.8414040208 + 0.9035310745 0.8593583703 0.8418424726 + 0.9039072990 0.8599231839 0.8422811031 + 0.9042834044 0.8604879975 0.8427196145 + 0.9046595097 0.8610526919 0.8431581855 + 0.9050356150 0.8616175056 0.8435966969 + 0.9054117799 0.8621823192 0.8440352082 + 0.9057878852 0.8627470136 0.8444737792 + 0.9061639905 0.8633118272 0.8449122906 + 0.9065403938 0.8638728857 0.8453506231 + 0.9069172144 0.8644266129 0.8457884789 + 0.9072939754 0.8649802804 0.8462263942 + 0.9076709151 0.8655340075 0.8466644287 + 0.9080476761 0.8660876751 0.8471022844 + 0.9084244967 0.8666414022 0.8475401998 + 0.9088013172 0.8671951294 0.8479781151 + 0.9091781974 0.8677487969 0.8484159708 + 0.9095550179 0.8683025241 0.8488538861 + 0.9099317789 0.8688561916 0.8492919207 + 0.9103087187 0.8694099188 0.8497297764 + 0.9106770158 0.8699470162 0.8501740098 + 0.9110450745 0.8704831004 0.8506183028 + 0.9114131927 0.8710191846 0.8510627151 + 0.9117813110 0.8715553284 0.8515071273 + 0.9121494293 0.8720914125 0.8519514799 + 0.9125174880 0.8726274967 0.8523958921 + 0.9128857255 0.8731635213 0.8528403044 + 0.9132537842 0.8736996055 0.8532845974 + 0.9136219025 0.8742356896 0.8537290096 + 0.9139900208 0.8747717738 0.8541734219 + 0.9143564105 0.8753021955 0.8546187878 + 0.9147205949 0.8758236766 0.8550655246 + 0.9150847793 0.8763452768 0.8555122018 + 0.9154489040 0.8768668175 0.8559588790 + 0.9158130884 0.8773882985 0.8564056158 + 0.9161772728 0.8779098988 0.8568522930 + 0.9165413976 0.8784313798 0.8572989702 + 0.9169055820 0.8789529204 0.8577457070 + 0.9172698259 0.8794744015 0.8581923842 + 0.9176338911 0.8799960017 0.8586391807 + 0.9179980755 0.8805174828 0.8590859175 + 0.9183627963 0.8810274005 0.8595321178 + 0.9187275767 0.8815318942 0.8599780798 + 0.9190924168 0.8820363283 0.8604242206 + 0.9194573164 0.8825408220 0.8608701825 + 0.9198220968 0.8830453157 0.8613162041 + 0.9201869965 0.8835498095 0.8617622852 + 0.9205517769 0.8840541840 0.8622083068 + 0.9209166765 0.8845586777 0.8626543283 + 0.9212815166 0.8850631714 0.8631004095 + 0.9216464162 0.8855676055 0.8635463715 + 0.9220111966 0.8860720992 0.8639923930 + 0.9223688841 0.8865699172 0.8644400239 + 0.9227253199 0.8870661259 0.8648878932 + 0.9230818152 0.8875622749 0.8653358221 + 0.9234381914 0.8880584836 0.8657836914 + 0.9237946272 0.8885546923 0.8662316203 + 0.9241510034 0.8890509009 0.8666793704 + 0.9245073795 0.8895472288 0.8671272993 + 0.9248638153 0.8900433779 0.8675752282 + 0.9252201915 0.8905395865 0.8680230975 + 0.9255766869 0.8910357952 0.8684710264 + 0.9259331226 0.8915320039 0.8689187765 + 0.9262825251 0.8920152187 0.8693681955 + 0.9266303778 0.8924947977 0.8698179722 + 0.9269781709 0.8929743767 0.8702676892 + 0.9273260832 0.8934540153 0.8707175255 + 0.9276738763 0.8939337134 0.8711671829 + 0.9280217886 0.8944132924 0.8716170192 + 0.9283695817 0.8948928714 0.8720666766 + 0.9287174940 0.8953725100 0.8725165129 + 0.9290654063 0.8958520889 0.8729661703 + 0.9294131994 0.8963317275 0.8734158874 + 0.9297611117 0.8968113065 0.8738657236 + 0.9301087260 0.8972867727 0.8743184805 + 0.9304562211 0.8977593184 0.8747732043 + 0.9308035970 0.8982318044 0.8752279282 + 0.9311510921 0.8987044096 0.8756825924 + 0.9314985871 0.8991768956 0.8761373162 + 0.9318460226 0.8996493816 0.8765919805 + 0.9321935177 0.9001219869 0.8770467043 + 0.9325410128 0.9005944729 0.8775014281 + 0.9328885078 0.9010670781 0.8779560924 + 0.9332358837 0.9015396237 0.8784108758 + 0.9335833788 0.9020121098 0.8788655996 + 0.9339289069 0.9024819732 0.8793206811 + 0.9342687130 0.9029427171 0.8797768950 + 0.9346085787 0.9034032822 0.8802332282 + 0.9349483848 0.9038640261 0.8806893826 + 0.9352881908 0.9043247104 0.8811457157 + 0.9356281161 0.9047853947 0.8816019893 + 0.9359679222 0.9052460194 0.8820582032 + 0.9363077283 0.9057067037 0.8825144768 + 0.9366474748 0.9061673880 0.8829706907 + 0.9369874001 0.9066280723 0.8834270239 + 0.9373272061 0.9070886970 0.8838832974 + 0.9376670122 0.9075493813 0.8843395114 + 0.9379975200 0.9080064297 0.8847972751 + 0.9383242726 0.9084616899 0.8852556944 + 0.9386510849 0.9089168906 0.8857141137 + 0.9389780164 0.9093722105 0.8861724734 + 0.9393048286 0.9098274708 0.8866308928 + 0.9396315813 0.9102827907 0.8870893121 + 0.9399585128 0.9107381105 0.8875476718 + 0.9402853251 0.9111933708 0.8880060911 + 0.9406121969 0.9116486907 0.8884645104 + 0.9409390092 0.9121040106 0.8889229298 + 0.9412658215 0.9125592709 0.8893812895 + 0.9415925741 0.9130145907 0.8898397088 + 0.9419143200 0.9134653211 0.8903028965 + 0.9422358871 0.9139158130 0.8907660246 + 0.9425575733 0.9143664241 0.8912292123 + 0.9428793192 0.9148169756 0.8916922808 + 0.9432010055 0.9152675271 0.8921554089 + 0.9435225725 0.9157180786 0.8926185966 + 0.9438443184 0.9161686897 0.8930817246 + 0.9441660047 0.9166191816 0.8935449123 + 0.9444875717 0.9170697927 0.8940079808 + 0.9448093176 0.9175204039 0.8944711089 + 0.9451310039 0.9179708958 0.8949342966 + 0.9454516768 0.9184200168 0.8953979015 + 0.9457663298 0.9188566804 0.8958650827 + 0.9460809231 0.9192935228 0.8963323236 + 0.9463955164 0.9197301865 0.8967995048 + 0.9467101097 0.9201670289 0.8972666860 + 0.9470247030 0.9206036925 0.8977338076 + 0.9473394156 0.9210404158 0.8982009888 + 0.9476540089 0.9214771986 0.8986682296 + 0.9479686022 0.9219139218 0.8991354108 + 0.9482831955 0.9223507047 0.8996024728 + 0.9485977888 0.9227874279 0.9000697136 + 0.9489123821 0.9232240915 0.9005368948 + 0.9492262006 0.9236606956 0.9010049105 + 0.9495301247 0.9240928888 0.9014831781 + 0.9498339891 0.9245252013 0.9019615054 + 0.9501379132 0.9249575138 0.9024397731 + 0.9504417777 0.9253897071 0.9029181004 + 0.9507457018 0.9258220196 0.9033964276 + 0.9510496259 0.9262542725 0.9038746953 + 0.9513534904 0.9266865253 0.9043530226 + 0.9516574144 0.9271187782 0.9048312902 + 0.9519612789 0.9275510907 0.9053096175 + 0.9522652030 0.9279832840 0.9057878852 + 0.9525691271 0.9284155965 0.9062662125 + 0.9528729916 0.9288479090 0.9067444801 + 0.9531697035 0.9292709231 0.9072285891 + 0.9534651041 0.9296919107 0.9077138901 + 0.9537603855 0.9301127791 0.9081990719 + 0.9540557265 0.9305338264 0.9086843729 + 0.9543510079 0.9309548140 0.9091696739 + 0.9546462893 0.9313756824 0.9096549749 + 0.9549415708 0.9317967296 0.9101402760 + 0.9552369118 0.9322175980 0.9106255770 + 0.9555321932 0.9326385856 0.9111108780 + 0.9558275938 0.9330595732 0.9115961194 + 0.9561228752 0.9334805012 0.9120814204 + 0.9564182162 0.9339014888 0.9125667214 + 0.9567111731 0.9343215823 0.9130539894 + 0.9570007920 0.9347404242 0.9135441184 + 0.9572904706 0.9351593256 0.9140341878 + 0.9575802088 0.9355781078 0.9145243168 + 0.9578698874 0.9359970093 0.9150145054 + 0.9581596255 0.9364159107 0.9155045748 + 0.9584493041 0.9368346930 0.9159947038 + 0.9587389827 0.9372535944 0.9164847732 + 0.9590287209 0.9376723766 0.9169749022 + 0.9593183994 0.9380912781 0.9174650908 + 0.9596080780 0.9385101199 0.9179552197 + 0.9598978162 0.9389290214 0.9184452891 + 0.9601874948 0.9393478036 0.9189354181 + 0.9604712129 0.9397596717 0.9194250703 + 0.9607533813 0.9401696920 0.9199147224 + 0.9610357285 0.9405797124 0.9204041958 + 0.9613178968 0.9409896135 0.9208937883 + 0.9616001844 0.9413995743 0.9213833809 + 0.9618824124 0.9418095946 0.9218729138 + 0.9621647000 0.9422196150 0.9223625064 + 0.9624469280 0.9426295757 0.9228520989 + 0.9627290964 0.9430395961 0.9233415723 + 0.9630113840 0.9434496164 0.9238312244 + 0.9632936120 0.9438595772 0.9243208170 + 0.9635758996 0.9442695975 0.9248104095 + 0.9638580084 0.9446796179 0.9252998829 + 0.9641314745 0.9450818896 0.9257894158 + 0.9644048810 0.9454842806 0.9262788892 + 0.9646784067 0.9458866119 0.9267684221 + 0.9649518728 0.9462888837 0.9272578955 + 0.9652252793 0.9466912150 0.9277474284 + 0.9654988050 0.9470934868 0.9282369018 + 0.9657722712 0.9474958181 0.9287263751 + 0.9660456777 0.9478980899 0.9292159081 + 0.9663192034 0.9483004808 0.9297053814 + 0.9665926099 0.9487028122 0.9301949143 + 0.9668660760 0.9491050839 0.9306843281 + 0.9671396017 0.9495074153 0.9311738014 + 0.9674128294 0.9499096274 0.9316633940 + 0.9676774144 0.9503046274 0.9321550131 + 0.9679421186 0.9506996274 0.9326465726 + 0.9682067037 0.9510946274 0.9331381917 + 0.9684712887 0.9514895082 0.9336298108 + 0.9687359929 0.9518845081 0.9341213703 + 0.9690005779 0.9522795081 0.9346129894 + 0.9692652225 0.9526745081 0.9351046085 + 0.9695299268 0.9530693889 0.9355962276 + 0.9697945118 0.9534643888 0.9360877872 + 0.9700590968 0.9538593888 0.9365794063 + 0.9703238010 0.9542543888 0.9370710254 + 0.9705883861 0.9546493292 0.9375625849 + 0.9708530903 0.9550443292 0.9380542040 + 0.9711099863 0.9554365277 0.9385489821 + 0.9713655114 0.9558281898 0.9390444160 + 0.9716209769 0.9562199116 0.9395396709 + 0.9718766212 0.9566115141 0.9400351048 + 0.9721320868 0.9570031762 0.9405304790 + 0.9723876119 0.9573948979 0.9410259128 + 0.9726430774 0.9577865005 0.9415212870 + 0.9728987217 0.9581782222 0.9420167208 + 0.9731541872 0.9585698247 0.9425120950 + 0.9734097123 0.9589614868 0.9430075288 + 0.9736652970 0.9593532085 0.9435027838 + 0.9739208221 0.9597448111 0.9439982176 + 0.9741762877 0.9601364732 0.9444935918 + 0.9744294882 0.9605246186 0.9449895024 + 0.9746801257 0.9609082937 0.9454861283 + 0.9749307036 0.9612920880 0.9459825754 + 0.9751814008 0.9616758823 0.9464792013 + 0.9754319787 0.9620596170 0.9469757080 + 0.9756826162 0.9624434114 0.9474722147 + 0.9759331942 0.9628272057 0.9479687810 + 0.9761837721 0.9632108808 0.9484652877 + 0.9764344096 0.9635946751 0.9489619136 + 0.9766849875 0.9639785290 0.9494584203 + 0.9769356251 0.9643622041 0.9499549270 + 0.9771862030 0.9647459984 0.9504514933 + 0.9774367809 0.9651297927 0.9509480000 + 0.9776874185 0.9655135274 0.9514446259 + 0.9779301882 0.9658905268 0.9519404769 + 0.9781720042 0.9662665129 0.9524363279 + 0.9784138203 0.9666424990 0.9529321790 + 0.9786555767 0.9670186043 0.9534279704 + 0.9788973927 0.9673945904 0.9539238811 + 0.9791392088 0.9677705765 0.9544196725 + 0.9793810248 0.9681466222 0.9549155831 + 0.9796229005 0.9685227275 0.9554113746 + 0.9798647165 0.9688987136 0.9559072852 + 0.9801064730 0.9692746997 0.9564030766 + 0.9803482890 0.9696506858 0.9568989873 + 0.9805901051 0.9700267911 0.9573947787 + 0.9808319211 0.9704027772 0.9578906894 + 0.9810727835 0.9707782269 0.9583864808 + 0.9813064933 0.9711478949 0.9588822722 + 0.9815400839 0.9715176225 0.9593781233 + 0.9817737937 0.9718871713 0.9598739147 + 0.9820073843 0.9722568989 0.9603695869 + 0.9822410941 0.9726266265 0.9608653784 + 0.9824746847 0.9729962945 0.9613612294 + 0.9827083945 0.9733660221 0.9618570209 + 0.9829419851 0.9737356901 0.9623526931 + 0.9831756949 0.9741054177 0.9628484845 + 0.9834094048 0.9744750857 0.9633442760 + 0.9836429954 0.9748448133 0.9638401270 + 0.9838767052 0.9752144814 0.9643357992 + 0.9841102958 0.9755842090 0.9648315907 + 0.9843431711 0.9759528041 0.9653275013 + 0.9845708013 0.9763129950 0.9658238292 + 0.9847983718 0.9766731858 0.9663200974 + 0.9850260019 0.9770334959 0.9668164253 + 0.9852535725 0.9773936868 0.9673126936 + 0.9854812026 0.9777538776 0.9678090215 + 0.9857087731 0.9781141877 0.9683052897 + 0.9859364033 0.9784743786 0.9688016176 + 0.9861639738 0.9788346291 0.9692978859 + 0.9863917232 0.9791948795 0.9697942138 + 0.9866192937 0.9795550704 0.9702904820 + 0.9868469238 0.9799153209 0.9707868099 + 0.9870744944 0.9802755713 0.9712830782 + 0.9873021245 0.9806358218 0.9717794061 + 0.9875296950 0.9809960127 0.9722756743 + 0.9877505898 0.9813417196 0.9727702737 + 0.9879710078 0.9816861153 0.9732648134 + 0.9881914258 0.9820305109 0.9737592936 + 0.9884117246 0.9823750257 0.9742537141 + 0.9886320829 0.9827194214 0.9747481942 + 0.9888525009 0.9830638170 0.9752426147 + 0.9890729189 0.9834082723 0.9757370949 + 0.9892932773 0.9837527275 0.9762315750 + 0.9895136952 0.9840971231 0.9767259955 + 0.9897341132 0.9844415188 0.9772204757 + 0.9899544716 0.9847859740 0.9777150154 + 0.9901748896 0.9851304293 0.9782093763 + 0.9903953075 0.9854748249 0.9787039161 + 0.9906157255 0.9858192801 0.9791983962 + 0.9908351898 0.9861568809 0.9796867967 + 0.9910538197 0.9864884019 0.9801697135 + 0.9912725091 0.9868198037 0.9806526899 + 0.9914911985 0.9871512055 0.9811356068 + 0.9917098284 0.9874826074 0.9816185832 + 0.9919285178 0.9878140092 0.9821015000 + 0.9921472073 0.9881454110 0.9825844765 + 0.9923658967 0.9884768724 0.9830673933 + 0.9925845265 0.9888082743 0.9835504293 + 0.9928032160 0.9891396761 0.9840332866 + 0.9930219054 0.9894710779 0.9845163226 + 0.9932404757 0.9898024797 0.9849991798 + 0.9934592247 0.9901340008 0.9854822159 + 0.9936779141 0.9904654026 0.9859650731 + 0.9938964844 0.9907968044 0.9864481091 + 0.9941064715 0.9911096096 0.9869107008 + 0.9943155050 0.9914203286 0.9873710275 + 0.9945245981 0.9917309880 0.9878314137 + 0.9947335720 0.9920417070 0.9882918000 + 0.9949426055 0.9923524261 0.9887521863 + 0.9951516986 0.9926630855 0.9892125726 + 0.9953606725 0.9929738045 0.9896730185 + 0.9955697060 0.9932845235 0.9901334047 + 0.9957787991 0.9935951829 0.9905936718 + 0.9959877729 0.9939059019 0.9910541177 + 0.9961968064 0.9942166209 0.9915145040 + 0.9964057803 0.9945272803 0.9919748902 + 0.9966148734 0.9948379993 0.9924352765 + 0.9968239069 0.9951487184 0.9928957224 + 0.9970322251 0.9954589009 0.9933549762 + 0.9972301126 0.9957615733 0.9937980175 + 0.9974278808 0.9960644245 0.9942409992 + 0.9976258278 0.9963670969 0.9946839809 + 0.9978235960 0.9966698885 0.9951270223 + 0.9980214834 0.9969726205 0.9955700040 + 0.9982193112 0.9972752929 0.9960129857 + 0.9984171987 0.9975780845 0.9964560270 + 0.9986150265 0.9978808165 0.9968990088 + 0.9988129139 0.9981836081 0.9973419905 + 0.9990106821 0.9984862804 0.9977849722 + 0.9992086291 0.9987890124 0.9982280135 + 0.9994063973 0.9990918040 0.9986709952 + 0.9996042848 0.9993944764 0.9991139770 + 0.9998021126 0.9996973276 0.9995570183 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/fujifilm_f-400.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/fujifilm_f-400.spi1d index bd32b36f..0f41ba22 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/fujifilm_f-400.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/fujifilm_f-400.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0603632890 0.0454680510 0.0325050801 - 0.0916145369 0.0672334507 0.0560519211 - 0.1141282991 0.0832497999 0.0737638026 - 0.1319569051 0.0963873789 0.0874102712 - 0.1474671066 0.1078206971 0.0974757820 - 0.1610251069 0.1180547997 0.1053474993 - 0.1732669026 0.1273127049 0.1128624976 - 0.1844484061 0.1358955950 0.1203550026 - 0.1948004961 0.1437651068 0.1278492957 - 0.2044326067 0.1510639042 0.1352435946 - 0.2135390937 0.1580073982 0.1424624026 - 0.2222227007 0.1645933986 0.1493878067 - 0.2305278927 0.1708792001 0.1560759991 - 0.2384292036 0.1768720001 0.1626075953 - 0.2459527999 0.1825958937 0.1688462943 - 0.2531175911 0.1880602986 0.1748789996 - 0.2599833906 0.1933407038 0.1807505935 - 0.2665782869 0.1983918995 0.1864421070 - 0.2729392946 0.2032617927 0.1920263022 - 0.2790789902 0.2080129981 0.1974907964 - 0.2850643992 0.2126218975 0.2027806044 - 0.2908408046 0.2170598060 0.2078876942 - 0.2965438068 0.2213744968 0.2128470987 - 0.3020817935 0.2255962938 0.2176713943 - 0.3074505031 0.2297365963 0.2223367989 - 0.3126476109 0.2337452024 0.2268441021 - 0.3176782131 0.2376385927 0.2312131971 - 0.3225966990 0.2414472997 0.2354972064 - 0.3274005949 0.2451924980 0.2396982014 - 0.3320432901 0.2488888949 0.2437300980 - 0.3365654051 0.2524788976 0.2476260960 - 0.3410258889 0.2559857070 0.2514533103 - 0.3453224003 0.2594096065 0.2552187145 - 0.3495548964 0.2627862990 0.2589012980 - 0.3537135124 0.2661119998 0.2625198960 - 0.3577654064 0.2694039047 0.2660697997 - 0.3617587984 0.2726261914 0.2695469856 - 0.3657540083 0.2757805884 0.2729555070 - 0.3696199059 0.2788713872 0.2763257921 - 0.3734039068 0.2819064856 0.2796599865 - 0.3770940006 0.2849085927 0.2829268873 - 0.3806684911 0.2878705859 0.2861185074 - 0.3841705024 0.2907994092 0.2892411947 - 0.3875944912 0.2937094867 0.2923243046 - 0.3909009099 0.2965498865 0.2953714132 - 0.3941605091 0.2993265986 0.2983838916 - 0.3973405063 0.3020490110 0.3013431132 - 0.4004949033 0.3047288954 0.3042708039 - 0.4036108851 0.3073731959 0.3071705103 - 0.4066615999 0.3100095093 0.3100284040 - 0.4096829891 0.3126212955 0.3128500879 - 0.4127061069 0.3152045012 0.3156496882 - 0.4156985879 0.3177570999 0.3184089959 - 0.4186556935 0.3202543855 0.3211377859 - 0.4215945005 0.3227119148 0.3238492906 - 0.4244618118 0.3251400888 0.3265129030 - 0.4272699058 0.3275529146 0.3291609883 - 0.4300499856 0.3299373984 0.3317633867 - 0.4328002036 0.3323051929 0.3343206048 - 0.4354938865 0.3346739113 0.3368437886 - 0.4381406009 0.3370254934 0.3393223882 - 0.4407618046 0.3393487930 0.3417755067 - 0.4433783889 0.3416441083 0.3441849053 - 0.4459331930 0.3439005911 0.3465532959 - 0.4484468997 0.3461334109 0.3488821089 - 0.4509404898 0.3483205140 0.3511759937 - 0.4534223080 0.3504905999 0.3534452021 - 0.4558776915 0.3526290059 0.3557046950 - 0.4582909048 0.3547618091 0.3579384089 - 0.4606809914 0.3568730056 0.3601483107 - 0.4630529881 0.3589904904 0.3623368144 - 0.4653714001 0.3610835075 0.3645074964 - 0.4676778913 0.3631530106 0.3666630983 - 0.4699575007 0.3652003109 0.3688057065 - 0.4722292125 0.3671888113 0.3709379137 - 0.4745065868 0.3691641092 0.3730610013 - 0.4767518938 0.3711192012 0.3751786053 - 0.4789671004 0.3730652034 0.3772909939 - 0.4811646044 0.3749937117 0.3793990016 - 0.4833407998 0.3769024014 0.3814974129 - 0.4854697883 0.3788143098 0.3835780919 - 0.4875837862 0.3807007074 0.3856568933 - 0.4896746874 0.3826003969 0.3877165020 - 0.4917361140 0.3844895065 0.3897739947 - 0.4937821031 0.3863629997 0.3918241858 - 0.4958266914 0.3882220984 0.3938601017 - 0.4978322089 0.3900676966 0.3958753943 - 0.4998154044 0.3919059038 0.3978557885 - 0.5017874837 0.3937377036 0.3998196125 - 0.5037454963 0.3955607116 0.4017657936 - 0.5056617856 0.3973649144 0.4036836028 - 0.5075917840 0.3991670012 0.4055893123 - 0.5095139742 0.4009510875 0.4074887931 - 0.5114189982 0.4027169943 0.4093823135 - 0.5132684112 0.4044859111 0.4112665951 - 0.5151203871 0.4062362909 0.4131282866 - 0.5169649720 0.4079819024 0.4149852991 - 0.5187842846 0.4097360075 0.4168485999 - 0.5205675960 0.4114727974 0.4186882079 - 0.5223402977 0.4132055044 0.4205282927 - 0.5241093040 0.4149374068 0.4223743081 - 0.5258463025 0.4166536033 0.4241972864 - 0.5275856256 0.4183549881 0.4260225892 - 0.5293406248 0.4200339913 0.4278545082 - 0.5310854912 0.4217008948 0.4296846986 - 0.5328161120 0.4233531058 0.4315181971 - 0.5345103741 0.4249719083 0.4333597124 - 0.5362126827 0.4265843928 0.4351972938 - 0.5379176140 0.4281908870 0.4370343983 - 0.5396010876 0.4297637045 0.4388785958 - 0.5412700772 0.4313359857 0.4407181144 - 0.5429202914 0.4329077005 0.4425528049 - 0.5445725918 0.4344569147 0.4443916082 - 0.5462222099 0.4360015094 0.4462321103 - 0.5478432775 0.4375520945 0.4480772018 - 0.5494470000 0.4390931129 0.4499236941 - 0.5510246158 0.4406209886 0.4517717957 - 0.5525965095 0.4421330988 0.4536193013 - 0.5541654825 0.4436380863 0.4554666877 - 0.5557032228 0.4451265037 0.4573141932 - 0.5572413206 0.4466089010 0.4591605067 - 0.5587986708 0.4480651915 0.4610021114 - 0.5603525043 0.4495176077 0.4628401995 - 0.5618990064 0.4509631097 0.4646731913 - 0.5634241104 0.4523977935 0.4664989114 - 0.5649273992 0.4538224936 0.4683187902 - 0.5664381981 0.4552442133 0.4701269865 - 0.5679532290 0.4566642940 0.4719302058 - 0.5694528818 0.4580616951 0.4737173021 - 0.5709474087 0.4594523013 0.4755006135 - 0.5723990798 0.4608454108 0.4772619009 - 0.5738419890 0.4622389972 0.4790205061 - 0.5752863288 0.4636129141 0.4807558954 - 0.5767307281 0.4649848938 0.4824900925 - 0.5781518221 0.4663662910 0.4842281938 - 0.5795705914 0.4677484035 0.4859664142 - 0.5809577703 0.4691171050 0.4876765907 - 0.5823417902 0.4704849124 0.4893858135 - 0.5837211013 0.4718532860 0.4910922945 - 0.5850995779 0.4732218087 0.4927985966 - 0.5864555240 0.4745810032 0.4944742918 - 0.5878059864 0.4759384096 0.4961453974 - 0.5891577005 0.4772793949 0.4978106916 - 0.5905098915 0.4786145985 0.4994742870 - 0.5918568969 0.4799452126 0.5011221766 - 0.5932002068 0.4812729955 0.5027614236 - 0.5945292115 0.4825868905 0.5043945909 - 0.5958389044 0.4838840961 0.5060214996 - 0.5971521139 0.4851810038 0.5076392889 - 0.5984765887 0.4864774048 0.5092359781 - 0.5998010039 0.4877730906 0.5108314157 - 0.6011152864 0.4890460968 0.5124111176 - 0.6024296284 0.4903191030 0.5139907002 - 0.6037279963 0.4915966988 0.5155413747 - 0.6050211787 0.4928756058 0.5170856118 - 0.6063179970 0.4941464067 0.5186198950 - 0.6076188087 0.4954087138 0.5201451778 - 0.6089168191 0.4966705143 0.5216658711 - 0.6101980805 0.4979301095 0.5231689811 - 0.6114792824 0.4991895854 0.5246720910 - 0.6127241850 0.5004386902 0.5261573792 - 0.6139590144 0.5016852021 0.5276395082 - 0.6151936054 0.5029277205 0.5291115046 - 0.6164277196 0.5041645765 0.5305709839 - 0.6176617742 0.5054013729 0.5320304036 - 0.6188758016 0.5066292882 0.5334817767 - 0.6200888753 0.5078570247 0.5349332094 - 0.6212956905 0.5090678930 0.5363718271 - 0.6224961281 0.5102629066 0.5378000140 - 0.6236965060 0.5114579201 0.5392268896 - 0.6248922944 0.5126475096 0.5406283140 - 0.6260880828 0.5138370991 0.5420296192 - 0.6272730827 0.5150145888 0.5434191227 - 0.6284447908 0.5161781907 0.5447971225 - 0.6296163797 0.5173417926 0.5461751223 - 0.6307746768 0.5185059905 0.5475438237 - 0.6319308877 0.5196703076 0.5489118099 - 0.6330848932 0.5208293200 0.5502725840 - 0.6342328191 0.5219740868 0.5516172051 - 0.6353806257 0.5231189132 0.5529618263 - 0.6365122199 0.5242677927 0.5542978048 - 0.6376327872 0.5254191756 0.5556291938 - 0.6387532949 0.5265706778 0.5569607019 - 0.6398714185 0.5277099013 0.5582699180 - 0.6409891248 0.5288481116 0.5595781803 - 0.6421049833 0.5299884081 0.5608838797 - 0.6432113051 0.5311387777 0.5621806979 - 0.6443176270 0.5322890878 0.5634775162 - 0.6454114914 0.5334355235 0.5647640824 - 0.6464858055 0.5345761180 0.5660377741 - 0.6475601792 0.5357167125 0.5673115849 - 0.6486206055 0.5368447900 0.5685880780 - 0.6496704817 0.5379639268 0.5698662996 - 0.6507204175 0.5390831232 0.5711445212 - 0.6517605186 0.5401955843 0.5724067092 - 0.6527966261 0.5413054824 0.5736641288 - 0.6538326740 0.5424153805 0.5749214292 - 0.6548563242 0.5435019732 0.5761708021 - 0.6558768749 0.5445836782 0.5774188042 - 0.6568974853 0.5456653237 0.5786669254 - 0.6579241753 0.5467439890 0.5798960924 - 0.6589519978 0.5478222966 0.5811234713 - 0.6599798203 0.5489006042 0.5823509097 - 0.6609984040 0.5499593019 0.5835713744 - 0.6620156169 0.5510154963 0.5847914219 - 0.6630328894 0.5520715714 0.5860112906 - 0.6640465260 0.5531296730 0.5872145891 - 0.6650593877 0.5541881919 0.5884156823 - 0.6660723090 0.5552465916 0.5896167755 - 0.6670761704 0.5562924743 0.5908033848 - 0.6680768728 0.5573346019 0.5919865966 - 0.6690776944 0.5583767295 0.5931698084 - 0.6700693965 0.5594154000 0.5943410993 - 0.6710556746 0.5604522824 0.5955067277 - 0.6720418930 0.5614892244 0.5966722965 - 0.6730256081 0.5625199080 0.5978301167 - 0.6740061045 0.5635436177 0.5989803076 - 0.6749866009 0.5645673275 0.6001303792 - 0.6759673953 0.5655882955 0.6012763977 - 0.6769493818 0.5666012168 0.6024119258 - 0.6779313087 0.5676140785 0.6035472751 - 0.6789131761 0.5686269999 0.6046826243 - 0.6798806787 0.5696319938 0.6058049798 - 0.6808475256 0.5706369281 0.6069275141 - 0.6818143129 0.5716418028 0.6080499887 - 0.6827753782 0.5726358294 0.6091644168 - 0.6837335229 0.5736240149 0.6102753282 - 0.6846914887 0.5746123195 0.6113861203 - 0.6856489778 0.5755988955 0.6124936938 - 0.6866049767 0.5765814185 0.6135942936 - 0.6875609756 0.5775640011 0.6146950126 - 0.6885170937 0.5785465240 0.6157956719 - 0.6894577146 0.5795089006 0.6168872714 - 0.6903957129 0.5804682970 0.6179779172 - 0.6913337111 0.5814276934 0.6190685034 - 0.6922674179 0.5823866725 0.6201496124 - 0.6931942105 0.5833454132 0.6212183833 - 0.6941210032 0.5843039751 0.6222872138 - 0.6950476766 0.5852625966 0.6233559847 - 0.6959589720 0.5862057209 0.6244189143 - 0.6968678236 0.5871466994 0.6254813075 - 0.6977766156 0.5880876780 0.6265437007 - 0.6986793280 0.5890266895 0.6276041269 - 0.6995667219 0.5899611712 0.6286605000 - 0.7004541159 0.5908957720 0.6297169924 - 0.7013415098 0.5918303132 0.6307734251 - 0.7022203803 0.5927559733 0.6318181753 - 0.7030950189 0.5936775208 0.6328582764 - 0.7039695978 0.5945990086 0.6338983774 - 0.7048441172 0.5955204964 0.6349384189 - 0.7057145834 0.5964314938 0.6359745860 - 0.7065848708 0.5973424911 0.6370108128 - 0.7074552178 0.5982534289 0.6380469203 - 0.7083240151 0.5991623998 0.6390789747 - 0.7091879845 0.6000658870 0.6401014924 - 0.7100520134 0.6009693146 0.6411240101 - 0.7109159827 0.6018726826 0.6421465278 - 0.7117736936 0.6027743220 0.6431624889 - 0.7126240134 0.6036738157 0.6441721916 - 0.7134742141 0.6045733094 0.6451817751 - 0.7143244743 0.6054729223 0.6461914778 - 0.7151702046 0.6063696742 0.6471874118 - 0.7160130143 0.6072648764 0.6481761932 - 0.7168557048 0.6081600785 0.6491649151 - 0.7176983953 0.6090552807 0.6501536965 - 0.7185257077 0.6099364161 0.6511443853 - 0.7193462253 0.6108118892 0.6521357298 - 0.7201668024 0.6116873026 0.6531270146 - 0.7209873199 0.6125627756 0.6541182995 - 0.7217960954 0.6134387851 0.6550964117 - 0.7226005793 0.6143149137 0.6560708880 - 0.7234051228 0.6151911020 0.6570454240 - 0.7242096066 0.6160672903 0.6580199003 - 0.7250012755 0.6169329286 0.6589906216 - 0.7257876992 0.6177945137 0.6599599719 - 0.7265741825 0.6186562181 0.6609295011 - 0.7273607254 0.6195178032 0.6618989706 - 0.7281492949 0.6203829050 0.6628596187 - 0.7289391756 0.6212499738 0.6638159752 - 0.7297291160 0.6221169829 0.6647723913 - 0.7305189967 0.6229839921 0.6657288074 - 0.7313020229 0.6238461137 0.6666783094 - 0.7320774198 0.6247032285 0.6676219106 - 0.7328528762 0.6255602837 0.6685655117 - 0.7336283922 0.6264173985 0.6695091128 - 0.7344021201 0.6272711754 0.6704490185 - 0.7351711988 0.6281173825 0.6713812947 - 0.7359403968 0.6289635897 0.6723136902 - 0.7367094755 0.6298096776 0.6732460856 - 0.7374784946 0.6306557059 0.6741766930 - 0.7382401228 0.6314933896 0.6750792861 - 0.7390015721 0.6323310733 0.6759818792 - 0.7397632003 0.6331688166 0.6768844724 - 0.7405247092 0.6340065002 0.6777871251 - 0.7412760854 0.6348341107 0.6786838174 - 0.7420225143 0.6356573105 0.6795783043 - 0.7427688837 0.6364803910 0.6804727912 - 0.7435153127 0.6373035908 0.6813672781 - 0.7442601919 0.6381258965 0.6822535992 - 0.7450010777 0.6389461756 0.6831231713 - 0.7457420230 0.6397663951 0.6839928031 - 0.7464829087 0.6405866742 0.6848623753 - 0.7472237945 0.6414070129 0.6857320070 - 0.7479677796 0.6422203183 0.6865984797 - 0.7487124205 0.6430320740 0.6874645948 - 0.7494571209 0.6438438892 0.6883307099 - 0.7502018213 0.6446557045 0.6891968250 - 0.7509424090 0.6454676986 0.6900563836 - 0.7516725063 0.6462804079 0.6909027100 - 0.7524024844 0.6470929980 0.6917490959 - 0.7531325817 0.6479057074 0.6925954223 - 0.7538626790 0.6487182975 0.6934418082 - 0.7545816898 0.6495214105 0.6942890882 - 0.7552953959 0.6503202915 0.6951367855 - 0.7560091019 0.6511192918 0.6959844828 - 0.7567229271 0.6519181728 0.6968321800 - 0.7574365735 0.6527169943 0.6976789236 - 0.7581534982 0.6535111070 0.6985118985 - 0.7588703036 0.6543052197 0.6993448734 - 0.7595872283 0.6550993919 0.7001779079 - 0.7603039742 0.6558935046 0.7010108829 - 0.7610172033 0.6566845775 0.7018424273 - 0.7617210150 0.6574687958 0.7026708722 - 0.7624248266 0.6582528949 0.7034993768 - 0.7631285191 0.6590371132 0.7043278813 - 0.7638322711 0.6598212123 0.7051563859 - 0.7645226121 0.6606013775 0.7059795260 - 0.7651978135 0.6613773108 0.7067975998 - 0.7658728957 0.6621533036 0.7076157928 - 0.7665480971 0.6629291773 0.7084339857 - 0.7672232985 0.6637052298 0.7092521191 - 0.7678937912 0.6644771099 0.7100641727 - 0.7685611248 0.6652464867 0.7108727098 - 0.7692285180 0.6660159230 0.7116813064 - 0.7698957920 0.6667852998 0.7124899030 - 0.7705631256 0.6675546765 0.7132984996 - 0.7712234855 0.6683161855 0.7141026258 - 0.7718799114 0.6690734029 0.7149047256 - 0.7725363970 0.6698306799 0.7157067060 - 0.7731928825 0.6705878973 0.7165088058 - 0.7738493085 0.6713451743 0.7173109055 - 0.7744975090 0.6721003056 0.7181041241 - 0.7751405239 0.6728541255 0.7188926935 - 0.7757834792 0.6736080050 0.7196813822 - 0.7764264941 0.6743618250 0.7204700112 - 0.7770696282 0.6751157045 0.7212585807 - 0.7777153850 0.6758605838 0.7220466137 - 0.7783638835 0.6765974164 0.7228341103 - 0.7790123820 0.6773343086 0.7236214876 - 0.7796608806 0.6780712008 0.7244089842 - 0.7803093791 0.6788079739 0.7251964808 - 0.7809547782 0.6795421243 0.7259809971 - 0.7815937996 0.6802709103 0.7267609239 - 0.7822328806 0.6809996963 0.7275406718 - 0.7828719020 0.6817284822 0.7283204794 - 0.7835109830 0.6824573278 0.7291004062 - 0.7841491103 0.6831848025 0.7298780084 - 0.7847760916 0.6839002967 0.7306407094 - 0.7854030132 0.6846157908 0.7314034104 - 0.7860299945 0.6853312850 0.7321661711 - 0.7866569757 0.6860468984 0.7329288721 - 0.7872840166 0.6867623925 0.7336915731 - 0.7879158854 0.6874758005 0.7344440222 - 0.7885494828 0.6881886125 0.7351937890 - 0.7891830206 0.6889013052 0.7359436154 - 0.7898166180 0.6896141171 0.7366933227 - 0.7904502153 0.6903269291 0.7374430895 - 0.7910810709 0.6910367012 0.7381889224 - 0.7917066216 0.6917409897 0.7389283180 - 0.7923321128 0.6924453974 0.7396677136 - 0.7929577231 0.6931496859 0.7404071093 - 0.7935832143 0.6938540936 0.7411465049 - 0.7942087054 0.6945583820 0.7418857813 - 0.7948155999 0.6952574253 0.7426193953 - 0.7954189181 0.6959553957 0.7433521152 - 0.7960221171 0.6966534257 0.7440848947 - 0.7966253757 0.6973515153 0.7448176146 - 0.7972286940 0.6980494857 0.7455503941 - 0.7978311181 0.6987463832 0.7462819219 - 0.7984310985 0.6994404197 0.7470108271 - 0.7990310788 0.7001342773 0.7477396131 - 0.7996311188 0.7008281946 0.7484685183 - 0.8002310991 0.7015221715 0.7491973042 - 0.8008310795 0.7022160888 0.7499262094 - 0.8014271259 0.7029095888 0.7506538033 - 0.8020204902 0.7036026716 0.7513806820 - 0.8026139140 0.7042958140 0.7521076202 - 0.8032072783 0.7049888968 0.7528346181 - 0.8038007021 0.7056819797 0.7535614967 - 0.8043941855 0.7063751221 0.7542883754 - 0.8049713969 0.7070598006 0.7550076842 - 0.8055459857 0.7077432871 0.7557262182 - 0.8061205745 0.7084268928 0.7564446926 - 0.8066952229 0.7091103792 0.7571632266 - 0.8072698712 0.7097939253 0.7578817010 - 0.8078448176 0.7104768157 0.7585989237 - 0.8084272742 0.7111461759 0.7593011260 - 0.8090099096 0.7118155956 0.7600033879 - 0.8095924258 0.7124850154 0.7607055902 - 0.8101750016 0.7131544948 0.7614077926 - 0.8107575178 0.7138239145 0.7621099949 - 0.8113396168 0.7144926786 0.7628099918 - 0.8119179010 0.7151576281 0.7634986043 - 0.8124963045 0.7158225179 0.7641872764 - 0.8130745888 0.7164874077 0.7648758888 - 0.8136529922 0.7171524167 0.7655645013 - 0.8142312765 0.7178173065 0.7662531137 - 0.8148078918 0.7184810042 0.7669401169 - 0.8153703213 0.7191364169 0.7676184177 - 0.8159328103 0.7197918892 0.7682967782 - 0.8164951801 0.7204474211 0.7689750791 - 0.8170577288 0.7211027741 0.7696533799 - 0.8176200986 0.7217583060 0.7703316808 - 0.8181825876 0.7224134803 0.7710090876 - 0.8187462091 0.7230613232 0.7716705799 - 0.8193098903 0.7237089872 0.7723321915 - 0.8198735714 0.7243567705 0.7729938030 - 0.8204373121 0.7250046134 0.7736552954 - 0.8210009933 0.7256522775 0.7743169069 - 0.8215646148 0.7263001204 0.7749783993 - 0.8221263289 0.7269431949 0.7756360173 - 0.8226876259 0.7275854945 0.7762929201 - 0.8232489228 0.7282276750 0.7769498825 - 0.8238101006 0.7288699746 0.7776067853 - 0.8243713975 0.7295122743 0.7782638073 - 0.8249326944 0.7301545143 0.7789208293 - 0.8254861832 0.7307953238 0.7795671821 - 0.8260335922 0.7314348221 0.7802065015 - 0.8265808821 0.7320743799 0.7808458209 - 0.8271281719 0.7327139974 0.7814850807 - 0.8276755810 0.7333536148 0.7821244001 - 0.8282228708 0.7339931130 0.7827637196 - 0.8287672997 0.7346323729 0.7834032774 - 0.8292987943 0.7352702022 0.7840442061 - 0.8298302293 0.7359080911 0.7846850157 - 0.8303616047 0.7365459800 0.7853258848 - 0.8308929801 0.7371838093 0.7859666944 - 0.8314244747 0.7378216982 0.7866076231 - 0.8319559097 0.7384595275 0.7872483730 - 0.8324869275 0.7390930057 0.7878805995 - 0.8330175877 0.7397248745 0.7885100245 - 0.8335484266 0.7403566837 0.7891393900 - 0.8340792060 0.7409886122 0.7897688150 - 0.8346099257 0.7416204810 0.7903981805 - 0.8351407051 0.7422522902 0.7910276055 - 0.8356696963 0.7428832054 0.7916554809 - 0.8361886144 0.7435089946 0.7922765017 - 0.8367075920 0.7441349030 0.7928975821 - 0.8372265100 0.7447606921 0.7935186028 - 0.8377454281 0.7453864813 0.7941396832 - 0.8382642865 0.7460123897 0.7947607040 - 0.8387833238 0.7466381788 0.7953817844 - 0.8392910957 0.7472589016 0.7959994078 - 0.8397874236 0.7478746176 0.7966139913 - 0.8402836919 0.7484902740 0.7972285748 - 0.8407800198 0.7491058707 0.7978432178 - 0.8412762880 0.7497215867 0.7984578013 - 0.8417726159 0.7503373027 0.7990723848 - 0.8422688842 0.7509530187 0.7996870279 - 0.8427659869 0.7515618801 0.8002871871 - 0.8432633877 0.7521684170 0.8008829951 - 0.8437607884 0.7527748942 0.8014789224 - 0.8442581892 0.7533814907 0.8020746708 - 0.8447555900 0.7539880276 0.8026704788 - 0.8452529907 0.7545946240 0.8032664061 - 0.8457503915 0.7552011013 0.8038622141 - 0.8462408781 0.7558019757 0.8044577241 - 0.8467301726 0.7564020753 0.8050531149 - 0.8472195864 0.7570022941 0.8056485057 - 0.8477090001 0.7576023936 0.8062438965 - 0.8481984138 0.7582024932 0.8068394065 - 0.8486878276 0.7588025928 0.8074347973 - 0.8491771817 0.7594026923 0.8080301881 - 0.8496577144 0.7599987984 0.8086149096 - 0.8501371741 0.7605946064 0.8091987967 - 0.8506168127 0.7611902952 0.8097828031 - 0.8510962725 0.7617859840 0.8103666902 - 0.8515759110 0.7623817921 0.8109506965 - 0.8520553708 0.7629774809 0.8115345836 - 0.8525350094 0.7635732293 0.8121185899 - 0.8530108929 0.7641665936 0.8127080798 - 0.8534861207 0.7647594213 0.8132985234 - 0.8539612889 0.7653523088 0.8138889074 - 0.8544363976 0.7659451962 0.8144792914 - 0.8549116254 0.7665380836 0.8150696754 - 0.8553867936 0.7671309114 0.8156599998 - 0.8558620214 0.7677237988 0.8162503839 - 0.8563336730 0.7683117986 0.8168362975 - 0.8568035960 0.7688974142 0.8174201250 - 0.8572735190 0.7694829702 0.8180038929 - 0.8577435017 0.7700685859 0.8185877204 - 0.8582134247 0.7706542015 0.8191714883 - 0.8586832881 0.7712398171 0.8197553754 - 0.8591532111 0.7718253732 0.8203392029 - 0.8596208096 0.7724086046 0.8209149837 - 0.8600844741 0.7729880810 0.8214796185 - 0.8605483174 0.7735676765 0.8220440745 - 0.8610121012 0.7741472125 0.8226085901 - 0.8614758253 0.7747266889 0.8231731057 - 0.8619396091 0.7753062248 0.8237376213 - 0.8624032736 0.7758857012 0.8243020773 - 0.8628671169 0.7764651775 0.8248665929 - 0.8633220196 0.7770413160 0.8254293203 - 0.8637769222 0.7776173949 0.8259919882 - 0.8642318249 0.7781934738 0.8265547752 - 0.8646866083 0.7787696123 0.8271175027 - 0.8651415110 0.7793456912 0.8276802897 - 0.8655962944 0.7799218297 0.8282430172 - 0.8660511971 0.7804979086 0.8288056850 - 0.8665007949 0.7810693979 0.8293619752 - 0.8669447899 0.7816361785 0.8299123049 - 0.8673887849 0.7822030187 0.8304625750 - 0.8678327799 0.7827697992 0.8310130239 - 0.8682768941 0.7833365202 0.8315632939 - 0.8687208891 0.7839033008 0.8321136236 - 0.8691648841 0.7844700813 0.8326638937 - 0.8696088791 0.7850369215 0.8332142234 - 0.8700435758 0.7855976820 0.8337548971 - 0.8704770207 0.7861577868 0.8342946172 - 0.8709102869 0.7867177725 0.8348342776 - 0.8713436127 0.7872778773 0.8353739977 - 0.8717769980 0.7878379226 0.8359137177 - 0.8722103238 0.7883980274 0.8364533782 - 0.8726437092 0.7889580131 0.8369930983 - 0.8730763793 0.7895172834 0.8375316858 - 0.8735055923 0.7900726795 0.8380653262 - 0.8739346862 0.7906280756 0.8385987878 - 0.8743638992 0.7911834717 0.8391324282 - 0.8747929931 0.7917389274 0.8396660089 - 0.8752222061 0.7922942042 0.8401995897 - 0.8756513000 0.7928496003 0.8407331705 - 0.8760805130 0.7934049964 0.8412668109 - 0.8765051961 0.7939593196 0.8417996168 - 0.8769198060 0.7945113182 0.8423308134 - 0.8773342967 0.7950633168 0.8428620100 - 0.8777489066 0.7956153154 0.8433932066 - 0.8781633973 0.7961671948 0.8439245224 - 0.8785778880 0.7967191935 0.8444557190 - 0.8789924979 0.7972711921 0.8449869156 - 0.8794069886 0.7978231907 0.8455181122 - 0.8798193932 0.7983725071 0.8460463881 - 0.8802276850 0.7989166975 0.8465698957 - 0.8806359768 0.7994610071 0.8470934033 - 0.8810443282 0.8000053167 0.8476169109 - 0.8814526200 0.8005495071 0.8481404185 - 0.8818609118 0.8010938168 0.8486639261 - 0.8822692037 0.8016381264 0.8491873741 - 0.8826774955 0.8021823764 0.8497108817 - 0.8830862045 0.8027231097 0.8502314091 - 0.8834958076 0.8032547235 0.8507447243 - 0.8839054704 0.8037862182 0.8512579799 - 0.8843150735 0.8043177724 0.8517714143 - 0.8847246766 0.8048493266 0.8522847295 - 0.8851343989 0.8053808808 0.8527981043 - 0.8855440021 0.8059123755 0.8533114195 - 0.8859536052 0.8064439893 0.8538246751 - 0.8863626719 0.8069751859 0.8543373942 - 0.8867633939 0.8075023293 0.8548439145 - 0.8871641159 0.8080292940 0.8553503752 - 0.8875647783 0.8085563779 0.8558567762 - 0.8879655004 0.8090834022 0.8563632965 - 0.8883662820 0.8096104860 0.8568698168 - 0.8887670040 0.8101376295 0.8573762774 - 0.8891677260 0.8106645942 0.8578827977 - 0.8895683885 0.8111916780 0.8583891988 - 0.8899617195 0.8117120862 0.8588874936 - 0.8903526068 0.8122305274 0.8593834043 - 0.8907433748 0.8127487898 0.8598793149 - 0.8911342025 0.8132671118 0.8603752255 - 0.8915249705 0.8137853742 0.8608711958 - 0.8919157982 0.8143036962 0.8613671064 - 0.8923066854 0.8148220778 0.8618630171 - 0.8926975131 0.8153403997 0.8623589277 - 0.8930876851 0.8158568144 0.8628520966 - 0.8934766054 0.8163688183 0.8633397818 - 0.8938655853 0.8168808818 0.8638275266 - 0.8942545056 0.8173928857 0.8643152118 - 0.8946434259 0.8179048896 0.8648028970 - 0.8950322866 0.8184170127 0.8652905822 - 0.8954212070 0.8189290166 0.8657783270 - 0.8958101869 0.8194410801 0.8662660122 - 0.8961991072 0.8199530840 0.8667536974 - 0.8965854049 0.8204619288 0.8672325015 - 0.8969708085 0.8209697008 0.8677083850 - 0.8973562121 0.8214774132 0.8681843281 - 0.8977414966 0.8219851255 0.8686602116 - 0.8981269002 0.8224927783 0.8691360950 - 0.8985123038 0.8230004907 0.8696119785 - 0.8988977075 0.8235082030 0.8700879216 - 0.8992829919 0.8240159154 0.8705638051 - 0.8996679783 0.8245236278 0.8710392118 - 0.9000402093 0.8250293732 0.8715068102 - 0.9004123807 0.8255351782 0.8719742894 - 0.9007844925 0.8260409832 0.8724418879 - 0.9011567235 0.8265467882 0.8729094267 - 0.9015287757 0.8270524740 0.8733770251 - 0.9019010067 0.8275582790 0.8738445044 - 0.9022731781 0.8280640841 0.8743121028 - 0.9026452899 0.8285698891 0.8747795820 - 0.9030135274 0.8290756941 0.8752462268 - 0.9033654928 0.8295813203 0.8757094145 - 0.9037175179 0.8300868869 0.8761726022 - 0.9040694833 0.8305925131 0.8766359091 - 0.9044215083 0.8310980797 0.8770990968 - 0.9047734737 0.8316035867 0.8775622845 - 0.9051254988 0.8321092129 0.8780254722 - 0.9054775238 0.8326147795 0.8784887791 - 0.9058294892 0.8331204057 0.8789520264 - 0.9061816931 0.8336247802 0.8794112802 - 0.9065344930 0.8341253996 0.8798596859 - 0.9068872929 0.8346260190 0.8803079724 - 0.9072400928 0.8351265788 0.8807563782 - 0.9075930119 0.8356270790 0.8812047243 - 0.9079458117 0.8361276984 0.8816530704 - 0.9082986116 0.8366283178 0.8821014762 - 0.9086514115 0.8371288776 0.8825498223 - 0.9090042114 0.8376294971 0.8829982281 - 0.9093561769 0.8381295800 0.8834459186 - 0.9097027183 0.8386269808 0.8838903904 - 0.9100490808 0.8391245008 0.8843349218 - 0.9103956223 0.8396219015 0.8847793937 - 0.9107421041 0.8401193023 0.8852239251 - 0.9110885859 0.8406167030 0.8856683969 - 0.9114350080 0.8411141038 0.8861129880 - 0.9117814898 0.8416115046 0.8865575194 - 0.9121279716 0.8421089053 0.8870019913 - 0.9124745131 0.8426063061 0.8874465227 - 0.9128088951 0.8430963159 0.8878840804 - 0.9131420851 0.8435854912 0.8883212209 - 0.9134753942 0.8440747857 0.8887583017 - 0.9138085842 0.8445640802 0.8891953826 - 0.9141417742 0.8450533748 0.8896325231 - 0.9144750237 0.8455426097 0.8900696039 - 0.9148082137 0.8460319042 0.8905066848 - 0.9151415229 0.8465211987 0.8909438252 - 0.9154747128 0.8470103741 0.8913809061 - 0.9158115983 0.8474963903 0.8918085098 - 0.9161515832 0.8479797244 0.8922289014 - 0.9164916277 0.8484628797 0.8926492929 - 0.9168316126 0.8489462137 0.8930696249 - 0.9171715975 0.8494294286 0.8934900165 - 0.9175117016 0.8499127030 0.8939104080 - 0.9178516865 0.8503959179 0.8943307996 - 0.9181916714 0.8508791924 0.8947511911 - 0.9185317159 0.8513624072 0.8951715827 - 0.9188717008 0.8518455029 0.8955916166 - 0.9192115068 0.8523247838 0.8960055113 - 0.9195513129 0.8528040051 0.8964195251 - 0.9198911190 0.8532832265 0.8968334198 - 0.9202309847 0.8537623882 0.8972473741 - 0.9205707908 0.8542416096 0.8976613283 - 0.9209105968 0.8547207713 0.8980752230 - 0.9212504029 0.8552001119 0.8984891772 - 0.9215902090 0.8556792736 0.8989030719 - 0.9219300151 0.8561584949 0.8993170261 - 0.9222667813 0.8566346169 0.8997275829 - 0.9225986004 0.8571056724 0.9001333117 - 0.9229304194 0.8575767875 0.9005389214 - 0.9232621193 0.8580479026 0.9009444714 - 0.9235938787 0.8585190177 0.9013502002 - 0.9239256978 0.8589900732 0.9017558098 - 0.9242575169 0.8594611883 0.9021614194 - 0.9245892763 0.8599323034 0.9025670886 - 0.9249210954 0.8604034185 0.9029726982 - 0.9252529144 0.8608745933 0.9033783078 - 0.9255800247 0.8613454103 0.9037811160 - 0.9259036779 0.8618162274 0.9041818976 - 0.9262273908 0.8622869253 0.9045827985 - 0.9265511036 0.8627576232 0.9049836993 - 0.9268748164 0.8632283211 0.9053844810 - 0.9271985292 0.8636990190 0.9057853818 - 0.9275221825 0.8641697764 0.9061862826 - 0.9278460145 0.8646404743 0.9065871239 - 0.9281697273 0.8651111722 0.9069880247 - 0.9284933805 0.8655819297 0.9073889256 - 0.9288159013 0.8660464287 0.9077849984 - 0.9291377068 0.8665074706 0.9081788063 - 0.9294595122 0.8669685721 0.9085724950 - 0.9297813773 0.8674296737 0.9089663029 - 0.9301031828 0.8678907752 0.9093599916 - 0.9304249883 0.8683518767 0.9097537994 - 0.9307467937 0.8688129783 0.9101474881 - 0.9310687184 0.8692740798 0.9105412960 - 0.9313905239 0.8697351813 0.9109349847 - 0.9317123294 0.8701962829 0.9113287926 - 0.9320325851 0.8706560135 0.9117175937 - 0.9323515892 0.8711144924 0.9121026993 - 0.9326705933 0.8715730906 0.9124878049 - 0.9329895973 0.8720316887 0.9128729105 - 0.9333087206 0.8724902272 0.9132580161 - 0.9336277246 0.8729488254 0.9136431217 - 0.9339467287 0.8734074235 0.9140282273 - 0.9342656732 0.8738659024 0.9144132733 - 0.9345846772 0.8743245006 0.9147983789 - 0.9349036813 0.8747830987 0.9151834846 - 0.9352200031 0.8752396703 0.9155669212 - 0.9355298877 0.8756920099 0.9159467816 - 0.9358398914 0.8761444092 0.9163267016 - 0.9361498952 0.8765966892 0.9167066216 - 0.9364597797 0.8770490885 0.9170864820 - 0.9367697835 0.8775014877 0.9174664021 - 0.9370797873 0.8779538274 0.9178462029 - 0.9373897910 0.8784062266 0.9182261229 - 0.9376996756 0.8788586259 0.9186059833 - 0.9380096793 0.8793109059 0.9189859033 - 0.9383196831 0.8797633052 0.9193658233 - 0.9386215210 0.8802114129 0.9197325110 - 0.9389225245 0.8806591034 0.9200983047 - 0.9392235279 0.8811067939 0.9204640985 - 0.9395244718 0.8815544844 0.9208298922 - 0.9398255944 0.8820021749 0.9211956859 - 0.9401265979 0.8824499249 0.9215614796 - 0.9404276013 0.8828976154 0.9219272733 - 0.9407287240 0.8833453059 0.9222931266 - 0.9410297275 0.8837931156 0.9226589203 - 0.9413306713 0.8842408061 0.9230247140 - 0.9416282177 0.8846876025 0.9233872294 - 0.9419195056 0.8851327896 0.9237444997 - 0.9422109127 0.8855779767 0.9241018295 - 0.9425022006 0.8860232830 0.9244589806 - 0.9427934885 0.8864685297 0.9248163104 - 0.9430847764 0.8869137764 0.9251735210 - 0.9433761239 0.8873590231 0.9255307913 - 0.9436674714 0.8878043294 0.9258880019 - 0.9439588189 0.8882495165 0.9262452722 - 0.9442501068 0.8886947036 0.9266026020 - 0.9445413947 0.8891400099 0.9269598126 - 0.9448264241 0.8895812035 0.9273141026 - 0.9451081157 0.8900203109 0.9276670814 - 0.9453896880 0.8904594779 0.9280200005 - 0.9456713796 0.8908985853 0.9283729792 - 0.9459530115 0.8913378119 0.9287258983 - 0.9462347031 0.8917769194 0.9290788770 - 0.9465163946 0.8922160864 0.9294317961 - 0.9467980266 0.8926551938 0.9297847748 - 0.9470797181 0.8930943012 0.9301376939 - 0.9473612905 0.8935335279 0.9304906726 - 0.9476429820 0.8939725757 0.9308435917 - 0.9479179978 0.8944123983 0.9311866164 - 0.9481912255 0.8948522806 0.9315270782 - 0.9484645128 0.8952922225 0.9318677187 - 0.9487376809 0.8957322240 0.9322082996 - 0.9490109086 0.8961721063 0.9325488806 - 0.9492841959 0.8966119885 0.9328895211 - 0.9495574236 0.8970518708 0.9332301021 - 0.9498305917 0.8974918127 0.9335706830 - 0.9501038790 0.8979318142 0.9339113235 - 0.9503771067 0.8983716965 0.9342517853 - 0.9506502748 0.8988115788 0.9345924258 - 0.9509218931 0.8992447257 0.9349266291 - 0.9511929750 0.8996757269 0.9352591038 - 0.9514641166 0.9001067281 0.9355915189 - 0.9517351985 0.9005377293 0.9359238744 - 0.9520063996 0.9009686708 0.9362562895 - 0.9522774816 0.9013996720 0.9365887046 - 0.9525486231 0.9018306732 0.9369211197 - 0.9528197050 0.9022616744 0.9372535944 - 0.9530907869 0.9026926756 0.9375860095 - 0.9533619285 0.9031236768 0.9379184246 - 0.9536330104 0.9035546780 0.9382507801 - 0.9538990855 0.9039816856 0.9385809898 - 0.9541615248 0.9044054747 0.9389094710 - 0.9544237852 0.9048293829 0.9392380118 - 0.9546862245 0.9052531719 0.9395664930 - 0.9549484849 0.9056770802 0.9398950934 - 0.9552109241 0.9061009884 0.9402235746 - 0.9554733038 0.9065247774 0.9405521154 - 0.9557356238 0.9069486856 0.9408807158 - 0.9559980035 0.9073724747 0.9412091970 - 0.9562603235 0.9077963829 0.9415376782 - 0.9565227032 0.9082202911 0.9418662190 - 0.9567822218 0.9086430073 0.9421923161 - 0.9570304155 0.9090616107 0.9425098896 - 0.9572787285 0.9094802141 0.9428274035 - 0.9575269818 0.9098988175 0.9431449771 - 0.9577752948 0.9103174210 0.9434624910 - 0.9580236077 0.9107360244 0.9437801242 - 0.9582718015 0.9111546278 0.9440975785 - 0.9585201144 0.9115732908 0.9444152117 - 0.9587684274 0.9119918942 0.9447327256 - 0.9590166807 0.9124104977 0.9450501800 - 0.9592649937 0.9128291011 0.9453678131 - 0.9595133066 0.9132477045 0.9456853271 - 0.9597612023 0.9136605859 0.9459949136 - 0.9600089192 0.9140707254 0.9463008046 - 0.9602565765 0.9144809246 0.9466066957 - 0.9605042934 0.9148911238 0.9469127059 - 0.9607520103 0.9153013229 0.9472185969 - 0.9609997272 0.9157114029 0.9475244880 - 0.9612473845 0.9161216021 0.9478303790 - 0.9614952207 0.9165318012 0.9481363893 - 0.9617428780 0.9169420004 0.9484422803 - 0.9619905949 0.9173520803 0.9487481713 - 0.9622383118 0.9177622795 0.9490541816 - 0.9624860287 0.9181724787 0.9493600726 - 0.9627341032 0.9185807109 0.9496526122 - 0.9629822969 0.9189888239 0.9499449730 - 0.9632303715 0.9193968773 0.9502373934 - 0.9634785056 0.9198051095 0.9505296946 - 0.9637266994 0.9202132225 0.9508221149 - 0.9639747739 0.9206213951 0.9511144161 - 0.9642229080 0.9210295081 0.9514067769 - 0.9644711018 0.9214376211 0.9516991973 - 0.9647191763 0.9218457937 0.9519914985 - 0.9649673104 0.9222539067 0.9522839189 - 0.9652153850 0.9226620197 0.9525762200 - 0.9654629827 0.9230692983 0.9528676867 - 0.9657052755 0.9234678149 0.9531521797 - 0.9659475088 0.9238663912 0.9534366727 - 0.9661898017 0.9242649078 0.9537212253 - 0.9664319754 0.9246634841 0.9540057182 - 0.9666743279 0.9250620008 0.9542902112 - 0.9669165015 0.9254605770 0.9545747042 - 0.9671587944 0.9258590937 0.9548591971 - 0.9674010277 0.9262577295 0.9551436901 - 0.9676433206 0.9266563058 0.9554281235 - 0.9678854942 0.9270548224 0.9557126164 - 0.9681277871 0.9274533987 0.9559971094 - 0.9683696032 0.9278519750 0.9562814832 - 0.9685997963 0.9282519817 0.9565635920 - 0.9688299894 0.9286519885 0.9568457007 - 0.9690603018 0.9290519953 0.9571278095 - 0.9692904949 0.9294521213 0.9574099779 - 0.9695206881 0.9298521280 0.9576920867 - 0.9697510004 0.9302520752 0.9579741955 - 0.9699811935 0.9306520820 0.9582563043 - 0.9702113867 0.9310522079 0.9585384130 - 0.9704415798 0.9314522147 0.9588205814 - 0.9706718922 0.9318522215 0.9591026902 - 0.9709020853 0.9322522283 0.9593847990 - 0.9711322784 0.9326522946 0.9596669078 - 0.9713497162 0.9330475926 0.9599413872 - 0.9715638161 0.9334416986 0.9602140784 - 0.9717779160 0.9338359237 0.9604868889 - 0.9719918966 0.9342300296 0.9607595801 - 0.9722059965 0.9346240759 0.9610322714 - 0.9724200964 0.9350183010 0.9613050222 - 0.9726341963 0.9354124069 0.9615777731 - 0.9728482962 0.9358065724 0.9618505239 - 0.9730623960 0.9362006783 0.9621232152 - 0.9732764959 0.9365949035 0.9623959064 - 0.9734905958 0.9369890094 0.9626687169 - 0.9737046957 0.9373831749 0.9629414082 - 0.9739152193 0.9377775788 0.9632083178 - 0.9741197228 0.9381725192 0.9634661078 - 0.9743241072 0.9385674000 0.9637240171 - 0.9745286107 0.9389622211 0.9639819264 - 0.9747331142 0.9393571019 0.9642397761 - 0.9749376178 0.9397519827 0.9644976258 - 0.9751421213 0.9401469231 0.9647554755 - 0.9753466249 0.9405416846 0.9650133848 - 0.9755511284 0.9409366250 0.9652711749 - 0.9757555723 0.9413315058 0.9655290842 - 0.9759600759 0.9417263865 0.9657869935 - 0.9761645794 0.9421213269 0.9660447836 - 0.9763690233 0.9425160885 0.9663026929 - 0.9765669107 0.9429088235 0.9665576220 - 0.9767628908 0.9433009028 0.9668117166 - 0.9769588113 0.9436929822 0.9670658112 - 0.9771547914 0.9440850019 0.9673200250 - 0.9773507118 0.9444770813 0.9675741196 - 0.9775466919 0.9448692203 0.9678282142 - 0.9777426124 0.9452611804 0.9680823088 - 0.9779385924 0.9456533194 0.9683364034 - 0.9781345129 0.9460453987 0.9685904980 - 0.9783304930 0.9464374781 0.9688445926 - 0.9785264134 0.9468294978 0.9690986872 - 0.9787223935 0.9472215772 0.9693527818 - 0.9789183140 0.9476137161 0.9696068764 - 0.9791035056 0.9479907155 0.9698532224 - 0.9792885184 0.9483674169 0.9700992703 - 0.9794734120 0.9487441778 0.9703454971 - 0.9796584249 0.9491209984 0.9705916047 - 0.9798433781 0.9494978189 0.9708377719 - 0.9800283909 0.9498745203 0.9710839987 - 0.9802134037 0.9502512813 0.9713301063 - 0.9803982973 0.9506281018 0.9715762734 - 0.9805833101 0.9510049224 0.9718223810 - 0.9807683229 0.9513816237 0.9720686078 - 0.9809532762 0.9517583847 0.9723147750 - 0.9811382890 0.9521352053 0.9725608826 - 0.9813233018 0.9525119066 0.9728069901 - 0.9815040231 0.9528858066 0.9730427265 - 0.9816848040 0.9532597065 0.9732784033 - 0.9818655849 0.9536336064 0.9735140800 - 0.9820463061 0.9540073872 0.9737498760 - 0.9822270870 0.9543812871 0.9739856124 - 0.9824079275 0.9547551870 0.9742212892 - 0.9825885892 0.9551290274 0.9744570255 - 0.9827694297 0.9555029273 0.9746928215 - 0.9829502106 0.9558768272 0.9749284983 - 0.9831308722 0.9562507272 0.9751641750 - 0.9833117127 0.9566245079 0.9753999114 - 0.9834924936 0.9569984078 0.9756355882 - 0.9836732149 0.9573723078 0.9758713841 - 0.9838451743 0.9577409029 0.9761005044 - 0.9840152264 0.9581084251 0.9763283134 - 0.9841852784 0.9584758878 0.9765561223 - 0.9843552709 0.9588434100 0.9767838717 - 0.9845253229 0.9592108130 0.9770116806 - 0.9846953750 0.9595782757 0.9772394896 - 0.9848654270 0.9599457979 0.9774672985 - 0.9850354791 0.9603133202 0.9776951075 - 0.9852054715 0.9606807828 0.9779229164 - 0.9853755832 0.9610483050 0.9781507254 - 0.9855455756 0.9614158273 0.9783784747 - 0.9857156277 0.9617832899 0.9786062837 - 0.9858856797 0.9621506929 0.9788340926 - 0.9860520959 0.9625151753 0.9790583253 - 0.9862139821 0.9628759027 0.9792780876 - 0.9863759875 0.9632365704 0.9794977903 - 0.9865378737 0.9635972977 0.9797176123 - 0.9866998792 0.9639580250 0.9799373746 - 0.9868618250 0.9643186927 0.9801570773 - 0.9870237112 0.9646794200 0.9803768992 - 0.9871857166 0.9650400877 0.9805967212 - 0.9873476028 0.9654006958 0.9808164239 - 0.9875096083 0.9657614231 0.9810361862 - 0.9876714945 0.9661220908 0.9812560081 - 0.9878333807 0.9664828181 0.9814757109 - 0.9879953861 0.9668434858 0.9816954732 - 0.9881572723 0.9672042131 0.9819152951 - 0.9883103967 0.9675583243 0.9821261764 - 0.9884620905 0.9679114223 0.9823359251 - 0.9886137843 0.9682645202 0.9825456142 - 0.9887655973 0.9686176181 0.9827553034 - 0.9889172912 0.9689707160 0.9829649925 - 0.9890691042 0.9693238139 0.9831746817 - 0.9892207980 0.9696769118 0.9833843708 - 0.9893726110 0.9700300097 0.9835941195 - 0.9895243049 0.9703831077 0.9838038087 - 0.9896761179 0.9707363248 0.9840136170 - 0.9898278117 0.9710894227 0.9842233062 - 0.9899795055 0.9714425206 0.9844329953 - 0.9901313186 0.9717956185 0.9846426845 - 0.9902822971 0.9721478224 0.9848514795 - 0.9904264808 0.9724920988 0.9850528240 - 0.9905707240 0.9728363752 0.9852542281 - 0.9907147884 0.9731807113 0.9854555130 - 0.9908589721 0.9735249281 0.9856567979 - 0.9910032153 0.9738692045 0.9858580828 - 0.9911473989 0.9742134809 0.9860594273 - 0.9912915230 0.9745578170 0.9862607121 - 0.9914357066 0.9749019742 0.9864619970 - 0.9915798903 0.9752463102 0.9866632819 - 0.9917240739 0.9755905867 0.9868646264 - 0.9918681979 0.9759349227 0.9870659113 - 0.9920123816 0.9762790799 0.9872671962 - 0.9921566248 0.9766234159 0.9874684811 - 0.9922997952 0.9769669175 0.9876685143 - 0.9924352765 0.9773039818 0.9878587723 - 0.9925708771 0.9776411057 0.9880490899 - 0.9927064180 0.9779782295 0.9882394075 - 0.9928418994 0.9783154130 0.9884296060 - 0.9929775000 0.9786524773 0.9886199236 - 0.9931129813 0.9789896011 0.9888101816 - 0.9932485223 0.9793267250 0.9890004992 - 0.9933841228 0.9796637893 0.9891908169 - 0.9935196042 0.9800009727 0.9893810153 - 0.9936550856 0.9803380966 0.9895712733 - 0.9937906861 0.9806752205 0.9897615910 - 0.9939262271 0.9810122848 0.9899519086 - 0.9940617085 0.9813494086 0.9901422262 - 0.9941973090 0.9816865921 0.9903324246 - 0.9943200946 0.9820179939 0.9905114174 - 0.9944419265 0.9823489785 0.9906893969 - 0.9945635796 0.9826800227 0.9908673167 - 0.9946854115 0.9830108881 0.9910452962 - 0.9948071241 0.9833418727 0.9912232757 - 0.9949287772 0.9836729169 0.9914013147 - 0.9950506091 0.9840037823 0.9915792942 - 0.9951723218 0.9843348265 0.9917572737 - 0.9952940941 0.9846658111 0.9919353127 - 0.9954158068 0.9849967957 0.9921132922 - 0.9955375791 0.9853277206 0.9922912717 - 0.9956592917 0.9856587052 0.9924693108 - 0.9957811236 0.9859896898 0.9926472902 - 0.9959027767 0.9863206148 0.9928253293 - 0.9960159063 0.9866454005 0.9930018783 - 0.9961208105 0.9869645238 0.9931772947 - 0.9962257147 0.9872835279 0.9933525920 - 0.9963306785 0.9876025915 0.9935278893 - 0.9964355826 0.9879215956 0.9937033057 - 0.9965404868 0.9882405996 0.9938786030 - 0.9966455102 0.9885597229 0.9940540195 - 0.9967504144 0.9888787270 0.9942293167 - 0.9968553185 0.9891976714 0.9944046736 - 0.9969602823 0.9895167947 0.9945799708 - 0.9970651865 0.9898357987 0.9947553873 - 0.9971700907 0.9901549220 0.9949306846 - 0.9972751141 0.9904739261 0.9951061010 - 0.9973800182 0.9907928705 0.9952813983 - 0.9974849224 0.9911119938 0.9954568148 - 0.9975774884 0.9914191961 0.9956181049 - 0.9976686239 0.9917250276 0.9957777858 - 0.9977596998 0.9920307994 0.9959375858 - 0.9978507757 0.9923365712 0.9960973263 - 0.9979419708 0.9926424026 0.9962570071 - 0.9980331063 0.9929481745 0.9964166880 - 0.9981241822 0.9932541251 0.9965764880 - 0.9982153177 0.9935598969 0.9967362285 - 0.9983065128 0.9938657284 0.9968959093 - 0.9983975887 0.9941715002 0.9970557094 - 0.9984887242 0.9944772720 0.9972153902 - 0.9985798001 0.9947832227 0.9973750710 - 0.9986709952 0.9950889945 0.9975348711 - 0.9987620711 0.9953948259 0.9976946115 - 0.9988523722 0.9956995249 0.9978532791 - 0.9989289045 0.9959862232 0.9979965091 - 0.9990053773 0.9962729216 0.9981396198 - 0.9990819097 0.9965596199 0.9982826710 - 0.9991583824 0.9968463182 0.9984257817 - 0.9992349148 0.9971330166 0.9985688925 - 0.9993113875 0.9974197149 0.9987120032 - 0.9993879199 0.9977064133 0.9988551140 - 0.9994643927 0.9979931116 0.9989982247 - 0.9995409250 0.9982798100 0.9991412759 - 0.9996175170 0.9985665083 0.9992843866 - 0.9996939898 0.9988532066 0.9994276166 - 0.9997705221 0.9991399050 0.9995707273 - 0.9998469949 0.9994266033 0.9997137785 - 0.9999235272 0.9997133017 0.9998568892 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0603632890 0.0454680510 0.0325050801 + 0.0916145369 0.0672334507 0.0560519211 + 0.1141282991 0.0832497999 0.0737638026 + 0.1319569051 0.0963873789 0.0874102712 + 0.1474671066 0.1078206971 0.0974757820 + 0.1610251069 0.1180547997 0.1053474993 + 0.1732669026 0.1273127049 0.1128624976 + 0.1844484061 0.1358955950 0.1203550026 + 0.1948004961 0.1437651068 0.1278492957 + 0.2044326067 0.1510639042 0.1352435946 + 0.2135390937 0.1580073982 0.1424624026 + 0.2222227007 0.1645933986 0.1493878067 + 0.2305278927 0.1708792001 0.1560759991 + 0.2384292036 0.1768720001 0.1626075953 + 0.2459527999 0.1825958937 0.1688462943 + 0.2531175911 0.1880602986 0.1748789996 + 0.2599833906 0.1933407038 0.1807505935 + 0.2665782869 0.1983918995 0.1864421070 + 0.2729392946 0.2032617927 0.1920263022 + 0.2790789902 0.2080129981 0.1974907964 + 0.2850643992 0.2126218975 0.2027806044 + 0.2908408046 0.2170598060 0.2078876942 + 0.2965438068 0.2213744968 0.2128470987 + 0.3020817935 0.2255962938 0.2176713943 + 0.3074505031 0.2297365963 0.2223367989 + 0.3126476109 0.2337452024 0.2268441021 + 0.3176782131 0.2376385927 0.2312131971 + 0.3225966990 0.2414472997 0.2354972064 + 0.3274005949 0.2451924980 0.2396982014 + 0.3320432901 0.2488888949 0.2437300980 + 0.3365654051 0.2524788976 0.2476260960 + 0.3410258889 0.2559857070 0.2514533103 + 0.3453224003 0.2594096065 0.2552187145 + 0.3495548964 0.2627862990 0.2589012980 + 0.3537135124 0.2661119998 0.2625198960 + 0.3577654064 0.2694039047 0.2660697997 + 0.3617587984 0.2726261914 0.2695469856 + 0.3657540083 0.2757805884 0.2729555070 + 0.3696199059 0.2788713872 0.2763257921 + 0.3734039068 0.2819064856 0.2796599865 + 0.3770940006 0.2849085927 0.2829268873 + 0.3806684911 0.2878705859 0.2861185074 + 0.3841705024 0.2907994092 0.2892411947 + 0.3875944912 0.2937094867 0.2923243046 + 0.3909009099 0.2965498865 0.2953714132 + 0.3941605091 0.2993265986 0.2983838916 + 0.3973405063 0.3020490110 0.3013431132 + 0.4004949033 0.3047288954 0.3042708039 + 0.4036108851 0.3073731959 0.3071705103 + 0.4066615999 0.3100095093 0.3100284040 + 0.4096829891 0.3126212955 0.3128500879 + 0.4127061069 0.3152045012 0.3156496882 + 0.4156985879 0.3177570999 0.3184089959 + 0.4186556935 0.3202543855 0.3211377859 + 0.4215945005 0.3227119148 0.3238492906 + 0.4244618118 0.3251400888 0.3265129030 + 0.4272699058 0.3275529146 0.3291609883 + 0.4300499856 0.3299373984 0.3317633867 + 0.4328002036 0.3323051929 0.3343206048 + 0.4354938865 0.3346739113 0.3368437886 + 0.4381406009 0.3370254934 0.3393223882 + 0.4407618046 0.3393487930 0.3417755067 + 0.4433783889 0.3416441083 0.3441849053 + 0.4459331930 0.3439005911 0.3465532959 + 0.4484468997 0.3461334109 0.3488821089 + 0.4509404898 0.3483205140 0.3511759937 + 0.4534223080 0.3504905999 0.3534452021 + 0.4558776915 0.3526290059 0.3557046950 + 0.4582909048 0.3547618091 0.3579384089 + 0.4606809914 0.3568730056 0.3601483107 + 0.4630529881 0.3589904904 0.3623368144 + 0.4653714001 0.3610835075 0.3645074964 + 0.4676778913 0.3631530106 0.3666630983 + 0.4699575007 0.3652003109 0.3688057065 + 0.4722292125 0.3671888113 0.3709379137 + 0.4745065868 0.3691641092 0.3730610013 + 0.4767518938 0.3711192012 0.3751786053 + 0.4789671004 0.3730652034 0.3772909939 + 0.4811646044 0.3749937117 0.3793990016 + 0.4833407998 0.3769024014 0.3814974129 + 0.4854697883 0.3788143098 0.3835780919 + 0.4875837862 0.3807007074 0.3856568933 + 0.4896746874 0.3826003969 0.3877165020 + 0.4917361140 0.3844895065 0.3897739947 + 0.4937821031 0.3863629997 0.3918241858 + 0.4958266914 0.3882220984 0.3938601017 + 0.4978322089 0.3900676966 0.3958753943 + 0.4998154044 0.3919059038 0.3978557885 + 0.5017874837 0.3937377036 0.3998196125 + 0.5037454963 0.3955607116 0.4017657936 + 0.5056617856 0.3973649144 0.4036836028 + 0.5075917840 0.3991670012 0.4055893123 + 0.5095139742 0.4009510875 0.4074887931 + 0.5114189982 0.4027169943 0.4093823135 + 0.5132684112 0.4044859111 0.4112665951 + 0.5151203871 0.4062362909 0.4131282866 + 0.5169649720 0.4079819024 0.4149852991 + 0.5187842846 0.4097360075 0.4168485999 + 0.5205675960 0.4114727974 0.4186882079 + 0.5223402977 0.4132055044 0.4205282927 + 0.5241093040 0.4149374068 0.4223743081 + 0.5258463025 0.4166536033 0.4241972864 + 0.5275856256 0.4183549881 0.4260225892 + 0.5293406248 0.4200339913 0.4278545082 + 0.5310854912 0.4217008948 0.4296846986 + 0.5328161120 0.4233531058 0.4315181971 + 0.5345103741 0.4249719083 0.4333597124 + 0.5362126827 0.4265843928 0.4351972938 + 0.5379176140 0.4281908870 0.4370343983 + 0.5396010876 0.4297637045 0.4388785958 + 0.5412700772 0.4313359857 0.4407181144 + 0.5429202914 0.4329077005 0.4425528049 + 0.5445725918 0.4344569147 0.4443916082 + 0.5462222099 0.4360015094 0.4462321103 + 0.5478432775 0.4375520945 0.4480772018 + 0.5494470000 0.4390931129 0.4499236941 + 0.5510246158 0.4406209886 0.4517717957 + 0.5525965095 0.4421330988 0.4536193013 + 0.5541654825 0.4436380863 0.4554666877 + 0.5557032228 0.4451265037 0.4573141932 + 0.5572413206 0.4466089010 0.4591605067 + 0.5587986708 0.4480651915 0.4610021114 + 0.5603525043 0.4495176077 0.4628401995 + 0.5618990064 0.4509631097 0.4646731913 + 0.5634241104 0.4523977935 0.4664989114 + 0.5649273992 0.4538224936 0.4683187902 + 0.5664381981 0.4552442133 0.4701269865 + 0.5679532290 0.4566642940 0.4719302058 + 0.5694528818 0.4580616951 0.4737173021 + 0.5709474087 0.4594523013 0.4755006135 + 0.5723990798 0.4608454108 0.4772619009 + 0.5738419890 0.4622389972 0.4790205061 + 0.5752863288 0.4636129141 0.4807558954 + 0.5767307281 0.4649848938 0.4824900925 + 0.5781518221 0.4663662910 0.4842281938 + 0.5795705914 0.4677484035 0.4859664142 + 0.5809577703 0.4691171050 0.4876765907 + 0.5823417902 0.4704849124 0.4893858135 + 0.5837211013 0.4718532860 0.4910922945 + 0.5850995779 0.4732218087 0.4927985966 + 0.5864555240 0.4745810032 0.4944742918 + 0.5878059864 0.4759384096 0.4961453974 + 0.5891577005 0.4772793949 0.4978106916 + 0.5905098915 0.4786145985 0.4994742870 + 0.5918568969 0.4799452126 0.5011221766 + 0.5932002068 0.4812729955 0.5027614236 + 0.5945292115 0.4825868905 0.5043945909 + 0.5958389044 0.4838840961 0.5060214996 + 0.5971521139 0.4851810038 0.5076392889 + 0.5984765887 0.4864774048 0.5092359781 + 0.5998010039 0.4877730906 0.5108314157 + 0.6011152864 0.4890460968 0.5124111176 + 0.6024296284 0.4903191030 0.5139907002 + 0.6037279963 0.4915966988 0.5155413747 + 0.6050211787 0.4928756058 0.5170856118 + 0.6063179970 0.4941464067 0.5186198950 + 0.6076188087 0.4954087138 0.5201451778 + 0.6089168191 0.4966705143 0.5216658711 + 0.6101980805 0.4979301095 0.5231689811 + 0.6114792824 0.4991895854 0.5246720910 + 0.6127241850 0.5004386902 0.5261573792 + 0.6139590144 0.5016852021 0.5276395082 + 0.6151936054 0.5029277205 0.5291115046 + 0.6164277196 0.5041645765 0.5305709839 + 0.6176617742 0.5054013729 0.5320304036 + 0.6188758016 0.5066292882 0.5334817767 + 0.6200888753 0.5078570247 0.5349332094 + 0.6212956905 0.5090678930 0.5363718271 + 0.6224961281 0.5102629066 0.5378000140 + 0.6236965060 0.5114579201 0.5392268896 + 0.6248922944 0.5126475096 0.5406283140 + 0.6260880828 0.5138370991 0.5420296192 + 0.6272730827 0.5150145888 0.5434191227 + 0.6284447908 0.5161781907 0.5447971225 + 0.6296163797 0.5173417926 0.5461751223 + 0.6307746768 0.5185059905 0.5475438237 + 0.6319308877 0.5196703076 0.5489118099 + 0.6330848932 0.5208293200 0.5502725840 + 0.6342328191 0.5219740868 0.5516172051 + 0.6353806257 0.5231189132 0.5529618263 + 0.6365122199 0.5242677927 0.5542978048 + 0.6376327872 0.5254191756 0.5556291938 + 0.6387532949 0.5265706778 0.5569607019 + 0.6398714185 0.5277099013 0.5582699180 + 0.6409891248 0.5288481116 0.5595781803 + 0.6421049833 0.5299884081 0.5608838797 + 0.6432113051 0.5311387777 0.5621806979 + 0.6443176270 0.5322890878 0.5634775162 + 0.6454114914 0.5334355235 0.5647640824 + 0.6464858055 0.5345761180 0.5660377741 + 0.6475601792 0.5357167125 0.5673115849 + 0.6486206055 0.5368447900 0.5685880780 + 0.6496704817 0.5379639268 0.5698662996 + 0.6507204175 0.5390831232 0.5711445212 + 0.6517605186 0.5401955843 0.5724067092 + 0.6527966261 0.5413054824 0.5736641288 + 0.6538326740 0.5424153805 0.5749214292 + 0.6548563242 0.5435019732 0.5761708021 + 0.6558768749 0.5445836782 0.5774188042 + 0.6568974853 0.5456653237 0.5786669254 + 0.6579241753 0.5467439890 0.5798960924 + 0.6589519978 0.5478222966 0.5811234713 + 0.6599798203 0.5489006042 0.5823509097 + 0.6609984040 0.5499593019 0.5835713744 + 0.6620156169 0.5510154963 0.5847914219 + 0.6630328894 0.5520715714 0.5860112906 + 0.6640465260 0.5531296730 0.5872145891 + 0.6650593877 0.5541881919 0.5884156823 + 0.6660723090 0.5552465916 0.5896167755 + 0.6670761704 0.5562924743 0.5908033848 + 0.6680768728 0.5573346019 0.5919865966 + 0.6690776944 0.5583767295 0.5931698084 + 0.6700693965 0.5594154000 0.5943410993 + 0.6710556746 0.5604522824 0.5955067277 + 0.6720418930 0.5614892244 0.5966722965 + 0.6730256081 0.5625199080 0.5978301167 + 0.6740061045 0.5635436177 0.5989803076 + 0.6749866009 0.5645673275 0.6001303792 + 0.6759673953 0.5655882955 0.6012763977 + 0.6769493818 0.5666012168 0.6024119258 + 0.6779313087 0.5676140785 0.6035472751 + 0.6789131761 0.5686269999 0.6046826243 + 0.6798806787 0.5696319938 0.6058049798 + 0.6808475256 0.5706369281 0.6069275141 + 0.6818143129 0.5716418028 0.6080499887 + 0.6827753782 0.5726358294 0.6091644168 + 0.6837335229 0.5736240149 0.6102753282 + 0.6846914887 0.5746123195 0.6113861203 + 0.6856489778 0.5755988955 0.6124936938 + 0.6866049767 0.5765814185 0.6135942936 + 0.6875609756 0.5775640011 0.6146950126 + 0.6885170937 0.5785465240 0.6157956719 + 0.6894577146 0.5795089006 0.6168872714 + 0.6903957129 0.5804682970 0.6179779172 + 0.6913337111 0.5814276934 0.6190685034 + 0.6922674179 0.5823866725 0.6201496124 + 0.6931942105 0.5833454132 0.6212183833 + 0.6941210032 0.5843039751 0.6222872138 + 0.6950476766 0.5852625966 0.6233559847 + 0.6959589720 0.5862057209 0.6244189143 + 0.6968678236 0.5871466994 0.6254813075 + 0.6977766156 0.5880876780 0.6265437007 + 0.6986793280 0.5890266895 0.6276041269 + 0.6995667219 0.5899611712 0.6286605000 + 0.7004541159 0.5908957720 0.6297169924 + 0.7013415098 0.5918303132 0.6307734251 + 0.7022203803 0.5927559733 0.6318181753 + 0.7030950189 0.5936775208 0.6328582764 + 0.7039695978 0.5945990086 0.6338983774 + 0.7048441172 0.5955204964 0.6349384189 + 0.7057145834 0.5964314938 0.6359745860 + 0.7065848708 0.5973424911 0.6370108128 + 0.7074552178 0.5982534289 0.6380469203 + 0.7083240151 0.5991623998 0.6390789747 + 0.7091879845 0.6000658870 0.6401014924 + 0.7100520134 0.6009693146 0.6411240101 + 0.7109159827 0.6018726826 0.6421465278 + 0.7117736936 0.6027743220 0.6431624889 + 0.7126240134 0.6036738157 0.6441721916 + 0.7134742141 0.6045733094 0.6451817751 + 0.7143244743 0.6054729223 0.6461914778 + 0.7151702046 0.6063696742 0.6471874118 + 0.7160130143 0.6072648764 0.6481761932 + 0.7168557048 0.6081600785 0.6491649151 + 0.7176983953 0.6090552807 0.6501536965 + 0.7185257077 0.6099364161 0.6511443853 + 0.7193462253 0.6108118892 0.6521357298 + 0.7201668024 0.6116873026 0.6531270146 + 0.7209873199 0.6125627756 0.6541182995 + 0.7217960954 0.6134387851 0.6550964117 + 0.7226005793 0.6143149137 0.6560708880 + 0.7234051228 0.6151911020 0.6570454240 + 0.7242096066 0.6160672903 0.6580199003 + 0.7250012755 0.6169329286 0.6589906216 + 0.7257876992 0.6177945137 0.6599599719 + 0.7265741825 0.6186562181 0.6609295011 + 0.7273607254 0.6195178032 0.6618989706 + 0.7281492949 0.6203829050 0.6628596187 + 0.7289391756 0.6212499738 0.6638159752 + 0.7297291160 0.6221169829 0.6647723913 + 0.7305189967 0.6229839921 0.6657288074 + 0.7313020229 0.6238461137 0.6666783094 + 0.7320774198 0.6247032285 0.6676219106 + 0.7328528762 0.6255602837 0.6685655117 + 0.7336283922 0.6264173985 0.6695091128 + 0.7344021201 0.6272711754 0.6704490185 + 0.7351711988 0.6281173825 0.6713812947 + 0.7359403968 0.6289635897 0.6723136902 + 0.7367094755 0.6298096776 0.6732460856 + 0.7374784946 0.6306557059 0.6741766930 + 0.7382401228 0.6314933896 0.6750792861 + 0.7390015721 0.6323310733 0.6759818792 + 0.7397632003 0.6331688166 0.6768844724 + 0.7405247092 0.6340065002 0.6777871251 + 0.7412760854 0.6348341107 0.6786838174 + 0.7420225143 0.6356573105 0.6795783043 + 0.7427688837 0.6364803910 0.6804727912 + 0.7435153127 0.6373035908 0.6813672781 + 0.7442601919 0.6381258965 0.6822535992 + 0.7450010777 0.6389461756 0.6831231713 + 0.7457420230 0.6397663951 0.6839928031 + 0.7464829087 0.6405866742 0.6848623753 + 0.7472237945 0.6414070129 0.6857320070 + 0.7479677796 0.6422203183 0.6865984797 + 0.7487124205 0.6430320740 0.6874645948 + 0.7494571209 0.6438438892 0.6883307099 + 0.7502018213 0.6446557045 0.6891968250 + 0.7509424090 0.6454676986 0.6900563836 + 0.7516725063 0.6462804079 0.6909027100 + 0.7524024844 0.6470929980 0.6917490959 + 0.7531325817 0.6479057074 0.6925954223 + 0.7538626790 0.6487182975 0.6934418082 + 0.7545816898 0.6495214105 0.6942890882 + 0.7552953959 0.6503202915 0.6951367855 + 0.7560091019 0.6511192918 0.6959844828 + 0.7567229271 0.6519181728 0.6968321800 + 0.7574365735 0.6527169943 0.6976789236 + 0.7581534982 0.6535111070 0.6985118985 + 0.7588703036 0.6543052197 0.6993448734 + 0.7595872283 0.6550993919 0.7001779079 + 0.7603039742 0.6558935046 0.7010108829 + 0.7610172033 0.6566845775 0.7018424273 + 0.7617210150 0.6574687958 0.7026708722 + 0.7624248266 0.6582528949 0.7034993768 + 0.7631285191 0.6590371132 0.7043278813 + 0.7638322711 0.6598212123 0.7051563859 + 0.7645226121 0.6606013775 0.7059795260 + 0.7651978135 0.6613773108 0.7067975998 + 0.7658728957 0.6621533036 0.7076157928 + 0.7665480971 0.6629291773 0.7084339857 + 0.7672232985 0.6637052298 0.7092521191 + 0.7678937912 0.6644771099 0.7100641727 + 0.7685611248 0.6652464867 0.7108727098 + 0.7692285180 0.6660159230 0.7116813064 + 0.7698957920 0.6667852998 0.7124899030 + 0.7705631256 0.6675546765 0.7132984996 + 0.7712234855 0.6683161855 0.7141026258 + 0.7718799114 0.6690734029 0.7149047256 + 0.7725363970 0.6698306799 0.7157067060 + 0.7731928825 0.6705878973 0.7165088058 + 0.7738493085 0.6713451743 0.7173109055 + 0.7744975090 0.6721003056 0.7181041241 + 0.7751405239 0.6728541255 0.7188926935 + 0.7757834792 0.6736080050 0.7196813822 + 0.7764264941 0.6743618250 0.7204700112 + 0.7770696282 0.6751157045 0.7212585807 + 0.7777153850 0.6758605838 0.7220466137 + 0.7783638835 0.6765974164 0.7228341103 + 0.7790123820 0.6773343086 0.7236214876 + 0.7796608806 0.6780712008 0.7244089842 + 0.7803093791 0.6788079739 0.7251964808 + 0.7809547782 0.6795421243 0.7259809971 + 0.7815937996 0.6802709103 0.7267609239 + 0.7822328806 0.6809996963 0.7275406718 + 0.7828719020 0.6817284822 0.7283204794 + 0.7835109830 0.6824573278 0.7291004062 + 0.7841491103 0.6831848025 0.7298780084 + 0.7847760916 0.6839002967 0.7306407094 + 0.7854030132 0.6846157908 0.7314034104 + 0.7860299945 0.6853312850 0.7321661711 + 0.7866569757 0.6860468984 0.7329288721 + 0.7872840166 0.6867623925 0.7336915731 + 0.7879158854 0.6874758005 0.7344440222 + 0.7885494828 0.6881886125 0.7351937890 + 0.7891830206 0.6889013052 0.7359436154 + 0.7898166180 0.6896141171 0.7366933227 + 0.7904502153 0.6903269291 0.7374430895 + 0.7910810709 0.6910367012 0.7381889224 + 0.7917066216 0.6917409897 0.7389283180 + 0.7923321128 0.6924453974 0.7396677136 + 0.7929577231 0.6931496859 0.7404071093 + 0.7935832143 0.6938540936 0.7411465049 + 0.7942087054 0.6945583820 0.7418857813 + 0.7948155999 0.6952574253 0.7426193953 + 0.7954189181 0.6959553957 0.7433521152 + 0.7960221171 0.6966534257 0.7440848947 + 0.7966253757 0.6973515153 0.7448176146 + 0.7972286940 0.6980494857 0.7455503941 + 0.7978311181 0.6987463832 0.7462819219 + 0.7984310985 0.6994404197 0.7470108271 + 0.7990310788 0.7001342773 0.7477396131 + 0.7996311188 0.7008281946 0.7484685183 + 0.8002310991 0.7015221715 0.7491973042 + 0.8008310795 0.7022160888 0.7499262094 + 0.8014271259 0.7029095888 0.7506538033 + 0.8020204902 0.7036026716 0.7513806820 + 0.8026139140 0.7042958140 0.7521076202 + 0.8032072783 0.7049888968 0.7528346181 + 0.8038007021 0.7056819797 0.7535614967 + 0.8043941855 0.7063751221 0.7542883754 + 0.8049713969 0.7070598006 0.7550076842 + 0.8055459857 0.7077432871 0.7557262182 + 0.8061205745 0.7084268928 0.7564446926 + 0.8066952229 0.7091103792 0.7571632266 + 0.8072698712 0.7097939253 0.7578817010 + 0.8078448176 0.7104768157 0.7585989237 + 0.8084272742 0.7111461759 0.7593011260 + 0.8090099096 0.7118155956 0.7600033879 + 0.8095924258 0.7124850154 0.7607055902 + 0.8101750016 0.7131544948 0.7614077926 + 0.8107575178 0.7138239145 0.7621099949 + 0.8113396168 0.7144926786 0.7628099918 + 0.8119179010 0.7151576281 0.7634986043 + 0.8124963045 0.7158225179 0.7641872764 + 0.8130745888 0.7164874077 0.7648758888 + 0.8136529922 0.7171524167 0.7655645013 + 0.8142312765 0.7178173065 0.7662531137 + 0.8148078918 0.7184810042 0.7669401169 + 0.8153703213 0.7191364169 0.7676184177 + 0.8159328103 0.7197918892 0.7682967782 + 0.8164951801 0.7204474211 0.7689750791 + 0.8170577288 0.7211027741 0.7696533799 + 0.8176200986 0.7217583060 0.7703316808 + 0.8181825876 0.7224134803 0.7710090876 + 0.8187462091 0.7230613232 0.7716705799 + 0.8193098903 0.7237089872 0.7723321915 + 0.8198735714 0.7243567705 0.7729938030 + 0.8204373121 0.7250046134 0.7736552954 + 0.8210009933 0.7256522775 0.7743169069 + 0.8215646148 0.7263001204 0.7749783993 + 0.8221263289 0.7269431949 0.7756360173 + 0.8226876259 0.7275854945 0.7762929201 + 0.8232489228 0.7282276750 0.7769498825 + 0.8238101006 0.7288699746 0.7776067853 + 0.8243713975 0.7295122743 0.7782638073 + 0.8249326944 0.7301545143 0.7789208293 + 0.8254861832 0.7307953238 0.7795671821 + 0.8260335922 0.7314348221 0.7802065015 + 0.8265808821 0.7320743799 0.7808458209 + 0.8271281719 0.7327139974 0.7814850807 + 0.8276755810 0.7333536148 0.7821244001 + 0.8282228708 0.7339931130 0.7827637196 + 0.8287672997 0.7346323729 0.7834032774 + 0.8292987943 0.7352702022 0.7840442061 + 0.8298302293 0.7359080911 0.7846850157 + 0.8303616047 0.7365459800 0.7853258848 + 0.8308929801 0.7371838093 0.7859666944 + 0.8314244747 0.7378216982 0.7866076231 + 0.8319559097 0.7384595275 0.7872483730 + 0.8324869275 0.7390930057 0.7878805995 + 0.8330175877 0.7397248745 0.7885100245 + 0.8335484266 0.7403566837 0.7891393900 + 0.8340792060 0.7409886122 0.7897688150 + 0.8346099257 0.7416204810 0.7903981805 + 0.8351407051 0.7422522902 0.7910276055 + 0.8356696963 0.7428832054 0.7916554809 + 0.8361886144 0.7435089946 0.7922765017 + 0.8367075920 0.7441349030 0.7928975821 + 0.8372265100 0.7447606921 0.7935186028 + 0.8377454281 0.7453864813 0.7941396832 + 0.8382642865 0.7460123897 0.7947607040 + 0.8387833238 0.7466381788 0.7953817844 + 0.8392910957 0.7472589016 0.7959994078 + 0.8397874236 0.7478746176 0.7966139913 + 0.8402836919 0.7484902740 0.7972285748 + 0.8407800198 0.7491058707 0.7978432178 + 0.8412762880 0.7497215867 0.7984578013 + 0.8417726159 0.7503373027 0.7990723848 + 0.8422688842 0.7509530187 0.7996870279 + 0.8427659869 0.7515618801 0.8002871871 + 0.8432633877 0.7521684170 0.8008829951 + 0.8437607884 0.7527748942 0.8014789224 + 0.8442581892 0.7533814907 0.8020746708 + 0.8447555900 0.7539880276 0.8026704788 + 0.8452529907 0.7545946240 0.8032664061 + 0.8457503915 0.7552011013 0.8038622141 + 0.8462408781 0.7558019757 0.8044577241 + 0.8467301726 0.7564020753 0.8050531149 + 0.8472195864 0.7570022941 0.8056485057 + 0.8477090001 0.7576023936 0.8062438965 + 0.8481984138 0.7582024932 0.8068394065 + 0.8486878276 0.7588025928 0.8074347973 + 0.8491771817 0.7594026923 0.8080301881 + 0.8496577144 0.7599987984 0.8086149096 + 0.8501371741 0.7605946064 0.8091987967 + 0.8506168127 0.7611902952 0.8097828031 + 0.8510962725 0.7617859840 0.8103666902 + 0.8515759110 0.7623817921 0.8109506965 + 0.8520553708 0.7629774809 0.8115345836 + 0.8525350094 0.7635732293 0.8121185899 + 0.8530108929 0.7641665936 0.8127080798 + 0.8534861207 0.7647594213 0.8132985234 + 0.8539612889 0.7653523088 0.8138889074 + 0.8544363976 0.7659451962 0.8144792914 + 0.8549116254 0.7665380836 0.8150696754 + 0.8553867936 0.7671309114 0.8156599998 + 0.8558620214 0.7677237988 0.8162503839 + 0.8563336730 0.7683117986 0.8168362975 + 0.8568035960 0.7688974142 0.8174201250 + 0.8572735190 0.7694829702 0.8180038929 + 0.8577435017 0.7700685859 0.8185877204 + 0.8582134247 0.7706542015 0.8191714883 + 0.8586832881 0.7712398171 0.8197553754 + 0.8591532111 0.7718253732 0.8203392029 + 0.8596208096 0.7724086046 0.8209149837 + 0.8600844741 0.7729880810 0.8214796185 + 0.8605483174 0.7735676765 0.8220440745 + 0.8610121012 0.7741472125 0.8226085901 + 0.8614758253 0.7747266889 0.8231731057 + 0.8619396091 0.7753062248 0.8237376213 + 0.8624032736 0.7758857012 0.8243020773 + 0.8628671169 0.7764651775 0.8248665929 + 0.8633220196 0.7770413160 0.8254293203 + 0.8637769222 0.7776173949 0.8259919882 + 0.8642318249 0.7781934738 0.8265547752 + 0.8646866083 0.7787696123 0.8271175027 + 0.8651415110 0.7793456912 0.8276802897 + 0.8655962944 0.7799218297 0.8282430172 + 0.8660511971 0.7804979086 0.8288056850 + 0.8665007949 0.7810693979 0.8293619752 + 0.8669447899 0.7816361785 0.8299123049 + 0.8673887849 0.7822030187 0.8304625750 + 0.8678327799 0.7827697992 0.8310130239 + 0.8682768941 0.7833365202 0.8315632939 + 0.8687208891 0.7839033008 0.8321136236 + 0.8691648841 0.7844700813 0.8326638937 + 0.8696088791 0.7850369215 0.8332142234 + 0.8700435758 0.7855976820 0.8337548971 + 0.8704770207 0.7861577868 0.8342946172 + 0.8709102869 0.7867177725 0.8348342776 + 0.8713436127 0.7872778773 0.8353739977 + 0.8717769980 0.7878379226 0.8359137177 + 0.8722103238 0.7883980274 0.8364533782 + 0.8726437092 0.7889580131 0.8369930983 + 0.8730763793 0.7895172834 0.8375316858 + 0.8735055923 0.7900726795 0.8380653262 + 0.8739346862 0.7906280756 0.8385987878 + 0.8743638992 0.7911834717 0.8391324282 + 0.8747929931 0.7917389274 0.8396660089 + 0.8752222061 0.7922942042 0.8401995897 + 0.8756513000 0.7928496003 0.8407331705 + 0.8760805130 0.7934049964 0.8412668109 + 0.8765051961 0.7939593196 0.8417996168 + 0.8769198060 0.7945113182 0.8423308134 + 0.8773342967 0.7950633168 0.8428620100 + 0.8777489066 0.7956153154 0.8433932066 + 0.8781633973 0.7961671948 0.8439245224 + 0.8785778880 0.7967191935 0.8444557190 + 0.8789924979 0.7972711921 0.8449869156 + 0.8794069886 0.7978231907 0.8455181122 + 0.8798193932 0.7983725071 0.8460463881 + 0.8802276850 0.7989166975 0.8465698957 + 0.8806359768 0.7994610071 0.8470934033 + 0.8810443282 0.8000053167 0.8476169109 + 0.8814526200 0.8005495071 0.8481404185 + 0.8818609118 0.8010938168 0.8486639261 + 0.8822692037 0.8016381264 0.8491873741 + 0.8826774955 0.8021823764 0.8497108817 + 0.8830862045 0.8027231097 0.8502314091 + 0.8834958076 0.8032547235 0.8507447243 + 0.8839054704 0.8037862182 0.8512579799 + 0.8843150735 0.8043177724 0.8517714143 + 0.8847246766 0.8048493266 0.8522847295 + 0.8851343989 0.8053808808 0.8527981043 + 0.8855440021 0.8059123755 0.8533114195 + 0.8859536052 0.8064439893 0.8538246751 + 0.8863626719 0.8069751859 0.8543373942 + 0.8867633939 0.8075023293 0.8548439145 + 0.8871641159 0.8080292940 0.8553503752 + 0.8875647783 0.8085563779 0.8558567762 + 0.8879655004 0.8090834022 0.8563632965 + 0.8883662820 0.8096104860 0.8568698168 + 0.8887670040 0.8101376295 0.8573762774 + 0.8891677260 0.8106645942 0.8578827977 + 0.8895683885 0.8111916780 0.8583891988 + 0.8899617195 0.8117120862 0.8588874936 + 0.8903526068 0.8122305274 0.8593834043 + 0.8907433748 0.8127487898 0.8598793149 + 0.8911342025 0.8132671118 0.8603752255 + 0.8915249705 0.8137853742 0.8608711958 + 0.8919157982 0.8143036962 0.8613671064 + 0.8923066854 0.8148220778 0.8618630171 + 0.8926975131 0.8153403997 0.8623589277 + 0.8930876851 0.8158568144 0.8628520966 + 0.8934766054 0.8163688183 0.8633397818 + 0.8938655853 0.8168808818 0.8638275266 + 0.8942545056 0.8173928857 0.8643152118 + 0.8946434259 0.8179048896 0.8648028970 + 0.8950322866 0.8184170127 0.8652905822 + 0.8954212070 0.8189290166 0.8657783270 + 0.8958101869 0.8194410801 0.8662660122 + 0.8961991072 0.8199530840 0.8667536974 + 0.8965854049 0.8204619288 0.8672325015 + 0.8969708085 0.8209697008 0.8677083850 + 0.8973562121 0.8214774132 0.8681843281 + 0.8977414966 0.8219851255 0.8686602116 + 0.8981269002 0.8224927783 0.8691360950 + 0.8985123038 0.8230004907 0.8696119785 + 0.8988977075 0.8235082030 0.8700879216 + 0.8992829919 0.8240159154 0.8705638051 + 0.8996679783 0.8245236278 0.8710392118 + 0.9000402093 0.8250293732 0.8715068102 + 0.9004123807 0.8255351782 0.8719742894 + 0.9007844925 0.8260409832 0.8724418879 + 0.9011567235 0.8265467882 0.8729094267 + 0.9015287757 0.8270524740 0.8733770251 + 0.9019010067 0.8275582790 0.8738445044 + 0.9022731781 0.8280640841 0.8743121028 + 0.9026452899 0.8285698891 0.8747795820 + 0.9030135274 0.8290756941 0.8752462268 + 0.9033654928 0.8295813203 0.8757094145 + 0.9037175179 0.8300868869 0.8761726022 + 0.9040694833 0.8305925131 0.8766359091 + 0.9044215083 0.8310980797 0.8770990968 + 0.9047734737 0.8316035867 0.8775622845 + 0.9051254988 0.8321092129 0.8780254722 + 0.9054775238 0.8326147795 0.8784887791 + 0.9058294892 0.8331204057 0.8789520264 + 0.9061816931 0.8336247802 0.8794112802 + 0.9065344930 0.8341253996 0.8798596859 + 0.9068872929 0.8346260190 0.8803079724 + 0.9072400928 0.8351265788 0.8807563782 + 0.9075930119 0.8356270790 0.8812047243 + 0.9079458117 0.8361276984 0.8816530704 + 0.9082986116 0.8366283178 0.8821014762 + 0.9086514115 0.8371288776 0.8825498223 + 0.9090042114 0.8376294971 0.8829982281 + 0.9093561769 0.8381295800 0.8834459186 + 0.9097027183 0.8386269808 0.8838903904 + 0.9100490808 0.8391245008 0.8843349218 + 0.9103956223 0.8396219015 0.8847793937 + 0.9107421041 0.8401193023 0.8852239251 + 0.9110885859 0.8406167030 0.8856683969 + 0.9114350080 0.8411141038 0.8861129880 + 0.9117814898 0.8416115046 0.8865575194 + 0.9121279716 0.8421089053 0.8870019913 + 0.9124745131 0.8426063061 0.8874465227 + 0.9128088951 0.8430963159 0.8878840804 + 0.9131420851 0.8435854912 0.8883212209 + 0.9134753942 0.8440747857 0.8887583017 + 0.9138085842 0.8445640802 0.8891953826 + 0.9141417742 0.8450533748 0.8896325231 + 0.9144750237 0.8455426097 0.8900696039 + 0.9148082137 0.8460319042 0.8905066848 + 0.9151415229 0.8465211987 0.8909438252 + 0.9154747128 0.8470103741 0.8913809061 + 0.9158115983 0.8474963903 0.8918085098 + 0.9161515832 0.8479797244 0.8922289014 + 0.9164916277 0.8484628797 0.8926492929 + 0.9168316126 0.8489462137 0.8930696249 + 0.9171715975 0.8494294286 0.8934900165 + 0.9175117016 0.8499127030 0.8939104080 + 0.9178516865 0.8503959179 0.8943307996 + 0.9181916714 0.8508791924 0.8947511911 + 0.9185317159 0.8513624072 0.8951715827 + 0.9188717008 0.8518455029 0.8955916166 + 0.9192115068 0.8523247838 0.8960055113 + 0.9195513129 0.8528040051 0.8964195251 + 0.9198911190 0.8532832265 0.8968334198 + 0.9202309847 0.8537623882 0.8972473741 + 0.9205707908 0.8542416096 0.8976613283 + 0.9209105968 0.8547207713 0.8980752230 + 0.9212504029 0.8552001119 0.8984891772 + 0.9215902090 0.8556792736 0.8989030719 + 0.9219300151 0.8561584949 0.8993170261 + 0.9222667813 0.8566346169 0.8997275829 + 0.9225986004 0.8571056724 0.9001333117 + 0.9229304194 0.8575767875 0.9005389214 + 0.9232621193 0.8580479026 0.9009444714 + 0.9235938787 0.8585190177 0.9013502002 + 0.9239256978 0.8589900732 0.9017558098 + 0.9242575169 0.8594611883 0.9021614194 + 0.9245892763 0.8599323034 0.9025670886 + 0.9249210954 0.8604034185 0.9029726982 + 0.9252529144 0.8608745933 0.9033783078 + 0.9255800247 0.8613454103 0.9037811160 + 0.9259036779 0.8618162274 0.9041818976 + 0.9262273908 0.8622869253 0.9045827985 + 0.9265511036 0.8627576232 0.9049836993 + 0.9268748164 0.8632283211 0.9053844810 + 0.9271985292 0.8636990190 0.9057853818 + 0.9275221825 0.8641697764 0.9061862826 + 0.9278460145 0.8646404743 0.9065871239 + 0.9281697273 0.8651111722 0.9069880247 + 0.9284933805 0.8655819297 0.9073889256 + 0.9288159013 0.8660464287 0.9077849984 + 0.9291377068 0.8665074706 0.9081788063 + 0.9294595122 0.8669685721 0.9085724950 + 0.9297813773 0.8674296737 0.9089663029 + 0.9301031828 0.8678907752 0.9093599916 + 0.9304249883 0.8683518767 0.9097537994 + 0.9307467937 0.8688129783 0.9101474881 + 0.9310687184 0.8692740798 0.9105412960 + 0.9313905239 0.8697351813 0.9109349847 + 0.9317123294 0.8701962829 0.9113287926 + 0.9320325851 0.8706560135 0.9117175937 + 0.9323515892 0.8711144924 0.9121026993 + 0.9326705933 0.8715730906 0.9124878049 + 0.9329895973 0.8720316887 0.9128729105 + 0.9333087206 0.8724902272 0.9132580161 + 0.9336277246 0.8729488254 0.9136431217 + 0.9339467287 0.8734074235 0.9140282273 + 0.9342656732 0.8738659024 0.9144132733 + 0.9345846772 0.8743245006 0.9147983789 + 0.9349036813 0.8747830987 0.9151834846 + 0.9352200031 0.8752396703 0.9155669212 + 0.9355298877 0.8756920099 0.9159467816 + 0.9358398914 0.8761444092 0.9163267016 + 0.9361498952 0.8765966892 0.9167066216 + 0.9364597797 0.8770490885 0.9170864820 + 0.9367697835 0.8775014877 0.9174664021 + 0.9370797873 0.8779538274 0.9178462029 + 0.9373897910 0.8784062266 0.9182261229 + 0.9376996756 0.8788586259 0.9186059833 + 0.9380096793 0.8793109059 0.9189859033 + 0.9383196831 0.8797633052 0.9193658233 + 0.9386215210 0.8802114129 0.9197325110 + 0.9389225245 0.8806591034 0.9200983047 + 0.9392235279 0.8811067939 0.9204640985 + 0.9395244718 0.8815544844 0.9208298922 + 0.9398255944 0.8820021749 0.9211956859 + 0.9401265979 0.8824499249 0.9215614796 + 0.9404276013 0.8828976154 0.9219272733 + 0.9407287240 0.8833453059 0.9222931266 + 0.9410297275 0.8837931156 0.9226589203 + 0.9413306713 0.8842408061 0.9230247140 + 0.9416282177 0.8846876025 0.9233872294 + 0.9419195056 0.8851327896 0.9237444997 + 0.9422109127 0.8855779767 0.9241018295 + 0.9425022006 0.8860232830 0.9244589806 + 0.9427934885 0.8864685297 0.9248163104 + 0.9430847764 0.8869137764 0.9251735210 + 0.9433761239 0.8873590231 0.9255307913 + 0.9436674714 0.8878043294 0.9258880019 + 0.9439588189 0.8882495165 0.9262452722 + 0.9442501068 0.8886947036 0.9266026020 + 0.9445413947 0.8891400099 0.9269598126 + 0.9448264241 0.8895812035 0.9273141026 + 0.9451081157 0.8900203109 0.9276670814 + 0.9453896880 0.8904594779 0.9280200005 + 0.9456713796 0.8908985853 0.9283729792 + 0.9459530115 0.8913378119 0.9287258983 + 0.9462347031 0.8917769194 0.9290788770 + 0.9465163946 0.8922160864 0.9294317961 + 0.9467980266 0.8926551938 0.9297847748 + 0.9470797181 0.8930943012 0.9301376939 + 0.9473612905 0.8935335279 0.9304906726 + 0.9476429820 0.8939725757 0.9308435917 + 0.9479179978 0.8944123983 0.9311866164 + 0.9481912255 0.8948522806 0.9315270782 + 0.9484645128 0.8952922225 0.9318677187 + 0.9487376809 0.8957322240 0.9322082996 + 0.9490109086 0.8961721063 0.9325488806 + 0.9492841959 0.8966119885 0.9328895211 + 0.9495574236 0.8970518708 0.9332301021 + 0.9498305917 0.8974918127 0.9335706830 + 0.9501038790 0.8979318142 0.9339113235 + 0.9503771067 0.8983716965 0.9342517853 + 0.9506502748 0.8988115788 0.9345924258 + 0.9509218931 0.8992447257 0.9349266291 + 0.9511929750 0.8996757269 0.9352591038 + 0.9514641166 0.9001067281 0.9355915189 + 0.9517351985 0.9005377293 0.9359238744 + 0.9520063996 0.9009686708 0.9362562895 + 0.9522774816 0.9013996720 0.9365887046 + 0.9525486231 0.9018306732 0.9369211197 + 0.9528197050 0.9022616744 0.9372535944 + 0.9530907869 0.9026926756 0.9375860095 + 0.9533619285 0.9031236768 0.9379184246 + 0.9536330104 0.9035546780 0.9382507801 + 0.9538990855 0.9039816856 0.9385809898 + 0.9541615248 0.9044054747 0.9389094710 + 0.9544237852 0.9048293829 0.9392380118 + 0.9546862245 0.9052531719 0.9395664930 + 0.9549484849 0.9056770802 0.9398950934 + 0.9552109241 0.9061009884 0.9402235746 + 0.9554733038 0.9065247774 0.9405521154 + 0.9557356238 0.9069486856 0.9408807158 + 0.9559980035 0.9073724747 0.9412091970 + 0.9562603235 0.9077963829 0.9415376782 + 0.9565227032 0.9082202911 0.9418662190 + 0.9567822218 0.9086430073 0.9421923161 + 0.9570304155 0.9090616107 0.9425098896 + 0.9572787285 0.9094802141 0.9428274035 + 0.9575269818 0.9098988175 0.9431449771 + 0.9577752948 0.9103174210 0.9434624910 + 0.9580236077 0.9107360244 0.9437801242 + 0.9582718015 0.9111546278 0.9440975785 + 0.9585201144 0.9115732908 0.9444152117 + 0.9587684274 0.9119918942 0.9447327256 + 0.9590166807 0.9124104977 0.9450501800 + 0.9592649937 0.9128291011 0.9453678131 + 0.9595133066 0.9132477045 0.9456853271 + 0.9597612023 0.9136605859 0.9459949136 + 0.9600089192 0.9140707254 0.9463008046 + 0.9602565765 0.9144809246 0.9466066957 + 0.9605042934 0.9148911238 0.9469127059 + 0.9607520103 0.9153013229 0.9472185969 + 0.9609997272 0.9157114029 0.9475244880 + 0.9612473845 0.9161216021 0.9478303790 + 0.9614952207 0.9165318012 0.9481363893 + 0.9617428780 0.9169420004 0.9484422803 + 0.9619905949 0.9173520803 0.9487481713 + 0.9622383118 0.9177622795 0.9490541816 + 0.9624860287 0.9181724787 0.9493600726 + 0.9627341032 0.9185807109 0.9496526122 + 0.9629822969 0.9189888239 0.9499449730 + 0.9632303715 0.9193968773 0.9502373934 + 0.9634785056 0.9198051095 0.9505296946 + 0.9637266994 0.9202132225 0.9508221149 + 0.9639747739 0.9206213951 0.9511144161 + 0.9642229080 0.9210295081 0.9514067769 + 0.9644711018 0.9214376211 0.9516991973 + 0.9647191763 0.9218457937 0.9519914985 + 0.9649673104 0.9222539067 0.9522839189 + 0.9652153850 0.9226620197 0.9525762200 + 0.9654629827 0.9230692983 0.9528676867 + 0.9657052755 0.9234678149 0.9531521797 + 0.9659475088 0.9238663912 0.9534366727 + 0.9661898017 0.9242649078 0.9537212253 + 0.9664319754 0.9246634841 0.9540057182 + 0.9666743279 0.9250620008 0.9542902112 + 0.9669165015 0.9254605770 0.9545747042 + 0.9671587944 0.9258590937 0.9548591971 + 0.9674010277 0.9262577295 0.9551436901 + 0.9676433206 0.9266563058 0.9554281235 + 0.9678854942 0.9270548224 0.9557126164 + 0.9681277871 0.9274533987 0.9559971094 + 0.9683696032 0.9278519750 0.9562814832 + 0.9685997963 0.9282519817 0.9565635920 + 0.9688299894 0.9286519885 0.9568457007 + 0.9690603018 0.9290519953 0.9571278095 + 0.9692904949 0.9294521213 0.9574099779 + 0.9695206881 0.9298521280 0.9576920867 + 0.9697510004 0.9302520752 0.9579741955 + 0.9699811935 0.9306520820 0.9582563043 + 0.9702113867 0.9310522079 0.9585384130 + 0.9704415798 0.9314522147 0.9588205814 + 0.9706718922 0.9318522215 0.9591026902 + 0.9709020853 0.9322522283 0.9593847990 + 0.9711322784 0.9326522946 0.9596669078 + 0.9713497162 0.9330475926 0.9599413872 + 0.9715638161 0.9334416986 0.9602140784 + 0.9717779160 0.9338359237 0.9604868889 + 0.9719918966 0.9342300296 0.9607595801 + 0.9722059965 0.9346240759 0.9610322714 + 0.9724200964 0.9350183010 0.9613050222 + 0.9726341963 0.9354124069 0.9615777731 + 0.9728482962 0.9358065724 0.9618505239 + 0.9730623960 0.9362006783 0.9621232152 + 0.9732764959 0.9365949035 0.9623959064 + 0.9734905958 0.9369890094 0.9626687169 + 0.9737046957 0.9373831749 0.9629414082 + 0.9739152193 0.9377775788 0.9632083178 + 0.9741197228 0.9381725192 0.9634661078 + 0.9743241072 0.9385674000 0.9637240171 + 0.9745286107 0.9389622211 0.9639819264 + 0.9747331142 0.9393571019 0.9642397761 + 0.9749376178 0.9397519827 0.9644976258 + 0.9751421213 0.9401469231 0.9647554755 + 0.9753466249 0.9405416846 0.9650133848 + 0.9755511284 0.9409366250 0.9652711749 + 0.9757555723 0.9413315058 0.9655290842 + 0.9759600759 0.9417263865 0.9657869935 + 0.9761645794 0.9421213269 0.9660447836 + 0.9763690233 0.9425160885 0.9663026929 + 0.9765669107 0.9429088235 0.9665576220 + 0.9767628908 0.9433009028 0.9668117166 + 0.9769588113 0.9436929822 0.9670658112 + 0.9771547914 0.9440850019 0.9673200250 + 0.9773507118 0.9444770813 0.9675741196 + 0.9775466919 0.9448692203 0.9678282142 + 0.9777426124 0.9452611804 0.9680823088 + 0.9779385924 0.9456533194 0.9683364034 + 0.9781345129 0.9460453987 0.9685904980 + 0.9783304930 0.9464374781 0.9688445926 + 0.9785264134 0.9468294978 0.9690986872 + 0.9787223935 0.9472215772 0.9693527818 + 0.9789183140 0.9476137161 0.9696068764 + 0.9791035056 0.9479907155 0.9698532224 + 0.9792885184 0.9483674169 0.9700992703 + 0.9794734120 0.9487441778 0.9703454971 + 0.9796584249 0.9491209984 0.9705916047 + 0.9798433781 0.9494978189 0.9708377719 + 0.9800283909 0.9498745203 0.9710839987 + 0.9802134037 0.9502512813 0.9713301063 + 0.9803982973 0.9506281018 0.9715762734 + 0.9805833101 0.9510049224 0.9718223810 + 0.9807683229 0.9513816237 0.9720686078 + 0.9809532762 0.9517583847 0.9723147750 + 0.9811382890 0.9521352053 0.9725608826 + 0.9813233018 0.9525119066 0.9728069901 + 0.9815040231 0.9528858066 0.9730427265 + 0.9816848040 0.9532597065 0.9732784033 + 0.9818655849 0.9536336064 0.9735140800 + 0.9820463061 0.9540073872 0.9737498760 + 0.9822270870 0.9543812871 0.9739856124 + 0.9824079275 0.9547551870 0.9742212892 + 0.9825885892 0.9551290274 0.9744570255 + 0.9827694297 0.9555029273 0.9746928215 + 0.9829502106 0.9558768272 0.9749284983 + 0.9831308722 0.9562507272 0.9751641750 + 0.9833117127 0.9566245079 0.9753999114 + 0.9834924936 0.9569984078 0.9756355882 + 0.9836732149 0.9573723078 0.9758713841 + 0.9838451743 0.9577409029 0.9761005044 + 0.9840152264 0.9581084251 0.9763283134 + 0.9841852784 0.9584758878 0.9765561223 + 0.9843552709 0.9588434100 0.9767838717 + 0.9845253229 0.9592108130 0.9770116806 + 0.9846953750 0.9595782757 0.9772394896 + 0.9848654270 0.9599457979 0.9774672985 + 0.9850354791 0.9603133202 0.9776951075 + 0.9852054715 0.9606807828 0.9779229164 + 0.9853755832 0.9610483050 0.9781507254 + 0.9855455756 0.9614158273 0.9783784747 + 0.9857156277 0.9617832899 0.9786062837 + 0.9858856797 0.9621506929 0.9788340926 + 0.9860520959 0.9625151753 0.9790583253 + 0.9862139821 0.9628759027 0.9792780876 + 0.9863759875 0.9632365704 0.9794977903 + 0.9865378737 0.9635972977 0.9797176123 + 0.9866998792 0.9639580250 0.9799373746 + 0.9868618250 0.9643186927 0.9801570773 + 0.9870237112 0.9646794200 0.9803768992 + 0.9871857166 0.9650400877 0.9805967212 + 0.9873476028 0.9654006958 0.9808164239 + 0.9875096083 0.9657614231 0.9810361862 + 0.9876714945 0.9661220908 0.9812560081 + 0.9878333807 0.9664828181 0.9814757109 + 0.9879953861 0.9668434858 0.9816954732 + 0.9881572723 0.9672042131 0.9819152951 + 0.9883103967 0.9675583243 0.9821261764 + 0.9884620905 0.9679114223 0.9823359251 + 0.9886137843 0.9682645202 0.9825456142 + 0.9887655973 0.9686176181 0.9827553034 + 0.9889172912 0.9689707160 0.9829649925 + 0.9890691042 0.9693238139 0.9831746817 + 0.9892207980 0.9696769118 0.9833843708 + 0.9893726110 0.9700300097 0.9835941195 + 0.9895243049 0.9703831077 0.9838038087 + 0.9896761179 0.9707363248 0.9840136170 + 0.9898278117 0.9710894227 0.9842233062 + 0.9899795055 0.9714425206 0.9844329953 + 0.9901313186 0.9717956185 0.9846426845 + 0.9902822971 0.9721478224 0.9848514795 + 0.9904264808 0.9724920988 0.9850528240 + 0.9905707240 0.9728363752 0.9852542281 + 0.9907147884 0.9731807113 0.9854555130 + 0.9908589721 0.9735249281 0.9856567979 + 0.9910032153 0.9738692045 0.9858580828 + 0.9911473989 0.9742134809 0.9860594273 + 0.9912915230 0.9745578170 0.9862607121 + 0.9914357066 0.9749019742 0.9864619970 + 0.9915798903 0.9752463102 0.9866632819 + 0.9917240739 0.9755905867 0.9868646264 + 0.9918681979 0.9759349227 0.9870659113 + 0.9920123816 0.9762790799 0.9872671962 + 0.9921566248 0.9766234159 0.9874684811 + 0.9922997952 0.9769669175 0.9876685143 + 0.9924352765 0.9773039818 0.9878587723 + 0.9925708771 0.9776411057 0.9880490899 + 0.9927064180 0.9779782295 0.9882394075 + 0.9928418994 0.9783154130 0.9884296060 + 0.9929775000 0.9786524773 0.9886199236 + 0.9931129813 0.9789896011 0.9888101816 + 0.9932485223 0.9793267250 0.9890004992 + 0.9933841228 0.9796637893 0.9891908169 + 0.9935196042 0.9800009727 0.9893810153 + 0.9936550856 0.9803380966 0.9895712733 + 0.9937906861 0.9806752205 0.9897615910 + 0.9939262271 0.9810122848 0.9899519086 + 0.9940617085 0.9813494086 0.9901422262 + 0.9941973090 0.9816865921 0.9903324246 + 0.9943200946 0.9820179939 0.9905114174 + 0.9944419265 0.9823489785 0.9906893969 + 0.9945635796 0.9826800227 0.9908673167 + 0.9946854115 0.9830108881 0.9910452962 + 0.9948071241 0.9833418727 0.9912232757 + 0.9949287772 0.9836729169 0.9914013147 + 0.9950506091 0.9840037823 0.9915792942 + 0.9951723218 0.9843348265 0.9917572737 + 0.9952940941 0.9846658111 0.9919353127 + 0.9954158068 0.9849967957 0.9921132922 + 0.9955375791 0.9853277206 0.9922912717 + 0.9956592917 0.9856587052 0.9924693108 + 0.9957811236 0.9859896898 0.9926472902 + 0.9959027767 0.9863206148 0.9928253293 + 0.9960159063 0.9866454005 0.9930018783 + 0.9961208105 0.9869645238 0.9931772947 + 0.9962257147 0.9872835279 0.9933525920 + 0.9963306785 0.9876025915 0.9935278893 + 0.9964355826 0.9879215956 0.9937033057 + 0.9965404868 0.9882405996 0.9938786030 + 0.9966455102 0.9885597229 0.9940540195 + 0.9967504144 0.9888787270 0.9942293167 + 0.9968553185 0.9891976714 0.9944046736 + 0.9969602823 0.9895167947 0.9945799708 + 0.9970651865 0.9898357987 0.9947553873 + 0.9971700907 0.9901549220 0.9949306846 + 0.9972751141 0.9904739261 0.9951061010 + 0.9973800182 0.9907928705 0.9952813983 + 0.9974849224 0.9911119938 0.9954568148 + 0.9975774884 0.9914191961 0.9956181049 + 0.9976686239 0.9917250276 0.9957777858 + 0.9977596998 0.9920307994 0.9959375858 + 0.9978507757 0.9923365712 0.9960973263 + 0.9979419708 0.9926424026 0.9962570071 + 0.9980331063 0.9929481745 0.9964166880 + 0.9981241822 0.9932541251 0.9965764880 + 0.9982153177 0.9935598969 0.9967362285 + 0.9983065128 0.9938657284 0.9968959093 + 0.9983975887 0.9941715002 0.9970557094 + 0.9984887242 0.9944772720 0.9972153902 + 0.9985798001 0.9947832227 0.9973750710 + 0.9986709952 0.9950889945 0.9975348711 + 0.9987620711 0.9953948259 0.9976946115 + 0.9988523722 0.9956995249 0.9978532791 + 0.9989289045 0.9959862232 0.9979965091 + 0.9990053773 0.9962729216 0.9981396198 + 0.9990819097 0.9965596199 0.9982826710 + 0.9991583824 0.9968463182 0.9984257817 + 0.9992349148 0.9971330166 0.9985688925 + 0.9993113875 0.9974197149 0.9987120032 + 0.9993879199 0.9977064133 0.9988551140 + 0.9994643927 0.9979931116 0.9989982247 + 0.9995409250 0.9982798100 0.9991412759 + 0.9996175170 0.9985665083 0.9992843866 + 0.9996939898 0.9988532066 0.9994276166 + 0.9997705221 0.9991399050 0.9995707273 + 0.9998469949 0.9994266033 0.9997137785 + 0.9999235272 0.9997133017 0.9998568892 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/fujifilm_fci.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/fujifilm_fci.spi1d index c4ba530a..ad6200f1 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/fujifilm_fci.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/fujifilm_fci.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0033529000 0.0050222902 0.0032476850 - 0.0072128819 0.0108181098 0.0077999481 - 0.0113698002 0.0162938796 0.0123000797 - 0.0156070897 0.0214859303 0.0166385006 - 0.0199807901 0.0263246894 0.0207970906 - 0.0243835095 0.0308334995 0.0247384906 - 0.0286132805 0.0350737683 0.0284963101 - 0.0324965417 0.0390635394 0.0320877396 - 0.0360950083 0.0428937711 0.0354904681 - 0.0395800695 0.0465790592 0.0387074091 - 0.0429466292 0.0501471013 0.0416742302 - 0.0462706387 0.0536180995 0.0443837605 - 0.0495057292 0.0570361391 0.0468798988 - 0.0526856817 0.0603578687 0.0492865406 - 0.0557528697 0.0635878816 0.0516750701 - 0.0587552190 0.0667311326 0.0540582016 - 0.0617134199 0.0698272586 0.0564595386 - 0.0646046400 0.0728573427 0.0588268489 - 0.0674475282 0.0758241564 0.0611601882 - 0.0702735782 0.0787356123 0.0634666085 - 0.0730631575 0.0815977603 0.0657490194 - 0.0758077726 0.0844080597 0.0680202469 - 0.0785278529 0.0871943235 0.0702772215 - 0.0811867565 0.0899614617 0.0725171790 - 0.0838195309 0.0926874802 0.0747344568 - 0.0863845199 0.0953808501 0.0769255832 - 0.0889284089 0.0980331302 0.0790845528 - 0.0914222598 0.1006408036 0.0812203065 - 0.0939096585 0.1032157987 0.0833272487 - 0.0963765979 0.1057695001 0.0854128227 - 0.0988149419 0.1082855985 0.0874713212 - 0.1012422964 0.1107629016 0.0895218775 - 0.1036200002 0.1132119000 0.0915501490 - 0.1059933975 0.1156331003 0.0935597792 - 0.1083453968 0.1180197969 0.0955581069 - 0.1106605977 0.1203960031 0.0975283831 - 0.1129731014 0.1227459982 0.0994904488 - 0.1152468994 0.1250717044 0.1014442965 - 0.1174897999 0.1273722053 0.1033670977 - 0.1197345033 0.1296402067 0.1052874029 - 0.1219704002 0.1319027990 0.1072127968 - 0.1241703033 0.1341574937 0.1091049984 - 0.1263649017 0.1363900006 0.1109855026 - 0.1285486072 0.1386093944 0.1128652021 - 0.1306926012 0.1408098936 0.1147162989 - 0.1328168064 0.1429897994 0.1165445000 - 0.1349381059 0.1451606005 0.1183724999 - 0.1370306015 0.1473139971 0.1202073991 - 0.1390942931 0.1494463980 0.1220164970 - 0.1411485970 0.1515574008 0.1238055974 - 0.1432065070 0.1536653936 0.1255939007 - 0.1452423036 0.1557527035 0.1273798943 - 0.1472520977 0.1578146964 0.1291383952 - 0.1492526978 0.1598653048 0.1308757067 - 0.1512524039 0.1619043052 0.1326085031 - 0.1532357931 0.1639177054 0.1343494058 - 0.1552000046 0.1659058034 0.1360910982 - 0.1571528018 0.1678874046 0.1378227025 - 0.1591075957 0.1698614955 0.1395412982 - 0.1610587984 0.1718100011 0.1412588060 - 0.1629907042 0.1737367064 0.1429695040 - 0.1649049968 0.1756578982 0.1446525007 - 0.1668141931 0.1775816977 0.1463117003 - 0.1687301993 0.1794971973 0.1479572952 - 0.1706393063 0.1813943982 0.1495992988 - 0.1725254953 0.1832813025 0.1512472928 - 0.1743942946 0.1851720065 0.1529006958 - 0.1762564927 0.1870612949 0.1545381993 - 0.1781218946 0.1889400929 0.1561595052 - 0.1799844950 0.1908051968 0.1577727944 - 0.1818296015 0.1926642060 0.1593860984 - 0.1836557984 0.1945220977 0.1609999985 - 0.1854719967 0.1963763982 0.1625989974 - 0.1872864068 0.1982188970 0.1641764939 - 0.1891041994 0.2000444978 0.1657395959 - 0.1909209043 0.2018636018 0.1672948003 - 0.1927119941 0.2036861926 0.1688483953 - 0.1944850981 0.2054955065 0.1704075038 - 0.1962500066 0.2072805017 0.1719689965 - 0.1980129927 0.2090491951 0.1735302061 - 0.1997700036 0.2108096033 0.1750829965 - 0.2015154958 0.2125660926 0.1766262054 - 0.2032480985 0.2143186033 0.1781643927 - 0.2049618065 0.2160613984 0.1797035038 - 0.2066676021 0.2177938968 0.1812402010 - 0.2083701938 0.2195162028 0.1827660948 - 0.2100692987 0.2212309986 0.1842781007 - 0.2117556036 0.2229442000 0.1857748926 - 0.2134384066 0.2246554941 0.1872583032 - 0.2151104063 0.2263627946 0.1887380928 - 0.2167720050 0.2280618995 0.1902170032 - 0.2184257954 0.2297437936 0.1916978061 - 0.2200689018 0.2314164937 0.1931888014 - 0.2217032015 0.2330885977 0.1946718991 - 0.2233294994 0.2347614020 0.1961459965 - 0.2249525934 0.2364338040 0.1976051033 - 0.2265653014 0.2380886972 0.1990566999 - 0.2281704992 0.2397311032 0.2005050927 - 0.2297694981 0.2413610071 0.2019519955 - 0.2313688993 0.2429879010 0.2034036070 - 0.2329650968 0.2446155995 0.2048534006 - 0.2345501930 0.2462483943 0.2062973976 - 0.2361329943 0.2478673011 0.2077303976 - 0.2377143055 0.2494771034 0.2091592997 - 0.2392926067 0.2510702908 0.2105841041 - 0.2408663034 0.2526575029 0.2120015025 - 0.2424364984 0.2542414963 0.2134159952 - 0.2439859062 0.2558271885 0.2148285061 - 0.2455307990 0.2574067116 0.2162439972 - 0.2470698953 0.2589786947 0.2176620960 - 0.2486097962 0.2605280876 0.2190825939 - 0.2501479089 0.2620697916 0.2204976976 - 0.2516750991 0.2635981143 0.2218970954 - 0.2531996965 0.2651236951 0.2232882977 - 0.2547213137 0.2666468024 0.2246735990 - 0.2562285066 0.2681731880 0.2260529995 - 0.2577311993 0.2696993053 0.2274315953 - 0.2592264116 0.2712090015 0.2288105935 - 0.2607224882 0.2727130949 0.2301889062 - 0.2622207999 0.2742044926 0.2315658033 - 0.2637135983 0.2756915987 0.2329387963 - 0.2652012110 0.2771748006 0.2343091965 - 0.2666864991 0.2786591947 0.2356632948 - 0.2681710124 0.2801440060 0.2370133996 - 0.2696504891 0.2816297114 0.2383510023 - 0.2711291909 0.2831149995 0.2396876067 - 0.2726014853 0.2845805883 0.2410192937 - 0.2740728855 0.2860446870 0.2423513979 - 0.2755376995 0.2874991000 0.2436847985 - 0.2770003974 0.2889532149 0.2450177968 - 0.2784548104 0.2904062867 0.2463497967 - 0.2799058855 0.2918587029 0.2476796955 - 0.2813473046 0.2933095098 0.2490058988 - 0.2827846110 0.2947534919 0.2503249049 - 0.2842110097 0.2961812913 0.2516323030 - 0.2856361866 0.2976050079 0.2529352903 - 0.2870579064 0.2990190089 0.2542308867 - 0.2884807885 0.3004320860 0.2555251122 - 0.2899079919 0.3018426001 0.2568162084 - 0.2913340926 0.3032543063 0.2581079900 - 0.2927548885 0.3046708107 0.2594015896 - 0.2941757143 0.3060868084 0.2606950998 - 0.2955969870 0.3074971139 0.2619884014 - 0.2970184088 0.3089073896 0.2632819116 - 0.2984277010 0.3103012145 0.2645807862 - 0.2998363078 0.3116944134 0.2658798099 - 0.3012431860 0.3130815029 0.2671642900 - 0.3026494980 0.3144671023 0.2684471011 - 0.3040472865 0.3158538043 0.2697230875 - 0.3054392934 0.3172411919 0.2709963024 - 0.3068284094 0.3186290860 0.2722682953 - 0.3082126975 0.3200176060 0.2735388875 - 0.3095946908 0.3214035928 0.2748104036 - 0.3109588921 0.3227730095 0.2760851979 - 0.3123230934 0.3241423070 0.2773601115 - 0.3136793077 0.3255028129 0.2786275148 - 0.3150336146 0.3268615901 0.2798944116 - 0.3163881004 0.3282201886 0.2811588943 - 0.3177425861 0.3295786977 0.2824218869 - 0.3190953135 0.3309383988 0.2836810946 - 0.3204368949 0.3323050141 0.2849281132 - 0.3217783868 0.3336715996 0.2861750126 - 0.3231084049 0.3350208998 0.2874141932 - 0.3244343996 0.3363648057 0.2886519134 - 0.3257552087 0.3377039135 0.2898885906 - 0.3270645142 0.3390335143 0.2911236882 - 0.3283737898 0.3403630853 0.2923589051 - 0.3296771944 0.3416889906 0.2935976982 - 0.3309794068 0.3430142105 0.2948367894 - 0.3322823942 0.3443411887 0.2960684001 - 0.3335868120 0.3456712961 0.2972904146 - 0.3348912001 0.3470014036 0.2985123992 - 0.3361887038 0.3483262956 0.2997243106 - 0.3374845088 0.3496501148 0.3009350896 - 0.3387758136 0.3509699106 0.3021413982 - 0.3400512934 0.3522767127 0.3033381999 - 0.3413268030 0.3535835147 0.3045350015 - 0.3425957859 0.3548851907 0.3057265878 - 0.3438603878 0.3561838865 0.3069159091 - 0.3451249003 0.3574824929 0.3081052899 - 0.3463880122 0.3587825894 0.3092936873 - 0.3476510942 0.3600828052 0.3104822040 - 0.3489136994 0.3613828123 0.3116725981 - 0.3501749933 0.3626824915 0.3128665984 - 0.3514364064 0.3639822900 0.3140606880 - 0.3526903093 0.3652707934 0.3152511120 - 0.3539369106 0.3665491939 0.3164390028 - 0.3551836014 0.3678277135 0.3176268935 - 0.3564186096 0.3690966964 0.3188065886 - 0.3576493859 0.3703624904 0.3199844956 - 0.3588801026 0.3716283143 0.3211624026 - 0.3601047099 0.3728910983 0.3223274946 - 0.3613288105 0.3741537035 0.3234925866 - 0.3625529110 0.3754166067 0.3246566951 - 0.3637793064 0.3766838908 0.3258154094 - 0.3650057018 0.3779512048 0.3269740045 - 0.3662337065 0.3792163134 0.3281326890 - 0.3674710095 0.3804695904 0.3292913139 - 0.3687084019 0.3817228973 0.3304499090 - 0.3699420094 0.3829731941 0.3316102922 - 0.3711602986 0.3842121959 0.3327750862 - 0.3723787069 0.3854511976 0.3339399993 - 0.3735949099 0.3866888881 0.3351032138 - 0.3748030066 0.3879219890 0.3362624049 - 0.3760110140 0.3891550899 0.3374215961 - 0.3772188127 0.3903886080 0.3385764062 - 0.3784255981 0.3916240931 0.3397186995 - 0.3796324134 0.3928596079 0.3408609927 - 0.3808397949 0.3940944076 0.3420014083 - 0.3820542097 0.3953225017 0.3431330025 - 0.3832687140 0.3965505064 0.3442645073 - 0.3844830990 0.3977786005 0.3453958035 - 0.3856852055 0.3989897966 0.3465226889 - 0.3868865967 0.4002004862 0.3476496041 - 0.3880881071 0.4014112055 0.3487764895 - 0.3892784119 0.4026145041 0.3499045968 - 0.3904655874 0.4038158953 0.3510330021 - 0.3916527927 0.4050172865 0.3521612883 - 0.3928368986 0.4062179923 0.3532944024 - 0.3940187097 0.4074181020 0.3544299901 - 0.3952004015 0.4086182117 0.3555656970 - 0.3963829875 0.4098179936 0.3566969037 - 0.3975678086 0.4110169113 0.3578208089 - 0.3987526894 0.4122158885 0.3589447141 - 0.3999375105 0.4134148955 0.3600678146 - 0.4011155963 0.4145950079 0.3611775935 - 0.4022935033 0.4157749116 0.3622873127 - 0.4034712911 0.4169546962 0.3633970916 - 0.4046387970 0.4181272984 0.3645012081 - 0.4057992101 0.4192951918 0.3656027913 - 0.4069595933 0.4204632044 0.3667044044 - 0.4081184864 0.4216302931 0.3678053021 - 0.4092698991 0.4227936864 0.3689045906 - 0.4104213119 0.4239571989 0.3700039089 - 0.4115728140 0.4251207113 0.3711032867 - 0.4127238989 0.4262751043 0.3721981943 - 0.4138750136 0.4274260998 0.3732920885 - 0.4150260091 0.4285770059 0.3743860126 - 0.4161767960 0.4297257066 0.3754799068 - 0.4173265100 0.4308654070 0.3765740991 - 0.4184763134 0.4320049882 0.3776682913 - 0.4196259975 0.4331446886 0.3787625134 - 0.4207628965 0.4342770875 0.3798545003 - 0.4218915105 0.4354051948 0.3809455037 - 0.4230200946 0.4365333021 0.3820365965 - 0.4241487980 0.4376614094 0.3831265867 - 0.4252648056 0.4387851059 0.3842037916 - 0.4263804853 0.4399087131 0.3852809966 - 0.4274963140 0.4410324097 0.3863582909 - 0.4286107123 0.4421544969 0.3874323070 - 0.4297220111 0.4432731867 0.3885014951 - 0.4308333099 0.4443919063 0.3895707130 - 0.4319446087 0.4455106854 0.3906398118 - 0.4330579937 0.4466271102 0.3917075098 - 0.4341728985 0.4477419853 0.3927743137 - 0.4352878034 0.4488568008 0.3938410878 - 0.4364027083 0.4499715865 0.3949080110 - 0.4375129938 0.4510774016 0.3959777057 - 0.4386217892 0.4521808028 0.3970479965 - 0.4397307038 0.4532842040 0.3981181979 - 0.4408395886 0.4543874860 0.3991884887 - 0.4419341087 0.4554865062 0.4002597928 - 0.4430272877 0.4565851986 0.4013310969 - 0.4441205859 0.4576838911 0.4024024010 - 0.4452137053 0.4587827027 0.4034720063 - 0.4462993145 0.4598833919 0.4045264125 - 0.4473848939 0.4609842002 0.4055807889 - 0.4484705031 0.4620848894 0.4066353142 - 0.4495561123 0.4631853998 0.4076882005 - 0.4506419003 0.4642808139 0.4087311029 - 0.4517278075 0.4653762877 0.4097740948 - 0.4528135955 0.4664717019 0.4108170867 - 0.4538995028 0.4675666988 0.4118593931 - 0.4549854994 0.4686492980 0.4128966033 - 0.4560714960 0.4697318971 0.4139338136 - 0.4571574926 0.4708144069 0.4149709940 - 0.4582434893 0.4718970060 0.4160081148 - 0.4593130946 0.4729729891 0.4170450866 - 0.4603815079 0.4740487039 0.4180820882 - 0.4614498913 0.4751243889 0.4191190898 - 0.4625183940 0.4762001038 0.4201560915 - 0.4635789990 0.4772703052 0.4211978018 - 0.4646374881 0.4783391058 0.4222402871 - 0.4656960070 0.4794079065 0.4232827127 - 0.4667544961 0.4804767072 0.4243251085 - 0.4678117931 0.4815419912 0.4253644943 - 0.4688681960 0.4826048911 0.4264025092 - 0.4699246883 0.4836677015 0.4274404049 - 0.4709810913 0.4847306013 0.4284783900 - 0.4720368981 0.4857918918 0.4295131862 - 0.4730910957 0.4868499935 0.4305432141 - 0.4741452932 0.4879080951 0.4315730929 - 0.4751994908 0.4889661968 0.4326030910 - 0.4762536883 0.4900242984 0.4336324930 - 0.4772971869 0.4910764992 0.4346519113 - 0.4783403873 0.4921286106 0.4356713891 - 0.4793835878 0.4931806922 0.4366908967 - 0.4804266989 0.4942328036 0.4377104044 - 0.4814636111 0.4952807128 0.4387266040 - 0.4824956954 0.4963257015 0.4397411942 - 0.4835278988 0.4973706007 0.4407558143 - 0.4845600128 0.4984155893 0.4417704046 - 0.4855917990 0.4994598031 0.4427852035 - 0.4866203964 0.5004987717 0.4438005090 - 0.4876489937 0.5015376806 0.4448158145 - 0.4886775911 0.5025765896 0.4458310902 - 0.4897061884 0.5036156178 0.4468463957 - 0.4907324910 0.5046514869 0.4478605092 - 0.4917572141 0.5056853294 0.4488740861 - 0.4927819073 0.5067191720 0.4498876035 - 0.4938066006 0.5077530146 0.4509010911 - 0.4948312938 0.5087867975 0.4519143999 - 0.4958443046 0.5098149776 0.4529229105 - 0.4968571067 0.5108430982 0.4539315104 - 0.4978697896 0.5118712783 0.4549399912 - 0.4988825917 0.5128993988 0.4559485912 - 0.4998916090 0.5139253736 0.4569540024 - 0.5008922219 0.5149468184 0.4579545856 - 0.5018928051 0.5159683228 0.4589551985 - 0.5028933883 0.5169898272 0.4599558115 - 0.5038939714 0.5180112123 0.4609563947 - 0.5048918128 0.5190292001 0.4619503915 - 0.5058875084 0.5200446248 0.4629409909 - 0.5068832040 0.5210601091 0.4639315009 - 0.5078788996 0.5220755935 0.4649220109 - 0.5088744760 0.5230910182 0.4659124911 - 0.5098662972 0.5240967274 0.4668990970 - 0.5108568072 0.5250995159 0.4678849876 - 0.5118473172 0.5261024237 0.4688709080 - 0.5128378868 0.5271052122 0.4698567986 - 0.5138283968 0.5281080008 0.4708425999 - 0.5148209929 0.5291042924 0.4718292058 - 0.5158140063 0.5300996900 0.4728156924 - 0.5168069005 0.5310950875 0.4738022983 - 0.5177997947 0.5320904851 0.4747888148 - 0.5187928081 0.5330858827 0.4757753909 - 0.5197734237 0.5340675712 0.4767676890 - 0.5207530260 0.5350484252 0.4777599871 - 0.5217326283 0.5360292196 0.4787522852 - 0.5227121115 0.5370100141 0.4797446132 - 0.5236917138 0.5379908085 0.4807369113 - 0.5246657729 0.5389664173 0.4817185104 - 0.5256392956 0.5399416089 0.4826998115 - 0.5266126990 0.5409168005 0.4836811125 - 0.5275862217 0.5418918729 0.4846624136 - 0.5285596251 0.5428671241 0.4856437147 - 0.5295339823 0.5438383222 0.4866138101 - 0.5305086970 0.5448085070 0.4875822067 - 0.5314832926 0.5457786918 0.4885506034 - 0.5324578881 0.5467488766 0.4895190895 - 0.5334326029 0.5477191806 0.4904874861 - 0.5344074965 0.5486854911 0.4914506078 - 0.5353826284 0.5496494770 0.4924111962 - 0.5363577008 0.5506135225 0.4933719039 - 0.5373327732 0.5515775084 0.4943326116 - 0.5383079052 0.5525414944 0.4952932000 - 0.5392804146 0.5535004735 0.4962526858 - 0.5402482152 0.5544509292 0.4972105920 - 0.5412160158 0.5554013848 0.4981685877 - 0.5421838164 0.5563517809 0.4991264939 - 0.5431516171 0.5573021770 0.5000844002 - 0.5441191792 0.5582525134 0.5010424852 - 0.5450795889 0.5591980219 0.5020027161 - 0.5460399985 0.5601435900 0.5029628873 - 0.5470004082 0.5610890985 0.5039231181 - 0.5479608178 0.5620347261 0.5048832297 - 0.5489212275 0.5629801750 0.5058434010 - 0.5498812795 0.5639281273 0.5067989230 - 0.5508412719 0.5648773909 0.5077521801 - 0.5518013239 0.5658267140 0.5087053776 - 0.5527613759 0.5667759776 0.5096586943 - 0.5537214279 0.5677253008 0.5106120110 - 0.5546813011 0.5686742067 0.5115646720 - 0.5556406975 0.5696194172 0.5125145912 - 0.5566000938 0.5705646276 0.5134643912 - 0.5575594902 0.5715097785 0.5144143105 - 0.5585188866 0.5724549890 0.5153642297 - 0.5594782829 0.5734001994 0.5163140893 - 0.5604329705 0.5743389726 0.5172604918 - 0.5613837242 0.5752723813 0.5182046294 - 0.5623344183 0.5762056708 0.5191487074 - 0.5632849932 0.5771390796 0.5200927854 - 0.5642356873 0.5780724883 0.5210369825 - 0.5651863813 0.5790058970 0.5219811201 - 0.5661293268 0.5799363256 0.5229173899 - 0.5670710802 0.5808663964 0.5238531828 - 0.5680128932 0.5817964077 0.5247889757 - 0.5689548254 0.5827264190 0.5257248878 - 0.5698965788 0.5836563706 0.5266606808 - 0.5708373189 0.5845860243 0.5275958776 - 0.5717707872 0.5855131745 0.5285283923 - 0.5727043152 0.5864403844 0.5294609070 - 0.5736377239 0.5873675942 0.5303934813 - 0.5745711923 0.5882948041 0.5313259959 - 0.5755046010 0.5892218947 0.5322586298 - 0.5764377117 0.5901458859 0.5331916809 - 0.5773698092 0.5910627246 0.5341259241 - 0.5783019066 0.5919793844 0.5350601077 - 0.5792340040 0.5928962231 0.5359942913 - 0.5801661015 0.5938128829 0.5369284749 - 0.5810983181 0.5947296023 0.5378627181 - 0.5820268989 0.5956423879 0.5387967229 - 0.5829498768 0.5965489149 0.5397306085 - 0.5838727951 0.5974555016 0.5406643748 - 0.5847957134 0.5983620286 0.5415983200 - 0.5857185721 0.5992684960 0.5425322056 - 0.5866414905 0.6001750231 0.5434659719 - 0.5875610113 0.6010807753 0.5443956852 - 0.5884745121 0.6019853950 0.5453197956 - 0.5893880129 0.6028900146 0.5462440252 - 0.5903015137 0.6037945747 0.5471680760 - 0.5912150145 0.6046991944 0.5480923057 - 0.5921285152 0.6056038141 0.5490164757 - 0.5930395126 0.6065080166 0.5499367118 - 0.5939435959 0.6074110866 0.5508491993 - 0.5948476791 0.6083140969 0.5517616868 - 0.5957517028 0.6092172265 0.5526741743 - 0.5966557860 0.6101201773 0.5535867810 - 0.5975599289 0.6110233068 0.5544993281 - 0.5984631777 0.6119254827 0.5554108024 - 0.5993574858 0.6128194928 0.5563166142 - 0.6002519131 0.6137136221 0.5572223067 - 0.6011462808 0.6146075726 0.5581281185 - 0.6020405889 0.6155017018 0.5590338111 - 0.6029350162 0.6163957119 0.5599395037 - 0.6038293242 0.6172897816 0.5608453155 - 0.6047170162 0.6181768179 0.5617492795 - 0.6056030989 0.6190621853 0.5626531243 - 0.6064891219 0.6199476123 0.5635567904 - 0.6073752046 0.6208329797 0.5644605756 - 0.6082612276 0.6217185259 0.5653644204 - 0.6091471910 0.6226038933 0.5662680864 - 0.6100293994 0.6234887838 0.5671733022 - 0.6109064221 0.6243731976 0.5680798888 - 0.6117833257 0.6252574921 0.5689864159 - 0.6126602888 0.6261419058 0.5698930025 - 0.6135371923 0.6270262003 0.5707995892 - 0.6144142151 0.6279106140 0.5717061758 - 0.6152911782 0.6287949085 0.5726128221 - 0.6161587834 0.6296725273 0.5735203028 - 0.6170259714 0.6305497885 0.5744279027 - 0.6178932190 0.6314271092 0.5753353834 - 0.6187602878 0.6323043704 0.5762429237 - 0.6196274757 0.6331816912 0.5771505237 - 0.6204947233 0.6340590119 0.5780580044 - 0.6213582158 0.6349307895 0.5789619088 - 0.6222159863 0.6357945204 0.5798614025 - 0.6230738163 0.6366583109 0.5807610154 - 0.6239315867 0.6375219822 0.5816605091 - 0.6247894168 0.6383857131 0.5825600028 - 0.6256471872 0.6392493844 0.5834594965 - 0.6265048981 0.6401131153 0.5843591094 - 0.6273571253 0.6409724951 0.5852504969 - 0.6282070279 0.6418303847 0.5861397982 - 0.6290569901 0.6426882148 0.5870289803 - 0.6299068928 0.6435461044 0.5879182816 - 0.6307569146 0.6444038749 0.5888075829 - 0.6316068172 0.6452617049 0.5896968842 - 0.6324567795 0.6461195946 0.5905860066 - 0.6333056092 0.6469790936 0.5914697051 - 0.6341544986 0.6478387117 0.5923534036 - 0.6350033283 0.6486982107 0.5932369828 - 0.6358520985 0.6495578289 0.5941206813 - 0.6367009282 0.6504173279 0.5950043797 - 0.6375498176 0.6512768865 0.5958880782 - 0.6383976936 0.6521351933 0.5967717171 - 0.6392375231 0.6529834867 0.5976545215 - 0.6400774121 0.6538317800 0.5985373855 - 0.6409171820 0.6546800733 0.5994203091 - 0.6417570114 0.6555284262 0.6003031135 - 0.6425967813 0.6563767195 0.6011859775 - 0.6434366107 0.6572250128 0.6020689011 - 0.6442754269 0.6580716968 0.6029512286 - 0.6451067924 0.6589083076 0.6038312912 - 0.6459382772 0.6597449780 0.6047112942 - 0.6467698216 0.6605815887 0.6055914164 - 0.6476011872 0.6614183187 0.6064714193 - 0.6484326720 0.6622549295 0.6073514819 - 0.6492642164 0.6630915999 0.6082314849 - 0.6500955820 0.6639279723 0.6091110110 - 0.6509268880 0.6647610068 0.6099858880 - 0.6517581940 0.6655939817 0.6108608842 - 0.6525893807 0.6664270163 0.6117358208 - 0.6534206867 0.6672599912 0.6126108170 - 0.6542518735 0.6680930853 0.6134856939 - 0.6550831795 0.6689261198 0.6143606901 - 0.6559144855 0.6697590947 0.6152356267 - 0.6567456722 0.6705949903 0.6160972714 - 0.6575769782 0.6714311838 0.6169580817 - 0.6584082246 0.6722674966 0.6178188920 - 0.6592394710 0.6731036901 0.6186797023 - 0.6600707769 0.6739400029 0.6195405126 - 0.6609020233 0.6747761965 0.6204013228 - 0.6617333293 0.6756125093 0.6212620735 - 0.6625599861 0.6764392257 0.6221166849 - 0.6633837223 0.6772598028 0.6229680777 - 0.6642073989 0.6780803204 0.6238194704 - 0.6650310755 0.6789008975 0.6246708035 - 0.6658548117 0.6797214150 0.6255221963 - 0.6666784883 0.6805419922 0.6263735294 - 0.6675022244 0.6813625097 0.6272249222 - 0.6683244705 0.6821811795 0.6280750036 - 0.6691414714 0.6829928756 0.6289216280 - 0.6699584723 0.6838045716 0.6297681928 - 0.6707754731 0.6846163869 0.6306148171 - 0.6715924740 0.6854280829 0.6314613819 - 0.6724094748 0.6862397790 0.6323080063 - 0.6732264757 0.6870514750 0.6331545711 - 0.6740434766 0.6878631711 0.6340011954 - 0.6748613715 0.6886737943 0.6348477006 - 0.6756796837 0.6894840002 0.6356942058 - 0.6764978766 0.6902940869 0.6365405917 - 0.6773161888 0.6911042929 0.6373870969 - 0.6781343818 0.6919143796 0.6382334828 - 0.6789526939 0.6927245855 0.6390799880 - 0.6797710061 0.6935346723 0.6399263740 - 0.6805893183 0.6943448186 0.6407734752 - 0.6814084053 0.6951546073 0.6416242123 - 0.6822274923 0.6959642768 0.6424750090 - 0.6830466986 0.6967741251 0.6433258057 - 0.6838657856 0.6975837946 0.6441764832 - 0.6846848726 0.6983935833 0.6450272799 - 0.6855040193 0.6992033124 0.6458780169 - 0.6863232255 0.7000131011 0.6467288136 - 0.6871380806 0.7008184195 0.6475781798 - 0.6879450083 0.7016159892 0.6484256983 - 0.6887519956 0.7024134994 0.6492732167 - 0.6895589828 0.7032110095 0.6501206756 - 0.6903660297 0.7040085793 0.6509681940 - 0.6911730170 0.7048060894 0.6518155932 - 0.6919798851 0.7056037188 0.6526631117 - 0.6927868724 0.7064012289 0.6535105705 - 0.6935915947 0.7071955800 0.6543517113 - 0.6943939924 0.7079871893 0.6551877260 - 0.6951963902 0.7087786794 0.6560238004 - 0.6959987879 0.7095702291 0.6568598151 - 0.6968013048 0.7103617787 0.6576958895 - 0.6976037025 0.7111533284 0.6585319042 - 0.6984061003 0.7119448185 0.6593679786 - 0.6992084980 0.7127364278 0.6602039933 - 0.7000085711 0.7135254145 0.6610358953 - 0.7008067966 0.7143123150 0.6618649960 - 0.7016050220 0.7150992155 0.6626940966 - 0.7024031878 0.7158861160 0.6635231972 - 0.7032014132 0.7166730165 0.6643521786 - 0.7039995790 0.7174599767 0.6651812792 - 0.7047978044 0.7182468772 0.6660103798 - 0.7055960298 0.7190337777 0.6668394804 - 0.7063922286 0.7198162079 0.6676673889 - 0.7071862221 0.7205935717 0.6684942245 - 0.7079802155 0.7213708758 0.6693210006 - 0.7087742090 0.7221482992 0.6701477766 - 0.7095682025 0.7229256034 0.6709744930 - 0.7103621960 0.7237030268 0.6718013287 - 0.7111561894 0.7244802713 0.6726281047 - 0.7119501829 0.7252575755 0.6734548807 - 0.7127416134 0.7260324955 0.6742823720 - 0.7135254741 0.7268006802 0.6751112938 - 0.7143093944 0.7275689244 0.6759402752 - 0.7150933146 0.7283371091 0.6767693162 - 0.7158772945 0.7291052938 0.6775982976 - 0.7166612148 0.7298734784 0.6784272790 - 0.7174450755 0.7306417227 0.6792562008 - 0.7182289958 0.7314097881 0.6800851822 - 0.7190129757 0.7321779728 0.6809142232 - 0.7197942138 0.7329385281 0.6817373037 - 0.7205750942 0.7336984277 0.6825603843 - 0.7213560939 0.7344583869 0.6833835244 - 0.7221370935 0.7352182865 0.6842064857 - 0.7229180932 0.7359783053 0.6850296259 - 0.7236990929 0.7367382050 0.6858527064 - 0.7244800925 0.7374982238 0.6866757870 - 0.7252609730 0.7382581234 0.6874988079 - 0.7260380983 0.7390146852 0.6883146763 - 0.7268108726 0.7397677898 0.6891244054 - 0.7275837064 0.7405210137 0.6899340749 - 0.7283565998 0.7412741184 0.6907438040 - 0.7291293740 0.7420272231 0.6915534735 - 0.7299022079 0.7427803278 0.6923632026 - 0.7306751013 0.7435334921 0.6931728721 - 0.7314478755 0.7442865968 0.6939826012 - 0.7322207093 0.7450397015 0.6947922707 - 0.7329800129 0.7457880974 0.6955934167 - 0.7337378860 0.7465360761 0.6963940263 - 0.7344958186 0.7472841740 0.6971946955 - 0.7352536917 0.7480322123 0.6979954243 - 0.7360116243 0.7487801909 0.6987960935 - 0.7367694974 0.7495282292 0.6995967031 - 0.7375273705 0.7502762079 0.7003973722 - 0.7382853031 0.7510241866 0.7011981010 - 0.7390416265 0.7517700195 0.7019975781 - 0.7397916913 0.7525079250 0.7027937174 - 0.7405418754 0.7532457709 0.7035897970 - 0.7412921190 0.7539836168 0.7043858767 - 0.7420421839 0.7547215223 0.7051818967 - 0.7427924275 0.7554594278 0.7059779763 - 0.7435424924 0.7561972141 0.7067741156 - 0.7442926764 0.7569351196 0.7075701952 - 0.7450429201 0.7576729059 0.7083662748 - 0.7457928061 0.7584072948 0.7091622949 - 0.7465423942 0.7591359019 0.7099583149 - 0.7472919226 0.7598643899 0.7107542157 - 0.7480415106 0.7605929971 0.7115501761 - 0.7487909794 0.7613216043 0.7123461962 - 0.7495406270 0.7620502114 0.7131420970 - 0.7502900958 0.7627788186 0.7139381170 - 0.7510396838 0.7635074258 0.7147340178 - 0.7517892122 0.7642359138 0.7155299783 - 0.7525364161 0.7649617195 0.7163277864 - 0.7532799244 0.7656834722 0.7171276808 - 0.7540233731 0.7664054036 0.7179276943 - 0.7547670007 0.7671272159 0.7187277079 - 0.7555105090 0.7678490281 0.7195277214 - 0.7562540770 0.7685709000 0.7203276157 - 0.7569975853 0.7692927122 0.7211276293 - 0.7577412128 0.7700145245 0.7219275832 - 0.7584847212 0.7707362771 0.7227275968 - 0.7592245936 0.7714542747 0.7235243917 - 0.7599560022 0.7721639276 0.7243157029 - 0.7606874108 0.7728734016 0.7251068950 - 0.7614188790 0.7735829949 0.7258980870 - 0.7621502876 0.7742925286 0.7266892791 - 0.7628818154 0.7750021219 0.7274805903 - 0.7636132240 0.7757117152 0.7282717824 - 0.7643446922 0.7764211893 0.7290629745 - 0.7650761008 0.7771307826 0.7298542261 - 0.7658072114 0.7778394818 0.7306442261 - 0.7665333748 0.7785378098 0.7314252257 - 0.7672595978 0.7792360783 0.7322061062 - 0.7679858208 0.7799345255 0.7329869866 - 0.7687121034 0.7806327939 0.7337679267 - 0.7694383264 0.7813311815 0.7345489264 - 0.7701644897 0.7820295095 0.7353298068 - 0.7708907127 0.7827278972 0.7361106873 - 0.7716168761 0.7834262252 0.7368916869 - 0.7723432183 0.7841246128 0.7376726270 - 0.7730664015 0.7848206162 0.7384489775 - 0.7737882733 0.7855157256 0.7392237782 - 0.7745103240 0.7862107754 0.7399985790 - 0.7752321959 0.7869058847 0.7407733798 - 0.7759541869 0.7876011133 0.7415481806 - 0.7766761184 0.7882962227 0.7423229814 - 0.7773981094 0.7889912724 0.7430977821 - 0.7781199813 0.7896863818 0.7438725829 - 0.7788419724 0.7903814912 0.7446473837 - 0.7795621753 0.7910752892 0.7454218268 - 0.7802751064 0.7917634845 0.7461947799 - 0.7809879780 0.7924516201 0.7469677925 - 0.7817009091 0.7931398153 0.7477408051 - 0.7824137807 0.7938280106 0.7485138178 - 0.7831267118 0.7945162058 0.7492867708 - 0.7838395834 0.7952044010 0.7500599027 - 0.7845525146 0.7958925962 0.7508329153 - 0.7852653861 0.7965807915 0.7516059279 - 0.7859783173 0.7972689867 0.7523788810 - 0.7866867781 0.7979509234 0.7531532049 - 0.7873914838 0.7986276150 0.7539284229 - 0.7880961895 0.7993041873 0.7547037005 - 0.7888008952 0.7999808192 0.7554789782 - 0.7895056009 0.8006573915 0.7562541962 - 0.7902103066 0.8013340831 0.7570294738 - 0.7909150124 0.8020107150 0.7578048110 - 0.7916197181 0.8026872873 0.7585800290 - 0.7923244238 0.8033639193 0.7593553066 - 0.7930291295 0.8040404916 0.7601305246 - 0.7937325239 0.8047140241 0.7609025836 - 0.7944355011 0.8053864241 0.7616736889 - 0.7951384783 0.8060588241 0.7624449134 - 0.7958415151 0.8067312241 0.7632160187 - 0.7965444922 0.8074036241 0.7639871240 - 0.7972475290 0.8080759048 0.7647582889 - 0.7979503870 0.8087483048 0.7655293941 - 0.7986534238 0.8094207048 0.7663004994 - 0.7993564010 0.8100931048 0.7670717239 - 0.8000593781 0.8107655048 0.7678428292 - 0.8007569909 0.8114361167 0.7686048746 - 0.8014531732 0.8121061921 0.7693650126 - 0.8021494150 0.8127763867 0.7701252103 - 0.8028455973 0.8134465218 0.7708852887 - 0.8035417795 0.8141167164 0.7716454864 - 0.8042379022 0.8147869110 0.7724056244 - 0.8049340844 0.8154569864 0.7731658220 - 0.8056303263 0.8161271811 0.7739259005 - 0.8063265085 0.8167973161 0.7746859789 - 0.8070226908 0.8174675107 0.7754461765 - 0.8077152967 0.8181294799 0.7762011886 - 0.8084064126 0.8187881112 0.7769542933 - 0.8090974092 0.8194466233 0.7777075171 - 0.8097885251 0.8201050758 0.7784606218 - 0.8104795218 0.8207635880 0.7792137265 - 0.8111705780 0.8214221001 0.7799667716 - 0.8118615746 0.8220806718 0.7807198763 - 0.8125526905 0.8227391839 0.7814729810 - 0.8132436872 0.8233976960 0.7822262049 - 0.8139348030 0.8240562081 0.7829793096 - 0.8146215081 0.8247125745 0.7837309837 - 0.8153027892 0.8253661990 0.7844812274 - 0.8159840703 0.8260198832 0.7852314115 - 0.8166654706 0.8266735077 0.7859815955 - 0.8173468113 0.8273271918 0.7867317796 - 0.8180280924 0.8279808760 0.7874820232 - 0.8187093735 0.8286345005 0.7882322073 - 0.8193907142 0.8292881846 0.7889823914 - 0.8200719953 0.8299418092 0.7897325754 - 0.8207532763 0.8305954933 0.7904828191 - 0.8214341998 0.8312489986 0.7912325859 - 0.8221067190 0.8318998218 0.7919784188 - 0.8227791786 0.8325507045 0.7927241921 - 0.8234515786 0.8332015276 0.7934700251 - 0.8241240978 0.8338522911 0.7942157984 - 0.8247966170 0.8345031738 0.7949615717 - 0.8254690766 0.8351539969 0.7957074046 - 0.8261414766 0.8358048797 0.7964531779 - 0.8268139958 0.8364557028 0.7971988916 - 0.8274865150 0.8371065259 0.7979447246 - 0.8281589150 0.8377574086 0.7986904979 - 0.8288310170 0.8384053111 0.7994338274 - 0.8295025229 0.8390501738 0.8001747727 - 0.8301740885 0.8396950960 0.8009157777 - 0.8308455944 0.8403400183 0.8016567826 - 0.8315172195 0.8409848809 0.8023977876 - 0.8321887255 0.8416298032 0.8031387925 - 0.8328602910 0.8422747254 0.8038797975 - 0.8335317969 0.8429197073 0.8046206832 - 0.8342034221 0.8435646296 0.8053616881 - 0.8348749280 0.8442094922 0.8061026931 - 0.8355464935 0.8448544145 0.8068436980 - 0.8362092972 0.8454958797 0.8075804114 - 0.8368695974 0.8461365104 0.8083159924 - 0.8375298977 0.8467770815 0.8090516925 - 0.8381903172 0.8474177122 0.8097872734 - 0.8388506174 0.8480582237 0.8105229735 - 0.8395109177 0.8486987948 0.8112586141 - 0.8401712775 0.8493394256 0.8119943142 - 0.8408315778 0.8499799967 0.8127298951 - 0.8414918780 0.8506206274 0.8134655952 - 0.8421522975 0.8512610793 0.8142011762 - 0.8428125978 0.8519017100 0.8149368763 - 0.8434634209 0.8525397778 0.8156663179 - 0.8441131711 0.8531776071 0.8163954020 - 0.8447631001 0.8538153768 0.8171244860 - 0.8454129100 0.8544532061 0.8178536296 - 0.8460627794 0.8550910950 0.8185827136 - 0.8467125893 0.8557289243 0.8193116784 - 0.8473623991 0.8563666940 0.8200408220 - 0.8480123281 0.8570045233 0.8207699060 - 0.8486620784 0.8576422930 0.8214989901 - 0.8493120074 0.8582801223 0.8222280741 - 0.8499618173 0.8589180112 0.8229570985 - 0.8506091237 0.8595502973 0.8236836791 - 0.8512560725 0.8601819873 0.8244099021 - 0.8519030809 0.8608136177 0.8251361847 - 0.8525500894 0.8614451885 0.8258624077 - 0.8531970978 0.8620768189 0.8265886903 - 0.8538441062 0.8627085090 0.8273149133 - 0.8544911146 0.8633400798 0.8280411959 - 0.8551381230 0.8639717102 0.8287674189 - 0.8557850718 0.8646034002 0.8294937015 - 0.8564320803 0.8652349710 0.8302199244 - 0.8570790291 0.8658666015 0.8309462070 - 0.8577216268 0.8664917946 0.8316696286 - 0.8583620787 0.8671138883 0.8323917985 - 0.8590025902 0.8677359819 0.8331140280 - 0.8596431017 0.8683581948 0.8338361979 - 0.8602834940 0.8689802885 0.8345584869 - 0.8609240055 0.8696023822 0.8352807164 - 0.8615645170 0.8702244759 0.8360028863 - 0.8622049093 0.8708466291 0.8367251158 - 0.8628454208 0.8714687824 0.8374474049 - 0.8634858727 0.8720908761 0.8381695747 - 0.8641263843 0.8727130294 0.8388918042 - 0.8647633195 0.8733326197 0.8396124840 - 0.8653923869 0.8739467263 0.8403300047 - 0.8660213947 0.8745607734 0.8410475850 - 0.8666505218 0.8751748800 0.8417651057 - 0.8672795296 0.8757889867 0.8424826860 - 0.8679085970 0.8764030933 0.8432003260 - 0.8685376048 0.8770172000 0.8439177871 - 0.8691666722 0.8776313066 0.8446354270 - 0.8697956800 0.8782454133 0.8453528881 - 0.8704248071 0.8788595200 0.8460705280 - 0.8710538149 0.8794736266 0.8467881083 - 0.8716828823 0.8800876737 0.8475056291 - 0.8723050952 0.8806946278 0.8482186794 - 0.8729252815 0.8812993169 0.8489307165 - 0.8735455275 0.8819040060 0.8496426940 - 0.8741655946 0.8825086951 0.8503546715 - 0.8747857809 0.8831133842 0.8510667086 - 0.8754060268 0.8837180734 0.8517786860 - 0.8760262132 0.8843228817 0.8524907231 - 0.8766463995 0.8849275708 0.8532027006 - 0.8772665262 0.8855323195 0.8539146781 - 0.8778867126 0.8861370087 0.8546267152 - 0.8785068989 0.8867416978 0.8553386927 - 0.8791267276 0.8873459101 0.8560500741 - 0.8797398210 0.8879420161 0.8567556143 - 0.8803529739 0.8885381818 0.8574609756 - 0.8809661865 0.8891344070 0.8581665158 - 0.8815793991 0.8897305727 0.8588718772 - 0.8821926117 0.8903266788 0.8595774174 - 0.8828057051 0.8909229040 0.8602827787 - 0.8834189177 0.8915191293 0.8609883189 - 0.8840320706 0.8921152949 0.8616936803 - 0.8846452832 0.8927114010 0.8623992205 - 0.8852584958 0.8933076262 0.8631045818 - 0.8858715892 0.8939037919 0.8638101220 - 0.8864827156 0.8944979906 0.8645150065 - 0.8870841861 0.8950834274 0.8652176857 - 0.8876857162 0.8956688046 0.8659204841 - 0.8882871866 0.8962541223 0.8666232228 - 0.8888887167 0.8968394995 0.8673259020 - 0.8894901872 0.8974248767 0.8680287004 - 0.8900917768 0.8980103135 0.8687313795 - 0.8906933069 0.8985956907 0.8694341779 - 0.8912947774 0.8991811275 0.8701369166 - 0.8918963075 0.8997665048 0.8708395958 - 0.8924977779 0.9003518224 0.8715423942 - 0.8930993080 0.9009371996 0.8722450733 - 0.8936997056 0.9015216231 0.8729472756 - 0.8942915797 0.9020984173 0.8736460209 - 0.8948835135 0.9026752114 0.8743448257 - 0.8954755068 0.9032518864 0.8750435114 - 0.8960673809 0.9038286805 0.8757423162 - 0.8966593742 0.9044054747 0.8764410019 - 0.8972513080 0.9049823284 0.8771396875 - 0.8978431821 0.9055591226 0.8778384924 - 0.8984351754 0.9061359167 0.8785371780 - 0.8990271091 0.9067127109 0.8792359829 - 0.8996191025 0.9072895050 0.8799347281 - 0.9002109766 0.9078662992 0.8806334138 - 0.9008030295 0.9084430933 0.8813322186 - 0.9013879895 0.9090055823 0.8820266724 - 0.9019721150 0.9095662236 0.8827208281 - 0.9025561810 0.9101268053 0.8834148049 - 0.9031403065 0.9106875062 0.8841089010 - 0.9037243724 0.9112480879 0.8848029971 - 0.9043084979 0.9118087292 0.8854969740 - 0.9048926234 0.9123693109 0.8861911297 - 0.9054766893 0.9129298925 0.8868852258 - 0.9060608149 0.9134904742 0.8875792027 - 0.9066448808 0.9140511155 0.8882732987 - 0.9072290063 0.9146116972 0.8889672756 - 0.9078130722 0.9151722789 0.8896613717 - 0.9083930254 0.9157286882 0.8903554082 - 0.9089676142 0.9162800908 0.8910493851 - 0.9095423222 0.9168314934 0.8917434216 - 0.9101169705 0.9173828959 0.8924373984 - 0.9106916189 0.9179342985 0.8931313753 - 0.9112663269 0.9184857011 0.8938252926 - 0.9118409753 0.9190371037 0.8945193291 - 0.9124156237 0.9195885062 0.8952133060 - 0.9129902720 0.9201399088 0.8959072828 - 0.9135649800 0.9206913114 0.8966013193 - 0.9141396880 0.9212427139 0.8972951770 - 0.9147142768 0.9217941165 0.8979892135 - 0.9152889848 0.9223455191 0.8986831903 - 0.9158567786 0.9228839278 0.8993654251 - 0.9164233804 0.9234200716 0.9000456929 - 0.9169899821 0.9239562154 0.9007260203 - 0.9175565839 0.9244924188 0.9014064074 - 0.9181231260 0.9250285029 0.9020866752 - 0.9186897278 0.9255647063 0.9027670026 - 0.9192563295 0.9261007905 0.9034472704 - 0.9198228717 0.9266369939 0.9041275978 - 0.9203894734 0.9271730781 0.9048079848 - 0.9209560156 0.9277092814 0.9054883122 - 0.9215226173 0.9282454252 0.9061685801 - 0.9220892191 0.9287816286 0.9068489075 - 0.9226552248 0.9293168783 0.9075281024 - 0.9232082963 0.9298372269 0.9081909060 - 0.9237614274 0.9303573966 0.9088537097 - 0.9243146181 0.9308776855 0.9095163941 - 0.9248676896 0.9313979745 0.9101791978 - 0.9254208207 0.9319182038 0.9108420014 - 0.9259738922 0.9324384928 0.9115046859 - 0.9265270829 0.9329587817 0.9121674895 - 0.9270802140 0.9334790111 0.9128302932 - 0.9276332855 0.9339993000 0.9134930968 - 0.9281864762 0.9345195889 0.9141557813 - 0.9287396073 0.9350398779 0.9148185849 - 0.9292926788 0.9355601072 0.9154813886 - 0.9298455119 0.9360799193 0.9161437750 - 0.9303920269 0.9365919232 0.9168015718 - 0.9309384823 0.9371039271 0.9174593091 - 0.9314851165 0.9376158118 0.9181171060 - 0.9320315719 0.9381278157 0.9187749028 - 0.9325782061 0.9386398196 0.9194325805 - 0.9331247211 0.9391518235 0.9200903773 - 0.9336711764 0.9396638274 0.9207481146 - 0.9342178106 0.9401757717 0.9214059114 - 0.9347643256 0.9406877756 0.9220637083 - 0.9353107810 0.9411997199 0.9227213860 - 0.9358574152 0.9417117238 0.9233791828 - 0.9364038706 0.9422237277 0.9240369797 - 0.9369503856 0.9427356720 0.9246947169 - 0.9374883771 0.9432380795 0.9253485203 - 0.9380251169 0.9437391758 0.9260017276 - 0.9385617971 0.9442402720 0.9266549945 - 0.9390985966 0.9447414279 0.9273082018 - 0.9396352768 0.9452425241 0.9279615283 - 0.9401720166 0.9457436204 0.9286146760 - 0.9407088161 0.9462447166 0.9292680025 - 0.9412454963 0.9467458129 0.9299212098 - 0.9417821765 0.9472469091 0.9305744767 - 0.9423189163 0.9477481246 0.9312276840 - 0.9428557158 0.9482492208 0.9318810105 - 0.9433923960 0.9487503171 0.9325342178 - 0.9439290762 0.9492514133 0.9331874847 - 0.9444622993 0.9497454166 0.9338380098 - 0.9449920058 0.9502323866 0.9344856739 - 0.9455215931 0.9507192969 0.9351335168 - 0.9460512996 0.9512062073 0.9357813001 - 0.9465808868 0.9516931772 0.9364290833 - 0.9471105933 0.9521800876 0.9370768070 - 0.9476401806 0.9526671171 0.9377245903 - 0.9481698275 0.9531540275 0.9383723736 - 0.9486994743 0.9536408782 0.9390202165 - 0.9492291212 0.9541279078 0.9396679997 - 0.9497588277 0.9546148181 0.9403157234 - 0.9502884150 0.9551017880 0.9409635067 - 0.9508181214 0.9555886984 0.9416112900 - 0.9513477087 0.9560756087 0.9422590733 - 0.9518715739 0.9565510750 0.9428948164 - 0.9523950815 0.9570255280 0.9435294867 - 0.9529185295 0.9574998021 0.9441642165 - 0.9534419179 0.9579741955 0.9447988868 - 0.9539653063 0.9584485888 0.9454336166 - 0.9544886947 0.9589229822 0.9460684061 - 0.9550120831 0.9593973756 0.9467030764 - 0.9555354714 0.9598717093 0.9473378062 - 0.9560589194 0.9603461027 0.9479724765 - 0.9565823078 0.9608204961 0.9486072063 - 0.9571056962 0.9612948895 0.9492419958 - 0.9576292038 0.9617692828 0.9498767257 - 0.9581525922 0.9622436166 0.9505113959 - 0.9586743712 0.9627162218 0.9511442780 - 0.9591860175 0.9631766081 0.9517663717 - 0.9596974850 0.9636369944 0.9523884058 - 0.9602090716 0.9640974998 0.9530103803 - 0.9607207179 0.9645578861 0.9536324739 - 0.9612321854 0.9650183916 0.9542545080 - 0.9617437720 0.9654787779 0.9548764825 - 0.9622554183 0.9659392238 0.9554985762 - 0.9627668858 0.9663997293 0.9561206102 - 0.9632784724 0.9668601155 0.9567425847 - 0.9637901187 0.9673205018 0.9573646784 - 0.9643015862 0.9677810073 0.9579867125 - 0.9648131728 0.9682413936 0.9586086869 - 0.9653248191 0.9687018991 0.9592307210 - 0.9658346176 0.9691603184 0.9598506093 - 0.9663344026 0.9696068764 0.9604582787 - 0.9668341279 0.9700533748 0.9610658884 - 0.9673337936 0.9704999924 0.9616736174 - 0.9678335786 0.9709466100 0.9622812271 - 0.9683333039 0.9713931084 0.9628888965 - 0.9688330293 0.9718397260 0.9634965062 - 0.9693328142 0.9722862840 0.9641041756 - 0.9698324800 0.9727329016 0.9647119045 - 0.9703322053 0.9731794000 0.9653195143 - 0.9708318710 0.9736260176 0.9659271836 - 0.9713317156 0.9740725756 0.9665347934 - 0.9718313813 0.9745190740 0.9671425223 - 0.9723311067 0.9749656916 0.9677500725 - 0.9728308916 0.9754123092 0.9683578014 - 0.9733167291 0.9758465290 0.9689434767 - 0.9738017917 0.9762800932 0.9695280790 - 0.9742867947 0.9767135978 0.9701126814 - 0.9747719169 0.9771471024 0.9706972241 - 0.9752569795 0.9775807261 0.9712818265 - 0.9757419825 0.9780141711 0.9718664289 - 0.9762271047 0.9784476757 0.9724509716 - 0.9767121077 0.9788812995 0.9730355740 - 0.9771971703 0.9793148041 0.9736201763 - 0.9776821733 0.9797484279 0.9742047787 - 0.9781672955 0.9801818728 0.9747893214 - 0.9786522985 0.9806153774 0.9753739238 - 0.9791374207 0.9810490012 0.9759585261 - 0.9796224236 0.9814825058 0.9765431285 - 0.9801008105 0.9819086790 0.9771106243 - 0.9805731773 0.9823285937 0.9776636958 - 0.9810456038 0.9827483892 0.9782167077 - 0.9815180898 0.9831681848 0.9787697196 - 0.9819905162 0.9835880995 0.9793226719 - 0.9824628830 0.9840078950 0.9798758030 - 0.9829354286 0.9844278097 0.9804288149 - 0.9834077954 0.9848476052 0.9809818268 - 0.9838802814 0.9852675200 0.9815347791 - 0.9843527079 0.9856873155 0.9820877910 - 0.9848250747 0.9861071706 0.9826409221 - 0.9852976203 0.9865270257 0.9831938744 - 0.9857699871 0.9869468212 0.9837468863 - 0.9862424731 0.9873666763 0.9842998981 - 0.9867148995 0.9877864718 0.9848530293 - 0.9871708751 0.9882004857 0.9853777289 - 0.9876251817 0.9886139035 0.9858996868 - 0.9880794883 0.9890273213 0.9864217043 - 0.9885339141 0.9894406796 0.9869437218 - 0.9889882207 0.9898540974 0.9874656796 - 0.9894425273 0.9902675152 0.9879876971 - 0.9898968935 0.9906808734 0.9885097146 - 0.9903512001 0.9910942912 0.9890317917 - 0.9908055067 0.9915077090 0.9895538092 - 0.9912598729 0.9919211268 0.9900758266 - 0.9917141795 0.9923344851 0.9905977845 - 0.9921684861 0.9927479029 0.9911198020 - 0.9926229119 0.9931613207 0.9916418195 - 0.9930772185 0.9935746789 0.9921637774 - 0.9935299754 0.9939873219 0.9926834702 - 0.9939612746 0.9943881035 0.9931712747 - 0.9943926930 0.9947890043 0.9936590791 - 0.9948239923 0.9951897860 0.9941468239 - 0.9952552915 0.9955906868 0.9946346283 - 0.9956867099 0.9959915280 0.9951223731 - 0.9961180091 0.9963924289 0.9956101179 - 0.9965493083 0.9967932105 0.9960979223 - 0.9969807267 0.9971941113 0.9965857267 - 0.9974120259 0.9975948930 0.9970734119 - 0.9978433251 0.9979957938 0.9975612164 - 0.9982746840 0.9983965755 0.9980489016 - 0.9987059832 0.9987974763 0.9985367060 - 0.9991372824 0.9991983175 0.9990245104 - 0.9995687008 0.9995992184 0.9995121956 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0033529000 0.0050222902 0.0032476850 + 0.0072128819 0.0108181098 0.0077999481 + 0.0113698002 0.0162938796 0.0123000797 + 0.0156070897 0.0214859303 0.0166385006 + 0.0199807901 0.0263246894 0.0207970906 + 0.0243835095 0.0308334995 0.0247384906 + 0.0286132805 0.0350737683 0.0284963101 + 0.0324965417 0.0390635394 0.0320877396 + 0.0360950083 0.0428937711 0.0354904681 + 0.0395800695 0.0465790592 0.0387074091 + 0.0429466292 0.0501471013 0.0416742302 + 0.0462706387 0.0536180995 0.0443837605 + 0.0495057292 0.0570361391 0.0468798988 + 0.0526856817 0.0603578687 0.0492865406 + 0.0557528697 0.0635878816 0.0516750701 + 0.0587552190 0.0667311326 0.0540582016 + 0.0617134199 0.0698272586 0.0564595386 + 0.0646046400 0.0728573427 0.0588268489 + 0.0674475282 0.0758241564 0.0611601882 + 0.0702735782 0.0787356123 0.0634666085 + 0.0730631575 0.0815977603 0.0657490194 + 0.0758077726 0.0844080597 0.0680202469 + 0.0785278529 0.0871943235 0.0702772215 + 0.0811867565 0.0899614617 0.0725171790 + 0.0838195309 0.0926874802 0.0747344568 + 0.0863845199 0.0953808501 0.0769255832 + 0.0889284089 0.0980331302 0.0790845528 + 0.0914222598 0.1006408036 0.0812203065 + 0.0939096585 0.1032157987 0.0833272487 + 0.0963765979 0.1057695001 0.0854128227 + 0.0988149419 0.1082855985 0.0874713212 + 0.1012422964 0.1107629016 0.0895218775 + 0.1036200002 0.1132119000 0.0915501490 + 0.1059933975 0.1156331003 0.0935597792 + 0.1083453968 0.1180197969 0.0955581069 + 0.1106605977 0.1203960031 0.0975283831 + 0.1129731014 0.1227459982 0.0994904488 + 0.1152468994 0.1250717044 0.1014442965 + 0.1174897999 0.1273722053 0.1033670977 + 0.1197345033 0.1296402067 0.1052874029 + 0.1219704002 0.1319027990 0.1072127968 + 0.1241703033 0.1341574937 0.1091049984 + 0.1263649017 0.1363900006 0.1109855026 + 0.1285486072 0.1386093944 0.1128652021 + 0.1306926012 0.1408098936 0.1147162989 + 0.1328168064 0.1429897994 0.1165445000 + 0.1349381059 0.1451606005 0.1183724999 + 0.1370306015 0.1473139971 0.1202073991 + 0.1390942931 0.1494463980 0.1220164970 + 0.1411485970 0.1515574008 0.1238055974 + 0.1432065070 0.1536653936 0.1255939007 + 0.1452423036 0.1557527035 0.1273798943 + 0.1472520977 0.1578146964 0.1291383952 + 0.1492526978 0.1598653048 0.1308757067 + 0.1512524039 0.1619043052 0.1326085031 + 0.1532357931 0.1639177054 0.1343494058 + 0.1552000046 0.1659058034 0.1360910982 + 0.1571528018 0.1678874046 0.1378227025 + 0.1591075957 0.1698614955 0.1395412982 + 0.1610587984 0.1718100011 0.1412588060 + 0.1629907042 0.1737367064 0.1429695040 + 0.1649049968 0.1756578982 0.1446525007 + 0.1668141931 0.1775816977 0.1463117003 + 0.1687301993 0.1794971973 0.1479572952 + 0.1706393063 0.1813943982 0.1495992988 + 0.1725254953 0.1832813025 0.1512472928 + 0.1743942946 0.1851720065 0.1529006958 + 0.1762564927 0.1870612949 0.1545381993 + 0.1781218946 0.1889400929 0.1561595052 + 0.1799844950 0.1908051968 0.1577727944 + 0.1818296015 0.1926642060 0.1593860984 + 0.1836557984 0.1945220977 0.1609999985 + 0.1854719967 0.1963763982 0.1625989974 + 0.1872864068 0.1982188970 0.1641764939 + 0.1891041994 0.2000444978 0.1657395959 + 0.1909209043 0.2018636018 0.1672948003 + 0.1927119941 0.2036861926 0.1688483953 + 0.1944850981 0.2054955065 0.1704075038 + 0.1962500066 0.2072805017 0.1719689965 + 0.1980129927 0.2090491951 0.1735302061 + 0.1997700036 0.2108096033 0.1750829965 + 0.2015154958 0.2125660926 0.1766262054 + 0.2032480985 0.2143186033 0.1781643927 + 0.2049618065 0.2160613984 0.1797035038 + 0.2066676021 0.2177938968 0.1812402010 + 0.2083701938 0.2195162028 0.1827660948 + 0.2100692987 0.2212309986 0.1842781007 + 0.2117556036 0.2229442000 0.1857748926 + 0.2134384066 0.2246554941 0.1872583032 + 0.2151104063 0.2263627946 0.1887380928 + 0.2167720050 0.2280618995 0.1902170032 + 0.2184257954 0.2297437936 0.1916978061 + 0.2200689018 0.2314164937 0.1931888014 + 0.2217032015 0.2330885977 0.1946718991 + 0.2233294994 0.2347614020 0.1961459965 + 0.2249525934 0.2364338040 0.1976051033 + 0.2265653014 0.2380886972 0.1990566999 + 0.2281704992 0.2397311032 0.2005050927 + 0.2297694981 0.2413610071 0.2019519955 + 0.2313688993 0.2429879010 0.2034036070 + 0.2329650968 0.2446155995 0.2048534006 + 0.2345501930 0.2462483943 0.2062973976 + 0.2361329943 0.2478673011 0.2077303976 + 0.2377143055 0.2494771034 0.2091592997 + 0.2392926067 0.2510702908 0.2105841041 + 0.2408663034 0.2526575029 0.2120015025 + 0.2424364984 0.2542414963 0.2134159952 + 0.2439859062 0.2558271885 0.2148285061 + 0.2455307990 0.2574067116 0.2162439972 + 0.2470698953 0.2589786947 0.2176620960 + 0.2486097962 0.2605280876 0.2190825939 + 0.2501479089 0.2620697916 0.2204976976 + 0.2516750991 0.2635981143 0.2218970954 + 0.2531996965 0.2651236951 0.2232882977 + 0.2547213137 0.2666468024 0.2246735990 + 0.2562285066 0.2681731880 0.2260529995 + 0.2577311993 0.2696993053 0.2274315953 + 0.2592264116 0.2712090015 0.2288105935 + 0.2607224882 0.2727130949 0.2301889062 + 0.2622207999 0.2742044926 0.2315658033 + 0.2637135983 0.2756915987 0.2329387963 + 0.2652012110 0.2771748006 0.2343091965 + 0.2666864991 0.2786591947 0.2356632948 + 0.2681710124 0.2801440060 0.2370133996 + 0.2696504891 0.2816297114 0.2383510023 + 0.2711291909 0.2831149995 0.2396876067 + 0.2726014853 0.2845805883 0.2410192937 + 0.2740728855 0.2860446870 0.2423513979 + 0.2755376995 0.2874991000 0.2436847985 + 0.2770003974 0.2889532149 0.2450177968 + 0.2784548104 0.2904062867 0.2463497967 + 0.2799058855 0.2918587029 0.2476796955 + 0.2813473046 0.2933095098 0.2490058988 + 0.2827846110 0.2947534919 0.2503249049 + 0.2842110097 0.2961812913 0.2516323030 + 0.2856361866 0.2976050079 0.2529352903 + 0.2870579064 0.2990190089 0.2542308867 + 0.2884807885 0.3004320860 0.2555251122 + 0.2899079919 0.3018426001 0.2568162084 + 0.2913340926 0.3032543063 0.2581079900 + 0.2927548885 0.3046708107 0.2594015896 + 0.2941757143 0.3060868084 0.2606950998 + 0.2955969870 0.3074971139 0.2619884014 + 0.2970184088 0.3089073896 0.2632819116 + 0.2984277010 0.3103012145 0.2645807862 + 0.2998363078 0.3116944134 0.2658798099 + 0.3012431860 0.3130815029 0.2671642900 + 0.3026494980 0.3144671023 0.2684471011 + 0.3040472865 0.3158538043 0.2697230875 + 0.3054392934 0.3172411919 0.2709963024 + 0.3068284094 0.3186290860 0.2722682953 + 0.3082126975 0.3200176060 0.2735388875 + 0.3095946908 0.3214035928 0.2748104036 + 0.3109588921 0.3227730095 0.2760851979 + 0.3123230934 0.3241423070 0.2773601115 + 0.3136793077 0.3255028129 0.2786275148 + 0.3150336146 0.3268615901 0.2798944116 + 0.3163881004 0.3282201886 0.2811588943 + 0.3177425861 0.3295786977 0.2824218869 + 0.3190953135 0.3309383988 0.2836810946 + 0.3204368949 0.3323050141 0.2849281132 + 0.3217783868 0.3336715996 0.2861750126 + 0.3231084049 0.3350208998 0.2874141932 + 0.3244343996 0.3363648057 0.2886519134 + 0.3257552087 0.3377039135 0.2898885906 + 0.3270645142 0.3390335143 0.2911236882 + 0.3283737898 0.3403630853 0.2923589051 + 0.3296771944 0.3416889906 0.2935976982 + 0.3309794068 0.3430142105 0.2948367894 + 0.3322823942 0.3443411887 0.2960684001 + 0.3335868120 0.3456712961 0.2972904146 + 0.3348912001 0.3470014036 0.2985123992 + 0.3361887038 0.3483262956 0.2997243106 + 0.3374845088 0.3496501148 0.3009350896 + 0.3387758136 0.3509699106 0.3021413982 + 0.3400512934 0.3522767127 0.3033381999 + 0.3413268030 0.3535835147 0.3045350015 + 0.3425957859 0.3548851907 0.3057265878 + 0.3438603878 0.3561838865 0.3069159091 + 0.3451249003 0.3574824929 0.3081052899 + 0.3463880122 0.3587825894 0.3092936873 + 0.3476510942 0.3600828052 0.3104822040 + 0.3489136994 0.3613828123 0.3116725981 + 0.3501749933 0.3626824915 0.3128665984 + 0.3514364064 0.3639822900 0.3140606880 + 0.3526903093 0.3652707934 0.3152511120 + 0.3539369106 0.3665491939 0.3164390028 + 0.3551836014 0.3678277135 0.3176268935 + 0.3564186096 0.3690966964 0.3188065886 + 0.3576493859 0.3703624904 0.3199844956 + 0.3588801026 0.3716283143 0.3211624026 + 0.3601047099 0.3728910983 0.3223274946 + 0.3613288105 0.3741537035 0.3234925866 + 0.3625529110 0.3754166067 0.3246566951 + 0.3637793064 0.3766838908 0.3258154094 + 0.3650057018 0.3779512048 0.3269740045 + 0.3662337065 0.3792163134 0.3281326890 + 0.3674710095 0.3804695904 0.3292913139 + 0.3687084019 0.3817228973 0.3304499090 + 0.3699420094 0.3829731941 0.3316102922 + 0.3711602986 0.3842121959 0.3327750862 + 0.3723787069 0.3854511976 0.3339399993 + 0.3735949099 0.3866888881 0.3351032138 + 0.3748030066 0.3879219890 0.3362624049 + 0.3760110140 0.3891550899 0.3374215961 + 0.3772188127 0.3903886080 0.3385764062 + 0.3784255981 0.3916240931 0.3397186995 + 0.3796324134 0.3928596079 0.3408609927 + 0.3808397949 0.3940944076 0.3420014083 + 0.3820542097 0.3953225017 0.3431330025 + 0.3832687140 0.3965505064 0.3442645073 + 0.3844830990 0.3977786005 0.3453958035 + 0.3856852055 0.3989897966 0.3465226889 + 0.3868865967 0.4002004862 0.3476496041 + 0.3880881071 0.4014112055 0.3487764895 + 0.3892784119 0.4026145041 0.3499045968 + 0.3904655874 0.4038158953 0.3510330021 + 0.3916527927 0.4050172865 0.3521612883 + 0.3928368986 0.4062179923 0.3532944024 + 0.3940187097 0.4074181020 0.3544299901 + 0.3952004015 0.4086182117 0.3555656970 + 0.3963829875 0.4098179936 0.3566969037 + 0.3975678086 0.4110169113 0.3578208089 + 0.3987526894 0.4122158885 0.3589447141 + 0.3999375105 0.4134148955 0.3600678146 + 0.4011155963 0.4145950079 0.3611775935 + 0.4022935033 0.4157749116 0.3622873127 + 0.4034712911 0.4169546962 0.3633970916 + 0.4046387970 0.4181272984 0.3645012081 + 0.4057992101 0.4192951918 0.3656027913 + 0.4069595933 0.4204632044 0.3667044044 + 0.4081184864 0.4216302931 0.3678053021 + 0.4092698991 0.4227936864 0.3689045906 + 0.4104213119 0.4239571989 0.3700039089 + 0.4115728140 0.4251207113 0.3711032867 + 0.4127238989 0.4262751043 0.3721981943 + 0.4138750136 0.4274260998 0.3732920885 + 0.4150260091 0.4285770059 0.3743860126 + 0.4161767960 0.4297257066 0.3754799068 + 0.4173265100 0.4308654070 0.3765740991 + 0.4184763134 0.4320049882 0.3776682913 + 0.4196259975 0.4331446886 0.3787625134 + 0.4207628965 0.4342770875 0.3798545003 + 0.4218915105 0.4354051948 0.3809455037 + 0.4230200946 0.4365333021 0.3820365965 + 0.4241487980 0.4376614094 0.3831265867 + 0.4252648056 0.4387851059 0.3842037916 + 0.4263804853 0.4399087131 0.3852809966 + 0.4274963140 0.4410324097 0.3863582909 + 0.4286107123 0.4421544969 0.3874323070 + 0.4297220111 0.4432731867 0.3885014951 + 0.4308333099 0.4443919063 0.3895707130 + 0.4319446087 0.4455106854 0.3906398118 + 0.4330579937 0.4466271102 0.3917075098 + 0.4341728985 0.4477419853 0.3927743137 + 0.4352878034 0.4488568008 0.3938410878 + 0.4364027083 0.4499715865 0.3949080110 + 0.4375129938 0.4510774016 0.3959777057 + 0.4386217892 0.4521808028 0.3970479965 + 0.4397307038 0.4532842040 0.3981181979 + 0.4408395886 0.4543874860 0.3991884887 + 0.4419341087 0.4554865062 0.4002597928 + 0.4430272877 0.4565851986 0.4013310969 + 0.4441205859 0.4576838911 0.4024024010 + 0.4452137053 0.4587827027 0.4034720063 + 0.4462993145 0.4598833919 0.4045264125 + 0.4473848939 0.4609842002 0.4055807889 + 0.4484705031 0.4620848894 0.4066353142 + 0.4495561123 0.4631853998 0.4076882005 + 0.4506419003 0.4642808139 0.4087311029 + 0.4517278075 0.4653762877 0.4097740948 + 0.4528135955 0.4664717019 0.4108170867 + 0.4538995028 0.4675666988 0.4118593931 + 0.4549854994 0.4686492980 0.4128966033 + 0.4560714960 0.4697318971 0.4139338136 + 0.4571574926 0.4708144069 0.4149709940 + 0.4582434893 0.4718970060 0.4160081148 + 0.4593130946 0.4729729891 0.4170450866 + 0.4603815079 0.4740487039 0.4180820882 + 0.4614498913 0.4751243889 0.4191190898 + 0.4625183940 0.4762001038 0.4201560915 + 0.4635789990 0.4772703052 0.4211978018 + 0.4646374881 0.4783391058 0.4222402871 + 0.4656960070 0.4794079065 0.4232827127 + 0.4667544961 0.4804767072 0.4243251085 + 0.4678117931 0.4815419912 0.4253644943 + 0.4688681960 0.4826048911 0.4264025092 + 0.4699246883 0.4836677015 0.4274404049 + 0.4709810913 0.4847306013 0.4284783900 + 0.4720368981 0.4857918918 0.4295131862 + 0.4730910957 0.4868499935 0.4305432141 + 0.4741452932 0.4879080951 0.4315730929 + 0.4751994908 0.4889661968 0.4326030910 + 0.4762536883 0.4900242984 0.4336324930 + 0.4772971869 0.4910764992 0.4346519113 + 0.4783403873 0.4921286106 0.4356713891 + 0.4793835878 0.4931806922 0.4366908967 + 0.4804266989 0.4942328036 0.4377104044 + 0.4814636111 0.4952807128 0.4387266040 + 0.4824956954 0.4963257015 0.4397411942 + 0.4835278988 0.4973706007 0.4407558143 + 0.4845600128 0.4984155893 0.4417704046 + 0.4855917990 0.4994598031 0.4427852035 + 0.4866203964 0.5004987717 0.4438005090 + 0.4876489937 0.5015376806 0.4448158145 + 0.4886775911 0.5025765896 0.4458310902 + 0.4897061884 0.5036156178 0.4468463957 + 0.4907324910 0.5046514869 0.4478605092 + 0.4917572141 0.5056853294 0.4488740861 + 0.4927819073 0.5067191720 0.4498876035 + 0.4938066006 0.5077530146 0.4509010911 + 0.4948312938 0.5087867975 0.4519143999 + 0.4958443046 0.5098149776 0.4529229105 + 0.4968571067 0.5108430982 0.4539315104 + 0.4978697896 0.5118712783 0.4549399912 + 0.4988825917 0.5128993988 0.4559485912 + 0.4998916090 0.5139253736 0.4569540024 + 0.5008922219 0.5149468184 0.4579545856 + 0.5018928051 0.5159683228 0.4589551985 + 0.5028933883 0.5169898272 0.4599558115 + 0.5038939714 0.5180112123 0.4609563947 + 0.5048918128 0.5190292001 0.4619503915 + 0.5058875084 0.5200446248 0.4629409909 + 0.5068832040 0.5210601091 0.4639315009 + 0.5078788996 0.5220755935 0.4649220109 + 0.5088744760 0.5230910182 0.4659124911 + 0.5098662972 0.5240967274 0.4668990970 + 0.5108568072 0.5250995159 0.4678849876 + 0.5118473172 0.5261024237 0.4688709080 + 0.5128378868 0.5271052122 0.4698567986 + 0.5138283968 0.5281080008 0.4708425999 + 0.5148209929 0.5291042924 0.4718292058 + 0.5158140063 0.5300996900 0.4728156924 + 0.5168069005 0.5310950875 0.4738022983 + 0.5177997947 0.5320904851 0.4747888148 + 0.5187928081 0.5330858827 0.4757753909 + 0.5197734237 0.5340675712 0.4767676890 + 0.5207530260 0.5350484252 0.4777599871 + 0.5217326283 0.5360292196 0.4787522852 + 0.5227121115 0.5370100141 0.4797446132 + 0.5236917138 0.5379908085 0.4807369113 + 0.5246657729 0.5389664173 0.4817185104 + 0.5256392956 0.5399416089 0.4826998115 + 0.5266126990 0.5409168005 0.4836811125 + 0.5275862217 0.5418918729 0.4846624136 + 0.5285596251 0.5428671241 0.4856437147 + 0.5295339823 0.5438383222 0.4866138101 + 0.5305086970 0.5448085070 0.4875822067 + 0.5314832926 0.5457786918 0.4885506034 + 0.5324578881 0.5467488766 0.4895190895 + 0.5334326029 0.5477191806 0.4904874861 + 0.5344074965 0.5486854911 0.4914506078 + 0.5353826284 0.5496494770 0.4924111962 + 0.5363577008 0.5506135225 0.4933719039 + 0.5373327732 0.5515775084 0.4943326116 + 0.5383079052 0.5525414944 0.4952932000 + 0.5392804146 0.5535004735 0.4962526858 + 0.5402482152 0.5544509292 0.4972105920 + 0.5412160158 0.5554013848 0.4981685877 + 0.5421838164 0.5563517809 0.4991264939 + 0.5431516171 0.5573021770 0.5000844002 + 0.5441191792 0.5582525134 0.5010424852 + 0.5450795889 0.5591980219 0.5020027161 + 0.5460399985 0.5601435900 0.5029628873 + 0.5470004082 0.5610890985 0.5039231181 + 0.5479608178 0.5620347261 0.5048832297 + 0.5489212275 0.5629801750 0.5058434010 + 0.5498812795 0.5639281273 0.5067989230 + 0.5508412719 0.5648773909 0.5077521801 + 0.5518013239 0.5658267140 0.5087053776 + 0.5527613759 0.5667759776 0.5096586943 + 0.5537214279 0.5677253008 0.5106120110 + 0.5546813011 0.5686742067 0.5115646720 + 0.5556406975 0.5696194172 0.5125145912 + 0.5566000938 0.5705646276 0.5134643912 + 0.5575594902 0.5715097785 0.5144143105 + 0.5585188866 0.5724549890 0.5153642297 + 0.5594782829 0.5734001994 0.5163140893 + 0.5604329705 0.5743389726 0.5172604918 + 0.5613837242 0.5752723813 0.5182046294 + 0.5623344183 0.5762056708 0.5191487074 + 0.5632849932 0.5771390796 0.5200927854 + 0.5642356873 0.5780724883 0.5210369825 + 0.5651863813 0.5790058970 0.5219811201 + 0.5661293268 0.5799363256 0.5229173899 + 0.5670710802 0.5808663964 0.5238531828 + 0.5680128932 0.5817964077 0.5247889757 + 0.5689548254 0.5827264190 0.5257248878 + 0.5698965788 0.5836563706 0.5266606808 + 0.5708373189 0.5845860243 0.5275958776 + 0.5717707872 0.5855131745 0.5285283923 + 0.5727043152 0.5864403844 0.5294609070 + 0.5736377239 0.5873675942 0.5303934813 + 0.5745711923 0.5882948041 0.5313259959 + 0.5755046010 0.5892218947 0.5322586298 + 0.5764377117 0.5901458859 0.5331916809 + 0.5773698092 0.5910627246 0.5341259241 + 0.5783019066 0.5919793844 0.5350601077 + 0.5792340040 0.5928962231 0.5359942913 + 0.5801661015 0.5938128829 0.5369284749 + 0.5810983181 0.5947296023 0.5378627181 + 0.5820268989 0.5956423879 0.5387967229 + 0.5829498768 0.5965489149 0.5397306085 + 0.5838727951 0.5974555016 0.5406643748 + 0.5847957134 0.5983620286 0.5415983200 + 0.5857185721 0.5992684960 0.5425322056 + 0.5866414905 0.6001750231 0.5434659719 + 0.5875610113 0.6010807753 0.5443956852 + 0.5884745121 0.6019853950 0.5453197956 + 0.5893880129 0.6028900146 0.5462440252 + 0.5903015137 0.6037945747 0.5471680760 + 0.5912150145 0.6046991944 0.5480923057 + 0.5921285152 0.6056038141 0.5490164757 + 0.5930395126 0.6065080166 0.5499367118 + 0.5939435959 0.6074110866 0.5508491993 + 0.5948476791 0.6083140969 0.5517616868 + 0.5957517028 0.6092172265 0.5526741743 + 0.5966557860 0.6101201773 0.5535867810 + 0.5975599289 0.6110233068 0.5544993281 + 0.5984631777 0.6119254827 0.5554108024 + 0.5993574858 0.6128194928 0.5563166142 + 0.6002519131 0.6137136221 0.5572223067 + 0.6011462808 0.6146075726 0.5581281185 + 0.6020405889 0.6155017018 0.5590338111 + 0.6029350162 0.6163957119 0.5599395037 + 0.6038293242 0.6172897816 0.5608453155 + 0.6047170162 0.6181768179 0.5617492795 + 0.6056030989 0.6190621853 0.5626531243 + 0.6064891219 0.6199476123 0.5635567904 + 0.6073752046 0.6208329797 0.5644605756 + 0.6082612276 0.6217185259 0.5653644204 + 0.6091471910 0.6226038933 0.5662680864 + 0.6100293994 0.6234887838 0.5671733022 + 0.6109064221 0.6243731976 0.5680798888 + 0.6117833257 0.6252574921 0.5689864159 + 0.6126602888 0.6261419058 0.5698930025 + 0.6135371923 0.6270262003 0.5707995892 + 0.6144142151 0.6279106140 0.5717061758 + 0.6152911782 0.6287949085 0.5726128221 + 0.6161587834 0.6296725273 0.5735203028 + 0.6170259714 0.6305497885 0.5744279027 + 0.6178932190 0.6314271092 0.5753353834 + 0.6187602878 0.6323043704 0.5762429237 + 0.6196274757 0.6331816912 0.5771505237 + 0.6204947233 0.6340590119 0.5780580044 + 0.6213582158 0.6349307895 0.5789619088 + 0.6222159863 0.6357945204 0.5798614025 + 0.6230738163 0.6366583109 0.5807610154 + 0.6239315867 0.6375219822 0.5816605091 + 0.6247894168 0.6383857131 0.5825600028 + 0.6256471872 0.6392493844 0.5834594965 + 0.6265048981 0.6401131153 0.5843591094 + 0.6273571253 0.6409724951 0.5852504969 + 0.6282070279 0.6418303847 0.5861397982 + 0.6290569901 0.6426882148 0.5870289803 + 0.6299068928 0.6435461044 0.5879182816 + 0.6307569146 0.6444038749 0.5888075829 + 0.6316068172 0.6452617049 0.5896968842 + 0.6324567795 0.6461195946 0.5905860066 + 0.6333056092 0.6469790936 0.5914697051 + 0.6341544986 0.6478387117 0.5923534036 + 0.6350033283 0.6486982107 0.5932369828 + 0.6358520985 0.6495578289 0.5941206813 + 0.6367009282 0.6504173279 0.5950043797 + 0.6375498176 0.6512768865 0.5958880782 + 0.6383976936 0.6521351933 0.5967717171 + 0.6392375231 0.6529834867 0.5976545215 + 0.6400774121 0.6538317800 0.5985373855 + 0.6409171820 0.6546800733 0.5994203091 + 0.6417570114 0.6555284262 0.6003031135 + 0.6425967813 0.6563767195 0.6011859775 + 0.6434366107 0.6572250128 0.6020689011 + 0.6442754269 0.6580716968 0.6029512286 + 0.6451067924 0.6589083076 0.6038312912 + 0.6459382772 0.6597449780 0.6047112942 + 0.6467698216 0.6605815887 0.6055914164 + 0.6476011872 0.6614183187 0.6064714193 + 0.6484326720 0.6622549295 0.6073514819 + 0.6492642164 0.6630915999 0.6082314849 + 0.6500955820 0.6639279723 0.6091110110 + 0.6509268880 0.6647610068 0.6099858880 + 0.6517581940 0.6655939817 0.6108608842 + 0.6525893807 0.6664270163 0.6117358208 + 0.6534206867 0.6672599912 0.6126108170 + 0.6542518735 0.6680930853 0.6134856939 + 0.6550831795 0.6689261198 0.6143606901 + 0.6559144855 0.6697590947 0.6152356267 + 0.6567456722 0.6705949903 0.6160972714 + 0.6575769782 0.6714311838 0.6169580817 + 0.6584082246 0.6722674966 0.6178188920 + 0.6592394710 0.6731036901 0.6186797023 + 0.6600707769 0.6739400029 0.6195405126 + 0.6609020233 0.6747761965 0.6204013228 + 0.6617333293 0.6756125093 0.6212620735 + 0.6625599861 0.6764392257 0.6221166849 + 0.6633837223 0.6772598028 0.6229680777 + 0.6642073989 0.6780803204 0.6238194704 + 0.6650310755 0.6789008975 0.6246708035 + 0.6658548117 0.6797214150 0.6255221963 + 0.6666784883 0.6805419922 0.6263735294 + 0.6675022244 0.6813625097 0.6272249222 + 0.6683244705 0.6821811795 0.6280750036 + 0.6691414714 0.6829928756 0.6289216280 + 0.6699584723 0.6838045716 0.6297681928 + 0.6707754731 0.6846163869 0.6306148171 + 0.6715924740 0.6854280829 0.6314613819 + 0.6724094748 0.6862397790 0.6323080063 + 0.6732264757 0.6870514750 0.6331545711 + 0.6740434766 0.6878631711 0.6340011954 + 0.6748613715 0.6886737943 0.6348477006 + 0.6756796837 0.6894840002 0.6356942058 + 0.6764978766 0.6902940869 0.6365405917 + 0.6773161888 0.6911042929 0.6373870969 + 0.6781343818 0.6919143796 0.6382334828 + 0.6789526939 0.6927245855 0.6390799880 + 0.6797710061 0.6935346723 0.6399263740 + 0.6805893183 0.6943448186 0.6407734752 + 0.6814084053 0.6951546073 0.6416242123 + 0.6822274923 0.6959642768 0.6424750090 + 0.6830466986 0.6967741251 0.6433258057 + 0.6838657856 0.6975837946 0.6441764832 + 0.6846848726 0.6983935833 0.6450272799 + 0.6855040193 0.6992033124 0.6458780169 + 0.6863232255 0.7000131011 0.6467288136 + 0.6871380806 0.7008184195 0.6475781798 + 0.6879450083 0.7016159892 0.6484256983 + 0.6887519956 0.7024134994 0.6492732167 + 0.6895589828 0.7032110095 0.6501206756 + 0.6903660297 0.7040085793 0.6509681940 + 0.6911730170 0.7048060894 0.6518155932 + 0.6919798851 0.7056037188 0.6526631117 + 0.6927868724 0.7064012289 0.6535105705 + 0.6935915947 0.7071955800 0.6543517113 + 0.6943939924 0.7079871893 0.6551877260 + 0.6951963902 0.7087786794 0.6560238004 + 0.6959987879 0.7095702291 0.6568598151 + 0.6968013048 0.7103617787 0.6576958895 + 0.6976037025 0.7111533284 0.6585319042 + 0.6984061003 0.7119448185 0.6593679786 + 0.6992084980 0.7127364278 0.6602039933 + 0.7000085711 0.7135254145 0.6610358953 + 0.7008067966 0.7143123150 0.6618649960 + 0.7016050220 0.7150992155 0.6626940966 + 0.7024031878 0.7158861160 0.6635231972 + 0.7032014132 0.7166730165 0.6643521786 + 0.7039995790 0.7174599767 0.6651812792 + 0.7047978044 0.7182468772 0.6660103798 + 0.7055960298 0.7190337777 0.6668394804 + 0.7063922286 0.7198162079 0.6676673889 + 0.7071862221 0.7205935717 0.6684942245 + 0.7079802155 0.7213708758 0.6693210006 + 0.7087742090 0.7221482992 0.6701477766 + 0.7095682025 0.7229256034 0.6709744930 + 0.7103621960 0.7237030268 0.6718013287 + 0.7111561894 0.7244802713 0.6726281047 + 0.7119501829 0.7252575755 0.6734548807 + 0.7127416134 0.7260324955 0.6742823720 + 0.7135254741 0.7268006802 0.6751112938 + 0.7143093944 0.7275689244 0.6759402752 + 0.7150933146 0.7283371091 0.6767693162 + 0.7158772945 0.7291052938 0.6775982976 + 0.7166612148 0.7298734784 0.6784272790 + 0.7174450755 0.7306417227 0.6792562008 + 0.7182289958 0.7314097881 0.6800851822 + 0.7190129757 0.7321779728 0.6809142232 + 0.7197942138 0.7329385281 0.6817373037 + 0.7205750942 0.7336984277 0.6825603843 + 0.7213560939 0.7344583869 0.6833835244 + 0.7221370935 0.7352182865 0.6842064857 + 0.7229180932 0.7359783053 0.6850296259 + 0.7236990929 0.7367382050 0.6858527064 + 0.7244800925 0.7374982238 0.6866757870 + 0.7252609730 0.7382581234 0.6874988079 + 0.7260380983 0.7390146852 0.6883146763 + 0.7268108726 0.7397677898 0.6891244054 + 0.7275837064 0.7405210137 0.6899340749 + 0.7283565998 0.7412741184 0.6907438040 + 0.7291293740 0.7420272231 0.6915534735 + 0.7299022079 0.7427803278 0.6923632026 + 0.7306751013 0.7435334921 0.6931728721 + 0.7314478755 0.7442865968 0.6939826012 + 0.7322207093 0.7450397015 0.6947922707 + 0.7329800129 0.7457880974 0.6955934167 + 0.7337378860 0.7465360761 0.6963940263 + 0.7344958186 0.7472841740 0.6971946955 + 0.7352536917 0.7480322123 0.6979954243 + 0.7360116243 0.7487801909 0.6987960935 + 0.7367694974 0.7495282292 0.6995967031 + 0.7375273705 0.7502762079 0.7003973722 + 0.7382853031 0.7510241866 0.7011981010 + 0.7390416265 0.7517700195 0.7019975781 + 0.7397916913 0.7525079250 0.7027937174 + 0.7405418754 0.7532457709 0.7035897970 + 0.7412921190 0.7539836168 0.7043858767 + 0.7420421839 0.7547215223 0.7051818967 + 0.7427924275 0.7554594278 0.7059779763 + 0.7435424924 0.7561972141 0.7067741156 + 0.7442926764 0.7569351196 0.7075701952 + 0.7450429201 0.7576729059 0.7083662748 + 0.7457928061 0.7584072948 0.7091622949 + 0.7465423942 0.7591359019 0.7099583149 + 0.7472919226 0.7598643899 0.7107542157 + 0.7480415106 0.7605929971 0.7115501761 + 0.7487909794 0.7613216043 0.7123461962 + 0.7495406270 0.7620502114 0.7131420970 + 0.7502900958 0.7627788186 0.7139381170 + 0.7510396838 0.7635074258 0.7147340178 + 0.7517892122 0.7642359138 0.7155299783 + 0.7525364161 0.7649617195 0.7163277864 + 0.7532799244 0.7656834722 0.7171276808 + 0.7540233731 0.7664054036 0.7179276943 + 0.7547670007 0.7671272159 0.7187277079 + 0.7555105090 0.7678490281 0.7195277214 + 0.7562540770 0.7685709000 0.7203276157 + 0.7569975853 0.7692927122 0.7211276293 + 0.7577412128 0.7700145245 0.7219275832 + 0.7584847212 0.7707362771 0.7227275968 + 0.7592245936 0.7714542747 0.7235243917 + 0.7599560022 0.7721639276 0.7243157029 + 0.7606874108 0.7728734016 0.7251068950 + 0.7614188790 0.7735829949 0.7258980870 + 0.7621502876 0.7742925286 0.7266892791 + 0.7628818154 0.7750021219 0.7274805903 + 0.7636132240 0.7757117152 0.7282717824 + 0.7643446922 0.7764211893 0.7290629745 + 0.7650761008 0.7771307826 0.7298542261 + 0.7658072114 0.7778394818 0.7306442261 + 0.7665333748 0.7785378098 0.7314252257 + 0.7672595978 0.7792360783 0.7322061062 + 0.7679858208 0.7799345255 0.7329869866 + 0.7687121034 0.7806327939 0.7337679267 + 0.7694383264 0.7813311815 0.7345489264 + 0.7701644897 0.7820295095 0.7353298068 + 0.7708907127 0.7827278972 0.7361106873 + 0.7716168761 0.7834262252 0.7368916869 + 0.7723432183 0.7841246128 0.7376726270 + 0.7730664015 0.7848206162 0.7384489775 + 0.7737882733 0.7855157256 0.7392237782 + 0.7745103240 0.7862107754 0.7399985790 + 0.7752321959 0.7869058847 0.7407733798 + 0.7759541869 0.7876011133 0.7415481806 + 0.7766761184 0.7882962227 0.7423229814 + 0.7773981094 0.7889912724 0.7430977821 + 0.7781199813 0.7896863818 0.7438725829 + 0.7788419724 0.7903814912 0.7446473837 + 0.7795621753 0.7910752892 0.7454218268 + 0.7802751064 0.7917634845 0.7461947799 + 0.7809879780 0.7924516201 0.7469677925 + 0.7817009091 0.7931398153 0.7477408051 + 0.7824137807 0.7938280106 0.7485138178 + 0.7831267118 0.7945162058 0.7492867708 + 0.7838395834 0.7952044010 0.7500599027 + 0.7845525146 0.7958925962 0.7508329153 + 0.7852653861 0.7965807915 0.7516059279 + 0.7859783173 0.7972689867 0.7523788810 + 0.7866867781 0.7979509234 0.7531532049 + 0.7873914838 0.7986276150 0.7539284229 + 0.7880961895 0.7993041873 0.7547037005 + 0.7888008952 0.7999808192 0.7554789782 + 0.7895056009 0.8006573915 0.7562541962 + 0.7902103066 0.8013340831 0.7570294738 + 0.7909150124 0.8020107150 0.7578048110 + 0.7916197181 0.8026872873 0.7585800290 + 0.7923244238 0.8033639193 0.7593553066 + 0.7930291295 0.8040404916 0.7601305246 + 0.7937325239 0.8047140241 0.7609025836 + 0.7944355011 0.8053864241 0.7616736889 + 0.7951384783 0.8060588241 0.7624449134 + 0.7958415151 0.8067312241 0.7632160187 + 0.7965444922 0.8074036241 0.7639871240 + 0.7972475290 0.8080759048 0.7647582889 + 0.7979503870 0.8087483048 0.7655293941 + 0.7986534238 0.8094207048 0.7663004994 + 0.7993564010 0.8100931048 0.7670717239 + 0.8000593781 0.8107655048 0.7678428292 + 0.8007569909 0.8114361167 0.7686048746 + 0.8014531732 0.8121061921 0.7693650126 + 0.8021494150 0.8127763867 0.7701252103 + 0.8028455973 0.8134465218 0.7708852887 + 0.8035417795 0.8141167164 0.7716454864 + 0.8042379022 0.8147869110 0.7724056244 + 0.8049340844 0.8154569864 0.7731658220 + 0.8056303263 0.8161271811 0.7739259005 + 0.8063265085 0.8167973161 0.7746859789 + 0.8070226908 0.8174675107 0.7754461765 + 0.8077152967 0.8181294799 0.7762011886 + 0.8084064126 0.8187881112 0.7769542933 + 0.8090974092 0.8194466233 0.7777075171 + 0.8097885251 0.8201050758 0.7784606218 + 0.8104795218 0.8207635880 0.7792137265 + 0.8111705780 0.8214221001 0.7799667716 + 0.8118615746 0.8220806718 0.7807198763 + 0.8125526905 0.8227391839 0.7814729810 + 0.8132436872 0.8233976960 0.7822262049 + 0.8139348030 0.8240562081 0.7829793096 + 0.8146215081 0.8247125745 0.7837309837 + 0.8153027892 0.8253661990 0.7844812274 + 0.8159840703 0.8260198832 0.7852314115 + 0.8166654706 0.8266735077 0.7859815955 + 0.8173468113 0.8273271918 0.7867317796 + 0.8180280924 0.8279808760 0.7874820232 + 0.8187093735 0.8286345005 0.7882322073 + 0.8193907142 0.8292881846 0.7889823914 + 0.8200719953 0.8299418092 0.7897325754 + 0.8207532763 0.8305954933 0.7904828191 + 0.8214341998 0.8312489986 0.7912325859 + 0.8221067190 0.8318998218 0.7919784188 + 0.8227791786 0.8325507045 0.7927241921 + 0.8234515786 0.8332015276 0.7934700251 + 0.8241240978 0.8338522911 0.7942157984 + 0.8247966170 0.8345031738 0.7949615717 + 0.8254690766 0.8351539969 0.7957074046 + 0.8261414766 0.8358048797 0.7964531779 + 0.8268139958 0.8364557028 0.7971988916 + 0.8274865150 0.8371065259 0.7979447246 + 0.8281589150 0.8377574086 0.7986904979 + 0.8288310170 0.8384053111 0.7994338274 + 0.8295025229 0.8390501738 0.8001747727 + 0.8301740885 0.8396950960 0.8009157777 + 0.8308455944 0.8403400183 0.8016567826 + 0.8315172195 0.8409848809 0.8023977876 + 0.8321887255 0.8416298032 0.8031387925 + 0.8328602910 0.8422747254 0.8038797975 + 0.8335317969 0.8429197073 0.8046206832 + 0.8342034221 0.8435646296 0.8053616881 + 0.8348749280 0.8442094922 0.8061026931 + 0.8355464935 0.8448544145 0.8068436980 + 0.8362092972 0.8454958797 0.8075804114 + 0.8368695974 0.8461365104 0.8083159924 + 0.8375298977 0.8467770815 0.8090516925 + 0.8381903172 0.8474177122 0.8097872734 + 0.8388506174 0.8480582237 0.8105229735 + 0.8395109177 0.8486987948 0.8112586141 + 0.8401712775 0.8493394256 0.8119943142 + 0.8408315778 0.8499799967 0.8127298951 + 0.8414918780 0.8506206274 0.8134655952 + 0.8421522975 0.8512610793 0.8142011762 + 0.8428125978 0.8519017100 0.8149368763 + 0.8434634209 0.8525397778 0.8156663179 + 0.8441131711 0.8531776071 0.8163954020 + 0.8447631001 0.8538153768 0.8171244860 + 0.8454129100 0.8544532061 0.8178536296 + 0.8460627794 0.8550910950 0.8185827136 + 0.8467125893 0.8557289243 0.8193116784 + 0.8473623991 0.8563666940 0.8200408220 + 0.8480123281 0.8570045233 0.8207699060 + 0.8486620784 0.8576422930 0.8214989901 + 0.8493120074 0.8582801223 0.8222280741 + 0.8499618173 0.8589180112 0.8229570985 + 0.8506091237 0.8595502973 0.8236836791 + 0.8512560725 0.8601819873 0.8244099021 + 0.8519030809 0.8608136177 0.8251361847 + 0.8525500894 0.8614451885 0.8258624077 + 0.8531970978 0.8620768189 0.8265886903 + 0.8538441062 0.8627085090 0.8273149133 + 0.8544911146 0.8633400798 0.8280411959 + 0.8551381230 0.8639717102 0.8287674189 + 0.8557850718 0.8646034002 0.8294937015 + 0.8564320803 0.8652349710 0.8302199244 + 0.8570790291 0.8658666015 0.8309462070 + 0.8577216268 0.8664917946 0.8316696286 + 0.8583620787 0.8671138883 0.8323917985 + 0.8590025902 0.8677359819 0.8331140280 + 0.8596431017 0.8683581948 0.8338361979 + 0.8602834940 0.8689802885 0.8345584869 + 0.8609240055 0.8696023822 0.8352807164 + 0.8615645170 0.8702244759 0.8360028863 + 0.8622049093 0.8708466291 0.8367251158 + 0.8628454208 0.8714687824 0.8374474049 + 0.8634858727 0.8720908761 0.8381695747 + 0.8641263843 0.8727130294 0.8388918042 + 0.8647633195 0.8733326197 0.8396124840 + 0.8653923869 0.8739467263 0.8403300047 + 0.8660213947 0.8745607734 0.8410475850 + 0.8666505218 0.8751748800 0.8417651057 + 0.8672795296 0.8757889867 0.8424826860 + 0.8679085970 0.8764030933 0.8432003260 + 0.8685376048 0.8770172000 0.8439177871 + 0.8691666722 0.8776313066 0.8446354270 + 0.8697956800 0.8782454133 0.8453528881 + 0.8704248071 0.8788595200 0.8460705280 + 0.8710538149 0.8794736266 0.8467881083 + 0.8716828823 0.8800876737 0.8475056291 + 0.8723050952 0.8806946278 0.8482186794 + 0.8729252815 0.8812993169 0.8489307165 + 0.8735455275 0.8819040060 0.8496426940 + 0.8741655946 0.8825086951 0.8503546715 + 0.8747857809 0.8831133842 0.8510667086 + 0.8754060268 0.8837180734 0.8517786860 + 0.8760262132 0.8843228817 0.8524907231 + 0.8766463995 0.8849275708 0.8532027006 + 0.8772665262 0.8855323195 0.8539146781 + 0.8778867126 0.8861370087 0.8546267152 + 0.8785068989 0.8867416978 0.8553386927 + 0.8791267276 0.8873459101 0.8560500741 + 0.8797398210 0.8879420161 0.8567556143 + 0.8803529739 0.8885381818 0.8574609756 + 0.8809661865 0.8891344070 0.8581665158 + 0.8815793991 0.8897305727 0.8588718772 + 0.8821926117 0.8903266788 0.8595774174 + 0.8828057051 0.8909229040 0.8602827787 + 0.8834189177 0.8915191293 0.8609883189 + 0.8840320706 0.8921152949 0.8616936803 + 0.8846452832 0.8927114010 0.8623992205 + 0.8852584958 0.8933076262 0.8631045818 + 0.8858715892 0.8939037919 0.8638101220 + 0.8864827156 0.8944979906 0.8645150065 + 0.8870841861 0.8950834274 0.8652176857 + 0.8876857162 0.8956688046 0.8659204841 + 0.8882871866 0.8962541223 0.8666232228 + 0.8888887167 0.8968394995 0.8673259020 + 0.8894901872 0.8974248767 0.8680287004 + 0.8900917768 0.8980103135 0.8687313795 + 0.8906933069 0.8985956907 0.8694341779 + 0.8912947774 0.8991811275 0.8701369166 + 0.8918963075 0.8997665048 0.8708395958 + 0.8924977779 0.9003518224 0.8715423942 + 0.8930993080 0.9009371996 0.8722450733 + 0.8936997056 0.9015216231 0.8729472756 + 0.8942915797 0.9020984173 0.8736460209 + 0.8948835135 0.9026752114 0.8743448257 + 0.8954755068 0.9032518864 0.8750435114 + 0.8960673809 0.9038286805 0.8757423162 + 0.8966593742 0.9044054747 0.8764410019 + 0.8972513080 0.9049823284 0.8771396875 + 0.8978431821 0.9055591226 0.8778384924 + 0.8984351754 0.9061359167 0.8785371780 + 0.8990271091 0.9067127109 0.8792359829 + 0.8996191025 0.9072895050 0.8799347281 + 0.9002109766 0.9078662992 0.8806334138 + 0.9008030295 0.9084430933 0.8813322186 + 0.9013879895 0.9090055823 0.8820266724 + 0.9019721150 0.9095662236 0.8827208281 + 0.9025561810 0.9101268053 0.8834148049 + 0.9031403065 0.9106875062 0.8841089010 + 0.9037243724 0.9112480879 0.8848029971 + 0.9043084979 0.9118087292 0.8854969740 + 0.9048926234 0.9123693109 0.8861911297 + 0.9054766893 0.9129298925 0.8868852258 + 0.9060608149 0.9134904742 0.8875792027 + 0.9066448808 0.9140511155 0.8882732987 + 0.9072290063 0.9146116972 0.8889672756 + 0.9078130722 0.9151722789 0.8896613717 + 0.9083930254 0.9157286882 0.8903554082 + 0.9089676142 0.9162800908 0.8910493851 + 0.9095423222 0.9168314934 0.8917434216 + 0.9101169705 0.9173828959 0.8924373984 + 0.9106916189 0.9179342985 0.8931313753 + 0.9112663269 0.9184857011 0.8938252926 + 0.9118409753 0.9190371037 0.8945193291 + 0.9124156237 0.9195885062 0.8952133060 + 0.9129902720 0.9201399088 0.8959072828 + 0.9135649800 0.9206913114 0.8966013193 + 0.9141396880 0.9212427139 0.8972951770 + 0.9147142768 0.9217941165 0.8979892135 + 0.9152889848 0.9223455191 0.8986831903 + 0.9158567786 0.9228839278 0.8993654251 + 0.9164233804 0.9234200716 0.9000456929 + 0.9169899821 0.9239562154 0.9007260203 + 0.9175565839 0.9244924188 0.9014064074 + 0.9181231260 0.9250285029 0.9020866752 + 0.9186897278 0.9255647063 0.9027670026 + 0.9192563295 0.9261007905 0.9034472704 + 0.9198228717 0.9266369939 0.9041275978 + 0.9203894734 0.9271730781 0.9048079848 + 0.9209560156 0.9277092814 0.9054883122 + 0.9215226173 0.9282454252 0.9061685801 + 0.9220892191 0.9287816286 0.9068489075 + 0.9226552248 0.9293168783 0.9075281024 + 0.9232082963 0.9298372269 0.9081909060 + 0.9237614274 0.9303573966 0.9088537097 + 0.9243146181 0.9308776855 0.9095163941 + 0.9248676896 0.9313979745 0.9101791978 + 0.9254208207 0.9319182038 0.9108420014 + 0.9259738922 0.9324384928 0.9115046859 + 0.9265270829 0.9329587817 0.9121674895 + 0.9270802140 0.9334790111 0.9128302932 + 0.9276332855 0.9339993000 0.9134930968 + 0.9281864762 0.9345195889 0.9141557813 + 0.9287396073 0.9350398779 0.9148185849 + 0.9292926788 0.9355601072 0.9154813886 + 0.9298455119 0.9360799193 0.9161437750 + 0.9303920269 0.9365919232 0.9168015718 + 0.9309384823 0.9371039271 0.9174593091 + 0.9314851165 0.9376158118 0.9181171060 + 0.9320315719 0.9381278157 0.9187749028 + 0.9325782061 0.9386398196 0.9194325805 + 0.9331247211 0.9391518235 0.9200903773 + 0.9336711764 0.9396638274 0.9207481146 + 0.9342178106 0.9401757717 0.9214059114 + 0.9347643256 0.9406877756 0.9220637083 + 0.9353107810 0.9411997199 0.9227213860 + 0.9358574152 0.9417117238 0.9233791828 + 0.9364038706 0.9422237277 0.9240369797 + 0.9369503856 0.9427356720 0.9246947169 + 0.9374883771 0.9432380795 0.9253485203 + 0.9380251169 0.9437391758 0.9260017276 + 0.9385617971 0.9442402720 0.9266549945 + 0.9390985966 0.9447414279 0.9273082018 + 0.9396352768 0.9452425241 0.9279615283 + 0.9401720166 0.9457436204 0.9286146760 + 0.9407088161 0.9462447166 0.9292680025 + 0.9412454963 0.9467458129 0.9299212098 + 0.9417821765 0.9472469091 0.9305744767 + 0.9423189163 0.9477481246 0.9312276840 + 0.9428557158 0.9482492208 0.9318810105 + 0.9433923960 0.9487503171 0.9325342178 + 0.9439290762 0.9492514133 0.9331874847 + 0.9444622993 0.9497454166 0.9338380098 + 0.9449920058 0.9502323866 0.9344856739 + 0.9455215931 0.9507192969 0.9351335168 + 0.9460512996 0.9512062073 0.9357813001 + 0.9465808868 0.9516931772 0.9364290833 + 0.9471105933 0.9521800876 0.9370768070 + 0.9476401806 0.9526671171 0.9377245903 + 0.9481698275 0.9531540275 0.9383723736 + 0.9486994743 0.9536408782 0.9390202165 + 0.9492291212 0.9541279078 0.9396679997 + 0.9497588277 0.9546148181 0.9403157234 + 0.9502884150 0.9551017880 0.9409635067 + 0.9508181214 0.9555886984 0.9416112900 + 0.9513477087 0.9560756087 0.9422590733 + 0.9518715739 0.9565510750 0.9428948164 + 0.9523950815 0.9570255280 0.9435294867 + 0.9529185295 0.9574998021 0.9441642165 + 0.9534419179 0.9579741955 0.9447988868 + 0.9539653063 0.9584485888 0.9454336166 + 0.9544886947 0.9589229822 0.9460684061 + 0.9550120831 0.9593973756 0.9467030764 + 0.9555354714 0.9598717093 0.9473378062 + 0.9560589194 0.9603461027 0.9479724765 + 0.9565823078 0.9608204961 0.9486072063 + 0.9571056962 0.9612948895 0.9492419958 + 0.9576292038 0.9617692828 0.9498767257 + 0.9581525922 0.9622436166 0.9505113959 + 0.9586743712 0.9627162218 0.9511442780 + 0.9591860175 0.9631766081 0.9517663717 + 0.9596974850 0.9636369944 0.9523884058 + 0.9602090716 0.9640974998 0.9530103803 + 0.9607207179 0.9645578861 0.9536324739 + 0.9612321854 0.9650183916 0.9542545080 + 0.9617437720 0.9654787779 0.9548764825 + 0.9622554183 0.9659392238 0.9554985762 + 0.9627668858 0.9663997293 0.9561206102 + 0.9632784724 0.9668601155 0.9567425847 + 0.9637901187 0.9673205018 0.9573646784 + 0.9643015862 0.9677810073 0.9579867125 + 0.9648131728 0.9682413936 0.9586086869 + 0.9653248191 0.9687018991 0.9592307210 + 0.9658346176 0.9691603184 0.9598506093 + 0.9663344026 0.9696068764 0.9604582787 + 0.9668341279 0.9700533748 0.9610658884 + 0.9673337936 0.9704999924 0.9616736174 + 0.9678335786 0.9709466100 0.9622812271 + 0.9683333039 0.9713931084 0.9628888965 + 0.9688330293 0.9718397260 0.9634965062 + 0.9693328142 0.9722862840 0.9641041756 + 0.9698324800 0.9727329016 0.9647119045 + 0.9703322053 0.9731794000 0.9653195143 + 0.9708318710 0.9736260176 0.9659271836 + 0.9713317156 0.9740725756 0.9665347934 + 0.9718313813 0.9745190740 0.9671425223 + 0.9723311067 0.9749656916 0.9677500725 + 0.9728308916 0.9754123092 0.9683578014 + 0.9733167291 0.9758465290 0.9689434767 + 0.9738017917 0.9762800932 0.9695280790 + 0.9742867947 0.9767135978 0.9701126814 + 0.9747719169 0.9771471024 0.9706972241 + 0.9752569795 0.9775807261 0.9712818265 + 0.9757419825 0.9780141711 0.9718664289 + 0.9762271047 0.9784476757 0.9724509716 + 0.9767121077 0.9788812995 0.9730355740 + 0.9771971703 0.9793148041 0.9736201763 + 0.9776821733 0.9797484279 0.9742047787 + 0.9781672955 0.9801818728 0.9747893214 + 0.9786522985 0.9806153774 0.9753739238 + 0.9791374207 0.9810490012 0.9759585261 + 0.9796224236 0.9814825058 0.9765431285 + 0.9801008105 0.9819086790 0.9771106243 + 0.9805731773 0.9823285937 0.9776636958 + 0.9810456038 0.9827483892 0.9782167077 + 0.9815180898 0.9831681848 0.9787697196 + 0.9819905162 0.9835880995 0.9793226719 + 0.9824628830 0.9840078950 0.9798758030 + 0.9829354286 0.9844278097 0.9804288149 + 0.9834077954 0.9848476052 0.9809818268 + 0.9838802814 0.9852675200 0.9815347791 + 0.9843527079 0.9856873155 0.9820877910 + 0.9848250747 0.9861071706 0.9826409221 + 0.9852976203 0.9865270257 0.9831938744 + 0.9857699871 0.9869468212 0.9837468863 + 0.9862424731 0.9873666763 0.9842998981 + 0.9867148995 0.9877864718 0.9848530293 + 0.9871708751 0.9882004857 0.9853777289 + 0.9876251817 0.9886139035 0.9858996868 + 0.9880794883 0.9890273213 0.9864217043 + 0.9885339141 0.9894406796 0.9869437218 + 0.9889882207 0.9898540974 0.9874656796 + 0.9894425273 0.9902675152 0.9879876971 + 0.9898968935 0.9906808734 0.9885097146 + 0.9903512001 0.9910942912 0.9890317917 + 0.9908055067 0.9915077090 0.9895538092 + 0.9912598729 0.9919211268 0.9900758266 + 0.9917141795 0.9923344851 0.9905977845 + 0.9921684861 0.9927479029 0.9911198020 + 0.9926229119 0.9931613207 0.9916418195 + 0.9930772185 0.9935746789 0.9921637774 + 0.9935299754 0.9939873219 0.9926834702 + 0.9939612746 0.9943881035 0.9931712747 + 0.9943926930 0.9947890043 0.9936590791 + 0.9948239923 0.9951897860 0.9941468239 + 0.9952552915 0.9955906868 0.9946346283 + 0.9956867099 0.9959915280 0.9951223731 + 0.9961180091 0.9963924289 0.9956101179 + 0.9965493083 0.9967932105 0.9960979223 + 0.9969807267 0.9971941113 0.9965857267 + 0.9974120259 0.9975948930 0.9970734119 + 0.9978433251 0.9979957938 0.9975612164 + 0.9982746840 0.9983965755 0.9980489016 + 0.9987059832 0.9987974763 0.9985367060 + 0.9991372824 0.9991983175 0.9990245104 + 0.9995687008 0.9995992184 0.9995121956 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/fujifilm_fp2900z.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/fujifilm_fp2900z.spi1d index 4d2caa3c..44622fa4 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/fujifilm_fp2900z.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/fujifilm_fp2900z.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0008503073 0.0060487022 0.0064002438 - 0.0031247020 0.0077070072 0.0084807090 - 0.0065889051 0.0093767531 0.0092614815 - 0.0087466193 0.0171994604 0.0101535600 - 0.0108801499 0.0271614902 0.0109591102 - 0.0145162800 0.0324796699 0.0117238201 - 0.0193788800 0.0365465991 0.0124966996 - 0.0240556896 0.0401393399 0.0133267604 - 0.0285850298 0.0431072004 0.0142630199 - 0.0330653489 0.0448529907 0.0153544797 - 0.0372812003 0.0457478799 0.0166501496 - 0.0410104282 0.0465190299 0.0183483995 - 0.0442945287 0.0478400812 0.0208099205 - 0.0474888906 0.0510223396 0.0238338206 - 0.0509595312 0.0559057482 0.0271846093 - 0.0551288798 0.0616184585 0.0307629295 - 0.0602344498 0.0672886521 0.0349612683 - 0.0657841787 0.0724673122 0.0396671183 - 0.0712287277 0.0778958723 0.0446968898 - 0.0760187581 0.0834463537 0.0498669893 - 0.0797053277 0.0889106467 0.0549938083 - 0.0827539414 0.0940806195 0.0598937795 - 0.0854174793 0.0987935290 0.0646180511 - 0.0878179371 0.1031830981 0.0695652813 - 0.0900772810 0.1073582023 0.0744954869 - 0.0923174620 0.1114075035 0.0791333169 - 0.0946604684 0.1154199019 0.0832034126 - 0.0972282588 0.1194840968 0.0864467174 - 0.1001427993 0.1236867979 0.0890749767 - 0.1034547016 0.1279577017 0.0913295001 - 0.1070737988 0.1322295070 0.0933255404 - 0.1109554023 0.1365102977 0.0951783583 - 0.1150562018 0.1408081055 0.0970032215 - 0.1193327978 0.1451306939 0.0989153683 - 0.1237417012 0.1494861990 0.1010300964 - 0.1282396019 0.1538825035 0.1034625992 - 0.1330090016 0.1583275944 0.1061728969 - 0.1381776035 0.1628293991 0.1090466008 - 0.1436399072 0.1673959941 0.1120757982 - 0.1492906064 0.1720408946 0.1152525023 - 0.1550242007 0.1768026054 0.1185685992 - 0.1607352942 0.1816647947 0.1220161989 - 0.1663185954 0.1866011024 0.1255874038 - 0.1716686040 0.1915850043 0.1292741001 - 0.1766799986 0.1965896934 0.1330682933 - 0.1812472045 0.2015890032 0.1369619966 - 0.1853772998 0.2065560967 0.1409474015 - 0.1893154979 0.2114645988 0.1450162977 - 0.1930917948 0.2164134979 0.1491608024 - 0.1967224926 0.2215435952 0.1533937007 - 0.2002238035 0.2267996967 0.1579982042 - 0.2036122978 0.2321251035 0.1629922986 - 0.2069043070 0.2374628037 0.1682806015 - 0.2101161033 0.2427562028 0.1737675965 - 0.2132640928 0.2479483038 0.1793580055 - 0.2163646966 0.2529824078 0.1849561036 - 0.2194342017 0.2578017116 0.1904667020 - 0.2224891037 0.2623493075 0.1957941055 - 0.2255457044 0.2665685117 0.2008430064 - 0.2286203057 0.2704024017 0.2055179030 - 0.2317294031 0.2737948895 0.2097503990 - 0.2348892987 0.2768653929 0.2138119936 - 0.2381162941 0.2797451913 0.2177951038 - 0.2414056957 0.2824516892 0.2216964066 - 0.2446805984 0.2850024104 0.2255128026 - 0.2479368001 0.2874149084 0.2292409986 - 0.2511793077 0.2897064984 0.2328778058 - 0.2544130981 0.2918949127 0.2364200950 - 0.2576431930 0.2939974070 0.2398646027 - 0.2608745992 0.2960315943 0.2432080954 - 0.2641122043 0.2980149090 0.2464473993 - 0.2673611939 0.2999647856 0.2495792955 - 0.2706263959 0.3018988967 0.2526006103 - 0.2739129066 0.3038344979 0.2555082142 - 0.2772257030 0.3057892025 0.2582986951 - 0.2805697918 0.3077805042 0.2609690130 - 0.2839502096 0.3098258972 0.2635160089 - 0.2874231935 0.3119427860 0.2659429908 - 0.2910543084 0.3141488135 0.2682636976 - 0.2948203087 0.3164612055 0.2704857886 - 0.2986972034 0.3188976943 0.2726168931 - 0.3026610911 0.3214755952 0.2746646106 - 0.3066878021 0.3242133856 0.2766366005 - 0.3107534945 0.3271133900 0.2785406113 - 0.3148340881 0.3301571906 0.2803840935 - 0.3189055920 0.3333252966 0.2821748853 - 0.3229441047 0.3365986049 0.2839204967 - 0.3269253969 0.3399577141 0.2856287062 - 0.3308256865 0.3433833122 0.2873069942 - 0.3346208036 0.3468560874 0.2889631093 - 0.3382869065 0.3503566980 0.2906045914 - 0.3417999148 0.3538660109 0.2922393084 - 0.3451356888 0.3573645949 0.2938747108 - 0.3482705057 0.3608331978 0.2955183983 - 0.3511801958 0.3642525077 0.2971782088 - 0.3538407981 0.3676030934 0.2988617122 - 0.3562282920 0.3708657920 0.3005765080 - 0.3583298028 0.3740212917 0.3023301959 - 0.3602797091 0.3770503104 0.3041304946 - 0.3621290028 0.3799622953 0.3059850931 - 0.3638837039 0.3828690052 0.3079015911 - 0.3655497134 0.3857800961 0.3098875880 - 0.3671329916 0.3886910081 0.3119508028 - 0.3686397970 0.3915975094 0.3140988052 - 0.3700759113 0.3944953084 0.3163442016 - 0.3714475036 0.3973797858 0.3187192976 - 0.3727603853 0.4002467990 0.3212150037 - 0.3740208149 0.4030919075 0.3238149881 - 0.3752345145 0.4059107900 0.3265028894 - 0.3764077127 0.4086990058 0.3292626143 - 0.3775463104 0.4114522040 0.3320775926 - 0.3786563873 0.4141660929 0.3349316120 - 0.3797439933 0.4168362021 0.3378083110 - 0.3808149099 0.4194582105 0.3406915069 - 0.3818753958 0.4220277965 0.3435646892 - 0.3829312921 0.4245404899 0.3464117050 - 0.3839887977 0.4269919991 0.3492161930 - 0.3850536942 0.4293780029 0.3519617915 - 0.3861320913 0.4316940010 0.3546322882 - 0.3872300982 0.4339357018 0.3572112024 - 0.3883534968 0.4360988140 0.3596824110 - 0.3895084858 0.4381788075 0.3620485067 - 0.3907010853 0.4401713908 0.3643788993 - 0.3919371068 0.4420722127 0.3666825891 - 0.3932228088 0.4438770115 0.3689602911 - 0.3945640028 0.4456013143 0.3712129891 - 0.3959667087 0.4472714067 0.3734416068 - 0.3974370956 0.4488897026 0.3756470084 - 0.3989810050 0.4504584968 0.3778300881 - 0.4006046057 0.4519804120 0.3799915910 - 0.4023137093 0.4534575939 0.3821327090 - 0.4041146040 0.4548926055 0.3842540085 - 0.4060753882 0.4562878013 0.3863565922 - 0.4082339108 0.4576455057 0.3884412050 - 0.4105701149 0.4589681923 0.3905088902 - 0.4130640924 0.4602581859 0.3925603926 - 0.4156959951 0.4615179896 0.3945967853 - 0.4184457064 0.4627498090 0.3966186941 - 0.4212931991 0.4639562070 0.3986273110 - 0.4242187142 0.4651395082 0.4006232023 - 0.4272021055 0.4663020968 0.4026075006 - 0.4302234948 0.4674463868 0.4045810997 - 0.4332629144 0.4685747921 0.4065446854 - 0.4363003075 0.4696896076 0.4084993005 - 0.4393157065 0.4707933962 0.4104458094 - 0.4422892928 0.4718883038 0.4123851061 - 0.4452010095 0.4729770124 0.4143181145 - 0.4480308890 0.4740616977 0.4162456095 - 0.4507589042 0.4751448035 0.4181686044 - 0.4533652067 0.4762287140 0.4200878143 - 0.4558297098 0.4773159027 0.4220044017 - 0.4581323862 0.4784086943 0.4239189923 - 0.4602535069 0.4795095026 0.4258325994 - 0.4622519016 0.4806207120 0.4277462065 - 0.4642114937 0.4817445874 0.4296604991 - 0.4661338031 0.4828838110 0.4315764010 - 0.4680199921 0.4840404987 0.4334949851 - 0.4698711932 0.4852172136 0.4354169965 - 0.4716886878 0.4864163101 0.4373432994 - 0.4734737873 0.4876401126 0.4392806888 - 0.4752275944 0.4888910949 0.4412496090 - 0.4769513011 0.4901716113 0.4432489872 - 0.4786463082 0.4914839864 0.4452759027 - 0.4803135991 0.4928306937 0.4473274052 - 0.4819546044 0.4942142069 0.4494006038 - 0.4835703969 0.4956367016 0.4514926076 - 0.4851622880 0.4971008003 0.4536004961 - 0.4867314994 0.4986090064 0.4557211995 - 0.4882791936 0.5001742244 0.4578520060 - 0.4898067117 0.5017988086 0.4599897861 - 0.4913150966 0.5034782290 0.4621317089 - 0.4928056002 0.5052075982 0.4642749131 - 0.4942795932 0.5069822073 0.4664162993 - 0.4957382083 0.5087974072 0.4685530961 - 0.4971826971 0.5106484294 0.4706822038 - 0.4986141920 0.5125305057 0.4728009105 - 0.5000339746 0.5144389868 0.4749062061 - 0.5014433265 0.5163691044 0.4769951105 - 0.5028433204 0.5183159709 0.4790647030 - 0.5042353272 0.5202751756 0.4811120927 - 0.5056204796 0.5222417116 0.4831343889 - 0.5070000291 0.5242109895 0.4851284921 - 0.5083752275 0.5261781812 0.4870916903 - 0.5097472072 0.5281386971 0.4890210032 - 0.5111172199 0.5300877094 0.4909133911 - 0.5124865770 0.5320205092 0.4927659929 - 0.5138564110 0.5339323282 0.4945758879 - 0.5152279735 0.5358185172 0.4963401854 - 0.5166025162 0.5376741886 0.4980559051 - 0.5179811716 0.5394948125 0.4997201860 - 0.5193653107 0.5412756205 0.5013300180 - 0.5207560062 0.5430117249 0.5028824806 - 0.5221545100 0.5446984768 0.5043746829 - 0.5235620141 0.5463312864 0.5058037043 - 0.5249798894 0.5479233265 0.5071665049 - 0.5264092088 0.5495030880 0.5084602833 - 0.5278512836 0.5510712266 0.5096821785 - 0.5293071866 0.5526280999 0.5108290911 - 0.5307784081 0.5541737080 0.5118982196 - 0.5322659016 0.5557085276 0.5128865242 - 0.5337721109 0.5572324991 0.5137987733 - 0.5353026986 0.5587460995 0.5146775842 - 0.5368571877 0.5602493286 0.5155304074 - 0.5384340286 0.5617424846 0.5163580179 - 0.5400314927 0.5632258058 0.5171611905 - 0.5416480899 0.5646995902 0.5179405808 - 0.5432822704 0.5661638975 0.5186970830 - 0.5449323058 0.5676190257 0.5194314122 - 0.5465967059 0.5690650940 0.5201442242 - 0.5482738018 0.5705024004 0.5208364129 - 0.5499621034 0.5719311833 0.5215085745 - 0.5516598821 0.5733516812 0.5221616030 - 0.5533655882 0.5747640133 0.5227962136 - 0.5550776720 0.5761684775 0.5234131217 - 0.5567945242 0.5775651932 0.5240131021 - 0.5585144758 0.5789545178 0.5245969892 - 0.5602360964 0.5803365111 0.5251653790 - 0.5619575977 0.5817114711 0.5257192254 - 0.5636774898 0.5830796957 0.5262590051 - 0.5653942227 0.5844413042 0.5267856717 - 0.5671060085 0.5857964754 0.5273000002 - 0.5688114166 0.5871455073 0.5278027058 - 0.5705088973 0.5884885788 0.5282945037 - 0.5721967220 0.5898259282 0.5287762284 - 0.5738732815 0.5911576748 0.5292484164 - 0.5755370855 0.5924841762 0.5297120810 - 0.5771865249 0.5938054919 0.5301678181 - 0.5788199902 0.5951220989 0.5306165218 - 0.5804358125 0.5964338779 0.5310587883 - 0.5820325017 0.5977414250 0.5314955115 - 0.5836083889 0.5990446210 0.5319272876 - 0.5851619244 0.6003437042 0.5323550105 - 0.5866914988 0.6016390920 0.5327793956 - 0.5881955028 0.6029310226 0.5332012177 - 0.5896723866 0.6042193770 0.5336210728 - 0.5911204815 0.6055046916 0.5340399146 - 0.5925382972 0.6067870855 0.5344585180 - 0.5939241052 0.6080667973 0.5348774195 - 0.5952764153 0.6093440056 0.5352975130 - 0.5965934992 0.6106188893 0.5357195735 - 0.5978739262 0.6118916869 0.5361443162 - 0.5991160274 0.6131626964 0.5365725160 - 0.6003181934 0.6144319773 0.5370048285 - 0.6014788747 0.6157000065 0.5374420881 - 0.6025965214 0.6169667244 0.5378851295 - 0.6036692858 0.6182324886 0.5383346081 - 0.6046959162 0.6194974780 0.5387912989 - 0.6056745052 0.6207618713 0.5392559171 - 0.6066036820 0.6220260262 0.5397291780 - 0.6074817777 0.6232900023 0.5402120948 - 0.6083071828 0.6245540977 0.5407050848 - 0.6090785265 0.6258184910 0.5412091017 - 0.6098169088 0.6270834804 0.5417249203 - 0.6105371714 0.6283515096 0.5422530770 - 0.6112400889 0.6296306252 0.5427945852 - 0.6119259000 0.6309205890 0.5433501005 - 0.6125950217 0.6322209239 0.5439202785 - 0.6132478714 0.6335307956 0.5445060134 - 0.6138849854 0.6348494887 0.5451080203 - 0.6145066023 0.6361764073 0.5457270145 - 0.6151131988 0.6375107765 0.5463637114 - 0.6157053113 0.6388520002 0.5470190048 - 0.6162831783 0.6401991844 0.5476934910 - 0.6168472767 0.6415519118 0.5483881235 - 0.6173980832 0.6429092288 0.5491034985 - 0.6179360747 0.6442704797 0.5498402715 - 0.6184614897 0.6456350088 0.5505995154 - 0.6189749241 0.6470022202 0.5513817072 - 0.6194766164 0.6483712792 0.5521876812 - 0.6199671030 0.6497414708 0.5530344248 - 0.6204468012 0.6511121988 0.5539386868 - 0.6209161282 0.6524828076 0.5548986197 - 0.6213753819 0.6538524032 0.5559120178 - 0.6218250990 0.6552203894 0.5569766164 - 0.6222656965 0.6565861106 0.5580903888 - 0.6226975918 0.6579487920 0.5592513084 - 0.6231212020 0.6593078971 0.5604572296 - 0.6235368848 0.6606624722 0.5617058873 - 0.6239451170 0.6620119810 0.5629951954 - 0.6243463159 0.6633557081 0.5643231869 - 0.6247407794 0.6646929979 0.5656875968 - 0.6251292229 0.6660230160 0.5670863986 - 0.6255117059 0.6673452258 0.5685173273 - 0.6258888841 0.6686587930 0.5699784160 - 0.6262611151 0.6699631214 0.5714673996 - 0.6266288161 0.6712573767 0.5729821920 - 0.6269922853 0.6725410223 0.5745208263 - 0.6273521781 0.6738132238 0.5760809779 - 0.6277086735 0.6750733852 0.5776606202 - 0.6280624270 0.6763207912 0.5792576075 - 0.6284136772 0.6775547266 0.5808697939 - 0.6287629008 0.6787744164 0.5824952126 - 0.6291105151 0.6799793243 0.5841314793 - 0.6294568777 0.6811686158 0.5857766271 - 0.6298025250 0.6823415756 0.5874286294 - 0.6301478148 0.6834977269 0.5890851021 - 0.6304932237 0.6846361160 0.5907440782 - 0.6308389902 0.6857560873 0.5924034715 - 0.6311857104 0.6868569851 0.5940610766 - 0.6315336823 0.6879382133 0.5957149267 - 0.6318835020 0.6889989972 0.5973626971 - 0.6322354078 0.6900386214 0.5990023017 - 0.6325898767 0.6910563111 0.6006317139 - 0.6329473853 0.6920514703 0.6022487283 - 0.6333082914 0.6930233836 0.6038513184 - 0.6336730123 0.6939712763 0.6054372191 - 0.6340420246 0.6948946714 0.6070044041 - 0.6344156265 0.6957926154 0.6085507870 - 0.6347942948 0.6966645122 0.6100741029 - 0.6351785064 0.6975097060 0.6115723848 - 0.6355686188 0.6983274817 0.6130434275 - 0.6359649897 0.6991170049 0.6144850850 - 0.6363682151 0.6998777986 0.6158952713 - 0.6367785931 0.7006090283 0.6172720194 - 0.6371966004 0.7013099790 0.6186128855 - 0.6376225948 0.7019799948 0.6199159026 - 0.6380569935 0.7026184201 0.6211791039 - 0.6385002136 0.7032272816 0.6224001050 - 0.6389526725 0.7038217783 0.6235768795 - 0.6394149065 0.7044044733 0.6247074008 - 0.6398872137 0.7049757838 0.6257894039 - 0.6403700113 0.7055358291 0.6268209219 - 0.6408637762 0.7060847282 0.6277996898 - 0.6413689256 0.7066227794 0.6287239790 - 0.6418856978 0.7071502209 0.6296194792 - 0.6424148083 0.7076671720 0.6305025220 - 0.6429564953 0.7081741095 0.6313732862 - 0.6435111761 0.7086709738 0.6322320104 - 0.6440793276 0.7091581821 0.6330789924 - 0.6446613073 0.7096359134 0.6339144111 - 0.6452575922 0.7101042867 0.6347385049 - 0.6458685994 0.7105637193 0.6355515718 - 0.6464946866 0.7110142112 0.6363537908 - 0.6471362710 0.7114561200 0.6371454000 - 0.6477938890 0.7118896246 0.6379265785 - 0.6484678984 0.7123149037 0.6386976838 - 0.6491587162 0.7127323151 0.6394590139 - 0.6498693228 0.7131419778 0.6402105093 - 0.6506212950 0.7135441899 0.6409527063 - 0.6514183879 0.7139390707 0.6416856050 - 0.6522591114 0.7143269181 0.6424096227 - 0.6531423926 0.7147079110 0.6431248784 - 0.6540668011 0.7150822878 0.6438316703 - 0.6550312042 0.7154504061 0.6445302963 - 0.6560341716 0.7158122063 0.6452208161 - 0.6570745707 0.7161681056 0.6459035873 - 0.6581510901 0.7165182829 0.6465789080 - 0.6592624187 0.7168630958 0.6472467780 - 0.6604073048 0.7172024846 0.6479076743 - 0.6615846157 0.7175369263 0.6485617161 - 0.6627929211 0.7178664804 0.6492090821 - 0.6640309095 0.7181913853 0.6498501897 - 0.6652973890 0.7185119987 0.6504852176 - 0.6665912271 0.7188284993 0.6511142254 - 0.6679108739 0.7191410065 0.6517375708 - 0.6692553163 0.7194498181 0.6523556113 - 0.6706231833 0.7197551131 0.6529684067 - 0.6720132232 0.7200570703 0.6535763144 - 0.6734240055 0.7203561068 0.6541793942 - 0.6748545170 0.7206522822 0.6547781229 - 0.6763033271 0.7209458947 0.6553725004 - 0.6777691841 0.7212371230 0.6559628844 - 0.6792508960 0.7215262055 0.6565495729 - 0.6807470918 0.7218133807 0.6571326852 - 0.6822565198 0.7220988274 0.6577125192 - 0.6837779880 0.7223827839 0.6582891941 - 0.6853101850 0.7226654887 0.6588631272 - 0.6868517995 0.7229471207 0.6594343781 - 0.6884016991 0.7232279778 0.6600034237 - 0.6899583936 0.7235082984 0.6605702043 - 0.6915208101 0.7237882018 0.6611351967 - 0.6930875182 0.7240679264 0.6616984010 - 0.6946573853 0.7243477702 0.6622602940 - 0.6962291002 0.7246279120 0.6628209949 - 0.6978012919 0.7249085903 0.6633808017 - 0.6993728876 0.7251899838 0.6639397740 - 0.7009423971 0.7254723907 0.6644983888 - 0.7025088072 0.7257559896 0.6650567055 - 0.7040706277 0.7260410190 0.6656150222 - 0.7056266069 0.7263275981 0.6661735773 - 0.7071756124 0.7266160846 0.6667326093 - 0.7087162733 0.7269067168 0.6672922969 - 0.7102472782 0.7271996140 0.6678529978 - 0.7117674947 0.7274950743 0.6684147716 - 0.7132756114 0.7277932763 0.6689779758 - 0.7147703171 0.7280943990 0.6695429087 - 0.7162501812 0.7283987999 0.6701096892 - 0.7177143097 0.7287065983 0.6706784964 - 0.7191610932 0.7290180922 0.6712498069 - 0.7205895185 0.7293334007 0.6718236208 - 0.7219980955 0.7296528220 0.6724001765 - 0.7233856916 0.7299764752 0.6729798913 - 0.7247509956 0.7303047776 0.6735628843 - 0.7260926962 0.7306377888 0.6741493940 - 0.7274097204 0.7309758067 0.6747397184 - 0.7287005186 0.7313190103 0.6753339767 - 0.7299638987 0.7316675782 0.6759325266 - 0.7311987281 0.7320219278 0.6765354276 - 0.7324035764 0.7323821187 0.6771430969 - 0.7335773706 0.7327483892 0.6777557731 - 0.7347186208 0.7331209779 0.6783735752 - 0.7358261943 0.7335001230 0.6789968014 - 0.7368987799 0.7338860035 0.6796255708 - 0.7379351258 0.7342789173 0.6802604198 - 0.7389339209 0.7346789837 0.6809012294 - 0.7398939133 0.7350866199 0.6815484166 - 0.7408269048 0.7355018258 0.6821988821 - 0.7417528033 0.7359247804 0.6828476787 - 0.7426717877 0.7363560200 0.6834948063 - 0.7435839772 0.7367956042 0.6841400862 - 0.7444893718 0.7372435927 0.6847838163 - 0.7453880906 0.7377005219 0.6854259968 - 0.7462803125 0.7381662726 0.6860665083 - 0.7471659184 0.7386413813 0.6867055893 - 0.7480450869 0.7391259074 0.6873431802 - 0.7489178777 0.7396200895 0.6879792809 - 0.7497844100 0.7401241064 0.6886140704 - 0.7506446242 0.7406383157 0.6892474890 - 0.7514986992 0.7411627769 0.6898795962 - 0.7523466945 0.7416979074 0.6905105114 - 0.7531886101 0.7422437072 0.6911401153 - 0.7540246844 0.7428004742 0.6917685866 - 0.7548547983 0.7433686256 0.6923959255 - 0.7556790709 0.7439481020 0.6930220723 - 0.7564978004 0.7445440292 0.6936473250 - 0.7573106885 0.7451735139 0.6942713857 - 0.7581180930 0.7458376884 0.6948946118 - 0.7589198947 0.7465357184 0.6955168843 - 0.7597163916 0.7472668290 0.6961382031 - 0.7605074048 0.7480301857 0.6967588067 - 0.7612931728 0.7488250732 0.6973785162 - 0.7620736957 0.7496505976 0.6979975104 - 0.7628490925 0.7505061030 0.6986157894 - 0.7636194229 0.7513905764 0.6992332935 - 0.7643846869 0.7523034215 0.6998503208 - 0.7651450038 0.7532438040 0.7004666924 - 0.7659006119 0.7542108297 0.7010825276 - 0.7666512728 0.7552037239 0.7016978264 - 0.7673972845 0.7562217712 0.7023125887 - 0.7681385875 0.7572641969 0.7029271126 - 0.7688754201 0.7583301067 0.7035411000 - 0.7696077228 0.7594187260 0.7041547894 - 0.7703356147 0.7605292797 0.7047681808 - 0.7710590959 0.7616609931 0.7053813934 - 0.7717784047 0.7628129721 0.7059943080 - 0.7724934220 0.7639846206 0.7066071033 - 0.7732043266 0.7651749253 0.7072197199 - 0.7739111781 0.7663831711 0.7078322768 - 0.7746139765 0.7676087022 0.7084447742 - 0.7753129005 0.7688505054 0.7090572715 - 0.7760080099 0.7701079249 0.7096698880 - 0.7766993046 0.7713800073 0.7102825046 - 0.7773869038 0.7726660967 0.7108952999 - 0.7780709267 0.7739654779 0.7115082145 - 0.7787513733 0.7752770782 0.7121213078 - 0.7794283032 0.7766004205 0.7127346992 - 0.7801018953 0.7779344916 0.7133483887 - 0.7807720900 0.7792785764 0.7139623761 - 0.7814390063 0.7806318998 0.7145767808 - 0.7821028233 0.7819936275 0.7151916027 - 0.7827634215 0.7833629251 0.7158069015 - 0.7834209800 0.7847390175 0.7164226174 - 0.7840756178 0.7861210704 0.7170389891 - 0.7847272754 0.7875084877 0.7176558971 - 0.7853761911 0.7889003158 0.7182734013 - 0.7860223055 0.7902957201 0.7188916206 - 0.7866656780 0.7916939855 0.7195104957 - 0.7873066068 0.7930942774 0.7201302052 - 0.7879447937 0.7944958806 0.7207505703 - 0.7885807157 0.7958979011 0.7213718891 - 0.7892140746 0.7972996235 0.7219941020 - 0.7898452282 0.7987000942 0.7226172090 - 0.7904739976 0.8000987172 0.7232413292 - 0.7911006808 0.8014945984 0.7238664031 - 0.7917252779 0.8028869033 0.7244924903 - 0.7923477888 0.8042749166 0.7251197100 - 0.7929683924 0.8056578040 0.7257481217 - 0.7935870886 0.8070347905 0.7263776064 - 0.7942039967 0.8084051013 0.7270082831 - 0.7948191166 0.8097679019 0.7276402712 - 0.7954326272 0.8111224174 0.7282735705 - 0.7960445285 0.8124678135 0.7289083004 - 0.7966548204 0.8138033152 0.7295442820 - 0.7972636819 0.8151280880 0.7301818132 - 0.7978711724 0.8164414167 0.7308207154 - 0.7984774113 0.8177425265 0.7314611077 - 0.7990823984 0.8190305233 0.7321031094 - 0.7996861935 0.8203045726 0.7327467203 - 0.8002889752 0.8215640187 0.7333918810 - 0.8008906841 0.8228080273 0.7340387702 - 0.8014913797 0.8240358233 0.7346873879 - 0.8020913005 0.8252465129 0.7353377938 - 0.8026903868 0.8264393210 0.7359899879 - 0.8032888174 0.8276134729 0.7366440296 - 0.8038864732 0.8287683129 0.7372999787 - 0.8044837117 0.8299028277 0.7379577756 - 0.8050802946 0.8310164213 0.7386177182 - 0.8056765199 0.8321080804 0.7392795086 - 0.8062723279 0.8331770897 0.7399433851 - 0.8068677783 0.8342227936 0.7406094074 - 0.8074631095 0.8352441788 0.7412775755 - 0.8080583215 0.8362407088 0.7419478893 - 0.8086534142 0.8372113109 0.7426204085 - 0.8092485070 0.8381553888 0.7432953119 - 0.8098437190 0.8390719891 0.7439724207 - 0.8104389906 0.8399605155 0.7446519136 - 0.8110345006 0.8408200145 0.7453336716 - 0.8116303086 0.8416497111 0.7460181117 - 0.8122264743 0.8424487710 0.7467048168 - 0.8128219247 0.8432167172 0.7473942041 - 0.8134155869 0.8439698815 0.7480859756 - 0.8140075803 0.8447198272 0.7487804890 - 0.8145980835 0.8454664946 0.7494776249 - 0.8151867986 0.8462098837 0.7501773834 - 0.8157740235 0.8469501138 0.7508800030 - 0.8163595200 0.8476870060 0.7515853047 - 0.8169434071 0.8484206796 0.7522934079 - 0.8175256848 0.8491510749 0.7530043721 - 0.8181064725 0.8498781919 0.7537181973 - 0.8186855912 0.8506020904 0.7544351220 - 0.8192632198 0.8513227105 0.7551549077 - 0.8198392987 0.8520401120 0.7558776736 - 0.8204138279 0.8527541161 0.7566035986 - 0.8209868073 0.8534649014 0.7573326230 - 0.8215582967 0.8541724086 0.7580646873 - 0.8221281767 0.8548766971 0.7588000894 - 0.8226966858 0.8555775881 0.7595385909 - 0.8232637048 0.8562753201 0.7602804899 - 0.8238291740 0.8569697142 0.7610256076 - 0.8243932724 0.8576607704 0.7617741823 - 0.8249558806 0.8583486080 0.7625260949 - 0.8255169988 0.8590331078 0.7632815242 - 0.8260768056 0.8597143292 0.7640402913 - 0.8266351223 0.8603922129 0.7648026943 - 0.8271920085 0.8610668182 0.7655686736 - 0.8277475238 0.8617380857 0.7663382292 - 0.8283016086 0.8624060750 0.7671114802 - 0.8288543820 0.8630707860 0.7678884268 - 0.8294057846 0.8637322187 0.7686690092 - 0.8299558163 0.8643901944 0.7694535255 - 0.8305044770 0.8650450110 0.7702416778 - 0.8310518861 0.8656963706 0.7710338831 - 0.8315979838 0.8663445115 0.7718299031 - 0.8321427107 0.8669893146 0.7726336718 - 0.8326861858 0.8676307797 0.7734528184 - 0.8332284093 0.8682689071 0.7742872238 - 0.8337693214 0.8689036965 0.7751364708 - 0.8343089223 0.8695350885 0.7760004997 - 0.8348472714 0.8701633215 0.7768787146 - 0.8353844285 0.8707880974 0.7777708173 - 0.8359202743 0.8714094758 0.7786765099 - 0.8364549875 0.8720275760 0.7795954943 - 0.8369885087 0.8726423979 0.7805274129 - 0.8375207782 0.8732538223 0.7814719081 - 0.8380519152 0.8738619089 0.7824286222 - 0.8385819197 0.8744665980 0.7833971977 - 0.8391106129 0.8750680089 0.7843773961 - 0.8396382928 0.8756660223 0.7853689194 - 0.8401647210 0.8762605786 0.7863711715 - 0.8406900764 0.8768519163 0.7873840928 - 0.8412142992 0.8774397969 0.7884072065 - 0.8417373896 0.8780243993 0.7894402146 - 0.8422595263 0.8786056042 0.7904826999 - 0.8427804112 0.8791834116 0.7915344834 - 0.8433002830 0.8797578216 0.7925950885 - 0.8438190818 0.8803288937 0.7936642766 - 0.8443368077 0.8808965087 0.7947415709 - 0.8448535800 0.8814607859 0.7958269119 - 0.8453692198 0.8820217848 0.7969195843 - 0.8458839059 0.8825793266 0.7980195880 - 0.8463975787 0.8831334114 0.7991263866 - 0.8469101787 0.8836842179 0.8002396822 - 0.8474218845 0.8842315078 0.8013591766 - 0.8479325771 0.8847755194 0.8024845123 - 0.8484423161 0.8853160739 0.8036152720 - 0.8489511013 0.8858531713 0.8047512770 - 0.8494588733 0.8863869905 0.8058922291 - 0.8499658704 0.8869172931 0.8070374727 - 0.8504719138 0.8874443173 0.8081870079 - 0.8509768844 0.8879678249 0.8093402982 - 0.8514810801 0.8884878755 0.8104971051 - 0.8519843817 0.8890047073 0.8116570711 - 0.8524867892 0.8895179033 0.8128197789 - 0.8529884219 0.8900278211 0.8139849901 - 0.8534891009 0.8905342817 0.8151524067 - 0.8539890051 0.8910372853 0.8163216114 - 0.8544880152 0.8915368915 0.8174921870 - 0.8549861908 0.8920329809 0.8186638951 - 0.8554837108 0.8925257921 0.8198364973 - 0.8559802771 0.8930150867 0.8210095167 - 0.8564761281 0.8935009241 0.8221825957 - 0.8569710851 0.8939834237 0.8233553767 - 0.8574653864 0.8944622874 0.8245278001 - 0.8579589725 0.8949378729 0.8256992102 - 0.8584517241 0.8954100013 0.8268694282 - 0.8589438200 0.8958786130 0.8280379772 - 0.8594350815 0.8963438272 0.8292046785 - 0.8599258065 0.8968055248 0.8303691745 - 0.8604156971 0.8972638249 0.8315311074 - 0.8609049916 0.8977186084 0.8326901197 - 0.8613935113 0.8981699944 0.8338457942 - 0.8618813753 0.8986179233 0.8349978924 - 0.8623687029 0.8990622759 0.8361461163 - 0.8628553152 0.8995032907 0.8372901082 - 0.8633412719 0.8999407887 0.8384293914 - 0.8638265729 0.9003748298 0.8395637870 - 0.8643113971 0.9008052945 0.8406928778 - 0.8647955060 0.9012324214 0.8418164253 - 0.8652790785 0.9016559720 0.8429340124 - 0.8657621145 0.9020761251 0.8440452218 - 0.8662444949 0.9024927020 0.8451498747 - 0.8667263985 0.9029058218 0.8462476134 - 0.8672077060 0.9033154845 0.8473380208 - 0.8676884770 0.9037215710 0.8484206796 - 0.8681687713 0.9041243196 0.8494954705 - 0.8686485291 0.9045233727 0.8505619764 - 0.8691278100 0.9049190879 0.8516197801 - 0.8696066141 0.9053112268 0.8526687026 - 0.8700848818 0.9056999087 0.8537082076 - 0.8705626726 0.9060850143 0.8547381163 - 0.8710401058 0.9064666033 0.8557580113 - 0.8715170026 0.9068447948 0.8567674756 - 0.8719936013 0.9072194099 0.8577663898 - 0.8724696040 0.9075905085 0.8587542772 - 0.8729453087 0.9079579711 0.8597307801 - 0.8734205961 0.9083220959 0.8606957197 - 0.8738955259 0.9086825848 0.8616485000 - 0.8743699789 0.9090396166 0.8625890017 - 0.8748440742 0.9093930721 0.8635169268 - 0.8753178716 0.9097430110 0.8644316196 - 0.8757913709 0.9100893736 0.8653330803 - 0.8762645125 0.9104322791 0.8662207723 - 0.8767372966 0.9107716084 0.8670945168 - 0.8772097826 0.9111074209 0.8679537773 - 0.8776819706 0.9114395976 0.8687983751 - 0.8781539202 0.9117683172 0.8696280122 - 0.8786255121 0.9120935202 0.8704422116 - 0.8790968060 0.9124150872 0.8712406158 - 0.8795679212 0.9127336144 0.8720229864 - 0.8800387979 0.9130501151 0.8727890253 - 0.8805093765 0.9133645296 0.8735383153 - 0.8809797764 0.9136769176 0.8742704988 - 0.8814499974 0.9139872789 0.8749852777 - 0.8819199800 0.9142956734 0.8756824136 - 0.8823897839 0.9146021008 0.8763614297 - 0.8828594089 0.9149065018 0.8770220280 - 0.8833289146 0.9152089953 0.8776637912 - 0.8837981820 0.9155095220 0.8782864809 - 0.8842672706 0.9158080816 0.8788897991 - 0.8847364187 0.9161047935 0.8794733882 - 0.8852053285 0.9163995981 0.8800367713 - 0.8856741190 0.9166926146 0.8805798292 - 0.8861427903 0.9169837236 0.8811022043 - 0.8866114020 0.9172729254 0.8816139102 - 0.8870798945 0.9175602794 0.8821213841 - 0.8875483871 0.9178459048 0.8826246262 - 0.8880168200 0.9181296825 0.8831235766 - 0.8884850740 0.9184116721 0.8836184740 - 0.8889533877 0.9186918736 0.8841090798 - 0.8894217014 0.9189704061 0.8845956922 - 0.8898900151 0.9192472100 0.8850781918 - 0.8903583288 0.9195222259 0.8855566978 - 0.8908265829 0.9197955132 0.8860313296 - 0.8912950158 0.9200671911 0.8865017891 - 0.8917633295 0.9203370810 0.8869684935 - 0.8922317028 0.9206054211 0.8874313235 - 0.8927001953 0.9208720922 0.8878902197 - 0.8931686878 0.9211370945 0.8883454204 - 0.8936372995 0.9214004874 0.8887968063 - 0.8941059709 0.9216622710 0.8892444968 - 0.8945748210 0.9219225049 0.8896884918 - 0.8950436711 0.9221811891 0.8901289105 - 0.8955127001 0.9224383235 0.8905656934 - 0.8959817886 0.9226939082 0.8909990191 - 0.8964511156 0.9229478836 0.8914287090 - 0.8969206214 0.9232004881 0.8918548822 - 0.8973901868 0.9234514832 0.8922777176 - 0.8978599906 0.9237011075 0.8926969767 - 0.8983299732 0.9239491820 0.8931130171 - 0.8988000751 0.9241958857 0.8935257196 - 0.8992708921 0.9244412184 0.8939350843 - 0.8997430205 0.9246850014 0.8943411708 - 0.9002165794 0.9249274731 0.8947440982 - 0.9006915092 0.9251685143 0.8951439261 - 0.9011679292 0.9254081845 0.8955404758 - 0.9016454816 0.9256466031 0.8959339857 - 0.9021244049 0.9258835912 0.8963243961 - 0.9026045203 0.9261192083 0.8967118263 - 0.9030858278 0.9263535738 0.8970962763 - 0.9035682082 0.9265866876 0.8974778056 - 0.9040517807 0.9268184900 0.8978564143 - 0.9045363069 0.9270491004 0.8982321024 - 0.9050219059 0.9272783995 0.8986049891 - 0.9055085182 0.9275065064 0.8989750743 - 0.9059960246 0.9277334213 0.8993424177 - 0.9064844251 0.9279590845 0.8997070789 - 0.9069736004 0.9281836152 0.9000691175 - 0.9074636102 0.9284068942 0.9004284143 - 0.9079543948 0.9286291003 0.9007850885 - 0.9084458947 0.9288501143 0.9011393189 - 0.9089381099 0.9290701151 0.9014909267 - 0.9094309211 0.9292889237 0.9018400908 - 0.9099243283 0.9295065999 0.9021868110 - 0.9104182720 0.9297233224 0.9025312066 - 0.9109126925 0.9299389124 0.9028730989 - 0.9114075899 0.9301534891 0.9032127857 - 0.9119029045 0.9303669930 0.9035500884 - 0.9123986959 0.9305794835 0.9038851857 - 0.9128947258 0.9307910800 0.9042180777 - 0.9133909941 0.9310016036 0.9045488238 - 0.9138876200 0.9312111735 0.9048773050 - 0.9143844247 0.9314197898 0.9052038193 - 0.9148814082 0.9316275120 0.9055281878 - 0.9153785110 0.9318342805 0.9058505893 - 0.9158756733 0.9320402145 0.9061710238 - 0.9163728952 0.9322451949 0.9064894915 - 0.9168701172 0.9324492812 0.9068061113 - 0.9173672795 0.9326525927 0.9071208239 - 0.9178643823 0.9328550100 0.9074336886 - 0.9183613062 0.9330565929 0.9077448249 - 0.9188581109 0.9332574010 0.9080541134 - 0.9193546772 0.9334573746 0.9083616734 - 0.9198510051 0.9336565733 0.9086676836 - 0.9203469753 0.9338549972 0.9089720249 - 0.9208427072 0.9340527058 0.9092746973 - 0.9213380218 0.9342496991 0.9095758796 - 0.9218327999 0.9344459176 0.9098755717 - 0.9223272204 0.9346414804 0.9101737142 - 0.9228211045 0.9348363280 0.9104704857 - 0.9233145118 0.9350305200 0.9107658267 - 0.9238072038 0.9352239966 0.9110596776 - 0.9242992997 0.9354168773 0.9113523960 - 0.9247906804 0.9356092215 0.9116436839 - 0.9252815247 0.9358007908 0.9119337797 - 0.9257714152 0.9359918833 0.9122226834 - 0.9262604713 0.9361823797 0.9125103951 - 0.9267488122 0.9363722801 0.9127969742 - 0.9272361994 0.9365615845 0.9130824804 - 0.9277226925 0.9367504716 0.9133669734 - 0.9282081723 0.9369388223 0.9136503935 - 0.9286926985 0.9371265769 0.9139328003 - 0.9291760921 0.9373139143 0.9142143130 - 0.9296584129 0.9375007749 0.9144948721 - 0.9301396012 0.9376872182 0.9147745967 - 0.9306195974 0.9378731251 0.9150534868 - 0.9310984015 0.9380586743 0.9153317213 - 0.9315758944 0.9382438064 0.9156090021 - 0.9320520163 0.9384285212 0.9158856869 - 0.9325268865 0.9386128783 0.9161617160 - 0.9330003262 0.9387968779 0.9164370894 - 0.9334722757 0.9389805198 0.9167118073 - 0.9339427948 0.9391639233 0.9169859886 - 0.9344118237 0.9393469095 0.9172596931 - 0.9348791838 0.9395294785 0.9175329804 - 0.9353449941 0.9397119880 0.9178056717 - 0.9358091950 0.9398940802 0.9180781245 - 0.9362716079 0.9400759935 0.9183501005 - 0.9367324114 0.9402576089 0.9186217785 - 0.9371913075 0.9404391050 0.9188932180 - 0.9376484752 0.9406203032 0.9191644192 - 0.9381036758 0.9408013225 0.9194353223 - 0.9385570884 0.9409821033 0.9197061062 - 0.9390084743 0.9411628246 0.9199767709 - 0.9394578934 0.9413434267 0.9202473164 - 0.9399052858 0.9415237904 0.9205178022 - 0.9403507113 0.9417039752 0.9207882881 - 0.9407938719 0.9418842196 0.9210587740 - 0.9412348866 0.9420642853 0.9213293791 - 0.9416738153 0.9422444105 0.9216001034 - 0.9421104193 0.9424244165 0.9218708873 - 0.9425446987 0.9426043034 0.9221419096 - 0.9429767132 0.9427841902 0.9224131107 - 0.9434064031 0.9429640770 0.9226844907 - 0.9438335896 0.9431440234 0.9229562879 - 0.9442583919 0.9433240294 0.9232283831 - 0.9446805716 0.9435039759 0.9235007763 - 0.9451004267 0.9436839819 0.9237737060 - 0.9455174804 0.9438641071 0.9240469933 - 0.9459319711 0.9440442920 0.9243208170 - 0.9463438988 0.9442245960 0.9245951176 - 0.9467530251 0.9444050193 0.9248698950 - 0.9471595287 0.9445855021 0.9251453876 - 0.9475631118 0.9447662234 0.9254214764 - 0.9479637742 0.9449470043 0.9256982803 - 0.9483616948 0.9451280832 0.9259757996 - 0.9487566948 0.9453092813 0.9262540936 - 0.9491487741 0.9454907179 0.9265332222 - 0.9495378137 0.9456723928 0.9268131256 - 0.9499238133 0.9458541870 0.9270938039 - 0.9503065944 0.9460363984 0.9273754954 - 0.9506863952 0.9462187886 0.9276580811 - 0.9510629773 0.9464014769 0.9279416800 - 0.9514362812 0.9465845227 0.9282262921 - 0.9518064857 0.9467678070 0.9285119772 - 0.9521732926 0.9469515085 0.9287987947 - 0.9525367022 0.9471355081 0.9290868044 - 0.9528967738 0.9473198056 0.9293758869 - 0.9532535076 0.9475045204 0.9296662211 - 0.9536067247 0.9476897120 0.9299578071 - 0.9539564252 0.9478752017 0.9302507043 - 0.9543024898 0.9480612278 0.9305449128 - 0.9546449780 0.9482476115 0.9308404922 - 0.9549838901 0.9484344125 0.9311375022 - 0.9553192258 0.9486218095 0.9314358830 - 0.9556506872 0.9488096237 0.9317358136 - 0.9559783936 0.9489979148 0.9320372939 - 0.9563022852 0.9491868019 0.9323403239 - 0.9566224217 0.9493761063 0.9326449037 - 0.9569386244 0.9495660067 0.9329512119 - 0.9572507739 0.9497565031 0.9332591891 - 0.9575591087 0.9499475956 0.9335687757 - 0.9578633904 0.9501392245 0.9338802099 - 0.9581636190 0.9503315091 0.9341934919 - 0.9584596753 0.9505243897 0.9345085025 - 0.9587516189 0.9507179260 0.9348254204 - 0.9590393901 0.9509121180 0.9351443052 - 0.9593229294 0.9511070251 0.9354649782 - 0.9596030712 0.9513025284 0.9357873797 - 0.9598832726 0.9514988065 0.9361097813 - 0.9601637125 0.9516956806 0.9364321828 - 0.9604443908 0.9518933892 0.9367544055 - 0.9607254267 0.9520918727 0.9370766282 - 0.9610065818 0.9522910714 0.9373986721 - 0.9612880945 0.9524911046 0.9377207160 - 0.9615697265 0.9526919127 0.9380425811 - 0.9618514776 0.9528934956 0.9383645058 - 0.9621335864 0.9530959129 0.9386863112 - 0.9624156952 0.9532992244 0.9390078783 - 0.9626979828 0.9535033107 0.9393295050 - 0.9629803896 0.9537082911 0.9396510720 - 0.9632629752 0.9539142251 0.9399725199 - 0.9635456204 0.9541209936 0.9402939081 - 0.9638283253 0.9543287158 0.9406151175 - 0.9641110897 0.9545372725 0.9409363270 - 0.9643939137 0.9547469020 0.9412574172 - 0.9646766782 0.9549574852 0.9415785074 - 0.9649596214 0.9551690221 0.9418994188 - 0.9652423859 0.9553815126 0.9422202706 - 0.9655252099 0.9555950761 0.9425410032 - 0.9658079743 0.9558095932 0.9428616762 - 0.9660906792 0.9560251832 0.9431822896 - 0.9663733840 0.9562419057 0.9435027838 - 0.9666560292 0.9564597011 0.9438232780 - 0.9669383764 0.9566785097 0.9441435933 - 0.9672207832 0.9568983912 0.9444639087 - 0.9675030112 0.9571195245 0.9447841048 - 0.9677850008 0.9573416710 0.9451041818 - 0.9680668712 0.9575650096 0.9454241991 - 0.9683486223 0.9577894807 0.9457440972 - 0.9686300755 0.9580152035 0.9460639954 - 0.9689114094 0.9582421184 0.9463837147 - 0.9691925049 0.9584702253 0.9467033744 - 0.9694733024 0.9586989880 0.9470229745 - 0.9697538018 0.9589279294 0.9473425150 - 0.9700341225 0.9591571093 0.9476618767 - 0.9703140259 0.9593864083 0.9479812980 - 0.9705935717 0.9596158862 0.9483004808 - 0.9708729982 0.9598456025 0.9486197233 - 0.9711518884 0.9600754976 0.9489387274 - 0.9714304805 0.9603055716 0.9492576718 - 0.9717087150 0.9605358243 0.9495766163 - 0.9719864726 0.9607661963 0.9498953819 - 0.9722638726 0.9609969258 0.9502142072 - 0.9725409150 0.9612277150 0.9505327940 - 0.9728174210 0.9614586830 0.9508513808 - 0.9730935097 0.9616897702 0.9511697888 - 0.9733691216 0.9619212151 0.9514881968 - 0.9736441970 0.9621527195 0.9518064857 - 0.9739187956 0.9623845220 0.9521247149 - 0.9741927981 0.9626163840 0.9524428248 - 0.9744663835 0.9628484249 0.9527608752 - 0.9747393131 0.9630807042 0.9530788064 - 0.9750117064 0.9633131027 0.9533966780 - 0.9752835035 0.9635457993 0.9537143707 - 0.9755547047 0.9637786150 0.9540321231 - 0.9758251905 0.9640114903 0.9543496966 - 0.9760950804 0.9642447233 0.9546672106 - 0.9763643742 0.9644780159 0.9549846053 - 0.9766330123 0.9647116065 0.9553018808 - 0.9769008756 0.9649453163 0.9556190968 - 0.9771680832 0.9651790857 0.9559363127 - 0.9774345160 0.9654132128 0.9562532902 - 0.9777001739 0.9656473994 0.9565703273 - 0.9779651761 0.9658818245 0.9568871856 - 0.9782294035 0.9661164284 0.9572039843 - 0.9784927964 0.9663510919 0.9575207233 - 0.9787554145 0.9665859938 0.9578372836 - 0.9790171981 0.9668210745 0.9581537843 - 0.9792782068 0.9670563936 0.9584702253 - 0.9795383215 0.9672918916 0.9587864876 - 0.9797974825 0.9675275087 0.9591028094 - 0.9800559282 0.9677633047 0.9594188929 - 0.9803133011 0.9679992199 0.9597349763 - 0.9805697799 0.9682353735 0.9600510001 - 0.9808254242 0.9684717059 0.9603667855 - 0.9810799956 0.9687082171 0.9606825709 - 0.9813336730 0.9689447880 0.9609982967 - 0.9815863967 0.9691815972 0.9613139033 - 0.9818381071 0.9694185853 0.9616295099 - 0.9820888042 0.9696558118 0.9619448781 - 0.9823384285 0.9698930979 0.9622601867 - 0.9825869799 0.9701306224 0.9625753760 - 0.9828345180 0.9703683257 0.9628906250 - 0.9830809832 0.9706060886 0.9632055759 - 0.9833263755 0.9708440900 0.9635205865 - 0.9835705757 0.9710823298 0.9638354778 - 0.9838137031 0.9713206291 0.9641503096 - 0.9840556979 0.9715592265 0.9644649029 - 0.9842965007 0.9717978239 0.9647794962 - 0.9845361114 0.9720367193 0.9650939703 - 0.9847744703 0.9722756743 0.9654083848 - 0.9850116968 0.9725148082 0.9657226801 - 0.9852476716 0.9727541804 0.9660369754 - 0.9854825139 0.9729936719 0.9663510919 - 0.9857159853 0.9732332826 0.9666650891 - 0.9859482050 0.9734731913 0.9669790864 - 0.9861791134 0.9737132192 0.9672929049 - 0.9864087105 0.9739533067 0.9676066041 - 0.9866369963 0.9741935730 0.9679203033 - 0.9868639112 0.9744340777 0.9682338834 - 0.9870895147 0.9746747017 0.9685472846 - 0.9873136878 0.9749155045 0.9688606858 - 0.9875364900 0.9751564860 0.9691740274 - 0.9877579212 0.9753975868 0.9694871902 - 0.9879779220 0.9756389260 0.9698001742 - 0.9881964922 0.9758803844 0.9701132178 - 0.9884135723 0.9761220217 0.9704260826 - 0.9886292219 0.9763637185 0.9707388878 - 0.9888433218 0.9766055942 0.9710515738 - 0.9890559912 0.9768477082 0.9713642001 - 0.9892671108 0.9770898819 0.9716768265 - 0.9894766212 0.9773322940 0.9719892144 - 0.9896847010 0.9775748849 0.9723014832 - 0.9898911119 0.9778175950 0.9726136923 - 0.9900959730 0.9780604839 0.9729259014 - 0.9902992249 0.9783034921 0.9732378721 - 0.9905009270 0.9785466790 0.9735497832 - 0.9907009006 0.9787899852 0.9738616943 - 0.9908992052 0.9790335298 0.9741734266 - 0.9910959005 0.9792770743 0.9744849801 - 0.9912909269 0.9795209169 0.9747965932 - 0.9914842248 0.9797648787 0.9751080275 - 0.9916757941 0.9800090194 0.9754194021 - 0.9918656945 0.9802532196 0.9757305980 - 0.9920538068 0.9804975986 0.9760417938 - 0.9922400713 0.9807422161 0.9763528705 - 0.9924247265 0.9809868932 0.9766638279 - 0.9926074743 0.9812318087 0.9769747257 - 0.9927883744 0.9814767838 0.9772855043 - 0.9929674864 0.9817218781 0.9775961041 - 0.9931448102 0.9819672704 0.9779067039 - 0.9933202267 0.9822127223 0.9782171845 - 0.9934937954 0.9824582934 0.9785274863 - 0.9936653972 0.9827041030 0.9788377881 - 0.9938350916 0.9829499722 0.9791479707 - 0.9940028787 0.9831960797 0.9794579744 - 0.9941688180 0.9834423065 0.9797679782 - 0.9943326712 0.9836885929 0.9800779223 - 0.9944946170 0.9839351177 0.9803876877 - 0.9946544766 0.9841818213 0.9806972742 - 0.9948124290 0.9844285846 0.9810069203 - 0.9949682951 0.9846755266 0.9813163877 - 0.9951221943 0.9849225879 0.9816257954 - 0.9952740073 0.9851698279 0.9819350243 - 0.9954236746 0.9854171872 0.9822441936 - 0.9955713153 0.9856647253 0.9825533032 - 0.9957168102 0.9859123826 0.9828622937 - 0.9958602190 0.9861602187 0.9831711054 - 0.9960014820 0.9864081740 0.9834799170 - 0.9961404800 0.9866563082 0.9837886095 - 0.9962775111 0.9869045019 0.9840971828 - 0.9964122176 0.9871528745 0.9844055772 - 0.9965447187 0.9874014258 0.9847139716 - 0.9966750145 0.9876500964 0.9850223064 - 0.9968031049 0.9878988862 0.9853304029 - 0.9969288707 0.9881477952 0.9856384993 - 0.9970524907 0.9883968830 0.9859464169 - 0.9971737266 0.9886462092 0.9862542748 - 0.9972926974 0.9888954759 0.9865620136 - 0.9974092841 0.9891449809 0.9868696928 - 0.9975237250 0.9893947244 0.9871771932 - 0.9976356030 0.9896445274 0.9874846935 - 0.9977452159 0.9898943901 0.9877920151 - 0.9978523850 0.9901443720 0.9880992770 - 0.9979572296 0.9903945923 0.9884064198 - 0.9980595708 0.9906449914 0.9887135029 - 0.9981595278 0.9908953905 0.9890204072 - 0.9982569814 0.9911460280 0.9893271923 - 0.9983521104 0.9913967848 0.9896339178 - 0.9984446168 0.9916477203 0.9899405837 - 0.9985346794 0.9918987155 0.9902470708 - 0.9986221790 0.9921497703 0.9905534983 - 0.9987071753 0.9924011230 0.9908598065 - 0.9987897277 0.9926524758 0.9911659956 - 0.9988695979 0.9929041266 0.9914721251 - 0.9989469051 0.9931557178 0.9917780757 - 0.9990215898 0.9934074879 0.9920840263 - 0.9990937710 0.9936594963 0.9923897982 - 0.9991632104 0.9939116240 0.9926955104 - 0.9992300868 0.9941638112 0.9930009842 - 0.9992942810 0.9944161177 0.9933065176 - 0.9993557930 0.9946686029 0.9936118722 - 0.9994146228 0.9949212074 0.9939171076 - 0.9994707108 0.9951738715 0.9942222834 - 0.9995239973 0.9954267740 0.9945272803 - 0.9995747209 0.9956797957 0.9948322773 - 0.9996225238 0.9959328771 0.9951370955 - 0.9996675849 0.9961860776 0.9954419136 - 0.9997099042 0.9964395165 0.9957464933 - 0.9997494221 0.9966930151 0.9960510135 - 0.9997860789 0.9969465733 0.9963554144 - 0.9998199940 0.9972004294 0.9966596961 - 0.9998509288 0.9974542856 0.9969639182 - 0.9998790026 0.9977083206 0.9972680211 - 0.9999042153 0.9979624152 0.9975720048 - 0.9999266267 0.9982166886 0.9978759289 - 0.9999458790 0.9984710813 0.9981796145 - 0.9999623895 0.9987255931 0.9984833002 - 0.9999759197 0.9989802241 0.9987869263 - 0.9999864101 0.9992349744 0.9990903139 - 0.9999939799 0.9994899035 0.9993935823 - 0.9999985099 0.9997448921 0.9996969104 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0008503073 0.0060487022 0.0064002438 + 0.0031247020 0.0077070072 0.0084807090 + 0.0065889051 0.0093767531 0.0092614815 + 0.0087466193 0.0171994604 0.0101535600 + 0.0108801499 0.0271614902 0.0109591102 + 0.0145162800 0.0324796699 0.0117238201 + 0.0193788800 0.0365465991 0.0124966996 + 0.0240556896 0.0401393399 0.0133267604 + 0.0285850298 0.0431072004 0.0142630199 + 0.0330653489 0.0448529907 0.0153544797 + 0.0372812003 0.0457478799 0.0166501496 + 0.0410104282 0.0465190299 0.0183483995 + 0.0442945287 0.0478400812 0.0208099205 + 0.0474888906 0.0510223396 0.0238338206 + 0.0509595312 0.0559057482 0.0271846093 + 0.0551288798 0.0616184585 0.0307629295 + 0.0602344498 0.0672886521 0.0349612683 + 0.0657841787 0.0724673122 0.0396671183 + 0.0712287277 0.0778958723 0.0446968898 + 0.0760187581 0.0834463537 0.0498669893 + 0.0797053277 0.0889106467 0.0549938083 + 0.0827539414 0.0940806195 0.0598937795 + 0.0854174793 0.0987935290 0.0646180511 + 0.0878179371 0.1031830981 0.0695652813 + 0.0900772810 0.1073582023 0.0744954869 + 0.0923174620 0.1114075035 0.0791333169 + 0.0946604684 0.1154199019 0.0832034126 + 0.0972282588 0.1194840968 0.0864467174 + 0.1001427993 0.1236867979 0.0890749767 + 0.1034547016 0.1279577017 0.0913295001 + 0.1070737988 0.1322295070 0.0933255404 + 0.1109554023 0.1365102977 0.0951783583 + 0.1150562018 0.1408081055 0.0970032215 + 0.1193327978 0.1451306939 0.0989153683 + 0.1237417012 0.1494861990 0.1010300964 + 0.1282396019 0.1538825035 0.1034625992 + 0.1330090016 0.1583275944 0.1061728969 + 0.1381776035 0.1628293991 0.1090466008 + 0.1436399072 0.1673959941 0.1120757982 + 0.1492906064 0.1720408946 0.1152525023 + 0.1550242007 0.1768026054 0.1185685992 + 0.1607352942 0.1816647947 0.1220161989 + 0.1663185954 0.1866011024 0.1255874038 + 0.1716686040 0.1915850043 0.1292741001 + 0.1766799986 0.1965896934 0.1330682933 + 0.1812472045 0.2015890032 0.1369619966 + 0.1853772998 0.2065560967 0.1409474015 + 0.1893154979 0.2114645988 0.1450162977 + 0.1930917948 0.2164134979 0.1491608024 + 0.1967224926 0.2215435952 0.1533937007 + 0.2002238035 0.2267996967 0.1579982042 + 0.2036122978 0.2321251035 0.1629922986 + 0.2069043070 0.2374628037 0.1682806015 + 0.2101161033 0.2427562028 0.1737675965 + 0.2132640928 0.2479483038 0.1793580055 + 0.2163646966 0.2529824078 0.1849561036 + 0.2194342017 0.2578017116 0.1904667020 + 0.2224891037 0.2623493075 0.1957941055 + 0.2255457044 0.2665685117 0.2008430064 + 0.2286203057 0.2704024017 0.2055179030 + 0.2317294031 0.2737948895 0.2097503990 + 0.2348892987 0.2768653929 0.2138119936 + 0.2381162941 0.2797451913 0.2177951038 + 0.2414056957 0.2824516892 0.2216964066 + 0.2446805984 0.2850024104 0.2255128026 + 0.2479368001 0.2874149084 0.2292409986 + 0.2511793077 0.2897064984 0.2328778058 + 0.2544130981 0.2918949127 0.2364200950 + 0.2576431930 0.2939974070 0.2398646027 + 0.2608745992 0.2960315943 0.2432080954 + 0.2641122043 0.2980149090 0.2464473993 + 0.2673611939 0.2999647856 0.2495792955 + 0.2706263959 0.3018988967 0.2526006103 + 0.2739129066 0.3038344979 0.2555082142 + 0.2772257030 0.3057892025 0.2582986951 + 0.2805697918 0.3077805042 0.2609690130 + 0.2839502096 0.3098258972 0.2635160089 + 0.2874231935 0.3119427860 0.2659429908 + 0.2910543084 0.3141488135 0.2682636976 + 0.2948203087 0.3164612055 0.2704857886 + 0.2986972034 0.3188976943 0.2726168931 + 0.3026610911 0.3214755952 0.2746646106 + 0.3066878021 0.3242133856 0.2766366005 + 0.3107534945 0.3271133900 0.2785406113 + 0.3148340881 0.3301571906 0.2803840935 + 0.3189055920 0.3333252966 0.2821748853 + 0.3229441047 0.3365986049 0.2839204967 + 0.3269253969 0.3399577141 0.2856287062 + 0.3308256865 0.3433833122 0.2873069942 + 0.3346208036 0.3468560874 0.2889631093 + 0.3382869065 0.3503566980 0.2906045914 + 0.3417999148 0.3538660109 0.2922393084 + 0.3451356888 0.3573645949 0.2938747108 + 0.3482705057 0.3608331978 0.2955183983 + 0.3511801958 0.3642525077 0.2971782088 + 0.3538407981 0.3676030934 0.2988617122 + 0.3562282920 0.3708657920 0.3005765080 + 0.3583298028 0.3740212917 0.3023301959 + 0.3602797091 0.3770503104 0.3041304946 + 0.3621290028 0.3799622953 0.3059850931 + 0.3638837039 0.3828690052 0.3079015911 + 0.3655497134 0.3857800961 0.3098875880 + 0.3671329916 0.3886910081 0.3119508028 + 0.3686397970 0.3915975094 0.3140988052 + 0.3700759113 0.3944953084 0.3163442016 + 0.3714475036 0.3973797858 0.3187192976 + 0.3727603853 0.4002467990 0.3212150037 + 0.3740208149 0.4030919075 0.3238149881 + 0.3752345145 0.4059107900 0.3265028894 + 0.3764077127 0.4086990058 0.3292626143 + 0.3775463104 0.4114522040 0.3320775926 + 0.3786563873 0.4141660929 0.3349316120 + 0.3797439933 0.4168362021 0.3378083110 + 0.3808149099 0.4194582105 0.3406915069 + 0.3818753958 0.4220277965 0.3435646892 + 0.3829312921 0.4245404899 0.3464117050 + 0.3839887977 0.4269919991 0.3492161930 + 0.3850536942 0.4293780029 0.3519617915 + 0.3861320913 0.4316940010 0.3546322882 + 0.3872300982 0.4339357018 0.3572112024 + 0.3883534968 0.4360988140 0.3596824110 + 0.3895084858 0.4381788075 0.3620485067 + 0.3907010853 0.4401713908 0.3643788993 + 0.3919371068 0.4420722127 0.3666825891 + 0.3932228088 0.4438770115 0.3689602911 + 0.3945640028 0.4456013143 0.3712129891 + 0.3959667087 0.4472714067 0.3734416068 + 0.3974370956 0.4488897026 0.3756470084 + 0.3989810050 0.4504584968 0.3778300881 + 0.4006046057 0.4519804120 0.3799915910 + 0.4023137093 0.4534575939 0.3821327090 + 0.4041146040 0.4548926055 0.3842540085 + 0.4060753882 0.4562878013 0.3863565922 + 0.4082339108 0.4576455057 0.3884412050 + 0.4105701149 0.4589681923 0.3905088902 + 0.4130640924 0.4602581859 0.3925603926 + 0.4156959951 0.4615179896 0.3945967853 + 0.4184457064 0.4627498090 0.3966186941 + 0.4212931991 0.4639562070 0.3986273110 + 0.4242187142 0.4651395082 0.4006232023 + 0.4272021055 0.4663020968 0.4026075006 + 0.4302234948 0.4674463868 0.4045810997 + 0.4332629144 0.4685747921 0.4065446854 + 0.4363003075 0.4696896076 0.4084993005 + 0.4393157065 0.4707933962 0.4104458094 + 0.4422892928 0.4718883038 0.4123851061 + 0.4452010095 0.4729770124 0.4143181145 + 0.4480308890 0.4740616977 0.4162456095 + 0.4507589042 0.4751448035 0.4181686044 + 0.4533652067 0.4762287140 0.4200878143 + 0.4558297098 0.4773159027 0.4220044017 + 0.4581323862 0.4784086943 0.4239189923 + 0.4602535069 0.4795095026 0.4258325994 + 0.4622519016 0.4806207120 0.4277462065 + 0.4642114937 0.4817445874 0.4296604991 + 0.4661338031 0.4828838110 0.4315764010 + 0.4680199921 0.4840404987 0.4334949851 + 0.4698711932 0.4852172136 0.4354169965 + 0.4716886878 0.4864163101 0.4373432994 + 0.4734737873 0.4876401126 0.4392806888 + 0.4752275944 0.4888910949 0.4412496090 + 0.4769513011 0.4901716113 0.4432489872 + 0.4786463082 0.4914839864 0.4452759027 + 0.4803135991 0.4928306937 0.4473274052 + 0.4819546044 0.4942142069 0.4494006038 + 0.4835703969 0.4956367016 0.4514926076 + 0.4851622880 0.4971008003 0.4536004961 + 0.4867314994 0.4986090064 0.4557211995 + 0.4882791936 0.5001742244 0.4578520060 + 0.4898067117 0.5017988086 0.4599897861 + 0.4913150966 0.5034782290 0.4621317089 + 0.4928056002 0.5052075982 0.4642749131 + 0.4942795932 0.5069822073 0.4664162993 + 0.4957382083 0.5087974072 0.4685530961 + 0.4971826971 0.5106484294 0.4706822038 + 0.4986141920 0.5125305057 0.4728009105 + 0.5000339746 0.5144389868 0.4749062061 + 0.5014433265 0.5163691044 0.4769951105 + 0.5028433204 0.5183159709 0.4790647030 + 0.5042353272 0.5202751756 0.4811120927 + 0.5056204796 0.5222417116 0.4831343889 + 0.5070000291 0.5242109895 0.4851284921 + 0.5083752275 0.5261781812 0.4870916903 + 0.5097472072 0.5281386971 0.4890210032 + 0.5111172199 0.5300877094 0.4909133911 + 0.5124865770 0.5320205092 0.4927659929 + 0.5138564110 0.5339323282 0.4945758879 + 0.5152279735 0.5358185172 0.4963401854 + 0.5166025162 0.5376741886 0.4980559051 + 0.5179811716 0.5394948125 0.4997201860 + 0.5193653107 0.5412756205 0.5013300180 + 0.5207560062 0.5430117249 0.5028824806 + 0.5221545100 0.5446984768 0.5043746829 + 0.5235620141 0.5463312864 0.5058037043 + 0.5249798894 0.5479233265 0.5071665049 + 0.5264092088 0.5495030880 0.5084602833 + 0.5278512836 0.5510712266 0.5096821785 + 0.5293071866 0.5526280999 0.5108290911 + 0.5307784081 0.5541737080 0.5118982196 + 0.5322659016 0.5557085276 0.5128865242 + 0.5337721109 0.5572324991 0.5137987733 + 0.5353026986 0.5587460995 0.5146775842 + 0.5368571877 0.5602493286 0.5155304074 + 0.5384340286 0.5617424846 0.5163580179 + 0.5400314927 0.5632258058 0.5171611905 + 0.5416480899 0.5646995902 0.5179405808 + 0.5432822704 0.5661638975 0.5186970830 + 0.5449323058 0.5676190257 0.5194314122 + 0.5465967059 0.5690650940 0.5201442242 + 0.5482738018 0.5705024004 0.5208364129 + 0.5499621034 0.5719311833 0.5215085745 + 0.5516598821 0.5733516812 0.5221616030 + 0.5533655882 0.5747640133 0.5227962136 + 0.5550776720 0.5761684775 0.5234131217 + 0.5567945242 0.5775651932 0.5240131021 + 0.5585144758 0.5789545178 0.5245969892 + 0.5602360964 0.5803365111 0.5251653790 + 0.5619575977 0.5817114711 0.5257192254 + 0.5636774898 0.5830796957 0.5262590051 + 0.5653942227 0.5844413042 0.5267856717 + 0.5671060085 0.5857964754 0.5273000002 + 0.5688114166 0.5871455073 0.5278027058 + 0.5705088973 0.5884885788 0.5282945037 + 0.5721967220 0.5898259282 0.5287762284 + 0.5738732815 0.5911576748 0.5292484164 + 0.5755370855 0.5924841762 0.5297120810 + 0.5771865249 0.5938054919 0.5301678181 + 0.5788199902 0.5951220989 0.5306165218 + 0.5804358125 0.5964338779 0.5310587883 + 0.5820325017 0.5977414250 0.5314955115 + 0.5836083889 0.5990446210 0.5319272876 + 0.5851619244 0.6003437042 0.5323550105 + 0.5866914988 0.6016390920 0.5327793956 + 0.5881955028 0.6029310226 0.5332012177 + 0.5896723866 0.6042193770 0.5336210728 + 0.5911204815 0.6055046916 0.5340399146 + 0.5925382972 0.6067870855 0.5344585180 + 0.5939241052 0.6080667973 0.5348774195 + 0.5952764153 0.6093440056 0.5352975130 + 0.5965934992 0.6106188893 0.5357195735 + 0.5978739262 0.6118916869 0.5361443162 + 0.5991160274 0.6131626964 0.5365725160 + 0.6003181934 0.6144319773 0.5370048285 + 0.6014788747 0.6157000065 0.5374420881 + 0.6025965214 0.6169667244 0.5378851295 + 0.6036692858 0.6182324886 0.5383346081 + 0.6046959162 0.6194974780 0.5387912989 + 0.6056745052 0.6207618713 0.5392559171 + 0.6066036820 0.6220260262 0.5397291780 + 0.6074817777 0.6232900023 0.5402120948 + 0.6083071828 0.6245540977 0.5407050848 + 0.6090785265 0.6258184910 0.5412091017 + 0.6098169088 0.6270834804 0.5417249203 + 0.6105371714 0.6283515096 0.5422530770 + 0.6112400889 0.6296306252 0.5427945852 + 0.6119259000 0.6309205890 0.5433501005 + 0.6125950217 0.6322209239 0.5439202785 + 0.6132478714 0.6335307956 0.5445060134 + 0.6138849854 0.6348494887 0.5451080203 + 0.6145066023 0.6361764073 0.5457270145 + 0.6151131988 0.6375107765 0.5463637114 + 0.6157053113 0.6388520002 0.5470190048 + 0.6162831783 0.6401991844 0.5476934910 + 0.6168472767 0.6415519118 0.5483881235 + 0.6173980832 0.6429092288 0.5491034985 + 0.6179360747 0.6442704797 0.5498402715 + 0.6184614897 0.6456350088 0.5505995154 + 0.6189749241 0.6470022202 0.5513817072 + 0.6194766164 0.6483712792 0.5521876812 + 0.6199671030 0.6497414708 0.5530344248 + 0.6204468012 0.6511121988 0.5539386868 + 0.6209161282 0.6524828076 0.5548986197 + 0.6213753819 0.6538524032 0.5559120178 + 0.6218250990 0.6552203894 0.5569766164 + 0.6222656965 0.6565861106 0.5580903888 + 0.6226975918 0.6579487920 0.5592513084 + 0.6231212020 0.6593078971 0.5604572296 + 0.6235368848 0.6606624722 0.5617058873 + 0.6239451170 0.6620119810 0.5629951954 + 0.6243463159 0.6633557081 0.5643231869 + 0.6247407794 0.6646929979 0.5656875968 + 0.6251292229 0.6660230160 0.5670863986 + 0.6255117059 0.6673452258 0.5685173273 + 0.6258888841 0.6686587930 0.5699784160 + 0.6262611151 0.6699631214 0.5714673996 + 0.6266288161 0.6712573767 0.5729821920 + 0.6269922853 0.6725410223 0.5745208263 + 0.6273521781 0.6738132238 0.5760809779 + 0.6277086735 0.6750733852 0.5776606202 + 0.6280624270 0.6763207912 0.5792576075 + 0.6284136772 0.6775547266 0.5808697939 + 0.6287629008 0.6787744164 0.5824952126 + 0.6291105151 0.6799793243 0.5841314793 + 0.6294568777 0.6811686158 0.5857766271 + 0.6298025250 0.6823415756 0.5874286294 + 0.6301478148 0.6834977269 0.5890851021 + 0.6304932237 0.6846361160 0.5907440782 + 0.6308389902 0.6857560873 0.5924034715 + 0.6311857104 0.6868569851 0.5940610766 + 0.6315336823 0.6879382133 0.5957149267 + 0.6318835020 0.6889989972 0.5973626971 + 0.6322354078 0.6900386214 0.5990023017 + 0.6325898767 0.6910563111 0.6006317139 + 0.6329473853 0.6920514703 0.6022487283 + 0.6333082914 0.6930233836 0.6038513184 + 0.6336730123 0.6939712763 0.6054372191 + 0.6340420246 0.6948946714 0.6070044041 + 0.6344156265 0.6957926154 0.6085507870 + 0.6347942948 0.6966645122 0.6100741029 + 0.6351785064 0.6975097060 0.6115723848 + 0.6355686188 0.6983274817 0.6130434275 + 0.6359649897 0.6991170049 0.6144850850 + 0.6363682151 0.6998777986 0.6158952713 + 0.6367785931 0.7006090283 0.6172720194 + 0.6371966004 0.7013099790 0.6186128855 + 0.6376225948 0.7019799948 0.6199159026 + 0.6380569935 0.7026184201 0.6211791039 + 0.6385002136 0.7032272816 0.6224001050 + 0.6389526725 0.7038217783 0.6235768795 + 0.6394149065 0.7044044733 0.6247074008 + 0.6398872137 0.7049757838 0.6257894039 + 0.6403700113 0.7055358291 0.6268209219 + 0.6408637762 0.7060847282 0.6277996898 + 0.6413689256 0.7066227794 0.6287239790 + 0.6418856978 0.7071502209 0.6296194792 + 0.6424148083 0.7076671720 0.6305025220 + 0.6429564953 0.7081741095 0.6313732862 + 0.6435111761 0.7086709738 0.6322320104 + 0.6440793276 0.7091581821 0.6330789924 + 0.6446613073 0.7096359134 0.6339144111 + 0.6452575922 0.7101042867 0.6347385049 + 0.6458685994 0.7105637193 0.6355515718 + 0.6464946866 0.7110142112 0.6363537908 + 0.6471362710 0.7114561200 0.6371454000 + 0.6477938890 0.7118896246 0.6379265785 + 0.6484678984 0.7123149037 0.6386976838 + 0.6491587162 0.7127323151 0.6394590139 + 0.6498693228 0.7131419778 0.6402105093 + 0.6506212950 0.7135441899 0.6409527063 + 0.6514183879 0.7139390707 0.6416856050 + 0.6522591114 0.7143269181 0.6424096227 + 0.6531423926 0.7147079110 0.6431248784 + 0.6540668011 0.7150822878 0.6438316703 + 0.6550312042 0.7154504061 0.6445302963 + 0.6560341716 0.7158122063 0.6452208161 + 0.6570745707 0.7161681056 0.6459035873 + 0.6581510901 0.7165182829 0.6465789080 + 0.6592624187 0.7168630958 0.6472467780 + 0.6604073048 0.7172024846 0.6479076743 + 0.6615846157 0.7175369263 0.6485617161 + 0.6627929211 0.7178664804 0.6492090821 + 0.6640309095 0.7181913853 0.6498501897 + 0.6652973890 0.7185119987 0.6504852176 + 0.6665912271 0.7188284993 0.6511142254 + 0.6679108739 0.7191410065 0.6517375708 + 0.6692553163 0.7194498181 0.6523556113 + 0.6706231833 0.7197551131 0.6529684067 + 0.6720132232 0.7200570703 0.6535763144 + 0.6734240055 0.7203561068 0.6541793942 + 0.6748545170 0.7206522822 0.6547781229 + 0.6763033271 0.7209458947 0.6553725004 + 0.6777691841 0.7212371230 0.6559628844 + 0.6792508960 0.7215262055 0.6565495729 + 0.6807470918 0.7218133807 0.6571326852 + 0.6822565198 0.7220988274 0.6577125192 + 0.6837779880 0.7223827839 0.6582891941 + 0.6853101850 0.7226654887 0.6588631272 + 0.6868517995 0.7229471207 0.6594343781 + 0.6884016991 0.7232279778 0.6600034237 + 0.6899583936 0.7235082984 0.6605702043 + 0.6915208101 0.7237882018 0.6611351967 + 0.6930875182 0.7240679264 0.6616984010 + 0.6946573853 0.7243477702 0.6622602940 + 0.6962291002 0.7246279120 0.6628209949 + 0.6978012919 0.7249085903 0.6633808017 + 0.6993728876 0.7251899838 0.6639397740 + 0.7009423971 0.7254723907 0.6644983888 + 0.7025088072 0.7257559896 0.6650567055 + 0.7040706277 0.7260410190 0.6656150222 + 0.7056266069 0.7263275981 0.6661735773 + 0.7071756124 0.7266160846 0.6667326093 + 0.7087162733 0.7269067168 0.6672922969 + 0.7102472782 0.7271996140 0.6678529978 + 0.7117674947 0.7274950743 0.6684147716 + 0.7132756114 0.7277932763 0.6689779758 + 0.7147703171 0.7280943990 0.6695429087 + 0.7162501812 0.7283987999 0.6701096892 + 0.7177143097 0.7287065983 0.6706784964 + 0.7191610932 0.7290180922 0.6712498069 + 0.7205895185 0.7293334007 0.6718236208 + 0.7219980955 0.7296528220 0.6724001765 + 0.7233856916 0.7299764752 0.6729798913 + 0.7247509956 0.7303047776 0.6735628843 + 0.7260926962 0.7306377888 0.6741493940 + 0.7274097204 0.7309758067 0.6747397184 + 0.7287005186 0.7313190103 0.6753339767 + 0.7299638987 0.7316675782 0.6759325266 + 0.7311987281 0.7320219278 0.6765354276 + 0.7324035764 0.7323821187 0.6771430969 + 0.7335773706 0.7327483892 0.6777557731 + 0.7347186208 0.7331209779 0.6783735752 + 0.7358261943 0.7335001230 0.6789968014 + 0.7368987799 0.7338860035 0.6796255708 + 0.7379351258 0.7342789173 0.6802604198 + 0.7389339209 0.7346789837 0.6809012294 + 0.7398939133 0.7350866199 0.6815484166 + 0.7408269048 0.7355018258 0.6821988821 + 0.7417528033 0.7359247804 0.6828476787 + 0.7426717877 0.7363560200 0.6834948063 + 0.7435839772 0.7367956042 0.6841400862 + 0.7444893718 0.7372435927 0.6847838163 + 0.7453880906 0.7377005219 0.6854259968 + 0.7462803125 0.7381662726 0.6860665083 + 0.7471659184 0.7386413813 0.6867055893 + 0.7480450869 0.7391259074 0.6873431802 + 0.7489178777 0.7396200895 0.6879792809 + 0.7497844100 0.7401241064 0.6886140704 + 0.7506446242 0.7406383157 0.6892474890 + 0.7514986992 0.7411627769 0.6898795962 + 0.7523466945 0.7416979074 0.6905105114 + 0.7531886101 0.7422437072 0.6911401153 + 0.7540246844 0.7428004742 0.6917685866 + 0.7548547983 0.7433686256 0.6923959255 + 0.7556790709 0.7439481020 0.6930220723 + 0.7564978004 0.7445440292 0.6936473250 + 0.7573106885 0.7451735139 0.6942713857 + 0.7581180930 0.7458376884 0.6948946118 + 0.7589198947 0.7465357184 0.6955168843 + 0.7597163916 0.7472668290 0.6961382031 + 0.7605074048 0.7480301857 0.6967588067 + 0.7612931728 0.7488250732 0.6973785162 + 0.7620736957 0.7496505976 0.6979975104 + 0.7628490925 0.7505061030 0.6986157894 + 0.7636194229 0.7513905764 0.6992332935 + 0.7643846869 0.7523034215 0.6998503208 + 0.7651450038 0.7532438040 0.7004666924 + 0.7659006119 0.7542108297 0.7010825276 + 0.7666512728 0.7552037239 0.7016978264 + 0.7673972845 0.7562217712 0.7023125887 + 0.7681385875 0.7572641969 0.7029271126 + 0.7688754201 0.7583301067 0.7035411000 + 0.7696077228 0.7594187260 0.7041547894 + 0.7703356147 0.7605292797 0.7047681808 + 0.7710590959 0.7616609931 0.7053813934 + 0.7717784047 0.7628129721 0.7059943080 + 0.7724934220 0.7639846206 0.7066071033 + 0.7732043266 0.7651749253 0.7072197199 + 0.7739111781 0.7663831711 0.7078322768 + 0.7746139765 0.7676087022 0.7084447742 + 0.7753129005 0.7688505054 0.7090572715 + 0.7760080099 0.7701079249 0.7096698880 + 0.7766993046 0.7713800073 0.7102825046 + 0.7773869038 0.7726660967 0.7108952999 + 0.7780709267 0.7739654779 0.7115082145 + 0.7787513733 0.7752770782 0.7121213078 + 0.7794283032 0.7766004205 0.7127346992 + 0.7801018953 0.7779344916 0.7133483887 + 0.7807720900 0.7792785764 0.7139623761 + 0.7814390063 0.7806318998 0.7145767808 + 0.7821028233 0.7819936275 0.7151916027 + 0.7827634215 0.7833629251 0.7158069015 + 0.7834209800 0.7847390175 0.7164226174 + 0.7840756178 0.7861210704 0.7170389891 + 0.7847272754 0.7875084877 0.7176558971 + 0.7853761911 0.7889003158 0.7182734013 + 0.7860223055 0.7902957201 0.7188916206 + 0.7866656780 0.7916939855 0.7195104957 + 0.7873066068 0.7930942774 0.7201302052 + 0.7879447937 0.7944958806 0.7207505703 + 0.7885807157 0.7958979011 0.7213718891 + 0.7892140746 0.7972996235 0.7219941020 + 0.7898452282 0.7987000942 0.7226172090 + 0.7904739976 0.8000987172 0.7232413292 + 0.7911006808 0.8014945984 0.7238664031 + 0.7917252779 0.8028869033 0.7244924903 + 0.7923477888 0.8042749166 0.7251197100 + 0.7929683924 0.8056578040 0.7257481217 + 0.7935870886 0.8070347905 0.7263776064 + 0.7942039967 0.8084051013 0.7270082831 + 0.7948191166 0.8097679019 0.7276402712 + 0.7954326272 0.8111224174 0.7282735705 + 0.7960445285 0.8124678135 0.7289083004 + 0.7966548204 0.8138033152 0.7295442820 + 0.7972636819 0.8151280880 0.7301818132 + 0.7978711724 0.8164414167 0.7308207154 + 0.7984774113 0.8177425265 0.7314611077 + 0.7990823984 0.8190305233 0.7321031094 + 0.7996861935 0.8203045726 0.7327467203 + 0.8002889752 0.8215640187 0.7333918810 + 0.8008906841 0.8228080273 0.7340387702 + 0.8014913797 0.8240358233 0.7346873879 + 0.8020913005 0.8252465129 0.7353377938 + 0.8026903868 0.8264393210 0.7359899879 + 0.8032888174 0.8276134729 0.7366440296 + 0.8038864732 0.8287683129 0.7372999787 + 0.8044837117 0.8299028277 0.7379577756 + 0.8050802946 0.8310164213 0.7386177182 + 0.8056765199 0.8321080804 0.7392795086 + 0.8062723279 0.8331770897 0.7399433851 + 0.8068677783 0.8342227936 0.7406094074 + 0.8074631095 0.8352441788 0.7412775755 + 0.8080583215 0.8362407088 0.7419478893 + 0.8086534142 0.8372113109 0.7426204085 + 0.8092485070 0.8381553888 0.7432953119 + 0.8098437190 0.8390719891 0.7439724207 + 0.8104389906 0.8399605155 0.7446519136 + 0.8110345006 0.8408200145 0.7453336716 + 0.8116303086 0.8416497111 0.7460181117 + 0.8122264743 0.8424487710 0.7467048168 + 0.8128219247 0.8432167172 0.7473942041 + 0.8134155869 0.8439698815 0.7480859756 + 0.8140075803 0.8447198272 0.7487804890 + 0.8145980835 0.8454664946 0.7494776249 + 0.8151867986 0.8462098837 0.7501773834 + 0.8157740235 0.8469501138 0.7508800030 + 0.8163595200 0.8476870060 0.7515853047 + 0.8169434071 0.8484206796 0.7522934079 + 0.8175256848 0.8491510749 0.7530043721 + 0.8181064725 0.8498781919 0.7537181973 + 0.8186855912 0.8506020904 0.7544351220 + 0.8192632198 0.8513227105 0.7551549077 + 0.8198392987 0.8520401120 0.7558776736 + 0.8204138279 0.8527541161 0.7566035986 + 0.8209868073 0.8534649014 0.7573326230 + 0.8215582967 0.8541724086 0.7580646873 + 0.8221281767 0.8548766971 0.7588000894 + 0.8226966858 0.8555775881 0.7595385909 + 0.8232637048 0.8562753201 0.7602804899 + 0.8238291740 0.8569697142 0.7610256076 + 0.8243932724 0.8576607704 0.7617741823 + 0.8249558806 0.8583486080 0.7625260949 + 0.8255169988 0.8590331078 0.7632815242 + 0.8260768056 0.8597143292 0.7640402913 + 0.8266351223 0.8603922129 0.7648026943 + 0.8271920085 0.8610668182 0.7655686736 + 0.8277475238 0.8617380857 0.7663382292 + 0.8283016086 0.8624060750 0.7671114802 + 0.8288543820 0.8630707860 0.7678884268 + 0.8294057846 0.8637322187 0.7686690092 + 0.8299558163 0.8643901944 0.7694535255 + 0.8305044770 0.8650450110 0.7702416778 + 0.8310518861 0.8656963706 0.7710338831 + 0.8315979838 0.8663445115 0.7718299031 + 0.8321427107 0.8669893146 0.7726336718 + 0.8326861858 0.8676307797 0.7734528184 + 0.8332284093 0.8682689071 0.7742872238 + 0.8337693214 0.8689036965 0.7751364708 + 0.8343089223 0.8695350885 0.7760004997 + 0.8348472714 0.8701633215 0.7768787146 + 0.8353844285 0.8707880974 0.7777708173 + 0.8359202743 0.8714094758 0.7786765099 + 0.8364549875 0.8720275760 0.7795954943 + 0.8369885087 0.8726423979 0.7805274129 + 0.8375207782 0.8732538223 0.7814719081 + 0.8380519152 0.8738619089 0.7824286222 + 0.8385819197 0.8744665980 0.7833971977 + 0.8391106129 0.8750680089 0.7843773961 + 0.8396382928 0.8756660223 0.7853689194 + 0.8401647210 0.8762605786 0.7863711715 + 0.8406900764 0.8768519163 0.7873840928 + 0.8412142992 0.8774397969 0.7884072065 + 0.8417373896 0.8780243993 0.7894402146 + 0.8422595263 0.8786056042 0.7904826999 + 0.8427804112 0.8791834116 0.7915344834 + 0.8433002830 0.8797578216 0.7925950885 + 0.8438190818 0.8803288937 0.7936642766 + 0.8443368077 0.8808965087 0.7947415709 + 0.8448535800 0.8814607859 0.7958269119 + 0.8453692198 0.8820217848 0.7969195843 + 0.8458839059 0.8825793266 0.7980195880 + 0.8463975787 0.8831334114 0.7991263866 + 0.8469101787 0.8836842179 0.8002396822 + 0.8474218845 0.8842315078 0.8013591766 + 0.8479325771 0.8847755194 0.8024845123 + 0.8484423161 0.8853160739 0.8036152720 + 0.8489511013 0.8858531713 0.8047512770 + 0.8494588733 0.8863869905 0.8058922291 + 0.8499658704 0.8869172931 0.8070374727 + 0.8504719138 0.8874443173 0.8081870079 + 0.8509768844 0.8879678249 0.8093402982 + 0.8514810801 0.8884878755 0.8104971051 + 0.8519843817 0.8890047073 0.8116570711 + 0.8524867892 0.8895179033 0.8128197789 + 0.8529884219 0.8900278211 0.8139849901 + 0.8534891009 0.8905342817 0.8151524067 + 0.8539890051 0.8910372853 0.8163216114 + 0.8544880152 0.8915368915 0.8174921870 + 0.8549861908 0.8920329809 0.8186638951 + 0.8554837108 0.8925257921 0.8198364973 + 0.8559802771 0.8930150867 0.8210095167 + 0.8564761281 0.8935009241 0.8221825957 + 0.8569710851 0.8939834237 0.8233553767 + 0.8574653864 0.8944622874 0.8245278001 + 0.8579589725 0.8949378729 0.8256992102 + 0.8584517241 0.8954100013 0.8268694282 + 0.8589438200 0.8958786130 0.8280379772 + 0.8594350815 0.8963438272 0.8292046785 + 0.8599258065 0.8968055248 0.8303691745 + 0.8604156971 0.8972638249 0.8315311074 + 0.8609049916 0.8977186084 0.8326901197 + 0.8613935113 0.8981699944 0.8338457942 + 0.8618813753 0.8986179233 0.8349978924 + 0.8623687029 0.8990622759 0.8361461163 + 0.8628553152 0.8995032907 0.8372901082 + 0.8633412719 0.8999407887 0.8384293914 + 0.8638265729 0.9003748298 0.8395637870 + 0.8643113971 0.9008052945 0.8406928778 + 0.8647955060 0.9012324214 0.8418164253 + 0.8652790785 0.9016559720 0.8429340124 + 0.8657621145 0.9020761251 0.8440452218 + 0.8662444949 0.9024927020 0.8451498747 + 0.8667263985 0.9029058218 0.8462476134 + 0.8672077060 0.9033154845 0.8473380208 + 0.8676884770 0.9037215710 0.8484206796 + 0.8681687713 0.9041243196 0.8494954705 + 0.8686485291 0.9045233727 0.8505619764 + 0.8691278100 0.9049190879 0.8516197801 + 0.8696066141 0.9053112268 0.8526687026 + 0.8700848818 0.9056999087 0.8537082076 + 0.8705626726 0.9060850143 0.8547381163 + 0.8710401058 0.9064666033 0.8557580113 + 0.8715170026 0.9068447948 0.8567674756 + 0.8719936013 0.9072194099 0.8577663898 + 0.8724696040 0.9075905085 0.8587542772 + 0.8729453087 0.9079579711 0.8597307801 + 0.8734205961 0.9083220959 0.8606957197 + 0.8738955259 0.9086825848 0.8616485000 + 0.8743699789 0.9090396166 0.8625890017 + 0.8748440742 0.9093930721 0.8635169268 + 0.8753178716 0.9097430110 0.8644316196 + 0.8757913709 0.9100893736 0.8653330803 + 0.8762645125 0.9104322791 0.8662207723 + 0.8767372966 0.9107716084 0.8670945168 + 0.8772097826 0.9111074209 0.8679537773 + 0.8776819706 0.9114395976 0.8687983751 + 0.8781539202 0.9117683172 0.8696280122 + 0.8786255121 0.9120935202 0.8704422116 + 0.8790968060 0.9124150872 0.8712406158 + 0.8795679212 0.9127336144 0.8720229864 + 0.8800387979 0.9130501151 0.8727890253 + 0.8805093765 0.9133645296 0.8735383153 + 0.8809797764 0.9136769176 0.8742704988 + 0.8814499974 0.9139872789 0.8749852777 + 0.8819199800 0.9142956734 0.8756824136 + 0.8823897839 0.9146021008 0.8763614297 + 0.8828594089 0.9149065018 0.8770220280 + 0.8833289146 0.9152089953 0.8776637912 + 0.8837981820 0.9155095220 0.8782864809 + 0.8842672706 0.9158080816 0.8788897991 + 0.8847364187 0.9161047935 0.8794733882 + 0.8852053285 0.9163995981 0.8800367713 + 0.8856741190 0.9166926146 0.8805798292 + 0.8861427903 0.9169837236 0.8811022043 + 0.8866114020 0.9172729254 0.8816139102 + 0.8870798945 0.9175602794 0.8821213841 + 0.8875483871 0.9178459048 0.8826246262 + 0.8880168200 0.9181296825 0.8831235766 + 0.8884850740 0.9184116721 0.8836184740 + 0.8889533877 0.9186918736 0.8841090798 + 0.8894217014 0.9189704061 0.8845956922 + 0.8898900151 0.9192472100 0.8850781918 + 0.8903583288 0.9195222259 0.8855566978 + 0.8908265829 0.9197955132 0.8860313296 + 0.8912950158 0.9200671911 0.8865017891 + 0.8917633295 0.9203370810 0.8869684935 + 0.8922317028 0.9206054211 0.8874313235 + 0.8927001953 0.9208720922 0.8878902197 + 0.8931686878 0.9211370945 0.8883454204 + 0.8936372995 0.9214004874 0.8887968063 + 0.8941059709 0.9216622710 0.8892444968 + 0.8945748210 0.9219225049 0.8896884918 + 0.8950436711 0.9221811891 0.8901289105 + 0.8955127001 0.9224383235 0.8905656934 + 0.8959817886 0.9226939082 0.8909990191 + 0.8964511156 0.9229478836 0.8914287090 + 0.8969206214 0.9232004881 0.8918548822 + 0.8973901868 0.9234514832 0.8922777176 + 0.8978599906 0.9237011075 0.8926969767 + 0.8983299732 0.9239491820 0.8931130171 + 0.8988000751 0.9241958857 0.8935257196 + 0.8992708921 0.9244412184 0.8939350843 + 0.8997430205 0.9246850014 0.8943411708 + 0.9002165794 0.9249274731 0.8947440982 + 0.9006915092 0.9251685143 0.8951439261 + 0.9011679292 0.9254081845 0.8955404758 + 0.9016454816 0.9256466031 0.8959339857 + 0.9021244049 0.9258835912 0.8963243961 + 0.9026045203 0.9261192083 0.8967118263 + 0.9030858278 0.9263535738 0.8970962763 + 0.9035682082 0.9265866876 0.8974778056 + 0.9040517807 0.9268184900 0.8978564143 + 0.9045363069 0.9270491004 0.8982321024 + 0.9050219059 0.9272783995 0.8986049891 + 0.9055085182 0.9275065064 0.8989750743 + 0.9059960246 0.9277334213 0.8993424177 + 0.9064844251 0.9279590845 0.8997070789 + 0.9069736004 0.9281836152 0.9000691175 + 0.9074636102 0.9284068942 0.9004284143 + 0.9079543948 0.9286291003 0.9007850885 + 0.9084458947 0.9288501143 0.9011393189 + 0.9089381099 0.9290701151 0.9014909267 + 0.9094309211 0.9292889237 0.9018400908 + 0.9099243283 0.9295065999 0.9021868110 + 0.9104182720 0.9297233224 0.9025312066 + 0.9109126925 0.9299389124 0.9028730989 + 0.9114075899 0.9301534891 0.9032127857 + 0.9119029045 0.9303669930 0.9035500884 + 0.9123986959 0.9305794835 0.9038851857 + 0.9128947258 0.9307910800 0.9042180777 + 0.9133909941 0.9310016036 0.9045488238 + 0.9138876200 0.9312111735 0.9048773050 + 0.9143844247 0.9314197898 0.9052038193 + 0.9148814082 0.9316275120 0.9055281878 + 0.9153785110 0.9318342805 0.9058505893 + 0.9158756733 0.9320402145 0.9061710238 + 0.9163728952 0.9322451949 0.9064894915 + 0.9168701172 0.9324492812 0.9068061113 + 0.9173672795 0.9326525927 0.9071208239 + 0.9178643823 0.9328550100 0.9074336886 + 0.9183613062 0.9330565929 0.9077448249 + 0.9188581109 0.9332574010 0.9080541134 + 0.9193546772 0.9334573746 0.9083616734 + 0.9198510051 0.9336565733 0.9086676836 + 0.9203469753 0.9338549972 0.9089720249 + 0.9208427072 0.9340527058 0.9092746973 + 0.9213380218 0.9342496991 0.9095758796 + 0.9218327999 0.9344459176 0.9098755717 + 0.9223272204 0.9346414804 0.9101737142 + 0.9228211045 0.9348363280 0.9104704857 + 0.9233145118 0.9350305200 0.9107658267 + 0.9238072038 0.9352239966 0.9110596776 + 0.9242992997 0.9354168773 0.9113523960 + 0.9247906804 0.9356092215 0.9116436839 + 0.9252815247 0.9358007908 0.9119337797 + 0.9257714152 0.9359918833 0.9122226834 + 0.9262604713 0.9361823797 0.9125103951 + 0.9267488122 0.9363722801 0.9127969742 + 0.9272361994 0.9365615845 0.9130824804 + 0.9277226925 0.9367504716 0.9133669734 + 0.9282081723 0.9369388223 0.9136503935 + 0.9286926985 0.9371265769 0.9139328003 + 0.9291760921 0.9373139143 0.9142143130 + 0.9296584129 0.9375007749 0.9144948721 + 0.9301396012 0.9376872182 0.9147745967 + 0.9306195974 0.9378731251 0.9150534868 + 0.9310984015 0.9380586743 0.9153317213 + 0.9315758944 0.9382438064 0.9156090021 + 0.9320520163 0.9384285212 0.9158856869 + 0.9325268865 0.9386128783 0.9161617160 + 0.9330003262 0.9387968779 0.9164370894 + 0.9334722757 0.9389805198 0.9167118073 + 0.9339427948 0.9391639233 0.9169859886 + 0.9344118237 0.9393469095 0.9172596931 + 0.9348791838 0.9395294785 0.9175329804 + 0.9353449941 0.9397119880 0.9178056717 + 0.9358091950 0.9398940802 0.9180781245 + 0.9362716079 0.9400759935 0.9183501005 + 0.9367324114 0.9402576089 0.9186217785 + 0.9371913075 0.9404391050 0.9188932180 + 0.9376484752 0.9406203032 0.9191644192 + 0.9381036758 0.9408013225 0.9194353223 + 0.9385570884 0.9409821033 0.9197061062 + 0.9390084743 0.9411628246 0.9199767709 + 0.9394578934 0.9413434267 0.9202473164 + 0.9399052858 0.9415237904 0.9205178022 + 0.9403507113 0.9417039752 0.9207882881 + 0.9407938719 0.9418842196 0.9210587740 + 0.9412348866 0.9420642853 0.9213293791 + 0.9416738153 0.9422444105 0.9216001034 + 0.9421104193 0.9424244165 0.9218708873 + 0.9425446987 0.9426043034 0.9221419096 + 0.9429767132 0.9427841902 0.9224131107 + 0.9434064031 0.9429640770 0.9226844907 + 0.9438335896 0.9431440234 0.9229562879 + 0.9442583919 0.9433240294 0.9232283831 + 0.9446805716 0.9435039759 0.9235007763 + 0.9451004267 0.9436839819 0.9237737060 + 0.9455174804 0.9438641071 0.9240469933 + 0.9459319711 0.9440442920 0.9243208170 + 0.9463438988 0.9442245960 0.9245951176 + 0.9467530251 0.9444050193 0.9248698950 + 0.9471595287 0.9445855021 0.9251453876 + 0.9475631118 0.9447662234 0.9254214764 + 0.9479637742 0.9449470043 0.9256982803 + 0.9483616948 0.9451280832 0.9259757996 + 0.9487566948 0.9453092813 0.9262540936 + 0.9491487741 0.9454907179 0.9265332222 + 0.9495378137 0.9456723928 0.9268131256 + 0.9499238133 0.9458541870 0.9270938039 + 0.9503065944 0.9460363984 0.9273754954 + 0.9506863952 0.9462187886 0.9276580811 + 0.9510629773 0.9464014769 0.9279416800 + 0.9514362812 0.9465845227 0.9282262921 + 0.9518064857 0.9467678070 0.9285119772 + 0.9521732926 0.9469515085 0.9287987947 + 0.9525367022 0.9471355081 0.9290868044 + 0.9528967738 0.9473198056 0.9293758869 + 0.9532535076 0.9475045204 0.9296662211 + 0.9536067247 0.9476897120 0.9299578071 + 0.9539564252 0.9478752017 0.9302507043 + 0.9543024898 0.9480612278 0.9305449128 + 0.9546449780 0.9482476115 0.9308404922 + 0.9549838901 0.9484344125 0.9311375022 + 0.9553192258 0.9486218095 0.9314358830 + 0.9556506872 0.9488096237 0.9317358136 + 0.9559783936 0.9489979148 0.9320372939 + 0.9563022852 0.9491868019 0.9323403239 + 0.9566224217 0.9493761063 0.9326449037 + 0.9569386244 0.9495660067 0.9329512119 + 0.9572507739 0.9497565031 0.9332591891 + 0.9575591087 0.9499475956 0.9335687757 + 0.9578633904 0.9501392245 0.9338802099 + 0.9581636190 0.9503315091 0.9341934919 + 0.9584596753 0.9505243897 0.9345085025 + 0.9587516189 0.9507179260 0.9348254204 + 0.9590393901 0.9509121180 0.9351443052 + 0.9593229294 0.9511070251 0.9354649782 + 0.9596030712 0.9513025284 0.9357873797 + 0.9598832726 0.9514988065 0.9361097813 + 0.9601637125 0.9516956806 0.9364321828 + 0.9604443908 0.9518933892 0.9367544055 + 0.9607254267 0.9520918727 0.9370766282 + 0.9610065818 0.9522910714 0.9373986721 + 0.9612880945 0.9524911046 0.9377207160 + 0.9615697265 0.9526919127 0.9380425811 + 0.9618514776 0.9528934956 0.9383645058 + 0.9621335864 0.9530959129 0.9386863112 + 0.9624156952 0.9532992244 0.9390078783 + 0.9626979828 0.9535033107 0.9393295050 + 0.9629803896 0.9537082911 0.9396510720 + 0.9632629752 0.9539142251 0.9399725199 + 0.9635456204 0.9541209936 0.9402939081 + 0.9638283253 0.9543287158 0.9406151175 + 0.9641110897 0.9545372725 0.9409363270 + 0.9643939137 0.9547469020 0.9412574172 + 0.9646766782 0.9549574852 0.9415785074 + 0.9649596214 0.9551690221 0.9418994188 + 0.9652423859 0.9553815126 0.9422202706 + 0.9655252099 0.9555950761 0.9425410032 + 0.9658079743 0.9558095932 0.9428616762 + 0.9660906792 0.9560251832 0.9431822896 + 0.9663733840 0.9562419057 0.9435027838 + 0.9666560292 0.9564597011 0.9438232780 + 0.9669383764 0.9566785097 0.9441435933 + 0.9672207832 0.9568983912 0.9444639087 + 0.9675030112 0.9571195245 0.9447841048 + 0.9677850008 0.9573416710 0.9451041818 + 0.9680668712 0.9575650096 0.9454241991 + 0.9683486223 0.9577894807 0.9457440972 + 0.9686300755 0.9580152035 0.9460639954 + 0.9689114094 0.9582421184 0.9463837147 + 0.9691925049 0.9584702253 0.9467033744 + 0.9694733024 0.9586989880 0.9470229745 + 0.9697538018 0.9589279294 0.9473425150 + 0.9700341225 0.9591571093 0.9476618767 + 0.9703140259 0.9593864083 0.9479812980 + 0.9705935717 0.9596158862 0.9483004808 + 0.9708729982 0.9598456025 0.9486197233 + 0.9711518884 0.9600754976 0.9489387274 + 0.9714304805 0.9603055716 0.9492576718 + 0.9717087150 0.9605358243 0.9495766163 + 0.9719864726 0.9607661963 0.9498953819 + 0.9722638726 0.9609969258 0.9502142072 + 0.9725409150 0.9612277150 0.9505327940 + 0.9728174210 0.9614586830 0.9508513808 + 0.9730935097 0.9616897702 0.9511697888 + 0.9733691216 0.9619212151 0.9514881968 + 0.9736441970 0.9621527195 0.9518064857 + 0.9739187956 0.9623845220 0.9521247149 + 0.9741927981 0.9626163840 0.9524428248 + 0.9744663835 0.9628484249 0.9527608752 + 0.9747393131 0.9630807042 0.9530788064 + 0.9750117064 0.9633131027 0.9533966780 + 0.9752835035 0.9635457993 0.9537143707 + 0.9755547047 0.9637786150 0.9540321231 + 0.9758251905 0.9640114903 0.9543496966 + 0.9760950804 0.9642447233 0.9546672106 + 0.9763643742 0.9644780159 0.9549846053 + 0.9766330123 0.9647116065 0.9553018808 + 0.9769008756 0.9649453163 0.9556190968 + 0.9771680832 0.9651790857 0.9559363127 + 0.9774345160 0.9654132128 0.9562532902 + 0.9777001739 0.9656473994 0.9565703273 + 0.9779651761 0.9658818245 0.9568871856 + 0.9782294035 0.9661164284 0.9572039843 + 0.9784927964 0.9663510919 0.9575207233 + 0.9787554145 0.9665859938 0.9578372836 + 0.9790171981 0.9668210745 0.9581537843 + 0.9792782068 0.9670563936 0.9584702253 + 0.9795383215 0.9672918916 0.9587864876 + 0.9797974825 0.9675275087 0.9591028094 + 0.9800559282 0.9677633047 0.9594188929 + 0.9803133011 0.9679992199 0.9597349763 + 0.9805697799 0.9682353735 0.9600510001 + 0.9808254242 0.9684717059 0.9603667855 + 0.9810799956 0.9687082171 0.9606825709 + 0.9813336730 0.9689447880 0.9609982967 + 0.9815863967 0.9691815972 0.9613139033 + 0.9818381071 0.9694185853 0.9616295099 + 0.9820888042 0.9696558118 0.9619448781 + 0.9823384285 0.9698930979 0.9622601867 + 0.9825869799 0.9701306224 0.9625753760 + 0.9828345180 0.9703683257 0.9628906250 + 0.9830809832 0.9706060886 0.9632055759 + 0.9833263755 0.9708440900 0.9635205865 + 0.9835705757 0.9710823298 0.9638354778 + 0.9838137031 0.9713206291 0.9641503096 + 0.9840556979 0.9715592265 0.9644649029 + 0.9842965007 0.9717978239 0.9647794962 + 0.9845361114 0.9720367193 0.9650939703 + 0.9847744703 0.9722756743 0.9654083848 + 0.9850116968 0.9725148082 0.9657226801 + 0.9852476716 0.9727541804 0.9660369754 + 0.9854825139 0.9729936719 0.9663510919 + 0.9857159853 0.9732332826 0.9666650891 + 0.9859482050 0.9734731913 0.9669790864 + 0.9861791134 0.9737132192 0.9672929049 + 0.9864087105 0.9739533067 0.9676066041 + 0.9866369963 0.9741935730 0.9679203033 + 0.9868639112 0.9744340777 0.9682338834 + 0.9870895147 0.9746747017 0.9685472846 + 0.9873136878 0.9749155045 0.9688606858 + 0.9875364900 0.9751564860 0.9691740274 + 0.9877579212 0.9753975868 0.9694871902 + 0.9879779220 0.9756389260 0.9698001742 + 0.9881964922 0.9758803844 0.9701132178 + 0.9884135723 0.9761220217 0.9704260826 + 0.9886292219 0.9763637185 0.9707388878 + 0.9888433218 0.9766055942 0.9710515738 + 0.9890559912 0.9768477082 0.9713642001 + 0.9892671108 0.9770898819 0.9716768265 + 0.9894766212 0.9773322940 0.9719892144 + 0.9896847010 0.9775748849 0.9723014832 + 0.9898911119 0.9778175950 0.9726136923 + 0.9900959730 0.9780604839 0.9729259014 + 0.9902992249 0.9783034921 0.9732378721 + 0.9905009270 0.9785466790 0.9735497832 + 0.9907009006 0.9787899852 0.9738616943 + 0.9908992052 0.9790335298 0.9741734266 + 0.9910959005 0.9792770743 0.9744849801 + 0.9912909269 0.9795209169 0.9747965932 + 0.9914842248 0.9797648787 0.9751080275 + 0.9916757941 0.9800090194 0.9754194021 + 0.9918656945 0.9802532196 0.9757305980 + 0.9920538068 0.9804975986 0.9760417938 + 0.9922400713 0.9807422161 0.9763528705 + 0.9924247265 0.9809868932 0.9766638279 + 0.9926074743 0.9812318087 0.9769747257 + 0.9927883744 0.9814767838 0.9772855043 + 0.9929674864 0.9817218781 0.9775961041 + 0.9931448102 0.9819672704 0.9779067039 + 0.9933202267 0.9822127223 0.9782171845 + 0.9934937954 0.9824582934 0.9785274863 + 0.9936653972 0.9827041030 0.9788377881 + 0.9938350916 0.9829499722 0.9791479707 + 0.9940028787 0.9831960797 0.9794579744 + 0.9941688180 0.9834423065 0.9797679782 + 0.9943326712 0.9836885929 0.9800779223 + 0.9944946170 0.9839351177 0.9803876877 + 0.9946544766 0.9841818213 0.9806972742 + 0.9948124290 0.9844285846 0.9810069203 + 0.9949682951 0.9846755266 0.9813163877 + 0.9951221943 0.9849225879 0.9816257954 + 0.9952740073 0.9851698279 0.9819350243 + 0.9954236746 0.9854171872 0.9822441936 + 0.9955713153 0.9856647253 0.9825533032 + 0.9957168102 0.9859123826 0.9828622937 + 0.9958602190 0.9861602187 0.9831711054 + 0.9960014820 0.9864081740 0.9834799170 + 0.9961404800 0.9866563082 0.9837886095 + 0.9962775111 0.9869045019 0.9840971828 + 0.9964122176 0.9871528745 0.9844055772 + 0.9965447187 0.9874014258 0.9847139716 + 0.9966750145 0.9876500964 0.9850223064 + 0.9968031049 0.9878988862 0.9853304029 + 0.9969288707 0.9881477952 0.9856384993 + 0.9970524907 0.9883968830 0.9859464169 + 0.9971737266 0.9886462092 0.9862542748 + 0.9972926974 0.9888954759 0.9865620136 + 0.9974092841 0.9891449809 0.9868696928 + 0.9975237250 0.9893947244 0.9871771932 + 0.9976356030 0.9896445274 0.9874846935 + 0.9977452159 0.9898943901 0.9877920151 + 0.9978523850 0.9901443720 0.9880992770 + 0.9979572296 0.9903945923 0.9884064198 + 0.9980595708 0.9906449914 0.9887135029 + 0.9981595278 0.9908953905 0.9890204072 + 0.9982569814 0.9911460280 0.9893271923 + 0.9983521104 0.9913967848 0.9896339178 + 0.9984446168 0.9916477203 0.9899405837 + 0.9985346794 0.9918987155 0.9902470708 + 0.9986221790 0.9921497703 0.9905534983 + 0.9987071753 0.9924011230 0.9908598065 + 0.9987897277 0.9926524758 0.9911659956 + 0.9988695979 0.9929041266 0.9914721251 + 0.9989469051 0.9931557178 0.9917780757 + 0.9990215898 0.9934074879 0.9920840263 + 0.9990937710 0.9936594963 0.9923897982 + 0.9991632104 0.9939116240 0.9926955104 + 0.9992300868 0.9941638112 0.9930009842 + 0.9992942810 0.9944161177 0.9933065176 + 0.9993557930 0.9946686029 0.9936118722 + 0.9994146228 0.9949212074 0.9939171076 + 0.9994707108 0.9951738715 0.9942222834 + 0.9995239973 0.9954267740 0.9945272803 + 0.9995747209 0.9956797957 0.9948322773 + 0.9996225238 0.9959328771 0.9951370955 + 0.9996675849 0.9961860776 0.9954419136 + 0.9997099042 0.9964395165 0.9957464933 + 0.9997494221 0.9966930151 0.9960510135 + 0.9997860789 0.9969465733 0.9963554144 + 0.9998199940 0.9972004294 0.9966596961 + 0.9998509288 0.9974542856 0.9969639182 + 0.9998790026 0.9977083206 0.9972680211 + 0.9999042153 0.9979624152 0.9975720048 + 0.9999266267 0.9982166886 0.9978759289 + 0.9999458790 0.9984710813 0.9981796145 + 0.9999623895 0.9987255931 0.9984833002 + 0.9999759197 0.9989802241 0.9987869263 + 0.9999864101 0.9992349744 0.9990903139 + 0.9999939799 0.9994899035 0.9993935823 + 0.9999985099 0.9997448921 0.9996969104 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3151.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3151.spi1d index 3fe7c939..db9a811d 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3151.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3151.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0037204761 0.0029971090 0.0034317160 - 0.0074409521 0.0059942179 0.0068634320 - 0.0112108104 0.0089913281 0.0103168199 - 0.0150894802 0.0121837398 0.0140005304 - 0.0189681500 0.0154752200 0.0176842492 - 0.0229456108 0.0187666994 0.0214565806 - 0.0269588791 0.0222466905 0.0253789201 - 0.0309982002 0.0258396305 0.0293012708 - 0.0351190008 0.0294325706 0.0334015414 - 0.0392398089 0.0332778692 0.0375403687 - 0.0434244089 0.0371704884 0.0417546108 - 0.0476234518 0.0411414988 0.0460793190 - 0.0518433899 0.0453211293 0.0504178591 - 0.0560905896 0.0495007597 0.0548906699 - 0.0603392683 0.0539112613 0.0593634807 - 0.0646051764 0.0583530813 0.0639266223 - 0.0688710883 0.0629366115 0.0685047507 - 0.0731304884 0.0676036403 0.0731234923 - 0.0773875490 0.0723571181 0.0777619109 - 0.0816316605 0.0772018284 0.0824085474 - 0.0858552009 0.0820983425 0.0870628431 - 0.0900777131 0.0870656669 0.0917078033 - 0.0942465588 0.0920592695 0.0963367820 - 0.0984154195 0.0970907733 0.1009529978 - 0.1025395989 0.1021251008 0.1055205986 - 0.1066365018 0.1071633995 0.1100865975 - 0.1107179970 0.1121815965 0.1145633981 - 0.1147293970 0.1171742007 0.1190401986 - 0.1187407970 0.1221275032 0.1234276965 - 0.1226869002 0.1270297021 0.1277907938 - 0.1266030073 0.1318825036 0.1320898980 - 0.1305056065 0.1366591007 0.1363231987 - 0.1343199015 0.1413903981 0.1405382007 - 0.1381341964 0.1460158974 0.1446315050 - 0.1418946981 0.1506181061 0.1487247944 - 0.1456034929 0.1550758928 0.1527182013 - 0.1493123025 0.1595336050 0.1566662937 - 0.1529341042 0.1638332009 0.1605917066 - 0.1565361023 0.1681143939 0.1643936932 - 0.1601340026 0.1722953022 0.1681956053 - 0.1636296958 0.1763972044 0.1719217002 - 0.1671254039 0.1804776043 0.1755793989 - 0.1706026942 0.1844025999 0.1792372018 - 0.1739941984 0.1883275062 0.1827841997 - 0.1773857027 0.1921540052 0.1863020957 - 0.1807540953 0.1959075928 0.1898200065 - 0.1840445995 0.1996611059 0.1932110041 - 0.1873351038 0.2032658011 0.1965949982 - 0.1906071007 0.2068558931 0.1999790967 - 0.1938004941 0.2104267031 0.2032371014 - 0.1969940066 0.2138625979 0.2064943016 - 0.2001820058 0.2172984928 0.2097516060 - 0.2032828927 0.2207036018 0.2128987014 - 0.2063837051 0.2239954025 0.2160367072 - 0.2094846070 0.2272873074 0.2191746980 - 0.2125124037 0.2305555940 0.2222305983 - 0.2155255973 0.2337138951 0.2252573073 - 0.2185387015 0.2368721068 0.2282838970 - 0.2215092927 0.2400290966 0.2312657982 - 0.2244399041 0.2430640012 0.2341890037 - 0.2273703963 0.2460989058 0.2371121943 - 0.2302929014 0.2491337955 0.2400342971 - 0.2331458032 0.2520878017 0.2428618073 - 0.2359987050 0.2550094128 0.2456893027 - 0.2388516068 0.2579309940 0.2485169023 - 0.2416612059 0.2608223855 0.2513025105 - 0.2444415987 0.2636403143 0.2540419102 - 0.2472219020 0.2664583027 0.2567811906 - 0.2500022054 0.2692762017 0.2595205903 - 0.2527149916 0.2720237970 0.2621931136 - 0.2554276884 0.2747471035 0.2648515105 - 0.2581403852 0.2774704993 0.2675099075 - 0.2608334124 0.2801876962 0.2701635957 - 0.2634831965 0.2828249037 0.2727478147 - 0.2661330998 0.2854621112 0.2753320038 - 0.2687830031 0.2880992889 0.2779161930 - 0.2714012861 0.2907147110 0.2804872990 - 0.2739928961 0.2932738066 0.2830036879 - 0.2765845060 0.2958329022 0.2855201960 - 0.2791759968 0.2983919084 0.2880367041 - 0.2817308009 0.3009246886 0.2905395031 - 0.2842684984 0.3034130037 0.2929942906 - 0.2868061066 0.3059012890 0.2954489887 - 0.2893437147 0.3083896041 0.2979038060 - 0.2918444872 0.3108552992 0.3003503084 - 0.2943322957 0.3132796884 0.3027490079 - 0.2968201041 0.3157039881 0.3051475883 - 0.2993079126 0.3181284070 0.3075462878 - 0.3017626107 0.3205395937 0.3099448979 - 0.3042044938 0.3229064047 0.3122938871 - 0.3066464067 0.3252730966 0.3146417141 - 0.3090882897 0.3276399076 0.3169896007 - 0.3115035892 0.3300065100 0.3193373978 - 0.3139032125 0.3323214948 0.3216522038 - 0.3163028061 0.3346365094 0.3239541054 - 0.3187023997 0.3369514942 0.3262560070 - 0.3210841119 0.3392665088 0.3285579085 - 0.3234448135 0.3415498137 0.3308443129 - 0.3258054852 0.3438183963 0.3331047893 - 0.3281661868 0.3460870087 0.3353652954 - 0.3305189013 0.3483555913 0.3376257122 - 0.3328438103 0.3506127894 0.3398861885 - 0.3351686895 0.3528400064 0.3421114087 - 0.3374935985 0.3550671935 0.3443346918 - 0.3398185074 0.3572942913 0.3465578854 - 0.3421131074 0.3595215082 0.3487811983 - 0.3444052041 0.3617197871 0.3509894013 - 0.3466972113 0.3639101088 0.3531793952 - 0.3489891887 0.3661004007 0.3553692997 - 0.3512642980 0.3682906926 0.3575592935 - 0.3535260856 0.3704738915 0.3597491980 - 0.3557879031 0.3726314902 0.3619127870 - 0.3580496907 0.3747890890 0.3640730083 - 0.3603076935 0.3769468069 0.3662331998 - 0.3625417054 0.3791044056 0.3683933914 - 0.3647755980 0.3812451065 0.3705467880 - 0.3670096099 0.3833738863 0.3726806045 - 0.3692435026 0.3855026960 0.3748143017 - 0.3714604974 0.3876315057 0.3769479990 - 0.3736687899 0.3897601962 0.3790816963 - 0.3758769929 0.3918665051 0.3812020123 - 0.3780852854 0.3939698935 0.3833121955 - 0.3802903891 0.3960733116 0.3854224086 - 0.3824748993 0.3981766999 0.3875325918 - 0.3846592903 0.4002771080 0.3896428049 - 0.3868438005 0.4023582935 0.3917357028 - 0.3890283108 0.4044393897 0.3938251138 - 0.3912005126 0.4065206051 0.3959144950 - 0.3933627903 0.4086017013 0.3980039060 - 0.3955250978 0.4106765091 0.4000923932 - 0.3976874053 0.4127382934 0.4021633863 - 0.3998498023 0.4148001075 0.4042344093 - 0.4019927979 0.4168618023 0.4063053131 - 0.4041343033 0.4189235866 0.4083763063 - 0.4062758982 0.4209772944 0.4104438126 - 0.4084174931 0.4230220914 0.4124985039 - 0.4105539024 0.4250670075 0.4145531058 - 0.4126757979 0.4271118045 0.4166077971 - 0.4147978127 0.4291566908 0.4186624885 - 0.4169197083 0.4311929047 0.4207122028 - 0.4190416038 0.4332230985 0.4227524102 - 0.4211533070 0.4352532029 0.4247925878 - 0.4232563972 0.4372833967 0.4268329144 - 0.4253596067 0.4393135011 0.4288730919 - 0.4274626970 0.4413351119 0.4309074879 - 0.4295659065 0.4433524013 0.4329349101 - 0.4316546023 0.4453696907 0.4349622130 - 0.4337396026 0.4473870099 0.4369894862 - 0.4358245134 0.4494042993 0.4390167892 - 0.4379095137 0.4514135122 0.4410381019 - 0.4399943948 0.4534195065 0.4430536926 - 0.4420616031 0.4554254115 0.4450694025 - 0.4441286922 0.4574314058 0.4470849931 - 0.4461956918 0.4594373107 0.4491006136 - 0.4482628107 0.4614359140 0.4511103034 - 0.4503270090 0.4634316862 0.4531151056 - 0.4523763061 0.4654273987 0.4551199973 - 0.4544255137 0.4674232006 0.4571247995 - 0.4564746916 0.4694189131 0.4591296911 - 0.4585238993 0.4714080989 0.4611288011 - 0.4605681002 0.4733943939 0.4631234109 - 0.4625990987 0.4753808081 0.4651181102 - 0.4646301866 0.4773671925 0.4671126902 - 0.4666613042 0.4793536067 0.4691073895 - 0.4686923921 0.4813339114 0.4710964859 - 0.4707168043 0.4833112955 0.4730812013 - 0.4727292955 0.4852887094 0.4750658870 - 0.4747416973 0.4872662127 0.4770506024 - 0.4767540991 0.4892435968 0.4790352881 - 0.4787665009 0.4912154973 0.4810147882 - 0.4807713032 0.4931839108 0.4829894006 - 0.4827642143 0.4951524138 0.4849640131 - 0.4847570956 0.4971208870 0.4869385958 - 0.4867500067 0.4990893006 0.4889132082 - 0.4887427986 0.5010527968 0.4908829927 - 0.4907281101 0.5030118823 0.4928469956 - 0.4927003086 0.5049710274 0.4948109984 - 0.4946725965 0.5069301128 0.4967750013 - 0.4966447949 0.5088893175 0.4987390041 - 0.4986169934 0.5108439922 0.5006989241 - 0.5005825758 0.5127928853 0.5026513934 - 0.5025327802 0.5147417784 0.5046039224 - 0.5044829845 0.5166907907 0.5065565109 - 0.5064331293 0.5186396837 0.5085089803 - 0.5083832741 0.5205851197 0.5104585290 - 0.5103294253 0.5225226283 0.5123981833 - 0.5122557878 0.5244600177 0.5143380165 - 0.5141823292 0.5263975263 0.5162776709 - 0.5161086917 0.5283349156 0.5182175040 - 0.5180351138 0.5302705169 0.5201560855 - 0.5199615955 0.5321947932 0.5220813751 - 0.5218629241 0.5341190100 0.5240067244 - 0.5237637162 0.5360432863 0.5259320140 - 0.5256645083 0.5379675031 0.5278573036 - 0.5275653005 0.5398917198 0.5297827125 - 0.5294660926 0.5418013930 0.5316932797 - 0.5313469172 0.5437102914 0.5336021781 - 0.5332199931 0.5456190705 0.5355110168 - 0.5350930095 0.5475279093 0.5374197960 - 0.5369660854 0.5494368076 0.5393285751 - 0.5388391018 0.5513328910 0.5412250757 - 0.5407006741 0.5532237291 0.5431150198 - 0.5425437093 0.5551146269 0.5450049043 - 0.5443866849 0.5570054054 0.5468947887 - 0.5462297201 0.5588961840 0.5487846732 - 0.5480726957 0.5607783198 0.5506668091 - 0.5499156117 0.5626481175 0.5525349975 - 0.5517275929 0.5645177960 0.5544031262 - 0.5535380840 0.5663875937 0.5562713146 - 0.5553485155 0.5682573915 0.5581395030 - 0.5571590066 0.5701255202 0.5600075126 - 0.5589694977 0.5719708800 0.5618509054 - 0.5607649088 0.5738161802 0.5636942983 - 0.5625402927 0.5756615996 0.5655375719 - 0.5643156767 0.5775070190 0.5673810244 - 0.5660911202 0.5793523192 0.5692244172 - 0.5678665042 0.5811793804 0.5710514784 - 0.5696418881 0.5829967260 0.5728666782 - 0.5713872910 0.5848138928 0.5746818781 - 0.5731251240 0.5866311789 0.5764971972 - 0.5748628974 0.5884484053 0.5783123970 - 0.5766006112 0.5902609825 0.5801253915 - 0.5783383846 0.5920462012 0.5819090009 - 0.5800744295 0.5938314199 0.5836926103 - 0.5817720294 0.5956165791 0.5854762197 - 0.5834695101 0.5974019170 0.5872597098 - 0.5851671100 0.5991870761 0.5890433192 - 0.5868647099 0.6009526253 0.5908104777 - 0.5885621905 0.6027017832 0.5925588012 - 0.5902531743 0.6044508815 0.5943070054 - 0.5919080973 0.6062000990 0.5960553288 - 0.5935629010 0.6079491973 0.5978035927 - 0.5952177048 0.6096982956 0.5995517969 - 0.5968725085 0.6114141941 0.6012710929 - 0.5985273123 0.6131231785 0.6029803753 - 0.6001772285 0.6148322225 0.6046897173 - 0.6017869115 0.6165410876 0.6063989997 - 0.6033965945 0.6182500720 0.6081082821 - 0.6050062776 0.6199591160 0.6098175049 - 0.6066160202 0.6216248870 0.6114888191 - 0.6082255840 0.6232897043 0.6131554842 - 0.6098353267 0.6249545217 0.6148222089 - 0.6114025116 0.6266192198 0.6164888144 - 0.6129649282 0.6282839775 0.6181554794 - 0.6145272255 0.6299487948 0.6198222041 - 0.6160895824 0.6315670013 0.6214476824 - 0.6176518798 0.6331837177 0.6230683923 - 0.6192142963 0.6348004937 0.6246889830 - 0.6207520962 0.6364172101 0.6263095737 - 0.6222651005 0.6380339265 0.6279302239 - 0.6237781048 0.6396507025 0.6295508146 - 0.6252911091 0.6412270069 0.6311358213 - 0.6268041134 0.6427921057 0.6327071190 - 0.6283171177 0.6443573236 0.6342784762 - 0.6298301220 0.6459224224 0.6358497739 - 0.6312978268 0.6474875808 0.6374210715 - 0.6327596903 0.6490526795 0.6389924288 - 0.6342216730 0.6505963206 0.6405450106 - 0.6356835961 0.6521067023 0.6420642138 - 0.6371455789 0.6536170840 0.6435832977 - 0.6386075020 0.6551274061 0.6451023817 - 0.6400669813 0.6566377878 0.6466215849 - 0.6414763927 0.6581482291 0.6481407285 - 0.6428859234 0.6596586108 0.6496598125 - 0.6442952752 0.6611245275 0.6511365175 - 0.6457048059 0.6625775099 0.6526008844 - 0.6471142769 0.6640303731 0.6540653110 - 0.6485236883 0.6654832959 0.6555296779 - 0.6499332190 0.6669362783 0.6569941044 - 0.6512916088 0.6683892012 0.6584585905 - 0.6526474953 0.6698421240 0.6599230170 - 0.6540033817 0.6712418795 0.6613336205 - 0.6553593278 0.6726351976 0.6627411842 - 0.6567150950 0.6740285158 0.6641489267 - 0.6580709815 0.6754218936 0.6655564904 - 0.6594269276 0.6768152118 0.6669641733 - 0.6607514024 0.6782084703 0.6683717966 - 0.6620529890 0.6796017885 0.6697794795 - 0.6633545756 0.6809514165 0.6711379290 - 0.6646561027 0.6822834015 0.6724871993 - 0.6659576893 0.6836155057 0.6738365293 - 0.6672592163 0.6849476099 0.6751857996 - 0.6685608029 0.6862797141 0.6765351295 - 0.6698623896 0.6876118183 0.6778843999 - 0.6711149812 0.6889439225 0.6792336702 - 0.6723617911 0.6902630925 0.6805573106 - 0.6736087203 0.6915329099 0.6818472147 - 0.6748554707 0.6928027868 0.6831371188 - 0.6761022806 0.6940726042 0.6844270229 - 0.6773492098 0.6953424811 0.6857169271 - 0.6785960197 0.6966122985 0.6870067716 - 0.6798428893 0.6978821754 0.6882966161 - 0.6810417771 0.6991519928 0.6895865202 - 0.6822339296 0.7004011273 0.6908357143 - 0.6834260225 0.7016083002 0.6920655966 - 0.6846179962 0.7028154731 0.6932954788 - 0.6858100891 0.7040227056 0.6945254803 - 0.6870021820 0.7052298784 0.6957554221 - 0.6881942153 0.7064371109 0.6969853044 - 0.6893863082 0.7076442838 0.6982151866 - 0.6905518770 0.7088515162 0.6994451880 - 0.6916894913 0.7100557089 0.7006422281 - 0.6928271055 0.7112004161 0.7018120885 - 0.6939646006 0.7123451233 0.7029820085 - 0.6951022148 0.7134897709 0.7041519284 - 0.6962398291 0.7146344781 0.7053217888 - 0.6973773837 0.7157791853 0.7064917088 - 0.6985148787 0.7169238925 0.7076616883 - 0.6996524930 0.7180685997 0.7088316083 - 0.7007526159 0.7192133069 0.7100014091 - 0.7018362880 0.7203385830 0.7111117244 - 0.7029200196 0.7214214802 0.7122220993 - 0.7040035725 0.7225043178 0.7133324146 - 0.7050873041 0.7235872149 0.7144426703 - 0.7061709762 0.7246699929 0.7155529857 - 0.7072547078 0.7257528901 0.7166634202 - 0.7083383203 0.7268357873 0.7177736759 - 0.7094219923 0.7279186249 0.7188839912 - 0.7104809284 0.7290015221 0.7199943066 - 0.7115116119 0.7300795913 0.7210462093 - 0.7125421762 0.7311016917 0.7220978141 - 0.7135729194 0.7321239114 0.7231494188 - 0.7146034837 0.7331460118 0.7242009044 - 0.7156342268 0.7341681719 0.7252525091 - 0.7166647911 0.7351902723 0.7263041139 - 0.7176954746 0.7362124920 0.7273557186 - 0.7187260985 0.7372345924 0.7284073234 - 0.7197567225 0.7382566929 0.7294588089 - 0.7207477093 0.7392789125 0.7304825187 - 0.7217264771 0.7402836084 0.7314764857 - 0.7227051854 0.7412465811 0.7324705720 - 0.7236840129 0.7422094941 0.7334647179 - 0.7246627808 0.7431725264 0.7344586849 - 0.7256414890 0.7441354990 0.7354527712 - 0.7266203165 0.7450984120 0.7364467978 - 0.7275990248 0.7460613847 0.7374408841 - 0.7285777926 0.7470242977 0.7384349704 - 0.7295565009 0.7479872704 0.7394289970 - 0.7305076122 0.7489503026 0.7403993011 - 0.7314357758 0.7499132156 0.7413372993 - 0.7323639989 0.7508239746 0.7422754169 - 0.7332922220 0.7517297268 0.7432134748 - 0.7342203856 0.7526353002 0.7441515923 - 0.7351486087 0.7535409927 0.7450897098 - 0.7360768914 0.7544466257 0.7460278273 - 0.7370051146 0.7553523183 0.7469658852 - 0.7379332781 0.7562578917 0.7479040027 - 0.7388615012 0.7571635842 0.7488421202 - 0.7397897243 0.7580692172 0.7497801781 - 0.7406799793 0.7589749098 0.7506768107 - 0.7415592074 0.7598804832 0.7515608072 - 0.7424383759 0.7607383132 0.7524446845 - 0.7433176041 0.7615888119 0.7533286810 - 0.7441967130 0.7624391913 0.7542126179 - 0.7450758815 0.7632896900 0.7550964952 - 0.7459551096 0.7641401887 0.7559804916 - 0.7468342781 0.7649906874 0.7568644285 - 0.7477135062 0.7658411264 0.7577484250 - 0.7485926747 0.7666916251 0.7586323023 - 0.7494719028 0.7675421238 0.7595162988 - 0.7503321767 0.7683925033 0.7603765726 - 0.7511640787 0.7692430019 0.7612084746 - 0.7519959211 0.7700877190 0.7620403171 - 0.7528278232 0.7708852887 0.7628720999 - 0.7536597252 0.7716829181 0.7637040019 - 0.7544915080 0.7724806070 0.7645357847 - 0.7553234100 0.7732781768 0.7653676271 - 0.7561553121 0.7740758061 0.7661995292 - 0.7569870949 0.7748734951 0.7670313120 - 0.7578189969 0.7756711245 0.7678630948 - 0.7586507797 0.7764686942 0.7686949968 - 0.7594826818 0.7772663832 0.7695267797 - 0.7602974176 0.7780640125 0.7703371048 - 0.7610837221 0.7788615823 0.7711189985 - 0.7618700266 0.7796592712 0.7719008923 - 0.7626562715 0.7804281116 0.7726829052 - 0.7634426951 0.7811753154 0.7734647989 - 0.7642289996 0.7819225788 0.7742466927 - 0.7650153041 0.7826699018 0.7750285864 - 0.7658017278 0.7834171057 0.7758104801 - 0.7665879726 0.7841644287 0.7765923738 - 0.7673742771 0.7849116921 0.7773743272 - 0.7681607008 0.7856588960 0.7781562805 - 0.7689470053 0.7864062190 0.7789381742 - 0.7697333097 0.7871534824 0.7797201276 - 0.7704908252 0.7879006863 0.7804713845 - 0.7712334991 0.7886480093 0.7812057137 - 0.7719761729 0.7893952727 0.7819399834 - 0.7727187872 0.7901334167 0.7826743126 - 0.7734615207 0.7908328772 0.7834085822 - 0.7742041945 0.7915323973 0.7841429114 - 0.7749469280 0.7922317982 0.7848771811 - 0.7756894827 0.7929313183 0.7856115103 - 0.7764322162 0.7936307788 0.7863457799 - 0.7771748900 0.7943302989 0.7870801091 - 0.7779176235 0.7950296998 0.7878143787 - 0.7786601782 0.7957292199 0.7885487080 - 0.7794029117 0.7964286804 0.7892829776 - 0.7801374197 0.7971280813 0.7900161743 - 0.7808383703 0.7978276014 0.7907052040 - 0.7815393209 0.7985271215 0.7913942933 - 0.7822402716 0.7992265224 0.7920833230 - 0.7829412222 0.7999259830 0.7927722931 - 0.7836421728 0.8005850911 0.7934613824 - 0.7843431234 0.8012393117 0.7941504121 - 0.7850440741 0.8018935919 0.7948395014 - 0.7857450843 0.8025478721 0.7955284715 - 0.7864459753 0.8032020926 0.7962175012 - 0.7871469855 0.8038563728 0.7969065905 - 0.7878478765 0.8045107126 0.7975956202 - 0.7885488868 0.8051649928 0.7982845902 - 0.7892497778 0.8058192134 0.7989736795 - 0.7899507880 0.8064734936 0.7996627092 - 0.7906147838 0.8071277738 0.8003299236 - 0.7912759781 0.8077819943 0.8009759784 - 0.7919371128 0.8084362745 0.8016222119 - 0.7925983071 0.8090906143 0.8022683859 - 0.7932595015 0.8097448945 0.8029146194 - 0.7939206958 0.8103731275 0.8035606742 - 0.7945818901 0.8109847903 0.8042069077 - 0.7952430844 0.8115965128 0.8048530817 - 0.7959042788 0.8122081757 0.8054991961 - 0.7965654731 0.8128197789 0.8061454296 - 0.7972267270 0.8134315014 0.8067916036 - 0.7978879213 0.8140432239 0.8074377775 - 0.7985491157 0.8146548271 0.8080838919 - 0.7992103100 0.8152664900 0.8087301254 - 0.7998715043 0.8158782125 0.8093762994 - 0.8005021811 0.8164898157 0.8100209832 - 0.8011255860 0.8171014786 0.8106266856 - 0.8017489910 0.8177132010 0.8112323880 - 0.8023723960 0.8183249235 0.8118380904 - 0.8029958010 0.8189365268 0.8124437928 - 0.8036192060 0.8195481896 0.8130494952 - 0.8042424917 0.8201494217 0.8136551976 - 0.8048658967 0.8207209706 0.8142609000 - 0.8054893017 0.8212926984 0.8148664832 - 0.8061127067 0.8218643069 0.8154721856 - 0.8067361116 0.8224359155 0.8160778880 - 0.8073595166 0.8230075836 0.8166835904 - 0.8079829216 0.8235791922 0.8172892928 - 0.8086063266 0.8241508007 0.8178949952 - 0.8092296720 0.8247225285 0.8185006976 - 0.8098530769 0.8252940774 0.8191064000 - 0.8104491234 0.8258656859 0.8197119832 - 0.8110365868 0.8264374137 0.8202977180 - 0.8116241097 0.8270090222 0.8208652735 - 0.8122116923 0.8275805712 0.8214328289 - 0.8127992153 0.8281521797 0.8220003843 - 0.8133867979 0.8287239075 0.8225678802 - 0.8139743209 0.8292955160 0.8231354952 - 0.8145617843 0.8298671246 0.8237029910 - 0.8151494265 0.8304100037 0.8242706060 - 0.8157368898 0.8309440017 0.8248381019 - 0.8163244724 0.8314781189 0.8254057169 - 0.8169119954 0.8320121765 0.8259732127 - 0.8174995184 0.8325462937 0.8265408278 - 0.8180871010 0.8330804110 0.8271083236 - 0.8186746240 0.8336145282 0.8276758790 - 0.8192622066 0.8341485858 0.8282433748 - 0.8198497295 0.8346827030 0.8288109899 - 0.8204119802 0.8352168202 0.8293784857 - 0.8209655881 0.8357508779 0.8299461007 - 0.8215191960 0.8362848759 0.8304811716 - 0.8220728040 0.8368189931 0.8310129046 - 0.8226264119 0.8373531103 0.8315445781 - 0.8231800199 0.8378872275 0.8320763111 - 0.8237336278 0.8384212852 0.8326079845 - 0.8242871761 0.8389554024 0.8331397176 - 0.8248407841 0.8394895196 0.8336713910 - 0.8253943920 0.8400220275 0.8342031240 - 0.8259480000 0.8405209780 0.8347347975 - 0.8265016079 0.8410199881 0.8352664709 - 0.8270552158 0.8415188789 0.8357982039 - 0.8276088238 0.8420178890 0.8363298774 - 0.8281623721 0.8425168991 0.8368616104 - 0.8287159801 0.8430157900 0.8373932838 - 0.8292695880 0.8435148001 0.8379250169 - 0.8298231959 0.8440138102 0.8384566903 - 0.8303549886 0.8445127010 0.8389884233 - 0.8308765292 0.8450117111 0.8395200968 - 0.8313980103 0.8455106020 0.8400486112 - 0.8319196105 0.8460096121 0.8405466080 - 0.8324410915 0.8465086222 0.8410447240 - 0.8329625726 0.8470075130 0.8415427804 - 0.8334841132 0.8475065231 0.8420407772 - 0.8340057135 0.8480054736 0.8425388932 - 0.8345271945 0.8485044241 0.8430370092 - 0.8350486755 0.8490033746 0.8435351253 - 0.8355702758 0.8495023847 0.8440331221 - 0.8360918164 0.8500012159 0.8445311785 - 0.8366132975 0.8504673839 0.8450292945 - 0.8371348977 0.8509336114 0.8455272913 - 0.8376563787 0.8513997197 0.8460254073 - 0.8381779194 0.8518658876 0.8465235233 - 0.8386995196 0.8523319960 0.8470215201 - 0.8392210007 0.8527982235 0.8475195765 - 0.8397424817 0.8532642722 0.8480176926 - 0.8402487040 0.8537304997 0.8485158086 - 0.8407400250 0.8541966081 0.8490138054 - 0.8412312865 0.8546627760 0.8495119214 - 0.8417226076 0.8551290035 0.8500093222 - 0.8422138095 0.8555951118 0.8504759073 - 0.8427050710 0.8560612798 0.8509424925 - 0.8431963921 0.8565273881 0.8514090180 - 0.8436875939 0.8569936156 0.8518756032 - 0.8441789150 0.8574597239 0.8523421884 - 0.8446701765 0.8579258919 0.8528087735 - 0.8451614976 0.8583920002 0.8532752991 - 0.8456526995 0.8588582277 0.8537418842 - 0.8461440206 0.8593243957 0.8542085290 - 0.8466352820 0.8597905040 0.8546749949 - 0.8471266031 0.8602398038 0.8551415801 - 0.8476178050 0.8606753945 0.8556082249 - 0.8481091261 0.8611109257 0.8560746908 - 0.8486003876 0.8615465164 0.8565412760 - 0.8490915895 0.8619819880 0.8570079207 - 0.8495829105 0.8624175787 0.8574743867 - 0.8500698805 0.8628532290 0.8579409719 - 0.8505327106 0.8632887006 0.8584076166 - 0.8509954214 0.8637242913 0.8588742018 - 0.8514581919 0.8641598225 0.8593407273 - 0.8519209027 0.8645954132 0.8598073125 - 0.8523836732 0.8650308847 0.8602566123 - 0.8528465033 0.8654664755 0.8606936932 - 0.8533092141 0.8659020066 0.8611307740 - 0.8537719846 0.8663375974 0.8615679145 - 0.8542348146 0.8667731285 0.8620049953 - 0.8546975255 0.8672087193 0.8624420762 - 0.8551602960 0.8676443100 0.8628792167 - 0.8556230068 0.8680797815 0.8633162975 - 0.8560857773 0.8685153723 0.8637533784 - 0.8565486073 0.8689509034 0.8641905189 - 0.8570113182 0.8693864942 0.8646275997 - 0.8574740887 0.8698220253 0.8650646806 - 0.8579369187 0.8702406883 0.8655018210 - 0.8583996296 0.8706477880 0.8659389019 - 0.8588624001 0.8710548282 0.8663759828 - 0.8593251705 0.8714619279 0.8668131232 - 0.8597878814 0.8718689084 0.8672502041 - 0.8602361083 0.8722760081 0.8676872849 - 0.8606721163 0.8726829886 0.8681244254 - 0.8611080050 0.8730900884 0.8685615063 - 0.8615438938 0.8734971285 0.8689985871 - 0.8619799018 0.8739041090 0.8694357276 - 0.8624157906 0.8743112087 0.8698728085 - 0.8628516793 0.8747181892 0.8702903986 - 0.8632876873 0.8751252890 0.8706998825 - 0.8637235761 0.8755323291 0.8711094856 - 0.8641595244 0.8759394288 0.8715190887 - 0.8645954728 0.8763464093 0.8719285727 - 0.8650314212 0.8767535090 0.8723381758 - 0.8654673100 0.8771604896 0.8727477789 - 0.8659031987 0.8775675893 0.8731573224 - 0.8663392067 0.8779746294 0.8735669255 - 0.8667750955 0.8783817291 0.8739765286 - 0.8672109842 0.8787887096 0.8743860126 - 0.8676469922 0.8791958094 0.8747956157 - 0.8680828810 0.8796027899 0.8752052188 - 0.8685188293 0.8800091743 0.8756147027 - 0.8689547777 0.8803896904 0.8760243058 - 0.8693907261 0.8807702065 0.8764339089 - 0.8698266149 0.8811507821 0.8768433928 - 0.8702474236 0.8815312982 0.8772529960 - 0.8706580997 0.8819118142 0.8776625991 - 0.8710687757 0.8822922707 0.8780720830 - 0.8714795113 0.8826727867 0.8784816861 - 0.8718901873 0.8830533028 0.8788912892 - 0.8723009229 0.8834338188 0.8793007731 - 0.8727115989 0.8838143945 0.8797103763 - 0.8731222749 0.8841949105 0.8801124096 - 0.8735330105 0.8845754266 0.8804963231 - 0.8739436865 0.8849558830 0.8808801770 - 0.8743544221 0.8853363991 0.8812639713 - 0.8747650981 0.8857169151 0.8816478848 - 0.8751757741 0.8860973716 0.8820317984 - 0.8755865097 0.8864778876 0.8824155927 - 0.8759971857 0.8868585229 0.8827995062 - 0.8764079213 0.8872389793 0.8831834197 - 0.8768185973 0.8876194954 0.8835672140 - 0.8772292733 0.8880000114 0.8839511275 - 0.8776400089 0.8883805275 0.8843349218 - 0.8780506849 0.8887609839 0.8847187757 - 0.8784614205 0.8891415000 0.8851026893 - 0.8788720965 0.8895220757 0.8854864836 - 0.8792827725 0.8899025917 0.8858703971 - 0.8796935081 0.8902646899 0.8862543106 - 0.8800982237 0.8906205893 0.8866381049 - 0.8804851770 0.8909764290 0.8870220184 - 0.8808721900 0.8913322091 0.8874058127 - 0.8812592030 0.8916881084 0.8877897263 - 0.8816462159 0.8920438886 0.8881735802 - 0.8820331097 0.8923997879 0.8885573745 - 0.8824201226 0.8927556276 0.8889412880 - 0.8828070760 0.8931114078 0.8893252015 - 0.8831940889 0.8934673071 0.8897089958 - 0.8835811019 0.8938230872 0.8900871277 - 0.8839681149 0.8941789269 0.8904470205 - 0.8843551278 0.8945348263 0.8908069134 - 0.8847420812 0.8948906064 0.8911666870 - 0.8851290941 0.8952463865 0.8915265799 - 0.8855161071 0.8956022859 0.8918864727 - 0.8859031200 0.8959581256 0.8922464252 - 0.8862900734 0.8963140249 0.8926063180 - 0.8866770864 0.8966698050 0.8929662108 - 0.8870640993 0.8970255852 0.8933259845 - 0.8874511123 0.8973814845 0.8936858773 - 0.8878381252 0.8977373242 0.8940458298 - 0.8882250786 0.8980931044 0.8944057226 - 0.8886120915 0.8984490037 0.8947656155 - 0.8889991045 0.8988047838 0.8951255083 - 0.8893861175 0.8991606832 0.8954852819 - 0.8897730708 0.8995165229 0.8958451748 - 0.8901509047 0.8998723030 0.8962051272 - 0.8905156255 0.9002134800 0.8965650201 - 0.8908804059 0.9005463719 0.8969249129 - 0.8912451267 0.9008792043 0.8972848058 - 0.8916099072 0.9012120962 0.8976445794 - 0.8919746280 0.9015449882 0.8980044723 - 0.8923392892 0.9018778801 0.8983644247 - 0.8927041292 0.9022107720 0.8987243176 - 0.8930687904 0.9025437236 0.8990842104 - 0.8934335709 0.9028766155 0.8994441032 - 0.8937982917 0.9032095075 0.8998038769 - 0.8941630721 0.9035423994 0.9001536965 - 0.8945277929 0.9038752913 0.9004911780 - 0.8948925138 0.9042081833 0.9008287191 - 0.8952572942 0.9045410752 0.9011662006 - 0.8956220150 0.9048740268 0.9015038013 - 0.8959867954 0.9052069187 0.9018412828 - 0.8963515162 0.9055398107 0.9021788239 - 0.8967161775 0.9058727026 0.9025163054 - 0.8970810175 0.9062055945 0.9028539062 - 0.8974456787 0.9065384865 0.9031913877 - 0.8978105187 0.9068713784 0.9035289288 - 0.8981751800 0.9072042704 0.9038664103 - 0.8985400200 0.9075372219 0.9042040110 - 0.8989046812 0.9078701138 0.9045414925 - 0.8992694020 0.9082030058 0.9048789740 - 0.8996341825 0.9085358977 0.9052165747 - 0.8999989033 0.9088687897 0.9055541158 - 0.9003428221 0.9092016816 0.9058915973 - 0.9006866813 0.9095345736 0.9062290788 - 0.9010306001 0.9098675251 0.9065666795 - 0.9013743997 0.9101876020 0.9069042206 - 0.9017183185 0.9104992151 0.9072417021 - 0.9020621181 0.9108107090 0.9075791836 - 0.9024059772 0.9111223221 0.9079167843 - 0.9027498960 0.9114338756 0.9082543254 - 0.9030936956 0.9117454886 0.9085918069 - 0.9034376144 0.9120571017 0.9089292884 - 0.9037814140 0.9123687148 0.9092668891 - 0.9041252732 0.9126802087 0.9096043706 - 0.9044691920 0.9129918218 0.9099419117 - 0.9048129916 0.9133033752 0.9102622271 - 0.9051569104 0.9136149883 0.9105789065 - 0.9055007100 0.9139266014 0.9108955860 - 0.9058446288 0.9142382145 0.9112123251 - 0.9061884284 0.9145497084 0.9115288854 - 0.9065322876 0.9148613214 0.9118456244 - 0.9068762064 0.9151728749 0.9121623039 - 0.9072200060 0.9154844880 0.9124789834 - 0.9075639248 0.9157961011 0.9127957225 - 0.9079077244 0.9161077142 0.9131124020 - 0.9082515836 0.9164192080 0.9134290814 - 0.9085955024 0.9167308211 0.9137458205 - 0.9089393020 0.9170423746 0.9140625000 - 0.9092832208 0.9173539877 0.9143791199 - 0.9096270204 0.9176656008 0.9146957994 - 0.9099708796 0.9179772139 0.9150124788 - 0.9102967978 0.9182887077 0.9153292179 - 0.9106211066 0.9186003208 0.9156458974 - 0.9109454155 0.9189118743 0.9159625769 - 0.9112696052 0.9192234874 0.9162793159 - 0.9115939140 0.9195351005 0.9165959954 - 0.9119182229 0.9198467135 0.9169126749 - 0.9122424126 0.9201481938 0.9172292948 - 0.9125667214 0.9204400182 0.9175459743 - 0.9128909707 0.9207317829 0.9178627133 - 0.9132152200 0.9210234880 0.9181793928 - 0.9135395288 0.9213153124 0.9184960723 - 0.9138637781 0.9216070771 0.9188128114 - 0.9141880274 0.9218989015 0.9191294909 - 0.9145122766 0.9221907258 0.9194461703 - 0.9148365855 0.9224824905 0.9197629094 - 0.9151608944 0.9227741957 0.9200747013 - 0.9154850841 0.9230660200 0.9203718901 - 0.9158093929 0.9233577847 0.9206691980 - 0.9161337018 0.9236496091 0.9209665060 - 0.9164578915 0.9239413738 0.9212638140 - 0.9167822003 0.9242331982 0.9215610027 - 0.9171065092 0.9245249033 0.9218583107 - 0.9174306989 0.9248167276 0.9221556187 - 0.9177550077 0.9251084924 0.9224528074 - 0.9180793166 0.9254003167 0.9227501154 - 0.9184035063 0.9256920815 0.9230474234 - 0.9187278152 0.9259837866 0.9233446717 - 0.9190521240 0.9262756109 0.9236419201 - 0.9193763733 0.9265673757 0.9239392281 - 0.9197006226 0.9268592000 0.9242364764 - 0.9200235009 0.9271510243 0.9245337844 - 0.9203293920 0.9274427891 0.9248309731 - 0.9206352830 0.9277344942 0.9251282811 - 0.9209411740 0.9280263186 0.9254255891 - 0.9212471247 0.9283180833 0.9257227778 - 0.9215530157 0.9286099076 0.9260200858 - 0.9218589067 0.9289016724 0.9263173938 - 0.9221647978 0.9291934967 0.9266147017 - 0.9224706888 0.9294852018 0.9269118905 - 0.9227765799 0.9297770262 0.9272091985 - 0.9230824709 0.9300644994 0.9275065064 - 0.9233884215 0.9303379059 0.9278036952 - 0.9236943126 0.9306111932 0.9281010032 - 0.9240002036 0.9308845997 0.9283983111 - 0.9243060946 0.9311580062 0.9286956191 - 0.9246119857 0.9314314127 0.9289928079 - 0.9249178767 0.9317048192 0.9292901158 - 0.9252238274 0.9319782257 0.9295874238 - 0.9255297184 0.9322515726 0.9298846722 - 0.9258356094 0.9325249791 0.9301708937 - 0.9261415005 0.9327983856 0.9304500222 - 0.9264473915 0.9330717921 0.9307292104 - 0.9267532825 0.9333450794 0.9310083985 - 0.9270591736 0.9336184859 0.9312875867 - 0.9273651242 0.9338918924 0.9315667748 - 0.9276710153 0.9341652989 0.9318460226 - 0.9279769063 0.9344387054 0.9321250916 - 0.9282827973 0.9347121119 0.9324042797 - 0.9285886884 0.9349855185 0.9326835275 - 0.9288945794 0.9352589250 0.9329627156 - 0.9292004704 0.9355322719 0.9332419038 - 0.9295064211 0.9358056188 0.9335210919 - 0.9298123121 0.9360790253 0.9338002205 - 0.9301115274 0.9363523722 0.9340794086 - 0.9304001927 0.9366257787 0.9343585968 - 0.9306889176 0.9368991852 0.9346377850 - 0.9309775829 0.9371725917 0.9349169731 - 0.9312661886 0.9374459982 0.9351962209 - 0.9315549135 0.9377194047 0.9354752898 - 0.9318435788 0.9379928112 0.9357544780 - 0.9321323037 0.9382662177 0.9360337257 - 0.9324209094 0.9385395050 0.9363129139 - 0.9327095747 0.9388129115 0.9365921021 - 0.9329982996 0.9390863180 0.9368712902 - 0.9332870245 0.9393597245 0.9371504188 - 0.9335756898 0.9396330714 0.9374296069 - 0.9338642955 0.9399064779 0.9377087951 - 0.9341530204 0.9401686192 0.9379879832 - 0.9344416857 0.9404249191 0.9382671714 - 0.9347304106 0.9406812787 0.9385464191 - 0.9350190163 0.9409375787 0.9388254881 - 0.9353076816 0.9411938787 0.9391046762 - 0.9355964065 0.9414501786 0.9393839240 - 0.9358850718 0.9417064786 0.9396631122 - 0.9361737967 0.9419627786 0.9399423003 - 0.9364624023 0.9422190785 0.9402080774 - 0.9367511272 0.9424753785 0.9404703975 - 0.9370397925 0.9427316785 0.9407327771 - 0.9373285174 0.9429879785 0.9409950972 - 0.9376171827 0.9432442784 0.9412574172 - 0.9379057884 0.9435005784 0.9415196776 - 0.9381945133 0.9437568784 0.9417821169 - 0.9384831786 0.9440131783 0.9420443773 - 0.9387719035 0.9442694783 0.9423066974 - 0.9390605092 0.9445257783 0.9425690770 - 0.9393491745 0.9447820783 0.9428313971 - 0.9396378994 0.9450383782 0.9430937171 - 0.9399266243 0.9452946782 0.9433560967 - 0.9402031898 0.9455509782 0.9436184168 - 0.9404757023 0.9458072782 0.9438806772 - 0.9407482743 0.9460635781 0.9441431165 - 0.9410207868 0.9463198781 0.9444053769 - 0.9412932992 0.9465761781 0.9446676970 - 0.9415658712 0.9468324780 0.9449300170 - 0.9418383837 0.9470888972 0.9451923966 - 0.9421108961 0.9473451972 0.9454547167 - 0.9423835278 0.9476014972 0.9457169771 - 0.9426559806 0.9478577971 0.9459794164 - 0.9429286122 0.9481140971 0.9462416768 - 0.9432011247 0.9483703971 0.9465039968 - 0.9434735775 0.9486266971 0.9467663765 - 0.9437462091 0.9488829970 0.9470286965 - 0.9440187216 0.9491392970 0.9472910166 - 0.9442911744 0.9493955970 0.9475533962 - 0.9445638061 0.9496518970 0.9478157163 - 0.9448363185 0.9499081969 0.9480779767 - 0.9451087713 0.9501543045 0.9483402967 - 0.9453814030 0.9503946900 0.9486026764 - 0.9456539154 0.9506351948 0.9488649964 - 0.9459264278 0.9508755803 0.9491273165 - 0.9461989999 0.9511160254 0.9493896961 - 0.9464715123 0.9513564706 0.9496520162 - 0.9467440248 0.9515969157 0.9499142766 - 0.9470165968 0.9518373013 0.9501661062 - 0.9472891092 0.9520778060 0.9504126906 - 0.9475616217 0.9523181915 0.9506592751 - 0.9478341937 0.9525585771 0.9509059787 - 0.9481067061 0.9527990818 0.9511526227 - 0.9483792186 0.9530395269 0.9513992071 - 0.9486517906 0.9532799125 0.9516459107 - 0.9489243031 0.9535202980 0.9518924952 - 0.9491968155 0.9537608027 0.9521390796 - 0.9494693875 0.9540011883 0.9523857832 - 0.9497419000 0.9542415738 0.9526324272 - 0.9500135779 0.9544820786 0.9528790116 - 0.9502710104 0.9547225237 0.9531255960 - 0.9505283833 0.9549629092 0.9533722997 - 0.9507858157 0.9552034140 0.9536188841 - 0.9510431886 0.9554437995 0.9538655281 - 0.9513006210 0.9556841850 0.9541121721 - 0.9515579939 0.9559246898 0.9543588161 - 0.9518154263 0.9561650753 0.9546054006 - 0.9520727992 0.9564055204 0.9548519850 - 0.9523301721 0.9566460252 0.9550986886 - 0.9525876045 0.9568864107 0.9553452730 - 0.9528449774 0.9571267962 0.9555919170 - 0.9531024098 0.9573671818 0.9558386207 - 0.9533597827 0.9576076865 0.9560852051 - 0.9536172152 0.9578480721 0.9563317895 - 0.9538745880 0.9580885172 0.9565784931 - 0.9541320205 0.9583290219 0.9568250775 - 0.9543893933 0.9585694075 0.9570717216 - 0.9546468258 0.9588097930 0.9573183060 - 0.9549041986 0.9590502977 0.9575650096 - 0.9551615715 0.9592906833 0.9578115940 - 0.9554190040 0.9595311284 0.9580581784 - 0.9556763768 0.9597715735 0.9583048820 - 0.9559338093 0.9600113034 0.9585515261 - 0.9561911821 0.9602369070 0.9587981105 - 0.9564486146 0.9604626298 0.9590448141 - 0.9567059875 0.9606882930 0.9592913985 - 0.9569634199 0.9609140158 0.9595379829 - 0.9572207928 0.9611396790 0.9597846270 - 0.9574782252 0.9613652825 0.9600294232 - 0.9577355981 0.9615910053 0.9602614045 - 0.9579929709 0.9618167281 0.9604933858 - 0.9582504034 0.9620423913 0.9607254267 - 0.9585077763 0.9622681141 0.9609574080 - 0.9587652087 0.9624937773 0.9611893892 - 0.9590225816 0.9627193809 0.9614213705 - 0.9592800140 0.9629451036 0.9616534114 - 0.9595373869 0.9631708264 0.9618853927 - 0.9597948194 0.9633964896 0.9621173739 - 0.9600493908 0.9636222124 0.9623494148 - 0.9602925777 0.9638478160 0.9625813961 - 0.9605358243 0.9640734792 0.9628133774 - 0.9607790112 0.9642992020 0.9630454183 - 0.9610221982 0.9645249248 0.9632773995 - 0.9612653852 0.9647505879 0.9635093808 - 0.9615085721 0.9649763107 0.9637414217 - 0.9617518187 0.9652019143 0.9639734030 - 0.9619951248 0.9654275775 0.9642053843 - 0.9622383118 0.9656533003 0.9644374251 - 0.9624814987 0.9658790231 0.9646694064 - 0.9627246857 0.9661046863 0.9649013877 - 0.9629678726 0.9663304090 0.9651334286 - 0.9632111192 0.9665560126 0.9653654099 - 0.9634543061 0.9667816758 0.9655973911 - 0.9636976123 0.9670073986 0.9658293724 - 0.9639407992 0.9672331214 0.9660614133 - 0.9641839862 0.9674587846 0.9662933946 - 0.9644271731 0.9676843882 0.9665253758 - 0.9646704197 0.9679101110 0.9667574167 - 0.9649136066 0.9681357741 0.9669893980 - 0.9651567936 0.9683614969 0.9672213793 - 0.9654000998 0.9685872197 0.9674534202 - 0.9656432867 0.9688128829 0.9676854014 - 0.9658864737 0.9690384865 0.9679173827 - 0.9661297202 0.9692642093 0.9681494236 - 0.9663729072 0.9694898725 0.9683814049 - 0.9666160941 0.9697155952 0.9686133862 - 0.9668592811 0.9699413180 0.9688454270 - 0.9671025276 0.9701567888 0.9690774083 - 0.9673457742 0.9703688025 0.9693093896 - 0.9675890207 0.9705808163 0.9695413709 - 0.9678322077 0.9707927108 0.9697734118 - 0.9680753946 0.9710047245 0.9700049758 - 0.9683185816 0.9712166786 0.9702234268 - 0.9685618281 0.9714285731 0.9704418182 - 0.9688050151 0.9716405869 0.9706600904 - 0.9690483212 0.9718526006 0.9708784819 - 0.9692915082 0.9720646143 0.9710968733 - 0.9695346951 0.9722765088 0.9713152051 - 0.9697778821 0.9724885225 0.9715335965 - 0.9700199962 0.9727004766 0.9717519283 - 0.9702498913 0.9729124904 0.9719703197 - 0.9704797864 0.9731243849 0.9721887112 - 0.9707096815 0.9733363986 0.9724069834 - 0.9709395766 0.9735484123 0.9726253748 - 0.9711694717 0.9737603068 0.9728438258 - 0.9713994265 0.9739723206 0.9730620980 - 0.9716293812 0.9741842747 0.9732804894 - 0.9718592763 0.9743962884 0.9734988213 - 0.9720891714 0.9746081829 0.9737172127 - 0.9723191261 0.9748201966 0.9739356041 - 0.9725490212 0.9750322104 0.9741538763 - 0.9727789164 0.9752442241 0.9743723273 - 0.9730088115 0.9754561186 0.9745907187 - 0.9732388258 0.9756680727 0.9748089910 - 0.9734687209 0.9758800864 0.9750273824 - 0.9736986160 0.9760921001 0.9752457142 - 0.9739285111 0.9763039947 0.9754641056 - 0.9741584063 0.9765160084 0.9756824970 - 0.9743883014 0.9767280221 0.9759008288 - 0.9746181965 0.9769399166 0.9761192203 - 0.9748480916 0.9771518707 0.9763376117 - 0.9750781059 0.9773638844 0.9765558839 - 0.9753080010 0.9775758982 0.9767742753 - 0.9755378962 0.9777877927 0.9769926071 - 0.9757677913 0.9779998064 0.9772109985 - 0.9759976864 0.9782118201 0.9774293900 - 0.9762275815 0.9784237742 0.9776477218 - 0.9764574766 0.9786357284 0.9778661132 - 0.9766874909 0.9788476825 0.9780845046 - 0.9769173861 0.9790596962 0.9783027768 - 0.9771472812 0.9792717099 0.9785212278 - 0.9773771763 0.9794836044 0.9787395000 - 0.9776070714 0.9796956182 0.9789578915 - 0.9778370261 0.9799075723 0.9791762829 - 0.9780669212 0.9801123738 0.9793946147 - 0.9782968760 0.9803115726 0.9796130061 - 0.9785267711 0.9805107713 0.9798313975 - 0.9787567258 0.9807100296 0.9800468087 - 0.9789866209 0.9809092879 0.9802525043 - 0.9792165160 0.9811084867 0.9804580808 - 0.9794464111 0.9813076854 0.9806637764 - 0.9796763062 0.9815068841 0.9808694124 - 0.9799063206 0.9817062020 0.9810751081 - 0.9801288247 0.9819054008 0.9812806845 - 0.9803462029 0.9821045995 0.9814863801 - 0.9805637002 0.9823039174 0.9816920161 - 0.9807810783 0.9825031161 0.9818977118 - 0.9809985757 0.9827023149 0.9821032882 - 0.9812160134 0.9829015136 0.9823089838 - 0.9814333916 0.9831007719 0.9825146198 - 0.9816508889 0.9832999706 0.9827203155 - 0.9818683267 0.9834992290 0.9829258919 - 0.9820858240 0.9836984277 0.9831315875 - 0.9823032022 0.9838976860 0.9833372235 - 0.9825206995 0.9840968847 0.9835429192 - 0.9827380776 0.9842960835 0.9837484956 - 0.9829555154 0.9844954014 0.9839541912 - 0.9831730127 0.9846946001 0.9841598272 - 0.9833903909 0.9848937988 0.9843655229 - 0.9836078882 0.9850929976 0.9845710993 - 0.9838253260 0.9852923155 0.9847767949 - 0.9840428233 0.9854915142 0.9849823713 - 0.9842602015 0.9856907129 0.9851881266 - 0.9844775796 0.9858899117 0.9853937030 - 0.9846950769 0.9860892296 0.9855993986 - 0.9849125147 0.9862884283 0.9858049750 - 0.9851300120 0.9864876270 0.9860106707 - 0.9853473902 0.9866868854 0.9862163067 - 0.9855648875 0.9868860841 0.9864220023 - 0.9857823253 0.9870852828 0.9866275787 - 0.9859997034 0.9872844815 0.9868332744 - 0.9862172008 0.9874837995 0.9870389104 - 0.9864345789 0.9876829982 0.9872446060 - 0.9866520762 0.9878821969 0.9874501824 - 0.9868695140 0.9880813956 0.9876558781 - 0.9870870113 0.9882807136 0.9878615141 - 0.9873043895 0.9884799123 0.9880672097 - 0.9875218272 0.9886791110 0.9882727861 - 0.9877393246 0.9888783097 0.9884784818 - 0.9879567027 0.9890776277 0.9886841178 - 0.9881742001 0.9892768264 0.9888898134 - 0.9883915782 0.9894760251 0.9890953898 - 0.9886090755 0.9896752834 0.9893010855 - 0.9888265133 0.9898744822 0.9895067215 - 0.9890438914 0.9900693297 0.9897124171 - 0.9892613888 0.9902567267 0.9899179935 - 0.9894788265 0.9904441237 0.9901164770 - 0.9896963239 0.9906314015 0.9903103113 - 0.9899137020 0.9908187985 0.9905040860 - 0.9901241064 0.9910061955 0.9906979203 - 0.9903299212 0.9911935925 0.9908916950 - 0.9905356169 0.9913808703 0.9910855293 - 0.9907413721 0.9915683270 0.9912793040 - 0.9909471273 0.9917557240 0.9914730787 - 0.9911528230 0.9919430017 0.9916669130 - 0.9913585782 0.9921303988 0.9918606877 - 0.9915642738 0.9923177958 0.9920545220 - 0.9917700887 0.9925051928 0.9922482967 - 0.9919757843 0.9926924706 0.9924420118 - 0.9921815991 0.9928799272 0.9926357865 - 0.9923872948 0.9930673242 0.9928296208 - 0.9925931096 0.9932546020 0.9930233955 - 0.9927988052 0.9934419990 0.9932171702 - 0.9930046201 0.9936293960 0.9934110045 - 0.9932103157 0.9938166738 0.9936047792 - 0.9934160709 0.9940040708 0.9937986135 - 0.9936218262 0.9941915274 0.9939923882 - 0.9938275814 0.9943789244 0.9941862226 - 0.9940332770 0.9945662022 0.9943799973 - 0.9942390919 0.9947535992 0.9945737720 - 0.9944447875 0.9949409962 0.9947676063 - 0.9946506023 0.9951282740 0.9949613810 - 0.9948562980 0.9953156710 0.9951552153 - 0.9950621128 0.9955031276 0.9953489900 - 0.9952678084 0.9956905246 0.9955427051 - 0.9954735041 0.9958778024 0.9957364798 - 0.9956793189 0.9960651994 0.9959303141 - 0.9958850145 0.9962525964 0.9961240888 - 0.9960908294 0.9964398742 0.9963179231 - 0.9962965250 0.9966272712 0.9965116978 - 0.9965022802 0.9968147278 0.9967054725 - 0.9967079759 0.9970021248 0.9968993068 - 0.9969137907 0.9971894026 0.9970930815 - 0.9971194863 0.9973767996 0.9972869158 - 0.9973253012 0.9975641966 0.9974806905 - 0.9975309968 0.9977514744 0.9976745248 - 0.9977368116 0.9979388714 0.9978682995 - 0.9979425073 0.9981263280 0.9980620742 - 0.9981483221 0.9983137250 0.9982559085 - 0.9983540177 0.9985010028 0.9984496832 - 0.9985597730 0.9986883998 0.9986433983 - 0.9987655282 0.9988757968 0.9988371730 - 0.9989712834 0.9990630746 0.9990310073 - 0.9991769791 0.9992504716 0.9992247820 - 0.9993827939 0.9994379282 0.9994186163 - 0.9995884895 0.9996253252 0.9996123910 - 0.9997943044 0.9998126030 0.9998062253 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0037204761 0.0029971090 0.0034317160 + 0.0074409521 0.0059942179 0.0068634320 + 0.0112108104 0.0089913281 0.0103168199 + 0.0150894802 0.0121837398 0.0140005304 + 0.0189681500 0.0154752200 0.0176842492 + 0.0229456108 0.0187666994 0.0214565806 + 0.0269588791 0.0222466905 0.0253789201 + 0.0309982002 0.0258396305 0.0293012708 + 0.0351190008 0.0294325706 0.0334015414 + 0.0392398089 0.0332778692 0.0375403687 + 0.0434244089 0.0371704884 0.0417546108 + 0.0476234518 0.0411414988 0.0460793190 + 0.0518433899 0.0453211293 0.0504178591 + 0.0560905896 0.0495007597 0.0548906699 + 0.0603392683 0.0539112613 0.0593634807 + 0.0646051764 0.0583530813 0.0639266223 + 0.0688710883 0.0629366115 0.0685047507 + 0.0731304884 0.0676036403 0.0731234923 + 0.0773875490 0.0723571181 0.0777619109 + 0.0816316605 0.0772018284 0.0824085474 + 0.0858552009 0.0820983425 0.0870628431 + 0.0900777131 0.0870656669 0.0917078033 + 0.0942465588 0.0920592695 0.0963367820 + 0.0984154195 0.0970907733 0.1009529978 + 0.1025395989 0.1021251008 0.1055205986 + 0.1066365018 0.1071633995 0.1100865975 + 0.1107179970 0.1121815965 0.1145633981 + 0.1147293970 0.1171742007 0.1190401986 + 0.1187407970 0.1221275032 0.1234276965 + 0.1226869002 0.1270297021 0.1277907938 + 0.1266030073 0.1318825036 0.1320898980 + 0.1305056065 0.1366591007 0.1363231987 + 0.1343199015 0.1413903981 0.1405382007 + 0.1381341964 0.1460158974 0.1446315050 + 0.1418946981 0.1506181061 0.1487247944 + 0.1456034929 0.1550758928 0.1527182013 + 0.1493123025 0.1595336050 0.1566662937 + 0.1529341042 0.1638332009 0.1605917066 + 0.1565361023 0.1681143939 0.1643936932 + 0.1601340026 0.1722953022 0.1681956053 + 0.1636296958 0.1763972044 0.1719217002 + 0.1671254039 0.1804776043 0.1755793989 + 0.1706026942 0.1844025999 0.1792372018 + 0.1739941984 0.1883275062 0.1827841997 + 0.1773857027 0.1921540052 0.1863020957 + 0.1807540953 0.1959075928 0.1898200065 + 0.1840445995 0.1996611059 0.1932110041 + 0.1873351038 0.2032658011 0.1965949982 + 0.1906071007 0.2068558931 0.1999790967 + 0.1938004941 0.2104267031 0.2032371014 + 0.1969940066 0.2138625979 0.2064943016 + 0.2001820058 0.2172984928 0.2097516060 + 0.2032828927 0.2207036018 0.2128987014 + 0.2063837051 0.2239954025 0.2160367072 + 0.2094846070 0.2272873074 0.2191746980 + 0.2125124037 0.2305555940 0.2222305983 + 0.2155255973 0.2337138951 0.2252573073 + 0.2185387015 0.2368721068 0.2282838970 + 0.2215092927 0.2400290966 0.2312657982 + 0.2244399041 0.2430640012 0.2341890037 + 0.2273703963 0.2460989058 0.2371121943 + 0.2302929014 0.2491337955 0.2400342971 + 0.2331458032 0.2520878017 0.2428618073 + 0.2359987050 0.2550094128 0.2456893027 + 0.2388516068 0.2579309940 0.2485169023 + 0.2416612059 0.2608223855 0.2513025105 + 0.2444415987 0.2636403143 0.2540419102 + 0.2472219020 0.2664583027 0.2567811906 + 0.2500022054 0.2692762017 0.2595205903 + 0.2527149916 0.2720237970 0.2621931136 + 0.2554276884 0.2747471035 0.2648515105 + 0.2581403852 0.2774704993 0.2675099075 + 0.2608334124 0.2801876962 0.2701635957 + 0.2634831965 0.2828249037 0.2727478147 + 0.2661330998 0.2854621112 0.2753320038 + 0.2687830031 0.2880992889 0.2779161930 + 0.2714012861 0.2907147110 0.2804872990 + 0.2739928961 0.2932738066 0.2830036879 + 0.2765845060 0.2958329022 0.2855201960 + 0.2791759968 0.2983919084 0.2880367041 + 0.2817308009 0.3009246886 0.2905395031 + 0.2842684984 0.3034130037 0.2929942906 + 0.2868061066 0.3059012890 0.2954489887 + 0.2893437147 0.3083896041 0.2979038060 + 0.2918444872 0.3108552992 0.3003503084 + 0.2943322957 0.3132796884 0.3027490079 + 0.2968201041 0.3157039881 0.3051475883 + 0.2993079126 0.3181284070 0.3075462878 + 0.3017626107 0.3205395937 0.3099448979 + 0.3042044938 0.3229064047 0.3122938871 + 0.3066464067 0.3252730966 0.3146417141 + 0.3090882897 0.3276399076 0.3169896007 + 0.3115035892 0.3300065100 0.3193373978 + 0.3139032125 0.3323214948 0.3216522038 + 0.3163028061 0.3346365094 0.3239541054 + 0.3187023997 0.3369514942 0.3262560070 + 0.3210841119 0.3392665088 0.3285579085 + 0.3234448135 0.3415498137 0.3308443129 + 0.3258054852 0.3438183963 0.3331047893 + 0.3281661868 0.3460870087 0.3353652954 + 0.3305189013 0.3483555913 0.3376257122 + 0.3328438103 0.3506127894 0.3398861885 + 0.3351686895 0.3528400064 0.3421114087 + 0.3374935985 0.3550671935 0.3443346918 + 0.3398185074 0.3572942913 0.3465578854 + 0.3421131074 0.3595215082 0.3487811983 + 0.3444052041 0.3617197871 0.3509894013 + 0.3466972113 0.3639101088 0.3531793952 + 0.3489891887 0.3661004007 0.3553692997 + 0.3512642980 0.3682906926 0.3575592935 + 0.3535260856 0.3704738915 0.3597491980 + 0.3557879031 0.3726314902 0.3619127870 + 0.3580496907 0.3747890890 0.3640730083 + 0.3603076935 0.3769468069 0.3662331998 + 0.3625417054 0.3791044056 0.3683933914 + 0.3647755980 0.3812451065 0.3705467880 + 0.3670096099 0.3833738863 0.3726806045 + 0.3692435026 0.3855026960 0.3748143017 + 0.3714604974 0.3876315057 0.3769479990 + 0.3736687899 0.3897601962 0.3790816963 + 0.3758769929 0.3918665051 0.3812020123 + 0.3780852854 0.3939698935 0.3833121955 + 0.3802903891 0.3960733116 0.3854224086 + 0.3824748993 0.3981766999 0.3875325918 + 0.3846592903 0.4002771080 0.3896428049 + 0.3868438005 0.4023582935 0.3917357028 + 0.3890283108 0.4044393897 0.3938251138 + 0.3912005126 0.4065206051 0.3959144950 + 0.3933627903 0.4086017013 0.3980039060 + 0.3955250978 0.4106765091 0.4000923932 + 0.3976874053 0.4127382934 0.4021633863 + 0.3998498023 0.4148001075 0.4042344093 + 0.4019927979 0.4168618023 0.4063053131 + 0.4041343033 0.4189235866 0.4083763063 + 0.4062758982 0.4209772944 0.4104438126 + 0.4084174931 0.4230220914 0.4124985039 + 0.4105539024 0.4250670075 0.4145531058 + 0.4126757979 0.4271118045 0.4166077971 + 0.4147978127 0.4291566908 0.4186624885 + 0.4169197083 0.4311929047 0.4207122028 + 0.4190416038 0.4332230985 0.4227524102 + 0.4211533070 0.4352532029 0.4247925878 + 0.4232563972 0.4372833967 0.4268329144 + 0.4253596067 0.4393135011 0.4288730919 + 0.4274626970 0.4413351119 0.4309074879 + 0.4295659065 0.4433524013 0.4329349101 + 0.4316546023 0.4453696907 0.4349622130 + 0.4337396026 0.4473870099 0.4369894862 + 0.4358245134 0.4494042993 0.4390167892 + 0.4379095137 0.4514135122 0.4410381019 + 0.4399943948 0.4534195065 0.4430536926 + 0.4420616031 0.4554254115 0.4450694025 + 0.4441286922 0.4574314058 0.4470849931 + 0.4461956918 0.4594373107 0.4491006136 + 0.4482628107 0.4614359140 0.4511103034 + 0.4503270090 0.4634316862 0.4531151056 + 0.4523763061 0.4654273987 0.4551199973 + 0.4544255137 0.4674232006 0.4571247995 + 0.4564746916 0.4694189131 0.4591296911 + 0.4585238993 0.4714080989 0.4611288011 + 0.4605681002 0.4733943939 0.4631234109 + 0.4625990987 0.4753808081 0.4651181102 + 0.4646301866 0.4773671925 0.4671126902 + 0.4666613042 0.4793536067 0.4691073895 + 0.4686923921 0.4813339114 0.4710964859 + 0.4707168043 0.4833112955 0.4730812013 + 0.4727292955 0.4852887094 0.4750658870 + 0.4747416973 0.4872662127 0.4770506024 + 0.4767540991 0.4892435968 0.4790352881 + 0.4787665009 0.4912154973 0.4810147882 + 0.4807713032 0.4931839108 0.4829894006 + 0.4827642143 0.4951524138 0.4849640131 + 0.4847570956 0.4971208870 0.4869385958 + 0.4867500067 0.4990893006 0.4889132082 + 0.4887427986 0.5010527968 0.4908829927 + 0.4907281101 0.5030118823 0.4928469956 + 0.4927003086 0.5049710274 0.4948109984 + 0.4946725965 0.5069301128 0.4967750013 + 0.4966447949 0.5088893175 0.4987390041 + 0.4986169934 0.5108439922 0.5006989241 + 0.5005825758 0.5127928853 0.5026513934 + 0.5025327802 0.5147417784 0.5046039224 + 0.5044829845 0.5166907907 0.5065565109 + 0.5064331293 0.5186396837 0.5085089803 + 0.5083832741 0.5205851197 0.5104585290 + 0.5103294253 0.5225226283 0.5123981833 + 0.5122557878 0.5244600177 0.5143380165 + 0.5141823292 0.5263975263 0.5162776709 + 0.5161086917 0.5283349156 0.5182175040 + 0.5180351138 0.5302705169 0.5201560855 + 0.5199615955 0.5321947932 0.5220813751 + 0.5218629241 0.5341190100 0.5240067244 + 0.5237637162 0.5360432863 0.5259320140 + 0.5256645083 0.5379675031 0.5278573036 + 0.5275653005 0.5398917198 0.5297827125 + 0.5294660926 0.5418013930 0.5316932797 + 0.5313469172 0.5437102914 0.5336021781 + 0.5332199931 0.5456190705 0.5355110168 + 0.5350930095 0.5475279093 0.5374197960 + 0.5369660854 0.5494368076 0.5393285751 + 0.5388391018 0.5513328910 0.5412250757 + 0.5407006741 0.5532237291 0.5431150198 + 0.5425437093 0.5551146269 0.5450049043 + 0.5443866849 0.5570054054 0.5468947887 + 0.5462297201 0.5588961840 0.5487846732 + 0.5480726957 0.5607783198 0.5506668091 + 0.5499156117 0.5626481175 0.5525349975 + 0.5517275929 0.5645177960 0.5544031262 + 0.5535380840 0.5663875937 0.5562713146 + 0.5553485155 0.5682573915 0.5581395030 + 0.5571590066 0.5701255202 0.5600075126 + 0.5589694977 0.5719708800 0.5618509054 + 0.5607649088 0.5738161802 0.5636942983 + 0.5625402927 0.5756615996 0.5655375719 + 0.5643156767 0.5775070190 0.5673810244 + 0.5660911202 0.5793523192 0.5692244172 + 0.5678665042 0.5811793804 0.5710514784 + 0.5696418881 0.5829967260 0.5728666782 + 0.5713872910 0.5848138928 0.5746818781 + 0.5731251240 0.5866311789 0.5764971972 + 0.5748628974 0.5884484053 0.5783123970 + 0.5766006112 0.5902609825 0.5801253915 + 0.5783383846 0.5920462012 0.5819090009 + 0.5800744295 0.5938314199 0.5836926103 + 0.5817720294 0.5956165791 0.5854762197 + 0.5834695101 0.5974019170 0.5872597098 + 0.5851671100 0.5991870761 0.5890433192 + 0.5868647099 0.6009526253 0.5908104777 + 0.5885621905 0.6027017832 0.5925588012 + 0.5902531743 0.6044508815 0.5943070054 + 0.5919080973 0.6062000990 0.5960553288 + 0.5935629010 0.6079491973 0.5978035927 + 0.5952177048 0.6096982956 0.5995517969 + 0.5968725085 0.6114141941 0.6012710929 + 0.5985273123 0.6131231785 0.6029803753 + 0.6001772285 0.6148322225 0.6046897173 + 0.6017869115 0.6165410876 0.6063989997 + 0.6033965945 0.6182500720 0.6081082821 + 0.6050062776 0.6199591160 0.6098175049 + 0.6066160202 0.6216248870 0.6114888191 + 0.6082255840 0.6232897043 0.6131554842 + 0.6098353267 0.6249545217 0.6148222089 + 0.6114025116 0.6266192198 0.6164888144 + 0.6129649282 0.6282839775 0.6181554794 + 0.6145272255 0.6299487948 0.6198222041 + 0.6160895824 0.6315670013 0.6214476824 + 0.6176518798 0.6331837177 0.6230683923 + 0.6192142963 0.6348004937 0.6246889830 + 0.6207520962 0.6364172101 0.6263095737 + 0.6222651005 0.6380339265 0.6279302239 + 0.6237781048 0.6396507025 0.6295508146 + 0.6252911091 0.6412270069 0.6311358213 + 0.6268041134 0.6427921057 0.6327071190 + 0.6283171177 0.6443573236 0.6342784762 + 0.6298301220 0.6459224224 0.6358497739 + 0.6312978268 0.6474875808 0.6374210715 + 0.6327596903 0.6490526795 0.6389924288 + 0.6342216730 0.6505963206 0.6405450106 + 0.6356835961 0.6521067023 0.6420642138 + 0.6371455789 0.6536170840 0.6435832977 + 0.6386075020 0.6551274061 0.6451023817 + 0.6400669813 0.6566377878 0.6466215849 + 0.6414763927 0.6581482291 0.6481407285 + 0.6428859234 0.6596586108 0.6496598125 + 0.6442952752 0.6611245275 0.6511365175 + 0.6457048059 0.6625775099 0.6526008844 + 0.6471142769 0.6640303731 0.6540653110 + 0.6485236883 0.6654832959 0.6555296779 + 0.6499332190 0.6669362783 0.6569941044 + 0.6512916088 0.6683892012 0.6584585905 + 0.6526474953 0.6698421240 0.6599230170 + 0.6540033817 0.6712418795 0.6613336205 + 0.6553593278 0.6726351976 0.6627411842 + 0.6567150950 0.6740285158 0.6641489267 + 0.6580709815 0.6754218936 0.6655564904 + 0.6594269276 0.6768152118 0.6669641733 + 0.6607514024 0.6782084703 0.6683717966 + 0.6620529890 0.6796017885 0.6697794795 + 0.6633545756 0.6809514165 0.6711379290 + 0.6646561027 0.6822834015 0.6724871993 + 0.6659576893 0.6836155057 0.6738365293 + 0.6672592163 0.6849476099 0.6751857996 + 0.6685608029 0.6862797141 0.6765351295 + 0.6698623896 0.6876118183 0.6778843999 + 0.6711149812 0.6889439225 0.6792336702 + 0.6723617911 0.6902630925 0.6805573106 + 0.6736087203 0.6915329099 0.6818472147 + 0.6748554707 0.6928027868 0.6831371188 + 0.6761022806 0.6940726042 0.6844270229 + 0.6773492098 0.6953424811 0.6857169271 + 0.6785960197 0.6966122985 0.6870067716 + 0.6798428893 0.6978821754 0.6882966161 + 0.6810417771 0.6991519928 0.6895865202 + 0.6822339296 0.7004011273 0.6908357143 + 0.6834260225 0.7016083002 0.6920655966 + 0.6846179962 0.7028154731 0.6932954788 + 0.6858100891 0.7040227056 0.6945254803 + 0.6870021820 0.7052298784 0.6957554221 + 0.6881942153 0.7064371109 0.6969853044 + 0.6893863082 0.7076442838 0.6982151866 + 0.6905518770 0.7088515162 0.6994451880 + 0.6916894913 0.7100557089 0.7006422281 + 0.6928271055 0.7112004161 0.7018120885 + 0.6939646006 0.7123451233 0.7029820085 + 0.6951022148 0.7134897709 0.7041519284 + 0.6962398291 0.7146344781 0.7053217888 + 0.6973773837 0.7157791853 0.7064917088 + 0.6985148787 0.7169238925 0.7076616883 + 0.6996524930 0.7180685997 0.7088316083 + 0.7007526159 0.7192133069 0.7100014091 + 0.7018362880 0.7203385830 0.7111117244 + 0.7029200196 0.7214214802 0.7122220993 + 0.7040035725 0.7225043178 0.7133324146 + 0.7050873041 0.7235872149 0.7144426703 + 0.7061709762 0.7246699929 0.7155529857 + 0.7072547078 0.7257528901 0.7166634202 + 0.7083383203 0.7268357873 0.7177736759 + 0.7094219923 0.7279186249 0.7188839912 + 0.7104809284 0.7290015221 0.7199943066 + 0.7115116119 0.7300795913 0.7210462093 + 0.7125421762 0.7311016917 0.7220978141 + 0.7135729194 0.7321239114 0.7231494188 + 0.7146034837 0.7331460118 0.7242009044 + 0.7156342268 0.7341681719 0.7252525091 + 0.7166647911 0.7351902723 0.7263041139 + 0.7176954746 0.7362124920 0.7273557186 + 0.7187260985 0.7372345924 0.7284073234 + 0.7197567225 0.7382566929 0.7294588089 + 0.7207477093 0.7392789125 0.7304825187 + 0.7217264771 0.7402836084 0.7314764857 + 0.7227051854 0.7412465811 0.7324705720 + 0.7236840129 0.7422094941 0.7334647179 + 0.7246627808 0.7431725264 0.7344586849 + 0.7256414890 0.7441354990 0.7354527712 + 0.7266203165 0.7450984120 0.7364467978 + 0.7275990248 0.7460613847 0.7374408841 + 0.7285777926 0.7470242977 0.7384349704 + 0.7295565009 0.7479872704 0.7394289970 + 0.7305076122 0.7489503026 0.7403993011 + 0.7314357758 0.7499132156 0.7413372993 + 0.7323639989 0.7508239746 0.7422754169 + 0.7332922220 0.7517297268 0.7432134748 + 0.7342203856 0.7526353002 0.7441515923 + 0.7351486087 0.7535409927 0.7450897098 + 0.7360768914 0.7544466257 0.7460278273 + 0.7370051146 0.7553523183 0.7469658852 + 0.7379332781 0.7562578917 0.7479040027 + 0.7388615012 0.7571635842 0.7488421202 + 0.7397897243 0.7580692172 0.7497801781 + 0.7406799793 0.7589749098 0.7506768107 + 0.7415592074 0.7598804832 0.7515608072 + 0.7424383759 0.7607383132 0.7524446845 + 0.7433176041 0.7615888119 0.7533286810 + 0.7441967130 0.7624391913 0.7542126179 + 0.7450758815 0.7632896900 0.7550964952 + 0.7459551096 0.7641401887 0.7559804916 + 0.7468342781 0.7649906874 0.7568644285 + 0.7477135062 0.7658411264 0.7577484250 + 0.7485926747 0.7666916251 0.7586323023 + 0.7494719028 0.7675421238 0.7595162988 + 0.7503321767 0.7683925033 0.7603765726 + 0.7511640787 0.7692430019 0.7612084746 + 0.7519959211 0.7700877190 0.7620403171 + 0.7528278232 0.7708852887 0.7628720999 + 0.7536597252 0.7716829181 0.7637040019 + 0.7544915080 0.7724806070 0.7645357847 + 0.7553234100 0.7732781768 0.7653676271 + 0.7561553121 0.7740758061 0.7661995292 + 0.7569870949 0.7748734951 0.7670313120 + 0.7578189969 0.7756711245 0.7678630948 + 0.7586507797 0.7764686942 0.7686949968 + 0.7594826818 0.7772663832 0.7695267797 + 0.7602974176 0.7780640125 0.7703371048 + 0.7610837221 0.7788615823 0.7711189985 + 0.7618700266 0.7796592712 0.7719008923 + 0.7626562715 0.7804281116 0.7726829052 + 0.7634426951 0.7811753154 0.7734647989 + 0.7642289996 0.7819225788 0.7742466927 + 0.7650153041 0.7826699018 0.7750285864 + 0.7658017278 0.7834171057 0.7758104801 + 0.7665879726 0.7841644287 0.7765923738 + 0.7673742771 0.7849116921 0.7773743272 + 0.7681607008 0.7856588960 0.7781562805 + 0.7689470053 0.7864062190 0.7789381742 + 0.7697333097 0.7871534824 0.7797201276 + 0.7704908252 0.7879006863 0.7804713845 + 0.7712334991 0.7886480093 0.7812057137 + 0.7719761729 0.7893952727 0.7819399834 + 0.7727187872 0.7901334167 0.7826743126 + 0.7734615207 0.7908328772 0.7834085822 + 0.7742041945 0.7915323973 0.7841429114 + 0.7749469280 0.7922317982 0.7848771811 + 0.7756894827 0.7929313183 0.7856115103 + 0.7764322162 0.7936307788 0.7863457799 + 0.7771748900 0.7943302989 0.7870801091 + 0.7779176235 0.7950296998 0.7878143787 + 0.7786601782 0.7957292199 0.7885487080 + 0.7794029117 0.7964286804 0.7892829776 + 0.7801374197 0.7971280813 0.7900161743 + 0.7808383703 0.7978276014 0.7907052040 + 0.7815393209 0.7985271215 0.7913942933 + 0.7822402716 0.7992265224 0.7920833230 + 0.7829412222 0.7999259830 0.7927722931 + 0.7836421728 0.8005850911 0.7934613824 + 0.7843431234 0.8012393117 0.7941504121 + 0.7850440741 0.8018935919 0.7948395014 + 0.7857450843 0.8025478721 0.7955284715 + 0.7864459753 0.8032020926 0.7962175012 + 0.7871469855 0.8038563728 0.7969065905 + 0.7878478765 0.8045107126 0.7975956202 + 0.7885488868 0.8051649928 0.7982845902 + 0.7892497778 0.8058192134 0.7989736795 + 0.7899507880 0.8064734936 0.7996627092 + 0.7906147838 0.8071277738 0.8003299236 + 0.7912759781 0.8077819943 0.8009759784 + 0.7919371128 0.8084362745 0.8016222119 + 0.7925983071 0.8090906143 0.8022683859 + 0.7932595015 0.8097448945 0.8029146194 + 0.7939206958 0.8103731275 0.8035606742 + 0.7945818901 0.8109847903 0.8042069077 + 0.7952430844 0.8115965128 0.8048530817 + 0.7959042788 0.8122081757 0.8054991961 + 0.7965654731 0.8128197789 0.8061454296 + 0.7972267270 0.8134315014 0.8067916036 + 0.7978879213 0.8140432239 0.8074377775 + 0.7985491157 0.8146548271 0.8080838919 + 0.7992103100 0.8152664900 0.8087301254 + 0.7998715043 0.8158782125 0.8093762994 + 0.8005021811 0.8164898157 0.8100209832 + 0.8011255860 0.8171014786 0.8106266856 + 0.8017489910 0.8177132010 0.8112323880 + 0.8023723960 0.8183249235 0.8118380904 + 0.8029958010 0.8189365268 0.8124437928 + 0.8036192060 0.8195481896 0.8130494952 + 0.8042424917 0.8201494217 0.8136551976 + 0.8048658967 0.8207209706 0.8142609000 + 0.8054893017 0.8212926984 0.8148664832 + 0.8061127067 0.8218643069 0.8154721856 + 0.8067361116 0.8224359155 0.8160778880 + 0.8073595166 0.8230075836 0.8166835904 + 0.8079829216 0.8235791922 0.8172892928 + 0.8086063266 0.8241508007 0.8178949952 + 0.8092296720 0.8247225285 0.8185006976 + 0.8098530769 0.8252940774 0.8191064000 + 0.8104491234 0.8258656859 0.8197119832 + 0.8110365868 0.8264374137 0.8202977180 + 0.8116241097 0.8270090222 0.8208652735 + 0.8122116923 0.8275805712 0.8214328289 + 0.8127992153 0.8281521797 0.8220003843 + 0.8133867979 0.8287239075 0.8225678802 + 0.8139743209 0.8292955160 0.8231354952 + 0.8145617843 0.8298671246 0.8237029910 + 0.8151494265 0.8304100037 0.8242706060 + 0.8157368898 0.8309440017 0.8248381019 + 0.8163244724 0.8314781189 0.8254057169 + 0.8169119954 0.8320121765 0.8259732127 + 0.8174995184 0.8325462937 0.8265408278 + 0.8180871010 0.8330804110 0.8271083236 + 0.8186746240 0.8336145282 0.8276758790 + 0.8192622066 0.8341485858 0.8282433748 + 0.8198497295 0.8346827030 0.8288109899 + 0.8204119802 0.8352168202 0.8293784857 + 0.8209655881 0.8357508779 0.8299461007 + 0.8215191960 0.8362848759 0.8304811716 + 0.8220728040 0.8368189931 0.8310129046 + 0.8226264119 0.8373531103 0.8315445781 + 0.8231800199 0.8378872275 0.8320763111 + 0.8237336278 0.8384212852 0.8326079845 + 0.8242871761 0.8389554024 0.8331397176 + 0.8248407841 0.8394895196 0.8336713910 + 0.8253943920 0.8400220275 0.8342031240 + 0.8259480000 0.8405209780 0.8347347975 + 0.8265016079 0.8410199881 0.8352664709 + 0.8270552158 0.8415188789 0.8357982039 + 0.8276088238 0.8420178890 0.8363298774 + 0.8281623721 0.8425168991 0.8368616104 + 0.8287159801 0.8430157900 0.8373932838 + 0.8292695880 0.8435148001 0.8379250169 + 0.8298231959 0.8440138102 0.8384566903 + 0.8303549886 0.8445127010 0.8389884233 + 0.8308765292 0.8450117111 0.8395200968 + 0.8313980103 0.8455106020 0.8400486112 + 0.8319196105 0.8460096121 0.8405466080 + 0.8324410915 0.8465086222 0.8410447240 + 0.8329625726 0.8470075130 0.8415427804 + 0.8334841132 0.8475065231 0.8420407772 + 0.8340057135 0.8480054736 0.8425388932 + 0.8345271945 0.8485044241 0.8430370092 + 0.8350486755 0.8490033746 0.8435351253 + 0.8355702758 0.8495023847 0.8440331221 + 0.8360918164 0.8500012159 0.8445311785 + 0.8366132975 0.8504673839 0.8450292945 + 0.8371348977 0.8509336114 0.8455272913 + 0.8376563787 0.8513997197 0.8460254073 + 0.8381779194 0.8518658876 0.8465235233 + 0.8386995196 0.8523319960 0.8470215201 + 0.8392210007 0.8527982235 0.8475195765 + 0.8397424817 0.8532642722 0.8480176926 + 0.8402487040 0.8537304997 0.8485158086 + 0.8407400250 0.8541966081 0.8490138054 + 0.8412312865 0.8546627760 0.8495119214 + 0.8417226076 0.8551290035 0.8500093222 + 0.8422138095 0.8555951118 0.8504759073 + 0.8427050710 0.8560612798 0.8509424925 + 0.8431963921 0.8565273881 0.8514090180 + 0.8436875939 0.8569936156 0.8518756032 + 0.8441789150 0.8574597239 0.8523421884 + 0.8446701765 0.8579258919 0.8528087735 + 0.8451614976 0.8583920002 0.8532752991 + 0.8456526995 0.8588582277 0.8537418842 + 0.8461440206 0.8593243957 0.8542085290 + 0.8466352820 0.8597905040 0.8546749949 + 0.8471266031 0.8602398038 0.8551415801 + 0.8476178050 0.8606753945 0.8556082249 + 0.8481091261 0.8611109257 0.8560746908 + 0.8486003876 0.8615465164 0.8565412760 + 0.8490915895 0.8619819880 0.8570079207 + 0.8495829105 0.8624175787 0.8574743867 + 0.8500698805 0.8628532290 0.8579409719 + 0.8505327106 0.8632887006 0.8584076166 + 0.8509954214 0.8637242913 0.8588742018 + 0.8514581919 0.8641598225 0.8593407273 + 0.8519209027 0.8645954132 0.8598073125 + 0.8523836732 0.8650308847 0.8602566123 + 0.8528465033 0.8654664755 0.8606936932 + 0.8533092141 0.8659020066 0.8611307740 + 0.8537719846 0.8663375974 0.8615679145 + 0.8542348146 0.8667731285 0.8620049953 + 0.8546975255 0.8672087193 0.8624420762 + 0.8551602960 0.8676443100 0.8628792167 + 0.8556230068 0.8680797815 0.8633162975 + 0.8560857773 0.8685153723 0.8637533784 + 0.8565486073 0.8689509034 0.8641905189 + 0.8570113182 0.8693864942 0.8646275997 + 0.8574740887 0.8698220253 0.8650646806 + 0.8579369187 0.8702406883 0.8655018210 + 0.8583996296 0.8706477880 0.8659389019 + 0.8588624001 0.8710548282 0.8663759828 + 0.8593251705 0.8714619279 0.8668131232 + 0.8597878814 0.8718689084 0.8672502041 + 0.8602361083 0.8722760081 0.8676872849 + 0.8606721163 0.8726829886 0.8681244254 + 0.8611080050 0.8730900884 0.8685615063 + 0.8615438938 0.8734971285 0.8689985871 + 0.8619799018 0.8739041090 0.8694357276 + 0.8624157906 0.8743112087 0.8698728085 + 0.8628516793 0.8747181892 0.8702903986 + 0.8632876873 0.8751252890 0.8706998825 + 0.8637235761 0.8755323291 0.8711094856 + 0.8641595244 0.8759394288 0.8715190887 + 0.8645954728 0.8763464093 0.8719285727 + 0.8650314212 0.8767535090 0.8723381758 + 0.8654673100 0.8771604896 0.8727477789 + 0.8659031987 0.8775675893 0.8731573224 + 0.8663392067 0.8779746294 0.8735669255 + 0.8667750955 0.8783817291 0.8739765286 + 0.8672109842 0.8787887096 0.8743860126 + 0.8676469922 0.8791958094 0.8747956157 + 0.8680828810 0.8796027899 0.8752052188 + 0.8685188293 0.8800091743 0.8756147027 + 0.8689547777 0.8803896904 0.8760243058 + 0.8693907261 0.8807702065 0.8764339089 + 0.8698266149 0.8811507821 0.8768433928 + 0.8702474236 0.8815312982 0.8772529960 + 0.8706580997 0.8819118142 0.8776625991 + 0.8710687757 0.8822922707 0.8780720830 + 0.8714795113 0.8826727867 0.8784816861 + 0.8718901873 0.8830533028 0.8788912892 + 0.8723009229 0.8834338188 0.8793007731 + 0.8727115989 0.8838143945 0.8797103763 + 0.8731222749 0.8841949105 0.8801124096 + 0.8735330105 0.8845754266 0.8804963231 + 0.8739436865 0.8849558830 0.8808801770 + 0.8743544221 0.8853363991 0.8812639713 + 0.8747650981 0.8857169151 0.8816478848 + 0.8751757741 0.8860973716 0.8820317984 + 0.8755865097 0.8864778876 0.8824155927 + 0.8759971857 0.8868585229 0.8827995062 + 0.8764079213 0.8872389793 0.8831834197 + 0.8768185973 0.8876194954 0.8835672140 + 0.8772292733 0.8880000114 0.8839511275 + 0.8776400089 0.8883805275 0.8843349218 + 0.8780506849 0.8887609839 0.8847187757 + 0.8784614205 0.8891415000 0.8851026893 + 0.8788720965 0.8895220757 0.8854864836 + 0.8792827725 0.8899025917 0.8858703971 + 0.8796935081 0.8902646899 0.8862543106 + 0.8800982237 0.8906205893 0.8866381049 + 0.8804851770 0.8909764290 0.8870220184 + 0.8808721900 0.8913322091 0.8874058127 + 0.8812592030 0.8916881084 0.8877897263 + 0.8816462159 0.8920438886 0.8881735802 + 0.8820331097 0.8923997879 0.8885573745 + 0.8824201226 0.8927556276 0.8889412880 + 0.8828070760 0.8931114078 0.8893252015 + 0.8831940889 0.8934673071 0.8897089958 + 0.8835811019 0.8938230872 0.8900871277 + 0.8839681149 0.8941789269 0.8904470205 + 0.8843551278 0.8945348263 0.8908069134 + 0.8847420812 0.8948906064 0.8911666870 + 0.8851290941 0.8952463865 0.8915265799 + 0.8855161071 0.8956022859 0.8918864727 + 0.8859031200 0.8959581256 0.8922464252 + 0.8862900734 0.8963140249 0.8926063180 + 0.8866770864 0.8966698050 0.8929662108 + 0.8870640993 0.8970255852 0.8933259845 + 0.8874511123 0.8973814845 0.8936858773 + 0.8878381252 0.8977373242 0.8940458298 + 0.8882250786 0.8980931044 0.8944057226 + 0.8886120915 0.8984490037 0.8947656155 + 0.8889991045 0.8988047838 0.8951255083 + 0.8893861175 0.8991606832 0.8954852819 + 0.8897730708 0.8995165229 0.8958451748 + 0.8901509047 0.8998723030 0.8962051272 + 0.8905156255 0.9002134800 0.8965650201 + 0.8908804059 0.9005463719 0.8969249129 + 0.8912451267 0.9008792043 0.8972848058 + 0.8916099072 0.9012120962 0.8976445794 + 0.8919746280 0.9015449882 0.8980044723 + 0.8923392892 0.9018778801 0.8983644247 + 0.8927041292 0.9022107720 0.8987243176 + 0.8930687904 0.9025437236 0.8990842104 + 0.8934335709 0.9028766155 0.8994441032 + 0.8937982917 0.9032095075 0.8998038769 + 0.8941630721 0.9035423994 0.9001536965 + 0.8945277929 0.9038752913 0.9004911780 + 0.8948925138 0.9042081833 0.9008287191 + 0.8952572942 0.9045410752 0.9011662006 + 0.8956220150 0.9048740268 0.9015038013 + 0.8959867954 0.9052069187 0.9018412828 + 0.8963515162 0.9055398107 0.9021788239 + 0.8967161775 0.9058727026 0.9025163054 + 0.8970810175 0.9062055945 0.9028539062 + 0.8974456787 0.9065384865 0.9031913877 + 0.8978105187 0.9068713784 0.9035289288 + 0.8981751800 0.9072042704 0.9038664103 + 0.8985400200 0.9075372219 0.9042040110 + 0.8989046812 0.9078701138 0.9045414925 + 0.8992694020 0.9082030058 0.9048789740 + 0.8996341825 0.9085358977 0.9052165747 + 0.8999989033 0.9088687897 0.9055541158 + 0.9003428221 0.9092016816 0.9058915973 + 0.9006866813 0.9095345736 0.9062290788 + 0.9010306001 0.9098675251 0.9065666795 + 0.9013743997 0.9101876020 0.9069042206 + 0.9017183185 0.9104992151 0.9072417021 + 0.9020621181 0.9108107090 0.9075791836 + 0.9024059772 0.9111223221 0.9079167843 + 0.9027498960 0.9114338756 0.9082543254 + 0.9030936956 0.9117454886 0.9085918069 + 0.9034376144 0.9120571017 0.9089292884 + 0.9037814140 0.9123687148 0.9092668891 + 0.9041252732 0.9126802087 0.9096043706 + 0.9044691920 0.9129918218 0.9099419117 + 0.9048129916 0.9133033752 0.9102622271 + 0.9051569104 0.9136149883 0.9105789065 + 0.9055007100 0.9139266014 0.9108955860 + 0.9058446288 0.9142382145 0.9112123251 + 0.9061884284 0.9145497084 0.9115288854 + 0.9065322876 0.9148613214 0.9118456244 + 0.9068762064 0.9151728749 0.9121623039 + 0.9072200060 0.9154844880 0.9124789834 + 0.9075639248 0.9157961011 0.9127957225 + 0.9079077244 0.9161077142 0.9131124020 + 0.9082515836 0.9164192080 0.9134290814 + 0.9085955024 0.9167308211 0.9137458205 + 0.9089393020 0.9170423746 0.9140625000 + 0.9092832208 0.9173539877 0.9143791199 + 0.9096270204 0.9176656008 0.9146957994 + 0.9099708796 0.9179772139 0.9150124788 + 0.9102967978 0.9182887077 0.9153292179 + 0.9106211066 0.9186003208 0.9156458974 + 0.9109454155 0.9189118743 0.9159625769 + 0.9112696052 0.9192234874 0.9162793159 + 0.9115939140 0.9195351005 0.9165959954 + 0.9119182229 0.9198467135 0.9169126749 + 0.9122424126 0.9201481938 0.9172292948 + 0.9125667214 0.9204400182 0.9175459743 + 0.9128909707 0.9207317829 0.9178627133 + 0.9132152200 0.9210234880 0.9181793928 + 0.9135395288 0.9213153124 0.9184960723 + 0.9138637781 0.9216070771 0.9188128114 + 0.9141880274 0.9218989015 0.9191294909 + 0.9145122766 0.9221907258 0.9194461703 + 0.9148365855 0.9224824905 0.9197629094 + 0.9151608944 0.9227741957 0.9200747013 + 0.9154850841 0.9230660200 0.9203718901 + 0.9158093929 0.9233577847 0.9206691980 + 0.9161337018 0.9236496091 0.9209665060 + 0.9164578915 0.9239413738 0.9212638140 + 0.9167822003 0.9242331982 0.9215610027 + 0.9171065092 0.9245249033 0.9218583107 + 0.9174306989 0.9248167276 0.9221556187 + 0.9177550077 0.9251084924 0.9224528074 + 0.9180793166 0.9254003167 0.9227501154 + 0.9184035063 0.9256920815 0.9230474234 + 0.9187278152 0.9259837866 0.9233446717 + 0.9190521240 0.9262756109 0.9236419201 + 0.9193763733 0.9265673757 0.9239392281 + 0.9197006226 0.9268592000 0.9242364764 + 0.9200235009 0.9271510243 0.9245337844 + 0.9203293920 0.9274427891 0.9248309731 + 0.9206352830 0.9277344942 0.9251282811 + 0.9209411740 0.9280263186 0.9254255891 + 0.9212471247 0.9283180833 0.9257227778 + 0.9215530157 0.9286099076 0.9260200858 + 0.9218589067 0.9289016724 0.9263173938 + 0.9221647978 0.9291934967 0.9266147017 + 0.9224706888 0.9294852018 0.9269118905 + 0.9227765799 0.9297770262 0.9272091985 + 0.9230824709 0.9300644994 0.9275065064 + 0.9233884215 0.9303379059 0.9278036952 + 0.9236943126 0.9306111932 0.9281010032 + 0.9240002036 0.9308845997 0.9283983111 + 0.9243060946 0.9311580062 0.9286956191 + 0.9246119857 0.9314314127 0.9289928079 + 0.9249178767 0.9317048192 0.9292901158 + 0.9252238274 0.9319782257 0.9295874238 + 0.9255297184 0.9322515726 0.9298846722 + 0.9258356094 0.9325249791 0.9301708937 + 0.9261415005 0.9327983856 0.9304500222 + 0.9264473915 0.9330717921 0.9307292104 + 0.9267532825 0.9333450794 0.9310083985 + 0.9270591736 0.9336184859 0.9312875867 + 0.9273651242 0.9338918924 0.9315667748 + 0.9276710153 0.9341652989 0.9318460226 + 0.9279769063 0.9344387054 0.9321250916 + 0.9282827973 0.9347121119 0.9324042797 + 0.9285886884 0.9349855185 0.9326835275 + 0.9288945794 0.9352589250 0.9329627156 + 0.9292004704 0.9355322719 0.9332419038 + 0.9295064211 0.9358056188 0.9335210919 + 0.9298123121 0.9360790253 0.9338002205 + 0.9301115274 0.9363523722 0.9340794086 + 0.9304001927 0.9366257787 0.9343585968 + 0.9306889176 0.9368991852 0.9346377850 + 0.9309775829 0.9371725917 0.9349169731 + 0.9312661886 0.9374459982 0.9351962209 + 0.9315549135 0.9377194047 0.9354752898 + 0.9318435788 0.9379928112 0.9357544780 + 0.9321323037 0.9382662177 0.9360337257 + 0.9324209094 0.9385395050 0.9363129139 + 0.9327095747 0.9388129115 0.9365921021 + 0.9329982996 0.9390863180 0.9368712902 + 0.9332870245 0.9393597245 0.9371504188 + 0.9335756898 0.9396330714 0.9374296069 + 0.9338642955 0.9399064779 0.9377087951 + 0.9341530204 0.9401686192 0.9379879832 + 0.9344416857 0.9404249191 0.9382671714 + 0.9347304106 0.9406812787 0.9385464191 + 0.9350190163 0.9409375787 0.9388254881 + 0.9353076816 0.9411938787 0.9391046762 + 0.9355964065 0.9414501786 0.9393839240 + 0.9358850718 0.9417064786 0.9396631122 + 0.9361737967 0.9419627786 0.9399423003 + 0.9364624023 0.9422190785 0.9402080774 + 0.9367511272 0.9424753785 0.9404703975 + 0.9370397925 0.9427316785 0.9407327771 + 0.9373285174 0.9429879785 0.9409950972 + 0.9376171827 0.9432442784 0.9412574172 + 0.9379057884 0.9435005784 0.9415196776 + 0.9381945133 0.9437568784 0.9417821169 + 0.9384831786 0.9440131783 0.9420443773 + 0.9387719035 0.9442694783 0.9423066974 + 0.9390605092 0.9445257783 0.9425690770 + 0.9393491745 0.9447820783 0.9428313971 + 0.9396378994 0.9450383782 0.9430937171 + 0.9399266243 0.9452946782 0.9433560967 + 0.9402031898 0.9455509782 0.9436184168 + 0.9404757023 0.9458072782 0.9438806772 + 0.9407482743 0.9460635781 0.9441431165 + 0.9410207868 0.9463198781 0.9444053769 + 0.9412932992 0.9465761781 0.9446676970 + 0.9415658712 0.9468324780 0.9449300170 + 0.9418383837 0.9470888972 0.9451923966 + 0.9421108961 0.9473451972 0.9454547167 + 0.9423835278 0.9476014972 0.9457169771 + 0.9426559806 0.9478577971 0.9459794164 + 0.9429286122 0.9481140971 0.9462416768 + 0.9432011247 0.9483703971 0.9465039968 + 0.9434735775 0.9486266971 0.9467663765 + 0.9437462091 0.9488829970 0.9470286965 + 0.9440187216 0.9491392970 0.9472910166 + 0.9442911744 0.9493955970 0.9475533962 + 0.9445638061 0.9496518970 0.9478157163 + 0.9448363185 0.9499081969 0.9480779767 + 0.9451087713 0.9501543045 0.9483402967 + 0.9453814030 0.9503946900 0.9486026764 + 0.9456539154 0.9506351948 0.9488649964 + 0.9459264278 0.9508755803 0.9491273165 + 0.9461989999 0.9511160254 0.9493896961 + 0.9464715123 0.9513564706 0.9496520162 + 0.9467440248 0.9515969157 0.9499142766 + 0.9470165968 0.9518373013 0.9501661062 + 0.9472891092 0.9520778060 0.9504126906 + 0.9475616217 0.9523181915 0.9506592751 + 0.9478341937 0.9525585771 0.9509059787 + 0.9481067061 0.9527990818 0.9511526227 + 0.9483792186 0.9530395269 0.9513992071 + 0.9486517906 0.9532799125 0.9516459107 + 0.9489243031 0.9535202980 0.9518924952 + 0.9491968155 0.9537608027 0.9521390796 + 0.9494693875 0.9540011883 0.9523857832 + 0.9497419000 0.9542415738 0.9526324272 + 0.9500135779 0.9544820786 0.9528790116 + 0.9502710104 0.9547225237 0.9531255960 + 0.9505283833 0.9549629092 0.9533722997 + 0.9507858157 0.9552034140 0.9536188841 + 0.9510431886 0.9554437995 0.9538655281 + 0.9513006210 0.9556841850 0.9541121721 + 0.9515579939 0.9559246898 0.9543588161 + 0.9518154263 0.9561650753 0.9546054006 + 0.9520727992 0.9564055204 0.9548519850 + 0.9523301721 0.9566460252 0.9550986886 + 0.9525876045 0.9568864107 0.9553452730 + 0.9528449774 0.9571267962 0.9555919170 + 0.9531024098 0.9573671818 0.9558386207 + 0.9533597827 0.9576076865 0.9560852051 + 0.9536172152 0.9578480721 0.9563317895 + 0.9538745880 0.9580885172 0.9565784931 + 0.9541320205 0.9583290219 0.9568250775 + 0.9543893933 0.9585694075 0.9570717216 + 0.9546468258 0.9588097930 0.9573183060 + 0.9549041986 0.9590502977 0.9575650096 + 0.9551615715 0.9592906833 0.9578115940 + 0.9554190040 0.9595311284 0.9580581784 + 0.9556763768 0.9597715735 0.9583048820 + 0.9559338093 0.9600113034 0.9585515261 + 0.9561911821 0.9602369070 0.9587981105 + 0.9564486146 0.9604626298 0.9590448141 + 0.9567059875 0.9606882930 0.9592913985 + 0.9569634199 0.9609140158 0.9595379829 + 0.9572207928 0.9611396790 0.9597846270 + 0.9574782252 0.9613652825 0.9600294232 + 0.9577355981 0.9615910053 0.9602614045 + 0.9579929709 0.9618167281 0.9604933858 + 0.9582504034 0.9620423913 0.9607254267 + 0.9585077763 0.9622681141 0.9609574080 + 0.9587652087 0.9624937773 0.9611893892 + 0.9590225816 0.9627193809 0.9614213705 + 0.9592800140 0.9629451036 0.9616534114 + 0.9595373869 0.9631708264 0.9618853927 + 0.9597948194 0.9633964896 0.9621173739 + 0.9600493908 0.9636222124 0.9623494148 + 0.9602925777 0.9638478160 0.9625813961 + 0.9605358243 0.9640734792 0.9628133774 + 0.9607790112 0.9642992020 0.9630454183 + 0.9610221982 0.9645249248 0.9632773995 + 0.9612653852 0.9647505879 0.9635093808 + 0.9615085721 0.9649763107 0.9637414217 + 0.9617518187 0.9652019143 0.9639734030 + 0.9619951248 0.9654275775 0.9642053843 + 0.9622383118 0.9656533003 0.9644374251 + 0.9624814987 0.9658790231 0.9646694064 + 0.9627246857 0.9661046863 0.9649013877 + 0.9629678726 0.9663304090 0.9651334286 + 0.9632111192 0.9665560126 0.9653654099 + 0.9634543061 0.9667816758 0.9655973911 + 0.9636976123 0.9670073986 0.9658293724 + 0.9639407992 0.9672331214 0.9660614133 + 0.9641839862 0.9674587846 0.9662933946 + 0.9644271731 0.9676843882 0.9665253758 + 0.9646704197 0.9679101110 0.9667574167 + 0.9649136066 0.9681357741 0.9669893980 + 0.9651567936 0.9683614969 0.9672213793 + 0.9654000998 0.9685872197 0.9674534202 + 0.9656432867 0.9688128829 0.9676854014 + 0.9658864737 0.9690384865 0.9679173827 + 0.9661297202 0.9692642093 0.9681494236 + 0.9663729072 0.9694898725 0.9683814049 + 0.9666160941 0.9697155952 0.9686133862 + 0.9668592811 0.9699413180 0.9688454270 + 0.9671025276 0.9701567888 0.9690774083 + 0.9673457742 0.9703688025 0.9693093896 + 0.9675890207 0.9705808163 0.9695413709 + 0.9678322077 0.9707927108 0.9697734118 + 0.9680753946 0.9710047245 0.9700049758 + 0.9683185816 0.9712166786 0.9702234268 + 0.9685618281 0.9714285731 0.9704418182 + 0.9688050151 0.9716405869 0.9706600904 + 0.9690483212 0.9718526006 0.9708784819 + 0.9692915082 0.9720646143 0.9710968733 + 0.9695346951 0.9722765088 0.9713152051 + 0.9697778821 0.9724885225 0.9715335965 + 0.9700199962 0.9727004766 0.9717519283 + 0.9702498913 0.9729124904 0.9719703197 + 0.9704797864 0.9731243849 0.9721887112 + 0.9707096815 0.9733363986 0.9724069834 + 0.9709395766 0.9735484123 0.9726253748 + 0.9711694717 0.9737603068 0.9728438258 + 0.9713994265 0.9739723206 0.9730620980 + 0.9716293812 0.9741842747 0.9732804894 + 0.9718592763 0.9743962884 0.9734988213 + 0.9720891714 0.9746081829 0.9737172127 + 0.9723191261 0.9748201966 0.9739356041 + 0.9725490212 0.9750322104 0.9741538763 + 0.9727789164 0.9752442241 0.9743723273 + 0.9730088115 0.9754561186 0.9745907187 + 0.9732388258 0.9756680727 0.9748089910 + 0.9734687209 0.9758800864 0.9750273824 + 0.9736986160 0.9760921001 0.9752457142 + 0.9739285111 0.9763039947 0.9754641056 + 0.9741584063 0.9765160084 0.9756824970 + 0.9743883014 0.9767280221 0.9759008288 + 0.9746181965 0.9769399166 0.9761192203 + 0.9748480916 0.9771518707 0.9763376117 + 0.9750781059 0.9773638844 0.9765558839 + 0.9753080010 0.9775758982 0.9767742753 + 0.9755378962 0.9777877927 0.9769926071 + 0.9757677913 0.9779998064 0.9772109985 + 0.9759976864 0.9782118201 0.9774293900 + 0.9762275815 0.9784237742 0.9776477218 + 0.9764574766 0.9786357284 0.9778661132 + 0.9766874909 0.9788476825 0.9780845046 + 0.9769173861 0.9790596962 0.9783027768 + 0.9771472812 0.9792717099 0.9785212278 + 0.9773771763 0.9794836044 0.9787395000 + 0.9776070714 0.9796956182 0.9789578915 + 0.9778370261 0.9799075723 0.9791762829 + 0.9780669212 0.9801123738 0.9793946147 + 0.9782968760 0.9803115726 0.9796130061 + 0.9785267711 0.9805107713 0.9798313975 + 0.9787567258 0.9807100296 0.9800468087 + 0.9789866209 0.9809092879 0.9802525043 + 0.9792165160 0.9811084867 0.9804580808 + 0.9794464111 0.9813076854 0.9806637764 + 0.9796763062 0.9815068841 0.9808694124 + 0.9799063206 0.9817062020 0.9810751081 + 0.9801288247 0.9819054008 0.9812806845 + 0.9803462029 0.9821045995 0.9814863801 + 0.9805637002 0.9823039174 0.9816920161 + 0.9807810783 0.9825031161 0.9818977118 + 0.9809985757 0.9827023149 0.9821032882 + 0.9812160134 0.9829015136 0.9823089838 + 0.9814333916 0.9831007719 0.9825146198 + 0.9816508889 0.9832999706 0.9827203155 + 0.9818683267 0.9834992290 0.9829258919 + 0.9820858240 0.9836984277 0.9831315875 + 0.9823032022 0.9838976860 0.9833372235 + 0.9825206995 0.9840968847 0.9835429192 + 0.9827380776 0.9842960835 0.9837484956 + 0.9829555154 0.9844954014 0.9839541912 + 0.9831730127 0.9846946001 0.9841598272 + 0.9833903909 0.9848937988 0.9843655229 + 0.9836078882 0.9850929976 0.9845710993 + 0.9838253260 0.9852923155 0.9847767949 + 0.9840428233 0.9854915142 0.9849823713 + 0.9842602015 0.9856907129 0.9851881266 + 0.9844775796 0.9858899117 0.9853937030 + 0.9846950769 0.9860892296 0.9855993986 + 0.9849125147 0.9862884283 0.9858049750 + 0.9851300120 0.9864876270 0.9860106707 + 0.9853473902 0.9866868854 0.9862163067 + 0.9855648875 0.9868860841 0.9864220023 + 0.9857823253 0.9870852828 0.9866275787 + 0.9859997034 0.9872844815 0.9868332744 + 0.9862172008 0.9874837995 0.9870389104 + 0.9864345789 0.9876829982 0.9872446060 + 0.9866520762 0.9878821969 0.9874501824 + 0.9868695140 0.9880813956 0.9876558781 + 0.9870870113 0.9882807136 0.9878615141 + 0.9873043895 0.9884799123 0.9880672097 + 0.9875218272 0.9886791110 0.9882727861 + 0.9877393246 0.9888783097 0.9884784818 + 0.9879567027 0.9890776277 0.9886841178 + 0.9881742001 0.9892768264 0.9888898134 + 0.9883915782 0.9894760251 0.9890953898 + 0.9886090755 0.9896752834 0.9893010855 + 0.9888265133 0.9898744822 0.9895067215 + 0.9890438914 0.9900693297 0.9897124171 + 0.9892613888 0.9902567267 0.9899179935 + 0.9894788265 0.9904441237 0.9901164770 + 0.9896963239 0.9906314015 0.9903103113 + 0.9899137020 0.9908187985 0.9905040860 + 0.9901241064 0.9910061955 0.9906979203 + 0.9903299212 0.9911935925 0.9908916950 + 0.9905356169 0.9913808703 0.9910855293 + 0.9907413721 0.9915683270 0.9912793040 + 0.9909471273 0.9917557240 0.9914730787 + 0.9911528230 0.9919430017 0.9916669130 + 0.9913585782 0.9921303988 0.9918606877 + 0.9915642738 0.9923177958 0.9920545220 + 0.9917700887 0.9925051928 0.9922482967 + 0.9919757843 0.9926924706 0.9924420118 + 0.9921815991 0.9928799272 0.9926357865 + 0.9923872948 0.9930673242 0.9928296208 + 0.9925931096 0.9932546020 0.9930233955 + 0.9927988052 0.9934419990 0.9932171702 + 0.9930046201 0.9936293960 0.9934110045 + 0.9932103157 0.9938166738 0.9936047792 + 0.9934160709 0.9940040708 0.9937986135 + 0.9936218262 0.9941915274 0.9939923882 + 0.9938275814 0.9943789244 0.9941862226 + 0.9940332770 0.9945662022 0.9943799973 + 0.9942390919 0.9947535992 0.9945737720 + 0.9944447875 0.9949409962 0.9947676063 + 0.9946506023 0.9951282740 0.9949613810 + 0.9948562980 0.9953156710 0.9951552153 + 0.9950621128 0.9955031276 0.9953489900 + 0.9952678084 0.9956905246 0.9955427051 + 0.9954735041 0.9958778024 0.9957364798 + 0.9956793189 0.9960651994 0.9959303141 + 0.9958850145 0.9962525964 0.9961240888 + 0.9960908294 0.9964398742 0.9963179231 + 0.9962965250 0.9966272712 0.9965116978 + 0.9965022802 0.9968147278 0.9967054725 + 0.9967079759 0.9970021248 0.9968993068 + 0.9969137907 0.9971894026 0.9970930815 + 0.9971194863 0.9973767996 0.9972869158 + 0.9973253012 0.9975641966 0.9974806905 + 0.9975309968 0.9977514744 0.9976745248 + 0.9977368116 0.9979388714 0.9978682995 + 0.9979425073 0.9981263280 0.9980620742 + 0.9981483221 0.9983137250 0.9982559085 + 0.9983540177 0.9985010028 0.9984496832 + 0.9985597730 0.9986883998 0.9986433983 + 0.9987655282 0.9988757968 0.9988371730 + 0.9989712834 0.9990630746 0.9990310073 + 0.9991769791 0.9992504716 0.9992247820 + 0.9993827939 0.9994379282 0.9994186163 + 0.9995884895 0.9996253252 0.9996123910 + 0.9997943044 0.9998126030 0.9998062253 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3152.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3152.spi1d index a144a4e5..6e96354c 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3152.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3152.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0031291239 0.0033278200 0.0041153808 - 0.0062582470 0.0066556409 0.0082307607 - 0.0093873711 0.0099834614 0.0125252204 - 0.0128003703 0.0136352098 0.0169547293 - 0.0162824895 0.0172885899 0.0214768499 - 0.0197646003 0.0210279003 0.0262027308 - 0.0235934593 0.0250145998 0.0309809893 - 0.0274474602 0.0290013105 0.0359734409 - 0.0314300507 0.0332363509 0.0410094596 - 0.0356648304 0.0375544094 0.0462271497 - 0.0398996100 0.0420100689 0.0514931008 - 0.0445012413 0.0466454811 0.0568850897 - 0.0491117798 0.0513610095 0.0623264983 - 0.0540077388 0.0562863797 0.0678355098 - 0.0589718111 0.0612730384 0.0733793527 - 0.0641835630 0.0664475709 0.0789457336 - 0.0694600120 0.0716837272 0.0845116675 - 0.0749636367 0.0770548731 0.0900767818 - 0.0805091709 0.0824873224 0.0955895111 - 0.0862180665 0.0879941806 0.1010826975 - 0.0919598788 0.0935461521 0.1064976007 - 0.0977663025 0.0991239473 0.1118654013 - 0.1035818979 0.1047077999 0.1171469018 - 0.1094032973 0.1102901995 0.1223551035 - 0.1151698977 0.1158241034 0.1274771988 - 0.1209099963 0.1213333011 0.1325096041 - 0.1265448928 0.1267662942 0.1374547929 - 0.1321122050 0.1321426034 0.1423096061 - 0.1375723034 0.1374354064 0.1470683962 - 0.1429159939 0.1426412016 0.1517543048 - 0.1481667012 0.1477651000 0.1563231945 - 0.1532676965 0.1527830958 0.1608552933 - 0.1582884043 0.1577192992 0.1652355939 - 0.1631516069 0.1625491977 0.1696159989 - 0.1679333001 0.1672876030 0.1738290042 - 0.1725793928 0.1719401926 0.1780259013 - 0.1771221012 0.1764778048 0.1821296066 - 0.1815796942 0.1809709072 0.1861504018 - 0.1858897954 0.1853097975 0.1901641935 - 0.1901896000 0.1896487027 0.1940180957 - 0.1942777932 0.1938105971 0.1978719980 - 0.1983660012 0.1979569942 0.2016555071 - 0.2023289055 0.2020100057 0.2053523958 - 0.2062083930 0.2059722990 0.2090492994 - 0.2100834996 0.2099346966 0.2126373947 - 0.2137690037 0.2137262970 0.2161877006 - 0.2174544036 0.2175149024 0.2197380066 - 0.2210844010 0.2212477028 0.2231622040 - 0.2245907933 0.2248737961 0.2265762985 - 0.2280972004 0.2284999043 0.2299904972 - 0.2315285057 0.2320373952 0.2332790941 - 0.2348707020 0.2355124056 0.2365673035 - 0.2382130027 0.2389874011 0.2398556024 - 0.2414855063 0.2423633039 0.2430329025 - 0.2446777970 0.2456984967 0.2462050021 - 0.2478702068 0.2490337044 0.2493772060 - 0.2510173023 0.2522774041 0.2524636090 - 0.2540734112 0.2554838061 0.2555291057 - 0.2571294904 0.2586902976 0.2585946023 - 0.2601780891 0.2618266940 0.2616071105 - 0.2631106079 0.2649149001 0.2645747066 - 0.2660430968 0.2680031061 0.2675423026 - 0.2689755857 0.2710529864 0.2704944909 - 0.2718352973 0.2740328908 0.2733725905 - 0.2746559978 0.2770127952 0.2762506008 - 0.2774766088 0.2799926102 0.2791286111 - 0.2802866101 0.2828738093 0.2819496095 - 0.2830061018 0.2857545912 0.2847456932 - 0.2857257128 0.2886354923 0.2875418067 - 0.2884452939 0.2914687991 0.2903288901 - 0.2911258042 0.2942593992 0.2930502892 - 0.2937541902 0.2970499098 0.2957716882 - 0.2963826954 0.2998403907 0.2984930873 - 0.2990111113 0.3025532961 0.3011840880 - 0.3015882969 0.3052614927 0.3038372993 - 0.3041346967 0.3079696894 0.3064906001 - 0.3066810966 0.3106591105 0.3091438115 - 0.3092274964 0.3132922947 0.3117550015 - 0.3117223978 0.3159255981 0.3143461943 - 0.3141950965 0.3185588121 0.3169373870 - 0.3166677058 0.3211610913 0.3195286095 - 0.3191402853 0.3237262070 0.3220736086 - 0.3215698004 0.3262912929 0.3246082962 - 0.3239761889 0.3288562894 0.3271430135 - 0.3263826966 0.3313871026 0.3296777904 - 0.3287890851 0.3338901997 0.3321677148 - 0.3311660886 0.3363932967 0.3346511126 - 0.3335132897 0.3388963938 0.3371345103 - 0.3358604908 0.3413681090 0.3396179080 - 0.3382078111 0.3438149989 0.3420617878 - 0.3405424953 0.3462618887 0.3444986045 - 0.3428367972 0.3487088084 0.3469353914 - 0.3451310992 0.3511315882 0.3493720889 - 0.3474254012 0.3535273969 0.3517774940 - 0.3497197032 0.3559232950 0.3541719019 - 0.3519726098 0.3583191037 0.3565663099 - 0.3542197943 0.3607012033 0.3589606881 - 0.3564670086 0.3630507886 0.3613333106 - 0.3587141931 0.3654003143 0.3636893034 - 0.3609434068 0.3677498996 0.3660452068 - 0.3631486893 0.3700976968 0.3684011996 - 0.3653540909 0.3724052906 0.3707458973 - 0.3675594032 0.3747127950 0.3730669022 - 0.3697646856 0.3770203888 0.3753877878 - 0.3719370067 0.3793280125 0.3777087927 - 0.3741051853 0.3816084862 0.3800294101 - 0.3762735128 0.3838779926 0.3823184967 - 0.3784418106 0.3861474097 0.3846076131 - 0.3806008995 0.3884168863 0.3868967891 - 0.3827365041 0.3906759024 0.3891859055 - 0.3848721981 0.3929106891 0.3914563060 - 0.3870078027 0.3951455057 0.3937163949 - 0.3891434073 0.3973802924 0.3959765136 - 0.3912619054 0.3996151090 0.3982365131 - 0.3933688104 0.4018237889 0.4004907012 - 0.3954758048 0.4040271044 0.4027242064 - 0.3975827098 0.4062303007 0.4049575925 - 0.3996897042 0.4084335864 0.4071910083 - 0.4017753005 0.4106284976 0.4094245136 - 0.4038572013 0.4128029048 0.4116396904 - 0.4059390128 0.4149773121 0.4138484895 - 0.4080209136 0.4171518087 0.4160574079 - 0.4101017118 0.4193261862 0.4182662964 - 0.4121617079 0.4214822948 0.4204702079 - 0.4142217934 0.4236302078 0.4226562977 - 0.4162817895 0.4257782102 0.4248422980 - 0.4183419049 0.4279260933 0.4270282984 - 0.4203982055 0.4300732017 0.4292142987 - 0.4224393070 0.4321965873 0.4313865900 - 0.4244804084 0.4343200028 0.4335511923 - 0.4265213907 0.4364433885 0.4357157946 - 0.4285624921 0.4385668933 0.4378803074 - 0.4305987060 0.4406827986 0.4400444925 - 0.4326233864 0.4427833855 0.4421885908 - 0.4346480072 0.4448839128 0.4443328083 - 0.4366726875 0.4469844997 0.4464769065 - 0.4386973083 0.4490849972 0.4486210942 - 0.4407168925 0.4511733949 0.4507581890 - 0.4427272975 0.4532522857 0.4528825879 - 0.4447377026 0.4553312063 0.4550068974 - 0.4467481077 0.4574100971 0.4571312964 - 0.4487585127 0.4594889879 0.4592556953 - 0.4507640898 0.4615522921 0.4613674879 - 0.4527620971 0.4636105001 0.4634723961 - 0.4547601044 0.4656687081 0.4655773938 - 0.4567581117 0.4677268863 0.4676823020 - 0.4587559998 0.4697850943 0.4697872102 - 0.4607498944 0.4718252122 0.4718747139 - 0.4627369046 0.4738632143 0.4739601016 - 0.4647239149 0.4759013057 0.4760456085 - 0.4667108953 0.4779393077 0.4781309962 - 0.4686979055 0.4799773097 0.4802143872 - 0.4706814885 0.4819954932 0.4822798967 - 0.4726586044 0.4840134084 0.4843454957 - 0.4746356905 0.4860314131 0.4864110053 - 0.4766128063 0.4880493879 0.4884765148 - 0.4785900116 0.4900667071 0.4905366004 - 0.4805643857 0.4920643866 0.4925814867 - 0.4825324118 0.4940621853 0.4946263134 - 0.4845002890 0.4960598946 0.4966711104 - 0.4864681959 0.4980576038 0.4987159967 - 0.4884361029 0.5000547767 0.5007526875 - 0.4904021919 0.5020316839 0.5027757287 - 0.4923611879 0.5040085912 0.5047987103 - 0.4943202138 0.5059854984 0.5068216920 - 0.4962792099 0.5079624057 0.5088446140 - 0.4982382059 0.5099393129 0.5108575821 - 0.5001962185 0.5118951201 0.5128573179 - 0.5021461844 0.5138502121 0.5148568749 - 0.5040960908 0.5158053041 0.5168566108 - 0.5060459971 0.5177603960 0.5188562870 - 0.5079959035 0.5197154880 0.5208451152 - 0.5099458098 0.5216509104 0.5228195786 - 0.5118862987 0.5235828757 0.5247941017 - 0.5138264894 0.5255150199 0.5267686248 - 0.5157666802 0.5274469852 0.5287430882 - 0.5177068710 0.5293790102 0.5307077169 - 0.5196471214 0.5312942863 0.5326548815 - 0.5215784907 0.5332015157 0.5346019864 - 0.5235080123 0.5351088047 0.5365492105 - 0.5254374146 0.5370160937 0.5384963751 - 0.5273668766 0.5389233828 0.5404369235 - 0.5292963982 0.5408189893 0.5423542857 - 0.5312179923 0.5426995754 0.5442718267 - 0.5331351161 0.5445801020 0.5461893082 - 0.5350522995 0.5464606881 0.5481067896 - 0.5369694829 0.5483412743 0.5500237942 - 0.5388866067 0.5502184033 0.5519090295 - 0.5407978296 0.5520700216 0.5537940860 - 0.5427005887 0.5539215803 0.5556792021 - 0.5446034074 0.5557731986 0.5575643778 - 0.5465062857 0.5576248169 0.5594494939 - 0.5484091043 0.5594763756 0.5613096952 - 0.5503091812 0.5613054037 0.5631597042 - 0.5521953106 0.5631254911 0.5650097132 - 0.5540813208 0.5649455786 0.5668596029 - 0.5559673905 0.5667657256 0.5687096119 - 0.5578534007 0.5685858130 0.5705479980 - 0.5597394705 0.5703982711 0.5723599195 - 0.5616086125 0.5721842051 0.5741717219 - 0.5634748936 0.5739700794 0.5759835839 - 0.5653412938 0.5757560730 0.5777953863 - 0.5672075748 0.5775420070 0.5796073079 - 0.5690739751 0.5793278813 0.5813869238 - 0.5709286928 0.5810908079 0.5831577182 - 0.5727720857 0.5828397274 0.5849283934 - 0.5746154189 0.5845885873 0.5866991878 - 0.5764586926 0.5863376260 0.5884699821 - 0.5783020854 0.5880864859 0.5902346969 - 0.5801432729 0.5898354053 0.5919613838 - 0.5819600224 0.5915482044 0.5936880708 - 0.5837767124 0.5932571888 0.5954148173 - 0.5855932832 0.5949661732 0.5971413851 - 0.5874099731 0.5966752172 0.5988680720 - 0.5892267227 0.5983842015 0.6005786061 - 0.5910258293 0.6000909209 0.6022583842 - 0.5928118229 0.6017571092 0.6039381027 - 0.5945978761 0.6034232974 0.6056178212 - 0.5963839889 0.6050894856 0.6072975993 - 0.5981701016 0.6067556739 0.6089773178 - 0.5999560952 0.6084219217 0.6106376052 - 0.6017082930 0.6100856066 0.6122676730 - 0.6034597158 0.6117061973 0.6138976812 - 0.6052110195 0.6133267879 0.6155278087 - 0.6069623828 0.6149473190 0.6171578765 - 0.6087138057 0.6165679097 0.6187878847 - 0.6104547977 0.6181885004 0.6204046011 - 0.6121671796 0.6198089719 0.6219825745 - 0.6138796806 0.6213870049 0.6235604882 - 0.6155921221 0.6229591966 0.6251384020 - 0.6173046231 0.6245315075 0.6267163754 - 0.6190170050 0.6261038184 0.6282942891 - 0.6207110882 0.6276760101 0.6298722029 - 0.6223804951 0.6292483211 0.6314002275 - 0.6240499020 0.6307939887 0.6329237819 - 0.6257191896 0.6323155761 0.6344473958 - 0.6273885965 0.6338371038 0.6359710097 - 0.6290580034 0.6353585720 0.6374946237 - 0.6307067871 0.6368800998 0.6390182972 - 0.6323291063 0.6384016275 0.6405218840 - 0.6339514256 0.6399230957 0.6419894099 - 0.6355736852 0.6413943768 0.6434568167 - 0.6371960044 0.6428629756 0.6449242830 - 0.6388183236 0.6443315744 0.6463916898 - 0.6404266953 0.6458001733 0.6478592157 - 0.6419981718 0.6472687721 0.6493266225 - 0.6435695887 0.6487373710 0.6507629156 - 0.6451410055 0.6501982808 0.6521728039 - 0.6467124820 0.6516122222 0.6535826921 - 0.6482838988 0.6530259848 0.6549925208 - 0.6498553753 0.6544399261 0.6564024091 - 0.6513776183 0.6558538079 0.6578121781 - 0.6528947949 0.6572676897 0.6592221260 - 0.6544119716 0.6586815119 0.6606057286 - 0.6559292078 0.6600915790 0.6619569063 - 0.6574463844 0.6614493132 0.6633082032 - 0.6589636207 0.6628069878 0.6646593809 - 0.6604626775 0.6641647220 0.6660106778 - 0.6619228125 0.6655225158 0.6673619151 - 0.6633828282 0.6668801904 0.6687130928 - 0.6648429036 0.6682379246 0.6700615883 - 0.6663029790 0.6695955992 0.6713535786 - 0.6677629948 0.6709131002 0.6726456285 - 0.6692231297 0.6722136736 0.6739376187 - 0.6706553102 0.6735141873 0.6752296090 - 0.6720557809 0.6748147011 0.6765217185 - 0.6734563112 0.6761152148 0.6778137088 - 0.6748567820 0.6774157286 0.6791056991 - 0.6762573719 0.6787163019 0.6803793907 - 0.6776579022 0.6800159812 0.6816120744 - 0.6790583730 0.6812587976 0.6828446984 - 0.6804388165 0.6825014949 0.6840773225 - 0.6817780137 0.6837441921 0.6853098869 - 0.6831172109 0.6849870086 0.6865425110 - 0.6844564080 0.6862297058 0.6877751946 - 0.6857956052 0.6874724030 0.6890078187 - 0.6871348023 0.6887152195 0.6902288795 - 0.6884739995 0.6899579167 0.6914023161 - 0.6898130774 0.6911445856 0.6925758123 - 0.6910985112 0.6923294067 0.6937493086 - 0.6923751831 0.6935142279 0.6949226856 - 0.6936519146 0.6946989894 0.6960961819 - 0.6949285269 0.6958836913 0.6972696185 - 0.6962051988 0.6970685124 0.6984431148 - 0.6974818110 0.6982532740 0.6996166110 - 0.6987584829 0.6994379759 0.7007505894 - 0.7000334263 0.7005925179 0.7018656135 - 0.7012469769 0.7017195225 0.7029805183 - 0.7024605870 0.7028465867 0.7040954232 - 0.7036740780 0.7039735913 0.7052103281 - 0.7048876882 0.7051007152 0.7063252926 - 0.7061012983 0.7062277198 0.7074401975 - 0.7073147893 0.7073547244 0.7085551023 - 0.7085283995 0.7084817886 0.7096700072 - 0.7097420096 0.7096087933 0.7107443810 - 0.7109059095 0.7106986046 0.7118018270 - 0.7120562792 0.7117685080 0.7128592134 - 0.7132068276 0.7128384709 0.7139164805 - 0.7143573165 0.7139083743 0.7149739265 - 0.7155078053 0.7149783969 0.7160311937 - 0.7166582942 0.7160483003 0.7170885801 - 0.7178087831 0.7171183228 0.7181460261 - 0.7189592123 0.7181882262 0.7192032933 - 0.7201038003 0.7192581296 0.7202467918 - 0.7211917043 0.7203109264 0.7212479115 - 0.7222797275 0.7213246822 0.7222489715 - 0.7233676910 0.7223384976 0.7232500911 - 0.7244557142 0.7233523130 0.7242512107 - 0.7255436182 0.7243661284 0.7252522707 - 0.7266315818 0.7253798842 0.7262532711 - 0.7277196050 0.7263938189 0.7272543907 - 0.7288076282 0.7274075747 0.7282555103 - 0.7298954725 0.7284213901 0.7292565703 - 0.7309280038 0.7294352055 0.7302436233 - 0.7319545150 0.7304247022 0.7311900258 - 0.7329810262 0.7313836813 0.7321363091 - 0.7340075970 0.7323426008 0.7330827117 - 0.7350341082 0.7333015800 0.7340291142 - 0.7360606194 0.7342606187 0.7349753976 - 0.7370871902 0.7352195978 0.7359218001 - 0.7381137013 0.7361785173 0.7368682027 - 0.7391402125 0.7371374965 0.7378144860 - 0.7401570082 0.7380964756 0.7387608886 - 0.7411236167 0.7390553951 0.7397072911 - 0.7420902252 0.7400135994 0.7406170964 - 0.7430567741 0.7409192920 0.7415105104 - 0.7440233827 0.7418249249 0.7424039245 - 0.7449899912 0.7427306175 0.7432973981 - 0.7459564805 0.7436363101 0.7441908121 - 0.7469230890 0.7445420027 0.7450842261 - 0.7478896976 0.7454476953 0.7459775805 - 0.7488563061 0.7463533282 0.7468711138 - 0.7498229146 0.7472590208 0.7477645278 - 0.7507420182 0.7481647134 0.7486578822 - 0.7516505122 0.7490704060 0.7495512962 - 0.7525590062 0.7499759793 0.7504193783 - 0.7534675002 0.7508316040 0.7512617707 - 0.7543759942 0.7516856790 0.7521042824 - 0.7552844882 0.7525398731 0.7529466748 - 0.7561929822 0.7533940077 0.7537891269 - 0.7571014762 0.7542482018 0.7546315789 - 0.7580100298 0.7551022768 0.7554739714 - 0.7589185238 0.7559564710 0.7563164234 - 0.7598270178 0.7568106055 0.7571588755 - 0.7606902122 0.7576647997 0.7580013275 - 0.7615427971 0.7585189939 0.7588437796 - 0.7623953223 0.7593731284 0.7596861720 - 0.7632479072 0.7602140903 0.7604979873 - 0.7641004920 0.7610186934 0.7612915039 - 0.7649530172 0.7618231773 0.7620850205 - 0.7658056021 0.7626277804 0.7628785968 - 0.7666581273 0.7634323835 0.7636721134 - 0.7675107121 0.7642369866 0.7644655704 - 0.7683631778 0.7650415897 0.7652592063 - 0.7692158222 0.7658461928 0.7660527229 - 0.7700641155 0.7666506767 0.7668461800 - 0.7708631158 0.7674552798 0.7676398158 - 0.7716619968 0.7682598829 0.7684332728 - 0.7724609971 0.7690644860 0.7692267895 - 0.7732599974 0.7698690891 0.7700191736 - 0.7740589976 0.7706338763 0.7707660198 - 0.7748579979 0.7713909745 0.7715128064 - 0.7756569982 0.7721480131 0.7722595930 - 0.7764559984 0.7729051113 0.7730063796 - 0.7772548795 0.7736622095 0.7737532258 - 0.7780538797 0.7744193077 0.7745000720 - 0.7788528800 0.7751764059 0.7752469182 - 0.7796518803 0.7759335041 0.7759937048 - 0.7804220915 0.7766906023 0.7767404914 - 0.7811700106 0.7774475813 0.7774872780 - 0.7819179296 0.7782046795 0.7782341242 - 0.7826659083 0.7789617777 0.7789809108 - 0.7834138274 0.7797188759 0.7797278166 - 0.7841616869 0.7804474831 0.7804462910 - 0.7849097252 0.7811592221 0.7811486125 - 0.7856575847 0.7818710208 0.7818508744 - 0.7864055037 0.7825827003 0.7825533152 - 0.7871534824 0.7832944989 0.7832555771 - 0.7879014015 0.7840061784 0.7839578986 - 0.7886493206 0.7847179770 0.7846602201 - 0.7893972993 0.7854297161 0.7853626013 - 0.7901358008 0.7861415148 0.7860649228 - 0.7908353209 0.7868531942 0.7867671847 - 0.7915347815 0.7875649929 0.7874695063 - 0.7922343016 0.7882766724 0.7881718874 - 0.7929338217 0.7889884710 0.7888742089 - 0.7936332226 0.7897002101 0.7895764709 - 0.7943326831 0.7903869748 0.7902621031 - 0.7950322032 0.7910556793 0.7909221053 - 0.7957317233 0.7917243242 0.7915822268 - 0.7964311838 0.7923929095 0.7922422886 - 0.7971307039 0.7930616140 0.7929024100 - 0.7978302240 0.7937301993 0.7935624719 - 0.7985296845 0.7943987846 0.7942225933 - 0.7992292047 0.7950674891 0.7948827147 - 0.7999287248 0.7957360744 0.7955427766 - 0.8005871177 0.7964047194 0.7962027788 - 0.8012408018 0.7970734239 0.7968629003 - 0.8018944860 0.7977420092 0.7975230217 - 0.8025482297 0.7984105945 0.7981830835 - 0.8032019138 0.7990792990 0.7988432050 - 0.8038555980 0.7997478843 0.7995033264 - 0.8045094013 0.8003910780 0.8001534939 - 0.8051630855 0.8010187745 0.8007736206 - 0.8058168292 0.8016465902 0.8013936877 - 0.8064705133 0.8022742867 0.8020138144 - 0.8071241975 0.8029021025 0.8026338816 - 0.8077778816 0.8035297990 0.8032540083 - 0.8084316254 0.8041576147 0.8038740754 - 0.8090854287 0.8047853112 0.8044942021 - 0.8097391129 0.8054131269 0.8051143289 - 0.8103669286 0.8060408235 0.8057343960 - 0.8109775186 0.8066685796 0.8063545227 - 0.8115881085 0.8072962761 0.8069745898 - 0.8121985793 0.8079240918 0.8075947165 - 0.8128092289 0.8085517883 0.8082147837 - 0.8134198189 0.8091796041 0.8088349104 - 0.8140304089 0.8098073006 0.8094549775 - 0.8146409988 0.8104082942 0.8100705743 - 0.8152515888 0.8109974265 0.8106529117 - 0.8158621788 0.8115864992 0.8112351894 - 0.8164728284 0.8121755719 0.8118175864 - 0.8170834184 0.8127647042 0.8123999238 - 0.8176940084 0.8133537173 0.8129822016 - 0.8183045983 0.8139427900 0.8135645986 - 0.8189151883 0.8145319223 0.8141468763 - 0.8195257187 0.8151209950 0.8147292733 - 0.8201273084 0.8157101274 0.8153116107 - 0.8206974268 0.8162992001 0.8158938885 - 0.8212674856 0.8168882728 0.8164762855 - 0.8218376040 0.8174774051 0.8170586228 - 0.8224077225 0.8180664778 0.8176409006 - 0.8229777813 0.8186556101 0.8182232976 - 0.8235478997 0.8192446828 0.8188055754 - 0.8241178989 0.8198336959 0.8193879724 - 0.8246880174 0.8203967214 0.8199703097 - 0.8252580762 0.8209493160 0.8205189109 - 0.8258281946 0.8215019107 0.8210656047 - 0.8263983130 0.8220545053 0.8216122985 - 0.8269683719 0.8226072192 0.8221591115 - 0.8275384903 0.8231598139 0.8227058053 - 0.8281086087 0.8237124085 0.8232526183 - 0.8286787271 0.8242650032 0.8237993121 - 0.8292487860 0.8248177171 0.8243461251 - 0.8298189044 0.8253703117 0.8248928189 - 0.8303630948 0.8259229064 0.8254395127 - 0.8308951855 0.8264755011 0.8259863257 - 0.8314273953 0.8270282149 0.8265330195 - 0.8319594860 0.8275808096 0.8270797729 - 0.8324916959 0.8281334043 0.8276265264 - 0.8330239058 0.8286859989 0.8281732798 - 0.8335559964 0.8292385936 0.8287199736 - 0.8340882063 0.8297913074 0.8292667866 - 0.8346202970 0.8303225040 0.8298134804 - 0.8351525068 0.8308408260 0.8303381801 - 0.8356845975 0.8313590884 0.8308513761 - 0.8362168074 0.8318774104 0.8313646913 - 0.8367488980 0.8323956728 0.8318778872 - 0.8372811079 0.8329139948 0.8323912024 - 0.8378131986 0.8334323168 0.8329045177 - 0.8383454084 0.8339505792 0.8334177136 - 0.8388774991 0.8344689012 0.8339310288 - 0.8394097090 0.8349872231 0.8344442248 - 0.8399417996 0.8355054855 0.8349574804 - 0.8404424191 0.8360238075 0.8354706764 - 0.8409391046 0.8365421295 0.8359839916 - 0.8414357901 0.8370603919 0.8364971876 - 0.8419324756 0.8375787139 0.8370105028 - 0.8424291015 0.8380969763 0.8375238180 - 0.8429257870 0.8386152983 0.8380370140 - 0.8434224725 0.8391336203 0.8385503292 - 0.8439192176 0.8396518826 0.8390635252 - 0.8444159031 0.8401595950 0.8395767808 - 0.8449125886 0.8406456113 0.8400844932 - 0.8454092741 0.8411316872 0.8405662775 - 0.8459059000 0.8416177034 0.8410481215 - 0.8464025855 0.8421037793 0.8415299058 - 0.8468992710 0.8425897956 0.8420116901 - 0.8473960161 0.8430758119 0.8424934745 - 0.8478927016 0.8435618877 0.8429753184 - 0.8483893871 0.8440479040 0.8434571028 - 0.8488860130 0.8445339799 0.8439388871 - 0.8493826985 0.8450199962 0.8444206715 - 0.8498793840 0.8455060720 0.8449025154 - 0.8503509760 0.8459920883 0.8453842998 - 0.8508145809 0.8464781046 0.8458660841 - 0.8512781858 0.8469641805 0.8463479280 - 0.8517417908 0.8474501967 0.8468297124 - 0.8522053957 0.8479362726 0.8473114967 - 0.8526690006 0.8484222889 0.8477932811 - 0.8531326056 0.8489084244 0.8482751250 - 0.8535962105 0.8493943810 0.8487569094 - 0.8540596962 0.8498803973 0.8492386937 - 0.8545233011 0.8503437042 0.8497204781 - 0.8549869061 0.8507995009 0.8501899242 - 0.8554505110 0.8512551785 0.8506422043 - 0.8559141159 0.8517109752 0.8510944843 - 0.8563777208 0.8521667719 0.8515468240 - 0.8568413258 0.8526226282 0.8519991040 - 0.8573048711 0.8530784249 0.8524513841 - 0.8577684760 0.8535342216 0.8529037237 - 0.8582320213 0.8539898992 0.8533560038 - 0.8586956263 0.8544456959 0.8538082838 - 0.8591591716 0.8549014926 0.8542606235 - 0.8596227765 0.8553572893 0.8547129035 - 0.8600807190 0.8558130860 0.8551651835 - 0.8605133891 0.8562688828 0.8556174040 - 0.8609461784 0.8567246795 0.8560696840 - 0.8613789082 0.8571804166 0.8565220237 - 0.8618116975 0.8576362133 0.8569743037 - 0.8622444272 0.8580920100 0.8574265838 - 0.8626772165 0.8585478067 0.8578789234 - 0.8631098866 0.8590036035 0.8583312035 - 0.8635426760 0.8594594002 0.8587834835 - 0.8639754057 0.8599150777 0.8592358232 - 0.8644081950 0.8603479266 0.8596881032 - 0.8648409247 0.8607752919 0.8601318002 - 0.8652737141 0.8612027168 0.8605564237 - 0.8657063842 0.8616302013 0.8609811068 - 0.8661391735 0.8620576262 0.8614056706 - 0.8665719032 0.8624849916 0.8618304133 - 0.8670045733 0.8629124761 0.8622549772 - 0.8674374223 0.8633399010 0.8626796007 - 0.8678700924 0.8637673855 0.8631042838 - 0.8683028817 0.8641948104 0.8635289073 - 0.8687356114 0.8646221757 0.8639535904 - 0.8691684008 0.8650497198 0.8643782139 - 0.8696010709 0.8654770851 0.8648027778 - 0.8700317144 0.8659045100 0.8652275205 - 0.8704357147 0.8663319945 0.8656520844 - 0.8708397746 0.8667594194 0.8660768270 - 0.8712437749 0.8671867847 0.8665013909 - 0.8716478944 0.8676143289 0.8669260740 - 0.8720518947 0.8680416942 0.8673506975 - 0.8724560142 0.8684691191 0.8677753210 - 0.8728600144 0.8688966036 0.8682000041 - 0.8732640743 0.8693240285 0.8686246276 - 0.8736680746 0.8697515130 0.8690493107 - 0.8740721941 0.8701677918 0.8694738746 - 0.8744761944 0.8705686927 0.8698986173 - 0.8748803139 0.8709695935 0.8703035116 - 0.8752843142 0.8713704944 0.8707023263 - 0.8756883740 0.8717713952 0.8711010218 - 0.8760923743 0.8721722960 0.8714997768 - 0.8764964938 0.8725731969 0.8718984723 - 0.8769004941 0.8729740977 0.8722972870 - 0.8773046136 0.8733749986 0.8726959825 - 0.8777086139 0.8737758994 0.8730947971 - 0.8781126738 0.8741769195 0.8734936118 - 0.8785166740 0.8745778203 0.8738923073 - 0.8789207935 0.8749787211 0.8742911220 - 0.8793247938 0.8753796220 0.8746898174 - 0.8797289133 0.8757805228 0.8750885725 - 0.8801240921 0.8761814237 0.8754873276 - 0.8805015087 0.8765823245 0.8758860826 - 0.8808789253 0.8769832253 0.8762847781 - 0.8812562227 0.8773841262 0.8766835928 - 0.8816335797 0.8777850270 0.8770824075 - 0.8820109963 0.8781859279 0.8774811029 - 0.8823882937 0.8785868287 0.8778799176 - 0.8827657104 0.8789877295 0.8782786131 - 0.8831431270 0.8793885708 0.8786774278 - 0.8835204244 0.8797894716 0.8790761232 - 0.8838977814 0.8801786900 0.8794748783 - 0.8842751980 0.8805547953 0.8798736930 - 0.8846524954 0.8809309006 0.8802558780 - 0.8850299120 0.8813070059 0.8806303740 - 0.8854073286 0.8816831112 0.8810049891 - 0.8857846260 0.8820592165 0.8813794851 - 0.8861619830 0.8824353218 0.8817539811 - 0.8865392804 0.8828114271 0.8821285963 - 0.8869166970 0.8831874728 0.8825030923 - 0.8872941136 0.8835635781 0.8828777075 - 0.8876714110 0.8839396834 0.8832522035 - 0.8880488276 0.8843157887 0.8836268187 - 0.8884261847 0.8846918941 0.8840013146 - 0.8888034821 0.8850679994 0.8843758106 - 0.8891808987 0.8854441047 0.8847504258 - 0.8895583153 0.8858202100 0.8851249218 - 0.8899356127 0.8861963153 0.8854994774 - 0.8902924061 0.8865724206 0.8858739734 - 0.8906450272 0.8869485259 0.8862485886 - 0.8909975886 0.8873245716 0.8866230845 - 0.8913502097 0.8877006769 0.8869975805 - 0.8917027116 0.8880767822 0.8873721957 - 0.8920552731 0.8884528875 0.8877466917 - 0.8924078941 0.8888289928 0.8881213069 - 0.8927605152 0.8892050982 0.8884958029 - 0.8931130171 0.8895812035 0.8888702989 - 0.8934655786 0.8899573088 0.8892449141 - 0.8938181996 0.8903127909 0.8896194100 - 0.8941708207 0.8906658292 0.8899940252 - 0.8945233226 0.8910186887 0.8903462291 - 0.8948758841 0.8913716078 0.8906980753 - 0.8952285051 0.8917245269 0.8910499811 - 0.8955811262 0.8920775056 0.8914020061 - 0.8959336281 0.8924304247 0.8917539120 - 0.8962861896 0.8927832842 0.8921058178 - 0.8966388106 0.8931362033 0.8924577236 - 0.8969913721 0.8934891820 0.8928096294 - 0.8973438740 0.8938421011 0.8931614757 - 0.8976964951 0.8941950202 0.8935133815 - 0.8980491161 0.8945478797 0.8938652873 - 0.8984016776 0.8949009180 0.8942171931 - 0.8987541795 0.8952537775 0.8945690989 - 0.8991068006 0.8956066966 0.8949210048 - 0.8994594216 0.8959596157 0.8952729106 - 0.8998119831 0.8963125944 0.8956248164 - 0.9001538157 0.8966655135 0.8959767222 - 0.9004834294 0.8970183730 0.8963286281 - 0.9008129239 0.8973712921 0.8966804743 - 0.9011424780 0.8977242708 0.8970323801 - 0.9014719725 0.8980771899 0.8973842859 - 0.9018015862 0.8984301090 0.8977361917 - 0.9021310806 0.8987830281 0.8980880976 - 0.9024606943 0.8991360068 0.8984400034 - 0.9027901888 0.8994889259 0.8987919092 - 0.9031198025 0.8998417854 0.8991438150 - 0.9034494162 0.9001827836 0.8994957209 - 0.9037789106 0.9005141258 0.8998476267 - 0.9041085243 0.9008454084 0.9001874924 - 0.9044380188 0.9011766911 0.9005182981 - 0.9047675729 0.9015079141 0.9008489847 - 0.9050971270 0.9018391967 0.9011797905 - 0.9054266810 0.9021704793 0.9015104771 - 0.9057561755 0.9025018215 0.9018412828 - 0.9060857892 0.9028331041 0.9021720290 - 0.9064154029 0.9031643867 0.9025027752 - 0.9067448974 0.9034956098 0.9028335214 - 0.9070745111 0.9038268924 0.9031643271 - 0.9074040055 0.9041581750 0.9034950137 - 0.9077336192 0.9044895172 0.9038258195 - 0.9080631137 0.9048207998 0.9041565061 - 0.9083927274 0.9051520824 0.9044873118 - 0.9087222219 0.9054833055 0.9048179984 - 0.9090517759 0.9058145881 0.9051488042 - 0.9093812704 0.9061458707 0.9054794908 - 0.9097108841 0.9064772129 0.9058102965 - 0.9100378156 0.9068084955 0.9061409831 - 0.9103459716 0.9071397781 0.9064717889 - 0.9106541872 0.9074711204 0.9068024755 - 0.9109624028 0.9078022838 0.9071332812 - 0.9112706184 0.9081336260 0.9074640274 - 0.9115787745 0.9084649086 0.9077947140 - 0.9118869901 0.9087961912 0.9081255198 - 0.9121950865 0.9091274738 0.9084562063 - 0.9125033021 0.9094588161 0.9087870121 - 0.9128115177 0.9097899795 0.9091176987 - 0.9131196737 0.9101138711 0.9094485044 - 0.9134278893 0.9104250073 0.9097791910 - 0.9137361050 0.9107360840 0.9101033807 - 0.9140443206 0.9110472202 0.9104143977 - 0.9143524766 0.9113582969 0.9107254148 - 0.9146605730 0.9116693735 0.9110363722 - 0.9149687886 0.9119803905 0.9113473892 - 0.9152770042 0.9122915268 0.9116582870 - 0.9155852199 0.9126026034 0.9119693041 - 0.9158933759 0.9129136801 0.9122803211 - 0.9162015915 0.9132248163 0.9125912786 - 0.9165098071 0.9135358930 0.9129022956 - 0.9168180227 0.9138470292 0.9132133126 - 0.9171261191 0.9141579866 0.9135242105 - 0.9174342752 0.9144691229 0.9138352275 - 0.9177424908 0.9147801995 0.9141461849 - 0.9180507064 0.9150912762 0.9144572020 - 0.9183589220 0.9154024124 0.9147682190 - 0.9186670780 0.9157134891 0.9150791764 - 0.9189752936 0.9160246253 0.9153901935 - 0.9192835093 0.9163355827 0.9157010913 - 0.9195916057 0.9166467190 0.9160121083 - 0.9198998213 0.9169577956 0.9163231254 - 0.9201946259 0.9172688723 0.9166340828 - 0.9204829931 0.9175800085 0.9169450998 - 0.9207713008 0.9178910851 0.9172561169 - 0.9210597277 0.9182022214 0.9175670147 - 0.9213480949 0.9185131788 0.9178779721 - 0.9216364026 0.9188243151 0.9181889892 - 0.9219248295 0.9191353917 0.9185000062 - 0.9222130775 0.9194465280 0.9188110232 - 0.9225015044 0.9197576046 0.9191219807 - 0.9227898717 0.9200645089 0.9194329977 - 0.9230781794 0.9203566909 0.9197438955 - 0.9233666062 0.9206489921 0.9200516939 - 0.9236549139 0.9209411740 0.9203441739 - 0.9239432812 0.9212334752 0.9206367135 - 0.9242315888 0.9215257168 0.9209291935 - 0.9245200157 0.9218178988 0.9212217927 - 0.9248083830 0.9221101999 0.9215142727 - 0.9250966907 0.9224023819 0.9218068123 - 0.9253851175 0.9226946235 0.9220994115 - 0.9256734252 0.9229869246 0.9223918915 - 0.9259617925 0.9232791066 0.9226843715 - 0.9262501001 0.9235714078 0.9229769111 - 0.9265385270 0.9238635898 0.9232695103 - 0.9268268943 0.9241557717 0.9235619903 - 0.9271152020 0.9244480729 0.9238544703 - 0.9274036288 0.9247403145 0.9241470098 - 0.9276918769 0.9250324965 0.9244396091 - 0.9279803038 0.9253247976 0.9247320890 - 0.9282686710 0.9256169796 0.9250246286 - 0.9285569787 0.9259092808 0.9253171086 - 0.9288454056 0.9262015224 0.9256097078 - 0.9291337132 0.9264937043 0.9259021878 - 0.9294220805 0.9267860055 0.9261947274 - 0.9297103882 0.9270781875 0.9264872074 - 0.9299988151 0.9273704886 0.9267798066 - 0.9302688241 0.9276626706 0.9270722866 - 0.9305387735 0.9279549122 0.9273648262 - 0.9308087826 0.9282472134 0.9276573062 - 0.9310786724 0.9285393953 0.9279499054 - 0.9313486814 0.9288315773 0.9282423854 - 0.9316185713 0.9291238785 0.9285349250 - 0.9318885803 0.9294161201 0.9288275242 - 0.9321585298 0.9297084212 0.9291200042 - 0.9324284792 0.9300006032 0.9294124842 - 0.9326984882 0.9302752018 0.9297050238 - 0.9329683781 0.9305499196 0.9299976230 - 0.9332383871 0.9308245182 0.9302729964 - 0.9335082769 0.9310991764 0.9305483103 - 0.9337782860 0.9313737750 0.9308236241 - 0.9340481758 0.9316484928 0.9310988784 - 0.9343181849 0.9319230914 0.9313740730 - 0.9345881939 0.9321978092 0.9316493869 - 0.9348580837 0.9324725270 0.9319247007 - 0.9351280928 0.9327471256 0.9322000146 - 0.9353979826 0.9330217838 0.9324753284 - 0.9356679916 0.9332963824 0.9327505827 - 0.9359378815 0.9335711002 0.9330258965 - 0.9362078905 0.9338456988 0.9333012104 - 0.9364778996 0.9341204166 0.9335764050 - 0.9367477894 0.9343950152 0.9338517189 - 0.9370177984 0.9346696734 0.9341269732 - 0.9372876883 0.9349442720 0.9344022870 - 0.9375576973 0.9352189898 0.9346776009 - 0.9378277063 0.9354937077 0.9349529147 - 0.9380975962 0.9357683063 0.9352282286 - 0.9383676052 0.9360430241 0.9355034828 - 0.9386374950 0.9363176227 0.9357786775 - 0.9389075041 0.9365922809 0.9360539913 - 0.9391773939 0.9368668795 0.9363293052 - 0.9394474030 0.9371415973 0.9366046190 - 0.9397174120 0.9374161959 0.9368798733 - 0.9399873018 0.9376909137 0.9371551871 - 0.9402409792 0.9379655719 0.9374305010 - 0.9404938817 0.9382401705 0.9377058148 - 0.9407467842 0.9385148883 0.9379811287 - 0.9409996271 0.9387894869 0.9382563233 - 0.9412525296 0.9390642047 0.9385315776 - 0.9415053725 0.9393388033 0.9388068914 - 0.9417582750 0.9396135211 0.9390822053 - 0.9420111775 0.9398881197 0.9393575191 - 0.9422640800 0.9401531219 0.9396327734 - 0.9425169230 0.9404113293 0.9399080873 - 0.9427698255 0.9406695962 0.9401726127 - 0.9430227280 0.9409278035 0.9404317737 - 0.9432755709 0.9411860704 0.9406909943 - 0.9435284734 0.9414442778 0.9409502149 - 0.9437813759 0.9417026043 0.9412093759 - 0.9440342188 0.9419608116 0.9414685965 - 0.9442871213 0.9422190785 0.9417278171 - 0.9445400238 0.9424772859 0.9419869781 - 0.9447929263 0.9427356124 0.9422461987 - 0.9450458288 0.9429938197 0.9425053000 - 0.9452986717 0.9432520866 0.9427645206 - 0.9455515146 0.9435104132 0.9430236816 - 0.9458044171 0.9437686205 0.9432829022 - 0.9460573196 0.9440268874 0.9435421228 - 0.9463102221 0.9442850947 0.9438012838 - 0.9465631247 0.9445434213 0.9440605044 - 0.9468160272 0.9448016286 0.9443197250 - 0.9470688105 0.9450598955 0.9445788860 - 0.9473217130 0.9453181028 0.9448379874 - 0.9475746155 0.9455764294 0.9450972080 - 0.9478275180 0.9458345771 0.9453564286 - 0.9480804205 0.9460929036 0.9456155896 - 0.9483332038 0.9463511109 0.9458748102 - 0.9485861063 0.9466093779 0.9461339712 - 0.9488390088 0.9468675852 0.9463931918 - 0.9490919113 0.9471259117 0.9466524124 - 0.9493448138 0.9473841190 0.9469115734 - 0.9495977163 0.9476423860 0.9471707940 - 0.9498504996 0.9479007125 0.9474298954 - 0.9500969052 0.9481589198 0.9476891160 - 0.9503340125 0.9484171867 0.9479482770 - 0.9505710006 0.9486753941 0.9482074976 - 0.9508081079 0.9489337206 0.9484667182 - 0.9510450959 0.9491919279 0.9487258792 - 0.9512820840 0.9494501948 0.9489850998 - 0.9515191913 0.9497084022 0.9492443204 - 0.9517561793 0.9499667287 0.9495034814 - 0.9519932270 0.9502115846 0.9497625828 - 0.9522302747 0.9504545927 0.9500206113 - 0.9524673223 0.9506974816 0.9502646923 - 0.9527043104 0.9509404898 0.9505088925 - 0.9529414177 0.9511833787 0.9507529736 - 0.9531784058 0.9514263868 0.9509971738 - 0.9534155130 0.9516692758 0.9512413144 - 0.9536525011 0.9519122839 0.9514855146 - 0.9538894892 0.9521551728 0.9517297149 - 0.9541265965 0.9523981810 0.9519737959 - 0.9543635845 0.9526411295 0.9522179961 - 0.9546005726 0.9528840780 0.9524620771 - 0.9548376799 0.9531270266 0.9527062774 - 0.9550747275 0.9533699751 0.9529504180 - 0.9553117156 0.9536129236 0.9531946182 - 0.9555488229 0.9538558722 0.9534386992 - 0.9557858109 0.9540988207 0.9536828995 - 0.9560227990 0.9543418288 0.9539269805 - 0.9562599063 0.9545847178 0.9541711807 - 0.9564968944 0.9548277259 0.9544153810 - 0.9567340016 0.9550706148 0.9546595216 - 0.9569709897 0.9553136230 0.9549037218 - 0.9572079778 0.9555565119 0.9551478028 - 0.9574450850 0.9557995200 0.9553920031 - 0.9576820731 0.9560424089 0.9556360841 - 0.9579191208 0.9562854171 0.9558802843 - 0.9581562281 0.9565284252 0.9561244249 - 0.9583932161 0.9567713141 0.9563686252 - 0.9586302042 0.9570143223 0.9566128254 - 0.9588673115 0.9572572112 0.9568569064 - 0.9591042995 0.9575002193 0.9571011066 - 0.9593414068 0.9577431083 0.9573451877 - 0.9595783949 0.9579861164 0.9575893879 - 0.9598153830 0.9582290053 0.9578335285 - 0.9600492120 0.9584720135 0.9580777287 - 0.9602714777 0.9587149024 0.9583218098 - 0.9604939222 0.9589579105 0.9585660100 - 0.9607161880 0.9592007995 0.9588100910 - 0.9609385133 0.9594438076 0.9590542912 - 0.9611607790 0.9596866965 0.9592984915 - 0.9613832235 0.9599297047 0.9595425725 - 0.9616054893 0.9601625204 0.9597867727 - 0.9618278146 0.9603912234 0.9600291252 - 0.9620500803 0.9606198072 0.9602593184 - 0.9622725248 0.9608485103 0.9604893923 - 0.9624947906 0.9610772133 0.9607195258 - 0.9627171159 0.9613059163 0.9609495997 - 0.9629393816 0.9615346193 0.9611796737 - 0.9631618261 0.9617632031 0.9614098072 - 0.9633840919 0.9619919062 0.9616398811 - 0.9636064172 0.9622206092 0.9618700147 - 0.9638286829 0.9624493122 0.9621000886 - 0.9640511274 0.9626778960 0.9623302817 - 0.9642733932 0.9629065990 0.9625604153 - 0.9644957185 0.9631353021 0.9627904892 - 0.9647179842 0.9633640051 0.9630206227 - 0.9649404287 0.9635927081 0.9632506967 - 0.9651626945 0.9638212919 0.9634807706 - 0.9653850198 0.9640499949 0.9637109041 - 0.9656072855 0.9642786980 0.9639409781 - 0.9658296704 0.9645074010 0.9641711712 - 0.9660519958 0.9647359848 0.9644013047 - 0.9662743211 0.9649646878 0.9646313787 - 0.9664965868 0.9651933908 0.9648615122 - 0.9667189717 0.9654220939 0.9650915861 - 0.9669412971 0.9656506777 0.9653217196 - 0.9671636224 0.9658793807 0.9655517936 - 0.9673860073 0.9661080837 0.9657819271 - 0.9676082730 0.9663367867 0.9660120010 - 0.9678305984 0.9665654898 0.9662421942 - 0.9680529237 0.9667940736 0.9664723277 - 0.9682753086 0.9670227766 0.9667024016 - 0.9684975743 0.9672514796 0.9669324756 - 0.9687198997 0.9674801826 0.9671626091 - 0.9689422250 0.9677088261 0.9673926830 - 0.9691646099 0.9679375291 0.9676228166 - 0.9693868756 0.9681661725 0.9678528905 - 0.9696092010 0.9683948755 0.9680830836 - 0.9698315263 0.9686235785 0.9683132172 - 0.9700505733 0.9688522220 0.9685432911 - 0.9702591896 0.9690809250 0.9687734246 - 0.9704679251 0.9693096280 0.9690034986 - 0.9706764817 0.9695382714 0.9692335725 - 0.9708852172 0.9697669148 0.9694637060 - 0.9710938931 0.9699956179 0.9696937799 - 0.9713025093 0.9702112079 0.9699239135 - 0.9715111852 0.9704266191 0.9701452851 - 0.9717199206 0.9706419110 0.9703623056 - 0.9719284773 0.9708573222 0.9705793262 - 0.9721372128 0.9710726738 0.9707962871 - 0.9723458886 0.9712880254 0.9710131884 - 0.9725545049 0.9715033770 0.9712302089 - 0.9727631807 0.9717187285 0.9714472294 - 0.9729719162 0.9719340801 0.9716641903 - 0.9731804729 0.9721494913 0.9718812108 - 0.9733892083 0.9723647833 0.9720981717 - 0.9735978842 0.9725801945 0.9723151922 - 0.9738065004 0.9727954865 0.9725322127 - 0.9740151763 0.9730108976 0.9727491736 - 0.9742239118 0.9732261896 0.9729661942 - 0.9744325280 0.9734416008 0.9731832147 - 0.9746412039 0.9736570120 0.9734001756 - 0.9748498797 0.9738723040 0.9736170769 - 0.9750584960 0.9740877151 0.9738340974 - 0.9752671719 0.9743030071 0.9740511179 - 0.9754759073 0.9745184183 0.9742680788 - 0.9756845236 0.9747338295 0.9744850993 - 0.9758931994 0.9749491215 0.9747021198 - 0.9761018753 0.9751644731 0.9749190807 - 0.9763104916 0.9753798246 0.9751361012 - 0.9765192270 0.9755951762 0.9753531218 - 0.9767279029 0.9758105278 0.9755700827 - 0.9769365191 0.9760258794 0.9757871032 - 0.9771451950 0.9762412906 0.9760041237 - 0.9773538709 0.9764565825 0.9762210846 - 0.9775624871 0.9766719937 0.9764379859 - 0.9777712226 0.9768872857 0.9766550064 - 0.9779798985 0.9771026969 0.9768720269 - 0.9781885147 0.9773179889 0.9770889878 - 0.9783971906 0.9775334001 0.9773060083 - 0.9786059260 0.9777488112 0.9775230289 - 0.9788144827 0.9779641032 0.9777399898 - 0.9790232182 0.9781795144 0.9779570103 - 0.9792318940 0.9783948064 0.9781739712 - 0.9794405103 0.9786102176 0.9783909917 - 0.9796491861 0.9788256288 0.9786080122 - 0.9798579216 0.9790409207 0.9788249731 - 0.9800624847 0.9792562723 0.9790418744 - 0.9802585244 0.9794716239 0.9792588949 - 0.9804543853 0.9796869755 0.9794759154 - 0.9806504250 0.9799023271 0.9796928763 - 0.9808464050 0.9801108837 0.9799098969 - 0.9810423851 0.9803137779 0.9801197052 - 0.9812384248 0.9805167913 0.9803245068 - 0.9814342856 0.9807196856 0.9805291891 - 0.9816303253 0.9809225798 0.9807338715 - 0.9818263054 0.9811254740 0.9809386730 - 0.9820222855 0.9813284874 0.9811434150 - 0.9822182059 0.9815313816 0.9813480973 - 0.9824141860 0.9817342758 0.9815528989 - 0.9826102257 0.9819372892 0.9817575812 - 0.9828062057 0.9821401834 0.9819623232 - 0.9830021858 0.9823430777 0.9821671247 - 0.9831981063 0.9825459719 0.9823718071 - 0.9833940864 0.9827489853 0.9825764894 - 0.9835901260 0.9829518795 0.9827812910 - 0.9837861061 0.9831547737 0.9829859734 - 0.9839820266 0.9833577275 0.9831907153 - 0.9841780066 0.9835606813 0.9833955169 - 0.9843739867 0.9837635756 0.9836001992 - 0.9845700264 0.9839665294 0.9838048816 - 0.9847660065 0.9841694832 0.9840096235 - 0.9849619269 0.9843723774 0.9842144251 - 0.9851579070 0.9845752716 0.9844191074 - 0.9853538871 0.9847782254 0.9846237898 - 0.9855499268 0.9849811792 0.9848285913 - 0.9857457876 0.9851840734 0.9850332737 - 0.9859418273 0.9853870273 0.9852380157 - 0.9861378074 0.9855899215 0.9854428172 - 0.9863337874 0.9857928753 0.9856474996 - 0.9865298271 0.9859958291 0.9858521819 - 0.9867256880 0.9861987233 0.9860569835 - 0.9869217277 0.9864016771 0.9862617254 - 0.9871177077 0.9866045713 0.9864664078 - 0.9873136878 0.9868075252 0.9866712093 - 0.9875096083 0.9870104194 0.9868758917 - 0.9877055883 0.9872133732 0.9870805740 - 0.9879016280 0.9874163270 0.9872853756 - 0.9880976081 0.9876192212 0.9874901175 - 0.9882935882 0.9878221154 0.9876947999 - 0.9884895086 0.9880251288 0.9878996015 - 0.9886854887 0.9882280231 0.9881042838 - 0.9888815284 0.9884309173 0.9883090258 - 0.9890775084 0.9886338711 0.9885138273 - 0.9892734289 0.9888368249 0.9887185097 - 0.9894694090 0.9890397191 0.9889231920 - 0.9896653891 0.9892426133 0.9891278744 - 0.9898614287 0.9894456267 0.9893326759 - 0.9900538921 0.9896485209 0.9895374179 - 0.9902380705 0.9898514152 0.9897421002 - 0.9904223084 0.9900512099 0.9899469018 - 0.9906064868 0.9902426004 0.9901431203 - 0.9907907248 0.9904338717 0.9903364182 - 0.9909747839 0.9906252027 0.9905297160 - 0.9911590219 0.9908164740 0.9907228947 - 0.9913432002 0.9910078049 0.9909161925 - 0.9915273786 0.9911991954 0.9911094904 - 0.9917116165 0.9913905263 0.9913027883 - 0.9918957949 0.9915817976 0.9914960265 - 0.9920799732 0.9917731285 0.9916893244 - 0.9922642112 0.9919645190 0.9918826222 - 0.9924482703 0.9921557903 0.9920758009 - 0.9926325083 0.9923471212 0.9922690988 - 0.9928166866 0.9925383925 0.9924623966 - 0.9930009246 0.9927297235 0.9926556945 - 0.9931851029 0.9929211140 0.9928488731 - 0.9933692813 0.9931123853 0.9930421710 - 0.9935535192 0.9933037162 0.9932355285 - 0.9937375784 0.9934949875 0.9934287071 - 0.9939218163 0.9936863780 0.9936220050 - 0.9941059947 0.9938777089 0.9938153028 - 0.9942901731 0.9940689802 0.9940086007 - 0.9944744110 0.9942603111 0.9942017794 - 0.9946585894 0.9944517016 0.9943950772 - 0.9948428273 0.9946429729 0.9945883751 - 0.9950270057 0.9948343039 0.9947816730 - 0.9952111244 0.9950255752 0.9949749112 - 0.9953953028 0.9952169061 0.9951682091 - 0.9955794811 0.9954082966 0.9953615069 - 0.9957637191 0.9955996275 0.9955546856 - 0.9959478974 0.9957908988 0.9957479835 - 0.9961320758 0.9959822297 0.9959412813 - 0.9963163137 0.9961736202 0.9961345792 - 0.9965003729 0.9963648915 0.9963278174 - 0.9966846108 0.9965562224 0.9965211153 - 0.9968687892 0.9967474937 0.9967144132 - 0.9970530272 0.9969388247 0.9969075918 - 0.9972372055 0.9971302152 0.9971008897 - 0.9974213839 0.9973214865 0.9972941875 - 0.9976056218 0.9975128174 0.9974874854 - 0.9977898002 0.9977040887 0.9976807237 - 0.9979739189 0.9978954792 0.9978740215 - 0.9981580973 0.9980868101 0.9980673194 - 0.9983422756 0.9982780814 0.9982606173 - 0.9985265136 0.9984694123 0.9984537959 - 0.9987106919 0.9986606836 0.9986470938 - 0.9988948703 0.9988520741 0.9988403916 - 0.9990791082 0.9990434051 0.9990335703 - 0.9992632866 0.9992346764 0.9992269278 - 0.9994474053 0.9994260073 0.9994202256 - 0.9996315837 0.9996173978 0.9996135235 - 0.9998158216 0.9998087287 0.9998067021 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0031291239 0.0033278200 0.0041153808 + 0.0062582470 0.0066556409 0.0082307607 + 0.0093873711 0.0099834614 0.0125252204 + 0.0128003703 0.0136352098 0.0169547293 + 0.0162824895 0.0172885899 0.0214768499 + 0.0197646003 0.0210279003 0.0262027308 + 0.0235934593 0.0250145998 0.0309809893 + 0.0274474602 0.0290013105 0.0359734409 + 0.0314300507 0.0332363509 0.0410094596 + 0.0356648304 0.0375544094 0.0462271497 + 0.0398996100 0.0420100689 0.0514931008 + 0.0445012413 0.0466454811 0.0568850897 + 0.0491117798 0.0513610095 0.0623264983 + 0.0540077388 0.0562863797 0.0678355098 + 0.0589718111 0.0612730384 0.0733793527 + 0.0641835630 0.0664475709 0.0789457336 + 0.0694600120 0.0716837272 0.0845116675 + 0.0749636367 0.0770548731 0.0900767818 + 0.0805091709 0.0824873224 0.0955895111 + 0.0862180665 0.0879941806 0.1010826975 + 0.0919598788 0.0935461521 0.1064976007 + 0.0977663025 0.0991239473 0.1118654013 + 0.1035818979 0.1047077999 0.1171469018 + 0.1094032973 0.1102901995 0.1223551035 + 0.1151698977 0.1158241034 0.1274771988 + 0.1209099963 0.1213333011 0.1325096041 + 0.1265448928 0.1267662942 0.1374547929 + 0.1321122050 0.1321426034 0.1423096061 + 0.1375723034 0.1374354064 0.1470683962 + 0.1429159939 0.1426412016 0.1517543048 + 0.1481667012 0.1477651000 0.1563231945 + 0.1532676965 0.1527830958 0.1608552933 + 0.1582884043 0.1577192992 0.1652355939 + 0.1631516069 0.1625491977 0.1696159989 + 0.1679333001 0.1672876030 0.1738290042 + 0.1725793928 0.1719401926 0.1780259013 + 0.1771221012 0.1764778048 0.1821296066 + 0.1815796942 0.1809709072 0.1861504018 + 0.1858897954 0.1853097975 0.1901641935 + 0.1901896000 0.1896487027 0.1940180957 + 0.1942777932 0.1938105971 0.1978719980 + 0.1983660012 0.1979569942 0.2016555071 + 0.2023289055 0.2020100057 0.2053523958 + 0.2062083930 0.2059722990 0.2090492994 + 0.2100834996 0.2099346966 0.2126373947 + 0.2137690037 0.2137262970 0.2161877006 + 0.2174544036 0.2175149024 0.2197380066 + 0.2210844010 0.2212477028 0.2231622040 + 0.2245907933 0.2248737961 0.2265762985 + 0.2280972004 0.2284999043 0.2299904972 + 0.2315285057 0.2320373952 0.2332790941 + 0.2348707020 0.2355124056 0.2365673035 + 0.2382130027 0.2389874011 0.2398556024 + 0.2414855063 0.2423633039 0.2430329025 + 0.2446777970 0.2456984967 0.2462050021 + 0.2478702068 0.2490337044 0.2493772060 + 0.2510173023 0.2522774041 0.2524636090 + 0.2540734112 0.2554838061 0.2555291057 + 0.2571294904 0.2586902976 0.2585946023 + 0.2601780891 0.2618266940 0.2616071105 + 0.2631106079 0.2649149001 0.2645747066 + 0.2660430968 0.2680031061 0.2675423026 + 0.2689755857 0.2710529864 0.2704944909 + 0.2718352973 0.2740328908 0.2733725905 + 0.2746559978 0.2770127952 0.2762506008 + 0.2774766088 0.2799926102 0.2791286111 + 0.2802866101 0.2828738093 0.2819496095 + 0.2830061018 0.2857545912 0.2847456932 + 0.2857257128 0.2886354923 0.2875418067 + 0.2884452939 0.2914687991 0.2903288901 + 0.2911258042 0.2942593992 0.2930502892 + 0.2937541902 0.2970499098 0.2957716882 + 0.2963826954 0.2998403907 0.2984930873 + 0.2990111113 0.3025532961 0.3011840880 + 0.3015882969 0.3052614927 0.3038372993 + 0.3041346967 0.3079696894 0.3064906001 + 0.3066810966 0.3106591105 0.3091438115 + 0.3092274964 0.3132922947 0.3117550015 + 0.3117223978 0.3159255981 0.3143461943 + 0.3141950965 0.3185588121 0.3169373870 + 0.3166677058 0.3211610913 0.3195286095 + 0.3191402853 0.3237262070 0.3220736086 + 0.3215698004 0.3262912929 0.3246082962 + 0.3239761889 0.3288562894 0.3271430135 + 0.3263826966 0.3313871026 0.3296777904 + 0.3287890851 0.3338901997 0.3321677148 + 0.3311660886 0.3363932967 0.3346511126 + 0.3335132897 0.3388963938 0.3371345103 + 0.3358604908 0.3413681090 0.3396179080 + 0.3382078111 0.3438149989 0.3420617878 + 0.3405424953 0.3462618887 0.3444986045 + 0.3428367972 0.3487088084 0.3469353914 + 0.3451310992 0.3511315882 0.3493720889 + 0.3474254012 0.3535273969 0.3517774940 + 0.3497197032 0.3559232950 0.3541719019 + 0.3519726098 0.3583191037 0.3565663099 + 0.3542197943 0.3607012033 0.3589606881 + 0.3564670086 0.3630507886 0.3613333106 + 0.3587141931 0.3654003143 0.3636893034 + 0.3609434068 0.3677498996 0.3660452068 + 0.3631486893 0.3700976968 0.3684011996 + 0.3653540909 0.3724052906 0.3707458973 + 0.3675594032 0.3747127950 0.3730669022 + 0.3697646856 0.3770203888 0.3753877878 + 0.3719370067 0.3793280125 0.3777087927 + 0.3741051853 0.3816084862 0.3800294101 + 0.3762735128 0.3838779926 0.3823184967 + 0.3784418106 0.3861474097 0.3846076131 + 0.3806008995 0.3884168863 0.3868967891 + 0.3827365041 0.3906759024 0.3891859055 + 0.3848721981 0.3929106891 0.3914563060 + 0.3870078027 0.3951455057 0.3937163949 + 0.3891434073 0.3973802924 0.3959765136 + 0.3912619054 0.3996151090 0.3982365131 + 0.3933688104 0.4018237889 0.4004907012 + 0.3954758048 0.4040271044 0.4027242064 + 0.3975827098 0.4062303007 0.4049575925 + 0.3996897042 0.4084335864 0.4071910083 + 0.4017753005 0.4106284976 0.4094245136 + 0.4038572013 0.4128029048 0.4116396904 + 0.4059390128 0.4149773121 0.4138484895 + 0.4080209136 0.4171518087 0.4160574079 + 0.4101017118 0.4193261862 0.4182662964 + 0.4121617079 0.4214822948 0.4204702079 + 0.4142217934 0.4236302078 0.4226562977 + 0.4162817895 0.4257782102 0.4248422980 + 0.4183419049 0.4279260933 0.4270282984 + 0.4203982055 0.4300732017 0.4292142987 + 0.4224393070 0.4321965873 0.4313865900 + 0.4244804084 0.4343200028 0.4335511923 + 0.4265213907 0.4364433885 0.4357157946 + 0.4285624921 0.4385668933 0.4378803074 + 0.4305987060 0.4406827986 0.4400444925 + 0.4326233864 0.4427833855 0.4421885908 + 0.4346480072 0.4448839128 0.4443328083 + 0.4366726875 0.4469844997 0.4464769065 + 0.4386973083 0.4490849972 0.4486210942 + 0.4407168925 0.4511733949 0.4507581890 + 0.4427272975 0.4532522857 0.4528825879 + 0.4447377026 0.4553312063 0.4550068974 + 0.4467481077 0.4574100971 0.4571312964 + 0.4487585127 0.4594889879 0.4592556953 + 0.4507640898 0.4615522921 0.4613674879 + 0.4527620971 0.4636105001 0.4634723961 + 0.4547601044 0.4656687081 0.4655773938 + 0.4567581117 0.4677268863 0.4676823020 + 0.4587559998 0.4697850943 0.4697872102 + 0.4607498944 0.4718252122 0.4718747139 + 0.4627369046 0.4738632143 0.4739601016 + 0.4647239149 0.4759013057 0.4760456085 + 0.4667108953 0.4779393077 0.4781309962 + 0.4686979055 0.4799773097 0.4802143872 + 0.4706814885 0.4819954932 0.4822798967 + 0.4726586044 0.4840134084 0.4843454957 + 0.4746356905 0.4860314131 0.4864110053 + 0.4766128063 0.4880493879 0.4884765148 + 0.4785900116 0.4900667071 0.4905366004 + 0.4805643857 0.4920643866 0.4925814867 + 0.4825324118 0.4940621853 0.4946263134 + 0.4845002890 0.4960598946 0.4966711104 + 0.4864681959 0.4980576038 0.4987159967 + 0.4884361029 0.5000547767 0.5007526875 + 0.4904021919 0.5020316839 0.5027757287 + 0.4923611879 0.5040085912 0.5047987103 + 0.4943202138 0.5059854984 0.5068216920 + 0.4962792099 0.5079624057 0.5088446140 + 0.4982382059 0.5099393129 0.5108575821 + 0.5001962185 0.5118951201 0.5128573179 + 0.5021461844 0.5138502121 0.5148568749 + 0.5040960908 0.5158053041 0.5168566108 + 0.5060459971 0.5177603960 0.5188562870 + 0.5079959035 0.5197154880 0.5208451152 + 0.5099458098 0.5216509104 0.5228195786 + 0.5118862987 0.5235828757 0.5247941017 + 0.5138264894 0.5255150199 0.5267686248 + 0.5157666802 0.5274469852 0.5287430882 + 0.5177068710 0.5293790102 0.5307077169 + 0.5196471214 0.5312942863 0.5326548815 + 0.5215784907 0.5332015157 0.5346019864 + 0.5235080123 0.5351088047 0.5365492105 + 0.5254374146 0.5370160937 0.5384963751 + 0.5273668766 0.5389233828 0.5404369235 + 0.5292963982 0.5408189893 0.5423542857 + 0.5312179923 0.5426995754 0.5442718267 + 0.5331351161 0.5445801020 0.5461893082 + 0.5350522995 0.5464606881 0.5481067896 + 0.5369694829 0.5483412743 0.5500237942 + 0.5388866067 0.5502184033 0.5519090295 + 0.5407978296 0.5520700216 0.5537940860 + 0.5427005887 0.5539215803 0.5556792021 + 0.5446034074 0.5557731986 0.5575643778 + 0.5465062857 0.5576248169 0.5594494939 + 0.5484091043 0.5594763756 0.5613096952 + 0.5503091812 0.5613054037 0.5631597042 + 0.5521953106 0.5631254911 0.5650097132 + 0.5540813208 0.5649455786 0.5668596029 + 0.5559673905 0.5667657256 0.5687096119 + 0.5578534007 0.5685858130 0.5705479980 + 0.5597394705 0.5703982711 0.5723599195 + 0.5616086125 0.5721842051 0.5741717219 + 0.5634748936 0.5739700794 0.5759835839 + 0.5653412938 0.5757560730 0.5777953863 + 0.5672075748 0.5775420070 0.5796073079 + 0.5690739751 0.5793278813 0.5813869238 + 0.5709286928 0.5810908079 0.5831577182 + 0.5727720857 0.5828397274 0.5849283934 + 0.5746154189 0.5845885873 0.5866991878 + 0.5764586926 0.5863376260 0.5884699821 + 0.5783020854 0.5880864859 0.5902346969 + 0.5801432729 0.5898354053 0.5919613838 + 0.5819600224 0.5915482044 0.5936880708 + 0.5837767124 0.5932571888 0.5954148173 + 0.5855932832 0.5949661732 0.5971413851 + 0.5874099731 0.5966752172 0.5988680720 + 0.5892267227 0.5983842015 0.6005786061 + 0.5910258293 0.6000909209 0.6022583842 + 0.5928118229 0.6017571092 0.6039381027 + 0.5945978761 0.6034232974 0.6056178212 + 0.5963839889 0.6050894856 0.6072975993 + 0.5981701016 0.6067556739 0.6089773178 + 0.5999560952 0.6084219217 0.6106376052 + 0.6017082930 0.6100856066 0.6122676730 + 0.6034597158 0.6117061973 0.6138976812 + 0.6052110195 0.6133267879 0.6155278087 + 0.6069623828 0.6149473190 0.6171578765 + 0.6087138057 0.6165679097 0.6187878847 + 0.6104547977 0.6181885004 0.6204046011 + 0.6121671796 0.6198089719 0.6219825745 + 0.6138796806 0.6213870049 0.6235604882 + 0.6155921221 0.6229591966 0.6251384020 + 0.6173046231 0.6245315075 0.6267163754 + 0.6190170050 0.6261038184 0.6282942891 + 0.6207110882 0.6276760101 0.6298722029 + 0.6223804951 0.6292483211 0.6314002275 + 0.6240499020 0.6307939887 0.6329237819 + 0.6257191896 0.6323155761 0.6344473958 + 0.6273885965 0.6338371038 0.6359710097 + 0.6290580034 0.6353585720 0.6374946237 + 0.6307067871 0.6368800998 0.6390182972 + 0.6323291063 0.6384016275 0.6405218840 + 0.6339514256 0.6399230957 0.6419894099 + 0.6355736852 0.6413943768 0.6434568167 + 0.6371960044 0.6428629756 0.6449242830 + 0.6388183236 0.6443315744 0.6463916898 + 0.6404266953 0.6458001733 0.6478592157 + 0.6419981718 0.6472687721 0.6493266225 + 0.6435695887 0.6487373710 0.6507629156 + 0.6451410055 0.6501982808 0.6521728039 + 0.6467124820 0.6516122222 0.6535826921 + 0.6482838988 0.6530259848 0.6549925208 + 0.6498553753 0.6544399261 0.6564024091 + 0.6513776183 0.6558538079 0.6578121781 + 0.6528947949 0.6572676897 0.6592221260 + 0.6544119716 0.6586815119 0.6606057286 + 0.6559292078 0.6600915790 0.6619569063 + 0.6574463844 0.6614493132 0.6633082032 + 0.6589636207 0.6628069878 0.6646593809 + 0.6604626775 0.6641647220 0.6660106778 + 0.6619228125 0.6655225158 0.6673619151 + 0.6633828282 0.6668801904 0.6687130928 + 0.6648429036 0.6682379246 0.6700615883 + 0.6663029790 0.6695955992 0.6713535786 + 0.6677629948 0.6709131002 0.6726456285 + 0.6692231297 0.6722136736 0.6739376187 + 0.6706553102 0.6735141873 0.6752296090 + 0.6720557809 0.6748147011 0.6765217185 + 0.6734563112 0.6761152148 0.6778137088 + 0.6748567820 0.6774157286 0.6791056991 + 0.6762573719 0.6787163019 0.6803793907 + 0.6776579022 0.6800159812 0.6816120744 + 0.6790583730 0.6812587976 0.6828446984 + 0.6804388165 0.6825014949 0.6840773225 + 0.6817780137 0.6837441921 0.6853098869 + 0.6831172109 0.6849870086 0.6865425110 + 0.6844564080 0.6862297058 0.6877751946 + 0.6857956052 0.6874724030 0.6890078187 + 0.6871348023 0.6887152195 0.6902288795 + 0.6884739995 0.6899579167 0.6914023161 + 0.6898130774 0.6911445856 0.6925758123 + 0.6910985112 0.6923294067 0.6937493086 + 0.6923751831 0.6935142279 0.6949226856 + 0.6936519146 0.6946989894 0.6960961819 + 0.6949285269 0.6958836913 0.6972696185 + 0.6962051988 0.6970685124 0.6984431148 + 0.6974818110 0.6982532740 0.6996166110 + 0.6987584829 0.6994379759 0.7007505894 + 0.7000334263 0.7005925179 0.7018656135 + 0.7012469769 0.7017195225 0.7029805183 + 0.7024605870 0.7028465867 0.7040954232 + 0.7036740780 0.7039735913 0.7052103281 + 0.7048876882 0.7051007152 0.7063252926 + 0.7061012983 0.7062277198 0.7074401975 + 0.7073147893 0.7073547244 0.7085551023 + 0.7085283995 0.7084817886 0.7096700072 + 0.7097420096 0.7096087933 0.7107443810 + 0.7109059095 0.7106986046 0.7118018270 + 0.7120562792 0.7117685080 0.7128592134 + 0.7132068276 0.7128384709 0.7139164805 + 0.7143573165 0.7139083743 0.7149739265 + 0.7155078053 0.7149783969 0.7160311937 + 0.7166582942 0.7160483003 0.7170885801 + 0.7178087831 0.7171183228 0.7181460261 + 0.7189592123 0.7181882262 0.7192032933 + 0.7201038003 0.7192581296 0.7202467918 + 0.7211917043 0.7203109264 0.7212479115 + 0.7222797275 0.7213246822 0.7222489715 + 0.7233676910 0.7223384976 0.7232500911 + 0.7244557142 0.7233523130 0.7242512107 + 0.7255436182 0.7243661284 0.7252522707 + 0.7266315818 0.7253798842 0.7262532711 + 0.7277196050 0.7263938189 0.7272543907 + 0.7288076282 0.7274075747 0.7282555103 + 0.7298954725 0.7284213901 0.7292565703 + 0.7309280038 0.7294352055 0.7302436233 + 0.7319545150 0.7304247022 0.7311900258 + 0.7329810262 0.7313836813 0.7321363091 + 0.7340075970 0.7323426008 0.7330827117 + 0.7350341082 0.7333015800 0.7340291142 + 0.7360606194 0.7342606187 0.7349753976 + 0.7370871902 0.7352195978 0.7359218001 + 0.7381137013 0.7361785173 0.7368682027 + 0.7391402125 0.7371374965 0.7378144860 + 0.7401570082 0.7380964756 0.7387608886 + 0.7411236167 0.7390553951 0.7397072911 + 0.7420902252 0.7400135994 0.7406170964 + 0.7430567741 0.7409192920 0.7415105104 + 0.7440233827 0.7418249249 0.7424039245 + 0.7449899912 0.7427306175 0.7432973981 + 0.7459564805 0.7436363101 0.7441908121 + 0.7469230890 0.7445420027 0.7450842261 + 0.7478896976 0.7454476953 0.7459775805 + 0.7488563061 0.7463533282 0.7468711138 + 0.7498229146 0.7472590208 0.7477645278 + 0.7507420182 0.7481647134 0.7486578822 + 0.7516505122 0.7490704060 0.7495512962 + 0.7525590062 0.7499759793 0.7504193783 + 0.7534675002 0.7508316040 0.7512617707 + 0.7543759942 0.7516856790 0.7521042824 + 0.7552844882 0.7525398731 0.7529466748 + 0.7561929822 0.7533940077 0.7537891269 + 0.7571014762 0.7542482018 0.7546315789 + 0.7580100298 0.7551022768 0.7554739714 + 0.7589185238 0.7559564710 0.7563164234 + 0.7598270178 0.7568106055 0.7571588755 + 0.7606902122 0.7576647997 0.7580013275 + 0.7615427971 0.7585189939 0.7588437796 + 0.7623953223 0.7593731284 0.7596861720 + 0.7632479072 0.7602140903 0.7604979873 + 0.7641004920 0.7610186934 0.7612915039 + 0.7649530172 0.7618231773 0.7620850205 + 0.7658056021 0.7626277804 0.7628785968 + 0.7666581273 0.7634323835 0.7636721134 + 0.7675107121 0.7642369866 0.7644655704 + 0.7683631778 0.7650415897 0.7652592063 + 0.7692158222 0.7658461928 0.7660527229 + 0.7700641155 0.7666506767 0.7668461800 + 0.7708631158 0.7674552798 0.7676398158 + 0.7716619968 0.7682598829 0.7684332728 + 0.7724609971 0.7690644860 0.7692267895 + 0.7732599974 0.7698690891 0.7700191736 + 0.7740589976 0.7706338763 0.7707660198 + 0.7748579979 0.7713909745 0.7715128064 + 0.7756569982 0.7721480131 0.7722595930 + 0.7764559984 0.7729051113 0.7730063796 + 0.7772548795 0.7736622095 0.7737532258 + 0.7780538797 0.7744193077 0.7745000720 + 0.7788528800 0.7751764059 0.7752469182 + 0.7796518803 0.7759335041 0.7759937048 + 0.7804220915 0.7766906023 0.7767404914 + 0.7811700106 0.7774475813 0.7774872780 + 0.7819179296 0.7782046795 0.7782341242 + 0.7826659083 0.7789617777 0.7789809108 + 0.7834138274 0.7797188759 0.7797278166 + 0.7841616869 0.7804474831 0.7804462910 + 0.7849097252 0.7811592221 0.7811486125 + 0.7856575847 0.7818710208 0.7818508744 + 0.7864055037 0.7825827003 0.7825533152 + 0.7871534824 0.7832944989 0.7832555771 + 0.7879014015 0.7840061784 0.7839578986 + 0.7886493206 0.7847179770 0.7846602201 + 0.7893972993 0.7854297161 0.7853626013 + 0.7901358008 0.7861415148 0.7860649228 + 0.7908353209 0.7868531942 0.7867671847 + 0.7915347815 0.7875649929 0.7874695063 + 0.7922343016 0.7882766724 0.7881718874 + 0.7929338217 0.7889884710 0.7888742089 + 0.7936332226 0.7897002101 0.7895764709 + 0.7943326831 0.7903869748 0.7902621031 + 0.7950322032 0.7910556793 0.7909221053 + 0.7957317233 0.7917243242 0.7915822268 + 0.7964311838 0.7923929095 0.7922422886 + 0.7971307039 0.7930616140 0.7929024100 + 0.7978302240 0.7937301993 0.7935624719 + 0.7985296845 0.7943987846 0.7942225933 + 0.7992292047 0.7950674891 0.7948827147 + 0.7999287248 0.7957360744 0.7955427766 + 0.8005871177 0.7964047194 0.7962027788 + 0.8012408018 0.7970734239 0.7968629003 + 0.8018944860 0.7977420092 0.7975230217 + 0.8025482297 0.7984105945 0.7981830835 + 0.8032019138 0.7990792990 0.7988432050 + 0.8038555980 0.7997478843 0.7995033264 + 0.8045094013 0.8003910780 0.8001534939 + 0.8051630855 0.8010187745 0.8007736206 + 0.8058168292 0.8016465902 0.8013936877 + 0.8064705133 0.8022742867 0.8020138144 + 0.8071241975 0.8029021025 0.8026338816 + 0.8077778816 0.8035297990 0.8032540083 + 0.8084316254 0.8041576147 0.8038740754 + 0.8090854287 0.8047853112 0.8044942021 + 0.8097391129 0.8054131269 0.8051143289 + 0.8103669286 0.8060408235 0.8057343960 + 0.8109775186 0.8066685796 0.8063545227 + 0.8115881085 0.8072962761 0.8069745898 + 0.8121985793 0.8079240918 0.8075947165 + 0.8128092289 0.8085517883 0.8082147837 + 0.8134198189 0.8091796041 0.8088349104 + 0.8140304089 0.8098073006 0.8094549775 + 0.8146409988 0.8104082942 0.8100705743 + 0.8152515888 0.8109974265 0.8106529117 + 0.8158621788 0.8115864992 0.8112351894 + 0.8164728284 0.8121755719 0.8118175864 + 0.8170834184 0.8127647042 0.8123999238 + 0.8176940084 0.8133537173 0.8129822016 + 0.8183045983 0.8139427900 0.8135645986 + 0.8189151883 0.8145319223 0.8141468763 + 0.8195257187 0.8151209950 0.8147292733 + 0.8201273084 0.8157101274 0.8153116107 + 0.8206974268 0.8162992001 0.8158938885 + 0.8212674856 0.8168882728 0.8164762855 + 0.8218376040 0.8174774051 0.8170586228 + 0.8224077225 0.8180664778 0.8176409006 + 0.8229777813 0.8186556101 0.8182232976 + 0.8235478997 0.8192446828 0.8188055754 + 0.8241178989 0.8198336959 0.8193879724 + 0.8246880174 0.8203967214 0.8199703097 + 0.8252580762 0.8209493160 0.8205189109 + 0.8258281946 0.8215019107 0.8210656047 + 0.8263983130 0.8220545053 0.8216122985 + 0.8269683719 0.8226072192 0.8221591115 + 0.8275384903 0.8231598139 0.8227058053 + 0.8281086087 0.8237124085 0.8232526183 + 0.8286787271 0.8242650032 0.8237993121 + 0.8292487860 0.8248177171 0.8243461251 + 0.8298189044 0.8253703117 0.8248928189 + 0.8303630948 0.8259229064 0.8254395127 + 0.8308951855 0.8264755011 0.8259863257 + 0.8314273953 0.8270282149 0.8265330195 + 0.8319594860 0.8275808096 0.8270797729 + 0.8324916959 0.8281334043 0.8276265264 + 0.8330239058 0.8286859989 0.8281732798 + 0.8335559964 0.8292385936 0.8287199736 + 0.8340882063 0.8297913074 0.8292667866 + 0.8346202970 0.8303225040 0.8298134804 + 0.8351525068 0.8308408260 0.8303381801 + 0.8356845975 0.8313590884 0.8308513761 + 0.8362168074 0.8318774104 0.8313646913 + 0.8367488980 0.8323956728 0.8318778872 + 0.8372811079 0.8329139948 0.8323912024 + 0.8378131986 0.8334323168 0.8329045177 + 0.8383454084 0.8339505792 0.8334177136 + 0.8388774991 0.8344689012 0.8339310288 + 0.8394097090 0.8349872231 0.8344442248 + 0.8399417996 0.8355054855 0.8349574804 + 0.8404424191 0.8360238075 0.8354706764 + 0.8409391046 0.8365421295 0.8359839916 + 0.8414357901 0.8370603919 0.8364971876 + 0.8419324756 0.8375787139 0.8370105028 + 0.8424291015 0.8380969763 0.8375238180 + 0.8429257870 0.8386152983 0.8380370140 + 0.8434224725 0.8391336203 0.8385503292 + 0.8439192176 0.8396518826 0.8390635252 + 0.8444159031 0.8401595950 0.8395767808 + 0.8449125886 0.8406456113 0.8400844932 + 0.8454092741 0.8411316872 0.8405662775 + 0.8459059000 0.8416177034 0.8410481215 + 0.8464025855 0.8421037793 0.8415299058 + 0.8468992710 0.8425897956 0.8420116901 + 0.8473960161 0.8430758119 0.8424934745 + 0.8478927016 0.8435618877 0.8429753184 + 0.8483893871 0.8440479040 0.8434571028 + 0.8488860130 0.8445339799 0.8439388871 + 0.8493826985 0.8450199962 0.8444206715 + 0.8498793840 0.8455060720 0.8449025154 + 0.8503509760 0.8459920883 0.8453842998 + 0.8508145809 0.8464781046 0.8458660841 + 0.8512781858 0.8469641805 0.8463479280 + 0.8517417908 0.8474501967 0.8468297124 + 0.8522053957 0.8479362726 0.8473114967 + 0.8526690006 0.8484222889 0.8477932811 + 0.8531326056 0.8489084244 0.8482751250 + 0.8535962105 0.8493943810 0.8487569094 + 0.8540596962 0.8498803973 0.8492386937 + 0.8545233011 0.8503437042 0.8497204781 + 0.8549869061 0.8507995009 0.8501899242 + 0.8554505110 0.8512551785 0.8506422043 + 0.8559141159 0.8517109752 0.8510944843 + 0.8563777208 0.8521667719 0.8515468240 + 0.8568413258 0.8526226282 0.8519991040 + 0.8573048711 0.8530784249 0.8524513841 + 0.8577684760 0.8535342216 0.8529037237 + 0.8582320213 0.8539898992 0.8533560038 + 0.8586956263 0.8544456959 0.8538082838 + 0.8591591716 0.8549014926 0.8542606235 + 0.8596227765 0.8553572893 0.8547129035 + 0.8600807190 0.8558130860 0.8551651835 + 0.8605133891 0.8562688828 0.8556174040 + 0.8609461784 0.8567246795 0.8560696840 + 0.8613789082 0.8571804166 0.8565220237 + 0.8618116975 0.8576362133 0.8569743037 + 0.8622444272 0.8580920100 0.8574265838 + 0.8626772165 0.8585478067 0.8578789234 + 0.8631098866 0.8590036035 0.8583312035 + 0.8635426760 0.8594594002 0.8587834835 + 0.8639754057 0.8599150777 0.8592358232 + 0.8644081950 0.8603479266 0.8596881032 + 0.8648409247 0.8607752919 0.8601318002 + 0.8652737141 0.8612027168 0.8605564237 + 0.8657063842 0.8616302013 0.8609811068 + 0.8661391735 0.8620576262 0.8614056706 + 0.8665719032 0.8624849916 0.8618304133 + 0.8670045733 0.8629124761 0.8622549772 + 0.8674374223 0.8633399010 0.8626796007 + 0.8678700924 0.8637673855 0.8631042838 + 0.8683028817 0.8641948104 0.8635289073 + 0.8687356114 0.8646221757 0.8639535904 + 0.8691684008 0.8650497198 0.8643782139 + 0.8696010709 0.8654770851 0.8648027778 + 0.8700317144 0.8659045100 0.8652275205 + 0.8704357147 0.8663319945 0.8656520844 + 0.8708397746 0.8667594194 0.8660768270 + 0.8712437749 0.8671867847 0.8665013909 + 0.8716478944 0.8676143289 0.8669260740 + 0.8720518947 0.8680416942 0.8673506975 + 0.8724560142 0.8684691191 0.8677753210 + 0.8728600144 0.8688966036 0.8682000041 + 0.8732640743 0.8693240285 0.8686246276 + 0.8736680746 0.8697515130 0.8690493107 + 0.8740721941 0.8701677918 0.8694738746 + 0.8744761944 0.8705686927 0.8698986173 + 0.8748803139 0.8709695935 0.8703035116 + 0.8752843142 0.8713704944 0.8707023263 + 0.8756883740 0.8717713952 0.8711010218 + 0.8760923743 0.8721722960 0.8714997768 + 0.8764964938 0.8725731969 0.8718984723 + 0.8769004941 0.8729740977 0.8722972870 + 0.8773046136 0.8733749986 0.8726959825 + 0.8777086139 0.8737758994 0.8730947971 + 0.8781126738 0.8741769195 0.8734936118 + 0.8785166740 0.8745778203 0.8738923073 + 0.8789207935 0.8749787211 0.8742911220 + 0.8793247938 0.8753796220 0.8746898174 + 0.8797289133 0.8757805228 0.8750885725 + 0.8801240921 0.8761814237 0.8754873276 + 0.8805015087 0.8765823245 0.8758860826 + 0.8808789253 0.8769832253 0.8762847781 + 0.8812562227 0.8773841262 0.8766835928 + 0.8816335797 0.8777850270 0.8770824075 + 0.8820109963 0.8781859279 0.8774811029 + 0.8823882937 0.8785868287 0.8778799176 + 0.8827657104 0.8789877295 0.8782786131 + 0.8831431270 0.8793885708 0.8786774278 + 0.8835204244 0.8797894716 0.8790761232 + 0.8838977814 0.8801786900 0.8794748783 + 0.8842751980 0.8805547953 0.8798736930 + 0.8846524954 0.8809309006 0.8802558780 + 0.8850299120 0.8813070059 0.8806303740 + 0.8854073286 0.8816831112 0.8810049891 + 0.8857846260 0.8820592165 0.8813794851 + 0.8861619830 0.8824353218 0.8817539811 + 0.8865392804 0.8828114271 0.8821285963 + 0.8869166970 0.8831874728 0.8825030923 + 0.8872941136 0.8835635781 0.8828777075 + 0.8876714110 0.8839396834 0.8832522035 + 0.8880488276 0.8843157887 0.8836268187 + 0.8884261847 0.8846918941 0.8840013146 + 0.8888034821 0.8850679994 0.8843758106 + 0.8891808987 0.8854441047 0.8847504258 + 0.8895583153 0.8858202100 0.8851249218 + 0.8899356127 0.8861963153 0.8854994774 + 0.8902924061 0.8865724206 0.8858739734 + 0.8906450272 0.8869485259 0.8862485886 + 0.8909975886 0.8873245716 0.8866230845 + 0.8913502097 0.8877006769 0.8869975805 + 0.8917027116 0.8880767822 0.8873721957 + 0.8920552731 0.8884528875 0.8877466917 + 0.8924078941 0.8888289928 0.8881213069 + 0.8927605152 0.8892050982 0.8884958029 + 0.8931130171 0.8895812035 0.8888702989 + 0.8934655786 0.8899573088 0.8892449141 + 0.8938181996 0.8903127909 0.8896194100 + 0.8941708207 0.8906658292 0.8899940252 + 0.8945233226 0.8910186887 0.8903462291 + 0.8948758841 0.8913716078 0.8906980753 + 0.8952285051 0.8917245269 0.8910499811 + 0.8955811262 0.8920775056 0.8914020061 + 0.8959336281 0.8924304247 0.8917539120 + 0.8962861896 0.8927832842 0.8921058178 + 0.8966388106 0.8931362033 0.8924577236 + 0.8969913721 0.8934891820 0.8928096294 + 0.8973438740 0.8938421011 0.8931614757 + 0.8976964951 0.8941950202 0.8935133815 + 0.8980491161 0.8945478797 0.8938652873 + 0.8984016776 0.8949009180 0.8942171931 + 0.8987541795 0.8952537775 0.8945690989 + 0.8991068006 0.8956066966 0.8949210048 + 0.8994594216 0.8959596157 0.8952729106 + 0.8998119831 0.8963125944 0.8956248164 + 0.9001538157 0.8966655135 0.8959767222 + 0.9004834294 0.8970183730 0.8963286281 + 0.9008129239 0.8973712921 0.8966804743 + 0.9011424780 0.8977242708 0.8970323801 + 0.9014719725 0.8980771899 0.8973842859 + 0.9018015862 0.8984301090 0.8977361917 + 0.9021310806 0.8987830281 0.8980880976 + 0.9024606943 0.8991360068 0.8984400034 + 0.9027901888 0.8994889259 0.8987919092 + 0.9031198025 0.8998417854 0.8991438150 + 0.9034494162 0.9001827836 0.8994957209 + 0.9037789106 0.9005141258 0.8998476267 + 0.9041085243 0.9008454084 0.9001874924 + 0.9044380188 0.9011766911 0.9005182981 + 0.9047675729 0.9015079141 0.9008489847 + 0.9050971270 0.9018391967 0.9011797905 + 0.9054266810 0.9021704793 0.9015104771 + 0.9057561755 0.9025018215 0.9018412828 + 0.9060857892 0.9028331041 0.9021720290 + 0.9064154029 0.9031643867 0.9025027752 + 0.9067448974 0.9034956098 0.9028335214 + 0.9070745111 0.9038268924 0.9031643271 + 0.9074040055 0.9041581750 0.9034950137 + 0.9077336192 0.9044895172 0.9038258195 + 0.9080631137 0.9048207998 0.9041565061 + 0.9083927274 0.9051520824 0.9044873118 + 0.9087222219 0.9054833055 0.9048179984 + 0.9090517759 0.9058145881 0.9051488042 + 0.9093812704 0.9061458707 0.9054794908 + 0.9097108841 0.9064772129 0.9058102965 + 0.9100378156 0.9068084955 0.9061409831 + 0.9103459716 0.9071397781 0.9064717889 + 0.9106541872 0.9074711204 0.9068024755 + 0.9109624028 0.9078022838 0.9071332812 + 0.9112706184 0.9081336260 0.9074640274 + 0.9115787745 0.9084649086 0.9077947140 + 0.9118869901 0.9087961912 0.9081255198 + 0.9121950865 0.9091274738 0.9084562063 + 0.9125033021 0.9094588161 0.9087870121 + 0.9128115177 0.9097899795 0.9091176987 + 0.9131196737 0.9101138711 0.9094485044 + 0.9134278893 0.9104250073 0.9097791910 + 0.9137361050 0.9107360840 0.9101033807 + 0.9140443206 0.9110472202 0.9104143977 + 0.9143524766 0.9113582969 0.9107254148 + 0.9146605730 0.9116693735 0.9110363722 + 0.9149687886 0.9119803905 0.9113473892 + 0.9152770042 0.9122915268 0.9116582870 + 0.9155852199 0.9126026034 0.9119693041 + 0.9158933759 0.9129136801 0.9122803211 + 0.9162015915 0.9132248163 0.9125912786 + 0.9165098071 0.9135358930 0.9129022956 + 0.9168180227 0.9138470292 0.9132133126 + 0.9171261191 0.9141579866 0.9135242105 + 0.9174342752 0.9144691229 0.9138352275 + 0.9177424908 0.9147801995 0.9141461849 + 0.9180507064 0.9150912762 0.9144572020 + 0.9183589220 0.9154024124 0.9147682190 + 0.9186670780 0.9157134891 0.9150791764 + 0.9189752936 0.9160246253 0.9153901935 + 0.9192835093 0.9163355827 0.9157010913 + 0.9195916057 0.9166467190 0.9160121083 + 0.9198998213 0.9169577956 0.9163231254 + 0.9201946259 0.9172688723 0.9166340828 + 0.9204829931 0.9175800085 0.9169450998 + 0.9207713008 0.9178910851 0.9172561169 + 0.9210597277 0.9182022214 0.9175670147 + 0.9213480949 0.9185131788 0.9178779721 + 0.9216364026 0.9188243151 0.9181889892 + 0.9219248295 0.9191353917 0.9185000062 + 0.9222130775 0.9194465280 0.9188110232 + 0.9225015044 0.9197576046 0.9191219807 + 0.9227898717 0.9200645089 0.9194329977 + 0.9230781794 0.9203566909 0.9197438955 + 0.9233666062 0.9206489921 0.9200516939 + 0.9236549139 0.9209411740 0.9203441739 + 0.9239432812 0.9212334752 0.9206367135 + 0.9242315888 0.9215257168 0.9209291935 + 0.9245200157 0.9218178988 0.9212217927 + 0.9248083830 0.9221101999 0.9215142727 + 0.9250966907 0.9224023819 0.9218068123 + 0.9253851175 0.9226946235 0.9220994115 + 0.9256734252 0.9229869246 0.9223918915 + 0.9259617925 0.9232791066 0.9226843715 + 0.9262501001 0.9235714078 0.9229769111 + 0.9265385270 0.9238635898 0.9232695103 + 0.9268268943 0.9241557717 0.9235619903 + 0.9271152020 0.9244480729 0.9238544703 + 0.9274036288 0.9247403145 0.9241470098 + 0.9276918769 0.9250324965 0.9244396091 + 0.9279803038 0.9253247976 0.9247320890 + 0.9282686710 0.9256169796 0.9250246286 + 0.9285569787 0.9259092808 0.9253171086 + 0.9288454056 0.9262015224 0.9256097078 + 0.9291337132 0.9264937043 0.9259021878 + 0.9294220805 0.9267860055 0.9261947274 + 0.9297103882 0.9270781875 0.9264872074 + 0.9299988151 0.9273704886 0.9267798066 + 0.9302688241 0.9276626706 0.9270722866 + 0.9305387735 0.9279549122 0.9273648262 + 0.9308087826 0.9282472134 0.9276573062 + 0.9310786724 0.9285393953 0.9279499054 + 0.9313486814 0.9288315773 0.9282423854 + 0.9316185713 0.9291238785 0.9285349250 + 0.9318885803 0.9294161201 0.9288275242 + 0.9321585298 0.9297084212 0.9291200042 + 0.9324284792 0.9300006032 0.9294124842 + 0.9326984882 0.9302752018 0.9297050238 + 0.9329683781 0.9305499196 0.9299976230 + 0.9332383871 0.9308245182 0.9302729964 + 0.9335082769 0.9310991764 0.9305483103 + 0.9337782860 0.9313737750 0.9308236241 + 0.9340481758 0.9316484928 0.9310988784 + 0.9343181849 0.9319230914 0.9313740730 + 0.9345881939 0.9321978092 0.9316493869 + 0.9348580837 0.9324725270 0.9319247007 + 0.9351280928 0.9327471256 0.9322000146 + 0.9353979826 0.9330217838 0.9324753284 + 0.9356679916 0.9332963824 0.9327505827 + 0.9359378815 0.9335711002 0.9330258965 + 0.9362078905 0.9338456988 0.9333012104 + 0.9364778996 0.9341204166 0.9335764050 + 0.9367477894 0.9343950152 0.9338517189 + 0.9370177984 0.9346696734 0.9341269732 + 0.9372876883 0.9349442720 0.9344022870 + 0.9375576973 0.9352189898 0.9346776009 + 0.9378277063 0.9354937077 0.9349529147 + 0.9380975962 0.9357683063 0.9352282286 + 0.9383676052 0.9360430241 0.9355034828 + 0.9386374950 0.9363176227 0.9357786775 + 0.9389075041 0.9365922809 0.9360539913 + 0.9391773939 0.9368668795 0.9363293052 + 0.9394474030 0.9371415973 0.9366046190 + 0.9397174120 0.9374161959 0.9368798733 + 0.9399873018 0.9376909137 0.9371551871 + 0.9402409792 0.9379655719 0.9374305010 + 0.9404938817 0.9382401705 0.9377058148 + 0.9407467842 0.9385148883 0.9379811287 + 0.9409996271 0.9387894869 0.9382563233 + 0.9412525296 0.9390642047 0.9385315776 + 0.9415053725 0.9393388033 0.9388068914 + 0.9417582750 0.9396135211 0.9390822053 + 0.9420111775 0.9398881197 0.9393575191 + 0.9422640800 0.9401531219 0.9396327734 + 0.9425169230 0.9404113293 0.9399080873 + 0.9427698255 0.9406695962 0.9401726127 + 0.9430227280 0.9409278035 0.9404317737 + 0.9432755709 0.9411860704 0.9406909943 + 0.9435284734 0.9414442778 0.9409502149 + 0.9437813759 0.9417026043 0.9412093759 + 0.9440342188 0.9419608116 0.9414685965 + 0.9442871213 0.9422190785 0.9417278171 + 0.9445400238 0.9424772859 0.9419869781 + 0.9447929263 0.9427356124 0.9422461987 + 0.9450458288 0.9429938197 0.9425053000 + 0.9452986717 0.9432520866 0.9427645206 + 0.9455515146 0.9435104132 0.9430236816 + 0.9458044171 0.9437686205 0.9432829022 + 0.9460573196 0.9440268874 0.9435421228 + 0.9463102221 0.9442850947 0.9438012838 + 0.9465631247 0.9445434213 0.9440605044 + 0.9468160272 0.9448016286 0.9443197250 + 0.9470688105 0.9450598955 0.9445788860 + 0.9473217130 0.9453181028 0.9448379874 + 0.9475746155 0.9455764294 0.9450972080 + 0.9478275180 0.9458345771 0.9453564286 + 0.9480804205 0.9460929036 0.9456155896 + 0.9483332038 0.9463511109 0.9458748102 + 0.9485861063 0.9466093779 0.9461339712 + 0.9488390088 0.9468675852 0.9463931918 + 0.9490919113 0.9471259117 0.9466524124 + 0.9493448138 0.9473841190 0.9469115734 + 0.9495977163 0.9476423860 0.9471707940 + 0.9498504996 0.9479007125 0.9474298954 + 0.9500969052 0.9481589198 0.9476891160 + 0.9503340125 0.9484171867 0.9479482770 + 0.9505710006 0.9486753941 0.9482074976 + 0.9508081079 0.9489337206 0.9484667182 + 0.9510450959 0.9491919279 0.9487258792 + 0.9512820840 0.9494501948 0.9489850998 + 0.9515191913 0.9497084022 0.9492443204 + 0.9517561793 0.9499667287 0.9495034814 + 0.9519932270 0.9502115846 0.9497625828 + 0.9522302747 0.9504545927 0.9500206113 + 0.9524673223 0.9506974816 0.9502646923 + 0.9527043104 0.9509404898 0.9505088925 + 0.9529414177 0.9511833787 0.9507529736 + 0.9531784058 0.9514263868 0.9509971738 + 0.9534155130 0.9516692758 0.9512413144 + 0.9536525011 0.9519122839 0.9514855146 + 0.9538894892 0.9521551728 0.9517297149 + 0.9541265965 0.9523981810 0.9519737959 + 0.9543635845 0.9526411295 0.9522179961 + 0.9546005726 0.9528840780 0.9524620771 + 0.9548376799 0.9531270266 0.9527062774 + 0.9550747275 0.9533699751 0.9529504180 + 0.9553117156 0.9536129236 0.9531946182 + 0.9555488229 0.9538558722 0.9534386992 + 0.9557858109 0.9540988207 0.9536828995 + 0.9560227990 0.9543418288 0.9539269805 + 0.9562599063 0.9545847178 0.9541711807 + 0.9564968944 0.9548277259 0.9544153810 + 0.9567340016 0.9550706148 0.9546595216 + 0.9569709897 0.9553136230 0.9549037218 + 0.9572079778 0.9555565119 0.9551478028 + 0.9574450850 0.9557995200 0.9553920031 + 0.9576820731 0.9560424089 0.9556360841 + 0.9579191208 0.9562854171 0.9558802843 + 0.9581562281 0.9565284252 0.9561244249 + 0.9583932161 0.9567713141 0.9563686252 + 0.9586302042 0.9570143223 0.9566128254 + 0.9588673115 0.9572572112 0.9568569064 + 0.9591042995 0.9575002193 0.9571011066 + 0.9593414068 0.9577431083 0.9573451877 + 0.9595783949 0.9579861164 0.9575893879 + 0.9598153830 0.9582290053 0.9578335285 + 0.9600492120 0.9584720135 0.9580777287 + 0.9602714777 0.9587149024 0.9583218098 + 0.9604939222 0.9589579105 0.9585660100 + 0.9607161880 0.9592007995 0.9588100910 + 0.9609385133 0.9594438076 0.9590542912 + 0.9611607790 0.9596866965 0.9592984915 + 0.9613832235 0.9599297047 0.9595425725 + 0.9616054893 0.9601625204 0.9597867727 + 0.9618278146 0.9603912234 0.9600291252 + 0.9620500803 0.9606198072 0.9602593184 + 0.9622725248 0.9608485103 0.9604893923 + 0.9624947906 0.9610772133 0.9607195258 + 0.9627171159 0.9613059163 0.9609495997 + 0.9629393816 0.9615346193 0.9611796737 + 0.9631618261 0.9617632031 0.9614098072 + 0.9633840919 0.9619919062 0.9616398811 + 0.9636064172 0.9622206092 0.9618700147 + 0.9638286829 0.9624493122 0.9621000886 + 0.9640511274 0.9626778960 0.9623302817 + 0.9642733932 0.9629065990 0.9625604153 + 0.9644957185 0.9631353021 0.9627904892 + 0.9647179842 0.9633640051 0.9630206227 + 0.9649404287 0.9635927081 0.9632506967 + 0.9651626945 0.9638212919 0.9634807706 + 0.9653850198 0.9640499949 0.9637109041 + 0.9656072855 0.9642786980 0.9639409781 + 0.9658296704 0.9645074010 0.9641711712 + 0.9660519958 0.9647359848 0.9644013047 + 0.9662743211 0.9649646878 0.9646313787 + 0.9664965868 0.9651933908 0.9648615122 + 0.9667189717 0.9654220939 0.9650915861 + 0.9669412971 0.9656506777 0.9653217196 + 0.9671636224 0.9658793807 0.9655517936 + 0.9673860073 0.9661080837 0.9657819271 + 0.9676082730 0.9663367867 0.9660120010 + 0.9678305984 0.9665654898 0.9662421942 + 0.9680529237 0.9667940736 0.9664723277 + 0.9682753086 0.9670227766 0.9667024016 + 0.9684975743 0.9672514796 0.9669324756 + 0.9687198997 0.9674801826 0.9671626091 + 0.9689422250 0.9677088261 0.9673926830 + 0.9691646099 0.9679375291 0.9676228166 + 0.9693868756 0.9681661725 0.9678528905 + 0.9696092010 0.9683948755 0.9680830836 + 0.9698315263 0.9686235785 0.9683132172 + 0.9700505733 0.9688522220 0.9685432911 + 0.9702591896 0.9690809250 0.9687734246 + 0.9704679251 0.9693096280 0.9690034986 + 0.9706764817 0.9695382714 0.9692335725 + 0.9708852172 0.9697669148 0.9694637060 + 0.9710938931 0.9699956179 0.9696937799 + 0.9713025093 0.9702112079 0.9699239135 + 0.9715111852 0.9704266191 0.9701452851 + 0.9717199206 0.9706419110 0.9703623056 + 0.9719284773 0.9708573222 0.9705793262 + 0.9721372128 0.9710726738 0.9707962871 + 0.9723458886 0.9712880254 0.9710131884 + 0.9725545049 0.9715033770 0.9712302089 + 0.9727631807 0.9717187285 0.9714472294 + 0.9729719162 0.9719340801 0.9716641903 + 0.9731804729 0.9721494913 0.9718812108 + 0.9733892083 0.9723647833 0.9720981717 + 0.9735978842 0.9725801945 0.9723151922 + 0.9738065004 0.9727954865 0.9725322127 + 0.9740151763 0.9730108976 0.9727491736 + 0.9742239118 0.9732261896 0.9729661942 + 0.9744325280 0.9734416008 0.9731832147 + 0.9746412039 0.9736570120 0.9734001756 + 0.9748498797 0.9738723040 0.9736170769 + 0.9750584960 0.9740877151 0.9738340974 + 0.9752671719 0.9743030071 0.9740511179 + 0.9754759073 0.9745184183 0.9742680788 + 0.9756845236 0.9747338295 0.9744850993 + 0.9758931994 0.9749491215 0.9747021198 + 0.9761018753 0.9751644731 0.9749190807 + 0.9763104916 0.9753798246 0.9751361012 + 0.9765192270 0.9755951762 0.9753531218 + 0.9767279029 0.9758105278 0.9755700827 + 0.9769365191 0.9760258794 0.9757871032 + 0.9771451950 0.9762412906 0.9760041237 + 0.9773538709 0.9764565825 0.9762210846 + 0.9775624871 0.9766719937 0.9764379859 + 0.9777712226 0.9768872857 0.9766550064 + 0.9779798985 0.9771026969 0.9768720269 + 0.9781885147 0.9773179889 0.9770889878 + 0.9783971906 0.9775334001 0.9773060083 + 0.9786059260 0.9777488112 0.9775230289 + 0.9788144827 0.9779641032 0.9777399898 + 0.9790232182 0.9781795144 0.9779570103 + 0.9792318940 0.9783948064 0.9781739712 + 0.9794405103 0.9786102176 0.9783909917 + 0.9796491861 0.9788256288 0.9786080122 + 0.9798579216 0.9790409207 0.9788249731 + 0.9800624847 0.9792562723 0.9790418744 + 0.9802585244 0.9794716239 0.9792588949 + 0.9804543853 0.9796869755 0.9794759154 + 0.9806504250 0.9799023271 0.9796928763 + 0.9808464050 0.9801108837 0.9799098969 + 0.9810423851 0.9803137779 0.9801197052 + 0.9812384248 0.9805167913 0.9803245068 + 0.9814342856 0.9807196856 0.9805291891 + 0.9816303253 0.9809225798 0.9807338715 + 0.9818263054 0.9811254740 0.9809386730 + 0.9820222855 0.9813284874 0.9811434150 + 0.9822182059 0.9815313816 0.9813480973 + 0.9824141860 0.9817342758 0.9815528989 + 0.9826102257 0.9819372892 0.9817575812 + 0.9828062057 0.9821401834 0.9819623232 + 0.9830021858 0.9823430777 0.9821671247 + 0.9831981063 0.9825459719 0.9823718071 + 0.9833940864 0.9827489853 0.9825764894 + 0.9835901260 0.9829518795 0.9827812910 + 0.9837861061 0.9831547737 0.9829859734 + 0.9839820266 0.9833577275 0.9831907153 + 0.9841780066 0.9835606813 0.9833955169 + 0.9843739867 0.9837635756 0.9836001992 + 0.9845700264 0.9839665294 0.9838048816 + 0.9847660065 0.9841694832 0.9840096235 + 0.9849619269 0.9843723774 0.9842144251 + 0.9851579070 0.9845752716 0.9844191074 + 0.9853538871 0.9847782254 0.9846237898 + 0.9855499268 0.9849811792 0.9848285913 + 0.9857457876 0.9851840734 0.9850332737 + 0.9859418273 0.9853870273 0.9852380157 + 0.9861378074 0.9855899215 0.9854428172 + 0.9863337874 0.9857928753 0.9856474996 + 0.9865298271 0.9859958291 0.9858521819 + 0.9867256880 0.9861987233 0.9860569835 + 0.9869217277 0.9864016771 0.9862617254 + 0.9871177077 0.9866045713 0.9864664078 + 0.9873136878 0.9868075252 0.9866712093 + 0.9875096083 0.9870104194 0.9868758917 + 0.9877055883 0.9872133732 0.9870805740 + 0.9879016280 0.9874163270 0.9872853756 + 0.9880976081 0.9876192212 0.9874901175 + 0.9882935882 0.9878221154 0.9876947999 + 0.9884895086 0.9880251288 0.9878996015 + 0.9886854887 0.9882280231 0.9881042838 + 0.9888815284 0.9884309173 0.9883090258 + 0.9890775084 0.9886338711 0.9885138273 + 0.9892734289 0.9888368249 0.9887185097 + 0.9894694090 0.9890397191 0.9889231920 + 0.9896653891 0.9892426133 0.9891278744 + 0.9898614287 0.9894456267 0.9893326759 + 0.9900538921 0.9896485209 0.9895374179 + 0.9902380705 0.9898514152 0.9897421002 + 0.9904223084 0.9900512099 0.9899469018 + 0.9906064868 0.9902426004 0.9901431203 + 0.9907907248 0.9904338717 0.9903364182 + 0.9909747839 0.9906252027 0.9905297160 + 0.9911590219 0.9908164740 0.9907228947 + 0.9913432002 0.9910078049 0.9909161925 + 0.9915273786 0.9911991954 0.9911094904 + 0.9917116165 0.9913905263 0.9913027883 + 0.9918957949 0.9915817976 0.9914960265 + 0.9920799732 0.9917731285 0.9916893244 + 0.9922642112 0.9919645190 0.9918826222 + 0.9924482703 0.9921557903 0.9920758009 + 0.9926325083 0.9923471212 0.9922690988 + 0.9928166866 0.9925383925 0.9924623966 + 0.9930009246 0.9927297235 0.9926556945 + 0.9931851029 0.9929211140 0.9928488731 + 0.9933692813 0.9931123853 0.9930421710 + 0.9935535192 0.9933037162 0.9932355285 + 0.9937375784 0.9934949875 0.9934287071 + 0.9939218163 0.9936863780 0.9936220050 + 0.9941059947 0.9938777089 0.9938153028 + 0.9942901731 0.9940689802 0.9940086007 + 0.9944744110 0.9942603111 0.9942017794 + 0.9946585894 0.9944517016 0.9943950772 + 0.9948428273 0.9946429729 0.9945883751 + 0.9950270057 0.9948343039 0.9947816730 + 0.9952111244 0.9950255752 0.9949749112 + 0.9953953028 0.9952169061 0.9951682091 + 0.9955794811 0.9954082966 0.9953615069 + 0.9957637191 0.9955996275 0.9955546856 + 0.9959478974 0.9957908988 0.9957479835 + 0.9961320758 0.9959822297 0.9959412813 + 0.9963163137 0.9961736202 0.9961345792 + 0.9965003729 0.9963648915 0.9963278174 + 0.9966846108 0.9965562224 0.9965211153 + 0.9968687892 0.9967474937 0.9967144132 + 0.9970530272 0.9969388247 0.9969075918 + 0.9972372055 0.9971302152 0.9971008897 + 0.9974213839 0.9973214865 0.9972941875 + 0.9976056218 0.9975128174 0.9974874854 + 0.9977898002 0.9977040887 0.9976807237 + 0.9979739189 0.9978954792 0.9978740215 + 0.9981580973 0.9980868101 0.9980673194 + 0.9983422756 0.9982780814 0.9982606173 + 0.9985265136 0.9984694123 0.9984537959 + 0.9987106919 0.9986606836 0.9986470938 + 0.9988948703 0.9988520741 0.9988403916 + 0.9990791082 0.9990434051 0.9990335703 + 0.9992632866 0.9992346764 0.9992269278 + 0.9994474053 0.9994260073 0.9994202256 + 0.9996315837 0.9996173978 0.9996135235 + 0.9998158216 0.9998087287 0.9998067021 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3153.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3153.spi1d index 589da4b3..468fb23e 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3153.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3153.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0033556900 0.0036243461 0.0021771761 - 0.0067113810 0.0072486931 0.0043543512 - 0.0100719603 0.0109450798 0.0065315268 - 0.0136722000 0.0148685100 0.0087087033 - 0.0172724500 0.0187919401 0.0109967999 - 0.0209288709 0.0229167007 0.0134465704 - 0.0247608796 0.0271310192 0.0158963501 - 0.0285928901 0.0314321816 0.0183461197 - 0.0325581394 0.0359185003 0.0208943095 - 0.0366007015 0.0404266603 0.0236469992 - 0.0406721011 0.0451549515 0.0263996907 - 0.0448958799 0.0498832315 0.0291523803 - 0.0491196699 0.0548080616 0.0321348682 - 0.0534582287 0.0597378612 0.0352196097 - 0.0578269996 0.0648122802 0.0383043587 - 0.0622479804 0.0698948205 0.0415499993 - 0.0667206198 0.0750741214 0.0449920595 - 0.0712093785 0.0802576020 0.0484341197 - 0.0757424608 0.0854823887 0.0520806797 - 0.0802765936 0.0907058865 0.0558979213 - 0.0848271400 0.0959210992 0.0597151592 - 0.0893776789 0.1011238992 0.0638853684 - 0.0939087570 0.1062820032 0.0680840537 - 0.0984367505 0.1114128977 0.0724848807 - 0.1029269993 0.1164738983 0.0770553872 - 0.1073973030 0.1214959025 0.0817479491 - 0.1118313000 0.1264283061 0.0866615325 - 0.1162149012 0.1313190013 0.0916693583 - 0.1205834970 0.1361002028 0.0968769416 - 0.1248577982 0.1408507973 0.1021751985 - 0.1291321069 0.1454659998 0.1076091975 - 0.1333063990 0.1500781029 0.1131248996 - 0.1374552995 0.1545193046 0.1187048033 - 0.1415515989 0.1589604020 0.1243304983 - 0.1455647051 0.1632665992 0.1299701035 - 0.1495777071 0.1675314009 0.1355862021 - 0.1534643024 0.1717227995 0.1411814988 - 0.1573361009 0.1758131981 0.1467006058 - 0.1611633003 0.1799037009 0.1521570981 - 0.1648924053 0.1838286966 0.1575206071 - 0.1686214954 0.1877496988 0.1627777070 - 0.1722615957 0.1916016042 0.1679431945 - 0.1758497059 0.1953604072 0.1729734987 - 0.1794376969 0.1991191953 0.1779143065 - 0.1829102039 0.2027603984 0.1827175021 - 0.1863611937 0.2063656002 0.1874202043 - 0.1898120940 0.2099706978 0.1920142025 - 0.1931387931 0.2134328038 0.1964761019 - 0.1964582950 0.2168937027 0.2008883059 - 0.1997777969 0.2203408033 0.2051143050 - 0.2029809058 0.2236669958 0.2093403041 - 0.2061755955 0.2269932926 0.2133757025 - 0.2093703002 0.2303075939 0.2173759937 - 0.2124706954 0.2335088998 0.2213031054 - 0.2155478001 0.2367102057 0.2250909060 - 0.2186249942 0.2399114966 0.2288787067 - 0.2216414064 0.2430005968 0.2325274944 - 0.2246086001 0.2460864037 0.2361178994 - 0.2275757939 0.2491723001 0.2397083044 - 0.2305241972 0.2521803081 0.2431315929 - 0.2333890945 0.2551597953 0.2465402037 - 0.2362540066 0.2581393123 0.2499487996 - 0.2391189039 0.2610821128 0.2531935871 - 0.2419181019 0.2639639974 0.2564358115 - 0.2446880937 0.2668457925 0.2596780956 - 0.2474582046 0.2697275877 0.2627840936 - 0.2502210140 0.2725282907 0.2658750117 - 0.2529034913 0.2753205895 0.2689658999 - 0.2555859983 0.2781128883 0.2719655037 - 0.2582685053 0.2808786035 0.2749192119 - 0.2609224021 0.2835890055 0.2778730094 - 0.2635244131 0.2862994075 0.2807919979 - 0.2661263049 0.2890098095 0.2836216092 - 0.2687281966 0.2916727960 0.2864513099 - 0.2712922990 0.2943083942 0.2892810106 - 0.2738203108 0.2969441116 0.2920272052 - 0.2763482928 0.2995797098 0.2947449982 - 0.2788763046 0.3021580875 0.2974627912 - 0.2813667059 0.3047255874 0.3001739085 - 0.2838270068 0.3072930872 0.3027910888 - 0.2862873077 0.3098605871 0.3054082096 - 0.2887476087 0.3123694956 0.3080253005 - 0.2911775112 0.3148748875 0.3106203079 - 0.2935760021 0.3173803985 0.3131470084 - 0.2959744036 0.3198859096 0.3156735897 - 0.2983728945 0.3223375976 0.3182002902 - 0.3007532060 0.3247866929 0.3207036853 - 0.3030953109 0.3272357881 0.3231492937 - 0.3054375052 0.3296849132 0.3255949020 - 0.3077796102 0.3320893943 0.3280405104 - 0.3101190925 0.3344874084 0.3304716051 - 0.3124099970 0.3368853033 0.3328445852 - 0.3147009015 0.3392831981 0.3352177143 - 0.3169918060 0.3416486979 0.3375906944 - 0.3192827106 0.3440001905 0.3399637043 - 0.3215416968 0.3463517129 0.3422729075 - 0.3237861097 0.3487032950 0.3445810974 - 0.3260306120 0.3510360122 0.3468891978 - 0.3282749951 0.3533455133 0.3491973877 - 0.3305096924 0.3556550145 0.3514679968 - 0.3327121139 0.3579646051 0.3537184000 - 0.3349145055 0.3602696061 0.3559688032 - 0.3371168971 0.3625411093 0.3582192957 - 0.3393192887 0.3648126125 0.3604589999 - 0.3414955139 0.3670840859 0.3626582026 - 0.3436599970 0.3693554997 0.3648572862 - 0.3458243906 0.3716023862 0.3670564890 - 0.3479889035 0.3738394976 0.3692556024 - 0.3501509130 0.3760766089 0.3714247048 - 0.3522812128 0.3783136904 0.3735783994 - 0.3544115126 0.3805431128 0.3757320940 - 0.3565418124 0.3827491105 0.3778859079 - 0.3586719930 0.3849549890 0.3800387979 - 0.3607907891 0.3871609867 0.3821524978 - 0.3628903031 0.3893668950 0.3842661977 - 0.3649899065 0.3915528059 0.3863798082 - 0.3670895100 0.3937306106 0.3884935081 - 0.3691891134 0.3959082961 0.3905969858 - 0.3712717891 0.3980861008 0.3926754892 - 0.3733437955 0.4002606869 0.3947539926 - 0.3754158914 0.4024128914 0.3968324065 - 0.3774879873 0.4045650959 0.3989109099 - 0.3795599937 0.4067172110 0.4009746909 - 0.3816126883 0.4088693857 0.4030224979 - 0.3836601973 0.4110105038 0.4050701857 - 0.3857075870 0.4131393135 0.4071179032 - 0.3877550960 0.4152680933 0.4091657102 - 0.3898024857 0.4173969030 0.4111976027 - 0.3918300867 0.4195258021 0.4132185876 - 0.3938556015 0.4216380119 0.4152396023 - 0.3958810866 0.4237453938 0.4172605872 - 0.3979066014 0.4258528054 0.4192816019 - 0.3999319971 0.4279601872 0.4212876856 - 0.4019384980 0.4300670028 0.4232856929 - 0.4039444029 0.4321546853 0.4252836108 - 0.4059503078 0.4342423081 0.4272815883 - 0.4079560935 0.4363299906 0.4292795062 - 0.4099619985 0.4384176135 0.4312647879 - 0.4119505882 0.4405007958 0.4332428873 - 0.4139389098 0.4425699115 0.4352209866 - 0.4159272015 0.4446390867 0.4371992052 - 0.4179156125 0.4467082024 0.4391773045 - 0.4199039042 0.4487774074 0.4411455989 - 0.4218772054 0.4508393109 0.4431068003 - 0.4238497913 0.4528909028 0.4450680017 - 0.4258224070 0.4549424946 0.4470292926 - 0.4277949929 0.4569939971 0.4489904940 - 0.4297676086 0.4590455890 0.4509448111 - 0.4317277074 0.4610880911 0.4528915882 - 0.4336861074 0.4631226957 0.4548385143 - 0.4356445074 0.4651573002 0.4567854106 - 0.4376029074 0.4671919048 0.4587321877 - 0.4395613074 0.4692265093 0.4606749117 - 0.4415096939 0.4712507129 0.4626095891 - 0.4434551895 0.4732685983 0.4645442963 - 0.4454007149 0.4752865136 0.4664790034 - 0.4473460913 0.4773043096 0.4684137106 - 0.4492915869 0.4793221951 0.4703465104 - 0.4512295127 0.4813289046 0.4722707868 - 0.4531629980 0.4833298922 0.4741952121 - 0.4550965130 0.4853309095 0.4761194885 - 0.4570299983 0.4873319864 0.4780437946 - 0.4589635134 0.4893330038 0.4799681902 - 0.4608916938 0.4913224876 0.4818837047 - 0.4628139138 0.4933061898 0.4837990999 - 0.4647361040 0.4952898920 0.4857144952 - 0.4666582942 0.4972735941 0.4876298904 - 0.4685805142 0.4992572963 0.4895452857 - 0.4704998136 0.5012295842 0.4914546907 - 0.4724110067 0.5031952262 0.4933620989 - 0.4743221998 0.5051606894 0.4952695966 - 0.4762333930 0.5071262717 0.4971770942 - 0.4781445861 0.5090917945 0.4990845919 - 0.4800555110 0.5110468864 0.5009883046 - 0.4819557965 0.5129930973 0.5028885007 - 0.4838559926 0.5149393082 0.5047886968 - 0.4857563078 0.5168855190 0.5066888928 - 0.4876565039 0.5188316703 0.5085890889 - 0.4895567894 0.5207694769 0.5104873776 - 0.4914484024 0.5226947069 0.5123804808 - 0.4933373034 0.5246199965 0.5142735243 - 0.4952262938 0.5265452862 0.5161666274 - 0.4971151948 0.5284705758 0.5180596113 - 0.4990041852 0.5303910971 0.5199525952 - 0.5008875132 0.5322936177 0.5218384266 - 0.5027645230 0.5341960788 0.5237240195 - 0.5046414733 0.5360984802 0.5256096125 - 0.5065184832 0.5380010009 0.5274953246 - 0.5083954930 0.5399035215 0.5293809175 - 0.5102707148 0.5417822003 0.5312609076 - 0.5121346712 0.5436596274 0.5331382751 - 0.5139986873 0.5455371141 0.5350157022 - 0.5158627033 0.5474146008 0.5368930101 - 0.5177267790 0.5492920280 0.5387703776 - 0.5195907950 0.5511524081 0.5406445265 - 0.5214436054 0.5530024171 0.5425122976 - 0.5232933164 0.5548523068 0.5443801284 - 0.5251430273 0.5567023158 0.5462480187 - 0.5269926786 0.5585523248 0.5481157899 - 0.5288422704 0.5603957176 0.5499836206 - 0.5306860209 0.5622155070 0.5518401861 - 0.5325195789 0.5640351772 0.5536966920 - 0.5343531966 0.5658550262 0.5555531979 - 0.5361868143 0.5676748157 0.5574097037 - 0.5380203724 0.5694946051 0.5592660904 - 0.5398539901 0.5712903738 0.5611143112 - 0.5416709185 0.5730770826 0.5629572272 - 0.5434862971 0.5748637915 0.5648000240 - 0.5453017950 0.5766503811 0.5666428208 - 0.5471172929 0.5784370899 0.5684856176 - 0.5489326715 0.5802192092 0.5703253746 - 0.5507397056 0.5819697976 0.5721517205 - 0.5525346994 0.5837203264 0.5739780068 - 0.5543295741 0.5854709148 0.5758042932 - 0.5561246276 0.5872213840 0.5776305795 - 0.5579195023 0.5889719725 0.5794569254 - 0.5597143769 0.5907062888 0.5812692046 - 0.5614898801 0.5924177170 0.5830757022 - 0.5632616878 0.5941290259 0.5848821998 - 0.5650333762 0.5958402753 0.5866885781 - 0.5668051839 0.5975517035 0.5884950757 - 0.5685769916 0.5992630124 0.5902976990 - 0.5703436136 0.6009503007 0.5920805931 - 0.5720893145 0.6026192904 0.5938634872 - 0.5738348961 0.6042883992 0.5956465006 - 0.5755805969 0.6059575081 0.5974293947 - 0.5773262978 0.6076264977 0.5992122889 - 0.5790719986 0.6092956066 0.6009799242 - 0.5808039904 0.6109384894 0.6027352214 - 0.5825204849 0.6125624180 0.6044905186 - 0.5842369795 0.6141862273 0.6062458158 - 0.5859534740 0.6158099771 0.6080011725 - 0.5876700282 0.6174339056 0.6097565293 - 0.5893865228 0.6190577149 0.6114842892 - 0.5910822153 0.6206613779 0.6132075787 - 0.5927662849 0.6222372055 0.6149309874 - 0.5944504142 0.6238129735 0.6166543961 - 0.5961344838 0.6253888011 0.6183778048 - 0.5978186131 0.6269646287 0.6200990081 - 0.5995026827 0.6285403967 0.6217858791 - 0.6011615992 0.6301125288 0.6234729290 - 0.6028100848 0.6316378117 0.6251598001 - 0.6044585109 0.6331629753 0.6268466711 - 0.6061068773 0.6346881986 0.6285337210 - 0.6077553034 0.6362134814 0.6302152276 - 0.6094037294 0.6377387047 0.6318612099 - 0.6110271811 0.6392639279 0.6335071921 - 0.6126366854 0.6407619119 0.6351531744 - 0.6142461896 0.6422343254 0.6367992163 - 0.6158555746 0.6437066793 0.6384451985 - 0.6174650788 0.6451790929 0.6400886178 - 0.6190745234 0.6466515064 0.6416891813 - 0.6206660867 0.6481239796 0.6432899237 - 0.6222333908 0.6495963931 0.6448904872 - 0.6238006949 0.6510291100 0.6464911103 - 0.6253681183 0.6524468064 0.6480916739 - 0.6269354224 0.6538646221 0.6496922970 - 0.6285027266 0.6552823186 0.6512529254 - 0.6300681233 0.6567000151 0.6528040171 - 0.6315903068 0.6581177115 0.6543551087 - 0.6331124902 0.6595354080 0.6559062004 - 0.6346346736 0.6609153748 0.6574572921 - 0.6361569166 0.6622769237 0.6590083838 - 0.6376791000 0.6636384726 0.6605402231 - 0.6392014027 0.6650000215 0.6620379090 - 0.6407008171 0.6663615108 0.6635357141 - 0.6421751976 0.6677231193 0.6650334001 - 0.6436495185 0.6690846086 0.6665312052 - 0.6451238990 0.6704273820 0.6680288911 - 0.6465982199 0.6717317104 0.6695266962 - 0.6480726004 0.6730359793 0.6709856987 - 0.6495469213 0.6743403077 0.6724267006 - 0.6509864926 0.6756445765 0.6738678217 - 0.6524105072 0.6769489050 0.6753088832 - 0.6538345814 0.6782531738 0.6767500043 - 0.6552587152 0.6795575023 0.6781910062 - 0.6566827297 0.6808236241 0.6796321273 - 0.6581068039 0.6820700169 0.6810289025 - 0.6595308781 0.6833164096 0.6824105978 - 0.6609197855 0.6845629215 0.6837922931 - 0.6622915864 0.6858093143 0.6851739287 - 0.6636633277 0.6870558262 0.6865556240 - 0.6650350094 0.6883022189 0.6879372001 - 0.6664068103 0.6895486116 0.6893188953 - 0.6677784920 0.6907581091 0.6906694174 - 0.6691501737 0.6919463873 0.6919894814 - 0.6705014110 0.6931347847 0.6933096051 - 0.6718192101 0.6943231821 0.6946297288 - 0.6731368899 0.6955115795 0.6959499121 - 0.6744546890 0.6966999769 0.6972699761 - 0.6757724285 0.6978883743 0.6985900998 - 0.6770902276 0.6990767717 0.6999102235 - 0.6784079075 0.7002522945 0.7011716962 - 0.6797257066 0.7013828754 0.7024288177 - 0.6809998155 0.7025135159 0.7036859989 - 0.6822623014 0.7036440969 0.7049431205 - 0.6835249066 0.7047746778 0.7062003016 - 0.6847875118 0.7059053183 0.7074574232 - 0.6860501170 0.7070358992 0.7087146044 - 0.6873127222 0.7081664801 0.7099717259 - 0.6885753274 0.7092970014 0.7111666203 - 0.6898378730 0.7104060054 0.7123600245 - 0.6910517216 0.7114794254 0.7135534286 - 0.6922584176 0.7125527859 0.7147467732 - 0.6934651136 0.7136262059 0.7159401774 - 0.6946716905 0.7146996260 0.7171337008 - 0.6958783865 0.7157731056 0.7183271050 - 0.6970850825 0.7168465257 0.7195205092 - 0.6982917786 0.7179198861 0.7206757069 - 0.6994984150 0.7189933062 0.7218052745 - 0.7006723285 0.7200632095 0.7229349017 - 0.7018228173 0.7210804224 0.7240644097 - 0.7029731870 0.7220975757 0.7251939774 - 0.7041236758 0.7231147885 0.7263236046 - 0.7052742243 0.7241321206 0.7274531722 - 0.7064247131 0.7251492739 0.7285827994 - 0.7075752020 0.7261664867 0.7297123075 - 0.7087255716 0.7271836996 0.7307947278 - 0.7098761201 0.7282009125 0.7318609953 - 0.7109766006 0.7292181253 0.7329272032 - 0.7120710015 0.7302225828 0.7339934707 - 0.7131654024 0.7311848998 0.7350596786 - 0.7142598033 0.7321472168 0.7361260056 - 0.7153543234 0.7331095934 0.7371922731 - 0.7164487243 0.7340719104 0.7382584810 - 0.7175431252 0.7350342274 0.7393248081 - 0.7186375260 0.7359964848 0.7403681874 - 0.7197319865 0.7369588017 0.7413722277 - 0.7207844853 0.7379211187 0.7423762083 - 0.7218233943 0.7388833761 0.7433801889 - 0.7228623033 0.7398456931 0.7443842292 - 0.7239012122 0.7407631874 0.7453882098 - 0.7249401212 0.7416722178 0.7463921905 - 0.7259790897 0.7425811887 0.7473962903 - 0.7270179987 0.7434902191 0.7484002709 - 0.7280569077 0.7443990707 0.7494043112 - 0.7290958166 0.7453081012 0.7503836155 - 0.7301275730 0.7462170720 0.7513269186 - 0.7311120033 0.7471259832 0.7522702217 - 0.7320963144 0.7480350137 0.7532135248 - 0.7330806255 0.7489439845 0.7541568279 - 0.7340649962 0.7498528957 0.7551000714 - 0.7350493073 0.7507187128 0.7560433745 - 0.7360336185 0.7515761256 0.7569866776 - 0.7370179296 0.7524334788 0.7579299808 - 0.7380023003 0.7532908916 0.7588732839 - 0.7389866114 0.7541484237 0.7598165870 - 0.7399709225 0.7550057769 0.7607126236 - 0.7409034967 0.7558631897 0.7615972161 - 0.7418344021 0.7567206025 0.7624816895 - 0.7427654266 0.7575780153 0.7633662820 - 0.7436963916 0.7584354281 0.7642508149 - 0.7446272969 0.7592927814 0.7651354074 - 0.7455583215 0.7601416111 0.7660198808 - 0.7464892864 0.7609493732 0.7669044733 - 0.7474203110 0.7617571950 0.7677890062 - 0.7483512163 0.7625650167 0.7686735988 - 0.7492821813 0.7633727789 0.7695580721 - 0.7502012849 0.7641806006 0.7704144120 - 0.7510803938 0.7649884224 0.7712424994 - 0.7519595027 0.7657961845 0.7720705271 - 0.7528386712 0.7666041255 0.7728986144 - 0.7537177801 0.7674118876 0.7737267017 - 0.7545968890 0.7682197094 0.7745547295 - 0.7554759979 0.7690274715 0.7753828168 - 0.7563552260 0.7698352933 0.7762109041 - 0.7572342753 0.7706053257 0.7770388722 - 0.7581133842 0.7713655829 0.7778670192 - 0.7589924932 0.7721258998 0.7786949873 - 0.7598717213 0.7728862166 0.7795230746 - 0.7607079744 0.7736464739 0.7803282738 - 0.7615370154 0.7744067907 0.7811022997 - 0.7623659968 0.7751671076 0.7818763852 - 0.7631949782 0.7759273052 0.7826504707 - 0.7640240192 0.7766876221 0.7834246159 - 0.7648531199 0.7774478793 0.7841985822 - 0.7656821012 0.7782081962 0.7849727273 - 0.7665110826 0.7789685130 0.7857468128 - 0.7673401237 0.7797287703 0.7865208983 - 0.7681691051 0.7804598808 0.7872949243 - 0.7689980865 0.7811748981 0.7880690098 - 0.7698271275 0.7818897963 0.7888430953 - 0.7706180215 0.7826048136 0.7896171212 - 0.7713987827 0.7833197117 0.7903652787 - 0.7721796036 0.7840347290 0.7910879850 - 0.7729604244 0.7847496271 0.7918108106 - 0.7737411857 0.7854645848 0.7925335169 - 0.7745220065 0.7861794829 0.7932562828 - 0.7753028274 0.7868943810 0.7939789891 - 0.7760835886 0.7876093984 0.7947018147 - 0.7768645287 0.7883242965 0.7954245210 - 0.7776452899 0.7890393138 0.7961472273 - 0.7784261107 0.7897542119 0.7968699932 - 0.7792068720 0.7904409170 0.7975926995 - 0.7799876928 0.7911127210 0.7983155251 - 0.7807230949 0.7917845249 0.7990381718 - 0.7814577222 0.7924563289 0.7997609973 - 0.7821922898 0.7931280732 0.8004512191 - 0.7829269767 0.7937998772 0.8011254072 - 0.7836616039 0.7944718003 0.8017995954 - 0.7843962908 0.7951436043 0.8024737239 - 0.7851309180 0.7958154082 0.8031479120 - 0.7858656049 0.7964872122 0.8038221002 - 0.7866001725 0.7971590161 0.8044962883 - 0.7873347998 0.7978308201 0.8051704168 - 0.7880694866 0.7985026836 0.8058446050 - 0.7888041139 0.7991744876 0.8065187931 - 0.7895388007 0.7998462915 0.8071929812 - 0.7902569771 0.8004866242 0.8078672290 - 0.7909476161 0.8011174798 0.8085412979 - 0.7916381955 0.8017483950 0.8092154860 - 0.7923287749 0.8023793101 0.8098896742 - 0.7930194139 0.8030102253 0.8105255961 - 0.7937099934 0.8036412001 0.8111540079 - 0.7944005728 0.8042721152 0.8117824197 - 0.7950912118 0.8049029708 0.8124107718 - 0.7957817912 0.8055338860 0.8130391836 - 0.7964723706 0.8061649203 0.8136675954 - 0.7971630096 0.8067957759 0.8142960072 - 0.7978535891 0.8074266911 0.8149244189 - 0.7985442281 0.8080576062 0.8155527711 - 0.7992348075 0.8086885214 0.8161811829 - 0.7999253869 0.8093194962 0.8168095946 - 0.8005787134 0.8099504113 0.8174380064 - 0.8012273908 0.8105456829 0.8180664182 - 0.8018761873 0.8111379147 0.8186947703 - 0.8025249243 0.8117302060 0.8193231821 - 0.8031736016 0.8123223782 0.8199515939 - 0.8038223982 0.8129147291 0.8205404282 - 0.8044710755 0.8135069013 0.8211258054 - 0.8051198125 0.8140991926 0.8217111826 - 0.8057686090 0.8146914244 0.8222966194 - 0.8064172864 0.8152837157 0.8228819966 - 0.8070660233 0.8158758879 0.8234674931 - 0.8077148199 0.8164681792 0.8240528703 - 0.8083634973 0.8170604110 0.8246383071 - 0.8090121746 0.8176527023 0.8252236843 - 0.8096609712 0.8182448745 0.8258091211 - 0.8102908134 0.8188372254 0.8263946176 - 0.8108997941 0.8194293976 0.8269799948 - 0.8115088940 0.8200203776 0.8275653720 - 0.8121178746 0.8205760717 0.8281508088 - 0.8127269745 0.8211318851 0.8287361860 - 0.8133360147 0.8216876984 0.8293216825 - 0.8139451146 0.8222435117 0.8299071193 - 0.8145542145 0.8227992058 0.8304585814 - 0.8151631951 0.8233550191 0.8310037851 - 0.8157722950 0.8239107728 0.8315489888 - 0.8163812757 0.8244665265 0.8320941925 - 0.8169903755 0.8250222802 0.8326392770 - 0.8175994158 0.8255780935 0.8331844807 - 0.8182085156 0.8261339068 0.8337296844 - 0.8188176155 0.8266896009 0.8342748880 - 0.8194265962 0.8272454143 0.8348199725 - 0.8200334907 0.8278012276 0.8353651762 - 0.8206049800 0.8283569217 0.8359103799 - 0.8211765885 0.8289126754 0.8364555836 - 0.8217481971 0.8294684887 0.8370007277 - 0.8223196864 0.8300228119 0.8375458717 - 0.8228912950 0.8305441737 0.8380910754 - 0.8234627843 0.8310655951 0.8386362791 - 0.8240343928 0.8315870166 0.8391814232 - 0.8246060014 0.8321084976 0.8397266269 - 0.8251774907 0.8326299191 0.8402529955 - 0.8257490993 0.8331512809 0.8407605886 - 0.8263205886 0.8336728215 0.8412681818 - 0.8268921971 0.8341941833 0.8417757750 - 0.8274638057 0.8347156048 0.8422834277 - 0.8280352950 0.8352370858 0.8427910209 - 0.8286069036 0.8357585073 0.8432986140 - 0.8291783929 0.8362799287 0.8438062072 - 0.8297500014 0.8368014097 0.8443138003 - 0.8303017020 0.8373227715 0.8448213935 - 0.8308379054 0.8378441930 0.8453289866 - 0.8313741088 0.8383656144 0.8458365798 - 0.8319103122 0.8388870955 0.8463441730 - 0.8324465156 0.8394085169 0.8468517065 - 0.8329827189 0.8399298787 0.8473592997 - 0.8335189223 0.8404234052 0.8478668928 - 0.8340551257 0.8409125805 0.8483744860 - 0.8345913291 0.8414016962 0.8488820791 - 0.8351274729 0.8418908715 0.8493896723 - 0.8356636763 0.8423801064 0.8498973250 - 0.8361998796 0.8428692222 0.8503770232 - 0.8367360830 0.8433583975 0.8508495092 - 0.8372722864 0.8438475132 0.8513221145 - 0.8378084898 0.8443366885 0.8517947197 - 0.8383446932 0.8448258042 0.8522673249 - 0.8388810158 0.8453149796 0.8527398109 - 0.8394172192 0.8458042145 0.8532124162 - 0.8399534225 0.8462932706 0.8536850214 - 0.8404592276 0.8467825055 0.8541575074 - 0.8409621119 0.8472716212 0.8546301126 - 0.8414651155 0.8477607965 0.8551027179 - 0.8419679999 0.8482499719 0.8555753231 - 0.8424710035 0.8487390876 0.8560478091 - 0.8429738879 0.8492283225 0.8565204144 - 0.8434768915 0.8497173786 0.8569930196 - 0.8439797759 0.8501937985 0.8574656248 - 0.8444827199 0.8506526947 0.8579381108 - 0.8449857235 0.8511115909 0.8584107161 - 0.8454886079 0.8515704274 0.8588833213 - 0.8459916115 0.8520293236 0.8593558073 - 0.8464944959 0.8524882197 0.8598284125 - 0.8469973803 0.8529471159 0.8602802157 - 0.8475003839 0.8534058928 0.8607202172 - 0.8480033278 0.8538647890 0.8611602187 - 0.8485062718 0.8543236852 0.8616002202 - 0.8490092158 0.8547825813 0.8620402217 - 0.8495122194 0.8552414775 0.8624802232 - 0.8500142097 0.8557003140 0.8629202247 - 0.8504859209 0.8561592102 0.8633602262 - 0.8509575725 0.8566181064 0.8638002276 - 0.8514292836 0.8570770025 0.8642402291 - 0.8519009948 0.8575357795 0.8646801710 - 0.8523727059 0.8579946756 0.8651201725 - 0.8528444171 0.8584535718 0.8655601740 - 0.8533161283 0.8589125276 0.8660001755 - 0.8537877798 0.8593714237 0.8664401770 - 0.8542594910 0.8598302007 0.8668801785 - 0.8547312021 0.8602712154 0.8673201799 - 0.8552029133 0.8607016802 0.8677601814 - 0.8556746244 0.8611322045 0.8682001829 - 0.8561462760 0.8615627289 0.8686401844 - 0.8566179872 0.8619931936 0.8690801859 - 0.8570896983 0.8624237180 0.8695201874 - 0.8575614095 0.8628543019 0.8699601889 - 0.8580331206 0.8632848263 0.8703727126 - 0.8585047722 0.8637152910 0.8707824945 - 0.8589764833 0.8641458154 0.8711922169 - 0.8594481945 0.8645762801 0.8716019988 - 0.8599199057 0.8650068045 0.8720117211 - 0.8603671789 0.8654373288 0.8724213839 - 0.8608096838 0.8658677936 0.8728312254 - 0.8612521291 0.8662983179 0.8732408881 - 0.8616945148 0.8667287827 0.8736507297 - 0.8621369004 0.8671593070 0.8740603924 - 0.8625792861 0.8675897717 0.8744701743 - 0.8630216718 0.8680202961 0.8748798966 - 0.8634641171 0.8684508204 0.8752896786 - 0.8639065027 0.8688812852 0.8756994009 - 0.8643488884 0.8693118095 0.8761091828 - 0.8647912741 0.8697422743 0.8765189052 - 0.8652337193 0.8701621294 0.8769286275 - 0.8656761050 0.8705661297 0.8773384094 - 0.8661184907 0.8709700108 0.8777480721 - 0.8665608764 0.8713740110 0.8781579137 - 0.8670033216 0.8717778921 0.8785675764 - 0.8674457073 0.8721818924 0.8789774179 - 0.8678880930 0.8725857735 0.8793870807 - 0.8683304787 0.8729897141 0.8797969222 - 0.8687729239 0.8733937144 0.8801925182 - 0.8692153096 0.8737975955 0.8805741072 - 0.8696576953 0.8742015958 0.8809558153 - 0.8700938821 0.8746054769 0.8813375235 - 0.8705089092 0.8750094771 0.8817191720 - 0.8709238768 0.8754134178 0.8821008801 - 0.8713387847 0.8758172989 0.8824825287 - 0.8717538118 0.8762212992 0.8828641772 - 0.8721687198 0.8766251802 0.8832458854 - 0.8725836873 0.8770291805 0.8836275935 - 0.8729987144 0.8774331212 0.8840093017 - 0.8734136224 0.8778371215 0.8843908906 - 0.8738285899 0.8782410026 0.8847725987 - 0.8742436171 0.8786448836 0.8851543069 - 0.8746585250 0.8790488839 0.8855360150 - 0.8750734925 0.8794528246 0.8859177232 - 0.8754885197 0.8798568249 0.8862993121 - 0.8759034276 0.8802446723 0.8866810203 - 0.8763183951 0.8806238174 0.8870627284 - 0.8767334223 0.8810029030 0.8874443769 - 0.8771482706 0.8813819885 0.8878260255 - 0.8775632977 0.8817610741 0.8882076740 - 0.8779783249 0.8821402192 0.8885893822 - 0.8783931732 0.8825193048 0.8889710903 - 0.8788082004 0.8828983903 0.8893527985 - 0.8792232275 0.8832774758 0.8897343874 - 0.8796380758 0.8836566210 0.8901082277 - 0.8800498247 0.8840357065 0.8904638886 - 0.8804391026 0.8844147921 0.8908194900 - 0.8808283806 0.8847938776 0.8911752105 - 0.8812177181 0.8851730227 0.8915308714 - 0.8816069961 0.8855521083 0.8918864727 - 0.8819962740 0.8859311938 0.8922421932 - 0.8823856115 0.8863102794 0.8925979137 - 0.8827748895 0.8866894245 0.8929535151 - 0.8831642270 0.8870685101 0.8933091760 - 0.8835535049 0.8874475956 0.8936648965 - 0.8839427233 0.8878266811 0.8940204978 - 0.8843320012 0.8882058263 0.8943762183 - 0.8847212791 0.8885849118 0.8947318792 - 0.8851106167 0.8889639974 0.8950874805 - 0.8854998946 0.8893430829 0.8954432011 - 0.8858891726 0.8897222877 0.8957989216 - 0.8862785101 0.8900951147 0.8961545229 - 0.8866677880 0.8904510140 0.8965101838 - 0.8870571256 0.8908069134 0.8968659043 - 0.8874464035 0.8911628127 0.8972215056 - 0.8878356814 0.8915187120 0.8975772262 - 0.8882250190 0.8918746114 0.8979328871 - 0.8886142969 0.8922305107 0.8982884884 - 0.8890035748 0.8925864100 0.8986442089 - 0.8893929124 0.8929423094 0.8989998102 - 0.8897821903 0.8932980895 0.8993554711 - 0.8901609182 0.8936539888 0.8997111917 - 0.8905261755 0.8940098882 0.9000623226 - 0.8908914924 0.8943657875 0.9003939033 - 0.8912568092 0.8947216868 0.9007254839 - 0.8916221261 0.8950775862 0.9010570049 - 0.8919873834 0.8954334855 0.9013885856 - 0.8923527002 0.8957893848 0.9017202258 - 0.8927180171 0.8961452842 0.9020516872 - 0.8930832744 0.8965011239 0.9023833275 - 0.8934485912 0.8968570232 0.9027149081 - 0.8938139081 0.8972129226 0.9030464888 - 0.8941792250 0.8975688219 0.9033780098 - 0.8945444822 0.8979247212 0.9037095904 - 0.8949097991 0.8982806206 0.9040411711 - 0.8952751160 0.8986365199 0.9043726921 - 0.8956403732 0.8989924192 0.9047042727 - 0.8960056901 0.8993481994 0.9050359130 - 0.8963710070 0.8997040987 0.9053673744 - 0.8967363238 0.9000564218 0.9056990147 - 0.8971015811 0.9003906250 0.9060305953 - 0.8974668980 0.9007248282 0.9063621759 - 0.8978322148 0.9010589719 0.9066936970 - 0.8981974721 0.9013931751 0.9070252776 - 0.8985627890 0.9017273784 0.9073569179 - 0.8989281058 0.9020615816 0.9076883793 - 0.8992933035 0.9023957849 0.9080200195 - 0.8996586204 0.9027299881 0.9083516002 - 0.9000225067 0.9030641913 0.9086831808 - 0.9003654122 0.9033983946 0.9090147018 - 0.9007081985 0.9037325978 0.9093462825 - 0.9010511041 0.9040668011 0.9096779227 - 0.9013940096 0.9044010043 0.9100087881 - 0.9017369151 0.9047352076 0.9103180766 - 0.9020798206 0.9050694108 0.9106273055 - 0.9024227262 0.9054036140 0.9109365940 - 0.9027655125 0.9057378173 0.9112458825 - 0.9031084180 0.9060720801 0.9115551114 - 0.9034513235 0.9064062834 0.9118643999 - 0.9037942290 0.9067404866 0.9121736884 - 0.9041370749 0.9070746899 0.9124829173 - 0.9044799805 0.9074088931 0.9127922058 - 0.9048228264 0.9077430964 0.9131013751 - 0.9051656723 0.9080772996 0.9134107232 - 0.9055085778 0.9084115028 0.9137200117 - 0.9058514833 0.9087457061 0.9140291810 - 0.9061943889 0.9090799093 0.9143385291 - 0.9065371752 0.9094141126 0.9146478176 - 0.9068800807 0.9097483158 0.9149569869 - 0.9072229862 0.9100775123 0.9152662754 - 0.9075658917 0.9103915095 0.9155756235 - 0.9079087973 0.9107053876 0.9158847928 - 0.9082517028 0.9110193849 0.9161940813 - 0.9085944891 0.9113333821 0.9165033102 - 0.9089373946 0.9116473198 0.9168125987 - 0.9092803001 0.9119613171 0.9171218872 - 0.9096232057 0.9122751951 0.9174311161 - 0.9099661112 0.9125891924 0.9177404046 - 0.9102901220 0.9129031897 0.9180496931 - 0.9106121063 0.9132171273 0.9183589220 - 0.9109339714 0.9135311246 0.9186682105 - 0.9112560153 0.9138450027 0.9189774990 - 0.9115778804 0.9141589999 0.9192867279 - 0.9118999243 0.9144729972 0.9195960164 - 0.9122217894 0.9147868752 0.9199051857 - 0.9125437737 0.9151008725 0.9202002287 - 0.9128656983 0.9154148102 0.9204887748 - 0.9131876826 0.9157288074 0.9207773805 - 0.9135096073 0.9160428047 0.9210659862 - 0.9138315916 0.9163566828 0.9213547111 - 0.9141535759 0.9166706800 0.9216433167 - 0.9144755006 0.9169846177 0.9219319224 - 0.9147974849 0.9172986150 0.9222205281 - 0.9151194096 0.9176126122 0.9225090742 - 0.9154413939 0.9179264903 0.9227977991 - 0.9157633185 0.9182404876 0.9230864048 - 0.9160853028 0.9185544252 0.9233750105 - 0.9164072275 0.9188684225 0.9236636162 - 0.9167292118 0.9191824198 0.9239522219 - 0.9170510769 0.9194962978 0.9242408276 - 0.9173731208 0.9198102951 0.9245294929 - 0.9176951051 0.9201167822 0.9248180985 - 0.9180170298 0.9204118252 0.9251067042 - 0.9183390141 0.9207069278 0.9253953099 - 0.9186608791 0.9210019112 0.9256839156 - 0.9189829230 0.9212970138 0.9259725809 - 0.9193047881 0.9215921164 0.9262611866 - 0.9196267724 0.9218870997 0.9265497923 - 0.9199486971 0.9221822023 0.9268383980 - 0.9202542901 0.9224773049 0.9271270037 - 0.9205567241 0.9227722883 0.9274156094 - 0.9208590984 0.9230673909 0.9277042747 - 0.9211614728 0.9233623743 0.9279928803 - 0.9214640260 0.9236574769 0.9282814860 - 0.9217664003 0.9239525795 0.9285700917 - 0.9220687747 0.9242476225 0.9288586974 - 0.9223712087 0.9245427251 0.9291474223 - 0.9226737022 0.9248378277 0.9294360280 - 0.9229760766 0.9251328111 0.9297245741 - 0.9232785106 0.9254279137 0.9300122857 - 0.9235808849 0.9257230163 0.9302818179 - 0.9238833189 0.9260179996 0.9305514097 - 0.9241858125 0.9263131022 0.9308208823 - 0.9244881868 0.9266080856 0.9310904145 - 0.9247906208 0.9269031882 0.9313598871 - 0.9250929952 0.9271982908 0.9316294193 - 0.9253954887 0.9274932742 0.9318988919 - 0.9256979227 0.9277883768 0.9321684241 - 0.9260002971 0.9280834794 0.9324380159 - 0.9263026714 0.9283785224 0.9327074885 - 0.9266052246 0.9286736250 0.9329770207 - 0.9269075990 0.9289686084 0.9332464933 - 0.9272099733 0.9292637110 0.9335160255 - 0.9275124073 0.9295588136 0.9337854981 - 0.9278147817 0.9298537970 0.9340549707 - 0.9281172752 0.9301400185 0.9343245029 - 0.9284197092 0.9304174185 0.9345940948 - 0.9287220836 0.9306948781 0.9348636270 - 0.9290245175 0.9309722781 0.9351330996 - 0.9293270111 0.9312496781 0.9354025722 - 0.9296293855 0.9315270782 0.9356721044 - 0.9299318194 0.9318045974 0.9359415770 - 0.9302201271 0.9320819974 0.9362111092 - 0.9305043221 0.9323593974 0.9364805818 - 0.9307885170 0.9326369166 0.9367501736 - 0.9310727119 0.9329143167 0.9370197058 - 0.9313569069 0.9331917167 0.9372891784 - 0.9316411018 0.9334691167 0.9375587106 - 0.9319252968 0.9337465763 0.9378281832 - 0.9322094917 0.9340239763 0.9380977154 - 0.9324936867 0.9343013763 0.9383671880 - 0.9327778816 0.9345787764 0.9386367202 - 0.9330620766 0.9348562956 0.9389063120 - 0.9333462715 0.9351336956 0.9391757846 - 0.9336305261 0.9354110956 0.9394453168 - 0.9339147210 0.9356886148 0.9397147894 - 0.9341989160 0.9359660149 0.9399843216 - 0.9344831109 0.9362434149 0.9402372241 - 0.9347673059 0.9365208149 0.9404889941 - 0.9350515008 0.9367982745 0.9407408237 - 0.9353356957 0.9370756745 0.9409927130 - 0.9356198907 0.9373530746 0.9412444830 - 0.9359040856 0.9376305938 0.9414963126 - 0.9361882806 0.9379079938 0.9417482018 - 0.9364724755 0.9381853938 0.9419999719 - 0.9367566705 0.9384627938 0.9422518015 - 0.9370409250 0.9387403131 0.9425036907 - 0.9373251200 0.9390177131 0.9427555203 - 0.9376093149 0.9392951131 0.9430072904 - 0.9378935099 0.9395725727 0.9432591796 - 0.9381777048 0.9398499727 0.9435110092 - 0.9384618998 0.9401198030 0.9437627792 - 0.9387460947 0.9403808117 0.9440147281 - 0.9390302896 0.9406418204 0.9442664981 - 0.9393144846 0.9409028292 0.9445183277 - 0.9395986795 0.9411637187 0.9447702169 - 0.9398828745 0.9414247274 0.9450219870 - 0.9401571155 0.9416856766 0.9452738166 - 0.9404243231 0.9419466257 0.9455257058 - 0.9406914711 0.9422075748 0.9457774758 - 0.9409586787 0.9424685836 0.9460293055 - 0.9412258863 0.9427295923 0.9462810755 - 0.9414930940 0.9429904819 0.9465330243 - 0.9417603016 0.9432514906 0.9467847943 - 0.9420275092 0.9435124993 0.9470366240 - 0.9422945976 0.9437733889 0.9472885132 - 0.9425618052 0.9440343976 0.9475402832 - 0.9428290129 0.9442954063 0.9477921128 - 0.9430962205 0.9445564151 0.9480440021 - 0.9433634281 0.9448173046 0.9482957721 - 0.9436305761 0.9450783134 0.9485476017 - 0.9438977838 0.9453393221 0.9487994909 - 0.9441649914 0.9456002116 0.9490513206 - 0.9444321990 0.9458612204 0.9493030906 - 0.9446994066 0.9461222291 0.9495549798 - 0.9449666142 0.9463831782 0.9498068094 - 0.9452338219 0.9466441274 0.9500548244 - 0.9455010295 0.9469050765 0.9502903223 - 0.9457681775 0.9471660852 0.9505258203 - 0.9460353851 0.9474269748 0.9507611990 - 0.9463025928 0.9476879835 0.9509966969 - 0.9465698004 0.9479489923 0.9512321949 - 0.9468370080 0.9482100010 0.9514675736 - 0.9471042156 0.9484708905 0.9517030716 - 0.9473714232 0.9487318993 0.9519386292 - 0.9476385713 0.9489929080 0.9521741271 - 0.9479057789 0.9492537975 0.9524095058 - 0.9481729865 0.9495148063 0.9526450038 - 0.9484401941 0.9497758150 0.9528805017 - 0.9487074018 0.9500346184 0.9531158805 - 0.9489746094 0.9502801895 0.9533513784 - 0.9492418170 0.9505258203 0.9535868764 - 0.9495090246 0.9507713914 0.9538223147 - 0.9497761726 0.9510170221 0.9540578127 - 0.9500408173 0.9512627125 0.9542933106 - 0.9502921104 0.9515082836 0.9545286894 - 0.9505435228 0.9517539144 0.9547641873 - 0.9507948160 0.9519994855 0.9549996853 - 0.9510461092 0.9522451162 0.9552351832 - 0.9512975216 0.9524906874 0.9554706216 - 0.9515488148 0.9527363181 0.9557061195 - 0.9518002272 0.9529820085 0.9559416175 - 0.9520515203 0.9532275796 0.9561769962 - 0.9523028135 0.9534732103 0.9564124942 - 0.9525542259 0.9537187815 0.9566479921 - 0.9528055191 0.9539644122 0.9568833709 - 0.9530568123 0.9542099833 0.9571189284 - 0.9533082247 0.9544556141 0.9573544264 - 0.9535595179 0.9547013044 0.9575898051 - 0.9538108110 0.9549468756 0.9578253031 - 0.9540622234 0.9551925063 0.9580608010 - 0.9543135166 0.9554380774 0.9582962990 - 0.9545649290 0.9556837082 0.9585316777 - 0.9548162222 0.9559292793 0.9587671757 - 0.9550675154 0.9561749101 0.9590026736 - 0.9553189278 0.9564206004 0.9592381120 - 0.9555702209 0.9566661716 0.9594736099 - 0.9558215141 0.9569118023 0.9597091079 - 0.9560729265 0.9571573734 0.9599444866 - 0.9563242197 0.9574030042 0.9601684213 - 0.9565755725 0.9576485753 0.9603887796 - 0.9568269253 0.9578942060 0.9606090784 - 0.9570782185 0.9581398964 0.9608293772 - 0.9573295712 0.9583855271 0.9610496759 - 0.9575809240 0.9586310983 0.9612699747 - 0.9578322172 0.9588767290 0.9614903927 - 0.9580836296 0.9591223001 0.9617106915 - 0.9583349228 0.9593678713 0.9619309902 - 0.9585862160 0.9596135020 0.9621512890 - 0.9588376284 0.9598590732 0.9623715878 - 0.9590889215 0.9600986838 0.9625920057 - 0.9593402743 0.9603298903 0.9628123045 - 0.9595916271 0.9605612159 0.9630326033 - 0.9598429203 0.9607924819 0.9632529020 - 0.9600886703 0.9610238075 0.9634733200 - 0.9603253007 0.9612550735 0.9636936188 - 0.9605618119 0.9614862800 0.9639139175 - 0.9607983232 0.9617176056 0.9641342163 - 0.9610348940 0.9619488716 0.9643545151 - 0.9612714052 0.9621801972 0.9645748734 - 0.9615079761 0.9624115229 0.9647951722 - 0.9617444873 0.9626427293 0.9650154710 - 0.9619809985 0.9628739953 0.9652358294 - 0.9622176290 0.9631053209 0.9654561877 - 0.9624540806 0.9633365870 0.9656764865 - 0.9626907110 0.9635679126 0.9658967853 - 0.9629272223 0.9637991190 0.9661170840 - 0.9631636739 0.9640303850 0.9663373828 - 0.9634003043 0.9642617106 0.9665578008 - 0.9636368155 0.9644929767 0.9667780995 - 0.9638733864 0.9647243023 0.9669983983 - 0.9641098976 0.9649556279 0.9672186971 - 0.9643464088 0.9651867747 0.9674391150 - 0.9645829797 0.9654181004 0.9676594138 - 0.9648194909 0.9656494260 0.9678797126 - 0.9650561213 0.9658806920 0.9681000113 - 0.9652925730 0.9661120176 0.9683203101 - 0.9655292034 0.9663432240 0.9685407281 - 0.9657657146 0.9665744901 0.9687610269 - 0.9660022259 0.9668058157 0.9689813256 - 0.9662387967 0.9670370817 0.9692016244 - 0.9664753079 0.9672684073 0.9694219232 - 0.9667118788 0.9674996138 0.9696422815 - 0.9669483900 0.9677308798 0.9698625803 - 0.9671849012 0.9679622054 0.9700775743 - 0.9674214721 0.9681934714 0.9702839255 - 0.9676579833 0.9684247971 0.9704902172 - 0.9678946137 0.9686561227 0.9706965089 - 0.9681311250 0.9688873291 0.9709028006 - 0.9683675766 0.9691185951 0.9711090922 - 0.9686042070 0.9693499207 0.9713153839 - 0.9688407183 0.9695811868 0.9715216756 - 0.9690772891 0.9698125124 0.9717280269 - 0.9693138003 0.9700412154 0.9719343185 - 0.9695503116 0.9702590704 0.9721406102 - 0.9697868824 0.9704769850 0.9723469019 - 0.9700220823 0.9706948996 0.9725531936 - 0.9702448249 0.9709128141 0.9727594852 - 0.9704675078 0.9711307287 0.9729657769 - 0.9706903100 0.9713485837 0.9731721282 - 0.9709129930 0.9715664983 0.9733784199 - 0.9711356759 0.9717844129 0.9735847116 - 0.9713584781 0.9720023274 0.9737908840 - 0.9715812206 0.9722201824 0.9739971757 - 0.9718039036 0.9724380970 0.9742035270 - 0.9720267057 0.9726560116 0.9744098186 - 0.9722493887 0.9728739262 0.9746161103 - 0.9724720716 0.9730917811 0.9748224020 - 0.9726948738 0.9733096957 0.9750286937 - 0.9729176164 0.9735276103 0.9752349854 - 0.9731402993 0.9737455845 0.9754412770 - 0.9733631015 0.9739634991 0.9756476283 - 0.9735857844 0.9741814137 0.9758539200 - 0.9738085270 0.9743993282 0.9760602117 - 0.9740313292 0.9746171832 0.9762665033 - 0.9742540121 0.9748350978 0.9764727950 - 0.9744768143 0.9750530124 0.9766790867 - 0.9746994972 0.9752709270 0.9768853784 - 0.9749221802 0.9754887819 0.9770917296 - 0.9751449823 0.9757066965 0.9772980213 - 0.9753677249 0.9759246111 0.9775043130 - 0.9755904078 0.9761425257 0.9777106047 - 0.9758132100 0.9763603806 0.9779168963 - 0.9760358930 0.9765782952 0.9781231284 - 0.9762585759 0.9767962098 0.9783294201 - 0.9764813781 0.9770141244 0.9785357118 - 0.9767041206 0.9772319794 0.9787420034 - 0.9769268036 0.9774498940 0.9789482951 - 0.9771496058 0.9776678085 0.9791545868 - 0.9773722887 0.9778857231 0.9793608785 - 0.9775949717 0.9781035781 0.9795672297 - 0.9778177738 0.9783214927 0.9797735214 - 0.9780405164 0.9785394073 0.9799798131 - 0.9782631993 0.9787573218 0.9801744223 - 0.9784860015 0.9789751768 0.9803677201 - 0.9787086844 0.9791930914 0.9805610180 - 0.9789314270 0.9794110060 0.9807543159 - 0.9791542292 0.9796289206 0.9809476137 - 0.9793769121 0.9798467755 0.9811409116 - 0.9795997143 0.9800609946 0.9813342094 - 0.9798223972 0.9802663922 0.9815275073 - 0.9800425172 0.9804717898 0.9817208052 - 0.9802523851 0.9806771874 0.9819141030 - 0.9804621935 0.9808827043 0.9821074009 - 0.9806721210 0.9810881019 0.9823006988 - 0.9808819294 0.9812934995 0.9824939966 - 0.9810917974 0.9814988971 0.9826872945 - 0.9813016057 0.9817042947 0.9828805923 - 0.9815114737 0.9819096923 0.9830738902 - 0.9817212820 0.9821150899 0.9832671881 - 0.9819312096 0.9823204875 0.9834604859 - 0.9821410179 0.9825258851 0.9836537838 - 0.9823508859 0.9827314019 0.9838470817 - 0.9825606942 0.9829367995 0.9840403795 - 0.9827706218 0.9831421971 0.9842336774 - 0.9829803705 0.9833475947 0.9844269753 - 0.9831902981 0.9835529923 0.9846202731 - 0.9834001064 0.9837583899 0.9848135710 - 0.9836099744 0.9839637876 0.9850069284 - 0.9838197827 0.9841691852 0.9852002263 - 0.9840297103 0.9843745828 0.9853935242 - 0.9842395186 0.9845800996 0.9855868220 - 0.9844493866 0.9847854972 0.9857801199 - 0.9846591949 0.9849908948 0.9859734178 - 0.9848691225 0.9851962924 0.9861667156 - 0.9850788713 0.9854016900 0.9863600135 - 0.9852887988 0.9856070876 0.9865533113 - 0.9854986072 0.9858124852 0.9867466092 - 0.9857084751 0.9860178828 0.9869399071 - 0.9859182835 0.9862232804 0.9871332049 - 0.9861282110 0.9864287972 0.9873265028 - 0.9863380194 0.9866341949 0.9875198007 - 0.9865478873 0.9868395925 0.9877130985 - 0.9867578149 0.9870449901 0.9879063964 - 0.9869676232 0.9872503877 0.9880996943 - 0.9871774912 0.9874557853 0.9882929921 - 0.9873872995 0.9876611829 0.9884862900 - 0.9875972271 0.9878665805 0.9886795878 - 0.9878069758 0.9880719781 0.9888730049 - 0.9880169034 0.9882774949 0.9890663028 - 0.9882267118 0.9884828925 0.9892596006 - 0.9884365797 0.9886882901 0.9894528985 - 0.9886463881 0.9888936877 0.9896461964 - 0.9888563156 0.9890990853 0.9898394942 - 0.9890661240 0.9893044829 0.9900307059 - 0.9892759919 0.9895098805 0.9902120233 - 0.9894858003 0.9897152781 0.9903932214 - 0.9896957278 0.9899206758 0.9905744791 - 0.9899054766 0.9901189804 0.9907557964 - 0.9901087880 0.9903126955 0.9909369946 - 0.9903066158 0.9905064702 0.9911183119 - 0.9905043840 0.9907001853 0.9912995100 - 0.9907022119 0.9908940196 0.9914808273 - 0.9909000993 0.9910876751 0.9916620851 - 0.9910979271 0.9912815094 0.9918432832 - 0.9912956953 0.9914752245 0.9920246005 - 0.9914935231 0.9916689992 0.9922057986 - 0.9916914105 0.9918627143 0.9923871160 - 0.9918891788 0.9920564294 0.9925683737 - 0.9920870066 0.9922502041 0.9927495718 - 0.9922847748 0.9924439192 0.9929308891 - 0.9924827218 0.9926376939 0.9931120872 - 0.9926804900 0.9928314090 0.9932934046 - 0.9928783178 0.9930251837 0.9934747219 - 0.9930760860 0.9932188988 0.9936559200 - 0.9932739735 0.9934126735 0.9938371778 - 0.9934718013 0.9936063886 0.9940183759 - 0.9936696291 0.9938002229 0.9941996932 - 0.9938673973 0.9939938784 0.9943810105 - 0.9940652847 0.9941875935 0.9945622087 - 0.9942631125 0.9943814278 0.9947435260 - 0.9944608808 0.9945750833 0.9949247241 - 0.9946587086 0.9947689176 0.9951059818 - 0.9948565960 0.9949625731 0.9952871799 - 0.9950544238 0.9951564074 0.9954684973 - 0.9952521920 0.9953501225 0.9956498146 - 0.9954500198 0.9955438972 0.9958310127 - 0.9956479073 0.9957376122 0.9960122705 - 0.9958456755 0.9959313273 0.9961935282 - 0.9960435033 0.9961251020 0.9963747859 - 0.9962412715 0.9963188171 0.9965561032 - 0.9964392185 0.9965125918 0.9967373013 - 0.9966369867 0.9967063069 0.9969186187 - 0.9968348145 0.9969000816 0.9970998168 - 0.9970325828 0.9970937967 0.9972810745 - 0.9972305298 0.9972875714 0.9974623919 - 0.9974282980 0.9974812865 0.9976435900 - 0.9976261258 0.9976751208 0.9978249073 - 0.9978238940 0.9978687763 0.9980061054 - 0.9980217814 0.9980624914 0.9981874228 - 0.9982196093 0.9982563257 0.9983686805 - 0.9984173775 0.9984499812 0.9985498786 - 0.9986152053 0.9986438155 0.9987311959 - 0.9988130927 0.9988374710 0.9989123940 - 0.9990109205 0.9990313053 0.9990937114 - 0.9992086887 0.9992250204 0.9992750287 - 0.9994065166 0.9994187951 0.9994562268 - 0.9996044040 0.9996125102 0.9996374846 - 0.9998021722 0.9998062849 0.9998186827 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0033556900 0.0036243461 0.0021771761 + 0.0067113810 0.0072486931 0.0043543512 + 0.0100719603 0.0109450798 0.0065315268 + 0.0136722000 0.0148685100 0.0087087033 + 0.0172724500 0.0187919401 0.0109967999 + 0.0209288709 0.0229167007 0.0134465704 + 0.0247608796 0.0271310192 0.0158963501 + 0.0285928901 0.0314321816 0.0183461197 + 0.0325581394 0.0359185003 0.0208943095 + 0.0366007015 0.0404266603 0.0236469992 + 0.0406721011 0.0451549515 0.0263996907 + 0.0448958799 0.0498832315 0.0291523803 + 0.0491196699 0.0548080616 0.0321348682 + 0.0534582287 0.0597378612 0.0352196097 + 0.0578269996 0.0648122802 0.0383043587 + 0.0622479804 0.0698948205 0.0415499993 + 0.0667206198 0.0750741214 0.0449920595 + 0.0712093785 0.0802576020 0.0484341197 + 0.0757424608 0.0854823887 0.0520806797 + 0.0802765936 0.0907058865 0.0558979213 + 0.0848271400 0.0959210992 0.0597151592 + 0.0893776789 0.1011238992 0.0638853684 + 0.0939087570 0.1062820032 0.0680840537 + 0.0984367505 0.1114128977 0.0724848807 + 0.1029269993 0.1164738983 0.0770553872 + 0.1073973030 0.1214959025 0.0817479491 + 0.1118313000 0.1264283061 0.0866615325 + 0.1162149012 0.1313190013 0.0916693583 + 0.1205834970 0.1361002028 0.0968769416 + 0.1248577982 0.1408507973 0.1021751985 + 0.1291321069 0.1454659998 0.1076091975 + 0.1333063990 0.1500781029 0.1131248996 + 0.1374552995 0.1545193046 0.1187048033 + 0.1415515989 0.1589604020 0.1243304983 + 0.1455647051 0.1632665992 0.1299701035 + 0.1495777071 0.1675314009 0.1355862021 + 0.1534643024 0.1717227995 0.1411814988 + 0.1573361009 0.1758131981 0.1467006058 + 0.1611633003 0.1799037009 0.1521570981 + 0.1648924053 0.1838286966 0.1575206071 + 0.1686214954 0.1877496988 0.1627777070 + 0.1722615957 0.1916016042 0.1679431945 + 0.1758497059 0.1953604072 0.1729734987 + 0.1794376969 0.1991191953 0.1779143065 + 0.1829102039 0.2027603984 0.1827175021 + 0.1863611937 0.2063656002 0.1874202043 + 0.1898120940 0.2099706978 0.1920142025 + 0.1931387931 0.2134328038 0.1964761019 + 0.1964582950 0.2168937027 0.2008883059 + 0.1997777969 0.2203408033 0.2051143050 + 0.2029809058 0.2236669958 0.2093403041 + 0.2061755955 0.2269932926 0.2133757025 + 0.2093703002 0.2303075939 0.2173759937 + 0.2124706954 0.2335088998 0.2213031054 + 0.2155478001 0.2367102057 0.2250909060 + 0.2186249942 0.2399114966 0.2288787067 + 0.2216414064 0.2430005968 0.2325274944 + 0.2246086001 0.2460864037 0.2361178994 + 0.2275757939 0.2491723001 0.2397083044 + 0.2305241972 0.2521803081 0.2431315929 + 0.2333890945 0.2551597953 0.2465402037 + 0.2362540066 0.2581393123 0.2499487996 + 0.2391189039 0.2610821128 0.2531935871 + 0.2419181019 0.2639639974 0.2564358115 + 0.2446880937 0.2668457925 0.2596780956 + 0.2474582046 0.2697275877 0.2627840936 + 0.2502210140 0.2725282907 0.2658750117 + 0.2529034913 0.2753205895 0.2689658999 + 0.2555859983 0.2781128883 0.2719655037 + 0.2582685053 0.2808786035 0.2749192119 + 0.2609224021 0.2835890055 0.2778730094 + 0.2635244131 0.2862994075 0.2807919979 + 0.2661263049 0.2890098095 0.2836216092 + 0.2687281966 0.2916727960 0.2864513099 + 0.2712922990 0.2943083942 0.2892810106 + 0.2738203108 0.2969441116 0.2920272052 + 0.2763482928 0.2995797098 0.2947449982 + 0.2788763046 0.3021580875 0.2974627912 + 0.2813667059 0.3047255874 0.3001739085 + 0.2838270068 0.3072930872 0.3027910888 + 0.2862873077 0.3098605871 0.3054082096 + 0.2887476087 0.3123694956 0.3080253005 + 0.2911775112 0.3148748875 0.3106203079 + 0.2935760021 0.3173803985 0.3131470084 + 0.2959744036 0.3198859096 0.3156735897 + 0.2983728945 0.3223375976 0.3182002902 + 0.3007532060 0.3247866929 0.3207036853 + 0.3030953109 0.3272357881 0.3231492937 + 0.3054375052 0.3296849132 0.3255949020 + 0.3077796102 0.3320893943 0.3280405104 + 0.3101190925 0.3344874084 0.3304716051 + 0.3124099970 0.3368853033 0.3328445852 + 0.3147009015 0.3392831981 0.3352177143 + 0.3169918060 0.3416486979 0.3375906944 + 0.3192827106 0.3440001905 0.3399637043 + 0.3215416968 0.3463517129 0.3422729075 + 0.3237861097 0.3487032950 0.3445810974 + 0.3260306120 0.3510360122 0.3468891978 + 0.3282749951 0.3533455133 0.3491973877 + 0.3305096924 0.3556550145 0.3514679968 + 0.3327121139 0.3579646051 0.3537184000 + 0.3349145055 0.3602696061 0.3559688032 + 0.3371168971 0.3625411093 0.3582192957 + 0.3393192887 0.3648126125 0.3604589999 + 0.3414955139 0.3670840859 0.3626582026 + 0.3436599970 0.3693554997 0.3648572862 + 0.3458243906 0.3716023862 0.3670564890 + 0.3479889035 0.3738394976 0.3692556024 + 0.3501509130 0.3760766089 0.3714247048 + 0.3522812128 0.3783136904 0.3735783994 + 0.3544115126 0.3805431128 0.3757320940 + 0.3565418124 0.3827491105 0.3778859079 + 0.3586719930 0.3849549890 0.3800387979 + 0.3607907891 0.3871609867 0.3821524978 + 0.3628903031 0.3893668950 0.3842661977 + 0.3649899065 0.3915528059 0.3863798082 + 0.3670895100 0.3937306106 0.3884935081 + 0.3691891134 0.3959082961 0.3905969858 + 0.3712717891 0.3980861008 0.3926754892 + 0.3733437955 0.4002606869 0.3947539926 + 0.3754158914 0.4024128914 0.3968324065 + 0.3774879873 0.4045650959 0.3989109099 + 0.3795599937 0.4067172110 0.4009746909 + 0.3816126883 0.4088693857 0.4030224979 + 0.3836601973 0.4110105038 0.4050701857 + 0.3857075870 0.4131393135 0.4071179032 + 0.3877550960 0.4152680933 0.4091657102 + 0.3898024857 0.4173969030 0.4111976027 + 0.3918300867 0.4195258021 0.4132185876 + 0.3938556015 0.4216380119 0.4152396023 + 0.3958810866 0.4237453938 0.4172605872 + 0.3979066014 0.4258528054 0.4192816019 + 0.3999319971 0.4279601872 0.4212876856 + 0.4019384980 0.4300670028 0.4232856929 + 0.4039444029 0.4321546853 0.4252836108 + 0.4059503078 0.4342423081 0.4272815883 + 0.4079560935 0.4363299906 0.4292795062 + 0.4099619985 0.4384176135 0.4312647879 + 0.4119505882 0.4405007958 0.4332428873 + 0.4139389098 0.4425699115 0.4352209866 + 0.4159272015 0.4446390867 0.4371992052 + 0.4179156125 0.4467082024 0.4391773045 + 0.4199039042 0.4487774074 0.4411455989 + 0.4218772054 0.4508393109 0.4431068003 + 0.4238497913 0.4528909028 0.4450680017 + 0.4258224070 0.4549424946 0.4470292926 + 0.4277949929 0.4569939971 0.4489904940 + 0.4297676086 0.4590455890 0.4509448111 + 0.4317277074 0.4610880911 0.4528915882 + 0.4336861074 0.4631226957 0.4548385143 + 0.4356445074 0.4651573002 0.4567854106 + 0.4376029074 0.4671919048 0.4587321877 + 0.4395613074 0.4692265093 0.4606749117 + 0.4415096939 0.4712507129 0.4626095891 + 0.4434551895 0.4732685983 0.4645442963 + 0.4454007149 0.4752865136 0.4664790034 + 0.4473460913 0.4773043096 0.4684137106 + 0.4492915869 0.4793221951 0.4703465104 + 0.4512295127 0.4813289046 0.4722707868 + 0.4531629980 0.4833298922 0.4741952121 + 0.4550965130 0.4853309095 0.4761194885 + 0.4570299983 0.4873319864 0.4780437946 + 0.4589635134 0.4893330038 0.4799681902 + 0.4608916938 0.4913224876 0.4818837047 + 0.4628139138 0.4933061898 0.4837990999 + 0.4647361040 0.4952898920 0.4857144952 + 0.4666582942 0.4972735941 0.4876298904 + 0.4685805142 0.4992572963 0.4895452857 + 0.4704998136 0.5012295842 0.4914546907 + 0.4724110067 0.5031952262 0.4933620989 + 0.4743221998 0.5051606894 0.4952695966 + 0.4762333930 0.5071262717 0.4971770942 + 0.4781445861 0.5090917945 0.4990845919 + 0.4800555110 0.5110468864 0.5009883046 + 0.4819557965 0.5129930973 0.5028885007 + 0.4838559926 0.5149393082 0.5047886968 + 0.4857563078 0.5168855190 0.5066888928 + 0.4876565039 0.5188316703 0.5085890889 + 0.4895567894 0.5207694769 0.5104873776 + 0.4914484024 0.5226947069 0.5123804808 + 0.4933373034 0.5246199965 0.5142735243 + 0.4952262938 0.5265452862 0.5161666274 + 0.4971151948 0.5284705758 0.5180596113 + 0.4990041852 0.5303910971 0.5199525952 + 0.5008875132 0.5322936177 0.5218384266 + 0.5027645230 0.5341960788 0.5237240195 + 0.5046414733 0.5360984802 0.5256096125 + 0.5065184832 0.5380010009 0.5274953246 + 0.5083954930 0.5399035215 0.5293809175 + 0.5102707148 0.5417822003 0.5312609076 + 0.5121346712 0.5436596274 0.5331382751 + 0.5139986873 0.5455371141 0.5350157022 + 0.5158627033 0.5474146008 0.5368930101 + 0.5177267790 0.5492920280 0.5387703776 + 0.5195907950 0.5511524081 0.5406445265 + 0.5214436054 0.5530024171 0.5425122976 + 0.5232933164 0.5548523068 0.5443801284 + 0.5251430273 0.5567023158 0.5462480187 + 0.5269926786 0.5585523248 0.5481157899 + 0.5288422704 0.5603957176 0.5499836206 + 0.5306860209 0.5622155070 0.5518401861 + 0.5325195789 0.5640351772 0.5536966920 + 0.5343531966 0.5658550262 0.5555531979 + 0.5361868143 0.5676748157 0.5574097037 + 0.5380203724 0.5694946051 0.5592660904 + 0.5398539901 0.5712903738 0.5611143112 + 0.5416709185 0.5730770826 0.5629572272 + 0.5434862971 0.5748637915 0.5648000240 + 0.5453017950 0.5766503811 0.5666428208 + 0.5471172929 0.5784370899 0.5684856176 + 0.5489326715 0.5802192092 0.5703253746 + 0.5507397056 0.5819697976 0.5721517205 + 0.5525346994 0.5837203264 0.5739780068 + 0.5543295741 0.5854709148 0.5758042932 + 0.5561246276 0.5872213840 0.5776305795 + 0.5579195023 0.5889719725 0.5794569254 + 0.5597143769 0.5907062888 0.5812692046 + 0.5614898801 0.5924177170 0.5830757022 + 0.5632616878 0.5941290259 0.5848821998 + 0.5650333762 0.5958402753 0.5866885781 + 0.5668051839 0.5975517035 0.5884950757 + 0.5685769916 0.5992630124 0.5902976990 + 0.5703436136 0.6009503007 0.5920805931 + 0.5720893145 0.6026192904 0.5938634872 + 0.5738348961 0.6042883992 0.5956465006 + 0.5755805969 0.6059575081 0.5974293947 + 0.5773262978 0.6076264977 0.5992122889 + 0.5790719986 0.6092956066 0.6009799242 + 0.5808039904 0.6109384894 0.6027352214 + 0.5825204849 0.6125624180 0.6044905186 + 0.5842369795 0.6141862273 0.6062458158 + 0.5859534740 0.6158099771 0.6080011725 + 0.5876700282 0.6174339056 0.6097565293 + 0.5893865228 0.6190577149 0.6114842892 + 0.5910822153 0.6206613779 0.6132075787 + 0.5927662849 0.6222372055 0.6149309874 + 0.5944504142 0.6238129735 0.6166543961 + 0.5961344838 0.6253888011 0.6183778048 + 0.5978186131 0.6269646287 0.6200990081 + 0.5995026827 0.6285403967 0.6217858791 + 0.6011615992 0.6301125288 0.6234729290 + 0.6028100848 0.6316378117 0.6251598001 + 0.6044585109 0.6331629753 0.6268466711 + 0.6061068773 0.6346881986 0.6285337210 + 0.6077553034 0.6362134814 0.6302152276 + 0.6094037294 0.6377387047 0.6318612099 + 0.6110271811 0.6392639279 0.6335071921 + 0.6126366854 0.6407619119 0.6351531744 + 0.6142461896 0.6422343254 0.6367992163 + 0.6158555746 0.6437066793 0.6384451985 + 0.6174650788 0.6451790929 0.6400886178 + 0.6190745234 0.6466515064 0.6416891813 + 0.6206660867 0.6481239796 0.6432899237 + 0.6222333908 0.6495963931 0.6448904872 + 0.6238006949 0.6510291100 0.6464911103 + 0.6253681183 0.6524468064 0.6480916739 + 0.6269354224 0.6538646221 0.6496922970 + 0.6285027266 0.6552823186 0.6512529254 + 0.6300681233 0.6567000151 0.6528040171 + 0.6315903068 0.6581177115 0.6543551087 + 0.6331124902 0.6595354080 0.6559062004 + 0.6346346736 0.6609153748 0.6574572921 + 0.6361569166 0.6622769237 0.6590083838 + 0.6376791000 0.6636384726 0.6605402231 + 0.6392014027 0.6650000215 0.6620379090 + 0.6407008171 0.6663615108 0.6635357141 + 0.6421751976 0.6677231193 0.6650334001 + 0.6436495185 0.6690846086 0.6665312052 + 0.6451238990 0.6704273820 0.6680288911 + 0.6465982199 0.6717317104 0.6695266962 + 0.6480726004 0.6730359793 0.6709856987 + 0.6495469213 0.6743403077 0.6724267006 + 0.6509864926 0.6756445765 0.6738678217 + 0.6524105072 0.6769489050 0.6753088832 + 0.6538345814 0.6782531738 0.6767500043 + 0.6552587152 0.6795575023 0.6781910062 + 0.6566827297 0.6808236241 0.6796321273 + 0.6581068039 0.6820700169 0.6810289025 + 0.6595308781 0.6833164096 0.6824105978 + 0.6609197855 0.6845629215 0.6837922931 + 0.6622915864 0.6858093143 0.6851739287 + 0.6636633277 0.6870558262 0.6865556240 + 0.6650350094 0.6883022189 0.6879372001 + 0.6664068103 0.6895486116 0.6893188953 + 0.6677784920 0.6907581091 0.6906694174 + 0.6691501737 0.6919463873 0.6919894814 + 0.6705014110 0.6931347847 0.6933096051 + 0.6718192101 0.6943231821 0.6946297288 + 0.6731368899 0.6955115795 0.6959499121 + 0.6744546890 0.6966999769 0.6972699761 + 0.6757724285 0.6978883743 0.6985900998 + 0.6770902276 0.6990767717 0.6999102235 + 0.6784079075 0.7002522945 0.7011716962 + 0.6797257066 0.7013828754 0.7024288177 + 0.6809998155 0.7025135159 0.7036859989 + 0.6822623014 0.7036440969 0.7049431205 + 0.6835249066 0.7047746778 0.7062003016 + 0.6847875118 0.7059053183 0.7074574232 + 0.6860501170 0.7070358992 0.7087146044 + 0.6873127222 0.7081664801 0.7099717259 + 0.6885753274 0.7092970014 0.7111666203 + 0.6898378730 0.7104060054 0.7123600245 + 0.6910517216 0.7114794254 0.7135534286 + 0.6922584176 0.7125527859 0.7147467732 + 0.6934651136 0.7136262059 0.7159401774 + 0.6946716905 0.7146996260 0.7171337008 + 0.6958783865 0.7157731056 0.7183271050 + 0.6970850825 0.7168465257 0.7195205092 + 0.6982917786 0.7179198861 0.7206757069 + 0.6994984150 0.7189933062 0.7218052745 + 0.7006723285 0.7200632095 0.7229349017 + 0.7018228173 0.7210804224 0.7240644097 + 0.7029731870 0.7220975757 0.7251939774 + 0.7041236758 0.7231147885 0.7263236046 + 0.7052742243 0.7241321206 0.7274531722 + 0.7064247131 0.7251492739 0.7285827994 + 0.7075752020 0.7261664867 0.7297123075 + 0.7087255716 0.7271836996 0.7307947278 + 0.7098761201 0.7282009125 0.7318609953 + 0.7109766006 0.7292181253 0.7329272032 + 0.7120710015 0.7302225828 0.7339934707 + 0.7131654024 0.7311848998 0.7350596786 + 0.7142598033 0.7321472168 0.7361260056 + 0.7153543234 0.7331095934 0.7371922731 + 0.7164487243 0.7340719104 0.7382584810 + 0.7175431252 0.7350342274 0.7393248081 + 0.7186375260 0.7359964848 0.7403681874 + 0.7197319865 0.7369588017 0.7413722277 + 0.7207844853 0.7379211187 0.7423762083 + 0.7218233943 0.7388833761 0.7433801889 + 0.7228623033 0.7398456931 0.7443842292 + 0.7239012122 0.7407631874 0.7453882098 + 0.7249401212 0.7416722178 0.7463921905 + 0.7259790897 0.7425811887 0.7473962903 + 0.7270179987 0.7434902191 0.7484002709 + 0.7280569077 0.7443990707 0.7494043112 + 0.7290958166 0.7453081012 0.7503836155 + 0.7301275730 0.7462170720 0.7513269186 + 0.7311120033 0.7471259832 0.7522702217 + 0.7320963144 0.7480350137 0.7532135248 + 0.7330806255 0.7489439845 0.7541568279 + 0.7340649962 0.7498528957 0.7551000714 + 0.7350493073 0.7507187128 0.7560433745 + 0.7360336185 0.7515761256 0.7569866776 + 0.7370179296 0.7524334788 0.7579299808 + 0.7380023003 0.7532908916 0.7588732839 + 0.7389866114 0.7541484237 0.7598165870 + 0.7399709225 0.7550057769 0.7607126236 + 0.7409034967 0.7558631897 0.7615972161 + 0.7418344021 0.7567206025 0.7624816895 + 0.7427654266 0.7575780153 0.7633662820 + 0.7436963916 0.7584354281 0.7642508149 + 0.7446272969 0.7592927814 0.7651354074 + 0.7455583215 0.7601416111 0.7660198808 + 0.7464892864 0.7609493732 0.7669044733 + 0.7474203110 0.7617571950 0.7677890062 + 0.7483512163 0.7625650167 0.7686735988 + 0.7492821813 0.7633727789 0.7695580721 + 0.7502012849 0.7641806006 0.7704144120 + 0.7510803938 0.7649884224 0.7712424994 + 0.7519595027 0.7657961845 0.7720705271 + 0.7528386712 0.7666041255 0.7728986144 + 0.7537177801 0.7674118876 0.7737267017 + 0.7545968890 0.7682197094 0.7745547295 + 0.7554759979 0.7690274715 0.7753828168 + 0.7563552260 0.7698352933 0.7762109041 + 0.7572342753 0.7706053257 0.7770388722 + 0.7581133842 0.7713655829 0.7778670192 + 0.7589924932 0.7721258998 0.7786949873 + 0.7598717213 0.7728862166 0.7795230746 + 0.7607079744 0.7736464739 0.7803282738 + 0.7615370154 0.7744067907 0.7811022997 + 0.7623659968 0.7751671076 0.7818763852 + 0.7631949782 0.7759273052 0.7826504707 + 0.7640240192 0.7766876221 0.7834246159 + 0.7648531199 0.7774478793 0.7841985822 + 0.7656821012 0.7782081962 0.7849727273 + 0.7665110826 0.7789685130 0.7857468128 + 0.7673401237 0.7797287703 0.7865208983 + 0.7681691051 0.7804598808 0.7872949243 + 0.7689980865 0.7811748981 0.7880690098 + 0.7698271275 0.7818897963 0.7888430953 + 0.7706180215 0.7826048136 0.7896171212 + 0.7713987827 0.7833197117 0.7903652787 + 0.7721796036 0.7840347290 0.7910879850 + 0.7729604244 0.7847496271 0.7918108106 + 0.7737411857 0.7854645848 0.7925335169 + 0.7745220065 0.7861794829 0.7932562828 + 0.7753028274 0.7868943810 0.7939789891 + 0.7760835886 0.7876093984 0.7947018147 + 0.7768645287 0.7883242965 0.7954245210 + 0.7776452899 0.7890393138 0.7961472273 + 0.7784261107 0.7897542119 0.7968699932 + 0.7792068720 0.7904409170 0.7975926995 + 0.7799876928 0.7911127210 0.7983155251 + 0.7807230949 0.7917845249 0.7990381718 + 0.7814577222 0.7924563289 0.7997609973 + 0.7821922898 0.7931280732 0.8004512191 + 0.7829269767 0.7937998772 0.8011254072 + 0.7836616039 0.7944718003 0.8017995954 + 0.7843962908 0.7951436043 0.8024737239 + 0.7851309180 0.7958154082 0.8031479120 + 0.7858656049 0.7964872122 0.8038221002 + 0.7866001725 0.7971590161 0.8044962883 + 0.7873347998 0.7978308201 0.8051704168 + 0.7880694866 0.7985026836 0.8058446050 + 0.7888041139 0.7991744876 0.8065187931 + 0.7895388007 0.7998462915 0.8071929812 + 0.7902569771 0.8004866242 0.8078672290 + 0.7909476161 0.8011174798 0.8085412979 + 0.7916381955 0.8017483950 0.8092154860 + 0.7923287749 0.8023793101 0.8098896742 + 0.7930194139 0.8030102253 0.8105255961 + 0.7937099934 0.8036412001 0.8111540079 + 0.7944005728 0.8042721152 0.8117824197 + 0.7950912118 0.8049029708 0.8124107718 + 0.7957817912 0.8055338860 0.8130391836 + 0.7964723706 0.8061649203 0.8136675954 + 0.7971630096 0.8067957759 0.8142960072 + 0.7978535891 0.8074266911 0.8149244189 + 0.7985442281 0.8080576062 0.8155527711 + 0.7992348075 0.8086885214 0.8161811829 + 0.7999253869 0.8093194962 0.8168095946 + 0.8005787134 0.8099504113 0.8174380064 + 0.8012273908 0.8105456829 0.8180664182 + 0.8018761873 0.8111379147 0.8186947703 + 0.8025249243 0.8117302060 0.8193231821 + 0.8031736016 0.8123223782 0.8199515939 + 0.8038223982 0.8129147291 0.8205404282 + 0.8044710755 0.8135069013 0.8211258054 + 0.8051198125 0.8140991926 0.8217111826 + 0.8057686090 0.8146914244 0.8222966194 + 0.8064172864 0.8152837157 0.8228819966 + 0.8070660233 0.8158758879 0.8234674931 + 0.8077148199 0.8164681792 0.8240528703 + 0.8083634973 0.8170604110 0.8246383071 + 0.8090121746 0.8176527023 0.8252236843 + 0.8096609712 0.8182448745 0.8258091211 + 0.8102908134 0.8188372254 0.8263946176 + 0.8108997941 0.8194293976 0.8269799948 + 0.8115088940 0.8200203776 0.8275653720 + 0.8121178746 0.8205760717 0.8281508088 + 0.8127269745 0.8211318851 0.8287361860 + 0.8133360147 0.8216876984 0.8293216825 + 0.8139451146 0.8222435117 0.8299071193 + 0.8145542145 0.8227992058 0.8304585814 + 0.8151631951 0.8233550191 0.8310037851 + 0.8157722950 0.8239107728 0.8315489888 + 0.8163812757 0.8244665265 0.8320941925 + 0.8169903755 0.8250222802 0.8326392770 + 0.8175994158 0.8255780935 0.8331844807 + 0.8182085156 0.8261339068 0.8337296844 + 0.8188176155 0.8266896009 0.8342748880 + 0.8194265962 0.8272454143 0.8348199725 + 0.8200334907 0.8278012276 0.8353651762 + 0.8206049800 0.8283569217 0.8359103799 + 0.8211765885 0.8289126754 0.8364555836 + 0.8217481971 0.8294684887 0.8370007277 + 0.8223196864 0.8300228119 0.8375458717 + 0.8228912950 0.8305441737 0.8380910754 + 0.8234627843 0.8310655951 0.8386362791 + 0.8240343928 0.8315870166 0.8391814232 + 0.8246060014 0.8321084976 0.8397266269 + 0.8251774907 0.8326299191 0.8402529955 + 0.8257490993 0.8331512809 0.8407605886 + 0.8263205886 0.8336728215 0.8412681818 + 0.8268921971 0.8341941833 0.8417757750 + 0.8274638057 0.8347156048 0.8422834277 + 0.8280352950 0.8352370858 0.8427910209 + 0.8286069036 0.8357585073 0.8432986140 + 0.8291783929 0.8362799287 0.8438062072 + 0.8297500014 0.8368014097 0.8443138003 + 0.8303017020 0.8373227715 0.8448213935 + 0.8308379054 0.8378441930 0.8453289866 + 0.8313741088 0.8383656144 0.8458365798 + 0.8319103122 0.8388870955 0.8463441730 + 0.8324465156 0.8394085169 0.8468517065 + 0.8329827189 0.8399298787 0.8473592997 + 0.8335189223 0.8404234052 0.8478668928 + 0.8340551257 0.8409125805 0.8483744860 + 0.8345913291 0.8414016962 0.8488820791 + 0.8351274729 0.8418908715 0.8493896723 + 0.8356636763 0.8423801064 0.8498973250 + 0.8361998796 0.8428692222 0.8503770232 + 0.8367360830 0.8433583975 0.8508495092 + 0.8372722864 0.8438475132 0.8513221145 + 0.8378084898 0.8443366885 0.8517947197 + 0.8383446932 0.8448258042 0.8522673249 + 0.8388810158 0.8453149796 0.8527398109 + 0.8394172192 0.8458042145 0.8532124162 + 0.8399534225 0.8462932706 0.8536850214 + 0.8404592276 0.8467825055 0.8541575074 + 0.8409621119 0.8472716212 0.8546301126 + 0.8414651155 0.8477607965 0.8551027179 + 0.8419679999 0.8482499719 0.8555753231 + 0.8424710035 0.8487390876 0.8560478091 + 0.8429738879 0.8492283225 0.8565204144 + 0.8434768915 0.8497173786 0.8569930196 + 0.8439797759 0.8501937985 0.8574656248 + 0.8444827199 0.8506526947 0.8579381108 + 0.8449857235 0.8511115909 0.8584107161 + 0.8454886079 0.8515704274 0.8588833213 + 0.8459916115 0.8520293236 0.8593558073 + 0.8464944959 0.8524882197 0.8598284125 + 0.8469973803 0.8529471159 0.8602802157 + 0.8475003839 0.8534058928 0.8607202172 + 0.8480033278 0.8538647890 0.8611602187 + 0.8485062718 0.8543236852 0.8616002202 + 0.8490092158 0.8547825813 0.8620402217 + 0.8495122194 0.8552414775 0.8624802232 + 0.8500142097 0.8557003140 0.8629202247 + 0.8504859209 0.8561592102 0.8633602262 + 0.8509575725 0.8566181064 0.8638002276 + 0.8514292836 0.8570770025 0.8642402291 + 0.8519009948 0.8575357795 0.8646801710 + 0.8523727059 0.8579946756 0.8651201725 + 0.8528444171 0.8584535718 0.8655601740 + 0.8533161283 0.8589125276 0.8660001755 + 0.8537877798 0.8593714237 0.8664401770 + 0.8542594910 0.8598302007 0.8668801785 + 0.8547312021 0.8602712154 0.8673201799 + 0.8552029133 0.8607016802 0.8677601814 + 0.8556746244 0.8611322045 0.8682001829 + 0.8561462760 0.8615627289 0.8686401844 + 0.8566179872 0.8619931936 0.8690801859 + 0.8570896983 0.8624237180 0.8695201874 + 0.8575614095 0.8628543019 0.8699601889 + 0.8580331206 0.8632848263 0.8703727126 + 0.8585047722 0.8637152910 0.8707824945 + 0.8589764833 0.8641458154 0.8711922169 + 0.8594481945 0.8645762801 0.8716019988 + 0.8599199057 0.8650068045 0.8720117211 + 0.8603671789 0.8654373288 0.8724213839 + 0.8608096838 0.8658677936 0.8728312254 + 0.8612521291 0.8662983179 0.8732408881 + 0.8616945148 0.8667287827 0.8736507297 + 0.8621369004 0.8671593070 0.8740603924 + 0.8625792861 0.8675897717 0.8744701743 + 0.8630216718 0.8680202961 0.8748798966 + 0.8634641171 0.8684508204 0.8752896786 + 0.8639065027 0.8688812852 0.8756994009 + 0.8643488884 0.8693118095 0.8761091828 + 0.8647912741 0.8697422743 0.8765189052 + 0.8652337193 0.8701621294 0.8769286275 + 0.8656761050 0.8705661297 0.8773384094 + 0.8661184907 0.8709700108 0.8777480721 + 0.8665608764 0.8713740110 0.8781579137 + 0.8670033216 0.8717778921 0.8785675764 + 0.8674457073 0.8721818924 0.8789774179 + 0.8678880930 0.8725857735 0.8793870807 + 0.8683304787 0.8729897141 0.8797969222 + 0.8687729239 0.8733937144 0.8801925182 + 0.8692153096 0.8737975955 0.8805741072 + 0.8696576953 0.8742015958 0.8809558153 + 0.8700938821 0.8746054769 0.8813375235 + 0.8705089092 0.8750094771 0.8817191720 + 0.8709238768 0.8754134178 0.8821008801 + 0.8713387847 0.8758172989 0.8824825287 + 0.8717538118 0.8762212992 0.8828641772 + 0.8721687198 0.8766251802 0.8832458854 + 0.8725836873 0.8770291805 0.8836275935 + 0.8729987144 0.8774331212 0.8840093017 + 0.8734136224 0.8778371215 0.8843908906 + 0.8738285899 0.8782410026 0.8847725987 + 0.8742436171 0.8786448836 0.8851543069 + 0.8746585250 0.8790488839 0.8855360150 + 0.8750734925 0.8794528246 0.8859177232 + 0.8754885197 0.8798568249 0.8862993121 + 0.8759034276 0.8802446723 0.8866810203 + 0.8763183951 0.8806238174 0.8870627284 + 0.8767334223 0.8810029030 0.8874443769 + 0.8771482706 0.8813819885 0.8878260255 + 0.8775632977 0.8817610741 0.8882076740 + 0.8779783249 0.8821402192 0.8885893822 + 0.8783931732 0.8825193048 0.8889710903 + 0.8788082004 0.8828983903 0.8893527985 + 0.8792232275 0.8832774758 0.8897343874 + 0.8796380758 0.8836566210 0.8901082277 + 0.8800498247 0.8840357065 0.8904638886 + 0.8804391026 0.8844147921 0.8908194900 + 0.8808283806 0.8847938776 0.8911752105 + 0.8812177181 0.8851730227 0.8915308714 + 0.8816069961 0.8855521083 0.8918864727 + 0.8819962740 0.8859311938 0.8922421932 + 0.8823856115 0.8863102794 0.8925979137 + 0.8827748895 0.8866894245 0.8929535151 + 0.8831642270 0.8870685101 0.8933091760 + 0.8835535049 0.8874475956 0.8936648965 + 0.8839427233 0.8878266811 0.8940204978 + 0.8843320012 0.8882058263 0.8943762183 + 0.8847212791 0.8885849118 0.8947318792 + 0.8851106167 0.8889639974 0.8950874805 + 0.8854998946 0.8893430829 0.8954432011 + 0.8858891726 0.8897222877 0.8957989216 + 0.8862785101 0.8900951147 0.8961545229 + 0.8866677880 0.8904510140 0.8965101838 + 0.8870571256 0.8908069134 0.8968659043 + 0.8874464035 0.8911628127 0.8972215056 + 0.8878356814 0.8915187120 0.8975772262 + 0.8882250190 0.8918746114 0.8979328871 + 0.8886142969 0.8922305107 0.8982884884 + 0.8890035748 0.8925864100 0.8986442089 + 0.8893929124 0.8929423094 0.8989998102 + 0.8897821903 0.8932980895 0.8993554711 + 0.8901609182 0.8936539888 0.8997111917 + 0.8905261755 0.8940098882 0.9000623226 + 0.8908914924 0.8943657875 0.9003939033 + 0.8912568092 0.8947216868 0.9007254839 + 0.8916221261 0.8950775862 0.9010570049 + 0.8919873834 0.8954334855 0.9013885856 + 0.8923527002 0.8957893848 0.9017202258 + 0.8927180171 0.8961452842 0.9020516872 + 0.8930832744 0.8965011239 0.9023833275 + 0.8934485912 0.8968570232 0.9027149081 + 0.8938139081 0.8972129226 0.9030464888 + 0.8941792250 0.8975688219 0.9033780098 + 0.8945444822 0.8979247212 0.9037095904 + 0.8949097991 0.8982806206 0.9040411711 + 0.8952751160 0.8986365199 0.9043726921 + 0.8956403732 0.8989924192 0.9047042727 + 0.8960056901 0.8993481994 0.9050359130 + 0.8963710070 0.8997040987 0.9053673744 + 0.8967363238 0.9000564218 0.9056990147 + 0.8971015811 0.9003906250 0.9060305953 + 0.8974668980 0.9007248282 0.9063621759 + 0.8978322148 0.9010589719 0.9066936970 + 0.8981974721 0.9013931751 0.9070252776 + 0.8985627890 0.9017273784 0.9073569179 + 0.8989281058 0.9020615816 0.9076883793 + 0.8992933035 0.9023957849 0.9080200195 + 0.8996586204 0.9027299881 0.9083516002 + 0.9000225067 0.9030641913 0.9086831808 + 0.9003654122 0.9033983946 0.9090147018 + 0.9007081985 0.9037325978 0.9093462825 + 0.9010511041 0.9040668011 0.9096779227 + 0.9013940096 0.9044010043 0.9100087881 + 0.9017369151 0.9047352076 0.9103180766 + 0.9020798206 0.9050694108 0.9106273055 + 0.9024227262 0.9054036140 0.9109365940 + 0.9027655125 0.9057378173 0.9112458825 + 0.9031084180 0.9060720801 0.9115551114 + 0.9034513235 0.9064062834 0.9118643999 + 0.9037942290 0.9067404866 0.9121736884 + 0.9041370749 0.9070746899 0.9124829173 + 0.9044799805 0.9074088931 0.9127922058 + 0.9048228264 0.9077430964 0.9131013751 + 0.9051656723 0.9080772996 0.9134107232 + 0.9055085778 0.9084115028 0.9137200117 + 0.9058514833 0.9087457061 0.9140291810 + 0.9061943889 0.9090799093 0.9143385291 + 0.9065371752 0.9094141126 0.9146478176 + 0.9068800807 0.9097483158 0.9149569869 + 0.9072229862 0.9100775123 0.9152662754 + 0.9075658917 0.9103915095 0.9155756235 + 0.9079087973 0.9107053876 0.9158847928 + 0.9082517028 0.9110193849 0.9161940813 + 0.9085944891 0.9113333821 0.9165033102 + 0.9089373946 0.9116473198 0.9168125987 + 0.9092803001 0.9119613171 0.9171218872 + 0.9096232057 0.9122751951 0.9174311161 + 0.9099661112 0.9125891924 0.9177404046 + 0.9102901220 0.9129031897 0.9180496931 + 0.9106121063 0.9132171273 0.9183589220 + 0.9109339714 0.9135311246 0.9186682105 + 0.9112560153 0.9138450027 0.9189774990 + 0.9115778804 0.9141589999 0.9192867279 + 0.9118999243 0.9144729972 0.9195960164 + 0.9122217894 0.9147868752 0.9199051857 + 0.9125437737 0.9151008725 0.9202002287 + 0.9128656983 0.9154148102 0.9204887748 + 0.9131876826 0.9157288074 0.9207773805 + 0.9135096073 0.9160428047 0.9210659862 + 0.9138315916 0.9163566828 0.9213547111 + 0.9141535759 0.9166706800 0.9216433167 + 0.9144755006 0.9169846177 0.9219319224 + 0.9147974849 0.9172986150 0.9222205281 + 0.9151194096 0.9176126122 0.9225090742 + 0.9154413939 0.9179264903 0.9227977991 + 0.9157633185 0.9182404876 0.9230864048 + 0.9160853028 0.9185544252 0.9233750105 + 0.9164072275 0.9188684225 0.9236636162 + 0.9167292118 0.9191824198 0.9239522219 + 0.9170510769 0.9194962978 0.9242408276 + 0.9173731208 0.9198102951 0.9245294929 + 0.9176951051 0.9201167822 0.9248180985 + 0.9180170298 0.9204118252 0.9251067042 + 0.9183390141 0.9207069278 0.9253953099 + 0.9186608791 0.9210019112 0.9256839156 + 0.9189829230 0.9212970138 0.9259725809 + 0.9193047881 0.9215921164 0.9262611866 + 0.9196267724 0.9218870997 0.9265497923 + 0.9199486971 0.9221822023 0.9268383980 + 0.9202542901 0.9224773049 0.9271270037 + 0.9205567241 0.9227722883 0.9274156094 + 0.9208590984 0.9230673909 0.9277042747 + 0.9211614728 0.9233623743 0.9279928803 + 0.9214640260 0.9236574769 0.9282814860 + 0.9217664003 0.9239525795 0.9285700917 + 0.9220687747 0.9242476225 0.9288586974 + 0.9223712087 0.9245427251 0.9291474223 + 0.9226737022 0.9248378277 0.9294360280 + 0.9229760766 0.9251328111 0.9297245741 + 0.9232785106 0.9254279137 0.9300122857 + 0.9235808849 0.9257230163 0.9302818179 + 0.9238833189 0.9260179996 0.9305514097 + 0.9241858125 0.9263131022 0.9308208823 + 0.9244881868 0.9266080856 0.9310904145 + 0.9247906208 0.9269031882 0.9313598871 + 0.9250929952 0.9271982908 0.9316294193 + 0.9253954887 0.9274932742 0.9318988919 + 0.9256979227 0.9277883768 0.9321684241 + 0.9260002971 0.9280834794 0.9324380159 + 0.9263026714 0.9283785224 0.9327074885 + 0.9266052246 0.9286736250 0.9329770207 + 0.9269075990 0.9289686084 0.9332464933 + 0.9272099733 0.9292637110 0.9335160255 + 0.9275124073 0.9295588136 0.9337854981 + 0.9278147817 0.9298537970 0.9340549707 + 0.9281172752 0.9301400185 0.9343245029 + 0.9284197092 0.9304174185 0.9345940948 + 0.9287220836 0.9306948781 0.9348636270 + 0.9290245175 0.9309722781 0.9351330996 + 0.9293270111 0.9312496781 0.9354025722 + 0.9296293855 0.9315270782 0.9356721044 + 0.9299318194 0.9318045974 0.9359415770 + 0.9302201271 0.9320819974 0.9362111092 + 0.9305043221 0.9323593974 0.9364805818 + 0.9307885170 0.9326369166 0.9367501736 + 0.9310727119 0.9329143167 0.9370197058 + 0.9313569069 0.9331917167 0.9372891784 + 0.9316411018 0.9334691167 0.9375587106 + 0.9319252968 0.9337465763 0.9378281832 + 0.9322094917 0.9340239763 0.9380977154 + 0.9324936867 0.9343013763 0.9383671880 + 0.9327778816 0.9345787764 0.9386367202 + 0.9330620766 0.9348562956 0.9389063120 + 0.9333462715 0.9351336956 0.9391757846 + 0.9336305261 0.9354110956 0.9394453168 + 0.9339147210 0.9356886148 0.9397147894 + 0.9341989160 0.9359660149 0.9399843216 + 0.9344831109 0.9362434149 0.9402372241 + 0.9347673059 0.9365208149 0.9404889941 + 0.9350515008 0.9367982745 0.9407408237 + 0.9353356957 0.9370756745 0.9409927130 + 0.9356198907 0.9373530746 0.9412444830 + 0.9359040856 0.9376305938 0.9414963126 + 0.9361882806 0.9379079938 0.9417482018 + 0.9364724755 0.9381853938 0.9419999719 + 0.9367566705 0.9384627938 0.9422518015 + 0.9370409250 0.9387403131 0.9425036907 + 0.9373251200 0.9390177131 0.9427555203 + 0.9376093149 0.9392951131 0.9430072904 + 0.9378935099 0.9395725727 0.9432591796 + 0.9381777048 0.9398499727 0.9435110092 + 0.9384618998 0.9401198030 0.9437627792 + 0.9387460947 0.9403808117 0.9440147281 + 0.9390302896 0.9406418204 0.9442664981 + 0.9393144846 0.9409028292 0.9445183277 + 0.9395986795 0.9411637187 0.9447702169 + 0.9398828745 0.9414247274 0.9450219870 + 0.9401571155 0.9416856766 0.9452738166 + 0.9404243231 0.9419466257 0.9455257058 + 0.9406914711 0.9422075748 0.9457774758 + 0.9409586787 0.9424685836 0.9460293055 + 0.9412258863 0.9427295923 0.9462810755 + 0.9414930940 0.9429904819 0.9465330243 + 0.9417603016 0.9432514906 0.9467847943 + 0.9420275092 0.9435124993 0.9470366240 + 0.9422945976 0.9437733889 0.9472885132 + 0.9425618052 0.9440343976 0.9475402832 + 0.9428290129 0.9442954063 0.9477921128 + 0.9430962205 0.9445564151 0.9480440021 + 0.9433634281 0.9448173046 0.9482957721 + 0.9436305761 0.9450783134 0.9485476017 + 0.9438977838 0.9453393221 0.9487994909 + 0.9441649914 0.9456002116 0.9490513206 + 0.9444321990 0.9458612204 0.9493030906 + 0.9446994066 0.9461222291 0.9495549798 + 0.9449666142 0.9463831782 0.9498068094 + 0.9452338219 0.9466441274 0.9500548244 + 0.9455010295 0.9469050765 0.9502903223 + 0.9457681775 0.9471660852 0.9505258203 + 0.9460353851 0.9474269748 0.9507611990 + 0.9463025928 0.9476879835 0.9509966969 + 0.9465698004 0.9479489923 0.9512321949 + 0.9468370080 0.9482100010 0.9514675736 + 0.9471042156 0.9484708905 0.9517030716 + 0.9473714232 0.9487318993 0.9519386292 + 0.9476385713 0.9489929080 0.9521741271 + 0.9479057789 0.9492537975 0.9524095058 + 0.9481729865 0.9495148063 0.9526450038 + 0.9484401941 0.9497758150 0.9528805017 + 0.9487074018 0.9500346184 0.9531158805 + 0.9489746094 0.9502801895 0.9533513784 + 0.9492418170 0.9505258203 0.9535868764 + 0.9495090246 0.9507713914 0.9538223147 + 0.9497761726 0.9510170221 0.9540578127 + 0.9500408173 0.9512627125 0.9542933106 + 0.9502921104 0.9515082836 0.9545286894 + 0.9505435228 0.9517539144 0.9547641873 + 0.9507948160 0.9519994855 0.9549996853 + 0.9510461092 0.9522451162 0.9552351832 + 0.9512975216 0.9524906874 0.9554706216 + 0.9515488148 0.9527363181 0.9557061195 + 0.9518002272 0.9529820085 0.9559416175 + 0.9520515203 0.9532275796 0.9561769962 + 0.9523028135 0.9534732103 0.9564124942 + 0.9525542259 0.9537187815 0.9566479921 + 0.9528055191 0.9539644122 0.9568833709 + 0.9530568123 0.9542099833 0.9571189284 + 0.9533082247 0.9544556141 0.9573544264 + 0.9535595179 0.9547013044 0.9575898051 + 0.9538108110 0.9549468756 0.9578253031 + 0.9540622234 0.9551925063 0.9580608010 + 0.9543135166 0.9554380774 0.9582962990 + 0.9545649290 0.9556837082 0.9585316777 + 0.9548162222 0.9559292793 0.9587671757 + 0.9550675154 0.9561749101 0.9590026736 + 0.9553189278 0.9564206004 0.9592381120 + 0.9555702209 0.9566661716 0.9594736099 + 0.9558215141 0.9569118023 0.9597091079 + 0.9560729265 0.9571573734 0.9599444866 + 0.9563242197 0.9574030042 0.9601684213 + 0.9565755725 0.9576485753 0.9603887796 + 0.9568269253 0.9578942060 0.9606090784 + 0.9570782185 0.9581398964 0.9608293772 + 0.9573295712 0.9583855271 0.9610496759 + 0.9575809240 0.9586310983 0.9612699747 + 0.9578322172 0.9588767290 0.9614903927 + 0.9580836296 0.9591223001 0.9617106915 + 0.9583349228 0.9593678713 0.9619309902 + 0.9585862160 0.9596135020 0.9621512890 + 0.9588376284 0.9598590732 0.9623715878 + 0.9590889215 0.9600986838 0.9625920057 + 0.9593402743 0.9603298903 0.9628123045 + 0.9595916271 0.9605612159 0.9630326033 + 0.9598429203 0.9607924819 0.9632529020 + 0.9600886703 0.9610238075 0.9634733200 + 0.9603253007 0.9612550735 0.9636936188 + 0.9605618119 0.9614862800 0.9639139175 + 0.9607983232 0.9617176056 0.9641342163 + 0.9610348940 0.9619488716 0.9643545151 + 0.9612714052 0.9621801972 0.9645748734 + 0.9615079761 0.9624115229 0.9647951722 + 0.9617444873 0.9626427293 0.9650154710 + 0.9619809985 0.9628739953 0.9652358294 + 0.9622176290 0.9631053209 0.9654561877 + 0.9624540806 0.9633365870 0.9656764865 + 0.9626907110 0.9635679126 0.9658967853 + 0.9629272223 0.9637991190 0.9661170840 + 0.9631636739 0.9640303850 0.9663373828 + 0.9634003043 0.9642617106 0.9665578008 + 0.9636368155 0.9644929767 0.9667780995 + 0.9638733864 0.9647243023 0.9669983983 + 0.9641098976 0.9649556279 0.9672186971 + 0.9643464088 0.9651867747 0.9674391150 + 0.9645829797 0.9654181004 0.9676594138 + 0.9648194909 0.9656494260 0.9678797126 + 0.9650561213 0.9658806920 0.9681000113 + 0.9652925730 0.9661120176 0.9683203101 + 0.9655292034 0.9663432240 0.9685407281 + 0.9657657146 0.9665744901 0.9687610269 + 0.9660022259 0.9668058157 0.9689813256 + 0.9662387967 0.9670370817 0.9692016244 + 0.9664753079 0.9672684073 0.9694219232 + 0.9667118788 0.9674996138 0.9696422815 + 0.9669483900 0.9677308798 0.9698625803 + 0.9671849012 0.9679622054 0.9700775743 + 0.9674214721 0.9681934714 0.9702839255 + 0.9676579833 0.9684247971 0.9704902172 + 0.9678946137 0.9686561227 0.9706965089 + 0.9681311250 0.9688873291 0.9709028006 + 0.9683675766 0.9691185951 0.9711090922 + 0.9686042070 0.9693499207 0.9713153839 + 0.9688407183 0.9695811868 0.9715216756 + 0.9690772891 0.9698125124 0.9717280269 + 0.9693138003 0.9700412154 0.9719343185 + 0.9695503116 0.9702590704 0.9721406102 + 0.9697868824 0.9704769850 0.9723469019 + 0.9700220823 0.9706948996 0.9725531936 + 0.9702448249 0.9709128141 0.9727594852 + 0.9704675078 0.9711307287 0.9729657769 + 0.9706903100 0.9713485837 0.9731721282 + 0.9709129930 0.9715664983 0.9733784199 + 0.9711356759 0.9717844129 0.9735847116 + 0.9713584781 0.9720023274 0.9737908840 + 0.9715812206 0.9722201824 0.9739971757 + 0.9718039036 0.9724380970 0.9742035270 + 0.9720267057 0.9726560116 0.9744098186 + 0.9722493887 0.9728739262 0.9746161103 + 0.9724720716 0.9730917811 0.9748224020 + 0.9726948738 0.9733096957 0.9750286937 + 0.9729176164 0.9735276103 0.9752349854 + 0.9731402993 0.9737455845 0.9754412770 + 0.9733631015 0.9739634991 0.9756476283 + 0.9735857844 0.9741814137 0.9758539200 + 0.9738085270 0.9743993282 0.9760602117 + 0.9740313292 0.9746171832 0.9762665033 + 0.9742540121 0.9748350978 0.9764727950 + 0.9744768143 0.9750530124 0.9766790867 + 0.9746994972 0.9752709270 0.9768853784 + 0.9749221802 0.9754887819 0.9770917296 + 0.9751449823 0.9757066965 0.9772980213 + 0.9753677249 0.9759246111 0.9775043130 + 0.9755904078 0.9761425257 0.9777106047 + 0.9758132100 0.9763603806 0.9779168963 + 0.9760358930 0.9765782952 0.9781231284 + 0.9762585759 0.9767962098 0.9783294201 + 0.9764813781 0.9770141244 0.9785357118 + 0.9767041206 0.9772319794 0.9787420034 + 0.9769268036 0.9774498940 0.9789482951 + 0.9771496058 0.9776678085 0.9791545868 + 0.9773722887 0.9778857231 0.9793608785 + 0.9775949717 0.9781035781 0.9795672297 + 0.9778177738 0.9783214927 0.9797735214 + 0.9780405164 0.9785394073 0.9799798131 + 0.9782631993 0.9787573218 0.9801744223 + 0.9784860015 0.9789751768 0.9803677201 + 0.9787086844 0.9791930914 0.9805610180 + 0.9789314270 0.9794110060 0.9807543159 + 0.9791542292 0.9796289206 0.9809476137 + 0.9793769121 0.9798467755 0.9811409116 + 0.9795997143 0.9800609946 0.9813342094 + 0.9798223972 0.9802663922 0.9815275073 + 0.9800425172 0.9804717898 0.9817208052 + 0.9802523851 0.9806771874 0.9819141030 + 0.9804621935 0.9808827043 0.9821074009 + 0.9806721210 0.9810881019 0.9823006988 + 0.9808819294 0.9812934995 0.9824939966 + 0.9810917974 0.9814988971 0.9826872945 + 0.9813016057 0.9817042947 0.9828805923 + 0.9815114737 0.9819096923 0.9830738902 + 0.9817212820 0.9821150899 0.9832671881 + 0.9819312096 0.9823204875 0.9834604859 + 0.9821410179 0.9825258851 0.9836537838 + 0.9823508859 0.9827314019 0.9838470817 + 0.9825606942 0.9829367995 0.9840403795 + 0.9827706218 0.9831421971 0.9842336774 + 0.9829803705 0.9833475947 0.9844269753 + 0.9831902981 0.9835529923 0.9846202731 + 0.9834001064 0.9837583899 0.9848135710 + 0.9836099744 0.9839637876 0.9850069284 + 0.9838197827 0.9841691852 0.9852002263 + 0.9840297103 0.9843745828 0.9853935242 + 0.9842395186 0.9845800996 0.9855868220 + 0.9844493866 0.9847854972 0.9857801199 + 0.9846591949 0.9849908948 0.9859734178 + 0.9848691225 0.9851962924 0.9861667156 + 0.9850788713 0.9854016900 0.9863600135 + 0.9852887988 0.9856070876 0.9865533113 + 0.9854986072 0.9858124852 0.9867466092 + 0.9857084751 0.9860178828 0.9869399071 + 0.9859182835 0.9862232804 0.9871332049 + 0.9861282110 0.9864287972 0.9873265028 + 0.9863380194 0.9866341949 0.9875198007 + 0.9865478873 0.9868395925 0.9877130985 + 0.9867578149 0.9870449901 0.9879063964 + 0.9869676232 0.9872503877 0.9880996943 + 0.9871774912 0.9874557853 0.9882929921 + 0.9873872995 0.9876611829 0.9884862900 + 0.9875972271 0.9878665805 0.9886795878 + 0.9878069758 0.9880719781 0.9888730049 + 0.9880169034 0.9882774949 0.9890663028 + 0.9882267118 0.9884828925 0.9892596006 + 0.9884365797 0.9886882901 0.9894528985 + 0.9886463881 0.9888936877 0.9896461964 + 0.9888563156 0.9890990853 0.9898394942 + 0.9890661240 0.9893044829 0.9900307059 + 0.9892759919 0.9895098805 0.9902120233 + 0.9894858003 0.9897152781 0.9903932214 + 0.9896957278 0.9899206758 0.9905744791 + 0.9899054766 0.9901189804 0.9907557964 + 0.9901087880 0.9903126955 0.9909369946 + 0.9903066158 0.9905064702 0.9911183119 + 0.9905043840 0.9907001853 0.9912995100 + 0.9907022119 0.9908940196 0.9914808273 + 0.9909000993 0.9910876751 0.9916620851 + 0.9910979271 0.9912815094 0.9918432832 + 0.9912956953 0.9914752245 0.9920246005 + 0.9914935231 0.9916689992 0.9922057986 + 0.9916914105 0.9918627143 0.9923871160 + 0.9918891788 0.9920564294 0.9925683737 + 0.9920870066 0.9922502041 0.9927495718 + 0.9922847748 0.9924439192 0.9929308891 + 0.9924827218 0.9926376939 0.9931120872 + 0.9926804900 0.9928314090 0.9932934046 + 0.9928783178 0.9930251837 0.9934747219 + 0.9930760860 0.9932188988 0.9936559200 + 0.9932739735 0.9934126735 0.9938371778 + 0.9934718013 0.9936063886 0.9940183759 + 0.9936696291 0.9938002229 0.9941996932 + 0.9938673973 0.9939938784 0.9943810105 + 0.9940652847 0.9941875935 0.9945622087 + 0.9942631125 0.9943814278 0.9947435260 + 0.9944608808 0.9945750833 0.9949247241 + 0.9946587086 0.9947689176 0.9951059818 + 0.9948565960 0.9949625731 0.9952871799 + 0.9950544238 0.9951564074 0.9954684973 + 0.9952521920 0.9953501225 0.9956498146 + 0.9954500198 0.9955438972 0.9958310127 + 0.9956479073 0.9957376122 0.9960122705 + 0.9958456755 0.9959313273 0.9961935282 + 0.9960435033 0.9961251020 0.9963747859 + 0.9962412715 0.9963188171 0.9965561032 + 0.9964392185 0.9965125918 0.9967373013 + 0.9966369867 0.9967063069 0.9969186187 + 0.9968348145 0.9969000816 0.9970998168 + 0.9970325828 0.9970937967 0.9972810745 + 0.9972305298 0.9972875714 0.9974623919 + 0.9974282980 0.9974812865 0.9976435900 + 0.9976261258 0.9976751208 0.9978249073 + 0.9978238940 0.9978687763 0.9980061054 + 0.9980217814 0.9980624914 0.9981874228 + 0.9982196093 0.9982563257 0.9983686805 + 0.9984173775 0.9984499812 0.9985498786 + 0.9986152053 0.9986438155 0.9987311959 + 0.9988130927 0.9988374710 0.9989123940 + 0.9990109205 0.9990313053 0.9990937114 + 0.9992086887 0.9992250204 0.9992750287 + 0.9994065166 0.9994187951 0.9994562268 + 0.9996044040 0.9996125102 0.9996374846 + 0.9998021722 0.9998062849 0.9998186827 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3154.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3154.spi1d index 9810d265..76922e1a 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3154.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3154.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0008270687 0.0012806139 0.0007092019 - 0.0016541370 0.0025612270 0.0014184040 - 0.0024812061 0.0038418409 0.0021276060 - 0.0033082750 0.0051224548 0.0028368081 - 0.0041353442 0.0064030681 0.0035460100 - 0.0049624122 0.0076836818 0.0042552119 - 0.0057894811 0.0089642964 0.0049644141 - 0.0066165500 0.0102737201 0.0056736148 - 0.0074436190 0.0117049702 0.0063828169 - 0.0082706874 0.0131362099 0.0070920191 - 0.0090977559 0.0145674599 0.0078012212 - 0.0099248253 0.0159987099 0.0085104229 - 0.0108320098 0.0174299609 0.0092196250 - 0.0117472196 0.0188612100 0.0099288272 - 0.0126624200 0.0203272700 0.0107055604 - 0.0135776196 0.0219289195 0.0114898197 - 0.0144928200 0.0235305596 0.0122740902 - 0.0154080195 0.0251322109 0.0130583504 - 0.0163232200 0.0267338492 0.0138426097 - 0.0172384195 0.0283354893 0.0146268802 - 0.0181536209 0.0299371406 0.0154111404 - 0.0190688204 0.0317234285 0.0161953997 - 0.0199840199 0.0335172787 0.0169796702 - 0.0209966209 0.0353111215 0.0177639294 - 0.0220109392 0.0371049605 0.0185481999 - 0.0230252706 0.0388987996 0.0193324592 - 0.0240395907 0.0407759696 0.0201293100 - 0.0250539202 0.0427856296 0.0209981594 - 0.0260682497 0.0447952896 0.0218669996 - 0.0270825699 0.0468049496 0.0227358509 - 0.0280968994 0.0488146096 0.0236046892 - 0.0291112196 0.0509230010 0.0244735405 - 0.0301393494 0.0531734005 0.0253423806 - 0.0312651210 0.0554237999 0.0262112301 - 0.0323909000 0.0576741993 0.0270800795 - 0.0335166790 0.0599245988 0.0279489197 - 0.0346424617 0.0624321401 0.0288177691 - 0.0357682407 0.0649486035 0.0296866093 - 0.0368940197 0.0674650520 0.0306164194 - 0.0380197987 0.0699815080 0.0315806195 - 0.0391455814 0.0727861822 0.0325448215 - 0.0403015316 0.0755929798 0.0335090198 - 0.0415524915 0.0783997923 0.0344732217 - 0.0428034514 0.0813405216 0.0354374312 - 0.0440544114 0.0844588801 0.0364016294 - 0.0453053713 0.0875772387 0.0373658314 - 0.0465563312 0.0907685384 0.0383300297 - 0.0478072912 0.0942139179 0.0392942317 - 0.0490582511 0.0976593122 0.0402872488 - 0.0503439009 0.1012116969 0.0413589515 - 0.0517351814 0.1049906015 0.0424306504 - 0.0531264618 0.1087696031 0.0435023494 - 0.0545177385 0.1127699018 0.0445740595 - 0.0559090301 0.1168769971 0.0456457585 - 0.0573003106 0.1210578978 0.0467174612 - 0.0586915910 0.1254729033 0.0477891602 - 0.0600922108 0.1298878938 0.0488608703 - 0.0616402999 0.1345676035 0.0499325693 - 0.0631883815 0.1392543018 0.0511177592 - 0.0647364631 0.1441261023 0.0523105711 - 0.0662845522 0.1490330994 0.0535033792 - 0.0678326264 0.1540661007 0.0546961986 - 0.0693807229 0.1591300964 0.0558890104 - 0.0710334405 0.1642663032 0.0570818186 - 0.0727559403 0.1694173962 0.0582746305 - 0.0744784400 0.1745833009 0.0594674386 - 0.0762009397 0.1797512025 0.0607356615 - 0.0779234320 0.1848731041 0.0620646887 - 0.0796459317 0.1899926066 0.0633937269 - 0.0815215707 0.1950089037 0.0647227690 - 0.0834368318 0.2000242025 0.0660518110 - 0.0853521004 0.2048937976 0.0673808381 - 0.0872673616 0.2097633928 0.0687098801 - 0.0891826227 0.2144646943 0.0700433925 - 0.0912189633 0.2191573977 0.0715252534 - 0.0933454409 0.2236897051 0.0730071068 - 0.0954719111 0.2281869054 0.0744889826 - 0.0975983888 0.2325620949 0.0759708434 - 0.0997248664 0.2368548959 0.0774526969 - 0.1020506024 0.2410928011 0.0789345577 - 0.1044057980 0.2451802045 0.0804644004 - 0.1067610979 0.2492675036 0.0821170136 - 0.1091163978 0.2531898916 0.0837696269 - 0.1116243005 0.2570762932 0.0854222402 - 0.1142238006 0.2609150112 0.0870748535 - 0.1168233976 0.2646088004 0.0887274593 - 0.1194228977 0.2683025002 0.0904237032 - 0.1222214997 0.2718980014 0.0922659934 - 0.1250769049 0.2754099071 0.0941082835 - 0.1279322952 0.2789216936 0.0959505737 - 0.1308598965 0.2823159099 0.0977928713 - 0.1339771003 0.2856579125 0.0996351615 - 0.1370943040 0.2889998853 0.1016450971 - 0.1402291059 0.2922315896 0.1036963984 - 0.1436063051 0.2954162955 0.1057477966 - 0.1469835043 0.2986010015 0.1077990979 - 0.1503873020 0.3017044067 0.1098503992 - 0.1540133953 0.3047442138 0.1121131033 - 0.1576395929 0.3077841103 0.1143923029 - 0.1613450944 0.3107880056 0.1166715994 - 0.1651988029 0.3136951029 0.1189509034 - 0.1690524071 0.3166022003 0.1213624030 - 0.1730536968 0.3195092976 0.1238868013 - 0.1771031022 0.3223156929 0.1264111996 - 0.1811967939 0.3251017034 0.1289356053 - 0.1854016036 0.3278875947 0.1316096932 - 0.1896062940 0.3306469023 0.1343929023 - 0.1939094067 0.3333224058 0.1371762007 - 0.1982226968 0.3359979987 0.1399594992 - 0.2025707066 0.3386735022 0.1430062056 - 0.2069430053 0.3412984908 0.1460568011 - 0.2113184035 0.3438735902 0.1491073966 - 0.2157011032 0.3464488089 0.1523478031 - 0.2200832069 0.3490239978 0.1556666940 - 0.2244319022 0.3515425920 0.1589855999 - 0.2287805974 0.3540267050 0.1624847949 - 0.2330776006 0.3565106988 0.1660633981 - 0.2373543978 0.3589948118 0.1696420014 - 0.2415924072 0.3614296019 0.1734369993 - 0.2457672954 0.3638310134 0.1772560030 - 0.2499420941 0.3662323952 0.1811341941 - 0.2539947033 0.3686338067 0.1851633936 - 0.2580456138 0.3710030019 0.1891925931 - 0.2620247900 0.3733294904 0.1933576018 - 0.2659370899 0.3756558895 0.1975566000 - 0.2698495090 0.3779824078 0.2018067986 - 0.2736209035 0.3802998960 0.2061281949 - 0.2773866057 0.3825586140 0.2104569972 - 0.2811067104 0.3848172128 0.2148493975 - 0.2847231030 0.3870758116 0.2192417979 - 0.2883394957 0.3893344998 0.2236506939 - 0.2918758094 0.3915497959 0.2280631959 - 0.2953441143 0.3937469125 0.2324603051 - 0.2988123894 0.3959440887 0.2368455976 - 0.3021861017 0.3981412947 0.2412119061 - 0.3055104911 0.4003298879 0.2455295026 - 0.3088349998 0.4024715126 0.2498470992 - 0.3120698929 0.4046129882 0.2540681958 - 0.3152567148 0.4067544937 0.2582857013 - 0.3184433877 0.4088960886 0.2624298036 - 0.3215635121 0.4110131860 0.2665235102 - 0.3246200085 0.4131042957 0.2705960870 - 0.3276765049 0.4151954055 0.2745501995 - 0.3307037055 0.4172866046 0.2785041928 - 0.3336381018 0.4193777144 0.2823660076 - 0.3365724981 0.4214367867 0.2861714959 - 0.3395070136 0.4234822094 0.2899771035 - 0.3423467875 0.4255276918 0.2936319113 - 0.3451676071 0.4275731146 0.2972859144 - 0.3479883969 0.4296185970 0.3009010851 - 0.3507789969 0.4316303134 0.3044045866 - 0.3534946144 0.4336344004 0.3079079986 - 0.3562101126 0.4356383979 0.3113526106 - 0.3589256108 0.4376423955 0.3147099912 - 0.3615824878 0.4396465123 0.3180673122 - 0.3642010987 0.4416194856 0.3213653862 - 0.3668195903 0.4435859025 0.3245829940 - 0.3694381118 0.4455522895 0.3278006911 - 0.3719865084 0.4475187063 0.3309766054 - 0.3745158017 0.4494850039 0.3340623081 - 0.3770450950 0.4514260888 0.3371481001 - 0.3795743883 0.4533582032 0.3402245045 - 0.3820356131 0.4552902877 0.3431868851 - 0.3844830990 0.4572224021 0.3461492956 - 0.3869304955 0.4591544867 0.3491117060 - 0.3893780112 0.4610689878 0.3519938886 - 0.3917695880 0.4629696906 0.3548417091 - 0.3941422105 0.4648705125 0.3576894999 - 0.3965147138 0.4667713046 0.3605172932 - 0.3988873065 0.4686720073 0.3632592857 - 0.4012235105 0.4705640972 0.3660013080 - 0.4035275877 0.4724361002 0.3687433004 - 0.4058316052 0.4743080139 0.3714326024 - 0.4081357121 0.4761799872 0.3740772903 - 0.4104278982 0.4780519009 0.3767220080 - 0.4126693010 0.4799239039 0.3793666065 - 0.4149107933 0.4817703068 0.3819434941 - 0.4171522856 0.4836156964 0.3844990134 - 0.4193938076 0.4854609966 0.3870545030 - 0.4215936065 0.4873062968 0.3896099925 - 0.4237779975 0.4891516864 0.3920964897 - 0.4259622991 0.4909836054 0.3945704997 - 0.4281466901 0.4928041101 0.3970445096 - 0.4303230941 0.4946246147 0.3995184898 - 0.4324553013 0.4964450002 0.4019325972 - 0.4345873892 0.4982655048 0.4043321908 - 0.4367195070 0.5000848770 0.4067319036 - 0.4388515949 0.5018820167 0.4091314971 - 0.4409618080 0.5036790967 0.4114879966 - 0.4430460930 0.5054761171 0.4138199091 - 0.4451304972 0.5072731972 0.4161519110 - 0.4472149014 0.5090702772 0.4184837937 - 0.4492993057 0.5108565092 0.4207941890 - 0.4513547122 0.5126311779 0.4230645001 - 0.4533953965 0.5144059062 0.4253348112 - 0.4554361105 0.5161805749 0.4276050925 - 0.4574767947 0.5179553032 0.4298754930 - 0.4595173895 0.5197299719 0.4320927858 - 0.4615274966 0.5214862823 0.4343070984 - 0.4635280967 0.5232393146 0.4365214109 - 0.4655286074 0.5249922872 0.4387356937 - 0.4675292075 0.5267453194 0.4409281015 - 0.4695298076 0.5284982920 0.4430915117 - 0.4715020955 0.5302482843 0.4452548027 - 0.4734657109 0.5319799185 0.4474182129 - 0.4754293859 0.5337114930 0.4495815039 - 0.4773930013 0.5354431868 0.4517075121 - 0.4793567061 0.5371748209 0.4538244009 - 0.4812973142 0.5389063954 0.4559414089 - 0.4832268059 0.5406302214 0.4580583870 - 0.4851562977 0.5423403978 0.4601719081 - 0.4870857894 0.5440505743 0.4622465968 - 0.4890153110 0.5457608104 0.4643214047 - 0.4909291863 0.5474711061 0.4663960934 - 0.4928269088 0.5491812825 0.4684709013 - 0.4947246015 0.5508800745 0.4705354869 - 0.4966222942 0.5525686145 0.4725716114 - 0.4985199869 0.5542569757 0.4746077955 - 0.5004112124 0.5559453964 0.4766440094 - 0.5022791028 0.5576338172 0.4786801040 - 0.5041469932 0.5593221784 0.4807038903 - 0.5060148835 0.5609971285 0.4827046990 - 0.5078828931 0.5626630187 0.4847055078 - 0.5097507834 0.5643289089 0.4867062867 - 0.5115942955 0.5659946799 0.4887070954 - 0.5134339929 0.5676606297 0.4906964004 - 0.5152738094 0.5693264008 0.4926646054 - 0.5171135068 0.5709782243 0.4946328104 - 0.5189533234 0.5726205111 0.4966011047 - 0.5207813978 0.5742626786 0.4985693097 - 0.5225942135 0.5759049058 0.5005292892 - 0.5244070292 0.5775471926 0.5024672747 - 0.5262197852 0.5791894197 0.5044053197 - 0.5280326009 0.5808190107 0.5063433051 - 0.5298454762 0.5824362040 0.5082812905 - 0.5316343904 0.5840533972 0.5102161169 - 0.5334210992 0.5856705904 0.5121259093 - 0.5352079272 0.5872877836 0.5140355825 - 0.5369945765 0.5889049768 0.5159453154 - 0.5387812853 0.5905135870 0.5178549886 - 0.5405598879 0.5921041965 0.5197647810 - 0.5423210263 0.5936946869 0.5216510296 - 0.5440822244 0.5952851772 0.5235339999 - 0.5458433032 0.5968757272 0.5254169106 - 0.5476043820 0.5984662175 0.5272998810 - 0.5493655801 0.6000556946 0.5291827917 - 0.5511103868 0.6016175747 0.5310512185 - 0.5528460741 0.6031795144 0.5329084992 - 0.5545817018 0.6047413945 0.5347657800 - 0.5563173890 0.6063032746 0.5366231203 - 0.5580530763 0.6078652143 0.5384802818 - 0.5597887039 0.6094270945 0.5403330922 - 0.5615018010 0.6109694839 0.5421653986 - 0.5632117987 0.6125006080 0.5439975858 - 0.5649217963 0.6140317917 0.5458298922 - 0.5666316748 0.6155629158 0.5476621985 - 0.5683416724 0.6170939803 0.5494945049 - 0.5700508952 0.6186252236 0.5513088703 - 0.5717346072 0.6201528907 0.5531163812 - 0.5734183192 0.6216509938 0.5549240112 - 0.5751019120 0.6231490970 0.5567315221 - 0.5767856240 0.6246472001 0.5585390925 - 0.5784692764 0.6261453032 0.5603417754 - 0.5801506042 0.6276432872 0.5621244907 - 0.5818070769 0.6291413903 0.5639072061 - 0.5834636092 0.6306244135 0.5656898022 - 0.5851200819 0.6320869923 0.5674725175 - 0.5867766142 0.6335496902 0.5692551136 - 0.5884330869 0.6350123286 0.5710229874 - 0.5900881290 0.6364750266 0.5727801919 - 0.5917161703 0.6379376054 0.5745375156 - 0.5933442712 0.6394003034 0.5762947202 - 0.5949723721 0.6408405900 0.5780519247 - 0.5966004729 0.6422653794 0.5798091292 - 0.5982285738 0.6436902285 0.5815427899 - 0.5998566747 0.6451150179 0.5832737088 - 0.6014575958 0.6465398073 0.5850045085 - 0.6030557752 0.6479645967 0.5867353082 - 0.6046540737 0.6493893862 0.5884662271 - 0.6062523127 0.6507912278 0.5901939273 - 0.6078504920 0.6521757841 0.5918970108 - 0.6094486713 0.6535604000 0.5936000943 - 0.6110262275 0.6549450159 0.5953032970 - 0.6125928164 0.6563295126 0.5970063806 - 0.6141594052 0.6577141285 0.5987095237 - 0.6157259941 0.6590986848 0.6004055142 - 0.6172925830 0.6604683995 0.6020792723 - 0.6188592911 0.6618105173 0.6037530899 - 0.6204167008 0.6631525755 0.6054267883 - 0.6219497919 0.6644946933 0.6071006060 - 0.6234828234 0.6658368111 0.6087743044 - 0.6250159144 0.6671789289 0.6104397178 - 0.6265488863 0.6685209274 0.6120821238 - 0.6280819774 0.6698629856 0.6137245297 - 0.6296150088 0.6711651087 0.6153669953 - 0.6311213970 0.6724625826 0.6170094013 - 0.6326187849 0.6737599969 0.6186518073 - 0.6341162920 0.6750574708 0.6202881932 - 0.6356136799 0.6763550043 0.6218971014 - 0.6371111274 0.6776524782 0.6235058904 - 0.6386085153 0.6789500117 0.6251147985 - 0.6401032805 0.6802387238 0.6267235875 - 0.6415628791 0.6814897060 0.6283324957 - 0.6430225968 0.6827408075 0.6299412847 - 0.6444821954 0.6839917898 0.6315155029 - 0.6459417939 0.6852428913 0.6330882907 - 0.6474015117 0.6864938736 0.6346611977 - 0.6488611102 0.6877449751 0.6362339854 - 0.6503120065 0.6889960170 0.6378068924 - 0.6517316103 0.6902375817 0.6393796802 - 0.6531512737 0.6914405823 0.6409292221 - 0.6545709968 0.6926437020 0.6424635053 - 0.6559906006 0.6938467026 0.6439977884 - 0.6574103236 0.6950497031 0.6455320716 - 0.6588299274 0.6962528229 0.6470664144 - 0.6602421999 0.6974558234 0.6486006975 - 0.6616197824 0.6986588836 0.6501314044 - 0.6629973054 0.6998618841 0.6516245008 - 0.6643748879 0.7010213137 0.6531175971 - 0.6657524109 0.7021750808 0.6546106935 - 0.6671299934 0.7033289075 0.6561039090 - 0.6685075164 0.7044826746 0.6575970054 - 0.6698850989 0.7056365013 0.6590901017 - 0.6712222099 0.7067903280 0.6605660915 - 0.6725556254 0.7079440951 0.6620154977 - 0.6738889813 0.7090979218 0.6634649038 - 0.6752223969 0.7102407813 0.6649141908 - 0.6765558124 0.7113444209 0.6663635969 - 0.6778892279 0.7124481201 0.6678130031 - 0.6792225838 0.7135518193 0.6692622900 - 0.6805368066 0.7146555185 0.6706889868 - 0.6818242073 0.7157592177 0.6720920801 - 0.6831117272 0.7168627977 0.6734951735 - 0.6843991280 0.7179664969 0.6748983264 - 0.6856865287 0.7190701962 0.6763014197 - 0.6869739294 0.7201659083 0.6777045727 - 0.6882612705 0.7212188840 0.6791077256 - 0.6895486712 0.7222719789 0.6804931164 - 0.6908051968 0.7233250737 0.6818476915 - 0.6920449734 0.7243781090 0.6832023263 - 0.6932848096 0.7254312038 0.6845569015 - 0.6945245862 0.7264842987 0.6859114766 - 0.6957643032 0.7275372744 0.6872661114 - 0.6970040798 0.7285904288 0.6886206269 - 0.6982439160 0.7296434045 0.6899752021 - 0.6994836926 0.7306630015 0.6912801862 - 0.7006949186 0.7316653132 0.6925842166 - 0.7018857002 0.7326676250 0.6938881874 - 0.7030766010 0.7336698771 0.6951922178 - 0.7042673826 0.7346723080 0.6964963078 - 0.7054582238 0.7356746197 0.6978002787 - 0.7066490054 0.7366768718 0.6991043091 - 0.7078399062 0.7376793027 0.7003920078 - 0.7090306878 0.7386816144 0.7016437054 - 0.7102121711 0.7396839261 0.7028955221 - 0.7113531232 0.7406517267 0.7041472197 - 0.7124938965 0.7416034937 0.7053989768 - 0.7136347294 0.7425553799 0.7066507936 - 0.7147756219 0.7435072064 0.7079024911 - 0.7159163952 0.7444590926 0.7091543078 - 0.7170572281 0.7454109192 0.7103886008 - 0.7181981206 0.7463628054 0.7115867734 - 0.7193388939 0.7473145723 0.7127850056 - 0.7204583883 0.7482665181 0.7139831781 - 0.7215486169 0.7492182851 0.7151812911 - 0.7226387858 0.7501612902 0.7163795233 - 0.7237290144 0.7510632277 0.7175776958 - 0.7248191237 0.7519652247 0.7187758088 - 0.7259092927 0.7528672218 0.7199739814 - 0.7269995213 0.7537690997 0.7211189270 - 0.7280896902 0.7546710968 0.7222625017 - 0.7291797996 0.7555730939 0.7234061956 - 0.7302573919 0.7564749718 0.7245498896 - 0.7312965989 0.7573770285 0.7256935835 - 0.7323358059 0.7582790256 0.7268372178 - 0.7333750129 0.7591809034 0.7279809117 - 0.7344142199 0.7600783706 0.7291246057 - 0.7354534268 0.7609313726 0.7302554250 - 0.7364925742 0.7617843747 0.7313441038 - 0.7375317812 0.7626373768 0.7324327826 - 0.7385709882 0.7634903789 0.7335215211 - 0.7396101952 0.7643433809 0.7346101999 - 0.7406175137 0.7651963830 0.7356988788 - 0.7416058183 0.7660493851 0.7367876172 - 0.7425940037 0.7669023871 0.7378762960 - 0.7435823083 0.7677553892 0.7389649749 - 0.7445706129 0.7686083913 0.7400509715 - 0.7455587983 0.7694613934 0.7410846949 - 0.7465471029 0.7702968121 0.7421182990 - 0.7475354075 0.7711020112 0.7431520224 - 0.7485235929 0.7719072104 0.7441856861 - 0.7495118976 0.7727124095 0.7452194095 - 0.7504745722 0.7735176086 0.7462530732 - 0.7514122725 0.7743229270 0.7472867966 - 0.7523499727 0.7751281261 0.7483205199 - 0.7532876730 0.7759333253 0.7493541837 - 0.7542254925 0.7767385244 0.7503672838 - 0.7551631927 0.7775437236 0.7513464093 - 0.7561008930 0.7783489823 0.7523254156 - 0.7570385933 0.7791541815 0.7533044815 - 0.7579764128 0.7799593806 0.7542834878 - 0.7589141130 0.7807205915 0.7552626133 - 0.7598518133 0.7814794779 0.7562416792 - 0.7607476115 0.7822383046 0.7572206855 - 0.7616354823 0.7829971910 0.7581998110 - 0.7625234127 0.7837560773 0.7591788173 - 0.7634112835 0.7845150232 0.7601491809 - 0.7642992139 0.7852737904 0.7610744238 - 0.7651870847 0.7860326767 0.7619994879 - 0.7660750151 0.7867916226 0.7629246712 - 0.7669628859 0.7875505090 0.7638499141 - 0.7678508162 0.7883092761 0.7647750974 - 0.7687386870 0.7890682220 0.7657002807 - 0.7696266174 0.7898271084 0.7666255236 - 0.7704861760 0.7905514240 0.7675507069 - 0.7713252902 0.7912656069 0.7684757710 - 0.7721645236 0.7919796705 0.7694010139 - 0.7730035782 0.7926939130 0.7703076005 - 0.7738426924 0.7934079766 0.7711800933 - 0.7746818066 0.7941222191 0.7720525265 - 0.7755209208 0.7948362827 0.7729249001 - 0.7763599753 0.7955505252 0.7737973928 - 0.7771990895 0.7962645888 0.7746698260 - 0.7780382037 0.7969787717 0.7755423188 - 0.7788773179 0.7976928949 0.7764146924 - 0.7797163725 0.7984070778 0.7772871852 - 0.7805240154 0.7991213202 0.7781596184 - 0.7813156247 0.7998353839 0.7790321112 - 0.7821071744 0.8005164862 0.7799044847 - 0.7828987241 0.8011876941 0.7807313204 - 0.7836902738 0.8018589020 0.7815523744 - 0.7844818830 0.8025301099 0.7823736072 - 0.7852734923 0.8032013178 0.7831947207 - 0.7860651016 0.8038725853 0.7840157747 - 0.7868565917 0.8045437932 0.7848370075 - 0.7876482010 0.8052150011 0.7856581211 - 0.7884398103 0.8058862090 0.7864792943 - 0.7892314196 0.8065574169 0.7873004079 - 0.7900215983 0.8072286248 0.7881215811 - 0.7907671928 0.8078997731 0.7889426947 - 0.7915127277 0.8085709810 0.7897638083 - 0.7922583222 0.8092421889 0.7905495763 - 0.7930037975 0.8099133968 0.7913212180 - 0.7937493920 0.8105489016 0.7920926809 - 0.7944949269 0.8111789823 0.7928642035 - 0.7952405214 0.8118091822 0.7936357856 - 0.7959859967 0.8124393821 0.7944073081 - 0.7967315912 0.8130695224 0.7951787710 - 0.7974771261 0.8136997223 0.7959504128 - 0.7982227206 0.8143298030 0.7967218757 - 0.7989683151 0.8149600029 0.7974933982 - 0.7997137904 0.8155900836 0.7982649803 - 0.8004320264 0.8162202835 0.7990365028 - 0.8011332154 0.8168504238 0.7998080254 - 0.8018345237 0.8174806237 0.8005437255 - 0.8025357127 0.8181107044 0.8012676239 - 0.8032369018 0.8187409043 0.8019914031 - 0.8039380908 0.8193711042 0.8027151823 - 0.8046392798 0.8200011253 0.8034390807 - 0.8053405285 0.8205922246 0.8041629195 - 0.8060417175 0.8211833239 0.8048866987 - 0.8067429066 0.8217743039 0.8056105971 - 0.8074440956 0.8223654032 0.8063343763 - 0.8081452847 0.8229563832 0.8070582151 - 0.8088464737 0.8235474825 0.8077819943 - 0.8095477223 0.8241385818 0.8085058928 - 0.8102337718 0.8247296214 0.8092296720 - 0.8108925223 0.8253207207 0.8099535108 - 0.8115512133 0.8259118199 0.8106346130 - 0.8122099042 0.8265028000 0.8113127947 - 0.8128685951 0.8270938993 0.8119909763 - 0.8135272264 0.8276848793 0.8126692176 - 0.8141859174 0.8282759786 0.8133473992 - 0.8148446083 0.8288670778 0.8140255809 - 0.8155032992 0.8294581175 0.8147038221 - 0.8161619902 0.8300461173 0.8153820038 - 0.8168206215 0.8306000829 0.8160601258 - 0.8174793124 0.8311539888 0.8167383075 - 0.8181380033 0.8317080140 0.8174164891 - 0.8187966943 0.8322619796 0.8180946708 - 0.8194553852 0.8328160048 0.8187729120 - 0.8201069832 0.8333699107 0.8194510937 - 0.8207250834 0.8339238763 0.8201209903 - 0.8213431239 0.8344779015 0.8207557201 - 0.8219612241 0.8350318074 0.8213903904 - 0.8225793242 0.8355857730 0.8220251203 - 0.8231973052 0.8361397982 0.8226597905 - 0.8238154054 0.8366938233 0.8232945204 - 0.8244335055 0.8372477293 0.8239293098 - 0.8250514865 0.8378016949 0.8245639801 - 0.8256695867 0.8383557200 0.8251987100 - 0.8262876868 0.8389096260 0.8258333802 - 0.8269057274 0.8394635916 0.8264681101 - 0.8275238276 0.8400164843 0.8271027803 - 0.8281419277 0.8405354023 0.8277375102 - 0.8287599087 0.8410543203 0.8283722997 - 0.8293780088 0.8415731192 0.8290070295 - 0.8299961090 0.8420919776 0.8296416998 - 0.8305758238 0.8426108956 0.8302584291 - 0.8311551809 0.8431298137 0.8308519125 - 0.8317345977 0.8436486721 0.8314453959 - 0.8323140740 0.8441675901 0.8320388794 - 0.8328934908 0.8446865082 0.8326324224 - 0.8334729075 0.8452054262 0.8332257867 - 0.8340523839 0.8457242846 0.8338193297 - 0.8346318007 0.8462432027 0.8344128132 - 0.8352112174 0.8467620015 0.8350062966 - 0.8357906938 0.8472809196 0.8355997205 - 0.8363701105 0.8477997780 0.8361932039 - 0.8369495273 0.8483186960 0.8367866874 - 0.8375290036 0.8488376141 0.8373801708 - 0.8381084204 0.8493564725 0.8379737139 - 0.8386877775 0.8498753905 0.8385670781 - 0.8392673135 0.8503690958 0.8391606212 - 0.8398466706 0.8508548737 0.8397541046 - 0.8403992057 0.8513407111 0.8403248191 - 0.8409420252 0.8518266082 0.8408793211 - 0.8414847851 0.8523123860 0.8414338231 - 0.8420276046 0.8527982235 0.8419883251 - 0.8425704837 0.8532840014 0.8425428271 - 0.8431133032 0.8537697792 0.8430973291 - 0.8436561227 0.8542556167 0.8436517715 - 0.8441988826 0.8547413945 0.8442062736 - 0.8447417021 0.8552271724 0.8447607756 - 0.8452845216 0.8557130098 0.8453152776 - 0.8458272815 0.8561987877 0.8458697796 - 0.8463702202 0.8566846251 0.8464242816 - 0.8469129801 0.8571704030 0.8469787836 - 0.8474557996 0.8576561809 0.8475332856 - 0.8479986191 0.8581420183 0.8480879068 - 0.8485413790 0.8586277962 0.8486424088 - 0.8490841985 0.8591135740 0.8491969109 - 0.8496270180 0.8595994115 0.8497514129 - 0.8501589894 0.8600798249 0.8502855897 - 0.8506671786 0.8605344296 0.8508033752 - 0.8511754274 0.8609890938 0.8513212204 - 0.8516836166 0.8614438176 0.8518390059 - 0.8521918058 0.8618984818 0.8523567915 - 0.8526999950 0.8623532057 0.8528745770 - 0.8532081842 0.8628078103 0.8533924222 - 0.8537163734 0.8632624745 0.8539102077 - 0.8542246819 0.8637171984 0.8544279933 - 0.8547328711 0.8641719222 0.8549457788 - 0.8552411199 0.8646265268 0.8554636240 - 0.8557493091 0.8650811911 0.8559814095 - 0.8562574983 0.8655359149 0.8564991951 - 0.8567656875 0.8659905791 0.8570169806 - 0.8572738767 0.8664453030 0.8575347066 - 0.8577821255 0.8668999076 0.8580524921 - 0.8582903147 0.8673545718 0.8585702777 - 0.8587985039 0.8678092957 0.8590881228 - 0.8593066931 0.8682640195 0.8596059084 - 0.8598148823 0.8687186241 0.8601155281 - 0.8603023887 0.8691732883 0.8605988026 - 0.8607779741 0.8696280122 0.8610820770 - 0.8612535000 0.8700773716 0.8615654111 - 0.8617290854 0.8705028296 0.8620486856 - 0.8622046709 0.8709282875 0.8625320196 - 0.8626803160 0.8713536859 0.8630152941 - 0.8631559014 0.8717792034 0.8634986281 - 0.8636314869 0.8722046018 0.8639819026 - 0.8641070724 0.8726301193 0.8644651771 - 0.8645827174 0.8730555177 0.8649485111 - 0.8650581837 0.8734809756 0.8654317856 - 0.8655338287 0.8739064932 0.8659151793 - 0.8660094142 0.8743318915 0.8663985133 - 0.8664849997 0.8747574091 0.8668817878 - 0.8669605851 0.8751828074 0.8673651218 - 0.8674361706 0.8756083250 0.8678483963 - 0.8679118156 0.8760337234 0.8683316708 - 0.8683874011 0.8764591813 0.8688150048 - 0.8688629866 0.8768845797 0.8692982793 - 0.8693385124 0.8773100972 0.8697816133 - 0.8698140979 0.8777354956 0.8702471852 - 0.8702710271 0.8781610131 0.8706982136 - 0.8707159758 0.8785864711 0.8711491823 - 0.8711609244 0.8790119290 0.8716002107 - 0.8716058135 0.8794373870 0.8720511198 - 0.8720507026 0.8798627853 0.8725020885 - 0.8724955916 0.8802697062 0.8729531169 - 0.8729405999 0.8806678057 0.8734040856 - 0.8733854890 0.8810659051 0.8738551140 - 0.8738303781 0.8814638853 0.8743060827 - 0.8742753267 0.8818619847 0.8747571111 - 0.8747202754 0.8822600842 0.8752080798 - 0.8751652241 0.8826581836 0.8756589890 - 0.8756101131 0.8830562234 0.8761100173 - 0.8760550022 0.8834543228 0.8765609860 - 0.8764998913 0.8838524222 0.8770120144 - 0.8769448996 0.8842504025 0.8774629831 - 0.8773897886 0.8846485019 0.8779140115 - 0.8778346777 0.8850466013 0.8783649802 - 0.8782796264 0.8854447007 0.8788160086 - 0.8787245750 0.8858426809 0.8792669773 - 0.8791695237 0.8862407804 0.8797178864 - 0.8796144128 0.8866388798 0.8801575899 - 0.8800554872 0.8870369196 0.8805783987 - 0.8804715872 0.8874350190 0.8809990883 - 0.8808878064 0.8878331184 0.8814198971 - 0.8813039064 0.8882312179 0.8818407059 - 0.8817201257 0.8886291981 0.8822613955 - 0.8821362257 0.8890272975 0.8826822042 - 0.8825523853 0.8894253969 0.8831030130 - 0.8829684854 0.8898233771 0.8835237026 - 0.8833847046 0.8902072906 0.8839445114 - 0.8838008046 0.8905797005 0.8843653202 - 0.8842170238 0.8909522295 0.8847860098 - 0.8846331239 0.8913245797 0.8852068186 - 0.8850492835 0.8916971087 0.8856276274 - 0.8854653835 0.8920695186 0.8860483170 - 0.8858816028 0.8924419880 0.8864691257 - 0.8862977028 0.8928143978 0.8868898749 - 0.8867139220 0.8931869268 0.8873106241 - 0.8871300817 0.8935592771 0.8877313733 - 0.8875461817 0.8939318061 0.8881521821 - 0.8879624009 0.8943042159 0.8885728717 - 0.8883785009 0.8946766853 0.8889936805 - 0.8887947202 0.8950490952 0.8894144893 - 0.8892108202 0.8954216242 0.8898351789 - 0.8896269798 0.8957939744 0.8902388215 - 0.8900402784 0.8961665034 0.8906313777 - 0.8904294968 0.8965389729 0.8910239935 - 0.8908187151 0.8969113827 0.8914164901 - 0.8912078738 0.8972839117 0.8918091059 - 0.8915970922 0.8976563215 0.8922016025 - 0.8919863105 0.8980287910 0.8925942183 - 0.8923755288 0.8984012008 0.8929867148 - 0.8927648067 0.8987737298 0.8933792710 - 0.8931540251 0.8991460800 0.8937718868 - 0.8935431838 0.8995186090 0.8941643834 - 0.8939324021 0.8998910189 0.8945569992 - 0.8943216205 0.9002465010 0.8949494958 - 0.8947107792 0.9005951285 0.8953421116 - 0.8950999975 0.9009435773 0.8957346082 - 0.8954892159 0.9012920856 0.8961272240 - 0.8958783746 0.9016405940 0.8965197802 - 0.8962675929 0.9019891024 0.8969122767 - 0.8966568112 0.9023376107 0.8973048925 - 0.8970460296 0.9026861191 0.8976973891 - 0.8974351883 0.9030346870 0.8980900049 - 0.8978244066 0.9033831954 0.8984825015 - 0.8982136250 0.9037317038 0.8988751173 - 0.8986027837 0.9040802121 0.8992676735 - 0.8989920020 0.9044287205 0.8996602297 - 0.8993812203 0.9047772288 0.9000492096 - 0.8997703791 0.9051256776 0.9004154801 - 0.9001492858 0.9054743052 0.9007818103 - 0.9005132914 0.9058228135 0.9011480212 - 0.9008772969 0.9061713219 0.9015142918 - 0.9012413025 0.9065197706 0.9018806219 - 0.9016053081 0.9068682790 0.9022467732 - 0.9019693732 0.9072167873 0.9026131034 - 0.9023333788 0.9075652957 0.9029793143 - 0.9026973844 0.9079139233 0.9033455849 - 0.9030613899 0.9082623720 0.9037119150 - 0.9034253955 0.9086108804 0.9040781260 - 0.9037894011 0.9089593887 0.9044443965 - 0.9041534066 0.9093078971 0.9048107266 - 0.9045174122 0.9096564054 0.9051768780 - 0.9048814178 0.9100046158 0.9055432081 - 0.9052454233 0.9103307724 0.9059094787 - 0.9056094289 0.9106569886 0.9062756896 - 0.9059733748 0.9109830856 0.9066420197 - 0.9063374996 0.9113093019 0.9070082903 - 0.9067015052 0.9116355181 0.9073745012 - 0.9070655107 0.9119616747 0.9077407718 - 0.9074295163 0.9122877717 0.9081071019 - 0.9077935219 0.9126139879 0.9084733129 - 0.9081575274 0.9129402041 0.9088395834 - 0.9085214734 0.9132663012 0.9092057943 - 0.9088854790 0.9135925174 0.9095721245 - 0.9092494845 0.9139186740 0.9099383950 - 0.9096134901 0.9142448902 0.9102842808 - 0.9099774957 0.9145709872 0.9106261134 - 0.9103195071 0.9148972034 0.9109678864 - 0.9106599092 0.9152234197 0.9113097191 - 0.9110003710 0.9155495763 0.9116513729 - 0.9113408923 0.9158756733 0.9119932055 - 0.9116814137 0.9162018895 0.9123349786 - 0.9120218158 0.9165281057 0.9126768112 - 0.9123622775 0.9168542027 0.9130185843 - 0.9127027988 0.9171804190 0.9133604169 - 0.9130433202 0.9175065756 0.9137021899 - 0.9133837819 0.9178327918 0.9140440226 - 0.9137241840 0.9181588888 0.9143857956 - 0.9140647054 0.9184851050 0.9147276282 - 0.9144052267 0.9188113213 0.9150692821 - 0.9147456884 0.9191374183 0.9154111147 - 0.9150860906 0.9194635749 0.9157528877 - 0.9154266119 0.9197897911 0.9160947204 - 0.9157670736 0.9201086164 0.9164364934 - 0.9161075950 0.9204139113 0.9167783260 - 0.9164479971 0.9207192063 0.9171200991 - 0.9167885184 0.9210246205 0.9174618721 - 0.9171289802 0.9213299155 0.9178037047 - 0.9174695015 0.9216352105 0.9181454778 - 0.9178099036 0.9219406247 0.9184871912 - 0.9181504250 0.9222459197 0.9188290238 - 0.9184908867 0.9225512743 0.9191707969 - 0.9188314080 0.9228566289 0.9195126295 - 0.9191719294 0.9231619239 0.9198544025 - 0.9195122719 0.9234672785 0.9201831222 - 0.9198527932 0.9237725735 0.9205021858 - 0.9201807976 0.9240779281 0.9208211899 - 0.9204993248 0.9243832827 0.9211401939 - 0.9208179116 0.9246885777 0.9214591980 - 0.9211363792 0.9249939919 0.9217783213 - 0.9214549065 0.9252992868 0.9220973253 - 0.9217733741 0.9256045818 0.9224163294 - 0.9220920205 0.9259099960 0.9227353930 - 0.9224104881 0.9262152910 0.9230543971 - 0.9227290154 0.9265205860 0.9233734012 - 0.9230474830 0.9268260002 0.9236925244 - 0.9233661294 0.9271312952 0.9240115285 - 0.9236845970 0.9274367094 0.9243304729 - 0.9240031242 0.9277420044 0.9246494770 - 0.9243215919 0.9280472994 0.9249686003 - 0.9246401787 0.9283527136 0.9252876043 - 0.9249587059 0.9286580086 0.9256066084 - 0.9252771735 0.9289633036 0.9259256721 - 0.9255957007 0.9292687178 0.9262446761 - 0.9259142876 0.9295740128 0.9265636802 - 0.9262328148 0.9298794270 0.9268828034 - 0.9265512824 0.9301729798 0.9272018075 - 0.9268698096 0.9304589033 0.9275208116 - 0.9271882772 0.9307448268 0.9278398752 - 0.9275069237 0.9310306907 0.9281588793 - 0.9278253913 0.9313166142 0.9284778833 - 0.9281439185 0.9316025972 0.9287968874 - 0.9284623861 0.9318885207 0.9291160107 - 0.9287809730 0.9321743846 0.9294350147 - 0.9290995002 0.9324603081 0.9297540188 - 0.9294180274 0.9327462912 0.9300681949 - 0.9297364950 0.9330322146 0.9303660989 - 0.9300515056 0.9333180785 0.9306640029 - 0.9303495884 0.9336040020 0.9309619069 - 0.9306476116 0.9338899851 0.9312596917 - 0.9309456944 0.9341759086 0.9315575957 - 0.9312437773 0.9344617724 0.9318554997 - 0.9315418005 0.9347476959 0.9321534038 - 0.9318398833 0.9350336194 0.9324513078 - 0.9321380258 0.9353196025 0.9327492118 - 0.9324359894 0.9356055260 0.9330469966 - 0.9327340722 0.9358913898 0.9333449006 - 0.9330320954 0.9361773133 0.9336428046 - 0.9333301783 0.9364632964 0.9339407086 - 0.9336283207 0.9367492199 0.9342386127 - 0.9339262843 0.9370350838 0.9345363975 - 0.9342244267 0.9373210073 0.9348343015 - 0.9345223904 0.9376069903 0.9351322055 - 0.9348204732 0.9378929138 0.9354301095 - 0.9351186156 0.9381787777 0.9357280135 - 0.9354165792 0.9384647012 0.9360259175 - 0.9357147217 0.9387506247 0.9363237023 - 0.9360128045 0.9390366077 0.9366216063 - 0.9363108277 0.9393224716 0.9369195104 - 0.9366089106 0.9396083951 0.9372174144 - 0.9369068742 0.9398943186 0.9375153184 - 0.9372050166 0.9401689172 0.9378132224 - 0.9375030994 0.9404367208 0.9381110072 - 0.9378011227 0.9407045245 0.9384089112 - 0.9380992055 0.9409723878 0.9387068152 - 0.9383972287 0.9412401915 0.9390047193 - 0.9386953115 0.9415081143 0.9393026233 - 0.9389933944 0.9417759180 0.9396004081 - 0.9392914176 0.9420437813 0.9398983121 - 0.9395895004 0.9423115849 0.9401832819 - 0.9398875833 0.9425793886 0.9404615164 - 0.9401736856 0.9428473115 0.9407398105 - 0.9404526949 0.9431151152 0.9410179853 - 0.9407318234 0.9433829784 0.9412962198 - 0.9410107732 0.9436507821 0.9415745139 - 0.9412897825 0.9439185858 0.9418526888 - 0.9415687919 0.9441865087 0.9421309829 - 0.9418478012 0.9444543123 0.9424092174 - 0.9421268106 0.9447221756 0.9426875114 - 0.9424058199 0.9449899793 0.9429656863 - 0.9426848292 0.9452577829 0.9432439804 - 0.9429637790 0.9455257058 0.9435222149 - 0.9432427883 0.9457935095 0.9438003898 - 0.9435217977 0.9460613728 0.9440786839 - 0.9438008070 0.9463291764 0.9443569183 - 0.9440798163 0.9465970993 0.9446352124 - 0.9443588257 0.9468649030 0.9449133873 - 0.9446377754 0.9471327066 0.9451916814 - 0.9449167848 0.9474006295 0.9454699159 - 0.9451957941 0.9476683736 0.9457480907 - 0.9454748034 0.9479362965 0.9460263848 - 0.9457538128 0.9482041001 0.9463046193 - 0.9460328221 0.9484719038 0.9465829134 - 0.9463117719 0.9487398267 0.9468610883 - 0.9465907812 0.9490075707 0.9471393824 - 0.9468697906 0.9492754936 0.9474176168 - 0.9471487999 0.9495432973 0.9476959109 - 0.9474278092 0.9498111010 0.9479740858 - 0.9477068186 0.9500740170 0.9482523203 - 0.9479858279 0.9503250122 0.9485306144 - 0.9482647777 0.9505760074 0.9488087893 - 0.9485437870 0.9508270025 0.9490870833 - 0.9488227963 0.9510779977 0.9493653178 - 0.9491018057 0.9513291121 0.9496436119 - 0.9493808150 0.9515801072 0.9499217868 - 0.9496598244 0.9518311024 0.9501869082 - 0.9499387741 0.9520820975 0.9504470229 - 0.9502040148 0.9523330927 0.9507070184 - 0.9504652023 0.9525840878 0.9509670138 - 0.9507265091 0.9528350830 0.9512270093 - 0.9509876966 0.9530860782 0.9514870048 - 0.9512490034 0.9533370733 0.9517470002 - 0.9515101910 0.9535881281 0.9520069957 - 0.9517714977 0.9538391232 0.9522671103 - 0.9520328045 0.9540901184 0.9525271058 - 0.9522939920 0.9543411136 0.9527871013 - 0.9525552988 0.9545921087 0.9530470967 - 0.9528164864 0.9548432231 0.9533070922 - 0.9530777931 0.9550942183 0.9535670877 - 0.9533389807 0.9553452134 0.9538270831 - 0.9536002874 0.9555962086 0.9540871978 - 0.9538615942 0.9558472037 0.9543471932 - 0.9541227818 0.9560981989 0.9546071887 - 0.9543840885 0.9563491940 0.9548671842 - 0.9546452761 0.9566001892 0.9551271796 - 0.9549065828 0.9568511844 0.9553871751 - 0.9551677704 0.9571021795 0.9556471705 - 0.9554290771 0.9573531747 0.9559072852 - 0.9556903839 0.9576042295 0.9561672807 - 0.9559515715 0.9578552246 0.9564272761 - 0.9562128782 0.9581062198 0.9566872716 - 0.9564741254 0.9583572149 0.9569473267 - 0.9567353725 0.9586083293 0.9572073221 - 0.9569966197 0.9588593245 0.9574673176 - 0.9572579265 0.9591103196 0.9577273726 - 0.9575191736 0.9593613148 0.9579874277 - 0.9577804208 0.9596123099 0.9582474232 - 0.9580417275 0.9598633051 0.9585074186 - 0.9583029151 0.9601072073 0.9587674141 - 0.9585642219 0.9603425264 0.9590274096 - 0.9588255286 0.9605777860 0.9592875242 - 0.9590867162 0.9608132243 0.9595475197 - 0.9593480229 0.9610484838 0.9598075151 - 0.9596092105 0.9612839222 0.9600630999 - 0.9598705173 0.9615191817 0.9603062272 - 0.9601234198 0.9617545009 0.9605492949 - 0.9603682160 0.9619898796 0.9607924223 - 0.9606128931 0.9622251987 0.9610354900 - 0.9608576298 0.9624605179 0.9612786174 - 0.9611024261 0.9626958966 0.9615216851 - 0.9613471031 0.9629312158 0.9617648125 - 0.9615918994 0.9631665945 0.9620078802 - 0.9618365765 0.9634019136 0.9622510076 - 0.9620813727 0.9636371732 0.9624940753 - 0.9623261094 0.9638726115 0.9627372026 - 0.9625709057 0.9641078711 0.9629802704 - 0.9628155828 0.9643431902 0.9632233977 - 0.9630603194 0.9645786285 0.9634665251 - 0.9633051157 0.9648138881 0.9637095928 - 0.9635497928 0.9650493264 0.9639527202 - 0.9637945890 0.9652845860 0.9641957879 - 0.9640393257 0.9655199051 0.9644389153 - 0.9642841220 0.9657552838 0.9646819830 - 0.9645287991 0.9659906030 0.9649251103 - 0.9647734761 0.9662259817 0.9651681781 - 0.9650182724 0.9664613008 0.9654113054 - 0.9652630091 0.9666966200 0.9656543732 - 0.9655078053 0.9669319987 0.9658975005 - 0.9657524824 0.9671673179 0.9661406279 - 0.9659972787 0.9674025774 0.9663836956 - 0.9662420154 0.9676380157 0.9666268229 - 0.9664868116 0.9678732753 0.9668698907 - 0.9667314887 0.9681087136 0.9671130180 - 0.9669762254 0.9683439732 0.9673560858 - 0.9672210217 0.9685792923 0.9675992131 - 0.9674656987 0.9688146710 0.9678422809 - 0.9677104950 0.9690499902 0.9680854082 - 0.9679551721 0.9692854285 0.9683284760 - 0.9682000279 0.9695206881 0.9685716033 - 0.9684447050 0.9697560072 0.9688146710 - 0.9686893821 0.9699913859 0.9690577984 - 0.9689341784 0.9702126980 0.9693009257 - 0.9691789150 0.9704334140 0.9695439935 - 0.9694237113 0.9706541896 0.9697871208 - 0.9696683884 0.9708749056 0.9700282216 - 0.9699131846 0.9710956812 0.9702556133 - 0.9701480269 0.9713163972 0.9704830050 - 0.9703773856 0.9715371728 0.9707103968 - 0.9706068039 0.9717580080 0.9709377885 - 0.9708361030 0.9719787240 0.9711652994 - 0.9710655212 0.9721994996 0.9713926911 - 0.9712948799 0.9724202156 0.9716200829 - 0.9715242982 0.9726409912 0.9718474746 - 0.9717537165 0.9728617072 0.9720749259 - 0.9719830155 0.9730824828 0.9723023176 - 0.9722123742 0.9733031988 0.9725297093 - 0.9724417925 0.9735239744 0.9727571011 - 0.9726712108 0.9737448096 0.9729844928 - 0.9729006290 0.9739655256 0.9732118845 - 0.9731299281 0.9741863012 0.9734392762 - 0.9733592868 0.9744070172 0.9736667275 - 0.9735887051 0.9746277928 0.9738941193 - 0.9738181233 0.9748485088 0.9741215110 - 0.9740474820 0.9750692844 0.9743489027 - 0.9742767811 0.9752901196 0.9745762944 - 0.9745061994 0.9755107760 0.9748036861 - 0.9747356176 0.9757316113 0.9750310779 - 0.9749649763 0.9759523273 0.9752585292 - 0.9751943946 0.9761731029 0.9754859209 - 0.9754236937 0.9763938189 0.9757133722 - 0.9756531119 0.9766145945 0.9759408236 - 0.9758824706 0.9768353105 0.9761682153 - 0.9761118889 0.9770560861 0.9763956070 - 0.9763413072 0.9772769213 0.9766229987 - 0.9765706062 0.9774975777 0.9768503904 - 0.9768000245 0.9777184129 0.9770777822 - 0.9770293832 0.9779391289 0.9773051739 - 0.9772588015 0.9781599045 0.9775326252 - 0.9774881005 0.9783806205 0.9777600169 - 0.9777175188 0.9786013961 0.9779874086 - 0.9779468775 0.9788221717 0.9782148004 - 0.9781762958 0.9790428877 0.9784421921 - 0.9784057140 0.9792637229 0.9786695838 - 0.9786350131 0.9794843793 0.9788969755 - 0.9788643718 0.9797052145 0.9791244268 - 0.9790937901 0.9799258709 0.9793518186 - 0.9793232083 0.9801377058 0.9795792103 - 0.9795526266 0.9803448915 0.9798066020 - 0.9797819257 0.9805520177 0.9800319076 - 0.9800106287 0.9807592034 0.9802446961 - 0.9802256823 0.9809663892 0.9804576039 - 0.9804407954 0.9811735749 0.9806703925 - 0.9806559086 0.9813808203 0.9808831811 - 0.9808710217 0.9815880060 0.9810960889 - 0.9810860753 0.9817951918 0.9813088775 - 0.9813011289 0.9820023775 0.9815217853 - 0.9815161824 0.9822095037 0.9817345738 - 0.9817312956 0.9824166894 0.9819474816 - 0.9819464087 0.9826238751 0.9821602702 - 0.9821615219 0.9828311205 0.9823731780 - 0.9823765755 0.9830383062 0.9825860262 - 0.9825916886 0.9832454920 0.9827988148 - 0.9828066826 0.9834526777 0.9830117226 - 0.9830217957 0.9836598039 0.9832245111 - 0.9832369089 0.9838669896 0.9834374189 - 0.9834520221 0.9840741754 0.9836502075 - 0.9836670756 0.9842814207 0.9838631153 - 0.9838821888 0.9844886065 0.9840759039 - 0.9840973020 0.9846957922 0.9842888117 - 0.9843124151 0.9849029779 0.9845016003 - 0.9845274091 0.9851101041 0.9847143888 - 0.9847425222 0.9853172898 0.9849272966 - 0.9849575758 0.9855244756 0.9851400852 - 0.9851726890 0.9857317209 0.9853529930 - 0.9853878021 0.9859389067 0.9855657816 - 0.9856029153 0.9861460924 0.9857786894 - 0.9858180285 0.9863532782 0.9859914780 - 0.9860330224 0.9865604043 0.9862043262 - 0.9862480760 0.9867675900 0.9864171743 - 0.9864631891 0.9869747758 0.9866300225 - 0.9866783023 0.9871820211 0.9868428707 - 0.9868934155 0.9873892069 0.9870557189 - 0.9871085286 0.9875963926 0.9872686267 - 0.9873235822 0.9878035784 0.9874814153 - 0.9875385761 0.9880108237 0.9876943231 - 0.9877536893 0.9882178903 0.9879071116 - 0.9879688025 0.9884250760 0.9881199002 - 0.9881839156 0.9886323214 0.9883328080 - 0.9883990288 0.9888395071 0.9885455966 - 0.9886140823 0.9890466928 0.9887585044 - 0.9888291955 0.9892538786 0.9889712930 - 0.9890443087 0.9894611239 0.9891842008 - 0.9892593026 0.9896681905 0.9893969893 - 0.9894744158 0.9898753762 0.9896098971 - 0.9896895289 0.9900776148 0.9898226857 - 0.9899045825 0.9902721047 0.9900333285 - 0.9901123047 0.9904667139 0.9902325869 - 0.9903141260 0.9906613231 0.9904320240 - 0.9905158877 0.9908558130 0.9906312823 - 0.9907177091 0.9910504222 0.9908306003 - 0.9909194112 0.9912449121 0.9910299778 - 0.9911211729 0.9914395213 0.9912292957 - 0.9913229942 0.9916340113 0.9914286137 - 0.9915248156 0.9918286204 0.9916279912 - 0.9917265773 0.9920232296 0.9918273091 - 0.9919283986 0.9922177196 0.9920266271 - 0.9921302199 0.9924123287 0.9922260046 - 0.9923319817 0.9926068187 0.9924253225 - 0.9925338030 0.9928014278 0.9926245809 - 0.9927356243 0.9929959178 0.9928240180 - 0.9929373264 0.9931905270 0.9930232763 - 0.9931390882 0.9933850765 0.9932225943 - 0.9933409095 0.9935796261 0.9934219718 - 0.9935426712 0.9937741756 0.9936212897 - 0.9937444925 0.9939687252 0.9938206077 - 0.9939463139 0.9941632748 0.9940199852 - 0.9941480756 0.9943578243 0.9942193031 - 0.9943498969 0.9945523739 0.9944186211 - 0.9945517182 0.9947469831 0.9946179986 - 0.9947534800 0.9949414730 0.9948173165 - 0.9949551821 0.9951360822 0.9950165749 - 0.9951570034 0.9953305721 0.9952160120 - 0.9953588247 0.9955251813 0.9954152703 - 0.9955605865 0.9957196712 0.9956145883 - 0.9957624078 0.9959142804 0.9958140254 - 0.9959642291 0.9961088896 0.9960132837 - 0.9961659908 0.9963033795 0.9962127209 - 0.9963678122 0.9964979887 0.9964119792 - 0.9965695739 0.9966924787 0.9966112971 - 0.9967713952 0.9968870878 0.9968106747 - 0.9969730973 0.9970815778 0.9970099926 - 0.9971749187 0.9972761869 0.9972093105 - 0.9973766804 0.9974707961 0.9974086881 - 0.9975785017 0.9976652861 0.9976080060 - 0.9977803230 0.9978598952 0.9978073239 - 0.9979820848 0.9980543852 0.9980067015 - 0.9981839061 0.9982489944 0.9982060194 - 0.9983857274 0.9984434843 0.9984052777 - 0.9985874891 0.9986380935 0.9986047149 - 0.9987893105 0.9988327026 0.9988039732 - 0.9989910126 0.9990271926 0.9990032911 - 0.9991927743 0.9992218018 0.9992027283 - 0.9993945956 0.9994162917 0.9994019866 - 0.9995964170 0.9996109009 0.9996013045 - 0.9997981787 0.9998053908 0.9998006821 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0008270687 0.0012806139 0.0007092019 + 0.0016541370 0.0025612270 0.0014184040 + 0.0024812061 0.0038418409 0.0021276060 + 0.0033082750 0.0051224548 0.0028368081 + 0.0041353442 0.0064030681 0.0035460100 + 0.0049624122 0.0076836818 0.0042552119 + 0.0057894811 0.0089642964 0.0049644141 + 0.0066165500 0.0102737201 0.0056736148 + 0.0074436190 0.0117049702 0.0063828169 + 0.0082706874 0.0131362099 0.0070920191 + 0.0090977559 0.0145674599 0.0078012212 + 0.0099248253 0.0159987099 0.0085104229 + 0.0108320098 0.0174299609 0.0092196250 + 0.0117472196 0.0188612100 0.0099288272 + 0.0126624200 0.0203272700 0.0107055604 + 0.0135776196 0.0219289195 0.0114898197 + 0.0144928200 0.0235305596 0.0122740902 + 0.0154080195 0.0251322109 0.0130583504 + 0.0163232200 0.0267338492 0.0138426097 + 0.0172384195 0.0283354893 0.0146268802 + 0.0181536209 0.0299371406 0.0154111404 + 0.0190688204 0.0317234285 0.0161953997 + 0.0199840199 0.0335172787 0.0169796702 + 0.0209966209 0.0353111215 0.0177639294 + 0.0220109392 0.0371049605 0.0185481999 + 0.0230252706 0.0388987996 0.0193324592 + 0.0240395907 0.0407759696 0.0201293100 + 0.0250539202 0.0427856296 0.0209981594 + 0.0260682497 0.0447952896 0.0218669996 + 0.0270825699 0.0468049496 0.0227358509 + 0.0280968994 0.0488146096 0.0236046892 + 0.0291112196 0.0509230010 0.0244735405 + 0.0301393494 0.0531734005 0.0253423806 + 0.0312651210 0.0554237999 0.0262112301 + 0.0323909000 0.0576741993 0.0270800795 + 0.0335166790 0.0599245988 0.0279489197 + 0.0346424617 0.0624321401 0.0288177691 + 0.0357682407 0.0649486035 0.0296866093 + 0.0368940197 0.0674650520 0.0306164194 + 0.0380197987 0.0699815080 0.0315806195 + 0.0391455814 0.0727861822 0.0325448215 + 0.0403015316 0.0755929798 0.0335090198 + 0.0415524915 0.0783997923 0.0344732217 + 0.0428034514 0.0813405216 0.0354374312 + 0.0440544114 0.0844588801 0.0364016294 + 0.0453053713 0.0875772387 0.0373658314 + 0.0465563312 0.0907685384 0.0383300297 + 0.0478072912 0.0942139179 0.0392942317 + 0.0490582511 0.0976593122 0.0402872488 + 0.0503439009 0.1012116969 0.0413589515 + 0.0517351814 0.1049906015 0.0424306504 + 0.0531264618 0.1087696031 0.0435023494 + 0.0545177385 0.1127699018 0.0445740595 + 0.0559090301 0.1168769971 0.0456457585 + 0.0573003106 0.1210578978 0.0467174612 + 0.0586915910 0.1254729033 0.0477891602 + 0.0600922108 0.1298878938 0.0488608703 + 0.0616402999 0.1345676035 0.0499325693 + 0.0631883815 0.1392543018 0.0511177592 + 0.0647364631 0.1441261023 0.0523105711 + 0.0662845522 0.1490330994 0.0535033792 + 0.0678326264 0.1540661007 0.0546961986 + 0.0693807229 0.1591300964 0.0558890104 + 0.0710334405 0.1642663032 0.0570818186 + 0.0727559403 0.1694173962 0.0582746305 + 0.0744784400 0.1745833009 0.0594674386 + 0.0762009397 0.1797512025 0.0607356615 + 0.0779234320 0.1848731041 0.0620646887 + 0.0796459317 0.1899926066 0.0633937269 + 0.0815215707 0.1950089037 0.0647227690 + 0.0834368318 0.2000242025 0.0660518110 + 0.0853521004 0.2048937976 0.0673808381 + 0.0872673616 0.2097633928 0.0687098801 + 0.0891826227 0.2144646943 0.0700433925 + 0.0912189633 0.2191573977 0.0715252534 + 0.0933454409 0.2236897051 0.0730071068 + 0.0954719111 0.2281869054 0.0744889826 + 0.0975983888 0.2325620949 0.0759708434 + 0.0997248664 0.2368548959 0.0774526969 + 0.1020506024 0.2410928011 0.0789345577 + 0.1044057980 0.2451802045 0.0804644004 + 0.1067610979 0.2492675036 0.0821170136 + 0.1091163978 0.2531898916 0.0837696269 + 0.1116243005 0.2570762932 0.0854222402 + 0.1142238006 0.2609150112 0.0870748535 + 0.1168233976 0.2646088004 0.0887274593 + 0.1194228977 0.2683025002 0.0904237032 + 0.1222214997 0.2718980014 0.0922659934 + 0.1250769049 0.2754099071 0.0941082835 + 0.1279322952 0.2789216936 0.0959505737 + 0.1308598965 0.2823159099 0.0977928713 + 0.1339771003 0.2856579125 0.0996351615 + 0.1370943040 0.2889998853 0.1016450971 + 0.1402291059 0.2922315896 0.1036963984 + 0.1436063051 0.2954162955 0.1057477966 + 0.1469835043 0.2986010015 0.1077990979 + 0.1503873020 0.3017044067 0.1098503992 + 0.1540133953 0.3047442138 0.1121131033 + 0.1576395929 0.3077841103 0.1143923029 + 0.1613450944 0.3107880056 0.1166715994 + 0.1651988029 0.3136951029 0.1189509034 + 0.1690524071 0.3166022003 0.1213624030 + 0.1730536968 0.3195092976 0.1238868013 + 0.1771031022 0.3223156929 0.1264111996 + 0.1811967939 0.3251017034 0.1289356053 + 0.1854016036 0.3278875947 0.1316096932 + 0.1896062940 0.3306469023 0.1343929023 + 0.1939094067 0.3333224058 0.1371762007 + 0.1982226968 0.3359979987 0.1399594992 + 0.2025707066 0.3386735022 0.1430062056 + 0.2069430053 0.3412984908 0.1460568011 + 0.2113184035 0.3438735902 0.1491073966 + 0.2157011032 0.3464488089 0.1523478031 + 0.2200832069 0.3490239978 0.1556666940 + 0.2244319022 0.3515425920 0.1589855999 + 0.2287805974 0.3540267050 0.1624847949 + 0.2330776006 0.3565106988 0.1660633981 + 0.2373543978 0.3589948118 0.1696420014 + 0.2415924072 0.3614296019 0.1734369993 + 0.2457672954 0.3638310134 0.1772560030 + 0.2499420941 0.3662323952 0.1811341941 + 0.2539947033 0.3686338067 0.1851633936 + 0.2580456138 0.3710030019 0.1891925931 + 0.2620247900 0.3733294904 0.1933576018 + 0.2659370899 0.3756558895 0.1975566000 + 0.2698495090 0.3779824078 0.2018067986 + 0.2736209035 0.3802998960 0.2061281949 + 0.2773866057 0.3825586140 0.2104569972 + 0.2811067104 0.3848172128 0.2148493975 + 0.2847231030 0.3870758116 0.2192417979 + 0.2883394957 0.3893344998 0.2236506939 + 0.2918758094 0.3915497959 0.2280631959 + 0.2953441143 0.3937469125 0.2324603051 + 0.2988123894 0.3959440887 0.2368455976 + 0.3021861017 0.3981412947 0.2412119061 + 0.3055104911 0.4003298879 0.2455295026 + 0.3088349998 0.4024715126 0.2498470992 + 0.3120698929 0.4046129882 0.2540681958 + 0.3152567148 0.4067544937 0.2582857013 + 0.3184433877 0.4088960886 0.2624298036 + 0.3215635121 0.4110131860 0.2665235102 + 0.3246200085 0.4131042957 0.2705960870 + 0.3276765049 0.4151954055 0.2745501995 + 0.3307037055 0.4172866046 0.2785041928 + 0.3336381018 0.4193777144 0.2823660076 + 0.3365724981 0.4214367867 0.2861714959 + 0.3395070136 0.4234822094 0.2899771035 + 0.3423467875 0.4255276918 0.2936319113 + 0.3451676071 0.4275731146 0.2972859144 + 0.3479883969 0.4296185970 0.3009010851 + 0.3507789969 0.4316303134 0.3044045866 + 0.3534946144 0.4336344004 0.3079079986 + 0.3562101126 0.4356383979 0.3113526106 + 0.3589256108 0.4376423955 0.3147099912 + 0.3615824878 0.4396465123 0.3180673122 + 0.3642010987 0.4416194856 0.3213653862 + 0.3668195903 0.4435859025 0.3245829940 + 0.3694381118 0.4455522895 0.3278006911 + 0.3719865084 0.4475187063 0.3309766054 + 0.3745158017 0.4494850039 0.3340623081 + 0.3770450950 0.4514260888 0.3371481001 + 0.3795743883 0.4533582032 0.3402245045 + 0.3820356131 0.4552902877 0.3431868851 + 0.3844830990 0.4572224021 0.3461492956 + 0.3869304955 0.4591544867 0.3491117060 + 0.3893780112 0.4610689878 0.3519938886 + 0.3917695880 0.4629696906 0.3548417091 + 0.3941422105 0.4648705125 0.3576894999 + 0.3965147138 0.4667713046 0.3605172932 + 0.3988873065 0.4686720073 0.3632592857 + 0.4012235105 0.4705640972 0.3660013080 + 0.4035275877 0.4724361002 0.3687433004 + 0.4058316052 0.4743080139 0.3714326024 + 0.4081357121 0.4761799872 0.3740772903 + 0.4104278982 0.4780519009 0.3767220080 + 0.4126693010 0.4799239039 0.3793666065 + 0.4149107933 0.4817703068 0.3819434941 + 0.4171522856 0.4836156964 0.3844990134 + 0.4193938076 0.4854609966 0.3870545030 + 0.4215936065 0.4873062968 0.3896099925 + 0.4237779975 0.4891516864 0.3920964897 + 0.4259622991 0.4909836054 0.3945704997 + 0.4281466901 0.4928041101 0.3970445096 + 0.4303230941 0.4946246147 0.3995184898 + 0.4324553013 0.4964450002 0.4019325972 + 0.4345873892 0.4982655048 0.4043321908 + 0.4367195070 0.5000848770 0.4067319036 + 0.4388515949 0.5018820167 0.4091314971 + 0.4409618080 0.5036790967 0.4114879966 + 0.4430460930 0.5054761171 0.4138199091 + 0.4451304972 0.5072731972 0.4161519110 + 0.4472149014 0.5090702772 0.4184837937 + 0.4492993057 0.5108565092 0.4207941890 + 0.4513547122 0.5126311779 0.4230645001 + 0.4533953965 0.5144059062 0.4253348112 + 0.4554361105 0.5161805749 0.4276050925 + 0.4574767947 0.5179553032 0.4298754930 + 0.4595173895 0.5197299719 0.4320927858 + 0.4615274966 0.5214862823 0.4343070984 + 0.4635280967 0.5232393146 0.4365214109 + 0.4655286074 0.5249922872 0.4387356937 + 0.4675292075 0.5267453194 0.4409281015 + 0.4695298076 0.5284982920 0.4430915117 + 0.4715020955 0.5302482843 0.4452548027 + 0.4734657109 0.5319799185 0.4474182129 + 0.4754293859 0.5337114930 0.4495815039 + 0.4773930013 0.5354431868 0.4517075121 + 0.4793567061 0.5371748209 0.4538244009 + 0.4812973142 0.5389063954 0.4559414089 + 0.4832268059 0.5406302214 0.4580583870 + 0.4851562977 0.5423403978 0.4601719081 + 0.4870857894 0.5440505743 0.4622465968 + 0.4890153110 0.5457608104 0.4643214047 + 0.4909291863 0.5474711061 0.4663960934 + 0.4928269088 0.5491812825 0.4684709013 + 0.4947246015 0.5508800745 0.4705354869 + 0.4966222942 0.5525686145 0.4725716114 + 0.4985199869 0.5542569757 0.4746077955 + 0.5004112124 0.5559453964 0.4766440094 + 0.5022791028 0.5576338172 0.4786801040 + 0.5041469932 0.5593221784 0.4807038903 + 0.5060148835 0.5609971285 0.4827046990 + 0.5078828931 0.5626630187 0.4847055078 + 0.5097507834 0.5643289089 0.4867062867 + 0.5115942955 0.5659946799 0.4887070954 + 0.5134339929 0.5676606297 0.4906964004 + 0.5152738094 0.5693264008 0.4926646054 + 0.5171135068 0.5709782243 0.4946328104 + 0.5189533234 0.5726205111 0.4966011047 + 0.5207813978 0.5742626786 0.4985693097 + 0.5225942135 0.5759049058 0.5005292892 + 0.5244070292 0.5775471926 0.5024672747 + 0.5262197852 0.5791894197 0.5044053197 + 0.5280326009 0.5808190107 0.5063433051 + 0.5298454762 0.5824362040 0.5082812905 + 0.5316343904 0.5840533972 0.5102161169 + 0.5334210992 0.5856705904 0.5121259093 + 0.5352079272 0.5872877836 0.5140355825 + 0.5369945765 0.5889049768 0.5159453154 + 0.5387812853 0.5905135870 0.5178549886 + 0.5405598879 0.5921041965 0.5197647810 + 0.5423210263 0.5936946869 0.5216510296 + 0.5440822244 0.5952851772 0.5235339999 + 0.5458433032 0.5968757272 0.5254169106 + 0.5476043820 0.5984662175 0.5272998810 + 0.5493655801 0.6000556946 0.5291827917 + 0.5511103868 0.6016175747 0.5310512185 + 0.5528460741 0.6031795144 0.5329084992 + 0.5545817018 0.6047413945 0.5347657800 + 0.5563173890 0.6063032746 0.5366231203 + 0.5580530763 0.6078652143 0.5384802818 + 0.5597887039 0.6094270945 0.5403330922 + 0.5615018010 0.6109694839 0.5421653986 + 0.5632117987 0.6125006080 0.5439975858 + 0.5649217963 0.6140317917 0.5458298922 + 0.5666316748 0.6155629158 0.5476621985 + 0.5683416724 0.6170939803 0.5494945049 + 0.5700508952 0.6186252236 0.5513088703 + 0.5717346072 0.6201528907 0.5531163812 + 0.5734183192 0.6216509938 0.5549240112 + 0.5751019120 0.6231490970 0.5567315221 + 0.5767856240 0.6246472001 0.5585390925 + 0.5784692764 0.6261453032 0.5603417754 + 0.5801506042 0.6276432872 0.5621244907 + 0.5818070769 0.6291413903 0.5639072061 + 0.5834636092 0.6306244135 0.5656898022 + 0.5851200819 0.6320869923 0.5674725175 + 0.5867766142 0.6335496902 0.5692551136 + 0.5884330869 0.6350123286 0.5710229874 + 0.5900881290 0.6364750266 0.5727801919 + 0.5917161703 0.6379376054 0.5745375156 + 0.5933442712 0.6394003034 0.5762947202 + 0.5949723721 0.6408405900 0.5780519247 + 0.5966004729 0.6422653794 0.5798091292 + 0.5982285738 0.6436902285 0.5815427899 + 0.5998566747 0.6451150179 0.5832737088 + 0.6014575958 0.6465398073 0.5850045085 + 0.6030557752 0.6479645967 0.5867353082 + 0.6046540737 0.6493893862 0.5884662271 + 0.6062523127 0.6507912278 0.5901939273 + 0.6078504920 0.6521757841 0.5918970108 + 0.6094486713 0.6535604000 0.5936000943 + 0.6110262275 0.6549450159 0.5953032970 + 0.6125928164 0.6563295126 0.5970063806 + 0.6141594052 0.6577141285 0.5987095237 + 0.6157259941 0.6590986848 0.6004055142 + 0.6172925830 0.6604683995 0.6020792723 + 0.6188592911 0.6618105173 0.6037530899 + 0.6204167008 0.6631525755 0.6054267883 + 0.6219497919 0.6644946933 0.6071006060 + 0.6234828234 0.6658368111 0.6087743044 + 0.6250159144 0.6671789289 0.6104397178 + 0.6265488863 0.6685209274 0.6120821238 + 0.6280819774 0.6698629856 0.6137245297 + 0.6296150088 0.6711651087 0.6153669953 + 0.6311213970 0.6724625826 0.6170094013 + 0.6326187849 0.6737599969 0.6186518073 + 0.6341162920 0.6750574708 0.6202881932 + 0.6356136799 0.6763550043 0.6218971014 + 0.6371111274 0.6776524782 0.6235058904 + 0.6386085153 0.6789500117 0.6251147985 + 0.6401032805 0.6802387238 0.6267235875 + 0.6415628791 0.6814897060 0.6283324957 + 0.6430225968 0.6827408075 0.6299412847 + 0.6444821954 0.6839917898 0.6315155029 + 0.6459417939 0.6852428913 0.6330882907 + 0.6474015117 0.6864938736 0.6346611977 + 0.6488611102 0.6877449751 0.6362339854 + 0.6503120065 0.6889960170 0.6378068924 + 0.6517316103 0.6902375817 0.6393796802 + 0.6531512737 0.6914405823 0.6409292221 + 0.6545709968 0.6926437020 0.6424635053 + 0.6559906006 0.6938467026 0.6439977884 + 0.6574103236 0.6950497031 0.6455320716 + 0.6588299274 0.6962528229 0.6470664144 + 0.6602421999 0.6974558234 0.6486006975 + 0.6616197824 0.6986588836 0.6501314044 + 0.6629973054 0.6998618841 0.6516245008 + 0.6643748879 0.7010213137 0.6531175971 + 0.6657524109 0.7021750808 0.6546106935 + 0.6671299934 0.7033289075 0.6561039090 + 0.6685075164 0.7044826746 0.6575970054 + 0.6698850989 0.7056365013 0.6590901017 + 0.6712222099 0.7067903280 0.6605660915 + 0.6725556254 0.7079440951 0.6620154977 + 0.6738889813 0.7090979218 0.6634649038 + 0.6752223969 0.7102407813 0.6649141908 + 0.6765558124 0.7113444209 0.6663635969 + 0.6778892279 0.7124481201 0.6678130031 + 0.6792225838 0.7135518193 0.6692622900 + 0.6805368066 0.7146555185 0.6706889868 + 0.6818242073 0.7157592177 0.6720920801 + 0.6831117272 0.7168627977 0.6734951735 + 0.6843991280 0.7179664969 0.6748983264 + 0.6856865287 0.7190701962 0.6763014197 + 0.6869739294 0.7201659083 0.6777045727 + 0.6882612705 0.7212188840 0.6791077256 + 0.6895486712 0.7222719789 0.6804931164 + 0.6908051968 0.7233250737 0.6818476915 + 0.6920449734 0.7243781090 0.6832023263 + 0.6932848096 0.7254312038 0.6845569015 + 0.6945245862 0.7264842987 0.6859114766 + 0.6957643032 0.7275372744 0.6872661114 + 0.6970040798 0.7285904288 0.6886206269 + 0.6982439160 0.7296434045 0.6899752021 + 0.6994836926 0.7306630015 0.6912801862 + 0.7006949186 0.7316653132 0.6925842166 + 0.7018857002 0.7326676250 0.6938881874 + 0.7030766010 0.7336698771 0.6951922178 + 0.7042673826 0.7346723080 0.6964963078 + 0.7054582238 0.7356746197 0.6978002787 + 0.7066490054 0.7366768718 0.6991043091 + 0.7078399062 0.7376793027 0.7003920078 + 0.7090306878 0.7386816144 0.7016437054 + 0.7102121711 0.7396839261 0.7028955221 + 0.7113531232 0.7406517267 0.7041472197 + 0.7124938965 0.7416034937 0.7053989768 + 0.7136347294 0.7425553799 0.7066507936 + 0.7147756219 0.7435072064 0.7079024911 + 0.7159163952 0.7444590926 0.7091543078 + 0.7170572281 0.7454109192 0.7103886008 + 0.7181981206 0.7463628054 0.7115867734 + 0.7193388939 0.7473145723 0.7127850056 + 0.7204583883 0.7482665181 0.7139831781 + 0.7215486169 0.7492182851 0.7151812911 + 0.7226387858 0.7501612902 0.7163795233 + 0.7237290144 0.7510632277 0.7175776958 + 0.7248191237 0.7519652247 0.7187758088 + 0.7259092927 0.7528672218 0.7199739814 + 0.7269995213 0.7537690997 0.7211189270 + 0.7280896902 0.7546710968 0.7222625017 + 0.7291797996 0.7555730939 0.7234061956 + 0.7302573919 0.7564749718 0.7245498896 + 0.7312965989 0.7573770285 0.7256935835 + 0.7323358059 0.7582790256 0.7268372178 + 0.7333750129 0.7591809034 0.7279809117 + 0.7344142199 0.7600783706 0.7291246057 + 0.7354534268 0.7609313726 0.7302554250 + 0.7364925742 0.7617843747 0.7313441038 + 0.7375317812 0.7626373768 0.7324327826 + 0.7385709882 0.7634903789 0.7335215211 + 0.7396101952 0.7643433809 0.7346101999 + 0.7406175137 0.7651963830 0.7356988788 + 0.7416058183 0.7660493851 0.7367876172 + 0.7425940037 0.7669023871 0.7378762960 + 0.7435823083 0.7677553892 0.7389649749 + 0.7445706129 0.7686083913 0.7400509715 + 0.7455587983 0.7694613934 0.7410846949 + 0.7465471029 0.7702968121 0.7421182990 + 0.7475354075 0.7711020112 0.7431520224 + 0.7485235929 0.7719072104 0.7441856861 + 0.7495118976 0.7727124095 0.7452194095 + 0.7504745722 0.7735176086 0.7462530732 + 0.7514122725 0.7743229270 0.7472867966 + 0.7523499727 0.7751281261 0.7483205199 + 0.7532876730 0.7759333253 0.7493541837 + 0.7542254925 0.7767385244 0.7503672838 + 0.7551631927 0.7775437236 0.7513464093 + 0.7561008930 0.7783489823 0.7523254156 + 0.7570385933 0.7791541815 0.7533044815 + 0.7579764128 0.7799593806 0.7542834878 + 0.7589141130 0.7807205915 0.7552626133 + 0.7598518133 0.7814794779 0.7562416792 + 0.7607476115 0.7822383046 0.7572206855 + 0.7616354823 0.7829971910 0.7581998110 + 0.7625234127 0.7837560773 0.7591788173 + 0.7634112835 0.7845150232 0.7601491809 + 0.7642992139 0.7852737904 0.7610744238 + 0.7651870847 0.7860326767 0.7619994879 + 0.7660750151 0.7867916226 0.7629246712 + 0.7669628859 0.7875505090 0.7638499141 + 0.7678508162 0.7883092761 0.7647750974 + 0.7687386870 0.7890682220 0.7657002807 + 0.7696266174 0.7898271084 0.7666255236 + 0.7704861760 0.7905514240 0.7675507069 + 0.7713252902 0.7912656069 0.7684757710 + 0.7721645236 0.7919796705 0.7694010139 + 0.7730035782 0.7926939130 0.7703076005 + 0.7738426924 0.7934079766 0.7711800933 + 0.7746818066 0.7941222191 0.7720525265 + 0.7755209208 0.7948362827 0.7729249001 + 0.7763599753 0.7955505252 0.7737973928 + 0.7771990895 0.7962645888 0.7746698260 + 0.7780382037 0.7969787717 0.7755423188 + 0.7788773179 0.7976928949 0.7764146924 + 0.7797163725 0.7984070778 0.7772871852 + 0.7805240154 0.7991213202 0.7781596184 + 0.7813156247 0.7998353839 0.7790321112 + 0.7821071744 0.8005164862 0.7799044847 + 0.7828987241 0.8011876941 0.7807313204 + 0.7836902738 0.8018589020 0.7815523744 + 0.7844818830 0.8025301099 0.7823736072 + 0.7852734923 0.8032013178 0.7831947207 + 0.7860651016 0.8038725853 0.7840157747 + 0.7868565917 0.8045437932 0.7848370075 + 0.7876482010 0.8052150011 0.7856581211 + 0.7884398103 0.8058862090 0.7864792943 + 0.7892314196 0.8065574169 0.7873004079 + 0.7900215983 0.8072286248 0.7881215811 + 0.7907671928 0.8078997731 0.7889426947 + 0.7915127277 0.8085709810 0.7897638083 + 0.7922583222 0.8092421889 0.7905495763 + 0.7930037975 0.8099133968 0.7913212180 + 0.7937493920 0.8105489016 0.7920926809 + 0.7944949269 0.8111789823 0.7928642035 + 0.7952405214 0.8118091822 0.7936357856 + 0.7959859967 0.8124393821 0.7944073081 + 0.7967315912 0.8130695224 0.7951787710 + 0.7974771261 0.8136997223 0.7959504128 + 0.7982227206 0.8143298030 0.7967218757 + 0.7989683151 0.8149600029 0.7974933982 + 0.7997137904 0.8155900836 0.7982649803 + 0.8004320264 0.8162202835 0.7990365028 + 0.8011332154 0.8168504238 0.7998080254 + 0.8018345237 0.8174806237 0.8005437255 + 0.8025357127 0.8181107044 0.8012676239 + 0.8032369018 0.8187409043 0.8019914031 + 0.8039380908 0.8193711042 0.8027151823 + 0.8046392798 0.8200011253 0.8034390807 + 0.8053405285 0.8205922246 0.8041629195 + 0.8060417175 0.8211833239 0.8048866987 + 0.8067429066 0.8217743039 0.8056105971 + 0.8074440956 0.8223654032 0.8063343763 + 0.8081452847 0.8229563832 0.8070582151 + 0.8088464737 0.8235474825 0.8077819943 + 0.8095477223 0.8241385818 0.8085058928 + 0.8102337718 0.8247296214 0.8092296720 + 0.8108925223 0.8253207207 0.8099535108 + 0.8115512133 0.8259118199 0.8106346130 + 0.8122099042 0.8265028000 0.8113127947 + 0.8128685951 0.8270938993 0.8119909763 + 0.8135272264 0.8276848793 0.8126692176 + 0.8141859174 0.8282759786 0.8133473992 + 0.8148446083 0.8288670778 0.8140255809 + 0.8155032992 0.8294581175 0.8147038221 + 0.8161619902 0.8300461173 0.8153820038 + 0.8168206215 0.8306000829 0.8160601258 + 0.8174793124 0.8311539888 0.8167383075 + 0.8181380033 0.8317080140 0.8174164891 + 0.8187966943 0.8322619796 0.8180946708 + 0.8194553852 0.8328160048 0.8187729120 + 0.8201069832 0.8333699107 0.8194510937 + 0.8207250834 0.8339238763 0.8201209903 + 0.8213431239 0.8344779015 0.8207557201 + 0.8219612241 0.8350318074 0.8213903904 + 0.8225793242 0.8355857730 0.8220251203 + 0.8231973052 0.8361397982 0.8226597905 + 0.8238154054 0.8366938233 0.8232945204 + 0.8244335055 0.8372477293 0.8239293098 + 0.8250514865 0.8378016949 0.8245639801 + 0.8256695867 0.8383557200 0.8251987100 + 0.8262876868 0.8389096260 0.8258333802 + 0.8269057274 0.8394635916 0.8264681101 + 0.8275238276 0.8400164843 0.8271027803 + 0.8281419277 0.8405354023 0.8277375102 + 0.8287599087 0.8410543203 0.8283722997 + 0.8293780088 0.8415731192 0.8290070295 + 0.8299961090 0.8420919776 0.8296416998 + 0.8305758238 0.8426108956 0.8302584291 + 0.8311551809 0.8431298137 0.8308519125 + 0.8317345977 0.8436486721 0.8314453959 + 0.8323140740 0.8441675901 0.8320388794 + 0.8328934908 0.8446865082 0.8326324224 + 0.8334729075 0.8452054262 0.8332257867 + 0.8340523839 0.8457242846 0.8338193297 + 0.8346318007 0.8462432027 0.8344128132 + 0.8352112174 0.8467620015 0.8350062966 + 0.8357906938 0.8472809196 0.8355997205 + 0.8363701105 0.8477997780 0.8361932039 + 0.8369495273 0.8483186960 0.8367866874 + 0.8375290036 0.8488376141 0.8373801708 + 0.8381084204 0.8493564725 0.8379737139 + 0.8386877775 0.8498753905 0.8385670781 + 0.8392673135 0.8503690958 0.8391606212 + 0.8398466706 0.8508548737 0.8397541046 + 0.8403992057 0.8513407111 0.8403248191 + 0.8409420252 0.8518266082 0.8408793211 + 0.8414847851 0.8523123860 0.8414338231 + 0.8420276046 0.8527982235 0.8419883251 + 0.8425704837 0.8532840014 0.8425428271 + 0.8431133032 0.8537697792 0.8430973291 + 0.8436561227 0.8542556167 0.8436517715 + 0.8441988826 0.8547413945 0.8442062736 + 0.8447417021 0.8552271724 0.8447607756 + 0.8452845216 0.8557130098 0.8453152776 + 0.8458272815 0.8561987877 0.8458697796 + 0.8463702202 0.8566846251 0.8464242816 + 0.8469129801 0.8571704030 0.8469787836 + 0.8474557996 0.8576561809 0.8475332856 + 0.8479986191 0.8581420183 0.8480879068 + 0.8485413790 0.8586277962 0.8486424088 + 0.8490841985 0.8591135740 0.8491969109 + 0.8496270180 0.8595994115 0.8497514129 + 0.8501589894 0.8600798249 0.8502855897 + 0.8506671786 0.8605344296 0.8508033752 + 0.8511754274 0.8609890938 0.8513212204 + 0.8516836166 0.8614438176 0.8518390059 + 0.8521918058 0.8618984818 0.8523567915 + 0.8526999950 0.8623532057 0.8528745770 + 0.8532081842 0.8628078103 0.8533924222 + 0.8537163734 0.8632624745 0.8539102077 + 0.8542246819 0.8637171984 0.8544279933 + 0.8547328711 0.8641719222 0.8549457788 + 0.8552411199 0.8646265268 0.8554636240 + 0.8557493091 0.8650811911 0.8559814095 + 0.8562574983 0.8655359149 0.8564991951 + 0.8567656875 0.8659905791 0.8570169806 + 0.8572738767 0.8664453030 0.8575347066 + 0.8577821255 0.8668999076 0.8580524921 + 0.8582903147 0.8673545718 0.8585702777 + 0.8587985039 0.8678092957 0.8590881228 + 0.8593066931 0.8682640195 0.8596059084 + 0.8598148823 0.8687186241 0.8601155281 + 0.8603023887 0.8691732883 0.8605988026 + 0.8607779741 0.8696280122 0.8610820770 + 0.8612535000 0.8700773716 0.8615654111 + 0.8617290854 0.8705028296 0.8620486856 + 0.8622046709 0.8709282875 0.8625320196 + 0.8626803160 0.8713536859 0.8630152941 + 0.8631559014 0.8717792034 0.8634986281 + 0.8636314869 0.8722046018 0.8639819026 + 0.8641070724 0.8726301193 0.8644651771 + 0.8645827174 0.8730555177 0.8649485111 + 0.8650581837 0.8734809756 0.8654317856 + 0.8655338287 0.8739064932 0.8659151793 + 0.8660094142 0.8743318915 0.8663985133 + 0.8664849997 0.8747574091 0.8668817878 + 0.8669605851 0.8751828074 0.8673651218 + 0.8674361706 0.8756083250 0.8678483963 + 0.8679118156 0.8760337234 0.8683316708 + 0.8683874011 0.8764591813 0.8688150048 + 0.8688629866 0.8768845797 0.8692982793 + 0.8693385124 0.8773100972 0.8697816133 + 0.8698140979 0.8777354956 0.8702471852 + 0.8702710271 0.8781610131 0.8706982136 + 0.8707159758 0.8785864711 0.8711491823 + 0.8711609244 0.8790119290 0.8716002107 + 0.8716058135 0.8794373870 0.8720511198 + 0.8720507026 0.8798627853 0.8725020885 + 0.8724955916 0.8802697062 0.8729531169 + 0.8729405999 0.8806678057 0.8734040856 + 0.8733854890 0.8810659051 0.8738551140 + 0.8738303781 0.8814638853 0.8743060827 + 0.8742753267 0.8818619847 0.8747571111 + 0.8747202754 0.8822600842 0.8752080798 + 0.8751652241 0.8826581836 0.8756589890 + 0.8756101131 0.8830562234 0.8761100173 + 0.8760550022 0.8834543228 0.8765609860 + 0.8764998913 0.8838524222 0.8770120144 + 0.8769448996 0.8842504025 0.8774629831 + 0.8773897886 0.8846485019 0.8779140115 + 0.8778346777 0.8850466013 0.8783649802 + 0.8782796264 0.8854447007 0.8788160086 + 0.8787245750 0.8858426809 0.8792669773 + 0.8791695237 0.8862407804 0.8797178864 + 0.8796144128 0.8866388798 0.8801575899 + 0.8800554872 0.8870369196 0.8805783987 + 0.8804715872 0.8874350190 0.8809990883 + 0.8808878064 0.8878331184 0.8814198971 + 0.8813039064 0.8882312179 0.8818407059 + 0.8817201257 0.8886291981 0.8822613955 + 0.8821362257 0.8890272975 0.8826822042 + 0.8825523853 0.8894253969 0.8831030130 + 0.8829684854 0.8898233771 0.8835237026 + 0.8833847046 0.8902072906 0.8839445114 + 0.8838008046 0.8905797005 0.8843653202 + 0.8842170238 0.8909522295 0.8847860098 + 0.8846331239 0.8913245797 0.8852068186 + 0.8850492835 0.8916971087 0.8856276274 + 0.8854653835 0.8920695186 0.8860483170 + 0.8858816028 0.8924419880 0.8864691257 + 0.8862977028 0.8928143978 0.8868898749 + 0.8867139220 0.8931869268 0.8873106241 + 0.8871300817 0.8935592771 0.8877313733 + 0.8875461817 0.8939318061 0.8881521821 + 0.8879624009 0.8943042159 0.8885728717 + 0.8883785009 0.8946766853 0.8889936805 + 0.8887947202 0.8950490952 0.8894144893 + 0.8892108202 0.8954216242 0.8898351789 + 0.8896269798 0.8957939744 0.8902388215 + 0.8900402784 0.8961665034 0.8906313777 + 0.8904294968 0.8965389729 0.8910239935 + 0.8908187151 0.8969113827 0.8914164901 + 0.8912078738 0.8972839117 0.8918091059 + 0.8915970922 0.8976563215 0.8922016025 + 0.8919863105 0.8980287910 0.8925942183 + 0.8923755288 0.8984012008 0.8929867148 + 0.8927648067 0.8987737298 0.8933792710 + 0.8931540251 0.8991460800 0.8937718868 + 0.8935431838 0.8995186090 0.8941643834 + 0.8939324021 0.8998910189 0.8945569992 + 0.8943216205 0.9002465010 0.8949494958 + 0.8947107792 0.9005951285 0.8953421116 + 0.8950999975 0.9009435773 0.8957346082 + 0.8954892159 0.9012920856 0.8961272240 + 0.8958783746 0.9016405940 0.8965197802 + 0.8962675929 0.9019891024 0.8969122767 + 0.8966568112 0.9023376107 0.8973048925 + 0.8970460296 0.9026861191 0.8976973891 + 0.8974351883 0.9030346870 0.8980900049 + 0.8978244066 0.9033831954 0.8984825015 + 0.8982136250 0.9037317038 0.8988751173 + 0.8986027837 0.9040802121 0.8992676735 + 0.8989920020 0.9044287205 0.8996602297 + 0.8993812203 0.9047772288 0.9000492096 + 0.8997703791 0.9051256776 0.9004154801 + 0.9001492858 0.9054743052 0.9007818103 + 0.9005132914 0.9058228135 0.9011480212 + 0.9008772969 0.9061713219 0.9015142918 + 0.9012413025 0.9065197706 0.9018806219 + 0.9016053081 0.9068682790 0.9022467732 + 0.9019693732 0.9072167873 0.9026131034 + 0.9023333788 0.9075652957 0.9029793143 + 0.9026973844 0.9079139233 0.9033455849 + 0.9030613899 0.9082623720 0.9037119150 + 0.9034253955 0.9086108804 0.9040781260 + 0.9037894011 0.9089593887 0.9044443965 + 0.9041534066 0.9093078971 0.9048107266 + 0.9045174122 0.9096564054 0.9051768780 + 0.9048814178 0.9100046158 0.9055432081 + 0.9052454233 0.9103307724 0.9059094787 + 0.9056094289 0.9106569886 0.9062756896 + 0.9059733748 0.9109830856 0.9066420197 + 0.9063374996 0.9113093019 0.9070082903 + 0.9067015052 0.9116355181 0.9073745012 + 0.9070655107 0.9119616747 0.9077407718 + 0.9074295163 0.9122877717 0.9081071019 + 0.9077935219 0.9126139879 0.9084733129 + 0.9081575274 0.9129402041 0.9088395834 + 0.9085214734 0.9132663012 0.9092057943 + 0.9088854790 0.9135925174 0.9095721245 + 0.9092494845 0.9139186740 0.9099383950 + 0.9096134901 0.9142448902 0.9102842808 + 0.9099774957 0.9145709872 0.9106261134 + 0.9103195071 0.9148972034 0.9109678864 + 0.9106599092 0.9152234197 0.9113097191 + 0.9110003710 0.9155495763 0.9116513729 + 0.9113408923 0.9158756733 0.9119932055 + 0.9116814137 0.9162018895 0.9123349786 + 0.9120218158 0.9165281057 0.9126768112 + 0.9123622775 0.9168542027 0.9130185843 + 0.9127027988 0.9171804190 0.9133604169 + 0.9130433202 0.9175065756 0.9137021899 + 0.9133837819 0.9178327918 0.9140440226 + 0.9137241840 0.9181588888 0.9143857956 + 0.9140647054 0.9184851050 0.9147276282 + 0.9144052267 0.9188113213 0.9150692821 + 0.9147456884 0.9191374183 0.9154111147 + 0.9150860906 0.9194635749 0.9157528877 + 0.9154266119 0.9197897911 0.9160947204 + 0.9157670736 0.9201086164 0.9164364934 + 0.9161075950 0.9204139113 0.9167783260 + 0.9164479971 0.9207192063 0.9171200991 + 0.9167885184 0.9210246205 0.9174618721 + 0.9171289802 0.9213299155 0.9178037047 + 0.9174695015 0.9216352105 0.9181454778 + 0.9178099036 0.9219406247 0.9184871912 + 0.9181504250 0.9222459197 0.9188290238 + 0.9184908867 0.9225512743 0.9191707969 + 0.9188314080 0.9228566289 0.9195126295 + 0.9191719294 0.9231619239 0.9198544025 + 0.9195122719 0.9234672785 0.9201831222 + 0.9198527932 0.9237725735 0.9205021858 + 0.9201807976 0.9240779281 0.9208211899 + 0.9204993248 0.9243832827 0.9211401939 + 0.9208179116 0.9246885777 0.9214591980 + 0.9211363792 0.9249939919 0.9217783213 + 0.9214549065 0.9252992868 0.9220973253 + 0.9217733741 0.9256045818 0.9224163294 + 0.9220920205 0.9259099960 0.9227353930 + 0.9224104881 0.9262152910 0.9230543971 + 0.9227290154 0.9265205860 0.9233734012 + 0.9230474830 0.9268260002 0.9236925244 + 0.9233661294 0.9271312952 0.9240115285 + 0.9236845970 0.9274367094 0.9243304729 + 0.9240031242 0.9277420044 0.9246494770 + 0.9243215919 0.9280472994 0.9249686003 + 0.9246401787 0.9283527136 0.9252876043 + 0.9249587059 0.9286580086 0.9256066084 + 0.9252771735 0.9289633036 0.9259256721 + 0.9255957007 0.9292687178 0.9262446761 + 0.9259142876 0.9295740128 0.9265636802 + 0.9262328148 0.9298794270 0.9268828034 + 0.9265512824 0.9301729798 0.9272018075 + 0.9268698096 0.9304589033 0.9275208116 + 0.9271882772 0.9307448268 0.9278398752 + 0.9275069237 0.9310306907 0.9281588793 + 0.9278253913 0.9313166142 0.9284778833 + 0.9281439185 0.9316025972 0.9287968874 + 0.9284623861 0.9318885207 0.9291160107 + 0.9287809730 0.9321743846 0.9294350147 + 0.9290995002 0.9324603081 0.9297540188 + 0.9294180274 0.9327462912 0.9300681949 + 0.9297364950 0.9330322146 0.9303660989 + 0.9300515056 0.9333180785 0.9306640029 + 0.9303495884 0.9336040020 0.9309619069 + 0.9306476116 0.9338899851 0.9312596917 + 0.9309456944 0.9341759086 0.9315575957 + 0.9312437773 0.9344617724 0.9318554997 + 0.9315418005 0.9347476959 0.9321534038 + 0.9318398833 0.9350336194 0.9324513078 + 0.9321380258 0.9353196025 0.9327492118 + 0.9324359894 0.9356055260 0.9330469966 + 0.9327340722 0.9358913898 0.9333449006 + 0.9330320954 0.9361773133 0.9336428046 + 0.9333301783 0.9364632964 0.9339407086 + 0.9336283207 0.9367492199 0.9342386127 + 0.9339262843 0.9370350838 0.9345363975 + 0.9342244267 0.9373210073 0.9348343015 + 0.9345223904 0.9376069903 0.9351322055 + 0.9348204732 0.9378929138 0.9354301095 + 0.9351186156 0.9381787777 0.9357280135 + 0.9354165792 0.9384647012 0.9360259175 + 0.9357147217 0.9387506247 0.9363237023 + 0.9360128045 0.9390366077 0.9366216063 + 0.9363108277 0.9393224716 0.9369195104 + 0.9366089106 0.9396083951 0.9372174144 + 0.9369068742 0.9398943186 0.9375153184 + 0.9372050166 0.9401689172 0.9378132224 + 0.9375030994 0.9404367208 0.9381110072 + 0.9378011227 0.9407045245 0.9384089112 + 0.9380992055 0.9409723878 0.9387068152 + 0.9383972287 0.9412401915 0.9390047193 + 0.9386953115 0.9415081143 0.9393026233 + 0.9389933944 0.9417759180 0.9396004081 + 0.9392914176 0.9420437813 0.9398983121 + 0.9395895004 0.9423115849 0.9401832819 + 0.9398875833 0.9425793886 0.9404615164 + 0.9401736856 0.9428473115 0.9407398105 + 0.9404526949 0.9431151152 0.9410179853 + 0.9407318234 0.9433829784 0.9412962198 + 0.9410107732 0.9436507821 0.9415745139 + 0.9412897825 0.9439185858 0.9418526888 + 0.9415687919 0.9441865087 0.9421309829 + 0.9418478012 0.9444543123 0.9424092174 + 0.9421268106 0.9447221756 0.9426875114 + 0.9424058199 0.9449899793 0.9429656863 + 0.9426848292 0.9452577829 0.9432439804 + 0.9429637790 0.9455257058 0.9435222149 + 0.9432427883 0.9457935095 0.9438003898 + 0.9435217977 0.9460613728 0.9440786839 + 0.9438008070 0.9463291764 0.9443569183 + 0.9440798163 0.9465970993 0.9446352124 + 0.9443588257 0.9468649030 0.9449133873 + 0.9446377754 0.9471327066 0.9451916814 + 0.9449167848 0.9474006295 0.9454699159 + 0.9451957941 0.9476683736 0.9457480907 + 0.9454748034 0.9479362965 0.9460263848 + 0.9457538128 0.9482041001 0.9463046193 + 0.9460328221 0.9484719038 0.9465829134 + 0.9463117719 0.9487398267 0.9468610883 + 0.9465907812 0.9490075707 0.9471393824 + 0.9468697906 0.9492754936 0.9474176168 + 0.9471487999 0.9495432973 0.9476959109 + 0.9474278092 0.9498111010 0.9479740858 + 0.9477068186 0.9500740170 0.9482523203 + 0.9479858279 0.9503250122 0.9485306144 + 0.9482647777 0.9505760074 0.9488087893 + 0.9485437870 0.9508270025 0.9490870833 + 0.9488227963 0.9510779977 0.9493653178 + 0.9491018057 0.9513291121 0.9496436119 + 0.9493808150 0.9515801072 0.9499217868 + 0.9496598244 0.9518311024 0.9501869082 + 0.9499387741 0.9520820975 0.9504470229 + 0.9502040148 0.9523330927 0.9507070184 + 0.9504652023 0.9525840878 0.9509670138 + 0.9507265091 0.9528350830 0.9512270093 + 0.9509876966 0.9530860782 0.9514870048 + 0.9512490034 0.9533370733 0.9517470002 + 0.9515101910 0.9535881281 0.9520069957 + 0.9517714977 0.9538391232 0.9522671103 + 0.9520328045 0.9540901184 0.9525271058 + 0.9522939920 0.9543411136 0.9527871013 + 0.9525552988 0.9545921087 0.9530470967 + 0.9528164864 0.9548432231 0.9533070922 + 0.9530777931 0.9550942183 0.9535670877 + 0.9533389807 0.9553452134 0.9538270831 + 0.9536002874 0.9555962086 0.9540871978 + 0.9538615942 0.9558472037 0.9543471932 + 0.9541227818 0.9560981989 0.9546071887 + 0.9543840885 0.9563491940 0.9548671842 + 0.9546452761 0.9566001892 0.9551271796 + 0.9549065828 0.9568511844 0.9553871751 + 0.9551677704 0.9571021795 0.9556471705 + 0.9554290771 0.9573531747 0.9559072852 + 0.9556903839 0.9576042295 0.9561672807 + 0.9559515715 0.9578552246 0.9564272761 + 0.9562128782 0.9581062198 0.9566872716 + 0.9564741254 0.9583572149 0.9569473267 + 0.9567353725 0.9586083293 0.9572073221 + 0.9569966197 0.9588593245 0.9574673176 + 0.9572579265 0.9591103196 0.9577273726 + 0.9575191736 0.9593613148 0.9579874277 + 0.9577804208 0.9596123099 0.9582474232 + 0.9580417275 0.9598633051 0.9585074186 + 0.9583029151 0.9601072073 0.9587674141 + 0.9585642219 0.9603425264 0.9590274096 + 0.9588255286 0.9605777860 0.9592875242 + 0.9590867162 0.9608132243 0.9595475197 + 0.9593480229 0.9610484838 0.9598075151 + 0.9596092105 0.9612839222 0.9600630999 + 0.9598705173 0.9615191817 0.9603062272 + 0.9601234198 0.9617545009 0.9605492949 + 0.9603682160 0.9619898796 0.9607924223 + 0.9606128931 0.9622251987 0.9610354900 + 0.9608576298 0.9624605179 0.9612786174 + 0.9611024261 0.9626958966 0.9615216851 + 0.9613471031 0.9629312158 0.9617648125 + 0.9615918994 0.9631665945 0.9620078802 + 0.9618365765 0.9634019136 0.9622510076 + 0.9620813727 0.9636371732 0.9624940753 + 0.9623261094 0.9638726115 0.9627372026 + 0.9625709057 0.9641078711 0.9629802704 + 0.9628155828 0.9643431902 0.9632233977 + 0.9630603194 0.9645786285 0.9634665251 + 0.9633051157 0.9648138881 0.9637095928 + 0.9635497928 0.9650493264 0.9639527202 + 0.9637945890 0.9652845860 0.9641957879 + 0.9640393257 0.9655199051 0.9644389153 + 0.9642841220 0.9657552838 0.9646819830 + 0.9645287991 0.9659906030 0.9649251103 + 0.9647734761 0.9662259817 0.9651681781 + 0.9650182724 0.9664613008 0.9654113054 + 0.9652630091 0.9666966200 0.9656543732 + 0.9655078053 0.9669319987 0.9658975005 + 0.9657524824 0.9671673179 0.9661406279 + 0.9659972787 0.9674025774 0.9663836956 + 0.9662420154 0.9676380157 0.9666268229 + 0.9664868116 0.9678732753 0.9668698907 + 0.9667314887 0.9681087136 0.9671130180 + 0.9669762254 0.9683439732 0.9673560858 + 0.9672210217 0.9685792923 0.9675992131 + 0.9674656987 0.9688146710 0.9678422809 + 0.9677104950 0.9690499902 0.9680854082 + 0.9679551721 0.9692854285 0.9683284760 + 0.9682000279 0.9695206881 0.9685716033 + 0.9684447050 0.9697560072 0.9688146710 + 0.9686893821 0.9699913859 0.9690577984 + 0.9689341784 0.9702126980 0.9693009257 + 0.9691789150 0.9704334140 0.9695439935 + 0.9694237113 0.9706541896 0.9697871208 + 0.9696683884 0.9708749056 0.9700282216 + 0.9699131846 0.9710956812 0.9702556133 + 0.9701480269 0.9713163972 0.9704830050 + 0.9703773856 0.9715371728 0.9707103968 + 0.9706068039 0.9717580080 0.9709377885 + 0.9708361030 0.9719787240 0.9711652994 + 0.9710655212 0.9721994996 0.9713926911 + 0.9712948799 0.9724202156 0.9716200829 + 0.9715242982 0.9726409912 0.9718474746 + 0.9717537165 0.9728617072 0.9720749259 + 0.9719830155 0.9730824828 0.9723023176 + 0.9722123742 0.9733031988 0.9725297093 + 0.9724417925 0.9735239744 0.9727571011 + 0.9726712108 0.9737448096 0.9729844928 + 0.9729006290 0.9739655256 0.9732118845 + 0.9731299281 0.9741863012 0.9734392762 + 0.9733592868 0.9744070172 0.9736667275 + 0.9735887051 0.9746277928 0.9738941193 + 0.9738181233 0.9748485088 0.9741215110 + 0.9740474820 0.9750692844 0.9743489027 + 0.9742767811 0.9752901196 0.9745762944 + 0.9745061994 0.9755107760 0.9748036861 + 0.9747356176 0.9757316113 0.9750310779 + 0.9749649763 0.9759523273 0.9752585292 + 0.9751943946 0.9761731029 0.9754859209 + 0.9754236937 0.9763938189 0.9757133722 + 0.9756531119 0.9766145945 0.9759408236 + 0.9758824706 0.9768353105 0.9761682153 + 0.9761118889 0.9770560861 0.9763956070 + 0.9763413072 0.9772769213 0.9766229987 + 0.9765706062 0.9774975777 0.9768503904 + 0.9768000245 0.9777184129 0.9770777822 + 0.9770293832 0.9779391289 0.9773051739 + 0.9772588015 0.9781599045 0.9775326252 + 0.9774881005 0.9783806205 0.9777600169 + 0.9777175188 0.9786013961 0.9779874086 + 0.9779468775 0.9788221717 0.9782148004 + 0.9781762958 0.9790428877 0.9784421921 + 0.9784057140 0.9792637229 0.9786695838 + 0.9786350131 0.9794843793 0.9788969755 + 0.9788643718 0.9797052145 0.9791244268 + 0.9790937901 0.9799258709 0.9793518186 + 0.9793232083 0.9801377058 0.9795792103 + 0.9795526266 0.9803448915 0.9798066020 + 0.9797819257 0.9805520177 0.9800319076 + 0.9800106287 0.9807592034 0.9802446961 + 0.9802256823 0.9809663892 0.9804576039 + 0.9804407954 0.9811735749 0.9806703925 + 0.9806559086 0.9813808203 0.9808831811 + 0.9808710217 0.9815880060 0.9810960889 + 0.9810860753 0.9817951918 0.9813088775 + 0.9813011289 0.9820023775 0.9815217853 + 0.9815161824 0.9822095037 0.9817345738 + 0.9817312956 0.9824166894 0.9819474816 + 0.9819464087 0.9826238751 0.9821602702 + 0.9821615219 0.9828311205 0.9823731780 + 0.9823765755 0.9830383062 0.9825860262 + 0.9825916886 0.9832454920 0.9827988148 + 0.9828066826 0.9834526777 0.9830117226 + 0.9830217957 0.9836598039 0.9832245111 + 0.9832369089 0.9838669896 0.9834374189 + 0.9834520221 0.9840741754 0.9836502075 + 0.9836670756 0.9842814207 0.9838631153 + 0.9838821888 0.9844886065 0.9840759039 + 0.9840973020 0.9846957922 0.9842888117 + 0.9843124151 0.9849029779 0.9845016003 + 0.9845274091 0.9851101041 0.9847143888 + 0.9847425222 0.9853172898 0.9849272966 + 0.9849575758 0.9855244756 0.9851400852 + 0.9851726890 0.9857317209 0.9853529930 + 0.9853878021 0.9859389067 0.9855657816 + 0.9856029153 0.9861460924 0.9857786894 + 0.9858180285 0.9863532782 0.9859914780 + 0.9860330224 0.9865604043 0.9862043262 + 0.9862480760 0.9867675900 0.9864171743 + 0.9864631891 0.9869747758 0.9866300225 + 0.9866783023 0.9871820211 0.9868428707 + 0.9868934155 0.9873892069 0.9870557189 + 0.9871085286 0.9875963926 0.9872686267 + 0.9873235822 0.9878035784 0.9874814153 + 0.9875385761 0.9880108237 0.9876943231 + 0.9877536893 0.9882178903 0.9879071116 + 0.9879688025 0.9884250760 0.9881199002 + 0.9881839156 0.9886323214 0.9883328080 + 0.9883990288 0.9888395071 0.9885455966 + 0.9886140823 0.9890466928 0.9887585044 + 0.9888291955 0.9892538786 0.9889712930 + 0.9890443087 0.9894611239 0.9891842008 + 0.9892593026 0.9896681905 0.9893969893 + 0.9894744158 0.9898753762 0.9896098971 + 0.9896895289 0.9900776148 0.9898226857 + 0.9899045825 0.9902721047 0.9900333285 + 0.9901123047 0.9904667139 0.9902325869 + 0.9903141260 0.9906613231 0.9904320240 + 0.9905158877 0.9908558130 0.9906312823 + 0.9907177091 0.9910504222 0.9908306003 + 0.9909194112 0.9912449121 0.9910299778 + 0.9911211729 0.9914395213 0.9912292957 + 0.9913229942 0.9916340113 0.9914286137 + 0.9915248156 0.9918286204 0.9916279912 + 0.9917265773 0.9920232296 0.9918273091 + 0.9919283986 0.9922177196 0.9920266271 + 0.9921302199 0.9924123287 0.9922260046 + 0.9923319817 0.9926068187 0.9924253225 + 0.9925338030 0.9928014278 0.9926245809 + 0.9927356243 0.9929959178 0.9928240180 + 0.9929373264 0.9931905270 0.9930232763 + 0.9931390882 0.9933850765 0.9932225943 + 0.9933409095 0.9935796261 0.9934219718 + 0.9935426712 0.9937741756 0.9936212897 + 0.9937444925 0.9939687252 0.9938206077 + 0.9939463139 0.9941632748 0.9940199852 + 0.9941480756 0.9943578243 0.9942193031 + 0.9943498969 0.9945523739 0.9944186211 + 0.9945517182 0.9947469831 0.9946179986 + 0.9947534800 0.9949414730 0.9948173165 + 0.9949551821 0.9951360822 0.9950165749 + 0.9951570034 0.9953305721 0.9952160120 + 0.9953588247 0.9955251813 0.9954152703 + 0.9955605865 0.9957196712 0.9956145883 + 0.9957624078 0.9959142804 0.9958140254 + 0.9959642291 0.9961088896 0.9960132837 + 0.9961659908 0.9963033795 0.9962127209 + 0.9963678122 0.9964979887 0.9964119792 + 0.9965695739 0.9966924787 0.9966112971 + 0.9967713952 0.9968870878 0.9968106747 + 0.9969730973 0.9970815778 0.9970099926 + 0.9971749187 0.9972761869 0.9972093105 + 0.9973766804 0.9974707961 0.9974086881 + 0.9975785017 0.9976652861 0.9976080060 + 0.9977803230 0.9978598952 0.9978073239 + 0.9979820848 0.9980543852 0.9980067015 + 0.9981839061 0.9982489944 0.9982060194 + 0.9983857274 0.9984434843 0.9984052777 + 0.9985874891 0.9986380935 0.9986047149 + 0.9987893105 0.9988327026 0.9988039732 + 0.9989910126 0.9990271926 0.9990032911 + 0.9991927743 0.9992218018 0.9992027283 + 0.9993945956 0.9994162917 0.9994019866 + 0.9995964170 0.9996109009 0.9996013045 + 0.9997981787 0.9998053908 0.9998006821 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3155.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3155.spi1d index 8fe8fd61..6e639e8b 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3155.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3155.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0048120469 0.0048120469 0.0048120469 - 0.0096240947 0.0096240947 0.0096240947 - 0.0145009197 0.0145009197 0.0145009197 - 0.0193832405 0.0193832405 0.0193832405 - 0.0242968407 0.0242968407 0.0242968407 - 0.0292149596 0.0292149596 0.0292149596 - 0.0341353305 0.0341353305 0.0341353305 - 0.0390561298 0.0390561298 0.0390561298 - 0.0439544208 0.0439544208 0.0439544208 - 0.0488473810 0.0488473810 0.0488473810 - 0.0536984690 0.0536984690 0.0536984690 - 0.0585366711 0.0585366711 0.0585366711 - 0.0633208230 0.0633208230 0.0633208230 - 0.0680815428 0.0680815428 0.0680815428 - 0.0727850720 0.0727850720 0.0727850720 - 0.0774499997 0.0774499997 0.0774499997 - 0.0820651725 0.0820651725 0.0820651725 - 0.0866203383 0.0866203383 0.0866203383 - 0.0911446288 0.0911446288 0.0911446288 - 0.0955801234 0.0955801234 0.0955801234 - 0.1000152007 0.1000152007 0.1000152007 - 0.1043246016 0.1043246016 0.1043246016 - 0.1086340994 0.1086340994 0.1086340994 - 0.1128552034 0.1128552034 0.1128552034 - 0.1170352995 0.1170352995 0.1170352995 - 0.1211775988 0.1211775988 0.1211775988 - 0.1252277046 0.1252277046 0.1252277046 - 0.1292777061 0.1292777061 0.1292777061 - 0.1332218945 0.1332218945 0.1332218945 - 0.1371431053 0.1371431053 0.1371431053 - 0.1410301030 0.1410301030 0.1410301030 - 0.1448252946 0.1448252946 0.1448252946 - 0.1486205012 0.1486205012 0.1486205012 - 0.1523379982 0.1523379982 0.1523379982 - 0.1560111046 0.1560111046 0.1560111046 - 0.1596841961 0.1596841961 0.1596841961 - 0.1632499993 0.1632499993 0.1632499993 - 0.1668058038 0.1668058038 0.1668058038 - 0.1703501940 0.1703501940 0.1703501940 - 0.1737938970 0.1737938970 0.1737938970 - 0.1772377044 0.1772377044 0.1772377044 - 0.1806603968 0.1806603968 0.1806603968 - 0.1839977056 0.1839977056 0.1839977056 - 0.1873349994 0.1873349994 0.1873349994 - 0.1906519979 0.1906519979 0.1906519979 - 0.1938886046 0.1938886046 0.1938886046 - 0.1971253008 0.1971253008 0.1971253008 - 0.2003512979 0.2003512979 0.2003512979 - 0.2034929991 0.2034929991 0.2034929991 - 0.2066347003 0.2066347003 0.2066347003 - 0.2097764015 0.2097764015 0.2097764015 - 0.2128352970 0.2128352970 0.2128352970 - 0.2158876956 0.2158876956 0.2158876956 - 0.2189401984 0.2189401984 0.2189401984 - 0.2219379991 0.2219379991 0.2219379991 - 0.2249068022 0.2249068022 0.2249068022 - 0.2278755009 0.2278755009 0.2278755009 - 0.2308219969 0.2308219969 0.2308219969 - 0.2337123007 0.2337123007 0.2337123007 - 0.2366026938 0.2366026938 0.2366026938 - 0.2394929975 0.2394929975 0.2394929975 - 0.2423229069 0.2423229069 0.2423229069 - 0.2451398969 0.2451398969 0.2451398969 - 0.2479569018 0.2479569018 0.2479569018 - 0.2507550120 0.2507550120 0.2507550120 - 0.2535035014 0.2535035014 0.2535035014 - 0.2562519908 0.2562519908 0.2562519908 - 0.2590003908 0.2590003908 0.2590003908 - 0.2617082000 0.2617082000 0.2617082000 - 0.2643926144 0.2643926144 0.2643926144 - 0.2670770884 0.2670770884 0.2670770884 - 0.2697615027 0.2697615027 0.2697615027 - 0.2723914981 0.2723914981 0.2723914981 - 0.2750161886 0.2750161886 0.2750161886 - 0.2776409090 0.2776409090 0.2776409090 - 0.2802599967 0.2802599967 0.2802599967 - 0.2828288972 0.2828288972 0.2828288972 - 0.2853977978 0.2853977978 0.2853977978 - 0.2879666984 0.2879666984 0.2879666984 - 0.2905246913 0.2905246913 0.2905246913 - 0.2930414975 0.2930414975 0.2930414975 - 0.2955583036 0.2955583036 0.2955583036 - 0.2980751097 0.2980751097 0.2980751097 - 0.3005805016 0.3005805016 0.3005805016 - 0.3030486107 0.3030486107 0.3030486107 - 0.3055166900 0.3055166900 0.3055166900 - 0.3079847991 0.3079847991 0.3079847991 - 0.3104445040 0.3104445040 0.3104445040 - 0.3128669858 0.3128669858 0.3128669858 - 0.3152895868 0.3152895868 0.3152895868 - 0.3177120984 0.3177120984 0.3177120984 - 0.3201321959 0.3201321959 0.3201321959 - 0.3225120902 0.3225120902 0.3225120902 - 0.3248918951 0.3248918951 0.3248918951 - 0.3272717893 0.3272717893 0.3272717893 - 0.3296515942 0.3296515942 0.3296515942 - 0.3319971859 0.3319971859 0.3319971859 - 0.3343369961 0.3343369961 0.3343369961 - 0.3366766870 0.3366766870 0.3366766870 - 0.3390164971 0.3390164971 0.3390164971 - 0.3413344026 0.3413344026 0.3413344026 - 0.3436363935 0.3436363935 0.3436363935 - 0.3459384143 0.3459384143 0.3459384143 - 0.3482404947 0.3482404947 0.3482404947 - 0.3505341113 0.3505341113 0.3505341113 - 0.3528004885 0.3528004885 0.3528004885 - 0.3550668955 0.3550668955 0.3550668955 - 0.3573333025 0.3573333025 0.3573333025 - 0.3595997095 0.3595997095 0.3595997095 - 0.3618383110 0.3618383110 0.3618383110 - 0.3640710115 0.3640710115 0.3640710115 - 0.3663035929 0.3663035929 0.3663035929 - 0.3685362935 0.3685362935 0.3685362935 - 0.3707579076 0.3707579076 0.3707579076 - 0.3729583919 0.3729583919 0.3729583919 - 0.3751589060 0.3751589060 0.3751589060 - 0.3773593903 0.3773593903 0.3773593903 - 0.3795599043 0.3795599043 0.3795599043 - 0.3817358911 0.3817358911 0.3817358911 - 0.3839055896 0.3839055896 0.3839055896 - 0.3860754073 0.3860754073 0.3860754073 - 0.3882451952 0.3882451952 0.3882451952 - 0.3904092908 0.3904092908 0.3904092908 - 0.3925495148 0.3925495148 0.3925495148 - 0.3946897089 0.3946897089 0.3946897089 - 0.3968299031 0.3968299031 0.3968299031 - 0.3989700973 0.3989700973 0.3989700973 - 0.4010953903 0.4010953903 0.4010953903 - 0.4032069147 0.4032069147 0.4032069147 - 0.4053184092 0.4053184092 0.4053184092 - 0.4074299037 0.4074299037 0.4074299037 - 0.4095414877 0.4095414877 0.4095414877 - 0.4116311073 0.4116311073 0.4116311073 - 0.4137147069 0.4137147069 0.4137147069 - 0.4157981873 0.4157981873 0.4157981873 - 0.4178817868 0.4178817868 0.4178817868 - 0.4199653864 0.4199653864 0.4199653864 - 0.4220218956 0.4220218956 0.4220218956 - 0.4240779877 0.4240779877 0.4240779877 - 0.4261341095 0.4261341095 0.4261341095 - 0.4281901121 0.4281901121 0.4281901121 - 0.4302429855 0.4302429855 0.4302429855 - 0.4322718084 0.4322718084 0.4322718084 - 0.4343006909 0.4343006909 0.4343006909 - 0.4363296032 0.4363296032 0.4363296032 - 0.4383583963 0.4383583963 0.4383583963 - 0.4403820932 0.4403820932 0.4403820932 - 0.4423837960 0.4423837960 0.4423837960 - 0.4443854988 0.4443854988 0.4443854988 - 0.4463872015 0.4463872015 0.4463872015 - 0.4483889043 0.4483889043 0.4483889043 - 0.4503852129 0.4503852129 0.4503852129 - 0.4523595870 0.4523595870 0.4523595870 - 0.4543339908 0.4543339908 0.4543339908 - 0.4563083053 0.4563083053 0.4563083053 - 0.4582827091 0.4582827091 0.4582827091 - 0.4602535069 0.4602535069 0.4602535069 - 0.4622001946 0.4622001946 0.4622001946 - 0.4641467929 0.4641467929 0.4641467929 - 0.4660935104 0.4660935104 0.4660935104 - 0.4680401981 0.4680401981 0.4680401981 - 0.4699868858 0.4699868858 0.4699868858 - 0.4719055891 0.4719055891 0.4719055891 - 0.4738239944 0.4738239944 0.4738239944 - 0.4757424891 0.4757424891 0.4757424891 - 0.4776608944 0.4776608944 0.4776608944 - 0.4795793891 0.4795793891 0.4795793891 - 0.4814752936 0.4814752936 0.4814752936 - 0.4833647907 0.4833647907 0.4833647907 - 0.4852542877 0.4852542877 0.4852542877 - 0.4871438146 0.4871438146 0.4871438146 - 0.4890333116 0.4890333116 0.4890333116 - 0.4909082949 0.4909082949 0.4909082949 - 0.4927679002 0.4927679002 0.4927679002 - 0.4946275949 0.4946275949 0.4946275949 - 0.4964872897 0.4964872897 0.4964872897 - 0.4983470142 0.4983470142 0.4983470142 - 0.5002033114 0.5002033114 0.5002033114 - 0.5020321012 0.5020321012 0.5020321012 - 0.5038608909 0.5038608909 0.5038608909 - 0.5056896806 0.5056896806 0.5056896806 - 0.5075185895 0.5075185895 0.5075185895 - 0.5093473792 0.5093473792 0.5093473792 - 0.5111556053 0.5111556053 0.5111556053 - 0.5129525065 0.5129525065 0.5129525065 - 0.5147492886 0.5147492886 0.5147492886 - 0.5165460706 0.5165460706 0.5165460706 - 0.5183429122 0.5183429122 0.5183429122 - 0.5201370716 0.5201370716 0.5201370716 - 0.5219007134 0.5219007134 0.5219007134 - 0.5236642957 0.5236642957 0.5236642957 - 0.5254278183 0.5254278183 0.5254278183 - 0.5271914005 0.5271914005 0.5271914005 - 0.5289549232 0.5289549232 0.5289549232 - 0.5307043791 0.5307043791 0.5307043791 - 0.5324332714 0.5324332714 0.5324332714 - 0.5341622829 0.5341622829 0.5341622829 - 0.5358911753 0.5358911753 0.5358911753 - 0.5376201868 0.5376201868 0.5376201868 - 0.5393490791 0.5393490791 0.5393490791 - 0.5410556197 0.5410556197 0.5410556197 - 0.5427485704 0.5427485704 0.5427485704 - 0.5444415212 0.5444415212 0.5444415212 - 0.5461344719 0.5461344719 0.5461344719 - 0.5478274226 0.5478274226 0.5478274226 - 0.5495203733 0.5495203733 0.5495203733 - 0.5511866212 0.5511866212 0.5511866212 - 0.5528420806 0.5528420806 0.5528420806 - 0.5544977188 0.5544977188 0.5544977188 - 0.5561531782 0.5561531782 0.5561531782 - 0.5578088164 0.5578088164 0.5578088164 - 0.5594642758 0.5594642758 0.5594642758 - 0.5610936284 0.5610936284 0.5610936284 - 0.5627104044 0.5627104044 0.5627104044 - 0.5643271208 0.5643271208 0.5643271208 - 0.5659437776 0.5659437776 0.5659437776 - 0.5675606132 0.5675606132 0.5675606132 - 0.5691773295 0.5691773295 0.5691773295 - 0.5707743168 0.5707743168 0.5707743168 - 0.5723509192 0.5723509192 0.5723509192 - 0.5739275217 0.5739275217 0.5739275217 - 0.5755040050 0.5755040050 0.5755040050 - 0.5770806074 0.5770806074 0.5770806074 - 0.5786570907 0.5786570907 0.5786570907 - 0.5802274942 0.5802274942 0.5802274942 - 0.5817626119 0.5817626119 0.5817626119 - 0.5832977295 0.5832977295 0.5832977295 - 0.5848327279 0.5848327279 0.5848327279 - 0.5863677859 0.5863677859 0.5863677859 - 0.5879029036 0.5879029036 0.5879029036 - 0.5894379020 0.5894379020 0.5894379020 - 0.5909458995 0.5909458995 0.5909458995 - 0.5924382806 0.5924382806 0.5924382806 - 0.5939307213 0.5939307213 0.5939307213 - 0.5954229832 0.5954229832 0.5954229832 - 0.5969154239 0.5969154239 0.5969154239 - 0.5984076858 0.5984076858 0.5984076858 - 0.5999001265 0.5999001265 0.5999001265 - 0.6013516188 0.6013516188 0.6013516188 - 0.6028001904 0.6028001904 0.6028001904 - 0.6042487025 0.6042487025 0.6042487025 - 0.6056972742 0.6056972742 0.6056972742 - 0.6071457863 0.6071457863 0.6071457863 - 0.6085944176 0.6085944176 0.6085944176 - 0.6100416183 0.6100416183 0.6100416183 - 0.6114454269 0.6114454269 0.6114454269 - 0.6128491759 0.6128491759 0.6128491759 - 0.6142529845 0.6142529845 0.6142529845 - 0.6156567931 0.6156567931 0.6156567931 - 0.6170606017 0.6170606017 0.6170606017 - 0.6184644103 0.6184644103 0.6184644103 - 0.6198682785 0.6198682785 0.6198682785 - 0.6212307811 0.6212307811 0.6212307811 - 0.6225889921 0.6225889921 0.6225889921 - 0.6239473224 0.6239473224 0.6239473224 - 0.6253055930 0.6253055930 0.6253055930 - 0.6266638041 0.6266638041 0.6266638041 - 0.6280220747 0.6280220747 0.6280220747 - 0.6293802857 0.6293802857 0.6293802857 - 0.6307135224 0.6307135224 0.6307135224 - 0.6320255995 0.6320255995 0.6320255995 - 0.6333376765 0.6333376765 0.6333376765 - 0.6346498132 0.6346498132 0.6346498132 - 0.6359618902 0.6359618902 0.6359618902 - 0.6372740269 0.6372740269 0.6372740269 - 0.6385861039 0.6385861039 0.6385861039 - 0.6398981810 0.6398981810 0.6398981810 - 0.6411672831 0.6411672831 0.6411672831 - 0.6424328089 0.6424328089 0.6424328089 - 0.6436982751 0.6436982751 0.6436982751 - 0.6449638009 0.6449638009 0.6449638009 - 0.6462293863 0.6462293863 0.6462293863 - 0.6474949121 0.6474949121 0.6474949121 - 0.6487603784 0.6487603784 0.6487603784 - 0.6500248909 0.6500248909 0.6500248909 - 0.6512436271 0.6512436271 0.6512436271 - 0.6524624228 0.6524624228 0.6524624228 - 0.6536810994 0.6536810994 0.6536810994 - 0.6548997760 0.6548997760 0.6548997760 - 0.6561185122 0.6561185122 0.6561185122 - 0.6573371887 0.6573371887 0.6573371887 - 0.6585559249 0.6585559249 0.6585559249 - 0.6597746015 0.6597746015 0.6597746015 - 0.6609550714 0.6609550714 0.6609550714 - 0.6621270180 0.6621270180 0.6621270180 - 0.6632987857 0.6632987857 0.6632987857 - 0.6644706726 0.6644706726 0.6644706726 - 0.6656426191 0.6656426191 0.6656426191 - 0.6668143868 0.6668143868 0.6668143868 - 0.6679862738 0.6679862738 0.6679862738 - 0.6691582203 0.6691582203 0.6691582203 - 0.6703168750 0.6703168750 0.6703168750 - 0.6714420915 0.6714420915 0.6714420915 - 0.6725673079 0.6725673079 0.6725673079 - 0.6736925244 0.6736925244 0.6736925244 - 0.6748176813 0.6748176813 0.6748176813 - 0.6759428978 0.6759428978 0.6759428978 - 0.6770681143 0.6770681143 0.6770681143 - 0.6781932712 0.6781932712 0.6781932712 - 0.6793184876 0.6793184876 0.6793184876 - 0.6804254055 0.6804254055 0.6804254055 - 0.6815043092 0.6815043092 0.6815043092 - 0.6825832129 0.6825832129 0.6825832129 - 0.6836621165 0.6836621165 0.6836621165 - 0.6847409010 0.6847409010 0.6847409010 - 0.6858198047 0.6858198047 0.6858198047 - 0.6868987083 0.6868987083 0.6868987083 - 0.6879776120 0.6879776120 0.6879776120 - 0.6890565157 0.6890565157 0.6890565157 - 0.6901295781 0.6901295781 0.6901295781 - 0.6911628246 0.6911628246 0.6911628246 - 0.6921958923 0.6921958923 0.6921958923 - 0.6932290196 0.6932290196 0.6932290196 - 0.6942620873 0.6942620873 0.6942620873 - 0.6952952147 0.6952952147 0.6952952147 - 0.6963282824 0.6963282824 0.6963282824 - 0.6973615289 0.6973615289 0.6973615289 - 0.6983945966 0.6983945966 0.6983945966 - 0.6994277239 0.6994277239 0.6994277239 - 0.7004407048 0.7004407048 0.7004407048 - 0.7014287710 0.7014287710 0.7014287710 - 0.7024167776 0.7024167776 0.7024167776 - 0.7034049034 0.7034049034 0.7034049034 - 0.7043930292 0.7043930292 0.7043930292 - 0.7053809762 0.7053809762 0.7053809762 - 0.7063691020 0.7063691020 0.7063691020 - 0.7073571086 0.7073571086 0.7073571086 - 0.7083451748 0.7083451748 0.7083451748 - 0.7093333006 0.7093333006 0.7093333006 - 0.7103068829 0.7103068829 0.7103068829 - 0.7112507820 0.7112507820 0.7112507820 - 0.7121946812 0.7121946812 0.7121946812 - 0.7131385207 0.7131385207 0.7131385207 - 0.7140824199 0.7140824199 0.7140824199 - 0.7150263190 0.7150263190 0.7150263190 - 0.7159700990 0.7159700990 0.7159700990 - 0.7169139981 0.7169139981 0.7169139981 - 0.7178577781 0.7178577781 0.7178577781 - 0.7188016772 0.7188016772 0.7188016772 - 0.7197455764 0.7197455764 0.7197455764 - 0.7206578851 0.7206578851 0.7206578851 - 0.7215585113 0.7215585113 0.7215585113 - 0.7224591970 0.7224591970 0.7224591970 - 0.7233598828 0.7233598828 0.7233598828 - 0.7242605090 0.7242605090 0.7242605090 - 0.7251611948 0.7251611948 0.7251611948 - 0.7260618806 0.7260618806 0.7260618806 - 0.7269625068 0.7269625068 0.7269625068 - 0.7278631926 0.7278631926 0.7278631926 - 0.7287638783 0.7287638783 0.7287638783 - 0.7296645045 0.7296645045 0.7296645045 - 0.7305387855 0.7305387855 0.7305387855 - 0.7313973904 0.7313973904 0.7313973904 - 0.7322559953 0.7322559953 0.7322559953 - 0.7331146002 0.7331146002 0.7331146002 - 0.7339732051 0.7339732051 0.7339732051 - 0.7348316908 0.7348316908 0.7348316908 - 0.7356902957 0.7356902957 0.7356902957 - 0.7365489006 0.7365489006 0.7365489006 - 0.7374075055 0.7374075055 0.7374075055 - 0.7382661104 0.7382661104 0.7382661104 - 0.7391247153 0.7391247153 0.7391247153 - 0.7399833202 0.7399833202 0.7399833202 - 0.7408018112 0.7408018112 0.7408018112 - 0.7416195273 0.7416195273 0.7416195273 - 0.7424373031 0.7424373031 0.7424373031 - 0.7432550192 0.7432550192 0.7432550192 - 0.7440726757 0.7440726757 0.7440726757 - 0.7448903918 0.7448903918 0.7448903918 - 0.7457082272 0.7457082272 0.7457082272 - 0.7465258837 0.7465258837 0.7465258837 - 0.7473435998 0.7473435998 0.7473435998 - 0.7481613755 0.7481613755 0.7481613755 - 0.7489790916 0.7489790916 0.7489790916 - 0.7497968078 0.7497968078 0.7497968078 - 0.7505847812 0.7505847812 0.7505847812 - 0.7513629794 0.7513629794 0.7513629794 - 0.7521411777 0.7521411777 0.7521411777 - 0.7529193759 0.7529193759 0.7529193759 - 0.7536975145 0.7536975145 0.7536975145 - 0.7544757128 0.7544757128 0.7544757128 - 0.7552539110 0.7552539110 0.7552539110 - 0.7560319901 0.7560319901 0.7560319901 - 0.7568101883 0.7568101883 0.7568101883 - 0.7575883865 0.7575883865 0.7575883865 - 0.7583665848 0.7583665848 0.7583665848 - 0.7591447234 0.7591447234 0.7591447234 - 0.7599229217 0.7599229217 0.7599229217 - 0.7606667280 0.7606667280 0.7606667280 - 0.7614067197 0.7614067197 0.7614067197 - 0.7621465921 0.7621465921 0.7621465921 - 0.7628865838 0.7628865838 0.7628865838 - 0.7636265755 0.7636265755 0.7636265755 - 0.7643666267 0.7643666267 0.7643666267 - 0.7651066184 0.7651066184 0.7651066184 - 0.7658466101 0.7658466101 0.7658466101 - 0.7665864825 0.7665864825 0.7665864825 - 0.7673264742 0.7673264742 0.7673264742 - 0.7680665255 0.7680665255 0.7680665255 - 0.7688065171 0.7688065171 0.7688065171 - 0.7695465088 0.7695465088 0.7695465088 - 0.7702721953 0.7702721953 0.7702721953 - 0.7709754109 0.7709754109 0.7709754109 - 0.7716786265 0.7716786265 0.7716786265 - 0.7723817825 0.7723817825 0.7723817825 - 0.7730849981 0.7730849981 0.7730849981 - 0.7737882733 0.7737882733 0.7737882733 - 0.7744914889 0.7744914889 0.7744914889 - 0.7751947045 0.7751947045 0.7751947045 - 0.7758979201 0.7758979201 0.7758979201 - 0.7766010761 0.7766010761 0.7766010761 - 0.7773042917 0.7773042917 0.7773042917 - 0.7780075073 0.7780075073 0.7780075073 - 0.7787107229 0.7787107229 0.7787107229 - 0.7794138789 0.7794138789 0.7794138789 - 0.7801111937 0.7801111937 0.7801111937 - 0.7807791233 0.7807791233 0.7807791233 - 0.7814469934 0.7814469934 0.7814469934 - 0.7821149230 0.7821149230 0.7821149230 - 0.7827826738 0.7827826738 0.7827826738 - 0.7834506035 0.7834506035 0.7834506035 - 0.7841184735 0.7841184735 0.7841184735 - 0.7847864032 0.7847864032 0.7847864032 - 0.7854542136 0.7854542136 0.7854542136 - 0.7861220837 0.7861220837 0.7861220837 - 0.7867900133 0.7867900133 0.7867900133 - 0.7874578834 0.7874578834 0.7874578834 - 0.7881256938 0.7881256938 0.7881256938 - 0.7887936234 0.7887936234 0.7887936234 - 0.7894614935 0.7894614935 0.7894614935 - 0.7901228070 0.7901228070 0.7901228070 - 0.7907568216 0.7907568216 0.7907568216 - 0.7913907766 0.7913907766 0.7913907766 - 0.7920247912 0.7920247912 0.7920247912 - 0.7926588058 0.7926588058 0.7926588058 - 0.7932928801 0.7932928801 0.7932928801 - 0.7939268947 0.7939268947 0.7939268947 - 0.7945609093 0.7945609093 0.7945609093 - 0.7951949239 0.7951949239 0.7951949239 - 0.7958288789 0.7958288789 0.7958288789 - 0.7964628935 0.7964628935 0.7964628935 - 0.7970969081 0.7970969081 0.7970969081 - 0.7977309227 0.7977309227 0.7977309227 - 0.7983648777 0.7983648777 0.7983648777 - 0.7989988923 0.7989988923 0.7989988923 - 0.7996329069 0.7996329069 0.7996329069 - 0.8002532721 0.8002532721 0.8002532721 - 0.8008549213 0.8008549213 0.8008549213 - 0.8014565110 0.8014565110 0.8014565110 - 0.8020581007 0.8020581007 0.8020581007 - 0.8026596904 0.8026596904 0.8026596904 - 0.8032613993 0.8032613993 0.8032613993 - 0.8038629889 0.8038629889 0.8038629889 - 0.8044645786 0.8044645786 0.8044645786 - 0.8050662279 0.8050662279 0.8050662279 - 0.8056678176 0.8056678176 0.8056678176 - 0.8062694073 0.8062694073 0.8062694073 - 0.8068709970 0.8068709970 0.8068709970 - 0.8074725866 0.8074725866 0.8074725866 - 0.8080741763 0.8080741763 0.8080741763 - 0.8086758256 0.8086758256 0.8086758256 - 0.8092774153 0.8092774153 0.8092774153 - 0.8098791242 0.8098791242 0.8098791242 - 0.8104559183 0.8104559183 0.8104559183 - 0.8110265732 0.8110265732 0.8110265732 - 0.8115972877 0.8115972877 0.8115972877 - 0.8121680021 0.8121680021 0.8121680021 - 0.8127385974 0.8127385974 0.8127385974 - 0.8133093119 0.8133093119 0.8133093119 - 0.8138800263 0.8138800263 0.8138800263 - 0.8144506216 0.8144506216 0.8144506216 - 0.8150212765 0.8150212765 0.8150212765 - 0.8155919909 0.8155919909 0.8155919909 - 0.8161625862 0.8161625862 0.8161625862 - 0.8167333007 0.8167333007 0.8167333007 - 0.8173040152 0.8173040152 0.8173040152 - 0.8178747296 0.8178747296 0.8178747296 - 0.8184453249 0.8184453249 0.8184453249 - 0.8190159798 0.8190159798 0.8190159798 - 0.8195866942 0.8195866942 0.8195866942 - 0.8201491833 0.8201491833 0.8201491833 - 0.8206903934 0.8206903934 0.8206903934 - 0.8212316036 0.8212316036 0.8212316036 - 0.8217726946 0.8217726946 0.8217726946 - 0.8223139048 0.8223139048 0.8223139048 - 0.8228551149 0.8228551149 0.8228551149 - 0.8233962059 0.8233962059 0.8233962059 - 0.8239374161 0.8239374161 0.8239374161 - 0.8244786263 0.8244786263 0.8244786263 - 0.8250197768 0.8250197768 0.8250197768 - 0.8255609274 0.8255609274 0.8255609274 - 0.8261020780 0.8261020780 0.8261020780 - 0.8266432881 0.8266432881 0.8266432881 - 0.8271844983 0.8271844983 0.8271844983 - 0.8277255893 0.8277255893 0.8277255893 - 0.8282667994 0.8282667994 0.8282667994 - 0.8288080096 0.8288080096 0.8288080096 - 0.8293492198 0.8293492198 0.8293492198 - 0.8298903108 0.8298903108 0.8298903108 - 0.8304091096 0.8304091096 0.8304091096 - 0.8309221864 0.8309221864 0.8309221864 - 0.8314353228 0.8314353228 0.8314353228 - 0.8319483995 0.8319483995 0.8319483995 - 0.8324614763 0.8324614763 0.8324614763 - 0.8329746127 0.8329746127 0.8329746127 - 0.8334876895 0.8334876895 0.8334876895 - 0.8340008259 0.8340008259 0.8340008259 - 0.8345139027 0.8345139027 0.8345139027 - 0.8350269794 0.8350269794 0.8350269794 - 0.8355401158 0.8355401158 0.8355401158 - 0.8360531926 0.8360531926 0.8360531926 - 0.8365663290 0.8365663290 0.8365663290 - 0.8370794058 0.8370794058 0.8370794058 - 0.8375924826 0.8375924826 0.8375924826 - 0.8381056190 0.8381056190 0.8381056190 - 0.8386186957 0.8386186957 0.8386186957 - 0.8391317725 0.8391317725 0.8391317725 - 0.8396449089 0.8396449089 0.8396449089 - 0.8401497006 0.8401497006 0.8401497006 - 0.8406361938 0.8406361938 0.8406361938 - 0.8411226273 0.8411226273 0.8411226273 - 0.8416090012 0.8416090012 0.8416090012 - 0.8420953751 0.8420953751 0.8420953751 - 0.8425818086 0.8425818086 0.8425818086 - 0.8430681825 0.8430681825 0.8430681825 - 0.8435546160 0.8435546160 0.8435546160 - 0.8440409899 0.8440409899 0.8440409899 - 0.8445274234 0.8445274234 0.8445274234 - 0.8450137973 0.8450137973 0.8450137973 - 0.8455001712 0.8455001712 0.8455001712 - 0.8459866047 0.8459866047 0.8459866047 - 0.8464729786 0.8464729786 0.8464729786 - 0.8469594121 0.8469594121 0.8469594121 - 0.8474457860 0.8474457860 0.8474457860 - 0.8479322195 0.8479322195 0.8479322195 - 0.8484185934 0.8484185934 0.8484185934 - 0.8489050269 0.8489050269 0.8489050269 - 0.8493914008 0.8493914008 0.8493914008 - 0.8498777747 0.8498777747 0.8498777747 - 0.8503453135 0.8503453135 0.8503453135 - 0.8508062959 0.8508062959 0.8508062959 - 0.8512673974 0.8512673974 0.8512673974 - 0.8517283797 0.8517283797 0.8517283797 - 0.8521894813 0.8521894813 0.8521894813 - 0.8526505828 0.8526505828 0.8526505828 - 0.8531116247 0.8531116247 0.8531116247 - 0.8535727262 0.8535727262 0.8535727262 - 0.8540337086 0.8540337086 0.8540337086 - 0.8544948101 0.8544948101 0.8544948101 - 0.8549559116 0.8549559116 0.8549559116 - 0.8554168940 0.8554168940 0.8554168940 - 0.8558779955 0.8558779955 0.8558779955 - 0.8563390970 0.8563390970 0.8563390970 - 0.8568000793 0.8568000793 0.8568000793 - 0.8572611809 0.8572611809 0.8572611809 - 0.8577222228 0.8577222228 0.8577222228 - 0.8581833243 0.8581833243 0.8581833243 - 0.8586444259 0.8586444259 0.8586444259 - 0.8591054082 0.8591054082 0.8591054082 - 0.8595665097 0.8595665097 0.8595665097 - 0.8600261211 0.8600261211 0.8600261211 - 0.8604630828 0.8604630828 0.8604630828 - 0.8609001040 0.8609001040 0.8609001040 - 0.8613371849 0.8613371849 0.8613371849 - 0.8617742062 0.8617742062 0.8617742062 - 0.8622112274 0.8622112274 0.8622112274 - 0.8626481891 0.8626481891 0.8626481891 - 0.8630852103 0.8630852103 0.8630852103 - 0.8635222912 0.8635222912 0.8635222912 - 0.8639593124 0.8639593124 0.8639593124 - 0.8643962741 0.8643962741 0.8643962741 - 0.8648332953 0.8648332953 0.8648332953 - 0.8652703166 0.8652703166 0.8652703166 - 0.8657073975 0.8657073975 0.8657073975 - 0.8661444187 0.8661444187 0.8661444187 - 0.8665813804 0.8665813804 0.8665813804 - 0.8670184016 0.8670184016 0.8670184016 - 0.8674554229 0.8674554229 0.8674554229 - 0.8678925037 0.8678925037 0.8678925037 - 0.8683295250 0.8683295250 0.8683295250 - 0.8687664866 0.8687664866 0.8687664866 - 0.8692035079 0.8692035079 0.8692035079 - 0.8696405888 0.8696405888 0.8696405888 - 0.8700734973 0.8700734973 0.8700734973 - 0.8704878092 0.8704878092 0.8704878092 - 0.8709020019 0.8709020019 0.8709020019 - 0.8713161945 0.8713161945 0.8713161945 - 0.8717305064 0.8717305064 0.8717305064 - 0.8721446991 0.8721446991 0.8721446991 - 0.8725590110 0.8725590110 0.8725590110 - 0.8729732037 0.8729732037 0.8729732037 - 0.8733873963 0.8733873963 0.8733873963 - 0.8738017082 0.8738017082 0.8738017082 - 0.8742159009 0.8742159009 0.8742159009 - 0.8746302128 0.8746302128 0.8746302128 - 0.8750444055 0.8750444055 0.8750444055 - 0.8754585981 0.8754585981 0.8754585981 - 0.8758729100 0.8758729100 0.8758729100 - 0.8762871027 0.8762871027 0.8762871027 - 0.8767014146 0.8767014146 0.8767014146 - 0.8771156073 0.8771156073 0.8771156073 - 0.8775297999 0.8775297999 0.8775297999 - 0.8779441118 0.8779441118 0.8779441118 - 0.8783583045 0.8783583045 0.8783583045 - 0.8787726164 0.8787726164 0.8787726164 - 0.8791868091 0.8791868091 0.8791868091 - 0.8796010017 0.8796010017 0.8796010017 - 0.8800144792 0.8800144792 0.8800144792 - 0.8804072142 0.8804072142 0.8804072142 - 0.8807997704 0.8807997704 0.8807997704 - 0.8811925054 0.8811925054 0.8811925054 - 0.8815851808 0.8815851808 0.8815851808 - 0.8819777966 0.8819777966 0.8819777966 - 0.8823704720 0.8823704720 0.8823704720 - 0.8827632070 0.8827632070 0.8827632070 - 0.8831558228 0.8831558228 0.8831558228 - 0.8835484982 0.8835484982 0.8835484982 - 0.8839411736 0.8839411736 0.8839411736 - 0.8843337893 0.8843337893 0.8843337893 - 0.8847265244 0.8847265244 0.8847265244 - 0.8851191998 0.8851191998 0.8851191998 - 0.8855118752 0.8855118752 0.8855118752 - 0.8859044909 0.8859044909 0.8859044909 - 0.8862972260 0.8862972260 0.8862972260 - 0.8866899014 0.8866899014 0.8866899014 - 0.8870825171 0.8870825171 0.8870825171 - 0.8874751925 0.8874751925 0.8874751925 - 0.8878679276 0.8878679276 0.8878679276 - 0.8882604837 0.8882604837 0.8882604837 - 0.8886532187 0.8886532187 0.8886532187 - 0.8890458941 0.8890458941 0.8890458941 - 0.8894385099 0.8894385099 0.8894385099 - 0.8898311853 0.8898311853 0.8898311853 - 0.8902121782 0.8902121782 0.8902121782 - 0.8905845284 0.8905845284 0.8905845284 - 0.8909568191 0.8909568191 0.8909568191 - 0.8913289905 0.8913289905 0.8913289905 - 0.8917012811 0.8917012811 0.8917012811 - 0.8920735121 0.8920735121 0.8920735121 - 0.8924458027 0.8924458027 0.8924458027 - 0.8928180933 0.8928180933 0.8928180933 - 0.8931903243 0.8931903243 0.8931903243 - 0.8935626149 0.8935626149 0.8935626149 - 0.8939347863 0.8939347863 0.8939347863 - 0.8943070769 0.8943070769 0.8943070769 - 0.8946794271 0.8946794271 0.8946794271 - 0.8950515985 0.8950515985 0.8950515985 - 0.8954238892 0.8954238892 0.8954238892 - 0.8957961202 0.8957961202 0.8957961202 - 0.8961684108 0.8961684108 0.8961684108 - 0.8965405822 0.8965405822 0.8965405822 - 0.8969128728 0.8969128728 0.8969128728 - 0.8972852230 0.8972852230 0.8972852230 - 0.8976573944 0.8976573944 0.8976573944 - 0.8980296850 0.8980296850 0.8980296850 - 0.8984019160 0.8984019160 0.8984019160 - 0.8987742066 0.8987742066 0.8987742066 - 0.8991464972 0.8991464972 0.8991464972 - 0.8995187283 0.8995187283 0.8995187283 - 0.8998910189 0.8998910189 0.8998910189 - 0.9002496004 0.9002496004 0.9002496004 - 0.9006025195 0.9006025195 0.9006025195 - 0.9009554982 0.9009554982 0.9009554982 - 0.9013084769 0.9013084769 0.9013084769 - 0.9016613960 0.9016613960 0.9016613960 - 0.9020143747 0.9020143747 0.9020143747 - 0.9023672938 0.9023672938 0.9023672938 - 0.9027202725 0.9027202725 0.9027202725 - 0.9030731916 0.9030731916 0.9030731916 - 0.9034261703 0.9034261703 0.9034261703 - 0.9037792087 0.9037792087 0.9037792087 - 0.9041321278 0.9041321278 0.9041321278 - 0.9044851065 0.9044851065 0.9044851065 - 0.9048380256 0.9048380256 0.9048380256 - 0.9051910043 0.9051910043 0.9051910043 - 0.9055439234 0.9055439234 0.9055439234 - 0.9058969021 0.9058969021 0.9058969021 - 0.9062498808 0.9062498808 0.9062498808 - 0.9066027999 0.9066027999 0.9066027999 - 0.9069557786 0.9069557786 0.9069557786 - 0.9073086977 0.9073086977 0.9073086977 - 0.9076616764 0.9076616764 0.9076616764 - 0.9080145955 0.9080145955 0.9080145955 - 0.9083675742 0.9083675742 0.9083675742 - 0.9087206125 0.9087206125 0.9087206125 - 0.9090734720 0.9090734720 0.9090734720 - 0.9094265103 0.9094265103 0.9094265103 - 0.9097794294 0.9097794294 0.9097794294 - 0.9101254940 0.9101254940 0.9101254940 - 0.9104602933 0.9104602933 0.9104602933 - 0.9107949734 0.9107949734 0.9107949734 - 0.9111297131 0.9111297131 0.9111297131 - 0.9114643931 0.9114643931 0.9114643931 - 0.9117990732 0.9117990732 0.9117990732 - 0.9121338129 0.9121338129 0.9121338129 - 0.9124684930 0.9124684930 0.9124684930 - 0.9128032923 0.9128032923 0.9128032923 - 0.9131379724 0.9131379724 0.9131379724 - 0.9134727120 0.9134727120 0.9134727120 - 0.9138073921 0.9138073921 0.9138073921 - 0.9141420722 0.9141420722 0.9141420722 - 0.9144768119 0.9144768119 0.9144768119 - 0.9148114920 0.9148114920 0.9148114920 - 0.9151462913 0.9151462913 0.9151462913 - 0.9154809713 0.9154809713 0.9154809713 - 0.9158157110 0.9158157110 0.9158157110 - 0.9161503911 0.9161503911 0.9161503911 - 0.9164850712 0.9164850712 0.9164850712 - 0.9168198109 0.9168198109 0.9168198109 - 0.9171544909 0.9171544909 0.9171544909 - 0.9174892902 0.9174892902 0.9174892902 - 0.9178239703 0.9178239703 0.9178239703 - 0.9181587100 0.9181587100 0.9181587100 - 0.9184933901 0.9184933901 0.9184933901 - 0.9188281298 0.9188281298 0.9188281298 - 0.9191628098 0.9191628098 0.9191628098 - 0.9194974899 0.9194974899 0.9194974899 - 0.9198322892 0.9198322892 0.9198322892 - 0.9201583862 0.9201583862 0.9201583862 - 0.9204757810 0.9204757810 0.9204757810 - 0.9207932949 0.9207932949 0.9207932949 - 0.9211108088 0.9211108088 0.9211108088 - 0.9214283228 0.9214283228 0.9214283228 - 0.9217457771 0.9217457771 0.9217457771 - 0.9220631719 0.9220631719 0.9220631719 - 0.9223806858 0.9223806858 0.9223806858 - 0.9226981997 0.9226981997 0.9226981997 - 0.9230157137 0.9230157137 0.9230157137 - 0.9233332276 0.9233332276 0.9233332276 - 0.9236506224 0.9236506224 0.9236506224 - 0.9239680767 0.9239680767 0.9239680767 - 0.9242855906 0.9242855906 0.9242855906 - 0.9246031046 0.9246031046 0.9246031046 - 0.9249204993 0.9249204993 0.9249204993 - 0.9252380133 0.9252380133 0.9252380133 - 0.9255555272 0.9255555272 0.9255555272 - 0.9258729815 0.9258729815 0.9258729815 - 0.9261904955 0.9261904955 0.9261904955 - 0.9265078902 0.9265078902 0.9265078902 - 0.9268254042 0.9268254042 0.9268254042 - 0.9271429181 0.9271429181 0.9271429181 - 0.9274603724 0.9274603724 0.9274603724 - 0.9277778268 0.9277778268 0.9277778268 - 0.9280952811 0.9280952811 0.9280952811 - 0.9284127951 0.9284127951 0.9284127951 - 0.9287303090 0.9287303090 0.9287303090 - 0.9290478230 0.9290478230 0.9290478230 - 0.9293652177 0.9293652177 0.9293652177 - 0.9296826720 0.9296826720 0.9296826720 - 0.9300001860 0.9300001860 0.9300001860 - 0.9303014278 0.9303014278 0.9303014278 - 0.9306026101 0.9306026101 0.9306026101 - 0.9309037924 0.9309037924 0.9309037924 - 0.9312049747 0.9312049747 0.9312049747 - 0.9315062165 0.9315062165 0.9315062165 - 0.9318073988 0.9318073988 0.9318073988 - 0.9321085811 0.9321085811 0.9321085811 - 0.9324098229 0.9324098229 0.9324098229 - 0.9327110052 0.9327110052 0.9327110052 - 0.9330121875 0.9330121875 0.9330121875 - 0.9333134294 0.9333134294 0.9333134294 - 0.9336146116 0.9336146116 0.9336146116 - 0.9339157939 0.9339157939 0.9339157939 - 0.9342169762 0.9342169762 0.9342169762 - 0.9345182180 0.9345182180 0.9345182180 - 0.9348194003 0.9348194003 0.9348194003 - 0.9351205826 0.9351205826 0.9351205826 - 0.9354218245 0.9354218245 0.9354218245 - 0.9357230067 0.9357230067 0.9357230067 - 0.9360241890 0.9360241890 0.9360241890 - 0.9363253713 0.9363253713 0.9363253713 - 0.9366264939 0.9366264939 0.9366264939 - 0.9369276762 0.9369276762 0.9369276762 - 0.9372289181 0.9372289181 0.9372289181 - 0.9375301003 0.9375301003 0.9375301003 - 0.9378312826 0.9378312826 0.9378312826 - 0.9381325245 0.9381325245 0.9381325245 - 0.9384337068 0.9384337068 0.9384337068 - 0.9387348890 0.9387348890 0.9387348890 - 0.9390360713 0.9390360713 0.9390360713 - 0.9393373132 0.9393373132 0.9393373132 - 0.9396384954 0.9396384954 0.9396384954 - 0.9399396777 0.9399396777 0.9399396777 - 0.9402285814 0.9402285814 0.9402285814 - 0.9405145049 0.9405145049 0.9405145049 - 0.9408003092 0.9408003092 0.9408003092 - 0.9410861135 0.9410861135 0.9410861135 - 0.9413719177 0.9413719177 0.9413719177 - 0.9416577816 0.9416577816 0.9416577816 - 0.9419435859 0.9419435859 0.9419435859 - 0.9422293901 0.9422293901 0.9422293901 - 0.9425153136 0.9425153136 0.9425153136 - 0.9428011179 0.9428011179 0.9428011179 - 0.9430869222 0.9430869222 0.9430869222 - 0.9433727264 0.9433727264 0.9433727264 - 0.9436585903 0.9436585903 0.9436585903 - 0.9439443946 0.9439443946 0.9439443946 - 0.9442301989 0.9442301989 0.9442301989 - 0.9445160031 0.9445160031 0.9445160031 - 0.9448019266 0.9448019266 0.9448019266 - 0.9450876713 0.9450876713 0.9450876713 - 0.9453734756 0.9453734756 0.9453734756 - 0.9456593990 0.9456593990 0.9456593990 - 0.9459452033 0.9459452033 0.9459452033 - 0.9462310076 0.9462310076 0.9462310076 - 0.9465168118 0.9465168118 0.9465168118 - 0.9468026757 0.9468026757 0.9468026757 - 0.9470884800 0.9470884800 0.9470884800 - 0.9473742843 0.9473742843 0.9473742843 - 0.9476600885 0.9476600885 0.9476600885 - 0.9479460120 0.9479460120 0.9479460120 - 0.9482318163 0.9482318163 0.9482318163 - 0.9485176206 0.9485176206 0.9485176206 - 0.9488034248 0.9488034248 0.9488034248 - 0.9490892887 0.9490892887 0.9490892887 - 0.9493750930 0.9493750930 0.9493750930 - 0.9496608973 0.9496608973 0.9496608973 - 0.9499468207 0.9499468207 0.9499468207 - 0.9502208233 0.9502208233 0.9502208233 - 0.9504920840 0.9504920840 0.9504920840 - 0.9507634044 0.9507634044 0.9507634044 - 0.9510347247 0.9510347247 0.9510347247 - 0.9513059855 0.9513059855 0.9513059855 - 0.9515774250 0.9515774250 0.9515774250 - 0.9518486857 0.9518486857 0.9518486857 - 0.9521200061 0.9521200061 0.9521200061 - 0.9523913264 0.9523913264 0.9523913264 - 0.9526625872 0.9526625872 0.9526625872 - 0.9529339075 0.9529339075 0.9529339075 - 0.9532052279 0.9532052279 0.9532052279 - 0.9534766078 0.9534766078 0.9534766078 - 0.9537479281 0.9537479281 0.9537479281 - 0.9540191889 0.9540191889 0.9540191889 - 0.9542905092 0.9542905092 0.9542905092 - 0.9545618296 0.9545618296 0.9545618296 - 0.9548330903 0.9548330903 0.9548330903 - 0.9551044106 0.9551044106 0.9551044106 - 0.9553757906 0.9553757906 0.9553757906 - 0.9556471109 0.9556471109 0.9556471109 - 0.9559183717 0.9559183717 0.9559183717 - 0.9561896920 0.9561896920 0.9561896920 - 0.9564610124 0.9564610124 0.9564610124 - 0.9567322731 0.9567322731 0.9567322731 - 0.9570037127 0.9570037127 0.9570037127 - 0.9572749734 0.9572749734 0.9572749734 - 0.9575462937 0.9575462937 0.9575462937 - 0.9578176141 0.9578176141 0.9578176141 - 0.9580888748 0.9580888748 0.9580888748 - 0.9583601952 0.9583601952 0.9583601952 - 0.9586315155 0.9586315155 0.9586315155 - 0.9589028955 0.9589028955 0.9589028955 - 0.9591742158 0.9591742158 0.9591742158 - 0.9594454765 0.9594454765 0.9594454765 - 0.9597167969 0.9597167969 0.9597167969 - 0.9599881172 0.9599881172 0.9599881172 - 0.9602463245 0.9602463245 0.9602463245 - 0.9605039954 0.9605039954 0.9605039954 - 0.9607616067 0.9607616067 0.9607616067 - 0.9610192180 0.9610192180 0.9610192180 - 0.9612768292 0.9612768292 0.9612768292 - 0.9615343809 0.9615343809 0.9615343809 - 0.9617919922 0.9617919922 0.9617919922 - 0.9620497227 0.9620497227 0.9620497227 - 0.9623072743 0.9623072743 0.9623072743 - 0.9625648856 0.9625648856 0.9625648856 - 0.9628224969 0.9628224969 0.9628224969 - 0.9630801082 0.9630801082 0.9630801082 - 0.9633377194 0.9633377194 0.9633377194 - 0.9635953903 0.9635953903 0.9635953903 - 0.9638530016 0.9638530016 0.9638530016 - 0.9641106129 0.9641106129 0.9641106129 - 0.9643682241 0.9643682241 0.9643682241 - 0.9646257758 0.9646257758 0.9646257758 - 0.9648833871 0.9648833871 0.9648833871 - 0.9651411176 0.9651411176 0.9651411176 - 0.9653987288 0.9653987288 0.9653987288 - 0.9656562805 0.9656562805 0.9656562805 - 0.9659138918 0.9659138918 0.9659138918 - 0.9661715031 0.9661715031 0.9661715031 - 0.9664291143 0.9664291143 0.9664291143 - 0.9666867852 0.9666867852 0.9666867852 - 0.9669443965 0.9669443965 0.9669443965 - 0.9672020078 0.9672020078 0.9672020078 - 0.9674596190 0.9674596190 0.9674596190 - 0.9677171707 0.9677171707 0.9677171707 - 0.9679747820 0.9679747820 0.9679747820 - 0.9682325125 0.9682325125 0.9682325125 - 0.9684901237 0.9684901237 0.9684901237 - 0.9687476754 0.9687476754 0.9687476754 - 0.9690052867 0.9690052867 0.9690052867 - 0.9692628980 0.9692628980 0.9692628980 - 0.9695205092 0.9695205092 0.9695205092 - 0.9697781801 0.9697781801 0.9697781801 - 0.9700340033 0.9700340033 0.9700340033 - 0.9702786803 0.9702786803 0.9702786803 - 0.9705232978 0.9705232978 0.9705232978 - 0.9707679749 0.9707679749 0.9707679749 - 0.9710127115 0.9710127115 0.9710127115 - 0.9712573886 0.9712573886 0.9712573886 - 0.9715021253 0.9715021253 0.9715021253 - 0.9717468023 0.9717468023 0.9717468023 - 0.9719914794 0.9719914794 0.9719914794 - 0.9722362161 0.9722362161 0.9722362161 - 0.9724807739 0.9724807739 0.9724807739 - 0.9727255106 0.9727255106 0.9727255106 - 0.9729701877 0.9729701877 0.9729701877 - 0.9732149243 0.9732149243 0.9732149243 - 0.9734596014 0.9734596014 0.9734596014 - 0.9737042785 0.9737042785 0.9737042785 - 0.9739490151 0.9739490151 0.9739490151 - 0.9741935730 0.9741935730 0.9741935730 - 0.9744383097 0.9744383097 0.9744383097 - 0.9746829867 0.9746829867 0.9746829867 - 0.9749277234 0.9749277234 0.9749277234 - 0.9751724005 0.9751724005 0.9751724005 - 0.9754170775 0.9754170775 0.9754170775 - 0.9756618142 0.9756618142 0.9756618142 - 0.9759064913 0.9759064913 0.9759064913 - 0.9761511087 0.9761511087 0.9761511087 - 0.9763957858 0.9763957858 0.9763957858 - 0.9766405225 0.9766405225 0.9766405225 - 0.9768851995 0.9768851995 0.9768851995 - 0.9771298766 0.9771298766 0.9771298766 - 0.9773746133 0.9773746133 0.9773746133 - 0.9776192904 0.9776192904 0.9776192904 - 0.9778640270 0.9778640270 0.9778640270 - 0.9781085849 0.9781085849 0.9781085849 - 0.9783533216 0.9783533216 0.9783533216 - 0.9785979986 0.9785979986 0.9785979986 - 0.9788426757 0.9788426757 0.9788426757 - 0.9790874124 0.9790874124 0.9790874124 - 0.9793320894 0.9793320894 0.9793320894 - 0.9795768261 0.9795768261 0.9795768261 - 0.9798213840 0.9798213840 0.9798213840 - 0.9800627828 0.9800627828 0.9800627828 - 0.9802953005 0.9802953005 0.9802953005 - 0.9805278182 0.9805278182 0.9805278182 - 0.9807602763 0.9807602763 0.9807602763 - 0.9809927940 0.9809927940 0.9809927940 - 0.9812251925 0.9812251925 0.9812251925 - 0.9814577103 0.9814577103 0.9814577103 - 0.9816902280 0.9816902280 0.9816902280 - 0.9819226861 0.9819226861 0.9819226861 - 0.9821552038 0.9821552038 0.9821552038 - 0.9823877215 0.9823877215 0.9823877215 - 0.9826201200 0.9826201200 0.9826201200 - 0.9828525782 0.9828525782 0.9828525782 - 0.9830850959 0.9830850959 0.9830850959 - 0.9833176136 0.9833176136 0.9833176136 - 0.9835500717 0.9835500717 0.9835500717 - 0.9837825894 0.9837825894 0.9837825894 - 0.9840149879 0.9840149879 0.9840149879 - 0.9842475057 0.9842475057 0.9842475057 - 0.9844800234 0.9844800234 0.9844800234 - 0.9847124815 0.9847124815 0.9847124815 - 0.9849449992 0.9849449992 0.9849449992 - 0.9851775169 0.9851775169 0.9851775169 - 0.9854099154 0.9854099154 0.9854099154 - 0.9856423736 0.9856423736 0.9856423736 - 0.9858748913 0.9858748913 0.9858748913 - 0.9861074090 0.9861074090 0.9861074090 - 0.9863399267 0.9863399267 0.9863399267 - 0.9865723848 0.9865723848 0.9865723848 - 0.9868047833 0.9868047833 0.9868047833 - 0.9870373011 0.9870373011 0.9870373011 - 0.9872698188 0.9872698188 0.9872698188 - 0.9875022769 0.9875022769 0.9875022769 - 0.9877347946 0.9877347946 0.9877347946 - 0.9879673123 0.9879673123 0.9879673123 - 0.9881997108 0.9881997108 0.9881997108 - 0.9884322286 0.9884322286 0.9884322286 - 0.9886646867 0.9886646867 0.9886646867 - 0.9888972044 0.9888972044 0.9888972044 - 0.9891297221 0.9891297221 0.9891297221 - 0.9893621206 0.9893621206 0.9893621206 - 0.9895945787 0.9895945787 0.9895945787 - 0.9898270965 0.9898270965 0.9898270965 - 0.9900565743 0.9900565743 0.9900565743 - 0.9902775884 0.9902775884 0.9902775884 - 0.9904986024 0.9904986024 0.9904986024 - 0.9907194972 0.9907194972 0.9907194972 - 0.9909405112 0.9909405112 0.9909405112 - 0.9911615252 0.9911615252 0.9911615252 - 0.9913824201 0.9913824201 0.9913824201 - 0.9916033745 0.9916033745 0.9916033745 - 0.9918243885 0.9918243885 0.9918243885 - 0.9920452833 0.9920452833 0.9920452833 - 0.9922662973 0.9922662973 0.9922662973 - 0.9924871922 0.9924871922 0.9924871922 - 0.9927082062 0.9927082062 0.9927082062 - 0.9929292202 0.9929292202 0.9929292202 - 0.9931501150 0.9931501150 0.9931501150 - 0.9933711290 0.9933711290 0.9933711290 - 0.9935920835 0.9935920835 0.9935920835 - 0.9938129783 0.9938129783 0.9938129783 - 0.9940339923 0.9940339923 0.9940339923 - 0.9942548871 0.9942548871 0.9942548871 - 0.9944759011 0.9944759011 0.9944759011 - 0.9946969151 0.9946969151 0.9946969151 - 0.9949178100 0.9949178100 0.9949178100 - 0.9951388240 0.9951388240 0.9951388240 - 0.9953597784 0.9953597784 0.9953597784 - 0.9955806732 0.9955806732 0.9955806732 - 0.9958016872 0.9958016872 0.9958016872 - 0.9960227013 0.9960227013 0.9960227013 - 0.9962435961 0.9962435961 0.9962435961 - 0.9964646101 0.9964646101 0.9964646101 - 0.9966855049 0.9966855049 0.9966855049 - 0.9969065189 0.9969065189 0.9969065189 - 0.9971274734 0.9971274734 0.9971274734 - 0.9973484278 0.9973484278 0.9973484278 - 0.9975693822 0.9975693822 0.9975693822 - 0.9977903962 0.9977903962 0.9977903962 - 0.9980112910 0.9980112910 0.9980112910 - 0.9982323050 0.9982323050 0.9982323050 - 0.9984533191 0.9984533191 0.9984533191 - 0.9986742139 0.9986742139 0.9986742139 - 0.9988952279 0.9988952279 0.9988952279 - 0.9991161227 0.9991161227 0.9991161227 - 0.9993370771 0.9993370771 0.9993370771 - 0.9995580912 0.9995580912 0.9995580912 - 0.9997789860 0.9997789860 0.9997789860 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0048120469 0.0048120469 0.0048120469 + 0.0096240947 0.0096240947 0.0096240947 + 0.0145009197 0.0145009197 0.0145009197 + 0.0193832405 0.0193832405 0.0193832405 + 0.0242968407 0.0242968407 0.0242968407 + 0.0292149596 0.0292149596 0.0292149596 + 0.0341353305 0.0341353305 0.0341353305 + 0.0390561298 0.0390561298 0.0390561298 + 0.0439544208 0.0439544208 0.0439544208 + 0.0488473810 0.0488473810 0.0488473810 + 0.0536984690 0.0536984690 0.0536984690 + 0.0585366711 0.0585366711 0.0585366711 + 0.0633208230 0.0633208230 0.0633208230 + 0.0680815428 0.0680815428 0.0680815428 + 0.0727850720 0.0727850720 0.0727850720 + 0.0774499997 0.0774499997 0.0774499997 + 0.0820651725 0.0820651725 0.0820651725 + 0.0866203383 0.0866203383 0.0866203383 + 0.0911446288 0.0911446288 0.0911446288 + 0.0955801234 0.0955801234 0.0955801234 + 0.1000152007 0.1000152007 0.1000152007 + 0.1043246016 0.1043246016 0.1043246016 + 0.1086340994 0.1086340994 0.1086340994 + 0.1128552034 0.1128552034 0.1128552034 + 0.1170352995 0.1170352995 0.1170352995 + 0.1211775988 0.1211775988 0.1211775988 + 0.1252277046 0.1252277046 0.1252277046 + 0.1292777061 0.1292777061 0.1292777061 + 0.1332218945 0.1332218945 0.1332218945 + 0.1371431053 0.1371431053 0.1371431053 + 0.1410301030 0.1410301030 0.1410301030 + 0.1448252946 0.1448252946 0.1448252946 + 0.1486205012 0.1486205012 0.1486205012 + 0.1523379982 0.1523379982 0.1523379982 + 0.1560111046 0.1560111046 0.1560111046 + 0.1596841961 0.1596841961 0.1596841961 + 0.1632499993 0.1632499993 0.1632499993 + 0.1668058038 0.1668058038 0.1668058038 + 0.1703501940 0.1703501940 0.1703501940 + 0.1737938970 0.1737938970 0.1737938970 + 0.1772377044 0.1772377044 0.1772377044 + 0.1806603968 0.1806603968 0.1806603968 + 0.1839977056 0.1839977056 0.1839977056 + 0.1873349994 0.1873349994 0.1873349994 + 0.1906519979 0.1906519979 0.1906519979 + 0.1938886046 0.1938886046 0.1938886046 + 0.1971253008 0.1971253008 0.1971253008 + 0.2003512979 0.2003512979 0.2003512979 + 0.2034929991 0.2034929991 0.2034929991 + 0.2066347003 0.2066347003 0.2066347003 + 0.2097764015 0.2097764015 0.2097764015 + 0.2128352970 0.2128352970 0.2128352970 + 0.2158876956 0.2158876956 0.2158876956 + 0.2189401984 0.2189401984 0.2189401984 + 0.2219379991 0.2219379991 0.2219379991 + 0.2249068022 0.2249068022 0.2249068022 + 0.2278755009 0.2278755009 0.2278755009 + 0.2308219969 0.2308219969 0.2308219969 + 0.2337123007 0.2337123007 0.2337123007 + 0.2366026938 0.2366026938 0.2366026938 + 0.2394929975 0.2394929975 0.2394929975 + 0.2423229069 0.2423229069 0.2423229069 + 0.2451398969 0.2451398969 0.2451398969 + 0.2479569018 0.2479569018 0.2479569018 + 0.2507550120 0.2507550120 0.2507550120 + 0.2535035014 0.2535035014 0.2535035014 + 0.2562519908 0.2562519908 0.2562519908 + 0.2590003908 0.2590003908 0.2590003908 + 0.2617082000 0.2617082000 0.2617082000 + 0.2643926144 0.2643926144 0.2643926144 + 0.2670770884 0.2670770884 0.2670770884 + 0.2697615027 0.2697615027 0.2697615027 + 0.2723914981 0.2723914981 0.2723914981 + 0.2750161886 0.2750161886 0.2750161886 + 0.2776409090 0.2776409090 0.2776409090 + 0.2802599967 0.2802599967 0.2802599967 + 0.2828288972 0.2828288972 0.2828288972 + 0.2853977978 0.2853977978 0.2853977978 + 0.2879666984 0.2879666984 0.2879666984 + 0.2905246913 0.2905246913 0.2905246913 + 0.2930414975 0.2930414975 0.2930414975 + 0.2955583036 0.2955583036 0.2955583036 + 0.2980751097 0.2980751097 0.2980751097 + 0.3005805016 0.3005805016 0.3005805016 + 0.3030486107 0.3030486107 0.3030486107 + 0.3055166900 0.3055166900 0.3055166900 + 0.3079847991 0.3079847991 0.3079847991 + 0.3104445040 0.3104445040 0.3104445040 + 0.3128669858 0.3128669858 0.3128669858 + 0.3152895868 0.3152895868 0.3152895868 + 0.3177120984 0.3177120984 0.3177120984 + 0.3201321959 0.3201321959 0.3201321959 + 0.3225120902 0.3225120902 0.3225120902 + 0.3248918951 0.3248918951 0.3248918951 + 0.3272717893 0.3272717893 0.3272717893 + 0.3296515942 0.3296515942 0.3296515942 + 0.3319971859 0.3319971859 0.3319971859 + 0.3343369961 0.3343369961 0.3343369961 + 0.3366766870 0.3366766870 0.3366766870 + 0.3390164971 0.3390164971 0.3390164971 + 0.3413344026 0.3413344026 0.3413344026 + 0.3436363935 0.3436363935 0.3436363935 + 0.3459384143 0.3459384143 0.3459384143 + 0.3482404947 0.3482404947 0.3482404947 + 0.3505341113 0.3505341113 0.3505341113 + 0.3528004885 0.3528004885 0.3528004885 + 0.3550668955 0.3550668955 0.3550668955 + 0.3573333025 0.3573333025 0.3573333025 + 0.3595997095 0.3595997095 0.3595997095 + 0.3618383110 0.3618383110 0.3618383110 + 0.3640710115 0.3640710115 0.3640710115 + 0.3663035929 0.3663035929 0.3663035929 + 0.3685362935 0.3685362935 0.3685362935 + 0.3707579076 0.3707579076 0.3707579076 + 0.3729583919 0.3729583919 0.3729583919 + 0.3751589060 0.3751589060 0.3751589060 + 0.3773593903 0.3773593903 0.3773593903 + 0.3795599043 0.3795599043 0.3795599043 + 0.3817358911 0.3817358911 0.3817358911 + 0.3839055896 0.3839055896 0.3839055896 + 0.3860754073 0.3860754073 0.3860754073 + 0.3882451952 0.3882451952 0.3882451952 + 0.3904092908 0.3904092908 0.3904092908 + 0.3925495148 0.3925495148 0.3925495148 + 0.3946897089 0.3946897089 0.3946897089 + 0.3968299031 0.3968299031 0.3968299031 + 0.3989700973 0.3989700973 0.3989700973 + 0.4010953903 0.4010953903 0.4010953903 + 0.4032069147 0.4032069147 0.4032069147 + 0.4053184092 0.4053184092 0.4053184092 + 0.4074299037 0.4074299037 0.4074299037 + 0.4095414877 0.4095414877 0.4095414877 + 0.4116311073 0.4116311073 0.4116311073 + 0.4137147069 0.4137147069 0.4137147069 + 0.4157981873 0.4157981873 0.4157981873 + 0.4178817868 0.4178817868 0.4178817868 + 0.4199653864 0.4199653864 0.4199653864 + 0.4220218956 0.4220218956 0.4220218956 + 0.4240779877 0.4240779877 0.4240779877 + 0.4261341095 0.4261341095 0.4261341095 + 0.4281901121 0.4281901121 0.4281901121 + 0.4302429855 0.4302429855 0.4302429855 + 0.4322718084 0.4322718084 0.4322718084 + 0.4343006909 0.4343006909 0.4343006909 + 0.4363296032 0.4363296032 0.4363296032 + 0.4383583963 0.4383583963 0.4383583963 + 0.4403820932 0.4403820932 0.4403820932 + 0.4423837960 0.4423837960 0.4423837960 + 0.4443854988 0.4443854988 0.4443854988 + 0.4463872015 0.4463872015 0.4463872015 + 0.4483889043 0.4483889043 0.4483889043 + 0.4503852129 0.4503852129 0.4503852129 + 0.4523595870 0.4523595870 0.4523595870 + 0.4543339908 0.4543339908 0.4543339908 + 0.4563083053 0.4563083053 0.4563083053 + 0.4582827091 0.4582827091 0.4582827091 + 0.4602535069 0.4602535069 0.4602535069 + 0.4622001946 0.4622001946 0.4622001946 + 0.4641467929 0.4641467929 0.4641467929 + 0.4660935104 0.4660935104 0.4660935104 + 0.4680401981 0.4680401981 0.4680401981 + 0.4699868858 0.4699868858 0.4699868858 + 0.4719055891 0.4719055891 0.4719055891 + 0.4738239944 0.4738239944 0.4738239944 + 0.4757424891 0.4757424891 0.4757424891 + 0.4776608944 0.4776608944 0.4776608944 + 0.4795793891 0.4795793891 0.4795793891 + 0.4814752936 0.4814752936 0.4814752936 + 0.4833647907 0.4833647907 0.4833647907 + 0.4852542877 0.4852542877 0.4852542877 + 0.4871438146 0.4871438146 0.4871438146 + 0.4890333116 0.4890333116 0.4890333116 + 0.4909082949 0.4909082949 0.4909082949 + 0.4927679002 0.4927679002 0.4927679002 + 0.4946275949 0.4946275949 0.4946275949 + 0.4964872897 0.4964872897 0.4964872897 + 0.4983470142 0.4983470142 0.4983470142 + 0.5002033114 0.5002033114 0.5002033114 + 0.5020321012 0.5020321012 0.5020321012 + 0.5038608909 0.5038608909 0.5038608909 + 0.5056896806 0.5056896806 0.5056896806 + 0.5075185895 0.5075185895 0.5075185895 + 0.5093473792 0.5093473792 0.5093473792 + 0.5111556053 0.5111556053 0.5111556053 + 0.5129525065 0.5129525065 0.5129525065 + 0.5147492886 0.5147492886 0.5147492886 + 0.5165460706 0.5165460706 0.5165460706 + 0.5183429122 0.5183429122 0.5183429122 + 0.5201370716 0.5201370716 0.5201370716 + 0.5219007134 0.5219007134 0.5219007134 + 0.5236642957 0.5236642957 0.5236642957 + 0.5254278183 0.5254278183 0.5254278183 + 0.5271914005 0.5271914005 0.5271914005 + 0.5289549232 0.5289549232 0.5289549232 + 0.5307043791 0.5307043791 0.5307043791 + 0.5324332714 0.5324332714 0.5324332714 + 0.5341622829 0.5341622829 0.5341622829 + 0.5358911753 0.5358911753 0.5358911753 + 0.5376201868 0.5376201868 0.5376201868 + 0.5393490791 0.5393490791 0.5393490791 + 0.5410556197 0.5410556197 0.5410556197 + 0.5427485704 0.5427485704 0.5427485704 + 0.5444415212 0.5444415212 0.5444415212 + 0.5461344719 0.5461344719 0.5461344719 + 0.5478274226 0.5478274226 0.5478274226 + 0.5495203733 0.5495203733 0.5495203733 + 0.5511866212 0.5511866212 0.5511866212 + 0.5528420806 0.5528420806 0.5528420806 + 0.5544977188 0.5544977188 0.5544977188 + 0.5561531782 0.5561531782 0.5561531782 + 0.5578088164 0.5578088164 0.5578088164 + 0.5594642758 0.5594642758 0.5594642758 + 0.5610936284 0.5610936284 0.5610936284 + 0.5627104044 0.5627104044 0.5627104044 + 0.5643271208 0.5643271208 0.5643271208 + 0.5659437776 0.5659437776 0.5659437776 + 0.5675606132 0.5675606132 0.5675606132 + 0.5691773295 0.5691773295 0.5691773295 + 0.5707743168 0.5707743168 0.5707743168 + 0.5723509192 0.5723509192 0.5723509192 + 0.5739275217 0.5739275217 0.5739275217 + 0.5755040050 0.5755040050 0.5755040050 + 0.5770806074 0.5770806074 0.5770806074 + 0.5786570907 0.5786570907 0.5786570907 + 0.5802274942 0.5802274942 0.5802274942 + 0.5817626119 0.5817626119 0.5817626119 + 0.5832977295 0.5832977295 0.5832977295 + 0.5848327279 0.5848327279 0.5848327279 + 0.5863677859 0.5863677859 0.5863677859 + 0.5879029036 0.5879029036 0.5879029036 + 0.5894379020 0.5894379020 0.5894379020 + 0.5909458995 0.5909458995 0.5909458995 + 0.5924382806 0.5924382806 0.5924382806 + 0.5939307213 0.5939307213 0.5939307213 + 0.5954229832 0.5954229832 0.5954229832 + 0.5969154239 0.5969154239 0.5969154239 + 0.5984076858 0.5984076858 0.5984076858 + 0.5999001265 0.5999001265 0.5999001265 + 0.6013516188 0.6013516188 0.6013516188 + 0.6028001904 0.6028001904 0.6028001904 + 0.6042487025 0.6042487025 0.6042487025 + 0.6056972742 0.6056972742 0.6056972742 + 0.6071457863 0.6071457863 0.6071457863 + 0.6085944176 0.6085944176 0.6085944176 + 0.6100416183 0.6100416183 0.6100416183 + 0.6114454269 0.6114454269 0.6114454269 + 0.6128491759 0.6128491759 0.6128491759 + 0.6142529845 0.6142529845 0.6142529845 + 0.6156567931 0.6156567931 0.6156567931 + 0.6170606017 0.6170606017 0.6170606017 + 0.6184644103 0.6184644103 0.6184644103 + 0.6198682785 0.6198682785 0.6198682785 + 0.6212307811 0.6212307811 0.6212307811 + 0.6225889921 0.6225889921 0.6225889921 + 0.6239473224 0.6239473224 0.6239473224 + 0.6253055930 0.6253055930 0.6253055930 + 0.6266638041 0.6266638041 0.6266638041 + 0.6280220747 0.6280220747 0.6280220747 + 0.6293802857 0.6293802857 0.6293802857 + 0.6307135224 0.6307135224 0.6307135224 + 0.6320255995 0.6320255995 0.6320255995 + 0.6333376765 0.6333376765 0.6333376765 + 0.6346498132 0.6346498132 0.6346498132 + 0.6359618902 0.6359618902 0.6359618902 + 0.6372740269 0.6372740269 0.6372740269 + 0.6385861039 0.6385861039 0.6385861039 + 0.6398981810 0.6398981810 0.6398981810 + 0.6411672831 0.6411672831 0.6411672831 + 0.6424328089 0.6424328089 0.6424328089 + 0.6436982751 0.6436982751 0.6436982751 + 0.6449638009 0.6449638009 0.6449638009 + 0.6462293863 0.6462293863 0.6462293863 + 0.6474949121 0.6474949121 0.6474949121 + 0.6487603784 0.6487603784 0.6487603784 + 0.6500248909 0.6500248909 0.6500248909 + 0.6512436271 0.6512436271 0.6512436271 + 0.6524624228 0.6524624228 0.6524624228 + 0.6536810994 0.6536810994 0.6536810994 + 0.6548997760 0.6548997760 0.6548997760 + 0.6561185122 0.6561185122 0.6561185122 + 0.6573371887 0.6573371887 0.6573371887 + 0.6585559249 0.6585559249 0.6585559249 + 0.6597746015 0.6597746015 0.6597746015 + 0.6609550714 0.6609550714 0.6609550714 + 0.6621270180 0.6621270180 0.6621270180 + 0.6632987857 0.6632987857 0.6632987857 + 0.6644706726 0.6644706726 0.6644706726 + 0.6656426191 0.6656426191 0.6656426191 + 0.6668143868 0.6668143868 0.6668143868 + 0.6679862738 0.6679862738 0.6679862738 + 0.6691582203 0.6691582203 0.6691582203 + 0.6703168750 0.6703168750 0.6703168750 + 0.6714420915 0.6714420915 0.6714420915 + 0.6725673079 0.6725673079 0.6725673079 + 0.6736925244 0.6736925244 0.6736925244 + 0.6748176813 0.6748176813 0.6748176813 + 0.6759428978 0.6759428978 0.6759428978 + 0.6770681143 0.6770681143 0.6770681143 + 0.6781932712 0.6781932712 0.6781932712 + 0.6793184876 0.6793184876 0.6793184876 + 0.6804254055 0.6804254055 0.6804254055 + 0.6815043092 0.6815043092 0.6815043092 + 0.6825832129 0.6825832129 0.6825832129 + 0.6836621165 0.6836621165 0.6836621165 + 0.6847409010 0.6847409010 0.6847409010 + 0.6858198047 0.6858198047 0.6858198047 + 0.6868987083 0.6868987083 0.6868987083 + 0.6879776120 0.6879776120 0.6879776120 + 0.6890565157 0.6890565157 0.6890565157 + 0.6901295781 0.6901295781 0.6901295781 + 0.6911628246 0.6911628246 0.6911628246 + 0.6921958923 0.6921958923 0.6921958923 + 0.6932290196 0.6932290196 0.6932290196 + 0.6942620873 0.6942620873 0.6942620873 + 0.6952952147 0.6952952147 0.6952952147 + 0.6963282824 0.6963282824 0.6963282824 + 0.6973615289 0.6973615289 0.6973615289 + 0.6983945966 0.6983945966 0.6983945966 + 0.6994277239 0.6994277239 0.6994277239 + 0.7004407048 0.7004407048 0.7004407048 + 0.7014287710 0.7014287710 0.7014287710 + 0.7024167776 0.7024167776 0.7024167776 + 0.7034049034 0.7034049034 0.7034049034 + 0.7043930292 0.7043930292 0.7043930292 + 0.7053809762 0.7053809762 0.7053809762 + 0.7063691020 0.7063691020 0.7063691020 + 0.7073571086 0.7073571086 0.7073571086 + 0.7083451748 0.7083451748 0.7083451748 + 0.7093333006 0.7093333006 0.7093333006 + 0.7103068829 0.7103068829 0.7103068829 + 0.7112507820 0.7112507820 0.7112507820 + 0.7121946812 0.7121946812 0.7121946812 + 0.7131385207 0.7131385207 0.7131385207 + 0.7140824199 0.7140824199 0.7140824199 + 0.7150263190 0.7150263190 0.7150263190 + 0.7159700990 0.7159700990 0.7159700990 + 0.7169139981 0.7169139981 0.7169139981 + 0.7178577781 0.7178577781 0.7178577781 + 0.7188016772 0.7188016772 0.7188016772 + 0.7197455764 0.7197455764 0.7197455764 + 0.7206578851 0.7206578851 0.7206578851 + 0.7215585113 0.7215585113 0.7215585113 + 0.7224591970 0.7224591970 0.7224591970 + 0.7233598828 0.7233598828 0.7233598828 + 0.7242605090 0.7242605090 0.7242605090 + 0.7251611948 0.7251611948 0.7251611948 + 0.7260618806 0.7260618806 0.7260618806 + 0.7269625068 0.7269625068 0.7269625068 + 0.7278631926 0.7278631926 0.7278631926 + 0.7287638783 0.7287638783 0.7287638783 + 0.7296645045 0.7296645045 0.7296645045 + 0.7305387855 0.7305387855 0.7305387855 + 0.7313973904 0.7313973904 0.7313973904 + 0.7322559953 0.7322559953 0.7322559953 + 0.7331146002 0.7331146002 0.7331146002 + 0.7339732051 0.7339732051 0.7339732051 + 0.7348316908 0.7348316908 0.7348316908 + 0.7356902957 0.7356902957 0.7356902957 + 0.7365489006 0.7365489006 0.7365489006 + 0.7374075055 0.7374075055 0.7374075055 + 0.7382661104 0.7382661104 0.7382661104 + 0.7391247153 0.7391247153 0.7391247153 + 0.7399833202 0.7399833202 0.7399833202 + 0.7408018112 0.7408018112 0.7408018112 + 0.7416195273 0.7416195273 0.7416195273 + 0.7424373031 0.7424373031 0.7424373031 + 0.7432550192 0.7432550192 0.7432550192 + 0.7440726757 0.7440726757 0.7440726757 + 0.7448903918 0.7448903918 0.7448903918 + 0.7457082272 0.7457082272 0.7457082272 + 0.7465258837 0.7465258837 0.7465258837 + 0.7473435998 0.7473435998 0.7473435998 + 0.7481613755 0.7481613755 0.7481613755 + 0.7489790916 0.7489790916 0.7489790916 + 0.7497968078 0.7497968078 0.7497968078 + 0.7505847812 0.7505847812 0.7505847812 + 0.7513629794 0.7513629794 0.7513629794 + 0.7521411777 0.7521411777 0.7521411777 + 0.7529193759 0.7529193759 0.7529193759 + 0.7536975145 0.7536975145 0.7536975145 + 0.7544757128 0.7544757128 0.7544757128 + 0.7552539110 0.7552539110 0.7552539110 + 0.7560319901 0.7560319901 0.7560319901 + 0.7568101883 0.7568101883 0.7568101883 + 0.7575883865 0.7575883865 0.7575883865 + 0.7583665848 0.7583665848 0.7583665848 + 0.7591447234 0.7591447234 0.7591447234 + 0.7599229217 0.7599229217 0.7599229217 + 0.7606667280 0.7606667280 0.7606667280 + 0.7614067197 0.7614067197 0.7614067197 + 0.7621465921 0.7621465921 0.7621465921 + 0.7628865838 0.7628865838 0.7628865838 + 0.7636265755 0.7636265755 0.7636265755 + 0.7643666267 0.7643666267 0.7643666267 + 0.7651066184 0.7651066184 0.7651066184 + 0.7658466101 0.7658466101 0.7658466101 + 0.7665864825 0.7665864825 0.7665864825 + 0.7673264742 0.7673264742 0.7673264742 + 0.7680665255 0.7680665255 0.7680665255 + 0.7688065171 0.7688065171 0.7688065171 + 0.7695465088 0.7695465088 0.7695465088 + 0.7702721953 0.7702721953 0.7702721953 + 0.7709754109 0.7709754109 0.7709754109 + 0.7716786265 0.7716786265 0.7716786265 + 0.7723817825 0.7723817825 0.7723817825 + 0.7730849981 0.7730849981 0.7730849981 + 0.7737882733 0.7737882733 0.7737882733 + 0.7744914889 0.7744914889 0.7744914889 + 0.7751947045 0.7751947045 0.7751947045 + 0.7758979201 0.7758979201 0.7758979201 + 0.7766010761 0.7766010761 0.7766010761 + 0.7773042917 0.7773042917 0.7773042917 + 0.7780075073 0.7780075073 0.7780075073 + 0.7787107229 0.7787107229 0.7787107229 + 0.7794138789 0.7794138789 0.7794138789 + 0.7801111937 0.7801111937 0.7801111937 + 0.7807791233 0.7807791233 0.7807791233 + 0.7814469934 0.7814469934 0.7814469934 + 0.7821149230 0.7821149230 0.7821149230 + 0.7827826738 0.7827826738 0.7827826738 + 0.7834506035 0.7834506035 0.7834506035 + 0.7841184735 0.7841184735 0.7841184735 + 0.7847864032 0.7847864032 0.7847864032 + 0.7854542136 0.7854542136 0.7854542136 + 0.7861220837 0.7861220837 0.7861220837 + 0.7867900133 0.7867900133 0.7867900133 + 0.7874578834 0.7874578834 0.7874578834 + 0.7881256938 0.7881256938 0.7881256938 + 0.7887936234 0.7887936234 0.7887936234 + 0.7894614935 0.7894614935 0.7894614935 + 0.7901228070 0.7901228070 0.7901228070 + 0.7907568216 0.7907568216 0.7907568216 + 0.7913907766 0.7913907766 0.7913907766 + 0.7920247912 0.7920247912 0.7920247912 + 0.7926588058 0.7926588058 0.7926588058 + 0.7932928801 0.7932928801 0.7932928801 + 0.7939268947 0.7939268947 0.7939268947 + 0.7945609093 0.7945609093 0.7945609093 + 0.7951949239 0.7951949239 0.7951949239 + 0.7958288789 0.7958288789 0.7958288789 + 0.7964628935 0.7964628935 0.7964628935 + 0.7970969081 0.7970969081 0.7970969081 + 0.7977309227 0.7977309227 0.7977309227 + 0.7983648777 0.7983648777 0.7983648777 + 0.7989988923 0.7989988923 0.7989988923 + 0.7996329069 0.7996329069 0.7996329069 + 0.8002532721 0.8002532721 0.8002532721 + 0.8008549213 0.8008549213 0.8008549213 + 0.8014565110 0.8014565110 0.8014565110 + 0.8020581007 0.8020581007 0.8020581007 + 0.8026596904 0.8026596904 0.8026596904 + 0.8032613993 0.8032613993 0.8032613993 + 0.8038629889 0.8038629889 0.8038629889 + 0.8044645786 0.8044645786 0.8044645786 + 0.8050662279 0.8050662279 0.8050662279 + 0.8056678176 0.8056678176 0.8056678176 + 0.8062694073 0.8062694073 0.8062694073 + 0.8068709970 0.8068709970 0.8068709970 + 0.8074725866 0.8074725866 0.8074725866 + 0.8080741763 0.8080741763 0.8080741763 + 0.8086758256 0.8086758256 0.8086758256 + 0.8092774153 0.8092774153 0.8092774153 + 0.8098791242 0.8098791242 0.8098791242 + 0.8104559183 0.8104559183 0.8104559183 + 0.8110265732 0.8110265732 0.8110265732 + 0.8115972877 0.8115972877 0.8115972877 + 0.8121680021 0.8121680021 0.8121680021 + 0.8127385974 0.8127385974 0.8127385974 + 0.8133093119 0.8133093119 0.8133093119 + 0.8138800263 0.8138800263 0.8138800263 + 0.8144506216 0.8144506216 0.8144506216 + 0.8150212765 0.8150212765 0.8150212765 + 0.8155919909 0.8155919909 0.8155919909 + 0.8161625862 0.8161625862 0.8161625862 + 0.8167333007 0.8167333007 0.8167333007 + 0.8173040152 0.8173040152 0.8173040152 + 0.8178747296 0.8178747296 0.8178747296 + 0.8184453249 0.8184453249 0.8184453249 + 0.8190159798 0.8190159798 0.8190159798 + 0.8195866942 0.8195866942 0.8195866942 + 0.8201491833 0.8201491833 0.8201491833 + 0.8206903934 0.8206903934 0.8206903934 + 0.8212316036 0.8212316036 0.8212316036 + 0.8217726946 0.8217726946 0.8217726946 + 0.8223139048 0.8223139048 0.8223139048 + 0.8228551149 0.8228551149 0.8228551149 + 0.8233962059 0.8233962059 0.8233962059 + 0.8239374161 0.8239374161 0.8239374161 + 0.8244786263 0.8244786263 0.8244786263 + 0.8250197768 0.8250197768 0.8250197768 + 0.8255609274 0.8255609274 0.8255609274 + 0.8261020780 0.8261020780 0.8261020780 + 0.8266432881 0.8266432881 0.8266432881 + 0.8271844983 0.8271844983 0.8271844983 + 0.8277255893 0.8277255893 0.8277255893 + 0.8282667994 0.8282667994 0.8282667994 + 0.8288080096 0.8288080096 0.8288080096 + 0.8293492198 0.8293492198 0.8293492198 + 0.8298903108 0.8298903108 0.8298903108 + 0.8304091096 0.8304091096 0.8304091096 + 0.8309221864 0.8309221864 0.8309221864 + 0.8314353228 0.8314353228 0.8314353228 + 0.8319483995 0.8319483995 0.8319483995 + 0.8324614763 0.8324614763 0.8324614763 + 0.8329746127 0.8329746127 0.8329746127 + 0.8334876895 0.8334876895 0.8334876895 + 0.8340008259 0.8340008259 0.8340008259 + 0.8345139027 0.8345139027 0.8345139027 + 0.8350269794 0.8350269794 0.8350269794 + 0.8355401158 0.8355401158 0.8355401158 + 0.8360531926 0.8360531926 0.8360531926 + 0.8365663290 0.8365663290 0.8365663290 + 0.8370794058 0.8370794058 0.8370794058 + 0.8375924826 0.8375924826 0.8375924826 + 0.8381056190 0.8381056190 0.8381056190 + 0.8386186957 0.8386186957 0.8386186957 + 0.8391317725 0.8391317725 0.8391317725 + 0.8396449089 0.8396449089 0.8396449089 + 0.8401497006 0.8401497006 0.8401497006 + 0.8406361938 0.8406361938 0.8406361938 + 0.8411226273 0.8411226273 0.8411226273 + 0.8416090012 0.8416090012 0.8416090012 + 0.8420953751 0.8420953751 0.8420953751 + 0.8425818086 0.8425818086 0.8425818086 + 0.8430681825 0.8430681825 0.8430681825 + 0.8435546160 0.8435546160 0.8435546160 + 0.8440409899 0.8440409899 0.8440409899 + 0.8445274234 0.8445274234 0.8445274234 + 0.8450137973 0.8450137973 0.8450137973 + 0.8455001712 0.8455001712 0.8455001712 + 0.8459866047 0.8459866047 0.8459866047 + 0.8464729786 0.8464729786 0.8464729786 + 0.8469594121 0.8469594121 0.8469594121 + 0.8474457860 0.8474457860 0.8474457860 + 0.8479322195 0.8479322195 0.8479322195 + 0.8484185934 0.8484185934 0.8484185934 + 0.8489050269 0.8489050269 0.8489050269 + 0.8493914008 0.8493914008 0.8493914008 + 0.8498777747 0.8498777747 0.8498777747 + 0.8503453135 0.8503453135 0.8503453135 + 0.8508062959 0.8508062959 0.8508062959 + 0.8512673974 0.8512673974 0.8512673974 + 0.8517283797 0.8517283797 0.8517283797 + 0.8521894813 0.8521894813 0.8521894813 + 0.8526505828 0.8526505828 0.8526505828 + 0.8531116247 0.8531116247 0.8531116247 + 0.8535727262 0.8535727262 0.8535727262 + 0.8540337086 0.8540337086 0.8540337086 + 0.8544948101 0.8544948101 0.8544948101 + 0.8549559116 0.8549559116 0.8549559116 + 0.8554168940 0.8554168940 0.8554168940 + 0.8558779955 0.8558779955 0.8558779955 + 0.8563390970 0.8563390970 0.8563390970 + 0.8568000793 0.8568000793 0.8568000793 + 0.8572611809 0.8572611809 0.8572611809 + 0.8577222228 0.8577222228 0.8577222228 + 0.8581833243 0.8581833243 0.8581833243 + 0.8586444259 0.8586444259 0.8586444259 + 0.8591054082 0.8591054082 0.8591054082 + 0.8595665097 0.8595665097 0.8595665097 + 0.8600261211 0.8600261211 0.8600261211 + 0.8604630828 0.8604630828 0.8604630828 + 0.8609001040 0.8609001040 0.8609001040 + 0.8613371849 0.8613371849 0.8613371849 + 0.8617742062 0.8617742062 0.8617742062 + 0.8622112274 0.8622112274 0.8622112274 + 0.8626481891 0.8626481891 0.8626481891 + 0.8630852103 0.8630852103 0.8630852103 + 0.8635222912 0.8635222912 0.8635222912 + 0.8639593124 0.8639593124 0.8639593124 + 0.8643962741 0.8643962741 0.8643962741 + 0.8648332953 0.8648332953 0.8648332953 + 0.8652703166 0.8652703166 0.8652703166 + 0.8657073975 0.8657073975 0.8657073975 + 0.8661444187 0.8661444187 0.8661444187 + 0.8665813804 0.8665813804 0.8665813804 + 0.8670184016 0.8670184016 0.8670184016 + 0.8674554229 0.8674554229 0.8674554229 + 0.8678925037 0.8678925037 0.8678925037 + 0.8683295250 0.8683295250 0.8683295250 + 0.8687664866 0.8687664866 0.8687664866 + 0.8692035079 0.8692035079 0.8692035079 + 0.8696405888 0.8696405888 0.8696405888 + 0.8700734973 0.8700734973 0.8700734973 + 0.8704878092 0.8704878092 0.8704878092 + 0.8709020019 0.8709020019 0.8709020019 + 0.8713161945 0.8713161945 0.8713161945 + 0.8717305064 0.8717305064 0.8717305064 + 0.8721446991 0.8721446991 0.8721446991 + 0.8725590110 0.8725590110 0.8725590110 + 0.8729732037 0.8729732037 0.8729732037 + 0.8733873963 0.8733873963 0.8733873963 + 0.8738017082 0.8738017082 0.8738017082 + 0.8742159009 0.8742159009 0.8742159009 + 0.8746302128 0.8746302128 0.8746302128 + 0.8750444055 0.8750444055 0.8750444055 + 0.8754585981 0.8754585981 0.8754585981 + 0.8758729100 0.8758729100 0.8758729100 + 0.8762871027 0.8762871027 0.8762871027 + 0.8767014146 0.8767014146 0.8767014146 + 0.8771156073 0.8771156073 0.8771156073 + 0.8775297999 0.8775297999 0.8775297999 + 0.8779441118 0.8779441118 0.8779441118 + 0.8783583045 0.8783583045 0.8783583045 + 0.8787726164 0.8787726164 0.8787726164 + 0.8791868091 0.8791868091 0.8791868091 + 0.8796010017 0.8796010017 0.8796010017 + 0.8800144792 0.8800144792 0.8800144792 + 0.8804072142 0.8804072142 0.8804072142 + 0.8807997704 0.8807997704 0.8807997704 + 0.8811925054 0.8811925054 0.8811925054 + 0.8815851808 0.8815851808 0.8815851808 + 0.8819777966 0.8819777966 0.8819777966 + 0.8823704720 0.8823704720 0.8823704720 + 0.8827632070 0.8827632070 0.8827632070 + 0.8831558228 0.8831558228 0.8831558228 + 0.8835484982 0.8835484982 0.8835484982 + 0.8839411736 0.8839411736 0.8839411736 + 0.8843337893 0.8843337893 0.8843337893 + 0.8847265244 0.8847265244 0.8847265244 + 0.8851191998 0.8851191998 0.8851191998 + 0.8855118752 0.8855118752 0.8855118752 + 0.8859044909 0.8859044909 0.8859044909 + 0.8862972260 0.8862972260 0.8862972260 + 0.8866899014 0.8866899014 0.8866899014 + 0.8870825171 0.8870825171 0.8870825171 + 0.8874751925 0.8874751925 0.8874751925 + 0.8878679276 0.8878679276 0.8878679276 + 0.8882604837 0.8882604837 0.8882604837 + 0.8886532187 0.8886532187 0.8886532187 + 0.8890458941 0.8890458941 0.8890458941 + 0.8894385099 0.8894385099 0.8894385099 + 0.8898311853 0.8898311853 0.8898311853 + 0.8902121782 0.8902121782 0.8902121782 + 0.8905845284 0.8905845284 0.8905845284 + 0.8909568191 0.8909568191 0.8909568191 + 0.8913289905 0.8913289905 0.8913289905 + 0.8917012811 0.8917012811 0.8917012811 + 0.8920735121 0.8920735121 0.8920735121 + 0.8924458027 0.8924458027 0.8924458027 + 0.8928180933 0.8928180933 0.8928180933 + 0.8931903243 0.8931903243 0.8931903243 + 0.8935626149 0.8935626149 0.8935626149 + 0.8939347863 0.8939347863 0.8939347863 + 0.8943070769 0.8943070769 0.8943070769 + 0.8946794271 0.8946794271 0.8946794271 + 0.8950515985 0.8950515985 0.8950515985 + 0.8954238892 0.8954238892 0.8954238892 + 0.8957961202 0.8957961202 0.8957961202 + 0.8961684108 0.8961684108 0.8961684108 + 0.8965405822 0.8965405822 0.8965405822 + 0.8969128728 0.8969128728 0.8969128728 + 0.8972852230 0.8972852230 0.8972852230 + 0.8976573944 0.8976573944 0.8976573944 + 0.8980296850 0.8980296850 0.8980296850 + 0.8984019160 0.8984019160 0.8984019160 + 0.8987742066 0.8987742066 0.8987742066 + 0.8991464972 0.8991464972 0.8991464972 + 0.8995187283 0.8995187283 0.8995187283 + 0.8998910189 0.8998910189 0.8998910189 + 0.9002496004 0.9002496004 0.9002496004 + 0.9006025195 0.9006025195 0.9006025195 + 0.9009554982 0.9009554982 0.9009554982 + 0.9013084769 0.9013084769 0.9013084769 + 0.9016613960 0.9016613960 0.9016613960 + 0.9020143747 0.9020143747 0.9020143747 + 0.9023672938 0.9023672938 0.9023672938 + 0.9027202725 0.9027202725 0.9027202725 + 0.9030731916 0.9030731916 0.9030731916 + 0.9034261703 0.9034261703 0.9034261703 + 0.9037792087 0.9037792087 0.9037792087 + 0.9041321278 0.9041321278 0.9041321278 + 0.9044851065 0.9044851065 0.9044851065 + 0.9048380256 0.9048380256 0.9048380256 + 0.9051910043 0.9051910043 0.9051910043 + 0.9055439234 0.9055439234 0.9055439234 + 0.9058969021 0.9058969021 0.9058969021 + 0.9062498808 0.9062498808 0.9062498808 + 0.9066027999 0.9066027999 0.9066027999 + 0.9069557786 0.9069557786 0.9069557786 + 0.9073086977 0.9073086977 0.9073086977 + 0.9076616764 0.9076616764 0.9076616764 + 0.9080145955 0.9080145955 0.9080145955 + 0.9083675742 0.9083675742 0.9083675742 + 0.9087206125 0.9087206125 0.9087206125 + 0.9090734720 0.9090734720 0.9090734720 + 0.9094265103 0.9094265103 0.9094265103 + 0.9097794294 0.9097794294 0.9097794294 + 0.9101254940 0.9101254940 0.9101254940 + 0.9104602933 0.9104602933 0.9104602933 + 0.9107949734 0.9107949734 0.9107949734 + 0.9111297131 0.9111297131 0.9111297131 + 0.9114643931 0.9114643931 0.9114643931 + 0.9117990732 0.9117990732 0.9117990732 + 0.9121338129 0.9121338129 0.9121338129 + 0.9124684930 0.9124684930 0.9124684930 + 0.9128032923 0.9128032923 0.9128032923 + 0.9131379724 0.9131379724 0.9131379724 + 0.9134727120 0.9134727120 0.9134727120 + 0.9138073921 0.9138073921 0.9138073921 + 0.9141420722 0.9141420722 0.9141420722 + 0.9144768119 0.9144768119 0.9144768119 + 0.9148114920 0.9148114920 0.9148114920 + 0.9151462913 0.9151462913 0.9151462913 + 0.9154809713 0.9154809713 0.9154809713 + 0.9158157110 0.9158157110 0.9158157110 + 0.9161503911 0.9161503911 0.9161503911 + 0.9164850712 0.9164850712 0.9164850712 + 0.9168198109 0.9168198109 0.9168198109 + 0.9171544909 0.9171544909 0.9171544909 + 0.9174892902 0.9174892902 0.9174892902 + 0.9178239703 0.9178239703 0.9178239703 + 0.9181587100 0.9181587100 0.9181587100 + 0.9184933901 0.9184933901 0.9184933901 + 0.9188281298 0.9188281298 0.9188281298 + 0.9191628098 0.9191628098 0.9191628098 + 0.9194974899 0.9194974899 0.9194974899 + 0.9198322892 0.9198322892 0.9198322892 + 0.9201583862 0.9201583862 0.9201583862 + 0.9204757810 0.9204757810 0.9204757810 + 0.9207932949 0.9207932949 0.9207932949 + 0.9211108088 0.9211108088 0.9211108088 + 0.9214283228 0.9214283228 0.9214283228 + 0.9217457771 0.9217457771 0.9217457771 + 0.9220631719 0.9220631719 0.9220631719 + 0.9223806858 0.9223806858 0.9223806858 + 0.9226981997 0.9226981997 0.9226981997 + 0.9230157137 0.9230157137 0.9230157137 + 0.9233332276 0.9233332276 0.9233332276 + 0.9236506224 0.9236506224 0.9236506224 + 0.9239680767 0.9239680767 0.9239680767 + 0.9242855906 0.9242855906 0.9242855906 + 0.9246031046 0.9246031046 0.9246031046 + 0.9249204993 0.9249204993 0.9249204993 + 0.9252380133 0.9252380133 0.9252380133 + 0.9255555272 0.9255555272 0.9255555272 + 0.9258729815 0.9258729815 0.9258729815 + 0.9261904955 0.9261904955 0.9261904955 + 0.9265078902 0.9265078902 0.9265078902 + 0.9268254042 0.9268254042 0.9268254042 + 0.9271429181 0.9271429181 0.9271429181 + 0.9274603724 0.9274603724 0.9274603724 + 0.9277778268 0.9277778268 0.9277778268 + 0.9280952811 0.9280952811 0.9280952811 + 0.9284127951 0.9284127951 0.9284127951 + 0.9287303090 0.9287303090 0.9287303090 + 0.9290478230 0.9290478230 0.9290478230 + 0.9293652177 0.9293652177 0.9293652177 + 0.9296826720 0.9296826720 0.9296826720 + 0.9300001860 0.9300001860 0.9300001860 + 0.9303014278 0.9303014278 0.9303014278 + 0.9306026101 0.9306026101 0.9306026101 + 0.9309037924 0.9309037924 0.9309037924 + 0.9312049747 0.9312049747 0.9312049747 + 0.9315062165 0.9315062165 0.9315062165 + 0.9318073988 0.9318073988 0.9318073988 + 0.9321085811 0.9321085811 0.9321085811 + 0.9324098229 0.9324098229 0.9324098229 + 0.9327110052 0.9327110052 0.9327110052 + 0.9330121875 0.9330121875 0.9330121875 + 0.9333134294 0.9333134294 0.9333134294 + 0.9336146116 0.9336146116 0.9336146116 + 0.9339157939 0.9339157939 0.9339157939 + 0.9342169762 0.9342169762 0.9342169762 + 0.9345182180 0.9345182180 0.9345182180 + 0.9348194003 0.9348194003 0.9348194003 + 0.9351205826 0.9351205826 0.9351205826 + 0.9354218245 0.9354218245 0.9354218245 + 0.9357230067 0.9357230067 0.9357230067 + 0.9360241890 0.9360241890 0.9360241890 + 0.9363253713 0.9363253713 0.9363253713 + 0.9366264939 0.9366264939 0.9366264939 + 0.9369276762 0.9369276762 0.9369276762 + 0.9372289181 0.9372289181 0.9372289181 + 0.9375301003 0.9375301003 0.9375301003 + 0.9378312826 0.9378312826 0.9378312826 + 0.9381325245 0.9381325245 0.9381325245 + 0.9384337068 0.9384337068 0.9384337068 + 0.9387348890 0.9387348890 0.9387348890 + 0.9390360713 0.9390360713 0.9390360713 + 0.9393373132 0.9393373132 0.9393373132 + 0.9396384954 0.9396384954 0.9396384954 + 0.9399396777 0.9399396777 0.9399396777 + 0.9402285814 0.9402285814 0.9402285814 + 0.9405145049 0.9405145049 0.9405145049 + 0.9408003092 0.9408003092 0.9408003092 + 0.9410861135 0.9410861135 0.9410861135 + 0.9413719177 0.9413719177 0.9413719177 + 0.9416577816 0.9416577816 0.9416577816 + 0.9419435859 0.9419435859 0.9419435859 + 0.9422293901 0.9422293901 0.9422293901 + 0.9425153136 0.9425153136 0.9425153136 + 0.9428011179 0.9428011179 0.9428011179 + 0.9430869222 0.9430869222 0.9430869222 + 0.9433727264 0.9433727264 0.9433727264 + 0.9436585903 0.9436585903 0.9436585903 + 0.9439443946 0.9439443946 0.9439443946 + 0.9442301989 0.9442301989 0.9442301989 + 0.9445160031 0.9445160031 0.9445160031 + 0.9448019266 0.9448019266 0.9448019266 + 0.9450876713 0.9450876713 0.9450876713 + 0.9453734756 0.9453734756 0.9453734756 + 0.9456593990 0.9456593990 0.9456593990 + 0.9459452033 0.9459452033 0.9459452033 + 0.9462310076 0.9462310076 0.9462310076 + 0.9465168118 0.9465168118 0.9465168118 + 0.9468026757 0.9468026757 0.9468026757 + 0.9470884800 0.9470884800 0.9470884800 + 0.9473742843 0.9473742843 0.9473742843 + 0.9476600885 0.9476600885 0.9476600885 + 0.9479460120 0.9479460120 0.9479460120 + 0.9482318163 0.9482318163 0.9482318163 + 0.9485176206 0.9485176206 0.9485176206 + 0.9488034248 0.9488034248 0.9488034248 + 0.9490892887 0.9490892887 0.9490892887 + 0.9493750930 0.9493750930 0.9493750930 + 0.9496608973 0.9496608973 0.9496608973 + 0.9499468207 0.9499468207 0.9499468207 + 0.9502208233 0.9502208233 0.9502208233 + 0.9504920840 0.9504920840 0.9504920840 + 0.9507634044 0.9507634044 0.9507634044 + 0.9510347247 0.9510347247 0.9510347247 + 0.9513059855 0.9513059855 0.9513059855 + 0.9515774250 0.9515774250 0.9515774250 + 0.9518486857 0.9518486857 0.9518486857 + 0.9521200061 0.9521200061 0.9521200061 + 0.9523913264 0.9523913264 0.9523913264 + 0.9526625872 0.9526625872 0.9526625872 + 0.9529339075 0.9529339075 0.9529339075 + 0.9532052279 0.9532052279 0.9532052279 + 0.9534766078 0.9534766078 0.9534766078 + 0.9537479281 0.9537479281 0.9537479281 + 0.9540191889 0.9540191889 0.9540191889 + 0.9542905092 0.9542905092 0.9542905092 + 0.9545618296 0.9545618296 0.9545618296 + 0.9548330903 0.9548330903 0.9548330903 + 0.9551044106 0.9551044106 0.9551044106 + 0.9553757906 0.9553757906 0.9553757906 + 0.9556471109 0.9556471109 0.9556471109 + 0.9559183717 0.9559183717 0.9559183717 + 0.9561896920 0.9561896920 0.9561896920 + 0.9564610124 0.9564610124 0.9564610124 + 0.9567322731 0.9567322731 0.9567322731 + 0.9570037127 0.9570037127 0.9570037127 + 0.9572749734 0.9572749734 0.9572749734 + 0.9575462937 0.9575462937 0.9575462937 + 0.9578176141 0.9578176141 0.9578176141 + 0.9580888748 0.9580888748 0.9580888748 + 0.9583601952 0.9583601952 0.9583601952 + 0.9586315155 0.9586315155 0.9586315155 + 0.9589028955 0.9589028955 0.9589028955 + 0.9591742158 0.9591742158 0.9591742158 + 0.9594454765 0.9594454765 0.9594454765 + 0.9597167969 0.9597167969 0.9597167969 + 0.9599881172 0.9599881172 0.9599881172 + 0.9602463245 0.9602463245 0.9602463245 + 0.9605039954 0.9605039954 0.9605039954 + 0.9607616067 0.9607616067 0.9607616067 + 0.9610192180 0.9610192180 0.9610192180 + 0.9612768292 0.9612768292 0.9612768292 + 0.9615343809 0.9615343809 0.9615343809 + 0.9617919922 0.9617919922 0.9617919922 + 0.9620497227 0.9620497227 0.9620497227 + 0.9623072743 0.9623072743 0.9623072743 + 0.9625648856 0.9625648856 0.9625648856 + 0.9628224969 0.9628224969 0.9628224969 + 0.9630801082 0.9630801082 0.9630801082 + 0.9633377194 0.9633377194 0.9633377194 + 0.9635953903 0.9635953903 0.9635953903 + 0.9638530016 0.9638530016 0.9638530016 + 0.9641106129 0.9641106129 0.9641106129 + 0.9643682241 0.9643682241 0.9643682241 + 0.9646257758 0.9646257758 0.9646257758 + 0.9648833871 0.9648833871 0.9648833871 + 0.9651411176 0.9651411176 0.9651411176 + 0.9653987288 0.9653987288 0.9653987288 + 0.9656562805 0.9656562805 0.9656562805 + 0.9659138918 0.9659138918 0.9659138918 + 0.9661715031 0.9661715031 0.9661715031 + 0.9664291143 0.9664291143 0.9664291143 + 0.9666867852 0.9666867852 0.9666867852 + 0.9669443965 0.9669443965 0.9669443965 + 0.9672020078 0.9672020078 0.9672020078 + 0.9674596190 0.9674596190 0.9674596190 + 0.9677171707 0.9677171707 0.9677171707 + 0.9679747820 0.9679747820 0.9679747820 + 0.9682325125 0.9682325125 0.9682325125 + 0.9684901237 0.9684901237 0.9684901237 + 0.9687476754 0.9687476754 0.9687476754 + 0.9690052867 0.9690052867 0.9690052867 + 0.9692628980 0.9692628980 0.9692628980 + 0.9695205092 0.9695205092 0.9695205092 + 0.9697781801 0.9697781801 0.9697781801 + 0.9700340033 0.9700340033 0.9700340033 + 0.9702786803 0.9702786803 0.9702786803 + 0.9705232978 0.9705232978 0.9705232978 + 0.9707679749 0.9707679749 0.9707679749 + 0.9710127115 0.9710127115 0.9710127115 + 0.9712573886 0.9712573886 0.9712573886 + 0.9715021253 0.9715021253 0.9715021253 + 0.9717468023 0.9717468023 0.9717468023 + 0.9719914794 0.9719914794 0.9719914794 + 0.9722362161 0.9722362161 0.9722362161 + 0.9724807739 0.9724807739 0.9724807739 + 0.9727255106 0.9727255106 0.9727255106 + 0.9729701877 0.9729701877 0.9729701877 + 0.9732149243 0.9732149243 0.9732149243 + 0.9734596014 0.9734596014 0.9734596014 + 0.9737042785 0.9737042785 0.9737042785 + 0.9739490151 0.9739490151 0.9739490151 + 0.9741935730 0.9741935730 0.9741935730 + 0.9744383097 0.9744383097 0.9744383097 + 0.9746829867 0.9746829867 0.9746829867 + 0.9749277234 0.9749277234 0.9749277234 + 0.9751724005 0.9751724005 0.9751724005 + 0.9754170775 0.9754170775 0.9754170775 + 0.9756618142 0.9756618142 0.9756618142 + 0.9759064913 0.9759064913 0.9759064913 + 0.9761511087 0.9761511087 0.9761511087 + 0.9763957858 0.9763957858 0.9763957858 + 0.9766405225 0.9766405225 0.9766405225 + 0.9768851995 0.9768851995 0.9768851995 + 0.9771298766 0.9771298766 0.9771298766 + 0.9773746133 0.9773746133 0.9773746133 + 0.9776192904 0.9776192904 0.9776192904 + 0.9778640270 0.9778640270 0.9778640270 + 0.9781085849 0.9781085849 0.9781085849 + 0.9783533216 0.9783533216 0.9783533216 + 0.9785979986 0.9785979986 0.9785979986 + 0.9788426757 0.9788426757 0.9788426757 + 0.9790874124 0.9790874124 0.9790874124 + 0.9793320894 0.9793320894 0.9793320894 + 0.9795768261 0.9795768261 0.9795768261 + 0.9798213840 0.9798213840 0.9798213840 + 0.9800627828 0.9800627828 0.9800627828 + 0.9802953005 0.9802953005 0.9802953005 + 0.9805278182 0.9805278182 0.9805278182 + 0.9807602763 0.9807602763 0.9807602763 + 0.9809927940 0.9809927940 0.9809927940 + 0.9812251925 0.9812251925 0.9812251925 + 0.9814577103 0.9814577103 0.9814577103 + 0.9816902280 0.9816902280 0.9816902280 + 0.9819226861 0.9819226861 0.9819226861 + 0.9821552038 0.9821552038 0.9821552038 + 0.9823877215 0.9823877215 0.9823877215 + 0.9826201200 0.9826201200 0.9826201200 + 0.9828525782 0.9828525782 0.9828525782 + 0.9830850959 0.9830850959 0.9830850959 + 0.9833176136 0.9833176136 0.9833176136 + 0.9835500717 0.9835500717 0.9835500717 + 0.9837825894 0.9837825894 0.9837825894 + 0.9840149879 0.9840149879 0.9840149879 + 0.9842475057 0.9842475057 0.9842475057 + 0.9844800234 0.9844800234 0.9844800234 + 0.9847124815 0.9847124815 0.9847124815 + 0.9849449992 0.9849449992 0.9849449992 + 0.9851775169 0.9851775169 0.9851775169 + 0.9854099154 0.9854099154 0.9854099154 + 0.9856423736 0.9856423736 0.9856423736 + 0.9858748913 0.9858748913 0.9858748913 + 0.9861074090 0.9861074090 0.9861074090 + 0.9863399267 0.9863399267 0.9863399267 + 0.9865723848 0.9865723848 0.9865723848 + 0.9868047833 0.9868047833 0.9868047833 + 0.9870373011 0.9870373011 0.9870373011 + 0.9872698188 0.9872698188 0.9872698188 + 0.9875022769 0.9875022769 0.9875022769 + 0.9877347946 0.9877347946 0.9877347946 + 0.9879673123 0.9879673123 0.9879673123 + 0.9881997108 0.9881997108 0.9881997108 + 0.9884322286 0.9884322286 0.9884322286 + 0.9886646867 0.9886646867 0.9886646867 + 0.9888972044 0.9888972044 0.9888972044 + 0.9891297221 0.9891297221 0.9891297221 + 0.9893621206 0.9893621206 0.9893621206 + 0.9895945787 0.9895945787 0.9895945787 + 0.9898270965 0.9898270965 0.9898270965 + 0.9900565743 0.9900565743 0.9900565743 + 0.9902775884 0.9902775884 0.9902775884 + 0.9904986024 0.9904986024 0.9904986024 + 0.9907194972 0.9907194972 0.9907194972 + 0.9909405112 0.9909405112 0.9909405112 + 0.9911615252 0.9911615252 0.9911615252 + 0.9913824201 0.9913824201 0.9913824201 + 0.9916033745 0.9916033745 0.9916033745 + 0.9918243885 0.9918243885 0.9918243885 + 0.9920452833 0.9920452833 0.9920452833 + 0.9922662973 0.9922662973 0.9922662973 + 0.9924871922 0.9924871922 0.9924871922 + 0.9927082062 0.9927082062 0.9927082062 + 0.9929292202 0.9929292202 0.9929292202 + 0.9931501150 0.9931501150 0.9931501150 + 0.9933711290 0.9933711290 0.9933711290 + 0.9935920835 0.9935920835 0.9935920835 + 0.9938129783 0.9938129783 0.9938129783 + 0.9940339923 0.9940339923 0.9940339923 + 0.9942548871 0.9942548871 0.9942548871 + 0.9944759011 0.9944759011 0.9944759011 + 0.9946969151 0.9946969151 0.9946969151 + 0.9949178100 0.9949178100 0.9949178100 + 0.9951388240 0.9951388240 0.9951388240 + 0.9953597784 0.9953597784 0.9953597784 + 0.9955806732 0.9955806732 0.9955806732 + 0.9958016872 0.9958016872 0.9958016872 + 0.9960227013 0.9960227013 0.9960227013 + 0.9962435961 0.9962435961 0.9962435961 + 0.9964646101 0.9964646101 0.9964646101 + 0.9966855049 0.9966855049 0.9966855049 + 0.9969065189 0.9969065189 0.9969065189 + 0.9971274734 0.9971274734 0.9971274734 + 0.9973484278 0.9973484278 0.9973484278 + 0.9975693822 0.9975693822 0.9975693822 + 0.9977903962 0.9977903962 0.9977903962 + 0.9980112910 0.9980112910 0.9980112910 + 0.9982323050 0.9982323050 0.9982323050 + 0.9984533191 0.9984533191 0.9984533191 + 0.9986742139 0.9986742139 0.9986742139 + 0.9988952279 0.9988952279 0.9988952279 + 0.9991161227 0.9991161227 0.9991161227 + 0.9993370771 0.9993370771 0.9993370771 + 0.9995580912 0.9995580912 0.9995580912 + 0.9997789860 0.9997789860 0.9997789860 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3156.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3156.spi1d index 5f4ce9fe..5036b92d 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3156.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_dscs_3156.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0012913510 0.0008980598 0.0004710060 - 0.0025827030 0.0017961200 0.0009420120 - 0.0038740539 0.0026941791 0.0014130180 - 0.0051654060 0.0035922390 0.0018840240 - 0.0064567570 0.0044902992 0.0023550300 - 0.0077481088 0.0053883591 0.0028260360 - 0.0090394598 0.0062864190 0.0032970421 - 0.0103690401 0.0071844789 0.0037680480 - 0.0118096303 0.0080825379 0.0042390539 - 0.0132502196 0.0089805983 0.0047100601 - 0.0146908201 0.0098786578 0.0051810662 - 0.0161314104 0.0108593097 0.0056520719 - 0.0175720006 0.0118528605 0.0061230781 - 0.0190125909 0.0128464103 0.0065940842 - 0.0205063205 0.0138399703 0.0070650899 - 0.0221158192 0.0148335202 0.0075360960 - 0.0237253308 0.0158270709 0.0080071017 - 0.0253348406 0.0168206207 0.0084781079 - 0.0269443505 0.0178141799 0.0089491140 - 0.0285538603 0.0188077297 0.0094201202 - 0.0301827304 0.0198012795 0.0098911263 - 0.0319830701 0.0208809301 0.0103980303 - 0.0337834209 0.0219821204 0.0109157199 - 0.0355837606 0.0230832994 0.0114334105 - 0.0373841003 0.0241844803 0.0119511103 - 0.0391844399 0.0252856594 0.0124687999 - 0.0411023311 0.0263868403 0.0129864896 - 0.0431175716 0.0274880193 0.0135041801 - 0.0451328084 0.0285892002 0.0140218800 - 0.0471480489 0.0296903793 0.0145395696 - 0.0491632894 0.0308787897 0.0150572602 - 0.0513193496 0.0321012996 0.0155749600 - 0.0535753891 0.0333238207 0.0160926506 - 0.0558314398 0.0345463417 0.0166103393 - 0.0580874793 0.0357688516 0.0171280298 - 0.0603842996 0.0369913690 0.0176457297 - 0.0629082322 0.0382138900 0.0181634203 - 0.0654321536 0.0394364111 0.0186811108 - 0.0679560676 0.0407326184 0.0191988107 - 0.0705360919 0.0420918800 0.0197164994 - 0.0733550414 0.0434511304 0.0202579591 - 0.0761739910 0.0448103808 0.0208282005 - 0.0789929330 0.0461696312 0.0213984400 - 0.0820178986 0.0475288890 0.0219686702 - 0.0851573721 0.0488881394 0.0225389097 - 0.0882968530 0.0502754003 0.0231091492 - 0.0915927812 0.0517885610 0.0236793794 - 0.0950742513 0.0533017218 0.0242496207 - 0.0985557213 0.0548148789 0.0248198602 - 0.1022457033 0.0563280284 0.0253900904 - 0.1060836017 0.0578411892 0.0259603299 - 0.1099215001 0.0593543500 0.0265305694 - 0.1141121984 0.0609665997 0.0271007996 - 0.1183103025 0.0626525879 0.0276710391 - 0.1227174997 0.0643385872 0.0282412805 - 0.1272657067 0.0660245866 0.0288115107 - 0.1319428980 0.0677105933 0.0293817502 - 0.1368145049 0.0693965778 0.0299519897 - 0.1417827010 0.0712067708 0.0305764992 - 0.1469336003 0.0730861723 0.0312060006 - 0.1521732956 0.0749655813 0.0318355002 - 0.1575435996 0.0768449828 0.0324649997 - 0.1629938930 0.0787243918 0.0330944993 - 0.1685118973 0.0806729570 0.0337239988 - 0.1740811020 0.0827676505 0.0343534984 - 0.1796693951 0.0848623365 0.0349830016 - 0.1852522939 0.0869570300 0.0356125012 - 0.1908237040 0.0890517235 0.0362420008 - 0.1963316947 0.0912766010 0.0368715003 - 0.2017958015 0.0936091468 0.0375009999 - 0.2071722001 0.0959417075 0.0381304994 - 0.2124657035 0.0982742608 0.0387599990 - 0.2176671028 0.1006745026 0.0393894985 - 0.2227557003 0.1032672003 0.0400210209 - 0.2277524024 0.1058600023 0.0407175012 - 0.2326270938 0.1084527001 0.0414139815 - 0.2374019027 0.1111586019 0.0421104617 - 0.2420724034 0.1140320972 0.0428069495 - 0.2466181964 0.1169055998 0.0435034297 - 0.2511056960 0.1197791025 0.0441999100 - 0.2554233968 0.1229275018 0.0448963903 - 0.2597410083 0.1260987967 0.0455928706 - 0.2638502121 0.1292701066 0.0462893508 - 0.2679460049 0.1326791048 0.0469858386 - 0.2719362974 0.1361591071 0.0476823188 - 0.2758204043 0.1396390945 0.0483787991 - 0.2797045112 0.1433980018 0.0490752794 - 0.2834044099 0.1471890956 0.0497717597 - 0.2870891094 0.1510583013 0.0505192392 - 0.2907347083 0.1551516056 0.0512915812 - 0.2942334116 0.1592448950 0.0520639084 - 0.2977321148 0.1635666043 0.0528362505 - 0.3011702895 0.1679400951 0.0536085814 - 0.3044967949 0.1724427938 0.0543809198 - 0.3078233004 0.1770606041 0.0551532507 - 0.3110949993 0.1817495972 0.0559255891 - 0.3142628968 0.1865632981 0.0566979311 - 0.3174307942 0.1914165020 0.0574702583 - 0.3205712140 0.1963680983 0.0582426004 - 0.3235935867 0.2013394982 0.0590149313 - 0.3266159892 0.2063657045 0.0597872697 - 0.3296383023 0.2113949060 0.0606219508 - 0.3325434029 0.2164323032 0.0614803396 - 0.3354324996 0.2214559019 0.0623387285 - 0.3383215964 0.2264461070 0.0631971210 - 0.3411597013 0.2314081937 0.0640555099 - 0.3439269960 0.2363009006 0.0649138913 - 0.3466942906 0.2411601990 0.0657722801 - 0.3494617045 0.2459158003 0.0666306689 - 0.3521394134 0.2506479919 0.0674890578 - 0.3547956049 0.2552376986 0.0683474466 - 0.3574517071 0.2598274946 0.0692058429 - 0.3601036966 0.2642399073 0.0700715408 - 0.3626584113 0.2686452866 0.0710277036 - 0.3652130067 0.2729164064 0.0719838589 - 0.3677676022 0.2771278918 0.0729400069 - 0.3703106046 0.2812767923 0.0738961697 - 0.3727726042 0.2852917016 0.0748523325 - 0.3752346039 0.2893066108 0.0758084878 - 0.3776965141 0.2931610048 0.0767646432 - 0.3801530898 0.2969819903 0.0777207986 - 0.3825305104 0.3007636070 0.0786769614 - 0.3849078119 0.3043971956 0.0796331167 - 0.3872852027 0.3080308139 0.0806578100 - 0.3896625936 0.3115825951 0.0817251801 - 0.3919736147 0.3150376081 0.0827925429 - 0.3942736089 0.3184925914 0.0838598981 - 0.3965736926 0.3218528032 0.0849272683 - 0.3988736868 0.3251396120 0.0859946311 - 0.4011375904 0.3284263909 0.0870619863 - 0.4033668935 0.3316313028 0.0881293565 - 0.4055961072 0.3347609043 0.0891967192 - 0.4078254104 0.3378903866 0.0902953967 - 0.4100531042 0.3409723938 0.0914893299 - 0.4122174084 0.3439559937 0.0926832631 - 0.4143818021 0.3469395041 0.0938771889 - 0.4165461957 0.3499230146 0.0950711221 - 0.4187105894 0.3527748883 0.0962650478 - 0.4208509922 0.3556233943 0.0974589810 - 0.4229558110 0.3584718108 0.0986529067 - 0.4250606000 0.3612624109 0.0998468399 - 0.4271655083 0.3639861941 0.1011663005 - 0.4292702973 0.3667100072 0.1025042981 - 0.4313394129 0.3694337904 0.1038423032 - 0.4333893955 0.3720667064 0.1051801965 - 0.4354394078 0.3746758103 0.1065182015 - 0.4374893904 0.3772849143 0.1078561991 - 0.4395394921 0.3798938990 0.1091940999 - 0.4415503144 0.3824017942 0.1105972007 - 0.4435496926 0.3849053979 0.1120989993 - 0.4455491900 0.3874089122 0.1136007011 - 0.4475485981 0.3899124861 0.1151024029 - 0.4495480955 0.3923225105 0.1166040972 - 0.4515112042 0.3947291076 0.1181057990 - 0.4534637928 0.3971355855 0.1196075976 - 0.4554164112 0.3995422125 0.1212465018 - 0.4573689997 0.4018765986 0.1229339987 - 0.4593215883 0.4041940868 0.1246215031 - 0.4612458050 0.4065114856 0.1263090074 - 0.4631549120 0.4088290036 0.1279965043 - 0.4650638998 0.4111058116 0.1296840012 - 0.4669730067 0.4133413136 0.1315421015 - 0.4688819945 0.4155766964 0.1334394962 - 0.4707742035 0.4178121984 0.1353369951 - 0.4726425111 0.4200460017 0.1372344047 - 0.4745109081 0.4222060144 0.1391319036 - 0.4763793051 0.4243659079 0.1411572993 - 0.4782476127 0.4265258014 0.1432905942 - 0.4801135957 0.4286858141 0.1454239935 - 0.4819437861 0.4308184981 0.1475573033 - 0.4837738872 0.4329088032 0.1496907026 - 0.4856041074 0.4349991083 0.1520487964 - 0.4874342084 0.4370894134 0.1544450074 - 0.4892643988 0.4391798079 0.1568412036 - 0.4910728931 0.4412310123 0.1592375040 - 0.4928669035 0.4432570040 0.1618310958 - 0.4946610034 0.4452829957 0.1645168066 - 0.4964550138 0.4473089874 0.1672026068 - 0.4982489944 0.4493350089 0.1698883027 - 0.5000422001 0.4513210058 0.1728748977 - 0.5018019080 0.4532875121 0.1758745015 - 0.5035614967 0.4552539885 0.1788741052 - 0.5053212047 0.4572204053 0.1820818037 - 0.5070807934 0.4591869116 0.1854145974 - 0.5088403821 0.4611209929 0.1887473017 - 0.5105888247 0.4630320966 0.1922948956 - 0.5123155117 0.4649432898 0.1959716976 - 0.5140420794 0.4668545127 0.1996484995 - 0.5157688260 0.4687657058 0.2036353946 - 0.5174953938 0.4706586897 0.2076551020 - 0.5192220807 0.4725184143 0.2118106931 - 0.5209311843 0.4743781090 0.2161566019 - 0.5226259232 0.4762378037 0.2205363065 - 0.5243206024 0.4780975878 0.2251745015 - 0.5260154009 0.4799573123 0.2298126072 - 0.5277100801 0.4817700088 0.2346820980 - 0.5294048190 0.4835816026 0.2395613045 - 0.5310792923 0.4853931963 0.2446004003 - 0.5327429175 0.4872049093 0.2496553957 - 0.5344064832 0.4890165031 0.2548052073 - 0.5360699892 0.4908075035 0.2599619925 - 0.5377336144 0.4925738871 0.2651447952 - 0.5393971205 0.4943403900 0.2703250945 - 0.5410410762 0.4961068928 0.2754637897 - 0.5426740050 0.4978733957 0.2805902958 - 0.5443068743 0.4996398985 0.2856245041 - 0.5459396839 0.5013725162 0.2906390131 - 0.5475726128 0.5030964017 0.2955218852 - 0.5492054224 0.5048202872 0.3003894985 - 0.5508226156 0.5065442920 0.3050881028 - 0.5524250269 0.5082681775 0.3097867072 - 0.5540273190 0.5099921227 0.3142902851 - 0.5556296706 0.5116760135 0.3187845051 - 0.5572320223 0.5133596063 0.3231228888 - 0.5588343740 0.5150431991 0.3274034858 - 0.5604283810 0.5167267919 0.3315997124 - 0.5620002151 0.5184103847 0.3356657028 - 0.5635718703 0.5200918913 0.3397318125 - 0.5651437044 0.5217370987 0.3436022103 - 0.5667154193 0.5233823061 0.3474588096 - 0.5682871938 0.5250275135 0.3512471020 - 0.5698590279 0.5266727209 0.3549034894 - 0.5714026093 0.5283179283 0.3585597873 - 0.5729435086 0.5299631953 0.3621017039 - 0.5744842887 0.5315724015 0.3655692935 - 0.5760251880 0.5331807733 0.3690369129 - 0.5775660276 0.5347892046 0.3723773956 - 0.5791069269 0.5363975167 0.3756690919 - 0.5806344748 0.5380058885 0.3789606988 - 0.5821440220 0.5396143198 0.3821411133 - 0.5836533904 0.5411956906 0.3852699995 - 0.5851628184 0.5427685976 0.3883990049 - 0.5866721869 0.5443415046 0.3914547861 - 0.5881816745 0.5459142923 0.3944340944 - 0.5896911025 0.5474871993 0.3974134028 - 0.5911749005 0.5490601063 0.4003747106 - 0.5926522017 0.5506191254 0.4032168984 - 0.5941295028 0.5521575212 0.4060590863 - 0.5956066847 0.5536959171 0.4089013040 - 0.5970839858 0.5552341938 0.4116666019 - 0.5985612869 0.5567725897 0.4143835008 - 0.6000376940 0.5583109856 0.4171003997 - 0.6014819145 0.5598493814 0.4198172987 - 0.6029260755 0.5613573790 0.4224275053 - 0.6043702960 0.5628619790 0.4250301123 - 0.6058145761 0.5643666983 0.4276326001 - 0.6072587967 0.5658714175 0.4302257001 - 0.6087030172 0.5673760772 0.4327239990 - 0.6101437807 0.5688806772 0.4352222979 - 0.6115540266 0.5703768730 0.4377205968 - 0.6129640937 0.5718483925 0.4402106106 - 0.6143742800 0.5733199120 0.4426138103 - 0.6157845259 0.5747913718 0.4450170994 - 0.6171945930 0.5762628913 0.4474202991 - 0.6186047792 0.5777344108 0.4498234987 - 0.6200146079 0.5792058706 0.4521464109 - 0.6213896275 0.5806623101 0.4544628859 - 0.6227645874 0.5821009278 0.4567793906 - 0.6241396070 0.5835396051 0.4590958953 - 0.6255146265 0.5849782228 0.4613640904 - 0.6268895864 0.5864169002 0.4636014104 - 0.6282646060 0.5878555179 0.4658387005 - 0.6296396255 0.5892940760 0.4680759907 - 0.6309878230 0.5907161236 0.4703032076 - 0.6323264837 0.5921220183 0.4724679887 - 0.6336652040 0.5935279727 0.4746327996 - 0.6350039244 0.5949339271 0.4767976999 - 0.6363425255 0.5963398218 0.4789625108 - 0.6376811862 0.5977457166 0.4810926914 - 0.6390199065 0.5991516113 0.4831911922 - 0.6403484941 0.6005445123 0.4852896035 - 0.6416497231 0.6019176841 0.4873880148 - 0.6429507732 0.6032907963 0.4894863963 - 0.6442520022 0.6046640277 0.4915387034 - 0.6455531716 0.6060370803 0.4935761094 - 0.6468542814 0.6074103117 0.4956134856 - 0.6481555104 0.6087834239 0.4976508021 - 0.6494566202 0.6101527810 0.4996882081 - 0.6507353187 0.6114928722 0.5016778708 - 0.6519978046 0.6128330827 0.5036588907 - 0.6532601714 0.6141732931 0.5056399703 - 0.6545227170 0.6155133843 0.5076211095 - 0.6557852030 0.6168535948 0.5096021295 - 0.6570476890 0.6181936860 0.5115414858 - 0.6583101749 0.6195338964 0.5134704709 - 0.6595727205 0.6208522916 0.5153993964 - 0.6608088017 0.6221591830 0.5173283815 - 0.6620315909 0.6234660149 0.5192573071 - 0.6632543206 0.6247729063 0.5211564898 - 0.6644769907 0.6260796785 0.5230368972 - 0.6656997204 0.6273866296 0.5249174237 - 0.6669223905 0.6286934018 0.5267977715 - 0.6681451201 0.6300002933 0.5286782980 - 0.6693677902 0.6312733889 0.5305452943 - 0.6705707908 0.6325464845 0.5323804021 - 0.6717528105 0.6338195801 0.5342155099 - 0.6729347110 0.6350926757 0.5360506177 - 0.6741166115 0.6363657713 0.5378857255 - 0.6752985716 0.6376389265 0.5397207737 - 0.6764804721 0.6389120221 0.5415198207 - 0.6776624918 0.6401801705 0.5433121920 - 0.6788443923 0.6414189935 0.5451046228 - 0.6800253987 0.6426578760 0.5468971133 - 0.6811656952 0.6438966990 0.5486894846 - 0.6823059916 0.6451355815 0.5504710078 - 0.6834462881 0.6463744044 0.5522230268 - 0.6845865846 0.6476132870 0.5539749861 - 0.6857268810 0.6488522291 0.5557268858 - 0.6868671179 0.6500884891 0.5574789047 - 0.6880074143 0.6512925029 0.5592309237 - 0.6891477108 0.6524965167 0.5609611869 - 0.6902772784 0.6537005901 0.5626745224 - 0.6913753152 0.6549046040 0.5643877983 - 0.6924731731 0.6561086774 0.5661010742 - 0.6935710907 0.6573126912 0.5678144097 - 0.6946691275 0.6585168242 0.5695276856 - 0.6957669854 0.6597207785 0.5712140203 - 0.6968650222 0.6608976722 0.5728901029 - 0.6979628801 0.6620662808 0.5745661259 - 0.6990607977 0.6632350087 0.5762422085 - 0.7001525760 0.6644036174 0.5779181719 - 0.7012076974 0.6655722857 0.5795943141 - 0.7022626996 0.6667408943 0.5812429190 - 0.7033178210 0.6679096222 0.5828827024 - 0.7043728828 0.6690781713 0.5845226049 - 0.7054278851 0.6702393293 0.5861623883 - 0.7064830065 0.6713719964 0.5878022909 - 0.7075381279 0.6725047231 0.5894420743 - 0.7085930705 0.6736373901 0.5910584927 - 0.7096481919 0.6747701168 0.5926628113 - 0.7106745243 0.6759027839 0.5942670703 - 0.7116863132 0.6770355105 0.5958713889 - 0.7126982212 0.6781681776 0.5974757075 - 0.7137101293 0.6793009043 0.5990800261 - 0.7147219777 0.6804196239 0.6006693244 - 0.7157338262 0.6815158129 0.6022384763 - 0.7167456746 0.6826120019 0.6038076282 - 0.7177575827 0.6837081909 0.6053767204 - 0.7187694907 0.6848043799 0.6069458723 - 0.7197813988 0.6859006286 0.6085150242 - 0.7207592726 0.6869968176 0.6100823283 - 0.7217279077 0.6880930066 0.6116163135 - 0.7226964235 0.6891891956 0.6131502986 - 0.7236649990 0.6902757883 0.6146842837 - 0.7246335745 0.6913350224 0.6162183881 - 0.7256022096 0.6923943162 0.6177523732 - 0.7265707850 0.6934534907 0.6192864180 - 0.7275393009 0.6945127249 0.6208015084 - 0.7285078764 0.6955720186 0.6223003268 - 0.7294765115 0.6966311932 0.6237990260 - 0.7304251790 0.6976904273 0.6252977252 - 0.7313506007 0.6987497211 0.6267964244 - 0.7322760224 0.6998088956 0.6282951236 - 0.7332013845 0.7008374929 0.6297938228 - 0.7341268063 0.7018594146 0.6312615871 - 0.7350522280 0.7028812170 0.6327245831 - 0.7359775901 0.7039030790 0.6341875196 - 0.7369030118 0.7049250007 0.6356505156 - 0.7378283739 0.7059468031 0.6371133924 - 0.7387537956 0.7069687247 0.6385763288 - 0.7396792173 0.7079905868 0.6400383115 - 0.7405766249 0.7090123892 0.6414648294 - 0.7414591908 0.7100329995 0.6428912878 - 0.7423416972 0.7110173106 0.6443178058 - 0.7432242036 0.7120015025 0.6457443833 - 0.7441068292 0.7129856944 0.6471709013 - 0.7449892759 0.7139698863 0.6485974193 - 0.7458719015 0.7149540782 0.6500232816 - 0.7467544079 0.7159383297 0.6514126062 - 0.7476369739 0.7169225216 0.6528018117 - 0.7485194802 0.7179067731 0.6541910768 - 0.7494021058 0.7188910246 0.6555804014 - 0.7502709031 0.7198752165 0.6569697261 - 0.7511112094 0.7208263874 0.6583588719 - 0.7519513965 0.7217727900 0.6597481966 - 0.7527915835 0.7227191925 0.6611062288 - 0.7536318898 0.7236654758 0.6624572873 - 0.7544720769 0.7246118784 0.6638084054 - 0.7553123236 0.7255582809 0.6651595235 - 0.7561525106 0.7265046835 0.6665105820 - 0.7569928169 0.7274510860 0.6678617001 - 0.7578330040 0.7283974886 0.6692128181 - 0.7586731911 0.7293438911 0.6705474854 - 0.7595134974 0.7302786708 0.6718593836 - 0.7603362203 0.7311872244 0.6731712818 - 0.7611348033 0.7320957184 0.6744831800 - 0.7619335055 0.7330042124 0.6757950187 - 0.7627320886 0.7339128256 0.6771069169 - 0.7635306716 0.7348213196 0.6784188151 - 0.7643293738 0.7357298136 0.6797307134 - 0.7651280165 0.7366383076 0.6810106039 - 0.7659267187 0.7375468016 0.6822822094 - 0.7667253017 0.7384554148 0.6835538149 - 0.7675240040 0.7393639088 0.6848254800 - 0.7683225870 0.7402610779 0.6860970855 - 0.7691212296 0.7411319017 0.6873686910 - 0.7699198723 0.7420026064 0.6886402965 - 0.7706819177 0.7428733706 0.6899120212 - 0.7714399099 0.7437440753 0.6911451221 - 0.7721979022 0.7446148992 0.6923754811 - 0.7729557753 0.7454857230 0.6936057806 - 0.7737138271 0.7463564277 0.6948360801 - 0.7744718194 0.7472271919 0.6960663795 - 0.7752296925 0.7480978966 0.6972966790 - 0.7759876847 0.7489687204 0.6985270977 - 0.7767456770 0.7498394847 0.6997573972 - 0.7775036097 0.7506796122 0.7009537816 - 0.7782616019 0.7515128255 0.7021418214 - 0.7790195942 0.7523460984 0.7033298016 - 0.7797775269 0.7531793118 0.7045179009 - 0.7805075049 0.7540125847 0.7057058811 - 0.7812259197 0.7548457980 0.7068939209 - 0.7819442153 0.7556790113 0.7080820203 - 0.7826625705 0.7565122843 0.7092700005 - 0.7833809257 0.7573454976 0.7104414105 - 0.7840992808 0.7581787705 0.7115862966 - 0.7848175764 0.7590119839 0.7127311826 - 0.7855359912 0.7598451972 0.7138760090 - 0.7862542868 0.7606481910 0.7150208950 - 0.7869727015 0.7614442706 0.7161657810 - 0.7876909971 0.7622404099 0.7173107266 - 0.7884094119 0.7630366087 0.7184554935 - 0.7891278267 0.7638326883 0.7196003795 - 0.7898461223 0.7646288276 0.7207167149 - 0.7905343175 0.7654249072 0.7218176723 - 0.7912142277 0.7662209868 0.7229186893 - 0.7918941975 0.7670171261 0.7240195870 - 0.7925741076 0.7678132057 0.7251206040 - 0.7932540178 0.7686092854 0.7262216210 - 0.7939339876 0.7694054246 0.7273225784 - 0.7946138978 0.7701922059 0.7284234762 - 0.7952939272 0.7709516883 0.7295244932 - 0.7959737778 0.7717111707 0.7306001782 - 0.7966536880 0.7724707127 0.7316566706 - 0.7973337173 0.7732301950 0.7327132821 - 0.7980136275 0.7739896774 0.7337697744 - 0.7986935973 0.7747492194 0.7348263264 - 0.7993735075 0.7755087018 0.7358828187 - 0.8000504971 0.7762681842 0.7369393110 - 0.8006933928 0.7770277262 0.7379958034 - 0.8013362288 0.7777872086 0.7390522957 - 0.8019790053 0.7785466909 0.7401041985 - 0.8026219010 0.7793061733 0.7411159277 - 0.8032646775 0.7800626159 0.7421275973 - 0.8039075732 0.7807862163 0.7431393266 - 0.8045504093 0.7815096974 0.7441509962 - 0.8051931858 0.7822332978 0.7451627254 - 0.8058360815 0.7829567790 0.7461743951 - 0.8064789176 0.7836803794 0.7471861243 - 0.8071216941 0.7844039202 0.7481977940 - 0.8077645898 0.7851275206 0.7492095232 - 0.8084074259 0.7858511209 0.7502114177 - 0.8090502024 0.7865746021 0.7511780858 - 0.8096930981 0.7872982025 0.7521448731 - 0.8103172779 0.7880216837 0.7531116009 - 0.8109244108 0.7887452841 0.7540783286 - 0.8115314841 0.7894688249 0.7550451159 - 0.8121386766 0.7901830077 0.7560117841 - 0.8127458096 0.7908713818 0.7569785714 - 0.8133528829 0.7915598154 0.7579452991 - 0.8139600754 0.7922481894 0.7589120269 - 0.8145672083 0.7929366231 0.7598788142 - 0.8151742816 0.7936249971 0.7608063221 - 0.8157814145 0.7943133712 0.7617281079 - 0.8163886070 0.7950018048 0.7626500130 - 0.8169956803 0.7956901193 0.7635719180 - 0.8176028132 0.7963784933 0.7644938231 - 0.8182100058 0.7970669270 0.7654156089 - 0.8188170791 0.7977553010 0.7663375139 - 0.8194242120 0.7984436750 0.7672594190 - 0.8200296164 0.7991321087 0.7681813240 - 0.8206024766 0.7998204827 0.7691031098 - 0.8211753964 0.8004835248 0.7700238228 - 0.8217483163 0.8011376858 0.7709010839 - 0.8223211169 0.8017917871 0.7717785239 - 0.8228939772 0.8024458885 0.7726557851 - 0.8234668970 0.8030999899 0.7735331059 - 0.8240398169 0.8037540913 0.7744104862 - 0.8246126771 0.8044083118 0.7752878070 - 0.8251855969 0.8050624132 0.7761651278 - 0.8257585168 0.8057165146 0.7770425081 - 0.8263313770 0.8063706160 0.7779198289 - 0.8269041777 0.8070247173 0.7787970901 - 0.8274770975 0.8076788187 0.7796744704 - 0.8280500174 0.8083329797 0.7805240750 - 0.8286228776 0.8089870811 0.7813575268 - 0.8291957974 0.8096411824 0.7821907997 - 0.8297687173 0.8102803230 0.7830241919 - 0.8303220272 0.8109011054 0.7838575244 - 0.8308622241 0.8115220070 0.7846909165 - 0.8314023018 0.8121427894 0.7855241895 - 0.8319424987 0.8127635717 0.7863575816 - 0.8324825764 0.8133844733 0.7871909142 - 0.8330227733 0.8140053153 0.7880243063 - 0.8335629106 0.8146260977 0.7888575792 - 0.8341031075 0.8152469993 0.7896909714 - 0.8346431851 0.8158677816 0.7904971242 - 0.8351833820 0.8164886236 0.7912873030 - 0.8357235193 0.8171095252 0.7920774221 - 0.8362637162 0.8177303076 0.7928674817 - 0.8368037939 0.8183512092 0.7936577201 - 0.8373439908 0.8189719915 0.7944477797 - 0.8378841281 0.8195927739 0.7952378988 - 0.8384243250 0.8202025890 0.7960280776 - 0.8389644027 0.8207911849 0.7968181968 - 0.8395045996 0.8213797808 0.7976083159 - 0.8400421143 0.8219683766 0.7983984947 - 0.8405510783 0.8225570917 0.7991886139 - 0.8410599828 0.8231456876 0.7999786735 - 0.8415690064 0.8237342834 0.8007277846 - 0.8420779109 0.8243228793 0.8014757037 - 0.8425868154 0.8249114752 0.8022235036 - 0.8430957794 0.8255001903 0.8029714227 - 0.8436046839 0.8260887861 0.8037192822 - 0.8441137075 0.8266773820 0.8044672012 - 0.8446226120 0.8272659779 0.8052151203 - 0.8451315165 0.8278546929 0.8059629798 - 0.8456404805 0.8284432888 0.8067108989 - 0.8461493850 0.8290318847 0.8074588180 - 0.8466584086 0.8296204805 0.8082066774 - 0.8471673131 0.8301981091 0.8089544773 - 0.8476762176 0.8307555914 0.8097023964 - 0.8481851816 0.8313131928 0.8104255795 - 0.8486940861 0.8318706751 0.8111323714 - 0.8492031097 0.8324282765 0.8118392229 - 0.8497120142 0.8329858184 0.8125458956 - 0.8502081037 0.8335434198 0.8132526875 - 0.8506873250 0.8341009021 0.8139594793 - 0.8511666059 0.8346583843 0.8146662712 - 0.8516458869 0.8352159858 0.8153731227 - 0.8521251082 0.8357735276 0.8160799146 - 0.8526043892 0.8363311291 0.8167867064 - 0.8530836105 0.8368886113 0.8174934983 - 0.8535628915 0.8374462128 0.8182002902 - 0.8540421724 0.8380036950 0.8189070821 - 0.8545213938 0.8385611773 0.8196138144 - 0.8550006747 0.8391187787 0.8203026056 - 0.8554800153 0.8396763206 0.8209695816 - 0.8559591770 0.8402212858 0.8216364980 - 0.8564385176 0.8407490253 0.8223034739 - 0.8569177985 0.8412765861 0.8229705095 - 0.8573970199 0.8418043256 0.8236374855 - 0.8578763008 0.8423318863 0.8243045211 - 0.8583555818 0.8428596258 0.8249714971 - 0.8588348031 0.8433871865 0.8256384730 - 0.8593140841 0.8439149261 0.8263055086 - 0.8597934246 0.8444424868 0.8269724846 - 0.8602566123 0.8449702263 0.8276394010 - 0.8607078195 0.8454977870 0.8283063769 - 0.8611589074 0.8460255265 0.8289734125 - 0.8616099954 0.8465530872 0.8296403885 - 0.8620610833 0.8470808268 0.8302897215 - 0.8625121713 0.8476083875 0.8309183121 - 0.8629633784 0.8481361270 0.8315469027 - 0.8634145260 0.8486636877 0.8321754932 - 0.8638656139 0.8491914272 0.8328040838 - 0.8643167019 0.8497189879 0.8334327936 - 0.8647677898 0.8502333164 0.8340613842 - 0.8652189970 0.8507323265 0.8346899748 - 0.8656700850 0.8512312174 0.8353186250 - 0.8661211729 0.8517302275 0.8359472156 - 0.8665723205 0.8522291780 0.8365758061 - 0.8670234084 0.8527281880 0.8372043967 - 0.8674746156 0.8532270789 0.8378329873 - 0.8679257035 0.8537260890 0.8384615779 - 0.8683767915 0.8542250991 0.8390902281 - 0.8688278794 0.8547241092 0.8397188783 - 0.8692790270 0.8552231193 0.8403270841 - 0.8697301745 0.8557220101 0.8409187794 - 0.8701705933 0.8562210202 0.8415105939 - 0.8705950975 0.8567199707 0.8421024084 - 0.8710194826 0.8572189808 0.8426941037 - 0.8714439869 0.8577179909 0.8432859182 - 0.8718684912 0.8582168818 0.8438776135 - 0.8722929955 0.8587158918 0.8444694281 - 0.8727173805 0.8592149019 0.8450611234 - 0.8731418848 0.8597139120 0.8456528783 - 0.8735663891 0.8602011204 0.8462445736 - 0.8739908934 0.8606727123 0.8468363881 - 0.8744153976 0.8611443043 0.8474280834 - 0.8748397827 0.8616157770 0.8480198979 - 0.8752642870 0.8620874286 0.8486115932 - 0.8756887913 0.8625589013 0.8492034078 - 0.8761132956 0.8630304933 0.8497951031 - 0.8765377998 0.8635020256 0.8503637910 - 0.8769621849 0.8639736176 0.8509203196 - 0.8773866892 0.8644450903 0.8514767885 - 0.8778111935 0.8649166822 0.8520333171 - 0.8782356977 0.8653882146 0.8525897861 - 0.8786600828 0.8658598065 0.8531463146 - 0.8790845871 0.8663312793 0.8537027836 - 0.8795090914 0.8668028712 0.8542593122 - 0.8799335957 0.8672744036 0.8548157811 - 0.8803368211 0.8677459955 0.8553723097 - 0.8807361126 0.8682175279 0.8559287786 - 0.8811354041 0.8686891198 0.8564853072 - 0.8815348148 0.8691605926 0.8570417166 - 0.8819341063 0.8696321845 0.8575981855 - 0.8823333979 0.8700979948 0.8581547141 - 0.8827326894 0.8705433011 0.8587111831 - 0.8831319809 0.8709887266 0.8592677116 - 0.8835312724 0.8714340925 0.8598241806 - 0.8839306235 0.8718795180 0.8603577018 - 0.8843299150 0.8723248839 0.8608806133 - 0.8847292066 0.8727701902 0.8614034057 - 0.8851284981 0.8732156157 0.8619263172 - 0.8855277896 0.8736609817 0.8624492288 - 0.8859272003 0.8741064072 0.8629720211 - 0.8863264918 0.8745517135 0.8634948730 - 0.8867257833 0.8749970794 0.8640177846 - 0.8871250749 0.8754425049 0.8645406961 - 0.8875244260 0.8758878708 0.8650634885 - 0.8879237175 0.8763331771 0.8655864000 - 0.8883230090 0.8767786026 0.8661093116 - 0.8887223005 0.8772240281 0.8666321039 - 0.8891215920 0.8776693940 0.8671550155 - 0.8895208836 0.8781148195 0.8676779270 - 0.8899201751 0.8785601258 0.8682007790 - 0.8903005719 0.8790054917 0.8687235713 - 0.8906760812 0.8794509172 0.8692464828 - 0.8910517097 0.8798962831 0.8697693944 - 0.8914272785 0.8803225160 0.8702744246 - 0.8918027878 0.8807430267 0.8707653284 - 0.8921784163 0.8811634183 0.8712561727 - 0.8925539851 0.8815839291 0.8717470765 - 0.8929294944 0.8820043206 0.8722379804 - 0.8933051229 0.8824247718 0.8727290034 - 0.8936806917 0.8828452229 0.8732199073 - 0.8940562010 0.8832656741 0.8737108111 - 0.8944318295 0.8836861849 0.8742017150 - 0.8948073983 0.8841065764 0.8746926188 - 0.8951830268 0.8845270872 0.8751835227 - 0.8955584764 0.8849474788 0.8756744862 - 0.8959341049 0.8853679895 0.8761653900 - 0.8963096738 0.8857883811 0.8766562939 - 0.8966851830 0.8862088919 0.8771471977 - 0.8970608115 0.8866292834 0.8776381016 - 0.8974363804 0.8870497942 0.8781290054 - 0.8978118896 0.8874701858 0.8786200285 - 0.8981875181 0.8878906965 0.8791108727 - 0.8985630870 0.8883110881 0.8796017766 - 0.8989385962 0.8887315989 0.8800870180 - 0.8993142247 0.8891521096 0.8805475831 - 0.8996897936 0.8895725012 0.8810082078 - 0.9000614882 0.8899930120 0.8814688921 - 0.9004147053 0.8903900981 0.8819295168 - 0.9007679224 0.8907868862 0.8823900819 - 0.9011210799 0.8911836743 0.8828508258 - 0.9014742970 0.8915804029 0.8833113909 - 0.9018275142 0.8919771910 0.8837720156 - 0.9021806717 0.8923739791 0.8842326999 - 0.9025338888 0.8927707076 0.8846933246 - 0.9028872252 0.8931674957 0.8851538897 - 0.9032403827 0.8935642838 0.8856145740 - 0.9035935998 0.8939610720 0.8860751987 - 0.9039468169 0.8943578005 0.8865358233 - 0.9042999744 0.8947545886 0.8869965076 - 0.9046531916 0.8951513767 0.8874570727 - 0.9050064087 0.8955481052 0.8879176974 - 0.9053596258 0.8959448934 0.8883783221 - 0.9057129025 0.8963416815 0.8888390064 - 0.9060661197 0.8967384100 0.8892995715 - 0.9064192772 0.8971351981 0.8897601962 - 0.9067724943 0.8975319862 0.8902071118 - 0.9071257114 0.8979287744 0.8906391263 - 0.9074789286 0.8983255029 0.8910710812 - 0.9078320861 0.8987222910 0.8915032148 - 0.9081853032 0.8991190791 0.8919352293 - 0.9085385203 0.8995158076 0.8923671842 - 0.9088917971 0.8999125957 0.8927991986 - 0.9092450142 0.9002919197 0.8932312131 - 0.9095981717 0.9006661773 0.8936632276 - 0.9099513888 0.9010404944 0.8940951824 - 0.9102864861 0.9014148116 0.8945271969 - 0.9106187224 0.9017891288 0.8949592113 - 0.9109507799 0.9021633863 0.8953912258 - 0.9112830162 0.9025377035 0.8958231807 - 0.9116151929 0.9029120207 0.8962551951 - 0.9119474292 0.9032862782 0.8966872096 - 0.9122796059 0.9036605954 0.8971192241 - 0.9126117229 0.9040349126 0.8975511789 - 0.9129438996 0.9044092298 0.8979831934 - 0.9132760763 0.9047834873 0.8984152079 - 0.9136083126 0.9051578045 0.8988472223 - 0.9139404893 0.9055321217 0.8992791772 - 0.9142726064 0.9059063792 0.8997111917 - 0.9146047831 0.9062806964 0.9001343250 - 0.9149370193 0.9066550732 0.9005392790 - 0.9152691960 0.9070293903 0.9009442925 - 0.9156013727 0.9074037075 0.9013493061 - 0.9159336090 0.9077780247 0.9017543197 - 0.9162657261 0.9081522822 0.9021592736 - 0.9165979028 0.9085265994 0.9025642872 - 0.9169300795 0.9089009166 0.9029693007 - 0.9172623158 0.9092751741 0.9033743143 - 0.9175944924 0.9096494913 0.9037793279 - 0.9179266095 0.9100223780 0.9041842818 - 0.9182587862 0.9103754759 0.9045892954 - 0.9185910225 0.9107285142 0.9049943089 - 0.9189231992 0.9110814929 0.9053993225 - 0.9192553759 0.9114345908 0.9058042765 - 0.9195876122 0.9117876291 0.9062092900 - 0.9199197292 0.9121406078 0.9066143036 - 0.9202368855 0.9124937057 0.9070193172 - 0.9205493927 0.9128466845 0.9074242711 - 0.9208617806 0.9131997228 0.9078292847 - 0.9211742282 0.9135528207 0.9082342982 - 0.9214866161 0.9139057994 0.9086393118 - 0.9217990041 0.9142587781 0.9090443254 - 0.9221115112 0.9146118760 0.9094492793 - 0.9224238992 0.9149649143 0.9098542929 - 0.9227362871 0.9153178930 0.9102429748 - 0.9230486751 0.9156708717 0.9106225967 - 0.9233611822 0.9160240293 0.9110022187 - 0.9236736298 0.9163770080 0.9113817811 - 0.9239860177 0.9167299867 0.9117614031 - 0.9242984056 0.9170830846 0.9121410251 - 0.9246109128 0.9174361229 0.9125205874 - 0.9249233007 0.9177891016 0.9129002094 - 0.9252356887 0.9181421995 0.9132797122 - 0.9255480766 0.9184951782 0.9136592746 - 0.9258605242 0.9188482165 0.9140388966 - 0.9261729717 0.9192013144 0.9144185185 - 0.9264854193 0.9195542932 0.9147980809 - 0.9267978072 0.9199072719 0.9151777029 - 0.9271101952 0.9202455282 0.9155573249 - 0.9274227023 0.9205784202 0.9159368873 - 0.9277350903 0.9209113121 0.9163165092 - 0.9280474782 0.9212443233 0.9166960120 - 0.9283599257 0.9215772152 0.9170755744 - 0.9286723137 0.9219101071 0.9174551964 - 0.9289848208 0.9222429991 0.9178348184 - 0.9292972088 0.9225758910 0.9182143807 - 0.9296095967 0.9229087830 0.9185940027 - 0.9299219847 0.9232416749 0.9189736247 - 0.9302204847 0.9235746861 0.9193531871 - 0.9305143952 0.9239075780 0.9197328091 - 0.9308083057 0.9242405295 0.9201052785 - 0.9311022162 0.9245734215 0.9204609990 - 0.9313960075 0.9249063134 0.9208167195 - 0.9316899180 0.9252392054 0.9211723804 - 0.9319838285 0.9255720973 0.9215281010 - 0.9322776794 0.9259051085 0.9218838215 - 0.9325714707 0.9262380004 0.9222394824 - 0.9328653812 0.9265708923 0.9225953221 - 0.9331592917 0.9269037843 0.9229509830 - 0.9334532022 0.9272366762 0.9233067036 - 0.9337469935 0.9275696278 0.9236624241 - 0.9340409040 0.9279025197 0.9240180850 - 0.9343348145 0.9282354712 0.9243738055 - 0.9346287251 0.9285684228 0.9247295260 - 0.9349225163 0.9289013147 0.9250851870 - 0.9352164268 0.9292342067 0.9254410267 - 0.9355102777 0.9295670986 0.9257966876 - 0.9358041286 0.9298999906 0.9261524081 - 0.9360979795 0.9302197099 0.9265081286 - 0.9363918900 0.9305335879 0.9268637896 - 0.9366858006 0.9308475256 0.9272195101 - 0.9369795918 0.9311614037 0.9275751710 - 0.9372735023 0.9314752817 0.9279308915 - 0.9375674129 0.9317892194 0.9282866120 - 0.9378613234 0.9321032166 0.9286423922 - 0.9381551147 0.9324170947 0.9289981127 - 0.9384490252 0.9327309728 0.9293537736 - 0.9387428761 0.9330449104 0.9297094941 - 0.9390367866 0.9333587885 0.9300611019 - 0.9393305779 0.9336727262 0.9303944111 - 0.9396244884 0.9339867234 0.9307277203 - 0.9399183989 0.9343006015 0.9310610294 - 0.9401996732 0.9346144795 0.9313942790 - 0.9404761791 0.9349284172 0.9317277074 - 0.9407526851 0.9352422953 0.9320610166 - 0.9410290718 0.9355561733 0.9323943257 - 0.9413055778 0.9358701706 0.9327275753 - 0.9415820837 0.9361841083 0.9330608845 - 0.9418585896 0.9364979863 0.9333941936 - 0.9421349764 0.9368119240 0.9337275028 - 0.9424114823 0.9371258020 0.9340608120 - 0.9426879883 0.9374397993 0.9343941808 - 0.9429644942 0.9377536774 0.9347274899 - 0.9432408810 0.9380676150 0.9350607991 - 0.9435173869 0.9383814931 0.9353941083 - 0.9437938929 0.9386953712 0.9357274175 - 0.9440703988 0.9390093088 0.9360607266 - 0.9443467855 0.9393233061 0.9363939762 - 0.9446232915 0.9396371841 0.9367272854 - 0.9448997974 0.9399511218 0.9370607138 - 0.9451763034 0.9402499199 0.9373940229 - 0.9454526901 0.9405459166 0.9377272725 - 0.9457291961 0.9408419132 0.9380605817 - 0.9460057020 0.9411379099 0.9383938909 - 0.9462822080 0.9414339066 0.9387272000 - 0.9465587139 0.9417299032 0.9390605092 - 0.9468351007 0.9420258999 0.9393938780 - 0.9471116066 0.9423218966 0.9397271872 - 0.9473881125 0.9426178932 0.9400566816 - 0.9476646185 0.9429138899 0.9403690100 - 0.9479410052 0.9432098269 0.9406812787 - 0.9482175112 0.9435058236 0.9409937263 - 0.9484940171 0.9438018203 0.9413059950 - 0.9487705231 0.9440978169 0.9416183233 - 0.9490469098 0.9443938136 0.9419305921 - 0.9493234158 0.9446898103 0.9422429800 - 0.9495999217 0.9449858069 0.9425553083 - 0.9498764277 0.9452818036 0.9428675771 - 0.9501438141 0.9455778003 0.9431800246 - 0.9504039884 0.9458737969 0.9434922934 - 0.9506642222 0.9461697936 0.9438046217 - 0.9509242773 0.9464657903 0.9441168904 - 0.9511845112 0.9467617869 0.9444292784 - 0.9514446855 0.9470577836 0.9447416067 - 0.9517048001 0.9473537803 0.9450538754 - 0.9519649744 0.9476497769 0.9453663230 - 0.9522252083 0.9479457736 0.9456785917 - 0.9524853230 0.9482417703 0.9459909201 - 0.9527454972 0.9485378265 0.9463031888 - 0.9530056715 0.9488338232 0.9466155767 - 0.9532657862 0.9491298199 0.9469279051 - 0.9535260201 0.9494258165 0.9472401738 - 0.9537861943 0.9497218132 0.9475526214 - 0.9540463090 0.9500167966 0.9478648901 - 0.9543064833 0.9502959251 0.9481772184 - 0.9545667171 0.9505749941 0.9484894872 - 0.9548268914 0.9508541226 0.9488018751 - 0.9550870061 0.9511331916 0.9491142035 - 0.9553471804 0.9514123201 0.9494264722 - 0.9556074142 0.9516913891 0.9497389197 - 0.9558675289 0.9519705176 0.9500480294 - 0.9561277032 0.9522495866 0.9503406882 - 0.9563878775 0.9525287747 0.9506334066 - 0.9566479921 0.9528079033 0.9509260058 - 0.9569082260 0.9530869722 0.9512187243 - 0.9571684003 0.9533661008 0.9515113831 - 0.9574285150 0.9536452293 0.9518041015 - 0.9576886892 0.9539242983 0.9520968199 - 0.9579489231 0.9542034268 0.9523894787 - 0.9582089782 0.9544824958 0.9526821971 - 0.9584692121 0.9547616243 0.9529749155 - 0.9587293863 0.9550406933 0.9532675743 - 0.9589895010 0.9553198814 0.9535602927 - 0.9592496753 0.9555990100 0.9538530111 - 0.9595099092 0.9558780789 0.9541457295 - 0.9597700238 0.9561572075 0.9544383883 - 0.9600284100 0.9564362764 0.9547309875 - 0.9602733254 0.9567154050 0.9550237060 - 0.9605181813 0.9569944739 0.9553164244 - 0.9607630968 0.9572736025 0.9556090832 - 0.9610080123 0.9575526714 0.9559018016 - 0.9612529278 0.9578318000 0.9561945200 - 0.9614977837 0.9581109881 0.9564871788 - 0.9617425799 0.9583901167 0.9567798972 - 0.9619874954 0.9586691856 0.9570726156 - 0.9622324109 0.9589483142 0.9573652744 - 0.9624773264 0.9592273831 0.9576579928 - 0.9627221823 0.9595065117 0.9579507113 - 0.9629670978 0.9597855806 0.9582434297 - 0.9632120132 0.9600610137 0.9585360885 - 0.9634569287 0.9603242278 0.9588286877 - 0.9637017846 0.9605875015 0.9591214061 - 0.9639465809 0.9608507156 0.9594141245 - 0.9641914964 0.9611139297 0.9597067833 - 0.9644364119 0.9613770843 0.9599995017 - 0.9646813273 0.9616402984 0.9602739215 - 0.9649261832 0.9619035125 0.9605482221 - 0.9651710987 0.9621667266 0.9608225226 - 0.9654160142 0.9624298811 0.9610968828 - 0.9656609297 0.9626930952 0.9613711834 - 0.9659057260 0.9629563093 0.9616456032 - 0.9661505818 0.9632195830 0.9619199038 - 0.9663954973 0.9634827971 0.9621942043 - 0.9666404128 0.9637460113 0.9624686241 - 0.9668853283 0.9640092254 0.9627429247 - 0.9671301842 0.9642723799 0.9630172253 - 0.9673750997 0.9645355940 0.9632915854 - 0.9676200151 0.9647988081 0.9635658860 - 0.9678648114 0.9650620222 0.9638401866 - 0.9681097269 0.9653251767 0.9641146064 - 0.9683545828 0.9655883908 0.9643889070 - 0.9685994983 0.9658517241 0.9646632075 - 0.9688444138 0.9661148787 0.9649376273 - 0.9690893292 0.9663780928 0.9652119279 - 0.9693341851 0.9666413069 0.9654862285 - 0.9695791006 0.9669045210 0.9657605886 - 0.9698240161 0.9671676755 0.9660348892 - 0.9700648189 0.9674308896 0.9663093090 - 0.9702954292 0.9676941037 0.9665836096 - 0.9705259800 0.9679573178 0.9668579102 - 0.9707565904 0.9682204723 0.9671322703 - 0.9709870815 0.9684838057 0.9674065709 - 0.9712176919 0.9687470198 0.9676808715 - 0.9714483023 0.9690101743 0.9679552913 - 0.9716789126 0.9692733884 0.9682295918 - 0.9719094038 0.9695366025 0.9685038924 - 0.9721400142 0.9697998166 0.9687783122 - 0.9723706245 0.9700593948 0.9690526128 - 0.9726011753 0.9703077078 0.9693269134 - 0.9728317857 0.9705559015 0.9696012735 - 0.9730622768 0.9708042145 0.9698755741 - 0.9732928872 0.9710524082 0.9701405764 - 0.9735234976 0.9713007212 0.9703977704 - 0.9737541080 0.9715489149 0.9706549048 - 0.9739845991 0.9717972279 0.9709120989 - 0.9742152095 0.9720454216 0.9711692929 - 0.9744458199 0.9722936749 0.9714264870 - 0.9746763706 0.9725419283 0.9716836810 - 0.9749069810 0.9727901816 0.9719408751 - 0.9751374722 0.9730383754 0.9721981287 - 0.9753680825 0.9732866883 0.9724552035 - 0.9755986929 0.9735348821 0.9727123976 - 0.9758293033 0.9737831950 0.9729695916 - 0.9760597944 0.9740313888 0.9732267857 - 0.9762904048 0.9742797017 0.9734839797 - 0.9765210152 0.9745278955 0.9737411737 - 0.9767516255 0.9747762084 0.9739984274 - 0.9769821763 0.9750244021 0.9742556214 - 0.9772127271 0.9752727151 0.9745126963 - 0.9774432778 0.9755209088 0.9747698903 - 0.9776738882 0.9757692218 0.9750270844 - 0.9779044986 0.9760174155 0.9752842784 - 0.9781349897 0.9762657285 0.9755414724 - 0.9783656001 0.9765139222 0.9757987261 - 0.9785962105 0.9767621756 0.9760559201 - 0.9788268209 0.9770104885 0.9763129950 - 0.9790573716 0.9772586823 0.9765701890 - 0.9792879224 0.9775069952 0.9768273830 - 0.9795184731 0.9777551889 0.9770845771 - 0.9797490835 0.9780035019 0.9773417711 - 0.9799796939 0.9782516956 0.9775990248 - 0.9801980257 0.9785000086 0.9778562188 - 0.9804152250 0.9787482023 0.9781134129 - 0.9806324244 0.9789965153 0.9783704877 - 0.9808496237 0.9792447090 0.9786276817 - 0.9810667038 0.9794930220 0.9788848758 - 0.9812839031 0.9797412157 0.9791421294 - 0.9815011024 0.9799895287 0.9793993235 - 0.9817183018 0.9802241921 0.9796565175 - 0.9819355011 0.9804583788 0.9799137115 - 0.9821525812 0.9806926250 0.9801601768 - 0.9823697805 0.9809268117 0.9804013968 - 0.9825869799 0.9811609983 0.9806426167 - 0.9828041792 0.9813951850 0.9808837175 - 0.9830213785 0.9816293120 0.9811248779 - 0.9832385182 0.9818634987 0.9813660979 - 0.9834557176 0.9820976853 0.9816073179 - 0.9836729169 0.9823318720 0.9818484783 - 0.9838901162 0.9825661182 0.9820895791 - 0.9841073155 0.9828003049 0.9823307991 - 0.9843243957 0.9830343723 0.9825720191 - 0.9845415950 0.9832686186 0.9828131795 - 0.9847587943 0.9835028052 0.9830542803 - 0.9849759936 0.9837369919 0.9832955003 - 0.9851931930 0.9839711785 0.9835367203 - 0.9854102731 0.9842054248 0.9837778807 - 0.9856274724 0.9844396114 0.9840189815 - 0.9858446717 0.9846736789 0.9842602015 - 0.9860618711 0.9849079251 0.9845014215 - 0.9862790108 0.9851421118 0.9847425818 - 0.9864962101 0.9853762984 0.9849838018 - 0.9867134094 0.9856104851 0.9852249026 - 0.9869306087 0.9858446717 0.9854661226 - 0.9871478081 0.9860787988 0.9857072830 - 0.9873648882 0.9863129854 0.9859485030 - 0.9875820875 0.9865471721 0.9861896038 - 0.9877992868 0.9867814183 0.9864308238 - 0.9880164862 0.9870156050 0.9866719842 - 0.9882336855 0.9872497916 0.9869132042 - 0.9884508252 0.9874839783 0.9871544242 - 0.9886680245 0.9877181053 0.9873955250 - 0.9888852239 0.9879522920 0.9876366854 - 0.9891024232 0.9881864786 0.9878779054 - 0.9893196225 0.9884207249 0.9881191254 - 0.9895367026 0.9886549115 0.9883602262 - 0.9897539020 0.9888890982 0.9886013865 - 0.9899711013 0.9891232252 0.9888426065 - 0.9901773930 0.9893574119 0.9890838265 - 0.9903820157 0.9895915985 0.9893249273 - 0.9905866981 0.9898257852 0.9895660877 - 0.9907913208 0.9900565743 0.9898073077 - 0.9909960032 0.9902775884 0.9900454879 - 0.9912006259 0.9904984832 0.9902716875 - 0.9914051890 0.9907194972 0.9904980063 - 0.9916098714 0.9909405112 0.9907242060 - 0.9918144941 0.9911614060 0.9909504056 - 0.9920191169 0.9913824201 0.9911767244 - 0.9922237992 0.9916033149 0.9914029241 - 0.9924284220 0.9918243289 0.9916291833 - 0.9926331043 0.9920452833 0.9918553829 - 0.9928377271 0.9922661781 0.9920815825 - 0.9930422902 0.9924871922 0.9923079014 - 0.9932469726 0.9927082062 0.9925341010 - 0.9934515953 0.9929291010 0.9927603006 - 0.9936562181 0.9931501150 0.9929866195 - 0.9938609004 0.9933711290 0.9932128191 - 0.9940655231 0.9935920238 0.9934390783 - 0.9942702055 0.9938129783 0.9936652780 - 0.9944748282 0.9940339923 0.9938914776 - 0.9946793914 0.9942548871 0.9941177964 - 0.9948840737 0.9944759011 0.9943439960 - 0.9950886965 0.9946967959 0.9945703149 - 0.9952933192 0.9949178100 0.9947965145 - 0.9954980016 0.9951388240 0.9950227141 - 0.9957026243 0.9953597188 0.9952489734 - 0.9959073067 0.9955806732 0.9954751730 - 0.9961119294 0.9958016872 0.9957014918 - 0.9963164926 0.9960225821 0.9959276915 - 0.9965211749 0.9962435961 0.9961538911 - 0.9967257977 0.9964646101 0.9963802099 - 0.9969304204 0.9966855049 0.9966064095 - 0.9971351027 0.9969065189 0.9968327284 - 0.9973397255 0.9971274734 0.9970589280 - 0.9975444078 0.9973484278 0.9972851276 - 0.9977489710 0.9975693822 0.9975113869 - 0.9979535937 0.9977903962 0.9977375865 - 0.9981582761 0.9980112910 0.9979637861 - 0.9983628988 0.9982323050 0.9981901050 - 0.9985675216 0.9984531999 0.9984163046 - 0.9987722039 0.9986742139 0.9986426234 - 0.9989768267 0.9988952279 0.9988688231 - 0.9991815090 0.9991161227 0.9990950227 - 0.9993860722 0.9993370771 0.9993212819 - 0.9995906949 0.9995580912 0.9995474815 - 0.9997953773 0.9997789860 0.9997738004 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0012913510 0.0008980598 0.0004710060 + 0.0025827030 0.0017961200 0.0009420120 + 0.0038740539 0.0026941791 0.0014130180 + 0.0051654060 0.0035922390 0.0018840240 + 0.0064567570 0.0044902992 0.0023550300 + 0.0077481088 0.0053883591 0.0028260360 + 0.0090394598 0.0062864190 0.0032970421 + 0.0103690401 0.0071844789 0.0037680480 + 0.0118096303 0.0080825379 0.0042390539 + 0.0132502196 0.0089805983 0.0047100601 + 0.0146908201 0.0098786578 0.0051810662 + 0.0161314104 0.0108593097 0.0056520719 + 0.0175720006 0.0118528605 0.0061230781 + 0.0190125909 0.0128464103 0.0065940842 + 0.0205063205 0.0138399703 0.0070650899 + 0.0221158192 0.0148335202 0.0075360960 + 0.0237253308 0.0158270709 0.0080071017 + 0.0253348406 0.0168206207 0.0084781079 + 0.0269443505 0.0178141799 0.0089491140 + 0.0285538603 0.0188077297 0.0094201202 + 0.0301827304 0.0198012795 0.0098911263 + 0.0319830701 0.0208809301 0.0103980303 + 0.0337834209 0.0219821204 0.0109157199 + 0.0355837606 0.0230832994 0.0114334105 + 0.0373841003 0.0241844803 0.0119511103 + 0.0391844399 0.0252856594 0.0124687999 + 0.0411023311 0.0263868403 0.0129864896 + 0.0431175716 0.0274880193 0.0135041801 + 0.0451328084 0.0285892002 0.0140218800 + 0.0471480489 0.0296903793 0.0145395696 + 0.0491632894 0.0308787897 0.0150572602 + 0.0513193496 0.0321012996 0.0155749600 + 0.0535753891 0.0333238207 0.0160926506 + 0.0558314398 0.0345463417 0.0166103393 + 0.0580874793 0.0357688516 0.0171280298 + 0.0603842996 0.0369913690 0.0176457297 + 0.0629082322 0.0382138900 0.0181634203 + 0.0654321536 0.0394364111 0.0186811108 + 0.0679560676 0.0407326184 0.0191988107 + 0.0705360919 0.0420918800 0.0197164994 + 0.0733550414 0.0434511304 0.0202579591 + 0.0761739910 0.0448103808 0.0208282005 + 0.0789929330 0.0461696312 0.0213984400 + 0.0820178986 0.0475288890 0.0219686702 + 0.0851573721 0.0488881394 0.0225389097 + 0.0882968530 0.0502754003 0.0231091492 + 0.0915927812 0.0517885610 0.0236793794 + 0.0950742513 0.0533017218 0.0242496207 + 0.0985557213 0.0548148789 0.0248198602 + 0.1022457033 0.0563280284 0.0253900904 + 0.1060836017 0.0578411892 0.0259603299 + 0.1099215001 0.0593543500 0.0265305694 + 0.1141121984 0.0609665997 0.0271007996 + 0.1183103025 0.0626525879 0.0276710391 + 0.1227174997 0.0643385872 0.0282412805 + 0.1272657067 0.0660245866 0.0288115107 + 0.1319428980 0.0677105933 0.0293817502 + 0.1368145049 0.0693965778 0.0299519897 + 0.1417827010 0.0712067708 0.0305764992 + 0.1469336003 0.0730861723 0.0312060006 + 0.1521732956 0.0749655813 0.0318355002 + 0.1575435996 0.0768449828 0.0324649997 + 0.1629938930 0.0787243918 0.0330944993 + 0.1685118973 0.0806729570 0.0337239988 + 0.1740811020 0.0827676505 0.0343534984 + 0.1796693951 0.0848623365 0.0349830016 + 0.1852522939 0.0869570300 0.0356125012 + 0.1908237040 0.0890517235 0.0362420008 + 0.1963316947 0.0912766010 0.0368715003 + 0.2017958015 0.0936091468 0.0375009999 + 0.2071722001 0.0959417075 0.0381304994 + 0.2124657035 0.0982742608 0.0387599990 + 0.2176671028 0.1006745026 0.0393894985 + 0.2227557003 0.1032672003 0.0400210209 + 0.2277524024 0.1058600023 0.0407175012 + 0.2326270938 0.1084527001 0.0414139815 + 0.2374019027 0.1111586019 0.0421104617 + 0.2420724034 0.1140320972 0.0428069495 + 0.2466181964 0.1169055998 0.0435034297 + 0.2511056960 0.1197791025 0.0441999100 + 0.2554233968 0.1229275018 0.0448963903 + 0.2597410083 0.1260987967 0.0455928706 + 0.2638502121 0.1292701066 0.0462893508 + 0.2679460049 0.1326791048 0.0469858386 + 0.2719362974 0.1361591071 0.0476823188 + 0.2758204043 0.1396390945 0.0483787991 + 0.2797045112 0.1433980018 0.0490752794 + 0.2834044099 0.1471890956 0.0497717597 + 0.2870891094 0.1510583013 0.0505192392 + 0.2907347083 0.1551516056 0.0512915812 + 0.2942334116 0.1592448950 0.0520639084 + 0.2977321148 0.1635666043 0.0528362505 + 0.3011702895 0.1679400951 0.0536085814 + 0.3044967949 0.1724427938 0.0543809198 + 0.3078233004 0.1770606041 0.0551532507 + 0.3110949993 0.1817495972 0.0559255891 + 0.3142628968 0.1865632981 0.0566979311 + 0.3174307942 0.1914165020 0.0574702583 + 0.3205712140 0.1963680983 0.0582426004 + 0.3235935867 0.2013394982 0.0590149313 + 0.3266159892 0.2063657045 0.0597872697 + 0.3296383023 0.2113949060 0.0606219508 + 0.3325434029 0.2164323032 0.0614803396 + 0.3354324996 0.2214559019 0.0623387285 + 0.3383215964 0.2264461070 0.0631971210 + 0.3411597013 0.2314081937 0.0640555099 + 0.3439269960 0.2363009006 0.0649138913 + 0.3466942906 0.2411601990 0.0657722801 + 0.3494617045 0.2459158003 0.0666306689 + 0.3521394134 0.2506479919 0.0674890578 + 0.3547956049 0.2552376986 0.0683474466 + 0.3574517071 0.2598274946 0.0692058429 + 0.3601036966 0.2642399073 0.0700715408 + 0.3626584113 0.2686452866 0.0710277036 + 0.3652130067 0.2729164064 0.0719838589 + 0.3677676022 0.2771278918 0.0729400069 + 0.3703106046 0.2812767923 0.0738961697 + 0.3727726042 0.2852917016 0.0748523325 + 0.3752346039 0.2893066108 0.0758084878 + 0.3776965141 0.2931610048 0.0767646432 + 0.3801530898 0.2969819903 0.0777207986 + 0.3825305104 0.3007636070 0.0786769614 + 0.3849078119 0.3043971956 0.0796331167 + 0.3872852027 0.3080308139 0.0806578100 + 0.3896625936 0.3115825951 0.0817251801 + 0.3919736147 0.3150376081 0.0827925429 + 0.3942736089 0.3184925914 0.0838598981 + 0.3965736926 0.3218528032 0.0849272683 + 0.3988736868 0.3251396120 0.0859946311 + 0.4011375904 0.3284263909 0.0870619863 + 0.4033668935 0.3316313028 0.0881293565 + 0.4055961072 0.3347609043 0.0891967192 + 0.4078254104 0.3378903866 0.0902953967 + 0.4100531042 0.3409723938 0.0914893299 + 0.4122174084 0.3439559937 0.0926832631 + 0.4143818021 0.3469395041 0.0938771889 + 0.4165461957 0.3499230146 0.0950711221 + 0.4187105894 0.3527748883 0.0962650478 + 0.4208509922 0.3556233943 0.0974589810 + 0.4229558110 0.3584718108 0.0986529067 + 0.4250606000 0.3612624109 0.0998468399 + 0.4271655083 0.3639861941 0.1011663005 + 0.4292702973 0.3667100072 0.1025042981 + 0.4313394129 0.3694337904 0.1038423032 + 0.4333893955 0.3720667064 0.1051801965 + 0.4354394078 0.3746758103 0.1065182015 + 0.4374893904 0.3772849143 0.1078561991 + 0.4395394921 0.3798938990 0.1091940999 + 0.4415503144 0.3824017942 0.1105972007 + 0.4435496926 0.3849053979 0.1120989993 + 0.4455491900 0.3874089122 0.1136007011 + 0.4475485981 0.3899124861 0.1151024029 + 0.4495480955 0.3923225105 0.1166040972 + 0.4515112042 0.3947291076 0.1181057990 + 0.4534637928 0.3971355855 0.1196075976 + 0.4554164112 0.3995422125 0.1212465018 + 0.4573689997 0.4018765986 0.1229339987 + 0.4593215883 0.4041940868 0.1246215031 + 0.4612458050 0.4065114856 0.1263090074 + 0.4631549120 0.4088290036 0.1279965043 + 0.4650638998 0.4111058116 0.1296840012 + 0.4669730067 0.4133413136 0.1315421015 + 0.4688819945 0.4155766964 0.1334394962 + 0.4707742035 0.4178121984 0.1353369951 + 0.4726425111 0.4200460017 0.1372344047 + 0.4745109081 0.4222060144 0.1391319036 + 0.4763793051 0.4243659079 0.1411572993 + 0.4782476127 0.4265258014 0.1432905942 + 0.4801135957 0.4286858141 0.1454239935 + 0.4819437861 0.4308184981 0.1475573033 + 0.4837738872 0.4329088032 0.1496907026 + 0.4856041074 0.4349991083 0.1520487964 + 0.4874342084 0.4370894134 0.1544450074 + 0.4892643988 0.4391798079 0.1568412036 + 0.4910728931 0.4412310123 0.1592375040 + 0.4928669035 0.4432570040 0.1618310958 + 0.4946610034 0.4452829957 0.1645168066 + 0.4964550138 0.4473089874 0.1672026068 + 0.4982489944 0.4493350089 0.1698883027 + 0.5000422001 0.4513210058 0.1728748977 + 0.5018019080 0.4532875121 0.1758745015 + 0.5035614967 0.4552539885 0.1788741052 + 0.5053212047 0.4572204053 0.1820818037 + 0.5070807934 0.4591869116 0.1854145974 + 0.5088403821 0.4611209929 0.1887473017 + 0.5105888247 0.4630320966 0.1922948956 + 0.5123155117 0.4649432898 0.1959716976 + 0.5140420794 0.4668545127 0.1996484995 + 0.5157688260 0.4687657058 0.2036353946 + 0.5174953938 0.4706586897 0.2076551020 + 0.5192220807 0.4725184143 0.2118106931 + 0.5209311843 0.4743781090 0.2161566019 + 0.5226259232 0.4762378037 0.2205363065 + 0.5243206024 0.4780975878 0.2251745015 + 0.5260154009 0.4799573123 0.2298126072 + 0.5277100801 0.4817700088 0.2346820980 + 0.5294048190 0.4835816026 0.2395613045 + 0.5310792923 0.4853931963 0.2446004003 + 0.5327429175 0.4872049093 0.2496553957 + 0.5344064832 0.4890165031 0.2548052073 + 0.5360699892 0.4908075035 0.2599619925 + 0.5377336144 0.4925738871 0.2651447952 + 0.5393971205 0.4943403900 0.2703250945 + 0.5410410762 0.4961068928 0.2754637897 + 0.5426740050 0.4978733957 0.2805902958 + 0.5443068743 0.4996398985 0.2856245041 + 0.5459396839 0.5013725162 0.2906390131 + 0.5475726128 0.5030964017 0.2955218852 + 0.5492054224 0.5048202872 0.3003894985 + 0.5508226156 0.5065442920 0.3050881028 + 0.5524250269 0.5082681775 0.3097867072 + 0.5540273190 0.5099921227 0.3142902851 + 0.5556296706 0.5116760135 0.3187845051 + 0.5572320223 0.5133596063 0.3231228888 + 0.5588343740 0.5150431991 0.3274034858 + 0.5604283810 0.5167267919 0.3315997124 + 0.5620002151 0.5184103847 0.3356657028 + 0.5635718703 0.5200918913 0.3397318125 + 0.5651437044 0.5217370987 0.3436022103 + 0.5667154193 0.5233823061 0.3474588096 + 0.5682871938 0.5250275135 0.3512471020 + 0.5698590279 0.5266727209 0.3549034894 + 0.5714026093 0.5283179283 0.3585597873 + 0.5729435086 0.5299631953 0.3621017039 + 0.5744842887 0.5315724015 0.3655692935 + 0.5760251880 0.5331807733 0.3690369129 + 0.5775660276 0.5347892046 0.3723773956 + 0.5791069269 0.5363975167 0.3756690919 + 0.5806344748 0.5380058885 0.3789606988 + 0.5821440220 0.5396143198 0.3821411133 + 0.5836533904 0.5411956906 0.3852699995 + 0.5851628184 0.5427685976 0.3883990049 + 0.5866721869 0.5443415046 0.3914547861 + 0.5881816745 0.5459142923 0.3944340944 + 0.5896911025 0.5474871993 0.3974134028 + 0.5911749005 0.5490601063 0.4003747106 + 0.5926522017 0.5506191254 0.4032168984 + 0.5941295028 0.5521575212 0.4060590863 + 0.5956066847 0.5536959171 0.4089013040 + 0.5970839858 0.5552341938 0.4116666019 + 0.5985612869 0.5567725897 0.4143835008 + 0.6000376940 0.5583109856 0.4171003997 + 0.6014819145 0.5598493814 0.4198172987 + 0.6029260755 0.5613573790 0.4224275053 + 0.6043702960 0.5628619790 0.4250301123 + 0.6058145761 0.5643666983 0.4276326001 + 0.6072587967 0.5658714175 0.4302257001 + 0.6087030172 0.5673760772 0.4327239990 + 0.6101437807 0.5688806772 0.4352222979 + 0.6115540266 0.5703768730 0.4377205968 + 0.6129640937 0.5718483925 0.4402106106 + 0.6143742800 0.5733199120 0.4426138103 + 0.6157845259 0.5747913718 0.4450170994 + 0.6171945930 0.5762628913 0.4474202991 + 0.6186047792 0.5777344108 0.4498234987 + 0.6200146079 0.5792058706 0.4521464109 + 0.6213896275 0.5806623101 0.4544628859 + 0.6227645874 0.5821009278 0.4567793906 + 0.6241396070 0.5835396051 0.4590958953 + 0.6255146265 0.5849782228 0.4613640904 + 0.6268895864 0.5864169002 0.4636014104 + 0.6282646060 0.5878555179 0.4658387005 + 0.6296396255 0.5892940760 0.4680759907 + 0.6309878230 0.5907161236 0.4703032076 + 0.6323264837 0.5921220183 0.4724679887 + 0.6336652040 0.5935279727 0.4746327996 + 0.6350039244 0.5949339271 0.4767976999 + 0.6363425255 0.5963398218 0.4789625108 + 0.6376811862 0.5977457166 0.4810926914 + 0.6390199065 0.5991516113 0.4831911922 + 0.6403484941 0.6005445123 0.4852896035 + 0.6416497231 0.6019176841 0.4873880148 + 0.6429507732 0.6032907963 0.4894863963 + 0.6442520022 0.6046640277 0.4915387034 + 0.6455531716 0.6060370803 0.4935761094 + 0.6468542814 0.6074103117 0.4956134856 + 0.6481555104 0.6087834239 0.4976508021 + 0.6494566202 0.6101527810 0.4996882081 + 0.6507353187 0.6114928722 0.5016778708 + 0.6519978046 0.6128330827 0.5036588907 + 0.6532601714 0.6141732931 0.5056399703 + 0.6545227170 0.6155133843 0.5076211095 + 0.6557852030 0.6168535948 0.5096021295 + 0.6570476890 0.6181936860 0.5115414858 + 0.6583101749 0.6195338964 0.5134704709 + 0.6595727205 0.6208522916 0.5153993964 + 0.6608088017 0.6221591830 0.5173283815 + 0.6620315909 0.6234660149 0.5192573071 + 0.6632543206 0.6247729063 0.5211564898 + 0.6644769907 0.6260796785 0.5230368972 + 0.6656997204 0.6273866296 0.5249174237 + 0.6669223905 0.6286934018 0.5267977715 + 0.6681451201 0.6300002933 0.5286782980 + 0.6693677902 0.6312733889 0.5305452943 + 0.6705707908 0.6325464845 0.5323804021 + 0.6717528105 0.6338195801 0.5342155099 + 0.6729347110 0.6350926757 0.5360506177 + 0.6741166115 0.6363657713 0.5378857255 + 0.6752985716 0.6376389265 0.5397207737 + 0.6764804721 0.6389120221 0.5415198207 + 0.6776624918 0.6401801705 0.5433121920 + 0.6788443923 0.6414189935 0.5451046228 + 0.6800253987 0.6426578760 0.5468971133 + 0.6811656952 0.6438966990 0.5486894846 + 0.6823059916 0.6451355815 0.5504710078 + 0.6834462881 0.6463744044 0.5522230268 + 0.6845865846 0.6476132870 0.5539749861 + 0.6857268810 0.6488522291 0.5557268858 + 0.6868671179 0.6500884891 0.5574789047 + 0.6880074143 0.6512925029 0.5592309237 + 0.6891477108 0.6524965167 0.5609611869 + 0.6902772784 0.6537005901 0.5626745224 + 0.6913753152 0.6549046040 0.5643877983 + 0.6924731731 0.6561086774 0.5661010742 + 0.6935710907 0.6573126912 0.5678144097 + 0.6946691275 0.6585168242 0.5695276856 + 0.6957669854 0.6597207785 0.5712140203 + 0.6968650222 0.6608976722 0.5728901029 + 0.6979628801 0.6620662808 0.5745661259 + 0.6990607977 0.6632350087 0.5762422085 + 0.7001525760 0.6644036174 0.5779181719 + 0.7012076974 0.6655722857 0.5795943141 + 0.7022626996 0.6667408943 0.5812429190 + 0.7033178210 0.6679096222 0.5828827024 + 0.7043728828 0.6690781713 0.5845226049 + 0.7054278851 0.6702393293 0.5861623883 + 0.7064830065 0.6713719964 0.5878022909 + 0.7075381279 0.6725047231 0.5894420743 + 0.7085930705 0.6736373901 0.5910584927 + 0.7096481919 0.6747701168 0.5926628113 + 0.7106745243 0.6759027839 0.5942670703 + 0.7116863132 0.6770355105 0.5958713889 + 0.7126982212 0.6781681776 0.5974757075 + 0.7137101293 0.6793009043 0.5990800261 + 0.7147219777 0.6804196239 0.6006693244 + 0.7157338262 0.6815158129 0.6022384763 + 0.7167456746 0.6826120019 0.6038076282 + 0.7177575827 0.6837081909 0.6053767204 + 0.7187694907 0.6848043799 0.6069458723 + 0.7197813988 0.6859006286 0.6085150242 + 0.7207592726 0.6869968176 0.6100823283 + 0.7217279077 0.6880930066 0.6116163135 + 0.7226964235 0.6891891956 0.6131502986 + 0.7236649990 0.6902757883 0.6146842837 + 0.7246335745 0.6913350224 0.6162183881 + 0.7256022096 0.6923943162 0.6177523732 + 0.7265707850 0.6934534907 0.6192864180 + 0.7275393009 0.6945127249 0.6208015084 + 0.7285078764 0.6955720186 0.6223003268 + 0.7294765115 0.6966311932 0.6237990260 + 0.7304251790 0.6976904273 0.6252977252 + 0.7313506007 0.6987497211 0.6267964244 + 0.7322760224 0.6998088956 0.6282951236 + 0.7332013845 0.7008374929 0.6297938228 + 0.7341268063 0.7018594146 0.6312615871 + 0.7350522280 0.7028812170 0.6327245831 + 0.7359775901 0.7039030790 0.6341875196 + 0.7369030118 0.7049250007 0.6356505156 + 0.7378283739 0.7059468031 0.6371133924 + 0.7387537956 0.7069687247 0.6385763288 + 0.7396792173 0.7079905868 0.6400383115 + 0.7405766249 0.7090123892 0.6414648294 + 0.7414591908 0.7100329995 0.6428912878 + 0.7423416972 0.7110173106 0.6443178058 + 0.7432242036 0.7120015025 0.6457443833 + 0.7441068292 0.7129856944 0.6471709013 + 0.7449892759 0.7139698863 0.6485974193 + 0.7458719015 0.7149540782 0.6500232816 + 0.7467544079 0.7159383297 0.6514126062 + 0.7476369739 0.7169225216 0.6528018117 + 0.7485194802 0.7179067731 0.6541910768 + 0.7494021058 0.7188910246 0.6555804014 + 0.7502709031 0.7198752165 0.6569697261 + 0.7511112094 0.7208263874 0.6583588719 + 0.7519513965 0.7217727900 0.6597481966 + 0.7527915835 0.7227191925 0.6611062288 + 0.7536318898 0.7236654758 0.6624572873 + 0.7544720769 0.7246118784 0.6638084054 + 0.7553123236 0.7255582809 0.6651595235 + 0.7561525106 0.7265046835 0.6665105820 + 0.7569928169 0.7274510860 0.6678617001 + 0.7578330040 0.7283974886 0.6692128181 + 0.7586731911 0.7293438911 0.6705474854 + 0.7595134974 0.7302786708 0.6718593836 + 0.7603362203 0.7311872244 0.6731712818 + 0.7611348033 0.7320957184 0.6744831800 + 0.7619335055 0.7330042124 0.6757950187 + 0.7627320886 0.7339128256 0.6771069169 + 0.7635306716 0.7348213196 0.6784188151 + 0.7643293738 0.7357298136 0.6797307134 + 0.7651280165 0.7366383076 0.6810106039 + 0.7659267187 0.7375468016 0.6822822094 + 0.7667253017 0.7384554148 0.6835538149 + 0.7675240040 0.7393639088 0.6848254800 + 0.7683225870 0.7402610779 0.6860970855 + 0.7691212296 0.7411319017 0.6873686910 + 0.7699198723 0.7420026064 0.6886402965 + 0.7706819177 0.7428733706 0.6899120212 + 0.7714399099 0.7437440753 0.6911451221 + 0.7721979022 0.7446148992 0.6923754811 + 0.7729557753 0.7454857230 0.6936057806 + 0.7737138271 0.7463564277 0.6948360801 + 0.7744718194 0.7472271919 0.6960663795 + 0.7752296925 0.7480978966 0.6972966790 + 0.7759876847 0.7489687204 0.6985270977 + 0.7767456770 0.7498394847 0.6997573972 + 0.7775036097 0.7506796122 0.7009537816 + 0.7782616019 0.7515128255 0.7021418214 + 0.7790195942 0.7523460984 0.7033298016 + 0.7797775269 0.7531793118 0.7045179009 + 0.7805075049 0.7540125847 0.7057058811 + 0.7812259197 0.7548457980 0.7068939209 + 0.7819442153 0.7556790113 0.7080820203 + 0.7826625705 0.7565122843 0.7092700005 + 0.7833809257 0.7573454976 0.7104414105 + 0.7840992808 0.7581787705 0.7115862966 + 0.7848175764 0.7590119839 0.7127311826 + 0.7855359912 0.7598451972 0.7138760090 + 0.7862542868 0.7606481910 0.7150208950 + 0.7869727015 0.7614442706 0.7161657810 + 0.7876909971 0.7622404099 0.7173107266 + 0.7884094119 0.7630366087 0.7184554935 + 0.7891278267 0.7638326883 0.7196003795 + 0.7898461223 0.7646288276 0.7207167149 + 0.7905343175 0.7654249072 0.7218176723 + 0.7912142277 0.7662209868 0.7229186893 + 0.7918941975 0.7670171261 0.7240195870 + 0.7925741076 0.7678132057 0.7251206040 + 0.7932540178 0.7686092854 0.7262216210 + 0.7939339876 0.7694054246 0.7273225784 + 0.7946138978 0.7701922059 0.7284234762 + 0.7952939272 0.7709516883 0.7295244932 + 0.7959737778 0.7717111707 0.7306001782 + 0.7966536880 0.7724707127 0.7316566706 + 0.7973337173 0.7732301950 0.7327132821 + 0.7980136275 0.7739896774 0.7337697744 + 0.7986935973 0.7747492194 0.7348263264 + 0.7993735075 0.7755087018 0.7358828187 + 0.8000504971 0.7762681842 0.7369393110 + 0.8006933928 0.7770277262 0.7379958034 + 0.8013362288 0.7777872086 0.7390522957 + 0.8019790053 0.7785466909 0.7401041985 + 0.8026219010 0.7793061733 0.7411159277 + 0.8032646775 0.7800626159 0.7421275973 + 0.8039075732 0.7807862163 0.7431393266 + 0.8045504093 0.7815096974 0.7441509962 + 0.8051931858 0.7822332978 0.7451627254 + 0.8058360815 0.7829567790 0.7461743951 + 0.8064789176 0.7836803794 0.7471861243 + 0.8071216941 0.7844039202 0.7481977940 + 0.8077645898 0.7851275206 0.7492095232 + 0.8084074259 0.7858511209 0.7502114177 + 0.8090502024 0.7865746021 0.7511780858 + 0.8096930981 0.7872982025 0.7521448731 + 0.8103172779 0.7880216837 0.7531116009 + 0.8109244108 0.7887452841 0.7540783286 + 0.8115314841 0.7894688249 0.7550451159 + 0.8121386766 0.7901830077 0.7560117841 + 0.8127458096 0.7908713818 0.7569785714 + 0.8133528829 0.7915598154 0.7579452991 + 0.8139600754 0.7922481894 0.7589120269 + 0.8145672083 0.7929366231 0.7598788142 + 0.8151742816 0.7936249971 0.7608063221 + 0.8157814145 0.7943133712 0.7617281079 + 0.8163886070 0.7950018048 0.7626500130 + 0.8169956803 0.7956901193 0.7635719180 + 0.8176028132 0.7963784933 0.7644938231 + 0.8182100058 0.7970669270 0.7654156089 + 0.8188170791 0.7977553010 0.7663375139 + 0.8194242120 0.7984436750 0.7672594190 + 0.8200296164 0.7991321087 0.7681813240 + 0.8206024766 0.7998204827 0.7691031098 + 0.8211753964 0.8004835248 0.7700238228 + 0.8217483163 0.8011376858 0.7709010839 + 0.8223211169 0.8017917871 0.7717785239 + 0.8228939772 0.8024458885 0.7726557851 + 0.8234668970 0.8030999899 0.7735331059 + 0.8240398169 0.8037540913 0.7744104862 + 0.8246126771 0.8044083118 0.7752878070 + 0.8251855969 0.8050624132 0.7761651278 + 0.8257585168 0.8057165146 0.7770425081 + 0.8263313770 0.8063706160 0.7779198289 + 0.8269041777 0.8070247173 0.7787970901 + 0.8274770975 0.8076788187 0.7796744704 + 0.8280500174 0.8083329797 0.7805240750 + 0.8286228776 0.8089870811 0.7813575268 + 0.8291957974 0.8096411824 0.7821907997 + 0.8297687173 0.8102803230 0.7830241919 + 0.8303220272 0.8109011054 0.7838575244 + 0.8308622241 0.8115220070 0.7846909165 + 0.8314023018 0.8121427894 0.7855241895 + 0.8319424987 0.8127635717 0.7863575816 + 0.8324825764 0.8133844733 0.7871909142 + 0.8330227733 0.8140053153 0.7880243063 + 0.8335629106 0.8146260977 0.7888575792 + 0.8341031075 0.8152469993 0.7896909714 + 0.8346431851 0.8158677816 0.7904971242 + 0.8351833820 0.8164886236 0.7912873030 + 0.8357235193 0.8171095252 0.7920774221 + 0.8362637162 0.8177303076 0.7928674817 + 0.8368037939 0.8183512092 0.7936577201 + 0.8373439908 0.8189719915 0.7944477797 + 0.8378841281 0.8195927739 0.7952378988 + 0.8384243250 0.8202025890 0.7960280776 + 0.8389644027 0.8207911849 0.7968181968 + 0.8395045996 0.8213797808 0.7976083159 + 0.8400421143 0.8219683766 0.7983984947 + 0.8405510783 0.8225570917 0.7991886139 + 0.8410599828 0.8231456876 0.7999786735 + 0.8415690064 0.8237342834 0.8007277846 + 0.8420779109 0.8243228793 0.8014757037 + 0.8425868154 0.8249114752 0.8022235036 + 0.8430957794 0.8255001903 0.8029714227 + 0.8436046839 0.8260887861 0.8037192822 + 0.8441137075 0.8266773820 0.8044672012 + 0.8446226120 0.8272659779 0.8052151203 + 0.8451315165 0.8278546929 0.8059629798 + 0.8456404805 0.8284432888 0.8067108989 + 0.8461493850 0.8290318847 0.8074588180 + 0.8466584086 0.8296204805 0.8082066774 + 0.8471673131 0.8301981091 0.8089544773 + 0.8476762176 0.8307555914 0.8097023964 + 0.8481851816 0.8313131928 0.8104255795 + 0.8486940861 0.8318706751 0.8111323714 + 0.8492031097 0.8324282765 0.8118392229 + 0.8497120142 0.8329858184 0.8125458956 + 0.8502081037 0.8335434198 0.8132526875 + 0.8506873250 0.8341009021 0.8139594793 + 0.8511666059 0.8346583843 0.8146662712 + 0.8516458869 0.8352159858 0.8153731227 + 0.8521251082 0.8357735276 0.8160799146 + 0.8526043892 0.8363311291 0.8167867064 + 0.8530836105 0.8368886113 0.8174934983 + 0.8535628915 0.8374462128 0.8182002902 + 0.8540421724 0.8380036950 0.8189070821 + 0.8545213938 0.8385611773 0.8196138144 + 0.8550006747 0.8391187787 0.8203026056 + 0.8554800153 0.8396763206 0.8209695816 + 0.8559591770 0.8402212858 0.8216364980 + 0.8564385176 0.8407490253 0.8223034739 + 0.8569177985 0.8412765861 0.8229705095 + 0.8573970199 0.8418043256 0.8236374855 + 0.8578763008 0.8423318863 0.8243045211 + 0.8583555818 0.8428596258 0.8249714971 + 0.8588348031 0.8433871865 0.8256384730 + 0.8593140841 0.8439149261 0.8263055086 + 0.8597934246 0.8444424868 0.8269724846 + 0.8602566123 0.8449702263 0.8276394010 + 0.8607078195 0.8454977870 0.8283063769 + 0.8611589074 0.8460255265 0.8289734125 + 0.8616099954 0.8465530872 0.8296403885 + 0.8620610833 0.8470808268 0.8302897215 + 0.8625121713 0.8476083875 0.8309183121 + 0.8629633784 0.8481361270 0.8315469027 + 0.8634145260 0.8486636877 0.8321754932 + 0.8638656139 0.8491914272 0.8328040838 + 0.8643167019 0.8497189879 0.8334327936 + 0.8647677898 0.8502333164 0.8340613842 + 0.8652189970 0.8507323265 0.8346899748 + 0.8656700850 0.8512312174 0.8353186250 + 0.8661211729 0.8517302275 0.8359472156 + 0.8665723205 0.8522291780 0.8365758061 + 0.8670234084 0.8527281880 0.8372043967 + 0.8674746156 0.8532270789 0.8378329873 + 0.8679257035 0.8537260890 0.8384615779 + 0.8683767915 0.8542250991 0.8390902281 + 0.8688278794 0.8547241092 0.8397188783 + 0.8692790270 0.8552231193 0.8403270841 + 0.8697301745 0.8557220101 0.8409187794 + 0.8701705933 0.8562210202 0.8415105939 + 0.8705950975 0.8567199707 0.8421024084 + 0.8710194826 0.8572189808 0.8426941037 + 0.8714439869 0.8577179909 0.8432859182 + 0.8718684912 0.8582168818 0.8438776135 + 0.8722929955 0.8587158918 0.8444694281 + 0.8727173805 0.8592149019 0.8450611234 + 0.8731418848 0.8597139120 0.8456528783 + 0.8735663891 0.8602011204 0.8462445736 + 0.8739908934 0.8606727123 0.8468363881 + 0.8744153976 0.8611443043 0.8474280834 + 0.8748397827 0.8616157770 0.8480198979 + 0.8752642870 0.8620874286 0.8486115932 + 0.8756887913 0.8625589013 0.8492034078 + 0.8761132956 0.8630304933 0.8497951031 + 0.8765377998 0.8635020256 0.8503637910 + 0.8769621849 0.8639736176 0.8509203196 + 0.8773866892 0.8644450903 0.8514767885 + 0.8778111935 0.8649166822 0.8520333171 + 0.8782356977 0.8653882146 0.8525897861 + 0.8786600828 0.8658598065 0.8531463146 + 0.8790845871 0.8663312793 0.8537027836 + 0.8795090914 0.8668028712 0.8542593122 + 0.8799335957 0.8672744036 0.8548157811 + 0.8803368211 0.8677459955 0.8553723097 + 0.8807361126 0.8682175279 0.8559287786 + 0.8811354041 0.8686891198 0.8564853072 + 0.8815348148 0.8691605926 0.8570417166 + 0.8819341063 0.8696321845 0.8575981855 + 0.8823333979 0.8700979948 0.8581547141 + 0.8827326894 0.8705433011 0.8587111831 + 0.8831319809 0.8709887266 0.8592677116 + 0.8835312724 0.8714340925 0.8598241806 + 0.8839306235 0.8718795180 0.8603577018 + 0.8843299150 0.8723248839 0.8608806133 + 0.8847292066 0.8727701902 0.8614034057 + 0.8851284981 0.8732156157 0.8619263172 + 0.8855277896 0.8736609817 0.8624492288 + 0.8859272003 0.8741064072 0.8629720211 + 0.8863264918 0.8745517135 0.8634948730 + 0.8867257833 0.8749970794 0.8640177846 + 0.8871250749 0.8754425049 0.8645406961 + 0.8875244260 0.8758878708 0.8650634885 + 0.8879237175 0.8763331771 0.8655864000 + 0.8883230090 0.8767786026 0.8661093116 + 0.8887223005 0.8772240281 0.8666321039 + 0.8891215920 0.8776693940 0.8671550155 + 0.8895208836 0.8781148195 0.8676779270 + 0.8899201751 0.8785601258 0.8682007790 + 0.8903005719 0.8790054917 0.8687235713 + 0.8906760812 0.8794509172 0.8692464828 + 0.8910517097 0.8798962831 0.8697693944 + 0.8914272785 0.8803225160 0.8702744246 + 0.8918027878 0.8807430267 0.8707653284 + 0.8921784163 0.8811634183 0.8712561727 + 0.8925539851 0.8815839291 0.8717470765 + 0.8929294944 0.8820043206 0.8722379804 + 0.8933051229 0.8824247718 0.8727290034 + 0.8936806917 0.8828452229 0.8732199073 + 0.8940562010 0.8832656741 0.8737108111 + 0.8944318295 0.8836861849 0.8742017150 + 0.8948073983 0.8841065764 0.8746926188 + 0.8951830268 0.8845270872 0.8751835227 + 0.8955584764 0.8849474788 0.8756744862 + 0.8959341049 0.8853679895 0.8761653900 + 0.8963096738 0.8857883811 0.8766562939 + 0.8966851830 0.8862088919 0.8771471977 + 0.8970608115 0.8866292834 0.8776381016 + 0.8974363804 0.8870497942 0.8781290054 + 0.8978118896 0.8874701858 0.8786200285 + 0.8981875181 0.8878906965 0.8791108727 + 0.8985630870 0.8883110881 0.8796017766 + 0.8989385962 0.8887315989 0.8800870180 + 0.8993142247 0.8891521096 0.8805475831 + 0.8996897936 0.8895725012 0.8810082078 + 0.9000614882 0.8899930120 0.8814688921 + 0.9004147053 0.8903900981 0.8819295168 + 0.9007679224 0.8907868862 0.8823900819 + 0.9011210799 0.8911836743 0.8828508258 + 0.9014742970 0.8915804029 0.8833113909 + 0.9018275142 0.8919771910 0.8837720156 + 0.9021806717 0.8923739791 0.8842326999 + 0.9025338888 0.8927707076 0.8846933246 + 0.9028872252 0.8931674957 0.8851538897 + 0.9032403827 0.8935642838 0.8856145740 + 0.9035935998 0.8939610720 0.8860751987 + 0.9039468169 0.8943578005 0.8865358233 + 0.9042999744 0.8947545886 0.8869965076 + 0.9046531916 0.8951513767 0.8874570727 + 0.9050064087 0.8955481052 0.8879176974 + 0.9053596258 0.8959448934 0.8883783221 + 0.9057129025 0.8963416815 0.8888390064 + 0.9060661197 0.8967384100 0.8892995715 + 0.9064192772 0.8971351981 0.8897601962 + 0.9067724943 0.8975319862 0.8902071118 + 0.9071257114 0.8979287744 0.8906391263 + 0.9074789286 0.8983255029 0.8910710812 + 0.9078320861 0.8987222910 0.8915032148 + 0.9081853032 0.8991190791 0.8919352293 + 0.9085385203 0.8995158076 0.8923671842 + 0.9088917971 0.8999125957 0.8927991986 + 0.9092450142 0.9002919197 0.8932312131 + 0.9095981717 0.9006661773 0.8936632276 + 0.9099513888 0.9010404944 0.8940951824 + 0.9102864861 0.9014148116 0.8945271969 + 0.9106187224 0.9017891288 0.8949592113 + 0.9109507799 0.9021633863 0.8953912258 + 0.9112830162 0.9025377035 0.8958231807 + 0.9116151929 0.9029120207 0.8962551951 + 0.9119474292 0.9032862782 0.8966872096 + 0.9122796059 0.9036605954 0.8971192241 + 0.9126117229 0.9040349126 0.8975511789 + 0.9129438996 0.9044092298 0.8979831934 + 0.9132760763 0.9047834873 0.8984152079 + 0.9136083126 0.9051578045 0.8988472223 + 0.9139404893 0.9055321217 0.8992791772 + 0.9142726064 0.9059063792 0.8997111917 + 0.9146047831 0.9062806964 0.9001343250 + 0.9149370193 0.9066550732 0.9005392790 + 0.9152691960 0.9070293903 0.9009442925 + 0.9156013727 0.9074037075 0.9013493061 + 0.9159336090 0.9077780247 0.9017543197 + 0.9162657261 0.9081522822 0.9021592736 + 0.9165979028 0.9085265994 0.9025642872 + 0.9169300795 0.9089009166 0.9029693007 + 0.9172623158 0.9092751741 0.9033743143 + 0.9175944924 0.9096494913 0.9037793279 + 0.9179266095 0.9100223780 0.9041842818 + 0.9182587862 0.9103754759 0.9045892954 + 0.9185910225 0.9107285142 0.9049943089 + 0.9189231992 0.9110814929 0.9053993225 + 0.9192553759 0.9114345908 0.9058042765 + 0.9195876122 0.9117876291 0.9062092900 + 0.9199197292 0.9121406078 0.9066143036 + 0.9202368855 0.9124937057 0.9070193172 + 0.9205493927 0.9128466845 0.9074242711 + 0.9208617806 0.9131997228 0.9078292847 + 0.9211742282 0.9135528207 0.9082342982 + 0.9214866161 0.9139057994 0.9086393118 + 0.9217990041 0.9142587781 0.9090443254 + 0.9221115112 0.9146118760 0.9094492793 + 0.9224238992 0.9149649143 0.9098542929 + 0.9227362871 0.9153178930 0.9102429748 + 0.9230486751 0.9156708717 0.9106225967 + 0.9233611822 0.9160240293 0.9110022187 + 0.9236736298 0.9163770080 0.9113817811 + 0.9239860177 0.9167299867 0.9117614031 + 0.9242984056 0.9170830846 0.9121410251 + 0.9246109128 0.9174361229 0.9125205874 + 0.9249233007 0.9177891016 0.9129002094 + 0.9252356887 0.9181421995 0.9132797122 + 0.9255480766 0.9184951782 0.9136592746 + 0.9258605242 0.9188482165 0.9140388966 + 0.9261729717 0.9192013144 0.9144185185 + 0.9264854193 0.9195542932 0.9147980809 + 0.9267978072 0.9199072719 0.9151777029 + 0.9271101952 0.9202455282 0.9155573249 + 0.9274227023 0.9205784202 0.9159368873 + 0.9277350903 0.9209113121 0.9163165092 + 0.9280474782 0.9212443233 0.9166960120 + 0.9283599257 0.9215772152 0.9170755744 + 0.9286723137 0.9219101071 0.9174551964 + 0.9289848208 0.9222429991 0.9178348184 + 0.9292972088 0.9225758910 0.9182143807 + 0.9296095967 0.9229087830 0.9185940027 + 0.9299219847 0.9232416749 0.9189736247 + 0.9302204847 0.9235746861 0.9193531871 + 0.9305143952 0.9239075780 0.9197328091 + 0.9308083057 0.9242405295 0.9201052785 + 0.9311022162 0.9245734215 0.9204609990 + 0.9313960075 0.9249063134 0.9208167195 + 0.9316899180 0.9252392054 0.9211723804 + 0.9319838285 0.9255720973 0.9215281010 + 0.9322776794 0.9259051085 0.9218838215 + 0.9325714707 0.9262380004 0.9222394824 + 0.9328653812 0.9265708923 0.9225953221 + 0.9331592917 0.9269037843 0.9229509830 + 0.9334532022 0.9272366762 0.9233067036 + 0.9337469935 0.9275696278 0.9236624241 + 0.9340409040 0.9279025197 0.9240180850 + 0.9343348145 0.9282354712 0.9243738055 + 0.9346287251 0.9285684228 0.9247295260 + 0.9349225163 0.9289013147 0.9250851870 + 0.9352164268 0.9292342067 0.9254410267 + 0.9355102777 0.9295670986 0.9257966876 + 0.9358041286 0.9298999906 0.9261524081 + 0.9360979795 0.9302197099 0.9265081286 + 0.9363918900 0.9305335879 0.9268637896 + 0.9366858006 0.9308475256 0.9272195101 + 0.9369795918 0.9311614037 0.9275751710 + 0.9372735023 0.9314752817 0.9279308915 + 0.9375674129 0.9317892194 0.9282866120 + 0.9378613234 0.9321032166 0.9286423922 + 0.9381551147 0.9324170947 0.9289981127 + 0.9384490252 0.9327309728 0.9293537736 + 0.9387428761 0.9330449104 0.9297094941 + 0.9390367866 0.9333587885 0.9300611019 + 0.9393305779 0.9336727262 0.9303944111 + 0.9396244884 0.9339867234 0.9307277203 + 0.9399183989 0.9343006015 0.9310610294 + 0.9401996732 0.9346144795 0.9313942790 + 0.9404761791 0.9349284172 0.9317277074 + 0.9407526851 0.9352422953 0.9320610166 + 0.9410290718 0.9355561733 0.9323943257 + 0.9413055778 0.9358701706 0.9327275753 + 0.9415820837 0.9361841083 0.9330608845 + 0.9418585896 0.9364979863 0.9333941936 + 0.9421349764 0.9368119240 0.9337275028 + 0.9424114823 0.9371258020 0.9340608120 + 0.9426879883 0.9374397993 0.9343941808 + 0.9429644942 0.9377536774 0.9347274899 + 0.9432408810 0.9380676150 0.9350607991 + 0.9435173869 0.9383814931 0.9353941083 + 0.9437938929 0.9386953712 0.9357274175 + 0.9440703988 0.9390093088 0.9360607266 + 0.9443467855 0.9393233061 0.9363939762 + 0.9446232915 0.9396371841 0.9367272854 + 0.9448997974 0.9399511218 0.9370607138 + 0.9451763034 0.9402499199 0.9373940229 + 0.9454526901 0.9405459166 0.9377272725 + 0.9457291961 0.9408419132 0.9380605817 + 0.9460057020 0.9411379099 0.9383938909 + 0.9462822080 0.9414339066 0.9387272000 + 0.9465587139 0.9417299032 0.9390605092 + 0.9468351007 0.9420258999 0.9393938780 + 0.9471116066 0.9423218966 0.9397271872 + 0.9473881125 0.9426178932 0.9400566816 + 0.9476646185 0.9429138899 0.9403690100 + 0.9479410052 0.9432098269 0.9406812787 + 0.9482175112 0.9435058236 0.9409937263 + 0.9484940171 0.9438018203 0.9413059950 + 0.9487705231 0.9440978169 0.9416183233 + 0.9490469098 0.9443938136 0.9419305921 + 0.9493234158 0.9446898103 0.9422429800 + 0.9495999217 0.9449858069 0.9425553083 + 0.9498764277 0.9452818036 0.9428675771 + 0.9501438141 0.9455778003 0.9431800246 + 0.9504039884 0.9458737969 0.9434922934 + 0.9506642222 0.9461697936 0.9438046217 + 0.9509242773 0.9464657903 0.9441168904 + 0.9511845112 0.9467617869 0.9444292784 + 0.9514446855 0.9470577836 0.9447416067 + 0.9517048001 0.9473537803 0.9450538754 + 0.9519649744 0.9476497769 0.9453663230 + 0.9522252083 0.9479457736 0.9456785917 + 0.9524853230 0.9482417703 0.9459909201 + 0.9527454972 0.9485378265 0.9463031888 + 0.9530056715 0.9488338232 0.9466155767 + 0.9532657862 0.9491298199 0.9469279051 + 0.9535260201 0.9494258165 0.9472401738 + 0.9537861943 0.9497218132 0.9475526214 + 0.9540463090 0.9500167966 0.9478648901 + 0.9543064833 0.9502959251 0.9481772184 + 0.9545667171 0.9505749941 0.9484894872 + 0.9548268914 0.9508541226 0.9488018751 + 0.9550870061 0.9511331916 0.9491142035 + 0.9553471804 0.9514123201 0.9494264722 + 0.9556074142 0.9516913891 0.9497389197 + 0.9558675289 0.9519705176 0.9500480294 + 0.9561277032 0.9522495866 0.9503406882 + 0.9563878775 0.9525287747 0.9506334066 + 0.9566479921 0.9528079033 0.9509260058 + 0.9569082260 0.9530869722 0.9512187243 + 0.9571684003 0.9533661008 0.9515113831 + 0.9574285150 0.9536452293 0.9518041015 + 0.9576886892 0.9539242983 0.9520968199 + 0.9579489231 0.9542034268 0.9523894787 + 0.9582089782 0.9544824958 0.9526821971 + 0.9584692121 0.9547616243 0.9529749155 + 0.9587293863 0.9550406933 0.9532675743 + 0.9589895010 0.9553198814 0.9535602927 + 0.9592496753 0.9555990100 0.9538530111 + 0.9595099092 0.9558780789 0.9541457295 + 0.9597700238 0.9561572075 0.9544383883 + 0.9600284100 0.9564362764 0.9547309875 + 0.9602733254 0.9567154050 0.9550237060 + 0.9605181813 0.9569944739 0.9553164244 + 0.9607630968 0.9572736025 0.9556090832 + 0.9610080123 0.9575526714 0.9559018016 + 0.9612529278 0.9578318000 0.9561945200 + 0.9614977837 0.9581109881 0.9564871788 + 0.9617425799 0.9583901167 0.9567798972 + 0.9619874954 0.9586691856 0.9570726156 + 0.9622324109 0.9589483142 0.9573652744 + 0.9624773264 0.9592273831 0.9576579928 + 0.9627221823 0.9595065117 0.9579507113 + 0.9629670978 0.9597855806 0.9582434297 + 0.9632120132 0.9600610137 0.9585360885 + 0.9634569287 0.9603242278 0.9588286877 + 0.9637017846 0.9605875015 0.9591214061 + 0.9639465809 0.9608507156 0.9594141245 + 0.9641914964 0.9611139297 0.9597067833 + 0.9644364119 0.9613770843 0.9599995017 + 0.9646813273 0.9616402984 0.9602739215 + 0.9649261832 0.9619035125 0.9605482221 + 0.9651710987 0.9621667266 0.9608225226 + 0.9654160142 0.9624298811 0.9610968828 + 0.9656609297 0.9626930952 0.9613711834 + 0.9659057260 0.9629563093 0.9616456032 + 0.9661505818 0.9632195830 0.9619199038 + 0.9663954973 0.9634827971 0.9621942043 + 0.9666404128 0.9637460113 0.9624686241 + 0.9668853283 0.9640092254 0.9627429247 + 0.9671301842 0.9642723799 0.9630172253 + 0.9673750997 0.9645355940 0.9632915854 + 0.9676200151 0.9647988081 0.9635658860 + 0.9678648114 0.9650620222 0.9638401866 + 0.9681097269 0.9653251767 0.9641146064 + 0.9683545828 0.9655883908 0.9643889070 + 0.9685994983 0.9658517241 0.9646632075 + 0.9688444138 0.9661148787 0.9649376273 + 0.9690893292 0.9663780928 0.9652119279 + 0.9693341851 0.9666413069 0.9654862285 + 0.9695791006 0.9669045210 0.9657605886 + 0.9698240161 0.9671676755 0.9660348892 + 0.9700648189 0.9674308896 0.9663093090 + 0.9702954292 0.9676941037 0.9665836096 + 0.9705259800 0.9679573178 0.9668579102 + 0.9707565904 0.9682204723 0.9671322703 + 0.9709870815 0.9684838057 0.9674065709 + 0.9712176919 0.9687470198 0.9676808715 + 0.9714483023 0.9690101743 0.9679552913 + 0.9716789126 0.9692733884 0.9682295918 + 0.9719094038 0.9695366025 0.9685038924 + 0.9721400142 0.9697998166 0.9687783122 + 0.9723706245 0.9700593948 0.9690526128 + 0.9726011753 0.9703077078 0.9693269134 + 0.9728317857 0.9705559015 0.9696012735 + 0.9730622768 0.9708042145 0.9698755741 + 0.9732928872 0.9710524082 0.9701405764 + 0.9735234976 0.9713007212 0.9703977704 + 0.9737541080 0.9715489149 0.9706549048 + 0.9739845991 0.9717972279 0.9709120989 + 0.9742152095 0.9720454216 0.9711692929 + 0.9744458199 0.9722936749 0.9714264870 + 0.9746763706 0.9725419283 0.9716836810 + 0.9749069810 0.9727901816 0.9719408751 + 0.9751374722 0.9730383754 0.9721981287 + 0.9753680825 0.9732866883 0.9724552035 + 0.9755986929 0.9735348821 0.9727123976 + 0.9758293033 0.9737831950 0.9729695916 + 0.9760597944 0.9740313888 0.9732267857 + 0.9762904048 0.9742797017 0.9734839797 + 0.9765210152 0.9745278955 0.9737411737 + 0.9767516255 0.9747762084 0.9739984274 + 0.9769821763 0.9750244021 0.9742556214 + 0.9772127271 0.9752727151 0.9745126963 + 0.9774432778 0.9755209088 0.9747698903 + 0.9776738882 0.9757692218 0.9750270844 + 0.9779044986 0.9760174155 0.9752842784 + 0.9781349897 0.9762657285 0.9755414724 + 0.9783656001 0.9765139222 0.9757987261 + 0.9785962105 0.9767621756 0.9760559201 + 0.9788268209 0.9770104885 0.9763129950 + 0.9790573716 0.9772586823 0.9765701890 + 0.9792879224 0.9775069952 0.9768273830 + 0.9795184731 0.9777551889 0.9770845771 + 0.9797490835 0.9780035019 0.9773417711 + 0.9799796939 0.9782516956 0.9775990248 + 0.9801980257 0.9785000086 0.9778562188 + 0.9804152250 0.9787482023 0.9781134129 + 0.9806324244 0.9789965153 0.9783704877 + 0.9808496237 0.9792447090 0.9786276817 + 0.9810667038 0.9794930220 0.9788848758 + 0.9812839031 0.9797412157 0.9791421294 + 0.9815011024 0.9799895287 0.9793993235 + 0.9817183018 0.9802241921 0.9796565175 + 0.9819355011 0.9804583788 0.9799137115 + 0.9821525812 0.9806926250 0.9801601768 + 0.9823697805 0.9809268117 0.9804013968 + 0.9825869799 0.9811609983 0.9806426167 + 0.9828041792 0.9813951850 0.9808837175 + 0.9830213785 0.9816293120 0.9811248779 + 0.9832385182 0.9818634987 0.9813660979 + 0.9834557176 0.9820976853 0.9816073179 + 0.9836729169 0.9823318720 0.9818484783 + 0.9838901162 0.9825661182 0.9820895791 + 0.9841073155 0.9828003049 0.9823307991 + 0.9843243957 0.9830343723 0.9825720191 + 0.9845415950 0.9832686186 0.9828131795 + 0.9847587943 0.9835028052 0.9830542803 + 0.9849759936 0.9837369919 0.9832955003 + 0.9851931930 0.9839711785 0.9835367203 + 0.9854102731 0.9842054248 0.9837778807 + 0.9856274724 0.9844396114 0.9840189815 + 0.9858446717 0.9846736789 0.9842602015 + 0.9860618711 0.9849079251 0.9845014215 + 0.9862790108 0.9851421118 0.9847425818 + 0.9864962101 0.9853762984 0.9849838018 + 0.9867134094 0.9856104851 0.9852249026 + 0.9869306087 0.9858446717 0.9854661226 + 0.9871478081 0.9860787988 0.9857072830 + 0.9873648882 0.9863129854 0.9859485030 + 0.9875820875 0.9865471721 0.9861896038 + 0.9877992868 0.9867814183 0.9864308238 + 0.9880164862 0.9870156050 0.9866719842 + 0.9882336855 0.9872497916 0.9869132042 + 0.9884508252 0.9874839783 0.9871544242 + 0.9886680245 0.9877181053 0.9873955250 + 0.9888852239 0.9879522920 0.9876366854 + 0.9891024232 0.9881864786 0.9878779054 + 0.9893196225 0.9884207249 0.9881191254 + 0.9895367026 0.9886549115 0.9883602262 + 0.9897539020 0.9888890982 0.9886013865 + 0.9899711013 0.9891232252 0.9888426065 + 0.9901773930 0.9893574119 0.9890838265 + 0.9903820157 0.9895915985 0.9893249273 + 0.9905866981 0.9898257852 0.9895660877 + 0.9907913208 0.9900565743 0.9898073077 + 0.9909960032 0.9902775884 0.9900454879 + 0.9912006259 0.9904984832 0.9902716875 + 0.9914051890 0.9907194972 0.9904980063 + 0.9916098714 0.9909405112 0.9907242060 + 0.9918144941 0.9911614060 0.9909504056 + 0.9920191169 0.9913824201 0.9911767244 + 0.9922237992 0.9916033149 0.9914029241 + 0.9924284220 0.9918243289 0.9916291833 + 0.9926331043 0.9920452833 0.9918553829 + 0.9928377271 0.9922661781 0.9920815825 + 0.9930422902 0.9924871922 0.9923079014 + 0.9932469726 0.9927082062 0.9925341010 + 0.9934515953 0.9929291010 0.9927603006 + 0.9936562181 0.9931501150 0.9929866195 + 0.9938609004 0.9933711290 0.9932128191 + 0.9940655231 0.9935920238 0.9934390783 + 0.9942702055 0.9938129783 0.9936652780 + 0.9944748282 0.9940339923 0.9938914776 + 0.9946793914 0.9942548871 0.9941177964 + 0.9948840737 0.9944759011 0.9943439960 + 0.9950886965 0.9946967959 0.9945703149 + 0.9952933192 0.9949178100 0.9947965145 + 0.9954980016 0.9951388240 0.9950227141 + 0.9957026243 0.9953597188 0.9952489734 + 0.9959073067 0.9955806732 0.9954751730 + 0.9961119294 0.9958016872 0.9957014918 + 0.9963164926 0.9960225821 0.9959276915 + 0.9965211749 0.9962435961 0.9961538911 + 0.9967257977 0.9964646101 0.9963802099 + 0.9969304204 0.9966855049 0.9966064095 + 0.9971351027 0.9969065189 0.9968327284 + 0.9973397255 0.9971274734 0.9970589280 + 0.9975444078 0.9973484278 0.9972851276 + 0.9977489710 0.9975693822 0.9975113869 + 0.9979535937 0.9977903962 0.9977375865 + 0.9981582761 0.9980112910 0.9979637861 + 0.9983628988 0.9982323050 0.9981901050 + 0.9985675216 0.9984531999 0.9984163046 + 0.9987722039 0.9986742139 0.9986426234 + 0.9989768267 0.9988952279 0.9988688231 + 0.9991815090 0.9991161227 0.9990950227 + 0.9993860722 0.9993370771 0.9993212819 + 0.9995906949 0.9995580912 0.9995474815 + 0.9997953773 0.9997789860 0.9997738004 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_100.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_100.spi1d index f78872fa..548123e4 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_100.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_100.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0004029108 0.0002821621 0.0001828344 - 0.0008352380 0.0005855577 0.0003765924 - 0.0012884660 0.0009102127 0.0005832637 - 0.0017661640 0.0012608080 0.0008021335 - 0.0022727679 0.0016344450 0.0010323920 - 0.0028016400 0.0020279640 0.0012744220 - 0.0033602209 0.0024469071 0.0015256481 - 0.0039473702 0.0028940800 0.0017888939 - 0.0045624170 0.0033709989 0.0020655000 - 0.0052064499 0.0038755860 0.0023565269 - 0.0058913450 0.0044098911 0.0026614380 - 0.0066106170 0.0049738358 0.0029808299 - 0.0073703690 0.0055717099 0.0033126650 - 0.0081646843 0.0062076761 0.0036578199 - 0.0090046516 0.0068754591 0.0040169759 - 0.0098797111 0.0075756288 0.0043923021 - 0.0108015500 0.0083136391 0.0047826152 - 0.0117765898 0.0090896897 0.0051890090 - 0.0128089404 0.0099051362 0.0056110788 - 0.0138962502 0.0107623404 0.0060491539 - 0.0150366900 0.0116626900 0.0065042591 - 0.0162367094 0.0126068601 0.0069758608 - 0.0174948201 0.0135949403 0.0074637011 - 0.0188118201 0.0146293398 0.0079666628 - 0.0201881006 0.0157077797 0.0084832394 - 0.0216275994 0.0168280900 0.0090179667 - 0.0231346209 0.0179986991 0.0095704449 - 0.0247129407 0.0192209203 0.0101442495 - 0.0263695195 0.0204982292 0.0107389204 - 0.0280960202 0.0218272507 0.0113510201 - 0.0298976302 0.0232012607 0.0119804395 - 0.0317782089 0.0246255603 0.0126269804 - 0.0337512381 0.0260962192 0.0132909399 - 0.0358026214 0.0276108794 0.0139709497 - 0.0379312597 0.0291825198 0.0146665303 - 0.0401499309 0.0308093708 0.0153818596 - 0.0424532183 0.0324951895 0.0161184706 - 0.0448440388 0.0342448987 0.0168759096 - 0.0473214611 0.0360610783 0.0176607408 - 0.0498819388 0.0379341505 0.0184673797 - 0.0525423586 0.0398601405 0.0192978401 - 0.0553006493 0.0418499894 0.0201489609 - 0.0581430085 0.0438929684 0.0210218206 - 0.0610603690 0.0459916182 0.0219117906 - 0.0640591532 0.0481535792 0.0228250604 - 0.0671426877 0.0503738187 0.0237576701 - 0.0703096837 0.0526381992 0.0247094091 - 0.0735653490 0.0549571402 0.0256804898 - 0.0769181773 0.0573286191 0.0266711209 - 0.0803592205 0.0597677194 0.0276827496 - 0.0838969871 0.0622712001 0.0287114196 - 0.0875223130 0.0648275465 0.0297578406 - 0.0912306681 0.0674386173 0.0308247507 - 0.0950332806 0.0701159835 0.0319116488 - 0.0989207476 0.0728433207 0.0330191404 - 0.1029006988 0.0756310001 0.0341475010 - 0.1069691032 0.0784748793 0.0352968313 - 0.1111252010 0.0813758895 0.0364708304 - 0.1153606027 0.0843334123 0.0376655087 - 0.1196606010 0.0873407498 0.0388817117 - 0.1240456998 0.0903989002 0.0401183404 - 0.1285089999 0.0934979469 0.0413741618 - 0.1330395043 0.0966412276 0.0426500700 - 0.1376578957 0.0998382494 0.0439491197 - 0.1423473954 0.1030810028 0.0452704318 - 0.1471080929 0.1063693017 0.0466123708 - 0.1519421935 0.1097078994 0.0479735583 - 0.1568460017 0.1131009012 0.0493564717 - 0.1618086994 0.1165459007 0.0507622100 - 0.1668350995 0.1200487018 0.0521893688 - 0.1719325930 0.1235952005 0.0536371209 - 0.1770922989 0.1271920949 0.0551049188 - 0.1823212057 0.1308318973 0.0565952100 - 0.1875973940 0.1345200986 0.0581106283 - 0.1929260045 0.1382502019 0.0596516207 - 0.1983052045 0.1420248002 0.0612149313 - 0.2037338018 0.1458451003 0.0627992302 - 0.2092058957 0.1496911049 0.0644068494 - 0.2147053927 0.1535611004 0.0660389215 - 0.2202475965 0.1574510038 0.0676914304 - 0.2258259058 0.1613723040 0.0693570822 - 0.2314376980 0.1653227061 0.0710399523 - 0.2370904982 0.1692909002 0.0727412999 - 0.2427740991 0.1732849032 0.0744594336 - 0.2484734952 0.1773027033 0.0761927664 - 0.2541978061 0.1813420951 0.0779421404 - 0.2599338889 0.1854172051 0.0797097385 - 0.2656922042 0.1895329952 0.0814976320 - 0.2714639008 0.1936807036 0.0833064169 - 0.2772484124 0.1978671998 0.0851336494 - 0.2830584943 0.2020945996 0.0869752765 - 0.2888925076 0.2063394040 0.0888287425 - 0.2947472036 0.2105993927 0.0906959027 - 0.3006064892 0.2148775011 0.0925794691 - 0.3064776063 0.2191728055 0.0944826677 - 0.3123730123 0.2234859020 0.0964089409 - 0.3182818890 0.2278206050 0.0983487070 - 0.3241820931 0.2321731001 0.1003040969 - 0.3300934136 0.2365338057 0.1022775024 - 0.3360261023 0.2409182042 0.1042753011 - 0.3419634998 0.2453251928 0.1062982008 - 0.3478932977 0.2497422993 0.1083303019 - 0.3538097143 0.2541579902 0.1103701964 - 0.3597145975 0.2585824132 0.1124206036 - 0.3656162918 0.2630195916 0.1144891009 - 0.3715220988 0.2674663961 0.1165636033 - 0.3774074912 0.2719157040 0.1186439022 - 0.3832853138 0.2763611078 0.1207353026 - 0.3891583085 0.2808066010 0.1228388026 - 0.3950240016 0.2852618098 0.1249521971 - 0.4008567035 0.2897278070 0.1270755976 - 0.4066801071 0.2942065001 0.1292198002 - 0.4124872983 0.2987079918 0.1313749999 - 0.4182811975 0.3031933904 0.1335413009 - 0.4240790904 0.3076609075 0.1357281953 - 0.4298571944 0.3121108115 0.1379179060 - 0.4356110096 0.3165585101 0.1401115060 - 0.4413394928 0.3209869862 0.1423158050 - 0.4470395148 0.3254047930 0.1445332021 - 0.4527026117 0.3298167884 0.1467590034 - 0.4583334923 0.3342174888 0.1489966065 - 0.4639326930 0.3386009932 0.1512416005 - 0.4695220888 0.3429653049 0.1534925997 - 0.4750846922 0.3473365903 0.1557577997 - 0.4806160927 0.3517040908 0.1580404937 - 0.4860872924 0.3560670912 0.1603357941 - 0.4915176928 0.3604130149 0.1626514047 - 0.4969187081 0.3647586107 0.1649708003 - 0.5022816062 0.3691031039 0.1672956944 - 0.5076225996 0.3734341860 0.1696293950 - 0.5129231811 0.3777643144 0.1719669998 - 0.5181546807 0.3820936084 0.1743122041 - 0.5233271718 0.3864077926 0.1766573936 - 0.5284634233 0.3907110095 0.1790025979 - 0.5335465074 0.3950001001 0.1813486069 - 0.5385751724 0.3992711008 0.1836972982 - 0.5435780883 0.4035327137 0.1860560030 - 0.5485448241 0.4077767134 0.1884246022 - 0.5534678102 0.4120056033 0.1908023953 - 0.5583646297 0.4162251055 0.1931803972 - 0.5632166862 0.4203993082 0.1955589950 - 0.5680477023 0.4245612025 0.1979466975 - 0.5728545189 0.4287038147 0.2003419995 - 0.5775995255 0.4328233004 0.2027534992 - 0.5823162794 0.4369328022 0.2051690072 - 0.5869674087 0.4410141110 0.2075875998 - 0.5915768743 0.4450846910 0.2100103050 - 0.5961518884 0.4491345882 0.2124343067 - 0.6006463170 0.4531683922 0.2148666978 - 0.6051229239 0.4571923018 0.2172998041 - 0.6095721126 0.4611825943 0.2197346985 - 0.6139861941 0.4651634991 0.2221716940 - 0.6183604002 0.4691112041 0.2246143073 - 0.6226823926 0.4730412066 0.2270615995 - 0.6269859076 0.4769391119 0.2295158058 - 0.6312270761 0.4808132946 0.2319785058 - 0.6354541779 0.4846689999 0.2344491929 - 0.6396340728 0.4885058105 0.2369215935 - 0.6438084245 0.4923363924 0.2393949032 - 0.6479752064 0.4961437881 0.2418684959 - 0.6520851254 0.4999468923 0.2443421036 - 0.6561610103 0.5037171841 0.2468205988 - 0.6601614952 0.5074794292 0.2493008971 - 0.6641432047 0.5112071037 0.2517744899 - 0.6680347919 0.5149219036 0.2542459071 - 0.6719154716 0.5186061263 0.2567091882 - 0.6757277846 0.5222734809 0.2591697872 - 0.6795322895 0.5259137750 0.2616206110 - 0.6833059192 0.5295230746 0.2640681863 - 0.6870679259 0.5330942273 0.2665097117 - 0.6908019781 0.5366339087 0.2689489126 - 0.6945022941 0.5401414037 0.2713932097 - 0.6981444955 0.5436180830 0.2738403976 - 0.7017527223 0.5470659733 0.2762868106 - 0.7053136826 0.5504943728 0.2787325978 - 0.7088435292 0.5539047718 0.2811723053 - 0.7123348117 0.5572900772 0.2836047113 - 0.7157995105 0.5606504083 0.2860364020 - 0.7192326784 0.5639901757 0.2884666920 - 0.7226405144 0.5673059225 0.2908954918 - 0.7260167003 0.5705996752 0.2933177948 - 0.7293487191 0.5738595724 0.2957400084 - 0.7326173782 0.5771085024 0.2981606126 - 0.7358595133 0.5803325772 0.3005810976 - 0.7390533090 0.5835512877 0.3029994965 - 0.7422223091 0.5867486000 0.3054173887 - 0.7453266978 0.5899420977 0.3078261018 - 0.7484182715 0.5930835009 0.3102284968 - 0.7514507174 0.5962249041 0.3126305044 - 0.7544791102 0.5993114114 0.3150317967 - 0.7574347258 0.6023926139 0.3174335063 - 0.7603902817 0.6054285169 0.3198406994 - 0.7632429004 0.6084480286 0.3222478032 - 0.7660846710 0.6114342213 0.3246580958 - 0.7688931227 0.6143910885 0.3270691931 - 0.7716894150 0.6173362732 0.3294793069 - 0.7744498253 0.6202554703 0.3318881989 - 0.7771797776 0.6231728196 0.3342961073 - 0.7798776031 0.6260603070 0.3366977870 - 0.7825078964 0.6289477944 0.3390995860 - 0.7851305008 0.6318117976 0.3414917886 - 0.7876675725 0.6346691847 0.3438811004 - 0.7902047038 0.6375092864 0.3462617099 - 0.7926716805 0.6403307915 0.3486273885 - 0.7951231003 0.6431398988 0.3509930074 - 0.7975525260 0.6458842754 0.3533450067 - 0.7999621034 0.6486287117 0.3556961119 - 0.8023657799 0.6513149738 0.3580406904 - 0.8047469854 0.6539878845 0.3603785932 - 0.8071283102 0.6566321254 0.3627164066 - 0.8094397187 0.6592395902 0.3650526106 - 0.8117408156 0.6618456841 0.3673886955 - 0.8139895797 0.6644176841 0.3697170913 - 0.8161870241 0.6669896841 0.3720383942 - 0.8183761239 0.6695259213 0.3743596971 - 0.8205000162 0.6720392704 0.3766688108 - 0.8226239085 0.6745491028 0.3789778948 - 0.8247258067 0.6770399213 0.3812792897 - 0.8268182278 0.6795307994 0.3835709095 - 0.8289020061 0.6819990277 0.3858624995 - 0.8309609294 0.6844567060 0.3881478012 - 0.8330196738 0.6869028211 0.3904320002 - 0.8350235224 0.6892951131 0.3927091956 - 0.8370118737 0.6916874051 0.3949654102 - 0.8389791846 0.6940457821 0.3972215950 - 0.8408939242 0.6963855028 0.3994632065 - 0.8428086042 0.6987230182 0.4016954005 - 0.8446682096 0.7010375261 0.4039277136 - 0.8465105295 0.7033519149 0.4061439037 - 0.8483420014 0.7056419253 0.4083589911 - 0.8501332998 0.7079067230 0.4105660021 - 0.8519244790 0.7101715803 0.4127483070 - 0.8536859751 0.7123854160 0.4149304926 - 0.8554301858 0.7145920992 0.4171046913 - 0.8571730852 0.7167863250 0.4192717075 - 0.8588685989 0.7189391255 0.4214386046 - 0.8605639935 0.7210919857 0.4236004949 - 0.8622372150 0.7232128978 0.4257610142 - 0.8638756275 0.7253087163 0.4279214144 - 0.8655139804 0.7274044156 0.4300718904 - 0.8671008945 0.7294499874 0.4322223067 - 0.8686684966 0.7314872742 0.4343703091 - 0.8702337742 0.7335180044 0.4365068972 - 0.8717327714 0.7355036736 0.4386434853 - 0.8732318878 0.7374892831 0.4407753050 - 0.8747140765 0.7394585013 0.4428966939 - 0.8761591911 0.7413989902 0.4450182021 - 0.8776043057 0.7433394194 0.4471280873 - 0.8790065050 0.7452735901 0.4492236078 - 0.8803750873 0.7472031116 0.4513190985 - 0.8817436099 0.7491325736 0.4533992112 - 0.8830839992 0.7510256767 0.4554660916 - 0.8844164014 0.7529067993 0.4575330019 - 0.8857486844 0.7547879815 0.4595870972 - 0.8870559931 0.7566236854 0.4616327882 - 0.8883624077 0.7584543824 0.4636783898 - 0.8896635771 0.7602844834 0.4657067060 - 0.8909289837 0.7620761991 0.4677248001 - 0.8921945095 0.7638679147 0.4697428942 - 0.8934394121 0.7656568289 0.4717616141 - 0.8946236968 0.7674204707 0.4737805128 - 0.8958079815 0.7691841722 0.4757995009 - 0.8969696164 0.7709385753 0.4778141081 - 0.8980888724 0.7726433873 0.4798257947 - 0.8992081881 0.7743480802 0.4818373919 - 0.9003195167 0.7760472894 0.4838384986 - 0.9014195800 0.7777205706 0.4858298898 - 0.9025197029 0.7793939114 0.4878214002 - 0.9035978913 0.7810617089 0.4898056984 - 0.9046490788 0.7827023864 0.4917800128 - 0.9057002068 0.7843430042 0.4937543869 - 0.9067236185 0.7859767079 0.4957253933 - 0.9077128172 0.7875623107 0.4976881146 - 0.9087020159 0.7891479731 0.4996508062 - 0.9096745849 0.7907313108 0.5016111732 - 0.9106239080 0.7922711968 0.5035545826 - 0.9115731716 0.7938110232 0.5054979920 - 0.9124993086 0.7953509092 0.5074414015 - 0.9133828282 0.7968598008 0.5093576908 - 0.9142662287 0.7983667254 0.5112711787 - 0.9151422977 0.7998737097 0.5131847262 - 0.9159966707 0.8013570905 0.5150825977 - 0.9168511033 0.8028343916 0.5169728994 - 0.9176996946 0.8043118119 0.5188632011 - 0.9185072780 0.8057656884 0.5207424760 - 0.9193148017 0.8072052002 0.5226051807 - 0.9201223254 0.8086445928 0.5244678855 - 0.9209262133 0.8100745082 0.5263286233 - 0.9217298031 0.8114904165 0.5281689167 - 0.9225333929 0.8129063249 0.5300090909 - 0.9233224988 0.8143162727 0.5318493843 - 0.9241070151 0.8156955242 0.5336812139 - 0.9248914719 0.8170747161 0.5355101228 - 0.9256622195 0.8184539080 0.5373389721 - 0.9264205098 0.8198124170 0.5391626954 - 0.9271788001 0.8211680055 0.5409774184 - 0.9279240966 0.8225237131 0.5427920222 - 0.9286308885 0.8238555193 0.5446066260 - 0.9293376803 0.8251689076 0.5463947058 - 0.9300444722 0.8264822960 0.5481811166 - 0.9307246208 0.8277850151 0.5499675870 - 0.9314032793 0.8290479183 0.5517386198 - 0.9320818782 0.8303108215 0.5534932017 - 0.9327319860 0.8315737247 0.5552479029 - 0.9333636761 0.8327975273 0.5570024848 - 0.9339953065 0.8340129256 0.5587229133 - 0.9346169233 0.8352283239 0.5604429841 - 0.9352049828 0.8364298940 0.5621631742 - 0.9357931018 0.8376104832 0.5638735890 - 0.9363812208 0.8387910128 0.5655727983 - 0.9369478822 0.8399716020 0.5672720075 - 0.9375097752 0.8411086798 0.5689712167 - 0.9380717874 0.8422427773 0.5706508160 - 0.9386349916 0.8433768749 0.5723282099 - 0.9392004013 0.8445004821 0.5740054846 - 0.9397658110 0.8456103802 0.5756754279 - 0.9403312206 0.8467202783 0.5773270726 - 0.9408558011 0.8478301764 0.5789787173 - 0.9413745999 0.8489074111 0.5806304216 - 0.9418935180 0.8499805927 0.5822653174 - 0.9423980713 0.8510538936 0.5838909149 - 0.9428763986 0.8521230221 0.5855165124 - 0.9433547258 0.8531830907 0.5871421099 - 0.9438331127 0.8542432189 0.5887404084 - 0.9443081021 0.8553032875 0.5903375149 - 0.9447823763 0.8563414812 0.5919346213 - 0.9452565908 0.8573703170 0.5935288072 - 0.9457269907 0.8583989739 0.5951129794 - 0.9461808801 0.8594260216 0.5966972113 - 0.9466348290 0.8604149818 0.5982813835 - 0.9470885992 0.8614040017 0.5998545289 - 0.9475203156 0.8623930812 0.6014149189 - 0.9479331970 0.8633654118 0.6029751897 - 0.9483461976 0.8643069267 0.6045355797 - 0.9487591982 0.8652483821 0.6060755253 - 0.9491466284 0.8661898971 0.6076046824 - 0.9495298862 0.8671160936 0.6091338992 - 0.9499133229 0.8680329919 0.6106631756 - 0.9502913952 0.8689497709 0.6121817827 - 0.9506375790 0.8698666096 0.6136978269 - 0.9509837031 0.8707538843 0.6152138710 - 0.9513298869 0.8716354966 0.6167299747 - 0.9516645074 0.8725172281 0.6182156801 - 0.9519801736 0.8733978868 0.6196988225 - 0.9522958994 0.8742434978 0.6211817861 - 0.9526115060 0.8750889897 0.6226648092 - 0.9529042244 0.8759344816 0.6241227984 - 0.9531800151 0.8767787218 0.6255803704 - 0.9534558058 0.8776161075 0.6270378828 - 0.9537314773 0.8784536123 0.6284952760 - 0.9539870024 0.8792909980 0.6299347878 - 0.9542350173 0.8801239729 0.6313741803 - 0.9544829726 0.8809452057 0.6328135729 - 0.9547309875 0.8817663789 0.6342530847 - 0.9549531937 0.8825874925 0.6356778741 - 0.9551706910 0.8833963275 0.6371027231 - 0.9553881884 0.8841794729 0.6385273933 - 0.9556056857 0.8849627972 0.6399521232 - 0.9558088183 0.8857461214 0.6413484812 - 0.9560105801 0.8865262866 0.6427426934 - 0.9562124014 0.8873007894 0.6441369057 - 0.9564142227 0.8880752921 0.6455309987 - 0.9566016793 0.8888496757 0.6469084024 - 0.9567884803 0.8896216154 0.6482821703 - 0.9569752812 0.8903880119 0.6496559978 - 0.9571620822 0.8911542892 0.6510298252 - 0.9573429823 0.8919206858 0.6523879170 - 0.9575235844 0.8926829100 0.6537382007 - 0.9577041864 0.8934323192 0.6550884843 - 0.9578847289 0.8941817284 0.6564388871 - 0.9580754042 0.8949311972 0.6577758193 - 0.9582675099 0.8956754208 0.6590979099 - 0.9584596157 0.8963845968 0.6604200006 - 0.9586517215 0.8970937729 0.6617420912 - 0.9588462114 0.8978030086 0.6630570889 - 0.9590412974 0.8985121846 0.6643490791 - 0.9592363834 0.8991947770 0.6656410098 - 0.9594315290 0.8998764753 0.6669330001 - 0.9596217275 0.9005582929 0.6682248712 - 0.9598088861 0.9012399912 0.6695014238 - 0.9599962234 0.9018872976 0.6707772017 - 0.9601835012 0.9025238752 0.6720529199 - 0.9603782296 0.9031605124 0.6733285785 - 0.9605820775 0.9037970901 0.6746001244 - 0.9607859850 0.9044255018 0.6758689880 - 0.9609898925 0.9050459862 0.6771379709 - 0.9611927271 0.9056665897 0.6784068942 - 0.9613916874 0.9062871933 0.6796714067 - 0.9615907073 0.9069045186 0.6809216142 - 0.9617896080 0.9075098038 0.6821717024 - 0.9619886279 0.9081152081 0.6834219098 - 0.9621633887 0.9087206125 0.6846719980 - 0.9623364210 0.9093260169 0.6859096885 - 0.9625093937 0.9099286199 0.6871445179 - 0.9626824260 0.9105308056 0.6883792281 - 0.9628574252 0.9111331105 0.6896139979 - 0.9630337954 0.9117352962 0.6908426285 - 0.9632101059 0.9123305082 0.6920586824 - 0.9633864760 0.9129180908 0.6932747960 - 0.9635632038 0.9135056138 0.6944910288 - 0.9637411237 0.9140931964 0.6957070827 - 0.9639191031 0.9146800041 0.6969081163 - 0.9640970230 0.9152501225 0.6981055140 - 0.9642748833 0.9158201814 0.6993029118 - 0.9644508958 0.9163902998 0.7005001903 - 0.9646263719 0.9169602990 0.7016944289 - 0.9648019075 0.9175173044 0.7028782964 - 0.9649773836 0.9180638790 0.7040622234 - 0.9651508927 0.9186105132 0.7052459717 - 0.9653204083 0.9191570878 0.7064298987 - 0.9654898047 0.9197033048 0.7075976133 - 0.9656593204 0.9202299118 0.7087550759 - 0.9658287168 0.9207565784 0.7099125981 - 0.9659901857 0.9212831855 0.7110701203 - 0.9661499262 0.9218099117 0.7122275233 - 0.9663096070 0.9223287106 0.7133823037 - 0.9664692283 0.9228376746 0.7145369053 - 0.9666253924 0.9233465791 0.7156915069 - 0.9667711854 0.9238554835 0.7168461084 - 0.9669169188 0.9243643880 0.7179933786 - 0.9670627117 0.9248356819 0.7191138864 - 0.9672083855 0.9252979159 0.7202343941 - 0.9673423171 0.9257600904 0.7213549018 - 0.9674692750 0.9262223244 0.7224754095 - 0.9675962925 0.9266850948 0.7235804796 - 0.9677233100 0.9271525741 0.7246658802 - 0.9678503275 0.9276201725 0.7257512808 - 0.9679561853 0.9280877113 0.7268366814 - 0.9680612087 0.9285551906 0.7279220819 - 0.9681661725 0.9290199280 0.7289987803 - 0.9682713151 0.9294800162 0.7300698757 - 0.9683721066 0.9299402237 0.7311409712 - 0.9684600830 0.9304003716 0.7322121263 - 0.9685481191 0.9308605194 0.7332832217 - 0.9686360955 0.9313055873 0.7343558073 - 0.9687241912 0.9317405820 0.7354289889 - 0.9687963128 0.9321755767 0.7365021110 - 0.9688509703 0.9326105714 0.7375752926 - 0.9689058065 0.9330456257 0.7386484146 - 0.9689605236 0.9334610105 0.7396969199 - 0.9690153003 0.9338703752 0.7407392263 - 0.9690718055 0.9342797995 0.7417815924 - 0.9691293836 0.9346892238 0.7428238988 - 0.9691869020 0.9350985885 0.7438662052 - 0.9692444205 0.9354946017 0.7448874712 - 0.9693019986 0.9358887076 0.7459040284 - 0.9693595767 0.9362828135 0.7469205260 - 0.9694172740 0.9366768003 0.7479370236 - 0.9694749713 0.9370709062 0.7489535213 - 0.9695327282 0.9374542832 0.7499631047 - 0.9695904255 0.9378368855 0.7509707212 - 0.9696562290 0.9382194877 0.7519782782 - 0.9697234035 0.9386020899 0.7529858947 - 0.9697905779 0.9389846921 0.7539935112 - 0.9698578119 0.9393534064 0.7549921870 - 0.9699249864 0.9397209883 0.7559866905 - 0.9700003266 0.9400886297 0.7569811940 - 0.9700766802 0.9404562116 0.7579756975 - 0.9701529741 0.9408237934 0.7589702010 - 0.9702293873 0.9411832094 0.7599533200 - 0.9703058004 0.9415413737 0.7609264255 - 0.9703885913 0.9418995976 0.7618994713 - 0.9704725742 0.9422578216 0.7628725767 - 0.9705566168 0.9426159859 0.7638456225 - 0.9706404805 0.9429603815 0.7648131251 - 0.9707245231 0.9433007836 0.7657690048 - 0.9708133936 0.9436411858 0.7667248249 - 0.9709036946 0.9439815879 0.7676807046 - 0.9709941149 0.9443218708 0.7686365843 - 0.9710844755 0.9446520805 0.7695921063 - 0.9711748958 0.9449757934 0.7705423832 - 0.9712682962 0.9452995062 0.7714927793 - 0.9713637233 0.9456232190 0.7724431753 - 0.9714590907 0.9459468722 0.7733935118 - 0.9715543985 0.9462659955 0.7743439078 - 0.9716498256 0.9465780258 0.7752857804 - 0.9717406034 0.9468899965 0.7762247920 - 0.9718248844 0.9472020268 0.7771639228 - 0.9719092846 0.9475139976 0.7781028748 - 0.9719936848 0.9478241205 0.7790420055 - 0.9720780849 0.9481198788 0.7799775004 - 0.9721639752 0.9484158158 0.7809069157 - 0.9722574949 0.9487115741 0.7818363905 - 0.9723510146 0.9490075111 0.7827658057 - 0.9724445939 0.9493033886 0.7836952806 - 0.9725381136 0.9495918155 0.7846246958 - 0.9726316929 0.9498783946 0.7855424881 - 0.9727249742 0.9501649141 0.7864595056 - 0.9728183150 0.9504513741 0.7873764038 - 0.9729115963 0.9507378936 0.7882934213 - 0.9730048776 0.9510220885 0.7892103791 - 0.9730982184 0.9513028860 0.7901170850 - 0.9731950164 0.9515836239 0.7910107970 - 0.9732956290 0.9518643022 0.7919045091 - 0.9733961225 0.9521449804 0.7927982211 - 0.9734966755 0.9524257779 0.7936918736 - 0.9735972285 0.9526972175 0.7945855856 - 0.9736980200 0.9529681802 0.7954729199 - 0.9738026261 0.9532393217 0.7963594198 - 0.9739071131 0.9535102844 0.7972459197 - 0.9740117192 0.9537813067 0.7981324196 - 0.9741162062 0.9540500045 0.7990188003 - 0.9742208123 0.9543160200 0.7999022007 - 0.9743257165 0.9545819759 0.8007770777 - 0.9744309187 0.9548478723 0.8016518950 - 0.9745361209 0.9551138878 0.8025267720 - 0.9746413231 0.9553797841 0.8034017086 - 0.9747465253 0.9556449056 0.8042765856 - 0.9748516083 0.9559097886 0.8051419854 - 0.9749541283 0.9561746716 0.8060011268 - 0.9750565290 0.9564396739 0.8068600893 - 0.9751588702 0.9567046165 0.8077191710 - 0.9752613902 0.9569633007 0.8085781932 - 0.9753637910 0.9572036862 0.8094372749 - 0.9754638076 0.9574440718 0.8102779984 - 0.9755604267 0.9576845765 0.8111159205 - 0.9756569266 0.9579250216 0.8119537234 - 0.9757534266 0.9581654072 0.8127915263 - 0.9758499861 0.9584035873 0.8136293888 - 0.9759464860 0.9586400986 0.8144659996 - 0.9760364890 0.9588767290 0.8152853847 - 0.9761242867 0.9591131806 0.8161048293 - 0.9762120247 0.9593496919 0.8169242144 - 0.9762998223 0.9595862031 0.8177435994 - 0.9763875008 0.9598243833 0.8185629845 - 0.9764752984 0.9600629807 0.8193802834 - 0.9765614867 0.9603015184 0.8201889992 - 0.9766476154 0.9605401158 0.8209977150 - 0.9767338037 0.9607787132 0.8218063712 - 0.9768199921 0.9610170722 0.8226152062 - 0.9769061804 0.9612380862 0.8234239221 - 0.9769896269 0.9614589810 0.8242325187 - 0.9770637155 0.9616798759 0.8250408173 - 0.9771376848 0.9619007707 0.8258491158 - 0.9772117138 0.9621217847 0.8266574144 - 0.9772858024 0.9623394012 0.8274655938 - 0.9773597717 0.9625350237 0.8282738924 - 0.9774324298 0.9627305865 0.8290752172 - 0.9775022864 0.9629262090 0.8298593760 - 0.9775723219 0.9631217718 0.8306434751 - 0.9776422977 0.9633173943 0.8314276934 - 0.9777122736 0.9635074139 0.8322117925 - 0.9777823091 0.9636734724 0.8329960108 - 0.9778506756 0.9638395905 0.8337736726 - 0.9779173136 0.9640057087 0.8345307708 - 0.9779838920 0.9641718268 0.8352878094 - 0.9780504704 0.9643378854 0.8360449076 - 0.9781171083 0.9645028710 0.8368018866 - 0.9781836867 0.9646632075 0.8375589848 - 0.9782491922 0.9648234844 0.8383142948 - 0.9783133864 0.9649837017 0.8390561938 - 0.9783775806 0.9651439786 0.8397982121 - 0.9784417748 0.9653041959 0.8405401111 - 0.9785060287 0.9654663801 0.8412820101 - 0.9785702229 0.9656417966 0.8420239091 - 0.9786368012 0.9658172727 0.8427659273 - 0.9787064791 0.9659928083 0.8434938788 - 0.9787762761 0.9661682844 0.8442208171 - 0.9788460135 0.9663438201 0.8449476957 - 0.9789156914 0.9665191770 0.8456746936 - 0.9789854288 0.9667049050 0.8464015722 - 0.9790521264 0.9668906927 0.8471285105 - 0.9791120887 0.9670764804 0.8478465080 - 0.9791721106 0.9672623277 0.8485602736 - 0.9792320728 0.9674479961 0.8492740989 - 0.9792920947 0.9676337838 0.8499878049 - 0.9793521166 0.9678208232 0.8507015705 - 0.9794105291 0.9680082202 0.8514152765 - 0.9794589877 0.9681956172 0.8521232009 - 0.9795076251 0.9683828950 0.8528202772 - 0.9795562029 0.9685702920 0.8535174131 - 0.9796047807 0.9687576890 0.8542146087 - 0.9796532989 0.9689376950 0.8549116850 - 0.9797018766 0.9691110253 0.8556088805 - 0.9797381759 0.9692844152 0.8563060164 - 0.9797729850 0.9694576859 0.8569779992 - 0.9798077941 0.9696310163 0.8576492071 - 0.9798427224 0.9698044062 0.8583204150 - 0.9798774719 0.9699763060 0.8589916229 - 0.9799122810 0.9701421857 0.8596627712 - 0.9799376130 0.9703081250 0.8603339791 - 0.9799560905 0.9704741240 0.8609988093 - 0.9799745083 0.9706400037 0.8616573811 - 0.9799929857 0.9708058834 0.8623160124 - 0.9800114036 0.9709718227 0.8629745841 - 0.9800298810 0.9711270928 0.8636332154 - 0.9800478220 0.9712795019 0.8642917871 - 0.9800634980 0.9714319110 0.8649504185 - 0.9800791740 0.9715842009 0.8655930758 - 0.9800949097 0.9717366099 0.8662353754 - 0.9801105857 0.9718890190 0.8668777943 - 0.9801263213 0.9720391035 0.8675202131 - 0.9801419973 0.9721841812 0.8681625724 - 0.9801493883 0.9723293185 0.8688049912 - 0.9801540971 0.9724743962 0.8694401979 - 0.9801586866 0.9726194739 0.8700637817 - 0.9801633954 0.9727646112 0.8706874251 - 0.9801681042 0.9729096293 0.8713108897 - 0.9801728129 0.9730468988 0.8719344735 - 0.9801768064 0.9731817245 0.8725581169 - 0.9801785946 0.9733164907 0.8731817007 - 0.9801803827 0.9734513164 0.8737874031 - 0.9801821709 0.9735862017 0.8743858933 - 0.9801840186 0.9737210274 0.8749843836 - 0.9801859260 0.9738534093 0.8755828738 - 0.9801877141 0.9739726186 0.8761814237 - 0.9801887274 0.9740918279 0.8767799139 - 0.9801893234 0.9742109776 0.8773784041 - 0.9801899195 0.9743301272 0.8779736757 - 0.9801905155 0.9744492769 0.8785690069 - 0.9801911116 0.9745684862 0.8791642189 - 0.9801917076 0.9746857285 0.8797594905 - 0.9801923037 0.9748008251 0.8803547025 - 0.9801943898 0.9749158025 0.8809499741 - 0.9801964760 0.9750308990 0.8815435767 - 0.9801986814 0.9751458764 0.8821282983 - 0.9802008271 0.9752609134 0.8827130198 - 0.9802029729 0.9753760099 0.8832976222 - 0.9802051187 0.9754949212 0.8838822842 - 0.9802073240 0.9756155014 0.8844670057 - 0.9802094102 0.9757360220 0.8850517273 - 0.9802116156 0.9758566022 0.8856287003 - 0.9802137017 0.9759771824 0.8861843944 - 0.9802159071 0.9760977030 0.8867400289 - 0.9802179933 0.9762182832 0.8872956038 - 0.9802200794 0.9763411283 0.8878511786 - 0.9802222848 0.9764642119 0.8884068131 - 0.9802243710 0.9765874147 0.8889623880 - 0.9802265763 0.9767104983 0.8895106912 - 0.9802287221 0.9768335819 0.8900415897 - 0.9802309275 0.9769567847 0.8905724883 - 0.9802330136 0.9770799279 0.8911035061 - 0.9802352190 0.9772124887 0.8916344047 - 0.9802373052 0.9773454070 0.8921653032 - 0.9802395105 0.9774783254 0.8926963210 - 0.9802415967 0.9776111841 0.8932245970 - 0.9802438021 0.9777441025 0.8937453032 - 0.9802458882 0.9778770208 0.8942660093 - 0.9802479744 0.9780098796 0.8947867155 - 0.9802501798 0.9781574011 0.8953074217 - 0.9802523255 0.9783051014 0.8958281875 - 0.9802544713 0.9784528017 0.8963488936 - 0.9802566171 0.9786006212 0.8968678117 - 0.9802588224 0.9787483215 0.8973752856 - 0.9802609086 0.9788960218 0.8978827000 - 0.9802631140 0.9790437222 0.8983901739 - 0.9802652001 0.9791916013 0.8988977075 - 0.9802674055 0.9793394804 0.8994051814 - 0.9802694917 0.9794874191 0.8999127150 - 0.9802716970 0.9796352983 0.9004201889 - 0.9802737832 0.9797831774 0.9009217024 - 0.9802759290 0.9799311161 0.9014229178 - 0.9802780747 0.9800789952 0.9019240737 - 0.9802802205 0.9802207947 0.9024252892 - 0.9802927971 0.9803609848 0.9029263854 - 0.9803174734 0.9805011749 0.9034276009 - 0.9803423285 0.9806414247 0.9039288163 - 0.9803670049 0.9807816148 0.9044218063 - 0.9803916812 0.9809218049 0.9049109221 - 0.9804164171 0.9810619950 0.9053999782 - 0.9804412127 0.9811941981 0.9058890939 - 0.9804722071 0.9813209176 0.9063782096 - 0.9805197120 0.9814475179 0.9068673253 - 0.9805672169 0.9815741777 0.9073563814 - 0.9806147814 0.9817007780 0.9078388810 - 0.9806622863 0.9818274975 0.9083073139 - 0.9807097912 0.9819540977 0.9087758064 - 0.9807572961 0.9820784926 0.9092441797 - 0.9808050990 0.9821977019 0.9097126126 - 0.9808717966 0.9823167920 0.9101811051 - 0.9809383750 0.9824360013 0.9106494784 - 0.9810050726 0.9825550914 0.9111179709 - 0.9810717106 0.9826743007 0.9115676880 - 0.9811384082 0.9827933908 0.9120151997 - 0.9812049866 0.9829124808 0.9124627113 - 0.9812716842 0.9830338955 0.9129102230 - 0.9813528061 0.9831554294 0.9133576751 - 0.9814409018 0.9832769036 0.9138051867 - 0.9815291166 0.9833983779 0.9142525792 - 0.9816172123 0.9835199118 0.9146981835 - 0.9817053080 0.9836413860 0.9151405096 - 0.9817934036 0.9837629199 0.9155828953 - 0.9818816185 0.9838852882 0.9160252213 - 0.9819723964 0.9840087891 0.9164674878 - 0.9820711017 0.9841321707 0.9169098735 - 0.9821699262 0.9842556715 0.9173521996 - 0.9822685719 0.9843791127 0.9177945852 - 0.9823673964 0.9845024943 0.9182294011 - 0.9824661016 0.9846259952 0.9186623096 - 0.9825648069 0.9847493768 0.9190952182 - 0.9826635718 0.9848722816 0.9195280075 - 0.9827668071 0.9849951267 0.9199609160 - 0.9828714132 0.9851179123 0.9203937054 - 0.9829760194 0.9852408171 0.9208266139 - 0.9830806255 0.9853636026 0.9212580919 - 0.9831852913 0.9854863882 0.9216784835 - 0.9832898974 0.9856092930 0.9220989943 - 0.9833945036 0.9857273102 0.9225193858 - 0.9834997058 0.9858360887 0.9229398966 - 0.9836072922 0.9859449267 0.9233602881 - 0.9837149978 0.9860537052 0.9237807989 - 0.9838227034 0.9861624837 0.9242011905 - 0.9839304090 0.9862713218 0.9246129990 - 0.9840379953 0.9863801003 0.9250090122 - 0.9841457009 0.9864888787 0.9254049063 - 0.9842534065 0.9865922928 0.9258008003 - 0.9843615294 0.9866927266 0.9261968136 - 0.9844700098 0.9867932200 0.9265927076 - 0.9845784903 0.9868935943 0.9269887209 - 0.9846869707 0.9869940281 0.9273846149 - 0.9847955108 0.9870945215 0.9277727008 - 0.9849039912 0.9871948957 0.9281532168 - 0.9850124121 0.9872953296 0.9285337925 - 0.9851208925 0.9873874187 0.9289144278 - 0.9852285981 0.9874783158 0.9292948842 - 0.9853361249 0.9875692129 0.9296755195 - 0.9854434729 0.9876601100 0.9300559759 - 0.9855508804 0.9877510071 0.9304366112 - 0.9856582880 0.9878419042 0.9308059812 - 0.9857658148 0.9879328012 0.9311676025 - 0.9858732224 0.9880235195 0.9315292239 - 0.9859805703 0.9881017208 0.9318907261 - 0.9860852957 0.9881799817 0.9322522879 - 0.9861897826 0.9882581830 0.9326137900 - 0.9862943888 0.9883363843 0.9329754114 - 0.9863988757 0.9884145856 0.9333369136 - 0.9865034223 0.9884927869 0.9336916804 - 0.9866079092 0.9885709882 0.9340415001 - 0.9867123961 0.9886481762 0.9343912005 - 0.9868168831 0.9887102842 0.9347409010 - 0.9869164228 0.9887723923 0.9350907207 - 0.9870160222 0.9888346195 0.9354404211 - 0.9871155024 0.9888967276 0.9357901216 - 0.9872151017 0.9889587760 0.9361398816 - 0.9873145819 0.9890210032 0.9364842176 - 0.9874141812 0.9890831113 0.9368222952 - 0.9875137210 0.9891446233 0.9371604919 - 0.9876132011 0.9891886115 0.9374986887 - 0.9877052903 0.9892325997 0.9378368855 - 0.9877973795 0.9892765880 0.9381750226 - 0.9878895283 0.9893206954 0.9385132194 - 0.9879816175 0.9893646836 0.9388514161 - 0.9880737066 0.9894086719 0.9391885996 - 0.9881657958 0.9894527197 0.9395235181 - 0.9882578850 0.9894968271 0.9398584962 - 0.9883499742 0.9895275831 0.9401934147 - 0.9884337187 0.9895573258 0.9405282736 - 0.9885166287 0.9895870090 0.9408631921 - 0.9885994792 0.9896166921 0.9411981106 - 0.9886823893 0.9896463752 0.9415330291 - 0.9887652993 0.9896761179 0.9418678880 - 0.9888483286 0.9897058010 0.9422001839 - 0.9889311790 0.9897354841 0.9425324798 - 0.9890140891 0.9897568822 0.9428647161 - 0.9890915155 0.9897748828 0.9431970119 - 0.9891669154 0.9897928834 0.9435293078 - 0.9892423153 0.9898108840 0.9438614845 - 0.9893177152 0.9898290038 0.9441937804 - 0.9893931150 0.9898470044 0.9445260167 - 0.9894685149 0.9898650050 0.9448490143 - 0.9895439148 0.9898830056 0.9451668262 - 0.9896193147 0.9898968935 0.9454845786 - 0.9896823168 0.9899048805 0.9458023906 - 0.9897314906 0.9899129272 0.9461202025 - 0.9897806048 0.9899209142 0.9464380145 - 0.9898297191 0.9899289012 0.9467557073 - 0.9898788929 0.9899368882 0.9470735192 - 0.9899280071 0.9899448752 0.9473884106 - 0.9899771214 0.9899529815 0.9476900101 - 0.9900262952 0.9899609089 0.9479916096 - 0.9900729060 0.9899665117 0.9482933283 - 0.9901000857 0.9899721146 0.9485949278 - 0.9901273847 0.9899777174 0.9488965869 - 0.9901546836 0.9899833202 0.9491981864 - 0.9901819229 0.9899889231 0.9494997859 - 0.9902092218 0.9899945259 0.9498015046 - 0.9902365208 0.9900001287 0.9501004815 - 0.9902637005 0.9900057912 0.9503980279 - 0.9902909994 0.9900106788 0.9506955147 - 0.9903110862 0.9900152087 0.9509931207 - 0.9903277755 0.9900196195 0.9512906075 - 0.9903444052 0.9900240898 0.9515880942 - 0.9903609753 0.9900286198 0.9518855810 - 0.9903776050 0.9900329709 0.9521831274 - 0.9903942943 0.9900375009 0.9524806142 - 0.9904109240 0.9900419116 0.9527738094 - 0.9904274940 0.9900463820 0.9530668855 - 0.9904429913 0.9900497794 0.9533600211 - 0.9904512763 0.9900531173 0.9536530972 - 0.9904596210 0.9900565147 0.9539461732 - 0.9904679060 0.9900599122 0.9542394280 - 0.9904761910 0.9900631905 0.9545325041 - 0.9904844761 0.9900665879 0.9548255801 - 0.9904927015 0.9900699854 0.9551169872 - 0.9905009866 0.9900733232 0.9554036856 - 0.9905092716 0.9900764227 0.9556903243 - 0.9905135036 0.9900786877 0.9559770226 - 0.9905136228 0.9900810122 0.9562636018 - 0.9905136824 0.9900833964 0.9565503001 - 0.9905138016 0.9900857210 0.9568368793 - 0.9905139208 0.9900879860 0.9571235776 - 0.9905139804 0.9900904298 0.9574102163 - 0.9905140996 0.9900926948 0.9576948285 - 0.9905142188 0.9900950193 0.9579765797 - 0.9905142188 0.9900968075 0.9582585096 - 0.9905145168 0.9900981784 0.9585403204 - 0.9905148149 0.9900994897 0.9588220716 - 0.9905151725 0.9901009202 0.9591038823 - 0.9905154705 0.9901022911 0.9593858123 - 0.9905158281 0.9901036024 0.9596676230 - 0.9905161262 0.9901049733 0.9599493742 - 0.9905164838 0.9901064038 0.9602293968 - 0.9905167818 0.9901077151 0.9605073929 - 0.9905170798 0.9901083708 0.9607855082 - 0.9905173779 0.9901089072 0.9610636234 - 0.9905176759 0.9901093245 0.9613416791 - 0.9905180931 0.9901098013 0.9616196752 - 0.9905183911 0.9901102185 0.9618977904 - 0.9905186892 0.9901106954 0.9621759057 - 0.9905189872 0.9901111126 0.9624540210 - 0.9905194044 0.9901115894 0.9627259970 - 0.9905197024 0.9901120067 0.9629914761 - 0.9905200005 0.9901124835 0.9632570148 - 0.9905202985 0.9901130199 0.9635224938 - 0.9905205965 0.9901136160 0.9637879729 - 0.9905210137 0.9901140928 0.9640535116 - 0.9905213118 0.9901146293 0.9643189907 - 0.9905216098 0.9901151061 0.9645845294 - 0.9905219078 0.9901155829 0.9648500085 - 0.9905223250 0.9901161194 0.9651170969 - 0.9905226231 0.9901167154 0.9653869867 - 0.9905229211 0.9901171923 0.9656568170 - 0.9905232191 0.9901177287 0.9659267068 - 0.9905235171 0.9901182055 0.9661965966 - 0.9905238748 0.9901186824 0.9664664268 - 0.9905241728 0.9901192188 0.9667363167 - 0.9905244708 0.9901198149 0.9670060873 - 0.9905248284 0.9901202917 0.9672759771 - 0.9905251861 0.9901208282 0.9675456285 - 0.9905254841 0.9901213050 0.9678133130 - 0.9905257821 0.9901217818 0.9680811167 - 0.9905260801 0.9901223183 0.9683488011 - 0.9905264974 0.9901229143 0.9686166048 - 0.9905267954 0.9901233912 0.9688842893 - 0.9905270934 0.9901239276 0.9691520929 - 0.9905273914 0.9901244044 0.9694197774 - 0.9905276895 0.9901248813 0.9696875811 - 0.9905281067 0.9901254177 0.9699553251 - 0.9905284047 0.9901258945 0.9702137709 - 0.9905287027 0.9901264906 0.9704701900 - 0.9905290008 0.9901270270 0.9707266092 - 0.9905294180 0.9901275039 0.9709830284 - 0.9905297160 0.9901279807 0.9712393880 - 0.9905300140 0.9901285172 0.9714958072 - 0.9905303121 0.9901289940 0.9717522264 - 0.9905306101 0.9901295900 0.9720085859 - 0.9905310273 0.9901301265 0.9722651243 - 0.9905313253 0.9901306033 0.9725131989 - 0.9905316234 0.9901310802 0.9727489948 - 0.9905319214 0.9901316166 0.9729847908 - 0.9905322790 0.9901320934 0.9732205868 - 0.9905325770 0.9901326895 0.9734563828 - 0.9905328751 0.9901332259 0.9736921787 - 0.9905331731 0.9901337028 0.9739279747 - 0.9905334711 0.9901341796 0.9741637707 - 0.9905338883 0.9901347160 0.9743996263 - 0.9905341864 0.9901351929 0.9746354222 - 0.9905344844 0.9901357889 0.9748558998 - 0.9905347824 0.9901363254 0.9750742912 - 0.9905351996 0.9901368022 0.9752926230 - 0.9905354977 0.9901372790 0.9755108953 - 0.9905357957 0.9901378155 0.9757292867 - 0.9905360937 0.9901382923 0.9759476185 - 0.9905363917 0.9901388884 0.9761658907 - 0.9905368090 0.9901394248 0.9763842821 - 0.9905371070 0.9901399016 0.9766026139 - 0.9905374050 0.9901403785 0.9768189192 - 0.9905377030 0.9901409149 0.9770290852 - 0.9905450940 0.9901413918 0.9772393107 - 0.9905598164 0.9901419282 0.9774494767 - 0.9905744791 0.9901425242 0.9776597023 - 0.9905890822 0.9901430011 0.9778699279 - 0.9906038046 0.9901434779 0.9780800939 - 0.9906185269 0.9901440144 0.9782903194 - 0.9906330705 0.9901444912 0.9785004854 - 0.9906477928 0.9901450276 0.9787107110 - 0.9906623960 0.9901456237 0.9789108038 - 0.9906771183 0.9901518822 0.9791017771 - 0.9906994104 0.9901679754 0.9792928100 - 0.9907227159 0.9901841879 0.9794839025 - 0.9907460213 0.9902004004 0.9796748757 - 0.9907693267 0.9902166128 0.9798659086 - 0.9907925725 0.9902328253 0.9800568819 - 0.9908158779 0.9902489781 0.9802479148 - 0.9908391833 0.9902651906 0.9804388881 - 0.9908624887 0.9902812839 0.9806299210 - 0.9908857942 0.9902974963 0.9808201790 - 0.9909120202 0.9903177023 0.9810101986 - 0.9909530282 0.9903402925 0.9812002182 - 0.9909939766 0.9903628826 0.9813901782 - 0.9910349250 0.9903854728 0.9815803170 - 0.9910758734 0.9904081225 0.9817702770 - 0.9911168814 0.9904307127 0.9819602966 - 0.9911578298 0.9904533029 0.9821503162 - 0.9911987782 0.9904758930 0.9823402762 - 0.9912397861 0.9904984832 0.9825302958 - 0.9912806749 0.9905210733 0.9827157855 - 0.9913269281 0.9905534983 0.9828997254 - 0.9913831949 0.9905890822 0.9830836058 - 0.9914394021 0.9906246066 0.9832674265 - 0.9914957285 0.9906600714 0.9834513068 - 0.9915518761 0.9906957150 0.9836351871 - 0.9916082025 0.9907311797 0.9838191271 - 0.9916644096 0.9907668233 0.9840028882 - 0.9917206764 0.9908022881 0.9841868281 - 0.9917770028 0.9908378124 0.9843707085 - 0.9918332100 0.9908733964 0.9845507741 - 0.9918966293 0.9909213781 0.9847291708 - 0.9919701815 0.9909728169 0.9849076271 - 0.9920437932 0.9910240769 0.9850859046 - 0.9921175241 0.9910753965 0.9852643013 - 0.9921910763 0.9911267757 0.9854426980 - 0.9922646880 0.9911780953 0.9856210947 - 0.9923382998 0.9912294149 0.9857994914 - 0.9924119115 0.9912807941 0.9859778881 - 0.9924855232 0.9913321137 0.9861562848 - 0.9925590754 0.9913833737 0.9863287807 - 0.9926332831 0.9914423823 0.9864963889 - 0.9927082062 0.9915043712 0.9866638780 - 0.9927831888 0.9915664196 0.9868314266 - 0.9928581119 0.9916284084 0.9869989157 - 0.9929330945 0.9916903973 0.9871664047 - 0.9930080175 0.9917523861 0.9873338938 - 0.9930830002 0.9918143153 0.9875013828 - 0.9931579828 0.9918763041 0.9876688719 - 0.9932329059 0.9919382930 0.9878364801 - 0.9933078885 0.9920002818 0.9879997969 - 0.9933835864 0.9920656085 0.9881514907 - 0.9934617877 0.9921336174 0.9883031845 - 0.9935399890 0.9922016859 0.9884548783 - 0.9936181903 0.9922698140 0.9886065722 - 0.9936962724 0.9923378825 0.9887583256 - 0.9937744737 0.9924060106 0.9889100194 - 0.9938526750 0.9924740791 0.9890617132 - 0.9939308763 0.9925422072 0.9892134070 - 0.9940090775 0.9926102757 0.9893651009 - 0.9940872788 0.9926782846 0.9895167947 - 0.9941654801 0.9927479029 0.9896612763 - 0.9942458868 0.9928218722 0.9898049235 - 0.9943264127 0.9928959012 0.9899485111 - 0.9944068789 0.9929698706 0.9900920987 - 0.9944872856 0.9930438995 0.9902356863 - 0.9945678115 0.9931179881 0.9903792739 - 0.9946482778 0.9931920171 0.9905229211 - 0.9947286844 0.9932659864 0.9906665087 - 0.9948092103 0.9933400154 0.9908100963 - 0.9948896766 0.9934139848 0.9909536839 - 0.9949702024 0.9934880137 0.9910960197 - 0.9950510263 0.9935687184 0.9912365079 - 0.9951320887 0.9936506152 0.9913768768 - 0.9952132106 0.9937325120 0.9915173054 - 0.9952942729 0.9938144088 0.9916576743 - 0.9953753948 0.9938963056 0.9917981029 - 0.9954563975 0.9939782023 0.9919384718 - 0.9955375195 0.9940600991 0.9920789003 - 0.9956185818 0.9941418767 0.9922193885 - 0.9956997037 0.9942237735 0.9923598170 - 0.9957808256 0.9943056703 0.9925001860 - 0.9958618283 0.9943904281 0.9926363230 - 0.9959421158 0.9944810271 0.9927713275 - 0.9960222840 0.9945715070 0.9929062724 - 0.9961025715 0.9946621060 0.9930412173 - 0.9961829185 0.9947527051 0.9931762218 - 0.9962632060 0.9948431849 0.9933112264 - 0.9963434935 0.9949337840 0.9934461117 - 0.9964237809 0.9950243235 0.9935811162 - 0.9965040088 0.9951149225 0.9937161207 - 0.9965842962 0.9952055216 0.9938511252 - 0.9966645837 0.9952960014 0.9939857125 - 0.9967439771 0.9953925014 0.9941160083 - 0.9968224764 0.9954916239 0.9942464232 - 0.9969010949 0.9955906868 0.9943767190 - 0.9969795942 0.9956899285 0.9945070148 - 0.9970582128 0.9957889915 0.9946373105 - 0.9971367121 0.9958881140 0.9947676063 - 0.9972152710 0.9959872961 0.9948979020 - 0.9972937703 0.9960864186 0.9950283170 - 0.9973723888 0.9961854815 0.9951586127 - 0.9974510074 0.9962846041 0.9952889085 - 0.9975295067 0.9963837862 0.9954196215 - 0.9976075292 0.9964898825 0.9955514073 - 0.9976853132 0.9965963960 0.9956831932 - 0.9977632165 0.9967029095 0.9958150983 - 0.9978411198 0.9968094230 0.9959468842 - 0.9979189038 0.9969159961 0.9960787296 - 0.9979968071 0.9970225096 0.9962105751 - 0.9980745912 0.9971290231 0.9963424206 - 0.9981524944 0.9972354770 0.9964742064 - 0.9982303977 0.9973421097 0.9966061115 - 0.9983081818 0.9974486232 0.9967378974 - 0.9983860850 0.9975554943 0.9968718290 - 0.9984629154 0.9976667166 0.9970114231 - 0.9985396862 0.9977778792 0.9971510768 - 0.9986165166 0.9978891015 0.9972906709 - 0.9986932874 0.9980002046 0.9974303246 - 0.9987701178 0.9981114268 0.9975699186 - 0.9988468885 0.9982225895 0.9977095723 - 0.9989237189 0.9983338118 0.9978492260 - 0.9990004897 0.9984449744 0.9979888201 - 0.9990773201 0.9985561967 0.9981284142 - 0.9991542101 0.9986674190 0.9982680082 - 0.9992305040 0.9987785816 0.9984086156 - 0.9993004799 0.9988896251 0.9985532761 - 0.9993703961 0.9990006089 0.9986979961 - 0.9994403720 0.9991117120 0.9988425970 - 0.9995102882 0.9992226958 0.9989873171 - 0.9995803237 0.9993337989 0.9991319776 - 0.9996501803 0.9994447827 0.9992766976 - 0.9997202158 0.9995558262 0.9994212985 - 0.9997900724 0.9996669292 0.9995660186 - 0.9998601079 0.9997779131 0.9997106791 - 0.9999300241 0.9998890162 0.9998552799 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0004029108 0.0002821621 0.0001828344 + 0.0008352380 0.0005855577 0.0003765924 + 0.0012884660 0.0009102127 0.0005832637 + 0.0017661640 0.0012608080 0.0008021335 + 0.0022727679 0.0016344450 0.0010323920 + 0.0028016400 0.0020279640 0.0012744220 + 0.0033602209 0.0024469071 0.0015256481 + 0.0039473702 0.0028940800 0.0017888939 + 0.0045624170 0.0033709989 0.0020655000 + 0.0052064499 0.0038755860 0.0023565269 + 0.0058913450 0.0044098911 0.0026614380 + 0.0066106170 0.0049738358 0.0029808299 + 0.0073703690 0.0055717099 0.0033126650 + 0.0081646843 0.0062076761 0.0036578199 + 0.0090046516 0.0068754591 0.0040169759 + 0.0098797111 0.0075756288 0.0043923021 + 0.0108015500 0.0083136391 0.0047826152 + 0.0117765898 0.0090896897 0.0051890090 + 0.0128089404 0.0099051362 0.0056110788 + 0.0138962502 0.0107623404 0.0060491539 + 0.0150366900 0.0116626900 0.0065042591 + 0.0162367094 0.0126068601 0.0069758608 + 0.0174948201 0.0135949403 0.0074637011 + 0.0188118201 0.0146293398 0.0079666628 + 0.0201881006 0.0157077797 0.0084832394 + 0.0216275994 0.0168280900 0.0090179667 + 0.0231346209 0.0179986991 0.0095704449 + 0.0247129407 0.0192209203 0.0101442495 + 0.0263695195 0.0204982292 0.0107389204 + 0.0280960202 0.0218272507 0.0113510201 + 0.0298976302 0.0232012607 0.0119804395 + 0.0317782089 0.0246255603 0.0126269804 + 0.0337512381 0.0260962192 0.0132909399 + 0.0358026214 0.0276108794 0.0139709497 + 0.0379312597 0.0291825198 0.0146665303 + 0.0401499309 0.0308093708 0.0153818596 + 0.0424532183 0.0324951895 0.0161184706 + 0.0448440388 0.0342448987 0.0168759096 + 0.0473214611 0.0360610783 0.0176607408 + 0.0498819388 0.0379341505 0.0184673797 + 0.0525423586 0.0398601405 0.0192978401 + 0.0553006493 0.0418499894 0.0201489609 + 0.0581430085 0.0438929684 0.0210218206 + 0.0610603690 0.0459916182 0.0219117906 + 0.0640591532 0.0481535792 0.0228250604 + 0.0671426877 0.0503738187 0.0237576701 + 0.0703096837 0.0526381992 0.0247094091 + 0.0735653490 0.0549571402 0.0256804898 + 0.0769181773 0.0573286191 0.0266711209 + 0.0803592205 0.0597677194 0.0276827496 + 0.0838969871 0.0622712001 0.0287114196 + 0.0875223130 0.0648275465 0.0297578406 + 0.0912306681 0.0674386173 0.0308247507 + 0.0950332806 0.0701159835 0.0319116488 + 0.0989207476 0.0728433207 0.0330191404 + 0.1029006988 0.0756310001 0.0341475010 + 0.1069691032 0.0784748793 0.0352968313 + 0.1111252010 0.0813758895 0.0364708304 + 0.1153606027 0.0843334123 0.0376655087 + 0.1196606010 0.0873407498 0.0388817117 + 0.1240456998 0.0903989002 0.0401183404 + 0.1285089999 0.0934979469 0.0413741618 + 0.1330395043 0.0966412276 0.0426500700 + 0.1376578957 0.0998382494 0.0439491197 + 0.1423473954 0.1030810028 0.0452704318 + 0.1471080929 0.1063693017 0.0466123708 + 0.1519421935 0.1097078994 0.0479735583 + 0.1568460017 0.1131009012 0.0493564717 + 0.1618086994 0.1165459007 0.0507622100 + 0.1668350995 0.1200487018 0.0521893688 + 0.1719325930 0.1235952005 0.0536371209 + 0.1770922989 0.1271920949 0.0551049188 + 0.1823212057 0.1308318973 0.0565952100 + 0.1875973940 0.1345200986 0.0581106283 + 0.1929260045 0.1382502019 0.0596516207 + 0.1983052045 0.1420248002 0.0612149313 + 0.2037338018 0.1458451003 0.0627992302 + 0.2092058957 0.1496911049 0.0644068494 + 0.2147053927 0.1535611004 0.0660389215 + 0.2202475965 0.1574510038 0.0676914304 + 0.2258259058 0.1613723040 0.0693570822 + 0.2314376980 0.1653227061 0.0710399523 + 0.2370904982 0.1692909002 0.0727412999 + 0.2427740991 0.1732849032 0.0744594336 + 0.2484734952 0.1773027033 0.0761927664 + 0.2541978061 0.1813420951 0.0779421404 + 0.2599338889 0.1854172051 0.0797097385 + 0.2656922042 0.1895329952 0.0814976320 + 0.2714639008 0.1936807036 0.0833064169 + 0.2772484124 0.1978671998 0.0851336494 + 0.2830584943 0.2020945996 0.0869752765 + 0.2888925076 0.2063394040 0.0888287425 + 0.2947472036 0.2105993927 0.0906959027 + 0.3006064892 0.2148775011 0.0925794691 + 0.3064776063 0.2191728055 0.0944826677 + 0.3123730123 0.2234859020 0.0964089409 + 0.3182818890 0.2278206050 0.0983487070 + 0.3241820931 0.2321731001 0.1003040969 + 0.3300934136 0.2365338057 0.1022775024 + 0.3360261023 0.2409182042 0.1042753011 + 0.3419634998 0.2453251928 0.1062982008 + 0.3478932977 0.2497422993 0.1083303019 + 0.3538097143 0.2541579902 0.1103701964 + 0.3597145975 0.2585824132 0.1124206036 + 0.3656162918 0.2630195916 0.1144891009 + 0.3715220988 0.2674663961 0.1165636033 + 0.3774074912 0.2719157040 0.1186439022 + 0.3832853138 0.2763611078 0.1207353026 + 0.3891583085 0.2808066010 0.1228388026 + 0.3950240016 0.2852618098 0.1249521971 + 0.4008567035 0.2897278070 0.1270755976 + 0.4066801071 0.2942065001 0.1292198002 + 0.4124872983 0.2987079918 0.1313749999 + 0.4182811975 0.3031933904 0.1335413009 + 0.4240790904 0.3076609075 0.1357281953 + 0.4298571944 0.3121108115 0.1379179060 + 0.4356110096 0.3165585101 0.1401115060 + 0.4413394928 0.3209869862 0.1423158050 + 0.4470395148 0.3254047930 0.1445332021 + 0.4527026117 0.3298167884 0.1467590034 + 0.4583334923 0.3342174888 0.1489966065 + 0.4639326930 0.3386009932 0.1512416005 + 0.4695220888 0.3429653049 0.1534925997 + 0.4750846922 0.3473365903 0.1557577997 + 0.4806160927 0.3517040908 0.1580404937 + 0.4860872924 0.3560670912 0.1603357941 + 0.4915176928 0.3604130149 0.1626514047 + 0.4969187081 0.3647586107 0.1649708003 + 0.5022816062 0.3691031039 0.1672956944 + 0.5076225996 0.3734341860 0.1696293950 + 0.5129231811 0.3777643144 0.1719669998 + 0.5181546807 0.3820936084 0.1743122041 + 0.5233271718 0.3864077926 0.1766573936 + 0.5284634233 0.3907110095 0.1790025979 + 0.5335465074 0.3950001001 0.1813486069 + 0.5385751724 0.3992711008 0.1836972982 + 0.5435780883 0.4035327137 0.1860560030 + 0.5485448241 0.4077767134 0.1884246022 + 0.5534678102 0.4120056033 0.1908023953 + 0.5583646297 0.4162251055 0.1931803972 + 0.5632166862 0.4203993082 0.1955589950 + 0.5680477023 0.4245612025 0.1979466975 + 0.5728545189 0.4287038147 0.2003419995 + 0.5775995255 0.4328233004 0.2027534992 + 0.5823162794 0.4369328022 0.2051690072 + 0.5869674087 0.4410141110 0.2075875998 + 0.5915768743 0.4450846910 0.2100103050 + 0.5961518884 0.4491345882 0.2124343067 + 0.6006463170 0.4531683922 0.2148666978 + 0.6051229239 0.4571923018 0.2172998041 + 0.6095721126 0.4611825943 0.2197346985 + 0.6139861941 0.4651634991 0.2221716940 + 0.6183604002 0.4691112041 0.2246143073 + 0.6226823926 0.4730412066 0.2270615995 + 0.6269859076 0.4769391119 0.2295158058 + 0.6312270761 0.4808132946 0.2319785058 + 0.6354541779 0.4846689999 0.2344491929 + 0.6396340728 0.4885058105 0.2369215935 + 0.6438084245 0.4923363924 0.2393949032 + 0.6479752064 0.4961437881 0.2418684959 + 0.6520851254 0.4999468923 0.2443421036 + 0.6561610103 0.5037171841 0.2468205988 + 0.6601614952 0.5074794292 0.2493008971 + 0.6641432047 0.5112071037 0.2517744899 + 0.6680347919 0.5149219036 0.2542459071 + 0.6719154716 0.5186061263 0.2567091882 + 0.6757277846 0.5222734809 0.2591697872 + 0.6795322895 0.5259137750 0.2616206110 + 0.6833059192 0.5295230746 0.2640681863 + 0.6870679259 0.5330942273 0.2665097117 + 0.6908019781 0.5366339087 0.2689489126 + 0.6945022941 0.5401414037 0.2713932097 + 0.6981444955 0.5436180830 0.2738403976 + 0.7017527223 0.5470659733 0.2762868106 + 0.7053136826 0.5504943728 0.2787325978 + 0.7088435292 0.5539047718 0.2811723053 + 0.7123348117 0.5572900772 0.2836047113 + 0.7157995105 0.5606504083 0.2860364020 + 0.7192326784 0.5639901757 0.2884666920 + 0.7226405144 0.5673059225 0.2908954918 + 0.7260167003 0.5705996752 0.2933177948 + 0.7293487191 0.5738595724 0.2957400084 + 0.7326173782 0.5771085024 0.2981606126 + 0.7358595133 0.5803325772 0.3005810976 + 0.7390533090 0.5835512877 0.3029994965 + 0.7422223091 0.5867486000 0.3054173887 + 0.7453266978 0.5899420977 0.3078261018 + 0.7484182715 0.5930835009 0.3102284968 + 0.7514507174 0.5962249041 0.3126305044 + 0.7544791102 0.5993114114 0.3150317967 + 0.7574347258 0.6023926139 0.3174335063 + 0.7603902817 0.6054285169 0.3198406994 + 0.7632429004 0.6084480286 0.3222478032 + 0.7660846710 0.6114342213 0.3246580958 + 0.7688931227 0.6143910885 0.3270691931 + 0.7716894150 0.6173362732 0.3294793069 + 0.7744498253 0.6202554703 0.3318881989 + 0.7771797776 0.6231728196 0.3342961073 + 0.7798776031 0.6260603070 0.3366977870 + 0.7825078964 0.6289477944 0.3390995860 + 0.7851305008 0.6318117976 0.3414917886 + 0.7876675725 0.6346691847 0.3438811004 + 0.7902047038 0.6375092864 0.3462617099 + 0.7926716805 0.6403307915 0.3486273885 + 0.7951231003 0.6431398988 0.3509930074 + 0.7975525260 0.6458842754 0.3533450067 + 0.7999621034 0.6486287117 0.3556961119 + 0.8023657799 0.6513149738 0.3580406904 + 0.8047469854 0.6539878845 0.3603785932 + 0.8071283102 0.6566321254 0.3627164066 + 0.8094397187 0.6592395902 0.3650526106 + 0.8117408156 0.6618456841 0.3673886955 + 0.8139895797 0.6644176841 0.3697170913 + 0.8161870241 0.6669896841 0.3720383942 + 0.8183761239 0.6695259213 0.3743596971 + 0.8205000162 0.6720392704 0.3766688108 + 0.8226239085 0.6745491028 0.3789778948 + 0.8247258067 0.6770399213 0.3812792897 + 0.8268182278 0.6795307994 0.3835709095 + 0.8289020061 0.6819990277 0.3858624995 + 0.8309609294 0.6844567060 0.3881478012 + 0.8330196738 0.6869028211 0.3904320002 + 0.8350235224 0.6892951131 0.3927091956 + 0.8370118737 0.6916874051 0.3949654102 + 0.8389791846 0.6940457821 0.3972215950 + 0.8408939242 0.6963855028 0.3994632065 + 0.8428086042 0.6987230182 0.4016954005 + 0.8446682096 0.7010375261 0.4039277136 + 0.8465105295 0.7033519149 0.4061439037 + 0.8483420014 0.7056419253 0.4083589911 + 0.8501332998 0.7079067230 0.4105660021 + 0.8519244790 0.7101715803 0.4127483070 + 0.8536859751 0.7123854160 0.4149304926 + 0.8554301858 0.7145920992 0.4171046913 + 0.8571730852 0.7167863250 0.4192717075 + 0.8588685989 0.7189391255 0.4214386046 + 0.8605639935 0.7210919857 0.4236004949 + 0.8622372150 0.7232128978 0.4257610142 + 0.8638756275 0.7253087163 0.4279214144 + 0.8655139804 0.7274044156 0.4300718904 + 0.8671008945 0.7294499874 0.4322223067 + 0.8686684966 0.7314872742 0.4343703091 + 0.8702337742 0.7335180044 0.4365068972 + 0.8717327714 0.7355036736 0.4386434853 + 0.8732318878 0.7374892831 0.4407753050 + 0.8747140765 0.7394585013 0.4428966939 + 0.8761591911 0.7413989902 0.4450182021 + 0.8776043057 0.7433394194 0.4471280873 + 0.8790065050 0.7452735901 0.4492236078 + 0.8803750873 0.7472031116 0.4513190985 + 0.8817436099 0.7491325736 0.4533992112 + 0.8830839992 0.7510256767 0.4554660916 + 0.8844164014 0.7529067993 0.4575330019 + 0.8857486844 0.7547879815 0.4595870972 + 0.8870559931 0.7566236854 0.4616327882 + 0.8883624077 0.7584543824 0.4636783898 + 0.8896635771 0.7602844834 0.4657067060 + 0.8909289837 0.7620761991 0.4677248001 + 0.8921945095 0.7638679147 0.4697428942 + 0.8934394121 0.7656568289 0.4717616141 + 0.8946236968 0.7674204707 0.4737805128 + 0.8958079815 0.7691841722 0.4757995009 + 0.8969696164 0.7709385753 0.4778141081 + 0.8980888724 0.7726433873 0.4798257947 + 0.8992081881 0.7743480802 0.4818373919 + 0.9003195167 0.7760472894 0.4838384986 + 0.9014195800 0.7777205706 0.4858298898 + 0.9025197029 0.7793939114 0.4878214002 + 0.9035978913 0.7810617089 0.4898056984 + 0.9046490788 0.7827023864 0.4917800128 + 0.9057002068 0.7843430042 0.4937543869 + 0.9067236185 0.7859767079 0.4957253933 + 0.9077128172 0.7875623107 0.4976881146 + 0.9087020159 0.7891479731 0.4996508062 + 0.9096745849 0.7907313108 0.5016111732 + 0.9106239080 0.7922711968 0.5035545826 + 0.9115731716 0.7938110232 0.5054979920 + 0.9124993086 0.7953509092 0.5074414015 + 0.9133828282 0.7968598008 0.5093576908 + 0.9142662287 0.7983667254 0.5112711787 + 0.9151422977 0.7998737097 0.5131847262 + 0.9159966707 0.8013570905 0.5150825977 + 0.9168511033 0.8028343916 0.5169728994 + 0.9176996946 0.8043118119 0.5188632011 + 0.9185072780 0.8057656884 0.5207424760 + 0.9193148017 0.8072052002 0.5226051807 + 0.9201223254 0.8086445928 0.5244678855 + 0.9209262133 0.8100745082 0.5263286233 + 0.9217298031 0.8114904165 0.5281689167 + 0.9225333929 0.8129063249 0.5300090909 + 0.9233224988 0.8143162727 0.5318493843 + 0.9241070151 0.8156955242 0.5336812139 + 0.9248914719 0.8170747161 0.5355101228 + 0.9256622195 0.8184539080 0.5373389721 + 0.9264205098 0.8198124170 0.5391626954 + 0.9271788001 0.8211680055 0.5409774184 + 0.9279240966 0.8225237131 0.5427920222 + 0.9286308885 0.8238555193 0.5446066260 + 0.9293376803 0.8251689076 0.5463947058 + 0.9300444722 0.8264822960 0.5481811166 + 0.9307246208 0.8277850151 0.5499675870 + 0.9314032793 0.8290479183 0.5517386198 + 0.9320818782 0.8303108215 0.5534932017 + 0.9327319860 0.8315737247 0.5552479029 + 0.9333636761 0.8327975273 0.5570024848 + 0.9339953065 0.8340129256 0.5587229133 + 0.9346169233 0.8352283239 0.5604429841 + 0.9352049828 0.8364298940 0.5621631742 + 0.9357931018 0.8376104832 0.5638735890 + 0.9363812208 0.8387910128 0.5655727983 + 0.9369478822 0.8399716020 0.5672720075 + 0.9375097752 0.8411086798 0.5689712167 + 0.9380717874 0.8422427773 0.5706508160 + 0.9386349916 0.8433768749 0.5723282099 + 0.9392004013 0.8445004821 0.5740054846 + 0.9397658110 0.8456103802 0.5756754279 + 0.9403312206 0.8467202783 0.5773270726 + 0.9408558011 0.8478301764 0.5789787173 + 0.9413745999 0.8489074111 0.5806304216 + 0.9418935180 0.8499805927 0.5822653174 + 0.9423980713 0.8510538936 0.5838909149 + 0.9428763986 0.8521230221 0.5855165124 + 0.9433547258 0.8531830907 0.5871421099 + 0.9438331127 0.8542432189 0.5887404084 + 0.9443081021 0.8553032875 0.5903375149 + 0.9447823763 0.8563414812 0.5919346213 + 0.9452565908 0.8573703170 0.5935288072 + 0.9457269907 0.8583989739 0.5951129794 + 0.9461808801 0.8594260216 0.5966972113 + 0.9466348290 0.8604149818 0.5982813835 + 0.9470885992 0.8614040017 0.5998545289 + 0.9475203156 0.8623930812 0.6014149189 + 0.9479331970 0.8633654118 0.6029751897 + 0.9483461976 0.8643069267 0.6045355797 + 0.9487591982 0.8652483821 0.6060755253 + 0.9491466284 0.8661898971 0.6076046824 + 0.9495298862 0.8671160936 0.6091338992 + 0.9499133229 0.8680329919 0.6106631756 + 0.9502913952 0.8689497709 0.6121817827 + 0.9506375790 0.8698666096 0.6136978269 + 0.9509837031 0.8707538843 0.6152138710 + 0.9513298869 0.8716354966 0.6167299747 + 0.9516645074 0.8725172281 0.6182156801 + 0.9519801736 0.8733978868 0.6196988225 + 0.9522958994 0.8742434978 0.6211817861 + 0.9526115060 0.8750889897 0.6226648092 + 0.9529042244 0.8759344816 0.6241227984 + 0.9531800151 0.8767787218 0.6255803704 + 0.9534558058 0.8776161075 0.6270378828 + 0.9537314773 0.8784536123 0.6284952760 + 0.9539870024 0.8792909980 0.6299347878 + 0.9542350173 0.8801239729 0.6313741803 + 0.9544829726 0.8809452057 0.6328135729 + 0.9547309875 0.8817663789 0.6342530847 + 0.9549531937 0.8825874925 0.6356778741 + 0.9551706910 0.8833963275 0.6371027231 + 0.9553881884 0.8841794729 0.6385273933 + 0.9556056857 0.8849627972 0.6399521232 + 0.9558088183 0.8857461214 0.6413484812 + 0.9560105801 0.8865262866 0.6427426934 + 0.9562124014 0.8873007894 0.6441369057 + 0.9564142227 0.8880752921 0.6455309987 + 0.9566016793 0.8888496757 0.6469084024 + 0.9567884803 0.8896216154 0.6482821703 + 0.9569752812 0.8903880119 0.6496559978 + 0.9571620822 0.8911542892 0.6510298252 + 0.9573429823 0.8919206858 0.6523879170 + 0.9575235844 0.8926829100 0.6537382007 + 0.9577041864 0.8934323192 0.6550884843 + 0.9578847289 0.8941817284 0.6564388871 + 0.9580754042 0.8949311972 0.6577758193 + 0.9582675099 0.8956754208 0.6590979099 + 0.9584596157 0.8963845968 0.6604200006 + 0.9586517215 0.8970937729 0.6617420912 + 0.9588462114 0.8978030086 0.6630570889 + 0.9590412974 0.8985121846 0.6643490791 + 0.9592363834 0.8991947770 0.6656410098 + 0.9594315290 0.8998764753 0.6669330001 + 0.9596217275 0.9005582929 0.6682248712 + 0.9598088861 0.9012399912 0.6695014238 + 0.9599962234 0.9018872976 0.6707772017 + 0.9601835012 0.9025238752 0.6720529199 + 0.9603782296 0.9031605124 0.6733285785 + 0.9605820775 0.9037970901 0.6746001244 + 0.9607859850 0.9044255018 0.6758689880 + 0.9609898925 0.9050459862 0.6771379709 + 0.9611927271 0.9056665897 0.6784068942 + 0.9613916874 0.9062871933 0.6796714067 + 0.9615907073 0.9069045186 0.6809216142 + 0.9617896080 0.9075098038 0.6821717024 + 0.9619886279 0.9081152081 0.6834219098 + 0.9621633887 0.9087206125 0.6846719980 + 0.9623364210 0.9093260169 0.6859096885 + 0.9625093937 0.9099286199 0.6871445179 + 0.9626824260 0.9105308056 0.6883792281 + 0.9628574252 0.9111331105 0.6896139979 + 0.9630337954 0.9117352962 0.6908426285 + 0.9632101059 0.9123305082 0.6920586824 + 0.9633864760 0.9129180908 0.6932747960 + 0.9635632038 0.9135056138 0.6944910288 + 0.9637411237 0.9140931964 0.6957070827 + 0.9639191031 0.9146800041 0.6969081163 + 0.9640970230 0.9152501225 0.6981055140 + 0.9642748833 0.9158201814 0.6993029118 + 0.9644508958 0.9163902998 0.7005001903 + 0.9646263719 0.9169602990 0.7016944289 + 0.9648019075 0.9175173044 0.7028782964 + 0.9649773836 0.9180638790 0.7040622234 + 0.9651508927 0.9186105132 0.7052459717 + 0.9653204083 0.9191570878 0.7064298987 + 0.9654898047 0.9197033048 0.7075976133 + 0.9656593204 0.9202299118 0.7087550759 + 0.9658287168 0.9207565784 0.7099125981 + 0.9659901857 0.9212831855 0.7110701203 + 0.9661499262 0.9218099117 0.7122275233 + 0.9663096070 0.9223287106 0.7133823037 + 0.9664692283 0.9228376746 0.7145369053 + 0.9666253924 0.9233465791 0.7156915069 + 0.9667711854 0.9238554835 0.7168461084 + 0.9669169188 0.9243643880 0.7179933786 + 0.9670627117 0.9248356819 0.7191138864 + 0.9672083855 0.9252979159 0.7202343941 + 0.9673423171 0.9257600904 0.7213549018 + 0.9674692750 0.9262223244 0.7224754095 + 0.9675962925 0.9266850948 0.7235804796 + 0.9677233100 0.9271525741 0.7246658802 + 0.9678503275 0.9276201725 0.7257512808 + 0.9679561853 0.9280877113 0.7268366814 + 0.9680612087 0.9285551906 0.7279220819 + 0.9681661725 0.9290199280 0.7289987803 + 0.9682713151 0.9294800162 0.7300698757 + 0.9683721066 0.9299402237 0.7311409712 + 0.9684600830 0.9304003716 0.7322121263 + 0.9685481191 0.9308605194 0.7332832217 + 0.9686360955 0.9313055873 0.7343558073 + 0.9687241912 0.9317405820 0.7354289889 + 0.9687963128 0.9321755767 0.7365021110 + 0.9688509703 0.9326105714 0.7375752926 + 0.9689058065 0.9330456257 0.7386484146 + 0.9689605236 0.9334610105 0.7396969199 + 0.9690153003 0.9338703752 0.7407392263 + 0.9690718055 0.9342797995 0.7417815924 + 0.9691293836 0.9346892238 0.7428238988 + 0.9691869020 0.9350985885 0.7438662052 + 0.9692444205 0.9354946017 0.7448874712 + 0.9693019986 0.9358887076 0.7459040284 + 0.9693595767 0.9362828135 0.7469205260 + 0.9694172740 0.9366768003 0.7479370236 + 0.9694749713 0.9370709062 0.7489535213 + 0.9695327282 0.9374542832 0.7499631047 + 0.9695904255 0.9378368855 0.7509707212 + 0.9696562290 0.9382194877 0.7519782782 + 0.9697234035 0.9386020899 0.7529858947 + 0.9697905779 0.9389846921 0.7539935112 + 0.9698578119 0.9393534064 0.7549921870 + 0.9699249864 0.9397209883 0.7559866905 + 0.9700003266 0.9400886297 0.7569811940 + 0.9700766802 0.9404562116 0.7579756975 + 0.9701529741 0.9408237934 0.7589702010 + 0.9702293873 0.9411832094 0.7599533200 + 0.9703058004 0.9415413737 0.7609264255 + 0.9703885913 0.9418995976 0.7618994713 + 0.9704725742 0.9422578216 0.7628725767 + 0.9705566168 0.9426159859 0.7638456225 + 0.9706404805 0.9429603815 0.7648131251 + 0.9707245231 0.9433007836 0.7657690048 + 0.9708133936 0.9436411858 0.7667248249 + 0.9709036946 0.9439815879 0.7676807046 + 0.9709941149 0.9443218708 0.7686365843 + 0.9710844755 0.9446520805 0.7695921063 + 0.9711748958 0.9449757934 0.7705423832 + 0.9712682962 0.9452995062 0.7714927793 + 0.9713637233 0.9456232190 0.7724431753 + 0.9714590907 0.9459468722 0.7733935118 + 0.9715543985 0.9462659955 0.7743439078 + 0.9716498256 0.9465780258 0.7752857804 + 0.9717406034 0.9468899965 0.7762247920 + 0.9718248844 0.9472020268 0.7771639228 + 0.9719092846 0.9475139976 0.7781028748 + 0.9719936848 0.9478241205 0.7790420055 + 0.9720780849 0.9481198788 0.7799775004 + 0.9721639752 0.9484158158 0.7809069157 + 0.9722574949 0.9487115741 0.7818363905 + 0.9723510146 0.9490075111 0.7827658057 + 0.9724445939 0.9493033886 0.7836952806 + 0.9725381136 0.9495918155 0.7846246958 + 0.9726316929 0.9498783946 0.7855424881 + 0.9727249742 0.9501649141 0.7864595056 + 0.9728183150 0.9504513741 0.7873764038 + 0.9729115963 0.9507378936 0.7882934213 + 0.9730048776 0.9510220885 0.7892103791 + 0.9730982184 0.9513028860 0.7901170850 + 0.9731950164 0.9515836239 0.7910107970 + 0.9732956290 0.9518643022 0.7919045091 + 0.9733961225 0.9521449804 0.7927982211 + 0.9734966755 0.9524257779 0.7936918736 + 0.9735972285 0.9526972175 0.7945855856 + 0.9736980200 0.9529681802 0.7954729199 + 0.9738026261 0.9532393217 0.7963594198 + 0.9739071131 0.9535102844 0.7972459197 + 0.9740117192 0.9537813067 0.7981324196 + 0.9741162062 0.9540500045 0.7990188003 + 0.9742208123 0.9543160200 0.7999022007 + 0.9743257165 0.9545819759 0.8007770777 + 0.9744309187 0.9548478723 0.8016518950 + 0.9745361209 0.9551138878 0.8025267720 + 0.9746413231 0.9553797841 0.8034017086 + 0.9747465253 0.9556449056 0.8042765856 + 0.9748516083 0.9559097886 0.8051419854 + 0.9749541283 0.9561746716 0.8060011268 + 0.9750565290 0.9564396739 0.8068600893 + 0.9751588702 0.9567046165 0.8077191710 + 0.9752613902 0.9569633007 0.8085781932 + 0.9753637910 0.9572036862 0.8094372749 + 0.9754638076 0.9574440718 0.8102779984 + 0.9755604267 0.9576845765 0.8111159205 + 0.9756569266 0.9579250216 0.8119537234 + 0.9757534266 0.9581654072 0.8127915263 + 0.9758499861 0.9584035873 0.8136293888 + 0.9759464860 0.9586400986 0.8144659996 + 0.9760364890 0.9588767290 0.8152853847 + 0.9761242867 0.9591131806 0.8161048293 + 0.9762120247 0.9593496919 0.8169242144 + 0.9762998223 0.9595862031 0.8177435994 + 0.9763875008 0.9598243833 0.8185629845 + 0.9764752984 0.9600629807 0.8193802834 + 0.9765614867 0.9603015184 0.8201889992 + 0.9766476154 0.9605401158 0.8209977150 + 0.9767338037 0.9607787132 0.8218063712 + 0.9768199921 0.9610170722 0.8226152062 + 0.9769061804 0.9612380862 0.8234239221 + 0.9769896269 0.9614589810 0.8242325187 + 0.9770637155 0.9616798759 0.8250408173 + 0.9771376848 0.9619007707 0.8258491158 + 0.9772117138 0.9621217847 0.8266574144 + 0.9772858024 0.9623394012 0.8274655938 + 0.9773597717 0.9625350237 0.8282738924 + 0.9774324298 0.9627305865 0.8290752172 + 0.9775022864 0.9629262090 0.8298593760 + 0.9775723219 0.9631217718 0.8306434751 + 0.9776422977 0.9633173943 0.8314276934 + 0.9777122736 0.9635074139 0.8322117925 + 0.9777823091 0.9636734724 0.8329960108 + 0.9778506756 0.9638395905 0.8337736726 + 0.9779173136 0.9640057087 0.8345307708 + 0.9779838920 0.9641718268 0.8352878094 + 0.9780504704 0.9643378854 0.8360449076 + 0.9781171083 0.9645028710 0.8368018866 + 0.9781836867 0.9646632075 0.8375589848 + 0.9782491922 0.9648234844 0.8383142948 + 0.9783133864 0.9649837017 0.8390561938 + 0.9783775806 0.9651439786 0.8397982121 + 0.9784417748 0.9653041959 0.8405401111 + 0.9785060287 0.9654663801 0.8412820101 + 0.9785702229 0.9656417966 0.8420239091 + 0.9786368012 0.9658172727 0.8427659273 + 0.9787064791 0.9659928083 0.8434938788 + 0.9787762761 0.9661682844 0.8442208171 + 0.9788460135 0.9663438201 0.8449476957 + 0.9789156914 0.9665191770 0.8456746936 + 0.9789854288 0.9667049050 0.8464015722 + 0.9790521264 0.9668906927 0.8471285105 + 0.9791120887 0.9670764804 0.8478465080 + 0.9791721106 0.9672623277 0.8485602736 + 0.9792320728 0.9674479961 0.8492740989 + 0.9792920947 0.9676337838 0.8499878049 + 0.9793521166 0.9678208232 0.8507015705 + 0.9794105291 0.9680082202 0.8514152765 + 0.9794589877 0.9681956172 0.8521232009 + 0.9795076251 0.9683828950 0.8528202772 + 0.9795562029 0.9685702920 0.8535174131 + 0.9796047807 0.9687576890 0.8542146087 + 0.9796532989 0.9689376950 0.8549116850 + 0.9797018766 0.9691110253 0.8556088805 + 0.9797381759 0.9692844152 0.8563060164 + 0.9797729850 0.9694576859 0.8569779992 + 0.9798077941 0.9696310163 0.8576492071 + 0.9798427224 0.9698044062 0.8583204150 + 0.9798774719 0.9699763060 0.8589916229 + 0.9799122810 0.9701421857 0.8596627712 + 0.9799376130 0.9703081250 0.8603339791 + 0.9799560905 0.9704741240 0.8609988093 + 0.9799745083 0.9706400037 0.8616573811 + 0.9799929857 0.9708058834 0.8623160124 + 0.9800114036 0.9709718227 0.8629745841 + 0.9800298810 0.9711270928 0.8636332154 + 0.9800478220 0.9712795019 0.8642917871 + 0.9800634980 0.9714319110 0.8649504185 + 0.9800791740 0.9715842009 0.8655930758 + 0.9800949097 0.9717366099 0.8662353754 + 0.9801105857 0.9718890190 0.8668777943 + 0.9801263213 0.9720391035 0.8675202131 + 0.9801419973 0.9721841812 0.8681625724 + 0.9801493883 0.9723293185 0.8688049912 + 0.9801540971 0.9724743962 0.8694401979 + 0.9801586866 0.9726194739 0.8700637817 + 0.9801633954 0.9727646112 0.8706874251 + 0.9801681042 0.9729096293 0.8713108897 + 0.9801728129 0.9730468988 0.8719344735 + 0.9801768064 0.9731817245 0.8725581169 + 0.9801785946 0.9733164907 0.8731817007 + 0.9801803827 0.9734513164 0.8737874031 + 0.9801821709 0.9735862017 0.8743858933 + 0.9801840186 0.9737210274 0.8749843836 + 0.9801859260 0.9738534093 0.8755828738 + 0.9801877141 0.9739726186 0.8761814237 + 0.9801887274 0.9740918279 0.8767799139 + 0.9801893234 0.9742109776 0.8773784041 + 0.9801899195 0.9743301272 0.8779736757 + 0.9801905155 0.9744492769 0.8785690069 + 0.9801911116 0.9745684862 0.8791642189 + 0.9801917076 0.9746857285 0.8797594905 + 0.9801923037 0.9748008251 0.8803547025 + 0.9801943898 0.9749158025 0.8809499741 + 0.9801964760 0.9750308990 0.8815435767 + 0.9801986814 0.9751458764 0.8821282983 + 0.9802008271 0.9752609134 0.8827130198 + 0.9802029729 0.9753760099 0.8832976222 + 0.9802051187 0.9754949212 0.8838822842 + 0.9802073240 0.9756155014 0.8844670057 + 0.9802094102 0.9757360220 0.8850517273 + 0.9802116156 0.9758566022 0.8856287003 + 0.9802137017 0.9759771824 0.8861843944 + 0.9802159071 0.9760977030 0.8867400289 + 0.9802179933 0.9762182832 0.8872956038 + 0.9802200794 0.9763411283 0.8878511786 + 0.9802222848 0.9764642119 0.8884068131 + 0.9802243710 0.9765874147 0.8889623880 + 0.9802265763 0.9767104983 0.8895106912 + 0.9802287221 0.9768335819 0.8900415897 + 0.9802309275 0.9769567847 0.8905724883 + 0.9802330136 0.9770799279 0.8911035061 + 0.9802352190 0.9772124887 0.8916344047 + 0.9802373052 0.9773454070 0.8921653032 + 0.9802395105 0.9774783254 0.8926963210 + 0.9802415967 0.9776111841 0.8932245970 + 0.9802438021 0.9777441025 0.8937453032 + 0.9802458882 0.9778770208 0.8942660093 + 0.9802479744 0.9780098796 0.8947867155 + 0.9802501798 0.9781574011 0.8953074217 + 0.9802523255 0.9783051014 0.8958281875 + 0.9802544713 0.9784528017 0.8963488936 + 0.9802566171 0.9786006212 0.8968678117 + 0.9802588224 0.9787483215 0.8973752856 + 0.9802609086 0.9788960218 0.8978827000 + 0.9802631140 0.9790437222 0.8983901739 + 0.9802652001 0.9791916013 0.8988977075 + 0.9802674055 0.9793394804 0.8994051814 + 0.9802694917 0.9794874191 0.8999127150 + 0.9802716970 0.9796352983 0.9004201889 + 0.9802737832 0.9797831774 0.9009217024 + 0.9802759290 0.9799311161 0.9014229178 + 0.9802780747 0.9800789952 0.9019240737 + 0.9802802205 0.9802207947 0.9024252892 + 0.9802927971 0.9803609848 0.9029263854 + 0.9803174734 0.9805011749 0.9034276009 + 0.9803423285 0.9806414247 0.9039288163 + 0.9803670049 0.9807816148 0.9044218063 + 0.9803916812 0.9809218049 0.9049109221 + 0.9804164171 0.9810619950 0.9053999782 + 0.9804412127 0.9811941981 0.9058890939 + 0.9804722071 0.9813209176 0.9063782096 + 0.9805197120 0.9814475179 0.9068673253 + 0.9805672169 0.9815741777 0.9073563814 + 0.9806147814 0.9817007780 0.9078388810 + 0.9806622863 0.9818274975 0.9083073139 + 0.9807097912 0.9819540977 0.9087758064 + 0.9807572961 0.9820784926 0.9092441797 + 0.9808050990 0.9821977019 0.9097126126 + 0.9808717966 0.9823167920 0.9101811051 + 0.9809383750 0.9824360013 0.9106494784 + 0.9810050726 0.9825550914 0.9111179709 + 0.9810717106 0.9826743007 0.9115676880 + 0.9811384082 0.9827933908 0.9120151997 + 0.9812049866 0.9829124808 0.9124627113 + 0.9812716842 0.9830338955 0.9129102230 + 0.9813528061 0.9831554294 0.9133576751 + 0.9814409018 0.9832769036 0.9138051867 + 0.9815291166 0.9833983779 0.9142525792 + 0.9816172123 0.9835199118 0.9146981835 + 0.9817053080 0.9836413860 0.9151405096 + 0.9817934036 0.9837629199 0.9155828953 + 0.9818816185 0.9838852882 0.9160252213 + 0.9819723964 0.9840087891 0.9164674878 + 0.9820711017 0.9841321707 0.9169098735 + 0.9821699262 0.9842556715 0.9173521996 + 0.9822685719 0.9843791127 0.9177945852 + 0.9823673964 0.9845024943 0.9182294011 + 0.9824661016 0.9846259952 0.9186623096 + 0.9825648069 0.9847493768 0.9190952182 + 0.9826635718 0.9848722816 0.9195280075 + 0.9827668071 0.9849951267 0.9199609160 + 0.9828714132 0.9851179123 0.9203937054 + 0.9829760194 0.9852408171 0.9208266139 + 0.9830806255 0.9853636026 0.9212580919 + 0.9831852913 0.9854863882 0.9216784835 + 0.9832898974 0.9856092930 0.9220989943 + 0.9833945036 0.9857273102 0.9225193858 + 0.9834997058 0.9858360887 0.9229398966 + 0.9836072922 0.9859449267 0.9233602881 + 0.9837149978 0.9860537052 0.9237807989 + 0.9838227034 0.9861624837 0.9242011905 + 0.9839304090 0.9862713218 0.9246129990 + 0.9840379953 0.9863801003 0.9250090122 + 0.9841457009 0.9864888787 0.9254049063 + 0.9842534065 0.9865922928 0.9258008003 + 0.9843615294 0.9866927266 0.9261968136 + 0.9844700098 0.9867932200 0.9265927076 + 0.9845784903 0.9868935943 0.9269887209 + 0.9846869707 0.9869940281 0.9273846149 + 0.9847955108 0.9870945215 0.9277727008 + 0.9849039912 0.9871948957 0.9281532168 + 0.9850124121 0.9872953296 0.9285337925 + 0.9851208925 0.9873874187 0.9289144278 + 0.9852285981 0.9874783158 0.9292948842 + 0.9853361249 0.9875692129 0.9296755195 + 0.9854434729 0.9876601100 0.9300559759 + 0.9855508804 0.9877510071 0.9304366112 + 0.9856582880 0.9878419042 0.9308059812 + 0.9857658148 0.9879328012 0.9311676025 + 0.9858732224 0.9880235195 0.9315292239 + 0.9859805703 0.9881017208 0.9318907261 + 0.9860852957 0.9881799817 0.9322522879 + 0.9861897826 0.9882581830 0.9326137900 + 0.9862943888 0.9883363843 0.9329754114 + 0.9863988757 0.9884145856 0.9333369136 + 0.9865034223 0.9884927869 0.9336916804 + 0.9866079092 0.9885709882 0.9340415001 + 0.9867123961 0.9886481762 0.9343912005 + 0.9868168831 0.9887102842 0.9347409010 + 0.9869164228 0.9887723923 0.9350907207 + 0.9870160222 0.9888346195 0.9354404211 + 0.9871155024 0.9888967276 0.9357901216 + 0.9872151017 0.9889587760 0.9361398816 + 0.9873145819 0.9890210032 0.9364842176 + 0.9874141812 0.9890831113 0.9368222952 + 0.9875137210 0.9891446233 0.9371604919 + 0.9876132011 0.9891886115 0.9374986887 + 0.9877052903 0.9892325997 0.9378368855 + 0.9877973795 0.9892765880 0.9381750226 + 0.9878895283 0.9893206954 0.9385132194 + 0.9879816175 0.9893646836 0.9388514161 + 0.9880737066 0.9894086719 0.9391885996 + 0.9881657958 0.9894527197 0.9395235181 + 0.9882578850 0.9894968271 0.9398584962 + 0.9883499742 0.9895275831 0.9401934147 + 0.9884337187 0.9895573258 0.9405282736 + 0.9885166287 0.9895870090 0.9408631921 + 0.9885994792 0.9896166921 0.9411981106 + 0.9886823893 0.9896463752 0.9415330291 + 0.9887652993 0.9896761179 0.9418678880 + 0.9888483286 0.9897058010 0.9422001839 + 0.9889311790 0.9897354841 0.9425324798 + 0.9890140891 0.9897568822 0.9428647161 + 0.9890915155 0.9897748828 0.9431970119 + 0.9891669154 0.9897928834 0.9435293078 + 0.9892423153 0.9898108840 0.9438614845 + 0.9893177152 0.9898290038 0.9441937804 + 0.9893931150 0.9898470044 0.9445260167 + 0.9894685149 0.9898650050 0.9448490143 + 0.9895439148 0.9898830056 0.9451668262 + 0.9896193147 0.9898968935 0.9454845786 + 0.9896823168 0.9899048805 0.9458023906 + 0.9897314906 0.9899129272 0.9461202025 + 0.9897806048 0.9899209142 0.9464380145 + 0.9898297191 0.9899289012 0.9467557073 + 0.9898788929 0.9899368882 0.9470735192 + 0.9899280071 0.9899448752 0.9473884106 + 0.9899771214 0.9899529815 0.9476900101 + 0.9900262952 0.9899609089 0.9479916096 + 0.9900729060 0.9899665117 0.9482933283 + 0.9901000857 0.9899721146 0.9485949278 + 0.9901273847 0.9899777174 0.9488965869 + 0.9901546836 0.9899833202 0.9491981864 + 0.9901819229 0.9899889231 0.9494997859 + 0.9902092218 0.9899945259 0.9498015046 + 0.9902365208 0.9900001287 0.9501004815 + 0.9902637005 0.9900057912 0.9503980279 + 0.9902909994 0.9900106788 0.9506955147 + 0.9903110862 0.9900152087 0.9509931207 + 0.9903277755 0.9900196195 0.9512906075 + 0.9903444052 0.9900240898 0.9515880942 + 0.9903609753 0.9900286198 0.9518855810 + 0.9903776050 0.9900329709 0.9521831274 + 0.9903942943 0.9900375009 0.9524806142 + 0.9904109240 0.9900419116 0.9527738094 + 0.9904274940 0.9900463820 0.9530668855 + 0.9904429913 0.9900497794 0.9533600211 + 0.9904512763 0.9900531173 0.9536530972 + 0.9904596210 0.9900565147 0.9539461732 + 0.9904679060 0.9900599122 0.9542394280 + 0.9904761910 0.9900631905 0.9545325041 + 0.9904844761 0.9900665879 0.9548255801 + 0.9904927015 0.9900699854 0.9551169872 + 0.9905009866 0.9900733232 0.9554036856 + 0.9905092716 0.9900764227 0.9556903243 + 0.9905135036 0.9900786877 0.9559770226 + 0.9905136228 0.9900810122 0.9562636018 + 0.9905136824 0.9900833964 0.9565503001 + 0.9905138016 0.9900857210 0.9568368793 + 0.9905139208 0.9900879860 0.9571235776 + 0.9905139804 0.9900904298 0.9574102163 + 0.9905140996 0.9900926948 0.9576948285 + 0.9905142188 0.9900950193 0.9579765797 + 0.9905142188 0.9900968075 0.9582585096 + 0.9905145168 0.9900981784 0.9585403204 + 0.9905148149 0.9900994897 0.9588220716 + 0.9905151725 0.9901009202 0.9591038823 + 0.9905154705 0.9901022911 0.9593858123 + 0.9905158281 0.9901036024 0.9596676230 + 0.9905161262 0.9901049733 0.9599493742 + 0.9905164838 0.9901064038 0.9602293968 + 0.9905167818 0.9901077151 0.9605073929 + 0.9905170798 0.9901083708 0.9607855082 + 0.9905173779 0.9901089072 0.9610636234 + 0.9905176759 0.9901093245 0.9613416791 + 0.9905180931 0.9901098013 0.9616196752 + 0.9905183911 0.9901102185 0.9618977904 + 0.9905186892 0.9901106954 0.9621759057 + 0.9905189872 0.9901111126 0.9624540210 + 0.9905194044 0.9901115894 0.9627259970 + 0.9905197024 0.9901120067 0.9629914761 + 0.9905200005 0.9901124835 0.9632570148 + 0.9905202985 0.9901130199 0.9635224938 + 0.9905205965 0.9901136160 0.9637879729 + 0.9905210137 0.9901140928 0.9640535116 + 0.9905213118 0.9901146293 0.9643189907 + 0.9905216098 0.9901151061 0.9645845294 + 0.9905219078 0.9901155829 0.9648500085 + 0.9905223250 0.9901161194 0.9651170969 + 0.9905226231 0.9901167154 0.9653869867 + 0.9905229211 0.9901171923 0.9656568170 + 0.9905232191 0.9901177287 0.9659267068 + 0.9905235171 0.9901182055 0.9661965966 + 0.9905238748 0.9901186824 0.9664664268 + 0.9905241728 0.9901192188 0.9667363167 + 0.9905244708 0.9901198149 0.9670060873 + 0.9905248284 0.9901202917 0.9672759771 + 0.9905251861 0.9901208282 0.9675456285 + 0.9905254841 0.9901213050 0.9678133130 + 0.9905257821 0.9901217818 0.9680811167 + 0.9905260801 0.9901223183 0.9683488011 + 0.9905264974 0.9901229143 0.9686166048 + 0.9905267954 0.9901233912 0.9688842893 + 0.9905270934 0.9901239276 0.9691520929 + 0.9905273914 0.9901244044 0.9694197774 + 0.9905276895 0.9901248813 0.9696875811 + 0.9905281067 0.9901254177 0.9699553251 + 0.9905284047 0.9901258945 0.9702137709 + 0.9905287027 0.9901264906 0.9704701900 + 0.9905290008 0.9901270270 0.9707266092 + 0.9905294180 0.9901275039 0.9709830284 + 0.9905297160 0.9901279807 0.9712393880 + 0.9905300140 0.9901285172 0.9714958072 + 0.9905303121 0.9901289940 0.9717522264 + 0.9905306101 0.9901295900 0.9720085859 + 0.9905310273 0.9901301265 0.9722651243 + 0.9905313253 0.9901306033 0.9725131989 + 0.9905316234 0.9901310802 0.9727489948 + 0.9905319214 0.9901316166 0.9729847908 + 0.9905322790 0.9901320934 0.9732205868 + 0.9905325770 0.9901326895 0.9734563828 + 0.9905328751 0.9901332259 0.9736921787 + 0.9905331731 0.9901337028 0.9739279747 + 0.9905334711 0.9901341796 0.9741637707 + 0.9905338883 0.9901347160 0.9743996263 + 0.9905341864 0.9901351929 0.9746354222 + 0.9905344844 0.9901357889 0.9748558998 + 0.9905347824 0.9901363254 0.9750742912 + 0.9905351996 0.9901368022 0.9752926230 + 0.9905354977 0.9901372790 0.9755108953 + 0.9905357957 0.9901378155 0.9757292867 + 0.9905360937 0.9901382923 0.9759476185 + 0.9905363917 0.9901388884 0.9761658907 + 0.9905368090 0.9901394248 0.9763842821 + 0.9905371070 0.9901399016 0.9766026139 + 0.9905374050 0.9901403785 0.9768189192 + 0.9905377030 0.9901409149 0.9770290852 + 0.9905450940 0.9901413918 0.9772393107 + 0.9905598164 0.9901419282 0.9774494767 + 0.9905744791 0.9901425242 0.9776597023 + 0.9905890822 0.9901430011 0.9778699279 + 0.9906038046 0.9901434779 0.9780800939 + 0.9906185269 0.9901440144 0.9782903194 + 0.9906330705 0.9901444912 0.9785004854 + 0.9906477928 0.9901450276 0.9787107110 + 0.9906623960 0.9901456237 0.9789108038 + 0.9906771183 0.9901518822 0.9791017771 + 0.9906994104 0.9901679754 0.9792928100 + 0.9907227159 0.9901841879 0.9794839025 + 0.9907460213 0.9902004004 0.9796748757 + 0.9907693267 0.9902166128 0.9798659086 + 0.9907925725 0.9902328253 0.9800568819 + 0.9908158779 0.9902489781 0.9802479148 + 0.9908391833 0.9902651906 0.9804388881 + 0.9908624887 0.9902812839 0.9806299210 + 0.9908857942 0.9902974963 0.9808201790 + 0.9909120202 0.9903177023 0.9810101986 + 0.9909530282 0.9903402925 0.9812002182 + 0.9909939766 0.9903628826 0.9813901782 + 0.9910349250 0.9903854728 0.9815803170 + 0.9910758734 0.9904081225 0.9817702770 + 0.9911168814 0.9904307127 0.9819602966 + 0.9911578298 0.9904533029 0.9821503162 + 0.9911987782 0.9904758930 0.9823402762 + 0.9912397861 0.9904984832 0.9825302958 + 0.9912806749 0.9905210733 0.9827157855 + 0.9913269281 0.9905534983 0.9828997254 + 0.9913831949 0.9905890822 0.9830836058 + 0.9914394021 0.9906246066 0.9832674265 + 0.9914957285 0.9906600714 0.9834513068 + 0.9915518761 0.9906957150 0.9836351871 + 0.9916082025 0.9907311797 0.9838191271 + 0.9916644096 0.9907668233 0.9840028882 + 0.9917206764 0.9908022881 0.9841868281 + 0.9917770028 0.9908378124 0.9843707085 + 0.9918332100 0.9908733964 0.9845507741 + 0.9918966293 0.9909213781 0.9847291708 + 0.9919701815 0.9909728169 0.9849076271 + 0.9920437932 0.9910240769 0.9850859046 + 0.9921175241 0.9910753965 0.9852643013 + 0.9921910763 0.9911267757 0.9854426980 + 0.9922646880 0.9911780953 0.9856210947 + 0.9923382998 0.9912294149 0.9857994914 + 0.9924119115 0.9912807941 0.9859778881 + 0.9924855232 0.9913321137 0.9861562848 + 0.9925590754 0.9913833737 0.9863287807 + 0.9926332831 0.9914423823 0.9864963889 + 0.9927082062 0.9915043712 0.9866638780 + 0.9927831888 0.9915664196 0.9868314266 + 0.9928581119 0.9916284084 0.9869989157 + 0.9929330945 0.9916903973 0.9871664047 + 0.9930080175 0.9917523861 0.9873338938 + 0.9930830002 0.9918143153 0.9875013828 + 0.9931579828 0.9918763041 0.9876688719 + 0.9932329059 0.9919382930 0.9878364801 + 0.9933078885 0.9920002818 0.9879997969 + 0.9933835864 0.9920656085 0.9881514907 + 0.9934617877 0.9921336174 0.9883031845 + 0.9935399890 0.9922016859 0.9884548783 + 0.9936181903 0.9922698140 0.9886065722 + 0.9936962724 0.9923378825 0.9887583256 + 0.9937744737 0.9924060106 0.9889100194 + 0.9938526750 0.9924740791 0.9890617132 + 0.9939308763 0.9925422072 0.9892134070 + 0.9940090775 0.9926102757 0.9893651009 + 0.9940872788 0.9926782846 0.9895167947 + 0.9941654801 0.9927479029 0.9896612763 + 0.9942458868 0.9928218722 0.9898049235 + 0.9943264127 0.9928959012 0.9899485111 + 0.9944068789 0.9929698706 0.9900920987 + 0.9944872856 0.9930438995 0.9902356863 + 0.9945678115 0.9931179881 0.9903792739 + 0.9946482778 0.9931920171 0.9905229211 + 0.9947286844 0.9932659864 0.9906665087 + 0.9948092103 0.9933400154 0.9908100963 + 0.9948896766 0.9934139848 0.9909536839 + 0.9949702024 0.9934880137 0.9910960197 + 0.9950510263 0.9935687184 0.9912365079 + 0.9951320887 0.9936506152 0.9913768768 + 0.9952132106 0.9937325120 0.9915173054 + 0.9952942729 0.9938144088 0.9916576743 + 0.9953753948 0.9938963056 0.9917981029 + 0.9954563975 0.9939782023 0.9919384718 + 0.9955375195 0.9940600991 0.9920789003 + 0.9956185818 0.9941418767 0.9922193885 + 0.9956997037 0.9942237735 0.9923598170 + 0.9957808256 0.9943056703 0.9925001860 + 0.9958618283 0.9943904281 0.9926363230 + 0.9959421158 0.9944810271 0.9927713275 + 0.9960222840 0.9945715070 0.9929062724 + 0.9961025715 0.9946621060 0.9930412173 + 0.9961829185 0.9947527051 0.9931762218 + 0.9962632060 0.9948431849 0.9933112264 + 0.9963434935 0.9949337840 0.9934461117 + 0.9964237809 0.9950243235 0.9935811162 + 0.9965040088 0.9951149225 0.9937161207 + 0.9965842962 0.9952055216 0.9938511252 + 0.9966645837 0.9952960014 0.9939857125 + 0.9967439771 0.9953925014 0.9941160083 + 0.9968224764 0.9954916239 0.9942464232 + 0.9969010949 0.9955906868 0.9943767190 + 0.9969795942 0.9956899285 0.9945070148 + 0.9970582128 0.9957889915 0.9946373105 + 0.9971367121 0.9958881140 0.9947676063 + 0.9972152710 0.9959872961 0.9948979020 + 0.9972937703 0.9960864186 0.9950283170 + 0.9973723888 0.9961854815 0.9951586127 + 0.9974510074 0.9962846041 0.9952889085 + 0.9975295067 0.9963837862 0.9954196215 + 0.9976075292 0.9964898825 0.9955514073 + 0.9976853132 0.9965963960 0.9956831932 + 0.9977632165 0.9967029095 0.9958150983 + 0.9978411198 0.9968094230 0.9959468842 + 0.9979189038 0.9969159961 0.9960787296 + 0.9979968071 0.9970225096 0.9962105751 + 0.9980745912 0.9971290231 0.9963424206 + 0.9981524944 0.9972354770 0.9964742064 + 0.9982303977 0.9973421097 0.9966061115 + 0.9983081818 0.9974486232 0.9967378974 + 0.9983860850 0.9975554943 0.9968718290 + 0.9984629154 0.9976667166 0.9970114231 + 0.9985396862 0.9977778792 0.9971510768 + 0.9986165166 0.9978891015 0.9972906709 + 0.9986932874 0.9980002046 0.9974303246 + 0.9987701178 0.9981114268 0.9975699186 + 0.9988468885 0.9982225895 0.9977095723 + 0.9989237189 0.9983338118 0.9978492260 + 0.9990004897 0.9984449744 0.9979888201 + 0.9990773201 0.9985561967 0.9981284142 + 0.9991542101 0.9986674190 0.9982680082 + 0.9992305040 0.9987785816 0.9984086156 + 0.9993004799 0.9988896251 0.9985532761 + 0.9993703961 0.9990006089 0.9986979961 + 0.9994403720 0.9991117120 0.9988425970 + 0.9995102882 0.9992226958 0.9989873171 + 0.9995803237 0.9993337989 0.9991319776 + 0.9996501803 0.9994447827 0.9992766976 + 0.9997202158 0.9995558262 0.9994212985 + 0.9997900724 0.9996669292 0.9995660186 + 0.9998601079 0.9997779131 0.9997106791 + 0.9999300241 0.9998890162 0.9998552799 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_100_plus.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_100_plus.spi1d index 9b7575b8..cbc87831 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_100_plus.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_100_plus.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0004568259 0.0001897309 0.0001937342 - 0.0009166480 0.0003902519 0.0003960232 - 0.0013811550 0.0006009749 0.0006110691 - 0.0018588870 0.0008188499 0.0008411508 - 0.0023563609 0.0010459250 0.0010869150 - 0.0028804501 0.0012829941 0.0013466120 - 0.0034379531 0.0015326150 0.0016192500 - 0.0040225340 0.0017928330 0.0019048119 - 0.0046354202 0.0020639880 0.0022035320 - 0.0052711908 0.0023474649 0.0025181000 - 0.0059363251 0.0026404259 0.0028487111 - 0.0066329152 0.0029452001 0.0031957659 - 0.0073490841 0.0032632940 0.0035582529 - 0.0080807637 0.0035944299 0.0039348011 - 0.0088355178 0.0039396151 0.0043245698 - 0.0096289609 0.0043014390 0.0047304570 - 0.0104655102 0.0046766838 0.0051527582 - 0.0113350702 0.0050640670 0.0055932738 - 0.0122496197 0.0054649492 0.0060512018 - 0.0131988404 0.0058779810 0.0065282308 - 0.0141810300 0.0063044452 0.0070242109 - 0.0151987597 0.0067441980 0.0075386940 - 0.0162404794 0.0072005908 0.0080726901 - 0.0173083302 0.0076743681 0.0086258948 - 0.0184124205 0.0081653623 0.0091975229 - 0.0195518192 0.0086749326 0.0097900750 - 0.0207353197 0.0092027588 0.0103992401 - 0.0219628699 0.0097467797 0.0110272104 - 0.0232388396 0.0103067802 0.0116758104 - 0.0245534405 0.0108824596 0.0123488903 - 0.0259076506 0.0114777200 0.0130474502 - 0.0273041893 0.0120923202 0.0137724997 - 0.0287391301 0.0127261700 0.0145242196 - 0.0302102603 0.0133811496 0.0153033203 - 0.0317212492 0.0140584502 0.0161061808 - 0.0332731493 0.0147604104 0.0169263799 - 0.0348760188 0.0154822599 0.0177620295 - 0.0365183204 0.0162258204 0.0186164491 - 0.0382087417 0.0169929601 0.0194896292 - 0.0399495289 0.0177801996 0.0203795508 - 0.0417297594 0.0185851697 0.0212911796 - 0.0435510091 0.0194129106 0.0222245902 - 0.0454129390 0.0202547908 0.0231861304 - 0.0473076887 0.0211168006 0.0241758395 - 0.0492347218 0.0219969898 0.0251984708 - 0.0512037501 0.0229012799 0.0262466706 - 0.0532191209 0.0238268599 0.0273228493 - 0.0552755818 0.0247761197 0.0284204800 - 0.0573842004 0.0257481597 0.0295423996 - 0.0595382415 0.0267426297 0.0306846593 - 0.0617300794 0.0277668294 0.0318456888 - 0.0639634207 0.0288175102 0.0330268405 - 0.0662460178 0.0298932791 0.0342224501 - 0.0685783774 0.0309970807 0.0354311503 - 0.0709521696 0.0321253613 0.0366570689 - 0.0733637214 0.0332747698 0.0379007794 - 0.0758268237 0.0344457105 0.0391649790 - 0.0783338398 0.0356396101 0.0404515490 - 0.0808990374 0.0368562713 0.0417614505 - 0.0835125297 0.0380946510 0.0430966802 - 0.0861754268 0.0393532701 0.0444576107 - 0.0888888985 0.0406360887 0.0458458997 - 0.0916479379 0.0419454388 0.0472616889 - 0.0944517180 0.0432827994 0.0486994199 - 0.0972917899 0.0446463488 0.0501526110 - 0.1001828983 0.0460356399 0.0516319200 - 0.1031202003 0.0474530496 0.0531338789 - 0.1061026007 0.0488977507 0.0546585694 - 0.1091277003 0.0503737107 0.0562016517 - 0.1121969000 0.0518880300 0.0577665791 - 0.1153125986 0.0534419082 0.0593559109 - 0.1184772998 0.0550320297 0.0609692894 - 0.1216840968 0.0566530786 0.0626069382 - 0.1249388009 0.0583061315 0.0642700866 - 0.1282431036 0.0599966310 0.0659606308 - 0.1315928996 0.0617280900 0.0676777512 - 0.1349757016 0.0634948835 0.0694218278 - 0.1383993030 0.0652914569 0.0711889118 - 0.1418682933 0.0671163797 0.0729750469 - 0.1453785002 0.0689697564 0.0747864991 - 0.1489267945 0.0708598867 0.0766221583 - 0.1525138021 0.0727824494 0.0784773678 - 0.1561374068 0.0747363791 0.0803539827 - 0.1597927064 0.0767190233 0.0822619572 - 0.1634854972 0.0787282810 0.0842007920 - 0.1672230065 0.0807615295 0.0861660689 - 0.1710027009 0.0828175321 0.0881549865 - 0.1748190969 0.0849011689 0.0901617482 - 0.1786739975 0.0870237797 0.0921890587 - 0.1825643927 0.0891877636 0.0942407027 - 0.1864794940 0.0913877636 0.0963157490 - 0.1904242039 0.0936163589 0.0984156132 - 0.1944115013 0.0958681777 0.1005403996 - 0.1984315068 0.0981432199 0.1026867032 - 0.2024817020 0.1004460007 0.1048531979 - 0.2065692991 0.1027875021 0.1070401967 - 0.2106909007 0.1051672027 0.1092483029 - 0.2148406059 0.1075756028 0.1114758030 - 0.2190158069 0.1100080982 0.1137152985 - 0.2232162058 0.1124617979 0.1159678996 - 0.2274380028 0.1149377003 0.1182356998 - 0.2316682935 0.1174454018 0.1205220968 - 0.2359098047 0.1199733987 0.1228303015 - 0.2401736975 0.1225221008 0.1251548976 - 0.2444601953 0.1250932068 0.1274984032 - 0.2487654984 0.1276911050 0.1298629940 - 0.2530876994 0.1303067952 0.1322595030 - 0.2574402988 0.1329407990 0.1346741021 - 0.2618153989 0.1355946958 0.1371085942 - 0.2662160099 0.1382690966 0.1395681947 - 0.2706286907 0.1409652978 0.1420474052 - 0.2750388086 0.1436827034 0.1445465982 - 0.2794604897 0.1464273036 0.1470647007 - 0.2838951945 0.1492000073 0.1496022046 - 0.2883372903 0.1519946009 0.1521558017 - 0.2927843034 0.1548147053 0.1547247022 - 0.2972404063 0.1576558053 0.1573172063 - 0.3016996980 0.1605056971 0.1599285007 - 0.3061661124 0.1633636951 0.1625496000 - 0.3106426001 0.1662496030 0.1651808023 - 0.3151304126 0.1691479981 0.1678352952 - 0.3196105063 0.1720582992 0.1704951972 - 0.3240850866 0.1749927998 0.1731574982 - 0.3285588920 0.1779374033 0.1758297980 - 0.3330225945 0.1808930039 0.1785068065 - 0.3374676108 0.1838704050 0.1811906993 - 0.3418787122 0.1868537068 0.1838902980 - 0.3462742865 0.1898439974 0.1865974069 - 0.3506546915 0.1928507984 0.1893136948 - 0.3550148010 0.1958658993 0.1920381039 - 0.3593491912 0.1988967061 0.1947682947 - 0.3636716902 0.2019404024 0.1975080073 - 0.3679802120 0.2049918026 0.2002497017 - 0.3722729981 0.2080595046 0.2029945999 - 0.3765603900 0.2111341059 0.2057335973 - 0.3808422089 0.2142200023 0.2084708959 - 0.3850818872 0.2173251063 0.2112185955 - 0.3893063068 0.2204380035 0.2139669061 - 0.3935188055 0.2235597968 0.2167157978 - 0.3977065980 0.2266864032 0.2194723934 - 0.4018787146 0.2298216969 0.2222321928 - 0.4060066938 0.2329684943 0.2249837965 - 0.4101200104 0.2361215949 0.2277349979 - 0.4142099917 0.2392960936 0.2304856032 - 0.4182651937 0.2424751967 0.2332389951 - 0.4223062098 0.2456675023 0.2359952033 - 0.4263221025 0.2488580942 0.2387603074 - 0.4303199053 0.2520464957 0.2415284067 - 0.4342919886 0.2552388906 0.2442979962 - 0.4382177889 0.2584339082 0.2470681965 - 0.4421251118 0.2616392970 0.2498411983 - 0.4459949136 0.2648470998 0.2526099086 - 0.4498532116 0.2680524886 0.2553699017 - 0.4536815882 0.2712588012 0.2581270933 - 0.4574846029 0.2744691968 0.2608813047 - 0.4612646103 0.2776809931 0.2636286914 - 0.4650110900 0.2808963060 0.2663717866 - 0.4687466919 0.2841087878 0.2691045105 - 0.4724290967 0.2873168886 0.2718333900 - 0.4761005938 0.2905252874 0.2745406926 - 0.4797236025 0.2937338948 0.2772436142 - 0.4833352864 0.2969321907 0.2799426019 - 0.4869242013 0.3001225889 0.2826412022 - 0.4905020893 0.3033182919 0.2853288949 - 0.4940682054 0.3065170944 0.2880161107 - 0.4975984991 0.3097181022 0.2906906009 - 0.5011065006 0.3129202127 0.2933647931 - 0.5045886040 0.3161264956 0.2960147858 - 0.5080611110 0.3193351030 0.2986648083 - 0.5115216970 0.3225524127 0.3013044894 - 0.5149798989 0.3257744014 0.3039439917 - 0.5184119940 0.3289855123 0.3065662980 - 0.5218411088 0.3321900964 0.3091877103 - 0.5252186060 0.3353936970 0.3117879033 - 0.5285930037 0.3385963142 0.3143858016 - 0.5319072008 0.3417963088 0.3169586957 - 0.5352197886 0.3449935913 0.3195259869 - 0.5384925008 0.3481869996 0.3220764101 - 0.5417643785 0.3513746858 0.3246203065 - 0.5449957848 0.3545599878 0.3271425962 - 0.5482258201 0.3577390015 0.3296504021 - 0.5514274836 0.3609147966 0.3321473897 - 0.5546270013 0.3640691936 0.3346312940 - 0.5577927828 0.3672235906 0.3371109962 - 0.5609536767 0.3703644872 0.3395805061 - 0.5640918016 0.3735049069 0.3420476019 - 0.5672239065 0.3766363859 0.3444921076 - 0.5703287125 0.3797653019 0.3469367027 - 0.5734214783 0.3828806877 0.3493632078 - 0.5764908195 0.3859854937 0.3517870903 - 0.5795422792 0.3890849948 0.3541991115 - 0.5825762749 0.3921729028 0.3566043973 - 0.5855858922 0.3952594101 0.3590024114 - 0.5885831118 0.3983199000 0.3613883853 - 0.5915424228 0.4013803899 0.3637714088 - 0.5944979191 0.4044235051 0.3661235869 - 0.5973832011 0.4074612856 0.3684759140 - 0.6002684832 0.4104872942 0.3708046079 - 0.6031265855 0.4134992957 0.3731263876 - 0.6059790850 0.4165073037 0.3754419982 - 0.6088135242 0.4194850922 0.3777495921 - 0.6116350889 0.4224627912 0.3800561130 - 0.6144289970 0.4254232049 0.3823485970 - 0.6171646714 0.4283778071 0.3846412003 - 0.6198990941 0.4313283861 0.3869284093 - 0.6225900054 0.4342721999 0.3892132044 - 0.6252809763 0.4372158945 0.3914948106 - 0.6279500127 0.4401437044 0.3937678039 - 0.6306102276 0.4430705905 0.3960408866 - 0.6332606077 0.4459845126 0.3983030021 - 0.6358942986 0.4488858879 0.4005627036 - 0.6385279298 0.4517867863 0.4028213918 - 0.6411197186 0.4546740949 0.4050782025 - 0.6437116265 0.4575614929 0.4073350132 - 0.6462585926 0.4604415894 0.4095784128 - 0.6487771869 0.4633156955 0.4118196964 - 0.6512870789 0.4661892056 0.4140588045 - 0.6537609100 0.4690420032 0.4162935913 - 0.6562346816 0.4718948007 0.4185284078 - 0.6586757898 0.4747399092 0.4207589924 - 0.6611058712 0.4775764942 0.4229885936 - 0.6635295749 0.4804131091 0.4252167940 - 0.6659365296 0.4832342863 0.4274396896 - 0.6683434248 0.4860537946 0.4296625853 - 0.6707113981 0.4888609052 0.4318814874 - 0.6730682254 0.4916389883 0.4340975881 - 0.6754177213 0.4944170117 0.4363138080 - 0.6777454019 0.4971829057 0.4385361969 - 0.6800730824 0.4999426007 0.4407587945 - 0.6823896766 0.5027021766 0.4429790080 - 0.6847013831 0.5054432154 0.4451938868 - 0.6870059967 0.5081841946 0.4474087059 - 0.6892570853 0.5109151006 0.4496183991 - 0.6915082932 0.5136244893 0.4518249035 - 0.6937376857 0.5163338184 0.4540314078 - 0.6959462762 0.5190213919 0.4562326968 - 0.6981548071 0.5216943026 0.4584333897 - 0.7003312111 0.5243672132 0.4606317878 - 0.7025029063 0.5270034075 0.4628174901 - 0.7046647072 0.5296329856 0.4650033116 - 0.7067890167 0.5322594047 0.4671837091 - 0.7089133263 0.5348445177 0.4693551958 - 0.7110230923 0.5374295712 0.4715267122 - 0.7131193280 0.5399996042 0.4736973047 - 0.7152153850 0.5425274968 0.4758673906 - 0.7172769904 0.5450553894 0.4780373871 - 0.7193294168 0.5475718975 0.4801994860 - 0.7213792801 0.5500730276 0.4823586047 - 0.7233660817 0.5525740981 0.4845176935 - 0.7253528237 0.5550534725 0.4866720140 - 0.7273290753 0.5575155020 0.4888255000 - 0.7292746902 0.5599774718 0.4909788966 - 0.7312203050 0.5624241233 0.4931291044 - 0.7331498861 0.5648627281 0.4952791035 - 0.7350592017 0.5673012733 0.4974288940 - 0.7369683981 0.5697147846 0.4995673001 - 0.7388588190 0.5721186996 0.5017058253 - 0.7407366037 0.5745226145 0.5038431287 - 0.7426143289 0.5768790841 0.5059599280 - 0.7444639802 0.5792204738 0.5080766082 - 0.7463030815 0.5815619826 0.5101922750 - 0.7481421232 0.5838884115 0.5122897029 - 0.7499330044 0.5862100720 0.5143870711 - 0.7517132163 0.5885317922 0.5164837837 - 0.7534933090 0.5908361077 0.5185531974 - 0.7552289963 0.5931339860 0.5206226110 - 0.7569586039 0.5954319835 0.5226920247 - 0.7586882710 0.5977076292 0.5247408152 - 0.7603802085 0.5999724269 0.5267890096 - 0.7620685101 0.6022372842 0.5288370848 - 0.7637568116 0.6044870019 0.5308629870 - 0.7654249072 0.6067258120 0.5328854918 - 0.7670909762 0.6089646220 0.5349079967 - 0.7687572241 0.6111888885 0.5369161963 - 0.7703763843 0.6133955717 0.5389193892 - 0.7719892263 0.6156023145 0.5409225225 - 0.7736020088 0.6177954078 0.5429117084 - 0.7751833797 0.6199551821 0.5448905230 - 0.7767575979 0.6221150160 0.5468692780 - 0.7783316970 0.6242699027 0.5488427281 - 0.7799007893 0.6263784170 0.5508071780 - 0.7814676762 0.6284868121 0.5527716875 - 0.7830346823 0.6305952072 0.5547339916 - 0.7845696211 0.6326767802 0.5566831827 - 0.7860810757 0.6347541213 0.5586323738 - 0.7875925899 0.6368314028 0.5605816245 - 0.7890849113 0.6388925910 0.5625146031 - 0.7905498147 0.6409428716 0.5644453764 - 0.7920145988 0.6429932117 0.5663762093 - 0.7934713960 0.6450356841 0.5682945848 - 0.7948976159 0.6470593810 0.5702040195 - 0.7963238955 0.6490830779 0.5721132755 - 0.7977501154 0.6511068940 0.5740166903 - 0.7991691232 0.6531022191 0.5759012103 - 0.8005877733 0.6550961733 0.5777856112 - 0.8020066023 0.6570903063 0.5796700716 - 0.8034045100 0.6590561271 0.5815389156 - 0.8047934771 0.6610018015 0.5834053159 - 0.8061826229 0.6629474759 0.5852717757 - 0.8075594902 0.6648828983 0.5871319771 - 0.8089175820 0.6667698026 0.5889847875 - 0.8102757931 0.6686565876 0.5908375978 - 0.8116320968 0.6705434918 0.5926902294 - 0.8129532933 0.6724057198 0.5945152044 - 0.8142744899 0.6742588282 0.5963401794 - 0.8155958056 0.6761118174 0.5981652141 - 0.8168948889 0.6779593229 0.5999823809 - 0.8181831241 0.6797894835 0.6017925739 - 0.8194714189 0.6816197038 0.6036027074 - 0.8207564950 0.6834499836 0.6054127812 - 0.8220325708 0.6852660775 0.6072114706 - 0.8233085871 0.6870769262 0.6090102196 - 0.8245846033 0.6888877153 0.6108089089 - 0.8258383870 0.6906918287 0.6126012802 - 0.8270868063 0.6924620271 0.6143857241 - 0.8283351064 0.6942321062 0.6161699891 - 0.8295804858 0.6960023046 0.6179543734 - 0.8308194280 0.6977444887 0.6197059155 - 0.8320583105 0.6994649768 0.6214510202 - 0.8332971931 0.7011855245 0.6231961846 - 0.8345112801 0.7029060125 0.6249343753 - 0.8357185721 0.7046008110 0.6266446710 - 0.8369258046 0.7062937021 0.6283550262 - 0.8381229043 0.7079864740 0.6300653219 - 0.8392837048 0.7096688151 0.6317604184 - 0.8404445052 0.7113192081 0.6334416270 - 0.8416053057 0.7129697204 0.6351227164 - 0.8427382708 0.7146201134 0.6368038058 - 0.8438537121 0.7162567973 0.6384651065 - 0.8449690938 0.7178813815 0.6401215792 - 0.8460845947 0.7195060253 0.6417779922 - 0.8471649885 0.7211306095 0.6434329748 - 0.8482443094 0.7227483988 0.6450561881 - 0.8493235707 0.7243642807 0.6466792822 - 0.8503887057 0.7259801030 0.6483023763 - 0.8514184952 0.7275959253 0.6499150991 - 0.8524482250 0.7291961908 0.6514953971 - 0.8534778953 0.7307960987 0.6530758142 - 0.8544930816 0.7323961258 0.6546561122 - 0.8554970026 0.7339910865 0.6562228203 - 0.8565009236 0.7355558872 0.6577693820 - 0.8575047851 0.7371206880 0.6593158841 - 0.8584917784 0.7386854887 0.6608623862 - 0.8594745994 0.7402415276 0.6623889208 - 0.8604573011 0.7417721748 0.6638967991 - 0.8614401221 0.7433028817 0.6654046774 - 0.8623782992 0.7448335886 0.6669126153 - 0.8633162975 0.7463521957 0.6683937907 - 0.8642541766 0.7478467226 0.6698569059 - 0.8651856780 0.7493410707 0.6713200212 - 0.8660802841 0.7508355975 0.6727830768 - 0.8669748902 0.7523164153 0.6742259264 - 0.8678696156 0.7537738085 0.6756564975 - 0.8687558770 0.7552313209 0.6770871878 - 0.8696188927 0.7566887140 0.6785178185 - 0.8704819083 0.7581344843 0.6799271703 - 0.8713449240 0.7595599294 0.6813234091 - 0.8722006083 0.7609851956 0.6827195883 - 0.8730415106 0.7624105215 0.6841157079 - 0.8738824725 0.7638270259 0.6854910851 - 0.8747234941 0.7652239203 0.6868504286 - 0.8755509257 0.7666208744 0.6882097721 - 0.8763540983 0.7680178285 0.6895691156 - 0.8771572709 0.7694097161 0.6909182072 - 0.8779605031 0.7707833052 0.6922556162 - 0.8787578940 0.7721568942 0.6935930252 - 0.8795443177 0.7735304832 0.6949303746 - 0.8803306818 0.7749016285 0.6962568164 - 0.8811172247 0.7762401104 0.6975601912 - 0.8818970919 0.7775787115 0.6988636851 - 0.8826605082 0.7789173126 0.7001671195 - 0.8834239841 0.7802559137 0.7014654875 - 0.8841875196 0.7815645933 0.7027323842 - 0.8849436045 0.7828695774 0.7039992213 - 0.8856664896 0.7841745019 0.7052659988 - 0.8863893151 0.7854794860 0.7065327764 - 0.8871122003 0.7867677808 0.7077702880 - 0.8878350854 0.7880474925 0.7090048194 - 0.8885604143 0.7893270850 0.7102392912 - 0.8892856836 0.7906067967 0.7114737034 - 0.8900108933 0.7918794155 0.7126892805 - 0.8907362223 0.7931404114 0.7138935924 - 0.8914461732 0.7944014072 0.7150977850 - 0.8921533227 0.7956624031 0.7163020968 - 0.8928604126 0.7969211936 0.7174999714 - 0.8935676217 0.7981446981 0.7186828256 - 0.8942708969 0.7993682027 0.7198656797 - 0.8949717283 0.8005917072 0.7210485935 - 0.8956726193 0.8018152118 0.7222315073 - 0.8963733912 0.8030143976 0.7233734727 - 0.8970720768 0.8042024970 0.7245125771 - 0.8977661729 0.8053905964 0.7256516814 - 0.8984603286 0.8065786958 0.7267907262 - 0.8991544247 0.8077601790 0.7279145122 - 0.8998485208 0.8089237809 0.7290235758 - 0.9005149007 0.8100873828 0.7301326990 - 0.9011809230 0.8112509847 0.7312418222 - 0.9018470049 0.8124147058 0.7323492169 - 0.9025130272 0.8135333061 0.7334213853 - 0.9031683207 0.8146411777 0.7344936728 - 0.9038167000 0.8157489896 0.7355659008 - 0.9044650793 0.8168568015 0.7366381288 - 0.9051135182 0.8179541826 0.7376983166 - 0.9057582021 0.8190261722 0.7387477160 - 0.9063869119 0.8200982213 0.7397971153 - 0.9070156217 0.8211702108 0.7408465743 - 0.9076442122 0.8222423196 0.7418959737 - 0.9082729220 0.8232967854 0.7429301143 - 0.9088765979 0.8243448734 0.7439635992 - 0.9094702005 0.8253930211 0.7449972034 - 0.9100638032 0.8264411092 0.7460306883 - 0.9106575251 0.8274878860 0.7470514178 - 0.9112505913 0.8285259008 0.7480484843 - 0.9118415713 0.8295637965 0.7490454912 - 0.9124326706 0.8306018114 0.7500426173 - 0.9130237103 0.8316397071 0.7510396242 - 0.9136146903 0.8326591849 0.7520161271 - 0.9142035246 0.8336567283 0.7529833913 - 0.9147909284 0.8346542120 0.7539507151 - 0.9153783917 0.8356516957 0.7549179792 - 0.9159657955 0.8366491795 0.7558851838 - 0.9165533185 0.8376235962 0.7568265200 - 0.9171171784 0.8385900259 0.7577672005 - 0.9176810980 0.8395562768 0.7587078214 - 0.9182450771 0.8405227065 0.7596483827 - 0.9188089967 0.8414890766 0.7605820894 - 0.9193634987 0.8424319029 0.7614898086 - 0.9198989272 0.8433743119 0.7623975277 - 0.9204342961 0.8443166018 0.7633051276 - 0.9209697843 0.8452588916 0.7642127872 - 0.9215052128 0.8461968899 0.7651119828 - 0.9220255017 0.8471127748 0.7659983039 - 0.9225360751 0.8480286002 0.7668846250 - 0.9230468273 0.8489444852 0.7677708864 - 0.9235574007 0.8498603106 0.7686572075 - 0.9240680933 0.8507688046 0.7695332766 - 0.9245592952 0.8516598940 0.7703999877 - 0.9250469208 0.8525511026 0.7712668180 - 0.9255344272 0.8534421921 0.7721335888 - 0.9260219932 0.8543332815 0.7730002999 - 0.9265089035 0.8552088737 0.7738537788 - 0.9269595146 0.8560588956 0.7746983767 - 0.9274101257 0.8569089174 0.7755429745 - 0.9278606176 0.8577588797 0.7763875723 - 0.9283112288 0.8586089015 0.7772322297 - 0.9287610054 0.8594486117 0.7780702710 - 0.9292058945 0.8602731228 0.7789046168 - 0.9296509027 0.8610975742 0.7797389030 - 0.9300957918 0.8619220853 0.7805731893 - 0.9305406809 0.8627465963 0.7814074755 - 0.9309821129 0.8635640740 0.7822362781 - 0.9314101934 0.8643702269 0.7830613256 - 0.9318383932 0.8651763201 0.7838863730 - 0.9322664738 0.8659824133 0.7847114205 - 0.9326946139 0.8667885065 0.7855364084 - 0.9331154227 0.8675866127 0.7863515019 - 0.9335113764 0.8683658838 0.7871572971 - 0.9339073896 0.8691452146 0.7879629731 - 0.9343034029 0.8699244857 0.7887688279 - 0.9346994758 0.8707038164 0.7895745039 - 0.9350925088 0.8714814186 0.7903773189 - 0.9354733229 0.8722500205 0.7911751866 - 0.9358540773 0.8730186224 0.7919729948 - 0.9362348914 0.8737872243 0.7927709222 - 0.9366155863 0.8745558262 0.7935687900 - 0.9369941950 0.8753244281 0.7943630815 - 0.9373536110 0.8760707974 0.7951408029 - 0.9377130270 0.8768165112 0.7959185839 - 0.9380723834 0.8775621057 0.7966964245 - 0.9384316802 0.8783078194 0.7974742055 - 0.9387910962 0.8790534139 0.7982519865 - 0.9391242266 0.8797795773 0.7990127802 - 0.9394561052 0.8804981112 0.7997723222 - 0.9397879839 0.8812165260 0.8005318046 - 0.9401199222 0.8819350004 0.8012912869 - 0.9404518008 0.8826534748 0.8020508289 - 0.9407855868 0.8833674192 0.8027989864 - 0.9411200881 0.8840746284 0.8035404086 - 0.9414545298 0.8847818971 0.8042817116 - 0.9417889714 0.8854891062 0.8050230145 - 0.9421234727 0.8861963749 0.8057643771 - 0.9424526095 0.8869026899 0.8065025806 - 0.9427763224 0.8875902891 0.8072319031 - 0.9430999160 0.8882780075 0.8079612255 - 0.9434236288 0.8889656067 0.8086904883 - 0.9437472224 0.8896532059 0.8094198108 - 0.9440696836 0.8903409243 0.8101490736 - 0.9443870783 0.8910048008 0.8108692765 - 0.9447045922 0.8916543722 0.8115875721 - 0.9450219870 0.8923041224 0.8123059273 - 0.9453393817 0.8929538131 0.8130242229 - 0.9456567764 0.8936033845 0.8137425184 - 0.9459626079 0.8942502141 0.8144533038 - 0.9462655187 0.8948795795 0.8151500821 - 0.9465684891 0.8955091238 0.8158469200 - 0.9468713999 0.8961384892 0.8165438175 - 0.9471743703 0.8967679739 0.8172405958 - 0.9474688172 0.8973973989 0.8179373741 - 0.9477491975 0.8980208039 0.8186287880 - 0.9480295777 0.8986400962 0.8193188906 - 0.9483100176 0.8992593884 0.8200089931 - 0.9485903978 0.8998786807 0.8206989765 - 0.9488707781 0.9004979730 0.8213890791 - 0.9491494298 0.9011172056 0.8220766783 - 0.9494279027 0.9017202854 0.8227545023 - 0.9497063160 0.9023234844 0.8234323859 - 0.9499847889 0.9029266238 0.8241102099 - 0.9502632022 0.9035297036 0.8247880936 - 0.9505476952 0.9041329026 0.8254659176 - 0.9508438706 0.9047262073 0.8261309862 - 0.9511401057 0.9053006768 0.8267852068 - 0.9514362812 0.9058750868 0.8274394274 - 0.9517325163 0.9064496160 0.8280935884 - 0.9520286918 0.9070240855 0.8287478089 - 0.9523310065 0.9075986147 0.8294020295 - 0.9526355267 0.9081652761 0.8300411105 - 0.9529399872 0.9087274075 0.8306767941 - 0.9532445073 0.9092894197 0.8313124776 - 0.9535490274 0.9098513722 0.8319482803 - 0.9538534284 0.9104133844 0.8325840235 - 0.9541549087 0.9109753966 0.8332191706 - 0.9544565082 0.9115229845 0.8338392973 - 0.9547581077 0.9120678902 0.8344593048 - 0.9550595880 0.9126127958 0.8350793123 - 0.9553611875 0.9131575823 0.8356993794 - 0.9556581974 0.9137024879 0.8363193870 - 0.9559459090 0.9142473936 0.8369358778 - 0.9562335014 0.9147704840 0.8375366926 - 0.9565210938 0.9152935147 0.8381375074 - 0.9568086863 0.9158164859 0.8387383223 - 0.9570962787 0.9163393974 0.8393390775 - 0.9573791027 0.9168624282 0.8399398923 - 0.9576573968 0.9173836112 0.8405390978 - 0.9579358101 0.9178845882 0.8411338925 - 0.9582142234 0.9183856249 0.8417286873 - 0.9584925771 0.9188866019 0.8423236012 - 0.9587709904 0.9193875790 0.8429183960 - 0.9590389132 0.9198886156 0.8435133100 - 0.9593018293 0.9203869104 0.8441042900 - 0.9595646262 0.9208633900 0.8446862102 - 0.9598274827 0.9213399291 0.8452680111 - 0.9600902796 0.9218164086 0.8458498716 - 0.9603531957 0.9222928882 0.8464317918 - 0.9606087208 0.9227694273 0.8470137119 - 0.9608618021 0.9232439995 0.8475909233 - 0.9611148834 0.9236922264 0.8481546044 - 0.9613680243 0.9241403937 0.8487182856 - 0.9616211057 0.9245886207 0.8492819071 - 0.9618741870 0.9250367880 0.8498455882 - 0.9621182084 0.9254850149 0.8504093289 - 0.9623593092 0.9259333014 0.8509712815 - 0.9626005292 0.9263677001 0.8515232801 - 0.9628416896 0.9268016815 0.8520752788 - 0.9630827904 0.9272356033 0.8526272774 - 0.9633240104 0.9276695848 0.8531792760 - 0.9635608792 0.9281035066 0.8537312746 - 0.9637960792 0.9285374880 0.8542832732 - 0.9640312791 0.9289491773 0.8548194766 - 0.9642664790 0.9293550849 0.8553553224 - 0.9645017982 0.9297609925 0.8558911085 - 0.9647369981 0.9301669002 0.8564267755 - 0.9649640918 0.9305728078 0.8569626212 - 0.9651858807 0.9309787154 0.8574984074 - 0.9654077888 0.9313749075 0.8580273986 - 0.9656295776 0.9317634106 0.8585538864 - 0.9658514261 0.9321519136 0.8590804935 - 0.9660732150 0.9325404763 0.8596069813 - 0.9662867188 0.9329289794 0.8601335287 - 0.9664887190 0.9333174825 0.8606600761 - 0.9666907787 0.9337003827 0.8611857295 - 0.9668927789 0.9340664744 0.8617103100 - 0.9670948982 0.9344326258 0.8622347713 - 0.9672968984 0.9347985983 0.8627592921 - 0.9674957991 0.9351646900 0.8632838130 - 0.9676799178 0.9355307817 0.8638082743 - 0.9678639174 0.9358968735 0.8643323183 - 0.9680479169 0.9362515807 0.8648458719 - 0.9682319164 0.9366047978 0.8653594851 - 0.9684159756 0.9369578958 0.8658730984 - 0.9685999751 0.9373109937 0.8663867116 - 0.9687842727 0.9376642108 0.8669003248 - 0.9689685702 0.9380173087 0.8674138784 - 0.9691529274 0.9383693933 0.8679202795 - 0.9693372250 0.9387202263 0.8684219718 - 0.9695215225 0.9390708804 0.8689237833 - 0.9697058201 0.9394217134 0.8694254756 - 0.9698892236 0.9397724271 0.8699272275 - 0.9700716734 0.9401232004 0.8704289794 - 0.9702541232 0.9404739141 0.8709301949 - 0.9704365134 0.9408150911 0.8714262247 - 0.9706190228 0.9411553741 0.8719220757 - 0.9708014131 0.9414957166 0.8724181056 - 0.9709836841 0.9418359995 0.8729140759 - 0.9711577892 0.9421764016 0.8734099865 - 0.9713318944 0.9425166845 0.8739060163 - 0.9715059996 0.9428496957 0.8744006157 - 0.9716801047 0.9431682229 0.8748937249 - 0.9718542099 0.9434866905 0.8753868937 - 0.9720283747 0.9438052177 0.8758801222 - 0.9721953273 0.9441236854 0.8763732910 - 0.9723548889 0.9444422722 0.8768664002 - 0.9725145102 0.9447607994 0.8773596287 - 0.9726741910 0.9450662732 0.8778408170 - 0.9728338122 0.9453657866 0.8783197999 - 0.9729933739 0.9456652999 0.8787987828 - 0.9731529951 0.9459646940 0.8792778254 - 0.9732946754 0.9462642074 0.8797568083 - 0.9734343886 0.9465637207 0.8802356720 - 0.9735741019 0.9468631744 0.8807138205 - 0.9737136960 0.9471693039 0.8811848164 - 0.9738534093 0.9474757910 0.8816556931 - 0.9739931226 0.9477822781 0.8821266890 - 0.9741299152 0.9480888247 0.8825976849 - 0.9742575288 0.9483953118 0.8830686212 - 0.9743850827 0.9487017989 0.8835396171 - 0.9745125771 0.9490079880 0.8840103149 - 0.9746401906 0.9493122101 0.8844805956 - 0.9747678041 0.9496163726 0.8849508166 - 0.9748952985 0.9499207139 0.8854210973 - 0.9750214219 0.9502248764 0.8858913183 - 0.9751458764 0.9505292177 0.8863615990 - 0.9752703905 0.9508333802 0.8868318200 - 0.9753949046 0.9511339068 0.8872954845 - 0.9755194783 0.9514247775 0.8877508044 - 0.9756439924 0.9517157078 0.8882061243 - 0.9757685065 0.9520065784 0.8886613846 - 0.9758896232 0.9522975087 0.8891167045 - 0.9760090113 0.9525884986 0.8895720243 - 0.9761283994 0.9528794289 0.8900272846 - 0.9762479067 0.9531695247 0.8904743195 - 0.9763672948 0.9534580112 0.8909124732 - 0.9764866829 0.9537466168 0.8913506866 - 0.9766061902 0.9540351033 0.8917888999 - 0.9767202735 0.9543237090 0.8922271132 - 0.9768331051 0.9546121955 0.8926653266 - 0.9769458771 0.9549006820 0.8931034803 - 0.9770585895 0.9551869035 0.8935418129 - 0.9771714211 0.9554678202 0.8939803243 - 0.9772841930 0.9557486773 0.8944187760 - 0.9773970246 0.9560295939 0.8948572874 - 0.9775123000 0.9563105106 0.8952959180 - 0.9776281118 0.9565914273 0.8957344294 - 0.9777439237 0.9568722844 0.8961728811 - 0.9778596759 0.9571502805 0.8966065049 - 0.9779754877 0.9574174285 0.8970314264 - 0.9780912995 0.9576845169 0.8974562883 - 0.9782071114 0.9579516053 0.8978813291 - 0.9783318043 0.9582186937 0.8983061910 - 0.9784587026 0.9584857821 0.8987311125 - 0.9785856009 0.9587529898 0.8991559744 - 0.9787124991 0.9590197206 0.8995798230 - 0.9788395166 0.9592756033 0.8999984860 - 0.9789664149 0.9595314860 0.9004172087 - 0.9790933132 0.9597874284 0.9008359909 - 0.9792243838 0.9600433111 0.9012547135 - 0.9793574214 0.9602991939 0.9016733766 - 0.9794905186 0.9605550766 0.9020922184 - 0.9796234965 0.9608110189 0.9025108814 - 0.9797564745 0.9610598087 0.9029234052 - 0.9798895717 0.9613065720 0.9033355117 - 0.9800226092 0.9615533948 0.9037474990 - 0.9801558256 0.9618002176 0.9041596055 - 0.9802892208 0.9620471001 0.9045715928 - 0.9804226160 0.9622939229 0.9049835801 - 0.9805560112 0.9625406861 0.9053956866 - 0.9806892872 0.9627807736 0.9057967067 - 0.9808226824 0.9630126953 0.9061899185 - 0.9809560776 0.9632446766 0.9065830708 - 0.9810881019 0.9634765983 0.9069762826 - 0.9812154174 0.9637085199 0.9073694944 - 0.9813426733 0.9639403820 0.9077627063 - 0.9814701080 0.9641724229 0.9081559181 - 0.9815974236 0.9644042850 0.9085468054 - 0.9817246795 0.9646360278 0.9089267254 - 0.9818521142 0.9648677111 0.9093065262 - 0.9819794297 0.9650995135 0.9096863866 - 0.9821053743 0.9653311968 0.9100661874 - 0.9822311997 0.9655628800 0.9104459882 - 0.9823570251 0.9657946825 0.9108259082 - 0.9824826717 0.9660264254 0.9112057090 - 0.9826084971 0.9662542939 0.9115757942 - 0.9827343225 0.9664790034 0.9119412899 - 0.9828600883 0.9667037129 0.9123067856 - 0.9829840064 0.9669284225 0.9126722217 - 0.9831055999 0.9671530724 0.9130377173 - 0.9832271934 0.9673777819 0.9134030938 - 0.9833487868 0.9676026106 0.9137685895 - 0.9834703803 0.9678273201 0.9141318202 - 0.9835919738 0.9680370092 0.9144750237 - 0.9837136269 0.9682462811 0.9148182273 - 0.9838352203 0.9684554935 0.9151614904 - 0.9839490056 0.9686648250 0.9155046940 - 0.9840624928 0.9688740969 0.9158478975 - 0.9841759801 0.9690833092 0.9161911011 - 0.9842895269 0.9692925811 0.9165343046 - 0.9844030142 0.9694945812 0.9168791175 - 0.9845163822 0.9696804285 0.9172258973 - 0.9846299291 0.9698662758 0.9175727963 - 0.9847432971 0.9700521231 0.9179195762 - 0.9848564267 0.9702379704 0.9182664156 - 0.9849694967 0.9704238176 0.9186133146 - 0.9850826859 0.9706097245 0.9189600945 - 0.9851958156 0.9707955718 0.9193068743 - 0.9853088856 0.9709764719 0.9196500182 - 0.9854220748 0.9711537957 0.9199913740 - 0.9855352044 0.9713311195 0.9203327894 - 0.9856474996 0.9715083838 0.9206743240 - 0.9857596159 0.9716858268 0.9210156798 - 0.9858716130 0.9718630910 0.9213570952 - 0.9859836102 0.9720404148 0.9216985703 - 0.9860956073 0.9722176790 0.9220399857 - 0.9862076044 0.9723867774 0.9223676920 - 0.9863196015 0.9725531936 0.9226933122 - 0.9864315987 0.9727196097 0.9230188727 - 0.9865403175 0.9728860259 0.9233446121 - 0.9866489172 0.9730523825 0.9236701727 - 0.9867573977 0.9732187986 0.9239959121 - 0.9868658781 0.9733852148 0.9243214726 - 0.9869744182 0.9735515714 0.9246470928 - 0.9870830178 0.9737042189 0.9249747992 - 0.9871914983 0.9738543034 0.9253026247 - 0.9872993231 0.9740043283 0.9256303906 - 0.9874020219 0.9741542935 0.9259580970 - 0.9875046015 0.9743043184 0.9262859225 - 0.9876071811 0.9744542837 0.9266136885 - 0.9877098203 0.9746043086 0.9269413948 - 0.9878123999 0.9747542739 0.9272692204 - 0.9879150987 0.9748876095 0.9276003242 - 0.9880176783 0.9750179052 0.9279316068 - 0.9881187081 0.9751480818 0.9282628298 - 0.9882128835 0.9752783775 0.9285941124 - 0.9883071184 0.9754086733 0.9289252758 - 0.9884012938 0.9755390286 0.9292566180 - 0.9884955287 0.9756692052 0.9295879006 - 0.9885897040 0.9757995009 0.9299191236 - 0.9886838794 0.9759184122 0.9302431941 - 0.9887781143 0.9760339260 0.9305663109 - 0.9888703227 0.9761493802 0.9308894873 - 0.9889534116 0.9762648940 0.9312126040 - 0.9890366197 0.9763804078 0.9315357208 - 0.9891197085 0.9764959216 0.9318587780 - 0.9892027974 0.9766113758 0.9321818948 - 0.9892858863 0.9767268896 0.9325050116 - 0.9893689752 0.9768431187 0.9328215718 - 0.9894521236 0.9769597054 0.9331353903 - 0.9895340800 0.9770764112 0.9334493279 - 0.9896032810 0.9771929979 0.9337630868 - 0.9896724820 0.9773095846 0.9340769053 - 0.9897418022 0.9774262905 0.9343907833 - 0.9898110032 0.9775428772 0.9347046018 - 0.9898802042 0.9776595235 0.9350184798 - 0.9899495244 0.9777796865 0.9353330135 - 0.9900187254 0.9779065847 0.9356483817 - 0.9900879264 0.9780334830 0.9359638095 - 0.9901421070 0.9781603813 0.9362791181 - 0.9901946187 0.9782872796 0.9365944862 - 0.9902470708 0.9784141779 0.9369099140 - 0.9902997017 0.9785410762 0.9372252822 - 0.9903522134 0.9786679745 0.9375407100 - 0.9904047847 0.9787948132 0.9378561974 - 0.9904572964 0.9789273143 0.9381728768 - 0.9905098081 0.9790598154 0.9384894967 - 0.9905501008 0.9791921973 0.9388061762 - 0.9905833006 0.9793246984 0.9391229153 - 0.9906166196 0.9794571996 0.9394394755 - 0.9906498194 0.9795895815 0.9397562146 - 0.9906830192 0.9797220826 0.9400727749 - 0.9907162189 0.9798545837 0.9403895140 - 0.9907494187 0.9799873829 0.9406992793 - 0.9907826185 0.9801204205 0.9410055280 - 0.9908105135 0.9802533984 0.9413117170 - 0.9908241034 0.9803863764 0.9416180253 - 0.9908378124 0.9805194139 0.9419242144 - 0.9908514023 0.9806525111 0.9422304034 - 0.9908649921 0.9807854891 0.9425367117 - 0.9908785820 0.9809185266 0.9428429008 - 0.9908922911 0.9810513854 0.9431486726 - 0.9909058809 0.9811798930 0.9434512854 - 0.9909194708 0.9813084006 0.9437538981 - 0.9909344912 0.9814370275 0.9440565705 - 0.9909498096 0.9815654755 0.9443591833 - 0.9909651279 0.9816941023 0.9446619153 - 0.9909805059 0.9818226099 0.9449645281 - 0.9909958243 0.9819511771 0.9452670813 - 0.9910110831 0.9820796847 0.9455698133 - 0.9910264015 0.9822043180 0.9458690882 - 0.9910417199 0.9823232293 0.9461650848 - 0.9910593033 0.9824422002 0.9464610219 - 0.9910830855 0.9825611711 0.9467570186 - 0.9911068082 0.9826800823 0.9470530152 - 0.9911304712 0.9827991128 0.9473490119 - 0.9911543131 0.9829180241 0.9476450086 - 0.9911779761 0.9830369949 0.9479410052 - 0.9912016988 0.9831560254 0.9482370019 - 0.9912254214 0.9832714796 0.9485340714 - 0.9912492037 0.9833855033 0.9488316178 - 0.9912775159 0.9834995270 0.9491291046 - 0.9913085103 0.9836134911 0.9494264722 - 0.9913393855 0.9837275147 0.9497240186 - 0.9913703799 0.9838414788 0.9500213861 - 0.9914013147 0.9839556217 0.9503188729 - 0.9914323092 0.9840695858 0.9506164193 - 0.9914631844 0.9841836095 0.9509137869 - 0.9914941788 0.9842914939 0.9512110949 - 0.9915251136 0.9843987226 0.9515084028 - 0.9915609956 0.9845058918 0.9518057108 - 0.9915972948 0.9846131206 0.9521030188 - 0.9916337132 0.9847202897 0.9524002075 - 0.9916700125 0.9848275185 0.9526975155 - 0.9917063117 0.9849346876 0.9529948235 - 0.9917426705 0.9850419164 0.9532920718 - 0.9917790294 0.9851490855 0.9535887241 - 0.9918153286 0.9852461815 0.9538760185 - 0.9918519855 0.9853430986 0.9541633129 - 0.9918923974 0.9854400158 0.9544506073 - 0.9919328094 0.9855368733 0.9547379017 - 0.9919732213 0.9856337905 0.9550251961 - 0.9920135736 0.9857307076 0.9553123713 - 0.9920539856 0.9858275056 0.9555997252 - 0.9920943975 0.9859244227 0.9558870196 - 0.9921348095 0.9860212803 0.9561737180 - 0.9921752214 0.9861149192 0.9564520121 - 0.9922161102 0.9862084985 0.9567303061 - 0.9922596216 0.9863020778 0.9570086002 - 0.9923030138 0.9863955975 0.9572867751 - 0.9923465252 0.9864891768 0.9575651288 - 0.9923899770 0.9865828156 0.9578434229 - 0.9924334884 0.9866762757 0.9581217170 - 0.9924769998 0.9867699146 0.9583998919 - 0.9925205112 0.9868633747 0.9586781859 - 0.9925640225 0.9869536757 0.9589493871 - 0.9926077724 0.9870436192 0.9592202902 - 0.9926536083 0.9871335030 0.9594913125 - 0.9926993251 0.9872233868 0.9597622156 - 0.9927451015 0.9873132706 0.9600331187 - 0.9927908778 0.9874032140 0.9603040218 - 0.9928365946 0.9874930978 0.9605749249 - 0.9928823709 0.9875829816 0.9608458877 - 0.9929282069 0.9876729250 0.9611167908 - 0.9929739237 0.9877598286 0.9613807201 - 0.9930197001 0.9878454208 0.9616423249 - 0.9930669069 0.9879310131 0.9619039893 - 0.9931141138 0.9880164862 0.9621657133 - 0.9931613207 0.9881020784 0.9624273777 - 0.9932085276 0.9881876707 0.9626889825 - 0.9932556748 0.9882733226 0.9629507065 - 0.9933028221 0.9883589149 0.9632123709 - 0.9933500290 0.9884445071 0.9634739757 - 0.9933971763 0.9885308146 0.9637349844 - 0.9934443831 0.9886181951 0.9639952183 - 0.9934918880 0.9887055755 0.9642552733 - 0.9935395122 0.9887928963 0.9645155072 - 0.9935870767 0.9888802767 0.9647756219 - 0.9936347008 0.9889677167 0.9650356770 - 0.9936823249 0.9890550971 0.9652959108 - 0.9937298894 0.9891424775 0.9655560255 - 0.9937775135 0.9892299175 0.9658161998 - 0.9938250780 0.9893172979 0.9660758972 - 0.9938727021 0.9894027710 0.9663285017 - 0.9939200282 0.9894883037 0.9665812254 - 0.9939668179 0.9895737767 0.9668337703 - 0.9940136075 0.9896594286 0.9670864940 - 0.9940603971 0.9897449017 0.9673390985 - 0.9941071868 0.9898303747 0.9675918221 - 0.9941540956 0.9899159074 0.9678444266 - 0.9942008853 0.9900014997 0.9680969715 - 0.9942476749 0.9900869727 0.9683496952 - 0.9942945242 0.9901695848 0.9685934186 - 0.9943410754 0.9902492166 0.9688298702 - 0.9943857789 0.9903287888 0.9690665007 - 0.9944304228 0.9904084206 0.9693030119 - 0.9944751263 0.9904879928 0.9695395827 - 0.9945198298 0.9905676246 0.9697762132 - 0.9945644140 0.9906471968 0.9700127244 - 0.9946091175 0.9907268286 0.9702492952 - 0.9946537018 0.9908064008 0.9704858065 - 0.9946984053 0.9908859730 0.9707223773 - 0.9947431087 0.9909566045 0.9709470272 - 0.9947856069 0.9910261035 0.9711709023 - 0.9948266745 0.9910956025 0.9713948965 - 0.9948678017 0.9911652207 0.9716188908 - 0.9949088097 0.9912347198 0.9718428254 - 0.9949498773 0.9913042188 0.9720668197 - 0.9949908853 0.9913737774 0.9722908139 - 0.9950320125 0.9914432764 0.9725146890 - 0.9950730801 0.9915127754 0.9727386832 - 0.9951140881 0.9915797710 0.9729639292 - 0.9951552153 0.9916355014 0.9731928706 - 0.9951915145 0.9916911721 0.9734219909 - 0.9952275753 0.9917469025 0.9736511111 - 0.9952636957 0.9918025732 0.9738801122 - 0.9952998161 0.9918583035 0.9741091728 - 0.9953358769 0.9919139743 0.9743381739 - 0.9953719974 0.9919697046 0.9745672941 - 0.9954081178 0.9920253754 0.9747962952 - 0.9954441786 0.9920811057 0.9750254154 - 0.9954802990 0.9921348095 0.9752535820 - 0.9955152273 0.9921852946 0.9754807949 - 0.9955464005 0.9922357798 0.9757078886 - 0.9955775142 0.9922863245 0.9759351015 - 0.9956086874 0.9923368096 0.9761621952 - 0.9956398010 0.9923872948 0.9763892889 - 0.9956709743 0.9924377799 0.9766165018 - 0.9957020879 0.9924883246 0.9768435955 - 0.9957333207 0.9925388098 0.9770708084 - 0.9957643747 0.9925892949 0.9772979021 - 0.9957956076 0.9926403165 0.9775205851 - 0.9958270788 0.9926918149 0.9777389169 - 0.9958592057 0.9927433729 0.9779571891 - 0.9958912134 0.9927948713 0.9781755209 - 0.9959232807 0.9928464293 0.9783937931 - 0.9959554076 0.9928979874 0.9786121249 - 0.9959874749 0.9929494858 0.9788303971 - 0.9960194826 0.9930011034 0.9790487289 - 0.9960516095 0.9930526018 0.9792670012 - 0.9960836768 0.9931041002 0.9794852138 - 0.9961156845 0.9931588173 0.9797025919 - 0.9961478114 0.9932174087 0.9799190164 - 0.9961798787 0.9932761192 0.9801353216 - 0.9962120056 0.9933347702 0.9803516269 - 0.9962440133 0.9933934808 0.9805679917 - 0.9962760806 0.9934520721 0.9807842970 - 0.9963082075 0.9935107827 0.9810007215 - 0.9963402152 0.9935694933 0.9812170267 - 0.9963722825 0.9936280847 0.9814332724 - 0.9964044094 0.9936867952 0.9816496968 - 0.9964364767 0.9937475920 0.9818658829 - 0.9964681864 0.9938132763 0.9820817709 - 0.9964997172 0.9938790202 0.9822977781 - 0.9965311289 0.9939447045 0.9825137258 - 0.9965624809 0.9940103889 0.9827296734 - 0.9965938926 0.9940760732 0.9829456210 - 0.9966253042 0.9941418171 0.9831615090 - 0.9966567159 0.9942075014 0.9833775163 - 0.9966881275 0.9942731857 0.9835934043 - 0.9967194796 0.9943389297 0.9838094115 - 0.9967508912 0.9944049120 0.9840244055 - 0.9967818856 0.9944759011 0.9842302203 - 0.9968118072 0.9945470095 0.9844359159 - 0.9968416095 0.9946179986 0.9846416712 - 0.9968714714 0.9946889877 0.9848474860 - 0.9969013929 0.9947599769 0.9850531816 - 0.9969311953 0.9948310256 0.9852589965 - 0.9969611168 0.9949020743 0.9854648113 - 0.9969909787 0.9949731231 0.9856705070 - 0.9970209002 0.9950441122 0.9858763218 - 0.9970507026 0.9951151013 0.9860820174 - 0.9970806837 0.9951882958 0.9862825871 - 0.9971145988 0.9952625036 0.9864811897 - 0.9971485138 0.9953365922 0.9866797924 - 0.9971824288 0.9954108000 0.9868783951 - 0.9972162843 0.9954848886 0.9870769978 - 0.9972501993 0.9955590963 0.9872756004 - 0.9972841144 0.9956331849 0.9874743223 - 0.9973180294 0.9957073927 0.9876729250 - 0.9973520041 0.9957814813 0.9878715277 - 0.9973859191 0.9958556890 0.9880700707 - 0.9974197745 0.9959298968 0.9882662892 - 0.9974561930 0.9960047007 0.9884554744 - 0.9974939227 0.9960793853 0.9886447787 - 0.9975317121 0.9961541295 0.9888340235 - 0.9975695014 0.9962288737 0.9890233278 - 0.9976071715 0.9963036180 0.9892125130 - 0.9976450205 0.9963783026 0.9894018173 - 0.9976828098 0.9964529872 0.9895910025 - 0.9977204800 0.9965277910 0.9897803068 - 0.9977583289 0.9966024756 0.9899694920 - 0.9977961183 0.9966772199 0.9901587963 - 0.9978342056 0.9967505932 0.9903476834 - 0.9978744984 0.9968233109 0.9905365109 - 0.9979146719 0.9968960285 0.9907252789 - 0.9979549050 0.9969686866 0.9909141064 - 0.9979951978 0.9970412850 0.9911028147 - 0.9980353713 0.9971140027 0.9912915826 - 0.9980756044 0.9971867204 0.9914804101 - 0.9981158972 0.9972593784 0.9916691780 - 0.9981560707 0.9973320961 0.9918578863 - 0.9981964231 0.9974046946 0.9920467138 - 0.9982365966 0.9974774122 0.9922354817 - 0.9982774258 0.9975458980 0.9924197197 - 0.9983186722 0.9976140857 0.9926037192 - 0.9983600974 0.9976822734 0.9927877188 - 0.9984014034 0.9977505207 0.9929717779 - 0.9984427094 0.9978188276 0.9931557775 - 0.9984840155 0.9978870153 0.9933397770 - 0.9985253215 0.9979552031 0.9935238957 - 0.9985666275 0.9980235100 0.9937078953 - 0.9986079931 0.9980916977 0.9938918948 - 0.9986492991 0.9981598854 0.9940760136 - 0.9986906052 0.9982272983 0.9942585826 - 0.9987317920 0.9982891083 0.9944334030 - 0.9987729192 0.9983509779 0.9946082234 - 0.9988141060 0.9984129071 0.9947831035 - 0.9988551736 0.9984747171 0.9949579239 - 0.9988964200 0.9985365868 0.9951328039 - 0.9989374876 0.9985983968 0.9953076243 - 0.9989786148 0.9986603260 0.9954825044 - 0.9990198016 0.9987221956 0.9956573248 - 0.9990609288 0.9987840056 0.9958320856 - 0.9991021156 0.9988458753 0.9960070252 - 0.9991431832 0.9989060760 0.9961810708 - 0.9991831779 0.9989601970 0.9963524938 - 0.9992231131 0.9990143776 0.9965239167 - 0.9992631078 0.9990684986 0.9966952801 - 0.9993029833 0.9991226196 0.9968667030 - 0.9993429780 0.9991768003 0.9970381856 - 0.9993829131 0.9992309213 0.9972096086 - 0.9994229078 0.9992849827 0.9973809719 - 0.9994627833 0.9993392229 0.9975523949 - 0.9995027781 0.9993932843 0.9977238178 - 0.9995427132 0.9994474053 0.9978951812 - 0.9995825291 0.9995000958 0.9980673790 - 0.9996204972 0.9995455146 0.9982430935 - 0.9996584058 0.9995909929 0.9984188080 - 0.9996963739 0.9996364117 0.9985945225 - 0.9997342825 0.9996818900 0.9987701774 - 0.9997723103 0.9997273088 0.9989458919 - 0.9998102188 0.9997727871 0.9991214871 - 0.9998481870 0.9998182058 0.9992972016 - 0.9998860955 0.9998636842 0.9994729161 - 0.9999241233 0.9999091029 0.9996485710 - 0.9999619722 0.9999545813 0.9998242855 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0004568259 0.0001897309 0.0001937342 + 0.0009166480 0.0003902519 0.0003960232 + 0.0013811550 0.0006009749 0.0006110691 + 0.0018588870 0.0008188499 0.0008411508 + 0.0023563609 0.0010459250 0.0010869150 + 0.0028804501 0.0012829941 0.0013466120 + 0.0034379531 0.0015326150 0.0016192500 + 0.0040225340 0.0017928330 0.0019048119 + 0.0046354202 0.0020639880 0.0022035320 + 0.0052711908 0.0023474649 0.0025181000 + 0.0059363251 0.0026404259 0.0028487111 + 0.0066329152 0.0029452001 0.0031957659 + 0.0073490841 0.0032632940 0.0035582529 + 0.0080807637 0.0035944299 0.0039348011 + 0.0088355178 0.0039396151 0.0043245698 + 0.0096289609 0.0043014390 0.0047304570 + 0.0104655102 0.0046766838 0.0051527582 + 0.0113350702 0.0050640670 0.0055932738 + 0.0122496197 0.0054649492 0.0060512018 + 0.0131988404 0.0058779810 0.0065282308 + 0.0141810300 0.0063044452 0.0070242109 + 0.0151987597 0.0067441980 0.0075386940 + 0.0162404794 0.0072005908 0.0080726901 + 0.0173083302 0.0076743681 0.0086258948 + 0.0184124205 0.0081653623 0.0091975229 + 0.0195518192 0.0086749326 0.0097900750 + 0.0207353197 0.0092027588 0.0103992401 + 0.0219628699 0.0097467797 0.0110272104 + 0.0232388396 0.0103067802 0.0116758104 + 0.0245534405 0.0108824596 0.0123488903 + 0.0259076506 0.0114777200 0.0130474502 + 0.0273041893 0.0120923202 0.0137724997 + 0.0287391301 0.0127261700 0.0145242196 + 0.0302102603 0.0133811496 0.0153033203 + 0.0317212492 0.0140584502 0.0161061808 + 0.0332731493 0.0147604104 0.0169263799 + 0.0348760188 0.0154822599 0.0177620295 + 0.0365183204 0.0162258204 0.0186164491 + 0.0382087417 0.0169929601 0.0194896292 + 0.0399495289 0.0177801996 0.0203795508 + 0.0417297594 0.0185851697 0.0212911796 + 0.0435510091 0.0194129106 0.0222245902 + 0.0454129390 0.0202547908 0.0231861304 + 0.0473076887 0.0211168006 0.0241758395 + 0.0492347218 0.0219969898 0.0251984708 + 0.0512037501 0.0229012799 0.0262466706 + 0.0532191209 0.0238268599 0.0273228493 + 0.0552755818 0.0247761197 0.0284204800 + 0.0573842004 0.0257481597 0.0295423996 + 0.0595382415 0.0267426297 0.0306846593 + 0.0617300794 0.0277668294 0.0318456888 + 0.0639634207 0.0288175102 0.0330268405 + 0.0662460178 0.0298932791 0.0342224501 + 0.0685783774 0.0309970807 0.0354311503 + 0.0709521696 0.0321253613 0.0366570689 + 0.0733637214 0.0332747698 0.0379007794 + 0.0758268237 0.0344457105 0.0391649790 + 0.0783338398 0.0356396101 0.0404515490 + 0.0808990374 0.0368562713 0.0417614505 + 0.0835125297 0.0380946510 0.0430966802 + 0.0861754268 0.0393532701 0.0444576107 + 0.0888888985 0.0406360887 0.0458458997 + 0.0916479379 0.0419454388 0.0472616889 + 0.0944517180 0.0432827994 0.0486994199 + 0.0972917899 0.0446463488 0.0501526110 + 0.1001828983 0.0460356399 0.0516319200 + 0.1031202003 0.0474530496 0.0531338789 + 0.1061026007 0.0488977507 0.0546585694 + 0.1091277003 0.0503737107 0.0562016517 + 0.1121969000 0.0518880300 0.0577665791 + 0.1153125986 0.0534419082 0.0593559109 + 0.1184772998 0.0550320297 0.0609692894 + 0.1216840968 0.0566530786 0.0626069382 + 0.1249388009 0.0583061315 0.0642700866 + 0.1282431036 0.0599966310 0.0659606308 + 0.1315928996 0.0617280900 0.0676777512 + 0.1349757016 0.0634948835 0.0694218278 + 0.1383993030 0.0652914569 0.0711889118 + 0.1418682933 0.0671163797 0.0729750469 + 0.1453785002 0.0689697564 0.0747864991 + 0.1489267945 0.0708598867 0.0766221583 + 0.1525138021 0.0727824494 0.0784773678 + 0.1561374068 0.0747363791 0.0803539827 + 0.1597927064 0.0767190233 0.0822619572 + 0.1634854972 0.0787282810 0.0842007920 + 0.1672230065 0.0807615295 0.0861660689 + 0.1710027009 0.0828175321 0.0881549865 + 0.1748190969 0.0849011689 0.0901617482 + 0.1786739975 0.0870237797 0.0921890587 + 0.1825643927 0.0891877636 0.0942407027 + 0.1864794940 0.0913877636 0.0963157490 + 0.1904242039 0.0936163589 0.0984156132 + 0.1944115013 0.0958681777 0.1005403996 + 0.1984315068 0.0981432199 0.1026867032 + 0.2024817020 0.1004460007 0.1048531979 + 0.2065692991 0.1027875021 0.1070401967 + 0.2106909007 0.1051672027 0.1092483029 + 0.2148406059 0.1075756028 0.1114758030 + 0.2190158069 0.1100080982 0.1137152985 + 0.2232162058 0.1124617979 0.1159678996 + 0.2274380028 0.1149377003 0.1182356998 + 0.2316682935 0.1174454018 0.1205220968 + 0.2359098047 0.1199733987 0.1228303015 + 0.2401736975 0.1225221008 0.1251548976 + 0.2444601953 0.1250932068 0.1274984032 + 0.2487654984 0.1276911050 0.1298629940 + 0.2530876994 0.1303067952 0.1322595030 + 0.2574402988 0.1329407990 0.1346741021 + 0.2618153989 0.1355946958 0.1371085942 + 0.2662160099 0.1382690966 0.1395681947 + 0.2706286907 0.1409652978 0.1420474052 + 0.2750388086 0.1436827034 0.1445465982 + 0.2794604897 0.1464273036 0.1470647007 + 0.2838951945 0.1492000073 0.1496022046 + 0.2883372903 0.1519946009 0.1521558017 + 0.2927843034 0.1548147053 0.1547247022 + 0.2972404063 0.1576558053 0.1573172063 + 0.3016996980 0.1605056971 0.1599285007 + 0.3061661124 0.1633636951 0.1625496000 + 0.3106426001 0.1662496030 0.1651808023 + 0.3151304126 0.1691479981 0.1678352952 + 0.3196105063 0.1720582992 0.1704951972 + 0.3240850866 0.1749927998 0.1731574982 + 0.3285588920 0.1779374033 0.1758297980 + 0.3330225945 0.1808930039 0.1785068065 + 0.3374676108 0.1838704050 0.1811906993 + 0.3418787122 0.1868537068 0.1838902980 + 0.3462742865 0.1898439974 0.1865974069 + 0.3506546915 0.1928507984 0.1893136948 + 0.3550148010 0.1958658993 0.1920381039 + 0.3593491912 0.1988967061 0.1947682947 + 0.3636716902 0.2019404024 0.1975080073 + 0.3679802120 0.2049918026 0.2002497017 + 0.3722729981 0.2080595046 0.2029945999 + 0.3765603900 0.2111341059 0.2057335973 + 0.3808422089 0.2142200023 0.2084708959 + 0.3850818872 0.2173251063 0.2112185955 + 0.3893063068 0.2204380035 0.2139669061 + 0.3935188055 0.2235597968 0.2167157978 + 0.3977065980 0.2266864032 0.2194723934 + 0.4018787146 0.2298216969 0.2222321928 + 0.4060066938 0.2329684943 0.2249837965 + 0.4101200104 0.2361215949 0.2277349979 + 0.4142099917 0.2392960936 0.2304856032 + 0.4182651937 0.2424751967 0.2332389951 + 0.4223062098 0.2456675023 0.2359952033 + 0.4263221025 0.2488580942 0.2387603074 + 0.4303199053 0.2520464957 0.2415284067 + 0.4342919886 0.2552388906 0.2442979962 + 0.4382177889 0.2584339082 0.2470681965 + 0.4421251118 0.2616392970 0.2498411983 + 0.4459949136 0.2648470998 0.2526099086 + 0.4498532116 0.2680524886 0.2553699017 + 0.4536815882 0.2712588012 0.2581270933 + 0.4574846029 0.2744691968 0.2608813047 + 0.4612646103 0.2776809931 0.2636286914 + 0.4650110900 0.2808963060 0.2663717866 + 0.4687466919 0.2841087878 0.2691045105 + 0.4724290967 0.2873168886 0.2718333900 + 0.4761005938 0.2905252874 0.2745406926 + 0.4797236025 0.2937338948 0.2772436142 + 0.4833352864 0.2969321907 0.2799426019 + 0.4869242013 0.3001225889 0.2826412022 + 0.4905020893 0.3033182919 0.2853288949 + 0.4940682054 0.3065170944 0.2880161107 + 0.4975984991 0.3097181022 0.2906906009 + 0.5011065006 0.3129202127 0.2933647931 + 0.5045886040 0.3161264956 0.2960147858 + 0.5080611110 0.3193351030 0.2986648083 + 0.5115216970 0.3225524127 0.3013044894 + 0.5149798989 0.3257744014 0.3039439917 + 0.5184119940 0.3289855123 0.3065662980 + 0.5218411088 0.3321900964 0.3091877103 + 0.5252186060 0.3353936970 0.3117879033 + 0.5285930037 0.3385963142 0.3143858016 + 0.5319072008 0.3417963088 0.3169586957 + 0.5352197886 0.3449935913 0.3195259869 + 0.5384925008 0.3481869996 0.3220764101 + 0.5417643785 0.3513746858 0.3246203065 + 0.5449957848 0.3545599878 0.3271425962 + 0.5482258201 0.3577390015 0.3296504021 + 0.5514274836 0.3609147966 0.3321473897 + 0.5546270013 0.3640691936 0.3346312940 + 0.5577927828 0.3672235906 0.3371109962 + 0.5609536767 0.3703644872 0.3395805061 + 0.5640918016 0.3735049069 0.3420476019 + 0.5672239065 0.3766363859 0.3444921076 + 0.5703287125 0.3797653019 0.3469367027 + 0.5734214783 0.3828806877 0.3493632078 + 0.5764908195 0.3859854937 0.3517870903 + 0.5795422792 0.3890849948 0.3541991115 + 0.5825762749 0.3921729028 0.3566043973 + 0.5855858922 0.3952594101 0.3590024114 + 0.5885831118 0.3983199000 0.3613883853 + 0.5915424228 0.4013803899 0.3637714088 + 0.5944979191 0.4044235051 0.3661235869 + 0.5973832011 0.4074612856 0.3684759140 + 0.6002684832 0.4104872942 0.3708046079 + 0.6031265855 0.4134992957 0.3731263876 + 0.6059790850 0.4165073037 0.3754419982 + 0.6088135242 0.4194850922 0.3777495921 + 0.6116350889 0.4224627912 0.3800561130 + 0.6144289970 0.4254232049 0.3823485970 + 0.6171646714 0.4283778071 0.3846412003 + 0.6198990941 0.4313283861 0.3869284093 + 0.6225900054 0.4342721999 0.3892132044 + 0.6252809763 0.4372158945 0.3914948106 + 0.6279500127 0.4401437044 0.3937678039 + 0.6306102276 0.4430705905 0.3960408866 + 0.6332606077 0.4459845126 0.3983030021 + 0.6358942986 0.4488858879 0.4005627036 + 0.6385279298 0.4517867863 0.4028213918 + 0.6411197186 0.4546740949 0.4050782025 + 0.6437116265 0.4575614929 0.4073350132 + 0.6462585926 0.4604415894 0.4095784128 + 0.6487771869 0.4633156955 0.4118196964 + 0.6512870789 0.4661892056 0.4140588045 + 0.6537609100 0.4690420032 0.4162935913 + 0.6562346816 0.4718948007 0.4185284078 + 0.6586757898 0.4747399092 0.4207589924 + 0.6611058712 0.4775764942 0.4229885936 + 0.6635295749 0.4804131091 0.4252167940 + 0.6659365296 0.4832342863 0.4274396896 + 0.6683434248 0.4860537946 0.4296625853 + 0.6707113981 0.4888609052 0.4318814874 + 0.6730682254 0.4916389883 0.4340975881 + 0.6754177213 0.4944170117 0.4363138080 + 0.6777454019 0.4971829057 0.4385361969 + 0.6800730824 0.4999426007 0.4407587945 + 0.6823896766 0.5027021766 0.4429790080 + 0.6847013831 0.5054432154 0.4451938868 + 0.6870059967 0.5081841946 0.4474087059 + 0.6892570853 0.5109151006 0.4496183991 + 0.6915082932 0.5136244893 0.4518249035 + 0.6937376857 0.5163338184 0.4540314078 + 0.6959462762 0.5190213919 0.4562326968 + 0.6981548071 0.5216943026 0.4584333897 + 0.7003312111 0.5243672132 0.4606317878 + 0.7025029063 0.5270034075 0.4628174901 + 0.7046647072 0.5296329856 0.4650033116 + 0.7067890167 0.5322594047 0.4671837091 + 0.7089133263 0.5348445177 0.4693551958 + 0.7110230923 0.5374295712 0.4715267122 + 0.7131193280 0.5399996042 0.4736973047 + 0.7152153850 0.5425274968 0.4758673906 + 0.7172769904 0.5450553894 0.4780373871 + 0.7193294168 0.5475718975 0.4801994860 + 0.7213792801 0.5500730276 0.4823586047 + 0.7233660817 0.5525740981 0.4845176935 + 0.7253528237 0.5550534725 0.4866720140 + 0.7273290753 0.5575155020 0.4888255000 + 0.7292746902 0.5599774718 0.4909788966 + 0.7312203050 0.5624241233 0.4931291044 + 0.7331498861 0.5648627281 0.4952791035 + 0.7350592017 0.5673012733 0.4974288940 + 0.7369683981 0.5697147846 0.4995673001 + 0.7388588190 0.5721186996 0.5017058253 + 0.7407366037 0.5745226145 0.5038431287 + 0.7426143289 0.5768790841 0.5059599280 + 0.7444639802 0.5792204738 0.5080766082 + 0.7463030815 0.5815619826 0.5101922750 + 0.7481421232 0.5838884115 0.5122897029 + 0.7499330044 0.5862100720 0.5143870711 + 0.7517132163 0.5885317922 0.5164837837 + 0.7534933090 0.5908361077 0.5185531974 + 0.7552289963 0.5931339860 0.5206226110 + 0.7569586039 0.5954319835 0.5226920247 + 0.7586882710 0.5977076292 0.5247408152 + 0.7603802085 0.5999724269 0.5267890096 + 0.7620685101 0.6022372842 0.5288370848 + 0.7637568116 0.6044870019 0.5308629870 + 0.7654249072 0.6067258120 0.5328854918 + 0.7670909762 0.6089646220 0.5349079967 + 0.7687572241 0.6111888885 0.5369161963 + 0.7703763843 0.6133955717 0.5389193892 + 0.7719892263 0.6156023145 0.5409225225 + 0.7736020088 0.6177954078 0.5429117084 + 0.7751833797 0.6199551821 0.5448905230 + 0.7767575979 0.6221150160 0.5468692780 + 0.7783316970 0.6242699027 0.5488427281 + 0.7799007893 0.6263784170 0.5508071780 + 0.7814676762 0.6284868121 0.5527716875 + 0.7830346823 0.6305952072 0.5547339916 + 0.7845696211 0.6326767802 0.5566831827 + 0.7860810757 0.6347541213 0.5586323738 + 0.7875925899 0.6368314028 0.5605816245 + 0.7890849113 0.6388925910 0.5625146031 + 0.7905498147 0.6409428716 0.5644453764 + 0.7920145988 0.6429932117 0.5663762093 + 0.7934713960 0.6450356841 0.5682945848 + 0.7948976159 0.6470593810 0.5702040195 + 0.7963238955 0.6490830779 0.5721132755 + 0.7977501154 0.6511068940 0.5740166903 + 0.7991691232 0.6531022191 0.5759012103 + 0.8005877733 0.6550961733 0.5777856112 + 0.8020066023 0.6570903063 0.5796700716 + 0.8034045100 0.6590561271 0.5815389156 + 0.8047934771 0.6610018015 0.5834053159 + 0.8061826229 0.6629474759 0.5852717757 + 0.8075594902 0.6648828983 0.5871319771 + 0.8089175820 0.6667698026 0.5889847875 + 0.8102757931 0.6686565876 0.5908375978 + 0.8116320968 0.6705434918 0.5926902294 + 0.8129532933 0.6724057198 0.5945152044 + 0.8142744899 0.6742588282 0.5963401794 + 0.8155958056 0.6761118174 0.5981652141 + 0.8168948889 0.6779593229 0.5999823809 + 0.8181831241 0.6797894835 0.6017925739 + 0.8194714189 0.6816197038 0.6036027074 + 0.8207564950 0.6834499836 0.6054127812 + 0.8220325708 0.6852660775 0.6072114706 + 0.8233085871 0.6870769262 0.6090102196 + 0.8245846033 0.6888877153 0.6108089089 + 0.8258383870 0.6906918287 0.6126012802 + 0.8270868063 0.6924620271 0.6143857241 + 0.8283351064 0.6942321062 0.6161699891 + 0.8295804858 0.6960023046 0.6179543734 + 0.8308194280 0.6977444887 0.6197059155 + 0.8320583105 0.6994649768 0.6214510202 + 0.8332971931 0.7011855245 0.6231961846 + 0.8345112801 0.7029060125 0.6249343753 + 0.8357185721 0.7046008110 0.6266446710 + 0.8369258046 0.7062937021 0.6283550262 + 0.8381229043 0.7079864740 0.6300653219 + 0.8392837048 0.7096688151 0.6317604184 + 0.8404445052 0.7113192081 0.6334416270 + 0.8416053057 0.7129697204 0.6351227164 + 0.8427382708 0.7146201134 0.6368038058 + 0.8438537121 0.7162567973 0.6384651065 + 0.8449690938 0.7178813815 0.6401215792 + 0.8460845947 0.7195060253 0.6417779922 + 0.8471649885 0.7211306095 0.6434329748 + 0.8482443094 0.7227483988 0.6450561881 + 0.8493235707 0.7243642807 0.6466792822 + 0.8503887057 0.7259801030 0.6483023763 + 0.8514184952 0.7275959253 0.6499150991 + 0.8524482250 0.7291961908 0.6514953971 + 0.8534778953 0.7307960987 0.6530758142 + 0.8544930816 0.7323961258 0.6546561122 + 0.8554970026 0.7339910865 0.6562228203 + 0.8565009236 0.7355558872 0.6577693820 + 0.8575047851 0.7371206880 0.6593158841 + 0.8584917784 0.7386854887 0.6608623862 + 0.8594745994 0.7402415276 0.6623889208 + 0.8604573011 0.7417721748 0.6638967991 + 0.8614401221 0.7433028817 0.6654046774 + 0.8623782992 0.7448335886 0.6669126153 + 0.8633162975 0.7463521957 0.6683937907 + 0.8642541766 0.7478467226 0.6698569059 + 0.8651856780 0.7493410707 0.6713200212 + 0.8660802841 0.7508355975 0.6727830768 + 0.8669748902 0.7523164153 0.6742259264 + 0.8678696156 0.7537738085 0.6756564975 + 0.8687558770 0.7552313209 0.6770871878 + 0.8696188927 0.7566887140 0.6785178185 + 0.8704819083 0.7581344843 0.6799271703 + 0.8713449240 0.7595599294 0.6813234091 + 0.8722006083 0.7609851956 0.6827195883 + 0.8730415106 0.7624105215 0.6841157079 + 0.8738824725 0.7638270259 0.6854910851 + 0.8747234941 0.7652239203 0.6868504286 + 0.8755509257 0.7666208744 0.6882097721 + 0.8763540983 0.7680178285 0.6895691156 + 0.8771572709 0.7694097161 0.6909182072 + 0.8779605031 0.7707833052 0.6922556162 + 0.8787578940 0.7721568942 0.6935930252 + 0.8795443177 0.7735304832 0.6949303746 + 0.8803306818 0.7749016285 0.6962568164 + 0.8811172247 0.7762401104 0.6975601912 + 0.8818970919 0.7775787115 0.6988636851 + 0.8826605082 0.7789173126 0.7001671195 + 0.8834239841 0.7802559137 0.7014654875 + 0.8841875196 0.7815645933 0.7027323842 + 0.8849436045 0.7828695774 0.7039992213 + 0.8856664896 0.7841745019 0.7052659988 + 0.8863893151 0.7854794860 0.7065327764 + 0.8871122003 0.7867677808 0.7077702880 + 0.8878350854 0.7880474925 0.7090048194 + 0.8885604143 0.7893270850 0.7102392912 + 0.8892856836 0.7906067967 0.7114737034 + 0.8900108933 0.7918794155 0.7126892805 + 0.8907362223 0.7931404114 0.7138935924 + 0.8914461732 0.7944014072 0.7150977850 + 0.8921533227 0.7956624031 0.7163020968 + 0.8928604126 0.7969211936 0.7174999714 + 0.8935676217 0.7981446981 0.7186828256 + 0.8942708969 0.7993682027 0.7198656797 + 0.8949717283 0.8005917072 0.7210485935 + 0.8956726193 0.8018152118 0.7222315073 + 0.8963733912 0.8030143976 0.7233734727 + 0.8970720768 0.8042024970 0.7245125771 + 0.8977661729 0.8053905964 0.7256516814 + 0.8984603286 0.8065786958 0.7267907262 + 0.8991544247 0.8077601790 0.7279145122 + 0.8998485208 0.8089237809 0.7290235758 + 0.9005149007 0.8100873828 0.7301326990 + 0.9011809230 0.8112509847 0.7312418222 + 0.9018470049 0.8124147058 0.7323492169 + 0.9025130272 0.8135333061 0.7334213853 + 0.9031683207 0.8146411777 0.7344936728 + 0.9038167000 0.8157489896 0.7355659008 + 0.9044650793 0.8168568015 0.7366381288 + 0.9051135182 0.8179541826 0.7376983166 + 0.9057582021 0.8190261722 0.7387477160 + 0.9063869119 0.8200982213 0.7397971153 + 0.9070156217 0.8211702108 0.7408465743 + 0.9076442122 0.8222423196 0.7418959737 + 0.9082729220 0.8232967854 0.7429301143 + 0.9088765979 0.8243448734 0.7439635992 + 0.9094702005 0.8253930211 0.7449972034 + 0.9100638032 0.8264411092 0.7460306883 + 0.9106575251 0.8274878860 0.7470514178 + 0.9112505913 0.8285259008 0.7480484843 + 0.9118415713 0.8295637965 0.7490454912 + 0.9124326706 0.8306018114 0.7500426173 + 0.9130237103 0.8316397071 0.7510396242 + 0.9136146903 0.8326591849 0.7520161271 + 0.9142035246 0.8336567283 0.7529833913 + 0.9147909284 0.8346542120 0.7539507151 + 0.9153783917 0.8356516957 0.7549179792 + 0.9159657955 0.8366491795 0.7558851838 + 0.9165533185 0.8376235962 0.7568265200 + 0.9171171784 0.8385900259 0.7577672005 + 0.9176810980 0.8395562768 0.7587078214 + 0.9182450771 0.8405227065 0.7596483827 + 0.9188089967 0.8414890766 0.7605820894 + 0.9193634987 0.8424319029 0.7614898086 + 0.9198989272 0.8433743119 0.7623975277 + 0.9204342961 0.8443166018 0.7633051276 + 0.9209697843 0.8452588916 0.7642127872 + 0.9215052128 0.8461968899 0.7651119828 + 0.9220255017 0.8471127748 0.7659983039 + 0.9225360751 0.8480286002 0.7668846250 + 0.9230468273 0.8489444852 0.7677708864 + 0.9235574007 0.8498603106 0.7686572075 + 0.9240680933 0.8507688046 0.7695332766 + 0.9245592952 0.8516598940 0.7703999877 + 0.9250469208 0.8525511026 0.7712668180 + 0.9255344272 0.8534421921 0.7721335888 + 0.9260219932 0.8543332815 0.7730002999 + 0.9265089035 0.8552088737 0.7738537788 + 0.9269595146 0.8560588956 0.7746983767 + 0.9274101257 0.8569089174 0.7755429745 + 0.9278606176 0.8577588797 0.7763875723 + 0.9283112288 0.8586089015 0.7772322297 + 0.9287610054 0.8594486117 0.7780702710 + 0.9292058945 0.8602731228 0.7789046168 + 0.9296509027 0.8610975742 0.7797389030 + 0.9300957918 0.8619220853 0.7805731893 + 0.9305406809 0.8627465963 0.7814074755 + 0.9309821129 0.8635640740 0.7822362781 + 0.9314101934 0.8643702269 0.7830613256 + 0.9318383932 0.8651763201 0.7838863730 + 0.9322664738 0.8659824133 0.7847114205 + 0.9326946139 0.8667885065 0.7855364084 + 0.9331154227 0.8675866127 0.7863515019 + 0.9335113764 0.8683658838 0.7871572971 + 0.9339073896 0.8691452146 0.7879629731 + 0.9343034029 0.8699244857 0.7887688279 + 0.9346994758 0.8707038164 0.7895745039 + 0.9350925088 0.8714814186 0.7903773189 + 0.9354733229 0.8722500205 0.7911751866 + 0.9358540773 0.8730186224 0.7919729948 + 0.9362348914 0.8737872243 0.7927709222 + 0.9366155863 0.8745558262 0.7935687900 + 0.9369941950 0.8753244281 0.7943630815 + 0.9373536110 0.8760707974 0.7951408029 + 0.9377130270 0.8768165112 0.7959185839 + 0.9380723834 0.8775621057 0.7966964245 + 0.9384316802 0.8783078194 0.7974742055 + 0.9387910962 0.8790534139 0.7982519865 + 0.9391242266 0.8797795773 0.7990127802 + 0.9394561052 0.8804981112 0.7997723222 + 0.9397879839 0.8812165260 0.8005318046 + 0.9401199222 0.8819350004 0.8012912869 + 0.9404518008 0.8826534748 0.8020508289 + 0.9407855868 0.8833674192 0.8027989864 + 0.9411200881 0.8840746284 0.8035404086 + 0.9414545298 0.8847818971 0.8042817116 + 0.9417889714 0.8854891062 0.8050230145 + 0.9421234727 0.8861963749 0.8057643771 + 0.9424526095 0.8869026899 0.8065025806 + 0.9427763224 0.8875902891 0.8072319031 + 0.9430999160 0.8882780075 0.8079612255 + 0.9434236288 0.8889656067 0.8086904883 + 0.9437472224 0.8896532059 0.8094198108 + 0.9440696836 0.8903409243 0.8101490736 + 0.9443870783 0.8910048008 0.8108692765 + 0.9447045922 0.8916543722 0.8115875721 + 0.9450219870 0.8923041224 0.8123059273 + 0.9453393817 0.8929538131 0.8130242229 + 0.9456567764 0.8936033845 0.8137425184 + 0.9459626079 0.8942502141 0.8144533038 + 0.9462655187 0.8948795795 0.8151500821 + 0.9465684891 0.8955091238 0.8158469200 + 0.9468713999 0.8961384892 0.8165438175 + 0.9471743703 0.8967679739 0.8172405958 + 0.9474688172 0.8973973989 0.8179373741 + 0.9477491975 0.8980208039 0.8186287880 + 0.9480295777 0.8986400962 0.8193188906 + 0.9483100176 0.8992593884 0.8200089931 + 0.9485903978 0.8998786807 0.8206989765 + 0.9488707781 0.9004979730 0.8213890791 + 0.9491494298 0.9011172056 0.8220766783 + 0.9494279027 0.9017202854 0.8227545023 + 0.9497063160 0.9023234844 0.8234323859 + 0.9499847889 0.9029266238 0.8241102099 + 0.9502632022 0.9035297036 0.8247880936 + 0.9505476952 0.9041329026 0.8254659176 + 0.9508438706 0.9047262073 0.8261309862 + 0.9511401057 0.9053006768 0.8267852068 + 0.9514362812 0.9058750868 0.8274394274 + 0.9517325163 0.9064496160 0.8280935884 + 0.9520286918 0.9070240855 0.8287478089 + 0.9523310065 0.9075986147 0.8294020295 + 0.9526355267 0.9081652761 0.8300411105 + 0.9529399872 0.9087274075 0.8306767941 + 0.9532445073 0.9092894197 0.8313124776 + 0.9535490274 0.9098513722 0.8319482803 + 0.9538534284 0.9104133844 0.8325840235 + 0.9541549087 0.9109753966 0.8332191706 + 0.9544565082 0.9115229845 0.8338392973 + 0.9547581077 0.9120678902 0.8344593048 + 0.9550595880 0.9126127958 0.8350793123 + 0.9553611875 0.9131575823 0.8356993794 + 0.9556581974 0.9137024879 0.8363193870 + 0.9559459090 0.9142473936 0.8369358778 + 0.9562335014 0.9147704840 0.8375366926 + 0.9565210938 0.9152935147 0.8381375074 + 0.9568086863 0.9158164859 0.8387383223 + 0.9570962787 0.9163393974 0.8393390775 + 0.9573791027 0.9168624282 0.8399398923 + 0.9576573968 0.9173836112 0.8405390978 + 0.9579358101 0.9178845882 0.8411338925 + 0.9582142234 0.9183856249 0.8417286873 + 0.9584925771 0.9188866019 0.8423236012 + 0.9587709904 0.9193875790 0.8429183960 + 0.9590389132 0.9198886156 0.8435133100 + 0.9593018293 0.9203869104 0.8441042900 + 0.9595646262 0.9208633900 0.8446862102 + 0.9598274827 0.9213399291 0.8452680111 + 0.9600902796 0.9218164086 0.8458498716 + 0.9603531957 0.9222928882 0.8464317918 + 0.9606087208 0.9227694273 0.8470137119 + 0.9608618021 0.9232439995 0.8475909233 + 0.9611148834 0.9236922264 0.8481546044 + 0.9613680243 0.9241403937 0.8487182856 + 0.9616211057 0.9245886207 0.8492819071 + 0.9618741870 0.9250367880 0.8498455882 + 0.9621182084 0.9254850149 0.8504093289 + 0.9623593092 0.9259333014 0.8509712815 + 0.9626005292 0.9263677001 0.8515232801 + 0.9628416896 0.9268016815 0.8520752788 + 0.9630827904 0.9272356033 0.8526272774 + 0.9633240104 0.9276695848 0.8531792760 + 0.9635608792 0.9281035066 0.8537312746 + 0.9637960792 0.9285374880 0.8542832732 + 0.9640312791 0.9289491773 0.8548194766 + 0.9642664790 0.9293550849 0.8553553224 + 0.9645017982 0.9297609925 0.8558911085 + 0.9647369981 0.9301669002 0.8564267755 + 0.9649640918 0.9305728078 0.8569626212 + 0.9651858807 0.9309787154 0.8574984074 + 0.9654077888 0.9313749075 0.8580273986 + 0.9656295776 0.9317634106 0.8585538864 + 0.9658514261 0.9321519136 0.8590804935 + 0.9660732150 0.9325404763 0.8596069813 + 0.9662867188 0.9329289794 0.8601335287 + 0.9664887190 0.9333174825 0.8606600761 + 0.9666907787 0.9337003827 0.8611857295 + 0.9668927789 0.9340664744 0.8617103100 + 0.9670948982 0.9344326258 0.8622347713 + 0.9672968984 0.9347985983 0.8627592921 + 0.9674957991 0.9351646900 0.8632838130 + 0.9676799178 0.9355307817 0.8638082743 + 0.9678639174 0.9358968735 0.8643323183 + 0.9680479169 0.9362515807 0.8648458719 + 0.9682319164 0.9366047978 0.8653594851 + 0.9684159756 0.9369578958 0.8658730984 + 0.9685999751 0.9373109937 0.8663867116 + 0.9687842727 0.9376642108 0.8669003248 + 0.9689685702 0.9380173087 0.8674138784 + 0.9691529274 0.9383693933 0.8679202795 + 0.9693372250 0.9387202263 0.8684219718 + 0.9695215225 0.9390708804 0.8689237833 + 0.9697058201 0.9394217134 0.8694254756 + 0.9698892236 0.9397724271 0.8699272275 + 0.9700716734 0.9401232004 0.8704289794 + 0.9702541232 0.9404739141 0.8709301949 + 0.9704365134 0.9408150911 0.8714262247 + 0.9706190228 0.9411553741 0.8719220757 + 0.9708014131 0.9414957166 0.8724181056 + 0.9709836841 0.9418359995 0.8729140759 + 0.9711577892 0.9421764016 0.8734099865 + 0.9713318944 0.9425166845 0.8739060163 + 0.9715059996 0.9428496957 0.8744006157 + 0.9716801047 0.9431682229 0.8748937249 + 0.9718542099 0.9434866905 0.8753868937 + 0.9720283747 0.9438052177 0.8758801222 + 0.9721953273 0.9441236854 0.8763732910 + 0.9723548889 0.9444422722 0.8768664002 + 0.9725145102 0.9447607994 0.8773596287 + 0.9726741910 0.9450662732 0.8778408170 + 0.9728338122 0.9453657866 0.8783197999 + 0.9729933739 0.9456652999 0.8787987828 + 0.9731529951 0.9459646940 0.8792778254 + 0.9732946754 0.9462642074 0.8797568083 + 0.9734343886 0.9465637207 0.8802356720 + 0.9735741019 0.9468631744 0.8807138205 + 0.9737136960 0.9471693039 0.8811848164 + 0.9738534093 0.9474757910 0.8816556931 + 0.9739931226 0.9477822781 0.8821266890 + 0.9741299152 0.9480888247 0.8825976849 + 0.9742575288 0.9483953118 0.8830686212 + 0.9743850827 0.9487017989 0.8835396171 + 0.9745125771 0.9490079880 0.8840103149 + 0.9746401906 0.9493122101 0.8844805956 + 0.9747678041 0.9496163726 0.8849508166 + 0.9748952985 0.9499207139 0.8854210973 + 0.9750214219 0.9502248764 0.8858913183 + 0.9751458764 0.9505292177 0.8863615990 + 0.9752703905 0.9508333802 0.8868318200 + 0.9753949046 0.9511339068 0.8872954845 + 0.9755194783 0.9514247775 0.8877508044 + 0.9756439924 0.9517157078 0.8882061243 + 0.9757685065 0.9520065784 0.8886613846 + 0.9758896232 0.9522975087 0.8891167045 + 0.9760090113 0.9525884986 0.8895720243 + 0.9761283994 0.9528794289 0.8900272846 + 0.9762479067 0.9531695247 0.8904743195 + 0.9763672948 0.9534580112 0.8909124732 + 0.9764866829 0.9537466168 0.8913506866 + 0.9766061902 0.9540351033 0.8917888999 + 0.9767202735 0.9543237090 0.8922271132 + 0.9768331051 0.9546121955 0.8926653266 + 0.9769458771 0.9549006820 0.8931034803 + 0.9770585895 0.9551869035 0.8935418129 + 0.9771714211 0.9554678202 0.8939803243 + 0.9772841930 0.9557486773 0.8944187760 + 0.9773970246 0.9560295939 0.8948572874 + 0.9775123000 0.9563105106 0.8952959180 + 0.9776281118 0.9565914273 0.8957344294 + 0.9777439237 0.9568722844 0.8961728811 + 0.9778596759 0.9571502805 0.8966065049 + 0.9779754877 0.9574174285 0.8970314264 + 0.9780912995 0.9576845169 0.8974562883 + 0.9782071114 0.9579516053 0.8978813291 + 0.9783318043 0.9582186937 0.8983061910 + 0.9784587026 0.9584857821 0.8987311125 + 0.9785856009 0.9587529898 0.8991559744 + 0.9787124991 0.9590197206 0.8995798230 + 0.9788395166 0.9592756033 0.8999984860 + 0.9789664149 0.9595314860 0.9004172087 + 0.9790933132 0.9597874284 0.9008359909 + 0.9792243838 0.9600433111 0.9012547135 + 0.9793574214 0.9602991939 0.9016733766 + 0.9794905186 0.9605550766 0.9020922184 + 0.9796234965 0.9608110189 0.9025108814 + 0.9797564745 0.9610598087 0.9029234052 + 0.9798895717 0.9613065720 0.9033355117 + 0.9800226092 0.9615533948 0.9037474990 + 0.9801558256 0.9618002176 0.9041596055 + 0.9802892208 0.9620471001 0.9045715928 + 0.9804226160 0.9622939229 0.9049835801 + 0.9805560112 0.9625406861 0.9053956866 + 0.9806892872 0.9627807736 0.9057967067 + 0.9808226824 0.9630126953 0.9061899185 + 0.9809560776 0.9632446766 0.9065830708 + 0.9810881019 0.9634765983 0.9069762826 + 0.9812154174 0.9637085199 0.9073694944 + 0.9813426733 0.9639403820 0.9077627063 + 0.9814701080 0.9641724229 0.9081559181 + 0.9815974236 0.9644042850 0.9085468054 + 0.9817246795 0.9646360278 0.9089267254 + 0.9818521142 0.9648677111 0.9093065262 + 0.9819794297 0.9650995135 0.9096863866 + 0.9821053743 0.9653311968 0.9100661874 + 0.9822311997 0.9655628800 0.9104459882 + 0.9823570251 0.9657946825 0.9108259082 + 0.9824826717 0.9660264254 0.9112057090 + 0.9826084971 0.9662542939 0.9115757942 + 0.9827343225 0.9664790034 0.9119412899 + 0.9828600883 0.9667037129 0.9123067856 + 0.9829840064 0.9669284225 0.9126722217 + 0.9831055999 0.9671530724 0.9130377173 + 0.9832271934 0.9673777819 0.9134030938 + 0.9833487868 0.9676026106 0.9137685895 + 0.9834703803 0.9678273201 0.9141318202 + 0.9835919738 0.9680370092 0.9144750237 + 0.9837136269 0.9682462811 0.9148182273 + 0.9838352203 0.9684554935 0.9151614904 + 0.9839490056 0.9686648250 0.9155046940 + 0.9840624928 0.9688740969 0.9158478975 + 0.9841759801 0.9690833092 0.9161911011 + 0.9842895269 0.9692925811 0.9165343046 + 0.9844030142 0.9694945812 0.9168791175 + 0.9845163822 0.9696804285 0.9172258973 + 0.9846299291 0.9698662758 0.9175727963 + 0.9847432971 0.9700521231 0.9179195762 + 0.9848564267 0.9702379704 0.9182664156 + 0.9849694967 0.9704238176 0.9186133146 + 0.9850826859 0.9706097245 0.9189600945 + 0.9851958156 0.9707955718 0.9193068743 + 0.9853088856 0.9709764719 0.9196500182 + 0.9854220748 0.9711537957 0.9199913740 + 0.9855352044 0.9713311195 0.9203327894 + 0.9856474996 0.9715083838 0.9206743240 + 0.9857596159 0.9716858268 0.9210156798 + 0.9858716130 0.9718630910 0.9213570952 + 0.9859836102 0.9720404148 0.9216985703 + 0.9860956073 0.9722176790 0.9220399857 + 0.9862076044 0.9723867774 0.9223676920 + 0.9863196015 0.9725531936 0.9226933122 + 0.9864315987 0.9727196097 0.9230188727 + 0.9865403175 0.9728860259 0.9233446121 + 0.9866489172 0.9730523825 0.9236701727 + 0.9867573977 0.9732187986 0.9239959121 + 0.9868658781 0.9733852148 0.9243214726 + 0.9869744182 0.9735515714 0.9246470928 + 0.9870830178 0.9737042189 0.9249747992 + 0.9871914983 0.9738543034 0.9253026247 + 0.9872993231 0.9740043283 0.9256303906 + 0.9874020219 0.9741542935 0.9259580970 + 0.9875046015 0.9743043184 0.9262859225 + 0.9876071811 0.9744542837 0.9266136885 + 0.9877098203 0.9746043086 0.9269413948 + 0.9878123999 0.9747542739 0.9272692204 + 0.9879150987 0.9748876095 0.9276003242 + 0.9880176783 0.9750179052 0.9279316068 + 0.9881187081 0.9751480818 0.9282628298 + 0.9882128835 0.9752783775 0.9285941124 + 0.9883071184 0.9754086733 0.9289252758 + 0.9884012938 0.9755390286 0.9292566180 + 0.9884955287 0.9756692052 0.9295879006 + 0.9885897040 0.9757995009 0.9299191236 + 0.9886838794 0.9759184122 0.9302431941 + 0.9887781143 0.9760339260 0.9305663109 + 0.9888703227 0.9761493802 0.9308894873 + 0.9889534116 0.9762648940 0.9312126040 + 0.9890366197 0.9763804078 0.9315357208 + 0.9891197085 0.9764959216 0.9318587780 + 0.9892027974 0.9766113758 0.9321818948 + 0.9892858863 0.9767268896 0.9325050116 + 0.9893689752 0.9768431187 0.9328215718 + 0.9894521236 0.9769597054 0.9331353903 + 0.9895340800 0.9770764112 0.9334493279 + 0.9896032810 0.9771929979 0.9337630868 + 0.9896724820 0.9773095846 0.9340769053 + 0.9897418022 0.9774262905 0.9343907833 + 0.9898110032 0.9775428772 0.9347046018 + 0.9898802042 0.9776595235 0.9350184798 + 0.9899495244 0.9777796865 0.9353330135 + 0.9900187254 0.9779065847 0.9356483817 + 0.9900879264 0.9780334830 0.9359638095 + 0.9901421070 0.9781603813 0.9362791181 + 0.9901946187 0.9782872796 0.9365944862 + 0.9902470708 0.9784141779 0.9369099140 + 0.9902997017 0.9785410762 0.9372252822 + 0.9903522134 0.9786679745 0.9375407100 + 0.9904047847 0.9787948132 0.9378561974 + 0.9904572964 0.9789273143 0.9381728768 + 0.9905098081 0.9790598154 0.9384894967 + 0.9905501008 0.9791921973 0.9388061762 + 0.9905833006 0.9793246984 0.9391229153 + 0.9906166196 0.9794571996 0.9394394755 + 0.9906498194 0.9795895815 0.9397562146 + 0.9906830192 0.9797220826 0.9400727749 + 0.9907162189 0.9798545837 0.9403895140 + 0.9907494187 0.9799873829 0.9406992793 + 0.9907826185 0.9801204205 0.9410055280 + 0.9908105135 0.9802533984 0.9413117170 + 0.9908241034 0.9803863764 0.9416180253 + 0.9908378124 0.9805194139 0.9419242144 + 0.9908514023 0.9806525111 0.9422304034 + 0.9908649921 0.9807854891 0.9425367117 + 0.9908785820 0.9809185266 0.9428429008 + 0.9908922911 0.9810513854 0.9431486726 + 0.9909058809 0.9811798930 0.9434512854 + 0.9909194708 0.9813084006 0.9437538981 + 0.9909344912 0.9814370275 0.9440565705 + 0.9909498096 0.9815654755 0.9443591833 + 0.9909651279 0.9816941023 0.9446619153 + 0.9909805059 0.9818226099 0.9449645281 + 0.9909958243 0.9819511771 0.9452670813 + 0.9910110831 0.9820796847 0.9455698133 + 0.9910264015 0.9822043180 0.9458690882 + 0.9910417199 0.9823232293 0.9461650848 + 0.9910593033 0.9824422002 0.9464610219 + 0.9910830855 0.9825611711 0.9467570186 + 0.9911068082 0.9826800823 0.9470530152 + 0.9911304712 0.9827991128 0.9473490119 + 0.9911543131 0.9829180241 0.9476450086 + 0.9911779761 0.9830369949 0.9479410052 + 0.9912016988 0.9831560254 0.9482370019 + 0.9912254214 0.9832714796 0.9485340714 + 0.9912492037 0.9833855033 0.9488316178 + 0.9912775159 0.9834995270 0.9491291046 + 0.9913085103 0.9836134911 0.9494264722 + 0.9913393855 0.9837275147 0.9497240186 + 0.9913703799 0.9838414788 0.9500213861 + 0.9914013147 0.9839556217 0.9503188729 + 0.9914323092 0.9840695858 0.9506164193 + 0.9914631844 0.9841836095 0.9509137869 + 0.9914941788 0.9842914939 0.9512110949 + 0.9915251136 0.9843987226 0.9515084028 + 0.9915609956 0.9845058918 0.9518057108 + 0.9915972948 0.9846131206 0.9521030188 + 0.9916337132 0.9847202897 0.9524002075 + 0.9916700125 0.9848275185 0.9526975155 + 0.9917063117 0.9849346876 0.9529948235 + 0.9917426705 0.9850419164 0.9532920718 + 0.9917790294 0.9851490855 0.9535887241 + 0.9918153286 0.9852461815 0.9538760185 + 0.9918519855 0.9853430986 0.9541633129 + 0.9918923974 0.9854400158 0.9544506073 + 0.9919328094 0.9855368733 0.9547379017 + 0.9919732213 0.9856337905 0.9550251961 + 0.9920135736 0.9857307076 0.9553123713 + 0.9920539856 0.9858275056 0.9555997252 + 0.9920943975 0.9859244227 0.9558870196 + 0.9921348095 0.9860212803 0.9561737180 + 0.9921752214 0.9861149192 0.9564520121 + 0.9922161102 0.9862084985 0.9567303061 + 0.9922596216 0.9863020778 0.9570086002 + 0.9923030138 0.9863955975 0.9572867751 + 0.9923465252 0.9864891768 0.9575651288 + 0.9923899770 0.9865828156 0.9578434229 + 0.9924334884 0.9866762757 0.9581217170 + 0.9924769998 0.9867699146 0.9583998919 + 0.9925205112 0.9868633747 0.9586781859 + 0.9925640225 0.9869536757 0.9589493871 + 0.9926077724 0.9870436192 0.9592202902 + 0.9926536083 0.9871335030 0.9594913125 + 0.9926993251 0.9872233868 0.9597622156 + 0.9927451015 0.9873132706 0.9600331187 + 0.9927908778 0.9874032140 0.9603040218 + 0.9928365946 0.9874930978 0.9605749249 + 0.9928823709 0.9875829816 0.9608458877 + 0.9929282069 0.9876729250 0.9611167908 + 0.9929739237 0.9877598286 0.9613807201 + 0.9930197001 0.9878454208 0.9616423249 + 0.9930669069 0.9879310131 0.9619039893 + 0.9931141138 0.9880164862 0.9621657133 + 0.9931613207 0.9881020784 0.9624273777 + 0.9932085276 0.9881876707 0.9626889825 + 0.9932556748 0.9882733226 0.9629507065 + 0.9933028221 0.9883589149 0.9632123709 + 0.9933500290 0.9884445071 0.9634739757 + 0.9933971763 0.9885308146 0.9637349844 + 0.9934443831 0.9886181951 0.9639952183 + 0.9934918880 0.9887055755 0.9642552733 + 0.9935395122 0.9887928963 0.9645155072 + 0.9935870767 0.9888802767 0.9647756219 + 0.9936347008 0.9889677167 0.9650356770 + 0.9936823249 0.9890550971 0.9652959108 + 0.9937298894 0.9891424775 0.9655560255 + 0.9937775135 0.9892299175 0.9658161998 + 0.9938250780 0.9893172979 0.9660758972 + 0.9938727021 0.9894027710 0.9663285017 + 0.9939200282 0.9894883037 0.9665812254 + 0.9939668179 0.9895737767 0.9668337703 + 0.9940136075 0.9896594286 0.9670864940 + 0.9940603971 0.9897449017 0.9673390985 + 0.9941071868 0.9898303747 0.9675918221 + 0.9941540956 0.9899159074 0.9678444266 + 0.9942008853 0.9900014997 0.9680969715 + 0.9942476749 0.9900869727 0.9683496952 + 0.9942945242 0.9901695848 0.9685934186 + 0.9943410754 0.9902492166 0.9688298702 + 0.9943857789 0.9903287888 0.9690665007 + 0.9944304228 0.9904084206 0.9693030119 + 0.9944751263 0.9904879928 0.9695395827 + 0.9945198298 0.9905676246 0.9697762132 + 0.9945644140 0.9906471968 0.9700127244 + 0.9946091175 0.9907268286 0.9702492952 + 0.9946537018 0.9908064008 0.9704858065 + 0.9946984053 0.9908859730 0.9707223773 + 0.9947431087 0.9909566045 0.9709470272 + 0.9947856069 0.9910261035 0.9711709023 + 0.9948266745 0.9910956025 0.9713948965 + 0.9948678017 0.9911652207 0.9716188908 + 0.9949088097 0.9912347198 0.9718428254 + 0.9949498773 0.9913042188 0.9720668197 + 0.9949908853 0.9913737774 0.9722908139 + 0.9950320125 0.9914432764 0.9725146890 + 0.9950730801 0.9915127754 0.9727386832 + 0.9951140881 0.9915797710 0.9729639292 + 0.9951552153 0.9916355014 0.9731928706 + 0.9951915145 0.9916911721 0.9734219909 + 0.9952275753 0.9917469025 0.9736511111 + 0.9952636957 0.9918025732 0.9738801122 + 0.9952998161 0.9918583035 0.9741091728 + 0.9953358769 0.9919139743 0.9743381739 + 0.9953719974 0.9919697046 0.9745672941 + 0.9954081178 0.9920253754 0.9747962952 + 0.9954441786 0.9920811057 0.9750254154 + 0.9954802990 0.9921348095 0.9752535820 + 0.9955152273 0.9921852946 0.9754807949 + 0.9955464005 0.9922357798 0.9757078886 + 0.9955775142 0.9922863245 0.9759351015 + 0.9956086874 0.9923368096 0.9761621952 + 0.9956398010 0.9923872948 0.9763892889 + 0.9956709743 0.9924377799 0.9766165018 + 0.9957020879 0.9924883246 0.9768435955 + 0.9957333207 0.9925388098 0.9770708084 + 0.9957643747 0.9925892949 0.9772979021 + 0.9957956076 0.9926403165 0.9775205851 + 0.9958270788 0.9926918149 0.9777389169 + 0.9958592057 0.9927433729 0.9779571891 + 0.9958912134 0.9927948713 0.9781755209 + 0.9959232807 0.9928464293 0.9783937931 + 0.9959554076 0.9928979874 0.9786121249 + 0.9959874749 0.9929494858 0.9788303971 + 0.9960194826 0.9930011034 0.9790487289 + 0.9960516095 0.9930526018 0.9792670012 + 0.9960836768 0.9931041002 0.9794852138 + 0.9961156845 0.9931588173 0.9797025919 + 0.9961478114 0.9932174087 0.9799190164 + 0.9961798787 0.9932761192 0.9801353216 + 0.9962120056 0.9933347702 0.9803516269 + 0.9962440133 0.9933934808 0.9805679917 + 0.9962760806 0.9934520721 0.9807842970 + 0.9963082075 0.9935107827 0.9810007215 + 0.9963402152 0.9935694933 0.9812170267 + 0.9963722825 0.9936280847 0.9814332724 + 0.9964044094 0.9936867952 0.9816496968 + 0.9964364767 0.9937475920 0.9818658829 + 0.9964681864 0.9938132763 0.9820817709 + 0.9964997172 0.9938790202 0.9822977781 + 0.9965311289 0.9939447045 0.9825137258 + 0.9965624809 0.9940103889 0.9827296734 + 0.9965938926 0.9940760732 0.9829456210 + 0.9966253042 0.9941418171 0.9831615090 + 0.9966567159 0.9942075014 0.9833775163 + 0.9966881275 0.9942731857 0.9835934043 + 0.9967194796 0.9943389297 0.9838094115 + 0.9967508912 0.9944049120 0.9840244055 + 0.9967818856 0.9944759011 0.9842302203 + 0.9968118072 0.9945470095 0.9844359159 + 0.9968416095 0.9946179986 0.9846416712 + 0.9968714714 0.9946889877 0.9848474860 + 0.9969013929 0.9947599769 0.9850531816 + 0.9969311953 0.9948310256 0.9852589965 + 0.9969611168 0.9949020743 0.9854648113 + 0.9969909787 0.9949731231 0.9856705070 + 0.9970209002 0.9950441122 0.9858763218 + 0.9970507026 0.9951151013 0.9860820174 + 0.9970806837 0.9951882958 0.9862825871 + 0.9971145988 0.9952625036 0.9864811897 + 0.9971485138 0.9953365922 0.9866797924 + 0.9971824288 0.9954108000 0.9868783951 + 0.9972162843 0.9954848886 0.9870769978 + 0.9972501993 0.9955590963 0.9872756004 + 0.9972841144 0.9956331849 0.9874743223 + 0.9973180294 0.9957073927 0.9876729250 + 0.9973520041 0.9957814813 0.9878715277 + 0.9973859191 0.9958556890 0.9880700707 + 0.9974197745 0.9959298968 0.9882662892 + 0.9974561930 0.9960047007 0.9884554744 + 0.9974939227 0.9960793853 0.9886447787 + 0.9975317121 0.9961541295 0.9888340235 + 0.9975695014 0.9962288737 0.9890233278 + 0.9976071715 0.9963036180 0.9892125130 + 0.9976450205 0.9963783026 0.9894018173 + 0.9976828098 0.9964529872 0.9895910025 + 0.9977204800 0.9965277910 0.9897803068 + 0.9977583289 0.9966024756 0.9899694920 + 0.9977961183 0.9966772199 0.9901587963 + 0.9978342056 0.9967505932 0.9903476834 + 0.9978744984 0.9968233109 0.9905365109 + 0.9979146719 0.9968960285 0.9907252789 + 0.9979549050 0.9969686866 0.9909141064 + 0.9979951978 0.9970412850 0.9911028147 + 0.9980353713 0.9971140027 0.9912915826 + 0.9980756044 0.9971867204 0.9914804101 + 0.9981158972 0.9972593784 0.9916691780 + 0.9981560707 0.9973320961 0.9918578863 + 0.9981964231 0.9974046946 0.9920467138 + 0.9982365966 0.9974774122 0.9922354817 + 0.9982774258 0.9975458980 0.9924197197 + 0.9983186722 0.9976140857 0.9926037192 + 0.9983600974 0.9976822734 0.9927877188 + 0.9984014034 0.9977505207 0.9929717779 + 0.9984427094 0.9978188276 0.9931557775 + 0.9984840155 0.9978870153 0.9933397770 + 0.9985253215 0.9979552031 0.9935238957 + 0.9985666275 0.9980235100 0.9937078953 + 0.9986079931 0.9980916977 0.9938918948 + 0.9986492991 0.9981598854 0.9940760136 + 0.9986906052 0.9982272983 0.9942585826 + 0.9987317920 0.9982891083 0.9944334030 + 0.9987729192 0.9983509779 0.9946082234 + 0.9988141060 0.9984129071 0.9947831035 + 0.9988551736 0.9984747171 0.9949579239 + 0.9988964200 0.9985365868 0.9951328039 + 0.9989374876 0.9985983968 0.9953076243 + 0.9989786148 0.9986603260 0.9954825044 + 0.9990198016 0.9987221956 0.9956573248 + 0.9990609288 0.9987840056 0.9958320856 + 0.9991021156 0.9988458753 0.9960070252 + 0.9991431832 0.9989060760 0.9961810708 + 0.9991831779 0.9989601970 0.9963524938 + 0.9992231131 0.9990143776 0.9965239167 + 0.9992631078 0.9990684986 0.9966952801 + 0.9993029833 0.9991226196 0.9968667030 + 0.9993429780 0.9991768003 0.9970381856 + 0.9993829131 0.9992309213 0.9972096086 + 0.9994229078 0.9992849827 0.9973809719 + 0.9994627833 0.9993392229 0.9975523949 + 0.9995027781 0.9993932843 0.9977238178 + 0.9995427132 0.9994474053 0.9978951812 + 0.9995825291 0.9995000958 0.9980673790 + 0.9996204972 0.9995455146 0.9982430935 + 0.9996584058 0.9995909929 0.9984188080 + 0.9996963739 0.9996364117 0.9985945225 + 0.9997342825 0.9996818900 0.9987701774 + 0.9997723103 0.9997273088 0.9989458919 + 0.9998102188 0.9997727871 0.9991214871 + 0.9998481870 0.9998182058 0.9992972016 + 0.9998860955 0.9998636842 0.9994729161 + 0.9999241233 0.9999091029 0.9996485710 + 0.9999619722 0.9999545813 0.9998242855 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_320t.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_320t.spi1d index 64ffe96f..ffa82842 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_320t.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_320t.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0002252978 0.0001790309 0.0001295420 - 0.0004642465 0.0003632053 0.0002623819 - 0.0007179323 0.0005541568 0.0004006781 - 0.0009859804 0.0007514893 0.0005452139 - 0.0012678721 0.0009573574 0.0006973416 - 0.0015588400 0.0011699020 0.0008563135 - 0.0018630330 0.0013911340 0.0010223610 - 0.0021819880 0.0016211460 0.0011926870 - 0.0025155889 0.0018565780 0.0013685439 - 0.0028599249 0.0021005161 0.0015487690 - 0.0032150650 0.0023508291 0.0017341210 - 0.0035848729 0.0026105260 0.0019261140 - 0.0039705001 0.0028831610 0.0021247759 - 0.0043733879 0.0031664821 0.0023306201 - 0.0047887620 0.0034602690 0.0025444049 - 0.0052145422 0.0037645160 0.0027661640 - 0.0056500668 0.0040802141 0.0029950261 - 0.0061007268 0.0044055800 0.0032312879 - 0.0065691760 0.0047432170 0.0034755419 - 0.0070610601 0.0050931652 0.0037271639 - 0.0075698970 0.0054551372 0.0039859852 - 0.0080941729 0.0058285771 0.0042510540 - 0.0086274194 0.0062132268 0.0045252559 - 0.0091798287 0.0066124098 0.0048091090 - 0.0097458120 0.0070239422 0.0051020859 - 0.0103271501 0.0074481708 0.0054048309 - 0.0109248599 0.0078837927 0.0057164561 - 0.0115405796 0.0083313556 0.0060368772 - 0.0121761197 0.0087913852 0.0063676350 - 0.0128270397 0.0092629055 0.0067081312 - 0.0134987198 0.0097476542 0.0070575029 - 0.0141942296 0.0102479504 0.0074174819 - 0.0149099398 0.0107645998 0.0077879899 - 0.0156464409 0.0112993503 0.0081683872 - 0.0164025500 0.0118529601 0.0085596293 - 0.0171775799 0.0124243395 0.0089649754 - 0.0179736298 0.0130097996 0.0093868207 - 0.0187904108 0.0136122899 0.0098197656 - 0.0196329206 0.0142326104 0.0102641098 - 0.0205026697 0.0148694804 0.0107198805 - 0.0213967599 0.0155238304 0.0111865401 - 0.0223194696 0.0161926802 0.0116670104 - 0.0232650694 0.0168744400 0.0121609699 - 0.0242343191 0.0175730493 0.0126681803 - 0.0252331793 0.0182904303 0.0131873302 - 0.0262477491 0.0190270096 0.0137195401 - 0.0272829495 0.0197816994 0.0142644998 - 0.0283390097 0.0205556992 0.0148215797 - 0.0294161700 0.0213519502 0.0153918499 - 0.0305210594 0.0221705697 0.0159767196 - 0.0316550806 0.0230139699 0.0165775400 - 0.0328192115 0.0238830894 0.0171922501 - 0.0340085700 0.0247773398 0.0178213306 - 0.0352222398 0.0256955195 0.0184620898 - 0.0364655294 0.0266327392 0.0191150103 - 0.0377436206 0.0275894795 0.0197834391 - 0.0390567891 0.0285663195 0.0204676799 - 0.0404002182 0.0295625906 0.0211681798 - 0.0417738296 0.0305766594 0.0218840092 - 0.0431732200 0.0316094011 0.0226133801 - 0.0445903614 0.0326650515 0.0233574491 - 0.0460246801 0.0337462984 0.0241182297 - 0.0474831313 0.0348512717 0.0248960592 - 0.0489709415 0.0359775797 0.0256870594 - 0.0504862815 0.0371238813 0.0264920406 - 0.0520257801 0.0382876694 0.0273139309 - 0.0535882413 0.0394724905 0.0281523801 - 0.0551759303 0.0406848602 0.0290120896 - 0.0567948110 0.0419235602 0.0298898704 - 0.0584423989 0.0431861505 0.0307816397 - 0.0601175204 0.0444719717 0.0316898413 - 0.0618218705 0.0457797199 0.0326160900 - 0.0635616332 0.0471059717 0.0335598104 - 0.0653382763 0.0484526493 0.0345212892 - 0.0671429336 0.0498179607 0.0355015099 - 0.0689680427 0.0512084812 0.0364993811 - 0.0708093792 0.0526283309 0.0375161916 - 0.0726710036 0.0540754311 0.0385530815 - 0.0745575577 0.0555453300 0.0396075509 - 0.0764711723 0.0570352189 0.0406817608 - 0.0784109309 0.0585439503 0.0417732410 - 0.0803771317 0.0600703284 0.0428790301 - 0.0823737681 0.0616120808 0.0439984985 - 0.0844059363 0.0631667823 0.0451391600 - 0.0864771232 0.0647454932 0.0462969095 - 0.0885818601 0.0663540736 0.0474717803 - 0.0907145590 0.0679863468 0.0486642793 - 0.0928772986 0.0696446896 0.0498744808 - 0.0950741023 0.0713270679 0.0511019900 - 0.0973075032 0.0730293319 0.0523469895 - 0.0995769203 0.0747480765 0.0536088012 - 0.1018780991 0.0764901415 0.0548868291 - 0.1042044014 0.0782487020 0.0561818182 - 0.1065538973 0.0800288767 0.0574982315 - 0.1089283004 0.0818311423 0.0588314198 - 0.1113215014 0.0836538672 0.0601810999 - 0.1137316003 0.0854980275 0.0615520887 - 0.1161573976 0.0873684287 0.0629485399 - 0.1185979992 0.0892611369 0.0643650293 - 0.1210614964 0.0911754370 0.0658011809 - 0.1235487014 0.0931112692 0.0672666430 - 0.1260568053 0.0950720683 0.0687462613 - 0.1285867989 0.0970498696 0.0702391118 - 0.1311426014 0.0990442634 0.0717507899 - 0.1337202042 0.1010560021 0.0732835308 - 0.1363096982 0.1031069979 0.0748315826 - 0.1389120072 0.1051762030 0.0763994083 - 0.1415282041 0.1072625965 0.0779832006 - 0.1441825926 0.1093714982 0.0795782432 - 0.1468576044 0.1115005016 0.0811884925 - 0.1495531946 0.1136446968 0.0828164518 - 0.1522718966 0.1158049032 0.0844542831 - 0.1550171971 0.1179929972 0.0861058980 - 0.1577734947 0.1201986000 0.0877750516 - 0.1605415046 0.1224215031 0.0894542485 - 0.1633353978 0.1246768013 0.0911507979 - 0.1661441028 0.1269477010 0.0928589627 - 0.1689610034 0.1292310059 0.0945746303 - 0.1717838943 0.1315293014 0.0963099599 - 0.1746120006 0.1338451058 0.0980574191 - 0.1774456948 0.1361752003 0.0998183563 - 0.1802875996 0.1385367960 0.1016084999 - 0.1831531972 0.1409143060 0.1034116000 - 0.1860264987 0.1433060020 0.1052436978 - 0.1889072955 0.1457217932 0.1070917994 - 0.1918103993 0.1481487006 0.1089527979 - 0.1947200000 0.1505914032 0.1108305007 - 0.1976352930 0.1530454010 0.1127144024 - 0.2005549073 0.1555085927 0.1146150976 - 0.2034773976 0.1579966992 0.1165283024 - 0.2064062953 0.1605020016 0.1184550002 - 0.2093463987 0.1630198956 0.1204015985 - 0.2122935951 0.1655519009 0.1223547012 - 0.2152556032 0.1680873036 0.1243321002 - 0.2182167023 0.1706292033 0.1263145953 - 0.2211762965 0.1731802970 0.1283138990 - 0.2241538018 0.1757373065 0.1303219944 - 0.2271365970 0.1783048064 0.1323408037 - 0.2301204056 0.1808757931 0.1343753040 - 0.2331143022 0.1834571958 0.1364185959 - 0.2361201942 0.1860488951 0.1384806037 - 0.2391359061 0.1886530071 0.1405466050 - 0.2421551943 0.1912735999 0.1426350027 - 0.2451844066 0.1939011961 0.1447259039 - 0.2482129037 0.1965422034 0.1468326002 - 0.2512401044 0.1991859972 0.1489423960 - 0.2542673051 0.2018409967 0.1510629058 - 0.2572943866 0.2045024037 0.1531887054 - 0.2603251040 0.2071764022 0.1553260982 - 0.2633563876 0.2098606974 0.1574766040 - 0.2663846016 0.2125551999 0.1596474946 - 0.2694126070 0.2152567953 0.1618286073 - 0.2724404037 0.2179622054 0.1640225053 - 0.2754598856 0.2206754982 0.1662251055 - 0.2784681916 0.2233911008 0.1684374958 - 0.2814692855 0.2261153013 0.1706584990 - 0.2844651043 0.2288406044 0.1728888005 - 0.2874703109 0.2315697968 0.1751244962 - 0.2904790938 0.2343001068 0.1773661971 - 0.2934839129 0.2370491028 0.1796108931 - 0.2964878976 0.2397976965 0.1818595976 - 0.2994953096 0.2425439954 0.1841097027 - 0.3025026917 0.2452909946 0.1863622963 - 0.3054913878 0.2480435967 0.1886177063 - 0.3084802032 0.2507981956 0.1908794940 - 0.3114694059 0.2535662055 0.1931429952 - 0.3144572079 0.2563355863 0.1954129934 - 0.3174341023 0.2591148913 0.1976844966 - 0.3204114139 0.2618939877 0.1999710053 - 0.3233911097 0.2646703124 0.2022576034 - 0.3263702095 0.2674466074 0.2045578957 - 0.3293448985 0.2702299058 0.2068590969 - 0.3323191106 0.2730130851 0.2091719061 - 0.3352886140 0.2757953107 0.2114882022 - 0.3382574916 0.2785772979 0.2138084024 - 0.3412111998 0.2813608050 0.2161314934 - 0.3441649079 0.2841446996 0.2184565067 - 0.3471063972 0.2869217098 0.2207846045 - 0.3500475883 0.2896957994 0.2231137007 - 0.3529730141 0.2924658060 0.2254499942 - 0.3558960855 0.2952325046 0.2277863026 - 0.3588060141 0.2979984879 0.2301243991 - 0.3617117107 0.3007633984 0.2324627936 - 0.3645960987 0.3035242856 0.2348098010 - 0.3674674034 0.3062672913 0.2371633947 - 0.3703210056 0.3090102971 0.2395182997 - 0.3731533885 0.3117265999 0.2418767959 - 0.3759778142 0.3144418001 0.2442353964 - 0.3787800074 0.3171553910 0.2465995997 - 0.3815816045 0.3198683858 0.2489643991 - 0.3843731880 0.3225778937 0.2513361871 - 0.3871648014 0.3252829909 0.2537139058 - 0.3899506927 0.3279863000 0.2560917139 - 0.3927353919 0.3306789994 0.2584705949 - 0.3955127001 0.3333716094 0.2608495951 - 0.3982842863 0.3360339999 0.2632276118 - 0.4010469019 0.3386904001 0.2656053007 - 0.4037859142 0.3413349092 0.2679820061 - 0.4065249860 0.3439669013 0.2703562975 - 0.4092408121 0.3465988040 0.2727305889 - 0.4119558036 0.3492295146 0.2751063108 - 0.4146685004 0.3518601954 0.2774823904 - 0.4173798859 0.3544926047 0.2798556983 - 0.4200862050 0.3571256995 0.2822236121 - 0.4227792919 0.3597556055 0.2845914066 - 0.4254722893 0.3623783886 0.2869648039 - 0.4281426072 0.3650011122 0.2893395126 - 0.4308102131 0.3676072955 0.2917163968 - 0.4334703088 0.3702105880 0.2940998971 - 0.4361222088 0.3728044033 0.2964833975 - 0.4387736022 0.3753828108 0.2988683879 - 0.4414154887 0.3779611886 0.3012540936 - 0.4440574050 0.3805244863 0.3036401868 - 0.4466896951 0.3830857873 0.3060306907 - 0.4493151903 0.3856397867 0.3084211946 - 0.4519402087 0.3881801963 0.3108114004 - 0.4545603991 0.3907206953 0.3132013977 - 0.4571805894 0.3932437003 0.3155913949 - 0.4597933888 0.3957622051 0.3179830909 - 0.4624007940 0.3982788026 0.3203752935 - 0.4650070071 0.4007883966 0.3227669895 - 0.4675796926 0.4032979906 0.3251537979 - 0.4701522887 0.4057931900 0.3275406957 - 0.4727115929 0.4082787931 0.3299230933 - 0.4752551019 0.4107643068 0.3322981000 - 0.4777986109 0.4132384062 0.3346731067 - 0.4803014100 0.4157119989 0.3370327055 - 0.4827977121 0.4181810915 0.3393830955 - 0.4852870107 0.4206390083 0.3417334855 - 0.4877533019 0.4230970144 0.3440715075 - 0.4902195036 0.4255495965 0.3464072049 - 0.4926728904 0.4279986918 0.3487423956 - 0.4951168895 0.4304477870 0.3510628939 - 0.4975608885 0.4328849018 0.3533833921 - 0.4999994040 0.4353202879 0.3556984961 - 0.5024372935 0.4377534091 0.3579910994 - 0.5048713088 0.4401684999 0.3602836132 - 0.5072880983 0.4425837100 0.3625684083 - 0.5097048879 0.4449890852 0.3648371100 - 0.5121037960 0.4473744929 0.3671058118 - 0.5144798160 0.4497599006 0.3693706095 - 0.5168557167 0.4521426857 0.3716300130 - 0.5192152858 0.4545232058 0.3738893867 - 0.5215666294 0.4569036067 0.3761452138 - 0.5239177942 0.4592863917 0.3783971071 - 0.5262326002 0.4616703987 0.3806490004 - 0.5285415053 0.4640542865 0.3828856945 - 0.5308492184 0.4664343894 0.3851085007 - 0.5331150889 0.4688135087 0.3873313069 - 0.5353810191 0.4711925089 0.3895424902 - 0.5376406908 0.4735459089 0.3917432129 - 0.5398690104 0.4758956134 0.3939439058 - 0.5420972705 0.4782454073 0.3961327970 - 0.5443140864 0.4805634022 0.3983095884 - 0.5464997292 0.4828793108 0.4004862905 - 0.5486851931 0.4851951003 0.4026562870 - 0.5508528948 0.4874885082 0.4048171043 - 0.5529860854 0.4897811115 0.4069780111 - 0.5551192164 0.4920735955 0.4091326892 - 0.5572410822 0.4943391979 0.4112750888 - 0.5593448877 0.4966034889 0.4134174883 - 0.5614485741 0.4988678992 0.4155569077 - 0.5635406971 0.5010880828 0.4176844060 - 0.5656147003 0.5033046007 0.4198119938 - 0.5676888227 0.5055211186 0.4219393134 - 0.5697504878 0.5077061057 0.4240606129 - 0.5717914104 0.5098856091 0.4261820018 - 0.5738322139 0.5120651722 0.4283033907 - 0.5758668780 0.5142083168 0.4304130077 - 0.5778881907 0.5163393021 0.4325203001 - 0.5799093843 0.5184702277 0.4346275926 - 0.5819228292 0.5205832124 0.4367291033 - 0.5839092731 0.5226848125 0.4388267994 - 0.5858957171 0.5247864127 0.4409244955 - 0.5878791809 0.5268790722 0.4430136979 - 0.5898365974 0.5289608240 0.4450865090 - 0.5917940140 0.5310423970 0.4471594095 - 0.5937513709 0.5331161022 0.4492312074 - 0.5956690907 0.5351639986 0.4512765110 - 0.5975846052 0.5372117758 0.4533218145 - 0.5995001197 0.5392596722 0.4553669989 - 0.6013880968 0.5412784815 0.4573917985 - 0.6032671928 0.5432971716 0.4594076872 - 0.6051464081 0.5453159213 0.4614236057 - 0.6070122123 0.5473181009 0.4634315073 - 0.6088663936 0.5493139029 0.4654226899 - 0.6107205153 0.5513098240 0.4674139023 - 0.6125701070 0.5532947779 0.4694051147 - 0.6144073009 0.5552641749 0.4713697135 - 0.6162444949 0.5572335720 0.4733316898 - 0.6180818081 0.5592026711 0.4752937853 - 0.6199104190 0.5611671209 0.4772514999 - 0.6217387915 0.5631313920 0.4792042077 - 0.6235671043 0.5650957823 0.4811568856 - 0.6253790855 0.5670524836 0.4831095934 - 0.6271820068 0.5690057278 0.4850563109 - 0.6289849281 0.5709587932 0.4870027900 - 0.6307768226 0.5729085207 0.4889493883 - 0.6325399876 0.5748482943 0.4908913076 - 0.6343032122 0.5767881870 0.4928275049 - 0.6360663772 0.5787280798 0.4947637022 - 0.6378102899 0.5806527734 0.4966998994 - 0.6395515203 0.5825737715 0.4986213148 - 0.6412925720 0.5844948292 0.5005409122 - 0.6430225968 0.5864080787 0.5024604797 - 0.6447380185 0.5883042812 0.5043705106 - 0.6464533806 0.5902004242 0.5062571168 - 0.6481688023 0.5920965075 0.5081436038 - 0.6498512030 0.5939829946 0.5100302100 - 0.6515321136 0.5958667994 0.5119035840 - 0.6532130241 0.5977506042 0.5137699842 - 0.6548699141 0.5996311903 0.5156363249 - 0.6564965248 0.6014996767 0.5175027251 - 0.6581230760 0.6033681035 0.5193520188 - 0.6597496867 0.6052364707 0.5212007165 - 0.6613441706 0.6070837975 0.5230494738 - 0.6629346013 0.6089169979 0.5248932838 - 0.6645249724 0.6107501984 0.5267223120 - 0.6661086082 0.6125832796 0.5285513997 - 0.6676759124 0.6143841743 0.5303804278 - 0.6692432761 0.6161835194 0.5322028995 - 0.6708106995 0.6179828048 0.5340183973 - 0.6723604202 0.6197698712 0.5358340144 - 0.6739010811 0.6215232015 0.5376495123 - 0.6754419208 0.6232764721 0.5394585729 - 0.6769825816 0.6250298023 0.5412647724 - 0.6784970164 0.6267579794 0.5430709720 - 0.6800110936 0.6284642816 0.5448771119 - 0.6815252900 0.6301707029 0.5466737747 - 0.6830374002 0.6318771243 0.5484687090 - 0.6845443249 0.6335585117 0.5502635837 - 0.6860511899 0.6352325082 0.5520585775 - 0.6875581145 0.6369063854 0.5538393855 - 0.6890510917 0.6385803223 0.5556197762 - 0.6905319095 0.6402255893 0.5574001074 - 0.6920126081 0.6418696046 0.5591800213 - 0.6934934258 0.6435136199 0.5609490871 - 0.6949405074 0.6451522708 0.5627182126 - 0.6963760257 0.6467463970 0.5644872189 - 0.6978114843 0.6483405232 0.5662546158 - 0.6992468834 0.6499347091 0.5680006146 - 0.7006595135 0.6515222788 0.5697466135 - 0.7020696998 0.6530866027 0.5714926720 - 0.7034800053 0.6546509266 0.5732375979 - 0.7048898935 0.6562151909 0.5749657750 - 0.7062891126 0.6577690244 0.5766940713 - 0.7076883912 0.6592959762 0.5784224272 - 0.7090877295 0.6608229876 0.5801504254 - 0.7104840875 0.6623499990 0.5818541050 - 0.7118595243 0.6638725996 0.5835577846 - 0.7132350206 0.6653854251 0.5852615237 - 0.7146103978 0.6668980718 0.5869652033 - 0.7159860134 0.6684108973 0.5886417031 - 0.7173621058 0.6699141264 0.5903154016 - 0.7187381983 0.6713935733 0.5919892192 - 0.7201142907 0.6728730798 0.5936629772 - 0.7214856148 0.6743525863 0.5953096747 - 0.7228314281 0.6758230925 0.5969482064 - 0.7241771221 0.6772614717 0.5985866785 - 0.7255228758 0.6786999702 0.6002252102 - 0.7268646955 0.6801384091 0.6018486023 - 0.7281752825 0.6815736294 0.6034609079 - 0.7294859290 0.6829831004 0.6050732136 - 0.7307965159 0.6843926907 0.6066855192 - 0.7321065068 0.6858022213 0.6082887053 - 0.7333903909 0.6872118115 0.6098747253 - 0.7346742749 0.6885958910 0.6114608049 - 0.7359582782 0.6899790168 0.6130468249 - 0.7372422218 0.6913622022 0.6146309972 - 0.7385115027 0.6927453279 0.6161890030 - 0.7397789955 0.6941304803 0.6177470088 - 0.7410464883 0.6955162883 0.6193048954 - 0.7423139811 0.6969019771 0.6208629012 - 0.7435634732 0.6982877851 0.6223968267 - 0.7448058128 0.6996648908 0.6239222288 - 0.7460480928 0.7010340095 0.6254476905 - 0.7472903728 0.7024030089 0.6269730926 - 0.7485197783 0.7037721276 0.6284868121 - 0.7497360110 0.7051346898 0.6299803257 - 0.7509521842 0.7064757943 0.6314738989 - 0.7521684170 0.7078167796 0.6329674721 - 0.7533807158 0.7091578841 0.6344609857 - 0.7545793056 0.7104989886 0.6359264851 - 0.7557778955 0.7118229270 0.6373904943 - 0.7569764256 0.7131447792 0.6388545036 - 0.7581750154 0.7144666910 0.6403185129 - 0.7593536973 0.7157886028 0.6417623162 - 0.7605302930 0.7170932889 0.6431841850 - 0.7617068887 0.7183820009 0.6446061730 - 0.7628834844 0.7196707129 0.6460281014 - 0.7640433908 0.7209593058 0.6474500895 - 0.7651894093 0.7222459912 0.6488506198 - 0.7663354278 0.7235112786 0.6502503753 - 0.7674813867 0.7247765064 0.6516500711 - 0.7686260939 0.7260417938 0.6530498266 - 0.7697625160 0.7273070812 0.6544442773 - 0.7708989978 0.7285519838 0.6558309197 - 0.7720354795 0.7297835946 0.6572173834 - 0.7731719017 0.7310153246 0.6586040258 - 0.7742877007 0.7322468758 0.6599906087 - 0.7753937244 0.7334775925 0.6613616943 - 0.7764996886 0.7346972227 0.6627286077 - 0.7776057124 0.7359166741 0.6640955210 - 0.7787057161 0.7371363044 0.6654623747 - 0.7797777057 0.7383558154 0.6668282747 - 0.7808495760 0.7395550013 0.6681864858 - 0.7819216251 0.7407342792 0.6695446968 - 0.7829934955 0.7419134974 0.6709029078 - 0.7840589285 0.7430927753 0.6722611785 - 0.7851203084 0.7442719936 0.6736127734 - 0.7861816883 0.7454271913 0.6749550104 - 0.7872430086 0.7465791702 0.6762971282 - 0.7883045077 0.7477310896 0.6776393056 - 0.7893705964 0.7488831282 0.6789814234 - 0.7904366851 0.7500287294 0.6803113818 - 0.7915027738 0.7511497736 0.6816344857 - 0.7925689220 0.7522708774 0.6829575896 - 0.7936292291 0.7533919811 0.6842806935 - 0.7946795225 0.7545130849 0.6856039166 - 0.7957298160 0.7556204796 0.6869193912 - 0.7967801094 0.7567132115 0.6882331967 - 0.7978304029 0.7578058243 0.6895471215 - 0.7988722920 0.7588983774 0.6908609867 - 0.7999097109 0.7599909902 0.6921749115 - 0.8009471893 0.7610651851 0.6934753060 - 0.8019847274 0.7621316910 0.6947749853 - 0.8030220866 0.7631980777 0.6960747838 - 0.8040328026 0.7642645240 0.6973745227 - 0.8050405979 0.7653309107 0.6986737847 - 0.8060483932 0.7663741708 0.6999592781 - 0.8070561886 0.7674140930 0.7012447715 - 0.8080614805 0.7684540153 0.7025303245 - 0.8090434074 0.7694938779 0.7038156986 - 0.8100252748 0.7705338001 0.7050995827 - 0.8110070825 0.7715379000 0.7063539028 - 0.8119890094 0.7725417018 0.7076082230 - 0.8129674792 0.7735455036 0.7088624835 - 0.8139352798 0.7745493054 0.7101168036 - 0.8149030805 0.7755506039 0.7113707066 - 0.8158710003 0.7765091062 0.7126060128 - 0.8168388009 0.7774677277 0.7138411999 - 0.8177986741 0.7784262896 0.7150765061 - 0.8187428117 0.7793847919 0.7163118124 - 0.8196867704 0.7803425789 0.7175469995 - 0.8206307888 0.7812892795 0.7187737226 - 0.8215748072 0.7822360992 0.7199997902 - 0.8225091100 0.7831827998 0.7212259173 - 0.8234264851 0.7841296196 0.7224519849 - 0.8243438005 0.7850759029 0.7236781120 - 0.8252611756 0.7860084176 0.7248950005 - 0.8261786103 0.7869408727 0.7261095047 - 0.8270888925 0.7878733873 0.7273240089 - 0.8279858232 0.7888059020 0.7285385728 - 0.8288828135 0.7897384167 0.7297530770 - 0.8297798038 0.7906383872 0.7309511900 - 0.8306767941 0.7915360928 0.7321383953 - 0.8315675855 0.7924336791 0.7333257198 - 0.8324421048 0.7933313847 0.7345129251 - 0.8333166242 0.7942289710 0.7357000709 - 0.8341910839 0.7951080203 0.7368817925 - 0.8350657225 0.7959815860 0.7380530834 - 0.8359373212 0.7968552709 0.7392244935 - 0.8367924094 0.7977288961 0.7403959036 - 0.8376473784 0.7986025810 0.7415671945 - 0.8385024071 0.7994632721 0.7427378297 - 0.8393573761 0.8003144264 0.7438889742 - 0.8402125239 0.8011655211 0.7450402975 - 0.8410465717 0.8020166159 0.7461915016 - 0.8418799043 0.8028677106 0.7473427057 - 0.8427131772 0.8037161231 0.7484940290 - 0.8435465097 0.8045580983 0.7496194839 - 0.8443799019 0.8054001927 0.7507324815 - 0.8452067971 0.8062422276 0.7518454194 - 0.8460311890 0.8070843220 0.7529584169 - 0.8468557000 0.8079264164 0.7540712953 - 0.8476800919 0.8087493777 0.7551763058 - 0.8485044837 0.8095721006 0.7562569976 - 0.8493136764 0.8103948832 0.7573378086 - 0.8501023054 0.8112176061 0.7584185004 - 0.8508908749 0.8120403886 0.7594993114 - 0.8516795039 0.8128635883 0.7605800033 - 0.8524680734 0.8136872053 0.7616460919 - 0.8532543182 0.8145108223 0.7627075911 - 0.8540086150 0.8153343797 0.7637690902 - 0.8547629714 0.8161579967 0.7648305893 - 0.8555173278 0.8169798255 0.7658920884 - 0.8562716842 0.8177930117 0.7669504285 - 0.8570259809 0.8186061978 0.7679973245 - 0.8577690721 0.8194193840 0.7690442204 - 0.8585063815 0.8202325106 0.7700911164 - 0.8592435718 0.8210456967 0.7711378932 - 0.8599808812 0.8218513727 0.7721847892 - 0.8607181907 0.8226534724 0.7732229233 - 0.8614515066 0.8234555125 0.7742553949 - 0.8621680737 0.8242576122 0.7752879858 - 0.8628845811 0.8250595927 0.7763205171 - 0.8636012077 0.8258600831 0.7773531079 - 0.8643177748 0.8266490102 0.7783856988 - 0.8650344014 0.8274379969 0.7793797255 - 0.8657494187 0.8282269239 0.7803717256 - 0.8664636016 0.8290157914 0.7813637853 - 0.8671777844 0.8298047185 0.7823559046 - 0.8678920269 0.8305819035 0.7833480239 - 0.8686062098 0.8313472867 0.7843325734 - 0.8693193793 0.8321127295 0.7852917910 - 0.8700184822 0.8328779936 0.7862508893 - 0.8707175851 0.8336433768 0.7872101068 - 0.8714166880 0.8344088197 0.7881693244 - 0.8721157908 0.8351647258 0.7891284227 - 0.8728148937 0.8359184861 0.7900806069 - 0.8734970093 0.8366723061 0.7910240293 - 0.8741555810 0.8374261856 0.7919672728 - 0.8748142123 0.8381800056 0.7929106951 - 0.8754727840 0.8389329910 0.7938539982 - 0.8761314750 0.8396766782 0.7947974205 - 0.8767901063 0.8404204249 0.7957333922 - 0.8774240017 0.8411641121 0.7966641784 - 0.8780478835 0.8419076800 0.7975950241 - 0.8786717057 0.8426514268 0.7985258102 - 0.8792955279 0.8433899879 0.7994565964 - 0.8799194098 0.8441156745 0.8003873825 - 0.8805431724 0.8448413014 0.8012884855 - 0.8811553121 0.8455669880 0.8021752238 - 0.8817664981 0.8462926149 0.8030619025 - 0.8823778033 0.8470183015 0.8039487004 - 0.8829889894 0.8477351069 0.8048353791 - 0.8836002946 0.8484396935 0.8057221174 - 0.8842098713 0.8491442800 0.8065841794 - 0.8848046064 0.8498489261 0.8074355125 - 0.8853992820 0.8505535126 0.8082867861 - 0.8859940171 0.8512580991 0.8091381192 - 0.8865886927 0.8519487977 0.8099893928 - 0.8871833086 0.8526254892 0.8108407259 - 0.8877705932 0.8533021212 0.8116824031 - 0.8883280158 0.8539788127 0.8125190139 - 0.8888853788 0.8546553850 0.8133556247 - 0.8894428015 0.8553320169 0.8141922951 - 0.8900002241 0.8560088277 0.8150289059 - 0.8905575871 0.8566855788 0.8158655167 - 0.8911083937 0.8573625088 0.8166936040 - 0.8916376829 0.8580393195 0.8175144792 - 0.8921670914 0.8587161899 0.8183354735 - 0.8926963806 0.8593930006 0.8191564083 - 0.8932257295 0.8600615859 0.8199774027 - 0.8937550783 0.8607198000 0.8207982779 - 0.8942791820 0.8613780141 0.8216099739 - 0.8947834969 0.8620361090 0.8224058747 - 0.8952878118 0.8626943231 0.8232018948 - 0.8957921863 0.8633524776 0.8239977956 - 0.8962965012 0.8640044928 0.8247938156 - 0.8968008161 0.8646433949 0.8255897760 - 0.8973022103 0.8652822971 0.8263819218 - 0.8977811933 0.8659210801 0.8271486163 - 0.8982601166 0.8665599823 0.8279151917 - 0.8987389803 0.8671988845 0.8286818862 - 0.8992179036 0.8678355217 0.8294485211 - 0.8996968865 0.8684571981 0.8302152157 - 0.9001758099 0.8690788746 0.8309819102 - 0.9006456137 0.8697006106 0.8317354918 - 0.9011150002 0.8703222871 0.8324862123 - 0.9015843272 0.8709440827 0.8332368731 - 0.9020537138 0.8715658188 0.8339877129 - 0.9025231004 0.8721610904 0.8347383738 - 0.9029924273 0.8727529049 0.8354892135 - 0.9034619927 0.8733446002 0.8362295032 - 0.9039316773 0.8739364147 0.8369569182 - 0.9044014812 0.8745281100 0.8376842737 - 0.9048712254 0.8751199245 0.8384116888 - 0.9053409100 0.8756937981 0.8391389847 - 0.9058105946 0.8762534857 0.8398663998 - 0.9062727094 0.8768131137 0.8405938148 - 0.9067248106 0.8773728013 0.8413041234 - 0.9071769118 0.8779324889 0.8420143127 - 0.9076290131 0.8784921169 0.8427246213 - 0.9080811143 0.8790519238 0.8434348702 - 0.9085332751 0.8796120286 0.8441451192 - 0.9089835286 0.8801720738 0.8448554277 - 0.9094098210 0.8807322979 0.8455566764 - 0.9098359942 0.8812924027 0.8462482095 - 0.9102622867 0.8818526268 0.8469398022 - 0.9106885791 0.8824126720 0.8476313949 - 0.9111148119 0.8829627037 0.8483229876 - 0.9115411043 0.8835083246 0.8490145206 - 0.9119545221 0.8840540051 0.8497061133 - 0.9123607874 0.8845996857 0.8503819108 - 0.9127669930 0.8851453066 0.8510559201 - 0.9131733179 0.8856909871 0.8517298102 - 0.9135795236 0.8862351179 0.8524037004 - 0.9139857888 0.8867712021 0.8530777097 - 0.9143912196 0.8873074055 0.8537515998 - 0.9147915244 0.8878436089 0.8544242978 - 0.9151918292 0.8883798122 0.8550928831 - 0.9155920744 0.8889160156 0.8557615280 - 0.9159923792 0.8894522190 0.8564301133 - 0.9163928032 0.8899856806 0.8570985794 - 0.9167931080 0.8905174136 0.8577672243 - 0.9171940088 0.8910490274 0.8584358096 - 0.9175955057 0.8915805817 0.8590922952 - 0.9179968834 0.8921123147 0.8597366810 - 0.9183983207 0.8926439285 0.8603811860 - 0.9187998176 0.8931754827 0.8610256910 - 0.9192011952 0.8936873078 0.8616700768 - 0.9196026921 0.8941966891 0.8623145819 - 0.9199982882 0.8947061896 0.8629590869 - 0.9203935862 0.8952155709 0.8635929823 - 0.9207888246 0.8957250714 0.8642224073 - 0.9211840034 0.8962345123 0.8648517728 - 0.9215791821 0.8967435956 0.8654811978 - 0.9219744205 0.8972501755 0.8661106229 - 0.9223681092 0.8977568150 0.8667399883 - 0.9227561951 0.8982635140 0.8673694134 - 0.9231444001 0.8987700939 0.8679875731 - 0.9235324860 0.8992767930 0.8686034083 - 0.9239205718 0.8997833729 0.8692191839 - 0.9243087769 0.9002879262 0.8698350191 - 0.9246969223 0.9007881284 0.8704507947 - 0.9250844717 0.9012882710 0.8710665703 - 0.9254714251 0.9017884135 0.8716825247 - 0.9258583188 0.9022886157 0.8722918034 - 0.9262452126 0.9027888179 0.8729001880 - 0.9266321063 0.9032890201 0.8735085726 - 0.9270188808 0.9037815928 0.8741170168 - 0.9274057746 0.9042645097 0.8747254014 - 0.9277873039 0.9047474265 0.8753337860 - 0.9281644821 0.9052304029 0.8759421706 - 0.9285417199 0.9057133198 0.8765338063 - 0.9289188981 0.9061961770 0.8771222234 - 0.9292961955 0.9066790938 0.8777105212 - 0.9296733737 0.9071536064 0.8782988787 - 0.9300506115 0.9076185226 0.8788872957 - 0.9304202199 0.9080834985 0.8794757128 - 0.9307851791 0.9085484147 0.8800640106 - 0.9311501980 0.9090133905 0.8806303740 - 0.9315152764 0.9094783068 0.8811889887 - 0.9318802953 0.9099432826 0.8817476034 - 0.9322453141 0.9103996754 0.8823062181 - 0.9326102734 0.9108446240 0.8828647733 - 0.9329578280 0.9112895131 0.8834233880 - 0.9332942963 0.9117342830 0.8839820027 - 0.9336308241 0.9121791720 0.8845335245 - 0.9339672923 0.9126241207 0.8850793839 - 0.9343038201 0.9130688906 0.8856253028 - 0.9346402884 0.9135128260 0.8861711025 - 0.9349768162 0.9139543176 0.8867170215 - 0.9352998137 0.9143959284 0.8872628808 - 0.9356107712 0.9148374200 0.8878087997 - 0.9359217882 0.9152789116 0.8883525729 - 0.9362328053 0.9157204032 0.8888916969 - 0.9365438819 0.9161620140 0.8894308209 - 0.9368548989 0.9166017175 0.8899700046 - 0.9371659160 0.9170259833 0.8905091286 - 0.9374731183 0.9174504280 0.8910481930 - 0.9377737045 0.9178748131 0.8915873170 - 0.9380741715 0.9182991982 0.8921263814 - 0.9383748174 0.9187235832 0.8926637769 - 0.9386752844 0.9191479087 0.8932011127 - 0.9389758706 0.9195722938 0.8937383294 - 0.9392765164 0.9199801087 0.8942756057 - 0.9395747185 0.9203845263 0.8948128819 - 0.9398567080 0.9207890034 0.8953502178 - 0.9401388168 0.9211934209 0.8958874941 - 0.9404208064 0.9215978980 0.8964194059 - 0.9407027960 0.9220023751 0.8969466090 - 0.9409849048 0.9224067926 0.8974738717 - 0.9412668943 0.9228081107 0.8980011940 - 0.9415488839 0.9232060909 0.8985285163 - 0.9418272972 0.9236040115 0.8990557194 - 0.9421048760 0.9240018725 0.8995829821 - 0.9423825145 0.9243999124 0.9001092911 - 0.9426599741 0.9247977734 0.9006150961 - 0.9429376125 0.9251956940 0.9011210203 - 0.9432151914 0.9255933166 0.9016268253 - 0.9434927106 0.9259833097 0.9021326900 - 0.9437695742 0.9263733029 0.9026384950 - 0.9440454841 0.9267632961 0.9031443000 - 0.9443213940 0.9271534085 0.9036502242 - 0.9445973039 0.9275434017 0.9041491747 - 0.9448732138 0.9279333949 0.9046404958 - 0.9451491237 0.9283233881 0.9051318169 - 0.9454249740 0.9286991954 0.9056230783 - 0.9457010031 0.9290624261 0.9061143994 - 0.9459671974 0.9294254780 0.9066057205 - 0.9462327957 0.9297887087 0.9070969224 - 0.9464982748 0.9301518798 0.9075881839 - 0.9467638135 0.9305151105 0.9080798030 - 0.9470294118 0.9308782816 0.9085714817 - 0.9472948909 0.9312413931 0.9090631008 - 0.9475604296 0.9315894246 0.9095547199 - 0.9478178024 0.9319362044 0.9100463986 - 0.9480611086 0.9322831035 0.9105380177 - 0.9483044744 0.9326300025 0.9110296965 - 0.9485477805 0.9329767823 0.9115203023 - 0.9487910867 0.9333236814 0.9119966030 - 0.9490345120 0.9336705804 0.9124727845 - 0.9492778182 0.9340136051 0.9129490852 - 0.9495211244 0.9343457222 0.9134253860 - 0.9497414231 0.9346777797 0.9139016271 - 0.9499527812 0.9350097775 0.9143779278 - 0.9501643181 0.9353418946 0.9148542285 - 0.9503756762 0.9356740117 0.9153259993 - 0.9505872130 0.9360061288 0.9157835841 - 0.9507985711 0.9363381267 0.9162412286 - 0.9510101080 0.9366635084 0.9166988134 - 0.9512215257 0.9369819164 0.9171562791 - 0.9514275789 0.9373003840 0.9176139235 - 0.9516335726 0.9376189113 0.9180715084 - 0.9518396258 0.9379373193 0.9185290933 - 0.9520456791 0.9382557869 0.9189860821 - 0.9522516727 0.9385743141 0.9194421172 - 0.9524577856 0.9388927221 0.9198979735 - 0.9526637793 0.9392104745 0.9203540087 - 0.9528685808 0.9395279288 0.9208099246 - 0.9530680180 0.9398452044 0.9212657809 - 0.9532672763 0.9401625991 0.9217218161 - 0.9534667134 0.9404799938 0.9221776724 - 0.9536660910 0.9407973289 0.9226272106 - 0.9538655281 0.9411147237 0.9230633974 - 0.9540649056 0.9414321184 0.9234995246 - 0.9542642832 0.9417477250 0.9239355922 - 0.9544624090 0.9420624971 0.9243717194 - 0.9546576738 0.9423773289 0.9248077869 - 0.9548529983 0.9426922202 0.9252439737 - 0.9550482035 0.9430069923 0.9256801009 - 0.9552435279 0.9433218837 0.9261130095 - 0.9554387927 0.9436367154 0.9265359044 - 0.9556339979 0.9439516068 0.9269587994 - 0.9558293223 0.9442648888 0.9273816943 - 0.9560242295 0.9445775747 0.9278045297 - 0.9562184215 0.9448903799 0.9282274246 - 0.9564124942 0.9452031255 0.9286503196 - 0.9566066861 0.9455158114 0.9290730953 - 0.9568008780 0.9458284974 0.9294950962 - 0.9569950104 0.9461413026 0.9299049973 - 0.9571892023 0.9464539886 0.9303148985 - 0.9573832750 0.9467725158 0.9307246804 - 0.9575788975 0.9470949173 0.9311345816 - 0.9577786922 0.9474173188 0.9315444827 - 0.9579784274 0.9477397799 0.9319543242 - 0.9581782222 0.9480621815 0.9323642254 - 0.9583780169 0.9483845830 0.9327741265 - 0.9585776925 0.9487069845 0.9331759810 - 0.9587774873 0.9490293860 0.9335759282 - 0.9589772820 0.9493505955 0.9339758158 - 0.9591774940 0.9496698976 0.9343758225 - 0.9593815804 0.9499891996 0.9347757101 - 0.9595857263 0.9503086209 0.9351757169 - 0.9597898126 0.9506279230 0.9355756044 - 0.9599938989 0.9509472251 0.9359754920 - 0.9601979852 0.9512665272 0.9363701940 - 0.9604020715 0.9515858889 0.9367583990 - 0.9606062174 0.9519029856 0.9371466041 - 0.9608103037 0.9522051215 0.9375349283 - 0.9610272050 0.9525070786 0.9379230738 - 0.9612463713 0.9528092146 0.9383112788 - 0.9614655972 0.9531112909 0.9386994839 - 0.9616847038 0.9534133077 0.9390878081 - 0.9619039297 0.9537153840 0.9394757748 - 0.9621229768 0.9540174007 0.9398490787 - 0.9623422027 0.9543194771 0.9402223229 - 0.9625613093 0.9546111822 0.9405955076 - 0.9627830982 0.9548994899 0.9409686923 - 0.9630070925 0.9551877975 0.9413419962 - 0.9632312059 0.9554761052 0.9417151809 - 0.9634553194 0.9557644129 0.9420884252 - 0.9636793733 0.9560527802 0.9424616098 - 0.9639034271 0.9563410878 0.9428309202 - 0.9641274810 0.9566293955 0.9431958199 - 0.9643515944 0.9569211006 0.9435607791 - 0.9645748734 0.9572210908 0.9439256787 - 0.9647920132 0.9575210214 0.9442906976 - 0.9650089741 0.9578210115 0.9446555972 - 0.9652261138 0.9581208825 0.9450206161 - 0.9654431939 0.9584208131 0.9453855157 - 0.9656602144 0.9587208033 0.9457504749 - 0.9658772945 0.9590206742 0.9461066127 - 0.9660943747 0.9593207240 0.9464613199 - 0.9663113952 0.9596204162 0.9468160272 - 0.9665256739 0.9599201083 0.9471706152 - 0.9667379260 0.9602196813 0.9475253224 - 0.9669501185 0.9605193734 0.9478799105 - 0.9671623111 0.9608191252 0.9482346177 - 0.9673745036 0.9611188173 0.9485892057 - 0.9675868154 0.9614183903 0.9489434958 - 0.9677990079 0.9617180824 0.9492951035 - 0.9680112004 0.9620164037 0.9496467113 - 0.9682233930 0.9623020887 0.9499983191 - 0.9684261084 0.9625877142 0.9503499866 - 0.9686285853 0.9628733993 0.9507015944 - 0.9688311219 0.9631590843 0.9510532022 - 0.9690335989 0.9634447098 0.9514048100 - 0.9692360759 0.9637303948 0.9517564178 - 0.9694386125 0.9640160203 0.9521070719 - 0.9696410894 0.9643017054 0.9524558187 - 0.9698436260 0.9645820260 0.9528045058 - 0.9700453281 0.9648528099 0.9531531930 - 0.9702448845 0.9651235938 0.9535018802 - 0.9704446197 0.9653943777 0.9538506269 - 0.9706441760 0.9656652212 0.9541993737 - 0.9708439112 0.9659358859 0.9545481205 - 0.9710435271 0.9662067294 0.9548968077 - 0.9712432027 0.9664775133 0.9552431107 - 0.9714428782 0.9667482972 0.9555861950 - 0.9716424942 0.9670147896 0.9559292197 - 0.9718437195 0.9672774076 0.9562721848 - 0.9720464945 0.9675399065 0.9566152096 - 0.9722493291 0.9678025246 0.9569581747 - 0.9724521041 0.9680650830 0.9573013186 - 0.9726549983 0.9683277011 0.9576442838 - 0.9728577733 0.9685903192 0.9579873085 - 0.9730606079 0.9688528776 0.9583258033 - 0.9732633829 0.9691154957 0.9586578012 - 0.9734662175 0.9693738818 0.9589897990 - 0.9736676216 0.9696295261 0.9593217969 - 0.9738680124 0.9698851109 0.9596537948 - 0.9740685225 0.9701406956 0.9599856734 - 0.9742689133 0.9703962803 0.9603176713 - 0.9744693041 0.9706519246 0.9606497288 - 0.9746698141 0.9709075093 0.9609817266 - 0.9748702049 0.9711630940 0.9613105059 - 0.9750707150 0.9714187980 0.9616318941 - 0.9752711058 0.9716637135 0.9619532824 - 0.9754704237 0.9719005227 0.9622746706 - 0.9756690860 0.9721373916 0.9625961185 - 0.9758678079 0.9723742008 0.9629175067 - 0.9760664105 0.9726111293 0.9632390141 - 0.9762650728 0.9728478789 0.9635604024 - 0.9764636755 0.9730846882 0.9638817906 - 0.9766623974 0.9733216166 0.9642019868 - 0.9768610001 0.9735584259 0.9645112753 - 0.9770597219 0.9737908244 0.9648205042 - 0.9772512913 0.9740175009 0.9651297927 - 0.9774367809 0.9742441773 0.9654390812 - 0.9776223898 0.9744709134 0.9657483101 - 0.9778078794 0.9746975899 0.9660575986 - 0.9779934287 0.9749243259 0.9663668871 - 0.9781789780 0.9751510024 0.9666761160 - 0.9783645272 0.9753776789 0.9669854045 - 0.9785500765 0.9756044149 0.9672716260 - 0.9787356257 0.9758260846 0.9675518870 - 0.9789177179 0.9760301113 0.9678322077 - 0.9790937901 0.9762340784 0.9681125283 - 0.9792699218 0.9764381051 0.9683927894 - 0.9794459939 0.9766421914 0.9686729908 - 0.9796221256 0.9768462181 0.9689533114 - 0.9797981977 0.9770501852 0.9692335725 - 0.9799743295 0.9772542119 0.9695138931 - 0.9801504016 0.9774581790 0.9697943926 - 0.9803264737 0.9776623249 0.9700750709 - 0.9805015922 0.9778532982 0.9703558087 - 0.9806643128 0.9780429006 0.9706364870 - 0.9808269739 0.9782323837 0.9709172249 - 0.9809898138 0.9784219861 0.9711979032 - 0.9811524749 0.9786115289 0.9714785814 - 0.9813151956 0.9788010716 0.9717593193 - 0.9814779162 0.9789906144 0.9720399976 - 0.9816406965 0.9791802168 0.9723206758 - 0.9818034172 0.9793697000 0.9725946784 - 0.9819660783 0.9795542955 0.9728677869 - 0.9821212888 0.9797332883 0.9731410146 - 0.9822732210 0.9799122810 0.9734141231 - 0.9824250937 0.9800912738 0.9736872911 - 0.9825769067 0.9802703261 0.9739603996 - 0.9827287793 0.9804493189 0.9742336273 - 0.9828807116 0.9806283116 0.9745066762 - 0.9830325842 0.9808073044 0.9747799039 - 0.9831845164 0.9809862971 0.9750474095 - 0.9833363891 0.9811652899 0.9753007293 - 0.9834849238 0.9813308120 0.9755538702 - 0.9836212993 0.9814954996 0.9758071899 - 0.9837576747 0.9816601872 0.9760603905 - 0.9838941097 0.9818249941 0.9763137102 - 0.9840304852 0.9819896817 0.9765669107 - 0.9841669798 0.9821544290 0.9768201709 - 0.9843034148 0.9823191762 0.9770733714 - 0.9844397902 0.9824839234 0.9773266912 - 0.9845762253 0.9826486111 0.9775664210 - 0.9847126007 0.9828112721 0.9777957797 - 0.9848353863 0.9829697013 0.9780251980 - 0.9849503040 0.9831280708 0.9782546163 - 0.9850652814 0.9832863808 0.9784839749 - 0.9851801991 0.9834448099 0.9787133932 - 0.9852951169 0.9836031199 0.9789428115 - 0.9854100943 0.9837614894 0.9791722894 - 0.9855250120 0.9839199185 0.9794017076 - 0.9856399298 0.9840782285 0.9796311259 - 0.9857547879 0.9842365980 0.9798449278 - 0.9858698249 0.9843990207 0.9800536036 - 0.9859718084 0.9845643044 0.9802622199 - 0.9860730767 0.9847295880 0.9804708958 - 0.9861744046 0.9848948717 0.9806795716 - 0.9862756729 0.9850602746 0.9808881879 - 0.9863771200 0.9852256179 0.9810969234 - 0.9864783883 0.9853909016 0.9813054800 - 0.9865797162 0.9855561852 0.9815142155 - 0.9866809845 0.9857215285 0.9817228913 - 0.9867823124 0.9858868122 0.9819098115 - 0.9868842959 0.9860410094 0.9820920825 - 0.9869905710 0.9861913919 0.9822744727 - 0.9870967865 0.9863417149 0.9824568033 - 0.9872030020 0.9864919782 0.9826391935 - 0.9873092771 0.9866423011 0.9828215241 - 0.9874154925 0.9867926240 0.9830037951 - 0.9875218272 0.9869430065 0.9831861854 - 0.9876279831 0.9870933294 0.9833685160 - 0.9877343178 0.9872435927 0.9835509062 - 0.9878404737 0.9873939157 0.9837235212 - 0.9879497886 0.9875226021 0.9838936925 - 0.9880681038 0.9876458049 0.9840639234 - 0.9881862998 0.9877688885 0.9842339754 - 0.9883046150 0.9878919721 0.9844042063 - 0.9884228706 0.9880151749 0.9845743775 - 0.9885411859 0.9881383181 0.9847446084 - 0.9886595011 0.9882614017 0.9849147201 - 0.9887778163 0.9883846045 0.9850848913 - 0.9888960123 0.9885076880 0.9852551222 - 0.9890143275 0.9886307716 0.9854249954 - 0.9891341925 0.9887586832 0.9855948091 - 0.9892588854 0.9888883233 0.9857646227 - 0.9893836975 0.9890179038 0.9859343767 - 0.9895085096 0.9891474247 0.9861043096 - 0.9896333218 0.9892770052 0.9862741232 - 0.9897580743 0.9894065857 0.9864438772 - 0.9898828268 0.9895361066 0.9866136909 - 0.9900075793 0.9896656871 0.9867835045 - 0.9901323915 0.9897953272 0.9869533181 - 0.9902572036 0.9899247885 0.9871199131 - 0.9903820157 0.9900571704 0.9872816801 - 0.9905074835 0.9901915789 0.9874433875 - 0.9906330109 0.9903261065 0.9876052141 - 0.9907584190 0.9904605150 0.9877669215 - 0.9908838868 0.9905949831 0.9879286885 - 0.9910094142 0.9907293916 0.9880905151 - 0.9911348224 0.9908639193 0.9882522225 - 0.9912602901 0.9909983277 0.9884139895 - 0.9913856983 0.9911326766 0.9885756969 - 0.9915112257 0.9912672043 0.9887369871 - 0.9916366935 0.9914013147 0.9888862967 - 0.9917575717 0.9915347099 0.9890357256 - 0.9918779731 0.9916681051 0.9891850948 - 0.9919983149 0.9918015003 0.9893345237 - 0.9921187162 0.9919348955 0.9894838929 - 0.9922391176 0.9920682907 0.9896332026 - 0.9923595190 0.9922016263 0.9897825718 - 0.9924799204 0.9923350215 0.9899320006 - 0.9926003218 0.9924684167 0.9900814295 - 0.9927207232 0.9926018119 0.9902307987 - 0.9928411245 0.9927352071 0.9903745055 - 0.9929553270 0.9928619862 0.9905139208 - 0.9930647016 0.9929882884 0.9906532764 - 0.9931741953 0.9931144714 0.9907926917 - 0.9932836294 0.9932407737 0.9909321070 - 0.9933931231 0.9933670759 0.9910715222 - 0.9935024977 0.9934933186 0.9912108779 - 0.9936119914 0.9936196208 0.9913502932 - 0.9937214851 0.9937458038 0.9914897084 - 0.9938309193 0.9938721061 0.9916291237 - 0.9939404130 0.9939982891 0.9917684793 - 0.9940490127 0.9941191077 0.9919059277 - 0.9941514730 0.9942321777 0.9920433164 - 0.9942539930 0.9943454266 0.9921807051 - 0.9943563938 0.9944584966 0.9923179746 - 0.9944589138 0.9945716858 0.9924554229 - 0.9945613742 0.9946848154 0.9925928116 - 0.9946638942 0.9947978854 0.9927302003 - 0.9947664142 0.9949110746 0.9928675294 - 0.9948688149 0.9950242043 0.9930049181 - 0.9949712753 0.9951373935 0.9931423068 - 0.9950737953 0.9952505231 0.9932782054 - 0.9951713085 0.9953569770 0.9934102893 - 0.9952647090 0.9954618812 0.9935423732 - 0.9953581095 0.9955667853 0.9936743975 - 0.9954516292 0.9956716895 0.9938064814 - 0.9955450296 0.9957765937 0.9939386249 - 0.9956383705 0.9958814979 0.9940707088 - 0.9957317710 0.9959864020 0.9942027926 - 0.9958252907 0.9960911870 0.9943348765 - 0.9959186912 0.9961960912 0.9944670200 - 0.9960120916 0.9963009953 0.9945989847 - 0.9961054921 0.9964051247 0.9947274923 - 0.9961966872 0.9965000153 0.9948515892 - 0.9962875843 0.9965949059 0.9949758053 - 0.9963784814 0.9966897964 0.9951000214 - 0.9964693785 0.9967846870 0.9952241182 - 0.9965602756 0.9968795776 0.9953482747 - 0.9966511726 0.9969745278 0.9954724908 - 0.9967421293 0.9970694184 0.9955965877 - 0.9968330264 0.9971643090 0.9957208037 - 0.9969239235 0.9972591996 0.9958449006 - 0.9970148206 0.9973540902 0.9959691167 - 0.9971055984 0.9974477887 0.9960889220 - 0.9971954226 0.9975377917 0.9962046742 - 0.9972853065 0.9976279140 0.9963206053 - 0.9973750710 0.9977179170 0.9964364171 - 0.9974650145 0.9978079200 0.9965522289 - 0.9975547791 0.9978979230 0.9966681004 - 0.9976447225 0.9979879260 0.9967839122 - 0.9977344871 0.9980779290 0.9968997240 - 0.9978243709 0.9981678724 0.9970155954 - 0.9979141951 0.9982579947 0.9971314073 - 0.9980040789 0.9983479977 0.9972472191 - 0.9980940223 0.9984341860 0.9973635077 - 0.9981843829 0.9985107780 0.9974802732 - 0.9982746840 0.9985873103 0.9975970984 - 0.9983651042 0.9986637831 0.9977139235 - 0.9984555244 0.9987403750 0.9978306890 - 0.9985458851 0.9988169074 0.9979475141 - 0.9986361861 0.9988934994 0.9980642796 - 0.9987266064 0.9989699721 0.9981811047 - 0.9988170266 0.9990465045 0.9982979298 - 0.9989073873 0.9991230965 0.9984146953 - 0.9989976883 0.9991996288 0.9985315204 - 0.9990869761 0.9992740154 0.9986500740 - 0.9991700053 0.9993399978 0.9987728000 - 0.9992529750 0.9994059801 0.9988955259 - 0.9993360043 0.9994720221 0.9990181923 - 0.9994189739 0.9995380044 0.9991409779 - 0.9995020032 0.9996039867 0.9992637038 - 0.9995849729 0.9996700287 0.9993864298 - 0.9996680021 0.9997360110 0.9995090961 - 0.9997509718 0.9998019934 0.9996318221 - 0.9998340011 0.9998679757 0.9997546077 - 0.9999169707 0.9999340177 0.9998772740 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0002252978 0.0001790309 0.0001295420 + 0.0004642465 0.0003632053 0.0002623819 + 0.0007179323 0.0005541568 0.0004006781 + 0.0009859804 0.0007514893 0.0005452139 + 0.0012678721 0.0009573574 0.0006973416 + 0.0015588400 0.0011699020 0.0008563135 + 0.0018630330 0.0013911340 0.0010223610 + 0.0021819880 0.0016211460 0.0011926870 + 0.0025155889 0.0018565780 0.0013685439 + 0.0028599249 0.0021005161 0.0015487690 + 0.0032150650 0.0023508291 0.0017341210 + 0.0035848729 0.0026105260 0.0019261140 + 0.0039705001 0.0028831610 0.0021247759 + 0.0043733879 0.0031664821 0.0023306201 + 0.0047887620 0.0034602690 0.0025444049 + 0.0052145422 0.0037645160 0.0027661640 + 0.0056500668 0.0040802141 0.0029950261 + 0.0061007268 0.0044055800 0.0032312879 + 0.0065691760 0.0047432170 0.0034755419 + 0.0070610601 0.0050931652 0.0037271639 + 0.0075698970 0.0054551372 0.0039859852 + 0.0080941729 0.0058285771 0.0042510540 + 0.0086274194 0.0062132268 0.0045252559 + 0.0091798287 0.0066124098 0.0048091090 + 0.0097458120 0.0070239422 0.0051020859 + 0.0103271501 0.0074481708 0.0054048309 + 0.0109248599 0.0078837927 0.0057164561 + 0.0115405796 0.0083313556 0.0060368772 + 0.0121761197 0.0087913852 0.0063676350 + 0.0128270397 0.0092629055 0.0067081312 + 0.0134987198 0.0097476542 0.0070575029 + 0.0141942296 0.0102479504 0.0074174819 + 0.0149099398 0.0107645998 0.0077879899 + 0.0156464409 0.0112993503 0.0081683872 + 0.0164025500 0.0118529601 0.0085596293 + 0.0171775799 0.0124243395 0.0089649754 + 0.0179736298 0.0130097996 0.0093868207 + 0.0187904108 0.0136122899 0.0098197656 + 0.0196329206 0.0142326104 0.0102641098 + 0.0205026697 0.0148694804 0.0107198805 + 0.0213967599 0.0155238304 0.0111865401 + 0.0223194696 0.0161926802 0.0116670104 + 0.0232650694 0.0168744400 0.0121609699 + 0.0242343191 0.0175730493 0.0126681803 + 0.0252331793 0.0182904303 0.0131873302 + 0.0262477491 0.0190270096 0.0137195401 + 0.0272829495 0.0197816994 0.0142644998 + 0.0283390097 0.0205556992 0.0148215797 + 0.0294161700 0.0213519502 0.0153918499 + 0.0305210594 0.0221705697 0.0159767196 + 0.0316550806 0.0230139699 0.0165775400 + 0.0328192115 0.0238830894 0.0171922501 + 0.0340085700 0.0247773398 0.0178213306 + 0.0352222398 0.0256955195 0.0184620898 + 0.0364655294 0.0266327392 0.0191150103 + 0.0377436206 0.0275894795 0.0197834391 + 0.0390567891 0.0285663195 0.0204676799 + 0.0404002182 0.0295625906 0.0211681798 + 0.0417738296 0.0305766594 0.0218840092 + 0.0431732200 0.0316094011 0.0226133801 + 0.0445903614 0.0326650515 0.0233574491 + 0.0460246801 0.0337462984 0.0241182297 + 0.0474831313 0.0348512717 0.0248960592 + 0.0489709415 0.0359775797 0.0256870594 + 0.0504862815 0.0371238813 0.0264920406 + 0.0520257801 0.0382876694 0.0273139309 + 0.0535882413 0.0394724905 0.0281523801 + 0.0551759303 0.0406848602 0.0290120896 + 0.0567948110 0.0419235602 0.0298898704 + 0.0584423989 0.0431861505 0.0307816397 + 0.0601175204 0.0444719717 0.0316898413 + 0.0618218705 0.0457797199 0.0326160900 + 0.0635616332 0.0471059717 0.0335598104 + 0.0653382763 0.0484526493 0.0345212892 + 0.0671429336 0.0498179607 0.0355015099 + 0.0689680427 0.0512084812 0.0364993811 + 0.0708093792 0.0526283309 0.0375161916 + 0.0726710036 0.0540754311 0.0385530815 + 0.0745575577 0.0555453300 0.0396075509 + 0.0764711723 0.0570352189 0.0406817608 + 0.0784109309 0.0585439503 0.0417732410 + 0.0803771317 0.0600703284 0.0428790301 + 0.0823737681 0.0616120808 0.0439984985 + 0.0844059363 0.0631667823 0.0451391600 + 0.0864771232 0.0647454932 0.0462969095 + 0.0885818601 0.0663540736 0.0474717803 + 0.0907145590 0.0679863468 0.0486642793 + 0.0928772986 0.0696446896 0.0498744808 + 0.0950741023 0.0713270679 0.0511019900 + 0.0973075032 0.0730293319 0.0523469895 + 0.0995769203 0.0747480765 0.0536088012 + 0.1018780991 0.0764901415 0.0548868291 + 0.1042044014 0.0782487020 0.0561818182 + 0.1065538973 0.0800288767 0.0574982315 + 0.1089283004 0.0818311423 0.0588314198 + 0.1113215014 0.0836538672 0.0601810999 + 0.1137316003 0.0854980275 0.0615520887 + 0.1161573976 0.0873684287 0.0629485399 + 0.1185979992 0.0892611369 0.0643650293 + 0.1210614964 0.0911754370 0.0658011809 + 0.1235487014 0.0931112692 0.0672666430 + 0.1260568053 0.0950720683 0.0687462613 + 0.1285867989 0.0970498696 0.0702391118 + 0.1311426014 0.0990442634 0.0717507899 + 0.1337202042 0.1010560021 0.0732835308 + 0.1363096982 0.1031069979 0.0748315826 + 0.1389120072 0.1051762030 0.0763994083 + 0.1415282041 0.1072625965 0.0779832006 + 0.1441825926 0.1093714982 0.0795782432 + 0.1468576044 0.1115005016 0.0811884925 + 0.1495531946 0.1136446968 0.0828164518 + 0.1522718966 0.1158049032 0.0844542831 + 0.1550171971 0.1179929972 0.0861058980 + 0.1577734947 0.1201986000 0.0877750516 + 0.1605415046 0.1224215031 0.0894542485 + 0.1633353978 0.1246768013 0.0911507979 + 0.1661441028 0.1269477010 0.0928589627 + 0.1689610034 0.1292310059 0.0945746303 + 0.1717838943 0.1315293014 0.0963099599 + 0.1746120006 0.1338451058 0.0980574191 + 0.1774456948 0.1361752003 0.0998183563 + 0.1802875996 0.1385367960 0.1016084999 + 0.1831531972 0.1409143060 0.1034116000 + 0.1860264987 0.1433060020 0.1052436978 + 0.1889072955 0.1457217932 0.1070917994 + 0.1918103993 0.1481487006 0.1089527979 + 0.1947200000 0.1505914032 0.1108305007 + 0.1976352930 0.1530454010 0.1127144024 + 0.2005549073 0.1555085927 0.1146150976 + 0.2034773976 0.1579966992 0.1165283024 + 0.2064062953 0.1605020016 0.1184550002 + 0.2093463987 0.1630198956 0.1204015985 + 0.2122935951 0.1655519009 0.1223547012 + 0.2152556032 0.1680873036 0.1243321002 + 0.2182167023 0.1706292033 0.1263145953 + 0.2211762965 0.1731802970 0.1283138990 + 0.2241538018 0.1757373065 0.1303219944 + 0.2271365970 0.1783048064 0.1323408037 + 0.2301204056 0.1808757931 0.1343753040 + 0.2331143022 0.1834571958 0.1364185959 + 0.2361201942 0.1860488951 0.1384806037 + 0.2391359061 0.1886530071 0.1405466050 + 0.2421551943 0.1912735999 0.1426350027 + 0.2451844066 0.1939011961 0.1447259039 + 0.2482129037 0.1965422034 0.1468326002 + 0.2512401044 0.1991859972 0.1489423960 + 0.2542673051 0.2018409967 0.1510629058 + 0.2572943866 0.2045024037 0.1531887054 + 0.2603251040 0.2071764022 0.1553260982 + 0.2633563876 0.2098606974 0.1574766040 + 0.2663846016 0.2125551999 0.1596474946 + 0.2694126070 0.2152567953 0.1618286073 + 0.2724404037 0.2179622054 0.1640225053 + 0.2754598856 0.2206754982 0.1662251055 + 0.2784681916 0.2233911008 0.1684374958 + 0.2814692855 0.2261153013 0.1706584990 + 0.2844651043 0.2288406044 0.1728888005 + 0.2874703109 0.2315697968 0.1751244962 + 0.2904790938 0.2343001068 0.1773661971 + 0.2934839129 0.2370491028 0.1796108931 + 0.2964878976 0.2397976965 0.1818595976 + 0.2994953096 0.2425439954 0.1841097027 + 0.3025026917 0.2452909946 0.1863622963 + 0.3054913878 0.2480435967 0.1886177063 + 0.3084802032 0.2507981956 0.1908794940 + 0.3114694059 0.2535662055 0.1931429952 + 0.3144572079 0.2563355863 0.1954129934 + 0.3174341023 0.2591148913 0.1976844966 + 0.3204114139 0.2618939877 0.1999710053 + 0.3233911097 0.2646703124 0.2022576034 + 0.3263702095 0.2674466074 0.2045578957 + 0.3293448985 0.2702299058 0.2068590969 + 0.3323191106 0.2730130851 0.2091719061 + 0.3352886140 0.2757953107 0.2114882022 + 0.3382574916 0.2785772979 0.2138084024 + 0.3412111998 0.2813608050 0.2161314934 + 0.3441649079 0.2841446996 0.2184565067 + 0.3471063972 0.2869217098 0.2207846045 + 0.3500475883 0.2896957994 0.2231137007 + 0.3529730141 0.2924658060 0.2254499942 + 0.3558960855 0.2952325046 0.2277863026 + 0.3588060141 0.2979984879 0.2301243991 + 0.3617117107 0.3007633984 0.2324627936 + 0.3645960987 0.3035242856 0.2348098010 + 0.3674674034 0.3062672913 0.2371633947 + 0.3703210056 0.3090102971 0.2395182997 + 0.3731533885 0.3117265999 0.2418767959 + 0.3759778142 0.3144418001 0.2442353964 + 0.3787800074 0.3171553910 0.2465995997 + 0.3815816045 0.3198683858 0.2489643991 + 0.3843731880 0.3225778937 0.2513361871 + 0.3871648014 0.3252829909 0.2537139058 + 0.3899506927 0.3279863000 0.2560917139 + 0.3927353919 0.3306789994 0.2584705949 + 0.3955127001 0.3333716094 0.2608495951 + 0.3982842863 0.3360339999 0.2632276118 + 0.4010469019 0.3386904001 0.2656053007 + 0.4037859142 0.3413349092 0.2679820061 + 0.4065249860 0.3439669013 0.2703562975 + 0.4092408121 0.3465988040 0.2727305889 + 0.4119558036 0.3492295146 0.2751063108 + 0.4146685004 0.3518601954 0.2774823904 + 0.4173798859 0.3544926047 0.2798556983 + 0.4200862050 0.3571256995 0.2822236121 + 0.4227792919 0.3597556055 0.2845914066 + 0.4254722893 0.3623783886 0.2869648039 + 0.4281426072 0.3650011122 0.2893395126 + 0.4308102131 0.3676072955 0.2917163968 + 0.4334703088 0.3702105880 0.2940998971 + 0.4361222088 0.3728044033 0.2964833975 + 0.4387736022 0.3753828108 0.2988683879 + 0.4414154887 0.3779611886 0.3012540936 + 0.4440574050 0.3805244863 0.3036401868 + 0.4466896951 0.3830857873 0.3060306907 + 0.4493151903 0.3856397867 0.3084211946 + 0.4519402087 0.3881801963 0.3108114004 + 0.4545603991 0.3907206953 0.3132013977 + 0.4571805894 0.3932437003 0.3155913949 + 0.4597933888 0.3957622051 0.3179830909 + 0.4624007940 0.3982788026 0.3203752935 + 0.4650070071 0.4007883966 0.3227669895 + 0.4675796926 0.4032979906 0.3251537979 + 0.4701522887 0.4057931900 0.3275406957 + 0.4727115929 0.4082787931 0.3299230933 + 0.4752551019 0.4107643068 0.3322981000 + 0.4777986109 0.4132384062 0.3346731067 + 0.4803014100 0.4157119989 0.3370327055 + 0.4827977121 0.4181810915 0.3393830955 + 0.4852870107 0.4206390083 0.3417334855 + 0.4877533019 0.4230970144 0.3440715075 + 0.4902195036 0.4255495965 0.3464072049 + 0.4926728904 0.4279986918 0.3487423956 + 0.4951168895 0.4304477870 0.3510628939 + 0.4975608885 0.4328849018 0.3533833921 + 0.4999994040 0.4353202879 0.3556984961 + 0.5024372935 0.4377534091 0.3579910994 + 0.5048713088 0.4401684999 0.3602836132 + 0.5072880983 0.4425837100 0.3625684083 + 0.5097048879 0.4449890852 0.3648371100 + 0.5121037960 0.4473744929 0.3671058118 + 0.5144798160 0.4497599006 0.3693706095 + 0.5168557167 0.4521426857 0.3716300130 + 0.5192152858 0.4545232058 0.3738893867 + 0.5215666294 0.4569036067 0.3761452138 + 0.5239177942 0.4592863917 0.3783971071 + 0.5262326002 0.4616703987 0.3806490004 + 0.5285415053 0.4640542865 0.3828856945 + 0.5308492184 0.4664343894 0.3851085007 + 0.5331150889 0.4688135087 0.3873313069 + 0.5353810191 0.4711925089 0.3895424902 + 0.5376406908 0.4735459089 0.3917432129 + 0.5398690104 0.4758956134 0.3939439058 + 0.5420972705 0.4782454073 0.3961327970 + 0.5443140864 0.4805634022 0.3983095884 + 0.5464997292 0.4828793108 0.4004862905 + 0.5486851931 0.4851951003 0.4026562870 + 0.5508528948 0.4874885082 0.4048171043 + 0.5529860854 0.4897811115 0.4069780111 + 0.5551192164 0.4920735955 0.4091326892 + 0.5572410822 0.4943391979 0.4112750888 + 0.5593448877 0.4966034889 0.4134174883 + 0.5614485741 0.4988678992 0.4155569077 + 0.5635406971 0.5010880828 0.4176844060 + 0.5656147003 0.5033046007 0.4198119938 + 0.5676888227 0.5055211186 0.4219393134 + 0.5697504878 0.5077061057 0.4240606129 + 0.5717914104 0.5098856091 0.4261820018 + 0.5738322139 0.5120651722 0.4283033907 + 0.5758668780 0.5142083168 0.4304130077 + 0.5778881907 0.5163393021 0.4325203001 + 0.5799093843 0.5184702277 0.4346275926 + 0.5819228292 0.5205832124 0.4367291033 + 0.5839092731 0.5226848125 0.4388267994 + 0.5858957171 0.5247864127 0.4409244955 + 0.5878791809 0.5268790722 0.4430136979 + 0.5898365974 0.5289608240 0.4450865090 + 0.5917940140 0.5310423970 0.4471594095 + 0.5937513709 0.5331161022 0.4492312074 + 0.5956690907 0.5351639986 0.4512765110 + 0.5975846052 0.5372117758 0.4533218145 + 0.5995001197 0.5392596722 0.4553669989 + 0.6013880968 0.5412784815 0.4573917985 + 0.6032671928 0.5432971716 0.4594076872 + 0.6051464081 0.5453159213 0.4614236057 + 0.6070122123 0.5473181009 0.4634315073 + 0.6088663936 0.5493139029 0.4654226899 + 0.6107205153 0.5513098240 0.4674139023 + 0.6125701070 0.5532947779 0.4694051147 + 0.6144073009 0.5552641749 0.4713697135 + 0.6162444949 0.5572335720 0.4733316898 + 0.6180818081 0.5592026711 0.4752937853 + 0.6199104190 0.5611671209 0.4772514999 + 0.6217387915 0.5631313920 0.4792042077 + 0.6235671043 0.5650957823 0.4811568856 + 0.6253790855 0.5670524836 0.4831095934 + 0.6271820068 0.5690057278 0.4850563109 + 0.6289849281 0.5709587932 0.4870027900 + 0.6307768226 0.5729085207 0.4889493883 + 0.6325399876 0.5748482943 0.4908913076 + 0.6343032122 0.5767881870 0.4928275049 + 0.6360663772 0.5787280798 0.4947637022 + 0.6378102899 0.5806527734 0.4966998994 + 0.6395515203 0.5825737715 0.4986213148 + 0.6412925720 0.5844948292 0.5005409122 + 0.6430225968 0.5864080787 0.5024604797 + 0.6447380185 0.5883042812 0.5043705106 + 0.6464533806 0.5902004242 0.5062571168 + 0.6481688023 0.5920965075 0.5081436038 + 0.6498512030 0.5939829946 0.5100302100 + 0.6515321136 0.5958667994 0.5119035840 + 0.6532130241 0.5977506042 0.5137699842 + 0.6548699141 0.5996311903 0.5156363249 + 0.6564965248 0.6014996767 0.5175027251 + 0.6581230760 0.6033681035 0.5193520188 + 0.6597496867 0.6052364707 0.5212007165 + 0.6613441706 0.6070837975 0.5230494738 + 0.6629346013 0.6089169979 0.5248932838 + 0.6645249724 0.6107501984 0.5267223120 + 0.6661086082 0.6125832796 0.5285513997 + 0.6676759124 0.6143841743 0.5303804278 + 0.6692432761 0.6161835194 0.5322028995 + 0.6708106995 0.6179828048 0.5340183973 + 0.6723604202 0.6197698712 0.5358340144 + 0.6739010811 0.6215232015 0.5376495123 + 0.6754419208 0.6232764721 0.5394585729 + 0.6769825816 0.6250298023 0.5412647724 + 0.6784970164 0.6267579794 0.5430709720 + 0.6800110936 0.6284642816 0.5448771119 + 0.6815252900 0.6301707029 0.5466737747 + 0.6830374002 0.6318771243 0.5484687090 + 0.6845443249 0.6335585117 0.5502635837 + 0.6860511899 0.6352325082 0.5520585775 + 0.6875581145 0.6369063854 0.5538393855 + 0.6890510917 0.6385803223 0.5556197762 + 0.6905319095 0.6402255893 0.5574001074 + 0.6920126081 0.6418696046 0.5591800213 + 0.6934934258 0.6435136199 0.5609490871 + 0.6949405074 0.6451522708 0.5627182126 + 0.6963760257 0.6467463970 0.5644872189 + 0.6978114843 0.6483405232 0.5662546158 + 0.6992468834 0.6499347091 0.5680006146 + 0.7006595135 0.6515222788 0.5697466135 + 0.7020696998 0.6530866027 0.5714926720 + 0.7034800053 0.6546509266 0.5732375979 + 0.7048898935 0.6562151909 0.5749657750 + 0.7062891126 0.6577690244 0.5766940713 + 0.7076883912 0.6592959762 0.5784224272 + 0.7090877295 0.6608229876 0.5801504254 + 0.7104840875 0.6623499990 0.5818541050 + 0.7118595243 0.6638725996 0.5835577846 + 0.7132350206 0.6653854251 0.5852615237 + 0.7146103978 0.6668980718 0.5869652033 + 0.7159860134 0.6684108973 0.5886417031 + 0.7173621058 0.6699141264 0.5903154016 + 0.7187381983 0.6713935733 0.5919892192 + 0.7201142907 0.6728730798 0.5936629772 + 0.7214856148 0.6743525863 0.5953096747 + 0.7228314281 0.6758230925 0.5969482064 + 0.7241771221 0.6772614717 0.5985866785 + 0.7255228758 0.6786999702 0.6002252102 + 0.7268646955 0.6801384091 0.6018486023 + 0.7281752825 0.6815736294 0.6034609079 + 0.7294859290 0.6829831004 0.6050732136 + 0.7307965159 0.6843926907 0.6066855192 + 0.7321065068 0.6858022213 0.6082887053 + 0.7333903909 0.6872118115 0.6098747253 + 0.7346742749 0.6885958910 0.6114608049 + 0.7359582782 0.6899790168 0.6130468249 + 0.7372422218 0.6913622022 0.6146309972 + 0.7385115027 0.6927453279 0.6161890030 + 0.7397789955 0.6941304803 0.6177470088 + 0.7410464883 0.6955162883 0.6193048954 + 0.7423139811 0.6969019771 0.6208629012 + 0.7435634732 0.6982877851 0.6223968267 + 0.7448058128 0.6996648908 0.6239222288 + 0.7460480928 0.7010340095 0.6254476905 + 0.7472903728 0.7024030089 0.6269730926 + 0.7485197783 0.7037721276 0.6284868121 + 0.7497360110 0.7051346898 0.6299803257 + 0.7509521842 0.7064757943 0.6314738989 + 0.7521684170 0.7078167796 0.6329674721 + 0.7533807158 0.7091578841 0.6344609857 + 0.7545793056 0.7104989886 0.6359264851 + 0.7557778955 0.7118229270 0.6373904943 + 0.7569764256 0.7131447792 0.6388545036 + 0.7581750154 0.7144666910 0.6403185129 + 0.7593536973 0.7157886028 0.6417623162 + 0.7605302930 0.7170932889 0.6431841850 + 0.7617068887 0.7183820009 0.6446061730 + 0.7628834844 0.7196707129 0.6460281014 + 0.7640433908 0.7209593058 0.6474500895 + 0.7651894093 0.7222459912 0.6488506198 + 0.7663354278 0.7235112786 0.6502503753 + 0.7674813867 0.7247765064 0.6516500711 + 0.7686260939 0.7260417938 0.6530498266 + 0.7697625160 0.7273070812 0.6544442773 + 0.7708989978 0.7285519838 0.6558309197 + 0.7720354795 0.7297835946 0.6572173834 + 0.7731719017 0.7310153246 0.6586040258 + 0.7742877007 0.7322468758 0.6599906087 + 0.7753937244 0.7334775925 0.6613616943 + 0.7764996886 0.7346972227 0.6627286077 + 0.7776057124 0.7359166741 0.6640955210 + 0.7787057161 0.7371363044 0.6654623747 + 0.7797777057 0.7383558154 0.6668282747 + 0.7808495760 0.7395550013 0.6681864858 + 0.7819216251 0.7407342792 0.6695446968 + 0.7829934955 0.7419134974 0.6709029078 + 0.7840589285 0.7430927753 0.6722611785 + 0.7851203084 0.7442719936 0.6736127734 + 0.7861816883 0.7454271913 0.6749550104 + 0.7872430086 0.7465791702 0.6762971282 + 0.7883045077 0.7477310896 0.6776393056 + 0.7893705964 0.7488831282 0.6789814234 + 0.7904366851 0.7500287294 0.6803113818 + 0.7915027738 0.7511497736 0.6816344857 + 0.7925689220 0.7522708774 0.6829575896 + 0.7936292291 0.7533919811 0.6842806935 + 0.7946795225 0.7545130849 0.6856039166 + 0.7957298160 0.7556204796 0.6869193912 + 0.7967801094 0.7567132115 0.6882331967 + 0.7978304029 0.7578058243 0.6895471215 + 0.7988722920 0.7588983774 0.6908609867 + 0.7999097109 0.7599909902 0.6921749115 + 0.8009471893 0.7610651851 0.6934753060 + 0.8019847274 0.7621316910 0.6947749853 + 0.8030220866 0.7631980777 0.6960747838 + 0.8040328026 0.7642645240 0.6973745227 + 0.8050405979 0.7653309107 0.6986737847 + 0.8060483932 0.7663741708 0.6999592781 + 0.8070561886 0.7674140930 0.7012447715 + 0.8080614805 0.7684540153 0.7025303245 + 0.8090434074 0.7694938779 0.7038156986 + 0.8100252748 0.7705338001 0.7050995827 + 0.8110070825 0.7715379000 0.7063539028 + 0.8119890094 0.7725417018 0.7076082230 + 0.8129674792 0.7735455036 0.7088624835 + 0.8139352798 0.7745493054 0.7101168036 + 0.8149030805 0.7755506039 0.7113707066 + 0.8158710003 0.7765091062 0.7126060128 + 0.8168388009 0.7774677277 0.7138411999 + 0.8177986741 0.7784262896 0.7150765061 + 0.8187428117 0.7793847919 0.7163118124 + 0.8196867704 0.7803425789 0.7175469995 + 0.8206307888 0.7812892795 0.7187737226 + 0.8215748072 0.7822360992 0.7199997902 + 0.8225091100 0.7831827998 0.7212259173 + 0.8234264851 0.7841296196 0.7224519849 + 0.8243438005 0.7850759029 0.7236781120 + 0.8252611756 0.7860084176 0.7248950005 + 0.8261786103 0.7869408727 0.7261095047 + 0.8270888925 0.7878733873 0.7273240089 + 0.8279858232 0.7888059020 0.7285385728 + 0.8288828135 0.7897384167 0.7297530770 + 0.8297798038 0.7906383872 0.7309511900 + 0.8306767941 0.7915360928 0.7321383953 + 0.8315675855 0.7924336791 0.7333257198 + 0.8324421048 0.7933313847 0.7345129251 + 0.8333166242 0.7942289710 0.7357000709 + 0.8341910839 0.7951080203 0.7368817925 + 0.8350657225 0.7959815860 0.7380530834 + 0.8359373212 0.7968552709 0.7392244935 + 0.8367924094 0.7977288961 0.7403959036 + 0.8376473784 0.7986025810 0.7415671945 + 0.8385024071 0.7994632721 0.7427378297 + 0.8393573761 0.8003144264 0.7438889742 + 0.8402125239 0.8011655211 0.7450402975 + 0.8410465717 0.8020166159 0.7461915016 + 0.8418799043 0.8028677106 0.7473427057 + 0.8427131772 0.8037161231 0.7484940290 + 0.8435465097 0.8045580983 0.7496194839 + 0.8443799019 0.8054001927 0.7507324815 + 0.8452067971 0.8062422276 0.7518454194 + 0.8460311890 0.8070843220 0.7529584169 + 0.8468557000 0.8079264164 0.7540712953 + 0.8476800919 0.8087493777 0.7551763058 + 0.8485044837 0.8095721006 0.7562569976 + 0.8493136764 0.8103948832 0.7573378086 + 0.8501023054 0.8112176061 0.7584185004 + 0.8508908749 0.8120403886 0.7594993114 + 0.8516795039 0.8128635883 0.7605800033 + 0.8524680734 0.8136872053 0.7616460919 + 0.8532543182 0.8145108223 0.7627075911 + 0.8540086150 0.8153343797 0.7637690902 + 0.8547629714 0.8161579967 0.7648305893 + 0.8555173278 0.8169798255 0.7658920884 + 0.8562716842 0.8177930117 0.7669504285 + 0.8570259809 0.8186061978 0.7679973245 + 0.8577690721 0.8194193840 0.7690442204 + 0.8585063815 0.8202325106 0.7700911164 + 0.8592435718 0.8210456967 0.7711378932 + 0.8599808812 0.8218513727 0.7721847892 + 0.8607181907 0.8226534724 0.7732229233 + 0.8614515066 0.8234555125 0.7742553949 + 0.8621680737 0.8242576122 0.7752879858 + 0.8628845811 0.8250595927 0.7763205171 + 0.8636012077 0.8258600831 0.7773531079 + 0.8643177748 0.8266490102 0.7783856988 + 0.8650344014 0.8274379969 0.7793797255 + 0.8657494187 0.8282269239 0.7803717256 + 0.8664636016 0.8290157914 0.7813637853 + 0.8671777844 0.8298047185 0.7823559046 + 0.8678920269 0.8305819035 0.7833480239 + 0.8686062098 0.8313472867 0.7843325734 + 0.8693193793 0.8321127295 0.7852917910 + 0.8700184822 0.8328779936 0.7862508893 + 0.8707175851 0.8336433768 0.7872101068 + 0.8714166880 0.8344088197 0.7881693244 + 0.8721157908 0.8351647258 0.7891284227 + 0.8728148937 0.8359184861 0.7900806069 + 0.8734970093 0.8366723061 0.7910240293 + 0.8741555810 0.8374261856 0.7919672728 + 0.8748142123 0.8381800056 0.7929106951 + 0.8754727840 0.8389329910 0.7938539982 + 0.8761314750 0.8396766782 0.7947974205 + 0.8767901063 0.8404204249 0.7957333922 + 0.8774240017 0.8411641121 0.7966641784 + 0.8780478835 0.8419076800 0.7975950241 + 0.8786717057 0.8426514268 0.7985258102 + 0.8792955279 0.8433899879 0.7994565964 + 0.8799194098 0.8441156745 0.8003873825 + 0.8805431724 0.8448413014 0.8012884855 + 0.8811553121 0.8455669880 0.8021752238 + 0.8817664981 0.8462926149 0.8030619025 + 0.8823778033 0.8470183015 0.8039487004 + 0.8829889894 0.8477351069 0.8048353791 + 0.8836002946 0.8484396935 0.8057221174 + 0.8842098713 0.8491442800 0.8065841794 + 0.8848046064 0.8498489261 0.8074355125 + 0.8853992820 0.8505535126 0.8082867861 + 0.8859940171 0.8512580991 0.8091381192 + 0.8865886927 0.8519487977 0.8099893928 + 0.8871833086 0.8526254892 0.8108407259 + 0.8877705932 0.8533021212 0.8116824031 + 0.8883280158 0.8539788127 0.8125190139 + 0.8888853788 0.8546553850 0.8133556247 + 0.8894428015 0.8553320169 0.8141922951 + 0.8900002241 0.8560088277 0.8150289059 + 0.8905575871 0.8566855788 0.8158655167 + 0.8911083937 0.8573625088 0.8166936040 + 0.8916376829 0.8580393195 0.8175144792 + 0.8921670914 0.8587161899 0.8183354735 + 0.8926963806 0.8593930006 0.8191564083 + 0.8932257295 0.8600615859 0.8199774027 + 0.8937550783 0.8607198000 0.8207982779 + 0.8942791820 0.8613780141 0.8216099739 + 0.8947834969 0.8620361090 0.8224058747 + 0.8952878118 0.8626943231 0.8232018948 + 0.8957921863 0.8633524776 0.8239977956 + 0.8962965012 0.8640044928 0.8247938156 + 0.8968008161 0.8646433949 0.8255897760 + 0.8973022103 0.8652822971 0.8263819218 + 0.8977811933 0.8659210801 0.8271486163 + 0.8982601166 0.8665599823 0.8279151917 + 0.8987389803 0.8671988845 0.8286818862 + 0.8992179036 0.8678355217 0.8294485211 + 0.8996968865 0.8684571981 0.8302152157 + 0.9001758099 0.8690788746 0.8309819102 + 0.9006456137 0.8697006106 0.8317354918 + 0.9011150002 0.8703222871 0.8324862123 + 0.9015843272 0.8709440827 0.8332368731 + 0.9020537138 0.8715658188 0.8339877129 + 0.9025231004 0.8721610904 0.8347383738 + 0.9029924273 0.8727529049 0.8354892135 + 0.9034619927 0.8733446002 0.8362295032 + 0.9039316773 0.8739364147 0.8369569182 + 0.9044014812 0.8745281100 0.8376842737 + 0.9048712254 0.8751199245 0.8384116888 + 0.9053409100 0.8756937981 0.8391389847 + 0.9058105946 0.8762534857 0.8398663998 + 0.9062727094 0.8768131137 0.8405938148 + 0.9067248106 0.8773728013 0.8413041234 + 0.9071769118 0.8779324889 0.8420143127 + 0.9076290131 0.8784921169 0.8427246213 + 0.9080811143 0.8790519238 0.8434348702 + 0.9085332751 0.8796120286 0.8441451192 + 0.9089835286 0.8801720738 0.8448554277 + 0.9094098210 0.8807322979 0.8455566764 + 0.9098359942 0.8812924027 0.8462482095 + 0.9102622867 0.8818526268 0.8469398022 + 0.9106885791 0.8824126720 0.8476313949 + 0.9111148119 0.8829627037 0.8483229876 + 0.9115411043 0.8835083246 0.8490145206 + 0.9119545221 0.8840540051 0.8497061133 + 0.9123607874 0.8845996857 0.8503819108 + 0.9127669930 0.8851453066 0.8510559201 + 0.9131733179 0.8856909871 0.8517298102 + 0.9135795236 0.8862351179 0.8524037004 + 0.9139857888 0.8867712021 0.8530777097 + 0.9143912196 0.8873074055 0.8537515998 + 0.9147915244 0.8878436089 0.8544242978 + 0.9151918292 0.8883798122 0.8550928831 + 0.9155920744 0.8889160156 0.8557615280 + 0.9159923792 0.8894522190 0.8564301133 + 0.9163928032 0.8899856806 0.8570985794 + 0.9167931080 0.8905174136 0.8577672243 + 0.9171940088 0.8910490274 0.8584358096 + 0.9175955057 0.8915805817 0.8590922952 + 0.9179968834 0.8921123147 0.8597366810 + 0.9183983207 0.8926439285 0.8603811860 + 0.9187998176 0.8931754827 0.8610256910 + 0.9192011952 0.8936873078 0.8616700768 + 0.9196026921 0.8941966891 0.8623145819 + 0.9199982882 0.8947061896 0.8629590869 + 0.9203935862 0.8952155709 0.8635929823 + 0.9207888246 0.8957250714 0.8642224073 + 0.9211840034 0.8962345123 0.8648517728 + 0.9215791821 0.8967435956 0.8654811978 + 0.9219744205 0.8972501755 0.8661106229 + 0.9223681092 0.8977568150 0.8667399883 + 0.9227561951 0.8982635140 0.8673694134 + 0.9231444001 0.8987700939 0.8679875731 + 0.9235324860 0.8992767930 0.8686034083 + 0.9239205718 0.8997833729 0.8692191839 + 0.9243087769 0.9002879262 0.8698350191 + 0.9246969223 0.9007881284 0.8704507947 + 0.9250844717 0.9012882710 0.8710665703 + 0.9254714251 0.9017884135 0.8716825247 + 0.9258583188 0.9022886157 0.8722918034 + 0.9262452126 0.9027888179 0.8729001880 + 0.9266321063 0.9032890201 0.8735085726 + 0.9270188808 0.9037815928 0.8741170168 + 0.9274057746 0.9042645097 0.8747254014 + 0.9277873039 0.9047474265 0.8753337860 + 0.9281644821 0.9052304029 0.8759421706 + 0.9285417199 0.9057133198 0.8765338063 + 0.9289188981 0.9061961770 0.8771222234 + 0.9292961955 0.9066790938 0.8777105212 + 0.9296733737 0.9071536064 0.8782988787 + 0.9300506115 0.9076185226 0.8788872957 + 0.9304202199 0.9080834985 0.8794757128 + 0.9307851791 0.9085484147 0.8800640106 + 0.9311501980 0.9090133905 0.8806303740 + 0.9315152764 0.9094783068 0.8811889887 + 0.9318802953 0.9099432826 0.8817476034 + 0.9322453141 0.9103996754 0.8823062181 + 0.9326102734 0.9108446240 0.8828647733 + 0.9329578280 0.9112895131 0.8834233880 + 0.9332942963 0.9117342830 0.8839820027 + 0.9336308241 0.9121791720 0.8845335245 + 0.9339672923 0.9126241207 0.8850793839 + 0.9343038201 0.9130688906 0.8856253028 + 0.9346402884 0.9135128260 0.8861711025 + 0.9349768162 0.9139543176 0.8867170215 + 0.9352998137 0.9143959284 0.8872628808 + 0.9356107712 0.9148374200 0.8878087997 + 0.9359217882 0.9152789116 0.8883525729 + 0.9362328053 0.9157204032 0.8888916969 + 0.9365438819 0.9161620140 0.8894308209 + 0.9368548989 0.9166017175 0.8899700046 + 0.9371659160 0.9170259833 0.8905091286 + 0.9374731183 0.9174504280 0.8910481930 + 0.9377737045 0.9178748131 0.8915873170 + 0.9380741715 0.9182991982 0.8921263814 + 0.9383748174 0.9187235832 0.8926637769 + 0.9386752844 0.9191479087 0.8932011127 + 0.9389758706 0.9195722938 0.8937383294 + 0.9392765164 0.9199801087 0.8942756057 + 0.9395747185 0.9203845263 0.8948128819 + 0.9398567080 0.9207890034 0.8953502178 + 0.9401388168 0.9211934209 0.8958874941 + 0.9404208064 0.9215978980 0.8964194059 + 0.9407027960 0.9220023751 0.8969466090 + 0.9409849048 0.9224067926 0.8974738717 + 0.9412668943 0.9228081107 0.8980011940 + 0.9415488839 0.9232060909 0.8985285163 + 0.9418272972 0.9236040115 0.8990557194 + 0.9421048760 0.9240018725 0.8995829821 + 0.9423825145 0.9243999124 0.9001092911 + 0.9426599741 0.9247977734 0.9006150961 + 0.9429376125 0.9251956940 0.9011210203 + 0.9432151914 0.9255933166 0.9016268253 + 0.9434927106 0.9259833097 0.9021326900 + 0.9437695742 0.9263733029 0.9026384950 + 0.9440454841 0.9267632961 0.9031443000 + 0.9443213940 0.9271534085 0.9036502242 + 0.9445973039 0.9275434017 0.9041491747 + 0.9448732138 0.9279333949 0.9046404958 + 0.9451491237 0.9283233881 0.9051318169 + 0.9454249740 0.9286991954 0.9056230783 + 0.9457010031 0.9290624261 0.9061143994 + 0.9459671974 0.9294254780 0.9066057205 + 0.9462327957 0.9297887087 0.9070969224 + 0.9464982748 0.9301518798 0.9075881839 + 0.9467638135 0.9305151105 0.9080798030 + 0.9470294118 0.9308782816 0.9085714817 + 0.9472948909 0.9312413931 0.9090631008 + 0.9475604296 0.9315894246 0.9095547199 + 0.9478178024 0.9319362044 0.9100463986 + 0.9480611086 0.9322831035 0.9105380177 + 0.9483044744 0.9326300025 0.9110296965 + 0.9485477805 0.9329767823 0.9115203023 + 0.9487910867 0.9333236814 0.9119966030 + 0.9490345120 0.9336705804 0.9124727845 + 0.9492778182 0.9340136051 0.9129490852 + 0.9495211244 0.9343457222 0.9134253860 + 0.9497414231 0.9346777797 0.9139016271 + 0.9499527812 0.9350097775 0.9143779278 + 0.9501643181 0.9353418946 0.9148542285 + 0.9503756762 0.9356740117 0.9153259993 + 0.9505872130 0.9360061288 0.9157835841 + 0.9507985711 0.9363381267 0.9162412286 + 0.9510101080 0.9366635084 0.9166988134 + 0.9512215257 0.9369819164 0.9171562791 + 0.9514275789 0.9373003840 0.9176139235 + 0.9516335726 0.9376189113 0.9180715084 + 0.9518396258 0.9379373193 0.9185290933 + 0.9520456791 0.9382557869 0.9189860821 + 0.9522516727 0.9385743141 0.9194421172 + 0.9524577856 0.9388927221 0.9198979735 + 0.9526637793 0.9392104745 0.9203540087 + 0.9528685808 0.9395279288 0.9208099246 + 0.9530680180 0.9398452044 0.9212657809 + 0.9532672763 0.9401625991 0.9217218161 + 0.9534667134 0.9404799938 0.9221776724 + 0.9536660910 0.9407973289 0.9226272106 + 0.9538655281 0.9411147237 0.9230633974 + 0.9540649056 0.9414321184 0.9234995246 + 0.9542642832 0.9417477250 0.9239355922 + 0.9544624090 0.9420624971 0.9243717194 + 0.9546576738 0.9423773289 0.9248077869 + 0.9548529983 0.9426922202 0.9252439737 + 0.9550482035 0.9430069923 0.9256801009 + 0.9552435279 0.9433218837 0.9261130095 + 0.9554387927 0.9436367154 0.9265359044 + 0.9556339979 0.9439516068 0.9269587994 + 0.9558293223 0.9442648888 0.9273816943 + 0.9560242295 0.9445775747 0.9278045297 + 0.9562184215 0.9448903799 0.9282274246 + 0.9564124942 0.9452031255 0.9286503196 + 0.9566066861 0.9455158114 0.9290730953 + 0.9568008780 0.9458284974 0.9294950962 + 0.9569950104 0.9461413026 0.9299049973 + 0.9571892023 0.9464539886 0.9303148985 + 0.9573832750 0.9467725158 0.9307246804 + 0.9575788975 0.9470949173 0.9311345816 + 0.9577786922 0.9474173188 0.9315444827 + 0.9579784274 0.9477397799 0.9319543242 + 0.9581782222 0.9480621815 0.9323642254 + 0.9583780169 0.9483845830 0.9327741265 + 0.9585776925 0.9487069845 0.9331759810 + 0.9587774873 0.9490293860 0.9335759282 + 0.9589772820 0.9493505955 0.9339758158 + 0.9591774940 0.9496698976 0.9343758225 + 0.9593815804 0.9499891996 0.9347757101 + 0.9595857263 0.9503086209 0.9351757169 + 0.9597898126 0.9506279230 0.9355756044 + 0.9599938989 0.9509472251 0.9359754920 + 0.9601979852 0.9512665272 0.9363701940 + 0.9604020715 0.9515858889 0.9367583990 + 0.9606062174 0.9519029856 0.9371466041 + 0.9608103037 0.9522051215 0.9375349283 + 0.9610272050 0.9525070786 0.9379230738 + 0.9612463713 0.9528092146 0.9383112788 + 0.9614655972 0.9531112909 0.9386994839 + 0.9616847038 0.9534133077 0.9390878081 + 0.9619039297 0.9537153840 0.9394757748 + 0.9621229768 0.9540174007 0.9398490787 + 0.9623422027 0.9543194771 0.9402223229 + 0.9625613093 0.9546111822 0.9405955076 + 0.9627830982 0.9548994899 0.9409686923 + 0.9630070925 0.9551877975 0.9413419962 + 0.9632312059 0.9554761052 0.9417151809 + 0.9634553194 0.9557644129 0.9420884252 + 0.9636793733 0.9560527802 0.9424616098 + 0.9639034271 0.9563410878 0.9428309202 + 0.9641274810 0.9566293955 0.9431958199 + 0.9643515944 0.9569211006 0.9435607791 + 0.9645748734 0.9572210908 0.9439256787 + 0.9647920132 0.9575210214 0.9442906976 + 0.9650089741 0.9578210115 0.9446555972 + 0.9652261138 0.9581208825 0.9450206161 + 0.9654431939 0.9584208131 0.9453855157 + 0.9656602144 0.9587208033 0.9457504749 + 0.9658772945 0.9590206742 0.9461066127 + 0.9660943747 0.9593207240 0.9464613199 + 0.9663113952 0.9596204162 0.9468160272 + 0.9665256739 0.9599201083 0.9471706152 + 0.9667379260 0.9602196813 0.9475253224 + 0.9669501185 0.9605193734 0.9478799105 + 0.9671623111 0.9608191252 0.9482346177 + 0.9673745036 0.9611188173 0.9485892057 + 0.9675868154 0.9614183903 0.9489434958 + 0.9677990079 0.9617180824 0.9492951035 + 0.9680112004 0.9620164037 0.9496467113 + 0.9682233930 0.9623020887 0.9499983191 + 0.9684261084 0.9625877142 0.9503499866 + 0.9686285853 0.9628733993 0.9507015944 + 0.9688311219 0.9631590843 0.9510532022 + 0.9690335989 0.9634447098 0.9514048100 + 0.9692360759 0.9637303948 0.9517564178 + 0.9694386125 0.9640160203 0.9521070719 + 0.9696410894 0.9643017054 0.9524558187 + 0.9698436260 0.9645820260 0.9528045058 + 0.9700453281 0.9648528099 0.9531531930 + 0.9702448845 0.9651235938 0.9535018802 + 0.9704446197 0.9653943777 0.9538506269 + 0.9706441760 0.9656652212 0.9541993737 + 0.9708439112 0.9659358859 0.9545481205 + 0.9710435271 0.9662067294 0.9548968077 + 0.9712432027 0.9664775133 0.9552431107 + 0.9714428782 0.9667482972 0.9555861950 + 0.9716424942 0.9670147896 0.9559292197 + 0.9718437195 0.9672774076 0.9562721848 + 0.9720464945 0.9675399065 0.9566152096 + 0.9722493291 0.9678025246 0.9569581747 + 0.9724521041 0.9680650830 0.9573013186 + 0.9726549983 0.9683277011 0.9576442838 + 0.9728577733 0.9685903192 0.9579873085 + 0.9730606079 0.9688528776 0.9583258033 + 0.9732633829 0.9691154957 0.9586578012 + 0.9734662175 0.9693738818 0.9589897990 + 0.9736676216 0.9696295261 0.9593217969 + 0.9738680124 0.9698851109 0.9596537948 + 0.9740685225 0.9701406956 0.9599856734 + 0.9742689133 0.9703962803 0.9603176713 + 0.9744693041 0.9706519246 0.9606497288 + 0.9746698141 0.9709075093 0.9609817266 + 0.9748702049 0.9711630940 0.9613105059 + 0.9750707150 0.9714187980 0.9616318941 + 0.9752711058 0.9716637135 0.9619532824 + 0.9754704237 0.9719005227 0.9622746706 + 0.9756690860 0.9721373916 0.9625961185 + 0.9758678079 0.9723742008 0.9629175067 + 0.9760664105 0.9726111293 0.9632390141 + 0.9762650728 0.9728478789 0.9635604024 + 0.9764636755 0.9730846882 0.9638817906 + 0.9766623974 0.9733216166 0.9642019868 + 0.9768610001 0.9735584259 0.9645112753 + 0.9770597219 0.9737908244 0.9648205042 + 0.9772512913 0.9740175009 0.9651297927 + 0.9774367809 0.9742441773 0.9654390812 + 0.9776223898 0.9744709134 0.9657483101 + 0.9778078794 0.9746975899 0.9660575986 + 0.9779934287 0.9749243259 0.9663668871 + 0.9781789780 0.9751510024 0.9666761160 + 0.9783645272 0.9753776789 0.9669854045 + 0.9785500765 0.9756044149 0.9672716260 + 0.9787356257 0.9758260846 0.9675518870 + 0.9789177179 0.9760301113 0.9678322077 + 0.9790937901 0.9762340784 0.9681125283 + 0.9792699218 0.9764381051 0.9683927894 + 0.9794459939 0.9766421914 0.9686729908 + 0.9796221256 0.9768462181 0.9689533114 + 0.9797981977 0.9770501852 0.9692335725 + 0.9799743295 0.9772542119 0.9695138931 + 0.9801504016 0.9774581790 0.9697943926 + 0.9803264737 0.9776623249 0.9700750709 + 0.9805015922 0.9778532982 0.9703558087 + 0.9806643128 0.9780429006 0.9706364870 + 0.9808269739 0.9782323837 0.9709172249 + 0.9809898138 0.9784219861 0.9711979032 + 0.9811524749 0.9786115289 0.9714785814 + 0.9813151956 0.9788010716 0.9717593193 + 0.9814779162 0.9789906144 0.9720399976 + 0.9816406965 0.9791802168 0.9723206758 + 0.9818034172 0.9793697000 0.9725946784 + 0.9819660783 0.9795542955 0.9728677869 + 0.9821212888 0.9797332883 0.9731410146 + 0.9822732210 0.9799122810 0.9734141231 + 0.9824250937 0.9800912738 0.9736872911 + 0.9825769067 0.9802703261 0.9739603996 + 0.9827287793 0.9804493189 0.9742336273 + 0.9828807116 0.9806283116 0.9745066762 + 0.9830325842 0.9808073044 0.9747799039 + 0.9831845164 0.9809862971 0.9750474095 + 0.9833363891 0.9811652899 0.9753007293 + 0.9834849238 0.9813308120 0.9755538702 + 0.9836212993 0.9814954996 0.9758071899 + 0.9837576747 0.9816601872 0.9760603905 + 0.9838941097 0.9818249941 0.9763137102 + 0.9840304852 0.9819896817 0.9765669107 + 0.9841669798 0.9821544290 0.9768201709 + 0.9843034148 0.9823191762 0.9770733714 + 0.9844397902 0.9824839234 0.9773266912 + 0.9845762253 0.9826486111 0.9775664210 + 0.9847126007 0.9828112721 0.9777957797 + 0.9848353863 0.9829697013 0.9780251980 + 0.9849503040 0.9831280708 0.9782546163 + 0.9850652814 0.9832863808 0.9784839749 + 0.9851801991 0.9834448099 0.9787133932 + 0.9852951169 0.9836031199 0.9789428115 + 0.9854100943 0.9837614894 0.9791722894 + 0.9855250120 0.9839199185 0.9794017076 + 0.9856399298 0.9840782285 0.9796311259 + 0.9857547879 0.9842365980 0.9798449278 + 0.9858698249 0.9843990207 0.9800536036 + 0.9859718084 0.9845643044 0.9802622199 + 0.9860730767 0.9847295880 0.9804708958 + 0.9861744046 0.9848948717 0.9806795716 + 0.9862756729 0.9850602746 0.9808881879 + 0.9863771200 0.9852256179 0.9810969234 + 0.9864783883 0.9853909016 0.9813054800 + 0.9865797162 0.9855561852 0.9815142155 + 0.9866809845 0.9857215285 0.9817228913 + 0.9867823124 0.9858868122 0.9819098115 + 0.9868842959 0.9860410094 0.9820920825 + 0.9869905710 0.9861913919 0.9822744727 + 0.9870967865 0.9863417149 0.9824568033 + 0.9872030020 0.9864919782 0.9826391935 + 0.9873092771 0.9866423011 0.9828215241 + 0.9874154925 0.9867926240 0.9830037951 + 0.9875218272 0.9869430065 0.9831861854 + 0.9876279831 0.9870933294 0.9833685160 + 0.9877343178 0.9872435927 0.9835509062 + 0.9878404737 0.9873939157 0.9837235212 + 0.9879497886 0.9875226021 0.9838936925 + 0.9880681038 0.9876458049 0.9840639234 + 0.9881862998 0.9877688885 0.9842339754 + 0.9883046150 0.9878919721 0.9844042063 + 0.9884228706 0.9880151749 0.9845743775 + 0.9885411859 0.9881383181 0.9847446084 + 0.9886595011 0.9882614017 0.9849147201 + 0.9887778163 0.9883846045 0.9850848913 + 0.9888960123 0.9885076880 0.9852551222 + 0.9890143275 0.9886307716 0.9854249954 + 0.9891341925 0.9887586832 0.9855948091 + 0.9892588854 0.9888883233 0.9857646227 + 0.9893836975 0.9890179038 0.9859343767 + 0.9895085096 0.9891474247 0.9861043096 + 0.9896333218 0.9892770052 0.9862741232 + 0.9897580743 0.9894065857 0.9864438772 + 0.9898828268 0.9895361066 0.9866136909 + 0.9900075793 0.9896656871 0.9867835045 + 0.9901323915 0.9897953272 0.9869533181 + 0.9902572036 0.9899247885 0.9871199131 + 0.9903820157 0.9900571704 0.9872816801 + 0.9905074835 0.9901915789 0.9874433875 + 0.9906330109 0.9903261065 0.9876052141 + 0.9907584190 0.9904605150 0.9877669215 + 0.9908838868 0.9905949831 0.9879286885 + 0.9910094142 0.9907293916 0.9880905151 + 0.9911348224 0.9908639193 0.9882522225 + 0.9912602901 0.9909983277 0.9884139895 + 0.9913856983 0.9911326766 0.9885756969 + 0.9915112257 0.9912672043 0.9887369871 + 0.9916366935 0.9914013147 0.9888862967 + 0.9917575717 0.9915347099 0.9890357256 + 0.9918779731 0.9916681051 0.9891850948 + 0.9919983149 0.9918015003 0.9893345237 + 0.9921187162 0.9919348955 0.9894838929 + 0.9922391176 0.9920682907 0.9896332026 + 0.9923595190 0.9922016263 0.9897825718 + 0.9924799204 0.9923350215 0.9899320006 + 0.9926003218 0.9924684167 0.9900814295 + 0.9927207232 0.9926018119 0.9902307987 + 0.9928411245 0.9927352071 0.9903745055 + 0.9929553270 0.9928619862 0.9905139208 + 0.9930647016 0.9929882884 0.9906532764 + 0.9931741953 0.9931144714 0.9907926917 + 0.9932836294 0.9932407737 0.9909321070 + 0.9933931231 0.9933670759 0.9910715222 + 0.9935024977 0.9934933186 0.9912108779 + 0.9936119914 0.9936196208 0.9913502932 + 0.9937214851 0.9937458038 0.9914897084 + 0.9938309193 0.9938721061 0.9916291237 + 0.9939404130 0.9939982891 0.9917684793 + 0.9940490127 0.9941191077 0.9919059277 + 0.9941514730 0.9942321777 0.9920433164 + 0.9942539930 0.9943454266 0.9921807051 + 0.9943563938 0.9944584966 0.9923179746 + 0.9944589138 0.9945716858 0.9924554229 + 0.9945613742 0.9946848154 0.9925928116 + 0.9946638942 0.9947978854 0.9927302003 + 0.9947664142 0.9949110746 0.9928675294 + 0.9948688149 0.9950242043 0.9930049181 + 0.9949712753 0.9951373935 0.9931423068 + 0.9950737953 0.9952505231 0.9932782054 + 0.9951713085 0.9953569770 0.9934102893 + 0.9952647090 0.9954618812 0.9935423732 + 0.9953581095 0.9955667853 0.9936743975 + 0.9954516292 0.9956716895 0.9938064814 + 0.9955450296 0.9957765937 0.9939386249 + 0.9956383705 0.9958814979 0.9940707088 + 0.9957317710 0.9959864020 0.9942027926 + 0.9958252907 0.9960911870 0.9943348765 + 0.9959186912 0.9961960912 0.9944670200 + 0.9960120916 0.9963009953 0.9945989847 + 0.9961054921 0.9964051247 0.9947274923 + 0.9961966872 0.9965000153 0.9948515892 + 0.9962875843 0.9965949059 0.9949758053 + 0.9963784814 0.9966897964 0.9951000214 + 0.9964693785 0.9967846870 0.9952241182 + 0.9965602756 0.9968795776 0.9953482747 + 0.9966511726 0.9969745278 0.9954724908 + 0.9967421293 0.9970694184 0.9955965877 + 0.9968330264 0.9971643090 0.9957208037 + 0.9969239235 0.9972591996 0.9958449006 + 0.9970148206 0.9973540902 0.9959691167 + 0.9971055984 0.9974477887 0.9960889220 + 0.9971954226 0.9975377917 0.9962046742 + 0.9972853065 0.9976279140 0.9963206053 + 0.9973750710 0.9977179170 0.9964364171 + 0.9974650145 0.9978079200 0.9965522289 + 0.9975547791 0.9978979230 0.9966681004 + 0.9976447225 0.9979879260 0.9967839122 + 0.9977344871 0.9980779290 0.9968997240 + 0.9978243709 0.9981678724 0.9970155954 + 0.9979141951 0.9982579947 0.9971314073 + 0.9980040789 0.9983479977 0.9972472191 + 0.9980940223 0.9984341860 0.9973635077 + 0.9981843829 0.9985107780 0.9974802732 + 0.9982746840 0.9985873103 0.9975970984 + 0.9983651042 0.9986637831 0.9977139235 + 0.9984555244 0.9987403750 0.9978306890 + 0.9985458851 0.9988169074 0.9979475141 + 0.9986361861 0.9988934994 0.9980642796 + 0.9987266064 0.9989699721 0.9981811047 + 0.9988170266 0.9990465045 0.9982979298 + 0.9989073873 0.9991230965 0.9984146953 + 0.9989976883 0.9991996288 0.9985315204 + 0.9990869761 0.9992740154 0.9986500740 + 0.9991700053 0.9993399978 0.9987728000 + 0.9992529750 0.9994059801 0.9988955259 + 0.9993360043 0.9994720221 0.9990181923 + 0.9994189739 0.9995380044 0.9991409779 + 0.9995020032 0.9996039867 0.9992637038 + 0.9995849729 0.9996700287 0.9993864298 + 0.9996680021 0.9997360110 0.9995090961 + 0.9997509718 0.9998019934 0.9996318221 + 0.9998340011 0.9998679757 0.9997546077 + 0.9999169707 0.9999340177 0.9998772740 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_400x.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_400x.spi1d index 3216d6e7..cfed82fc 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_400x.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_400x.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0003428414 0.0002644671 0.0001401811 - 0.0007192744 0.0005423503 0.0002875368 - 0.0011335440 0.0008326409 0.0004389792 - 0.0015786689 0.0011433410 0.0005958441 - 0.0020476291 0.0014799440 0.0007588092 - 0.0025404820 0.0018298780 0.0009286210 - 0.0030616501 0.0021946840 0.0011060070 - 0.0036125139 0.0025735211 0.0012919310 - 0.0041929889 0.0029686880 0.0014847160 - 0.0048043700 0.0033767780 0.0016857330 - 0.0054497430 0.0038043610 0.0018942290 - 0.0061183511 0.0042493441 0.0021089639 - 0.0068146689 0.0047143549 0.0023315491 - 0.0075340671 0.0051967530 0.0025640430 - 0.0082950303 0.0057056639 0.0028035671 - 0.0090911062 0.0062496541 0.0030509511 - 0.0099175517 0.0068204752 0.0033067439 - 0.0107904803 0.0074211471 0.0035727150 - 0.0117002502 0.0080482997 0.0038465990 - 0.0126570398 0.0087003373 0.0041274340 - 0.0136648295 0.0093829166 0.0044194721 - 0.0147075197 0.0101065598 0.0047251140 - 0.0157950595 0.0108555797 0.0050429730 - 0.0169297699 0.0116311004 0.0053711529 - 0.0181103703 0.0124387797 0.0057113222 - 0.0193332806 0.0132759996 0.0060676620 - 0.0206129998 0.0141413296 0.0064390958 - 0.0219328105 0.0150437802 0.0068241018 - 0.0233088601 0.0159855708 0.0072196471 - 0.0247331504 0.0169505794 0.0076266280 - 0.0262072906 0.0179369505 0.0080475174 - 0.0277359709 0.0189558994 0.0084815770 - 0.0293284301 0.0200254805 0.0089281071 - 0.0309679396 0.0211107805 0.0093870452 - 0.0326597206 0.0222219098 0.0098592760 - 0.0343937390 0.0233689900 0.0103457300 - 0.0361940786 0.0245578606 0.0108465999 - 0.0380464606 0.0257728007 0.0113617396 - 0.0399493314 0.0270075500 0.0118910903 - 0.0419140086 0.0282745995 0.0124359597 - 0.0439488515 0.0296080001 0.0129955998 - 0.0460439511 0.0309876800 0.0135690300 - 0.0481984690 0.0323878303 0.0141631197 - 0.0504118018 0.0338152684 0.0147708496 - 0.0526927300 0.0352800488 0.0153927896 - 0.0550270416 0.0367900804 0.0160309207 - 0.0574264787 0.0383353084 0.0166857298 - 0.0598822795 0.0398985818 0.0173553694 - 0.0623928793 0.0414841883 0.0180462394 - 0.0649654418 0.0431171395 0.0187484995 - 0.0676013976 0.0448169708 0.0194632709 - 0.0702958703 0.0465711690 0.0201920494 - 0.0730418488 0.0483480282 0.0209390707 - 0.0758519098 0.0501504205 0.0217033103 - 0.0787305906 0.0519958399 0.0224795192 - 0.0816697702 0.0538894385 0.0232669692 - 0.0846705586 0.0558331497 0.0240675099 - 0.0877186880 0.0578123406 0.0248920992 - 0.0908257365 0.0598101094 0.0257320199 - 0.0940086022 0.0618289597 0.0265861396 - 0.0972353294 0.0639070272 0.0274642892 - 0.1005206034 0.0660314709 0.0283592492 - 0.1038618013 0.0682074204 0.0292693600 - 0.1072567031 0.0704347715 0.0302035604 - 0.1107103974 0.0726976171 0.0311573707 - 0.1142259985 0.0749783218 0.0321260095 - 0.1177944019 0.0772918463 0.0331200808 - 0.1214151010 0.0796481296 0.0341317691 - 0.1250821054 0.0820898935 0.0351568311 - 0.1287855953 0.0845760182 0.0362133905 - 0.1325509995 0.0870889798 0.0372799113 - 0.1363651007 0.0896213502 0.0383602083 - 0.1402193010 0.0921822414 0.0394625701 - 0.1441060007 0.0947721824 0.0405752398 - 0.1480046958 0.0973958895 0.0417135507 - 0.1519186944 0.1000884995 0.0428665988 - 0.1558499932 0.1028292999 0.0440348983 - 0.1598220021 0.1056241989 0.0452243313 - 0.1638336033 0.1084792018 0.0464208983 - 0.1678801030 0.1113488972 0.0476398095 - 0.1719820052 0.1142351031 0.0488679707 - 0.1761175990 0.1171704978 0.0501159504 - 0.1802733988 0.1201329008 0.0513837487 - 0.1844418943 0.1231521964 0.0526686385 - 0.1886288971 0.1262086034 0.0539824814 - 0.1928298026 0.1292911023 0.0553071685 - 0.1970759928 0.1324020028 0.0566712618 - 0.2013373971 0.1355181038 0.0580410585 - 0.2056180984 0.1386440992 0.0594506897 - 0.2099272013 0.1417901963 0.0608652011 - 0.2142390013 0.1449521929 0.0623159185 - 0.2185354978 0.1481644064 0.0637758672 - 0.2228298932 0.1513926983 0.0652757511 - 0.2271223068 0.1547017992 0.0667859018 - 0.2314382046 0.1580224037 0.0683313832 - 0.2357621044 0.1613737047 0.0698857680 - 0.2401127070 0.1647437066 0.0714698881 - 0.2444832027 0.1681362987 0.0730611980 - 0.2488923073 0.1715582013 0.0746805519 - 0.2533152103 0.1749991030 0.0763059929 - 0.2577489913 0.1784823984 0.0779676214 - 0.2621993124 0.1819818020 0.0796313584 - 0.2666550875 0.1855379939 0.0813349113 - 0.2711015940 0.1891071051 0.0830384567 - 0.2755472958 0.1927215010 0.0847683027 - 0.2799974084 0.1963423938 0.0865003392 - 0.2844476104 0.2000225931 0.0882561207 - 0.2888987958 0.2037090063 0.0900193900 - 0.2933452129 0.2073960006 0.0917988792 - 0.2977733016 0.2110829949 0.0935909674 - 0.3021937013 0.2147787064 0.0953960270 - 0.3065907061 0.2184758037 0.0972275063 - 0.3109777868 0.2221865058 0.0990616009 - 0.3153359890 0.2259006053 0.1009372994 - 0.3196929097 0.2296067029 0.1028129980 - 0.3240458071 0.2333091944 0.1047334969 - 0.3283928931 0.2370095998 0.1066685989 - 0.3327110112 0.2407082021 0.1086295024 - 0.3370262086 0.2444002032 0.1106277034 - 0.3413048983 0.2480811030 0.1126269996 - 0.3455835879 0.2517619133 0.1146645993 - 0.3498139083 0.2554424107 0.1167021990 - 0.3540413976 0.2591229081 0.1187660992 - 0.3582440019 0.2627831101 0.1208472997 - 0.3624396026 0.2664416134 0.1229355037 - 0.3666112125 0.2701123059 0.1250668019 - 0.3707655966 0.2737897933 0.1271981001 - 0.3749122918 0.2774769068 0.1293662041 - 0.3790445030 0.2811841965 0.1315550953 - 0.3831717968 0.2848913968 0.1337489933 - 0.3872435987 0.2885521054 0.1359899044 - 0.3913154006 0.2922112942 0.1382308006 - 0.3953064978 0.2958714068 0.1404815018 - 0.3992744088 0.2995322049 0.1427416056 - 0.4032138884 0.3031899035 0.1450016946 - 0.4071162045 0.3068281114 0.1472705007 - 0.4110161960 0.3104662001 0.1495404989 - 0.4148718119 0.3140855134 0.1518152952 - 0.4187273979 0.3176954091 0.1541071981 - 0.4225727022 0.3213038146 0.1563991010 - 0.4264118969 0.3249039054 0.1586962938 - 0.4302516878 0.3285039961 0.1609987020 - 0.4340943098 0.3320690989 0.1633010954 - 0.4379369915 0.3356105089 0.1656249017 - 0.4417563081 0.3391517997 0.1679558009 - 0.4455639124 0.3426533043 0.1702867001 - 0.4493665993 0.3461548090 0.1726520061 - 0.4531346858 0.3496446013 0.1750191003 - 0.4569028914 0.3531137109 0.1773868054 - 0.4606313109 0.3565827906 0.1797600985 - 0.4643262029 0.3600330055 0.1821334064 - 0.4680210054 0.3634741902 0.1845103949 - 0.4716567993 0.3669154048 0.1869013011 - 0.4752872884 0.3703399003 0.1892922968 - 0.4789054096 0.3737635016 0.1916884929 - 0.4824860990 0.3771829009 0.1940988004 - 0.4860669076 0.3805834055 0.1965090930 - 0.4896276891 0.3839837909 0.1989264935 - 0.4931711853 0.3873957992 0.2013619989 - 0.4967147112 0.3908275962 0.2037975937 - 0.5001958013 0.3942595124 0.2062384039 - 0.5036591887 0.3976672888 0.2086957991 - 0.5071225762 0.4010519087 0.2111531943 - 0.5105044842 0.4044364095 0.2136141956 - 0.5138846040 0.4077871144 0.2160944939 - 0.5172544718 0.4111149907 0.2185747027 - 0.5205571055 0.4144429862 0.2210548967 - 0.5238596201 0.4177626073 0.2235321999 - 0.5271421075 0.4210774004 0.2260096073 - 0.5303608179 0.4243923128 0.2284868956 - 0.5335794091 0.4276579022 0.2309691012 - 0.5367782116 0.4308958948 0.2334523946 - 0.5399305224 0.4341338873 0.2359357029 - 0.5430827141 0.4373329878 0.2384334058 - 0.5462197065 0.4405049086 0.2409410030 - 0.5493230820 0.4436767995 0.2434486002 - 0.5524265170 0.4468128979 0.2459536940 - 0.5555045009 0.4499109983 0.2484524995 - 0.5585151911 0.4530090988 0.2509512901 - 0.5615258813 0.4560930133 0.2534500957 - 0.5645241141 0.4591484070 0.2559669018 - 0.5674679279 0.4622037113 0.2584846020 - 0.5704116821 0.4652596116 0.2610023916 - 0.5733518004 0.4683186114 0.2635214031 - 0.5762202740 0.4713774920 0.2660414875 - 0.5790888071 0.4744364917 0.2685616016 - 0.5819572806 0.4774442017 0.2710804045 - 0.5847468972 0.4804444015 0.2735868096 - 0.5875238180 0.4834446013 0.2760930955 - 0.5903006792 0.4864538014 0.2785995007 - 0.5930446982 0.4894700050 0.2811084092 - 0.5957688093 0.4924862981 0.2836191952 - 0.5984929204 0.4954943061 0.2861298919 - 0.6011797786 0.4984689057 0.2886404097 - 0.6037912965 0.5014435053 0.2911294997 - 0.6064026952 0.5044180751 0.2936185896 - 0.6090142131 0.5073412061 0.2961077094 - 0.6115440130 0.5102497935 0.2985940874 - 0.6140723825 0.5131583810 0.3010762036 - 0.6166006923 0.5160554051 0.3035584092 - 0.6191328168 0.5189260840 0.3060404956 - 0.6216673851 0.5217968822 0.3085111082 - 0.6242020726 0.5246676803 0.3109765947 - 0.6267291903 0.5274786949 0.3134421110 - 0.6292034984 0.5302724242 0.3159075975 - 0.6316779256 0.5330660939 0.3183633983 - 0.6341521740 0.5358421206 0.3208185136 - 0.6366034746 0.5385411978 0.3232735991 - 0.6390401721 0.5412402749 0.3257263005 - 0.6414769292 0.5439392924 0.3281632960 - 0.6439121962 0.5465759039 0.3306002915 - 0.6463143826 0.5491589904 0.3330372870 - 0.6487165093 0.5517421961 0.3354707062 - 0.6511186957 0.5543252826 0.3378941119 - 0.6535044909 0.5568907857 0.3403174877 - 0.6558670998 0.5594527125 0.3427408934 - 0.6582297087 0.5620146990 0.3451600075 - 0.6605921984 0.5645741224 0.3475705981 - 0.6629050970 0.5670986772 0.3499810994 - 0.6651992202 0.5696232915 0.3523916006 - 0.6674932837 0.5721479058 0.3547942042 - 0.6697874069 0.5746628046 0.3571807146 - 0.6720268130 0.5771465898 0.3595671952 - 0.6742650867 0.5796303749 0.3619537950 - 0.6765033007 0.5821142197 0.3643409908 - 0.6787335277 0.5846080780 0.3667303920 - 0.6809238791 0.5871208906 0.3691197932 - 0.6831144094 0.5896338224 0.3715091050 - 0.6853048205 0.5921465755 0.3738974929 - 0.6874766946 0.5946516991 0.3762792051 - 0.6896049976 0.5971447825 0.3786608875 - 0.6917333007 0.5996379256 0.3810425997 - 0.6938616037 0.6021310091 0.3834243119 - 0.6959702969 0.6046125293 0.3858002126 - 0.6980445981 0.6070746183 0.3881756961 - 0.7001190186 0.6095365882 0.3905512989 - 0.7021933198 0.6119986773 0.3929268122 - 0.7042300105 0.6144391894 0.3952862024 - 0.7062014937 0.6168267131 0.3976376057 - 0.7081729770 0.6192142963 0.3999890983 - 0.7101445198 0.6216018200 0.4023405910 - 0.7120977044 0.6239740252 0.4046814144 - 0.7140085101 0.6262511015 0.4070011973 - 0.7159193754 0.6285281777 0.4093208909 - 0.7178301811 0.6308053732 0.4116407037 - 0.7197390199 0.6330825090 0.4139603972 - 0.7216380239 0.6352655292 0.4162608087 - 0.7235370874 0.6374403238 0.4185597897 - 0.7254360914 0.6396151185 0.4208588004 - 0.7273350954 0.6417899728 0.4231578112 - 0.7291654944 0.6438943744 0.4254482090 - 0.7309942245 0.6459581256 0.4277282059 - 0.7328227758 0.6480216980 0.4300082028 - 0.7346515059 0.6500852704 0.4322882891 - 0.7364507914 0.6521360278 0.4345682859 - 0.7382380962 0.6541525126 0.4368130863 - 0.7400254011 0.6561689973 0.4390538931 - 0.7418125868 0.6581856012 0.4412946999 - 0.7435858846 0.6602020860 0.4435355067 - 0.7453356981 0.6621177197 0.4457657039 - 0.7470855117 0.6640151739 0.4479677975 - 0.7488353252 0.6659126282 0.4501698911 - 0.7505850792 0.6678100824 0.4523718953 - 0.7522721887 0.6696808934 0.4545739889 - 0.7539572716 0.6715016961 0.4567523003 - 0.7556424141 0.6733224988 0.4589129984 - 0.7573276162 0.6751433015 0.4610736966 - 0.7590008974 0.6769641042 0.4632343948 - 0.7606620789 0.6787366271 0.4653950036 - 0.7623233199 0.6804947853 0.4675134122 - 0.7639845014 0.6822531223 0.4696210027 - 0.7656456828 0.6840112805 0.4717285931 - 0.7672730088 0.6857655048 0.4738362134 - 0.7688981295 0.6874911189 0.4759437144 - 0.7705233097 0.6892166138 0.4780164063 - 0.7721484900 0.6909422278 0.4800868034 - 0.7737551928 0.6926677227 0.4821572006 - 0.7753220201 0.6943894029 0.4842276871 - 0.7768889070 0.6961051822 0.4862979054 - 0.7784556746 0.6978209019 0.4883329868 - 0.7800226212 0.6995366812 0.4903681874 - 0.7815604806 0.7012524009 0.4924033880 - 0.7830802798 0.7029501796 0.4944385886 - 0.7846000791 0.7046359777 0.4964737892 - 0.7861198783 0.7063217163 0.4984565973 - 0.7876396775 0.7080073953 0.5004389286 - 0.7891361713 0.7096931934 0.5024212003 - 0.7906283140 0.7113500237 0.5044035912 - 0.7921203971 0.7129955292 0.5063859224 - 0.7936124802 0.7146410942 0.5083438754 - 0.7951045036 0.7162865996 0.5102990866 - 0.7965341210 0.7179322243 0.5122541785 - 0.7979624867 0.7195677161 0.5142092705 - 0.7993909717 0.7212001085 0.5161644220 - 0.8008195162 0.7228323817 0.5181018710 - 0.8022456169 0.7244647741 0.5200322866 - 0.8036258817 0.7260971069 0.5219627023 - 0.8050063252 0.7276834846 0.5238931179 - 0.8063865900 0.7292519212 0.5258234739 - 0.8077669144 0.7308204174 0.5277478099 - 0.8091464043 0.7323887944 0.5296645164 - 0.8105019927 0.7339571714 0.5315812826 - 0.8118575215 0.7355170846 0.5334979892 - 0.8132131100 0.7370710969 0.5354146957 - 0.8145685792 0.7386251092 0.5373278856 - 0.8159241080 0.7401791215 0.5392090082 - 0.8172050714 0.7417330742 0.5410900712 - 0.8184810877 0.7432689071 0.5429710746 - 0.8197571039 0.7447763085 0.5448521972 - 0.8210331798 0.7462838292 0.5467333198 - 0.8223091960 0.7477911711 0.5485991836 - 0.8235437870 0.7492985725 0.5504574180 - 0.8247652054 0.7507972121 0.5523154736 - 0.8259866834 0.7522162199 0.5541737080 - 0.8272081017 0.7536352873 0.5560318232 - 0.8284295797 0.7550544143 0.5578854084 - 0.8296269178 0.7564734817 0.5597094297 - 0.8307995200 0.7578924894 0.5615333915 - 0.8319721818 0.7592418194 0.5633574128 - 0.8331449032 0.7605643868 0.5651813745 - 0.8343175054 0.7618870139 0.5670053959 - 0.8354796767 0.7632095814 0.5688126087 - 0.8365802765 0.7645322084 0.5706043243 - 0.8376808167 0.7658463717 0.5723959208 - 0.8387814164 0.7671360970 0.5741875768 - 0.8398818970 0.7684258819 0.5759791732 - 0.8409824967 0.7697157264 0.5777708888 - 0.8420794010 0.7710055113 0.5795426965 - 0.8431748152 0.7722951770 0.5813097954 - 0.8442702293 0.7735279799 0.5830768943 - 0.8453655839 0.7747368217 0.5848439932 - 0.8464609981 0.7759457231 0.5866110921 - 0.8475471139 0.7771545053 0.5883777738 - 0.8485909104 0.7783632874 0.5900940895 - 0.8496348262 0.7795684934 0.5918104053 - 0.8506786823 0.7807170153 0.5935267210 - 0.8517224789 0.7818654180 0.5952430964 - 0.8527663946 0.7830138803 0.5969594121 - 0.8537737131 0.7841622829 0.5986704230 - 0.8547530770 0.7853106856 0.6003401279 - 0.8557325006 0.7864531875 0.6020097733 - 0.8567119241 0.7875832915 0.6036794186 - 0.8576912880 0.7887133956 0.6053491235 - 0.8586707115 0.7898433805 0.6070188284 - 0.8596333861 0.7909734845 0.6086828709 - 0.8605933189 0.7921035886 0.6103072166 - 0.8615533113 0.7932407856 0.6119315028 - 0.8625131845 0.7943854928 0.6135557890 - 0.8634731770 0.7955302000 0.6151800752 - 0.8644313216 0.7966749072 0.6168044209 - 0.8653596044 0.7978194952 0.6184266806 - 0.8662878871 0.7989642024 0.6199904084 - 0.8672162294 0.8000708222 0.6215540767 - 0.8681445122 0.8011484742 0.6231178045 - 0.8690729141 0.8022263050 0.6246814728 - 0.8699941039 0.8033040166 0.6262450814 - 0.8708788157 0.8043817282 0.6278088093 - 0.8717635870 0.8054593801 0.6293694973 - 0.8726484179 0.8065189123 0.6309295893 - 0.8735331297 0.8075633049 0.6324898005 - 0.8744179010 0.8086076975 0.6340498924 - 0.8752959967 0.8096520901 0.6356101036 - 0.8761417270 0.8106964827 0.6371701956 - 0.8769873977 0.8117408752 0.6387233734 - 0.8778331280 0.8127887249 0.6402710080 - 0.8786787987 0.8138411045 0.6418185830 - 0.8795245290 0.8148934841 0.6433662176 - 0.8803673983 0.8159459233 0.6449137926 - 0.8811801076 0.8169983029 0.6464614868 - 0.8819928765 0.8180506825 0.6480066180 - 0.8828055859 0.8190910220 0.6495386958 - 0.8836184144 0.8200846910 0.6510708928 - 0.8844311237 0.8210783005 0.6526029706 - 0.8852438927 0.8220720291 0.6541351080 - 0.8860117793 0.8230656981 0.6556673050 - 0.8867745996 0.8240593076 0.6571993828 - 0.8875373006 0.8250529766 0.6587041020 - 0.8883000016 0.8260309100 0.6601948738 - 0.8890628219 0.8270068169 0.6616855860 - 0.8898255229 0.8279826045 0.6631764174 - 0.8905583024 0.8289585114 0.6646671891 - 0.8912715912 0.8299342990 0.6661580205 - 0.8919848800 0.8309102058 0.6676467061 - 0.8926982284 0.8318812847 0.6691030264 - 0.8934115767 0.8328469992 0.6705592871 - 0.8941249251 0.8338125944 0.6720156074 - 0.8948332071 0.8347781897 0.6734719276 - 0.8955228925 0.8357439041 0.6749281883 - 0.8962126970 0.8367094994 0.6763845086 - 0.8969023824 0.8376750946 0.6778261065 - 0.8975921869 0.8385697007 0.6792402864 - 0.8982818723 0.8394591808 0.6806545854 - 0.8989716768 0.8403486013 0.6820688248 - 0.8996360898 0.8412380219 0.6834830046 - 0.9002904892 0.8421275020 0.6848973036 - 0.9009450078 0.8430169225 0.6863114834 - 0.9015994072 0.8438990116 0.6877009869 - 0.9022538066 0.8447635770 0.6890684962 - 0.9029083252 0.8456280828 0.6904361248 - 0.9035584927 0.8464927077 0.6918036938 - 0.9041764140 0.8473572731 0.6931712031 - 0.9047942758 0.8482218981 0.6945387721 - 0.9054121971 0.8490865231 0.6959062815 - 0.9060301781 0.8499292731 0.6972550750 - 0.9066480994 0.8507544994 0.6985912919 - 0.9072660208 0.8515797257 0.6999275088 - 0.9078578949 0.8524048924 0.7012637258 - 0.9084116817 0.8532301188 0.7025998831 - 0.9089655280 0.8540552855 0.7039362192 - 0.9095193148 0.8548805118 0.7052723765 - 0.9100729823 0.8557091951 0.7065873146 - 0.9106268287 0.8565393090 0.7078853250 - 0.9111806154 0.8573694825 0.7091832161 - 0.9117125273 0.8581997156 0.7104812264 - 0.9122291207 0.8590298891 0.7117791176 - 0.9127455950 0.8598600030 0.7130771279 - 0.9132621288 0.8606901765 0.7143750191 - 0.9137787223 0.8614907861 0.7156553864 - 0.9142951965 0.8622807264 0.7169089913 - 0.9148116708 0.8630706072 0.7181627154 - 0.9153102040 0.8638604879 0.7194163203 - 0.9157993793 0.8646503091 0.7206699252 - 0.9162884951 0.8654401898 0.7219234705 - 0.9167776108 0.8662300706 0.7231771946 - 0.9172667861 0.8670079708 0.7244259715 - 0.9177559018 0.8677796721 0.7256401777 - 0.9182450175 0.8685513735 0.7268543243 - 0.9187002778 0.8693230152 0.7280684114 - 0.9191343188 0.8700947165 0.7292826176 - 0.9195683002 0.8708662987 0.7304967046 - 0.9200022817 0.8716380000 0.7317107916 - 0.9204363227 0.8723940253 0.7329249978 - 0.9208703041 0.8731313944 0.7341042161 - 0.9213042855 0.8738688231 0.7352702022 - 0.9217389822 0.8746061921 0.7364361882 - 0.9221745729 0.8753436804 0.7376021743 - 0.9226102233 0.8760811090 0.7387682796 - 0.9230456948 0.8768184781 0.7399343252 - 0.9234812856 0.8775455952 0.7411003113 - 0.9239168763 0.8782122135 0.7422618270 - 0.9243525267 0.8788788915 0.7434059978 - 0.9247846007 0.8795455098 0.7445502877 - 0.9252011776 0.8802121878 0.7456945777 - 0.9256178141 0.8808789253 0.7468389273 - 0.9260342717 0.8815454841 0.7479830980 - 0.9264509082 0.8822122216 0.7491273880 - 0.9268674850 0.8828688264 0.7502716780 - 0.9272841215 0.8835214972 0.7514011860 - 0.9277005792 0.8841741085 0.7525197268 - 0.9280946255 0.8848267794 0.7536382079 - 0.9284824133 0.8854793906 0.7547566891 - 0.9288702011 0.8861320019 0.7558751702 - 0.9292579889 0.8867846727 0.7569937110 - 0.9296457767 0.8874309063 0.7581123114 - 0.9300336242 0.8880439997 0.7592307925 - 0.9304214716 0.8886572123 0.7603214979 - 0.9308112860 0.8892703056 0.7614058256 - 0.9312064052 0.8898835182 0.7624900937 - 0.9316015244 0.8904966116 0.7635744214 - 0.9319965839 0.8911098242 0.7646586895 - 0.9323917031 0.8917229176 0.7657430172 - 0.9327868223 0.8923314214 0.7668272853 - 0.9331818819 0.8929349184 0.7679116130 - 0.9335770011 0.8935384154 0.7689800262 - 0.9339686036 0.8941419125 0.7700471282 - 0.9343581796 0.8947455287 0.7711141109 - 0.9347478151 0.8953490257 0.7721812129 - 0.9351373911 0.8959525228 0.7732483149 - 0.9355270267 0.8965560198 0.7743152976 - 0.9359166026 0.8971521854 0.7753823996 - 0.9363061786 0.8977450132 0.7764495015 - 0.9366958141 0.8983377814 0.7774727941 - 0.9370884895 0.8989306092 0.7784914970 - 0.9374815822 0.8995233774 0.7795102000 - 0.9378746748 0.9001162052 0.7805289030 - 0.9382678270 0.9007089734 0.7815477252 - 0.9386609197 0.9013018012 0.7825664282 - 0.9390540123 0.9018632174 0.7835850716 - 0.9394471049 0.9024146795 0.7846037745 - 0.9398401976 0.9029660821 0.7855820060 - 0.9402080178 0.9035176039 0.7865473032 - 0.9405754209 0.9040690064 0.7875126004 - 0.9409428835 0.9046205282 0.7884777784 - 0.9413102865 0.9051718712 0.7894430757 - 0.9416776896 0.9057233930 0.7904083729 - 0.9420450926 0.9062736034 0.7913736105 - 0.9424126148 0.9068232179 0.7923389077 - 0.9427800179 0.9073727727 0.7932832241 - 0.9431211948 0.9079225063 0.7942044735 - 0.9434615970 0.9084721208 0.7951257825 - 0.9438019991 0.9090217948 0.7960470915 - 0.9441424012 0.9095714092 0.7969682813 - 0.9444829226 0.9101210237 0.7978895903 - 0.9448233247 0.9106593132 0.7988108993 - 0.9451637268 0.9111868739 0.7997322083 - 0.9455041289 0.9117143750 0.8006522059 - 0.9458433986 0.9122418761 0.8015512228 - 0.9461824298 0.9127693772 0.8024501204 - 0.9465214014 0.9132968783 0.8033490777 - 0.9468603730 0.9138243794 0.8042480946 - 0.9471994042 0.9143518806 0.8051471114 - 0.9475384951 0.9148817062 0.8060461283 - 0.9478775263 0.9154210091 0.8069450855 - 0.9482164979 0.9159603715 0.8078441024 - 0.9485536218 0.9164997935 0.8087291718 - 0.9488890767 0.9170392156 0.8096008897 - 0.9492245913 0.9175785184 0.8104726076 - 0.9495601058 0.9181178808 0.8113443255 - 0.9498956203 0.9186573029 0.8122159839 - 0.9502310157 0.9191967249 0.8130877018 - 0.9505664706 0.9197019935 0.8139594197 - 0.9509019852 0.9201949835 0.8148310781 - 0.9512333870 0.9206879139 0.8157027960 - 0.9515367150 0.9211807847 0.8165382743 - 0.9518401027 0.9216737747 0.8173654079 - 0.9521434903 0.9221667051 0.8181924820 - 0.9524468184 0.9226595759 0.8190196753 - 0.9527502060 0.9231525064 0.8198468089 - 0.9530535936 0.9236370921 0.8206740022 - 0.9533569813 0.9240685105 0.8215010762 - 0.9536603093 0.9244999290 0.8223283291 - 0.9539365172 0.9249312282 0.8231554031 - 0.9541925788 0.9253625870 0.8239536285 - 0.9544487000 0.9257940054 0.8247510791 - 0.9547048807 0.9262254238 0.8255487084 - 0.9549610019 0.9266567230 0.8263462782 - 0.9552171230 0.9270880818 0.8271439075 - 0.9554733038 0.9275091887 0.8279414773 - 0.9557294250 0.9279149771 0.8287389874 - 0.9559856057 0.9283208251 0.8295366168 - 0.9562343955 0.9287264943 0.8303341866 - 0.9564822912 0.9291322827 0.8311038017 - 0.9567301273 0.9295380116 0.8318734765 - 0.9569780231 0.9299438000 0.8326432109 - 0.9572259188 0.9303495884 0.8334128261 - 0.9574738145 0.9307553172 0.8341825008 - 0.9577217102 0.9311472178 0.8349521756 - 0.9579694867 0.9315264225 0.8357219100 - 0.9582170844 0.9319056273 0.8364915252 - 0.9584596753 0.9322847724 0.8372612000 - 0.9587022066 0.9326639771 0.8380025029 - 0.9589446783 0.9330431223 0.8387396932 - 0.9591872096 0.9334223270 0.8394770026 - 0.9594296813 0.9338014722 0.8402141929 - 0.9596722126 0.9341806769 0.8409515023 - 0.9599146843 0.9345543981 0.8416886926 - 0.9601572156 0.9349225760 0.8424260020 - 0.9603989124 0.9352908134 0.8431631923 - 0.9606298208 0.9356588721 0.8439005017 - 0.9608606100 0.9360271096 0.8446270823 - 0.9610915184 0.9363952875 0.8453466296 - 0.9613224268 0.9367635250 0.8460661769 - 0.9615532756 0.9371317029 0.8467857838 - 0.9617841840 0.9374998808 0.8475053906 - 0.9620149732 0.9378612041 0.8482248783 - 0.9622458816 0.9382085204 0.8489444852 - 0.9624767900 0.9385557771 0.8496640921 - 0.9627113938 0.9389030933 0.8503836989 - 0.9629462957 0.9392504096 0.8510984778 - 0.9631810784 0.9395977259 0.8517916203 - 0.9634159207 0.9399449825 0.8524847031 - 0.9636507034 0.9402922988 0.8531777263 - 0.9638856053 0.9406396151 0.8538708091 - 0.9641203880 0.9409868717 0.8545638919 - 0.9643551707 0.9413303137 0.8552569151 - 0.9645900726 0.9416738153 0.8559499979 - 0.9648157954 0.9420171976 0.8566430211 - 0.9650372267 0.9423606992 0.8573361039 - 0.9652585983 0.9427040815 0.8580203056 - 0.9654800892 0.9430475831 0.8586984873 - 0.9657015204 0.9433910251 0.8593767285 - 0.9659228921 0.9437345266 0.8600549102 - 0.9661443233 0.9440779090 0.8607330918 - 0.9663656950 0.9444156289 0.8614112735 - 0.9665871263 0.9447482824 0.8620895147 - 0.9668061733 0.9450809956 0.8627676964 - 0.9670186043 0.9454137087 0.8634458780 - 0.9672309160 0.9457464218 0.8641241193 - 0.9674432278 0.9460790157 0.8647658229 - 0.9676554799 0.9464117289 0.8654016256 - 0.9678679109 0.9467443824 0.8660374284 - 0.9680802226 0.9470770955 0.8666731715 - 0.9682924747 0.9474098086 0.8673089743 - 0.9685047865 0.9477065802 0.8679447770 - 0.9687172174 0.9479998946 0.8685806990 - 0.9689158201 0.9482930899 0.8692165017 - 0.9691076875 0.9485862851 0.8698523045 - 0.9692996740 0.9488794804 0.8704881072 - 0.9694917202 0.9491726756 0.8711016178 - 0.9696837068 0.9494658709 0.8717142940 - 0.9698755741 0.9497591257 0.8723270893 - 0.9700676203 0.9500523210 0.8729398847 - 0.9702596068 0.9503493905 0.8735526204 - 0.9704515934 0.9506735206 0.8741654158 - 0.9706435204 0.9509977102 0.8747782111 - 0.9708371758 0.9513218999 0.8753908873 - 0.9710310102 0.9516459703 0.8760036826 - 0.9712247849 0.9519702196 0.8766164780 - 0.9714186192 0.9522942901 0.8772181273 - 0.9716123939 0.9526184797 0.8778185844 - 0.9718062282 0.9529427290 0.8784192204 - 0.9720000029 0.9532667994 0.8790197968 - 0.9721937776 0.9535914063 0.8796203136 - 0.9723876119 0.9539176226 0.8802208900 - 0.9725819230 0.9542437792 0.8808215261 - 0.9727863073 0.9545699954 0.8814219832 - 0.9729905725 0.9548962116 0.8820226192 - 0.9731948972 0.9552224278 0.8826230764 - 0.9733992815 0.9555485845 0.8832188249 - 0.9736036062 0.9558748007 0.8838123083 - 0.9738078713 0.9562010169 0.8844056726 - 0.9740121961 0.9565271735 0.8849992156 - 0.9742165804 0.9568511248 0.8855926991 - 0.9744209051 0.9571459889 0.8861861825 - 0.9746251702 0.9574409127 0.8867797256 - 0.9748240113 0.9577358961 0.8873730898 - 0.9750226736 0.9580308199 0.8879665732 - 0.9752213955 0.9583256841 0.8885601163 - 0.9754201770 0.9586207271 0.8891478777 - 0.9756188989 0.9589155912 0.8897228837 - 0.9758176208 0.9592105150 0.8902978897 - 0.9760162830 0.9595054984 0.8908728957 - 0.9762151241 0.9598004222 0.8914479017 - 0.9764137864 0.9600865245 0.8920229077 - 0.9766125083 0.9603704214 0.8925979137 - 0.9767935276 0.9606543183 0.8931729198 - 0.9769693017 0.9609382749 0.8937479258 - 0.9771450758 0.9612221718 0.8943228722 - 0.9773209095 0.9615061283 0.8948978782 - 0.9774966836 0.9617900848 0.8954554200 - 0.9776725173 0.9620739818 0.8960071206 - 0.9778482914 0.9623578787 0.8965588808 - 0.9780241251 0.9626418948 0.8971105814 - 0.9781998992 0.9629204869 0.8976622820 - 0.9783756733 0.9631900191 0.8982141018 - 0.9785385132 0.9634596109 0.8987658024 - 0.9786810875 0.9637290835 0.8993175030 - 0.9788236022 0.9639986157 0.8998693228 - 0.9789661765 0.9642680883 0.9004210234 - 0.9791086912 0.9645376801 0.9009723067 - 0.9792513251 0.9648072124 0.9015076160 - 0.9793937802 0.9650766850 0.9020429850 - 0.9795364141 0.9653462172 0.9025782943 - 0.9796789289 0.9656158090 0.9031136036 - 0.9798215032 0.9658737183 0.9036489725 - 0.9799640179 0.9661279917 0.9041842818 - 0.9800881743 0.9663822055 0.9047195911 - 0.9802088141 0.9666364789 0.9052549005 - 0.9803295135 0.9668908119 0.9057903290 - 0.9804500937 0.9671450853 0.9063255787 - 0.9805706739 0.9673992991 0.9068583846 - 0.9806913733 0.9676535726 0.9073730111 - 0.9808120131 0.9679079056 0.9078876972 - 0.9809325933 0.9681621790 0.9084023237 - 0.9810532928 0.9684163928 0.9089170098 - 0.9811738729 0.9686573148 0.9094316959 - 0.9812927842 0.9688978791 0.9099463224 - 0.9813982248 0.9691385031 0.9104610085 - 0.9815034866 0.9693791270 0.9109755754 - 0.9816088080 0.9696196914 0.9114903212 - 0.9817141294 0.9698603153 0.9120050073 - 0.9818193913 0.9701009989 0.9125185013 - 0.9819247127 0.9703416228 0.9130074978 - 0.9820299745 0.9705821872 0.9134966135 - 0.9821352959 0.9708228111 0.9139856100 - 0.9822406173 0.9710630178 0.9144747257 - 0.9823458791 0.9712886810 0.9149637222 - 0.9824467897 0.9715144038 0.9154527783 - 0.9825308919 0.9717401266 0.9159417748 - 0.9826151133 0.9719656706 0.9164308906 - 0.9826992154 0.9721913934 0.9169198871 - 0.9827833176 0.9724171162 0.9174090028 - 0.9828674793 0.9726427794 0.9178979993 - 0.9829515815 0.9728683829 0.9183769822 - 0.9830356836 0.9730941057 0.9188531041 - 0.9831197858 0.9733198285 0.9193292260 - 0.9832040071 0.9735454917 0.9198052883 - 0.9832881093 0.9737340212 0.9202814102 - 0.9833707809 0.9739177823 0.9207574725 - 0.9834439754 0.9741016030 0.9212337136 - 0.9835172892 0.9742854238 0.9217097759 - 0.9835904837 0.9744691253 0.9221858978 - 0.9836636782 0.9746528864 0.9226620197 - 0.9837368727 0.9748367071 0.9231380820 - 0.9838101864 0.9750205278 0.9236043096 - 0.9838833809 0.9752042890 0.9240511060 - 0.9839565754 0.9753881097 0.9244979024 - 0.9840298891 0.9755718708 0.9249446988 - 0.9841030836 0.9757249951 0.9253914952 - 0.9841762781 0.9758548141 0.9258382916 - 0.9842556119 0.9759846926 0.9262850881 - 0.9843357205 0.9761145711 0.9267318249 - 0.9844158292 0.9762445092 0.9271786213 - 0.9844958782 0.9763743877 0.9276254177 - 0.9845761061 0.9765042067 0.9280722141 - 0.9846562147 0.9766340852 0.9285190105 - 0.9847363234 0.9767640233 0.9289492965 - 0.9848164916 0.9768939018 0.9293727875 - 0.9848966002 0.9770237803 0.9297962785 - 0.9849767089 0.9771535993 0.9302198291 - 0.9850568175 0.9772822857 0.9306433201 - 0.9851419926 0.9774109721 0.9310668111 - 0.9852340221 0.9775397778 0.9314903021 - 0.9853258729 0.9776685238 0.9319137931 - 0.9854179025 0.9777972102 0.9323372841 - 0.9855098724 0.9779258966 0.9327607751 - 0.9856017828 0.9780547023 0.9331843257 - 0.9856938124 0.9781833887 0.9336078167 - 0.9857857227 0.9783120751 0.9340065718 - 0.9858776927 0.9784408808 0.9344022870 - 0.9859697223 0.9785696268 0.9347980022 - 0.9860615730 0.9787036777 0.9351937175 - 0.9861536026 0.9788467884 0.9355893731 - 0.9862487912 0.9789898992 0.9359850883 - 0.9863448143 0.9791330099 0.9363806844 - 0.9864408970 0.9792761207 0.9367763996 - 0.9865369201 0.9794191718 0.9371721148 - 0.9866328835 0.9795622230 0.9375677705 - 0.9867290258 0.9797052741 0.9379634857 - 0.9868249893 0.9798483849 0.9383592010 - 0.9869210124 0.9799914956 0.9387403131 - 0.9870170951 0.9801346064 0.9391192794 - 0.9871131182 0.9802777171 0.9394981861 - 0.9872090816 0.9804211855 0.9398772120 - 0.9873052239 0.9805650711 0.9402561784 - 0.9874122143 0.9807090163 0.9406352043 - 0.9875195026 0.9808530211 0.9410141706 - 0.9876267910 0.9809969068 0.9413931966 - 0.9877340198 0.9811409116 0.9417721033 - 0.9878413081 0.9812847972 0.9421511292 - 0.9879485965 0.9814286828 0.9425300956 - 0.9880558848 0.9815726876 0.9429091215 - 0.9881631136 0.9817165732 0.9432799816 - 0.9882704020 0.9818605781 0.9436467290 - 0.9883776903 0.9820045233 0.9440134168 - 0.9884849787 0.9821479917 0.9443802238 - 0.9885923266 0.9822906852 0.9447469115 - 0.9887036085 0.9824334979 0.9451135993 - 0.9888151288 0.9825763106 0.9454804063 - 0.9889264703 0.9827191234 0.9458470941 - 0.9890379906 0.9828618765 0.9462139010 - 0.9891495109 0.9830046892 0.9465805888 - 0.9892609715 0.9831475019 0.9469472766 - 0.9893724918 0.9832903147 0.9473140836 - 0.9894838929 0.9834331274 0.9476754069 - 0.9895954132 0.9835758805 0.9480211139 - 0.9897068739 0.9837186933 0.9483667016 - 0.9898183942 0.9838594198 0.9487122893 - 0.9899299145 0.9839885831 0.9490579963 - 0.9900401235 0.9841178060 0.9494035840 - 0.9901499152 0.9842470884 0.9497491717 - 0.9902598262 0.9843763113 0.9500948787 - 0.9903696179 0.9845055938 0.9504405260 - 0.9904795289 0.9846348166 0.9507861137 - 0.9905893207 0.9847640991 0.9511318207 - 0.9906991124 0.9848933220 0.9514774084 - 0.9908090234 0.9850226045 0.9518229961 - 0.9909188151 0.9851518273 0.9521476030 - 0.9910287261 0.9852811098 0.9524605274 - 0.9911385179 0.9854102731 0.9527733922 - 0.9912483096 0.9855383039 0.9530863166 - 0.9913557172 0.9856656790 0.9533991814 - 0.9914582968 0.9857931137 0.9537121058 - 0.9915608764 0.9859204888 0.9540249705 - 0.9916635156 0.9860478044 0.9543378949 - 0.9917660952 0.9861751795 0.9546508193 - 0.9918686748 0.9863026142 0.9549636841 - 0.9919713140 0.9864299893 0.9552766085 - 0.9920738935 0.9865574241 0.9555894732 - 0.9921764731 0.9866846800 0.9559023976 - 0.9922791123 0.9868121147 0.9562044144 - 0.9923816919 0.9869394898 0.9565039873 - 0.9924842715 0.9870668054 0.9568036795 - 0.9925869107 0.9871830940 0.9571033120 - 0.9926795959 0.9872993827 0.9574030042 - 0.9927687049 0.9874156713 0.9577025771 - 0.9928578734 0.9875320196 0.9580023289 - 0.9929469824 0.9876484275 0.9583019018 - 0.9930362105 0.9877647161 0.9586015940 - 0.9931253195 0.9878810048 0.9589012265 - 0.9932144880 0.9879972935 0.9592009187 - 0.9933035970 0.9881135821 0.9595004916 - 0.9933928251 0.9882298708 0.9598001838 - 0.9934818745 0.9883462787 0.9600859880 - 0.9935711026 0.9884626269 0.9603683949 - 0.9936602116 0.9885759950 0.9606508017 - 0.9937493801 0.9886701107 0.9609330893 - 0.9938194156 0.9887642264 0.9612154961 - 0.9938877821 0.9888582230 0.9614979029 - 0.9939562082 0.9889522791 0.9617803097 - 0.9940245748 0.9890463948 0.9620627165 - 0.9940930009 0.9891405106 0.9623451233 - 0.9941614866 0.9892346263 0.9626274109 - 0.9942299128 0.9893286228 0.9629098177 - 0.9942982793 0.9894226789 0.9631922245 - 0.9943667054 0.9895167947 0.9634745717 - 0.9944350719 0.9896109104 0.9637439251 - 0.9945034981 0.9897050261 0.9640030265 - 0.9945719838 0.9897977114 0.9642621875 - 0.9946404099 0.9898560047 0.9645212889 - 0.9946832061 0.9899142981 0.9647803903 - 0.9947232008 0.9899725914 0.9650396109 - 0.9947631955 0.9900308847 0.9652987123 - 0.9948033094 0.9900891781 0.9655578732 - 0.9948433042 0.9901474714 0.9658169746 - 0.9948832989 0.9902058244 0.9660760760 - 0.9949232936 0.9902641177 0.9663352966 - 0.9949632883 0.9903224111 0.9665943980 - 0.9950032830 0.9903807044 0.9668534994 - 0.9950432777 0.9904391170 0.9671121836 - 0.9950833917 0.9904974103 0.9673674703 - 0.9951233864 0.9905557036 0.9676228166 - 0.9951633811 0.9905830026 0.9678779840 - 0.9951838851 0.9905992150 0.9681332707 - 0.9951946735 0.9906154275 0.9683884978 - 0.9952054024 0.9906315804 0.9686437845 - 0.9952160716 0.9906477928 0.9688990116 - 0.9952269197 0.9906640053 0.9691542983 - 0.9952375889 0.9906800985 0.9694095254 - 0.9952483177 0.9906963110 0.9696648121 - 0.9952589869 0.9907125235 0.9699200988 - 0.9952697754 0.9907286763 0.9701753259 - 0.9952805042 0.9907448888 0.9704306126 - 0.9952911735 0.9907611012 0.9706832170 - 0.9953020215 0.9907773137 0.9709324241 - 0.9953126907 0.9907947183 0.9711815715 - 0.9953212142 0.9908165932 0.9714307785 - 0.9953212142 0.9908384085 0.9716799855 - 0.9953212142 0.9908602834 0.9719290733 - 0.9953212142 0.9908822179 0.9721782804 - 0.9953212142 0.9909040928 0.9724274874 - 0.9953212142 0.9909260273 0.9726766944 - 0.9953212142 0.9909479022 0.9729259014 - 0.9953212142 0.9909697771 0.9731751084 - 0.9953212142 0.9909917116 0.9734243155 - 0.9953212142 0.9910135865 0.9736735225 - 0.9953212142 0.9910355210 0.9739227295 - 0.9953212142 0.9910573959 0.9741604924 - 0.9953212142 0.9910792708 0.9743885994 - 0.9953212142 0.9911091924 0.9746165872 - 0.9953212142 0.9911481738 0.9748446941 - 0.9953212142 0.9911870956 0.9750726819 - 0.9953212142 0.9912260771 0.9753007889 - 0.9953212142 0.9912649989 0.9755288959 - 0.9953212142 0.9913039804 0.9757568836 - 0.9953212142 0.9913429022 0.9759849906 - 0.9953212142 0.9913818836 0.9762129784 - 0.9953212142 0.9914208055 0.9764410853 - 0.9953212142 0.9914597869 0.9766690731 - 0.9953212142 0.9914987087 0.9768971801 - 0.9953212142 0.9915376902 0.9771252871 - 0.9953212142 0.9915766120 0.9773470759 - 0.9953212142 0.9916155934 0.9775602818 - 0.9953212142 0.9916595817 0.9777734280 - 0.9953212142 0.9917085767 0.9779865742 - 0.9953212142 0.9917575717 0.9781997800 - 0.9953212142 0.9918066263 0.9784129262 - 0.9953212142 0.9918555021 0.9786260724 - 0.9953212142 0.9919044971 0.9788392186 - 0.9953212142 0.9919534922 0.9790524244 - 0.9953212142 0.9920024872 0.9792655706 - 0.9953212142 0.9920514822 0.9794787169 - 0.9953212142 0.9921004176 0.9796919227 - 0.9953212142 0.9921494126 0.9799050093 - 0.9953212142 0.9921984076 0.9801182151 - 0.9953212142 0.9922474027 0.9803304076 - 0.9953212142 0.9922963977 0.9805278778 - 0.9953212142 0.9923471808 0.9807254076 - 0.9953224063 0.9924020767 0.9809228182 - 0.9953249097 0.9924570918 0.9811202884 - 0.9953272939 0.9925119877 0.9813178182 - 0.9953297973 0.9925670028 0.9815152884 - 0.9953323007 0.9926218987 0.9817128181 - 0.9953346848 0.9926769137 0.9819102883 - 0.9953371882 0.9927318096 0.9821078181 - 0.9953395724 0.9927868247 0.9823052883 - 0.9953420758 0.9928417206 0.9825028181 - 0.9953445196 0.9928966761 0.9827002287 - 0.9953470230 0.9929515719 0.9828976989 - 0.9953494072 0.9930065870 0.9830952287 - 0.9953519106 0.9930614829 0.9832865000 - 0.9953542948 0.9931164980 0.9834707975 - 0.9953567982 0.9931734204 0.9836549759 - 0.9953591824 0.9932305217 0.9838392735 - 0.9953616858 0.9932876825 0.9840235710 - 0.9953641891 0.9933447838 0.9842079282 - 0.9953665733 0.9934020042 0.9843922257 - 0.9953690767 0.9934591055 0.9845765233 - 0.9953715205 0.9935163260 0.9847608209 - 0.9953740239 0.9935734272 0.9849451184 - 0.9953764081 0.9936305881 0.9851294160 - 0.9953789115 0.9936876893 0.9853137136 - 0.9953812957 0.9937449098 0.9854980111 - 0.9953837991 0.9938020110 0.9856823087 - 0.9953861833 0.9938591719 0.9858666062 - 0.9953886867 0.9939162731 0.9860420227 - 0.9953911901 0.9939727783 0.9862126112 - 0.9953935742 0.9940270185 0.9863831997 - 0.9953960776 0.9940811992 0.9865537882 - 0.9953985214 0.9941353798 0.9867243767 - 0.9954010248 0.9941896200 0.9868950248 - 0.9954034090 0.9942439198 0.9870656133 - 0.9954059124 0.9942981005 0.9872363210 - 0.9954082966 0.9943522811 0.9874069095 - 0.9954108000 0.9944065213 0.9875774980 - 0.9954131842 0.9944607019 0.9877480865 - 0.9954156876 0.9945148826 0.9879186749 - 0.9954180717 0.9945691228 0.9880893230 - 0.9954205751 0.9946233034 0.9882599115 - 0.9954230785 0.9946774840 0.9884305000 - 0.9954255223 0.9947317243 0.9885919094 - 0.9954280257 0.9947829843 0.9887465835 - 0.9954304099 0.9948282242 0.9889013767 - 0.9954329133 0.9948732853 0.9890561104 - 0.9954352975 0.9949185252 0.9892109036 - 0.9954378009 0.9949635863 0.9893655777 - 0.9954401851 0.9950088263 0.9895203114 - 0.9954426885 0.9950540066 0.9896751046 - 0.9954450727 0.9950991273 0.9898297787 - 0.9954475760 0.9951443076 0.9899845719 - 0.9954500198 0.9951894283 0.9901393056 - 0.9954525232 0.9952346087 0.9902940989 - 0.9954550266 0.9952797890 0.9904487729 - 0.9954574108 0.9953249097 0.9906036258 - 0.9954599142 0.9953700900 0.9907582998 - 0.9954622984 0.9954152107 0.9909070134 - 0.9954648018 0.9954605103 0.9910390973 - 0.9954801202 0.9955064058 0.9911711812 - 0.9955012202 0.9955521822 0.9913033247 - 0.9955222011 0.9955980778 0.9914354086 - 0.9955431819 0.9956439137 0.9915674925 - 0.9955642223 0.9956898093 0.9916995764 - 0.9955853224 0.9957355857 0.9918317199 - 0.9956063032 0.9957814813 0.9919638038 - 0.9956272840 0.9958273172 0.9920958877 - 0.9956483245 0.9958732128 0.9922279716 - 0.9956693053 0.9959189892 0.9923601151 - 0.9956904054 0.9959648848 0.9924923182 - 0.9957113862 0.9960107207 0.9926244020 - 0.9957324266 0.9960566163 0.9927564859 - 0.9957534075 0.9961023927 0.9928886294 - 0.9957743883 0.9961482882 0.9930137992 - 0.9957994223 0.9961935878 0.9931361079 - 0.9958305955 0.9962387085 0.9932584167 - 0.9958618283 0.9962838292 0.9933807254 - 0.9958930016 0.9963290095 0.9935030937 - 0.9959241748 0.9963740706 0.9936254025 - 0.9959552884 0.9964193106 0.9937477112 - 0.9959865212 0.9964643717 0.9938700199 - 0.9960176945 0.9965096116 0.9939923882 - 0.9960489273 0.9965546727 0.9941146970 - 0.9960801005 0.9965999126 0.9942370057 - 0.9961112738 0.9966449738 0.9943593144 - 0.9961425066 0.9966902137 0.9944816828 - 0.9961736798 0.9967352748 0.9946039915 - 0.9962049127 0.9967803955 0.9947263002 - 0.9962360859 0.9968255758 0.9948486090 - 0.9962673187 0.9968703985 0.9949529171 - 0.9963043928 0.9969134927 0.9950549006 - 0.9963427782 0.9969565868 0.9951568842 - 0.9963811040 0.9969998002 0.9952589273 - 0.9964194894 0.9970428944 0.9953609109 - 0.9964578152 0.9970859885 0.9954628944 - 0.9964962006 0.9971290827 0.9955649972 - 0.9965345263 0.9971721768 0.9956669807 - 0.9965729117 0.9972152710 0.9957690239 - 0.9966111779 0.9972584248 0.9958710074 - 0.9966496229 0.9973015189 0.9959729910 - 0.9966878891 0.9973446131 0.9960749745 - 0.9967262745 0.9973878264 0.9961770177 - 0.9967646003 0.9974309206 0.9962790012 - 0.9968029857 0.9974740148 0.9963809848 - 0.9968413115 0.9975171089 0.9964830279 - 0.9968796968 0.9975619912 0.9965667129 - 0.9969285727 0.9976121783 0.9966462255 - 0.9969775081 0.9976624250 0.9967256784 - 0.9970263839 0.9977126122 0.9968051910 - 0.9970752001 0.9977626801 0.9968847036 - 0.9971240759 0.9978129268 0.9969642162 - 0.9971730113 0.9978631139 0.9970437884 - 0.9972218871 0.9979133010 0.9971233010 - 0.9972707033 0.9979634285 0.9972028136 - 0.9973195791 0.9980136156 0.9972823262 - 0.9973685145 0.9980638027 0.9973617792 - 0.9974173903 0.9981139898 0.9974412918 - 0.9974662066 0.9981641769 0.9975208044 - 0.9975150824 0.9982143044 0.9976003170 - 0.9975640178 0.9982644916 0.9976798296 - 0.9976128936 0.9983146787 0.9977592826 - 0.9976617098 0.9983649850 0.9978352189 - 0.9977235794 0.9984163046 0.9979071021 - 0.9977864027 0.9984676242 0.9979789257 - 0.9978492260 0.9985188842 0.9980508089 - 0.9979119897 0.9985702038 0.9981226921 - 0.9979748130 0.9986215234 0.9981945157 - 0.9980375767 0.9986727834 0.9982663989 - 0.9981004000 0.9987241030 0.9983382821 - 0.9981632233 0.9987754226 0.9984102249 - 0.9982259870 0.9988266826 0.9984819889 - 0.9982886910 0.9988780022 0.9985538721 - 0.9983515143 0.9989293218 0.9986258149 - 0.9984142780 0.9989805818 0.9986975789 - 0.9984771013 0.9990319014 0.9987695217 - 0.9985399246 0.9990832210 0.9988414049 - 0.9986026883 0.9991344810 0.9989132881 - 0.9986655116 0.9991858006 0.9989851117 - 0.9987378716 0.9992346764 0.9990463257 - 0.9988167882 0.9992824793 0.9991058707 - 0.9988957047 0.9993302822 0.9991654754 - 0.9989746213 0.9993782043 0.9992250800 - 0.9990534782 0.9994260073 0.9992846847 - 0.9991322756 0.9994738102 0.9993442893 - 0.9992111921 0.9995216727 0.9994038939 - 0.9992901087 0.9995694757 0.9994634986 - 0.9993690252 0.9996172786 0.9995231032 - 0.9994478226 0.9996652007 0.9995828271 - 0.9995266795 0.9997130036 0.9996423721 - 0.9996055961 0.9997608066 0.9997019768 - 0.9996845126 0.9998087287 0.9997615814 - 0.9997634292 0.9998564720 0.9998211861 - 0.9998422265 0.9999042749 0.9998807907 - 0.9999210835 0.9999521971 0.9999403954 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0003428414 0.0002644671 0.0001401811 + 0.0007192744 0.0005423503 0.0002875368 + 0.0011335440 0.0008326409 0.0004389792 + 0.0015786689 0.0011433410 0.0005958441 + 0.0020476291 0.0014799440 0.0007588092 + 0.0025404820 0.0018298780 0.0009286210 + 0.0030616501 0.0021946840 0.0011060070 + 0.0036125139 0.0025735211 0.0012919310 + 0.0041929889 0.0029686880 0.0014847160 + 0.0048043700 0.0033767780 0.0016857330 + 0.0054497430 0.0038043610 0.0018942290 + 0.0061183511 0.0042493441 0.0021089639 + 0.0068146689 0.0047143549 0.0023315491 + 0.0075340671 0.0051967530 0.0025640430 + 0.0082950303 0.0057056639 0.0028035671 + 0.0090911062 0.0062496541 0.0030509511 + 0.0099175517 0.0068204752 0.0033067439 + 0.0107904803 0.0074211471 0.0035727150 + 0.0117002502 0.0080482997 0.0038465990 + 0.0126570398 0.0087003373 0.0041274340 + 0.0136648295 0.0093829166 0.0044194721 + 0.0147075197 0.0101065598 0.0047251140 + 0.0157950595 0.0108555797 0.0050429730 + 0.0169297699 0.0116311004 0.0053711529 + 0.0181103703 0.0124387797 0.0057113222 + 0.0193332806 0.0132759996 0.0060676620 + 0.0206129998 0.0141413296 0.0064390958 + 0.0219328105 0.0150437802 0.0068241018 + 0.0233088601 0.0159855708 0.0072196471 + 0.0247331504 0.0169505794 0.0076266280 + 0.0262072906 0.0179369505 0.0080475174 + 0.0277359709 0.0189558994 0.0084815770 + 0.0293284301 0.0200254805 0.0089281071 + 0.0309679396 0.0211107805 0.0093870452 + 0.0326597206 0.0222219098 0.0098592760 + 0.0343937390 0.0233689900 0.0103457300 + 0.0361940786 0.0245578606 0.0108465999 + 0.0380464606 0.0257728007 0.0113617396 + 0.0399493314 0.0270075500 0.0118910903 + 0.0419140086 0.0282745995 0.0124359597 + 0.0439488515 0.0296080001 0.0129955998 + 0.0460439511 0.0309876800 0.0135690300 + 0.0481984690 0.0323878303 0.0141631197 + 0.0504118018 0.0338152684 0.0147708496 + 0.0526927300 0.0352800488 0.0153927896 + 0.0550270416 0.0367900804 0.0160309207 + 0.0574264787 0.0383353084 0.0166857298 + 0.0598822795 0.0398985818 0.0173553694 + 0.0623928793 0.0414841883 0.0180462394 + 0.0649654418 0.0431171395 0.0187484995 + 0.0676013976 0.0448169708 0.0194632709 + 0.0702958703 0.0465711690 0.0201920494 + 0.0730418488 0.0483480282 0.0209390707 + 0.0758519098 0.0501504205 0.0217033103 + 0.0787305906 0.0519958399 0.0224795192 + 0.0816697702 0.0538894385 0.0232669692 + 0.0846705586 0.0558331497 0.0240675099 + 0.0877186880 0.0578123406 0.0248920992 + 0.0908257365 0.0598101094 0.0257320199 + 0.0940086022 0.0618289597 0.0265861396 + 0.0972353294 0.0639070272 0.0274642892 + 0.1005206034 0.0660314709 0.0283592492 + 0.1038618013 0.0682074204 0.0292693600 + 0.1072567031 0.0704347715 0.0302035604 + 0.1107103974 0.0726976171 0.0311573707 + 0.1142259985 0.0749783218 0.0321260095 + 0.1177944019 0.0772918463 0.0331200808 + 0.1214151010 0.0796481296 0.0341317691 + 0.1250821054 0.0820898935 0.0351568311 + 0.1287855953 0.0845760182 0.0362133905 + 0.1325509995 0.0870889798 0.0372799113 + 0.1363651007 0.0896213502 0.0383602083 + 0.1402193010 0.0921822414 0.0394625701 + 0.1441060007 0.0947721824 0.0405752398 + 0.1480046958 0.0973958895 0.0417135507 + 0.1519186944 0.1000884995 0.0428665988 + 0.1558499932 0.1028292999 0.0440348983 + 0.1598220021 0.1056241989 0.0452243313 + 0.1638336033 0.1084792018 0.0464208983 + 0.1678801030 0.1113488972 0.0476398095 + 0.1719820052 0.1142351031 0.0488679707 + 0.1761175990 0.1171704978 0.0501159504 + 0.1802733988 0.1201329008 0.0513837487 + 0.1844418943 0.1231521964 0.0526686385 + 0.1886288971 0.1262086034 0.0539824814 + 0.1928298026 0.1292911023 0.0553071685 + 0.1970759928 0.1324020028 0.0566712618 + 0.2013373971 0.1355181038 0.0580410585 + 0.2056180984 0.1386440992 0.0594506897 + 0.2099272013 0.1417901963 0.0608652011 + 0.2142390013 0.1449521929 0.0623159185 + 0.2185354978 0.1481644064 0.0637758672 + 0.2228298932 0.1513926983 0.0652757511 + 0.2271223068 0.1547017992 0.0667859018 + 0.2314382046 0.1580224037 0.0683313832 + 0.2357621044 0.1613737047 0.0698857680 + 0.2401127070 0.1647437066 0.0714698881 + 0.2444832027 0.1681362987 0.0730611980 + 0.2488923073 0.1715582013 0.0746805519 + 0.2533152103 0.1749991030 0.0763059929 + 0.2577489913 0.1784823984 0.0779676214 + 0.2621993124 0.1819818020 0.0796313584 + 0.2666550875 0.1855379939 0.0813349113 + 0.2711015940 0.1891071051 0.0830384567 + 0.2755472958 0.1927215010 0.0847683027 + 0.2799974084 0.1963423938 0.0865003392 + 0.2844476104 0.2000225931 0.0882561207 + 0.2888987958 0.2037090063 0.0900193900 + 0.2933452129 0.2073960006 0.0917988792 + 0.2977733016 0.2110829949 0.0935909674 + 0.3021937013 0.2147787064 0.0953960270 + 0.3065907061 0.2184758037 0.0972275063 + 0.3109777868 0.2221865058 0.0990616009 + 0.3153359890 0.2259006053 0.1009372994 + 0.3196929097 0.2296067029 0.1028129980 + 0.3240458071 0.2333091944 0.1047334969 + 0.3283928931 0.2370095998 0.1066685989 + 0.3327110112 0.2407082021 0.1086295024 + 0.3370262086 0.2444002032 0.1106277034 + 0.3413048983 0.2480811030 0.1126269996 + 0.3455835879 0.2517619133 0.1146645993 + 0.3498139083 0.2554424107 0.1167021990 + 0.3540413976 0.2591229081 0.1187660992 + 0.3582440019 0.2627831101 0.1208472997 + 0.3624396026 0.2664416134 0.1229355037 + 0.3666112125 0.2701123059 0.1250668019 + 0.3707655966 0.2737897933 0.1271981001 + 0.3749122918 0.2774769068 0.1293662041 + 0.3790445030 0.2811841965 0.1315550953 + 0.3831717968 0.2848913968 0.1337489933 + 0.3872435987 0.2885521054 0.1359899044 + 0.3913154006 0.2922112942 0.1382308006 + 0.3953064978 0.2958714068 0.1404815018 + 0.3992744088 0.2995322049 0.1427416056 + 0.4032138884 0.3031899035 0.1450016946 + 0.4071162045 0.3068281114 0.1472705007 + 0.4110161960 0.3104662001 0.1495404989 + 0.4148718119 0.3140855134 0.1518152952 + 0.4187273979 0.3176954091 0.1541071981 + 0.4225727022 0.3213038146 0.1563991010 + 0.4264118969 0.3249039054 0.1586962938 + 0.4302516878 0.3285039961 0.1609987020 + 0.4340943098 0.3320690989 0.1633010954 + 0.4379369915 0.3356105089 0.1656249017 + 0.4417563081 0.3391517997 0.1679558009 + 0.4455639124 0.3426533043 0.1702867001 + 0.4493665993 0.3461548090 0.1726520061 + 0.4531346858 0.3496446013 0.1750191003 + 0.4569028914 0.3531137109 0.1773868054 + 0.4606313109 0.3565827906 0.1797600985 + 0.4643262029 0.3600330055 0.1821334064 + 0.4680210054 0.3634741902 0.1845103949 + 0.4716567993 0.3669154048 0.1869013011 + 0.4752872884 0.3703399003 0.1892922968 + 0.4789054096 0.3737635016 0.1916884929 + 0.4824860990 0.3771829009 0.1940988004 + 0.4860669076 0.3805834055 0.1965090930 + 0.4896276891 0.3839837909 0.1989264935 + 0.4931711853 0.3873957992 0.2013619989 + 0.4967147112 0.3908275962 0.2037975937 + 0.5001958013 0.3942595124 0.2062384039 + 0.5036591887 0.3976672888 0.2086957991 + 0.5071225762 0.4010519087 0.2111531943 + 0.5105044842 0.4044364095 0.2136141956 + 0.5138846040 0.4077871144 0.2160944939 + 0.5172544718 0.4111149907 0.2185747027 + 0.5205571055 0.4144429862 0.2210548967 + 0.5238596201 0.4177626073 0.2235321999 + 0.5271421075 0.4210774004 0.2260096073 + 0.5303608179 0.4243923128 0.2284868956 + 0.5335794091 0.4276579022 0.2309691012 + 0.5367782116 0.4308958948 0.2334523946 + 0.5399305224 0.4341338873 0.2359357029 + 0.5430827141 0.4373329878 0.2384334058 + 0.5462197065 0.4405049086 0.2409410030 + 0.5493230820 0.4436767995 0.2434486002 + 0.5524265170 0.4468128979 0.2459536940 + 0.5555045009 0.4499109983 0.2484524995 + 0.5585151911 0.4530090988 0.2509512901 + 0.5615258813 0.4560930133 0.2534500957 + 0.5645241141 0.4591484070 0.2559669018 + 0.5674679279 0.4622037113 0.2584846020 + 0.5704116821 0.4652596116 0.2610023916 + 0.5733518004 0.4683186114 0.2635214031 + 0.5762202740 0.4713774920 0.2660414875 + 0.5790888071 0.4744364917 0.2685616016 + 0.5819572806 0.4774442017 0.2710804045 + 0.5847468972 0.4804444015 0.2735868096 + 0.5875238180 0.4834446013 0.2760930955 + 0.5903006792 0.4864538014 0.2785995007 + 0.5930446982 0.4894700050 0.2811084092 + 0.5957688093 0.4924862981 0.2836191952 + 0.5984929204 0.4954943061 0.2861298919 + 0.6011797786 0.4984689057 0.2886404097 + 0.6037912965 0.5014435053 0.2911294997 + 0.6064026952 0.5044180751 0.2936185896 + 0.6090142131 0.5073412061 0.2961077094 + 0.6115440130 0.5102497935 0.2985940874 + 0.6140723825 0.5131583810 0.3010762036 + 0.6166006923 0.5160554051 0.3035584092 + 0.6191328168 0.5189260840 0.3060404956 + 0.6216673851 0.5217968822 0.3085111082 + 0.6242020726 0.5246676803 0.3109765947 + 0.6267291903 0.5274786949 0.3134421110 + 0.6292034984 0.5302724242 0.3159075975 + 0.6316779256 0.5330660939 0.3183633983 + 0.6341521740 0.5358421206 0.3208185136 + 0.6366034746 0.5385411978 0.3232735991 + 0.6390401721 0.5412402749 0.3257263005 + 0.6414769292 0.5439392924 0.3281632960 + 0.6439121962 0.5465759039 0.3306002915 + 0.6463143826 0.5491589904 0.3330372870 + 0.6487165093 0.5517421961 0.3354707062 + 0.6511186957 0.5543252826 0.3378941119 + 0.6535044909 0.5568907857 0.3403174877 + 0.6558670998 0.5594527125 0.3427408934 + 0.6582297087 0.5620146990 0.3451600075 + 0.6605921984 0.5645741224 0.3475705981 + 0.6629050970 0.5670986772 0.3499810994 + 0.6651992202 0.5696232915 0.3523916006 + 0.6674932837 0.5721479058 0.3547942042 + 0.6697874069 0.5746628046 0.3571807146 + 0.6720268130 0.5771465898 0.3595671952 + 0.6742650867 0.5796303749 0.3619537950 + 0.6765033007 0.5821142197 0.3643409908 + 0.6787335277 0.5846080780 0.3667303920 + 0.6809238791 0.5871208906 0.3691197932 + 0.6831144094 0.5896338224 0.3715091050 + 0.6853048205 0.5921465755 0.3738974929 + 0.6874766946 0.5946516991 0.3762792051 + 0.6896049976 0.5971447825 0.3786608875 + 0.6917333007 0.5996379256 0.3810425997 + 0.6938616037 0.6021310091 0.3834243119 + 0.6959702969 0.6046125293 0.3858002126 + 0.6980445981 0.6070746183 0.3881756961 + 0.7001190186 0.6095365882 0.3905512989 + 0.7021933198 0.6119986773 0.3929268122 + 0.7042300105 0.6144391894 0.3952862024 + 0.7062014937 0.6168267131 0.3976376057 + 0.7081729770 0.6192142963 0.3999890983 + 0.7101445198 0.6216018200 0.4023405910 + 0.7120977044 0.6239740252 0.4046814144 + 0.7140085101 0.6262511015 0.4070011973 + 0.7159193754 0.6285281777 0.4093208909 + 0.7178301811 0.6308053732 0.4116407037 + 0.7197390199 0.6330825090 0.4139603972 + 0.7216380239 0.6352655292 0.4162608087 + 0.7235370874 0.6374403238 0.4185597897 + 0.7254360914 0.6396151185 0.4208588004 + 0.7273350954 0.6417899728 0.4231578112 + 0.7291654944 0.6438943744 0.4254482090 + 0.7309942245 0.6459581256 0.4277282059 + 0.7328227758 0.6480216980 0.4300082028 + 0.7346515059 0.6500852704 0.4322882891 + 0.7364507914 0.6521360278 0.4345682859 + 0.7382380962 0.6541525126 0.4368130863 + 0.7400254011 0.6561689973 0.4390538931 + 0.7418125868 0.6581856012 0.4412946999 + 0.7435858846 0.6602020860 0.4435355067 + 0.7453356981 0.6621177197 0.4457657039 + 0.7470855117 0.6640151739 0.4479677975 + 0.7488353252 0.6659126282 0.4501698911 + 0.7505850792 0.6678100824 0.4523718953 + 0.7522721887 0.6696808934 0.4545739889 + 0.7539572716 0.6715016961 0.4567523003 + 0.7556424141 0.6733224988 0.4589129984 + 0.7573276162 0.6751433015 0.4610736966 + 0.7590008974 0.6769641042 0.4632343948 + 0.7606620789 0.6787366271 0.4653950036 + 0.7623233199 0.6804947853 0.4675134122 + 0.7639845014 0.6822531223 0.4696210027 + 0.7656456828 0.6840112805 0.4717285931 + 0.7672730088 0.6857655048 0.4738362134 + 0.7688981295 0.6874911189 0.4759437144 + 0.7705233097 0.6892166138 0.4780164063 + 0.7721484900 0.6909422278 0.4800868034 + 0.7737551928 0.6926677227 0.4821572006 + 0.7753220201 0.6943894029 0.4842276871 + 0.7768889070 0.6961051822 0.4862979054 + 0.7784556746 0.6978209019 0.4883329868 + 0.7800226212 0.6995366812 0.4903681874 + 0.7815604806 0.7012524009 0.4924033880 + 0.7830802798 0.7029501796 0.4944385886 + 0.7846000791 0.7046359777 0.4964737892 + 0.7861198783 0.7063217163 0.4984565973 + 0.7876396775 0.7080073953 0.5004389286 + 0.7891361713 0.7096931934 0.5024212003 + 0.7906283140 0.7113500237 0.5044035912 + 0.7921203971 0.7129955292 0.5063859224 + 0.7936124802 0.7146410942 0.5083438754 + 0.7951045036 0.7162865996 0.5102990866 + 0.7965341210 0.7179322243 0.5122541785 + 0.7979624867 0.7195677161 0.5142092705 + 0.7993909717 0.7212001085 0.5161644220 + 0.8008195162 0.7228323817 0.5181018710 + 0.8022456169 0.7244647741 0.5200322866 + 0.8036258817 0.7260971069 0.5219627023 + 0.8050063252 0.7276834846 0.5238931179 + 0.8063865900 0.7292519212 0.5258234739 + 0.8077669144 0.7308204174 0.5277478099 + 0.8091464043 0.7323887944 0.5296645164 + 0.8105019927 0.7339571714 0.5315812826 + 0.8118575215 0.7355170846 0.5334979892 + 0.8132131100 0.7370710969 0.5354146957 + 0.8145685792 0.7386251092 0.5373278856 + 0.8159241080 0.7401791215 0.5392090082 + 0.8172050714 0.7417330742 0.5410900712 + 0.8184810877 0.7432689071 0.5429710746 + 0.8197571039 0.7447763085 0.5448521972 + 0.8210331798 0.7462838292 0.5467333198 + 0.8223091960 0.7477911711 0.5485991836 + 0.8235437870 0.7492985725 0.5504574180 + 0.8247652054 0.7507972121 0.5523154736 + 0.8259866834 0.7522162199 0.5541737080 + 0.8272081017 0.7536352873 0.5560318232 + 0.8284295797 0.7550544143 0.5578854084 + 0.8296269178 0.7564734817 0.5597094297 + 0.8307995200 0.7578924894 0.5615333915 + 0.8319721818 0.7592418194 0.5633574128 + 0.8331449032 0.7605643868 0.5651813745 + 0.8343175054 0.7618870139 0.5670053959 + 0.8354796767 0.7632095814 0.5688126087 + 0.8365802765 0.7645322084 0.5706043243 + 0.8376808167 0.7658463717 0.5723959208 + 0.8387814164 0.7671360970 0.5741875768 + 0.8398818970 0.7684258819 0.5759791732 + 0.8409824967 0.7697157264 0.5777708888 + 0.8420794010 0.7710055113 0.5795426965 + 0.8431748152 0.7722951770 0.5813097954 + 0.8442702293 0.7735279799 0.5830768943 + 0.8453655839 0.7747368217 0.5848439932 + 0.8464609981 0.7759457231 0.5866110921 + 0.8475471139 0.7771545053 0.5883777738 + 0.8485909104 0.7783632874 0.5900940895 + 0.8496348262 0.7795684934 0.5918104053 + 0.8506786823 0.7807170153 0.5935267210 + 0.8517224789 0.7818654180 0.5952430964 + 0.8527663946 0.7830138803 0.5969594121 + 0.8537737131 0.7841622829 0.5986704230 + 0.8547530770 0.7853106856 0.6003401279 + 0.8557325006 0.7864531875 0.6020097733 + 0.8567119241 0.7875832915 0.6036794186 + 0.8576912880 0.7887133956 0.6053491235 + 0.8586707115 0.7898433805 0.6070188284 + 0.8596333861 0.7909734845 0.6086828709 + 0.8605933189 0.7921035886 0.6103072166 + 0.8615533113 0.7932407856 0.6119315028 + 0.8625131845 0.7943854928 0.6135557890 + 0.8634731770 0.7955302000 0.6151800752 + 0.8644313216 0.7966749072 0.6168044209 + 0.8653596044 0.7978194952 0.6184266806 + 0.8662878871 0.7989642024 0.6199904084 + 0.8672162294 0.8000708222 0.6215540767 + 0.8681445122 0.8011484742 0.6231178045 + 0.8690729141 0.8022263050 0.6246814728 + 0.8699941039 0.8033040166 0.6262450814 + 0.8708788157 0.8043817282 0.6278088093 + 0.8717635870 0.8054593801 0.6293694973 + 0.8726484179 0.8065189123 0.6309295893 + 0.8735331297 0.8075633049 0.6324898005 + 0.8744179010 0.8086076975 0.6340498924 + 0.8752959967 0.8096520901 0.6356101036 + 0.8761417270 0.8106964827 0.6371701956 + 0.8769873977 0.8117408752 0.6387233734 + 0.8778331280 0.8127887249 0.6402710080 + 0.8786787987 0.8138411045 0.6418185830 + 0.8795245290 0.8148934841 0.6433662176 + 0.8803673983 0.8159459233 0.6449137926 + 0.8811801076 0.8169983029 0.6464614868 + 0.8819928765 0.8180506825 0.6480066180 + 0.8828055859 0.8190910220 0.6495386958 + 0.8836184144 0.8200846910 0.6510708928 + 0.8844311237 0.8210783005 0.6526029706 + 0.8852438927 0.8220720291 0.6541351080 + 0.8860117793 0.8230656981 0.6556673050 + 0.8867745996 0.8240593076 0.6571993828 + 0.8875373006 0.8250529766 0.6587041020 + 0.8883000016 0.8260309100 0.6601948738 + 0.8890628219 0.8270068169 0.6616855860 + 0.8898255229 0.8279826045 0.6631764174 + 0.8905583024 0.8289585114 0.6646671891 + 0.8912715912 0.8299342990 0.6661580205 + 0.8919848800 0.8309102058 0.6676467061 + 0.8926982284 0.8318812847 0.6691030264 + 0.8934115767 0.8328469992 0.6705592871 + 0.8941249251 0.8338125944 0.6720156074 + 0.8948332071 0.8347781897 0.6734719276 + 0.8955228925 0.8357439041 0.6749281883 + 0.8962126970 0.8367094994 0.6763845086 + 0.8969023824 0.8376750946 0.6778261065 + 0.8975921869 0.8385697007 0.6792402864 + 0.8982818723 0.8394591808 0.6806545854 + 0.8989716768 0.8403486013 0.6820688248 + 0.8996360898 0.8412380219 0.6834830046 + 0.9002904892 0.8421275020 0.6848973036 + 0.9009450078 0.8430169225 0.6863114834 + 0.9015994072 0.8438990116 0.6877009869 + 0.9022538066 0.8447635770 0.6890684962 + 0.9029083252 0.8456280828 0.6904361248 + 0.9035584927 0.8464927077 0.6918036938 + 0.9041764140 0.8473572731 0.6931712031 + 0.9047942758 0.8482218981 0.6945387721 + 0.9054121971 0.8490865231 0.6959062815 + 0.9060301781 0.8499292731 0.6972550750 + 0.9066480994 0.8507544994 0.6985912919 + 0.9072660208 0.8515797257 0.6999275088 + 0.9078578949 0.8524048924 0.7012637258 + 0.9084116817 0.8532301188 0.7025998831 + 0.9089655280 0.8540552855 0.7039362192 + 0.9095193148 0.8548805118 0.7052723765 + 0.9100729823 0.8557091951 0.7065873146 + 0.9106268287 0.8565393090 0.7078853250 + 0.9111806154 0.8573694825 0.7091832161 + 0.9117125273 0.8581997156 0.7104812264 + 0.9122291207 0.8590298891 0.7117791176 + 0.9127455950 0.8598600030 0.7130771279 + 0.9132621288 0.8606901765 0.7143750191 + 0.9137787223 0.8614907861 0.7156553864 + 0.9142951965 0.8622807264 0.7169089913 + 0.9148116708 0.8630706072 0.7181627154 + 0.9153102040 0.8638604879 0.7194163203 + 0.9157993793 0.8646503091 0.7206699252 + 0.9162884951 0.8654401898 0.7219234705 + 0.9167776108 0.8662300706 0.7231771946 + 0.9172667861 0.8670079708 0.7244259715 + 0.9177559018 0.8677796721 0.7256401777 + 0.9182450175 0.8685513735 0.7268543243 + 0.9187002778 0.8693230152 0.7280684114 + 0.9191343188 0.8700947165 0.7292826176 + 0.9195683002 0.8708662987 0.7304967046 + 0.9200022817 0.8716380000 0.7317107916 + 0.9204363227 0.8723940253 0.7329249978 + 0.9208703041 0.8731313944 0.7341042161 + 0.9213042855 0.8738688231 0.7352702022 + 0.9217389822 0.8746061921 0.7364361882 + 0.9221745729 0.8753436804 0.7376021743 + 0.9226102233 0.8760811090 0.7387682796 + 0.9230456948 0.8768184781 0.7399343252 + 0.9234812856 0.8775455952 0.7411003113 + 0.9239168763 0.8782122135 0.7422618270 + 0.9243525267 0.8788788915 0.7434059978 + 0.9247846007 0.8795455098 0.7445502877 + 0.9252011776 0.8802121878 0.7456945777 + 0.9256178141 0.8808789253 0.7468389273 + 0.9260342717 0.8815454841 0.7479830980 + 0.9264509082 0.8822122216 0.7491273880 + 0.9268674850 0.8828688264 0.7502716780 + 0.9272841215 0.8835214972 0.7514011860 + 0.9277005792 0.8841741085 0.7525197268 + 0.9280946255 0.8848267794 0.7536382079 + 0.9284824133 0.8854793906 0.7547566891 + 0.9288702011 0.8861320019 0.7558751702 + 0.9292579889 0.8867846727 0.7569937110 + 0.9296457767 0.8874309063 0.7581123114 + 0.9300336242 0.8880439997 0.7592307925 + 0.9304214716 0.8886572123 0.7603214979 + 0.9308112860 0.8892703056 0.7614058256 + 0.9312064052 0.8898835182 0.7624900937 + 0.9316015244 0.8904966116 0.7635744214 + 0.9319965839 0.8911098242 0.7646586895 + 0.9323917031 0.8917229176 0.7657430172 + 0.9327868223 0.8923314214 0.7668272853 + 0.9331818819 0.8929349184 0.7679116130 + 0.9335770011 0.8935384154 0.7689800262 + 0.9339686036 0.8941419125 0.7700471282 + 0.9343581796 0.8947455287 0.7711141109 + 0.9347478151 0.8953490257 0.7721812129 + 0.9351373911 0.8959525228 0.7732483149 + 0.9355270267 0.8965560198 0.7743152976 + 0.9359166026 0.8971521854 0.7753823996 + 0.9363061786 0.8977450132 0.7764495015 + 0.9366958141 0.8983377814 0.7774727941 + 0.9370884895 0.8989306092 0.7784914970 + 0.9374815822 0.8995233774 0.7795102000 + 0.9378746748 0.9001162052 0.7805289030 + 0.9382678270 0.9007089734 0.7815477252 + 0.9386609197 0.9013018012 0.7825664282 + 0.9390540123 0.9018632174 0.7835850716 + 0.9394471049 0.9024146795 0.7846037745 + 0.9398401976 0.9029660821 0.7855820060 + 0.9402080178 0.9035176039 0.7865473032 + 0.9405754209 0.9040690064 0.7875126004 + 0.9409428835 0.9046205282 0.7884777784 + 0.9413102865 0.9051718712 0.7894430757 + 0.9416776896 0.9057233930 0.7904083729 + 0.9420450926 0.9062736034 0.7913736105 + 0.9424126148 0.9068232179 0.7923389077 + 0.9427800179 0.9073727727 0.7932832241 + 0.9431211948 0.9079225063 0.7942044735 + 0.9434615970 0.9084721208 0.7951257825 + 0.9438019991 0.9090217948 0.7960470915 + 0.9441424012 0.9095714092 0.7969682813 + 0.9444829226 0.9101210237 0.7978895903 + 0.9448233247 0.9106593132 0.7988108993 + 0.9451637268 0.9111868739 0.7997322083 + 0.9455041289 0.9117143750 0.8006522059 + 0.9458433986 0.9122418761 0.8015512228 + 0.9461824298 0.9127693772 0.8024501204 + 0.9465214014 0.9132968783 0.8033490777 + 0.9468603730 0.9138243794 0.8042480946 + 0.9471994042 0.9143518806 0.8051471114 + 0.9475384951 0.9148817062 0.8060461283 + 0.9478775263 0.9154210091 0.8069450855 + 0.9482164979 0.9159603715 0.8078441024 + 0.9485536218 0.9164997935 0.8087291718 + 0.9488890767 0.9170392156 0.8096008897 + 0.9492245913 0.9175785184 0.8104726076 + 0.9495601058 0.9181178808 0.8113443255 + 0.9498956203 0.9186573029 0.8122159839 + 0.9502310157 0.9191967249 0.8130877018 + 0.9505664706 0.9197019935 0.8139594197 + 0.9509019852 0.9201949835 0.8148310781 + 0.9512333870 0.9206879139 0.8157027960 + 0.9515367150 0.9211807847 0.8165382743 + 0.9518401027 0.9216737747 0.8173654079 + 0.9521434903 0.9221667051 0.8181924820 + 0.9524468184 0.9226595759 0.8190196753 + 0.9527502060 0.9231525064 0.8198468089 + 0.9530535936 0.9236370921 0.8206740022 + 0.9533569813 0.9240685105 0.8215010762 + 0.9536603093 0.9244999290 0.8223283291 + 0.9539365172 0.9249312282 0.8231554031 + 0.9541925788 0.9253625870 0.8239536285 + 0.9544487000 0.9257940054 0.8247510791 + 0.9547048807 0.9262254238 0.8255487084 + 0.9549610019 0.9266567230 0.8263462782 + 0.9552171230 0.9270880818 0.8271439075 + 0.9554733038 0.9275091887 0.8279414773 + 0.9557294250 0.9279149771 0.8287389874 + 0.9559856057 0.9283208251 0.8295366168 + 0.9562343955 0.9287264943 0.8303341866 + 0.9564822912 0.9291322827 0.8311038017 + 0.9567301273 0.9295380116 0.8318734765 + 0.9569780231 0.9299438000 0.8326432109 + 0.9572259188 0.9303495884 0.8334128261 + 0.9574738145 0.9307553172 0.8341825008 + 0.9577217102 0.9311472178 0.8349521756 + 0.9579694867 0.9315264225 0.8357219100 + 0.9582170844 0.9319056273 0.8364915252 + 0.9584596753 0.9322847724 0.8372612000 + 0.9587022066 0.9326639771 0.8380025029 + 0.9589446783 0.9330431223 0.8387396932 + 0.9591872096 0.9334223270 0.8394770026 + 0.9594296813 0.9338014722 0.8402141929 + 0.9596722126 0.9341806769 0.8409515023 + 0.9599146843 0.9345543981 0.8416886926 + 0.9601572156 0.9349225760 0.8424260020 + 0.9603989124 0.9352908134 0.8431631923 + 0.9606298208 0.9356588721 0.8439005017 + 0.9608606100 0.9360271096 0.8446270823 + 0.9610915184 0.9363952875 0.8453466296 + 0.9613224268 0.9367635250 0.8460661769 + 0.9615532756 0.9371317029 0.8467857838 + 0.9617841840 0.9374998808 0.8475053906 + 0.9620149732 0.9378612041 0.8482248783 + 0.9622458816 0.9382085204 0.8489444852 + 0.9624767900 0.9385557771 0.8496640921 + 0.9627113938 0.9389030933 0.8503836989 + 0.9629462957 0.9392504096 0.8510984778 + 0.9631810784 0.9395977259 0.8517916203 + 0.9634159207 0.9399449825 0.8524847031 + 0.9636507034 0.9402922988 0.8531777263 + 0.9638856053 0.9406396151 0.8538708091 + 0.9641203880 0.9409868717 0.8545638919 + 0.9643551707 0.9413303137 0.8552569151 + 0.9645900726 0.9416738153 0.8559499979 + 0.9648157954 0.9420171976 0.8566430211 + 0.9650372267 0.9423606992 0.8573361039 + 0.9652585983 0.9427040815 0.8580203056 + 0.9654800892 0.9430475831 0.8586984873 + 0.9657015204 0.9433910251 0.8593767285 + 0.9659228921 0.9437345266 0.8600549102 + 0.9661443233 0.9440779090 0.8607330918 + 0.9663656950 0.9444156289 0.8614112735 + 0.9665871263 0.9447482824 0.8620895147 + 0.9668061733 0.9450809956 0.8627676964 + 0.9670186043 0.9454137087 0.8634458780 + 0.9672309160 0.9457464218 0.8641241193 + 0.9674432278 0.9460790157 0.8647658229 + 0.9676554799 0.9464117289 0.8654016256 + 0.9678679109 0.9467443824 0.8660374284 + 0.9680802226 0.9470770955 0.8666731715 + 0.9682924747 0.9474098086 0.8673089743 + 0.9685047865 0.9477065802 0.8679447770 + 0.9687172174 0.9479998946 0.8685806990 + 0.9689158201 0.9482930899 0.8692165017 + 0.9691076875 0.9485862851 0.8698523045 + 0.9692996740 0.9488794804 0.8704881072 + 0.9694917202 0.9491726756 0.8711016178 + 0.9696837068 0.9494658709 0.8717142940 + 0.9698755741 0.9497591257 0.8723270893 + 0.9700676203 0.9500523210 0.8729398847 + 0.9702596068 0.9503493905 0.8735526204 + 0.9704515934 0.9506735206 0.8741654158 + 0.9706435204 0.9509977102 0.8747782111 + 0.9708371758 0.9513218999 0.8753908873 + 0.9710310102 0.9516459703 0.8760036826 + 0.9712247849 0.9519702196 0.8766164780 + 0.9714186192 0.9522942901 0.8772181273 + 0.9716123939 0.9526184797 0.8778185844 + 0.9718062282 0.9529427290 0.8784192204 + 0.9720000029 0.9532667994 0.8790197968 + 0.9721937776 0.9535914063 0.8796203136 + 0.9723876119 0.9539176226 0.8802208900 + 0.9725819230 0.9542437792 0.8808215261 + 0.9727863073 0.9545699954 0.8814219832 + 0.9729905725 0.9548962116 0.8820226192 + 0.9731948972 0.9552224278 0.8826230764 + 0.9733992815 0.9555485845 0.8832188249 + 0.9736036062 0.9558748007 0.8838123083 + 0.9738078713 0.9562010169 0.8844056726 + 0.9740121961 0.9565271735 0.8849992156 + 0.9742165804 0.9568511248 0.8855926991 + 0.9744209051 0.9571459889 0.8861861825 + 0.9746251702 0.9574409127 0.8867797256 + 0.9748240113 0.9577358961 0.8873730898 + 0.9750226736 0.9580308199 0.8879665732 + 0.9752213955 0.9583256841 0.8885601163 + 0.9754201770 0.9586207271 0.8891478777 + 0.9756188989 0.9589155912 0.8897228837 + 0.9758176208 0.9592105150 0.8902978897 + 0.9760162830 0.9595054984 0.8908728957 + 0.9762151241 0.9598004222 0.8914479017 + 0.9764137864 0.9600865245 0.8920229077 + 0.9766125083 0.9603704214 0.8925979137 + 0.9767935276 0.9606543183 0.8931729198 + 0.9769693017 0.9609382749 0.8937479258 + 0.9771450758 0.9612221718 0.8943228722 + 0.9773209095 0.9615061283 0.8948978782 + 0.9774966836 0.9617900848 0.8954554200 + 0.9776725173 0.9620739818 0.8960071206 + 0.9778482914 0.9623578787 0.8965588808 + 0.9780241251 0.9626418948 0.8971105814 + 0.9781998992 0.9629204869 0.8976622820 + 0.9783756733 0.9631900191 0.8982141018 + 0.9785385132 0.9634596109 0.8987658024 + 0.9786810875 0.9637290835 0.8993175030 + 0.9788236022 0.9639986157 0.8998693228 + 0.9789661765 0.9642680883 0.9004210234 + 0.9791086912 0.9645376801 0.9009723067 + 0.9792513251 0.9648072124 0.9015076160 + 0.9793937802 0.9650766850 0.9020429850 + 0.9795364141 0.9653462172 0.9025782943 + 0.9796789289 0.9656158090 0.9031136036 + 0.9798215032 0.9658737183 0.9036489725 + 0.9799640179 0.9661279917 0.9041842818 + 0.9800881743 0.9663822055 0.9047195911 + 0.9802088141 0.9666364789 0.9052549005 + 0.9803295135 0.9668908119 0.9057903290 + 0.9804500937 0.9671450853 0.9063255787 + 0.9805706739 0.9673992991 0.9068583846 + 0.9806913733 0.9676535726 0.9073730111 + 0.9808120131 0.9679079056 0.9078876972 + 0.9809325933 0.9681621790 0.9084023237 + 0.9810532928 0.9684163928 0.9089170098 + 0.9811738729 0.9686573148 0.9094316959 + 0.9812927842 0.9688978791 0.9099463224 + 0.9813982248 0.9691385031 0.9104610085 + 0.9815034866 0.9693791270 0.9109755754 + 0.9816088080 0.9696196914 0.9114903212 + 0.9817141294 0.9698603153 0.9120050073 + 0.9818193913 0.9701009989 0.9125185013 + 0.9819247127 0.9703416228 0.9130074978 + 0.9820299745 0.9705821872 0.9134966135 + 0.9821352959 0.9708228111 0.9139856100 + 0.9822406173 0.9710630178 0.9144747257 + 0.9823458791 0.9712886810 0.9149637222 + 0.9824467897 0.9715144038 0.9154527783 + 0.9825308919 0.9717401266 0.9159417748 + 0.9826151133 0.9719656706 0.9164308906 + 0.9826992154 0.9721913934 0.9169198871 + 0.9827833176 0.9724171162 0.9174090028 + 0.9828674793 0.9726427794 0.9178979993 + 0.9829515815 0.9728683829 0.9183769822 + 0.9830356836 0.9730941057 0.9188531041 + 0.9831197858 0.9733198285 0.9193292260 + 0.9832040071 0.9735454917 0.9198052883 + 0.9832881093 0.9737340212 0.9202814102 + 0.9833707809 0.9739177823 0.9207574725 + 0.9834439754 0.9741016030 0.9212337136 + 0.9835172892 0.9742854238 0.9217097759 + 0.9835904837 0.9744691253 0.9221858978 + 0.9836636782 0.9746528864 0.9226620197 + 0.9837368727 0.9748367071 0.9231380820 + 0.9838101864 0.9750205278 0.9236043096 + 0.9838833809 0.9752042890 0.9240511060 + 0.9839565754 0.9753881097 0.9244979024 + 0.9840298891 0.9755718708 0.9249446988 + 0.9841030836 0.9757249951 0.9253914952 + 0.9841762781 0.9758548141 0.9258382916 + 0.9842556119 0.9759846926 0.9262850881 + 0.9843357205 0.9761145711 0.9267318249 + 0.9844158292 0.9762445092 0.9271786213 + 0.9844958782 0.9763743877 0.9276254177 + 0.9845761061 0.9765042067 0.9280722141 + 0.9846562147 0.9766340852 0.9285190105 + 0.9847363234 0.9767640233 0.9289492965 + 0.9848164916 0.9768939018 0.9293727875 + 0.9848966002 0.9770237803 0.9297962785 + 0.9849767089 0.9771535993 0.9302198291 + 0.9850568175 0.9772822857 0.9306433201 + 0.9851419926 0.9774109721 0.9310668111 + 0.9852340221 0.9775397778 0.9314903021 + 0.9853258729 0.9776685238 0.9319137931 + 0.9854179025 0.9777972102 0.9323372841 + 0.9855098724 0.9779258966 0.9327607751 + 0.9856017828 0.9780547023 0.9331843257 + 0.9856938124 0.9781833887 0.9336078167 + 0.9857857227 0.9783120751 0.9340065718 + 0.9858776927 0.9784408808 0.9344022870 + 0.9859697223 0.9785696268 0.9347980022 + 0.9860615730 0.9787036777 0.9351937175 + 0.9861536026 0.9788467884 0.9355893731 + 0.9862487912 0.9789898992 0.9359850883 + 0.9863448143 0.9791330099 0.9363806844 + 0.9864408970 0.9792761207 0.9367763996 + 0.9865369201 0.9794191718 0.9371721148 + 0.9866328835 0.9795622230 0.9375677705 + 0.9867290258 0.9797052741 0.9379634857 + 0.9868249893 0.9798483849 0.9383592010 + 0.9869210124 0.9799914956 0.9387403131 + 0.9870170951 0.9801346064 0.9391192794 + 0.9871131182 0.9802777171 0.9394981861 + 0.9872090816 0.9804211855 0.9398772120 + 0.9873052239 0.9805650711 0.9402561784 + 0.9874122143 0.9807090163 0.9406352043 + 0.9875195026 0.9808530211 0.9410141706 + 0.9876267910 0.9809969068 0.9413931966 + 0.9877340198 0.9811409116 0.9417721033 + 0.9878413081 0.9812847972 0.9421511292 + 0.9879485965 0.9814286828 0.9425300956 + 0.9880558848 0.9815726876 0.9429091215 + 0.9881631136 0.9817165732 0.9432799816 + 0.9882704020 0.9818605781 0.9436467290 + 0.9883776903 0.9820045233 0.9440134168 + 0.9884849787 0.9821479917 0.9443802238 + 0.9885923266 0.9822906852 0.9447469115 + 0.9887036085 0.9824334979 0.9451135993 + 0.9888151288 0.9825763106 0.9454804063 + 0.9889264703 0.9827191234 0.9458470941 + 0.9890379906 0.9828618765 0.9462139010 + 0.9891495109 0.9830046892 0.9465805888 + 0.9892609715 0.9831475019 0.9469472766 + 0.9893724918 0.9832903147 0.9473140836 + 0.9894838929 0.9834331274 0.9476754069 + 0.9895954132 0.9835758805 0.9480211139 + 0.9897068739 0.9837186933 0.9483667016 + 0.9898183942 0.9838594198 0.9487122893 + 0.9899299145 0.9839885831 0.9490579963 + 0.9900401235 0.9841178060 0.9494035840 + 0.9901499152 0.9842470884 0.9497491717 + 0.9902598262 0.9843763113 0.9500948787 + 0.9903696179 0.9845055938 0.9504405260 + 0.9904795289 0.9846348166 0.9507861137 + 0.9905893207 0.9847640991 0.9511318207 + 0.9906991124 0.9848933220 0.9514774084 + 0.9908090234 0.9850226045 0.9518229961 + 0.9909188151 0.9851518273 0.9521476030 + 0.9910287261 0.9852811098 0.9524605274 + 0.9911385179 0.9854102731 0.9527733922 + 0.9912483096 0.9855383039 0.9530863166 + 0.9913557172 0.9856656790 0.9533991814 + 0.9914582968 0.9857931137 0.9537121058 + 0.9915608764 0.9859204888 0.9540249705 + 0.9916635156 0.9860478044 0.9543378949 + 0.9917660952 0.9861751795 0.9546508193 + 0.9918686748 0.9863026142 0.9549636841 + 0.9919713140 0.9864299893 0.9552766085 + 0.9920738935 0.9865574241 0.9555894732 + 0.9921764731 0.9866846800 0.9559023976 + 0.9922791123 0.9868121147 0.9562044144 + 0.9923816919 0.9869394898 0.9565039873 + 0.9924842715 0.9870668054 0.9568036795 + 0.9925869107 0.9871830940 0.9571033120 + 0.9926795959 0.9872993827 0.9574030042 + 0.9927687049 0.9874156713 0.9577025771 + 0.9928578734 0.9875320196 0.9580023289 + 0.9929469824 0.9876484275 0.9583019018 + 0.9930362105 0.9877647161 0.9586015940 + 0.9931253195 0.9878810048 0.9589012265 + 0.9932144880 0.9879972935 0.9592009187 + 0.9933035970 0.9881135821 0.9595004916 + 0.9933928251 0.9882298708 0.9598001838 + 0.9934818745 0.9883462787 0.9600859880 + 0.9935711026 0.9884626269 0.9603683949 + 0.9936602116 0.9885759950 0.9606508017 + 0.9937493801 0.9886701107 0.9609330893 + 0.9938194156 0.9887642264 0.9612154961 + 0.9938877821 0.9888582230 0.9614979029 + 0.9939562082 0.9889522791 0.9617803097 + 0.9940245748 0.9890463948 0.9620627165 + 0.9940930009 0.9891405106 0.9623451233 + 0.9941614866 0.9892346263 0.9626274109 + 0.9942299128 0.9893286228 0.9629098177 + 0.9942982793 0.9894226789 0.9631922245 + 0.9943667054 0.9895167947 0.9634745717 + 0.9944350719 0.9896109104 0.9637439251 + 0.9945034981 0.9897050261 0.9640030265 + 0.9945719838 0.9897977114 0.9642621875 + 0.9946404099 0.9898560047 0.9645212889 + 0.9946832061 0.9899142981 0.9647803903 + 0.9947232008 0.9899725914 0.9650396109 + 0.9947631955 0.9900308847 0.9652987123 + 0.9948033094 0.9900891781 0.9655578732 + 0.9948433042 0.9901474714 0.9658169746 + 0.9948832989 0.9902058244 0.9660760760 + 0.9949232936 0.9902641177 0.9663352966 + 0.9949632883 0.9903224111 0.9665943980 + 0.9950032830 0.9903807044 0.9668534994 + 0.9950432777 0.9904391170 0.9671121836 + 0.9950833917 0.9904974103 0.9673674703 + 0.9951233864 0.9905557036 0.9676228166 + 0.9951633811 0.9905830026 0.9678779840 + 0.9951838851 0.9905992150 0.9681332707 + 0.9951946735 0.9906154275 0.9683884978 + 0.9952054024 0.9906315804 0.9686437845 + 0.9952160716 0.9906477928 0.9688990116 + 0.9952269197 0.9906640053 0.9691542983 + 0.9952375889 0.9906800985 0.9694095254 + 0.9952483177 0.9906963110 0.9696648121 + 0.9952589869 0.9907125235 0.9699200988 + 0.9952697754 0.9907286763 0.9701753259 + 0.9952805042 0.9907448888 0.9704306126 + 0.9952911735 0.9907611012 0.9706832170 + 0.9953020215 0.9907773137 0.9709324241 + 0.9953126907 0.9907947183 0.9711815715 + 0.9953212142 0.9908165932 0.9714307785 + 0.9953212142 0.9908384085 0.9716799855 + 0.9953212142 0.9908602834 0.9719290733 + 0.9953212142 0.9908822179 0.9721782804 + 0.9953212142 0.9909040928 0.9724274874 + 0.9953212142 0.9909260273 0.9726766944 + 0.9953212142 0.9909479022 0.9729259014 + 0.9953212142 0.9909697771 0.9731751084 + 0.9953212142 0.9909917116 0.9734243155 + 0.9953212142 0.9910135865 0.9736735225 + 0.9953212142 0.9910355210 0.9739227295 + 0.9953212142 0.9910573959 0.9741604924 + 0.9953212142 0.9910792708 0.9743885994 + 0.9953212142 0.9911091924 0.9746165872 + 0.9953212142 0.9911481738 0.9748446941 + 0.9953212142 0.9911870956 0.9750726819 + 0.9953212142 0.9912260771 0.9753007889 + 0.9953212142 0.9912649989 0.9755288959 + 0.9953212142 0.9913039804 0.9757568836 + 0.9953212142 0.9913429022 0.9759849906 + 0.9953212142 0.9913818836 0.9762129784 + 0.9953212142 0.9914208055 0.9764410853 + 0.9953212142 0.9914597869 0.9766690731 + 0.9953212142 0.9914987087 0.9768971801 + 0.9953212142 0.9915376902 0.9771252871 + 0.9953212142 0.9915766120 0.9773470759 + 0.9953212142 0.9916155934 0.9775602818 + 0.9953212142 0.9916595817 0.9777734280 + 0.9953212142 0.9917085767 0.9779865742 + 0.9953212142 0.9917575717 0.9781997800 + 0.9953212142 0.9918066263 0.9784129262 + 0.9953212142 0.9918555021 0.9786260724 + 0.9953212142 0.9919044971 0.9788392186 + 0.9953212142 0.9919534922 0.9790524244 + 0.9953212142 0.9920024872 0.9792655706 + 0.9953212142 0.9920514822 0.9794787169 + 0.9953212142 0.9921004176 0.9796919227 + 0.9953212142 0.9921494126 0.9799050093 + 0.9953212142 0.9921984076 0.9801182151 + 0.9953212142 0.9922474027 0.9803304076 + 0.9953212142 0.9922963977 0.9805278778 + 0.9953212142 0.9923471808 0.9807254076 + 0.9953224063 0.9924020767 0.9809228182 + 0.9953249097 0.9924570918 0.9811202884 + 0.9953272939 0.9925119877 0.9813178182 + 0.9953297973 0.9925670028 0.9815152884 + 0.9953323007 0.9926218987 0.9817128181 + 0.9953346848 0.9926769137 0.9819102883 + 0.9953371882 0.9927318096 0.9821078181 + 0.9953395724 0.9927868247 0.9823052883 + 0.9953420758 0.9928417206 0.9825028181 + 0.9953445196 0.9928966761 0.9827002287 + 0.9953470230 0.9929515719 0.9828976989 + 0.9953494072 0.9930065870 0.9830952287 + 0.9953519106 0.9930614829 0.9832865000 + 0.9953542948 0.9931164980 0.9834707975 + 0.9953567982 0.9931734204 0.9836549759 + 0.9953591824 0.9932305217 0.9838392735 + 0.9953616858 0.9932876825 0.9840235710 + 0.9953641891 0.9933447838 0.9842079282 + 0.9953665733 0.9934020042 0.9843922257 + 0.9953690767 0.9934591055 0.9845765233 + 0.9953715205 0.9935163260 0.9847608209 + 0.9953740239 0.9935734272 0.9849451184 + 0.9953764081 0.9936305881 0.9851294160 + 0.9953789115 0.9936876893 0.9853137136 + 0.9953812957 0.9937449098 0.9854980111 + 0.9953837991 0.9938020110 0.9856823087 + 0.9953861833 0.9938591719 0.9858666062 + 0.9953886867 0.9939162731 0.9860420227 + 0.9953911901 0.9939727783 0.9862126112 + 0.9953935742 0.9940270185 0.9863831997 + 0.9953960776 0.9940811992 0.9865537882 + 0.9953985214 0.9941353798 0.9867243767 + 0.9954010248 0.9941896200 0.9868950248 + 0.9954034090 0.9942439198 0.9870656133 + 0.9954059124 0.9942981005 0.9872363210 + 0.9954082966 0.9943522811 0.9874069095 + 0.9954108000 0.9944065213 0.9875774980 + 0.9954131842 0.9944607019 0.9877480865 + 0.9954156876 0.9945148826 0.9879186749 + 0.9954180717 0.9945691228 0.9880893230 + 0.9954205751 0.9946233034 0.9882599115 + 0.9954230785 0.9946774840 0.9884305000 + 0.9954255223 0.9947317243 0.9885919094 + 0.9954280257 0.9947829843 0.9887465835 + 0.9954304099 0.9948282242 0.9889013767 + 0.9954329133 0.9948732853 0.9890561104 + 0.9954352975 0.9949185252 0.9892109036 + 0.9954378009 0.9949635863 0.9893655777 + 0.9954401851 0.9950088263 0.9895203114 + 0.9954426885 0.9950540066 0.9896751046 + 0.9954450727 0.9950991273 0.9898297787 + 0.9954475760 0.9951443076 0.9899845719 + 0.9954500198 0.9951894283 0.9901393056 + 0.9954525232 0.9952346087 0.9902940989 + 0.9954550266 0.9952797890 0.9904487729 + 0.9954574108 0.9953249097 0.9906036258 + 0.9954599142 0.9953700900 0.9907582998 + 0.9954622984 0.9954152107 0.9909070134 + 0.9954648018 0.9954605103 0.9910390973 + 0.9954801202 0.9955064058 0.9911711812 + 0.9955012202 0.9955521822 0.9913033247 + 0.9955222011 0.9955980778 0.9914354086 + 0.9955431819 0.9956439137 0.9915674925 + 0.9955642223 0.9956898093 0.9916995764 + 0.9955853224 0.9957355857 0.9918317199 + 0.9956063032 0.9957814813 0.9919638038 + 0.9956272840 0.9958273172 0.9920958877 + 0.9956483245 0.9958732128 0.9922279716 + 0.9956693053 0.9959189892 0.9923601151 + 0.9956904054 0.9959648848 0.9924923182 + 0.9957113862 0.9960107207 0.9926244020 + 0.9957324266 0.9960566163 0.9927564859 + 0.9957534075 0.9961023927 0.9928886294 + 0.9957743883 0.9961482882 0.9930137992 + 0.9957994223 0.9961935878 0.9931361079 + 0.9958305955 0.9962387085 0.9932584167 + 0.9958618283 0.9962838292 0.9933807254 + 0.9958930016 0.9963290095 0.9935030937 + 0.9959241748 0.9963740706 0.9936254025 + 0.9959552884 0.9964193106 0.9937477112 + 0.9959865212 0.9964643717 0.9938700199 + 0.9960176945 0.9965096116 0.9939923882 + 0.9960489273 0.9965546727 0.9941146970 + 0.9960801005 0.9965999126 0.9942370057 + 0.9961112738 0.9966449738 0.9943593144 + 0.9961425066 0.9966902137 0.9944816828 + 0.9961736798 0.9967352748 0.9946039915 + 0.9962049127 0.9967803955 0.9947263002 + 0.9962360859 0.9968255758 0.9948486090 + 0.9962673187 0.9968703985 0.9949529171 + 0.9963043928 0.9969134927 0.9950549006 + 0.9963427782 0.9969565868 0.9951568842 + 0.9963811040 0.9969998002 0.9952589273 + 0.9964194894 0.9970428944 0.9953609109 + 0.9964578152 0.9970859885 0.9954628944 + 0.9964962006 0.9971290827 0.9955649972 + 0.9965345263 0.9971721768 0.9956669807 + 0.9965729117 0.9972152710 0.9957690239 + 0.9966111779 0.9972584248 0.9958710074 + 0.9966496229 0.9973015189 0.9959729910 + 0.9966878891 0.9973446131 0.9960749745 + 0.9967262745 0.9973878264 0.9961770177 + 0.9967646003 0.9974309206 0.9962790012 + 0.9968029857 0.9974740148 0.9963809848 + 0.9968413115 0.9975171089 0.9964830279 + 0.9968796968 0.9975619912 0.9965667129 + 0.9969285727 0.9976121783 0.9966462255 + 0.9969775081 0.9976624250 0.9967256784 + 0.9970263839 0.9977126122 0.9968051910 + 0.9970752001 0.9977626801 0.9968847036 + 0.9971240759 0.9978129268 0.9969642162 + 0.9971730113 0.9978631139 0.9970437884 + 0.9972218871 0.9979133010 0.9971233010 + 0.9972707033 0.9979634285 0.9972028136 + 0.9973195791 0.9980136156 0.9972823262 + 0.9973685145 0.9980638027 0.9973617792 + 0.9974173903 0.9981139898 0.9974412918 + 0.9974662066 0.9981641769 0.9975208044 + 0.9975150824 0.9982143044 0.9976003170 + 0.9975640178 0.9982644916 0.9976798296 + 0.9976128936 0.9983146787 0.9977592826 + 0.9976617098 0.9983649850 0.9978352189 + 0.9977235794 0.9984163046 0.9979071021 + 0.9977864027 0.9984676242 0.9979789257 + 0.9978492260 0.9985188842 0.9980508089 + 0.9979119897 0.9985702038 0.9981226921 + 0.9979748130 0.9986215234 0.9981945157 + 0.9980375767 0.9986727834 0.9982663989 + 0.9981004000 0.9987241030 0.9983382821 + 0.9981632233 0.9987754226 0.9984102249 + 0.9982259870 0.9988266826 0.9984819889 + 0.9982886910 0.9988780022 0.9985538721 + 0.9983515143 0.9989293218 0.9986258149 + 0.9984142780 0.9989805818 0.9986975789 + 0.9984771013 0.9990319014 0.9987695217 + 0.9985399246 0.9990832210 0.9988414049 + 0.9986026883 0.9991344810 0.9989132881 + 0.9986655116 0.9991858006 0.9989851117 + 0.9987378716 0.9992346764 0.9990463257 + 0.9988167882 0.9992824793 0.9991058707 + 0.9988957047 0.9993302822 0.9991654754 + 0.9989746213 0.9993782043 0.9992250800 + 0.9990534782 0.9994260073 0.9992846847 + 0.9991322756 0.9994738102 0.9993442893 + 0.9992111921 0.9995216727 0.9994038939 + 0.9992901087 0.9995694757 0.9994634986 + 0.9993690252 0.9996172786 0.9995231032 + 0.9994478226 0.9996652007 0.9995828271 + 0.9995266795 0.9997130036 0.9996423721 + 0.9996055961 0.9997608066 0.9997019768 + 0.9996845126 0.9998087287 0.9997615814 + 0.9997634292 0.9998564720 0.9998211861 + 0.9998422265 0.9999042749 0.9998807907 + 0.9999210835 0.9999521971 0.9999403954 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_64.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_64.spi1d index 31d4b94e..d0c1916c 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_64.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_64.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0003177276 0.0001941380 0.0001446141 - 0.0006504654 0.0003951061 0.0002949580 - 0.0010024470 0.0006061521 0.0004526987 - 0.0013694729 0.0008284810 0.0006180415 - 0.0017452050 0.0010607000 0.0007899137 - 0.0021349210 0.0013002760 0.0009697909 - 0.0025417069 0.0015479270 0.0011566620 - 0.0029633259 0.0018080650 0.0013511210 - 0.0033958510 0.0020795320 0.0015535100 - 0.0038457969 0.0023645640 0.0017640300 - 0.0043162489 0.0026637421 0.0019820260 - 0.0048077060 0.0029760071 0.0022097649 - 0.0053204661 0.0033004310 0.0024460619 - 0.0058609000 0.0036376549 0.0026919940 - 0.0064359619 0.0039854120 0.0029474010 - 0.0070374971 0.0043447139 0.0032128871 - 0.0076601300 0.0047178762 0.0034874950 - 0.0083078844 0.0051065790 0.0037723500 - 0.0089809727 0.0055108499 0.0040672510 - 0.0096816579 0.0059318668 0.0043718130 - 0.0104107000 0.0063705379 0.0046862401 - 0.0111686802 0.0068254122 0.0050117052 - 0.0119535998 0.0072979759 0.0053493860 - 0.0127734104 0.0077873981 0.0056989561 - 0.0136253396 0.0082948767 0.0060607451 - 0.0145092504 0.0088192495 0.0064340578 - 0.0154185202 0.0093626482 0.0068193851 - 0.0163518004 0.0099276053 0.0072161448 - 0.0173176397 0.0105159301 0.0076254872 - 0.0183163304 0.0111296400 0.0080476142 - 0.0193513595 0.0117682600 0.0084802704 - 0.0204212200 0.0124294702 0.0089247832 - 0.0215315297 0.0131156798 0.0093830954 - 0.0226835497 0.0138275698 0.0098542161 - 0.0238747206 0.0145641901 0.0103377597 - 0.0251002200 0.0153265698 0.0108324597 - 0.0263673309 0.0161136203 0.0113382703 - 0.0276758503 0.0169268306 0.0118559897 - 0.0290231593 0.0177657008 0.0123869004 - 0.0304085892 0.0186326504 0.0129301697 - 0.0318260118 0.0195293408 0.0134853199 - 0.0332819484 0.0204478092 0.0140506905 - 0.0347733498 0.0213932004 0.0146252401 - 0.0363029502 0.0223640501 0.0152128004 - 0.0378724001 0.0233600903 0.0158141498 - 0.0394730903 0.0243795607 0.0164301693 - 0.0411083102 0.0254261997 0.0170600507 - 0.0427853093 0.0264967792 0.0177041702 - 0.0444947183 0.0275935791 0.0183603093 - 0.0462447405 0.0287111904 0.0190260392 - 0.0480214991 0.0298579894 0.0197018404 - 0.0498294383 0.0310345795 0.0203909706 - 0.0516717583 0.0322387293 0.0210961197 - 0.0535553694 0.0334732905 0.0218155701 - 0.0554724000 0.0347375497 0.0225481000 - 0.0574280284 0.0360305011 0.0232938007 - 0.0594230108 0.0373505987 0.0240523200 - 0.0614495687 0.0386989899 0.0248243101 - 0.0635085627 0.0400783718 0.0256102104 - 0.0656058565 0.0414873511 0.0264102798 - 0.0677408203 0.0429296084 0.0272227693 - 0.0699115321 0.0444077104 0.0280466098 - 0.0721197501 0.0459156111 0.0288844202 - 0.0743607506 0.0474466383 0.0297372099 - 0.0766320974 0.0490012988 0.0306023601 - 0.0789411888 0.0505829789 0.0314776190 - 0.0812913403 0.0521959811 0.0323632210 - 0.0836748034 0.0538401082 0.0332610384 - 0.0860934034 0.0555139296 0.0341722593 - 0.0885434672 0.0572180599 0.0350961611 - 0.0910215825 0.0589511804 0.0360324383 - 0.0935282633 0.0607071407 0.0369832404 - 0.0960653424 0.0624810904 0.0379483290 - 0.0986272693 0.0642725229 0.0389264598 - 0.1012184024 0.0660862923 0.0399172194 - 0.1038417965 0.0679221675 0.0409205295 - 0.1064930037 0.0697782189 0.0419377983 - 0.1091663986 0.0716602802 0.0429698490 - 0.1118554026 0.0735708624 0.0440155715 - 0.1145604029 0.0755072236 0.0450732484 - 0.1172852963 0.0774691924 0.0461424887 - 0.1200316027 0.0794569179 0.0472251885 - 0.1228056028 0.0814680681 0.0483251885 - 0.1256061941 0.0835034922 0.0494407713 - 0.1284314990 0.0855652988 0.0505689494 - 0.1312845945 0.0876543671 0.0517092496 - 0.1341657043 0.0897689089 0.0528627299 - 0.1370669007 0.0919097364 0.0540304407 - 0.1399842948 0.0940792933 0.0552122705 - 0.1429264992 0.0962784812 0.0564116687 - 0.1458958983 0.0985052586 0.0576241314 - 0.1488838047 0.1007623002 0.0588485189 - 0.1518885940 0.1030460969 0.0600855909 - 0.1549195051 0.1053560004 0.0613366812 - 0.1579769999 0.1076906994 0.0626053810 - 0.1610527933 0.1100478992 0.0638882369 - 0.1641492993 0.1124247015 0.0651839301 - 0.1672637016 0.1148206964 0.0664921403 - 0.1703891009 0.1172372997 0.0678140298 - 0.1735228002 0.1196703017 0.0691531822 - 0.1766714007 0.1221214011 0.0705029890 - 0.1798343062 0.1245933995 0.0718628988 - 0.1830091029 0.1270965040 0.0732344314 - 0.1861923933 0.1296184957 0.0746224895 - 0.1893824935 0.1321596056 0.0760240182 - 0.1925912052 0.1347205937 0.0774392113 - 0.1958242953 0.1373080015 0.0788744465 - 0.1990808994 0.1399126947 0.0803256184 - 0.2023572028 0.1425279975 0.0817875490 - 0.2056479007 0.1451531053 0.0832600370 - 0.2089466006 0.1477964967 0.0847453102 - 0.2122559994 0.1504597068 0.0862387866 - 0.2155746967 0.1531386971 0.0877406523 - 0.2189022005 0.1558340043 0.0892590731 - 0.2222436965 0.1585476995 0.0907859504 - 0.2255931050 0.1612742990 0.0923209935 - 0.2289551944 0.1640138030 0.0938680470 - 0.2323365957 0.1667792052 0.0954221934 - 0.2357338965 0.1695535034 0.0969835818 - 0.2391352057 0.1723369062 0.0985596627 - 0.2425421029 0.1751406044 0.1001449004 - 0.2459709942 0.1779552996 0.1017419994 - 0.2494048029 0.1807786971 0.1033572033 - 0.2528398931 0.1836172044 0.1049804986 - 0.2562730014 0.1864625067 0.1066166982 - 0.2597048879 0.1893129051 0.1082694978 - 0.2631390095 0.1921741962 0.1099312007 - 0.2665784955 0.1950452030 0.1116095036 - 0.2700111866 0.1979237944 0.1132924035 - 0.2734400928 0.2008170038 0.1149808988 - 0.2768634856 0.2037173063 0.1166793033 - 0.2802909017 0.2066275030 0.1183826998 - 0.2837207019 0.2095531970 0.1201020032 - 0.2871545851 0.2124857008 0.1218283027 - 0.2905927896 0.2154320031 0.1235624999 - 0.2940321863 0.2183827013 0.1253096014 - 0.2974657118 0.2213367969 0.1270615011 - 0.3008908927 0.2242961973 0.1288305074 - 0.3043087125 0.2272599936 0.1306017935 - 0.3077211082 0.2302363962 0.1323782951 - 0.3111295104 0.2332222015 0.1341592073 - 0.3145295084 0.2362163067 0.1359436959 - 0.3179244995 0.2392161936 0.1377421021 - 0.3213168085 0.2422180027 0.1395442933 - 0.3247092962 0.2452265024 0.1413614005 - 0.3281050920 0.2482353002 0.1431804001 - 0.3315083981 0.2512444854 0.1450074017 - 0.3348960876 0.2542502880 0.1468372047 - 0.3382737041 0.2572538853 0.1486722976 - 0.3416486979 0.2602624893 0.1505120993 - 0.3450245857 0.2632722855 0.1523568034 - 0.3484072983 0.2662931085 0.1542067975 - 0.3517858982 0.2693150043 0.1560602039 - 0.3551585078 0.2723414004 0.1579221934 - 0.3585281074 0.2753702104 0.1597875059 - 0.3618960977 0.2784039080 0.1616556942 - 0.3652538955 0.2814370096 0.1635244936 - 0.3686091900 0.2844694853 0.1653988063 - 0.3719412088 0.2874999940 0.1672738940 - 0.3752706051 0.2905291021 0.1691565067 - 0.3785892129 0.2935574949 0.1710398048 - 0.3819032013 0.2965855002 0.1729318947 - 0.3852080107 0.2996020913 0.1748245955 - 0.3885084093 0.3026154935 0.1767162979 - 0.3918038011 0.3056125045 0.1786078960 - 0.3950856030 0.3086062074 0.1804991066 - 0.3983576894 0.3115797043 0.1823901981 - 0.4016107023 0.3145501018 0.1842828989 - 0.4048540890 0.3175120950 0.1861757934 - 0.4080660939 0.3204728961 0.1880692989 - 0.4112665951 0.3234159946 0.1899631023 - 0.4144412875 0.3263562918 0.1918607950 - 0.4176087081 0.3292810023 0.1937602013 - 0.4207499921 0.3322027028 0.1956637949 - 0.4238848984 0.3351123035 0.1975702047 - 0.4270057082 0.3380188048 0.1994725019 - 0.4301230907 0.3409165144 0.2013705969 - 0.4332121015 0.3438107073 0.2032697052 - 0.4362936020 0.3467020988 0.2051707059 - 0.4393599927 0.3495917916 0.2070730031 - 0.4424214065 0.3524695039 0.2089823037 - 0.4454444051 0.3553349078 0.2108915001 - 0.4484499991 0.3581919074 0.2128047943 - 0.4514287114 0.3610335886 0.2147184014 - 0.4543898106 0.3638724089 0.2166287005 - 0.4573354125 0.3666979969 0.2185377926 - 0.4602653980 0.3695237041 0.2204501927 - 0.4631789029 0.3723416924 0.2223664969 - 0.4660640955 0.3751595914 0.2242840976 - 0.4689424038 0.3779619932 0.2262071967 - 0.4717960060 0.3807600141 0.2281304002 - 0.4746480882 0.3835526109 0.2300624996 - 0.4774642885 0.3863407075 0.2319957018 - 0.4802804887 0.3891260922 0.2339314967 - 0.4830631018 0.3919047117 0.2358691990 - 0.4858393073 0.3946832120 0.2378090024 - 0.4886021912 0.3974429071 0.2397564054 - 0.4913567901 0.4002025127 0.2417037040 - 0.4941014051 0.4029656053 0.2436508983 - 0.4968290031 0.4057300985 0.2455980927 - 0.4995543063 0.4084877074 0.2475478053 - 0.5022572875 0.4112327099 0.2495003939 - 0.5049604177 0.4139777124 0.2514531016 - 0.5076419115 0.4167033136 0.2534070015 - 0.5103181005 0.4194287956 0.2553609014 - 0.5129829049 0.4221394062 0.2573103011 - 0.5156350732 0.4248403907 0.2592563927 - 0.5182831287 0.4275377989 0.2612015903 - 0.5209032893 0.4302201867 0.2631373107 - 0.5235236287 0.4329026937 0.2650730908 - 0.5261074901 0.4355578125 0.2670041025 - 0.5286799073 0.4382039905 0.2689318061 - 0.5312321782 0.4408400953 0.2708589137 - 0.5337486863 0.4434512854 0.2727747858 - 0.5362653136 0.4460625947 0.2746906877 - 0.5387454033 0.4486452043 0.2766082883 - 0.5412231088 0.4512208104 0.2785277963 - 0.5436828136 0.4537886977 0.2804473042 - 0.5461235046 0.4563367963 0.2823554873 - 0.5485640168 0.4588850141 0.2842623889 - 0.5509892702 0.4614095986 0.2861675918 - 0.5534147024 0.4639258087 0.2880688012 - 0.5558288097 0.4664359093 0.2899698913 - 0.5582318902 0.4689145088 0.2918665111 - 0.5606350899 0.4713929892 0.2937605083 - 0.5629963875 0.4738500118 0.2956545055 - 0.5653566718 0.4762899876 0.2975474000 - 0.5677015781 0.4787299931 0.2994402945 - 0.5700250864 0.4811362922 0.3013291955 - 0.5723484755 0.4835399985 0.3032068908 - 0.5746430159 0.4859381914 0.3050845861 - 0.5769314766 0.4883219898 0.3069567978 - 0.5792130232 0.4907056987 0.3088240921 - 0.5814688802 0.4930708110 0.3106913865 - 0.5837246776 0.4954228103 0.3125565946 - 0.5859628916 0.4977746904 0.3144212067 - 0.5881881118 0.5000953078 0.3162856996 - 0.5904132128 0.5024111271 0.3181492090 - 0.5926191807 0.5047224760 0.3200125992 - 0.5948230028 0.5070027113 0.3218753934 - 0.5970178246 0.5092828870 0.3237349093 - 0.5991765857 0.5115441084 0.3255943954 - 0.6013354063 0.5137696862 0.3274545968 - 0.6034780741 0.5159952044 0.3293164074 - 0.6056026816 0.5182105899 0.3311783075 - 0.6077274084 0.5204172730 0.3330385983 - 0.6098302007 0.5226240754 0.3348967135 - 0.6119242907 0.5248165131 0.3367548883 - 0.6140184999 0.5270028710 0.3386108875 - 0.6160811782 0.5291892886 0.3404648006 - 0.6181414723 0.5313519239 0.3423185945 - 0.6201986074 0.5335099101 0.3441708982 - 0.6222298145 0.5356678963 0.3460218906 - 0.6242610812 0.5378025174 0.3478729129 - 0.6262831092 0.5399355888 0.3497231901 - 0.6282786727 0.5420684218 0.3515729904 - 0.6302741766 0.5441771150 0.3534227014 - 0.6322615147 0.5462856889 0.3552713096 - 0.6342349052 0.5483934283 0.3571187854 - 0.6362084150 0.5504837036 0.3589664102 - 0.6381633878 0.5525740981 0.3608083129 - 0.6400970817 0.5546634793 0.3626450896 - 0.6420307159 0.5567383170 0.3644818962 - 0.6439446807 0.5588130951 0.3663117886 - 0.6458404064 0.5608872771 0.3681330979 - 0.6477361917 0.5629431009 0.3699544966 - 0.6496070027 0.5649988055 0.3717769086 - 0.6514570713 0.5670546293 0.3736014068 - 0.6533071995 0.5690968037 0.3754259944 - 0.6551311016 0.5711386800 0.3772482872 - 0.6569324732 0.5731804967 0.3790614903 - 0.6587339044 0.5751888156 0.3808746040 - 0.6605200768 0.5771927238 0.3826875985 - 0.6622908711 0.5791965723 0.3844920993 - 0.6640617251 0.5811750889 0.3862965107 - 0.6658166051 0.5831453800 0.3881008923 - 0.6675499082 0.5851157904 0.3898971975 - 0.6692832112 0.5870721936 0.3916915953 - 0.6710019112 0.5890191197 0.3934859037 - 0.6726891994 0.5909659863 0.3952794969 - 0.6743766069 0.5928977728 0.3970726132 - 0.6760562062 0.5948076248 0.3988656104 - 0.6776999831 0.5967172980 0.4006549120 - 0.6793438196 0.5986235142 0.4024339914 - 0.6809874177 0.6005135775 0.4042131007 - 0.6826078892 0.6024037004 0.4059922099 - 0.6842284203 0.6042937040 0.4077546000 - 0.6858488917 0.6061664820 0.4095160961 - 0.6874536872 0.6080377102 0.4112777114 - 0.6890544891 0.6099088788 0.4130330980 - 0.6906551719 0.6117550135 0.4147844911 - 0.6922481060 0.6135854721 0.4165357947 - 0.6938346028 0.6154159904 0.4182860851 - 0.6954210997 0.6172372103 0.4200322032 - 0.6969972849 0.6190348268 0.4217784107 - 0.6985461712 0.6208323240 0.4235245883 - 0.7000951767 0.6226298809 0.4252654016 - 0.7016441226 0.6243864298 0.4270048141 - 0.7031658292 0.6261391044 0.4287441969 - 0.7046864033 0.6278917193 0.4304836094 - 0.7062069774 0.6296277046 0.4322234094 - 0.7077113986 0.6313474774 0.4339630902 - 0.7092053294 0.6330673099 0.4357028008 - 0.7106992006 0.6347842813 0.4374335110 - 0.7121831179 0.6364610791 0.4391623139 - 0.7136318088 0.6381378770 0.4408909976 - 0.7150804996 0.6398146749 0.4426156878 - 0.7165293097 0.6414784789 0.4443300068 - 0.7179604173 0.6431329846 0.4460442066 - 0.7193871140 0.6447873712 0.4477584064 - 0.7208138108 0.6464402080 0.4494678080 - 0.7222303152 0.6480689049 0.4511752129 - 0.7236266732 0.6496976018 0.4528824985 - 0.7250230908 0.6513261795 0.4545893967 - 0.7264196277 0.6529368162 0.4562911987 - 0.7277973294 0.6545298100 0.4579930007 - 0.7291715741 0.6561228037 0.4596947134 - 0.7305458188 0.6577157974 0.4613985121 - 0.7319157720 0.6592859030 0.4631048143 - 0.7332760096 0.6608538032 0.4648110867 - 0.7346363068 0.6624217033 0.4665173888 - 0.7359964848 0.6639863253 0.4682199955 - 0.7373399734 0.6655421257 0.4699214101 - 0.7386773825 0.6670979857 0.4716227055 - 0.7400146723 0.6686539054 0.4733240902 - 0.7413491011 0.6702005863 0.4750083089 - 0.7426620722 0.6717411876 0.4766924977 - 0.7439751029 0.6732819080 0.4783768058 - 0.7452880740 0.6748226285 0.4800589979 - 0.7465865016 0.6763488054 0.4817341864 - 0.7478682995 0.6778733134 0.4834094048 - 0.7491500974 0.6793978214 0.4850845933 - 0.7504318953 0.6809164882 0.4867551029 - 0.7516843081 0.6823992729 0.4884186983 - 0.7529281974 0.6838821173 0.4900822937 - 0.7541720867 0.6853649020 0.4917457998 - 0.7554147243 0.6868361831 0.4934011996 - 0.7566283941 0.6882851720 0.4950498044 - 0.7578421831 0.6897342205 0.4966984093 - 0.7590559721 0.6911832094 0.4983471036 - 0.7602614760 0.6926218867 0.4999813139 - 0.7614449263 0.6940507293 0.5016077161 - 0.7626283765 0.6954795122 0.5032340884 - 0.7638118267 0.6969082952 0.5048604012 - 0.7649738789 0.6983196139 0.5064805746 - 0.7661107183 0.6997205019 0.5080981851 - 0.7672476172 0.7011213899 0.5097157955 - 0.7683845162 0.7025222778 0.5113334060 - 0.7695062757 0.7039136291 0.5129374266 - 0.7706182003 0.7053009868 0.5145360827 - 0.7717301250 0.7066884041 0.5161347985 - 0.7728418708 0.7080758810 0.5177333951 - 0.7739313245 0.7094560266 0.5193111897 - 0.7750114799 0.7108336091 0.5208796263 - 0.7760915756 0.7122113109 0.5224481225 - 0.7771716714 0.7135888934 0.5240164995 - 0.7782340050 0.7149400711 0.5255811214 - 0.7792907953 0.7162821293 0.5271432996 - 0.7803477049 0.7176241279 0.5287054777 - 0.7814046144 0.7189661264 0.5302677155 - 0.7824332118 0.7202813029 0.5318275094 - 0.7834541202 0.7215846181 0.5333846807 - 0.7844750285 0.7228879929 0.5349419713 - 0.7854959965 0.7241913080 0.5364993215 - 0.7865008712 0.7254747152 0.5380507708 - 0.7875007987 0.7267451286 0.5395910144 - 0.7885007858 0.7280154228 0.5411313176 - 0.7895007730 0.7292857170 0.5426715016 - 0.7904886007 0.7305502295 0.5442109704 - 0.7914711237 0.7318078876 0.5457459092 - 0.7924535871 0.7330656052 0.5472807288 - 0.7934361100 0.7343233228 0.5488154888 - 0.7943943143 0.7355695963 0.5503503084 - 0.7953352928 0.7367874980 0.5518705845 - 0.7962762713 0.7380053997 0.5533890128 - 0.7972173095 0.7392233014 0.5549073815 - 0.7981507182 0.7404392958 0.5564258099 - 0.7990738750 0.7416235209 0.5579320788 - 0.7999970913 0.7428076863 0.5594297051 - 0.8009203076 0.7439919710 0.5609273911 - 0.8018379807 0.7451761961 0.5624250174 - 0.8027368188 0.7463406920 0.5639188886 - 0.8036355972 0.7474998236 0.5654007196 - 0.8045343757 0.7486588955 0.5668826103 - 0.8054332137 0.7498180866 0.5683643818 - 0.8063105941 0.7509673834 0.5698462725 - 0.8071876168 0.7521057129 0.5713130832 - 0.8080645800 0.7532439232 0.5727772713 - 0.8089416027 0.7543821931 0.5742415190 - 0.8098158240 0.7555181980 0.5757057071 - 0.8106887937 0.7566338778 0.5771644711 - 0.8115618229 0.7577496767 0.5786156058 - 0.8124346733 0.7588654757 0.5800666213 - 0.8132982850 0.7599812150 0.5815176964 - 0.8141440153 0.7610852718 0.5829687715 - 0.8149898052 0.7621840835 0.5843982100 - 0.8158354759 0.7632827759 0.5858259201 - 0.8166813254 0.7643815875 0.5872535706 - 0.8174951077 0.7654762268 0.5886812210 - 0.8183075190 0.7665566802 0.5901004076 - 0.8191198707 0.7676370740 0.5915061831 - 0.8199322820 0.7687175870 0.5929120183 - 0.8207353950 0.7697979808 0.5943177938 - 0.8215301037 0.7708619833 0.5957236290 - 0.8223248124 0.7719196081 0.5971149802 - 0.8231195211 0.7729771137 0.5985029936 - 0.8239132166 0.7740346193 0.5998910069 - 0.8246840835 0.7750877142 0.6012789011 - 0.8254551291 0.7761170268 0.6026648283 - 0.8262259960 0.7771462798 0.6040399075 - 0.8269969821 0.7781755924 0.6054151058 - 0.8277590871 0.7792049050 0.6067903042 - 0.8285127878 0.7802203894 0.6081655025 - 0.8292664289 0.7812247872 0.6095303893 - 0.8300201297 0.7822293043 0.6108843088 - 0.8307737112 0.7832337022 0.6122382283 - 0.8315144181 0.7842381001 0.6135920882 - 0.8322539926 0.7852112055 0.6149461269 - 0.8329936862 0.7861806750 0.6162794232 - 0.8337333202 0.7871502042 0.6176058054 - 0.8344634175 0.7881196737 0.6189321876 - 0.8351711035 0.7890831828 0.6202585101 - 0.8358789086 0.7900207043 0.6215848923 - 0.8365865946 0.7909581065 0.6228948832 - 0.8372943997 0.7918956280 0.6242042184 - 0.8379939198 0.7928329706 0.6255134940 - 0.8386886120 0.7937645912 0.6268228292 - 0.8393831849 0.7946882248 0.6281303167 - 0.8400778174 0.7956116796 0.6294245124 - 0.8407723904 0.7965353131 0.6307187080 - 0.8414523005 0.7974588871 0.6320130229 - 0.8421304226 0.7983703017 0.6333072186 - 0.8428084850 0.7992740870 0.6345958710 - 0.8434864879 0.8001778126 0.6358658075 - 0.8441630006 0.8010815978 0.6371356845 - 0.8448262811 0.8019853830 0.6384056211 - 0.8454895020 0.8028789163 0.6396754980 - 0.8461527824 0.8037689924 0.6409432292 - 0.8468161225 0.8046591878 0.6422051191 - 0.8474723101 0.8055493236 0.6434670091 - 0.8481097817 0.8064395189 0.6447290182 - 0.8487473130 0.8073065877 0.6459909081 - 0.8493847847 0.8081693053 0.6472480297 - 0.8500223160 0.8090320826 0.6484932899 - 0.8506528139 0.8098948002 0.6497384906 - 0.8512719870 0.8107575178 0.6509838104 - 0.8518912196 0.8115878105 0.6522290111 - 0.8525105119 0.8124132156 0.6534695029 - 0.8531296849 0.8132386804 0.6546944976 - 0.8537434936 0.8140640855 0.6559193730 - 0.8543506861 0.8148896098 0.6571443081 - 0.8549579978 0.8157023191 0.6583693027 - 0.8555651903 0.8165128231 0.6595931053 - 0.8561723828 0.8173233271 0.6608086228 - 0.8567661047 0.8181337714 0.6620242000 - 0.8573439121 0.8189442754 0.6632397771 - 0.8579216003 0.8197408915 0.6644554138 - 0.8584992886 0.8205333948 0.6656709909 - 0.8590769768 0.8213258982 0.6668658853 - 0.8596463799 0.8221184015 0.6680600047 - 0.8602035046 0.8229109049 0.6692541838 - 0.8607606888 0.8236929774 0.6704483032 - 0.8613178134 0.8244690895 0.6716424227 - 0.8618749976 0.8252452016 0.6728152037 - 0.8624271750 0.8260213137 0.6739807129 - 0.8629680276 0.8267974854 0.6751462221 - 0.8635088801 0.8275653720 0.6763116717 - 0.8640497923 0.8283231854 0.6774771810 - 0.8645905852 0.8290811181 0.6786333919 - 0.8651301265 0.8298389912 0.6797791719 - 0.8656617999 0.8305969238 0.6809248924 - 0.8661935925 0.8313513994 0.6820706725 - 0.8667253256 0.8320925832 0.6832165122 - 0.8672569990 0.8328338265 0.6843608022 - 0.8677886724 0.8335750103 0.6854962111 - 0.8683093786 0.8343161941 0.6866315007 - 0.8688293099 0.8350573778 0.6877667904 - 0.8693491817 0.8357766271 0.6889020801 - 0.8698691130 0.8364936113 0.6900374889 - 0.8703891039 0.8372104764 0.6911699772 - 0.8708937764 0.8379275203 0.6923016906 - 0.8713908792 0.8386443853 0.6934332848 - 0.8718879819 0.8393507004 0.6945648789 - 0.8723850846 0.8400480151 0.6956964731 - 0.8728821874 0.8407453895 0.6968225241 - 0.8733705282 0.8414428234 0.6979349852 - 0.8738415837 0.8421400785 0.6990476251 - 0.8743125796 0.8428345919 0.7001600862 - 0.8747836947 0.8435099125 0.7012727261 - 0.8752548099 0.8441851735 0.7023853064 - 0.8757258058 0.8448604941 0.7034785748 - 0.8761900067 0.8455356956 0.7045670152 - 0.8766540289 0.8462110162 0.7056552768 - 0.8771178722 0.8468806148 0.7067435980 - 0.8775817752 0.8475472927 0.7078319192 - 0.8780456781 0.8482139111 0.7089164257 - 0.8785020113 0.8488805294 0.7099874020 - 0.8789511919 0.8495470881 0.7110584974 - 0.8794003725 0.8502116799 0.7121295929 - 0.8798494935 0.8508617282 0.7132006288 - 0.8802986741 0.8515115976 0.7142717242 - 0.8807477951 0.8521615267 0.7153381109 - 0.8811936975 0.8528113961 0.7164012194 - 0.8816397190 0.8534613252 0.7174643278 - 0.8820856214 0.8541015983 0.7185273767 - 0.8825315237 0.8547332287 0.7195906043 - 0.8829774261 0.8553647995 0.7206537127 - 0.8834267855 0.8559963703 0.7216954231 - 0.8838804960 0.8566280007 0.7227349877 - 0.8843340874 0.8572596908 0.7237744927 - 0.8847877979 0.8578745127 0.7248139977 - 0.8852415085 0.8584867120 0.7258535028 - 0.8856950998 0.8590989709 0.7268896103 - 0.8861432076 0.8597112894 0.7279071808 - 0.8865901828 0.8603236079 0.7289246917 - 0.8870372176 0.8609315753 0.7299422026 - 0.8874841928 0.8615155816 0.7309597731 - 0.8879311085 0.8620995879 0.7319772840 - 0.8883765936 0.8626837134 0.7329847217 - 0.8888130188 0.8632677197 0.7339726090 - 0.8892495036 0.8638517857 0.7349603772 - 0.8896859288 0.8644294739 0.7359483242 - 0.8901224136 0.8649967909 0.7369360924 - 0.8905587792 0.8655641079 0.7379239798 - 0.8909912109 0.8661314249 0.7389044762 - 0.8914173841 0.8666986227 0.7398765087 - 0.8918436170 0.8672658801 0.7408484817 - 0.8922697902 0.8678284287 0.7418205738 - 0.8926960230 0.8683868051 0.7427926064 - 0.8931223154 0.8689451218 0.7437645793 - 0.8935400248 0.8695034981 0.7447342873 - 0.8939518929 0.8700618148 0.7457019091 - 0.8943638206 0.8706201911 0.7466694713 - 0.8947756886 0.8711758852 0.7476369739 - 0.8951874971 0.8717300892 0.7486045957 - 0.8955994248 0.8722842932 0.7495722175 - 0.8960080743 0.8728384972 0.7505385876 - 0.8964155912 0.8733927011 0.7515040040 - 0.8968231082 0.8739469051 0.7524694800 - 0.8972306252 0.8744983077 0.7534348965 - 0.8976380229 0.8750482798 0.7544003129 - 0.8980454803 0.8755983114 0.7553657293 - 0.8984466195 0.8761482835 0.7563228011 - 0.8988459110 0.8766983151 0.7572699785 - 0.8992452025 0.8772484064 0.7582172155 - 0.8996444941 0.8777912259 0.7591643929 - 0.9000439048 0.8783296943 0.7601115108 - 0.9004431963 0.8788682222 0.7610586882 - 0.9008390903 0.8794066906 0.7620021105 - 0.9012339711 0.8799452186 0.7629373074 - 0.9016289711 0.8804838061 0.7638723850 - 0.9020239711 0.8810167909 0.7648075819 - 0.9024189711 0.8815448284 0.7657427788 - 0.9028139114 0.8820728064 0.7666779757 - 0.9032065868 0.8826007843 0.7676120996 - 0.9035984278 0.8831288218 0.7685371041 - 0.9039902091 0.8836567998 0.7694622278 - 0.9043819904 0.8841773272 0.7703871727 - 0.9047737718 0.8846845031 0.7713122964 - 0.9051656127 0.8851916790 0.7722373009 - 0.9055474997 0.8856989145 0.7731624246 - 0.9059227109 0.8862062097 0.7740741968 - 0.9062979221 0.8867133856 0.7749837041 - 0.9066730738 0.8872200251 0.7758932710 - 0.9070482850 0.8877230883 0.7768027782 - 0.9074234962 0.8882262111 0.7777122855 - 0.9077919126 0.8887292147 0.7786217928 - 0.9081501961 0.8892322779 0.7795184851 - 0.9085084796 0.8897352815 0.7804030776 - 0.9088668227 0.8902384043 0.7812876105 - 0.9092251062 0.8907387853 0.7821720839 - 0.9095833898 0.8912386894 0.7830566168 - 0.9099410176 0.8917385936 0.7839412093 - 0.9102939963 0.8922384977 0.7848230004 - 0.9106469750 0.8927385211 0.7856823802 - 0.9110000134 0.8932384253 0.7865417004 - 0.9113529921 0.8937345147 0.7874010801 - 0.9117060900 0.8942261934 0.7882605195 - 0.9120591283 0.8947178721 0.7891197801 - 0.9124001861 0.8952096105 0.7899792194 - 0.9127386212 0.8957012892 0.7908260822 - 0.9130769968 0.8961930871 0.7916657925 - 0.9134153724 0.8966842890 0.7925053835 - 0.9137538075 0.8971561790 0.7933450937 - 0.9140921831 0.8976281881 0.7941846848 - 0.9144207835 0.8981001973 0.7950243950 - 0.9147356153 0.8985722065 0.7958624959 - 0.9150503874 0.8990442157 0.7966837883 - 0.9153652191 0.8995161057 0.7975050807 - 0.9156799912 0.8999810219 0.7983263731 - 0.9159948230 0.9004393816 0.7991477847 - 0.9163095951 0.9008978009 0.7999690771 - 0.9166138768 0.9013561010 0.8007904291 - 0.9169176221 0.9018145204 0.8015990257 - 0.9172213078 0.9022728205 0.8023931980 - 0.9175249934 0.9027311802 0.8031873107 - 0.9178286791 0.9031661749 0.8039814830 - 0.9181324244 0.9035997987 0.8047757149 - 0.9184330106 0.9040333033 0.8055698872 - 0.9187291861 0.9044669271 0.8063641191 - 0.9190254211 0.9049004912 0.8071421981 - 0.9193214774 0.9053339958 0.8079159856 - 0.9196177125 0.9057644010 0.8086897135 - 0.9199138284 0.9061868787 0.8094635010 - 0.9202100039 0.9066092968 0.8102372289 - 0.9205018878 0.9070317745 0.8110108972 - 0.9207926989 0.9074543118 0.8117843866 - 0.9210835099 0.9078767896 0.8125452995 - 0.9213743210 0.9082993269 0.8133062124 - 0.9216650724 0.9087104201 0.8140671253 - 0.9219558835 0.9091129899 0.8148279786 - 0.9222459793 0.9095156193 0.8155888915 - 0.9225308895 0.9099181890 0.8163498044 - 0.9228156805 0.9103208184 0.8171091080 - 0.9231005907 0.9107235074 0.8178617954 - 0.9233853817 0.9111260772 0.8186144233 - 0.9236702919 0.9115067720 0.8193669915 - 0.9239550829 0.9118813276 0.8201196790 - 0.9242395163 0.9122558832 0.8208723068 - 0.9245231748 0.9126303792 0.8216248751 - 0.9248068929 0.9130049944 0.8223708868 - 0.9250906110 0.9133794904 0.8230999112 - 0.9253743291 0.9137541056 0.8238288164 - 0.9256579280 0.9141280055 0.8245577812 - 0.9259415865 0.9145019054 0.8252866864 - 0.9262230992 0.9148758054 0.8260157108 - 0.9265028238 0.9152497053 0.8267446160 - 0.9267824888 0.9156236053 0.8274663091 - 0.9270622134 0.9159975052 0.8281710148 - 0.9273418784 0.9163714051 0.8288756013 - 0.9276214838 0.9167358279 0.8295801878 - 0.9279012084 0.9171000123 0.8302847743 - 0.9281796813 0.9174641967 0.8309894204 - 0.9284574986 0.9178283811 0.8316941261 - 0.9287353158 0.9181925058 0.8323919773 - 0.9290130734 0.9185566902 0.8330684900 - 0.9292908907 0.9189208746 0.8337450027 - 0.9295685887 0.9192627072 0.8344215155 - 0.9298464060 0.9196035266 0.8350980878 - 0.9301217794 0.9199442863 0.8357746005 - 0.9303957820 0.9202849865 0.8364511132 - 0.9306699038 0.9206258059 0.8371245265 - 0.9309440255 0.9209665060 0.8377707005 - 0.9312180877 0.9213073254 0.8384168744 - 0.9314922094 0.9216343164 0.8390631080 - 0.9317662716 0.9219589829 0.8397092819 - 0.9320312738 0.9222837090 0.8403555155 - 0.9322906733 0.9226084948 0.8410016894 - 0.9325500727 0.9229332209 0.8416479230 - 0.9328094721 0.9232580066 0.8422836065 - 0.9330688119 0.9235826731 0.8429179788 - 0.9333282113 0.9238941073 0.8435524106 - 0.9335876107 0.9241997004 0.8441867828 - 0.9338458180 0.9245051742 0.8448212147 - 0.9341027141 0.9248107076 0.8454555869 - 0.9343596101 0.9251161814 0.8460900187 - 0.9346163869 0.9254217148 0.8467177749 - 0.9348732829 0.9257271886 0.8473411202 - 0.9351301789 0.9260296226 0.8479645252 - 0.9353870749 0.9263281822 0.8485878706 - 0.9356436729 0.9266266823 0.8492112160 - 0.9358993769 0.9269253016 0.8498346210 - 0.9361552000 0.9272238016 0.8504580259 - 0.9364109039 0.9275224209 0.8510761857 - 0.9366667271 0.9278209209 0.8516775966 - 0.9369223714 0.9281198978 0.8522790074 - 0.9371781945 0.9284216762 0.8528804183 - 0.9374338984 0.9287235737 0.8534818292 - 0.9376984239 0.9290254116 0.8540831804 - 0.9379627705 0.9293273091 0.8546845913 - 0.9382272959 0.9296290874 0.8552860022 - 0.9384917021 0.9299309850 0.8558799028 - 0.9387561083 0.9302328229 0.8564717770 - 0.9390205741 0.9305325747 0.8570637703 - 0.9392849803 0.9308316112 0.8576557040 - 0.9395543933 0.9311305285 0.8582475781 - 0.9398267269 0.9314295053 0.8588395119 - 0.9400988817 0.9317284822 0.8594313860 - 0.9403710961 0.9320273995 0.8600232005 - 0.9406434298 0.9323263764 0.8606147766 - 0.9409155846 0.9326242208 0.8612062931 - 0.9411879182 0.9329177737 0.8617979288 - 0.9414601922 0.9332113862 0.8623893857 - 0.9417330027 0.9335049987 0.8629810214 - 0.9420056939 0.9337986112 0.8635724783 - 0.9422785044 0.9340922236 0.8641641140 - 0.9425511956 0.9343857765 0.8647502065 - 0.9428240061 0.9346793890 0.8653333783 - 0.9430966973 0.9349703789 0.8659164906 - 0.9433695078 0.9352599978 0.8664996028 - 0.9436365962 0.9355496168 0.8670827150 - 0.9439011216 0.9358391762 0.8676658273 - 0.9441655278 0.9361287951 0.8682489991 - 0.9444299936 0.9364184141 0.8688321114 - 0.9446943998 0.9367079735 0.8694059253 - 0.9449589252 0.9369975924 0.8699793220 - 0.9452232718 0.9372946024 0.8705525994 - 0.9454872012 0.9375914931 0.8711258769 - 0.9457437992 0.9378883839 0.8716992736 - 0.9460003972 0.9381853938 0.8722726107 - 0.9462569952 0.9384822845 0.8728458881 - 0.9465134740 0.9387792945 0.8734164834 - 0.9467700720 0.9390761852 0.8739734888 - 0.9470267296 0.9393727779 0.8745303750 - 0.9472833276 0.9396685958 0.8750873804 - 0.9475367069 0.9399644136 0.8756443858 - 0.9477851987 0.9402601719 0.8762012720 - 0.9480336905 0.9405561090 0.8767582774 - 0.9482821822 0.9408519268 0.8773152828 - 0.9485306740 0.9411476851 0.8778693080 - 0.9487792253 0.9414435029 0.8784165978 - 0.9490277171 0.9417338967 0.8789640069 - 0.9492762089 0.9420179129 0.8795112967 - 0.9495244026 0.9423019886 0.8800587058 - 0.9497724175 0.9425861239 0.8806061149 - 0.9500203729 0.9428701997 0.8811534047 - 0.9502683878 0.9431542754 0.8817008138 - 0.9505164027 0.9434382915 0.8822470903 - 0.9507644176 0.9437224269 0.8827915788 - 0.9510124922 0.9440007806 0.8833361864 - 0.9512605071 0.9442747831 0.8838806748 - 0.9515028000 0.9445487857 0.8844252229 - 0.9517431855 0.9448227882 0.8849697113 - 0.9519836903 0.9450967908 0.8855141997 - 0.9522241950 0.9453707933 0.8860588074 - 0.9524646997 0.9456447959 0.8865966797 - 0.9527052045 0.9459187984 0.8871210217 - 0.9529457092 0.9461904168 0.8876453042 - 0.9531862140 0.9464601874 0.8881695867 - 0.9534236193 0.9467300177 0.8886939883 - 0.9536604285 0.9469999075 0.8892182708 - 0.9538971782 0.9472696781 0.8897426128 - 0.9541339278 0.9475396276 0.8902668953 - 0.9543706775 0.9478093982 0.8907887936 - 0.9546074867 0.9480792284 0.8913012743 - 0.9548442960 0.9483507872 0.8918138146 - 0.9550809860 0.9486241937 0.8923262954 - 0.9553158283 0.9488974810 0.8928387761 - 0.9555500746 0.9491708279 0.8933513761 - 0.9557843804 0.9494441152 0.8938639164 - 0.9560186863 0.9497175217 0.8943763971 - 0.9562528729 0.9499908090 0.8948888779 - 0.9564871788 0.9502640963 0.8953927159 - 0.9567214847 0.9505357742 0.8958964944 - 0.9569557905 0.9508042932 0.8964002132 - 0.9571847916 0.9510728717 0.8969039917 - 0.9574114084 0.9513413906 0.8974078298 - 0.9576380253 0.9516100287 0.8979116082 - 0.9578645825 0.9518784881 0.8984153271 - 0.9580911994 0.9521470070 0.8989191055 - 0.9583178759 0.9524155855 0.8994185925 - 0.9585444927 0.9526826739 0.8999162912 - 0.9587711096 0.9529364109 0.9004138708 - 0.9589902163 0.9531900287 0.9009116292 - 0.9592015147 0.9534437060 0.9014092088 - 0.9594126940 0.9536973238 0.9019069076 - 0.9596238732 0.9539508820 0.9024044871 - 0.9598351121 0.9542046189 0.9029021859 - 0.9600464106 0.9544581771 0.9033970833 - 0.9602575898 0.9547119141 0.9038860798 - 0.9604688287 0.9549633861 0.9043750167 - 0.9606760144 0.9552143216 0.9048638940 - 0.9608663917 0.9554650784 0.9053527713 - 0.9610567093 0.9557158947 0.9058417082 - 0.9612470865 0.9559667706 0.9063305855 - 0.9614375234 0.9562175870 0.9068195224 - 0.9616279006 0.9564685225 0.9073083997 - 0.9618182778 0.9567192793 0.9077844024 - 0.9620087147 0.9569705129 0.9082577825 - 0.9621990919 0.9572222233 0.9087312222 - 0.9623833895 0.9574738741 0.9092046022 - 0.9625663757 0.9577257037 0.9096779823 - 0.9627494216 0.9579774141 0.9101514220 - 0.9629324079 0.9582291245 0.9106246829 - 0.9631155133 0.9584808946 0.9110981226 - 0.9632984996 0.9587326050 0.9115682244 - 0.9634814858 0.9589843154 0.9120290875 - 0.9636644721 0.9592264891 0.9124898911 - 0.9638484120 0.9594662786 0.9129506946 - 0.9640339017 0.9597061872 0.9134116173 - 0.9642195106 0.9599459767 0.9138724208 - 0.9644050002 0.9601858258 0.9143332243 - 0.9645904899 0.9604256749 0.9147940278 - 0.9647760987 0.9606655240 0.9152548909 - 0.9649615884 0.9609053731 0.9157108068 - 0.9651471972 0.9611442089 0.9161633849 - 0.9653326869 0.9613788724 0.9166160226 - 0.9655120969 0.9616137147 0.9170686007 - 0.9656897783 0.9618483782 0.9175212979 - 0.9658675194 0.9620831013 0.9179738760 - 0.9660452008 0.9623178840 0.9184265137 - 0.9662228227 0.9625526071 0.9188790917 - 0.9664005041 0.9627872705 0.9193317294 - 0.9665781856 0.9630219936 0.9197779894 - 0.9667559266 0.9632468820 0.9202229977 - 0.9669327736 0.9634612799 0.9206681252 - 0.9671043158 0.9636756778 0.9211130738 - 0.9672759175 0.9638900757 0.9215580821 - 0.9674474001 0.9641044736 0.9220032096 - 0.9676188827 0.9643189907 0.9224482179 - 0.9677904844 0.9645333886 0.9228932858 - 0.9679620266 0.9647477865 0.9233382940 - 0.9681335092 0.9649621844 0.9237800241 - 0.9683051109 0.9651690125 0.9242216945 - 0.9684718251 0.9653723240 0.9246633053 - 0.9686310291 0.9655755758 0.9251049757 - 0.9687901139 0.9657788873 0.9255465865 - 0.9689491987 0.9659821987 0.9259883165 - 0.9691084027 0.9661855102 0.9264299273 - 0.9692674875 0.9663888216 0.9268715978 - 0.9694265723 0.9665920138 0.9273126125 - 0.9695857763 0.9667953253 0.9277383089 - 0.9697449207 0.9669919014 0.9281641245 - 0.9699026942 0.9671866894 0.9285898209 - 0.9700592756 0.9673815966 0.9290155172 - 0.9702159166 0.9675763845 0.9294412136 - 0.9703724980 0.9677711725 0.9298669100 - 0.9705290794 0.9679660201 0.9302927256 - 0.9706857204 0.9681608081 0.9307184219 - 0.9708423018 0.9683555961 0.9311441183 - 0.9709988832 0.9685503840 0.9315564036 - 0.9711555243 0.9687359929 0.9319686294 - 0.9713115096 0.9689192772 0.9323807955 - 0.9714668989 0.9691025019 0.9327929020 - 0.9716224074 0.9692857862 0.9332051277 - 0.9717779160 0.9694690108 0.9336172938 - 0.9719334245 0.9696522951 0.9340295196 - 0.9720888138 0.9698355198 0.9344416857 - 0.9722443223 0.9700188041 0.9348539114 - 0.9723997712 0.9702020288 0.9352610111 - 0.9725552797 0.9703844786 0.9356672168 - 0.9727123976 0.9705666900 0.9360734224 - 0.9728708863 0.9707489014 0.9364796281 - 0.9730293751 0.9709311128 0.9368857741 - 0.9731879234 0.9711133242 0.9372920990 - 0.9733464122 0.9712954164 0.9376983047 - 0.9735049009 0.9714776278 0.9381045103 - 0.9736633897 0.9716597795 0.9385107160 - 0.9738218784 0.9718419909 0.9389162064 - 0.9739804268 0.9720212221 0.9393213987 - 0.9741386175 0.9721977711 0.9397264719 - 0.9742962122 0.9723743796 0.9401317239 - 0.9744538069 0.9725509882 0.9405367970 - 0.9746114016 0.9727275968 0.9409419894 - 0.9747691154 0.9729040861 0.9413471222 - 0.9749267101 0.9730806947 0.9417521954 - 0.9750843048 0.9732573032 0.9421573877 - 0.9752420187 0.9734339118 0.9425604939 - 0.9753996134 0.9736099839 0.9429581165 - 0.9755573869 0.9737846851 0.9433556795 - 0.9757156968 0.9739593267 0.9437533021 - 0.9758740067 0.9741340280 0.9441508055 - 0.9760323763 0.9743086100 0.9445484281 - 0.9761906862 0.9744831920 0.9449459910 - 0.9763489962 0.9746578932 0.9453436136 - 0.9765074253 0.9748324752 0.9457411766 - 0.9766656756 0.9750071764 0.9461387992 - 0.9768239856 0.9751818180 0.9465218186 - 0.9769824147 0.9753580093 0.9469017982 - 0.9771348834 0.9755343795 0.9472817183 - 0.9772865176 0.9757108092 0.9476615787 - 0.9774382114 0.9758871794 0.9480414987 - 0.9775897861 0.9760636091 0.9484214187 - 0.9777414203 0.9762399793 0.9488013983 - 0.9778929949 0.9764164090 0.9491813183 - 0.9780446887 0.9765927792 0.9495611787 - 0.9781963229 0.9767692089 0.9499387145 - 0.9783478975 0.9769436717 0.9503098726 - 0.9785013795 0.9771149158 0.9506810904 - 0.9786573052 0.9772862196 0.9510523081 - 0.9788131118 0.9774574041 0.9514235258 - 0.9789689779 0.9776285887 0.9517948031 - 0.9791249037 0.9777997732 0.9521660209 - 0.9792807102 0.9779710174 0.9525371790 - 0.9794365764 0.9781423211 0.9529083967 - 0.9795925021 0.9783135056 0.9532796144 - 0.9797484279 0.9784846902 0.9536480904 - 0.9799041748 0.9786469936 0.9540150762 - 0.9800559878 0.9788064957 0.9543820024 - 0.9802073240 0.9789659977 0.9547489882 - 0.9803586006 0.9791253805 0.9551159143 - 0.9805098772 0.9792848825 0.9554827809 - 0.9806612730 0.9794443846 0.9558498263 - 0.9808126092 0.9796038866 0.9562166929 - 0.9809638858 0.9797633886 0.9565836787 - 0.9811152220 0.9799227715 0.9569506049 - 0.9812664986 0.9800817966 0.9573094845 - 0.9814171195 0.9802325964 0.9576671720 - 0.9815658927 0.9803833961 0.9580247998 - 0.9817147255 0.9805341959 0.9583824873 - 0.9818633795 0.9806848764 0.9587401748 - 0.9820122123 0.9808356762 0.9590978026 - 0.9821609259 0.9809864759 0.9594554901 - 0.9823096991 0.9811372757 0.9598131776 - 0.9824584126 0.9812880754 0.9601708055 - 0.9826071858 0.9814388752 0.9605284929 - 0.9827558994 0.9815893173 0.9608864188 - 0.9829031229 0.9817385077 0.9612444043 - 0.9830487967 0.9818876982 0.9616022706 - 0.9831944108 0.9820368886 0.9619601965 - 0.9833400846 0.9821860790 0.9623181820 - 0.9834856987 0.9823353291 0.9626761079 - 0.9836313725 0.9824845195 0.9630339742 - 0.9837771058 0.9826337099 0.9633920193 - 0.9839227200 0.9827829003 0.9637498856 - 0.9840683937 0.9829320908 0.9641076922 - 0.9842140079 0.9830825925 0.9644622207 - 0.9843599200 0.9832354188 0.9648168087 - 0.9845057726 0.9833881855 0.9651712775 - 0.9846516848 0.9835410118 0.9655258060 - 0.9847975969 0.9836937785 0.9658802748 - 0.9849435091 0.9838467240 0.9662348032 - 0.9850894213 0.9839994907 0.9665892720 - 0.9852352738 0.9841523170 0.9669439197 - 0.9853813052 0.9843050838 0.9672983885 - 0.9855272174 0.9844579101 0.9676529169 - 0.9856731296 0.9846124053 0.9679955244 - 0.9858202934 0.9847701192 0.9683375955 - 0.9859679937 0.9849278927 0.9686797857 - 0.9861156940 0.9850856066 0.9690219164 - 0.9862633944 0.9852433801 0.9693641067 - 0.9864110947 0.9854010940 0.9697061777 - 0.9865589142 0.9855589271 0.9700484276 - 0.9867066145 0.9857165813 0.9703904986 - 0.9868543148 0.9858744144 0.9707326889 - 0.9870020151 0.9860321283 0.9710748196 - 0.9871497154 0.9861894846 0.9714078903 - 0.9872943163 0.9863452911 0.9717383981 - 0.9874371886 0.9865010977 0.9720689058 - 0.9875801206 0.9866569042 0.9723994136 - 0.9877228737 0.9868127704 0.9727299213 - 0.9878658056 0.9869685769 0.9730604291 - 0.9880086780 0.9871243834 0.9733908772 - 0.9881514907 0.9872803092 0.9737213850 - 0.9882944226 0.9874361157 0.9740518928 - 0.9884372950 0.9875919223 0.9743824005 - 0.9885801077 0.9877477288 0.9747108221 - 0.9887198210 0.9878988266 0.9750369191 - 0.9888557196 0.9880496860 0.9753628969 - 0.9889916182 0.9882006049 0.9756889939 - 0.9891275167 0.9883515239 0.9760150909 - 0.9892634749 0.9885023832 0.9763411880 - 0.9893993735 0.9886533022 0.9766672254 - 0.9895352721 0.9888042212 0.9769933224 - 0.9896711707 0.9889550805 0.9773194194 - 0.9898071885 0.9891059995 0.9776453972 - 0.9899430871 0.9892569184 0.9779708982 - 0.9900798202 0.9894033074 0.9782887101 - 0.9902198911 0.9895470738 0.9786065817 - 0.9903600216 0.9896908998 0.9789245129 - 0.9905000925 0.9898347259 0.9792423248 - 0.9906402826 0.9899784923 0.9795601964 - 0.9907804132 0.9901223183 0.9798781276 - 0.9909204841 0.9902660847 0.9801958799 - 0.9910606146 0.9904099107 0.9805138111 - 0.9912006855 0.9905536771 0.9808316827 - 0.9913408756 0.9906975031 0.9811494946 - 0.9914810061 0.9908416271 0.9814661145 - 0.9916189909 0.9909868836 0.9817817807 - 0.9917564988 0.9911323190 0.9820973873 - 0.9918940067 0.9912775755 0.9824131131 - 0.9920315146 0.9914230108 0.9827287197 - 0.9921690226 0.9915683270 0.9830443263 - 0.9923064709 0.9917137027 0.9833599925 - 0.9924440980 0.9918590188 0.9836755991 - 0.9925816059 0.9920043945 0.9839913249 - 0.9927191138 0.9921497107 0.9843068719 - 0.9928566217 0.9922950268 0.9846224785 - 0.9929937124 0.9924386144 0.9849327803 - 0.9931297898 0.9925811291 0.9852427244 - 0.9932658076 0.9927235842 0.9855526090 - 0.9934018850 0.9928659797 0.9858624935 - 0.9935380220 0.9930084944 0.9861723781 - 0.9936740994 0.9931508899 0.9864822030 - 0.9938101768 0.9932934046 0.9867920876 - 0.9939463139 0.9934358001 0.9871019721 - 0.9940823913 0.9935783148 0.9874119163 - 0.9942185283 0.9937207103 0.9877216816 - 0.9943544865 0.9938632250 0.9880297184 - 0.9944881201 0.9940040112 0.9883286953 - 0.9946203232 0.9941446781 0.9886276722 - 0.9947524071 0.9942854047 0.9889267087 - 0.9948846102 0.9944260716 0.9892256856 - 0.9950168133 0.9945669174 0.9895246029 - 0.9951490164 0.9947075844 0.9898235798 - 0.9952812195 0.9948483109 0.9901226163 - 0.9954133034 0.9949889779 0.9904215932 - 0.9955455065 0.9951297045 0.9907206297 - 0.9956777096 0.9952703714 0.9910196066 - 0.9958099127 0.9954108000 0.9913132787 - 0.9959365129 0.9955487251 0.9915956259 - 0.9960625768 0.9956865907 0.9918777943 - 0.9961885810 0.9958245158 0.9921600223 - 0.9963147044 0.9959623814 0.9924421906 - 0.9964408278 0.9961003065 0.9927244186 - 0.9965668917 0.9962381721 0.9930065870 - 0.9966928959 0.9963760972 0.9932888150 - 0.9968190193 0.9965140224 0.9935709834 - 0.9969450831 0.9966518879 0.9938532114 - 0.9970712066 0.9967898130 0.9941353798 - 0.9971972108 0.9969279766 0.9944099784 - 0.9973220825 0.9970669746 0.9946690202 - 0.9974470139 0.9972059131 0.9949278831 - 0.9975720048 0.9973449111 0.9951869249 - 0.9976968765 0.9974839091 0.9954459071 - 0.9978219271 0.9976229072 0.9957048297 - 0.9979467988 0.9977619052 0.9959638119 - 0.9980716705 0.9979009032 0.9962226748 - 0.9981967211 0.9980399013 0.9964817166 - 0.9983215928 0.9981788993 0.9967406988 - 0.9984465241 0.9983178973 0.9969996214 - 0.9985718727 0.9984570742 0.9972563982 - 0.9987018108 0.9985973835 0.9975059032 - 0.9988316298 0.9987375736 0.9977552891 - 0.9989613891 0.9988778830 0.9980046749 - 0.9990912080 0.9990180731 0.9982541203 - 0.9992210865 0.9991583824 0.9985035062 - 0.9993509054 0.9992986917 0.9987528920 - 0.9994807243 0.9994388819 0.9990022779 - 0.9996104836 0.9995791912 0.9992517829 - 0.9997404218 0.9997195005 0.9995012283 - 0.9998701811 0.9998596907 0.9997506142 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0003177276 0.0001941380 0.0001446141 + 0.0006504654 0.0003951061 0.0002949580 + 0.0010024470 0.0006061521 0.0004526987 + 0.0013694729 0.0008284810 0.0006180415 + 0.0017452050 0.0010607000 0.0007899137 + 0.0021349210 0.0013002760 0.0009697909 + 0.0025417069 0.0015479270 0.0011566620 + 0.0029633259 0.0018080650 0.0013511210 + 0.0033958510 0.0020795320 0.0015535100 + 0.0038457969 0.0023645640 0.0017640300 + 0.0043162489 0.0026637421 0.0019820260 + 0.0048077060 0.0029760071 0.0022097649 + 0.0053204661 0.0033004310 0.0024460619 + 0.0058609000 0.0036376549 0.0026919940 + 0.0064359619 0.0039854120 0.0029474010 + 0.0070374971 0.0043447139 0.0032128871 + 0.0076601300 0.0047178762 0.0034874950 + 0.0083078844 0.0051065790 0.0037723500 + 0.0089809727 0.0055108499 0.0040672510 + 0.0096816579 0.0059318668 0.0043718130 + 0.0104107000 0.0063705379 0.0046862401 + 0.0111686802 0.0068254122 0.0050117052 + 0.0119535998 0.0072979759 0.0053493860 + 0.0127734104 0.0077873981 0.0056989561 + 0.0136253396 0.0082948767 0.0060607451 + 0.0145092504 0.0088192495 0.0064340578 + 0.0154185202 0.0093626482 0.0068193851 + 0.0163518004 0.0099276053 0.0072161448 + 0.0173176397 0.0105159301 0.0076254872 + 0.0183163304 0.0111296400 0.0080476142 + 0.0193513595 0.0117682600 0.0084802704 + 0.0204212200 0.0124294702 0.0089247832 + 0.0215315297 0.0131156798 0.0093830954 + 0.0226835497 0.0138275698 0.0098542161 + 0.0238747206 0.0145641901 0.0103377597 + 0.0251002200 0.0153265698 0.0108324597 + 0.0263673309 0.0161136203 0.0113382703 + 0.0276758503 0.0169268306 0.0118559897 + 0.0290231593 0.0177657008 0.0123869004 + 0.0304085892 0.0186326504 0.0129301697 + 0.0318260118 0.0195293408 0.0134853199 + 0.0332819484 0.0204478092 0.0140506905 + 0.0347733498 0.0213932004 0.0146252401 + 0.0363029502 0.0223640501 0.0152128004 + 0.0378724001 0.0233600903 0.0158141498 + 0.0394730903 0.0243795607 0.0164301693 + 0.0411083102 0.0254261997 0.0170600507 + 0.0427853093 0.0264967792 0.0177041702 + 0.0444947183 0.0275935791 0.0183603093 + 0.0462447405 0.0287111904 0.0190260392 + 0.0480214991 0.0298579894 0.0197018404 + 0.0498294383 0.0310345795 0.0203909706 + 0.0516717583 0.0322387293 0.0210961197 + 0.0535553694 0.0334732905 0.0218155701 + 0.0554724000 0.0347375497 0.0225481000 + 0.0574280284 0.0360305011 0.0232938007 + 0.0594230108 0.0373505987 0.0240523200 + 0.0614495687 0.0386989899 0.0248243101 + 0.0635085627 0.0400783718 0.0256102104 + 0.0656058565 0.0414873511 0.0264102798 + 0.0677408203 0.0429296084 0.0272227693 + 0.0699115321 0.0444077104 0.0280466098 + 0.0721197501 0.0459156111 0.0288844202 + 0.0743607506 0.0474466383 0.0297372099 + 0.0766320974 0.0490012988 0.0306023601 + 0.0789411888 0.0505829789 0.0314776190 + 0.0812913403 0.0521959811 0.0323632210 + 0.0836748034 0.0538401082 0.0332610384 + 0.0860934034 0.0555139296 0.0341722593 + 0.0885434672 0.0572180599 0.0350961611 + 0.0910215825 0.0589511804 0.0360324383 + 0.0935282633 0.0607071407 0.0369832404 + 0.0960653424 0.0624810904 0.0379483290 + 0.0986272693 0.0642725229 0.0389264598 + 0.1012184024 0.0660862923 0.0399172194 + 0.1038417965 0.0679221675 0.0409205295 + 0.1064930037 0.0697782189 0.0419377983 + 0.1091663986 0.0716602802 0.0429698490 + 0.1118554026 0.0735708624 0.0440155715 + 0.1145604029 0.0755072236 0.0450732484 + 0.1172852963 0.0774691924 0.0461424887 + 0.1200316027 0.0794569179 0.0472251885 + 0.1228056028 0.0814680681 0.0483251885 + 0.1256061941 0.0835034922 0.0494407713 + 0.1284314990 0.0855652988 0.0505689494 + 0.1312845945 0.0876543671 0.0517092496 + 0.1341657043 0.0897689089 0.0528627299 + 0.1370669007 0.0919097364 0.0540304407 + 0.1399842948 0.0940792933 0.0552122705 + 0.1429264992 0.0962784812 0.0564116687 + 0.1458958983 0.0985052586 0.0576241314 + 0.1488838047 0.1007623002 0.0588485189 + 0.1518885940 0.1030460969 0.0600855909 + 0.1549195051 0.1053560004 0.0613366812 + 0.1579769999 0.1076906994 0.0626053810 + 0.1610527933 0.1100478992 0.0638882369 + 0.1641492993 0.1124247015 0.0651839301 + 0.1672637016 0.1148206964 0.0664921403 + 0.1703891009 0.1172372997 0.0678140298 + 0.1735228002 0.1196703017 0.0691531822 + 0.1766714007 0.1221214011 0.0705029890 + 0.1798343062 0.1245933995 0.0718628988 + 0.1830091029 0.1270965040 0.0732344314 + 0.1861923933 0.1296184957 0.0746224895 + 0.1893824935 0.1321596056 0.0760240182 + 0.1925912052 0.1347205937 0.0774392113 + 0.1958242953 0.1373080015 0.0788744465 + 0.1990808994 0.1399126947 0.0803256184 + 0.2023572028 0.1425279975 0.0817875490 + 0.2056479007 0.1451531053 0.0832600370 + 0.2089466006 0.1477964967 0.0847453102 + 0.2122559994 0.1504597068 0.0862387866 + 0.2155746967 0.1531386971 0.0877406523 + 0.2189022005 0.1558340043 0.0892590731 + 0.2222436965 0.1585476995 0.0907859504 + 0.2255931050 0.1612742990 0.0923209935 + 0.2289551944 0.1640138030 0.0938680470 + 0.2323365957 0.1667792052 0.0954221934 + 0.2357338965 0.1695535034 0.0969835818 + 0.2391352057 0.1723369062 0.0985596627 + 0.2425421029 0.1751406044 0.1001449004 + 0.2459709942 0.1779552996 0.1017419994 + 0.2494048029 0.1807786971 0.1033572033 + 0.2528398931 0.1836172044 0.1049804986 + 0.2562730014 0.1864625067 0.1066166982 + 0.2597048879 0.1893129051 0.1082694978 + 0.2631390095 0.1921741962 0.1099312007 + 0.2665784955 0.1950452030 0.1116095036 + 0.2700111866 0.1979237944 0.1132924035 + 0.2734400928 0.2008170038 0.1149808988 + 0.2768634856 0.2037173063 0.1166793033 + 0.2802909017 0.2066275030 0.1183826998 + 0.2837207019 0.2095531970 0.1201020032 + 0.2871545851 0.2124857008 0.1218283027 + 0.2905927896 0.2154320031 0.1235624999 + 0.2940321863 0.2183827013 0.1253096014 + 0.2974657118 0.2213367969 0.1270615011 + 0.3008908927 0.2242961973 0.1288305074 + 0.3043087125 0.2272599936 0.1306017935 + 0.3077211082 0.2302363962 0.1323782951 + 0.3111295104 0.2332222015 0.1341592073 + 0.3145295084 0.2362163067 0.1359436959 + 0.3179244995 0.2392161936 0.1377421021 + 0.3213168085 0.2422180027 0.1395442933 + 0.3247092962 0.2452265024 0.1413614005 + 0.3281050920 0.2482353002 0.1431804001 + 0.3315083981 0.2512444854 0.1450074017 + 0.3348960876 0.2542502880 0.1468372047 + 0.3382737041 0.2572538853 0.1486722976 + 0.3416486979 0.2602624893 0.1505120993 + 0.3450245857 0.2632722855 0.1523568034 + 0.3484072983 0.2662931085 0.1542067975 + 0.3517858982 0.2693150043 0.1560602039 + 0.3551585078 0.2723414004 0.1579221934 + 0.3585281074 0.2753702104 0.1597875059 + 0.3618960977 0.2784039080 0.1616556942 + 0.3652538955 0.2814370096 0.1635244936 + 0.3686091900 0.2844694853 0.1653988063 + 0.3719412088 0.2874999940 0.1672738940 + 0.3752706051 0.2905291021 0.1691565067 + 0.3785892129 0.2935574949 0.1710398048 + 0.3819032013 0.2965855002 0.1729318947 + 0.3852080107 0.2996020913 0.1748245955 + 0.3885084093 0.3026154935 0.1767162979 + 0.3918038011 0.3056125045 0.1786078960 + 0.3950856030 0.3086062074 0.1804991066 + 0.3983576894 0.3115797043 0.1823901981 + 0.4016107023 0.3145501018 0.1842828989 + 0.4048540890 0.3175120950 0.1861757934 + 0.4080660939 0.3204728961 0.1880692989 + 0.4112665951 0.3234159946 0.1899631023 + 0.4144412875 0.3263562918 0.1918607950 + 0.4176087081 0.3292810023 0.1937602013 + 0.4207499921 0.3322027028 0.1956637949 + 0.4238848984 0.3351123035 0.1975702047 + 0.4270057082 0.3380188048 0.1994725019 + 0.4301230907 0.3409165144 0.2013705969 + 0.4332121015 0.3438107073 0.2032697052 + 0.4362936020 0.3467020988 0.2051707059 + 0.4393599927 0.3495917916 0.2070730031 + 0.4424214065 0.3524695039 0.2089823037 + 0.4454444051 0.3553349078 0.2108915001 + 0.4484499991 0.3581919074 0.2128047943 + 0.4514287114 0.3610335886 0.2147184014 + 0.4543898106 0.3638724089 0.2166287005 + 0.4573354125 0.3666979969 0.2185377926 + 0.4602653980 0.3695237041 0.2204501927 + 0.4631789029 0.3723416924 0.2223664969 + 0.4660640955 0.3751595914 0.2242840976 + 0.4689424038 0.3779619932 0.2262071967 + 0.4717960060 0.3807600141 0.2281304002 + 0.4746480882 0.3835526109 0.2300624996 + 0.4774642885 0.3863407075 0.2319957018 + 0.4802804887 0.3891260922 0.2339314967 + 0.4830631018 0.3919047117 0.2358691990 + 0.4858393073 0.3946832120 0.2378090024 + 0.4886021912 0.3974429071 0.2397564054 + 0.4913567901 0.4002025127 0.2417037040 + 0.4941014051 0.4029656053 0.2436508983 + 0.4968290031 0.4057300985 0.2455980927 + 0.4995543063 0.4084877074 0.2475478053 + 0.5022572875 0.4112327099 0.2495003939 + 0.5049604177 0.4139777124 0.2514531016 + 0.5076419115 0.4167033136 0.2534070015 + 0.5103181005 0.4194287956 0.2553609014 + 0.5129829049 0.4221394062 0.2573103011 + 0.5156350732 0.4248403907 0.2592563927 + 0.5182831287 0.4275377989 0.2612015903 + 0.5209032893 0.4302201867 0.2631373107 + 0.5235236287 0.4329026937 0.2650730908 + 0.5261074901 0.4355578125 0.2670041025 + 0.5286799073 0.4382039905 0.2689318061 + 0.5312321782 0.4408400953 0.2708589137 + 0.5337486863 0.4434512854 0.2727747858 + 0.5362653136 0.4460625947 0.2746906877 + 0.5387454033 0.4486452043 0.2766082883 + 0.5412231088 0.4512208104 0.2785277963 + 0.5436828136 0.4537886977 0.2804473042 + 0.5461235046 0.4563367963 0.2823554873 + 0.5485640168 0.4588850141 0.2842623889 + 0.5509892702 0.4614095986 0.2861675918 + 0.5534147024 0.4639258087 0.2880688012 + 0.5558288097 0.4664359093 0.2899698913 + 0.5582318902 0.4689145088 0.2918665111 + 0.5606350899 0.4713929892 0.2937605083 + 0.5629963875 0.4738500118 0.2956545055 + 0.5653566718 0.4762899876 0.2975474000 + 0.5677015781 0.4787299931 0.2994402945 + 0.5700250864 0.4811362922 0.3013291955 + 0.5723484755 0.4835399985 0.3032068908 + 0.5746430159 0.4859381914 0.3050845861 + 0.5769314766 0.4883219898 0.3069567978 + 0.5792130232 0.4907056987 0.3088240921 + 0.5814688802 0.4930708110 0.3106913865 + 0.5837246776 0.4954228103 0.3125565946 + 0.5859628916 0.4977746904 0.3144212067 + 0.5881881118 0.5000953078 0.3162856996 + 0.5904132128 0.5024111271 0.3181492090 + 0.5926191807 0.5047224760 0.3200125992 + 0.5948230028 0.5070027113 0.3218753934 + 0.5970178246 0.5092828870 0.3237349093 + 0.5991765857 0.5115441084 0.3255943954 + 0.6013354063 0.5137696862 0.3274545968 + 0.6034780741 0.5159952044 0.3293164074 + 0.6056026816 0.5182105899 0.3311783075 + 0.6077274084 0.5204172730 0.3330385983 + 0.6098302007 0.5226240754 0.3348967135 + 0.6119242907 0.5248165131 0.3367548883 + 0.6140184999 0.5270028710 0.3386108875 + 0.6160811782 0.5291892886 0.3404648006 + 0.6181414723 0.5313519239 0.3423185945 + 0.6201986074 0.5335099101 0.3441708982 + 0.6222298145 0.5356678963 0.3460218906 + 0.6242610812 0.5378025174 0.3478729129 + 0.6262831092 0.5399355888 0.3497231901 + 0.6282786727 0.5420684218 0.3515729904 + 0.6302741766 0.5441771150 0.3534227014 + 0.6322615147 0.5462856889 0.3552713096 + 0.6342349052 0.5483934283 0.3571187854 + 0.6362084150 0.5504837036 0.3589664102 + 0.6381633878 0.5525740981 0.3608083129 + 0.6400970817 0.5546634793 0.3626450896 + 0.6420307159 0.5567383170 0.3644818962 + 0.6439446807 0.5588130951 0.3663117886 + 0.6458404064 0.5608872771 0.3681330979 + 0.6477361917 0.5629431009 0.3699544966 + 0.6496070027 0.5649988055 0.3717769086 + 0.6514570713 0.5670546293 0.3736014068 + 0.6533071995 0.5690968037 0.3754259944 + 0.6551311016 0.5711386800 0.3772482872 + 0.6569324732 0.5731804967 0.3790614903 + 0.6587339044 0.5751888156 0.3808746040 + 0.6605200768 0.5771927238 0.3826875985 + 0.6622908711 0.5791965723 0.3844920993 + 0.6640617251 0.5811750889 0.3862965107 + 0.6658166051 0.5831453800 0.3881008923 + 0.6675499082 0.5851157904 0.3898971975 + 0.6692832112 0.5870721936 0.3916915953 + 0.6710019112 0.5890191197 0.3934859037 + 0.6726891994 0.5909659863 0.3952794969 + 0.6743766069 0.5928977728 0.3970726132 + 0.6760562062 0.5948076248 0.3988656104 + 0.6776999831 0.5967172980 0.4006549120 + 0.6793438196 0.5986235142 0.4024339914 + 0.6809874177 0.6005135775 0.4042131007 + 0.6826078892 0.6024037004 0.4059922099 + 0.6842284203 0.6042937040 0.4077546000 + 0.6858488917 0.6061664820 0.4095160961 + 0.6874536872 0.6080377102 0.4112777114 + 0.6890544891 0.6099088788 0.4130330980 + 0.6906551719 0.6117550135 0.4147844911 + 0.6922481060 0.6135854721 0.4165357947 + 0.6938346028 0.6154159904 0.4182860851 + 0.6954210997 0.6172372103 0.4200322032 + 0.6969972849 0.6190348268 0.4217784107 + 0.6985461712 0.6208323240 0.4235245883 + 0.7000951767 0.6226298809 0.4252654016 + 0.7016441226 0.6243864298 0.4270048141 + 0.7031658292 0.6261391044 0.4287441969 + 0.7046864033 0.6278917193 0.4304836094 + 0.7062069774 0.6296277046 0.4322234094 + 0.7077113986 0.6313474774 0.4339630902 + 0.7092053294 0.6330673099 0.4357028008 + 0.7106992006 0.6347842813 0.4374335110 + 0.7121831179 0.6364610791 0.4391623139 + 0.7136318088 0.6381378770 0.4408909976 + 0.7150804996 0.6398146749 0.4426156878 + 0.7165293097 0.6414784789 0.4443300068 + 0.7179604173 0.6431329846 0.4460442066 + 0.7193871140 0.6447873712 0.4477584064 + 0.7208138108 0.6464402080 0.4494678080 + 0.7222303152 0.6480689049 0.4511752129 + 0.7236266732 0.6496976018 0.4528824985 + 0.7250230908 0.6513261795 0.4545893967 + 0.7264196277 0.6529368162 0.4562911987 + 0.7277973294 0.6545298100 0.4579930007 + 0.7291715741 0.6561228037 0.4596947134 + 0.7305458188 0.6577157974 0.4613985121 + 0.7319157720 0.6592859030 0.4631048143 + 0.7332760096 0.6608538032 0.4648110867 + 0.7346363068 0.6624217033 0.4665173888 + 0.7359964848 0.6639863253 0.4682199955 + 0.7373399734 0.6655421257 0.4699214101 + 0.7386773825 0.6670979857 0.4716227055 + 0.7400146723 0.6686539054 0.4733240902 + 0.7413491011 0.6702005863 0.4750083089 + 0.7426620722 0.6717411876 0.4766924977 + 0.7439751029 0.6732819080 0.4783768058 + 0.7452880740 0.6748226285 0.4800589979 + 0.7465865016 0.6763488054 0.4817341864 + 0.7478682995 0.6778733134 0.4834094048 + 0.7491500974 0.6793978214 0.4850845933 + 0.7504318953 0.6809164882 0.4867551029 + 0.7516843081 0.6823992729 0.4884186983 + 0.7529281974 0.6838821173 0.4900822937 + 0.7541720867 0.6853649020 0.4917457998 + 0.7554147243 0.6868361831 0.4934011996 + 0.7566283941 0.6882851720 0.4950498044 + 0.7578421831 0.6897342205 0.4966984093 + 0.7590559721 0.6911832094 0.4983471036 + 0.7602614760 0.6926218867 0.4999813139 + 0.7614449263 0.6940507293 0.5016077161 + 0.7626283765 0.6954795122 0.5032340884 + 0.7638118267 0.6969082952 0.5048604012 + 0.7649738789 0.6983196139 0.5064805746 + 0.7661107183 0.6997205019 0.5080981851 + 0.7672476172 0.7011213899 0.5097157955 + 0.7683845162 0.7025222778 0.5113334060 + 0.7695062757 0.7039136291 0.5129374266 + 0.7706182003 0.7053009868 0.5145360827 + 0.7717301250 0.7066884041 0.5161347985 + 0.7728418708 0.7080758810 0.5177333951 + 0.7739313245 0.7094560266 0.5193111897 + 0.7750114799 0.7108336091 0.5208796263 + 0.7760915756 0.7122113109 0.5224481225 + 0.7771716714 0.7135888934 0.5240164995 + 0.7782340050 0.7149400711 0.5255811214 + 0.7792907953 0.7162821293 0.5271432996 + 0.7803477049 0.7176241279 0.5287054777 + 0.7814046144 0.7189661264 0.5302677155 + 0.7824332118 0.7202813029 0.5318275094 + 0.7834541202 0.7215846181 0.5333846807 + 0.7844750285 0.7228879929 0.5349419713 + 0.7854959965 0.7241913080 0.5364993215 + 0.7865008712 0.7254747152 0.5380507708 + 0.7875007987 0.7267451286 0.5395910144 + 0.7885007858 0.7280154228 0.5411313176 + 0.7895007730 0.7292857170 0.5426715016 + 0.7904886007 0.7305502295 0.5442109704 + 0.7914711237 0.7318078876 0.5457459092 + 0.7924535871 0.7330656052 0.5472807288 + 0.7934361100 0.7343233228 0.5488154888 + 0.7943943143 0.7355695963 0.5503503084 + 0.7953352928 0.7367874980 0.5518705845 + 0.7962762713 0.7380053997 0.5533890128 + 0.7972173095 0.7392233014 0.5549073815 + 0.7981507182 0.7404392958 0.5564258099 + 0.7990738750 0.7416235209 0.5579320788 + 0.7999970913 0.7428076863 0.5594297051 + 0.8009203076 0.7439919710 0.5609273911 + 0.8018379807 0.7451761961 0.5624250174 + 0.8027368188 0.7463406920 0.5639188886 + 0.8036355972 0.7474998236 0.5654007196 + 0.8045343757 0.7486588955 0.5668826103 + 0.8054332137 0.7498180866 0.5683643818 + 0.8063105941 0.7509673834 0.5698462725 + 0.8071876168 0.7521057129 0.5713130832 + 0.8080645800 0.7532439232 0.5727772713 + 0.8089416027 0.7543821931 0.5742415190 + 0.8098158240 0.7555181980 0.5757057071 + 0.8106887937 0.7566338778 0.5771644711 + 0.8115618229 0.7577496767 0.5786156058 + 0.8124346733 0.7588654757 0.5800666213 + 0.8132982850 0.7599812150 0.5815176964 + 0.8141440153 0.7610852718 0.5829687715 + 0.8149898052 0.7621840835 0.5843982100 + 0.8158354759 0.7632827759 0.5858259201 + 0.8166813254 0.7643815875 0.5872535706 + 0.8174951077 0.7654762268 0.5886812210 + 0.8183075190 0.7665566802 0.5901004076 + 0.8191198707 0.7676370740 0.5915061831 + 0.8199322820 0.7687175870 0.5929120183 + 0.8207353950 0.7697979808 0.5943177938 + 0.8215301037 0.7708619833 0.5957236290 + 0.8223248124 0.7719196081 0.5971149802 + 0.8231195211 0.7729771137 0.5985029936 + 0.8239132166 0.7740346193 0.5998910069 + 0.8246840835 0.7750877142 0.6012789011 + 0.8254551291 0.7761170268 0.6026648283 + 0.8262259960 0.7771462798 0.6040399075 + 0.8269969821 0.7781755924 0.6054151058 + 0.8277590871 0.7792049050 0.6067903042 + 0.8285127878 0.7802203894 0.6081655025 + 0.8292664289 0.7812247872 0.6095303893 + 0.8300201297 0.7822293043 0.6108843088 + 0.8307737112 0.7832337022 0.6122382283 + 0.8315144181 0.7842381001 0.6135920882 + 0.8322539926 0.7852112055 0.6149461269 + 0.8329936862 0.7861806750 0.6162794232 + 0.8337333202 0.7871502042 0.6176058054 + 0.8344634175 0.7881196737 0.6189321876 + 0.8351711035 0.7890831828 0.6202585101 + 0.8358789086 0.7900207043 0.6215848923 + 0.8365865946 0.7909581065 0.6228948832 + 0.8372943997 0.7918956280 0.6242042184 + 0.8379939198 0.7928329706 0.6255134940 + 0.8386886120 0.7937645912 0.6268228292 + 0.8393831849 0.7946882248 0.6281303167 + 0.8400778174 0.7956116796 0.6294245124 + 0.8407723904 0.7965353131 0.6307187080 + 0.8414523005 0.7974588871 0.6320130229 + 0.8421304226 0.7983703017 0.6333072186 + 0.8428084850 0.7992740870 0.6345958710 + 0.8434864879 0.8001778126 0.6358658075 + 0.8441630006 0.8010815978 0.6371356845 + 0.8448262811 0.8019853830 0.6384056211 + 0.8454895020 0.8028789163 0.6396754980 + 0.8461527824 0.8037689924 0.6409432292 + 0.8468161225 0.8046591878 0.6422051191 + 0.8474723101 0.8055493236 0.6434670091 + 0.8481097817 0.8064395189 0.6447290182 + 0.8487473130 0.8073065877 0.6459909081 + 0.8493847847 0.8081693053 0.6472480297 + 0.8500223160 0.8090320826 0.6484932899 + 0.8506528139 0.8098948002 0.6497384906 + 0.8512719870 0.8107575178 0.6509838104 + 0.8518912196 0.8115878105 0.6522290111 + 0.8525105119 0.8124132156 0.6534695029 + 0.8531296849 0.8132386804 0.6546944976 + 0.8537434936 0.8140640855 0.6559193730 + 0.8543506861 0.8148896098 0.6571443081 + 0.8549579978 0.8157023191 0.6583693027 + 0.8555651903 0.8165128231 0.6595931053 + 0.8561723828 0.8173233271 0.6608086228 + 0.8567661047 0.8181337714 0.6620242000 + 0.8573439121 0.8189442754 0.6632397771 + 0.8579216003 0.8197408915 0.6644554138 + 0.8584992886 0.8205333948 0.6656709909 + 0.8590769768 0.8213258982 0.6668658853 + 0.8596463799 0.8221184015 0.6680600047 + 0.8602035046 0.8229109049 0.6692541838 + 0.8607606888 0.8236929774 0.6704483032 + 0.8613178134 0.8244690895 0.6716424227 + 0.8618749976 0.8252452016 0.6728152037 + 0.8624271750 0.8260213137 0.6739807129 + 0.8629680276 0.8267974854 0.6751462221 + 0.8635088801 0.8275653720 0.6763116717 + 0.8640497923 0.8283231854 0.6774771810 + 0.8645905852 0.8290811181 0.6786333919 + 0.8651301265 0.8298389912 0.6797791719 + 0.8656617999 0.8305969238 0.6809248924 + 0.8661935925 0.8313513994 0.6820706725 + 0.8667253256 0.8320925832 0.6832165122 + 0.8672569990 0.8328338265 0.6843608022 + 0.8677886724 0.8335750103 0.6854962111 + 0.8683093786 0.8343161941 0.6866315007 + 0.8688293099 0.8350573778 0.6877667904 + 0.8693491817 0.8357766271 0.6889020801 + 0.8698691130 0.8364936113 0.6900374889 + 0.8703891039 0.8372104764 0.6911699772 + 0.8708937764 0.8379275203 0.6923016906 + 0.8713908792 0.8386443853 0.6934332848 + 0.8718879819 0.8393507004 0.6945648789 + 0.8723850846 0.8400480151 0.6956964731 + 0.8728821874 0.8407453895 0.6968225241 + 0.8733705282 0.8414428234 0.6979349852 + 0.8738415837 0.8421400785 0.6990476251 + 0.8743125796 0.8428345919 0.7001600862 + 0.8747836947 0.8435099125 0.7012727261 + 0.8752548099 0.8441851735 0.7023853064 + 0.8757258058 0.8448604941 0.7034785748 + 0.8761900067 0.8455356956 0.7045670152 + 0.8766540289 0.8462110162 0.7056552768 + 0.8771178722 0.8468806148 0.7067435980 + 0.8775817752 0.8475472927 0.7078319192 + 0.8780456781 0.8482139111 0.7089164257 + 0.8785020113 0.8488805294 0.7099874020 + 0.8789511919 0.8495470881 0.7110584974 + 0.8794003725 0.8502116799 0.7121295929 + 0.8798494935 0.8508617282 0.7132006288 + 0.8802986741 0.8515115976 0.7142717242 + 0.8807477951 0.8521615267 0.7153381109 + 0.8811936975 0.8528113961 0.7164012194 + 0.8816397190 0.8534613252 0.7174643278 + 0.8820856214 0.8541015983 0.7185273767 + 0.8825315237 0.8547332287 0.7195906043 + 0.8829774261 0.8553647995 0.7206537127 + 0.8834267855 0.8559963703 0.7216954231 + 0.8838804960 0.8566280007 0.7227349877 + 0.8843340874 0.8572596908 0.7237744927 + 0.8847877979 0.8578745127 0.7248139977 + 0.8852415085 0.8584867120 0.7258535028 + 0.8856950998 0.8590989709 0.7268896103 + 0.8861432076 0.8597112894 0.7279071808 + 0.8865901828 0.8603236079 0.7289246917 + 0.8870372176 0.8609315753 0.7299422026 + 0.8874841928 0.8615155816 0.7309597731 + 0.8879311085 0.8620995879 0.7319772840 + 0.8883765936 0.8626837134 0.7329847217 + 0.8888130188 0.8632677197 0.7339726090 + 0.8892495036 0.8638517857 0.7349603772 + 0.8896859288 0.8644294739 0.7359483242 + 0.8901224136 0.8649967909 0.7369360924 + 0.8905587792 0.8655641079 0.7379239798 + 0.8909912109 0.8661314249 0.7389044762 + 0.8914173841 0.8666986227 0.7398765087 + 0.8918436170 0.8672658801 0.7408484817 + 0.8922697902 0.8678284287 0.7418205738 + 0.8926960230 0.8683868051 0.7427926064 + 0.8931223154 0.8689451218 0.7437645793 + 0.8935400248 0.8695034981 0.7447342873 + 0.8939518929 0.8700618148 0.7457019091 + 0.8943638206 0.8706201911 0.7466694713 + 0.8947756886 0.8711758852 0.7476369739 + 0.8951874971 0.8717300892 0.7486045957 + 0.8955994248 0.8722842932 0.7495722175 + 0.8960080743 0.8728384972 0.7505385876 + 0.8964155912 0.8733927011 0.7515040040 + 0.8968231082 0.8739469051 0.7524694800 + 0.8972306252 0.8744983077 0.7534348965 + 0.8976380229 0.8750482798 0.7544003129 + 0.8980454803 0.8755983114 0.7553657293 + 0.8984466195 0.8761482835 0.7563228011 + 0.8988459110 0.8766983151 0.7572699785 + 0.8992452025 0.8772484064 0.7582172155 + 0.8996444941 0.8777912259 0.7591643929 + 0.9000439048 0.8783296943 0.7601115108 + 0.9004431963 0.8788682222 0.7610586882 + 0.9008390903 0.8794066906 0.7620021105 + 0.9012339711 0.8799452186 0.7629373074 + 0.9016289711 0.8804838061 0.7638723850 + 0.9020239711 0.8810167909 0.7648075819 + 0.9024189711 0.8815448284 0.7657427788 + 0.9028139114 0.8820728064 0.7666779757 + 0.9032065868 0.8826007843 0.7676120996 + 0.9035984278 0.8831288218 0.7685371041 + 0.9039902091 0.8836567998 0.7694622278 + 0.9043819904 0.8841773272 0.7703871727 + 0.9047737718 0.8846845031 0.7713122964 + 0.9051656127 0.8851916790 0.7722373009 + 0.9055474997 0.8856989145 0.7731624246 + 0.9059227109 0.8862062097 0.7740741968 + 0.9062979221 0.8867133856 0.7749837041 + 0.9066730738 0.8872200251 0.7758932710 + 0.9070482850 0.8877230883 0.7768027782 + 0.9074234962 0.8882262111 0.7777122855 + 0.9077919126 0.8887292147 0.7786217928 + 0.9081501961 0.8892322779 0.7795184851 + 0.9085084796 0.8897352815 0.7804030776 + 0.9088668227 0.8902384043 0.7812876105 + 0.9092251062 0.8907387853 0.7821720839 + 0.9095833898 0.8912386894 0.7830566168 + 0.9099410176 0.8917385936 0.7839412093 + 0.9102939963 0.8922384977 0.7848230004 + 0.9106469750 0.8927385211 0.7856823802 + 0.9110000134 0.8932384253 0.7865417004 + 0.9113529921 0.8937345147 0.7874010801 + 0.9117060900 0.8942261934 0.7882605195 + 0.9120591283 0.8947178721 0.7891197801 + 0.9124001861 0.8952096105 0.7899792194 + 0.9127386212 0.8957012892 0.7908260822 + 0.9130769968 0.8961930871 0.7916657925 + 0.9134153724 0.8966842890 0.7925053835 + 0.9137538075 0.8971561790 0.7933450937 + 0.9140921831 0.8976281881 0.7941846848 + 0.9144207835 0.8981001973 0.7950243950 + 0.9147356153 0.8985722065 0.7958624959 + 0.9150503874 0.8990442157 0.7966837883 + 0.9153652191 0.8995161057 0.7975050807 + 0.9156799912 0.8999810219 0.7983263731 + 0.9159948230 0.9004393816 0.7991477847 + 0.9163095951 0.9008978009 0.7999690771 + 0.9166138768 0.9013561010 0.8007904291 + 0.9169176221 0.9018145204 0.8015990257 + 0.9172213078 0.9022728205 0.8023931980 + 0.9175249934 0.9027311802 0.8031873107 + 0.9178286791 0.9031661749 0.8039814830 + 0.9181324244 0.9035997987 0.8047757149 + 0.9184330106 0.9040333033 0.8055698872 + 0.9187291861 0.9044669271 0.8063641191 + 0.9190254211 0.9049004912 0.8071421981 + 0.9193214774 0.9053339958 0.8079159856 + 0.9196177125 0.9057644010 0.8086897135 + 0.9199138284 0.9061868787 0.8094635010 + 0.9202100039 0.9066092968 0.8102372289 + 0.9205018878 0.9070317745 0.8110108972 + 0.9207926989 0.9074543118 0.8117843866 + 0.9210835099 0.9078767896 0.8125452995 + 0.9213743210 0.9082993269 0.8133062124 + 0.9216650724 0.9087104201 0.8140671253 + 0.9219558835 0.9091129899 0.8148279786 + 0.9222459793 0.9095156193 0.8155888915 + 0.9225308895 0.9099181890 0.8163498044 + 0.9228156805 0.9103208184 0.8171091080 + 0.9231005907 0.9107235074 0.8178617954 + 0.9233853817 0.9111260772 0.8186144233 + 0.9236702919 0.9115067720 0.8193669915 + 0.9239550829 0.9118813276 0.8201196790 + 0.9242395163 0.9122558832 0.8208723068 + 0.9245231748 0.9126303792 0.8216248751 + 0.9248068929 0.9130049944 0.8223708868 + 0.9250906110 0.9133794904 0.8230999112 + 0.9253743291 0.9137541056 0.8238288164 + 0.9256579280 0.9141280055 0.8245577812 + 0.9259415865 0.9145019054 0.8252866864 + 0.9262230992 0.9148758054 0.8260157108 + 0.9265028238 0.9152497053 0.8267446160 + 0.9267824888 0.9156236053 0.8274663091 + 0.9270622134 0.9159975052 0.8281710148 + 0.9273418784 0.9163714051 0.8288756013 + 0.9276214838 0.9167358279 0.8295801878 + 0.9279012084 0.9171000123 0.8302847743 + 0.9281796813 0.9174641967 0.8309894204 + 0.9284574986 0.9178283811 0.8316941261 + 0.9287353158 0.9181925058 0.8323919773 + 0.9290130734 0.9185566902 0.8330684900 + 0.9292908907 0.9189208746 0.8337450027 + 0.9295685887 0.9192627072 0.8344215155 + 0.9298464060 0.9196035266 0.8350980878 + 0.9301217794 0.9199442863 0.8357746005 + 0.9303957820 0.9202849865 0.8364511132 + 0.9306699038 0.9206258059 0.8371245265 + 0.9309440255 0.9209665060 0.8377707005 + 0.9312180877 0.9213073254 0.8384168744 + 0.9314922094 0.9216343164 0.8390631080 + 0.9317662716 0.9219589829 0.8397092819 + 0.9320312738 0.9222837090 0.8403555155 + 0.9322906733 0.9226084948 0.8410016894 + 0.9325500727 0.9229332209 0.8416479230 + 0.9328094721 0.9232580066 0.8422836065 + 0.9330688119 0.9235826731 0.8429179788 + 0.9333282113 0.9238941073 0.8435524106 + 0.9335876107 0.9241997004 0.8441867828 + 0.9338458180 0.9245051742 0.8448212147 + 0.9341027141 0.9248107076 0.8454555869 + 0.9343596101 0.9251161814 0.8460900187 + 0.9346163869 0.9254217148 0.8467177749 + 0.9348732829 0.9257271886 0.8473411202 + 0.9351301789 0.9260296226 0.8479645252 + 0.9353870749 0.9263281822 0.8485878706 + 0.9356436729 0.9266266823 0.8492112160 + 0.9358993769 0.9269253016 0.8498346210 + 0.9361552000 0.9272238016 0.8504580259 + 0.9364109039 0.9275224209 0.8510761857 + 0.9366667271 0.9278209209 0.8516775966 + 0.9369223714 0.9281198978 0.8522790074 + 0.9371781945 0.9284216762 0.8528804183 + 0.9374338984 0.9287235737 0.8534818292 + 0.9376984239 0.9290254116 0.8540831804 + 0.9379627705 0.9293273091 0.8546845913 + 0.9382272959 0.9296290874 0.8552860022 + 0.9384917021 0.9299309850 0.8558799028 + 0.9387561083 0.9302328229 0.8564717770 + 0.9390205741 0.9305325747 0.8570637703 + 0.9392849803 0.9308316112 0.8576557040 + 0.9395543933 0.9311305285 0.8582475781 + 0.9398267269 0.9314295053 0.8588395119 + 0.9400988817 0.9317284822 0.8594313860 + 0.9403710961 0.9320273995 0.8600232005 + 0.9406434298 0.9323263764 0.8606147766 + 0.9409155846 0.9326242208 0.8612062931 + 0.9411879182 0.9329177737 0.8617979288 + 0.9414601922 0.9332113862 0.8623893857 + 0.9417330027 0.9335049987 0.8629810214 + 0.9420056939 0.9337986112 0.8635724783 + 0.9422785044 0.9340922236 0.8641641140 + 0.9425511956 0.9343857765 0.8647502065 + 0.9428240061 0.9346793890 0.8653333783 + 0.9430966973 0.9349703789 0.8659164906 + 0.9433695078 0.9352599978 0.8664996028 + 0.9436365962 0.9355496168 0.8670827150 + 0.9439011216 0.9358391762 0.8676658273 + 0.9441655278 0.9361287951 0.8682489991 + 0.9444299936 0.9364184141 0.8688321114 + 0.9446943998 0.9367079735 0.8694059253 + 0.9449589252 0.9369975924 0.8699793220 + 0.9452232718 0.9372946024 0.8705525994 + 0.9454872012 0.9375914931 0.8711258769 + 0.9457437992 0.9378883839 0.8716992736 + 0.9460003972 0.9381853938 0.8722726107 + 0.9462569952 0.9384822845 0.8728458881 + 0.9465134740 0.9387792945 0.8734164834 + 0.9467700720 0.9390761852 0.8739734888 + 0.9470267296 0.9393727779 0.8745303750 + 0.9472833276 0.9396685958 0.8750873804 + 0.9475367069 0.9399644136 0.8756443858 + 0.9477851987 0.9402601719 0.8762012720 + 0.9480336905 0.9405561090 0.8767582774 + 0.9482821822 0.9408519268 0.8773152828 + 0.9485306740 0.9411476851 0.8778693080 + 0.9487792253 0.9414435029 0.8784165978 + 0.9490277171 0.9417338967 0.8789640069 + 0.9492762089 0.9420179129 0.8795112967 + 0.9495244026 0.9423019886 0.8800587058 + 0.9497724175 0.9425861239 0.8806061149 + 0.9500203729 0.9428701997 0.8811534047 + 0.9502683878 0.9431542754 0.8817008138 + 0.9505164027 0.9434382915 0.8822470903 + 0.9507644176 0.9437224269 0.8827915788 + 0.9510124922 0.9440007806 0.8833361864 + 0.9512605071 0.9442747831 0.8838806748 + 0.9515028000 0.9445487857 0.8844252229 + 0.9517431855 0.9448227882 0.8849697113 + 0.9519836903 0.9450967908 0.8855141997 + 0.9522241950 0.9453707933 0.8860588074 + 0.9524646997 0.9456447959 0.8865966797 + 0.9527052045 0.9459187984 0.8871210217 + 0.9529457092 0.9461904168 0.8876453042 + 0.9531862140 0.9464601874 0.8881695867 + 0.9534236193 0.9467300177 0.8886939883 + 0.9536604285 0.9469999075 0.8892182708 + 0.9538971782 0.9472696781 0.8897426128 + 0.9541339278 0.9475396276 0.8902668953 + 0.9543706775 0.9478093982 0.8907887936 + 0.9546074867 0.9480792284 0.8913012743 + 0.9548442960 0.9483507872 0.8918138146 + 0.9550809860 0.9486241937 0.8923262954 + 0.9553158283 0.9488974810 0.8928387761 + 0.9555500746 0.9491708279 0.8933513761 + 0.9557843804 0.9494441152 0.8938639164 + 0.9560186863 0.9497175217 0.8943763971 + 0.9562528729 0.9499908090 0.8948888779 + 0.9564871788 0.9502640963 0.8953927159 + 0.9567214847 0.9505357742 0.8958964944 + 0.9569557905 0.9508042932 0.8964002132 + 0.9571847916 0.9510728717 0.8969039917 + 0.9574114084 0.9513413906 0.8974078298 + 0.9576380253 0.9516100287 0.8979116082 + 0.9578645825 0.9518784881 0.8984153271 + 0.9580911994 0.9521470070 0.8989191055 + 0.9583178759 0.9524155855 0.8994185925 + 0.9585444927 0.9526826739 0.8999162912 + 0.9587711096 0.9529364109 0.9004138708 + 0.9589902163 0.9531900287 0.9009116292 + 0.9592015147 0.9534437060 0.9014092088 + 0.9594126940 0.9536973238 0.9019069076 + 0.9596238732 0.9539508820 0.9024044871 + 0.9598351121 0.9542046189 0.9029021859 + 0.9600464106 0.9544581771 0.9033970833 + 0.9602575898 0.9547119141 0.9038860798 + 0.9604688287 0.9549633861 0.9043750167 + 0.9606760144 0.9552143216 0.9048638940 + 0.9608663917 0.9554650784 0.9053527713 + 0.9610567093 0.9557158947 0.9058417082 + 0.9612470865 0.9559667706 0.9063305855 + 0.9614375234 0.9562175870 0.9068195224 + 0.9616279006 0.9564685225 0.9073083997 + 0.9618182778 0.9567192793 0.9077844024 + 0.9620087147 0.9569705129 0.9082577825 + 0.9621990919 0.9572222233 0.9087312222 + 0.9623833895 0.9574738741 0.9092046022 + 0.9625663757 0.9577257037 0.9096779823 + 0.9627494216 0.9579774141 0.9101514220 + 0.9629324079 0.9582291245 0.9106246829 + 0.9631155133 0.9584808946 0.9110981226 + 0.9632984996 0.9587326050 0.9115682244 + 0.9634814858 0.9589843154 0.9120290875 + 0.9636644721 0.9592264891 0.9124898911 + 0.9638484120 0.9594662786 0.9129506946 + 0.9640339017 0.9597061872 0.9134116173 + 0.9642195106 0.9599459767 0.9138724208 + 0.9644050002 0.9601858258 0.9143332243 + 0.9645904899 0.9604256749 0.9147940278 + 0.9647760987 0.9606655240 0.9152548909 + 0.9649615884 0.9609053731 0.9157108068 + 0.9651471972 0.9611442089 0.9161633849 + 0.9653326869 0.9613788724 0.9166160226 + 0.9655120969 0.9616137147 0.9170686007 + 0.9656897783 0.9618483782 0.9175212979 + 0.9658675194 0.9620831013 0.9179738760 + 0.9660452008 0.9623178840 0.9184265137 + 0.9662228227 0.9625526071 0.9188790917 + 0.9664005041 0.9627872705 0.9193317294 + 0.9665781856 0.9630219936 0.9197779894 + 0.9667559266 0.9632468820 0.9202229977 + 0.9669327736 0.9634612799 0.9206681252 + 0.9671043158 0.9636756778 0.9211130738 + 0.9672759175 0.9638900757 0.9215580821 + 0.9674474001 0.9641044736 0.9220032096 + 0.9676188827 0.9643189907 0.9224482179 + 0.9677904844 0.9645333886 0.9228932858 + 0.9679620266 0.9647477865 0.9233382940 + 0.9681335092 0.9649621844 0.9237800241 + 0.9683051109 0.9651690125 0.9242216945 + 0.9684718251 0.9653723240 0.9246633053 + 0.9686310291 0.9655755758 0.9251049757 + 0.9687901139 0.9657788873 0.9255465865 + 0.9689491987 0.9659821987 0.9259883165 + 0.9691084027 0.9661855102 0.9264299273 + 0.9692674875 0.9663888216 0.9268715978 + 0.9694265723 0.9665920138 0.9273126125 + 0.9695857763 0.9667953253 0.9277383089 + 0.9697449207 0.9669919014 0.9281641245 + 0.9699026942 0.9671866894 0.9285898209 + 0.9700592756 0.9673815966 0.9290155172 + 0.9702159166 0.9675763845 0.9294412136 + 0.9703724980 0.9677711725 0.9298669100 + 0.9705290794 0.9679660201 0.9302927256 + 0.9706857204 0.9681608081 0.9307184219 + 0.9708423018 0.9683555961 0.9311441183 + 0.9709988832 0.9685503840 0.9315564036 + 0.9711555243 0.9687359929 0.9319686294 + 0.9713115096 0.9689192772 0.9323807955 + 0.9714668989 0.9691025019 0.9327929020 + 0.9716224074 0.9692857862 0.9332051277 + 0.9717779160 0.9694690108 0.9336172938 + 0.9719334245 0.9696522951 0.9340295196 + 0.9720888138 0.9698355198 0.9344416857 + 0.9722443223 0.9700188041 0.9348539114 + 0.9723997712 0.9702020288 0.9352610111 + 0.9725552797 0.9703844786 0.9356672168 + 0.9727123976 0.9705666900 0.9360734224 + 0.9728708863 0.9707489014 0.9364796281 + 0.9730293751 0.9709311128 0.9368857741 + 0.9731879234 0.9711133242 0.9372920990 + 0.9733464122 0.9712954164 0.9376983047 + 0.9735049009 0.9714776278 0.9381045103 + 0.9736633897 0.9716597795 0.9385107160 + 0.9738218784 0.9718419909 0.9389162064 + 0.9739804268 0.9720212221 0.9393213987 + 0.9741386175 0.9721977711 0.9397264719 + 0.9742962122 0.9723743796 0.9401317239 + 0.9744538069 0.9725509882 0.9405367970 + 0.9746114016 0.9727275968 0.9409419894 + 0.9747691154 0.9729040861 0.9413471222 + 0.9749267101 0.9730806947 0.9417521954 + 0.9750843048 0.9732573032 0.9421573877 + 0.9752420187 0.9734339118 0.9425604939 + 0.9753996134 0.9736099839 0.9429581165 + 0.9755573869 0.9737846851 0.9433556795 + 0.9757156968 0.9739593267 0.9437533021 + 0.9758740067 0.9741340280 0.9441508055 + 0.9760323763 0.9743086100 0.9445484281 + 0.9761906862 0.9744831920 0.9449459910 + 0.9763489962 0.9746578932 0.9453436136 + 0.9765074253 0.9748324752 0.9457411766 + 0.9766656756 0.9750071764 0.9461387992 + 0.9768239856 0.9751818180 0.9465218186 + 0.9769824147 0.9753580093 0.9469017982 + 0.9771348834 0.9755343795 0.9472817183 + 0.9772865176 0.9757108092 0.9476615787 + 0.9774382114 0.9758871794 0.9480414987 + 0.9775897861 0.9760636091 0.9484214187 + 0.9777414203 0.9762399793 0.9488013983 + 0.9778929949 0.9764164090 0.9491813183 + 0.9780446887 0.9765927792 0.9495611787 + 0.9781963229 0.9767692089 0.9499387145 + 0.9783478975 0.9769436717 0.9503098726 + 0.9785013795 0.9771149158 0.9506810904 + 0.9786573052 0.9772862196 0.9510523081 + 0.9788131118 0.9774574041 0.9514235258 + 0.9789689779 0.9776285887 0.9517948031 + 0.9791249037 0.9777997732 0.9521660209 + 0.9792807102 0.9779710174 0.9525371790 + 0.9794365764 0.9781423211 0.9529083967 + 0.9795925021 0.9783135056 0.9532796144 + 0.9797484279 0.9784846902 0.9536480904 + 0.9799041748 0.9786469936 0.9540150762 + 0.9800559878 0.9788064957 0.9543820024 + 0.9802073240 0.9789659977 0.9547489882 + 0.9803586006 0.9791253805 0.9551159143 + 0.9805098772 0.9792848825 0.9554827809 + 0.9806612730 0.9794443846 0.9558498263 + 0.9808126092 0.9796038866 0.9562166929 + 0.9809638858 0.9797633886 0.9565836787 + 0.9811152220 0.9799227715 0.9569506049 + 0.9812664986 0.9800817966 0.9573094845 + 0.9814171195 0.9802325964 0.9576671720 + 0.9815658927 0.9803833961 0.9580247998 + 0.9817147255 0.9805341959 0.9583824873 + 0.9818633795 0.9806848764 0.9587401748 + 0.9820122123 0.9808356762 0.9590978026 + 0.9821609259 0.9809864759 0.9594554901 + 0.9823096991 0.9811372757 0.9598131776 + 0.9824584126 0.9812880754 0.9601708055 + 0.9826071858 0.9814388752 0.9605284929 + 0.9827558994 0.9815893173 0.9608864188 + 0.9829031229 0.9817385077 0.9612444043 + 0.9830487967 0.9818876982 0.9616022706 + 0.9831944108 0.9820368886 0.9619601965 + 0.9833400846 0.9821860790 0.9623181820 + 0.9834856987 0.9823353291 0.9626761079 + 0.9836313725 0.9824845195 0.9630339742 + 0.9837771058 0.9826337099 0.9633920193 + 0.9839227200 0.9827829003 0.9637498856 + 0.9840683937 0.9829320908 0.9641076922 + 0.9842140079 0.9830825925 0.9644622207 + 0.9843599200 0.9832354188 0.9648168087 + 0.9845057726 0.9833881855 0.9651712775 + 0.9846516848 0.9835410118 0.9655258060 + 0.9847975969 0.9836937785 0.9658802748 + 0.9849435091 0.9838467240 0.9662348032 + 0.9850894213 0.9839994907 0.9665892720 + 0.9852352738 0.9841523170 0.9669439197 + 0.9853813052 0.9843050838 0.9672983885 + 0.9855272174 0.9844579101 0.9676529169 + 0.9856731296 0.9846124053 0.9679955244 + 0.9858202934 0.9847701192 0.9683375955 + 0.9859679937 0.9849278927 0.9686797857 + 0.9861156940 0.9850856066 0.9690219164 + 0.9862633944 0.9852433801 0.9693641067 + 0.9864110947 0.9854010940 0.9697061777 + 0.9865589142 0.9855589271 0.9700484276 + 0.9867066145 0.9857165813 0.9703904986 + 0.9868543148 0.9858744144 0.9707326889 + 0.9870020151 0.9860321283 0.9710748196 + 0.9871497154 0.9861894846 0.9714078903 + 0.9872943163 0.9863452911 0.9717383981 + 0.9874371886 0.9865010977 0.9720689058 + 0.9875801206 0.9866569042 0.9723994136 + 0.9877228737 0.9868127704 0.9727299213 + 0.9878658056 0.9869685769 0.9730604291 + 0.9880086780 0.9871243834 0.9733908772 + 0.9881514907 0.9872803092 0.9737213850 + 0.9882944226 0.9874361157 0.9740518928 + 0.9884372950 0.9875919223 0.9743824005 + 0.9885801077 0.9877477288 0.9747108221 + 0.9887198210 0.9878988266 0.9750369191 + 0.9888557196 0.9880496860 0.9753628969 + 0.9889916182 0.9882006049 0.9756889939 + 0.9891275167 0.9883515239 0.9760150909 + 0.9892634749 0.9885023832 0.9763411880 + 0.9893993735 0.9886533022 0.9766672254 + 0.9895352721 0.9888042212 0.9769933224 + 0.9896711707 0.9889550805 0.9773194194 + 0.9898071885 0.9891059995 0.9776453972 + 0.9899430871 0.9892569184 0.9779708982 + 0.9900798202 0.9894033074 0.9782887101 + 0.9902198911 0.9895470738 0.9786065817 + 0.9903600216 0.9896908998 0.9789245129 + 0.9905000925 0.9898347259 0.9792423248 + 0.9906402826 0.9899784923 0.9795601964 + 0.9907804132 0.9901223183 0.9798781276 + 0.9909204841 0.9902660847 0.9801958799 + 0.9910606146 0.9904099107 0.9805138111 + 0.9912006855 0.9905536771 0.9808316827 + 0.9913408756 0.9906975031 0.9811494946 + 0.9914810061 0.9908416271 0.9814661145 + 0.9916189909 0.9909868836 0.9817817807 + 0.9917564988 0.9911323190 0.9820973873 + 0.9918940067 0.9912775755 0.9824131131 + 0.9920315146 0.9914230108 0.9827287197 + 0.9921690226 0.9915683270 0.9830443263 + 0.9923064709 0.9917137027 0.9833599925 + 0.9924440980 0.9918590188 0.9836755991 + 0.9925816059 0.9920043945 0.9839913249 + 0.9927191138 0.9921497107 0.9843068719 + 0.9928566217 0.9922950268 0.9846224785 + 0.9929937124 0.9924386144 0.9849327803 + 0.9931297898 0.9925811291 0.9852427244 + 0.9932658076 0.9927235842 0.9855526090 + 0.9934018850 0.9928659797 0.9858624935 + 0.9935380220 0.9930084944 0.9861723781 + 0.9936740994 0.9931508899 0.9864822030 + 0.9938101768 0.9932934046 0.9867920876 + 0.9939463139 0.9934358001 0.9871019721 + 0.9940823913 0.9935783148 0.9874119163 + 0.9942185283 0.9937207103 0.9877216816 + 0.9943544865 0.9938632250 0.9880297184 + 0.9944881201 0.9940040112 0.9883286953 + 0.9946203232 0.9941446781 0.9886276722 + 0.9947524071 0.9942854047 0.9889267087 + 0.9948846102 0.9944260716 0.9892256856 + 0.9950168133 0.9945669174 0.9895246029 + 0.9951490164 0.9947075844 0.9898235798 + 0.9952812195 0.9948483109 0.9901226163 + 0.9954133034 0.9949889779 0.9904215932 + 0.9955455065 0.9951297045 0.9907206297 + 0.9956777096 0.9952703714 0.9910196066 + 0.9958099127 0.9954108000 0.9913132787 + 0.9959365129 0.9955487251 0.9915956259 + 0.9960625768 0.9956865907 0.9918777943 + 0.9961885810 0.9958245158 0.9921600223 + 0.9963147044 0.9959623814 0.9924421906 + 0.9964408278 0.9961003065 0.9927244186 + 0.9965668917 0.9962381721 0.9930065870 + 0.9966928959 0.9963760972 0.9932888150 + 0.9968190193 0.9965140224 0.9935709834 + 0.9969450831 0.9966518879 0.9938532114 + 0.9970712066 0.9967898130 0.9941353798 + 0.9971972108 0.9969279766 0.9944099784 + 0.9973220825 0.9970669746 0.9946690202 + 0.9974470139 0.9972059131 0.9949278831 + 0.9975720048 0.9973449111 0.9951869249 + 0.9976968765 0.9974839091 0.9954459071 + 0.9978219271 0.9976229072 0.9957048297 + 0.9979467988 0.9977619052 0.9959638119 + 0.9980716705 0.9979009032 0.9962226748 + 0.9981967211 0.9980399013 0.9964817166 + 0.9983215928 0.9981788993 0.9967406988 + 0.9984465241 0.9983178973 0.9969996214 + 0.9985718727 0.9984570742 0.9972563982 + 0.9987018108 0.9985973835 0.9975059032 + 0.9988316298 0.9987375736 0.9977552891 + 0.9989613891 0.9988778830 0.9980046749 + 0.9990912080 0.9990180731 0.9982541203 + 0.9992210865 0.9991583824 0.9985035062 + 0.9993509054 0.9992986917 0.9987528920 + 0.9994807243 0.9994388819 0.9990022779 + 0.9996104836 0.9995791912 0.9992517829 + 0.9997404218 0.9997195005 0.9995012283 + 0.9998701811 0.9998596907 0.9997506142 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_64t.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_64t.spi1d index e0066fa2..bed9be69 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_64t.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_64t.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0007026231 0.0005291322 0.0003438386 - 0.0014857050 0.0011212280 0.0007142003 - 0.0023536440 0.0017713160 0.0011152170 - 0.0033091281 0.0024790310 0.0015472880 - 0.0043581589 0.0032436289 0.0020068779 - 0.0055047469 0.0040647909 0.0024986709 - 0.0067437058 0.0049467869 0.0030213159 - 0.0080656186 0.0058862939 0.0035776510 - 0.0094755674 0.0068916250 0.0041668881 - 0.0109947799 0.0079634041 0.0047885012 - 0.0125997197 0.0091056926 0.0054450892 - 0.0142905395 0.0103072003 0.0061361468 - 0.0160812791 0.0115757296 0.0068590711 - 0.0179866701 0.0129182898 0.0076176482 - 0.0199946500 0.0143368002 0.0084124254 - 0.0220939498 0.0158436093 0.0092439801 - 0.0242956392 0.0174334496 0.0101133203 - 0.0266019199 0.0191023294 0.0110254698 - 0.0290070400 0.0208547898 0.0119773299 - 0.0315142386 0.0226893406 0.0129693104 - 0.0341268592 0.0246077292 0.0140018696 - 0.0368515886 0.0266096294 0.0150737204 - 0.0396952704 0.0286963899 0.0161837209 - 0.0426549911 0.0308631808 0.0173352305 - 0.0457203016 0.0331055112 0.0185222793 - 0.0488859490 0.0354270898 0.0197516698 - 0.0521433800 0.0378220901 0.0210211892 - 0.0554969683 0.0402937494 0.0223336108 - 0.0589461401 0.0428398289 0.0236905906 - 0.0624969490 0.0454611294 0.0250933897 - 0.0661549270 0.0481612310 0.0265355706 - 0.0699189603 0.0509477705 0.0280245394 - 0.0737859830 0.0538181216 0.0295602791 - 0.0777632222 0.0567707308 0.0311385598 - 0.0818370879 0.0598130785 0.0327619798 - 0.0860176310 0.0629488677 0.0344315693 - 0.0903011784 0.0661693737 0.0361473896 - 0.0946921036 0.0694710910 0.0379051082 - 0.0991754308 0.0728540421 0.0397059508 - 0.1037482023 0.0763145164 0.0415495113 - 0.1084047034 0.0798475519 0.0434369706 - 0.1131415963 0.0834460035 0.0453708693 - 0.1179561019 0.0871294513 0.0473461710 - 0.1228469983 0.0908933803 0.0493673384 - 0.1278257072 0.0947273821 0.0514261499 - 0.1328838021 0.0986372530 0.0535301305 - 0.1380223036 0.1026311964 0.0556773506 - 0.1432214975 0.1067062020 0.0578665212 - 0.1484941989 0.1108566970 0.0601071604 - 0.1538525969 0.1150842011 0.0623893999 - 0.1592795998 0.1193784997 0.0647117496 - 0.1647690982 0.1237355024 0.0670745894 - 0.1703308970 0.1281584054 0.0694851503 - 0.1759331971 0.1326268017 0.0719340369 - 0.1815855950 0.1371477991 0.0744230300 - 0.1872842014 0.1417109966 0.0769482180 - 0.1930216998 0.1463264972 0.0795174912 - 0.1988134980 0.1509988010 0.0821337178 - 0.2046463042 0.1557403058 0.0847895518 - 0.2105436027 0.1605253071 0.0874943137 - 0.2164897025 0.1653804928 0.0902354270 - 0.2224933952 0.1702827066 0.0930147171 - 0.2285283953 0.1752299964 0.0958402529 - 0.2345996946 0.1802355051 0.0987048224 - 0.2407190949 0.1852916926 0.1016040966 - 0.2468644977 0.1903931051 0.1045442000 - 0.2530235052 0.1955378056 0.1075213999 - 0.2591835856 0.2007202953 0.1105407029 - 0.2653717995 0.2059361935 0.1135976985 - 0.2715786099 0.2111911029 0.1166936010 - 0.2777847946 0.2164549977 0.1198228002 - 0.2839922011 0.2217417061 0.1229953989 - 0.2902092040 0.2270426005 0.1262023002 - 0.2964079082 0.2323473990 0.1294420958 - 0.3025959134 0.2376510054 0.1327183992 - 0.3087722957 0.2429573983 0.1360210031 - 0.3149321079 0.2482704073 0.1393564939 - 0.3210678101 0.2535850108 0.1427318007 - 0.3272047043 0.2589012980 0.1461382955 - 0.3333187997 0.2642197907 0.1495731026 - 0.3394154012 0.2695527971 0.1530420035 - 0.3454918861 0.2748903036 0.1565402001 - 0.3515484929 0.2802191973 0.1600638032 - 0.3576078117 0.2855531871 0.1636112034 - 0.3636479080 0.2909021080 0.1671815962 - 0.3696722090 0.2962611914 0.1707742065 - 0.3756689131 0.3016174138 0.1743832976 - 0.3816395998 0.3069770932 0.1780118048 - 0.3875685036 0.3123393059 0.1816574037 - 0.3934744000 0.3176954985 0.1853218973 - 0.3993682861 0.3230510056 0.1890095025 - 0.4052383006 0.3284165859 0.1927215010 - 0.4110727012 0.3337813914 0.1964581013 - 0.4168589115 0.3391354084 0.2002173960 - 0.4226124883 0.3444840014 0.2039882988 - 0.4283437133 0.3498201966 0.2077811956 - 0.4340476990 0.3551411927 0.2115973979 - 0.4397180080 0.3604322970 0.2154300958 - 0.4453499019 0.3657175899 0.2192791998 - 0.4509378076 0.3709863126 0.2231477052 - 0.4564813077 0.3762353063 0.2270365059 - 0.4619686007 0.3814581037 0.2309399992 - 0.4674080908 0.3866479099 0.2348691970 - 0.4728043079 0.3918175101 0.2388215065 - 0.4781545103 0.3969655931 0.2427939028 - 0.4834581017 0.4020648003 0.2467841059 - 0.4887135029 0.4071336091 0.2507896125 - 0.4939058125 0.4121663868 0.2548081875 - 0.4990504086 0.4171527028 0.2588363886 - 0.5041617155 0.4221054912 0.2628706992 - 0.5092216134 0.4270165861 0.2669112086 - 0.5142281055 0.4319120049 0.2709617913 - 0.5191956758 0.4367876947 0.2750129104 - 0.5241003036 0.4416337907 0.2790606022 - 0.5289487243 0.4464288056 0.2831023932 - 0.5337404013 0.4511983097 0.2871333063 - 0.5384750962 0.4559360147 0.2911599874 - 0.5431362987 0.4606370032 0.2951760888 - 0.5477464795 0.4652822018 0.2991817892 - 0.5523136854 0.4699006975 0.3031803966 - 0.5568382740 0.4744859934 0.3071657121 - 0.5613049269 0.4790317118 0.3111445010 - 0.5657191277 0.4835188091 0.3151197135 - 0.5700793862 0.4879747033 0.3190810978 - 0.5743808150 0.4923951924 0.3230370879 - 0.5786144733 0.4967536032 0.3269884884 - 0.5828108191 0.5010759830 0.3309372962 - 0.5869714022 0.5053607821 0.3348788023 - 0.5910838246 0.5095916986 0.3388131857 - 0.5951429009 0.5137745738 0.3427393138 - 0.5991551280 0.5179340243 0.3466500044 - 0.6030982733 0.5220689774 0.3505468071 - 0.6069651246 0.5261592269 0.3544330001 - 0.6107723713 0.5302231908 0.3583126962 - 0.6144962907 0.5342565775 0.3621850014 - 0.6181483865 0.5382382274 0.3660446107 - 0.6217647195 0.5421987772 0.3698979020 - 0.6253350973 0.5461328030 0.3737412989 - 0.6288506985 0.5500105023 0.3775792122 - 0.6323326230 0.5538635254 0.3814140856 - 0.6357542872 0.5576797724 0.3852416873 - 0.6391350031 0.5614629984 0.3890604079 - 0.6424909234 0.5652248263 0.3928706050 - 0.6457951069 0.5689184070 0.3966498971 - 0.6490675807 0.5725790858 0.4004212022 - 0.6523084044 0.5761985779 0.4041798115 - 0.6554991007 0.5797787905 0.4079318047 - 0.6586679220 0.5833429098 0.4116778970 - 0.6617909074 0.5868695974 0.4154145122 - 0.6648769975 0.5903773904 0.4191451073 - 0.6679384708 0.5938647985 0.4228456020 - 0.6709473133 0.5972957015 0.4265331924 - 0.6739342213 0.6007062197 0.4302001894 - 0.6768646836 0.6040558815 0.4338527918 - 0.6797602177 0.6073855162 0.4374971986 - 0.6826251745 0.6106783748 0.4411129951 - 0.6854385138 0.6139551997 0.4447222054 - 0.6882362962 0.6172215939 0.4483056068 - 0.6909871101 0.6204239726 0.4518764913 - 0.6937232018 0.6236106753 0.4554122090 - 0.6964259148 0.6267350912 0.4589309096 - 0.6990894079 0.6298514009 0.4624271989 - 0.7017171979 0.6329439878 0.4658999145 - 0.7042943835 0.6360031962 0.4693557024 - 0.7068523169 0.6390181780 0.4727905095 - 0.7093623877 0.6420083046 0.4762170017 - 0.7118644714 0.6449810863 0.4796274900 - 0.7143244743 0.6479082704 0.4830344915 - 0.7167754769 0.6508182287 0.4864130020 - 0.7191916108 0.6536681056 0.4897888005 - 0.7215893865 0.6565065980 0.4931452870 - 0.7239490151 0.6593149900 0.4965006113 - 0.7262917757 0.6621205807 0.4998255074 - 0.7286117077 0.6648774147 0.5031486154 - 0.7308964729 0.6676303744 0.5064402819 - 0.7331464291 0.6703311205 0.5097302794 - 0.7353671193 0.6730266809 0.5129926205 - 0.7375649214 0.6756759286 0.5162526965 - 0.7397302985 0.6783176064 0.5194661021 - 0.7418738008 0.6808996201 0.5226792097 - 0.7439814806 0.6834768057 0.5258497000 - 0.7460665703 0.6860129833 0.5290201902 - 0.7481163740 0.6885464191 0.5321524143 - 0.7501432896 0.6910448074 0.5352824926 - 0.7521458268 0.6935424209 0.5383893847 - 0.7541307211 0.6960023046 0.5414927006 - 0.7560967207 0.6984623075 0.5445662737 - 0.7580459714 0.7008609176 0.5476304889 - 0.7599691153 0.7032560110 0.5506601930 - 0.7618615031 0.7055974007 0.5536696911 - 0.7637408972 0.7079290748 0.5566582084 - 0.7655979991 0.7102227807 0.5596238971 - 0.7674412727 0.7125021815 0.5625798106 - 0.7692431808 0.7147536278 0.5655139089 - 0.7710394859 0.7169851065 0.5684432983 - 0.7727897167 0.7192016244 0.5713353753 - 0.7745398879 0.7213971019 0.5742275119 - 0.7762631774 0.7235780954 0.5770719051 - 0.7779850960 0.7257089019 0.5799102783 - 0.7796735764 0.7278391123 0.5827142000 - 0.7813516259 0.7299191952 0.5854988098 - 0.7830191255 0.7319993973 0.5882691145 - 0.7846779227 0.7340465784 0.5910149813 - 0.7863258123 0.7360842228 0.5937579870 - 0.7879499197 0.7381075025 0.5964655280 - 0.7895722985 0.7401170731 0.5991730094 - 0.7911682725 0.7421147823 0.6018517017 - 0.7927643061 0.7440714836 0.6045210958 - 0.7943251133 0.7460281849 0.6071749926 - 0.7958757281 0.7479510903 0.6098067760 - 0.7974109054 0.7498708963 0.6124358773 - 0.7989286184 0.7517752051 0.6150038242 - 0.8004400134 0.7536681890 0.6175717711 - 0.8019111753 0.7555513978 0.6200997233 - 0.8033823967 0.7573989034 0.6226056814 - 0.8048381209 0.7592465281 0.6251029968 - 0.8062893748 0.7610545754 0.6275702119 - 0.8077279925 0.7628545165 0.6300374269 - 0.8091471791 0.7646414042 0.6324740052 - 0.8105664849 0.7664098144 0.6349017024 - 0.8119462729 0.7681781054 0.6373167038 - 0.8133255243 0.7698963284 0.6397023201 - 0.8146849871 0.7716131806 0.6420879960 - 0.8160281777 0.7733101845 0.6444455981 - 0.8173695207 0.7749879956 0.6467962861 - 0.8186913133 0.7766653895 0.6491395235 - 0.8200129867 0.7783104777 0.6514626145 - 0.8213170171 0.7799556255 0.6537855864 - 0.8226091266 0.7815762162 0.6560755968 - 0.8238981962 0.7831717730 0.6583523750 - 0.8251538873 0.7847673893 0.6606240273 - 0.8264095783 0.7863461971 0.6628612876 - 0.8276506066 0.7879241705 0.6650987267 - 0.8288792968 0.7894842029 0.6673101187 - 0.8301079869 0.7910140753 0.6694974899 - 0.8313009739 0.7925441265 0.6716848016 - 0.8324930072 0.7940390110 0.6738216877 - 0.8336700201 0.7955235243 0.6759517193 - 0.8348218799 0.7970044017 0.6780722141 - 0.8359737992 0.7984637022 0.6801573038 - 0.8371133804 0.7999228835 0.6822422743 - 0.8382487893 0.8013638258 0.6843022108 - 0.8393824100 0.8027846813 0.6863384247 - 0.8404983878 0.8042055964 0.6883745193 - 0.8416143060 0.8055989742 0.6903743148 - 0.8427138925 0.8069851995 0.6923640966 - 0.8437898159 0.8083692193 0.6943526864 - 0.8448657990 0.8097262979 0.6963012815 - 0.8459321260 0.8110834956 0.6982498765 - 0.8469941020 0.8124284744 0.7001879215 - 0.8480560780 0.8137484789 0.7020930052 - 0.8491019011 0.8150684834 0.7039980888 - 0.8501470089 0.8163607121 0.7058871984 - 0.8511853814 0.8176304102 0.7077549100 - 0.8521944284 0.8189001083 0.7096226215 - 0.8532034159 0.8201503158 0.7114666104 - 0.8542025089 0.8213939071 0.7132933736 - 0.8551856279 0.8226374984 0.7151201963 - 0.8561686873 0.8238468766 0.7169187069 - 0.8571283221 0.8250527978 0.7187049985 - 0.8580685258 0.8262571096 0.7204912901 - 0.8590087295 0.8274198174 0.7222427130 - 0.8599494100 0.8285825253 0.7239848971 - 0.8608903289 0.8297395110 0.7257270217 - 0.8618311882 0.8308607936 0.7274336815 - 0.8627600074 0.8319821954 0.7291340828 - 0.8636854887 0.8330979943 0.7308344841 - 0.8646110892 0.8341925740 0.7325049043 - 0.8655208945 0.8352872729 0.7341710925 - 0.8664281964 0.8363729715 0.7358372808 - 0.8673354983 0.8374316096 0.7374768853 - 0.8682217002 0.8384901285 0.7391127944 - 0.8691056967 0.8395386934 0.7407485843 - 0.8699898124 0.8405579925 0.7423660159 - 0.8708515763 0.8415772915 0.7439796925 - 0.8717116117 0.8425924182 0.7455934882 - 0.8725715280 0.8435931206 0.7471752763 - 0.8734114170 0.8445938230 0.7487472296 - 0.8742489219 0.8455908895 0.7503191829 - 0.8750864267 0.8465701938 0.7518702149 - 0.8759170175 0.8475493789 0.7534102798 - 0.8767464161 0.8485258222 0.7549505234 - 0.8775756955 0.8494656086 0.7564716935 - 0.8783869743 0.8504052758 0.7579756975 - 0.8791924715 0.8513450027 0.7594797015 - 0.8799980283 0.8522825241 0.7609726191 - 0.8807792068 0.8532198071 0.7624449730 - 0.8815463185 0.8541572094 0.7639175057 - 0.8823133707 0.8550791144 0.7653834224 - 0.8830744028 0.8559969068 0.7668110132 - 0.8838288188 0.8569146991 0.7682386041 - 0.8845831156 0.8578169942 0.7696663141 - 0.8853343725 0.8587086201 0.7710726857 - 0.8860781193 0.8596001863 0.7724769711 - 0.8868216872 0.8604816794 0.7738813162 - 0.8875637054 0.8613451123 0.7752763033 - 0.8882815242 0.8622086048 0.7766659856 - 0.8889992833 0.8630689979 0.7780556083 - 0.8897172213 0.8639014959 0.7794315815 - 0.8904141784 0.8647338748 0.7807787061 - 0.8911063075 0.8655663729 0.7821257710 - 0.8917983770 0.8663951159 0.7834728956 - 0.8924919963 0.8672229052 0.7847924829 - 0.8931869864 0.8680506945 0.7861111760 - 0.8938819766 0.8688691258 0.7874299884 - 0.8945751190 0.8696764112 0.7887393832 - 0.8952596188 0.8704836965 0.7900424004 - 0.8959441185 0.8712878227 0.7913452983 - 0.8966286182 0.8720598817 0.7926430702 - 0.8972942233 0.8728318810 0.7939155102 - 0.8979557157 0.8736039996 0.7951877713 - 0.8986173272 0.8743599057 0.7964602113 - 0.8992745876 0.8751083016 0.7977172136 - 0.8999263048 0.8758566976 0.7989680767 - 0.9005779028 0.8766024709 0.8002188802 - 0.9012296200 0.8773398995 0.8014593720 - 0.9018729925 0.8780772090 0.8026626706 - 0.9025161862 0.8788145781 0.8038660288 - 0.9031593800 0.8795428872 0.8050693274 - 0.9037922025 0.8802682757 0.8062509894 - 0.9044145942 0.8809937239 0.8074226975 - 0.9050371051 0.8817160130 0.8085945249 - 0.9056594968 0.8824285269 0.8097618222 - 0.9062777162 0.8831409812 0.8108941913 - 0.9068958759 0.8838534951 0.8120266199 - 0.9075139761 0.8845480084 0.8131589890 - 0.9081308842 0.8852338791 0.8142759204 - 0.9087464213 0.8859198093 0.8153775930 - 0.9093617797 0.8866025805 0.8164793253 - 0.9099771976 0.8872501254 0.8175811172 - 0.9105805159 0.8878976703 0.8186519146 - 0.9111816287 0.8885452151 0.8197175860 - 0.9117826819 0.8891865015 0.8207833171 - 0.9123781919 0.8898199797 0.8218469024 - 0.9129536748 0.8904533982 0.8229005933 - 0.9135290980 0.8910868764 0.8239542246 - 0.9141045809 0.8917121887 0.8250077963 - 0.9146608114 0.8923351169 0.8260518909 - 0.9152011871 0.8929579854 0.8270838261 - 0.9157415032 0.8935797215 0.8281157017 - 0.9162818789 0.8941801190 0.8291476965 - 0.9168179035 0.8947805762 0.8301671743 - 0.9173532128 0.8953809738 0.8311806917 - 0.9178884029 0.8959742785 0.8321940899 - 0.9184231758 0.8965504169 0.8332076073 - 0.9189543128 0.8971264958 0.8341963291 - 0.9194852710 0.8977025747 0.8351811171 - 0.9200162888 0.8982679844 0.8361659050 - 0.9205431938 0.8988227844 0.8371505141 - 0.9210606217 0.8993775249 0.8381142020 - 0.9215779901 0.8999322057 0.8390778899 - 0.9220954180 0.9004656076 0.8400415778 - 0.9226071239 0.9009882212 0.8410019279 - 0.9231125712 0.9015107751 0.8419361115 - 0.9236180782 0.9020333290 0.8428702950 - 0.9241235256 0.9025334716 0.8438045979 - 0.9246153235 0.9030274749 0.8447362781 - 0.9250978231 0.9035214782 0.8456568718 - 0.9255803227 0.9040154815 0.8465775847 - 0.9260628223 0.9045063853 0.8474981785 - 0.9265328050 0.9049968123 0.8484144211 - 0.9269970059 0.9054871798 0.8493136764 - 0.9274612069 0.9059776068 0.8502129912 - 0.9279254079 0.9064562917 0.8511123061 - 0.9283785820 0.9069336057 0.8520066738 - 0.9288275838 0.9074109793 0.8528792262 - 0.9292767048 0.9078882933 0.8537517190 - 0.9297257066 0.9083464146 0.8546240926 - 0.9301574826 0.9088020921 0.8554927111 - 0.9305828214 0.9092578888 0.8563321233 - 0.9310081005 0.9097136259 0.8571714759 - 0.9314333797 0.9101629257 0.8580110073 - 0.9318456054 0.9106109142 0.8588501811 - 0.9322518110 0.9110590219 0.8596740961 - 0.9326580763 0.9115070105 0.8604980111 - 0.9330642819 0.9119517207 0.8613219261 - 0.9334682822 0.9123954177 0.8621457815 - 0.9338707924 0.9128389955 0.8629440069 - 0.9342733026 0.9132825732 0.8637380004 - 0.9346758127 0.9137256145 0.8645321131 - 0.9350727201 0.9141682982 0.8653261065 - 0.9354630113 0.9146109819 0.8661041856 - 0.9358533025 0.9150536060 0.8668742180 - 0.9362437129 0.9154921770 0.8676440716 - 0.9366282821 0.9159253836 0.8684141040 - 0.9369992018 0.9163584709 0.8691806197 - 0.9373700023 0.9167916179 0.8699421883 - 0.9377409220 0.9172230959 0.8707038164 - 0.9381108284 0.9176480174 0.8714653850 - 0.9384678006 0.9180728793 0.8722239733 - 0.9388247728 0.9184976816 0.8729615808 - 0.9391818047 0.9189226031 0.8736993074 - 0.9395387769 0.9193425775 0.8744369149 - 0.9398884177 0.9197623134 0.8751745820 - 0.9402360916 0.9201819897 0.8758922219 - 0.9405838847 0.9206016064 0.8766046166 - 0.9409316182 0.9210132957 0.8773170114 - 0.9412720203 0.9214196801 0.8780294061 - 0.9416047931 0.9218261838 0.8787423968 - 0.9419375062 0.9222326279 0.8794564009 - 0.9422702193 0.9226341248 0.8801705241 - 0.9426016808 0.9230197072 0.8808845282 - 0.9429249763 0.9234051704 0.8815984726 - 0.9432482719 0.9237906933 0.8823022246 - 0.9435716271 0.9241762161 0.8830056190 - 0.9438948035 0.9245483875 0.8837090731 - 0.9442144036 0.9249175191 0.8844125271 - 0.9445325136 0.9252867103 0.8851082921 - 0.9448506236 0.9256557822 0.8857964277 - 0.9451687932 0.9260243177 0.8864845037 - 0.9454866052 0.9263916016 0.8871726990 - 0.9458038807 0.9267588258 0.8878607750 - 0.9461210966 0.9271261096 0.8885198236 - 0.9464383721 0.9274932742 0.8891779184 - 0.9467555881 0.9278637767 0.8898360729 - 0.9470669031 0.9282348752 0.8904942274 - 0.9473763108 0.9286059737 0.8911436796 - 0.9476857185 0.9289770126 0.8917800188 - 0.9479951262 0.9293472171 0.8924162984 - 0.9483039975 0.9297147989 0.8930525780 - 0.9486111999 0.9300825000 0.8936889172 - 0.9489184022 0.9304502010 0.8942996860 - 0.9492256045 0.9308179021 0.8949031234 - 0.9495326877 0.9311780930 0.8955063820 - 0.9498348832 0.9315345883 0.8961097002 - 0.9501338005 0.9318912029 0.8967118263 - 0.9504327774 0.9322476983 0.8973019719 - 0.9507316947 0.9326038957 0.8978921175 - 0.9510306716 0.9329422712 0.8984823227 - 0.9513161778 0.9332805872 0.8990725279 - 0.9516012073 0.9336189032 0.8996539712 - 0.9518861771 0.9339572787 0.9002208710 - 0.9521712065 0.9342914224 0.9007877707 - 0.9524512291 0.9346157908 0.9013546109 - 0.9527168274 0.9349402189 0.9019215107 - 0.9529824257 0.9352647066 0.9024795890 - 0.9532480240 0.9355890751 0.9030315876 - 0.9535136223 0.9359090924 0.9035837054 - 0.9537726045 0.9362252951 0.9041357040 - 0.9540249705 0.9365414977 0.9046877027 - 0.9542772770 0.9368577003 0.9052293897 - 0.9545297027 0.9371739030 0.9057677984 - 0.9547821283 0.9374797940 0.9063060880 - 0.9550374746 0.9377818108 0.9068444967 - 0.9552941918 0.9380838871 0.9073829055 - 0.9555509090 0.9383859038 0.9079146981 - 0.9558076262 0.9386879206 0.9084457159 - 0.9560642838 0.9389883280 0.9089766145 - 0.9563276768 0.9392884970 0.9095075727 - 0.9565923810 0.9395886064 0.9100385904 - 0.9568570852 0.9398887157 0.9105516076 - 0.9571216702 0.9401888251 0.9110634923 - 0.9573863745 0.9404814839 0.9115753770 - 0.9576531053 0.9407737255 0.9120873809 - 0.9579198956 0.9410659075 0.9125993252 - 0.9581868052 0.9413580894 0.9130939245 - 0.9584535956 0.9416502714 0.9135876894 - 0.9587203860 0.9419289231 0.9140815139 - 0.9589838982 0.9422072768 0.9145752788 - 0.9592472911 0.9424856901 0.9150691032 - 0.9595106244 0.9427641034 0.9155386090 - 0.9597740173 0.9430423975 0.9160056710 - 0.9600374103 0.9433137178 0.9164727926 - 0.9602922201 0.9435846806 0.9169399142 - 0.9605466127 0.9438555837 0.9174069762 - 0.9608010054 0.9441264868 0.9178671837 - 0.9610555172 0.9443975091 0.9183259010 - 0.9613099098 0.9446647167 0.9187846184 - 0.9615522027 0.9449313283 0.9192432165 - 0.9617928863 0.9451979995 0.9197018743 - 0.9620336294 0.9454646707 0.9201496840 - 0.9622743130 0.9457312822 0.9205917716 - 0.9625151157 0.9459909201 0.9210339785 - 0.9627497196 0.9462481141 0.9214761853 - 0.9629824162 0.9465051889 0.9219183922 - 0.9632151127 0.9467623830 0.9223541021 - 0.9634478092 0.9470195174 0.9227821827 - 0.9636805058 0.9472687244 0.9232103229 - 0.9639062881 0.9475113153 0.9236382842 - 0.9641271234 0.9477539062 0.9240664244 - 0.9643478990 0.9479964972 0.9244915843 - 0.9645686746 0.9482392073 0.9249045253 - 0.9647895098 0.9484791160 0.9253175259 - 0.9650083780 0.9487127066 0.9257304072 - 0.9652239084 0.9489462972 0.9261432886 - 0.9654393196 0.9491798878 0.9265562296 - 0.9656546712 0.9494134784 0.9269577861 - 0.9658702016 0.9496471286 0.9273576736 - 0.9660856128 0.9498767257 0.9277576208 - 0.9663015008 0.9501063228 0.9281575084 - 0.9665173888 0.9503358006 0.9285573959 - 0.9667332768 0.9505653977 0.9289472103 - 0.9669492245 0.9507949948 0.9293261766 - 0.9671651125 0.9510244727 0.9297050834 - 0.9673789144 0.9512541294 0.9300841093 - 0.9675917029 0.9514836073 0.9304630756 - 0.9678044915 0.9517130852 0.9308412075 - 0.9680173993 0.9519426823 0.9312015176 - 0.9682301879 0.9521713257 0.9315618277 - 0.9684410095 0.9523969293 0.9319220185 - 0.9686465859 0.9526224732 0.9322823286 - 0.9688522816 0.9528480172 0.9326425791 - 0.9690579176 0.9530736208 0.9330028892 - 0.9692634940 0.9532991052 0.9333631992 - 0.9694690704 0.9535260201 0.9337235093 - 0.9696654081 0.9537532926 0.9340837002 - 0.9698594809 0.9539806843 0.9344440103 - 0.9700536728 0.9542080164 0.9348043203 - 0.9702479243 0.9544352889 0.9351575971 - 0.9704419971 0.9546620846 0.9355108738 - 0.9706341028 0.9548869133 0.9358642101 - 0.9708212018 0.9551116228 0.9362174273 - 0.9710081816 0.9553362727 0.9365707040 - 0.9711952806 0.9555611014 0.9369189143 - 0.9713823795 0.9557858109 0.9372587800 - 0.9715694189 0.9560061097 0.9375988245 - 0.9717484713 0.9562234282 0.9379386902 - 0.9719241261 0.9564408064 0.9382786155 - 0.9720997214 0.9566581845 0.9386184812 - 0.9722753167 0.9568755031 0.9389562011 - 0.9724509120 0.9570928812 0.9392927885 - 0.9726263285 0.9572992921 0.9396293759 - 0.9727976918 0.9575052261 0.9399660826 - 0.9729689956 0.9577109814 0.9403027296 - 0.9731404185 0.9579169154 0.9406393766 - 0.9733117223 0.9581226707 0.9409670830 - 0.9734830856 0.9583271146 0.9412943721 - 0.9736548066 0.9585263133 0.9416217208 - 0.9738274217 0.9587255120 0.9419491291 - 0.9739999175 0.9589247108 0.9422764182 - 0.9741724133 0.9591240287 0.9426026940 - 0.9743450284 0.9593232274 0.9429237247 - 0.9745175242 0.9595177770 0.9432445765 - 0.9746888876 0.9597066045 0.9435654879 - 0.9748594165 0.9598953724 0.9438865185 - 0.9750298858 0.9600841999 0.9442073703 - 0.9752004147 0.9602730274 0.9445272088 - 0.9753708839 0.9604617953 0.9448443055 - 0.9755414724 0.9606478810 0.9451612830 - 0.9757080078 0.9608321786 0.9454783201 - 0.9758731723 0.9610164762 0.9457954168 - 0.9760383964 0.9612007737 0.9461123943 - 0.9762036204 0.9613850713 0.9464284778 - 0.9763687849 0.9615694284 0.9467430115 - 0.9765340090 0.9617481232 0.9470574856 - 0.9766919017 0.9619243741 0.9473719001 - 0.9768484235 0.9621006846 0.9476863742 - 0.9770050049 0.9622771144 0.9480009079 - 0.9771615267 0.9624534249 0.9483118057 - 0.9773181081 0.9626296759 0.9486168027 - 0.9774745703 0.9628033042 0.9489219189 - 0.9776207805 0.9629759192 0.9492269158 - 0.9777655005 0.9631485939 0.9495319128 - 0.9779102802 0.9633212090 0.9498370290 - 0.9780550003 0.9634938240 0.9501398802 - 0.9781997800 0.9636663795 0.9504382014 - 0.9783445001 0.9638398290 0.9507365227 - 0.9784767032 0.9640133977 0.9510347843 - 0.9786067009 0.9641870856 0.9513331056 - 0.9787368178 0.9643607736 0.9516314268 - 0.9788668752 0.9645345211 0.9519273043 - 0.9789968729 0.9647082090 0.9522131085 - 0.9791269898 0.9648808241 0.9524989128 - 0.9792442918 0.9650526047 0.9527845979 - 0.9793577790 0.9652245045 0.9530704021 - 0.9794713259 0.9653962851 0.9533562064 - 0.9795848131 0.9655681849 0.9536418915 - 0.9796983004 0.9657400250 0.9539101720 - 0.9798117280 0.9659097195 0.9541785121 - 0.9799193740 0.9660761952 0.9544466734 - 0.9800233841 0.9662427902 0.9547150135 - 0.9801273942 0.9664093852 0.9549832940 - 0.9802314043 0.9665759802 0.9552515745 - 0.9803354144 0.9667425752 0.9555094838 - 0.9804394245 0.9669076800 0.9557638168 - 0.9805418849 0.9670665264 0.9560182095 - 0.9806420207 0.9672253132 0.9562726021 - 0.9807420969 0.9673839808 0.9565269947 - 0.9808421731 0.9675428271 0.9567813873 - 0.9809423089 0.9677016139 0.9570345283 - 0.9810423851 0.9678602815 0.9572860003 - 0.9811419249 0.9680219889 0.9575374126 - 0.9812365174 0.9681841135 0.9577888250 - 0.9813309908 0.9683461785 0.9580402970 - 0.9814255834 0.9685081840 0.9582917094 - 0.9815201163 0.9686703086 0.9585428834 - 0.9816147089 0.9688323736 0.9587876201 - 0.9817091823 0.9689944983 0.9590322971 - 0.9817987084 0.9691566229 0.9592769146 - 0.9818863273 0.9693186879 0.9595215917 - 0.9819738865 0.9694808125 0.9597663283 - 0.9820615053 0.9696428776 0.9600110054 - 0.9821491241 0.9698050022 0.9602541924 - 0.9822366834 0.9699668884 0.9604964256 - 0.9823241830 0.9701260924 0.9607385993 - 0.9824113846 0.9702852964 0.9609807730 - 0.9824985862 0.9704445004 0.9612230062 - 0.9825857878 0.9706035852 0.9614651203 - 0.9826729298 0.9707627892 0.9617071152 - 0.9827600718 0.9709219933 0.9619415998 - 0.9828472733 0.9710779786 0.9621762037 - 0.9829329252 0.9712315798 0.9624108076 - 0.9830182195 0.9713851213 0.9626452923 - 0.9831033945 0.9715387225 0.9628798962 - 0.9831886888 0.9716922045 0.9631145000 - 0.9832739234 0.9718458056 0.9633466005 - 0.9833592176 0.9719992876 0.9635754228 - 0.9834436178 0.9721450210 0.9638043046 - 0.9835252166 0.9722905159 0.9640331864 - 0.9836068749 0.9724360704 0.9642621279 - 0.9836885929 0.9725816250 0.9644908905 - 0.9837701917 0.9727271199 0.9647197723 - 0.9838519096 0.9728726149 0.9649403095 - 0.9839335084 0.9730172753 0.9651581049 - 0.9840176105 0.9731602073 0.9653759003 - 0.9841032028 0.9733030796 0.9655936956 - 0.9841887951 0.9734460711 0.9658116102 - 0.9842743874 0.9735890031 0.9660294056 - 0.9843600988 0.9737318754 0.9662470818 - 0.9844456911 0.9738749266 0.9664586186 - 0.9845312834 0.9740148783 0.9666702151 - 0.9846189022 0.9741529226 0.9668816924 - 0.9847068191 0.9742910266 0.9670932293 - 0.9847946167 0.9744290709 0.9673047066 - 0.9848824739 0.9745671749 0.9675163031 - 0.9849703908 0.9747052193 0.9677258134 - 0.9850581884 0.9748433232 0.9679268003 - 0.9851459861 0.9749873877 0.9681277871 - 0.9852334261 0.9751328230 0.9683287740 - 0.9853208065 0.9752783179 0.9685298204 - 0.9854081869 0.9754236937 0.9687308073 - 0.9854956269 0.9755691886 0.9689317942 - 0.9855828881 0.9757146835 0.9691283703 - 0.9856703281 0.9758601189 0.9693145752 - 0.9857584238 0.9760093093 0.9695008993 - 0.9858487844 0.9761587977 0.9696871042 - 0.9859390855 0.9763082862 0.9698734283 - 0.9860295057 0.9764577746 0.9700595737 - 0.9861198068 0.9766073227 0.9702458978 - 0.9862102270 0.9767568111 0.9704262018 - 0.9863005280 0.9769061804 0.9705942273 - 0.9863908887 0.9770559072 0.9707623124 - 0.9864816070 0.9772055745 0.9709302783 - 0.9865723252 0.9773551822 0.9710983038 - 0.9866629839 0.9775049090 0.9712663889 - 0.9867537022 0.9776545763 0.9714344144 - 0.9868444204 0.9778043032 0.9715989828 - 0.9869350195 0.9779539108 0.9717546105 - 0.9870250821 0.9781007171 0.9719101787 - 0.9871137738 0.9782474041 0.9720658064 - 0.9872025251 0.9783940911 0.9722213745 - 0.9872910976 0.9785407782 0.9723770022 - 0.9873797894 0.9786874056 0.9725325704 - 0.9874684811 0.9788340926 0.9726879001 - 0.9875571132 0.9789807796 0.9728415012 - 0.9876449704 0.9791290164 0.9729952216 - 0.9877302051 0.9792773128 0.9731488824 - 0.9878153205 0.9794257283 0.9733024836 - 0.9879004955 0.9795740843 0.9734562039 - 0.9879856110 0.9797223806 0.9736099243 - 0.9880707860 0.9798707962 0.9737635255 - 0.9881559014 0.9800190926 0.9739124179 - 0.9882411957 0.9801667929 0.9740610719 - 0.9883286953 0.9803141952 0.9742097855 - 0.9884161949 0.9804617167 0.9743584991 - 0.9885035753 0.9806091189 0.9745070934 - 0.9885910749 0.9807565212 0.9746558070 - 0.9886785746 0.9809039235 0.9748045206 - 0.9887660146 0.9810513258 0.9749522209 - 0.9888535142 0.9811971784 0.9750993252 - 0.9889419079 0.9813411236 0.9752464890 - 0.9890304208 0.9814851284 0.9753937125 - 0.9891189933 0.9816290736 0.9755408764 - 0.9892076254 0.9817730784 0.9756880999 - 0.9892961979 0.9819170237 0.9758353233 - 0.9893847108 0.9820610285 0.9759811163 - 0.9894732833 0.9822043180 0.9761242867 - 0.9895619154 0.9823424220 0.9762675166 - 0.9896506071 0.9824805260 0.9764106870 - 0.9897391796 0.9826186895 0.9765539169 - 0.9898278117 0.9827567935 0.9766970873 - 0.9899165034 0.9828948975 0.9768403172 - 0.9900050759 0.9830330014 0.9769834280 - 0.9900938272 0.9831712246 0.9771208167 - 0.9901822805 0.9833034277 0.9772574902 - 0.9902700186 0.9834333062 0.9773942232 - 0.9903578162 0.9835631847 0.9775308967 - 0.9904454947 0.9836931229 0.9776676297 - 0.9905332923 0.9838230014 0.9778043032 - 0.9906209707 0.9839528799 0.9779409766 - 0.9907088280 0.9840828180 0.9780768752 - 0.9907965064 0.9842103124 0.9782114029 - 0.9908831120 0.9843295217 0.9783458114 - 0.9909690022 0.9844487906 0.9784802794 - 0.9910550117 0.9845681190 0.9786148071 - 0.9911409020 0.9846873283 0.9787492752 - 0.9912267923 0.9848065972 0.9788838029 - 0.9913126826 0.9849258065 0.9790182114 - 0.9913986921 0.9850450754 0.9791532755 - 0.9914844036 0.9851558208 0.9792885184 - 0.9915674925 0.9852620959 0.9794237018 - 0.9916505814 0.9853683114 0.9795590043 - 0.9917337894 0.9854745269 0.9796941876 - 0.9918168783 0.9855806828 0.9798293710 - 0.9919000864 0.9856870174 0.9799646139 - 0.9919831753 0.9857931733 0.9800996780 - 0.9920663834 0.9858993888 0.9802330732 - 0.9921478033 0.9859907031 0.9803665876 - 0.9922270775 0.9860820174 0.9805001020 - 0.9923064709 0.9861732721 0.9806334972 - 0.9923858047 0.9862647057 0.9807670116 - 0.9924650788 0.9863560200 0.9809004068 - 0.9925444126 0.9864472747 0.9810339212 - 0.9926236868 0.9865385890 0.9811660051 - 0.9927030802 0.9866275787 0.9812954068 - 0.9927787781 0.9867103100 0.9814248085 - 0.9928531051 0.9867929220 0.9815542102 - 0.9929273725 0.9868754745 0.9816836119 - 0.9930018187 0.9869580865 0.9818130732 - 0.9930760860 0.9870408177 0.9819424748 - 0.9931504130 0.9871234298 0.9820718765 - 0.9932246804 0.9872059822 0.9821982980 - 0.9932990074 0.9872871041 0.9823215008 - 0.9933676124 0.9873663783 0.9824448228 - 0.9934356213 0.9874455929 0.9825680852 - 0.9935035706 0.9875249267 0.9826912880 - 0.9935715795 0.9876040816 0.9828146100 - 0.9936395884 0.9876832962 0.9829378724 - 0.9937075973 0.9877625704 0.9830611944 - 0.9937756062 0.9878417850 0.9831799269 - 0.9938433766 0.9879186749 0.9832950234 - 0.9939035773 0.9879934192 0.9834101796 - 0.9939638972 0.9880682230 0.9835252762 - 0.9940240979 0.9881430268 0.9836404920 - 0.9940842986 0.9882177711 0.9837555885 - 0.9941444993 0.9882925749 0.9838708043 - 0.9942047000 0.9883673191 0.9839859009 - 0.9942649007 0.9884421229 0.9840955734 - 0.9943246841 0.9885138273 0.9842007160 - 0.9943755865 0.9885829091 0.9843057990 - 0.9944264293 0.9886521101 0.9844108224 - 0.9944772720 0.9887213111 0.9845159054 - 0.9945281148 0.9887903929 0.9846209884 - 0.9945790172 0.9888595939 0.9847260714 - 0.9946299195 0.9889286757 0.9848312140 - 0.9946807027 0.9889978766 0.9849311113 - 0.9947316051 0.9890637994 0.9850243926 - 0.9947717190 0.9891260266 0.9851177931 - 0.9948117137 0.9891883135 0.9852110744 - 0.9948517084 0.9892506003 0.9853044748 - 0.9948915839 0.9893128872 0.9853978157 - 0.9949315786 0.9893751740 0.9854912162 - 0.9949715734 0.9894374013 0.9855846167 - 0.9950115085 0.9894996881 0.9856761098 - 0.9950515032 0.9895594120 0.9857621193 - 0.9950813055 0.9896134734 0.9858481288 - 0.9951090217 0.9896675944 0.9859341979 - 0.9951366782 0.9897217155 0.9860202074 - 0.9951645136 0.9897757769 0.9861062169 - 0.9951922297 0.9898298979 0.9861922860 - 0.9952198863 0.9898840189 0.9862782955 - 0.9952477217 0.9899380207 0.9863643050 - 0.9952753782 0.9899916053 0.9864417911 - 0.9952955842 0.9900360703 0.9865189195 - 0.9953106046 0.9900807142 0.9865959883 - 0.9953256845 0.9901251793 0.9866731763 - 0.9953408241 0.9901698232 0.9867503047 - 0.9953557849 0.9902142882 0.9868274927 - 0.9953709245 0.9902588725 0.9869046211 - 0.9953858852 0.9903033972 0.9869816899 - 0.9954010248 0.9903479815 0.9870569706 - 0.9954146743 0.9903848171 0.9871309996 - 0.9954246283 0.9904186130 0.9872050285 - 0.9954345822 0.9904522896 0.9872788787 - 0.9954444766 0.9904860854 0.9873529077 - 0.9954544902 0.9905198216 0.9874268770 - 0.9954643846 0.9905534983 0.9875009060 - 0.9954743981 0.9905872941 0.9875748754 - 0.9954842925 0.9906209707 0.9876483083 - 0.9954943061 0.9906514287 0.9877181053 - 0.9954999089 0.9906733036 0.9877879024 - 0.9955047965 0.9906951189 0.9878576994 - 0.9955096245 0.9907169938 0.9879274964 - 0.9955145121 0.9907388091 0.9879974127 - 0.9955192804 0.9907606840 0.9880672097 - 0.9955242276 0.9907824993 0.9881370068 - 0.9955291152 0.9908043742 0.9882068038 - 0.9955338836 0.9908261895 0.9882770777 - 0.9955371022 0.9908388257 0.9883475900 - 0.9955371022 0.9908484817 0.9884182215 - 0.9955371022 0.9908581972 0.9884887934 - 0.9955371022 0.9908677936 0.9885594249 - 0.9955371022 0.9908775091 0.9886299968 - 0.9955371022 0.9908872247 0.9887005091 - 0.9955371022 0.9908968806 0.9887710810 - 0.9955371022 0.9909064770 0.9888417125 - 0.9955371022 0.9909154773 0.9889119267 - 0.9955376983 0.9909201860 0.9889820218 - 0.9955384135 0.9909248948 0.9890521765 - 0.9955391884 0.9909297228 0.9891223907 - 0.9955399036 0.9909343719 0.9891924858 - 0.9955406189 0.9909390807 0.9892627001 - 0.9955413938 0.9909437895 0.9893327951 - 0.9955421090 0.9909486175 0.9894030094 - 0.9955428839 0.9909533262 0.9894726872 - 0.9955435991 0.9909560084 0.9895405769 - 0.9955443740 0.9909560084 0.9896085262 - 0.9955450892 0.9909560084 0.9896764159 - 0.9955459237 0.9909560084 0.9897443056 - 0.9955465794 0.9909560084 0.9898123145 - 0.9955472946 0.9909560084 0.9898802042 - 0.9955481291 0.9909560084 0.9899480939 - 0.9955487847 0.9909560084 0.9900159836 - 0.9955496192 0.9909560084 0.9900823832 - 0.9955502748 0.9909573793 0.9901462197 - 0.9955511093 0.9909597039 0.9902099967 - 0.9955518246 0.9909620285 0.9902737141 - 0.9955524802 0.9909642935 0.9903374910 - 0.9955533147 0.9909666181 0.9904012084 - 0.9955539703 0.9909688830 0.9904649854 - 0.9955548048 0.9909712076 0.9905287027 - 0.9955555201 0.9909734726 0.9905924797 - 0.9955562949 0.9909757972 0.9906535745 - 0.9955570102 0.9909781218 0.9907112718 - 0.9955577850 0.9909803867 0.9907689095 - 0.9955585003 0.9909827113 0.9908266068 - 0.9955592155 0.9909849763 0.9908841848 - 0.9955599904 0.9909873009 0.9909418821 - 0.9955607057 0.9909896255 0.9909995198 - 0.9955614805 0.9909918904 0.9910572171 - 0.9955621958 0.9909942150 0.9911147952 - 0.9955629706 0.9909964800 0.9911692739 - 0.9955636859 0.9910038114 0.9912189841 - 0.9955644011 0.9910129905 0.9912686944 - 0.9955651760 0.9910221100 0.9913184047 - 0.9955658913 0.9910312891 0.9913681149 - 0.9955667257 0.9910405278 0.9914177060 - 0.9955673814 0.9910495877 0.9914674163 - 0.9955682158 0.9910588264 0.9915171266 - 0.9955688715 0.9910680056 0.9915667772 - 0.9955697060 0.9910771251 0.9916139841 - 0.9955704212 0.9910891056 0.9916545749 - 0.9955710769 0.9911026955 0.9916951060 - 0.9955719113 0.9911162853 0.9917356968 - 0.9955726266 0.9911298752 0.9917762280 - 0.9955734015 0.9911434054 0.9918168187 - 0.9955741167 0.9911569953 0.9918574095 - 0.9955748916 0.9911705852 0.9918978810 - 0.9955756068 0.9911841750 0.9919384718 - 0.9955763221 0.9911978245 0.9919788837 - 0.9955770969 0.9912132025 0.9920160770 - 0.9955778122 0.9912310243 0.9920532703 - 0.9955785871 0.9912489057 0.9920905232 - 0.9955793023 0.9912667274 0.9921277165 - 0.9955800772 0.9912844896 0.9921649098 - 0.9955807924 0.9913023114 0.9922021031 - 0.9955816269 0.9913200736 0.9922392964 - 0.9955822825 0.9913378954 0.9922764897 - 0.9955829978 0.9913557172 0.9923136830 - 0.9955837727 0.9913741946 0.9923480153 - 0.9955844879 0.9913958907 0.9923812747 - 0.9955853224 0.9914177060 0.9924144745 - 0.9955859780 0.9914395213 0.9924477935 - 0.9955868125 0.9914612770 0.9924809933 - 0.9955875278 0.9914830923 0.9925143123 - 0.9955881834 0.9915049076 0.9925475121 - 0.9955890179 0.9915266037 0.9925807118 - 0.9955896735 0.9915484190 0.9926139712 - 0.9955905080 0.9915701747 0.9926487803 - 0.9955912232 0.9915946722 0.9926875234 - 0.9955919981 0.9916201234 0.9927260876 - 0.9955927134 0.9916453958 0.9927647710 - 0.9955934882 0.9916707873 0.9928033948 - 0.9955942035 0.9916961193 0.9928420782 - 0.9955949187 0.9917215109 0.9928807020 - 0.9955956936 0.9917467833 0.9929193854 - 0.9955964088 0.9917721748 0.9929580092 - 0.9955971837 0.9917975068 0.9929966927 - 0.9955980182 0.9918254018 0.9930391908 - 0.9956101775 0.9918603897 0.9930828214 - 0.9956223965 0.9918953180 0.9931263924 - 0.9956346154 0.9919303060 0.9931700826 - 0.9956468940 0.9919652939 0.9932137132 - 0.9956591129 0.9920002222 0.9932574034 - 0.9956712723 0.9920352101 0.9933009744 - 0.9956834912 0.9920700788 0.9933447242 - 0.9956957102 0.9921051264 0.9933882952 - 0.9957079291 0.9921399951 0.9934325218 - 0.9957221746 0.9921805263 0.9934805036 - 0.9957391024 0.9922237992 0.9935284853 - 0.9957559705 0.9922670722 0.9935765266 - 0.9957728982 0.9923102856 0.9936243892 - 0.9957898259 0.9923536181 0.9936723709 - 0.9958066940 0.9923968911 0.9937204123 - 0.9958236814 0.9924401045 0.9937682748 - 0.9958406091 0.9924833775 0.9938163161 - 0.9958574772 0.9925267100 0.9938642979 - 0.9958744049 0.9925699234 0.9939137101 - 0.9958940744 0.9926193953 0.9939653277 - 0.9959146976 0.9926689863 0.9940168262 - 0.9959353805 0.9927186966 0.9940683842 - 0.9959560037 0.9927682877 0.9941200018 - 0.9959766865 0.9928179979 0.9941715002 - 0.9959973097 0.9928675890 0.9942231178 - 0.9960179925 0.9929172993 0.9942746162 - 0.9960386157 0.9929668903 0.9943261743 - 0.9960592985 0.9930166006 0.9943777919 - 0.9960799217 0.9930669069 0.9944308996 - 0.9961034060 0.9931212068 0.9944851995 - 0.9961270094 0.9931753874 0.9945394993 - 0.9961506128 0.9932296276 0.9945937991 - 0.9961740971 0.9932839274 0.9946479797 - 0.9961977005 0.9933381081 0.9947022796 - 0.9962213039 0.9933922887 0.9947565794 - 0.9962449074 0.9934465885 0.9948108792 - 0.9962685108 0.9935008287 0.9948651791 - 0.9962921143 0.9935550094 0.9949194789 - 0.9963157773 0.9936100245 0.9949749112 - 0.9963418245 0.9936673045 0.9950308800 - 0.9963676929 0.9937245846 0.9950869083 - 0.9963936210 0.9937819242 0.9951428771 - 0.9964196086 0.9938392043 0.9951989055 - 0.9964454770 0.9938964844 0.9952549934 - 0.9964715242 0.9939538240 0.9953110218 - 0.9964973927 0.9940111041 0.9953669906 - 0.9965233207 0.9940683842 0.9954230189 - 0.9965493083 0.9941257238 0.9954789877 - 0.9965752959 0.9941834211 0.9955353737 - 0.9966030717 0.9942424893 0.9955919981 - 0.9966309071 0.9943016768 0.9956486225 - 0.9966586828 0.9943608046 0.9957051873 - 0.9966865182 0.9944199920 0.9957618713 - 0.9967144132 0.9944791794 0.9958184958 - 0.9967421889 0.9945383072 0.9958751202 - 0.9967700243 0.9945974946 0.9959316850 - 0.9967978001 0.9946566224 0.9959883094 - 0.9968255758 0.9947158098 0.9960448742 - 0.9968534112 0.9947745800 0.9961013198 - 0.9968826771 0.9948306084 0.9961574078 - 0.9969120026 0.9948865175 0.9962133765 - 0.9969413280 0.9949424267 0.9962695241 - 0.9969705939 0.9949982762 0.9963254929 - 0.9969999194 0.9950541854 0.9963815212 - 0.9970293045 0.9951100945 0.9964376092 - 0.9970585704 0.9951661229 0.9964935780 - 0.9970878959 0.9952219725 0.9965497255 - 0.9971172214 0.9952778816 0.9966056943 - 0.9971464872 0.9953337908 0.9966616035 - 0.9971768856 0.9953922033 0.9967159033 - 0.9972077012 0.9954509735 0.9967702031 - 0.9972383976 0.9955098033 0.9968245029 - 0.9972692132 0.9955686927 0.9968789220 - 0.9973000288 0.9956275225 0.9969332218 - 0.9973307848 0.9956862926 0.9969875216 - 0.9973614812 0.9957451224 0.9970418811 - 0.9973922968 0.9958038926 0.9970961809 - 0.9974231124 0.9958627224 0.9971504807 - 0.9974539280 0.9959216118 0.9972047806 - 0.9974855185 0.9959818721 0.9972571731 - 0.9975185990 0.9960436821 0.9973087907 - 0.9975516796 0.9961054921 0.9973604083 - 0.9975848198 0.9961673021 0.9974119067 - 0.9976179004 0.9962291121 0.9974635243 - 0.9976509809 0.9962909222 0.9975150824 - 0.9976841211 0.9963526726 0.9975667000 - 0.9977172017 0.9964144826 0.9976183176 - 0.9977502823 0.9964762926 0.9976698756 - 0.9977834225 0.9965381026 0.9977214932 - 0.9978165030 0.9965999722 0.9977722764 - 0.9978520870 0.9966641068 0.9978203177 - 0.9978877902 0.9967283010 0.9978682995 - 0.9979234934 0.9967923760 0.9979162812 - 0.9979591966 0.9968565702 0.9979643226 - 0.9979950190 0.9969207048 0.9980123043 - 0.9980307221 0.9969848990 0.9980602860 - 0.9980664253 0.9970489740 0.9981083274 - 0.9981021881 0.9971132278 0.9981563091 - 0.9981378913 0.9971773028 0.9982042909 - 0.9981735945 0.9972414970 0.9982522726 - 0.9982103705 0.9973062277 0.9982975721 - 0.9982488155 0.9973716736 0.9983413815 - 0.9982872009 0.9974371791 0.9983850718 - 0.9983255863 0.9975026846 0.9984288812 - 0.9983639717 0.9975680709 0.9984725714 - 0.9984024167 0.9976335764 0.9985163808 - 0.9984408021 0.9976990819 0.9985600710 - 0.9984791875 0.9977645874 0.9986038804 - 0.9985176921 0.9978300929 0.9986475706 - 0.9985560775 0.9978954792 0.9986913800 - 0.9985945225 0.9979609847 0.9987350702 - 0.9986345768 0.9980263114 0.9987767935 - 0.9986755252 0.9980915785 0.9988182187 - 0.9987164140 0.9981567860 0.9988597035 - 0.9987573028 0.9982221127 0.9989011288 - 0.9987981915 0.9982873797 0.9989426136 - 0.9988390803 0.9983525872 0.9989839792 - 0.9988800287 0.9984179139 0.9990255237 - 0.9989209175 0.9984831214 0.9990668893 - 0.9989618063 0.9985483885 0.9991083741 - 0.9990026951 0.9986137152 0.9991497993 - 0.9990435839 0.9986789227 0.9991911054 - 0.9990863204 0.9987418056 0.9992294908 - 0.9991291761 0.9988046288 0.9992678165 - 0.9991720915 0.9988675117 0.9993060827 - 0.9992150068 0.9989302754 0.9993445277 - 0.9992579222 0.9989930987 0.9993827939 - 0.9993007779 0.9990559220 0.9994211793 - 0.9993436933 0.9991186857 0.9994595051 - 0.9993866086 0.9991815090 0.9994978905 - 0.9994295239 0.9992442727 0.9995362163 - 0.9994723797 0.9993070960 0.9995746017 - 0.9995154142 0.9993696809 0.9996126294 - 0.9995594025 0.9994270205 0.9996479154 - 0.9996035099 0.9994843006 0.9996830821 - 0.9996474981 0.9995415807 0.9997183084 - 0.9996916056 0.9995989203 0.9997534752 - 0.9997357130 0.9996562004 0.9997887015 - 0.9997797012 0.9997134805 0.9998239279 - 0.9998238087 0.9997708201 0.9998590946 - 0.9998677969 0.9998281002 0.9998943806 - 0.9999119043 0.9998853803 0.9999296069 - 0.9999558926 0.9999427199 0.9999647737 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0007026231 0.0005291322 0.0003438386 + 0.0014857050 0.0011212280 0.0007142003 + 0.0023536440 0.0017713160 0.0011152170 + 0.0033091281 0.0024790310 0.0015472880 + 0.0043581589 0.0032436289 0.0020068779 + 0.0055047469 0.0040647909 0.0024986709 + 0.0067437058 0.0049467869 0.0030213159 + 0.0080656186 0.0058862939 0.0035776510 + 0.0094755674 0.0068916250 0.0041668881 + 0.0109947799 0.0079634041 0.0047885012 + 0.0125997197 0.0091056926 0.0054450892 + 0.0142905395 0.0103072003 0.0061361468 + 0.0160812791 0.0115757296 0.0068590711 + 0.0179866701 0.0129182898 0.0076176482 + 0.0199946500 0.0143368002 0.0084124254 + 0.0220939498 0.0158436093 0.0092439801 + 0.0242956392 0.0174334496 0.0101133203 + 0.0266019199 0.0191023294 0.0110254698 + 0.0290070400 0.0208547898 0.0119773299 + 0.0315142386 0.0226893406 0.0129693104 + 0.0341268592 0.0246077292 0.0140018696 + 0.0368515886 0.0266096294 0.0150737204 + 0.0396952704 0.0286963899 0.0161837209 + 0.0426549911 0.0308631808 0.0173352305 + 0.0457203016 0.0331055112 0.0185222793 + 0.0488859490 0.0354270898 0.0197516698 + 0.0521433800 0.0378220901 0.0210211892 + 0.0554969683 0.0402937494 0.0223336108 + 0.0589461401 0.0428398289 0.0236905906 + 0.0624969490 0.0454611294 0.0250933897 + 0.0661549270 0.0481612310 0.0265355706 + 0.0699189603 0.0509477705 0.0280245394 + 0.0737859830 0.0538181216 0.0295602791 + 0.0777632222 0.0567707308 0.0311385598 + 0.0818370879 0.0598130785 0.0327619798 + 0.0860176310 0.0629488677 0.0344315693 + 0.0903011784 0.0661693737 0.0361473896 + 0.0946921036 0.0694710910 0.0379051082 + 0.0991754308 0.0728540421 0.0397059508 + 0.1037482023 0.0763145164 0.0415495113 + 0.1084047034 0.0798475519 0.0434369706 + 0.1131415963 0.0834460035 0.0453708693 + 0.1179561019 0.0871294513 0.0473461710 + 0.1228469983 0.0908933803 0.0493673384 + 0.1278257072 0.0947273821 0.0514261499 + 0.1328838021 0.0986372530 0.0535301305 + 0.1380223036 0.1026311964 0.0556773506 + 0.1432214975 0.1067062020 0.0578665212 + 0.1484941989 0.1108566970 0.0601071604 + 0.1538525969 0.1150842011 0.0623893999 + 0.1592795998 0.1193784997 0.0647117496 + 0.1647690982 0.1237355024 0.0670745894 + 0.1703308970 0.1281584054 0.0694851503 + 0.1759331971 0.1326268017 0.0719340369 + 0.1815855950 0.1371477991 0.0744230300 + 0.1872842014 0.1417109966 0.0769482180 + 0.1930216998 0.1463264972 0.0795174912 + 0.1988134980 0.1509988010 0.0821337178 + 0.2046463042 0.1557403058 0.0847895518 + 0.2105436027 0.1605253071 0.0874943137 + 0.2164897025 0.1653804928 0.0902354270 + 0.2224933952 0.1702827066 0.0930147171 + 0.2285283953 0.1752299964 0.0958402529 + 0.2345996946 0.1802355051 0.0987048224 + 0.2407190949 0.1852916926 0.1016040966 + 0.2468644977 0.1903931051 0.1045442000 + 0.2530235052 0.1955378056 0.1075213999 + 0.2591835856 0.2007202953 0.1105407029 + 0.2653717995 0.2059361935 0.1135976985 + 0.2715786099 0.2111911029 0.1166936010 + 0.2777847946 0.2164549977 0.1198228002 + 0.2839922011 0.2217417061 0.1229953989 + 0.2902092040 0.2270426005 0.1262023002 + 0.2964079082 0.2323473990 0.1294420958 + 0.3025959134 0.2376510054 0.1327183992 + 0.3087722957 0.2429573983 0.1360210031 + 0.3149321079 0.2482704073 0.1393564939 + 0.3210678101 0.2535850108 0.1427318007 + 0.3272047043 0.2589012980 0.1461382955 + 0.3333187997 0.2642197907 0.1495731026 + 0.3394154012 0.2695527971 0.1530420035 + 0.3454918861 0.2748903036 0.1565402001 + 0.3515484929 0.2802191973 0.1600638032 + 0.3576078117 0.2855531871 0.1636112034 + 0.3636479080 0.2909021080 0.1671815962 + 0.3696722090 0.2962611914 0.1707742065 + 0.3756689131 0.3016174138 0.1743832976 + 0.3816395998 0.3069770932 0.1780118048 + 0.3875685036 0.3123393059 0.1816574037 + 0.3934744000 0.3176954985 0.1853218973 + 0.3993682861 0.3230510056 0.1890095025 + 0.4052383006 0.3284165859 0.1927215010 + 0.4110727012 0.3337813914 0.1964581013 + 0.4168589115 0.3391354084 0.2002173960 + 0.4226124883 0.3444840014 0.2039882988 + 0.4283437133 0.3498201966 0.2077811956 + 0.4340476990 0.3551411927 0.2115973979 + 0.4397180080 0.3604322970 0.2154300958 + 0.4453499019 0.3657175899 0.2192791998 + 0.4509378076 0.3709863126 0.2231477052 + 0.4564813077 0.3762353063 0.2270365059 + 0.4619686007 0.3814581037 0.2309399992 + 0.4674080908 0.3866479099 0.2348691970 + 0.4728043079 0.3918175101 0.2388215065 + 0.4781545103 0.3969655931 0.2427939028 + 0.4834581017 0.4020648003 0.2467841059 + 0.4887135029 0.4071336091 0.2507896125 + 0.4939058125 0.4121663868 0.2548081875 + 0.4990504086 0.4171527028 0.2588363886 + 0.5041617155 0.4221054912 0.2628706992 + 0.5092216134 0.4270165861 0.2669112086 + 0.5142281055 0.4319120049 0.2709617913 + 0.5191956758 0.4367876947 0.2750129104 + 0.5241003036 0.4416337907 0.2790606022 + 0.5289487243 0.4464288056 0.2831023932 + 0.5337404013 0.4511983097 0.2871333063 + 0.5384750962 0.4559360147 0.2911599874 + 0.5431362987 0.4606370032 0.2951760888 + 0.5477464795 0.4652822018 0.2991817892 + 0.5523136854 0.4699006975 0.3031803966 + 0.5568382740 0.4744859934 0.3071657121 + 0.5613049269 0.4790317118 0.3111445010 + 0.5657191277 0.4835188091 0.3151197135 + 0.5700793862 0.4879747033 0.3190810978 + 0.5743808150 0.4923951924 0.3230370879 + 0.5786144733 0.4967536032 0.3269884884 + 0.5828108191 0.5010759830 0.3309372962 + 0.5869714022 0.5053607821 0.3348788023 + 0.5910838246 0.5095916986 0.3388131857 + 0.5951429009 0.5137745738 0.3427393138 + 0.5991551280 0.5179340243 0.3466500044 + 0.6030982733 0.5220689774 0.3505468071 + 0.6069651246 0.5261592269 0.3544330001 + 0.6107723713 0.5302231908 0.3583126962 + 0.6144962907 0.5342565775 0.3621850014 + 0.6181483865 0.5382382274 0.3660446107 + 0.6217647195 0.5421987772 0.3698979020 + 0.6253350973 0.5461328030 0.3737412989 + 0.6288506985 0.5500105023 0.3775792122 + 0.6323326230 0.5538635254 0.3814140856 + 0.6357542872 0.5576797724 0.3852416873 + 0.6391350031 0.5614629984 0.3890604079 + 0.6424909234 0.5652248263 0.3928706050 + 0.6457951069 0.5689184070 0.3966498971 + 0.6490675807 0.5725790858 0.4004212022 + 0.6523084044 0.5761985779 0.4041798115 + 0.6554991007 0.5797787905 0.4079318047 + 0.6586679220 0.5833429098 0.4116778970 + 0.6617909074 0.5868695974 0.4154145122 + 0.6648769975 0.5903773904 0.4191451073 + 0.6679384708 0.5938647985 0.4228456020 + 0.6709473133 0.5972957015 0.4265331924 + 0.6739342213 0.6007062197 0.4302001894 + 0.6768646836 0.6040558815 0.4338527918 + 0.6797602177 0.6073855162 0.4374971986 + 0.6826251745 0.6106783748 0.4411129951 + 0.6854385138 0.6139551997 0.4447222054 + 0.6882362962 0.6172215939 0.4483056068 + 0.6909871101 0.6204239726 0.4518764913 + 0.6937232018 0.6236106753 0.4554122090 + 0.6964259148 0.6267350912 0.4589309096 + 0.6990894079 0.6298514009 0.4624271989 + 0.7017171979 0.6329439878 0.4658999145 + 0.7042943835 0.6360031962 0.4693557024 + 0.7068523169 0.6390181780 0.4727905095 + 0.7093623877 0.6420083046 0.4762170017 + 0.7118644714 0.6449810863 0.4796274900 + 0.7143244743 0.6479082704 0.4830344915 + 0.7167754769 0.6508182287 0.4864130020 + 0.7191916108 0.6536681056 0.4897888005 + 0.7215893865 0.6565065980 0.4931452870 + 0.7239490151 0.6593149900 0.4965006113 + 0.7262917757 0.6621205807 0.4998255074 + 0.7286117077 0.6648774147 0.5031486154 + 0.7308964729 0.6676303744 0.5064402819 + 0.7331464291 0.6703311205 0.5097302794 + 0.7353671193 0.6730266809 0.5129926205 + 0.7375649214 0.6756759286 0.5162526965 + 0.7397302985 0.6783176064 0.5194661021 + 0.7418738008 0.6808996201 0.5226792097 + 0.7439814806 0.6834768057 0.5258497000 + 0.7460665703 0.6860129833 0.5290201902 + 0.7481163740 0.6885464191 0.5321524143 + 0.7501432896 0.6910448074 0.5352824926 + 0.7521458268 0.6935424209 0.5383893847 + 0.7541307211 0.6960023046 0.5414927006 + 0.7560967207 0.6984623075 0.5445662737 + 0.7580459714 0.7008609176 0.5476304889 + 0.7599691153 0.7032560110 0.5506601930 + 0.7618615031 0.7055974007 0.5536696911 + 0.7637408972 0.7079290748 0.5566582084 + 0.7655979991 0.7102227807 0.5596238971 + 0.7674412727 0.7125021815 0.5625798106 + 0.7692431808 0.7147536278 0.5655139089 + 0.7710394859 0.7169851065 0.5684432983 + 0.7727897167 0.7192016244 0.5713353753 + 0.7745398879 0.7213971019 0.5742275119 + 0.7762631774 0.7235780954 0.5770719051 + 0.7779850960 0.7257089019 0.5799102783 + 0.7796735764 0.7278391123 0.5827142000 + 0.7813516259 0.7299191952 0.5854988098 + 0.7830191255 0.7319993973 0.5882691145 + 0.7846779227 0.7340465784 0.5910149813 + 0.7863258123 0.7360842228 0.5937579870 + 0.7879499197 0.7381075025 0.5964655280 + 0.7895722985 0.7401170731 0.5991730094 + 0.7911682725 0.7421147823 0.6018517017 + 0.7927643061 0.7440714836 0.6045210958 + 0.7943251133 0.7460281849 0.6071749926 + 0.7958757281 0.7479510903 0.6098067760 + 0.7974109054 0.7498708963 0.6124358773 + 0.7989286184 0.7517752051 0.6150038242 + 0.8004400134 0.7536681890 0.6175717711 + 0.8019111753 0.7555513978 0.6200997233 + 0.8033823967 0.7573989034 0.6226056814 + 0.8048381209 0.7592465281 0.6251029968 + 0.8062893748 0.7610545754 0.6275702119 + 0.8077279925 0.7628545165 0.6300374269 + 0.8091471791 0.7646414042 0.6324740052 + 0.8105664849 0.7664098144 0.6349017024 + 0.8119462729 0.7681781054 0.6373167038 + 0.8133255243 0.7698963284 0.6397023201 + 0.8146849871 0.7716131806 0.6420879960 + 0.8160281777 0.7733101845 0.6444455981 + 0.8173695207 0.7749879956 0.6467962861 + 0.8186913133 0.7766653895 0.6491395235 + 0.8200129867 0.7783104777 0.6514626145 + 0.8213170171 0.7799556255 0.6537855864 + 0.8226091266 0.7815762162 0.6560755968 + 0.8238981962 0.7831717730 0.6583523750 + 0.8251538873 0.7847673893 0.6606240273 + 0.8264095783 0.7863461971 0.6628612876 + 0.8276506066 0.7879241705 0.6650987267 + 0.8288792968 0.7894842029 0.6673101187 + 0.8301079869 0.7910140753 0.6694974899 + 0.8313009739 0.7925441265 0.6716848016 + 0.8324930072 0.7940390110 0.6738216877 + 0.8336700201 0.7955235243 0.6759517193 + 0.8348218799 0.7970044017 0.6780722141 + 0.8359737992 0.7984637022 0.6801573038 + 0.8371133804 0.7999228835 0.6822422743 + 0.8382487893 0.8013638258 0.6843022108 + 0.8393824100 0.8027846813 0.6863384247 + 0.8404983878 0.8042055964 0.6883745193 + 0.8416143060 0.8055989742 0.6903743148 + 0.8427138925 0.8069851995 0.6923640966 + 0.8437898159 0.8083692193 0.6943526864 + 0.8448657990 0.8097262979 0.6963012815 + 0.8459321260 0.8110834956 0.6982498765 + 0.8469941020 0.8124284744 0.7001879215 + 0.8480560780 0.8137484789 0.7020930052 + 0.8491019011 0.8150684834 0.7039980888 + 0.8501470089 0.8163607121 0.7058871984 + 0.8511853814 0.8176304102 0.7077549100 + 0.8521944284 0.8189001083 0.7096226215 + 0.8532034159 0.8201503158 0.7114666104 + 0.8542025089 0.8213939071 0.7132933736 + 0.8551856279 0.8226374984 0.7151201963 + 0.8561686873 0.8238468766 0.7169187069 + 0.8571283221 0.8250527978 0.7187049985 + 0.8580685258 0.8262571096 0.7204912901 + 0.8590087295 0.8274198174 0.7222427130 + 0.8599494100 0.8285825253 0.7239848971 + 0.8608903289 0.8297395110 0.7257270217 + 0.8618311882 0.8308607936 0.7274336815 + 0.8627600074 0.8319821954 0.7291340828 + 0.8636854887 0.8330979943 0.7308344841 + 0.8646110892 0.8341925740 0.7325049043 + 0.8655208945 0.8352872729 0.7341710925 + 0.8664281964 0.8363729715 0.7358372808 + 0.8673354983 0.8374316096 0.7374768853 + 0.8682217002 0.8384901285 0.7391127944 + 0.8691056967 0.8395386934 0.7407485843 + 0.8699898124 0.8405579925 0.7423660159 + 0.8708515763 0.8415772915 0.7439796925 + 0.8717116117 0.8425924182 0.7455934882 + 0.8725715280 0.8435931206 0.7471752763 + 0.8734114170 0.8445938230 0.7487472296 + 0.8742489219 0.8455908895 0.7503191829 + 0.8750864267 0.8465701938 0.7518702149 + 0.8759170175 0.8475493789 0.7534102798 + 0.8767464161 0.8485258222 0.7549505234 + 0.8775756955 0.8494656086 0.7564716935 + 0.8783869743 0.8504052758 0.7579756975 + 0.8791924715 0.8513450027 0.7594797015 + 0.8799980283 0.8522825241 0.7609726191 + 0.8807792068 0.8532198071 0.7624449730 + 0.8815463185 0.8541572094 0.7639175057 + 0.8823133707 0.8550791144 0.7653834224 + 0.8830744028 0.8559969068 0.7668110132 + 0.8838288188 0.8569146991 0.7682386041 + 0.8845831156 0.8578169942 0.7696663141 + 0.8853343725 0.8587086201 0.7710726857 + 0.8860781193 0.8596001863 0.7724769711 + 0.8868216872 0.8604816794 0.7738813162 + 0.8875637054 0.8613451123 0.7752763033 + 0.8882815242 0.8622086048 0.7766659856 + 0.8889992833 0.8630689979 0.7780556083 + 0.8897172213 0.8639014959 0.7794315815 + 0.8904141784 0.8647338748 0.7807787061 + 0.8911063075 0.8655663729 0.7821257710 + 0.8917983770 0.8663951159 0.7834728956 + 0.8924919963 0.8672229052 0.7847924829 + 0.8931869864 0.8680506945 0.7861111760 + 0.8938819766 0.8688691258 0.7874299884 + 0.8945751190 0.8696764112 0.7887393832 + 0.8952596188 0.8704836965 0.7900424004 + 0.8959441185 0.8712878227 0.7913452983 + 0.8966286182 0.8720598817 0.7926430702 + 0.8972942233 0.8728318810 0.7939155102 + 0.8979557157 0.8736039996 0.7951877713 + 0.8986173272 0.8743599057 0.7964602113 + 0.8992745876 0.8751083016 0.7977172136 + 0.8999263048 0.8758566976 0.7989680767 + 0.9005779028 0.8766024709 0.8002188802 + 0.9012296200 0.8773398995 0.8014593720 + 0.9018729925 0.8780772090 0.8026626706 + 0.9025161862 0.8788145781 0.8038660288 + 0.9031593800 0.8795428872 0.8050693274 + 0.9037922025 0.8802682757 0.8062509894 + 0.9044145942 0.8809937239 0.8074226975 + 0.9050371051 0.8817160130 0.8085945249 + 0.9056594968 0.8824285269 0.8097618222 + 0.9062777162 0.8831409812 0.8108941913 + 0.9068958759 0.8838534951 0.8120266199 + 0.9075139761 0.8845480084 0.8131589890 + 0.9081308842 0.8852338791 0.8142759204 + 0.9087464213 0.8859198093 0.8153775930 + 0.9093617797 0.8866025805 0.8164793253 + 0.9099771976 0.8872501254 0.8175811172 + 0.9105805159 0.8878976703 0.8186519146 + 0.9111816287 0.8885452151 0.8197175860 + 0.9117826819 0.8891865015 0.8207833171 + 0.9123781919 0.8898199797 0.8218469024 + 0.9129536748 0.8904533982 0.8229005933 + 0.9135290980 0.8910868764 0.8239542246 + 0.9141045809 0.8917121887 0.8250077963 + 0.9146608114 0.8923351169 0.8260518909 + 0.9152011871 0.8929579854 0.8270838261 + 0.9157415032 0.8935797215 0.8281157017 + 0.9162818789 0.8941801190 0.8291476965 + 0.9168179035 0.8947805762 0.8301671743 + 0.9173532128 0.8953809738 0.8311806917 + 0.9178884029 0.8959742785 0.8321940899 + 0.9184231758 0.8965504169 0.8332076073 + 0.9189543128 0.8971264958 0.8341963291 + 0.9194852710 0.8977025747 0.8351811171 + 0.9200162888 0.8982679844 0.8361659050 + 0.9205431938 0.8988227844 0.8371505141 + 0.9210606217 0.8993775249 0.8381142020 + 0.9215779901 0.8999322057 0.8390778899 + 0.9220954180 0.9004656076 0.8400415778 + 0.9226071239 0.9009882212 0.8410019279 + 0.9231125712 0.9015107751 0.8419361115 + 0.9236180782 0.9020333290 0.8428702950 + 0.9241235256 0.9025334716 0.8438045979 + 0.9246153235 0.9030274749 0.8447362781 + 0.9250978231 0.9035214782 0.8456568718 + 0.9255803227 0.9040154815 0.8465775847 + 0.9260628223 0.9045063853 0.8474981785 + 0.9265328050 0.9049968123 0.8484144211 + 0.9269970059 0.9054871798 0.8493136764 + 0.9274612069 0.9059776068 0.8502129912 + 0.9279254079 0.9064562917 0.8511123061 + 0.9283785820 0.9069336057 0.8520066738 + 0.9288275838 0.9074109793 0.8528792262 + 0.9292767048 0.9078882933 0.8537517190 + 0.9297257066 0.9083464146 0.8546240926 + 0.9301574826 0.9088020921 0.8554927111 + 0.9305828214 0.9092578888 0.8563321233 + 0.9310081005 0.9097136259 0.8571714759 + 0.9314333797 0.9101629257 0.8580110073 + 0.9318456054 0.9106109142 0.8588501811 + 0.9322518110 0.9110590219 0.8596740961 + 0.9326580763 0.9115070105 0.8604980111 + 0.9330642819 0.9119517207 0.8613219261 + 0.9334682822 0.9123954177 0.8621457815 + 0.9338707924 0.9128389955 0.8629440069 + 0.9342733026 0.9132825732 0.8637380004 + 0.9346758127 0.9137256145 0.8645321131 + 0.9350727201 0.9141682982 0.8653261065 + 0.9354630113 0.9146109819 0.8661041856 + 0.9358533025 0.9150536060 0.8668742180 + 0.9362437129 0.9154921770 0.8676440716 + 0.9366282821 0.9159253836 0.8684141040 + 0.9369992018 0.9163584709 0.8691806197 + 0.9373700023 0.9167916179 0.8699421883 + 0.9377409220 0.9172230959 0.8707038164 + 0.9381108284 0.9176480174 0.8714653850 + 0.9384678006 0.9180728793 0.8722239733 + 0.9388247728 0.9184976816 0.8729615808 + 0.9391818047 0.9189226031 0.8736993074 + 0.9395387769 0.9193425775 0.8744369149 + 0.9398884177 0.9197623134 0.8751745820 + 0.9402360916 0.9201819897 0.8758922219 + 0.9405838847 0.9206016064 0.8766046166 + 0.9409316182 0.9210132957 0.8773170114 + 0.9412720203 0.9214196801 0.8780294061 + 0.9416047931 0.9218261838 0.8787423968 + 0.9419375062 0.9222326279 0.8794564009 + 0.9422702193 0.9226341248 0.8801705241 + 0.9426016808 0.9230197072 0.8808845282 + 0.9429249763 0.9234051704 0.8815984726 + 0.9432482719 0.9237906933 0.8823022246 + 0.9435716271 0.9241762161 0.8830056190 + 0.9438948035 0.9245483875 0.8837090731 + 0.9442144036 0.9249175191 0.8844125271 + 0.9445325136 0.9252867103 0.8851082921 + 0.9448506236 0.9256557822 0.8857964277 + 0.9451687932 0.9260243177 0.8864845037 + 0.9454866052 0.9263916016 0.8871726990 + 0.9458038807 0.9267588258 0.8878607750 + 0.9461210966 0.9271261096 0.8885198236 + 0.9464383721 0.9274932742 0.8891779184 + 0.9467555881 0.9278637767 0.8898360729 + 0.9470669031 0.9282348752 0.8904942274 + 0.9473763108 0.9286059737 0.8911436796 + 0.9476857185 0.9289770126 0.8917800188 + 0.9479951262 0.9293472171 0.8924162984 + 0.9483039975 0.9297147989 0.8930525780 + 0.9486111999 0.9300825000 0.8936889172 + 0.9489184022 0.9304502010 0.8942996860 + 0.9492256045 0.9308179021 0.8949031234 + 0.9495326877 0.9311780930 0.8955063820 + 0.9498348832 0.9315345883 0.8961097002 + 0.9501338005 0.9318912029 0.8967118263 + 0.9504327774 0.9322476983 0.8973019719 + 0.9507316947 0.9326038957 0.8978921175 + 0.9510306716 0.9329422712 0.8984823227 + 0.9513161778 0.9332805872 0.8990725279 + 0.9516012073 0.9336189032 0.8996539712 + 0.9518861771 0.9339572787 0.9002208710 + 0.9521712065 0.9342914224 0.9007877707 + 0.9524512291 0.9346157908 0.9013546109 + 0.9527168274 0.9349402189 0.9019215107 + 0.9529824257 0.9352647066 0.9024795890 + 0.9532480240 0.9355890751 0.9030315876 + 0.9535136223 0.9359090924 0.9035837054 + 0.9537726045 0.9362252951 0.9041357040 + 0.9540249705 0.9365414977 0.9046877027 + 0.9542772770 0.9368577003 0.9052293897 + 0.9545297027 0.9371739030 0.9057677984 + 0.9547821283 0.9374797940 0.9063060880 + 0.9550374746 0.9377818108 0.9068444967 + 0.9552941918 0.9380838871 0.9073829055 + 0.9555509090 0.9383859038 0.9079146981 + 0.9558076262 0.9386879206 0.9084457159 + 0.9560642838 0.9389883280 0.9089766145 + 0.9563276768 0.9392884970 0.9095075727 + 0.9565923810 0.9395886064 0.9100385904 + 0.9568570852 0.9398887157 0.9105516076 + 0.9571216702 0.9401888251 0.9110634923 + 0.9573863745 0.9404814839 0.9115753770 + 0.9576531053 0.9407737255 0.9120873809 + 0.9579198956 0.9410659075 0.9125993252 + 0.9581868052 0.9413580894 0.9130939245 + 0.9584535956 0.9416502714 0.9135876894 + 0.9587203860 0.9419289231 0.9140815139 + 0.9589838982 0.9422072768 0.9145752788 + 0.9592472911 0.9424856901 0.9150691032 + 0.9595106244 0.9427641034 0.9155386090 + 0.9597740173 0.9430423975 0.9160056710 + 0.9600374103 0.9433137178 0.9164727926 + 0.9602922201 0.9435846806 0.9169399142 + 0.9605466127 0.9438555837 0.9174069762 + 0.9608010054 0.9441264868 0.9178671837 + 0.9610555172 0.9443975091 0.9183259010 + 0.9613099098 0.9446647167 0.9187846184 + 0.9615522027 0.9449313283 0.9192432165 + 0.9617928863 0.9451979995 0.9197018743 + 0.9620336294 0.9454646707 0.9201496840 + 0.9622743130 0.9457312822 0.9205917716 + 0.9625151157 0.9459909201 0.9210339785 + 0.9627497196 0.9462481141 0.9214761853 + 0.9629824162 0.9465051889 0.9219183922 + 0.9632151127 0.9467623830 0.9223541021 + 0.9634478092 0.9470195174 0.9227821827 + 0.9636805058 0.9472687244 0.9232103229 + 0.9639062881 0.9475113153 0.9236382842 + 0.9641271234 0.9477539062 0.9240664244 + 0.9643478990 0.9479964972 0.9244915843 + 0.9645686746 0.9482392073 0.9249045253 + 0.9647895098 0.9484791160 0.9253175259 + 0.9650083780 0.9487127066 0.9257304072 + 0.9652239084 0.9489462972 0.9261432886 + 0.9654393196 0.9491798878 0.9265562296 + 0.9656546712 0.9494134784 0.9269577861 + 0.9658702016 0.9496471286 0.9273576736 + 0.9660856128 0.9498767257 0.9277576208 + 0.9663015008 0.9501063228 0.9281575084 + 0.9665173888 0.9503358006 0.9285573959 + 0.9667332768 0.9505653977 0.9289472103 + 0.9669492245 0.9507949948 0.9293261766 + 0.9671651125 0.9510244727 0.9297050834 + 0.9673789144 0.9512541294 0.9300841093 + 0.9675917029 0.9514836073 0.9304630756 + 0.9678044915 0.9517130852 0.9308412075 + 0.9680173993 0.9519426823 0.9312015176 + 0.9682301879 0.9521713257 0.9315618277 + 0.9684410095 0.9523969293 0.9319220185 + 0.9686465859 0.9526224732 0.9322823286 + 0.9688522816 0.9528480172 0.9326425791 + 0.9690579176 0.9530736208 0.9330028892 + 0.9692634940 0.9532991052 0.9333631992 + 0.9694690704 0.9535260201 0.9337235093 + 0.9696654081 0.9537532926 0.9340837002 + 0.9698594809 0.9539806843 0.9344440103 + 0.9700536728 0.9542080164 0.9348043203 + 0.9702479243 0.9544352889 0.9351575971 + 0.9704419971 0.9546620846 0.9355108738 + 0.9706341028 0.9548869133 0.9358642101 + 0.9708212018 0.9551116228 0.9362174273 + 0.9710081816 0.9553362727 0.9365707040 + 0.9711952806 0.9555611014 0.9369189143 + 0.9713823795 0.9557858109 0.9372587800 + 0.9715694189 0.9560061097 0.9375988245 + 0.9717484713 0.9562234282 0.9379386902 + 0.9719241261 0.9564408064 0.9382786155 + 0.9720997214 0.9566581845 0.9386184812 + 0.9722753167 0.9568755031 0.9389562011 + 0.9724509120 0.9570928812 0.9392927885 + 0.9726263285 0.9572992921 0.9396293759 + 0.9727976918 0.9575052261 0.9399660826 + 0.9729689956 0.9577109814 0.9403027296 + 0.9731404185 0.9579169154 0.9406393766 + 0.9733117223 0.9581226707 0.9409670830 + 0.9734830856 0.9583271146 0.9412943721 + 0.9736548066 0.9585263133 0.9416217208 + 0.9738274217 0.9587255120 0.9419491291 + 0.9739999175 0.9589247108 0.9422764182 + 0.9741724133 0.9591240287 0.9426026940 + 0.9743450284 0.9593232274 0.9429237247 + 0.9745175242 0.9595177770 0.9432445765 + 0.9746888876 0.9597066045 0.9435654879 + 0.9748594165 0.9598953724 0.9438865185 + 0.9750298858 0.9600841999 0.9442073703 + 0.9752004147 0.9602730274 0.9445272088 + 0.9753708839 0.9604617953 0.9448443055 + 0.9755414724 0.9606478810 0.9451612830 + 0.9757080078 0.9608321786 0.9454783201 + 0.9758731723 0.9610164762 0.9457954168 + 0.9760383964 0.9612007737 0.9461123943 + 0.9762036204 0.9613850713 0.9464284778 + 0.9763687849 0.9615694284 0.9467430115 + 0.9765340090 0.9617481232 0.9470574856 + 0.9766919017 0.9619243741 0.9473719001 + 0.9768484235 0.9621006846 0.9476863742 + 0.9770050049 0.9622771144 0.9480009079 + 0.9771615267 0.9624534249 0.9483118057 + 0.9773181081 0.9626296759 0.9486168027 + 0.9774745703 0.9628033042 0.9489219189 + 0.9776207805 0.9629759192 0.9492269158 + 0.9777655005 0.9631485939 0.9495319128 + 0.9779102802 0.9633212090 0.9498370290 + 0.9780550003 0.9634938240 0.9501398802 + 0.9781997800 0.9636663795 0.9504382014 + 0.9783445001 0.9638398290 0.9507365227 + 0.9784767032 0.9640133977 0.9510347843 + 0.9786067009 0.9641870856 0.9513331056 + 0.9787368178 0.9643607736 0.9516314268 + 0.9788668752 0.9645345211 0.9519273043 + 0.9789968729 0.9647082090 0.9522131085 + 0.9791269898 0.9648808241 0.9524989128 + 0.9792442918 0.9650526047 0.9527845979 + 0.9793577790 0.9652245045 0.9530704021 + 0.9794713259 0.9653962851 0.9533562064 + 0.9795848131 0.9655681849 0.9536418915 + 0.9796983004 0.9657400250 0.9539101720 + 0.9798117280 0.9659097195 0.9541785121 + 0.9799193740 0.9660761952 0.9544466734 + 0.9800233841 0.9662427902 0.9547150135 + 0.9801273942 0.9664093852 0.9549832940 + 0.9802314043 0.9665759802 0.9552515745 + 0.9803354144 0.9667425752 0.9555094838 + 0.9804394245 0.9669076800 0.9557638168 + 0.9805418849 0.9670665264 0.9560182095 + 0.9806420207 0.9672253132 0.9562726021 + 0.9807420969 0.9673839808 0.9565269947 + 0.9808421731 0.9675428271 0.9567813873 + 0.9809423089 0.9677016139 0.9570345283 + 0.9810423851 0.9678602815 0.9572860003 + 0.9811419249 0.9680219889 0.9575374126 + 0.9812365174 0.9681841135 0.9577888250 + 0.9813309908 0.9683461785 0.9580402970 + 0.9814255834 0.9685081840 0.9582917094 + 0.9815201163 0.9686703086 0.9585428834 + 0.9816147089 0.9688323736 0.9587876201 + 0.9817091823 0.9689944983 0.9590322971 + 0.9817987084 0.9691566229 0.9592769146 + 0.9818863273 0.9693186879 0.9595215917 + 0.9819738865 0.9694808125 0.9597663283 + 0.9820615053 0.9696428776 0.9600110054 + 0.9821491241 0.9698050022 0.9602541924 + 0.9822366834 0.9699668884 0.9604964256 + 0.9823241830 0.9701260924 0.9607385993 + 0.9824113846 0.9702852964 0.9609807730 + 0.9824985862 0.9704445004 0.9612230062 + 0.9825857878 0.9706035852 0.9614651203 + 0.9826729298 0.9707627892 0.9617071152 + 0.9827600718 0.9709219933 0.9619415998 + 0.9828472733 0.9710779786 0.9621762037 + 0.9829329252 0.9712315798 0.9624108076 + 0.9830182195 0.9713851213 0.9626452923 + 0.9831033945 0.9715387225 0.9628798962 + 0.9831886888 0.9716922045 0.9631145000 + 0.9832739234 0.9718458056 0.9633466005 + 0.9833592176 0.9719992876 0.9635754228 + 0.9834436178 0.9721450210 0.9638043046 + 0.9835252166 0.9722905159 0.9640331864 + 0.9836068749 0.9724360704 0.9642621279 + 0.9836885929 0.9725816250 0.9644908905 + 0.9837701917 0.9727271199 0.9647197723 + 0.9838519096 0.9728726149 0.9649403095 + 0.9839335084 0.9730172753 0.9651581049 + 0.9840176105 0.9731602073 0.9653759003 + 0.9841032028 0.9733030796 0.9655936956 + 0.9841887951 0.9734460711 0.9658116102 + 0.9842743874 0.9735890031 0.9660294056 + 0.9843600988 0.9737318754 0.9662470818 + 0.9844456911 0.9738749266 0.9664586186 + 0.9845312834 0.9740148783 0.9666702151 + 0.9846189022 0.9741529226 0.9668816924 + 0.9847068191 0.9742910266 0.9670932293 + 0.9847946167 0.9744290709 0.9673047066 + 0.9848824739 0.9745671749 0.9675163031 + 0.9849703908 0.9747052193 0.9677258134 + 0.9850581884 0.9748433232 0.9679268003 + 0.9851459861 0.9749873877 0.9681277871 + 0.9852334261 0.9751328230 0.9683287740 + 0.9853208065 0.9752783179 0.9685298204 + 0.9854081869 0.9754236937 0.9687308073 + 0.9854956269 0.9755691886 0.9689317942 + 0.9855828881 0.9757146835 0.9691283703 + 0.9856703281 0.9758601189 0.9693145752 + 0.9857584238 0.9760093093 0.9695008993 + 0.9858487844 0.9761587977 0.9696871042 + 0.9859390855 0.9763082862 0.9698734283 + 0.9860295057 0.9764577746 0.9700595737 + 0.9861198068 0.9766073227 0.9702458978 + 0.9862102270 0.9767568111 0.9704262018 + 0.9863005280 0.9769061804 0.9705942273 + 0.9863908887 0.9770559072 0.9707623124 + 0.9864816070 0.9772055745 0.9709302783 + 0.9865723252 0.9773551822 0.9710983038 + 0.9866629839 0.9775049090 0.9712663889 + 0.9867537022 0.9776545763 0.9714344144 + 0.9868444204 0.9778043032 0.9715989828 + 0.9869350195 0.9779539108 0.9717546105 + 0.9870250821 0.9781007171 0.9719101787 + 0.9871137738 0.9782474041 0.9720658064 + 0.9872025251 0.9783940911 0.9722213745 + 0.9872910976 0.9785407782 0.9723770022 + 0.9873797894 0.9786874056 0.9725325704 + 0.9874684811 0.9788340926 0.9726879001 + 0.9875571132 0.9789807796 0.9728415012 + 0.9876449704 0.9791290164 0.9729952216 + 0.9877302051 0.9792773128 0.9731488824 + 0.9878153205 0.9794257283 0.9733024836 + 0.9879004955 0.9795740843 0.9734562039 + 0.9879856110 0.9797223806 0.9736099243 + 0.9880707860 0.9798707962 0.9737635255 + 0.9881559014 0.9800190926 0.9739124179 + 0.9882411957 0.9801667929 0.9740610719 + 0.9883286953 0.9803141952 0.9742097855 + 0.9884161949 0.9804617167 0.9743584991 + 0.9885035753 0.9806091189 0.9745070934 + 0.9885910749 0.9807565212 0.9746558070 + 0.9886785746 0.9809039235 0.9748045206 + 0.9887660146 0.9810513258 0.9749522209 + 0.9888535142 0.9811971784 0.9750993252 + 0.9889419079 0.9813411236 0.9752464890 + 0.9890304208 0.9814851284 0.9753937125 + 0.9891189933 0.9816290736 0.9755408764 + 0.9892076254 0.9817730784 0.9756880999 + 0.9892961979 0.9819170237 0.9758353233 + 0.9893847108 0.9820610285 0.9759811163 + 0.9894732833 0.9822043180 0.9761242867 + 0.9895619154 0.9823424220 0.9762675166 + 0.9896506071 0.9824805260 0.9764106870 + 0.9897391796 0.9826186895 0.9765539169 + 0.9898278117 0.9827567935 0.9766970873 + 0.9899165034 0.9828948975 0.9768403172 + 0.9900050759 0.9830330014 0.9769834280 + 0.9900938272 0.9831712246 0.9771208167 + 0.9901822805 0.9833034277 0.9772574902 + 0.9902700186 0.9834333062 0.9773942232 + 0.9903578162 0.9835631847 0.9775308967 + 0.9904454947 0.9836931229 0.9776676297 + 0.9905332923 0.9838230014 0.9778043032 + 0.9906209707 0.9839528799 0.9779409766 + 0.9907088280 0.9840828180 0.9780768752 + 0.9907965064 0.9842103124 0.9782114029 + 0.9908831120 0.9843295217 0.9783458114 + 0.9909690022 0.9844487906 0.9784802794 + 0.9910550117 0.9845681190 0.9786148071 + 0.9911409020 0.9846873283 0.9787492752 + 0.9912267923 0.9848065972 0.9788838029 + 0.9913126826 0.9849258065 0.9790182114 + 0.9913986921 0.9850450754 0.9791532755 + 0.9914844036 0.9851558208 0.9792885184 + 0.9915674925 0.9852620959 0.9794237018 + 0.9916505814 0.9853683114 0.9795590043 + 0.9917337894 0.9854745269 0.9796941876 + 0.9918168783 0.9855806828 0.9798293710 + 0.9919000864 0.9856870174 0.9799646139 + 0.9919831753 0.9857931733 0.9800996780 + 0.9920663834 0.9858993888 0.9802330732 + 0.9921478033 0.9859907031 0.9803665876 + 0.9922270775 0.9860820174 0.9805001020 + 0.9923064709 0.9861732721 0.9806334972 + 0.9923858047 0.9862647057 0.9807670116 + 0.9924650788 0.9863560200 0.9809004068 + 0.9925444126 0.9864472747 0.9810339212 + 0.9926236868 0.9865385890 0.9811660051 + 0.9927030802 0.9866275787 0.9812954068 + 0.9927787781 0.9867103100 0.9814248085 + 0.9928531051 0.9867929220 0.9815542102 + 0.9929273725 0.9868754745 0.9816836119 + 0.9930018187 0.9869580865 0.9818130732 + 0.9930760860 0.9870408177 0.9819424748 + 0.9931504130 0.9871234298 0.9820718765 + 0.9932246804 0.9872059822 0.9821982980 + 0.9932990074 0.9872871041 0.9823215008 + 0.9933676124 0.9873663783 0.9824448228 + 0.9934356213 0.9874455929 0.9825680852 + 0.9935035706 0.9875249267 0.9826912880 + 0.9935715795 0.9876040816 0.9828146100 + 0.9936395884 0.9876832962 0.9829378724 + 0.9937075973 0.9877625704 0.9830611944 + 0.9937756062 0.9878417850 0.9831799269 + 0.9938433766 0.9879186749 0.9832950234 + 0.9939035773 0.9879934192 0.9834101796 + 0.9939638972 0.9880682230 0.9835252762 + 0.9940240979 0.9881430268 0.9836404920 + 0.9940842986 0.9882177711 0.9837555885 + 0.9941444993 0.9882925749 0.9838708043 + 0.9942047000 0.9883673191 0.9839859009 + 0.9942649007 0.9884421229 0.9840955734 + 0.9943246841 0.9885138273 0.9842007160 + 0.9943755865 0.9885829091 0.9843057990 + 0.9944264293 0.9886521101 0.9844108224 + 0.9944772720 0.9887213111 0.9845159054 + 0.9945281148 0.9887903929 0.9846209884 + 0.9945790172 0.9888595939 0.9847260714 + 0.9946299195 0.9889286757 0.9848312140 + 0.9946807027 0.9889978766 0.9849311113 + 0.9947316051 0.9890637994 0.9850243926 + 0.9947717190 0.9891260266 0.9851177931 + 0.9948117137 0.9891883135 0.9852110744 + 0.9948517084 0.9892506003 0.9853044748 + 0.9948915839 0.9893128872 0.9853978157 + 0.9949315786 0.9893751740 0.9854912162 + 0.9949715734 0.9894374013 0.9855846167 + 0.9950115085 0.9894996881 0.9856761098 + 0.9950515032 0.9895594120 0.9857621193 + 0.9950813055 0.9896134734 0.9858481288 + 0.9951090217 0.9896675944 0.9859341979 + 0.9951366782 0.9897217155 0.9860202074 + 0.9951645136 0.9897757769 0.9861062169 + 0.9951922297 0.9898298979 0.9861922860 + 0.9952198863 0.9898840189 0.9862782955 + 0.9952477217 0.9899380207 0.9863643050 + 0.9952753782 0.9899916053 0.9864417911 + 0.9952955842 0.9900360703 0.9865189195 + 0.9953106046 0.9900807142 0.9865959883 + 0.9953256845 0.9901251793 0.9866731763 + 0.9953408241 0.9901698232 0.9867503047 + 0.9953557849 0.9902142882 0.9868274927 + 0.9953709245 0.9902588725 0.9869046211 + 0.9953858852 0.9903033972 0.9869816899 + 0.9954010248 0.9903479815 0.9870569706 + 0.9954146743 0.9903848171 0.9871309996 + 0.9954246283 0.9904186130 0.9872050285 + 0.9954345822 0.9904522896 0.9872788787 + 0.9954444766 0.9904860854 0.9873529077 + 0.9954544902 0.9905198216 0.9874268770 + 0.9954643846 0.9905534983 0.9875009060 + 0.9954743981 0.9905872941 0.9875748754 + 0.9954842925 0.9906209707 0.9876483083 + 0.9954943061 0.9906514287 0.9877181053 + 0.9954999089 0.9906733036 0.9877879024 + 0.9955047965 0.9906951189 0.9878576994 + 0.9955096245 0.9907169938 0.9879274964 + 0.9955145121 0.9907388091 0.9879974127 + 0.9955192804 0.9907606840 0.9880672097 + 0.9955242276 0.9907824993 0.9881370068 + 0.9955291152 0.9908043742 0.9882068038 + 0.9955338836 0.9908261895 0.9882770777 + 0.9955371022 0.9908388257 0.9883475900 + 0.9955371022 0.9908484817 0.9884182215 + 0.9955371022 0.9908581972 0.9884887934 + 0.9955371022 0.9908677936 0.9885594249 + 0.9955371022 0.9908775091 0.9886299968 + 0.9955371022 0.9908872247 0.9887005091 + 0.9955371022 0.9908968806 0.9887710810 + 0.9955371022 0.9909064770 0.9888417125 + 0.9955371022 0.9909154773 0.9889119267 + 0.9955376983 0.9909201860 0.9889820218 + 0.9955384135 0.9909248948 0.9890521765 + 0.9955391884 0.9909297228 0.9891223907 + 0.9955399036 0.9909343719 0.9891924858 + 0.9955406189 0.9909390807 0.9892627001 + 0.9955413938 0.9909437895 0.9893327951 + 0.9955421090 0.9909486175 0.9894030094 + 0.9955428839 0.9909533262 0.9894726872 + 0.9955435991 0.9909560084 0.9895405769 + 0.9955443740 0.9909560084 0.9896085262 + 0.9955450892 0.9909560084 0.9896764159 + 0.9955459237 0.9909560084 0.9897443056 + 0.9955465794 0.9909560084 0.9898123145 + 0.9955472946 0.9909560084 0.9898802042 + 0.9955481291 0.9909560084 0.9899480939 + 0.9955487847 0.9909560084 0.9900159836 + 0.9955496192 0.9909560084 0.9900823832 + 0.9955502748 0.9909573793 0.9901462197 + 0.9955511093 0.9909597039 0.9902099967 + 0.9955518246 0.9909620285 0.9902737141 + 0.9955524802 0.9909642935 0.9903374910 + 0.9955533147 0.9909666181 0.9904012084 + 0.9955539703 0.9909688830 0.9904649854 + 0.9955548048 0.9909712076 0.9905287027 + 0.9955555201 0.9909734726 0.9905924797 + 0.9955562949 0.9909757972 0.9906535745 + 0.9955570102 0.9909781218 0.9907112718 + 0.9955577850 0.9909803867 0.9907689095 + 0.9955585003 0.9909827113 0.9908266068 + 0.9955592155 0.9909849763 0.9908841848 + 0.9955599904 0.9909873009 0.9909418821 + 0.9955607057 0.9909896255 0.9909995198 + 0.9955614805 0.9909918904 0.9910572171 + 0.9955621958 0.9909942150 0.9911147952 + 0.9955629706 0.9909964800 0.9911692739 + 0.9955636859 0.9910038114 0.9912189841 + 0.9955644011 0.9910129905 0.9912686944 + 0.9955651760 0.9910221100 0.9913184047 + 0.9955658913 0.9910312891 0.9913681149 + 0.9955667257 0.9910405278 0.9914177060 + 0.9955673814 0.9910495877 0.9914674163 + 0.9955682158 0.9910588264 0.9915171266 + 0.9955688715 0.9910680056 0.9915667772 + 0.9955697060 0.9910771251 0.9916139841 + 0.9955704212 0.9910891056 0.9916545749 + 0.9955710769 0.9911026955 0.9916951060 + 0.9955719113 0.9911162853 0.9917356968 + 0.9955726266 0.9911298752 0.9917762280 + 0.9955734015 0.9911434054 0.9918168187 + 0.9955741167 0.9911569953 0.9918574095 + 0.9955748916 0.9911705852 0.9918978810 + 0.9955756068 0.9911841750 0.9919384718 + 0.9955763221 0.9911978245 0.9919788837 + 0.9955770969 0.9912132025 0.9920160770 + 0.9955778122 0.9912310243 0.9920532703 + 0.9955785871 0.9912489057 0.9920905232 + 0.9955793023 0.9912667274 0.9921277165 + 0.9955800772 0.9912844896 0.9921649098 + 0.9955807924 0.9913023114 0.9922021031 + 0.9955816269 0.9913200736 0.9922392964 + 0.9955822825 0.9913378954 0.9922764897 + 0.9955829978 0.9913557172 0.9923136830 + 0.9955837727 0.9913741946 0.9923480153 + 0.9955844879 0.9913958907 0.9923812747 + 0.9955853224 0.9914177060 0.9924144745 + 0.9955859780 0.9914395213 0.9924477935 + 0.9955868125 0.9914612770 0.9924809933 + 0.9955875278 0.9914830923 0.9925143123 + 0.9955881834 0.9915049076 0.9925475121 + 0.9955890179 0.9915266037 0.9925807118 + 0.9955896735 0.9915484190 0.9926139712 + 0.9955905080 0.9915701747 0.9926487803 + 0.9955912232 0.9915946722 0.9926875234 + 0.9955919981 0.9916201234 0.9927260876 + 0.9955927134 0.9916453958 0.9927647710 + 0.9955934882 0.9916707873 0.9928033948 + 0.9955942035 0.9916961193 0.9928420782 + 0.9955949187 0.9917215109 0.9928807020 + 0.9955956936 0.9917467833 0.9929193854 + 0.9955964088 0.9917721748 0.9929580092 + 0.9955971837 0.9917975068 0.9929966927 + 0.9955980182 0.9918254018 0.9930391908 + 0.9956101775 0.9918603897 0.9930828214 + 0.9956223965 0.9918953180 0.9931263924 + 0.9956346154 0.9919303060 0.9931700826 + 0.9956468940 0.9919652939 0.9932137132 + 0.9956591129 0.9920002222 0.9932574034 + 0.9956712723 0.9920352101 0.9933009744 + 0.9956834912 0.9920700788 0.9933447242 + 0.9956957102 0.9921051264 0.9933882952 + 0.9957079291 0.9921399951 0.9934325218 + 0.9957221746 0.9921805263 0.9934805036 + 0.9957391024 0.9922237992 0.9935284853 + 0.9957559705 0.9922670722 0.9935765266 + 0.9957728982 0.9923102856 0.9936243892 + 0.9957898259 0.9923536181 0.9936723709 + 0.9958066940 0.9923968911 0.9937204123 + 0.9958236814 0.9924401045 0.9937682748 + 0.9958406091 0.9924833775 0.9938163161 + 0.9958574772 0.9925267100 0.9938642979 + 0.9958744049 0.9925699234 0.9939137101 + 0.9958940744 0.9926193953 0.9939653277 + 0.9959146976 0.9926689863 0.9940168262 + 0.9959353805 0.9927186966 0.9940683842 + 0.9959560037 0.9927682877 0.9941200018 + 0.9959766865 0.9928179979 0.9941715002 + 0.9959973097 0.9928675890 0.9942231178 + 0.9960179925 0.9929172993 0.9942746162 + 0.9960386157 0.9929668903 0.9943261743 + 0.9960592985 0.9930166006 0.9943777919 + 0.9960799217 0.9930669069 0.9944308996 + 0.9961034060 0.9931212068 0.9944851995 + 0.9961270094 0.9931753874 0.9945394993 + 0.9961506128 0.9932296276 0.9945937991 + 0.9961740971 0.9932839274 0.9946479797 + 0.9961977005 0.9933381081 0.9947022796 + 0.9962213039 0.9933922887 0.9947565794 + 0.9962449074 0.9934465885 0.9948108792 + 0.9962685108 0.9935008287 0.9948651791 + 0.9962921143 0.9935550094 0.9949194789 + 0.9963157773 0.9936100245 0.9949749112 + 0.9963418245 0.9936673045 0.9950308800 + 0.9963676929 0.9937245846 0.9950869083 + 0.9963936210 0.9937819242 0.9951428771 + 0.9964196086 0.9938392043 0.9951989055 + 0.9964454770 0.9938964844 0.9952549934 + 0.9964715242 0.9939538240 0.9953110218 + 0.9964973927 0.9940111041 0.9953669906 + 0.9965233207 0.9940683842 0.9954230189 + 0.9965493083 0.9941257238 0.9954789877 + 0.9965752959 0.9941834211 0.9955353737 + 0.9966030717 0.9942424893 0.9955919981 + 0.9966309071 0.9943016768 0.9956486225 + 0.9966586828 0.9943608046 0.9957051873 + 0.9966865182 0.9944199920 0.9957618713 + 0.9967144132 0.9944791794 0.9958184958 + 0.9967421889 0.9945383072 0.9958751202 + 0.9967700243 0.9945974946 0.9959316850 + 0.9967978001 0.9946566224 0.9959883094 + 0.9968255758 0.9947158098 0.9960448742 + 0.9968534112 0.9947745800 0.9961013198 + 0.9968826771 0.9948306084 0.9961574078 + 0.9969120026 0.9948865175 0.9962133765 + 0.9969413280 0.9949424267 0.9962695241 + 0.9969705939 0.9949982762 0.9963254929 + 0.9969999194 0.9950541854 0.9963815212 + 0.9970293045 0.9951100945 0.9964376092 + 0.9970585704 0.9951661229 0.9964935780 + 0.9970878959 0.9952219725 0.9965497255 + 0.9971172214 0.9952778816 0.9966056943 + 0.9971464872 0.9953337908 0.9966616035 + 0.9971768856 0.9953922033 0.9967159033 + 0.9972077012 0.9954509735 0.9967702031 + 0.9972383976 0.9955098033 0.9968245029 + 0.9972692132 0.9955686927 0.9968789220 + 0.9973000288 0.9956275225 0.9969332218 + 0.9973307848 0.9956862926 0.9969875216 + 0.9973614812 0.9957451224 0.9970418811 + 0.9973922968 0.9958038926 0.9970961809 + 0.9974231124 0.9958627224 0.9971504807 + 0.9974539280 0.9959216118 0.9972047806 + 0.9974855185 0.9959818721 0.9972571731 + 0.9975185990 0.9960436821 0.9973087907 + 0.9975516796 0.9961054921 0.9973604083 + 0.9975848198 0.9961673021 0.9974119067 + 0.9976179004 0.9962291121 0.9974635243 + 0.9976509809 0.9962909222 0.9975150824 + 0.9976841211 0.9963526726 0.9975667000 + 0.9977172017 0.9964144826 0.9976183176 + 0.9977502823 0.9964762926 0.9976698756 + 0.9977834225 0.9965381026 0.9977214932 + 0.9978165030 0.9965999722 0.9977722764 + 0.9978520870 0.9966641068 0.9978203177 + 0.9978877902 0.9967283010 0.9978682995 + 0.9979234934 0.9967923760 0.9979162812 + 0.9979591966 0.9968565702 0.9979643226 + 0.9979950190 0.9969207048 0.9980123043 + 0.9980307221 0.9969848990 0.9980602860 + 0.9980664253 0.9970489740 0.9981083274 + 0.9981021881 0.9971132278 0.9981563091 + 0.9981378913 0.9971773028 0.9982042909 + 0.9981735945 0.9972414970 0.9982522726 + 0.9982103705 0.9973062277 0.9982975721 + 0.9982488155 0.9973716736 0.9983413815 + 0.9982872009 0.9974371791 0.9983850718 + 0.9983255863 0.9975026846 0.9984288812 + 0.9983639717 0.9975680709 0.9984725714 + 0.9984024167 0.9976335764 0.9985163808 + 0.9984408021 0.9976990819 0.9985600710 + 0.9984791875 0.9977645874 0.9986038804 + 0.9985176921 0.9978300929 0.9986475706 + 0.9985560775 0.9978954792 0.9986913800 + 0.9985945225 0.9979609847 0.9987350702 + 0.9986345768 0.9980263114 0.9987767935 + 0.9986755252 0.9980915785 0.9988182187 + 0.9987164140 0.9981567860 0.9988597035 + 0.9987573028 0.9982221127 0.9989011288 + 0.9987981915 0.9982873797 0.9989426136 + 0.9988390803 0.9983525872 0.9989839792 + 0.9988800287 0.9984179139 0.9990255237 + 0.9989209175 0.9984831214 0.9990668893 + 0.9989618063 0.9985483885 0.9991083741 + 0.9990026951 0.9986137152 0.9991497993 + 0.9990435839 0.9986789227 0.9991911054 + 0.9990863204 0.9987418056 0.9992294908 + 0.9991291761 0.9988046288 0.9992678165 + 0.9991720915 0.9988675117 0.9993060827 + 0.9992150068 0.9989302754 0.9993445277 + 0.9992579222 0.9989930987 0.9993827939 + 0.9993007779 0.9990559220 0.9994211793 + 0.9993436933 0.9991186857 0.9994595051 + 0.9993866086 0.9991815090 0.9994978905 + 0.9994295239 0.9992442727 0.9995362163 + 0.9994723797 0.9993070960 0.9995746017 + 0.9995154142 0.9993696809 0.9996126294 + 0.9995594025 0.9994270205 0.9996479154 + 0.9996035099 0.9994843006 0.9996830821 + 0.9996474981 0.9995415807 0.9997183084 + 0.9996916056 0.9995989203 0.9997534752 + 0.9997357130 0.9996562004 0.9997887015 + 0.9997797012 0.9997134805 0.9998239279 + 0.9998238087 0.9997708201 0.9998590946 + 0.9998677969 0.9998281002 0.9998943806 + 0.9999119043 0.9998853803 0.9999296069 + 0.9999558926 0.9999427199 0.9999647737 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_e100s.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_e100s.spi1d index cad63e66..c34146de 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_e100s.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_ektachrome_e100s.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0003622885 0.0002231146 0.0002231146 - 0.0007382494 0.0004596260 0.0004596260 - 0.0011324980 0.0007107497 0.0007107497 - 0.0015447290 0.0009748340 0.0009748340 - 0.0019822840 0.0012541140 0.0012541140 - 0.0024496829 0.0015510730 0.0015510730 - 0.0029426741 0.0018673710 0.0018673710 - 0.0034610520 0.0022024000 0.0022024000 - 0.0040061148 0.0025572821 0.0025572821 - 0.0045792251 0.0029304300 0.0029304300 - 0.0051838872 0.0033235741 0.0033235741 - 0.0058237561 0.0037373570 0.0037373570 - 0.0064960350 0.0041693011 0.0041693011 - 0.0072039221 0.0046230098 0.0046230098 - 0.0079505397 0.0050979760 0.0050979760 - 0.0087367687 0.0055915830 0.0055915830 - 0.0095584840 0.0061070491 0.0061070491 - 0.0104124900 0.0066442229 0.0066442229 - 0.0112989703 0.0071999640 0.0071999640 - 0.0122200102 0.0077770371 0.0077770371 - 0.0131755099 0.0083772372 0.0083772372 - 0.0141713005 0.0089993617 0.0089993617 - 0.0152068604 0.0096444618 0.0096444618 - 0.0162801705 0.0103151305 0.0103151305 - 0.0173835605 0.0110131698 0.0110131698 - 0.0185210407 0.0117386896 0.0117386896 - 0.0196854398 0.0124893300 0.0124893300 - 0.0208813995 0.0132624302 0.0132624302 - 0.0221185796 0.0140622798 0.0140622798 - 0.0234044399 0.0148894899 0.0148894899 - 0.0247398503 0.0157391094 0.0157391094 - 0.0261158608 0.0166204907 0.0166204907 - 0.0275394097 0.0175324902 0.0175324902 - 0.0290060304 0.0184764005 0.0184764005 - 0.0305129606 0.0194491595 0.0194491595 - 0.0320634693 0.0204504505 0.0204504505 - 0.0336536989 0.0214823205 0.0214823205 - 0.0352861397 0.0225437600 0.0225437600 - 0.0369675495 0.0236390196 0.0236390196 - 0.0386981517 0.0247705802 0.0247705802 - 0.0404745191 0.0259348303 0.0259348303 - 0.0422977097 0.0271375105 0.0271375105 - 0.0441726409 0.0283817798 0.0283817798 - 0.0460996106 0.0296590906 0.0296590906 - 0.0480887406 0.0309759099 0.0309759099 - 0.0501275212 0.0323227607 0.0323227607 - 0.0522207692 0.0337067991 0.0337067991 - 0.0543671884 0.0351255499 0.0351255499 - 0.0565663688 0.0365805998 0.0365805998 - 0.0588187315 0.0380711295 0.0380711295 - 0.0611272007 0.0396083593 0.0396083593 - 0.0634748489 0.0411784090 0.0411784090 - 0.0658760667 0.0427908003 0.0427908003 - 0.0683400705 0.0444480814 0.0444480814 - 0.0708511695 0.0461456515 0.0461456515 - 0.0734233931 0.0478809699 0.0478809699 - 0.0760477036 0.0496524908 0.0496524908 - 0.0787281767 0.0514654815 0.0514654815 - 0.0814533383 0.0533178188 0.0533178188 - 0.0842291415 0.0552090518 0.0552090518 - 0.0870640278 0.0571336001 0.0571336001 - 0.0899450928 0.0590961091 0.0590961091 - 0.0928783119 0.0610942282 0.0610942282 - 0.0958742201 0.0631353706 0.0631353706 - 0.0989081487 0.0652145892 0.0652145892 - 0.1019897982 0.0673257634 0.0673257634 - 0.1051272005 0.0694703013 0.0694703013 - 0.1083052009 0.0716492608 0.0716492608 - 0.1115389019 0.0738634914 0.0738634914 - 0.1148212031 0.0761086568 0.0761086568 - 0.1181529984 0.0783852488 0.0783852488 - 0.1215258017 0.0806891099 0.0806891099 - 0.1249411032 0.0830274820 0.0830274820 - 0.1283981055 0.0854015574 0.0854015574 - 0.1318998039 0.0878068879 0.0878068879 - 0.1354435980 0.0902427882 0.0902427882 - 0.1390310973 0.0927055702 0.0927055702 - 0.1426659971 0.0952004492 0.0952004492 - 0.1463364065 0.0977323800 0.0977323800 - 0.1500398070 0.1002930999 0.1002930999 - 0.1537826061 0.1028838009 0.1028838009 - 0.1575697958 0.1055003032 0.1055003032 - 0.1613817066 0.1081380993 0.1081380993 - 0.1652164012 0.1108009964 0.1108009964 - 0.1690932959 0.1134867966 0.1134867966 - 0.1729968935 0.1161940992 0.1161940992 - 0.1769410968 0.1189275980 0.1189275980 - 0.1809352040 0.1216920018 0.1216920018 - 0.1849523038 0.1244781017 0.1244781017 - 0.1889985055 0.1272816956 0.1272816956 - 0.1930781007 0.1301010996 0.1301010996 - 0.1971940994 0.1329389066 0.1329389066 - 0.2013469040 0.1358011961 0.1358011961 - 0.2055312991 0.1386940926 0.1386940926 - 0.2097439021 0.1416130960 0.1416130960 - 0.2139831036 0.1445533037 0.1445533037 - 0.2182556987 0.1475161016 0.1475161016 - 0.2225302011 0.1505060941 0.1505060941 - 0.2268306017 0.1535229981 0.1535229981 - 0.2311580926 0.1565562934 0.1565562934 - 0.2355093956 0.1596101969 0.1596101969 - 0.2398717999 0.1626808047 0.1626808047 - 0.2442272007 0.1657689065 0.1657689065 - 0.2485899031 0.1688731015 0.1688731015 - 0.2529604137 0.1719909012 0.1719909012 - 0.2573277056 0.1751365066 0.1751365066 - 0.2616989017 0.1782941073 0.1782941073 - 0.2660799026 0.1814624071 0.1814624071 - 0.2704606056 0.1846423000 0.1846423000 - 0.2748312056 0.1878450066 0.1878450066 - 0.2791987956 0.1910510063 0.1910510063 - 0.2835668027 0.1942614019 0.1942614019 - 0.2879388928 0.1974803954 0.1974803954 - 0.2923108041 0.2007077038 0.2007077038 - 0.2966752946 0.2039379030 0.2039379030 - 0.3010303974 0.2071726024 0.2071726024 - 0.3053742945 0.2104312927 0.2104312927 - 0.3097003102 0.2136991024 0.2136991024 - 0.3140352070 0.2169768065 0.2169768065 - 0.3183707893 0.2202724963 0.2202724963 - 0.3226906061 0.2235676050 0.2235676050 - 0.3270021975 0.2268693000 0.2268693000 - 0.3313120902 0.2301850021 0.2301850021 - 0.3356218934 0.2334984988 0.2334984988 - 0.3399325907 0.2368156016 0.2368156016 - 0.3442339897 0.2401401997 0.2401401997 - 0.3485251963 0.2434774041 0.2434774041 - 0.3527927101 0.2468263060 0.2468263060 - 0.3570409119 0.2501969039 0.2501969039 - 0.3612729013 0.2535721958 0.2535721958 - 0.3654753864 0.2569538951 0.2569538951 - 0.3696604967 0.2603574097 0.2603574097 - 0.3738316894 0.2637628019 0.2637628019 - 0.3779726923 0.2671697140 0.2671697140 - 0.3820927143 0.2705962062 0.2705962062 - 0.3861967027 0.2740235031 0.2740235031 - 0.3902786970 0.2774519026 0.2774519026 - 0.3943498135 0.2808937132 0.2808937132 - 0.3984076083 0.2843413949 0.2843413949 - 0.4024240077 0.2878027856 0.2878027856 - 0.4064257145 0.2912661135 0.2912661135 - 0.4104036987 0.2947317958 0.2947317958 - 0.4143416882 0.2981860042 0.2981860042 - 0.4182564914 0.3016363978 0.3016363978 - 0.4221282005 0.3050855994 0.3050855994 - 0.4259957075 0.3085319102 0.3085319102 - 0.4298554957 0.3119722009 0.3119722009 - 0.4336968064 0.3154138923 0.3154138923 - 0.4375284910 0.3188566864 0.3188566864 - 0.4413171113 0.3222773075 0.3222773075 - 0.4450969100 0.3256925941 0.3256925941 - 0.4488551915 0.3290956020 0.3290956020 - 0.4526067972 0.3324885964 0.3324885964 - 0.4563522935 0.3358506858 0.3358506858 - 0.4600827098 0.3392089009 0.3392089009 - 0.4638085067 0.3425619900 0.3425619900 - 0.4674848914 0.3459056914 0.3459056914 - 0.4711487889 0.3492426872 0.3492426872 - 0.4747653008 0.3525683880 0.3525683880 - 0.4783576131 0.3558900058 0.3558900058 - 0.4819136858 0.3591865897 0.3591865897 - 0.4854589999 0.3624787927 0.3624787927 - 0.4889962077 0.3657391071 0.3657391071 - 0.4925225973 0.3689970076 0.3689970076 - 0.4960449040 0.3722279966 0.3722279966 - 0.4995129108 0.3754577041 0.3754577041 - 0.5029718280 0.3786748946 0.3786748946 - 0.5064191222 0.3818857074 0.3818857074 - 0.5098642111 0.3850541115 0.3850541115 - 0.5132802725 0.3882220984 0.3882220984 - 0.5166903138 0.3913877010 0.3913877010 - 0.5200588107 0.3945508003 0.3945508003 - 0.5234236717 0.3976989985 0.3976989985 - 0.5267717838 0.4008451104 0.4008451104 - 0.5301045775 0.4039750993 0.4039750993 - 0.5333794951 0.4071030021 0.4071030021 - 0.5366390944 0.4102006853 0.4102006853 - 0.5398436189 0.4132984877 0.4132984877 - 0.5430372953 0.4163655937 0.4163655937 - 0.5461881757 0.4194326997 0.4194326997 - 0.5493322015 0.4224604964 0.4224604964 - 0.5524412990 0.4254842997 0.4254842997 - 0.5555446744 0.4284946918 0.4284946918 - 0.5586010814 0.4315015972 0.4315015972 - 0.5616549253 0.4344901145 0.4344901145 - 0.5646228194 0.4374687970 0.4374687970 - 0.5675905943 0.4404281080 0.4404281080 - 0.5705261230 0.4433673024 0.4433673024 - 0.5734590292 0.4462912083 0.4462912083 - 0.5763446093 0.4491803050 0.4491803050 - 0.5792183876 0.4520643055 0.4520643055 - 0.5820407867 0.4549018145 0.4549018145 - 0.5848355889 0.4577394128 0.4577394128 - 0.5875927210 0.4605427086 0.4605427086 - 0.5903086066 0.4633406997 0.4633406997 - 0.5930045843 0.4661261141 0.4661261141 - 0.5956503153 0.4689033926 0.4689033926 - 0.5982949138 0.4716737866 0.4716737866 - 0.6009235978 0.4744301140 0.4744301140 - 0.6035524011 0.4771854877 0.4771854877 - 0.6061716080 0.4799031019 0.4799031019 - 0.6087887883 0.4826206863 0.4826206863 - 0.6113740206 0.4853239954 0.4853239954 - 0.6139330864 0.4880208969 0.4880208969 - 0.6164814830 0.4907048047 0.4907048047 - 0.6189994812 0.4933629036 0.4933629036 - 0.6215174794 0.4960210919 0.4960210919 - 0.6240079999 0.4986290932 0.4986290932 - 0.6264969110 0.5012347102 0.5012347102 - 0.6289771795 0.5038244128 0.5038244128 - 0.6314516068 0.5064008236 0.5064008236 - 0.6339148283 0.5089727044 0.5089727044 - 0.6363407969 0.5115088224 0.5115088224 - 0.6387668252 0.5140448213 0.5140448213 - 0.6411426067 0.5165610909 0.5165610909 - 0.6435087919 0.5190668106 0.5190668106 - 0.6458575130 0.5215663910 0.5215663910 - 0.6481820941 0.5240356922 0.5240356922 - 0.6505066752 0.5265049934 0.5265049934 - 0.6528093815 0.5289555192 0.5289555192 - 0.6551114917 0.5313962102 0.5313962102 - 0.6573888063 0.5338327885 0.5338327885 - 0.6596413255 0.5362383723 0.5362383723 - 0.6618937254 0.5386438966 0.5386438966 - 0.6641113162 0.5410385132 0.5410385132 - 0.6663289070 0.5434240103 0.5434240103 - 0.6685289741 0.5458095074 0.5458095074 - 0.6707097888 0.5481578112 0.5481578112 - 0.6728904843 0.5505042076 0.5505042076 - 0.6750382781 0.5528417826 0.5528417826 - 0.6771832705 0.5551611185 0.5551611185 - 0.6793134809 0.5574803948 0.5574803948 - 0.6814150810 0.5597792864 0.5597792864 - 0.6835166812 0.5620682836 0.5620682836 - 0.6855754852 0.5643572211 0.5643572211 - 0.6876184940 0.5665962100 0.5665962100 - 0.6896557808 0.5688347220 0.5688347220 - 0.6916378736 0.5710633993 0.5710633993 - 0.6936199069 0.5732669830 0.5732669830 - 0.6955876946 0.5754706860 0.5754706860 - 0.6975364089 0.5776560903 0.5776560903 - 0.6994851232 0.5798259974 0.5798259974 - 0.7013980150 0.5819959044 0.5819959044 - 0.7032977939 0.5841305852 0.5841305852 - 0.7051973939 0.5862547755 0.5862547755 - 0.7070760727 0.5883789062 0.5883789062 - 0.7089548111 0.5904625058 0.5904625058 - 0.7108243704 0.5925447941 0.5925447941 - 0.7126672864 0.5946221948 0.5946221948 - 0.7145102024 0.5966691971 0.5966691971 - 0.7163286209 0.5987161994 0.5987161994 - 0.7181193829 0.6007555127 0.6007555127 - 0.7199102044 0.6027747989 0.6027747989 - 0.7216753960 0.6047940254 0.6047940254 - 0.7234271765 0.6068055034 0.6068055034 - 0.7251790166 0.6088048220 0.6088048220 - 0.7269017100 0.6108040214 0.6108040214 - 0.7286177278 0.6127884984 0.6127884984 - 0.7303336263 0.6147556901 0.6147556901 - 0.7319886088 0.6167228818 0.6167228818 - 0.7336395979 0.6186692119 0.6186692119 - 0.7352887988 0.6205952168 0.6205952168 - 0.7368847132 0.6225212216 0.6225212216 - 0.7384806275 0.6244304776 0.6244304776 - 0.7400730848 0.6263242960 0.6263242960 - 0.7416328788 0.6282181740 0.6282181740 - 0.7431927919 0.6301015019 0.6301015019 - 0.7447494268 0.6319742799 0.6319742799 - 0.7462829947 0.6338471174 0.6338471174 - 0.7478165030 0.6356995106 0.6356995106 - 0.7493439913 0.6375269890 0.6375269890 - 0.7508273125 0.6393544078 0.6393544078 - 0.7523106933 0.6411694288 0.6411694288 - 0.7537910938 0.6429628134 0.6429628134 - 0.7552406788 0.6447560787 0.6447560787 - 0.7566903234 0.6465445161 0.6465445161 - 0.7581396103 0.6483175755 0.6483175755 - 0.7595729828 0.6500906944 0.6500906944 - 0.7610062957 0.6518586874 0.6518586874 - 0.7624397278 0.6535770297 0.6535770297 - 0.7638341784 0.6552951932 0.6552951932 - 0.7652254105 0.6570134759 0.6570134759 - 0.7666165233 0.6587092280 0.6587092280 - 0.7679901719 0.6604024768 0.6604024768 - 0.7693589926 0.6620957851 0.6620957851 - 0.7707278728 0.6637632847 0.6637632847 - 0.7720705867 0.6654189229 0.6654189229 - 0.7733970284 0.6670743823 0.6670743823 - 0.7747234702 0.6687203050 0.6687203050 - 0.7760393023 0.6703534126 0.6703534126 - 0.7773399949 0.6719865203 0.6719865203 - 0.7786408067 0.6736162901 0.6736162901 - 0.7799372077 0.6752253771 0.6752253771 - 0.7812135220 0.6768345833 0.6768345833 - 0.7824897170 0.6784437895 0.6784437895 - 0.7837659717 0.6800181270 0.6800181270 - 0.7850177884 0.6815842986 0.6815842986 - 0.7862669826 0.6831504703 0.6831504703 - 0.7875161767 0.6846908927 0.6846908927 - 0.7887520194 0.6861993074 0.6861993074 - 0.7899786830 0.6877077222 0.6877077222 - 0.7912054062 0.6892145872 0.6892145872 - 0.7924262285 0.6906890869 0.6906890869 - 0.7936294079 0.6921635866 0.6921635866 - 0.7948325872 0.6936382055 0.6936382055 - 0.7960357070 0.6950960159 0.6950960159 - 0.7972220778 0.6965432167 0.6965432167 - 0.7984061241 0.6979902983 0.6979902983 - 0.7995899916 0.6994342804 0.6994342804 - 0.8007580042 0.7008578181 0.7008578181 - 0.8019068241 0.7022812963 0.7022812963 - 0.8030557036 0.7037047744 0.7037047744 - 0.8042045236 0.7051180005 0.7051180005 - 0.8053393960 0.7065249085 0.7065249085 - 0.8064743876 0.7079318166 0.7079318166 - 0.8076093793 0.7093364000 0.7093364000 - 0.8087307215 0.7107117772 0.7107117772 - 0.8098388910 0.7120870948 0.7120870948 - 0.8109471798 0.7134624124 0.7134624124 - 0.8120555282 0.7148199081 0.7148199081 - 0.8131191134 0.7161585093 0.7161585093 - 0.8141819239 0.7174972296 0.7174972296 - 0.8152446747 0.7188357711 0.7188357711 - 0.8162903190 0.7201492190 0.7201492190 - 0.8173117042 0.7214584947 0.7214584947 - 0.8183330894 0.7227677703 0.7227677703 - 0.8193544149 0.7240737081 0.7240737081 - 0.8203511238 0.7253654003 0.7253654003 - 0.8213419914 0.7266569734 0.7266569734 - 0.8223329186 0.7279486060 0.7279486060 - 0.8233172894 0.7292181849 0.7292181849 - 0.8242688179 0.7304645777 0.7304645777 - 0.8252202272 0.7317109704 0.7317109704 - 0.8261716962 0.7329574227 0.7329574227 - 0.8271054029 0.7341820002 0.7341820002 - 0.8280197978 0.7353988290 0.7353988290 - 0.8289341927 0.7366157174 0.7366157174 - 0.8298485875 0.7378326058 0.7378326058 - 0.8307464719 0.7390285134 0.7390285134 - 0.8316389918 0.7402232885 0.7402232885 - 0.8325315118 0.7414181828 0.7414181828 - 0.8334239721 0.7426074743 0.7426074743 - 0.8342829943 0.7437586784 0.7437586784 - 0.8351417184 0.7449098229 0.7449098229 - 0.8360003829 0.7460609078 0.7460609078 - 0.8368576169 0.7472000122 0.7472000122 - 0.8377087116 0.7483038902 0.7483038902 - 0.8385598063 0.7494078875 0.7494078875 - 0.8394109011 0.7505118251 0.7505118251 - 0.8402518034 0.7516049743 0.7516049743 - 0.8410723209 0.7526779175 0.7526779175 - 0.8418927789 0.7537506819 0.7537506819 - 0.8427134156 0.7548235059 0.7548235059 - 0.8435238004 0.7558841705 0.7558841705 - 0.8443217278 0.7569258809 0.7569258809 - 0.8451194763 0.7579675913 0.7579675913 - 0.8459174037 0.7590093017 0.7590093017 - 0.8467006087 0.7600433230 0.7600433230 - 0.8474699259 0.7610656023 0.7610656023 - 0.8482393026 0.7620878220 0.7620878220 - 0.8490086198 0.7631099820 0.7631099820 - 0.8497787714 0.7641208768 0.7641208768 - 0.8505498171 0.7651112080 0.7651112080 - 0.8513206840 0.7661014795 0.7661014795 - 0.8520916104 0.7670918107 0.7670918107 - 0.8528512120 0.7680724263 0.7680724263 - 0.8536006212 0.7690268159 0.7690268159 - 0.8543499708 0.7699812055 0.7699812055 - 0.8550993800 0.7709355950 0.7709355950 - 0.8558359146 0.7718878984 0.7718878984 - 0.8565574884 0.7728270292 0.7728270292 - 0.8572790027 0.7737662196 0.7737662196 - 0.8580005765 0.7747054100 0.7747054100 - 0.8587099910 0.7756444812 0.7756444812 - 0.8593978286 0.7765566707 0.7765566707 - 0.8600856066 0.7774680257 0.7774680257 - 0.8607733846 0.7783793211 0.7783793211 - 0.8614575863 0.7792906165 0.7792906165 - 0.8621283770 0.7801936865 0.7801936865 - 0.8627992272 0.7810940146 0.7810940146 - 0.8634700179 0.7819942832 0.7819942832 - 0.8641406894 0.7828946114 0.7828946114 - 0.8648099899 0.7837845087 0.7837845087 - 0.8654794097 0.7846636772 0.7846636772 - 0.8661487103 0.7855427861 0.7855427861 - 0.8668180108 0.7864218950 0.7864218950 - 0.8674687743 0.7872971892 0.7872971892 - 0.8681142926 0.7881550193 0.7881550193 - 0.8687598109 0.7890126705 0.7890126705 - 0.8694053292 0.7898705006 0.7898705006 - 0.8700295091 0.7907282114 0.7907282114 - 0.8706318736 0.7915742993 0.7915742993 - 0.8712342978 0.7924178839 0.7924178839 - 0.8718366027 0.7932615280 0.7932615280 - 0.8724340200 0.7941049933 0.7941049933 - 0.8730055094 0.7949374914 0.7949374914 - 0.8735769987 0.7957543135 0.7957543135 - 0.8741484880 0.7965711951 0.7965711951 - 0.8747199774 0.7973880172 0.7973880172 - 0.8752837777 0.7982048988 0.7982048988 - 0.8758454919 0.7989869118 0.7989869118 - 0.8764070868 0.7997676134 0.7997676134 - 0.8769688010 0.8005483150 0.8005483150 - 0.8775206208 0.8013290167 0.8013290167 - 0.8780546188 0.8021016717 0.8021016717 - 0.8785886765 0.8028652072 0.8028652072 - 0.8791226745 0.8036286831 0.8036286831 - 0.8796567917 0.8043922186 0.8043922186 - 0.8801620007 0.8051556945 0.8051556945 - 0.8806633949 0.8058984876 0.8058984876 - 0.8811649084 0.8066393733 0.8066393733 - 0.8816663027 0.8073803186 0.8073803186 - 0.8821567297 0.8081210852 0.8081210852 - 0.8826276064 0.8088607192 0.8088607192 - 0.8830984831 0.8095976114 0.8095976114 - 0.8835694194 0.8103343844 0.8103343844 - 0.8840402961 0.8110712171 0.8110712171 - 0.8845028877 0.8118081093 0.8118081093 - 0.8849632740 0.8125373125 0.8125373125 - 0.8854237199 0.8132628202 0.8132628202 - 0.8858841062 0.8139883280 0.8139883280 - 0.8863461018 0.8147137761 0.8147137761 - 0.8868156075 0.8154392838 0.8154392838 - 0.8872851133 0.8161317110 0.8161317110 - 0.8877546191 0.8168231845 0.8168231845 - 0.8882240057 0.8175147176 0.8175147176 - 0.8886895180 0.8182061911 0.8182061911 - 0.8891507983 0.8188943863 0.8188943863 - 0.8896121979 0.8195710182 0.8195710182 - 0.8900735974 0.8202475905 0.8202475905 - 0.8905348778 0.8209241033 0.8209241033 - 0.8909785748 0.8216006756 0.8216006756 - 0.8914170861 0.8222699761 0.8222699761 - 0.8918557167 0.8229292035 0.8229292035 - 0.8922942281 0.8235884905 0.8235884905 - 0.8927327991 0.8242477179 0.8242477179 - 0.8931676149 0.8249068856 0.8249068856 - 0.8936023712 0.8255603909 0.8255603909 - 0.8940371871 0.8262094259 0.8262094259 - 0.8944720030 0.8268585205 0.8268585205 - 0.8949096203 0.8275076151 0.8275076151 - 0.8953595161 0.8281567097 0.8281567097 - 0.8958094716 0.8287855983 0.8287855983 - 0.8962594271 0.8294041753 0.8294041753 - 0.8967093825 0.8300228119 0.8300228119 - 0.8971595168 0.8306415081 0.8306415081 - 0.8976098895 0.8312600851 0.8312600851 - 0.8980603218 0.8318728209 0.8318728209 - 0.8985106945 0.8324831128 0.8324831128 - 0.8989611268 0.8330934048 0.8330934048 - 0.8994054198 0.8337035775 0.8337035775 - 0.8998395801 0.8343139291 0.8343139291 - 0.9002736807 0.8349158764 0.8349158764 - 0.9007077813 0.8355141878 0.8355141878 - 0.9011420012 0.8361124992 0.8361124992 - 0.9015651941 0.8367106915 0.8367106915 - 0.9019711018 0.8373090029 0.8373090029 - 0.9023771286 0.8378884792 0.8378884792 - 0.9027829766 0.8384566903 0.8384566903 - 0.9031888843 0.8390247822 0.8390247822 - 0.9035964012 0.8395929933 0.8395929933 - 0.9040068984 0.8401612043 0.8401612043 - 0.9044172764 0.8407253027 0.8407253027 - 0.9048277140 0.8412854075 0.8412854075 - 0.9052380919 0.8418455124 0.8418455124 - 0.9056466222 0.8424056768 0.8424056768 - 0.9060494900 0.8429657817 0.8429657817 - 0.9064524770 0.8435230255 0.8435230255 - 0.9068554044 0.8440740705 0.8440740705 - 0.9072582722 0.8446251750 0.8446251750 - 0.9076607823 0.8451762795 0.8451762795 - 0.9080597162 0.8457275033 0.8457275033 - 0.9084585905 0.8462774158 0.8462774158 - 0.9088575244 0.8468161225 0.8468161225 - 0.9092563987 0.8473547101 0.8473547101 - 0.9096552730 0.8478932977 0.8478932977 - 0.9100391865 0.8484320045 0.8484320045 - 0.9104216099 0.8489705920 0.8489705920 - 0.9108039141 0.8494958878 0.8494958878 - 0.9111862779 0.8500180244 0.8500180244 - 0.9115687013 0.8505399823 0.8505399823 - 0.9119420052 0.8510619998 0.8510619998 - 0.9123104215 0.8515840173 0.8515840173 - 0.9126788974 0.8521022201 0.8521022201 - 0.9130473137 0.8526157737 0.8526157737 - 0.9134156704 0.8531293273 0.8531293273 - 0.9137811065 0.8536428809 0.8536428809 - 0.9141399860 0.8541564941 0.8541564941 - 0.9144989848 0.8546689749 0.8546689749 - 0.9148579240 0.8551610112 0.8551610112 - 0.9152169228 0.8556529880 0.8556529880 - 0.9155759215 0.8561450243 0.8561450243 - 0.9159141779 0.8566370010 0.8566370010 - 0.9162515998 0.8571289182 0.8571289182 - 0.9165889025 0.8576136231 0.8576136231 - 0.9169263244 0.8580930233 0.8580930233 - 0.9172636867 0.8585724235 0.8585724235 - 0.9175904989 0.8590518832 0.8590518832 - 0.9179077148 0.8595312834 0.8595312834 - 0.9182248116 0.8600099087 0.8600099087 - 0.9185420275 0.8604763150 0.8604763150 - 0.9188591242 0.8609427810 0.8609427810 - 0.9191758037 0.8614091873 0.8614091873 - 0.9194787145 0.8618755937 0.8618755937 - 0.9197816253 0.8623420000 0.8623420000 - 0.9200844169 0.8628141284 0.8628141284 - 0.9203873277 0.8632925153 0.8632925153 - 0.9206901789 0.8637707829 0.8637707829 - 0.9209961295 0.8642491102 0.8642491102 - 0.9213051796 0.8647273779 0.8647273779 - 0.9216142893 0.8652058244 0.8652058244 - 0.9219235182 0.8656808734 0.8656808734 - 0.9222326279 0.8661553860 0.8661553860 - 0.9225417972 0.8666298985 0.8666298985 - 0.9228463769 0.8671044111 0.8671044111 - 0.9231504202 0.8675789237 0.8675789237 - 0.9234544039 0.8680508733 0.8680508733 - 0.9237583876 0.8685060740 0.8685060740 - 0.9240623713 0.8689612746 0.8689612746 - 0.9243630767 0.8694164753 0.8694164753 - 0.9246509075 0.8698716164 0.8698716164 - 0.9249386787 0.8703268170 0.8703268170 - 0.9252265096 0.8707792163 0.8707792163 - 0.9255142212 0.8712267280 0.8712267280 - 0.9258019924 0.8716741204 0.8716741204 - 0.9260783792 0.8721215725 0.8721215725 - 0.9263415933 0.8725690246 0.8725690246 - 0.9266049266 0.8730164766 0.8730164766 - 0.9268680811 0.8734666109 0.8734666109 - 0.9271314144 0.8739191890 0.8739191890 - 0.9273946285 0.8743718266 0.8743718266 - 0.9276558757 0.8748244047 0.8748244047 - 0.9279161096 0.8752769232 0.8752769232 - 0.9281764030 0.8757295012 0.8757295012 - 0.9284366965 0.8761757016 0.8761757016 - 0.9286969900 0.8766183257 0.8766183257 - 0.9289572835 0.8770608902 0.8770608902 - 0.9292187095 0.8775033951 0.8775033951 - 0.9294803143 0.8779460192 0.8779460192 - 0.9297419190 0.8783885837 0.8783885837 - 0.9300035238 0.8788154125 0.8788154125 - 0.9302651286 0.8792347908 0.8792347908 - 0.9305267930 0.8796542287 0.8796542287 - 0.9307944179 0.8800736070 0.8800736070 - 0.9310628176 0.8804931045 0.8804931045 - 0.9313312173 0.8809124827 0.8809124827 - 0.9315996170 0.8813328147 0.8813328147 - 0.9318680167 0.8817536831 0.8817536831 - 0.9321364164 0.8821746111 0.8821746111 - 0.9324026108 0.8825954199 0.8825954199 - 0.9326686263 0.8830162883 0.8830162883 - 0.9329345822 0.8834370971 0.8834370971 - 0.9332005978 0.8838580251 0.8838580251 - 0.9334664941 0.8842790127 0.8842790127 - 0.9337325096 0.8846998811 0.8846998811 - 0.9339888096 0.8851209283 0.8851209283 - 0.9342433810 0.8855419159 0.8855419159 - 0.9344980121 0.8859627843 0.8859627843 - 0.9347525835 0.8863782883 0.8863782883 - 0.9350072145 0.8867862821 0.8867862821 - 0.9352617860 0.8871942759 0.8871942759 - 0.9355120063 0.8876023293 0.8876023293 - 0.9357606769 0.8880103230 0.8880103230 - 0.9360094070 0.8884181976 0.8884181976 - 0.9362581968 0.8888232708 0.8888232708 - 0.9365069270 0.8892168999 0.8892168999 - 0.9367555976 0.8896105289 0.8896105289 - 0.9369971156 0.8900040984 0.8900040984 - 0.9372326732 0.8903976083 0.8903976083 - 0.9374684095 0.8907911777 0.8907911777 - 0.9377040267 0.8911848068 0.8911848068 - 0.9379395843 0.8915761113 0.8915761113 - 0.9381753206 0.8919671178 0.8919671178 - 0.9384089708 0.8923581839 0.8923581839 - 0.9386385083 0.8927491903 0.8927491903 - 0.9388679266 0.8931403160 0.8931403160 - 0.9390972853 0.8935313225 0.8935313225 - 0.9393268228 0.8939135075 0.8939135075 - 0.9395561814 0.8942884803 0.8942884803 - 0.9397855997 0.8946635723 0.8946635723 - 0.9400131106 0.8950387239 0.8950387239 - 0.9402405024 0.8954138160 0.8954138160 - 0.9404678941 0.8957887888 0.8957887888 - 0.9406952858 0.8961619735 0.8961619735 - 0.9409226775 0.8965228200 0.8965228200 - 0.9411501288 0.8968836069 0.8968836069 - 0.9413790703 0.8972443938 0.8972443938 - 0.9416092038 0.8976051211 0.8976051211 - 0.9418392777 0.8979659081 0.8979659081 - 0.9420694113 0.8983266950 0.8983266950 - 0.9422994852 0.8986871243 0.8986871243 - 0.9425296187 0.8990470767 0.8990470767 - 0.9427592158 0.8994072080 0.8994072080 - 0.9429851174 0.8997672796 0.8997672796 - 0.9432110190 0.9001272917 0.9001272917 - 0.9434369206 0.9004874229 0.9004874229 - 0.9436628222 0.9008464217 0.9008464217 - 0.9438887239 0.9011930227 0.9011930227 - 0.9441146255 0.9015396237 0.9015396237 - 0.9443339109 0.9018862247 0.9018862247 - 0.9445490241 0.9022328258 0.9022328258 - 0.9447640777 0.9025794268 0.9025794268 - 0.9449791908 0.9029260278 0.9029260278 - 0.9451943040 0.9032617211 0.9032617211 - 0.9454094172 0.9035840034 0.9035840034 - 0.9456244111 0.9039062262 0.9039062262 - 0.9458345175 0.9042283893 0.9042283893 - 0.9460445046 0.9045506120 0.9045506120 - 0.9462546110 0.9048727751 0.9048727751 - 0.9464645982 0.9051949978 0.9051949978 - 0.9466745853 0.9055181146 0.9055181146 - 0.9468846917 0.9058414102 0.9058414102 - 0.9470942020 0.9061647058 0.9061647058 - 0.9473028183 0.9064880013 0.9064880013 - 0.9475114942 0.9068112969 0.9068112969 - 0.9477201104 0.9071347117 0.9071347117 - 0.9479287267 0.9074580073 0.9074580073 - 0.9481372833 0.9077845216 0.9077845216 - 0.9483458996 0.9081110954 0.9081110954 - 0.9485498071 0.9084377289 0.9084377289 - 0.9487506747 0.9087643027 0.9087643027 - 0.9489514828 0.9090908766 0.9090908766 - 0.9491522908 0.9094176292 0.9094176292 - 0.9493530989 0.9097433090 0.9097433090 - 0.9495540261 0.9100611806 0.9100611806 - 0.9497547746 0.9103791714 0.9103791714 - 0.9499650002 0.9106972218 0.9106972218 - 0.9501776099 0.9110150933 0.9110150933 - 0.9503902197 0.9113330841 0.9113330841 - 0.9506028295 0.9116510749 0.9116510749 - 0.9508153796 0.9119694829 0.9119694829 - 0.9510279894 0.9122899771 0.9122899771 - 0.9512405992 0.9126105905 0.9126105905 - 0.9514577985 0.9129310846 0.9129310846 - 0.9516754150 0.9132516980 0.9132516980 - 0.9518930912 0.9135721922 0.9135721922 - 0.9521107078 0.9138928056 0.9138928056 - 0.9523283839 0.9142141938 0.9142141938 - 0.9525460005 0.9145398736 0.9145398736 - 0.9527636766 0.9148656130 0.9148656130 - 0.9529777169 0.9151912928 0.9151912928 - 0.9531913996 0.9155169725 0.9155169725 - 0.9534052014 0.9158427119 0.9158427119 - 0.9536188841 0.9161683917 0.9161683917 - 0.9538326859 0.9164946079 0.9164946079 - 0.9540464878 0.9168255925 0.9168255925 - 0.9542601705 0.9171565771 0.9171565771 - 0.9544739723 0.9174876213 0.9174876213 - 0.9546877146 0.9178186059 0.9178186059 - 0.9549015164 0.9181495905 0.9181495905 - 0.9551151991 0.9184805155 0.9184805155 - 0.9553288817 0.9188115001 0.9188115001 - 0.9555426836 0.9191362858 0.9191362858 - 0.9557564259 0.9194607139 0.9194607139 - 0.9559655190 0.9197850823 0.9197850823 - 0.9561733007 0.9201093912 0.9201093912 - 0.9563812017 0.9204338193 0.9204338193 - 0.9565889835 0.9207581878 0.9207581878 - 0.9567968845 0.9210826159 0.9210826159 - 0.9570047259 0.9214031100 0.9214031100 - 0.9572126269 0.9217219949 0.9217219949 - 0.9574205279 0.9220409989 0.9220409989 - 0.9576283097 0.9223598838 0.9223598838 - 0.9578362107 0.9226788878 0.9226788878 - 0.9580441117 0.9229977727 0.9229977727 - 0.9582520127 0.9233167768 0.9233167768 - 0.9584599137 0.9236333966 0.9236333966 - 0.9586678147 0.9239464998 0.9239464998 - 0.9588736892 0.9242596030 0.9242596030 - 0.9590756893 0.9245728254 0.9245728254 - 0.9592776895 0.9248859286 0.9248859286 - 0.9594796896 0.9251989722 0.9251989722 - 0.9596816897 0.9255120754 0.9255120754 - 0.9598835707 0.9258251786 0.9258251786 - 0.9600855708 0.9261450171 0.9261450171 - 0.9602875710 0.9264646769 0.9264646769 - 0.9604778290 0.9267843962 0.9267843962 - 0.9606677890 0.9271041751 0.9271041751 - 0.9608578086 0.9274238944 0.9274238944 - 0.9610478282 0.9277436137 0.9277436137 - 0.9612377882 0.9280633926 0.9280633926 - 0.9614278078 0.9283816814 0.9283816814 - 0.9616177082 0.9286987782 0.9286987782 - 0.9617974758 0.9290158749 0.9290158749 - 0.9619693160 0.9293330908 0.9293330908 - 0.9621409774 0.9296501875 0.9296501875 - 0.9623126984 0.9299672842 0.9299672842 - 0.9624844193 0.9302843809 0.9302843809 - 0.9626560807 0.9306014776 0.9306014776 - 0.9628278017 0.9309071898 0.9309071898 - 0.9629982710 0.9312127233 0.9312127233 - 0.9631454945 0.9315180779 0.9315180779 - 0.9632925987 0.9318236113 0.9318236113 - 0.9634398222 0.9321290851 0.9321290851 - 0.9635869861 0.9324346185 0.9324346185 - 0.9637340903 0.9327399731 0.9327399731 - 0.9638813138 0.9330422282 0.9330422282 - 0.9640284777 0.9333382249 0.9333382249 - 0.9641625285 0.9336342812 0.9336342812 - 0.9642810822 0.9339302778 0.9339302778 - 0.9643996954 0.9342262745 0.9342262745 - 0.9645183086 0.9345223904 0.9345223904 - 0.9646369219 0.9348183870 0.9348183870 - 0.9647554755 0.9351143837 0.9351143837 - 0.9648740888 0.9354104996 0.9354104996 - 0.9649927020 0.9357064962 0.9357064962 - 0.9651054740 0.9360026121 0.9360026121 - 0.9652165771 0.9362986088 0.9362986088 - 0.9653276801 0.9365946054 0.9365946054 - 0.9654389024 0.9368907213 0.9368907213 - 0.9655500054 0.9371867180 0.9371867180 - 0.9656612277 0.9374827743 0.9374827743 - 0.9657722712 0.9377809167 0.9377809167 - 0.9658837914 0.9380794764 0.9380794764 - 0.9660049081 0.9383779764 0.9383779764 - 0.9661260247 0.9386765957 0.9386765957 - 0.9662470818 0.9389750957 0.9389750957 - 0.9663681984 0.9392737150 0.9392737150 - 0.9664893150 0.9395722151 0.9395722151 - 0.9666103721 0.9398707747 0.9398707747 - 0.9667314887 0.9401614070 0.9401614070 - 0.9668537974 0.9404516220 0.9404516220 - 0.9669811130 0.9407417774 0.9407417774 - 0.9671084285 0.9410318732 0.9410318732 - 0.9672356844 0.9413220882 0.9413220882 - 0.9673628807 0.9416123033 0.9416123033 - 0.9674901962 0.9419023991 0.9419023991 - 0.9676175117 0.9421926141 0.9421926141 - 0.9677448273 0.9424642920 0.9424642920 - 0.9678722024 0.9427356124 0.9427356124 - 0.9680001140 0.9430068731 0.9430068731 - 0.9681280255 0.9432781935 0.9432781935 - 0.9682558775 0.9435495138 0.9435495138 - 0.9683837891 0.9438207150 0.9438207150 - 0.9685117006 0.9440919757 0.9440919757 - 0.9686396122 0.9443632960 0.9443632960 - 0.9687675238 0.9446105957 0.9446105957 - 0.9688940048 0.9448559284 0.9448559284 - 0.9690169096 0.9451010823 0.9451010823 - 0.9691396952 0.9453462958 0.9453462958 - 0.9692625999 0.9455916286 0.9455916286 - 0.9693853855 0.9458367825 0.9458367825 - 0.9695081711 0.9460821152 0.9460821152 - 0.9696310759 0.9463273287 0.9463273287 - 0.9697539210 0.9465690255 0.9465690255 - 0.9698749781 0.9468097091 0.9468097091 - 0.9699872136 0.9470502734 0.9470502734 - 0.9700993896 0.9472910166 0.9472910166 - 0.9702115059 0.9475315809 0.9475315809 - 0.9703236818 0.9477723241 0.9477723241 - 0.9704359174 0.9480128884 0.9480128884 - 0.9705479741 0.9482535124 0.9482535124 - 0.9706602097 0.9484931827 0.9484931827 - 0.9707723260 0.9487320185 0.9487320185 - 0.9708713889 0.9489706755 0.9489706755 - 0.9709702730 0.9492095113 0.9492095113 - 0.9710690975 0.9494482875 0.9494482875 - 0.9711679220 0.9496871233 0.9496871233 - 0.9712666869 0.9499258995 0.9499258995 - 0.9713655114 0.9501646757 0.9501646757 - 0.9714642763 0.9504017234 0.9504017234 - 0.9715631008 0.9506325722 0.9506325722 - 0.9716647267 0.9508634806 0.9508634806 - 0.9717674255 0.9510943890 0.9510943890 - 0.9718701243 0.9513254166 0.9513254166 - 0.9719728231 0.9515563250 0.9515563250 - 0.9720755219 0.9517871737 0.9517871737 - 0.9721782207 0.9520180821 0.9520180821 - 0.9722809792 0.9522489905 0.9522489905 - 0.9723836780 0.9524822235 0.9524822235 - 0.9724901915 0.9527158737 0.9527158737 - 0.9726037979 0.9529495835 0.9529495835 - 0.9727175236 0.9531832933 0.9531832933 - 0.9728311896 0.9534170032 0.9534170032 - 0.9729447961 0.9536505938 0.9536505938 - 0.9730585217 0.9538843036 0.9538843036 - 0.9731721878 0.9541180134 0.9541180134 - 0.9732859135 0.9543501735 0.9543501735 - 0.9733995199 0.9545791745 0.9545791745 - 0.9735301733 0.9548081160 0.9548081160 - 0.9736629128 0.9550369978 0.9550369978 - 0.9737957120 0.9552658796 0.9552658796 - 0.9739285111 0.9554948211 0.9554948211 - 0.9740611911 0.9557238221 0.9557238221 - 0.9741939902 0.9559527040 0.9559527040 - 0.9743267894 0.9561815858 0.9561815858 - 0.9744595289 0.9564020038 0.9564020038 - 0.9745973945 0.9566190243 0.9566190243 - 0.9747449160 0.9568361044 0.9568361044 - 0.9748924971 0.9570531249 0.9570531249 - 0.9750400186 0.9572700858 0.9572700858 - 0.9751874804 0.9574871063 0.9574871063 - 0.9753351212 0.9577041268 0.9577041268 - 0.9754825830 0.9579210877 0.9579210877 - 0.9756301045 0.9581381083 0.9581381083 - 0.9757776856 0.9583560228 0.9583560228 - 0.9759315848 0.9585739970 0.9585739970 - 0.9760882258 0.9587919116 0.9587919116 - 0.9762448072 0.9590098262 0.9590098262 - 0.9764013886 0.9592276812 0.9592276812 - 0.9765580297 0.9594457150 0.9594457150 - 0.9767146111 0.9596636295 0.9596636295 - 0.9768711925 0.9598814845 0.9598814845 - 0.9770277739 0.9601004124 0.9601004124 - 0.9771844149 0.9603220224 0.9603220224 - 0.9773445129 0.9605436921 0.9605436921 - 0.9775046706 0.9607654214 0.9607654214 - 0.9776650071 0.9609870911 0.9609870911 - 0.9778252244 0.9612088203 0.9612088203 - 0.9779855013 0.9614304900 0.9614304900 - 0.9781457186 0.9616522193 0.9616522193 - 0.9783058763 0.9618738890 0.9618738890 - 0.9784662127 0.9620940089 0.9620940089 - 0.9786261916 0.9623115063 0.9623115063 - 0.9787849188 0.9625288844 0.9625288844 - 0.9789437056 0.9627463818 0.9627463818 - 0.9791023731 0.9629638195 0.9629638195 - 0.9792612195 0.9631813169 0.9631813169 - 0.9794198871 0.9633988142 0.9633988142 - 0.9795786738 0.9636161923 0.9636161923 - 0.9797374010 0.9638336897 0.9638336897 - 0.9798961878 0.9640504122 0.9640504122 - 0.9800534844 0.9642658830 0.9642658830 - 0.9802057147 0.9644814730 0.9644814730 - 0.9803580046 0.9646970034 0.9646970034 - 0.9805101752 0.9649125934 0.9649125934 - 0.9806625247 0.9651281834 0.9651281834 - 0.9808148146 0.9653437138 0.9653437138 - 0.9809669852 0.9655593038 0.9655593038 - 0.9811192751 0.9657747746 0.9657747746 - 0.9812715054 0.9659873843 0.9659873843 - 0.9814214110 0.9661936760 0.9661936760 - 0.9815621972 0.9664000273 0.9664000273 - 0.9817029834 0.9666063190 0.9666063190 - 0.9818438292 0.9668126106 0.9668126106 - 0.9819844961 0.9670189023 0.9670189023 - 0.9821252823 0.9672251940 0.9672251940 - 0.9822661281 0.9674314260 0.9674314260 - 0.9824067950 0.9676377177 0.9676377177 - 0.9825475812 0.9678443074 0.9678443074 - 0.9826868773 0.9680523872 0.9680523872 - 0.9828112125 0.9682605267 0.9682605267 - 0.9829354882 0.9684684873 0.9684684873 - 0.9830598235 0.9686766267 0.9686766267 - 0.9831839800 0.9688847065 0.9688847065 - 0.9833083153 0.9690927863 0.9690927863 - 0.9834325910 0.9693009257 0.9693009257 - 0.9835569263 0.9695090055 0.9695090055 - 0.9836812019 0.9697170854 0.9697170854 - 0.9838054180 0.9699230194 0.9699230194 - 0.9839122891 0.9701287746 0.9701287746 - 0.9840167165 0.9703344703 0.9703344703 - 0.9841210246 0.9705402255 0.9705402255 - 0.9842253923 0.9707459211 0.9707459211 - 0.9843298197 0.9709516168 0.9709516168 - 0.9844341278 0.9711573124 0.9711573124 - 0.9845384955 0.9713630080 0.9713630080 - 0.9846428037 0.9715687037 0.9715687037 - 0.9847471714 0.9717782140 0.9717782140 - 0.9848499894 0.9719914198 0.9719914198 - 0.9849516153 0.9722046256 0.9722046256 - 0.9850531816 0.9724177718 0.9724177718 - 0.9851548076 0.9726309180 0.9726309180 - 0.9852563143 0.9728441238 0.9728441238 - 0.9853578806 0.9730573297 0.9730573297 - 0.9854595065 0.9732704759 0.9732704759 - 0.9855610728 0.9734836817 0.9734836817 - 0.9856626987 0.9736968279 0.9736968279 - 0.9857648015 0.9739102125 0.9739102125 - 0.9858701229 0.9741235971 0.9741235971 - 0.9859753847 0.9743369222 0.9743369222 - 0.9860807061 0.9745503068 0.9745503068 - 0.9861860275 0.9747636914 0.9747636914 - 0.9862912893 0.9749770164 0.9749770164 - 0.9863966107 0.9751904011 0.9751904011 - 0.9865018725 0.9754037857 0.9754037857 - 0.9866071939 0.9756171703 0.9756171703 - 0.9867125154 0.9758273959 0.9758273959 - 0.9868178964 0.9760339856 0.9760339856 - 0.9869232774 0.9762405753 0.9762405753 - 0.9870287776 0.9764472842 0.9764472842 - 0.9871342778 0.9766538739 0.9766538739 - 0.9872397184 0.9768605232 0.9768605232 - 0.9873452187 0.9770671129 0.9770671129 - 0.9874507189 0.9772738218 0.9772738218 - 0.9875562191 0.9774804115 0.9774804115 - 0.9876616001 0.9776870012 0.9776870012 - 0.9877671003 0.9778900146 0.9778900146 - 0.9878692031 0.9780920148 0.9780920148 - 0.9879713058 0.9782938957 0.9782938957 - 0.9880735278 0.9784958959 0.9784958959 - 0.9881755710 0.9786978960 0.9786978960 - 0.9882776737 0.9788998961 0.9788998961 - 0.9883798957 0.9791017771 0.9791017771 - 0.9884819984 0.9793037772 0.9793037772 - 0.9885841012 0.9795057774 0.9795057774 - 0.9886862040 0.9797077179 0.9797077179 - 0.9887884855 0.9798991084 0.9798991084 - 0.9888911843 0.9800903797 0.9800903797 - 0.9889938831 0.9802817702 0.9802817702 - 0.9890967011 0.9804731011 0.9804731011 - 0.9891993999 0.9806643724 0.9806643724 - 0.9893020988 0.9808558226 0.9808558226 - 0.9894047976 0.9810470939 0.9810470939 - 0.9895074964 0.9812384844 0.9812384844 - 0.9896101952 0.9814298153 0.9814298153 - 0.9897128940 0.9816213250 0.9816213250 - 0.9898141026 0.9818137884 0.9818137884 - 0.9899132252 0.9820061922 0.9820061922 - 0.9900122881 0.9821987152 0.9821987152 - 0.9901114106 0.9823911190 0.9823911190 - 0.9902104735 0.9825835824 0.9825835824 - 0.9903095961 0.9827759862 0.9827759862 - 0.9904087186 0.9829685092 0.9829685092 - 0.9905077815 0.9831609130 0.9831609130 - 0.9906069040 0.9833533764 0.9833533764 - 0.9907060266 0.9835463166 0.9835463166 - 0.9908010960 0.9837430716 0.9837430716 - 0.9908921719 0.9839398265 0.9839398265 - 0.9909833074 0.9841365218 0.9841365218 - 0.9910743833 0.9843332767 0.9843332767 - 0.9911653996 0.9845299721 0.9845299721 - 0.9912564754 0.9847267270 0.9847267270 - 0.9913476110 0.9849234819 0.9849234819 - 0.9914386868 0.9851201773 0.9851201773 - 0.9915298223 0.9853168726 0.9853168726 - 0.9916208982 0.9855136275 0.9855136275 - 0.9917073250 0.9857079983 0.9857079983 - 0.9917883277 0.9859024286 0.9859024286 - 0.9918693900 0.9860966802 0.9860966802 - 0.9919503927 0.9862911105 0.9862911105 - 0.9920313954 0.9864854217 0.9864854217 - 0.9921123981 0.9866797924 0.9866797924 - 0.9921934009 0.9868742228 0.9868742228 - 0.9922744036 0.9870684743 0.9870684743 - 0.9923554063 0.9872629046 0.9872629046 - 0.9924364090 0.9874572754 0.9874572754 - 0.9925166965 0.9876400828 0.9876400828 - 0.9925954938 0.9878200889 0.9878200889 - 0.9926741719 0.9880000949 0.9880000949 - 0.9927530289 0.9881801009 0.9881801009 - 0.9928318262 0.9883599877 0.9883599877 - 0.9929105043 0.9885399938 0.9885399938 - 0.9929893017 0.9887199998 0.9887199998 - 0.9930680990 0.9889000058 0.9889000058 - 0.9931467772 0.9890800118 0.9890800118 - 0.9932255745 0.9892598987 0.9892598987 - 0.9933044910 0.9894347787 0.9894347787 - 0.9933881760 0.9896039963 0.9896039963 - 0.9934718013 0.9897732735 0.9897732735 - 0.9935554266 0.9899424911 0.9899424911 - 0.9936391115 0.9901117086 0.9901117086 - 0.9937226772 0.9902809858 0.9902809858 - 0.9938063025 0.9904502034 0.9904502034 - 0.9938899875 0.9906194210 0.9906194210 - 0.9939736128 0.9907886982 0.9907886982 - 0.9940572977 0.9909579158 0.9909579158 - 0.9941409230 0.9911267161 0.9911267161 - 0.9942286015 0.9912837148 0.9912837148 - 0.9943184853 0.9914405942 0.9914405942 - 0.9944083095 0.9915975928 0.9915975928 - 0.9944980741 0.9917545915 0.9917545915 - 0.9945880175 0.9919115901 0.9919115901 - 0.9946777821 0.9920685887 0.9920685887 - 0.9947676063 0.9922255874 0.9922255874 - 0.9948573709 0.9923825860 0.9923825860 - 0.9949473143 0.9925395846 0.9925395846 - 0.9950370789 0.9926965833 0.9926965833 - 0.9951277971 0.9928529263 0.9928529263 - 0.9952226877 0.9930083752 0.9930083752 - 0.9953176975 0.9931638837 0.9931638837 - 0.9954125881 0.9933193922 0.9933193922 - 0.9955074787 0.9934750199 0.9934750199 - 0.9956024289 0.9936305285 0.9936305285 - 0.9956973791 0.9937859774 0.9937859774 - 0.9957923293 0.9939414859 0.9939414859 - 0.9958872199 0.9940969944 0.9940969944 - 0.9959821105 0.9942525029 0.9942525029 - 0.9960770011 0.9944080710 0.9944080710 - 0.9961733222 0.9945561886 0.9945561886 - 0.9962705970 0.9947028756 0.9947028756 - 0.9963678718 0.9948495030 0.9948495030 - 0.9964652061 0.9949960709 0.9949960709 - 0.9965624809 0.9951426983 0.9951426983 - 0.9966596961 0.9952893853 0.9952893853 - 0.9967569709 0.9954360127 0.9954360127 - 0.9968543053 0.9955825806 0.9955825806 - 0.9969515800 0.9957293272 0.9957293272 - 0.9970489144 0.9958758950 0.9958758950 - 0.9971461892 0.9960218072 0.9960218072 - 0.9972419143 0.9961522818 0.9961522818 - 0.9973374009 0.9962828159 0.9962828159 - 0.9974328876 0.9964132905 0.9964132905 - 0.9975283742 0.9965438247 0.9965438247 - 0.9976239204 0.9966742992 0.9966742992 - 0.9977192879 0.9968047738 0.9968047738 - 0.9978147745 0.9969353080 0.9969353080 - 0.9979103208 0.9970657825 0.9970657825 - 0.9980058074 0.9971963167 0.9971963167 - 0.9981012940 0.9973267913 0.9973267913 - 0.9981966019 0.9974548221 0.9974548221 - 0.9982848763 0.9975687265 0.9975687265 - 0.9983732104 0.9976826906 0.9976826906 - 0.9984614849 0.9977965951 0.9977965951 - 0.9985498190 0.9979106188 0.9979106188 - 0.9986380935 0.9980245233 0.9980245233 - 0.9987264276 0.9981384277 0.9981384277 - 0.9988147020 0.9982523918 0.9982523918 - 0.9989029765 0.9983662963 0.9983662963 - 0.9989913106 0.9984803200 0.9984803200 - 0.9990795851 0.9985942245 0.9985942245 - 0.9991670847 0.9987086058 0.9987086058 - 0.9992427826 0.9988260269 0.9988260269 - 0.9993184805 0.9989433885 0.9989433885 - 0.9993941784 0.9990608096 0.9990608096 - 0.9994698763 0.9991781712 0.9991781712 - 0.9995456934 0.9992955923 0.9992955923 - 0.9996213913 0.9994130135 0.9994130135 - 0.9996970892 0.9995303750 0.9995303750 - 0.9997727871 0.9996477962 0.9996477962 - 0.9998486042 0.9997652173 0.9997652173 - 0.9999243021 0.9998825788 0.9998825788 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0003622885 0.0002231146 0.0002231146 + 0.0007382494 0.0004596260 0.0004596260 + 0.0011324980 0.0007107497 0.0007107497 + 0.0015447290 0.0009748340 0.0009748340 + 0.0019822840 0.0012541140 0.0012541140 + 0.0024496829 0.0015510730 0.0015510730 + 0.0029426741 0.0018673710 0.0018673710 + 0.0034610520 0.0022024000 0.0022024000 + 0.0040061148 0.0025572821 0.0025572821 + 0.0045792251 0.0029304300 0.0029304300 + 0.0051838872 0.0033235741 0.0033235741 + 0.0058237561 0.0037373570 0.0037373570 + 0.0064960350 0.0041693011 0.0041693011 + 0.0072039221 0.0046230098 0.0046230098 + 0.0079505397 0.0050979760 0.0050979760 + 0.0087367687 0.0055915830 0.0055915830 + 0.0095584840 0.0061070491 0.0061070491 + 0.0104124900 0.0066442229 0.0066442229 + 0.0112989703 0.0071999640 0.0071999640 + 0.0122200102 0.0077770371 0.0077770371 + 0.0131755099 0.0083772372 0.0083772372 + 0.0141713005 0.0089993617 0.0089993617 + 0.0152068604 0.0096444618 0.0096444618 + 0.0162801705 0.0103151305 0.0103151305 + 0.0173835605 0.0110131698 0.0110131698 + 0.0185210407 0.0117386896 0.0117386896 + 0.0196854398 0.0124893300 0.0124893300 + 0.0208813995 0.0132624302 0.0132624302 + 0.0221185796 0.0140622798 0.0140622798 + 0.0234044399 0.0148894899 0.0148894899 + 0.0247398503 0.0157391094 0.0157391094 + 0.0261158608 0.0166204907 0.0166204907 + 0.0275394097 0.0175324902 0.0175324902 + 0.0290060304 0.0184764005 0.0184764005 + 0.0305129606 0.0194491595 0.0194491595 + 0.0320634693 0.0204504505 0.0204504505 + 0.0336536989 0.0214823205 0.0214823205 + 0.0352861397 0.0225437600 0.0225437600 + 0.0369675495 0.0236390196 0.0236390196 + 0.0386981517 0.0247705802 0.0247705802 + 0.0404745191 0.0259348303 0.0259348303 + 0.0422977097 0.0271375105 0.0271375105 + 0.0441726409 0.0283817798 0.0283817798 + 0.0460996106 0.0296590906 0.0296590906 + 0.0480887406 0.0309759099 0.0309759099 + 0.0501275212 0.0323227607 0.0323227607 + 0.0522207692 0.0337067991 0.0337067991 + 0.0543671884 0.0351255499 0.0351255499 + 0.0565663688 0.0365805998 0.0365805998 + 0.0588187315 0.0380711295 0.0380711295 + 0.0611272007 0.0396083593 0.0396083593 + 0.0634748489 0.0411784090 0.0411784090 + 0.0658760667 0.0427908003 0.0427908003 + 0.0683400705 0.0444480814 0.0444480814 + 0.0708511695 0.0461456515 0.0461456515 + 0.0734233931 0.0478809699 0.0478809699 + 0.0760477036 0.0496524908 0.0496524908 + 0.0787281767 0.0514654815 0.0514654815 + 0.0814533383 0.0533178188 0.0533178188 + 0.0842291415 0.0552090518 0.0552090518 + 0.0870640278 0.0571336001 0.0571336001 + 0.0899450928 0.0590961091 0.0590961091 + 0.0928783119 0.0610942282 0.0610942282 + 0.0958742201 0.0631353706 0.0631353706 + 0.0989081487 0.0652145892 0.0652145892 + 0.1019897982 0.0673257634 0.0673257634 + 0.1051272005 0.0694703013 0.0694703013 + 0.1083052009 0.0716492608 0.0716492608 + 0.1115389019 0.0738634914 0.0738634914 + 0.1148212031 0.0761086568 0.0761086568 + 0.1181529984 0.0783852488 0.0783852488 + 0.1215258017 0.0806891099 0.0806891099 + 0.1249411032 0.0830274820 0.0830274820 + 0.1283981055 0.0854015574 0.0854015574 + 0.1318998039 0.0878068879 0.0878068879 + 0.1354435980 0.0902427882 0.0902427882 + 0.1390310973 0.0927055702 0.0927055702 + 0.1426659971 0.0952004492 0.0952004492 + 0.1463364065 0.0977323800 0.0977323800 + 0.1500398070 0.1002930999 0.1002930999 + 0.1537826061 0.1028838009 0.1028838009 + 0.1575697958 0.1055003032 0.1055003032 + 0.1613817066 0.1081380993 0.1081380993 + 0.1652164012 0.1108009964 0.1108009964 + 0.1690932959 0.1134867966 0.1134867966 + 0.1729968935 0.1161940992 0.1161940992 + 0.1769410968 0.1189275980 0.1189275980 + 0.1809352040 0.1216920018 0.1216920018 + 0.1849523038 0.1244781017 0.1244781017 + 0.1889985055 0.1272816956 0.1272816956 + 0.1930781007 0.1301010996 0.1301010996 + 0.1971940994 0.1329389066 0.1329389066 + 0.2013469040 0.1358011961 0.1358011961 + 0.2055312991 0.1386940926 0.1386940926 + 0.2097439021 0.1416130960 0.1416130960 + 0.2139831036 0.1445533037 0.1445533037 + 0.2182556987 0.1475161016 0.1475161016 + 0.2225302011 0.1505060941 0.1505060941 + 0.2268306017 0.1535229981 0.1535229981 + 0.2311580926 0.1565562934 0.1565562934 + 0.2355093956 0.1596101969 0.1596101969 + 0.2398717999 0.1626808047 0.1626808047 + 0.2442272007 0.1657689065 0.1657689065 + 0.2485899031 0.1688731015 0.1688731015 + 0.2529604137 0.1719909012 0.1719909012 + 0.2573277056 0.1751365066 0.1751365066 + 0.2616989017 0.1782941073 0.1782941073 + 0.2660799026 0.1814624071 0.1814624071 + 0.2704606056 0.1846423000 0.1846423000 + 0.2748312056 0.1878450066 0.1878450066 + 0.2791987956 0.1910510063 0.1910510063 + 0.2835668027 0.1942614019 0.1942614019 + 0.2879388928 0.1974803954 0.1974803954 + 0.2923108041 0.2007077038 0.2007077038 + 0.2966752946 0.2039379030 0.2039379030 + 0.3010303974 0.2071726024 0.2071726024 + 0.3053742945 0.2104312927 0.2104312927 + 0.3097003102 0.2136991024 0.2136991024 + 0.3140352070 0.2169768065 0.2169768065 + 0.3183707893 0.2202724963 0.2202724963 + 0.3226906061 0.2235676050 0.2235676050 + 0.3270021975 0.2268693000 0.2268693000 + 0.3313120902 0.2301850021 0.2301850021 + 0.3356218934 0.2334984988 0.2334984988 + 0.3399325907 0.2368156016 0.2368156016 + 0.3442339897 0.2401401997 0.2401401997 + 0.3485251963 0.2434774041 0.2434774041 + 0.3527927101 0.2468263060 0.2468263060 + 0.3570409119 0.2501969039 0.2501969039 + 0.3612729013 0.2535721958 0.2535721958 + 0.3654753864 0.2569538951 0.2569538951 + 0.3696604967 0.2603574097 0.2603574097 + 0.3738316894 0.2637628019 0.2637628019 + 0.3779726923 0.2671697140 0.2671697140 + 0.3820927143 0.2705962062 0.2705962062 + 0.3861967027 0.2740235031 0.2740235031 + 0.3902786970 0.2774519026 0.2774519026 + 0.3943498135 0.2808937132 0.2808937132 + 0.3984076083 0.2843413949 0.2843413949 + 0.4024240077 0.2878027856 0.2878027856 + 0.4064257145 0.2912661135 0.2912661135 + 0.4104036987 0.2947317958 0.2947317958 + 0.4143416882 0.2981860042 0.2981860042 + 0.4182564914 0.3016363978 0.3016363978 + 0.4221282005 0.3050855994 0.3050855994 + 0.4259957075 0.3085319102 0.3085319102 + 0.4298554957 0.3119722009 0.3119722009 + 0.4336968064 0.3154138923 0.3154138923 + 0.4375284910 0.3188566864 0.3188566864 + 0.4413171113 0.3222773075 0.3222773075 + 0.4450969100 0.3256925941 0.3256925941 + 0.4488551915 0.3290956020 0.3290956020 + 0.4526067972 0.3324885964 0.3324885964 + 0.4563522935 0.3358506858 0.3358506858 + 0.4600827098 0.3392089009 0.3392089009 + 0.4638085067 0.3425619900 0.3425619900 + 0.4674848914 0.3459056914 0.3459056914 + 0.4711487889 0.3492426872 0.3492426872 + 0.4747653008 0.3525683880 0.3525683880 + 0.4783576131 0.3558900058 0.3558900058 + 0.4819136858 0.3591865897 0.3591865897 + 0.4854589999 0.3624787927 0.3624787927 + 0.4889962077 0.3657391071 0.3657391071 + 0.4925225973 0.3689970076 0.3689970076 + 0.4960449040 0.3722279966 0.3722279966 + 0.4995129108 0.3754577041 0.3754577041 + 0.5029718280 0.3786748946 0.3786748946 + 0.5064191222 0.3818857074 0.3818857074 + 0.5098642111 0.3850541115 0.3850541115 + 0.5132802725 0.3882220984 0.3882220984 + 0.5166903138 0.3913877010 0.3913877010 + 0.5200588107 0.3945508003 0.3945508003 + 0.5234236717 0.3976989985 0.3976989985 + 0.5267717838 0.4008451104 0.4008451104 + 0.5301045775 0.4039750993 0.4039750993 + 0.5333794951 0.4071030021 0.4071030021 + 0.5366390944 0.4102006853 0.4102006853 + 0.5398436189 0.4132984877 0.4132984877 + 0.5430372953 0.4163655937 0.4163655937 + 0.5461881757 0.4194326997 0.4194326997 + 0.5493322015 0.4224604964 0.4224604964 + 0.5524412990 0.4254842997 0.4254842997 + 0.5555446744 0.4284946918 0.4284946918 + 0.5586010814 0.4315015972 0.4315015972 + 0.5616549253 0.4344901145 0.4344901145 + 0.5646228194 0.4374687970 0.4374687970 + 0.5675905943 0.4404281080 0.4404281080 + 0.5705261230 0.4433673024 0.4433673024 + 0.5734590292 0.4462912083 0.4462912083 + 0.5763446093 0.4491803050 0.4491803050 + 0.5792183876 0.4520643055 0.4520643055 + 0.5820407867 0.4549018145 0.4549018145 + 0.5848355889 0.4577394128 0.4577394128 + 0.5875927210 0.4605427086 0.4605427086 + 0.5903086066 0.4633406997 0.4633406997 + 0.5930045843 0.4661261141 0.4661261141 + 0.5956503153 0.4689033926 0.4689033926 + 0.5982949138 0.4716737866 0.4716737866 + 0.6009235978 0.4744301140 0.4744301140 + 0.6035524011 0.4771854877 0.4771854877 + 0.6061716080 0.4799031019 0.4799031019 + 0.6087887883 0.4826206863 0.4826206863 + 0.6113740206 0.4853239954 0.4853239954 + 0.6139330864 0.4880208969 0.4880208969 + 0.6164814830 0.4907048047 0.4907048047 + 0.6189994812 0.4933629036 0.4933629036 + 0.6215174794 0.4960210919 0.4960210919 + 0.6240079999 0.4986290932 0.4986290932 + 0.6264969110 0.5012347102 0.5012347102 + 0.6289771795 0.5038244128 0.5038244128 + 0.6314516068 0.5064008236 0.5064008236 + 0.6339148283 0.5089727044 0.5089727044 + 0.6363407969 0.5115088224 0.5115088224 + 0.6387668252 0.5140448213 0.5140448213 + 0.6411426067 0.5165610909 0.5165610909 + 0.6435087919 0.5190668106 0.5190668106 + 0.6458575130 0.5215663910 0.5215663910 + 0.6481820941 0.5240356922 0.5240356922 + 0.6505066752 0.5265049934 0.5265049934 + 0.6528093815 0.5289555192 0.5289555192 + 0.6551114917 0.5313962102 0.5313962102 + 0.6573888063 0.5338327885 0.5338327885 + 0.6596413255 0.5362383723 0.5362383723 + 0.6618937254 0.5386438966 0.5386438966 + 0.6641113162 0.5410385132 0.5410385132 + 0.6663289070 0.5434240103 0.5434240103 + 0.6685289741 0.5458095074 0.5458095074 + 0.6707097888 0.5481578112 0.5481578112 + 0.6728904843 0.5505042076 0.5505042076 + 0.6750382781 0.5528417826 0.5528417826 + 0.6771832705 0.5551611185 0.5551611185 + 0.6793134809 0.5574803948 0.5574803948 + 0.6814150810 0.5597792864 0.5597792864 + 0.6835166812 0.5620682836 0.5620682836 + 0.6855754852 0.5643572211 0.5643572211 + 0.6876184940 0.5665962100 0.5665962100 + 0.6896557808 0.5688347220 0.5688347220 + 0.6916378736 0.5710633993 0.5710633993 + 0.6936199069 0.5732669830 0.5732669830 + 0.6955876946 0.5754706860 0.5754706860 + 0.6975364089 0.5776560903 0.5776560903 + 0.6994851232 0.5798259974 0.5798259974 + 0.7013980150 0.5819959044 0.5819959044 + 0.7032977939 0.5841305852 0.5841305852 + 0.7051973939 0.5862547755 0.5862547755 + 0.7070760727 0.5883789062 0.5883789062 + 0.7089548111 0.5904625058 0.5904625058 + 0.7108243704 0.5925447941 0.5925447941 + 0.7126672864 0.5946221948 0.5946221948 + 0.7145102024 0.5966691971 0.5966691971 + 0.7163286209 0.5987161994 0.5987161994 + 0.7181193829 0.6007555127 0.6007555127 + 0.7199102044 0.6027747989 0.6027747989 + 0.7216753960 0.6047940254 0.6047940254 + 0.7234271765 0.6068055034 0.6068055034 + 0.7251790166 0.6088048220 0.6088048220 + 0.7269017100 0.6108040214 0.6108040214 + 0.7286177278 0.6127884984 0.6127884984 + 0.7303336263 0.6147556901 0.6147556901 + 0.7319886088 0.6167228818 0.6167228818 + 0.7336395979 0.6186692119 0.6186692119 + 0.7352887988 0.6205952168 0.6205952168 + 0.7368847132 0.6225212216 0.6225212216 + 0.7384806275 0.6244304776 0.6244304776 + 0.7400730848 0.6263242960 0.6263242960 + 0.7416328788 0.6282181740 0.6282181740 + 0.7431927919 0.6301015019 0.6301015019 + 0.7447494268 0.6319742799 0.6319742799 + 0.7462829947 0.6338471174 0.6338471174 + 0.7478165030 0.6356995106 0.6356995106 + 0.7493439913 0.6375269890 0.6375269890 + 0.7508273125 0.6393544078 0.6393544078 + 0.7523106933 0.6411694288 0.6411694288 + 0.7537910938 0.6429628134 0.6429628134 + 0.7552406788 0.6447560787 0.6447560787 + 0.7566903234 0.6465445161 0.6465445161 + 0.7581396103 0.6483175755 0.6483175755 + 0.7595729828 0.6500906944 0.6500906944 + 0.7610062957 0.6518586874 0.6518586874 + 0.7624397278 0.6535770297 0.6535770297 + 0.7638341784 0.6552951932 0.6552951932 + 0.7652254105 0.6570134759 0.6570134759 + 0.7666165233 0.6587092280 0.6587092280 + 0.7679901719 0.6604024768 0.6604024768 + 0.7693589926 0.6620957851 0.6620957851 + 0.7707278728 0.6637632847 0.6637632847 + 0.7720705867 0.6654189229 0.6654189229 + 0.7733970284 0.6670743823 0.6670743823 + 0.7747234702 0.6687203050 0.6687203050 + 0.7760393023 0.6703534126 0.6703534126 + 0.7773399949 0.6719865203 0.6719865203 + 0.7786408067 0.6736162901 0.6736162901 + 0.7799372077 0.6752253771 0.6752253771 + 0.7812135220 0.6768345833 0.6768345833 + 0.7824897170 0.6784437895 0.6784437895 + 0.7837659717 0.6800181270 0.6800181270 + 0.7850177884 0.6815842986 0.6815842986 + 0.7862669826 0.6831504703 0.6831504703 + 0.7875161767 0.6846908927 0.6846908927 + 0.7887520194 0.6861993074 0.6861993074 + 0.7899786830 0.6877077222 0.6877077222 + 0.7912054062 0.6892145872 0.6892145872 + 0.7924262285 0.6906890869 0.6906890869 + 0.7936294079 0.6921635866 0.6921635866 + 0.7948325872 0.6936382055 0.6936382055 + 0.7960357070 0.6950960159 0.6950960159 + 0.7972220778 0.6965432167 0.6965432167 + 0.7984061241 0.6979902983 0.6979902983 + 0.7995899916 0.6994342804 0.6994342804 + 0.8007580042 0.7008578181 0.7008578181 + 0.8019068241 0.7022812963 0.7022812963 + 0.8030557036 0.7037047744 0.7037047744 + 0.8042045236 0.7051180005 0.7051180005 + 0.8053393960 0.7065249085 0.7065249085 + 0.8064743876 0.7079318166 0.7079318166 + 0.8076093793 0.7093364000 0.7093364000 + 0.8087307215 0.7107117772 0.7107117772 + 0.8098388910 0.7120870948 0.7120870948 + 0.8109471798 0.7134624124 0.7134624124 + 0.8120555282 0.7148199081 0.7148199081 + 0.8131191134 0.7161585093 0.7161585093 + 0.8141819239 0.7174972296 0.7174972296 + 0.8152446747 0.7188357711 0.7188357711 + 0.8162903190 0.7201492190 0.7201492190 + 0.8173117042 0.7214584947 0.7214584947 + 0.8183330894 0.7227677703 0.7227677703 + 0.8193544149 0.7240737081 0.7240737081 + 0.8203511238 0.7253654003 0.7253654003 + 0.8213419914 0.7266569734 0.7266569734 + 0.8223329186 0.7279486060 0.7279486060 + 0.8233172894 0.7292181849 0.7292181849 + 0.8242688179 0.7304645777 0.7304645777 + 0.8252202272 0.7317109704 0.7317109704 + 0.8261716962 0.7329574227 0.7329574227 + 0.8271054029 0.7341820002 0.7341820002 + 0.8280197978 0.7353988290 0.7353988290 + 0.8289341927 0.7366157174 0.7366157174 + 0.8298485875 0.7378326058 0.7378326058 + 0.8307464719 0.7390285134 0.7390285134 + 0.8316389918 0.7402232885 0.7402232885 + 0.8325315118 0.7414181828 0.7414181828 + 0.8334239721 0.7426074743 0.7426074743 + 0.8342829943 0.7437586784 0.7437586784 + 0.8351417184 0.7449098229 0.7449098229 + 0.8360003829 0.7460609078 0.7460609078 + 0.8368576169 0.7472000122 0.7472000122 + 0.8377087116 0.7483038902 0.7483038902 + 0.8385598063 0.7494078875 0.7494078875 + 0.8394109011 0.7505118251 0.7505118251 + 0.8402518034 0.7516049743 0.7516049743 + 0.8410723209 0.7526779175 0.7526779175 + 0.8418927789 0.7537506819 0.7537506819 + 0.8427134156 0.7548235059 0.7548235059 + 0.8435238004 0.7558841705 0.7558841705 + 0.8443217278 0.7569258809 0.7569258809 + 0.8451194763 0.7579675913 0.7579675913 + 0.8459174037 0.7590093017 0.7590093017 + 0.8467006087 0.7600433230 0.7600433230 + 0.8474699259 0.7610656023 0.7610656023 + 0.8482393026 0.7620878220 0.7620878220 + 0.8490086198 0.7631099820 0.7631099820 + 0.8497787714 0.7641208768 0.7641208768 + 0.8505498171 0.7651112080 0.7651112080 + 0.8513206840 0.7661014795 0.7661014795 + 0.8520916104 0.7670918107 0.7670918107 + 0.8528512120 0.7680724263 0.7680724263 + 0.8536006212 0.7690268159 0.7690268159 + 0.8543499708 0.7699812055 0.7699812055 + 0.8550993800 0.7709355950 0.7709355950 + 0.8558359146 0.7718878984 0.7718878984 + 0.8565574884 0.7728270292 0.7728270292 + 0.8572790027 0.7737662196 0.7737662196 + 0.8580005765 0.7747054100 0.7747054100 + 0.8587099910 0.7756444812 0.7756444812 + 0.8593978286 0.7765566707 0.7765566707 + 0.8600856066 0.7774680257 0.7774680257 + 0.8607733846 0.7783793211 0.7783793211 + 0.8614575863 0.7792906165 0.7792906165 + 0.8621283770 0.7801936865 0.7801936865 + 0.8627992272 0.7810940146 0.7810940146 + 0.8634700179 0.7819942832 0.7819942832 + 0.8641406894 0.7828946114 0.7828946114 + 0.8648099899 0.7837845087 0.7837845087 + 0.8654794097 0.7846636772 0.7846636772 + 0.8661487103 0.7855427861 0.7855427861 + 0.8668180108 0.7864218950 0.7864218950 + 0.8674687743 0.7872971892 0.7872971892 + 0.8681142926 0.7881550193 0.7881550193 + 0.8687598109 0.7890126705 0.7890126705 + 0.8694053292 0.7898705006 0.7898705006 + 0.8700295091 0.7907282114 0.7907282114 + 0.8706318736 0.7915742993 0.7915742993 + 0.8712342978 0.7924178839 0.7924178839 + 0.8718366027 0.7932615280 0.7932615280 + 0.8724340200 0.7941049933 0.7941049933 + 0.8730055094 0.7949374914 0.7949374914 + 0.8735769987 0.7957543135 0.7957543135 + 0.8741484880 0.7965711951 0.7965711951 + 0.8747199774 0.7973880172 0.7973880172 + 0.8752837777 0.7982048988 0.7982048988 + 0.8758454919 0.7989869118 0.7989869118 + 0.8764070868 0.7997676134 0.7997676134 + 0.8769688010 0.8005483150 0.8005483150 + 0.8775206208 0.8013290167 0.8013290167 + 0.8780546188 0.8021016717 0.8021016717 + 0.8785886765 0.8028652072 0.8028652072 + 0.8791226745 0.8036286831 0.8036286831 + 0.8796567917 0.8043922186 0.8043922186 + 0.8801620007 0.8051556945 0.8051556945 + 0.8806633949 0.8058984876 0.8058984876 + 0.8811649084 0.8066393733 0.8066393733 + 0.8816663027 0.8073803186 0.8073803186 + 0.8821567297 0.8081210852 0.8081210852 + 0.8826276064 0.8088607192 0.8088607192 + 0.8830984831 0.8095976114 0.8095976114 + 0.8835694194 0.8103343844 0.8103343844 + 0.8840402961 0.8110712171 0.8110712171 + 0.8845028877 0.8118081093 0.8118081093 + 0.8849632740 0.8125373125 0.8125373125 + 0.8854237199 0.8132628202 0.8132628202 + 0.8858841062 0.8139883280 0.8139883280 + 0.8863461018 0.8147137761 0.8147137761 + 0.8868156075 0.8154392838 0.8154392838 + 0.8872851133 0.8161317110 0.8161317110 + 0.8877546191 0.8168231845 0.8168231845 + 0.8882240057 0.8175147176 0.8175147176 + 0.8886895180 0.8182061911 0.8182061911 + 0.8891507983 0.8188943863 0.8188943863 + 0.8896121979 0.8195710182 0.8195710182 + 0.8900735974 0.8202475905 0.8202475905 + 0.8905348778 0.8209241033 0.8209241033 + 0.8909785748 0.8216006756 0.8216006756 + 0.8914170861 0.8222699761 0.8222699761 + 0.8918557167 0.8229292035 0.8229292035 + 0.8922942281 0.8235884905 0.8235884905 + 0.8927327991 0.8242477179 0.8242477179 + 0.8931676149 0.8249068856 0.8249068856 + 0.8936023712 0.8255603909 0.8255603909 + 0.8940371871 0.8262094259 0.8262094259 + 0.8944720030 0.8268585205 0.8268585205 + 0.8949096203 0.8275076151 0.8275076151 + 0.8953595161 0.8281567097 0.8281567097 + 0.8958094716 0.8287855983 0.8287855983 + 0.8962594271 0.8294041753 0.8294041753 + 0.8967093825 0.8300228119 0.8300228119 + 0.8971595168 0.8306415081 0.8306415081 + 0.8976098895 0.8312600851 0.8312600851 + 0.8980603218 0.8318728209 0.8318728209 + 0.8985106945 0.8324831128 0.8324831128 + 0.8989611268 0.8330934048 0.8330934048 + 0.8994054198 0.8337035775 0.8337035775 + 0.8998395801 0.8343139291 0.8343139291 + 0.9002736807 0.8349158764 0.8349158764 + 0.9007077813 0.8355141878 0.8355141878 + 0.9011420012 0.8361124992 0.8361124992 + 0.9015651941 0.8367106915 0.8367106915 + 0.9019711018 0.8373090029 0.8373090029 + 0.9023771286 0.8378884792 0.8378884792 + 0.9027829766 0.8384566903 0.8384566903 + 0.9031888843 0.8390247822 0.8390247822 + 0.9035964012 0.8395929933 0.8395929933 + 0.9040068984 0.8401612043 0.8401612043 + 0.9044172764 0.8407253027 0.8407253027 + 0.9048277140 0.8412854075 0.8412854075 + 0.9052380919 0.8418455124 0.8418455124 + 0.9056466222 0.8424056768 0.8424056768 + 0.9060494900 0.8429657817 0.8429657817 + 0.9064524770 0.8435230255 0.8435230255 + 0.9068554044 0.8440740705 0.8440740705 + 0.9072582722 0.8446251750 0.8446251750 + 0.9076607823 0.8451762795 0.8451762795 + 0.9080597162 0.8457275033 0.8457275033 + 0.9084585905 0.8462774158 0.8462774158 + 0.9088575244 0.8468161225 0.8468161225 + 0.9092563987 0.8473547101 0.8473547101 + 0.9096552730 0.8478932977 0.8478932977 + 0.9100391865 0.8484320045 0.8484320045 + 0.9104216099 0.8489705920 0.8489705920 + 0.9108039141 0.8494958878 0.8494958878 + 0.9111862779 0.8500180244 0.8500180244 + 0.9115687013 0.8505399823 0.8505399823 + 0.9119420052 0.8510619998 0.8510619998 + 0.9123104215 0.8515840173 0.8515840173 + 0.9126788974 0.8521022201 0.8521022201 + 0.9130473137 0.8526157737 0.8526157737 + 0.9134156704 0.8531293273 0.8531293273 + 0.9137811065 0.8536428809 0.8536428809 + 0.9141399860 0.8541564941 0.8541564941 + 0.9144989848 0.8546689749 0.8546689749 + 0.9148579240 0.8551610112 0.8551610112 + 0.9152169228 0.8556529880 0.8556529880 + 0.9155759215 0.8561450243 0.8561450243 + 0.9159141779 0.8566370010 0.8566370010 + 0.9162515998 0.8571289182 0.8571289182 + 0.9165889025 0.8576136231 0.8576136231 + 0.9169263244 0.8580930233 0.8580930233 + 0.9172636867 0.8585724235 0.8585724235 + 0.9175904989 0.8590518832 0.8590518832 + 0.9179077148 0.8595312834 0.8595312834 + 0.9182248116 0.8600099087 0.8600099087 + 0.9185420275 0.8604763150 0.8604763150 + 0.9188591242 0.8609427810 0.8609427810 + 0.9191758037 0.8614091873 0.8614091873 + 0.9194787145 0.8618755937 0.8618755937 + 0.9197816253 0.8623420000 0.8623420000 + 0.9200844169 0.8628141284 0.8628141284 + 0.9203873277 0.8632925153 0.8632925153 + 0.9206901789 0.8637707829 0.8637707829 + 0.9209961295 0.8642491102 0.8642491102 + 0.9213051796 0.8647273779 0.8647273779 + 0.9216142893 0.8652058244 0.8652058244 + 0.9219235182 0.8656808734 0.8656808734 + 0.9222326279 0.8661553860 0.8661553860 + 0.9225417972 0.8666298985 0.8666298985 + 0.9228463769 0.8671044111 0.8671044111 + 0.9231504202 0.8675789237 0.8675789237 + 0.9234544039 0.8680508733 0.8680508733 + 0.9237583876 0.8685060740 0.8685060740 + 0.9240623713 0.8689612746 0.8689612746 + 0.9243630767 0.8694164753 0.8694164753 + 0.9246509075 0.8698716164 0.8698716164 + 0.9249386787 0.8703268170 0.8703268170 + 0.9252265096 0.8707792163 0.8707792163 + 0.9255142212 0.8712267280 0.8712267280 + 0.9258019924 0.8716741204 0.8716741204 + 0.9260783792 0.8721215725 0.8721215725 + 0.9263415933 0.8725690246 0.8725690246 + 0.9266049266 0.8730164766 0.8730164766 + 0.9268680811 0.8734666109 0.8734666109 + 0.9271314144 0.8739191890 0.8739191890 + 0.9273946285 0.8743718266 0.8743718266 + 0.9276558757 0.8748244047 0.8748244047 + 0.9279161096 0.8752769232 0.8752769232 + 0.9281764030 0.8757295012 0.8757295012 + 0.9284366965 0.8761757016 0.8761757016 + 0.9286969900 0.8766183257 0.8766183257 + 0.9289572835 0.8770608902 0.8770608902 + 0.9292187095 0.8775033951 0.8775033951 + 0.9294803143 0.8779460192 0.8779460192 + 0.9297419190 0.8783885837 0.8783885837 + 0.9300035238 0.8788154125 0.8788154125 + 0.9302651286 0.8792347908 0.8792347908 + 0.9305267930 0.8796542287 0.8796542287 + 0.9307944179 0.8800736070 0.8800736070 + 0.9310628176 0.8804931045 0.8804931045 + 0.9313312173 0.8809124827 0.8809124827 + 0.9315996170 0.8813328147 0.8813328147 + 0.9318680167 0.8817536831 0.8817536831 + 0.9321364164 0.8821746111 0.8821746111 + 0.9324026108 0.8825954199 0.8825954199 + 0.9326686263 0.8830162883 0.8830162883 + 0.9329345822 0.8834370971 0.8834370971 + 0.9332005978 0.8838580251 0.8838580251 + 0.9334664941 0.8842790127 0.8842790127 + 0.9337325096 0.8846998811 0.8846998811 + 0.9339888096 0.8851209283 0.8851209283 + 0.9342433810 0.8855419159 0.8855419159 + 0.9344980121 0.8859627843 0.8859627843 + 0.9347525835 0.8863782883 0.8863782883 + 0.9350072145 0.8867862821 0.8867862821 + 0.9352617860 0.8871942759 0.8871942759 + 0.9355120063 0.8876023293 0.8876023293 + 0.9357606769 0.8880103230 0.8880103230 + 0.9360094070 0.8884181976 0.8884181976 + 0.9362581968 0.8888232708 0.8888232708 + 0.9365069270 0.8892168999 0.8892168999 + 0.9367555976 0.8896105289 0.8896105289 + 0.9369971156 0.8900040984 0.8900040984 + 0.9372326732 0.8903976083 0.8903976083 + 0.9374684095 0.8907911777 0.8907911777 + 0.9377040267 0.8911848068 0.8911848068 + 0.9379395843 0.8915761113 0.8915761113 + 0.9381753206 0.8919671178 0.8919671178 + 0.9384089708 0.8923581839 0.8923581839 + 0.9386385083 0.8927491903 0.8927491903 + 0.9388679266 0.8931403160 0.8931403160 + 0.9390972853 0.8935313225 0.8935313225 + 0.9393268228 0.8939135075 0.8939135075 + 0.9395561814 0.8942884803 0.8942884803 + 0.9397855997 0.8946635723 0.8946635723 + 0.9400131106 0.8950387239 0.8950387239 + 0.9402405024 0.8954138160 0.8954138160 + 0.9404678941 0.8957887888 0.8957887888 + 0.9406952858 0.8961619735 0.8961619735 + 0.9409226775 0.8965228200 0.8965228200 + 0.9411501288 0.8968836069 0.8968836069 + 0.9413790703 0.8972443938 0.8972443938 + 0.9416092038 0.8976051211 0.8976051211 + 0.9418392777 0.8979659081 0.8979659081 + 0.9420694113 0.8983266950 0.8983266950 + 0.9422994852 0.8986871243 0.8986871243 + 0.9425296187 0.8990470767 0.8990470767 + 0.9427592158 0.8994072080 0.8994072080 + 0.9429851174 0.8997672796 0.8997672796 + 0.9432110190 0.9001272917 0.9001272917 + 0.9434369206 0.9004874229 0.9004874229 + 0.9436628222 0.9008464217 0.9008464217 + 0.9438887239 0.9011930227 0.9011930227 + 0.9441146255 0.9015396237 0.9015396237 + 0.9443339109 0.9018862247 0.9018862247 + 0.9445490241 0.9022328258 0.9022328258 + 0.9447640777 0.9025794268 0.9025794268 + 0.9449791908 0.9029260278 0.9029260278 + 0.9451943040 0.9032617211 0.9032617211 + 0.9454094172 0.9035840034 0.9035840034 + 0.9456244111 0.9039062262 0.9039062262 + 0.9458345175 0.9042283893 0.9042283893 + 0.9460445046 0.9045506120 0.9045506120 + 0.9462546110 0.9048727751 0.9048727751 + 0.9464645982 0.9051949978 0.9051949978 + 0.9466745853 0.9055181146 0.9055181146 + 0.9468846917 0.9058414102 0.9058414102 + 0.9470942020 0.9061647058 0.9061647058 + 0.9473028183 0.9064880013 0.9064880013 + 0.9475114942 0.9068112969 0.9068112969 + 0.9477201104 0.9071347117 0.9071347117 + 0.9479287267 0.9074580073 0.9074580073 + 0.9481372833 0.9077845216 0.9077845216 + 0.9483458996 0.9081110954 0.9081110954 + 0.9485498071 0.9084377289 0.9084377289 + 0.9487506747 0.9087643027 0.9087643027 + 0.9489514828 0.9090908766 0.9090908766 + 0.9491522908 0.9094176292 0.9094176292 + 0.9493530989 0.9097433090 0.9097433090 + 0.9495540261 0.9100611806 0.9100611806 + 0.9497547746 0.9103791714 0.9103791714 + 0.9499650002 0.9106972218 0.9106972218 + 0.9501776099 0.9110150933 0.9110150933 + 0.9503902197 0.9113330841 0.9113330841 + 0.9506028295 0.9116510749 0.9116510749 + 0.9508153796 0.9119694829 0.9119694829 + 0.9510279894 0.9122899771 0.9122899771 + 0.9512405992 0.9126105905 0.9126105905 + 0.9514577985 0.9129310846 0.9129310846 + 0.9516754150 0.9132516980 0.9132516980 + 0.9518930912 0.9135721922 0.9135721922 + 0.9521107078 0.9138928056 0.9138928056 + 0.9523283839 0.9142141938 0.9142141938 + 0.9525460005 0.9145398736 0.9145398736 + 0.9527636766 0.9148656130 0.9148656130 + 0.9529777169 0.9151912928 0.9151912928 + 0.9531913996 0.9155169725 0.9155169725 + 0.9534052014 0.9158427119 0.9158427119 + 0.9536188841 0.9161683917 0.9161683917 + 0.9538326859 0.9164946079 0.9164946079 + 0.9540464878 0.9168255925 0.9168255925 + 0.9542601705 0.9171565771 0.9171565771 + 0.9544739723 0.9174876213 0.9174876213 + 0.9546877146 0.9178186059 0.9178186059 + 0.9549015164 0.9181495905 0.9181495905 + 0.9551151991 0.9184805155 0.9184805155 + 0.9553288817 0.9188115001 0.9188115001 + 0.9555426836 0.9191362858 0.9191362858 + 0.9557564259 0.9194607139 0.9194607139 + 0.9559655190 0.9197850823 0.9197850823 + 0.9561733007 0.9201093912 0.9201093912 + 0.9563812017 0.9204338193 0.9204338193 + 0.9565889835 0.9207581878 0.9207581878 + 0.9567968845 0.9210826159 0.9210826159 + 0.9570047259 0.9214031100 0.9214031100 + 0.9572126269 0.9217219949 0.9217219949 + 0.9574205279 0.9220409989 0.9220409989 + 0.9576283097 0.9223598838 0.9223598838 + 0.9578362107 0.9226788878 0.9226788878 + 0.9580441117 0.9229977727 0.9229977727 + 0.9582520127 0.9233167768 0.9233167768 + 0.9584599137 0.9236333966 0.9236333966 + 0.9586678147 0.9239464998 0.9239464998 + 0.9588736892 0.9242596030 0.9242596030 + 0.9590756893 0.9245728254 0.9245728254 + 0.9592776895 0.9248859286 0.9248859286 + 0.9594796896 0.9251989722 0.9251989722 + 0.9596816897 0.9255120754 0.9255120754 + 0.9598835707 0.9258251786 0.9258251786 + 0.9600855708 0.9261450171 0.9261450171 + 0.9602875710 0.9264646769 0.9264646769 + 0.9604778290 0.9267843962 0.9267843962 + 0.9606677890 0.9271041751 0.9271041751 + 0.9608578086 0.9274238944 0.9274238944 + 0.9610478282 0.9277436137 0.9277436137 + 0.9612377882 0.9280633926 0.9280633926 + 0.9614278078 0.9283816814 0.9283816814 + 0.9616177082 0.9286987782 0.9286987782 + 0.9617974758 0.9290158749 0.9290158749 + 0.9619693160 0.9293330908 0.9293330908 + 0.9621409774 0.9296501875 0.9296501875 + 0.9623126984 0.9299672842 0.9299672842 + 0.9624844193 0.9302843809 0.9302843809 + 0.9626560807 0.9306014776 0.9306014776 + 0.9628278017 0.9309071898 0.9309071898 + 0.9629982710 0.9312127233 0.9312127233 + 0.9631454945 0.9315180779 0.9315180779 + 0.9632925987 0.9318236113 0.9318236113 + 0.9634398222 0.9321290851 0.9321290851 + 0.9635869861 0.9324346185 0.9324346185 + 0.9637340903 0.9327399731 0.9327399731 + 0.9638813138 0.9330422282 0.9330422282 + 0.9640284777 0.9333382249 0.9333382249 + 0.9641625285 0.9336342812 0.9336342812 + 0.9642810822 0.9339302778 0.9339302778 + 0.9643996954 0.9342262745 0.9342262745 + 0.9645183086 0.9345223904 0.9345223904 + 0.9646369219 0.9348183870 0.9348183870 + 0.9647554755 0.9351143837 0.9351143837 + 0.9648740888 0.9354104996 0.9354104996 + 0.9649927020 0.9357064962 0.9357064962 + 0.9651054740 0.9360026121 0.9360026121 + 0.9652165771 0.9362986088 0.9362986088 + 0.9653276801 0.9365946054 0.9365946054 + 0.9654389024 0.9368907213 0.9368907213 + 0.9655500054 0.9371867180 0.9371867180 + 0.9656612277 0.9374827743 0.9374827743 + 0.9657722712 0.9377809167 0.9377809167 + 0.9658837914 0.9380794764 0.9380794764 + 0.9660049081 0.9383779764 0.9383779764 + 0.9661260247 0.9386765957 0.9386765957 + 0.9662470818 0.9389750957 0.9389750957 + 0.9663681984 0.9392737150 0.9392737150 + 0.9664893150 0.9395722151 0.9395722151 + 0.9666103721 0.9398707747 0.9398707747 + 0.9667314887 0.9401614070 0.9401614070 + 0.9668537974 0.9404516220 0.9404516220 + 0.9669811130 0.9407417774 0.9407417774 + 0.9671084285 0.9410318732 0.9410318732 + 0.9672356844 0.9413220882 0.9413220882 + 0.9673628807 0.9416123033 0.9416123033 + 0.9674901962 0.9419023991 0.9419023991 + 0.9676175117 0.9421926141 0.9421926141 + 0.9677448273 0.9424642920 0.9424642920 + 0.9678722024 0.9427356124 0.9427356124 + 0.9680001140 0.9430068731 0.9430068731 + 0.9681280255 0.9432781935 0.9432781935 + 0.9682558775 0.9435495138 0.9435495138 + 0.9683837891 0.9438207150 0.9438207150 + 0.9685117006 0.9440919757 0.9440919757 + 0.9686396122 0.9443632960 0.9443632960 + 0.9687675238 0.9446105957 0.9446105957 + 0.9688940048 0.9448559284 0.9448559284 + 0.9690169096 0.9451010823 0.9451010823 + 0.9691396952 0.9453462958 0.9453462958 + 0.9692625999 0.9455916286 0.9455916286 + 0.9693853855 0.9458367825 0.9458367825 + 0.9695081711 0.9460821152 0.9460821152 + 0.9696310759 0.9463273287 0.9463273287 + 0.9697539210 0.9465690255 0.9465690255 + 0.9698749781 0.9468097091 0.9468097091 + 0.9699872136 0.9470502734 0.9470502734 + 0.9700993896 0.9472910166 0.9472910166 + 0.9702115059 0.9475315809 0.9475315809 + 0.9703236818 0.9477723241 0.9477723241 + 0.9704359174 0.9480128884 0.9480128884 + 0.9705479741 0.9482535124 0.9482535124 + 0.9706602097 0.9484931827 0.9484931827 + 0.9707723260 0.9487320185 0.9487320185 + 0.9708713889 0.9489706755 0.9489706755 + 0.9709702730 0.9492095113 0.9492095113 + 0.9710690975 0.9494482875 0.9494482875 + 0.9711679220 0.9496871233 0.9496871233 + 0.9712666869 0.9499258995 0.9499258995 + 0.9713655114 0.9501646757 0.9501646757 + 0.9714642763 0.9504017234 0.9504017234 + 0.9715631008 0.9506325722 0.9506325722 + 0.9716647267 0.9508634806 0.9508634806 + 0.9717674255 0.9510943890 0.9510943890 + 0.9718701243 0.9513254166 0.9513254166 + 0.9719728231 0.9515563250 0.9515563250 + 0.9720755219 0.9517871737 0.9517871737 + 0.9721782207 0.9520180821 0.9520180821 + 0.9722809792 0.9522489905 0.9522489905 + 0.9723836780 0.9524822235 0.9524822235 + 0.9724901915 0.9527158737 0.9527158737 + 0.9726037979 0.9529495835 0.9529495835 + 0.9727175236 0.9531832933 0.9531832933 + 0.9728311896 0.9534170032 0.9534170032 + 0.9729447961 0.9536505938 0.9536505938 + 0.9730585217 0.9538843036 0.9538843036 + 0.9731721878 0.9541180134 0.9541180134 + 0.9732859135 0.9543501735 0.9543501735 + 0.9733995199 0.9545791745 0.9545791745 + 0.9735301733 0.9548081160 0.9548081160 + 0.9736629128 0.9550369978 0.9550369978 + 0.9737957120 0.9552658796 0.9552658796 + 0.9739285111 0.9554948211 0.9554948211 + 0.9740611911 0.9557238221 0.9557238221 + 0.9741939902 0.9559527040 0.9559527040 + 0.9743267894 0.9561815858 0.9561815858 + 0.9744595289 0.9564020038 0.9564020038 + 0.9745973945 0.9566190243 0.9566190243 + 0.9747449160 0.9568361044 0.9568361044 + 0.9748924971 0.9570531249 0.9570531249 + 0.9750400186 0.9572700858 0.9572700858 + 0.9751874804 0.9574871063 0.9574871063 + 0.9753351212 0.9577041268 0.9577041268 + 0.9754825830 0.9579210877 0.9579210877 + 0.9756301045 0.9581381083 0.9581381083 + 0.9757776856 0.9583560228 0.9583560228 + 0.9759315848 0.9585739970 0.9585739970 + 0.9760882258 0.9587919116 0.9587919116 + 0.9762448072 0.9590098262 0.9590098262 + 0.9764013886 0.9592276812 0.9592276812 + 0.9765580297 0.9594457150 0.9594457150 + 0.9767146111 0.9596636295 0.9596636295 + 0.9768711925 0.9598814845 0.9598814845 + 0.9770277739 0.9601004124 0.9601004124 + 0.9771844149 0.9603220224 0.9603220224 + 0.9773445129 0.9605436921 0.9605436921 + 0.9775046706 0.9607654214 0.9607654214 + 0.9776650071 0.9609870911 0.9609870911 + 0.9778252244 0.9612088203 0.9612088203 + 0.9779855013 0.9614304900 0.9614304900 + 0.9781457186 0.9616522193 0.9616522193 + 0.9783058763 0.9618738890 0.9618738890 + 0.9784662127 0.9620940089 0.9620940089 + 0.9786261916 0.9623115063 0.9623115063 + 0.9787849188 0.9625288844 0.9625288844 + 0.9789437056 0.9627463818 0.9627463818 + 0.9791023731 0.9629638195 0.9629638195 + 0.9792612195 0.9631813169 0.9631813169 + 0.9794198871 0.9633988142 0.9633988142 + 0.9795786738 0.9636161923 0.9636161923 + 0.9797374010 0.9638336897 0.9638336897 + 0.9798961878 0.9640504122 0.9640504122 + 0.9800534844 0.9642658830 0.9642658830 + 0.9802057147 0.9644814730 0.9644814730 + 0.9803580046 0.9646970034 0.9646970034 + 0.9805101752 0.9649125934 0.9649125934 + 0.9806625247 0.9651281834 0.9651281834 + 0.9808148146 0.9653437138 0.9653437138 + 0.9809669852 0.9655593038 0.9655593038 + 0.9811192751 0.9657747746 0.9657747746 + 0.9812715054 0.9659873843 0.9659873843 + 0.9814214110 0.9661936760 0.9661936760 + 0.9815621972 0.9664000273 0.9664000273 + 0.9817029834 0.9666063190 0.9666063190 + 0.9818438292 0.9668126106 0.9668126106 + 0.9819844961 0.9670189023 0.9670189023 + 0.9821252823 0.9672251940 0.9672251940 + 0.9822661281 0.9674314260 0.9674314260 + 0.9824067950 0.9676377177 0.9676377177 + 0.9825475812 0.9678443074 0.9678443074 + 0.9826868773 0.9680523872 0.9680523872 + 0.9828112125 0.9682605267 0.9682605267 + 0.9829354882 0.9684684873 0.9684684873 + 0.9830598235 0.9686766267 0.9686766267 + 0.9831839800 0.9688847065 0.9688847065 + 0.9833083153 0.9690927863 0.9690927863 + 0.9834325910 0.9693009257 0.9693009257 + 0.9835569263 0.9695090055 0.9695090055 + 0.9836812019 0.9697170854 0.9697170854 + 0.9838054180 0.9699230194 0.9699230194 + 0.9839122891 0.9701287746 0.9701287746 + 0.9840167165 0.9703344703 0.9703344703 + 0.9841210246 0.9705402255 0.9705402255 + 0.9842253923 0.9707459211 0.9707459211 + 0.9843298197 0.9709516168 0.9709516168 + 0.9844341278 0.9711573124 0.9711573124 + 0.9845384955 0.9713630080 0.9713630080 + 0.9846428037 0.9715687037 0.9715687037 + 0.9847471714 0.9717782140 0.9717782140 + 0.9848499894 0.9719914198 0.9719914198 + 0.9849516153 0.9722046256 0.9722046256 + 0.9850531816 0.9724177718 0.9724177718 + 0.9851548076 0.9726309180 0.9726309180 + 0.9852563143 0.9728441238 0.9728441238 + 0.9853578806 0.9730573297 0.9730573297 + 0.9854595065 0.9732704759 0.9732704759 + 0.9855610728 0.9734836817 0.9734836817 + 0.9856626987 0.9736968279 0.9736968279 + 0.9857648015 0.9739102125 0.9739102125 + 0.9858701229 0.9741235971 0.9741235971 + 0.9859753847 0.9743369222 0.9743369222 + 0.9860807061 0.9745503068 0.9745503068 + 0.9861860275 0.9747636914 0.9747636914 + 0.9862912893 0.9749770164 0.9749770164 + 0.9863966107 0.9751904011 0.9751904011 + 0.9865018725 0.9754037857 0.9754037857 + 0.9866071939 0.9756171703 0.9756171703 + 0.9867125154 0.9758273959 0.9758273959 + 0.9868178964 0.9760339856 0.9760339856 + 0.9869232774 0.9762405753 0.9762405753 + 0.9870287776 0.9764472842 0.9764472842 + 0.9871342778 0.9766538739 0.9766538739 + 0.9872397184 0.9768605232 0.9768605232 + 0.9873452187 0.9770671129 0.9770671129 + 0.9874507189 0.9772738218 0.9772738218 + 0.9875562191 0.9774804115 0.9774804115 + 0.9876616001 0.9776870012 0.9776870012 + 0.9877671003 0.9778900146 0.9778900146 + 0.9878692031 0.9780920148 0.9780920148 + 0.9879713058 0.9782938957 0.9782938957 + 0.9880735278 0.9784958959 0.9784958959 + 0.9881755710 0.9786978960 0.9786978960 + 0.9882776737 0.9788998961 0.9788998961 + 0.9883798957 0.9791017771 0.9791017771 + 0.9884819984 0.9793037772 0.9793037772 + 0.9885841012 0.9795057774 0.9795057774 + 0.9886862040 0.9797077179 0.9797077179 + 0.9887884855 0.9798991084 0.9798991084 + 0.9888911843 0.9800903797 0.9800903797 + 0.9889938831 0.9802817702 0.9802817702 + 0.9890967011 0.9804731011 0.9804731011 + 0.9891993999 0.9806643724 0.9806643724 + 0.9893020988 0.9808558226 0.9808558226 + 0.9894047976 0.9810470939 0.9810470939 + 0.9895074964 0.9812384844 0.9812384844 + 0.9896101952 0.9814298153 0.9814298153 + 0.9897128940 0.9816213250 0.9816213250 + 0.9898141026 0.9818137884 0.9818137884 + 0.9899132252 0.9820061922 0.9820061922 + 0.9900122881 0.9821987152 0.9821987152 + 0.9901114106 0.9823911190 0.9823911190 + 0.9902104735 0.9825835824 0.9825835824 + 0.9903095961 0.9827759862 0.9827759862 + 0.9904087186 0.9829685092 0.9829685092 + 0.9905077815 0.9831609130 0.9831609130 + 0.9906069040 0.9833533764 0.9833533764 + 0.9907060266 0.9835463166 0.9835463166 + 0.9908010960 0.9837430716 0.9837430716 + 0.9908921719 0.9839398265 0.9839398265 + 0.9909833074 0.9841365218 0.9841365218 + 0.9910743833 0.9843332767 0.9843332767 + 0.9911653996 0.9845299721 0.9845299721 + 0.9912564754 0.9847267270 0.9847267270 + 0.9913476110 0.9849234819 0.9849234819 + 0.9914386868 0.9851201773 0.9851201773 + 0.9915298223 0.9853168726 0.9853168726 + 0.9916208982 0.9855136275 0.9855136275 + 0.9917073250 0.9857079983 0.9857079983 + 0.9917883277 0.9859024286 0.9859024286 + 0.9918693900 0.9860966802 0.9860966802 + 0.9919503927 0.9862911105 0.9862911105 + 0.9920313954 0.9864854217 0.9864854217 + 0.9921123981 0.9866797924 0.9866797924 + 0.9921934009 0.9868742228 0.9868742228 + 0.9922744036 0.9870684743 0.9870684743 + 0.9923554063 0.9872629046 0.9872629046 + 0.9924364090 0.9874572754 0.9874572754 + 0.9925166965 0.9876400828 0.9876400828 + 0.9925954938 0.9878200889 0.9878200889 + 0.9926741719 0.9880000949 0.9880000949 + 0.9927530289 0.9881801009 0.9881801009 + 0.9928318262 0.9883599877 0.9883599877 + 0.9929105043 0.9885399938 0.9885399938 + 0.9929893017 0.9887199998 0.9887199998 + 0.9930680990 0.9889000058 0.9889000058 + 0.9931467772 0.9890800118 0.9890800118 + 0.9932255745 0.9892598987 0.9892598987 + 0.9933044910 0.9894347787 0.9894347787 + 0.9933881760 0.9896039963 0.9896039963 + 0.9934718013 0.9897732735 0.9897732735 + 0.9935554266 0.9899424911 0.9899424911 + 0.9936391115 0.9901117086 0.9901117086 + 0.9937226772 0.9902809858 0.9902809858 + 0.9938063025 0.9904502034 0.9904502034 + 0.9938899875 0.9906194210 0.9906194210 + 0.9939736128 0.9907886982 0.9907886982 + 0.9940572977 0.9909579158 0.9909579158 + 0.9941409230 0.9911267161 0.9911267161 + 0.9942286015 0.9912837148 0.9912837148 + 0.9943184853 0.9914405942 0.9914405942 + 0.9944083095 0.9915975928 0.9915975928 + 0.9944980741 0.9917545915 0.9917545915 + 0.9945880175 0.9919115901 0.9919115901 + 0.9946777821 0.9920685887 0.9920685887 + 0.9947676063 0.9922255874 0.9922255874 + 0.9948573709 0.9923825860 0.9923825860 + 0.9949473143 0.9925395846 0.9925395846 + 0.9950370789 0.9926965833 0.9926965833 + 0.9951277971 0.9928529263 0.9928529263 + 0.9952226877 0.9930083752 0.9930083752 + 0.9953176975 0.9931638837 0.9931638837 + 0.9954125881 0.9933193922 0.9933193922 + 0.9955074787 0.9934750199 0.9934750199 + 0.9956024289 0.9936305285 0.9936305285 + 0.9956973791 0.9937859774 0.9937859774 + 0.9957923293 0.9939414859 0.9939414859 + 0.9958872199 0.9940969944 0.9940969944 + 0.9959821105 0.9942525029 0.9942525029 + 0.9960770011 0.9944080710 0.9944080710 + 0.9961733222 0.9945561886 0.9945561886 + 0.9962705970 0.9947028756 0.9947028756 + 0.9963678718 0.9948495030 0.9948495030 + 0.9964652061 0.9949960709 0.9949960709 + 0.9965624809 0.9951426983 0.9951426983 + 0.9966596961 0.9952893853 0.9952893853 + 0.9967569709 0.9954360127 0.9954360127 + 0.9968543053 0.9955825806 0.9955825806 + 0.9969515800 0.9957293272 0.9957293272 + 0.9970489144 0.9958758950 0.9958758950 + 0.9971461892 0.9960218072 0.9960218072 + 0.9972419143 0.9961522818 0.9961522818 + 0.9973374009 0.9962828159 0.9962828159 + 0.9974328876 0.9964132905 0.9964132905 + 0.9975283742 0.9965438247 0.9965438247 + 0.9976239204 0.9966742992 0.9966742992 + 0.9977192879 0.9968047738 0.9968047738 + 0.9978147745 0.9969353080 0.9969353080 + 0.9979103208 0.9970657825 0.9970657825 + 0.9980058074 0.9971963167 0.9971963167 + 0.9981012940 0.9973267913 0.9973267913 + 0.9981966019 0.9974548221 0.9974548221 + 0.9982848763 0.9975687265 0.9975687265 + 0.9983732104 0.9976826906 0.9976826906 + 0.9984614849 0.9977965951 0.9977965951 + 0.9985498190 0.9979106188 0.9979106188 + 0.9986380935 0.9980245233 0.9980245233 + 0.9987264276 0.9981384277 0.9981384277 + 0.9988147020 0.9982523918 0.9982523918 + 0.9989029765 0.9983662963 0.9983662963 + 0.9989913106 0.9984803200 0.9984803200 + 0.9990795851 0.9985942245 0.9985942245 + 0.9991670847 0.9987086058 0.9987086058 + 0.9992427826 0.9988260269 0.9988260269 + 0.9993184805 0.9989433885 0.9989433885 + 0.9993941784 0.9990608096 0.9990608096 + 0.9994698763 0.9991781712 0.9991781712 + 0.9995456934 0.9992955923 0.9992955923 + 0.9996213913 0.9994130135 0.9994130135 + 0.9996970892 0.9995303750 0.9995303750 + 0.9997727871 0.9996477962 0.9996477962 + 0.9998486042 0.9997652173 0.9997652173 + 0.9999243021 0.9998825788 0.9998825788 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_gold_100.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_gold_100.spi1d index fe5e2189..0dd57aef 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_gold_100.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_gold_100.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0085512707 0.0085512707 0.0040388089 - 0.0166211706 0.0166211706 0.0078869723 - 0.0239217207 0.0239217207 0.0115889302 - 0.0305970907 0.0305970907 0.0151835401 - 0.0367598310 0.0367598310 0.0186785609 - 0.0425867885 0.0425867885 0.0220463201 - 0.0481229611 0.0481229611 0.0253170207 - 0.0533615910 0.0533615910 0.0285046306 - 0.0583925582 0.0583925582 0.0316370614 - 0.0632038563 0.0632038563 0.0346923992 - 0.0678458065 0.0678458065 0.0376951098 - 0.0723663494 0.0723663494 0.0406254604 - 0.0767436773 0.0767436773 0.0434999615 - 0.0809408724 0.0809408724 0.0463499017 - 0.0850247815 0.0850247815 0.0491519198 - 0.0890202522 0.0890202522 0.0518954284 - 0.0929083526 0.0929083526 0.0545983687 - 0.0967549235 0.0967549235 0.0572627783 - 0.1004967019 0.1004967019 0.0598945618 - 0.1041401029 0.1041401029 0.0624886788 - 0.1076878980 0.1076878980 0.0650598332 - 0.1111321002 0.1111321002 0.0676003397 - 0.1145434007 0.1145434007 0.0700881407 - 0.1179336980 0.1179336980 0.0725532621 - 0.1213003024 0.1213003024 0.0749963596 - 0.1246234998 0.1246234998 0.0774110928 - 0.1278907061 0.1278907061 0.0798068568 - 0.1311019957 0.1311019957 0.0821953192 - 0.1342176050 0.1342176050 0.0845582336 - 0.1372642964 0.1372642964 0.0868936703 - 0.1402868927 0.1402868927 0.0891988575 - 0.1433071941 0.1433071941 0.0914948583 - 0.1463229060 0.1463229060 0.0937692001 - 0.1493196040 0.1493196040 0.0960261002 - 0.1522929072 0.1522929072 0.0982703716 - 0.1552488059 0.1552488059 0.1005069017 - 0.1581514031 0.1581514031 0.1027444005 - 0.1609746069 0.1609746069 0.1049712971 - 0.1637527943 0.1637527943 0.1071826965 - 0.1664921939 0.1664921939 0.1093685031 - 0.1691893935 0.1691893935 0.1115406975 - 0.1718754023 0.1718754023 0.1136908978 - 0.1745572984 0.1745572984 0.1158114970 - 0.1772463024 0.1772463024 0.1179215983 - 0.1799204051 0.1799204051 0.1200228035 - 0.1825827062 0.1825827062 0.1221131980 - 0.1852307022 0.1852307022 0.1241903976 - 0.1878415048 0.1878415048 0.1262644976 - 0.1904177964 0.1904177964 0.1283268929 - 0.1929835975 0.1929835975 0.1303768009 - 0.1955551058 0.1955551058 0.1324191988 - 0.1980939955 0.1980939955 0.1344535947 - 0.2005960941 0.2005960941 0.1364852041 - 0.2030780017 0.2030780017 0.1385065019 - 0.2055498958 0.2055498958 0.1404985934 - 0.2080249041 0.2080249041 0.1424852014 - 0.2104936987 0.2104936987 0.1444734931 - 0.2129532993 0.2129532993 0.1464570016 - 0.2153917998 0.2153917998 0.1484331042 - 0.2178138942 0.2178138942 0.1504009962 - 0.2202032059 0.2202032059 0.1523738950 - 0.2225545049 0.2225545049 0.1543436050 - 0.2248750031 0.2248750031 0.1563021988 - 0.2271793038 0.2271793038 0.1582521945 - 0.2294784039 0.2294784039 0.1601931006 - 0.2317692935 0.2317692935 0.1621181965 - 0.2340328991 0.2340328991 0.1640346050 - 0.2362951934 0.2362951934 0.1659477949 - 0.2385396957 0.2385396957 0.1678593010 - 0.2407682985 0.2407682985 0.1697596014 - 0.2429841012 0.2429841012 0.1716434956 - 0.2451926023 0.2451926023 0.1735229045 - 0.2473980039 0.2473980039 0.1754001975 - 0.2495967001 0.2495967001 0.1772678047 - 0.2518011928 0.2518011928 0.1791307926 - 0.2540042996 0.2540042996 0.1809882969 - 0.2561990917 0.2561990917 0.1828391999 - 0.2583766878 0.2583766878 0.1846980006 - 0.2605302930 0.2605302930 0.1865592003 - 0.2626619041 0.2626619041 0.1884168983 - 0.2647795081 0.2647795081 0.1902665943 - 0.2668896914 0.2668896914 0.1921050996 - 0.2689956129 0.2689956129 0.1939377934 - 0.2710914016 0.2710914016 0.1957681030 - 0.2731707096 0.2731707096 0.1975923926 - 0.2752295136 0.2752295136 0.1994120032 - 0.2772876024 0.2772876024 0.2012308985 - 0.2793470919 0.2793470919 0.2030453980 - 0.2813898921 0.2813898921 0.2048553973 - 0.2834160924 0.2834160924 0.2066580951 - 0.2854352891 0.2854352891 0.2084507048 - 0.2874428928 0.2874428928 0.2102348953 - 0.2894397080 0.2894397080 0.2120144963 - 0.2914342880 0.2914342880 0.2137901038 - 0.2934266925 0.2934266925 0.2155577987 - 0.2954250872 0.2954250872 0.2173161060 - 0.2974228859 0.2974228859 0.2190670073 - 0.2994059920 0.2994059920 0.2208126932 - 0.3013804853 0.3013804853 0.2225555927 - 0.3033458889 0.3033458889 0.2242987007 - 0.3052974939 0.3052974939 0.2260434926 - 0.3072321117 0.3072321117 0.2277884930 - 0.3091601133 0.3091601133 0.2295302004 - 0.3110798895 0.3110798895 0.2312685996 - 0.3129976094 0.3129976094 0.2329937071 - 0.3149156868 0.3149156868 0.2347085029 - 0.3168309927 0.3168309927 0.2364155948 - 0.3187418878 0.3187418878 0.2381125987 - 0.3206557930 0.3206557930 0.2397992015 - 0.3225586116 0.3225586116 0.2414820939 - 0.3244569898 0.3244569898 0.2431667000 - 0.3263500929 0.3263500929 0.2448462993 - 0.3282350898 0.3282350898 0.2465201020 - 0.3301092982 0.3301092982 0.2481863946 - 0.3319670856 0.3319670856 0.2498456985 - 0.3338209093 0.3338209093 0.2515039146 - 0.3356688023 0.3356688023 0.2531544864 - 0.3375090063 0.3375090063 0.2548001111 - 0.3393396139 0.3393396139 0.2564423978 - 0.3411692083 0.3411692083 0.2580820024 - 0.3429956138 0.3429956138 0.2597126067 - 0.3448179960 0.3448179960 0.2613438070 - 0.3466367126 0.3466367126 0.2629727125 - 0.3484556079 0.3484556079 0.2645977139 - 0.3502711058 0.3502711058 0.2662194967 - 0.3520781100 0.3520781100 0.2678410113 - 0.3538728952 0.3538728952 0.2694599032 - 0.3556573093 0.3556573093 0.2710745037 - 0.3574326932 0.3574326932 0.2726924121 - 0.3591986001 0.3591986001 0.2743096948 - 0.3609502912 0.3609502912 0.2759279013 - 0.3626965880 0.3626965880 0.2775534987 - 0.3644376099 0.3644376099 0.2791810930 - 0.3661716878 0.3661716878 0.2808074951 - 0.3679068089 0.3679068089 0.2824282944 - 0.3696404099 0.3696404099 0.2840456963 - 0.3713721931 0.3713721931 0.2856619060 - 0.3730970919 0.3730970919 0.2872773111 - 0.3748179972 0.3748179972 0.2888830006 - 0.3765349090 0.3765349090 0.2904838920 - 0.3782503903 0.3782503903 0.2920790017 - 0.3799589872 0.3799589872 0.2936722934 - 0.3816598058 0.3816598058 0.2952640057 - 0.3833526969 0.3833526969 0.2968522906 - 0.3850415051 0.3850415051 0.2984321117 - 0.3867242932 0.3867242932 0.3000073135 - 0.3883965015 0.3883965015 0.3015750051 - 0.3900654912 0.3900654912 0.3031345010 - 0.3917332888 0.3917332888 0.3046869040 - 0.3933978081 0.3933978081 0.3062342107 - 0.3950597942 0.3950597942 0.3077783883 - 0.3967128098 0.3967128098 0.3093160987 - 0.3983623087 0.3983623087 0.3108538985 - 0.4000048041 0.4000048041 0.3123919070 - 0.4016445875 0.4016445875 0.3139241934 - 0.4032827914 0.4032827914 0.3154524863 - 0.4049166143 0.4049166143 0.3169719875 - 0.4065478146 0.4065478146 0.3184897006 - 0.4081723094 0.4081723094 0.3200064003 - 0.4097915888 0.4097915888 0.3215180039 - 0.4114066064 0.4114066064 0.3230280876 - 0.4130186141 0.4130186141 0.3245337009 - 0.4146288931 0.4146288931 0.3260375857 - 0.4162240028 0.4162240028 0.3275386989 - 0.4178164005 0.4178164005 0.3290353119 - 0.4194015861 0.4194015861 0.3305288851 - 0.4209814072 0.4209814072 0.3320241868 - 0.4225557148 0.4225557148 0.3335199058 - 0.4241304100 0.4241304100 0.3350135982 - 0.4257053137 0.4257053137 0.3365071118 - 0.4272772074 0.4272772074 0.3379994035 - 0.4288483858 0.4288483858 0.3394888043 - 0.4304055870 0.4304055870 0.3409718871 - 0.4319624007 0.4319624007 0.3424525857 - 0.4335188866 0.4335188866 0.3439300954 - 0.4350732863 0.4350732863 0.3454062939 - 0.4366205037 0.4366205037 0.3468815088 - 0.4381664991 0.4381664991 0.3483560085 - 0.4397099912 0.4397099912 0.3498300910 - 0.4412516952 0.4412516952 0.3513006866 - 0.4427905977 0.4427905977 0.3527697921 - 0.4443261921 0.4443261921 0.3542422950 - 0.4458572865 0.4458572865 0.3557159901 - 0.4473834038 0.4473834038 0.3571884036 - 0.4489029944 0.4489029944 0.3586604893 - 0.4504176080 0.4504176080 0.3601266146 - 0.4519256949 0.4519256949 0.3615907133 - 0.4534310102 0.4534310102 0.3630562127 - 0.4549323916 0.4549323916 0.3645220101 - 0.4564324915 0.4564324915 0.3659847081 - 0.4579305947 0.4579305947 0.3674460053 - 0.4594289064 0.4594289064 0.3689005077 - 0.4609279037 0.4609279037 0.3703509867 - 0.4624246955 0.4624246955 0.3718009889 - 0.4639141858 0.4639141858 0.3732503951 - 0.4654031098 0.4654031098 0.3746976852 - 0.4668875039 0.4668875039 0.3761422932 - 0.4683718085 0.4683718085 0.3775857091 - 0.4698401988 0.4698401988 0.3790262043 - 0.4713082910 0.4713082910 0.3804647923 - 0.4727697968 0.4727697968 0.3818928897 - 0.4742299914 0.4742299914 0.3833209872 - 0.4756909013 0.4756909013 0.3847407997 - 0.4771521986 0.4771521986 0.3861603141 - 0.4786089063 0.4786089063 0.3875763118 - 0.4800598919 0.4800598919 0.3889910877 - 0.4815098047 0.4815098047 0.3904035985 - 0.4829556048 0.4829556048 0.3918142915 - 0.4844014049 0.4844014049 0.3932228088 - 0.4858314991 0.4858314991 0.3946261108 - 0.4872612059 0.4872612059 0.3960292935 - 0.4886828065 0.4886828065 0.3974260986 - 0.4901008904 0.4901008904 0.3988228142 - 0.4915114045 0.4915114045 0.4002167881 - 0.4929108918 0.4929108918 0.4016098976 - 0.4943102002 0.4943102002 0.4030028880 - 0.4957073927 0.4957073927 0.4043959975 - 0.4971045852 0.4971045852 0.4057886899 - 0.4984934032 0.4984934032 0.4071742892 - 0.4998792112 0.4998792112 0.4085598886 - 0.5012617707 0.5012617707 0.4099406004 - 0.5026388168 0.5026388168 0.4113186896 - 0.5040158033 0.5040158033 0.4126941860 - 0.5053874850 0.5053874850 0.4140625894 - 0.5067589283 0.5067589283 0.4154309928 - 0.5081269145 0.5081269145 0.4167915881 - 0.5094919205 0.5094919205 0.4181506932 - 0.5108559132 0.5108559132 0.4195038080 - 0.5122082829 0.5122082829 0.4208473861 - 0.5135607719 0.5135607719 0.4221909940 - 0.5149065852 0.5149065852 0.4235303998 - 0.5162479877 0.5162479877 0.4248695076 - 0.5175889134 0.5175889134 0.4262058139 - 0.5189247727 0.5189247727 0.4275380969 - 0.5202606916 0.5202606916 0.4288704097 - 0.5215886235 0.5215886235 0.4302046895 - 0.5229104161 0.5229104161 0.4315393865 - 0.5242322087 0.5242322087 0.4328733981 - 0.5255554914 0.5255554914 0.4342060983 - 0.5268787742 0.5268787742 0.4355388880 - 0.5281956792 0.5281956792 0.4368667901 - 0.5295035243 0.5295035243 0.4381929040 - 0.5308111906 0.5308111906 0.4395191073 - 0.5321189761 0.5321189761 0.4408474863 - 0.5334268212 0.5334268212 0.4421758950 - 0.5347332954 0.5347332954 0.4435034096 - 0.5360326171 0.5360326171 0.4448300004 - 0.5373319983 0.5373319983 0.4461565018 - 0.5386298895 0.5386298895 0.4474779069 - 0.5399259925 0.5399259925 0.4487977922 - 0.5412220955 0.5412220955 0.4501172900 - 0.5425089002 0.5425089002 0.4514313042 - 0.5437930822 0.5437930822 0.4527452886 - 0.5450770259 0.5450770259 0.4540582001 - 0.5463545918 0.5463545918 0.4553689063 - 0.5476322174 0.5476322174 0.4566796124 - 0.5489053726 0.5489053726 0.4579856992 - 0.5501675010 0.5501675010 0.4592883885 - 0.5514295101 0.5514295101 0.4605911970 - 0.5526881218 0.5526881218 0.4618827999 - 0.5539432168 0.5539432168 0.4631715119 - 0.5551983714 0.5551983714 0.4644601941 - 0.5564481020 0.5564481020 0.4657382965 - 0.5576952100 0.5576952100 0.4670161009 - 0.5589423180 0.5589423180 0.4682930112 - 0.5601819158 0.5601819158 0.4695635140 - 0.5614199042 0.5614199042 0.4708338976 - 0.5626578927 0.5626578927 0.4721035063 - 0.5638862252 0.5638862252 0.4733701050 - 0.5651137829 0.5651137829 0.4746366143 - 0.5663412213 0.5663412213 0.4759002030 - 0.5675578117 0.5675578117 0.4771572948 - 0.5687745214 0.5687745214 0.4784145057 - 0.5699902177 0.5699902177 0.4796695113 - 0.5711944103 0.5711944103 0.4809209108 - 0.5723987222 0.5723987222 0.4821723998 - 0.5736017227 0.5736017227 0.4834229946 - 0.5747933984 0.5747933984 0.4846724868 - 0.5759850144 0.5759850144 0.4859218895 - 0.5771753788 0.5771753788 0.4871684015 - 0.5783529878 0.5783529878 0.4884099960 - 0.5795305967 0.5795305967 0.4896515906 - 0.5807074904 0.5807074904 0.4908894002 - 0.5818707943 0.5818707943 0.4921191931 - 0.5830342174 0.5830342174 0.4933489859 - 0.5841975808 0.5841975808 0.4945765138 - 0.5853506923 0.5853506923 0.4957973957 - 0.5865036249 0.5865036249 0.4970183074 - 0.5876564980 0.5876564980 0.4982382059 - 0.5888124108 0.5888124108 0.4994524121 - 0.5899686813 0.5899686813 0.5006666183 - 0.5911250710 0.5911250710 0.5018808246 - 0.5922722220 0.5922722220 0.5030846000 - 0.5934156775 0.5934156775 0.5042884946 - 0.5945591927 0.5945591927 0.5054923296 - 0.5956988931 0.5956988931 0.5066840053 - 0.5968354940 0.5968354940 0.5078731179 - 0.5979719758 0.5979719758 0.5090621710 - 0.5991039276 0.5991039276 0.5102484822 - 0.6002264023 0.6002264023 0.5114330053 - 0.6013488770 0.6013488770 0.5126175284 - 0.6024705768 0.6024705768 0.5138002038 - 0.6035845876 0.6035845876 0.5149796009 - 0.6046985984 0.6046985984 0.5161591172 - 0.6058124900 0.6058124900 0.5173382759 - 0.6069167256 0.6069167256 0.5185145736 - 0.6080183983 0.6080183983 0.5196908116 - 0.6091201901 0.6091201901 0.5208671093 - 0.6102225184 0.6102225184 0.5220367908 - 0.6113256216 0.6113256216 0.5232045054 - 0.6124286056 0.6124286056 0.5243722200 - 0.6135300994 0.6135300994 0.5255355239 - 0.6146209836 0.6146209836 0.5266925097 - 0.6157119274 0.6157119274 0.5278496146 - 0.6168028712 0.6168028712 0.5290066004 - 0.6178948283 0.6178948283 0.5301629901 - 0.6189870238 0.6189870238 0.5313194990 - 0.6200792193 0.6200792193 0.5324760079 - 0.6211677194 0.6211677194 0.5336307287 - 0.6222478747 0.6222478747 0.5347841978 - 0.6233282089 0.6233282089 0.5359376073 - 0.6244084239 0.6244084239 0.5370901227 - 0.6254897118 0.6254897118 0.5382356048 - 0.6265712976 0.6265712976 0.5393812060 - 0.6276528239 0.6276528239 0.5405266881 - 0.6287301183 0.6287301183 0.5416650176 - 0.6297997832 0.6297997832 0.5427989960 - 0.6308693886 0.6308693886 0.5439329147 - 0.6319391131 0.6319391131 0.5450661778 - 0.6330032945 0.6330032945 0.5461930037 - 0.6340664029 0.6340664029 0.5473198295 - 0.6351293921 0.6351293921 0.5484464765 - 0.6361892819 0.6361892819 0.5495709777 - 0.6372389197 0.6372389197 0.5506932139 - 0.6382886171 0.6382886171 0.5518153906 - 0.6393381953 0.6393381953 0.5529376864 - 0.6403782964 0.6403782964 0.5540469885 - 0.6414123774 0.6414123774 0.5551546812 - 0.6424465179 0.6424465179 0.5562624931 - 0.6434807181 0.6434807181 0.5573676229 - 0.6445010900 0.6445010900 0.5584642291 - 0.6455209851 0.6455209851 0.5595607758 - 0.6465408802 0.6465408802 0.5606573820 - 0.6475573182 0.6475573182 0.5617504716 - 0.6485638022 0.6485638022 0.5628405809 - 0.6495704055 0.6495704055 0.5639305711 - 0.6505770087 0.6505770087 0.5650206804 - 0.6515771747 0.6515771747 0.5661026239 - 0.6525719166 0.6525719166 0.5671824217 - 0.6535665989 0.6535665989 0.5682622194 - 0.6545614004 0.6545614004 0.5693418980 - 0.6555476785 0.6555476785 0.5704169869 - 0.6565313935 0.6565313935 0.5714920163 - 0.6575151086 0.6575151086 0.5725671053 - 0.6584988236 0.6584988236 0.5736408830 - 0.6594734192 0.6594734192 0.5747092962 - 0.6604474783 0.6604474783 0.5757777095 - 0.6614215970 0.6614215970 0.5768461227 - 0.6623948812 0.6623948812 0.5779128075 - 0.6633601785 0.6633601785 0.5789762139 - 0.6643254757 0.6643254757 0.5800395012 - 0.6652907133 0.6652907133 0.5811027884 - 0.6662545800 0.6662545800 0.5821601152 - 0.6672123075 0.6672123075 0.5832095742 - 0.6681699157 0.6681699157 0.5842590928 - 0.6691275835 0.6691275835 0.5853086114 - 0.6700835824 0.6700835824 0.5863516927 - 0.6710343957 0.6710343957 0.5873885155 - 0.6719852090 0.6719852090 0.5884252787 - 0.6729360223 0.6729360223 0.5894619823 - 0.6738809943 0.6738809943 0.5904929042 - 0.6748093963 0.6748093963 0.5915179849 - 0.6757377982 0.6757377982 0.5925431252 - 0.6766662002 0.6766662002 0.5935683250 - 0.6775926948 0.6775926948 0.5945895910 - 0.6785131097 0.6785131097 0.5956068039 - 0.6794335842 0.6794335842 0.5966240168 - 0.6803539991 0.6803539991 0.5976412296 - 0.6812713742 0.6812713742 0.5986567140 - 0.6821733713 0.6821733713 0.5996695757 - 0.6830754280 0.6830754280 0.6006824970 - 0.6839774251 0.6839774251 0.6016954780 - 0.6848791242 0.6848791242 0.6027052999 - 0.6857768893 0.6857768893 0.6037077904 - 0.6866745949 0.6866745949 0.6047102213 - 0.6875724196 0.6875724196 0.6057127118 - 0.6884701252 0.6884701252 0.6067149043 - 0.6893548965 0.6893548965 0.6077160239 - 0.6902378798 0.6902378798 0.6087170243 - 0.6911209822 0.6911209822 0.6097180247 - 0.6920040250 0.6920040250 0.6107190251 - 0.6928865910 0.6928865910 0.6117115021 - 0.6937689781 0.6937689781 0.6127033234 - 0.6946514249 0.6946514249 0.6136950850 - 0.6955338120 0.6955338120 0.6146869063 - 0.6964113712 0.6964113712 0.6156741977 - 0.6972827911 0.6972827911 0.6166592240 - 0.6981540918 0.6981540918 0.6176441908 - 0.6990255117 0.6990255117 0.6186292171 - 0.6998962164 0.6998962164 0.6196087003 - 0.7007628083 0.7007628083 0.6205787063 - 0.7016295195 0.7016295195 0.6215487123 - 0.7024961114 0.7024961114 0.6225187182 - 0.7033627033 0.7033627033 0.6234884858 - 0.7042225003 0.7042225003 0.6244485974 - 0.7050803900 0.7050803900 0.6254087090 - 0.7059382796 0.7059382796 0.6263688207 - 0.7067962885 0.7067962885 0.6273288727 - 0.7076508999 0.7076508999 0.6282864809 - 0.7085005045 0.7085005045 0.6292427778 - 0.7093501091 0.7093501091 0.6301990747 - 0.7101997137 0.7101997137 0.6311553717 - 0.7110493183 0.7110493183 0.6321104765 - 0.7118933797 0.7118933797 0.6330611706 - 0.7127372026 0.7127372026 0.6340118051 - 0.7135810852 0.7135810852 0.6349623799 - 0.7144250274 0.7144250274 0.6359130740 - 0.7152673006 0.7152673006 0.6368628144 - 0.7161080241 0.7161080241 0.6378120780 - 0.7169486880 0.7169486880 0.6387614012 - 0.7177892923 0.7177892923 0.6397107244 - 0.7186300159 0.7186300159 0.6406584978 - 0.7194681168 0.7194681168 0.6415982842 - 0.7203060985 0.7203060985 0.6425381899 - 0.7211440802 0.7211440802 0.6434779763 - 0.7219821215 0.7219821215 0.6444178820 - 0.7228178978 0.7228178978 0.6453549862 - 0.7236496806 0.7236496806 0.6462903023 - 0.7244815826 0.7244815826 0.6472254992 - 0.7253134847 0.7253134847 0.6481606960 - 0.7261453271 0.7261453271 0.6490960121 - 0.7269656062 0.7269656062 0.6500247121 - 0.7277814746 0.7277814746 0.6509529948 - 0.7285972834 0.7285972834 0.6518813968 - 0.7294130921 0.7294130921 0.6528097987 - 0.7302284837 0.7302284837 0.6537358165 - 0.7310312986 0.7310312986 0.6546552181 - 0.7318341136 0.7318341136 0.6555745006 - 0.7326369286 0.7326369286 0.6564939022 - 0.7334398031 0.7334398031 0.6574133039 - 0.7342393994 0.7342393994 0.6583278179 - 0.7350317240 0.7350317240 0.6592379212 - 0.7358239889 0.7358239889 0.6601479053 - 0.7366163135 0.7366163135 0.6610580087 - 0.7374085188 0.7374085188 0.6619681120 - 0.7381967902 0.7381967902 0.6628733873 - 0.7389813066 0.7389813066 0.6637769938 - 0.7397658229 0.7397658229 0.6646806002 - 0.7405503988 0.7405503988 0.6655843258 - 0.7413349152 0.7413349152 0.6664878726 - 0.7421157956 0.7421157956 0.6673874855 - 0.7428950071 0.7428950071 0.6682866216 - 0.7436742187 0.7436742187 0.6691858172 - 0.7444533706 0.7444533706 0.6700848937 - 0.7452325821 0.7452325821 0.6709840894 - 0.7460039258 0.7460039258 0.6718817949 - 0.7467731833 0.7467731833 0.6727795005 - 0.7475423813 0.7475423813 0.6736772060 - 0.7483115792 0.7483115792 0.6745749712 - 0.7490807772 0.7490807772 0.6754717827 - 0.7498486042 0.7498486042 0.6763551831 - 0.7506161928 0.7506161928 0.6772385240 - 0.7513837814 0.7513837814 0.6781218052 - 0.7521514297 0.7521514297 0.6790052056 - 0.7529190183 0.7529190183 0.6798881292 - 0.7536765933 0.7536765933 0.6807652712 - 0.7544323802 0.7544323802 0.6816424131 - 0.7551882863 0.7551882863 0.6825196147 - 0.7559440732 0.7559440732 0.6833966970 - 0.7566999793 0.7566999793 0.6842737198 - 0.7574527860 0.7574527860 0.6851336956 - 0.7582048178 0.7582048178 0.6859937906 - 0.7589567900 0.7589567900 0.6868538260 - 0.7597088218 0.7597088218 0.6877138019 - 0.7604607940 0.7604607940 0.6885737777 - 0.7612053156 0.7612053156 0.6894273758 - 0.7619463801 0.7619463801 0.6902801991 - 0.7626876235 0.7626876235 0.6911330819 - 0.7634288073 0.7634288073 0.6919859052 - 0.7641698718 0.7641698718 0.6928387284 - 0.7649000883 0.7649000883 0.6936817169 - 0.7656204104 0.7656204104 0.6945211887 - 0.7663406134 0.7663406134 0.6953607202 - 0.7670608759 0.7670608759 0.6962001920 - 0.7677811980 0.7677811980 0.6970396042 - 0.7684996128 0.7684996128 0.6978752017 - 0.7692136168 0.7692136168 0.6987071037 - 0.7699276209 0.7699276209 0.6995388865 - 0.7706416249 0.7706416249 0.7003707886 - 0.7713556290 0.7713556290 0.7012026906 - 0.7720690966 0.7720690966 0.7020341158 - 0.7727692723 0.7727692723 0.7028638721 - 0.7734695077 0.7734695077 0.7036936879 - 0.7741696835 0.7741696835 0.7045235038 - 0.7748699188 0.7748699188 0.7053533792 - 0.7755700946 0.7755700946 0.7061831951 - 0.7762656212 0.7762656212 0.7070057988 - 0.7769591808 0.7769591808 0.7078278065 - 0.7776529193 0.7776529193 0.7086496949 - 0.7783464789 0.7783464789 0.7094715834 - 0.7790400982 0.7790400982 0.7102934718 - 0.7797309756 0.7797309756 0.7111132741 - 0.7804160714 0.7804160714 0.7119309902 - 0.7811012268 0.7811012268 0.7127488256 - 0.7817863226 0.7817863226 0.7135666013 - 0.7824714184 0.7824714184 0.7143843174 - 0.7831565142 0.7831565142 0.7152014971 - 0.7838354707 0.7838354707 0.7160099149 - 0.7845135927 0.7845135927 0.7168182135 - 0.7851915956 0.7851915956 0.7176265717 - 0.7858697176 0.7858697176 0.7184348702 - 0.7865477800 0.7865477800 0.7192432880 - 0.7872189879 0.7872189879 0.7200449109 - 0.7878777981 0.7878777981 0.7208411098 - 0.7885367274 0.7885367274 0.7216373086 - 0.7891955972 0.7891955972 0.7224333882 - 0.7898544073 0.7898544073 0.7232295871 - 0.7905132771 0.7905132771 0.7240257859 - 0.7911655903 0.7911655903 0.7248097062 - 0.7918161750 0.7918161750 0.7255933881 - 0.7924668193 0.7924668193 0.7263771296 - 0.7931174040 0.7931174040 0.7271608114 - 0.7937679887 0.7937679887 0.7279444933 - 0.7944167256 0.7944167256 0.7287240028 - 0.7950556278 0.7950556278 0.7294961214 - 0.7956945896 0.7956945896 0.7302681208 - 0.7963336110 0.7963336110 0.7310401797 - 0.7969725728 0.7969725728 0.7318121791 - 0.7976115942 0.7976115942 0.7325842977 - 0.7982401848 0.7982401848 0.7333511114 - 0.7988576889 0.7988576889 0.7341153026 - 0.7994753122 0.7994753122 0.7348794937 - 0.8000928164 0.8000928164 0.7356436849 - 0.8007103205 0.8007103205 0.7364078760 - 0.8013278246 0.8013278246 0.7371721268 - 0.8019409180 0.8019409180 0.7379267216 - 0.8025524020 0.8025524020 0.7386804819 - 0.8031638861 0.8031638861 0.7394343019 - 0.8037754297 0.8037754297 0.7401881218 - 0.8043869138 0.8043869138 0.7409418225 - 0.8049983978 0.8049983978 0.7416955233 - 0.8055992126 0.8055992126 0.7424479723 - 0.8061988950 0.8061988950 0.7432004809 - 0.8067986965 0.8067986965 0.7439529896 - 0.8073984981 0.8073984981 0.7447054982 - 0.8079982996 0.8079982996 0.7454580069 - 0.8085979223 0.8085979223 0.7462081909 - 0.8091943860 0.8091943860 0.7469503880 - 0.8097909093 0.8097909093 0.7476925850 - 0.8103873134 0.8103873134 0.7484347820 - 0.8109837770 0.8109837770 0.7491769791 - 0.8115803003 0.8115803003 0.7499191761 - 0.8121759295 0.8121759295 0.7506610751 - 0.8127642274 0.8127642274 0.7514020801 - 0.8133525252 0.8133525252 0.7521430850 - 0.8139408231 0.8139408231 0.7528840899 - 0.8145290017 0.8145290017 0.7536250949 - 0.8151172996 0.8151172996 0.7543660998 - 0.8157039881 0.8157039881 0.7551041842 - 0.8162763119 0.8162763119 0.7558351159 - 0.8168486953 0.8168486953 0.7565659285 - 0.8174210787 0.8174210787 0.7572966814 - 0.8179934025 0.8179934025 0.7580274940 - 0.8185657859 0.8185657859 0.7587584257 - 0.8191381097 0.8191381097 0.7594878078 - 0.8197093010 0.8197093010 0.7602121234 - 0.8202804923 0.8202804923 0.7609363794 - 0.8208516836 0.8208516836 0.7616605759 - 0.8214228749 0.8214228749 0.7623848915 - 0.8219941258 0.8219941258 0.7631092072 - 0.8225653172 0.8225653172 0.7638326287 - 0.8231304884 0.8231304884 0.7645456791 - 0.8236950040 0.8236950040 0.7652587891 - 0.8242594004 0.8242594004 0.7659717798 - 0.8248239160 0.8248239160 0.7666848898 - 0.8253883719 0.8253883719 0.7673979998 - 0.8259528875 0.8259528875 0.7681111097 - 0.8265138268 0.8265138268 0.7688196898 - 0.8270732164 0.8270732164 0.7695276737 - 0.8276326060 0.8276326060 0.7702357173 - 0.8281919956 0.8281919956 0.7709437013 - 0.8287513852 0.8287513852 0.7716516852 - 0.8293107748 0.8293107748 0.7723596096 - 0.8298665881 0.8298665881 0.7730653286 - 0.8304176927 0.8304176927 0.7737694979 - 0.8309687972 0.8309687972 0.7744737267 - 0.8315199018 0.8315199018 0.7751778960 - 0.8320710063 0.8320710063 0.7758821249 - 0.8326221108 0.8326221108 0.7765862942 - 0.8331729174 0.8331729174 0.7772877216 - 0.8337209225 0.8337209225 0.7779809833 - 0.8342689276 0.8342689276 0.7786744237 - 0.8348168135 0.8348168135 0.7793678045 - 0.8353648186 0.8353648186 0.7800611854 - 0.8359127045 0.8359127045 0.7807545066 - 0.8364607096 0.8364607096 0.7814478874 - 0.8370041251 0.8370041251 0.7821326256 - 0.8375453949 0.8375453949 0.7828158140 - 0.8380867243 0.8380867243 0.7834990025 - 0.8386281133 0.8386281133 0.7841823101 - 0.8391693830 0.8391693830 0.7848654985 - 0.8397107124 0.8397107124 0.7855486870 - 0.8402513862 0.8402513862 0.7862282991 - 0.8407893777 0.8407893777 0.7869021297 - 0.8413274288 0.8413274288 0.7875759006 - 0.8418654799 0.8418654799 0.7882496119 - 0.8424034715 0.8424034715 0.7889233828 - 0.8429415226 0.8429415226 0.7895972133 - 0.8434795737 0.8434795737 0.7902709246 - 0.8440154791 0.8440154791 0.7909373045 - 0.8445503712 0.8445503712 0.7916023135 - 0.8450853229 0.8450853229 0.7922673225 - 0.8456202149 0.8456202149 0.7929322124 - 0.8461551070 0.8461551070 0.7935972214 - 0.8466899991 0.8466899991 0.7942621112 - 0.8472244143 0.8472244143 0.7949252725 - 0.8477507234 0.8477507234 0.7955821753 - 0.8482769728 0.8482769728 0.7962390184 - 0.8488032818 0.8488032818 0.7968958020 - 0.8493295908 0.8493295908 0.7975527048 - 0.8498557806 0.8498557806 0.7982094884 - 0.8503820896 0.8503820896 0.7988663912 - 0.8509023786 0.8509023786 0.7995193005 - 0.8514125943 0.8514125943 0.8001686931 - 0.8519228101 0.8519228101 0.8008180857 - 0.8524330258 0.8524330258 0.8014674783 - 0.8529431224 0.8529431224 0.8021169901 - 0.8534532785 0.8534532785 0.8027663827 - 0.8539634943 0.8539634943 0.8034157753 - 0.8544731736 0.8544731736 0.8040614128 - 0.8549826145 0.8549826145 0.8047059178 - 0.8554919958 0.8554919958 0.8053503036 - 0.8560013175 0.8560013175 0.8059946895 - 0.8565106988 0.8565106988 0.8066391945 - 0.8570200801 0.8570200801 0.8072835803 - 0.8575295210 0.8575295210 0.8079280257 - 0.8580337167 0.8580337167 0.8085677028 - 0.8585366011 0.8585366011 0.8092070818 - 0.8590394258 0.8590394258 0.8098465204 - 0.8595423102 0.8595423102 0.8104857802 - 0.8600450754 0.8600450754 0.8111252189 - 0.8605480194 0.8605480194 0.8117644787 - 0.8610509038 0.8610509038 0.8124037981 - 0.8615514040 0.8615514040 0.8130391240 - 0.8620514870 0.8620514870 0.8136743903 - 0.8625516891 0.8625516891 0.8143097162 - 0.8630518913 0.8630518913 0.8149451017 - 0.8635520935 0.8635520935 0.8155804276 - 0.8640522957 0.8640522957 0.8162156940 - 0.8645523787 0.8645523787 0.8168507218 - 0.8650534749 0.8650534749 0.8174775243 - 0.8655548096 0.8655548096 0.8181043863 - 0.8660560250 0.8660560250 0.8187311888 - 0.8665571809 0.8665571809 0.8193581104 - 0.8670583963 0.8670583963 0.8199849129 - 0.8675596714 0.8675596714 0.8206117749 - 0.8680608869 0.8680608869 0.8212385774 - 0.8685551882 0.8685551882 0.8218612075 - 0.8690482974 0.8690482974 0.8224837184 - 0.8695412874 0.8695412874 0.8231062293 - 0.8700343966 0.8700343966 0.8237286210 - 0.8705273867 0.8705273867 0.8243510723 - 0.8710203767 0.8710203767 0.8249735832 - 0.8715134859 0.8715134859 0.8255960941 - 0.8719956875 0.8719956875 0.8262088895 - 0.8724732995 0.8724732995 0.8268200159 - 0.8729509711 0.8729509711 0.8274310827 - 0.8734285831 0.8734285831 0.8280420899 - 0.8739063144 0.8739063144 0.8286532164 - 0.8743839264 0.8743839264 0.8292642832 - 0.8748615980 0.8748615980 0.8298754096 - 0.8753389120 0.8753389120 0.8304854035 - 0.8758158088 0.8758158088 0.8310946822 - 0.8762927055 0.8762927055 0.8317040801 - 0.8767696023 0.8767696023 0.8323134780 - 0.8772464991 0.8772464991 0.8329228163 - 0.8777233958 0.8777233958 0.8335322142 - 0.8782002926 0.8782002926 0.8341416121 - 0.8786755800 0.8786755800 0.8347480297 - 0.8791444898 0.8791444898 0.8353486061 - 0.8796133995 0.8796133995 0.8359493017 - 0.8800823092 0.8800823092 0.8365498781 - 0.8805512190 0.8805512190 0.8371505737 - 0.8810201287 0.8810201287 0.8377512097 - 0.8814889789 0.8814889789 0.8383519053 - 0.8819578886 0.8819578886 0.8389524817 - 0.8824142218 0.8824142218 0.8395484090 - 0.8828679919 0.8828679919 0.8401440978 - 0.8833217025 0.8833217025 0.8407397866 - 0.8837754726 0.8837754726 0.8413354158 - 0.8842293024 0.8842293024 0.8419311047 - 0.8846830726 0.8846830726 0.8425267935 - 0.8851369023 0.8851369023 0.8431224823 - 0.8855882287 0.8855882287 0.8437144756 - 0.8860356212 0.8860356212 0.8443030119 - 0.8864830136 0.8864830136 0.8448914886 - 0.8869304061 0.8869304061 0.8454800844 - 0.8873776793 0.8873776793 0.8460686207 - 0.8878250718 0.8878250718 0.8466570973 - 0.8882725239 0.8882725239 0.8472455740 - 0.8887199163 0.8887199163 0.8478341103 - 0.8891612291 0.8891612291 0.8484159112 - 0.8896014094 0.8896014094 0.8489974737 - 0.8900417089 0.8900417089 0.8495792150 - 0.8904820085 0.8904820085 0.8501607776 - 0.8909221888 0.8909221888 0.8507425189 - 0.8913624883 0.8913624883 0.8513240814 - 0.8918027282 0.8918027282 0.8519057035 - 0.8922411799 0.8922411799 0.8524854779 - 0.8926743269 0.8926743269 0.8530620933 - 0.8931074142 0.8931074142 0.8536388278 - 0.8935403824 0.8935403824 0.8542153835 - 0.8939735293 0.8939735293 0.8547919989 - 0.8944066167 0.8944066167 0.8553686142 - 0.8948397040 0.8948397040 0.8559452891 - 0.8952727914 0.8952727914 0.8565219045 - 0.8957030177 0.8957030177 0.8570963740 - 0.8961309195 0.8961309195 0.8576697707 - 0.8965588212 0.8965588212 0.8582432866 - 0.8969867229 0.8969867229 0.8588168025 - 0.8974146843 0.8974146843 0.8593901992 - 0.8978425860 0.8978425860 0.8599637151 - 0.8982704878 0.8982704878 0.8605371714 - 0.8986983895 0.8986983895 0.8611106873 - 0.8991193175 0.8991193175 0.8616771102 - 0.8995382190 0.8995382190 0.8622428179 - 0.8999571204 0.8999571204 0.8628084064 - 0.9003760219 0.9003760219 0.8633741140 - 0.9007949233 0.9007949233 0.8639398217 - 0.9012138247 0.9012138247 0.8645054102 - 0.9016327262 0.9016327262 0.8650711179 - 0.9020516276 0.9020516276 0.8656365275 - 0.9024695158 0.9024695158 0.8661962152 - 0.9028872848 0.9028872848 0.8667557836 - 0.9033051729 0.9033051729 0.8673154116 - 0.9037230015 0.9037230015 0.8678750992 - 0.9041407704 0.9041407704 0.8684347272 - 0.9045585990 0.9045585990 0.8689942956 - 0.9049764872 0.9049764872 0.8695539236 - 0.9053943157 0.9053943157 0.8701127172 - 0.9058037996 0.9058037996 0.8706625104 - 0.9062129855 0.9062129855 0.8712123036 - 0.9066222906 0.9066222906 0.8717620969 - 0.9070315957 0.9070315957 0.8723118901 - 0.9074407816 0.9074407816 0.8728616834 - 0.9078500867 0.9078500867 0.8734114170 - 0.9082593918 0.9082593918 0.8739612103 - 0.9086686969 0.9086686969 0.8745108247 - 0.9090744257 0.9090744257 0.8750563860 - 0.9094799757 0.9094799757 0.8756021261 - 0.9098855853 0.9098855853 0.8761476874 - 0.9102911949 0.9102911949 0.8766933084 - 0.9106968045 0.9106968045 0.8772389293 - 0.9111024141 0.9111024141 0.8777846098 - 0.9115080237 0.9115080237 0.8783301711 - 0.9119135737 0.9119135737 0.8788757920 - 0.9123172760 0.9123172760 0.8794146180 - 0.9127206802 0.9127206802 0.8799529076 - 0.9131240845 0.9131240845 0.8804911971 - 0.9135274291 0.9135274291 0.8810294867 - 0.9139307737 0.9139307737 0.8815677762 - 0.9143341780 0.9143341780 0.8821061254 - 0.9147375822 0.9147375822 0.8826442957 - 0.9151409268 0.9151409268 0.8831825852 - 0.9155405164 0.9155405164 0.8837168813 - 0.9159377217 0.9159377217 0.8842496276 - 0.9163349271 0.9163349271 0.8847821951 - 0.9167321920 0.9167321920 0.8853148818 - 0.9171293974 0.9171293974 0.8858476281 - 0.9175266027 0.9175266027 0.8863803148 - 0.9179238081 0.9179238081 0.8869130015 - 0.9183210135 0.9183210135 0.8874456882 - 0.9187163711 0.9187163711 0.8879768252 - 0.9191077948 0.9191077948 0.8885056973 - 0.9194992185 0.9194992185 0.8890346885 - 0.9198907018 0.9198907018 0.8895636201 - 0.9202821255 0.9202821255 0.8900926113 - 0.9206734896 0.9206734896 0.8906214833 - 0.9210649133 0.9210649133 0.8911504745 - 0.9214563966 0.9214563966 0.8916794062 - 0.9218478203 0.9218478203 0.8922079802 - 0.9222431183 0.9222431183 0.8927248120 - 0.9226387143 0.9226387143 0.8932415843 - 0.9230343103 0.9230343103 0.8937584162 - 0.9234299064 0.9234299064 0.8942751288 - 0.9238255024 0.9238255024 0.8947919011 - 0.9242212176 0.9242212176 0.8953086734 - 0.9246168137 0.9246168137 0.8958255053 - 0.9250124097 0.9250124097 0.8963422179 - 0.9254066944 0.9254066944 0.8968577981 - 0.9257994294 0.9257994294 0.8973724246 - 0.9261922240 0.9261922240 0.8978869915 - 0.9265850186 0.9265850186 0.8984016776 - 0.9269778132 0.9269778132 0.8989163041 - 0.9273704886 0.9273704886 0.8994308710 - 0.9277632833 0.9277632833 0.8999454975 - 0.9281560779 0.9281560779 0.9004601240 - 0.9285488129 0.9285488129 0.9009746909 - 0.9289314151 0.9289314151 0.9014821053 - 0.9293128252 0.9293128252 0.9019892216 - 0.9296941161 0.9296941161 0.9024963975 - 0.9300754070 0.9300754070 0.9030035138 - 0.9304568172 0.9304568172 0.9035106897 - 0.9308381081 0.9308381081 0.9040178061 - 0.9312195182 0.9312195182 0.9045249820 - 0.9316008091 0.9316008091 0.9050320983 - 0.9319807887 0.9319807887 0.9055399895 - 0.9323564172 0.9323564172 0.9060496092 - 0.9327319860 0.9327319860 0.9065591097 - 0.9331076145 0.9331076145 0.9070687294 - 0.9334831834 0.9334831834 0.9075781703 - 0.9338588119 0.9338588119 0.9080877900 - 0.9342343807 0.9342343807 0.9085972905 - 0.9346100092 0.9346100092 0.9091069102 - 0.9349855781 0.9349855781 0.9096164107 - 0.9353610277 0.9353610277 0.9101235867 - 0.9357361794 0.9357361794 0.9106287956 - 0.9361115098 0.9361115098 0.9111341238 - 0.9364867210 0.9364867210 0.9116392732 - 0.9368619919 0.9368619919 0.9121444821 - 0.9372372031 0.9372372031 0.9126498103 - 0.9376124740 0.9376124740 0.9131550193 - 0.9379876852 0.9379876852 0.9136602879 - 0.9383630157 0.9383630157 0.9141654968 - 0.9387336969 0.9387336969 0.9146683216 - 0.9391016960 0.9391016960 0.9151700735 - 0.9394698143 0.9394698143 0.9156718850 - 0.9398378134 0.9398378134 0.9161736965 - 0.9402058125 0.9402058125 0.9166755080 - 0.9405738115 0.9405738115 0.9171772003 - 0.9409418106 0.9409418106 0.9176790118 - 0.9413098097 0.9413098097 0.9181808233 - 0.9416779280 0.9416779280 0.9186825752 - 0.9420405030 0.9420405030 0.9191823006 - 0.9424005747 0.9424005747 0.9196813107 - 0.9427607059 0.9427607059 0.9201803803 - 0.9431207180 0.9431207180 0.9206793904 - 0.9434807897 0.9434807897 0.9211784005 - 0.9438409209 0.9438409209 0.9216774106 - 0.9442009926 0.9442009926 0.9221764803 - 0.9445611238 0.9445611238 0.9226754904 - 0.9449210763 0.9449210763 0.9231745005 - 0.9452818036 0.9452818036 0.9236670136 - 0.9456428289 0.9456428289 0.9241566062 - 0.9460036755 0.9460036755 0.9246463180 - 0.9463647008 0.9463647008 0.9251359105 - 0.9467257261 0.9467257261 0.9256255031 - 0.9470866919 0.9470866919 0.9261152148 - 0.9474475980 0.9474475980 0.9266048074 - 0.9478086233 0.9478086233 0.9270945191 - 0.9481695890 0.9481695890 0.9275841117 - 0.9485284090 0.9485284090 0.9280738831 - 0.9488850236 0.9488850236 0.9285637140 - 0.9492415786 0.9492415786 0.9290536046 - 0.9495981932 0.9495981932 0.9295433760 - 0.9499548078 0.9499548078 0.9300333261 - 0.9503114223 0.9503114223 0.9305232167 - 0.9506679773 0.9506679773 0.9310129881 - 0.9510245919 0.9510245919 0.9315028787 - 0.9513812065 0.9513812065 0.9319928288 - 0.9517353773 0.9517353773 0.9324811101 - 0.9520826936 0.9520826936 0.9329659939 - 0.9524298906 0.9524298906 0.9334508777 - 0.9527770877 0.9527770877 0.9339358211 - 0.9531244040 0.9531244040 0.9344208241 - 0.9534716010 0.9534716010 0.9349057078 - 0.9538187981 0.9538187981 0.9353905916 - 0.9541659951 0.9541659951 0.9358754754 - 0.9545133114 0.9545133114 0.9363604784 - 0.9548605084 0.9548605084 0.9368454218 - 0.9552074075 0.9552074075 0.9373251796 - 0.9555541873 0.9555541873 0.9378048778 - 0.9559010267 0.9559010267 0.9382845759 - 0.9562478065 0.9562478065 0.9387642145 - 0.9565945864 0.9565945864 0.9392439127 - 0.9569414258 0.9569414258 0.9397236109 - 0.9572882056 0.9572882056 0.9402033091 - 0.9576349854 0.9576349854 0.9406828880 - 0.9579818249 0.9579818249 0.9411625862 - 0.9583277106 0.9583277106 0.9416403770 - 0.9586725235 0.9586725235 0.9421163201 - 0.9590172172 0.9590172172 0.9425923228 - 0.9593619704 0.9593619704 0.9430683255 - 0.9597067237 0.9597067237 0.9435443282 - 0.9600514770 0.9600514770 0.9440202713 - 0.9603962898 0.9603962898 0.9444962144 - 0.9607409835 0.9607409835 0.9449722171 - 0.9610857964 0.9610857964 0.9454482198 - 0.9614306092 0.9614306092 0.9459242225 - 0.9617655277 0.9617655277 0.9463955164 - 0.9620996714 0.9620996714 0.9468665719 - 0.9624338150 0.9624338150 0.9473378062 - 0.9627678990 0.9627678990 0.9478089213 - 0.9631021023 0.9631021023 0.9482800961 - 0.9634361863 0.9634361863 0.9487512112 - 0.9637703896 0.9637703896 0.9492223859 - 0.9641044736 0.9641044736 0.9496935010 - 0.9644386172 0.9644386172 0.9501646757 - 0.9647706747 0.9647706747 0.9506316185 - 0.9650973082 0.9650973082 0.9510896206 - 0.9654238820 0.9654238820 0.9515476823 - 0.9657505155 0.9657505155 0.9520056844 - 0.9660770893 0.9660770893 0.9524638057 - 0.9664037228 0.9664037228 0.9529218078 - 0.9667302966 0.9667302966 0.9533798099 - 0.9670568705 0.9670568705 0.9538378716 - 0.9673835039 0.9673835039 0.9542958736 - 0.9677100778 0.9677100778 0.9547539949 - 0.9680343866 0.9680343866 0.9552072287 - 0.9683567286 0.9683567286 0.9556567073 - 0.9686790109 0.9686790109 0.9561061859 - 0.9690012932 0.9690012932 0.9565556049 - 0.9693235159 0.9693235159 0.9570050836 - 0.9696457982 0.9696457982 0.9574546218 - 0.9699680805 0.9699680805 0.9579041004 - 0.9702903032 0.9702903032 0.9583535790 - 0.9706125855 0.9706125855 0.9588031173 - 0.9709349275 0.9709349275 0.9592525959 - 0.9712525010 0.9712525010 0.9596999288 - 0.9715678096 0.9715678096 0.9601463079 - 0.9718831182 0.9718831182 0.9605926275 - 0.9721983075 0.9721983075 0.9610390067 - 0.9725136161 0.9725136161 0.9614853859 - 0.9728289247 0.9728289247 0.9619318247 - 0.9731441736 0.9731441736 0.9623782039 - 0.9734594226 0.9734594226 0.9628245234 - 0.9737746716 0.9737746716 0.9632709026 - 0.9740899801 0.9740899801 0.9637172818 - 0.9744006991 0.9744006991 0.9641577005 - 0.9747095108 0.9747095108 0.9645959735 - 0.9750183225 0.9750183225 0.9650343060 - 0.9753270745 0.9753270745 0.9654725194 - 0.9756358266 0.9756358266 0.9659107924 - 0.9759445786 0.9759445786 0.9663491249 - 0.9762533903 0.9762533903 0.9667872787 - 0.9765622020 0.9765622020 0.9672256112 - 0.9768708944 0.9768708944 0.9676638246 - 0.9771797061 0.9771797061 0.9681020975 - 0.9774832129 0.9774832129 0.9685348868 - 0.9777835011 0.9777835011 0.9689648747 - 0.9780837893 0.9780837893 0.9693949223 - 0.9783840775 0.9783840775 0.9698249102 - 0.9786844254 0.9786844254 0.9702548981 - 0.9789847136 0.9789847136 0.9706848264 - 0.9792850018 0.9792850018 0.9711148143 - 0.9795852900 0.9795852900 0.9715448022 - 0.9798855782 0.9798855782 0.9719747901 - 0.9801859260 0.9801859260 0.9724047780 - 0.9804825187 0.9804825187 0.9728298187 - 0.9807745814 0.9807745814 0.9732491970 - 0.9810665846 0.9810665846 0.9736686945 - 0.9813587070 0.9813587070 0.9740880728 - 0.9816508293 0.9816508293 0.9745075107 - 0.9819427729 0.9819427729 0.9749270082 - 0.9822348952 0.9822348952 0.9753463864 - 0.9825268984 0.9825268984 0.9757658243 - 0.9828190207 0.9828190207 0.9761853218 - 0.9831110239 0.9831110239 0.9766047001 - 0.9834020734 0.9834020734 0.9770221114 - 0.9836878181 0.9836878181 0.9774296284 - 0.9839735031 0.9839735031 0.9778370857 - 0.9842591882 0.9842591882 0.9782447219 - 0.9845448732 0.9845448732 0.9786521792 - 0.9848306179 0.9848306179 0.9790596962 - 0.9851163030 0.9851163030 0.9794672132 - 0.9854019880 0.9854019880 0.9798747897 - 0.9856876135 0.9856876135 0.9802823067 - 0.9859732985 0.9859732985 0.9806898236 - 0.9862589836 0.9862589836 0.9810972810 - 0.9865384102 0.9865384102 0.9814953208 - 0.9868156910 0.9868156910 0.9818902016 - 0.9870929122 0.9870929122 0.9822852015 - 0.9873701930 0.9873701930 0.9826802015 - 0.9876474142 0.9876474142 0.9830750823 - 0.9879245758 0.9879245758 0.9834700823 - 0.9882019162 0.9882019162 0.9838650823 - 0.9884790778 0.9884790778 0.9842600226 - 0.9887564182 0.9887564182 0.9846550226 - 0.9890335798 0.9890335798 0.9850500226 - 0.9893082976 0.9893082976 0.9854415059 - 0.9895743132 0.9895743132 0.9858223200 - 0.9898403287 0.9898403287 0.9862031937 - 0.9901062846 0.9901062846 0.9865840077 - 0.9903723001 0.9903723001 0.9869648218 - 0.9906383157 0.9906383157 0.9873455763 - 0.9909042120 0.9909042120 0.9877265096 - 0.9911702275 0.9911702275 0.9881073236 - 0.9914361835 0.9914361835 0.9884880781 - 0.9917021990 0.9917021990 0.9888688922 - 0.9919682145 0.9919682145 0.9892497063 - 0.9922304153 0.9922304153 0.9896146059 - 0.9924901128 0.9924901128 0.9899690151 - 0.9927496910 0.9927496910 0.9903233051 - 0.9930093288 0.9930093288 0.9906777143 - 0.9932690263 0.9932690263 0.9910321236 - 0.9935286045 0.9935286045 0.9913864136 - 0.9937881827 0.9937881827 0.9917408228 - 0.9940478206 0.9940478206 0.9920951724 - 0.9943075180 0.9943075180 0.9924495220 - 0.9945670962 0.9945670962 0.9928038716 - 0.9948266745 0.9948266745 0.9931582808 - 0.9950770140 0.9950770140 0.9934852719 - 0.9953256249 0.9953256249 0.9938076735 - 0.9955741167 0.9955741167 0.9941300750 - 0.9958227277 0.9958227277 0.9944524169 - 0.9960712194 0.9960712194 0.9947748184 - 0.9963197708 0.9963197708 0.9950972199 - 0.9965683222 0.9965683222 0.9954195023 - 0.9968168736 0.9968168736 0.9957419038 - 0.9970654249 0.9970654249 0.9960643053 - 0.9973139763 0.9973139763 0.9963865876 - 0.9975625277 0.9975625277 0.9967089891 - 0.9977846146 0.9977846146 0.9970085025 - 0.9980061054 0.9980061054 0.9973075986 - 0.9982277155 0.9982277155 0.9976068139 - 0.9984492064 0.9984492064 0.9979059100 - 0.9986708164 0.9986708164 0.9982051253 - 0.9988923073 0.9988923073 0.9985042214 - 0.9991137981 0.9991137981 0.9988033772 - 0.9993354082 0.9993354082 0.9991024733 - 0.9995568991 0.9995568991 0.9994016886 - 0.9997785091 0.9997785091 0.9997007847 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0085512707 0.0085512707 0.0040388089 + 0.0166211706 0.0166211706 0.0078869723 + 0.0239217207 0.0239217207 0.0115889302 + 0.0305970907 0.0305970907 0.0151835401 + 0.0367598310 0.0367598310 0.0186785609 + 0.0425867885 0.0425867885 0.0220463201 + 0.0481229611 0.0481229611 0.0253170207 + 0.0533615910 0.0533615910 0.0285046306 + 0.0583925582 0.0583925582 0.0316370614 + 0.0632038563 0.0632038563 0.0346923992 + 0.0678458065 0.0678458065 0.0376951098 + 0.0723663494 0.0723663494 0.0406254604 + 0.0767436773 0.0767436773 0.0434999615 + 0.0809408724 0.0809408724 0.0463499017 + 0.0850247815 0.0850247815 0.0491519198 + 0.0890202522 0.0890202522 0.0518954284 + 0.0929083526 0.0929083526 0.0545983687 + 0.0967549235 0.0967549235 0.0572627783 + 0.1004967019 0.1004967019 0.0598945618 + 0.1041401029 0.1041401029 0.0624886788 + 0.1076878980 0.1076878980 0.0650598332 + 0.1111321002 0.1111321002 0.0676003397 + 0.1145434007 0.1145434007 0.0700881407 + 0.1179336980 0.1179336980 0.0725532621 + 0.1213003024 0.1213003024 0.0749963596 + 0.1246234998 0.1246234998 0.0774110928 + 0.1278907061 0.1278907061 0.0798068568 + 0.1311019957 0.1311019957 0.0821953192 + 0.1342176050 0.1342176050 0.0845582336 + 0.1372642964 0.1372642964 0.0868936703 + 0.1402868927 0.1402868927 0.0891988575 + 0.1433071941 0.1433071941 0.0914948583 + 0.1463229060 0.1463229060 0.0937692001 + 0.1493196040 0.1493196040 0.0960261002 + 0.1522929072 0.1522929072 0.0982703716 + 0.1552488059 0.1552488059 0.1005069017 + 0.1581514031 0.1581514031 0.1027444005 + 0.1609746069 0.1609746069 0.1049712971 + 0.1637527943 0.1637527943 0.1071826965 + 0.1664921939 0.1664921939 0.1093685031 + 0.1691893935 0.1691893935 0.1115406975 + 0.1718754023 0.1718754023 0.1136908978 + 0.1745572984 0.1745572984 0.1158114970 + 0.1772463024 0.1772463024 0.1179215983 + 0.1799204051 0.1799204051 0.1200228035 + 0.1825827062 0.1825827062 0.1221131980 + 0.1852307022 0.1852307022 0.1241903976 + 0.1878415048 0.1878415048 0.1262644976 + 0.1904177964 0.1904177964 0.1283268929 + 0.1929835975 0.1929835975 0.1303768009 + 0.1955551058 0.1955551058 0.1324191988 + 0.1980939955 0.1980939955 0.1344535947 + 0.2005960941 0.2005960941 0.1364852041 + 0.2030780017 0.2030780017 0.1385065019 + 0.2055498958 0.2055498958 0.1404985934 + 0.2080249041 0.2080249041 0.1424852014 + 0.2104936987 0.2104936987 0.1444734931 + 0.2129532993 0.2129532993 0.1464570016 + 0.2153917998 0.2153917998 0.1484331042 + 0.2178138942 0.2178138942 0.1504009962 + 0.2202032059 0.2202032059 0.1523738950 + 0.2225545049 0.2225545049 0.1543436050 + 0.2248750031 0.2248750031 0.1563021988 + 0.2271793038 0.2271793038 0.1582521945 + 0.2294784039 0.2294784039 0.1601931006 + 0.2317692935 0.2317692935 0.1621181965 + 0.2340328991 0.2340328991 0.1640346050 + 0.2362951934 0.2362951934 0.1659477949 + 0.2385396957 0.2385396957 0.1678593010 + 0.2407682985 0.2407682985 0.1697596014 + 0.2429841012 0.2429841012 0.1716434956 + 0.2451926023 0.2451926023 0.1735229045 + 0.2473980039 0.2473980039 0.1754001975 + 0.2495967001 0.2495967001 0.1772678047 + 0.2518011928 0.2518011928 0.1791307926 + 0.2540042996 0.2540042996 0.1809882969 + 0.2561990917 0.2561990917 0.1828391999 + 0.2583766878 0.2583766878 0.1846980006 + 0.2605302930 0.2605302930 0.1865592003 + 0.2626619041 0.2626619041 0.1884168983 + 0.2647795081 0.2647795081 0.1902665943 + 0.2668896914 0.2668896914 0.1921050996 + 0.2689956129 0.2689956129 0.1939377934 + 0.2710914016 0.2710914016 0.1957681030 + 0.2731707096 0.2731707096 0.1975923926 + 0.2752295136 0.2752295136 0.1994120032 + 0.2772876024 0.2772876024 0.2012308985 + 0.2793470919 0.2793470919 0.2030453980 + 0.2813898921 0.2813898921 0.2048553973 + 0.2834160924 0.2834160924 0.2066580951 + 0.2854352891 0.2854352891 0.2084507048 + 0.2874428928 0.2874428928 0.2102348953 + 0.2894397080 0.2894397080 0.2120144963 + 0.2914342880 0.2914342880 0.2137901038 + 0.2934266925 0.2934266925 0.2155577987 + 0.2954250872 0.2954250872 0.2173161060 + 0.2974228859 0.2974228859 0.2190670073 + 0.2994059920 0.2994059920 0.2208126932 + 0.3013804853 0.3013804853 0.2225555927 + 0.3033458889 0.3033458889 0.2242987007 + 0.3052974939 0.3052974939 0.2260434926 + 0.3072321117 0.3072321117 0.2277884930 + 0.3091601133 0.3091601133 0.2295302004 + 0.3110798895 0.3110798895 0.2312685996 + 0.3129976094 0.3129976094 0.2329937071 + 0.3149156868 0.3149156868 0.2347085029 + 0.3168309927 0.3168309927 0.2364155948 + 0.3187418878 0.3187418878 0.2381125987 + 0.3206557930 0.3206557930 0.2397992015 + 0.3225586116 0.3225586116 0.2414820939 + 0.3244569898 0.3244569898 0.2431667000 + 0.3263500929 0.3263500929 0.2448462993 + 0.3282350898 0.3282350898 0.2465201020 + 0.3301092982 0.3301092982 0.2481863946 + 0.3319670856 0.3319670856 0.2498456985 + 0.3338209093 0.3338209093 0.2515039146 + 0.3356688023 0.3356688023 0.2531544864 + 0.3375090063 0.3375090063 0.2548001111 + 0.3393396139 0.3393396139 0.2564423978 + 0.3411692083 0.3411692083 0.2580820024 + 0.3429956138 0.3429956138 0.2597126067 + 0.3448179960 0.3448179960 0.2613438070 + 0.3466367126 0.3466367126 0.2629727125 + 0.3484556079 0.3484556079 0.2645977139 + 0.3502711058 0.3502711058 0.2662194967 + 0.3520781100 0.3520781100 0.2678410113 + 0.3538728952 0.3538728952 0.2694599032 + 0.3556573093 0.3556573093 0.2710745037 + 0.3574326932 0.3574326932 0.2726924121 + 0.3591986001 0.3591986001 0.2743096948 + 0.3609502912 0.3609502912 0.2759279013 + 0.3626965880 0.3626965880 0.2775534987 + 0.3644376099 0.3644376099 0.2791810930 + 0.3661716878 0.3661716878 0.2808074951 + 0.3679068089 0.3679068089 0.2824282944 + 0.3696404099 0.3696404099 0.2840456963 + 0.3713721931 0.3713721931 0.2856619060 + 0.3730970919 0.3730970919 0.2872773111 + 0.3748179972 0.3748179972 0.2888830006 + 0.3765349090 0.3765349090 0.2904838920 + 0.3782503903 0.3782503903 0.2920790017 + 0.3799589872 0.3799589872 0.2936722934 + 0.3816598058 0.3816598058 0.2952640057 + 0.3833526969 0.3833526969 0.2968522906 + 0.3850415051 0.3850415051 0.2984321117 + 0.3867242932 0.3867242932 0.3000073135 + 0.3883965015 0.3883965015 0.3015750051 + 0.3900654912 0.3900654912 0.3031345010 + 0.3917332888 0.3917332888 0.3046869040 + 0.3933978081 0.3933978081 0.3062342107 + 0.3950597942 0.3950597942 0.3077783883 + 0.3967128098 0.3967128098 0.3093160987 + 0.3983623087 0.3983623087 0.3108538985 + 0.4000048041 0.4000048041 0.3123919070 + 0.4016445875 0.4016445875 0.3139241934 + 0.4032827914 0.4032827914 0.3154524863 + 0.4049166143 0.4049166143 0.3169719875 + 0.4065478146 0.4065478146 0.3184897006 + 0.4081723094 0.4081723094 0.3200064003 + 0.4097915888 0.4097915888 0.3215180039 + 0.4114066064 0.4114066064 0.3230280876 + 0.4130186141 0.4130186141 0.3245337009 + 0.4146288931 0.4146288931 0.3260375857 + 0.4162240028 0.4162240028 0.3275386989 + 0.4178164005 0.4178164005 0.3290353119 + 0.4194015861 0.4194015861 0.3305288851 + 0.4209814072 0.4209814072 0.3320241868 + 0.4225557148 0.4225557148 0.3335199058 + 0.4241304100 0.4241304100 0.3350135982 + 0.4257053137 0.4257053137 0.3365071118 + 0.4272772074 0.4272772074 0.3379994035 + 0.4288483858 0.4288483858 0.3394888043 + 0.4304055870 0.4304055870 0.3409718871 + 0.4319624007 0.4319624007 0.3424525857 + 0.4335188866 0.4335188866 0.3439300954 + 0.4350732863 0.4350732863 0.3454062939 + 0.4366205037 0.4366205037 0.3468815088 + 0.4381664991 0.4381664991 0.3483560085 + 0.4397099912 0.4397099912 0.3498300910 + 0.4412516952 0.4412516952 0.3513006866 + 0.4427905977 0.4427905977 0.3527697921 + 0.4443261921 0.4443261921 0.3542422950 + 0.4458572865 0.4458572865 0.3557159901 + 0.4473834038 0.4473834038 0.3571884036 + 0.4489029944 0.4489029944 0.3586604893 + 0.4504176080 0.4504176080 0.3601266146 + 0.4519256949 0.4519256949 0.3615907133 + 0.4534310102 0.4534310102 0.3630562127 + 0.4549323916 0.4549323916 0.3645220101 + 0.4564324915 0.4564324915 0.3659847081 + 0.4579305947 0.4579305947 0.3674460053 + 0.4594289064 0.4594289064 0.3689005077 + 0.4609279037 0.4609279037 0.3703509867 + 0.4624246955 0.4624246955 0.3718009889 + 0.4639141858 0.4639141858 0.3732503951 + 0.4654031098 0.4654031098 0.3746976852 + 0.4668875039 0.4668875039 0.3761422932 + 0.4683718085 0.4683718085 0.3775857091 + 0.4698401988 0.4698401988 0.3790262043 + 0.4713082910 0.4713082910 0.3804647923 + 0.4727697968 0.4727697968 0.3818928897 + 0.4742299914 0.4742299914 0.3833209872 + 0.4756909013 0.4756909013 0.3847407997 + 0.4771521986 0.4771521986 0.3861603141 + 0.4786089063 0.4786089063 0.3875763118 + 0.4800598919 0.4800598919 0.3889910877 + 0.4815098047 0.4815098047 0.3904035985 + 0.4829556048 0.4829556048 0.3918142915 + 0.4844014049 0.4844014049 0.3932228088 + 0.4858314991 0.4858314991 0.3946261108 + 0.4872612059 0.4872612059 0.3960292935 + 0.4886828065 0.4886828065 0.3974260986 + 0.4901008904 0.4901008904 0.3988228142 + 0.4915114045 0.4915114045 0.4002167881 + 0.4929108918 0.4929108918 0.4016098976 + 0.4943102002 0.4943102002 0.4030028880 + 0.4957073927 0.4957073927 0.4043959975 + 0.4971045852 0.4971045852 0.4057886899 + 0.4984934032 0.4984934032 0.4071742892 + 0.4998792112 0.4998792112 0.4085598886 + 0.5012617707 0.5012617707 0.4099406004 + 0.5026388168 0.5026388168 0.4113186896 + 0.5040158033 0.5040158033 0.4126941860 + 0.5053874850 0.5053874850 0.4140625894 + 0.5067589283 0.5067589283 0.4154309928 + 0.5081269145 0.5081269145 0.4167915881 + 0.5094919205 0.5094919205 0.4181506932 + 0.5108559132 0.5108559132 0.4195038080 + 0.5122082829 0.5122082829 0.4208473861 + 0.5135607719 0.5135607719 0.4221909940 + 0.5149065852 0.5149065852 0.4235303998 + 0.5162479877 0.5162479877 0.4248695076 + 0.5175889134 0.5175889134 0.4262058139 + 0.5189247727 0.5189247727 0.4275380969 + 0.5202606916 0.5202606916 0.4288704097 + 0.5215886235 0.5215886235 0.4302046895 + 0.5229104161 0.5229104161 0.4315393865 + 0.5242322087 0.5242322087 0.4328733981 + 0.5255554914 0.5255554914 0.4342060983 + 0.5268787742 0.5268787742 0.4355388880 + 0.5281956792 0.5281956792 0.4368667901 + 0.5295035243 0.5295035243 0.4381929040 + 0.5308111906 0.5308111906 0.4395191073 + 0.5321189761 0.5321189761 0.4408474863 + 0.5334268212 0.5334268212 0.4421758950 + 0.5347332954 0.5347332954 0.4435034096 + 0.5360326171 0.5360326171 0.4448300004 + 0.5373319983 0.5373319983 0.4461565018 + 0.5386298895 0.5386298895 0.4474779069 + 0.5399259925 0.5399259925 0.4487977922 + 0.5412220955 0.5412220955 0.4501172900 + 0.5425089002 0.5425089002 0.4514313042 + 0.5437930822 0.5437930822 0.4527452886 + 0.5450770259 0.5450770259 0.4540582001 + 0.5463545918 0.5463545918 0.4553689063 + 0.5476322174 0.5476322174 0.4566796124 + 0.5489053726 0.5489053726 0.4579856992 + 0.5501675010 0.5501675010 0.4592883885 + 0.5514295101 0.5514295101 0.4605911970 + 0.5526881218 0.5526881218 0.4618827999 + 0.5539432168 0.5539432168 0.4631715119 + 0.5551983714 0.5551983714 0.4644601941 + 0.5564481020 0.5564481020 0.4657382965 + 0.5576952100 0.5576952100 0.4670161009 + 0.5589423180 0.5589423180 0.4682930112 + 0.5601819158 0.5601819158 0.4695635140 + 0.5614199042 0.5614199042 0.4708338976 + 0.5626578927 0.5626578927 0.4721035063 + 0.5638862252 0.5638862252 0.4733701050 + 0.5651137829 0.5651137829 0.4746366143 + 0.5663412213 0.5663412213 0.4759002030 + 0.5675578117 0.5675578117 0.4771572948 + 0.5687745214 0.5687745214 0.4784145057 + 0.5699902177 0.5699902177 0.4796695113 + 0.5711944103 0.5711944103 0.4809209108 + 0.5723987222 0.5723987222 0.4821723998 + 0.5736017227 0.5736017227 0.4834229946 + 0.5747933984 0.5747933984 0.4846724868 + 0.5759850144 0.5759850144 0.4859218895 + 0.5771753788 0.5771753788 0.4871684015 + 0.5783529878 0.5783529878 0.4884099960 + 0.5795305967 0.5795305967 0.4896515906 + 0.5807074904 0.5807074904 0.4908894002 + 0.5818707943 0.5818707943 0.4921191931 + 0.5830342174 0.5830342174 0.4933489859 + 0.5841975808 0.5841975808 0.4945765138 + 0.5853506923 0.5853506923 0.4957973957 + 0.5865036249 0.5865036249 0.4970183074 + 0.5876564980 0.5876564980 0.4982382059 + 0.5888124108 0.5888124108 0.4994524121 + 0.5899686813 0.5899686813 0.5006666183 + 0.5911250710 0.5911250710 0.5018808246 + 0.5922722220 0.5922722220 0.5030846000 + 0.5934156775 0.5934156775 0.5042884946 + 0.5945591927 0.5945591927 0.5054923296 + 0.5956988931 0.5956988931 0.5066840053 + 0.5968354940 0.5968354940 0.5078731179 + 0.5979719758 0.5979719758 0.5090621710 + 0.5991039276 0.5991039276 0.5102484822 + 0.6002264023 0.6002264023 0.5114330053 + 0.6013488770 0.6013488770 0.5126175284 + 0.6024705768 0.6024705768 0.5138002038 + 0.6035845876 0.6035845876 0.5149796009 + 0.6046985984 0.6046985984 0.5161591172 + 0.6058124900 0.6058124900 0.5173382759 + 0.6069167256 0.6069167256 0.5185145736 + 0.6080183983 0.6080183983 0.5196908116 + 0.6091201901 0.6091201901 0.5208671093 + 0.6102225184 0.6102225184 0.5220367908 + 0.6113256216 0.6113256216 0.5232045054 + 0.6124286056 0.6124286056 0.5243722200 + 0.6135300994 0.6135300994 0.5255355239 + 0.6146209836 0.6146209836 0.5266925097 + 0.6157119274 0.6157119274 0.5278496146 + 0.6168028712 0.6168028712 0.5290066004 + 0.6178948283 0.6178948283 0.5301629901 + 0.6189870238 0.6189870238 0.5313194990 + 0.6200792193 0.6200792193 0.5324760079 + 0.6211677194 0.6211677194 0.5336307287 + 0.6222478747 0.6222478747 0.5347841978 + 0.6233282089 0.6233282089 0.5359376073 + 0.6244084239 0.6244084239 0.5370901227 + 0.6254897118 0.6254897118 0.5382356048 + 0.6265712976 0.6265712976 0.5393812060 + 0.6276528239 0.6276528239 0.5405266881 + 0.6287301183 0.6287301183 0.5416650176 + 0.6297997832 0.6297997832 0.5427989960 + 0.6308693886 0.6308693886 0.5439329147 + 0.6319391131 0.6319391131 0.5450661778 + 0.6330032945 0.6330032945 0.5461930037 + 0.6340664029 0.6340664029 0.5473198295 + 0.6351293921 0.6351293921 0.5484464765 + 0.6361892819 0.6361892819 0.5495709777 + 0.6372389197 0.6372389197 0.5506932139 + 0.6382886171 0.6382886171 0.5518153906 + 0.6393381953 0.6393381953 0.5529376864 + 0.6403782964 0.6403782964 0.5540469885 + 0.6414123774 0.6414123774 0.5551546812 + 0.6424465179 0.6424465179 0.5562624931 + 0.6434807181 0.6434807181 0.5573676229 + 0.6445010900 0.6445010900 0.5584642291 + 0.6455209851 0.6455209851 0.5595607758 + 0.6465408802 0.6465408802 0.5606573820 + 0.6475573182 0.6475573182 0.5617504716 + 0.6485638022 0.6485638022 0.5628405809 + 0.6495704055 0.6495704055 0.5639305711 + 0.6505770087 0.6505770087 0.5650206804 + 0.6515771747 0.6515771747 0.5661026239 + 0.6525719166 0.6525719166 0.5671824217 + 0.6535665989 0.6535665989 0.5682622194 + 0.6545614004 0.6545614004 0.5693418980 + 0.6555476785 0.6555476785 0.5704169869 + 0.6565313935 0.6565313935 0.5714920163 + 0.6575151086 0.6575151086 0.5725671053 + 0.6584988236 0.6584988236 0.5736408830 + 0.6594734192 0.6594734192 0.5747092962 + 0.6604474783 0.6604474783 0.5757777095 + 0.6614215970 0.6614215970 0.5768461227 + 0.6623948812 0.6623948812 0.5779128075 + 0.6633601785 0.6633601785 0.5789762139 + 0.6643254757 0.6643254757 0.5800395012 + 0.6652907133 0.6652907133 0.5811027884 + 0.6662545800 0.6662545800 0.5821601152 + 0.6672123075 0.6672123075 0.5832095742 + 0.6681699157 0.6681699157 0.5842590928 + 0.6691275835 0.6691275835 0.5853086114 + 0.6700835824 0.6700835824 0.5863516927 + 0.6710343957 0.6710343957 0.5873885155 + 0.6719852090 0.6719852090 0.5884252787 + 0.6729360223 0.6729360223 0.5894619823 + 0.6738809943 0.6738809943 0.5904929042 + 0.6748093963 0.6748093963 0.5915179849 + 0.6757377982 0.6757377982 0.5925431252 + 0.6766662002 0.6766662002 0.5935683250 + 0.6775926948 0.6775926948 0.5945895910 + 0.6785131097 0.6785131097 0.5956068039 + 0.6794335842 0.6794335842 0.5966240168 + 0.6803539991 0.6803539991 0.5976412296 + 0.6812713742 0.6812713742 0.5986567140 + 0.6821733713 0.6821733713 0.5996695757 + 0.6830754280 0.6830754280 0.6006824970 + 0.6839774251 0.6839774251 0.6016954780 + 0.6848791242 0.6848791242 0.6027052999 + 0.6857768893 0.6857768893 0.6037077904 + 0.6866745949 0.6866745949 0.6047102213 + 0.6875724196 0.6875724196 0.6057127118 + 0.6884701252 0.6884701252 0.6067149043 + 0.6893548965 0.6893548965 0.6077160239 + 0.6902378798 0.6902378798 0.6087170243 + 0.6911209822 0.6911209822 0.6097180247 + 0.6920040250 0.6920040250 0.6107190251 + 0.6928865910 0.6928865910 0.6117115021 + 0.6937689781 0.6937689781 0.6127033234 + 0.6946514249 0.6946514249 0.6136950850 + 0.6955338120 0.6955338120 0.6146869063 + 0.6964113712 0.6964113712 0.6156741977 + 0.6972827911 0.6972827911 0.6166592240 + 0.6981540918 0.6981540918 0.6176441908 + 0.6990255117 0.6990255117 0.6186292171 + 0.6998962164 0.6998962164 0.6196087003 + 0.7007628083 0.7007628083 0.6205787063 + 0.7016295195 0.7016295195 0.6215487123 + 0.7024961114 0.7024961114 0.6225187182 + 0.7033627033 0.7033627033 0.6234884858 + 0.7042225003 0.7042225003 0.6244485974 + 0.7050803900 0.7050803900 0.6254087090 + 0.7059382796 0.7059382796 0.6263688207 + 0.7067962885 0.7067962885 0.6273288727 + 0.7076508999 0.7076508999 0.6282864809 + 0.7085005045 0.7085005045 0.6292427778 + 0.7093501091 0.7093501091 0.6301990747 + 0.7101997137 0.7101997137 0.6311553717 + 0.7110493183 0.7110493183 0.6321104765 + 0.7118933797 0.7118933797 0.6330611706 + 0.7127372026 0.7127372026 0.6340118051 + 0.7135810852 0.7135810852 0.6349623799 + 0.7144250274 0.7144250274 0.6359130740 + 0.7152673006 0.7152673006 0.6368628144 + 0.7161080241 0.7161080241 0.6378120780 + 0.7169486880 0.7169486880 0.6387614012 + 0.7177892923 0.7177892923 0.6397107244 + 0.7186300159 0.7186300159 0.6406584978 + 0.7194681168 0.7194681168 0.6415982842 + 0.7203060985 0.7203060985 0.6425381899 + 0.7211440802 0.7211440802 0.6434779763 + 0.7219821215 0.7219821215 0.6444178820 + 0.7228178978 0.7228178978 0.6453549862 + 0.7236496806 0.7236496806 0.6462903023 + 0.7244815826 0.7244815826 0.6472254992 + 0.7253134847 0.7253134847 0.6481606960 + 0.7261453271 0.7261453271 0.6490960121 + 0.7269656062 0.7269656062 0.6500247121 + 0.7277814746 0.7277814746 0.6509529948 + 0.7285972834 0.7285972834 0.6518813968 + 0.7294130921 0.7294130921 0.6528097987 + 0.7302284837 0.7302284837 0.6537358165 + 0.7310312986 0.7310312986 0.6546552181 + 0.7318341136 0.7318341136 0.6555745006 + 0.7326369286 0.7326369286 0.6564939022 + 0.7334398031 0.7334398031 0.6574133039 + 0.7342393994 0.7342393994 0.6583278179 + 0.7350317240 0.7350317240 0.6592379212 + 0.7358239889 0.7358239889 0.6601479053 + 0.7366163135 0.7366163135 0.6610580087 + 0.7374085188 0.7374085188 0.6619681120 + 0.7381967902 0.7381967902 0.6628733873 + 0.7389813066 0.7389813066 0.6637769938 + 0.7397658229 0.7397658229 0.6646806002 + 0.7405503988 0.7405503988 0.6655843258 + 0.7413349152 0.7413349152 0.6664878726 + 0.7421157956 0.7421157956 0.6673874855 + 0.7428950071 0.7428950071 0.6682866216 + 0.7436742187 0.7436742187 0.6691858172 + 0.7444533706 0.7444533706 0.6700848937 + 0.7452325821 0.7452325821 0.6709840894 + 0.7460039258 0.7460039258 0.6718817949 + 0.7467731833 0.7467731833 0.6727795005 + 0.7475423813 0.7475423813 0.6736772060 + 0.7483115792 0.7483115792 0.6745749712 + 0.7490807772 0.7490807772 0.6754717827 + 0.7498486042 0.7498486042 0.6763551831 + 0.7506161928 0.7506161928 0.6772385240 + 0.7513837814 0.7513837814 0.6781218052 + 0.7521514297 0.7521514297 0.6790052056 + 0.7529190183 0.7529190183 0.6798881292 + 0.7536765933 0.7536765933 0.6807652712 + 0.7544323802 0.7544323802 0.6816424131 + 0.7551882863 0.7551882863 0.6825196147 + 0.7559440732 0.7559440732 0.6833966970 + 0.7566999793 0.7566999793 0.6842737198 + 0.7574527860 0.7574527860 0.6851336956 + 0.7582048178 0.7582048178 0.6859937906 + 0.7589567900 0.7589567900 0.6868538260 + 0.7597088218 0.7597088218 0.6877138019 + 0.7604607940 0.7604607940 0.6885737777 + 0.7612053156 0.7612053156 0.6894273758 + 0.7619463801 0.7619463801 0.6902801991 + 0.7626876235 0.7626876235 0.6911330819 + 0.7634288073 0.7634288073 0.6919859052 + 0.7641698718 0.7641698718 0.6928387284 + 0.7649000883 0.7649000883 0.6936817169 + 0.7656204104 0.7656204104 0.6945211887 + 0.7663406134 0.7663406134 0.6953607202 + 0.7670608759 0.7670608759 0.6962001920 + 0.7677811980 0.7677811980 0.6970396042 + 0.7684996128 0.7684996128 0.6978752017 + 0.7692136168 0.7692136168 0.6987071037 + 0.7699276209 0.7699276209 0.6995388865 + 0.7706416249 0.7706416249 0.7003707886 + 0.7713556290 0.7713556290 0.7012026906 + 0.7720690966 0.7720690966 0.7020341158 + 0.7727692723 0.7727692723 0.7028638721 + 0.7734695077 0.7734695077 0.7036936879 + 0.7741696835 0.7741696835 0.7045235038 + 0.7748699188 0.7748699188 0.7053533792 + 0.7755700946 0.7755700946 0.7061831951 + 0.7762656212 0.7762656212 0.7070057988 + 0.7769591808 0.7769591808 0.7078278065 + 0.7776529193 0.7776529193 0.7086496949 + 0.7783464789 0.7783464789 0.7094715834 + 0.7790400982 0.7790400982 0.7102934718 + 0.7797309756 0.7797309756 0.7111132741 + 0.7804160714 0.7804160714 0.7119309902 + 0.7811012268 0.7811012268 0.7127488256 + 0.7817863226 0.7817863226 0.7135666013 + 0.7824714184 0.7824714184 0.7143843174 + 0.7831565142 0.7831565142 0.7152014971 + 0.7838354707 0.7838354707 0.7160099149 + 0.7845135927 0.7845135927 0.7168182135 + 0.7851915956 0.7851915956 0.7176265717 + 0.7858697176 0.7858697176 0.7184348702 + 0.7865477800 0.7865477800 0.7192432880 + 0.7872189879 0.7872189879 0.7200449109 + 0.7878777981 0.7878777981 0.7208411098 + 0.7885367274 0.7885367274 0.7216373086 + 0.7891955972 0.7891955972 0.7224333882 + 0.7898544073 0.7898544073 0.7232295871 + 0.7905132771 0.7905132771 0.7240257859 + 0.7911655903 0.7911655903 0.7248097062 + 0.7918161750 0.7918161750 0.7255933881 + 0.7924668193 0.7924668193 0.7263771296 + 0.7931174040 0.7931174040 0.7271608114 + 0.7937679887 0.7937679887 0.7279444933 + 0.7944167256 0.7944167256 0.7287240028 + 0.7950556278 0.7950556278 0.7294961214 + 0.7956945896 0.7956945896 0.7302681208 + 0.7963336110 0.7963336110 0.7310401797 + 0.7969725728 0.7969725728 0.7318121791 + 0.7976115942 0.7976115942 0.7325842977 + 0.7982401848 0.7982401848 0.7333511114 + 0.7988576889 0.7988576889 0.7341153026 + 0.7994753122 0.7994753122 0.7348794937 + 0.8000928164 0.8000928164 0.7356436849 + 0.8007103205 0.8007103205 0.7364078760 + 0.8013278246 0.8013278246 0.7371721268 + 0.8019409180 0.8019409180 0.7379267216 + 0.8025524020 0.8025524020 0.7386804819 + 0.8031638861 0.8031638861 0.7394343019 + 0.8037754297 0.8037754297 0.7401881218 + 0.8043869138 0.8043869138 0.7409418225 + 0.8049983978 0.8049983978 0.7416955233 + 0.8055992126 0.8055992126 0.7424479723 + 0.8061988950 0.8061988950 0.7432004809 + 0.8067986965 0.8067986965 0.7439529896 + 0.8073984981 0.8073984981 0.7447054982 + 0.8079982996 0.8079982996 0.7454580069 + 0.8085979223 0.8085979223 0.7462081909 + 0.8091943860 0.8091943860 0.7469503880 + 0.8097909093 0.8097909093 0.7476925850 + 0.8103873134 0.8103873134 0.7484347820 + 0.8109837770 0.8109837770 0.7491769791 + 0.8115803003 0.8115803003 0.7499191761 + 0.8121759295 0.8121759295 0.7506610751 + 0.8127642274 0.8127642274 0.7514020801 + 0.8133525252 0.8133525252 0.7521430850 + 0.8139408231 0.8139408231 0.7528840899 + 0.8145290017 0.8145290017 0.7536250949 + 0.8151172996 0.8151172996 0.7543660998 + 0.8157039881 0.8157039881 0.7551041842 + 0.8162763119 0.8162763119 0.7558351159 + 0.8168486953 0.8168486953 0.7565659285 + 0.8174210787 0.8174210787 0.7572966814 + 0.8179934025 0.8179934025 0.7580274940 + 0.8185657859 0.8185657859 0.7587584257 + 0.8191381097 0.8191381097 0.7594878078 + 0.8197093010 0.8197093010 0.7602121234 + 0.8202804923 0.8202804923 0.7609363794 + 0.8208516836 0.8208516836 0.7616605759 + 0.8214228749 0.8214228749 0.7623848915 + 0.8219941258 0.8219941258 0.7631092072 + 0.8225653172 0.8225653172 0.7638326287 + 0.8231304884 0.8231304884 0.7645456791 + 0.8236950040 0.8236950040 0.7652587891 + 0.8242594004 0.8242594004 0.7659717798 + 0.8248239160 0.8248239160 0.7666848898 + 0.8253883719 0.8253883719 0.7673979998 + 0.8259528875 0.8259528875 0.7681111097 + 0.8265138268 0.8265138268 0.7688196898 + 0.8270732164 0.8270732164 0.7695276737 + 0.8276326060 0.8276326060 0.7702357173 + 0.8281919956 0.8281919956 0.7709437013 + 0.8287513852 0.8287513852 0.7716516852 + 0.8293107748 0.8293107748 0.7723596096 + 0.8298665881 0.8298665881 0.7730653286 + 0.8304176927 0.8304176927 0.7737694979 + 0.8309687972 0.8309687972 0.7744737267 + 0.8315199018 0.8315199018 0.7751778960 + 0.8320710063 0.8320710063 0.7758821249 + 0.8326221108 0.8326221108 0.7765862942 + 0.8331729174 0.8331729174 0.7772877216 + 0.8337209225 0.8337209225 0.7779809833 + 0.8342689276 0.8342689276 0.7786744237 + 0.8348168135 0.8348168135 0.7793678045 + 0.8353648186 0.8353648186 0.7800611854 + 0.8359127045 0.8359127045 0.7807545066 + 0.8364607096 0.8364607096 0.7814478874 + 0.8370041251 0.8370041251 0.7821326256 + 0.8375453949 0.8375453949 0.7828158140 + 0.8380867243 0.8380867243 0.7834990025 + 0.8386281133 0.8386281133 0.7841823101 + 0.8391693830 0.8391693830 0.7848654985 + 0.8397107124 0.8397107124 0.7855486870 + 0.8402513862 0.8402513862 0.7862282991 + 0.8407893777 0.8407893777 0.7869021297 + 0.8413274288 0.8413274288 0.7875759006 + 0.8418654799 0.8418654799 0.7882496119 + 0.8424034715 0.8424034715 0.7889233828 + 0.8429415226 0.8429415226 0.7895972133 + 0.8434795737 0.8434795737 0.7902709246 + 0.8440154791 0.8440154791 0.7909373045 + 0.8445503712 0.8445503712 0.7916023135 + 0.8450853229 0.8450853229 0.7922673225 + 0.8456202149 0.8456202149 0.7929322124 + 0.8461551070 0.8461551070 0.7935972214 + 0.8466899991 0.8466899991 0.7942621112 + 0.8472244143 0.8472244143 0.7949252725 + 0.8477507234 0.8477507234 0.7955821753 + 0.8482769728 0.8482769728 0.7962390184 + 0.8488032818 0.8488032818 0.7968958020 + 0.8493295908 0.8493295908 0.7975527048 + 0.8498557806 0.8498557806 0.7982094884 + 0.8503820896 0.8503820896 0.7988663912 + 0.8509023786 0.8509023786 0.7995193005 + 0.8514125943 0.8514125943 0.8001686931 + 0.8519228101 0.8519228101 0.8008180857 + 0.8524330258 0.8524330258 0.8014674783 + 0.8529431224 0.8529431224 0.8021169901 + 0.8534532785 0.8534532785 0.8027663827 + 0.8539634943 0.8539634943 0.8034157753 + 0.8544731736 0.8544731736 0.8040614128 + 0.8549826145 0.8549826145 0.8047059178 + 0.8554919958 0.8554919958 0.8053503036 + 0.8560013175 0.8560013175 0.8059946895 + 0.8565106988 0.8565106988 0.8066391945 + 0.8570200801 0.8570200801 0.8072835803 + 0.8575295210 0.8575295210 0.8079280257 + 0.8580337167 0.8580337167 0.8085677028 + 0.8585366011 0.8585366011 0.8092070818 + 0.8590394258 0.8590394258 0.8098465204 + 0.8595423102 0.8595423102 0.8104857802 + 0.8600450754 0.8600450754 0.8111252189 + 0.8605480194 0.8605480194 0.8117644787 + 0.8610509038 0.8610509038 0.8124037981 + 0.8615514040 0.8615514040 0.8130391240 + 0.8620514870 0.8620514870 0.8136743903 + 0.8625516891 0.8625516891 0.8143097162 + 0.8630518913 0.8630518913 0.8149451017 + 0.8635520935 0.8635520935 0.8155804276 + 0.8640522957 0.8640522957 0.8162156940 + 0.8645523787 0.8645523787 0.8168507218 + 0.8650534749 0.8650534749 0.8174775243 + 0.8655548096 0.8655548096 0.8181043863 + 0.8660560250 0.8660560250 0.8187311888 + 0.8665571809 0.8665571809 0.8193581104 + 0.8670583963 0.8670583963 0.8199849129 + 0.8675596714 0.8675596714 0.8206117749 + 0.8680608869 0.8680608869 0.8212385774 + 0.8685551882 0.8685551882 0.8218612075 + 0.8690482974 0.8690482974 0.8224837184 + 0.8695412874 0.8695412874 0.8231062293 + 0.8700343966 0.8700343966 0.8237286210 + 0.8705273867 0.8705273867 0.8243510723 + 0.8710203767 0.8710203767 0.8249735832 + 0.8715134859 0.8715134859 0.8255960941 + 0.8719956875 0.8719956875 0.8262088895 + 0.8724732995 0.8724732995 0.8268200159 + 0.8729509711 0.8729509711 0.8274310827 + 0.8734285831 0.8734285831 0.8280420899 + 0.8739063144 0.8739063144 0.8286532164 + 0.8743839264 0.8743839264 0.8292642832 + 0.8748615980 0.8748615980 0.8298754096 + 0.8753389120 0.8753389120 0.8304854035 + 0.8758158088 0.8758158088 0.8310946822 + 0.8762927055 0.8762927055 0.8317040801 + 0.8767696023 0.8767696023 0.8323134780 + 0.8772464991 0.8772464991 0.8329228163 + 0.8777233958 0.8777233958 0.8335322142 + 0.8782002926 0.8782002926 0.8341416121 + 0.8786755800 0.8786755800 0.8347480297 + 0.8791444898 0.8791444898 0.8353486061 + 0.8796133995 0.8796133995 0.8359493017 + 0.8800823092 0.8800823092 0.8365498781 + 0.8805512190 0.8805512190 0.8371505737 + 0.8810201287 0.8810201287 0.8377512097 + 0.8814889789 0.8814889789 0.8383519053 + 0.8819578886 0.8819578886 0.8389524817 + 0.8824142218 0.8824142218 0.8395484090 + 0.8828679919 0.8828679919 0.8401440978 + 0.8833217025 0.8833217025 0.8407397866 + 0.8837754726 0.8837754726 0.8413354158 + 0.8842293024 0.8842293024 0.8419311047 + 0.8846830726 0.8846830726 0.8425267935 + 0.8851369023 0.8851369023 0.8431224823 + 0.8855882287 0.8855882287 0.8437144756 + 0.8860356212 0.8860356212 0.8443030119 + 0.8864830136 0.8864830136 0.8448914886 + 0.8869304061 0.8869304061 0.8454800844 + 0.8873776793 0.8873776793 0.8460686207 + 0.8878250718 0.8878250718 0.8466570973 + 0.8882725239 0.8882725239 0.8472455740 + 0.8887199163 0.8887199163 0.8478341103 + 0.8891612291 0.8891612291 0.8484159112 + 0.8896014094 0.8896014094 0.8489974737 + 0.8900417089 0.8900417089 0.8495792150 + 0.8904820085 0.8904820085 0.8501607776 + 0.8909221888 0.8909221888 0.8507425189 + 0.8913624883 0.8913624883 0.8513240814 + 0.8918027282 0.8918027282 0.8519057035 + 0.8922411799 0.8922411799 0.8524854779 + 0.8926743269 0.8926743269 0.8530620933 + 0.8931074142 0.8931074142 0.8536388278 + 0.8935403824 0.8935403824 0.8542153835 + 0.8939735293 0.8939735293 0.8547919989 + 0.8944066167 0.8944066167 0.8553686142 + 0.8948397040 0.8948397040 0.8559452891 + 0.8952727914 0.8952727914 0.8565219045 + 0.8957030177 0.8957030177 0.8570963740 + 0.8961309195 0.8961309195 0.8576697707 + 0.8965588212 0.8965588212 0.8582432866 + 0.8969867229 0.8969867229 0.8588168025 + 0.8974146843 0.8974146843 0.8593901992 + 0.8978425860 0.8978425860 0.8599637151 + 0.8982704878 0.8982704878 0.8605371714 + 0.8986983895 0.8986983895 0.8611106873 + 0.8991193175 0.8991193175 0.8616771102 + 0.8995382190 0.8995382190 0.8622428179 + 0.8999571204 0.8999571204 0.8628084064 + 0.9003760219 0.9003760219 0.8633741140 + 0.9007949233 0.9007949233 0.8639398217 + 0.9012138247 0.9012138247 0.8645054102 + 0.9016327262 0.9016327262 0.8650711179 + 0.9020516276 0.9020516276 0.8656365275 + 0.9024695158 0.9024695158 0.8661962152 + 0.9028872848 0.9028872848 0.8667557836 + 0.9033051729 0.9033051729 0.8673154116 + 0.9037230015 0.9037230015 0.8678750992 + 0.9041407704 0.9041407704 0.8684347272 + 0.9045585990 0.9045585990 0.8689942956 + 0.9049764872 0.9049764872 0.8695539236 + 0.9053943157 0.9053943157 0.8701127172 + 0.9058037996 0.9058037996 0.8706625104 + 0.9062129855 0.9062129855 0.8712123036 + 0.9066222906 0.9066222906 0.8717620969 + 0.9070315957 0.9070315957 0.8723118901 + 0.9074407816 0.9074407816 0.8728616834 + 0.9078500867 0.9078500867 0.8734114170 + 0.9082593918 0.9082593918 0.8739612103 + 0.9086686969 0.9086686969 0.8745108247 + 0.9090744257 0.9090744257 0.8750563860 + 0.9094799757 0.9094799757 0.8756021261 + 0.9098855853 0.9098855853 0.8761476874 + 0.9102911949 0.9102911949 0.8766933084 + 0.9106968045 0.9106968045 0.8772389293 + 0.9111024141 0.9111024141 0.8777846098 + 0.9115080237 0.9115080237 0.8783301711 + 0.9119135737 0.9119135737 0.8788757920 + 0.9123172760 0.9123172760 0.8794146180 + 0.9127206802 0.9127206802 0.8799529076 + 0.9131240845 0.9131240845 0.8804911971 + 0.9135274291 0.9135274291 0.8810294867 + 0.9139307737 0.9139307737 0.8815677762 + 0.9143341780 0.9143341780 0.8821061254 + 0.9147375822 0.9147375822 0.8826442957 + 0.9151409268 0.9151409268 0.8831825852 + 0.9155405164 0.9155405164 0.8837168813 + 0.9159377217 0.9159377217 0.8842496276 + 0.9163349271 0.9163349271 0.8847821951 + 0.9167321920 0.9167321920 0.8853148818 + 0.9171293974 0.9171293974 0.8858476281 + 0.9175266027 0.9175266027 0.8863803148 + 0.9179238081 0.9179238081 0.8869130015 + 0.9183210135 0.9183210135 0.8874456882 + 0.9187163711 0.9187163711 0.8879768252 + 0.9191077948 0.9191077948 0.8885056973 + 0.9194992185 0.9194992185 0.8890346885 + 0.9198907018 0.9198907018 0.8895636201 + 0.9202821255 0.9202821255 0.8900926113 + 0.9206734896 0.9206734896 0.8906214833 + 0.9210649133 0.9210649133 0.8911504745 + 0.9214563966 0.9214563966 0.8916794062 + 0.9218478203 0.9218478203 0.8922079802 + 0.9222431183 0.9222431183 0.8927248120 + 0.9226387143 0.9226387143 0.8932415843 + 0.9230343103 0.9230343103 0.8937584162 + 0.9234299064 0.9234299064 0.8942751288 + 0.9238255024 0.9238255024 0.8947919011 + 0.9242212176 0.9242212176 0.8953086734 + 0.9246168137 0.9246168137 0.8958255053 + 0.9250124097 0.9250124097 0.8963422179 + 0.9254066944 0.9254066944 0.8968577981 + 0.9257994294 0.9257994294 0.8973724246 + 0.9261922240 0.9261922240 0.8978869915 + 0.9265850186 0.9265850186 0.8984016776 + 0.9269778132 0.9269778132 0.8989163041 + 0.9273704886 0.9273704886 0.8994308710 + 0.9277632833 0.9277632833 0.8999454975 + 0.9281560779 0.9281560779 0.9004601240 + 0.9285488129 0.9285488129 0.9009746909 + 0.9289314151 0.9289314151 0.9014821053 + 0.9293128252 0.9293128252 0.9019892216 + 0.9296941161 0.9296941161 0.9024963975 + 0.9300754070 0.9300754070 0.9030035138 + 0.9304568172 0.9304568172 0.9035106897 + 0.9308381081 0.9308381081 0.9040178061 + 0.9312195182 0.9312195182 0.9045249820 + 0.9316008091 0.9316008091 0.9050320983 + 0.9319807887 0.9319807887 0.9055399895 + 0.9323564172 0.9323564172 0.9060496092 + 0.9327319860 0.9327319860 0.9065591097 + 0.9331076145 0.9331076145 0.9070687294 + 0.9334831834 0.9334831834 0.9075781703 + 0.9338588119 0.9338588119 0.9080877900 + 0.9342343807 0.9342343807 0.9085972905 + 0.9346100092 0.9346100092 0.9091069102 + 0.9349855781 0.9349855781 0.9096164107 + 0.9353610277 0.9353610277 0.9101235867 + 0.9357361794 0.9357361794 0.9106287956 + 0.9361115098 0.9361115098 0.9111341238 + 0.9364867210 0.9364867210 0.9116392732 + 0.9368619919 0.9368619919 0.9121444821 + 0.9372372031 0.9372372031 0.9126498103 + 0.9376124740 0.9376124740 0.9131550193 + 0.9379876852 0.9379876852 0.9136602879 + 0.9383630157 0.9383630157 0.9141654968 + 0.9387336969 0.9387336969 0.9146683216 + 0.9391016960 0.9391016960 0.9151700735 + 0.9394698143 0.9394698143 0.9156718850 + 0.9398378134 0.9398378134 0.9161736965 + 0.9402058125 0.9402058125 0.9166755080 + 0.9405738115 0.9405738115 0.9171772003 + 0.9409418106 0.9409418106 0.9176790118 + 0.9413098097 0.9413098097 0.9181808233 + 0.9416779280 0.9416779280 0.9186825752 + 0.9420405030 0.9420405030 0.9191823006 + 0.9424005747 0.9424005747 0.9196813107 + 0.9427607059 0.9427607059 0.9201803803 + 0.9431207180 0.9431207180 0.9206793904 + 0.9434807897 0.9434807897 0.9211784005 + 0.9438409209 0.9438409209 0.9216774106 + 0.9442009926 0.9442009926 0.9221764803 + 0.9445611238 0.9445611238 0.9226754904 + 0.9449210763 0.9449210763 0.9231745005 + 0.9452818036 0.9452818036 0.9236670136 + 0.9456428289 0.9456428289 0.9241566062 + 0.9460036755 0.9460036755 0.9246463180 + 0.9463647008 0.9463647008 0.9251359105 + 0.9467257261 0.9467257261 0.9256255031 + 0.9470866919 0.9470866919 0.9261152148 + 0.9474475980 0.9474475980 0.9266048074 + 0.9478086233 0.9478086233 0.9270945191 + 0.9481695890 0.9481695890 0.9275841117 + 0.9485284090 0.9485284090 0.9280738831 + 0.9488850236 0.9488850236 0.9285637140 + 0.9492415786 0.9492415786 0.9290536046 + 0.9495981932 0.9495981932 0.9295433760 + 0.9499548078 0.9499548078 0.9300333261 + 0.9503114223 0.9503114223 0.9305232167 + 0.9506679773 0.9506679773 0.9310129881 + 0.9510245919 0.9510245919 0.9315028787 + 0.9513812065 0.9513812065 0.9319928288 + 0.9517353773 0.9517353773 0.9324811101 + 0.9520826936 0.9520826936 0.9329659939 + 0.9524298906 0.9524298906 0.9334508777 + 0.9527770877 0.9527770877 0.9339358211 + 0.9531244040 0.9531244040 0.9344208241 + 0.9534716010 0.9534716010 0.9349057078 + 0.9538187981 0.9538187981 0.9353905916 + 0.9541659951 0.9541659951 0.9358754754 + 0.9545133114 0.9545133114 0.9363604784 + 0.9548605084 0.9548605084 0.9368454218 + 0.9552074075 0.9552074075 0.9373251796 + 0.9555541873 0.9555541873 0.9378048778 + 0.9559010267 0.9559010267 0.9382845759 + 0.9562478065 0.9562478065 0.9387642145 + 0.9565945864 0.9565945864 0.9392439127 + 0.9569414258 0.9569414258 0.9397236109 + 0.9572882056 0.9572882056 0.9402033091 + 0.9576349854 0.9576349854 0.9406828880 + 0.9579818249 0.9579818249 0.9411625862 + 0.9583277106 0.9583277106 0.9416403770 + 0.9586725235 0.9586725235 0.9421163201 + 0.9590172172 0.9590172172 0.9425923228 + 0.9593619704 0.9593619704 0.9430683255 + 0.9597067237 0.9597067237 0.9435443282 + 0.9600514770 0.9600514770 0.9440202713 + 0.9603962898 0.9603962898 0.9444962144 + 0.9607409835 0.9607409835 0.9449722171 + 0.9610857964 0.9610857964 0.9454482198 + 0.9614306092 0.9614306092 0.9459242225 + 0.9617655277 0.9617655277 0.9463955164 + 0.9620996714 0.9620996714 0.9468665719 + 0.9624338150 0.9624338150 0.9473378062 + 0.9627678990 0.9627678990 0.9478089213 + 0.9631021023 0.9631021023 0.9482800961 + 0.9634361863 0.9634361863 0.9487512112 + 0.9637703896 0.9637703896 0.9492223859 + 0.9641044736 0.9641044736 0.9496935010 + 0.9644386172 0.9644386172 0.9501646757 + 0.9647706747 0.9647706747 0.9506316185 + 0.9650973082 0.9650973082 0.9510896206 + 0.9654238820 0.9654238820 0.9515476823 + 0.9657505155 0.9657505155 0.9520056844 + 0.9660770893 0.9660770893 0.9524638057 + 0.9664037228 0.9664037228 0.9529218078 + 0.9667302966 0.9667302966 0.9533798099 + 0.9670568705 0.9670568705 0.9538378716 + 0.9673835039 0.9673835039 0.9542958736 + 0.9677100778 0.9677100778 0.9547539949 + 0.9680343866 0.9680343866 0.9552072287 + 0.9683567286 0.9683567286 0.9556567073 + 0.9686790109 0.9686790109 0.9561061859 + 0.9690012932 0.9690012932 0.9565556049 + 0.9693235159 0.9693235159 0.9570050836 + 0.9696457982 0.9696457982 0.9574546218 + 0.9699680805 0.9699680805 0.9579041004 + 0.9702903032 0.9702903032 0.9583535790 + 0.9706125855 0.9706125855 0.9588031173 + 0.9709349275 0.9709349275 0.9592525959 + 0.9712525010 0.9712525010 0.9596999288 + 0.9715678096 0.9715678096 0.9601463079 + 0.9718831182 0.9718831182 0.9605926275 + 0.9721983075 0.9721983075 0.9610390067 + 0.9725136161 0.9725136161 0.9614853859 + 0.9728289247 0.9728289247 0.9619318247 + 0.9731441736 0.9731441736 0.9623782039 + 0.9734594226 0.9734594226 0.9628245234 + 0.9737746716 0.9737746716 0.9632709026 + 0.9740899801 0.9740899801 0.9637172818 + 0.9744006991 0.9744006991 0.9641577005 + 0.9747095108 0.9747095108 0.9645959735 + 0.9750183225 0.9750183225 0.9650343060 + 0.9753270745 0.9753270745 0.9654725194 + 0.9756358266 0.9756358266 0.9659107924 + 0.9759445786 0.9759445786 0.9663491249 + 0.9762533903 0.9762533903 0.9667872787 + 0.9765622020 0.9765622020 0.9672256112 + 0.9768708944 0.9768708944 0.9676638246 + 0.9771797061 0.9771797061 0.9681020975 + 0.9774832129 0.9774832129 0.9685348868 + 0.9777835011 0.9777835011 0.9689648747 + 0.9780837893 0.9780837893 0.9693949223 + 0.9783840775 0.9783840775 0.9698249102 + 0.9786844254 0.9786844254 0.9702548981 + 0.9789847136 0.9789847136 0.9706848264 + 0.9792850018 0.9792850018 0.9711148143 + 0.9795852900 0.9795852900 0.9715448022 + 0.9798855782 0.9798855782 0.9719747901 + 0.9801859260 0.9801859260 0.9724047780 + 0.9804825187 0.9804825187 0.9728298187 + 0.9807745814 0.9807745814 0.9732491970 + 0.9810665846 0.9810665846 0.9736686945 + 0.9813587070 0.9813587070 0.9740880728 + 0.9816508293 0.9816508293 0.9745075107 + 0.9819427729 0.9819427729 0.9749270082 + 0.9822348952 0.9822348952 0.9753463864 + 0.9825268984 0.9825268984 0.9757658243 + 0.9828190207 0.9828190207 0.9761853218 + 0.9831110239 0.9831110239 0.9766047001 + 0.9834020734 0.9834020734 0.9770221114 + 0.9836878181 0.9836878181 0.9774296284 + 0.9839735031 0.9839735031 0.9778370857 + 0.9842591882 0.9842591882 0.9782447219 + 0.9845448732 0.9845448732 0.9786521792 + 0.9848306179 0.9848306179 0.9790596962 + 0.9851163030 0.9851163030 0.9794672132 + 0.9854019880 0.9854019880 0.9798747897 + 0.9856876135 0.9856876135 0.9802823067 + 0.9859732985 0.9859732985 0.9806898236 + 0.9862589836 0.9862589836 0.9810972810 + 0.9865384102 0.9865384102 0.9814953208 + 0.9868156910 0.9868156910 0.9818902016 + 0.9870929122 0.9870929122 0.9822852015 + 0.9873701930 0.9873701930 0.9826802015 + 0.9876474142 0.9876474142 0.9830750823 + 0.9879245758 0.9879245758 0.9834700823 + 0.9882019162 0.9882019162 0.9838650823 + 0.9884790778 0.9884790778 0.9842600226 + 0.9887564182 0.9887564182 0.9846550226 + 0.9890335798 0.9890335798 0.9850500226 + 0.9893082976 0.9893082976 0.9854415059 + 0.9895743132 0.9895743132 0.9858223200 + 0.9898403287 0.9898403287 0.9862031937 + 0.9901062846 0.9901062846 0.9865840077 + 0.9903723001 0.9903723001 0.9869648218 + 0.9906383157 0.9906383157 0.9873455763 + 0.9909042120 0.9909042120 0.9877265096 + 0.9911702275 0.9911702275 0.9881073236 + 0.9914361835 0.9914361835 0.9884880781 + 0.9917021990 0.9917021990 0.9888688922 + 0.9919682145 0.9919682145 0.9892497063 + 0.9922304153 0.9922304153 0.9896146059 + 0.9924901128 0.9924901128 0.9899690151 + 0.9927496910 0.9927496910 0.9903233051 + 0.9930093288 0.9930093288 0.9906777143 + 0.9932690263 0.9932690263 0.9910321236 + 0.9935286045 0.9935286045 0.9913864136 + 0.9937881827 0.9937881827 0.9917408228 + 0.9940478206 0.9940478206 0.9920951724 + 0.9943075180 0.9943075180 0.9924495220 + 0.9945670962 0.9945670962 0.9928038716 + 0.9948266745 0.9948266745 0.9931582808 + 0.9950770140 0.9950770140 0.9934852719 + 0.9953256249 0.9953256249 0.9938076735 + 0.9955741167 0.9955741167 0.9941300750 + 0.9958227277 0.9958227277 0.9944524169 + 0.9960712194 0.9960712194 0.9947748184 + 0.9963197708 0.9963197708 0.9950972199 + 0.9965683222 0.9965683222 0.9954195023 + 0.9968168736 0.9968168736 0.9957419038 + 0.9970654249 0.9970654249 0.9960643053 + 0.9973139763 0.9973139763 0.9963865876 + 0.9975625277 0.9975625277 0.9967089891 + 0.9977846146 0.9977846146 0.9970085025 + 0.9980061054 0.9980061054 0.9973075986 + 0.9982277155 0.9982277155 0.9976068139 + 0.9984492064 0.9984492064 0.9979059100 + 0.9986708164 0.9986708164 0.9982051253 + 0.9988923073 0.9988923073 0.9985042214 + 0.9991137981 0.9991137981 0.9988033772 + 0.9993354082 0.9993354082 0.9991024733 + 0.9995568991 0.9995568991 0.9994016886 + 0.9997785091 0.9997785091 0.9997007847 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_gold_200.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_gold_200.spi1d index 7681128f..d741362c 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_gold_200.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_gold_200.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0069576460 0.0052925618 0.0033476830 - 0.0141364904 0.0107551003 0.0067514139 - 0.0207183994 0.0159495007 0.0101133399 - 0.0267989002 0.0208614096 0.0133742504 - 0.0324718915 0.0254526306 0.0165257901 - 0.0378164984 0.0299201701 0.0196132604 - 0.0428869314 0.0341088213 0.0225949902 - 0.0477258898 0.0381722189 0.0255131796 - 0.0523908511 0.0420860387 0.0283894595 - 0.0568610094 0.0458872616 0.0311939605 - 0.0611275584 0.0495900512 0.0339626707 - 0.0653016865 0.0531945191 0.0367048383 - 0.0693744496 0.0567364991 0.0393822007 - 0.0733573064 0.0602095909 0.0420081317 - 0.0771774426 0.0635916665 0.0445935093 - 0.0809165314 0.0668876693 0.0471374691 - 0.0845568478 0.0701269805 0.0496489182 - 0.0881353170 0.0733123422 0.0521427989 - 0.0916419923 0.0764425769 0.0546066016 - 0.0950766504 0.0795146525 0.0570084900 - 0.0984379798 0.0825331509 0.0593760982 - 0.1017495021 0.0855173692 0.0617147498 - 0.1049598977 0.0884445384 0.0640344396 - 0.1081260964 0.0913117006 0.0663272068 - 0.1112463027 0.0941295624 0.0686026216 - 0.1143177003 0.0968901590 0.0708457306 - 0.1173487976 0.0996170789 0.0730642602 - 0.1203667000 0.1023124009 0.0752720088 - 0.1233123019 0.1049759015 0.0774757266 - 0.1262118965 0.1076003984 0.0796506926 - 0.1290674955 0.1101882979 0.0817981884 - 0.1318753064 0.1127377972 0.0839270800 - 0.1346521974 0.1152800992 0.0860472471 - 0.1373953074 0.1178082004 0.0881496072 - 0.1401219070 0.1203086972 0.0902255625 - 0.1428176016 0.1227945015 0.0922822729 - 0.1454751045 0.1252799928 0.0943331197 - 0.1481045038 0.1277426928 0.0963695124 - 0.1507305056 0.1301932037 0.0983908027 - 0.1533288956 0.1326095015 0.1003959030 - 0.1558901072 0.1350027025 0.1023878008 - 0.1584240943 0.1373652965 0.1043625996 - 0.1609431952 0.1397027969 0.1063340977 - 0.1634577960 0.1420170069 0.1083092019 - 0.1659422070 0.1442991942 0.1102795005 - 0.1683879048 0.1465636939 0.1122395992 - 0.1708077043 0.1488011926 0.1141989976 - 0.1732032001 0.1510327011 0.1161565036 - 0.1755875945 0.1532423943 0.1181034967 - 0.1779661030 0.1554349959 0.1200399995 - 0.1803372949 0.1576205045 0.1219606996 - 0.1826853007 0.1597833931 0.1238709986 - 0.1850018948 0.1619350016 0.1257669926 - 0.1873046011 0.1640775949 0.1276451051 - 0.1896051019 0.1661971062 0.1295011044 - 0.1918939054 0.1682944000 0.1313541979 - 0.1941768974 0.1703854054 0.1332028955 - 0.1964391023 0.1724753976 0.1350436956 - 0.1986718029 0.1745598018 0.1368709952 - 0.2008887976 0.1766363978 0.1386903971 - 0.2030901015 0.1787001044 0.1405090988 - 0.2052837014 0.1807560027 0.1423261017 - 0.2074694037 0.1827982068 0.1441355944 - 0.2096378058 0.1848137975 0.1459414959 - 0.2117996961 0.1868076026 0.1477414072 - 0.2139513046 0.1887951046 0.1495409012 - 0.2160806060 0.1907595992 0.1513372064 - 0.2181894034 0.1927050054 0.1531285048 - 0.2202865928 0.1946384013 0.1549127996 - 0.2223830074 0.1965693980 0.1566943973 - 0.2244744003 0.1984919012 0.1584592015 - 0.2265536934 0.2003978044 0.1602202058 - 0.2286141068 0.2022926956 0.1619769037 - 0.2306680977 0.2041870058 0.1637236029 - 0.2327179015 0.2060835958 0.1654655933 - 0.2347580940 0.2079713047 0.1672006994 - 0.2367894948 0.2098497003 0.1689288020 - 0.2388225049 0.2117146999 0.1706545055 - 0.2408446074 0.2135720998 0.1723743975 - 0.2428611964 0.2154268026 0.1740881950 - 0.2448751032 0.2172740996 0.1757902950 - 0.2468796968 0.2191029936 0.1774809957 - 0.2488763928 0.2209150046 0.1791640073 - 0.2508633137 0.2227170020 0.1808429956 - 0.2528334856 0.2245123982 0.1825204939 - 0.2547827065 0.2263000011 0.1841987073 - 0.2567098141 0.2280700058 0.1858744025 - 0.2586239874 0.2298292071 0.1875447035 - 0.2605265081 0.2315824926 0.1892118007 - 0.2624250948 0.2333337963 0.1908793002 - 0.2643153071 0.2350836992 0.1925469935 - 0.2661927938 0.2368271947 0.1942107975 - 0.2680534124 0.2385641932 0.1958703995 - 0.2698996067 0.2402985990 0.1975174993 - 0.2717376053 0.2420333028 0.1991517991 - 0.2735638916 0.2437696010 0.2007769942 - 0.2753801942 0.2455032021 0.2023956031 - 0.2771906853 0.2472324073 0.2040095031 - 0.2789969146 0.2489607930 0.2056213021 - 0.2808091938 0.2506855130 0.2072338015 - 0.2826234102 0.2524011135 0.2088443041 - 0.2844350040 0.2541044950 0.2104482055 - 0.2862409055 0.2558018863 0.2120479047 - 0.2880494893 0.2574988008 0.2136453986 - 0.2898606062 0.2591859102 0.2152310014 - 0.2916708887 0.2608619034 0.2168077976 - 0.2934772968 0.2625282109 0.2183786035 - 0.2952775061 0.2641865015 0.2199451029 - 0.2970677912 0.2658401132 0.2215090990 - 0.2988429070 0.2674838006 0.2230720073 - 0.3006018996 0.2691144943 0.2246360928 - 0.3023515940 0.2707369030 0.2262039930 - 0.3040930927 0.2723526061 0.2277673930 - 0.3058268130 0.2739633918 0.2293279022 - 0.3075599968 0.2755722106 0.2308869064 - 0.3092887998 0.2771736085 0.2324458063 - 0.3110117018 0.2787660062 0.2339975983 - 0.3127267957 0.2803517878 0.2355509996 - 0.3144324124 0.2819328010 0.2371080071 - 0.3161234856 0.2835094035 0.2386678010 - 0.3178110123 0.2850857973 0.2402258068 - 0.3194932044 0.2866651118 0.2417753041 - 0.3211686909 0.2882451117 0.2433187068 - 0.3228329122 0.2898192108 0.2448568046 - 0.3244825006 0.2913886905 0.2463894933 - 0.3261260092 0.2929497957 0.2479178011 - 0.3277656138 0.2945039868 0.2494443953 - 0.3293995857 0.2960574031 0.2509700954 - 0.3310334086 0.2976092100 0.2524966896 - 0.3326669931 0.2991577983 0.2540231049 - 0.3343001902 0.3007017970 0.2555483878 - 0.3359291852 0.3022336960 0.2570703030 - 0.3375551999 0.3037579954 0.2585889101 - 0.3391782939 0.3052760959 0.2601051033 - 0.3408016860 0.3067946136 0.2616184056 - 0.3424184024 0.3083077967 0.2631227970 - 0.3440291882 0.3098174036 0.2646223903 - 0.3456344008 0.3113240898 0.2661170959 - 0.3472355008 0.3128254116 0.2675963044 - 0.3488337100 0.3143222034 0.2690717876 - 0.3504329920 0.3158083856 0.2705436051 - 0.3520256877 0.3172895908 0.2720068991 - 0.3536128998 0.3187673986 0.2734695077 - 0.3551953137 0.3202396035 0.2749320865 - 0.3567740023 0.3217110932 0.2763895094 - 0.3583478928 0.3231821060 0.2778449059 - 0.3599140942 0.3246476054 0.2792975903 - 0.3614780009 0.3261092901 0.2807494998 - 0.3630394042 0.3275642991 0.2822004855 - 0.3645978868 0.3290148973 0.2836417854 - 0.3661546111 0.3304624856 0.2850796878 - 0.3677040935 0.3318977952 0.2865127027 - 0.3692491949 0.3333328068 0.2879436016 - 0.3707874119 0.3347673118 0.2893734872 - 0.3723173141 0.3362007141 0.2908020020 - 0.3738436997 0.3376331925 0.2922300100 - 0.3753637075 0.3390626013 0.2936573029 - 0.3768812120 0.3404957056 0.2950794101 - 0.3783938885 0.3419339061 0.2964994907 - 0.3798997998 0.3433656096 0.2979145050 - 0.3814015985 0.3447945118 0.2993288934 - 0.3829030991 0.3462235928 0.3007413149 - 0.3844040930 0.3476490080 0.3021497130 - 0.3859013915 0.3490602970 0.3035539091 - 0.3873961866 0.3504692018 0.3049587905 - 0.3888866007 0.3518750966 0.3063640893 - 0.3903723955 0.3532764912 0.3077667952 - 0.3918546140 0.3546754122 0.3091700077 - 0.3933346868 0.3560653925 0.3105787933 - 0.3948141932 0.3574534059 0.3119877875 - 0.3962810040 0.3588356972 0.3133974075 - 0.3977459073 0.3602159917 0.3148044050 - 0.3992024958 0.3615838885 0.3162082136 - 0.4006575048 0.3629511893 0.3176136911 - 0.4021052122 0.3643172085 0.3190205097 - 0.4035489857 0.3656820059 0.3204261065 - 0.4049836993 0.3670449853 0.3218308985 - 0.4064174891 0.3684046865 0.3232277036 - 0.4078499079 0.3697603047 0.3246217966 - 0.4092806876 0.3711144030 0.3260087967 - 0.4107097983 0.3724671006 0.3273941875 - 0.4121398926 0.3738192022 0.3287746906 - 0.4135712087 0.3751707971 0.3301542997 - 0.4149979949 0.3765240014 0.3315235972 - 0.4164206088 0.3778783083 0.3328914046 - 0.4178411961 0.3792268932 0.3342565000 - 0.4192601144 0.3805715144 0.3356212080 - 0.4206768870 0.3819119930 0.3369806111 - 0.4220918119 0.3832496107 0.3383392990 - 0.4235030115 0.3845824897 0.3396934867 - 0.4249098897 0.3859112859 0.3410468102 - 0.4263136089 0.3872357011 0.3423964083 - 0.4277122915 0.3885548115 0.3437449038 - 0.4291099906 0.3898749053 0.3450933099 - 0.4305053949 0.3911963999 0.3464416862 - 0.4319001138 0.3925139904 0.3477911949 - 0.4332916141 0.3938207030 0.3491415083 - 0.4346827865 0.3951269984 0.3504894078 - 0.4360674918 0.3964309096 0.3518337011 - 0.4374521971 0.3977347016 0.3531750143 - 0.4388273954 0.3990345001 0.3545077145 - 0.4402016103 0.4003342092 0.3558402061 - 0.4415682852 0.4016295075 0.3571698070 - 0.4429320991 0.4029237032 0.3584994078 - 0.4442929029 0.4042161107 0.3598237038 - 0.4456507862 0.4055073857 0.3611471951 - 0.4470078051 0.4067941010 0.3624671996 - 0.4483624101 0.4080730081 0.3637853861 - 0.4497171044 0.4093518853 0.3651016057 - 0.4510729909 0.4106301963 0.3664149940 - 0.4524287879 0.4119085073 0.3677273989 - 0.4537774920 0.4131798148 0.3690330088 - 0.4551241994 0.4144499004 0.3703387082 - 0.4564689100 0.4157176912 0.3716449142 - 0.4578113854 0.4169836044 0.3729512095 - 0.4591516852 0.4182476103 0.3742561936 - 0.4604795873 0.4195050895 0.3755598962 - 0.4618076086 0.4207625091 0.3768633008 - 0.4631280005 0.4220119119 0.3781648874 - 0.4644466043 0.4232602119 0.3794665039 - 0.4657608867 0.4245094061 0.3807671964 - 0.4670695066 0.4257597029 0.3820675910 - 0.4683777988 0.4270094037 0.3833664060 - 0.4696762860 0.4282543957 0.3846625090 - 0.4709748924 0.4294993877 0.3859587014 - 0.4722695947 0.4307434857 0.3872511983 - 0.4735617936 0.4319871962 0.3885434866 - 0.4748536050 0.4332297146 0.3898344040 - 0.4761427939 0.4344683886 0.3911238909 - 0.4774321020 0.4357070923 0.3924132884 - 0.4787220955 0.4369412065 0.3936966956 - 0.4800123870 0.4381737113 0.3949801922 - 0.4813013077 0.4394079149 0.3962610066 - 0.4825831950 0.4406479895 0.3975391090 - 0.4838651121 0.4418880939 0.3988172114 - 0.4851444066 0.4431279004 0.4000911117 - 0.4864222109 0.4443677068 0.4013648033 - 0.4876995981 0.4456071854 0.4026384950 - 0.4889726043 0.4468455017 0.4039121866 - 0.4902456999 0.4480836987 0.4051857889 - 0.4915165007 0.4493125081 0.4064609110 - 0.4927848876 0.4505336881 0.4077362120 - 0.4940532148 0.4517548084 0.4090102911 - 0.4953213036 0.4529699087 0.4102798104 - 0.4965893030 0.4541845024 0.4115493000 - 0.4978551865 0.4553967118 0.4128125012 - 0.4991137087 0.4566031992 0.4140706956 - 0.5003721714 0.4578095973 0.4153288901 - 0.5016276836 0.4590105116 0.4165867865 - 0.5028806925 0.4602079988 0.4178445935 - 0.5041337013 0.4614056051 0.4191029072 - 0.5053765774 0.4626047909 0.4203636944 - 0.5066177249 0.4638043046 0.4216246009 - 0.5078586936 0.4650028944 0.4228836000 - 0.5090987086 0.4661977887 0.4241401851 - 0.5103386045 0.4673928022 0.4253968894 - 0.5115761161 0.4685871899 0.4266510904 - 0.5128089190 0.4697811902 0.4279040992 - 0.5140416026 0.4709751010 0.4291571975 - 0.5152726173 0.4721660018 0.4304040968 - 0.5165020823 0.4733552933 0.4316503108 - 0.5177316070 0.4745444953 0.4328961074 - 0.5189521909 0.4757269919 0.4341362119 - 0.5201696157 0.4769079983 0.4353761971 - 0.5213869810 0.4780888855 0.4366151094 - 0.5225998163 0.4792577922 0.4378497899 - 0.5238118768 0.4804260135 0.4390844107 - 0.5250239968 0.4815933108 0.4403162897 - 0.5262380242 0.4827486873 0.4415428042 - 0.5274519920 0.4839040935 0.4427692890 - 0.5286653042 0.4850594103 0.4439926147 - 0.5298718810 0.4862147868 0.4452114105 - 0.5310785174 0.4873701930 0.4464302063 - 0.5322846174 0.4885238111 0.4476467967 - 0.5334876776 0.4896706939 0.4488607943 - 0.5346907973 0.4908174872 0.4500747919 - 0.5358918905 0.4919629097 0.4512878060 - 0.5370826721 0.4931035042 0.4524998069 - 0.5382735133 0.4942440987 0.4537116885 - 0.5394629836 0.4953829944 0.4549197853 - 0.5406448841 0.4965158999 0.4561235011 - 0.5418269038 0.4976488054 0.4573271871 - 0.5430071950 0.4987801909 0.4585284889 - 0.5441747904 0.4999046922 0.4597266018 - 0.5453423858 0.5010291934 0.4609247148 - 0.5465098023 0.5021533966 0.4621219039 - 0.5476737022 0.5032743216 0.4633173048 - 0.5488376021 0.5043951869 0.4645127952 - 0.5500016212 0.5055161715 0.4657061100 - 0.5511487722 0.5066317916 0.4668928087 - 0.5522946715 0.5077472925 0.4680793881 - 0.5534405112 0.5088629127 0.4692654908 - 0.5545827150 0.5099775791 0.4704455137 - 0.5557237267 0.5110921264 0.4716255963 - 0.5568647981 0.5122066140 0.4728055894 - 0.5579981804 0.5133147240 0.4739825130 - 0.5591266751 0.5144196153 0.4751591980 - 0.5602552295 0.5155246258 0.4763357937 - 0.5613806248 0.5166273117 0.4775057137 - 0.5625013113 0.5177274942 0.4786727130 - 0.5636219978 0.5188276768 0.4798397124 - 0.5647404194 0.5199279785 0.4809997082 - 0.5658451915 0.5210285187 0.4821512997 - 0.5669500828 0.5221291184 0.4833030105 - 0.5680549145 0.5232295990 0.4844540060 - 0.5691527128 0.5243226886 0.4856021106 - 0.5702494979 0.5254151821 0.4867500961 - 0.5713462234 0.5265076756 0.4878982008 - 0.5724427104 0.5276014209 0.4890469909 - 0.5735387802 0.5286958218 0.4901959002 - 0.5746350288 0.5297902226 0.4913448989 - 0.5757296085 0.5308814049 0.4924908876 - 0.5768179297 0.5319637060 0.4936339855 - 0.5779061913 0.5330458879 0.4947771132 - 0.5789945722 0.5341281295 0.4959191084 - 0.5800734758 0.5352011919 0.4970515966 - 0.5811501741 0.5362728834 0.4981842041 - 0.5822268724 0.5373445749 0.4993166924 - 0.5832991004 0.5384107232 0.5004442930 - 0.5843636990 0.5394697189 0.5015696883 - 0.5854284167 0.5405287147 0.5026950240 - 0.5864930749 0.5415875912 0.5038194060 - 0.5875535011 0.5426415801 0.5049406290 - 0.5886135101 0.5436955094 0.5060616732 - 0.5896735787 0.5447493196 0.5071828961 - 0.5907285810 0.5458040833 0.5082973838 - 0.5917763710 0.5468599796 0.5094091892 - 0.5928242803 0.5479158163 0.5105209947 - 0.5938721895 0.5489715934 0.5116317272 - 0.5949155092 0.5500273705 0.5127375722 - 0.5959581733 0.5510830879 0.5138434768 - 0.5970007777 0.5521388054 0.5149493814 - 0.5980414748 0.5531933904 0.5160537958 - 0.5990775228 0.5542458892 0.5171571970 - 0.6001135111 0.5552983284 0.5182604790 - 0.6011496186 0.5563508272 0.5193638802 - 0.6021804810 0.5573993921 0.5204594731 - 0.6032091975 0.5584467053 0.5215548873 - 0.6042377949 0.5594938993 0.5226503015 - 0.6052662134 0.5605403781 0.5237420797 - 0.6062859893 0.5615794063 0.5248262882 - 0.6073058248 0.5626183152 0.5259104967 - 0.6083257198 0.5636572838 0.5269945860 - 0.6093432903 0.5646920800 0.5280733109 - 0.6103563905 0.5657204986 0.5291488171 - 0.6113694906 0.5667489171 0.5302242041 - 0.6123825908 0.5677773952 0.5312995911 - 0.6133952141 0.5688034892 0.5323687792 - 0.6144075990 0.5698282719 0.5334370732 - 0.6154199243 0.5708531737 0.5345054865 - 0.6164321899 0.5718780756 0.5355728865 - 0.6174443960 0.5728979111 0.5366333723 - 0.6184564233 0.5739170909 0.5376937985 - 0.6194685102 0.5749362707 0.5387542248 - 0.6204805970 0.5759549737 0.5398129225 - 0.6214926243 0.5769659281 0.5408675075 - 0.6225045919 0.5779768825 0.5419220924 - 0.6235166192 0.5789877772 0.5429766774 - 0.6245272756 0.5799971223 0.5440295935 - 0.6255301237 0.5809996724 0.5450801849 - 0.6265329719 0.5820024014 0.5461307168 - 0.6275358796 0.5830050111 0.5471813083 - 0.6285378933 0.5840060711 0.5482295156 - 0.6295374036 0.5850033760 0.5492755175 - 0.6305369139 0.5860008001 0.5503215790 - 0.6315364242 0.5869981050 0.5513677001 - 0.6325309277 0.5879935026 0.5524119735 - 0.6335136890 0.5889853239 0.5534552932 - 0.6344965100 0.5899770856 0.5544984937 - 0.6354792714 0.5909689069 0.5555416942 - 0.6364601254 0.5919576883 0.5565804243 - 0.6374369264 0.5929412246 0.5576159954 - 0.6384136081 0.5939248204 0.5586516261 - 0.6393904090 0.5949084163 0.5596871972 - 0.6403620839 0.5958868861 0.5607174039 - 0.6413215995 0.5968555808 0.5617434978 - 0.6422811151 0.5978243947 0.5627695918 - 0.6432405114 0.5987930894 0.5637956858 - 0.6441984773 0.5997601151 0.5648216009 - 0.6451513767 0.6007226706 0.5658472776 - 0.6461042762 0.6016852260 0.5668728948 - 0.6470571756 0.6026477218 0.5678985715 - 0.6480082870 0.6036084294 0.5689231753 - 0.6489437819 0.6045597792 0.5699462295 - 0.6498792171 0.6055111885 0.5709691048 - 0.6508147120 0.6064625978 0.5719920993 - 0.6517502069 0.6074140072 0.5730134845 - 0.6526796222 0.6083596945 0.5740293860 - 0.6536086798 0.6093053818 0.5750454068 - 0.6545377970 0.6102510095 0.5760613084 - 0.6554667950 0.6111965775 0.5770772099 - 0.6563835144 0.6121419072 0.5780894756 - 0.6572958231 0.6130869985 0.5791018009 - 0.6582080126 0.6140322089 0.5801141262 - 0.6591202021 0.6149773002 0.5811263919 - 0.6600306034 0.6159195900 0.5821347833 - 0.6609390974 0.6168593168 0.5831419230 - 0.6618475914 0.6177989841 0.5841491222 - 0.6627560854 0.6187387705 0.5851562023 - 0.6636629105 0.6196792722 0.5861611962 - 0.6645622253 0.6206220984 0.5871635079 - 0.6654614806 0.6215649247 0.5881658792 - 0.6663607955 0.6225078106 0.5891681910 - 0.6672601104 0.6234505773 0.5901697874 - 0.6681554914 0.6243839860 0.5911645889 - 0.6690502167 0.6253163815 0.5921593904 - 0.6699448824 0.6262487769 0.5931541920 - 0.6708396077 0.6271811724 0.5941489935 - 0.6717275977 0.6281095743 0.5951371193 - 0.6726074219 0.6290342212 0.5961226821 - 0.6734871864 0.6299588084 0.5971081853 - 0.6743670106 0.6308833957 0.5980938077 - 0.6752467752 0.6318072081 0.5990791917 - 0.6761245131 0.6327196956 0.6000642180 - 0.6770021915 0.6336323023 0.6010491848 - 0.6778799295 0.6345447898 0.6020342112 - 0.6787576079 0.6354573965 0.6030192971 - 0.6796311140 0.6363689899 0.6039971709 - 0.6805009842 0.6372799277 0.6049730778 - 0.6813709140 0.6381908059 0.6059491038 - 0.6822407246 0.6391018033 0.6069250703 - 0.6831105947 0.6400123835 0.6078994870 - 0.6839790940 0.6409156919 0.6088691950 - 0.6848475933 0.6418190002 0.6098389030 - 0.6857160926 0.6427223086 0.6108086705 - 0.6865844727 0.6436256766 0.6117783785 - 0.6874501705 0.6445271969 0.6127467155 - 0.6883115768 0.6454266906 0.6137142777 - 0.6891731024 0.6463261843 0.6146818995 - 0.6900346279 0.6472256780 0.6156495214 - 0.6908959746 0.6481251717 0.6166170239 - 0.6917508841 0.6490163803 0.6175795197 - 0.6926038861 0.6499059200 0.6185420156 - 0.6934568882 0.6507952809 0.6195045114 - 0.6943098903 0.6516848207 0.6204670072 - 0.6951622963 0.6525734067 0.6214286089 - 0.6960077286 0.6534568071 0.6223886013 - 0.6968531013 0.6543400884 0.6233485937 - 0.6976984739 0.6552234888 0.6243085861 - 0.6985439062 0.6561068296 0.6252685785 - 0.6993870735 0.6569851041 0.6262217164 - 0.7002261877 0.6578559279 0.6271703839 - 0.7010654211 0.6587265730 0.6281189919 - 0.7019044757 0.6595973969 0.6290677190 - 0.7027435899 0.6604681015 0.6300163269 - 0.7035796046 0.6613349915 0.6309583187 - 0.7044131756 0.6621994972 0.6318988800 - 0.7052468061 0.6630640030 0.6328393817 - 0.7060803771 0.6639283895 0.6337800026 - 0.7069140077 0.6647928953 0.6347205043 - 0.7077404857 0.6656516790 0.6356571913 - 0.7085642815 0.6665087938 0.6365938187 - 0.7093880773 0.6673660278 0.6375303864 - 0.7102118731 0.6682230830 0.6384670138 - 0.7110357881 0.6690803170 0.6394029856 - 0.7118616104 0.6699306965 0.6403322220 - 0.7126877904 0.6707803011 0.6412613988 - 0.7135139704 0.6716299057 0.6421905756 - 0.7143402100 0.6724795103 0.6431198120 - 0.7151665092 0.6733291149 0.6440477967 - 0.7159851789 0.6741706729 0.6449677944 - 0.7168028951 0.6750116944 0.6458877921 - 0.7176206708 0.6758527756 0.6468077898 - 0.7184385061 0.6766939163 0.6477277875 - 0.7192562819 0.6775348783 0.6486470103 - 0.7200725079 0.6783677936 0.6495617032 - 0.7208884954 0.6792001724 0.6504762769 - 0.7217044830 0.6800324917 0.6513909101 - 0.7225204110 0.6808648705 0.6523056030 - 0.7233363986 0.6816973090 0.6532199979 - 0.7241454124 0.6825289130 0.6541327238 - 0.7249531150 0.6833605170 0.6550455093 - 0.7257608771 0.6841921210 0.6559581757 - 0.7265685797 0.6850237250 0.6568709016 - 0.7273762822 0.6858553290 0.6577836275 - 0.7281739712 0.6866853833 0.6586869955 - 0.7289680243 0.6875150800 0.6595901251 - 0.7297620773 0.6883447766 0.6604931951 - 0.7305560708 0.6891744733 0.6613963842 - 0.7313501239 0.6900042295 0.6622995138 - 0.7321395278 0.6908270121 0.6631954908 - 0.7329254746 0.6916458011 0.6640897989 - 0.7337114811 0.6924644709 0.6649841070 - 0.7344974875 0.6932832003 0.6658784747 - 0.7352834940 0.6941019893 0.6667727828 - 0.7360627055 0.6949160099 0.6676589251 - 0.7368296981 0.6957229972 0.6685394049 - 0.7375966907 0.6965299845 0.6694198847 - 0.7383636832 0.6973370910 0.6703003049 - 0.7391306758 0.6981440783 0.6711807847 - 0.7398970723 0.6989498138 0.6720594168 - 0.7406572104 0.6997461915 0.6729338169 - 0.7414172292 0.7005426884 0.6738082767 - 0.7421773076 0.7013391256 0.6746826768 - 0.7429373860 0.7021355033 0.6755570769 - 0.7436974049 0.7029320002 0.6764315963 - 0.7444459200 0.7037230134 0.6772987247 - 0.7451906204 0.7045127153 0.6781656742 - 0.7459353209 0.7053024173 0.6790326238 - 0.7466799021 0.7060920000 0.6798995733 - 0.7474246025 0.7068817019 0.6807665825 - 0.7481679916 0.7076675892 0.6816290021 - 0.7489092946 0.7084479928 0.6824882030 - 0.7496504784 0.7092282772 0.6833475232 - 0.7503917217 0.7100086808 0.6842067242 - 0.7511329055 0.7107890844 0.6850659847 - 0.7518740892 0.7115694284 0.6859251857 - 0.7526044250 0.7123498917 0.6867846847 - 0.7533336878 0.7131304145 0.6876441836 - 0.7540628910 0.7139108777 0.6885036230 - 0.7547922134 0.7146914005 0.6893631220 - 0.7555214167 0.7154719234 0.6902225018 - 0.7562506199 0.7162482738 0.6910750866 - 0.7569795251 0.7170194983 0.6919236183 - 0.7577084899 0.7177907825 0.6927719712 - 0.7584375143 0.7185620070 0.6936205029 - 0.7591664791 0.7193332911 0.6944689751 - 0.7598955035 0.7201045156 0.6953169107 - 0.7606170177 0.7208759189 0.6961556077 - 0.7613371015 0.7216472030 0.6969943047 - 0.7620571852 0.7224184871 0.6978331208 - 0.7627773285 0.7231898904 0.6986718178 - 0.7634974122 0.7239611745 0.6995105147 - 0.7642151713 0.7247301936 0.7003443837 - 0.7649244070 0.7254924178 0.7011716962 - 0.7656335831 0.7262545228 0.7019990087 - 0.7663428783 0.7270166874 0.7028263211 - 0.7670521140 0.7277787924 0.7036535740 - 0.7677612901 0.7285410166 0.7044810057 - 0.7684661150 0.7293031812 0.7053043842 - 0.7691670060 0.7300654054 0.7061262727 - 0.7698678970 0.7308276296 0.7069482803 - 0.7705687284 0.7315897942 0.7077702880 - 0.7712696195 0.7323520184 0.7085922956 - 0.7719705105 0.7331141829 0.7094143033 - 0.7726665735 0.7338690162 0.7102290988 - 0.7733613253 0.7346221209 0.7110437751 - 0.7740560174 0.7353752255 0.7118585110 - 0.7747508287 0.7361282706 0.7126731873 - 0.7754455209 0.7368813753 0.7134879231 - 0.7761402130 0.7376344800 0.7143009901 - 0.7768254876 0.7383875847 0.7151066065 - 0.7775105238 0.7391408086 0.7159122229 - 0.7781955004 0.7398939133 0.7167177796 - 0.7788804173 0.7406470776 0.7175235152 - 0.7795653939 0.7414001822 0.7183290720 - 0.7802500725 0.7421522141 0.7191318870 - 0.7809323072 0.7428963184 0.7199280858 - 0.7816144824 0.7436404824 0.7207242846 - 0.7822967172 0.7443845868 0.7215204239 - 0.7829788923 0.7451288104 0.7223166227 - 0.7836611271 0.7458729148 0.7231128216 - 0.7843412757 0.7466160059 0.7239063978 - 0.7850098014 0.7473546267 0.7246955037 - 0.7856782079 0.7480933070 0.7254844904 - 0.7863466740 0.7488319278 0.7262734771 - 0.7870151997 0.7495706081 0.7270625234 - 0.7876836061 0.7503092289 0.7278516293 - 0.7883511186 0.7510460019 0.7286369205 - 0.7890127897 0.7517744899 0.7294149995 - 0.7896745801 0.7525030971 0.7301930785 - 0.7903363109 0.7532315850 0.7309712172 - 0.7909979820 0.7539601922 0.7317492962 - 0.7916597128 0.7546886802 0.7325273752 - 0.7923207879 0.7554165721 0.7333045006 - 0.7929738760 0.7561392784 0.7340788841 - 0.7936270237 0.7568619847 0.7348533273 - 0.7942801118 0.7575846910 0.7356275916 - 0.7949333191 0.7583073974 0.7364019752 - 0.7955864072 0.7590301037 0.7371764183 - 0.7962394953 0.7597528100 0.7379497886 - 0.7968860865 0.7604665160 0.7387167215 - 0.7975323200 0.7611799240 0.7394834757 - 0.7981784940 0.7618933916 0.7402504086 - 0.7988247275 0.7626067996 0.7410172820 - 0.7994709015 0.7633203268 0.7417842150 - 0.8001170754 0.7640337944 0.7425510883 - 0.8007584810 0.7647390962 0.7433149815 - 0.8013982177 0.7654420137 0.7440785766 - 0.8020377755 0.7661449909 0.7448422909 - 0.8026775122 0.7668480277 0.7456058860 - 0.8033171892 0.7675510049 0.7463694811 - 0.8039568067 0.7682539821 0.7471331954 - 0.8045893908 0.7689520717 0.7478901148 - 0.8052138090 0.7696456909 0.7486435175 - 0.8058382869 0.7703393102 0.7493969202 - 0.8064628243 0.7710329294 0.7501503825 - 0.8070873022 0.7717264891 0.7509037852 - 0.8077117801 0.7724201083 0.7516571879 - 0.8083351851 0.7731121182 0.7524083853 - 0.8089513183 0.7737967968 0.7531548738 - 0.8095672727 0.7744815946 0.7539014220 - 0.8101834059 0.7751662731 0.7546479106 - 0.8107994795 0.7758510709 0.7553943992 - 0.8114156127 0.7765358090 0.7561408877 - 0.8120316863 0.7772204876 0.7568873763 - 0.8126400709 0.7778990865 0.7576224804 - 0.8132457137 0.7785758972 0.7583565116 - 0.8138511777 0.7792527080 0.7590904236 - 0.8144568205 0.7799293995 0.7598243952 - 0.8150624037 0.7806062102 0.7605584264 - 0.8156679869 0.7812829018 0.7612923980 - 0.8162711859 0.7819581032 0.7620232105 - 0.8168674707 0.7826291919 0.7627500296 - 0.8174636960 0.7833003998 0.7634767890 - 0.8180599809 0.7839716077 0.7642034888 - 0.8186563253 0.7846428156 0.7649303079 - 0.8192526102 0.7853139043 0.7656571269 - 0.8198487759 0.7859851122 0.7663838863 - 0.8204402924 0.7866560817 0.7671036124 - 0.8210297227 0.7873269916 0.7678225040 - 0.8216190934 0.7879980206 0.7685413957 - 0.8222085834 0.7886688709 0.7692602873 - 0.8227980137 0.7893397808 0.7699791789 - 0.8233875036 0.7900106907 0.7706980705 - 0.8239762187 0.7906810045 0.7714146972 - 0.8245580792 0.7913463712 0.7721254230 - 0.8251398802 0.7920119166 0.7728360295 - 0.8257218003 0.7926774025 0.7735465765 - 0.8263037205 0.7933428288 0.7742571831 - 0.8268855214 0.7940083146 0.7749677896 - 0.8274673820 0.7946738005 0.7756783962 - 0.8280459046 0.7953346968 0.7763838172 - 0.8286194801 0.7959898114 0.7770854831 - 0.8291931152 0.7966449857 0.7777872086 - 0.8297666907 0.7973002195 0.7784888744 - 0.8303403258 0.7979552746 0.7791904807 - 0.8309139013 0.7986105084 0.7798922062 - 0.8314874768 0.7992656827 0.7805938721 - 0.8320547938 0.7999172211 0.7812880278 - 0.8326187134 0.8005670905 0.7819803953 - 0.8331826925 0.8012170196 0.7826728821 - 0.8337466121 0.8018668294 0.7833653092 - 0.8343105912 0.8025166988 0.7840577960 - 0.8348745704 0.8031666279 0.7847502232 - 0.8354384899 0.8038163781 0.7854425907 - 0.8359956145 0.8044540882 0.7861267924 - 0.8365511894 0.8050895929 0.7868108153 - 0.8371068239 0.8057252169 0.7874947786 - 0.8376625180 0.8063607216 0.7881788015 - 0.8382180929 0.8069962263 0.7888628244 - 0.8387737274 0.8076316714 0.7895467877 - 0.8393293023 0.8082672954 0.7902303934 - 0.8398807049 0.8088986278 0.7909095287 - 0.8404316902 0.8095297217 0.7915886045 - 0.8409826756 0.8101608157 0.7922676206 - 0.8415337205 0.8107919097 0.7929466963 - 0.8420847058 0.8114230037 0.7936257720 - 0.8426356912 0.8120540977 0.7943047881 - 0.8431866765 0.8126851916 0.7949832082 - 0.8437362909 0.8133051991 0.7956548929 - 0.8442857862 0.8139246702 0.7963265777 - 0.8448352814 0.8145442009 0.7969983816 - 0.8453847766 0.8151637912 0.7976701260 - 0.8459343910 0.8157833219 0.7983418107 - 0.8464838862 0.8164027929 0.7990134954 - 0.8470333815 0.8170223236 0.7996851802 - 0.8475741148 0.8176347017 0.8003575802 - 0.8481134772 0.8182463050 0.8010299802 - 0.8486528993 0.8188579082 0.8017024994 - 0.8491923213 0.8194695115 0.8023748994 - 0.8497316837 0.8200811148 0.8030472994 - 0.8502712250 0.8206927180 0.8037198186 - 0.8508105874 0.8213043213 0.8043922186 - 0.8513522744 0.8219084144 0.8050565720 - 0.8518949151 0.8225101233 0.8057200909 - 0.8524376154 0.8231118917 0.8063836098 - 0.8529801965 0.8237137198 0.8070471287 - 0.8535227776 0.8243154287 0.8077105284 - 0.8540654182 0.8249171972 0.8083739877 - 0.8546079993 0.8255189061 0.8090375066 - 0.8551477790 0.8261185288 0.8096969724 - 0.8556851149 0.8267164230 0.8103548288 - 0.8562222719 0.8273143172 0.8110125065 - 0.8567596078 0.8279122114 0.8116701841 - 0.8572968245 0.8285101056 0.8123279214 - 0.8578341007 0.8291079998 0.8129855990 - 0.8583713174 0.8297058940 0.8136432767 - 0.8589066863 0.8303020000 0.8142979741 - 0.8594359756 0.8308932185 0.8149477839 - 0.8599653244 0.8314844966 0.8155977130 - 0.8604946136 0.8320757151 0.8162475228 - 0.8610237837 0.8326669931 0.8168973923 - 0.8615530729 0.8332582116 0.8175472021 - 0.8620824218 0.8338493705 0.8181970716 - 0.8626117110 0.8344407082 0.8188468814 - 0.8631411791 0.8350260854 0.8194969296 - 0.8636707067 0.8356109262 0.8201467991 - 0.8642001748 0.8361955881 0.8207967877 - 0.8647297025 0.8367804289 0.8214467764 - 0.8652591705 0.8373650908 0.8220967054 - 0.8657886982 0.8379498124 0.8227466941 - 0.8663182259 0.8385345936 0.8233966231 - 0.8668442965 0.8391172886 0.8240422010 - 0.8673655987 0.8396975994 0.8246843815 - 0.8678870201 0.8402777910 0.8253266215 - 0.8684083223 0.8408579826 0.8259688020 - 0.8689296842 0.8414382935 0.8266109824 - 0.8694509864 0.8420184851 0.8272532225 - 0.8699724078 0.8425986767 0.8278954029 - 0.8704937100 0.8431789279 0.8285375834 - 0.8710048795 0.8437566757 0.8291798830 - 0.8715147972 0.8443341851 0.8298223019 - 0.8720247149 0.8449115753 0.8304646015 - 0.8725346923 0.8454890847 0.8311069012 - 0.8730446100 0.8460665941 0.8317492008 - 0.8735545278 0.8466441035 0.8323916197 - 0.8740643859 0.8472216129 0.8330339193 - 0.8745729923 0.8477984071 0.8336730003 - 0.8750782013 0.8483735919 0.8343076706 - 0.8755834103 0.8489488959 0.8349422812 - 0.8760886192 0.8495240808 0.8355768919 - 0.8765938282 0.8500993252 0.8362116218 - 0.8770989776 0.8506745100 0.8368461728 - 0.8776041865 0.8512498140 0.8374807835 - 0.8781093955 0.8518249989 0.8381155133 - 0.8786090016 0.8523975015 0.8387501836 - 0.8791047931 0.8529682755 0.8393849134 - 0.8796005249 0.8535389900 0.8400195837 - 0.8800961971 0.8541098237 0.8406543732 - 0.8805919886 0.8546805978 0.8412891030 - 0.8810877204 0.8552513719 0.8419238925 - 0.8815835118 0.8558220863 0.8425586224 - 0.8820791841 0.8563929200 0.8431932926 - 0.8825731277 0.8569598198 0.8438208103 - 0.8830664754 0.8575260043 0.8444479108 - 0.8835598826 0.8580921292 0.8450748920 - 0.8840534091 0.8586583138 0.8457019925 - 0.8845468163 0.8592243791 0.8463289738 - 0.8850402236 0.8597906232 0.8469560146 - 0.8855336905 0.8603568077 0.8475831151 - 0.8860270977 0.8609228730 0.8482097983 - 0.8865162134 0.8614861965 0.8488330245 - 0.8870049715 0.8620492816 0.8494560719 - 0.8874937892 0.8626124859 0.8500791788 - 0.8879824877 0.8631756902 0.8507024050 - 0.8884713054 0.8637387753 0.8513255119 - 0.8889601231 0.8643019795 0.8519486189 - 0.8894488811 0.8648651242 0.8525717854 - 0.8899376988 0.8654280901 0.8531945944 - 0.8904203176 0.8659834862 0.8538153768 - 0.8909028172 0.8665388227 0.8544362187 - 0.8913853765 0.8670940995 0.8550570011 - 0.8918679953 0.8676494956 0.8556777835 - 0.8923504949 0.8682047725 0.8562985063 - 0.8928331137 0.8687602282 0.8569192886 - 0.8933156133 0.8693155050 0.8575400710 - 0.8937981725 0.8698707819 0.8581606150 - 0.8942810297 0.8704199791 0.8587781191 - 0.8947638273 0.8709689975 0.8593956232 - 0.8952466846 0.8715180755 0.8600131869 - 0.8957294822 0.8720670938 0.8606306911 - 0.8962123990 0.8726161718 0.8612481952 - 0.8966951966 0.8731653094 0.8618656993 - 0.8971779943 0.8737143278 0.8624832034 - 0.8976609111 0.8742634058 0.8631008267 - 0.8981388211 0.8748087883 0.8637112975 - 0.8986158967 0.8753535748 0.8643215895 - 0.8990929723 0.8758984208 0.8649318814 - 0.8995701075 0.8764432073 0.8655421138 - 0.9000471234 0.8769881129 0.8661524057 - 0.9005241990 0.8775328994 0.8667626977 - 0.9010012746 0.8780776858 0.8673729897 - 0.9014782906 0.8786225915 0.8679832816 - 0.9019498825 0.8791623712 0.8685910106 - 0.9024183154 0.8796998262 0.8691979051 - 0.9028868079 0.8802371025 0.8698047996 - 0.9033551812 0.8807744980 0.8704116940 - 0.9038236737 0.8813118935 0.8710185289 - 0.9042921066 0.8818492293 0.8716254234 - 0.9047604799 0.8823866248 0.8722323179 - 0.9052289724 0.8829239011 0.8728392124 - 0.9056965113 0.8834607005 0.8734431267 - 0.9061623216 0.8839964867 0.8740432858 - 0.9066281915 0.8845322132 0.8746436238 - 0.9070940018 0.8850679994 0.8752437830 - 0.9075598121 0.8856037855 0.8758441210 - 0.9080256224 0.8861395121 0.8764442801 - 0.9084914923 0.8866752982 0.8770446181 - 0.9089573026 0.8872110248 0.8776447773 - 0.9094231129 0.8877468109 0.8782445788 - 0.9098821282 0.8882740736 0.8788375258 - 0.9103407860 0.8888012767 0.8794304132 - 0.9107996225 0.8893284202 0.8800233006 - 0.9112582803 0.8898556232 0.8806161284 - 0.9117169976 0.8903827071 0.8812090158 - 0.9121757150 0.8909099102 0.8818019032 - 0.9126343727 0.8914369941 0.8823947906 - 0.9130930901 0.8919641972 0.8829876184 - 0.9135512710 0.8924918771 0.8835765123 - 0.9140090942 0.8930202723 0.8841624260 - 0.9144667983 0.8935487270 0.8847482800 - 0.9149245024 0.8940771222 0.8853343129 - 0.9153823256 0.8946055174 0.8859202266 - 0.9158400297 0.8951339126 0.8865060806 - 0.9162976742 0.8956621885 0.8870919943 - 0.9167554975 0.8961905837 0.8876780272 - 0.9172132015 0.8967189789 0.8882638216 - 0.9176644087 0.8972421288 0.8888434768 - 0.9181149006 0.8977648020 0.8894230723 - 0.9185655117 0.8982874751 0.8900027871 - 0.9190160036 0.8988103271 0.8905823827 - 0.9194664955 0.8993330002 0.8911620975 - 0.9199171066 0.8998556733 0.8917416930 - 0.9203675985 0.9003784060 0.8923214078 - 0.9208182096 0.9009010792 0.8929010034 - 0.9212675095 0.9014238715 0.8934786916 - 0.9217131734 0.9019467831 0.8940525055 - 0.9221590161 0.9024696946 0.8946262002 - 0.9226046801 0.9029926062 0.8952000141 - 0.9230504036 0.9035155177 0.8957738280 - 0.9234961271 0.9040384293 0.8963475823 - 0.9239419103 0.9045612812 0.8969212770 - 0.9243875742 0.9050841928 0.8974950910 - 0.9248332977 0.9056071043 0.8980689049 - 0.9252752066 0.9061285257 0.8986396790 - 0.9257131219 0.9066482782 0.8992083073 - 0.9261509776 0.9071682096 0.8997768164 - 0.9265888929 0.9076880813 0.9003453851 - 0.9270266891 0.9082080126 0.9009140134 - 0.9274646044 0.9087278247 0.9014825821 - 0.9279025197 0.9092476964 0.9020512104 - 0.9283403754 0.9097676277 0.9026197791 - 0.9287782907 0.9102874994 0.9031882882 - 0.9292140007 0.9107999802 0.9037535191 - 0.9296483994 0.9113087058 0.9043173194 - 0.9300829172 0.9118173122 0.9048811793 - 0.9305173159 0.9123259783 0.9054449797 - 0.9309517741 0.9128345847 0.9060088992 - 0.9313861728 0.9133433104 0.9065728188 - 0.9318206906 0.9138519168 0.9071366191 - 0.9322550893 0.9143605828 0.9077004790 - 0.9326896071 0.9148693085 0.9082642794 - 0.9331244230 0.9153769016 0.9088249803 - 0.9335594177 0.9158840775 0.9093847871 - 0.9339944124 0.9163913131 0.9099445939 - 0.9344294071 0.9168984294 0.9105042815 - 0.9348644018 0.9174056053 0.9110640883 - 0.9352993965 0.9179127812 0.9116238952 - 0.9357343912 0.9184200168 0.9121837020 - 0.9361693859 0.9189271927 0.9127433896 - 0.9366044998 0.9194344282 0.9133031964 - 0.9370371103 0.9199361205 0.9138603806 - 0.9374685287 0.9204351902 0.9144166708 - 0.9378998876 0.9209342003 0.9149729013 - 0.9383313060 0.9214332104 0.9155290723 - 0.9387627244 0.9219322205 0.9160854220 - 0.9391940832 0.9224311709 0.9166415930 - 0.9396255016 0.9229301810 0.9171978831 - 0.9400569201 0.9234291911 0.9177541137 - 0.9404882789 0.9239282012 0.9183102846 - 0.9409155250 0.9244257212 0.9188649058 - 0.9413387179 0.9249218106 0.9194182158 - 0.9417619705 0.9254179001 0.9199715257 - 0.9421852231 0.9259139895 0.9205247760 - 0.9426084161 0.9264100790 0.9210780859 - 0.9430316091 0.9269062281 0.9216313958 - 0.9434548020 0.9274023175 0.9221848249 - 0.9438781142 0.9278984070 0.9227380753 - 0.9443013072 0.9283944964 0.9232913852 - 0.9447239041 0.9288893938 0.9238408208 - 0.9451450109 0.9293813109 0.9243822098 - 0.9455661178 0.9298732281 0.9249237180 - 0.9459871054 0.9303650856 0.9254652262 - 0.9464082122 0.9308570027 0.9260066748 - 0.9468291998 0.9313489199 0.9265481830 - 0.9472503066 0.9318407774 0.9270896912 - 0.9476714134 0.9323326945 0.9276311994 - 0.9480924010 0.9328246117 0.9281727076 - 0.9485135078 0.9333165288 0.9287142158 - 0.9489266276 0.9338006973 0.9292520881 - 0.9493390918 0.9342843890 0.9297899008 - 0.9497516155 0.9347680807 0.9303277731 - 0.9501640797 0.9352517724 0.9308655858 - 0.9505766034 0.9357355833 0.9314035177 - 0.9509891272 0.9362192750 0.9319412708 - 0.9514014721 0.9367030263 0.9324790835 - 0.9518139958 0.9371867180 0.9330170155 - 0.9522265196 0.9376704097 0.9335548282 - 0.9526365995 0.9381523132 0.9340875149 - 0.9530441761 0.9386324883 0.9346156716 - 0.9534518123 0.9391127229 0.9351440072 - 0.9538593888 0.9395927787 0.9356722236 - 0.9542670250 0.9400730133 0.9362003803 - 0.9546746016 0.9405531287 0.9367287159 - 0.9550821781 0.9410333037 0.9372568727 - 0.9554898143 0.9415134192 0.9377852082 - 0.9558972716 0.9419935942 0.9383134246 - 0.9563049078 0.9424737096 0.9388415813 - 0.9567065835 0.9429457784 0.9393643737 - 0.9571079016 0.9434173703 0.9398869872 - 0.9575092196 0.9438890219 0.9404097199 - 0.9579104185 0.9443606138 0.9409322739 - 0.9583116770 0.9448322058 0.9414550066 - 0.9587129951 0.9453037977 0.9419776797 - 0.9591141939 0.9457753897 0.9425002933 - 0.9595155120 0.9462471008 0.9430230260 - 0.9599167705 0.9467186928 0.9435455799 - 0.9603134990 0.9471881986 0.9440639019 - 0.9606987238 0.9476525784 0.9445734024 - 0.9610838294 0.9481170177 0.9450829029 - 0.9614689946 0.9485813975 0.9455924034 - 0.9618542194 0.9490458965 0.9461019039 - 0.9622393250 0.9495102763 0.9466114044 - 0.9626244903 0.9499747157 0.9471209049 - 0.9630097151 0.9504390955 0.9476304054 - 0.9633948803 0.9509035945 0.9481399059 - 0.9637799859 0.9513679743 0.9486494064 - 0.9641588926 0.9518290758 0.9491565824 - 0.9645323157 0.9522874951 0.9496620297 - 0.9649056196 0.9527457952 0.9501674175 - 0.9652789831 0.9532042146 0.9506728053 - 0.9656522870 0.9536625147 0.9511781931 - 0.9660257101 0.9541208744 0.9516835809 - 0.9663990140 0.9545791745 0.9521890283 - 0.9667723775 0.9550374746 0.9526944160 - 0.9671456814 0.9554958940 0.9531999230 - 0.9675191045 0.9559541941 0.9537053108 - 0.9678881764 0.9564117789 0.9542062283 - 0.9682552218 0.9568691254 0.9547054172 - 0.9686222076 0.9573262930 0.9552046061 - 0.9689893126 0.9577835798 0.9557037950 - 0.9693562984 0.9582408071 0.9562029839 - 0.9697232842 0.9586980939 0.9567021728 - 0.9700903296 0.9591553211 0.9572014213 - 0.9704573750 0.9596126080 0.9577006102 - 0.9708244205 0.9600697756 0.9581997991 - 0.9711914062 0.9605271220 0.9586989880 - 0.9715511799 0.9609804749 0.9591947794 - 0.9719079137 0.9614323974 0.9596894979 - 0.9722645283 0.9618843198 0.9601842165 - 0.9726212025 0.9623361230 0.9606788754 - 0.9729778767 0.9627879858 0.9611735940 - 0.9733346105 0.9632399082 0.9616683125 - 0.9736912847 0.9636917710 0.9621629715 - 0.9740480185 0.9641436934 0.9626576900 - 0.9744045734 0.9645954967 0.9631524086 - 0.9747613072 0.9650474191 0.9636471272 - 0.9751121998 0.9654958248 0.9641352892 - 0.9754595757 0.9659423232 0.9646201730 - 0.9758070111 0.9663887024 0.9651051164 - 0.9761543870 0.9668352008 0.9655900002 - 0.9765018225 0.9672815800 0.9660750031 - 0.9768491983 0.9677280784 0.9665598869 - 0.9771965146 0.9681745172 0.9670447707 - 0.9775438905 0.9686208963 0.9675297141 - 0.9778913260 0.9690673947 0.9680147171 - 0.9782387018 0.9695137739 0.9684996009 - 0.9785814285 0.9699571133 0.9689792991 - 0.9789180160 0.9703966975 0.9694532752 - 0.9792547226 0.9708362222 0.9699273109 - 0.9795914292 0.9712756872 0.9704012871 - 0.9799280763 0.9717152119 0.9708753228 - 0.9802647829 0.9721547961 0.9713492990 - 0.9806014895 0.9725943208 0.9718232751 - 0.9809380770 0.9730337858 0.9722973108 - 0.9812747836 0.9734733105 0.9727712870 - 0.9816114902 0.9739128947 0.9732453823 - 0.9819465280 0.9743511081 0.9737172723 - 0.9822719097 0.9747827053 0.9741798043 - 0.9825971723 0.9752143025 0.9746423960 - 0.9829226136 0.9756458998 0.9751049280 - 0.9832479954 0.9760774970 0.9755674005 - 0.9835733175 0.9765089750 0.9760299921 - 0.9838986993 0.9769405723 0.9764925241 - 0.9842240810 0.9773722291 0.9769549966 - 0.9845495224 0.9778038263 0.9774175286 - 0.9848747849 0.9782354236 0.9778801203 - 0.9852002263 0.9786669016 0.9783425927 - 0.9855154157 0.9790912867 0.9787945747 - 0.9858270288 0.9795132279 0.9792435169 - 0.9861387014 0.9799351096 0.9796923995 - 0.9864503741 0.9803571105 0.9801412225 - 0.9867621064 0.9807789922 0.9805901051 - 0.9870737791 0.9812008739 0.9810389876 - 0.9873855114 0.9816228747 0.9814878702 - 0.9876971245 0.9820448160 0.9819366932 - 0.9880087972 0.9824666977 0.9823855758 - 0.9883205295 0.9828886986 0.9828345180 - 0.9886283278 0.9833080173 0.9832788706 - 0.9889227748 0.9837185740 0.9837099910 - 0.9892172813 0.9841291904 0.9841409922 - 0.9895117879 0.9845399261 0.9845719934 - 0.9898062944 0.9849504828 0.9850029945 - 0.9901008010 0.9853610992 0.9854339957 - 0.9903953075 0.9857717156 0.9858649969 - 0.9906898141 0.9861822724 0.9862959981 - 0.9909843206 0.9865928888 0.9867269993 - 0.9912788272 0.9870036244 0.9871580005 - 0.9915732741 0.9874141812 0.9875890017 - 0.9918602109 0.9878157973 0.9880052805 - 0.9921419024 0.9882115126 0.9884120822 - 0.9924237132 0.9886071086 0.9888188839 - 0.9927054048 0.9890027046 0.9892256856 - 0.9929870963 0.9893983006 0.9896324873 - 0.9932689071 0.9897938967 0.9900392890 - 0.9935505986 0.9901894927 0.9904460907 - 0.9938322902 0.9905850887 0.9908528924 - 0.9941141009 0.9909806848 0.9912596941 - 0.9943957925 0.9913762808 0.9916664958 - 0.9946774840 0.9917719960 0.9920732975 - 0.9949371219 0.9921513796 0.9924513102 - 0.9951925874 0.9925280213 0.9928246140 - 0.9954481125 0.9929047227 0.9931977987 - 0.9957035780 0.9932813048 0.9935709834 - 0.9959591031 0.9936578870 0.9939442277 - 0.9962146282 0.9940345287 0.9943174124 - 0.9964702129 0.9944111109 0.9946905971 - 0.9967256784 0.9947876930 0.9950639009 - 0.9969812036 0.9951642752 0.9954370856 - 0.9972367287 0.9955409169 0.9958102703 - 0.9974921942 0.9959174991 0.9961835146 - 0.9977207184 0.9962887168 0.9965307713 - 0.9979485869 0.9966598749 0.9968776703 - 0.9981765151 0.9970309734 0.9972246289 - 0.9984045029 0.9974020720 0.9975715280 - 0.9986323714 0.9977731705 0.9979184866 - 0.9988602996 0.9981443882 0.9982653856 - 0.9990882874 0.9985154867 0.9986122847 - 0.9993162155 0.9988865852 0.9989591837 - 0.9995440841 0.9992576838 0.9993062019 - 0.9997720718 0.9996289015 0.9996531010 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0069576460 0.0052925618 0.0033476830 + 0.0141364904 0.0107551003 0.0067514139 + 0.0207183994 0.0159495007 0.0101133399 + 0.0267989002 0.0208614096 0.0133742504 + 0.0324718915 0.0254526306 0.0165257901 + 0.0378164984 0.0299201701 0.0196132604 + 0.0428869314 0.0341088213 0.0225949902 + 0.0477258898 0.0381722189 0.0255131796 + 0.0523908511 0.0420860387 0.0283894595 + 0.0568610094 0.0458872616 0.0311939605 + 0.0611275584 0.0495900512 0.0339626707 + 0.0653016865 0.0531945191 0.0367048383 + 0.0693744496 0.0567364991 0.0393822007 + 0.0733573064 0.0602095909 0.0420081317 + 0.0771774426 0.0635916665 0.0445935093 + 0.0809165314 0.0668876693 0.0471374691 + 0.0845568478 0.0701269805 0.0496489182 + 0.0881353170 0.0733123422 0.0521427989 + 0.0916419923 0.0764425769 0.0546066016 + 0.0950766504 0.0795146525 0.0570084900 + 0.0984379798 0.0825331509 0.0593760982 + 0.1017495021 0.0855173692 0.0617147498 + 0.1049598977 0.0884445384 0.0640344396 + 0.1081260964 0.0913117006 0.0663272068 + 0.1112463027 0.0941295624 0.0686026216 + 0.1143177003 0.0968901590 0.0708457306 + 0.1173487976 0.0996170789 0.0730642602 + 0.1203667000 0.1023124009 0.0752720088 + 0.1233123019 0.1049759015 0.0774757266 + 0.1262118965 0.1076003984 0.0796506926 + 0.1290674955 0.1101882979 0.0817981884 + 0.1318753064 0.1127377972 0.0839270800 + 0.1346521974 0.1152800992 0.0860472471 + 0.1373953074 0.1178082004 0.0881496072 + 0.1401219070 0.1203086972 0.0902255625 + 0.1428176016 0.1227945015 0.0922822729 + 0.1454751045 0.1252799928 0.0943331197 + 0.1481045038 0.1277426928 0.0963695124 + 0.1507305056 0.1301932037 0.0983908027 + 0.1533288956 0.1326095015 0.1003959030 + 0.1558901072 0.1350027025 0.1023878008 + 0.1584240943 0.1373652965 0.1043625996 + 0.1609431952 0.1397027969 0.1063340977 + 0.1634577960 0.1420170069 0.1083092019 + 0.1659422070 0.1442991942 0.1102795005 + 0.1683879048 0.1465636939 0.1122395992 + 0.1708077043 0.1488011926 0.1141989976 + 0.1732032001 0.1510327011 0.1161565036 + 0.1755875945 0.1532423943 0.1181034967 + 0.1779661030 0.1554349959 0.1200399995 + 0.1803372949 0.1576205045 0.1219606996 + 0.1826853007 0.1597833931 0.1238709986 + 0.1850018948 0.1619350016 0.1257669926 + 0.1873046011 0.1640775949 0.1276451051 + 0.1896051019 0.1661971062 0.1295011044 + 0.1918939054 0.1682944000 0.1313541979 + 0.1941768974 0.1703854054 0.1332028955 + 0.1964391023 0.1724753976 0.1350436956 + 0.1986718029 0.1745598018 0.1368709952 + 0.2008887976 0.1766363978 0.1386903971 + 0.2030901015 0.1787001044 0.1405090988 + 0.2052837014 0.1807560027 0.1423261017 + 0.2074694037 0.1827982068 0.1441355944 + 0.2096378058 0.1848137975 0.1459414959 + 0.2117996961 0.1868076026 0.1477414072 + 0.2139513046 0.1887951046 0.1495409012 + 0.2160806060 0.1907595992 0.1513372064 + 0.2181894034 0.1927050054 0.1531285048 + 0.2202865928 0.1946384013 0.1549127996 + 0.2223830074 0.1965693980 0.1566943973 + 0.2244744003 0.1984919012 0.1584592015 + 0.2265536934 0.2003978044 0.1602202058 + 0.2286141068 0.2022926956 0.1619769037 + 0.2306680977 0.2041870058 0.1637236029 + 0.2327179015 0.2060835958 0.1654655933 + 0.2347580940 0.2079713047 0.1672006994 + 0.2367894948 0.2098497003 0.1689288020 + 0.2388225049 0.2117146999 0.1706545055 + 0.2408446074 0.2135720998 0.1723743975 + 0.2428611964 0.2154268026 0.1740881950 + 0.2448751032 0.2172740996 0.1757902950 + 0.2468796968 0.2191029936 0.1774809957 + 0.2488763928 0.2209150046 0.1791640073 + 0.2508633137 0.2227170020 0.1808429956 + 0.2528334856 0.2245123982 0.1825204939 + 0.2547827065 0.2263000011 0.1841987073 + 0.2567098141 0.2280700058 0.1858744025 + 0.2586239874 0.2298292071 0.1875447035 + 0.2605265081 0.2315824926 0.1892118007 + 0.2624250948 0.2333337963 0.1908793002 + 0.2643153071 0.2350836992 0.1925469935 + 0.2661927938 0.2368271947 0.1942107975 + 0.2680534124 0.2385641932 0.1958703995 + 0.2698996067 0.2402985990 0.1975174993 + 0.2717376053 0.2420333028 0.1991517991 + 0.2735638916 0.2437696010 0.2007769942 + 0.2753801942 0.2455032021 0.2023956031 + 0.2771906853 0.2472324073 0.2040095031 + 0.2789969146 0.2489607930 0.2056213021 + 0.2808091938 0.2506855130 0.2072338015 + 0.2826234102 0.2524011135 0.2088443041 + 0.2844350040 0.2541044950 0.2104482055 + 0.2862409055 0.2558018863 0.2120479047 + 0.2880494893 0.2574988008 0.2136453986 + 0.2898606062 0.2591859102 0.2152310014 + 0.2916708887 0.2608619034 0.2168077976 + 0.2934772968 0.2625282109 0.2183786035 + 0.2952775061 0.2641865015 0.2199451029 + 0.2970677912 0.2658401132 0.2215090990 + 0.2988429070 0.2674838006 0.2230720073 + 0.3006018996 0.2691144943 0.2246360928 + 0.3023515940 0.2707369030 0.2262039930 + 0.3040930927 0.2723526061 0.2277673930 + 0.3058268130 0.2739633918 0.2293279022 + 0.3075599968 0.2755722106 0.2308869064 + 0.3092887998 0.2771736085 0.2324458063 + 0.3110117018 0.2787660062 0.2339975983 + 0.3127267957 0.2803517878 0.2355509996 + 0.3144324124 0.2819328010 0.2371080071 + 0.3161234856 0.2835094035 0.2386678010 + 0.3178110123 0.2850857973 0.2402258068 + 0.3194932044 0.2866651118 0.2417753041 + 0.3211686909 0.2882451117 0.2433187068 + 0.3228329122 0.2898192108 0.2448568046 + 0.3244825006 0.2913886905 0.2463894933 + 0.3261260092 0.2929497957 0.2479178011 + 0.3277656138 0.2945039868 0.2494443953 + 0.3293995857 0.2960574031 0.2509700954 + 0.3310334086 0.2976092100 0.2524966896 + 0.3326669931 0.2991577983 0.2540231049 + 0.3343001902 0.3007017970 0.2555483878 + 0.3359291852 0.3022336960 0.2570703030 + 0.3375551999 0.3037579954 0.2585889101 + 0.3391782939 0.3052760959 0.2601051033 + 0.3408016860 0.3067946136 0.2616184056 + 0.3424184024 0.3083077967 0.2631227970 + 0.3440291882 0.3098174036 0.2646223903 + 0.3456344008 0.3113240898 0.2661170959 + 0.3472355008 0.3128254116 0.2675963044 + 0.3488337100 0.3143222034 0.2690717876 + 0.3504329920 0.3158083856 0.2705436051 + 0.3520256877 0.3172895908 0.2720068991 + 0.3536128998 0.3187673986 0.2734695077 + 0.3551953137 0.3202396035 0.2749320865 + 0.3567740023 0.3217110932 0.2763895094 + 0.3583478928 0.3231821060 0.2778449059 + 0.3599140942 0.3246476054 0.2792975903 + 0.3614780009 0.3261092901 0.2807494998 + 0.3630394042 0.3275642991 0.2822004855 + 0.3645978868 0.3290148973 0.2836417854 + 0.3661546111 0.3304624856 0.2850796878 + 0.3677040935 0.3318977952 0.2865127027 + 0.3692491949 0.3333328068 0.2879436016 + 0.3707874119 0.3347673118 0.2893734872 + 0.3723173141 0.3362007141 0.2908020020 + 0.3738436997 0.3376331925 0.2922300100 + 0.3753637075 0.3390626013 0.2936573029 + 0.3768812120 0.3404957056 0.2950794101 + 0.3783938885 0.3419339061 0.2964994907 + 0.3798997998 0.3433656096 0.2979145050 + 0.3814015985 0.3447945118 0.2993288934 + 0.3829030991 0.3462235928 0.3007413149 + 0.3844040930 0.3476490080 0.3021497130 + 0.3859013915 0.3490602970 0.3035539091 + 0.3873961866 0.3504692018 0.3049587905 + 0.3888866007 0.3518750966 0.3063640893 + 0.3903723955 0.3532764912 0.3077667952 + 0.3918546140 0.3546754122 0.3091700077 + 0.3933346868 0.3560653925 0.3105787933 + 0.3948141932 0.3574534059 0.3119877875 + 0.3962810040 0.3588356972 0.3133974075 + 0.3977459073 0.3602159917 0.3148044050 + 0.3992024958 0.3615838885 0.3162082136 + 0.4006575048 0.3629511893 0.3176136911 + 0.4021052122 0.3643172085 0.3190205097 + 0.4035489857 0.3656820059 0.3204261065 + 0.4049836993 0.3670449853 0.3218308985 + 0.4064174891 0.3684046865 0.3232277036 + 0.4078499079 0.3697603047 0.3246217966 + 0.4092806876 0.3711144030 0.3260087967 + 0.4107097983 0.3724671006 0.3273941875 + 0.4121398926 0.3738192022 0.3287746906 + 0.4135712087 0.3751707971 0.3301542997 + 0.4149979949 0.3765240014 0.3315235972 + 0.4164206088 0.3778783083 0.3328914046 + 0.4178411961 0.3792268932 0.3342565000 + 0.4192601144 0.3805715144 0.3356212080 + 0.4206768870 0.3819119930 0.3369806111 + 0.4220918119 0.3832496107 0.3383392990 + 0.4235030115 0.3845824897 0.3396934867 + 0.4249098897 0.3859112859 0.3410468102 + 0.4263136089 0.3872357011 0.3423964083 + 0.4277122915 0.3885548115 0.3437449038 + 0.4291099906 0.3898749053 0.3450933099 + 0.4305053949 0.3911963999 0.3464416862 + 0.4319001138 0.3925139904 0.3477911949 + 0.4332916141 0.3938207030 0.3491415083 + 0.4346827865 0.3951269984 0.3504894078 + 0.4360674918 0.3964309096 0.3518337011 + 0.4374521971 0.3977347016 0.3531750143 + 0.4388273954 0.3990345001 0.3545077145 + 0.4402016103 0.4003342092 0.3558402061 + 0.4415682852 0.4016295075 0.3571698070 + 0.4429320991 0.4029237032 0.3584994078 + 0.4442929029 0.4042161107 0.3598237038 + 0.4456507862 0.4055073857 0.3611471951 + 0.4470078051 0.4067941010 0.3624671996 + 0.4483624101 0.4080730081 0.3637853861 + 0.4497171044 0.4093518853 0.3651016057 + 0.4510729909 0.4106301963 0.3664149940 + 0.4524287879 0.4119085073 0.3677273989 + 0.4537774920 0.4131798148 0.3690330088 + 0.4551241994 0.4144499004 0.3703387082 + 0.4564689100 0.4157176912 0.3716449142 + 0.4578113854 0.4169836044 0.3729512095 + 0.4591516852 0.4182476103 0.3742561936 + 0.4604795873 0.4195050895 0.3755598962 + 0.4618076086 0.4207625091 0.3768633008 + 0.4631280005 0.4220119119 0.3781648874 + 0.4644466043 0.4232602119 0.3794665039 + 0.4657608867 0.4245094061 0.3807671964 + 0.4670695066 0.4257597029 0.3820675910 + 0.4683777988 0.4270094037 0.3833664060 + 0.4696762860 0.4282543957 0.3846625090 + 0.4709748924 0.4294993877 0.3859587014 + 0.4722695947 0.4307434857 0.3872511983 + 0.4735617936 0.4319871962 0.3885434866 + 0.4748536050 0.4332297146 0.3898344040 + 0.4761427939 0.4344683886 0.3911238909 + 0.4774321020 0.4357070923 0.3924132884 + 0.4787220955 0.4369412065 0.3936966956 + 0.4800123870 0.4381737113 0.3949801922 + 0.4813013077 0.4394079149 0.3962610066 + 0.4825831950 0.4406479895 0.3975391090 + 0.4838651121 0.4418880939 0.3988172114 + 0.4851444066 0.4431279004 0.4000911117 + 0.4864222109 0.4443677068 0.4013648033 + 0.4876995981 0.4456071854 0.4026384950 + 0.4889726043 0.4468455017 0.4039121866 + 0.4902456999 0.4480836987 0.4051857889 + 0.4915165007 0.4493125081 0.4064609110 + 0.4927848876 0.4505336881 0.4077362120 + 0.4940532148 0.4517548084 0.4090102911 + 0.4953213036 0.4529699087 0.4102798104 + 0.4965893030 0.4541845024 0.4115493000 + 0.4978551865 0.4553967118 0.4128125012 + 0.4991137087 0.4566031992 0.4140706956 + 0.5003721714 0.4578095973 0.4153288901 + 0.5016276836 0.4590105116 0.4165867865 + 0.5028806925 0.4602079988 0.4178445935 + 0.5041337013 0.4614056051 0.4191029072 + 0.5053765774 0.4626047909 0.4203636944 + 0.5066177249 0.4638043046 0.4216246009 + 0.5078586936 0.4650028944 0.4228836000 + 0.5090987086 0.4661977887 0.4241401851 + 0.5103386045 0.4673928022 0.4253968894 + 0.5115761161 0.4685871899 0.4266510904 + 0.5128089190 0.4697811902 0.4279040992 + 0.5140416026 0.4709751010 0.4291571975 + 0.5152726173 0.4721660018 0.4304040968 + 0.5165020823 0.4733552933 0.4316503108 + 0.5177316070 0.4745444953 0.4328961074 + 0.5189521909 0.4757269919 0.4341362119 + 0.5201696157 0.4769079983 0.4353761971 + 0.5213869810 0.4780888855 0.4366151094 + 0.5225998163 0.4792577922 0.4378497899 + 0.5238118768 0.4804260135 0.4390844107 + 0.5250239968 0.4815933108 0.4403162897 + 0.5262380242 0.4827486873 0.4415428042 + 0.5274519920 0.4839040935 0.4427692890 + 0.5286653042 0.4850594103 0.4439926147 + 0.5298718810 0.4862147868 0.4452114105 + 0.5310785174 0.4873701930 0.4464302063 + 0.5322846174 0.4885238111 0.4476467967 + 0.5334876776 0.4896706939 0.4488607943 + 0.5346907973 0.4908174872 0.4500747919 + 0.5358918905 0.4919629097 0.4512878060 + 0.5370826721 0.4931035042 0.4524998069 + 0.5382735133 0.4942440987 0.4537116885 + 0.5394629836 0.4953829944 0.4549197853 + 0.5406448841 0.4965158999 0.4561235011 + 0.5418269038 0.4976488054 0.4573271871 + 0.5430071950 0.4987801909 0.4585284889 + 0.5441747904 0.4999046922 0.4597266018 + 0.5453423858 0.5010291934 0.4609247148 + 0.5465098023 0.5021533966 0.4621219039 + 0.5476737022 0.5032743216 0.4633173048 + 0.5488376021 0.5043951869 0.4645127952 + 0.5500016212 0.5055161715 0.4657061100 + 0.5511487722 0.5066317916 0.4668928087 + 0.5522946715 0.5077472925 0.4680793881 + 0.5534405112 0.5088629127 0.4692654908 + 0.5545827150 0.5099775791 0.4704455137 + 0.5557237267 0.5110921264 0.4716255963 + 0.5568647981 0.5122066140 0.4728055894 + 0.5579981804 0.5133147240 0.4739825130 + 0.5591266751 0.5144196153 0.4751591980 + 0.5602552295 0.5155246258 0.4763357937 + 0.5613806248 0.5166273117 0.4775057137 + 0.5625013113 0.5177274942 0.4786727130 + 0.5636219978 0.5188276768 0.4798397124 + 0.5647404194 0.5199279785 0.4809997082 + 0.5658451915 0.5210285187 0.4821512997 + 0.5669500828 0.5221291184 0.4833030105 + 0.5680549145 0.5232295990 0.4844540060 + 0.5691527128 0.5243226886 0.4856021106 + 0.5702494979 0.5254151821 0.4867500961 + 0.5713462234 0.5265076756 0.4878982008 + 0.5724427104 0.5276014209 0.4890469909 + 0.5735387802 0.5286958218 0.4901959002 + 0.5746350288 0.5297902226 0.4913448989 + 0.5757296085 0.5308814049 0.4924908876 + 0.5768179297 0.5319637060 0.4936339855 + 0.5779061913 0.5330458879 0.4947771132 + 0.5789945722 0.5341281295 0.4959191084 + 0.5800734758 0.5352011919 0.4970515966 + 0.5811501741 0.5362728834 0.4981842041 + 0.5822268724 0.5373445749 0.4993166924 + 0.5832991004 0.5384107232 0.5004442930 + 0.5843636990 0.5394697189 0.5015696883 + 0.5854284167 0.5405287147 0.5026950240 + 0.5864930749 0.5415875912 0.5038194060 + 0.5875535011 0.5426415801 0.5049406290 + 0.5886135101 0.5436955094 0.5060616732 + 0.5896735787 0.5447493196 0.5071828961 + 0.5907285810 0.5458040833 0.5082973838 + 0.5917763710 0.5468599796 0.5094091892 + 0.5928242803 0.5479158163 0.5105209947 + 0.5938721895 0.5489715934 0.5116317272 + 0.5949155092 0.5500273705 0.5127375722 + 0.5959581733 0.5510830879 0.5138434768 + 0.5970007777 0.5521388054 0.5149493814 + 0.5980414748 0.5531933904 0.5160537958 + 0.5990775228 0.5542458892 0.5171571970 + 0.6001135111 0.5552983284 0.5182604790 + 0.6011496186 0.5563508272 0.5193638802 + 0.6021804810 0.5573993921 0.5204594731 + 0.6032091975 0.5584467053 0.5215548873 + 0.6042377949 0.5594938993 0.5226503015 + 0.6052662134 0.5605403781 0.5237420797 + 0.6062859893 0.5615794063 0.5248262882 + 0.6073058248 0.5626183152 0.5259104967 + 0.6083257198 0.5636572838 0.5269945860 + 0.6093432903 0.5646920800 0.5280733109 + 0.6103563905 0.5657204986 0.5291488171 + 0.6113694906 0.5667489171 0.5302242041 + 0.6123825908 0.5677773952 0.5312995911 + 0.6133952141 0.5688034892 0.5323687792 + 0.6144075990 0.5698282719 0.5334370732 + 0.6154199243 0.5708531737 0.5345054865 + 0.6164321899 0.5718780756 0.5355728865 + 0.6174443960 0.5728979111 0.5366333723 + 0.6184564233 0.5739170909 0.5376937985 + 0.6194685102 0.5749362707 0.5387542248 + 0.6204805970 0.5759549737 0.5398129225 + 0.6214926243 0.5769659281 0.5408675075 + 0.6225045919 0.5779768825 0.5419220924 + 0.6235166192 0.5789877772 0.5429766774 + 0.6245272756 0.5799971223 0.5440295935 + 0.6255301237 0.5809996724 0.5450801849 + 0.6265329719 0.5820024014 0.5461307168 + 0.6275358796 0.5830050111 0.5471813083 + 0.6285378933 0.5840060711 0.5482295156 + 0.6295374036 0.5850033760 0.5492755175 + 0.6305369139 0.5860008001 0.5503215790 + 0.6315364242 0.5869981050 0.5513677001 + 0.6325309277 0.5879935026 0.5524119735 + 0.6335136890 0.5889853239 0.5534552932 + 0.6344965100 0.5899770856 0.5544984937 + 0.6354792714 0.5909689069 0.5555416942 + 0.6364601254 0.5919576883 0.5565804243 + 0.6374369264 0.5929412246 0.5576159954 + 0.6384136081 0.5939248204 0.5586516261 + 0.6393904090 0.5949084163 0.5596871972 + 0.6403620839 0.5958868861 0.5607174039 + 0.6413215995 0.5968555808 0.5617434978 + 0.6422811151 0.5978243947 0.5627695918 + 0.6432405114 0.5987930894 0.5637956858 + 0.6441984773 0.5997601151 0.5648216009 + 0.6451513767 0.6007226706 0.5658472776 + 0.6461042762 0.6016852260 0.5668728948 + 0.6470571756 0.6026477218 0.5678985715 + 0.6480082870 0.6036084294 0.5689231753 + 0.6489437819 0.6045597792 0.5699462295 + 0.6498792171 0.6055111885 0.5709691048 + 0.6508147120 0.6064625978 0.5719920993 + 0.6517502069 0.6074140072 0.5730134845 + 0.6526796222 0.6083596945 0.5740293860 + 0.6536086798 0.6093053818 0.5750454068 + 0.6545377970 0.6102510095 0.5760613084 + 0.6554667950 0.6111965775 0.5770772099 + 0.6563835144 0.6121419072 0.5780894756 + 0.6572958231 0.6130869985 0.5791018009 + 0.6582080126 0.6140322089 0.5801141262 + 0.6591202021 0.6149773002 0.5811263919 + 0.6600306034 0.6159195900 0.5821347833 + 0.6609390974 0.6168593168 0.5831419230 + 0.6618475914 0.6177989841 0.5841491222 + 0.6627560854 0.6187387705 0.5851562023 + 0.6636629105 0.6196792722 0.5861611962 + 0.6645622253 0.6206220984 0.5871635079 + 0.6654614806 0.6215649247 0.5881658792 + 0.6663607955 0.6225078106 0.5891681910 + 0.6672601104 0.6234505773 0.5901697874 + 0.6681554914 0.6243839860 0.5911645889 + 0.6690502167 0.6253163815 0.5921593904 + 0.6699448824 0.6262487769 0.5931541920 + 0.6708396077 0.6271811724 0.5941489935 + 0.6717275977 0.6281095743 0.5951371193 + 0.6726074219 0.6290342212 0.5961226821 + 0.6734871864 0.6299588084 0.5971081853 + 0.6743670106 0.6308833957 0.5980938077 + 0.6752467752 0.6318072081 0.5990791917 + 0.6761245131 0.6327196956 0.6000642180 + 0.6770021915 0.6336323023 0.6010491848 + 0.6778799295 0.6345447898 0.6020342112 + 0.6787576079 0.6354573965 0.6030192971 + 0.6796311140 0.6363689899 0.6039971709 + 0.6805009842 0.6372799277 0.6049730778 + 0.6813709140 0.6381908059 0.6059491038 + 0.6822407246 0.6391018033 0.6069250703 + 0.6831105947 0.6400123835 0.6078994870 + 0.6839790940 0.6409156919 0.6088691950 + 0.6848475933 0.6418190002 0.6098389030 + 0.6857160926 0.6427223086 0.6108086705 + 0.6865844727 0.6436256766 0.6117783785 + 0.6874501705 0.6445271969 0.6127467155 + 0.6883115768 0.6454266906 0.6137142777 + 0.6891731024 0.6463261843 0.6146818995 + 0.6900346279 0.6472256780 0.6156495214 + 0.6908959746 0.6481251717 0.6166170239 + 0.6917508841 0.6490163803 0.6175795197 + 0.6926038861 0.6499059200 0.6185420156 + 0.6934568882 0.6507952809 0.6195045114 + 0.6943098903 0.6516848207 0.6204670072 + 0.6951622963 0.6525734067 0.6214286089 + 0.6960077286 0.6534568071 0.6223886013 + 0.6968531013 0.6543400884 0.6233485937 + 0.6976984739 0.6552234888 0.6243085861 + 0.6985439062 0.6561068296 0.6252685785 + 0.6993870735 0.6569851041 0.6262217164 + 0.7002261877 0.6578559279 0.6271703839 + 0.7010654211 0.6587265730 0.6281189919 + 0.7019044757 0.6595973969 0.6290677190 + 0.7027435899 0.6604681015 0.6300163269 + 0.7035796046 0.6613349915 0.6309583187 + 0.7044131756 0.6621994972 0.6318988800 + 0.7052468061 0.6630640030 0.6328393817 + 0.7060803771 0.6639283895 0.6337800026 + 0.7069140077 0.6647928953 0.6347205043 + 0.7077404857 0.6656516790 0.6356571913 + 0.7085642815 0.6665087938 0.6365938187 + 0.7093880773 0.6673660278 0.6375303864 + 0.7102118731 0.6682230830 0.6384670138 + 0.7110357881 0.6690803170 0.6394029856 + 0.7118616104 0.6699306965 0.6403322220 + 0.7126877904 0.6707803011 0.6412613988 + 0.7135139704 0.6716299057 0.6421905756 + 0.7143402100 0.6724795103 0.6431198120 + 0.7151665092 0.6733291149 0.6440477967 + 0.7159851789 0.6741706729 0.6449677944 + 0.7168028951 0.6750116944 0.6458877921 + 0.7176206708 0.6758527756 0.6468077898 + 0.7184385061 0.6766939163 0.6477277875 + 0.7192562819 0.6775348783 0.6486470103 + 0.7200725079 0.6783677936 0.6495617032 + 0.7208884954 0.6792001724 0.6504762769 + 0.7217044830 0.6800324917 0.6513909101 + 0.7225204110 0.6808648705 0.6523056030 + 0.7233363986 0.6816973090 0.6532199979 + 0.7241454124 0.6825289130 0.6541327238 + 0.7249531150 0.6833605170 0.6550455093 + 0.7257608771 0.6841921210 0.6559581757 + 0.7265685797 0.6850237250 0.6568709016 + 0.7273762822 0.6858553290 0.6577836275 + 0.7281739712 0.6866853833 0.6586869955 + 0.7289680243 0.6875150800 0.6595901251 + 0.7297620773 0.6883447766 0.6604931951 + 0.7305560708 0.6891744733 0.6613963842 + 0.7313501239 0.6900042295 0.6622995138 + 0.7321395278 0.6908270121 0.6631954908 + 0.7329254746 0.6916458011 0.6640897989 + 0.7337114811 0.6924644709 0.6649841070 + 0.7344974875 0.6932832003 0.6658784747 + 0.7352834940 0.6941019893 0.6667727828 + 0.7360627055 0.6949160099 0.6676589251 + 0.7368296981 0.6957229972 0.6685394049 + 0.7375966907 0.6965299845 0.6694198847 + 0.7383636832 0.6973370910 0.6703003049 + 0.7391306758 0.6981440783 0.6711807847 + 0.7398970723 0.6989498138 0.6720594168 + 0.7406572104 0.6997461915 0.6729338169 + 0.7414172292 0.7005426884 0.6738082767 + 0.7421773076 0.7013391256 0.6746826768 + 0.7429373860 0.7021355033 0.6755570769 + 0.7436974049 0.7029320002 0.6764315963 + 0.7444459200 0.7037230134 0.6772987247 + 0.7451906204 0.7045127153 0.6781656742 + 0.7459353209 0.7053024173 0.6790326238 + 0.7466799021 0.7060920000 0.6798995733 + 0.7474246025 0.7068817019 0.6807665825 + 0.7481679916 0.7076675892 0.6816290021 + 0.7489092946 0.7084479928 0.6824882030 + 0.7496504784 0.7092282772 0.6833475232 + 0.7503917217 0.7100086808 0.6842067242 + 0.7511329055 0.7107890844 0.6850659847 + 0.7518740892 0.7115694284 0.6859251857 + 0.7526044250 0.7123498917 0.6867846847 + 0.7533336878 0.7131304145 0.6876441836 + 0.7540628910 0.7139108777 0.6885036230 + 0.7547922134 0.7146914005 0.6893631220 + 0.7555214167 0.7154719234 0.6902225018 + 0.7562506199 0.7162482738 0.6910750866 + 0.7569795251 0.7170194983 0.6919236183 + 0.7577084899 0.7177907825 0.6927719712 + 0.7584375143 0.7185620070 0.6936205029 + 0.7591664791 0.7193332911 0.6944689751 + 0.7598955035 0.7201045156 0.6953169107 + 0.7606170177 0.7208759189 0.6961556077 + 0.7613371015 0.7216472030 0.6969943047 + 0.7620571852 0.7224184871 0.6978331208 + 0.7627773285 0.7231898904 0.6986718178 + 0.7634974122 0.7239611745 0.6995105147 + 0.7642151713 0.7247301936 0.7003443837 + 0.7649244070 0.7254924178 0.7011716962 + 0.7656335831 0.7262545228 0.7019990087 + 0.7663428783 0.7270166874 0.7028263211 + 0.7670521140 0.7277787924 0.7036535740 + 0.7677612901 0.7285410166 0.7044810057 + 0.7684661150 0.7293031812 0.7053043842 + 0.7691670060 0.7300654054 0.7061262727 + 0.7698678970 0.7308276296 0.7069482803 + 0.7705687284 0.7315897942 0.7077702880 + 0.7712696195 0.7323520184 0.7085922956 + 0.7719705105 0.7331141829 0.7094143033 + 0.7726665735 0.7338690162 0.7102290988 + 0.7733613253 0.7346221209 0.7110437751 + 0.7740560174 0.7353752255 0.7118585110 + 0.7747508287 0.7361282706 0.7126731873 + 0.7754455209 0.7368813753 0.7134879231 + 0.7761402130 0.7376344800 0.7143009901 + 0.7768254876 0.7383875847 0.7151066065 + 0.7775105238 0.7391408086 0.7159122229 + 0.7781955004 0.7398939133 0.7167177796 + 0.7788804173 0.7406470776 0.7175235152 + 0.7795653939 0.7414001822 0.7183290720 + 0.7802500725 0.7421522141 0.7191318870 + 0.7809323072 0.7428963184 0.7199280858 + 0.7816144824 0.7436404824 0.7207242846 + 0.7822967172 0.7443845868 0.7215204239 + 0.7829788923 0.7451288104 0.7223166227 + 0.7836611271 0.7458729148 0.7231128216 + 0.7843412757 0.7466160059 0.7239063978 + 0.7850098014 0.7473546267 0.7246955037 + 0.7856782079 0.7480933070 0.7254844904 + 0.7863466740 0.7488319278 0.7262734771 + 0.7870151997 0.7495706081 0.7270625234 + 0.7876836061 0.7503092289 0.7278516293 + 0.7883511186 0.7510460019 0.7286369205 + 0.7890127897 0.7517744899 0.7294149995 + 0.7896745801 0.7525030971 0.7301930785 + 0.7903363109 0.7532315850 0.7309712172 + 0.7909979820 0.7539601922 0.7317492962 + 0.7916597128 0.7546886802 0.7325273752 + 0.7923207879 0.7554165721 0.7333045006 + 0.7929738760 0.7561392784 0.7340788841 + 0.7936270237 0.7568619847 0.7348533273 + 0.7942801118 0.7575846910 0.7356275916 + 0.7949333191 0.7583073974 0.7364019752 + 0.7955864072 0.7590301037 0.7371764183 + 0.7962394953 0.7597528100 0.7379497886 + 0.7968860865 0.7604665160 0.7387167215 + 0.7975323200 0.7611799240 0.7394834757 + 0.7981784940 0.7618933916 0.7402504086 + 0.7988247275 0.7626067996 0.7410172820 + 0.7994709015 0.7633203268 0.7417842150 + 0.8001170754 0.7640337944 0.7425510883 + 0.8007584810 0.7647390962 0.7433149815 + 0.8013982177 0.7654420137 0.7440785766 + 0.8020377755 0.7661449909 0.7448422909 + 0.8026775122 0.7668480277 0.7456058860 + 0.8033171892 0.7675510049 0.7463694811 + 0.8039568067 0.7682539821 0.7471331954 + 0.8045893908 0.7689520717 0.7478901148 + 0.8052138090 0.7696456909 0.7486435175 + 0.8058382869 0.7703393102 0.7493969202 + 0.8064628243 0.7710329294 0.7501503825 + 0.8070873022 0.7717264891 0.7509037852 + 0.8077117801 0.7724201083 0.7516571879 + 0.8083351851 0.7731121182 0.7524083853 + 0.8089513183 0.7737967968 0.7531548738 + 0.8095672727 0.7744815946 0.7539014220 + 0.8101834059 0.7751662731 0.7546479106 + 0.8107994795 0.7758510709 0.7553943992 + 0.8114156127 0.7765358090 0.7561408877 + 0.8120316863 0.7772204876 0.7568873763 + 0.8126400709 0.7778990865 0.7576224804 + 0.8132457137 0.7785758972 0.7583565116 + 0.8138511777 0.7792527080 0.7590904236 + 0.8144568205 0.7799293995 0.7598243952 + 0.8150624037 0.7806062102 0.7605584264 + 0.8156679869 0.7812829018 0.7612923980 + 0.8162711859 0.7819581032 0.7620232105 + 0.8168674707 0.7826291919 0.7627500296 + 0.8174636960 0.7833003998 0.7634767890 + 0.8180599809 0.7839716077 0.7642034888 + 0.8186563253 0.7846428156 0.7649303079 + 0.8192526102 0.7853139043 0.7656571269 + 0.8198487759 0.7859851122 0.7663838863 + 0.8204402924 0.7866560817 0.7671036124 + 0.8210297227 0.7873269916 0.7678225040 + 0.8216190934 0.7879980206 0.7685413957 + 0.8222085834 0.7886688709 0.7692602873 + 0.8227980137 0.7893397808 0.7699791789 + 0.8233875036 0.7900106907 0.7706980705 + 0.8239762187 0.7906810045 0.7714146972 + 0.8245580792 0.7913463712 0.7721254230 + 0.8251398802 0.7920119166 0.7728360295 + 0.8257218003 0.7926774025 0.7735465765 + 0.8263037205 0.7933428288 0.7742571831 + 0.8268855214 0.7940083146 0.7749677896 + 0.8274673820 0.7946738005 0.7756783962 + 0.8280459046 0.7953346968 0.7763838172 + 0.8286194801 0.7959898114 0.7770854831 + 0.8291931152 0.7966449857 0.7777872086 + 0.8297666907 0.7973002195 0.7784888744 + 0.8303403258 0.7979552746 0.7791904807 + 0.8309139013 0.7986105084 0.7798922062 + 0.8314874768 0.7992656827 0.7805938721 + 0.8320547938 0.7999172211 0.7812880278 + 0.8326187134 0.8005670905 0.7819803953 + 0.8331826925 0.8012170196 0.7826728821 + 0.8337466121 0.8018668294 0.7833653092 + 0.8343105912 0.8025166988 0.7840577960 + 0.8348745704 0.8031666279 0.7847502232 + 0.8354384899 0.8038163781 0.7854425907 + 0.8359956145 0.8044540882 0.7861267924 + 0.8365511894 0.8050895929 0.7868108153 + 0.8371068239 0.8057252169 0.7874947786 + 0.8376625180 0.8063607216 0.7881788015 + 0.8382180929 0.8069962263 0.7888628244 + 0.8387737274 0.8076316714 0.7895467877 + 0.8393293023 0.8082672954 0.7902303934 + 0.8398807049 0.8088986278 0.7909095287 + 0.8404316902 0.8095297217 0.7915886045 + 0.8409826756 0.8101608157 0.7922676206 + 0.8415337205 0.8107919097 0.7929466963 + 0.8420847058 0.8114230037 0.7936257720 + 0.8426356912 0.8120540977 0.7943047881 + 0.8431866765 0.8126851916 0.7949832082 + 0.8437362909 0.8133051991 0.7956548929 + 0.8442857862 0.8139246702 0.7963265777 + 0.8448352814 0.8145442009 0.7969983816 + 0.8453847766 0.8151637912 0.7976701260 + 0.8459343910 0.8157833219 0.7983418107 + 0.8464838862 0.8164027929 0.7990134954 + 0.8470333815 0.8170223236 0.7996851802 + 0.8475741148 0.8176347017 0.8003575802 + 0.8481134772 0.8182463050 0.8010299802 + 0.8486528993 0.8188579082 0.8017024994 + 0.8491923213 0.8194695115 0.8023748994 + 0.8497316837 0.8200811148 0.8030472994 + 0.8502712250 0.8206927180 0.8037198186 + 0.8508105874 0.8213043213 0.8043922186 + 0.8513522744 0.8219084144 0.8050565720 + 0.8518949151 0.8225101233 0.8057200909 + 0.8524376154 0.8231118917 0.8063836098 + 0.8529801965 0.8237137198 0.8070471287 + 0.8535227776 0.8243154287 0.8077105284 + 0.8540654182 0.8249171972 0.8083739877 + 0.8546079993 0.8255189061 0.8090375066 + 0.8551477790 0.8261185288 0.8096969724 + 0.8556851149 0.8267164230 0.8103548288 + 0.8562222719 0.8273143172 0.8110125065 + 0.8567596078 0.8279122114 0.8116701841 + 0.8572968245 0.8285101056 0.8123279214 + 0.8578341007 0.8291079998 0.8129855990 + 0.8583713174 0.8297058940 0.8136432767 + 0.8589066863 0.8303020000 0.8142979741 + 0.8594359756 0.8308932185 0.8149477839 + 0.8599653244 0.8314844966 0.8155977130 + 0.8604946136 0.8320757151 0.8162475228 + 0.8610237837 0.8326669931 0.8168973923 + 0.8615530729 0.8332582116 0.8175472021 + 0.8620824218 0.8338493705 0.8181970716 + 0.8626117110 0.8344407082 0.8188468814 + 0.8631411791 0.8350260854 0.8194969296 + 0.8636707067 0.8356109262 0.8201467991 + 0.8642001748 0.8361955881 0.8207967877 + 0.8647297025 0.8367804289 0.8214467764 + 0.8652591705 0.8373650908 0.8220967054 + 0.8657886982 0.8379498124 0.8227466941 + 0.8663182259 0.8385345936 0.8233966231 + 0.8668442965 0.8391172886 0.8240422010 + 0.8673655987 0.8396975994 0.8246843815 + 0.8678870201 0.8402777910 0.8253266215 + 0.8684083223 0.8408579826 0.8259688020 + 0.8689296842 0.8414382935 0.8266109824 + 0.8694509864 0.8420184851 0.8272532225 + 0.8699724078 0.8425986767 0.8278954029 + 0.8704937100 0.8431789279 0.8285375834 + 0.8710048795 0.8437566757 0.8291798830 + 0.8715147972 0.8443341851 0.8298223019 + 0.8720247149 0.8449115753 0.8304646015 + 0.8725346923 0.8454890847 0.8311069012 + 0.8730446100 0.8460665941 0.8317492008 + 0.8735545278 0.8466441035 0.8323916197 + 0.8740643859 0.8472216129 0.8330339193 + 0.8745729923 0.8477984071 0.8336730003 + 0.8750782013 0.8483735919 0.8343076706 + 0.8755834103 0.8489488959 0.8349422812 + 0.8760886192 0.8495240808 0.8355768919 + 0.8765938282 0.8500993252 0.8362116218 + 0.8770989776 0.8506745100 0.8368461728 + 0.8776041865 0.8512498140 0.8374807835 + 0.8781093955 0.8518249989 0.8381155133 + 0.8786090016 0.8523975015 0.8387501836 + 0.8791047931 0.8529682755 0.8393849134 + 0.8796005249 0.8535389900 0.8400195837 + 0.8800961971 0.8541098237 0.8406543732 + 0.8805919886 0.8546805978 0.8412891030 + 0.8810877204 0.8552513719 0.8419238925 + 0.8815835118 0.8558220863 0.8425586224 + 0.8820791841 0.8563929200 0.8431932926 + 0.8825731277 0.8569598198 0.8438208103 + 0.8830664754 0.8575260043 0.8444479108 + 0.8835598826 0.8580921292 0.8450748920 + 0.8840534091 0.8586583138 0.8457019925 + 0.8845468163 0.8592243791 0.8463289738 + 0.8850402236 0.8597906232 0.8469560146 + 0.8855336905 0.8603568077 0.8475831151 + 0.8860270977 0.8609228730 0.8482097983 + 0.8865162134 0.8614861965 0.8488330245 + 0.8870049715 0.8620492816 0.8494560719 + 0.8874937892 0.8626124859 0.8500791788 + 0.8879824877 0.8631756902 0.8507024050 + 0.8884713054 0.8637387753 0.8513255119 + 0.8889601231 0.8643019795 0.8519486189 + 0.8894488811 0.8648651242 0.8525717854 + 0.8899376988 0.8654280901 0.8531945944 + 0.8904203176 0.8659834862 0.8538153768 + 0.8909028172 0.8665388227 0.8544362187 + 0.8913853765 0.8670940995 0.8550570011 + 0.8918679953 0.8676494956 0.8556777835 + 0.8923504949 0.8682047725 0.8562985063 + 0.8928331137 0.8687602282 0.8569192886 + 0.8933156133 0.8693155050 0.8575400710 + 0.8937981725 0.8698707819 0.8581606150 + 0.8942810297 0.8704199791 0.8587781191 + 0.8947638273 0.8709689975 0.8593956232 + 0.8952466846 0.8715180755 0.8600131869 + 0.8957294822 0.8720670938 0.8606306911 + 0.8962123990 0.8726161718 0.8612481952 + 0.8966951966 0.8731653094 0.8618656993 + 0.8971779943 0.8737143278 0.8624832034 + 0.8976609111 0.8742634058 0.8631008267 + 0.8981388211 0.8748087883 0.8637112975 + 0.8986158967 0.8753535748 0.8643215895 + 0.8990929723 0.8758984208 0.8649318814 + 0.8995701075 0.8764432073 0.8655421138 + 0.9000471234 0.8769881129 0.8661524057 + 0.9005241990 0.8775328994 0.8667626977 + 0.9010012746 0.8780776858 0.8673729897 + 0.9014782906 0.8786225915 0.8679832816 + 0.9019498825 0.8791623712 0.8685910106 + 0.9024183154 0.8796998262 0.8691979051 + 0.9028868079 0.8802371025 0.8698047996 + 0.9033551812 0.8807744980 0.8704116940 + 0.9038236737 0.8813118935 0.8710185289 + 0.9042921066 0.8818492293 0.8716254234 + 0.9047604799 0.8823866248 0.8722323179 + 0.9052289724 0.8829239011 0.8728392124 + 0.9056965113 0.8834607005 0.8734431267 + 0.9061623216 0.8839964867 0.8740432858 + 0.9066281915 0.8845322132 0.8746436238 + 0.9070940018 0.8850679994 0.8752437830 + 0.9075598121 0.8856037855 0.8758441210 + 0.9080256224 0.8861395121 0.8764442801 + 0.9084914923 0.8866752982 0.8770446181 + 0.9089573026 0.8872110248 0.8776447773 + 0.9094231129 0.8877468109 0.8782445788 + 0.9098821282 0.8882740736 0.8788375258 + 0.9103407860 0.8888012767 0.8794304132 + 0.9107996225 0.8893284202 0.8800233006 + 0.9112582803 0.8898556232 0.8806161284 + 0.9117169976 0.8903827071 0.8812090158 + 0.9121757150 0.8909099102 0.8818019032 + 0.9126343727 0.8914369941 0.8823947906 + 0.9130930901 0.8919641972 0.8829876184 + 0.9135512710 0.8924918771 0.8835765123 + 0.9140090942 0.8930202723 0.8841624260 + 0.9144667983 0.8935487270 0.8847482800 + 0.9149245024 0.8940771222 0.8853343129 + 0.9153823256 0.8946055174 0.8859202266 + 0.9158400297 0.8951339126 0.8865060806 + 0.9162976742 0.8956621885 0.8870919943 + 0.9167554975 0.8961905837 0.8876780272 + 0.9172132015 0.8967189789 0.8882638216 + 0.9176644087 0.8972421288 0.8888434768 + 0.9181149006 0.8977648020 0.8894230723 + 0.9185655117 0.8982874751 0.8900027871 + 0.9190160036 0.8988103271 0.8905823827 + 0.9194664955 0.8993330002 0.8911620975 + 0.9199171066 0.8998556733 0.8917416930 + 0.9203675985 0.9003784060 0.8923214078 + 0.9208182096 0.9009010792 0.8929010034 + 0.9212675095 0.9014238715 0.8934786916 + 0.9217131734 0.9019467831 0.8940525055 + 0.9221590161 0.9024696946 0.8946262002 + 0.9226046801 0.9029926062 0.8952000141 + 0.9230504036 0.9035155177 0.8957738280 + 0.9234961271 0.9040384293 0.8963475823 + 0.9239419103 0.9045612812 0.8969212770 + 0.9243875742 0.9050841928 0.8974950910 + 0.9248332977 0.9056071043 0.8980689049 + 0.9252752066 0.9061285257 0.8986396790 + 0.9257131219 0.9066482782 0.8992083073 + 0.9261509776 0.9071682096 0.8997768164 + 0.9265888929 0.9076880813 0.9003453851 + 0.9270266891 0.9082080126 0.9009140134 + 0.9274646044 0.9087278247 0.9014825821 + 0.9279025197 0.9092476964 0.9020512104 + 0.9283403754 0.9097676277 0.9026197791 + 0.9287782907 0.9102874994 0.9031882882 + 0.9292140007 0.9107999802 0.9037535191 + 0.9296483994 0.9113087058 0.9043173194 + 0.9300829172 0.9118173122 0.9048811793 + 0.9305173159 0.9123259783 0.9054449797 + 0.9309517741 0.9128345847 0.9060088992 + 0.9313861728 0.9133433104 0.9065728188 + 0.9318206906 0.9138519168 0.9071366191 + 0.9322550893 0.9143605828 0.9077004790 + 0.9326896071 0.9148693085 0.9082642794 + 0.9331244230 0.9153769016 0.9088249803 + 0.9335594177 0.9158840775 0.9093847871 + 0.9339944124 0.9163913131 0.9099445939 + 0.9344294071 0.9168984294 0.9105042815 + 0.9348644018 0.9174056053 0.9110640883 + 0.9352993965 0.9179127812 0.9116238952 + 0.9357343912 0.9184200168 0.9121837020 + 0.9361693859 0.9189271927 0.9127433896 + 0.9366044998 0.9194344282 0.9133031964 + 0.9370371103 0.9199361205 0.9138603806 + 0.9374685287 0.9204351902 0.9144166708 + 0.9378998876 0.9209342003 0.9149729013 + 0.9383313060 0.9214332104 0.9155290723 + 0.9387627244 0.9219322205 0.9160854220 + 0.9391940832 0.9224311709 0.9166415930 + 0.9396255016 0.9229301810 0.9171978831 + 0.9400569201 0.9234291911 0.9177541137 + 0.9404882789 0.9239282012 0.9183102846 + 0.9409155250 0.9244257212 0.9188649058 + 0.9413387179 0.9249218106 0.9194182158 + 0.9417619705 0.9254179001 0.9199715257 + 0.9421852231 0.9259139895 0.9205247760 + 0.9426084161 0.9264100790 0.9210780859 + 0.9430316091 0.9269062281 0.9216313958 + 0.9434548020 0.9274023175 0.9221848249 + 0.9438781142 0.9278984070 0.9227380753 + 0.9443013072 0.9283944964 0.9232913852 + 0.9447239041 0.9288893938 0.9238408208 + 0.9451450109 0.9293813109 0.9243822098 + 0.9455661178 0.9298732281 0.9249237180 + 0.9459871054 0.9303650856 0.9254652262 + 0.9464082122 0.9308570027 0.9260066748 + 0.9468291998 0.9313489199 0.9265481830 + 0.9472503066 0.9318407774 0.9270896912 + 0.9476714134 0.9323326945 0.9276311994 + 0.9480924010 0.9328246117 0.9281727076 + 0.9485135078 0.9333165288 0.9287142158 + 0.9489266276 0.9338006973 0.9292520881 + 0.9493390918 0.9342843890 0.9297899008 + 0.9497516155 0.9347680807 0.9303277731 + 0.9501640797 0.9352517724 0.9308655858 + 0.9505766034 0.9357355833 0.9314035177 + 0.9509891272 0.9362192750 0.9319412708 + 0.9514014721 0.9367030263 0.9324790835 + 0.9518139958 0.9371867180 0.9330170155 + 0.9522265196 0.9376704097 0.9335548282 + 0.9526365995 0.9381523132 0.9340875149 + 0.9530441761 0.9386324883 0.9346156716 + 0.9534518123 0.9391127229 0.9351440072 + 0.9538593888 0.9395927787 0.9356722236 + 0.9542670250 0.9400730133 0.9362003803 + 0.9546746016 0.9405531287 0.9367287159 + 0.9550821781 0.9410333037 0.9372568727 + 0.9554898143 0.9415134192 0.9377852082 + 0.9558972716 0.9419935942 0.9383134246 + 0.9563049078 0.9424737096 0.9388415813 + 0.9567065835 0.9429457784 0.9393643737 + 0.9571079016 0.9434173703 0.9398869872 + 0.9575092196 0.9438890219 0.9404097199 + 0.9579104185 0.9443606138 0.9409322739 + 0.9583116770 0.9448322058 0.9414550066 + 0.9587129951 0.9453037977 0.9419776797 + 0.9591141939 0.9457753897 0.9425002933 + 0.9595155120 0.9462471008 0.9430230260 + 0.9599167705 0.9467186928 0.9435455799 + 0.9603134990 0.9471881986 0.9440639019 + 0.9606987238 0.9476525784 0.9445734024 + 0.9610838294 0.9481170177 0.9450829029 + 0.9614689946 0.9485813975 0.9455924034 + 0.9618542194 0.9490458965 0.9461019039 + 0.9622393250 0.9495102763 0.9466114044 + 0.9626244903 0.9499747157 0.9471209049 + 0.9630097151 0.9504390955 0.9476304054 + 0.9633948803 0.9509035945 0.9481399059 + 0.9637799859 0.9513679743 0.9486494064 + 0.9641588926 0.9518290758 0.9491565824 + 0.9645323157 0.9522874951 0.9496620297 + 0.9649056196 0.9527457952 0.9501674175 + 0.9652789831 0.9532042146 0.9506728053 + 0.9656522870 0.9536625147 0.9511781931 + 0.9660257101 0.9541208744 0.9516835809 + 0.9663990140 0.9545791745 0.9521890283 + 0.9667723775 0.9550374746 0.9526944160 + 0.9671456814 0.9554958940 0.9531999230 + 0.9675191045 0.9559541941 0.9537053108 + 0.9678881764 0.9564117789 0.9542062283 + 0.9682552218 0.9568691254 0.9547054172 + 0.9686222076 0.9573262930 0.9552046061 + 0.9689893126 0.9577835798 0.9557037950 + 0.9693562984 0.9582408071 0.9562029839 + 0.9697232842 0.9586980939 0.9567021728 + 0.9700903296 0.9591553211 0.9572014213 + 0.9704573750 0.9596126080 0.9577006102 + 0.9708244205 0.9600697756 0.9581997991 + 0.9711914062 0.9605271220 0.9586989880 + 0.9715511799 0.9609804749 0.9591947794 + 0.9719079137 0.9614323974 0.9596894979 + 0.9722645283 0.9618843198 0.9601842165 + 0.9726212025 0.9623361230 0.9606788754 + 0.9729778767 0.9627879858 0.9611735940 + 0.9733346105 0.9632399082 0.9616683125 + 0.9736912847 0.9636917710 0.9621629715 + 0.9740480185 0.9641436934 0.9626576900 + 0.9744045734 0.9645954967 0.9631524086 + 0.9747613072 0.9650474191 0.9636471272 + 0.9751121998 0.9654958248 0.9641352892 + 0.9754595757 0.9659423232 0.9646201730 + 0.9758070111 0.9663887024 0.9651051164 + 0.9761543870 0.9668352008 0.9655900002 + 0.9765018225 0.9672815800 0.9660750031 + 0.9768491983 0.9677280784 0.9665598869 + 0.9771965146 0.9681745172 0.9670447707 + 0.9775438905 0.9686208963 0.9675297141 + 0.9778913260 0.9690673947 0.9680147171 + 0.9782387018 0.9695137739 0.9684996009 + 0.9785814285 0.9699571133 0.9689792991 + 0.9789180160 0.9703966975 0.9694532752 + 0.9792547226 0.9708362222 0.9699273109 + 0.9795914292 0.9712756872 0.9704012871 + 0.9799280763 0.9717152119 0.9708753228 + 0.9802647829 0.9721547961 0.9713492990 + 0.9806014895 0.9725943208 0.9718232751 + 0.9809380770 0.9730337858 0.9722973108 + 0.9812747836 0.9734733105 0.9727712870 + 0.9816114902 0.9739128947 0.9732453823 + 0.9819465280 0.9743511081 0.9737172723 + 0.9822719097 0.9747827053 0.9741798043 + 0.9825971723 0.9752143025 0.9746423960 + 0.9829226136 0.9756458998 0.9751049280 + 0.9832479954 0.9760774970 0.9755674005 + 0.9835733175 0.9765089750 0.9760299921 + 0.9838986993 0.9769405723 0.9764925241 + 0.9842240810 0.9773722291 0.9769549966 + 0.9845495224 0.9778038263 0.9774175286 + 0.9848747849 0.9782354236 0.9778801203 + 0.9852002263 0.9786669016 0.9783425927 + 0.9855154157 0.9790912867 0.9787945747 + 0.9858270288 0.9795132279 0.9792435169 + 0.9861387014 0.9799351096 0.9796923995 + 0.9864503741 0.9803571105 0.9801412225 + 0.9867621064 0.9807789922 0.9805901051 + 0.9870737791 0.9812008739 0.9810389876 + 0.9873855114 0.9816228747 0.9814878702 + 0.9876971245 0.9820448160 0.9819366932 + 0.9880087972 0.9824666977 0.9823855758 + 0.9883205295 0.9828886986 0.9828345180 + 0.9886283278 0.9833080173 0.9832788706 + 0.9889227748 0.9837185740 0.9837099910 + 0.9892172813 0.9841291904 0.9841409922 + 0.9895117879 0.9845399261 0.9845719934 + 0.9898062944 0.9849504828 0.9850029945 + 0.9901008010 0.9853610992 0.9854339957 + 0.9903953075 0.9857717156 0.9858649969 + 0.9906898141 0.9861822724 0.9862959981 + 0.9909843206 0.9865928888 0.9867269993 + 0.9912788272 0.9870036244 0.9871580005 + 0.9915732741 0.9874141812 0.9875890017 + 0.9918602109 0.9878157973 0.9880052805 + 0.9921419024 0.9882115126 0.9884120822 + 0.9924237132 0.9886071086 0.9888188839 + 0.9927054048 0.9890027046 0.9892256856 + 0.9929870963 0.9893983006 0.9896324873 + 0.9932689071 0.9897938967 0.9900392890 + 0.9935505986 0.9901894927 0.9904460907 + 0.9938322902 0.9905850887 0.9908528924 + 0.9941141009 0.9909806848 0.9912596941 + 0.9943957925 0.9913762808 0.9916664958 + 0.9946774840 0.9917719960 0.9920732975 + 0.9949371219 0.9921513796 0.9924513102 + 0.9951925874 0.9925280213 0.9928246140 + 0.9954481125 0.9929047227 0.9931977987 + 0.9957035780 0.9932813048 0.9935709834 + 0.9959591031 0.9936578870 0.9939442277 + 0.9962146282 0.9940345287 0.9943174124 + 0.9964702129 0.9944111109 0.9946905971 + 0.9967256784 0.9947876930 0.9950639009 + 0.9969812036 0.9951642752 0.9954370856 + 0.9972367287 0.9955409169 0.9958102703 + 0.9974921942 0.9959174991 0.9961835146 + 0.9977207184 0.9962887168 0.9965307713 + 0.9979485869 0.9966598749 0.9968776703 + 0.9981765151 0.9970309734 0.9972246289 + 0.9984045029 0.9974020720 0.9975715280 + 0.9986323714 0.9977731705 0.9979184866 + 0.9988602996 0.9981443882 0.9982653856 + 0.9990882874 0.9985154867 0.9986122847 + 0.9993162155 0.9988865852 0.9989591837 + 0.9995440841 0.9992576838 0.9993062019 + 0.9997720718 0.9996289015 0.9996531010 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kaf-2001.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kaf-2001.spi1d index 5eb8e03b..5642ed8b 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kaf-2001.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kaf-2001.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 1 -{ - 0.0000000000 - 0.0007931371 - 0.0016355170 - 0.0025503801 - 0.0035146240 - 0.0045273229 - 0.0055715218 - 0.0066442778 - 0.0077339248 - 0.0088365739 - 0.0099448124 - 0.0110545401 - 0.0121633904 - 0.0132660801 - 0.0143607901 - 0.0154447202 - 0.0165135600 - 0.0175563600 - 0.0185688492 - 0.0195335001 - 0.0204569604 - 0.0213513691 - 0.0222338792 - 0.0231126193 - 0.0239920896 - 0.0248705894 - 0.0257443301 - 0.0266080908 - 0.0274574608 - 0.0282867905 - 0.0290999208 - 0.0299087707 - 0.0307223909 - 0.0315502807 - 0.0323930904 - 0.0332537815 - 0.0341344401 - 0.0350381099 - 0.0359668396 - 0.0369246788 - 0.0379154012 - 0.0389400795 - 0.0400040112 - 0.0411124304 - 0.0422693789 - 0.0434693992 - 0.0447056293 - 0.0459665395 - 0.0472429208 - 0.0485244282 - 0.0498028807 - 0.0510769486 - 0.0523513183 - 0.0536344387 - 0.0549293309 - 0.0562351793 - 0.0575465411 - 0.0588550903 - 0.0601528510 - 0.0614360608 - 0.0626917332 - 0.0639152676 - 0.0650884286 - 0.0662079304 - 0.0672680065 - 0.0682774335 - 0.0692317113 - 0.0701425374 - 0.0710024387 - 0.0718212798 - 0.0725976974 - 0.0733437091 - 0.0740561932 - 0.0747447014 - 0.0754096508 - 0.0760633573 - 0.0767156705 - 0.0773765966 - 0.0780583993 - 0.0787628815 - 0.0794979781 - 0.0802626163 - 0.0810654610 - 0.0819049180 - 0.0827899724 - 0.0837192386 - 0.0847032070 - 0.0857442319 - 0.0868611634 - 0.0880450904 - 0.0892995670 - 0.0906111673 - 0.0919737294 - 0.0933712497 - 0.0947946832 - 0.0962312222 - 0.0976707786 - 0.0991016179 - 0.1005088016 - 0.1018795967 - 0.1031991988 - 0.1044576988 - 0.1056412980 - 0.1067342982 - 0.1077463999 - 0.1087004021 - 0.1096137986 - 0.1104959026 - 0.1113545969 - 0.1121921018 - 0.1130077988 - 0.1137992963 - 0.1145685986 - 0.1153163984 - 0.1160470024 - 0.1167626977 - 0.1174698994 - 0.1181747988 - 0.1188858002 - 0.1196158975 - 0.1203754023 - 0.1211868003 - 0.1220536008 - 0.1229866967 - 0.1239702031 - 0.1250043064 - 0.1260707974 - 0.1271626055 - 0.1282667071 - 0.1293743998 - 0.1304752976 - 0.1315566003 - 0.1326127052 - 0.1336387992 - 0.1346348971 - 0.1355908066 - 0.1365199983 - 0.1374216974 - 0.1383025050 - 0.1391607970 - 0.1400025934 - 0.1408285946 - 0.1416438073 - 0.1424539983 - 0.1432684958 - 0.1440957040 - 0.1449375004 - 0.1457988024 - 0.1466775984 - 0.1475750059 - 0.1484851986 - 0.1494164020 - 0.1503666043 - 0.1513417959 - 0.1523495018 - 0.1534101069 - 0.1545234025 - 0.1556780040 - 0.1568565965 - 0.1580476016 - 0.1592365056 - 0.1604065001 - 0.1615456045 - 0.1626373976 - 0.1636672020 - 0.1646257043 - 0.1655281931 - 0.1663973033 - 0.1672482938 - 0.1680918038 - 0.1689291000 - 0.1697597057 - 0.1705808938 - 0.1713818014 - 0.1721594036 - 0.1729159951 - 0.1736537963 - 0.1743783057 - 0.1750947982 - 0.1758113056 - 0.1765365005 - 0.1772823930 - 0.1780716032 - 0.1789128035 - 0.1798152030 - 0.1807664931 - 0.1817689985 - 0.1828158051 - 0.1839095950 - 0.1850427985 - 0.1862190962 - 0.1874303073 - 0.1886713058 - 0.1899241060 - 0.1911779046 - 0.1924244016 - 0.1936572939 - 0.1948782951 - 0.1960881054 - 0.1972880065 - 0.1984702945 - 0.1996252984 - 0.2007281035 - 0.2017628998 - 0.2027094960 - 0.2036039978 - 0.2044609040 - 0.2053059042 - 0.2061520070 - 0.2070064992 - 0.2078751028 - 0.2087562978 - 0.2096406966 - 0.2105236948 - 0.2113939971 - 0.2122493982 - 0.2130886018 - 0.2139182985 - 0.2147479951 - 0.2155908048 - 0.2164690942 - 0.2174035013 - 0.2184235007 - 0.2195228934 - 0.2206815928 - 0.2218745053 - 0.2230865955 - 0.2243078947 - 0.2255353034 - 0.2267691046 - 0.2280129045 - 0.2292630970 - 0.2305141985 - 0.2317567021 - 0.2329764068 - 0.2341637015 - 0.2353121042 - 0.2364190072 - 0.2374805957 - 0.2384950072 - 0.2394600958 - 0.2403741032 - 0.2412285060 - 0.2420233935 - 0.2427742034 - 0.2434999943 - 0.2442153990 - 0.2449319065 - 0.2456566989 - 0.2463952005 - 0.2471493930 - 0.2479220033 - 0.2487130016 - 0.2495251000 - 0.2503561974 - 0.2512075901 - 0.2520760000 - 0.2529664040 - 0.2538853884 - 0.2548437119 - 0.2558579147 - 0.2569623888 - 0.2581419051 - 0.2593897879 - 0.2606801093 - 0.2619997859 - 0.2633245885 - 0.2646321058 - 0.2659066021 - 0.2671222091 - 0.2682681084 - 0.2693215907 - 0.2703152001 - 0.2712577879 - 0.2721709907 - 0.2730554938 - 0.2739250958 - 0.2747743130 - 0.2756054997 - 0.2764146030 - 0.2772051990 - 0.2779743075 - 0.2787271142 - 0.2794634998 - 0.2801890969 - 0.2809084058 - 0.2816320062 - 0.2823747098 - 0.2831506133 - 0.2839820981 - 0.2848649919 - 0.2858025134 - 0.2867873013 - 0.2878226936 - 0.2889021933 - 0.2900269926 - 0.2911930978 - 0.2923978865 - 0.2936300933 - 0.2948761880 - 0.2961241901 - 0.2973628938 - 0.2985885143 - 0.2998012006 - 0.3010047972 - 0.3021970987 - 0.3033716977 - 0.3045161963 - 0.3056159914 - 0.3066501915 - 0.3076123893 - 0.3085128963 - 0.3093748093 - 0.3102149069 - 0.3110443950 - 0.3118737936 - 0.3127109110 - 0.3135589957 - 0.3144189119 - 0.3152843118 - 0.3161520958 - 0.3170174956 - 0.3178755939 - 0.3187314868 - 0.3195927143 - 0.3204680979 - 0.3213739991 - 0.3223293126 - 0.3233569860 - 0.3244507909 - 0.3255977035 - 0.3267756999 - 0.3279756904 - 0.3291803002 - 0.3303748071 - 0.3315485120 - 0.3326843977 - 0.3337798119 - 0.3348152936 - 0.3358032107 - 0.3367348909 - 0.3376190960 - 0.3384444118 - 0.3392320871 - 0.3399783969 - 0.3406944871 - 0.3413780928 - 0.3420386910 - 0.3426808119 - 0.3433162868 - 0.3439547122 - 0.3446053863 - 0.3452802002 - 0.3459802866 - 0.3467136025 - 0.3474789858 - 0.3482855856 - 0.3491322994 - 0.3500328958 - 0.3509812057 - 0.3519834876 - 0.3530359864 - 0.3541389108 - 0.3552891910 - 0.3564738035 - 0.3576740026 - 0.3588739932 - 0.3600645959 - 0.3612402976 - 0.3623969853 - 0.3635292947 - 0.3646346033 - 0.3657099903 - 0.3667556047 - 0.3677704930 - 0.3687556982 - 0.3697125912 - 0.3706437945 - 0.3715595901 - 0.3724690080 - 0.3733772933 - 0.3742752969 - 0.3751674891 - 0.3760527074 - 0.3769319057 - 0.3778083026 - 0.3786880076 - 0.3795790076 - 0.3804906011 - 0.3814407885 - 0.3824321926 - 0.3834637105 - 0.3845176101 - 0.3855882883 - 0.3866708875 - 0.3877635002 - 0.3888686001 - 0.3899909854 - 0.3911277056 - 0.3922775984 - 0.3934313059 - 0.3945826888 - 0.3957261145 - 0.3968504965 - 0.3979482055 - 0.3990120888 - 0.4000467956 - 0.4010437131 - 0.4020088017 - 0.4029388130 - 0.4038451016 - 0.4047274888 - 0.4055930078 - 0.4064427018 - 0.4072813094 - 0.4081071913 - 0.4089260995 - 0.4097451866 - 0.4105694890 - 0.4114066064 - 0.4122569859 - 0.4131241143 - 0.4140050113 - 0.4148985147 - 0.4158008993 - 0.4167236090 - 0.4176675081 - 0.4186491966 - 0.4196803868 - 0.4207774997 - 0.4219251871 - 0.4231063128 - 0.4243052900 - 0.4255081117 - 0.4267005026 - 0.4278686941 - 0.4290013015 - 0.4300687015 - 0.4310568869 - 0.4319736958 - 0.4328454137 - 0.4336949885 - 0.4345388114 - 0.4353840053 - 0.4362317026 - 0.4370762110 - 0.4379085898 - 0.4387196004 - 0.4395040870 - 0.4402616024 - 0.4409970045 - 0.4417150915 - 0.4424203932 - 0.4431223869 - 0.4438334107 - 0.4445698857 - 0.4453583956 - 0.4462080896 - 0.4471251965 - 0.4480918944 - 0.4491102993 - 0.4501725137 - 0.4512811005 - 0.4524315000 - 0.4536258876 - 0.4548498988 - 0.4561010897 - 0.4573644996 - 0.4586338103 - 0.4599033892 - 0.4611744881 - 0.4624508917 - 0.4637391865 - 0.4650414884 - 0.4663577974 - 0.4676811099 - 0.4690015018 - 0.4703139961 - 0.4716050029 - 0.4728696048 - 0.4740908146 - 0.4752691984 - 0.4763813913 - 0.4774327874 - 0.4784271121 - 0.4793924987 - 0.4803338945 - 0.4812591970 - 0.4821656942 - 0.4830561876 - 0.4839268029 - 0.4847800136 - 0.4856149852 - 0.4864366055 - 0.4872506857 - 0.4880648851 - 0.4888868928 - 0.4897224009 - 0.4905739129 - 0.4914413095 - 0.4923292100 - 0.4932385087 - 0.4941751063 - 0.4951438010 - 0.4961484075 - 0.4971834123 - 0.4982456863 - 0.4993270040 - 0.5004199147 - 0.5015118122 - 0.5025907755 - 0.5036478043 - 0.5046734214 - 0.5056589842 - 0.5065951943 - 0.5074753761 - 0.5082963705 - 0.5090649724 - 0.5098019242 - 0.5105221868 - 0.5112395287 - 0.5119636059 - 0.5126994252 - 0.5134509206 - 0.5142197013 - 0.5150089860 - 0.5158177018 - 0.5166478157 - 0.5174968243 - 0.5183656812 - 0.5192511082 - 0.5201653838 - 0.5211125016 - 0.5221105218 - 0.5231735706 - 0.5243276954 - 0.5255516171 - 0.5268324018 - 0.5281450152 - 0.5294731259 - 0.5307943225 - 0.5320841074 - 0.5333306789 - 0.5345025063 - 0.5355963111 - 0.5366004109 - 0.5375565886 - 0.5384708047 - 0.5393584967 - 0.5402259827 - 0.5410857797 - 0.5419417024 - 0.5427981019 - 0.5436624289 - 0.5445290208 - 0.5453966260 - 0.5462577939 - 0.5471146107 - 0.5479719043 - 0.5488393903 - 0.5497273803 - 0.5506578088 - 0.5516471267 - 0.5527126789 - 0.5538355112 - 0.5549985170 - 0.5561832786 - 0.5573828816 - 0.5585942864 - 0.5598186255 - 0.5610554814 - 0.5623052716 - 0.5635616183 - 0.5648146272 - 0.5660511851 - 0.5672566295 - 0.5684217215 - 0.5695446134 - 0.5706257820 - 0.5716617107 - 0.5726498961 - 0.5735875964 - 0.5744670033 - 0.5752794743 - 0.5760352015 - 0.5767551064 - 0.5774592161 - 0.5781627893 - 0.5788754821 - 0.5796039104 - 0.5803539157 - 0.5811277032 - 0.5819321871 - 0.5827584863 - 0.5836037993 - 0.5844542980 - 0.5853049755 - 0.5861505866 - 0.5869960189 - 0.5878553987 - 0.5887522101 - 0.5897055268 - 0.5907446146 - 0.5918523073 - 0.5930091739 - 0.5941920877 - 0.5953928828 - 0.5965957046 - 0.5977848172 - 0.5989491940 - 0.6000605226 - 0.6011224985 - 0.6021242142 - 0.6030789018 - 0.6039801836 - 0.6048383713 - 0.6056500077 - 0.6064301133 - 0.6071758270 - 0.6078960896 - 0.6085894704 - 0.6092637181 - 0.6099225283 - 0.6105781794 - 0.6112405062 - 0.6119177938 - 0.6126191020 - 0.6133441925 - 0.6141006947 - 0.6148883104 - 0.6157158017 - 0.6165835857 - 0.6175035834 - 0.6184763908 - 0.6195145249 - 0.6206189990 - 0.6218029857 - 0.6230565906 - 0.6243699789 - 0.6257280111 - 0.6271185279 - 0.6285330057 - 0.6299641132 - 0.6314023137 - 0.6328353286 - 0.6342498064 - 0.6356307864 - 0.6369662285 - 0.6382427216 - 0.6394453049 - 0.6405580044 - 0.6415867805 - 0.6425542831 - 0.6434767246 - 0.6443691254 - 0.6452344060 - 0.6460803747 - 0.6469035745 - 0.6477041245 - 0.6484807730 - 0.6492365003 - 0.6499711871 - 0.6506897807 - 0.6513956785 - 0.6520953774 - 0.6527956128 - 0.6535081267 - 0.6542478800 - 0.6550295949 - 0.6558724046 - 0.6567714214 - 0.6577283144 - 0.6587334871 - 0.6597931981 - 0.6608967781 - 0.6620492935 - 0.6632428169 - 0.6644775271 - 0.6657373905 - 0.6670100093 - 0.6682801247 - 0.6695359945 - 0.6707767248 - 0.6719986200 - 0.6732079983 - 0.6743972898 - 0.6755645871 - 0.6766923070 - 0.6777756810 - 0.6787837744 - 0.6797292233 - 0.6806203127 - 0.6814811826 - 0.6823214889 - 0.6831535101 - 0.6839842200 - 0.6848192215 - 0.6856622100 - 0.6865146756 - 0.6873624921 - 0.6882051826 - 0.6890435219 - 0.6898779273 - 0.6907151937 - 0.6915640235 - 0.6924375296 - 0.6933465004 - 0.6943172812 - 0.6953549981 - 0.6964592934 - 0.6976081729 - 0.6987894177 - 0.6999887824 - 0.7011907101 - 0.7023807764 - 0.7035446763 - 0.7046695948 - 0.7057371736 - 0.7067508101 - 0.7077292204 - 0.7086814046 - 0.7096115947 - 0.7105264068 - 0.7114247084 - 0.7123047709 - 0.7131667137 - 0.7140098214 - 0.7148374915 - 0.7156537771 - 0.7164676785 - 0.7172846794 - 0.7181128860 - 0.7189571261 - 0.7198163867 - 0.7206944227 - 0.7215924859 - 0.7225160003 - 0.7234672904 - 0.7244563103 - 0.7254763842 - 0.7265270948 - 0.7275997996 - 0.7286885977 - 0.7297828197 - 0.7308688164 - 0.7319386005 - 0.7329788804 - 0.7339875102 - 0.7349454761 - 0.7358558774 - 0.7367025018 - 0.7374975085 - 0.7382442951 - 0.7389717102 - 0.7396886945 - 0.7404084206 - 0.7411383986 - 0.7418810129 - 0.7426424026 - 0.7434210181 - 0.7442209125 - 0.7450360060 - 0.7458639741 - 0.7466968894 - 0.7475343943 - 0.7483779788 - 0.7492403984 - 0.7501326203 - 0.7510781288 - 0.7520893216 - 0.7531732917 - 0.7543053031 - 0.7554739714 - 0.7566642761 - 0.7578607798 - 0.7590482831 - 0.7602115273 - 0.7613403201 - 0.7624225020 - 0.7634521127 - 0.7644327283 - 0.7653704882 - 0.7662618756 - 0.7671076059 - 0.7679154277 - 0.7686895132 - 0.7694332004 - 0.7701491714 - 0.7708417177 - 0.7715176940 - 0.7721850276 - 0.7728537917 - 0.7735335827 - 0.7742335796 - 0.7749583125 - 0.7757118940 - 0.7764955759 - 0.7773144841 - 0.7781693935 - 0.7790681720 - 0.7800117135 - 0.7810093164 - 0.7820671797 - 0.7831969857 - 0.7843930721 - 0.7856580019 - 0.7869743705 - 0.7883390784 - 0.7897359133 - 0.7911605239 - 0.7926027179 - 0.7940539122 - 0.7954995036 - 0.7969205976 - 0.7983062267 - 0.7996307015 - 0.8008906841 - 0.8020377755 - 0.8030900955 - 0.8040465713 - 0.8049607873 - 0.8058419228 - 0.8067098856 - 0.8075734973 - 0.8084387779 - 0.8093091846 - 0.8101882935 - 0.8110724092 - 0.8119565248 - 0.8128302097 - 0.8136907816 - 0.8145390153 - 0.8153827786 - 0.8162342906 - 0.8171085715 - 0.8180341125 - 0.8190286756 - 0.8201074004 - 0.8212432861 - 0.8224158287 - 0.8236094713 - 0.8248127103 - 0.8260108232 - 0.8271877766 - 0.8283281922 - 0.8294121027 - 0.8304437995 - 0.8314198852 - 0.8323469162 - 0.8332254887 - 0.8340582848 - 0.8348531723 - 0.8356149197 - 0.8363471031 - 0.8370530009 - 0.8377360106 - 0.8384007215 - 0.8390572071 - 0.8397147059 - 0.8403840065 - 0.8410733938 - 0.8417865038 - 0.8425278068 - 0.8432993889 - 0.8441076279 - 0.8449550271 - 0.8458499908 - 0.8467941880 - 0.8477939963 - 0.8488482833 - 0.8499671817 - 0.8511453867 - 0.8523722291 - 0.8536306024 - 0.8549143076 - 0.8562114239 - 0.8575189710 - 0.8588296771 - 0.8601360917 - 0.8614333272 - 0.8627083898 - 0.8639556766 - 0.8651599884 - 0.8663218021 - 0.8674148917 - 0.8684523106 - 0.8694357276 - 0.8703922033 - 0.8713268042 - 0.8722450137 - 0.8731473088 - 0.8740348220 - 0.8749024868 - 0.8757532239 - 0.8765848875 - 0.8774042130 - 0.8782153726 - 0.8790270090 - 0.8798487186 - 0.8806846738 - 0.8815388083 - 0.8824067116 - 0.8832883239 - 0.8841770291 - 0.8850684762 - 0.8859568238 - 0.8868265748 - 0.8876711726 - 0.8884992003 - 0.8893253803 - 0.8901607990 - 0.8910120130 - 0.8918834925 - 0.8927683234 - 0.8936566114 - 0.8945342898 - 0.8953920007 - 0.8962271810 - 0.8970484138 - 0.8978644013 - 0.8986830711 - 0.8995125890 - 0.9003558159 - 0.9012147188 - 0.9020918012 - 0.9029911757 - 0.9039173126 - 0.9048787951 - 0.9058734775 - 0.9069008231 - 0.9079614282 - 0.9090539217 - 0.9101731777 - 0.9113155007 - 0.9124684930 - 0.9136266112 - 0.9147831798 - 0.9159303904 - 0.9170637727 - 0.9181826115 - 0.9192898273 - 0.9203851819 - 0.9214702845 - 0.9225413203 - 0.9235910773 - 0.9246032834 - 0.9255704284 - 0.9264910221 - 0.9273847938 - 0.9282647967 - 0.9291424155 - 0.9300234914 - 0.9309046865 - 0.9317824841 - 0.9326478243 - 0.9334927201 - 0.9343230128 - 0.9351406097 - 0.9359554052 - 0.9367738962 - 0.9376012087 - 0.9384446144 - 0.9393029213 - 0.9401808977 - 0.9410789013 - 0.9420050979 - 0.9429584146 - 0.9439467192 - 0.9449679255 - 0.9460309148 - 0.9471349120 - 0.9482825994 - 0.9494689107 - 0.9506921768 - 0.9519376755 - 0.9531915188 - 0.9544414282 - 0.9556764960 - 0.9568939209 - 0.9580923915 - 0.9592747092 - 0.9604387283 - 0.9615803957 - 0.9626855850 - 0.9637448192 - 0.9647368789 - 0.9656659961 - 0.9665510058 - 0.9674115777 - 0.9682607055 - 0.9691076875 - 0.9699587822 - 0.9708169103 - 0.9716814160 - 0.9725438952 - 0.9734039903 - 0.9742600918 - 0.9751064181 - 0.9759439826 - 0.9767826200 - 0.9776347280 - 0.9785134792 - 0.9794371724 - 0.9804236293 - 0.9814878702 - 0.9826123118 - 0.9837834239 - 0.9849817157 - 0.9861915708 - 0.9873952866 - 0.9885756969 - 0.9897236228 - 0.9908238053 - 0.9918766022 - 0.9928745031 - 0.9938510060 - 0.9947990775 - 0.9957082272 - 0.9965645075 - 0.9973732829 - 0.9981198907 - 0.9988123775 - 0.9994353056 - 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 1 +{ + 0.0000000000 + 0.0007931371 + 0.0016355170 + 0.0025503801 + 0.0035146240 + 0.0045273229 + 0.0055715218 + 0.0066442778 + 0.0077339248 + 0.0088365739 + 0.0099448124 + 0.0110545401 + 0.0121633904 + 0.0132660801 + 0.0143607901 + 0.0154447202 + 0.0165135600 + 0.0175563600 + 0.0185688492 + 0.0195335001 + 0.0204569604 + 0.0213513691 + 0.0222338792 + 0.0231126193 + 0.0239920896 + 0.0248705894 + 0.0257443301 + 0.0266080908 + 0.0274574608 + 0.0282867905 + 0.0290999208 + 0.0299087707 + 0.0307223909 + 0.0315502807 + 0.0323930904 + 0.0332537815 + 0.0341344401 + 0.0350381099 + 0.0359668396 + 0.0369246788 + 0.0379154012 + 0.0389400795 + 0.0400040112 + 0.0411124304 + 0.0422693789 + 0.0434693992 + 0.0447056293 + 0.0459665395 + 0.0472429208 + 0.0485244282 + 0.0498028807 + 0.0510769486 + 0.0523513183 + 0.0536344387 + 0.0549293309 + 0.0562351793 + 0.0575465411 + 0.0588550903 + 0.0601528510 + 0.0614360608 + 0.0626917332 + 0.0639152676 + 0.0650884286 + 0.0662079304 + 0.0672680065 + 0.0682774335 + 0.0692317113 + 0.0701425374 + 0.0710024387 + 0.0718212798 + 0.0725976974 + 0.0733437091 + 0.0740561932 + 0.0747447014 + 0.0754096508 + 0.0760633573 + 0.0767156705 + 0.0773765966 + 0.0780583993 + 0.0787628815 + 0.0794979781 + 0.0802626163 + 0.0810654610 + 0.0819049180 + 0.0827899724 + 0.0837192386 + 0.0847032070 + 0.0857442319 + 0.0868611634 + 0.0880450904 + 0.0892995670 + 0.0906111673 + 0.0919737294 + 0.0933712497 + 0.0947946832 + 0.0962312222 + 0.0976707786 + 0.0991016179 + 0.1005088016 + 0.1018795967 + 0.1031991988 + 0.1044576988 + 0.1056412980 + 0.1067342982 + 0.1077463999 + 0.1087004021 + 0.1096137986 + 0.1104959026 + 0.1113545969 + 0.1121921018 + 0.1130077988 + 0.1137992963 + 0.1145685986 + 0.1153163984 + 0.1160470024 + 0.1167626977 + 0.1174698994 + 0.1181747988 + 0.1188858002 + 0.1196158975 + 0.1203754023 + 0.1211868003 + 0.1220536008 + 0.1229866967 + 0.1239702031 + 0.1250043064 + 0.1260707974 + 0.1271626055 + 0.1282667071 + 0.1293743998 + 0.1304752976 + 0.1315566003 + 0.1326127052 + 0.1336387992 + 0.1346348971 + 0.1355908066 + 0.1365199983 + 0.1374216974 + 0.1383025050 + 0.1391607970 + 0.1400025934 + 0.1408285946 + 0.1416438073 + 0.1424539983 + 0.1432684958 + 0.1440957040 + 0.1449375004 + 0.1457988024 + 0.1466775984 + 0.1475750059 + 0.1484851986 + 0.1494164020 + 0.1503666043 + 0.1513417959 + 0.1523495018 + 0.1534101069 + 0.1545234025 + 0.1556780040 + 0.1568565965 + 0.1580476016 + 0.1592365056 + 0.1604065001 + 0.1615456045 + 0.1626373976 + 0.1636672020 + 0.1646257043 + 0.1655281931 + 0.1663973033 + 0.1672482938 + 0.1680918038 + 0.1689291000 + 0.1697597057 + 0.1705808938 + 0.1713818014 + 0.1721594036 + 0.1729159951 + 0.1736537963 + 0.1743783057 + 0.1750947982 + 0.1758113056 + 0.1765365005 + 0.1772823930 + 0.1780716032 + 0.1789128035 + 0.1798152030 + 0.1807664931 + 0.1817689985 + 0.1828158051 + 0.1839095950 + 0.1850427985 + 0.1862190962 + 0.1874303073 + 0.1886713058 + 0.1899241060 + 0.1911779046 + 0.1924244016 + 0.1936572939 + 0.1948782951 + 0.1960881054 + 0.1972880065 + 0.1984702945 + 0.1996252984 + 0.2007281035 + 0.2017628998 + 0.2027094960 + 0.2036039978 + 0.2044609040 + 0.2053059042 + 0.2061520070 + 0.2070064992 + 0.2078751028 + 0.2087562978 + 0.2096406966 + 0.2105236948 + 0.2113939971 + 0.2122493982 + 0.2130886018 + 0.2139182985 + 0.2147479951 + 0.2155908048 + 0.2164690942 + 0.2174035013 + 0.2184235007 + 0.2195228934 + 0.2206815928 + 0.2218745053 + 0.2230865955 + 0.2243078947 + 0.2255353034 + 0.2267691046 + 0.2280129045 + 0.2292630970 + 0.2305141985 + 0.2317567021 + 0.2329764068 + 0.2341637015 + 0.2353121042 + 0.2364190072 + 0.2374805957 + 0.2384950072 + 0.2394600958 + 0.2403741032 + 0.2412285060 + 0.2420233935 + 0.2427742034 + 0.2434999943 + 0.2442153990 + 0.2449319065 + 0.2456566989 + 0.2463952005 + 0.2471493930 + 0.2479220033 + 0.2487130016 + 0.2495251000 + 0.2503561974 + 0.2512075901 + 0.2520760000 + 0.2529664040 + 0.2538853884 + 0.2548437119 + 0.2558579147 + 0.2569623888 + 0.2581419051 + 0.2593897879 + 0.2606801093 + 0.2619997859 + 0.2633245885 + 0.2646321058 + 0.2659066021 + 0.2671222091 + 0.2682681084 + 0.2693215907 + 0.2703152001 + 0.2712577879 + 0.2721709907 + 0.2730554938 + 0.2739250958 + 0.2747743130 + 0.2756054997 + 0.2764146030 + 0.2772051990 + 0.2779743075 + 0.2787271142 + 0.2794634998 + 0.2801890969 + 0.2809084058 + 0.2816320062 + 0.2823747098 + 0.2831506133 + 0.2839820981 + 0.2848649919 + 0.2858025134 + 0.2867873013 + 0.2878226936 + 0.2889021933 + 0.2900269926 + 0.2911930978 + 0.2923978865 + 0.2936300933 + 0.2948761880 + 0.2961241901 + 0.2973628938 + 0.2985885143 + 0.2998012006 + 0.3010047972 + 0.3021970987 + 0.3033716977 + 0.3045161963 + 0.3056159914 + 0.3066501915 + 0.3076123893 + 0.3085128963 + 0.3093748093 + 0.3102149069 + 0.3110443950 + 0.3118737936 + 0.3127109110 + 0.3135589957 + 0.3144189119 + 0.3152843118 + 0.3161520958 + 0.3170174956 + 0.3178755939 + 0.3187314868 + 0.3195927143 + 0.3204680979 + 0.3213739991 + 0.3223293126 + 0.3233569860 + 0.3244507909 + 0.3255977035 + 0.3267756999 + 0.3279756904 + 0.3291803002 + 0.3303748071 + 0.3315485120 + 0.3326843977 + 0.3337798119 + 0.3348152936 + 0.3358032107 + 0.3367348909 + 0.3376190960 + 0.3384444118 + 0.3392320871 + 0.3399783969 + 0.3406944871 + 0.3413780928 + 0.3420386910 + 0.3426808119 + 0.3433162868 + 0.3439547122 + 0.3446053863 + 0.3452802002 + 0.3459802866 + 0.3467136025 + 0.3474789858 + 0.3482855856 + 0.3491322994 + 0.3500328958 + 0.3509812057 + 0.3519834876 + 0.3530359864 + 0.3541389108 + 0.3552891910 + 0.3564738035 + 0.3576740026 + 0.3588739932 + 0.3600645959 + 0.3612402976 + 0.3623969853 + 0.3635292947 + 0.3646346033 + 0.3657099903 + 0.3667556047 + 0.3677704930 + 0.3687556982 + 0.3697125912 + 0.3706437945 + 0.3715595901 + 0.3724690080 + 0.3733772933 + 0.3742752969 + 0.3751674891 + 0.3760527074 + 0.3769319057 + 0.3778083026 + 0.3786880076 + 0.3795790076 + 0.3804906011 + 0.3814407885 + 0.3824321926 + 0.3834637105 + 0.3845176101 + 0.3855882883 + 0.3866708875 + 0.3877635002 + 0.3888686001 + 0.3899909854 + 0.3911277056 + 0.3922775984 + 0.3934313059 + 0.3945826888 + 0.3957261145 + 0.3968504965 + 0.3979482055 + 0.3990120888 + 0.4000467956 + 0.4010437131 + 0.4020088017 + 0.4029388130 + 0.4038451016 + 0.4047274888 + 0.4055930078 + 0.4064427018 + 0.4072813094 + 0.4081071913 + 0.4089260995 + 0.4097451866 + 0.4105694890 + 0.4114066064 + 0.4122569859 + 0.4131241143 + 0.4140050113 + 0.4148985147 + 0.4158008993 + 0.4167236090 + 0.4176675081 + 0.4186491966 + 0.4196803868 + 0.4207774997 + 0.4219251871 + 0.4231063128 + 0.4243052900 + 0.4255081117 + 0.4267005026 + 0.4278686941 + 0.4290013015 + 0.4300687015 + 0.4310568869 + 0.4319736958 + 0.4328454137 + 0.4336949885 + 0.4345388114 + 0.4353840053 + 0.4362317026 + 0.4370762110 + 0.4379085898 + 0.4387196004 + 0.4395040870 + 0.4402616024 + 0.4409970045 + 0.4417150915 + 0.4424203932 + 0.4431223869 + 0.4438334107 + 0.4445698857 + 0.4453583956 + 0.4462080896 + 0.4471251965 + 0.4480918944 + 0.4491102993 + 0.4501725137 + 0.4512811005 + 0.4524315000 + 0.4536258876 + 0.4548498988 + 0.4561010897 + 0.4573644996 + 0.4586338103 + 0.4599033892 + 0.4611744881 + 0.4624508917 + 0.4637391865 + 0.4650414884 + 0.4663577974 + 0.4676811099 + 0.4690015018 + 0.4703139961 + 0.4716050029 + 0.4728696048 + 0.4740908146 + 0.4752691984 + 0.4763813913 + 0.4774327874 + 0.4784271121 + 0.4793924987 + 0.4803338945 + 0.4812591970 + 0.4821656942 + 0.4830561876 + 0.4839268029 + 0.4847800136 + 0.4856149852 + 0.4864366055 + 0.4872506857 + 0.4880648851 + 0.4888868928 + 0.4897224009 + 0.4905739129 + 0.4914413095 + 0.4923292100 + 0.4932385087 + 0.4941751063 + 0.4951438010 + 0.4961484075 + 0.4971834123 + 0.4982456863 + 0.4993270040 + 0.5004199147 + 0.5015118122 + 0.5025907755 + 0.5036478043 + 0.5046734214 + 0.5056589842 + 0.5065951943 + 0.5074753761 + 0.5082963705 + 0.5090649724 + 0.5098019242 + 0.5105221868 + 0.5112395287 + 0.5119636059 + 0.5126994252 + 0.5134509206 + 0.5142197013 + 0.5150089860 + 0.5158177018 + 0.5166478157 + 0.5174968243 + 0.5183656812 + 0.5192511082 + 0.5201653838 + 0.5211125016 + 0.5221105218 + 0.5231735706 + 0.5243276954 + 0.5255516171 + 0.5268324018 + 0.5281450152 + 0.5294731259 + 0.5307943225 + 0.5320841074 + 0.5333306789 + 0.5345025063 + 0.5355963111 + 0.5366004109 + 0.5375565886 + 0.5384708047 + 0.5393584967 + 0.5402259827 + 0.5410857797 + 0.5419417024 + 0.5427981019 + 0.5436624289 + 0.5445290208 + 0.5453966260 + 0.5462577939 + 0.5471146107 + 0.5479719043 + 0.5488393903 + 0.5497273803 + 0.5506578088 + 0.5516471267 + 0.5527126789 + 0.5538355112 + 0.5549985170 + 0.5561832786 + 0.5573828816 + 0.5585942864 + 0.5598186255 + 0.5610554814 + 0.5623052716 + 0.5635616183 + 0.5648146272 + 0.5660511851 + 0.5672566295 + 0.5684217215 + 0.5695446134 + 0.5706257820 + 0.5716617107 + 0.5726498961 + 0.5735875964 + 0.5744670033 + 0.5752794743 + 0.5760352015 + 0.5767551064 + 0.5774592161 + 0.5781627893 + 0.5788754821 + 0.5796039104 + 0.5803539157 + 0.5811277032 + 0.5819321871 + 0.5827584863 + 0.5836037993 + 0.5844542980 + 0.5853049755 + 0.5861505866 + 0.5869960189 + 0.5878553987 + 0.5887522101 + 0.5897055268 + 0.5907446146 + 0.5918523073 + 0.5930091739 + 0.5941920877 + 0.5953928828 + 0.5965957046 + 0.5977848172 + 0.5989491940 + 0.6000605226 + 0.6011224985 + 0.6021242142 + 0.6030789018 + 0.6039801836 + 0.6048383713 + 0.6056500077 + 0.6064301133 + 0.6071758270 + 0.6078960896 + 0.6085894704 + 0.6092637181 + 0.6099225283 + 0.6105781794 + 0.6112405062 + 0.6119177938 + 0.6126191020 + 0.6133441925 + 0.6141006947 + 0.6148883104 + 0.6157158017 + 0.6165835857 + 0.6175035834 + 0.6184763908 + 0.6195145249 + 0.6206189990 + 0.6218029857 + 0.6230565906 + 0.6243699789 + 0.6257280111 + 0.6271185279 + 0.6285330057 + 0.6299641132 + 0.6314023137 + 0.6328353286 + 0.6342498064 + 0.6356307864 + 0.6369662285 + 0.6382427216 + 0.6394453049 + 0.6405580044 + 0.6415867805 + 0.6425542831 + 0.6434767246 + 0.6443691254 + 0.6452344060 + 0.6460803747 + 0.6469035745 + 0.6477041245 + 0.6484807730 + 0.6492365003 + 0.6499711871 + 0.6506897807 + 0.6513956785 + 0.6520953774 + 0.6527956128 + 0.6535081267 + 0.6542478800 + 0.6550295949 + 0.6558724046 + 0.6567714214 + 0.6577283144 + 0.6587334871 + 0.6597931981 + 0.6608967781 + 0.6620492935 + 0.6632428169 + 0.6644775271 + 0.6657373905 + 0.6670100093 + 0.6682801247 + 0.6695359945 + 0.6707767248 + 0.6719986200 + 0.6732079983 + 0.6743972898 + 0.6755645871 + 0.6766923070 + 0.6777756810 + 0.6787837744 + 0.6797292233 + 0.6806203127 + 0.6814811826 + 0.6823214889 + 0.6831535101 + 0.6839842200 + 0.6848192215 + 0.6856622100 + 0.6865146756 + 0.6873624921 + 0.6882051826 + 0.6890435219 + 0.6898779273 + 0.6907151937 + 0.6915640235 + 0.6924375296 + 0.6933465004 + 0.6943172812 + 0.6953549981 + 0.6964592934 + 0.6976081729 + 0.6987894177 + 0.6999887824 + 0.7011907101 + 0.7023807764 + 0.7035446763 + 0.7046695948 + 0.7057371736 + 0.7067508101 + 0.7077292204 + 0.7086814046 + 0.7096115947 + 0.7105264068 + 0.7114247084 + 0.7123047709 + 0.7131667137 + 0.7140098214 + 0.7148374915 + 0.7156537771 + 0.7164676785 + 0.7172846794 + 0.7181128860 + 0.7189571261 + 0.7198163867 + 0.7206944227 + 0.7215924859 + 0.7225160003 + 0.7234672904 + 0.7244563103 + 0.7254763842 + 0.7265270948 + 0.7275997996 + 0.7286885977 + 0.7297828197 + 0.7308688164 + 0.7319386005 + 0.7329788804 + 0.7339875102 + 0.7349454761 + 0.7358558774 + 0.7367025018 + 0.7374975085 + 0.7382442951 + 0.7389717102 + 0.7396886945 + 0.7404084206 + 0.7411383986 + 0.7418810129 + 0.7426424026 + 0.7434210181 + 0.7442209125 + 0.7450360060 + 0.7458639741 + 0.7466968894 + 0.7475343943 + 0.7483779788 + 0.7492403984 + 0.7501326203 + 0.7510781288 + 0.7520893216 + 0.7531732917 + 0.7543053031 + 0.7554739714 + 0.7566642761 + 0.7578607798 + 0.7590482831 + 0.7602115273 + 0.7613403201 + 0.7624225020 + 0.7634521127 + 0.7644327283 + 0.7653704882 + 0.7662618756 + 0.7671076059 + 0.7679154277 + 0.7686895132 + 0.7694332004 + 0.7701491714 + 0.7708417177 + 0.7715176940 + 0.7721850276 + 0.7728537917 + 0.7735335827 + 0.7742335796 + 0.7749583125 + 0.7757118940 + 0.7764955759 + 0.7773144841 + 0.7781693935 + 0.7790681720 + 0.7800117135 + 0.7810093164 + 0.7820671797 + 0.7831969857 + 0.7843930721 + 0.7856580019 + 0.7869743705 + 0.7883390784 + 0.7897359133 + 0.7911605239 + 0.7926027179 + 0.7940539122 + 0.7954995036 + 0.7969205976 + 0.7983062267 + 0.7996307015 + 0.8008906841 + 0.8020377755 + 0.8030900955 + 0.8040465713 + 0.8049607873 + 0.8058419228 + 0.8067098856 + 0.8075734973 + 0.8084387779 + 0.8093091846 + 0.8101882935 + 0.8110724092 + 0.8119565248 + 0.8128302097 + 0.8136907816 + 0.8145390153 + 0.8153827786 + 0.8162342906 + 0.8171085715 + 0.8180341125 + 0.8190286756 + 0.8201074004 + 0.8212432861 + 0.8224158287 + 0.8236094713 + 0.8248127103 + 0.8260108232 + 0.8271877766 + 0.8283281922 + 0.8294121027 + 0.8304437995 + 0.8314198852 + 0.8323469162 + 0.8332254887 + 0.8340582848 + 0.8348531723 + 0.8356149197 + 0.8363471031 + 0.8370530009 + 0.8377360106 + 0.8384007215 + 0.8390572071 + 0.8397147059 + 0.8403840065 + 0.8410733938 + 0.8417865038 + 0.8425278068 + 0.8432993889 + 0.8441076279 + 0.8449550271 + 0.8458499908 + 0.8467941880 + 0.8477939963 + 0.8488482833 + 0.8499671817 + 0.8511453867 + 0.8523722291 + 0.8536306024 + 0.8549143076 + 0.8562114239 + 0.8575189710 + 0.8588296771 + 0.8601360917 + 0.8614333272 + 0.8627083898 + 0.8639556766 + 0.8651599884 + 0.8663218021 + 0.8674148917 + 0.8684523106 + 0.8694357276 + 0.8703922033 + 0.8713268042 + 0.8722450137 + 0.8731473088 + 0.8740348220 + 0.8749024868 + 0.8757532239 + 0.8765848875 + 0.8774042130 + 0.8782153726 + 0.8790270090 + 0.8798487186 + 0.8806846738 + 0.8815388083 + 0.8824067116 + 0.8832883239 + 0.8841770291 + 0.8850684762 + 0.8859568238 + 0.8868265748 + 0.8876711726 + 0.8884992003 + 0.8893253803 + 0.8901607990 + 0.8910120130 + 0.8918834925 + 0.8927683234 + 0.8936566114 + 0.8945342898 + 0.8953920007 + 0.8962271810 + 0.8970484138 + 0.8978644013 + 0.8986830711 + 0.8995125890 + 0.9003558159 + 0.9012147188 + 0.9020918012 + 0.9029911757 + 0.9039173126 + 0.9048787951 + 0.9058734775 + 0.9069008231 + 0.9079614282 + 0.9090539217 + 0.9101731777 + 0.9113155007 + 0.9124684930 + 0.9136266112 + 0.9147831798 + 0.9159303904 + 0.9170637727 + 0.9181826115 + 0.9192898273 + 0.9203851819 + 0.9214702845 + 0.9225413203 + 0.9235910773 + 0.9246032834 + 0.9255704284 + 0.9264910221 + 0.9273847938 + 0.9282647967 + 0.9291424155 + 0.9300234914 + 0.9309046865 + 0.9317824841 + 0.9326478243 + 0.9334927201 + 0.9343230128 + 0.9351406097 + 0.9359554052 + 0.9367738962 + 0.9376012087 + 0.9384446144 + 0.9393029213 + 0.9401808977 + 0.9410789013 + 0.9420050979 + 0.9429584146 + 0.9439467192 + 0.9449679255 + 0.9460309148 + 0.9471349120 + 0.9482825994 + 0.9494689107 + 0.9506921768 + 0.9519376755 + 0.9531915188 + 0.9544414282 + 0.9556764960 + 0.9568939209 + 0.9580923915 + 0.9592747092 + 0.9604387283 + 0.9615803957 + 0.9626855850 + 0.9637448192 + 0.9647368789 + 0.9656659961 + 0.9665510058 + 0.9674115777 + 0.9682607055 + 0.9691076875 + 0.9699587822 + 0.9708169103 + 0.9716814160 + 0.9725438952 + 0.9734039903 + 0.9742600918 + 0.9751064181 + 0.9759439826 + 0.9767826200 + 0.9776347280 + 0.9785134792 + 0.9794371724 + 0.9804236293 + 0.9814878702 + 0.9826123118 + 0.9837834239 + 0.9849817157 + 0.9861915708 + 0.9873952866 + 0.9885756969 + 0.9897236228 + 0.9908238053 + 0.9918766022 + 0.9928745031 + 0.9938510060 + 0.9947990775 + 0.9957082272 + 0.9965645075 + 0.9973732829 + 0.9981198907 + 0.9988123775 + 0.9994353056 + 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kaf-3000.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kaf-3000.spi1d index 179c1dfa..7e714d64 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kaf-3000.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kaf-3000.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 1 -{ - 0.0000000000 - 0.0008239537 - 0.0017113760 - 0.0026572610 - 0.0036410000 - 0.0046517309 - 0.0056753629 - 0.0066966042 - 0.0077035818 - 0.0086956657 - 0.0096768690 - 0.0106551796 - 0.0116374604 - 0.0126282601 - 0.0136336396 - 0.0146547500 - 0.0156989004 - 0.0167712905 - 0.0178667009 - 0.0189784598 - 0.0201001503 - 0.0212287605 - 0.0223587994 - 0.0234838799 - 0.0245989803 - 0.0257005394 - 0.0267930105 - 0.0278819893 - 0.0289712399 - 0.0300603900 - 0.0311466791 - 0.0322293788 - 0.0333095305 - 0.0343901701 - 0.0354710110 - 0.0365408882 - 0.0375862904 - 0.0386093296 - 0.0396146104 - 0.0406112485 - 0.0416014791 - 0.0425814800 - 0.0435459092 - 0.0444903001 - 0.0454233699 - 0.0463445894 - 0.0472576804 - 0.0481645502 - 0.0490692593 - 0.0499759391 - 0.0508891717 - 0.0518166013 - 0.0527626611 - 0.0537226386 - 0.0546919592 - 0.0556649007 - 0.0566373095 - 0.0576051287 - 0.0585623085 - 0.0595092699 - 0.0604476705 - 0.0613846295 - 0.0623276606 - 0.0632801205 - 0.0642424524 - 0.0652119219 - 0.0661871582 - 0.0671750307 - 0.0681754425 - 0.0691855326 - 0.0701999068 - 0.0712177306 - 0.0722424909 - 0.0732808188 - 0.0743458495 - 0.0754341781 - 0.0765458792 - 0.0776749030 - 0.0788224265 - 0.0799827874 - 0.0811530426 - 0.0823262334 - 0.0834952369 - 0.0846570507 - 0.0858169273 - 0.0869782567 - 0.0881420672 - 0.0893090963 - 0.0904768184 - 0.0916401520 - 0.0927933380 - 0.0939279124 - 0.0950335264 - 0.0961028486 - 0.0971330479 - 0.0981383100 - 0.0991264507 - 0.1001088023 - 0.1010833979 - 0.1020457000 - 0.1029841006 - 0.1038997024 - 0.1047933027 - 0.1056651995 - 0.1065146998 - 0.1073456034 - 0.1081662029 - 0.1089894995 - 0.1098269969 - 0.1106915995 - 0.1115795970 - 0.1124866977 - 0.1134070009 - 0.1143371984 - 0.1152724028 - 0.1162075028 - 0.1171367019 - 0.1180611029 - 0.1189863980 - 0.1199202016 - 0.1209200025 - 0.1219737977 - 0.1230672970 - 0.1241782010 - 0.1253035069 - 0.1264393032 - 0.1275773048 - 0.1287124008 - 0.1298429966 - 0.1309712976 - 0.1321021020 - 0.1332444996 - 0.1343923956 - 0.1355278045 - 0.1366423070 - 0.1377331018 - 0.1388027072 - 0.1398545951 - 0.1408876032 - 0.1419010013 - 0.1428834945 - 0.1438401043 - 0.1447667032 - 0.1456668973 - 0.1465339959 - 0.1473750025 - 0.1481927037 - 0.1490017027 - 0.1498129964 - 0.1506288052 - 0.1514475048 - 0.1522585005 - 0.1530572027 - 0.1538338065 - 0.1545996070 - 0.1553678066 - 0.1561364979 - 0.1569079012 - 0.1576897949 - 0.1584908068 - 0.1593147069 - 0.1601596028 - 0.1610313058 - 0.1619254947 - 0.1628465056 - 0.1637898982 - 0.1647551060 - 0.1657350957 - 0.1667309999 - 0.1677470058 - 0.1687898934 - 0.1698749065 - 0.1710059941 - 0.1721913069 - 0.1734060049 - 0.1746453941 - 0.1758995950 - 0.1771610975 - 0.1784185022 - 0.1796668023 - 0.1809030026 - 0.1821372062 - 0.1833820045 - 0.1846434027 - 0.1859240979 - 0.1872127950 - 0.1884990036 - 0.1897740960 - 0.1910288036 - 0.1922529936 - 0.1934324950 - 0.1945621967 - 0.1956468970 - 0.1966817975 - 0.1976771951 - 0.1986268014 - 0.1995413005 - 0.2004172951 - 0.2012622952 - 0.2020770013 - 0.2028657049 - 0.2036325037 - 0.2043817043 - 0.2051236928 - 0.2058676034 - 0.2066213936 - 0.2074044049 - 0.2082141936 - 0.2090529948 - 0.2099052966 - 0.2107656002 - 0.2116280943 - 0.2124830037 - 0.2133235037 - 0.2141488940 - 0.2149658054 - 0.2157903016 - 0.2166465074 - 0.2175375968 - 0.2184633017 - 0.2194138020 - 0.2203830928 - 0.2213633060 - 0.2223457992 - 0.2233193070 - 0.2242776006 - 0.2252182066 - 0.2261507064 - 0.2270810008 - 0.2280130982 - 0.2289510965 - 0.2298972011 - 0.2308543026 - 0.2318239957 - 0.2328100950 - 0.2338117063 - 0.2348213047 - 0.2358337045 - 0.2368506938 - 0.2378790975 - 0.2389242053 - 0.2399875969 - 0.2410621047 - 0.2421357036 - 0.2432024926 - 0.2442667931 - 0.2453292012 - 0.2463901937 - 0.2474516928 - 0.2485162020 - 0.2495937049 - 0.2506928146 - 0.2518276870 - 0.2529919147 - 0.2541776896 - 0.2553750873 - 0.2565745115 - 0.2577669024 - 0.2589448988 - 0.2600898147 - 0.2612054944 - 0.2622891963 - 0.2633585930 - 0.2644208074 - 0.2654806077 - 0.2665413916 - 0.2676038146 - 0.2686690092 - 0.2697396874 - 0.2708159983 - 0.2718920112 - 0.2729560137 - 0.2739931047 - 0.2749989033 - 0.2759633064 - 0.2768782079 - 0.2777278125 - 0.2785072923 - 0.2792311013 - 0.2799170017 - 0.2805862129 - 0.2812471092 - 0.2819086015 - 0.2825776935 - 0.2832584083 - 0.2839589119 - 0.2846930027 - 0.2854613066 - 0.2862626016 - 0.2870804965 - 0.2879104912 - 0.2887445986 - 0.2895751894 - 0.2903952003 - 0.2912051082 - 0.2920134962 - 0.2928391099 - 0.2937021852 - 0.2946189046 - 0.2955986857 - 0.2966246903 - 0.2976948917 - 0.2987993956 - 0.2999471128 - 0.3011271954 - 0.3023349047 - 0.3035596907 - 0.3047932982 - 0.3060269952 - 0.3072519898 - 0.3084549904 - 0.3096269071 - 0.3107633889 - 0.3118777871 - 0.3129779100 - 0.3140724897 - 0.3151645064 - 0.3162542880 - 0.3173392117 - 0.3184188902 - 0.3194957972 - 0.3205688894 - 0.3216289878 - 0.3226647973 - 0.3236807883 - 0.3246811926 - 0.3256719112 - 0.3266536891 - 0.3276238143 - 0.3285810947 - 0.3295294940 - 0.3304790854 - 0.3314437866 - 0.3324182034 - 0.3333995938 - 0.3343864977 - 0.3353833854 - 0.3363946080 - 0.3374285102 - 0.3384769857 - 0.3395330906 - 0.3405885100 - 0.3416405916 - 0.3426834047 - 0.3437139094 - 0.3447251916 - 0.3457196951 - 0.3466978967 - 0.3476671875 - 0.3486328125 - 0.3495933115 - 0.3505493999 - 0.3514994979 - 0.3524489999 - 0.3534050882 - 0.3543708026 - 0.3553416133 - 0.3563007116 - 0.3572334945 - 0.3581455946 - 0.3590348959 - 0.3599027991 - 0.3607496023 - 0.3615792990 - 0.3623996973 - 0.3632219136 - 0.3640663028 - 0.3649503887 - 0.3658618033 - 0.3667995036 - 0.3677533865 - 0.3687168062 - 0.3696813881 - 0.3706347942 - 0.3715701103 - 0.3724884987 - 0.3733963072 - 0.3743011057 - 0.3752076924 - 0.3761194050 - 0.3770408034 - 0.3779734969 - 0.3789207041 - 0.3798849881 - 0.3808659017 - 0.3818587959 - 0.3828561008 - 0.3838579953 - 0.3848698139 - 0.3859066069 - 0.3869642019 - 0.3880375028 - 0.3891113997 - 0.3901838064 - 0.3912554085 - 0.3923274875 - 0.3934000134 - 0.3944740891 - 0.3955540955 - 0.3966433108 - 0.3977557123 - 0.3989006877 - 0.4000787139 - 0.4012784064 - 0.4024907053 - 0.4037069976 - 0.4049186110 - 0.4061144888 - 0.4072830081 - 0.4084154963 - 0.4095208943 - 0.4106051922 - 0.4116787910 - 0.4127461016 - 0.4138118029 - 0.4148772061 - 0.4159424007 - 0.4170075953 - 0.4180721939 - 0.4191268086 - 0.4201577902 - 0.4211621881 - 0.4221380055 - 0.4230895936 - 0.4240145087 - 0.4249120951 - 0.4257782996 - 0.4266144037 - 0.4274252057 - 0.4282115996 - 0.4289813936 - 0.4297375083 - 0.4304881990 - 0.4312418997 - 0.4320062995 - 0.4327985048 - 0.4336202145 - 0.4344716072 - 0.4353393018 - 0.4362154007 - 0.4370937943 - 0.4379679859 - 0.4388278127 - 0.4396711886 - 0.4405008852 - 0.4413329065 - 0.4421812892 - 0.4430595934 - 0.4439747036 - 0.4449261129 - 0.4459050000 - 0.4469138086 - 0.4479498863 - 0.4490180016 - 0.4501048923 - 0.4512071908 - 0.4523200095 - 0.4534398019 - 0.4545612931 - 0.4556761980 - 0.4567787051 - 0.4578686059 - 0.4589490891 - 0.4600251913 - 0.4611012936 - 0.4621792138 - 0.4632557929 - 0.4643296897 - 0.4654009044 - 0.4664717913 - 0.4675419927 - 0.4686031938 - 0.4696451128 - 0.4706639946 - 0.4716722965 - 0.4726729095 - 0.4736702144 - 0.4746587873 - 0.4756379128 - 0.4766108096 - 0.4775831103 - 0.4785614014 - 0.4795452058 - 0.4805310071 - 0.4815202951 - 0.4825159013 - 0.4835259020 - 0.4845573902 - 0.4856084883 - 0.4866689146 - 0.4877327979 - 0.4888001978 - 0.4898718894 - 0.4909470081 - 0.4920218885 - 0.4930953085 - 0.4941661954 - 0.4952344000 - 0.4963006973 - 0.4973612130 - 0.4984084964 - 0.4994311035 - 0.5004292727 - 0.5014035106 - 0.5023515224 - 0.5032755733 - 0.5041642785 - 0.5050222874 - 0.5058479905 - 0.5066505075 - 0.5074304938 - 0.5081961155 - 0.5089520812 - 0.5097069740 - 0.5104699135 - 0.5112537742 - 0.5120664239 - 0.5129058957 - 0.5137627721 - 0.5146341920 - 0.5155118704 - 0.5163893104 - 0.5172569752 - 0.5181056857 - 0.5189391971 - 0.5197678208 - 0.5206072927 - 0.5214716196 - 0.5223724246 - 0.5233045220 - 0.5242694020 - 0.5252612829 - 0.5262829065 - 0.5273327231 - 0.5284060836 - 0.5294969082 - 0.5305972099 - 0.5317053795 - 0.5328170061 - 0.5339269042 - 0.5350281000 - 0.5361132026 - 0.5371879935 - 0.5382577777 - 0.5393270850 - 0.5403997898 - 0.5414752960 - 0.5425496101 - 0.5436221957 - 0.5446935892 - 0.5457674861 - 0.5468341112 - 0.5478798747 - 0.5489076972 - 0.5499224067 - 0.5509316921 - 0.5519384742 - 0.5529379845 - 0.5539290905 - 0.5549092293 - 0.5558865070 - 0.5568640232 - 0.5578449965 - 0.5588300228 - 0.5598173738 - 0.5608099103 - 0.5618140101 - 0.5628398061 - 0.5638893247 - 0.5649536848 - 0.5660216212 - 0.5670881271 - 0.5681570768 - 0.5692278743 - 0.5702992082 - 0.5713713169 - 0.5724447966 - 0.5735238791 - 0.5746114850 - 0.5757126212 - 0.5768241882 - 0.5779408813 - 0.5790569782 - 0.5801681280 - 0.5812727213 - 0.5823650956 - 0.5834385157 - 0.5844845176 - 0.5854976773 - 0.5864800811 - 0.5874279737 - 0.5883439779 - 0.5892180204 - 0.5900630951 - 0.5908904076 - 0.5917155147 - 0.5925496817 - 0.5933985710 - 0.5942596197 - 0.5951249003 - 0.5959877968 - 0.5968421102 - 0.5976834297 - 0.5984950066 - 0.5992792249 - 0.6000332832 - 0.6007766724 - 0.6015186906 - 0.6022672057 - 0.6030321717 - 0.6038194895 - 0.6046321988 - 0.6054781079 - 0.6063600779 - 0.6072819233 - 0.6082324982 - 0.6092107296 - 0.6102107763 - 0.6112337708 - 0.6122769713 - 0.6133307219 - 0.6143916249 - 0.6154521704 - 0.6165164113 - 0.6175839901 - 0.6186556220 - 0.6197301745 - 0.6208109260 - 0.6219016910 - 0.6230133772 - 0.6241537929 - 0.6253302097 - 0.6265388727 - 0.6277642846 - 0.6289970875 - 0.6302282214 - 0.6314477921 - 0.6326466203 - 0.6338086724 - 0.6349387169 - 0.6360424757 - 0.6371321082 - 0.6382141113 - 0.6392921209 - 0.6403672099 - 0.6414381862 - 0.6425055265 - 0.6435723901 - 0.6446369290 - 0.6456918120 - 0.6467275023 - 0.6477332711 - 0.6487026811 - 0.6496292949 - 0.6505001187 - 0.6513100863 - 0.6520529985 - 0.6527563930 - 0.6534317136 - 0.6540933847 - 0.6547484994 - 0.6554052234 - 0.6560714245 - 0.6567540765 - 0.6574617028 - 0.6582099795 - 0.6589958072 - 0.6598116755 - 0.6606448889 - 0.6614922881 - 0.6623446941 - 0.6631942987 - 0.6640357971 - 0.6648727059 - 0.6657136083 - 0.6665757895 - 0.6674811244 - 0.6684336066 - 0.6694403887 - 0.6704826951 - 0.6715667248 - 0.6726835966 - 0.6738361716 - 0.6750171781 - 0.6762194037 - 0.6774324775 - 0.6786479950 - 0.6798574924 - 0.6810536981 - 0.6822184920 - 0.6833531857 - 0.6844527125 - 0.6855350137 - 0.6866068244 - 0.6876752973 - 0.6887421012 - 0.6898087859 - 0.6908742785 - 0.6919373870 - 0.6929990053 - 0.6940523982 - 0.6950901151 - 0.6961060762 - 0.6971120238 - 0.6981120706 - 0.6991108060 - 0.7001062036 - 0.7010952830 - 0.7020772099 - 0.7030572295 - 0.7040420771 - 0.7050337195 - 0.7060307860 - 0.7070298195 - 0.7080342174 - 0.7090519071 - 0.7100886703 - 0.7111470103 - 0.7122195959 - 0.7132937908 - 0.7143607140 - 0.7154163122 - 0.7164533138 - 0.7174721956 - 0.7184677124 - 0.7194433808 - 0.7204009295 - 0.7213470936 - 0.7222880721 - 0.7232226133 - 0.7241495252 - 0.7250617146 - 0.7259560227 - 0.7268221974 - 0.7276610136 - 0.7284513116 - 0.7291994095 - 0.7299144268 - 0.7306104898 - 0.7312946916 - 0.7319728136 - 0.7326508164 - 0.7333354950 - 0.7340328097 - 0.7347601056 - 0.7355272174 - 0.7363470793 - 0.7372058034 - 0.7380936742 - 0.7390053868 - 0.7399355173 - 0.7408784032 - 0.7418274879 - 0.7427803278 - 0.7437379956 - 0.7447054982 - 0.7456836104 - 0.7466775775 - 0.7476887703 - 0.7487217188 - 0.7497777939 - 0.7508600950 - 0.7519810796 - 0.7531362176 - 0.7543209195 - 0.7555187941 - 0.7567229867 - 0.7579241991 - 0.7591124177 - 0.7602767944 - 0.7614082098 - 0.7625083923 - 0.7635900974 - 0.7646592855 - 0.7657235861 - 0.7667865157 - 0.7678502202 - 0.7689155936 - 0.7699834704 - 0.7710577846 - 0.7721307278 - 0.7731891870 - 0.7742263079 - 0.7752460241 - 0.7762551904 - 0.7772603035 - 0.7782598138 - 0.7792512178 - 0.7802270055 - 0.7811887860 - 0.7821359038 - 0.7830716968 - 0.7839983106 - 0.7849181890 - 0.7858353853 - 0.7867543101 - 0.7876856923 - 0.7886385918 - 0.7896096706 - 0.7905899286 - 0.7915676236 - 0.7925336957 - 0.7934818268 - 0.7944025993 - 0.7952839136 - 0.7961304784 - 0.7969564199 - 0.7977765799 - 0.7986037731 - 0.7994433045 - 0.8003032207 - 0.8011813164 - 0.8020802736 - 0.8030009270 - 0.8039441705 - 0.8048942089 - 0.8058394194 - 0.8067725897 - 0.8077021837 - 0.8086333871 - 0.8095719814 - 0.8105241060 - 0.8114864826 - 0.8124600053 - 0.8134480119 - 0.8144549131 - 0.8154819012 - 0.8165273070 - 0.8175873160 - 0.8186547160 - 0.8197259903 - 0.8207991123 - 0.8218631744 - 0.8229097724 - 0.8239313960 - 0.8249354959 - 0.8259270191 - 0.8269088864 - 0.8278800249 - 0.8288375139 - 0.8297861814 - 0.8307325840 - 0.8316870928 - 0.8326554894 - 0.8336303234 - 0.8346098065 - 0.8355954289 - 0.8365911245 - 0.8376066089 - 0.8386394978 - 0.8396871090 - 0.8407385945 - 0.8417950273 - 0.8428555131 - 0.8439220190 - 0.8449907899 - 0.8460597992 - 0.8471270800 - 0.8481943011 - 0.8492627144 - 0.8503319025 - 0.8513954878 - 0.8524438739 - 0.8534613848 - 0.8544461131 - 0.8553860188 - 0.8562803864 - 0.8571103811 - 0.8578768969 - 0.8585957885 - 0.8592829704 - 0.8599522114 - 0.8606104255 - 0.8612663150 - 0.8619267941 - 0.8625972271 - 0.8632894158 - 0.8640218973 - 0.8647894859 - 0.8655846119 - 0.8664000034 - 0.8672314882 - 0.8680710793 - 0.8689119220 - 0.8697466254 - 0.8705791235 - 0.8714179993 - 0.8722798824 - 0.8731784225 - 0.8741275072 - 0.8751282096 - 0.8761690259 - 0.8772516847 - 0.8783689141 - 0.8795279264 - 0.8807160258 - 0.8819314837 - 0.8831598163 - 0.8843933940 - 0.8856235147 - 0.8868407011 - 0.8880308867 - 0.8891866803 - 0.8903077245 - 0.8914114833 - 0.8925023079 - 0.8935890198 - 0.8946737051 - 0.8957551122 - 0.8968315125 - 0.8979024887 - 0.8989741206 - 0.9000406861 - 0.9010896087 - 0.9021161199 - 0.9031258225 - 0.9041231871 - 0.9051136971 - 0.9060966969 - 0.9070687890 - 0.9080281854 - 0.9089828134 - 0.9099376798 - 0.9109022021 - 0.9118763208 - 0.9128565788 - 0.9138407707 - 0.9148361087 - 0.9158486128 - 0.9168851972 - 0.9179406762 - 0.9190021753 - 0.9200580716 - 0.9210987091 - 0.9221140742 - 0.9230914712 - 0.9240095019 - 0.9248781800 - 0.9257149100 - 0.9265410900 - 0.9273703098 - 0.9282112718 - 0.9290707111 - 0.9299505949 - 0.9308521152 - 0.9317784905 - 0.9327259064 - 0.9336796999 - 0.9346215725 - 0.9355351925 - 0.9364286065 - 0.9372990131 - 0.9381507039 - 0.9389857054 - 0.9398118854 - 0.9406411052 - 0.9414886832 - 0.9423733950 - 0.9433202744 - 0.9443135262 - 0.9453420043 - 0.9463896155 - 0.9474490881 - 0.9485102892 - 0.9495586157 - 0.9505851269 - 0.9515897036 - 0.9525790811 - 0.9535614848 - 0.9545397162 - 0.9555109143 - 0.9564700127 - 0.9574239254 - 0.9583799839 - 0.9593443274 - 0.9603207707 - 0.9613075852 - 0.9623004794 - 0.9633029103 - 0.9643172026 - 0.9653546214 - 0.9664106965 - 0.9674816728 - 0.9685521722 - 0.9696255922 - 0.9707052708 - 0.9717903733 - 0.9728767872 - 0.9739595056 - 0.9750362039 - 0.9761099815 - 0.9771819115 - 0.9782528877 - 0.9793108106 - 0.9803507924 - 0.9813615084 - 0.9823462963 - 0.9833049178 - 0.9842357039 - 0.9851344228 - 0.9859966040 - 0.9868291020 - 0.9876328707 - 0.9884179831 - 0.9891839027 - 0.9899348021 - 0.9906693101 - 0.9913892150 - 0.9920961857 - 0.9927936792 - 0.9934824109 - 0.9941564202 - 0.9948133826 - 0.9954445958 - 0.9960510731 - 0.9966241717 - 0.9971635938 - 0.9976590276 - 0.9981124997 - 0.9985358715 - 0.9989339113 - 0.9993115067 - 0.9996662736 - 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 1 +{ + 0.0000000000 + 0.0008239537 + 0.0017113760 + 0.0026572610 + 0.0036410000 + 0.0046517309 + 0.0056753629 + 0.0066966042 + 0.0077035818 + 0.0086956657 + 0.0096768690 + 0.0106551796 + 0.0116374604 + 0.0126282601 + 0.0136336396 + 0.0146547500 + 0.0156989004 + 0.0167712905 + 0.0178667009 + 0.0189784598 + 0.0201001503 + 0.0212287605 + 0.0223587994 + 0.0234838799 + 0.0245989803 + 0.0257005394 + 0.0267930105 + 0.0278819893 + 0.0289712399 + 0.0300603900 + 0.0311466791 + 0.0322293788 + 0.0333095305 + 0.0343901701 + 0.0354710110 + 0.0365408882 + 0.0375862904 + 0.0386093296 + 0.0396146104 + 0.0406112485 + 0.0416014791 + 0.0425814800 + 0.0435459092 + 0.0444903001 + 0.0454233699 + 0.0463445894 + 0.0472576804 + 0.0481645502 + 0.0490692593 + 0.0499759391 + 0.0508891717 + 0.0518166013 + 0.0527626611 + 0.0537226386 + 0.0546919592 + 0.0556649007 + 0.0566373095 + 0.0576051287 + 0.0585623085 + 0.0595092699 + 0.0604476705 + 0.0613846295 + 0.0623276606 + 0.0632801205 + 0.0642424524 + 0.0652119219 + 0.0661871582 + 0.0671750307 + 0.0681754425 + 0.0691855326 + 0.0701999068 + 0.0712177306 + 0.0722424909 + 0.0732808188 + 0.0743458495 + 0.0754341781 + 0.0765458792 + 0.0776749030 + 0.0788224265 + 0.0799827874 + 0.0811530426 + 0.0823262334 + 0.0834952369 + 0.0846570507 + 0.0858169273 + 0.0869782567 + 0.0881420672 + 0.0893090963 + 0.0904768184 + 0.0916401520 + 0.0927933380 + 0.0939279124 + 0.0950335264 + 0.0961028486 + 0.0971330479 + 0.0981383100 + 0.0991264507 + 0.1001088023 + 0.1010833979 + 0.1020457000 + 0.1029841006 + 0.1038997024 + 0.1047933027 + 0.1056651995 + 0.1065146998 + 0.1073456034 + 0.1081662029 + 0.1089894995 + 0.1098269969 + 0.1106915995 + 0.1115795970 + 0.1124866977 + 0.1134070009 + 0.1143371984 + 0.1152724028 + 0.1162075028 + 0.1171367019 + 0.1180611029 + 0.1189863980 + 0.1199202016 + 0.1209200025 + 0.1219737977 + 0.1230672970 + 0.1241782010 + 0.1253035069 + 0.1264393032 + 0.1275773048 + 0.1287124008 + 0.1298429966 + 0.1309712976 + 0.1321021020 + 0.1332444996 + 0.1343923956 + 0.1355278045 + 0.1366423070 + 0.1377331018 + 0.1388027072 + 0.1398545951 + 0.1408876032 + 0.1419010013 + 0.1428834945 + 0.1438401043 + 0.1447667032 + 0.1456668973 + 0.1465339959 + 0.1473750025 + 0.1481927037 + 0.1490017027 + 0.1498129964 + 0.1506288052 + 0.1514475048 + 0.1522585005 + 0.1530572027 + 0.1538338065 + 0.1545996070 + 0.1553678066 + 0.1561364979 + 0.1569079012 + 0.1576897949 + 0.1584908068 + 0.1593147069 + 0.1601596028 + 0.1610313058 + 0.1619254947 + 0.1628465056 + 0.1637898982 + 0.1647551060 + 0.1657350957 + 0.1667309999 + 0.1677470058 + 0.1687898934 + 0.1698749065 + 0.1710059941 + 0.1721913069 + 0.1734060049 + 0.1746453941 + 0.1758995950 + 0.1771610975 + 0.1784185022 + 0.1796668023 + 0.1809030026 + 0.1821372062 + 0.1833820045 + 0.1846434027 + 0.1859240979 + 0.1872127950 + 0.1884990036 + 0.1897740960 + 0.1910288036 + 0.1922529936 + 0.1934324950 + 0.1945621967 + 0.1956468970 + 0.1966817975 + 0.1976771951 + 0.1986268014 + 0.1995413005 + 0.2004172951 + 0.2012622952 + 0.2020770013 + 0.2028657049 + 0.2036325037 + 0.2043817043 + 0.2051236928 + 0.2058676034 + 0.2066213936 + 0.2074044049 + 0.2082141936 + 0.2090529948 + 0.2099052966 + 0.2107656002 + 0.2116280943 + 0.2124830037 + 0.2133235037 + 0.2141488940 + 0.2149658054 + 0.2157903016 + 0.2166465074 + 0.2175375968 + 0.2184633017 + 0.2194138020 + 0.2203830928 + 0.2213633060 + 0.2223457992 + 0.2233193070 + 0.2242776006 + 0.2252182066 + 0.2261507064 + 0.2270810008 + 0.2280130982 + 0.2289510965 + 0.2298972011 + 0.2308543026 + 0.2318239957 + 0.2328100950 + 0.2338117063 + 0.2348213047 + 0.2358337045 + 0.2368506938 + 0.2378790975 + 0.2389242053 + 0.2399875969 + 0.2410621047 + 0.2421357036 + 0.2432024926 + 0.2442667931 + 0.2453292012 + 0.2463901937 + 0.2474516928 + 0.2485162020 + 0.2495937049 + 0.2506928146 + 0.2518276870 + 0.2529919147 + 0.2541776896 + 0.2553750873 + 0.2565745115 + 0.2577669024 + 0.2589448988 + 0.2600898147 + 0.2612054944 + 0.2622891963 + 0.2633585930 + 0.2644208074 + 0.2654806077 + 0.2665413916 + 0.2676038146 + 0.2686690092 + 0.2697396874 + 0.2708159983 + 0.2718920112 + 0.2729560137 + 0.2739931047 + 0.2749989033 + 0.2759633064 + 0.2768782079 + 0.2777278125 + 0.2785072923 + 0.2792311013 + 0.2799170017 + 0.2805862129 + 0.2812471092 + 0.2819086015 + 0.2825776935 + 0.2832584083 + 0.2839589119 + 0.2846930027 + 0.2854613066 + 0.2862626016 + 0.2870804965 + 0.2879104912 + 0.2887445986 + 0.2895751894 + 0.2903952003 + 0.2912051082 + 0.2920134962 + 0.2928391099 + 0.2937021852 + 0.2946189046 + 0.2955986857 + 0.2966246903 + 0.2976948917 + 0.2987993956 + 0.2999471128 + 0.3011271954 + 0.3023349047 + 0.3035596907 + 0.3047932982 + 0.3060269952 + 0.3072519898 + 0.3084549904 + 0.3096269071 + 0.3107633889 + 0.3118777871 + 0.3129779100 + 0.3140724897 + 0.3151645064 + 0.3162542880 + 0.3173392117 + 0.3184188902 + 0.3194957972 + 0.3205688894 + 0.3216289878 + 0.3226647973 + 0.3236807883 + 0.3246811926 + 0.3256719112 + 0.3266536891 + 0.3276238143 + 0.3285810947 + 0.3295294940 + 0.3304790854 + 0.3314437866 + 0.3324182034 + 0.3333995938 + 0.3343864977 + 0.3353833854 + 0.3363946080 + 0.3374285102 + 0.3384769857 + 0.3395330906 + 0.3405885100 + 0.3416405916 + 0.3426834047 + 0.3437139094 + 0.3447251916 + 0.3457196951 + 0.3466978967 + 0.3476671875 + 0.3486328125 + 0.3495933115 + 0.3505493999 + 0.3514994979 + 0.3524489999 + 0.3534050882 + 0.3543708026 + 0.3553416133 + 0.3563007116 + 0.3572334945 + 0.3581455946 + 0.3590348959 + 0.3599027991 + 0.3607496023 + 0.3615792990 + 0.3623996973 + 0.3632219136 + 0.3640663028 + 0.3649503887 + 0.3658618033 + 0.3667995036 + 0.3677533865 + 0.3687168062 + 0.3696813881 + 0.3706347942 + 0.3715701103 + 0.3724884987 + 0.3733963072 + 0.3743011057 + 0.3752076924 + 0.3761194050 + 0.3770408034 + 0.3779734969 + 0.3789207041 + 0.3798849881 + 0.3808659017 + 0.3818587959 + 0.3828561008 + 0.3838579953 + 0.3848698139 + 0.3859066069 + 0.3869642019 + 0.3880375028 + 0.3891113997 + 0.3901838064 + 0.3912554085 + 0.3923274875 + 0.3934000134 + 0.3944740891 + 0.3955540955 + 0.3966433108 + 0.3977557123 + 0.3989006877 + 0.4000787139 + 0.4012784064 + 0.4024907053 + 0.4037069976 + 0.4049186110 + 0.4061144888 + 0.4072830081 + 0.4084154963 + 0.4095208943 + 0.4106051922 + 0.4116787910 + 0.4127461016 + 0.4138118029 + 0.4148772061 + 0.4159424007 + 0.4170075953 + 0.4180721939 + 0.4191268086 + 0.4201577902 + 0.4211621881 + 0.4221380055 + 0.4230895936 + 0.4240145087 + 0.4249120951 + 0.4257782996 + 0.4266144037 + 0.4274252057 + 0.4282115996 + 0.4289813936 + 0.4297375083 + 0.4304881990 + 0.4312418997 + 0.4320062995 + 0.4327985048 + 0.4336202145 + 0.4344716072 + 0.4353393018 + 0.4362154007 + 0.4370937943 + 0.4379679859 + 0.4388278127 + 0.4396711886 + 0.4405008852 + 0.4413329065 + 0.4421812892 + 0.4430595934 + 0.4439747036 + 0.4449261129 + 0.4459050000 + 0.4469138086 + 0.4479498863 + 0.4490180016 + 0.4501048923 + 0.4512071908 + 0.4523200095 + 0.4534398019 + 0.4545612931 + 0.4556761980 + 0.4567787051 + 0.4578686059 + 0.4589490891 + 0.4600251913 + 0.4611012936 + 0.4621792138 + 0.4632557929 + 0.4643296897 + 0.4654009044 + 0.4664717913 + 0.4675419927 + 0.4686031938 + 0.4696451128 + 0.4706639946 + 0.4716722965 + 0.4726729095 + 0.4736702144 + 0.4746587873 + 0.4756379128 + 0.4766108096 + 0.4775831103 + 0.4785614014 + 0.4795452058 + 0.4805310071 + 0.4815202951 + 0.4825159013 + 0.4835259020 + 0.4845573902 + 0.4856084883 + 0.4866689146 + 0.4877327979 + 0.4888001978 + 0.4898718894 + 0.4909470081 + 0.4920218885 + 0.4930953085 + 0.4941661954 + 0.4952344000 + 0.4963006973 + 0.4973612130 + 0.4984084964 + 0.4994311035 + 0.5004292727 + 0.5014035106 + 0.5023515224 + 0.5032755733 + 0.5041642785 + 0.5050222874 + 0.5058479905 + 0.5066505075 + 0.5074304938 + 0.5081961155 + 0.5089520812 + 0.5097069740 + 0.5104699135 + 0.5112537742 + 0.5120664239 + 0.5129058957 + 0.5137627721 + 0.5146341920 + 0.5155118704 + 0.5163893104 + 0.5172569752 + 0.5181056857 + 0.5189391971 + 0.5197678208 + 0.5206072927 + 0.5214716196 + 0.5223724246 + 0.5233045220 + 0.5242694020 + 0.5252612829 + 0.5262829065 + 0.5273327231 + 0.5284060836 + 0.5294969082 + 0.5305972099 + 0.5317053795 + 0.5328170061 + 0.5339269042 + 0.5350281000 + 0.5361132026 + 0.5371879935 + 0.5382577777 + 0.5393270850 + 0.5403997898 + 0.5414752960 + 0.5425496101 + 0.5436221957 + 0.5446935892 + 0.5457674861 + 0.5468341112 + 0.5478798747 + 0.5489076972 + 0.5499224067 + 0.5509316921 + 0.5519384742 + 0.5529379845 + 0.5539290905 + 0.5549092293 + 0.5558865070 + 0.5568640232 + 0.5578449965 + 0.5588300228 + 0.5598173738 + 0.5608099103 + 0.5618140101 + 0.5628398061 + 0.5638893247 + 0.5649536848 + 0.5660216212 + 0.5670881271 + 0.5681570768 + 0.5692278743 + 0.5702992082 + 0.5713713169 + 0.5724447966 + 0.5735238791 + 0.5746114850 + 0.5757126212 + 0.5768241882 + 0.5779408813 + 0.5790569782 + 0.5801681280 + 0.5812727213 + 0.5823650956 + 0.5834385157 + 0.5844845176 + 0.5854976773 + 0.5864800811 + 0.5874279737 + 0.5883439779 + 0.5892180204 + 0.5900630951 + 0.5908904076 + 0.5917155147 + 0.5925496817 + 0.5933985710 + 0.5942596197 + 0.5951249003 + 0.5959877968 + 0.5968421102 + 0.5976834297 + 0.5984950066 + 0.5992792249 + 0.6000332832 + 0.6007766724 + 0.6015186906 + 0.6022672057 + 0.6030321717 + 0.6038194895 + 0.6046321988 + 0.6054781079 + 0.6063600779 + 0.6072819233 + 0.6082324982 + 0.6092107296 + 0.6102107763 + 0.6112337708 + 0.6122769713 + 0.6133307219 + 0.6143916249 + 0.6154521704 + 0.6165164113 + 0.6175839901 + 0.6186556220 + 0.6197301745 + 0.6208109260 + 0.6219016910 + 0.6230133772 + 0.6241537929 + 0.6253302097 + 0.6265388727 + 0.6277642846 + 0.6289970875 + 0.6302282214 + 0.6314477921 + 0.6326466203 + 0.6338086724 + 0.6349387169 + 0.6360424757 + 0.6371321082 + 0.6382141113 + 0.6392921209 + 0.6403672099 + 0.6414381862 + 0.6425055265 + 0.6435723901 + 0.6446369290 + 0.6456918120 + 0.6467275023 + 0.6477332711 + 0.6487026811 + 0.6496292949 + 0.6505001187 + 0.6513100863 + 0.6520529985 + 0.6527563930 + 0.6534317136 + 0.6540933847 + 0.6547484994 + 0.6554052234 + 0.6560714245 + 0.6567540765 + 0.6574617028 + 0.6582099795 + 0.6589958072 + 0.6598116755 + 0.6606448889 + 0.6614922881 + 0.6623446941 + 0.6631942987 + 0.6640357971 + 0.6648727059 + 0.6657136083 + 0.6665757895 + 0.6674811244 + 0.6684336066 + 0.6694403887 + 0.6704826951 + 0.6715667248 + 0.6726835966 + 0.6738361716 + 0.6750171781 + 0.6762194037 + 0.6774324775 + 0.6786479950 + 0.6798574924 + 0.6810536981 + 0.6822184920 + 0.6833531857 + 0.6844527125 + 0.6855350137 + 0.6866068244 + 0.6876752973 + 0.6887421012 + 0.6898087859 + 0.6908742785 + 0.6919373870 + 0.6929990053 + 0.6940523982 + 0.6950901151 + 0.6961060762 + 0.6971120238 + 0.6981120706 + 0.6991108060 + 0.7001062036 + 0.7010952830 + 0.7020772099 + 0.7030572295 + 0.7040420771 + 0.7050337195 + 0.7060307860 + 0.7070298195 + 0.7080342174 + 0.7090519071 + 0.7100886703 + 0.7111470103 + 0.7122195959 + 0.7132937908 + 0.7143607140 + 0.7154163122 + 0.7164533138 + 0.7174721956 + 0.7184677124 + 0.7194433808 + 0.7204009295 + 0.7213470936 + 0.7222880721 + 0.7232226133 + 0.7241495252 + 0.7250617146 + 0.7259560227 + 0.7268221974 + 0.7276610136 + 0.7284513116 + 0.7291994095 + 0.7299144268 + 0.7306104898 + 0.7312946916 + 0.7319728136 + 0.7326508164 + 0.7333354950 + 0.7340328097 + 0.7347601056 + 0.7355272174 + 0.7363470793 + 0.7372058034 + 0.7380936742 + 0.7390053868 + 0.7399355173 + 0.7408784032 + 0.7418274879 + 0.7427803278 + 0.7437379956 + 0.7447054982 + 0.7456836104 + 0.7466775775 + 0.7476887703 + 0.7487217188 + 0.7497777939 + 0.7508600950 + 0.7519810796 + 0.7531362176 + 0.7543209195 + 0.7555187941 + 0.7567229867 + 0.7579241991 + 0.7591124177 + 0.7602767944 + 0.7614082098 + 0.7625083923 + 0.7635900974 + 0.7646592855 + 0.7657235861 + 0.7667865157 + 0.7678502202 + 0.7689155936 + 0.7699834704 + 0.7710577846 + 0.7721307278 + 0.7731891870 + 0.7742263079 + 0.7752460241 + 0.7762551904 + 0.7772603035 + 0.7782598138 + 0.7792512178 + 0.7802270055 + 0.7811887860 + 0.7821359038 + 0.7830716968 + 0.7839983106 + 0.7849181890 + 0.7858353853 + 0.7867543101 + 0.7876856923 + 0.7886385918 + 0.7896096706 + 0.7905899286 + 0.7915676236 + 0.7925336957 + 0.7934818268 + 0.7944025993 + 0.7952839136 + 0.7961304784 + 0.7969564199 + 0.7977765799 + 0.7986037731 + 0.7994433045 + 0.8003032207 + 0.8011813164 + 0.8020802736 + 0.8030009270 + 0.8039441705 + 0.8048942089 + 0.8058394194 + 0.8067725897 + 0.8077021837 + 0.8086333871 + 0.8095719814 + 0.8105241060 + 0.8114864826 + 0.8124600053 + 0.8134480119 + 0.8144549131 + 0.8154819012 + 0.8165273070 + 0.8175873160 + 0.8186547160 + 0.8197259903 + 0.8207991123 + 0.8218631744 + 0.8229097724 + 0.8239313960 + 0.8249354959 + 0.8259270191 + 0.8269088864 + 0.8278800249 + 0.8288375139 + 0.8297861814 + 0.8307325840 + 0.8316870928 + 0.8326554894 + 0.8336303234 + 0.8346098065 + 0.8355954289 + 0.8365911245 + 0.8376066089 + 0.8386394978 + 0.8396871090 + 0.8407385945 + 0.8417950273 + 0.8428555131 + 0.8439220190 + 0.8449907899 + 0.8460597992 + 0.8471270800 + 0.8481943011 + 0.8492627144 + 0.8503319025 + 0.8513954878 + 0.8524438739 + 0.8534613848 + 0.8544461131 + 0.8553860188 + 0.8562803864 + 0.8571103811 + 0.8578768969 + 0.8585957885 + 0.8592829704 + 0.8599522114 + 0.8606104255 + 0.8612663150 + 0.8619267941 + 0.8625972271 + 0.8632894158 + 0.8640218973 + 0.8647894859 + 0.8655846119 + 0.8664000034 + 0.8672314882 + 0.8680710793 + 0.8689119220 + 0.8697466254 + 0.8705791235 + 0.8714179993 + 0.8722798824 + 0.8731784225 + 0.8741275072 + 0.8751282096 + 0.8761690259 + 0.8772516847 + 0.8783689141 + 0.8795279264 + 0.8807160258 + 0.8819314837 + 0.8831598163 + 0.8843933940 + 0.8856235147 + 0.8868407011 + 0.8880308867 + 0.8891866803 + 0.8903077245 + 0.8914114833 + 0.8925023079 + 0.8935890198 + 0.8946737051 + 0.8957551122 + 0.8968315125 + 0.8979024887 + 0.8989741206 + 0.9000406861 + 0.9010896087 + 0.9021161199 + 0.9031258225 + 0.9041231871 + 0.9051136971 + 0.9060966969 + 0.9070687890 + 0.9080281854 + 0.9089828134 + 0.9099376798 + 0.9109022021 + 0.9118763208 + 0.9128565788 + 0.9138407707 + 0.9148361087 + 0.9158486128 + 0.9168851972 + 0.9179406762 + 0.9190021753 + 0.9200580716 + 0.9210987091 + 0.9221140742 + 0.9230914712 + 0.9240095019 + 0.9248781800 + 0.9257149100 + 0.9265410900 + 0.9273703098 + 0.9282112718 + 0.9290707111 + 0.9299505949 + 0.9308521152 + 0.9317784905 + 0.9327259064 + 0.9336796999 + 0.9346215725 + 0.9355351925 + 0.9364286065 + 0.9372990131 + 0.9381507039 + 0.9389857054 + 0.9398118854 + 0.9406411052 + 0.9414886832 + 0.9423733950 + 0.9433202744 + 0.9443135262 + 0.9453420043 + 0.9463896155 + 0.9474490881 + 0.9485102892 + 0.9495586157 + 0.9505851269 + 0.9515897036 + 0.9525790811 + 0.9535614848 + 0.9545397162 + 0.9555109143 + 0.9564700127 + 0.9574239254 + 0.9583799839 + 0.9593443274 + 0.9603207707 + 0.9613075852 + 0.9623004794 + 0.9633029103 + 0.9643172026 + 0.9653546214 + 0.9664106965 + 0.9674816728 + 0.9685521722 + 0.9696255922 + 0.9707052708 + 0.9717903733 + 0.9728767872 + 0.9739595056 + 0.9750362039 + 0.9761099815 + 0.9771819115 + 0.9782528877 + 0.9793108106 + 0.9803507924 + 0.9813615084 + 0.9823462963 + 0.9833049178 + 0.9842357039 + 0.9851344228 + 0.9859966040 + 0.9868291020 + 0.9876328707 + 0.9884179831 + 0.9891839027 + 0.9899348021 + 0.9906693101 + 0.9913892150 + 0.9920961857 + 0.9927936792 + 0.9934824109 + 0.9941564202 + 0.9948133826 + 0.9954445958 + 0.9960510731 + 0.9966241717 + 0.9971635938 + 0.9976590276 + 0.9981124997 + 0.9985358715 + 0.9989339113 + 0.9993115067 + 0.9996662736 + 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kai-0311.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kai-0311.spi1d index a382f093..aa328b94 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kai-0311.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kai-0311.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 1 -{ - 0.0000000000 - 0.0016059499 - 0.0032039359 - 0.0047925338 - 0.0063681109 - 0.0079261437 - 0.0094620753 - 0.0109717799 - 0.0124478899 - 0.0138870701 - 0.0152903199 - 0.0166584495 - 0.0179926194 - 0.0192952193 - 0.0205696207 - 0.0218127500 - 0.0230190307 - 0.0241936091 - 0.0253404994 - 0.0264616702 - 0.0275589097 - 0.0286343992 - 0.0296888705 - 0.0307215601 - 0.0317354687 - 0.0327326916 - 0.0337138698 - 0.0346802585 - 0.0356336907 - 0.0365758091 - 0.0375103503 - 0.0384392217 - 0.0393667594 - 0.0402943417 - 0.0412232615 - 0.0421543792 - 0.0430876091 - 0.0440218188 - 0.0449589603 - 0.0459001884 - 0.0468437783 - 0.0477895401 - 0.0487375483 - 0.0496875793 - 0.0506395511 - 0.0515942089 - 0.0525525808 - 0.0535144582 - 0.0544794910 - 0.0554478317 - 0.0564199202 - 0.0573957488 - 0.0583746210 - 0.0593575612 - 0.0603439212 - 0.0613333285 - 0.0623258613 - 0.0633219033 - 0.0643215328 - 0.0653229132 - 0.0663260892 - 0.0673301816 - 0.0683355704 - 0.0693410188 - 0.0703461990 - 0.0713518485 - 0.0723578706 - 0.0733634382 - 0.0743684769 - 0.0753747523 - 0.0763814375 - 0.0773879215 - 0.0783942565 - 0.0794009119 - 0.0804076791 - 0.0814127624 - 0.0824181885 - 0.0834240466 - 0.0844298676 - 0.0854352489 - 0.0864404887 - 0.0874472633 - 0.0884538963 - 0.0894602016 - 0.0904667079 - 0.0914734229 - 0.0924795270 - 0.0934846178 - 0.0944902077 - 0.0954962522 - 0.0965018719 - 0.0975077078 - 0.0985157192 - 0.0995259583 - 0.1005385965 - 0.1015558019 - 0.1025787964 - 0.1036100015 - 0.1046487987 - 0.1056960970 - 0.1067529991 - 0.1078196988 - 0.1088960990 - 0.1099824011 - 0.1110825986 - 0.1121942028 - 0.1133178994 - 0.1144538969 - 0.1156022996 - 0.1167630032 - 0.1179357022 - 0.1191222966 - 0.1203233972 - 0.1215377003 - 0.1227634028 - 0.1240004003 - 0.1252481937 - 0.1265043020 - 0.1277696937 - 0.1290419996 - 0.1303198040 - 0.1316013932 - 0.1328857988 - 0.1341720074 - 0.1354583055 - 0.1367450058 - 0.1380295008 - 0.1393115968 - 0.1405912936 - 0.1418687999 - 0.1431439966 - 0.1444166005 - 0.1456868947 - 0.1469538957 - 0.1482183039 - 0.1494797021 - 0.1507384926 - 0.1519940048 - 0.1532454938 - 0.1544931978 - 0.1557371020 - 0.1569786072 - 0.1582174003 - 0.1594552994 - 0.1606920063 - 0.1619275063 - 0.1631624997 - 0.1643957943 - 0.1656281948 - 0.1668587029 - 0.1680880040 - 0.1693165004 - 0.1705441028 - 0.1717692018 - 0.1729893982 - 0.1742039025 - 0.1754093021 - 0.1766050011 - 0.1777881980 - 0.1789582074 - 0.1801135987 - 0.1812521964 - 0.1823734939 - 0.1834750026 - 0.1845587045 - 0.1856247932 - 0.1866752952 - 0.1877115965 - 0.1887340993 - 0.1897435039 - 0.1907389015 - 0.1917219013 - 0.1926929951 - 0.1936533004 - 0.1946032941 - 0.1955437064 - 0.1964759976 - 0.1974004060 - 0.1983195990 - 0.1992345005 - 0.2001473010 - 0.2010585964 - 0.2019691020 - 0.2028799951 - 0.2037910968 - 0.2047035992 - 0.2056163996 - 0.2065296024 - 0.2074443996 - 0.2083611935 - 0.2092798948 - 0.2101999074 - 0.2111200988 - 0.2120410055 - 0.2129642069 - 0.2138908952 - 0.2148218006 - 0.2157568038 - 0.2166953981 - 0.2176382989 - 0.2185852975 - 0.2195377946 - 0.2204961032 - 0.2214609981 - 0.2224321067 - 0.2234085947 - 0.2243912965 - 0.2253790945 - 0.2263737023 - 0.2273754030 - 0.2283847928 - 0.2294009030 - 0.2304224074 - 0.2314486951 - 0.2324783951 - 0.2335128933 - 0.2345526069 - 0.2355960011 - 0.2366417050 - 0.2376891971 - 0.2387381941 - 0.2397857010 - 0.2408334017 - 0.2418812960 - 0.2429291010 - 0.2439766973 - 0.2450250983 - 0.2460754067 - 0.2471272945 - 0.2481832057 - 0.2492412031 - 0.2503012121 - 0.2513638139 - 0.2524305880 - 0.2535029054 - 0.2545804977 - 0.2556641996 - 0.2567521036 - 0.2578442991 - 0.2589425147 - 0.2600479126 - 0.2611603141 - 0.2622781098 - 0.2634013891 - 0.2645292878 - 0.2656618953 - 0.2667996883 - 0.2679435015 - 0.2690925896 - 0.2702454925 - 0.2714022100 - 0.2725617886 - 0.2737230957 - 0.2748858929 - 0.2760506868 - 0.2772169113 - 0.2783834040 - 0.2795504928 - 0.2807163894 - 0.2818816900 - 0.2830451131 - 0.2842077017 - 0.2853684127 - 0.2865259051 - 0.2876791954 - 0.2888275087 - 0.2899697125 - 0.2911050022 - 0.2922331095 - 0.2933534086 - 0.2944662869 - 0.2955724895 - 0.2966713011 - 0.2977609038 - 0.2988406122 - 0.2999115884 - 0.3009747863 - 0.3020302951 - 0.3030776978 - 0.3041163087 - 0.3051471114 - 0.3061706126 - 0.3071883917 - 0.3082000911 - 0.3092052937 - 0.3102042973 - 0.3111982048 - 0.3121899068 - 0.3131796122 - 0.3141691983 - 0.3151580095 - 0.3161459863 - 0.3171347082 - 0.3181253076 - 0.3191179037 - 0.3201133013 - 0.3211115897 - 0.3221124113 - 0.3231154978 - 0.3241209090 - 0.3251287043 - 0.3261393011 - 0.3271540999 - 0.3281734884 - 0.3291963935 - 0.3302215934 - 0.3312487006 - 0.3322781920 - 0.3333105147 - 0.3343453109 - 0.3353832066 - 0.3364242017 - 0.3374665082 - 0.3385097980 - 0.3395538926 - 0.3405987024 - 0.3416439891 - 0.3426909149 - 0.3437392116 - 0.3447869122 - 0.3458350003 - 0.3468832076 - 0.3479309082 - 0.3489786088 - 0.3500263095 - 0.3510748148 - 0.3521223962 - 0.3531706035 - 0.3542183042 - 0.3552643061 - 0.3563081026 - 0.3573493958 - 0.3583877981 - 0.3594241142 - 0.3604596853 - 0.3614932895 - 0.3625242114 - 0.3635517061 - 0.3645758927 - 0.3655968010 - 0.3666146100 - 0.3676308095 - 0.3686437011 - 0.3696531951 - 0.3706597984 - 0.3716636896 - 0.3726654053 - 0.3736641109 - 0.3746609092 - 0.3756552041 - 0.3766474128 - 0.3776386082 - 0.3786295056 - 0.3796210885 - 0.3806126118 - 0.3816055954 - 0.3825981021 - 0.3835909069 - 0.3845843971 - 0.3855786026 - 0.3865729868 - 0.3875676095 - 0.3885630965 - 0.3895589113 - 0.3905552030 - 0.3915528059 - 0.3925518990 - 0.3935522139 - 0.3945525885 - 0.3955546916 - 0.3965587914 - 0.3975653052 - 0.3985737860 - 0.3995841146 - 0.4005962908 - 0.4016096890 - 0.4026258886 - 0.4036448896 - 0.4046654999 - 0.4056873918 - 0.4067110121 - 0.4077360928 - 0.4087626040 - 0.4097931087 - 0.4108271897 - 0.4118641913 - 0.4129031003 - 0.4139435887 - 0.4149853885 - 0.4160276055 - 0.4170711040 - 0.4181162119 - 0.4191623926 - 0.4202099144 - 0.4212583899 - 0.4223065972 - 0.4233539999 - 0.4244020879 - 0.4254502952 - 0.4264976084 - 0.4275454879 - 0.4285939932 - 0.4296421111 - 0.4306895137 - 0.4317376912 - 0.4327858984 - 0.4338330925 - 0.4348810017 - 0.4359295964 - 0.4369775951 - 0.4380249977 - 0.4390732050 - 0.4401214123 - 0.4411686063 - 0.4422164857 - 0.4432651997 - 0.4443131089 - 0.4453606009 - 0.4464088082 - 0.4474568963 - 0.4485040903 - 0.4495520890 - 0.4506008029 - 0.4516485929 - 0.4526961148 - 0.4537442923 - 0.4547924101 - 0.4558396041 - 0.4568876028 - 0.4579362869 - 0.4589839876 - 0.4600316882 - 0.4610798061 - 0.4621278942 - 0.4631750882 - 0.4642232060 - 0.4652718902 - 0.4663195014 - 0.4673672020 - 0.4684154093 - 0.4694634080 - 0.4705106914 - 0.4715586901 - 0.4726074934 - 0.4736549854 - 0.4747028053 - 0.4757508934 - 0.4767988920 - 0.4778462052 - 0.4788942039 - 0.4799430966 - 0.4809904993 - 0.4820382893 - 0.4830864966 - 0.4841342866 - 0.4851816893 - 0.4862298071 - 0.4872786999 - 0.4883260131 - 0.4893738031 - 0.4904220104 - 0.4914698005 - 0.4925172925 - 0.4935654104 - 0.4946143031 - 0.4956614971 - 0.4967094064 - 0.4977576137 - 0.4988053143 - 0.4998528063 - 0.5009009838 - 0.5019497871 - 0.5029969215 - 0.5040448904 - 0.5050930977 - 0.5061408281 - 0.5071883202 - 0.5082365870 - 0.5092852712 - 0.5103324056 - 0.5113804936 - 0.5124287009 - 0.5134763122 - 0.5145239234 - 0.5155721903 - 0.5166208148 - 0.5176678896 - 0.5187159777 - 0.5197641850 - 0.5208117962 - 0.5218594074 - 0.5229076743 - 0.5239562988 - 0.5250033736 - 0.5260515809 - 0.5270997882 - 0.5281472802 - 0.5291950107 - 0.5302432775 - 0.5312917829 - 0.5323389769 - 0.5333871245 - 0.5344352722 - 0.5354828238 - 0.5365304947 - 0.5375788808 - 0.5386273265 - 0.5396745205 - 0.5407227278 - 0.5417708755 - 0.5428183079 - 0.5438659787 - 0.5449144840 - 0.5459626913 - 0.5470100045 - 0.5480582118 - 0.5491064191 - 0.5501537919 - 0.5512015820 - 0.5522500873 - 0.5532981753 - 0.5543456078 - 0.5553936958 - 0.5564420223 - 0.5574892759 - 0.5585371256 - 0.5595856905 - 0.5606337190 - 0.5616810918 - 0.5627292991 - 0.5637775064 - 0.5648247004 - 0.5658726096 - 0.5669211745 - 0.5679692030 - 0.5690163970 - 0.5700634122 - 0.5711091757 - 0.5721533895 - 0.5731964707 - 0.5742384791 - 0.5752782226 - 0.5763154030 - 0.5773493052 - 0.5783768892 - 0.5793967247 - 0.5804104805 - 0.5814169049 - 0.5824149251 - 0.5834047794 - 0.5843868852 - 0.5853602290 - 0.5863254070 - 0.5872825980 - 0.5882346034 - 0.5891796947 - 0.5901175141 - 0.5910484195 - 0.5919718146 - 0.5928866863 - 0.5937936902 - 0.5946958065 - 0.5955929160 - 0.5964863896 - 0.5973765254 - 0.5982630849 - 0.5991474986 - 0.6000298262 - 0.6009125710 - 0.6017950773 - 0.6026778221 - 0.6035615206 - 0.6044462919 - 0.6053317189 - 0.6062173843 - 0.6071026921 - 0.6079885960 - 0.6088755727 - 0.6097620726 - 0.6106479764 - 0.6115351915 - 0.6124256849 - 0.6133217216 - 0.6142216921 - 0.6151272058 - 0.6160376072 - 0.6169527173 - 0.6178737283 - 0.6188027263 - 0.6197397113 - 0.6206821203 - 0.6216303110 - 0.6225833893 - 0.6235426068 - 0.6245095134 - 0.6254850030 - 0.6264699101 - 0.6274635792 - 0.6284654737 - 0.6294741035 - 0.6304889917 - 0.6315106750 - 0.6325399280 - 0.6335754991 - 0.6346142292 - 0.6356559992 - 0.6366981864 - 0.6377419233 - 0.6387875080 - 0.6398347020 - 0.6408823729 - 0.6419299841 - 0.6429790854 - 0.6440268755 - 0.6450746059 - 0.6461223960 - 0.6471703053 - 0.6482179761 - 0.6492655277 - 0.6503146887 - 0.6513624191 - 0.6524100900 - 0.6534578800 - 0.6545057893 - 0.6555535197 - 0.6566010714 - 0.6576502919 - 0.6586980224 - 0.6597455740 - 0.6607934833 - 0.6618412733 - 0.6628890038 - 0.6639366746 - 0.6649858952 - 0.6660335064 - 0.6670811772 - 0.6681290269 - 0.6691768169 - 0.6702244878 - 0.6712722778 - 0.6723213792 - 0.6733689904 - 0.6744167209 - 0.6754645109 - 0.6765123010 - 0.6775599718 - 0.6786078811 - 0.6796569228 - 0.6807044744 - 0.6817513704 - 0.6827965975 - 0.6838402748 - 0.6848834753 - 0.6859273911 - 0.6869723797 - 0.6880162954 - 0.6890599728 - 0.6901013255 - 0.6911396980 - 0.6921761036 - 0.6932122111 - 0.6942487955 - 0.6952843070 - 0.6963180900 - 0.6973488927 - 0.6983770728 - 0.6994026899 - 0.7004272938 - 0.7014517188 - 0.7024747133 - 0.7034965158 - 0.7045164704 - 0.7055342793 - 0.7065513730 - 0.7075693011 - 0.7085878849 - 0.7096056938 - 0.7106239200 - 0.7116401792 - 0.7126544118 - 0.7136672139 - 0.7146797776 - 0.7156928778 - 0.7167074084 - 0.7177237272 - 0.7187396884 - 0.7197557092 - 0.7207708955 - 0.7217856050 - 0.7227994204 - 0.7238122225 - 0.7248240113 - 0.7258350253 - 0.7268440127 - 0.7278503776 - 0.7288548946 - 0.7298564911 - 0.7308545709 - 0.7318491936 - 0.7328398228 - 0.7338244915 - 0.7348057032 - 0.7357832789 - 0.7367560267 - 0.7377241850 - 0.7386888862 - 0.7396495938 - 0.7406045794 - 0.7415547967 - 0.7425006032 - 0.7434419990 - 0.7443798780 - 0.7453150749 - 0.7462481856 - 0.7471804023 - 0.7481107712 - 0.7490395904 - 0.7499657869 - 0.7508903742 - 0.7518150210 - 0.7527397871 - 0.7536643744 - 0.7545883060 - 0.7555134296 - 0.7564398050 - 0.7573670149 - 0.7582947016 - 0.7592232227 - 0.7601537108 - 0.7610865235 - 0.7620201111 - 0.7629551888 - 0.7638924122 - 0.7648314238 - 0.7657712102 - 0.7667114735 - 0.7676535845 - 0.7685970068 - 0.7695416212 - 0.7704880238 - 0.7714353204 - 0.7723832130 - 0.7733322978 - 0.7742838264 - 0.7752370238 - 0.7761909962 - 0.7771469951 - 0.7781038880 - 0.7790613770 - 0.7800198793 - 0.7809798121 - 0.7819411755 - 0.7829020023 - 0.7838628888 - 0.7848243117 - 0.7857862115 - 0.7867478132 - 0.7877084017 - 0.7886680961 - 0.7896274924 - 0.7905864716 - 0.7915449142 - 0.7925025225 - 0.7934594154 - 0.7944151759 - 0.7953701019 - 0.7963243127 - 0.7972785234 - 0.7982317209 - 0.7991852760 - 0.8001390100 - 0.8010920286 - 0.8020442128 - 0.8029962182 - 0.8039492965 - 0.8049013019 - 0.8058534861 - 0.8068069220 - 0.8077614903 - 0.8087171912 - 0.8096737266 - 0.8106310964 - 0.8115898967 - 0.8125504851 - 0.8135117292 - 0.8144730926 - 0.8154355288 - 0.8163995743 - 0.8173645139 - 0.8183292150 - 0.8192948103 - 0.8202611208 - 0.8212277889 - 0.8221948743 - 0.8231617808 - 0.8241283894 - 0.8250942826 - 0.8260607123 - 0.8270269036 - 0.8279932737 - 0.8289610147 - 0.8299307227 - 0.8309032917 - 0.8318769932 - 0.8328524828 - 0.8338282704 - 0.8348054886 - 0.8357861042 - 0.8367711902 - 0.8377606273 - 0.8387532234 - 0.8397489190 - 0.8407462239 - 0.8417472839 - 0.8427532911 - 0.8437641859 - 0.8447790146 - 0.8457974195 - 0.8468207717 - 0.8478456736 - 0.8488733768 - 0.8499053717 - 0.8509420156 - 0.8519824147 - 0.8530250788 - 0.8540698290 - 0.8551142812 - 0.8561593294 - 0.8572056293 - 0.8582528830 - 0.8593006134 - 0.8603482842 - 0.8613973856 - 0.8624451160 - 0.8634920716 - 0.8645371199 - 0.8655791283 - 0.8666179776 - 0.8676548004 - 0.8686900735 - 0.8697226048 - 0.8707528710 - 0.8717787862 - 0.8727999926 - 0.8738167882 - 0.8748298287 - 0.8758404255 - 0.8768457770 - 0.8778470755 - 0.8788424730 - 0.8798323870 - 0.8808172941 - 0.8817983866 - 0.8827769756 - 0.8837512732 - 0.8847230077 - 0.8856912255 - 0.8866552711 - 0.8876156807 - 0.8885743022 - 0.8895329833 - 0.8904910088 - 0.8914486766 - 0.8924050927 - 0.8933603764 - 0.8943151832 - 0.8952693939 - 0.8962222934 - 0.8971732855 - 0.8981227875 - 0.8990685940 - 0.9000113010 - 0.9009503126 - 0.9018849134 - 0.9028136134 - 0.9037343860 - 0.9046463966 - 0.9055463076 - 0.9064350128 - 0.9073122144 - 0.9081791043 - 0.9090356827 - 0.9098809958 - 0.9107148051 - 0.9115344882 - 0.9123430252 - 0.9131417871 - 0.9139323235 - 0.9147145152 - 0.9154878259 - 0.9162527919 - 0.9170091748 - 0.9177588224 - 0.9185020924 - 0.9192394018 - 0.9199714065 - 0.9206994176 - 0.9214246869 - 0.9221466780 - 0.9228668809 - 0.9235867262 - 0.9243060946 - 0.9250255823 - 0.9257463217 - 0.9264692068 - 0.9271945953 - 0.9279209971 - 0.9286499023 - 0.9293829203 - 0.9301195741 - 0.9308602214 - 0.9316061139 - 0.9323577285 - 0.9331133962 - 0.9338728786 - 0.9346380830 - 0.9354091287 - 0.9361863136 - 0.9369704127 - 0.9377611279 - 0.9385566115 - 0.9393566251 - 0.9401628971 - 0.9409751892 - 0.9417932034 - 0.9426162839 - 0.9434437156 - 0.9442740083 - 0.9451068044 - 0.9459424019 - 0.9467787147 - 0.9476162195 - 0.9484547973 - 0.9492936134 - 0.9501317143 - 0.9509698153 - 0.9518089294 - 0.9526466727 - 0.9534838200 - 0.9543187022 - 0.9551491737 - 0.9559733272 - 0.9567909241 - 0.9575985074 - 0.9583951831 - 0.9591814876 - 0.9599570036 - 0.9607214928 - 0.9614744782 - 0.9622160196 - 0.9629415870 - 0.9636477828 - 0.9643381238 - 0.9650136828 - 0.9656763077 - 0.9663271904 - 0.9669669271 - 0.9675945044 - 0.9682068825 - 0.9688068032 - 0.9693955779 - 0.9699755907 - 0.9705489278 - 0.9711167216 - 0.9716787934 - 0.9722322226 - 0.9727811813 - 0.9733269215 - 0.9738715291 - 0.9744166136 - 0.9749621153 - 0.9755073190 - 0.9760522246 - 0.9765970707 - 0.9771423936 - 0.9776893854 - 0.9782382250 - 0.9787877202 - 0.9793367982 - 0.9798851013 - 0.9804323912 - 0.9809777737 - 0.9815199971 - 0.9820588231 - 0.9825943708 - 0.9831265211 - 0.9836531281 - 0.9841737151 - 0.9846891761 - 0.9851980805 - 0.9857000709 - 0.9861956239 - 0.9866847992 - 0.9871674180 - 0.9876440167 - 0.9881147146 - 0.9885799289 - 0.9890390038 - 0.9894917011 - 0.9899380803 - 0.9903783798 - 0.9908131957 - 0.9912436008 - 0.9916706085 - 0.9920943975 - 0.9925168157 - 0.9929386973 - 0.9933599234 - 0.9937797189 - 0.9941993952 - 0.9946184158 - 0.9950373173 - 0.9954559803 - 0.9958732724 - 0.9962888956 - 0.9967045188 - 0.9971199036 - 0.9975342751 - 0.9979482293 - 0.9983612895 - 0.9987732172 - 0.9991837740 - 0.9995930195 - 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 1 +{ + 0.0000000000 + 0.0016059499 + 0.0032039359 + 0.0047925338 + 0.0063681109 + 0.0079261437 + 0.0094620753 + 0.0109717799 + 0.0124478899 + 0.0138870701 + 0.0152903199 + 0.0166584495 + 0.0179926194 + 0.0192952193 + 0.0205696207 + 0.0218127500 + 0.0230190307 + 0.0241936091 + 0.0253404994 + 0.0264616702 + 0.0275589097 + 0.0286343992 + 0.0296888705 + 0.0307215601 + 0.0317354687 + 0.0327326916 + 0.0337138698 + 0.0346802585 + 0.0356336907 + 0.0365758091 + 0.0375103503 + 0.0384392217 + 0.0393667594 + 0.0402943417 + 0.0412232615 + 0.0421543792 + 0.0430876091 + 0.0440218188 + 0.0449589603 + 0.0459001884 + 0.0468437783 + 0.0477895401 + 0.0487375483 + 0.0496875793 + 0.0506395511 + 0.0515942089 + 0.0525525808 + 0.0535144582 + 0.0544794910 + 0.0554478317 + 0.0564199202 + 0.0573957488 + 0.0583746210 + 0.0593575612 + 0.0603439212 + 0.0613333285 + 0.0623258613 + 0.0633219033 + 0.0643215328 + 0.0653229132 + 0.0663260892 + 0.0673301816 + 0.0683355704 + 0.0693410188 + 0.0703461990 + 0.0713518485 + 0.0723578706 + 0.0733634382 + 0.0743684769 + 0.0753747523 + 0.0763814375 + 0.0773879215 + 0.0783942565 + 0.0794009119 + 0.0804076791 + 0.0814127624 + 0.0824181885 + 0.0834240466 + 0.0844298676 + 0.0854352489 + 0.0864404887 + 0.0874472633 + 0.0884538963 + 0.0894602016 + 0.0904667079 + 0.0914734229 + 0.0924795270 + 0.0934846178 + 0.0944902077 + 0.0954962522 + 0.0965018719 + 0.0975077078 + 0.0985157192 + 0.0995259583 + 0.1005385965 + 0.1015558019 + 0.1025787964 + 0.1036100015 + 0.1046487987 + 0.1056960970 + 0.1067529991 + 0.1078196988 + 0.1088960990 + 0.1099824011 + 0.1110825986 + 0.1121942028 + 0.1133178994 + 0.1144538969 + 0.1156022996 + 0.1167630032 + 0.1179357022 + 0.1191222966 + 0.1203233972 + 0.1215377003 + 0.1227634028 + 0.1240004003 + 0.1252481937 + 0.1265043020 + 0.1277696937 + 0.1290419996 + 0.1303198040 + 0.1316013932 + 0.1328857988 + 0.1341720074 + 0.1354583055 + 0.1367450058 + 0.1380295008 + 0.1393115968 + 0.1405912936 + 0.1418687999 + 0.1431439966 + 0.1444166005 + 0.1456868947 + 0.1469538957 + 0.1482183039 + 0.1494797021 + 0.1507384926 + 0.1519940048 + 0.1532454938 + 0.1544931978 + 0.1557371020 + 0.1569786072 + 0.1582174003 + 0.1594552994 + 0.1606920063 + 0.1619275063 + 0.1631624997 + 0.1643957943 + 0.1656281948 + 0.1668587029 + 0.1680880040 + 0.1693165004 + 0.1705441028 + 0.1717692018 + 0.1729893982 + 0.1742039025 + 0.1754093021 + 0.1766050011 + 0.1777881980 + 0.1789582074 + 0.1801135987 + 0.1812521964 + 0.1823734939 + 0.1834750026 + 0.1845587045 + 0.1856247932 + 0.1866752952 + 0.1877115965 + 0.1887340993 + 0.1897435039 + 0.1907389015 + 0.1917219013 + 0.1926929951 + 0.1936533004 + 0.1946032941 + 0.1955437064 + 0.1964759976 + 0.1974004060 + 0.1983195990 + 0.1992345005 + 0.2001473010 + 0.2010585964 + 0.2019691020 + 0.2028799951 + 0.2037910968 + 0.2047035992 + 0.2056163996 + 0.2065296024 + 0.2074443996 + 0.2083611935 + 0.2092798948 + 0.2101999074 + 0.2111200988 + 0.2120410055 + 0.2129642069 + 0.2138908952 + 0.2148218006 + 0.2157568038 + 0.2166953981 + 0.2176382989 + 0.2185852975 + 0.2195377946 + 0.2204961032 + 0.2214609981 + 0.2224321067 + 0.2234085947 + 0.2243912965 + 0.2253790945 + 0.2263737023 + 0.2273754030 + 0.2283847928 + 0.2294009030 + 0.2304224074 + 0.2314486951 + 0.2324783951 + 0.2335128933 + 0.2345526069 + 0.2355960011 + 0.2366417050 + 0.2376891971 + 0.2387381941 + 0.2397857010 + 0.2408334017 + 0.2418812960 + 0.2429291010 + 0.2439766973 + 0.2450250983 + 0.2460754067 + 0.2471272945 + 0.2481832057 + 0.2492412031 + 0.2503012121 + 0.2513638139 + 0.2524305880 + 0.2535029054 + 0.2545804977 + 0.2556641996 + 0.2567521036 + 0.2578442991 + 0.2589425147 + 0.2600479126 + 0.2611603141 + 0.2622781098 + 0.2634013891 + 0.2645292878 + 0.2656618953 + 0.2667996883 + 0.2679435015 + 0.2690925896 + 0.2702454925 + 0.2714022100 + 0.2725617886 + 0.2737230957 + 0.2748858929 + 0.2760506868 + 0.2772169113 + 0.2783834040 + 0.2795504928 + 0.2807163894 + 0.2818816900 + 0.2830451131 + 0.2842077017 + 0.2853684127 + 0.2865259051 + 0.2876791954 + 0.2888275087 + 0.2899697125 + 0.2911050022 + 0.2922331095 + 0.2933534086 + 0.2944662869 + 0.2955724895 + 0.2966713011 + 0.2977609038 + 0.2988406122 + 0.2999115884 + 0.3009747863 + 0.3020302951 + 0.3030776978 + 0.3041163087 + 0.3051471114 + 0.3061706126 + 0.3071883917 + 0.3082000911 + 0.3092052937 + 0.3102042973 + 0.3111982048 + 0.3121899068 + 0.3131796122 + 0.3141691983 + 0.3151580095 + 0.3161459863 + 0.3171347082 + 0.3181253076 + 0.3191179037 + 0.3201133013 + 0.3211115897 + 0.3221124113 + 0.3231154978 + 0.3241209090 + 0.3251287043 + 0.3261393011 + 0.3271540999 + 0.3281734884 + 0.3291963935 + 0.3302215934 + 0.3312487006 + 0.3322781920 + 0.3333105147 + 0.3343453109 + 0.3353832066 + 0.3364242017 + 0.3374665082 + 0.3385097980 + 0.3395538926 + 0.3405987024 + 0.3416439891 + 0.3426909149 + 0.3437392116 + 0.3447869122 + 0.3458350003 + 0.3468832076 + 0.3479309082 + 0.3489786088 + 0.3500263095 + 0.3510748148 + 0.3521223962 + 0.3531706035 + 0.3542183042 + 0.3552643061 + 0.3563081026 + 0.3573493958 + 0.3583877981 + 0.3594241142 + 0.3604596853 + 0.3614932895 + 0.3625242114 + 0.3635517061 + 0.3645758927 + 0.3655968010 + 0.3666146100 + 0.3676308095 + 0.3686437011 + 0.3696531951 + 0.3706597984 + 0.3716636896 + 0.3726654053 + 0.3736641109 + 0.3746609092 + 0.3756552041 + 0.3766474128 + 0.3776386082 + 0.3786295056 + 0.3796210885 + 0.3806126118 + 0.3816055954 + 0.3825981021 + 0.3835909069 + 0.3845843971 + 0.3855786026 + 0.3865729868 + 0.3875676095 + 0.3885630965 + 0.3895589113 + 0.3905552030 + 0.3915528059 + 0.3925518990 + 0.3935522139 + 0.3945525885 + 0.3955546916 + 0.3965587914 + 0.3975653052 + 0.3985737860 + 0.3995841146 + 0.4005962908 + 0.4016096890 + 0.4026258886 + 0.4036448896 + 0.4046654999 + 0.4056873918 + 0.4067110121 + 0.4077360928 + 0.4087626040 + 0.4097931087 + 0.4108271897 + 0.4118641913 + 0.4129031003 + 0.4139435887 + 0.4149853885 + 0.4160276055 + 0.4170711040 + 0.4181162119 + 0.4191623926 + 0.4202099144 + 0.4212583899 + 0.4223065972 + 0.4233539999 + 0.4244020879 + 0.4254502952 + 0.4264976084 + 0.4275454879 + 0.4285939932 + 0.4296421111 + 0.4306895137 + 0.4317376912 + 0.4327858984 + 0.4338330925 + 0.4348810017 + 0.4359295964 + 0.4369775951 + 0.4380249977 + 0.4390732050 + 0.4401214123 + 0.4411686063 + 0.4422164857 + 0.4432651997 + 0.4443131089 + 0.4453606009 + 0.4464088082 + 0.4474568963 + 0.4485040903 + 0.4495520890 + 0.4506008029 + 0.4516485929 + 0.4526961148 + 0.4537442923 + 0.4547924101 + 0.4558396041 + 0.4568876028 + 0.4579362869 + 0.4589839876 + 0.4600316882 + 0.4610798061 + 0.4621278942 + 0.4631750882 + 0.4642232060 + 0.4652718902 + 0.4663195014 + 0.4673672020 + 0.4684154093 + 0.4694634080 + 0.4705106914 + 0.4715586901 + 0.4726074934 + 0.4736549854 + 0.4747028053 + 0.4757508934 + 0.4767988920 + 0.4778462052 + 0.4788942039 + 0.4799430966 + 0.4809904993 + 0.4820382893 + 0.4830864966 + 0.4841342866 + 0.4851816893 + 0.4862298071 + 0.4872786999 + 0.4883260131 + 0.4893738031 + 0.4904220104 + 0.4914698005 + 0.4925172925 + 0.4935654104 + 0.4946143031 + 0.4956614971 + 0.4967094064 + 0.4977576137 + 0.4988053143 + 0.4998528063 + 0.5009009838 + 0.5019497871 + 0.5029969215 + 0.5040448904 + 0.5050930977 + 0.5061408281 + 0.5071883202 + 0.5082365870 + 0.5092852712 + 0.5103324056 + 0.5113804936 + 0.5124287009 + 0.5134763122 + 0.5145239234 + 0.5155721903 + 0.5166208148 + 0.5176678896 + 0.5187159777 + 0.5197641850 + 0.5208117962 + 0.5218594074 + 0.5229076743 + 0.5239562988 + 0.5250033736 + 0.5260515809 + 0.5270997882 + 0.5281472802 + 0.5291950107 + 0.5302432775 + 0.5312917829 + 0.5323389769 + 0.5333871245 + 0.5344352722 + 0.5354828238 + 0.5365304947 + 0.5375788808 + 0.5386273265 + 0.5396745205 + 0.5407227278 + 0.5417708755 + 0.5428183079 + 0.5438659787 + 0.5449144840 + 0.5459626913 + 0.5470100045 + 0.5480582118 + 0.5491064191 + 0.5501537919 + 0.5512015820 + 0.5522500873 + 0.5532981753 + 0.5543456078 + 0.5553936958 + 0.5564420223 + 0.5574892759 + 0.5585371256 + 0.5595856905 + 0.5606337190 + 0.5616810918 + 0.5627292991 + 0.5637775064 + 0.5648247004 + 0.5658726096 + 0.5669211745 + 0.5679692030 + 0.5690163970 + 0.5700634122 + 0.5711091757 + 0.5721533895 + 0.5731964707 + 0.5742384791 + 0.5752782226 + 0.5763154030 + 0.5773493052 + 0.5783768892 + 0.5793967247 + 0.5804104805 + 0.5814169049 + 0.5824149251 + 0.5834047794 + 0.5843868852 + 0.5853602290 + 0.5863254070 + 0.5872825980 + 0.5882346034 + 0.5891796947 + 0.5901175141 + 0.5910484195 + 0.5919718146 + 0.5928866863 + 0.5937936902 + 0.5946958065 + 0.5955929160 + 0.5964863896 + 0.5973765254 + 0.5982630849 + 0.5991474986 + 0.6000298262 + 0.6009125710 + 0.6017950773 + 0.6026778221 + 0.6035615206 + 0.6044462919 + 0.6053317189 + 0.6062173843 + 0.6071026921 + 0.6079885960 + 0.6088755727 + 0.6097620726 + 0.6106479764 + 0.6115351915 + 0.6124256849 + 0.6133217216 + 0.6142216921 + 0.6151272058 + 0.6160376072 + 0.6169527173 + 0.6178737283 + 0.6188027263 + 0.6197397113 + 0.6206821203 + 0.6216303110 + 0.6225833893 + 0.6235426068 + 0.6245095134 + 0.6254850030 + 0.6264699101 + 0.6274635792 + 0.6284654737 + 0.6294741035 + 0.6304889917 + 0.6315106750 + 0.6325399280 + 0.6335754991 + 0.6346142292 + 0.6356559992 + 0.6366981864 + 0.6377419233 + 0.6387875080 + 0.6398347020 + 0.6408823729 + 0.6419299841 + 0.6429790854 + 0.6440268755 + 0.6450746059 + 0.6461223960 + 0.6471703053 + 0.6482179761 + 0.6492655277 + 0.6503146887 + 0.6513624191 + 0.6524100900 + 0.6534578800 + 0.6545057893 + 0.6555535197 + 0.6566010714 + 0.6576502919 + 0.6586980224 + 0.6597455740 + 0.6607934833 + 0.6618412733 + 0.6628890038 + 0.6639366746 + 0.6649858952 + 0.6660335064 + 0.6670811772 + 0.6681290269 + 0.6691768169 + 0.6702244878 + 0.6712722778 + 0.6723213792 + 0.6733689904 + 0.6744167209 + 0.6754645109 + 0.6765123010 + 0.6775599718 + 0.6786078811 + 0.6796569228 + 0.6807044744 + 0.6817513704 + 0.6827965975 + 0.6838402748 + 0.6848834753 + 0.6859273911 + 0.6869723797 + 0.6880162954 + 0.6890599728 + 0.6901013255 + 0.6911396980 + 0.6921761036 + 0.6932122111 + 0.6942487955 + 0.6952843070 + 0.6963180900 + 0.6973488927 + 0.6983770728 + 0.6994026899 + 0.7004272938 + 0.7014517188 + 0.7024747133 + 0.7034965158 + 0.7045164704 + 0.7055342793 + 0.7065513730 + 0.7075693011 + 0.7085878849 + 0.7096056938 + 0.7106239200 + 0.7116401792 + 0.7126544118 + 0.7136672139 + 0.7146797776 + 0.7156928778 + 0.7167074084 + 0.7177237272 + 0.7187396884 + 0.7197557092 + 0.7207708955 + 0.7217856050 + 0.7227994204 + 0.7238122225 + 0.7248240113 + 0.7258350253 + 0.7268440127 + 0.7278503776 + 0.7288548946 + 0.7298564911 + 0.7308545709 + 0.7318491936 + 0.7328398228 + 0.7338244915 + 0.7348057032 + 0.7357832789 + 0.7367560267 + 0.7377241850 + 0.7386888862 + 0.7396495938 + 0.7406045794 + 0.7415547967 + 0.7425006032 + 0.7434419990 + 0.7443798780 + 0.7453150749 + 0.7462481856 + 0.7471804023 + 0.7481107712 + 0.7490395904 + 0.7499657869 + 0.7508903742 + 0.7518150210 + 0.7527397871 + 0.7536643744 + 0.7545883060 + 0.7555134296 + 0.7564398050 + 0.7573670149 + 0.7582947016 + 0.7592232227 + 0.7601537108 + 0.7610865235 + 0.7620201111 + 0.7629551888 + 0.7638924122 + 0.7648314238 + 0.7657712102 + 0.7667114735 + 0.7676535845 + 0.7685970068 + 0.7695416212 + 0.7704880238 + 0.7714353204 + 0.7723832130 + 0.7733322978 + 0.7742838264 + 0.7752370238 + 0.7761909962 + 0.7771469951 + 0.7781038880 + 0.7790613770 + 0.7800198793 + 0.7809798121 + 0.7819411755 + 0.7829020023 + 0.7838628888 + 0.7848243117 + 0.7857862115 + 0.7867478132 + 0.7877084017 + 0.7886680961 + 0.7896274924 + 0.7905864716 + 0.7915449142 + 0.7925025225 + 0.7934594154 + 0.7944151759 + 0.7953701019 + 0.7963243127 + 0.7972785234 + 0.7982317209 + 0.7991852760 + 0.8001390100 + 0.8010920286 + 0.8020442128 + 0.8029962182 + 0.8039492965 + 0.8049013019 + 0.8058534861 + 0.8068069220 + 0.8077614903 + 0.8087171912 + 0.8096737266 + 0.8106310964 + 0.8115898967 + 0.8125504851 + 0.8135117292 + 0.8144730926 + 0.8154355288 + 0.8163995743 + 0.8173645139 + 0.8183292150 + 0.8192948103 + 0.8202611208 + 0.8212277889 + 0.8221948743 + 0.8231617808 + 0.8241283894 + 0.8250942826 + 0.8260607123 + 0.8270269036 + 0.8279932737 + 0.8289610147 + 0.8299307227 + 0.8309032917 + 0.8318769932 + 0.8328524828 + 0.8338282704 + 0.8348054886 + 0.8357861042 + 0.8367711902 + 0.8377606273 + 0.8387532234 + 0.8397489190 + 0.8407462239 + 0.8417472839 + 0.8427532911 + 0.8437641859 + 0.8447790146 + 0.8457974195 + 0.8468207717 + 0.8478456736 + 0.8488733768 + 0.8499053717 + 0.8509420156 + 0.8519824147 + 0.8530250788 + 0.8540698290 + 0.8551142812 + 0.8561593294 + 0.8572056293 + 0.8582528830 + 0.8593006134 + 0.8603482842 + 0.8613973856 + 0.8624451160 + 0.8634920716 + 0.8645371199 + 0.8655791283 + 0.8666179776 + 0.8676548004 + 0.8686900735 + 0.8697226048 + 0.8707528710 + 0.8717787862 + 0.8727999926 + 0.8738167882 + 0.8748298287 + 0.8758404255 + 0.8768457770 + 0.8778470755 + 0.8788424730 + 0.8798323870 + 0.8808172941 + 0.8817983866 + 0.8827769756 + 0.8837512732 + 0.8847230077 + 0.8856912255 + 0.8866552711 + 0.8876156807 + 0.8885743022 + 0.8895329833 + 0.8904910088 + 0.8914486766 + 0.8924050927 + 0.8933603764 + 0.8943151832 + 0.8952693939 + 0.8962222934 + 0.8971732855 + 0.8981227875 + 0.8990685940 + 0.9000113010 + 0.9009503126 + 0.9018849134 + 0.9028136134 + 0.9037343860 + 0.9046463966 + 0.9055463076 + 0.9064350128 + 0.9073122144 + 0.9081791043 + 0.9090356827 + 0.9098809958 + 0.9107148051 + 0.9115344882 + 0.9123430252 + 0.9131417871 + 0.9139323235 + 0.9147145152 + 0.9154878259 + 0.9162527919 + 0.9170091748 + 0.9177588224 + 0.9185020924 + 0.9192394018 + 0.9199714065 + 0.9206994176 + 0.9214246869 + 0.9221466780 + 0.9228668809 + 0.9235867262 + 0.9243060946 + 0.9250255823 + 0.9257463217 + 0.9264692068 + 0.9271945953 + 0.9279209971 + 0.9286499023 + 0.9293829203 + 0.9301195741 + 0.9308602214 + 0.9316061139 + 0.9323577285 + 0.9331133962 + 0.9338728786 + 0.9346380830 + 0.9354091287 + 0.9361863136 + 0.9369704127 + 0.9377611279 + 0.9385566115 + 0.9393566251 + 0.9401628971 + 0.9409751892 + 0.9417932034 + 0.9426162839 + 0.9434437156 + 0.9442740083 + 0.9451068044 + 0.9459424019 + 0.9467787147 + 0.9476162195 + 0.9484547973 + 0.9492936134 + 0.9501317143 + 0.9509698153 + 0.9518089294 + 0.9526466727 + 0.9534838200 + 0.9543187022 + 0.9551491737 + 0.9559733272 + 0.9567909241 + 0.9575985074 + 0.9583951831 + 0.9591814876 + 0.9599570036 + 0.9607214928 + 0.9614744782 + 0.9622160196 + 0.9629415870 + 0.9636477828 + 0.9643381238 + 0.9650136828 + 0.9656763077 + 0.9663271904 + 0.9669669271 + 0.9675945044 + 0.9682068825 + 0.9688068032 + 0.9693955779 + 0.9699755907 + 0.9705489278 + 0.9711167216 + 0.9716787934 + 0.9722322226 + 0.9727811813 + 0.9733269215 + 0.9738715291 + 0.9744166136 + 0.9749621153 + 0.9755073190 + 0.9760522246 + 0.9765970707 + 0.9771423936 + 0.9776893854 + 0.9782382250 + 0.9787877202 + 0.9793367982 + 0.9798851013 + 0.9804323912 + 0.9809777737 + 0.9815199971 + 0.9820588231 + 0.9825943708 + 0.9831265211 + 0.9836531281 + 0.9841737151 + 0.9846891761 + 0.9851980805 + 0.9857000709 + 0.9861956239 + 0.9866847992 + 0.9871674180 + 0.9876440167 + 0.9881147146 + 0.9885799289 + 0.9890390038 + 0.9894917011 + 0.9899380803 + 0.9903783798 + 0.9908131957 + 0.9912436008 + 0.9916706085 + 0.9920943975 + 0.9925168157 + 0.9929386973 + 0.9933599234 + 0.9937797189 + 0.9941993952 + 0.9946184158 + 0.9950373173 + 0.9954559803 + 0.9958732724 + 0.9962888956 + 0.9967045188 + 0.9971199036 + 0.9975342751 + 0.9979482293 + 0.9983612895 + 0.9987732172 + 0.9991837740 + 0.9995930195 + 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kai-0372.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kai-0372.spi1d index 11eae36a..7febe1e7 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kai-0372.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kai-0372.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 1 -{ - 0.0000000000 - 0.0014187270 - 0.0028485169 - 0.0042931479 - 0.0057487562 - 0.0072099129 - 0.0086716842 - 0.0101303104 - 0.0115825599 - 0.0130244503 - 0.0144514600 - 0.0158604495 - 0.0172503907 - 0.0186199509 - 0.0199686904 - 0.0212968104 - 0.0226029102 - 0.0238885507 - 0.0251513291 - 0.0263916291 - 0.0276103709 - 0.0288079195 - 0.0299857892 - 0.0311455000 - 0.0322885215 - 0.0334165096 - 0.0345297195 - 0.0356270298 - 0.0367095582 - 0.0377803184 - 0.0388391204 - 0.0398863591 - 0.0409265012 - 0.0419625007 - 0.0429961495 - 0.0440296903 - 0.0450651906 - 0.0461052805 - 0.0471526794 - 0.0482083187 - 0.0492714494 - 0.0503420308 - 0.0514214598 - 0.0525106192 - 0.0536095686 - 0.0547187701 - 0.0558380783 - 0.0569689386 - 0.0581119917 - 0.0592671484 - 0.0604341701 - 0.0616126209 - 0.0628031567 - 0.0640061498 - 0.0652208924 - 0.0664469600 - 0.0676843375 - 0.0689309686 - 0.0701846108 - 0.0714438409 - 0.0727065727 - 0.0739705265 - 0.0752329826 - 0.0764907077 - 0.0777410120 - 0.0789828077 - 0.0802149475 - 0.0814361572 - 0.0826453418 - 0.0838416368 - 0.0850252435 - 0.0861970633 - 0.0873567313 - 0.0885045677 - 0.0896422267 - 0.0907687172 - 0.0918845534 - 0.0929897428 - 0.0940850228 - 0.0951717719 - 0.0962497294 - 0.0973178074 - 0.0983761922 - 0.0994268134 - 0.1004718989 - 0.1015126035 - 0.1025492996 - 0.1035822034 - 0.1046125963 - 0.1056426987 - 0.1066747978 - 0.1077087969 - 0.1087436005 - 0.1097813025 - 0.1108229011 - 0.1118687019 - 0.1129188985 - 0.1139732003 - 0.1150322035 - 0.1160956025 - 0.1171633974 - 0.1182366982 - 0.1193158031 - 0.1204013005 - 0.1214938983 - 0.1225937977 - 0.1236993000 - 0.1248105019 - 0.1259278059 - 0.1270509064 - 0.1281802952 - 0.1293144971 - 0.1304522008 - 0.1315927953 - 0.1327365935 - 0.1338832974 - 0.1350333989 - 0.1361864060 - 0.1373412013 - 0.1384979934 - 0.1396571994 - 0.1408192068 - 0.1419841945 - 0.1431522071 - 0.1443247050 - 0.1455022991 - 0.1466850936 - 0.1478727013 - 0.1490653008 - 0.1502631009 - 0.1514662057 - 0.1526743025 - 0.1538881063 - 0.1551087052 - 0.1563372016 - 0.1575720012 - 0.1588124931 - 0.1600576937 - 0.1613081992 - 0.1625638008 - 0.1638233066 - 0.1650867015 - 0.1663545072 - 0.1676256955 - 0.1688988060 - 0.1701712012 - 0.1714410037 - 0.1727070063 - 0.1739681065 - 0.1752225012 - 0.1764675975 - 0.1777033061 - 0.1789285988 - 0.1801432073 - 0.1813486964 - 0.1825450063 - 0.1837306023 - 0.1849053055 - 0.1860695928 - 0.1872238964 - 0.1883683056 - 0.1895032972 - 0.1906294972 - 0.1917479932 - 0.1928582937 - 0.1939616054 - 0.1950584054 - 0.1961483955 - 0.1972329021 - 0.1983121932 - 0.1993871927 - 0.2004588991 - 0.2015292943 - 0.2025997043 - 0.2036705017 - 0.2047414035 - 0.2058123052 - 0.2068836987 - 0.2079551965 - 0.2090263963 - 0.2100979984 - 0.2111708969 - 0.2122471929 - 0.2133263052 - 0.2144072950 - 0.2154909968 - 0.2165782005 - 0.2176689953 - 0.2187630981 - 0.2198591977 - 0.2209576964 - 0.2220599949 - 0.2231667042 - 0.2242773026 - 0.2253910005 - 0.2265066952 - 0.2276258022 - 0.2287493050 - 0.2298769057 - 0.2310086936 - 0.2321445048 - 0.2332839072 - 0.2344263941 - 0.2355709970 - 0.2367177010 - 0.2378668934 - 0.2390172035 - 0.2401686013 - 0.2413198054 - 0.2424692959 - 0.2436169982 - 0.2447617054 - 0.2459024042 - 0.2470397949 - 0.2481732070 - 0.2493015975 - 0.2504242957 - 0.2515400052 - 0.2526471913 - 0.2537457049 - 0.2548359036 - 0.2559179068 - 0.2569903135 - 0.2580535114 - 0.2591086030 - 0.2601554990 - 0.2611947060 - 0.2622261941 - 0.2632490993 - 0.2642633021 - 0.2652696073 - 0.2662687898 - 0.2672609985 - 0.2682465911 - 0.2692261040 - 0.2702012062 - 0.2711735070 - 0.2721416056 - 0.2731072903 - 0.2740713060 - 0.2750340104 - 0.2759960890 - 0.2769576907 - 0.2779189944 - 0.2788805068 - 0.2798438966 - 0.2808105946 - 0.2817816138 - 0.2827576995 - 0.2837403119 - 0.2847315073 - 0.2857330143 - 0.2867462039 - 0.2877714932 - 0.2888084948 - 0.2898587883 - 0.2909233868 - 0.2920028865 - 0.2930966914 - 0.2942047119 - 0.2953279912 - 0.2964681983 - 0.2976255119 - 0.2987984121 - 0.2999858856 - 0.3011890948 - 0.3024066985 - 0.3036352992 - 0.3048754930 - 0.3061271012 - 0.3073874116 - 0.3086515963 - 0.3099172115 - 0.3111822903 - 0.3124446869 - 0.3137021959 - 0.3149535060 - 0.3161973953 - 0.3174318075 - 0.3186556995 - 0.3198691010 - 0.3210715055 - 0.3222627044 - 0.3234440982 - 0.3246166110 - 0.3257783055 - 0.3269309998 - 0.3280749023 - 0.3292089999 - 0.3303337097 - 0.3314495981 - 0.3325574100 - 0.3336577117 - 0.3347519934 - 0.3358413875 - 0.3369264007 - 0.3380081058 - 0.3390851021 - 0.3401587903 - 0.3412316144 - 0.3423039913 - 0.3433766961 - 0.3444486856 - 0.3455202878 - 0.3465914130 - 0.3476617932 - 0.3487313986 - 0.3498007059 - 0.3508692086 - 0.3519366980 - 0.3530029953 - 0.3540687859 - 0.3551346064 - 0.3562012017 - 0.3572680950 - 0.3583359122 - 0.3594044149 - 0.3604733944 - 0.3615426123 - 0.3626110852 - 0.3636786044 - 0.3647457063 - 0.3658131063 - 0.3668813109 - 0.3679508865 - 0.3690212965 - 0.3700926006 - 0.3711653054 - 0.3722378910 - 0.3733103871 - 0.3743830025 - 0.3754549921 - 0.3765265942 - 0.3775976896 - 0.3786680102 - 0.3797375858 - 0.3808068037 - 0.3818753064 - 0.3829427958 - 0.3840090036 - 0.3850749135 - 0.3861418962 - 0.3872109056 - 0.3882800937 - 0.3893499076 - 0.3904218972 - 0.3914968073 - 0.3925746083 - 0.3936544955 - 0.3947367072 - 0.3958213925 - 0.3969092071 - 0.3980003893 - 0.3990949988 - 0.4001933932 - 0.4012970924 - 0.4024052918 - 0.4035159945 - 0.4046308994 - 0.4057500064 - 0.4068731964 - 0.4080010056 - 0.4091325998 - 0.4102680981 - 0.4114075899 - 0.4125505984 - 0.4136967063 - 0.4148448110 - 0.4159938991 - 0.4171437919 - 0.4182941914 - 0.4194458127 - 0.4205985963 - 0.4217509925 - 0.4229008853 - 0.4240474999 - 0.4251902997 - 0.4263280034 - 0.4274600148 - 0.4285857975 - 0.4297052026 - 0.4308179021 - 0.4319226146 - 0.4330177009 - 0.4341033101 - 0.4351783991 - 0.4362432957 - 0.4372982979 - 0.4383428991 - 0.4393776059 - 0.4404034913 - 0.4414204061 - 0.4424285889 - 0.4434289932 - 0.4444229007 - 0.4454112053 - 0.4463939071 - 0.4473721981 - 0.4483464062 - 0.4493157864 - 0.4502818882 - 0.4512456954 - 0.4522084892 - 0.4531711042 - 0.4541336894 - 0.4550961852 - 0.4560593963 - 0.4570237100 - 0.4579895139 - 0.4589586854 - 0.4599328041 - 0.4609122872 - 0.4618987143 - 0.4628936052 - 0.4638980031 - 0.4649139047 - 0.4659439027 - 0.4669868946 - 0.4680424035 - 0.4691106081 - 0.4701919854 - 0.4712862968 - 0.4723939002 - 0.4735164940 - 0.4746555984 - 0.4758110046 - 0.4769817889 - 0.4781683981 - 0.4793711007 - 0.4805893898 - 0.4818193913 - 0.4830603004 - 0.4843108952 - 0.4855696857 - 0.4868358970 - 0.4881083071 - 0.4893856049 - 0.4906665087 - 0.4919497073 - 0.4932326972 - 0.4945130944 - 0.4957897067 - 0.4970611036 - 0.4983257055 - 0.4995830953 - 0.5008316040 - 0.5020682216 - 0.5032951832 - 0.5045124292 - 0.5057162046 - 0.5069069862 - 0.5080850720 - 0.5092489123 - 0.5103979707 - 0.5115343928 - 0.5126597285 - 0.5137739182 - 0.5148767829 - 0.5159683228 - 0.5170493722 - 0.5181217790 - 0.5191835165 - 0.5202353001 - 0.5212790966 - 0.5223165751 - 0.5233479738 - 0.5243744254 - 0.5253980756 - 0.5264204144 - 0.5274419188 - 0.5284639001 - 0.5294873118 - 0.5305123925 - 0.5315392017 - 0.5325689912 - 0.5336017013 - 0.5346363187 - 0.5356730819 - 0.5367122293 - 0.5377532840 - 0.5387955904 - 0.5398390293 - 0.5408840179 - 0.5419310927 - 0.5429803729 - 0.5440319777 - 0.5450868011 - 0.5461450219 - 0.5472044945 - 0.5482643843 - 0.5493268967 - 0.5503926873 - 0.5514603257 - 0.5525295138 - 0.5535998940 - 0.5546712279 - 0.5557432771 - 0.5568156838 - 0.5578880906 - 0.5589600801 - 0.5600317717 - 0.5611037016 - 0.5621762276 - 0.5632488728 - 0.5643215775 - 0.5653941035 - 0.5664659739 - 0.5675367713 - 0.5686054826 - 0.5696707964 - 0.5707319975 - 0.5717889071 - 0.5728414059 - 0.5738887787 - 0.5749312043 - 0.5759691000 - 0.5770025849 - 0.5780308247 - 0.5790535808 - 0.5800703168 - 0.5810797811 - 0.5820823908 - 0.5830807090 - 0.5840756297 - 0.5850659013 - 0.5860515833 - 0.5870333910 - 0.5880113840 - 0.5889859200 - 0.5899571776 - 0.5909250975 - 0.5918905139 - 0.5928549767 - 0.5938175917 - 0.5947793126 - 0.5957412124 - 0.5967022181 - 0.5976634026 - 0.5986256003 - 0.5995882750 - 0.6005504727 - 0.6015123725 - 0.6024752855 - 0.6034411192 - 0.6044095755 - 0.6053802967 - 0.6063545942 - 0.6073334813 - 0.6083168983 - 0.6093050838 - 0.6102979183 - 0.6112931967 - 0.6122915149 - 0.6132950187 - 0.6143041849 - 0.6153184175 - 0.6163371205 - 0.6173605919 - 0.6183896065 - 0.6194241047 - 0.6204634905 - 0.6215072870 - 0.6225566268 - 0.6236104965 - 0.6246663928 - 0.6257253885 - 0.6267880201 - 0.6278533936 - 0.6289204955 - 0.6299880743 - 0.6310554743 - 0.6321223974 - 0.6331890821 - 0.6342563033 - 0.6353240013 - 0.6363924742 - 0.6374620795 - 0.6385326982 - 0.6396048069 - 0.6406791806 - 0.6417564750 - 0.6428371072 - 0.6439194083 - 0.6450036168 - 0.6460906863 - 0.6471815109 - 0.6482766867 - 0.6493766904 - 0.6504800916 - 0.6515864134 - 0.6526964903 - 0.6538105011 - 0.6549285054 - 0.6560505033 - 0.6571758986 - 0.6583045721 - 0.6594374180 - 0.6605746746 - 0.6617149115 - 0.6628565192 - 0.6639989018 - 0.6651424170 - 0.6662871838 - 0.6674332023 - 0.6685804129 - 0.6697282791 - 0.6708768010 - 0.6720256209 - 0.6731721759 - 0.6743155122 - 0.6754553914 - 0.6765918732 - 0.6777250171 - 0.6788529754 - 0.6799747944 - 0.6810904145 - 0.6821988225 - 0.6832993031 - 0.6843917966 - 0.6854761839 - 0.6865515113 - 0.6876170039 - 0.6886734962 - 0.6897208095 - 0.6907575727 - 0.6917873025 - 0.6928105950 - 0.6938270926 - 0.6948360205 - 0.6958370209 - 0.6968308091 - 0.6978185773 - 0.6988012791 - 0.6997786760 - 0.7007514834 - 0.7017211914 - 0.7026876807 - 0.7036520243 - 0.7046157122 - 0.7055789828 - 0.7065418959 - 0.7075042725 - 0.7084662914 - 0.7094286084 - 0.7103922963 - 0.7113580704 - 0.7123268843 - 0.7132992744 - 0.7142757773 - 0.7152578831 - 0.7162470222 - 0.7172421813 - 0.7182437181 - 0.7192531824 - 0.7202717066 - 0.7212985754 - 0.7223340273 - 0.7233784199 - 0.7244324088 - 0.7254965901 - 0.7265704274 - 0.7276533246 - 0.7287458181 - 0.7298483849 - 0.7309604883 - 0.7320820093 - 0.7332108021 - 0.7343450189 - 0.7354853153 - 0.7366309166 - 0.7377815247 - 0.7389364839 - 0.7400947213 - 0.7412542105 - 0.7424135208 - 0.7435715199 - 0.7447264194 - 0.7458763719 - 0.7470207214 - 0.7481592894 - 0.7492917776 - 0.7504174113 - 0.7515341043 - 0.7526403069 - 0.7537366748 - 0.7548218966 - 0.7558972239 - 0.7569634914 - 0.7580205798 - 0.7590687275 - 0.7601084113 - 0.7611390948 - 0.7621600032 - 0.7631716728 - 0.7641760707 - 0.7651739717 - 0.7661651969 - 0.7671486735 - 0.7681260109 - 0.7690988183 - 0.7700666189 - 0.7710312009 - 0.7719941735 - 0.7729566097 - 0.7739174962 - 0.7748776078 - 0.7758383155 - 0.7767999768 - 0.7777621150 - 0.7787244916 - 0.7796872258 - 0.7806501985 - 0.7816135883 - 0.7825772762 - 0.7835413218 - 0.7845047116 - 0.7854663730 - 0.7864274979 - 0.7873893976 - 0.7883517742 - 0.7893143296 - 0.7902770042 - 0.7912396193 - 0.7922011018 - 0.7931612134 - 0.7941213846 - 0.7950831056 - 0.7960450053 - 0.7970073223 - 0.7979699969 - 0.7989329100 - 0.7998961806 - 0.8008598089 - 0.8018236756 - 0.8027874231 - 0.8037496805 - 0.8047108054 - 0.8056722879 - 0.8066347241 - 0.8075972199 - 0.8085597754 - 0.8095226288 - 0.8104845881 - 0.8114448190 - 0.8124048710 - 0.8133671284 - 0.8143309951 - 0.8152961731 - 0.8162639141 - 0.8172360063 - 0.8182134032 - 0.8191961050 - 0.8201839924 - 0.8211780787 - 0.8221784234 - 0.8231853843 - 0.8242005110 - 0.8252243996 - 0.8262565136 - 0.8272976875 - 0.8283491135 - 0.8294097185 - 0.8304784894 - 0.8315557837 - 0.8326426744 - 0.8337395787 - 0.8348457217 - 0.8359597921 - 0.8370814919 - 0.8382107019 - 0.8393465281 - 0.8404887915 - 0.8416360021 - 0.8427860141 - 0.8439379930 - 0.8450908065 - 0.8462439775 - 0.8473960161 - 0.8485453725 - 0.8496916890 - 0.8508347273 - 0.8519737124 - 0.8531072140 - 0.8542333841 - 0.8553516269 - 0.8564621210 - 0.8575645089 - 0.8586577177 - 0.8597412109 - 0.8608148098 - 0.8618785143 - 0.8629317284 - 0.8639749885 - 0.8650091290 - 0.8660346270 - 0.8670523167 - 0.8680629730 - 0.8690673709 - 0.8700646162 - 0.8710550070 - 0.8720393777 - 0.8730176091 - 0.8739910126 - 0.8749604225 - 0.8759278059 - 0.8768939972 - 0.8778584003 - 0.8788213134 - 0.8797836900 - 0.8807460070 - 0.8817077279 - 0.8826689720 - 0.8836311102 - 0.8845937848 - 0.8855553865 - 0.8865150809 - 0.8874728084 - 0.8884277940 - 0.8893800974 - 0.8903309107 - 0.8912788033 - 0.8922230005 - 0.8931636810 - 0.8941028714 - 0.8950411081 - 0.8959773779 - 0.8969107866 - 0.8978416920 - 0.8987693787 - 0.8996942043 - 0.9006167054 - 0.9015371799 - 0.9024555087 - 0.9033702016 - 0.9042806029 - 0.9051871896 - 0.9060915709 - 0.9069947004 - 0.9078966975 - 0.9087975025 - 0.9096974730 - 0.9105958939 - 0.9114922881 - 0.9123886228 - 0.9132850766 - 0.9141798019 - 0.9150691032 - 0.9159536958 - 0.9168338180 - 0.9177089930 - 0.9185785055 - 0.9194408059 - 0.9202948809 - 0.9211413264 - 0.9219813943 - 0.9228157997 - 0.9236447215 - 0.9244682193 - 0.9252852798 - 0.9260939956 - 0.9268960953 - 0.9276924133 - 0.9284831285 - 0.9292683005 - 0.9300467968 - 0.9308174849 - 0.9315804839 - 0.9323381186 - 0.9330930114 - 0.9338461757 - 0.9345973730 - 0.9353471994 - 0.9360958934 - 0.9368441105 - 0.9375926256 - 0.9383410811 - 0.9390903115 - 0.9398398995 - 0.9405894279 - 0.9413390756 - 0.9420892000 - 0.9428393245 - 0.9435886741 - 0.9443354011 - 0.9450780153 - 0.9458169937 - 0.9465532899 - 0.9472861886 - 0.9480143785 - 0.9487375021 - 0.9494557977 - 0.9501700997 - 0.9508795142 - 0.9515821934 - 0.9522789717 - 0.9529712796 - 0.9536584020 - 0.9543386102 - 0.9550122023 - 0.9556815028 - 0.9563481808 - 0.9570119977 - 0.9576709867 - 0.9583243728 - 0.9589740038 - 0.9596223235 - 0.9602677822 - 0.9609097242 - 0.9615486264 - 0.9621847272 - 0.9628177285 - 0.9634479284 - 0.9640747905 - 0.9646962881 - 0.9653108716 - 0.9659178257 - 0.9665163755 - 0.9671065211 - 0.9676873088 - 0.9682564139 - 0.9688133001 - 0.9693595767 - 0.9698956013 - 0.9704216719 - 0.9709377885 - 0.9714441895 - 0.9719427228 - 0.9724339247 - 0.9729166031 - 0.9733896852 - 0.9738553166 - 0.9743154049 - 0.9747707248 - 0.9752202034 - 0.9756631851 - 0.9761012197 - 0.9765369296 - 0.9769701958 - 0.9774006009 - 0.9778288007 - 0.9782562256 - 0.9786834121 - 0.9791104794 - 0.9795380831 - 0.9799662828 - 0.9803938866 - 0.9808201194 - 0.9812436104 - 0.9816619754 - 0.9820752144 - 0.9824826717 - 0.9828830957 - 0.9832751155 - 0.9836578965 - 0.9840303063 - 0.9843910933 - 0.9847406745 - 0.9850798845 - 0.9854090214 - 0.9857276082 - 0.9860363007 - 0.9863356948 - 0.9866275191 - 0.9869117141 - 0.9871860147 - 0.9874522090 - 0.9877108932 - 0.9879605770 - 0.9882022738 - 0.9884371161 - 0.9886657000 - 0.9888893962 - 0.9891092777 - 0.9893258214 - 0.9895405769 - 0.9897546768 - 0.9899684191 - 0.9901812077 - 0.9903942943 - 0.9906080961 - 0.9908217788 - 0.9910365939 - 0.9912521243 - 0.9914677143 - 0.9916833043 - 0.9918984771 - 0.9921129942 - 0.9923266172 - 0.9925395846 - 0.9927518964 - 0.9929633737 - 0.9931753278 - 0.9933875799 - 0.9935998917 - 0.9938135147 - 0.9940276146 - 0.9942417741 - 0.9944561124 - 0.9946699739 - 0.9948837757 - 0.9950978160 - 0.9953122139 - 0.9955270886 - 0.9957417846 - 0.9959560037 - 0.9961696267 - 0.9963824153 - 0.9965956211 - 0.9968093038 - 0.9970229864 - 0.9972379208 - 0.9974533916 - 0.9976689816 - 0.9978845716 - 0.9980998039 - 0.9983143210 - 0.9985278249 - 0.9987403750 - 0.9989517927 - 0.9991620183 - 0.9993718863 - 0.9995810986 - 0.9997903705 - 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 1 +{ + 0.0000000000 + 0.0014187270 + 0.0028485169 + 0.0042931479 + 0.0057487562 + 0.0072099129 + 0.0086716842 + 0.0101303104 + 0.0115825599 + 0.0130244503 + 0.0144514600 + 0.0158604495 + 0.0172503907 + 0.0186199509 + 0.0199686904 + 0.0212968104 + 0.0226029102 + 0.0238885507 + 0.0251513291 + 0.0263916291 + 0.0276103709 + 0.0288079195 + 0.0299857892 + 0.0311455000 + 0.0322885215 + 0.0334165096 + 0.0345297195 + 0.0356270298 + 0.0367095582 + 0.0377803184 + 0.0388391204 + 0.0398863591 + 0.0409265012 + 0.0419625007 + 0.0429961495 + 0.0440296903 + 0.0450651906 + 0.0461052805 + 0.0471526794 + 0.0482083187 + 0.0492714494 + 0.0503420308 + 0.0514214598 + 0.0525106192 + 0.0536095686 + 0.0547187701 + 0.0558380783 + 0.0569689386 + 0.0581119917 + 0.0592671484 + 0.0604341701 + 0.0616126209 + 0.0628031567 + 0.0640061498 + 0.0652208924 + 0.0664469600 + 0.0676843375 + 0.0689309686 + 0.0701846108 + 0.0714438409 + 0.0727065727 + 0.0739705265 + 0.0752329826 + 0.0764907077 + 0.0777410120 + 0.0789828077 + 0.0802149475 + 0.0814361572 + 0.0826453418 + 0.0838416368 + 0.0850252435 + 0.0861970633 + 0.0873567313 + 0.0885045677 + 0.0896422267 + 0.0907687172 + 0.0918845534 + 0.0929897428 + 0.0940850228 + 0.0951717719 + 0.0962497294 + 0.0973178074 + 0.0983761922 + 0.0994268134 + 0.1004718989 + 0.1015126035 + 0.1025492996 + 0.1035822034 + 0.1046125963 + 0.1056426987 + 0.1066747978 + 0.1077087969 + 0.1087436005 + 0.1097813025 + 0.1108229011 + 0.1118687019 + 0.1129188985 + 0.1139732003 + 0.1150322035 + 0.1160956025 + 0.1171633974 + 0.1182366982 + 0.1193158031 + 0.1204013005 + 0.1214938983 + 0.1225937977 + 0.1236993000 + 0.1248105019 + 0.1259278059 + 0.1270509064 + 0.1281802952 + 0.1293144971 + 0.1304522008 + 0.1315927953 + 0.1327365935 + 0.1338832974 + 0.1350333989 + 0.1361864060 + 0.1373412013 + 0.1384979934 + 0.1396571994 + 0.1408192068 + 0.1419841945 + 0.1431522071 + 0.1443247050 + 0.1455022991 + 0.1466850936 + 0.1478727013 + 0.1490653008 + 0.1502631009 + 0.1514662057 + 0.1526743025 + 0.1538881063 + 0.1551087052 + 0.1563372016 + 0.1575720012 + 0.1588124931 + 0.1600576937 + 0.1613081992 + 0.1625638008 + 0.1638233066 + 0.1650867015 + 0.1663545072 + 0.1676256955 + 0.1688988060 + 0.1701712012 + 0.1714410037 + 0.1727070063 + 0.1739681065 + 0.1752225012 + 0.1764675975 + 0.1777033061 + 0.1789285988 + 0.1801432073 + 0.1813486964 + 0.1825450063 + 0.1837306023 + 0.1849053055 + 0.1860695928 + 0.1872238964 + 0.1883683056 + 0.1895032972 + 0.1906294972 + 0.1917479932 + 0.1928582937 + 0.1939616054 + 0.1950584054 + 0.1961483955 + 0.1972329021 + 0.1983121932 + 0.1993871927 + 0.2004588991 + 0.2015292943 + 0.2025997043 + 0.2036705017 + 0.2047414035 + 0.2058123052 + 0.2068836987 + 0.2079551965 + 0.2090263963 + 0.2100979984 + 0.2111708969 + 0.2122471929 + 0.2133263052 + 0.2144072950 + 0.2154909968 + 0.2165782005 + 0.2176689953 + 0.2187630981 + 0.2198591977 + 0.2209576964 + 0.2220599949 + 0.2231667042 + 0.2242773026 + 0.2253910005 + 0.2265066952 + 0.2276258022 + 0.2287493050 + 0.2298769057 + 0.2310086936 + 0.2321445048 + 0.2332839072 + 0.2344263941 + 0.2355709970 + 0.2367177010 + 0.2378668934 + 0.2390172035 + 0.2401686013 + 0.2413198054 + 0.2424692959 + 0.2436169982 + 0.2447617054 + 0.2459024042 + 0.2470397949 + 0.2481732070 + 0.2493015975 + 0.2504242957 + 0.2515400052 + 0.2526471913 + 0.2537457049 + 0.2548359036 + 0.2559179068 + 0.2569903135 + 0.2580535114 + 0.2591086030 + 0.2601554990 + 0.2611947060 + 0.2622261941 + 0.2632490993 + 0.2642633021 + 0.2652696073 + 0.2662687898 + 0.2672609985 + 0.2682465911 + 0.2692261040 + 0.2702012062 + 0.2711735070 + 0.2721416056 + 0.2731072903 + 0.2740713060 + 0.2750340104 + 0.2759960890 + 0.2769576907 + 0.2779189944 + 0.2788805068 + 0.2798438966 + 0.2808105946 + 0.2817816138 + 0.2827576995 + 0.2837403119 + 0.2847315073 + 0.2857330143 + 0.2867462039 + 0.2877714932 + 0.2888084948 + 0.2898587883 + 0.2909233868 + 0.2920028865 + 0.2930966914 + 0.2942047119 + 0.2953279912 + 0.2964681983 + 0.2976255119 + 0.2987984121 + 0.2999858856 + 0.3011890948 + 0.3024066985 + 0.3036352992 + 0.3048754930 + 0.3061271012 + 0.3073874116 + 0.3086515963 + 0.3099172115 + 0.3111822903 + 0.3124446869 + 0.3137021959 + 0.3149535060 + 0.3161973953 + 0.3174318075 + 0.3186556995 + 0.3198691010 + 0.3210715055 + 0.3222627044 + 0.3234440982 + 0.3246166110 + 0.3257783055 + 0.3269309998 + 0.3280749023 + 0.3292089999 + 0.3303337097 + 0.3314495981 + 0.3325574100 + 0.3336577117 + 0.3347519934 + 0.3358413875 + 0.3369264007 + 0.3380081058 + 0.3390851021 + 0.3401587903 + 0.3412316144 + 0.3423039913 + 0.3433766961 + 0.3444486856 + 0.3455202878 + 0.3465914130 + 0.3476617932 + 0.3487313986 + 0.3498007059 + 0.3508692086 + 0.3519366980 + 0.3530029953 + 0.3540687859 + 0.3551346064 + 0.3562012017 + 0.3572680950 + 0.3583359122 + 0.3594044149 + 0.3604733944 + 0.3615426123 + 0.3626110852 + 0.3636786044 + 0.3647457063 + 0.3658131063 + 0.3668813109 + 0.3679508865 + 0.3690212965 + 0.3700926006 + 0.3711653054 + 0.3722378910 + 0.3733103871 + 0.3743830025 + 0.3754549921 + 0.3765265942 + 0.3775976896 + 0.3786680102 + 0.3797375858 + 0.3808068037 + 0.3818753064 + 0.3829427958 + 0.3840090036 + 0.3850749135 + 0.3861418962 + 0.3872109056 + 0.3882800937 + 0.3893499076 + 0.3904218972 + 0.3914968073 + 0.3925746083 + 0.3936544955 + 0.3947367072 + 0.3958213925 + 0.3969092071 + 0.3980003893 + 0.3990949988 + 0.4001933932 + 0.4012970924 + 0.4024052918 + 0.4035159945 + 0.4046308994 + 0.4057500064 + 0.4068731964 + 0.4080010056 + 0.4091325998 + 0.4102680981 + 0.4114075899 + 0.4125505984 + 0.4136967063 + 0.4148448110 + 0.4159938991 + 0.4171437919 + 0.4182941914 + 0.4194458127 + 0.4205985963 + 0.4217509925 + 0.4229008853 + 0.4240474999 + 0.4251902997 + 0.4263280034 + 0.4274600148 + 0.4285857975 + 0.4297052026 + 0.4308179021 + 0.4319226146 + 0.4330177009 + 0.4341033101 + 0.4351783991 + 0.4362432957 + 0.4372982979 + 0.4383428991 + 0.4393776059 + 0.4404034913 + 0.4414204061 + 0.4424285889 + 0.4434289932 + 0.4444229007 + 0.4454112053 + 0.4463939071 + 0.4473721981 + 0.4483464062 + 0.4493157864 + 0.4502818882 + 0.4512456954 + 0.4522084892 + 0.4531711042 + 0.4541336894 + 0.4550961852 + 0.4560593963 + 0.4570237100 + 0.4579895139 + 0.4589586854 + 0.4599328041 + 0.4609122872 + 0.4618987143 + 0.4628936052 + 0.4638980031 + 0.4649139047 + 0.4659439027 + 0.4669868946 + 0.4680424035 + 0.4691106081 + 0.4701919854 + 0.4712862968 + 0.4723939002 + 0.4735164940 + 0.4746555984 + 0.4758110046 + 0.4769817889 + 0.4781683981 + 0.4793711007 + 0.4805893898 + 0.4818193913 + 0.4830603004 + 0.4843108952 + 0.4855696857 + 0.4868358970 + 0.4881083071 + 0.4893856049 + 0.4906665087 + 0.4919497073 + 0.4932326972 + 0.4945130944 + 0.4957897067 + 0.4970611036 + 0.4983257055 + 0.4995830953 + 0.5008316040 + 0.5020682216 + 0.5032951832 + 0.5045124292 + 0.5057162046 + 0.5069069862 + 0.5080850720 + 0.5092489123 + 0.5103979707 + 0.5115343928 + 0.5126597285 + 0.5137739182 + 0.5148767829 + 0.5159683228 + 0.5170493722 + 0.5181217790 + 0.5191835165 + 0.5202353001 + 0.5212790966 + 0.5223165751 + 0.5233479738 + 0.5243744254 + 0.5253980756 + 0.5264204144 + 0.5274419188 + 0.5284639001 + 0.5294873118 + 0.5305123925 + 0.5315392017 + 0.5325689912 + 0.5336017013 + 0.5346363187 + 0.5356730819 + 0.5367122293 + 0.5377532840 + 0.5387955904 + 0.5398390293 + 0.5408840179 + 0.5419310927 + 0.5429803729 + 0.5440319777 + 0.5450868011 + 0.5461450219 + 0.5472044945 + 0.5482643843 + 0.5493268967 + 0.5503926873 + 0.5514603257 + 0.5525295138 + 0.5535998940 + 0.5546712279 + 0.5557432771 + 0.5568156838 + 0.5578880906 + 0.5589600801 + 0.5600317717 + 0.5611037016 + 0.5621762276 + 0.5632488728 + 0.5643215775 + 0.5653941035 + 0.5664659739 + 0.5675367713 + 0.5686054826 + 0.5696707964 + 0.5707319975 + 0.5717889071 + 0.5728414059 + 0.5738887787 + 0.5749312043 + 0.5759691000 + 0.5770025849 + 0.5780308247 + 0.5790535808 + 0.5800703168 + 0.5810797811 + 0.5820823908 + 0.5830807090 + 0.5840756297 + 0.5850659013 + 0.5860515833 + 0.5870333910 + 0.5880113840 + 0.5889859200 + 0.5899571776 + 0.5909250975 + 0.5918905139 + 0.5928549767 + 0.5938175917 + 0.5947793126 + 0.5957412124 + 0.5967022181 + 0.5976634026 + 0.5986256003 + 0.5995882750 + 0.6005504727 + 0.6015123725 + 0.6024752855 + 0.6034411192 + 0.6044095755 + 0.6053802967 + 0.6063545942 + 0.6073334813 + 0.6083168983 + 0.6093050838 + 0.6102979183 + 0.6112931967 + 0.6122915149 + 0.6132950187 + 0.6143041849 + 0.6153184175 + 0.6163371205 + 0.6173605919 + 0.6183896065 + 0.6194241047 + 0.6204634905 + 0.6215072870 + 0.6225566268 + 0.6236104965 + 0.6246663928 + 0.6257253885 + 0.6267880201 + 0.6278533936 + 0.6289204955 + 0.6299880743 + 0.6310554743 + 0.6321223974 + 0.6331890821 + 0.6342563033 + 0.6353240013 + 0.6363924742 + 0.6374620795 + 0.6385326982 + 0.6396048069 + 0.6406791806 + 0.6417564750 + 0.6428371072 + 0.6439194083 + 0.6450036168 + 0.6460906863 + 0.6471815109 + 0.6482766867 + 0.6493766904 + 0.6504800916 + 0.6515864134 + 0.6526964903 + 0.6538105011 + 0.6549285054 + 0.6560505033 + 0.6571758986 + 0.6583045721 + 0.6594374180 + 0.6605746746 + 0.6617149115 + 0.6628565192 + 0.6639989018 + 0.6651424170 + 0.6662871838 + 0.6674332023 + 0.6685804129 + 0.6697282791 + 0.6708768010 + 0.6720256209 + 0.6731721759 + 0.6743155122 + 0.6754553914 + 0.6765918732 + 0.6777250171 + 0.6788529754 + 0.6799747944 + 0.6810904145 + 0.6821988225 + 0.6832993031 + 0.6843917966 + 0.6854761839 + 0.6865515113 + 0.6876170039 + 0.6886734962 + 0.6897208095 + 0.6907575727 + 0.6917873025 + 0.6928105950 + 0.6938270926 + 0.6948360205 + 0.6958370209 + 0.6968308091 + 0.6978185773 + 0.6988012791 + 0.6997786760 + 0.7007514834 + 0.7017211914 + 0.7026876807 + 0.7036520243 + 0.7046157122 + 0.7055789828 + 0.7065418959 + 0.7075042725 + 0.7084662914 + 0.7094286084 + 0.7103922963 + 0.7113580704 + 0.7123268843 + 0.7132992744 + 0.7142757773 + 0.7152578831 + 0.7162470222 + 0.7172421813 + 0.7182437181 + 0.7192531824 + 0.7202717066 + 0.7212985754 + 0.7223340273 + 0.7233784199 + 0.7244324088 + 0.7254965901 + 0.7265704274 + 0.7276533246 + 0.7287458181 + 0.7298483849 + 0.7309604883 + 0.7320820093 + 0.7332108021 + 0.7343450189 + 0.7354853153 + 0.7366309166 + 0.7377815247 + 0.7389364839 + 0.7400947213 + 0.7412542105 + 0.7424135208 + 0.7435715199 + 0.7447264194 + 0.7458763719 + 0.7470207214 + 0.7481592894 + 0.7492917776 + 0.7504174113 + 0.7515341043 + 0.7526403069 + 0.7537366748 + 0.7548218966 + 0.7558972239 + 0.7569634914 + 0.7580205798 + 0.7590687275 + 0.7601084113 + 0.7611390948 + 0.7621600032 + 0.7631716728 + 0.7641760707 + 0.7651739717 + 0.7661651969 + 0.7671486735 + 0.7681260109 + 0.7690988183 + 0.7700666189 + 0.7710312009 + 0.7719941735 + 0.7729566097 + 0.7739174962 + 0.7748776078 + 0.7758383155 + 0.7767999768 + 0.7777621150 + 0.7787244916 + 0.7796872258 + 0.7806501985 + 0.7816135883 + 0.7825772762 + 0.7835413218 + 0.7845047116 + 0.7854663730 + 0.7864274979 + 0.7873893976 + 0.7883517742 + 0.7893143296 + 0.7902770042 + 0.7912396193 + 0.7922011018 + 0.7931612134 + 0.7941213846 + 0.7950831056 + 0.7960450053 + 0.7970073223 + 0.7979699969 + 0.7989329100 + 0.7998961806 + 0.8008598089 + 0.8018236756 + 0.8027874231 + 0.8037496805 + 0.8047108054 + 0.8056722879 + 0.8066347241 + 0.8075972199 + 0.8085597754 + 0.8095226288 + 0.8104845881 + 0.8114448190 + 0.8124048710 + 0.8133671284 + 0.8143309951 + 0.8152961731 + 0.8162639141 + 0.8172360063 + 0.8182134032 + 0.8191961050 + 0.8201839924 + 0.8211780787 + 0.8221784234 + 0.8231853843 + 0.8242005110 + 0.8252243996 + 0.8262565136 + 0.8272976875 + 0.8283491135 + 0.8294097185 + 0.8304784894 + 0.8315557837 + 0.8326426744 + 0.8337395787 + 0.8348457217 + 0.8359597921 + 0.8370814919 + 0.8382107019 + 0.8393465281 + 0.8404887915 + 0.8416360021 + 0.8427860141 + 0.8439379930 + 0.8450908065 + 0.8462439775 + 0.8473960161 + 0.8485453725 + 0.8496916890 + 0.8508347273 + 0.8519737124 + 0.8531072140 + 0.8542333841 + 0.8553516269 + 0.8564621210 + 0.8575645089 + 0.8586577177 + 0.8597412109 + 0.8608148098 + 0.8618785143 + 0.8629317284 + 0.8639749885 + 0.8650091290 + 0.8660346270 + 0.8670523167 + 0.8680629730 + 0.8690673709 + 0.8700646162 + 0.8710550070 + 0.8720393777 + 0.8730176091 + 0.8739910126 + 0.8749604225 + 0.8759278059 + 0.8768939972 + 0.8778584003 + 0.8788213134 + 0.8797836900 + 0.8807460070 + 0.8817077279 + 0.8826689720 + 0.8836311102 + 0.8845937848 + 0.8855553865 + 0.8865150809 + 0.8874728084 + 0.8884277940 + 0.8893800974 + 0.8903309107 + 0.8912788033 + 0.8922230005 + 0.8931636810 + 0.8941028714 + 0.8950411081 + 0.8959773779 + 0.8969107866 + 0.8978416920 + 0.8987693787 + 0.8996942043 + 0.9006167054 + 0.9015371799 + 0.9024555087 + 0.9033702016 + 0.9042806029 + 0.9051871896 + 0.9060915709 + 0.9069947004 + 0.9078966975 + 0.9087975025 + 0.9096974730 + 0.9105958939 + 0.9114922881 + 0.9123886228 + 0.9132850766 + 0.9141798019 + 0.9150691032 + 0.9159536958 + 0.9168338180 + 0.9177089930 + 0.9185785055 + 0.9194408059 + 0.9202948809 + 0.9211413264 + 0.9219813943 + 0.9228157997 + 0.9236447215 + 0.9244682193 + 0.9252852798 + 0.9260939956 + 0.9268960953 + 0.9276924133 + 0.9284831285 + 0.9292683005 + 0.9300467968 + 0.9308174849 + 0.9315804839 + 0.9323381186 + 0.9330930114 + 0.9338461757 + 0.9345973730 + 0.9353471994 + 0.9360958934 + 0.9368441105 + 0.9375926256 + 0.9383410811 + 0.9390903115 + 0.9398398995 + 0.9405894279 + 0.9413390756 + 0.9420892000 + 0.9428393245 + 0.9435886741 + 0.9443354011 + 0.9450780153 + 0.9458169937 + 0.9465532899 + 0.9472861886 + 0.9480143785 + 0.9487375021 + 0.9494557977 + 0.9501700997 + 0.9508795142 + 0.9515821934 + 0.9522789717 + 0.9529712796 + 0.9536584020 + 0.9543386102 + 0.9550122023 + 0.9556815028 + 0.9563481808 + 0.9570119977 + 0.9576709867 + 0.9583243728 + 0.9589740038 + 0.9596223235 + 0.9602677822 + 0.9609097242 + 0.9615486264 + 0.9621847272 + 0.9628177285 + 0.9634479284 + 0.9640747905 + 0.9646962881 + 0.9653108716 + 0.9659178257 + 0.9665163755 + 0.9671065211 + 0.9676873088 + 0.9682564139 + 0.9688133001 + 0.9693595767 + 0.9698956013 + 0.9704216719 + 0.9709377885 + 0.9714441895 + 0.9719427228 + 0.9724339247 + 0.9729166031 + 0.9733896852 + 0.9738553166 + 0.9743154049 + 0.9747707248 + 0.9752202034 + 0.9756631851 + 0.9761012197 + 0.9765369296 + 0.9769701958 + 0.9774006009 + 0.9778288007 + 0.9782562256 + 0.9786834121 + 0.9791104794 + 0.9795380831 + 0.9799662828 + 0.9803938866 + 0.9808201194 + 0.9812436104 + 0.9816619754 + 0.9820752144 + 0.9824826717 + 0.9828830957 + 0.9832751155 + 0.9836578965 + 0.9840303063 + 0.9843910933 + 0.9847406745 + 0.9850798845 + 0.9854090214 + 0.9857276082 + 0.9860363007 + 0.9863356948 + 0.9866275191 + 0.9869117141 + 0.9871860147 + 0.9874522090 + 0.9877108932 + 0.9879605770 + 0.9882022738 + 0.9884371161 + 0.9886657000 + 0.9888893962 + 0.9891092777 + 0.9893258214 + 0.9895405769 + 0.9897546768 + 0.9899684191 + 0.9901812077 + 0.9903942943 + 0.9906080961 + 0.9908217788 + 0.9910365939 + 0.9912521243 + 0.9914677143 + 0.9916833043 + 0.9918984771 + 0.9921129942 + 0.9923266172 + 0.9925395846 + 0.9927518964 + 0.9929633737 + 0.9931753278 + 0.9933875799 + 0.9935998917 + 0.9938135147 + 0.9940276146 + 0.9942417741 + 0.9944561124 + 0.9946699739 + 0.9948837757 + 0.9950978160 + 0.9953122139 + 0.9955270886 + 0.9957417846 + 0.9959560037 + 0.9961696267 + 0.9963824153 + 0.9965956211 + 0.9968093038 + 0.9970229864 + 0.9972379208 + 0.9974533916 + 0.9976689816 + 0.9978845716 + 0.9980998039 + 0.9983143210 + 0.9985278249 + 0.9987403750 + 0.9989517927 + 0.9991620183 + 0.9993718863 + 0.9995810986 + 0.9997903705 + 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kai-1010.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kai-1010.spi1d index 8fb1df72..bb780530 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kai-1010.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kai-1010.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 1 -{ - 0.0000000000 - 0.0015970930 - 0.0032108270 - 0.0048384252 - 0.0064732390 - 0.0081081763 - 0.0097375941 - 0.0113548804 - 0.0129528800 - 0.0145261399 - 0.0160703696 - 0.0175831709 - 0.0190630294 - 0.0205102507 - 0.0219259299 - 0.0233127493 - 0.0246715993 - 0.0260010492 - 0.0272994898 - 0.0285670608 - 0.0298028998 - 0.0310098194 - 0.0321902186 - 0.0333435684 - 0.0344703794 - 0.0355738886 - 0.0366554409 - 0.0377166718 - 0.0387589782 - 0.0397843607 - 0.0407951698 - 0.0417945310 - 0.0427865982 - 0.0437750705 - 0.0447619408 - 0.0457485691 - 0.0467366986 - 0.0477271006 - 0.0487186685 - 0.0497123487 - 0.0507081710 - 0.0517065898 - 0.0527076609 - 0.0537111685 - 0.0547169782 - 0.0557249896 - 0.0567362607 - 0.0577516407 - 0.0587707087 - 0.0597942285 - 0.0608223192 - 0.0618541911 - 0.0628895611 - 0.0639279112 - 0.0649689585 - 0.0660121813 - 0.0670581907 - 0.0681073666 - 0.0691590682 - 0.0702132285 - 0.0712696612 - 0.0723281205 - 0.0733884126 - 0.0744497478 - 0.0755119175 - 0.0765746608 - 0.0776376799 - 0.0787018836 - 0.0797673315 - 0.0808331519 - 0.0818990618 - 0.0829651132 - 0.0840317011 - 0.0850984082 - 0.0861652717 - 0.0872325823 - 0.0882994309 - 0.0893664882 - 0.0904344097 - 0.0915023014 - 0.0925698727 - 0.0936371982 - 0.0947050005 - 0.0957735702 - 0.0968423486 - 0.0979111120 - 0.0989789367 - 0.1000462994 - 0.1011143997 - 0.1021834984 - 0.1032544971 - 0.1043291986 - 0.1054088995 - 0.1064950973 - 0.1075880006 - 0.1086886004 - 0.1097972989 - 0.1109144986 - 0.1120416969 - 0.1131796017 - 0.1143285036 - 0.1154886037 - 0.1166604012 - 0.1178442016 - 0.1190397963 - 0.1202476025 - 0.1214689016 - 0.1227027997 - 0.1239494979 - 0.1252094954 - 0.1264819056 - 0.1277655959 - 0.1290591955 - 0.1303616017 - 0.1316712052 - 0.1329866052 - 0.1343073994 - 0.1356315017 - 0.1369580030 - 0.1382858008 - 0.1396144927 - 0.1409430057 - 0.1422718018 - 0.1436003000 - 0.1449290067 - 0.1462575048 - 0.1475863010 - 0.1489147991 - 0.1502435058 - 0.1515720040 - 0.1529008001 - 0.1542291939 - 0.1555580050 - 0.1568865031 - 0.1582152992 - 0.1595436931 - 0.1608725041 - 0.1622010022 - 0.1635297984 - 0.1648582071 - 0.1661870033 - 0.1675153971 - 0.1688434035 - 0.1701688021 - 0.1714915931 - 0.1728098989 - 0.1741224974 - 0.1754271984 - 0.1767227948 - 0.1780070066 - 0.1792788059 - 0.1805343926 - 0.1817727983 - 0.1829933971 - 0.1841960996 - 0.1853809059 - 0.1865482926 - 0.1876983047 - 0.1888300031 - 0.1899435073 - 0.1910407990 - 0.1921222061 - 0.1931896061 - 0.1942428052 - 0.1952829063 - 0.1963098943 - 0.1973251998 - 0.1983291954 - 0.1993234009 - 0.2003085017 - 0.2012864053 - 0.2022587955 - 0.2032283992 - 0.2041957974 - 0.2051616013 - 0.2061273009 - 0.2070931047 - 0.2080595940 - 0.2090270072 - 0.2099955976 - 0.2109649032 - 0.2119348943 - 0.2129058987 - 0.2138797939 - 0.2148582041 - 0.2158416957 - 0.2168301046 - 0.2178225964 - 0.2188192010 - 0.2198204994 - 0.2208279073 - 0.2218416035 - 0.2228616029 - 0.2238875031 - 0.2249186039 - 0.2259557992 - 0.2269994020 - 0.2280489057 - 0.2291045040 - 0.2301660031 - 0.2312328964 - 0.2323053926 - 0.2333831042 - 0.2344655991 - 0.2355525047 - 0.2366431057 - 0.2377368063 - 0.2388329953 - 0.2399305999 - 0.2410296053 - 0.2421302944 - 0.2432314008 - 0.2443311065 - 0.2454299033 - 0.2465299964 - 0.2476332933 - 0.2487404048 - 0.2498511970 - 0.2509643137 - 0.2520790994 - 0.2531974018 - 0.2543210089 - 0.2554507852 - 0.2565864921 - 0.2577267885 - 0.2588709891 - 0.2600202858 - 0.2611759901 - 0.2623381019 - 0.2635067105 - 0.2646813989 - 0.2658607960 - 0.2670449913 - 0.2682338953 - 0.2694280148 - 0.2706277072 - 0.2718321085 - 0.2730394900 - 0.2742492855 - 0.2754603922 - 0.2766728103 - 0.2778874040 - 0.2791036963 - 0.2803192139 - 0.2815321982 - 0.2827436030 - 0.2839539051 - 0.2851620913 - 0.2863675058 - 0.2875689864 - 0.2887648046 - 0.2899549007 - 0.2911390960 - 0.2923170924 - 0.2934887111 - 0.2946529090 - 0.2958090901 - 0.2969568968 - 0.2980965972 - 0.2992289066 - 0.3003537953 - 0.3014717996 - 0.3025816977 - 0.3036831915 - 0.3047770858 - 0.3058654070 - 0.3069495857 - 0.3080292046 - 0.3091028929 - 0.3101705909 - 0.3112339079 - 0.3122943938 - 0.3133535087 - 0.3144116104 - 0.3154678941 - 0.3165228963 - 0.3175787926 - 0.3186368942 - 0.3196958899 - 0.3207564056 - 0.3218187094 - 0.3228828907 - 0.3239508867 - 0.3250220120 - 0.3260951936 - 0.3271706104 - 0.3282474875 - 0.3293263912 - 0.3304072917 - 0.3314906955 - 0.3325760067 - 0.3336637020 - 0.3347538114 - 0.3358460069 - 0.3369395137 - 0.3380326927 - 0.3391278088 - 0.3402253985 - 0.3413242996 - 0.3424232900 - 0.3435212970 - 0.3446190953 - 0.3457185030 - 0.3468185961 - 0.3479177952 - 0.3490158916 - 0.3501127958 - 0.3512093127 - 0.3523041010 - 0.3533973098 - 0.3544895947 - 0.3555799127 - 0.3566688895 - 0.3577584922 - 0.3588475883 - 0.3599354923 - 0.3610216081 - 0.3621057868 - 0.3631885946 - 0.3642696142 - 0.3653489947 - 0.3664267957 - 0.3675036132 - 0.3685795963 - 0.3696550131 - 0.3707289100 - 0.3718005121 - 0.3728711009 - 0.3739413917 - 0.3750106096 - 0.3760792017 - 0.3771468997 - 0.3782134950 - 0.3792808056 - 0.3803485930 - 0.3814165890 - 0.3824853003 - 0.3835538924 - 0.3846215010 - 0.3856872022 - 0.3867521882 - 0.3878181875 - 0.3888849020 - 0.3899520934 - 0.3910199106 - 0.3920885026 - 0.3931584954 - 0.3942297995 - 0.3953022957 - 0.3963758051 - 0.3974499106 - 0.3985247016 - 0.3996005952 - 0.4006777108 - 0.4017564952 - 0.4028376937 - 0.4039207101 - 0.4050048888 - 0.4060907960 - 0.4071780145 - 0.4082660973 - 0.4093562067 - 0.4104484916 - 0.4115422070 - 0.4126375020 - 0.4137335122 - 0.4148307145 - 0.4159297049 - 0.4170301855 - 0.4181306064 - 0.4192298055 - 0.4203279912 - 0.4214271009 - 0.4225274920 - 0.4236279130 - 0.4247274995 - 0.4258260131 - 0.4269242883 - 0.4280230105 - 0.4291217029 - 0.4302200079 - 0.4313178957 - 0.4324154854 - 0.4335126877 - 0.4346086085 - 0.4357028902 - 0.4367968142 - 0.4378911853 - 0.4389862120 - 0.4400813878 - 0.4411762953 - 0.4422703981 - 0.4433647990 - 0.4444605112 - 0.4455575943 - 0.4466555119 - 0.4477527142 - 0.4488491118 - 0.4499464929 - 0.4510458112 - 0.4521470070 - 0.4532484114 - 0.4543491900 - 0.4554497004 - 0.4565511048 - 0.4576537907 - 0.4587570131 - 0.4598602057 - 0.4609631896 - 0.4620659947 - 0.4631690979 - 0.4642724097 - 0.4653758109 - 0.4664795995 - 0.4675835967 - 0.4686864913 - 0.4697881043 - 0.4708893895 - 0.4719910920 - 0.4730935991 - 0.4741966128 - 0.4752987921 - 0.4763998985 - 0.4775001109 - 0.4785997868 - 0.4797008038 - 0.4808028042 - 0.4819043875 - 0.4830043018 - 0.4841024876 - 0.4852007926 - 0.4863008857 - 0.4874025881 - 0.4885045886 - 0.4896053970 - 0.4907051027 - 0.4918052852 - 0.4929065108 - 0.4940088987 - 0.4951120019 - 0.4962143004 - 0.4973159134 - 0.4984172881 - 0.4995189011 - 0.5006219745 - 0.5017260909 - 0.5028306842 - 0.5039374232 - 0.5050470233 - 0.5061581731 - 0.5072711706 - 0.5083870292 - 0.5095049739 - 0.5106257796 - 0.5117477775 - 0.5128695965 - 0.5139923096 - 0.5151175261 - 0.5162450075 - 0.5173735023 - 0.5185019970 - 0.5196316242 - 0.5207641125 - 0.5218989849 - 0.5230355859 - 0.5241733193 - 0.5253111720 - 0.5264499187 - 0.5275893807 - 0.5287293792 - 0.5298690200 - 0.5310084820 - 0.5321471095 - 0.5332846045 - 0.5344207883 - 0.5355551839 - 0.5366871953 - 0.5378178954 - 0.5389484763 - 0.5400791168 - 0.5412082076 - 0.5423350930 - 0.5434600115 - 0.5445839763 - 0.5457080007 - 0.5468308926 - 0.5479511023 - 0.5490679741 - 0.5501825213 - 0.5512952805 - 0.5524055958 - 0.5535135269 - 0.5546203852 - 0.5557265282 - 0.5568308234 - 0.5579332709 - 0.5590345860 - 0.5601357222 - 0.5612370968 - 0.5623382926 - 0.5634381175 - 0.5645357966 - 0.5656321049 - 0.5667272210 - 0.5678204894 - 0.5689107776 - 0.5699964166 - 0.5710760951 - 0.5721501112 - 0.5732188821 - 0.5742821097 - 0.5753387213 - 0.5763881803 - 0.5774298906 - 0.5784643292 - 0.5794925094 - 0.5805140138 - 0.5815274119 - 0.5825325847 - 0.5835304260 - 0.5845211148 - 0.5855050087 - 0.5864828229 - 0.5874555707 - 0.5884236097 - 0.5893865824 - 0.5903437138 - 0.5912957191 - 0.5922436714 - 0.5931885242 - 0.5941303968 - 0.5950701833 - 0.5960083008 - 0.5969455242 - 0.5978826284 - 0.5988199711 - 0.5997573733 - 0.6006944180 - 0.6016309261 - 0.6025679111 - 0.6035056710 - 0.6044442058 - 0.6053841710 - 0.6063274741 - 0.6072760224 - 0.6082304716 - 0.6091914177 - 0.6101577878 - 0.6111288071 - 0.6121054292 - 0.6130893230 - 0.6140809059 - 0.6150801778 - 0.6160869002 - 0.6171011925 - 0.6181237102 - 0.6191561222 - 0.6201986074 - 0.6212494969 - 0.6223081946 - 0.6233735085 - 0.6244443059 - 0.6255208850 - 0.6266024113 - 0.6276888251 - 0.6287800074 - 0.6298750043 - 0.6309717894 - 0.6320694089 - 0.6331679225 - 0.6342682838 - 0.6353698969 - 0.6364706755 - 0.6375694871 - 0.6386672854 - 0.6397666931 - 0.6408684254 - 0.6419709921 - 0.6430730224 - 0.6441742182 - 0.6452761889 - 0.6463810205 - 0.6474881172 - 0.6485970020 - 0.6497082114 - 0.6508216262 - 0.6519368887 - 0.6530544162 - 0.6541745067 - 0.6552979946 - 0.6564245224 - 0.6575521231 - 0.6586806178 - 0.6598100066 - 0.6609410048 - 0.6620740294 - 0.6632086039 - 0.6643440127 - 0.6654803157 - 0.6666182280 - 0.6677569747 - 0.6688960791 - 0.6700353026 - 0.6711744070 - 0.6723119020 - 0.6734473109 - 0.6745814085 - 0.6757146120 - 0.6768456101 - 0.6779726744 - 0.6790955067 - 0.6802144051 - 0.6813290119 - 0.6824392080 - 0.6835449934 - 0.6846470833 - 0.6857451797 - 0.6868383884 - 0.6879264116 - 0.6890088916 - 0.6900870204 - 0.6911612749 - 0.6922308803 - 0.6932950020 - 0.6943535805 - 0.6954085231 - 0.6964603066 - 0.6975083947 - 0.6985530257 - 0.6995959282 - 0.7006379962 - 0.7016788721 - 0.7027179003 - 0.7037551999 - 0.7047919035 - 0.7058281898 - 0.7068638206 - 0.7078987956 - 0.7089341879 - 0.7099704742 - 0.7110071182 - 0.7120435834 - 0.7130793929 - 0.7141141295 - 0.7151473761 - 0.7161788940 - 0.7172086835 - 0.7182366848 - 0.7192623019 - 0.7202852964 - 0.7213062048 - 0.7223259211 - 0.7233440876 - 0.7243599892 - 0.7253736258 - 0.7263845205 - 0.7273933291 - 0.7283999920 - 0.7294043899 - 0.7304064035 - 0.7314063907 - 0.7324048877 - 0.7334017158 - 0.7343968749 - 0.7353904247 - 0.7363826036 - 0.7373743057 - 0.7383652925 - 0.7393556833 - 0.7403460741 - 0.7413364053 - 0.7423262000 - 0.7433158755 - 0.7443063855 - 0.7452973723 - 0.7462878227 - 0.7472770810 - 0.7482659221 - 0.7492548823 - 0.7502439022 - 0.7512320876 - 0.7522203922 - 0.7532092929 - 0.7541986108 - 0.7551875114 - 0.7561764121 - 0.7571650743 - 0.7581533194 - 0.7591395974 - 0.7601243854 - 0.7611085773 - 0.7620921731 - 0.7630745769 - 0.7640557885 - 0.7650362253 - 0.7660158277 - 0.7669950724 - 0.7679738998 - 0.7689527273 - 0.7699314952 - 0.7709106803 - 0.7718899846 - 0.7728688121 - 0.7738466263 - 0.7748246789 - 0.7758036852 - 0.7767832279 - 0.7777634859 - 0.7787454724 - 0.7797296047 - 0.7807155252 - 0.7817035913 - 0.7826942801 - 0.7836887836 - 0.7846868038 - 0.7856897116 - 0.7866985202 - 0.7877131104 - 0.7887325287 - 0.7897546887 - 0.7907801867 - 0.7918102145 - 0.7928441763 - 0.7938814163 - 0.7949218750 - 0.7959659100 - 0.7970144153 - 0.7980663180 - 0.7991213799 - 0.8001785874 - 0.8012385964 - 0.8023005128 - 0.8033639193 - 0.8044286966 - 0.8054934740 - 0.8065580130 - 0.8076239824 - 0.8086903095 - 0.8097563982 - 0.8108221292 - 0.8118875027 - 0.8129522204 - 0.8140147924 - 0.8150777221 - 0.8161420822 - 0.8172072172 - 0.8182731867 - 0.8193401098 - 0.8204079866 - 0.8214777708 - 0.8225491047 - 0.8236216903 - 0.8246955872 - 0.8257701993 - 0.8268460035 - 0.8279229999 - 0.8290016055 - 0.8300824761 - 0.8311660886 - 0.8322516084 - 0.8333384991 - 0.8344269991 - 0.8355159163 - 0.8366057277 - 0.8376973867 - 0.8387901783 - 0.8398838043 - 0.8409780264 - 0.8420727849 - 0.8431686759 - 0.8442658782 - 0.8453642130 - 0.8464624882 - 0.8475595713 - 0.8486564159 - 0.8497542143 - 0.8508523703 - 0.8519496918 - 0.8530451059 - 0.8541380763 - 0.8552293777 - 0.8563184738 - 0.8574041724 - 0.8584862947 - 0.8595638275 - 0.8606367111 - 0.8617051244 - 0.8627687097 - 0.8638274074 - 0.8648813963 - 0.8659303784 - 0.8669738770 - 0.8680111766 - 0.8690428734 - 0.8700705171 - 0.8710942268 - 0.8721132874 - 0.8731275201 - 0.8741363287 - 0.8751401901 - 0.8761405945 - 0.8771389127 - 0.8781350851 - 0.8791291118 - 0.8801203966 - 0.8811098933 - 0.8820986748 - 0.8830870986 - 0.8840739131 - 0.8850582242 - 0.8860396743 - 0.8870177269 - 0.8879904747 - 0.8889573812 - 0.8899183869 - 0.8908733130 - 0.8918204904 - 0.8927587271 - 0.8936867714 - 0.8946046233 - 0.8955129981 - 0.8964123726 - 0.8973016739 - 0.8981794715 - 0.8990461230 - 0.8999027014 - 0.9007506967 - 0.9015905857 - 0.9024220705 - 0.9032449722 - 0.9040595293 - 0.9048662186 - 0.9056664705 - 0.9064612985 - 0.9072514176 - 0.9080368280 - 0.9088181257 - 0.9095960855 - 0.9103735089 - 0.9111509919 - 0.9119281173 - 0.9127063155 - 0.9134865999 - 0.9142683744 - 0.9150518179 - 0.9158385992 - 0.9166294932 - 0.9174247980 - 0.9182245731 - 0.9190281034 - 0.9198362827 - 0.9206498861 - 0.9214689732 - 0.9222937822 - 0.9231243134 - 0.9239605069 - 0.9248024225 - 0.9256507754 - 0.9265056252 - 0.9273657203 - 0.9282308221 - 0.9291006923 - 0.9299767017 - 0.9308583736 - 0.9317452908 - 0.9326366186 - 0.9335306883 - 0.9344261289 - 0.9353219867 - 0.9362199903 - 0.9371207952 - 0.9380227923 - 0.9389240742 - 0.9398236871 - 0.9407200813 - 0.9416117072 - 0.9424964190 - 0.9433727860 - 0.9442400932 - 0.9450957775 - 0.9459366202 - 0.9467617273 - 0.9475710988 - 0.9483641982 - 0.9491404891 - 0.9498987198 - 0.9506378770 - 0.9513601065 - 0.9520673156 - 0.9527601004 - 0.9534382224 - 0.9541031122 - 0.9547548890 - 0.9553934932 - 0.9560201764 - 0.9566345811 - 0.9572381973 - 0.9578328729 - 0.9584187269 - 0.9589971900 - 0.9595693946 - 0.9601373076 - 0.9607033134 - 0.9612680078 - 0.9618327022 - 0.9623975158 - 0.9629616737 - 0.9635255933 - 0.9640890956 - 0.9646533728 - 0.9652183056 - 0.9657815099 - 0.9663417935 - 0.9668993950 - 0.9674541950 - 0.9680057764 - 0.9685540199 - 0.9690970778 - 0.9696341753 - 0.9701663256 - 0.9706926942 - 0.9712131023 - 0.9717286825 - 0.9722387791 - 0.9727417827 - 0.9732379913 - 0.9737278819 - 0.9742118120 - 0.9746910930 - 0.9751664996 - 0.9756376147 - 0.9761040211 - 0.9765653014 - 0.9770221114 - 0.9774755836 - 0.9779269099 - 0.9783759117 - 0.9788222909 - 0.9792655706 - 0.9797067046 - 0.9801476002 - 0.9805889726 - 0.9810296893 - 0.9814689755 - 0.9819055796 - 0.9823380709 - 0.9827659726 - 0.9831880927 - 0.9836019874 - 0.9840061069 - 0.9844006896 - 0.9847850204 - 0.9851579070 - 0.9855204225 - 0.9858720899 - 0.9862110019 - 0.9865372181 - 0.9868519902 - 0.9871559739 - 0.9874501824 - 0.9877349734 - 0.9880086184 - 0.9882714152 - 0.9885252118 - 0.9887697101 - 0.9890058041 - 0.9892352223 - 0.9894577861 - 0.9896727204 - 0.9898800850 - 0.9900823832 - 0.9902830720 - 0.9904839993 - 0.9906855226 - 0.9908869267 - 0.9910880923 - 0.9912897944 - 0.9914922714 - 0.9916949272 - 0.9918974042 - 0.9920992851 - 0.9923006296 - 0.9925016761 - 0.9927030206 - 0.9929041266 - 0.9931046963 - 0.9933041930 - 0.9935020804 - 0.9936985970 - 0.9938936234 - 0.9940873981 - 0.9942802787 - 0.9944714904 - 0.9946609735 - 0.9948499203 - 0.9950379729 - 0.9952244163 - 0.9954094291 - 0.9955927134 - 0.9957733750 - 0.9959521890 - 0.9961301088 - 0.9963076711 - 0.9964857101 - 0.9966629744 - 0.9968385100 - 0.9970129728 - 0.9971876144 - 0.9973618984 - 0.9975370169 - 0.9977136254 - 0.9978904128 - 0.9980655909 - 0.9982392192 - 0.9984129071 - 0.9985876083 - 0.9987636805 - 0.9989405274 - 0.9991179705 - 0.9992954731 - 0.9994722009 - 0.9996479750 - 0.9998239875 - 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 1 +{ + 0.0000000000 + 0.0015970930 + 0.0032108270 + 0.0048384252 + 0.0064732390 + 0.0081081763 + 0.0097375941 + 0.0113548804 + 0.0129528800 + 0.0145261399 + 0.0160703696 + 0.0175831709 + 0.0190630294 + 0.0205102507 + 0.0219259299 + 0.0233127493 + 0.0246715993 + 0.0260010492 + 0.0272994898 + 0.0285670608 + 0.0298028998 + 0.0310098194 + 0.0321902186 + 0.0333435684 + 0.0344703794 + 0.0355738886 + 0.0366554409 + 0.0377166718 + 0.0387589782 + 0.0397843607 + 0.0407951698 + 0.0417945310 + 0.0427865982 + 0.0437750705 + 0.0447619408 + 0.0457485691 + 0.0467366986 + 0.0477271006 + 0.0487186685 + 0.0497123487 + 0.0507081710 + 0.0517065898 + 0.0527076609 + 0.0537111685 + 0.0547169782 + 0.0557249896 + 0.0567362607 + 0.0577516407 + 0.0587707087 + 0.0597942285 + 0.0608223192 + 0.0618541911 + 0.0628895611 + 0.0639279112 + 0.0649689585 + 0.0660121813 + 0.0670581907 + 0.0681073666 + 0.0691590682 + 0.0702132285 + 0.0712696612 + 0.0723281205 + 0.0733884126 + 0.0744497478 + 0.0755119175 + 0.0765746608 + 0.0776376799 + 0.0787018836 + 0.0797673315 + 0.0808331519 + 0.0818990618 + 0.0829651132 + 0.0840317011 + 0.0850984082 + 0.0861652717 + 0.0872325823 + 0.0882994309 + 0.0893664882 + 0.0904344097 + 0.0915023014 + 0.0925698727 + 0.0936371982 + 0.0947050005 + 0.0957735702 + 0.0968423486 + 0.0979111120 + 0.0989789367 + 0.1000462994 + 0.1011143997 + 0.1021834984 + 0.1032544971 + 0.1043291986 + 0.1054088995 + 0.1064950973 + 0.1075880006 + 0.1086886004 + 0.1097972989 + 0.1109144986 + 0.1120416969 + 0.1131796017 + 0.1143285036 + 0.1154886037 + 0.1166604012 + 0.1178442016 + 0.1190397963 + 0.1202476025 + 0.1214689016 + 0.1227027997 + 0.1239494979 + 0.1252094954 + 0.1264819056 + 0.1277655959 + 0.1290591955 + 0.1303616017 + 0.1316712052 + 0.1329866052 + 0.1343073994 + 0.1356315017 + 0.1369580030 + 0.1382858008 + 0.1396144927 + 0.1409430057 + 0.1422718018 + 0.1436003000 + 0.1449290067 + 0.1462575048 + 0.1475863010 + 0.1489147991 + 0.1502435058 + 0.1515720040 + 0.1529008001 + 0.1542291939 + 0.1555580050 + 0.1568865031 + 0.1582152992 + 0.1595436931 + 0.1608725041 + 0.1622010022 + 0.1635297984 + 0.1648582071 + 0.1661870033 + 0.1675153971 + 0.1688434035 + 0.1701688021 + 0.1714915931 + 0.1728098989 + 0.1741224974 + 0.1754271984 + 0.1767227948 + 0.1780070066 + 0.1792788059 + 0.1805343926 + 0.1817727983 + 0.1829933971 + 0.1841960996 + 0.1853809059 + 0.1865482926 + 0.1876983047 + 0.1888300031 + 0.1899435073 + 0.1910407990 + 0.1921222061 + 0.1931896061 + 0.1942428052 + 0.1952829063 + 0.1963098943 + 0.1973251998 + 0.1983291954 + 0.1993234009 + 0.2003085017 + 0.2012864053 + 0.2022587955 + 0.2032283992 + 0.2041957974 + 0.2051616013 + 0.2061273009 + 0.2070931047 + 0.2080595940 + 0.2090270072 + 0.2099955976 + 0.2109649032 + 0.2119348943 + 0.2129058987 + 0.2138797939 + 0.2148582041 + 0.2158416957 + 0.2168301046 + 0.2178225964 + 0.2188192010 + 0.2198204994 + 0.2208279073 + 0.2218416035 + 0.2228616029 + 0.2238875031 + 0.2249186039 + 0.2259557992 + 0.2269994020 + 0.2280489057 + 0.2291045040 + 0.2301660031 + 0.2312328964 + 0.2323053926 + 0.2333831042 + 0.2344655991 + 0.2355525047 + 0.2366431057 + 0.2377368063 + 0.2388329953 + 0.2399305999 + 0.2410296053 + 0.2421302944 + 0.2432314008 + 0.2443311065 + 0.2454299033 + 0.2465299964 + 0.2476332933 + 0.2487404048 + 0.2498511970 + 0.2509643137 + 0.2520790994 + 0.2531974018 + 0.2543210089 + 0.2554507852 + 0.2565864921 + 0.2577267885 + 0.2588709891 + 0.2600202858 + 0.2611759901 + 0.2623381019 + 0.2635067105 + 0.2646813989 + 0.2658607960 + 0.2670449913 + 0.2682338953 + 0.2694280148 + 0.2706277072 + 0.2718321085 + 0.2730394900 + 0.2742492855 + 0.2754603922 + 0.2766728103 + 0.2778874040 + 0.2791036963 + 0.2803192139 + 0.2815321982 + 0.2827436030 + 0.2839539051 + 0.2851620913 + 0.2863675058 + 0.2875689864 + 0.2887648046 + 0.2899549007 + 0.2911390960 + 0.2923170924 + 0.2934887111 + 0.2946529090 + 0.2958090901 + 0.2969568968 + 0.2980965972 + 0.2992289066 + 0.3003537953 + 0.3014717996 + 0.3025816977 + 0.3036831915 + 0.3047770858 + 0.3058654070 + 0.3069495857 + 0.3080292046 + 0.3091028929 + 0.3101705909 + 0.3112339079 + 0.3122943938 + 0.3133535087 + 0.3144116104 + 0.3154678941 + 0.3165228963 + 0.3175787926 + 0.3186368942 + 0.3196958899 + 0.3207564056 + 0.3218187094 + 0.3228828907 + 0.3239508867 + 0.3250220120 + 0.3260951936 + 0.3271706104 + 0.3282474875 + 0.3293263912 + 0.3304072917 + 0.3314906955 + 0.3325760067 + 0.3336637020 + 0.3347538114 + 0.3358460069 + 0.3369395137 + 0.3380326927 + 0.3391278088 + 0.3402253985 + 0.3413242996 + 0.3424232900 + 0.3435212970 + 0.3446190953 + 0.3457185030 + 0.3468185961 + 0.3479177952 + 0.3490158916 + 0.3501127958 + 0.3512093127 + 0.3523041010 + 0.3533973098 + 0.3544895947 + 0.3555799127 + 0.3566688895 + 0.3577584922 + 0.3588475883 + 0.3599354923 + 0.3610216081 + 0.3621057868 + 0.3631885946 + 0.3642696142 + 0.3653489947 + 0.3664267957 + 0.3675036132 + 0.3685795963 + 0.3696550131 + 0.3707289100 + 0.3718005121 + 0.3728711009 + 0.3739413917 + 0.3750106096 + 0.3760792017 + 0.3771468997 + 0.3782134950 + 0.3792808056 + 0.3803485930 + 0.3814165890 + 0.3824853003 + 0.3835538924 + 0.3846215010 + 0.3856872022 + 0.3867521882 + 0.3878181875 + 0.3888849020 + 0.3899520934 + 0.3910199106 + 0.3920885026 + 0.3931584954 + 0.3942297995 + 0.3953022957 + 0.3963758051 + 0.3974499106 + 0.3985247016 + 0.3996005952 + 0.4006777108 + 0.4017564952 + 0.4028376937 + 0.4039207101 + 0.4050048888 + 0.4060907960 + 0.4071780145 + 0.4082660973 + 0.4093562067 + 0.4104484916 + 0.4115422070 + 0.4126375020 + 0.4137335122 + 0.4148307145 + 0.4159297049 + 0.4170301855 + 0.4181306064 + 0.4192298055 + 0.4203279912 + 0.4214271009 + 0.4225274920 + 0.4236279130 + 0.4247274995 + 0.4258260131 + 0.4269242883 + 0.4280230105 + 0.4291217029 + 0.4302200079 + 0.4313178957 + 0.4324154854 + 0.4335126877 + 0.4346086085 + 0.4357028902 + 0.4367968142 + 0.4378911853 + 0.4389862120 + 0.4400813878 + 0.4411762953 + 0.4422703981 + 0.4433647990 + 0.4444605112 + 0.4455575943 + 0.4466555119 + 0.4477527142 + 0.4488491118 + 0.4499464929 + 0.4510458112 + 0.4521470070 + 0.4532484114 + 0.4543491900 + 0.4554497004 + 0.4565511048 + 0.4576537907 + 0.4587570131 + 0.4598602057 + 0.4609631896 + 0.4620659947 + 0.4631690979 + 0.4642724097 + 0.4653758109 + 0.4664795995 + 0.4675835967 + 0.4686864913 + 0.4697881043 + 0.4708893895 + 0.4719910920 + 0.4730935991 + 0.4741966128 + 0.4752987921 + 0.4763998985 + 0.4775001109 + 0.4785997868 + 0.4797008038 + 0.4808028042 + 0.4819043875 + 0.4830043018 + 0.4841024876 + 0.4852007926 + 0.4863008857 + 0.4874025881 + 0.4885045886 + 0.4896053970 + 0.4907051027 + 0.4918052852 + 0.4929065108 + 0.4940088987 + 0.4951120019 + 0.4962143004 + 0.4973159134 + 0.4984172881 + 0.4995189011 + 0.5006219745 + 0.5017260909 + 0.5028306842 + 0.5039374232 + 0.5050470233 + 0.5061581731 + 0.5072711706 + 0.5083870292 + 0.5095049739 + 0.5106257796 + 0.5117477775 + 0.5128695965 + 0.5139923096 + 0.5151175261 + 0.5162450075 + 0.5173735023 + 0.5185019970 + 0.5196316242 + 0.5207641125 + 0.5218989849 + 0.5230355859 + 0.5241733193 + 0.5253111720 + 0.5264499187 + 0.5275893807 + 0.5287293792 + 0.5298690200 + 0.5310084820 + 0.5321471095 + 0.5332846045 + 0.5344207883 + 0.5355551839 + 0.5366871953 + 0.5378178954 + 0.5389484763 + 0.5400791168 + 0.5412082076 + 0.5423350930 + 0.5434600115 + 0.5445839763 + 0.5457080007 + 0.5468308926 + 0.5479511023 + 0.5490679741 + 0.5501825213 + 0.5512952805 + 0.5524055958 + 0.5535135269 + 0.5546203852 + 0.5557265282 + 0.5568308234 + 0.5579332709 + 0.5590345860 + 0.5601357222 + 0.5612370968 + 0.5623382926 + 0.5634381175 + 0.5645357966 + 0.5656321049 + 0.5667272210 + 0.5678204894 + 0.5689107776 + 0.5699964166 + 0.5710760951 + 0.5721501112 + 0.5732188821 + 0.5742821097 + 0.5753387213 + 0.5763881803 + 0.5774298906 + 0.5784643292 + 0.5794925094 + 0.5805140138 + 0.5815274119 + 0.5825325847 + 0.5835304260 + 0.5845211148 + 0.5855050087 + 0.5864828229 + 0.5874555707 + 0.5884236097 + 0.5893865824 + 0.5903437138 + 0.5912957191 + 0.5922436714 + 0.5931885242 + 0.5941303968 + 0.5950701833 + 0.5960083008 + 0.5969455242 + 0.5978826284 + 0.5988199711 + 0.5997573733 + 0.6006944180 + 0.6016309261 + 0.6025679111 + 0.6035056710 + 0.6044442058 + 0.6053841710 + 0.6063274741 + 0.6072760224 + 0.6082304716 + 0.6091914177 + 0.6101577878 + 0.6111288071 + 0.6121054292 + 0.6130893230 + 0.6140809059 + 0.6150801778 + 0.6160869002 + 0.6171011925 + 0.6181237102 + 0.6191561222 + 0.6201986074 + 0.6212494969 + 0.6223081946 + 0.6233735085 + 0.6244443059 + 0.6255208850 + 0.6266024113 + 0.6276888251 + 0.6287800074 + 0.6298750043 + 0.6309717894 + 0.6320694089 + 0.6331679225 + 0.6342682838 + 0.6353698969 + 0.6364706755 + 0.6375694871 + 0.6386672854 + 0.6397666931 + 0.6408684254 + 0.6419709921 + 0.6430730224 + 0.6441742182 + 0.6452761889 + 0.6463810205 + 0.6474881172 + 0.6485970020 + 0.6497082114 + 0.6508216262 + 0.6519368887 + 0.6530544162 + 0.6541745067 + 0.6552979946 + 0.6564245224 + 0.6575521231 + 0.6586806178 + 0.6598100066 + 0.6609410048 + 0.6620740294 + 0.6632086039 + 0.6643440127 + 0.6654803157 + 0.6666182280 + 0.6677569747 + 0.6688960791 + 0.6700353026 + 0.6711744070 + 0.6723119020 + 0.6734473109 + 0.6745814085 + 0.6757146120 + 0.6768456101 + 0.6779726744 + 0.6790955067 + 0.6802144051 + 0.6813290119 + 0.6824392080 + 0.6835449934 + 0.6846470833 + 0.6857451797 + 0.6868383884 + 0.6879264116 + 0.6890088916 + 0.6900870204 + 0.6911612749 + 0.6922308803 + 0.6932950020 + 0.6943535805 + 0.6954085231 + 0.6964603066 + 0.6975083947 + 0.6985530257 + 0.6995959282 + 0.7006379962 + 0.7016788721 + 0.7027179003 + 0.7037551999 + 0.7047919035 + 0.7058281898 + 0.7068638206 + 0.7078987956 + 0.7089341879 + 0.7099704742 + 0.7110071182 + 0.7120435834 + 0.7130793929 + 0.7141141295 + 0.7151473761 + 0.7161788940 + 0.7172086835 + 0.7182366848 + 0.7192623019 + 0.7202852964 + 0.7213062048 + 0.7223259211 + 0.7233440876 + 0.7243599892 + 0.7253736258 + 0.7263845205 + 0.7273933291 + 0.7283999920 + 0.7294043899 + 0.7304064035 + 0.7314063907 + 0.7324048877 + 0.7334017158 + 0.7343968749 + 0.7353904247 + 0.7363826036 + 0.7373743057 + 0.7383652925 + 0.7393556833 + 0.7403460741 + 0.7413364053 + 0.7423262000 + 0.7433158755 + 0.7443063855 + 0.7452973723 + 0.7462878227 + 0.7472770810 + 0.7482659221 + 0.7492548823 + 0.7502439022 + 0.7512320876 + 0.7522203922 + 0.7532092929 + 0.7541986108 + 0.7551875114 + 0.7561764121 + 0.7571650743 + 0.7581533194 + 0.7591395974 + 0.7601243854 + 0.7611085773 + 0.7620921731 + 0.7630745769 + 0.7640557885 + 0.7650362253 + 0.7660158277 + 0.7669950724 + 0.7679738998 + 0.7689527273 + 0.7699314952 + 0.7709106803 + 0.7718899846 + 0.7728688121 + 0.7738466263 + 0.7748246789 + 0.7758036852 + 0.7767832279 + 0.7777634859 + 0.7787454724 + 0.7797296047 + 0.7807155252 + 0.7817035913 + 0.7826942801 + 0.7836887836 + 0.7846868038 + 0.7856897116 + 0.7866985202 + 0.7877131104 + 0.7887325287 + 0.7897546887 + 0.7907801867 + 0.7918102145 + 0.7928441763 + 0.7938814163 + 0.7949218750 + 0.7959659100 + 0.7970144153 + 0.7980663180 + 0.7991213799 + 0.8001785874 + 0.8012385964 + 0.8023005128 + 0.8033639193 + 0.8044286966 + 0.8054934740 + 0.8065580130 + 0.8076239824 + 0.8086903095 + 0.8097563982 + 0.8108221292 + 0.8118875027 + 0.8129522204 + 0.8140147924 + 0.8150777221 + 0.8161420822 + 0.8172072172 + 0.8182731867 + 0.8193401098 + 0.8204079866 + 0.8214777708 + 0.8225491047 + 0.8236216903 + 0.8246955872 + 0.8257701993 + 0.8268460035 + 0.8279229999 + 0.8290016055 + 0.8300824761 + 0.8311660886 + 0.8322516084 + 0.8333384991 + 0.8344269991 + 0.8355159163 + 0.8366057277 + 0.8376973867 + 0.8387901783 + 0.8398838043 + 0.8409780264 + 0.8420727849 + 0.8431686759 + 0.8442658782 + 0.8453642130 + 0.8464624882 + 0.8475595713 + 0.8486564159 + 0.8497542143 + 0.8508523703 + 0.8519496918 + 0.8530451059 + 0.8541380763 + 0.8552293777 + 0.8563184738 + 0.8574041724 + 0.8584862947 + 0.8595638275 + 0.8606367111 + 0.8617051244 + 0.8627687097 + 0.8638274074 + 0.8648813963 + 0.8659303784 + 0.8669738770 + 0.8680111766 + 0.8690428734 + 0.8700705171 + 0.8710942268 + 0.8721132874 + 0.8731275201 + 0.8741363287 + 0.8751401901 + 0.8761405945 + 0.8771389127 + 0.8781350851 + 0.8791291118 + 0.8801203966 + 0.8811098933 + 0.8820986748 + 0.8830870986 + 0.8840739131 + 0.8850582242 + 0.8860396743 + 0.8870177269 + 0.8879904747 + 0.8889573812 + 0.8899183869 + 0.8908733130 + 0.8918204904 + 0.8927587271 + 0.8936867714 + 0.8946046233 + 0.8955129981 + 0.8964123726 + 0.8973016739 + 0.8981794715 + 0.8990461230 + 0.8999027014 + 0.9007506967 + 0.9015905857 + 0.9024220705 + 0.9032449722 + 0.9040595293 + 0.9048662186 + 0.9056664705 + 0.9064612985 + 0.9072514176 + 0.9080368280 + 0.9088181257 + 0.9095960855 + 0.9103735089 + 0.9111509919 + 0.9119281173 + 0.9127063155 + 0.9134865999 + 0.9142683744 + 0.9150518179 + 0.9158385992 + 0.9166294932 + 0.9174247980 + 0.9182245731 + 0.9190281034 + 0.9198362827 + 0.9206498861 + 0.9214689732 + 0.9222937822 + 0.9231243134 + 0.9239605069 + 0.9248024225 + 0.9256507754 + 0.9265056252 + 0.9273657203 + 0.9282308221 + 0.9291006923 + 0.9299767017 + 0.9308583736 + 0.9317452908 + 0.9326366186 + 0.9335306883 + 0.9344261289 + 0.9353219867 + 0.9362199903 + 0.9371207952 + 0.9380227923 + 0.9389240742 + 0.9398236871 + 0.9407200813 + 0.9416117072 + 0.9424964190 + 0.9433727860 + 0.9442400932 + 0.9450957775 + 0.9459366202 + 0.9467617273 + 0.9475710988 + 0.9483641982 + 0.9491404891 + 0.9498987198 + 0.9506378770 + 0.9513601065 + 0.9520673156 + 0.9527601004 + 0.9534382224 + 0.9541031122 + 0.9547548890 + 0.9553934932 + 0.9560201764 + 0.9566345811 + 0.9572381973 + 0.9578328729 + 0.9584187269 + 0.9589971900 + 0.9595693946 + 0.9601373076 + 0.9607033134 + 0.9612680078 + 0.9618327022 + 0.9623975158 + 0.9629616737 + 0.9635255933 + 0.9640890956 + 0.9646533728 + 0.9652183056 + 0.9657815099 + 0.9663417935 + 0.9668993950 + 0.9674541950 + 0.9680057764 + 0.9685540199 + 0.9690970778 + 0.9696341753 + 0.9701663256 + 0.9706926942 + 0.9712131023 + 0.9717286825 + 0.9722387791 + 0.9727417827 + 0.9732379913 + 0.9737278819 + 0.9742118120 + 0.9746910930 + 0.9751664996 + 0.9756376147 + 0.9761040211 + 0.9765653014 + 0.9770221114 + 0.9774755836 + 0.9779269099 + 0.9783759117 + 0.9788222909 + 0.9792655706 + 0.9797067046 + 0.9801476002 + 0.9805889726 + 0.9810296893 + 0.9814689755 + 0.9819055796 + 0.9823380709 + 0.9827659726 + 0.9831880927 + 0.9836019874 + 0.9840061069 + 0.9844006896 + 0.9847850204 + 0.9851579070 + 0.9855204225 + 0.9858720899 + 0.9862110019 + 0.9865372181 + 0.9868519902 + 0.9871559739 + 0.9874501824 + 0.9877349734 + 0.9880086184 + 0.9882714152 + 0.9885252118 + 0.9887697101 + 0.9890058041 + 0.9892352223 + 0.9894577861 + 0.9896727204 + 0.9898800850 + 0.9900823832 + 0.9902830720 + 0.9904839993 + 0.9906855226 + 0.9908869267 + 0.9910880923 + 0.9912897944 + 0.9914922714 + 0.9916949272 + 0.9918974042 + 0.9920992851 + 0.9923006296 + 0.9925016761 + 0.9927030206 + 0.9929041266 + 0.9931046963 + 0.9933041930 + 0.9935020804 + 0.9936985970 + 0.9938936234 + 0.9940873981 + 0.9942802787 + 0.9944714904 + 0.9946609735 + 0.9948499203 + 0.9950379729 + 0.9952244163 + 0.9954094291 + 0.9955927134 + 0.9957733750 + 0.9959521890 + 0.9961301088 + 0.9963076711 + 0.9964857101 + 0.9966629744 + 0.9968385100 + 0.9970129728 + 0.9971876144 + 0.9973618984 + 0.9975370169 + 0.9977136254 + 0.9978904128 + 0.9980655909 + 0.9982392192 + 0.9984129071 + 0.9985876083 + 0.9987636805 + 0.9989405274 + 0.9991179705 + 0.9992954731 + 0.9994722009 + 0.9996479750 + 0.9998239875 + 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kodachrome_200.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kodachrome_200.spi1d index 05790a82..19107feb 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kodachrome_200.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kodachrome_200.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0001730574 0.0002039562 0.0002358485 - 0.0003522631 0.0004149732 0.0004803593 - 0.0005379661 0.0006318853 0.0007290206 - 0.0007310679 0.0008542882 0.0009827145 - 0.0009340477 0.0010819150 0.0012466050 - 0.0011457250 0.0013172789 0.0015175150 - 0.0013664630 0.0015590760 0.0017966420 - 0.0015953979 0.0018074680 0.0020813060 - 0.0018292820 0.0020661030 0.0023712160 - 0.0020738509 0.0023327931 0.0026701461 - 0.0023281691 0.0026087810 0.0029817061 - 0.0025920530 0.0028905689 0.0033033220 - 0.0028679480 0.0031836890 0.0036326619 - 0.0031529539 0.0034913400 0.0039701960 - 0.0034448660 0.0038095911 0.0043217931 - 0.0037472011 0.0041361330 0.0046877442 - 0.0040617771 0.0044678152 0.0050627491 - 0.0043859989 0.0048073409 0.0054407581 - 0.0047217272 0.0051559508 0.0058272062 - 0.0050699208 0.0055170869 0.0062212390 - 0.0054345722 0.0058912761 0.0066273510 - 0.0058116391 0.0062767328 0.0070466590 - 0.0062011848 0.0066723628 0.0074762749 - 0.0066031339 0.0070798998 0.0079114493 - 0.0070177661 0.0075020911 0.0083535369 - 0.0074442741 0.0079338206 0.0088046975 - 0.0078795254 0.0083771208 0.0092653846 - 0.0083278697 0.0088326121 0.0097344546 - 0.0087915091 0.0092987726 0.0102126198 - 0.0092691761 0.0097725317 0.0107003897 - 0.0097623114 0.0102546504 0.0111954799 - 0.0102694500 0.0107495701 0.0116990302 - 0.0107906098 0.0112578301 0.0122114504 - 0.0113268001 0.0117790196 0.0127311097 - 0.0118786199 0.0123126702 0.0132614998 - 0.0124431401 0.0128612900 0.0138018904 - 0.0130219003 0.0134234196 0.0143521996 - 0.0136152301 0.0139996996 0.0149130402 - 0.0142232198 0.0145876799 0.0154852197 - 0.0148449196 0.0151869999 0.0160684697 - 0.0154783595 0.0158025604 0.0166603103 - 0.0161256995 0.0164336897 0.0172653906 - 0.0167863201 0.0170805994 0.0178804602 - 0.0174631495 0.0177476797 0.0185050704 - 0.0181544498 0.0184269901 0.0191421397 - 0.0188641995 0.0191224609 0.0197905600 - 0.0195880793 0.0198333003 0.0204491094 - 0.0203275606 0.0205603093 0.0211143699 - 0.0210811403 0.0212992709 0.0217879806 - 0.0218534302 0.0220546499 0.0224671308 - 0.0226451308 0.0228254497 0.0231546499 - 0.0234524105 0.0236116406 0.0238485206 - 0.0242763907 0.0244143009 0.0245480109 - 0.0251218900 0.0252302606 0.0252581500 - 0.0259851404 0.0260641407 0.0259789899 - 0.0268620700 0.0269184001 0.0267140791 - 0.0277548693 0.0277873296 0.0274658203 - 0.0286648702 0.0286731608 0.0282310992 - 0.0295908507 0.0295777693 0.0290101692 - 0.0305334404 0.0304972902 0.0298030600 - 0.0314947292 0.0314330608 0.0306055099 - 0.0324744582 0.0323868990 0.0314168893 - 0.0334764086 0.0333601981 0.0322372504 - 0.0344995111 0.0343528390 0.0330651999 - 0.0355402492 0.0353610590 0.0339045897 - 0.0365981199 0.0363844894 0.0347526409 - 0.0376747511 0.0374244489 0.0356094018 - 0.0387722701 0.0384774804 0.0364748016 - 0.0398925804 0.0395460315 0.0373485014 - 0.0410357118 0.0406353809 0.0382347293 - 0.0421971716 0.0417462699 0.0391340293 - 0.0433758609 0.0428716205 0.0400448591 - 0.0445724390 0.0440130197 0.0409630090 - 0.0457868986 0.0451716594 0.0418913700 - 0.0470213406 0.0463434607 0.0428329296 - 0.0482786596 0.0475279316 0.0437831692 - 0.0495616607 0.0487296283 0.0447406098 - 0.0508708991 0.0499518812 0.0457080491 - 0.0522039905 0.0511935391 0.0466858111 - 0.0535621308 0.0524507686 0.0476739705 - 0.0549456812 0.0537222885 0.0486743301 - 0.0563542917 0.0550098717 0.0496832319 - 0.0577872396 0.0563169084 0.0507013202 - 0.0592435412 0.0576429814 0.0517350510 - 0.0607235916 0.0589821301 0.0527782217 - 0.0622299090 0.0603348911 0.0538305715 - 0.0637641624 0.0617051981 0.0548948608 - 0.0653300881 0.0630972311 0.0559714995 - 0.0669195279 0.0645076036 0.0570586994 - 0.0685284436 0.0659297109 0.0581539795 - 0.0701561198 0.0673606992 0.0592579208 - 0.0718030334 0.0688040778 0.0603738800 - 0.0734695569 0.0702652484 0.0615030490 - 0.0751590133 0.0717460513 0.0626446307 - 0.0768723935 0.0732394904 0.0637940615 - 0.0786073431 0.0747426003 0.0649530664 - 0.0803640932 0.0762560293 0.0661245733 - 0.0821429417 0.0777818337 0.0673098490 - 0.0839471966 0.0793204084 0.0685084313 - 0.0857824534 0.0808681324 0.0697233528 - 0.0876402482 0.0824251175 0.0709487796 - 0.0895186588 0.0839939788 0.0721847713 - 0.0914160535 0.0855768770 0.0734331012 - 0.0933414400 0.0871768892 0.0746951923 - 0.0952875987 0.0887894779 0.0759687126 - 0.0972552076 0.0904145017 0.0772481188 - 0.0992452875 0.0920512825 0.0785354823 - 0.1012666970 0.0936988667 0.0798321664 - 0.1033084020 0.0953587517 0.0811416209 - 0.1053688005 0.0970306993 0.0824629217 - 0.1074498966 0.0987144411 0.0837924406 - 0.1095537990 0.1004106030 0.0851297081 - 0.1116733998 0.1021208018 0.0864788592 - 0.1138081029 0.1038433984 0.0878428370 - 0.1159645021 0.1055795997 0.0892181471 - 0.1181332022 0.1073433980 0.0906047598 - 0.1203133017 0.1091196015 0.0920063481 - 0.1225123033 0.1109066978 0.0934172571 - 0.1247252971 0.1127050966 0.0948378369 - 0.1269482970 0.1145213023 0.0962786898 - 0.1291864067 0.1163515970 0.0977344289 - 0.1314398050 0.1182034016 0.0992026329 - 0.1337047964 0.1200674996 0.1006904021 - 0.1359920055 0.1219405010 0.1021960005 - 0.1382973045 0.1238282993 0.1037138999 - 0.1406147927 0.1257331967 0.1052495018 - 0.1429530978 0.1276489943 0.1068022996 - 0.1453036964 0.1295824945 0.1083668992 - 0.1476657987 0.1315266043 0.1099525020 - 0.1500508040 0.1334788054 0.1115524024 - 0.1524461955 0.1354573965 0.1131626964 - 0.1548593938 0.1374479979 0.1147975996 - 0.1572892964 0.1394518018 0.1164444983 - 0.1597270072 0.1414680034 0.1181050986 - 0.1621783972 0.1434909999 0.1197827980 - 0.1646371931 0.1455283016 0.1214680970 - 0.1671067029 0.1475757957 0.1231684014 - 0.1695881933 0.1496292949 0.1248828992 - 0.1720747054 0.1517004967 0.1266061962 - 0.1745768040 0.1537802070 0.1283615977 - 0.1770841032 0.1558783948 0.1301272064 - 0.1796021014 0.1579937041 0.1319141984 - 0.1821261942 0.1601195037 0.1337158978 - 0.1846542060 0.1622675061 0.1355265975 - 0.1871972978 0.1644203067 0.1373614073 - 0.1897435039 0.1665845066 0.1392045021 - 0.1922962070 0.1687586010 0.1410738975 - 0.1948485970 0.1709450036 0.1429603994 - 0.1974000037 0.1731521040 0.1448680013 - 0.1999529004 0.1753689051 0.1467960030 - 0.2025073022 0.1776115000 0.1487334073 - 0.2050623000 0.1798598021 0.1506955028 - 0.2076175958 0.1821341962 0.1526625007 - 0.2101784945 0.1844155937 0.1546497941 - 0.2127404958 0.1867152005 0.1566448063 - 0.2153038979 0.1890224069 0.1586592048 - 0.2178677022 0.1913391948 0.1606916934 - 0.2204349041 0.1936641932 0.1627465039 - 0.2230034024 0.1959953010 0.1648250967 - 0.2255780995 0.1983426064 0.1669203043 - 0.2281538993 0.2006970942 0.1690406948 - 0.2307337970 0.2030775994 0.1711710989 - 0.2333167940 0.2054647952 0.1733302027 - 0.2359074056 0.2078783959 0.1754958034 - 0.2384988964 0.2102957964 0.1776867956 - 0.2410926074 0.2127331942 0.1798807979 - 0.2436847985 0.2151719928 0.1820942014 - 0.2462743968 0.2176246047 0.1843084991 - 0.2488645017 0.2200776041 0.1865465939 - 0.2514558136 0.2225456983 0.1887851954 - 0.2540476024 0.2250140011 0.1910569072 - 0.2566407025 0.2275063992 0.1933290064 - 0.2592354119 0.2299994975 0.1956388056 - 0.2618348897 0.2325156033 0.1979486048 - 0.2644349933 0.2350329012 0.2002822012 - 0.2670376897 0.2375658005 0.2026163042 - 0.2696410120 0.2401002944 0.2049711943 - 0.2722525895 0.2426490933 0.2073276043 - 0.2748641074 0.2452007979 0.2097111940 - 0.2774798870 0.2477629036 0.2120991051 - 0.2800959945 0.2503288984 0.2145109028 - 0.2827027142 0.2529033124 0.2169295996 - 0.2853069007 0.2554830909 0.2193607986 - 0.2879045010 0.2580693960 0.2217984051 - 0.2904979885 0.2606629133 0.2242448032 - 0.2930887938 0.2632611990 0.2266992033 - 0.2956756949 0.2658706009 0.2291606069 - 0.2982619107 0.2684814036 0.2316346020 - 0.3008450866 0.2711029053 0.2341126055 - 0.3034282923 0.2737244070 0.2366093993 - 0.3060013056 0.2763522863 0.2391061932 - 0.3085739017 0.2789807022 0.2416251004 - 0.3111369014 0.2816146910 0.2441447973 - 0.3136956990 0.2842512131 0.2466782033 - 0.3162499070 0.2868914008 0.2492164969 - 0.3187974095 0.2895368934 0.2517606020 - 0.3213433921 0.2921831906 0.2543109953 - 0.3238751888 0.2948344946 0.2568640113 - 0.3264068961 0.2974858880 0.2594281137 - 0.3289282918 0.3001478016 0.2619921863 - 0.3314462900 0.3028118908 0.2645705938 - 0.3339582086 0.3054809868 0.2671507895 - 0.3364607096 0.3081552088 0.2697336972 - 0.3389630914 0.3108294010 0.2723186910 - 0.3414388001 0.3135038018 0.2749026120 - 0.3439145088 0.3161781132 0.2774826884 - 0.3463768065 0.3188591003 0.2800627947 - 0.3488295972 0.3215422034 0.2826369107 - 0.3512794971 0.3242281079 0.2852098048 - 0.3537116945 0.3269194067 0.2877875865 - 0.3561440110 0.3296107054 0.2903720140 - 0.3585642874 0.3322994113 0.2929564118 - 0.3609788120 0.3349879086 0.2955451012 - 0.3633924127 0.3376730978 0.2981339097 - 0.3658021092 0.3403542042 0.3007180095 - 0.3682116866 0.3430353105 0.3032977879 - 0.3706200123 0.3457179964 0.3058773875 - 0.3730275035 0.3484008908 0.3084514141 - 0.3754344881 0.3510805964 0.3110255003 - 0.3778376877 0.3537563086 0.3135905862 - 0.3802408874 0.3564321101 0.3161478043 - 0.3826422989 0.3591000140 0.3187049925 - 0.3850421906 0.3617669940 0.3212369084 - 0.3874421120 0.3644312024 0.3237684965 - 0.3898290098 0.3670895100 0.3262920082 - 0.3922148049 0.3697476983 0.3288041949 - 0.3945944011 0.3723874092 0.3313164115 - 0.3969587982 0.3750202060 0.3338195086 - 0.3993231058 0.3776504993 0.3363206089 - 0.4016804099 0.3802587092 0.3388189077 - 0.4040333927 0.3828668892 0.3413054943 - 0.4063864946 0.3854654133 0.3437921107 - 0.4087263942 0.3880515099 0.3462659121 - 0.4110653996 0.3906376958 0.3487288952 - 0.4134008884 0.3932111859 0.3511919975 - 0.4157232046 0.3957807124 0.3536264896 - 0.4180454910 0.3983494937 0.3560566008 - 0.4203620851 0.4009037912 0.3584819138 - 0.4226720035 0.4034580886 0.3608815968 - 0.4249818921 0.4060049951 0.3632813096 - 0.4272814989 0.4085350931 0.3656719923 - 0.4295763969 0.4110653102 0.3680498004 - 0.4318712056 0.4135862887 0.3704276979 - 0.4341445863 0.4160991907 0.3727912009 - 0.4364148080 0.4186122119 0.3751470149 - 0.4386841953 0.4211213887 0.3775027096 - 0.4409301877 0.4236291945 0.3798316121 - 0.4431762993 0.4261370003 0.3821555972 - 0.4454182088 0.4286366105 0.3844789863 - 0.4476383030 0.4311352968 0.3867709041 - 0.4498583972 0.4336335957 0.3890627027 - 0.4520725906 0.4361194074 0.3913497925 - 0.4542694986 0.4386051893 0.3936108947 - 0.4564662874 0.4410878122 0.3958719075 - 0.4586519897 0.4435504973 0.3981297910 - 0.4608140886 0.4460130930 0.4003787935 - 0.4629763067 0.4484688938 0.4026277065 - 0.4651244879 0.4508990049 0.4048694074 - 0.4672470987 0.4533290863 0.4070971012 - 0.4693697989 0.4557510018 0.4093249142 - 0.4714809954 0.4581488967 0.4115391076 - 0.4735712111 0.4605467916 0.4137321115 - 0.4756614864 0.4629383981 0.4159250855 - 0.4777419865 0.4653125107 0.4181059897 - 0.4798024893 0.4676867127 0.4202688932 - 0.4818629026 0.4700528085 0.4224317074 - 0.4839158952 0.4723933935 0.4245786965 - 0.4859476089 0.4747340083 0.4266997874 - 0.4879794121 0.4770675004 0.4288209081 - 0.4900068939 0.4793699086 0.4309298098 - 0.4920125008 0.4816722870 0.4330140054 - 0.4940181971 0.4839723110 0.4350981116 - 0.4960232973 0.4862492085 0.4371761978 - 0.4980137050 0.4885261953 0.4392358065 - 0.5000041127 0.4908030927 0.4412953854 - 0.5019946098 0.4930540919 0.4433520138 - 0.5039594769 0.4953044951 0.4453881085 - 0.5059207082 0.4975548983 0.4474242032 - 0.5078818798 0.4997824132 0.4494602084 - 0.5098254085 0.5020049810 0.4514681101 - 0.5117604136 0.5042275190 0.4534752965 - 0.5136954188 0.5064237118 0.4554825127 - 0.5156211853 0.5086045861 0.4574632049 - 0.5175352097 0.5107855201 0.4594367146 - 0.5194491744 0.5129476190 0.4614103138 - 0.5213580728 0.5150821805 0.4633643031 - 0.5232421756 0.5172169209 0.4653035104 - 0.5251262784 0.5193458796 0.4672426879 - 0.5270103812 0.5214424729 0.4691694975 - 0.5288773179 0.5235390067 0.4710688889 - 0.5307416916 0.5256354809 0.4729683995 - 0.5326061249 0.5277007818 0.4748673141 - 0.5344613791 0.5297605991 0.4767382145 - 0.5363087058 0.5318204165 0.4786092043 - 0.5381559134 0.5338671207 0.4804801047 - 0.5400000215 0.5359017849 0.4823342860 - 0.5418263078 0.5379365087 0.4841809869 - 0.5436527133 0.5399680138 0.4860278070 - 0.5454791188 0.5419824719 0.4878644049 - 0.5472977161 0.5439968705 0.4896807075 - 0.5491135716 0.5460113287 0.4914970100 - 0.5509294868 0.5480144024 0.4933133125 - 0.5527412295 0.5500137210 0.4951044023 - 0.5545426011 0.5520130992 0.4968936145 - 0.5563439727 0.5540094972 0.4986828864 - 0.5581454039 0.5559996963 0.5004619956 - 0.5599331260 0.5579897761 0.5022308230 - 0.5617172718 0.5599799752 0.5039995909 - 0.5635015965 0.5619543791 0.5057681203 - 0.5652781725 0.5639256835 0.5075237751 - 0.5670350790 0.5658969283 0.5092794895 - 0.5687919259 0.5678616166 0.5110352039 - 0.5705487728 0.5698119998 0.5127810836 - 0.5722795725 0.5717625022 0.5145177245 - 0.5740000010 0.5737128854 0.5162543058 - 0.5757204890 0.5756404996 0.5179908276 - 0.5774372816 0.5775611997 0.5197011828 - 0.5791262984 0.5794818997 0.5214105248 - 0.5808153152 0.5813961029 0.5231198072 - 0.5825042129 0.5832800865 0.5248202085 - 0.5841786861 0.5851641297 0.5265055895 - 0.5858368874 0.5870479941 0.5281910896 - 0.5874952078 0.5889140964 0.5298765898 - 0.5891535282 0.5907651186 0.5315344930 - 0.5907946825 0.5926162004 0.5331832170 - 0.5924314260 0.5944672227 0.5348318219 - 0.5940682292 0.5962749720 0.5364782810 - 0.5957034230 0.5980765820 0.5381013751 - 0.5973179936 0.5998781919 0.5397245884 - 0.5989326239 0.6016728878 0.5413476825 - 0.6005473137 0.6034333706 0.5429692268 - 0.6021553278 0.6051939130 0.5445882082 - 0.6037486792 0.6069543958 0.5462071896 - 0.6053420901 0.6087045074 0.5478262901 - 0.6069355011 0.6104400754 0.5494415760 - 0.6085183024 0.6121757030 0.5510547757 - 0.6100909710 0.6139113903 0.5526679754 - 0.6116636992 0.6156324148 0.5542811751 - 0.6132364273 0.6173446774 0.5558875799 - 0.6147910953 0.6190569997 0.5574927926 - 0.6163368225 0.6207693219 0.5590980053 - 0.6178824902 0.6224603057 0.5607026815 - 0.6194280982 0.6241456866 0.5622801781 - 0.6209585071 0.6258311272 0.5638577938 - 0.6224846840 0.6275166273 0.5654352903 - 0.6240109205 0.6291700006 0.5670077801 - 0.6255372167 0.6308205724 0.5685514212 - 0.6270471811 0.6324712038 0.5700950027 - 0.6285545826 0.6341215968 0.5716385841 - 0.6300619841 0.6357386112 0.5731750131 - 0.6315693855 0.6373555064 0.5746886730 - 0.6330655813 0.6389724016 0.5762023926 - 0.6345605254 0.6405878067 0.5777161121 - 0.6360552907 0.6421716809 0.5792214870 - 0.6375501156 0.6437556744 0.5807061791 - 0.6390339136 0.6453396082 0.5821908116 - 0.6405161023 0.6469221711 0.5836755037 - 0.6419982910 0.6484758854 0.5851554871 - 0.6434804797 0.6500297189 0.5866240859 - 0.6449518800 0.6515833735 0.5880926847 - 0.6464208961 0.6531370878 0.5895612240 - 0.6478899121 0.6546549797 0.5910267234 - 0.6493589282 0.6561728716 0.5924829245 - 0.6508315802 0.6576908231 0.5939390063 - 0.6523056030 0.6592087150 0.5953950882 - 0.6537796259 0.6606941223 0.5968484879 - 0.6552537084 0.6621763706 0.5982881188 - 0.6567261219 0.6636587977 0.5997276902 - 0.6581972837 0.6651412249 0.6011673808 - 0.6596685052 0.6666092873 0.6026061773 - 0.6611397266 0.6680734754 0.6040263772 - 0.6626036763 0.6695376039 0.6054465771 - 0.6640564799 0.6710017920 0.6068667769 - 0.6655092835 0.6724609733 0.6082869768 - 0.6669620872 0.6739168167 0.6096944213 - 0.6684113145 0.6753727198 0.6110997796 - 0.6698417068 0.6768286228 0.6125053167 - 0.6712719798 0.6782851815 0.6139106750 - 0.6727023125 0.6797429919 0.6153100133 - 0.6741325855 0.6812009215 0.6167058945 - 0.6755437255 0.6826586723 0.6181018949 - 0.6769521236 0.6841160059 0.6194978952 - 0.6783605218 0.6855689287 0.6208894253 - 0.6797689795 0.6870219111 0.6222738028 - 0.6811653972 0.6884748936 0.6236581206 - 0.6825522780 0.6899278760 0.6250424981 - 0.6839390993 0.6913648248 0.6264259219 - 0.6853259206 0.6927976012 0.6277984977 - 0.6867079735 0.6942304969 0.6291711926 - 0.6880686283 0.6956632733 0.6305438280 - 0.6894292831 0.6970888972 0.6319164038 - 0.6907898784 0.6985052824 0.6332756281 - 0.6921504736 0.6999217272 0.6346296072 - 0.6934959888 0.7013381124 0.6359835863 - 0.6948367953 0.7027540803 0.6373376250 - 0.6961774826 0.7041571140 0.6386855245 - 0.6975182295 0.7055600882 0.6400206089 - 0.6988571882 0.7069631219 0.6413556933 - 0.7001917958 0.7083662152 0.6426907778 - 0.7015264034 0.7097566724 0.6440259218 - 0.7028610110 0.7111378908 0.6453583837 - 0.7041956186 0.7125192285 0.6466904879 - 0.7055236101 0.7139005065 0.6480225921 - 0.7068496943 0.7152801752 0.6493546963 - 0.7081757784 0.7166393995 0.6506857872 - 0.7095018029 0.7179986238 0.6520152092 - 0.7108235955 0.7193577886 0.6533446908 - 0.7121292949 0.7207170129 0.6546741128 - 0.7134348750 0.7220675945 0.6560035944 - 0.7147405148 0.7234103084 0.6573339105 - 0.7160462141 0.7247529030 0.6586644053 - 0.7173392177 0.7260956168 0.6599950194 - 0.7186235189 0.7274382114 0.6613255143 - 0.7199078798 0.7287701964 0.6626539230 - 0.7211923003 0.7301012278 0.6639747024 - 0.7224767208 0.7314321995 0.6652954817 - 0.7237458229 0.7327631712 0.6666163206 - 0.7250137925 0.7340887785 0.6679372191 - 0.7262817025 0.7353991866 0.6692438722 - 0.7275496721 0.7367095947 0.6705400944 - 0.7288151979 0.7380200028 0.6718363166 - 0.7300717235 0.7393304110 0.6731324792 - 0.7313283086 0.7406290770 0.6744287014 - 0.7325847745 0.7419185042 0.6757104993 - 0.7338414192 0.7432078719 0.6769903898 - 0.7350922823 0.7444974184 0.6782702208 - 0.7363361716 0.7457867861 0.6795499921 - 0.7375801206 0.7470659018 0.6808269024 - 0.7388241291 0.7483425140 0.6820834875 - 0.7400680184 0.7496191263 0.6833400726 - 0.7413054109 0.7508956790 0.6845965981 - 0.7425388098 0.7521722913 0.6858531833 - 0.7437722087 0.7534267902 0.6871013045 - 0.7450056076 0.7546814084 0.6883319020 - 0.7462391257 0.7559359074 0.6895623803 - 0.7474638224 0.7571905255 0.6907929778 - 0.7486857176 0.7584406137 0.6920235753 - 0.7499076128 0.7596641779 0.6932476759 - 0.7511295080 0.7608876824 0.6944645047 - 0.7523514032 0.7621111870 0.6956813931 - 0.7535635829 0.7633348107 0.6968982816 - 0.7547737956 0.7645512819 0.6981152296 - 0.7559840083 0.7657446265 0.6993272901 - 0.7571942210 0.7669377923 0.7005357742 - 0.7584043741 0.7681310773 0.7017441988 - 0.7596076727 0.7693244219 0.7029526830 - 0.7608097196 0.7705097795 0.7041612267 - 0.7620118260 0.7716736197 0.7053571939 - 0.7632138729 0.7728374004 0.7065454125 - 0.7644159794 0.7740011811 0.7077335715 - 0.7655978799 0.7751650810 0.7089217901 - 0.7667754889 0.7763243914 0.7101098895 - 0.7679529786 0.7774704099 0.7112854123 - 0.7691305876 0.7786164284 0.7124524117 - 0.7703080773 0.7797623873 0.7136192918 - 0.7714644074 0.7809084058 0.7147862911 - 0.7726129889 0.7820516229 0.7159532905 - 0.7737615705 0.7831820846 0.7171167135 - 0.7749102116 0.7843126059 0.7182772160 - 0.7760587931 0.7854431868 0.7194375992 - 0.7771977186 0.7865737081 0.7205979824 - 0.7783300877 0.7877029777 0.7217584252 - 0.7794623971 0.7888091207 0.7229158282 - 0.7805947065 0.7899152040 0.7240688205 - 0.7817270160 0.7910214067 0.7252218723 - 0.7828512192 0.7921274900 0.7263749242 - 0.7839630246 0.7932336926 0.7275279164 - 0.7850748897 0.7943174243 0.7286769748 - 0.7861866951 0.7953978181 0.7298130989 - 0.7872986197 0.7964780927 0.7309491038 - 0.7884055972 0.7975584865 0.7320852280 - 0.7894845009 0.7986388803 0.7332212925 - 0.7905632854 0.7997053862 0.7343574166 - 0.7916421294 0.8007637262 0.7354778051 - 0.7927209735 0.8018221259 0.7365983129 - 0.7937998176 0.8028805256 0.7377187014 - 0.7948549986 0.8039388061 0.7388392091 - 0.7959054112 0.8049921989 0.7399595976 - 0.7969558835 0.8060358167 0.7410690188 - 0.7980064154 0.8070793748 0.7421737909 - 0.7990568280 0.8081229925 0.7432786822 - 0.8000957966 0.8091667295 0.7443835735 - 0.8011217117 0.8102102876 0.7454885244 - 0.8021476269 0.8112317920 0.7465876937 - 0.8031734824 0.8122528791 0.7476770878 - 0.8041993976 0.8132740259 0.7487664819 - 0.8052238822 0.8142951727 0.7498558760 - 0.8062176704 0.8153163195 0.7509453297 - 0.8072115183 0.8163194060 0.7520347238 - 0.8082051873 0.8173090816 0.7531046867 - 0.8091989756 0.8182986975 0.7541738749 - 0.8101927042 0.8192883730 0.7552431822 - 0.8111652136 0.8202779889 0.7563123703 - 0.8121206760 0.8212653995 0.7573816776 - 0.8130761981 0.8222320080 0.7584378123 - 0.8140317798 0.8231986761 0.7594807744 - 0.8149873018 0.8241654038 0.7605237961 - 0.8159424067 0.8251320720 0.7615668178 - 0.8168678880 0.8260987997 0.7626097202 - 0.8177933097 0.8270537257 0.7636526823 - 0.8187186718 0.8279997706 0.7646675706 - 0.8196440935 0.8289458752 0.7656818032 - 0.8205695152 0.8298919797 0.7666959763 - 0.8214858174 0.8308380842 0.7677102089 - 0.8223888278 0.8317841887 0.7687243819 - 0.8232917786 0.8327031732 0.7697293162 - 0.8241947889 0.8336216807 0.7707198262 - 0.8250979185 0.8345401883 0.7717103958 - 0.8260009289 0.8354586959 0.7727010250 - 0.8268916011 0.8363772035 0.7736914754 - 0.8277779818 0.8372876048 0.7746821046 - 0.8286644816 0.8381816149 0.7756630182 - 0.8295509219 0.8390756845 0.7766405940 - 0.8304374218 0.8399696946 0.7776182890 - 0.8313233852 0.8408638239 0.7785959840 - 0.8321877122 0.8417577744 0.7795736194 - 0.8330519795 0.8426352143 0.7805503011 - 0.8339163065 0.8435022831 0.7815071940 - 0.8347805738 0.8443694711 0.7824640870 - 0.8356449008 0.8452367187 0.7834209204 - 0.8365029097 0.8461037874 0.7843778133 - 0.8373395801 0.8469709754 0.7853347063 - 0.8381764293 0.8478137851 0.7862827182 - 0.8390132189 0.8486523032 0.7872080207 - 0.8398498893 0.8494908214 0.7881333232 - 0.8406866789 0.8503292203 0.7890586257 - 0.8415160179 0.8511676788 0.7899839282 - 0.8423328996 0.8520057797 0.7909091711 - 0.8431499004 0.8528186083 0.7918230891 - 0.8439669013 0.8536313176 0.7927232981 - 0.8447839022 0.8544440269 0.7936236262 - 0.8456009030 0.8552566767 0.7945237756 - 0.8464123011 0.8560693860 0.7954239845 - 0.8472172022 0.8568791151 0.7963243127 - 0.8480221033 0.8576657772 0.7972148061 - 0.8488271236 0.8584524989 0.7980977297 - 0.8496320248 0.8592392206 0.7989805937 - 0.8504369259 0.8600258827 0.7998635173 - 0.8512371778 0.8608126044 0.8007463813 - 0.8520324230 0.8615955114 0.8016293049 - 0.8528277278 0.8623574972 0.8024988174 - 0.8536229134 0.8631194234 0.8033595085 - 0.8544182181 0.8638814092 0.8042201996 - 0.8552135229 0.8646433949 0.8050808907 - 0.8559978008 0.8654053211 0.8059417009 - 0.8567681909 0.8661652207 0.8068023920 - 0.8575387001 0.8669108748 0.8076456189 - 0.8583090901 0.8676565289 0.8084762096 - 0.8590794802 0.8684021235 0.8093069196 - 0.8598498702 0.8691477776 0.8101375103 - 0.8606119752 0.8698933721 0.8109682202 - 0.8613570929 0.8706384897 0.8117988706 - 0.8621020913 0.8713684082 0.8126195073 - 0.8628472090 0.8720982075 0.8134300709 - 0.8635922074 0.8728280067 0.8142405748 - 0.8643373251 0.8735578060 0.8150511980 - 0.8650797009 0.8742876053 0.8158618212 - 0.8658074141 0.8750174046 0.8166723847 - 0.8665351272 0.8757197857 0.8174800873 - 0.8672627807 0.8764181733 0.8182824254 - 0.8679904938 0.8771166205 0.8190848231 - 0.8687182069 0.8778150082 0.8198871017 - 0.8694459200 0.8785135150 0.8206894994 - 0.8701534271 0.8792119026 0.8214918971 - 0.8708590865 0.8798918724 0.8222925067 - 0.8715646863 0.8805618882 0.8230811954 - 0.8722702861 0.8812319040 0.8238700032 - 0.8729758859 0.8819019794 0.8246586919 - 0.8736816049 0.8825719953 0.8254473805 - 0.8743742704 0.8832420111 0.8262361884 - 0.8750584722 0.8839061260 0.8270248771 - 0.8757427931 0.8845593929 0.8277987242 - 0.8764269948 0.8852127194 0.8285697103 - 0.8771111965 0.8858659863 0.8293406963 - 0.8777955174 0.8865193725 0.8301116824 - 0.8784751296 0.8871726990 0.8308827281 - 0.8791388869 0.8878260255 0.8316535950 - 0.8798025846 0.8884590268 0.8324189782 - 0.8804662824 0.8890919089 0.8331785202 - 0.8811300993 0.8897247910 0.8339378834 - 0.8817937970 0.8903576732 0.8346974254 - 0.8824576139 0.8909906745 0.8354569077 - 0.8831110001 0.8916236162 0.8362162709 - 0.8837615848 0.8922482729 0.8369745016 - 0.8844121099 0.8928667903 0.8377140760 - 0.8850625753 0.8934854269 0.8384537101 - 0.8857132196 0.8941038847 0.8391932845 - 0.8863636851 0.8947224021 0.8399327993 - 0.8870092034 0.8953409195 0.8406723738 - 0.8876399994 0.8959586024 0.8414120078 - 0.8882709146 0.8965657949 0.8421400785 - 0.8889017105 0.8971729279 0.8428595066 - 0.8895326257 0.8977800012 0.8435789943 - 0.8901634216 0.8983871937 0.8442984223 - 0.8907942772 0.8989943266 0.8450179100 - 0.8914089799 0.8996015191 0.8457372785 - 0.8920158744 0.9001982212 0.8464565277 - 0.8926227093 0.9007843733 0.8471537828 - 0.8932296038 0.9013705850 0.8478512168 - 0.8938364983 0.9019567966 0.8485484719 - 0.8944432735 0.9025430083 0.8492457867 - 0.8950501084 0.9031292200 0.8499431014 - 0.8956450820 0.9037153721 0.8506404161 - 0.8962401152 0.9042838812 0.8513295054 - 0.8968350887 0.9048495293 0.8520048261 - 0.8974301815 0.9054149985 0.8526800871 - 0.8980252147 0.9059805274 0.8533554077 - 0.8986201882 0.9065459967 0.8540307283 - 0.8992115259 0.9071115851 0.8547059894 - 0.8997929096 0.9076750875 0.8553813100 - 0.9003742933 0.9082267284 0.8560460806 - 0.9009556174 0.9087783098 0.8567056060 - 0.9015370011 0.9093298912 0.8573651910 - 0.9021183848 0.9098814726 0.8580247760 - 0.9026998281 0.9104331136 0.8586843014 - 0.9032738805 0.9109846950 0.8593438864 - 0.9038397074 0.9115307927 0.8600034714 - 0.9044054747 0.9120668769 0.8606566191 - 0.9049713016 0.9126030207 0.8613088727 - 0.9055371284 0.9131391048 0.8619611859 - 0.9061030149 0.9136751294 0.8626134992 - 0.9066687822 0.9142112136 0.8632658124 - 0.9072257876 0.9147472978 0.8639181256 - 0.9077767730 0.9152768850 0.8645702004 - 0.9083276987 0.9157999158 0.8652151227 - 0.9088786840 0.9163230062 0.8658599854 - 0.9094297290 0.9168459773 0.8665049076 - 0.9099807143 0.9173690081 0.8671498895 - 0.9105315804 0.9178919792 0.8677948117 - 0.9110671878 0.9184150100 0.8684396744 - 0.9115940928 0.9189338088 0.8690823913 - 0.9121209979 0.9194490910 0.8697077036 - 0.9126479030 0.9199643731 0.8703330159 - 0.9131749272 0.9204797149 0.8709583282 - 0.9137017727 0.9209951162 0.8715835214 - 0.9142286777 0.9215103984 0.8722087741 - 0.9147396088 0.9220256805 0.8728340864 - 0.9152405262 0.9225335121 0.8734576106 - 0.9157413244 0.9230350852 0.8740668297 - 0.9162421227 0.9235367775 0.8746759892 - 0.9167429805 0.9240384102 0.8752852082 - 0.9172437787 0.9245399833 0.8758944273 - 0.9177445769 0.9250416756 0.8765035868 - 0.9182330966 0.9255433083 0.8771128058 - 0.9187096953 0.9260321856 0.8777214885 - 0.9191861749 0.9265064001 0.8783164024 - 0.9196627736 0.9269806743 0.8789111972 - 0.9201393127 0.9274548888 0.8795061111 - 0.9206159115 0.9279291034 0.8801010251 - 0.9210923910 0.9284033775 0.8806958199 - 0.9215654731 0.9288775921 0.8812906742 - 0.9220311046 0.9293445945 0.8818855286 - 0.9224967957 0.9297950268 0.8824583292 - 0.9229624271 0.9302453995 0.8830280900 - 0.9234281182 0.9306957722 0.8835980296 - 0.9238938093 0.9311462045 0.8841677904 - 0.9243593812 0.9315965772 0.8847376108 - 0.9248239994 0.9320470095 0.8853073716 - 0.9252728820 0.9324961901 0.8858771920 - 0.9257218242 0.9329271913 0.8864341974 - 0.9261707067 0.9333581924 0.8869844079 - 0.9266195893 0.9337891936 0.8875344992 - 0.9270685911 0.9342203140 0.8880845904 - 0.9275174737 0.9346513152 0.8886348009 - 0.9279664159 0.9350823164 0.8891848922 - 0.9283986092 0.9355133772 0.8897349834 - 0.9288244843 0.9359328151 0.8902794123 - 0.9292504191 0.9363483191 0.8908125758 - 0.9296762943 0.9367638826 0.8913459182 - 0.9301022291 0.9371793866 0.8918790817 - 0.9305281043 0.9375948906 0.8924124241 - 0.9309539795 0.9380103946 0.8929455876 - 0.9313746095 0.9384258986 0.8934788108 - 0.9317831993 0.9388350248 0.8940120935 - 0.9321917892 0.9392321110 0.8945347071 - 0.9326003790 0.9396290779 0.8950567245 - 0.9330090880 0.9400262237 0.8955786824 - 0.9334176779 0.9404231906 0.8961006999 - 0.9338263273 0.9408202767 0.8966227770 - 0.9342349172 0.9412173033 0.8971447945 - 0.9346324205 0.9416143894 0.8976668119 - 0.9350271821 0.9419872165 0.8981863856 - 0.9354221225 0.9423564076 0.8987035155 - 0.9358168840 0.9427254796 0.8992205262 - 0.9362118244 0.9430946708 0.8997374773 - 0.9366065860 0.9434638023 0.9002544880 - 0.9370014071 0.9438329935 0.9007714987 - 0.9373939037 0.9442021251 0.9012885094 - 0.9377782941 0.9445647001 0.9018055797 - 0.9381626844 0.9449126720 0.9023094177 - 0.9385470748 0.9452607036 0.9028121829 - 0.9389315844 0.9456086159 0.9033150077 - 0.9393159747 0.9459565878 0.9038177729 - 0.9397004247 0.9463046193 0.9043205976 - 0.9400848150 0.9466525912 0.9048234224 - 0.9404577017 0.9470005035 0.9053261876 - 0.9408218265 0.9473389983 0.9058225155 - 0.9411860108 0.9476727843 0.9063037038 - 0.9415500760 0.9480065703 0.9067848921 - 0.9419142008 0.9483404160 0.9072660804 - 0.9422783852 0.9486742020 0.9077473283 - 0.9426425099 0.9490079880 0.9082283974 - 0.9430066943 0.9493417740 0.9087095857 - 0.9433518052 0.9496756196 0.9091907740 - 0.9436928034 0.9499998093 0.9096608162 - 0.9440338016 0.9503235817 0.9101228118 - 0.9443747997 0.9506472945 0.9105848074 - 0.9447157979 0.9509710073 0.9110468030 - 0.9450569153 0.9512947202 0.9115087986 - 0.9453979135 0.9516184926 0.9119707942 - 0.9457389116 0.9519422054 0.9124327898 - 0.9460589290 0.9522647262 0.9128947854 - 0.9463787079 0.9525792003 0.9133455157 - 0.9466984868 0.9528936744 0.9137930870 - 0.9470183253 0.9532083273 0.9142407179 - 0.9473382235 0.9535228014 0.9146882296 - 0.9476580024 0.9538373947 0.9151358008 - 0.9479777813 0.9541519284 0.9155833721 - 0.9482966065 0.9544664025 0.9160308838 - 0.9486029744 0.9547755718 0.9164785147 - 0.9489094019 0.9550681114 0.9169083238 - 0.9492158294 0.9553607106 0.9173362255 - 0.9495223165 0.9556531906 0.9177641273 - 0.9498286843 0.9559456706 0.9181920290 - 0.9501351118 0.9562382102 0.9186198115 - 0.9504414797 0.9565308094 0.9190477133 - 0.9507477880 0.9568232894 0.9194756150 - 0.9510523081 0.9571123123 0.9199035168 - 0.9513568878 0.9573916793 0.9203202724 - 0.9516614079 0.9576711059 0.9207363725 - 0.9519659877 0.9579504728 0.9211524725 - 0.9522706270 0.9582298994 0.9215685129 - 0.9525750875 0.9585093260 0.9219846129 - 0.9528797269 0.9587886930 0.9224005938 - 0.9531841874 0.9590681195 0.9228166938 - 0.9534816146 0.9593474865 0.9232327938 - 0.9537789822 0.9596264958 0.9236450791 - 0.9540762901 0.9599055052 0.9240568280 - 0.9543737173 0.9601845145 0.9244685769 - 0.9546710253 0.9604635239 0.9248803854 - 0.9549683928 0.9607424736 0.9252920747 - 0.9552657008 0.9610214829 0.9257038832 - 0.9555630088 0.9613004923 0.9261155725 - 0.9558495283 0.9615790844 0.9265273809 - 0.9561336040 0.9618505239 0.9269320965 - 0.9564176202 0.9621219039 0.9273340702 - 0.9567016959 0.9623932838 0.9277362227 - 0.9569857121 0.9626647234 0.9281383157 - 0.9572697878 0.9629361033 0.9285402894 - 0.9575538039 0.9632074833 0.9289423823 - 0.9578378797 0.9634789228 0.9293444157 - 0.9581202269 0.9637503028 0.9297465086 - 0.9584012032 0.9640163779 0.9301460981 - 0.9586821795 0.9642812014 0.9305431843 - 0.9589632154 0.9645460248 0.9309402704 - 0.9592441916 0.9648107886 0.9313374758 - 0.9595252872 0.9650756121 0.9317346215 - 0.9598063231 0.9653403759 0.9321317077 - 0.9600872993 0.9656051993 0.9325287938 - 0.9603686929 0.9658700228 0.9329259992 - 0.9606518149 0.9661340714 0.9333214760 - 0.9609349966 0.9663974047 0.9337096810 - 0.9612181187 0.9666607976 0.9340978265 - 0.9615011811 0.9669241905 0.9344859719 - 0.9617843032 0.9671875238 0.9348741770 - 0.9620674253 0.9674509168 0.9352623820 - 0.9623506069 0.9677141905 0.9356505871 - 0.9626337290 0.9679775834 0.9360387921 - 0.9629138708 0.9682406783 0.9364269972 - 0.9631931782 0.9685000777 0.9368088841 - 0.9634724855 0.9687594771 0.9371889234 - 0.9637517929 0.9690188169 0.9375690222 - 0.9640312195 0.9692782164 0.9379490018 - 0.9643105268 0.9695376158 0.9383289814 - 0.9645897746 0.9697968960 0.9387090802 - 0.9648690820 0.9700562954 0.9390891194 - 0.9651448727 0.9703155756 0.9394692183 - 0.9654085040 0.9705702066 0.9398465157 - 0.9656721950 0.9708209038 0.9402164221 - 0.9659358263 0.9710716009 0.9405863881 - 0.9661995173 0.9713222980 0.9409562945 - 0.9664632082 0.9715729952 0.9413263202 - 0.9667267799 0.9718236923 0.9416962266 - 0.9669904709 0.9720743895 0.9420661926 - 0.9672541022 0.9723250866 0.9424360991 - 0.9675124884 0.9725757241 0.9428061247 - 0.9677672982 0.9728142023 0.9431679249 - 0.9680221081 0.9730519056 0.9435257912 - 0.9682769179 0.9732896090 0.9438837171 - 0.9685317278 0.9735273123 0.9442415833 - 0.9687864780 0.9737650752 0.9445996284 - 0.9690412879 0.9740027785 0.9449574947 - 0.9692960978 0.9742404819 0.9453154206 - 0.9695509076 0.9744781852 0.9456732869 - 0.9698060155 0.9747121930 0.9460312128 - 0.9700611830 0.9749348760 0.9463843107 - 0.9703164101 0.9751574993 0.9467371702 - 0.9705715179 0.9753801823 0.9470902085 - 0.9708266854 0.9756028056 0.9474431276 - 0.9710819125 0.9758254886 0.9477961063 - 0.9713370800 0.9760481715 0.9481490254 - 0.9715921879 0.9762707949 0.9485020041 - 0.9718468189 0.9764934778 0.9488549829 - 0.9720928073 0.9767106771 0.9492064714 - 0.9723387957 0.9769214988 0.9495524764 - 0.9725847840 0.9771322012 0.9498984814 - 0.9728307724 0.9773430228 0.9502444863 - 0.9730768204 0.9775537848 0.9505904913 - 0.9733228087 0.9777646065 0.9509364963 - 0.9735687971 0.9779753089 0.9512825012 - 0.9738147855 0.9781860709 0.9516285062 - 0.9740588069 0.9783968925 0.9519745111 - 0.9742915034 0.9786027074 0.9523156285 - 0.9745242000 0.9788047075 0.9526464939 - 0.9747567773 0.9790067077 0.9529774189 - 0.9749894738 0.9792087078 0.9533084035 - 0.9752221704 0.9794108272 0.9536393285 - 0.9754549265 0.9796128273 0.9539703131 - 0.9756876230 0.9798148274 0.9543011785 - 0.9759202003 0.9800168276 0.9546322227 - 0.9761514068 0.9802188277 0.9549630880 - 0.9763734937 0.9804111123 0.9552907944 - 0.9765955806 0.9805964828 0.9556127787 - 0.9768176079 0.9807819128 0.9559348226 - 0.9770396948 0.9809672832 0.9562568069 - 0.9772617817 0.9811527133 0.9565787911 - 0.9774839282 0.9813380837 0.9569007754 - 0.9777060151 0.9815235138 0.9572228193 - 0.9779281020 0.9817088842 0.9575446844 - 0.9781497121 0.9818943143 0.9578667283 - 0.9783595204 0.9820705056 0.9581860900 - 0.9785693884 0.9822378159 0.9584987760 - 0.9787791967 0.9824051261 0.9588115811 - 0.9789891243 0.9825723767 0.9591243267 - 0.9791988730 0.9827396870 0.9594370723 - 0.9794088006 0.9829071164 0.9597498178 - 0.9796186090 0.9830744267 0.9600626230 - 0.9798284769 0.9832416773 0.9603753090 - 0.9800382853 0.9834089875 0.9606881142 - 0.9802370071 0.9835721850 0.9609987736 - 0.9804332852 0.9837269783 0.9612951279 - 0.9806296825 0.9838817716 0.9615914822 - 0.9808260202 0.9840366244 0.9618877769 - 0.9810224175 0.9841914177 0.9621840715 - 0.9812188148 0.9843462110 0.9624804258 - 0.9814150929 0.9845011234 0.9627767205 - 0.9816114902 0.9846559167 0.9630730152 - 0.9818078279 0.9848107100 0.9633693099 - 0.9819964767 0.9849649072 0.9636656046 - 0.9821782112 0.9851077199 0.9639506936 - 0.9823600054 0.9852504730 0.9642338157 - 0.9825417995 0.9853932858 0.9645169973 - 0.9827235937 0.9855362177 0.9648001790 - 0.9829053879 0.9856789708 0.9650833011 - 0.9830871224 0.9858217835 0.9653664827 - 0.9832689166 0.9859645963 0.9656497240 - 0.9834507108 0.9861074090 0.9659329057 - 0.9836298227 0.9862502217 0.9662160277 - 0.9837905765 0.9863815904 0.9664933085 - 0.9839512706 0.9865074158 0.9667642713 - 0.9841120243 0.9866331816 0.9670352936 - 0.9842727780 0.9867590070 0.9673063159 - 0.9844334722 0.9868847728 0.9675772786 - 0.9845942855 0.9870105982 0.9678484201 - 0.9847549796 0.9871364236 0.9681193829 - 0.9849156737 0.9872621894 0.9683904052 - 0.9850764871 0.9873880148 0.9686614275 - 0.9852262139 0.9875094891 0.9689320922 - 0.9853692055 0.9876127243 0.9691805243 - 0.9855121970 0.9877158999 0.9694290161 - 0.9856551886 0.9878190160 0.9696773887 - 0.9857981801 0.9879221916 0.9699258208 - 0.9859411120 0.9880254269 0.9701743126 - 0.9860841036 0.9881286025 0.9704226851 - 0.9862270951 0.9882317781 0.9706711769 - 0.9863700867 0.9883350134 0.9709196091 - 0.9865130186 0.9884381890 0.9711681008 - 0.9866449833 0.9885314703 0.9714096785 - 0.9867771268 0.9886183739 0.9716426134 - 0.9869090915 0.9887052774 0.9718756080 - 0.9870411754 0.9887921810 0.9721086025 - 0.9871733189 0.9888790846 0.9723415971 - 0.9873052835 0.9889659882 0.9725744724 - 0.9874374270 0.9890528917 0.9728075266 - 0.9875693917 0.9891397953 0.9730405211 - 0.9877014756 0.9892266989 0.9732735157 - 0.9878289104 0.9893134832 0.9735063910 - 0.9879450202 0.9893946052 0.9737377763 - 0.9880610704 0.9894750714 0.9739686251 - 0.9881772995 0.9895555973 0.9741994739 - 0.9882934093 0.9896361232 0.9744303226 - 0.9884095192 0.9897165895 0.9746611714 - 0.9885256886 0.9897971153 0.9748920202 - 0.9886417985 0.9898777008 0.9751229286 - 0.9887579083 0.9899582267 0.9753537774 - 0.9888740778 0.9900386930 0.9755846262 - 0.9889798164 0.9901186228 0.9758154750 - 0.9890741706 0.9901947975 0.9760391712 - 0.9891685247 0.9902709723 0.9762626886 - 0.9892628193 0.9903472066 0.9764863253 - 0.9893571138 0.9904233813 0.9767099023 - 0.9894514084 0.9904996157 0.9769334793 - 0.9895458221 0.9905757904 0.9771571159 - 0.9896401167 0.9906520247 0.9773806930 - 0.9897344112 0.9907281995 0.9776043296 - 0.9898287058 0.9908043742 0.9778279066 - 0.9899169207 0.9908795953 0.9780502915 - 0.9900001884 0.9909514785 0.9782621861 - 0.9900833964 0.9910233021 0.9784740806 - 0.9901666045 0.9910951853 0.9786859751 - 0.9902498126 0.9911670089 0.9788979292 - 0.9903330207 0.9912388921 0.9791098237 - 0.9904162884 0.9913107157 0.9793217182 - 0.9904994965 0.9913825989 0.9795336127 - 0.9905827045 0.9914544225 0.9797455072 - 0.9906659126 0.9915261865 0.9799574018 - 0.9907503128 0.9915974736 0.9801694751 - 0.9908356071 0.9916664958 0.9803833961 - 0.9909210205 0.9917355180 0.9805971980 - 0.9910063744 0.9918044806 0.9808109999 - 0.9910917878 0.9918735027 0.9810249209 - 0.9911772013 0.9919425249 0.9812387228 - 0.9912626147 0.9920114875 0.9814525247 - 0.9913480282 0.9920805097 0.9816663861 - 0.9914333820 0.9921495914 0.9818801880 - 0.9915186763 0.9922186136 0.9820939898 - 0.9916055202 0.9922881126 0.9823077917 - 0.9916943908 0.9923617244 0.9825233221 - 0.9917833805 0.9924353957 0.9827387929 - 0.9918723106 0.9925091267 0.9829543233 - 0.9919611812 0.9925826788 0.9831697941 - 0.9920501709 0.9926564097 0.9833853245 - 0.9921391010 0.9927300215 0.9836007953 - 0.9922280908 0.9928036928 0.9838163257 - 0.9923170209 0.9928774238 0.9840317965 - 0.9924060106 0.9929509759 0.9842473269 - 0.9924944043 0.9930247068 0.9844626784 - 0.9925794005 0.9931048155 0.9846640825 - 0.9926643968 0.9931856990 0.9848614931 - 0.9927493930 0.9932667017 0.9850590229 - 0.9928343892 0.9933475852 0.9852564931 - 0.9929193854 0.9934285283 0.9854540229 - 0.9930043817 0.9935094714 0.9856514931 - 0.9930893779 0.9935904145 0.9858490229 - 0.9931743741 0.9936712980 0.9860464931 - 0.9932593703 0.9937523007 0.9862440228 - 0.9933443069 0.9938331842 0.9864414930 - 0.9934259057 0.9939159155 0.9866343737 - 0.9935063124 0.9940000772 0.9868218899 - 0.9935867786 0.9940842986 0.9870094061 - 0.9936671853 0.9941684008 0.9871969223 - 0.9937477112 0.9942526221 0.9873843789 - 0.9938281178 0.9943367243 0.9875720143 - 0.9939085841 0.9944208860 0.9877594709 - 0.9939889908 0.9945051074 0.9879469872 - 0.9940693974 0.9945892096 0.9881345034 - 0.9941499233 0.9946733713 0.9883220792 - 0.9942312241 0.9947574139 0.9885097146 - 0.9943147898 0.9948405027 0.9887015224 - 0.9943982959 0.9949235916 0.9888933897 - 0.9944819212 0.9950066805 0.9890851974 - 0.9945654869 0.9950898290 0.9892770052 - 0.9946489930 0.9951729178 0.9894688129 - 0.9947326183 0.9952560067 0.9896606803 - 0.9948161840 0.9953390956 0.9898524880 - 0.9948996902 0.9954221845 0.9900442958 - 0.9949833155 0.9955052733 0.9902361035 - 0.9950668812 0.9955884218 0.9904279709 - 0.9951515198 0.9956672788 0.9906188846 - 0.9952365160 0.9957426190 0.9908090234 - 0.9953215122 0.9958178997 0.9909991026 - 0.9954065084 0.9958932996 0.9911891818 - 0.9954916239 0.9959685802 0.9913793206 - 0.9955766201 0.9960439205 0.9915693998 - 0.9956616163 0.9961193204 0.9917594790 - 0.9957466722 0.9961946011 0.9919496179 - 0.9958317280 0.9962698817 0.9921396971 - 0.9959167242 0.9963452816 0.9923297763 - 0.9960017800 0.9964206219 0.9925199151 - 0.9960864782 0.9964954853 0.9927043915 - 0.9961711764 0.9965702891 0.9928877950 - 0.9962558746 0.9966450930 0.9930710793 - 0.9963405728 0.9967198968 0.9932544231 - 0.9964252710 0.9967947006 0.9934378266 - 0.9965100288 0.9968695045 0.9936211109 - 0.9965947270 0.9969443083 0.9938043952 - 0.9966794252 0.9970191121 0.9939877987 - 0.9967641234 0.9970939159 0.9941710830 - 0.9968488216 0.9971687198 0.9943544269 - 0.9969335198 0.9972440004 0.9945374727 - 0.9970176220 0.9973254204 0.9947144985 - 0.9971017241 0.9974067807 0.9948914051 - 0.9971858859 0.9974880815 0.9950683117 - 0.9972699881 0.9975695014 0.9952452779 - 0.9973540902 0.9976508021 0.9954221845 - 0.9974381924 0.9977322221 0.9955992103 - 0.9975224137 0.9978135824 0.9957761168 - 0.9976065159 0.9978948832 0.9959530830 - 0.9976906180 0.9979763031 0.9961299896 - 0.9977747798 0.9980576038 0.9963068962 - 0.9978600740 0.9981396198 0.9964826107 - 0.9979493022 0.9982247949 0.9966511130 - 0.9980384707 0.9983099103 0.9968194962 - 0.9981275797 0.9983950257 0.9969878793 - 0.9982168078 0.9984800816 0.9971563220 - 0.9983059764 0.9985653162 0.9973248243 - 0.9983952045 0.9986503720 0.9974932075 - 0.9984843731 0.9987354875 0.9976615906 - 0.9985736012 0.9988206029 0.9978299737 - 0.9986627102 0.9989057779 0.9979984760 - 0.9987518787 0.9989908934 0.9981669188 - 0.9988439083 0.9990757704 0.9983330965 - 0.9989489913 0.9991598725 0.9984846115 - 0.9990540743 0.9992439151 0.9986361265 - 0.9991592169 0.9993278980 0.9987877011 - 0.9992642999 0.9994118810 0.9989392161 - 0.9993693829 0.9994959235 0.9990907907 - 0.9994745255 0.9995799065 0.9992423058 - 0.9995796084 0.9996638894 0.9993938208 - 0.9996846914 0.9997479916 0.9995453954 - 0.9997897744 0.9998319745 0.9996969104 - 0.9998949170 0.9999160171 0.9998484850 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0001730574 0.0002039562 0.0002358485 + 0.0003522631 0.0004149732 0.0004803593 + 0.0005379661 0.0006318853 0.0007290206 + 0.0007310679 0.0008542882 0.0009827145 + 0.0009340477 0.0010819150 0.0012466050 + 0.0011457250 0.0013172789 0.0015175150 + 0.0013664630 0.0015590760 0.0017966420 + 0.0015953979 0.0018074680 0.0020813060 + 0.0018292820 0.0020661030 0.0023712160 + 0.0020738509 0.0023327931 0.0026701461 + 0.0023281691 0.0026087810 0.0029817061 + 0.0025920530 0.0028905689 0.0033033220 + 0.0028679480 0.0031836890 0.0036326619 + 0.0031529539 0.0034913400 0.0039701960 + 0.0034448660 0.0038095911 0.0043217931 + 0.0037472011 0.0041361330 0.0046877442 + 0.0040617771 0.0044678152 0.0050627491 + 0.0043859989 0.0048073409 0.0054407581 + 0.0047217272 0.0051559508 0.0058272062 + 0.0050699208 0.0055170869 0.0062212390 + 0.0054345722 0.0058912761 0.0066273510 + 0.0058116391 0.0062767328 0.0070466590 + 0.0062011848 0.0066723628 0.0074762749 + 0.0066031339 0.0070798998 0.0079114493 + 0.0070177661 0.0075020911 0.0083535369 + 0.0074442741 0.0079338206 0.0088046975 + 0.0078795254 0.0083771208 0.0092653846 + 0.0083278697 0.0088326121 0.0097344546 + 0.0087915091 0.0092987726 0.0102126198 + 0.0092691761 0.0097725317 0.0107003897 + 0.0097623114 0.0102546504 0.0111954799 + 0.0102694500 0.0107495701 0.0116990302 + 0.0107906098 0.0112578301 0.0122114504 + 0.0113268001 0.0117790196 0.0127311097 + 0.0118786199 0.0123126702 0.0132614998 + 0.0124431401 0.0128612900 0.0138018904 + 0.0130219003 0.0134234196 0.0143521996 + 0.0136152301 0.0139996996 0.0149130402 + 0.0142232198 0.0145876799 0.0154852197 + 0.0148449196 0.0151869999 0.0160684697 + 0.0154783595 0.0158025604 0.0166603103 + 0.0161256995 0.0164336897 0.0172653906 + 0.0167863201 0.0170805994 0.0178804602 + 0.0174631495 0.0177476797 0.0185050704 + 0.0181544498 0.0184269901 0.0191421397 + 0.0188641995 0.0191224609 0.0197905600 + 0.0195880793 0.0198333003 0.0204491094 + 0.0203275606 0.0205603093 0.0211143699 + 0.0210811403 0.0212992709 0.0217879806 + 0.0218534302 0.0220546499 0.0224671308 + 0.0226451308 0.0228254497 0.0231546499 + 0.0234524105 0.0236116406 0.0238485206 + 0.0242763907 0.0244143009 0.0245480109 + 0.0251218900 0.0252302606 0.0252581500 + 0.0259851404 0.0260641407 0.0259789899 + 0.0268620700 0.0269184001 0.0267140791 + 0.0277548693 0.0277873296 0.0274658203 + 0.0286648702 0.0286731608 0.0282310992 + 0.0295908507 0.0295777693 0.0290101692 + 0.0305334404 0.0304972902 0.0298030600 + 0.0314947292 0.0314330608 0.0306055099 + 0.0324744582 0.0323868990 0.0314168893 + 0.0334764086 0.0333601981 0.0322372504 + 0.0344995111 0.0343528390 0.0330651999 + 0.0355402492 0.0353610590 0.0339045897 + 0.0365981199 0.0363844894 0.0347526409 + 0.0376747511 0.0374244489 0.0356094018 + 0.0387722701 0.0384774804 0.0364748016 + 0.0398925804 0.0395460315 0.0373485014 + 0.0410357118 0.0406353809 0.0382347293 + 0.0421971716 0.0417462699 0.0391340293 + 0.0433758609 0.0428716205 0.0400448591 + 0.0445724390 0.0440130197 0.0409630090 + 0.0457868986 0.0451716594 0.0418913700 + 0.0470213406 0.0463434607 0.0428329296 + 0.0482786596 0.0475279316 0.0437831692 + 0.0495616607 0.0487296283 0.0447406098 + 0.0508708991 0.0499518812 0.0457080491 + 0.0522039905 0.0511935391 0.0466858111 + 0.0535621308 0.0524507686 0.0476739705 + 0.0549456812 0.0537222885 0.0486743301 + 0.0563542917 0.0550098717 0.0496832319 + 0.0577872396 0.0563169084 0.0507013202 + 0.0592435412 0.0576429814 0.0517350510 + 0.0607235916 0.0589821301 0.0527782217 + 0.0622299090 0.0603348911 0.0538305715 + 0.0637641624 0.0617051981 0.0548948608 + 0.0653300881 0.0630972311 0.0559714995 + 0.0669195279 0.0645076036 0.0570586994 + 0.0685284436 0.0659297109 0.0581539795 + 0.0701561198 0.0673606992 0.0592579208 + 0.0718030334 0.0688040778 0.0603738800 + 0.0734695569 0.0702652484 0.0615030490 + 0.0751590133 0.0717460513 0.0626446307 + 0.0768723935 0.0732394904 0.0637940615 + 0.0786073431 0.0747426003 0.0649530664 + 0.0803640932 0.0762560293 0.0661245733 + 0.0821429417 0.0777818337 0.0673098490 + 0.0839471966 0.0793204084 0.0685084313 + 0.0857824534 0.0808681324 0.0697233528 + 0.0876402482 0.0824251175 0.0709487796 + 0.0895186588 0.0839939788 0.0721847713 + 0.0914160535 0.0855768770 0.0734331012 + 0.0933414400 0.0871768892 0.0746951923 + 0.0952875987 0.0887894779 0.0759687126 + 0.0972552076 0.0904145017 0.0772481188 + 0.0992452875 0.0920512825 0.0785354823 + 0.1012666970 0.0936988667 0.0798321664 + 0.1033084020 0.0953587517 0.0811416209 + 0.1053688005 0.0970306993 0.0824629217 + 0.1074498966 0.0987144411 0.0837924406 + 0.1095537990 0.1004106030 0.0851297081 + 0.1116733998 0.1021208018 0.0864788592 + 0.1138081029 0.1038433984 0.0878428370 + 0.1159645021 0.1055795997 0.0892181471 + 0.1181332022 0.1073433980 0.0906047598 + 0.1203133017 0.1091196015 0.0920063481 + 0.1225123033 0.1109066978 0.0934172571 + 0.1247252971 0.1127050966 0.0948378369 + 0.1269482970 0.1145213023 0.0962786898 + 0.1291864067 0.1163515970 0.0977344289 + 0.1314398050 0.1182034016 0.0992026329 + 0.1337047964 0.1200674996 0.1006904021 + 0.1359920055 0.1219405010 0.1021960005 + 0.1382973045 0.1238282993 0.1037138999 + 0.1406147927 0.1257331967 0.1052495018 + 0.1429530978 0.1276489943 0.1068022996 + 0.1453036964 0.1295824945 0.1083668992 + 0.1476657987 0.1315266043 0.1099525020 + 0.1500508040 0.1334788054 0.1115524024 + 0.1524461955 0.1354573965 0.1131626964 + 0.1548593938 0.1374479979 0.1147975996 + 0.1572892964 0.1394518018 0.1164444983 + 0.1597270072 0.1414680034 0.1181050986 + 0.1621783972 0.1434909999 0.1197827980 + 0.1646371931 0.1455283016 0.1214680970 + 0.1671067029 0.1475757957 0.1231684014 + 0.1695881933 0.1496292949 0.1248828992 + 0.1720747054 0.1517004967 0.1266061962 + 0.1745768040 0.1537802070 0.1283615977 + 0.1770841032 0.1558783948 0.1301272064 + 0.1796021014 0.1579937041 0.1319141984 + 0.1821261942 0.1601195037 0.1337158978 + 0.1846542060 0.1622675061 0.1355265975 + 0.1871972978 0.1644203067 0.1373614073 + 0.1897435039 0.1665845066 0.1392045021 + 0.1922962070 0.1687586010 0.1410738975 + 0.1948485970 0.1709450036 0.1429603994 + 0.1974000037 0.1731521040 0.1448680013 + 0.1999529004 0.1753689051 0.1467960030 + 0.2025073022 0.1776115000 0.1487334073 + 0.2050623000 0.1798598021 0.1506955028 + 0.2076175958 0.1821341962 0.1526625007 + 0.2101784945 0.1844155937 0.1546497941 + 0.2127404958 0.1867152005 0.1566448063 + 0.2153038979 0.1890224069 0.1586592048 + 0.2178677022 0.1913391948 0.1606916934 + 0.2204349041 0.1936641932 0.1627465039 + 0.2230034024 0.1959953010 0.1648250967 + 0.2255780995 0.1983426064 0.1669203043 + 0.2281538993 0.2006970942 0.1690406948 + 0.2307337970 0.2030775994 0.1711710989 + 0.2333167940 0.2054647952 0.1733302027 + 0.2359074056 0.2078783959 0.1754958034 + 0.2384988964 0.2102957964 0.1776867956 + 0.2410926074 0.2127331942 0.1798807979 + 0.2436847985 0.2151719928 0.1820942014 + 0.2462743968 0.2176246047 0.1843084991 + 0.2488645017 0.2200776041 0.1865465939 + 0.2514558136 0.2225456983 0.1887851954 + 0.2540476024 0.2250140011 0.1910569072 + 0.2566407025 0.2275063992 0.1933290064 + 0.2592354119 0.2299994975 0.1956388056 + 0.2618348897 0.2325156033 0.1979486048 + 0.2644349933 0.2350329012 0.2002822012 + 0.2670376897 0.2375658005 0.2026163042 + 0.2696410120 0.2401002944 0.2049711943 + 0.2722525895 0.2426490933 0.2073276043 + 0.2748641074 0.2452007979 0.2097111940 + 0.2774798870 0.2477629036 0.2120991051 + 0.2800959945 0.2503288984 0.2145109028 + 0.2827027142 0.2529033124 0.2169295996 + 0.2853069007 0.2554830909 0.2193607986 + 0.2879045010 0.2580693960 0.2217984051 + 0.2904979885 0.2606629133 0.2242448032 + 0.2930887938 0.2632611990 0.2266992033 + 0.2956756949 0.2658706009 0.2291606069 + 0.2982619107 0.2684814036 0.2316346020 + 0.3008450866 0.2711029053 0.2341126055 + 0.3034282923 0.2737244070 0.2366093993 + 0.3060013056 0.2763522863 0.2391061932 + 0.3085739017 0.2789807022 0.2416251004 + 0.3111369014 0.2816146910 0.2441447973 + 0.3136956990 0.2842512131 0.2466782033 + 0.3162499070 0.2868914008 0.2492164969 + 0.3187974095 0.2895368934 0.2517606020 + 0.3213433921 0.2921831906 0.2543109953 + 0.3238751888 0.2948344946 0.2568640113 + 0.3264068961 0.2974858880 0.2594281137 + 0.3289282918 0.3001478016 0.2619921863 + 0.3314462900 0.3028118908 0.2645705938 + 0.3339582086 0.3054809868 0.2671507895 + 0.3364607096 0.3081552088 0.2697336972 + 0.3389630914 0.3108294010 0.2723186910 + 0.3414388001 0.3135038018 0.2749026120 + 0.3439145088 0.3161781132 0.2774826884 + 0.3463768065 0.3188591003 0.2800627947 + 0.3488295972 0.3215422034 0.2826369107 + 0.3512794971 0.3242281079 0.2852098048 + 0.3537116945 0.3269194067 0.2877875865 + 0.3561440110 0.3296107054 0.2903720140 + 0.3585642874 0.3322994113 0.2929564118 + 0.3609788120 0.3349879086 0.2955451012 + 0.3633924127 0.3376730978 0.2981339097 + 0.3658021092 0.3403542042 0.3007180095 + 0.3682116866 0.3430353105 0.3032977879 + 0.3706200123 0.3457179964 0.3058773875 + 0.3730275035 0.3484008908 0.3084514141 + 0.3754344881 0.3510805964 0.3110255003 + 0.3778376877 0.3537563086 0.3135905862 + 0.3802408874 0.3564321101 0.3161478043 + 0.3826422989 0.3591000140 0.3187049925 + 0.3850421906 0.3617669940 0.3212369084 + 0.3874421120 0.3644312024 0.3237684965 + 0.3898290098 0.3670895100 0.3262920082 + 0.3922148049 0.3697476983 0.3288041949 + 0.3945944011 0.3723874092 0.3313164115 + 0.3969587982 0.3750202060 0.3338195086 + 0.3993231058 0.3776504993 0.3363206089 + 0.4016804099 0.3802587092 0.3388189077 + 0.4040333927 0.3828668892 0.3413054943 + 0.4063864946 0.3854654133 0.3437921107 + 0.4087263942 0.3880515099 0.3462659121 + 0.4110653996 0.3906376958 0.3487288952 + 0.4134008884 0.3932111859 0.3511919975 + 0.4157232046 0.3957807124 0.3536264896 + 0.4180454910 0.3983494937 0.3560566008 + 0.4203620851 0.4009037912 0.3584819138 + 0.4226720035 0.4034580886 0.3608815968 + 0.4249818921 0.4060049951 0.3632813096 + 0.4272814989 0.4085350931 0.3656719923 + 0.4295763969 0.4110653102 0.3680498004 + 0.4318712056 0.4135862887 0.3704276979 + 0.4341445863 0.4160991907 0.3727912009 + 0.4364148080 0.4186122119 0.3751470149 + 0.4386841953 0.4211213887 0.3775027096 + 0.4409301877 0.4236291945 0.3798316121 + 0.4431762993 0.4261370003 0.3821555972 + 0.4454182088 0.4286366105 0.3844789863 + 0.4476383030 0.4311352968 0.3867709041 + 0.4498583972 0.4336335957 0.3890627027 + 0.4520725906 0.4361194074 0.3913497925 + 0.4542694986 0.4386051893 0.3936108947 + 0.4564662874 0.4410878122 0.3958719075 + 0.4586519897 0.4435504973 0.3981297910 + 0.4608140886 0.4460130930 0.4003787935 + 0.4629763067 0.4484688938 0.4026277065 + 0.4651244879 0.4508990049 0.4048694074 + 0.4672470987 0.4533290863 0.4070971012 + 0.4693697989 0.4557510018 0.4093249142 + 0.4714809954 0.4581488967 0.4115391076 + 0.4735712111 0.4605467916 0.4137321115 + 0.4756614864 0.4629383981 0.4159250855 + 0.4777419865 0.4653125107 0.4181059897 + 0.4798024893 0.4676867127 0.4202688932 + 0.4818629026 0.4700528085 0.4224317074 + 0.4839158952 0.4723933935 0.4245786965 + 0.4859476089 0.4747340083 0.4266997874 + 0.4879794121 0.4770675004 0.4288209081 + 0.4900068939 0.4793699086 0.4309298098 + 0.4920125008 0.4816722870 0.4330140054 + 0.4940181971 0.4839723110 0.4350981116 + 0.4960232973 0.4862492085 0.4371761978 + 0.4980137050 0.4885261953 0.4392358065 + 0.5000041127 0.4908030927 0.4412953854 + 0.5019946098 0.4930540919 0.4433520138 + 0.5039594769 0.4953044951 0.4453881085 + 0.5059207082 0.4975548983 0.4474242032 + 0.5078818798 0.4997824132 0.4494602084 + 0.5098254085 0.5020049810 0.4514681101 + 0.5117604136 0.5042275190 0.4534752965 + 0.5136954188 0.5064237118 0.4554825127 + 0.5156211853 0.5086045861 0.4574632049 + 0.5175352097 0.5107855201 0.4594367146 + 0.5194491744 0.5129476190 0.4614103138 + 0.5213580728 0.5150821805 0.4633643031 + 0.5232421756 0.5172169209 0.4653035104 + 0.5251262784 0.5193458796 0.4672426879 + 0.5270103812 0.5214424729 0.4691694975 + 0.5288773179 0.5235390067 0.4710688889 + 0.5307416916 0.5256354809 0.4729683995 + 0.5326061249 0.5277007818 0.4748673141 + 0.5344613791 0.5297605991 0.4767382145 + 0.5363087058 0.5318204165 0.4786092043 + 0.5381559134 0.5338671207 0.4804801047 + 0.5400000215 0.5359017849 0.4823342860 + 0.5418263078 0.5379365087 0.4841809869 + 0.5436527133 0.5399680138 0.4860278070 + 0.5454791188 0.5419824719 0.4878644049 + 0.5472977161 0.5439968705 0.4896807075 + 0.5491135716 0.5460113287 0.4914970100 + 0.5509294868 0.5480144024 0.4933133125 + 0.5527412295 0.5500137210 0.4951044023 + 0.5545426011 0.5520130992 0.4968936145 + 0.5563439727 0.5540094972 0.4986828864 + 0.5581454039 0.5559996963 0.5004619956 + 0.5599331260 0.5579897761 0.5022308230 + 0.5617172718 0.5599799752 0.5039995909 + 0.5635015965 0.5619543791 0.5057681203 + 0.5652781725 0.5639256835 0.5075237751 + 0.5670350790 0.5658969283 0.5092794895 + 0.5687919259 0.5678616166 0.5110352039 + 0.5705487728 0.5698119998 0.5127810836 + 0.5722795725 0.5717625022 0.5145177245 + 0.5740000010 0.5737128854 0.5162543058 + 0.5757204890 0.5756404996 0.5179908276 + 0.5774372816 0.5775611997 0.5197011828 + 0.5791262984 0.5794818997 0.5214105248 + 0.5808153152 0.5813961029 0.5231198072 + 0.5825042129 0.5832800865 0.5248202085 + 0.5841786861 0.5851641297 0.5265055895 + 0.5858368874 0.5870479941 0.5281910896 + 0.5874952078 0.5889140964 0.5298765898 + 0.5891535282 0.5907651186 0.5315344930 + 0.5907946825 0.5926162004 0.5331832170 + 0.5924314260 0.5944672227 0.5348318219 + 0.5940682292 0.5962749720 0.5364782810 + 0.5957034230 0.5980765820 0.5381013751 + 0.5973179936 0.5998781919 0.5397245884 + 0.5989326239 0.6016728878 0.5413476825 + 0.6005473137 0.6034333706 0.5429692268 + 0.6021553278 0.6051939130 0.5445882082 + 0.6037486792 0.6069543958 0.5462071896 + 0.6053420901 0.6087045074 0.5478262901 + 0.6069355011 0.6104400754 0.5494415760 + 0.6085183024 0.6121757030 0.5510547757 + 0.6100909710 0.6139113903 0.5526679754 + 0.6116636992 0.6156324148 0.5542811751 + 0.6132364273 0.6173446774 0.5558875799 + 0.6147910953 0.6190569997 0.5574927926 + 0.6163368225 0.6207693219 0.5590980053 + 0.6178824902 0.6224603057 0.5607026815 + 0.6194280982 0.6241456866 0.5622801781 + 0.6209585071 0.6258311272 0.5638577938 + 0.6224846840 0.6275166273 0.5654352903 + 0.6240109205 0.6291700006 0.5670077801 + 0.6255372167 0.6308205724 0.5685514212 + 0.6270471811 0.6324712038 0.5700950027 + 0.6285545826 0.6341215968 0.5716385841 + 0.6300619841 0.6357386112 0.5731750131 + 0.6315693855 0.6373555064 0.5746886730 + 0.6330655813 0.6389724016 0.5762023926 + 0.6345605254 0.6405878067 0.5777161121 + 0.6360552907 0.6421716809 0.5792214870 + 0.6375501156 0.6437556744 0.5807061791 + 0.6390339136 0.6453396082 0.5821908116 + 0.6405161023 0.6469221711 0.5836755037 + 0.6419982910 0.6484758854 0.5851554871 + 0.6434804797 0.6500297189 0.5866240859 + 0.6449518800 0.6515833735 0.5880926847 + 0.6464208961 0.6531370878 0.5895612240 + 0.6478899121 0.6546549797 0.5910267234 + 0.6493589282 0.6561728716 0.5924829245 + 0.6508315802 0.6576908231 0.5939390063 + 0.6523056030 0.6592087150 0.5953950882 + 0.6537796259 0.6606941223 0.5968484879 + 0.6552537084 0.6621763706 0.5982881188 + 0.6567261219 0.6636587977 0.5997276902 + 0.6581972837 0.6651412249 0.6011673808 + 0.6596685052 0.6666092873 0.6026061773 + 0.6611397266 0.6680734754 0.6040263772 + 0.6626036763 0.6695376039 0.6054465771 + 0.6640564799 0.6710017920 0.6068667769 + 0.6655092835 0.6724609733 0.6082869768 + 0.6669620872 0.6739168167 0.6096944213 + 0.6684113145 0.6753727198 0.6110997796 + 0.6698417068 0.6768286228 0.6125053167 + 0.6712719798 0.6782851815 0.6139106750 + 0.6727023125 0.6797429919 0.6153100133 + 0.6741325855 0.6812009215 0.6167058945 + 0.6755437255 0.6826586723 0.6181018949 + 0.6769521236 0.6841160059 0.6194978952 + 0.6783605218 0.6855689287 0.6208894253 + 0.6797689795 0.6870219111 0.6222738028 + 0.6811653972 0.6884748936 0.6236581206 + 0.6825522780 0.6899278760 0.6250424981 + 0.6839390993 0.6913648248 0.6264259219 + 0.6853259206 0.6927976012 0.6277984977 + 0.6867079735 0.6942304969 0.6291711926 + 0.6880686283 0.6956632733 0.6305438280 + 0.6894292831 0.6970888972 0.6319164038 + 0.6907898784 0.6985052824 0.6332756281 + 0.6921504736 0.6999217272 0.6346296072 + 0.6934959888 0.7013381124 0.6359835863 + 0.6948367953 0.7027540803 0.6373376250 + 0.6961774826 0.7041571140 0.6386855245 + 0.6975182295 0.7055600882 0.6400206089 + 0.6988571882 0.7069631219 0.6413556933 + 0.7001917958 0.7083662152 0.6426907778 + 0.7015264034 0.7097566724 0.6440259218 + 0.7028610110 0.7111378908 0.6453583837 + 0.7041956186 0.7125192285 0.6466904879 + 0.7055236101 0.7139005065 0.6480225921 + 0.7068496943 0.7152801752 0.6493546963 + 0.7081757784 0.7166393995 0.6506857872 + 0.7095018029 0.7179986238 0.6520152092 + 0.7108235955 0.7193577886 0.6533446908 + 0.7121292949 0.7207170129 0.6546741128 + 0.7134348750 0.7220675945 0.6560035944 + 0.7147405148 0.7234103084 0.6573339105 + 0.7160462141 0.7247529030 0.6586644053 + 0.7173392177 0.7260956168 0.6599950194 + 0.7186235189 0.7274382114 0.6613255143 + 0.7199078798 0.7287701964 0.6626539230 + 0.7211923003 0.7301012278 0.6639747024 + 0.7224767208 0.7314321995 0.6652954817 + 0.7237458229 0.7327631712 0.6666163206 + 0.7250137925 0.7340887785 0.6679372191 + 0.7262817025 0.7353991866 0.6692438722 + 0.7275496721 0.7367095947 0.6705400944 + 0.7288151979 0.7380200028 0.6718363166 + 0.7300717235 0.7393304110 0.6731324792 + 0.7313283086 0.7406290770 0.6744287014 + 0.7325847745 0.7419185042 0.6757104993 + 0.7338414192 0.7432078719 0.6769903898 + 0.7350922823 0.7444974184 0.6782702208 + 0.7363361716 0.7457867861 0.6795499921 + 0.7375801206 0.7470659018 0.6808269024 + 0.7388241291 0.7483425140 0.6820834875 + 0.7400680184 0.7496191263 0.6833400726 + 0.7413054109 0.7508956790 0.6845965981 + 0.7425388098 0.7521722913 0.6858531833 + 0.7437722087 0.7534267902 0.6871013045 + 0.7450056076 0.7546814084 0.6883319020 + 0.7462391257 0.7559359074 0.6895623803 + 0.7474638224 0.7571905255 0.6907929778 + 0.7486857176 0.7584406137 0.6920235753 + 0.7499076128 0.7596641779 0.6932476759 + 0.7511295080 0.7608876824 0.6944645047 + 0.7523514032 0.7621111870 0.6956813931 + 0.7535635829 0.7633348107 0.6968982816 + 0.7547737956 0.7645512819 0.6981152296 + 0.7559840083 0.7657446265 0.6993272901 + 0.7571942210 0.7669377923 0.7005357742 + 0.7584043741 0.7681310773 0.7017441988 + 0.7596076727 0.7693244219 0.7029526830 + 0.7608097196 0.7705097795 0.7041612267 + 0.7620118260 0.7716736197 0.7053571939 + 0.7632138729 0.7728374004 0.7065454125 + 0.7644159794 0.7740011811 0.7077335715 + 0.7655978799 0.7751650810 0.7089217901 + 0.7667754889 0.7763243914 0.7101098895 + 0.7679529786 0.7774704099 0.7112854123 + 0.7691305876 0.7786164284 0.7124524117 + 0.7703080773 0.7797623873 0.7136192918 + 0.7714644074 0.7809084058 0.7147862911 + 0.7726129889 0.7820516229 0.7159532905 + 0.7737615705 0.7831820846 0.7171167135 + 0.7749102116 0.7843126059 0.7182772160 + 0.7760587931 0.7854431868 0.7194375992 + 0.7771977186 0.7865737081 0.7205979824 + 0.7783300877 0.7877029777 0.7217584252 + 0.7794623971 0.7888091207 0.7229158282 + 0.7805947065 0.7899152040 0.7240688205 + 0.7817270160 0.7910214067 0.7252218723 + 0.7828512192 0.7921274900 0.7263749242 + 0.7839630246 0.7932336926 0.7275279164 + 0.7850748897 0.7943174243 0.7286769748 + 0.7861866951 0.7953978181 0.7298130989 + 0.7872986197 0.7964780927 0.7309491038 + 0.7884055972 0.7975584865 0.7320852280 + 0.7894845009 0.7986388803 0.7332212925 + 0.7905632854 0.7997053862 0.7343574166 + 0.7916421294 0.8007637262 0.7354778051 + 0.7927209735 0.8018221259 0.7365983129 + 0.7937998176 0.8028805256 0.7377187014 + 0.7948549986 0.8039388061 0.7388392091 + 0.7959054112 0.8049921989 0.7399595976 + 0.7969558835 0.8060358167 0.7410690188 + 0.7980064154 0.8070793748 0.7421737909 + 0.7990568280 0.8081229925 0.7432786822 + 0.8000957966 0.8091667295 0.7443835735 + 0.8011217117 0.8102102876 0.7454885244 + 0.8021476269 0.8112317920 0.7465876937 + 0.8031734824 0.8122528791 0.7476770878 + 0.8041993976 0.8132740259 0.7487664819 + 0.8052238822 0.8142951727 0.7498558760 + 0.8062176704 0.8153163195 0.7509453297 + 0.8072115183 0.8163194060 0.7520347238 + 0.8082051873 0.8173090816 0.7531046867 + 0.8091989756 0.8182986975 0.7541738749 + 0.8101927042 0.8192883730 0.7552431822 + 0.8111652136 0.8202779889 0.7563123703 + 0.8121206760 0.8212653995 0.7573816776 + 0.8130761981 0.8222320080 0.7584378123 + 0.8140317798 0.8231986761 0.7594807744 + 0.8149873018 0.8241654038 0.7605237961 + 0.8159424067 0.8251320720 0.7615668178 + 0.8168678880 0.8260987997 0.7626097202 + 0.8177933097 0.8270537257 0.7636526823 + 0.8187186718 0.8279997706 0.7646675706 + 0.8196440935 0.8289458752 0.7656818032 + 0.8205695152 0.8298919797 0.7666959763 + 0.8214858174 0.8308380842 0.7677102089 + 0.8223888278 0.8317841887 0.7687243819 + 0.8232917786 0.8327031732 0.7697293162 + 0.8241947889 0.8336216807 0.7707198262 + 0.8250979185 0.8345401883 0.7717103958 + 0.8260009289 0.8354586959 0.7727010250 + 0.8268916011 0.8363772035 0.7736914754 + 0.8277779818 0.8372876048 0.7746821046 + 0.8286644816 0.8381816149 0.7756630182 + 0.8295509219 0.8390756845 0.7766405940 + 0.8304374218 0.8399696946 0.7776182890 + 0.8313233852 0.8408638239 0.7785959840 + 0.8321877122 0.8417577744 0.7795736194 + 0.8330519795 0.8426352143 0.7805503011 + 0.8339163065 0.8435022831 0.7815071940 + 0.8347805738 0.8443694711 0.7824640870 + 0.8356449008 0.8452367187 0.7834209204 + 0.8365029097 0.8461037874 0.7843778133 + 0.8373395801 0.8469709754 0.7853347063 + 0.8381764293 0.8478137851 0.7862827182 + 0.8390132189 0.8486523032 0.7872080207 + 0.8398498893 0.8494908214 0.7881333232 + 0.8406866789 0.8503292203 0.7890586257 + 0.8415160179 0.8511676788 0.7899839282 + 0.8423328996 0.8520057797 0.7909091711 + 0.8431499004 0.8528186083 0.7918230891 + 0.8439669013 0.8536313176 0.7927232981 + 0.8447839022 0.8544440269 0.7936236262 + 0.8456009030 0.8552566767 0.7945237756 + 0.8464123011 0.8560693860 0.7954239845 + 0.8472172022 0.8568791151 0.7963243127 + 0.8480221033 0.8576657772 0.7972148061 + 0.8488271236 0.8584524989 0.7980977297 + 0.8496320248 0.8592392206 0.7989805937 + 0.8504369259 0.8600258827 0.7998635173 + 0.8512371778 0.8608126044 0.8007463813 + 0.8520324230 0.8615955114 0.8016293049 + 0.8528277278 0.8623574972 0.8024988174 + 0.8536229134 0.8631194234 0.8033595085 + 0.8544182181 0.8638814092 0.8042201996 + 0.8552135229 0.8646433949 0.8050808907 + 0.8559978008 0.8654053211 0.8059417009 + 0.8567681909 0.8661652207 0.8068023920 + 0.8575387001 0.8669108748 0.8076456189 + 0.8583090901 0.8676565289 0.8084762096 + 0.8590794802 0.8684021235 0.8093069196 + 0.8598498702 0.8691477776 0.8101375103 + 0.8606119752 0.8698933721 0.8109682202 + 0.8613570929 0.8706384897 0.8117988706 + 0.8621020913 0.8713684082 0.8126195073 + 0.8628472090 0.8720982075 0.8134300709 + 0.8635922074 0.8728280067 0.8142405748 + 0.8643373251 0.8735578060 0.8150511980 + 0.8650797009 0.8742876053 0.8158618212 + 0.8658074141 0.8750174046 0.8166723847 + 0.8665351272 0.8757197857 0.8174800873 + 0.8672627807 0.8764181733 0.8182824254 + 0.8679904938 0.8771166205 0.8190848231 + 0.8687182069 0.8778150082 0.8198871017 + 0.8694459200 0.8785135150 0.8206894994 + 0.8701534271 0.8792119026 0.8214918971 + 0.8708590865 0.8798918724 0.8222925067 + 0.8715646863 0.8805618882 0.8230811954 + 0.8722702861 0.8812319040 0.8238700032 + 0.8729758859 0.8819019794 0.8246586919 + 0.8736816049 0.8825719953 0.8254473805 + 0.8743742704 0.8832420111 0.8262361884 + 0.8750584722 0.8839061260 0.8270248771 + 0.8757427931 0.8845593929 0.8277987242 + 0.8764269948 0.8852127194 0.8285697103 + 0.8771111965 0.8858659863 0.8293406963 + 0.8777955174 0.8865193725 0.8301116824 + 0.8784751296 0.8871726990 0.8308827281 + 0.8791388869 0.8878260255 0.8316535950 + 0.8798025846 0.8884590268 0.8324189782 + 0.8804662824 0.8890919089 0.8331785202 + 0.8811300993 0.8897247910 0.8339378834 + 0.8817937970 0.8903576732 0.8346974254 + 0.8824576139 0.8909906745 0.8354569077 + 0.8831110001 0.8916236162 0.8362162709 + 0.8837615848 0.8922482729 0.8369745016 + 0.8844121099 0.8928667903 0.8377140760 + 0.8850625753 0.8934854269 0.8384537101 + 0.8857132196 0.8941038847 0.8391932845 + 0.8863636851 0.8947224021 0.8399327993 + 0.8870092034 0.8953409195 0.8406723738 + 0.8876399994 0.8959586024 0.8414120078 + 0.8882709146 0.8965657949 0.8421400785 + 0.8889017105 0.8971729279 0.8428595066 + 0.8895326257 0.8977800012 0.8435789943 + 0.8901634216 0.8983871937 0.8442984223 + 0.8907942772 0.8989943266 0.8450179100 + 0.8914089799 0.8996015191 0.8457372785 + 0.8920158744 0.9001982212 0.8464565277 + 0.8926227093 0.9007843733 0.8471537828 + 0.8932296038 0.9013705850 0.8478512168 + 0.8938364983 0.9019567966 0.8485484719 + 0.8944432735 0.9025430083 0.8492457867 + 0.8950501084 0.9031292200 0.8499431014 + 0.8956450820 0.9037153721 0.8506404161 + 0.8962401152 0.9042838812 0.8513295054 + 0.8968350887 0.9048495293 0.8520048261 + 0.8974301815 0.9054149985 0.8526800871 + 0.8980252147 0.9059805274 0.8533554077 + 0.8986201882 0.9065459967 0.8540307283 + 0.8992115259 0.9071115851 0.8547059894 + 0.8997929096 0.9076750875 0.8553813100 + 0.9003742933 0.9082267284 0.8560460806 + 0.9009556174 0.9087783098 0.8567056060 + 0.9015370011 0.9093298912 0.8573651910 + 0.9021183848 0.9098814726 0.8580247760 + 0.9026998281 0.9104331136 0.8586843014 + 0.9032738805 0.9109846950 0.8593438864 + 0.9038397074 0.9115307927 0.8600034714 + 0.9044054747 0.9120668769 0.8606566191 + 0.9049713016 0.9126030207 0.8613088727 + 0.9055371284 0.9131391048 0.8619611859 + 0.9061030149 0.9136751294 0.8626134992 + 0.9066687822 0.9142112136 0.8632658124 + 0.9072257876 0.9147472978 0.8639181256 + 0.9077767730 0.9152768850 0.8645702004 + 0.9083276987 0.9157999158 0.8652151227 + 0.9088786840 0.9163230062 0.8658599854 + 0.9094297290 0.9168459773 0.8665049076 + 0.9099807143 0.9173690081 0.8671498895 + 0.9105315804 0.9178919792 0.8677948117 + 0.9110671878 0.9184150100 0.8684396744 + 0.9115940928 0.9189338088 0.8690823913 + 0.9121209979 0.9194490910 0.8697077036 + 0.9126479030 0.9199643731 0.8703330159 + 0.9131749272 0.9204797149 0.8709583282 + 0.9137017727 0.9209951162 0.8715835214 + 0.9142286777 0.9215103984 0.8722087741 + 0.9147396088 0.9220256805 0.8728340864 + 0.9152405262 0.9225335121 0.8734576106 + 0.9157413244 0.9230350852 0.8740668297 + 0.9162421227 0.9235367775 0.8746759892 + 0.9167429805 0.9240384102 0.8752852082 + 0.9172437787 0.9245399833 0.8758944273 + 0.9177445769 0.9250416756 0.8765035868 + 0.9182330966 0.9255433083 0.8771128058 + 0.9187096953 0.9260321856 0.8777214885 + 0.9191861749 0.9265064001 0.8783164024 + 0.9196627736 0.9269806743 0.8789111972 + 0.9201393127 0.9274548888 0.8795061111 + 0.9206159115 0.9279291034 0.8801010251 + 0.9210923910 0.9284033775 0.8806958199 + 0.9215654731 0.9288775921 0.8812906742 + 0.9220311046 0.9293445945 0.8818855286 + 0.9224967957 0.9297950268 0.8824583292 + 0.9229624271 0.9302453995 0.8830280900 + 0.9234281182 0.9306957722 0.8835980296 + 0.9238938093 0.9311462045 0.8841677904 + 0.9243593812 0.9315965772 0.8847376108 + 0.9248239994 0.9320470095 0.8853073716 + 0.9252728820 0.9324961901 0.8858771920 + 0.9257218242 0.9329271913 0.8864341974 + 0.9261707067 0.9333581924 0.8869844079 + 0.9266195893 0.9337891936 0.8875344992 + 0.9270685911 0.9342203140 0.8880845904 + 0.9275174737 0.9346513152 0.8886348009 + 0.9279664159 0.9350823164 0.8891848922 + 0.9283986092 0.9355133772 0.8897349834 + 0.9288244843 0.9359328151 0.8902794123 + 0.9292504191 0.9363483191 0.8908125758 + 0.9296762943 0.9367638826 0.8913459182 + 0.9301022291 0.9371793866 0.8918790817 + 0.9305281043 0.9375948906 0.8924124241 + 0.9309539795 0.9380103946 0.8929455876 + 0.9313746095 0.9384258986 0.8934788108 + 0.9317831993 0.9388350248 0.8940120935 + 0.9321917892 0.9392321110 0.8945347071 + 0.9326003790 0.9396290779 0.8950567245 + 0.9330090880 0.9400262237 0.8955786824 + 0.9334176779 0.9404231906 0.8961006999 + 0.9338263273 0.9408202767 0.8966227770 + 0.9342349172 0.9412173033 0.8971447945 + 0.9346324205 0.9416143894 0.8976668119 + 0.9350271821 0.9419872165 0.8981863856 + 0.9354221225 0.9423564076 0.8987035155 + 0.9358168840 0.9427254796 0.8992205262 + 0.9362118244 0.9430946708 0.8997374773 + 0.9366065860 0.9434638023 0.9002544880 + 0.9370014071 0.9438329935 0.9007714987 + 0.9373939037 0.9442021251 0.9012885094 + 0.9377782941 0.9445647001 0.9018055797 + 0.9381626844 0.9449126720 0.9023094177 + 0.9385470748 0.9452607036 0.9028121829 + 0.9389315844 0.9456086159 0.9033150077 + 0.9393159747 0.9459565878 0.9038177729 + 0.9397004247 0.9463046193 0.9043205976 + 0.9400848150 0.9466525912 0.9048234224 + 0.9404577017 0.9470005035 0.9053261876 + 0.9408218265 0.9473389983 0.9058225155 + 0.9411860108 0.9476727843 0.9063037038 + 0.9415500760 0.9480065703 0.9067848921 + 0.9419142008 0.9483404160 0.9072660804 + 0.9422783852 0.9486742020 0.9077473283 + 0.9426425099 0.9490079880 0.9082283974 + 0.9430066943 0.9493417740 0.9087095857 + 0.9433518052 0.9496756196 0.9091907740 + 0.9436928034 0.9499998093 0.9096608162 + 0.9440338016 0.9503235817 0.9101228118 + 0.9443747997 0.9506472945 0.9105848074 + 0.9447157979 0.9509710073 0.9110468030 + 0.9450569153 0.9512947202 0.9115087986 + 0.9453979135 0.9516184926 0.9119707942 + 0.9457389116 0.9519422054 0.9124327898 + 0.9460589290 0.9522647262 0.9128947854 + 0.9463787079 0.9525792003 0.9133455157 + 0.9466984868 0.9528936744 0.9137930870 + 0.9470183253 0.9532083273 0.9142407179 + 0.9473382235 0.9535228014 0.9146882296 + 0.9476580024 0.9538373947 0.9151358008 + 0.9479777813 0.9541519284 0.9155833721 + 0.9482966065 0.9544664025 0.9160308838 + 0.9486029744 0.9547755718 0.9164785147 + 0.9489094019 0.9550681114 0.9169083238 + 0.9492158294 0.9553607106 0.9173362255 + 0.9495223165 0.9556531906 0.9177641273 + 0.9498286843 0.9559456706 0.9181920290 + 0.9501351118 0.9562382102 0.9186198115 + 0.9504414797 0.9565308094 0.9190477133 + 0.9507477880 0.9568232894 0.9194756150 + 0.9510523081 0.9571123123 0.9199035168 + 0.9513568878 0.9573916793 0.9203202724 + 0.9516614079 0.9576711059 0.9207363725 + 0.9519659877 0.9579504728 0.9211524725 + 0.9522706270 0.9582298994 0.9215685129 + 0.9525750875 0.9585093260 0.9219846129 + 0.9528797269 0.9587886930 0.9224005938 + 0.9531841874 0.9590681195 0.9228166938 + 0.9534816146 0.9593474865 0.9232327938 + 0.9537789822 0.9596264958 0.9236450791 + 0.9540762901 0.9599055052 0.9240568280 + 0.9543737173 0.9601845145 0.9244685769 + 0.9546710253 0.9604635239 0.9248803854 + 0.9549683928 0.9607424736 0.9252920747 + 0.9552657008 0.9610214829 0.9257038832 + 0.9555630088 0.9613004923 0.9261155725 + 0.9558495283 0.9615790844 0.9265273809 + 0.9561336040 0.9618505239 0.9269320965 + 0.9564176202 0.9621219039 0.9273340702 + 0.9567016959 0.9623932838 0.9277362227 + 0.9569857121 0.9626647234 0.9281383157 + 0.9572697878 0.9629361033 0.9285402894 + 0.9575538039 0.9632074833 0.9289423823 + 0.9578378797 0.9634789228 0.9293444157 + 0.9581202269 0.9637503028 0.9297465086 + 0.9584012032 0.9640163779 0.9301460981 + 0.9586821795 0.9642812014 0.9305431843 + 0.9589632154 0.9645460248 0.9309402704 + 0.9592441916 0.9648107886 0.9313374758 + 0.9595252872 0.9650756121 0.9317346215 + 0.9598063231 0.9653403759 0.9321317077 + 0.9600872993 0.9656051993 0.9325287938 + 0.9603686929 0.9658700228 0.9329259992 + 0.9606518149 0.9661340714 0.9333214760 + 0.9609349966 0.9663974047 0.9337096810 + 0.9612181187 0.9666607976 0.9340978265 + 0.9615011811 0.9669241905 0.9344859719 + 0.9617843032 0.9671875238 0.9348741770 + 0.9620674253 0.9674509168 0.9352623820 + 0.9623506069 0.9677141905 0.9356505871 + 0.9626337290 0.9679775834 0.9360387921 + 0.9629138708 0.9682406783 0.9364269972 + 0.9631931782 0.9685000777 0.9368088841 + 0.9634724855 0.9687594771 0.9371889234 + 0.9637517929 0.9690188169 0.9375690222 + 0.9640312195 0.9692782164 0.9379490018 + 0.9643105268 0.9695376158 0.9383289814 + 0.9645897746 0.9697968960 0.9387090802 + 0.9648690820 0.9700562954 0.9390891194 + 0.9651448727 0.9703155756 0.9394692183 + 0.9654085040 0.9705702066 0.9398465157 + 0.9656721950 0.9708209038 0.9402164221 + 0.9659358263 0.9710716009 0.9405863881 + 0.9661995173 0.9713222980 0.9409562945 + 0.9664632082 0.9715729952 0.9413263202 + 0.9667267799 0.9718236923 0.9416962266 + 0.9669904709 0.9720743895 0.9420661926 + 0.9672541022 0.9723250866 0.9424360991 + 0.9675124884 0.9725757241 0.9428061247 + 0.9677672982 0.9728142023 0.9431679249 + 0.9680221081 0.9730519056 0.9435257912 + 0.9682769179 0.9732896090 0.9438837171 + 0.9685317278 0.9735273123 0.9442415833 + 0.9687864780 0.9737650752 0.9445996284 + 0.9690412879 0.9740027785 0.9449574947 + 0.9692960978 0.9742404819 0.9453154206 + 0.9695509076 0.9744781852 0.9456732869 + 0.9698060155 0.9747121930 0.9460312128 + 0.9700611830 0.9749348760 0.9463843107 + 0.9703164101 0.9751574993 0.9467371702 + 0.9705715179 0.9753801823 0.9470902085 + 0.9708266854 0.9756028056 0.9474431276 + 0.9710819125 0.9758254886 0.9477961063 + 0.9713370800 0.9760481715 0.9481490254 + 0.9715921879 0.9762707949 0.9485020041 + 0.9718468189 0.9764934778 0.9488549829 + 0.9720928073 0.9767106771 0.9492064714 + 0.9723387957 0.9769214988 0.9495524764 + 0.9725847840 0.9771322012 0.9498984814 + 0.9728307724 0.9773430228 0.9502444863 + 0.9730768204 0.9775537848 0.9505904913 + 0.9733228087 0.9777646065 0.9509364963 + 0.9735687971 0.9779753089 0.9512825012 + 0.9738147855 0.9781860709 0.9516285062 + 0.9740588069 0.9783968925 0.9519745111 + 0.9742915034 0.9786027074 0.9523156285 + 0.9745242000 0.9788047075 0.9526464939 + 0.9747567773 0.9790067077 0.9529774189 + 0.9749894738 0.9792087078 0.9533084035 + 0.9752221704 0.9794108272 0.9536393285 + 0.9754549265 0.9796128273 0.9539703131 + 0.9756876230 0.9798148274 0.9543011785 + 0.9759202003 0.9800168276 0.9546322227 + 0.9761514068 0.9802188277 0.9549630880 + 0.9763734937 0.9804111123 0.9552907944 + 0.9765955806 0.9805964828 0.9556127787 + 0.9768176079 0.9807819128 0.9559348226 + 0.9770396948 0.9809672832 0.9562568069 + 0.9772617817 0.9811527133 0.9565787911 + 0.9774839282 0.9813380837 0.9569007754 + 0.9777060151 0.9815235138 0.9572228193 + 0.9779281020 0.9817088842 0.9575446844 + 0.9781497121 0.9818943143 0.9578667283 + 0.9783595204 0.9820705056 0.9581860900 + 0.9785693884 0.9822378159 0.9584987760 + 0.9787791967 0.9824051261 0.9588115811 + 0.9789891243 0.9825723767 0.9591243267 + 0.9791988730 0.9827396870 0.9594370723 + 0.9794088006 0.9829071164 0.9597498178 + 0.9796186090 0.9830744267 0.9600626230 + 0.9798284769 0.9832416773 0.9603753090 + 0.9800382853 0.9834089875 0.9606881142 + 0.9802370071 0.9835721850 0.9609987736 + 0.9804332852 0.9837269783 0.9612951279 + 0.9806296825 0.9838817716 0.9615914822 + 0.9808260202 0.9840366244 0.9618877769 + 0.9810224175 0.9841914177 0.9621840715 + 0.9812188148 0.9843462110 0.9624804258 + 0.9814150929 0.9845011234 0.9627767205 + 0.9816114902 0.9846559167 0.9630730152 + 0.9818078279 0.9848107100 0.9633693099 + 0.9819964767 0.9849649072 0.9636656046 + 0.9821782112 0.9851077199 0.9639506936 + 0.9823600054 0.9852504730 0.9642338157 + 0.9825417995 0.9853932858 0.9645169973 + 0.9827235937 0.9855362177 0.9648001790 + 0.9829053879 0.9856789708 0.9650833011 + 0.9830871224 0.9858217835 0.9653664827 + 0.9832689166 0.9859645963 0.9656497240 + 0.9834507108 0.9861074090 0.9659329057 + 0.9836298227 0.9862502217 0.9662160277 + 0.9837905765 0.9863815904 0.9664933085 + 0.9839512706 0.9865074158 0.9667642713 + 0.9841120243 0.9866331816 0.9670352936 + 0.9842727780 0.9867590070 0.9673063159 + 0.9844334722 0.9868847728 0.9675772786 + 0.9845942855 0.9870105982 0.9678484201 + 0.9847549796 0.9871364236 0.9681193829 + 0.9849156737 0.9872621894 0.9683904052 + 0.9850764871 0.9873880148 0.9686614275 + 0.9852262139 0.9875094891 0.9689320922 + 0.9853692055 0.9876127243 0.9691805243 + 0.9855121970 0.9877158999 0.9694290161 + 0.9856551886 0.9878190160 0.9696773887 + 0.9857981801 0.9879221916 0.9699258208 + 0.9859411120 0.9880254269 0.9701743126 + 0.9860841036 0.9881286025 0.9704226851 + 0.9862270951 0.9882317781 0.9706711769 + 0.9863700867 0.9883350134 0.9709196091 + 0.9865130186 0.9884381890 0.9711681008 + 0.9866449833 0.9885314703 0.9714096785 + 0.9867771268 0.9886183739 0.9716426134 + 0.9869090915 0.9887052774 0.9718756080 + 0.9870411754 0.9887921810 0.9721086025 + 0.9871733189 0.9888790846 0.9723415971 + 0.9873052835 0.9889659882 0.9725744724 + 0.9874374270 0.9890528917 0.9728075266 + 0.9875693917 0.9891397953 0.9730405211 + 0.9877014756 0.9892266989 0.9732735157 + 0.9878289104 0.9893134832 0.9735063910 + 0.9879450202 0.9893946052 0.9737377763 + 0.9880610704 0.9894750714 0.9739686251 + 0.9881772995 0.9895555973 0.9741994739 + 0.9882934093 0.9896361232 0.9744303226 + 0.9884095192 0.9897165895 0.9746611714 + 0.9885256886 0.9897971153 0.9748920202 + 0.9886417985 0.9898777008 0.9751229286 + 0.9887579083 0.9899582267 0.9753537774 + 0.9888740778 0.9900386930 0.9755846262 + 0.9889798164 0.9901186228 0.9758154750 + 0.9890741706 0.9901947975 0.9760391712 + 0.9891685247 0.9902709723 0.9762626886 + 0.9892628193 0.9903472066 0.9764863253 + 0.9893571138 0.9904233813 0.9767099023 + 0.9894514084 0.9904996157 0.9769334793 + 0.9895458221 0.9905757904 0.9771571159 + 0.9896401167 0.9906520247 0.9773806930 + 0.9897344112 0.9907281995 0.9776043296 + 0.9898287058 0.9908043742 0.9778279066 + 0.9899169207 0.9908795953 0.9780502915 + 0.9900001884 0.9909514785 0.9782621861 + 0.9900833964 0.9910233021 0.9784740806 + 0.9901666045 0.9910951853 0.9786859751 + 0.9902498126 0.9911670089 0.9788979292 + 0.9903330207 0.9912388921 0.9791098237 + 0.9904162884 0.9913107157 0.9793217182 + 0.9904994965 0.9913825989 0.9795336127 + 0.9905827045 0.9914544225 0.9797455072 + 0.9906659126 0.9915261865 0.9799574018 + 0.9907503128 0.9915974736 0.9801694751 + 0.9908356071 0.9916664958 0.9803833961 + 0.9909210205 0.9917355180 0.9805971980 + 0.9910063744 0.9918044806 0.9808109999 + 0.9910917878 0.9918735027 0.9810249209 + 0.9911772013 0.9919425249 0.9812387228 + 0.9912626147 0.9920114875 0.9814525247 + 0.9913480282 0.9920805097 0.9816663861 + 0.9914333820 0.9921495914 0.9818801880 + 0.9915186763 0.9922186136 0.9820939898 + 0.9916055202 0.9922881126 0.9823077917 + 0.9916943908 0.9923617244 0.9825233221 + 0.9917833805 0.9924353957 0.9827387929 + 0.9918723106 0.9925091267 0.9829543233 + 0.9919611812 0.9925826788 0.9831697941 + 0.9920501709 0.9926564097 0.9833853245 + 0.9921391010 0.9927300215 0.9836007953 + 0.9922280908 0.9928036928 0.9838163257 + 0.9923170209 0.9928774238 0.9840317965 + 0.9924060106 0.9929509759 0.9842473269 + 0.9924944043 0.9930247068 0.9844626784 + 0.9925794005 0.9931048155 0.9846640825 + 0.9926643968 0.9931856990 0.9848614931 + 0.9927493930 0.9932667017 0.9850590229 + 0.9928343892 0.9933475852 0.9852564931 + 0.9929193854 0.9934285283 0.9854540229 + 0.9930043817 0.9935094714 0.9856514931 + 0.9930893779 0.9935904145 0.9858490229 + 0.9931743741 0.9936712980 0.9860464931 + 0.9932593703 0.9937523007 0.9862440228 + 0.9933443069 0.9938331842 0.9864414930 + 0.9934259057 0.9939159155 0.9866343737 + 0.9935063124 0.9940000772 0.9868218899 + 0.9935867786 0.9940842986 0.9870094061 + 0.9936671853 0.9941684008 0.9871969223 + 0.9937477112 0.9942526221 0.9873843789 + 0.9938281178 0.9943367243 0.9875720143 + 0.9939085841 0.9944208860 0.9877594709 + 0.9939889908 0.9945051074 0.9879469872 + 0.9940693974 0.9945892096 0.9881345034 + 0.9941499233 0.9946733713 0.9883220792 + 0.9942312241 0.9947574139 0.9885097146 + 0.9943147898 0.9948405027 0.9887015224 + 0.9943982959 0.9949235916 0.9888933897 + 0.9944819212 0.9950066805 0.9890851974 + 0.9945654869 0.9950898290 0.9892770052 + 0.9946489930 0.9951729178 0.9894688129 + 0.9947326183 0.9952560067 0.9896606803 + 0.9948161840 0.9953390956 0.9898524880 + 0.9948996902 0.9954221845 0.9900442958 + 0.9949833155 0.9955052733 0.9902361035 + 0.9950668812 0.9955884218 0.9904279709 + 0.9951515198 0.9956672788 0.9906188846 + 0.9952365160 0.9957426190 0.9908090234 + 0.9953215122 0.9958178997 0.9909991026 + 0.9954065084 0.9958932996 0.9911891818 + 0.9954916239 0.9959685802 0.9913793206 + 0.9955766201 0.9960439205 0.9915693998 + 0.9956616163 0.9961193204 0.9917594790 + 0.9957466722 0.9961946011 0.9919496179 + 0.9958317280 0.9962698817 0.9921396971 + 0.9959167242 0.9963452816 0.9923297763 + 0.9960017800 0.9964206219 0.9925199151 + 0.9960864782 0.9964954853 0.9927043915 + 0.9961711764 0.9965702891 0.9928877950 + 0.9962558746 0.9966450930 0.9930710793 + 0.9963405728 0.9967198968 0.9932544231 + 0.9964252710 0.9967947006 0.9934378266 + 0.9965100288 0.9968695045 0.9936211109 + 0.9965947270 0.9969443083 0.9938043952 + 0.9966794252 0.9970191121 0.9939877987 + 0.9967641234 0.9970939159 0.9941710830 + 0.9968488216 0.9971687198 0.9943544269 + 0.9969335198 0.9972440004 0.9945374727 + 0.9970176220 0.9973254204 0.9947144985 + 0.9971017241 0.9974067807 0.9948914051 + 0.9971858859 0.9974880815 0.9950683117 + 0.9972699881 0.9975695014 0.9952452779 + 0.9973540902 0.9976508021 0.9954221845 + 0.9974381924 0.9977322221 0.9955992103 + 0.9975224137 0.9978135824 0.9957761168 + 0.9976065159 0.9978948832 0.9959530830 + 0.9976906180 0.9979763031 0.9961299896 + 0.9977747798 0.9980576038 0.9963068962 + 0.9978600740 0.9981396198 0.9964826107 + 0.9979493022 0.9982247949 0.9966511130 + 0.9980384707 0.9983099103 0.9968194962 + 0.9981275797 0.9983950257 0.9969878793 + 0.9982168078 0.9984800816 0.9971563220 + 0.9983059764 0.9985653162 0.9973248243 + 0.9983952045 0.9986503720 0.9974932075 + 0.9984843731 0.9987354875 0.9976615906 + 0.9985736012 0.9988206029 0.9978299737 + 0.9986627102 0.9989057779 0.9979984760 + 0.9987518787 0.9989908934 0.9981669188 + 0.9988439083 0.9990757704 0.9983330965 + 0.9989489913 0.9991598725 0.9984846115 + 0.9990540743 0.9992439151 0.9986361265 + 0.9991592169 0.9993278980 0.9987877011 + 0.9992642999 0.9994118810 0.9989392161 + 0.9993693829 0.9994959235 0.9990907907 + 0.9994745255 0.9995799065 0.9992423058 + 0.9995796084 0.9996638894 0.9993938208 + 0.9996846914 0.9997479916 0.9995453954 + 0.9997897744 0.9998319745 0.9996969104 + 0.9998949170 0.9999160171 0.9998484850 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kodachrome_25.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kodachrome_25.spi1d index b450b1b9..e48864b0 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kodachrome_25.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kodachrome_25.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0001187211 0.0001161198 0.0001305900 - 0.0002416660 0.0002335670 0.0002639299 - 0.0003686174 0.0003530100 0.0003996764 - 0.0004986166 0.0004744168 0.0005390237 - 0.0006320932 0.0005989556 0.0006801367 - 0.0007689670 0.0007275027 0.0008226076 - 0.0009098884 0.0008590615 0.0009674262 - 0.0010546240 0.0009952968 0.0011148060 - 0.0012056520 0.0011357040 0.0012659699 - 0.0013610790 0.0012804960 0.0014208819 - 0.0015210240 0.0014290370 0.0015805050 - 0.0016857639 0.0015831300 0.0017423360 - 0.0018555140 0.0017423540 0.0019072110 - 0.0020300581 0.0019068220 0.0020741550 - 0.0022078981 0.0020767010 0.0022443461 - 0.0023916010 0.0022511871 0.0024184019 - 0.0025805370 0.0024301831 0.0025977520 - 0.0027740600 0.0026134001 0.0027815269 - 0.0029722259 0.0028022721 0.0029672419 - 0.0031753390 0.0029984950 0.0031548510 - 0.0033845089 0.0032002409 0.0033454280 - 0.0036013669 0.0034078530 0.0035393371 - 0.0038235600 0.0036215940 0.0037365381 - 0.0040512760 0.0038406281 0.0039370959 - 0.0042870231 0.0040662498 0.0041424572 - 0.0045294450 0.0042975969 0.0043519312 - 0.0047791190 0.0045345472 0.0045640962 - 0.0050357901 0.0047785440 0.0047785449 - 0.0052975160 0.0050289081 0.0049969312 - 0.0055647590 0.0052849171 0.0052185720 - 0.0058401441 0.0055476031 0.0054451409 - 0.0061251819 0.0058162622 0.0056769978 - 0.0064190081 0.0060910610 0.0059134620 - 0.0067208251 0.0063716578 0.0061544981 - 0.0070298752 0.0066589769 0.0064014881 - 0.0073465379 0.0069537512 0.0066520870 - 0.0076715760 0.0072558299 0.0069075110 - 0.0080045266 0.0075645968 0.0071683591 - 0.0083457762 0.0078814235 0.0074348552 - 0.0086941188 0.0082036890 0.0077047022 - 0.0090481956 0.0085330773 0.0079792934 - 0.0094094584 0.0088689746 0.0082590310 - 0.0097797252 0.0092114061 0.0085430183 - 0.0101604396 0.0095595680 0.0088318810 - 0.0105508799 0.0099120708 0.0091247372 - 0.0109501397 0.0102693997 0.0094229234 - 0.0113572897 0.0106335897 0.0097273486 - 0.0117729101 0.0110058896 0.0100365505 - 0.0121964402 0.0113846296 0.0103524001 - 0.0126260100 0.0117704803 0.0106731104 - 0.0130626997 0.0121654198 0.0109990695 - 0.0135076502 0.0125680296 0.0113301203 - 0.0139606902 0.0129768401 0.0116658099 - 0.0144202998 0.0133933499 0.0120067801 - 0.0148866000 0.0138179902 0.0123553397 - 0.0153607102 0.0142508997 0.0127089303 - 0.0158437602 0.0146913100 0.0130658597 - 0.0163364708 0.0151374601 0.0134272100 - 0.0168359205 0.0155895501 0.0137940804 - 0.0173442401 0.0160493609 0.0141672902 - 0.0178608205 0.0165161695 0.0145466598 - 0.0183882900 0.0169901606 0.0149336299 - 0.0189235695 0.0174715705 0.0153278597 - 0.0194664709 0.0179615095 0.0157276094 - 0.0200162604 0.0184623506 0.0161328409 - 0.0205752291 0.0189736001 0.0165440794 - 0.0211429205 0.0194928199 0.0169631094 - 0.0217175093 0.0200189203 0.0173870400 - 0.0222972203 0.0205519404 0.0178171601 - 0.0228821803 0.0210929699 0.0182553697 - 0.0234732702 0.0216418691 0.0187003091 - 0.0240713898 0.0221973900 0.0191515498 - 0.0246774498 0.0227600299 0.0196103808 - 0.0252923407 0.0233312007 0.0200770292 - 0.0259165801 0.0239111893 0.0205497108 - 0.0265481994 0.0245017596 0.0210285001 - 0.0271885395 0.0251027998 0.0215149205 - 0.0278372504 0.0257091504 0.0220096204 - 0.0284956601 0.0263228696 0.0225120597 - 0.0291631296 0.0269445796 0.0230217203 - 0.0298364591 0.0275744200 0.0235392991 - 0.0305197891 0.0282128304 0.0240652803 - 0.0312113408 0.0288609006 0.0246002506 - 0.0319130905 0.0295199603 0.0251446608 - 0.0326239392 0.0301856901 0.0256962199 - 0.0333417691 0.0308586191 0.0262538604 - 0.0340690203 0.0315388404 0.0268181600 - 0.0347989090 0.0322262309 0.0273908004 - 0.0355309397 0.0329242386 0.0279702805 - 0.0362654515 0.0336307585 0.0285563804 - 0.0370042995 0.0343444385 0.0291495994 - 0.0377499983 0.0350654498 0.0297501199 - 0.0385023095 0.0357952416 0.0303586796 - 0.0392612107 0.0365385190 0.0309723206 - 0.0400277786 0.0372893997 0.0315904692 - 0.0408011489 0.0380479097 0.0322141387 - 0.0415779687 0.0388149507 0.0328444093 - 0.0423575304 0.0395870805 0.0334823392 - 0.0431431308 0.0403656401 0.0341259614 - 0.0439345613 0.0411506817 0.0347744413 - 0.0447305702 0.0419436581 0.0354271904 - 0.0455311611 0.0427440405 0.0360897183 - 0.0463386402 0.0435508788 0.0367595218 - 0.0471522398 0.0443644710 0.0374358408 - 0.0479747392 0.0451885909 0.0381190106 - 0.0488112308 0.0460180305 0.0388101190 - 0.0496566407 0.0468529202 0.0395072289 - 0.0505135618 0.0476984307 0.0402105600 - 0.0513870418 0.0485514998 0.0409212708 - 0.0522648282 0.0494111106 0.0416350588 - 0.0531454012 0.0502829701 0.0423520505 - 0.0540309697 0.0511616506 0.0430774391 - 0.0549204089 0.0520450994 0.0438090898 - 0.0558179915 0.0529359803 0.0445459113 - 0.0567257814 0.0538352691 0.0452902392 - 0.0576398782 0.0547406413 0.0460413806 - 0.0585679710 0.0556575507 0.0467979498 - 0.0595032796 0.0565782711 0.0475642905 - 0.0604466088 0.0575027503 0.0483360402 - 0.0614023097 0.0584334396 0.0491129309 - 0.0623621605 0.0593668409 0.0499006808 - 0.0633294508 0.0603048317 0.0506933182 - 0.0643018410 0.0612500012 0.0514932014 - 0.0652783364 0.0621989109 0.0522950403 - 0.0662618205 0.0631573200 0.0530990995 - 0.0672477037 0.0641201437 0.0539113805 - 0.0682398304 0.0650890917 0.0547297895 - 0.0692377687 0.0660655573 0.0555534288 - 0.0702422187 0.0670448914 0.0563857108 - 0.0712599233 0.0680295005 0.0572213605 - 0.0722822472 0.0690181032 0.0580649488 - 0.0733231679 0.0700139031 0.0589125901 - 0.0743673295 0.0710175782 0.0597636290 - 0.0754222274 0.0720258802 0.0606212094 - 0.0764820576 0.0730462894 0.0614807904 - 0.0775483102 0.0740699619 0.0623459816 - 0.0786194429 0.0751056671 0.0632140934 - 0.0796934590 0.0761416107 0.0640869290 - 0.0807745531 0.0771779865 0.0649647191 - 0.0818573833 0.0782172531 0.0658454672 - 0.0829574317 0.0792589337 0.0667318478 - 0.0840600133 0.0803091377 0.0676197112 - 0.0851803720 0.0813626423 0.0685161799 - 0.0863037184 0.0824246481 0.0694149882 - 0.0874389708 0.0834880322 0.0703213289 - 0.0885784179 0.0845618397 0.0712313801 - 0.0897280127 0.0856374800 0.0721469596 - 0.0908830017 0.0867242888 0.0730709806 - 0.0920473710 0.0878123492 0.0740019605 - 0.0932181776 0.0889043733 0.0749416873 - 0.0943980217 0.0899982378 0.0758856833 - 0.0955819637 0.0910960734 0.0768347234 - 0.0967710093 0.0921994597 0.0777852014 - 0.0979645699 0.0933120996 0.0787435994 - 0.0991635621 0.0944301113 0.0797032714 - 0.1003675982 0.0955557898 0.0806734487 - 0.1015781984 0.0966856703 0.0816444829 - 0.1027939990 0.0978211835 0.0826287419 - 0.1040176004 0.0989596322 0.0836134627 - 0.1052478999 0.1001020968 0.0846056491 - 0.1064907983 0.1012483984 0.0855979323 - 0.1077373996 0.1024006978 0.0865952373 - 0.1089940965 0.1035557985 0.0875926092 - 0.1102522984 0.1047163978 0.0886019021 - 0.1115181968 0.1058802009 0.0896116272 - 0.1127844006 0.1070526987 0.0906278268 - 0.1140555963 0.1082267985 0.0916445777 - 0.1153267995 0.1094079986 0.0926688984 - 0.1166016012 0.1105891988 0.0936945230 - 0.1178767011 0.1117718965 0.0947236270 - 0.1191575974 0.1129546016 0.0957538188 - 0.1204406992 0.1141434982 0.0967877209 - 0.1217328012 0.1153329015 0.0978235975 - 0.1230328009 0.1165269986 0.0988596827 - 0.1243387014 0.1177226007 0.0998959318 - 0.1256576031 0.1189235002 0.1009349972 - 0.1269775033 0.1201286018 0.1019790992 - 0.1283123046 0.1213376969 0.1030249000 - 0.1296471059 0.1225541010 0.1040780023 - 0.1309920996 0.1237716004 0.1051312014 - 0.1323397011 0.1249968037 0.1061976030 - 0.1336949021 0.1262219995 0.1072641984 - 0.1350574046 0.1274487972 0.1083405018 - 0.1364232004 0.1286759973 0.1094200984 - 0.1378028989 0.1299048960 0.1105057970 - 0.1391825974 0.1311350018 0.1115979999 - 0.1405749023 0.1323675066 0.1126938015 - 0.1419695020 0.1336061060 0.1138031036 - 0.1433696002 0.1348447949 0.1149123013 - 0.1447755992 0.1360920966 0.1160330996 - 0.1461827010 0.1373400986 0.1171550006 - 0.1476009935 0.1385940015 0.1182890981 - 0.1490193009 0.1398521960 0.1194318980 - 0.1504455954 0.1411112994 0.1205772012 - 0.1518757045 0.1423743069 0.1217304021 - 0.1533088982 0.1436371952 0.1228836030 - 0.1547531039 0.1449093074 0.1240445971 - 0.1561972052 0.1461839974 0.1252069026 - 0.1576558948 0.1474615037 0.1263778955 - 0.1591195017 0.1487443000 0.1275596023 - 0.1605871022 0.1500270963 0.1287415028 - 0.1620666981 0.1513230950 0.1299324036 - 0.1635462940 0.1526210010 0.1311233938 - 0.1650296003 0.1539209038 0.1323246062 - 0.1665142030 0.1552240998 0.1335338056 - 0.1680004001 0.1565274000 0.1347434968 - 0.1694938987 0.1578350067 0.1359594017 - 0.1709873974 0.1591434032 0.1371753067 - 0.1724853069 0.1604537964 0.1383929998 - 0.1739861071 0.1617684960 0.1396120936 - 0.1754869968 0.1630831063 0.1408315003 - 0.1769933999 0.1644009054 0.1420598030 - 0.1784998029 0.1657195985 0.1432881951 - 0.1800076067 0.1670403928 0.1445214003 - 0.1815180033 0.1683724970 0.1457598060 - 0.1830282956 0.1697044969 0.1469981968 - 0.1845442951 0.1710446030 0.1482491940 - 0.1860629022 0.1723918021 0.1495018005 - 0.1875815988 0.1737388968 0.1507571042 - 0.1891085953 0.1750912964 0.1520196050 - 0.1906356961 0.1764443964 0.1532821953 - 0.1921654046 0.1777988970 0.1545532942 - 0.1937022060 0.1791588962 0.1558299065 - 0.1952389926 0.1805189997 0.1571063995 - 0.1967843026 0.1818803996 0.1583925039 - 0.1983377039 0.1832433045 0.1596792042 - 0.1998911053 0.1846061945 0.1609704942 - 0.2014496028 0.1859742999 0.1622772068 - 0.2030100971 0.1873442978 0.1635839045 - 0.2045706064 0.1887143999 0.1648986936 - 0.2061385065 0.1900894940 0.1662220061 - 0.2077071965 0.1914649010 0.1675451994 - 0.2092768997 0.1928412020 0.1688794047 - 0.2108619064 0.1942230016 0.1702177972 - 0.2124470025 0.1956048012 0.1715561002 - 0.2140337974 0.1969860941 0.1729072928 - 0.2156284004 0.1983664036 0.1742596030 - 0.2172230035 0.1997465938 0.1756128073 - 0.2188204974 0.2011310011 0.1769745052 - 0.2204252034 0.2025205046 0.1783363074 - 0.2220298946 0.2039099038 0.1797029972 - 0.2236385942 0.2053045928 0.1810847074 - 0.2252545953 0.2067034990 0.1824664026 - 0.2268707007 0.2081024051 0.1838538945 - 0.2284909934 0.2095097005 0.1852518022 - 0.2301183045 0.2109221071 0.1866496950 - 0.2317456007 0.2123343945 0.1880539954 - 0.2333762050 0.2137437016 0.1894665956 - 0.2350125015 0.2151515931 0.1908791959 - 0.2366486937 0.2165593952 0.1922982931 - 0.2382874936 0.2179677039 0.1937243044 - 0.2399311960 0.2193762958 0.1951504052 - 0.2415750027 0.2207849026 0.1965806931 - 0.2432212979 0.2221989036 0.1980153024 - 0.2448745072 0.2236157060 0.1994500011 - 0.2465275973 0.2250325978 0.2008942068 - 0.2481831014 0.2264544070 0.2023486942 - 0.2498493940 0.2278795987 0.2038031965 - 0.2515155971 0.2293048054 0.2052644044 - 0.2531822920 0.2307375968 0.2067341059 - 0.2548569143 0.2321777046 0.2082037032 - 0.2565315068 0.2336179018 0.2096809000 - 0.2582060993 0.2350665033 0.2111717016 - 0.2598899007 0.2365286052 0.2126623988 - 0.2615750134 0.2379907966 0.2141568065 - 0.2632600963 0.2394558936 0.2156627029 - 0.2649540901 0.2409311980 0.2171684951 - 0.2666524947 0.2424065024 0.2186762989 - 0.2683508992 0.2438821048 0.2202025950 - 0.2700530887 0.2453690022 0.2217289060 - 0.2717601955 0.2468558997 0.2232550979 - 0.2734673023 0.2483427972 0.2247944027 - 0.2751758993 0.2498389930 0.2263347954 - 0.2768929005 0.2513372898 0.2278753072 - 0.2786099911 0.2528356910 0.2294308990 - 0.2803269923 0.2543384135 0.2309931070 - 0.2820504904 0.2558448911 0.2325553000 - 0.2837752998 0.2573513091 0.2341264933 - 0.2854999900 0.2588596940 0.2357089967 - 0.2872273922 0.2603740990 0.2372916043 - 0.2889575064 0.2618885040 0.2388751954 - 0.2906875908 0.2634029090 0.2404648066 - 0.2924185097 0.2649247050 0.2420545071 - 0.2941546142 0.2664470971 0.2436441034 - 0.2958906889 0.2679696083 0.2452432960 - 0.2976269126 0.2694908977 0.2468443960 - 0.2993660867 0.2710112929 0.2484454066 - 0.3011066020 0.2725316882 0.2500450909 - 0.3028470874 0.2740519941 0.2516433001 - 0.3045875132 0.2755717933 0.2532413900 - 0.3063274026 0.2770917118 0.2548404038 - 0.3080672026 0.2786116004 0.2564507127 - 0.3098070920 0.2801361978 0.2580609918 - 0.3115398884 0.2816629112 0.2596713006 - 0.3132705092 0.2831897140 0.2612847984 - 0.3150011003 0.2847178876 0.2629001141 - 0.3167325854 0.2862516940 0.2645153999 - 0.3184674084 0.2877854109 0.2661319077 - 0.3202022016 0.2893190980 0.2677547932 - 0.3219369948 0.2908464968 0.2693777084 - 0.3236666918 0.2923713028 0.2710005045 - 0.3253937066 0.2938959897 0.2726289034 - 0.3271206915 0.2954205871 0.2742598951 - 0.3288466036 0.2969438136 0.2758910060 - 0.3305548131 0.2984670103 0.2775225043 - 0.3322629035 0.2999902070 0.2791568041 - 0.3339709938 0.3015184104 0.2807911932 - 0.3356797099 0.3030506968 0.2824254930 - 0.3373891115 0.3045830131 0.2840622962 - 0.3390985131 0.3061152101 0.2857011855 - 0.3408079147 0.3076440990 0.2873399854 - 0.3425270021 0.3091726899 0.2889789045 - 0.3442499042 0.3107013106 0.2906231880 - 0.3459726870 0.3122313023 0.2922680080 - 0.3476955891 0.3137654960 0.2939127982 - 0.3494232893 0.3152998090 0.2955600917 - 0.3511509895 0.3168340027 0.2972145975 - 0.3528788090 0.3183681071 0.2988691032 - 0.3546074033 0.3199020922 0.3005236089 - 0.3563388884 0.3214359879 0.3021857142 - 0.3580704033 0.3229700029 0.3038538992 - 0.3598020077 0.3245058954 0.3055221140 - 0.3615402877 0.3260425031 0.3071902990 - 0.3632878065 0.3275789917 0.3088661134 - 0.3650352955 0.3291155100 0.3105435967 - 0.3667826951 0.3306686878 0.3122211099 - 0.3685362041 0.3322221935 0.3138988018 - 0.3702940941 0.3337756097 0.3155812025 - 0.3720518947 0.3353295028 0.3172636032 - 0.3738098145 0.3368858099 0.3189460039 - 0.3755826056 0.3384419978 0.3206303120 - 0.3773621023 0.3399981856 0.3223223090 - 0.3791415989 0.3415560126 0.3240141869 - 0.3809210956 0.3431178033 0.3257062137 - 0.3827106953 0.3446795046 0.3274028003 - 0.3845036030 0.3462412059 0.3291088939 - 0.3862965107 0.3478037119 0.3308149874 - 0.3880893886 0.3493674099 0.3325211108 - 0.3898766935 0.3509311080 0.3342320919 - 0.3916623890 0.3524948061 0.3359496891 - 0.3934481144 0.3540599942 0.3376674056 - 0.3952338099 0.3556275070 0.3393850923 - 0.3970318139 0.3571951091 0.3411099911 - 0.3988335133 0.3587625921 0.3428429961 - 0.4006352127 0.3603334129 0.3445760012 - 0.4024369121 0.3619092107 0.3463088870 - 0.4042539001 0.3634850085 0.3480437994 - 0.4060774148 0.3650608063 0.3497805893 - 0.4079008996 0.3666354120 0.3515174985 - 0.4097244143 0.3682079017 0.3532544076 - 0.4115552008 0.3697803915 0.3549940884 - 0.4133909941 0.3713529110 0.3567374945 - 0.4152266979 0.3729279935 0.3584809899 - 0.4170624912 0.3745107949 0.3602243960 - 0.4189001918 0.3760935962 0.3619695008 - 0.4207401872 0.3776763082 0.3637174964 - 0.4225800931 0.3792597950 0.3654656112 - 0.4244200885 0.3808481097 0.3672136962 - 0.4262587130 0.3824365139 0.3689666986 - 0.4280926883 0.3840247989 0.3707352877 - 0.4299268126 0.3856132030 0.3725039065 - 0.4317609072 0.3872022033 0.3742724061 - 0.4335947931 0.3887912929 0.3760411143 - 0.4354149997 0.3903804123 0.3778105080 - 0.4372350872 0.3919695020 0.3795798123 - 0.4390552044 0.3935660124 0.3813492060 - 0.4408754110 0.3951654136 0.3831185997 - 0.4426893890 0.3967649043 0.3848997056 - 0.4445011020 0.3983643949 0.3866828084 - 0.4463126957 0.3999699950 0.3884659111 - 0.4481244087 0.4015834928 0.3902488947 - 0.4499397874 0.4031969011 0.3920351863 - 0.4517613053 0.4048103988 0.3938237131 - 0.4535827041 0.4064252973 0.3956122100 - 0.4554041922 0.4080508053 0.3974005878 - 0.4572255909 0.4096763134 0.3991900980 - 0.4590497911 0.4113017917 0.4009819925 - 0.4608739018 0.4129272997 0.4027737975 - 0.4626981020 0.4145581126 0.4045656919 - 0.4645222127 0.4161905944 0.4063574970 - 0.4663369954 0.4178231061 0.4081600904 - 0.4681450129 0.4194557071 0.4099633992 - 0.4699530005 0.4210889935 0.4117667079 - 0.4717608988 0.4227238894 0.4135699868 - 0.4735681117 0.4243587852 0.4153786004 - 0.4753684103 0.4259937108 0.4171918035 - 0.4771685898 0.4276286066 0.4190050066 - 0.4789687991 0.4292654097 0.4208182096 - 0.4807690084 0.4309025109 0.4226312041 - 0.4825682044 0.4325394928 0.4244414866 - 0.4843665957 0.4341765046 0.4262517989 - 0.4861649871 0.4358190894 0.4280619919 - 0.4879634082 0.4374710917 0.4298723042 - 0.4897617102 0.4391230941 0.4316802025 - 0.4915564060 0.4407750964 0.4334864914 - 0.4933511019 0.4424270988 0.4352926910 - 0.4951457083 0.4440836906 0.4370990098 - 0.4969404042 0.4457412958 0.4389046133 - 0.4987323880 0.4473989010 0.4406982064 - 0.5005198121 0.4490565062 0.4424918890 - 0.5023072958 0.4507141113 0.4442855120 - 0.5040947795 0.4523720145 0.4460791945 - 0.5058822036 0.4540297985 0.4478673041 - 0.5076612830 0.4556876123 0.4496487081 - 0.5094367862 0.4573453963 0.4514302015 - 0.5112122893 0.4589950144 0.4532116055 - 0.5129877925 0.4606379867 0.4549930990 - 0.5147632957 0.4622808993 0.4567654133 - 0.5165187120 0.4639238119 0.4585354924 - 0.5182737112 0.4655668139 0.4603056014 - 0.5200287104 0.4672066867 0.4620757997 - 0.5217838287 0.4688459933 0.4638450146 - 0.5235412717 0.4704853892 0.4656072855 - 0.5253081918 0.4721247852 0.4673697054 - 0.5270751119 0.4737643003 0.4691320062 - 0.5288419724 0.4754061103 0.4708943069 - 0.5306090117 0.4770478904 0.4726507962 - 0.5323774219 0.4786897004 0.4743973911 - 0.5341482759 0.4803315997 0.4761438966 - 0.5359191895 0.4819717109 0.4778904021 - 0.5376901031 0.4836075008 0.4796369076 - 0.5394610167 0.4852432907 0.4813722968 - 0.5412335992 0.4868789911 0.4830994010 - 0.5430079103 0.4885148108 0.4848263860 - 0.5447822213 0.4901467860 0.4865534008 - 0.5465564728 0.4917733967 0.4882805049 - 0.5483307838 0.4934000075 0.4900054038 - 0.5500996709 0.4950265884 0.4917294979 - 0.5518643856 0.4966531992 0.4934537113 - 0.5536291003 0.4982692897 0.4951778054 - 0.5553938746 0.4998747110 0.4969018996 - 0.5571585894 0.5014801025 0.4986188114 - 0.5589029789 0.5030853748 0.5003340244 - 0.5606324077 0.5046908259 0.5020492077 - 0.5623617768 0.5062916875 0.5037645102 - 0.5640913248 0.5078886151 0.5054796934 - 0.5658206940 0.5094854236 0.5071663260 - 0.5675324798 0.5110822916 0.5088484883 - 0.5692291260 0.5126792192 0.5105305910 - 0.5709257126 0.5142681003 0.5122128129 - 0.5726222992 0.5158489943 0.5138949156 - 0.5743188858 0.5174300075 0.5155733228 - 0.5760101080 0.5190109015 0.5172510743 - 0.5776947141 0.5205919147 0.5189288259 - 0.5793792009 0.5221624970 0.5206065774 - 0.5810636878 0.5237188935 0.5222843289 - 0.5827482939 0.5252754092 0.5239499807 - 0.5844303966 0.5268318057 0.5256124735 - 0.5861067772 0.5283882022 0.5272750854 - 0.5877830982 0.5299438238 0.5289375782 - 0.5894594789 0.5314972997 0.5306000710 - 0.5911359191 0.5330507159 0.5322602987 - 0.5928109884 0.5346041918 0.5339192748 - 0.5944728255 0.5361576080 0.5355783105 - 0.5961346030 0.5377094746 0.5372374058 - 0.5977964997 0.5392475128 0.5388963819 - 0.5994582772 0.5407854915 0.5405499935 - 0.6011201143 0.5423235297 0.5421981215 - 0.6027656794 0.5438615084 0.5438463092 - 0.6044077277 0.5453994274 0.5454944968 - 0.6060497165 0.5469250083 0.5471426845 - 0.6076918244 0.5484485030 0.5487864017 - 0.6093338132 0.5499718785 0.5504183173 - 0.6109706163 0.5514953732 0.5520501733 - 0.6126019955 0.5530188084 0.5536820292 - 0.6142334938 0.5545319915 0.5553138852 - 0.6158648729 0.5560368896 0.5569456816 - 0.6174963713 0.5575417876 0.5585566759 - 0.6191260219 0.5590466857 0.5601673126 - 0.6207424998 0.5605515838 0.5617777705 - 0.6223589182 0.5620545745 0.5633884072 - 0.6239753962 0.5635496974 0.5649989843 - 0.6255918741 0.5650447011 0.5666049719 - 0.6272084117 0.5665397048 0.5682082772 - 0.6288107038 0.5680348277 0.5698117018 - 0.6304064989 0.5695297718 0.5714150071 - 0.6320024133 0.5710126162 0.5730183125 - 0.6335982084 0.5724920034 0.5746158957 - 0.6351940036 0.5739712715 0.5761947036 - 0.6367834806 0.5754505992 0.5777735114 - 0.6383458972 0.5769299269 0.5793523788 - 0.6399083138 0.5784004927 0.5809311867 - 0.6414706707 0.5798507929 0.5825099945 - 0.6430330873 0.5813010931 0.5840647817 - 0.6445955038 0.5827513933 0.5856127143 - 0.6461372972 0.5842016935 0.5871605873 - 0.6476672888 0.5856518745 0.5887084007 - 0.6491973996 0.5870863795 0.5902562737 - 0.6507273912 0.5885158777 0.5917943120 - 0.6522575021 0.5899453759 0.5933035016 - 0.6537867785 0.5913748741 0.5948126912 - 0.6552811861 0.5928043723 0.5963220000 - 0.6567755938 0.5942304730 0.5978311896 - 0.6582700014 0.5956394076 0.5993403792 - 0.6597645283 0.5970482826 0.6008219123 - 0.6612588763 0.5984572768 0.6022902131 - 0.6627463102 0.5998662114 0.6037583947 - 0.6642202735 0.6012750864 0.6052265763 - 0.6656942964 0.6026799083 0.6066948771 - 0.6671683192 0.6040807962 0.6081615090 - 0.6686422825 0.6054816842 0.6095842719 - 0.6701163054 0.6068825722 0.6110069752 - 0.6715676188 0.6082835793 0.6124296784 - 0.6730042100 0.6096845269 0.6138523817 - 0.6744406819 0.6110807061 0.6152750850 - 0.6758772135 0.6124755740 0.6166884899 - 0.6773136854 0.6138706207 0.6180831194 - 0.6787502170 0.6152656078 0.6194776297 - 0.6801577210 0.6166605949 0.6208721995 - 0.6815580130 0.6180554032 0.6222668290 - 0.6829584241 0.6194347143 0.6236612797 - 0.6843587160 0.6208140254 0.6250466704 - 0.6857591271 0.6221933961 0.6264250875 - 0.6871594191 0.6235727072 0.6278035045 - 0.6885198951 0.6249520779 0.6291819215 - 0.6898779273 0.6263288856 0.6305602789 - 0.6912357807 0.6276934147 0.6319386959 - 0.6925938129 0.6290578842 0.6332967877 - 0.6939517856 0.6304224133 0.6346477866 - 0.6953091025 0.6317868829 0.6359987855 - 0.6966227889 0.6331512928 0.6373497844 - 0.6979364753 0.6345124841 0.6387007833 - 0.6992502213 0.6358644962 0.6400517821 - 0.7005639076 0.6372163892 0.6413751841 - 0.7018774748 0.6385684013 0.6426938176 - 0.7031903863 0.6399202943 0.6440123916 - 0.7044675946 0.6412723064 0.6453310251 - 0.7057446837 0.6426153183 0.6466495991 - 0.7070218921 0.6439368129 0.6479681730 - 0.7082989812 0.6452583075 0.6492698789 - 0.7095761895 0.6465796828 0.6505696774 - 0.7108533978 0.6479011774 0.6518694162 - 0.7121080160 0.6492226720 0.6531692147 - 0.7133617997 0.6505348086 0.6544690132 - 0.7146157026 0.6518198252 0.6557686925 - 0.7158694863 0.6531049013 0.6570504904 - 0.7171233296 0.6543899179 0.6583297849 - 0.7183771133 0.6556748748 0.6596090198 - 0.7195919752 0.6569600105 0.6608883142 - 0.7207996845 0.6582419872 0.6621676087 - 0.7220073938 0.6595075130 0.6634469032 - 0.7232151031 0.6607730985 0.6647216082 - 0.7244228721 0.6620386243 0.6659951806 - 0.7256305814 0.6633040905 0.6672688723 - 0.7268121243 0.6645696759 0.6685425043 - 0.7279800177 0.6658352017 0.6698160768 - 0.7291477919 0.6670749784 0.6710897088 - 0.7303156257 0.6683145165 0.6723518968 - 0.7314835191 0.6695539951 0.6736078858 - 0.7326512933 0.6707934737 0.6748638749 - 0.7338057160 0.6720330119 0.6761198044 - 0.7349399924 0.6732723713 0.6773757935 - 0.7360742092 0.6744793057 0.6786317229 - 0.7372084260 0.6756755710 0.6798825264 - 0.7383427024 0.6768718958 0.6811262965 - 0.7394769192 0.6780682206 0.6823701859 - 0.7406092882 0.6792644858 0.6836140156 - 0.7417197227 0.6804608107 0.6848577857 - 0.7428302169 0.6816480756 0.6861016750 - 0.7439405918 0.6828244925 0.6873413920 - 0.7450510263 0.6840010285 0.6885573864 - 0.7461614013 0.6851773858 0.6897733212 - 0.7472717762 0.6863538027 0.6909891963 - 0.7483662963 0.6875302196 0.6922051907 - 0.7494534254 0.6887052059 0.6934211254 - 0.7505404949 0.6898639202 0.6946370006 - 0.7516276240 0.6910225153 0.6958392859 - 0.7527148128 0.6921812296 0.6970379949 - 0.7538018823 0.6933398843 0.6982367039 - 0.7548860908 0.6944984794 0.6994354129 - 0.7559602857 0.6956571937 0.7006341219 - 0.7570344210 0.6968036890 0.7018327713 - 0.7581086159 0.6979433894 0.7030227184 - 0.7591826916 0.6990832090 0.7041977048 - 0.7602568865 0.7002230287 0.7053728104 - 0.7613310218 0.7013627291 0.7065479159 - 0.7623906136 0.7025024891 0.7077229023 - 0.7634438276 0.7036383748 0.7088980079 - 0.7644969225 0.7047494054 0.7100731134 - 0.7655500174 0.7058604956 0.7112265229 - 0.7666031122 0.7069715858 0.7123770118 - 0.7676562071 0.7080826759 0.7135276198 - 0.7687063813 0.7091938257 0.7146781087 - 0.7697265148 0.7103049159 0.7158285975 - 0.7707465887 0.7114003897 0.7169790864 - 0.7717667222 0.7124834061 0.7181215286 - 0.7727867961 0.7135664225 0.7192468047 - 0.7738069296 0.7146493793 0.7203720808 - 0.7748268843 0.7157322764 0.7214972973 - 0.7758402824 0.7168152928 0.7226225734 - 0.7768442035 0.7178983092 0.7237479091 - 0.7778481245 0.7189552784 0.7248731256 - 0.7788519859 0.7200095057 0.7259737253 - 0.7798559070 0.7210636735 0.7270630002 - 0.7808597088 0.7221180201 0.7281523943 - 0.7818635702 0.7231721878 0.7292417288 - 0.7828533053 0.7242264152 0.7303311229 - 0.7838358283 0.7252786756 0.7314203978 - 0.7848182917 0.7263221145 0.7325097919 - 0.7858008742 0.7273653746 0.7335510850 - 0.7867833972 0.7284088135 0.7345916033 - 0.7877659202 0.7294521928 0.7356320024 - 0.7887483835 0.7304955721 0.7366725206 - 0.7897030711 0.7315390110 0.7377129793 - 0.7906526923 0.7325803041 0.7387534976 - 0.7916023135 0.7336180806 0.7397875190 - 0.7925518751 0.7346559763 0.7407963872 - 0.7935014963 0.7356938720 0.7418051958 - 0.7944511175 0.7367317080 0.7428141236 - 0.7954006791 0.7377696037 0.7438229918 - 0.7963156104 0.7388074994 0.7448318005 - 0.7972288132 0.7398343086 0.7458407283 - 0.7981420159 0.7408506870 0.7468411922 - 0.7990550995 0.7418671250 0.7478262782 - 0.7999683022 0.7428835034 0.7488114238 - 0.8008813858 0.7438998818 0.7497966290 - 0.8017945886 0.7449163795 0.7507817149 - 0.8026857972 0.7459328175 0.7517668009 - 0.8035765886 0.7469387054 0.7527520061 - 0.8044673800 0.7479367852 0.7537305951 - 0.8053581119 0.7489348054 0.7547006011 - 0.8062489033 0.7499328852 0.7556706071 - 0.8071396947 0.7509309053 0.7566406131 - 0.8080304861 0.7519289255 0.7576106191 - 0.8089057207 0.7529270053 0.7585806251 - 0.8097798228 0.7539178133 0.7595505714 - 0.8106539249 0.7549026012 0.7605128884 - 0.8115280271 0.7558873892 0.7614648938 - 0.8124021888 0.7568721771 0.7624170184 - 0.8132762909 0.7578570843 0.7633690834 - 0.8141503930 0.7588418722 0.7643210888 - 0.8150117993 0.7598267198 0.7652732134 - 0.8158702850 0.7607942820 0.7662252188 - 0.8167288899 0.7617409825 0.7671691775 - 0.8175873756 0.7626876831 0.7680981755 - 0.8184459805 0.7636343837 0.7690271735 - 0.8193045259 0.7645810843 0.7699561715 - 0.8201630116 0.7655277848 0.7708852887 - 0.8210126758 0.7664744854 0.7718142867 - 0.8218567967 0.7674121261 0.7727432847 - 0.8227009177 0.7683262229 0.7736694813 - 0.8235449791 0.7692403197 0.7745841146 - 0.8243891001 0.7701544166 0.7754986882 - 0.8252332211 0.7710685134 0.7764133215 - 0.8260772824 0.7719824910 0.7773278952 - 0.8269128799 0.7728965878 0.7782425284 - 0.8277320266 0.7738102078 0.7791571021 - 0.8285509944 0.7747033834 0.7800716758 - 0.8293700814 0.7755966783 0.7809833884 - 0.8301892281 0.7764899135 0.7818949223 - 0.8310083151 0.7773832083 0.7828063965 - 0.8318274021 0.7782763839 0.7837178707 - 0.8326464295 0.7791696787 0.7846295238 - 0.8334459066 0.7800629139 0.7855409980 - 0.8342450261 0.7809472084 0.7864524722 - 0.8350440860 0.7818276286 0.7873616815 - 0.8358432055 0.7827079892 0.7882695794 - 0.8366423845 0.7835882902 0.7891774774 - 0.8374415040 0.7844687104 0.7900853753 - 0.8382406235 0.7853490710 0.7909933925 - 0.8390303850 0.7862293720 0.7919012904 - 0.8398123980 0.7871022224 0.7928091884 - 0.8405944705 0.7879561782 0.7937148809 - 0.8413766026 0.7888103127 0.7946143746 - 0.8421586156 0.7896643281 0.7955139279 - 0.8429406881 0.7905182838 0.7964133024 - 0.8437227011 0.7913722992 0.7973127961 - 0.8445037007 0.7922263741 0.7982122898 - 0.8452532887 0.7930803895 0.7991117835 - 0.8460028768 0.7939234972 0.8000112772 - 0.8467524052 0.7947638035 0.8008962870 - 0.8475019932 0.7956042290 0.8017777801 - 0.8482515812 0.7964444757 0.8026592135 - 0.8490012288 0.7972847819 0.8035405874 - 0.8497508168 0.7981252074 0.8044220209 - 0.8504915833 0.7989655137 0.8053035140 - 0.8512210250 0.7998054028 0.8061848879 - 0.8519504070 0.8006436229 0.8070607185 - 0.8526797891 0.8014817834 0.8079198003 - 0.8534091711 0.8023201227 0.8087788820 - 0.8541386127 0.8031582832 0.8096380234 - 0.8548679948 0.8039965034 0.8104969859 - 0.8555973768 0.8048347831 0.8113561273 - 0.8563107252 0.8056730032 0.8122152090 - 0.8570188284 0.8065040112 0.8130742908 - 0.8577268720 0.8073294759 0.8139200807 - 0.8584349751 0.8081550002 0.8147575855 - 0.8591430783 0.8089805841 0.8155952096 - 0.8598511815 0.8098061085 0.8164327741 - 0.8605592251 0.8106316924 0.8172702789 - 0.8612673283 0.8114572167 0.8181079030 - 0.8619577289 0.8122826815 0.8189455271 - 0.8626478910 0.8130952716 0.8197829723 - 0.8633381128 0.8139054179 0.8205946088 - 0.8640282750 0.8147153854 0.8214033842 - 0.8647184968 0.8155254722 0.8222122192 - 0.8654087186 0.8163354993 0.8230209947 - 0.8660988808 0.8171455860 0.8238298297 - 0.8667852283 0.8179556131 0.8246386051 - 0.8674482107 0.8187654018 0.8254473805 - 0.8681111932 0.8195549250 0.8262543082 - 0.8687742949 0.8203443289 0.8270450234 - 0.8694372773 0.8211337924 0.8278357983 - 0.8701003194 0.8219231963 0.8286265135 - 0.8707633018 0.8227127194 0.8294172883 - 0.8714262843 0.8235021830 0.8302080035 - 0.8720872998 0.8242915869 0.8309987783 - 0.8727403283 0.8250802159 0.8317894936 - 0.8733932972 0.8258609772 0.8325753212 - 0.8740463257 0.8266419172 0.8333438039 - 0.8746992946 0.8274227977 0.8341122866 - 0.8753523231 0.8282036781 0.8348808289 - 0.8760052919 0.8289846182 0.8356493115 - 0.8766583204 0.8297654986 0.8364177942 - 0.8773071170 0.8305463791 0.8371862173 - 0.8779379725 0.8313267827 0.8379547000 - 0.8785688877 0.8321037292 0.8387172222 - 0.8791998029 0.8328806162 0.8394616246 - 0.8798307180 0.8336575031 0.8402060866 - 0.8804615736 0.8344345093 0.8409506083 - 0.8810924888 0.8352113962 0.8416950703 - 0.8817234039 0.8359882832 0.8424395919 - 0.8823524117 0.8367652297 0.8431841135 - 0.8829597831 0.8375415206 0.8439285755 - 0.8835672736 0.8383017182 0.8446694016 - 0.8841748238 0.8390619159 0.8453953266 - 0.8847821951 0.8398221135 0.8461210728 - 0.8853896856 0.8405823112 0.8468469977 - 0.8859971762 0.8413426280 0.8475728035 - 0.8866046071 0.8421028256 0.8482987285 - 0.8872120976 0.8428630233 0.8490244746 - 0.8877959847 0.8436232209 0.8497503996 - 0.8883770108 0.8443626761 0.8504748940 - 0.8889580965 0.8450992703 0.8511757255 - 0.8895391226 0.8458358049 0.8518766165 - 0.8901200891 0.8465722799 0.8525773883 - 0.8907011151 0.8473088145 0.8532782793 - 0.8912820816 0.8480454087 0.8539791107 - 0.8918631077 0.8487818837 0.8546800017 - 0.8924347758 0.8495184183 0.8553807735 - 0.8930004239 0.8502457142 0.8560817242 - 0.8935660124 0.8509672284 0.8567734957 - 0.8941316009 0.8516888022 0.8574634194 - 0.8946971893 0.8524103165 0.8581532240 - 0.8952628970 0.8531318903 0.8588430882 - 0.8958284855 0.8538534045 0.8595330119 - 0.8963940740 0.8545749784 0.8602228761 - 0.8969566822 0.8552964926 0.8609126806 - 0.8975095749 0.8560137153 0.8616026044 - 0.8980625272 0.8567193747 0.8622909784 - 0.8986153007 0.8574250937 0.8629779816 - 0.8991681933 0.8581306934 0.8636649847 - 0.8997210264 0.8588364124 0.8643519878 - 0.9002739191 0.8595420718 0.8650388718 - 0.9008268118 0.8602476716 0.8657258749 - 0.9013795853 0.8609533906 0.8664128780 - 0.9019126892 0.8616591096 0.8670998812 - 0.9024397731 0.8623467088 0.8677850962 - 0.9029669166 0.8630310297 0.8684529066 - 0.9034940004 0.8637151718 0.8691207170 - 0.9040210843 0.8643994927 0.8697885871 - 0.9045482278 0.8650836945 0.8704563975 - 0.9050753117 0.8657680154 0.8711242080 - 0.9056023955 0.8664522171 0.8717920184 - 0.9061269760 0.8671364784 0.8724598885 - 0.9066426754 0.8678159714 0.8731276989 - 0.9071583152 0.8684850931 0.8737882972 - 0.9076738954 0.8691542149 0.8744438291 - 0.9081895947 0.8698232174 0.8750993013 - 0.9087051749 0.8704922795 0.8757547736 - 0.9092208147 0.8711614013 0.8764103055 - 0.9097363949 0.8718305230 0.8770657778 - 0.9102520943 0.8724995852 0.8777213097 - 0.9107689857 0.8731687069 0.8783767223 - 0.9112870097 0.8738380075 0.8790321946 - 0.9118049741 0.8745074868 0.8796920180 - 0.9123229980 0.8751769066 0.8803519011 - 0.9128410220 0.8758463264 0.8810117245 - 0.9133591056 0.8765158057 0.8816714883 - 0.9138771296 0.8771852255 0.8823313117 - 0.9143950939 0.8778547049 0.8829911947 - 0.9149131179 0.8785241246 0.8836510181 - 0.9154273868 0.8791936040 0.8843107820 - 0.9159408808 0.8798611164 0.8849681020 - 0.9164543748 0.8805286884 0.8856195807 - 0.9169679284 0.8811962008 0.8862711191 - 0.9174814224 0.8818637133 0.8869225979 - 0.9179949164 0.8825312853 0.8875740767 - 0.9185084105 0.8831987977 0.8882256746 - 0.9190219045 0.8838664293 0.8888772130 - 0.9195353985 0.8845338821 0.8895286918 - 0.9200410843 0.8851994872 0.8901801705 - 0.9205467105 0.8858562112 0.8908231258 - 0.9210522175 0.8865129948 0.8914582133 - 0.9215577841 0.8871697187 0.8920934200 - 0.9220634103 0.8878263831 0.8927285075 - 0.9225689769 0.8884831071 0.8933635950 - 0.9230744839 0.8891397715 0.8939986825 - 0.9235801101 0.8897964954 0.8946338892 - 0.9240844846 0.8904532194 0.8952689767 - 0.9245653152 0.8911066055 0.8959041238 - 0.9250460863 0.8917511106 0.8965278268 - 0.9255269170 0.8923956156 0.8971452713 - 0.9260076880 0.8930401206 0.8977627754 - 0.9264885187 0.8936846256 0.8983802795 - 0.9269692898 0.8943290710 0.8989977241 - 0.9274501204 0.8949735761 0.8996152282 - 0.9279310107 0.8956180811 0.9002326727 - 0.9284117222 0.8962625861 0.9008501768 - 0.9288892150 0.8969045281 0.9014676809 - 0.9293668270 0.8975396156 0.9020715952 - 0.9298443198 0.8981747031 0.9026690125 - 0.9303218126 0.8988097906 0.9032663703 - 0.9307994246 0.8994448185 0.9038637877 - 0.9312769175 0.9000799060 0.9044612050 - 0.9317544103 0.9007149935 0.9050586224 - 0.9322319031 0.9013500810 0.9056559801 - 0.9327095151 0.9019851089 0.9062533975 - 0.9331722856 0.9026165009 0.9068508148 - 0.9336332083 0.9032307267 0.9074404240 - 0.9340941906 0.9038448930 0.9080247879 - 0.9345551133 0.9044591188 0.9086092114 - 0.9350160956 0.9050732851 0.9091936946 - 0.9354770780 0.9056875110 0.9097781181 - 0.9359380007 0.9063016772 0.9103624821 - 0.9363989830 0.9069159031 0.9109470248 - 0.9368600249 0.9075301290 0.9115313888 - 0.9373095036 0.9081442952 0.9121158719 - 0.9377532005 0.9087433815 0.9126855135 - 0.9381968975 0.9093422294 0.9132357836 - 0.9386405945 0.9099410176 0.9137859941 - 0.9390842915 0.9105398059 0.9143362045 - 0.9395279884 0.9111385942 0.9148864746 - 0.9399716854 0.9117373824 0.9154366851 - 0.9404153824 0.9123361707 0.9159868956 - 0.9408590794 0.9129350185 0.9165371060 - 0.9412962198 0.9135338068 0.9170873761 - 0.9417200089 0.9141229987 0.9176347852 - 0.9421437979 0.9147080183 0.9181675911 - 0.9425675869 0.9152929187 0.9187005162 - 0.9429913759 0.9158778191 0.9192333221 - 0.9434152246 0.9164627194 0.9197661877 - 0.9438390136 0.9170476198 0.9202991128 - 0.9442628026 0.9176325202 0.9208319187 - 0.9446865916 0.9182174802 0.9213647842 - 0.9451104999 0.9188023806 0.9218975902 - 0.9455274940 0.9193829298 0.9224305153 - 0.9459438920 0.9199525118 0.9229544997 - 0.9463601708 0.9205220938 0.9234762192 - 0.9467766285 0.9210916758 0.9239978790 - 0.9471930265 0.9216613173 0.9245195985 - 0.9476093054 0.9222308993 0.9250413775 - 0.9480257034 0.9228004813 0.9255630970 - 0.9484419823 0.9233701229 0.9260848165 - 0.9488583803 0.9239397049 0.9266064763 - 0.9492663741 0.9245092869 0.9271281958 - 0.9496603012 0.9250699878 0.9276459217 - 0.9500542879 0.9256283045 0.9281548262 - 0.9504482150 0.9261865020 0.9286637902 - 0.9508420825 0.9267448187 0.9291728139 - 0.9512361288 0.9273030758 0.9296817780 - 0.9516299963 0.9278613925 0.9301908016 - 0.9520239234 0.9284197092 0.9306998253 - 0.9524179101 0.9289779067 0.9312087297 - 0.9528117776 0.9295362234 0.9317176938 - 0.9531949759 0.9300904870 0.9322267175 - 0.9535744190 0.9306278229 0.9327245951 - 0.9539538026 0.9311652184 0.9332185984 - 0.9543331861 0.9317024946 0.9337126017 - 0.9547126293 0.9322397709 0.9342067242 - 0.9550918937 0.9327772260 0.9347007275 - 0.9554712772 0.9333145022 0.9351947904 - 0.9558507204 0.9338517785 0.9356887937 - 0.9562301040 0.9343891144 0.9361827970 - 0.9566094875 0.9349265099 0.9366769195 - 0.9569786787 0.9354528785 0.9371693134 - 0.9573479295 0.9359676838 0.9376392961 - 0.9577171206 0.9364824891 0.9381092787 - 0.9580863714 0.9369972944 0.9385793209 - 0.9584556222 0.9375120997 0.9390493035 - 0.9588248134 0.9380269051 0.9395192862 - 0.9591940045 0.9385417104 0.9399893880 - 0.9595631957 0.9390563965 0.9404593706 - 0.9599325061 0.9395712018 0.9409294128 - 0.9602993131 0.9400860071 0.9413993955 - 0.9606528878 0.9405859709 0.9418666959 - 0.9610065818 0.9410789013 0.9423272014 - 0.9613602161 0.9415717125 0.9427877069 - 0.9617139101 0.9420645237 0.9432482123 - 0.9620674849 0.9425572753 0.9437087178 - 0.9624211788 0.9430500865 0.9441692233 - 0.9627748728 0.9435428977 0.9446297288 - 0.9631285071 0.9440357089 0.9450901747 - 0.9634822011 0.9445285201 0.9455507994 - 0.9638335109 0.9450212717 0.9460113049 - 0.9641757011 0.9455037117 0.9464758039 - 0.9645178914 0.9459828138 0.9469466209 - 0.9648600817 0.9464619160 0.9474173784 - 0.9652022719 0.9469410181 0.9478881955 - 0.9655445218 0.9474201202 0.9483590126 - 0.9658865929 0.9478992224 0.9488298297 - 0.9662287831 0.9483783245 0.9493005872 - 0.9665709734 0.9488574266 0.9497714043 - 0.9669132233 0.9493365288 0.9502422214 - 0.9672523737 0.9498155713 0.9507129788 - 0.9675732851 0.9502952099 0.9511811733 - 0.9678940773 0.9507750273 0.9516453743 - 0.9682149887 0.9512547255 0.9521095753 - 0.9685357809 0.9517344236 0.9525737166 - 0.9688566923 0.9522141814 0.9530379176 - 0.9691774845 0.9526938796 0.9535021186 - 0.9694983959 0.9531735778 0.9539663196 - 0.9698193073 0.9536533952 0.9544305205 - 0.9701400995 0.9541330934 0.9548947215 - 0.9704610109 0.9546129107 0.9553589225 - 0.9707775712 0.9550942779 0.9558221102 - 0.9710940123 0.9555768967 0.9562827945 - 0.9714105129 0.9560595751 0.9567434788 - 0.9717268944 0.9565421939 0.9572042227 - 0.9720433950 0.9570248127 0.9576649070 - 0.9723598957 0.9575073719 0.9581255913 - 0.9726762772 0.9579899907 0.9585862756 - 0.9729927778 0.9584727287 0.9590470195 - 0.9733092785 0.9589552879 0.9595077038 - 0.9736257195 0.9594379067 0.9599683881 - 0.9739437103 0.9599211216 0.9604294896 - 0.9742622972 0.9604055285 0.9608955979 - 0.9745808840 0.9608898163 0.9613618255 - 0.9748994708 0.9613741040 0.9618278742 - 0.9752181768 0.9618583918 0.9622939825 - 0.9755368233 0.9623427987 0.9627602100 - 0.9758554101 0.9628270864 0.9632263184 - 0.9761739969 0.9633113742 0.9636924267 - 0.9764927030 0.9637957215 0.9641585946 - 0.9768112898 0.9642801285 0.9646247029 - 0.9771254063 0.9647644162 0.9650908113 - 0.9774289727 0.9652258158 0.9655427933 - 0.9777324796 0.9656870961 0.9659900069 - 0.9780361056 0.9661483765 0.9664372206 - 0.9783396125 0.9666097164 0.9668844938 - 0.9786431789 0.9670709968 0.9673317075 - 0.9789466858 0.9675322771 0.9677789211 - 0.9792503119 0.9679936171 0.9682261944 - 0.9795538187 0.9684548974 0.9686734080 - 0.9798573852 0.9689161777 0.9691206217 - 0.9801608920 0.9693775177 0.9695677757 - 0.9804582000 0.9698340297 0.9700127244 - 0.9807540178 0.9702861905 0.9704523087 - 0.9810497165 0.9707384109 0.9708918929 - 0.9813454151 0.9711905718 0.9713315964 - 0.9816411734 0.9716427922 0.9717711806 - 0.9819368720 0.9720950127 0.9722108245 - 0.9822325706 0.9725471139 0.9726504087 - 0.9825283885 0.9729992747 0.9730899930 - 0.9828240871 0.9734514952 0.9735295773 - 0.9831197858 0.9739037156 0.9739692807 - 0.9834141135 0.9743558764 0.9744089246 - 0.9837012887 0.9748013020 0.9748445153 - 0.9839885831 0.9752463102 0.9752789736 - 0.9842758179 0.9756913185 0.9757133722 - 0.9845629930 0.9761363268 0.9761477709 - 0.9848502874 0.9765812755 0.9765822291 - 0.9851375222 0.9770262837 0.9770166278 - 0.9854248166 0.9774712920 0.9774510264 - 0.9857119918 0.9779163003 0.9778854251 - 0.9859992266 0.9783613086 0.9783198237 - 0.9862865210 0.9788061976 0.9787542224 - 0.9865726829 0.9792487025 0.9791879058 - 0.9868577123 0.9796836972 0.9796146154 - 0.9871426225 0.9801185727 0.9800413847 - 0.9874275923 0.9805535078 0.9804680943 - 0.9877125025 0.9809883833 0.9808948040 - 0.9879974723 0.9814233184 0.9813215733 - 0.9882823825 0.9818581939 0.9817482829 - 0.9885674119 0.9822931886 0.9821749926 - 0.9888523221 0.9827281237 0.9826018214 - 0.9891372919 0.9831629992 0.9830284715 - 0.9894222021 0.9835978746 0.9834551811 - 0.9896981120 0.9840263128 0.9838774800 - 0.9899675846 0.9844464064 0.9842901230 - 0.9902371764 0.9848663807 0.9847027063 - 0.9905067086 0.9852864146 0.9851151705 - 0.9907763004 0.9857063890 0.9855278134 - 0.9910457730 0.9861264825 0.9859403968 - 0.9913154244 0.9865465164 0.9863529205 - 0.9915848970 0.9869664907 0.9867655039 - 0.9918544888 0.9873865843 0.9871780276 - 0.9921240211 0.9878066182 0.9875906110 - 0.9923936129 0.9882265925 0.9880031943 - 0.9926524758 0.9886332750 0.9884101152 - 0.9929050803 0.9890270233 0.9888089895 - 0.9931576252 0.9894207120 0.9892079830 - 0.9934101105 0.9898144007 0.9896069169 - 0.9936627150 0.9902082086 0.9900057912 - 0.9939152002 0.9906018972 0.9904047251 - 0.9941678047 0.9909955859 0.9908037186 - 0.9944202900 0.9913892746 0.9912025928 - 0.9946727753 0.9917830229 0.9916015267 - 0.9949253798 0.9921767116 0.9920004010 - 0.9951779246 0.9925704002 0.9923992753 - 0.9954192042 0.9929419160 0.9927871823 - 0.9956508279 0.9932870269 0.9931579232 - 0.9958823919 0.9936321974 0.9935287237 - 0.9961140156 0.9939773083 0.9938995242 - 0.9963455796 0.9943224788 0.9942703247 - 0.9965772033 0.9946675897 0.9946411252 - 0.9968088269 0.9950128198 0.9950119257 - 0.9970403910 0.9953578711 0.9953826070 - 0.9972720146 0.9957031012 0.9957534075 - 0.9975035191 0.9960482121 0.9961242080 - 0.9977350831 0.9963933825 0.9964950085 - 0.9979518056 0.9967247844 0.9968441129 - 0.9981380105 0.9970226288 0.9971309900 - 0.9983242154 0.9973202944 0.9974179268 - 0.9985104203 0.9976180196 0.9977048039 - 0.9986966252 0.9979158044 0.9979916811 - 0.9988827705 0.9982135296 0.9982786179 - 0.9990689754 0.9985113144 0.9985654950 - 0.9992551804 0.9988089800 0.9988523722 - 0.9994413853 0.9991068244 0.9991393089 - 0.9996275902 0.9994044900 0.9994261861 - 0.9998137951 0.9997022748 0.9997131228 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0001187211 0.0001161198 0.0001305900 + 0.0002416660 0.0002335670 0.0002639299 + 0.0003686174 0.0003530100 0.0003996764 + 0.0004986166 0.0004744168 0.0005390237 + 0.0006320932 0.0005989556 0.0006801367 + 0.0007689670 0.0007275027 0.0008226076 + 0.0009098884 0.0008590615 0.0009674262 + 0.0010546240 0.0009952968 0.0011148060 + 0.0012056520 0.0011357040 0.0012659699 + 0.0013610790 0.0012804960 0.0014208819 + 0.0015210240 0.0014290370 0.0015805050 + 0.0016857639 0.0015831300 0.0017423360 + 0.0018555140 0.0017423540 0.0019072110 + 0.0020300581 0.0019068220 0.0020741550 + 0.0022078981 0.0020767010 0.0022443461 + 0.0023916010 0.0022511871 0.0024184019 + 0.0025805370 0.0024301831 0.0025977520 + 0.0027740600 0.0026134001 0.0027815269 + 0.0029722259 0.0028022721 0.0029672419 + 0.0031753390 0.0029984950 0.0031548510 + 0.0033845089 0.0032002409 0.0033454280 + 0.0036013669 0.0034078530 0.0035393371 + 0.0038235600 0.0036215940 0.0037365381 + 0.0040512760 0.0038406281 0.0039370959 + 0.0042870231 0.0040662498 0.0041424572 + 0.0045294450 0.0042975969 0.0043519312 + 0.0047791190 0.0045345472 0.0045640962 + 0.0050357901 0.0047785440 0.0047785449 + 0.0052975160 0.0050289081 0.0049969312 + 0.0055647590 0.0052849171 0.0052185720 + 0.0058401441 0.0055476031 0.0054451409 + 0.0061251819 0.0058162622 0.0056769978 + 0.0064190081 0.0060910610 0.0059134620 + 0.0067208251 0.0063716578 0.0061544981 + 0.0070298752 0.0066589769 0.0064014881 + 0.0073465379 0.0069537512 0.0066520870 + 0.0076715760 0.0072558299 0.0069075110 + 0.0080045266 0.0075645968 0.0071683591 + 0.0083457762 0.0078814235 0.0074348552 + 0.0086941188 0.0082036890 0.0077047022 + 0.0090481956 0.0085330773 0.0079792934 + 0.0094094584 0.0088689746 0.0082590310 + 0.0097797252 0.0092114061 0.0085430183 + 0.0101604396 0.0095595680 0.0088318810 + 0.0105508799 0.0099120708 0.0091247372 + 0.0109501397 0.0102693997 0.0094229234 + 0.0113572897 0.0106335897 0.0097273486 + 0.0117729101 0.0110058896 0.0100365505 + 0.0121964402 0.0113846296 0.0103524001 + 0.0126260100 0.0117704803 0.0106731104 + 0.0130626997 0.0121654198 0.0109990695 + 0.0135076502 0.0125680296 0.0113301203 + 0.0139606902 0.0129768401 0.0116658099 + 0.0144202998 0.0133933499 0.0120067801 + 0.0148866000 0.0138179902 0.0123553397 + 0.0153607102 0.0142508997 0.0127089303 + 0.0158437602 0.0146913100 0.0130658597 + 0.0163364708 0.0151374601 0.0134272100 + 0.0168359205 0.0155895501 0.0137940804 + 0.0173442401 0.0160493609 0.0141672902 + 0.0178608205 0.0165161695 0.0145466598 + 0.0183882900 0.0169901606 0.0149336299 + 0.0189235695 0.0174715705 0.0153278597 + 0.0194664709 0.0179615095 0.0157276094 + 0.0200162604 0.0184623506 0.0161328409 + 0.0205752291 0.0189736001 0.0165440794 + 0.0211429205 0.0194928199 0.0169631094 + 0.0217175093 0.0200189203 0.0173870400 + 0.0222972203 0.0205519404 0.0178171601 + 0.0228821803 0.0210929699 0.0182553697 + 0.0234732702 0.0216418691 0.0187003091 + 0.0240713898 0.0221973900 0.0191515498 + 0.0246774498 0.0227600299 0.0196103808 + 0.0252923407 0.0233312007 0.0200770292 + 0.0259165801 0.0239111893 0.0205497108 + 0.0265481994 0.0245017596 0.0210285001 + 0.0271885395 0.0251027998 0.0215149205 + 0.0278372504 0.0257091504 0.0220096204 + 0.0284956601 0.0263228696 0.0225120597 + 0.0291631296 0.0269445796 0.0230217203 + 0.0298364591 0.0275744200 0.0235392991 + 0.0305197891 0.0282128304 0.0240652803 + 0.0312113408 0.0288609006 0.0246002506 + 0.0319130905 0.0295199603 0.0251446608 + 0.0326239392 0.0301856901 0.0256962199 + 0.0333417691 0.0308586191 0.0262538604 + 0.0340690203 0.0315388404 0.0268181600 + 0.0347989090 0.0322262309 0.0273908004 + 0.0355309397 0.0329242386 0.0279702805 + 0.0362654515 0.0336307585 0.0285563804 + 0.0370042995 0.0343444385 0.0291495994 + 0.0377499983 0.0350654498 0.0297501199 + 0.0385023095 0.0357952416 0.0303586796 + 0.0392612107 0.0365385190 0.0309723206 + 0.0400277786 0.0372893997 0.0315904692 + 0.0408011489 0.0380479097 0.0322141387 + 0.0415779687 0.0388149507 0.0328444093 + 0.0423575304 0.0395870805 0.0334823392 + 0.0431431308 0.0403656401 0.0341259614 + 0.0439345613 0.0411506817 0.0347744413 + 0.0447305702 0.0419436581 0.0354271904 + 0.0455311611 0.0427440405 0.0360897183 + 0.0463386402 0.0435508788 0.0367595218 + 0.0471522398 0.0443644710 0.0374358408 + 0.0479747392 0.0451885909 0.0381190106 + 0.0488112308 0.0460180305 0.0388101190 + 0.0496566407 0.0468529202 0.0395072289 + 0.0505135618 0.0476984307 0.0402105600 + 0.0513870418 0.0485514998 0.0409212708 + 0.0522648282 0.0494111106 0.0416350588 + 0.0531454012 0.0502829701 0.0423520505 + 0.0540309697 0.0511616506 0.0430774391 + 0.0549204089 0.0520450994 0.0438090898 + 0.0558179915 0.0529359803 0.0445459113 + 0.0567257814 0.0538352691 0.0452902392 + 0.0576398782 0.0547406413 0.0460413806 + 0.0585679710 0.0556575507 0.0467979498 + 0.0595032796 0.0565782711 0.0475642905 + 0.0604466088 0.0575027503 0.0483360402 + 0.0614023097 0.0584334396 0.0491129309 + 0.0623621605 0.0593668409 0.0499006808 + 0.0633294508 0.0603048317 0.0506933182 + 0.0643018410 0.0612500012 0.0514932014 + 0.0652783364 0.0621989109 0.0522950403 + 0.0662618205 0.0631573200 0.0530990995 + 0.0672477037 0.0641201437 0.0539113805 + 0.0682398304 0.0650890917 0.0547297895 + 0.0692377687 0.0660655573 0.0555534288 + 0.0702422187 0.0670448914 0.0563857108 + 0.0712599233 0.0680295005 0.0572213605 + 0.0722822472 0.0690181032 0.0580649488 + 0.0733231679 0.0700139031 0.0589125901 + 0.0743673295 0.0710175782 0.0597636290 + 0.0754222274 0.0720258802 0.0606212094 + 0.0764820576 0.0730462894 0.0614807904 + 0.0775483102 0.0740699619 0.0623459816 + 0.0786194429 0.0751056671 0.0632140934 + 0.0796934590 0.0761416107 0.0640869290 + 0.0807745531 0.0771779865 0.0649647191 + 0.0818573833 0.0782172531 0.0658454672 + 0.0829574317 0.0792589337 0.0667318478 + 0.0840600133 0.0803091377 0.0676197112 + 0.0851803720 0.0813626423 0.0685161799 + 0.0863037184 0.0824246481 0.0694149882 + 0.0874389708 0.0834880322 0.0703213289 + 0.0885784179 0.0845618397 0.0712313801 + 0.0897280127 0.0856374800 0.0721469596 + 0.0908830017 0.0867242888 0.0730709806 + 0.0920473710 0.0878123492 0.0740019605 + 0.0932181776 0.0889043733 0.0749416873 + 0.0943980217 0.0899982378 0.0758856833 + 0.0955819637 0.0910960734 0.0768347234 + 0.0967710093 0.0921994597 0.0777852014 + 0.0979645699 0.0933120996 0.0787435994 + 0.0991635621 0.0944301113 0.0797032714 + 0.1003675982 0.0955557898 0.0806734487 + 0.1015781984 0.0966856703 0.0816444829 + 0.1027939990 0.0978211835 0.0826287419 + 0.1040176004 0.0989596322 0.0836134627 + 0.1052478999 0.1001020968 0.0846056491 + 0.1064907983 0.1012483984 0.0855979323 + 0.1077373996 0.1024006978 0.0865952373 + 0.1089940965 0.1035557985 0.0875926092 + 0.1102522984 0.1047163978 0.0886019021 + 0.1115181968 0.1058802009 0.0896116272 + 0.1127844006 0.1070526987 0.0906278268 + 0.1140555963 0.1082267985 0.0916445777 + 0.1153267995 0.1094079986 0.0926688984 + 0.1166016012 0.1105891988 0.0936945230 + 0.1178767011 0.1117718965 0.0947236270 + 0.1191575974 0.1129546016 0.0957538188 + 0.1204406992 0.1141434982 0.0967877209 + 0.1217328012 0.1153329015 0.0978235975 + 0.1230328009 0.1165269986 0.0988596827 + 0.1243387014 0.1177226007 0.0998959318 + 0.1256576031 0.1189235002 0.1009349972 + 0.1269775033 0.1201286018 0.1019790992 + 0.1283123046 0.1213376969 0.1030249000 + 0.1296471059 0.1225541010 0.1040780023 + 0.1309920996 0.1237716004 0.1051312014 + 0.1323397011 0.1249968037 0.1061976030 + 0.1336949021 0.1262219995 0.1072641984 + 0.1350574046 0.1274487972 0.1083405018 + 0.1364232004 0.1286759973 0.1094200984 + 0.1378028989 0.1299048960 0.1105057970 + 0.1391825974 0.1311350018 0.1115979999 + 0.1405749023 0.1323675066 0.1126938015 + 0.1419695020 0.1336061060 0.1138031036 + 0.1433696002 0.1348447949 0.1149123013 + 0.1447755992 0.1360920966 0.1160330996 + 0.1461827010 0.1373400986 0.1171550006 + 0.1476009935 0.1385940015 0.1182890981 + 0.1490193009 0.1398521960 0.1194318980 + 0.1504455954 0.1411112994 0.1205772012 + 0.1518757045 0.1423743069 0.1217304021 + 0.1533088982 0.1436371952 0.1228836030 + 0.1547531039 0.1449093074 0.1240445971 + 0.1561972052 0.1461839974 0.1252069026 + 0.1576558948 0.1474615037 0.1263778955 + 0.1591195017 0.1487443000 0.1275596023 + 0.1605871022 0.1500270963 0.1287415028 + 0.1620666981 0.1513230950 0.1299324036 + 0.1635462940 0.1526210010 0.1311233938 + 0.1650296003 0.1539209038 0.1323246062 + 0.1665142030 0.1552240998 0.1335338056 + 0.1680004001 0.1565274000 0.1347434968 + 0.1694938987 0.1578350067 0.1359594017 + 0.1709873974 0.1591434032 0.1371753067 + 0.1724853069 0.1604537964 0.1383929998 + 0.1739861071 0.1617684960 0.1396120936 + 0.1754869968 0.1630831063 0.1408315003 + 0.1769933999 0.1644009054 0.1420598030 + 0.1784998029 0.1657195985 0.1432881951 + 0.1800076067 0.1670403928 0.1445214003 + 0.1815180033 0.1683724970 0.1457598060 + 0.1830282956 0.1697044969 0.1469981968 + 0.1845442951 0.1710446030 0.1482491940 + 0.1860629022 0.1723918021 0.1495018005 + 0.1875815988 0.1737388968 0.1507571042 + 0.1891085953 0.1750912964 0.1520196050 + 0.1906356961 0.1764443964 0.1532821953 + 0.1921654046 0.1777988970 0.1545532942 + 0.1937022060 0.1791588962 0.1558299065 + 0.1952389926 0.1805189997 0.1571063995 + 0.1967843026 0.1818803996 0.1583925039 + 0.1983377039 0.1832433045 0.1596792042 + 0.1998911053 0.1846061945 0.1609704942 + 0.2014496028 0.1859742999 0.1622772068 + 0.2030100971 0.1873442978 0.1635839045 + 0.2045706064 0.1887143999 0.1648986936 + 0.2061385065 0.1900894940 0.1662220061 + 0.2077071965 0.1914649010 0.1675451994 + 0.2092768997 0.1928412020 0.1688794047 + 0.2108619064 0.1942230016 0.1702177972 + 0.2124470025 0.1956048012 0.1715561002 + 0.2140337974 0.1969860941 0.1729072928 + 0.2156284004 0.1983664036 0.1742596030 + 0.2172230035 0.1997465938 0.1756128073 + 0.2188204974 0.2011310011 0.1769745052 + 0.2204252034 0.2025205046 0.1783363074 + 0.2220298946 0.2039099038 0.1797029972 + 0.2236385942 0.2053045928 0.1810847074 + 0.2252545953 0.2067034990 0.1824664026 + 0.2268707007 0.2081024051 0.1838538945 + 0.2284909934 0.2095097005 0.1852518022 + 0.2301183045 0.2109221071 0.1866496950 + 0.2317456007 0.2123343945 0.1880539954 + 0.2333762050 0.2137437016 0.1894665956 + 0.2350125015 0.2151515931 0.1908791959 + 0.2366486937 0.2165593952 0.1922982931 + 0.2382874936 0.2179677039 0.1937243044 + 0.2399311960 0.2193762958 0.1951504052 + 0.2415750027 0.2207849026 0.1965806931 + 0.2432212979 0.2221989036 0.1980153024 + 0.2448745072 0.2236157060 0.1994500011 + 0.2465275973 0.2250325978 0.2008942068 + 0.2481831014 0.2264544070 0.2023486942 + 0.2498493940 0.2278795987 0.2038031965 + 0.2515155971 0.2293048054 0.2052644044 + 0.2531822920 0.2307375968 0.2067341059 + 0.2548569143 0.2321777046 0.2082037032 + 0.2565315068 0.2336179018 0.2096809000 + 0.2582060993 0.2350665033 0.2111717016 + 0.2598899007 0.2365286052 0.2126623988 + 0.2615750134 0.2379907966 0.2141568065 + 0.2632600963 0.2394558936 0.2156627029 + 0.2649540901 0.2409311980 0.2171684951 + 0.2666524947 0.2424065024 0.2186762989 + 0.2683508992 0.2438821048 0.2202025950 + 0.2700530887 0.2453690022 0.2217289060 + 0.2717601955 0.2468558997 0.2232550979 + 0.2734673023 0.2483427972 0.2247944027 + 0.2751758993 0.2498389930 0.2263347954 + 0.2768929005 0.2513372898 0.2278753072 + 0.2786099911 0.2528356910 0.2294308990 + 0.2803269923 0.2543384135 0.2309931070 + 0.2820504904 0.2558448911 0.2325553000 + 0.2837752998 0.2573513091 0.2341264933 + 0.2854999900 0.2588596940 0.2357089967 + 0.2872273922 0.2603740990 0.2372916043 + 0.2889575064 0.2618885040 0.2388751954 + 0.2906875908 0.2634029090 0.2404648066 + 0.2924185097 0.2649247050 0.2420545071 + 0.2941546142 0.2664470971 0.2436441034 + 0.2958906889 0.2679696083 0.2452432960 + 0.2976269126 0.2694908977 0.2468443960 + 0.2993660867 0.2710112929 0.2484454066 + 0.3011066020 0.2725316882 0.2500450909 + 0.3028470874 0.2740519941 0.2516433001 + 0.3045875132 0.2755717933 0.2532413900 + 0.3063274026 0.2770917118 0.2548404038 + 0.3080672026 0.2786116004 0.2564507127 + 0.3098070920 0.2801361978 0.2580609918 + 0.3115398884 0.2816629112 0.2596713006 + 0.3132705092 0.2831897140 0.2612847984 + 0.3150011003 0.2847178876 0.2629001141 + 0.3167325854 0.2862516940 0.2645153999 + 0.3184674084 0.2877854109 0.2661319077 + 0.3202022016 0.2893190980 0.2677547932 + 0.3219369948 0.2908464968 0.2693777084 + 0.3236666918 0.2923713028 0.2710005045 + 0.3253937066 0.2938959897 0.2726289034 + 0.3271206915 0.2954205871 0.2742598951 + 0.3288466036 0.2969438136 0.2758910060 + 0.3305548131 0.2984670103 0.2775225043 + 0.3322629035 0.2999902070 0.2791568041 + 0.3339709938 0.3015184104 0.2807911932 + 0.3356797099 0.3030506968 0.2824254930 + 0.3373891115 0.3045830131 0.2840622962 + 0.3390985131 0.3061152101 0.2857011855 + 0.3408079147 0.3076440990 0.2873399854 + 0.3425270021 0.3091726899 0.2889789045 + 0.3442499042 0.3107013106 0.2906231880 + 0.3459726870 0.3122313023 0.2922680080 + 0.3476955891 0.3137654960 0.2939127982 + 0.3494232893 0.3152998090 0.2955600917 + 0.3511509895 0.3168340027 0.2972145975 + 0.3528788090 0.3183681071 0.2988691032 + 0.3546074033 0.3199020922 0.3005236089 + 0.3563388884 0.3214359879 0.3021857142 + 0.3580704033 0.3229700029 0.3038538992 + 0.3598020077 0.3245058954 0.3055221140 + 0.3615402877 0.3260425031 0.3071902990 + 0.3632878065 0.3275789917 0.3088661134 + 0.3650352955 0.3291155100 0.3105435967 + 0.3667826951 0.3306686878 0.3122211099 + 0.3685362041 0.3322221935 0.3138988018 + 0.3702940941 0.3337756097 0.3155812025 + 0.3720518947 0.3353295028 0.3172636032 + 0.3738098145 0.3368858099 0.3189460039 + 0.3755826056 0.3384419978 0.3206303120 + 0.3773621023 0.3399981856 0.3223223090 + 0.3791415989 0.3415560126 0.3240141869 + 0.3809210956 0.3431178033 0.3257062137 + 0.3827106953 0.3446795046 0.3274028003 + 0.3845036030 0.3462412059 0.3291088939 + 0.3862965107 0.3478037119 0.3308149874 + 0.3880893886 0.3493674099 0.3325211108 + 0.3898766935 0.3509311080 0.3342320919 + 0.3916623890 0.3524948061 0.3359496891 + 0.3934481144 0.3540599942 0.3376674056 + 0.3952338099 0.3556275070 0.3393850923 + 0.3970318139 0.3571951091 0.3411099911 + 0.3988335133 0.3587625921 0.3428429961 + 0.4006352127 0.3603334129 0.3445760012 + 0.4024369121 0.3619092107 0.3463088870 + 0.4042539001 0.3634850085 0.3480437994 + 0.4060774148 0.3650608063 0.3497805893 + 0.4079008996 0.3666354120 0.3515174985 + 0.4097244143 0.3682079017 0.3532544076 + 0.4115552008 0.3697803915 0.3549940884 + 0.4133909941 0.3713529110 0.3567374945 + 0.4152266979 0.3729279935 0.3584809899 + 0.4170624912 0.3745107949 0.3602243960 + 0.4189001918 0.3760935962 0.3619695008 + 0.4207401872 0.3776763082 0.3637174964 + 0.4225800931 0.3792597950 0.3654656112 + 0.4244200885 0.3808481097 0.3672136962 + 0.4262587130 0.3824365139 0.3689666986 + 0.4280926883 0.3840247989 0.3707352877 + 0.4299268126 0.3856132030 0.3725039065 + 0.4317609072 0.3872022033 0.3742724061 + 0.4335947931 0.3887912929 0.3760411143 + 0.4354149997 0.3903804123 0.3778105080 + 0.4372350872 0.3919695020 0.3795798123 + 0.4390552044 0.3935660124 0.3813492060 + 0.4408754110 0.3951654136 0.3831185997 + 0.4426893890 0.3967649043 0.3848997056 + 0.4445011020 0.3983643949 0.3866828084 + 0.4463126957 0.3999699950 0.3884659111 + 0.4481244087 0.4015834928 0.3902488947 + 0.4499397874 0.4031969011 0.3920351863 + 0.4517613053 0.4048103988 0.3938237131 + 0.4535827041 0.4064252973 0.3956122100 + 0.4554041922 0.4080508053 0.3974005878 + 0.4572255909 0.4096763134 0.3991900980 + 0.4590497911 0.4113017917 0.4009819925 + 0.4608739018 0.4129272997 0.4027737975 + 0.4626981020 0.4145581126 0.4045656919 + 0.4645222127 0.4161905944 0.4063574970 + 0.4663369954 0.4178231061 0.4081600904 + 0.4681450129 0.4194557071 0.4099633992 + 0.4699530005 0.4210889935 0.4117667079 + 0.4717608988 0.4227238894 0.4135699868 + 0.4735681117 0.4243587852 0.4153786004 + 0.4753684103 0.4259937108 0.4171918035 + 0.4771685898 0.4276286066 0.4190050066 + 0.4789687991 0.4292654097 0.4208182096 + 0.4807690084 0.4309025109 0.4226312041 + 0.4825682044 0.4325394928 0.4244414866 + 0.4843665957 0.4341765046 0.4262517989 + 0.4861649871 0.4358190894 0.4280619919 + 0.4879634082 0.4374710917 0.4298723042 + 0.4897617102 0.4391230941 0.4316802025 + 0.4915564060 0.4407750964 0.4334864914 + 0.4933511019 0.4424270988 0.4352926910 + 0.4951457083 0.4440836906 0.4370990098 + 0.4969404042 0.4457412958 0.4389046133 + 0.4987323880 0.4473989010 0.4406982064 + 0.5005198121 0.4490565062 0.4424918890 + 0.5023072958 0.4507141113 0.4442855120 + 0.5040947795 0.4523720145 0.4460791945 + 0.5058822036 0.4540297985 0.4478673041 + 0.5076612830 0.4556876123 0.4496487081 + 0.5094367862 0.4573453963 0.4514302015 + 0.5112122893 0.4589950144 0.4532116055 + 0.5129877925 0.4606379867 0.4549930990 + 0.5147632957 0.4622808993 0.4567654133 + 0.5165187120 0.4639238119 0.4585354924 + 0.5182737112 0.4655668139 0.4603056014 + 0.5200287104 0.4672066867 0.4620757997 + 0.5217838287 0.4688459933 0.4638450146 + 0.5235412717 0.4704853892 0.4656072855 + 0.5253081918 0.4721247852 0.4673697054 + 0.5270751119 0.4737643003 0.4691320062 + 0.5288419724 0.4754061103 0.4708943069 + 0.5306090117 0.4770478904 0.4726507962 + 0.5323774219 0.4786897004 0.4743973911 + 0.5341482759 0.4803315997 0.4761438966 + 0.5359191895 0.4819717109 0.4778904021 + 0.5376901031 0.4836075008 0.4796369076 + 0.5394610167 0.4852432907 0.4813722968 + 0.5412335992 0.4868789911 0.4830994010 + 0.5430079103 0.4885148108 0.4848263860 + 0.5447822213 0.4901467860 0.4865534008 + 0.5465564728 0.4917733967 0.4882805049 + 0.5483307838 0.4934000075 0.4900054038 + 0.5500996709 0.4950265884 0.4917294979 + 0.5518643856 0.4966531992 0.4934537113 + 0.5536291003 0.4982692897 0.4951778054 + 0.5553938746 0.4998747110 0.4969018996 + 0.5571585894 0.5014801025 0.4986188114 + 0.5589029789 0.5030853748 0.5003340244 + 0.5606324077 0.5046908259 0.5020492077 + 0.5623617768 0.5062916875 0.5037645102 + 0.5640913248 0.5078886151 0.5054796934 + 0.5658206940 0.5094854236 0.5071663260 + 0.5675324798 0.5110822916 0.5088484883 + 0.5692291260 0.5126792192 0.5105305910 + 0.5709257126 0.5142681003 0.5122128129 + 0.5726222992 0.5158489943 0.5138949156 + 0.5743188858 0.5174300075 0.5155733228 + 0.5760101080 0.5190109015 0.5172510743 + 0.5776947141 0.5205919147 0.5189288259 + 0.5793792009 0.5221624970 0.5206065774 + 0.5810636878 0.5237188935 0.5222843289 + 0.5827482939 0.5252754092 0.5239499807 + 0.5844303966 0.5268318057 0.5256124735 + 0.5861067772 0.5283882022 0.5272750854 + 0.5877830982 0.5299438238 0.5289375782 + 0.5894594789 0.5314972997 0.5306000710 + 0.5911359191 0.5330507159 0.5322602987 + 0.5928109884 0.5346041918 0.5339192748 + 0.5944728255 0.5361576080 0.5355783105 + 0.5961346030 0.5377094746 0.5372374058 + 0.5977964997 0.5392475128 0.5388963819 + 0.5994582772 0.5407854915 0.5405499935 + 0.6011201143 0.5423235297 0.5421981215 + 0.6027656794 0.5438615084 0.5438463092 + 0.6044077277 0.5453994274 0.5454944968 + 0.6060497165 0.5469250083 0.5471426845 + 0.6076918244 0.5484485030 0.5487864017 + 0.6093338132 0.5499718785 0.5504183173 + 0.6109706163 0.5514953732 0.5520501733 + 0.6126019955 0.5530188084 0.5536820292 + 0.6142334938 0.5545319915 0.5553138852 + 0.6158648729 0.5560368896 0.5569456816 + 0.6174963713 0.5575417876 0.5585566759 + 0.6191260219 0.5590466857 0.5601673126 + 0.6207424998 0.5605515838 0.5617777705 + 0.6223589182 0.5620545745 0.5633884072 + 0.6239753962 0.5635496974 0.5649989843 + 0.6255918741 0.5650447011 0.5666049719 + 0.6272084117 0.5665397048 0.5682082772 + 0.6288107038 0.5680348277 0.5698117018 + 0.6304064989 0.5695297718 0.5714150071 + 0.6320024133 0.5710126162 0.5730183125 + 0.6335982084 0.5724920034 0.5746158957 + 0.6351940036 0.5739712715 0.5761947036 + 0.6367834806 0.5754505992 0.5777735114 + 0.6383458972 0.5769299269 0.5793523788 + 0.6399083138 0.5784004927 0.5809311867 + 0.6414706707 0.5798507929 0.5825099945 + 0.6430330873 0.5813010931 0.5840647817 + 0.6445955038 0.5827513933 0.5856127143 + 0.6461372972 0.5842016935 0.5871605873 + 0.6476672888 0.5856518745 0.5887084007 + 0.6491973996 0.5870863795 0.5902562737 + 0.6507273912 0.5885158777 0.5917943120 + 0.6522575021 0.5899453759 0.5933035016 + 0.6537867785 0.5913748741 0.5948126912 + 0.6552811861 0.5928043723 0.5963220000 + 0.6567755938 0.5942304730 0.5978311896 + 0.6582700014 0.5956394076 0.5993403792 + 0.6597645283 0.5970482826 0.6008219123 + 0.6612588763 0.5984572768 0.6022902131 + 0.6627463102 0.5998662114 0.6037583947 + 0.6642202735 0.6012750864 0.6052265763 + 0.6656942964 0.6026799083 0.6066948771 + 0.6671683192 0.6040807962 0.6081615090 + 0.6686422825 0.6054816842 0.6095842719 + 0.6701163054 0.6068825722 0.6110069752 + 0.6715676188 0.6082835793 0.6124296784 + 0.6730042100 0.6096845269 0.6138523817 + 0.6744406819 0.6110807061 0.6152750850 + 0.6758772135 0.6124755740 0.6166884899 + 0.6773136854 0.6138706207 0.6180831194 + 0.6787502170 0.6152656078 0.6194776297 + 0.6801577210 0.6166605949 0.6208721995 + 0.6815580130 0.6180554032 0.6222668290 + 0.6829584241 0.6194347143 0.6236612797 + 0.6843587160 0.6208140254 0.6250466704 + 0.6857591271 0.6221933961 0.6264250875 + 0.6871594191 0.6235727072 0.6278035045 + 0.6885198951 0.6249520779 0.6291819215 + 0.6898779273 0.6263288856 0.6305602789 + 0.6912357807 0.6276934147 0.6319386959 + 0.6925938129 0.6290578842 0.6332967877 + 0.6939517856 0.6304224133 0.6346477866 + 0.6953091025 0.6317868829 0.6359987855 + 0.6966227889 0.6331512928 0.6373497844 + 0.6979364753 0.6345124841 0.6387007833 + 0.6992502213 0.6358644962 0.6400517821 + 0.7005639076 0.6372163892 0.6413751841 + 0.7018774748 0.6385684013 0.6426938176 + 0.7031903863 0.6399202943 0.6440123916 + 0.7044675946 0.6412723064 0.6453310251 + 0.7057446837 0.6426153183 0.6466495991 + 0.7070218921 0.6439368129 0.6479681730 + 0.7082989812 0.6452583075 0.6492698789 + 0.7095761895 0.6465796828 0.6505696774 + 0.7108533978 0.6479011774 0.6518694162 + 0.7121080160 0.6492226720 0.6531692147 + 0.7133617997 0.6505348086 0.6544690132 + 0.7146157026 0.6518198252 0.6557686925 + 0.7158694863 0.6531049013 0.6570504904 + 0.7171233296 0.6543899179 0.6583297849 + 0.7183771133 0.6556748748 0.6596090198 + 0.7195919752 0.6569600105 0.6608883142 + 0.7207996845 0.6582419872 0.6621676087 + 0.7220073938 0.6595075130 0.6634469032 + 0.7232151031 0.6607730985 0.6647216082 + 0.7244228721 0.6620386243 0.6659951806 + 0.7256305814 0.6633040905 0.6672688723 + 0.7268121243 0.6645696759 0.6685425043 + 0.7279800177 0.6658352017 0.6698160768 + 0.7291477919 0.6670749784 0.6710897088 + 0.7303156257 0.6683145165 0.6723518968 + 0.7314835191 0.6695539951 0.6736078858 + 0.7326512933 0.6707934737 0.6748638749 + 0.7338057160 0.6720330119 0.6761198044 + 0.7349399924 0.6732723713 0.6773757935 + 0.7360742092 0.6744793057 0.6786317229 + 0.7372084260 0.6756755710 0.6798825264 + 0.7383427024 0.6768718958 0.6811262965 + 0.7394769192 0.6780682206 0.6823701859 + 0.7406092882 0.6792644858 0.6836140156 + 0.7417197227 0.6804608107 0.6848577857 + 0.7428302169 0.6816480756 0.6861016750 + 0.7439405918 0.6828244925 0.6873413920 + 0.7450510263 0.6840010285 0.6885573864 + 0.7461614013 0.6851773858 0.6897733212 + 0.7472717762 0.6863538027 0.6909891963 + 0.7483662963 0.6875302196 0.6922051907 + 0.7494534254 0.6887052059 0.6934211254 + 0.7505404949 0.6898639202 0.6946370006 + 0.7516276240 0.6910225153 0.6958392859 + 0.7527148128 0.6921812296 0.6970379949 + 0.7538018823 0.6933398843 0.6982367039 + 0.7548860908 0.6944984794 0.6994354129 + 0.7559602857 0.6956571937 0.7006341219 + 0.7570344210 0.6968036890 0.7018327713 + 0.7581086159 0.6979433894 0.7030227184 + 0.7591826916 0.6990832090 0.7041977048 + 0.7602568865 0.7002230287 0.7053728104 + 0.7613310218 0.7013627291 0.7065479159 + 0.7623906136 0.7025024891 0.7077229023 + 0.7634438276 0.7036383748 0.7088980079 + 0.7644969225 0.7047494054 0.7100731134 + 0.7655500174 0.7058604956 0.7112265229 + 0.7666031122 0.7069715858 0.7123770118 + 0.7676562071 0.7080826759 0.7135276198 + 0.7687063813 0.7091938257 0.7146781087 + 0.7697265148 0.7103049159 0.7158285975 + 0.7707465887 0.7114003897 0.7169790864 + 0.7717667222 0.7124834061 0.7181215286 + 0.7727867961 0.7135664225 0.7192468047 + 0.7738069296 0.7146493793 0.7203720808 + 0.7748268843 0.7157322764 0.7214972973 + 0.7758402824 0.7168152928 0.7226225734 + 0.7768442035 0.7178983092 0.7237479091 + 0.7778481245 0.7189552784 0.7248731256 + 0.7788519859 0.7200095057 0.7259737253 + 0.7798559070 0.7210636735 0.7270630002 + 0.7808597088 0.7221180201 0.7281523943 + 0.7818635702 0.7231721878 0.7292417288 + 0.7828533053 0.7242264152 0.7303311229 + 0.7838358283 0.7252786756 0.7314203978 + 0.7848182917 0.7263221145 0.7325097919 + 0.7858008742 0.7273653746 0.7335510850 + 0.7867833972 0.7284088135 0.7345916033 + 0.7877659202 0.7294521928 0.7356320024 + 0.7887483835 0.7304955721 0.7366725206 + 0.7897030711 0.7315390110 0.7377129793 + 0.7906526923 0.7325803041 0.7387534976 + 0.7916023135 0.7336180806 0.7397875190 + 0.7925518751 0.7346559763 0.7407963872 + 0.7935014963 0.7356938720 0.7418051958 + 0.7944511175 0.7367317080 0.7428141236 + 0.7954006791 0.7377696037 0.7438229918 + 0.7963156104 0.7388074994 0.7448318005 + 0.7972288132 0.7398343086 0.7458407283 + 0.7981420159 0.7408506870 0.7468411922 + 0.7990550995 0.7418671250 0.7478262782 + 0.7999683022 0.7428835034 0.7488114238 + 0.8008813858 0.7438998818 0.7497966290 + 0.8017945886 0.7449163795 0.7507817149 + 0.8026857972 0.7459328175 0.7517668009 + 0.8035765886 0.7469387054 0.7527520061 + 0.8044673800 0.7479367852 0.7537305951 + 0.8053581119 0.7489348054 0.7547006011 + 0.8062489033 0.7499328852 0.7556706071 + 0.8071396947 0.7509309053 0.7566406131 + 0.8080304861 0.7519289255 0.7576106191 + 0.8089057207 0.7529270053 0.7585806251 + 0.8097798228 0.7539178133 0.7595505714 + 0.8106539249 0.7549026012 0.7605128884 + 0.8115280271 0.7558873892 0.7614648938 + 0.8124021888 0.7568721771 0.7624170184 + 0.8132762909 0.7578570843 0.7633690834 + 0.8141503930 0.7588418722 0.7643210888 + 0.8150117993 0.7598267198 0.7652732134 + 0.8158702850 0.7607942820 0.7662252188 + 0.8167288899 0.7617409825 0.7671691775 + 0.8175873756 0.7626876831 0.7680981755 + 0.8184459805 0.7636343837 0.7690271735 + 0.8193045259 0.7645810843 0.7699561715 + 0.8201630116 0.7655277848 0.7708852887 + 0.8210126758 0.7664744854 0.7718142867 + 0.8218567967 0.7674121261 0.7727432847 + 0.8227009177 0.7683262229 0.7736694813 + 0.8235449791 0.7692403197 0.7745841146 + 0.8243891001 0.7701544166 0.7754986882 + 0.8252332211 0.7710685134 0.7764133215 + 0.8260772824 0.7719824910 0.7773278952 + 0.8269128799 0.7728965878 0.7782425284 + 0.8277320266 0.7738102078 0.7791571021 + 0.8285509944 0.7747033834 0.7800716758 + 0.8293700814 0.7755966783 0.7809833884 + 0.8301892281 0.7764899135 0.7818949223 + 0.8310083151 0.7773832083 0.7828063965 + 0.8318274021 0.7782763839 0.7837178707 + 0.8326464295 0.7791696787 0.7846295238 + 0.8334459066 0.7800629139 0.7855409980 + 0.8342450261 0.7809472084 0.7864524722 + 0.8350440860 0.7818276286 0.7873616815 + 0.8358432055 0.7827079892 0.7882695794 + 0.8366423845 0.7835882902 0.7891774774 + 0.8374415040 0.7844687104 0.7900853753 + 0.8382406235 0.7853490710 0.7909933925 + 0.8390303850 0.7862293720 0.7919012904 + 0.8398123980 0.7871022224 0.7928091884 + 0.8405944705 0.7879561782 0.7937148809 + 0.8413766026 0.7888103127 0.7946143746 + 0.8421586156 0.7896643281 0.7955139279 + 0.8429406881 0.7905182838 0.7964133024 + 0.8437227011 0.7913722992 0.7973127961 + 0.8445037007 0.7922263741 0.7982122898 + 0.8452532887 0.7930803895 0.7991117835 + 0.8460028768 0.7939234972 0.8000112772 + 0.8467524052 0.7947638035 0.8008962870 + 0.8475019932 0.7956042290 0.8017777801 + 0.8482515812 0.7964444757 0.8026592135 + 0.8490012288 0.7972847819 0.8035405874 + 0.8497508168 0.7981252074 0.8044220209 + 0.8504915833 0.7989655137 0.8053035140 + 0.8512210250 0.7998054028 0.8061848879 + 0.8519504070 0.8006436229 0.8070607185 + 0.8526797891 0.8014817834 0.8079198003 + 0.8534091711 0.8023201227 0.8087788820 + 0.8541386127 0.8031582832 0.8096380234 + 0.8548679948 0.8039965034 0.8104969859 + 0.8555973768 0.8048347831 0.8113561273 + 0.8563107252 0.8056730032 0.8122152090 + 0.8570188284 0.8065040112 0.8130742908 + 0.8577268720 0.8073294759 0.8139200807 + 0.8584349751 0.8081550002 0.8147575855 + 0.8591430783 0.8089805841 0.8155952096 + 0.8598511815 0.8098061085 0.8164327741 + 0.8605592251 0.8106316924 0.8172702789 + 0.8612673283 0.8114572167 0.8181079030 + 0.8619577289 0.8122826815 0.8189455271 + 0.8626478910 0.8130952716 0.8197829723 + 0.8633381128 0.8139054179 0.8205946088 + 0.8640282750 0.8147153854 0.8214033842 + 0.8647184968 0.8155254722 0.8222122192 + 0.8654087186 0.8163354993 0.8230209947 + 0.8660988808 0.8171455860 0.8238298297 + 0.8667852283 0.8179556131 0.8246386051 + 0.8674482107 0.8187654018 0.8254473805 + 0.8681111932 0.8195549250 0.8262543082 + 0.8687742949 0.8203443289 0.8270450234 + 0.8694372773 0.8211337924 0.8278357983 + 0.8701003194 0.8219231963 0.8286265135 + 0.8707633018 0.8227127194 0.8294172883 + 0.8714262843 0.8235021830 0.8302080035 + 0.8720872998 0.8242915869 0.8309987783 + 0.8727403283 0.8250802159 0.8317894936 + 0.8733932972 0.8258609772 0.8325753212 + 0.8740463257 0.8266419172 0.8333438039 + 0.8746992946 0.8274227977 0.8341122866 + 0.8753523231 0.8282036781 0.8348808289 + 0.8760052919 0.8289846182 0.8356493115 + 0.8766583204 0.8297654986 0.8364177942 + 0.8773071170 0.8305463791 0.8371862173 + 0.8779379725 0.8313267827 0.8379547000 + 0.8785688877 0.8321037292 0.8387172222 + 0.8791998029 0.8328806162 0.8394616246 + 0.8798307180 0.8336575031 0.8402060866 + 0.8804615736 0.8344345093 0.8409506083 + 0.8810924888 0.8352113962 0.8416950703 + 0.8817234039 0.8359882832 0.8424395919 + 0.8823524117 0.8367652297 0.8431841135 + 0.8829597831 0.8375415206 0.8439285755 + 0.8835672736 0.8383017182 0.8446694016 + 0.8841748238 0.8390619159 0.8453953266 + 0.8847821951 0.8398221135 0.8461210728 + 0.8853896856 0.8405823112 0.8468469977 + 0.8859971762 0.8413426280 0.8475728035 + 0.8866046071 0.8421028256 0.8482987285 + 0.8872120976 0.8428630233 0.8490244746 + 0.8877959847 0.8436232209 0.8497503996 + 0.8883770108 0.8443626761 0.8504748940 + 0.8889580965 0.8450992703 0.8511757255 + 0.8895391226 0.8458358049 0.8518766165 + 0.8901200891 0.8465722799 0.8525773883 + 0.8907011151 0.8473088145 0.8532782793 + 0.8912820816 0.8480454087 0.8539791107 + 0.8918631077 0.8487818837 0.8546800017 + 0.8924347758 0.8495184183 0.8553807735 + 0.8930004239 0.8502457142 0.8560817242 + 0.8935660124 0.8509672284 0.8567734957 + 0.8941316009 0.8516888022 0.8574634194 + 0.8946971893 0.8524103165 0.8581532240 + 0.8952628970 0.8531318903 0.8588430882 + 0.8958284855 0.8538534045 0.8595330119 + 0.8963940740 0.8545749784 0.8602228761 + 0.8969566822 0.8552964926 0.8609126806 + 0.8975095749 0.8560137153 0.8616026044 + 0.8980625272 0.8567193747 0.8622909784 + 0.8986153007 0.8574250937 0.8629779816 + 0.8991681933 0.8581306934 0.8636649847 + 0.8997210264 0.8588364124 0.8643519878 + 0.9002739191 0.8595420718 0.8650388718 + 0.9008268118 0.8602476716 0.8657258749 + 0.9013795853 0.8609533906 0.8664128780 + 0.9019126892 0.8616591096 0.8670998812 + 0.9024397731 0.8623467088 0.8677850962 + 0.9029669166 0.8630310297 0.8684529066 + 0.9034940004 0.8637151718 0.8691207170 + 0.9040210843 0.8643994927 0.8697885871 + 0.9045482278 0.8650836945 0.8704563975 + 0.9050753117 0.8657680154 0.8711242080 + 0.9056023955 0.8664522171 0.8717920184 + 0.9061269760 0.8671364784 0.8724598885 + 0.9066426754 0.8678159714 0.8731276989 + 0.9071583152 0.8684850931 0.8737882972 + 0.9076738954 0.8691542149 0.8744438291 + 0.9081895947 0.8698232174 0.8750993013 + 0.9087051749 0.8704922795 0.8757547736 + 0.9092208147 0.8711614013 0.8764103055 + 0.9097363949 0.8718305230 0.8770657778 + 0.9102520943 0.8724995852 0.8777213097 + 0.9107689857 0.8731687069 0.8783767223 + 0.9112870097 0.8738380075 0.8790321946 + 0.9118049741 0.8745074868 0.8796920180 + 0.9123229980 0.8751769066 0.8803519011 + 0.9128410220 0.8758463264 0.8810117245 + 0.9133591056 0.8765158057 0.8816714883 + 0.9138771296 0.8771852255 0.8823313117 + 0.9143950939 0.8778547049 0.8829911947 + 0.9149131179 0.8785241246 0.8836510181 + 0.9154273868 0.8791936040 0.8843107820 + 0.9159408808 0.8798611164 0.8849681020 + 0.9164543748 0.8805286884 0.8856195807 + 0.9169679284 0.8811962008 0.8862711191 + 0.9174814224 0.8818637133 0.8869225979 + 0.9179949164 0.8825312853 0.8875740767 + 0.9185084105 0.8831987977 0.8882256746 + 0.9190219045 0.8838664293 0.8888772130 + 0.9195353985 0.8845338821 0.8895286918 + 0.9200410843 0.8851994872 0.8901801705 + 0.9205467105 0.8858562112 0.8908231258 + 0.9210522175 0.8865129948 0.8914582133 + 0.9215577841 0.8871697187 0.8920934200 + 0.9220634103 0.8878263831 0.8927285075 + 0.9225689769 0.8884831071 0.8933635950 + 0.9230744839 0.8891397715 0.8939986825 + 0.9235801101 0.8897964954 0.8946338892 + 0.9240844846 0.8904532194 0.8952689767 + 0.9245653152 0.8911066055 0.8959041238 + 0.9250460863 0.8917511106 0.8965278268 + 0.9255269170 0.8923956156 0.8971452713 + 0.9260076880 0.8930401206 0.8977627754 + 0.9264885187 0.8936846256 0.8983802795 + 0.9269692898 0.8943290710 0.8989977241 + 0.9274501204 0.8949735761 0.8996152282 + 0.9279310107 0.8956180811 0.9002326727 + 0.9284117222 0.8962625861 0.9008501768 + 0.9288892150 0.8969045281 0.9014676809 + 0.9293668270 0.8975396156 0.9020715952 + 0.9298443198 0.8981747031 0.9026690125 + 0.9303218126 0.8988097906 0.9032663703 + 0.9307994246 0.8994448185 0.9038637877 + 0.9312769175 0.9000799060 0.9044612050 + 0.9317544103 0.9007149935 0.9050586224 + 0.9322319031 0.9013500810 0.9056559801 + 0.9327095151 0.9019851089 0.9062533975 + 0.9331722856 0.9026165009 0.9068508148 + 0.9336332083 0.9032307267 0.9074404240 + 0.9340941906 0.9038448930 0.9080247879 + 0.9345551133 0.9044591188 0.9086092114 + 0.9350160956 0.9050732851 0.9091936946 + 0.9354770780 0.9056875110 0.9097781181 + 0.9359380007 0.9063016772 0.9103624821 + 0.9363989830 0.9069159031 0.9109470248 + 0.9368600249 0.9075301290 0.9115313888 + 0.9373095036 0.9081442952 0.9121158719 + 0.9377532005 0.9087433815 0.9126855135 + 0.9381968975 0.9093422294 0.9132357836 + 0.9386405945 0.9099410176 0.9137859941 + 0.9390842915 0.9105398059 0.9143362045 + 0.9395279884 0.9111385942 0.9148864746 + 0.9399716854 0.9117373824 0.9154366851 + 0.9404153824 0.9123361707 0.9159868956 + 0.9408590794 0.9129350185 0.9165371060 + 0.9412962198 0.9135338068 0.9170873761 + 0.9417200089 0.9141229987 0.9176347852 + 0.9421437979 0.9147080183 0.9181675911 + 0.9425675869 0.9152929187 0.9187005162 + 0.9429913759 0.9158778191 0.9192333221 + 0.9434152246 0.9164627194 0.9197661877 + 0.9438390136 0.9170476198 0.9202991128 + 0.9442628026 0.9176325202 0.9208319187 + 0.9446865916 0.9182174802 0.9213647842 + 0.9451104999 0.9188023806 0.9218975902 + 0.9455274940 0.9193829298 0.9224305153 + 0.9459438920 0.9199525118 0.9229544997 + 0.9463601708 0.9205220938 0.9234762192 + 0.9467766285 0.9210916758 0.9239978790 + 0.9471930265 0.9216613173 0.9245195985 + 0.9476093054 0.9222308993 0.9250413775 + 0.9480257034 0.9228004813 0.9255630970 + 0.9484419823 0.9233701229 0.9260848165 + 0.9488583803 0.9239397049 0.9266064763 + 0.9492663741 0.9245092869 0.9271281958 + 0.9496603012 0.9250699878 0.9276459217 + 0.9500542879 0.9256283045 0.9281548262 + 0.9504482150 0.9261865020 0.9286637902 + 0.9508420825 0.9267448187 0.9291728139 + 0.9512361288 0.9273030758 0.9296817780 + 0.9516299963 0.9278613925 0.9301908016 + 0.9520239234 0.9284197092 0.9306998253 + 0.9524179101 0.9289779067 0.9312087297 + 0.9528117776 0.9295362234 0.9317176938 + 0.9531949759 0.9300904870 0.9322267175 + 0.9535744190 0.9306278229 0.9327245951 + 0.9539538026 0.9311652184 0.9332185984 + 0.9543331861 0.9317024946 0.9337126017 + 0.9547126293 0.9322397709 0.9342067242 + 0.9550918937 0.9327772260 0.9347007275 + 0.9554712772 0.9333145022 0.9351947904 + 0.9558507204 0.9338517785 0.9356887937 + 0.9562301040 0.9343891144 0.9361827970 + 0.9566094875 0.9349265099 0.9366769195 + 0.9569786787 0.9354528785 0.9371693134 + 0.9573479295 0.9359676838 0.9376392961 + 0.9577171206 0.9364824891 0.9381092787 + 0.9580863714 0.9369972944 0.9385793209 + 0.9584556222 0.9375120997 0.9390493035 + 0.9588248134 0.9380269051 0.9395192862 + 0.9591940045 0.9385417104 0.9399893880 + 0.9595631957 0.9390563965 0.9404593706 + 0.9599325061 0.9395712018 0.9409294128 + 0.9602993131 0.9400860071 0.9413993955 + 0.9606528878 0.9405859709 0.9418666959 + 0.9610065818 0.9410789013 0.9423272014 + 0.9613602161 0.9415717125 0.9427877069 + 0.9617139101 0.9420645237 0.9432482123 + 0.9620674849 0.9425572753 0.9437087178 + 0.9624211788 0.9430500865 0.9441692233 + 0.9627748728 0.9435428977 0.9446297288 + 0.9631285071 0.9440357089 0.9450901747 + 0.9634822011 0.9445285201 0.9455507994 + 0.9638335109 0.9450212717 0.9460113049 + 0.9641757011 0.9455037117 0.9464758039 + 0.9645178914 0.9459828138 0.9469466209 + 0.9648600817 0.9464619160 0.9474173784 + 0.9652022719 0.9469410181 0.9478881955 + 0.9655445218 0.9474201202 0.9483590126 + 0.9658865929 0.9478992224 0.9488298297 + 0.9662287831 0.9483783245 0.9493005872 + 0.9665709734 0.9488574266 0.9497714043 + 0.9669132233 0.9493365288 0.9502422214 + 0.9672523737 0.9498155713 0.9507129788 + 0.9675732851 0.9502952099 0.9511811733 + 0.9678940773 0.9507750273 0.9516453743 + 0.9682149887 0.9512547255 0.9521095753 + 0.9685357809 0.9517344236 0.9525737166 + 0.9688566923 0.9522141814 0.9530379176 + 0.9691774845 0.9526938796 0.9535021186 + 0.9694983959 0.9531735778 0.9539663196 + 0.9698193073 0.9536533952 0.9544305205 + 0.9701400995 0.9541330934 0.9548947215 + 0.9704610109 0.9546129107 0.9553589225 + 0.9707775712 0.9550942779 0.9558221102 + 0.9710940123 0.9555768967 0.9562827945 + 0.9714105129 0.9560595751 0.9567434788 + 0.9717268944 0.9565421939 0.9572042227 + 0.9720433950 0.9570248127 0.9576649070 + 0.9723598957 0.9575073719 0.9581255913 + 0.9726762772 0.9579899907 0.9585862756 + 0.9729927778 0.9584727287 0.9590470195 + 0.9733092785 0.9589552879 0.9595077038 + 0.9736257195 0.9594379067 0.9599683881 + 0.9739437103 0.9599211216 0.9604294896 + 0.9742622972 0.9604055285 0.9608955979 + 0.9745808840 0.9608898163 0.9613618255 + 0.9748994708 0.9613741040 0.9618278742 + 0.9752181768 0.9618583918 0.9622939825 + 0.9755368233 0.9623427987 0.9627602100 + 0.9758554101 0.9628270864 0.9632263184 + 0.9761739969 0.9633113742 0.9636924267 + 0.9764927030 0.9637957215 0.9641585946 + 0.9768112898 0.9642801285 0.9646247029 + 0.9771254063 0.9647644162 0.9650908113 + 0.9774289727 0.9652258158 0.9655427933 + 0.9777324796 0.9656870961 0.9659900069 + 0.9780361056 0.9661483765 0.9664372206 + 0.9783396125 0.9666097164 0.9668844938 + 0.9786431789 0.9670709968 0.9673317075 + 0.9789466858 0.9675322771 0.9677789211 + 0.9792503119 0.9679936171 0.9682261944 + 0.9795538187 0.9684548974 0.9686734080 + 0.9798573852 0.9689161777 0.9691206217 + 0.9801608920 0.9693775177 0.9695677757 + 0.9804582000 0.9698340297 0.9700127244 + 0.9807540178 0.9702861905 0.9704523087 + 0.9810497165 0.9707384109 0.9708918929 + 0.9813454151 0.9711905718 0.9713315964 + 0.9816411734 0.9716427922 0.9717711806 + 0.9819368720 0.9720950127 0.9722108245 + 0.9822325706 0.9725471139 0.9726504087 + 0.9825283885 0.9729992747 0.9730899930 + 0.9828240871 0.9734514952 0.9735295773 + 0.9831197858 0.9739037156 0.9739692807 + 0.9834141135 0.9743558764 0.9744089246 + 0.9837012887 0.9748013020 0.9748445153 + 0.9839885831 0.9752463102 0.9752789736 + 0.9842758179 0.9756913185 0.9757133722 + 0.9845629930 0.9761363268 0.9761477709 + 0.9848502874 0.9765812755 0.9765822291 + 0.9851375222 0.9770262837 0.9770166278 + 0.9854248166 0.9774712920 0.9774510264 + 0.9857119918 0.9779163003 0.9778854251 + 0.9859992266 0.9783613086 0.9783198237 + 0.9862865210 0.9788061976 0.9787542224 + 0.9865726829 0.9792487025 0.9791879058 + 0.9868577123 0.9796836972 0.9796146154 + 0.9871426225 0.9801185727 0.9800413847 + 0.9874275923 0.9805535078 0.9804680943 + 0.9877125025 0.9809883833 0.9808948040 + 0.9879974723 0.9814233184 0.9813215733 + 0.9882823825 0.9818581939 0.9817482829 + 0.9885674119 0.9822931886 0.9821749926 + 0.9888523221 0.9827281237 0.9826018214 + 0.9891372919 0.9831629992 0.9830284715 + 0.9894222021 0.9835978746 0.9834551811 + 0.9896981120 0.9840263128 0.9838774800 + 0.9899675846 0.9844464064 0.9842901230 + 0.9902371764 0.9848663807 0.9847027063 + 0.9905067086 0.9852864146 0.9851151705 + 0.9907763004 0.9857063890 0.9855278134 + 0.9910457730 0.9861264825 0.9859403968 + 0.9913154244 0.9865465164 0.9863529205 + 0.9915848970 0.9869664907 0.9867655039 + 0.9918544888 0.9873865843 0.9871780276 + 0.9921240211 0.9878066182 0.9875906110 + 0.9923936129 0.9882265925 0.9880031943 + 0.9926524758 0.9886332750 0.9884101152 + 0.9929050803 0.9890270233 0.9888089895 + 0.9931576252 0.9894207120 0.9892079830 + 0.9934101105 0.9898144007 0.9896069169 + 0.9936627150 0.9902082086 0.9900057912 + 0.9939152002 0.9906018972 0.9904047251 + 0.9941678047 0.9909955859 0.9908037186 + 0.9944202900 0.9913892746 0.9912025928 + 0.9946727753 0.9917830229 0.9916015267 + 0.9949253798 0.9921767116 0.9920004010 + 0.9951779246 0.9925704002 0.9923992753 + 0.9954192042 0.9929419160 0.9927871823 + 0.9956508279 0.9932870269 0.9931579232 + 0.9958823919 0.9936321974 0.9935287237 + 0.9961140156 0.9939773083 0.9938995242 + 0.9963455796 0.9943224788 0.9942703247 + 0.9965772033 0.9946675897 0.9946411252 + 0.9968088269 0.9950128198 0.9950119257 + 0.9970403910 0.9953578711 0.9953826070 + 0.9972720146 0.9957031012 0.9957534075 + 0.9975035191 0.9960482121 0.9961242080 + 0.9977350831 0.9963933825 0.9964950085 + 0.9979518056 0.9967247844 0.9968441129 + 0.9981380105 0.9970226288 0.9971309900 + 0.9983242154 0.9973202944 0.9974179268 + 0.9985104203 0.9976180196 0.9977048039 + 0.9986966252 0.9979158044 0.9979916811 + 0.9988827705 0.9982135296 0.9982786179 + 0.9990689754 0.9985113144 0.9985654950 + 0.9992551804 0.9988089800 0.9988523722 + 0.9994413853 0.9991068244 0.9991393089 + 0.9996275902 0.9994044900 0.9994261861 + 0.9998137951 0.9997022748 0.9997131228 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kodachrome_64.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kodachrome_64.spi1d index 1de002a9..6a547bc3 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kodachrome_64.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_kodachrome_64.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0001951461 0.0001957432 0.0001951461 - 0.0003971827 0.0004013765 0.0003971827 - 0.0006093314 0.0006168035 0.0006093314 - 0.0008331265 0.0008421433 0.0008331265 - 0.0010693250 0.0010800430 0.0010693250 - 0.0013165741 0.0013327420 0.0013165741 - 0.0015723180 0.0015964990 0.0015723180 - 0.0018371070 0.0018691909 0.0018371070 - 0.0021111390 0.0021534991 0.0021111390 - 0.0023975959 0.0024501081 0.0023975959 - 0.0026947369 0.0027620669 0.0026947369 - 0.0030043761 0.0030861360 0.0030043761 - 0.0033243729 0.0034196740 0.0033243729 - 0.0036564060 0.0037631679 0.0036564060 - 0.0040000519 0.0041189152 0.0040000519 - 0.0043583522 0.0044872048 0.0043583522 - 0.0047301622 0.0048662322 0.0047301622 - 0.0051159370 0.0052598631 0.0051159370 - 0.0055168238 0.0056668199 0.0055168238 - 0.0059318189 0.0060855579 0.0059318189 - 0.0063606128 0.0065190848 0.0063606128 - 0.0068039121 0.0069621201 0.0068039121 - 0.0072610150 0.0074171298 0.0072610150 - 0.0077324822 0.0078812325 0.0077324822 - 0.0082186935 0.0083566681 0.0082186935 - 0.0087195607 0.0088426983 0.0087195607 - 0.0092373807 0.0093412735 0.0092373807 - 0.0097713200 0.0098493528 0.0097713200 - 0.0103215296 0.0103719197 0.0103215296 - 0.0108859697 0.0109069496 0.0108859697 - 0.0114682503 0.0114534898 0.0114682503 - 0.0120688202 0.0120149804 0.0120688202 - 0.0126871103 0.0125900703 0.0126871103 - 0.0133261904 0.0131813204 0.0133261904 - 0.0139810601 0.0137880603 0.0139810601 - 0.0146569004 0.0144142900 0.0146569004 - 0.0153518198 0.0150552802 0.0153518198 - 0.0160680693 0.0157083794 0.0160680693 - 0.0168081895 0.0163766202 0.0168081895 - 0.0175653305 0.0170597993 0.0175653305 - 0.0183405094 0.0177551508 0.0183405094 - 0.0191343091 0.0184656195 0.0191343091 - 0.0199443996 0.0191936009 0.0199443996 - 0.0207724404 0.0199350491 0.0207724404 - 0.0216247104 0.0206901692 0.0216247104 - 0.0224967394 0.0214668401 0.0224967394 - 0.0233900901 0.0222598594 0.0233900901 - 0.0243038405 0.0230733994 0.0243038405 - 0.0252429191 0.0239060391 0.0252429191 - 0.0262041409 0.0247574691 0.0262041409 - 0.0271870699 0.0256274194 0.0271870699 - 0.0281923302 0.0265152995 0.0281923302 - 0.0292217396 0.0274243094 0.0292217396 - 0.0302699897 0.0283501595 0.0302699897 - 0.0313403010 0.0292933993 0.0313403010 - 0.0324379094 0.0302557293 0.0324379094 - 0.0335581601 0.0312350094 0.0335581601 - 0.0347037092 0.0322324112 0.0347037092 - 0.0358735509 0.0332520008 0.0358735509 - 0.0370708592 0.0342949703 0.0370708592 - 0.0382963195 0.0353550985 0.0382963195 - 0.0395508707 0.0364397392 0.0395508707 - 0.0408340394 0.0375451893 0.0408340394 - 0.0421415716 0.0386712812 0.0421415716 - 0.0434691012 0.0398181006 0.0434691012 - 0.0448197499 0.0409855917 0.0448197499 - 0.0461917110 0.0421767011 0.0461917110 - 0.0475859195 0.0433861688 0.0475859195 - 0.0489974618 0.0446146913 0.0489974618 - 0.0504221395 0.0458587706 0.0504221395 - 0.0518664904 0.0471218303 0.0518664904 - 0.0533314086 0.0484039001 0.0533314086 - 0.0548129790 0.0497035906 0.0548129790 - 0.0563097298 0.0510205887 0.0563097298 - 0.0578255616 0.0523589589 0.0578255616 - 0.0593563803 0.0537226684 0.0593563803 - 0.0609117001 0.0551059917 0.0609117001 - 0.0624887496 0.0565031916 0.0624887496 - 0.0640849099 0.0579188913 0.0640849099 - 0.0657032207 0.0593533292 0.0657032207 - 0.0673418716 0.0608051494 0.0673418716 - 0.0690022632 0.0622783490 0.0690022632 - 0.0706820935 0.0637728870 0.0706820935 - 0.0723833889 0.0652925819 0.0723833889 - 0.0741088763 0.0668374375 0.0741088763 - 0.0758581236 0.0684004799 0.0758581236 - 0.0776309818 0.0699783191 0.0776309818 - 0.0794283003 0.0715712607 0.0794283003 - 0.0812476426 0.0731753632 0.0812476426 - 0.0830830187 0.0747953877 0.0830830187 - 0.0849313214 0.0764375329 0.0849313214 - 0.0867945403 0.0780991465 0.0867945403 - 0.0886720195 0.0797795132 0.0886720195 - 0.0905641615 0.0814804882 0.0905641615 - 0.0924730599 0.0832031295 0.0924730599 - 0.0944008604 0.0849441215 0.0944008604 - 0.0963497087 0.0866999105 0.0963497087 - 0.0983169824 0.0884718299 0.0983169824 - 0.1002992019 0.0902594104 0.1002992019 - 0.1022920981 0.0920619667 0.1022920981 - 0.1043059006 0.0938806906 0.1043059006 - 0.1063338965 0.0957234576 0.1063338965 - 0.1083754003 0.0975851119 0.1083754003 - 0.1104298979 0.0994653329 0.1104298979 - 0.1125015020 0.1013638005 0.1125015020 - 0.1145863980 0.1032824963 0.1145863980 - 0.1166832000 0.1052192971 0.1166832000 - 0.1187919974 0.1071742028 0.1187919974 - 0.1209281981 0.1091483980 0.1209281981 - 0.1230840012 0.1111427024 0.1230840012 - 0.1252560019 0.1131450012 0.1252560019 - 0.1274449974 0.1151549965 0.1274449974 - 0.1296389997 0.1171758026 0.1296389997 - 0.1318369955 0.1192134991 0.1318369955 - 0.1340391934 0.1212674007 0.1340391934 - 0.1362587065 0.1233419999 0.1362587065 - 0.1384842992 0.1254397035 0.1384842992 - 0.1407155991 0.1275476962 0.1407155991 - 0.1429519951 0.1296651959 0.1429519951 - 0.1451932937 0.1317926049 0.1451932937 - 0.1474387050 0.1339260936 0.1474387050 - 0.1496894062 0.1360657066 0.1496894062 - 0.1519508958 0.1382188946 0.1519508958 - 0.1542215943 0.1403802931 0.1542215943 - 0.1564984024 0.1425506026 0.1564984024 - 0.1587806046 0.1447318047 0.1587806046 - 0.1610693038 0.1469233930 0.1610693038 - 0.1633670926 0.1491298974 0.1633670926 - 0.1656686068 0.1513458043 0.1656686068 - 0.1679760069 0.1535694003 0.1679760069 - 0.1702944934 0.1558119059 0.1702944934 - 0.1726182997 0.1580559015 0.1726182997 - 0.1749459952 0.1603008062 0.1749459952 - 0.1772750020 0.1625521928 0.1772750020 - 0.1796054989 0.1648124009 0.1796054989 - 0.1819469035 0.1670871973 0.1819469035 - 0.1842930019 0.1693769991 0.1842930019 - 0.1866527051 0.1716732979 0.1866527051 - 0.1890151054 0.1739847064 0.1890151054 - 0.1913814992 0.1763011068 0.1913814992 - 0.1937530041 0.1786264926 0.1937530041 - 0.1961268932 0.1809585989 0.1961268932 - 0.1984995008 0.1832942963 0.1984995008 - 0.2008748949 0.1856396049 0.2008748949 - 0.2032615989 0.1879891008 0.2032615989 - 0.2056463957 0.1903553009 0.2056463957 - 0.2080287933 0.1927254945 0.2080287933 - 0.2104109973 0.1951016933 0.2104109973 - 0.2127932012 0.1974837035 0.2127932012 - 0.2151795030 0.1998693049 0.2151795030 - 0.2175665051 0.2022628039 0.2175665051 - 0.2199523002 0.2046581060 0.2199523002 - 0.2223390937 0.2070544064 0.2223390937 - 0.2247312069 0.2094524950 0.2247312069 - 0.2271240950 0.2118602991 0.2271240950 - 0.2295188010 0.2142691016 0.2295188010 - 0.2319131047 0.2166801989 0.2319131047 - 0.2343070954 0.2190932930 0.2343070954 - 0.2366984040 0.2215093970 0.2366984040 - 0.2390874028 0.2239297926 0.2390874028 - 0.2414730936 0.2263545990 0.2414730936 - 0.2438565046 0.2287807018 0.2438565046 - 0.2462437004 0.2312079072 0.2462437004 - 0.2486331016 0.2336384058 0.2486331016 - 0.2510172129 0.2360707074 0.2510172129 - 0.2533988059 0.2385022044 0.2533988059 - 0.2557758093 0.2409331948 0.2557758093 - 0.2581504881 0.2433750033 0.2581504881 - 0.2605293095 0.2458223999 0.2605293095 - 0.2629100978 0.2482777983 0.2629100978 - 0.2652882934 0.2507374883 0.2652882934 - 0.2676649988 0.2532019913 0.2676649988 - 0.2700409889 0.2556692958 0.2700409889 - 0.2724165916 0.2581461072 0.2724165916 - 0.2747870982 0.2606298029 0.2747870982 - 0.2771523893 0.2631142139 0.2771523893 - 0.2795102894 0.2655993104 0.2795102894 - 0.2818570137 0.2680785954 0.2818570137 - 0.2842001915 0.2705487907 0.2842001915 - 0.2865338027 0.2730177939 0.2865338027 - 0.2888650894 0.2754839063 0.2888650894 - 0.2911804914 0.2779509127 0.2911804914 - 0.2934958041 0.2804240882 0.2934958041 - 0.2957996130 0.2828972936 0.2957996130 - 0.2981027961 0.2853642106 0.2981027961 - 0.3004046082 0.2878307104 0.3004046082 - 0.3027057946 0.2902925909 0.3027057946 - 0.3050028980 0.2927531898 0.3050028980 - 0.3072963953 0.2952114940 0.3072963953 - 0.3095870912 0.2976680100 0.3095870912 - 0.3118711114 0.3001233041 0.3118711114 - 0.3141548038 0.3025760055 0.3141548038 - 0.3164271116 0.3050285876 0.3164271116 - 0.3186993897 0.3074803948 0.3186993897 - 0.3209618926 0.3099322915 0.3209618926 - 0.3232209980 0.3123745024 0.3232209980 - 0.3254742026 0.3148137033 0.3254742026 - 0.3277195096 0.3172554970 0.3277195096 - 0.3299638033 0.3197003901 0.3299638033 - 0.3321967125 0.3221448064 0.3321967125 - 0.3344297111 0.3245866895 0.3344297111 - 0.3366633058 0.3270285130 0.3366633058 - 0.3388971984 0.3294712901 0.3388971984 - 0.3411270976 0.3319145143 0.3411270976 - 0.3433490098 0.3343560100 0.3433490098 - 0.3455708921 0.3367947936 0.3455708921 - 0.3477726877 0.3392334878 0.3477726877 - 0.3499726951 0.3416740894 0.3499726951 - 0.3521771133 0.3441148996 0.3521771133 - 0.3543866873 0.3465546072 0.3543866873 - 0.3565962017 0.3489935100 0.3565962017 - 0.3587929904 0.3514319956 0.3587929904 - 0.3609898984 0.3538624048 0.3609898984 - 0.3631897867 0.3562928140 0.3631897867 - 0.3653928041 0.3587242961 0.3653928041 - 0.3675957918 0.3611567020 0.3675957918 - 0.3697830141 0.3635889888 0.3697830141 - 0.3719697893 0.3660191894 0.3719697893 - 0.3741520047 0.3684493899 0.3741520047 - 0.3763279915 0.3708696961 0.3763279915 - 0.3785040975 0.3732793927 0.3785040975 - 0.3806638122 0.3756890893 0.3806638122 - 0.3828206062 0.3780809045 0.3828206062 - 0.3849751055 0.3804706931 0.3849751055 - 0.3871222138 0.3828578889 0.3871222138 - 0.3892692924 0.3852390945 0.3892692924 - 0.3914063871 0.3876202106 0.3914063871 - 0.3935371041 0.3899951875 0.3935371041 - 0.3956677020 0.3923670948 0.3956677020 - 0.3977789879 0.3947389126 0.3977789879 - 0.3998892009 0.3970890045 0.3998892009 - 0.4019930959 0.3994390965 0.4019930959 - 0.4040785134 0.4017911851 0.4040785134 - 0.4061638117 0.4041475952 0.4061638117 - 0.4082497060 0.4065040052 0.4082497060 - 0.4103358984 0.4088486135 0.4103358984 - 0.4124220908 0.4111852944 0.4124220908 - 0.4144963026 0.4135220051 0.4144963026 - 0.4165670872 0.4158436060 0.4165670872 - 0.4186378121 0.4181624949 0.4186378121 - 0.4206883013 0.4204812944 0.4206883013 - 0.4227387011 0.4227986038 0.4227387011 - 0.4247851968 0.4251160026 0.4247851968 - 0.4268149137 0.4274305999 0.4268149137 - 0.4288446903 0.4297378063 0.4288446903 - 0.4308727980 0.4320450127 0.4308727980 - 0.4328976870 0.4343562126 0.4328976870 - 0.4349226058 0.4366728067 0.4349226058 - 0.4369452894 0.4389894009 0.4369452894 - 0.4389654100 0.4412983954 0.4389654100 - 0.4409855008 0.4436013103 0.4409855008 - 0.4429948926 0.4459043145 0.4429948926 - 0.4449959099 0.4482020140 0.4449959099 - 0.4469969869 0.4504969120 0.4469969869 - 0.4489865005 0.4527918994 0.4489865005 - 0.4509691894 0.4550856948 0.4509691894 - 0.4529519081 0.4573791921 0.4529519081 - 0.4549242854 0.4596726894 0.4549242854 - 0.4568913877 0.4619601071 0.4568913877 - 0.4588586092 0.4642454982 0.4588586092 - 0.4608167112 0.4665308893 0.4608167112 - 0.4627703130 0.4688045979 0.4627703130 - 0.4647239149 0.4710747004 0.4647239149 - 0.4666719139 0.4733447134 0.4666719139 - 0.4686164856 0.4756112993 0.4686164856 - 0.4705610871 0.4778766036 0.4705610871 - 0.4724955857 0.4801419973 0.4724955857 - 0.4744223058 0.4823893905 0.4744223058 - 0.4763489962 0.4846282899 0.4763489962 - 0.4782694876 0.4868671000 0.4782694876 - 0.4801832139 0.4890891910 0.4801832139 - 0.4820967913 0.4912990928 0.4820967913 - 0.4840039015 0.4935089946 0.4840039015 - 0.4858981967 0.4957109988 0.4858981967 - 0.4877924025 0.4979032874 0.4877924025 - 0.4896839857 0.5000956059 0.4896839857 - 0.4915628135 0.5022779703 0.4915628135 - 0.4934414923 0.5044360161 0.4934414923 - 0.4953202009 0.5065940022 0.4953202009 - 0.4971916080 0.5087499022 0.4971916080 - 0.4990628064 0.5108857751 0.4990628064 - 0.5009341240 0.5130217075 0.5009341240 - 0.5027933121 0.5151576996 0.5027933121 - 0.5046482086 0.5172629952 0.5046482086 - 0.5065031052 0.5193634033 0.5065031052 - 0.5083466172 0.5214638710 0.5083466172 - 0.5101770759 0.5235527754 0.5101770759 - 0.5120074749 0.5256341100 0.5120074749 - 0.5138359070 0.5277153850 0.5138359070 - 0.5156533122 0.5297918916 0.5156533122 - 0.5174707174 0.5318573117 0.5174707174 - 0.5192881227 0.5339226127 0.5192881227 - 0.5211005211 0.5359879732 0.5211005211 - 0.5229117274 0.5380240083 0.5229117274 - 0.5247228742 0.5400587916 0.5247228742 - 0.5265293121 0.5420935750 0.5265293121 - 0.5283294916 0.5441210866 0.5283294916 - 0.5301296711 0.5461434126 0.5301296711 - 0.5319293737 0.5481657982 0.5319293737 - 0.5337138772 0.5501838923 0.5337138772 - 0.5354983807 0.5521820784 0.5354983807 - 0.5372828841 0.5541803241 0.5372828841 - 0.5390647054 0.5561785102 0.5390647054 - 0.5408445001 0.5581638813 0.5408445001 - 0.5426242948 0.5601447225 0.5426242948 - 0.5444021821 0.5621253848 0.5444021821 - 0.5461611152 0.5640997291 0.5461611152 - 0.5479199290 0.5660542846 0.5479199290 - 0.5496788025 0.5680087805 0.5496788025 - 0.5514246821 0.5699632764 0.5514246821 - 0.5531604886 0.5718948841 0.5531604886 - 0.5548964143 0.5738177896 0.5548964143 - 0.5566322207 0.5757405758 0.5566322207 - 0.5583571196 0.5776597857 0.5583571196 - 0.5600818992 0.5795599818 0.5600818992 - 0.5618067980 0.5814602971 0.5618067980 - 0.5635148287 0.5833604932 0.5635148287 - 0.5651975274 0.5852445960 0.5651975274 - 0.5668801069 0.5871161819 0.5668801069 - 0.5685628057 0.5889877081 0.5685628057 - 0.5702366233 0.5908592939 0.5702366233 - 0.5719075203 0.5926998258 0.5719075203 - 0.5735784769 0.5945379734 0.5735784769 - 0.5752487183 0.5963761210 0.5752487183 - 0.5769106746 0.5982087851 0.5769106746 - 0.5785726905 0.6000252962 0.5785726905 - 0.5802345872 0.6018419266 0.5802345872 - 0.5818874240 0.6036584973 0.5818874240 - 0.5835233927 0.6054543257 0.5835233927 - 0.5851594210 0.6072319746 0.5851594210 - 0.5867953897 0.6090096831 0.5867953897 - 0.5884160995 0.6107872725 0.5884160995 - 0.5900263786 0.6125335097 0.5900263786 - 0.5916365981 0.6142706275 0.5916365981 - 0.5932468176 0.6160078049 0.5932468176 - 0.5948414207 0.6177449226 0.5948414207 - 0.5964319110 0.6194577217 0.5964319110 - 0.5980222821 0.6211699843 0.5980222821 - 0.5996127725 0.6228823066 0.5996127725 - 0.6011689901 0.6245880127 0.6011689901 - 0.6027231812 0.6262522936 0.6027231812 - 0.6042773724 0.6279166937 0.6042773724 - 0.6058307290 0.6295809746 0.6058307290 - 0.6073688269 0.6312366724 0.6073688269 - 0.6089069843 0.6328672767 0.6089069843 - 0.6104450822 0.6344978213 0.6104450822 - 0.6119816899 0.6361284256 0.6119816899 - 0.6135081053 0.6377481818 0.6135081053 - 0.6150345206 0.6393465996 0.6150345206 - 0.6165608764 0.6409450173 0.6165608764 - 0.6180859804 0.6425433755 0.6180859804 - 0.6196045876 0.6441305876 0.6196045876 - 0.6211233139 0.6456986070 0.6211233139 - 0.6226419210 0.6472666860 0.6226419210 - 0.6241582036 0.6488348246 0.6241582036 - 0.6256613135 0.6503942013 0.6256613135 - 0.6271644831 0.6519383788 0.6271644831 - 0.6286675930 0.6534824967 0.6286675930 - 0.6301684976 0.6550266743 0.6301684976 - 0.6316462159 0.6565703154 0.6316462159 - 0.6331238747 0.6581128836 0.6331238747 - 0.6346017122 0.6596555114 0.6346017122 - 0.6360793710 0.6611980796 0.6360793710 - 0.6375383735 0.6627355218 0.6375383735 - 0.6389971972 0.6642531753 0.6389971972 - 0.6404560208 0.6657708883 0.6404560208 - 0.6419147849 0.6672887206 0.6419147849 - 0.6433644891 0.6688051224 0.6433644891 - 0.6448125243 0.6703053713 0.6448125243 - 0.6462605000 0.6718056202 0.6462605000 - 0.6477084756 0.6733058095 0.6477084756 - 0.6491544247 0.6748061180 0.6491544247 - 0.6505993009 0.6762984991 0.6505993009 - 0.6520441771 0.6777899265 0.6520441771 - 0.6534891129 0.6792812943 0.6534891129 - 0.6549304724 0.6807727218 0.6549304724 - 0.6563671231 0.6822533011 0.6563671231 - 0.6578037143 0.6837282181 0.6578037143 - 0.6592403054 0.6852030158 0.6592403054 - 0.6606746912 0.6866778731 0.6606746912 - 0.6620953083 0.6881393194 0.6620953083 - 0.6635159254 0.6895787120 0.6635159254 - 0.6649364829 0.6910182238 0.6649364829 - 0.6663571000 0.6924576163 0.6663571000 - 0.6677600145 0.6938955784 0.6677600145 - 0.6691588163 0.6953092813 0.6691588163 - 0.6705574989 0.6967229843 0.6705574989 - 0.6719563007 0.6981366873 0.6719563007 - 0.6733427048 0.6995503902 0.6733427048 - 0.6747133136 0.7009537220 0.6747133136 - 0.6760839224 0.7023522854 0.6760839224 - 0.6774544120 0.7037507892 0.6774544120 - 0.6788250208 0.7051494122 0.6788250208 - 0.6801913977 0.7065389752 0.6801913977 - 0.6815578938 0.7079041004 0.6815578938 - 0.6829243898 0.7092692852 0.6829243898 - 0.6842908859 0.7106345296 0.6842908859 - 0.6856526136 0.7119995952 0.6856526136 - 0.6870101094 0.7133480906 0.6870101094 - 0.6883677244 0.7146925926 0.6883677244 - 0.6897252202 0.7160370946 0.6897252202 - 0.6910821199 0.7173815966 0.6910821199 - 0.6924132109 0.7187203765 0.6924132109 - 0.6937443018 0.7200453877 0.6937443018 - 0.6950753927 0.7213705182 0.6950753927 - 0.6964064837 0.7226955295 0.6964064837 - 0.6977304816 0.7240206003 0.6977304816 - 0.6990460753 0.7253168225 0.6990460753 - 0.7003617287 0.7266023755 0.7003617287 - 0.7016773224 0.7278879881 0.7016773224 - 0.7029929161 0.7291736007 0.7029929161 - 0.7043045759 0.7304561734 0.7043045759 - 0.7056154013 0.7317163944 0.7056154013 - 0.7069262266 0.7329766154 0.7069262266 - 0.7082371116 0.7342368960 0.7082371116 - 0.7095441818 0.7354971170 0.7095441818 - 0.7108336091 0.7367405891 0.7108336091 - 0.7121229768 0.7379640937 0.7121229768 - 0.7134124041 0.7391875982 0.7134124041 - 0.7147017717 0.7404111028 0.7147017717 - 0.7159798741 0.7416346073 0.7159798741 - 0.7172445059 0.7428414226 0.7172445059 - 0.7185090184 0.7440423965 0.7185090184 - 0.7197735906 0.7452433705 0.7197735906 - 0.7210381031 0.7464445233 0.7210381031 - 0.7222943902 0.7476454973 0.7222943902 - 0.7235469222 0.7488263845 0.7235469222 - 0.7247992754 0.7500069737 0.7247992754 - 0.7260518074 0.7511875033 0.7260518074 - 0.7273042202 0.7523680925 0.7273042202 - 0.7285580039 0.7535430193 0.7285580039 - 0.7298120260 0.7546893954 0.7298120260 - 0.7310661077 0.7558357120 0.7310661077 - 0.7323200703 0.7569820881 0.7323200703 - 0.7335740924 0.7581285238 0.7335740924 - 0.7348139286 0.7592620850 0.7348139286 - 0.7360535264 0.7603654861 0.7360535264 - 0.7372931838 0.7614690065 0.7372931838 - 0.7385329008 0.7625724077 0.7385329008 - 0.7397707105 0.7636758089 0.7397707105 - 0.7409819961 0.7647761106 0.7409819961 - 0.7421932817 0.7658712268 0.7421932817 - 0.7434046865 0.7669662833 0.7434046865 - 0.7446159720 0.7680615187 0.7446159720 - 0.7458249927 0.7691565752 0.7458249927 - 0.7470089793 0.7702389956 0.7470089793 - 0.7481930852 0.7713027000 0.7481930852 - 0.7493770719 0.7723662853 0.7493770719 - 0.7505611181 0.7734299898 0.7505611181 - 0.7517436147 0.7744936943 0.7517436147 - 0.7528991103 0.7755483985 0.7528991103 - 0.7540546060 0.7765882015 0.7540546060 - 0.7552101016 0.7776281238 0.7552101016 - 0.7563655972 0.7786679268 0.7563655972 - 0.7575210929 0.7797077894 0.7575210929 - 0.7586535215 0.7807422280 0.7586535215 - 0.7597848773 0.7817639112 0.7597848773 - 0.7609164119 0.7827855945 0.7609164119 - 0.7620478868 0.7838072181 0.7620478868 - 0.7631794214 0.7848289013 0.7631794214 - 0.7643039823 0.7858467102 0.7643039823 - 0.7654271126 0.7868434787 0.7654271126 - 0.7665501833 0.7878404260 0.7665501833 - 0.7676733136 0.7888373137 0.7676733136 - 0.7687965035 0.7898342013 0.7687965035 - 0.7699062824 0.7908310890 0.7699062824 - 0.7710080147 0.7917973995 0.7710080147 - 0.7721098065 0.7927626967 0.7721098065 - 0.7732115984 0.7937279940 0.7732115984 - 0.7743132710 0.7946931720 0.7743132710 - 0.7754092216 0.7956585288 0.7754092216 - 0.7764948010 0.7966089845 0.7764948010 - 0.7775803804 0.7975537777 0.7775803804 - 0.7786660790 0.7984985113 0.7786660790 - 0.7797517180 0.7994433045 0.7797517180 - 0.7808365822 0.8003879786 0.7808365822 - 0.7819091082 0.8013278842 0.7819091082 - 0.7829815745 0.8022608757 0.7829815745 - 0.7840539813 0.8031939268 0.7840539813 - 0.7851265073 0.8041269183 0.7851265073 - 0.7861989737 0.8050599098 0.7861989737 - 0.7872558832 0.8059914708 0.7872558832 - 0.7883058190 0.8068947196 0.7883058190 - 0.7893555760 0.8077979088 0.7893555760 - 0.7904053926 0.8087010980 0.7904053926 - 0.7914553285 0.8096042871 0.7914553285 - 0.7924997807 0.8105074763 0.7924997807 - 0.7935289741 0.8113902807 0.7935289741 - 0.7945582867 0.8122608066 0.7945582867 - 0.7955874801 0.8131312728 0.7955874801 - 0.7966167927 0.8140019178 0.7966167927 - 0.7976459861 0.8148723841 0.7976459861 - 0.7986531258 0.8157399297 0.7986531258 - 0.7996534705 0.8165895939 0.7996534705 - 0.8006538749 0.8174393773 0.8006538749 - 0.8016542792 0.8182891011 0.8016542792 - 0.8026548028 0.8191388249 0.8026548028 - 0.8036468029 0.8199884892 0.8036468029 - 0.8046095967 0.8208152056 0.8046095967 - 0.8055725098 0.8216269016 0.8055725098 - 0.8065354228 0.8224384785 0.8065354228 - 0.8074982762 0.8232501745 0.8074982762 - 0.8084611893 0.8240618110 0.8084611893 - 0.8094077110 0.8248733878 0.8094077110 - 0.8103441000 0.8256731033 0.8103441000 - 0.8112804890 0.8264726996 0.8112804890 - 0.8122168779 0.8272724152 0.8122168779 - 0.8131533265 0.8280720711 0.8131533265 - 0.8140897155 0.8288717270 0.8140897155 - 0.8150094748 0.8296630979 0.8150094748 - 0.8159283996 0.8304389119 0.8159283996 - 0.8168473244 0.8312146068 0.8168473244 - 0.8177661896 0.8319904208 0.8177661896 - 0.8186851144 0.8327661157 0.8186851144 - 0.8195986152 0.8335418105 0.8195986152 - 0.8204926848 0.8343117237 0.8204926848 - 0.8213868737 0.8350778818 0.8213868737 - 0.8222811222 0.8358440995 0.8222811222 - 0.8231751919 0.8366103768 0.8231751919 - 0.8240693808 0.8373765945 0.8240693808 - 0.8249509931 0.8381428719 0.8249509931 - 0.8258157969 0.8389006257 0.8258157969 - 0.8266806006 0.8396567106 0.8266806006 - 0.8275455236 0.8404127955 0.8275455236 - 0.8284103274 0.8411688805 0.8284103274 - 0.8292750716 0.8419250250 0.8292750716 - 0.8301308751 0.8426809907 0.8301308751 - 0.8309800029 0.8434000015 0.8309800029 - 0.8318290710 0.8441187143 0.8318290710 - 0.8326780796 0.8448373079 0.8326780796 - 0.8335272074 0.8455560207 0.8335272074 - 0.8343762755 0.8462746143 0.8343762755 - 0.8352112174 0.8469917774 0.8352112174 - 0.8360387087 0.8476917148 0.8360387087 - 0.8368662000 0.8483915925 0.8368662000 - 0.8376936913 0.8490914702 0.8376936913 - 0.8385211825 0.8497914076 0.8385211825 - 0.8393486738 0.8504914045 0.8393486738 - 0.8401600718 0.8511893749 0.8401600718 - 0.8409640193 0.8518716097 0.8409640193 - 0.8417677879 0.8525537848 0.8417677879 - 0.8425716162 0.8532360196 0.8425716162 - 0.8433753848 0.8539181948 0.8433753848 - 0.8441792130 0.8546004295 0.8441792130 - 0.8449677825 0.8552805781 0.8449677825 - 0.8457477093 0.8559337258 0.8457477093 - 0.8465275764 0.8565866947 0.8465275764 - 0.8473075032 0.8572397828 0.8473075032 - 0.8480873704 0.8578928709 0.8480873704 - 0.8488671780 0.8585458994 0.8488671780 - 0.8496319056 0.8591989875 0.8496319056 - 0.8503823876 0.8598427773 0.8503823876 - 0.8511329889 0.8604860902 0.8511329889 - 0.8518834710 0.8611295223 0.8518834710 - 0.8526340723 0.8617728949 0.8526340723 - 0.8533846736 0.8624163270 0.8533846736 - 0.8541216850 0.8630596995 0.8541216850 - 0.8548328876 0.8636872172 0.8548328876 - 0.8555439711 0.8643105030 0.8555439711 - 0.8562551737 0.8649337888 0.8562551737 - 0.8569663167 0.8655570149 0.8569663167 - 0.8576774001 0.8661803007 0.8576774001 - 0.8583872914 0.8668035865 0.8583872914 - 0.8590850830 0.8674191236 0.8590850830 - 0.8597828150 0.8680282831 0.8597828150 - 0.8604806066 0.8686373830 0.8604806066 - 0.8611783981 0.8692466021 0.8611783981 - 0.8618761897 0.8698558211 0.8618761897 - 0.8625739813 0.8704649806 0.8625739813 - 0.8632513881 0.8710712790 0.8632513881 - 0.8639233112 0.8716691136 0.8639233112 - 0.8645951748 0.8722668886 0.8645951748 - 0.8652670979 0.8728647232 0.8652670979 - 0.8659390211 0.8734624982 0.8659390211 - 0.8666108847 0.8740602732 0.8666108847 - 0.8672788739 0.8746581078 0.8672788739 - 0.8679410815 0.8752396703 0.8679410815 - 0.8686032891 0.8758190274 0.8686032891 - 0.8692653775 0.8763983250 0.8692653775 - 0.8699275851 0.8769776225 0.8699275851 - 0.8705897927 0.8775568008 0.8705897927 - 0.8712518811 0.8781360984 0.8712518811 - 0.8719059825 0.8787040114 0.8719059825 - 0.8725597262 0.8792570233 0.8725597262 - 0.8732132912 0.8798100948 0.8732132912 - 0.8738669753 0.8803631067 0.8738669753 - 0.8745207191 0.8809161186 0.8745207191 - 0.8751742840 0.8814691901 0.8751742840 - 0.8758152127 0.8820222020 0.8758152127 - 0.8764392734 0.8825591207 0.8764392734 - 0.8770633936 0.8830946088 0.8770633936 - 0.8776875138 0.8836302161 0.8776875138 - 0.8783116937 0.8841657043 0.8783116937 - 0.8789358139 0.8847011924 0.8789358139 - 0.8795598745 0.8852367997 0.8795598745 - 0.8801736236 0.8857638836 0.8801736236 - 0.8807854056 0.8862745762 0.8807854056 - 0.8813971281 0.8867853284 0.8813971281 - 0.8820089102 0.8872959018 0.8820089102 - 0.8826205730 0.8878065944 0.8826205730 - 0.8832324147 0.8883172870 0.8832324147 - 0.8838424087 0.8888279796 0.8838424087 - 0.8844444752 0.8893265724 0.8844444752 - 0.8850466013 0.8898196220 0.8850466013 - 0.8856487274 0.8903126717 0.8856487274 - 0.8862507939 0.8908057213 0.8862507939 - 0.8868529201 0.8912987709 0.8868529201 - 0.8874549866 0.8917918205 0.8874549866 - 0.8880429268 0.8922848105 0.8880429268 - 0.8886129260 0.8927574158 0.8886129260 - 0.8891829252 0.8932290077 0.8891829252 - 0.8897529840 0.8937007189 0.8897529840 - 0.8903229833 0.8941723108 0.8903229833 - 0.8908931017 0.8946439028 0.8908931017 - 0.8914631009 0.8951156139 0.8914631009 - 0.8920254111 0.8955851793 0.8920254111 - 0.8925833702 0.8960440755 0.8925833702 - 0.8931413889 0.8965029716 0.8931413889 - 0.8936994076 0.8969618082 0.8936994076 - 0.8942574263 0.8974207044 0.8942574263 - 0.8948153257 0.8978796005 0.8948153257 - 0.8953732848 0.8983384967 0.8953732848 - 0.8959251046 0.8987923265 0.8959251046 - 0.8964747787 0.8992335200 0.8964747787 - 0.8970245719 0.8996747136 0.8970245719 - 0.8975743055 0.9001159072 0.8975743055 - 0.8981240988 0.9005571008 0.8981240988 - 0.8986737728 0.9009982944 0.8986737728 - 0.8992236257 0.9014394283 0.8992236257 - 0.8997610211 0.9018741846 0.8997610211 - 0.9002953172 0.9022963047 0.9002953172 - 0.9008296132 0.9027183056 0.9008296132 - 0.9013640285 0.9031404257 0.9013640285 - 0.9018983245 0.9035624266 0.9018983245 - 0.9024326205 0.9039844871 0.9024326205 - 0.9029669166 0.9044064879 0.9029669166 - 0.9034835100 0.9048255086 0.9034835100 - 0.9039946795 0.9052377939 0.9039946795 - 0.9045059085 0.9056500196 0.9045059085 - 0.9050170779 0.9060623050 0.9050170779 - 0.9055283070 0.9064744711 0.9055283070 - 0.9060394764 0.9068868160 0.9060394764 - 0.9065507054 0.9072989821 0.9065507054 - 0.9070444107 0.9077087045 0.9070444107 - 0.9075291157 0.9081088901 0.9075291157 - 0.9080138206 0.9085091949 0.9080138206 - 0.9084984064 0.9089093804 0.9084984064 - 0.9089831114 0.9093096256 0.9089831114 - 0.9094678164 0.9097098708 0.9094678164 - 0.9099525213 0.9101101160 0.9099525213 - 0.9104328752 0.9105101824 0.9104328752 - 0.9109085202 0.9109075069 0.9109085202 - 0.9113841057 0.9113047719 0.9113841057 - 0.9118596911 0.9117019773 0.9118596911 - 0.9123352170 0.9120993018 0.9123352170 - 0.9128108025 0.9124966264 0.9128108025 - 0.9132863879 0.9128937721 0.9132863879 - 0.9137566090 0.9132910967 0.9137566090 - 0.9142068028 0.9136837125 0.9142068028 - 0.9146568775 0.9140750170 0.9146568775 - 0.9151070714 0.9144663215 0.9151070714 - 0.9155572057 0.9148576260 0.9155572057 - 0.9160073996 0.9152488708 0.9160073996 - 0.9164574742 0.9156401753 0.9164574742 - 0.9169077277 0.9160314798 0.9169077277 - 0.9173452258 0.9164192080 0.9173452258 - 0.9177808166 0.9168025851 0.9177808166 - 0.9182164073 0.9171860218 0.9182164073 - 0.9186519980 0.9175693989 0.9186519980 - 0.9190875292 0.9179527760 0.9190875292 - 0.9195231199 0.9183362126 0.9195231199 - 0.9199587107 0.9187195897 0.9199587107 - 0.9203907251 0.9191030860 0.9203907251 - 0.9208180904 0.9194877148 0.9208180904 - 0.9212455153 0.9198724031 0.9212455153 - 0.9216729999 0.9202570915 0.9216729999 - 0.9221004248 0.9206417203 0.9221004248 - 0.9225277901 0.9210264087 0.9225277901 - 0.9229552746 0.9214110970 0.9229552746 - 0.9233826995 0.9217957258 0.9233826995 - 0.9237878919 0.9221795201 0.9237878919 - 0.9241918921 0.9225625992 0.9241918921 - 0.9245958924 0.9229457974 0.9245958924 - 0.9249997735 0.9233288765 0.9249997735 - 0.9254037738 0.9237120152 0.9254037738 - 0.9258077741 0.9240950942 0.9258077741 - 0.9262117743 0.9244781733 0.9262117743 - 0.9266104102 0.9248613119 0.9266104102 - 0.9269998074 0.9252420068 0.9269998074 - 0.9273892045 0.9256225824 0.9273892045 - 0.9277786016 0.9260032177 0.9277786016 - 0.9281679988 0.9263836741 0.9281679988 - 0.9285573959 0.9267643094 0.9285573959 - 0.9289467931 0.9271448851 0.9289467931 - 0.9293361902 0.9275255203 0.9293361902 - 0.9297145009 0.9279053807 0.9297145009 - 0.9300879240 0.9282836914 0.9300879240 - 0.9304612875 0.9286620021 0.9304612875 - 0.9308347106 0.9290403128 0.9308347106 - 0.9312080741 0.9294186831 0.9312080741 - 0.9315814972 0.9297969937 0.9315814972 - 0.9319549203 0.9301753044 0.9319549203 - 0.9323282838 0.9305536151 0.9323282838 - 0.9326866269 0.9309234023 0.9326866269 - 0.9330440164 0.9312868118 0.9330440164 - 0.9334015250 0.9316502810 0.9334015250 - 0.9337589741 0.9320136905 0.9337589741 - 0.9341164827 0.9323772192 0.9341164827 - 0.9344739914 0.9327406287 0.9344739914 - 0.9348315001 0.9331040978 0.9348315001 - 0.9351881146 0.9334675074 0.9351881146 - 0.9355376959 0.9338105917 0.9355376959 - 0.9358873963 0.9341468215 0.9358873963 - 0.9362370968 0.9344829917 0.9362370968 - 0.9365866780 0.9348192215 0.9365866780 - 0.9369363785 0.9351553917 0.9369363785 - 0.9372860789 0.9354916215 0.9372860789 - 0.9376357794 0.9358279109 0.9376357794 - 0.9379813075 0.9361640811 0.9379813075 - 0.9383106828 0.9364855886 0.9383106828 - 0.9386399984 0.9368044734 0.9386399984 - 0.9389693141 0.9371232986 0.9389693141 - 0.9392986298 0.9374421239 0.9392986298 - 0.9396280050 0.9377608895 0.9396280050 - 0.9399573207 0.9380797148 0.9399573207 - 0.9402865767 0.9383985996 0.9402865767 - 0.9406142831 0.9387174249 0.9406142831 - 0.9409353733 0.9390224218 0.9409353733 - 0.9412565231 0.9393249750 0.9412565231 - 0.9415774941 0.9396277070 0.9415774941 - 0.9418985844 0.9399303198 0.9418985844 - 0.9422196746 0.9402328730 0.9422196746 - 0.9425407052 0.9405354857 0.9425407052 - 0.9428617954 0.9408380985 0.9428617954 - 0.9431818128 0.9411407113 0.9431818128 - 0.9434933066 0.9414327741 0.9434933066 - 0.9438048005 0.9417217970 0.9438048005 - 0.9441161752 0.9420107007 0.9441161752 - 0.9444277287 0.9422997236 0.9444277287 - 0.9447392225 0.9425886273 0.9447392225 - 0.9450505972 0.9428775907 0.9450505972 - 0.9453620911 0.9431664944 0.9453620911 - 0.9456735849 0.9434555173 0.9456735849 - 0.9459661841 0.9437438250 0.9459661841 - 0.9462574720 0.9440317750 0.9462574720 - 0.9465487003 0.9443197250 0.9465487003 - 0.9468399882 0.9446076155 0.9468399882 - 0.9471312165 0.9448956251 0.9471312165 - 0.9474225044 0.9451835155 0.9474225044 - 0.9477136731 0.9454715252 0.9477136731 - 0.9480050206 0.9457594156 0.9480050206 - 0.9482976794 0.9460437298 0.9482976794 - 0.9485909939 0.9463211894 0.9485909939 - 0.9488844275 0.9465987086 0.9488844275 - 0.9491778016 0.9468762279 0.9491778016 - 0.9494711757 0.9471536875 0.9494711757 - 0.9497646093 0.9474310875 0.9497646093 - 0.9500579834 0.9477086067 0.9500579834 - 0.9503514171 0.9479861259 0.9503514171 - 0.9506422281 0.9482635856 0.9506422281 - 0.9509274960 0.9485384226 0.9509274960 - 0.9512127042 0.9488133192 0.9512127042 - 0.9514979720 0.9490882158 0.9514979720 - 0.9517831802 0.9493629932 0.9517831802 - 0.9520685077 0.9496378899 0.9520685077 - 0.9523537159 0.9499127269 0.9523537159 - 0.9526389241 0.9501876235 0.9526389241 - 0.9529241920 0.9504625201 0.9529241920 - 0.9532032013 0.9507294893 0.9532032013 - 0.9534810781 0.9509904981 0.9534810781 - 0.9537590742 0.9512513876 0.9537590742 - 0.9540370703 0.9515123963 0.9540370703 - 0.9543150067 0.9517734051 0.9543150067 - 0.9545930028 0.9520342946 0.9545930028 - 0.9548709989 0.9522953033 0.9548709989 - 0.9551489949 0.9525561929 0.9551489949 - 0.9554250240 0.9528167844 0.9554250240 - 0.9556970000 0.9530668259 0.9556970000 - 0.9559689760 0.9533166885 0.9559689760 - 0.9562410712 0.9535666108 0.9562410712 - 0.9565131068 0.9538165927 0.9565131068 - 0.9567850828 0.9540665150 0.9567850828 - 0.9570571184 0.9543164968 0.9570571184 - 0.9573290944 0.9545664191 0.9573290944 - 0.9576011896 0.9548164010 0.9576011896 - 0.9578632116 0.9550620914 0.9578632116 - 0.9581208229 0.9553020000 0.9581208229 - 0.9583783746 0.9555419087 0.9583783746 - 0.9586359859 0.9557818174 0.9586359859 - 0.9588935971 0.9560217261 0.9588935971 - 0.9591512084 0.9562615156 0.9591512084 - 0.9594088197 0.9565014243 0.9594088197 - 0.9596663713 0.9567412734 0.9596663713 - 0.9599239826 0.9569811821 0.9599239826 - 0.9601870775 0.9572148919 0.9601870775 - 0.9604502916 0.9574459791 0.9604502916 - 0.9607136250 0.9576771259 0.9607136250 - 0.9609768987 0.9579082131 0.9609768987 - 0.9612401128 0.9581393003 0.9612401128 - 0.9615033865 0.9583703876 0.9615033865 - 0.9617667198 0.9586014748 0.9617667198 - 0.9620298743 0.9588325024 0.9620298743 - 0.9622896910 0.9590635896 0.9622896910 - 0.9625295997 0.9592813849 0.9625295997 - 0.9627695084 0.9594975710 0.9627695084 - 0.9630094171 0.9597136974 0.9630094171 - 0.9632493258 0.9599298835 0.9632493258 - 0.9634891748 0.9601460099 0.9634891748 - 0.9637290835 0.9603621960 0.9637290835 - 0.9639688730 0.9605783820 0.9639688730 - 0.9642087817 0.9607945085 0.9642087817 - 0.9644483924 0.9610106945 0.9644483924 - 0.9646872282 0.9612244964 0.9646872282 - 0.9649258852 0.9614382982 0.9649258852 - 0.9651646018 0.9616519809 0.9651646018 - 0.9654033184 0.9618657827 0.9654033184 - 0.9656419754 0.9620795846 0.9656419754 - 0.9658808112 0.9622933865 0.9658808112 - 0.9661195278 0.9625071883 0.9661195278 - 0.9663581848 0.9627209902 0.9663581848 - 0.9665971994 0.9629346728 0.9665971994 - 0.9668369889 0.9631499052 0.9668369889 - 0.9670768976 0.9633650780 0.9670768976 - 0.9673168063 0.9635801911 0.9673168063 - 0.9675567150 0.9637954235 0.9675567150 - 0.9677965045 0.9640105963 0.9677965045 - 0.9680364132 0.9642258286 0.9680364132 - 0.9682763219 0.9644410014 0.9682763219 - 0.9685161114 0.9646561146 0.9685161114 - 0.9687551260 0.9648712873 0.9687551260 - 0.9689875245 0.9650797844 0.9689875245 - 0.9692199230 0.9652873874 0.9692199230 - 0.9694522023 0.9654949903 0.9694522023 - 0.9696846008 0.9657025933 0.9696846008 - 0.9699168801 0.9659103155 0.9699168801 - 0.9701492786 0.9661179185 0.9701492786 - 0.9703816175 0.9663255215 0.9703816175 - 0.9706140161 0.9665331244 0.9706140161 - 0.9708464146 0.9667407274 0.9708464146 - 0.9710645080 0.9669466019 0.9710645080 - 0.9712811708 0.9671518207 0.9712811708 - 0.9714977741 0.9673569202 0.9714977741 - 0.9717144966 0.9675620198 0.9717144966 - 0.9719312191 0.9677671790 0.9719312191 - 0.9721478820 0.9679722786 0.9721478820 - 0.9723646045 0.9681774974 0.9723646045 - 0.9725812078 0.9683825970 0.9725812078 - 0.9727978706 0.9685876966 0.9727978706 - 0.9730128050 0.9687932730 0.9730128050 - 0.9732266068 0.9689996243 0.9732266068 - 0.9734402895 0.9692057967 0.9734402895 - 0.9736539721 0.9694120884 0.9736539721 - 0.9738677144 0.9696183205 0.9738677144 - 0.9740815163 0.9698246121 0.9740815163 - 0.9742951989 0.9700307846 0.9742951989 - 0.9745088816 0.9702370763 0.9745088816 - 0.9747226834 0.9704433084 0.9747226834 - 0.9749327898 0.9706496000 0.9749327898 - 0.9751276970 0.9708499908 0.9751276970 - 0.9753227234 0.9710503817 0.9753227234 - 0.9755175710 0.9712507129 0.9755175710 - 0.9757124782 0.9714511037 0.9757124782 - 0.9759075046 0.9716514945 0.9759075046 - 0.9761024117 0.9718518853 0.9761024117 - 0.9762973785 0.9720522761 0.9762973785 - 0.9764922857 0.9722527266 0.9764922857 - 0.9766871929 0.9724531174 0.9766871929 - 0.9768773913 0.9726468921 0.9768773913 - 0.9770653844 0.9728342891 0.9770653844 - 0.9772533774 0.9730216265 0.9772533774 - 0.9774413109 0.9732090235 0.9774413109 - 0.9776293039 0.9733963013 0.9776293039 - 0.9778172970 0.9735835791 0.9778172970 - 0.9780052900 0.9737709761 0.9780052900 - 0.9781932235 0.9739583135 0.9781932235 - 0.9783812165 0.9741457105 0.9783812165 - 0.9785689116 0.9743329883 0.9785689116 - 0.9787533879 0.9745092988 0.9787533879 - 0.9789379239 0.9746842980 0.9789379239 - 0.9791222811 0.9748592973 0.9791222811 - 0.9793068171 0.9750342965 0.9793068171 - 0.9794911742 0.9752092957 0.9794911742 - 0.9796757102 0.9753842950 0.9796757102 - 0.9798601270 0.9755592942 0.9798601270 - 0.9800446033 0.9757342935 0.9800446033 - 0.9802290201 0.9759092927 0.9802290201 - 0.9804099202 0.9760805964 0.9804099202 - 0.9805839062 0.9762361050 0.9805839062 - 0.9807580113 0.9763916731 0.9807580113 - 0.9809321165 0.9765471816 0.9809321165 - 0.9811062217 0.9767026901 0.9811062217 - 0.9812802076 0.9768583179 0.9812802076 - 0.9814543128 0.9770138264 0.9814543128 - 0.9816284180 0.9771692753 0.9816284180 - 0.9818025231 0.9773249030 0.9818025231 - 0.9819766283 0.9774804115 0.9819766283 - 0.9821426272 0.9776375294 0.9821426272 - 0.9823012948 0.9777972102 0.9823012948 - 0.9824600816 0.9779568911 0.9824600816 - 0.9826188087 0.9781165719 0.9826188087 - 0.9827775955 0.9782761931 0.9827775955 - 0.9829363227 0.9784358740 0.9829363227 - 0.9830951095 0.9785956144 0.9830951095 - 0.9832537770 0.9787552953 0.9832537770 - 0.9834125042 0.9789149761 0.9834125042 - 0.9835712910 0.9790747166 0.9835712910 - 0.9837297201 0.9792376757 0.9837297201 - 0.9838877916 0.9794046283 0.9838877916 - 0.9840459824 0.9795715213 0.9840459824 - 0.9842041135 0.9797384143 0.9842041135 - 0.9843623042 0.9799053073 0.9843623042 - 0.9845203757 0.9800723195 0.9845203757 - 0.9846786261 0.9802392125 0.9846786261 - 0.9848366976 0.9804061055 0.9848366976 - 0.9849948883 0.9805729985 0.9849948883 - 0.9851530194 0.9807398915 0.9851530194 - 0.9853115082 0.9809073210 0.9853115082 - 0.9854704142 0.9810754061 0.9854704142 - 0.9856293201 0.9812434912 0.9856293201 - 0.9857881069 0.9814115167 0.9857881069 - 0.9859470129 0.9815796018 0.9859470129 - 0.9861057997 0.9817476869 0.9861057997 - 0.9862647057 0.9819157124 0.9862647057 - 0.9864236116 0.9820837975 0.9864236116 - 0.9865823984 0.9822518826 0.9865823984 - 0.9867413044 0.9824199080 0.9867413044 - 0.9868972898 0.9825896025 0.9868972898 - 0.9870492816 0.9827628732 0.9870492816 - 0.9872012138 0.9829362035 0.9872012138 - 0.9873532057 0.9831094742 0.9873532057 - 0.9875051975 0.9832828045 0.9875051975 - 0.9876571894 0.9834560752 0.9876571894 - 0.9878091812 0.9836292863 0.9878091812 - 0.9879611731 0.9838026166 0.9879611731 - 0.9881131053 0.9839758873 0.9881131053 - 0.9882650971 0.9841492176 0.9882650971 - 0.9884155989 0.9843227863 0.9884155989 - 0.9885584116 0.9845007062 0.9885584116 - 0.9887011051 0.9846786261 0.9887011051 - 0.9888437986 0.9848564863 0.9888437986 - 0.9889864922 0.9850344062 0.9889864922 - 0.9891293049 0.9852123260 0.9891293049 - 0.9892719984 0.9853901863 0.9892719984 - 0.9894146919 0.9855681062 0.9894146919 - 0.9895573854 0.9857460260 0.9895573854 - 0.9897001982 0.9859238863 0.9897001982 - 0.9898428917 0.9861018062 0.9898428917 - 0.9899815917 0.9862813950 0.9899815917 - 0.9901192784 0.9864618182 0.9901192784 - 0.9902570248 0.9866421819 0.9902570248 - 0.9903945923 0.9868224859 0.9903945923 - 0.9905322790 0.9870029092 0.9905322790 - 0.9906700253 0.9871832728 0.9906700253 - 0.9908077121 0.9873635769 0.9908077121 - 0.9909452796 0.9875440001 0.9909452796 - 0.9910830259 0.9877244234 0.9910830259 - 0.9912207127 0.9879047871 0.9912207127 - 0.9913563728 0.9880853295 0.9913563728 - 0.9914879799 0.9882667065 0.9914879799 - 0.9916195273 0.9884480238 0.9916195273 - 0.9917510152 0.9886294007 0.9917510152 - 0.9918825030 0.9888107181 0.9918825030 - 0.9920139909 0.9889919758 0.9920139909 - 0.9921454787 0.9891734123 0.9921454787 - 0.9922770262 0.9893547297 0.9922770262 - 0.9924085140 0.9895361066 0.9924085140 - 0.9925400019 0.9897174239 0.9925400019 - 0.9926714897 0.9898986816 0.9926714897 - 0.9928073883 0.9900819063 0.9928073883 - 0.9929450154 0.9902659059 0.9929450154 - 0.9930827022 0.9904500246 0.9930827022 - 0.9932203293 0.9906340241 0.9932203293 - 0.9933580160 0.9908180833 0.9933580160 - 0.9934955835 0.9910022020 0.9934955835 - 0.9936332107 0.9911862016 0.9936332107 - 0.9937708974 0.9913703203 0.9937708974 - 0.9939085245 0.9915543795 0.9939085245 - 0.9940462112 0.9917383790 0.9940462112 - 0.9941837192 0.9919224977 0.9941837192 - 0.9943144917 0.9921045899 0.9943144917 - 0.9944453835 0.9922866821 0.9944453835 - 0.9945762157 0.9924687147 0.9945762157 - 0.9947069883 0.9926506877 0.9947069883 - 0.9948378205 0.9928327799 0.9948378205 - 0.9949685931 0.9930148125 0.9949685931 - 0.9950994253 0.9931967854 0.9950994253 - 0.9952301979 0.9933788776 0.9952301979 - 0.9953609705 0.9935609102 0.9953609705 - 0.9954918027 0.9937428832 0.9954918027 - 0.9956232905 0.9939253926 0.9956232905 - 0.9957578778 0.9941105843 0.9957578778 - 0.9958925843 0.9942957163 0.9958925843 - 0.9960271716 0.9944809079 0.9960271716 - 0.9961618781 0.9946660995 0.9961618781 - 0.9962965250 0.9948512912 0.9962965250 - 0.9964311123 0.9950364232 0.9964311123 - 0.9965658188 0.9952216148 0.9965658188 - 0.9967004061 0.9954068065 0.9967004061 - 0.9968351126 0.9955918789 0.9968351126 - 0.9969696999 0.9957770705 0.9969696999 - 0.9971042275 0.9959601164 0.9971042275 - 0.9972379804 0.9961351752 0.9972379804 - 0.9973717928 0.9963101745 0.9973717928 - 0.9975057244 0.9964852929 0.9975057244 - 0.9976394773 0.9966604114 0.9976394773 - 0.9977734089 0.9968354702 0.9977734089 - 0.9979072213 0.9970105290 0.9979072213 - 0.9980410933 0.9971855879 0.9980410933 - 0.9981749058 0.9973607063 0.9981749058 - 0.9983087778 0.9975357056 0.9983087778 - 0.9984425902 0.9977108240 0.9984425902 - 0.9985756874 0.9978886843 0.9985756874 - 0.9987052083 0.9980806112 0.9987052083 - 0.9988346100 0.9982725978 0.9988346100 - 0.9989640713 0.9984645247 0.9989640713 - 0.9990935922 0.9986563921 0.9990935922 - 0.9992231131 0.9988483787 0.9992231131 - 0.9993525743 0.9990403056 0.9993525743 - 0.9994820952 0.9992322922 0.9994820952 - 0.9996114969 0.9994242191 0.9996114969 - 0.9997410178 0.9996160865 0.9997410178 - 0.9998704791 0.9998080730 0.9998704791 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0001951461 0.0001957432 0.0001951461 + 0.0003971827 0.0004013765 0.0003971827 + 0.0006093314 0.0006168035 0.0006093314 + 0.0008331265 0.0008421433 0.0008331265 + 0.0010693250 0.0010800430 0.0010693250 + 0.0013165741 0.0013327420 0.0013165741 + 0.0015723180 0.0015964990 0.0015723180 + 0.0018371070 0.0018691909 0.0018371070 + 0.0021111390 0.0021534991 0.0021111390 + 0.0023975959 0.0024501081 0.0023975959 + 0.0026947369 0.0027620669 0.0026947369 + 0.0030043761 0.0030861360 0.0030043761 + 0.0033243729 0.0034196740 0.0033243729 + 0.0036564060 0.0037631679 0.0036564060 + 0.0040000519 0.0041189152 0.0040000519 + 0.0043583522 0.0044872048 0.0043583522 + 0.0047301622 0.0048662322 0.0047301622 + 0.0051159370 0.0052598631 0.0051159370 + 0.0055168238 0.0056668199 0.0055168238 + 0.0059318189 0.0060855579 0.0059318189 + 0.0063606128 0.0065190848 0.0063606128 + 0.0068039121 0.0069621201 0.0068039121 + 0.0072610150 0.0074171298 0.0072610150 + 0.0077324822 0.0078812325 0.0077324822 + 0.0082186935 0.0083566681 0.0082186935 + 0.0087195607 0.0088426983 0.0087195607 + 0.0092373807 0.0093412735 0.0092373807 + 0.0097713200 0.0098493528 0.0097713200 + 0.0103215296 0.0103719197 0.0103215296 + 0.0108859697 0.0109069496 0.0108859697 + 0.0114682503 0.0114534898 0.0114682503 + 0.0120688202 0.0120149804 0.0120688202 + 0.0126871103 0.0125900703 0.0126871103 + 0.0133261904 0.0131813204 0.0133261904 + 0.0139810601 0.0137880603 0.0139810601 + 0.0146569004 0.0144142900 0.0146569004 + 0.0153518198 0.0150552802 0.0153518198 + 0.0160680693 0.0157083794 0.0160680693 + 0.0168081895 0.0163766202 0.0168081895 + 0.0175653305 0.0170597993 0.0175653305 + 0.0183405094 0.0177551508 0.0183405094 + 0.0191343091 0.0184656195 0.0191343091 + 0.0199443996 0.0191936009 0.0199443996 + 0.0207724404 0.0199350491 0.0207724404 + 0.0216247104 0.0206901692 0.0216247104 + 0.0224967394 0.0214668401 0.0224967394 + 0.0233900901 0.0222598594 0.0233900901 + 0.0243038405 0.0230733994 0.0243038405 + 0.0252429191 0.0239060391 0.0252429191 + 0.0262041409 0.0247574691 0.0262041409 + 0.0271870699 0.0256274194 0.0271870699 + 0.0281923302 0.0265152995 0.0281923302 + 0.0292217396 0.0274243094 0.0292217396 + 0.0302699897 0.0283501595 0.0302699897 + 0.0313403010 0.0292933993 0.0313403010 + 0.0324379094 0.0302557293 0.0324379094 + 0.0335581601 0.0312350094 0.0335581601 + 0.0347037092 0.0322324112 0.0347037092 + 0.0358735509 0.0332520008 0.0358735509 + 0.0370708592 0.0342949703 0.0370708592 + 0.0382963195 0.0353550985 0.0382963195 + 0.0395508707 0.0364397392 0.0395508707 + 0.0408340394 0.0375451893 0.0408340394 + 0.0421415716 0.0386712812 0.0421415716 + 0.0434691012 0.0398181006 0.0434691012 + 0.0448197499 0.0409855917 0.0448197499 + 0.0461917110 0.0421767011 0.0461917110 + 0.0475859195 0.0433861688 0.0475859195 + 0.0489974618 0.0446146913 0.0489974618 + 0.0504221395 0.0458587706 0.0504221395 + 0.0518664904 0.0471218303 0.0518664904 + 0.0533314086 0.0484039001 0.0533314086 + 0.0548129790 0.0497035906 0.0548129790 + 0.0563097298 0.0510205887 0.0563097298 + 0.0578255616 0.0523589589 0.0578255616 + 0.0593563803 0.0537226684 0.0593563803 + 0.0609117001 0.0551059917 0.0609117001 + 0.0624887496 0.0565031916 0.0624887496 + 0.0640849099 0.0579188913 0.0640849099 + 0.0657032207 0.0593533292 0.0657032207 + 0.0673418716 0.0608051494 0.0673418716 + 0.0690022632 0.0622783490 0.0690022632 + 0.0706820935 0.0637728870 0.0706820935 + 0.0723833889 0.0652925819 0.0723833889 + 0.0741088763 0.0668374375 0.0741088763 + 0.0758581236 0.0684004799 0.0758581236 + 0.0776309818 0.0699783191 0.0776309818 + 0.0794283003 0.0715712607 0.0794283003 + 0.0812476426 0.0731753632 0.0812476426 + 0.0830830187 0.0747953877 0.0830830187 + 0.0849313214 0.0764375329 0.0849313214 + 0.0867945403 0.0780991465 0.0867945403 + 0.0886720195 0.0797795132 0.0886720195 + 0.0905641615 0.0814804882 0.0905641615 + 0.0924730599 0.0832031295 0.0924730599 + 0.0944008604 0.0849441215 0.0944008604 + 0.0963497087 0.0866999105 0.0963497087 + 0.0983169824 0.0884718299 0.0983169824 + 0.1002992019 0.0902594104 0.1002992019 + 0.1022920981 0.0920619667 0.1022920981 + 0.1043059006 0.0938806906 0.1043059006 + 0.1063338965 0.0957234576 0.1063338965 + 0.1083754003 0.0975851119 0.1083754003 + 0.1104298979 0.0994653329 0.1104298979 + 0.1125015020 0.1013638005 0.1125015020 + 0.1145863980 0.1032824963 0.1145863980 + 0.1166832000 0.1052192971 0.1166832000 + 0.1187919974 0.1071742028 0.1187919974 + 0.1209281981 0.1091483980 0.1209281981 + 0.1230840012 0.1111427024 0.1230840012 + 0.1252560019 0.1131450012 0.1252560019 + 0.1274449974 0.1151549965 0.1274449974 + 0.1296389997 0.1171758026 0.1296389997 + 0.1318369955 0.1192134991 0.1318369955 + 0.1340391934 0.1212674007 0.1340391934 + 0.1362587065 0.1233419999 0.1362587065 + 0.1384842992 0.1254397035 0.1384842992 + 0.1407155991 0.1275476962 0.1407155991 + 0.1429519951 0.1296651959 0.1429519951 + 0.1451932937 0.1317926049 0.1451932937 + 0.1474387050 0.1339260936 0.1474387050 + 0.1496894062 0.1360657066 0.1496894062 + 0.1519508958 0.1382188946 0.1519508958 + 0.1542215943 0.1403802931 0.1542215943 + 0.1564984024 0.1425506026 0.1564984024 + 0.1587806046 0.1447318047 0.1587806046 + 0.1610693038 0.1469233930 0.1610693038 + 0.1633670926 0.1491298974 0.1633670926 + 0.1656686068 0.1513458043 0.1656686068 + 0.1679760069 0.1535694003 0.1679760069 + 0.1702944934 0.1558119059 0.1702944934 + 0.1726182997 0.1580559015 0.1726182997 + 0.1749459952 0.1603008062 0.1749459952 + 0.1772750020 0.1625521928 0.1772750020 + 0.1796054989 0.1648124009 0.1796054989 + 0.1819469035 0.1670871973 0.1819469035 + 0.1842930019 0.1693769991 0.1842930019 + 0.1866527051 0.1716732979 0.1866527051 + 0.1890151054 0.1739847064 0.1890151054 + 0.1913814992 0.1763011068 0.1913814992 + 0.1937530041 0.1786264926 0.1937530041 + 0.1961268932 0.1809585989 0.1961268932 + 0.1984995008 0.1832942963 0.1984995008 + 0.2008748949 0.1856396049 0.2008748949 + 0.2032615989 0.1879891008 0.2032615989 + 0.2056463957 0.1903553009 0.2056463957 + 0.2080287933 0.1927254945 0.2080287933 + 0.2104109973 0.1951016933 0.2104109973 + 0.2127932012 0.1974837035 0.2127932012 + 0.2151795030 0.1998693049 0.2151795030 + 0.2175665051 0.2022628039 0.2175665051 + 0.2199523002 0.2046581060 0.2199523002 + 0.2223390937 0.2070544064 0.2223390937 + 0.2247312069 0.2094524950 0.2247312069 + 0.2271240950 0.2118602991 0.2271240950 + 0.2295188010 0.2142691016 0.2295188010 + 0.2319131047 0.2166801989 0.2319131047 + 0.2343070954 0.2190932930 0.2343070954 + 0.2366984040 0.2215093970 0.2366984040 + 0.2390874028 0.2239297926 0.2390874028 + 0.2414730936 0.2263545990 0.2414730936 + 0.2438565046 0.2287807018 0.2438565046 + 0.2462437004 0.2312079072 0.2462437004 + 0.2486331016 0.2336384058 0.2486331016 + 0.2510172129 0.2360707074 0.2510172129 + 0.2533988059 0.2385022044 0.2533988059 + 0.2557758093 0.2409331948 0.2557758093 + 0.2581504881 0.2433750033 0.2581504881 + 0.2605293095 0.2458223999 0.2605293095 + 0.2629100978 0.2482777983 0.2629100978 + 0.2652882934 0.2507374883 0.2652882934 + 0.2676649988 0.2532019913 0.2676649988 + 0.2700409889 0.2556692958 0.2700409889 + 0.2724165916 0.2581461072 0.2724165916 + 0.2747870982 0.2606298029 0.2747870982 + 0.2771523893 0.2631142139 0.2771523893 + 0.2795102894 0.2655993104 0.2795102894 + 0.2818570137 0.2680785954 0.2818570137 + 0.2842001915 0.2705487907 0.2842001915 + 0.2865338027 0.2730177939 0.2865338027 + 0.2888650894 0.2754839063 0.2888650894 + 0.2911804914 0.2779509127 0.2911804914 + 0.2934958041 0.2804240882 0.2934958041 + 0.2957996130 0.2828972936 0.2957996130 + 0.2981027961 0.2853642106 0.2981027961 + 0.3004046082 0.2878307104 0.3004046082 + 0.3027057946 0.2902925909 0.3027057946 + 0.3050028980 0.2927531898 0.3050028980 + 0.3072963953 0.2952114940 0.3072963953 + 0.3095870912 0.2976680100 0.3095870912 + 0.3118711114 0.3001233041 0.3118711114 + 0.3141548038 0.3025760055 0.3141548038 + 0.3164271116 0.3050285876 0.3164271116 + 0.3186993897 0.3074803948 0.3186993897 + 0.3209618926 0.3099322915 0.3209618926 + 0.3232209980 0.3123745024 0.3232209980 + 0.3254742026 0.3148137033 0.3254742026 + 0.3277195096 0.3172554970 0.3277195096 + 0.3299638033 0.3197003901 0.3299638033 + 0.3321967125 0.3221448064 0.3321967125 + 0.3344297111 0.3245866895 0.3344297111 + 0.3366633058 0.3270285130 0.3366633058 + 0.3388971984 0.3294712901 0.3388971984 + 0.3411270976 0.3319145143 0.3411270976 + 0.3433490098 0.3343560100 0.3433490098 + 0.3455708921 0.3367947936 0.3455708921 + 0.3477726877 0.3392334878 0.3477726877 + 0.3499726951 0.3416740894 0.3499726951 + 0.3521771133 0.3441148996 0.3521771133 + 0.3543866873 0.3465546072 0.3543866873 + 0.3565962017 0.3489935100 0.3565962017 + 0.3587929904 0.3514319956 0.3587929904 + 0.3609898984 0.3538624048 0.3609898984 + 0.3631897867 0.3562928140 0.3631897867 + 0.3653928041 0.3587242961 0.3653928041 + 0.3675957918 0.3611567020 0.3675957918 + 0.3697830141 0.3635889888 0.3697830141 + 0.3719697893 0.3660191894 0.3719697893 + 0.3741520047 0.3684493899 0.3741520047 + 0.3763279915 0.3708696961 0.3763279915 + 0.3785040975 0.3732793927 0.3785040975 + 0.3806638122 0.3756890893 0.3806638122 + 0.3828206062 0.3780809045 0.3828206062 + 0.3849751055 0.3804706931 0.3849751055 + 0.3871222138 0.3828578889 0.3871222138 + 0.3892692924 0.3852390945 0.3892692924 + 0.3914063871 0.3876202106 0.3914063871 + 0.3935371041 0.3899951875 0.3935371041 + 0.3956677020 0.3923670948 0.3956677020 + 0.3977789879 0.3947389126 0.3977789879 + 0.3998892009 0.3970890045 0.3998892009 + 0.4019930959 0.3994390965 0.4019930959 + 0.4040785134 0.4017911851 0.4040785134 + 0.4061638117 0.4041475952 0.4061638117 + 0.4082497060 0.4065040052 0.4082497060 + 0.4103358984 0.4088486135 0.4103358984 + 0.4124220908 0.4111852944 0.4124220908 + 0.4144963026 0.4135220051 0.4144963026 + 0.4165670872 0.4158436060 0.4165670872 + 0.4186378121 0.4181624949 0.4186378121 + 0.4206883013 0.4204812944 0.4206883013 + 0.4227387011 0.4227986038 0.4227387011 + 0.4247851968 0.4251160026 0.4247851968 + 0.4268149137 0.4274305999 0.4268149137 + 0.4288446903 0.4297378063 0.4288446903 + 0.4308727980 0.4320450127 0.4308727980 + 0.4328976870 0.4343562126 0.4328976870 + 0.4349226058 0.4366728067 0.4349226058 + 0.4369452894 0.4389894009 0.4369452894 + 0.4389654100 0.4412983954 0.4389654100 + 0.4409855008 0.4436013103 0.4409855008 + 0.4429948926 0.4459043145 0.4429948926 + 0.4449959099 0.4482020140 0.4449959099 + 0.4469969869 0.4504969120 0.4469969869 + 0.4489865005 0.4527918994 0.4489865005 + 0.4509691894 0.4550856948 0.4509691894 + 0.4529519081 0.4573791921 0.4529519081 + 0.4549242854 0.4596726894 0.4549242854 + 0.4568913877 0.4619601071 0.4568913877 + 0.4588586092 0.4642454982 0.4588586092 + 0.4608167112 0.4665308893 0.4608167112 + 0.4627703130 0.4688045979 0.4627703130 + 0.4647239149 0.4710747004 0.4647239149 + 0.4666719139 0.4733447134 0.4666719139 + 0.4686164856 0.4756112993 0.4686164856 + 0.4705610871 0.4778766036 0.4705610871 + 0.4724955857 0.4801419973 0.4724955857 + 0.4744223058 0.4823893905 0.4744223058 + 0.4763489962 0.4846282899 0.4763489962 + 0.4782694876 0.4868671000 0.4782694876 + 0.4801832139 0.4890891910 0.4801832139 + 0.4820967913 0.4912990928 0.4820967913 + 0.4840039015 0.4935089946 0.4840039015 + 0.4858981967 0.4957109988 0.4858981967 + 0.4877924025 0.4979032874 0.4877924025 + 0.4896839857 0.5000956059 0.4896839857 + 0.4915628135 0.5022779703 0.4915628135 + 0.4934414923 0.5044360161 0.4934414923 + 0.4953202009 0.5065940022 0.4953202009 + 0.4971916080 0.5087499022 0.4971916080 + 0.4990628064 0.5108857751 0.4990628064 + 0.5009341240 0.5130217075 0.5009341240 + 0.5027933121 0.5151576996 0.5027933121 + 0.5046482086 0.5172629952 0.5046482086 + 0.5065031052 0.5193634033 0.5065031052 + 0.5083466172 0.5214638710 0.5083466172 + 0.5101770759 0.5235527754 0.5101770759 + 0.5120074749 0.5256341100 0.5120074749 + 0.5138359070 0.5277153850 0.5138359070 + 0.5156533122 0.5297918916 0.5156533122 + 0.5174707174 0.5318573117 0.5174707174 + 0.5192881227 0.5339226127 0.5192881227 + 0.5211005211 0.5359879732 0.5211005211 + 0.5229117274 0.5380240083 0.5229117274 + 0.5247228742 0.5400587916 0.5247228742 + 0.5265293121 0.5420935750 0.5265293121 + 0.5283294916 0.5441210866 0.5283294916 + 0.5301296711 0.5461434126 0.5301296711 + 0.5319293737 0.5481657982 0.5319293737 + 0.5337138772 0.5501838923 0.5337138772 + 0.5354983807 0.5521820784 0.5354983807 + 0.5372828841 0.5541803241 0.5372828841 + 0.5390647054 0.5561785102 0.5390647054 + 0.5408445001 0.5581638813 0.5408445001 + 0.5426242948 0.5601447225 0.5426242948 + 0.5444021821 0.5621253848 0.5444021821 + 0.5461611152 0.5640997291 0.5461611152 + 0.5479199290 0.5660542846 0.5479199290 + 0.5496788025 0.5680087805 0.5496788025 + 0.5514246821 0.5699632764 0.5514246821 + 0.5531604886 0.5718948841 0.5531604886 + 0.5548964143 0.5738177896 0.5548964143 + 0.5566322207 0.5757405758 0.5566322207 + 0.5583571196 0.5776597857 0.5583571196 + 0.5600818992 0.5795599818 0.5600818992 + 0.5618067980 0.5814602971 0.5618067980 + 0.5635148287 0.5833604932 0.5635148287 + 0.5651975274 0.5852445960 0.5651975274 + 0.5668801069 0.5871161819 0.5668801069 + 0.5685628057 0.5889877081 0.5685628057 + 0.5702366233 0.5908592939 0.5702366233 + 0.5719075203 0.5926998258 0.5719075203 + 0.5735784769 0.5945379734 0.5735784769 + 0.5752487183 0.5963761210 0.5752487183 + 0.5769106746 0.5982087851 0.5769106746 + 0.5785726905 0.6000252962 0.5785726905 + 0.5802345872 0.6018419266 0.5802345872 + 0.5818874240 0.6036584973 0.5818874240 + 0.5835233927 0.6054543257 0.5835233927 + 0.5851594210 0.6072319746 0.5851594210 + 0.5867953897 0.6090096831 0.5867953897 + 0.5884160995 0.6107872725 0.5884160995 + 0.5900263786 0.6125335097 0.5900263786 + 0.5916365981 0.6142706275 0.5916365981 + 0.5932468176 0.6160078049 0.5932468176 + 0.5948414207 0.6177449226 0.5948414207 + 0.5964319110 0.6194577217 0.5964319110 + 0.5980222821 0.6211699843 0.5980222821 + 0.5996127725 0.6228823066 0.5996127725 + 0.6011689901 0.6245880127 0.6011689901 + 0.6027231812 0.6262522936 0.6027231812 + 0.6042773724 0.6279166937 0.6042773724 + 0.6058307290 0.6295809746 0.6058307290 + 0.6073688269 0.6312366724 0.6073688269 + 0.6089069843 0.6328672767 0.6089069843 + 0.6104450822 0.6344978213 0.6104450822 + 0.6119816899 0.6361284256 0.6119816899 + 0.6135081053 0.6377481818 0.6135081053 + 0.6150345206 0.6393465996 0.6150345206 + 0.6165608764 0.6409450173 0.6165608764 + 0.6180859804 0.6425433755 0.6180859804 + 0.6196045876 0.6441305876 0.6196045876 + 0.6211233139 0.6456986070 0.6211233139 + 0.6226419210 0.6472666860 0.6226419210 + 0.6241582036 0.6488348246 0.6241582036 + 0.6256613135 0.6503942013 0.6256613135 + 0.6271644831 0.6519383788 0.6271644831 + 0.6286675930 0.6534824967 0.6286675930 + 0.6301684976 0.6550266743 0.6301684976 + 0.6316462159 0.6565703154 0.6316462159 + 0.6331238747 0.6581128836 0.6331238747 + 0.6346017122 0.6596555114 0.6346017122 + 0.6360793710 0.6611980796 0.6360793710 + 0.6375383735 0.6627355218 0.6375383735 + 0.6389971972 0.6642531753 0.6389971972 + 0.6404560208 0.6657708883 0.6404560208 + 0.6419147849 0.6672887206 0.6419147849 + 0.6433644891 0.6688051224 0.6433644891 + 0.6448125243 0.6703053713 0.6448125243 + 0.6462605000 0.6718056202 0.6462605000 + 0.6477084756 0.6733058095 0.6477084756 + 0.6491544247 0.6748061180 0.6491544247 + 0.6505993009 0.6762984991 0.6505993009 + 0.6520441771 0.6777899265 0.6520441771 + 0.6534891129 0.6792812943 0.6534891129 + 0.6549304724 0.6807727218 0.6549304724 + 0.6563671231 0.6822533011 0.6563671231 + 0.6578037143 0.6837282181 0.6578037143 + 0.6592403054 0.6852030158 0.6592403054 + 0.6606746912 0.6866778731 0.6606746912 + 0.6620953083 0.6881393194 0.6620953083 + 0.6635159254 0.6895787120 0.6635159254 + 0.6649364829 0.6910182238 0.6649364829 + 0.6663571000 0.6924576163 0.6663571000 + 0.6677600145 0.6938955784 0.6677600145 + 0.6691588163 0.6953092813 0.6691588163 + 0.6705574989 0.6967229843 0.6705574989 + 0.6719563007 0.6981366873 0.6719563007 + 0.6733427048 0.6995503902 0.6733427048 + 0.6747133136 0.7009537220 0.6747133136 + 0.6760839224 0.7023522854 0.6760839224 + 0.6774544120 0.7037507892 0.6774544120 + 0.6788250208 0.7051494122 0.6788250208 + 0.6801913977 0.7065389752 0.6801913977 + 0.6815578938 0.7079041004 0.6815578938 + 0.6829243898 0.7092692852 0.6829243898 + 0.6842908859 0.7106345296 0.6842908859 + 0.6856526136 0.7119995952 0.6856526136 + 0.6870101094 0.7133480906 0.6870101094 + 0.6883677244 0.7146925926 0.6883677244 + 0.6897252202 0.7160370946 0.6897252202 + 0.6910821199 0.7173815966 0.6910821199 + 0.6924132109 0.7187203765 0.6924132109 + 0.6937443018 0.7200453877 0.6937443018 + 0.6950753927 0.7213705182 0.6950753927 + 0.6964064837 0.7226955295 0.6964064837 + 0.6977304816 0.7240206003 0.6977304816 + 0.6990460753 0.7253168225 0.6990460753 + 0.7003617287 0.7266023755 0.7003617287 + 0.7016773224 0.7278879881 0.7016773224 + 0.7029929161 0.7291736007 0.7029929161 + 0.7043045759 0.7304561734 0.7043045759 + 0.7056154013 0.7317163944 0.7056154013 + 0.7069262266 0.7329766154 0.7069262266 + 0.7082371116 0.7342368960 0.7082371116 + 0.7095441818 0.7354971170 0.7095441818 + 0.7108336091 0.7367405891 0.7108336091 + 0.7121229768 0.7379640937 0.7121229768 + 0.7134124041 0.7391875982 0.7134124041 + 0.7147017717 0.7404111028 0.7147017717 + 0.7159798741 0.7416346073 0.7159798741 + 0.7172445059 0.7428414226 0.7172445059 + 0.7185090184 0.7440423965 0.7185090184 + 0.7197735906 0.7452433705 0.7197735906 + 0.7210381031 0.7464445233 0.7210381031 + 0.7222943902 0.7476454973 0.7222943902 + 0.7235469222 0.7488263845 0.7235469222 + 0.7247992754 0.7500069737 0.7247992754 + 0.7260518074 0.7511875033 0.7260518074 + 0.7273042202 0.7523680925 0.7273042202 + 0.7285580039 0.7535430193 0.7285580039 + 0.7298120260 0.7546893954 0.7298120260 + 0.7310661077 0.7558357120 0.7310661077 + 0.7323200703 0.7569820881 0.7323200703 + 0.7335740924 0.7581285238 0.7335740924 + 0.7348139286 0.7592620850 0.7348139286 + 0.7360535264 0.7603654861 0.7360535264 + 0.7372931838 0.7614690065 0.7372931838 + 0.7385329008 0.7625724077 0.7385329008 + 0.7397707105 0.7636758089 0.7397707105 + 0.7409819961 0.7647761106 0.7409819961 + 0.7421932817 0.7658712268 0.7421932817 + 0.7434046865 0.7669662833 0.7434046865 + 0.7446159720 0.7680615187 0.7446159720 + 0.7458249927 0.7691565752 0.7458249927 + 0.7470089793 0.7702389956 0.7470089793 + 0.7481930852 0.7713027000 0.7481930852 + 0.7493770719 0.7723662853 0.7493770719 + 0.7505611181 0.7734299898 0.7505611181 + 0.7517436147 0.7744936943 0.7517436147 + 0.7528991103 0.7755483985 0.7528991103 + 0.7540546060 0.7765882015 0.7540546060 + 0.7552101016 0.7776281238 0.7552101016 + 0.7563655972 0.7786679268 0.7563655972 + 0.7575210929 0.7797077894 0.7575210929 + 0.7586535215 0.7807422280 0.7586535215 + 0.7597848773 0.7817639112 0.7597848773 + 0.7609164119 0.7827855945 0.7609164119 + 0.7620478868 0.7838072181 0.7620478868 + 0.7631794214 0.7848289013 0.7631794214 + 0.7643039823 0.7858467102 0.7643039823 + 0.7654271126 0.7868434787 0.7654271126 + 0.7665501833 0.7878404260 0.7665501833 + 0.7676733136 0.7888373137 0.7676733136 + 0.7687965035 0.7898342013 0.7687965035 + 0.7699062824 0.7908310890 0.7699062824 + 0.7710080147 0.7917973995 0.7710080147 + 0.7721098065 0.7927626967 0.7721098065 + 0.7732115984 0.7937279940 0.7732115984 + 0.7743132710 0.7946931720 0.7743132710 + 0.7754092216 0.7956585288 0.7754092216 + 0.7764948010 0.7966089845 0.7764948010 + 0.7775803804 0.7975537777 0.7775803804 + 0.7786660790 0.7984985113 0.7786660790 + 0.7797517180 0.7994433045 0.7797517180 + 0.7808365822 0.8003879786 0.7808365822 + 0.7819091082 0.8013278842 0.7819091082 + 0.7829815745 0.8022608757 0.7829815745 + 0.7840539813 0.8031939268 0.7840539813 + 0.7851265073 0.8041269183 0.7851265073 + 0.7861989737 0.8050599098 0.7861989737 + 0.7872558832 0.8059914708 0.7872558832 + 0.7883058190 0.8068947196 0.7883058190 + 0.7893555760 0.8077979088 0.7893555760 + 0.7904053926 0.8087010980 0.7904053926 + 0.7914553285 0.8096042871 0.7914553285 + 0.7924997807 0.8105074763 0.7924997807 + 0.7935289741 0.8113902807 0.7935289741 + 0.7945582867 0.8122608066 0.7945582867 + 0.7955874801 0.8131312728 0.7955874801 + 0.7966167927 0.8140019178 0.7966167927 + 0.7976459861 0.8148723841 0.7976459861 + 0.7986531258 0.8157399297 0.7986531258 + 0.7996534705 0.8165895939 0.7996534705 + 0.8006538749 0.8174393773 0.8006538749 + 0.8016542792 0.8182891011 0.8016542792 + 0.8026548028 0.8191388249 0.8026548028 + 0.8036468029 0.8199884892 0.8036468029 + 0.8046095967 0.8208152056 0.8046095967 + 0.8055725098 0.8216269016 0.8055725098 + 0.8065354228 0.8224384785 0.8065354228 + 0.8074982762 0.8232501745 0.8074982762 + 0.8084611893 0.8240618110 0.8084611893 + 0.8094077110 0.8248733878 0.8094077110 + 0.8103441000 0.8256731033 0.8103441000 + 0.8112804890 0.8264726996 0.8112804890 + 0.8122168779 0.8272724152 0.8122168779 + 0.8131533265 0.8280720711 0.8131533265 + 0.8140897155 0.8288717270 0.8140897155 + 0.8150094748 0.8296630979 0.8150094748 + 0.8159283996 0.8304389119 0.8159283996 + 0.8168473244 0.8312146068 0.8168473244 + 0.8177661896 0.8319904208 0.8177661896 + 0.8186851144 0.8327661157 0.8186851144 + 0.8195986152 0.8335418105 0.8195986152 + 0.8204926848 0.8343117237 0.8204926848 + 0.8213868737 0.8350778818 0.8213868737 + 0.8222811222 0.8358440995 0.8222811222 + 0.8231751919 0.8366103768 0.8231751919 + 0.8240693808 0.8373765945 0.8240693808 + 0.8249509931 0.8381428719 0.8249509931 + 0.8258157969 0.8389006257 0.8258157969 + 0.8266806006 0.8396567106 0.8266806006 + 0.8275455236 0.8404127955 0.8275455236 + 0.8284103274 0.8411688805 0.8284103274 + 0.8292750716 0.8419250250 0.8292750716 + 0.8301308751 0.8426809907 0.8301308751 + 0.8309800029 0.8434000015 0.8309800029 + 0.8318290710 0.8441187143 0.8318290710 + 0.8326780796 0.8448373079 0.8326780796 + 0.8335272074 0.8455560207 0.8335272074 + 0.8343762755 0.8462746143 0.8343762755 + 0.8352112174 0.8469917774 0.8352112174 + 0.8360387087 0.8476917148 0.8360387087 + 0.8368662000 0.8483915925 0.8368662000 + 0.8376936913 0.8490914702 0.8376936913 + 0.8385211825 0.8497914076 0.8385211825 + 0.8393486738 0.8504914045 0.8393486738 + 0.8401600718 0.8511893749 0.8401600718 + 0.8409640193 0.8518716097 0.8409640193 + 0.8417677879 0.8525537848 0.8417677879 + 0.8425716162 0.8532360196 0.8425716162 + 0.8433753848 0.8539181948 0.8433753848 + 0.8441792130 0.8546004295 0.8441792130 + 0.8449677825 0.8552805781 0.8449677825 + 0.8457477093 0.8559337258 0.8457477093 + 0.8465275764 0.8565866947 0.8465275764 + 0.8473075032 0.8572397828 0.8473075032 + 0.8480873704 0.8578928709 0.8480873704 + 0.8488671780 0.8585458994 0.8488671780 + 0.8496319056 0.8591989875 0.8496319056 + 0.8503823876 0.8598427773 0.8503823876 + 0.8511329889 0.8604860902 0.8511329889 + 0.8518834710 0.8611295223 0.8518834710 + 0.8526340723 0.8617728949 0.8526340723 + 0.8533846736 0.8624163270 0.8533846736 + 0.8541216850 0.8630596995 0.8541216850 + 0.8548328876 0.8636872172 0.8548328876 + 0.8555439711 0.8643105030 0.8555439711 + 0.8562551737 0.8649337888 0.8562551737 + 0.8569663167 0.8655570149 0.8569663167 + 0.8576774001 0.8661803007 0.8576774001 + 0.8583872914 0.8668035865 0.8583872914 + 0.8590850830 0.8674191236 0.8590850830 + 0.8597828150 0.8680282831 0.8597828150 + 0.8604806066 0.8686373830 0.8604806066 + 0.8611783981 0.8692466021 0.8611783981 + 0.8618761897 0.8698558211 0.8618761897 + 0.8625739813 0.8704649806 0.8625739813 + 0.8632513881 0.8710712790 0.8632513881 + 0.8639233112 0.8716691136 0.8639233112 + 0.8645951748 0.8722668886 0.8645951748 + 0.8652670979 0.8728647232 0.8652670979 + 0.8659390211 0.8734624982 0.8659390211 + 0.8666108847 0.8740602732 0.8666108847 + 0.8672788739 0.8746581078 0.8672788739 + 0.8679410815 0.8752396703 0.8679410815 + 0.8686032891 0.8758190274 0.8686032891 + 0.8692653775 0.8763983250 0.8692653775 + 0.8699275851 0.8769776225 0.8699275851 + 0.8705897927 0.8775568008 0.8705897927 + 0.8712518811 0.8781360984 0.8712518811 + 0.8719059825 0.8787040114 0.8719059825 + 0.8725597262 0.8792570233 0.8725597262 + 0.8732132912 0.8798100948 0.8732132912 + 0.8738669753 0.8803631067 0.8738669753 + 0.8745207191 0.8809161186 0.8745207191 + 0.8751742840 0.8814691901 0.8751742840 + 0.8758152127 0.8820222020 0.8758152127 + 0.8764392734 0.8825591207 0.8764392734 + 0.8770633936 0.8830946088 0.8770633936 + 0.8776875138 0.8836302161 0.8776875138 + 0.8783116937 0.8841657043 0.8783116937 + 0.8789358139 0.8847011924 0.8789358139 + 0.8795598745 0.8852367997 0.8795598745 + 0.8801736236 0.8857638836 0.8801736236 + 0.8807854056 0.8862745762 0.8807854056 + 0.8813971281 0.8867853284 0.8813971281 + 0.8820089102 0.8872959018 0.8820089102 + 0.8826205730 0.8878065944 0.8826205730 + 0.8832324147 0.8883172870 0.8832324147 + 0.8838424087 0.8888279796 0.8838424087 + 0.8844444752 0.8893265724 0.8844444752 + 0.8850466013 0.8898196220 0.8850466013 + 0.8856487274 0.8903126717 0.8856487274 + 0.8862507939 0.8908057213 0.8862507939 + 0.8868529201 0.8912987709 0.8868529201 + 0.8874549866 0.8917918205 0.8874549866 + 0.8880429268 0.8922848105 0.8880429268 + 0.8886129260 0.8927574158 0.8886129260 + 0.8891829252 0.8932290077 0.8891829252 + 0.8897529840 0.8937007189 0.8897529840 + 0.8903229833 0.8941723108 0.8903229833 + 0.8908931017 0.8946439028 0.8908931017 + 0.8914631009 0.8951156139 0.8914631009 + 0.8920254111 0.8955851793 0.8920254111 + 0.8925833702 0.8960440755 0.8925833702 + 0.8931413889 0.8965029716 0.8931413889 + 0.8936994076 0.8969618082 0.8936994076 + 0.8942574263 0.8974207044 0.8942574263 + 0.8948153257 0.8978796005 0.8948153257 + 0.8953732848 0.8983384967 0.8953732848 + 0.8959251046 0.8987923265 0.8959251046 + 0.8964747787 0.8992335200 0.8964747787 + 0.8970245719 0.8996747136 0.8970245719 + 0.8975743055 0.9001159072 0.8975743055 + 0.8981240988 0.9005571008 0.8981240988 + 0.8986737728 0.9009982944 0.8986737728 + 0.8992236257 0.9014394283 0.8992236257 + 0.8997610211 0.9018741846 0.8997610211 + 0.9002953172 0.9022963047 0.9002953172 + 0.9008296132 0.9027183056 0.9008296132 + 0.9013640285 0.9031404257 0.9013640285 + 0.9018983245 0.9035624266 0.9018983245 + 0.9024326205 0.9039844871 0.9024326205 + 0.9029669166 0.9044064879 0.9029669166 + 0.9034835100 0.9048255086 0.9034835100 + 0.9039946795 0.9052377939 0.9039946795 + 0.9045059085 0.9056500196 0.9045059085 + 0.9050170779 0.9060623050 0.9050170779 + 0.9055283070 0.9064744711 0.9055283070 + 0.9060394764 0.9068868160 0.9060394764 + 0.9065507054 0.9072989821 0.9065507054 + 0.9070444107 0.9077087045 0.9070444107 + 0.9075291157 0.9081088901 0.9075291157 + 0.9080138206 0.9085091949 0.9080138206 + 0.9084984064 0.9089093804 0.9084984064 + 0.9089831114 0.9093096256 0.9089831114 + 0.9094678164 0.9097098708 0.9094678164 + 0.9099525213 0.9101101160 0.9099525213 + 0.9104328752 0.9105101824 0.9104328752 + 0.9109085202 0.9109075069 0.9109085202 + 0.9113841057 0.9113047719 0.9113841057 + 0.9118596911 0.9117019773 0.9118596911 + 0.9123352170 0.9120993018 0.9123352170 + 0.9128108025 0.9124966264 0.9128108025 + 0.9132863879 0.9128937721 0.9132863879 + 0.9137566090 0.9132910967 0.9137566090 + 0.9142068028 0.9136837125 0.9142068028 + 0.9146568775 0.9140750170 0.9146568775 + 0.9151070714 0.9144663215 0.9151070714 + 0.9155572057 0.9148576260 0.9155572057 + 0.9160073996 0.9152488708 0.9160073996 + 0.9164574742 0.9156401753 0.9164574742 + 0.9169077277 0.9160314798 0.9169077277 + 0.9173452258 0.9164192080 0.9173452258 + 0.9177808166 0.9168025851 0.9177808166 + 0.9182164073 0.9171860218 0.9182164073 + 0.9186519980 0.9175693989 0.9186519980 + 0.9190875292 0.9179527760 0.9190875292 + 0.9195231199 0.9183362126 0.9195231199 + 0.9199587107 0.9187195897 0.9199587107 + 0.9203907251 0.9191030860 0.9203907251 + 0.9208180904 0.9194877148 0.9208180904 + 0.9212455153 0.9198724031 0.9212455153 + 0.9216729999 0.9202570915 0.9216729999 + 0.9221004248 0.9206417203 0.9221004248 + 0.9225277901 0.9210264087 0.9225277901 + 0.9229552746 0.9214110970 0.9229552746 + 0.9233826995 0.9217957258 0.9233826995 + 0.9237878919 0.9221795201 0.9237878919 + 0.9241918921 0.9225625992 0.9241918921 + 0.9245958924 0.9229457974 0.9245958924 + 0.9249997735 0.9233288765 0.9249997735 + 0.9254037738 0.9237120152 0.9254037738 + 0.9258077741 0.9240950942 0.9258077741 + 0.9262117743 0.9244781733 0.9262117743 + 0.9266104102 0.9248613119 0.9266104102 + 0.9269998074 0.9252420068 0.9269998074 + 0.9273892045 0.9256225824 0.9273892045 + 0.9277786016 0.9260032177 0.9277786016 + 0.9281679988 0.9263836741 0.9281679988 + 0.9285573959 0.9267643094 0.9285573959 + 0.9289467931 0.9271448851 0.9289467931 + 0.9293361902 0.9275255203 0.9293361902 + 0.9297145009 0.9279053807 0.9297145009 + 0.9300879240 0.9282836914 0.9300879240 + 0.9304612875 0.9286620021 0.9304612875 + 0.9308347106 0.9290403128 0.9308347106 + 0.9312080741 0.9294186831 0.9312080741 + 0.9315814972 0.9297969937 0.9315814972 + 0.9319549203 0.9301753044 0.9319549203 + 0.9323282838 0.9305536151 0.9323282838 + 0.9326866269 0.9309234023 0.9326866269 + 0.9330440164 0.9312868118 0.9330440164 + 0.9334015250 0.9316502810 0.9334015250 + 0.9337589741 0.9320136905 0.9337589741 + 0.9341164827 0.9323772192 0.9341164827 + 0.9344739914 0.9327406287 0.9344739914 + 0.9348315001 0.9331040978 0.9348315001 + 0.9351881146 0.9334675074 0.9351881146 + 0.9355376959 0.9338105917 0.9355376959 + 0.9358873963 0.9341468215 0.9358873963 + 0.9362370968 0.9344829917 0.9362370968 + 0.9365866780 0.9348192215 0.9365866780 + 0.9369363785 0.9351553917 0.9369363785 + 0.9372860789 0.9354916215 0.9372860789 + 0.9376357794 0.9358279109 0.9376357794 + 0.9379813075 0.9361640811 0.9379813075 + 0.9383106828 0.9364855886 0.9383106828 + 0.9386399984 0.9368044734 0.9386399984 + 0.9389693141 0.9371232986 0.9389693141 + 0.9392986298 0.9374421239 0.9392986298 + 0.9396280050 0.9377608895 0.9396280050 + 0.9399573207 0.9380797148 0.9399573207 + 0.9402865767 0.9383985996 0.9402865767 + 0.9406142831 0.9387174249 0.9406142831 + 0.9409353733 0.9390224218 0.9409353733 + 0.9412565231 0.9393249750 0.9412565231 + 0.9415774941 0.9396277070 0.9415774941 + 0.9418985844 0.9399303198 0.9418985844 + 0.9422196746 0.9402328730 0.9422196746 + 0.9425407052 0.9405354857 0.9425407052 + 0.9428617954 0.9408380985 0.9428617954 + 0.9431818128 0.9411407113 0.9431818128 + 0.9434933066 0.9414327741 0.9434933066 + 0.9438048005 0.9417217970 0.9438048005 + 0.9441161752 0.9420107007 0.9441161752 + 0.9444277287 0.9422997236 0.9444277287 + 0.9447392225 0.9425886273 0.9447392225 + 0.9450505972 0.9428775907 0.9450505972 + 0.9453620911 0.9431664944 0.9453620911 + 0.9456735849 0.9434555173 0.9456735849 + 0.9459661841 0.9437438250 0.9459661841 + 0.9462574720 0.9440317750 0.9462574720 + 0.9465487003 0.9443197250 0.9465487003 + 0.9468399882 0.9446076155 0.9468399882 + 0.9471312165 0.9448956251 0.9471312165 + 0.9474225044 0.9451835155 0.9474225044 + 0.9477136731 0.9454715252 0.9477136731 + 0.9480050206 0.9457594156 0.9480050206 + 0.9482976794 0.9460437298 0.9482976794 + 0.9485909939 0.9463211894 0.9485909939 + 0.9488844275 0.9465987086 0.9488844275 + 0.9491778016 0.9468762279 0.9491778016 + 0.9494711757 0.9471536875 0.9494711757 + 0.9497646093 0.9474310875 0.9497646093 + 0.9500579834 0.9477086067 0.9500579834 + 0.9503514171 0.9479861259 0.9503514171 + 0.9506422281 0.9482635856 0.9506422281 + 0.9509274960 0.9485384226 0.9509274960 + 0.9512127042 0.9488133192 0.9512127042 + 0.9514979720 0.9490882158 0.9514979720 + 0.9517831802 0.9493629932 0.9517831802 + 0.9520685077 0.9496378899 0.9520685077 + 0.9523537159 0.9499127269 0.9523537159 + 0.9526389241 0.9501876235 0.9526389241 + 0.9529241920 0.9504625201 0.9529241920 + 0.9532032013 0.9507294893 0.9532032013 + 0.9534810781 0.9509904981 0.9534810781 + 0.9537590742 0.9512513876 0.9537590742 + 0.9540370703 0.9515123963 0.9540370703 + 0.9543150067 0.9517734051 0.9543150067 + 0.9545930028 0.9520342946 0.9545930028 + 0.9548709989 0.9522953033 0.9548709989 + 0.9551489949 0.9525561929 0.9551489949 + 0.9554250240 0.9528167844 0.9554250240 + 0.9556970000 0.9530668259 0.9556970000 + 0.9559689760 0.9533166885 0.9559689760 + 0.9562410712 0.9535666108 0.9562410712 + 0.9565131068 0.9538165927 0.9565131068 + 0.9567850828 0.9540665150 0.9567850828 + 0.9570571184 0.9543164968 0.9570571184 + 0.9573290944 0.9545664191 0.9573290944 + 0.9576011896 0.9548164010 0.9576011896 + 0.9578632116 0.9550620914 0.9578632116 + 0.9581208229 0.9553020000 0.9581208229 + 0.9583783746 0.9555419087 0.9583783746 + 0.9586359859 0.9557818174 0.9586359859 + 0.9588935971 0.9560217261 0.9588935971 + 0.9591512084 0.9562615156 0.9591512084 + 0.9594088197 0.9565014243 0.9594088197 + 0.9596663713 0.9567412734 0.9596663713 + 0.9599239826 0.9569811821 0.9599239826 + 0.9601870775 0.9572148919 0.9601870775 + 0.9604502916 0.9574459791 0.9604502916 + 0.9607136250 0.9576771259 0.9607136250 + 0.9609768987 0.9579082131 0.9609768987 + 0.9612401128 0.9581393003 0.9612401128 + 0.9615033865 0.9583703876 0.9615033865 + 0.9617667198 0.9586014748 0.9617667198 + 0.9620298743 0.9588325024 0.9620298743 + 0.9622896910 0.9590635896 0.9622896910 + 0.9625295997 0.9592813849 0.9625295997 + 0.9627695084 0.9594975710 0.9627695084 + 0.9630094171 0.9597136974 0.9630094171 + 0.9632493258 0.9599298835 0.9632493258 + 0.9634891748 0.9601460099 0.9634891748 + 0.9637290835 0.9603621960 0.9637290835 + 0.9639688730 0.9605783820 0.9639688730 + 0.9642087817 0.9607945085 0.9642087817 + 0.9644483924 0.9610106945 0.9644483924 + 0.9646872282 0.9612244964 0.9646872282 + 0.9649258852 0.9614382982 0.9649258852 + 0.9651646018 0.9616519809 0.9651646018 + 0.9654033184 0.9618657827 0.9654033184 + 0.9656419754 0.9620795846 0.9656419754 + 0.9658808112 0.9622933865 0.9658808112 + 0.9661195278 0.9625071883 0.9661195278 + 0.9663581848 0.9627209902 0.9663581848 + 0.9665971994 0.9629346728 0.9665971994 + 0.9668369889 0.9631499052 0.9668369889 + 0.9670768976 0.9633650780 0.9670768976 + 0.9673168063 0.9635801911 0.9673168063 + 0.9675567150 0.9637954235 0.9675567150 + 0.9677965045 0.9640105963 0.9677965045 + 0.9680364132 0.9642258286 0.9680364132 + 0.9682763219 0.9644410014 0.9682763219 + 0.9685161114 0.9646561146 0.9685161114 + 0.9687551260 0.9648712873 0.9687551260 + 0.9689875245 0.9650797844 0.9689875245 + 0.9692199230 0.9652873874 0.9692199230 + 0.9694522023 0.9654949903 0.9694522023 + 0.9696846008 0.9657025933 0.9696846008 + 0.9699168801 0.9659103155 0.9699168801 + 0.9701492786 0.9661179185 0.9701492786 + 0.9703816175 0.9663255215 0.9703816175 + 0.9706140161 0.9665331244 0.9706140161 + 0.9708464146 0.9667407274 0.9708464146 + 0.9710645080 0.9669466019 0.9710645080 + 0.9712811708 0.9671518207 0.9712811708 + 0.9714977741 0.9673569202 0.9714977741 + 0.9717144966 0.9675620198 0.9717144966 + 0.9719312191 0.9677671790 0.9719312191 + 0.9721478820 0.9679722786 0.9721478820 + 0.9723646045 0.9681774974 0.9723646045 + 0.9725812078 0.9683825970 0.9725812078 + 0.9727978706 0.9685876966 0.9727978706 + 0.9730128050 0.9687932730 0.9730128050 + 0.9732266068 0.9689996243 0.9732266068 + 0.9734402895 0.9692057967 0.9734402895 + 0.9736539721 0.9694120884 0.9736539721 + 0.9738677144 0.9696183205 0.9738677144 + 0.9740815163 0.9698246121 0.9740815163 + 0.9742951989 0.9700307846 0.9742951989 + 0.9745088816 0.9702370763 0.9745088816 + 0.9747226834 0.9704433084 0.9747226834 + 0.9749327898 0.9706496000 0.9749327898 + 0.9751276970 0.9708499908 0.9751276970 + 0.9753227234 0.9710503817 0.9753227234 + 0.9755175710 0.9712507129 0.9755175710 + 0.9757124782 0.9714511037 0.9757124782 + 0.9759075046 0.9716514945 0.9759075046 + 0.9761024117 0.9718518853 0.9761024117 + 0.9762973785 0.9720522761 0.9762973785 + 0.9764922857 0.9722527266 0.9764922857 + 0.9766871929 0.9724531174 0.9766871929 + 0.9768773913 0.9726468921 0.9768773913 + 0.9770653844 0.9728342891 0.9770653844 + 0.9772533774 0.9730216265 0.9772533774 + 0.9774413109 0.9732090235 0.9774413109 + 0.9776293039 0.9733963013 0.9776293039 + 0.9778172970 0.9735835791 0.9778172970 + 0.9780052900 0.9737709761 0.9780052900 + 0.9781932235 0.9739583135 0.9781932235 + 0.9783812165 0.9741457105 0.9783812165 + 0.9785689116 0.9743329883 0.9785689116 + 0.9787533879 0.9745092988 0.9787533879 + 0.9789379239 0.9746842980 0.9789379239 + 0.9791222811 0.9748592973 0.9791222811 + 0.9793068171 0.9750342965 0.9793068171 + 0.9794911742 0.9752092957 0.9794911742 + 0.9796757102 0.9753842950 0.9796757102 + 0.9798601270 0.9755592942 0.9798601270 + 0.9800446033 0.9757342935 0.9800446033 + 0.9802290201 0.9759092927 0.9802290201 + 0.9804099202 0.9760805964 0.9804099202 + 0.9805839062 0.9762361050 0.9805839062 + 0.9807580113 0.9763916731 0.9807580113 + 0.9809321165 0.9765471816 0.9809321165 + 0.9811062217 0.9767026901 0.9811062217 + 0.9812802076 0.9768583179 0.9812802076 + 0.9814543128 0.9770138264 0.9814543128 + 0.9816284180 0.9771692753 0.9816284180 + 0.9818025231 0.9773249030 0.9818025231 + 0.9819766283 0.9774804115 0.9819766283 + 0.9821426272 0.9776375294 0.9821426272 + 0.9823012948 0.9777972102 0.9823012948 + 0.9824600816 0.9779568911 0.9824600816 + 0.9826188087 0.9781165719 0.9826188087 + 0.9827775955 0.9782761931 0.9827775955 + 0.9829363227 0.9784358740 0.9829363227 + 0.9830951095 0.9785956144 0.9830951095 + 0.9832537770 0.9787552953 0.9832537770 + 0.9834125042 0.9789149761 0.9834125042 + 0.9835712910 0.9790747166 0.9835712910 + 0.9837297201 0.9792376757 0.9837297201 + 0.9838877916 0.9794046283 0.9838877916 + 0.9840459824 0.9795715213 0.9840459824 + 0.9842041135 0.9797384143 0.9842041135 + 0.9843623042 0.9799053073 0.9843623042 + 0.9845203757 0.9800723195 0.9845203757 + 0.9846786261 0.9802392125 0.9846786261 + 0.9848366976 0.9804061055 0.9848366976 + 0.9849948883 0.9805729985 0.9849948883 + 0.9851530194 0.9807398915 0.9851530194 + 0.9853115082 0.9809073210 0.9853115082 + 0.9854704142 0.9810754061 0.9854704142 + 0.9856293201 0.9812434912 0.9856293201 + 0.9857881069 0.9814115167 0.9857881069 + 0.9859470129 0.9815796018 0.9859470129 + 0.9861057997 0.9817476869 0.9861057997 + 0.9862647057 0.9819157124 0.9862647057 + 0.9864236116 0.9820837975 0.9864236116 + 0.9865823984 0.9822518826 0.9865823984 + 0.9867413044 0.9824199080 0.9867413044 + 0.9868972898 0.9825896025 0.9868972898 + 0.9870492816 0.9827628732 0.9870492816 + 0.9872012138 0.9829362035 0.9872012138 + 0.9873532057 0.9831094742 0.9873532057 + 0.9875051975 0.9832828045 0.9875051975 + 0.9876571894 0.9834560752 0.9876571894 + 0.9878091812 0.9836292863 0.9878091812 + 0.9879611731 0.9838026166 0.9879611731 + 0.9881131053 0.9839758873 0.9881131053 + 0.9882650971 0.9841492176 0.9882650971 + 0.9884155989 0.9843227863 0.9884155989 + 0.9885584116 0.9845007062 0.9885584116 + 0.9887011051 0.9846786261 0.9887011051 + 0.9888437986 0.9848564863 0.9888437986 + 0.9889864922 0.9850344062 0.9889864922 + 0.9891293049 0.9852123260 0.9891293049 + 0.9892719984 0.9853901863 0.9892719984 + 0.9894146919 0.9855681062 0.9894146919 + 0.9895573854 0.9857460260 0.9895573854 + 0.9897001982 0.9859238863 0.9897001982 + 0.9898428917 0.9861018062 0.9898428917 + 0.9899815917 0.9862813950 0.9899815917 + 0.9901192784 0.9864618182 0.9901192784 + 0.9902570248 0.9866421819 0.9902570248 + 0.9903945923 0.9868224859 0.9903945923 + 0.9905322790 0.9870029092 0.9905322790 + 0.9906700253 0.9871832728 0.9906700253 + 0.9908077121 0.9873635769 0.9908077121 + 0.9909452796 0.9875440001 0.9909452796 + 0.9910830259 0.9877244234 0.9910830259 + 0.9912207127 0.9879047871 0.9912207127 + 0.9913563728 0.9880853295 0.9913563728 + 0.9914879799 0.9882667065 0.9914879799 + 0.9916195273 0.9884480238 0.9916195273 + 0.9917510152 0.9886294007 0.9917510152 + 0.9918825030 0.9888107181 0.9918825030 + 0.9920139909 0.9889919758 0.9920139909 + 0.9921454787 0.9891734123 0.9921454787 + 0.9922770262 0.9893547297 0.9922770262 + 0.9924085140 0.9895361066 0.9924085140 + 0.9925400019 0.9897174239 0.9925400019 + 0.9926714897 0.9898986816 0.9926714897 + 0.9928073883 0.9900819063 0.9928073883 + 0.9929450154 0.9902659059 0.9929450154 + 0.9930827022 0.9904500246 0.9930827022 + 0.9932203293 0.9906340241 0.9932203293 + 0.9933580160 0.9908180833 0.9933580160 + 0.9934955835 0.9910022020 0.9934955835 + 0.9936332107 0.9911862016 0.9936332107 + 0.9937708974 0.9913703203 0.9937708974 + 0.9939085245 0.9915543795 0.9939085245 + 0.9940462112 0.9917383790 0.9940462112 + 0.9941837192 0.9919224977 0.9941837192 + 0.9943144917 0.9921045899 0.9943144917 + 0.9944453835 0.9922866821 0.9944453835 + 0.9945762157 0.9924687147 0.9945762157 + 0.9947069883 0.9926506877 0.9947069883 + 0.9948378205 0.9928327799 0.9948378205 + 0.9949685931 0.9930148125 0.9949685931 + 0.9950994253 0.9931967854 0.9950994253 + 0.9952301979 0.9933788776 0.9952301979 + 0.9953609705 0.9935609102 0.9953609705 + 0.9954918027 0.9937428832 0.9954918027 + 0.9956232905 0.9939253926 0.9956232905 + 0.9957578778 0.9941105843 0.9957578778 + 0.9958925843 0.9942957163 0.9958925843 + 0.9960271716 0.9944809079 0.9960271716 + 0.9961618781 0.9946660995 0.9961618781 + 0.9962965250 0.9948512912 0.9962965250 + 0.9964311123 0.9950364232 0.9964311123 + 0.9965658188 0.9952216148 0.9965658188 + 0.9967004061 0.9954068065 0.9967004061 + 0.9968351126 0.9955918789 0.9968351126 + 0.9969696999 0.9957770705 0.9969696999 + 0.9971042275 0.9959601164 0.9971042275 + 0.9972379804 0.9961351752 0.9972379804 + 0.9973717928 0.9963101745 0.9973717928 + 0.9975057244 0.9964852929 0.9975057244 + 0.9976394773 0.9966604114 0.9976394773 + 0.9977734089 0.9968354702 0.9977734089 + 0.9979072213 0.9970105290 0.9979072213 + 0.9980410933 0.9971855879 0.9980410933 + 0.9981749058 0.9973607063 0.9981749058 + 0.9983087778 0.9975357056 0.9983087778 + 0.9984425902 0.9977108240 0.9984425902 + 0.9985756874 0.9978886843 0.9985756874 + 0.9987052083 0.9980806112 0.9987052083 + 0.9988346100 0.9982725978 0.9988346100 + 0.9989640713 0.9984645247 0.9989640713 + 0.9990935922 0.9986563921 0.9990935922 + 0.9992231131 0.9988483787 0.9992231131 + 0.9993525743 0.9990403056 0.9993525743 + 0.9994820952 0.9992322922 0.9994820952 + 0.9996114969 0.9994242191 0.9996114969 + 0.9997410178 0.9996160865 0.9997410178 + 0.9998704791 0.9998080730 0.9998704791 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_max_zoom_800.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_max_zoom_800.spi1d index bb80ebdb..5c0ee60a 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_max_zoom_800.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_max_zoom_800.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0084391097 0.0075763101 0.0055803522 - 0.0169892292 0.0151253100 0.0110553904 - 0.0246818494 0.0219931193 0.0160910394 - 0.0317661613 0.0282940008 0.0208101291 - 0.0382325202 0.0340048485 0.0251940992 - 0.0441172384 0.0393486507 0.0293568596 - 0.0496861897 0.0445001610 0.0333589911 - 0.0550585985 0.0493195914 0.0371792093 - 0.0601223111 0.0539280511 0.0408726782 - 0.0649642274 0.0583757199 0.0444436409 - 0.0695851371 0.0626427680 0.0479431897 - 0.0740094930 0.0667798966 0.0513359718 - 0.0783147216 0.0707852617 0.0546398088 - 0.0825306326 0.0747059137 0.0578773208 - 0.0866608396 0.0785023794 0.0610598996 - 0.0907057226 0.0822077468 0.0641641617 - 0.0946363509 0.0858238488 0.0672321990 - 0.0984090120 0.0893622264 0.0702527389 - 0.1020661965 0.0928608030 0.0732008815 - 0.1056471020 0.0963046327 0.0760966092 - 0.1091898009 0.0996690318 0.0789577886 - 0.1126779020 0.1029520035 0.0817804411 - 0.1161487997 0.1061682031 0.0845761225 - 0.1195472032 0.1093337014 0.0873135701 - 0.1229045019 0.1124323979 0.0900195017 - 0.1262066960 0.1155048013 0.0926894471 - 0.1294586062 0.1185453981 0.0953314528 - 0.1326504052 0.1215455979 0.0979523510 - 0.1357606053 0.1245016977 0.1005545035 - 0.1388241053 0.1273857951 0.1031209007 - 0.1418274045 0.1302534044 0.1056765020 - 0.1447373033 0.1330838054 0.1081990004 - 0.1476096958 0.1358837932 0.1106970012 - 0.1504617035 0.1386432946 0.1131801978 - 0.1533035040 0.1413915008 0.1156400964 - 0.1561179012 0.1440995932 0.1180797964 - 0.1589269936 0.1467884034 0.1204816997 - 0.1617161930 0.1494296938 0.1228761971 - 0.1644722074 0.1520594060 0.1252495944 - 0.1672009975 0.1546591967 0.1275874972 - 0.1699149013 0.1572349966 0.1299159974 - 0.1725970954 0.1597743034 0.1322304010 - 0.1752271056 0.1622851938 0.1345029026 - 0.1778230965 0.1647903025 0.1367422938 - 0.1803787053 0.1672617942 0.1389641017 - 0.1828950047 0.1697237939 0.1411608011 - 0.1853982061 0.1721518934 0.1433494985 - 0.1878785938 0.1745534986 0.1455025971 - 0.1903571934 0.1769458950 0.1476327926 - 0.1928332001 0.1793050021 0.1497652978 - 0.1952915937 0.1816422045 0.1519078016 - 0.1977207959 0.1839872003 0.1540307999 - 0.2001152039 0.1863050014 0.1561508030 - 0.2025077045 0.1886059940 0.1582663059 - 0.2049013972 0.1908763051 0.1603844017 - 0.2072762996 0.1931104958 0.1624784023 - 0.2096343935 0.1953328997 0.1645596027 - 0.2119597048 0.1975608021 0.1666202992 - 0.2142512947 0.1997732073 0.1686652005 - 0.2165051997 0.2019650936 0.1706995964 - 0.2187242955 0.2041545063 0.1727278978 - 0.2209309936 0.2063177973 0.1747494042 - 0.2231297940 0.2084506005 0.1767428070 - 0.2253059000 0.2105776966 0.1787129939 - 0.2274693996 0.2127068937 0.1806745976 - 0.2296334058 0.2148050070 0.1826314926 - 0.2317734957 0.2168878019 0.1845801026 - 0.2338978052 0.2189736068 0.1865082979 - 0.2360040992 0.2210465968 0.1884184927 - 0.2380760014 0.2230933011 0.1903171986 - 0.2401296049 0.2251244932 0.1922022998 - 0.2421852946 0.2271171063 0.1940843016 - 0.2442210019 0.2290779948 0.1959476024 - 0.2462366074 0.2310225964 0.1977925003 - 0.2482510954 0.2329605967 0.1996293068 - 0.2502493858 0.2348946929 0.2014590949 - 0.2522226870 0.2368202060 0.2032801062 - 0.2541834116 0.2387333065 0.2050863057 - 0.2561317980 0.2406367958 0.2068803012 - 0.2580586970 0.2425450981 0.2086673975 - 0.2599673867 0.2444512993 0.2104519010 - 0.2618735135 0.2463417053 0.2122323960 - 0.2637856007 0.2482246011 0.2140039951 - 0.2656947076 0.2501086891 0.2157679945 - 0.2675924003 0.2519941926 0.2175332010 - 0.2694917023 0.2538610101 0.2192887962 - 0.2713840008 0.2557184100 0.2210364938 - 0.2732622921 0.2575724125 0.2227811962 - 0.2751278877 0.2594226897 0.2245233059 - 0.2769821882 0.2612658143 0.2262614965 - 0.2788152099 0.2630968094 0.2279942930 - 0.2806366980 0.2649213970 0.2297101021 - 0.2824515104 0.2667475939 0.2314116955 - 0.2842657864 0.2685596049 0.2331091017 - 0.2860780060 0.2703500092 0.2348040938 - 0.2878656983 0.2721207142 0.2364936024 - 0.2896395028 0.2738777995 0.2381702960 - 0.2914094925 0.2756209075 0.2398329973 - 0.2931736112 0.2773512006 0.2414873987 - 0.2949214876 0.2790701985 0.2431395948 - 0.2966547906 0.2807764113 0.2447883040 - 0.2983818054 0.2824758887 0.2464286983 - 0.3001014888 0.2841669023 0.2480624020 - 0.3017992973 0.2858343124 0.2496916056 - 0.3034814894 0.2875004113 0.2513076067 - 0.3051558137 0.2891632915 0.2529129982 - 0.3068262041 0.2908203900 0.2545062900 - 0.3085027933 0.2924638093 0.2560915053 - 0.3101766109 0.2941013873 0.2576715052 - 0.3118444085 0.2957335114 0.2592464983 - 0.3135068119 0.2973626852 0.2608287036 - 0.3151695132 0.2989872992 0.2624095082 - 0.3168335855 0.3006061018 0.2639889121 - 0.3184846938 0.3022069037 0.2655547857 - 0.3201304972 0.3038052917 0.2671144009 - 0.3217692077 0.3054029942 0.2686659992 - 0.3234055936 0.3069913089 0.2702147961 - 0.3250352144 0.3085735142 0.2717638910 - 0.3266384900 0.3101415038 0.2733153105 - 0.3282344043 0.3117021918 0.2748562098 - 0.3298248947 0.3132582009 0.2763910890 - 0.3314053118 0.3148199916 0.2779105008 - 0.3329854906 0.3163794875 0.2794237137 - 0.3345662951 0.3179340065 0.2809270918 - 0.3361378908 0.3194747865 0.2824262977 - 0.3377039135 0.3210082948 0.2839238942 - 0.3392634988 0.3225387931 0.2854253054 - 0.3408218026 0.3240653872 0.2869231999 - 0.3423773050 0.3255797923 0.2884125113 - 0.3439236879 0.3270798028 0.2898918986 - 0.3454602063 0.3285647929 0.2913635075 - 0.3469888866 0.3300414979 0.2928242981 - 0.3485142887 0.3315150142 0.2942822874 - 0.3500323892 0.3329941928 0.2957330048 - 0.3515473902 0.3344734013 0.2971836030 - 0.3530417979 0.3359495103 0.2986334860 - 0.3545320034 0.3374226987 0.3000814021 - 0.3560110033 0.3388882875 0.3015252054 - 0.3574849069 0.3403522074 0.3029659092 - 0.3589515090 0.3418137133 0.3044033051 - 0.3604162931 0.3432742059 0.3058296144 - 0.3618794084 0.3447338939 0.3072482944 - 0.3633390963 0.3461844921 0.3086608946 - 0.3647964895 0.3476294875 0.3100705147 - 0.3662520945 0.3490661085 0.3114804029 - 0.3677066863 0.3504985869 0.3128904998 - 0.3691571951 0.3519344032 0.3143031001 - 0.3706058860 0.3533715010 0.3157165051 - 0.3720482886 0.3548054099 0.3171201050 - 0.3734880090 0.3562380075 0.3185209036 - 0.3749237061 0.3576630950 0.3199110925 - 0.3763574958 0.3590848148 0.3212977052 - 0.3777863979 0.3605031073 0.3226788938 - 0.3792122900 0.3619194031 0.3240576982 - 0.3806298971 0.3633339107 0.3254354894 - 0.3820405006 0.3647469878 0.3268128037 - 0.3834429979 0.3661527932 0.3281848133 - 0.3848352134 0.3675504029 0.3295519948 - 0.3862208128 0.3689433932 0.3309170902 - 0.3875918090 0.3703270853 0.3322786987 - 0.3889611959 0.3717108071 0.3336370885 - 0.3903222978 0.3730942905 0.3349848986 - 0.3916833997 0.3744777143 0.3363322914 - 0.3930288851 0.3758434951 0.3376722932 - 0.3943736851 0.3772087991 0.3390122950 - 0.3957099915 0.3785588145 0.3403486907 - 0.3970431089 0.3799039125 0.3416841924 - 0.3983731866 0.3812474012 0.3430136144 - 0.3996999860 0.3825891912 0.3443383873 - 0.4010235071 0.3839282095 0.3456582129 - 0.4023340046 0.3852578998 0.3469654024 - 0.4036445916 0.3865875006 0.3482725918 - 0.4049448073 0.3878997862 0.3495655954 - 0.4062437117 0.3892104924 0.3508580923 - 0.4075376093 0.3905136883 0.3521454036 - 0.4088271856 0.3918113112 0.3534295857 - 0.4101122916 0.3931080997 0.3547120988 - 0.4113762975 0.3944020867 0.3559896052 - 0.4126402140 0.3956961036 0.3572671115 - 0.4138968885 0.3969767988 0.3585371077 - 0.4151513875 0.3982538879 0.3598057032 - 0.4164057970 0.3995262980 0.3610734046 - 0.4176602066 0.4007898867 0.3623399138 - 0.4189145863 0.4020535052 0.3636063933 - 0.4201653004 0.4033161998 0.3648683131 - 0.4214152992 0.4045788944 0.3661297858 - 0.4226615131 0.4058408141 0.3673880100 - 0.4239003062 0.4071016014 0.3686417937 - 0.4251390994 0.4083622992 0.3698956072 - 0.4263815880 0.4096068144 0.3711366951 - 0.4276249111 0.4108476937 0.3723761141 - 0.4288657010 0.4120868146 0.3736143112 - 0.4300980866 0.4133198857 0.3748500049 - 0.4313304126 0.4145531058 0.3760857880 - 0.4325571954 0.4157873988 0.3773151934 - 0.4337803125 0.4170222878 0.3785417974 - 0.4350034893 0.4182572067 0.3797681034 - 0.4362299144 0.4194813967 0.3809858859 - 0.4374563098 0.4207052886 0.3822036982 - 0.4386776090 0.4219268858 0.3834176064 - 0.4398860931 0.4231427014 0.3846245110 - 0.4410946071 0.4243586063 0.3858315051 - 0.4422994852 0.4255749881 0.3870328963 - 0.4435017109 0.4267917871 0.3882313073 - 0.4447039068 0.4280084968 0.3894298077 - 0.4459021091 0.4292202890 0.3906225860 - 0.4470992088 0.4304310977 0.3918147981 - 0.4482960999 0.4316413999 0.3930067122 - 0.4494735003 0.4328393936 0.3941971064 - 0.4506509900 0.4340375066 0.3953875005 - 0.4518275857 0.4352346957 0.3965758979 - 0.4530000091 0.4364287853 0.3977586031 - 0.4541724920 0.4376227856 0.3989411891 - 0.4553438127 0.4388133883 0.4001224041 - 0.4565126896 0.4399971962 0.4013010859 - 0.4576815069 0.4411810040 0.4024797976 - 0.4588429928 0.4423573911 0.4036575854 - 0.4599938095 0.4435240030 0.4048345089 - 0.4611446857 0.4446905851 0.4060113132 - 0.4622924924 0.4458532035 0.4071837068 - 0.4634366930 0.4470112920 0.4083524048 - 0.4645808041 0.4481694102 0.4095211029 - 0.4657213092 0.4493247867 0.4106839895 - 0.4668577015 0.4504772127 0.4118421078 - 0.4679940939 0.4516296983 0.4130002856 - 0.4691272080 0.4527780116 0.4141499102 - 0.4702562094 0.4539214969 0.4152916968 - 0.4713852108 0.4550648928 0.4164333940 - 0.4725120962 0.4562053084 0.4175724983 - 0.4736359119 0.4573408067 0.4187080860 - 0.4747596085 0.4584763944 0.4198437035 - 0.4758782089 0.4596096873 0.4209780991 - 0.4769828022 0.4607372880 0.4221099913 - 0.4780873954 0.4618648887 0.4232420027 - 0.4791901112 0.4629899859 0.4243740141 - 0.4802770019 0.4640980959 0.4255062938 - 0.4813640118 0.4652062058 0.4266386926 - 0.4824509025 0.4663142860 0.4277710021 - 0.4835343957 0.4674096107 0.4288952053 - 0.4846175909 0.4685041010 0.4300192893 - 0.4857007861 0.4695985019 0.4311434031 - 0.4867846966 0.4706852138 0.4322606921 - 0.4878689945 0.4717684984 0.4333756864 - 0.4889532924 0.4728518128 0.4344907105 - 0.4900343120 0.4739331901 0.4356062114 - 0.4911102951 0.4750120044 0.4367220104 - 0.4921863079 0.4760906994 0.4378378987 - 0.4932622910 0.4771688879 0.4389531910 - 0.4943380952 0.4782414138 0.4400649071 - 0.4954138100 0.4793137908 0.4411765039 - 0.4964896142 0.4803862870 0.4422881901 - 0.4975619018 0.4814558029 0.4433946908 - 0.4986326098 0.4825241864 0.4444997013 - 0.4997034073 0.4835925102 0.4456045926 - 0.5007709861 0.4846589863 0.4467051029 - 0.5018302202 0.4857206941 0.4477970898 - 0.5028892756 0.4867824912 0.4488891065 - 0.5039485097 0.4878442883 0.4499810040 - 0.5050029755 0.4888986051 0.4510704875 - 0.5060563087 0.4899514019 0.4521594942 - 0.5071097016 0.4910041988 0.4532485902 - 0.5081599951 0.4920547903 0.4543373883 - 0.5092030168 0.4931007922 0.4554258883 - 0.5102459788 0.4941467047 0.4565143883 - 0.5112890005 0.4951925874 0.4576028883 - 0.5123233795 0.4962320924 0.4586854875 - 0.5133545995 0.4972695112 0.4597668052 - 0.5143858790 0.4983069003 0.4608480036 - 0.5154163241 0.4993439913 0.4619261920 - 0.5164411068 0.5003792048 0.4629918933 - 0.5174658895 0.5014144182 0.4640575945 - 0.5184906721 0.5024495721 0.4651232958 - 0.5195080042 0.5034834743 0.4661859870 - 0.5205168128 0.5045161247 0.4672459960 - 0.5215256214 0.5055487752 0.4683060944 - 0.5225343704 0.5065814257 0.4693661034 - 0.5235391855 0.5076125264 0.4704234004 - 0.5245428085 0.5086430907 0.4714801013 - 0.5255464911 0.5096737742 0.4725368917 - 0.5265501142 0.5107042789 0.4735935032 - 0.5275589824 0.5117307901 0.4746482074 - 0.5285679102 0.5127573013 0.4757030010 - 0.5295767188 0.5137838721 0.4767577052 - 0.5305829048 0.5148088932 0.4778087139 - 0.5315781236 0.5158286095 0.4788492024 - 0.5325732827 0.5168482065 0.4798896909 - 0.5335685015 0.5178679228 0.4809302986 - 0.5345616937 0.5188835859 0.4819678068 - 0.5355507731 0.5198920965 0.4830011129 - 0.5365399122 0.5209006071 0.4840342999 - 0.5375291109 0.5219091773 0.4850674868 - 0.5385140777 0.5229142904 0.4861018956 - 0.5394936800 0.5239152908 0.4871374071 - 0.5404732823 0.5249161720 0.4881727993 - 0.5414528847 0.5259171724 0.4892083108 - 0.5424301028 0.5269111991 0.4902406037 - 0.5434045196 0.5278977156 0.4912700951 - 0.5443788171 0.5288841724 0.4922995865 - 0.5453531742 0.5298706889 0.4933289886 - 0.5463275909 0.5308517814 0.4943523109 - 0.5473020077 0.5318266153 0.4953694046 - 0.5482764244 0.5328012705 0.4963865876 - 0.5492507815 0.5337759852 0.4974038005 - 0.5502228141 0.5347477794 0.4984166026 - 0.5511909127 0.5357149243 0.4994237125 - 0.5521588922 0.5366820097 0.5004308820 - 0.5531268716 0.5376489758 0.5014380217 - 0.5540925264 0.5386124253 0.5024443269 - 0.5550501943 0.5395649076 0.5034484267 - 0.5560079813 0.5405173898 0.5044525266 - 0.5569657087 0.5414698124 0.5054566264 - 0.5579233170 0.5424215198 0.5064595938 - 0.5588781238 0.5433623791 0.5074540973 - 0.5598328710 0.5443031788 0.5084484816 - 0.5607876778 0.5452440977 0.5094429851 - 0.5617424846 0.5461850166 0.5104374886 - 0.5626881123 0.5471224189 0.5114172101 - 0.5636314154 0.5480589867 0.5123947859 - 0.5645747781 0.5489956737 0.5133724809 - 0.5655180812 0.5499323010 0.5143501163 - 0.5664566755 0.5508654118 0.5153220296 - 0.5673907995 0.5517954230 0.5162897706 - 0.5683248043 0.5527253747 0.5172575712 - 0.5692589283 0.5536553860 0.5182253718 - 0.5701919794 0.5545833111 0.5191931129 - 0.5711194873 0.5555014014 0.5201603770 - 0.5720471144 0.5564193726 0.5211277008 - 0.5729746819 0.5573375225 0.5220949054 - 0.5739021897 0.5582554936 0.5230622292 - 0.5748242736 0.5591751933 0.5240322948 - 0.5757443905 0.5600954294 0.5250031948 - 0.5766643882 0.5610156059 0.5259739757 - 0.5775845051 0.5619357824 0.5269448757 - 0.5785031915 0.5628547072 0.5279111862 - 0.5794178843 0.5637699962 0.5288679004 - 0.5803325176 0.5646852851 0.5298246741 - 0.5812472105 0.5656005740 0.5307815075 - 0.5821619034 0.5665159225 0.5317382216 - 0.5830687881 0.5674222708 0.5326874852 - 0.5839726925 0.5683256984 0.5336347222 - 0.5848765969 0.5692290068 0.5345818996 - 0.5857805014 0.5701323152 0.5355291963 - 0.5866835713 0.5710356236 0.5364757776 - 0.5875809193 0.5719385743 0.5374200940 - 0.5884782076 0.5728415251 0.5383644104 - 0.5893754959 0.5737444758 0.5393087268 - 0.5902727246 0.5746474266 0.5402529836 - 0.5911679864 0.5755448937 0.5411959887 - 0.5920606852 0.5764362812 0.5421379209 - 0.5929535031 0.5773277283 0.5430796742 - 0.5938463211 0.5782189965 0.5440214872 - 0.5947390795 0.5791103840 0.5449634194 - 0.5956212878 0.5799939036 0.5458949208 - 0.5964996219 0.5808746815 0.5468239188 - 0.5973780155 0.5817555785 0.5477527976 - 0.5982564092 0.5826364160 0.5486817956 - 0.5991346836 0.5835173130 0.5496106148 - 0.6000012755 0.5843989253 0.5505310297 - 0.6008672118 0.5852805972 0.5514513254 - 0.6017332077 0.5861622095 0.5523716807 - 0.6025990844 0.5870438814 0.5532919765 - 0.6034640074 0.5879251957 0.5542123914 - 0.6043193936 0.5888038874 0.5551329255 - 0.6051747203 0.5896825194 0.5560534000 - 0.6060299873 0.5905610919 0.5569739938 - 0.6068853140 0.5914397240 0.5578945279 - 0.6077398062 0.5923171043 0.5588160753 - 0.6085901856 0.5931892991 0.5597410798 - 0.6094405055 0.5940614939 0.5606660247 - 0.6102908254 0.5949336886 0.5615910292 - 0.6111412048 0.5958060026 0.5625159144 - 0.6119889021 0.5966781974 0.5634377003 - 0.6128252149 0.5975505114 0.5643500090 - 0.6136614084 0.5984228253 0.5652621984 - 0.6144977212 0.5992950201 0.5661745071 - 0.6153339148 0.6001672745 0.5670866966 - 0.6161686182 0.6010373831 0.5679973960 - 0.6169930100 0.6018946171 0.5689010024 - 0.6178174019 0.6027516723 0.5698046088 - 0.6186416745 0.6036089063 0.5707082152 - 0.6194661260 0.6044660807 0.5716118813 - 0.6202905178 0.6053230762 0.5725151896 - 0.6211113930 0.6061736941 0.5734145045 - 0.6219323277 0.6070241928 0.5743138194 - 0.6227532029 0.6078748107 0.5752130747 - 0.6235740781 0.6087254286 0.5761123896 - 0.6243950725 0.6095759273 0.5770117044 - 0.6252127886 0.6104241014 0.5779047012 - 0.6260297894 0.6112717986 0.5787969232 - 0.6268466711 0.6121194959 0.5796890259 - 0.6276636720 0.6129671931 0.5805811286 - 0.6284806728 0.6138148904 0.5814732909 - 0.6292939782 0.6146571040 0.5823628902 - 0.6301044226 0.6154950857 0.5832508206 - 0.6309146881 0.6163331270 0.5841388106 - 0.6317250729 0.6171712279 0.5850268006 - 0.6325353980 0.6180092096 0.5859146714 - 0.6333454847 0.6188457012 0.5868017077 - 0.6341544986 0.6196774244 0.5876858830 - 0.6349633932 0.6205092072 0.5885701776 - 0.6357724071 0.6213408709 0.5894544721 - 0.6365814209 0.6221725941 0.5903387070 - 0.6373903155 0.6230043173 0.5912230015 - 0.6381942034 0.6238338947 0.5921056867 - 0.6389968991 0.6246629953 0.5929881930 - 0.6397994757 0.6254922152 0.5938706994 - 0.6406021714 0.6263213158 0.5947532058 - 0.6414049268 0.6271504164 0.5956355929 - 0.6422066092 0.6279762983 0.5965138078 - 0.6430063248 0.6287959814 0.5973852873 - 0.6438059807 0.6296157837 0.5982567072 - 0.6446056962 0.6304355264 0.5991281271 - 0.6454054117 0.6312552094 0.5999996066 - 0.6462051868 0.6320748925 0.6008710265 - 0.6469957829 0.6328898072 0.6017335057 - 0.6477838159 0.6337034106 0.6025940776 - 0.6485717297 0.6345170736 0.6034547091 - 0.6493597031 0.6353306770 0.6043154001 - 0.6501476765 0.6361442804 0.6051759720 - 0.6509342790 0.6369585991 0.6060351729 - 0.6517130733 0.6377763152 0.6068887711 - 0.6524918079 0.6385939717 0.6077424288 - 0.6532706022 0.6394116879 0.6085960269 - 0.6540492773 0.6402294040 0.6094496250 - 0.6548280716 0.6410471201 0.6103032231 - 0.6556051970 0.6418619156 0.6111536026 - 0.6563801169 0.6426733732 0.6120008230 - 0.6571549773 0.6434847713 0.6128481030 - 0.6579298973 0.6442962289 0.6136953235 - 0.6587048173 0.6451076269 0.6145424843 - 0.6594796777 0.6459190249 0.6153897047 - 0.6602489948 0.6467270255 0.6162346005 - 0.6610152721 0.6475332975 0.6170787215 - 0.6617817283 0.6483396292 0.6179227829 - 0.6625480056 0.6491457820 0.6187667847 - 0.6633144021 0.6499521136 0.6196109056 - 0.6640806794 0.6507583857 0.6204549074 - 0.6648426056 0.6515669823 0.6212937236 - 0.6656033993 0.6523761153 0.6221314073 - 0.6663640738 0.6531851888 0.6229692101 - 0.6671249270 0.6539943218 0.6238070130 - 0.6678856015 0.6548035145 0.6246448159 - 0.6686462760 0.6556125879 0.6254826188 - 0.6694011092 0.6564118862 0.6263144016 - 0.6701549292 0.6572096944 0.6271455884 - 0.6709085703 0.6580076218 0.6279767752 - 0.6716623902 0.6588053703 0.6288080215 - 0.6724160910 0.6596031785 0.6296392083 - 0.6731699109 0.6604009867 0.6304703951 - 0.6739165187 0.6611961722 0.6312931180 - 0.6746615767 0.6619908214 0.6321147084 - 0.6754066944 0.6627854109 0.6329362988 - 0.6761518121 0.6635800004 0.6337578297 - 0.6768969893 0.6643745899 0.6345794201 - 0.6776421070 0.6651691794 0.6354010105 - 0.6783866882 0.6659619808 0.6362180114 - 0.6791309714 0.6667541862 0.6370335817 - 0.6798753738 0.6675463915 0.6378492713 - 0.6806197762 0.6683385968 0.6386650205 - 0.6813641787 0.6691308022 0.6394807100 - 0.6821085215 0.6699230075 0.6402962804 - 0.6828457713 0.6707084775 0.6411076188 - 0.6835768819 0.6714885235 0.6419157982 - 0.6843081117 0.6722685099 0.6427239776 - 0.6850392222 0.6730484962 0.6435320973 - 0.6857702732 0.6738284826 0.6443402767 - 0.6865015030 0.6746084094 0.6451485157 - 0.6872311234 0.6753873229 0.6459537148 - 0.6879568100 0.6761630774 0.6467525959 - 0.6886826158 0.6769388914 0.6475514174 - 0.6894083023 0.6777147055 0.6483502984 - 0.6901339889 0.6784905195 0.6491491199 - 0.6908596754 0.6792662740 0.6499480009 - 0.6915854216 0.6800420880 0.6507468224 - 0.6923050880 0.6808115244 0.6515396833 - 0.6930242777 0.6815804243 0.6523323059 - 0.6937434077 0.6823492050 0.6531249285 - 0.6944625974 0.6831181049 0.6539174914 - 0.6951817274 0.6838870049 0.6547101140 - 0.6959009171 0.6846559048 0.6555026770 - 0.6966161728 0.6854197979 0.6562970281 - 0.6973273754 0.6861783862 0.6570926905 - 0.6980385184 0.6869369745 0.6578884721 - 0.6987496018 0.6876956224 0.6586841941 - 0.6994606853 0.6884542108 0.6594799757 - 0.7001718283 0.6892127991 0.6602758169 - 0.7008829117 0.6899713874 0.6610714793 - 0.7015895844 0.6907256246 0.6618655920 - 0.7022960782 0.6914795041 0.6626598239 - 0.7030025125 0.6922335029 0.6634538770 - 0.7037090063 0.6929875016 0.6642479897 - 0.7044153810 0.6937413812 0.6650421023 - 0.7051218152 0.6944953799 0.6658362150 - 0.7058250904 0.6952432990 0.6666283011 - 0.7065216899 0.6959791780 0.6674168706 - 0.7072182894 0.6967151165 0.6682054996 - 0.7079148889 0.6974509954 0.6689941287 - 0.7086114287 0.6981868744 0.6697826982 - 0.7093080282 0.6989226937 0.6705713272 - 0.7100046277 0.6996585727 0.6713598967 - 0.7106981277 0.7003877759 0.6721469760 - 0.7113897204 0.7011129260 0.6729333997 - 0.7120811939 0.7018380165 0.6737198234 - 0.7127727866 0.7025631070 0.6745061278 - 0.7134643793 0.7032880783 0.6752924919 - 0.7141559720 0.7040132284 0.6760789156 - 0.7148475051 0.7047383189 0.6768652201 - 0.7155426741 0.7054607272 0.6776456833 - 0.7162386775 0.7061824799 0.6784251928 - 0.7169346809 0.7069042921 0.6792047024 - 0.7176308036 0.7076261044 0.6799842715 - 0.7183268070 0.7083479166 0.6807637811 - 0.7190228105 0.7090697289 0.6815432906 - 0.7197188139 0.7097914815 0.6823228002 - 0.7204080224 0.7105045915 0.6830921769 - 0.7210965157 0.7112171054 0.6838611960 - 0.7217851281 0.7119296193 0.6846302748 - 0.7224736214 0.7126420140 0.6853992939 - 0.7231621742 0.7133545280 0.6861683130 - 0.7238507271 0.7140669823 0.6869373918 - 0.7245392799 0.7147794962 0.6877064109 - 0.7252277136 0.7154844999 0.6884679794 - 0.7259160280 0.7161890268 0.6892294884 - 0.7266044021 0.7168934941 0.6899909973 - 0.7272927165 0.7175980806 0.6907523870 - 0.7279810905 0.7183026075 0.6915138960 - 0.7286694050 0.7190070748 0.6922754049 - 0.7293577790 0.7197117209 0.6930367947 - 0.7300446033 0.7204123139 0.6937997937 - 0.7307311893 0.7211123109 0.6945629120 - 0.7314177752 0.7218121886 0.6953259706 - 0.7321043015 0.7225121856 0.6960890889 - 0.7327908874 0.7232121825 0.6968522072 - 0.7334774733 0.7239121795 0.6976153255 - 0.7341641188 0.7246121764 0.6983783841 - 0.7348474860 0.7253112793 0.6991379261 - 0.7355294824 0.7260100842 0.6998959780 - 0.7362114787 0.7267090082 0.7006539702 - 0.7368934155 0.7274078131 0.7014120817 - 0.7375754118 0.7281066179 0.7021701932 - 0.7382574081 0.7288054228 0.7029281855 - 0.7389392853 0.7295042276 0.7036862969 - 0.7396183014 0.7302008867 0.7044391036 - 0.7402927279 0.7308946252 0.7051851749 - 0.7409670949 0.7315883040 0.7059314251 - 0.7416415811 0.7322819829 0.7066774964 - 0.7423160076 0.7329757214 0.7074236870 - 0.7429903746 0.7336694002 0.7081698179 - 0.7436649203 0.7343631983 0.7089160085 - 0.7443389893 0.7350566983 0.7096619010 - 0.7450042963 0.7357468009 0.7104052901 - 0.7456694841 0.7364367843 0.7111486793 - 0.7463346720 0.7371268868 0.7118921280 - 0.7469999790 0.7378168702 0.7126353979 - 0.7476652265 0.7385069728 0.7133787870 - 0.7483304739 0.7391970158 0.7141221762 - 0.7489957213 0.7398871183 0.7148655057 - 0.7496609092 0.7405748963 0.7156080008 - 0.7503259182 0.7412611246 0.7163497806 - 0.7509909868 0.7419472933 0.7170916200 - 0.7516561151 0.7426335216 0.7178335190 - 0.7523211241 0.7433196902 0.7185752988 - 0.7529861927 0.7440059185 0.7193170786 - 0.7536513209 0.7446920872 0.7200589180 - 0.7543162704 0.7453783154 0.7208005786 - 0.7549796104 0.7460603714 0.7215341926 - 0.7556427717 0.7467426062 0.7222678065 - 0.7563059926 0.7474247217 0.7230014205 - 0.7569692731 0.7481067777 0.7237349749 - 0.7576324940 0.7487888932 0.7244685888 - 0.7582957149 0.7494711280 0.7252022028 - 0.7589588761 0.7501531839 0.7259358168 - 0.7596207261 0.7508332729 0.7266669273 - 0.7602791786 0.7515090108 0.7273933887 - 0.7609376907 0.7521848083 0.7281197906 - 0.7615962029 0.7528604865 0.7288463116 - 0.7622547150 0.7535362244 0.7295727134 - 0.7629132271 0.7542120218 0.7302991152 - 0.7635716796 0.7548877001 0.7310255766 - 0.7642301917 0.7555633783 0.7317519784 - 0.7648857832 0.7562360168 0.7324793935 - 0.7655386925 0.7569059134 0.7332075834 - 0.7661917210 0.7575758100 0.7339357734 - 0.7668445706 0.7582457066 0.7346640229 - 0.7674975991 0.7589156032 0.7353920937 - 0.7681505084 0.7595854998 0.7361202836 - 0.7688034773 0.7602553964 0.7368484735 - 0.7694563866 0.7609252930 0.7375766039 - 0.7701069713 0.7615932822 0.7383013964 - 0.7707563043 0.7622603774 0.7390245795 - 0.7714055777 0.7629274726 0.7397478223 - 0.7720549107 0.7635946274 0.7404710054 - 0.7727041841 0.7642617226 0.7411941886 - 0.7733533978 0.7649288177 0.7419173717 - 0.7740026712 0.7655959129 0.7426406145 - 0.7746520042 0.7662630081 0.7433637977 - 0.7752993703 0.7669281960 0.7440791726 - 0.7759457231 0.7675923109 0.7447909713 - 0.7765920162 0.7682564259 0.7455028296 - 0.7772383094 0.7689204812 0.7462146282 - 0.7778846025 0.7695845962 0.7469264269 - 0.7785308957 0.7702487111 0.7476382256 - 0.7791771889 0.7709128261 0.7483499050 - 0.7798236012 0.7715768814 0.7490617037 - 0.7804623246 0.7722384930 0.7497718930 - 0.7810946703 0.7728980184 0.7504810095 - 0.7817270756 0.7735574841 0.7511901259 - 0.7823594213 0.7742168903 0.7518991232 - 0.7829918265 0.7748764157 0.7526081800 - 0.7836241722 0.7755358815 0.7533171773 - 0.7842565775 0.7761954069 0.7540262938 - 0.7848889828 0.7768548727 0.7547354102 - 0.7855191231 0.7775127292 0.7554432154 - 0.7861444950 0.7781673074 0.7561488748 - 0.7867699265 0.7788218856 0.7568545938 - 0.7873954177 0.7794765830 0.7575603127 - 0.7880207896 0.7801312208 0.7582659721 - 0.7886462212 0.7807857990 0.7589715719 - 0.7892715931 0.7814403772 0.7596772909 - 0.7898970842 0.7820950150 0.7603830099 - 0.7905223966 0.7827494740 0.7610880733 - 0.7911401987 0.7833991051 0.7617810965 - 0.7917578816 0.7840486765 0.7624740005 - 0.7923756838 0.7846983075 0.7631670237 - 0.7929934859 0.7853478789 0.7638599873 - 0.7936112285 0.7859973907 0.7645528913 - 0.7942289710 0.7866470218 0.7652459145 - 0.7948467135 0.7872965932 0.7659388185 - 0.7954645157 0.7879462242 0.7666317821 - 0.7960770130 0.7885926962 0.7673208714 - 0.7966856956 0.7892370224 0.7680076957 - 0.7972943783 0.7898812890 0.7686944008 - 0.7979031205 0.7905256152 0.7693811059 - 0.7985118032 0.7911698818 0.7700678110 - 0.7991204858 0.7918142080 0.7707545757 - 0.7997291088 0.7924584746 0.7714412808 - 0.8003377914 0.7931028008 0.7721279860 - 0.8009464741 0.7937468290 0.7728148103 - 0.8015543222 0.7943841815 0.7735018730 - 0.8021621108 0.7950215936 0.7741889954 - 0.8027698994 0.7956590056 0.7748761773 - 0.8033776879 0.7962964177 0.7755632997 - 0.8039855957 0.7969338298 0.7762504816 - 0.8045933843 0.7975711823 0.7769376040 - 0.8052011728 0.7982085943 0.7776247859 - 0.8058090210 0.7988460064 0.7783119082 - 0.8064159751 0.7994800806 0.7789977193 - 0.8070214987 0.8001086712 0.7796815038 - 0.8076270223 0.8007372022 0.7803652883 - 0.8082324862 0.8013656735 0.7810490131 - 0.8088380098 0.8019942045 0.7817327976 - 0.8094434738 0.8026227951 0.7824165225 - 0.8100489974 0.8032513261 0.7831003070 - 0.8106545210 0.8038797975 0.7837840915 - 0.8112599850 0.8045083284 0.7844678164 - 0.8118627071 0.8051316142 0.7851473093 - 0.8124632239 0.8057510257 0.7858238816 - 0.8130636811 0.8063703775 0.7865005732 - 0.8136641979 0.8069897890 0.7871773243 - 0.8142647147 0.8076092005 0.7878538966 - 0.8148651719 0.8082286119 0.7885305882 - 0.8154656887 0.8088480234 0.7892072201 - 0.8160662055 0.8094673753 0.7898839116 - 0.8166667223 0.8100867867 0.7905604839 - 0.8172659874 0.8107038736 0.7912366986 - 0.8178647161 0.8113198876 0.7919127941 - 0.8184633851 0.8119359016 0.7925887704 - 0.8190621138 0.8125519156 0.7932648063 - 0.8196607828 0.8131679296 0.7939407825 - 0.8202595115 0.8137838840 0.7946168184 - 0.8208581805 0.8143998981 0.7952927947 - 0.8214569092 0.8150159121 0.7959687710 - 0.8220555782 0.8156319261 0.7966448069 - 0.8226491213 0.8162443042 0.7973200083 - 0.8232396245 0.8168547153 0.7979946733 - 0.8238301873 0.8174651861 0.7986695170 - 0.8244208097 0.8180755973 0.7993443012 - 0.8250113726 0.8186861277 0.8000190258 - 0.8256019950 0.8192964792 0.8006938100 - 0.8261926174 0.8199068904 0.8013685942 - 0.8267831802 0.8205174208 0.8020433187 - 0.8273738027 0.8211277723 0.8027181029 - 0.8279619217 0.8217353821 0.8033875227 - 0.8285475969 0.8223400116 0.8040519953 - 0.8291332126 0.8229447007 0.8047165871 - 0.8297188878 0.8235492706 0.8053811789 - 0.8303046227 0.8241540194 0.8060457110 - 0.8308901787 0.8247585893 0.8067103028 - 0.8314759135 0.8253632784 0.8073748946 - 0.8320615888 0.8259679079 0.8080394864 - 0.8326472044 0.8265725970 0.8087040186 - 0.8332327008 0.8271760941 0.8093675971 - 0.8338174224 0.8277755976 0.8100281954 - 0.8344022036 0.8283752203 0.8106889129 - 0.8349869251 0.8289747834 0.8113495111 - 0.8355717063 0.8295742869 0.8120101094 - 0.8361564279 0.8301739097 0.8126708269 - 0.8367410898 0.8307734132 0.8133314252 - 0.8373258710 0.8313729763 0.8139920831 - 0.8379105926 0.8319725990 0.8146526814 - 0.8384953737 0.8325721025 0.8153132796 - 0.8390772939 0.8331714272 0.8159744740 - 0.8396586180 0.8337705731 0.8166357279 - 0.8402400017 0.8343697786 0.8172969222 - 0.8408213258 0.8349689245 0.8179581165 - 0.8414027095 0.8355680704 0.8186193109 - 0.8419839740 0.8361672759 0.8192806244 - 0.8425654173 0.8367664814 0.8199418187 - 0.8431466818 0.8373656869 0.8206030130 - 0.8437281251 0.8379648924 0.8212642074 - 0.8443076015 0.8385624290 0.8219218254 - 0.8448830247 0.8391563296 0.8225721121 - 0.8454583883 0.8397501707 0.8232223988 - 0.8460338116 0.8403440118 0.8238726258 - 0.8466091752 0.8409379125 0.8245229125 - 0.8471845984 0.8415316939 0.8251731992 - 0.8477599025 0.8421255946 0.8258234859 - 0.8483353257 0.8427193761 0.8264737129 - 0.8489106894 0.8433132768 0.8271239996 - 0.8494861126 0.8439071178 0.8277742863 - 0.8500558138 0.8444991112 0.8284201026 - 0.8506222963 0.8450899720 0.8290638924 - 0.8511888981 0.8456808925 0.8297076821 - 0.8517553806 0.8462718129 0.8303514719 - 0.8523219228 0.8468626738 0.8309952021 - 0.8528885245 0.8474537134 0.8316389918 - 0.8534550071 0.8480445743 0.8322827816 - 0.8540216088 0.8486354947 0.8329265118 - 0.8545880914 0.8492264152 0.8335703015 - 0.8551546931 0.8498172760 0.8342140913 - 0.8557175994 0.8504027724 0.8348590136 - 0.8562799096 0.8509873748 0.8355041742 - 0.8568422794 0.8515719771 0.8361492753 - 0.8574045897 0.8521565795 0.8367943764 - 0.8579670191 0.8527411819 0.8374394774 - 0.8585293293 0.8533257842 0.8380845785 - 0.8590915799 0.8539103866 0.8387296796 - 0.8596540093 0.8544949889 0.8393748999 - 0.8602163196 0.8550797105 0.8400200009 - 0.8607786894 0.8556643128 0.8406651020 - 0.8613408804 0.8562449217 0.8413107991 - 0.8619030714 0.8568252921 0.8419564962 - 0.8624653816 0.8574056029 0.8426023126 - 0.8630275726 0.8579859734 0.8432480097 - 0.8635898232 0.8585664034 0.8438937068 - 0.8641520739 0.8591467738 0.8445394039 - 0.8647143245 0.8597272038 0.8451852202 - 0.8652765155 0.8603075743 0.8458309174 - 0.8658387065 0.8608880043 0.8464766145 - 0.8664010167 0.8614683747 0.8471223116 - 0.8669610023 0.8620395064 0.8477607965 - 0.8675208092 0.8626096249 0.8483986259 - 0.8680804968 0.8631796241 0.8490363955 - 0.8686403036 0.8637496829 0.8496742249 - 0.8691999912 0.8643196821 0.8503119946 - 0.8697597980 0.8648898005 0.8509498835 - 0.8703196049 0.8654597998 0.8515877128 - 0.8708792925 0.8660299182 0.8522254825 - 0.8714390993 0.8665999174 0.8528633118 - 0.8719987869 0.8671699762 0.8535010815 - 0.8725550175 0.8677337766 0.8541374803 - 0.8731096983 0.8682951927 0.8547734022 - 0.8736643195 0.8688564897 0.8554093242 - 0.8742190003 0.8694179058 0.8560451865 - 0.8747736216 0.8699792027 0.8566811085 - 0.8753283024 0.8705404997 0.8573169708 - 0.8758829236 0.8711019158 0.8579528928 - 0.8764376044 0.8716632128 0.8585888147 - 0.8769922256 0.8722246289 0.8592246175 - 0.8775469065 0.8727859259 0.8598604798 - 0.8781003952 0.8733474016 0.8604974151 - 0.8786519766 0.8739091754 0.8611357212 - 0.8792036176 0.8744710088 0.8617740870 - 0.8797551990 0.8750327826 0.8624123931 - 0.8803067803 0.8755946755 0.8630506992 - 0.8808584213 0.8761565089 0.8636891246 - 0.8814098835 0.8767182827 0.8643273711 - 0.8819615245 0.8772801161 0.8649656773 - 0.8825131059 0.8778418899 0.8656041026 - 0.8830646873 0.8784037232 0.8662424088 - 0.8836163282 0.8789654970 0.8668807149 - 0.8841652870 0.8795219064 0.8675110936 - 0.8847140074 0.8800777793 0.8681409955 - 0.8852627277 0.8806337714 0.8687707782 - 0.8858113885 0.8811897039 0.8694006205 - 0.8863601089 0.8817455769 0.8700305223 - 0.8869088292 0.8823016286 0.8706603050 - 0.8874574900 0.8828575015 0.8712900877 - 0.8880062103 0.8834133744 0.8719198704 - 0.8885548711 0.8839693069 0.8725497723 - 0.8891035914 0.8845252991 0.8731796145 - 0.8896498084 0.8850799799 0.8738080263 - 0.8901898265 0.8856320977 0.8744335175 - 0.8907297850 0.8861840963 0.8750590086 - 0.8912699223 0.8867360950 0.8756844997 - 0.8918098807 0.8872882128 0.8763099909 - 0.8923500180 0.8878402114 0.8769354820 - 0.8928899765 0.8883923292 0.8775609732 - 0.8934299946 0.8889443278 0.8781865239 - 0.8939700723 0.8894963861 0.8788120151 - 0.8945100904 0.8900483847 0.8794375062 - 0.8950502276 0.8906005025 0.8800629973 - 0.8955878019 0.8911523819 0.8806902766 - 0.8961235285 0.8917043209 0.8813189268 - 0.8966590762 0.8922562003 0.8819475174 - 0.8971948028 0.8928080201 0.8825762272 - 0.8977304101 0.8933598995 0.8832048178 - 0.8982660770 0.8939117789 0.8838335276 - 0.8988016844 0.8944637179 0.8844621181 - 0.8993372917 0.8950155973 0.8850907087 - 0.8998730183 0.8955674171 0.8857194185 - 0.9004086256 0.8961192966 0.8863480091 - 0.9009442925 0.8966711760 0.8869767189 - 0.9014744759 0.8972206712 0.8876035810 - 0.9020017982 0.8977689147 0.8882297873 - 0.9025291800 0.8983170986 0.8888558745 - 0.9030565023 0.8988652825 0.8894820213 - 0.9035838246 0.8994135261 0.8901082277 - 0.9041110873 0.8999617100 0.8907343149 - 0.9046384096 0.9005097747 0.8913604021 - 0.9051656723 0.9010580182 0.8919866085 - 0.9056929946 0.9016062021 0.8926126957 - 0.9062203169 0.9021543860 0.8932387829 - 0.9067475796 0.9027026296 0.8938649893 - 0.9072715044 0.9032499790 0.8944861889 - 0.9077931046 0.9037966728 0.8951045275 - 0.9083147049 0.9043434858 0.8957228065 - 0.9088363051 0.9048901796 0.8963410854 - 0.9093579054 0.9054369926 0.8969594240 - 0.9098795056 0.9059836864 0.8975777030 - 0.9104011059 0.9065304995 0.8981959224 - 0.9109227061 0.9070771933 0.8988142014 - 0.9114443064 0.9076240063 0.8994324803 - 0.9119659066 0.9081707001 0.9000508189 - 0.9124875069 0.9087175131 0.9006690979 - 0.9130079150 0.9092611074 0.9012864828 - 0.9135265946 0.9098002911 0.9019027948 - 0.9140452743 0.9103394747 0.9025191069 - 0.9145638943 0.9108787179 0.9031354189 - 0.9150825739 0.9114179015 0.9037516713 - 0.9156013131 0.9119570851 0.9043679833 - 0.9161199927 0.9124963284 0.9049841762 - 0.9166386724 0.9130355120 0.9056004882 - 0.9171572924 0.9135746956 0.9062168002 - 0.9176759720 0.9141138792 0.9068331122 - 0.9181947112 0.9146531224 0.9074494243 - 0.9187132120 0.9151920080 0.9080653191 - 0.9192286134 0.9157255292 0.9086763859 - 0.9197440147 0.9162589908 0.9092875123 - 0.9202594161 0.9167925715 0.9098985791 - 0.9207748175 0.9173260927 0.9105097055 - 0.9212902188 0.9178596735 0.9111207724 - 0.9218056202 0.9183931947 0.9117318988 - 0.9223210216 0.9189267159 0.9123430252 - 0.9228364229 0.9194602966 0.9129540920 - 0.9233518243 0.9199938178 0.9135652184 - 0.9238672256 0.9205272794 0.9141762853 - 0.9243826270 0.9210609198 0.9147874117 - 0.9248952866 0.9215934277 0.9153928161 - 0.9254056215 0.9221249819 0.9159939289 - 0.9259160161 0.9226565957 0.9165949821 - 0.9264264107 0.9231882095 0.9171959758 - 0.9269366860 0.9237198830 0.9177970886 - 0.9274470806 0.9242514968 0.9183982015 - 0.9279574752 0.9247831106 0.9189993143 - 0.9284678102 0.9253147840 0.9196004272 - 0.9289782047 0.9258463979 0.9202014208 - 0.9294885993 0.9263780117 0.9208024740 - 0.9299988747 0.9269096255 0.9214035869 - 0.9305093288 0.9274412990 0.9220046997 - 0.9310135245 0.9279668927 0.9225972295 - 0.9315168858 0.9284918904 0.9231889248 - 0.9320203066 0.9290168881 0.9237805009 - 0.9325237870 0.9295418262 0.9243721962 - 0.9330272079 0.9300668240 0.9249637723 - 0.9335306287 0.9305917025 0.9255555272 - 0.9340341091 0.9311167002 0.9261471033 - 0.9345374703 0.9316416979 0.9267387986 - 0.9350408912 0.9321665764 0.9273303747 - 0.9355443120 0.9326915741 0.9279221296 - 0.9360477924 0.9332165718 0.9285137057 - 0.9365504980 0.9337409139 0.9291048050 - 0.9370446801 0.9342573285 0.9296892881 - 0.9375388026 0.9347736835 0.9302738905 - 0.9380329847 0.9352902174 0.9308584929 - 0.9385271072 0.9358065724 0.9314429760 - 0.9390212893 0.9363229871 0.9320275784 - 0.9395154119 0.9368394017 0.9326121807 - 0.9400095940 0.9373558760 0.9331967235 - 0.9405037165 0.9378722906 0.9337813258 - 0.9409978986 0.9383887053 0.9343659282 - 0.9414920211 0.9389051199 0.9349504113 - 0.9419862032 0.9394215941 0.9355350137 - 0.9424793124 0.9399371743 0.9361184239 - 0.9429621100 0.9404459000 0.9366925955 - 0.9434450269 0.9409546256 0.9372667074 - 0.9439278841 0.9414631724 0.9378408194 - 0.9444106817 0.9419718981 0.9384148717 - 0.9448935986 0.9424806237 0.9389889836 - 0.9453763962 0.9429891706 0.9395630956 - 0.9458593130 0.9434978962 0.9401373267 - 0.9463421106 0.9440066218 0.9407113791 - 0.9468250275 0.9445152283 0.9412854910 - 0.9473078847 0.9450238943 0.9418596029 - 0.9477906823 0.9455326200 0.9424337149 - 0.9482735991 0.9460412264 0.9430078268 - 0.9487515092 0.9465460181 0.9435728788 - 0.9492291808 0.9470505714 0.9441375732 - 0.9497069120 0.9475551844 0.9447023273 - 0.9501845837 0.9480597973 0.9452670813 - 0.9506623149 0.9485644102 0.9458317757 - 0.9511399865 0.9490690231 0.9463965297 - 0.9516175985 0.9495735168 0.9469612241 - 0.9520953298 0.9500781298 0.9475259781 - 0.9525730014 0.9505826831 0.9480906725 - 0.9530506730 0.9510872960 0.9486554265 - 0.9535284042 0.9515919089 0.9492201209 - 0.9540060759 0.9520965219 0.9497848749 - 0.9544798732 0.9525997043 0.9503433108 - 0.9549517035 0.9531021118 0.9508985281 - 0.9554234743 0.9536045194 0.9514538050 - 0.9558951855 0.9541069269 0.9520090222 - 0.9563670158 0.9546092749 0.9525641799 - 0.9568387270 0.9551116824 0.9531193972 - 0.9573104978 0.9556140900 0.9536746740 - 0.9577823281 0.9561164975 0.9542298913 - 0.9582539797 0.9566189051 0.9547851086 - 0.9587258101 0.9571213722 0.9553403258 - 0.9591975212 0.9576237798 0.9558954835 - 0.9596692920 0.9581261873 0.9564508200 - 0.9601396918 0.9586272836 0.9570031166 - 0.9606035948 0.9591223001 0.9575427771 - 0.9610674977 0.9596173167 0.9580823779 - 0.9615314007 0.9601122737 0.9586220980 - 0.9619953036 0.9606072903 0.9591618180 - 0.9624592066 0.9611024261 0.9597014189 - 0.9629231095 0.9615973830 0.9602410793 - 0.9633870125 0.9620923996 0.9607807994 - 0.9638509154 0.9625874162 0.9613204002 - 0.9643148184 0.9630824924 0.9618601203 - 0.9647787809 0.9635775089 0.9623997808 - 0.9652426839 0.9640725255 0.9629393816 - 0.9657065868 0.9645674825 0.9634791017 - 0.9661654830 0.9650583267 0.9640104771 - 0.9666200876 0.9655454755 0.9645349979 - 0.9670745730 0.9660326242 0.9650595188 - 0.9675291777 0.9665197730 0.9655839801 - 0.9679837823 0.9670069218 0.9661085010 - 0.9684383869 0.9674940705 0.9666330218 - 0.9688928723 0.9679812193 0.9671574831 - 0.9693474770 0.9684684277 0.9676818848 - 0.9698020816 0.9689555764 0.9682064056 - 0.9702566862 0.9694427252 0.9687309265 - 0.9707111716 0.9699298739 0.9692553878 - 0.9711657763 0.9704170227 0.9697799087 - 0.9716203809 0.9709041715 0.9703044295 - 0.9720690846 0.9713829756 0.9708178043 - 0.9725155234 0.9718586206 0.9713271856 - 0.9729619026 0.9723342061 0.9718365073 - 0.9734084010 0.9728097916 0.9723458290 - 0.9738547802 0.9732853770 0.9728550911 - 0.9743012190 0.9737610221 0.9733644724 - 0.9747477174 0.9742366076 0.9738737941 - 0.9751940966 0.9747121930 0.9743831158 - 0.9756405950 0.9751877785 0.9748924971 - 0.9760869741 0.9756634235 0.9754018188 - 0.9765334129 0.9761390090 0.9759110808 - 0.9769799113 0.9766145945 0.9764205217 - 0.9774262905 0.9770901799 0.9769297838 - 0.9778676033 0.9775533080 0.9774206877 - 0.9783070087 0.9780119061 0.9779049754 - 0.9787464142 0.9784705043 0.9783893824 - 0.9791858196 0.9789291024 0.9788737893 - 0.9796252251 0.9793877006 0.9793581963 - 0.9800645709 0.9798462987 0.9798424840 - 0.9805039763 0.9803048968 0.9803268909 - 0.9809433818 0.9807634950 0.9808112979 - 0.9813829064 0.9812219739 0.9812957048 - 0.9818223119 0.9816805720 0.9817801118 - 0.9822617173 0.9821392298 0.9822643995 - 0.9827011228 0.9825978279 0.9827488065 - 0.9831405282 0.9830564260 0.9832332134 - 0.9835739136 0.9835022092 0.9837002158 - 0.9840028286 0.9839385152 0.9841542840 - 0.9844316244 0.9843747020 0.9846084714 - 0.9848604798 0.9848110080 0.9850625992 - 0.9852893949 0.9852473140 0.9855167866 - 0.9857181907 0.9856835008 0.9859709144 - 0.9861471057 0.9861198068 0.9864251018 - 0.9865760207 0.9865561128 0.9868792295 - 0.9870048165 0.9869922996 0.9873334169 - 0.9874336720 0.9874286056 0.9877874851 - 0.9878625870 0.9878647923 0.9882416725 - 0.9882913828 0.9883010983 0.9886958003 - 0.9887202978 0.9887374043 0.9891499877 - 0.9891455173 0.9891687036 0.9895961881 - 0.9895583987 0.9895836115 0.9900164008 - 0.9899712801 0.9899985790 0.9904366136 - 0.9903842807 0.9904134870 0.9908568263 - 0.9907972217 0.9908285141 0.9912769794 - 0.9912101030 0.9912434220 0.9916970730 - 0.9916231036 0.9916582704 0.9921172857 - 0.9920359850 0.9920732975 0.9925374985 - 0.9924489260 0.9924882054 0.9929577112 - 0.9928618073 0.9929031134 0.9933779240 - 0.9932748079 0.9933180809 0.9937980771 - 0.9936876893 0.9937329888 0.9942182899 - 0.9941005707 0.9941480160 0.9946383834 - 0.9945135117 0.9945629239 0.9950585961 - 0.9949113131 0.9949585795 0.9954301715 - 0.9953026772 0.9953464270 0.9957817197 - 0.9956942201 0.9957342148 0.9961332083 - 0.9960855842 0.9961220026 0.9964848161 - 0.9964770079 0.9965097904 0.9968363047 - 0.9968684912 0.9968975782 0.9971877933 - 0.9972599149 0.9972854257 0.9975392818 - 0.9976513982 0.9976732135 0.9978908896 - 0.9980428219 0.9980610013 0.9982423782 - 0.9984341860 0.9984487891 0.9985939264 - 0.9988257289 0.9988365769 0.9989454150 - 0.9992170930 0.9992244244 0.9992970228 - 0.9996085763 0.9996122122 0.9996485114 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0084391097 0.0075763101 0.0055803522 + 0.0169892292 0.0151253100 0.0110553904 + 0.0246818494 0.0219931193 0.0160910394 + 0.0317661613 0.0282940008 0.0208101291 + 0.0382325202 0.0340048485 0.0251940992 + 0.0441172384 0.0393486507 0.0293568596 + 0.0496861897 0.0445001610 0.0333589911 + 0.0550585985 0.0493195914 0.0371792093 + 0.0601223111 0.0539280511 0.0408726782 + 0.0649642274 0.0583757199 0.0444436409 + 0.0695851371 0.0626427680 0.0479431897 + 0.0740094930 0.0667798966 0.0513359718 + 0.0783147216 0.0707852617 0.0546398088 + 0.0825306326 0.0747059137 0.0578773208 + 0.0866608396 0.0785023794 0.0610598996 + 0.0907057226 0.0822077468 0.0641641617 + 0.0946363509 0.0858238488 0.0672321990 + 0.0984090120 0.0893622264 0.0702527389 + 0.1020661965 0.0928608030 0.0732008815 + 0.1056471020 0.0963046327 0.0760966092 + 0.1091898009 0.0996690318 0.0789577886 + 0.1126779020 0.1029520035 0.0817804411 + 0.1161487997 0.1061682031 0.0845761225 + 0.1195472032 0.1093337014 0.0873135701 + 0.1229045019 0.1124323979 0.0900195017 + 0.1262066960 0.1155048013 0.0926894471 + 0.1294586062 0.1185453981 0.0953314528 + 0.1326504052 0.1215455979 0.0979523510 + 0.1357606053 0.1245016977 0.1005545035 + 0.1388241053 0.1273857951 0.1031209007 + 0.1418274045 0.1302534044 0.1056765020 + 0.1447373033 0.1330838054 0.1081990004 + 0.1476096958 0.1358837932 0.1106970012 + 0.1504617035 0.1386432946 0.1131801978 + 0.1533035040 0.1413915008 0.1156400964 + 0.1561179012 0.1440995932 0.1180797964 + 0.1589269936 0.1467884034 0.1204816997 + 0.1617161930 0.1494296938 0.1228761971 + 0.1644722074 0.1520594060 0.1252495944 + 0.1672009975 0.1546591967 0.1275874972 + 0.1699149013 0.1572349966 0.1299159974 + 0.1725970954 0.1597743034 0.1322304010 + 0.1752271056 0.1622851938 0.1345029026 + 0.1778230965 0.1647903025 0.1367422938 + 0.1803787053 0.1672617942 0.1389641017 + 0.1828950047 0.1697237939 0.1411608011 + 0.1853982061 0.1721518934 0.1433494985 + 0.1878785938 0.1745534986 0.1455025971 + 0.1903571934 0.1769458950 0.1476327926 + 0.1928332001 0.1793050021 0.1497652978 + 0.1952915937 0.1816422045 0.1519078016 + 0.1977207959 0.1839872003 0.1540307999 + 0.2001152039 0.1863050014 0.1561508030 + 0.2025077045 0.1886059940 0.1582663059 + 0.2049013972 0.1908763051 0.1603844017 + 0.2072762996 0.1931104958 0.1624784023 + 0.2096343935 0.1953328997 0.1645596027 + 0.2119597048 0.1975608021 0.1666202992 + 0.2142512947 0.1997732073 0.1686652005 + 0.2165051997 0.2019650936 0.1706995964 + 0.2187242955 0.2041545063 0.1727278978 + 0.2209309936 0.2063177973 0.1747494042 + 0.2231297940 0.2084506005 0.1767428070 + 0.2253059000 0.2105776966 0.1787129939 + 0.2274693996 0.2127068937 0.1806745976 + 0.2296334058 0.2148050070 0.1826314926 + 0.2317734957 0.2168878019 0.1845801026 + 0.2338978052 0.2189736068 0.1865082979 + 0.2360040992 0.2210465968 0.1884184927 + 0.2380760014 0.2230933011 0.1903171986 + 0.2401296049 0.2251244932 0.1922022998 + 0.2421852946 0.2271171063 0.1940843016 + 0.2442210019 0.2290779948 0.1959476024 + 0.2462366074 0.2310225964 0.1977925003 + 0.2482510954 0.2329605967 0.1996293068 + 0.2502493858 0.2348946929 0.2014590949 + 0.2522226870 0.2368202060 0.2032801062 + 0.2541834116 0.2387333065 0.2050863057 + 0.2561317980 0.2406367958 0.2068803012 + 0.2580586970 0.2425450981 0.2086673975 + 0.2599673867 0.2444512993 0.2104519010 + 0.2618735135 0.2463417053 0.2122323960 + 0.2637856007 0.2482246011 0.2140039951 + 0.2656947076 0.2501086891 0.2157679945 + 0.2675924003 0.2519941926 0.2175332010 + 0.2694917023 0.2538610101 0.2192887962 + 0.2713840008 0.2557184100 0.2210364938 + 0.2732622921 0.2575724125 0.2227811962 + 0.2751278877 0.2594226897 0.2245233059 + 0.2769821882 0.2612658143 0.2262614965 + 0.2788152099 0.2630968094 0.2279942930 + 0.2806366980 0.2649213970 0.2297101021 + 0.2824515104 0.2667475939 0.2314116955 + 0.2842657864 0.2685596049 0.2331091017 + 0.2860780060 0.2703500092 0.2348040938 + 0.2878656983 0.2721207142 0.2364936024 + 0.2896395028 0.2738777995 0.2381702960 + 0.2914094925 0.2756209075 0.2398329973 + 0.2931736112 0.2773512006 0.2414873987 + 0.2949214876 0.2790701985 0.2431395948 + 0.2966547906 0.2807764113 0.2447883040 + 0.2983818054 0.2824758887 0.2464286983 + 0.3001014888 0.2841669023 0.2480624020 + 0.3017992973 0.2858343124 0.2496916056 + 0.3034814894 0.2875004113 0.2513076067 + 0.3051558137 0.2891632915 0.2529129982 + 0.3068262041 0.2908203900 0.2545062900 + 0.3085027933 0.2924638093 0.2560915053 + 0.3101766109 0.2941013873 0.2576715052 + 0.3118444085 0.2957335114 0.2592464983 + 0.3135068119 0.2973626852 0.2608287036 + 0.3151695132 0.2989872992 0.2624095082 + 0.3168335855 0.3006061018 0.2639889121 + 0.3184846938 0.3022069037 0.2655547857 + 0.3201304972 0.3038052917 0.2671144009 + 0.3217692077 0.3054029942 0.2686659992 + 0.3234055936 0.3069913089 0.2702147961 + 0.3250352144 0.3085735142 0.2717638910 + 0.3266384900 0.3101415038 0.2733153105 + 0.3282344043 0.3117021918 0.2748562098 + 0.3298248947 0.3132582009 0.2763910890 + 0.3314053118 0.3148199916 0.2779105008 + 0.3329854906 0.3163794875 0.2794237137 + 0.3345662951 0.3179340065 0.2809270918 + 0.3361378908 0.3194747865 0.2824262977 + 0.3377039135 0.3210082948 0.2839238942 + 0.3392634988 0.3225387931 0.2854253054 + 0.3408218026 0.3240653872 0.2869231999 + 0.3423773050 0.3255797923 0.2884125113 + 0.3439236879 0.3270798028 0.2898918986 + 0.3454602063 0.3285647929 0.2913635075 + 0.3469888866 0.3300414979 0.2928242981 + 0.3485142887 0.3315150142 0.2942822874 + 0.3500323892 0.3329941928 0.2957330048 + 0.3515473902 0.3344734013 0.2971836030 + 0.3530417979 0.3359495103 0.2986334860 + 0.3545320034 0.3374226987 0.3000814021 + 0.3560110033 0.3388882875 0.3015252054 + 0.3574849069 0.3403522074 0.3029659092 + 0.3589515090 0.3418137133 0.3044033051 + 0.3604162931 0.3432742059 0.3058296144 + 0.3618794084 0.3447338939 0.3072482944 + 0.3633390963 0.3461844921 0.3086608946 + 0.3647964895 0.3476294875 0.3100705147 + 0.3662520945 0.3490661085 0.3114804029 + 0.3677066863 0.3504985869 0.3128904998 + 0.3691571951 0.3519344032 0.3143031001 + 0.3706058860 0.3533715010 0.3157165051 + 0.3720482886 0.3548054099 0.3171201050 + 0.3734880090 0.3562380075 0.3185209036 + 0.3749237061 0.3576630950 0.3199110925 + 0.3763574958 0.3590848148 0.3212977052 + 0.3777863979 0.3605031073 0.3226788938 + 0.3792122900 0.3619194031 0.3240576982 + 0.3806298971 0.3633339107 0.3254354894 + 0.3820405006 0.3647469878 0.3268128037 + 0.3834429979 0.3661527932 0.3281848133 + 0.3848352134 0.3675504029 0.3295519948 + 0.3862208128 0.3689433932 0.3309170902 + 0.3875918090 0.3703270853 0.3322786987 + 0.3889611959 0.3717108071 0.3336370885 + 0.3903222978 0.3730942905 0.3349848986 + 0.3916833997 0.3744777143 0.3363322914 + 0.3930288851 0.3758434951 0.3376722932 + 0.3943736851 0.3772087991 0.3390122950 + 0.3957099915 0.3785588145 0.3403486907 + 0.3970431089 0.3799039125 0.3416841924 + 0.3983731866 0.3812474012 0.3430136144 + 0.3996999860 0.3825891912 0.3443383873 + 0.4010235071 0.3839282095 0.3456582129 + 0.4023340046 0.3852578998 0.3469654024 + 0.4036445916 0.3865875006 0.3482725918 + 0.4049448073 0.3878997862 0.3495655954 + 0.4062437117 0.3892104924 0.3508580923 + 0.4075376093 0.3905136883 0.3521454036 + 0.4088271856 0.3918113112 0.3534295857 + 0.4101122916 0.3931080997 0.3547120988 + 0.4113762975 0.3944020867 0.3559896052 + 0.4126402140 0.3956961036 0.3572671115 + 0.4138968885 0.3969767988 0.3585371077 + 0.4151513875 0.3982538879 0.3598057032 + 0.4164057970 0.3995262980 0.3610734046 + 0.4176602066 0.4007898867 0.3623399138 + 0.4189145863 0.4020535052 0.3636063933 + 0.4201653004 0.4033161998 0.3648683131 + 0.4214152992 0.4045788944 0.3661297858 + 0.4226615131 0.4058408141 0.3673880100 + 0.4239003062 0.4071016014 0.3686417937 + 0.4251390994 0.4083622992 0.3698956072 + 0.4263815880 0.4096068144 0.3711366951 + 0.4276249111 0.4108476937 0.3723761141 + 0.4288657010 0.4120868146 0.3736143112 + 0.4300980866 0.4133198857 0.3748500049 + 0.4313304126 0.4145531058 0.3760857880 + 0.4325571954 0.4157873988 0.3773151934 + 0.4337803125 0.4170222878 0.3785417974 + 0.4350034893 0.4182572067 0.3797681034 + 0.4362299144 0.4194813967 0.3809858859 + 0.4374563098 0.4207052886 0.3822036982 + 0.4386776090 0.4219268858 0.3834176064 + 0.4398860931 0.4231427014 0.3846245110 + 0.4410946071 0.4243586063 0.3858315051 + 0.4422994852 0.4255749881 0.3870328963 + 0.4435017109 0.4267917871 0.3882313073 + 0.4447039068 0.4280084968 0.3894298077 + 0.4459021091 0.4292202890 0.3906225860 + 0.4470992088 0.4304310977 0.3918147981 + 0.4482960999 0.4316413999 0.3930067122 + 0.4494735003 0.4328393936 0.3941971064 + 0.4506509900 0.4340375066 0.3953875005 + 0.4518275857 0.4352346957 0.3965758979 + 0.4530000091 0.4364287853 0.3977586031 + 0.4541724920 0.4376227856 0.3989411891 + 0.4553438127 0.4388133883 0.4001224041 + 0.4565126896 0.4399971962 0.4013010859 + 0.4576815069 0.4411810040 0.4024797976 + 0.4588429928 0.4423573911 0.4036575854 + 0.4599938095 0.4435240030 0.4048345089 + 0.4611446857 0.4446905851 0.4060113132 + 0.4622924924 0.4458532035 0.4071837068 + 0.4634366930 0.4470112920 0.4083524048 + 0.4645808041 0.4481694102 0.4095211029 + 0.4657213092 0.4493247867 0.4106839895 + 0.4668577015 0.4504772127 0.4118421078 + 0.4679940939 0.4516296983 0.4130002856 + 0.4691272080 0.4527780116 0.4141499102 + 0.4702562094 0.4539214969 0.4152916968 + 0.4713852108 0.4550648928 0.4164333940 + 0.4725120962 0.4562053084 0.4175724983 + 0.4736359119 0.4573408067 0.4187080860 + 0.4747596085 0.4584763944 0.4198437035 + 0.4758782089 0.4596096873 0.4209780991 + 0.4769828022 0.4607372880 0.4221099913 + 0.4780873954 0.4618648887 0.4232420027 + 0.4791901112 0.4629899859 0.4243740141 + 0.4802770019 0.4640980959 0.4255062938 + 0.4813640118 0.4652062058 0.4266386926 + 0.4824509025 0.4663142860 0.4277710021 + 0.4835343957 0.4674096107 0.4288952053 + 0.4846175909 0.4685041010 0.4300192893 + 0.4857007861 0.4695985019 0.4311434031 + 0.4867846966 0.4706852138 0.4322606921 + 0.4878689945 0.4717684984 0.4333756864 + 0.4889532924 0.4728518128 0.4344907105 + 0.4900343120 0.4739331901 0.4356062114 + 0.4911102951 0.4750120044 0.4367220104 + 0.4921863079 0.4760906994 0.4378378987 + 0.4932622910 0.4771688879 0.4389531910 + 0.4943380952 0.4782414138 0.4400649071 + 0.4954138100 0.4793137908 0.4411765039 + 0.4964896142 0.4803862870 0.4422881901 + 0.4975619018 0.4814558029 0.4433946908 + 0.4986326098 0.4825241864 0.4444997013 + 0.4997034073 0.4835925102 0.4456045926 + 0.5007709861 0.4846589863 0.4467051029 + 0.5018302202 0.4857206941 0.4477970898 + 0.5028892756 0.4867824912 0.4488891065 + 0.5039485097 0.4878442883 0.4499810040 + 0.5050029755 0.4888986051 0.4510704875 + 0.5060563087 0.4899514019 0.4521594942 + 0.5071097016 0.4910041988 0.4532485902 + 0.5081599951 0.4920547903 0.4543373883 + 0.5092030168 0.4931007922 0.4554258883 + 0.5102459788 0.4941467047 0.4565143883 + 0.5112890005 0.4951925874 0.4576028883 + 0.5123233795 0.4962320924 0.4586854875 + 0.5133545995 0.4972695112 0.4597668052 + 0.5143858790 0.4983069003 0.4608480036 + 0.5154163241 0.4993439913 0.4619261920 + 0.5164411068 0.5003792048 0.4629918933 + 0.5174658895 0.5014144182 0.4640575945 + 0.5184906721 0.5024495721 0.4651232958 + 0.5195080042 0.5034834743 0.4661859870 + 0.5205168128 0.5045161247 0.4672459960 + 0.5215256214 0.5055487752 0.4683060944 + 0.5225343704 0.5065814257 0.4693661034 + 0.5235391855 0.5076125264 0.4704234004 + 0.5245428085 0.5086430907 0.4714801013 + 0.5255464911 0.5096737742 0.4725368917 + 0.5265501142 0.5107042789 0.4735935032 + 0.5275589824 0.5117307901 0.4746482074 + 0.5285679102 0.5127573013 0.4757030010 + 0.5295767188 0.5137838721 0.4767577052 + 0.5305829048 0.5148088932 0.4778087139 + 0.5315781236 0.5158286095 0.4788492024 + 0.5325732827 0.5168482065 0.4798896909 + 0.5335685015 0.5178679228 0.4809302986 + 0.5345616937 0.5188835859 0.4819678068 + 0.5355507731 0.5198920965 0.4830011129 + 0.5365399122 0.5209006071 0.4840342999 + 0.5375291109 0.5219091773 0.4850674868 + 0.5385140777 0.5229142904 0.4861018956 + 0.5394936800 0.5239152908 0.4871374071 + 0.5404732823 0.5249161720 0.4881727993 + 0.5414528847 0.5259171724 0.4892083108 + 0.5424301028 0.5269111991 0.4902406037 + 0.5434045196 0.5278977156 0.4912700951 + 0.5443788171 0.5288841724 0.4922995865 + 0.5453531742 0.5298706889 0.4933289886 + 0.5463275909 0.5308517814 0.4943523109 + 0.5473020077 0.5318266153 0.4953694046 + 0.5482764244 0.5328012705 0.4963865876 + 0.5492507815 0.5337759852 0.4974038005 + 0.5502228141 0.5347477794 0.4984166026 + 0.5511909127 0.5357149243 0.4994237125 + 0.5521588922 0.5366820097 0.5004308820 + 0.5531268716 0.5376489758 0.5014380217 + 0.5540925264 0.5386124253 0.5024443269 + 0.5550501943 0.5395649076 0.5034484267 + 0.5560079813 0.5405173898 0.5044525266 + 0.5569657087 0.5414698124 0.5054566264 + 0.5579233170 0.5424215198 0.5064595938 + 0.5588781238 0.5433623791 0.5074540973 + 0.5598328710 0.5443031788 0.5084484816 + 0.5607876778 0.5452440977 0.5094429851 + 0.5617424846 0.5461850166 0.5104374886 + 0.5626881123 0.5471224189 0.5114172101 + 0.5636314154 0.5480589867 0.5123947859 + 0.5645747781 0.5489956737 0.5133724809 + 0.5655180812 0.5499323010 0.5143501163 + 0.5664566755 0.5508654118 0.5153220296 + 0.5673907995 0.5517954230 0.5162897706 + 0.5683248043 0.5527253747 0.5172575712 + 0.5692589283 0.5536553860 0.5182253718 + 0.5701919794 0.5545833111 0.5191931129 + 0.5711194873 0.5555014014 0.5201603770 + 0.5720471144 0.5564193726 0.5211277008 + 0.5729746819 0.5573375225 0.5220949054 + 0.5739021897 0.5582554936 0.5230622292 + 0.5748242736 0.5591751933 0.5240322948 + 0.5757443905 0.5600954294 0.5250031948 + 0.5766643882 0.5610156059 0.5259739757 + 0.5775845051 0.5619357824 0.5269448757 + 0.5785031915 0.5628547072 0.5279111862 + 0.5794178843 0.5637699962 0.5288679004 + 0.5803325176 0.5646852851 0.5298246741 + 0.5812472105 0.5656005740 0.5307815075 + 0.5821619034 0.5665159225 0.5317382216 + 0.5830687881 0.5674222708 0.5326874852 + 0.5839726925 0.5683256984 0.5336347222 + 0.5848765969 0.5692290068 0.5345818996 + 0.5857805014 0.5701323152 0.5355291963 + 0.5866835713 0.5710356236 0.5364757776 + 0.5875809193 0.5719385743 0.5374200940 + 0.5884782076 0.5728415251 0.5383644104 + 0.5893754959 0.5737444758 0.5393087268 + 0.5902727246 0.5746474266 0.5402529836 + 0.5911679864 0.5755448937 0.5411959887 + 0.5920606852 0.5764362812 0.5421379209 + 0.5929535031 0.5773277283 0.5430796742 + 0.5938463211 0.5782189965 0.5440214872 + 0.5947390795 0.5791103840 0.5449634194 + 0.5956212878 0.5799939036 0.5458949208 + 0.5964996219 0.5808746815 0.5468239188 + 0.5973780155 0.5817555785 0.5477527976 + 0.5982564092 0.5826364160 0.5486817956 + 0.5991346836 0.5835173130 0.5496106148 + 0.6000012755 0.5843989253 0.5505310297 + 0.6008672118 0.5852805972 0.5514513254 + 0.6017332077 0.5861622095 0.5523716807 + 0.6025990844 0.5870438814 0.5532919765 + 0.6034640074 0.5879251957 0.5542123914 + 0.6043193936 0.5888038874 0.5551329255 + 0.6051747203 0.5896825194 0.5560534000 + 0.6060299873 0.5905610919 0.5569739938 + 0.6068853140 0.5914397240 0.5578945279 + 0.6077398062 0.5923171043 0.5588160753 + 0.6085901856 0.5931892991 0.5597410798 + 0.6094405055 0.5940614939 0.5606660247 + 0.6102908254 0.5949336886 0.5615910292 + 0.6111412048 0.5958060026 0.5625159144 + 0.6119889021 0.5966781974 0.5634377003 + 0.6128252149 0.5975505114 0.5643500090 + 0.6136614084 0.5984228253 0.5652621984 + 0.6144977212 0.5992950201 0.5661745071 + 0.6153339148 0.6001672745 0.5670866966 + 0.6161686182 0.6010373831 0.5679973960 + 0.6169930100 0.6018946171 0.5689010024 + 0.6178174019 0.6027516723 0.5698046088 + 0.6186416745 0.6036089063 0.5707082152 + 0.6194661260 0.6044660807 0.5716118813 + 0.6202905178 0.6053230762 0.5725151896 + 0.6211113930 0.6061736941 0.5734145045 + 0.6219323277 0.6070241928 0.5743138194 + 0.6227532029 0.6078748107 0.5752130747 + 0.6235740781 0.6087254286 0.5761123896 + 0.6243950725 0.6095759273 0.5770117044 + 0.6252127886 0.6104241014 0.5779047012 + 0.6260297894 0.6112717986 0.5787969232 + 0.6268466711 0.6121194959 0.5796890259 + 0.6276636720 0.6129671931 0.5805811286 + 0.6284806728 0.6138148904 0.5814732909 + 0.6292939782 0.6146571040 0.5823628902 + 0.6301044226 0.6154950857 0.5832508206 + 0.6309146881 0.6163331270 0.5841388106 + 0.6317250729 0.6171712279 0.5850268006 + 0.6325353980 0.6180092096 0.5859146714 + 0.6333454847 0.6188457012 0.5868017077 + 0.6341544986 0.6196774244 0.5876858830 + 0.6349633932 0.6205092072 0.5885701776 + 0.6357724071 0.6213408709 0.5894544721 + 0.6365814209 0.6221725941 0.5903387070 + 0.6373903155 0.6230043173 0.5912230015 + 0.6381942034 0.6238338947 0.5921056867 + 0.6389968991 0.6246629953 0.5929881930 + 0.6397994757 0.6254922152 0.5938706994 + 0.6406021714 0.6263213158 0.5947532058 + 0.6414049268 0.6271504164 0.5956355929 + 0.6422066092 0.6279762983 0.5965138078 + 0.6430063248 0.6287959814 0.5973852873 + 0.6438059807 0.6296157837 0.5982567072 + 0.6446056962 0.6304355264 0.5991281271 + 0.6454054117 0.6312552094 0.5999996066 + 0.6462051868 0.6320748925 0.6008710265 + 0.6469957829 0.6328898072 0.6017335057 + 0.6477838159 0.6337034106 0.6025940776 + 0.6485717297 0.6345170736 0.6034547091 + 0.6493597031 0.6353306770 0.6043154001 + 0.6501476765 0.6361442804 0.6051759720 + 0.6509342790 0.6369585991 0.6060351729 + 0.6517130733 0.6377763152 0.6068887711 + 0.6524918079 0.6385939717 0.6077424288 + 0.6532706022 0.6394116879 0.6085960269 + 0.6540492773 0.6402294040 0.6094496250 + 0.6548280716 0.6410471201 0.6103032231 + 0.6556051970 0.6418619156 0.6111536026 + 0.6563801169 0.6426733732 0.6120008230 + 0.6571549773 0.6434847713 0.6128481030 + 0.6579298973 0.6442962289 0.6136953235 + 0.6587048173 0.6451076269 0.6145424843 + 0.6594796777 0.6459190249 0.6153897047 + 0.6602489948 0.6467270255 0.6162346005 + 0.6610152721 0.6475332975 0.6170787215 + 0.6617817283 0.6483396292 0.6179227829 + 0.6625480056 0.6491457820 0.6187667847 + 0.6633144021 0.6499521136 0.6196109056 + 0.6640806794 0.6507583857 0.6204549074 + 0.6648426056 0.6515669823 0.6212937236 + 0.6656033993 0.6523761153 0.6221314073 + 0.6663640738 0.6531851888 0.6229692101 + 0.6671249270 0.6539943218 0.6238070130 + 0.6678856015 0.6548035145 0.6246448159 + 0.6686462760 0.6556125879 0.6254826188 + 0.6694011092 0.6564118862 0.6263144016 + 0.6701549292 0.6572096944 0.6271455884 + 0.6709085703 0.6580076218 0.6279767752 + 0.6716623902 0.6588053703 0.6288080215 + 0.6724160910 0.6596031785 0.6296392083 + 0.6731699109 0.6604009867 0.6304703951 + 0.6739165187 0.6611961722 0.6312931180 + 0.6746615767 0.6619908214 0.6321147084 + 0.6754066944 0.6627854109 0.6329362988 + 0.6761518121 0.6635800004 0.6337578297 + 0.6768969893 0.6643745899 0.6345794201 + 0.6776421070 0.6651691794 0.6354010105 + 0.6783866882 0.6659619808 0.6362180114 + 0.6791309714 0.6667541862 0.6370335817 + 0.6798753738 0.6675463915 0.6378492713 + 0.6806197762 0.6683385968 0.6386650205 + 0.6813641787 0.6691308022 0.6394807100 + 0.6821085215 0.6699230075 0.6402962804 + 0.6828457713 0.6707084775 0.6411076188 + 0.6835768819 0.6714885235 0.6419157982 + 0.6843081117 0.6722685099 0.6427239776 + 0.6850392222 0.6730484962 0.6435320973 + 0.6857702732 0.6738284826 0.6443402767 + 0.6865015030 0.6746084094 0.6451485157 + 0.6872311234 0.6753873229 0.6459537148 + 0.6879568100 0.6761630774 0.6467525959 + 0.6886826158 0.6769388914 0.6475514174 + 0.6894083023 0.6777147055 0.6483502984 + 0.6901339889 0.6784905195 0.6491491199 + 0.6908596754 0.6792662740 0.6499480009 + 0.6915854216 0.6800420880 0.6507468224 + 0.6923050880 0.6808115244 0.6515396833 + 0.6930242777 0.6815804243 0.6523323059 + 0.6937434077 0.6823492050 0.6531249285 + 0.6944625974 0.6831181049 0.6539174914 + 0.6951817274 0.6838870049 0.6547101140 + 0.6959009171 0.6846559048 0.6555026770 + 0.6966161728 0.6854197979 0.6562970281 + 0.6973273754 0.6861783862 0.6570926905 + 0.6980385184 0.6869369745 0.6578884721 + 0.6987496018 0.6876956224 0.6586841941 + 0.6994606853 0.6884542108 0.6594799757 + 0.7001718283 0.6892127991 0.6602758169 + 0.7008829117 0.6899713874 0.6610714793 + 0.7015895844 0.6907256246 0.6618655920 + 0.7022960782 0.6914795041 0.6626598239 + 0.7030025125 0.6922335029 0.6634538770 + 0.7037090063 0.6929875016 0.6642479897 + 0.7044153810 0.6937413812 0.6650421023 + 0.7051218152 0.6944953799 0.6658362150 + 0.7058250904 0.6952432990 0.6666283011 + 0.7065216899 0.6959791780 0.6674168706 + 0.7072182894 0.6967151165 0.6682054996 + 0.7079148889 0.6974509954 0.6689941287 + 0.7086114287 0.6981868744 0.6697826982 + 0.7093080282 0.6989226937 0.6705713272 + 0.7100046277 0.6996585727 0.6713598967 + 0.7106981277 0.7003877759 0.6721469760 + 0.7113897204 0.7011129260 0.6729333997 + 0.7120811939 0.7018380165 0.6737198234 + 0.7127727866 0.7025631070 0.6745061278 + 0.7134643793 0.7032880783 0.6752924919 + 0.7141559720 0.7040132284 0.6760789156 + 0.7148475051 0.7047383189 0.6768652201 + 0.7155426741 0.7054607272 0.6776456833 + 0.7162386775 0.7061824799 0.6784251928 + 0.7169346809 0.7069042921 0.6792047024 + 0.7176308036 0.7076261044 0.6799842715 + 0.7183268070 0.7083479166 0.6807637811 + 0.7190228105 0.7090697289 0.6815432906 + 0.7197188139 0.7097914815 0.6823228002 + 0.7204080224 0.7105045915 0.6830921769 + 0.7210965157 0.7112171054 0.6838611960 + 0.7217851281 0.7119296193 0.6846302748 + 0.7224736214 0.7126420140 0.6853992939 + 0.7231621742 0.7133545280 0.6861683130 + 0.7238507271 0.7140669823 0.6869373918 + 0.7245392799 0.7147794962 0.6877064109 + 0.7252277136 0.7154844999 0.6884679794 + 0.7259160280 0.7161890268 0.6892294884 + 0.7266044021 0.7168934941 0.6899909973 + 0.7272927165 0.7175980806 0.6907523870 + 0.7279810905 0.7183026075 0.6915138960 + 0.7286694050 0.7190070748 0.6922754049 + 0.7293577790 0.7197117209 0.6930367947 + 0.7300446033 0.7204123139 0.6937997937 + 0.7307311893 0.7211123109 0.6945629120 + 0.7314177752 0.7218121886 0.6953259706 + 0.7321043015 0.7225121856 0.6960890889 + 0.7327908874 0.7232121825 0.6968522072 + 0.7334774733 0.7239121795 0.6976153255 + 0.7341641188 0.7246121764 0.6983783841 + 0.7348474860 0.7253112793 0.6991379261 + 0.7355294824 0.7260100842 0.6998959780 + 0.7362114787 0.7267090082 0.7006539702 + 0.7368934155 0.7274078131 0.7014120817 + 0.7375754118 0.7281066179 0.7021701932 + 0.7382574081 0.7288054228 0.7029281855 + 0.7389392853 0.7295042276 0.7036862969 + 0.7396183014 0.7302008867 0.7044391036 + 0.7402927279 0.7308946252 0.7051851749 + 0.7409670949 0.7315883040 0.7059314251 + 0.7416415811 0.7322819829 0.7066774964 + 0.7423160076 0.7329757214 0.7074236870 + 0.7429903746 0.7336694002 0.7081698179 + 0.7436649203 0.7343631983 0.7089160085 + 0.7443389893 0.7350566983 0.7096619010 + 0.7450042963 0.7357468009 0.7104052901 + 0.7456694841 0.7364367843 0.7111486793 + 0.7463346720 0.7371268868 0.7118921280 + 0.7469999790 0.7378168702 0.7126353979 + 0.7476652265 0.7385069728 0.7133787870 + 0.7483304739 0.7391970158 0.7141221762 + 0.7489957213 0.7398871183 0.7148655057 + 0.7496609092 0.7405748963 0.7156080008 + 0.7503259182 0.7412611246 0.7163497806 + 0.7509909868 0.7419472933 0.7170916200 + 0.7516561151 0.7426335216 0.7178335190 + 0.7523211241 0.7433196902 0.7185752988 + 0.7529861927 0.7440059185 0.7193170786 + 0.7536513209 0.7446920872 0.7200589180 + 0.7543162704 0.7453783154 0.7208005786 + 0.7549796104 0.7460603714 0.7215341926 + 0.7556427717 0.7467426062 0.7222678065 + 0.7563059926 0.7474247217 0.7230014205 + 0.7569692731 0.7481067777 0.7237349749 + 0.7576324940 0.7487888932 0.7244685888 + 0.7582957149 0.7494711280 0.7252022028 + 0.7589588761 0.7501531839 0.7259358168 + 0.7596207261 0.7508332729 0.7266669273 + 0.7602791786 0.7515090108 0.7273933887 + 0.7609376907 0.7521848083 0.7281197906 + 0.7615962029 0.7528604865 0.7288463116 + 0.7622547150 0.7535362244 0.7295727134 + 0.7629132271 0.7542120218 0.7302991152 + 0.7635716796 0.7548877001 0.7310255766 + 0.7642301917 0.7555633783 0.7317519784 + 0.7648857832 0.7562360168 0.7324793935 + 0.7655386925 0.7569059134 0.7332075834 + 0.7661917210 0.7575758100 0.7339357734 + 0.7668445706 0.7582457066 0.7346640229 + 0.7674975991 0.7589156032 0.7353920937 + 0.7681505084 0.7595854998 0.7361202836 + 0.7688034773 0.7602553964 0.7368484735 + 0.7694563866 0.7609252930 0.7375766039 + 0.7701069713 0.7615932822 0.7383013964 + 0.7707563043 0.7622603774 0.7390245795 + 0.7714055777 0.7629274726 0.7397478223 + 0.7720549107 0.7635946274 0.7404710054 + 0.7727041841 0.7642617226 0.7411941886 + 0.7733533978 0.7649288177 0.7419173717 + 0.7740026712 0.7655959129 0.7426406145 + 0.7746520042 0.7662630081 0.7433637977 + 0.7752993703 0.7669281960 0.7440791726 + 0.7759457231 0.7675923109 0.7447909713 + 0.7765920162 0.7682564259 0.7455028296 + 0.7772383094 0.7689204812 0.7462146282 + 0.7778846025 0.7695845962 0.7469264269 + 0.7785308957 0.7702487111 0.7476382256 + 0.7791771889 0.7709128261 0.7483499050 + 0.7798236012 0.7715768814 0.7490617037 + 0.7804623246 0.7722384930 0.7497718930 + 0.7810946703 0.7728980184 0.7504810095 + 0.7817270756 0.7735574841 0.7511901259 + 0.7823594213 0.7742168903 0.7518991232 + 0.7829918265 0.7748764157 0.7526081800 + 0.7836241722 0.7755358815 0.7533171773 + 0.7842565775 0.7761954069 0.7540262938 + 0.7848889828 0.7768548727 0.7547354102 + 0.7855191231 0.7775127292 0.7554432154 + 0.7861444950 0.7781673074 0.7561488748 + 0.7867699265 0.7788218856 0.7568545938 + 0.7873954177 0.7794765830 0.7575603127 + 0.7880207896 0.7801312208 0.7582659721 + 0.7886462212 0.7807857990 0.7589715719 + 0.7892715931 0.7814403772 0.7596772909 + 0.7898970842 0.7820950150 0.7603830099 + 0.7905223966 0.7827494740 0.7610880733 + 0.7911401987 0.7833991051 0.7617810965 + 0.7917578816 0.7840486765 0.7624740005 + 0.7923756838 0.7846983075 0.7631670237 + 0.7929934859 0.7853478789 0.7638599873 + 0.7936112285 0.7859973907 0.7645528913 + 0.7942289710 0.7866470218 0.7652459145 + 0.7948467135 0.7872965932 0.7659388185 + 0.7954645157 0.7879462242 0.7666317821 + 0.7960770130 0.7885926962 0.7673208714 + 0.7966856956 0.7892370224 0.7680076957 + 0.7972943783 0.7898812890 0.7686944008 + 0.7979031205 0.7905256152 0.7693811059 + 0.7985118032 0.7911698818 0.7700678110 + 0.7991204858 0.7918142080 0.7707545757 + 0.7997291088 0.7924584746 0.7714412808 + 0.8003377914 0.7931028008 0.7721279860 + 0.8009464741 0.7937468290 0.7728148103 + 0.8015543222 0.7943841815 0.7735018730 + 0.8021621108 0.7950215936 0.7741889954 + 0.8027698994 0.7956590056 0.7748761773 + 0.8033776879 0.7962964177 0.7755632997 + 0.8039855957 0.7969338298 0.7762504816 + 0.8045933843 0.7975711823 0.7769376040 + 0.8052011728 0.7982085943 0.7776247859 + 0.8058090210 0.7988460064 0.7783119082 + 0.8064159751 0.7994800806 0.7789977193 + 0.8070214987 0.8001086712 0.7796815038 + 0.8076270223 0.8007372022 0.7803652883 + 0.8082324862 0.8013656735 0.7810490131 + 0.8088380098 0.8019942045 0.7817327976 + 0.8094434738 0.8026227951 0.7824165225 + 0.8100489974 0.8032513261 0.7831003070 + 0.8106545210 0.8038797975 0.7837840915 + 0.8112599850 0.8045083284 0.7844678164 + 0.8118627071 0.8051316142 0.7851473093 + 0.8124632239 0.8057510257 0.7858238816 + 0.8130636811 0.8063703775 0.7865005732 + 0.8136641979 0.8069897890 0.7871773243 + 0.8142647147 0.8076092005 0.7878538966 + 0.8148651719 0.8082286119 0.7885305882 + 0.8154656887 0.8088480234 0.7892072201 + 0.8160662055 0.8094673753 0.7898839116 + 0.8166667223 0.8100867867 0.7905604839 + 0.8172659874 0.8107038736 0.7912366986 + 0.8178647161 0.8113198876 0.7919127941 + 0.8184633851 0.8119359016 0.7925887704 + 0.8190621138 0.8125519156 0.7932648063 + 0.8196607828 0.8131679296 0.7939407825 + 0.8202595115 0.8137838840 0.7946168184 + 0.8208581805 0.8143998981 0.7952927947 + 0.8214569092 0.8150159121 0.7959687710 + 0.8220555782 0.8156319261 0.7966448069 + 0.8226491213 0.8162443042 0.7973200083 + 0.8232396245 0.8168547153 0.7979946733 + 0.8238301873 0.8174651861 0.7986695170 + 0.8244208097 0.8180755973 0.7993443012 + 0.8250113726 0.8186861277 0.8000190258 + 0.8256019950 0.8192964792 0.8006938100 + 0.8261926174 0.8199068904 0.8013685942 + 0.8267831802 0.8205174208 0.8020433187 + 0.8273738027 0.8211277723 0.8027181029 + 0.8279619217 0.8217353821 0.8033875227 + 0.8285475969 0.8223400116 0.8040519953 + 0.8291332126 0.8229447007 0.8047165871 + 0.8297188878 0.8235492706 0.8053811789 + 0.8303046227 0.8241540194 0.8060457110 + 0.8308901787 0.8247585893 0.8067103028 + 0.8314759135 0.8253632784 0.8073748946 + 0.8320615888 0.8259679079 0.8080394864 + 0.8326472044 0.8265725970 0.8087040186 + 0.8332327008 0.8271760941 0.8093675971 + 0.8338174224 0.8277755976 0.8100281954 + 0.8344022036 0.8283752203 0.8106889129 + 0.8349869251 0.8289747834 0.8113495111 + 0.8355717063 0.8295742869 0.8120101094 + 0.8361564279 0.8301739097 0.8126708269 + 0.8367410898 0.8307734132 0.8133314252 + 0.8373258710 0.8313729763 0.8139920831 + 0.8379105926 0.8319725990 0.8146526814 + 0.8384953737 0.8325721025 0.8153132796 + 0.8390772939 0.8331714272 0.8159744740 + 0.8396586180 0.8337705731 0.8166357279 + 0.8402400017 0.8343697786 0.8172969222 + 0.8408213258 0.8349689245 0.8179581165 + 0.8414027095 0.8355680704 0.8186193109 + 0.8419839740 0.8361672759 0.8192806244 + 0.8425654173 0.8367664814 0.8199418187 + 0.8431466818 0.8373656869 0.8206030130 + 0.8437281251 0.8379648924 0.8212642074 + 0.8443076015 0.8385624290 0.8219218254 + 0.8448830247 0.8391563296 0.8225721121 + 0.8454583883 0.8397501707 0.8232223988 + 0.8460338116 0.8403440118 0.8238726258 + 0.8466091752 0.8409379125 0.8245229125 + 0.8471845984 0.8415316939 0.8251731992 + 0.8477599025 0.8421255946 0.8258234859 + 0.8483353257 0.8427193761 0.8264737129 + 0.8489106894 0.8433132768 0.8271239996 + 0.8494861126 0.8439071178 0.8277742863 + 0.8500558138 0.8444991112 0.8284201026 + 0.8506222963 0.8450899720 0.8290638924 + 0.8511888981 0.8456808925 0.8297076821 + 0.8517553806 0.8462718129 0.8303514719 + 0.8523219228 0.8468626738 0.8309952021 + 0.8528885245 0.8474537134 0.8316389918 + 0.8534550071 0.8480445743 0.8322827816 + 0.8540216088 0.8486354947 0.8329265118 + 0.8545880914 0.8492264152 0.8335703015 + 0.8551546931 0.8498172760 0.8342140913 + 0.8557175994 0.8504027724 0.8348590136 + 0.8562799096 0.8509873748 0.8355041742 + 0.8568422794 0.8515719771 0.8361492753 + 0.8574045897 0.8521565795 0.8367943764 + 0.8579670191 0.8527411819 0.8374394774 + 0.8585293293 0.8533257842 0.8380845785 + 0.8590915799 0.8539103866 0.8387296796 + 0.8596540093 0.8544949889 0.8393748999 + 0.8602163196 0.8550797105 0.8400200009 + 0.8607786894 0.8556643128 0.8406651020 + 0.8613408804 0.8562449217 0.8413107991 + 0.8619030714 0.8568252921 0.8419564962 + 0.8624653816 0.8574056029 0.8426023126 + 0.8630275726 0.8579859734 0.8432480097 + 0.8635898232 0.8585664034 0.8438937068 + 0.8641520739 0.8591467738 0.8445394039 + 0.8647143245 0.8597272038 0.8451852202 + 0.8652765155 0.8603075743 0.8458309174 + 0.8658387065 0.8608880043 0.8464766145 + 0.8664010167 0.8614683747 0.8471223116 + 0.8669610023 0.8620395064 0.8477607965 + 0.8675208092 0.8626096249 0.8483986259 + 0.8680804968 0.8631796241 0.8490363955 + 0.8686403036 0.8637496829 0.8496742249 + 0.8691999912 0.8643196821 0.8503119946 + 0.8697597980 0.8648898005 0.8509498835 + 0.8703196049 0.8654597998 0.8515877128 + 0.8708792925 0.8660299182 0.8522254825 + 0.8714390993 0.8665999174 0.8528633118 + 0.8719987869 0.8671699762 0.8535010815 + 0.8725550175 0.8677337766 0.8541374803 + 0.8731096983 0.8682951927 0.8547734022 + 0.8736643195 0.8688564897 0.8554093242 + 0.8742190003 0.8694179058 0.8560451865 + 0.8747736216 0.8699792027 0.8566811085 + 0.8753283024 0.8705404997 0.8573169708 + 0.8758829236 0.8711019158 0.8579528928 + 0.8764376044 0.8716632128 0.8585888147 + 0.8769922256 0.8722246289 0.8592246175 + 0.8775469065 0.8727859259 0.8598604798 + 0.8781003952 0.8733474016 0.8604974151 + 0.8786519766 0.8739091754 0.8611357212 + 0.8792036176 0.8744710088 0.8617740870 + 0.8797551990 0.8750327826 0.8624123931 + 0.8803067803 0.8755946755 0.8630506992 + 0.8808584213 0.8761565089 0.8636891246 + 0.8814098835 0.8767182827 0.8643273711 + 0.8819615245 0.8772801161 0.8649656773 + 0.8825131059 0.8778418899 0.8656041026 + 0.8830646873 0.8784037232 0.8662424088 + 0.8836163282 0.8789654970 0.8668807149 + 0.8841652870 0.8795219064 0.8675110936 + 0.8847140074 0.8800777793 0.8681409955 + 0.8852627277 0.8806337714 0.8687707782 + 0.8858113885 0.8811897039 0.8694006205 + 0.8863601089 0.8817455769 0.8700305223 + 0.8869088292 0.8823016286 0.8706603050 + 0.8874574900 0.8828575015 0.8712900877 + 0.8880062103 0.8834133744 0.8719198704 + 0.8885548711 0.8839693069 0.8725497723 + 0.8891035914 0.8845252991 0.8731796145 + 0.8896498084 0.8850799799 0.8738080263 + 0.8901898265 0.8856320977 0.8744335175 + 0.8907297850 0.8861840963 0.8750590086 + 0.8912699223 0.8867360950 0.8756844997 + 0.8918098807 0.8872882128 0.8763099909 + 0.8923500180 0.8878402114 0.8769354820 + 0.8928899765 0.8883923292 0.8775609732 + 0.8934299946 0.8889443278 0.8781865239 + 0.8939700723 0.8894963861 0.8788120151 + 0.8945100904 0.8900483847 0.8794375062 + 0.8950502276 0.8906005025 0.8800629973 + 0.8955878019 0.8911523819 0.8806902766 + 0.8961235285 0.8917043209 0.8813189268 + 0.8966590762 0.8922562003 0.8819475174 + 0.8971948028 0.8928080201 0.8825762272 + 0.8977304101 0.8933598995 0.8832048178 + 0.8982660770 0.8939117789 0.8838335276 + 0.8988016844 0.8944637179 0.8844621181 + 0.8993372917 0.8950155973 0.8850907087 + 0.8998730183 0.8955674171 0.8857194185 + 0.9004086256 0.8961192966 0.8863480091 + 0.9009442925 0.8966711760 0.8869767189 + 0.9014744759 0.8972206712 0.8876035810 + 0.9020017982 0.8977689147 0.8882297873 + 0.9025291800 0.8983170986 0.8888558745 + 0.9030565023 0.8988652825 0.8894820213 + 0.9035838246 0.8994135261 0.8901082277 + 0.9041110873 0.8999617100 0.8907343149 + 0.9046384096 0.9005097747 0.8913604021 + 0.9051656723 0.9010580182 0.8919866085 + 0.9056929946 0.9016062021 0.8926126957 + 0.9062203169 0.9021543860 0.8932387829 + 0.9067475796 0.9027026296 0.8938649893 + 0.9072715044 0.9032499790 0.8944861889 + 0.9077931046 0.9037966728 0.8951045275 + 0.9083147049 0.9043434858 0.8957228065 + 0.9088363051 0.9048901796 0.8963410854 + 0.9093579054 0.9054369926 0.8969594240 + 0.9098795056 0.9059836864 0.8975777030 + 0.9104011059 0.9065304995 0.8981959224 + 0.9109227061 0.9070771933 0.8988142014 + 0.9114443064 0.9076240063 0.8994324803 + 0.9119659066 0.9081707001 0.9000508189 + 0.9124875069 0.9087175131 0.9006690979 + 0.9130079150 0.9092611074 0.9012864828 + 0.9135265946 0.9098002911 0.9019027948 + 0.9140452743 0.9103394747 0.9025191069 + 0.9145638943 0.9108787179 0.9031354189 + 0.9150825739 0.9114179015 0.9037516713 + 0.9156013131 0.9119570851 0.9043679833 + 0.9161199927 0.9124963284 0.9049841762 + 0.9166386724 0.9130355120 0.9056004882 + 0.9171572924 0.9135746956 0.9062168002 + 0.9176759720 0.9141138792 0.9068331122 + 0.9181947112 0.9146531224 0.9074494243 + 0.9187132120 0.9151920080 0.9080653191 + 0.9192286134 0.9157255292 0.9086763859 + 0.9197440147 0.9162589908 0.9092875123 + 0.9202594161 0.9167925715 0.9098985791 + 0.9207748175 0.9173260927 0.9105097055 + 0.9212902188 0.9178596735 0.9111207724 + 0.9218056202 0.9183931947 0.9117318988 + 0.9223210216 0.9189267159 0.9123430252 + 0.9228364229 0.9194602966 0.9129540920 + 0.9233518243 0.9199938178 0.9135652184 + 0.9238672256 0.9205272794 0.9141762853 + 0.9243826270 0.9210609198 0.9147874117 + 0.9248952866 0.9215934277 0.9153928161 + 0.9254056215 0.9221249819 0.9159939289 + 0.9259160161 0.9226565957 0.9165949821 + 0.9264264107 0.9231882095 0.9171959758 + 0.9269366860 0.9237198830 0.9177970886 + 0.9274470806 0.9242514968 0.9183982015 + 0.9279574752 0.9247831106 0.9189993143 + 0.9284678102 0.9253147840 0.9196004272 + 0.9289782047 0.9258463979 0.9202014208 + 0.9294885993 0.9263780117 0.9208024740 + 0.9299988747 0.9269096255 0.9214035869 + 0.9305093288 0.9274412990 0.9220046997 + 0.9310135245 0.9279668927 0.9225972295 + 0.9315168858 0.9284918904 0.9231889248 + 0.9320203066 0.9290168881 0.9237805009 + 0.9325237870 0.9295418262 0.9243721962 + 0.9330272079 0.9300668240 0.9249637723 + 0.9335306287 0.9305917025 0.9255555272 + 0.9340341091 0.9311167002 0.9261471033 + 0.9345374703 0.9316416979 0.9267387986 + 0.9350408912 0.9321665764 0.9273303747 + 0.9355443120 0.9326915741 0.9279221296 + 0.9360477924 0.9332165718 0.9285137057 + 0.9365504980 0.9337409139 0.9291048050 + 0.9370446801 0.9342573285 0.9296892881 + 0.9375388026 0.9347736835 0.9302738905 + 0.9380329847 0.9352902174 0.9308584929 + 0.9385271072 0.9358065724 0.9314429760 + 0.9390212893 0.9363229871 0.9320275784 + 0.9395154119 0.9368394017 0.9326121807 + 0.9400095940 0.9373558760 0.9331967235 + 0.9405037165 0.9378722906 0.9337813258 + 0.9409978986 0.9383887053 0.9343659282 + 0.9414920211 0.9389051199 0.9349504113 + 0.9419862032 0.9394215941 0.9355350137 + 0.9424793124 0.9399371743 0.9361184239 + 0.9429621100 0.9404459000 0.9366925955 + 0.9434450269 0.9409546256 0.9372667074 + 0.9439278841 0.9414631724 0.9378408194 + 0.9444106817 0.9419718981 0.9384148717 + 0.9448935986 0.9424806237 0.9389889836 + 0.9453763962 0.9429891706 0.9395630956 + 0.9458593130 0.9434978962 0.9401373267 + 0.9463421106 0.9440066218 0.9407113791 + 0.9468250275 0.9445152283 0.9412854910 + 0.9473078847 0.9450238943 0.9418596029 + 0.9477906823 0.9455326200 0.9424337149 + 0.9482735991 0.9460412264 0.9430078268 + 0.9487515092 0.9465460181 0.9435728788 + 0.9492291808 0.9470505714 0.9441375732 + 0.9497069120 0.9475551844 0.9447023273 + 0.9501845837 0.9480597973 0.9452670813 + 0.9506623149 0.9485644102 0.9458317757 + 0.9511399865 0.9490690231 0.9463965297 + 0.9516175985 0.9495735168 0.9469612241 + 0.9520953298 0.9500781298 0.9475259781 + 0.9525730014 0.9505826831 0.9480906725 + 0.9530506730 0.9510872960 0.9486554265 + 0.9535284042 0.9515919089 0.9492201209 + 0.9540060759 0.9520965219 0.9497848749 + 0.9544798732 0.9525997043 0.9503433108 + 0.9549517035 0.9531021118 0.9508985281 + 0.9554234743 0.9536045194 0.9514538050 + 0.9558951855 0.9541069269 0.9520090222 + 0.9563670158 0.9546092749 0.9525641799 + 0.9568387270 0.9551116824 0.9531193972 + 0.9573104978 0.9556140900 0.9536746740 + 0.9577823281 0.9561164975 0.9542298913 + 0.9582539797 0.9566189051 0.9547851086 + 0.9587258101 0.9571213722 0.9553403258 + 0.9591975212 0.9576237798 0.9558954835 + 0.9596692920 0.9581261873 0.9564508200 + 0.9601396918 0.9586272836 0.9570031166 + 0.9606035948 0.9591223001 0.9575427771 + 0.9610674977 0.9596173167 0.9580823779 + 0.9615314007 0.9601122737 0.9586220980 + 0.9619953036 0.9606072903 0.9591618180 + 0.9624592066 0.9611024261 0.9597014189 + 0.9629231095 0.9615973830 0.9602410793 + 0.9633870125 0.9620923996 0.9607807994 + 0.9638509154 0.9625874162 0.9613204002 + 0.9643148184 0.9630824924 0.9618601203 + 0.9647787809 0.9635775089 0.9623997808 + 0.9652426839 0.9640725255 0.9629393816 + 0.9657065868 0.9645674825 0.9634791017 + 0.9661654830 0.9650583267 0.9640104771 + 0.9666200876 0.9655454755 0.9645349979 + 0.9670745730 0.9660326242 0.9650595188 + 0.9675291777 0.9665197730 0.9655839801 + 0.9679837823 0.9670069218 0.9661085010 + 0.9684383869 0.9674940705 0.9666330218 + 0.9688928723 0.9679812193 0.9671574831 + 0.9693474770 0.9684684277 0.9676818848 + 0.9698020816 0.9689555764 0.9682064056 + 0.9702566862 0.9694427252 0.9687309265 + 0.9707111716 0.9699298739 0.9692553878 + 0.9711657763 0.9704170227 0.9697799087 + 0.9716203809 0.9709041715 0.9703044295 + 0.9720690846 0.9713829756 0.9708178043 + 0.9725155234 0.9718586206 0.9713271856 + 0.9729619026 0.9723342061 0.9718365073 + 0.9734084010 0.9728097916 0.9723458290 + 0.9738547802 0.9732853770 0.9728550911 + 0.9743012190 0.9737610221 0.9733644724 + 0.9747477174 0.9742366076 0.9738737941 + 0.9751940966 0.9747121930 0.9743831158 + 0.9756405950 0.9751877785 0.9748924971 + 0.9760869741 0.9756634235 0.9754018188 + 0.9765334129 0.9761390090 0.9759110808 + 0.9769799113 0.9766145945 0.9764205217 + 0.9774262905 0.9770901799 0.9769297838 + 0.9778676033 0.9775533080 0.9774206877 + 0.9783070087 0.9780119061 0.9779049754 + 0.9787464142 0.9784705043 0.9783893824 + 0.9791858196 0.9789291024 0.9788737893 + 0.9796252251 0.9793877006 0.9793581963 + 0.9800645709 0.9798462987 0.9798424840 + 0.9805039763 0.9803048968 0.9803268909 + 0.9809433818 0.9807634950 0.9808112979 + 0.9813829064 0.9812219739 0.9812957048 + 0.9818223119 0.9816805720 0.9817801118 + 0.9822617173 0.9821392298 0.9822643995 + 0.9827011228 0.9825978279 0.9827488065 + 0.9831405282 0.9830564260 0.9832332134 + 0.9835739136 0.9835022092 0.9837002158 + 0.9840028286 0.9839385152 0.9841542840 + 0.9844316244 0.9843747020 0.9846084714 + 0.9848604798 0.9848110080 0.9850625992 + 0.9852893949 0.9852473140 0.9855167866 + 0.9857181907 0.9856835008 0.9859709144 + 0.9861471057 0.9861198068 0.9864251018 + 0.9865760207 0.9865561128 0.9868792295 + 0.9870048165 0.9869922996 0.9873334169 + 0.9874336720 0.9874286056 0.9877874851 + 0.9878625870 0.9878647923 0.9882416725 + 0.9882913828 0.9883010983 0.9886958003 + 0.9887202978 0.9887374043 0.9891499877 + 0.9891455173 0.9891687036 0.9895961881 + 0.9895583987 0.9895836115 0.9900164008 + 0.9899712801 0.9899985790 0.9904366136 + 0.9903842807 0.9904134870 0.9908568263 + 0.9907972217 0.9908285141 0.9912769794 + 0.9912101030 0.9912434220 0.9916970730 + 0.9916231036 0.9916582704 0.9921172857 + 0.9920359850 0.9920732975 0.9925374985 + 0.9924489260 0.9924882054 0.9929577112 + 0.9928618073 0.9929031134 0.9933779240 + 0.9932748079 0.9933180809 0.9937980771 + 0.9936876893 0.9937329888 0.9942182899 + 0.9941005707 0.9941480160 0.9946383834 + 0.9945135117 0.9945629239 0.9950585961 + 0.9949113131 0.9949585795 0.9954301715 + 0.9953026772 0.9953464270 0.9957817197 + 0.9956942201 0.9957342148 0.9961332083 + 0.9960855842 0.9961220026 0.9964848161 + 0.9964770079 0.9965097904 0.9968363047 + 0.9968684912 0.9968975782 0.9971877933 + 0.9972599149 0.9972854257 0.9975392818 + 0.9976513982 0.9976732135 0.9978908896 + 0.9980428219 0.9980610013 0.9982423782 + 0.9984341860 0.9984487891 0.9985939264 + 0.9988257289 0.9988365769 0.9989454150 + 0.9992170930 0.9992244244 0.9992970228 + 0.9996085763 0.9996122122 0.9996485114 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_100t.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_100t.spi1d index b71e38e9..d543c846 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_100t.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_100t.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0078889886 0.0072905929 0.0057835742 - 0.0155280400 0.0142424498 0.0110800304 - 0.0223259609 0.0204148795 0.0157516599 - 0.0282847900 0.0258345399 0.0199356508 - 0.0338022411 0.0308287591 0.0238022301 - 0.0387409404 0.0354198888 0.0274327602 - 0.0434868000 0.0397184007 0.0308620799 - 0.0479348600 0.0437957905 0.0341565795 - 0.0521249995 0.0477067716 0.0373548716 - 0.0560929887 0.0514680110 0.0404086895 - 0.0599000603 0.0551049784 0.0433653891 - 0.0636395887 0.0585507713 0.0462335311 - 0.0673073381 0.0619252101 0.0490648411 - 0.0708674565 0.0652377531 0.0518452115 - 0.0742941722 0.0684634522 0.0545356311 - 0.0776154101 0.0715824589 0.0571359284 - 0.0808518305 0.0746066868 0.0596979707 - 0.0840119570 0.0775315836 0.0622345991 - 0.0870610103 0.0803711265 0.0647289529 - 0.0900357366 0.0831752867 0.0671606734 - 0.0929580331 0.0859571993 0.0695598572 - 0.0958346874 0.0887065008 0.0719215125 - 0.0986543819 0.0914179012 0.0742511526 - 0.1014280990 0.0940862000 0.0765401274 - 0.1041439995 0.0967277288 0.0788079128 - 0.1068262011 0.0993322283 0.0810511932 - 0.1094703972 0.1018816978 0.0832685530 - 0.1121027023 0.1043870971 0.0854593366 - 0.1147300974 0.1068421006 0.0876216292 - 0.1173269004 0.1092239022 0.0897513777 - 0.1198854968 0.1115904003 0.0918506086 - 0.1224000975 0.1139374003 0.0939252079 - 0.1248695031 0.1162879989 0.0959772170 - 0.1273149997 0.1186162010 0.0980132222 - 0.1297475994 0.1209222972 0.1000507995 - 0.1321358979 0.1232006028 0.1020739973 - 0.1344795972 0.1254397035 0.1040984020 - 0.1368041933 0.1276575029 0.1060978994 - 0.1391019970 0.1298466027 0.1080757976 - 0.1413698047 0.1320064962 0.1100341976 - 0.1435928941 0.1341473013 0.1119635031 - 0.1457892954 0.1362611055 0.1138700023 - 0.1479541063 0.1383730024 0.1157647967 - 0.1501048058 0.1404493004 0.1176467985 - 0.1522224993 0.1425213069 0.1195039973 - 0.1543388963 0.1445824057 0.1213504001 - 0.1564362943 0.1466197073 0.1231838018 - 0.1585174948 0.1486462057 0.1250088066 - 0.1605857015 0.1506392062 0.1268348992 - 0.1626347005 0.1526177973 0.1286439002 - 0.1646862030 0.1546075940 0.1304429024 - 0.1667335927 0.1565793008 0.1322406977 - 0.1687652022 0.1585436016 0.1340295970 - 0.1707780957 0.1604935974 0.1357980967 - 0.1727759987 0.1624072939 0.1375523061 - 0.1747658998 0.1643151045 0.1393052936 - 0.1767490953 0.1662092954 0.1410588026 - 0.1787372977 0.1680783033 0.1427990049 - 0.1807080060 0.1699371934 0.1445256025 - 0.1826650947 0.1718004048 0.1462455988 - 0.1846058965 0.1736536026 0.1479606032 - 0.1865195930 0.1754879057 0.1496645957 - 0.1884177029 0.1773201972 0.1513437033 - 0.1903073043 0.1791377962 0.1530140042 - 0.1921796948 0.1809280068 0.1546792984 - 0.1940302998 0.1827086061 0.1563450992 - 0.1958549023 0.1844934970 0.1580031067 - 0.1976682991 0.1862594038 0.1596433967 - 0.1994695961 0.1880051941 0.1612734050 - 0.2012577057 0.1897480041 0.1628941000 - 0.2030259073 0.1914830059 0.1645098031 - 0.2047819048 0.1931872070 0.1661248058 - 0.2065176964 0.1948740035 0.1677244008 - 0.2082248926 0.1965582073 0.1693145037 - 0.2099235952 0.1982422024 0.1708988994 - 0.2116315067 0.1999278069 0.1724735051 - 0.2133346945 0.2015964985 0.1740391999 - 0.2150184065 0.2032589018 0.1755944937 - 0.2167019993 0.2049262971 0.1771457046 - 0.2183800042 0.2065898031 0.1786843985 - 0.2200523019 0.2082391977 0.1802140027 - 0.2217144072 0.2098809928 0.1817325056 - 0.2233763039 0.2115218937 0.1832381040 - 0.2250375003 0.2131509036 0.1847354025 - 0.2266878933 0.2147578001 0.1862273067 - 0.2283222973 0.2163494974 0.1877095997 - 0.2299540937 0.2179363966 0.1891891062 - 0.2315769047 0.2195288986 0.1906653941 - 0.2331923991 0.2211087048 0.1921359003 - 0.2347972989 0.2226725072 0.1936012954 - 0.2363962978 0.2242244929 0.1950621009 - 0.2379969060 0.2257725000 0.1965180039 - 0.2395913005 0.2273270935 0.1979725063 - 0.2411745936 0.2288744003 0.1994252056 - 0.2427513003 0.2304067016 0.2008765042 - 0.2443210930 0.2319256067 0.2023257017 - 0.2458810061 0.2334371954 0.2037705034 - 0.2474320978 0.2349530011 0.2052136958 - 0.2489770949 0.2364764959 0.2066580951 - 0.2505179048 0.2380003035 0.2081031054 - 0.2520565093 0.2395125926 0.2095465064 - 0.2535842061 0.2410165966 0.2109802067 - 0.2551011145 0.2425200939 0.2124101967 - 0.2566075027 0.2440229058 0.2138369977 - 0.2581019104 0.2455164939 0.2152577937 - 0.2595993876 0.2469965965 0.2166765928 - 0.2610979974 0.2484685034 0.2180949003 - 0.2625960112 0.2499337047 0.2195139974 - 0.2640933990 0.2513996959 0.2209234983 - 0.2655884027 0.2528609037 0.2223280966 - 0.2670783103 0.2543111145 0.2237278074 - 0.2685579062 0.2557500899 0.2251172066 - 0.2700324059 0.2571833134 0.2265006006 - 0.2715004086 0.2586081922 0.2278756052 - 0.2729619145 0.2600342035 0.2292470932 - 0.2744210064 0.2614606023 0.2306170017 - 0.2758783996 0.2628875077 0.2319861054 - 0.2773306072 0.2643063962 0.2333521992 - 0.2787795067 0.2657200098 0.2347161025 - 0.2802197933 0.2671194077 0.2360662967 - 0.2816554010 0.2685183883 0.2374131978 - 0.2830832005 0.2699165940 0.2387543023 - 0.2845051885 0.2713209987 0.2400884926 - 0.2859246135 0.2727270126 0.2414194047 - 0.2873364091 0.2741222978 0.2427445948 - 0.2887467146 0.2755121887 0.2440689951 - 0.2901543975 0.2768926919 0.2453920990 - 0.2915565968 0.2782715857 0.2467114031 - 0.2929557860 0.2796497047 0.2480283976 - 0.2943511903 0.2810314000 0.2493402064 - 0.2957446873 0.2824105918 0.2506507039 - 0.2971296012 0.2837719917 0.2519558966 - 0.2985079885 0.2851282954 0.2532579899 - 0.2998743057 0.2864750028 0.2545535862 - 0.3012337983 0.2878203988 0.2558461130 - 0.3025875092 0.2891646028 0.2571358085 - 0.3039326966 0.2905083001 0.2584241033 - 0.3052744865 0.2918517888 0.2597118020 - 0.3066090047 0.2931913137 0.2609947026 - 0.3079420924 0.2945300043 0.2622764111 - 0.3092617095 0.2958604097 0.2635529041 - 0.3105807900 0.2971901000 0.2648288012 - 0.3119010031 0.2985129058 0.2660990953 - 0.3132201135 0.2998352051 0.2673687041 - 0.3145304024 0.3011538088 0.2686296105 - 0.3158377111 0.3024721146 0.2698898911 - 0.3171265125 0.3037881851 0.2711451054 - 0.3184145093 0.3051016033 0.2723999918 - 0.3196972907 0.3063997030 0.2736532986 - 0.3209798932 0.3076966107 0.2749058902 - 0.3222613931 0.3089852929 0.2761533856 - 0.3235423863 0.3102742136 0.2774004936 - 0.3248170912 0.3115651011 0.2786423862 - 0.3260917962 0.3128559887 0.2798843086 - 0.3273518085 0.3141461015 0.2811206877 - 0.3286117911 0.3154362142 0.2823570967 - 0.3298718929 0.3167093098 0.2835853994 - 0.3311319947 0.3179805875 0.2848125100 - 0.3323861063 0.3192439079 0.2860355973 - 0.3336383998 0.3205048144 0.2872571945 - 0.3348825872 0.3217664957 0.2884761989 - 0.3361215889 0.3230285943 0.2896933854 - 0.3373571038 0.3242911100 0.2909066081 - 0.3385877907 0.3255540133 0.2921139002 - 0.3398196995 0.3268145919 0.2933216989 - 0.3410564065 0.3280658126 0.2945317924 - 0.3422931135 0.3293170929 0.2957417965 - 0.3435252011 0.3305566907 0.2969467938 - 0.3447571099 0.3317956924 0.2981514931 - 0.3459787965 0.3330340087 0.2993502021 - 0.3471955061 0.3342720866 0.3005459011 - 0.3484120071 0.3355073035 0.3017396927 - 0.3496277034 0.3367376029 0.3029299974 - 0.3508433104 0.3379676044 0.3041202128 - 0.3520529866 0.3391881883 0.3053084910 - 0.3532628119 0.3404087126 0.3064967096 - 0.3544704020 0.3416185081 0.3076831102 - 0.3556770086 0.3428226113 0.3088682890 - 0.3568829894 0.3440254033 0.3100539148 - 0.3580872118 0.3452241123 0.3112407029 - 0.3592914045 0.3464227021 0.3124274015 - 0.3604977131 0.3476234078 0.3136095107 - 0.3617044985 0.3488247097 0.3147901893 - 0.3629097939 0.3500243127 0.3159680068 - 0.3641118109 0.3512204885 0.3171392083 - 0.3653138876 0.3524166942 0.3183102906 - 0.3665078878 0.3536022007 0.3194763064 - 0.3677001894 0.3547854125 0.3206410110 - 0.3688907921 0.3559671938 0.3218050003 - 0.3700768948 0.3571453989 0.3229666948 - 0.3712630868 0.3583235145 0.3241283894 - 0.3724423051 0.3595018089 0.3252857924 - 0.3736186028 0.3606801927 0.3264412880 - 0.3747946024 0.3618564010 0.3275963068 - 0.3759672940 0.3630137146 0.3287459016 - 0.3771401048 0.3641709983 0.3298955858 - 0.3783135116 0.3653210104 0.3310425878 - 0.3794879019 0.3664624989 0.3321863115 - 0.3806622028 0.3676039875 0.3333300054 - 0.3818334043 0.3687413037 0.3344663978 - 0.3830037117 0.3698773980 0.3356004953 - 0.3841735125 0.3710137904 0.3367342949 - 0.3853363097 0.3721551001 0.3378620148 - 0.3864991069 0.3732962906 0.3389897943 - 0.3876604140 0.3744358122 0.3401161134 - 0.3888185024 0.3755710125 0.3412387073 - 0.3899765909 0.3767063022 0.3423613906 - 0.3911325037 0.3778327107 0.3434790969 - 0.3922863007 0.3789506853 0.3445914984 - 0.3934400976 0.3800686896 0.3457039893 - 0.3945881128 0.3811818957 0.3468174934 - 0.3957333863 0.3822929859 0.3479315042 - 0.3968786001 0.3834039867 0.3490453959 - 0.3980174065 0.3845176995 0.3501490057 - 0.3991548121 0.3856320083 0.3512499034 - 0.4002921879 0.3867461979 0.3523508012 - 0.4014247954 0.3878605962 0.3534472883 - 0.4025569856 0.3889749944 0.3545432985 - 0.4036892951 0.3900893927 0.3556393087 - 0.4048125148 0.3911888003 0.3567320108 - 0.4059354067 0.3922877014 0.3578243852 - 0.4070582986 0.3933865130 0.3589169085 - 0.4081676006 0.3944776952 0.3600068986 - 0.4092766047 0.3955687881 0.3610968888 - 0.4103856981 0.3966597915 0.3621867895 - 0.4114840925 0.3977529109 0.3632732928 - 0.4125820994 0.3988460898 0.3643594980 - 0.4136799872 0.3999392986 0.3654457033 - 0.4147689044 0.4010387957 0.3665322065 - 0.4158565998 0.4021390080 0.3676187098 - 0.4169442952 0.4032391906 0.3687050939 - 0.4180271924 0.4043292999 0.3697864115 - 0.4191086888 0.4054166079 0.3708660007 - 0.4201901853 0.4065037966 0.3719455004 - 0.4212687910 0.4075860977 0.3730236888 - 0.4223456979 0.4086655080 0.3741010129 - 0.4234224856 0.4097448885 0.3751783073 - 0.4244934916 0.4108251035 0.3762541115 - 0.4255565107 0.4119066000 0.3773277998 - 0.4266194999 0.4129880965 0.3784016073 - 0.4276801050 0.4140700102 0.3794747889 - 0.4287301898 0.4151534140 0.3805457950 - 0.4297803044 0.4162367880 0.3816167116 - 0.4308303893 0.4173201919 0.3826876879 - 0.4318735898 0.4183954000 0.3837552965 - 0.4329161942 0.4194698036 0.3848226070 - 0.4339587092 0.4205440879 0.3858897984 - 0.4349972010 0.4216128886 0.3869574070 - 0.4360328019 0.4226776063 0.3880251944 - 0.4370684922 0.4237422943 0.3890931010 - 0.4381031096 0.4248071015 0.3901597857 - 0.4391340017 0.4258719981 0.3912216127 - 0.4401648939 0.4269368947 0.3922834098 - 0.4411959052 0.4280017018 0.3933452070 - 0.4422194064 0.4290623963 0.3943997025 - 0.4432410896 0.4301218987 0.3954519928 - 0.4442627132 0.4311814904 0.3965043128 - 0.4452854097 0.4322386980 0.3975546062 - 0.4463103116 0.4332911074 0.3986003101 - 0.4473350942 0.4343433976 0.3996460140 - 0.4483599067 0.4353958070 0.4006916881 - 0.4493798018 0.4364373982 0.4017345905 - 0.4503985941 0.4374763966 0.4027766883 - 0.4514173865 0.4385155141 0.4038187861 - 0.4524323940 0.4395537078 0.4048602879 - 0.4534361959 0.4405891001 0.4059000015 - 0.4544399977 0.4416244924 0.4069396853 - 0.4554437995 0.4426599145 0.4079793990 - 0.4564459026 0.4436990917 0.4090223908 - 0.4574469030 0.4447405040 0.4100672901 - 0.4584479928 0.4457817972 0.4111123085 - 0.4594491124 0.4468230903 0.4121572077 - 0.4604510069 0.4478600025 0.4131881893 - 0.4614529908 0.4488969147 0.4142186940 - 0.4624550045 0.4499337971 0.4152492881 - 0.4634560049 0.4509662986 0.4162780941 - 0.4644544125 0.4519883096 0.4173018932 - 0.4654527009 0.4530102015 0.4183256924 - 0.4664511085 0.4540322125 0.4193494916 - 0.4674443901 0.4550513029 0.4203704000 - 0.4684332907 0.4560678899 0.4213885963 - 0.4694221020 0.4570845068 0.4224067032 - 0.4704109132 0.4581010938 0.4234248996 - 0.4714013934 0.4591206014 0.4244410992 - 0.4723925889 0.4601413012 0.4254566133 - 0.4733838141 0.4611620009 0.4264720976 - 0.4743750095 0.4621827006 0.4274876118 - 0.4753603935 0.4631980062 0.4285031855 - 0.4763449132 0.4642125070 0.4295187891 - 0.4773294032 0.4652270079 0.4305343926 - 0.4783138931 0.4662415087 0.4315499961 - 0.4792841971 0.4672405124 0.4325644970 - 0.4802536964 0.4682387114 0.4335789979 - 0.4812232852 0.4692369103 0.4345934987 - 0.4821928144 0.4702351093 0.4356079996 - 0.4831587970 0.4712263942 0.4366190135 - 0.4841246903 0.4722175002 0.4376297891 - 0.4850907028 0.4732086957 0.4386407137 - 0.4860565960 0.4741998911 0.4396516085 - 0.4870223999 0.4751932025 0.4406566024 - 0.4879881144 0.4761866033 0.4416612983 - 0.4889538884 0.4771799147 0.4426659942 - 0.4899196029 0.4781732857 0.4436706901 - 0.4908815026 0.4791612923 0.4446693063 - 0.4918429852 0.4801487923 0.4456669092 - 0.4928044081 0.4811362028 0.4466646016 - 0.4937658906 0.4821236134 0.4476622045 - 0.4947195053 0.4831021130 0.4486567080 - 0.4956707954 0.4840778112 0.4496499896 - 0.4966219962 0.4850535095 0.4506433010 - 0.4975732863 0.4860292077 0.4516366124 - 0.4985210896 0.4869993031 0.4526301026 - 0.4994663000 0.4879654050 0.4536238015 - 0.5004115105 0.4889315069 0.4546175897 - 0.5013567209 0.4898976088 0.4556112885 - 0.5022984147 0.4908634126 0.4566054046 - 0.5032333732 0.4918287992 0.4576002955 - 0.5041682720 0.4927941859 0.4585951865 - 0.5051032901 0.4937596023 0.4595901966 - 0.5060374737 0.4947249889 0.4605847895 - 0.5069562197 0.4956893921 0.4615682960 - 0.5078749061 0.4966539145 0.4625518024 - 0.5087934732 0.4976184070 0.4635351896 - 0.5097122192 0.4985828996 0.4645186961 - 0.5106272101 0.4995400012 0.4654968977 - 0.5115404725 0.5004938245 0.4664725959 - 0.5124536753 0.5014476180 0.4674482942 - 0.5133669972 0.5024012923 0.4684239030 - 0.5142818093 0.5033521056 0.4693973958 - 0.5152004957 0.5042945743 0.4703643918 - 0.5161191821 0.5052371025 0.4713312984 - 0.5170379281 0.5061795712 0.4722982943 - 0.5179566145 0.5071220994 0.4732652009 - 0.5188748837 0.5080627203 0.4742276073 - 0.5197929740 0.5090026855 0.4751887023 - 0.5207111835 0.5099428296 0.4761497974 - 0.5216293931 0.5108827949 0.4771108925 - 0.5225464106 0.5118234158 0.4780718088 - 0.5234599113 0.5127655268 0.4790318906 - 0.5243734121 0.5137075186 0.4799919128 - 0.5252867937 0.5146496296 0.4809519947 - 0.5262002945 0.5155916810 0.4819121063 - 0.5271133780 0.5165237784 0.4828720093 - 0.5280262232 0.5174506903 0.4838317931 - 0.5289388895 0.5183774829 0.4847916067 - 0.5298516750 0.5193043947 0.4857513011 - 0.5307645202 0.5202311873 0.4867111146 - 0.5316731930 0.5211473107 0.4876579940 - 0.5325819254 0.5220631957 0.4886044860 - 0.5334905982 0.5229790807 0.4895510077 - 0.5343992710 0.5238950253 0.4904974997 - 0.5353068709 0.5248103142 0.4914431870 - 0.5362116098 0.5257238150 0.4923861027 - 0.5371162891 0.5266373754 0.4933289886 - 0.5380210280 0.5275508761 0.4942719936 - 0.5389257073 0.5284643769 0.4952149093 - 0.5398265719 0.5293797851 0.4961544871 - 0.5407235026 0.5302973986 0.4970901012 - 0.5416203141 0.5312148929 0.4980256855 - 0.5425171256 0.5321325064 0.4989612997 - 0.5434139967 0.5330500007 0.4998969138 - 0.5443124175 0.5339617729 0.5008286834 - 0.5452117920 0.5348700881 0.5017580986 - 0.5461112261 0.5357784033 0.5026873946 - 0.5470106006 0.5366867185 0.5036168098 - 0.5479100943 0.5375949740 0.5045462251 - 0.5488089919 0.5384951830 0.5054730773 - 0.5497077107 0.5393918157 0.5063987970 - 0.5506064892 0.5402885079 0.5073245764 - 0.5515052080 0.5411852002 0.5082504153 - 0.5524039268 0.5420818925 0.5091760755 - 0.5533022285 0.5429762006 0.5100972056 - 0.5542001724 0.5438696742 0.5110161901 - 0.5550982952 0.5447632074 0.5119352937 - 0.5559964180 0.5456566811 0.5128542781 - 0.5568944216 0.5465502143 0.5137733221 - 0.5577896833 0.5474457145 0.5146881938 - 0.5586835146 0.5483422875 0.5156006813 - 0.5595772862 0.5492389202 0.5165132880 - 0.5604711175 0.5501356125 0.5174257755 - 0.5613648891 0.5510321856 0.5183383822 - 0.5622547269 0.5519235730 0.5192475915 - 0.5631409883 0.5528106093 0.5201537013 - 0.5640273094 0.5536975861 0.5210598111 - 0.5649135709 0.5545846224 0.5219659209 - 0.5657997727 0.5554715991 0.5228719711 - 0.5666835904 0.5563544035 0.5237771869 - 0.5675624013 0.5572292209 0.5246803164 - 0.5684412718 0.5581040978 0.5255835056 - 0.5693200827 0.5589789152 0.5264865756 - 0.5701988935 0.5598536730 0.5273898244 - 0.5710769296 0.5607281923 0.5282927155 - 0.5719450116 0.5615991950 0.5291919112 - 0.5728130937 0.5624700785 0.5300911069 - 0.5736811757 0.5633410215 0.5309904218 - 0.5745493174 0.5642119050 0.5318896174 - 0.5754173994 0.5650827885 0.5327888131 - 0.5762851238 0.5659552813 0.5336858034 - 0.5771527290 0.5668284297 0.5345818996 - 0.5780202746 0.5677015185 0.5354781151 - 0.5788878798 0.5685747266 0.5363742113 - 0.5797554851 0.5694478154 0.5372703075 - 0.5806217790 0.5703194737 0.5381652713 - 0.5814853907 0.5711885095 0.5390577912 - 0.5823488832 0.5720576048 0.5399503112 - 0.5832124949 0.5729265809 0.5408427119 - 0.5840761065 0.5737956762 0.5417351723 - 0.5849397182 0.5746647120 0.5426276922 - 0.5858016014 0.5755252838 0.5435200930 - 0.5866631269 0.5763844252 0.5444124937 - 0.5875247121 0.5772435069 0.5453047752 - 0.5883861780 0.5781025290 0.5461971760 - 0.5892478228 0.5789616108 0.5470895171 - 0.5901088119 0.5798196197 0.5479819179 - 0.5909687281 0.5806747079 0.5488741994 - 0.5918287039 0.5815299153 0.5497664213 - 0.5926886201 0.5823850036 0.5506587029 - 0.5935484767 0.5832400918 0.5515509844 - 0.5944083929 0.5840952992 0.5524433255 - 0.5952606797 0.5849506259 0.5533313751 - 0.5961092114 0.5858058929 0.5542173982 - 0.5969576836 0.5866612196 0.5551033020 - 0.5978062153 0.5875164866 0.5559893250 - 0.5986546874 0.5883718729 0.5568752289 - 0.5995032191 0.5892271996 0.5577610731 - 0.6003429294 0.5900741816 0.5586416721 - 0.6011822820 0.5909208059 0.5595219135 - 0.6020215750 0.5917673707 0.5604020953 - 0.6028609872 0.5926139951 0.5612822771 - 0.6037002802 0.5934606194 0.5621625185 - 0.6045382023 0.5943056941 0.5630425215 - 0.6053689718 0.5951434970 0.5639213920 - 0.6061998010 0.5959812999 0.5648003221 - 0.6070305705 0.5968191028 0.5656793118 - 0.6078615189 0.5976567864 0.5665581822 - 0.6086922884 0.5984945893 0.5674371123 - 0.6095213890 0.5993313789 0.5683159828 - 0.6103464961 0.6001660228 0.5691947937 - 0.6111717224 0.6010006070 0.5700736046 - 0.6119968891 0.6018350720 0.5709522963 - 0.6128221154 0.6026697159 0.5718311071 - 0.6136472821 0.6035043001 0.5727099180 - 0.6144716144 0.6043398976 0.5735872984 - 0.6152943969 0.6051772237 0.5744621754 - 0.6161171794 0.6060143709 0.5753371716 - 0.6169400215 0.6068516970 0.5762121081 - 0.6177628040 0.6076890230 0.5770869851 - 0.6185857058 0.6085262895 0.5779619813 - 0.6194078922 0.6093611121 0.5788385272 - 0.6202291846 0.6101909280 0.5797184110 - 0.6210504770 0.6110206842 0.5805981755 - 0.6218718290 0.6118505001 0.5814781189 - 0.6226930022 0.6126803756 0.5823580027 - 0.6235142946 0.6135101914 0.5832378268 - 0.6243357062 0.6143385172 0.5841162801 - 0.6251572967 0.6151615977 0.5849894285 - 0.6259788871 0.6159847975 0.5858625770 - 0.6268004775 0.6168079972 0.5867357254 - 0.6276221275 0.6176310778 0.5876088738 - 0.6284437180 0.6184542775 0.5884820223 - 0.6292651892 0.6192772985 0.5893549919 - 0.6300833821 0.6200963259 0.5902221799 - 0.6309016943 0.6209151745 0.5910894275 - 0.6317198873 0.6217342019 0.5919566154 - 0.6325381994 0.6225532293 0.5928236842 - 0.6333565116 0.6233721972 0.5936908722 - 0.6341747046 0.6241912246 0.5945581198 - 0.6349883080 0.6250126958 0.5954239964 - 0.6358004212 0.6258351207 0.5962893963 - 0.6366125941 0.6266574860 0.5971549153 - 0.6374247074 0.6274797916 0.5980203152 - 0.6382368207 0.6283022165 0.5988857746 - 0.6390489936 0.6291245222 0.5997511744 - 0.6398593783 0.6299480796 0.6006168723 - 0.6406673789 0.6307734847 0.6014829874 - 0.6414753199 0.6315988898 0.6023491025 - 0.6422833204 0.6324242949 0.6032152176 - 0.6430912018 0.6332497001 0.6040812731 - 0.6438992023 0.6340751052 0.6049473882 - 0.6447070837 0.6349005103 0.6058135033 - 0.6455112100 0.6357209086 0.6066793203 - 0.6463152170 0.6365411878 0.6075451970 - 0.6471192241 0.6373614073 0.6084110141 - 0.6479231119 0.6381816268 0.6092767715 - 0.6487271190 0.6390019059 0.6101427078 - 0.6495311260 0.6398221254 0.6110085249 - 0.6503347754 0.6406397820 0.6118733883 - 0.6511383057 0.6414539814 0.6127370000 - 0.6519417763 0.6422681808 0.6136006117 - 0.6527453065 0.6430823803 0.6144642234 - 0.6535487771 0.6438966990 0.6153277755 - 0.6543523073 0.6447108984 0.6161913872 - 0.6551557779 0.6455250978 0.6170549989 - 0.6559563279 0.6463404298 0.6179105043 - 0.6567561030 0.6471558809 0.6187639832 - 0.6575558782 0.6479713917 0.6196174026 - 0.6583557129 0.6487870216 0.6204708219 - 0.6591554880 0.6496024728 0.6213243008 - 0.6599553227 0.6504179835 0.6221777201 - 0.6607547998 0.6512334943 0.6230304241 - 0.6615508795 0.6520485878 0.6238759756 - 0.6623470187 0.6528638005 0.6247217059 - 0.6631432176 0.6536788940 0.6255673170 - 0.6639392972 0.6544939876 0.6264129281 - 0.6647353768 0.6553090811 0.6272584796 - 0.6655315161 0.6561242938 0.6281040907 - 0.6663259268 0.6569358706 0.6289486289 - 0.6671159267 0.6577392220 0.6297904253 - 0.6679059863 0.6585425735 0.6306322813 - 0.6686959863 0.6593459845 0.6314740777 - 0.6694861054 0.6601492763 0.6323158741 - 0.6702761054 0.6609526873 0.6331576705 - 0.6710662246 0.6617559791 0.6339995265 - 0.6718522906 0.6625568271 0.6348415017 - 0.6726325750 0.6633538008 0.6356834769 - 0.6734127998 0.6641507149 0.6365255713 - 0.6741930842 0.6649476886 0.6373677254 - 0.6749733090 0.6657446027 0.6382098198 - 0.6757534742 0.6665415764 0.6390517950 - 0.6765338182 0.6673386097 0.6398938894 - 0.6773107052 0.6681358814 0.6407364011 - 0.6780828834 0.6689336896 0.6415793896 - 0.6788551211 0.6697314978 0.6424224973 - 0.6796272993 0.6705293059 0.6432656050 - 0.6803994775 0.6713271141 0.6441085935 - 0.6811717153 0.6721249223 0.6449517012 - 0.6819438934 0.6729226708 0.6457946897 - 0.6827138066 0.6737201810 0.6466346979 - 0.6834788918 0.6745169163 0.6474680901 - 0.6842439771 0.6753137112 0.6483014226 - 0.6850091219 0.6761105061 0.6491346955 - 0.6857742071 0.6769071817 0.6499680281 - 0.6865392923 0.6777039766 0.6508014202 - 0.6873043776 0.6785007715 0.6516346931 - 0.6880689263 0.6792957187 0.6524670720 - 0.6888297200 0.6800801754 0.6532937884 - 0.6895905137 0.6808646917 0.6541203856 - 0.6903513074 0.6816490889 0.6549469829 - 0.6911121011 0.6824336052 0.6557736993 - 0.6918728948 0.6832181215 0.6566002965 - 0.6926336884 0.6840025187 0.6574270129 - 0.6933944821 0.6847869754 0.6582536101 - 0.6941537261 0.6855652928 0.6590738297 - 0.6949126720 0.6863427758 0.6598929167 - 0.6956717968 0.6871201992 0.6607121229 - 0.6964308023 0.6878976226 0.6615312099 - 0.6971898079 0.6886749864 0.6623502970 - 0.6979488134 0.6894524097 0.6631695032 - 0.6987078190 0.6902298927 0.6639885902 - 0.6994645000 0.6910073161 0.6648052931 - 0.7002186179 0.6917846799 0.6656193733 - 0.7009727955 0.6925622225 0.6664335132 - 0.7017269731 0.6933395863 0.6672475934 - 0.7024810910 0.6941171288 0.6680616140 - 0.7032353282 0.6948944926 0.6688756943 - 0.7039895058 0.6956719756 0.6696897745 - 0.7047435045 0.6964492798 0.6705039144 - 0.7054880261 0.6972190738 0.6713137031 - 0.7062326074 0.6979889870 0.6721233726 - 0.7069771290 0.6987587810 0.6729332209 - 0.7077215910 0.6995285749 0.6737430096 - 0.7084661722 0.7002984881 0.6745527983 - 0.7092106938 0.7010682821 0.6753625274 - 0.7099552751 0.7018380761 0.6761723161 - 0.7106971145 0.7026013136 0.6769785881 - 0.7114353776 0.7033559084 0.6777802110 - 0.7121738195 0.7041105032 0.6785817742 - 0.7129122019 0.7048650980 0.6793833971 - 0.7136505246 0.7056195736 0.6801850200 - 0.7143889070 0.7063742280 0.6809865236 - 0.7151272893 0.7071288228 0.6817880869 - 0.7158656120 0.7078834176 0.6825897098 - 0.7165970206 0.7086318135 0.6833915710 - 0.7173261046 0.7093781829 0.6841934919 - 0.7180553079 0.7101244926 0.6849954128 - 0.7187843919 0.7108709216 0.6857973933 - 0.7195134759 0.7116172910 0.6865993142 - 0.7202426791 0.7123637199 0.6874011755 - 0.7209718227 0.7131100893 0.6882030964 - 0.7217009068 0.7138565183 0.6890050769 - 0.7224162817 0.7146016955 0.6897974014 - 0.7231312990 0.7153468132 0.6905894279 - 0.7238463163 0.7160919905 0.6913813949 - 0.7245612144 0.7168372273 0.6921733022 - 0.7252761722 0.7175822854 0.6929653287 - 0.7259911895 0.7183275223 0.6937571764 - 0.7267062068 0.7190726995 0.6945492029 - 0.7274206877 0.7198178172 0.6953408718 - 0.7281301022 0.7205628157 0.6961287260 - 0.7288395166 0.7213078141 0.6969165802 - 0.7295488715 0.7220528126 0.6977043748 - 0.7302582860 0.7227978110 0.6984922886 - 0.7309677005 0.7235428095 0.6992802024 - 0.7316771150 0.7242878079 0.7000679970 - 0.7323865294 0.7250328064 0.7008559108 - 0.7330961823 0.7257779241 0.7016434073 - 0.7338085175 0.7265239954 0.7024263740 - 0.7345207930 0.7272701263 0.7032094002 - 0.7352331281 0.7280163169 0.7039923072 - 0.7359454036 0.7287623882 0.7047752738 - 0.7366576791 0.7295085192 0.7055583000 - 0.7373700142 0.7302545905 0.7063413262 - 0.7380822897 0.7310007215 0.7071242929 - 0.7387946248 0.7317467928 0.7079071999 - 0.7395036817 0.7324852943 0.7086831927 - 0.7402127981 0.7332236171 0.7094588876 - 0.7409217954 0.7339618206 0.7102347016 - 0.7416307926 0.7347000837 0.7110103965 - 0.7423397899 0.7354382873 0.7117860913 - 0.7430489063 0.7361766100 0.7125617862 - 0.7437579036 0.7369148731 0.7133374810 - 0.7444669008 0.7376530766 0.7141131759 - 0.7451686263 0.7383893132 0.7148858905 - 0.7458680272 0.7391248941 0.7156574726 - 0.7465673089 0.7398604155 0.7164289951 - 0.7472665906 0.7405959964 0.7172005773 - 0.7479659915 0.7413315177 0.7179722190 - 0.7486652732 0.7420669794 0.7187438011 - 0.7493646145 0.7428026199 0.7195153832 - 0.7500640154 0.7435380816 0.7202870250 - 0.7507627010 0.7442746162 0.7210568786 - 0.7514607906 0.7450124025 0.7218245268 - 0.7521588206 0.7457501888 0.7225919962 - 0.7528567910 0.7464879751 0.7233595252 - 0.7535548210 0.7472257018 0.7241271138 - 0.7542527914 0.7479634881 0.7248945832 - 0.7549508214 0.7487012744 0.7256621718 - 0.7556489110 0.7494391203 0.7264297009 - 0.7563468814 0.7501769066 0.7271971703 - 0.7570462227 0.7509121895 0.7279605269 - 0.7577455044 0.7516474724 0.7287237048 - 0.7584449053 0.7523828149 0.7294868827 - 0.7591441870 0.7531180978 0.7302501202 - 0.7598435879 0.7538533807 0.7310132980 - 0.7605429292 0.7545887232 0.7317764163 - 0.7612422109 0.7553240061 0.7325395942 - 0.7619416118 0.7560592890 0.7333027720 - 0.7626401782 0.7567914128 0.7340652943 - 0.7633376718 0.7575193048 0.7348269820 - 0.7640352249 0.7582470775 0.7355887294 - 0.7647327781 0.7589750290 0.7363502979 - 0.7654302716 0.7597029209 0.7371119857 - 0.7661278844 0.7604308128 0.7378736138 - 0.7668253779 0.7611585855 0.7386353016 - 0.7675229907 0.7618864775 0.7393969297 - 0.7682204843 0.7626144290 0.7401586175 - 0.7689135075 0.7633404136 0.7409185767 - 0.7696049809 0.7640656829 0.7416778803 - 0.7702965140 0.7647910714 0.7424373031 - 0.7709881067 0.7655165195 0.7431966066 - 0.7716795802 0.7662419081 0.7439560294 - 0.7723711133 0.7669671774 0.7447152734 - 0.7730625868 0.7676926255 0.7454746962 - 0.7737541199 0.7684180140 0.7462341189 - 0.7744455934 0.7691434026 0.7469934225 - 0.7751392722 0.7698696852 0.7477499843 - 0.7758330107 0.7705960274 0.7485064268 - 0.7765266895 0.7713223100 0.7492629290 - 0.7772204280 0.7720487118 0.7500193119 - 0.7779141068 0.7727749944 0.7507756948 - 0.7786077857 0.7735012770 0.7515321970 - 0.7793015242 0.7742276788 0.7522885799 - 0.7799953222 0.7749540210 0.7530450225 - 0.7806888223 0.7756797075 0.7538012266 - 0.7813785076 0.7763937712 0.7545518279 - 0.7820681930 0.7771078944 0.7553023100 - 0.7827579975 0.7778220177 0.7560529113 - 0.7834476829 0.7785360217 0.7568033934 - 0.7841374874 0.7792500854 0.7575538754 - 0.7848271728 0.7799642086 0.7583044767 - 0.7855169773 0.7806782722 0.7590550184 - 0.7862067223 0.7813923955 0.7598056197 - 0.7868962884 0.7821062803 0.7605561018 - 0.7875761986 0.7828112841 0.7613086104 - 0.7882561088 0.7835162878 0.7620609999 - 0.7889360785 0.7842212915 0.7628135085 - 0.7896159887 0.7849262953 0.7635658979 - 0.7902958989 0.7856312990 0.7643182874 - 0.7909759283 0.7863363028 0.7650707960 - 0.7916557789 0.7870413065 0.7658231854 - 0.7923356891 0.7877463102 0.7665756941 - 0.7930157185 0.7884513736 0.7673280835 - 0.7936940193 0.7891541123 0.7680817842 - 0.7943720818 0.7898566127 0.7688357830 - 0.7950502038 0.7905589938 0.7695897222 - 0.7957283258 0.7912613750 0.7703436017 - 0.7964063883 0.7919638157 0.7710976005 - 0.7970845103 0.7926663160 0.7718514800 - 0.7977625728 0.7933686972 0.7726054788 - 0.7984406948 0.7940710783 0.7733594179 - 0.7991188169 0.7947735190 0.7741132975 - 0.7997971773 0.7954763770 0.7748653293 - 0.8004758954 0.7961794734 0.7756156921 - 0.8011544943 0.7968826890 0.7763661146 - 0.8018332124 0.7975857854 0.7771165967 - 0.8025118113 0.7982890010 0.7778670192 - 0.8031905293 0.7989920974 0.7786175013 - 0.8038691282 0.7996953130 0.7793679237 - 0.8045477867 0.8003984094 0.7801182866 - 0.8052263856 0.8011015058 0.7808688283 - 0.8059049249 0.8018025160 0.7816187143 - 0.8065828085 0.8024935126 0.7823663950 - 0.8072608113 0.8031843901 0.7831140757 - 0.8079386950 0.8038753867 0.7838618159 - 0.8086165786 0.8045663238 0.7846094966 - 0.8092945218 0.8052573204 0.7853571773 - 0.8099725246 0.8059481978 0.7861049175 - 0.8106504083 0.8066391945 0.7868525982 - 0.8113282919 0.8073300719 0.7876002789 - 0.8120061755 0.8080211282 0.7883480191 - 0.8126829267 0.8087062240 0.7890954018 - 0.8133587837 0.8093882203 0.7898424864 - 0.8140347004 0.8100702167 0.7905896902 - 0.8147106767 0.8107520938 0.7913368940 - 0.8153865933 0.8114340901 0.7920840979 - 0.8160626292 0.8121160865 0.7928311825 - 0.8167384863 0.8127980828 0.7935783863 - 0.8174144030 0.8134800196 0.7943255901 - 0.8180903792 0.8141620159 0.7950726748 - 0.8187662959 0.8148440123 0.7958198786 - 0.8194429874 0.8155233860 0.7965657711 - 0.8201196194 0.8162028193 0.7973116040 - 0.8207963109 0.8168823123 0.7980574965 - 0.8214728832 0.8175616860 0.7988033891 - 0.8221495748 0.8182411194 0.7995492220 - 0.8228263259 0.8189204931 0.8002951145 - 0.8235028982 0.8195999265 0.8010408878 - 0.8241795897 0.8202794194 0.8017867804 - 0.8248562217 0.8209587932 0.8025326133 - 0.8255324960 0.8216388822 0.8032788038 - 0.8262073994 0.8223220706 0.8040260077 - 0.8268823028 0.8230053782 0.8047732711 - 0.8275572062 0.8236886263 0.8055204749 - 0.8282319903 0.8243718743 0.8062677979 - 0.8289068937 0.8250551224 0.8070150018 - 0.8295817971 0.8257383108 0.8077623248 - 0.8302565813 0.8264216185 0.8085095286 - 0.8309314847 0.8271048069 0.8092567921 - 0.8316063881 0.8277881145 0.8100039959 - 0.8322811723 0.8284710050 0.8107491732 - 0.8329560161 0.8291531205 0.8114882708 - 0.8336306810 0.8298351169 0.8122274280 - 0.8343054056 0.8305171728 0.8129665256 - 0.8349801898 0.8311992884 0.8137056231 - 0.8356549144 0.8318814039 0.8144447207 - 0.8363295794 0.8325635195 0.8151838183 - 0.8370043039 0.8332455754 0.8159229159 - 0.8376790881 0.8339275718 0.8166620135 - 0.8383538127 0.8346096873 0.8174011111 - 0.8390284777 0.8352907896 0.8181397915 - 0.8397032022 0.8359678984 0.8188765049 - 0.8403778076 0.8366451263 0.8196132779 - 0.8410524726 0.8373221755 0.8203501105 - 0.8417270780 0.8379992843 0.8210868835 - 0.8424018025 0.8386765122 0.8218235970 - 0.8430765271 0.8393536210 0.8225604296 - 0.8437510729 0.8400307298 0.8232972026 - 0.8444257975 0.8407077789 0.8240339756 - 0.8451004028 0.8413850069 0.8247706890 - 0.8457751274 0.8420621157 0.8255075216 - 0.8464422822 0.8427348137 0.8262410164 - 0.8471094966 0.8434073925 0.8269745111 - 0.8477767110 0.8440800905 0.8277080059 - 0.8484439254 0.8447527289 0.8284415007 - 0.8491110802 0.8454254270 0.8291749954 - 0.8497782946 0.8460981250 0.8299083710 - 0.8504455090 0.8467707038 0.8306419253 - 0.8511127234 0.8474434018 0.8313754201 - 0.8517798185 0.8481160998 0.8321089149 - 0.8524469733 0.8487886786 0.8328424096 - 0.8531115055 0.8494626284 0.8335720897 - 0.8537746072 0.8501369953 0.8342999220 - 0.8544377089 0.8508114219 0.8350275755 - 0.8551008105 0.8514857888 0.8357554078 - 0.8557639122 0.8521602154 0.8364831209 - 0.8564270735 0.8528345823 0.8372108936 - 0.8570901752 0.8535091281 0.8379387259 - 0.8577532768 0.8541834950 0.8386663795 - 0.8584163785 0.8548579216 0.8393942118 - 0.8590794802 0.8555322886 0.8401219249 - 0.8597424030 0.8562076092 0.8408495188 - 0.8604043722 0.8568869233 0.8415760994 - 0.8610665202 0.8575661778 0.8423027992 - 0.8617284894 0.8582454920 0.8430293798 - 0.8623905182 0.8589248061 0.8437560797 - 0.8630526066 0.8596040010 0.8444827199 - 0.8637145758 0.8602833152 0.8452093005 - 0.8643766046 0.8609626293 0.8459360003 - 0.8650386930 0.8616418839 0.8466625810 - 0.8657007217 0.8623211980 0.8473892212 - 0.8663626909 0.8630003929 0.8481159210 - 0.8670225143 0.8636755943 0.8488407731 - 0.8676806092 0.8643475175 0.8495643139 - 0.8683385849 0.8650195003 0.8502879143 - 0.8689966798 0.8656914234 0.8510113955 - 0.8696547747 0.8663634062 0.8517348766 - 0.8703128099 0.8670353889 0.8524584770 - 0.8709709048 0.8677073121 0.8531820178 - 0.8716288805 0.8683792949 0.8539056182 - 0.8722869754 0.8690512180 0.8546290994 - 0.8729450703 0.8697232008 0.8553525805 - 0.8736031055 0.8703951836 0.8560761809 - 0.8742563128 0.8710637093 0.8567957282 - 0.8749082088 0.8717314005 0.8575140834 - 0.8755602241 0.8723990917 0.8582326174 - 0.8762121201 0.8730667830 0.8589509726 - 0.8768640161 0.8737344742 0.8596695065 - 0.8775159717 0.8744022846 0.8603879809 - 0.8781679273 0.8750699759 0.8611063957 - 0.8788198829 0.8757377267 0.8618249297 - 0.8794717789 0.8764054179 0.8625432849 - 0.8801236749 0.8770731091 0.8632618189 - 0.8807756901 0.8777408004 0.8639801741 - 0.8814250231 0.8784101009 0.8646934032 - 0.8820741177 0.8790795803 0.8654057980 - 0.8827230930 0.8797491193 0.8661181927 - 0.8833721280 0.8804187179 0.8668307066 - 0.8840212226 0.8810881972 0.8675431013 - 0.8846701980 0.8817576766 0.8682554960 - 0.8853191733 0.8824272156 0.8689680099 - 0.8859683275 0.8830966949 0.8696804047 - 0.8866173029 0.8837661743 0.8703927994 - 0.8872662783 0.8844357133 0.8711053133 - 0.8879153728 0.8851051927 0.8718177080 - 0.8885645270 0.8857750893 0.8725255728 - 0.8892136812 0.8864449859 0.8732324839 - 0.8898627758 0.8871150017 0.8739395142 - 0.8905119896 0.8877848983 0.8746464252 - 0.8911612034 0.8884547949 0.8753533959 - 0.8918102980 0.8891248107 0.8760603070 - 0.8924595118 0.8897947073 0.8767672777 - 0.8931086063 0.8904647231 0.8774741888 - 0.8937578201 0.8911346197 0.8781810999 - 0.8944069743 0.8918045163 0.8788880706 - 0.8950561285 0.8924744725 0.8795949817 - 0.8957039118 0.8931388259 0.8802983165 - 0.8963508010 0.8937997222 0.8809992075 - 0.8969975710 0.8944606185 0.8817002177 - 0.8976445198 0.8951215148 0.8824012280 - 0.8982914090 0.8957824111 0.8831021786 - 0.8989382982 0.8964433074 0.8838031888 - 0.8995851278 0.8971043229 0.8845040798 - 0.9002320170 0.8977652192 0.8852050900 - 0.9008789062 0.8984261155 0.8859061003 - 0.9015257955 0.8990870118 0.8866071105 - 0.9021726847 0.8997479081 0.8873080015 - 0.9028183222 0.9004076123 0.8880085945 - 0.9034605026 0.9010636806 0.8887078166 - 0.9041026831 0.9017199278 0.8894069791 - 0.9047449231 0.9023761153 0.8901063204 - 0.9053872228 0.9030323029 0.8908054829 - 0.9060294032 0.9036884904 0.8915048242 - 0.9066715837 0.9043446183 0.8922039866 - 0.9073138237 0.9050008059 0.8929033279 - 0.9079560041 0.9056569934 0.8936024904 - 0.9085981846 0.9063131809 0.8943017721 - 0.9092404246 0.9069694281 0.8950009942 - 0.9098826051 0.9076254964 0.8957002759 - 0.9105213881 0.9082816243 0.8964002132 - 0.9111589789 0.9089375138 0.8971003890 - 0.9117966294 0.9095935225 0.8978006244 - 0.9124342203 0.9102494717 0.8985008001 - 0.9130718112 0.9109054804 0.8992009759 - 0.9137094021 0.9115614891 0.8999012709 - 0.9143469930 0.9122173786 0.9006015062 - 0.9149845839 0.9128733873 0.9013016820 - 0.9156221747 0.9135293961 0.9020019174 - 0.9162598252 0.9141854048 0.9027020931 - 0.9168974161 0.9148412943 0.9034023285 - 0.9175344110 0.9154965281 0.9041017890 - 0.9181634784 0.9161416292 0.9047920108 - 0.9187926054 0.9167866707 0.9054822922 - 0.9194216728 0.9174317718 0.9061725140 - 0.9200507998 0.9180769920 0.9068626761 - 0.9206799865 0.9187220931 0.9075530171 - 0.9213091135 0.9193671942 0.9082431793 - 0.9219381809 0.9200122952 0.9089334011 - 0.9225673079 0.9206575155 0.9096236825 - 0.9231963754 0.9213026166 0.9103139043 - 0.9238255024 0.9219477177 0.9110041857 - 0.9244546294 0.9225928187 0.9116944075 - 0.9250795841 0.9232333899 0.9123820066 - 0.9256916046 0.9238601923 0.9130614996 - 0.9263036847 0.9244869947 0.9137409925 - 0.9269157052 0.9251136780 0.9144204855 - 0.9275277853 0.9257404804 0.9150999784 - 0.9281398058 0.9263672233 0.9157794714 - 0.9287518263 0.9269940257 0.9164590836 - 0.9293639064 0.9276207089 0.9171385765 - 0.9299759269 0.9282475114 0.9178181291 - 0.9305880070 0.9288743138 0.9184976220 - 0.9312000275 0.9295009971 0.9191771150 - 0.9318121076 0.9301277995 0.9198566079 - 0.9324201941 0.9307510853 0.9205334187 - 0.9330167174 0.9313644767 0.9212021232 - 0.9336131811 0.9319778085 0.9218708873 - 0.9342097044 0.9325911999 0.9225397110 - 0.9348061085 0.9332044721 0.9232084155 - 0.9354025722 0.9338178039 0.9238771796 - 0.9359990954 0.9344311953 0.9245458841 - 0.9365956187 0.9350445271 0.9252147079 - 0.9371920824 0.9356579185 0.9258834720 - 0.9377886057 0.9362711906 0.9265521765 - 0.9383851290 0.9368845820 0.9272210002 - 0.9389814734 0.9374979138 0.9278897047 - 0.9395771027 0.9381104708 0.9285578728 - 0.9401646256 0.9387161732 0.9292200804 - 0.9407520294 0.9393218756 0.9298822880 - 0.9413393736 0.9399275780 0.9305444956 - 0.9419268966 0.9405332804 0.9312068224 - 0.9425143003 0.9411389232 0.9318689704 - 0.9431017041 0.9417446256 0.9325311780 - 0.9436892271 0.9423503280 0.9331933856 - 0.9442765713 0.9429559708 0.9338555932 - 0.9448639750 0.9435616732 0.9345179200 - 0.9454514980 0.9441673756 0.9351801276 - 0.9460389018 0.9447730780 0.9358422756 - 0.9466264248 0.9453787804 0.9365044832 - 0.9472072124 0.9459720850 0.9371623993 - 0.9477860928 0.9465619922 0.9378191233 - 0.9483650923 0.9471517801 0.9384757876 - 0.9489439726 0.9477416277 0.9391323924 - 0.9495229721 0.9483314753 0.9397891164 - 0.9501019120 0.9489213228 0.9404457211 - 0.9506809115 0.9495111704 0.9411023855 - 0.9512599111 0.9501010180 0.9417591095 - 0.9518387914 0.9506909251 0.9424157143 - 0.9524177909 0.9512807131 0.9430723786 - 0.9529966712 0.9518705010 0.9437291026 - 0.9535756707 0.9524604082 0.9443857074 - 0.9541510940 0.9530453086 0.9450407028 - 0.9547178149 0.9536182880 0.9456915855 - 0.9552845955 0.9541913867 0.9463424087 - 0.9558513761 0.9547644258 0.9469932914 - 0.9564182162 0.9553375244 0.9476441145 - 0.9569848776 0.9559105039 0.9482949972 - 0.9575517178 0.9564834833 0.9489458203 - 0.9581184983 0.9570565820 0.9495967031 - 0.9586852789 0.9576296210 0.9502475262 - 0.9592519999 0.9582027197 0.9508984089 - 0.9598187804 0.9587756991 0.9515491724 - 0.9603856206 0.9593486786 0.9522001147 - 0.9609522820 0.9599217772 0.9528508782 - 0.9615089297 0.9604855180 0.9534955025 - 0.9620596170 0.9610437751 0.9541364908 - 0.9626103044 0.9616020918 0.9547774792 - 0.9631609917 0.9621605277 0.9554185271 - 0.9637116790 0.9627187848 0.9560595155 - 0.9642623067 0.9632771015 0.9567005038 - 0.9648129940 0.9638354182 0.9573414922 - 0.9653636813 0.9643937945 0.9579824805 - 0.9659144282 0.9649521112 0.9586235285 - 0.9664651155 0.9655104280 0.9592645168 - 0.9670158029 0.9660686851 0.9599055052 - 0.9675664902 0.9666271210 0.9605464935 - 0.9681171775 0.9671853781 0.9611874819 - 0.9686546922 0.9677324295 0.9618178010 - 0.9691889882 0.9682767987 0.9624454975 - 0.9697234035 0.9688212276 0.9630730748 - 0.9702578187 0.9693654776 0.9637007713 - 0.9707921147 0.9699099064 0.9643284082 - 0.9713264704 0.9704542160 0.9649561048 - 0.9718608856 0.9709985852 0.9655836821 - 0.9723951817 0.9715428948 0.9662113786 - 0.9729295969 0.9720873237 0.9668390155 - 0.9734640121 0.9726316929 0.9674667120 - 0.9739983082 0.9731760025 0.9680942893 - 0.9745327234 0.9737203717 0.9687219858 - 0.9750670791 0.9742646813 0.9693496227 - 0.9755846262 0.9747933149 0.9699637890 - 0.9760981202 0.9753180742 0.9705746174 - 0.9766116142 0.9758427739 0.9711853862 - 0.9771251082 0.9763675928 0.9717962742 - 0.9776384830 0.9768922925 0.9724071026 - 0.9781519771 0.9774171114 0.9730178714 - 0.9786654711 0.9779418111 0.9736286998 - 0.9791790247 0.9784665704 0.9742395878 - 0.9796923995 0.9789913297 0.9748504162 - 0.9802058935 0.9795160890 0.9754611850 - 0.9807193875 0.9800407887 0.9760720730 - 0.9812328815 0.9805656075 0.9766829014 - 0.9817463160 0.9810903072 0.9772937298 - 0.9822406173 0.9815968871 0.9778919816 - 0.9827232957 0.9820926189 0.9784827828 - 0.9832059741 0.9825881720 0.9790735841 - 0.9836887121 0.9830839038 0.9796643257 - 0.9841712713 0.9835795164 0.9802551270 - 0.9846540093 0.9840751886 0.9808458090 - 0.9851366878 0.9845708013 0.9814366102 - 0.9856194258 0.9850664735 0.9820274115 - 0.9861021042 0.9855620861 0.9826180935 - 0.9865847826 0.9860578179 0.9832088947 - 0.9870674014 0.9865533710 0.9837995768 - 0.9875500798 0.9870491028 0.9843903780 - 0.9880328178 0.9875447154 0.9849811792 - 0.9885063171 0.9880321026 0.9855628014 - 0.9889544845 0.9884964824 0.9861189723 - 0.9894027114 0.9889609218 0.9866752028 - 0.9898508787 0.9894253016 0.9872314930 - 0.9902991056 0.9898896813 0.9877877235 - 0.9907472730 0.9903541207 0.9883438945 - 0.9911954999 0.9908185005 0.9889001250 - 0.9916437268 0.9912828803 0.9894564152 - 0.9920918941 0.9917473197 0.9900125861 - 0.9925400019 0.9922116995 0.9905688167 - 0.9929882288 0.9926760793 0.9911249876 - 0.9934363961 0.9931405187 0.9916812778 - 0.9938846231 0.9936048985 0.9922375083 - 0.9943327904 0.9940692782 0.9927936792 - 0.9947488904 0.9945036173 0.9933192730 - 0.9951528907 0.9949263930 0.9938331842 - 0.9955567718 0.9953492284 0.9943470955 - 0.9959607124 0.9957720041 0.9948610067 - 0.9963645935 0.9961947799 0.9953749180 - 0.9967685938 0.9966176152 0.9958888292 - 0.9971724749 0.9970403910 0.9964026809 - 0.9975764155 0.9974632263 0.9969165921 - 0.9979804158 0.9978860021 0.9974305034 - 0.9983842969 0.9983087778 0.9979444146 - 0.9987881780 0.9987316132 0.9984583259 - 0.9991921186 0.9991543889 0.9989721775 - 0.9995961189 0.9995772243 0.9994860888 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0078889886 0.0072905929 0.0057835742 + 0.0155280400 0.0142424498 0.0110800304 + 0.0223259609 0.0204148795 0.0157516599 + 0.0282847900 0.0258345399 0.0199356508 + 0.0338022411 0.0308287591 0.0238022301 + 0.0387409404 0.0354198888 0.0274327602 + 0.0434868000 0.0397184007 0.0308620799 + 0.0479348600 0.0437957905 0.0341565795 + 0.0521249995 0.0477067716 0.0373548716 + 0.0560929887 0.0514680110 0.0404086895 + 0.0599000603 0.0551049784 0.0433653891 + 0.0636395887 0.0585507713 0.0462335311 + 0.0673073381 0.0619252101 0.0490648411 + 0.0708674565 0.0652377531 0.0518452115 + 0.0742941722 0.0684634522 0.0545356311 + 0.0776154101 0.0715824589 0.0571359284 + 0.0808518305 0.0746066868 0.0596979707 + 0.0840119570 0.0775315836 0.0622345991 + 0.0870610103 0.0803711265 0.0647289529 + 0.0900357366 0.0831752867 0.0671606734 + 0.0929580331 0.0859571993 0.0695598572 + 0.0958346874 0.0887065008 0.0719215125 + 0.0986543819 0.0914179012 0.0742511526 + 0.1014280990 0.0940862000 0.0765401274 + 0.1041439995 0.0967277288 0.0788079128 + 0.1068262011 0.0993322283 0.0810511932 + 0.1094703972 0.1018816978 0.0832685530 + 0.1121027023 0.1043870971 0.0854593366 + 0.1147300974 0.1068421006 0.0876216292 + 0.1173269004 0.1092239022 0.0897513777 + 0.1198854968 0.1115904003 0.0918506086 + 0.1224000975 0.1139374003 0.0939252079 + 0.1248695031 0.1162879989 0.0959772170 + 0.1273149997 0.1186162010 0.0980132222 + 0.1297475994 0.1209222972 0.1000507995 + 0.1321358979 0.1232006028 0.1020739973 + 0.1344795972 0.1254397035 0.1040984020 + 0.1368041933 0.1276575029 0.1060978994 + 0.1391019970 0.1298466027 0.1080757976 + 0.1413698047 0.1320064962 0.1100341976 + 0.1435928941 0.1341473013 0.1119635031 + 0.1457892954 0.1362611055 0.1138700023 + 0.1479541063 0.1383730024 0.1157647967 + 0.1501048058 0.1404493004 0.1176467985 + 0.1522224993 0.1425213069 0.1195039973 + 0.1543388963 0.1445824057 0.1213504001 + 0.1564362943 0.1466197073 0.1231838018 + 0.1585174948 0.1486462057 0.1250088066 + 0.1605857015 0.1506392062 0.1268348992 + 0.1626347005 0.1526177973 0.1286439002 + 0.1646862030 0.1546075940 0.1304429024 + 0.1667335927 0.1565793008 0.1322406977 + 0.1687652022 0.1585436016 0.1340295970 + 0.1707780957 0.1604935974 0.1357980967 + 0.1727759987 0.1624072939 0.1375523061 + 0.1747658998 0.1643151045 0.1393052936 + 0.1767490953 0.1662092954 0.1410588026 + 0.1787372977 0.1680783033 0.1427990049 + 0.1807080060 0.1699371934 0.1445256025 + 0.1826650947 0.1718004048 0.1462455988 + 0.1846058965 0.1736536026 0.1479606032 + 0.1865195930 0.1754879057 0.1496645957 + 0.1884177029 0.1773201972 0.1513437033 + 0.1903073043 0.1791377962 0.1530140042 + 0.1921796948 0.1809280068 0.1546792984 + 0.1940302998 0.1827086061 0.1563450992 + 0.1958549023 0.1844934970 0.1580031067 + 0.1976682991 0.1862594038 0.1596433967 + 0.1994695961 0.1880051941 0.1612734050 + 0.2012577057 0.1897480041 0.1628941000 + 0.2030259073 0.1914830059 0.1645098031 + 0.2047819048 0.1931872070 0.1661248058 + 0.2065176964 0.1948740035 0.1677244008 + 0.2082248926 0.1965582073 0.1693145037 + 0.2099235952 0.1982422024 0.1708988994 + 0.2116315067 0.1999278069 0.1724735051 + 0.2133346945 0.2015964985 0.1740391999 + 0.2150184065 0.2032589018 0.1755944937 + 0.2167019993 0.2049262971 0.1771457046 + 0.2183800042 0.2065898031 0.1786843985 + 0.2200523019 0.2082391977 0.1802140027 + 0.2217144072 0.2098809928 0.1817325056 + 0.2233763039 0.2115218937 0.1832381040 + 0.2250375003 0.2131509036 0.1847354025 + 0.2266878933 0.2147578001 0.1862273067 + 0.2283222973 0.2163494974 0.1877095997 + 0.2299540937 0.2179363966 0.1891891062 + 0.2315769047 0.2195288986 0.1906653941 + 0.2331923991 0.2211087048 0.1921359003 + 0.2347972989 0.2226725072 0.1936012954 + 0.2363962978 0.2242244929 0.1950621009 + 0.2379969060 0.2257725000 0.1965180039 + 0.2395913005 0.2273270935 0.1979725063 + 0.2411745936 0.2288744003 0.1994252056 + 0.2427513003 0.2304067016 0.2008765042 + 0.2443210930 0.2319256067 0.2023257017 + 0.2458810061 0.2334371954 0.2037705034 + 0.2474320978 0.2349530011 0.2052136958 + 0.2489770949 0.2364764959 0.2066580951 + 0.2505179048 0.2380003035 0.2081031054 + 0.2520565093 0.2395125926 0.2095465064 + 0.2535842061 0.2410165966 0.2109802067 + 0.2551011145 0.2425200939 0.2124101967 + 0.2566075027 0.2440229058 0.2138369977 + 0.2581019104 0.2455164939 0.2152577937 + 0.2595993876 0.2469965965 0.2166765928 + 0.2610979974 0.2484685034 0.2180949003 + 0.2625960112 0.2499337047 0.2195139974 + 0.2640933990 0.2513996959 0.2209234983 + 0.2655884027 0.2528609037 0.2223280966 + 0.2670783103 0.2543111145 0.2237278074 + 0.2685579062 0.2557500899 0.2251172066 + 0.2700324059 0.2571833134 0.2265006006 + 0.2715004086 0.2586081922 0.2278756052 + 0.2729619145 0.2600342035 0.2292470932 + 0.2744210064 0.2614606023 0.2306170017 + 0.2758783996 0.2628875077 0.2319861054 + 0.2773306072 0.2643063962 0.2333521992 + 0.2787795067 0.2657200098 0.2347161025 + 0.2802197933 0.2671194077 0.2360662967 + 0.2816554010 0.2685183883 0.2374131978 + 0.2830832005 0.2699165940 0.2387543023 + 0.2845051885 0.2713209987 0.2400884926 + 0.2859246135 0.2727270126 0.2414194047 + 0.2873364091 0.2741222978 0.2427445948 + 0.2887467146 0.2755121887 0.2440689951 + 0.2901543975 0.2768926919 0.2453920990 + 0.2915565968 0.2782715857 0.2467114031 + 0.2929557860 0.2796497047 0.2480283976 + 0.2943511903 0.2810314000 0.2493402064 + 0.2957446873 0.2824105918 0.2506507039 + 0.2971296012 0.2837719917 0.2519558966 + 0.2985079885 0.2851282954 0.2532579899 + 0.2998743057 0.2864750028 0.2545535862 + 0.3012337983 0.2878203988 0.2558461130 + 0.3025875092 0.2891646028 0.2571358085 + 0.3039326966 0.2905083001 0.2584241033 + 0.3052744865 0.2918517888 0.2597118020 + 0.3066090047 0.2931913137 0.2609947026 + 0.3079420924 0.2945300043 0.2622764111 + 0.3092617095 0.2958604097 0.2635529041 + 0.3105807900 0.2971901000 0.2648288012 + 0.3119010031 0.2985129058 0.2660990953 + 0.3132201135 0.2998352051 0.2673687041 + 0.3145304024 0.3011538088 0.2686296105 + 0.3158377111 0.3024721146 0.2698898911 + 0.3171265125 0.3037881851 0.2711451054 + 0.3184145093 0.3051016033 0.2723999918 + 0.3196972907 0.3063997030 0.2736532986 + 0.3209798932 0.3076966107 0.2749058902 + 0.3222613931 0.3089852929 0.2761533856 + 0.3235423863 0.3102742136 0.2774004936 + 0.3248170912 0.3115651011 0.2786423862 + 0.3260917962 0.3128559887 0.2798843086 + 0.3273518085 0.3141461015 0.2811206877 + 0.3286117911 0.3154362142 0.2823570967 + 0.3298718929 0.3167093098 0.2835853994 + 0.3311319947 0.3179805875 0.2848125100 + 0.3323861063 0.3192439079 0.2860355973 + 0.3336383998 0.3205048144 0.2872571945 + 0.3348825872 0.3217664957 0.2884761989 + 0.3361215889 0.3230285943 0.2896933854 + 0.3373571038 0.3242911100 0.2909066081 + 0.3385877907 0.3255540133 0.2921139002 + 0.3398196995 0.3268145919 0.2933216989 + 0.3410564065 0.3280658126 0.2945317924 + 0.3422931135 0.3293170929 0.2957417965 + 0.3435252011 0.3305566907 0.2969467938 + 0.3447571099 0.3317956924 0.2981514931 + 0.3459787965 0.3330340087 0.2993502021 + 0.3471955061 0.3342720866 0.3005459011 + 0.3484120071 0.3355073035 0.3017396927 + 0.3496277034 0.3367376029 0.3029299974 + 0.3508433104 0.3379676044 0.3041202128 + 0.3520529866 0.3391881883 0.3053084910 + 0.3532628119 0.3404087126 0.3064967096 + 0.3544704020 0.3416185081 0.3076831102 + 0.3556770086 0.3428226113 0.3088682890 + 0.3568829894 0.3440254033 0.3100539148 + 0.3580872118 0.3452241123 0.3112407029 + 0.3592914045 0.3464227021 0.3124274015 + 0.3604977131 0.3476234078 0.3136095107 + 0.3617044985 0.3488247097 0.3147901893 + 0.3629097939 0.3500243127 0.3159680068 + 0.3641118109 0.3512204885 0.3171392083 + 0.3653138876 0.3524166942 0.3183102906 + 0.3665078878 0.3536022007 0.3194763064 + 0.3677001894 0.3547854125 0.3206410110 + 0.3688907921 0.3559671938 0.3218050003 + 0.3700768948 0.3571453989 0.3229666948 + 0.3712630868 0.3583235145 0.3241283894 + 0.3724423051 0.3595018089 0.3252857924 + 0.3736186028 0.3606801927 0.3264412880 + 0.3747946024 0.3618564010 0.3275963068 + 0.3759672940 0.3630137146 0.3287459016 + 0.3771401048 0.3641709983 0.3298955858 + 0.3783135116 0.3653210104 0.3310425878 + 0.3794879019 0.3664624989 0.3321863115 + 0.3806622028 0.3676039875 0.3333300054 + 0.3818334043 0.3687413037 0.3344663978 + 0.3830037117 0.3698773980 0.3356004953 + 0.3841735125 0.3710137904 0.3367342949 + 0.3853363097 0.3721551001 0.3378620148 + 0.3864991069 0.3732962906 0.3389897943 + 0.3876604140 0.3744358122 0.3401161134 + 0.3888185024 0.3755710125 0.3412387073 + 0.3899765909 0.3767063022 0.3423613906 + 0.3911325037 0.3778327107 0.3434790969 + 0.3922863007 0.3789506853 0.3445914984 + 0.3934400976 0.3800686896 0.3457039893 + 0.3945881128 0.3811818957 0.3468174934 + 0.3957333863 0.3822929859 0.3479315042 + 0.3968786001 0.3834039867 0.3490453959 + 0.3980174065 0.3845176995 0.3501490057 + 0.3991548121 0.3856320083 0.3512499034 + 0.4002921879 0.3867461979 0.3523508012 + 0.4014247954 0.3878605962 0.3534472883 + 0.4025569856 0.3889749944 0.3545432985 + 0.4036892951 0.3900893927 0.3556393087 + 0.4048125148 0.3911888003 0.3567320108 + 0.4059354067 0.3922877014 0.3578243852 + 0.4070582986 0.3933865130 0.3589169085 + 0.4081676006 0.3944776952 0.3600068986 + 0.4092766047 0.3955687881 0.3610968888 + 0.4103856981 0.3966597915 0.3621867895 + 0.4114840925 0.3977529109 0.3632732928 + 0.4125820994 0.3988460898 0.3643594980 + 0.4136799872 0.3999392986 0.3654457033 + 0.4147689044 0.4010387957 0.3665322065 + 0.4158565998 0.4021390080 0.3676187098 + 0.4169442952 0.4032391906 0.3687050939 + 0.4180271924 0.4043292999 0.3697864115 + 0.4191086888 0.4054166079 0.3708660007 + 0.4201901853 0.4065037966 0.3719455004 + 0.4212687910 0.4075860977 0.3730236888 + 0.4223456979 0.4086655080 0.3741010129 + 0.4234224856 0.4097448885 0.3751783073 + 0.4244934916 0.4108251035 0.3762541115 + 0.4255565107 0.4119066000 0.3773277998 + 0.4266194999 0.4129880965 0.3784016073 + 0.4276801050 0.4140700102 0.3794747889 + 0.4287301898 0.4151534140 0.3805457950 + 0.4297803044 0.4162367880 0.3816167116 + 0.4308303893 0.4173201919 0.3826876879 + 0.4318735898 0.4183954000 0.3837552965 + 0.4329161942 0.4194698036 0.3848226070 + 0.4339587092 0.4205440879 0.3858897984 + 0.4349972010 0.4216128886 0.3869574070 + 0.4360328019 0.4226776063 0.3880251944 + 0.4370684922 0.4237422943 0.3890931010 + 0.4381031096 0.4248071015 0.3901597857 + 0.4391340017 0.4258719981 0.3912216127 + 0.4401648939 0.4269368947 0.3922834098 + 0.4411959052 0.4280017018 0.3933452070 + 0.4422194064 0.4290623963 0.3943997025 + 0.4432410896 0.4301218987 0.3954519928 + 0.4442627132 0.4311814904 0.3965043128 + 0.4452854097 0.4322386980 0.3975546062 + 0.4463103116 0.4332911074 0.3986003101 + 0.4473350942 0.4343433976 0.3996460140 + 0.4483599067 0.4353958070 0.4006916881 + 0.4493798018 0.4364373982 0.4017345905 + 0.4503985941 0.4374763966 0.4027766883 + 0.4514173865 0.4385155141 0.4038187861 + 0.4524323940 0.4395537078 0.4048602879 + 0.4534361959 0.4405891001 0.4059000015 + 0.4544399977 0.4416244924 0.4069396853 + 0.4554437995 0.4426599145 0.4079793990 + 0.4564459026 0.4436990917 0.4090223908 + 0.4574469030 0.4447405040 0.4100672901 + 0.4584479928 0.4457817972 0.4111123085 + 0.4594491124 0.4468230903 0.4121572077 + 0.4604510069 0.4478600025 0.4131881893 + 0.4614529908 0.4488969147 0.4142186940 + 0.4624550045 0.4499337971 0.4152492881 + 0.4634560049 0.4509662986 0.4162780941 + 0.4644544125 0.4519883096 0.4173018932 + 0.4654527009 0.4530102015 0.4183256924 + 0.4664511085 0.4540322125 0.4193494916 + 0.4674443901 0.4550513029 0.4203704000 + 0.4684332907 0.4560678899 0.4213885963 + 0.4694221020 0.4570845068 0.4224067032 + 0.4704109132 0.4581010938 0.4234248996 + 0.4714013934 0.4591206014 0.4244410992 + 0.4723925889 0.4601413012 0.4254566133 + 0.4733838141 0.4611620009 0.4264720976 + 0.4743750095 0.4621827006 0.4274876118 + 0.4753603935 0.4631980062 0.4285031855 + 0.4763449132 0.4642125070 0.4295187891 + 0.4773294032 0.4652270079 0.4305343926 + 0.4783138931 0.4662415087 0.4315499961 + 0.4792841971 0.4672405124 0.4325644970 + 0.4802536964 0.4682387114 0.4335789979 + 0.4812232852 0.4692369103 0.4345934987 + 0.4821928144 0.4702351093 0.4356079996 + 0.4831587970 0.4712263942 0.4366190135 + 0.4841246903 0.4722175002 0.4376297891 + 0.4850907028 0.4732086957 0.4386407137 + 0.4860565960 0.4741998911 0.4396516085 + 0.4870223999 0.4751932025 0.4406566024 + 0.4879881144 0.4761866033 0.4416612983 + 0.4889538884 0.4771799147 0.4426659942 + 0.4899196029 0.4781732857 0.4436706901 + 0.4908815026 0.4791612923 0.4446693063 + 0.4918429852 0.4801487923 0.4456669092 + 0.4928044081 0.4811362028 0.4466646016 + 0.4937658906 0.4821236134 0.4476622045 + 0.4947195053 0.4831021130 0.4486567080 + 0.4956707954 0.4840778112 0.4496499896 + 0.4966219962 0.4850535095 0.4506433010 + 0.4975732863 0.4860292077 0.4516366124 + 0.4985210896 0.4869993031 0.4526301026 + 0.4994663000 0.4879654050 0.4536238015 + 0.5004115105 0.4889315069 0.4546175897 + 0.5013567209 0.4898976088 0.4556112885 + 0.5022984147 0.4908634126 0.4566054046 + 0.5032333732 0.4918287992 0.4576002955 + 0.5041682720 0.4927941859 0.4585951865 + 0.5051032901 0.4937596023 0.4595901966 + 0.5060374737 0.4947249889 0.4605847895 + 0.5069562197 0.4956893921 0.4615682960 + 0.5078749061 0.4966539145 0.4625518024 + 0.5087934732 0.4976184070 0.4635351896 + 0.5097122192 0.4985828996 0.4645186961 + 0.5106272101 0.4995400012 0.4654968977 + 0.5115404725 0.5004938245 0.4664725959 + 0.5124536753 0.5014476180 0.4674482942 + 0.5133669972 0.5024012923 0.4684239030 + 0.5142818093 0.5033521056 0.4693973958 + 0.5152004957 0.5042945743 0.4703643918 + 0.5161191821 0.5052371025 0.4713312984 + 0.5170379281 0.5061795712 0.4722982943 + 0.5179566145 0.5071220994 0.4732652009 + 0.5188748837 0.5080627203 0.4742276073 + 0.5197929740 0.5090026855 0.4751887023 + 0.5207111835 0.5099428296 0.4761497974 + 0.5216293931 0.5108827949 0.4771108925 + 0.5225464106 0.5118234158 0.4780718088 + 0.5234599113 0.5127655268 0.4790318906 + 0.5243734121 0.5137075186 0.4799919128 + 0.5252867937 0.5146496296 0.4809519947 + 0.5262002945 0.5155916810 0.4819121063 + 0.5271133780 0.5165237784 0.4828720093 + 0.5280262232 0.5174506903 0.4838317931 + 0.5289388895 0.5183774829 0.4847916067 + 0.5298516750 0.5193043947 0.4857513011 + 0.5307645202 0.5202311873 0.4867111146 + 0.5316731930 0.5211473107 0.4876579940 + 0.5325819254 0.5220631957 0.4886044860 + 0.5334905982 0.5229790807 0.4895510077 + 0.5343992710 0.5238950253 0.4904974997 + 0.5353068709 0.5248103142 0.4914431870 + 0.5362116098 0.5257238150 0.4923861027 + 0.5371162891 0.5266373754 0.4933289886 + 0.5380210280 0.5275508761 0.4942719936 + 0.5389257073 0.5284643769 0.4952149093 + 0.5398265719 0.5293797851 0.4961544871 + 0.5407235026 0.5302973986 0.4970901012 + 0.5416203141 0.5312148929 0.4980256855 + 0.5425171256 0.5321325064 0.4989612997 + 0.5434139967 0.5330500007 0.4998969138 + 0.5443124175 0.5339617729 0.5008286834 + 0.5452117920 0.5348700881 0.5017580986 + 0.5461112261 0.5357784033 0.5026873946 + 0.5470106006 0.5366867185 0.5036168098 + 0.5479100943 0.5375949740 0.5045462251 + 0.5488089919 0.5384951830 0.5054730773 + 0.5497077107 0.5393918157 0.5063987970 + 0.5506064892 0.5402885079 0.5073245764 + 0.5515052080 0.5411852002 0.5082504153 + 0.5524039268 0.5420818925 0.5091760755 + 0.5533022285 0.5429762006 0.5100972056 + 0.5542001724 0.5438696742 0.5110161901 + 0.5550982952 0.5447632074 0.5119352937 + 0.5559964180 0.5456566811 0.5128542781 + 0.5568944216 0.5465502143 0.5137733221 + 0.5577896833 0.5474457145 0.5146881938 + 0.5586835146 0.5483422875 0.5156006813 + 0.5595772862 0.5492389202 0.5165132880 + 0.5604711175 0.5501356125 0.5174257755 + 0.5613648891 0.5510321856 0.5183383822 + 0.5622547269 0.5519235730 0.5192475915 + 0.5631409883 0.5528106093 0.5201537013 + 0.5640273094 0.5536975861 0.5210598111 + 0.5649135709 0.5545846224 0.5219659209 + 0.5657997727 0.5554715991 0.5228719711 + 0.5666835904 0.5563544035 0.5237771869 + 0.5675624013 0.5572292209 0.5246803164 + 0.5684412718 0.5581040978 0.5255835056 + 0.5693200827 0.5589789152 0.5264865756 + 0.5701988935 0.5598536730 0.5273898244 + 0.5710769296 0.5607281923 0.5282927155 + 0.5719450116 0.5615991950 0.5291919112 + 0.5728130937 0.5624700785 0.5300911069 + 0.5736811757 0.5633410215 0.5309904218 + 0.5745493174 0.5642119050 0.5318896174 + 0.5754173994 0.5650827885 0.5327888131 + 0.5762851238 0.5659552813 0.5336858034 + 0.5771527290 0.5668284297 0.5345818996 + 0.5780202746 0.5677015185 0.5354781151 + 0.5788878798 0.5685747266 0.5363742113 + 0.5797554851 0.5694478154 0.5372703075 + 0.5806217790 0.5703194737 0.5381652713 + 0.5814853907 0.5711885095 0.5390577912 + 0.5823488832 0.5720576048 0.5399503112 + 0.5832124949 0.5729265809 0.5408427119 + 0.5840761065 0.5737956762 0.5417351723 + 0.5849397182 0.5746647120 0.5426276922 + 0.5858016014 0.5755252838 0.5435200930 + 0.5866631269 0.5763844252 0.5444124937 + 0.5875247121 0.5772435069 0.5453047752 + 0.5883861780 0.5781025290 0.5461971760 + 0.5892478228 0.5789616108 0.5470895171 + 0.5901088119 0.5798196197 0.5479819179 + 0.5909687281 0.5806747079 0.5488741994 + 0.5918287039 0.5815299153 0.5497664213 + 0.5926886201 0.5823850036 0.5506587029 + 0.5935484767 0.5832400918 0.5515509844 + 0.5944083929 0.5840952992 0.5524433255 + 0.5952606797 0.5849506259 0.5533313751 + 0.5961092114 0.5858058929 0.5542173982 + 0.5969576836 0.5866612196 0.5551033020 + 0.5978062153 0.5875164866 0.5559893250 + 0.5986546874 0.5883718729 0.5568752289 + 0.5995032191 0.5892271996 0.5577610731 + 0.6003429294 0.5900741816 0.5586416721 + 0.6011822820 0.5909208059 0.5595219135 + 0.6020215750 0.5917673707 0.5604020953 + 0.6028609872 0.5926139951 0.5612822771 + 0.6037002802 0.5934606194 0.5621625185 + 0.6045382023 0.5943056941 0.5630425215 + 0.6053689718 0.5951434970 0.5639213920 + 0.6061998010 0.5959812999 0.5648003221 + 0.6070305705 0.5968191028 0.5656793118 + 0.6078615189 0.5976567864 0.5665581822 + 0.6086922884 0.5984945893 0.5674371123 + 0.6095213890 0.5993313789 0.5683159828 + 0.6103464961 0.6001660228 0.5691947937 + 0.6111717224 0.6010006070 0.5700736046 + 0.6119968891 0.6018350720 0.5709522963 + 0.6128221154 0.6026697159 0.5718311071 + 0.6136472821 0.6035043001 0.5727099180 + 0.6144716144 0.6043398976 0.5735872984 + 0.6152943969 0.6051772237 0.5744621754 + 0.6161171794 0.6060143709 0.5753371716 + 0.6169400215 0.6068516970 0.5762121081 + 0.6177628040 0.6076890230 0.5770869851 + 0.6185857058 0.6085262895 0.5779619813 + 0.6194078922 0.6093611121 0.5788385272 + 0.6202291846 0.6101909280 0.5797184110 + 0.6210504770 0.6110206842 0.5805981755 + 0.6218718290 0.6118505001 0.5814781189 + 0.6226930022 0.6126803756 0.5823580027 + 0.6235142946 0.6135101914 0.5832378268 + 0.6243357062 0.6143385172 0.5841162801 + 0.6251572967 0.6151615977 0.5849894285 + 0.6259788871 0.6159847975 0.5858625770 + 0.6268004775 0.6168079972 0.5867357254 + 0.6276221275 0.6176310778 0.5876088738 + 0.6284437180 0.6184542775 0.5884820223 + 0.6292651892 0.6192772985 0.5893549919 + 0.6300833821 0.6200963259 0.5902221799 + 0.6309016943 0.6209151745 0.5910894275 + 0.6317198873 0.6217342019 0.5919566154 + 0.6325381994 0.6225532293 0.5928236842 + 0.6333565116 0.6233721972 0.5936908722 + 0.6341747046 0.6241912246 0.5945581198 + 0.6349883080 0.6250126958 0.5954239964 + 0.6358004212 0.6258351207 0.5962893963 + 0.6366125941 0.6266574860 0.5971549153 + 0.6374247074 0.6274797916 0.5980203152 + 0.6382368207 0.6283022165 0.5988857746 + 0.6390489936 0.6291245222 0.5997511744 + 0.6398593783 0.6299480796 0.6006168723 + 0.6406673789 0.6307734847 0.6014829874 + 0.6414753199 0.6315988898 0.6023491025 + 0.6422833204 0.6324242949 0.6032152176 + 0.6430912018 0.6332497001 0.6040812731 + 0.6438992023 0.6340751052 0.6049473882 + 0.6447070837 0.6349005103 0.6058135033 + 0.6455112100 0.6357209086 0.6066793203 + 0.6463152170 0.6365411878 0.6075451970 + 0.6471192241 0.6373614073 0.6084110141 + 0.6479231119 0.6381816268 0.6092767715 + 0.6487271190 0.6390019059 0.6101427078 + 0.6495311260 0.6398221254 0.6110085249 + 0.6503347754 0.6406397820 0.6118733883 + 0.6511383057 0.6414539814 0.6127370000 + 0.6519417763 0.6422681808 0.6136006117 + 0.6527453065 0.6430823803 0.6144642234 + 0.6535487771 0.6438966990 0.6153277755 + 0.6543523073 0.6447108984 0.6161913872 + 0.6551557779 0.6455250978 0.6170549989 + 0.6559563279 0.6463404298 0.6179105043 + 0.6567561030 0.6471558809 0.6187639832 + 0.6575558782 0.6479713917 0.6196174026 + 0.6583557129 0.6487870216 0.6204708219 + 0.6591554880 0.6496024728 0.6213243008 + 0.6599553227 0.6504179835 0.6221777201 + 0.6607547998 0.6512334943 0.6230304241 + 0.6615508795 0.6520485878 0.6238759756 + 0.6623470187 0.6528638005 0.6247217059 + 0.6631432176 0.6536788940 0.6255673170 + 0.6639392972 0.6544939876 0.6264129281 + 0.6647353768 0.6553090811 0.6272584796 + 0.6655315161 0.6561242938 0.6281040907 + 0.6663259268 0.6569358706 0.6289486289 + 0.6671159267 0.6577392220 0.6297904253 + 0.6679059863 0.6585425735 0.6306322813 + 0.6686959863 0.6593459845 0.6314740777 + 0.6694861054 0.6601492763 0.6323158741 + 0.6702761054 0.6609526873 0.6331576705 + 0.6710662246 0.6617559791 0.6339995265 + 0.6718522906 0.6625568271 0.6348415017 + 0.6726325750 0.6633538008 0.6356834769 + 0.6734127998 0.6641507149 0.6365255713 + 0.6741930842 0.6649476886 0.6373677254 + 0.6749733090 0.6657446027 0.6382098198 + 0.6757534742 0.6665415764 0.6390517950 + 0.6765338182 0.6673386097 0.6398938894 + 0.6773107052 0.6681358814 0.6407364011 + 0.6780828834 0.6689336896 0.6415793896 + 0.6788551211 0.6697314978 0.6424224973 + 0.6796272993 0.6705293059 0.6432656050 + 0.6803994775 0.6713271141 0.6441085935 + 0.6811717153 0.6721249223 0.6449517012 + 0.6819438934 0.6729226708 0.6457946897 + 0.6827138066 0.6737201810 0.6466346979 + 0.6834788918 0.6745169163 0.6474680901 + 0.6842439771 0.6753137112 0.6483014226 + 0.6850091219 0.6761105061 0.6491346955 + 0.6857742071 0.6769071817 0.6499680281 + 0.6865392923 0.6777039766 0.6508014202 + 0.6873043776 0.6785007715 0.6516346931 + 0.6880689263 0.6792957187 0.6524670720 + 0.6888297200 0.6800801754 0.6532937884 + 0.6895905137 0.6808646917 0.6541203856 + 0.6903513074 0.6816490889 0.6549469829 + 0.6911121011 0.6824336052 0.6557736993 + 0.6918728948 0.6832181215 0.6566002965 + 0.6926336884 0.6840025187 0.6574270129 + 0.6933944821 0.6847869754 0.6582536101 + 0.6941537261 0.6855652928 0.6590738297 + 0.6949126720 0.6863427758 0.6598929167 + 0.6956717968 0.6871201992 0.6607121229 + 0.6964308023 0.6878976226 0.6615312099 + 0.6971898079 0.6886749864 0.6623502970 + 0.6979488134 0.6894524097 0.6631695032 + 0.6987078190 0.6902298927 0.6639885902 + 0.6994645000 0.6910073161 0.6648052931 + 0.7002186179 0.6917846799 0.6656193733 + 0.7009727955 0.6925622225 0.6664335132 + 0.7017269731 0.6933395863 0.6672475934 + 0.7024810910 0.6941171288 0.6680616140 + 0.7032353282 0.6948944926 0.6688756943 + 0.7039895058 0.6956719756 0.6696897745 + 0.7047435045 0.6964492798 0.6705039144 + 0.7054880261 0.6972190738 0.6713137031 + 0.7062326074 0.6979889870 0.6721233726 + 0.7069771290 0.6987587810 0.6729332209 + 0.7077215910 0.6995285749 0.6737430096 + 0.7084661722 0.7002984881 0.6745527983 + 0.7092106938 0.7010682821 0.6753625274 + 0.7099552751 0.7018380761 0.6761723161 + 0.7106971145 0.7026013136 0.6769785881 + 0.7114353776 0.7033559084 0.6777802110 + 0.7121738195 0.7041105032 0.6785817742 + 0.7129122019 0.7048650980 0.6793833971 + 0.7136505246 0.7056195736 0.6801850200 + 0.7143889070 0.7063742280 0.6809865236 + 0.7151272893 0.7071288228 0.6817880869 + 0.7158656120 0.7078834176 0.6825897098 + 0.7165970206 0.7086318135 0.6833915710 + 0.7173261046 0.7093781829 0.6841934919 + 0.7180553079 0.7101244926 0.6849954128 + 0.7187843919 0.7108709216 0.6857973933 + 0.7195134759 0.7116172910 0.6865993142 + 0.7202426791 0.7123637199 0.6874011755 + 0.7209718227 0.7131100893 0.6882030964 + 0.7217009068 0.7138565183 0.6890050769 + 0.7224162817 0.7146016955 0.6897974014 + 0.7231312990 0.7153468132 0.6905894279 + 0.7238463163 0.7160919905 0.6913813949 + 0.7245612144 0.7168372273 0.6921733022 + 0.7252761722 0.7175822854 0.6929653287 + 0.7259911895 0.7183275223 0.6937571764 + 0.7267062068 0.7190726995 0.6945492029 + 0.7274206877 0.7198178172 0.6953408718 + 0.7281301022 0.7205628157 0.6961287260 + 0.7288395166 0.7213078141 0.6969165802 + 0.7295488715 0.7220528126 0.6977043748 + 0.7302582860 0.7227978110 0.6984922886 + 0.7309677005 0.7235428095 0.6992802024 + 0.7316771150 0.7242878079 0.7000679970 + 0.7323865294 0.7250328064 0.7008559108 + 0.7330961823 0.7257779241 0.7016434073 + 0.7338085175 0.7265239954 0.7024263740 + 0.7345207930 0.7272701263 0.7032094002 + 0.7352331281 0.7280163169 0.7039923072 + 0.7359454036 0.7287623882 0.7047752738 + 0.7366576791 0.7295085192 0.7055583000 + 0.7373700142 0.7302545905 0.7063413262 + 0.7380822897 0.7310007215 0.7071242929 + 0.7387946248 0.7317467928 0.7079071999 + 0.7395036817 0.7324852943 0.7086831927 + 0.7402127981 0.7332236171 0.7094588876 + 0.7409217954 0.7339618206 0.7102347016 + 0.7416307926 0.7347000837 0.7110103965 + 0.7423397899 0.7354382873 0.7117860913 + 0.7430489063 0.7361766100 0.7125617862 + 0.7437579036 0.7369148731 0.7133374810 + 0.7444669008 0.7376530766 0.7141131759 + 0.7451686263 0.7383893132 0.7148858905 + 0.7458680272 0.7391248941 0.7156574726 + 0.7465673089 0.7398604155 0.7164289951 + 0.7472665906 0.7405959964 0.7172005773 + 0.7479659915 0.7413315177 0.7179722190 + 0.7486652732 0.7420669794 0.7187438011 + 0.7493646145 0.7428026199 0.7195153832 + 0.7500640154 0.7435380816 0.7202870250 + 0.7507627010 0.7442746162 0.7210568786 + 0.7514607906 0.7450124025 0.7218245268 + 0.7521588206 0.7457501888 0.7225919962 + 0.7528567910 0.7464879751 0.7233595252 + 0.7535548210 0.7472257018 0.7241271138 + 0.7542527914 0.7479634881 0.7248945832 + 0.7549508214 0.7487012744 0.7256621718 + 0.7556489110 0.7494391203 0.7264297009 + 0.7563468814 0.7501769066 0.7271971703 + 0.7570462227 0.7509121895 0.7279605269 + 0.7577455044 0.7516474724 0.7287237048 + 0.7584449053 0.7523828149 0.7294868827 + 0.7591441870 0.7531180978 0.7302501202 + 0.7598435879 0.7538533807 0.7310132980 + 0.7605429292 0.7545887232 0.7317764163 + 0.7612422109 0.7553240061 0.7325395942 + 0.7619416118 0.7560592890 0.7333027720 + 0.7626401782 0.7567914128 0.7340652943 + 0.7633376718 0.7575193048 0.7348269820 + 0.7640352249 0.7582470775 0.7355887294 + 0.7647327781 0.7589750290 0.7363502979 + 0.7654302716 0.7597029209 0.7371119857 + 0.7661278844 0.7604308128 0.7378736138 + 0.7668253779 0.7611585855 0.7386353016 + 0.7675229907 0.7618864775 0.7393969297 + 0.7682204843 0.7626144290 0.7401586175 + 0.7689135075 0.7633404136 0.7409185767 + 0.7696049809 0.7640656829 0.7416778803 + 0.7702965140 0.7647910714 0.7424373031 + 0.7709881067 0.7655165195 0.7431966066 + 0.7716795802 0.7662419081 0.7439560294 + 0.7723711133 0.7669671774 0.7447152734 + 0.7730625868 0.7676926255 0.7454746962 + 0.7737541199 0.7684180140 0.7462341189 + 0.7744455934 0.7691434026 0.7469934225 + 0.7751392722 0.7698696852 0.7477499843 + 0.7758330107 0.7705960274 0.7485064268 + 0.7765266895 0.7713223100 0.7492629290 + 0.7772204280 0.7720487118 0.7500193119 + 0.7779141068 0.7727749944 0.7507756948 + 0.7786077857 0.7735012770 0.7515321970 + 0.7793015242 0.7742276788 0.7522885799 + 0.7799953222 0.7749540210 0.7530450225 + 0.7806888223 0.7756797075 0.7538012266 + 0.7813785076 0.7763937712 0.7545518279 + 0.7820681930 0.7771078944 0.7553023100 + 0.7827579975 0.7778220177 0.7560529113 + 0.7834476829 0.7785360217 0.7568033934 + 0.7841374874 0.7792500854 0.7575538754 + 0.7848271728 0.7799642086 0.7583044767 + 0.7855169773 0.7806782722 0.7590550184 + 0.7862067223 0.7813923955 0.7598056197 + 0.7868962884 0.7821062803 0.7605561018 + 0.7875761986 0.7828112841 0.7613086104 + 0.7882561088 0.7835162878 0.7620609999 + 0.7889360785 0.7842212915 0.7628135085 + 0.7896159887 0.7849262953 0.7635658979 + 0.7902958989 0.7856312990 0.7643182874 + 0.7909759283 0.7863363028 0.7650707960 + 0.7916557789 0.7870413065 0.7658231854 + 0.7923356891 0.7877463102 0.7665756941 + 0.7930157185 0.7884513736 0.7673280835 + 0.7936940193 0.7891541123 0.7680817842 + 0.7943720818 0.7898566127 0.7688357830 + 0.7950502038 0.7905589938 0.7695897222 + 0.7957283258 0.7912613750 0.7703436017 + 0.7964063883 0.7919638157 0.7710976005 + 0.7970845103 0.7926663160 0.7718514800 + 0.7977625728 0.7933686972 0.7726054788 + 0.7984406948 0.7940710783 0.7733594179 + 0.7991188169 0.7947735190 0.7741132975 + 0.7997971773 0.7954763770 0.7748653293 + 0.8004758954 0.7961794734 0.7756156921 + 0.8011544943 0.7968826890 0.7763661146 + 0.8018332124 0.7975857854 0.7771165967 + 0.8025118113 0.7982890010 0.7778670192 + 0.8031905293 0.7989920974 0.7786175013 + 0.8038691282 0.7996953130 0.7793679237 + 0.8045477867 0.8003984094 0.7801182866 + 0.8052263856 0.8011015058 0.7808688283 + 0.8059049249 0.8018025160 0.7816187143 + 0.8065828085 0.8024935126 0.7823663950 + 0.8072608113 0.8031843901 0.7831140757 + 0.8079386950 0.8038753867 0.7838618159 + 0.8086165786 0.8045663238 0.7846094966 + 0.8092945218 0.8052573204 0.7853571773 + 0.8099725246 0.8059481978 0.7861049175 + 0.8106504083 0.8066391945 0.7868525982 + 0.8113282919 0.8073300719 0.7876002789 + 0.8120061755 0.8080211282 0.7883480191 + 0.8126829267 0.8087062240 0.7890954018 + 0.8133587837 0.8093882203 0.7898424864 + 0.8140347004 0.8100702167 0.7905896902 + 0.8147106767 0.8107520938 0.7913368940 + 0.8153865933 0.8114340901 0.7920840979 + 0.8160626292 0.8121160865 0.7928311825 + 0.8167384863 0.8127980828 0.7935783863 + 0.8174144030 0.8134800196 0.7943255901 + 0.8180903792 0.8141620159 0.7950726748 + 0.8187662959 0.8148440123 0.7958198786 + 0.8194429874 0.8155233860 0.7965657711 + 0.8201196194 0.8162028193 0.7973116040 + 0.8207963109 0.8168823123 0.7980574965 + 0.8214728832 0.8175616860 0.7988033891 + 0.8221495748 0.8182411194 0.7995492220 + 0.8228263259 0.8189204931 0.8002951145 + 0.8235028982 0.8195999265 0.8010408878 + 0.8241795897 0.8202794194 0.8017867804 + 0.8248562217 0.8209587932 0.8025326133 + 0.8255324960 0.8216388822 0.8032788038 + 0.8262073994 0.8223220706 0.8040260077 + 0.8268823028 0.8230053782 0.8047732711 + 0.8275572062 0.8236886263 0.8055204749 + 0.8282319903 0.8243718743 0.8062677979 + 0.8289068937 0.8250551224 0.8070150018 + 0.8295817971 0.8257383108 0.8077623248 + 0.8302565813 0.8264216185 0.8085095286 + 0.8309314847 0.8271048069 0.8092567921 + 0.8316063881 0.8277881145 0.8100039959 + 0.8322811723 0.8284710050 0.8107491732 + 0.8329560161 0.8291531205 0.8114882708 + 0.8336306810 0.8298351169 0.8122274280 + 0.8343054056 0.8305171728 0.8129665256 + 0.8349801898 0.8311992884 0.8137056231 + 0.8356549144 0.8318814039 0.8144447207 + 0.8363295794 0.8325635195 0.8151838183 + 0.8370043039 0.8332455754 0.8159229159 + 0.8376790881 0.8339275718 0.8166620135 + 0.8383538127 0.8346096873 0.8174011111 + 0.8390284777 0.8352907896 0.8181397915 + 0.8397032022 0.8359678984 0.8188765049 + 0.8403778076 0.8366451263 0.8196132779 + 0.8410524726 0.8373221755 0.8203501105 + 0.8417270780 0.8379992843 0.8210868835 + 0.8424018025 0.8386765122 0.8218235970 + 0.8430765271 0.8393536210 0.8225604296 + 0.8437510729 0.8400307298 0.8232972026 + 0.8444257975 0.8407077789 0.8240339756 + 0.8451004028 0.8413850069 0.8247706890 + 0.8457751274 0.8420621157 0.8255075216 + 0.8464422822 0.8427348137 0.8262410164 + 0.8471094966 0.8434073925 0.8269745111 + 0.8477767110 0.8440800905 0.8277080059 + 0.8484439254 0.8447527289 0.8284415007 + 0.8491110802 0.8454254270 0.8291749954 + 0.8497782946 0.8460981250 0.8299083710 + 0.8504455090 0.8467707038 0.8306419253 + 0.8511127234 0.8474434018 0.8313754201 + 0.8517798185 0.8481160998 0.8321089149 + 0.8524469733 0.8487886786 0.8328424096 + 0.8531115055 0.8494626284 0.8335720897 + 0.8537746072 0.8501369953 0.8342999220 + 0.8544377089 0.8508114219 0.8350275755 + 0.8551008105 0.8514857888 0.8357554078 + 0.8557639122 0.8521602154 0.8364831209 + 0.8564270735 0.8528345823 0.8372108936 + 0.8570901752 0.8535091281 0.8379387259 + 0.8577532768 0.8541834950 0.8386663795 + 0.8584163785 0.8548579216 0.8393942118 + 0.8590794802 0.8555322886 0.8401219249 + 0.8597424030 0.8562076092 0.8408495188 + 0.8604043722 0.8568869233 0.8415760994 + 0.8610665202 0.8575661778 0.8423027992 + 0.8617284894 0.8582454920 0.8430293798 + 0.8623905182 0.8589248061 0.8437560797 + 0.8630526066 0.8596040010 0.8444827199 + 0.8637145758 0.8602833152 0.8452093005 + 0.8643766046 0.8609626293 0.8459360003 + 0.8650386930 0.8616418839 0.8466625810 + 0.8657007217 0.8623211980 0.8473892212 + 0.8663626909 0.8630003929 0.8481159210 + 0.8670225143 0.8636755943 0.8488407731 + 0.8676806092 0.8643475175 0.8495643139 + 0.8683385849 0.8650195003 0.8502879143 + 0.8689966798 0.8656914234 0.8510113955 + 0.8696547747 0.8663634062 0.8517348766 + 0.8703128099 0.8670353889 0.8524584770 + 0.8709709048 0.8677073121 0.8531820178 + 0.8716288805 0.8683792949 0.8539056182 + 0.8722869754 0.8690512180 0.8546290994 + 0.8729450703 0.8697232008 0.8553525805 + 0.8736031055 0.8703951836 0.8560761809 + 0.8742563128 0.8710637093 0.8567957282 + 0.8749082088 0.8717314005 0.8575140834 + 0.8755602241 0.8723990917 0.8582326174 + 0.8762121201 0.8730667830 0.8589509726 + 0.8768640161 0.8737344742 0.8596695065 + 0.8775159717 0.8744022846 0.8603879809 + 0.8781679273 0.8750699759 0.8611063957 + 0.8788198829 0.8757377267 0.8618249297 + 0.8794717789 0.8764054179 0.8625432849 + 0.8801236749 0.8770731091 0.8632618189 + 0.8807756901 0.8777408004 0.8639801741 + 0.8814250231 0.8784101009 0.8646934032 + 0.8820741177 0.8790795803 0.8654057980 + 0.8827230930 0.8797491193 0.8661181927 + 0.8833721280 0.8804187179 0.8668307066 + 0.8840212226 0.8810881972 0.8675431013 + 0.8846701980 0.8817576766 0.8682554960 + 0.8853191733 0.8824272156 0.8689680099 + 0.8859683275 0.8830966949 0.8696804047 + 0.8866173029 0.8837661743 0.8703927994 + 0.8872662783 0.8844357133 0.8711053133 + 0.8879153728 0.8851051927 0.8718177080 + 0.8885645270 0.8857750893 0.8725255728 + 0.8892136812 0.8864449859 0.8732324839 + 0.8898627758 0.8871150017 0.8739395142 + 0.8905119896 0.8877848983 0.8746464252 + 0.8911612034 0.8884547949 0.8753533959 + 0.8918102980 0.8891248107 0.8760603070 + 0.8924595118 0.8897947073 0.8767672777 + 0.8931086063 0.8904647231 0.8774741888 + 0.8937578201 0.8911346197 0.8781810999 + 0.8944069743 0.8918045163 0.8788880706 + 0.8950561285 0.8924744725 0.8795949817 + 0.8957039118 0.8931388259 0.8802983165 + 0.8963508010 0.8937997222 0.8809992075 + 0.8969975710 0.8944606185 0.8817002177 + 0.8976445198 0.8951215148 0.8824012280 + 0.8982914090 0.8957824111 0.8831021786 + 0.8989382982 0.8964433074 0.8838031888 + 0.8995851278 0.8971043229 0.8845040798 + 0.9002320170 0.8977652192 0.8852050900 + 0.9008789062 0.8984261155 0.8859061003 + 0.9015257955 0.8990870118 0.8866071105 + 0.9021726847 0.8997479081 0.8873080015 + 0.9028183222 0.9004076123 0.8880085945 + 0.9034605026 0.9010636806 0.8887078166 + 0.9041026831 0.9017199278 0.8894069791 + 0.9047449231 0.9023761153 0.8901063204 + 0.9053872228 0.9030323029 0.8908054829 + 0.9060294032 0.9036884904 0.8915048242 + 0.9066715837 0.9043446183 0.8922039866 + 0.9073138237 0.9050008059 0.8929033279 + 0.9079560041 0.9056569934 0.8936024904 + 0.9085981846 0.9063131809 0.8943017721 + 0.9092404246 0.9069694281 0.8950009942 + 0.9098826051 0.9076254964 0.8957002759 + 0.9105213881 0.9082816243 0.8964002132 + 0.9111589789 0.9089375138 0.8971003890 + 0.9117966294 0.9095935225 0.8978006244 + 0.9124342203 0.9102494717 0.8985008001 + 0.9130718112 0.9109054804 0.8992009759 + 0.9137094021 0.9115614891 0.8999012709 + 0.9143469930 0.9122173786 0.9006015062 + 0.9149845839 0.9128733873 0.9013016820 + 0.9156221747 0.9135293961 0.9020019174 + 0.9162598252 0.9141854048 0.9027020931 + 0.9168974161 0.9148412943 0.9034023285 + 0.9175344110 0.9154965281 0.9041017890 + 0.9181634784 0.9161416292 0.9047920108 + 0.9187926054 0.9167866707 0.9054822922 + 0.9194216728 0.9174317718 0.9061725140 + 0.9200507998 0.9180769920 0.9068626761 + 0.9206799865 0.9187220931 0.9075530171 + 0.9213091135 0.9193671942 0.9082431793 + 0.9219381809 0.9200122952 0.9089334011 + 0.9225673079 0.9206575155 0.9096236825 + 0.9231963754 0.9213026166 0.9103139043 + 0.9238255024 0.9219477177 0.9110041857 + 0.9244546294 0.9225928187 0.9116944075 + 0.9250795841 0.9232333899 0.9123820066 + 0.9256916046 0.9238601923 0.9130614996 + 0.9263036847 0.9244869947 0.9137409925 + 0.9269157052 0.9251136780 0.9144204855 + 0.9275277853 0.9257404804 0.9150999784 + 0.9281398058 0.9263672233 0.9157794714 + 0.9287518263 0.9269940257 0.9164590836 + 0.9293639064 0.9276207089 0.9171385765 + 0.9299759269 0.9282475114 0.9178181291 + 0.9305880070 0.9288743138 0.9184976220 + 0.9312000275 0.9295009971 0.9191771150 + 0.9318121076 0.9301277995 0.9198566079 + 0.9324201941 0.9307510853 0.9205334187 + 0.9330167174 0.9313644767 0.9212021232 + 0.9336131811 0.9319778085 0.9218708873 + 0.9342097044 0.9325911999 0.9225397110 + 0.9348061085 0.9332044721 0.9232084155 + 0.9354025722 0.9338178039 0.9238771796 + 0.9359990954 0.9344311953 0.9245458841 + 0.9365956187 0.9350445271 0.9252147079 + 0.9371920824 0.9356579185 0.9258834720 + 0.9377886057 0.9362711906 0.9265521765 + 0.9383851290 0.9368845820 0.9272210002 + 0.9389814734 0.9374979138 0.9278897047 + 0.9395771027 0.9381104708 0.9285578728 + 0.9401646256 0.9387161732 0.9292200804 + 0.9407520294 0.9393218756 0.9298822880 + 0.9413393736 0.9399275780 0.9305444956 + 0.9419268966 0.9405332804 0.9312068224 + 0.9425143003 0.9411389232 0.9318689704 + 0.9431017041 0.9417446256 0.9325311780 + 0.9436892271 0.9423503280 0.9331933856 + 0.9442765713 0.9429559708 0.9338555932 + 0.9448639750 0.9435616732 0.9345179200 + 0.9454514980 0.9441673756 0.9351801276 + 0.9460389018 0.9447730780 0.9358422756 + 0.9466264248 0.9453787804 0.9365044832 + 0.9472072124 0.9459720850 0.9371623993 + 0.9477860928 0.9465619922 0.9378191233 + 0.9483650923 0.9471517801 0.9384757876 + 0.9489439726 0.9477416277 0.9391323924 + 0.9495229721 0.9483314753 0.9397891164 + 0.9501019120 0.9489213228 0.9404457211 + 0.9506809115 0.9495111704 0.9411023855 + 0.9512599111 0.9501010180 0.9417591095 + 0.9518387914 0.9506909251 0.9424157143 + 0.9524177909 0.9512807131 0.9430723786 + 0.9529966712 0.9518705010 0.9437291026 + 0.9535756707 0.9524604082 0.9443857074 + 0.9541510940 0.9530453086 0.9450407028 + 0.9547178149 0.9536182880 0.9456915855 + 0.9552845955 0.9541913867 0.9463424087 + 0.9558513761 0.9547644258 0.9469932914 + 0.9564182162 0.9553375244 0.9476441145 + 0.9569848776 0.9559105039 0.9482949972 + 0.9575517178 0.9564834833 0.9489458203 + 0.9581184983 0.9570565820 0.9495967031 + 0.9586852789 0.9576296210 0.9502475262 + 0.9592519999 0.9582027197 0.9508984089 + 0.9598187804 0.9587756991 0.9515491724 + 0.9603856206 0.9593486786 0.9522001147 + 0.9609522820 0.9599217772 0.9528508782 + 0.9615089297 0.9604855180 0.9534955025 + 0.9620596170 0.9610437751 0.9541364908 + 0.9626103044 0.9616020918 0.9547774792 + 0.9631609917 0.9621605277 0.9554185271 + 0.9637116790 0.9627187848 0.9560595155 + 0.9642623067 0.9632771015 0.9567005038 + 0.9648129940 0.9638354182 0.9573414922 + 0.9653636813 0.9643937945 0.9579824805 + 0.9659144282 0.9649521112 0.9586235285 + 0.9664651155 0.9655104280 0.9592645168 + 0.9670158029 0.9660686851 0.9599055052 + 0.9675664902 0.9666271210 0.9605464935 + 0.9681171775 0.9671853781 0.9611874819 + 0.9686546922 0.9677324295 0.9618178010 + 0.9691889882 0.9682767987 0.9624454975 + 0.9697234035 0.9688212276 0.9630730748 + 0.9702578187 0.9693654776 0.9637007713 + 0.9707921147 0.9699099064 0.9643284082 + 0.9713264704 0.9704542160 0.9649561048 + 0.9718608856 0.9709985852 0.9655836821 + 0.9723951817 0.9715428948 0.9662113786 + 0.9729295969 0.9720873237 0.9668390155 + 0.9734640121 0.9726316929 0.9674667120 + 0.9739983082 0.9731760025 0.9680942893 + 0.9745327234 0.9737203717 0.9687219858 + 0.9750670791 0.9742646813 0.9693496227 + 0.9755846262 0.9747933149 0.9699637890 + 0.9760981202 0.9753180742 0.9705746174 + 0.9766116142 0.9758427739 0.9711853862 + 0.9771251082 0.9763675928 0.9717962742 + 0.9776384830 0.9768922925 0.9724071026 + 0.9781519771 0.9774171114 0.9730178714 + 0.9786654711 0.9779418111 0.9736286998 + 0.9791790247 0.9784665704 0.9742395878 + 0.9796923995 0.9789913297 0.9748504162 + 0.9802058935 0.9795160890 0.9754611850 + 0.9807193875 0.9800407887 0.9760720730 + 0.9812328815 0.9805656075 0.9766829014 + 0.9817463160 0.9810903072 0.9772937298 + 0.9822406173 0.9815968871 0.9778919816 + 0.9827232957 0.9820926189 0.9784827828 + 0.9832059741 0.9825881720 0.9790735841 + 0.9836887121 0.9830839038 0.9796643257 + 0.9841712713 0.9835795164 0.9802551270 + 0.9846540093 0.9840751886 0.9808458090 + 0.9851366878 0.9845708013 0.9814366102 + 0.9856194258 0.9850664735 0.9820274115 + 0.9861021042 0.9855620861 0.9826180935 + 0.9865847826 0.9860578179 0.9832088947 + 0.9870674014 0.9865533710 0.9837995768 + 0.9875500798 0.9870491028 0.9843903780 + 0.9880328178 0.9875447154 0.9849811792 + 0.9885063171 0.9880321026 0.9855628014 + 0.9889544845 0.9884964824 0.9861189723 + 0.9894027114 0.9889609218 0.9866752028 + 0.9898508787 0.9894253016 0.9872314930 + 0.9902991056 0.9898896813 0.9877877235 + 0.9907472730 0.9903541207 0.9883438945 + 0.9911954999 0.9908185005 0.9889001250 + 0.9916437268 0.9912828803 0.9894564152 + 0.9920918941 0.9917473197 0.9900125861 + 0.9925400019 0.9922116995 0.9905688167 + 0.9929882288 0.9926760793 0.9911249876 + 0.9934363961 0.9931405187 0.9916812778 + 0.9938846231 0.9936048985 0.9922375083 + 0.9943327904 0.9940692782 0.9927936792 + 0.9947488904 0.9945036173 0.9933192730 + 0.9951528907 0.9949263930 0.9938331842 + 0.9955567718 0.9953492284 0.9943470955 + 0.9959607124 0.9957720041 0.9948610067 + 0.9963645935 0.9961947799 0.9953749180 + 0.9967685938 0.9966176152 0.9958888292 + 0.9971724749 0.9970403910 0.9964026809 + 0.9975764155 0.9974632263 0.9969165921 + 0.9979804158 0.9978860021 0.9974305034 + 0.9983842969 0.9983087778 0.9979444146 + 0.9987881780 0.9987316132 0.9984583259 + 0.9991921186 0.9991543889 0.9989721775 + 0.9995961189 0.9995772243 0.9994860888 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_160nc.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_160nc.spi1d index e61ba0ee..d0de9fae 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_160nc.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_160nc.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0104342103 0.0097368704 0.0081004426 - 0.0202932805 0.0187927708 0.0152669698 - 0.0287755504 0.0266450606 0.0213894807 - 0.0361699387 0.0334015600 0.0267654601 - 0.0427807011 0.0394648015 0.0316598192 - 0.0487997085 0.0450807586 0.0361505710 - 0.0544521399 0.0502359308 0.0404325016 - 0.0596807487 0.0550900213 0.0445077084 - 0.0646303892 0.0597528182 0.0484247990 - 0.0693302229 0.0641593412 0.0521505699 - 0.0737845525 0.0683351308 0.0556968004 - 0.0780915692 0.0723901764 0.0591261685 - 0.0822694898 0.0763313472 0.0624415018 - 0.0863260701 0.0801697522 0.0657054484 - 0.0902359709 0.0839016736 0.0689003170 - 0.0940105170 0.0874804035 0.0719991401 - 0.0976487026 0.0909066126 0.0750158876 - 0.1011712030 0.0942689478 0.0779601783 - 0.1046311036 0.0975821987 0.0808535069 - 0.1080254018 0.1008832976 0.0837027133 - 0.1113561988 0.1041185036 0.0864896476 - 0.1146244034 0.1072700024 0.0892440900 - 0.1178375036 0.1103363037 0.0919481963 - 0.1209478006 0.1133145019 0.0945950523 - 0.1239672974 0.1161829010 0.0971959010 - 0.1269288063 0.1190017015 0.0997639596 - 0.1298519969 0.1217684969 0.1022854969 - 0.1327518970 0.1245191991 0.1047450975 - 0.1356167048 0.1272488981 0.1071709991 - 0.1384285986 0.1299587935 0.1095672026 - 0.1411979049 0.1326563060 0.1119202971 - 0.1438945979 0.1353050023 0.1142712981 - 0.1465526074 0.1378984004 0.1166002005 - 0.1491758972 0.1404460073 0.1189038008 - 0.1517738998 0.1429633051 0.1211721972 - 0.1543568969 0.1454481930 0.1234188005 - 0.1569266021 0.1478973031 0.1256408989 - 0.1594693065 0.1503026038 0.1278519034 - 0.1619731039 0.1526698023 0.1300269067 - 0.1644496024 0.1550289989 0.1322003007 - 0.1668978930 0.1573766023 0.1343421042 - 0.1693100929 0.1597099006 0.1364780068 - 0.1717022955 0.1620142013 0.1385955065 - 0.1740535051 0.1643050015 0.1406960934 - 0.1763895005 0.1665803045 0.1427800953 - 0.1786942929 0.1688499004 0.1448349953 - 0.1809858978 0.1710920930 0.1468731016 - 0.1832552999 0.1733157039 0.1488973945 - 0.1855040044 0.1754987985 0.1508941948 - 0.1877363026 0.1776573956 0.1528868973 - 0.1899455935 0.1797675043 0.1548427939 - 0.1921364963 0.1818484068 0.1567813009 - 0.1943152994 0.1839254946 0.1587089002 - 0.1964662969 0.1859872043 0.1605975032 - 0.1986126006 0.1880321950 0.1624716967 - 0.2007299960 0.1900521070 0.1643451005 - 0.2028377950 0.1920509040 0.1662126929 - 0.2049141973 0.1940355003 0.1680644006 - 0.2069772035 0.1960064024 0.1699177027 - 0.2090151012 0.1979558021 0.1717617065 - 0.2110265940 0.1998984069 0.1735890955 - 0.2130140960 0.2018423975 0.1754163951 - 0.2149661928 0.2037782073 0.1772357970 - 0.2169061005 0.2057027072 0.1790401042 - 0.2188301980 0.2076229006 0.1808345020 - 0.2207366973 0.2095226049 0.1826318949 - 0.2226286978 0.2114018947 0.1844107956 - 0.2245007008 0.2132596970 0.1861657947 - 0.2263693064 0.2151038945 0.1879153997 - 0.2282283008 0.2169471979 0.1896584928 - 0.2300662994 0.2187916934 0.1913760006 - 0.2318931073 0.2206248939 0.1930719018 - 0.2337073982 0.2224474996 0.1947627068 - 0.2355096936 0.2242646962 0.1964586973 - 0.2373178005 0.2260648012 0.1981409937 - 0.2391194999 0.2278497964 0.1998063028 - 0.2409114987 0.2296245992 0.2014670074 - 0.2427058071 0.2313791960 0.2031272948 - 0.2444884032 0.2331136018 0.2047773004 - 0.2462590933 0.2348330021 0.2064110041 - 0.2480255961 0.2365438938 0.2080343962 - 0.2497811019 0.2382504940 0.2096567005 - 0.2515194118 0.2399522066 0.2112801969 - 0.2532550991 0.2416505069 0.2129054964 - 0.2549892068 0.2433426976 0.2145148963 - 0.2567017972 0.2450259030 0.2161193043 - 0.2584092021 0.2467028052 0.2177298963 - 0.2601015866 0.2483644933 0.2193389982 - 0.2617790103 0.2500090897 0.2209367007 - 0.2634465098 0.2516413033 0.2225185931 - 0.2651093900 0.2532702088 0.2240934074 - 0.2667602897 0.2549068034 0.2256740034 - 0.2683969140 0.2565379143 0.2272486985 - 0.2700237036 0.2581647933 0.2288096994 - 0.2716447115 0.2597900033 0.2303546071 - 0.2732641101 0.2614181042 0.2318903059 - 0.2748670876 0.2630349994 0.2334287018 - 0.2764695883 0.2646454871 0.2349649072 - 0.2780736983 0.2662520111 0.2364968956 - 0.2796798050 0.2678571939 0.2380219996 - 0.2812705934 0.2694469988 0.2395333946 - 0.2828595936 0.2710259855 0.2410411984 - 0.2844449878 0.2725957036 0.2425480038 - 0.2860139906 0.2741582990 0.2440553010 - 0.2875689864 0.2757219076 0.2455565929 - 0.2891198993 0.2772797048 0.2470498979 - 0.2906715870 0.2788243890 0.2485287040 - 0.2922109962 0.2803615928 0.2500022054 - 0.2937461138 0.2818956077 0.2514745891 - 0.2952778041 0.2834267020 0.2529469132 - 0.2968105972 0.2849394083 0.2543964088 - 0.2983357012 0.2864452004 0.2558352947 - 0.2998512089 0.2879436016 0.2572622895 - 0.3013514876 0.2894333899 0.2586838007 - 0.3028486967 0.2909137011 0.2601067126 - 0.3043426871 0.2923789024 0.2615340948 - 0.3058384061 0.2938305140 0.2629559040 - 0.3073315024 0.2952756882 0.2643721104 - 0.3088150918 0.2967103124 0.2657704055 - 0.3102928102 0.2981418073 0.2671636939 - 0.3117668927 0.2995713055 0.2685531974 - 0.3132205009 0.3009906113 0.2699464858 - 0.3146683872 0.3024047017 0.2713372111 - 0.3161059022 0.3038088977 0.2727209032 - 0.3175452054 0.3052091897 0.2740899920 - 0.3189851940 0.3066073060 0.2754499018 - 0.3204107881 0.3079979122 0.2767995894 - 0.3218325078 0.3093867898 0.2781487107 - 0.3232437968 0.3107711971 0.2794989049 - 0.3246552944 0.3121513128 0.2808524072 - 0.3260669112 0.3135269880 0.2822099924 - 0.3274610937 0.3148944974 0.2835561037 - 0.3288491964 0.3162589073 0.2848969996 - 0.3302272856 0.3176145852 0.2862280905 - 0.3316037059 0.3189670146 0.2875579000 - 0.3329727948 0.3203001022 0.2888858914 - 0.3343341947 0.3216342926 0.2902134955 - 0.3356775939 0.3229714930 0.2915399969 - 0.3370169997 0.3243103921 0.2928608954 - 0.3383513093 0.3256517947 0.2941726148 - 0.3396857977 0.3269968033 0.2954798043 - 0.3410206139 0.3283447921 0.2967823148 - 0.3423599005 0.3296887875 0.2980845869 - 0.3437018096 0.3310303092 0.2993867099 - 0.3450402915 0.3323693871 0.3006789982 - 0.3463771939 0.3337073028 0.3019658029 - 0.3477115035 0.3350473046 0.3032433093 - 0.3490447998 0.3363879919 0.3045162857 - 0.3503681123 0.3377214968 0.3057813048 - 0.3516876996 0.3390519917 0.3070425987 - 0.3530015051 0.3403775096 0.3083038032 - 0.3543129861 0.3417007923 0.3095650971 - 0.3556298018 0.3430233002 0.3108316958 - 0.3569492996 0.3443453014 0.3121018112 - 0.3582645059 0.3456591964 0.3133673072 - 0.3595766127 0.3469670117 0.3146286905 - 0.3608855009 0.3482683003 0.3158847988 - 0.3621911108 0.3495621085 0.3171336055 - 0.3634957969 0.3508524001 0.3183821142 - 0.3647992015 0.3521358073 0.3196297884 - 0.3661004901 0.3534179032 0.3208785057 - 0.3673923910 0.3546931148 0.3221335113 - 0.3686844110 0.3559682965 0.3233886063 - 0.3699693084 0.3572291136 0.3246343136 - 0.3712542057 0.3584896028 0.3258793950 - 0.3725373149 0.3597457111 0.3271150887 - 0.3738197982 0.3610003889 0.3283470869 - 0.3750964105 0.3622534871 0.3295781016 - 0.3763675094 0.3635050952 0.3308078945 - 0.3776369989 0.3647533059 0.3320389092 - 0.3789013028 0.3659900129 0.3332746923 - 0.3801654875 0.3672266901 0.3345105052 - 0.3814153969 0.3684552014 0.3357368112 - 0.3826642036 0.3696827888 0.3369618058 - 0.3838984966 0.3709067106 0.3381794989 - 0.3851222992 0.3721277118 0.3393906951 - 0.3863449097 0.3733451962 0.3406014144 - 0.3875628114 0.3745481074 0.3418095112 - 0.3887807131 0.3757509887 0.3430176079 - 0.3899891078 0.3769412935 0.3442226052 - 0.3911952078 0.3781279922 0.3454265893 - 0.3923954070 0.3793081939 0.3466267884 - 0.3935852945 0.3804757893 0.3478181064 - 0.3947750926 0.3816434145 0.3490093946 - 0.3959554136 0.3828100860 0.3501937985 - 0.3971343935 0.3839766979 0.3513768911 - 0.3983117044 0.3851422071 0.3525578976 - 0.3994863927 0.3863059878 0.3537346125 - 0.4006612003 0.3874697983 0.3549112976 - 0.4018234015 0.3886323869 0.3560853004 - 0.4029830992 0.3897947073 0.3572584987 - 0.4041410089 0.3909561932 0.3584313095 - 0.4052937031 0.3921152949 0.3596020937 - 0.4064464867 0.3932743073 0.3607727885 - 0.4075922072 0.3944297135 0.3619342148 - 0.4087341130 0.3955830038 0.3630889952 - 0.4098758996 0.3967362046 0.3642438948 - 0.4110023081 0.3978854120 0.3653908968 - 0.4121286869 0.3990345001 0.3665375113 - 0.4132547975 0.4001820982 0.3676846027 - 0.4143803120 0.4013260901 0.3688332140 - 0.4155057073 0.4024702013 0.3699817061 - 0.4166313112 0.4036095142 0.3711296916 - 0.4177572131 0.4047453105 0.3722772896 - 0.4188829958 0.4058811963 0.3734247983 - 0.4199970961 0.4070141912 0.3745591044 - 0.4211089909 0.4081465006 0.3756898940 - 0.4222208858 0.4092785120 0.3768205941 - 0.4233328998 0.4104020000 0.3779422939 - 0.4244450033 0.4115254879 0.3790639043 - 0.4255572855 0.4126478136 0.3801856935 - 0.4266704023 0.4137653112 0.3813084066 - 0.4277834892 0.4148826897 0.3824310899 - 0.4288921058 0.4160000980 0.3835541010 - 0.4299925864 0.4171172976 0.3846774995 - 0.4310930073 0.4182344079 0.3858009875 - 0.4321939051 0.4193536937 0.3869203925 - 0.4332954884 0.4204756916 0.3880335093 - 0.4343971014 0.4215976894 0.3891465962 - 0.4354996085 0.4227164984 0.3902549148 - 0.4366030097 0.4238317907 0.3913570940 - 0.4377062917 0.4249469936 0.3924593031 - 0.4388040006 0.4260604084 0.3935610950 - 0.4398961961 0.4271718860 0.3946624100 - 0.4409883022 0.4282833934 0.3957636952 - 0.4420804083 0.4293954074 0.3968654871 - 0.4431723058 0.4305078983 0.3979677856 - 0.4442642033 0.4316205084 0.3990701139 - 0.4453507066 0.4327288866 0.4001703858 - 0.4464294016 0.4338311851 0.4012669921 - 0.4475080073 0.4349335134 0.4023635983 - 0.4485828876 0.4360336959 0.4034568965 - 0.4496490061 0.4371289909 0.4045400023 - 0.4507152140 0.4382242858 0.4056229889 - 0.4517813921 0.4393191934 0.4067057967 - 0.4528484046 0.4404121935 0.4077850878 - 0.4539153874 0.4415051043 0.4088644981 - 0.4549823999 0.4425981045 0.4099437892 - 0.4560458958 0.4436711967 0.4110217094 - 0.4571090937 0.4447430074 0.4120995104 - 0.4581724107 0.4458146989 0.4131771922 - 0.4592300057 0.4468767941 0.4142549038 - 0.4602853060 0.4479345977 0.4153324962 - 0.4613406062 0.4489924014 0.4164102077 - 0.4623945951 0.4500474930 0.4174832106 - 0.4634467959 0.4510987103 0.4185484946 - 0.4644989967 0.4521498978 0.4196138084 - 0.4655506909 0.4532003999 0.4206787944 - 0.4665969014 0.4542433918 0.4217385948 - 0.4676432014 0.4552864134 0.4227983057 - 0.4686895013 0.4563294947 0.4238579869 - 0.4697276056 0.4573652148 0.4249185920 - 0.4707626998 0.4583980143 0.4259794950 - 0.4717977941 0.4594308138 0.4270403981 - 0.4728317857 0.4604609907 0.4280979037 - 0.4738632143 0.4614850879 0.4291453958 - 0.4748947024 0.4625090957 0.4301928878 - 0.4759261906 0.4635331929 0.4312404990 - 0.4769518971 0.4645487964 0.4322755933 - 0.4779764116 0.4655626118 0.4333074987 - 0.4790009856 0.4665764868 0.4343394041 - 0.4800215960 0.4675863981 0.4353694916 - 0.4810340106 0.4685879052 0.4363949895 - 0.4820463955 0.4695892930 0.4374206066 - 0.4830586910 0.4705908000 0.4384461045 - 0.4840714037 0.4715859890 0.4394738078 - 0.4850840867 0.4725790918 0.4405022860 - 0.4860967994 0.4735723138 0.4415307939 - 0.4871076941 0.4745646119 0.4425587952 - 0.4881089032 0.4755522013 0.4435825944 - 0.4891101122 0.4765397906 0.4446065128 - 0.4901114106 0.4775272906 0.4456303120 - 0.4911071956 0.4785116911 0.4466493130 - 0.4920977056 0.4794926047 0.4476625025 - 0.4930881858 0.4804734886 0.4486756027 - 0.4940786958 0.4814544916 0.4496887922 - 0.4950703979 0.4824301004 0.4506951869 - 0.4960624874 0.4834043086 0.4516992867 - 0.4970546067 0.4843784869 0.4527033865 - 0.4980463088 0.4853524864 0.4537074864 - 0.4990282059 0.4863195121 0.4547115862 - 0.5000100732 0.4872865975 0.4557158053 - 0.5009920001 0.4882535934 0.4567199051 - 0.5019717216 0.4892205000 0.4577234983 - 0.5029439926 0.4901863933 0.4587247968 - 0.5039163828 0.4911524057 0.4597260952 - 0.5048887134 0.4921183884 0.4607273936 - 0.5058619976 0.4930835068 0.4617233872 - 0.5068370104 0.4940471947 0.4627084136 - 0.5078119040 0.4950107932 0.4636934102 - 0.5087867975 0.4959745109 0.4646784067 - 0.5097575188 0.4969376922 0.4656594992 - 0.5107231736 0.4979003966 0.4666348100 - 0.5116888285 0.4988631010 0.4676100016 - 0.5126544833 0.4998258054 0.4685853124 - 0.5136160254 0.5007869005 0.4695602953 - 0.5145732164 0.5017461777 0.4705348015 - 0.5155304074 0.5027055740 0.4715093076 - 0.5164874792 0.5036649108 0.4724838138 - 0.5174427032 0.5046222210 0.4734596908 - 0.5183954835 0.5055770278 0.4744375944 - 0.5193483829 0.5065317750 0.4754154980 - 0.5203012228 0.5074865818 0.4763934016 - 0.5212494731 0.5084401965 0.4773665071 - 0.5221908092 0.5093917847 0.4783307016 - 0.5231320858 0.5103434920 0.4792948067 - 0.5240733027 0.5112950802 0.4802590013 - 0.5250124931 0.5122454166 0.4812211096 - 0.5259457231 0.5131919980 0.4821757078 - 0.5268787742 0.5141385794 0.4831303060 - 0.5278120041 0.5150852203 0.4840849936 - 0.5287448764 0.5160313845 0.4850395918 - 0.5296741724 0.5169715881 0.4859933853 - 0.5306035280 0.5179117918 0.4869472086 - 0.5315328836 0.5188519955 0.4879010022 - 0.5324621797 0.5197921991 0.4888547957 - 0.5333819985 0.5207303166 0.4898129106 - 0.5343000293 0.5216680169 0.4907721877 - 0.5352180004 0.5226057172 0.4917314053 - 0.5361359715 0.5235434175 0.4926905930 - 0.5370497108 0.5244811177 0.4936460853 - 0.5379598141 0.5254188180 0.4945977032 - 0.5388697982 0.5263565183 0.4955492914 - 0.5397799015 0.5272942185 0.4965009093 - 0.5406906009 0.5282303095 0.4974510968 - 0.5416039824 0.5291588902 0.4983924925 - 0.5425174832 0.5300874114 0.4993340075 - 0.5434309244 0.5310158730 0.5002753735 - 0.5443444252 0.5319445133 0.5012168884 - 0.5452517867 0.5328708887 0.5021572113 - 0.5461574793 0.5337966084 0.5030971766 - 0.5470631123 0.5347223878 0.5040370822 - 0.5479686856 0.5356481075 0.5049769878 - 0.5488722920 0.5365749002 0.5059170723 - 0.5497710109 0.5375043154 0.5068572760 - 0.5506697893 0.5384337902 0.5077975988 - 0.5515685081 0.5393632054 0.5087379217 - 0.5524672270 0.5402926207 0.5096781850 - 0.5533673763 0.5412126780 0.5106148124 - 0.5542681217 0.5421293974 0.5115498900 - 0.5551686883 0.5430461168 0.5124850273 - 0.5560693741 0.5439627767 0.5134201050 - 0.5569679737 0.5448786020 0.5143538713 - 0.5578554273 0.5457888842 0.5152763128 - 0.5587428212 0.5466991067 0.5161986947 - 0.5596302152 0.5476093888 0.5171210766 - 0.5605176091 0.5485196114 0.5180435181 - 0.5614027977 0.5494297743 0.5189639926 - 0.5622857213 0.5503401160 0.5198820829 - 0.5631685853 0.5512502789 0.5208001733 - 0.5640513897 0.5521605015 0.5217183828 - 0.5649343133 0.5530707240 0.5226364732 - 0.5658143759 0.5539742112 0.5235576034 - 0.5666937232 0.5548753738 0.5244798064 - 0.5675728917 0.5557765961 0.5254021287 - 0.5684521794 0.5566778183 0.5263242722 - 0.5693315268 0.5575789213 0.5272465944 - 0.5701966882 0.5584716201 0.5281707048 - 0.5710616112 0.5593640208 0.5290948749 - 0.5719264746 0.5602563024 0.5300192237 - 0.5727913976 0.5611487031 0.5309433937 - 0.5736559033 0.5620400906 0.5318667889 - 0.5745171905 0.5629237890 0.5327801108 - 0.5753784776 0.5638076067 0.5336934924 - 0.5762398243 0.5646913052 0.5346068144 - 0.5771011114 0.5655751228 0.5355200768 - 0.5779622793 0.5664567947 0.5364326835 - 0.5788232088 0.5673298240 0.5373411775 - 0.5796840787 0.5682027936 0.5382497907 - 0.5805448890 0.5690757036 0.5391582847 - 0.5814058185 0.5699486732 0.5400668979 - 0.5822650790 0.5708199739 0.5409761071 - 0.5831186175 0.5716847777 0.5418882966 - 0.5839720964 0.5725495219 0.5428004861 - 0.5848255754 0.5734142065 0.5437126756 - 0.5856791139 0.5742788911 0.5446249247 - 0.5865321159 0.5751429796 0.5455365181 - 0.5873820782 0.5760030746 0.5464432240 - 0.5882321000 0.5768632293 0.5473498702 - 0.5890821218 0.5777233839 0.5482565165 - 0.5899320841 0.5785834789 0.5491632223 - 0.5907821059 0.5794435143 0.5500699282 - 0.5916317105 0.5802974105 0.5509622097 - 0.5924813151 0.5811514258 0.5518543720 - 0.5933309793 0.5820053816 0.5527465940 - 0.5941805840 0.5828593969 0.5536388159 - 0.5950301886 0.5837132931 0.5545309782 - 0.5958740115 0.5845609903 0.5554181933 - 0.5967167020 0.5854074955 0.5563042164 - 0.5975593925 0.5862538815 0.5571901202 - 0.5984022021 0.5871003866 0.5580760837 - 0.5992448926 0.5879467726 0.5589619875 - 0.6000844836 0.5887897015 0.5598490238 - 0.6009218097 0.5896298885 0.5607368946 - 0.6017590761 0.5904700756 0.5616248250 - 0.6025965214 0.5913102031 0.5625126958 - 0.6034337878 0.5921503901 0.5634006262 - 0.6042696834 0.5929887891 0.5642879009 - 0.6051009893 0.5938212872 0.5651726723 - 0.6059322953 0.5946537852 0.5660576224 - 0.6067636013 0.5954862833 0.5669425130 - 0.6075949073 0.5963187218 0.5678274035 - 0.6084262133 0.5971512198 0.5687121749 - 0.6092525721 0.5979760885 0.5695930719 - 0.6100782156 0.5987992883 0.5704730153 - 0.6109036803 0.5996224880 0.5713528991 - 0.6117292047 0.6004456878 0.5722327232 - 0.6125547886 0.6012688875 0.5731126070 - 0.6133785844 0.6020901203 0.5739895105 - 0.6141995192 0.6029073000 0.5748600960 - 0.6150202751 0.6037245989 0.5757306814 - 0.6158410907 0.6045418978 0.5766013265 - 0.6166620255 0.6053591967 0.5774719119 - 0.6174827814 0.6061763763 0.5783424973 - 0.6182973981 0.6069906950 0.5792120099 - 0.6191102862 0.6078041792 0.5800812840 - 0.6199231744 0.6086177230 0.5809506178 - 0.6207361817 0.6094312072 0.5818198919 - 0.6215491295 0.6102446914 0.5826891065 - 0.6223602295 0.6110575199 0.5835583806 - 0.6231619120 0.6118662953 0.5844280720 - 0.6239635944 0.6126750708 0.5852977037 - 0.6247653961 0.6134839058 0.5861672759 - 0.6255670786 0.6142926812 0.5870369077 - 0.6263688207 0.6151015162 0.5879064798 - 0.6271702051 0.6159092188 0.5887740254 - 0.6279712915 0.6167153120 0.5896387100 - 0.6287723780 0.6175215244 0.5905032754 - 0.6295735240 0.6183276176 0.5913679004 - 0.6303744912 0.6191337705 0.5922325850 - 0.6311755776 0.6199399233 0.5930972099 - 0.6319767833 0.6207464933 0.5939545035 - 0.6327779293 0.6215533018 0.5948076844 - 0.6335790753 0.6223601103 0.5956608057 - 0.6343802810 0.6231669188 0.5965139866 - 0.6351814866 0.6239737272 0.5973672271 - 0.6359826922 0.6247804761 0.5982202888 - 0.6367757916 0.6255866289 0.5990706086 - 0.6375672221 0.6263926029 0.5999200940 - 0.6383585930 0.6271985173 0.6007695794 - 0.6391499043 0.6280043721 0.6016191244 - 0.6399412751 0.6288104057 0.6024686098 - 0.6407327056 0.6296163201 0.6033179760 - 0.6415246129 0.6304203272 0.6041709185 - 0.6423165202 0.6312239766 0.6050243974 - 0.6431084275 0.6320276260 0.6058778763 - 0.6439003944 0.6328312755 0.6067314744 - 0.6446923018 0.6336349845 0.6075850129 - 0.6454842091 0.6344386935 0.6084384918 - 0.6462714076 0.6352415085 0.6092901230 - 0.6470577121 0.6360440850 0.6101412773 - 0.6478440166 0.6368467808 0.6109923720 - 0.6486303806 0.6376494169 0.6118435860 - 0.6494166851 0.6384519935 0.6126946807 - 0.6502029896 0.6392546892 0.6135458946 - 0.6509804726 0.6400555968 0.6143918037 - 0.6517549157 0.6408560276 0.6152355075 - 0.6525294185 0.6416562796 0.6160792112 - 0.6533038020 0.6424565911 0.6169229150 - 0.6540781856 0.6432569027 0.6177666187 - 0.6548526883 0.6440572739 0.6186103225 - 0.6556271911 0.6448554993 0.6194509268 - 0.6564018726 0.6456521153 0.6202887297 - 0.6571764946 0.6464486718 0.6211264729 - 0.6579511762 0.6472452879 0.6219642162 - 0.6587259173 0.6480419040 0.6228020191 - 0.6595004797 0.6488385201 0.6236398220 - 0.6602733731 0.6496348977 0.6244779825 - 0.6610416770 0.6504310966 0.6253173947 - 0.6618101001 0.6512271762 0.6261568069 - 0.6625784039 0.6520233750 0.6269962192 - 0.6633468270 0.6528195143 0.6278355718 - 0.6641150713 0.6536157131 0.6286749244 - 0.6648834944 0.6544117928 0.6295142770 - 0.6656407118 0.6552035809 0.6303495765 - 0.6663972139 0.6559950113 0.6311843991 - 0.6671535969 0.6567865014 0.6320192814 - 0.6679100990 0.6575778723 0.6328541040 - 0.6686664820 0.6583693027 0.6336889267 - 0.6694229841 0.6591607928 0.6345236897 - 0.6701784730 0.6599478722 0.6353541017 - 0.6709331274 0.6607304215 0.6361792088 - 0.6716876030 0.6615129113 0.6370043159 - 0.6724421978 0.6622954011 0.6378294230 - 0.6731967926 0.6630778909 0.6386544704 - 0.6739513278 0.6638603210 0.6394795775 - 0.6747059226 0.6646428108 0.6403046846 - 0.6754562259 0.6654220223 0.6411225200 - 0.6762061715 0.6662008762 0.6419395804 - 0.6769562960 0.6669799089 0.6427568197 - 0.6777064204 0.6677588820 0.6435738802 - 0.6784564257 0.6685379148 0.6443911195 - 0.6792064905 0.6693168283 0.6452081800 - 0.6799539924 0.6700953245 0.6460253000 - 0.6806966066 0.6708728075 0.6468420029 - 0.6814391017 0.6716501713 0.6476588249 - 0.6821817160 0.6724277139 0.6484755278 - 0.6829242706 0.6732050776 0.6492922902 - 0.6836668253 0.6739826202 0.6501089931 - 0.6844093800 0.6747599840 0.6509258151 - 0.6851518154 0.6755318046 0.6517397165 - 0.6858941913 0.6762999892 0.6525517702 - 0.6866366267 0.6770682931 0.6533638239 - 0.6873788834 0.6778365970 0.6541758180 - 0.6881213188 0.6786047816 0.6549878716 - 0.6888636947 0.6793730855 0.6557999253 - 0.6896060705 0.6801413894 0.6566119194 - 0.6903458238 0.6809049249 0.6574186087 - 0.6910849214 0.6816673875 0.6582239866 - 0.6918240786 0.6824299097 0.6590293050 - 0.6925632954 0.6831923723 0.6598346829 - 0.6933023930 0.6839548945 0.6606400013 - 0.6940416098 0.6847174168 0.6614453793 - 0.6947807074 0.6854798794 0.6622506976 - 0.6955140829 0.6862388253 0.6630463004 - 0.6962471008 0.6869974136 0.6638408899 - 0.6969801188 0.6877560019 0.6646354795 - 0.6977130771 0.6885147095 0.6654301882 - 0.6984460950 0.6892732978 0.6662247777 - 0.6991791129 0.6900318861 0.6670194268 - 0.6999120116 0.6907905936 0.6678140163 - 0.7006403208 0.6915383935 0.6686055064 - 0.7013683915 0.6922855973 0.6693965793 - 0.7020965219 0.6930328012 0.6701877713 - 0.7028244734 0.6937800050 0.6709790230 - 0.7035526037 0.6945272088 0.6717702150 - 0.7042806745 0.6952744126 0.6725614071 - 0.7050086856 0.6960216165 0.6733525991 - 0.7057327032 0.6967638135 0.6741454005 - 0.7064561248 0.6975051165 0.6749384999 - 0.7071794868 0.6982464194 0.6757315993 - 0.7079027891 0.6989877820 0.6765246987 - 0.7086262107 0.6997290850 0.6773177981 - 0.7093495727 0.7004703879 0.6781108975 - 0.7100729942 0.7012118101 0.6789039969 - 0.7107927203 0.7019538879 0.6796910167 - 0.7115108967 0.7026965022 0.6804748774 - 0.7122290134 0.7034389973 0.6812587976 - 0.7129471898 0.7041814923 0.6820427775 - 0.7136653066 0.7049241066 0.6828266978 - 0.7143834829 0.7056666017 0.6836106181 - 0.7151015997 0.7064090967 0.6843945980 - 0.7158182263 0.7071503997 0.6851742268 - 0.7165327072 0.7078899145 0.6859470010 - 0.7172471285 0.7086293101 0.6867198944 - 0.7179616094 0.7093688250 0.6874927282 - 0.7186759710 0.7101082206 0.6882656217 - 0.7193905115 0.7108476758 0.6890385151 - 0.7201048732 0.7115870714 0.6898112893 - 0.7208188772 0.7123262882 0.6905841231 - 0.7215241194 0.7130588889 0.6913529038 - 0.7222291827 0.7137916088 0.6921218038 - 0.7229344249 0.7145242095 0.6928905845 - 0.7236396074 0.7152568102 0.6936594248 - 0.7243446708 0.7159894109 0.6944282055 - 0.7250499129 0.7167220712 0.6951969862 - 0.7257549763 0.7174546719 0.6959658861 - 0.7264562845 0.7181851268 0.6967344284 - 0.7271547914 0.7189137936 0.6975026131 - 0.7278531790 0.7196425796 0.6982707977 - 0.7285516262 0.7203713059 0.6990389824 - 0.7292500734 0.7210999727 0.6998072267 - 0.7299485207 0.7218288183 0.7005754113 - 0.7306470275 0.7225574851 0.7013435960 - 0.7313454151 0.7232862115 0.7021117806 - 0.7320457101 0.7240095735 0.7028660774 - 0.7327460051 0.7247328758 0.7036200762 - 0.7334461808 0.7254562974 0.7043740153 - 0.7341464758 0.7261795998 0.7051280141 - 0.7348467708 0.7269030213 0.7058818936 - 0.7355470061 0.7276263237 0.7066358924 - 0.7362473011 0.7283496857 0.7073897719 - 0.7369453907 0.7290710211 0.7081403732 - 0.7376388907 0.7297881246 0.7088832259 - 0.7383323908 0.7305051088 0.7096260190 - 0.7390258908 0.7312222123 0.7103686929 - 0.7397195101 0.7319391966 0.7111114860 - 0.7404130101 0.7326561809 0.7118542194 - 0.7411065102 0.7333732843 0.7125970125 - 0.7418000102 0.7340903282 0.7133396864 - 0.7424902916 0.7348045707 0.7140802741 - 0.7431777716 0.7355164289 0.7148187757 - 0.7438653708 0.7362282276 0.7155572176 - 0.7445529103 0.7369400263 0.7162957191 - 0.7452403903 0.7376518250 0.7170342207 - 0.7459279895 0.7383636832 0.7177727222 - 0.7466155291 0.7390754819 0.7185112238 - 0.7473030090 0.7397872806 0.7192496061 - 0.7479922771 0.7404950857 0.7199897766 - 0.7486823797 0.7412006855 0.7207309008 - 0.7493724227 0.7419062853 0.7214720249 - 0.7500625253 0.7426118851 0.7222132087 - 0.7507526278 0.7433174849 0.7229542732 - 0.7514426708 0.7440230846 0.7236953974 - 0.7521327734 0.7447286844 0.7244365215 - 0.7528228760 0.7454342842 0.7251777053 - 0.7535089850 0.7461348772 0.7259193063 - 0.7541931868 0.7468327880 0.7266612053 - 0.7548773885 0.7475308180 0.7274032235 - 0.7555614710 0.7482287288 0.7281451225 - 0.7562456727 0.7489265800 0.7288870215 - 0.7569298744 0.7496244907 0.7296289802 - 0.7576140165 0.7503225207 0.7303708792 - 0.7582982183 0.7510203719 0.7311127782 - 0.7589793801 0.7517145872 0.7318496704 - 0.7596583962 0.7524058223 0.7325822115 - 0.7603374124 0.7530969977 0.7333146930 - 0.7610164285 0.7537881732 0.7340471148 - 0.7616953254 0.7544794083 0.7347795963 - 0.7623742819 0.7551705837 0.7355120778 - 0.7630532980 0.7558618188 0.7362444997 - 0.7637323141 0.7565529943 0.7369769812 - 0.7644116879 0.7572417259 0.7377079725 - 0.7650921941 0.7579252720 0.7384359241 - 0.7657725811 0.7586089969 0.7391638756 - 0.7664530277 0.7592927217 0.7398918271 - 0.7671334147 0.7599763274 0.7406197190 - 0.7678138018 0.7606599927 0.7413476110 - 0.7684941888 0.7613437176 0.7420756221 - 0.7691746950 0.7620273232 0.7428035140 - 0.7698547840 0.7627109885 0.7435314059 - 0.7705252171 0.7633913159 0.7442615032 - 0.7711957097 0.7640715837 0.7449914813 - 0.7718660831 0.7647519708 0.7457215190 - 0.7725365162 0.7654322982 0.7464514971 - 0.7732068896 0.7661126852 0.7471815944 - 0.7738773227 0.7667930126 0.7479115725 - 0.7745476961 0.7674732804 0.7486416101 - 0.7752181292 0.7681537271 0.7493717074 - 0.7758864760 0.7688329220 0.7501016855 - 0.7765535116 0.7695114017 0.7508317232 - 0.7772204876 0.7701898217 0.7515617013 - 0.7778875232 0.7708683014 0.7522916794 - 0.7785544991 0.7715467215 0.7530217767 - 0.7792214155 0.7722252011 0.7537518144 - 0.7798883915 0.7729036212 0.7544817924 - 0.7805554271 0.7735821009 0.7552117705 - 0.7812224030 0.7742605805 0.7559416890 - 0.7818884850 0.7749400735 0.7566617131 - 0.7825545073 0.7756196856 0.7573816776 - 0.7832205892 0.7762991786 0.7581015825 - 0.7838866711 0.7769787908 0.7588216066 - 0.7845528126 0.7776582837 0.7595415711 - 0.7852188945 0.7783377767 0.7602615952 - 0.7858849764 0.7790173888 0.7609816194 - 0.7865511179 0.7796968818 0.7617015839 - 0.7872148752 0.7803745866 0.7624198198 - 0.7878748178 0.7810488939 0.7631347179 - 0.7885347009 0.7817232013 0.7638494968 - 0.7891945839 0.7823975086 0.7645643950 - 0.7898545265 0.7830718756 0.7652792931 - 0.7905144095 0.7837461829 0.7659941912 - 0.7911742926 0.7844204903 0.7667090893 - 0.7918342948 0.7850949168 0.7674239278 - 0.7924941778 0.7857692242 0.7681388259 - 0.7931510806 0.7864413261 0.7688534856 - 0.7938057780 0.7871118188 0.7695679069 - 0.7944605947 0.7877823114 0.7702823877 - 0.7951152921 0.7884528041 0.7709968090 - 0.7957701087 0.7891234159 0.7717112899 - 0.7964249253 0.7897939086 0.7724257112 - 0.7970796227 0.7904644012 0.7731400728 - 0.7977343798 0.7911348939 0.7738546133 - 0.7983890772 0.7918053865 0.7745689750 - 0.7990390062 0.7924752831 0.7752832174 - 0.7996864915 0.7931450009 0.7759972811 - 0.8003339767 0.7938145995 0.7767112851 - 0.8009815216 0.7944843173 0.7774252892 - 0.8016290069 0.7951539159 0.7781392932 - 0.8022764921 0.7958235741 0.7788534164 - 0.8029239774 0.7964931726 0.7795674205 - 0.8035715222 0.7971628904 0.7802814245 - 0.8042190075 0.7978324890 0.7809954882 - 0.8048598766 0.7984976172 0.7817047834 - 0.8054972887 0.7991601825 0.7824113965 - 0.8061345816 0.7998228073 0.7831178904 - 0.8067719936 0.8004853725 0.7838245034 - 0.8074094057 0.8011479974 0.7845311165 - 0.8080468178 0.8018106222 0.7852376103 - 0.8086841106 0.8024731874 0.7859442234 - 0.8093215227 0.8031358123 0.7866507769 - 0.8099588752 0.8037983775 0.7873573899 - 0.8105943203 0.8044582009 0.7880617976 - 0.8112279177 0.8051152229 0.7887639999 - 0.8118613958 0.8057723045 0.7894663215 - 0.8124949932 0.8064293265 0.7901685238 - 0.8131284714 0.8070862889 0.7908707261 - 0.8137621284 0.8077433109 0.7915729880 - 0.8143956065 0.8084002733 0.7922751904 - 0.8150292039 0.8090572953 0.7929773927 - 0.8156626821 0.8097143769 0.7936797142 - 0.8162947893 0.8103706837 0.7943819761 - 0.8169224262 0.8110250235 0.7950844765 - 0.8175500035 0.8116793036 0.7957870960 - 0.8181775808 0.8123335838 0.7964897156 - 0.8188052177 0.8129879236 0.7971922755 - 0.8194326758 0.8136422038 0.7978948951 - 0.8200603127 0.8142964244 0.7985973954 - 0.8206878901 0.8149507046 0.7993000150 - 0.8213155270 0.8156049848 0.8000025749 - 0.8219431043 0.8162593246 0.8007051945 - 0.8225635290 0.8169105053 0.8014042974 - 0.8231825829 0.8175610900 0.8021026254 - 0.8238015771 0.8182116747 0.8028008938 - 0.8244206905 0.8188623190 0.8034992814 - 0.8250398040 0.8195129037 0.8041976094 - 0.8256589174 0.8201634884 0.8048958778 - 0.8262779117 0.8208140731 0.8055942059 - 0.8268970251 0.8214647174 0.8062925935 - 0.8275160789 0.8221153021 0.8069909215 - 0.8281350136 0.8227658272 0.8076866269 - 0.8287534714 0.8234161139 0.8083760738 - 0.8293719888 0.8240664005 0.8090655804 - 0.8299906254 0.8247166872 0.8097550869 - 0.8306090832 0.8253669739 0.8104445934 - 0.8312276006 0.8260173798 0.8111341000 - 0.8318461776 0.8266677260 0.8118236065 - 0.8324646950 0.8273180127 0.8125131130 - 0.8330832124 0.8279682994 0.8132026196 - 0.8337017298 0.8286185861 0.8138921261 - 0.8343204260 0.8292688131 0.8145787716 - 0.8349391818 0.8299189210 0.8152639866 - 0.8355578780 0.8305690289 0.8159490824 - 0.8361766934 0.8312191963 0.8166342974 - 0.8367953897 0.8318693042 0.8173193932 - 0.8374142051 0.8325194716 0.8180044889 - 0.8380329013 0.8331695795 0.8186897039 - 0.8386517167 0.8338196874 0.8193747997 - 0.8392704129 0.8344699144 0.8200598955 - 0.8398892283 0.8351200223 0.8207451105 - 0.8405013084 0.8357700706 0.8214318156 - 0.8411123753 0.8364201188 0.8221188188 - 0.8417235017 0.8370702267 0.8228058219 - 0.8423346281 0.8377202749 0.8234927058 - 0.8429456949 0.8383703232 0.8241797090 - 0.8435568213 0.8390203714 0.8248667121 - 0.8441678882 0.8396704197 0.8255537152 - 0.8447790146 0.8403205276 0.8262405992 - 0.8453900814 0.8409705162 0.8269276023 - 0.8460012078 0.8416206241 0.8276146054 - 0.8466123939 0.8422645926 0.8282997012 - 0.8472235799 0.8429083228 0.8289846778 - 0.8478348255 0.8435521126 0.8296697140 - 0.8484458923 0.8441957831 0.8303546906 - 0.8490570784 0.8448395133 0.8310397267 - 0.8496683240 0.8454831839 0.8317247033 - 0.8502795100 0.8461269736 0.8324096799 - 0.8508906960 0.8467707038 0.8330947161 - 0.8515018821 0.8474143744 0.8337796926 - 0.8521130085 0.8480581045 0.8344647288 - 0.8527246714 0.8486986756 0.8351467848 - 0.8533365130 0.8493388295 0.8358284831 - 0.8539482951 0.8499789834 0.8365101814 - 0.8545600176 0.8506191969 0.8371918797 - 0.8551717997 0.8512594104 0.8378735781 - 0.8557835817 0.8518995047 0.8385552764 - 0.8563953042 0.8525397182 0.8392369151 - 0.8570070863 0.8531798720 0.8399186134 - 0.8576189280 0.8538200259 0.8406003118 - 0.8582305908 0.8544601798 0.8412820101 - 0.8588374257 0.8550989032 0.8419591784 - 0.8594421744 0.8557369709 0.8426344991 - 0.8600469828 0.8563752174 0.8433098197 - 0.8606517911 0.8570132852 0.8439850211 - 0.8612565994 0.8576514125 0.8446602821 - 0.8618612885 0.8582894802 0.8453356028 - 0.8624660969 0.8589276075 0.8460108042 - 0.8630709052 0.8595656753 0.8466861248 - 0.8636757135 0.8602039218 0.8473613858 - 0.8642805219 0.8608419895 0.8480365872 - 0.8648855090 0.8614770174 0.8487117887 - 0.8654909730 0.8621069193 0.8493868113 - 0.8660963774 0.8627368212 0.8500617146 - 0.8667019010 0.8633667231 0.8507366776 - 0.8673073053 0.8639966249 0.8514117002 - 0.8679128289 0.8646265268 0.8520867229 - 0.8685181737 0.8652564287 0.8527616262 - 0.8691236973 0.8658862710 0.8534365892 - 0.8697291017 0.8665162921 0.8541116118 - 0.8703346252 0.8671461940 0.8547865152 - 0.8709400296 0.8677760959 0.8554614782 - 0.8715463281 0.8684011102 0.8561388254 - 0.8721526861 0.8690257072 0.8568162918 - 0.8727591038 0.8696503043 0.8574938178 - 0.8733655214 0.8702749014 0.8581712842 - 0.8739719987 0.8708994985 0.8588488102 - 0.8745784163 0.8715240955 0.8595262766 - 0.8751847744 0.8721486926 0.8602038026 - 0.8757911921 0.8727732897 0.8608813286 - 0.8763976097 0.8733978868 0.8615587950 - 0.8770040274 0.8740224242 0.8622363210 - 0.8776084781 0.8746458292 0.8629111052 - 0.8782086968 0.8752664924 0.8635792732 - 0.8788089752 0.8758872151 0.8642475009 - 0.8794091940 0.8765078187 0.8649157286 - 0.8800094724 0.8771284819 0.8655838966 - 0.8806096911 0.8777492046 0.8662521243 - 0.8812100291 0.8783698082 0.8669202924 - 0.8818103075 0.8789904714 0.8675885201 - 0.8824105263 0.8796111941 0.8682566881 - 0.8830108047 0.8802317977 0.8689249158 - 0.8836110234 0.8808525205 0.8695930839 - 0.8842111826 0.8814677000 0.8702577949 - 0.8848112822 0.8820785880 0.8709195852 - 0.8854113817 0.8826894164 0.8715813160 - 0.8860116005 0.8833001852 0.8722429872 - 0.8866117001 0.8839110136 0.8729047775 - 0.8872117996 0.8845217824 0.8735665083 - 0.8878118992 0.8851326108 0.8742281795 - 0.8884119987 0.8857433796 0.8748900294 - 0.8890120983 0.8863543272 0.8755517006 - 0.8896123171 0.8869650960 0.8762133718 - 0.8902124166 0.8875759244 0.8768752217 - 0.8908076882 0.8881832957 0.8775365949 - 0.8914005160 0.8887888193 0.8781977892 - 0.8919934034 0.8893944025 0.8788589835 - 0.8925861716 0.8899999857 0.8795201778 - 0.8931789994 0.8906055093 0.8801813722 - 0.8937718272 0.8912110925 0.8808426261 - 0.8943647146 0.8918166757 0.8815038204 - 0.8949574828 0.8924221992 0.8821650147 - 0.8955503106 0.8930277824 0.8828263283 - 0.8961430788 0.8936333060 0.8834875226 - 0.8967359066 0.8942388892 0.8841487169 - 0.8973246217 0.8948436975 0.8848111033 - 0.8979104757 0.8954480290 0.8854743838 - 0.8984965086 0.8960524201 0.8861377835 - 0.8990824223 0.8966566920 0.8868011236 - 0.8996682763 0.8972610235 0.8874644041 - 0.9002543092 0.8978654146 0.8881276846 - 0.9008402228 0.8984696865 0.8887910247 - 0.9014261961 0.8990740180 0.8894543052 - 0.9020121098 0.8996782899 0.8901175857 - 0.9025980234 0.9002826810 0.8907809258 - 0.9031839967 0.9008870125 0.8914443254 - 0.9037700891 0.9014902115 0.8921037912 - 0.9043565989 0.9020918012 0.8927577138 - 0.9049429893 0.9026933908 0.8934116960 - 0.9055293798 0.9032949805 0.8940656185 - 0.9061157703 0.9038965702 0.8947194815 - 0.9067022800 0.9044982195 0.8953734040 - 0.9072886705 0.9050998092 0.8960273266 - 0.9078751206 0.9057013988 0.8966811895 - 0.9084615707 0.9063029885 0.8973351121 - 0.9090480208 0.9069045782 0.8979889750 - 0.9096344113 0.9075062275 0.8986428976 - 0.9102206826 0.9081075191 0.8992965221 - 0.9108051062 0.9087041020 0.8999441266 - 0.9113894105 0.9093006849 0.9005916119 - 0.9119737744 0.9098973274 0.9012390971 - 0.9125580788 0.9104939103 0.9018865824 - 0.9131425023 0.9110904932 0.9025341868 - 0.9137269258 0.9116870761 0.9031816721 - 0.9143111706 0.9122837186 0.9038292170 - 0.9148955941 0.9128803015 0.9044768214 - 0.9154798985 0.9134768844 0.9051243067 - 0.9160643220 0.9140735269 0.9057717919 - 0.9166486263 0.9146699905 0.9064192772 - 0.9172297716 0.9152629972 0.9070658088 - 0.9178082943 0.9158529043 0.9077115059 - 0.9183868766 0.9164428711 0.9083572030 - 0.9189653993 0.9170327783 0.9090027809 - 0.9195439816 0.9176228046 0.9096484780 - 0.9201225042 0.9182127118 0.9102941155 - 0.9207010865 0.9188026786 0.9109398127 - 0.9212796092 0.9193927050 0.9115853906 - 0.9218580723 0.9199826121 0.9122310877 - 0.9224367142 0.9205725789 0.9128767252 - 0.9230151772 0.9211624861 0.9135224223 - 0.9235938191 0.9217525125 0.9141680002 - 0.9241641760 0.9223377109 0.9148079753 - 0.9247336984 0.9229223728 0.9154471159 - 0.9253032207 0.9235070944 0.9160863161 - 0.9258726239 0.9240918159 0.9167255163 - 0.9264420867 0.9246764183 0.9173647165 - 0.9270116091 0.9252610803 0.9180039167 - 0.9275810719 0.9258458018 0.9186431170 - 0.9281504750 0.9264305234 0.9192823172 - 0.9287199974 0.9270151854 0.9199213982 - 0.9292895198 0.9275997877 0.9205605984 - 0.9298589826 0.9281845093 0.9211997986 - 0.9304273725 0.9287682176 0.9218379855 - 0.9309837222 0.9293403029 0.9224632978 - 0.9315400124 0.9299123883 0.9230884910 - 0.9320963025 0.9304844737 0.9237136841 - 0.9326524734 0.9310566187 0.9243389964 - 0.9332088232 0.9316285849 0.9249641895 - 0.9337651134 0.9322006702 0.9255895019 - 0.9343214035 0.9327728152 0.9262146950 - 0.9348776937 0.9333449006 0.9268398881 - 0.9354339242 0.9339169860 0.9274652004 - 0.9359902143 0.9344890118 0.9280903935 - 0.9365465045 0.9350610971 0.9287157059 - 0.9371017814 0.9356322885 0.9293395877 - 0.9376482964 0.9361947179 0.9299502969 - 0.9381946921 0.9367572069 0.9305610061 - 0.9387412071 0.9373196959 0.9311717153 - 0.9392877221 0.9378821254 0.9317824244 - 0.9398341775 0.9384446144 0.9323930740 - 0.9403805733 0.9390071034 0.9330037832 - 0.9409270883 0.9395694733 0.9336146116 - 0.9414736032 0.9401320219 0.9342253208 - 0.9420201182 0.9406945109 0.9348359704 - 0.9425665140 0.9412570000 0.9354466796 - 0.9431130290 0.9418194294 0.9360573888 - 0.9436594844 0.9423819184 0.9366680980 - 0.9441972971 0.9429398775 0.9372712970 - 0.9447346926 0.9434977174 0.9378741980 - 0.9452720881 0.9440554976 0.9384769797 - 0.9458096027 0.9446132779 0.9390798807 - 0.9463469982 0.9451711178 0.9396827221 - 0.9468843937 0.9457287788 0.9402856231 - 0.9474219084 0.9462866187 0.9408884048 - 0.9479593039 0.9468443990 0.9414913058 - 0.9484966993 0.9474021792 0.9420940876 - 0.9490342140 0.9479600191 0.9426969290 - 0.9495716095 0.9485177994 0.9432997704 - 0.9501090050 0.9490755796 0.9439026117 - 0.9506387115 0.9496260285 0.9444972873 - 0.9511643052 0.9501724839 0.9450876117 - 0.9516900182 0.9507189989 0.9456778765 - 0.9522156119 0.9512655139 0.9462682009 - 0.9527412057 0.9518120289 0.9468585253 - 0.9532667994 0.9523584843 0.9474487901 - 0.9537923932 0.9529049993 0.9480391145 - 0.9543179870 0.9534515142 0.9486293197 - 0.9548435807 0.9539980292 0.9492195845 - 0.9553691745 0.9545444846 0.9498099089 - 0.9558948278 0.9550909996 0.9504001737 - 0.9564204216 0.9556375146 0.9509904981 - 0.9569442272 0.9561818838 0.9515780210 - 0.9574593902 0.9567170143 0.9521524906 - 0.9579746127 0.9572520852 0.9527270198 - 0.9584897757 0.9577872157 0.9533016086 - 0.9590049982 0.9583222866 0.9538760781 - 0.9595202208 0.9588574171 0.9544506073 - 0.9600353837 0.9593924880 0.9550250769 - 0.9605506063 0.9599276185 0.9555996060 - 0.9610658288 0.9604626298 0.9561740756 - 0.9615809917 0.9609977007 0.9567486048 - 0.9620962143 0.9615327716 0.9573231936 - 0.9626114964 0.9620679021 0.9578977227 - 0.9631267190 0.9626029730 0.9584721923 - 0.9636358023 0.9631323218 0.9590396285 - 0.9641396999 0.9636566043 0.9596006870 - 0.9646437168 0.9641808867 0.9601618052 - 0.9651476145 0.9647052288 0.9607229829 - 0.9656515121 0.9652295113 0.9612841010 - 0.9661555290 0.9657537937 0.9618452787 - 0.9666594267 0.9662780762 0.9624063969 - 0.9671633244 0.9668024182 0.9629675150 - 0.9676672816 0.9673267007 0.9635286927 - 0.9681711793 0.9678509831 0.9640898108 - 0.9686750770 0.9683753252 0.9646509886 - 0.9691790938 0.9688996077 0.9652121067 - 0.9696829915 0.9694238901 0.9657732248 - 0.9701765180 0.9699373245 0.9663273096 - 0.9706659913 0.9704464078 0.9668784738 - 0.9711555243 0.9709556103 0.9674296975 - 0.9716449976 0.9714648128 0.9679809809 - 0.9721345901 0.9719740152 0.9685322046 - 0.9726241231 0.9724830985 0.9690834880 - 0.9731135964 0.9729923010 0.9696347117 - 0.9736031294 0.9735015035 0.9701858759 - 0.9740926027 0.9740107059 0.9707372189 - 0.9745820761 0.9745197892 0.9712883830 - 0.9750717282 0.9750289917 0.9718397260 - 0.9755612016 0.9755381942 0.9723908901 - 0.9760506749 0.9760473967 0.9729421139 - 0.9765306115 0.9765431881 0.9734861851 - 0.9770072103 0.9770342112 0.9740275145 - 0.9774836898 0.9775252938 0.9745689034 - 0.9779602885 0.9780163765 0.9751102924 - 0.9784368277 0.9785073996 0.9756516814 - 0.9789134264 0.9789984822 0.9761930704 - 0.9793899059 0.9794895053 0.9767343998 - 0.9798665047 0.9799805880 0.9772757888 - 0.9803431034 0.9804716110 0.9778171778 - 0.9808195829 0.9809626937 0.9783586264 - 0.9812961817 0.9814537764 0.9789000154 - 0.9817727208 0.9819447994 0.9794412851 - 0.9822493196 0.9824358821 0.9799826741 - 0.9827123284 0.9829128981 0.9805108905 - 0.9831652045 0.9833794236 0.9810290933 - 0.9836181998 0.9838458896 0.9815472960 - 0.9840711951 0.9843124151 0.9820654988 - 0.9845240712 0.9847788811 0.9825838208 - 0.9849771261 0.9852454066 0.9831020236 - 0.9854301214 0.9857119918 0.9836202264 - 0.9858829975 0.9861785173 0.9841384292 - 0.9863359928 0.9866449833 0.9846565723 - 0.9867889881 0.9871115088 0.9851748943 - 0.9872419238 0.9875779748 0.9856930971 - 0.9876949191 0.9880445004 0.9862112999 - 0.9881477952 0.9885110855 0.9867295027 - 0.9885957837 0.9889689088 0.9872413278 - 0.9890270829 0.9893978834 0.9877312779 - 0.9894583821 0.9898269176 0.9882212877 - 0.9898896813 0.9902558923 0.9887112975 - 0.9903209805 0.9906849265 0.9892013073 - 0.9907522798 0.9911139011 0.9896913171 - 0.9911835790 0.9915428758 0.9901813269 - 0.9916148782 0.9919719100 0.9906713963 - 0.9920461774 0.9924008846 0.9911614060 - 0.9924774766 0.9928299189 0.9916514158 - 0.9929087758 0.9932588935 0.9921414256 - 0.9933400750 0.9936879277 0.9926313758 - 0.9937713742 0.9941169024 0.9931213856 - 0.9942026734 0.9945458770 0.9936113954 - 0.9946215749 0.9949464798 0.9940770864 - 0.9950352907 0.9953352213 0.9945327044 - 0.9954490066 0.9957239032 0.9949883223 - 0.9958627224 0.9961127043 0.9954438806 - 0.9962764978 0.9965013862 0.9958994985 - 0.9966902137 0.9968901277 0.9963551760 - 0.9971039295 0.9972789288 0.9968107939 - 0.9975175858 0.9976676106 0.9972664118 - 0.9979314208 0.9980562925 0.9977220297 - 0.9983450770 0.9984450936 0.9981775880 - 0.9987587929 0.9988337755 0.9986332059 - 0.9991725087 0.9992225170 0.9990888238 - 0.9995862842 0.9996113181 0.9995443821 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0104342103 0.0097368704 0.0081004426 + 0.0202932805 0.0187927708 0.0152669698 + 0.0287755504 0.0266450606 0.0213894807 + 0.0361699387 0.0334015600 0.0267654601 + 0.0427807011 0.0394648015 0.0316598192 + 0.0487997085 0.0450807586 0.0361505710 + 0.0544521399 0.0502359308 0.0404325016 + 0.0596807487 0.0550900213 0.0445077084 + 0.0646303892 0.0597528182 0.0484247990 + 0.0693302229 0.0641593412 0.0521505699 + 0.0737845525 0.0683351308 0.0556968004 + 0.0780915692 0.0723901764 0.0591261685 + 0.0822694898 0.0763313472 0.0624415018 + 0.0863260701 0.0801697522 0.0657054484 + 0.0902359709 0.0839016736 0.0689003170 + 0.0940105170 0.0874804035 0.0719991401 + 0.0976487026 0.0909066126 0.0750158876 + 0.1011712030 0.0942689478 0.0779601783 + 0.1046311036 0.0975821987 0.0808535069 + 0.1080254018 0.1008832976 0.0837027133 + 0.1113561988 0.1041185036 0.0864896476 + 0.1146244034 0.1072700024 0.0892440900 + 0.1178375036 0.1103363037 0.0919481963 + 0.1209478006 0.1133145019 0.0945950523 + 0.1239672974 0.1161829010 0.0971959010 + 0.1269288063 0.1190017015 0.0997639596 + 0.1298519969 0.1217684969 0.1022854969 + 0.1327518970 0.1245191991 0.1047450975 + 0.1356167048 0.1272488981 0.1071709991 + 0.1384285986 0.1299587935 0.1095672026 + 0.1411979049 0.1326563060 0.1119202971 + 0.1438945979 0.1353050023 0.1142712981 + 0.1465526074 0.1378984004 0.1166002005 + 0.1491758972 0.1404460073 0.1189038008 + 0.1517738998 0.1429633051 0.1211721972 + 0.1543568969 0.1454481930 0.1234188005 + 0.1569266021 0.1478973031 0.1256408989 + 0.1594693065 0.1503026038 0.1278519034 + 0.1619731039 0.1526698023 0.1300269067 + 0.1644496024 0.1550289989 0.1322003007 + 0.1668978930 0.1573766023 0.1343421042 + 0.1693100929 0.1597099006 0.1364780068 + 0.1717022955 0.1620142013 0.1385955065 + 0.1740535051 0.1643050015 0.1406960934 + 0.1763895005 0.1665803045 0.1427800953 + 0.1786942929 0.1688499004 0.1448349953 + 0.1809858978 0.1710920930 0.1468731016 + 0.1832552999 0.1733157039 0.1488973945 + 0.1855040044 0.1754987985 0.1508941948 + 0.1877363026 0.1776573956 0.1528868973 + 0.1899455935 0.1797675043 0.1548427939 + 0.1921364963 0.1818484068 0.1567813009 + 0.1943152994 0.1839254946 0.1587089002 + 0.1964662969 0.1859872043 0.1605975032 + 0.1986126006 0.1880321950 0.1624716967 + 0.2007299960 0.1900521070 0.1643451005 + 0.2028377950 0.1920509040 0.1662126929 + 0.2049141973 0.1940355003 0.1680644006 + 0.2069772035 0.1960064024 0.1699177027 + 0.2090151012 0.1979558021 0.1717617065 + 0.2110265940 0.1998984069 0.1735890955 + 0.2130140960 0.2018423975 0.1754163951 + 0.2149661928 0.2037782073 0.1772357970 + 0.2169061005 0.2057027072 0.1790401042 + 0.2188301980 0.2076229006 0.1808345020 + 0.2207366973 0.2095226049 0.1826318949 + 0.2226286978 0.2114018947 0.1844107956 + 0.2245007008 0.2132596970 0.1861657947 + 0.2263693064 0.2151038945 0.1879153997 + 0.2282283008 0.2169471979 0.1896584928 + 0.2300662994 0.2187916934 0.1913760006 + 0.2318931073 0.2206248939 0.1930719018 + 0.2337073982 0.2224474996 0.1947627068 + 0.2355096936 0.2242646962 0.1964586973 + 0.2373178005 0.2260648012 0.1981409937 + 0.2391194999 0.2278497964 0.1998063028 + 0.2409114987 0.2296245992 0.2014670074 + 0.2427058071 0.2313791960 0.2031272948 + 0.2444884032 0.2331136018 0.2047773004 + 0.2462590933 0.2348330021 0.2064110041 + 0.2480255961 0.2365438938 0.2080343962 + 0.2497811019 0.2382504940 0.2096567005 + 0.2515194118 0.2399522066 0.2112801969 + 0.2532550991 0.2416505069 0.2129054964 + 0.2549892068 0.2433426976 0.2145148963 + 0.2567017972 0.2450259030 0.2161193043 + 0.2584092021 0.2467028052 0.2177298963 + 0.2601015866 0.2483644933 0.2193389982 + 0.2617790103 0.2500090897 0.2209367007 + 0.2634465098 0.2516413033 0.2225185931 + 0.2651093900 0.2532702088 0.2240934074 + 0.2667602897 0.2549068034 0.2256740034 + 0.2683969140 0.2565379143 0.2272486985 + 0.2700237036 0.2581647933 0.2288096994 + 0.2716447115 0.2597900033 0.2303546071 + 0.2732641101 0.2614181042 0.2318903059 + 0.2748670876 0.2630349994 0.2334287018 + 0.2764695883 0.2646454871 0.2349649072 + 0.2780736983 0.2662520111 0.2364968956 + 0.2796798050 0.2678571939 0.2380219996 + 0.2812705934 0.2694469988 0.2395333946 + 0.2828595936 0.2710259855 0.2410411984 + 0.2844449878 0.2725957036 0.2425480038 + 0.2860139906 0.2741582990 0.2440553010 + 0.2875689864 0.2757219076 0.2455565929 + 0.2891198993 0.2772797048 0.2470498979 + 0.2906715870 0.2788243890 0.2485287040 + 0.2922109962 0.2803615928 0.2500022054 + 0.2937461138 0.2818956077 0.2514745891 + 0.2952778041 0.2834267020 0.2529469132 + 0.2968105972 0.2849394083 0.2543964088 + 0.2983357012 0.2864452004 0.2558352947 + 0.2998512089 0.2879436016 0.2572622895 + 0.3013514876 0.2894333899 0.2586838007 + 0.3028486967 0.2909137011 0.2601067126 + 0.3043426871 0.2923789024 0.2615340948 + 0.3058384061 0.2938305140 0.2629559040 + 0.3073315024 0.2952756882 0.2643721104 + 0.3088150918 0.2967103124 0.2657704055 + 0.3102928102 0.2981418073 0.2671636939 + 0.3117668927 0.2995713055 0.2685531974 + 0.3132205009 0.3009906113 0.2699464858 + 0.3146683872 0.3024047017 0.2713372111 + 0.3161059022 0.3038088977 0.2727209032 + 0.3175452054 0.3052091897 0.2740899920 + 0.3189851940 0.3066073060 0.2754499018 + 0.3204107881 0.3079979122 0.2767995894 + 0.3218325078 0.3093867898 0.2781487107 + 0.3232437968 0.3107711971 0.2794989049 + 0.3246552944 0.3121513128 0.2808524072 + 0.3260669112 0.3135269880 0.2822099924 + 0.3274610937 0.3148944974 0.2835561037 + 0.3288491964 0.3162589073 0.2848969996 + 0.3302272856 0.3176145852 0.2862280905 + 0.3316037059 0.3189670146 0.2875579000 + 0.3329727948 0.3203001022 0.2888858914 + 0.3343341947 0.3216342926 0.2902134955 + 0.3356775939 0.3229714930 0.2915399969 + 0.3370169997 0.3243103921 0.2928608954 + 0.3383513093 0.3256517947 0.2941726148 + 0.3396857977 0.3269968033 0.2954798043 + 0.3410206139 0.3283447921 0.2967823148 + 0.3423599005 0.3296887875 0.2980845869 + 0.3437018096 0.3310303092 0.2993867099 + 0.3450402915 0.3323693871 0.3006789982 + 0.3463771939 0.3337073028 0.3019658029 + 0.3477115035 0.3350473046 0.3032433093 + 0.3490447998 0.3363879919 0.3045162857 + 0.3503681123 0.3377214968 0.3057813048 + 0.3516876996 0.3390519917 0.3070425987 + 0.3530015051 0.3403775096 0.3083038032 + 0.3543129861 0.3417007923 0.3095650971 + 0.3556298018 0.3430233002 0.3108316958 + 0.3569492996 0.3443453014 0.3121018112 + 0.3582645059 0.3456591964 0.3133673072 + 0.3595766127 0.3469670117 0.3146286905 + 0.3608855009 0.3482683003 0.3158847988 + 0.3621911108 0.3495621085 0.3171336055 + 0.3634957969 0.3508524001 0.3183821142 + 0.3647992015 0.3521358073 0.3196297884 + 0.3661004901 0.3534179032 0.3208785057 + 0.3673923910 0.3546931148 0.3221335113 + 0.3686844110 0.3559682965 0.3233886063 + 0.3699693084 0.3572291136 0.3246343136 + 0.3712542057 0.3584896028 0.3258793950 + 0.3725373149 0.3597457111 0.3271150887 + 0.3738197982 0.3610003889 0.3283470869 + 0.3750964105 0.3622534871 0.3295781016 + 0.3763675094 0.3635050952 0.3308078945 + 0.3776369989 0.3647533059 0.3320389092 + 0.3789013028 0.3659900129 0.3332746923 + 0.3801654875 0.3672266901 0.3345105052 + 0.3814153969 0.3684552014 0.3357368112 + 0.3826642036 0.3696827888 0.3369618058 + 0.3838984966 0.3709067106 0.3381794989 + 0.3851222992 0.3721277118 0.3393906951 + 0.3863449097 0.3733451962 0.3406014144 + 0.3875628114 0.3745481074 0.3418095112 + 0.3887807131 0.3757509887 0.3430176079 + 0.3899891078 0.3769412935 0.3442226052 + 0.3911952078 0.3781279922 0.3454265893 + 0.3923954070 0.3793081939 0.3466267884 + 0.3935852945 0.3804757893 0.3478181064 + 0.3947750926 0.3816434145 0.3490093946 + 0.3959554136 0.3828100860 0.3501937985 + 0.3971343935 0.3839766979 0.3513768911 + 0.3983117044 0.3851422071 0.3525578976 + 0.3994863927 0.3863059878 0.3537346125 + 0.4006612003 0.3874697983 0.3549112976 + 0.4018234015 0.3886323869 0.3560853004 + 0.4029830992 0.3897947073 0.3572584987 + 0.4041410089 0.3909561932 0.3584313095 + 0.4052937031 0.3921152949 0.3596020937 + 0.4064464867 0.3932743073 0.3607727885 + 0.4075922072 0.3944297135 0.3619342148 + 0.4087341130 0.3955830038 0.3630889952 + 0.4098758996 0.3967362046 0.3642438948 + 0.4110023081 0.3978854120 0.3653908968 + 0.4121286869 0.3990345001 0.3665375113 + 0.4132547975 0.4001820982 0.3676846027 + 0.4143803120 0.4013260901 0.3688332140 + 0.4155057073 0.4024702013 0.3699817061 + 0.4166313112 0.4036095142 0.3711296916 + 0.4177572131 0.4047453105 0.3722772896 + 0.4188829958 0.4058811963 0.3734247983 + 0.4199970961 0.4070141912 0.3745591044 + 0.4211089909 0.4081465006 0.3756898940 + 0.4222208858 0.4092785120 0.3768205941 + 0.4233328998 0.4104020000 0.3779422939 + 0.4244450033 0.4115254879 0.3790639043 + 0.4255572855 0.4126478136 0.3801856935 + 0.4266704023 0.4137653112 0.3813084066 + 0.4277834892 0.4148826897 0.3824310899 + 0.4288921058 0.4160000980 0.3835541010 + 0.4299925864 0.4171172976 0.3846774995 + 0.4310930073 0.4182344079 0.3858009875 + 0.4321939051 0.4193536937 0.3869203925 + 0.4332954884 0.4204756916 0.3880335093 + 0.4343971014 0.4215976894 0.3891465962 + 0.4354996085 0.4227164984 0.3902549148 + 0.4366030097 0.4238317907 0.3913570940 + 0.4377062917 0.4249469936 0.3924593031 + 0.4388040006 0.4260604084 0.3935610950 + 0.4398961961 0.4271718860 0.3946624100 + 0.4409883022 0.4282833934 0.3957636952 + 0.4420804083 0.4293954074 0.3968654871 + 0.4431723058 0.4305078983 0.3979677856 + 0.4442642033 0.4316205084 0.3990701139 + 0.4453507066 0.4327288866 0.4001703858 + 0.4464294016 0.4338311851 0.4012669921 + 0.4475080073 0.4349335134 0.4023635983 + 0.4485828876 0.4360336959 0.4034568965 + 0.4496490061 0.4371289909 0.4045400023 + 0.4507152140 0.4382242858 0.4056229889 + 0.4517813921 0.4393191934 0.4067057967 + 0.4528484046 0.4404121935 0.4077850878 + 0.4539153874 0.4415051043 0.4088644981 + 0.4549823999 0.4425981045 0.4099437892 + 0.4560458958 0.4436711967 0.4110217094 + 0.4571090937 0.4447430074 0.4120995104 + 0.4581724107 0.4458146989 0.4131771922 + 0.4592300057 0.4468767941 0.4142549038 + 0.4602853060 0.4479345977 0.4153324962 + 0.4613406062 0.4489924014 0.4164102077 + 0.4623945951 0.4500474930 0.4174832106 + 0.4634467959 0.4510987103 0.4185484946 + 0.4644989967 0.4521498978 0.4196138084 + 0.4655506909 0.4532003999 0.4206787944 + 0.4665969014 0.4542433918 0.4217385948 + 0.4676432014 0.4552864134 0.4227983057 + 0.4686895013 0.4563294947 0.4238579869 + 0.4697276056 0.4573652148 0.4249185920 + 0.4707626998 0.4583980143 0.4259794950 + 0.4717977941 0.4594308138 0.4270403981 + 0.4728317857 0.4604609907 0.4280979037 + 0.4738632143 0.4614850879 0.4291453958 + 0.4748947024 0.4625090957 0.4301928878 + 0.4759261906 0.4635331929 0.4312404990 + 0.4769518971 0.4645487964 0.4322755933 + 0.4779764116 0.4655626118 0.4333074987 + 0.4790009856 0.4665764868 0.4343394041 + 0.4800215960 0.4675863981 0.4353694916 + 0.4810340106 0.4685879052 0.4363949895 + 0.4820463955 0.4695892930 0.4374206066 + 0.4830586910 0.4705908000 0.4384461045 + 0.4840714037 0.4715859890 0.4394738078 + 0.4850840867 0.4725790918 0.4405022860 + 0.4860967994 0.4735723138 0.4415307939 + 0.4871076941 0.4745646119 0.4425587952 + 0.4881089032 0.4755522013 0.4435825944 + 0.4891101122 0.4765397906 0.4446065128 + 0.4901114106 0.4775272906 0.4456303120 + 0.4911071956 0.4785116911 0.4466493130 + 0.4920977056 0.4794926047 0.4476625025 + 0.4930881858 0.4804734886 0.4486756027 + 0.4940786958 0.4814544916 0.4496887922 + 0.4950703979 0.4824301004 0.4506951869 + 0.4960624874 0.4834043086 0.4516992867 + 0.4970546067 0.4843784869 0.4527033865 + 0.4980463088 0.4853524864 0.4537074864 + 0.4990282059 0.4863195121 0.4547115862 + 0.5000100732 0.4872865975 0.4557158053 + 0.5009920001 0.4882535934 0.4567199051 + 0.5019717216 0.4892205000 0.4577234983 + 0.5029439926 0.4901863933 0.4587247968 + 0.5039163828 0.4911524057 0.4597260952 + 0.5048887134 0.4921183884 0.4607273936 + 0.5058619976 0.4930835068 0.4617233872 + 0.5068370104 0.4940471947 0.4627084136 + 0.5078119040 0.4950107932 0.4636934102 + 0.5087867975 0.4959745109 0.4646784067 + 0.5097575188 0.4969376922 0.4656594992 + 0.5107231736 0.4979003966 0.4666348100 + 0.5116888285 0.4988631010 0.4676100016 + 0.5126544833 0.4998258054 0.4685853124 + 0.5136160254 0.5007869005 0.4695602953 + 0.5145732164 0.5017461777 0.4705348015 + 0.5155304074 0.5027055740 0.4715093076 + 0.5164874792 0.5036649108 0.4724838138 + 0.5174427032 0.5046222210 0.4734596908 + 0.5183954835 0.5055770278 0.4744375944 + 0.5193483829 0.5065317750 0.4754154980 + 0.5203012228 0.5074865818 0.4763934016 + 0.5212494731 0.5084401965 0.4773665071 + 0.5221908092 0.5093917847 0.4783307016 + 0.5231320858 0.5103434920 0.4792948067 + 0.5240733027 0.5112950802 0.4802590013 + 0.5250124931 0.5122454166 0.4812211096 + 0.5259457231 0.5131919980 0.4821757078 + 0.5268787742 0.5141385794 0.4831303060 + 0.5278120041 0.5150852203 0.4840849936 + 0.5287448764 0.5160313845 0.4850395918 + 0.5296741724 0.5169715881 0.4859933853 + 0.5306035280 0.5179117918 0.4869472086 + 0.5315328836 0.5188519955 0.4879010022 + 0.5324621797 0.5197921991 0.4888547957 + 0.5333819985 0.5207303166 0.4898129106 + 0.5343000293 0.5216680169 0.4907721877 + 0.5352180004 0.5226057172 0.4917314053 + 0.5361359715 0.5235434175 0.4926905930 + 0.5370497108 0.5244811177 0.4936460853 + 0.5379598141 0.5254188180 0.4945977032 + 0.5388697982 0.5263565183 0.4955492914 + 0.5397799015 0.5272942185 0.4965009093 + 0.5406906009 0.5282303095 0.4974510968 + 0.5416039824 0.5291588902 0.4983924925 + 0.5425174832 0.5300874114 0.4993340075 + 0.5434309244 0.5310158730 0.5002753735 + 0.5443444252 0.5319445133 0.5012168884 + 0.5452517867 0.5328708887 0.5021572113 + 0.5461574793 0.5337966084 0.5030971766 + 0.5470631123 0.5347223878 0.5040370822 + 0.5479686856 0.5356481075 0.5049769878 + 0.5488722920 0.5365749002 0.5059170723 + 0.5497710109 0.5375043154 0.5068572760 + 0.5506697893 0.5384337902 0.5077975988 + 0.5515685081 0.5393632054 0.5087379217 + 0.5524672270 0.5402926207 0.5096781850 + 0.5533673763 0.5412126780 0.5106148124 + 0.5542681217 0.5421293974 0.5115498900 + 0.5551686883 0.5430461168 0.5124850273 + 0.5560693741 0.5439627767 0.5134201050 + 0.5569679737 0.5448786020 0.5143538713 + 0.5578554273 0.5457888842 0.5152763128 + 0.5587428212 0.5466991067 0.5161986947 + 0.5596302152 0.5476093888 0.5171210766 + 0.5605176091 0.5485196114 0.5180435181 + 0.5614027977 0.5494297743 0.5189639926 + 0.5622857213 0.5503401160 0.5198820829 + 0.5631685853 0.5512502789 0.5208001733 + 0.5640513897 0.5521605015 0.5217183828 + 0.5649343133 0.5530707240 0.5226364732 + 0.5658143759 0.5539742112 0.5235576034 + 0.5666937232 0.5548753738 0.5244798064 + 0.5675728917 0.5557765961 0.5254021287 + 0.5684521794 0.5566778183 0.5263242722 + 0.5693315268 0.5575789213 0.5272465944 + 0.5701966882 0.5584716201 0.5281707048 + 0.5710616112 0.5593640208 0.5290948749 + 0.5719264746 0.5602563024 0.5300192237 + 0.5727913976 0.5611487031 0.5309433937 + 0.5736559033 0.5620400906 0.5318667889 + 0.5745171905 0.5629237890 0.5327801108 + 0.5753784776 0.5638076067 0.5336934924 + 0.5762398243 0.5646913052 0.5346068144 + 0.5771011114 0.5655751228 0.5355200768 + 0.5779622793 0.5664567947 0.5364326835 + 0.5788232088 0.5673298240 0.5373411775 + 0.5796840787 0.5682027936 0.5382497907 + 0.5805448890 0.5690757036 0.5391582847 + 0.5814058185 0.5699486732 0.5400668979 + 0.5822650790 0.5708199739 0.5409761071 + 0.5831186175 0.5716847777 0.5418882966 + 0.5839720964 0.5725495219 0.5428004861 + 0.5848255754 0.5734142065 0.5437126756 + 0.5856791139 0.5742788911 0.5446249247 + 0.5865321159 0.5751429796 0.5455365181 + 0.5873820782 0.5760030746 0.5464432240 + 0.5882321000 0.5768632293 0.5473498702 + 0.5890821218 0.5777233839 0.5482565165 + 0.5899320841 0.5785834789 0.5491632223 + 0.5907821059 0.5794435143 0.5500699282 + 0.5916317105 0.5802974105 0.5509622097 + 0.5924813151 0.5811514258 0.5518543720 + 0.5933309793 0.5820053816 0.5527465940 + 0.5941805840 0.5828593969 0.5536388159 + 0.5950301886 0.5837132931 0.5545309782 + 0.5958740115 0.5845609903 0.5554181933 + 0.5967167020 0.5854074955 0.5563042164 + 0.5975593925 0.5862538815 0.5571901202 + 0.5984022021 0.5871003866 0.5580760837 + 0.5992448926 0.5879467726 0.5589619875 + 0.6000844836 0.5887897015 0.5598490238 + 0.6009218097 0.5896298885 0.5607368946 + 0.6017590761 0.5904700756 0.5616248250 + 0.6025965214 0.5913102031 0.5625126958 + 0.6034337878 0.5921503901 0.5634006262 + 0.6042696834 0.5929887891 0.5642879009 + 0.6051009893 0.5938212872 0.5651726723 + 0.6059322953 0.5946537852 0.5660576224 + 0.6067636013 0.5954862833 0.5669425130 + 0.6075949073 0.5963187218 0.5678274035 + 0.6084262133 0.5971512198 0.5687121749 + 0.6092525721 0.5979760885 0.5695930719 + 0.6100782156 0.5987992883 0.5704730153 + 0.6109036803 0.5996224880 0.5713528991 + 0.6117292047 0.6004456878 0.5722327232 + 0.6125547886 0.6012688875 0.5731126070 + 0.6133785844 0.6020901203 0.5739895105 + 0.6141995192 0.6029073000 0.5748600960 + 0.6150202751 0.6037245989 0.5757306814 + 0.6158410907 0.6045418978 0.5766013265 + 0.6166620255 0.6053591967 0.5774719119 + 0.6174827814 0.6061763763 0.5783424973 + 0.6182973981 0.6069906950 0.5792120099 + 0.6191102862 0.6078041792 0.5800812840 + 0.6199231744 0.6086177230 0.5809506178 + 0.6207361817 0.6094312072 0.5818198919 + 0.6215491295 0.6102446914 0.5826891065 + 0.6223602295 0.6110575199 0.5835583806 + 0.6231619120 0.6118662953 0.5844280720 + 0.6239635944 0.6126750708 0.5852977037 + 0.6247653961 0.6134839058 0.5861672759 + 0.6255670786 0.6142926812 0.5870369077 + 0.6263688207 0.6151015162 0.5879064798 + 0.6271702051 0.6159092188 0.5887740254 + 0.6279712915 0.6167153120 0.5896387100 + 0.6287723780 0.6175215244 0.5905032754 + 0.6295735240 0.6183276176 0.5913679004 + 0.6303744912 0.6191337705 0.5922325850 + 0.6311755776 0.6199399233 0.5930972099 + 0.6319767833 0.6207464933 0.5939545035 + 0.6327779293 0.6215533018 0.5948076844 + 0.6335790753 0.6223601103 0.5956608057 + 0.6343802810 0.6231669188 0.5965139866 + 0.6351814866 0.6239737272 0.5973672271 + 0.6359826922 0.6247804761 0.5982202888 + 0.6367757916 0.6255866289 0.5990706086 + 0.6375672221 0.6263926029 0.5999200940 + 0.6383585930 0.6271985173 0.6007695794 + 0.6391499043 0.6280043721 0.6016191244 + 0.6399412751 0.6288104057 0.6024686098 + 0.6407327056 0.6296163201 0.6033179760 + 0.6415246129 0.6304203272 0.6041709185 + 0.6423165202 0.6312239766 0.6050243974 + 0.6431084275 0.6320276260 0.6058778763 + 0.6439003944 0.6328312755 0.6067314744 + 0.6446923018 0.6336349845 0.6075850129 + 0.6454842091 0.6344386935 0.6084384918 + 0.6462714076 0.6352415085 0.6092901230 + 0.6470577121 0.6360440850 0.6101412773 + 0.6478440166 0.6368467808 0.6109923720 + 0.6486303806 0.6376494169 0.6118435860 + 0.6494166851 0.6384519935 0.6126946807 + 0.6502029896 0.6392546892 0.6135458946 + 0.6509804726 0.6400555968 0.6143918037 + 0.6517549157 0.6408560276 0.6152355075 + 0.6525294185 0.6416562796 0.6160792112 + 0.6533038020 0.6424565911 0.6169229150 + 0.6540781856 0.6432569027 0.6177666187 + 0.6548526883 0.6440572739 0.6186103225 + 0.6556271911 0.6448554993 0.6194509268 + 0.6564018726 0.6456521153 0.6202887297 + 0.6571764946 0.6464486718 0.6211264729 + 0.6579511762 0.6472452879 0.6219642162 + 0.6587259173 0.6480419040 0.6228020191 + 0.6595004797 0.6488385201 0.6236398220 + 0.6602733731 0.6496348977 0.6244779825 + 0.6610416770 0.6504310966 0.6253173947 + 0.6618101001 0.6512271762 0.6261568069 + 0.6625784039 0.6520233750 0.6269962192 + 0.6633468270 0.6528195143 0.6278355718 + 0.6641150713 0.6536157131 0.6286749244 + 0.6648834944 0.6544117928 0.6295142770 + 0.6656407118 0.6552035809 0.6303495765 + 0.6663972139 0.6559950113 0.6311843991 + 0.6671535969 0.6567865014 0.6320192814 + 0.6679100990 0.6575778723 0.6328541040 + 0.6686664820 0.6583693027 0.6336889267 + 0.6694229841 0.6591607928 0.6345236897 + 0.6701784730 0.6599478722 0.6353541017 + 0.6709331274 0.6607304215 0.6361792088 + 0.6716876030 0.6615129113 0.6370043159 + 0.6724421978 0.6622954011 0.6378294230 + 0.6731967926 0.6630778909 0.6386544704 + 0.6739513278 0.6638603210 0.6394795775 + 0.6747059226 0.6646428108 0.6403046846 + 0.6754562259 0.6654220223 0.6411225200 + 0.6762061715 0.6662008762 0.6419395804 + 0.6769562960 0.6669799089 0.6427568197 + 0.6777064204 0.6677588820 0.6435738802 + 0.6784564257 0.6685379148 0.6443911195 + 0.6792064905 0.6693168283 0.6452081800 + 0.6799539924 0.6700953245 0.6460253000 + 0.6806966066 0.6708728075 0.6468420029 + 0.6814391017 0.6716501713 0.6476588249 + 0.6821817160 0.6724277139 0.6484755278 + 0.6829242706 0.6732050776 0.6492922902 + 0.6836668253 0.6739826202 0.6501089931 + 0.6844093800 0.6747599840 0.6509258151 + 0.6851518154 0.6755318046 0.6517397165 + 0.6858941913 0.6762999892 0.6525517702 + 0.6866366267 0.6770682931 0.6533638239 + 0.6873788834 0.6778365970 0.6541758180 + 0.6881213188 0.6786047816 0.6549878716 + 0.6888636947 0.6793730855 0.6557999253 + 0.6896060705 0.6801413894 0.6566119194 + 0.6903458238 0.6809049249 0.6574186087 + 0.6910849214 0.6816673875 0.6582239866 + 0.6918240786 0.6824299097 0.6590293050 + 0.6925632954 0.6831923723 0.6598346829 + 0.6933023930 0.6839548945 0.6606400013 + 0.6940416098 0.6847174168 0.6614453793 + 0.6947807074 0.6854798794 0.6622506976 + 0.6955140829 0.6862388253 0.6630463004 + 0.6962471008 0.6869974136 0.6638408899 + 0.6969801188 0.6877560019 0.6646354795 + 0.6977130771 0.6885147095 0.6654301882 + 0.6984460950 0.6892732978 0.6662247777 + 0.6991791129 0.6900318861 0.6670194268 + 0.6999120116 0.6907905936 0.6678140163 + 0.7006403208 0.6915383935 0.6686055064 + 0.7013683915 0.6922855973 0.6693965793 + 0.7020965219 0.6930328012 0.6701877713 + 0.7028244734 0.6937800050 0.6709790230 + 0.7035526037 0.6945272088 0.6717702150 + 0.7042806745 0.6952744126 0.6725614071 + 0.7050086856 0.6960216165 0.6733525991 + 0.7057327032 0.6967638135 0.6741454005 + 0.7064561248 0.6975051165 0.6749384999 + 0.7071794868 0.6982464194 0.6757315993 + 0.7079027891 0.6989877820 0.6765246987 + 0.7086262107 0.6997290850 0.6773177981 + 0.7093495727 0.7004703879 0.6781108975 + 0.7100729942 0.7012118101 0.6789039969 + 0.7107927203 0.7019538879 0.6796910167 + 0.7115108967 0.7026965022 0.6804748774 + 0.7122290134 0.7034389973 0.6812587976 + 0.7129471898 0.7041814923 0.6820427775 + 0.7136653066 0.7049241066 0.6828266978 + 0.7143834829 0.7056666017 0.6836106181 + 0.7151015997 0.7064090967 0.6843945980 + 0.7158182263 0.7071503997 0.6851742268 + 0.7165327072 0.7078899145 0.6859470010 + 0.7172471285 0.7086293101 0.6867198944 + 0.7179616094 0.7093688250 0.6874927282 + 0.7186759710 0.7101082206 0.6882656217 + 0.7193905115 0.7108476758 0.6890385151 + 0.7201048732 0.7115870714 0.6898112893 + 0.7208188772 0.7123262882 0.6905841231 + 0.7215241194 0.7130588889 0.6913529038 + 0.7222291827 0.7137916088 0.6921218038 + 0.7229344249 0.7145242095 0.6928905845 + 0.7236396074 0.7152568102 0.6936594248 + 0.7243446708 0.7159894109 0.6944282055 + 0.7250499129 0.7167220712 0.6951969862 + 0.7257549763 0.7174546719 0.6959658861 + 0.7264562845 0.7181851268 0.6967344284 + 0.7271547914 0.7189137936 0.6975026131 + 0.7278531790 0.7196425796 0.6982707977 + 0.7285516262 0.7203713059 0.6990389824 + 0.7292500734 0.7210999727 0.6998072267 + 0.7299485207 0.7218288183 0.7005754113 + 0.7306470275 0.7225574851 0.7013435960 + 0.7313454151 0.7232862115 0.7021117806 + 0.7320457101 0.7240095735 0.7028660774 + 0.7327460051 0.7247328758 0.7036200762 + 0.7334461808 0.7254562974 0.7043740153 + 0.7341464758 0.7261795998 0.7051280141 + 0.7348467708 0.7269030213 0.7058818936 + 0.7355470061 0.7276263237 0.7066358924 + 0.7362473011 0.7283496857 0.7073897719 + 0.7369453907 0.7290710211 0.7081403732 + 0.7376388907 0.7297881246 0.7088832259 + 0.7383323908 0.7305051088 0.7096260190 + 0.7390258908 0.7312222123 0.7103686929 + 0.7397195101 0.7319391966 0.7111114860 + 0.7404130101 0.7326561809 0.7118542194 + 0.7411065102 0.7333732843 0.7125970125 + 0.7418000102 0.7340903282 0.7133396864 + 0.7424902916 0.7348045707 0.7140802741 + 0.7431777716 0.7355164289 0.7148187757 + 0.7438653708 0.7362282276 0.7155572176 + 0.7445529103 0.7369400263 0.7162957191 + 0.7452403903 0.7376518250 0.7170342207 + 0.7459279895 0.7383636832 0.7177727222 + 0.7466155291 0.7390754819 0.7185112238 + 0.7473030090 0.7397872806 0.7192496061 + 0.7479922771 0.7404950857 0.7199897766 + 0.7486823797 0.7412006855 0.7207309008 + 0.7493724227 0.7419062853 0.7214720249 + 0.7500625253 0.7426118851 0.7222132087 + 0.7507526278 0.7433174849 0.7229542732 + 0.7514426708 0.7440230846 0.7236953974 + 0.7521327734 0.7447286844 0.7244365215 + 0.7528228760 0.7454342842 0.7251777053 + 0.7535089850 0.7461348772 0.7259193063 + 0.7541931868 0.7468327880 0.7266612053 + 0.7548773885 0.7475308180 0.7274032235 + 0.7555614710 0.7482287288 0.7281451225 + 0.7562456727 0.7489265800 0.7288870215 + 0.7569298744 0.7496244907 0.7296289802 + 0.7576140165 0.7503225207 0.7303708792 + 0.7582982183 0.7510203719 0.7311127782 + 0.7589793801 0.7517145872 0.7318496704 + 0.7596583962 0.7524058223 0.7325822115 + 0.7603374124 0.7530969977 0.7333146930 + 0.7610164285 0.7537881732 0.7340471148 + 0.7616953254 0.7544794083 0.7347795963 + 0.7623742819 0.7551705837 0.7355120778 + 0.7630532980 0.7558618188 0.7362444997 + 0.7637323141 0.7565529943 0.7369769812 + 0.7644116879 0.7572417259 0.7377079725 + 0.7650921941 0.7579252720 0.7384359241 + 0.7657725811 0.7586089969 0.7391638756 + 0.7664530277 0.7592927217 0.7398918271 + 0.7671334147 0.7599763274 0.7406197190 + 0.7678138018 0.7606599927 0.7413476110 + 0.7684941888 0.7613437176 0.7420756221 + 0.7691746950 0.7620273232 0.7428035140 + 0.7698547840 0.7627109885 0.7435314059 + 0.7705252171 0.7633913159 0.7442615032 + 0.7711957097 0.7640715837 0.7449914813 + 0.7718660831 0.7647519708 0.7457215190 + 0.7725365162 0.7654322982 0.7464514971 + 0.7732068896 0.7661126852 0.7471815944 + 0.7738773227 0.7667930126 0.7479115725 + 0.7745476961 0.7674732804 0.7486416101 + 0.7752181292 0.7681537271 0.7493717074 + 0.7758864760 0.7688329220 0.7501016855 + 0.7765535116 0.7695114017 0.7508317232 + 0.7772204876 0.7701898217 0.7515617013 + 0.7778875232 0.7708683014 0.7522916794 + 0.7785544991 0.7715467215 0.7530217767 + 0.7792214155 0.7722252011 0.7537518144 + 0.7798883915 0.7729036212 0.7544817924 + 0.7805554271 0.7735821009 0.7552117705 + 0.7812224030 0.7742605805 0.7559416890 + 0.7818884850 0.7749400735 0.7566617131 + 0.7825545073 0.7756196856 0.7573816776 + 0.7832205892 0.7762991786 0.7581015825 + 0.7838866711 0.7769787908 0.7588216066 + 0.7845528126 0.7776582837 0.7595415711 + 0.7852188945 0.7783377767 0.7602615952 + 0.7858849764 0.7790173888 0.7609816194 + 0.7865511179 0.7796968818 0.7617015839 + 0.7872148752 0.7803745866 0.7624198198 + 0.7878748178 0.7810488939 0.7631347179 + 0.7885347009 0.7817232013 0.7638494968 + 0.7891945839 0.7823975086 0.7645643950 + 0.7898545265 0.7830718756 0.7652792931 + 0.7905144095 0.7837461829 0.7659941912 + 0.7911742926 0.7844204903 0.7667090893 + 0.7918342948 0.7850949168 0.7674239278 + 0.7924941778 0.7857692242 0.7681388259 + 0.7931510806 0.7864413261 0.7688534856 + 0.7938057780 0.7871118188 0.7695679069 + 0.7944605947 0.7877823114 0.7702823877 + 0.7951152921 0.7884528041 0.7709968090 + 0.7957701087 0.7891234159 0.7717112899 + 0.7964249253 0.7897939086 0.7724257112 + 0.7970796227 0.7904644012 0.7731400728 + 0.7977343798 0.7911348939 0.7738546133 + 0.7983890772 0.7918053865 0.7745689750 + 0.7990390062 0.7924752831 0.7752832174 + 0.7996864915 0.7931450009 0.7759972811 + 0.8003339767 0.7938145995 0.7767112851 + 0.8009815216 0.7944843173 0.7774252892 + 0.8016290069 0.7951539159 0.7781392932 + 0.8022764921 0.7958235741 0.7788534164 + 0.8029239774 0.7964931726 0.7795674205 + 0.8035715222 0.7971628904 0.7802814245 + 0.8042190075 0.7978324890 0.7809954882 + 0.8048598766 0.7984976172 0.7817047834 + 0.8054972887 0.7991601825 0.7824113965 + 0.8061345816 0.7998228073 0.7831178904 + 0.8067719936 0.8004853725 0.7838245034 + 0.8074094057 0.8011479974 0.7845311165 + 0.8080468178 0.8018106222 0.7852376103 + 0.8086841106 0.8024731874 0.7859442234 + 0.8093215227 0.8031358123 0.7866507769 + 0.8099588752 0.8037983775 0.7873573899 + 0.8105943203 0.8044582009 0.7880617976 + 0.8112279177 0.8051152229 0.7887639999 + 0.8118613958 0.8057723045 0.7894663215 + 0.8124949932 0.8064293265 0.7901685238 + 0.8131284714 0.8070862889 0.7908707261 + 0.8137621284 0.8077433109 0.7915729880 + 0.8143956065 0.8084002733 0.7922751904 + 0.8150292039 0.8090572953 0.7929773927 + 0.8156626821 0.8097143769 0.7936797142 + 0.8162947893 0.8103706837 0.7943819761 + 0.8169224262 0.8110250235 0.7950844765 + 0.8175500035 0.8116793036 0.7957870960 + 0.8181775808 0.8123335838 0.7964897156 + 0.8188052177 0.8129879236 0.7971922755 + 0.8194326758 0.8136422038 0.7978948951 + 0.8200603127 0.8142964244 0.7985973954 + 0.8206878901 0.8149507046 0.7993000150 + 0.8213155270 0.8156049848 0.8000025749 + 0.8219431043 0.8162593246 0.8007051945 + 0.8225635290 0.8169105053 0.8014042974 + 0.8231825829 0.8175610900 0.8021026254 + 0.8238015771 0.8182116747 0.8028008938 + 0.8244206905 0.8188623190 0.8034992814 + 0.8250398040 0.8195129037 0.8041976094 + 0.8256589174 0.8201634884 0.8048958778 + 0.8262779117 0.8208140731 0.8055942059 + 0.8268970251 0.8214647174 0.8062925935 + 0.8275160789 0.8221153021 0.8069909215 + 0.8281350136 0.8227658272 0.8076866269 + 0.8287534714 0.8234161139 0.8083760738 + 0.8293719888 0.8240664005 0.8090655804 + 0.8299906254 0.8247166872 0.8097550869 + 0.8306090832 0.8253669739 0.8104445934 + 0.8312276006 0.8260173798 0.8111341000 + 0.8318461776 0.8266677260 0.8118236065 + 0.8324646950 0.8273180127 0.8125131130 + 0.8330832124 0.8279682994 0.8132026196 + 0.8337017298 0.8286185861 0.8138921261 + 0.8343204260 0.8292688131 0.8145787716 + 0.8349391818 0.8299189210 0.8152639866 + 0.8355578780 0.8305690289 0.8159490824 + 0.8361766934 0.8312191963 0.8166342974 + 0.8367953897 0.8318693042 0.8173193932 + 0.8374142051 0.8325194716 0.8180044889 + 0.8380329013 0.8331695795 0.8186897039 + 0.8386517167 0.8338196874 0.8193747997 + 0.8392704129 0.8344699144 0.8200598955 + 0.8398892283 0.8351200223 0.8207451105 + 0.8405013084 0.8357700706 0.8214318156 + 0.8411123753 0.8364201188 0.8221188188 + 0.8417235017 0.8370702267 0.8228058219 + 0.8423346281 0.8377202749 0.8234927058 + 0.8429456949 0.8383703232 0.8241797090 + 0.8435568213 0.8390203714 0.8248667121 + 0.8441678882 0.8396704197 0.8255537152 + 0.8447790146 0.8403205276 0.8262405992 + 0.8453900814 0.8409705162 0.8269276023 + 0.8460012078 0.8416206241 0.8276146054 + 0.8466123939 0.8422645926 0.8282997012 + 0.8472235799 0.8429083228 0.8289846778 + 0.8478348255 0.8435521126 0.8296697140 + 0.8484458923 0.8441957831 0.8303546906 + 0.8490570784 0.8448395133 0.8310397267 + 0.8496683240 0.8454831839 0.8317247033 + 0.8502795100 0.8461269736 0.8324096799 + 0.8508906960 0.8467707038 0.8330947161 + 0.8515018821 0.8474143744 0.8337796926 + 0.8521130085 0.8480581045 0.8344647288 + 0.8527246714 0.8486986756 0.8351467848 + 0.8533365130 0.8493388295 0.8358284831 + 0.8539482951 0.8499789834 0.8365101814 + 0.8545600176 0.8506191969 0.8371918797 + 0.8551717997 0.8512594104 0.8378735781 + 0.8557835817 0.8518995047 0.8385552764 + 0.8563953042 0.8525397182 0.8392369151 + 0.8570070863 0.8531798720 0.8399186134 + 0.8576189280 0.8538200259 0.8406003118 + 0.8582305908 0.8544601798 0.8412820101 + 0.8588374257 0.8550989032 0.8419591784 + 0.8594421744 0.8557369709 0.8426344991 + 0.8600469828 0.8563752174 0.8433098197 + 0.8606517911 0.8570132852 0.8439850211 + 0.8612565994 0.8576514125 0.8446602821 + 0.8618612885 0.8582894802 0.8453356028 + 0.8624660969 0.8589276075 0.8460108042 + 0.8630709052 0.8595656753 0.8466861248 + 0.8636757135 0.8602039218 0.8473613858 + 0.8642805219 0.8608419895 0.8480365872 + 0.8648855090 0.8614770174 0.8487117887 + 0.8654909730 0.8621069193 0.8493868113 + 0.8660963774 0.8627368212 0.8500617146 + 0.8667019010 0.8633667231 0.8507366776 + 0.8673073053 0.8639966249 0.8514117002 + 0.8679128289 0.8646265268 0.8520867229 + 0.8685181737 0.8652564287 0.8527616262 + 0.8691236973 0.8658862710 0.8534365892 + 0.8697291017 0.8665162921 0.8541116118 + 0.8703346252 0.8671461940 0.8547865152 + 0.8709400296 0.8677760959 0.8554614782 + 0.8715463281 0.8684011102 0.8561388254 + 0.8721526861 0.8690257072 0.8568162918 + 0.8727591038 0.8696503043 0.8574938178 + 0.8733655214 0.8702749014 0.8581712842 + 0.8739719987 0.8708994985 0.8588488102 + 0.8745784163 0.8715240955 0.8595262766 + 0.8751847744 0.8721486926 0.8602038026 + 0.8757911921 0.8727732897 0.8608813286 + 0.8763976097 0.8733978868 0.8615587950 + 0.8770040274 0.8740224242 0.8622363210 + 0.8776084781 0.8746458292 0.8629111052 + 0.8782086968 0.8752664924 0.8635792732 + 0.8788089752 0.8758872151 0.8642475009 + 0.8794091940 0.8765078187 0.8649157286 + 0.8800094724 0.8771284819 0.8655838966 + 0.8806096911 0.8777492046 0.8662521243 + 0.8812100291 0.8783698082 0.8669202924 + 0.8818103075 0.8789904714 0.8675885201 + 0.8824105263 0.8796111941 0.8682566881 + 0.8830108047 0.8802317977 0.8689249158 + 0.8836110234 0.8808525205 0.8695930839 + 0.8842111826 0.8814677000 0.8702577949 + 0.8848112822 0.8820785880 0.8709195852 + 0.8854113817 0.8826894164 0.8715813160 + 0.8860116005 0.8833001852 0.8722429872 + 0.8866117001 0.8839110136 0.8729047775 + 0.8872117996 0.8845217824 0.8735665083 + 0.8878118992 0.8851326108 0.8742281795 + 0.8884119987 0.8857433796 0.8748900294 + 0.8890120983 0.8863543272 0.8755517006 + 0.8896123171 0.8869650960 0.8762133718 + 0.8902124166 0.8875759244 0.8768752217 + 0.8908076882 0.8881832957 0.8775365949 + 0.8914005160 0.8887888193 0.8781977892 + 0.8919934034 0.8893944025 0.8788589835 + 0.8925861716 0.8899999857 0.8795201778 + 0.8931789994 0.8906055093 0.8801813722 + 0.8937718272 0.8912110925 0.8808426261 + 0.8943647146 0.8918166757 0.8815038204 + 0.8949574828 0.8924221992 0.8821650147 + 0.8955503106 0.8930277824 0.8828263283 + 0.8961430788 0.8936333060 0.8834875226 + 0.8967359066 0.8942388892 0.8841487169 + 0.8973246217 0.8948436975 0.8848111033 + 0.8979104757 0.8954480290 0.8854743838 + 0.8984965086 0.8960524201 0.8861377835 + 0.8990824223 0.8966566920 0.8868011236 + 0.8996682763 0.8972610235 0.8874644041 + 0.9002543092 0.8978654146 0.8881276846 + 0.9008402228 0.8984696865 0.8887910247 + 0.9014261961 0.8990740180 0.8894543052 + 0.9020121098 0.8996782899 0.8901175857 + 0.9025980234 0.9002826810 0.8907809258 + 0.9031839967 0.9008870125 0.8914443254 + 0.9037700891 0.9014902115 0.8921037912 + 0.9043565989 0.9020918012 0.8927577138 + 0.9049429893 0.9026933908 0.8934116960 + 0.9055293798 0.9032949805 0.8940656185 + 0.9061157703 0.9038965702 0.8947194815 + 0.9067022800 0.9044982195 0.8953734040 + 0.9072886705 0.9050998092 0.8960273266 + 0.9078751206 0.9057013988 0.8966811895 + 0.9084615707 0.9063029885 0.8973351121 + 0.9090480208 0.9069045782 0.8979889750 + 0.9096344113 0.9075062275 0.8986428976 + 0.9102206826 0.9081075191 0.8992965221 + 0.9108051062 0.9087041020 0.8999441266 + 0.9113894105 0.9093006849 0.9005916119 + 0.9119737744 0.9098973274 0.9012390971 + 0.9125580788 0.9104939103 0.9018865824 + 0.9131425023 0.9110904932 0.9025341868 + 0.9137269258 0.9116870761 0.9031816721 + 0.9143111706 0.9122837186 0.9038292170 + 0.9148955941 0.9128803015 0.9044768214 + 0.9154798985 0.9134768844 0.9051243067 + 0.9160643220 0.9140735269 0.9057717919 + 0.9166486263 0.9146699905 0.9064192772 + 0.9172297716 0.9152629972 0.9070658088 + 0.9178082943 0.9158529043 0.9077115059 + 0.9183868766 0.9164428711 0.9083572030 + 0.9189653993 0.9170327783 0.9090027809 + 0.9195439816 0.9176228046 0.9096484780 + 0.9201225042 0.9182127118 0.9102941155 + 0.9207010865 0.9188026786 0.9109398127 + 0.9212796092 0.9193927050 0.9115853906 + 0.9218580723 0.9199826121 0.9122310877 + 0.9224367142 0.9205725789 0.9128767252 + 0.9230151772 0.9211624861 0.9135224223 + 0.9235938191 0.9217525125 0.9141680002 + 0.9241641760 0.9223377109 0.9148079753 + 0.9247336984 0.9229223728 0.9154471159 + 0.9253032207 0.9235070944 0.9160863161 + 0.9258726239 0.9240918159 0.9167255163 + 0.9264420867 0.9246764183 0.9173647165 + 0.9270116091 0.9252610803 0.9180039167 + 0.9275810719 0.9258458018 0.9186431170 + 0.9281504750 0.9264305234 0.9192823172 + 0.9287199974 0.9270151854 0.9199213982 + 0.9292895198 0.9275997877 0.9205605984 + 0.9298589826 0.9281845093 0.9211997986 + 0.9304273725 0.9287682176 0.9218379855 + 0.9309837222 0.9293403029 0.9224632978 + 0.9315400124 0.9299123883 0.9230884910 + 0.9320963025 0.9304844737 0.9237136841 + 0.9326524734 0.9310566187 0.9243389964 + 0.9332088232 0.9316285849 0.9249641895 + 0.9337651134 0.9322006702 0.9255895019 + 0.9343214035 0.9327728152 0.9262146950 + 0.9348776937 0.9333449006 0.9268398881 + 0.9354339242 0.9339169860 0.9274652004 + 0.9359902143 0.9344890118 0.9280903935 + 0.9365465045 0.9350610971 0.9287157059 + 0.9371017814 0.9356322885 0.9293395877 + 0.9376482964 0.9361947179 0.9299502969 + 0.9381946921 0.9367572069 0.9305610061 + 0.9387412071 0.9373196959 0.9311717153 + 0.9392877221 0.9378821254 0.9317824244 + 0.9398341775 0.9384446144 0.9323930740 + 0.9403805733 0.9390071034 0.9330037832 + 0.9409270883 0.9395694733 0.9336146116 + 0.9414736032 0.9401320219 0.9342253208 + 0.9420201182 0.9406945109 0.9348359704 + 0.9425665140 0.9412570000 0.9354466796 + 0.9431130290 0.9418194294 0.9360573888 + 0.9436594844 0.9423819184 0.9366680980 + 0.9441972971 0.9429398775 0.9372712970 + 0.9447346926 0.9434977174 0.9378741980 + 0.9452720881 0.9440554976 0.9384769797 + 0.9458096027 0.9446132779 0.9390798807 + 0.9463469982 0.9451711178 0.9396827221 + 0.9468843937 0.9457287788 0.9402856231 + 0.9474219084 0.9462866187 0.9408884048 + 0.9479593039 0.9468443990 0.9414913058 + 0.9484966993 0.9474021792 0.9420940876 + 0.9490342140 0.9479600191 0.9426969290 + 0.9495716095 0.9485177994 0.9432997704 + 0.9501090050 0.9490755796 0.9439026117 + 0.9506387115 0.9496260285 0.9444972873 + 0.9511643052 0.9501724839 0.9450876117 + 0.9516900182 0.9507189989 0.9456778765 + 0.9522156119 0.9512655139 0.9462682009 + 0.9527412057 0.9518120289 0.9468585253 + 0.9532667994 0.9523584843 0.9474487901 + 0.9537923932 0.9529049993 0.9480391145 + 0.9543179870 0.9534515142 0.9486293197 + 0.9548435807 0.9539980292 0.9492195845 + 0.9553691745 0.9545444846 0.9498099089 + 0.9558948278 0.9550909996 0.9504001737 + 0.9564204216 0.9556375146 0.9509904981 + 0.9569442272 0.9561818838 0.9515780210 + 0.9574593902 0.9567170143 0.9521524906 + 0.9579746127 0.9572520852 0.9527270198 + 0.9584897757 0.9577872157 0.9533016086 + 0.9590049982 0.9583222866 0.9538760781 + 0.9595202208 0.9588574171 0.9544506073 + 0.9600353837 0.9593924880 0.9550250769 + 0.9605506063 0.9599276185 0.9555996060 + 0.9610658288 0.9604626298 0.9561740756 + 0.9615809917 0.9609977007 0.9567486048 + 0.9620962143 0.9615327716 0.9573231936 + 0.9626114964 0.9620679021 0.9578977227 + 0.9631267190 0.9626029730 0.9584721923 + 0.9636358023 0.9631323218 0.9590396285 + 0.9641396999 0.9636566043 0.9596006870 + 0.9646437168 0.9641808867 0.9601618052 + 0.9651476145 0.9647052288 0.9607229829 + 0.9656515121 0.9652295113 0.9612841010 + 0.9661555290 0.9657537937 0.9618452787 + 0.9666594267 0.9662780762 0.9624063969 + 0.9671633244 0.9668024182 0.9629675150 + 0.9676672816 0.9673267007 0.9635286927 + 0.9681711793 0.9678509831 0.9640898108 + 0.9686750770 0.9683753252 0.9646509886 + 0.9691790938 0.9688996077 0.9652121067 + 0.9696829915 0.9694238901 0.9657732248 + 0.9701765180 0.9699373245 0.9663273096 + 0.9706659913 0.9704464078 0.9668784738 + 0.9711555243 0.9709556103 0.9674296975 + 0.9716449976 0.9714648128 0.9679809809 + 0.9721345901 0.9719740152 0.9685322046 + 0.9726241231 0.9724830985 0.9690834880 + 0.9731135964 0.9729923010 0.9696347117 + 0.9736031294 0.9735015035 0.9701858759 + 0.9740926027 0.9740107059 0.9707372189 + 0.9745820761 0.9745197892 0.9712883830 + 0.9750717282 0.9750289917 0.9718397260 + 0.9755612016 0.9755381942 0.9723908901 + 0.9760506749 0.9760473967 0.9729421139 + 0.9765306115 0.9765431881 0.9734861851 + 0.9770072103 0.9770342112 0.9740275145 + 0.9774836898 0.9775252938 0.9745689034 + 0.9779602885 0.9780163765 0.9751102924 + 0.9784368277 0.9785073996 0.9756516814 + 0.9789134264 0.9789984822 0.9761930704 + 0.9793899059 0.9794895053 0.9767343998 + 0.9798665047 0.9799805880 0.9772757888 + 0.9803431034 0.9804716110 0.9778171778 + 0.9808195829 0.9809626937 0.9783586264 + 0.9812961817 0.9814537764 0.9789000154 + 0.9817727208 0.9819447994 0.9794412851 + 0.9822493196 0.9824358821 0.9799826741 + 0.9827123284 0.9829128981 0.9805108905 + 0.9831652045 0.9833794236 0.9810290933 + 0.9836181998 0.9838458896 0.9815472960 + 0.9840711951 0.9843124151 0.9820654988 + 0.9845240712 0.9847788811 0.9825838208 + 0.9849771261 0.9852454066 0.9831020236 + 0.9854301214 0.9857119918 0.9836202264 + 0.9858829975 0.9861785173 0.9841384292 + 0.9863359928 0.9866449833 0.9846565723 + 0.9867889881 0.9871115088 0.9851748943 + 0.9872419238 0.9875779748 0.9856930971 + 0.9876949191 0.9880445004 0.9862112999 + 0.9881477952 0.9885110855 0.9867295027 + 0.9885957837 0.9889689088 0.9872413278 + 0.9890270829 0.9893978834 0.9877312779 + 0.9894583821 0.9898269176 0.9882212877 + 0.9898896813 0.9902558923 0.9887112975 + 0.9903209805 0.9906849265 0.9892013073 + 0.9907522798 0.9911139011 0.9896913171 + 0.9911835790 0.9915428758 0.9901813269 + 0.9916148782 0.9919719100 0.9906713963 + 0.9920461774 0.9924008846 0.9911614060 + 0.9924774766 0.9928299189 0.9916514158 + 0.9929087758 0.9932588935 0.9921414256 + 0.9933400750 0.9936879277 0.9926313758 + 0.9937713742 0.9941169024 0.9931213856 + 0.9942026734 0.9945458770 0.9936113954 + 0.9946215749 0.9949464798 0.9940770864 + 0.9950352907 0.9953352213 0.9945327044 + 0.9954490066 0.9957239032 0.9949883223 + 0.9958627224 0.9961127043 0.9954438806 + 0.9962764978 0.9965013862 0.9958994985 + 0.9966902137 0.9968901277 0.9963551760 + 0.9971039295 0.9972789288 0.9968107939 + 0.9975175858 0.9976676106 0.9972664118 + 0.9979314208 0.9980562925 0.9977220297 + 0.9983450770 0.9984450936 0.9981775880 + 0.9987587929 0.9988337755 0.9986332059 + 0.9991725087 0.9992225170 0.9990888238 + 0.9995862842 0.9996113181 0.9995443821 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_160vc.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_160vc.spi1d index 1025144f..1c655c58 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_160vc.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_160vc.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0077699851 0.0072091222 0.0057455739 - 0.0153281903 0.0140618198 0.0110133700 - 0.0220492706 0.0201340895 0.0156183802 - 0.0279948898 0.0255677104 0.0198130608 - 0.0334264189 0.0305160508 0.0236329101 - 0.0383813009 0.0350662991 0.0272729397 - 0.0430751182 0.0393643305 0.0306853205 - 0.0474476218 0.0434310399 0.0339677408 - 0.0515641496 0.0472853705 0.0371497907 - 0.0555446185 0.0509948209 0.0402266905 - 0.0594275706 0.0545908697 0.0432020612 - 0.0631866604 0.0580725186 0.0460699908 - 0.0668074489 0.0614085086 0.0488363616 - 0.0702805966 0.0646550804 0.0515515506 - 0.0736529976 0.0678500831 0.0542215705 - 0.0769146830 0.0709684193 0.0568457693 - 0.0801084563 0.0739683583 0.0594256707 - 0.0832033977 0.0768856406 0.0619675815 - 0.0862210765 0.0797707886 0.0644541681 - 0.0891883075 0.0826155171 0.0668781176 - 0.0921097621 0.0853902176 0.0692649484 - 0.0950033367 0.0881099030 0.0716069788 - 0.0978709385 0.0907903165 0.0739388466 - 0.1006767005 0.0934374928 0.0762372017 - 0.1034490988 0.0960375518 0.0785102323 - 0.1061699986 0.0985936001 0.0807417631 - 0.1088621020 0.1011143997 0.0829524100 - 0.1115100011 0.1035860032 0.0851420909 - 0.1141035035 0.1060298979 0.0873048082 - 0.1166575998 0.1084465012 0.0894576833 - 0.1191660017 0.1108224019 0.0915750787 - 0.1216190010 0.1131646037 0.0936675370 - 0.1240210012 0.1154721975 0.0957391188 - 0.1263785958 0.1177325025 0.0977701619 - 0.1287032962 0.1199773997 0.0997919589 - 0.1309965998 0.1222233996 0.1018017009 - 0.1332729012 0.1244523004 0.1037892997 - 0.1355164945 0.1266828030 0.1057759970 - 0.1377673000 0.1288989931 0.1077485010 - 0.1400064975 0.1310786009 0.1096967012 - 0.1422519982 0.1332331002 0.1116394028 - 0.1444955021 0.1353804022 0.1135682017 - 0.1467241049 0.1374987066 0.1154882982 - 0.1489524990 0.1396068931 0.1173892990 - 0.1511590034 0.1416790038 0.1192753017 - 0.1533526033 0.1437290013 0.1211493984 - 0.1555102021 0.1457712948 0.1229984984 - 0.1576402932 0.1477762014 0.1248319969 - 0.1597459018 0.1497581005 0.1266517043 - 0.1618217975 0.1517187059 0.1284562945 - 0.1638692021 0.1536567956 0.1302371025 - 0.1658921987 0.1555901021 0.1320014000 - 0.1678850949 0.1574811935 0.1337444037 - 0.1698528975 0.1593548954 0.1354825944 - 0.1718069017 0.1612353027 0.1372071058 - 0.1737360060 0.1631090939 0.1389127970 - 0.1756428927 0.1649706960 0.1406110972 - 0.1775456071 0.1668394953 0.1423041970 - 0.1794250011 0.1687040031 0.1439954042 - 0.1812936962 0.1705541015 0.1456899941 - 0.1831597984 0.1724057049 0.1473833025 - 0.1850039065 0.1742419004 0.1490696967 - 0.1868477017 0.1760638058 0.1507641971 - 0.1886914074 0.1778782010 0.1524640024 - 0.1905229986 0.1796796024 0.1541469991 - 0.1923421025 0.1814686954 0.1558177024 - 0.1941601038 0.1832399964 0.1574839056 - 0.1959781945 0.1850111932 0.1591470987 - 0.1978068054 0.1867780983 0.1608041972 - 0.1996330023 0.1885201037 0.1624432057 - 0.2014473975 0.1902448982 0.1640713960 - 0.2032514960 0.1919703931 0.1656913012 - 0.2050576061 0.1936956942 0.1672880948 - 0.2068455070 0.1953990012 0.1688698977 - 0.2086115927 0.1970870048 0.1704351008 - 0.2103770971 0.1987718940 0.1719868034 - 0.2121285051 0.2004460990 0.1735249013 - 0.2138638943 0.2020982951 0.1750528961 - 0.2155895978 0.2037346959 0.1765847057 - 0.2173081934 0.2053682953 0.1781219989 - 0.2190068960 0.2069963962 0.1796503961 - 0.2206840962 0.2086153030 0.1811677068 - 0.2223511040 0.2102248967 0.1826826036 - 0.2240124047 0.2118259966 0.1841962934 - 0.2256612033 0.2134266943 0.1857064962 - 0.2272907943 0.2150333971 0.1872134954 - 0.2289057970 0.2166189998 0.1887131035 - 0.2305078059 0.2181905061 0.1902053952 - 0.2321005017 0.2197569013 0.1916909963 - 0.2336920053 0.2213245928 0.1931720972 - 0.2352899015 0.2228953987 0.1946519017 - 0.2368838936 0.2244486064 0.1961286962 - 0.2384622991 0.2259978950 0.1976062059 - 0.2400310934 0.2275449038 0.1990849972 - 0.2415942997 0.2290931940 0.2005598992 - 0.2431554943 0.2306476980 0.2020221055 - 0.2447042018 0.2321884036 0.2034724057 - 0.2462362051 0.2337166965 0.2049137056 - 0.2477615029 0.2352366000 0.2063475996 - 0.2492882013 0.2367514074 0.2077756971 - 0.2508074939 0.2382549047 0.2092022002 - 0.2523134053 0.2397526950 0.2106287032 - 0.2538112998 0.2412422001 0.2120527029 - 0.2553072870 0.2427164018 0.2134691030 - 0.2568025887 0.2441851944 0.2148848027 - 0.2582916915 0.2456547022 0.2162963003 - 0.2597728968 0.2471264005 0.2177021056 - 0.2612324953 0.2485917062 0.2190997005 - 0.2626926899 0.2500481904 0.2204964012 - 0.2641533017 0.2514964044 0.2218922973 - 0.2656058967 0.2529383004 0.2232878953 - 0.2670469880 0.2543821037 0.2246783972 - 0.2684782147 0.2558273077 0.2260642052 - 0.2699055076 0.2572557926 0.2274404019 - 0.2713302076 0.2586747110 0.2288105041 - 0.2727521956 0.2600823939 0.2301730961 - 0.2741548121 0.2614834905 0.2315272987 - 0.2755548060 0.2628856003 0.2328795046 - 0.2769533098 0.2642917037 0.2342298031 - 0.2783550918 0.2656919062 0.2355791926 - 0.2797575891 0.2670885921 0.2369273007 - 0.2811529040 0.2684741020 0.2382656932 - 0.2825410068 0.2698574066 0.2396034002 - 0.2839199007 0.2712377012 0.2409401983 - 0.2853043079 0.2726213932 0.2422693074 - 0.2866902053 0.2740063071 0.2435951978 - 0.2880729139 0.2753944993 0.2449122965 - 0.2894502878 0.2767789960 0.2462285012 - 0.2908177972 0.2781563997 0.2475429028 - 0.2921772897 0.2795270979 0.2488564998 - 0.2935316861 0.2808933854 0.2501694858 - 0.2948884964 0.2822520137 0.2514741123 - 0.2962454855 0.2836092114 0.2527768016 - 0.2975969017 0.2849698961 0.2540780902 - 0.2989454865 0.2863264978 0.2553772032 - 0.3002853096 0.2876687944 0.2566682100 - 0.3016225100 0.2890065014 0.2579559088 - 0.3029560149 0.2903367877 0.2592380047 - 0.3042886853 0.2916674018 0.2605186999 - 0.3056207895 0.2929981947 0.2617981136 - 0.3069526851 0.2943269908 0.2630766928 - 0.3082844913 0.2956545949 0.2643547952 - 0.3096182942 0.2969661951 0.2656278014 - 0.3109529912 0.2982715070 0.2668986917 - 0.3122856021 0.2995666862 0.2681745887 - 0.3136177063 0.3008590043 0.2694520056 - 0.3149411976 0.3021504879 0.2707208097 - 0.3162628114 0.3034417927 0.2719872892 - 0.3175767064 0.3047347069 0.2732458115 - 0.3188889921 0.3060280085 0.2745023072 - 0.3202066123 0.3073163033 0.2757557929 - 0.3215256035 0.3086032867 0.2770085931 - 0.3228304088 0.3098782003 0.2782612145 - 0.3241305947 0.3111487925 0.2795138955 - 0.3254284859 0.3124164939 0.2807643116 - 0.3267253041 0.3136827946 0.2820135057 - 0.3280225098 0.3149532080 0.2832610905 - 0.3293201029 0.3162268996 0.2845073938 - 0.3306170106 0.3174982071 0.2857488990 - 0.3319132030 0.3187662959 0.2869836092 - 0.3332048953 0.3200294971 0.2882154882 - 0.3344849050 0.3212794960 0.2894394994 - 0.3357647061 0.3225289881 0.2906630933 - 0.3370434046 0.3237726986 0.2918823063 - 0.3383221924 0.3250164092 0.2931014895 - 0.3395951092 0.3262596130 0.2943179011 - 0.3408671021 0.3275027871 0.2955336869 - 0.3421292007 0.3287386000 0.2967428863 - 0.3433851898 0.3299694955 0.2979474962 - 0.3446362913 0.3311983943 0.2991509140 - 0.3458774090 0.3324227929 0.3003517985 - 0.3471182883 0.3336473107 0.3015527129 - 0.3483507931 0.3348652124 0.3027420044 - 0.3495832980 0.3360829949 0.3039312065 - 0.3508102000 0.3373039961 0.3051137030 - 0.3520343006 0.3385266066 0.3062925041 - 0.3532547951 0.3397471011 0.3074702024 - 0.3544664085 0.3409624100 0.3086448014 - 0.3556779921 0.3421775997 0.3098194897 - 0.3568766117 0.3433755934 0.3109912872 - 0.3580735922 0.3445712030 0.3121627867 - 0.3592629135 0.3457629979 0.3133305013 - 0.3604426980 0.3469501138 0.3144932091 - 0.3616225123 0.3481371999 0.3156560063 - 0.3627896905 0.3493269086 0.3168224096 - 0.3639565110 0.3505167067 0.3179889917 - 0.3651241064 0.3517034054 0.3191502094 - 0.3662925065 0.3528864980 0.3203046918 - 0.3674609065 0.3540695012 0.3214592934 - 0.3686209023 0.3552398086 0.3226110041 - 0.3697803020 0.3564088047 0.3237625062 - 0.3709327877 0.3575739861 0.3249132931 - 0.3720724881 0.3587315083 0.3260628879 - 0.3732121885 0.3598890007 0.3272125125 - 0.3743419051 0.3610461056 0.3283613026 - 0.3754678965 0.3622029126 0.3295099139 - 0.3765943944 0.3633604050 0.3306581080 - 0.3777253032 0.3645246029 0.3318021894 - 0.3788562119 0.3656888902 0.3329462111 - 0.3799856901 0.3668493927 0.3340887129 - 0.3811131120 0.3680044115 0.3352287114 - 0.3822405040 0.3691594899 0.3363687098 - 0.3833586872 0.3703058958 0.3375062048 - 0.3844727874 0.3714483976 0.3386425078 - 0.3855867982 0.3725908995 0.3397788107 - 0.3867011964 0.3737317026 0.3409082890 - 0.3878157139 0.3748725057 0.3420371115 - 0.3889299929 0.3760136962 0.3431653976 - 0.3900434077 0.3771579862 0.3442898989 - 0.3911567926 0.3783023059 0.3454144001 - 0.3922654092 0.3794414103 0.3465386033 - 0.3933632076 0.3805679977 0.3476622999 - 0.3944610059 0.3816947043 0.3487859070 - 0.3955565989 0.3828161955 0.3499088883 - 0.3966492116 0.3839308023 0.3510310948 - 0.3977417946 0.3850454092 0.3521533012 - 0.3988339901 0.3861593008 0.3532767892 - 0.3999255896 0.3872723877 0.3544014096 - 0.4010173082 0.3883855045 0.3555260003 - 0.4021055102 0.3894925117 0.3566437066 - 0.4031910896 0.3905951083 0.3577559888 - 0.4042766988 0.3916975856 0.3588683009 - 0.4053548872 0.3927960992 0.3599740863 - 0.4064280987 0.3938918114 0.3610751033 - 0.4075013101 0.3949874938 0.3621760905 - 0.4085747004 0.3960804939 0.3632729053 - 0.4096482992 0.3971712887 0.3643661141 - 0.4107218087 0.3982622027 0.3654592037 - 0.4117960930 0.3993507028 0.3665483892 - 0.4128710926 0.4004369974 0.3676328957 - 0.4139460921 0.4015232921 0.3687174916 - 0.4150182009 0.4026117027 0.3698010147 - 0.4160853922 0.4037036896 0.3708826005 - 0.4171527028 0.4047957063 0.3719643056 - 0.4182184935 0.4058865011 0.3730461895 - 0.4192796946 0.4069728851 0.3741292059 - 0.4203408957 0.4080593884 0.3752123117 - 0.4214020073 0.4091454148 0.3762952089 - 0.4224613011 0.4102149904 0.3773654103 - 0.4235205054 0.4112845063 0.3784357011 - 0.4245797992 0.4123539925 0.3795059025 - 0.4256359935 0.4134182930 0.3805727065 - 0.4266915023 0.4144811928 0.3816384077 - 0.4277468920 0.4155440927 0.3827041090 - 0.4287993014 0.4166063070 0.3837668896 - 0.4298486114 0.4176676869 0.3848268092 - 0.4308980107 0.4187290967 0.3858866096 - 0.4319469929 0.4197882116 0.3869450092 - 0.4329943061 0.4208357930 0.3879956901 - 0.4340417087 0.4218834043 0.3890464008 - 0.4350889921 0.4229309857 0.3900969923 - 0.4361298084 0.4239682853 0.3911440074 - 0.4371689856 0.4250029922 0.3921900094 - 0.4382081926 0.4260376990 0.3932358921 - 0.4392425120 0.4270702899 0.3942803144 - 0.4402691126 0.4280996025 0.3953221142 - 0.4412955940 0.4291290045 0.3963639140 - 0.4423221946 0.4301582873 0.3974055946 - 0.4433423877 0.4311893880 0.3984431028 - 0.4443620145 0.4322207868 0.3994801044 - 0.4453817010 0.4332520962 0.4005171955 - 0.4464051127 0.4342832863 0.4015508890 - 0.4474340975 0.4353140891 0.4025794864 - 0.4484629929 0.4363448918 0.4036081135 - 0.4494918883 0.4373758137 0.4046367109 - 0.4505175948 0.4384073019 0.4056606889 - 0.4515425861 0.4394389093 0.4066838920 - 0.4525676072 0.4404704869 0.4077070057 - 0.4535906017 0.4415003955 0.4087300897 - 0.4546070099 0.4425247014 0.4097532928 - 0.4556233883 0.4435490072 0.4107764065 - 0.4566397965 0.4445734024 0.4117994905 - 0.4576533139 0.4455991089 0.4128246903 - 0.4586645961 0.4466257989 0.4138514996 - 0.4596759081 0.4476524889 0.4148781896 - 0.4606871903 0.4486792088 0.4159049988 - 0.4616912901 0.4497005045 0.4169327021 - 0.4626944065 0.4507211149 0.4179604948 - 0.4636974931 0.4517416060 0.4189884067 - 0.4646998942 0.4527609050 0.4200156033 - 0.4656972885 0.4537692964 0.4210371971 - 0.4666947126 0.4547776878 0.4220587909 - 0.4676921070 0.4557861090 0.4230804145 - 0.4686877131 0.4567916095 0.4241015017 - 0.4696792960 0.4577902853 0.4251210093 - 0.4706709981 0.4587889910 0.4261406064 - 0.4716626108 0.4597877860 0.4271601140 - 0.4726535082 0.4607864022 0.4281783998 - 0.4736435115 0.4617848992 0.4291948974 - 0.4746335149 0.4627833962 0.4302113950 - 0.4756234884 0.4637818933 0.4312278926 - 0.4766106009 0.4647786915 0.4322431087 - 0.4775952101 0.4657739997 0.4332571924 - 0.4785799086 0.4667693079 0.4342711866 - 0.4795644879 0.4677644968 0.4352853000 - 0.4805473983 0.4687533975 0.4362984002 - 0.4815289080 0.4697372913 0.4373108149 - 0.4825105071 0.4707211852 0.4383231103 - 0.4834921062 0.4717051089 0.4393354952 - 0.4844721854 0.4726847112 0.4403477013 - 0.4854511917 0.4736607075 0.4413597882 - 0.4864301980 0.4746366143 0.4423719049 - 0.4874091148 0.4756126106 0.4433839917 - 0.4883851111 0.4765883088 0.4443916976 - 0.4893577993 0.4775637090 0.4453943074 - 0.4903304875 0.4785391092 0.4463968873 - 0.4913032055 0.4795145094 0.4473994076 - 0.4922738075 0.4804913104 0.4484009147 - 0.4932405055 0.4814707935 0.4494000971 - 0.4942072034 0.4824503064 0.4503993094 - 0.4951739907 0.4834297895 0.4513984025 - 0.4961400926 0.4844071865 0.4523966908 - 0.4971033931 0.4853728116 0.4533889890 - 0.4980666041 0.4863384068 0.4543812871 - 0.4990297854 0.4873039126 0.4553737044 - 0.4999929965 0.4882695079 0.4563660026 - 0.5009500980 0.4892266095 0.4573510885 - 0.5019065142 0.4901829064 0.4583353102 - 0.5028629899 0.4911391139 0.4593194127 - 0.5038195252 0.4920954108 0.4603036046 - 0.5047739148 0.4930514991 0.4612889886 - 0.5057268739 0.4940074086 0.4622752070 - 0.5066800117 0.4949632883 0.4632613957 - 0.5076330900 0.4959192872 0.4642477036 - 0.5085847974 0.4968729913 0.4652321041 - 0.5095326900 0.4978204072 0.4662106931 - 0.5104805231 0.4987677932 0.4671893120 - 0.5114284158 0.4997152090 0.4681679010 - 0.5123761892 0.5006626248 0.4691464901 - 0.5133184791 0.5016015172 0.4701164067 - 0.5142596960 0.5025386214 0.4710843861 - 0.5152009130 0.5034757853 0.4720525146 - 0.5161420703 0.5044130087 0.4730204940 - 0.5170794725 0.5053501725 0.4739845991 - 0.5180101991 0.5062875152 0.4749414027 - 0.5189409852 0.5072247982 0.4758982956 - 0.5198717117 0.5081620812 0.4768550992 - 0.5208024979 0.5090993047 0.4778119028 - 0.5217247009 0.5100327730 0.4787622094 - 0.5226451755 0.5109654069 0.4797109962 - 0.5235655904 0.5118979812 0.4806598127 - 0.5244860053 0.5128306150 0.4816085100 - 0.5254061222 0.5137627125 0.4825567007 - 0.5263252258 0.5146926045 0.4835023880 - 0.5272442102 0.5156226158 0.4844481051 - 0.5281633139 0.5165526271 0.4853937924 - 0.5290824175 0.5174825191 0.4863395095 - 0.5299966931 0.5184153914 0.4872838855 - 0.5309072733 0.5193507075 0.4882271886 - 0.5318177938 0.5202859044 0.4891706109 - 0.5327283740 0.5212211013 0.4901140034 - 0.5336390138 0.5221564174 0.4910573065 - 0.5345441103 0.5230833292 0.4919950068 - 0.5354480147 0.5240085125 0.4929313958 - 0.5363519788 0.5249336958 0.4938678145 - 0.5372558832 0.5258588791 0.4948042035 - 0.5381596088 0.5267838836 0.4957407117 - 0.5390570760 0.5277035236 0.4966802001 - 0.5399546027 0.5286231041 0.4976195991 - 0.5408521295 0.5295426846 0.4985590875 - 0.5417495966 0.5304622054 0.4994986057 - 0.5426462889 0.5313823819 0.5004374981 - 0.5435394049 0.5323048830 0.5013735890 - 0.5444325805 0.5332273841 0.5023097992 - 0.5453258157 0.5341498852 0.5032458901 - 0.5462188721 0.5350723863 0.5041819811 - 0.5471100807 0.5359948874 0.5051165819 - 0.5479956269 0.5369172096 0.5060467720 - 0.5488811731 0.5378395915 0.5069770217 - 0.5497667789 0.5387619138 0.5079072118 - 0.5506523848 0.5396841764 0.5088372827 - 0.5515369177 0.5406029820 0.5097665787 - 0.5524184704 0.5415124297 0.5106934905 - 0.5533000827 0.5424216986 0.5116204023 - 0.5541815758 0.5433310866 0.5125473142 - 0.5550631881 0.5442404747 0.5134742260 - 0.5559430122 0.5451484919 0.5144008994 - 0.5568165183 0.5460516214 0.5153269768 - 0.5576900840 0.5469545722 0.5162531734 - 0.5585635900 0.5478577018 0.5171793103 - 0.5594370961 0.5487607121 0.5181055069 - 0.5603092909 0.5496640205 0.5190312862 - 0.5611693859 0.5505691171 0.5199540257 - 0.5620294809 0.5514742732 0.5208765864 - 0.5628895760 0.5523794293 0.5217992067 - 0.5637496710 0.5532845855 0.5227218866 - 0.5646098852 0.5541896820 0.5236445069 - 0.5654671192 0.5550884008 0.5245587230 - 0.5663241148 0.5559865236 0.5254719853 - 0.5671809912 0.5568845272 0.5263851881 - 0.5680379868 0.5577825904 0.5272983909 - 0.5688949227 0.5586807132 0.5282117128 - 0.5697519183 0.5595684052 0.5291250944 - 0.5706089139 0.5604503751 0.5300387144 - 0.5714659095 0.5613324046 0.5309522748 - 0.5723227859 0.5622143745 0.5318658948 - 0.5731797814 0.5630964041 0.5327795148 - 0.5740355849 0.5639759898 0.5336920023 - 0.5748888254 0.5648500919 0.5346015096 - 0.5757420063 0.5657241940 0.5355110765 - 0.5765951872 0.5665982962 0.5364207029 - 0.5774484873 0.5674723983 0.5373303294 - 0.5783017278 0.5683465004 0.5382397771 - 0.5791468024 0.5692207813 0.5391464233 - 0.5799911022 0.5700951219 0.5400525928 - 0.5808354020 0.5709695220 0.5409587026 - 0.5816797018 0.5718438029 0.5418648720 - 0.5825240016 0.5727180839 0.5427711010 - 0.5833687782 0.5735895038 0.5436769128 - 0.5842140913 0.5744568110 0.5445824265 - 0.5850595236 0.5753241181 0.5454878807 - 0.5859047771 0.5761914253 0.5463933945 - 0.5867500901 0.5770586729 0.5472989082 - 0.5875955224 0.5779259801 0.5482044220 - 0.5884351730 0.5787826777 0.5491033792 - 0.5892739892 0.5796378255 0.5500012040 - 0.5901129246 0.5804927945 0.5508990288 - 0.5909516811 0.5813478231 0.5517967939 - 0.5917904973 0.5822029114 0.5526946187 - 0.5926260948 0.5830556154 0.5535923243 - 0.5934513211 0.5839005113 0.5544894934 - 0.5942764282 0.5847454071 0.5553867221 - 0.5951015949 0.5855903029 0.5562840104 - 0.5959268212 0.5864353180 0.5571811795 - 0.5967519283 0.5872802138 0.5580784082 - 0.5975750089 0.5881240964 0.5589731932 - 0.5983961225 0.5889670253 0.5598654747 - 0.5992172956 0.5898098946 0.5607578158 - 0.6000384092 0.5906528234 0.5616500974 - 0.6008595824 0.5914956927 0.5625423789 - 0.6016806960 0.5923386216 0.5634347200 - 0.6025013924 0.5931785107 0.5643216968 - 0.6033217907 0.5940173864 0.5652067065 - 0.6041421890 0.5948563218 0.5660917163 - 0.6049627066 0.5956951976 0.5669767261 - 0.6057831049 0.5965340734 0.5678616762 - 0.6066035032 0.5973730087 0.5687466860 - 0.6074202061 0.5982081890 0.5696277022 - 0.6082363725 0.5990427732 0.5705081224 - 0.6090525985 0.5998774171 0.5713884830 - 0.6098688245 0.6007121205 0.5722689033 - 0.6106849909 0.6015467048 0.5731493235 - 0.6115012169 0.6023812890 0.5740296841 - 0.6123086214 0.6032109857 0.5749081969 - 0.6131153107 0.6040402055 0.5757864714 - 0.6139221191 0.6048694849 0.5766646862 - 0.6147289276 0.6056988239 0.5775430202 - 0.6155356765 0.6065281034 0.5784212947 - 0.6163424850 0.6073573232 0.5792995095 - 0.6171492934 0.6081854105 0.5801749825 - 0.6179562211 0.6090133786 0.5810500979 - 0.6187630296 0.6098414063 0.5819252133 - 0.6195698977 0.6106693745 0.5828003287 - 0.6203767061 0.6114974022 0.5836755037 - 0.6211835742 0.6123253107 0.5845506191 - 0.6219863296 0.6131554246 0.5854259729 - 0.6227880716 0.6139858961 0.5863015056 - 0.6235898733 0.6148164272 0.5871769786 - 0.6243916750 0.6156468987 0.5880525112 - 0.6251934767 0.6164774895 0.5889279842 - 0.6259952784 0.6173080206 0.5898035169 - 0.6267918944 0.6181306243 0.5906723738 - 0.6275851727 0.6189482212 0.5915369987 - 0.6283785105 0.6197658777 0.5924015045 - 0.6291717887 0.6205834746 0.5932661295 - 0.6299651265 0.6214010715 0.5941306949 - 0.6307584047 0.6222187877 0.5949952006 - 0.6315498948 0.6230332851 0.5958583951 - 0.6323379278 0.6238421202 0.5967187881 - 0.6331260204 0.6246507764 0.5975791812 - 0.6339139938 0.6254594922 0.5984395146 - 0.6347020268 0.6262683272 0.5992999077 - 0.6354900002 0.6270769835 0.6001603007 - 0.6362780929 0.6278856993 0.6010206938 - 0.6370700002 0.6286913753 0.6018776298 - 0.6378619075 0.6294969916 0.6027345061 - 0.6386538148 0.6303027272 0.6035913825 - 0.6394457817 0.6311082840 0.6044483185 - 0.6402376890 0.6319140196 0.6053051949 - 0.6410297155 0.6327196956 0.6061620116 - 0.6418200135 0.6335238814 0.6070173979 - 0.6426091194 0.6343269944 0.6078712940 - 0.6433981061 0.6351301074 0.6087253094 - 0.6441872120 0.6359332204 0.6095792055 - 0.6449761987 0.6367362738 0.6104332209 - 0.6457653046 0.6375393867 0.6112871170 - 0.6465541720 0.6383423209 0.6121410131 - 0.6473349929 0.6391350031 0.6129897237 - 0.6481158733 0.6399276853 0.6138383746 - 0.6488966942 0.6407204270 0.6146870852 - 0.6496775150 0.6415132284 0.6155357957 - 0.6504582763 0.6423059106 0.6163845062 - 0.6512390971 0.6430985928 0.6172332168 - 0.6520215869 0.6438899040 0.6180806756 - 0.6528064013 0.6446790099 0.6189264059 - 0.6535912752 0.6454681754 0.6197721958 - 0.6543760896 0.6462572813 0.6206179261 - 0.6551610231 0.6470463872 0.6214635968 - 0.6559458971 0.6478356123 0.6223093271 - 0.6567307115 0.6486247182 0.6231549978 - 0.6575127840 0.6494132280 0.6240007281 - 0.6582934856 0.6502012014 0.6248463988 - 0.6590741873 0.6509892941 0.6256921291 - 0.6598550081 0.6517773867 0.6265377998 - 0.6606357098 0.6525654793 0.6273834109 - 0.6614164114 0.6533535719 0.6282290816 - 0.6621971726 0.6541417241 0.6290748119 - 0.6629760265 0.6549354196 0.6299114823 - 0.6637545228 0.6557300091 0.6307469010 - 0.6645330787 0.6565244794 0.6315823197 - 0.6653115749 0.6573191285 0.6324176788 - 0.6660900712 0.6581135988 0.6332530975 - 0.6668686867 0.6589081883 0.6340885758 - 0.6676471829 0.6597027183 0.6349239945 - 0.6684235930 0.6604955792 0.6357483864 - 0.6691998839 0.6612884998 0.6365724802 - 0.6699762940 0.6620813012 0.6373966932 - 0.6707525849 0.6628742218 0.6382207870 - 0.6715289950 0.6636670232 0.6390450001 - 0.6723052859 0.6644598842 0.6398692131 - 0.6730816960 0.6652526855 0.6406933069 - 0.6738588214 0.6660354733 0.6415144801 - 0.6746358275 0.6668183208 0.6423355937 - 0.6754128933 0.6676011086 0.6431567073 - 0.6761900187 0.6683838964 0.6439778209 - 0.6769670844 0.6691666842 0.6447988749 - 0.6777442098 0.6699494123 0.6456201077 - 0.6785212159 0.6707322001 0.6464412212 - 0.6792948246 0.6715126038 0.6472591758 - 0.6800680757 0.6722927094 0.6480770111 - 0.6808413863 0.6730728745 0.6488947272 - 0.6816146970 0.6738529801 0.6497125030 - 0.6823881269 0.6746330857 0.6505302191 - 0.6831613779 0.6754133105 0.6513479948 - 0.6839346886 0.6761934161 0.6521657705 - 0.6847041249 0.6769770980 0.6529843807 - 0.6854723096 0.6777619720 0.6538034081 - 0.6862404943 0.6785469055 0.6546223760 - 0.6870086193 0.6793317199 0.6554414034 - 0.6877768040 0.6801165938 0.6562603116 - 0.6885449886 0.6809015274 0.6570792794 - 0.6893131137 0.6816864014 0.6578983068 - 0.6900815964 0.6824722290 0.6587166190 - 0.6908504963 0.6832591295 0.6595342159 - 0.6916193962 0.6840459108 0.6603516936 - 0.6923881769 0.6848328114 0.6611691713 - 0.6931570768 0.6856197119 0.6619867086 - 0.6939259768 0.6864064932 0.6628043056 - 0.6946948171 0.6871933937 0.6636217833 - 0.6954628825 0.6879794002 0.6644390821 - 0.6962239146 0.6887571216 0.6652535796 - 0.6969848275 0.6895347238 0.6660680771 - 0.6977458000 0.6903123856 0.6668826938 - 0.6985067725 0.6910901070 0.6676971912 - 0.6992678046 0.6918678284 0.6685116887 - 0.7000287175 0.6926453710 0.6693261862 - 0.7007896900 0.6934230924 0.6701408029 - 0.7015470862 0.6941987276 0.6709529757 - 0.7023022771 0.6949729919 0.6717637181 - 0.7030575275 0.6957471967 0.6725745201 - 0.7038127184 0.6965215206 0.6733852029 - 0.7045679092 0.6972957850 0.6741960049 - 0.7053231001 0.6980701089 0.6750066876 - 0.7060784101 0.6988443136 0.6758173704 - 0.7068333030 0.6996185780 0.6766281128 - 0.7075828910 0.7003933787 0.6774358153 - 0.7083325982 0.7011681199 0.6782433987 - 0.7090821862 0.7019429207 0.6790511012 - 0.7098318934 0.7027176023 0.6798588037 - 0.7105816007 0.7034922838 0.6806665063 - 0.7113311887 0.7042670846 0.6814742088 - 0.7120808959 0.7050418258 0.6822819114 - 0.7128294110 0.7058115005 0.6830887198 - 0.7135760784 0.7065724730 0.6838940978 - 0.7143228054 0.7073335052 0.6846994758 - 0.7150694132 0.7080944777 0.6855047941 - 0.7158160806 0.7088556290 0.6863101721 - 0.7165628076 0.7096166015 0.6871154904 - 0.7173094749 0.7103775740 0.6879209280 - 0.7180560827 0.7111386061 0.6887263060 - 0.7187995911 0.7118936777 0.6895337105 - 0.7195408940 0.7126445174 0.6903426051 - 0.7202820778 0.7133952975 0.6911514997 - 0.7210233808 0.7141461968 0.6919603944 - 0.7217646241 0.7148969769 0.6927692890 - 0.7225059271 0.7156478763 0.6935781837 - 0.7232471704 0.7163987160 0.6943870783 - 0.7239884138 0.7171496153 0.6951959729 - 0.7247272134 0.7178981900 0.6960002184 - 0.7254649997 0.7186459899 0.6968023777 - 0.7262027860 0.7193936706 0.6976047158 - 0.7269405723 0.7201414108 0.6984068751 - 0.7276784182 0.7208890915 0.6992090940 - 0.7284160852 0.7216367722 0.7000113130 - 0.7291538715 0.7223845124 0.7008134723 - 0.7298917174 0.7231323123 0.7016156912 - 0.7306271195 0.7238802910 0.7024155259 - 0.7313616276 0.7246286273 0.7032142282 - 0.7320960760 0.7253767848 0.7040128708 - 0.7328305244 0.7261251211 0.7048115730 - 0.7335649729 0.7268732786 0.7056102753 - 0.7342994809 0.7276216149 0.7064089775 - 0.7350339890 0.7283697724 0.7072076797 - 0.7357683778 0.7291179895 0.7080063820 - 0.7364991903 0.7298579812 0.7088025808 - 0.7372276783 0.7305926085 0.7095971107 - 0.7379561067 0.7313271761 0.7103915811 - 0.7386844754 0.7320618033 0.7111861110 - 0.7394129038 0.7327963710 0.7119805217 - 0.7401413918 0.7335309982 0.7127749920 - 0.7408698201 0.7342656255 0.7135695219 - 0.7415981889 0.7350000739 0.7143639922 - 0.7423244119 0.7357305884 0.7151564956 - 0.7430471778 0.7364547253 0.7159457207 - 0.7437700033 0.7371786833 0.7167348862 - 0.7444928288 0.7379027009 0.7175241113 - 0.7452155948 0.7386267781 0.7183133960 - 0.7459384203 0.7393507957 0.7191026211 - 0.7466611862 0.7400748730 0.7198917866 - 0.7473840117 0.7407988906 0.7206810117 - 0.7481065989 0.7415227294 0.7214697003 - 0.7488253713 0.7422428727 0.7222498059 - 0.7495442033 0.7429631948 0.7230297923 - 0.7502629757 0.7436835170 0.7238097787 - 0.7509818077 0.7444037199 0.7245898843 - 0.7517006993 0.7451239824 0.7253698707 - 0.7524194717 0.7458441854 0.7261499763 - 0.7531383038 0.7465645075 0.7269300222 - 0.7538570762 0.7472847104 0.7277100086 - 0.7545717955 0.7480040193 0.7284893990 - 0.7552838922 0.7487226725 0.7292684913 - 0.7559961081 0.7494413853 0.7300475240 - 0.7567083240 0.7501600981 0.7308264971 - 0.7574204206 0.7508788109 0.7316055894 - 0.7581325769 0.7515975237 0.7323846221 - 0.7588447928 0.7523161769 0.7331635952 - 0.7595570087 0.7530348897 0.7339426875 - 0.7602682710 0.7537531853 0.7347213030 - 0.7609707117 0.7544673085 0.7354943752 - 0.7616729736 0.7551813126 0.7362675071 - 0.7623752952 0.7558953166 0.7370406985 - 0.7630776167 0.7566093802 0.7378137708 - 0.7637799978 0.7573233843 0.7385869026 - 0.7644823194 0.7580373883 0.7393599749 - 0.7651845813 0.7587515116 0.7401331067 - 0.7658869028 0.7594655156 0.7409062982 - 0.7665888071 0.7601786256 0.7416762710 - 0.7672901750 0.7608903050 0.7424415946 - 0.7679914832 0.7616019845 0.7432069182 - 0.7686929107 0.7623137236 0.7439723015 - 0.7693942189 0.7630254030 0.7447376251 - 0.7700955868 0.7637370825 0.7455030084 - 0.7707968950 0.7644488811 0.7462682724 - 0.7714983225 0.7651606202 0.7470335960 - 0.7721995711 0.7658722997 0.7477989793 - 0.7729007006 0.7665820122 0.7485641241 - 0.7736015916 0.7672904134 0.7493289709 - 0.7743026018 0.7679988742 0.7500939965 - 0.7750034928 0.7687073946 0.7508589029 - 0.7757043839 0.7694157958 0.7516239285 - 0.7764053941 0.7701243162 0.7523887753 - 0.7771062851 0.7708327174 0.7531538010 - 0.7778071761 0.7715411782 0.7539188266 - 0.7785081267 0.7722496986 0.7546836734 - 0.7792059779 0.7729594111 0.7554478049 - 0.7799026966 0.7736696005 0.7562115788 - 0.7805994153 0.7743797898 0.7569754124 - 0.7812960744 0.7750899196 0.7577391863 - 0.7819927931 0.7758001089 0.7585030198 - 0.7826895118 0.7765102983 0.7592667937 - 0.7833861709 0.7772204876 0.7600306273 - 0.7840828896 0.7779306769 0.7607944012 - 0.7847796082 0.7786409259 0.7615581751 - 0.7854750156 0.7793545127 0.7623156905 - 0.7861698866 0.7800694704 0.7630702257 - 0.7868646979 0.7807846069 0.7638248205 - 0.7875596285 0.7814996243 0.7645792961 - 0.7882543802 0.7822147012 0.7653338909 - 0.7889491916 0.7829297185 0.7660884261 - 0.7896441221 0.7836447954 0.7668430209 - 0.7903388739 0.7843598127 0.7675974965 - 0.7910338044 0.7850748897 0.7683520913 - 0.7917258739 0.7857853770 0.7691050172 - 0.7924156785 0.7864919901 0.7698563933 - 0.7931054831 0.7871986032 0.7706078887 - 0.7937952876 0.7879050970 0.7713593245 - 0.7944852114 0.7886117101 0.7721108198 - 0.7951750159 0.7893183231 0.7728621960 - 0.7958648205 0.7900248766 0.7736136913 - 0.7965546250 0.7907314897 0.7743651271 - 0.7972444296 0.7914381027 0.7751166224 - 0.7979317904 0.7921432853 0.7758662105 - 0.7986127138 0.7928448915 0.7766107917 - 0.7992935777 0.7935463786 0.7773553729 - 0.7999745011 0.7942479253 0.7781000733 - 0.8006554246 0.7949494123 0.7788447142 - 0.8013362885 0.7956508994 0.7795892954 - 0.8020172119 0.7963523865 0.7803338766 - 0.8026980758 0.7970538735 0.7810785174 - 0.8033789992 0.7977554798 0.7818232179 - 0.8040599227 0.7984570265 0.7825677991 - 0.8047353029 0.7991585135 0.7833089828 - 0.8054097891 0.7998598814 0.7840498090 - 0.8060843945 0.8005614281 0.7847905159 - 0.8067588806 0.8012629151 0.7855312228 - 0.8074334264 0.8019642830 0.7862719893 - 0.8081079721 0.8026658297 0.7870126963 - 0.8087825179 0.8033673167 0.7877534032 - 0.8094571233 0.8040686846 0.7884941101 - 0.8101316094 0.8047701716 0.7892348766 - 0.8108057976 0.8054717779 0.7899757028 - 0.8114792705 0.8061736822 0.7907167077 - 0.8121528029 0.8068755865 0.7914577127 - 0.8128262758 0.8075774908 0.7921987772 - 0.8134996891 0.8082793951 0.7929397821 - 0.8141732216 0.8089812994 0.7936807871 - 0.8148466945 0.8096832037 0.7944217920 - 0.8155202270 0.8103851080 0.7951627970 - 0.8161935806 0.8110868931 0.7959039211 - 0.8168671131 0.8117887974 0.7966449261 - 0.8175389767 0.8124853969 0.7973858118 - 0.8182101250 0.8131793737 0.7981265783 - 0.8188812733 0.8138735294 0.7988674045 - 0.8195524216 0.8145676255 0.7996081710 - 0.8202236295 0.8152617216 0.8003489971 - 0.8208947182 0.8159558177 0.8010898232 - 0.8215658069 0.8166497946 0.8018305898 - 0.8222370148 0.8173438907 0.8025714159 - 0.8229081035 0.8180379868 0.8033121824 - 0.8235793114 0.8187320828 0.8040530086 - 0.8242446780 0.8194221854 0.8047931194 - 0.8249096274 0.8201119900 0.8055329919 - 0.8255745173 0.8208016753 0.8062729836 - 0.8262394071 0.8214914203 0.8070129156 - 0.8269042969 0.8221811056 0.8077529073 - 0.8275691867 0.8228709102 0.8084927797 - 0.8282340169 0.8235605955 0.8092327714 - 0.8288989067 0.8242502809 0.8099727035 - 0.8295637965 0.8249400258 0.8107126951 - 0.8302286863 0.8256297708 0.8114526272 - 0.8308899999 0.8263183832 0.8121870756 - 0.8315511942 0.8270071149 0.8129215240 - 0.8322123885 0.8276957870 0.8136559129 - 0.8328735828 0.8283843994 0.8143903017 - 0.8335347772 0.8290730715 0.8151246905 - 0.8341959715 0.8297618032 0.8158590198 - 0.8348572254 0.8304504156 0.8165934086 - 0.8355184197 0.8311390877 0.8173277974 - 0.8361796141 0.8318278193 0.8180621862 - 0.8368408084 0.8325163722 0.8187965751 - 0.8374971747 0.8331937194 0.8195257187 - 0.8381533027 0.8338699937 0.8202543855 - 0.8388093710 0.8345463872 0.8209831119 - 0.8394654989 0.8352227807 0.8217117190 - 0.8401216269 0.8358991742 0.8224403858 - 0.8407776952 0.8365755081 0.8231691122 - 0.8414338231 0.8372519016 0.8238977790 - 0.8420898914 0.8379282951 0.8246265054 - 0.8427460194 0.8386046886 0.8253551722 - 0.8434020877 0.8392810822 0.8260837793 - 0.8440536261 0.8399528265 0.8268138170 - 0.8447033763 0.8406227827 0.8275440931 - 0.8453533053 0.8412929177 0.8282744884 - 0.8460031152 0.8419629931 0.8290048838 - 0.8466529846 0.8426330090 0.8297352195 - 0.8473027945 0.8433030844 0.8304656148 - 0.8479527235 0.8439732194 0.8311958909 - 0.8486024737 0.8446431756 0.8319262862 - 0.8492524028 0.8453133106 0.8326566815 - 0.8499022126 0.8459833860 0.8333870173 - 0.8505504727 0.8466514945 0.8341168165 - 0.8511967063 0.8473168015 0.8348454833 - 0.8518428206 0.8479821086 0.8355743289 - 0.8524889946 0.8486474156 0.8363031149 - 0.8531351089 0.8493127227 0.8370317817 - 0.8537812829 0.8499780297 0.8377606273 - 0.8544273973 0.8506432772 0.8384894133 - 0.8550735712 0.8513085842 0.8392181993 - 0.8557196856 0.8519738913 0.8399469256 - 0.8563657999 0.8526391983 0.8406757116 - 0.8570119739 0.8533045053 0.8414044976 - 0.8576570153 0.8539714217 0.8421310186 - 0.8583019972 0.8546383977 0.8428574204 - 0.8589469790 0.8553053737 0.8435838223 - 0.8595920205 0.8559722900 0.8443102241 - 0.8602368832 0.8566393256 0.8450366259 - 0.8608819246 0.8573063016 0.8457630277 - 0.8615269065 0.8579732180 0.8464894295 - 0.8621718884 0.8586401939 0.8472157717 - 0.8628168702 0.8593072295 0.8479421139 - 0.8634617925 0.8599740863 0.8486685157 - 0.8641055226 0.8606404066 0.8493939042 - 0.8647466898 0.8613052964 0.8501170874 - 0.8653877974 0.8619701266 0.8508402705 - 0.8660289049 0.8626350164 0.8515635729 - 0.8666700125 0.8632999063 0.8522868156 - 0.8673111796 0.8639646769 0.8530099988 - 0.8679522872 0.8646296263 0.8537331820 - 0.8685933948 0.8652945161 0.8544564843 - 0.8692345023 0.8659592867 0.8551797271 - 0.8698757291 0.8666241765 0.8559029102 - 0.8705167770 0.8672891259 0.8566260934 - 0.8711531758 0.8679519296 0.8573492169 - 0.8717862964 0.8686133027 0.8580722213 - 0.8724194169 0.8692746758 0.8587951064 - 0.8730524182 0.8699361086 0.8595181108 - 0.8736854792 0.8705974221 0.8602411151 - 0.8743184805 0.8712587953 0.8609640002 - 0.8749516010 0.8719202280 0.8616870046 - 0.8755847216 0.8725816011 0.8624100089 - 0.8762177229 0.8732429147 0.8631328940 - 0.8768507838 0.8739042878 0.8638558984 - 0.8774839044 0.8745657206 0.8645789027 - 0.8781164885 0.8752220869 0.8652966022 - 0.8787490129 0.8758761287 0.8660116792 - 0.8793814778 0.8765301108 0.8667268157 - 0.8800138831 0.8771839738 0.8674418926 - 0.8806464076 0.8778380156 0.8681570888 - 0.8812788725 0.8784919977 0.8688722253 - 0.8819113970 0.8791459799 0.8695873022 - 0.8825438023 0.8798000216 0.8703024983 - 0.8831763268 0.8804540038 0.8710175753 - 0.8838087916 0.8811079860 0.8717327118 - 0.8844413161 0.8817619085 0.8724479079 - 0.8850709200 0.8824151754 0.8731632233 - 0.8856989741 0.8830680847 0.8738787770 - 0.8863270283 0.8837209940 0.8745942712 - 0.8869550824 0.8843739033 0.8753098845 - 0.8875830770 0.8850268126 0.8760253787 - 0.8882111907 0.8856797218 0.8767408729 - 0.8888391852 0.8863325715 0.8774564862 - 0.8894672990 0.8869854808 0.8781719804 - 0.8900952935 0.8876383901 0.8788874745 - 0.8907234073 0.8882912993 0.8796030879 - 0.8913514018 0.8889442086 0.8803185821 - 0.8919770122 0.8895980120 0.8810324073 - 0.8925994039 0.8902531266 0.8817437887 - 0.8932216763 0.8909081221 0.8824552298 - 0.8938440084 0.8915631771 0.8831664920 - 0.8944662809 0.8922182918 0.8838778734 - 0.8950886130 0.8928732872 0.8845893145 - 0.8957110047 0.8935284019 0.8853006959 - 0.8963332772 0.8941833973 0.8860120773 - 0.8969556093 0.8948385119 0.8867235184 - 0.8975778818 0.8954935074 0.8874347806 - 0.8982002735 0.8961486220 0.8881462216 - 0.8988224864 0.8968032002 0.8888574839 - 0.8994441032 0.8974525928 0.8895666003 - 0.9000657201 0.8981019258 0.8902757764 - 0.9006872773 0.8987513185 0.8909850121 - 0.9013088942 0.8994005919 0.8916941881 - 0.9019305110 0.9000499845 0.8924034238 - 0.9025521278 0.9006993771 0.8931125998 - 0.9031736851 0.9013487101 0.8938217759 - 0.9037953019 0.9019981027 0.8945308924 - 0.9044169188 0.9026473761 0.8952401280 - 0.9050384760 0.9032968283 0.8959493041 - 0.9056600928 0.9039461017 0.8966584802 - 0.9062818885 0.9045925736 0.8973668218 - 0.9069039822 0.9052367806 0.8980745077 - 0.9075260162 0.9058809876 0.8987820745 - 0.9081479907 0.9065251946 0.8994898200 - 0.9087700844 0.9071695209 0.9001973867 - 0.9093921185 0.9078137279 0.9009050727 - 0.9100140929 0.9084578753 0.9016128182 - 0.9106361866 0.9091020823 0.9023203850 - 0.9112582207 0.9097462893 0.9030280709 - 0.9118801951 0.9103906155 0.9037356973 - 0.9125022888 0.9110348225 0.9044433832 - 0.9131243229 0.9116790295 0.9051510096 - 0.9137399197 0.9123209715 0.9058520794 - 0.9143548012 0.9129626751 0.9065523744 - 0.9149696827 0.9136044979 0.9072527885 - 0.9155846834 0.9142462015 0.9079532027 - 0.9161996245 0.9148880243 0.9086536169 - 0.9168145061 0.9155297875 0.9093539715 - 0.9174293876 0.9161714911 0.9100543261 - 0.9180443883 0.9168133140 0.9107546806 - 0.9186593294 0.9174550176 0.9114550948 - 0.9192742109 0.9180967808 0.9121555090 - 0.9198892117 0.9187384844 0.9128558040 - 0.9205033779 0.9193800092 0.9135550857 - 0.9211103916 0.9200189710 0.9142435193 - 0.9217175245 0.9206579924 0.9149317741 - 0.9223244786 0.9212968946 0.9156202078 - 0.9229314923 0.9219359159 0.9163085222 - 0.9235386252 0.9225748777 0.9169968963 - 0.9241455793 0.9232137799 0.9176852107 - 0.9247527122 0.9238528013 0.9183735847 - 0.9253597260 0.9244918227 0.9190618992 - 0.9259667993 0.9251307845 0.9197502732 - 0.9265738130 0.9257696867 0.9204387069 - 0.9271808863 0.9264087081 0.9211270213 - 0.9277874231 0.9270467758 0.9218143225 - 0.9283900261 0.9276784062 0.9224932790 - 0.9289926887 0.9283099771 0.9231721759 - 0.9295952916 0.9289416075 0.9238511920 - 0.9301978946 0.9295731783 0.9245300889 - 0.9308004975 0.9302048087 0.9252091050 - 0.9314031005 0.9308363795 0.9258880019 - 0.9320058227 0.9314680099 0.9265670180 - 0.9326084256 0.9320995212 0.9272459149 - 0.9332110286 0.9327310920 0.9279248714 - 0.9338135719 0.9333627224 0.9286038876 - 0.9344161749 0.9339942932 0.9292827845 - 0.9350188971 0.9346259236 0.9299618006 - 0.9356135726 0.9352514148 0.9306330085 - 0.9362081289 0.9358767867 0.9313039184 - 0.9368026257 0.9365020990 0.9319748878 - 0.9373971820 0.9371274710 0.9326457977 - 0.9379916787 0.9377527833 0.9333167076 - 0.9385861754 0.9383780956 0.9339876771 - 0.9391806722 0.9390035272 0.9346585870 - 0.9397752285 0.9396287799 0.9353296161 - 0.9403697252 0.9402542114 0.9360005260 - 0.9409642816 0.9408795238 0.9366714954 - 0.9415587783 0.9415047765 0.9373424053 - 0.9421532750 0.9421302080 0.9380133748 - 0.9427425861 0.9427515268 0.9386783242 - 0.9433292150 0.9433708787 0.9393401742 - 0.9439157844 0.9439901710 0.9400020242 - 0.9445024133 0.9446095824 0.9406638741 - 0.9450889826 0.9452288747 0.9413257837 - 0.9456756115 0.9458482265 0.9419876933 - 0.9462621808 0.9464675784 0.9426494837 - 0.9468489289 0.9470868707 0.9433113933 - 0.9474354982 0.9477062821 0.9439733028 - 0.9480221272 0.9483255744 0.9446350932 - 0.9486086965 0.9489449263 0.9452970028 - 0.9491953254 0.9495642781 0.9459589124 - 0.9497808814 0.9501816034 0.9466190934 - 0.9503620863 0.9507901073 0.9472720027 - 0.9509434104 0.9513986707 0.9479249120 - 0.9515246153 0.9520071745 0.9485778809 - 0.9521058798 0.9526157975 0.9492307901 - 0.9526870847 0.9532243013 0.9498836994 - 0.9532684088 0.9538329244 0.9505366087 - 0.9538496137 0.9544414878 0.9511895776 - 0.9544308782 0.9550499916 0.9518424869 - 0.9550120831 0.9556586146 0.9524953961 - 0.9555934072 0.9562671185 0.9531484246 - 0.9561746120 0.9568756819 0.9538012743 - 0.9567558765 0.9574841857 0.9544541836 - 0.9573335052 0.9580839872 0.9551010728 - 0.9579079747 0.9586765170 0.9557430148 - 0.9584826231 0.9592689872 0.9563847780 - 0.9590572119 0.9598613977 0.9570267200 - 0.9596316814 0.9604539275 0.9576684833 - 0.9602062702 0.9610463977 0.9583104253 - 0.9607809186 0.9616388083 0.9589521885 - 0.9613553882 0.9622312784 0.9595940709 - 0.9619299769 0.9628238082 0.9602360129 - 0.9625046253 0.9634162784 0.9608777761 - 0.9630790949 0.9640086889 0.9615197182 - 0.9636536837 0.9646012187 0.9621614814 - 0.9642282724 0.9651936889 0.9628034234 - 0.9647949934 0.9657722712 0.9634308219 - 0.9653589129 0.9663459063 0.9640529156 - 0.9659227133 0.9669194818 0.9646750093 - 0.9664865732 0.9674929976 0.9652972221 - 0.9670504928 0.9680665731 0.9659193158 - 0.9676142931 0.9686402082 0.9665414095 - 0.9681782126 0.9692137837 0.9671635032 - 0.9687420726 0.9697872996 0.9677855968 - 0.9693059921 0.9703608751 0.9684076905 - 0.9698697925 0.9709345102 0.9690297842 - 0.9704337120 0.9715080857 0.9696519971 - 0.9709975719 0.9720816016 0.9702740908 - 0.9715613723 0.9726551771 0.9708961844 - 0.9721186161 0.9732081294 0.9714968801 - 0.9726734757 0.9737539291 0.9720901847 - 0.9732283950 0.9742997289 0.9726834893 - 0.9737833142 0.9748454094 0.9732767940 - 0.9743381739 0.9753912091 0.9738700986 - 0.9748930931 0.9759370089 0.9744634032 - 0.9754480124 0.9764826894 0.9750567079 - 0.9760028720 0.9770284891 0.9756498933 - 0.9765577912 0.9775742888 0.9762431979 - 0.9771127105 0.9781200290 0.9768365026 - 0.9776676297 0.9786658287 0.9774298072 - 0.9782224894 0.9792116284 0.9780231118 - 0.9787774086 0.9797573090 0.9786164165 - 0.9793238044 0.9802883863 0.9791917205 - 0.9798640013 0.9808087945 0.9797537923 - 0.9804041982 0.9813292027 0.9803159237 - 0.9809442759 0.9818496108 0.9808779955 - 0.9814844728 0.9823700190 0.9814401269 - 0.9820247293 0.9828904271 0.9820021987 - 0.9825649261 0.9834107757 0.9825642705 - 0.9831050038 0.9839311838 0.9831264019 - 0.9836452007 0.9844515920 0.9836884737 - 0.9841853976 0.9849720001 0.9842506051 - 0.9847255945 0.9854924083 0.9848126769 - 0.9852656722 0.9860128760 0.9853748083 - 0.9858059287 0.9865332842 0.9859368205 - 0.9863404036 0.9870470166 0.9864904881 - 0.9868565202 0.9875389934 0.9870164990 - 0.9873725176 0.9880309105 0.9875423908 - 0.9878885150 0.9885228872 0.9880682826 - 0.9884045124 0.9890148044 0.9885942936 - 0.9889205098 0.9895067811 0.9891201854 - 0.9894366264 0.9899986982 0.9896460772 - 0.9899526238 0.9904906750 0.9901720881 - 0.9904686213 0.9909825921 0.9906979799 - 0.9909846187 0.9914746284 0.9912238717 - 0.9915006161 0.9919664860 0.9917498827 - 0.9920166731 0.9924585223 0.9922757745 - 0.9925326705 0.9929504991 0.9928017855 - 0.9930487275 0.9934424162 0.9933276772 - 0.9935505986 0.9939172864 0.9938179255 - 0.9940466881 0.9943851829 0.9942935109 - 0.9945427775 0.9948530793 0.9947689772 - 0.9950389266 0.9953209758 0.9952446222 - 0.9955350161 0.9957888722 0.9957200885 - 0.9960311055 0.9962568283 0.9961956739 - 0.9965273142 0.9967247248 0.9966711998 - 0.9970234036 0.9971926212 0.9971467257 - 0.9975194931 0.9976605177 0.9976223111 - 0.9980155826 0.9981284142 0.9980977774 - 0.9985116720 0.9985963106 0.9985734224 - 0.9990078211 0.9990642071 0.9990488887 - 0.9995039105 0.9995321035 0.9995244741 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0077699851 0.0072091222 0.0057455739 + 0.0153281903 0.0140618198 0.0110133700 + 0.0220492706 0.0201340895 0.0156183802 + 0.0279948898 0.0255677104 0.0198130608 + 0.0334264189 0.0305160508 0.0236329101 + 0.0383813009 0.0350662991 0.0272729397 + 0.0430751182 0.0393643305 0.0306853205 + 0.0474476218 0.0434310399 0.0339677408 + 0.0515641496 0.0472853705 0.0371497907 + 0.0555446185 0.0509948209 0.0402266905 + 0.0594275706 0.0545908697 0.0432020612 + 0.0631866604 0.0580725186 0.0460699908 + 0.0668074489 0.0614085086 0.0488363616 + 0.0702805966 0.0646550804 0.0515515506 + 0.0736529976 0.0678500831 0.0542215705 + 0.0769146830 0.0709684193 0.0568457693 + 0.0801084563 0.0739683583 0.0594256707 + 0.0832033977 0.0768856406 0.0619675815 + 0.0862210765 0.0797707886 0.0644541681 + 0.0891883075 0.0826155171 0.0668781176 + 0.0921097621 0.0853902176 0.0692649484 + 0.0950033367 0.0881099030 0.0716069788 + 0.0978709385 0.0907903165 0.0739388466 + 0.1006767005 0.0934374928 0.0762372017 + 0.1034490988 0.0960375518 0.0785102323 + 0.1061699986 0.0985936001 0.0807417631 + 0.1088621020 0.1011143997 0.0829524100 + 0.1115100011 0.1035860032 0.0851420909 + 0.1141035035 0.1060298979 0.0873048082 + 0.1166575998 0.1084465012 0.0894576833 + 0.1191660017 0.1108224019 0.0915750787 + 0.1216190010 0.1131646037 0.0936675370 + 0.1240210012 0.1154721975 0.0957391188 + 0.1263785958 0.1177325025 0.0977701619 + 0.1287032962 0.1199773997 0.0997919589 + 0.1309965998 0.1222233996 0.1018017009 + 0.1332729012 0.1244523004 0.1037892997 + 0.1355164945 0.1266828030 0.1057759970 + 0.1377673000 0.1288989931 0.1077485010 + 0.1400064975 0.1310786009 0.1096967012 + 0.1422519982 0.1332331002 0.1116394028 + 0.1444955021 0.1353804022 0.1135682017 + 0.1467241049 0.1374987066 0.1154882982 + 0.1489524990 0.1396068931 0.1173892990 + 0.1511590034 0.1416790038 0.1192753017 + 0.1533526033 0.1437290013 0.1211493984 + 0.1555102021 0.1457712948 0.1229984984 + 0.1576402932 0.1477762014 0.1248319969 + 0.1597459018 0.1497581005 0.1266517043 + 0.1618217975 0.1517187059 0.1284562945 + 0.1638692021 0.1536567956 0.1302371025 + 0.1658921987 0.1555901021 0.1320014000 + 0.1678850949 0.1574811935 0.1337444037 + 0.1698528975 0.1593548954 0.1354825944 + 0.1718069017 0.1612353027 0.1372071058 + 0.1737360060 0.1631090939 0.1389127970 + 0.1756428927 0.1649706960 0.1406110972 + 0.1775456071 0.1668394953 0.1423041970 + 0.1794250011 0.1687040031 0.1439954042 + 0.1812936962 0.1705541015 0.1456899941 + 0.1831597984 0.1724057049 0.1473833025 + 0.1850039065 0.1742419004 0.1490696967 + 0.1868477017 0.1760638058 0.1507641971 + 0.1886914074 0.1778782010 0.1524640024 + 0.1905229986 0.1796796024 0.1541469991 + 0.1923421025 0.1814686954 0.1558177024 + 0.1941601038 0.1832399964 0.1574839056 + 0.1959781945 0.1850111932 0.1591470987 + 0.1978068054 0.1867780983 0.1608041972 + 0.1996330023 0.1885201037 0.1624432057 + 0.2014473975 0.1902448982 0.1640713960 + 0.2032514960 0.1919703931 0.1656913012 + 0.2050576061 0.1936956942 0.1672880948 + 0.2068455070 0.1953990012 0.1688698977 + 0.2086115927 0.1970870048 0.1704351008 + 0.2103770971 0.1987718940 0.1719868034 + 0.2121285051 0.2004460990 0.1735249013 + 0.2138638943 0.2020982951 0.1750528961 + 0.2155895978 0.2037346959 0.1765847057 + 0.2173081934 0.2053682953 0.1781219989 + 0.2190068960 0.2069963962 0.1796503961 + 0.2206840962 0.2086153030 0.1811677068 + 0.2223511040 0.2102248967 0.1826826036 + 0.2240124047 0.2118259966 0.1841962934 + 0.2256612033 0.2134266943 0.1857064962 + 0.2272907943 0.2150333971 0.1872134954 + 0.2289057970 0.2166189998 0.1887131035 + 0.2305078059 0.2181905061 0.1902053952 + 0.2321005017 0.2197569013 0.1916909963 + 0.2336920053 0.2213245928 0.1931720972 + 0.2352899015 0.2228953987 0.1946519017 + 0.2368838936 0.2244486064 0.1961286962 + 0.2384622991 0.2259978950 0.1976062059 + 0.2400310934 0.2275449038 0.1990849972 + 0.2415942997 0.2290931940 0.2005598992 + 0.2431554943 0.2306476980 0.2020221055 + 0.2447042018 0.2321884036 0.2034724057 + 0.2462362051 0.2337166965 0.2049137056 + 0.2477615029 0.2352366000 0.2063475996 + 0.2492882013 0.2367514074 0.2077756971 + 0.2508074939 0.2382549047 0.2092022002 + 0.2523134053 0.2397526950 0.2106287032 + 0.2538112998 0.2412422001 0.2120527029 + 0.2553072870 0.2427164018 0.2134691030 + 0.2568025887 0.2441851944 0.2148848027 + 0.2582916915 0.2456547022 0.2162963003 + 0.2597728968 0.2471264005 0.2177021056 + 0.2612324953 0.2485917062 0.2190997005 + 0.2626926899 0.2500481904 0.2204964012 + 0.2641533017 0.2514964044 0.2218922973 + 0.2656058967 0.2529383004 0.2232878953 + 0.2670469880 0.2543821037 0.2246783972 + 0.2684782147 0.2558273077 0.2260642052 + 0.2699055076 0.2572557926 0.2274404019 + 0.2713302076 0.2586747110 0.2288105041 + 0.2727521956 0.2600823939 0.2301730961 + 0.2741548121 0.2614834905 0.2315272987 + 0.2755548060 0.2628856003 0.2328795046 + 0.2769533098 0.2642917037 0.2342298031 + 0.2783550918 0.2656919062 0.2355791926 + 0.2797575891 0.2670885921 0.2369273007 + 0.2811529040 0.2684741020 0.2382656932 + 0.2825410068 0.2698574066 0.2396034002 + 0.2839199007 0.2712377012 0.2409401983 + 0.2853043079 0.2726213932 0.2422693074 + 0.2866902053 0.2740063071 0.2435951978 + 0.2880729139 0.2753944993 0.2449122965 + 0.2894502878 0.2767789960 0.2462285012 + 0.2908177972 0.2781563997 0.2475429028 + 0.2921772897 0.2795270979 0.2488564998 + 0.2935316861 0.2808933854 0.2501694858 + 0.2948884964 0.2822520137 0.2514741123 + 0.2962454855 0.2836092114 0.2527768016 + 0.2975969017 0.2849698961 0.2540780902 + 0.2989454865 0.2863264978 0.2553772032 + 0.3002853096 0.2876687944 0.2566682100 + 0.3016225100 0.2890065014 0.2579559088 + 0.3029560149 0.2903367877 0.2592380047 + 0.3042886853 0.2916674018 0.2605186999 + 0.3056207895 0.2929981947 0.2617981136 + 0.3069526851 0.2943269908 0.2630766928 + 0.3082844913 0.2956545949 0.2643547952 + 0.3096182942 0.2969661951 0.2656278014 + 0.3109529912 0.2982715070 0.2668986917 + 0.3122856021 0.2995666862 0.2681745887 + 0.3136177063 0.3008590043 0.2694520056 + 0.3149411976 0.3021504879 0.2707208097 + 0.3162628114 0.3034417927 0.2719872892 + 0.3175767064 0.3047347069 0.2732458115 + 0.3188889921 0.3060280085 0.2745023072 + 0.3202066123 0.3073163033 0.2757557929 + 0.3215256035 0.3086032867 0.2770085931 + 0.3228304088 0.3098782003 0.2782612145 + 0.3241305947 0.3111487925 0.2795138955 + 0.3254284859 0.3124164939 0.2807643116 + 0.3267253041 0.3136827946 0.2820135057 + 0.3280225098 0.3149532080 0.2832610905 + 0.3293201029 0.3162268996 0.2845073938 + 0.3306170106 0.3174982071 0.2857488990 + 0.3319132030 0.3187662959 0.2869836092 + 0.3332048953 0.3200294971 0.2882154882 + 0.3344849050 0.3212794960 0.2894394994 + 0.3357647061 0.3225289881 0.2906630933 + 0.3370434046 0.3237726986 0.2918823063 + 0.3383221924 0.3250164092 0.2931014895 + 0.3395951092 0.3262596130 0.2943179011 + 0.3408671021 0.3275027871 0.2955336869 + 0.3421292007 0.3287386000 0.2967428863 + 0.3433851898 0.3299694955 0.2979474962 + 0.3446362913 0.3311983943 0.2991509140 + 0.3458774090 0.3324227929 0.3003517985 + 0.3471182883 0.3336473107 0.3015527129 + 0.3483507931 0.3348652124 0.3027420044 + 0.3495832980 0.3360829949 0.3039312065 + 0.3508102000 0.3373039961 0.3051137030 + 0.3520343006 0.3385266066 0.3062925041 + 0.3532547951 0.3397471011 0.3074702024 + 0.3544664085 0.3409624100 0.3086448014 + 0.3556779921 0.3421775997 0.3098194897 + 0.3568766117 0.3433755934 0.3109912872 + 0.3580735922 0.3445712030 0.3121627867 + 0.3592629135 0.3457629979 0.3133305013 + 0.3604426980 0.3469501138 0.3144932091 + 0.3616225123 0.3481371999 0.3156560063 + 0.3627896905 0.3493269086 0.3168224096 + 0.3639565110 0.3505167067 0.3179889917 + 0.3651241064 0.3517034054 0.3191502094 + 0.3662925065 0.3528864980 0.3203046918 + 0.3674609065 0.3540695012 0.3214592934 + 0.3686209023 0.3552398086 0.3226110041 + 0.3697803020 0.3564088047 0.3237625062 + 0.3709327877 0.3575739861 0.3249132931 + 0.3720724881 0.3587315083 0.3260628879 + 0.3732121885 0.3598890007 0.3272125125 + 0.3743419051 0.3610461056 0.3283613026 + 0.3754678965 0.3622029126 0.3295099139 + 0.3765943944 0.3633604050 0.3306581080 + 0.3777253032 0.3645246029 0.3318021894 + 0.3788562119 0.3656888902 0.3329462111 + 0.3799856901 0.3668493927 0.3340887129 + 0.3811131120 0.3680044115 0.3352287114 + 0.3822405040 0.3691594899 0.3363687098 + 0.3833586872 0.3703058958 0.3375062048 + 0.3844727874 0.3714483976 0.3386425078 + 0.3855867982 0.3725908995 0.3397788107 + 0.3867011964 0.3737317026 0.3409082890 + 0.3878157139 0.3748725057 0.3420371115 + 0.3889299929 0.3760136962 0.3431653976 + 0.3900434077 0.3771579862 0.3442898989 + 0.3911567926 0.3783023059 0.3454144001 + 0.3922654092 0.3794414103 0.3465386033 + 0.3933632076 0.3805679977 0.3476622999 + 0.3944610059 0.3816947043 0.3487859070 + 0.3955565989 0.3828161955 0.3499088883 + 0.3966492116 0.3839308023 0.3510310948 + 0.3977417946 0.3850454092 0.3521533012 + 0.3988339901 0.3861593008 0.3532767892 + 0.3999255896 0.3872723877 0.3544014096 + 0.4010173082 0.3883855045 0.3555260003 + 0.4021055102 0.3894925117 0.3566437066 + 0.4031910896 0.3905951083 0.3577559888 + 0.4042766988 0.3916975856 0.3588683009 + 0.4053548872 0.3927960992 0.3599740863 + 0.4064280987 0.3938918114 0.3610751033 + 0.4075013101 0.3949874938 0.3621760905 + 0.4085747004 0.3960804939 0.3632729053 + 0.4096482992 0.3971712887 0.3643661141 + 0.4107218087 0.3982622027 0.3654592037 + 0.4117960930 0.3993507028 0.3665483892 + 0.4128710926 0.4004369974 0.3676328957 + 0.4139460921 0.4015232921 0.3687174916 + 0.4150182009 0.4026117027 0.3698010147 + 0.4160853922 0.4037036896 0.3708826005 + 0.4171527028 0.4047957063 0.3719643056 + 0.4182184935 0.4058865011 0.3730461895 + 0.4192796946 0.4069728851 0.3741292059 + 0.4203408957 0.4080593884 0.3752123117 + 0.4214020073 0.4091454148 0.3762952089 + 0.4224613011 0.4102149904 0.3773654103 + 0.4235205054 0.4112845063 0.3784357011 + 0.4245797992 0.4123539925 0.3795059025 + 0.4256359935 0.4134182930 0.3805727065 + 0.4266915023 0.4144811928 0.3816384077 + 0.4277468920 0.4155440927 0.3827041090 + 0.4287993014 0.4166063070 0.3837668896 + 0.4298486114 0.4176676869 0.3848268092 + 0.4308980107 0.4187290967 0.3858866096 + 0.4319469929 0.4197882116 0.3869450092 + 0.4329943061 0.4208357930 0.3879956901 + 0.4340417087 0.4218834043 0.3890464008 + 0.4350889921 0.4229309857 0.3900969923 + 0.4361298084 0.4239682853 0.3911440074 + 0.4371689856 0.4250029922 0.3921900094 + 0.4382081926 0.4260376990 0.3932358921 + 0.4392425120 0.4270702899 0.3942803144 + 0.4402691126 0.4280996025 0.3953221142 + 0.4412955940 0.4291290045 0.3963639140 + 0.4423221946 0.4301582873 0.3974055946 + 0.4433423877 0.4311893880 0.3984431028 + 0.4443620145 0.4322207868 0.3994801044 + 0.4453817010 0.4332520962 0.4005171955 + 0.4464051127 0.4342832863 0.4015508890 + 0.4474340975 0.4353140891 0.4025794864 + 0.4484629929 0.4363448918 0.4036081135 + 0.4494918883 0.4373758137 0.4046367109 + 0.4505175948 0.4384073019 0.4056606889 + 0.4515425861 0.4394389093 0.4066838920 + 0.4525676072 0.4404704869 0.4077070057 + 0.4535906017 0.4415003955 0.4087300897 + 0.4546070099 0.4425247014 0.4097532928 + 0.4556233883 0.4435490072 0.4107764065 + 0.4566397965 0.4445734024 0.4117994905 + 0.4576533139 0.4455991089 0.4128246903 + 0.4586645961 0.4466257989 0.4138514996 + 0.4596759081 0.4476524889 0.4148781896 + 0.4606871903 0.4486792088 0.4159049988 + 0.4616912901 0.4497005045 0.4169327021 + 0.4626944065 0.4507211149 0.4179604948 + 0.4636974931 0.4517416060 0.4189884067 + 0.4646998942 0.4527609050 0.4200156033 + 0.4656972885 0.4537692964 0.4210371971 + 0.4666947126 0.4547776878 0.4220587909 + 0.4676921070 0.4557861090 0.4230804145 + 0.4686877131 0.4567916095 0.4241015017 + 0.4696792960 0.4577902853 0.4251210093 + 0.4706709981 0.4587889910 0.4261406064 + 0.4716626108 0.4597877860 0.4271601140 + 0.4726535082 0.4607864022 0.4281783998 + 0.4736435115 0.4617848992 0.4291948974 + 0.4746335149 0.4627833962 0.4302113950 + 0.4756234884 0.4637818933 0.4312278926 + 0.4766106009 0.4647786915 0.4322431087 + 0.4775952101 0.4657739997 0.4332571924 + 0.4785799086 0.4667693079 0.4342711866 + 0.4795644879 0.4677644968 0.4352853000 + 0.4805473983 0.4687533975 0.4362984002 + 0.4815289080 0.4697372913 0.4373108149 + 0.4825105071 0.4707211852 0.4383231103 + 0.4834921062 0.4717051089 0.4393354952 + 0.4844721854 0.4726847112 0.4403477013 + 0.4854511917 0.4736607075 0.4413597882 + 0.4864301980 0.4746366143 0.4423719049 + 0.4874091148 0.4756126106 0.4433839917 + 0.4883851111 0.4765883088 0.4443916976 + 0.4893577993 0.4775637090 0.4453943074 + 0.4903304875 0.4785391092 0.4463968873 + 0.4913032055 0.4795145094 0.4473994076 + 0.4922738075 0.4804913104 0.4484009147 + 0.4932405055 0.4814707935 0.4494000971 + 0.4942072034 0.4824503064 0.4503993094 + 0.4951739907 0.4834297895 0.4513984025 + 0.4961400926 0.4844071865 0.4523966908 + 0.4971033931 0.4853728116 0.4533889890 + 0.4980666041 0.4863384068 0.4543812871 + 0.4990297854 0.4873039126 0.4553737044 + 0.4999929965 0.4882695079 0.4563660026 + 0.5009500980 0.4892266095 0.4573510885 + 0.5019065142 0.4901829064 0.4583353102 + 0.5028629899 0.4911391139 0.4593194127 + 0.5038195252 0.4920954108 0.4603036046 + 0.5047739148 0.4930514991 0.4612889886 + 0.5057268739 0.4940074086 0.4622752070 + 0.5066800117 0.4949632883 0.4632613957 + 0.5076330900 0.4959192872 0.4642477036 + 0.5085847974 0.4968729913 0.4652321041 + 0.5095326900 0.4978204072 0.4662106931 + 0.5104805231 0.4987677932 0.4671893120 + 0.5114284158 0.4997152090 0.4681679010 + 0.5123761892 0.5006626248 0.4691464901 + 0.5133184791 0.5016015172 0.4701164067 + 0.5142596960 0.5025386214 0.4710843861 + 0.5152009130 0.5034757853 0.4720525146 + 0.5161420703 0.5044130087 0.4730204940 + 0.5170794725 0.5053501725 0.4739845991 + 0.5180101991 0.5062875152 0.4749414027 + 0.5189409852 0.5072247982 0.4758982956 + 0.5198717117 0.5081620812 0.4768550992 + 0.5208024979 0.5090993047 0.4778119028 + 0.5217247009 0.5100327730 0.4787622094 + 0.5226451755 0.5109654069 0.4797109962 + 0.5235655904 0.5118979812 0.4806598127 + 0.5244860053 0.5128306150 0.4816085100 + 0.5254061222 0.5137627125 0.4825567007 + 0.5263252258 0.5146926045 0.4835023880 + 0.5272442102 0.5156226158 0.4844481051 + 0.5281633139 0.5165526271 0.4853937924 + 0.5290824175 0.5174825191 0.4863395095 + 0.5299966931 0.5184153914 0.4872838855 + 0.5309072733 0.5193507075 0.4882271886 + 0.5318177938 0.5202859044 0.4891706109 + 0.5327283740 0.5212211013 0.4901140034 + 0.5336390138 0.5221564174 0.4910573065 + 0.5345441103 0.5230833292 0.4919950068 + 0.5354480147 0.5240085125 0.4929313958 + 0.5363519788 0.5249336958 0.4938678145 + 0.5372558832 0.5258588791 0.4948042035 + 0.5381596088 0.5267838836 0.4957407117 + 0.5390570760 0.5277035236 0.4966802001 + 0.5399546027 0.5286231041 0.4976195991 + 0.5408521295 0.5295426846 0.4985590875 + 0.5417495966 0.5304622054 0.4994986057 + 0.5426462889 0.5313823819 0.5004374981 + 0.5435394049 0.5323048830 0.5013735890 + 0.5444325805 0.5332273841 0.5023097992 + 0.5453258157 0.5341498852 0.5032458901 + 0.5462188721 0.5350723863 0.5041819811 + 0.5471100807 0.5359948874 0.5051165819 + 0.5479956269 0.5369172096 0.5060467720 + 0.5488811731 0.5378395915 0.5069770217 + 0.5497667789 0.5387619138 0.5079072118 + 0.5506523848 0.5396841764 0.5088372827 + 0.5515369177 0.5406029820 0.5097665787 + 0.5524184704 0.5415124297 0.5106934905 + 0.5533000827 0.5424216986 0.5116204023 + 0.5541815758 0.5433310866 0.5125473142 + 0.5550631881 0.5442404747 0.5134742260 + 0.5559430122 0.5451484919 0.5144008994 + 0.5568165183 0.5460516214 0.5153269768 + 0.5576900840 0.5469545722 0.5162531734 + 0.5585635900 0.5478577018 0.5171793103 + 0.5594370961 0.5487607121 0.5181055069 + 0.5603092909 0.5496640205 0.5190312862 + 0.5611693859 0.5505691171 0.5199540257 + 0.5620294809 0.5514742732 0.5208765864 + 0.5628895760 0.5523794293 0.5217992067 + 0.5637496710 0.5532845855 0.5227218866 + 0.5646098852 0.5541896820 0.5236445069 + 0.5654671192 0.5550884008 0.5245587230 + 0.5663241148 0.5559865236 0.5254719853 + 0.5671809912 0.5568845272 0.5263851881 + 0.5680379868 0.5577825904 0.5272983909 + 0.5688949227 0.5586807132 0.5282117128 + 0.5697519183 0.5595684052 0.5291250944 + 0.5706089139 0.5604503751 0.5300387144 + 0.5714659095 0.5613324046 0.5309522748 + 0.5723227859 0.5622143745 0.5318658948 + 0.5731797814 0.5630964041 0.5327795148 + 0.5740355849 0.5639759898 0.5336920023 + 0.5748888254 0.5648500919 0.5346015096 + 0.5757420063 0.5657241940 0.5355110765 + 0.5765951872 0.5665982962 0.5364207029 + 0.5774484873 0.5674723983 0.5373303294 + 0.5783017278 0.5683465004 0.5382397771 + 0.5791468024 0.5692207813 0.5391464233 + 0.5799911022 0.5700951219 0.5400525928 + 0.5808354020 0.5709695220 0.5409587026 + 0.5816797018 0.5718438029 0.5418648720 + 0.5825240016 0.5727180839 0.5427711010 + 0.5833687782 0.5735895038 0.5436769128 + 0.5842140913 0.5744568110 0.5445824265 + 0.5850595236 0.5753241181 0.5454878807 + 0.5859047771 0.5761914253 0.5463933945 + 0.5867500901 0.5770586729 0.5472989082 + 0.5875955224 0.5779259801 0.5482044220 + 0.5884351730 0.5787826777 0.5491033792 + 0.5892739892 0.5796378255 0.5500012040 + 0.5901129246 0.5804927945 0.5508990288 + 0.5909516811 0.5813478231 0.5517967939 + 0.5917904973 0.5822029114 0.5526946187 + 0.5926260948 0.5830556154 0.5535923243 + 0.5934513211 0.5839005113 0.5544894934 + 0.5942764282 0.5847454071 0.5553867221 + 0.5951015949 0.5855903029 0.5562840104 + 0.5959268212 0.5864353180 0.5571811795 + 0.5967519283 0.5872802138 0.5580784082 + 0.5975750089 0.5881240964 0.5589731932 + 0.5983961225 0.5889670253 0.5598654747 + 0.5992172956 0.5898098946 0.5607578158 + 0.6000384092 0.5906528234 0.5616500974 + 0.6008595824 0.5914956927 0.5625423789 + 0.6016806960 0.5923386216 0.5634347200 + 0.6025013924 0.5931785107 0.5643216968 + 0.6033217907 0.5940173864 0.5652067065 + 0.6041421890 0.5948563218 0.5660917163 + 0.6049627066 0.5956951976 0.5669767261 + 0.6057831049 0.5965340734 0.5678616762 + 0.6066035032 0.5973730087 0.5687466860 + 0.6074202061 0.5982081890 0.5696277022 + 0.6082363725 0.5990427732 0.5705081224 + 0.6090525985 0.5998774171 0.5713884830 + 0.6098688245 0.6007121205 0.5722689033 + 0.6106849909 0.6015467048 0.5731493235 + 0.6115012169 0.6023812890 0.5740296841 + 0.6123086214 0.6032109857 0.5749081969 + 0.6131153107 0.6040402055 0.5757864714 + 0.6139221191 0.6048694849 0.5766646862 + 0.6147289276 0.6056988239 0.5775430202 + 0.6155356765 0.6065281034 0.5784212947 + 0.6163424850 0.6073573232 0.5792995095 + 0.6171492934 0.6081854105 0.5801749825 + 0.6179562211 0.6090133786 0.5810500979 + 0.6187630296 0.6098414063 0.5819252133 + 0.6195698977 0.6106693745 0.5828003287 + 0.6203767061 0.6114974022 0.5836755037 + 0.6211835742 0.6123253107 0.5845506191 + 0.6219863296 0.6131554246 0.5854259729 + 0.6227880716 0.6139858961 0.5863015056 + 0.6235898733 0.6148164272 0.5871769786 + 0.6243916750 0.6156468987 0.5880525112 + 0.6251934767 0.6164774895 0.5889279842 + 0.6259952784 0.6173080206 0.5898035169 + 0.6267918944 0.6181306243 0.5906723738 + 0.6275851727 0.6189482212 0.5915369987 + 0.6283785105 0.6197658777 0.5924015045 + 0.6291717887 0.6205834746 0.5932661295 + 0.6299651265 0.6214010715 0.5941306949 + 0.6307584047 0.6222187877 0.5949952006 + 0.6315498948 0.6230332851 0.5958583951 + 0.6323379278 0.6238421202 0.5967187881 + 0.6331260204 0.6246507764 0.5975791812 + 0.6339139938 0.6254594922 0.5984395146 + 0.6347020268 0.6262683272 0.5992999077 + 0.6354900002 0.6270769835 0.6001603007 + 0.6362780929 0.6278856993 0.6010206938 + 0.6370700002 0.6286913753 0.6018776298 + 0.6378619075 0.6294969916 0.6027345061 + 0.6386538148 0.6303027272 0.6035913825 + 0.6394457817 0.6311082840 0.6044483185 + 0.6402376890 0.6319140196 0.6053051949 + 0.6410297155 0.6327196956 0.6061620116 + 0.6418200135 0.6335238814 0.6070173979 + 0.6426091194 0.6343269944 0.6078712940 + 0.6433981061 0.6351301074 0.6087253094 + 0.6441872120 0.6359332204 0.6095792055 + 0.6449761987 0.6367362738 0.6104332209 + 0.6457653046 0.6375393867 0.6112871170 + 0.6465541720 0.6383423209 0.6121410131 + 0.6473349929 0.6391350031 0.6129897237 + 0.6481158733 0.6399276853 0.6138383746 + 0.6488966942 0.6407204270 0.6146870852 + 0.6496775150 0.6415132284 0.6155357957 + 0.6504582763 0.6423059106 0.6163845062 + 0.6512390971 0.6430985928 0.6172332168 + 0.6520215869 0.6438899040 0.6180806756 + 0.6528064013 0.6446790099 0.6189264059 + 0.6535912752 0.6454681754 0.6197721958 + 0.6543760896 0.6462572813 0.6206179261 + 0.6551610231 0.6470463872 0.6214635968 + 0.6559458971 0.6478356123 0.6223093271 + 0.6567307115 0.6486247182 0.6231549978 + 0.6575127840 0.6494132280 0.6240007281 + 0.6582934856 0.6502012014 0.6248463988 + 0.6590741873 0.6509892941 0.6256921291 + 0.6598550081 0.6517773867 0.6265377998 + 0.6606357098 0.6525654793 0.6273834109 + 0.6614164114 0.6533535719 0.6282290816 + 0.6621971726 0.6541417241 0.6290748119 + 0.6629760265 0.6549354196 0.6299114823 + 0.6637545228 0.6557300091 0.6307469010 + 0.6645330787 0.6565244794 0.6315823197 + 0.6653115749 0.6573191285 0.6324176788 + 0.6660900712 0.6581135988 0.6332530975 + 0.6668686867 0.6589081883 0.6340885758 + 0.6676471829 0.6597027183 0.6349239945 + 0.6684235930 0.6604955792 0.6357483864 + 0.6691998839 0.6612884998 0.6365724802 + 0.6699762940 0.6620813012 0.6373966932 + 0.6707525849 0.6628742218 0.6382207870 + 0.6715289950 0.6636670232 0.6390450001 + 0.6723052859 0.6644598842 0.6398692131 + 0.6730816960 0.6652526855 0.6406933069 + 0.6738588214 0.6660354733 0.6415144801 + 0.6746358275 0.6668183208 0.6423355937 + 0.6754128933 0.6676011086 0.6431567073 + 0.6761900187 0.6683838964 0.6439778209 + 0.6769670844 0.6691666842 0.6447988749 + 0.6777442098 0.6699494123 0.6456201077 + 0.6785212159 0.6707322001 0.6464412212 + 0.6792948246 0.6715126038 0.6472591758 + 0.6800680757 0.6722927094 0.6480770111 + 0.6808413863 0.6730728745 0.6488947272 + 0.6816146970 0.6738529801 0.6497125030 + 0.6823881269 0.6746330857 0.6505302191 + 0.6831613779 0.6754133105 0.6513479948 + 0.6839346886 0.6761934161 0.6521657705 + 0.6847041249 0.6769770980 0.6529843807 + 0.6854723096 0.6777619720 0.6538034081 + 0.6862404943 0.6785469055 0.6546223760 + 0.6870086193 0.6793317199 0.6554414034 + 0.6877768040 0.6801165938 0.6562603116 + 0.6885449886 0.6809015274 0.6570792794 + 0.6893131137 0.6816864014 0.6578983068 + 0.6900815964 0.6824722290 0.6587166190 + 0.6908504963 0.6832591295 0.6595342159 + 0.6916193962 0.6840459108 0.6603516936 + 0.6923881769 0.6848328114 0.6611691713 + 0.6931570768 0.6856197119 0.6619867086 + 0.6939259768 0.6864064932 0.6628043056 + 0.6946948171 0.6871933937 0.6636217833 + 0.6954628825 0.6879794002 0.6644390821 + 0.6962239146 0.6887571216 0.6652535796 + 0.6969848275 0.6895347238 0.6660680771 + 0.6977458000 0.6903123856 0.6668826938 + 0.6985067725 0.6910901070 0.6676971912 + 0.6992678046 0.6918678284 0.6685116887 + 0.7000287175 0.6926453710 0.6693261862 + 0.7007896900 0.6934230924 0.6701408029 + 0.7015470862 0.6941987276 0.6709529757 + 0.7023022771 0.6949729919 0.6717637181 + 0.7030575275 0.6957471967 0.6725745201 + 0.7038127184 0.6965215206 0.6733852029 + 0.7045679092 0.6972957850 0.6741960049 + 0.7053231001 0.6980701089 0.6750066876 + 0.7060784101 0.6988443136 0.6758173704 + 0.7068333030 0.6996185780 0.6766281128 + 0.7075828910 0.7003933787 0.6774358153 + 0.7083325982 0.7011681199 0.6782433987 + 0.7090821862 0.7019429207 0.6790511012 + 0.7098318934 0.7027176023 0.6798588037 + 0.7105816007 0.7034922838 0.6806665063 + 0.7113311887 0.7042670846 0.6814742088 + 0.7120808959 0.7050418258 0.6822819114 + 0.7128294110 0.7058115005 0.6830887198 + 0.7135760784 0.7065724730 0.6838940978 + 0.7143228054 0.7073335052 0.6846994758 + 0.7150694132 0.7080944777 0.6855047941 + 0.7158160806 0.7088556290 0.6863101721 + 0.7165628076 0.7096166015 0.6871154904 + 0.7173094749 0.7103775740 0.6879209280 + 0.7180560827 0.7111386061 0.6887263060 + 0.7187995911 0.7118936777 0.6895337105 + 0.7195408940 0.7126445174 0.6903426051 + 0.7202820778 0.7133952975 0.6911514997 + 0.7210233808 0.7141461968 0.6919603944 + 0.7217646241 0.7148969769 0.6927692890 + 0.7225059271 0.7156478763 0.6935781837 + 0.7232471704 0.7163987160 0.6943870783 + 0.7239884138 0.7171496153 0.6951959729 + 0.7247272134 0.7178981900 0.6960002184 + 0.7254649997 0.7186459899 0.6968023777 + 0.7262027860 0.7193936706 0.6976047158 + 0.7269405723 0.7201414108 0.6984068751 + 0.7276784182 0.7208890915 0.6992090940 + 0.7284160852 0.7216367722 0.7000113130 + 0.7291538715 0.7223845124 0.7008134723 + 0.7298917174 0.7231323123 0.7016156912 + 0.7306271195 0.7238802910 0.7024155259 + 0.7313616276 0.7246286273 0.7032142282 + 0.7320960760 0.7253767848 0.7040128708 + 0.7328305244 0.7261251211 0.7048115730 + 0.7335649729 0.7268732786 0.7056102753 + 0.7342994809 0.7276216149 0.7064089775 + 0.7350339890 0.7283697724 0.7072076797 + 0.7357683778 0.7291179895 0.7080063820 + 0.7364991903 0.7298579812 0.7088025808 + 0.7372276783 0.7305926085 0.7095971107 + 0.7379561067 0.7313271761 0.7103915811 + 0.7386844754 0.7320618033 0.7111861110 + 0.7394129038 0.7327963710 0.7119805217 + 0.7401413918 0.7335309982 0.7127749920 + 0.7408698201 0.7342656255 0.7135695219 + 0.7415981889 0.7350000739 0.7143639922 + 0.7423244119 0.7357305884 0.7151564956 + 0.7430471778 0.7364547253 0.7159457207 + 0.7437700033 0.7371786833 0.7167348862 + 0.7444928288 0.7379027009 0.7175241113 + 0.7452155948 0.7386267781 0.7183133960 + 0.7459384203 0.7393507957 0.7191026211 + 0.7466611862 0.7400748730 0.7198917866 + 0.7473840117 0.7407988906 0.7206810117 + 0.7481065989 0.7415227294 0.7214697003 + 0.7488253713 0.7422428727 0.7222498059 + 0.7495442033 0.7429631948 0.7230297923 + 0.7502629757 0.7436835170 0.7238097787 + 0.7509818077 0.7444037199 0.7245898843 + 0.7517006993 0.7451239824 0.7253698707 + 0.7524194717 0.7458441854 0.7261499763 + 0.7531383038 0.7465645075 0.7269300222 + 0.7538570762 0.7472847104 0.7277100086 + 0.7545717955 0.7480040193 0.7284893990 + 0.7552838922 0.7487226725 0.7292684913 + 0.7559961081 0.7494413853 0.7300475240 + 0.7567083240 0.7501600981 0.7308264971 + 0.7574204206 0.7508788109 0.7316055894 + 0.7581325769 0.7515975237 0.7323846221 + 0.7588447928 0.7523161769 0.7331635952 + 0.7595570087 0.7530348897 0.7339426875 + 0.7602682710 0.7537531853 0.7347213030 + 0.7609707117 0.7544673085 0.7354943752 + 0.7616729736 0.7551813126 0.7362675071 + 0.7623752952 0.7558953166 0.7370406985 + 0.7630776167 0.7566093802 0.7378137708 + 0.7637799978 0.7573233843 0.7385869026 + 0.7644823194 0.7580373883 0.7393599749 + 0.7651845813 0.7587515116 0.7401331067 + 0.7658869028 0.7594655156 0.7409062982 + 0.7665888071 0.7601786256 0.7416762710 + 0.7672901750 0.7608903050 0.7424415946 + 0.7679914832 0.7616019845 0.7432069182 + 0.7686929107 0.7623137236 0.7439723015 + 0.7693942189 0.7630254030 0.7447376251 + 0.7700955868 0.7637370825 0.7455030084 + 0.7707968950 0.7644488811 0.7462682724 + 0.7714983225 0.7651606202 0.7470335960 + 0.7721995711 0.7658722997 0.7477989793 + 0.7729007006 0.7665820122 0.7485641241 + 0.7736015916 0.7672904134 0.7493289709 + 0.7743026018 0.7679988742 0.7500939965 + 0.7750034928 0.7687073946 0.7508589029 + 0.7757043839 0.7694157958 0.7516239285 + 0.7764053941 0.7701243162 0.7523887753 + 0.7771062851 0.7708327174 0.7531538010 + 0.7778071761 0.7715411782 0.7539188266 + 0.7785081267 0.7722496986 0.7546836734 + 0.7792059779 0.7729594111 0.7554478049 + 0.7799026966 0.7736696005 0.7562115788 + 0.7805994153 0.7743797898 0.7569754124 + 0.7812960744 0.7750899196 0.7577391863 + 0.7819927931 0.7758001089 0.7585030198 + 0.7826895118 0.7765102983 0.7592667937 + 0.7833861709 0.7772204876 0.7600306273 + 0.7840828896 0.7779306769 0.7607944012 + 0.7847796082 0.7786409259 0.7615581751 + 0.7854750156 0.7793545127 0.7623156905 + 0.7861698866 0.7800694704 0.7630702257 + 0.7868646979 0.7807846069 0.7638248205 + 0.7875596285 0.7814996243 0.7645792961 + 0.7882543802 0.7822147012 0.7653338909 + 0.7889491916 0.7829297185 0.7660884261 + 0.7896441221 0.7836447954 0.7668430209 + 0.7903388739 0.7843598127 0.7675974965 + 0.7910338044 0.7850748897 0.7683520913 + 0.7917258739 0.7857853770 0.7691050172 + 0.7924156785 0.7864919901 0.7698563933 + 0.7931054831 0.7871986032 0.7706078887 + 0.7937952876 0.7879050970 0.7713593245 + 0.7944852114 0.7886117101 0.7721108198 + 0.7951750159 0.7893183231 0.7728621960 + 0.7958648205 0.7900248766 0.7736136913 + 0.7965546250 0.7907314897 0.7743651271 + 0.7972444296 0.7914381027 0.7751166224 + 0.7979317904 0.7921432853 0.7758662105 + 0.7986127138 0.7928448915 0.7766107917 + 0.7992935777 0.7935463786 0.7773553729 + 0.7999745011 0.7942479253 0.7781000733 + 0.8006554246 0.7949494123 0.7788447142 + 0.8013362885 0.7956508994 0.7795892954 + 0.8020172119 0.7963523865 0.7803338766 + 0.8026980758 0.7970538735 0.7810785174 + 0.8033789992 0.7977554798 0.7818232179 + 0.8040599227 0.7984570265 0.7825677991 + 0.8047353029 0.7991585135 0.7833089828 + 0.8054097891 0.7998598814 0.7840498090 + 0.8060843945 0.8005614281 0.7847905159 + 0.8067588806 0.8012629151 0.7855312228 + 0.8074334264 0.8019642830 0.7862719893 + 0.8081079721 0.8026658297 0.7870126963 + 0.8087825179 0.8033673167 0.7877534032 + 0.8094571233 0.8040686846 0.7884941101 + 0.8101316094 0.8047701716 0.7892348766 + 0.8108057976 0.8054717779 0.7899757028 + 0.8114792705 0.8061736822 0.7907167077 + 0.8121528029 0.8068755865 0.7914577127 + 0.8128262758 0.8075774908 0.7921987772 + 0.8134996891 0.8082793951 0.7929397821 + 0.8141732216 0.8089812994 0.7936807871 + 0.8148466945 0.8096832037 0.7944217920 + 0.8155202270 0.8103851080 0.7951627970 + 0.8161935806 0.8110868931 0.7959039211 + 0.8168671131 0.8117887974 0.7966449261 + 0.8175389767 0.8124853969 0.7973858118 + 0.8182101250 0.8131793737 0.7981265783 + 0.8188812733 0.8138735294 0.7988674045 + 0.8195524216 0.8145676255 0.7996081710 + 0.8202236295 0.8152617216 0.8003489971 + 0.8208947182 0.8159558177 0.8010898232 + 0.8215658069 0.8166497946 0.8018305898 + 0.8222370148 0.8173438907 0.8025714159 + 0.8229081035 0.8180379868 0.8033121824 + 0.8235793114 0.8187320828 0.8040530086 + 0.8242446780 0.8194221854 0.8047931194 + 0.8249096274 0.8201119900 0.8055329919 + 0.8255745173 0.8208016753 0.8062729836 + 0.8262394071 0.8214914203 0.8070129156 + 0.8269042969 0.8221811056 0.8077529073 + 0.8275691867 0.8228709102 0.8084927797 + 0.8282340169 0.8235605955 0.8092327714 + 0.8288989067 0.8242502809 0.8099727035 + 0.8295637965 0.8249400258 0.8107126951 + 0.8302286863 0.8256297708 0.8114526272 + 0.8308899999 0.8263183832 0.8121870756 + 0.8315511942 0.8270071149 0.8129215240 + 0.8322123885 0.8276957870 0.8136559129 + 0.8328735828 0.8283843994 0.8143903017 + 0.8335347772 0.8290730715 0.8151246905 + 0.8341959715 0.8297618032 0.8158590198 + 0.8348572254 0.8304504156 0.8165934086 + 0.8355184197 0.8311390877 0.8173277974 + 0.8361796141 0.8318278193 0.8180621862 + 0.8368408084 0.8325163722 0.8187965751 + 0.8374971747 0.8331937194 0.8195257187 + 0.8381533027 0.8338699937 0.8202543855 + 0.8388093710 0.8345463872 0.8209831119 + 0.8394654989 0.8352227807 0.8217117190 + 0.8401216269 0.8358991742 0.8224403858 + 0.8407776952 0.8365755081 0.8231691122 + 0.8414338231 0.8372519016 0.8238977790 + 0.8420898914 0.8379282951 0.8246265054 + 0.8427460194 0.8386046886 0.8253551722 + 0.8434020877 0.8392810822 0.8260837793 + 0.8440536261 0.8399528265 0.8268138170 + 0.8447033763 0.8406227827 0.8275440931 + 0.8453533053 0.8412929177 0.8282744884 + 0.8460031152 0.8419629931 0.8290048838 + 0.8466529846 0.8426330090 0.8297352195 + 0.8473027945 0.8433030844 0.8304656148 + 0.8479527235 0.8439732194 0.8311958909 + 0.8486024737 0.8446431756 0.8319262862 + 0.8492524028 0.8453133106 0.8326566815 + 0.8499022126 0.8459833860 0.8333870173 + 0.8505504727 0.8466514945 0.8341168165 + 0.8511967063 0.8473168015 0.8348454833 + 0.8518428206 0.8479821086 0.8355743289 + 0.8524889946 0.8486474156 0.8363031149 + 0.8531351089 0.8493127227 0.8370317817 + 0.8537812829 0.8499780297 0.8377606273 + 0.8544273973 0.8506432772 0.8384894133 + 0.8550735712 0.8513085842 0.8392181993 + 0.8557196856 0.8519738913 0.8399469256 + 0.8563657999 0.8526391983 0.8406757116 + 0.8570119739 0.8533045053 0.8414044976 + 0.8576570153 0.8539714217 0.8421310186 + 0.8583019972 0.8546383977 0.8428574204 + 0.8589469790 0.8553053737 0.8435838223 + 0.8595920205 0.8559722900 0.8443102241 + 0.8602368832 0.8566393256 0.8450366259 + 0.8608819246 0.8573063016 0.8457630277 + 0.8615269065 0.8579732180 0.8464894295 + 0.8621718884 0.8586401939 0.8472157717 + 0.8628168702 0.8593072295 0.8479421139 + 0.8634617925 0.8599740863 0.8486685157 + 0.8641055226 0.8606404066 0.8493939042 + 0.8647466898 0.8613052964 0.8501170874 + 0.8653877974 0.8619701266 0.8508402705 + 0.8660289049 0.8626350164 0.8515635729 + 0.8666700125 0.8632999063 0.8522868156 + 0.8673111796 0.8639646769 0.8530099988 + 0.8679522872 0.8646296263 0.8537331820 + 0.8685933948 0.8652945161 0.8544564843 + 0.8692345023 0.8659592867 0.8551797271 + 0.8698757291 0.8666241765 0.8559029102 + 0.8705167770 0.8672891259 0.8566260934 + 0.8711531758 0.8679519296 0.8573492169 + 0.8717862964 0.8686133027 0.8580722213 + 0.8724194169 0.8692746758 0.8587951064 + 0.8730524182 0.8699361086 0.8595181108 + 0.8736854792 0.8705974221 0.8602411151 + 0.8743184805 0.8712587953 0.8609640002 + 0.8749516010 0.8719202280 0.8616870046 + 0.8755847216 0.8725816011 0.8624100089 + 0.8762177229 0.8732429147 0.8631328940 + 0.8768507838 0.8739042878 0.8638558984 + 0.8774839044 0.8745657206 0.8645789027 + 0.8781164885 0.8752220869 0.8652966022 + 0.8787490129 0.8758761287 0.8660116792 + 0.8793814778 0.8765301108 0.8667268157 + 0.8800138831 0.8771839738 0.8674418926 + 0.8806464076 0.8778380156 0.8681570888 + 0.8812788725 0.8784919977 0.8688722253 + 0.8819113970 0.8791459799 0.8695873022 + 0.8825438023 0.8798000216 0.8703024983 + 0.8831763268 0.8804540038 0.8710175753 + 0.8838087916 0.8811079860 0.8717327118 + 0.8844413161 0.8817619085 0.8724479079 + 0.8850709200 0.8824151754 0.8731632233 + 0.8856989741 0.8830680847 0.8738787770 + 0.8863270283 0.8837209940 0.8745942712 + 0.8869550824 0.8843739033 0.8753098845 + 0.8875830770 0.8850268126 0.8760253787 + 0.8882111907 0.8856797218 0.8767408729 + 0.8888391852 0.8863325715 0.8774564862 + 0.8894672990 0.8869854808 0.8781719804 + 0.8900952935 0.8876383901 0.8788874745 + 0.8907234073 0.8882912993 0.8796030879 + 0.8913514018 0.8889442086 0.8803185821 + 0.8919770122 0.8895980120 0.8810324073 + 0.8925994039 0.8902531266 0.8817437887 + 0.8932216763 0.8909081221 0.8824552298 + 0.8938440084 0.8915631771 0.8831664920 + 0.8944662809 0.8922182918 0.8838778734 + 0.8950886130 0.8928732872 0.8845893145 + 0.8957110047 0.8935284019 0.8853006959 + 0.8963332772 0.8941833973 0.8860120773 + 0.8969556093 0.8948385119 0.8867235184 + 0.8975778818 0.8954935074 0.8874347806 + 0.8982002735 0.8961486220 0.8881462216 + 0.8988224864 0.8968032002 0.8888574839 + 0.8994441032 0.8974525928 0.8895666003 + 0.9000657201 0.8981019258 0.8902757764 + 0.9006872773 0.8987513185 0.8909850121 + 0.9013088942 0.8994005919 0.8916941881 + 0.9019305110 0.9000499845 0.8924034238 + 0.9025521278 0.9006993771 0.8931125998 + 0.9031736851 0.9013487101 0.8938217759 + 0.9037953019 0.9019981027 0.8945308924 + 0.9044169188 0.9026473761 0.8952401280 + 0.9050384760 0.9032968283 0.8959493041 + 0.9056600928 0.9039461017 0.8966584802 + 0.9062818885 0.9045925736 0.8973668218 + 0.9069039822 0.9052367806 0.8980745077 + 0.9075260162 0.9058809876 0.8987820745 + 0.9081479907 0.9065251946 0.8994898200 + 0.9087700844 0.9071695209 0.9001973867 + 0.9093921185 0.9078137279 0.9009050727 + 0.9100140929 0.9084578753 0.9016128182 + 0.9106361866 0.9091020823 0.9023203850 + 0.9112582207 0.9097462893 0.9030280709 + 0.9118801951 0.9103906155 0.9037356973 + 0.9125022888 0.9110348225 0.9044433832 + 0.9131243229 0.9116790295 0.9051510096 + 0.9137399197 0.9123209715 0.9058520794 + 0.9143548012 0.9129626751 0.9065523744 + 0.9149696827 0.9136044979 0.9072527885 + 0.9155846834 0.9142462015 0.9079532027 + 0.9161996245 0.9148880243 0.9086536169 + 0.9168145061 0.9155297875 0.9093539715 + 0.9174293876 0.9161714911 0.9100543261 + 0.9180443883 0.9168133140 0.9107546806 + 0.9186593294 0.9174550176 0.9114550948 + 0.9192742109 0.9180967808 0.9121555090 + 0.9198892117 0.9187384844 0.9128558040 + 0.9205033779 0.9193800092 0.9135550857 + 0.9211103916 0.9200189710 0.9142435193 + 0.9217175245 0.9206579924 0.9149317741 + 0.9223244786 0.9212968946 0.9156202078 + 0.9229314923 0.9219359159 0.9163085222 + 0.9235386252 0.9225748777 0.9169968963 + 0.9241455793 0.9232137799 0.9176852107 + 0.9247527122 0.9238528013 0.9183735847 + 0.9253597260 0.9244918227 0.9190618992 + 0.9259667993 0.9251307845 0.9197502732 + 0.9265738130 0.9257696867 0.9204387069 + 0.9271808863 0.9264087081 0.9211270213 + 0.9277874231 0.9270467758 0.9218143225 + 0.9283900261 0.9276784062 0.9224932790 + 0.9289926887 0.9283099771 0.9231721759 + 0.9295952916 0.9289416075 0.9238511920 + 0.9301978946 0.9295731783 0.9245300889 + 0.9308004975 0.9302048087 0.9252091050 + 0.9314031005 0.9308363795 0.9258880019 + 0.9320058227 0.9314680099 0.9265670180 + 0.9326084256 0.9320995212 0.9272459149 + 0.9332110286 0.9327310920 0.9279248714 + 0.9338135719 0.9333627224 0.9286038876 + 0.9344161749 0.9339942932 0.9292827845 + 0.9350188971 0.9346259236 0.9299618006 + 0.9356135726 0.9352514148 0.9306330085 + 0.9362081289 0.9358767867 0.9313039184 + 0.9368026257 0.9365020990 0.9319748878 + 0.9373971820 0.9371274710 0.9326457977 + 0.9379916787 0.9377527833 0.9333167076 + 0.9385861754 0.9383780956 0.9339876771 + 0.9391806722 0.9390035272 0.9346585870 + 0.9397752285 0.9396287799 0.9353296161 + 0.9403697252 0.9402542114 0.9360005260 + 0.9409642816 0.9408795238 0.9366714954 + 0.9415587783 0.9415047765 0.9373424053 + 0.9421532750 0.9421302080 0.9380133748 + 0.9427425861 0.9427515268 0.9386783242 + 0.9433292150 0.9433708787 0.9393401742 + 0.9439157844 0.9439901710 0.9400020242 + 0.9445024133 0.9446095824 0.9406638741 + 0.9450889826 0.9452288747 0.9413257837 + 0.9456756115 0.9458482265 0.9419876933 + 0.9462621808 0.9464675784 0.9426494837 + 0.9468489289 0.9470868707 0.9433113933 + 0.9474354982 0.9477062821 0.9439733028 + 0.9480221272 0.9483255744 0.9446350932 + 0.9486086965 0.9489449263 0.9452970028 + 0.9491953254 0.9495642781 0.9459589124 + 0.9497808814 0.9501816034 0.9466190934 + 0.9503620863 0.9507901073 0.9472720027 + 0.9509434104 0.9513986707 0.9479249120 + 0.9515246153 0.9520071745 0.9485778809 + 0.9521058798 0.9526157975 0.9492307901 + 0.9526870847 0.9532243013 0.9498836994 + 0.9532684088 0.9538329244 0.9505366087 + 0.9538496137 0.9544414878 0.9511895776 + 0.9544308782 0.9550499916 0.9518424869 + 0.9550120831 0.9556586146 0.9524953961 + 0.9555934072 0.9562671185 0.9531484246 + 0.9561746120 0.9568756819 0.9538012743 + 0.9567558765 0.9574841857 0.9544541836 + 0.9573335052 0.9580839872 0.9551010728 + 0.9579079747 0.9586765170 0.9557430148 + 0.9584826231 0.9592689872 0.9563847780 + 0.9590572119 0.9598613977 0.9570267200 + 0.9596316814 0.9604539275 0.9576684833 + 0.9602062702 0.9610463977 0.9583104253 + 0.9607809186 0.9616388083 0.9589521885 + 0.9613553882 0.9622312784 0.9595940709 + 0.9619299769 0.9628238082 0.9602360129 + 0.9625046253 0.9634162784 0.9608777761 + 0.9630790949 0.9640086889 0.9615197182 + 0.9636536837 0.9646012187 0.9621614814 + 0.9642282724 0.9651936889 0.9628034234 + 0.9647949934 0.9657722712 0.9634308219 + 0.9653589129 0.9663459063 0.9640529156 + 0.9659227133 0.9669194818 0.9646750093 + 0.9664865732 0.9674929976 0.9652972221 + 0.9670504928 0.9680665731 0.9659193158 + 0.9676142931 0.9686402082 0.9665414095 + 0.9681782126 0.9692137837 0.9671635032 + 0.9687420726 0.9697872996 0.9677855968 + 0.9693059921 0.9703608751 0.9684076905 + 0.9698697925 0.9709345102 0.9690297842 + 0.9704337120 0.9715080857 0.9696519971 + 0.9709975719 0.9720816016 0.9702740908 + 0.9715613723 0.9726551771 0.9708961844 + 0.9721186161 0.9732081294 0.9714968801 + 0.9726734757 0.9737539291 0.9720901847 + 0.9732283950 0.9742997289 0.9726834893 + 0.9737833142 0.9748454094 0.9732767940 + 0.9743381739 0.9753912091 0.9738700986 + 0.9748930931 0.9759370089 0.9744634032 + 0.9754480124 0.9764826894 0.9750567079 + 0.9760028720 0.9770284891 0.9756498933 + 0.9765577912 0.9775742888 0.9762431979 + 0.9771127105 0.9781200290 0.9768365026 + 0.9776676297 0.9786658287 0.9774298072 + 0.9782224894 0.9792116284 0.9780231118 + 0.9787774086 0.9797573090 0.9786164165 + 0.9793238044 0.9802883863 0.9791917205 + 0.9798640013 0.9808087945 0.9797537923 + 0.9804041982 0.9813292027 0.9803159237 + 0.9809442759 0.9818496108 0.9808779955 + 0.9814844728 0.9823700190 0.9814401269 + 0.9820247293 0.9828904271 0.9820021987 + 0.9825649261 0.9834107757 0.9825642705 + 0.9831050038 0.9839311838 0.9831264019 + 0.9836452007 0.9844515920 0.9836884737 + 0.9841853976 0.9849720001 0.9842506051 + 0.9847255945 0.9854924083 0.9848126769 + 0.9852656722 0.9860128760 0.9853748083 + 0.9858059287 0.9865332842 0.9859368205 + 0.9863404036 0.9870470166 0.9864904881 + 0.9868565202 0.9875389934 0.9870164990 + 0.9873725176 0.9880309105 0.9875423908 + 0.9878885150 0.9885228872 0.9880682826 + 0.9884045124 0.9890148044 0.9885942936 + 0.9889205098 0.9895067811 0.9891201854 + 0.9894366264 0.9899986982 0.9896460772 + 0.9899526238 0.9904906750 0.9901720881 + 0.9904686213 0.9909825921 0.9906979799 + 0.9909846187 0.9914746284 0.9912238717 + 0.9915006161 0.9919664860 0.9917498827 + 0.9920166731 0.9924585223 0.9922757745 + 0.9925326705 0.9929504991 0.9928017855 + 0.9930487275 0.9934424162 0.9933276772 + 0.9935505986 0.9939172864 0.9938179255 + 0.9940466881 0.9943851829 0.9942935109 + 0.9945427775 0.9948530793 0.9947689772 + 0.9950389266 0.9953209758 0.9952446222 + 0.9955350161 0.9957888722 0.9957200885 + 0.9960311055 0.9962568283 0.9961956739 + 0.9965273142 0.9967247248 0.9966711998 + 0.9970234036 0.9971926212 0.9971467257 + 0.9975194931 0.9976605177 0.9976223111 + 0.9980155826 0.9981284142 0.9980977774 + 0.9985116720 0.9985963106 0.9985734224 + 0.9990078211 0.9990642071 0.9990488887 + 0.9995039105 0.9995321035 0.9995244741 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_400nc.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_400nc.spi1d index b475f263..5c56bb3c 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_400nc.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_400nc.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0114686396 0.0107471403 0.0087564439 - 0.0217512399 0.0202643294 0.0160814300 - 0.0303464700 0.0281717796 0.0221705399 - 0.0377242714 0.0349318609 0.0275946204 - 0.0443095490 0.0409396514 0.0324226208 - 0.0503021218 0.0464737117 0.0369786993 - 0.0558728315 0.0517093204 0.0412629806 - 0.0610256903 0.0566021390 0.0452965386 - 0.0659263134 0.0612265393 0.0491837002 - 0.0706434101 0.0656422004 0.0528963394 - 0.0751574785 0.0698595867 0.0564641915 - 0.0794606581 0.0738903284 0.0599343516 - 0.0835847408 0.0778156295 0.0632929727 - 0.0876147822 0.0816217065 0.0665517896 - 0.0915386677 0.0853431597 0.0697500929 - 0.0953285396 0.0889591500 0.0728649125 - 0.0989516675 0.0924931094 0.0759018511 - 0.1024663970 0.0958689004 0.0788350999 - 0.1058961004 0.0991332605 0.0816867501 - 0.1092602983 0.1023299024 0.0844683573 - 0.1125695035 0.1054401994 0.0872167125 - 0.1157974973 0.1085370034 0.0899668783 - 0.1189695969 0.1115973964 0.0926889405 - 0.1221098974 0.1145918965 0.0953712016 - 0.1251875013 0.1175365001 0.0979943499 - 0.1281960011 0.1204660982 0.1005420014 - 0.1311468929 0.1233218014 0.1030569002 - 0.1340449005 0.1260855943 0.1055212021 - 0.1368933022 0.1287900954 0.1079170033 - 0.1396844983 0.1314754933 0.1103053018 - 0.1424379051 0.1341181993 0.1126656979 - 0.1451424062 0.1367079020 0.1150159985 - 0.1478171945 0.1392980963 0.1173409969 - 0.1504548937 0.1418841928 0.1196570992 - 0.1530500054 0.1444393992 0.1219561994 - 0.1555911005 0.1469538957 0.1242327988 - 0.1581020057 0.1494355053 0.1264850050 - 0.1605874002 0.1518718004 0.1287028939 - 0.1630426943 0.1542987972 0.1308919936 - 0.1654826999 0.1566974074 0.1330559999 - 0.1679003984 0.1590469033 0.1351830959 - 0.1703007966 0.1613458991 0.1372949034 - 0.1726765931 0.1636165977 0.1393575966 - 0.1750312001 0.1658513993 0.1414173990 - 0.1773743033 0.1680632979 0.1434593946 - 0.1796950996 0.1702526957 0.1454783976 - 0.1819853038 0.1724139005 0.1474941969 - 0.1842298955 0.1745467931 0.1494760960 - 0.1864486933 0.1766640991 0.1514540017 - 0.1886311024 0.1787831932 0.1534274966 - 0.1907832026 0.1809104979 0.1553764045 - 0.1929026991 0.1830410957 0.1573255062 - 0.1950244009 0.1851601005 0.1592600048 - 0.1971314996 0.1872643977 0.1611716002 - 0.1992381066 0.1893534064 0.1630840003 - 0.2013241053 0.1914180070 0.1649879962 - 0.2034000009 0.1934722960 0.1668666005 - 0.2054556012 0.1955029964 0.1687465012 - 0.2074957937 0.1975183934 0.1706251055 - 0.2095358968 0.1995349973 0.1724743992 - 0.2115648985 0.2015223950 0.1743170023 - 0.2135839015 0.2034906000 0.1761588007 - 0.2155832052 0.2054294050 0.1779755950 - 0.2175676972 0.2073418945 0.1797723025 - 0.2195197046 0.2092444003 0.1815697998 - 0.2214522064 0.2111348063 0.1833599955 - 0.2233704031 0.2130120993 0.1851364970 - 0.2252652049 0.2148773074 0.1868986040 - 0.2271434069 0.2167267948 0.1886556000 - 0.2290212065 0.2185603976 0.1903920025 - 0.2308852971 0.2203781009 0.1921014935 - 0.2327376008 0.2221764028 0.1937994957 - 0.2345706969 0.2239618003 0.1954976022 - 0.2363775074 0.2257477045 0.1971796006 - 0.2381743938 0.2275269926 0.1988390982 - 0.2399536967 0.2292968929 0.2004887015 - 0.2417089045 0.2310636044 0.2021429986 - 0.2434546947 0.2328283936 0.2037986070 - 0.2451961935 0.2345885932 0.2054408938 - 0.2469274998 0.2363439947 0.2070686072 - 0.2486505061 0.2380899042 0.2086946070 - 0.2503683865 0.2398183942 0.2103239000 - 0.2520723045 0.2415340990 0.2119438946 - 0.2537688017 0.2432458997 0.2135417014 - 0.2554596066 0.2449575067 0.2151297927 - 0.2571457028 0.2466547936 0.2167214006 - 0.2588266134 0.2483399063 0.2183215022 - 0.2605035007 0.2500117123 0.2199174017 - 0.2621727884 0.2516688108 0.2215020061 - 0.2638291121 0.2533113062 0.2230799049 - 0.2654761076 0.2549428046 0.2246621996 - 0.2671324015 0.2565666139 0.2262382060 - 0.2687830031 0.2581740022 0.2278027982 - 0.2704266906 0.2597663105 0.2293560058 - 0.2720651031 0.2613466978 0.2309001982 - 0.2737013996 0.2629151940 0.2324444950 - 0.2753258049 0.2644810081 0.2339832038 - 0.2769433856 0.2660413086 0.2355152965 - 0.2785556018 0.2675940990 0.2370389998 - 0.2801620960 0.2691347897 0.2385461032 - 0.2817465067 0.2706714869 0.2400489002 - 0.2833232880 0.2722041011 0.2415542006 - 0.2848966122 0.2737326026 0.2430642992 - 0.2864696980 0.2752566040 0.2445680946 - 0.2880344093 0.2767753899 0.2460584939 - 0.2895927131 0.2782889903 0.2475391030 - 0.2911433876 0.2797945142 0.2490137070 - 0.2926709950 0.2812936008 0.2504926920 - 0.2941831052 0.2827874124 0.2519704103 - 0.2956764996 0.2842740119 0.2534419000 - 0.2971622944 0.2857593000 0.2548941076 - 0.2986418903 0.2872450948 0.2563388944 - 0.3001124859 0.2887324095 0.2577767074 - 0.3015663028 0.2902269065 0.2592169046 - 0.3030163050 0.2917178869 0.2606565952 - 0.3044652939 0.2931961119 0.2620925903 - 0.3059149981 0.2946687043 0.2635230124 - 0.3073624074 0.2961393893 0.2649494112 - 0.3087939918 0.2976126969 0.2663621902 - 0.3102258146 0.2990801930 0.2677738965 - 0.3116576970 0.3005422950 0.2691845894 - 0.3130910993 0.3019973040 0.2705928087 - 0.3145203888 0.3034510911 0.2719972134 - 0.3159373105 0.3049038947 0.2733885944 - 0.3173550963 0.3063476980 0.2747704983 - 0.3187735975 0.3077847958 0.2761443853 - 0.3201943040 0.3092113137 0.2775141895 - 0.3216128945 0.3106358945 0.2788833082 - 0.3230192065 0.3120585084 0.2802532911 - 0.3244253993 0.3134770095 0.2816143036 - 0.3258317113 0.3148897886 0.2829608023 - 0.3272288144 0.3162913918 0.2842969000 - 0.3286210001 0.3176868856 0.2856262922 - 0.3300077021 0.3190723956 0.2869521081 - 0.3313938081 0.3204556108 0.2882770896 - 0.3327834904 0.3218250871 0.2896099091 - 0.3341748118 0.3231914043 0.2909416854 - 0.3355718851 0.3245447874 0.2922688127 - 0.3369680941 0.3258947134 0.2935900092 - 0.3383626044 0.3272379041 0.2948979139 - 0.3397515118 0.3285779953 0.2962028980 - 0.3411338925 0.3299140036 0.2975037992 - 0.3425045013 0.3312394917 0.2988072932 - 0.3438651860 0.3325557113 0.3001132905 - 0.3452149034 0.3338659108 0.3014146984 - 0.3465574980 0.3351719081 0.3027124107 - 0.3478950858 0.3364771008 0.3039973974 - 0.3492299020 0.3377816975 0.3052738011 - 0.3505575955 0.3390836120 0.3065445125 - 0.3518814147 0.3403840065 0.3078114986 - 0.3531937003 0.3416804969 0.3090800047 - 0.3544990122 0.3429746926 0.3103496134 - 0.3558034003 0.3442664146 0.3116236925 - 0.3571070135 0.3455562890 0.3129017055 - 0.3584021032 0.3468429148 0.3141731024 - 0.3596883118 0.3481261134 0.3154368103 - 0.3609679937 0.3494060040 0.3166972995 - 0.3622376919 0.3506805003 0.3179521859 - 0.3635073900 0.3519532084 0.3192068040 - 0.3647772074 0.3532207906 0.3204610050 - 0.3660452962 0.3544875979 0.3217146099 - 0.3672988117 0.3557465971 0.3229593039 - 0.3685522974 0.3570055962 0.3242039979 - 0.3697921932 0.3582544923 0.3254339099 - 0.3710305989 0.3595022857 0.3266617060 - 0.3722676039 0.3607459962 0.3278838992 - 0.3735038042 0.3619877994 0.3291031122 - 0.3747293949 0.3632284999 0.3303220868 - 0.3759411871 0.3644677997 0.3315409124 - 0.3771511018 0.3657064140 0.3327583075 - 0.3783504069 0.3669404984 0.3339653015 - 0.3795498013 0.3681747019 0.3351722956 - 0.3807409108 0.3694047034 0.3363747001 - 0.3819305003 0.3706338108 0.3375759125 - 0.3831126988 0.3718604147 0.3387717009 - 0.3842875063 0.3730843961 0.3399612010 - 0.3854615092 0.3743064106 0.3411506116 - 0.3866302967 0.3755132854 0.3423393965 - 0.3877991140 0.3767201900 0.3435282111 - 0.3889720142 0.3779211938 0.3447239101 - 0.3901464939 0.3791198134 0.3459227979 - 0.3913210928 0.3803184032 0.3471212983 - 0.3924961090 0.3815169036 0.3483184874 - 0.3936710954 0.3827154934 0.3495157063 - 0.3948434889 0.3839044869 0.3507024050 - 0.3960154057 0.3850910068 0.3518857956 - 0.3971866965 0.3862751126 0.3530685008 - 0.3983570039 0.3874529898 0.3542487919 - 0.3995273113 0.3886308074 0.3554292023 - 0.4006893039 0.3898057938 0.3566128016 - 0.4018486142 0.3909797966 0.3577977121 - 0.4030069113 0.3921526968 0.3589808941 - 0.4041604102 0.3933204114 0.3601540029 - 0.4053137898 0.3944880962 0.3613269925 - 0.4064702988 0.3956514001 0.3624922931 - 0.4076293111 0.3968113065 0.3636507988 - 0.4087882042 0.3979710937 0.3648093045 - 0.4099336863 0.3991281092 0.3659642935 - 0.4110778868 0.4002847970 0.3671188951 - 0.4122214019 0.4014416039 0.3682743013 - 0.4133622944 0.4025982916 0.3694328070 - 0.4145030975 0.4037550092 0.3705911934 - 0.4156427979 0.4049058855 0.3717437983 - 0.4167811871 0.4060513079 0.3728901148 - 0.4179196954 0.4071967900 0.3740364909 - 0.4190456867 0.4083349109 0.3751721084 - 0.4201678038 0.4094707966 0.3763037920 - 0.4212898910 0.4106067121 0.3774355054 - 0.4224098921 0.4117316902 0.3785631061 - 0.4235298932 0.4128564000 0.3796902895 - 0.4246496856 0.4139792919 0.3808172047 - 0.4257684946 0.4150904119 0.3819408119 - 0.4268873036 0.4162015915 0.3830643892 - 0.4280022085 0.4173096120 0.3841860890 - 0.4291062951 0.4184089899 0.3853017092 - 0.4302105010 0.4195083976 0.3864172995 - 0.4313144088 0.4206028879 0.3875293136 - 0.4324178994 0.4216887951 0.3886339068 - 0.4335213900 0.4227746129 0.3897385001 - 0.4346221089 0.4238578081 0.3908396959 - 0.4357188940 0.4249371886 0.3919354081 - 0.4368157089 0.4260165095 0.3930310011 - 0.4379082918 0.4270943105 0.3941242993 - 0.4389953017 0.4281699061 0.3952139020 - 0.4400823116 0.4292455018 0.3963035047 - 0.4411674142 0.4303215146 0.3973940909 - 0.4422496855 0.4313979149 0.3984861970 - 0.4433319867 0.4324742854 0.3995783925 - 0.4444083869 0.4335477054 0.4006663859 - 0.4454728067 0.4346148074 0.4017446041 - 0.4465371966 0.4356819093 0.4028229117 - 0.4475978017 0.4367474020 0.4038993120 - 0.4486451149 0.4378075898 0.4049679935 - 0.4496923983 0.4388678074 0.4060367942 - 0.4507394135 0.4399277866 0.4071055055 - 0.4517830014 0.4409854114 0.4081743062 - 0.4528265893 0.4420430064 0.4092431962 - 0.4538702071 0.4431006014 0.4103119969 - 0.4549070001 0.4441545904 0.4113866985 - 0.4559428096 0.4452080131 0.4124625027 - 0.4569785893 0.4462614954 0.4135383964 - 0.4580070078 0.4473111033 0.4146097898 - 0.4590308964 0.4483582973 0.4156782031 - 0.4600548148 0.4494056106 0.4167467058 - 0.4610792100 0.4504511952 0.4178118110 - 0.4621045887 0.4514938891 0.4188697040 - 0.4631299078 0.4525366127 0.4199275970 - 0.4641552866 0.4535791874 0.4209854901 - 0.4651829898 0.4546158910 0.4220423996 - 0.4662106931 0.4556525946 0.4230993092 - 0.4672383964 0.4566892982 0.4241561890 - 0.4682591856 0.4577207863 0.4252122045 - 0.4692761898 0.4587494135 0.4262678027 - 0.4702933133 0.4597780108 0.4273233116 - 0.4713107944 0.4608063996 0.4283765852 - 0.4723303020 0.4618338943 0.4294205010 - 0.4733496904 0.4628615081 0.4304643869 - 0.4743691981 0.4638890922 0.4315083921 - 0.4753845930 0.4649214149 0.4325434864 - 0.4763987958 0.4659551084 0.4335753918 - 0.4774129987 0.4669888914 0.4346072972 - 0.4784238040 0.4680216014 0.4356386065 - 0.4794248044 0.4690510035 0.4366680086 - 0.4804258049 0.4700804055 0.4376974106 - 0.4814268053 0.4711098969 0.4387268126 - 0.4824292958 0.4721364081 0.4397540987 - 0.4834325016 0.4731616974 0.4407801926 - 0.4844357967 0.4741868973 0.4418064058 - 0.4854384959 0.4752117991 0.4428318143 - 0.4864360988 0.4762332141 0.4438445866 - 0.4874337018 0.4772545099 0.4448573887 - 0.4884313047 0.4782758951 0.4458701015 - 0.4894233942 0.4792912900 0.4468775094 - 0.4904080927 0.4802986085 0.4478765130 - 0.4913927913 0.4813058972 0.4488754869 - 0.4923774898 0.4823131859 0.4498746097 - 0.4933621883 0.4833154976 0.4508705139 - 0.4943468869 0.4843156934 0.4518648982 - 0.4953315854 0.4853160083 0.4528594017 - 0.4963161945 0.4863162041 0.4538537860 - 0.4972892106 0.4873186946 0.4548524916 - 0.4982616901 0.4883211851 0.4558514953 - 0.4992341995 0.4893237948 0.4568504989 - 0.5002049804 0.4903247058 0.4578497112 - 0.5011661053 0.4913162887 0.4588507116 - 0.5021272898 0.4923079908 0.4598517120 - 0.5030884743 0.4932996035 0.4608527124 - 0.5040500164 0.4942877889 0.4618506134 - 0.5050122142 0.4952678084 0.4628399909 - 0.5059744120 0.4962477982 0.4638293982 - 0.5069364905 0.4972277880 0.4648188055 - 0.5078973770 0.4982069135 0.4658063948 - 0.5088561773 0.4991844893 0.4667905867 - 0.5098149180 0.5001621842 0.4677748084 - 0.5107737184 0.5011398196 0.4687590897 - 0.5117294192 0.5021172762 0.4697437882 - 0.5126808882 0.5030944943 0.4707294106 - 0.5136322975 0.5040717721 0.4717150033 - 0.5145838261 0.5050491095 0.4727005959 - 0.5155330896 0.5060225725 0.4736824036 - 0.5164790154 0.5069903135 0.4746578038 - 0.5174250007 0.5079579949 0.4756332040 - 0.5183709264 0.5089256763 0.4766086042 - 0.5193127990 0.5098916888 0.4775812924 - 0.5202462077 0.5108541250 0.4785476029 - 0.5211796165 0.5118165016 0.4795139134 - 0.5221130252 0.5127789974 0.4804801941 - 0.5230445862 0.5137407780 0.4814454019 - 0.5239686966 0.5147001743 0.4824050069 - 0.5248929262 0.5156595111 0.4833647013 - 0.5258170962 0.5166189075 0.4843243062 - 0.5267412066 0.5175781846 0.4852839112 - 0.5276607275 0.5185251236 0.4862455130 - 0.5285801291 0.5194721222 0.4872071147 - 0.5294994712 0.5204190016 0.4881686866 - 0.5304189920 0.5213660002 0.4891302884 - 0.5313292146 0.5223047137 0.4900931120 - 0.5322365761 0.5232409239 0.4910562038 - 0.5331441164 0.5241770744 0.4920192957 - 0.5340514779 0.5251134038 0.4929825068 - 0.5349549055 0.5260437131 0.4939421117 - 0.5358536243 0.5269672871 0.4948973954 - 0.5367524028 0.5278909802 0.4958527088 - 0.5376511216 0.5288146138 0.4968079925 - 0.5385501981 0.5297368169 0.4977625012 - 0.5394514799 0.5306468010 0.4987092912 - 0.5403528214 0.5315569043 0.4996561110 - 0.5412541032 0.5324670076 0.5006029010 - 0.5421553850 0.5333769917 0.5015497208 - 0.5430505276 0.5342804193 0.5024966002 - 0.5439429879 0.5351806879 0.5034434199 - 0.5448353887 0.5360810161 0.5043902993 - 0.5457279086 0.5369812846 0.5053371191 - 0.5466185808 0.5378801227 0.5062835217 - 0.5475032926 0.5387730002 0.5072280169 - 0.5483881235 0.5396658778 0.5081725121 - 0.5492727757 0.5405586958 0.5091168880 - 0.5501574874 0.5414515734 0.5100613832 - 0.5510445833 0.5423436761 0.5109959245 - 0.5519328117 0.5432354808 0.5119252205 - 0.5528209805 0.5441272259 0.5128543973 - 0.5537092090 0.5450189710 0.5137836933 - 0.5545967221 0.5459102988 0.5147123933 - 0.5554770827 0.5467962027 0.5156326890 - 0.5563575029 0.5476821065 0.5165529251 - 0.5572379231 0.5485680103 0.5174731016 - 0.5581182837 0.5494539142 0.5183933973 - 0.5589959025 0.5503361821 0.5193132758 - 0.5598695874 0.5512136221 0.5202329159 - 0.5607432723 0.5520910025 0.5211526155 - 0.5616170168 0.5529683828 0.5220721960 - 0.5624907017 0.5538457036 0.5229917765 - 0.5633659959 0.5547165871 0.5239105821 - 0.5642418861 0.5555846095 0.5248289704 - 0.5651177168 0.5564525723 0.5257474184 - 0.5659936070 0.5573204756 0.5266659260 - 0.5668694973 0.5581884980 0.5275843143 - 0.5677334070 0.5590493083 0.5284937024 - 0.5685961246 0.5599092841 0.5294020176 - 0.5694587827 0.5607693791 0.5303102136 - 0.5703215003 0.5616294146 0.5312184095 - 0.5711839199 0.5624892712 0.5321263075 - 0.5720422268 0.5633450747 0.5330240726 - 0.5729004741 0.5642009974 0.5339217782 - 0.5737587214 0.5650569201 0.5348194838 - 0.5746170282 0.5659127235 0.5357171893 - 0.5754747987 0.5667679906 0.5366147161 - 0.5763297081 0.5676189065 0.5375100970 - 0.5771846175 0.5684698820 0.5384054184 - 0.5780395269 0.5693207979 0.5393007994 - 0.5788943768 0.5701717734 0.5401961803 - 0.5797473192 0.5710228086 0.5410919785 - 0.5805879831 0.5718737841 0.5419911146 - 0.5814288259 0.5727248788 0.5428900719 - 0.5822694898 0.5735759735 0.5437892079 - 0.5831102133 0.5744271278 0.5446882248 - 0.5839506984 0.5752779245 0.5455867052 - 0.5847879052 0.5761266947 0.5464770794 - 0.5856251717 0.5769755244 0.5473675132 - 0.5864624977 0.5778242946 0.5482578874 - 0.5872997046 0.5786731243 0.5491483212 - 0.5881369710 0.5795218945 0.5500386953 - 0.5889738798 0.5803651810 0.5509185791 - 0.5898107290 0.5812082887 0.5517979264 - 0.5906475782 0.5820513964 0.5526772141 - 0.5914844871 0.5828943849 0.5535565019 - 0.5923212767 0.5837374926 0.5544357896 - 0.5931525826 0.5845798850 0.5553128123 - 0.5939822793 0.5854220986 0.5561890006 - 0.5948119760 0.5862641931 0.5570651889 - 0.5956416130 0.5871064067 0.5579413772 - 0.5964713097 0.5879485011 0.5588176250 - 0.5972992182 0.5887913704 0.5596930981 - 0.5981255174 0.5896348953 0.5605677962 - 0.5989518166 0.5904784799 0.5614423752 - 0.5997781157 0.5913220048 0.5623170733 - 0.6006044149 0.5921655297 0.5631917715 - 0.6014305949 0.5930098891 0.5640664101 - 0.6022564769 0.5938585997 0.5649405122 - 0.6030824184 0.5947071910 0.5658146739 - 0.6039084196 0.5955557823 0.5666887760 - 0.6047343016 0.5964043736 0.5675628781 - 0.6055601835 0.5972530246 0.5684370995 - 0.6063808799 0.5980986953 0.5693032742 - 0.6072000861 0.5989435911 0.5701668262 - 0.6080192924 0.5997886062 0.5710303783 - 0.6088384986 0.6006335020 0.5718938708 - 0.6096577048 0.6014783978 0.5727574229 - 0.6104760170 0.6023222804 0.5736197233 - 0.6112921238 0.6031637192 0.5744786263 - 0.6121081710 0.6040052176 0.5753375292 - 0.6129242778 0.6048465967 0.5761963725 - 0.6137403846 0.6056880951 0.5770552158 - 0.6145564914 0.6065294743 0.5779141188 - 0.6153724194 0.6073685288 0.5787752271 - 0.6161882877 0.6082066298 0.5796372294 - 0.6170042157 0.6090446711 0.5804991722 - 0.6178200245 0.6098828912 0.5813611746 - 0.6186358929 0.6107209921 0.5822231770 - 0.6194511056 0.6115579009 0.5830852985 - 0.6202605963 0.6123844981 0.5839483142 - 0.6210700870 0.6132110953 0.5848112106 - 0.6218796968 0.6140376925 0.5856742263 - 0.6226891875 0.6148642898 0.5865371823 - 0.6234986782 0.6156908870 0.5874001980 - 0.6243069768 0.6165152192 0.5882589817 - 0.6251134276 0.6173359752 0.5891109109 - 0.6259198189 0.6181567907 0.5899628997 - 0.6267263293 0.6189776063 0.5908148289 - 0.6275327206 0.6197984219 0.5916668177 - 0.6283391118 0.6206191778 0.5925186872 - 0.6291438937 0.6214398742 0.5933675170 - 0.6299477220 0.6222605109 0.5942140818 - 0.6307514906 0.6230810285 0.5950608253 - 0.6315551996 0.6239016056 0.5959073901 - 0.6323590279 0.6247221231 0.5967540145 - 0.6331627965 0.6255427003 0.5976006985 - 0.6339570284 0.6263571978 0.5984491706 - 0.6347482204 0.6271697283 0.5992982984 - 0.6355394125 0.6279821992 0.6001474261 - 0.6363306046 0.6287947297 0.6009964943 - 0.6371217966 0.6296073198 0.6018456221 - 0.6379129887 0.6304197907 0.6026948094 - 0.6387022138 0.6312273741 0.6035407782 - 0.6394910812 0.6320338845 0.6043862104 - 0.6402798891 0.6328405142 0.6052315235 - 0.6410686970 0.6336470246 0.6060767770 - 0.6418575048 0.6344534755 0.6069220901 - 0.6426463127 0.6352599859 0.6077674031 - 0.6434336901 0.6360661983 0.6086056828 - 0.6442208290 0.6368721128 0.6094418764 - 0.6450079083 0.6376780868 0.6102781296 - 0.6457949877 0.6384841204 0.6111143827 - 0.6465821266 0.6392900944 0.6119505763 - 0.6473692060 0.6400961280 0.6127868295 - 0.6481484771 0.6408988237 0.6136180162 - 0.6489244103 0.6417000294 0.6144466996 - 0.6497004032 0.6425011158 0.6152755022 - 0.6504762769 0.6433023214 0.6161041856 - 0.6512522101 0.6441034079 0.6169329286 - 0.6520280838 0.6449046135 0.6177616119 - 0.6528034210 0.6457012296 0.6185904145 - 0.6535782218 0.6464933753 0.6194192767 - 0.6543530226 0.6472855210 0.6202481985 - 0.6551278234 0.6480777264 0.6210771203 - 0.6559026241 0.6488698125 0.6219059825 - 0.6566773057 0.6496618986 0.6227349043 - 0.6574509144 0.6504533291 0.6235638261 - 0.6582205296 0.6512417793 0.6243926287 - 0.6589900851 0.6520304084 0.6252213120 - 0.6597597003 0.6528189778 0.6260501146 - 0.6605293155 0.6536074877 0.6268787980 - 0.6612988710 0.6543961167 0.6277076006 - 0.6620684266 0.6551846862 0.6285362840 - 0.6628314257 0.6559717059 0.6293578744 - 0.6635934114 0.6567586064 0.6301782727 - 0.6643555164 0.6575453877 0.6309986711 - 0.6651175022 0.6583322883 0.6318191290 - 0.6658794880 0.6591191292 0.6326395273 - 0.6666415930 0.6599059105 0.6334599257 - 0.6674026251 0.6606888175 0.6342766881 - 0.6681625247 0.6614663005 0.6350883245 - 0.6689224243 0.6622439027 0.6359000206 - 0.6696822047 0.6630213857 0.6367117167 - 0.6704421043 0.6637989879 0.6375232935 - 0.6712018847 0.6645764709 0.6383349895 - 0.6719617844 0.6653540134 0.6391466856 - 0.6727159023 0.6661242843 0.6399570704 - 0.6734694839 0.6668937802 0.6407673955 - 0.6742231250 0.6676632762 0.6415776014 - 0.6749765873 0.6684327722 0.6423879266 - 0.6757302284 0.6692023277 0.6431981921 - 0.6764838099 0.6699718237 0.6440083981 - 0.6772350073 0.6707385778 0.6448181868 - 0.6779803038 0.6714984775 0.6456267834 - 0.6787256002 0.6722583771 0.6464353204 - 0.6794708967 0.6730182767 0.6472437978 - 0.6802163124 0.6737781763 0.6480523944 - 0.6809616089 0.6745380759 0.6488608718 - 0.6817069054 0.6752979755 0.6496694088 - 0.6824505925 0.6760519147 0.6504725218 - 0.6831930876 0.6768013239 0.6512712836 - 0.6839357018 0.6775506735 0.6520701051 - 0.6846781969 0.6783000827 0.6528689265 - 0.6854206920 0.6790493727 0.6536676884 - 0.6861631870 0.6797987819 0.6544665098 - 0.6869056821 0.6805481911 0.6552652717 - 0.6876466274 0.6812918782 0.6560565233 - 0.6883869171 0.6820340157 0.6568452716 - 0.6891273260 0.6827760935 0.6576340795 - 0.6898676157 0.6835181713 0.6584228873 - 0.6906079054 0.6842603087 0.6592115760 - 0.6913483143 0.6850023866 0.6600003839 - 0.6920886040 0.6857444048 0.6607891917 - 0.6928188801 0.6864817142 0.6615756750 - 0.6935479045 0.6872183084 0.6623616815 - 0.6942768097 0.6879549026 0.6631476879 - 0.6950057745 0.6886916161 0.6639338136 - 0.6957347989 0.6894282103 0.6647198200 - 0.6964638233 0.6901648045 0.6655058861 - 0.6971927285 0.6909013987 0.6662918925 - 0.6979197860 0.6916376948 0.6670784950 - 0.6986466050 0.6923738718 0.6678652167 - 0.6993734241 0.6931101084 0.6686518788 - 0.7001003027 0.6938462853 0.6694386005 - 0.7008271217 0.6945825219 0.6702252030 - 0.7015538812 0.6953186989 0.6710119247 - 0.7022808194 0.6960549951 0.6717985868 - 0.7030063272 0.6967895031 0.6725746989 - 0.7037315965 0.6975237131 0.6733481884 - 0.7044569254 0.6982578039 0.6741216779 - 0.7051821947 0.6989920139 0.6748952270 - 0.7059075236 0.6997262239 0.6756685972 - 0.7066327929 0.7004603744 0.6764420867 - 0.7073581219 0.7011945248 0.6772155762 - 0.7080765963 0.7019280195 0.6779829264 - 0.7087914944 0.7026610970 0.6787465811 - 0.7095062733 0.7033942938 0.6795102954 - 0.7102211714 0.7041273713 0.6802740097 - 0.7109360099 0.7048605084 0.6810376048 - 0.7116509080 0.7055935860 0.6818013191 - 0.7123658061 0.7063267231 0.6825649738 - 0.7130801082 0.7070569992 0.6833276749 - 0.7137935758 0.7077823877 0.6840885878 - 0.7145071030 0.7085077763 0.6848495007 - 0.7152206898 0.7092332244 0.6856104732 - 0.7159342170 0.7099586129 0.6863713861 - 0.7166476846 0.7106840014 0.6871322989 - 0.7173612118 0.7114093900 0.6878932714 - 0.7180746794 0.7121347785 0.6886541843 - 0.7187874913 0.7128537893 0.6894174218 - 0.7195001841 0.7135729194 0.6901807785 - 0.7202129960 0.7142918706 0.6909440756 - 0.7209258080 0.7150110006 0.6917073727 - 0.7216385007 0.7157300115 0.6924707294 - 0.7223513126 0.7164490223 0.6932340264 - 0.7230641246 0.7171680927 0.6939973235 - 0.7237719297 0.7178853154 0.6947563887 - 0.7244753242 0.7186009884 0.6955114007 - 0.7251787782 0.7193167210 0.6962664127 - 0.7258821726 0.7200323939 0.6970214248 - 0.7265856862 0.7207481265 0.6977763772 - 0.7272890806 0.7214637995 0.6985313892 - 0.7279925942 0.7221794128 0.6992864013 - 0.7286959887 0.7228950858 0.7000414133 - 0.7293974161 0.7236070037 0.7007868290 - 0.7300986052 0.7243188024 0.7015315890 - 0.7307999134 0.7250304818 0.7022762895 - 0.7315011024 0.7257422209 0.7030211091 - 0.7322024107 0.7264540195 0.7037658095 - 0.7329035997 0.7271656990 0.7045106292 - 0.7336049080 0.7278773785 0.7052553296 - 0.7343035936 0.7285876274 0.7059991956 - 0.7349956036 0.7292935252 0.7067406774 - 0.7356876731 0.7299994826 0.7074822187 - 0.7363798022 0.7307054996 0.7082235813 - 0.7370718122 0.7314115167 0.7089651227 - 0.7377638817 0.7321174741 0.7097066045 - 0.7384558916 0.7328234911 0.7104480267 - 0.7391480207 0.7335295081 0.7111895084 - 0.7398372293 0.7342340946 0.7119327188 - 0.7405235767 0.7349374294 0.7126777768 - 0.7412099838 0.7356405854 0.7134228945 - 0.7418963909 0.7363438010 0.7141680121 - 0.7425827980 0.7370470762 0.7149130702 - 0.7432692051 0.7377502918 0.7156581879 - 0.7439556122 0.7384536266 0.7164033055 - 0.7446420193 0.7391567826 0.7171484232 - 0.7453268766 0.7398598194 0.7178943753 - 0.7460108995 0.7405626178 0.7186409831 - 0.7466949224 0.7412652969 0.7193875909 - 0.7473788857 0.7419680953 0.7201341987 - 0.7480629086 0.7426708937 0.7208808064 - 0.7487468719 0.7433736920 0.7216274142 - 0.7494308949 0.7440764904 0.7223740220 - 0.7501149178 0.7447792292 0.7231205106 - 0.7507920861 0.7454807758 0.7238616943 - 0.7514652014 0.7461814880 0.7245994210 - 0.7521383166 0.7468823195 0.7253370285 - 0.7528113127 0.7475829720 0.7260746956 - 0.7534844279 0.7482838035 0.7268123031 - 0.7541574836 0.7489845753 0.7275499105 - 0.7548305988 0.7496852875 0.7282875776 - 0.7555037141 0.7503861189 0.7290251851 - 0.7561715245 0.7510858178 0.7297608852 - 0.7568343878 0.7517846227 0.7304946780 - 0.7574973106 0.7524833083 0.7312284708 - 0.7581601739 0.7531821132 0.7319623232 - 0.7588232160 0.7538809180 0.7326961160 - 0.7594860792 0.7545796037 0.7334299088 - 0.7601490021 0.7552784085 0.7341635823 - 0.7608119249 0.7559772134 0.7348973751 - 0.7614743710 0.7566753030 0.7356321216 - 0.7621356845 0.7573720217 0.7363687754 - 0.7627969980 0.7580686808 0.7371054888 - 0.7634583116 0.7587653995 0.7378422022 - 0.7641196251 0.7594621181 0.7385789156 - 0.7647809982 0.7601587772 0.7393156290 - 0.7654423118 0.7608554959 0.7400524020 - 0.7661036253 0.7615522146 0.7407891154 - 0.7667648792 0.7622489929 0.7415258288 - 0.7674220204 0.7629417777 0.7422630191 - 0.7680791020 0.7636345029 0.7430003285 - 0.7687361240 0.7643272877 0.7437375188 - 0.7693930864 0.7650200725 0.7444748282 - 0.7700502276 0.7657127976 0.7452120185 - 0.7707071900 0.7664055824 0.7459493279 - 0.7713642120 0.7670983076 0.7466865182 - 0.7720212936 0.7677910924 0.7474238276 - 0.7726746202 0.7684819102 0.7481557727 - 0.7733249068 0.7691711783 0.7488833070 - 0.7739751935 0.7698603868 0.7496107817 - 0.7746254802 0.7705495954 0.7503383160 - 0.7752758265 0.7712389231 0.7510657907 - 0.7759261131 0.7719280720 0.7517933249 - 0.7765762806 0.7726173997 0.7525207996 - 0.7772266269 0.7733066082 0.7532482743 - 0.7778769135 0.7739958167 0.7539758086 - 0.7785270810 0.7746813893 0.7546988130 - 0.7791771889 0.7753669024 0.7554215789 - 0.7798274159 0.7760524154 0.7561445236 - 0.7804775238 0.7767379284 0.7568672895 - 0.7811276913 0.7774235010 0.7575901747 - 0.7817777991 0.7781090140 0.7583130002 - 0.7824279070 0.7787945271 0.7590358853 - 0.7830780745 0.7794799805 0.7597587109 - 0.7837272286 0.7801622152 0.7604821920 - 0.7843742967 0.7808375955 0.7612072229 - 0.7850214839 0.7815129757 0.7619320750 - 0.7856686115 0.7821882963 0.7626569867 - 0.7863156796 0.7828636765 0.7633818984 - 0.7869629264 0.7835391164 0.7641068101 - 0.7876099944 0.7842144966 0.7648317218 - 0.7882571220 0.7848898768 0.7655565739 - 0.7889041901 0.7855653167 0.7662814856 - 0.7895485163 0.7862368822 0.7670075297 - 0.7901902795 0.7869054079 0.7677345276 - 0.7908321023 0.7875739932 0.7684614062 - 0.7914739847 0.7882425189 0.7691884041 - 0.7921158075 0.7889109850 0.7699152827 - 0.7927575707 0.7895795107 0.7706422806 - 0.7933995128 0.7902479768 0.7713692188 - 0.7940412760 0.7909166217 0.7720962167 - 0.7946830988 0.7915850878 0.7728230953 - 0.7953270078 0.7922512889 0.7735466957 - 0.7959719896 0.7929161191 0.7742683291 - 0.7966169715 0.7935810089 0.7749899030 - 0.7972620130 0.7942458987 0.7757114768 - 0.7979069948 0.7949107289 0.7764332294 - 0.7985519171 0.7955756187 0.7771548033 - 0.7991968989 0.7962405086 0.7778763771 - 0.7998418808 0.7969053984 0.7785980105 - 0.8004869223 0.7975702286 0.7793195844 - 0.8011307120 0.7982336879 0.7800378203 - 0.8017739058 0.7988961935 0.7807539105 - 0.8024169803 0.7995588183 0.7814698815 - 0.8030601144 0.8002213836 0.7821859121 - 0.8037033081 0.8008840084 0.7829018831 - 0.8043463826 0.8015465140 0.7836179137 - 0.8049895763 0.8022090793 0.7843340039 - 0.8056327105 0.8028717041 0.7850499749 - 0.8062757850 0.8035342097 0.7857660055 - 0.8069155812 0.8041942716 0.7864825130 - 0.8075515032 0.8048514128 0.7871994972 - 0.8081874251 0.8055086136 0.7879164815 - 0.8088234067 0.8061658144 0.7886335254 - 0.8094593287 0.8068230152 0.7893505096 - 0.8100953102 0.8074802160 0.7900676131 - 0.8107311726 0.8081374168 0.7907845974 - 0.8113672137 0.8087946177 0.7915015817 - 0.8120030761 0.8094516993 0.7922186255 - 0.8126395941 0.8101087809 0.7929350138 - 0.8132783771 0.8107650876 0.7936488986 - 0.8139171004 0.8114215136 0.7943627834 - 0.8145558834 0.8120778203 0.7950767279 - 0.8151946068 0.8127341866 0.7957904935 - 0.8158333898 0.8133906126 0.7965043783 - 0.8164721131 0.8140469193 0.7972183228 - 0.8171108961 0.8147032857 0.7979322076 - 0.8177496195 0.8153595924 0.7986459732 - 0.8183884025 0.8160160184 0.7993599176 - 0.8190252781 0.8166726828 0.8000699878 - 0.8196616769 0.8173294067 0.8007791042 - 0.8202980757 0.8179860711 0.8014882803 - 0.8209344745 0.8186429143 0.8021973968 - 0.8215708733 0.8192995787 0.8029065132 - 0.8222072721 0.8199563026 0.8036156297 - 0.8228437901 0.8206130862 0.8043248057 - 0.8234801888 0.8212698102 0.8050339222 - 0.8241165876 0.8219264746 0.8057429790 - 0.8247510195 0.8225817084 0.8064500093 - 0.8253805041 0.8232325912 0.8071511984 - 0.8260099292 0.8238834739 0.8078523874 - 0.8266394138 0.8245344758 0.8085535169 - 0.8272687793 0.8251854181 0.8092547059 - 0.8278983235 0.8258364201 0.8099558949 - 0.8285278082 0.8264873028 0.8106570840 - 0.8291571736 0.8271383047 0.8113582730 - 0.8297867179 0.8277891874 0.8120595217 - 0.8304160833 0.8284401298 0.8127607107 - 0.8310465217 0.8290880919 0.8134608269 - 0.8316774964 0.8297342062 0.8141604066 - 0.8323084116 0.8303803205 0.8148599267 - 0.8329393864 0.8310263753 0.8155595064 - 0.8335703015 0.8316724896 0.8162590265 - 0.8342012763 0.8323186040 0.8169584870 - 0.8348321915 0.8329647779 0.8176581264 - 0.8354632258 0.8336108923 0.8183575869 - 0.8360940814 0.8342570066 0.8190571070 - 0.8367251158 0.8349031210 0.8197566867 - 0.8373507261 0.8355441093 0.8204557896 - 0.8379753232 0.8361840844 0.8211548924 - 0.8385999203 0.8368241787 0.8218539953 - 0.8392245173 0.8374642134 0.8225530982 - 0.8398491144 0.8381041884 0.8232522011 - 0.8404737711 0.8387442231 0.8239513040 - 0.8410984278 0.8393843174 0.8246504068 - 0.8417230248 0.8400242925 0.8253493905 - 0.8423476219 0.8406643271 0.8260484934 - 0.8429722190 0.8413044214 0.8267475963 - 0.8435915112 0.8419380188 0.8274425268 - 0.8442103863 0.8425710201 0.8281369209 - 0.8448292017 0.8432040811 0.8288313746 - 0.8454480767 0.8438372016 0.8295258284 - 0.8460670114 0.8444703221 0.8302202821 - 0.8466858268 0.8451033831 0.8309146762 - 0.8473047018 0.8457365036 0.8316091895 - 0.8479235768 0.8463695049 0.8323035836 - 0.8485423923 0.8470026255 0.8329980969 - 0.8491613269 0.8476356864 0.8336924911 - 0.8497756720 0.8482635021 0.8343786001 - 0.8503894210 0.8488906026 0.8350632787 - 0.8510032296 0.8495177031 0.8357480764 - 0.8516169190 0.8501448035 0.8364328146 - 0.8522306085 0.8507717848 0.8371176124 - 0.8528444171 0.8513988853 0.8378022909 - 0.8534581065 0.8520259857 0.8384870291 - 0.8540717959 0.8526530266 0.8391718268 - 0.8546856046 0.8532801270 0.8398565054 - 0.8552992940 0.8539072275 0.8405413032 - 0.8559076786 0.8545295000 0.8412235975 - 0.8565136194 0.8551496863 0.8419046998 - 0.8571195006 0.8557698727 0.8425859213 - 0.8577255011 0.8563901186 0.8432670832 - 0.8583313823 0.8570103049 0.8439481854 - 0.8589373231 0.8576303720 0.8446294069 - 0.8595432043 0.8582506180 0.8453105092 - 0.8601492047 0.8588708043 0.8459916711 - 0.8607550859 0.8594909906 0.8466727734 - 0.8613610268 0.8601111770 0.8473539948 - 0.8619657755 0.8607302904 0.8480353951 - 0.8625686765 0.8613476157 0.8487172723 - 0.8631715178 0.8619648814 0.8493992090 - 0.8637742996 0.8625822067 0.8500810862 - 0.8643772006 0.8631994724 0.8507630825 - 0.8649799824 0.8638167977 0.8514450192 - 0.8655828238 0.8644341230 0.8521268964 - 0.8661857247 0.8650513291 0.8528087735 - 0.8667885065 0.8656685948 0.8534907103 - 0.8673914075 0.8662859201 0.8541725874 - 0.8679941893 0.8669031858 0.8548545241 - 0.8685936928 0.8675162196 0.8555287719 - 0.8691927195 0.8681287169 0.8562021255 - 0.8697918057 0.8687412143 0.8568754196 - 0.8703907728 0.8693537116 0.8575487137 - 0.8709899187 0.8699662089 0.8582220078 - 0.8715888858 0.8705787063 0.8588953018 - 0.8721879721 0.8711912036 0.8595685959 - 0.8727869987 0.8718037009 0.8602418900 - 0.8733860850 0.8724161983 0.8609151840 - 0.8739851117 0.8730286956 0.8615884781 - 0.8745809793 0.8736391068 0.8622589707 - 0.8751688004 0.8742442727 0.8629223108 - 0.8757566214 0.8748494983 0.8635854721 - 0.8763445020 0.8754547238 0.8642486930 - 0.8769323230 0.8760598898 0.8649119735 - 0.8775202036 0.8766651750 0.8655751944 - 0.8781080246 0.8772704005 0.8662384152 - 0.8786959052 0.8778756261 0.8669016957 - 0.8792837262 0.8784807920 0.8675649166 - 0.8798714876 0.8790860176 0.8682280779 - 0.8804594278 0.8796913028 0.8688914180 - 0.8810451031 0.8802946210 0.8695524931 - 0.8816289902 0.8808963895 0.8702116013 - 0.8822128773 0.8814982176 0.8708708286 - 0.8827967048 0.8820999861 0.8715299964 - 0.8833805919 0.8827018142 0.8721891046 - 0.8839644790 0.8833035827 0.8728482723 - 0.8845484257 0.8839052916 0.8735073805 - 0.8851323128 0.8845071197 0.8741666079 - 0.8857161999 0.8851088881 0.8748257756 - 0.8863000274 0.8857107162 0.8754848838 - 0.8868839145 0.8863124847 0.8761441112 - 0.8874658942 0.8869127035 0.8768044114 - 0.8880469203 0.8875120878 0.8774654865 - 0.8886278272 0.8881114721 0.8781265020 - 0.8892087936 0.8887107968 0.8787875772 - 0.8897898197 0.8893101811 0.8794485927 - 0.8903707266 0.8899096251 0.8801097274 - 0.8909516931 0.8905088902 0.8807706833 - 0.8915325999 0.8911082745 0.8814318180 - 0.8921136260 0.8917077184 0.8820927739 - 0.8926944733 0.8923069835 0.8827539086 - 0.8932754993 0.8929064274 0.8834149241 - 0.8938503861 0.8935058713 0.8840740919 - 0.8944211006 0.8941053748 0.8847320080 - 0.8949918151 0.8947049975 0.8853899240 - 0.8955624104 0.8953045011 0.8860477805 - 0.8961331248 0.8959041238 0.8867056966 - 0.8967037797 0.8965036273 0.8873636127 - 0.8972744942 0.8971031904 0.8880215287 - 0.8978450894 0.8977026939 0.8886793852 - 0.8984158039 0.8983023167 0.8893373013 - 0.8989865184 0.8989018202 0.8899952173 - 0.8995571733 0.8995013237 0.8906530738 - 0.9001259804 0.9000989199 0.8913081884 - 0.9006919265 0.9006931782 0.8919588923 - 0.9012578726 0.9012876153 0.8926094770 - 0.9018238187 0.9018818736 0.8932601810 - 0.9023898244 0.9024763107 0.8939108849 - 0.9029557109 0.9030706286 0.8945615888 - 0.9035217166 0.9036650062 0.8952121735 - 0.9040876031 0.9042593241 0.8958628774 - 0.9046536088 0.9048537016 0.8965135813 - 0.9052194953 0.9054480195 0.8971642852 - 0.9057855010 0.9060423970 0.8978149295 - 0.9063512087 0.9066365957 0.8984653950 - 0.9069120884 0.9072268009 0.8991103768 - 0.9074729085 0.9078170061 0.8997554183 - 0.9080337882 0.9084072113 0.9004002810 - 0.9085947275 0.9089974165 0.9010453224 - 0.9091554880 0.9095876217 0.9016903043 - 0.9097164273 0.9101778865 0.9023352265 - 0.9102773070 0.9107680917 0.9029802084 - 0.9108381867 0.9113582969 0.9036251903 - 0.9113990068 0.9119485021 0.9042701721 - 0.9119598866 0.9125387073 0.9049150944 - 0.9125208259 0.9131289124 0.9055600762 - 0.9130790234 0.9137184024 0.9062044024 - 0.9136350155 0.9143071771 0.9068480134 - 0.9141910076 0.9148960114 0.9074916244 - 0.9147469997 0.9154847860 0.9081352949 - 0.9153029919 0.9160736799 0.9087789059 - 0.9158589840 0.9166625142 0.9094225168 - 0.9164149761 0.9172512889 0.9100661874 - 0.9169710279 0.9178401232 0.9107097983 - 0.9175270200 0.9184290171 0.9113535285 - 0.9180830121 0.9190177917 0.9119970798 - 0.9186390042 0.9196066260 0.9126406908 - 0.9191949964 0.9201954007 0.9132844210 - 0.9197478294 0.9207763076 0.9139227867 - 0.9203000069 0.9213560820 0.9145606160 - 0.9208523035 0.9219357967 0.9151983261 - 0.9214046001 0.9225155711 0.9158360958 - 0.9219568968 0.9230954051 0.9164738059 - 0.9225091934 0.9236751795 0.9171115756 - 0.9230613708 0.9242548943 0.9177492857 - 0.9236137271 0.9248347282 0.9183871150 - 0.9241660237 0.9254145026 0.9190248251 - 0.9247183204 0.9259942770 0.9196625948 - 0.9252706170 0.9265739918 0.9203003049 - 0.9258226752 0.9271532297 0.9209372997 - 0.9263734221 0.9277231097 0.9215620756 - 0.9269239902 0.9282929897 0.9221869111 - 0.9274746776 0.9288629889 0.9228116870 - 0.9280253053 0.9294329286 0.9234365225 - 0.9285759926 0.9300028086 0.9240614176 - 0.9291266203 0.9305726886 0.9246861935 - 0.9296773076 0.9311426282 0.9253110290 - 0.9302278757 0.9317126274 0.9259358048 - 0.9307786226 0.9322825074 0.9265605807 - 0.9313291907 0.9328523874 0.9271854162 - 0.9318798780 0.9334223270 0.9278101921 - 0.9324297905 0.9339916706 0.9284340143 - 0.9329723716 0.9345554709 0.9290462136 - 0.9335150719 0.9351192117 0.9296582937 - 0.9340577126 0.9356828928 0.9302704930 - 0.9346002936 0.9362466931 0.9308826923 - 0.9351428747 0.9368103743 0.9314947724 - 0.9356855154 0.9373741150 0.9321069717 - 0.9362280965 0.9379379153 0.9327191114 - 0.9367706776 0.9385015965 0.9333313107 - 0.9373133183 0.9390652776 0.9339435101 - 0.9378558993 0.9396290779 0.9345555902 - 0.9383984804 0.9401928186 0.9351677895 - 0.9389411211 0.9407566190 0.9357799292 - 0.9394807220 0.9413191080 0.9363880754 - 0.9400200844 0.9418814778 0.9369959235 - 0.9405596256 0.9424440265 0.9376037717 - 0.9410989881 0.9430063963 0.9382116795 - 0.9416384101 0.9435688853 0.9388195276 - 0.9421778917 0.9441313148 0.9394273758 - 0.9427173138 0.9446938038 0.9400352836 - 0.9432566762 0.9452561736 0.9406430721 - 0.9437962174 0.9458187222 0.9412509799 - 0.9443355799 0.9463812113 0.9418588877 - 0.9448750019 0.9469435811 0.9424667954 - 0.9454144835 0.9475061297 0.9430745840 - 0.9459543228 0.9480664730 0.9436799884 - 0.9464944005 0.9486256838 0.9442839026 - 0.9470344782 0.9491848946 0.9448878169 - 0.9475746155 0.9497442245 0.9454916716 - 0.9481146932 0.9503033757 0.9460955858 - 0.9486547709 0.9508627057 0.9466995001 - 0.9491950274 0.9514219165 0.9473034143 - 0.9497351050 0.9519811869 0.9479073286 - 0.9502751827 0.9525403976 0.9485111833 - 0.9508153200 0.9530997276 0.9491150975 - 0.9513553977 0.9536588788 0.9497190118 - 0.9518954754 0.9542182088 0.9503229856 - 0.9524348974 0.9547764063 0.9509251714 - 0.9529715180 0.9553294778 0.9515191913 - 0.9535080194 0.9558826089 0.9521132112 - 0.9540445209 0.9564357996 0.9527071714 - 0.9545810819 0.9569888711 0.9533010721 - 0.9551175833 0.9575420022 0.9538950920 - 0.9556540847 0.9580950737 0.9544891119 - 0.9561907053 0.9586482048 0.9550830722 - 0.9567272067 0.9592013955 0.9556770921 - 0.9572637081 0.9597545266 0.9562711120 - 0.9578003287 0.9603075981 0.9568650723 - 0.9583367705 0.9608607292 0.9574589729 - 0.9588732719 0.9614138007 0.9580529928 - 0.9594082236 0.9619622827 0.9586417079 - 0.9599416256 0.9625064731 0.9592257142 - 0.9604749084 0.9630507231 0.9598097205 - 0.9610083103 0.9635949135 0.9603936076 - 0.9615417123 0.9641392231 0.9609776139 - 0.9620749950 0.9646834135 0.9615616202 - 0.9626083970 0.9652276039 0.9621456265 - 0.9631417990 0.9657717943 0.9627295136 - 0.9636752009 0.9663161039 0.9633135200 - 0.9642084837 0.9668602943 0.9638975263 - 0.9647418857 0.9674044847 0.9644814134 - 0.9652752876 0.9679486752 0.9650654197 - 0.9658085704 0.9684929848 0.9656494260 - 0.9663383961 0.9690269232 0.9662256837 - 0.9668667912 0.9695569277 0.9667990208 - 0.9673951864 0.9700868130 0.9673721790 - 0.9679235816 0.9706168175 0.9679455161 - 0.9684519768 0.9711467028 0.9685187936 - 0.9689803720 0.9716767073 0.9690920115 - 0.9695087075 0.9722065926 0.9696652889 - 0.9700371027 0.9727365971 0.9702386260 - 0.9705654979 0.9732664824 0.9708117843 - 0.9710938931 0.9737964272 0.9713851213 - 0.9716222882 0.9743263721 0.9719583988 - 0.9721506834 0.9748563170 0.9725316167 - 0.9726790786 0.9753863215 0.9731048942 - 0.9732028842 0.9759041071 0.9736629725 - 0.9737251997 0.9764174819 0.9742156267 - 0.9742473960 0.9769309759 0.9747681022 - 0.9747695923 0.9774444103 0.9753205776 - 0.9752919078 0.9779577851 0.9758731723 - 0.9758141041 0.9784712195 0.9764257073 - 0.9763363004 0.9789845943 0.9769781828 - 0.9768586159 0.9794980288 0.9775307178 - 0.9773808122 0.9800114036 0.9780833125 - 0.9779031277 0.9805248976 0.9786357880 - 0.9784253240 0.9810382724 0.9791883230 - 0.9789475203 0.9815517068 0.9797409177 - 0.9794697762 0.9820650816 0.9802933931 - 0.9799848199 0.9825599790 0.9808300734 - 0.9804943800 0.9830408096 0.9813547134 - 0.9810041189 0.9835215807 0.9818794131 - 0.9815136790 0.9840024114 0.9824039936 - 0.9820232987 0.9844831824 0.9829286933 - 0.9825329781 0.9849640131 0.9834532738 - 0.9830425978 0.9854447842 0.9839779735 - 0.9835522175 0.9859256148 0.9845026135 - 0.9840617776 0.9864063859 0.9850273132 - 0.9845715165 0.9868872166 0.9855518937 - 0.9850810766 0.9873679876 0.9860765934 - 0.9855906963 0.9878488183 0.9866011739 - 0.9861003757 0.9883295894 0.9871258736 - 0.9866076112 0.9888011813 0.9876428843 - 0.9871065021 0.9892416000 0.9881339073 - 0.9876055121 0.9896820188 0.9886249900 - 0.9881045222 0.9901223779 0.9891160131 - 0.9886034727 0.9905629158 0.9896070957 - 0.9891024828 0.9910032749 0.9900981188 - 0.9896014929 0.9914436936 0.9905890822 - 0.9901003838 0.9918841124 0.9910802245 - 0.9905993938 0.9923244715 0.9915711880 - 0.9910984039 0.9927648902 0.9920622706 - 0.9915974140 0.9932053089 0.9925532937 - 0.9920964241 0.9936457276 0.9930443764 - 0.9925953746 0.9940860868 0.9935353994 - 0.9930943847 0.9945266247 0.9940264821 - 0.9935892224 0.9949312806 0.9944710732 - 0.9940823913 0.9953212142 0.9948964119 - 0.9945755005 0.9957110882 0.9953216910 - 0.9950686097 0.9961010218 0.9957470298 - 0.9955617785 0.9964908957 0.9961723089 - 0.9960548878 0.9968808293 0.9965975881 - 0.9965479970 0.9972707033 0.9970229268 - 0.9970412254 0.9976605773 0.9974482059 - 0.9975342751 0.9980505109 0.9978734851 - 0.9980275035 0.9984403849 0.9982988238 - 0.9985206127 0.9988303185 0.9987241030 - 0.9990137219 0.9992201924 0.9991493821 - 0.9995068908 0.9996101260 0.9995747209 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0114686396 0.0107471403 0.0087564439 + 0.0217512399 0.0202643294 0.0160814300 + 0.0303464700 0.0281717796 0.0221705399 + 0.0377242714 0.0349318609 0.0275946204 + 0.0443095490 0.0409396514 0.0324226208 + 0.0503021218 0.0464737117 0.0369786993 + 0.0558728315 0.0517093204 0.0412629806 + 0.0610256903 0.0566021390 0.0452965386 + 0.0659263134 0.0612265393 0.0491837002 + 0.0706434101 0.0656422004 0.0528963394 + 0.0751574785 0.0698595867 0.0564641915 + 0.0794606581 0.0738903284 0.0599343516 + 0.0835847408 0.0778156295 0.0632929727 + 0.0876147822 0.0816217065 0.0665517896 + 0.0915386677 0.0853431597 0.0697500929 + 0.0953285396 0.0889591500 0.0728649125 + 0.0989516675 0.0924931094 0.0759018511 + 0.1024663970 0.0958689004 0.0788350999 + 0.1058961004 0.0991332605 0.0816867501 + 0.1092602983 0.1023299024 0.0844683573 + 0.1125695035 0.1054401994 0.0872167125 + 0.1157974973 0.1085370034 0.0899668783 + 0.1189695969 0.1115973964 0.0926889405 + 0.1221098974 0.1145918965 0.0953712016 + 0.1251875013 0.1175365001 0.0979943499 + 0.1281960011 0.1204660982 0.1005420014 + 0.1311468929 0.1233218014 0.1030569002 + 0.1340449005 0.1260855943 0.1055212021 + 0.1368933022 0.1287900954 0.1079170033 + 0.1396844983 0.1314754933 0.1103053018 + 0.1424379051 0.1341181993 0.1126656979 + 0.1451424062 0.1367079020 0.1150159985 + 0.1478171945 0.1392980963 0.1173409969 + 0.1504548937 0.1418841928 0.1196570992 + 0.1530500054 0.1444393992 0.1219561994 + 0.1555911005 0.1469538957 0.1242327988 + 0.1581020057 0.1494355053 0.1264850050 + 0.1605874002 0.1518718004 0.1287028939 + 0.1630426943 0.1542987972 0.1308919936 + 0.1654826999 0.1566974074 0.1330559999 + 0.1679003984 0.1590469033 0.1351830959 + 0.1703007966 0.1613458991 0.1372949034 + 0.1726765931 0.1636165977 0.1393575966 + 0.1750312001 0.1658513993 0.1414173990 + 0.1773743033 0.1680632979 0.1434593946 + 0.1796950996 0.1702526957 0.1454783976 + 0.1819853038 0.1724139005 0.1474941969 + 0.1842298955 0.1745467931 0.1494760960 + 0.1864486933 0.1766640991 0.1514540017 + 0.1886311024 0.1787831932 0.1534274966 + 0.1907832026 0.1809104979 0.1553764045 + 0.1929026991 0.1830410957 0.1573255062 + 0.1950244009 0.1851601005 0.1592600048 + 0.1971314996 0.1872643977 0.1611716002 + 0.1992381066 0.1893534064 0.1630840003 + 0.2013241053 0.1914180070 0.1649879962 + 0.2034000009 0.1934722960 0.1668666005 + 0.2054556012 0.1955029964 0.1687465012 + 0.2074957937 0.1975183934 0.1706251055 + 0.2095358968 0.1995349973 0.1724743992 + 0.2115648985 0.2015223950 0.1743170023 + 0.2135839015 0.2034906000 0.1761588007 + 0.2155832052 0.2054294050 0.1779755950 + 0.2175676972 0.2073418945 0.1797723025 + 0.2195197046 0.2092444003 0.1815697998 + 0.2214522064 0.2111348063 0.1833599955 + 0.2233704031 0.2130120993 0.1851364970 + 0.2252652049 0.2148773074 0.1868986040 + 0.2271434069 0.2167267948 0.1886556000 + 0.2290212065 0.2185603976 0.1903920025 + 0.2308852971 0.2203781009 0.1921014935 + 0.2327376008 0.2221764028 0.1937994957 + 0.2345706969 0.2239618003 0.1954976022 + 0.2363775074 0.2257477045 0.1971796006 + 0.2381743938 0.2275269926 0.1988390982 + 0.2399536967 0.2292968929 0.2004887015 + 0.2417089045 0.2310636044 0.2021429986 + 0.2434546947 0.2328283936 0.2037986070 + 0.2451961935 0.2345885932 0.2054408938 + 0.2469274998 0.2363439947 0.2070686072 + 0.2486505061 0.2380899042 0.2086946070 + 0.2503683865 0.2398183942 0.2103239000 + 0.2520723045 0.2415340990 0.2119438946 + 0.2537688017 0.2432458997 0.2135417014 + 0.2554596066 0.2449575067 0.2151297927 + 0.2571457028 0.2466547936 0.2167214006 + 0.2588266134 0.2483399063 0.2183215022 + 0.2605035007 0.2500117123 0.2199174017 + 0.2621727884 0.2516688108 0.2215020061 + 0.2638291121 0.2533113062 0.2230799049 + 0.2654761076 0.2549428046 0.2246621996 + 0.2671324015 0.2565666139 0.2262382060 + 0.2687830031 0.2581740022 0.2278027982 + 0.2704266906 0.2597663105 0.2293560058 + 0.2720651031 0.2613466978 0.2309001982 + 0.2737013996 0.2629151940 0.2324444950 + 0.2753258049 0.2644810081 0.2339832038 + 0.2769433856 0.2660413086 0.2355152965 + 0.2785556018 0.2675940990 0.2370389998 + 0.2801620960 0.2691347897 0.2385461032 + 0.2817465067 0.2706714869 0.2400489002 + 0.2833232880 0.2722041011 0.2415542006 + 0.2848966122 0.2737326026 0.2430642992 + 0.2864696980 0.2752566040 0.2445680946 + 0.2880344093 0.2767753899 0.2460584939 + 0.2895927131 0.2782889903 0.2475391030 + 0.2911433876 0.2797945142 0.2490137070 + 0.2926709950 0.2812936008 0.2504926920 + 0.2941831052 0.2827874124 0.2519704103 + 0.2956764996 0.2842740119 0.2534419000 + 0.2971622944 0.2857593000 0.2548941076 + 0.2986418903 0.2872450948 0.2563388944 + 0.3001124859 0.2887324095 0.2577767074 + 0.3015663028 0.2902269065 0.2592169046 + 0.3030163050 0.2917178869 0.2606565952 + 0.3044652939 0.2931961119 0.2620925903 + 0.3059149981 0.2946687043 0.2635230124 + 0.3073624074 0.2961393893 0.2649494112 + 0.3087939918 0.2976126969 0.2663621902 + 0.3102258146 0.2990801930 0.2677738965 + 0.3116576970 0.3005422950 0.2691845894 + 0.3130910993 0.3019973040 0.2705928087 + 0.3145203888 0.3034510911 0.2719972134 + 0.3159373105 0.3049038947 0.2733885944 + 0.3173550963 0.3063476980 0.2747704983 + 0.3187735975 0.3077847958 0.2761443853 + 0.3201943040 0.3092113137 0.2775141895 + 0.3216128945 0.3106358945 0.2788833082 + 0.3230192065 0.3120585084 0.2802532911 + 0.3244253993 0.3134770095 0.2816143036 + 0.3258317113 0.3148897886 0.2829608023 + 0.3272288144 0.3162913918 0.2842969000 + 0.3286210001 0.3176868856 0.2856262922 + 0.3300077021 0.3190723956 0.2869521081 + 0.3313938081 0.3204556108 0.2882770896 + 0.3327834904 0.3218250871 0.2896099091 + 0.3341748118 0.3231914043 0.2909416854 + 0.3355718851 0.3245447874 0.2922688127 + 0.3369680941 0.3258947134 0.2935900092 + 0.3383626044 0.3272379041 0.2948979139 + 0.3397515118 0.3285779953 0.2962028980 + 0.3411338925 0.3299140036 0.2975037992 + 0.3425045013 0.3312394917 0.2988072932 + 0.3438651860 0.3325557113 0.3001132905 + 0.3452149034 0.3338659108 0.3014146984 + 0.3465574980 0.3351719081 0.3027124107 + 0.3478950858 0.3364771008 0.3039973974 + 0.3492299020 0.3377816975 0.3052738011 + 0.3505575955 0.3390836120 0.3065445125 + 0.3518814147 0.3403840065 0.3078114986 + 0.3531937003 0.3416804969 0.3090800047 + 0.3544990122 0.3429746926 0.3103496134 + 0.3558034003 0.3442664146 0.3116236925 + 0.3571070135 0.3455562890 0.3129017055 + 0.3584021032 0.3468429148 0.3141731024 + 0.3596883118 0.3481261134 0.3154368103 + 0.3609679937 0.3494060040 0.3166972995 + 0.3622376919 0.3506805003 0.3179521859 + 0.3635073900 0.3519532084 0.3192068040 + 0.3647772074 0.3532207906 0.3204610050 + 0.3660452962 0.3544875979 0.3217146099 + 0.3672988117 0.3557465971 0.3229593039 + 0.3685522974 0.3570055962 0.3242039979 + 0.3697921932 0.3582544923 0.3254339099 + 0.3710305989 0.3595022857 0.3266617060 + 0.3722676039 0.3607459962 0.3278838992 + 0.3735038042 0.3619877994 0.3291031122 + 0.3747293949 0.3632284999 0.3303220868 + 0.3759411871 0.3644677997 0.3315409124 + 0.3771511018 0.3657064140 0.3327583075 + 0.3783504069 0.3669404984 0.3339653015 + 0.3795498013 0.3681747019 0.3351722956 + 0.3807409108 0.3694047034 0.3363747001 + 0.3819305003 0.3706338108 0.3375759125 + 0.3831126988 0.3718604147 0.3387717009 + 0.3842875063 0.3730843961 0.3399612010 + 0.3854615092 0.3743064106 0.3411506116 + 0.3866302967 0.3755132854 0.3423393965 + 0.3877991140 0.3767201900 0.3435282111 + 0.3889720142 0.3779211938 0.3447239101 + 0.3901464939 0.3791198134 0.3459227979 + 0.3913210928 0.3803184032 0.3471212983 + 0.3924961090 0.3815169036 0.3483184874 + 0.3936710954 0.3827154934 0.3495157063 + 0.3948434889 0.3839044869 0.3507024050 + 0.3960154057 0.3850910068 0.3518857956 + 0.3971866965 0.3862751126 0.3530685008 + 0.3983570039 0.3874529898 0.3542487919 + 0.3995273113 0.3886308074 0.3554292023 + 0.4006893039 0.3898057938 0.3566128016 + 0.4018486142 0.3909797966 0.3577977121 + 0.4030069113 0.3921526968 0.3589808941 + 0.4041604102 0.3933204114 0.3601540029 + 0.4053137898 0.3944880962 0.3613269925 + 0.4064702988 0.3956514001 0.3624922931 + 0.4076293111 0.3968113065 0.3636507988 + 0.4087882042 0.3979710937 0.3648093045 + 0.4099336863 0.3991281092 0.3659642935 + 0.4110778868 0.4002847970 0.3671188951 + 0.4122214019 0.4014416039 0.3682743013 + 0.4133622944 0.4025982916 0.3694328070 + 0.4145030975 0.4037550092 0.3705911934 + 0.4156427979 0.4049058855 0.3717437983 + 0.4167811871 0.4060513079 0.3728901148 + 0.4179196954 0.4071967900 0.3740364909 + 0.4190456867 0.4083349109 0.3751721084 + 0.4201678038 0.4094707966 0.3763037920 + 0.4212898910 0.4106067121 0.3774355054 + 0.4224098921 0.4117316902 0.3785631061 + 0.4235298932 0.4128564000 0.3796902895 + 0.4246496856 0.4139792919 0.3808172047 + 0.4257684946 0.4150904119 0.3819408119 + 0.4268873036 0.4162015915 0.3830643892 + 0.4280022085 0.4173096120 0.3841860890 + 0.4291062951 0.4184089899 0.3853017092 + 0.4302105010 0.4195083976 0.3864172995 + 0.4313144088 0.4206028879 0.3875293136 + 0.4324178994 0.4216887951 0.3886339068 + 0.4335213900 0.4227746129 0.3897385001 + 0.4346221089 0.4238578081 0.3908396959 + 0.4357188940 0.4249371886 0.3919354081 + 0.4368157089 0.4260165095 0.3930310011 + 0.4379082918 0.4270943105 0.3941242993 + 0.4389953017 0.4281699061 0.3952139020 + 0.4400823116 0.4292455018 0.3963035047 + 0.4411674142 0.4303215146 0.3973940909 + 0.4422496855 0.4313979149 0.3984861970 + 0.4433319867 0.4324742854 0.3995783925 + 0.4444083869 0.4335477054 0.4006663859 + 0.4454728067 0.4346148074 0.4017446041 + 0.4465371966 0.4356819093 0.4028229117 + 0.4475978017 0.4367474020 0.4038993120 + 0.4486451149 0.4378075898 0.4049679935 + 0.4496923983 0.4388678074 0.4060367942 + 0.4507394135 0.4399277866 0.4071055055 + 0.4517830014 0.4409854114 0.4081743062 + 0.4528265893 0.4420430064 0.4092431962 + 0.4538702071 0.4431006014 0.4103119969 + 0.4549070001 0.4441545904 0.4113866985 + 0.4559428096 0.4452080131 0.4124625027 + 0.4569785893 0.4462614954 0.4135383964 + 0.4580070078 0.4473111033 0.4146097898 + 0.4590308964 0.4483582973 0.4156782031 + 0.4600548148 0.4494056106 0.4167467058 + 0.4610792100 0.4504511952 0.4178118110 + 0.4621045887 0.4514938891 0.4188697040 + 0.4631299078 0.4525366127 0.4199275970 + 0.4641552866 0.4535791874 0.4209854901 + 0.4651829898 0.4546158910 0.4220423996 + 0.4662106931 0.4556525946 0.4230993092 + 0.4672383964 0.4566892982 0.4241561890 + 0.4682591856 0.4577207863 0.4252122045 + 0.4692761898 0.4587494135 0.4262678027 + 0.4702933133 0.4597780108 0.4273233116 + 0.4713107944 0.4608063996 0.4283765852 + 0.4723303020 0.4618338943 0.4294205010 + 0.4733496904 0.4628615081 0.4304643869 + 0.4743691981 0.4638890922 0.4315083921 + 0.4753845930 0.4649214149 0.4325434864 + 0.4763987958 0.4659551084 0.4335753918 + 0.4774129987 0.4669888914 0.4346072972 + 0.4784238040 0.4680216014 0.4356386065 + 0.4794248044 0.4690510035 0.4366680086 + 0.4804258049 0.4700804055 0.4376974106 + 0.4814268053 0.4711098969 0.4387268126 + 0.4824292958 0.4721364081 0.4397540987 + 0.4834325016 0.4731616974 0.4407801926 + 0.4844357967 0.4741868973 0.4418064058 + 0.4854384959 0.4752117991 0.4428318143 + 0.4864360988 0.4762332141 0.4438445866 + 0.4874337018 0.4772545099 0.4448573887 + 0.4884313047 0.4782758951 0.4458701015 + 0.4894233942 0.4792912900 0.4468775094 + 0.4904080927 0.4802986085 0.4478765130 + 0.4913927913 0.4813058972 0.4488754869 + 0.4923774898 0.4823131859 0.4498746097 + 0.4933621883 0.4833154976 0.4508705139 + 0.4943468869 0.4843156934 0.4518648982 + 0.4953315854 0.4853160083 0.4528594017 + 0.4963161945 0.4863162041 0.4538537860 + 0.4972892106 0.4873186946 0.4548524916 + 0.4982616901 0.4883211851 0.4558514953 + 0.4992341995 0.4893237948 0.4568504989 + 0.5002049804 0.4903247058 0.4578497112 + 0.5011661053 0.4913162887 0.4588507116 + 0.5021272898 0.4923079908 0.4598517120 + 0.5030884743 0.4932996035 0.4608527124 + 0.5040500164 0.4942877889 0.4618506134 + 0.5050122142 0.4952678084 0.4628399909 + 0.5059744120 0.4962477982 0.4638293982 + 0.5069364905 0.4972277880 0.4648188055 + 0.5078973770 0.4982069135 0.4658063948 + 0.5088561773 0.4991844893 0.4667905867 + 0.5098149180 0.5001621842 0.4677748084 + 0.5107737184 0.5011398196 0.4687590897 + 0.5117294192 0.5021172762 0.4697437882 + 0.5126808882 0.5030944943 0.4707294106 + 0.5136322975 0.5040717721 0.4717150033 + 0.5145838261 0.5050491095 0.4727005959 + 0.5155330896 0.5060225725 0.4736824036 + 0.5164790154 0.5069903135 0.4746578038 + 0.5174250007 0.5079579949 0.4756332040 + 0.5183709264 0.5089256763 0.4766086042 + 0.5193127990 0.5098916888 0.4775812924 + 0.5202462077 0.5108541250 0.4785476029 + 0.5211796165 0.5118165016 0.4795139134 + 0.5221130252 0.5127789974 0.4804801941 + 0.5230445862 0.5137407780 0.4814454019 + 0.5239686966 0.5147001743 0.4824050069 + 0.5248929262 0.5156595111 0.4833647013 + 0.5258170962 0.5166189075 0.4843243062 + 0.5267412066 0.5175781846 0.4852839112 + 0.5276607275 0.5185251236 0.4862455130 + 0.5285801291 0.5194721222 0.4872071147 + 0.5294994712 0.5204190016 0.4881686866 + 0.5304189920 0.5213660002 0.4891302884 + 0.5313292146 0.5223047137 0.4900931120 + 0.5322365761 0.5232409239 0.4910562038 + 0.5331441164 0.5241770744 0.4920192957 + 0.5340514779 0.5251134038 0.4929825068 + 0.5349549055 0.5260437131 0.4939421117 + 0.5358536243 0.5269672871 0.4948973954 + 0.5367524028 0.5278909802 0.4958527088 + 0.5376511216 0.5288146138 0.4968079925 + 0.5385501981 0.5297368169 0.4977625012 + 0.5394514799 0.5306468010 0.4987092912 + 0.5403528214 0.5315569043 0.4996561110 + 0.5412541032 0.5324670076 0.5006029010 + 0.5421553850 0.5333769917 0.5015497208 + 0.5430505276 0.5342804193 0.5024966002 + 0.5439429879 0.5351806879 0.5034434199 + 0.5448353887 0.5360810161 0.5043902993 + 0.5457279086 0.5369812846 0.5053371191 + 0.5466185808 0.5378801227 0.5062835217 + 0.5475032926 0.5387730002 0.5072280169 + 0.5483881235 0.5396658778 0.5081725121 + 0.5492727757 0.5405586958 0.5091168880 + 0.5501574874 0.5414515734 0.5100613832 + 0.5510445833 0.5423436761 0.5109959245 + 0.5519328117 0.5432354808 0.5119252205 + 0.5528209805 0.5441272259 0.5128543973 + 0.5537092090 0.5450189710 0.5137836933 + 0.5545967221 0.5459102988 0.5147123933 + 0.5554770827 0.5467962027 0.5156326890 + 0.5563575029 0.5476821065 0.5165529251 + 0.5572379231 0.5485680103 0.5174731016 + 0.5581182837 0.5494539142 0.5183933973 + 0.5589959025 0.5503361821 0.5193132758 + 0.5598695874 0.5512136221 0.5202329159 + 0.5607432723 0.5520910025 0.5211526155 + 0.5616170168 0.5529683828 0.5220721960 + 0.5624907017 0.5538457036 0.5229917765 + 0.5633659959 0.5547165871 0.5239105821 + 0.5642418861 0.5555846095 0.5248289704 + 0.5651177168 0.5564525723 0.5257474184 + 0.5659936070 0.5573204756 0.5266659260 + 0.5668694973 0.5581884980 0.5275843143 + 0.5677334070 0.5590493083 0.5284937024 + 0.5685961246 0.5599092841 0.5294020176 + 0.5694587827 0.5607693791 0.5303102136 + 0.5703215003 0.5616294146 0.5312184095 + 0.5711839199 0.5624892712 0.5321263075 + 0.5720422268 0.5633450747 0.5330240726 + 0.5729004741 0.5642009974 0.5339217782 + 0.5737587214 0.5650569201 0.5348194838 + 0.5746170282 0.5659127235 0.5357171893 + 0.5754747987 0.5667679906 0.5366147161 + 0.5763297081 0.5676189065 0.5375100970 + 0.5771846175 0.5684698820 0.5384054184 + 0.5780395269 0.5693207979 0.5393007994 + 0.5788943768 0.5701717734 0.5401961803 + 0.5797473192 0.5710228086 0.5410919785 + 0.5805879831 0.5718737841 0.5419911146 + 0.5814288259 0.5727248788 0.5428900719 + 0.5822694898 0.5735759735 0.5437892079 + 0.5831102133 0.5744271278 0.5446882248 + 0.5839506984 0.5752779245 0.5455867052 + 0.5847879052 0.5761266947 0.5464770794 + 0.5856251717 0.5769755244 0.5473675132 + 0.5864624977 0.5778242946 0.5482578874 + 0.5872997046 0.5786731243 0.5491483212 + 0.5881369710 0.5795218945 0.5500386953 + 0.5889738798 0.5803651810 0.5509185791 + 0.5898107290 0.5812082887 0.5517979264 + 0.5906475782 0.5820513964 0.5526772141 + 0.5914844871 0.5828943849 0.5535565019 + 0.5923212767 0.5837374926 0.5544357896 + 0.5931525826 0.5845798850 0.5553128123 + 0.5939822793 0.5854220986 0.5561890006 + 0.5948119760 0.5862641931 0.5570651889 + 0.5956416130 0.5871064067 0.5579413772 + 0.5964713097 0.5879485011 0.5588176250 + 0.5972992182 0.5887913704 0.5596930981 + 0.5981255174 0.5896348953 0.5605677962 + 0.5989518166 0.5904784799 0.5614423752 + 0.5997781157 0.5913220048 0.5623170733 + 0.6006044149 0.5921655297 0.5631917715 + 0.6014305949 0.5930098891 0.5640664101 + 0.6022564769 0.5938585997 0.5649405122 + 0.6030824184 0.5947071910 0.5658146739 + 0.6039084196 0.5955557823 0.5666887760 + 0.6047343016 0.5964043736 0.5675628781 + 0.6055601835 0.5972530246 0.5684370995 + 0.6063808799 0.5980986953 0.5693032742 + 0.6072000861 0.5989435911 0.5701668262 + 0.6080192924 0.5997886062 0.5710303783 + 0.6088384986 0.6006335020 0.5718938708 + 0.6096577048 0.6014783978 0.5727574229 + 0.6104760170 0.6023222804 0.5736197233 + 0.6112921238 0.6031637192 0.5744786263 + 0.6121081710 0.6040052176 0.5753375292 + 0.6129242778 0.6048465967 0.5761963725 + 0.6137403846 0.6056880951 0.5770552158 + 0.6145564914 0.6065294743 0.5779141188 + 0.6153724194 0.6073685288 0.5787752271 + 0.6161882877 0.6082066298 0.5796372294 + 0.6170042157 0.6090446711 0.5804991722 + 0.6178200245 0.6098828912 0.5813611746 + 0.6186358929 0.6107209921 0.5822231770 + 0.6194511056 0.6115579009 0.5830852985 + 0.6202605963 0.6123844981 0.5839483142 + 0.6210700870 0.6132110953 0.5848112106 + 0.6218796968 0.6140376925 0.5856742263 + 0.6226891875 0.6148642898 0.5865371823 + 0.6234986782 0.6156908870 0.5874001980 + 0.6243069768 0.6165152192 0.5882589817 + 0.6251134276 0.6173359752 0.5891109109 + 0.6259198189 0.6181567907 0.5899628997 + 0.6267263293 0.6189776063 0.5908148289 + 0.6275327206 0.6197984219 0.5916668177 + 0.6283391118 0.6206191778 0.5925186872 + 0.6291438937 0.6214398742 0.5933675170 + 0.6299477220 0.6222605109 0.5942140818 + 0.6307514906 0.6230810285 0.5950608253 + 0.6315551996 0.6239016056 0.5959073901 + 0.6323590279 0.6247221231 0.5967540145 + 0.6331627965 0.6255427003 0.5976006985 + 0.6339570284 0.6263571978 0.5984491706 + 0.6347482204 0.6271697283 0.5992982984 + 0.6355394125 0.6279821992 0.6001474261 + 0.6363306046 0.6287947297 0.6009964943 + 0.6371217966 0.6296073198 0.6018456221 + 0.6379129887 0.6304197907 0.6026948094 + 0.6387022138 0.6312273741 0.6035407782 + 0.6394910812 0.6320338845 0.6043862104 + 0.6402798891 0.6328405142 0.6052315235 + 0.6410686970 0.6336470246 0.6060767770 + 0.6418575048 0.6344534755 0.6069220901 + 0.6426463127 0.6352599859 0.6077674031 + 0.6434336901 0.6360661983 0.6086056828 + 0.6442208290 0.6368721128 0.6094418764 + 0.6450079083 0.6376780868 0.6102781296 + 0.6457949877 0.6384841204 0.6111143827 + 0.6465821266 0.6392900944 0.6119505763 + 0.6473692060 0.6400961280 0.6127868295 + 0.6481484771 0.6408988237 0.6136180162 + 0.6489244103 0.6417000294 0.6144466996 + 0.6497004032 0.6425011158 0.6152755022 + 0.6504762769 0.6433023214 0.6161041856 + 0.6512522101 0.6441034079 0.6169329286 + 0.6520280838 0.6449046135 0.6177616119 + 0.6528034210 0.6457012296 0.6185904145 + 0.6535782218 0.6464933753 0.6194192767 + 0.6543530226 0.6472855210 0.6202481985 + 0.6551278234 0.6480777264 0.6210771203 + 0.6559026241 0.6488698125 0.6219059825 + 0.6566773057 0.6496618986 0.6227349043 + 0.6574509144 0.6504533291 0.6235638261 + 0.6582205296 0.6512417793 0.6243926287 + 0.6589900851 0.6520304084 0.6252213120 + 0.6597597003 0.6528189778 0.6260501146 + 0.6605293155 0.6536074877 0.6268787980 + 0.6612988710 0.6543961167 0.6277076006 + 0.6620684266 0.6551846862 0.6285362840 + 0.6628314257 0.6559717059 0.6293578744 + 0.6635934114 0.6567586064 0.6301782727 + 0.6643555164 0.6575453877 0.6309986711 + 0.6651175022 0.6583322883 0.6318191290 + 0.6658794880 0.6591191292 0.6326395273 + 0.6666415930 0.6599059105 0.6334599257 + 0.6674026251 0.6606888175 0.6342766881 + 0.6681625247 0.6614663005 0.6350883245 + 0.6689224243 0.6622439027 0.6359000206 + 0.6696822047 0.6630213857 0.6367117167 + 0.6704421043 0.6637989879 0.6375232935 + 0.6712018847 0.6645764709 0.6383349895 + 0.6719617844 0.6653540134 0.6391466856 + 0.6727159023 0.6661242843 0.6399570704 + 0.6734694839 0.6668937802 0.6407673955 + 0.6742231250 0.6676632762 0.6415776014 + 0.6749765873 0.6684327722 0.6423879266 + 0.6757302284 0.6692023277 0.6431981921 + 0.6764838099 0.6699718237 0.6440083981 + 0.6772350073 0.6707385778 0.6448181868 + 0.6779803038 0.6714984775 0.6456267834 + 0.6787256002 0.6722583771 0.6464353204 + 0.6794708967 0.6730182767 0.6472437978 + 0.6802163124 0.6737781763 0.6480523944 + 0.6809616089 0.6745380759 0.6488608718 + 0.6817069054 0.6752979755 0.6496694088 + 0.6824505925 0.6760519147 0.6504725218 + 0.6831930876 0.6768013239 0.6512712836 + 0.6839357018 0.6775506735 0.6520701051 + 0.6846781969 0.6783000827 0.6528689265 + 0.6854206920 0.6790493727 0.6536676884 + 0.6861631870 0.6797987819 0.6544665098 + 0.6869056821 0.6805481911 0.6552652717 + 0.6876466274 0.6812918782 0.6560565233 + 0.6883869171 0.6820340157 0.6568452716 + 0.6891273260 0.6827760935 0.6576340795 + 0.6898676157 0.6835181713 0.6584228873 + 0.6906079054 0.6842603087 0.6592115760 + 0.6913483143 0.6850023866 0.6600003839 + 0.6920886040 0.6857444048 0.6607891917 + 0.6928188801 0.6864817142 0.6615756750 + 0.6935479045 0.6872183084 0.6623616815 + 0.6942768097 0.6879549026 0.6631476879 + 0.6950057745 0.6886916161 0.6639338136 + 0.6957347989 0.6894282103 0.6647198200 + 0.6964638233 0.6901648045 0.6655058861 + 0.6971927285 0.6909013987 0.6662918925 + 0.6979197860 0.6916376948 0.6670784950 + 0.6986466050 0.6923738718 0.6678652167 + 0.6993734241 0.6931101084 0.6686518788 + 0.7001003027 0.6938462853 0.6694386005 + 0.7008271217 0.6945825219 0.6702252030 + 0.7015538812 0.6953186989 0.6710119247 + 0.7022808194 0.6960549951 0.6717985868 + 0.7030063272 0.6967895031 0.6725746989 + 0.7037315965 0.6975237131 0.6733481884 + 0.7044569254 0.6982578039 0.6741216779 + 0.7051821947 0.6989920139 0.6748952270 + 0.7059075236 0.6997262239 0.6756685972 + 0.7066327929 0.7004603744 0.6764420867 + 0.7073581219 0.7011945248 0.6772155762 + 0.7080765963 0.7019280195 0.6779829264 + 0.7087914944 0.7026610970 0.6787465811 + 0.7095062733 0.7033942938 0.6795102954 + 0.7102211714 0.7041273713 0.6802740097 + 0.7109360099 0.7048605084 0.6810376048 + 0.7116509080 0.7055935860 0.6818013191 + 0.7123658061 0.7063267231 0.6825649738 + 0.7130801082 0.7070569992 0.6833276749 + 0.7137935758 0.7077823877 0.6840885878 + 0.7145071030 0.7085077763 0.6848495007 + 0.7152206898 0.7092332244 0.6856104732 + 0.7159342170 0.7099586129 0.6863713861 + 0.7166476846 0.7106840014 0.6871322989 + 0.7173612118 0.7114093900 0.6878932714 + 0.7180746794 0.7121347785 0.6886541843 + 0.7187874913 0.7128537893 0.6894174218 + 0.7195001841 0.7135729194 0.6901807785 + 0.7202129960 0.7142918706 0.6909440756 + 0.7209258080 0.7150110006 0.6917073727 + 0.7216385007 0.7157300115 0.6924707294 + 0.7223513126 0.7164490223 0.6932340264 + 0.7230641246 0.7171680927 0.6939973235 + 0.7237719297 0.7178853154 0.6947563887 + 0.7244753242 0.7186009884 0.6955114007 + 0.7251787782 0.7193167210 0.6962664127 + 0.7258821726 0.7200323939 0.6970214248 + 0.7265856862 0.7207481265 0.6977763772 + 0.7272890806 0.7214637995 0.6985313892 + 0.7279925942 0.7221794128 0.6992864013 + 0.7286959887 0.7228950858 0.7000414133 + 0.7293974161 0.7236070037 0.7007868290 + 0.7300986052 0.7243188024 0.7015315890 + 0.7307999134 0.7250304818 0.7022762895 + 0.7315011024 0.7257422209 0.7030211091 + 0.7322024107 0.7264540195 0.7037658095 + 0.7329035997 0.7271656990 0.7045106292 + 0.7336049080 0.7278773785 0.7052553296 + 0.7343035936 0.7285876274 0.7059991956 + 0.7349956036 0.7292935252 0.7067406774 + 0.7356876731 0.7299994826 0.7074822187 + 0.7363798022 0.7307054996 0.7082235813 + 0.7370718122 0.7314115167 0.7089651227 + 0.7377638817 0.7321174741 0.7097066045 + 0.7384558916 0.7328234911 0.7104480267 + 0.7391480207 0.7335295081 0.7111895084 + 0.7398372293 0.7342340946 0.7119327188 + 0.7405235767 0.7349374294 0.7126777768 + 0.7412099838 0.7356405854 0.7134228945 + 0.7418963909 0.7363438010 0.7141680121 + 0.7425827980 0.7370470762 0.7149130702 + 0.7432692051 0.7377502918 0.7156581879 + 0.7439556122 0.7384536266 0.7164033055 + 0.7446420193 0.7391567826 0.7171484232 + 0.7453268766 0.7398598194 0.7178943753 + 0.7460108995 0.7405626178 0.7186409831 + 0.7466949224 0.7412652969 0.7193875909 + 0.7473788857 0.7419680953 0.7201341987 + 0.7480629086 0.7426708937 0.7208808064 + 0.7487468719 0.7433736920 0.7216274142 + 0.7494308949 0.7440764904 0.7223740220 + 0.7501149178 0.7447792292 0.7231205106 + 0.7507920861 0.7454807758 0.7238616943 + 0.7514652014 0.7461814880 0.7245994210 + 0.7521383166 0.7468823195 0.7253370285 + 0.7528113127 0.7475829720 0.7260746956 + 0.7534844279 0.7482838035 0.7268123031 + 0.7541574836 0.7489845753 0.7275499105 + 0.7548305988 0.7496852875 0.7282875776 + 0.7555037141 0.7503861189 0.7290251851 + 0.7561715245 0.7510858178 0.7297608852 + 0.7568343878 0.7517846227 0.7304946780 + 0.7574973106 0.7524833083 0.7312284708 + 0.7581601739 0.7531821132 0.7319623232 + 0.7588232160 0.7538809180 0.7326961160 + 0.7594860792 0.7545796037 0.7334299088 + 0.7601490021 0.7552784085 0.7341635823 + 0.7608119249 0.7559772134 0.7348973751 + 0.7614743710 0.7566753030 0.7356321216 + 0.7621356845 0.7573720217 0.7363687754 + 0.7627969980 0.7580686808 0.7371054888 + 0.7634583116 0.7587653995 0.7378422022 + 0.7641196251 0.7594621181 0.7385789156 + 0.7647809982 0.7601587772 0.7393156290 + 0.7654423118 0.7608554959 0.7400524020 + 0.7661036253 0.7615522146 0.7407891154 + 0.7667648792 0.7622489929 0.7415258288 + 0.7674220204 0.7629417777 0.7422630191 + 0.7680791020 0.7636345029 0.7430003285 + 0.7687361240 0.7643272877 0.7437375188 + 0.7693930864 0.7650200725 0.7444748282 + 0.7700502276 0.7657127976 0.7452120185 + 0.7707071900 0.7664055824 0.7459493279 + 0.7713642120 0.7670983076 0.7466865182 + 0.7720212936 0.7677910924 0.7474238276 + 0.7726746202 0.7684819102 0.7481557727 + 0.7733249068 0.7691711783 0.7488833070 + 0.7739751935 0.7698603868 0.7496107817 + 0.7746254802 0.7705495954 0.7503383160 + 0.7752758265 0.7712389231 0.7510657907 + 0.7759261131 0.7719280720 0.7517933249 + 0.7765762806 0.7726173997 0.7525207996 + 0.7772266269 0.7733066082 0.7532482743 + 0.7778769135 0.7739958167 0.7539758086 + 0.7785270810 0.7746813893 0.7546988130 + 0.7791771889 0.7753669024 0.7554215789 + 0.7798274159 0.7760524154 0.7561445236 + 0.7804775238 0.7767379284 0.7568672895 + 0.7811276913 0.7774235010 0.7575901747 + 0.7817777991 0.7781090140 0.7583130002 + 0.7824279070 0.7787945271 0.7590358853 + 0.7830780745 0.7794799805 0.7597587109 + 0.7837272286 0.7801622152 0.7604821920 + 0.7843742967 0.7808375955 0.7612072229 + 0.7850214839 0.7815129757 0.7619320750 + 0.7856686115 0.7821882963 0.7626569867 + 0.7863156796 0.7828636765 0.7633818984 + 0.7869629264 0.7835391164 0.7641068101 + 0.7876099944 0.7842144966 0.7648317218 + 0.7882571220 0.7848898768 0.7655565739 + 0.7889041901 0.7855653167 0.7662814856 + 0.7895485163 0.7862368822 0.7670075297 + 0.7901902795 0.7869054079 0.7677345276 + 0.7908321023 0.7875739932 0.7684614062 + 0.7914739847 0.7882425189 0.7691884041 + 0.7921158075 0.7889109850 0.7699152827 + 0.7927575707 0.7895795107 0.7706422806 + 0.7933995128 0.7902479768 0.7713692188 + 0.7940412760 0.7909166217 0.7720962167 + 0.7946830988 0.7915850878 0.7728230953 + 0.7953270078 0.7922512889 0.7735466957 + 0.7959719896 0.7929161191 0.7742683291 + 0.7966169715 0.7935810089 0.7749899030 + 0.7972620130 0.7942458987 0.7757114768 + 0.7979069948 0.7949107289 0.7764332294 + 0.7985519171 0.7955756187 0.7771548033 + 0.7991968989 0.7962405086 0.7778763771 + 0.7998418808 0.7969053984 0.7785980105 + 0.8004869223 0.7975702286 0.7793195844 + 0.8011307120 0.7982336879 0.7800378203 + 0.8017739058 0.7988961935 0.7807539105 + 0.8024169803 0.7995588183 0.7814698815 + 0.8030601144 0.8002213836 0.7821859121 + 0.8037033081 0.8008840084 0.7829018831 + 0.8043463826 0.8015465140 0.7836179137 + 0.8049895763 0.8022090793 0.7843340039 + 0.8056327105 0.8028717041 0.7850499749 + 0.8062757850 0.8035342097 0.7857660055 + 0.8069155812 0.8041942716 0.7864825130 + 0.8075515032 0.8048514128 0.7871994972 + 0.8081874251 0.8055086136 0.7879164815 + 0.8088234067 0.8061658144 0.7886335254 + 0.8094593287 0.8068230152 0.7893505096 + 0.8100953102 0.8074802160 0.7900676131 + 0.8107311726 0.8081374168 0.7907845974 + 0.8113672137 0.8087946177 0.7915015817 + 0.8120030761 0.8094516993 0.7922186255 + 0.8126395941 0.8101087809 0.7929350138 + 0.8132783771 0.8107650876 0.7936488986 + 0.8139171004 0.8114215136 0.7943627834 + 0.8145558834 0.8120778203 0.7950767279 + 0.8151946068 0.8127341866 0.7957904935 + 0.8158333898 0.8133906126 0.7965043783 + 0.8164721131 0.8140469193 0.7972183228 + 0.8171108961 0.8147032857 0.7979322076 + 0.8177496195 0.8153595924 0.7986459732 + 0.8183884025 0.8160160184 0.7993599176 + 0.8190252781 0.8166726828 0.8000699878 + 0.8196616769 0.8173294067 0.8007791042 + 0.8202980757 0.8179860711 0.8014882803 + 0.8209344745 0.8186429143 0.8021973968 + 0.8215708733 0.8192995787 0.8029065132 + 0.8222072721 0.8199563026 0.8036156297 + 0.8228437901 0.8206130862 0.8043248057 + 0.8234801888 0.8212698102 0.8050339222 + 0.8241165876 0.8219264746 0.8057429790 + 0.8247510195 0.8225817084 0.8064500093 + 0.8253805041 0.8232325912 0.8071511984 + 0.8260099292 0.8238834739 0.8078523874 + 0.8266394138 0.8245344758 0.8085535169 + 0.8272687793 0.8251854181 0.8092547059 + 0.8278983235 0.8258364201 0.8099558949 + 0.8285278082 0.8264873028 0.8106570840 + 0.8291571736 0.8271383047 0.8113582730 + 0.8297867179 0.8277891874 0.8120595217 + 0.8304160833 0.8284401298 0.8127607107 + 0.8310465217 0.8290880919 0.8134608269 + 0.8316774964 0.8297342062 0.8141604066 + 0.8323084116 0.8303803205 0.8148599267 + 0.8329393864 0.8310263753 0.8155595064 + 0.8335703015 0.8316724896 0.8162590265 + 0.8342012763 0.8323186040 0.8169584870 + 0.8348321915 0.8329647779 0.8176581264 + 0.8354632258 0.8336108923 0.8183575869 + 0.8360940814 0.8342570066 0.8190571070 + 0.8367251158 0.8349031210 0.8197566867 + 0.8373507261 0.8355441093 0.8204557896 + 0.8379753232 0.8361840844 0.8211548924 + 0.8385999203 0.8368241787 0.8218539953 + 0.8392245173 0.8374642134 0.8225530982 + 0.8398491144 0.8381041884 0.8232522011 + 0.8404737711 0.8387442231 0.8239513040 + 0.8410984278 0.8393843174 0.8246504068 + 0.8417230248 0.8400242925 0.8253493905 + 0.8423476219 0.8406643271 0.8260484934 + 0.8429722190 0.8413044214 0.8267475963 + 0.8435915112 0.8419380188 0.8274425268 + 0.8442103863 0.8425710201 0.8281369209 + 0.8448292017 0.8432040811 0.8288313746 + 0.8454480767 0.8438372016 0.8295258284 + 0.8460670114 0.8444703221 0.8302202821 + 0.8466858268 0.8451033831 0.8309146762 + 0.8473047018 0.8457365036 0.8316091895 + 0.8479235768 0.8463695049 0.8323035836 + 0.8485423923 0.8470026255 0.8329980969 + 0.8491613269 0.8476356864 0.8336924911 + 0.8497756720 0.8482635021 0.8343786001 + 0.8503894210 0.8488906026 0.8350632787 + 0.8510032296 0.8495177031 0.8357480764 + 0.8516169190 0.8501448035 0.8364328146 + 0.8522306085 0.8507717848 0.8371176124 + 0.8528444171 0.8513988853 0.8378022909 + 0.8534581065 0.8520259857 0.8384870291 + 0.8540717959 0.8526530266 0.8391718268 + 0.8546856046 0.8532801270 0.8398565054 + 0.8552992940 0.8539072275 0.8405413032 + 0.8559076786 0.8545295000 0.8412235975 + 0.8565136194 0.8551496863 0.8419046998 + 0.8571195006 0.8557698727 0.8425859213 + 0.8577255011 0.8563901186 0.8432670832 + 0.8583313823 0.8570103049 0.8439481854 + 0.8589373231 0.8576303720 0.8446294069 + 0.8595432043 0.8582506180 0.8453105092 + 0.8601492047 0.8588708043 0.8459916711 + 0.8607550859 0.8594909906 0.8466727734 + 0.8613610268 0.8601111770 0.8473539948 + 0.8619657755 0.8607302904 0.8480353951 + 0.8625686765 0.8613476157 0.8487172723 + 0.8631715178 0.8619648814 0.8493992090 + 0.8637742996 0.8625822067 0.8500810862 + 0.8643772006 0.8631994724 0.8507630825 + 0.8649799824 0.8638167977 0.8514450192 + 0.8655828238 0.8644341230 0.8521268964 + 0.8661857247 0.8650513291 0.8528087735 + 0.8667885065 0.8656685948 0.8534907103 + 0.8673914075 0.8662859201 0.8541725874 + 0.8679941893 0.8669031858 0.8548545241 + 0.8685936928 0.8675162196 0.8555287719 + 0.8691927195 0.8681287169 0.8562021255 + 0.8697918057 0.8687412143 0.8568754196 + 0.8703907728 0.8693537116 0.8575487137 + 0.8709899187 0.8699662089 0.8582220078 + 0.8715888858 0.8705787063 0.8588953018 + 0.8721879721 0.8711912036 0.8595685959 + 0.8727869987 0.8718037009 0.8602418900 + 0.8733860850 0.8724161983 0.8609151840 + 0.8739851117 0.8730286956 0.8615884781 + 0.8745809793 0.8736391068 0.8622589707 + 0.8751688004 0.8742442727 0.8629223108 + 0.8757566214 0.8748494983 0.8635854721 + 0.8763445020 0.8754547238 0.8642486930 + 0.8769323230 0.8760598898 0.8649119735 + 0.8775202036 0.8766651750 0.8655751944 + 0.8781080246 0.8772704005 0.8662384152 + 0.8786959052 0.8778756261 0.8669016957 + 0.8792837262 0.8784807920 0.8675649166 + 0.8798714876 0.8790860176 0.8682280779 + 0.8804594278 0.8796913028 0.8688914180 + 0.8810451031 0.8802946210 0.8695524931 + 0.8816289902 0.8808963895 0.8702116013 + 0.8822128773 0.8814982176 0.8708708286 + 0.8827967048 0.8820999861 0.8715299964 + 0.8833805919 0.8827018142 0.8721891046 + 0.8839644790 0.8833035827 0.8728482723 + 0.8845484257 0.8839052916 0.8735073805 + 0.8851323128 0.8845071197 0.8741666079 + 0.8857161999 0.8851088881 0.8748257756 + 0.8863000274 0.8857107162 0.8754848838 + 0.8868839145 0.8863124847 0.8761441112 + 0.8874658942 0.8869127035 0.8768044114 + 0.8880469203 0.8875120878 0.8774654865 + 0.8886278272 0.8881114721 0.8781265020 + 0.8892087936 0.8887107968 0.8787875772 + 0.8897898197 0.8893101811 0.8794485927 + 0.8903707266 0.8899096251 0.8801097274 + 0.8909516931 0.8905088902 0.8807706833 + 0.8915325999 0.8911082745 0.8814318180 + 0.8921136260 0.8917077184 0.8820927739 + 0.8926944733 0.8923069835 0.8827539086 + 0.8932754993 0.8929064274 0.8834149241 + 0.8938503861 0.8935058713 0.8840740919 + 0.8944211006 0.8941053748 0.8847320080 + 0.8949918151 0.8947049975 0.8853899240 + 0.8955624104 0.8953045011 0.8860477805 + 0.8961331248 0.8959041238 0.8867056966 + 0.8967037797 0.8965036273 0.8873636127 + 0.8972744942 0.8971031904 0.8880215287 + 0.8978450894 0.8977026939 0.8886793852 + 0.8984158039 0.8983023167 0.8893373013 + 0.8989865184 0.8989018202 0.8899952173 + 0.8995571733 0.8995013237 0.8906530738 + 0.9001259804 0.9000989199 0.8913081884 + 0.9006919265 0.9006931782 0.8919588923 + 0.9012578726 0.9012876153 0.8926094770 + 0.9018238187 0.9018818736 0.8932601810 + 0.9023898244 0.9024763107 0.8939108849 + 0.9029557109 0.9030706286 0.8945615888 + 0.9035217166 0.9036650062 0.8952121735 + 0.9040876031 0.9042593241 0.8958628774 + 0.9046536088 0.9048537016 0.8965135813 + 0.9052194953 0.9054480195 0.8971642852 + 0.9057855010 0.9060423970 0.8978149295 + 0.9063512087 0.9066365957 0.8984653950 + 0.9069120884 0.9072268009 0.8991103768 + 0.9074729085 0.9078170061 0.8997554183 + 0.9080337882 0.9084072113 0.9004002810 + 0.9085947275 0.9089974165 0.9010453224 + 0.9091554880 0.9095876217 0.9016903043 + 0.9097164273 0.9101778865 0.9023352265 + 0.9102773070 0.9107680917 0.9029802084 + 0.9108381867 0.9113582969 0.9036251903 + 0.9113990068 0.9119485021 0.9042701721 + 0.9119598866 0.9125387073 0.9049150944 + 0.9125208259 0.9131289124 0.9055600762 + 0.9130790234 0.9137184024 0.9062044024 + 0.9136350155 0.9143071771 0.9068480134 + 0.9141910076 0.9148960114 0.9074916244 + 0.9147469997 0.9154847860 0.9081352949 + 0.9153029919 0.9160736799 0.9087789059 + 0.9158589840 0.9166625142 0.9094225168 + 0.9164149761 0.9172512889 0.9100661874 + 0.9169710279 0.9178401232 0.9107097983 + 0.9175270200 0.9184290171 0.9113535285 + 0.9180830121 0.9190177917 0.9119970798 + 0.9186390042 0.9196066260 0.9126406908 + 0.9191949964 0.9201954007 0.9132844210 + 0.9197478294 0.9207763076 0.9139227867 + 0.9203000069 0.9213560820 0.9145606160 + 0.9208523035 0.9219357967 0.9151983261 + 0.9214046001 0.9225155711 0.9158360958 + 0.9219568968 0.9230954051 0.9164738059 + 0.9225091934 0.9236751795 0.9171115756 + 0.9230613708 0.9242548943 0.9177492857 + 0.9236137271 0.9248347282 0.9183871150 + 0.9241660237 0.9254145026 0.9190248251 + 0.9247183204 0.9259942770 0.9196625948 + 0.9252706170 0.9265739918 0.9203003049 + 0.9258226752 0.9271532297 0.9209372997 + 0.9263734221 0.9277231097 0.9215620756 + 0.9269239902 0.9282929897 0.9221869111 + 0.9274746776 0.9288629889 0.9228116870 + 0.9280253053 0.9294329286 0.9234365225 + 0.9285759926 0.9300028086 0.9240614176 + 0.9291266203 0.9305726886 0.9246861935 + 0.9296773076 0.9311426282 0.9253110290 + 0.9302278757 0.9317126274 0.9259358048 + 0.9307786226 0.9322825074 0.9265605807 + 0.9313291907 0.9328523874 0.9271854162 + 0.9318798780 0.9334223270 0.9278101921 + 0.9324297905 0.9339916706 0.9284340143 + 0.9329723716 0.9345554709 0.9290462136 + 0.9335150719 0.9351192117 0.9296582937 + 0.9340577126 0.9356828928 0.9302704930 + 0.9346002936 0.9362466931 0.9308826923 + 0.9351428747 0.9368103743 0.9314947724 + 0.9356855154 0.9373741150 0.9321069717 + 0.9362280965 0.9379379153 0.9327191114 + 0.9367706776 0.9385015965 0.9333313107 + 0.9373133183 0.9390652776 0.9339435101 + 0.9378558993 0.9396290779 0.9345555902 + 0.9383984804 0.9401928186 0.9351677895 + 0.9389411211 0.9407566190 0.9357799292 + 0.9394807220 0.9413191080 0.9363880754 + 0.9400200844 0.9418814778 0.9369959235 + 0.9405596256 0.9424440265 0.9376037717 + 0.9410989881 0.9430063963 0.9382116795 + 0.9416384101 0.9435688853 0.9388195276 + 0.9421778917 0.9441313148 0.9394273758 + 0.9427173138 0.9446938038 0.9400352836 + 0.9432566762 0.9452561736 0.9406430721 + 0.9437962174 0.9458187222 0.9412509799 + 0.9443355799 0.9463812113 0.9418588877 + 0.9448750019 0.9469435811 0.9424667954 + 0.9454144835 0.9475061297 0.9430745840 + 0.9459543228 0.9480664730 0.9436799884 + 0.9464944005 0.9486256838 0.9442839026 + 0.9470344782 0.9491848946 0.9448878169 + 0.9475746155 0.9497442245 0.9454916716 + 0.9481146932 0.9503033757 0.9460955858 + 0.9486547709 0.9508627057 0.9466995001 + 0.9491950274 0.9514219165 0.9473034143 + 0.9497351050 0.9519811869 0.9479073286 + 0.9502751827 0.9525403976 0.9485111833 + 0.9508153200 0.9530997276 0.9491150975 + 0.9513553977 0.9536588788 0.9497190118 + 0.9518954754 0.9542182088 0.9503229856 + 0.9524348974 0.9547764063 0.9509251714 + 0.9529715180 0.9553294778 0.9515191913 + 0.9535080194 0.9558826089 0.9521132112 + 0.9540445209 0.9564357996 0.9527071714 + 0.9545810819 0.9569888711 0.9533010721 + 0.9551175833 0.9575420022 0.9538950920 + 0.9556540847 0.9580950737 0.9544891119 + 0.9561907053 0.9586482048 0.9550830722 + 0.9567272067 0.9592013955 0.9556770921 + 0.9572637081 0.9597545266 0.9562711120 + 0.9578003287 0.9603075981 0.9568650723 + 0.9583367705 0.9608607292 0.9574589729 + 0.9588732719 0.9614138007 0.9580529928 + 0.9594082236 0.9619622827 0.9586417079 + 0.9599416256 0.9625064731 0.9592257142 + 0.9604749084 0.9630507231 0.9598097205 + 0.9610083103 0.9635949135 0.9603936076 + 0.9615417123 0.9641392231 0.9609776139 + 0.9620749950 0.9646834135 0.9615616202 + 0.9626083970 0.9652276039 0.9621456265 + 0.9631417990 0.9657717943 0.9627295136 + 0.9636752009 0.9663161039 0.9633135200 + 0.9642084837 0.9668602943 0.9638975263 + 0.9647418857 0.9674044847 0.9644814134 + 0.9652752876 0.9679486752 0.9650654197 + 0.9658085704 0.9684929848 0.9656494260 + 0.9663383961 0.9690269232 0.9662256837 + 0.9668667912 0.9695569277 0.9667990208 + 0.9673951864 0.9700868130 0.9673721790 + 0.9679235816 0.9706168175 0.9679455161 + 0.9684519768 0.9711467028 0.9685187936 + 0.9689803720 0.9716767073 0.9690920115 + 0.9695087075 0.9722065926 0.9696652889 + 0.9700371027 0.9727365971 0.9702386260 + 0.9705654979 0.9732664824 0.9708117843 + 0.9710938931 0.9737964272 0.9713851213 + 0.9716222882 0.9743263721 0.9719583988 + 0.9721506834 0.9748563170 0.9725316167 + 0.9726790786 0.9753863215 0.9731048942 + 0.9732028842 0.9759041071 0.9736629725 + 0.9737251997 0.9764174819 0.9742156267 + 0.9742473960 0.9769309759 0.9747681022 + 0.9747695923 0.9774444103 0.9753205776 + 0.9752919078 0.9779577851 0.9758731723 + 0.9758141041 0.9784712195 0.9764257073 + 0.9763363004 0.9789845943 0.9769781828 + 0.9768586159 0.9794980288 0.9775307178 + 0.9773808122 0.9800114036 0.9780833125 + 0.9779031277 0.9805248976 0.9786357880 + 0.9784253240 0.9810382724 0.9791883230 + 0.9789475203 0.9815517068 0.9797409177 + 0.9794697762 0.9820650816 0.9802933931 + 0.9799848199 0.9825599790 0.9808300734 + 0.9804943800 0.9830408096 0.9813547134 + 0.9810041189 0.9835215807 0.9818794131 + 0.9815136790 0.9840024114 0.9824039936 + 0.9820232987 0.9844831824 0.9829286933 + 0.9825329781 0.9849640131 0.9834532738 + 0.9830425978 0.9854447842 0.9839779735 + 0.9835522175 0.9859256148 0.9845026135 + 0.9840617776 0.9864063859 0.9850273132 + 0.9845715165 0.9868872166 0.9855518937 + 0.9850810766 0.9873679876 0.9860765934 + 0.9855906963 0.9878488183 0.9866011739 + 0.9861003757 0.9883295894 0.9871258736 + 0.9866076112 0.9888011813 0.9876428843 + 0.9871065021 0.9892416000 0.9881339073 + 0.9876055121 0.9896820188 0.9886249900 + 0.9881045222 0.9901223779 0.9891160131 + 0.9886034727 0.9905629158 0.9896070957 + 0.9891024828 0.9910032749 0.9900981188 + 0.9896014929 0.9914436936 0.9905890822 + 0.9901003838 0.9918841124 0.9910802245 + 0.9905993938 0.9923244715 0.9915711880 + 0.9910984039 0.9927648902 0.9920622706 + 0.9915974140 0.9932053089 0.9925532937 + 0.9920964241 0.9936457276 0.9930443764 + 0.9925953746 0.9940860868 0.9935353994 + 0.9930943847 0.9945266247 0.9940264821 + 0.9935892224 0.9949312806 0.9944710732 + 0.9940823913 0.9953212142 0.9948964119 + 0.9945755005 0.9957110882 0.9953216910 + 0.9950686097 0.9961010218 0.9957470298 + 0.9955617785 0.9964908957 0.9961723089 + 0.9960548878 0.9968808293 0.9965975881 + 0.9965479970 0.9972707033 0.9970229268 + 0.9970412254 0.9976605773 0.9974482059 + 0.9975342751 0.9980505109 0.9978734851 + 0.9980275035 0.9984403849 0.9982988238 + 0.9985206127 0.9988303185 0.9987241030 + 0.9990137219 0.9992201924 0.9991493821 + 0.9995068908 0.9996101260 0.9995747209 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_400vc.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_400vc.spi1d index 3bbe6e7b..7bc9b9a2 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_400vc.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_400vc.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0086291339 0.0078005488 0.0061553409 - 0.0166279394 0.0149212098 0.0115477303 - 0.0234502498 0.0210635699 0.0162159093 - 0.0294977706 0.0264640693 0.0204165298 - 0.0349041112 0.0313926600 0.0242713001 - 0.0398604982 0.0359628201 0.0279128905 - 0.0445825197 0.0402758084 0.0313868709 - 0.0489878207 0.0443142913 0.0346947797 - 0.0532177314 0.0482137911 0.0378837399 - 0.0572355017 0.0519315116 0.0409737006 - 0.0610651597 0.0555156991 0.0439543799 - 0.0647805408 0.0590067990 0.0468649715 - 0.0684034899 0.0623898692 0.0496960990 - 0.0719185919 0.0656935796 0.0524532497 - 0.0753464922 0.0688895881 0.0551457889 - 0.0786892101 0.0719943792 0.0577957407 - 0.0819205269 0.0749978498 0.0603696108 - 0.0850563273 0.0779183879 0.0628900677 - 0.0881082267 0.0807836503 0.0653679967 - 0.0910997614 0.0836165026 0.0678165331 - 0.0940506533 0.0864003077 0.0702505037 - 0.0969373211 0.0891356766 0.0726604089 - 0.0997503623 0.0918305814 0.0750340968 - 0.1025146991 0.0944881588 0.0773660317 - 0.1052661017 0.0971041918 0.0796465725 - 0.1079823002 0.0997008383 0.0818920732 - 0.1106742024 0.1022529975 0.0840972289 - 0.1133475006 0.1047334000 0.0862670168 - 0.1159679964 0.1071522981 0.0884032026 - 0.1185553968 0.1095331982 0.0905269608 - 0.1210843995 0.1118893027 0.0926406905 - 0.1235641018 0.1142370030 0.0947492719 - 0.1260133982 0.1165516973 0.0968295410 - 0.1284331977 0.1188732013 0.0988966897 - 0.1308183968 0.1211699992 0.1009425968 - 0.1331772059 0.1234538034 0.1029729024 - 0.1355085969 0.1256996989 0.1049892977 - 0.1378096938 0.1279380023 0.1069732010 - 0.1400966942 0.1301369965 0.1089397967 - 0.1423414052 0.1323242038 0.1109104976 - 0.1445591003 0.1344671994 0.1128705963 - 0.1467431933 0.1365955025 0.1148163974 - 0.1489122063 0.1386837959 0.1167598963 - 0.1510476023 0.1407471001 0.1186747029 - 0.1531807929 0.1428090930 0.1205610037 - 0.1553075016 0.1448387057 0.1224154979 - 0.1574216038 0.1468652934 0.1242436990 - 0.1595198959 0.1488717943 0.1260575950 - 0.1615947932 0.1508519948 0.1278496981 - 0.1636703014 0.1528411955 0.1296253949 - 0.1657373011 0.1548041999 0.1313930005 - 0.1677850038 0.1567573994 0.1331461966 - 0.1698163003 0.1587090939 0.1349010020 - 0.1718430966 0.1606369019 0.1366534978 - 0.1738559008 0.1625535041 0.1383929998 - 0.1758597046 0.1644703001 0.1401329041 - 0.1778582931 0.1663694978 0.1418718994 - 0.1798356026 0.1682510972 0.1436001062 - 0.1817937940 0.1701292992 0.1453143060 - 0.1837290972 0.1719716042 0.1470246017 - 0.1856500059 0.1737944931 0.1487206072 - 0.1875600070 0.1756183952 0.1504009068 - 0.1894560009 0.1774290055 0.1520728022 - 0.1913280934 0.1792113930 0.1537348032 - 0.1931733042 0.1809861064 0.1554002017 - 0.1949937940 0.1827580035 0.1570577025 - 0.1968041956 0.1845048070 0.1587098986 - 0.1985995024 0.1862314939 0.1603520066 - 0.2003758997 0.1879574060 0.1619874984 - 0.2021435946 0.1896872967 0.1636175960 - 0.2039009035 0.1914020032 0.1652458012 - 0.2056314051 0.1931017041 0.1668601930 - 0.2073460072 0.1948022991 0.1684632003 - 0.2090656012 0.1965142041 0.1700568050 - 0.2107871026 0.1982153952 0.1716396064 - 0.2125017941 0.1999046952 0.1732196957 - 0.2142132074 0.2015919983 0.1747861952 - 0.2159194946 0.2032641023 0.1763374954 - 0.2176184952 0.2049120069 0.1778807938 - 0.2193064988 0.2065432072 0.1794188023 - 0.2209898978 0.2081688046 0.1809532940 - 0.2226741016 0.2097914070 0.1824879944 - 0.2243521959 0.2114066035 0.1840219945 - 0.2260199040 0.2130115032 0.1855459064 - 0.2276818007 0.2146048993 0.1870635003 - 0.2293322980 0.2161996961 0.1885785013 - 0.2309723049 0.2177951932 0.1900880039 - 0.2325980961 0.2193817049 0.1915943027 - 0.2342166007 0.2209558040 0.1930999011 - 0.2358343005 0.2225205004 0.1946053058 - 0.2374477983 0.2240840048 0.1961077005 - 0.2390588969 0.2256534994 0.1975979954 - 0.2406685948 0.2272066027 0.1990832984 - 0.2422688007 0.2287465930 0.2005632073 - 0.2438556999 0.2302782983 0.2020380944 - 0.2454296947 0.2318089008 0.2035095990 - 0.2469906062 0.2333444953 0.2049811035 - 0.2485449016 0.2348676026 0.2064442933 - 0.2500948012 0.2363775969 0.2079003006 - 0.2516424954 0.2378765941 0.2093524039 - 0.2531889081 0.2393742949 0.2107995003 - 0.2547358871 0.2408725023 0.2122382969 - 0.2562828064 0.2423699051 0.2136691064 - 0.2578260899 0.2438611984 0.2150931954 - 0.2593621910 0.2453383952 0.2165144980 - 0.2608925104 0.2468091995 0.2179322988 - 0.2624158859 0.2482766062 0.2193447053 - 0.2639254928 0.2497424036 0.2207508981 - 0.2654292881 0.2512080073 0.2221533954 - 0.2669275999 0.2526736856 0.2235517055 - 0.2684175968 0.2541224957 0.2249393016 - 0.2699044943 0.2555668056 0.2263229042 - 0.2713881135 0.2570067048 0.2277022004 - 0.2728621960 0.2584454119 0.2290780991 - 0.2743291855 0.2598778009 0.2304508984 - 0.2757858038 0.2613000870 0.2318188995 - 0.2772283852 0.2627094984 0.2331835032 - 0.2786627114 0.2641144991 0.2345474958 - 0.2800801098 0.2655147016 0.2359123975 - 0.2814942896 0.2669171095 0.2372698933 - 0.2829064131 0.2683207989 0.2386226058 - 0.2843183875 0.2697081864 0.2399670929 - 0.2857283056 0.2710885108 0.2413100004 - 0.2871338129 0.2724551857 0.2426497042 - 0.2885296047 0.2738170922 0.2439855933 - 0.2899203002 0.2751764059 0.2453195006 - 0.2913027108 0.2765411139 0.2466553003 - 0.2926810980 0.2779048085 0.2479909062 - 0.2940480113 0.2792651057 0.2493253052 - 0.2954030931 0.2806189060 0.2506527901 - 0.2967463136 0.2819663882 0.2519730926 - 0.2980836928 0.2833054066 0.2532927990 - 0.2994191051 0.2846412957 0.2546122074 - 0.3007619083 0.2859781981 0.2559292912 - 0.3021047115 0.2873162031 0.2572456896 - 0.3034440875 0.2886605859 0.2585586011 - 0.3047791123 0.2899999022 0.2598730028 - 0.3061031103 0.2913272977 0.2611911893 - 0.3074271083 0.2926500142 0.2625049949 - 0.3087511957 0.2939665914 0.2638128996 - 0.3100728095 0.2952839136 0.2651130855 - 0.3113921881 0.2966020107 0.2664063871 - 0.3127081990 0.2979249954 0.2676930130 - 0.3140223026 0.2992509007 0.2689754069 - 0.3153294921 0.3005655110 0.2702546120 - 0.3166336119 0.3018752038 0.2715322971 - 0.3179413080 0.3031772077 0.2728073001 - 0.3192504048 0.3044762909 0.2740811110 - 0.3205552101 0.3057742119 0.2753461003 - 0.3218581975 0.3070715070 0.2766075134 - 0.3231492043 0.3083544075 0.2778604031 - 0.3244349062 0.3096314073 0.2791095972 - 0.3257190883 0.3108929992 0.2803491056 - 0.3270024955 0.3121466935 0.2815833986 - 0.3282819092 0.3133935034 0.2828159034 - 0.3295584023 0.3146354854 0.2840468884 - 0.3308312893 0.3158772886 0.2852747142 - 0.3321002126 0.3171189129 0.2864989042 - 0.3333649933 0.3183605075 0.2877179980 - 0.3346219063 0.3196018934 0.2889271975 - 0.3358792067 0.3208408952 0.2901360095 - 0.3371385932 0.3220691979 0.2913432121 - 0.3383980095 0.3232975006 0.2925504148 - 0.3396492004 0.3245128095 0.2937470973 - 0.3409003019 0.3257280886 0.2949436903 - 0.3421368003 0.3269419074 0.2961353958 - 0.3433690071 0.3281553984 0.2973257005 - 0.3445987105 0.3293735981 0.2985163927 - 0.3458260894 0.3305962980 0.2997077107 - 0.3470515013 0.3318206072 0.3008984029 - 0.3482699096 0.3330497146 0.3020871878 - 0.3494882882 0.3342789114 0.3032760024 - 0.3507004976 0.3354977071 0.3044616878 - 0.3519122005 0.3367156088 0.3056469858 - 0.3531202972 0.3379318118 0.3068349957 - 0.3543255925 0.3391467035 0.3080250025 - 0.3555307984 0.3403621018 0.3092128932 - 0.3567352891 0.3415792882 0.3103924990 - 0.3579396904 0.3427965045 0.3115719855 - 0.3591400981 0.3439978957 0.3127532005 - 0.3603394926 0.3451952934 0.3139348030 - 0.3615379930 0.3463875949 0.3151153028 - 0.3627347052 0.3475709856 0.3162938058 - 0.3639313877 0.3487544060 0.3174723089 - 0.3651260138 0.3499352038 0.3186487854 - 0.3663201928 0.3511157036 0.3198249936 - 0.3675152957 0.3522970974 0.3210014999 - 0.3687118888 0.3534801006 0.3221786916 - 0.3699085116 0.3546631038 0.3233557940 - 0.3711037934 0.3558301926 0.3245329857 - 0.3722988069 0.3569942117 0.3257102966 - 0.3734931946 0.3581548035 0.3268885911 - 0.3746860027 0.3593055010 0.3280701935 - 0.3758786917 0.3604561985 0.3292517960 - 0.3770669103 0.3616054058 0.3304269016 - 0.3782525063 0.3627535999 0.3315981925 - 0.3794381022 0.3639019132 0.3327696025 - 0.3806130886 0.3650487065 0.3339313865 - 0.3817881048 0.3661954105 0.3350929916 - 0.3829604089 0.3673394918 0.3362526000 - 0.3841269910 0.3684779108 0.3374074101 - 0.3852936029 0.3696162999 0.3385623097 - 0.3864552975 0.3707447052 0.3397139013 - 0.3876137137 0.3718662858 0.3408631980 - 0.3887721896 0.3729878962 0.3420126140 - 0.3899284899 0.3741044998 0.3431645036 - 0.3910844922 0.3752200007 0.3443169892 - 0.3922401071 0.3763357997 0.3454687893 - 0.3933893144 0.3774558902 0.3466030955 - 0.3945386112 0.3785760105 0.3477374911 - 0.3956862092 0.3796963990 0.3488697112 - 0.3968279064 0.3808178902 0.3499937057 - 0.3979696035 0.3819392920 0.3511176109 - 0.3991076052 0.3830555975 0.3522385061 - 0.4002385139 0.3841623068 0.3533534110 - 0.4013693929 0.3852689862 0.3544682860 - 0.4024933875 0.3863726854 0.3555805981 - 0.4036085010 0.3874725103 0.3566895127 - 0.4047234952 0.3885723054 0.3577983975 - 0.4058330059 0.3896723092 0.3589071929 - 0.4069365859 0.3907727003 0.3600156903 - 0.4080401957 0.3918730021 0.3611243069 - 0.4091409147 0.3929738104 0.3622342050 - 0.4102385938 0.3940750062 0.3633455932 - 0.4113362134 0.3951762021 0.3644570112 - 0.4124296904 0.3962723017 0.3655647933 - 0.4135183096 0.3973628879 0.3666686118 - 0.4146068990 0.3984535933 0.3677723110 - 0.4156936109 0.3995419145 0.3688744903 - 0.4167774022 0.4006268978 0.3699742854 - 0.4178611040 0.4017120004 0.3710741103 - 0.4189420044 0.4027974904 0.3721716106 - 0.4200159013 0.4038844109 0.3732632101 - 0.4210897088 0.4049712121 0.3743548989 - 0.4221613109 0.4060561061 0.3754456043 - 0.4232189953 0.4071291983 0.3765301108 - 0.4242765903 0.4082024097 0.3776144981 - 0.4253341854 0.4092755914 0.3786988854 - 0.4263879955 0.4103333056 0.3797815144 - 0.4274415076 0.4113903046 0.3808639050 - 0.4284951091 0.4124472141 0.3819462955 - 0.4295487106 0.4134993851 0.3830277026 - 0.4306023121 0.4145495892 0.3841085136 - 0.4316559136 0.4155999124 0.3851892948 - 0.4327076077 0.4166513085 0.3862665892 - 0.4337567091 0.4177044034 0.3873386979 - 0.4348058105 0.4187574983 0.3884108961 - 0.4358538985 0.4198100865 0.3894824088 - 0.4368919134 0.4208576977 0.3905462921 - 0.4379299879 0.4219053984 0.3916102946 - 0.4389680028 0.4229530990 0.3926742971 - 0.4400067031 0.4239928126 0.3937354982 - 0.4410456121 0.4250294864 0.3947955072 - 0.4420844913 0.4260661900 0.3958554864 - 0.4431211054 0.4271000028 0.3969140053 - 0.4441522956 0.4281272888 0.3979691863 - 0.4451833963 0.4291546047 0.3990243077 - 0.4462144971 0.4301818907 0.4000793993 - 0.4472317100 0.4312092960 0.4011329114 - 0.4482460916 0.4322367013 0.4021860957 - 0.4492605031 0.4332641959 0.4032391906 - 0.4502733946 0.4342932105 0.4042918980 - 0.4512828887 0.4353255928 0.4053437114 - 0.4522925019 0.4363580048 0.4063954949 - 0.4533020854 0.4373905063 0.4074473083 - 0.4543111026 0.4384124875 0.4084874094 - 0.4553197920 0.4394313991 0.4095237851 - 0.4563286006 0.4404503107 0.4105601907 - 0.4573364854 0.4414677918 0.4115959108 - 0.4583398104 0.4424777925 0.4126274884 - 0.4593431056 0.4434877038 0.4136590958 - 0.4603464007 0.4444975853 0.4146907032 - 0.4613440037 0.4455075860 0.4157181084 - 0.4623357058 0.4465177059 0.4167410135 - 0.4633274078 0.4475277066 0.4177640080 - 0.4643191993 0.4485377967 0.4187869132 - 0.4653109908 0.4495514035 0.4198037088 - 0.4663026929 0.4505659938 0.4208188057 - 0.4672945142 0.4515804946 0.4218339026 - 0.4682861865 0.4525944889 0.4228489995 - 0.4692744911 0.4535953999 0.4238614142 - 0.4702627957 0.4545961916 0.4248737097 - 0.4712511003 0.4555971026 0.4258860946 - 0.4722385108 0.4565958977 0.4268980026 - 0.4732227027 0.4575876892 0.4279080927 - 0.4742070138 0.4585795999 0.4289183021 - 0.4751912951 0.4595713913 0.4299283922 - 0.4761728942 0.4605633020 0.4309360981 - 0.4771496952 0.4615552127 0.4319396913 - 0.4781264961 0.4625470936 0.4329431951 - 0.4791032970 0.4635390043 0.4339466989 - 0.4800817966 0.4645307064 0.4349519014 - 0.4810624123 0.4655222893 0.4359593987 - 0.4820429981 0.4665139019 0.4369668067 - 0.4830236137 0.4675053954 0.4379743040 - 0.4840019941 0.4684920907 0.4389804900 - 0.4849779904 0.4694738090 0.4399853051 - 0.4859539866 0.4704554975 0.4409902096 - 0.4869301021 0.4714370966 0.4419949949 - 0.4879001081 0.4724138081 0.4429973066 - 0.4888631999 0.4733850062 0.4439963996 - 0.4898262918 0.4743562043 0.4449956119 - 0.4907892942 0.4753274024 0.4459947944 - 0.4917517006 0.4762980044 0.4469918907 - 0.4927130938 0.4772675931 0.4479857981 - 0.4936743975 0.4782372117 0.4489797056 - 0.4946357012 0.4792068005 0.4499736130 - 0.4955956042 0.4801762998 0.4509660006 - 0.4965516925 0.4811457098 0.4519538879 - 0.4975079000 0.4821150899 0.4529418051 - 0.4984639883 0.4830844998 0.4539296925 - 0.4994196892 0.4840534031 0.4549174011 - 0.5003700852 0.4850172102 0.4559034109 - 0.5013206005 0.4859808981 0.4568893015 - 0.5022711158 0.4869445860 0.4578751922 - 0.5032215118 0.4879083037 0.4588611126 - 0.5041645169 0.4888612032 0.4598459005 - 0.5051059127 0.4898119867 0.4608305097 - 0.5060474277 0.4907627106 0.4618149996 - 0.5069888234 0.4917134941 0.4627996087 - 0.5079312921 0.4926620126 0.4637798965 - 0.5088745952 0.4936085045 0.4647563100 - 0.5098178983 0.4945549965 0.4657326937 - 0.5107612014 0.4955014884 0.4667089880 - 0.5117039084 0.4964486957 0.4676840901 - 0.5126433969 0.4973995090 0.4686524868 - 0.5135828853 0.4983502924 0.4696210027 - 0.5145223737 0.4993009865 0.4705893993 - 0.5154619217 0.5002518296 0.4715577960 - 0.5163952708 0.5012043118 0.4725238979 - 0.5173267126 0.5021573901 0.4734891057 - 0.5182582140 0.5031105280 0.4744544029 - 0.5191895962 0.5040636063 0.4754197001 - 0.5201197863 0.5050135255 0.4763843119 - 0.5210466981 0.5059555173 0.4773474038 - 0.5219736099 0.5068973899 0.4783104062 - 0.5229005218 0.5078393817 0.4792734981 - 0.5238274932 0.5087813735 0.4802365899 - 0.5247486830 0.5097197294 0.4811953008 - 0.5256680846 0.5106567144 0.4821526110 - 0.5265874267 0.5115938187 0.4831098914 - 0.5275068283 0.5125308037 0.4840672016 - 0.5284252763 0.5134680271 0.4850249887 - 0.5293390155 0.5144063234 0.4859859049 - 0.5302528143 0.5153445005 0.4869467914 - 0.5311664939 0.5162826777 0.4879077077 - 0.5320801735 0.5172209740 0.4888687134 - 0.5329914093 0.5181534290 0.4898253977 - 0.5338997245 0.5190796256 0.4907774925 - 0.5348079801 0.5200058222 0.4917295873 - 0.5357164145 0.5209320784 0.4926817119 - 0.5366247296 0.5218582749 0.4936338067 - 0.5375316739 0.5227739215 0.4945797026 - 0.5384383202 0.5236862898 0.4955236018 - 0.5393447876 0.5245985985 0.4964675009 - 0.5402513742 0.5255110264 0.4974114001 - 0.5411579013 0.5264232755 0.4983552992 - 0.5420578122 0.5273302197 0.4992960095 - 0.5429574251 0.5282369256 0.5002363920 - 0.5438570976 0.5291436911 0.5011768937 - 0.5447567105 0.5300503969 0.5021173954 - 0.5456550717 0.5309572220 0.5030577779 - 0.5465437174 0.5318645835 0.5039975047 - 0.5474323034 0.5327720046 0.5049371123 - 0.5483208895 0.5336794257 0.5058767796 - 0.5492094755 0.5345867872 0.5068165064 - 0.5500965118 0.5354917049 0.5077555180 - 0.5509771109 0.5363870263 0.5086914897 - 0.5518578291 0.5372822881 0.5096275210 - 0.5527384281 0.5381776094 0.5105633736 - 0.5536190271 0.5390728712 0.5114994049 - 0.5545002222 0.5399649739 0.5124346018 - 0.5553833246 0.5408456922 0.5133668780 - 0.5562664866 0.5417262912 0.5142992139 - 0.5571495891 0.5426068902 0.5152313709 - 0.5580328107 0.5434876084 0.5161637068 - 0.5589159131 0.5443671942 0.5170946121 - 0.5597988963 0.5452414751 0.5180175900 - 0.5606818795 0.5461158156 0.5189406872 - 0.5615649223 0.5469899774 0.5198637843 - 0.5624479055 0.5478643179 0.5207868218 - 0.5633308887 0.5487385988 0.5217097998 - 0.5642132163 0.5496147871 0.5226305127 - 0.5650954247 0.5504909158 0.5235512257 - 0.5659775734 0.5513671041 0.5244718790 - 0.5668599010 0.5522431731 0.5253925920 - 0.5677421093 0.5531194210 0.5263133049 - 0.5686208010 0.5539948940 0.5272328854 - 0.5694987178 0.5548701882 0.5281524062 - 0.5703766942 0.5557454824 0.5290718079 - 0.5712546110 0.5566208959 0.5299912095 - 0.5721325874 0.5574961901 0.5309106112 - 0.5730063915 0.5583639145 0.5318285227 - 0.5738772154 0.5592259765 0.5327451229 - 0.5747479200 0.5600880980 0.5336617231 - 0.5756186843 0.5609502196 0.5345783234 - 0.5764895082 0.5618122816 0.5354949236 - 0.5773584247 0.5626726747 0.5364094973 - 0.5782213807 0.5635278821 0.5373175144 - 0.5790842772 0.5643829703 0.5382254124 - 0.5799472928 0.5652381182 0.5391334295 - 0.5808103085 0.5660933256 0.5400413871 - 0.5816733241 0.5669484138 0.5409492850 - 0.5825272799 0.5678043962 0.5418502092 - 0.5833795071 0.5686606169 0.5427495241 - 0.5842316747 0.5695167184 0.5436487794 - 0.5850839019 0.5703728795 0.5445482135 - 0.5859361291 0.5712291002 0.5454475284 - 0.5867881775 0.5720869899 0.5463449955 - 0.5876401067 0.5729479790 0.5472388864 - 0.5884919167 0.5738090873 0.5481327772 - 0.5893437862 0.5746700764 0.5490267277 - 0.5901955962 0.5755311847 0.5499206781 - 0.5910475254 0.5763922930 0.5508146286 - 0.5918961763 0.5772445202 0.5517038107 - 0.5927441716 0.5780944824 0.5525919199 - 0.5935921073 0.5789445043 0.5534799099 - 0.5944401026 0.5797945857 0.5543678999 - 0.5952879786 0.5806446075 0.5552558899 - 0.5961351991 0.5814933777 0.5561434031 - 0.5969787836 0.5823364854 0.5570282936 - 0.5978223085 0.5831795931 0.5579131246 - 0.5986657739 0.5840227008 0.5587978959 - 0.5995092988 0.5848656893 0.5596827865 - 0.6003528833 0.5857087970 0.5605676174 - 0.6011931896 0.5865523219 0.5614522099 - 0.6020298004 0.5873963237 0.5623363853 - 0.6028664112 0.5882403255 0.5632206202 - 0.6037030220 0.5890843272 0.5641047955 - 0.6045395732 0.5899283290 0.5649890900 - 0.6053761840 0.5907722712 0.5658733249 - 0.6062102914 0.5916194916 0.5667579174 - 0.6070430875 0.5924683213 0.5676426291 - 0.6078760028 0.5933170915 0.5685272813 - 0.6087087989 0.5941658020 0.5694121122 - 0.6095417142 0.5950145721 0.5702968240 - 0.6103746295 0.5958634019 0.5711815953 - 0.6112043858 0.5967034101 0.5720716119 - 0.6120336056 0.5975413918 0.5729628205 - 0.6128627062 0.5983793736 0.5738540292 - 0.6136919260 0.5992174149 0.5747451782 - 0.6145210862 0.6000553966 0.5756365061 - 0.6153501868 0.6008933783 0.5765277147 - 0.6161777973 0.6017255187 0.5774099231 - 0.6170051098 0.6025568247 0.5782908201 - 0.6178324223 0.6033880115 0.5791717172 - 0.6186597943 0.6042193174 0.5800524950 - 0.6194871068 0.6050505042 0.5809333920 - 0.6203144193 0.6058818102 0.5818142295 - 0.6211380959 0.6067138910 0.5826920271 - 0.6219611764 0.6075462103 0.5835691094 - 0.6227841973 0.6083784103 0.5844463110 - 0.6236072779 0.6092107296 0.5853233933 - 0.6244302988 0.6100429893 0.5862005949 - 0.6252533793 0.6108751893 0.5870776772 - 0.6260753274 0.6117079258 0.5879529119 - 0.6268969178 0.6125406027 0.5888274908 - 0.6277183890 0.6133733988 0.5897021294 - 0.6285399199 0.6142061949 0.5905765891 - 0.6293615103 0.6150388718 0.5914512277 - 0.6301829815 0.6158717275 0.5923258066 - 0.6310017109 0.6166999936 0.5932002068 - 0.6318179965 0.6175248027 0.5940744281 - 0.6326344013 0.6183496118 0.5949487090 - 0.6334508061 0.6191744208 0.5958228707 - 0.6342670918 0.6199991703 0.5966970921 - 0.6350834966 0.6208239794 0.5975713730 - 0.6358969808 0.6216465235 0.5984457135 - 0.6367033720 0.6224632263 0.5993201137 - 0.6375098228 0.6232798100 0.6001945138 - 0.6383162141 0.6240965128 0.6010689735 - 0.6391226053 0.6249132156 0.6019433737 - 0.6399289966 0.6257297993 0.6028177738 - 0.6407353878 0.6265465021 0.6036921740 - 0.6415356994 0.6273627281 0.6045618057 - 0.6423354745 0.6281787753 0.6054310799 - 0.6431353092 0.6289948821 0.6063002944 - 0.6439350843 0.6298109889 0.6071695089 - 0.6447349191 0.6306270957 0.6080387235 - 0.6455346942 0.6314432025 0.6089078784 - 0.6463314891 0.6322597861 0.6097748876 - 0.6471250057 0.6330770850 0.6106392741 - 0.6479185820 0.6338943243 0.6115037799 - 0.6487122178 0.6347116232 0.6123682261 - 0.6495056748 0.6355288029 0.6132326722 - 0.6502993107 0.6363461018 0.6140971184 - 0.6510928869 0.6371632814 0.6149616241 - 0.6518819928 0.6379770041 0.6158189178 - 0.6526708007 0.6387906075 0.6166757941 - 0.6534597278 0.6396042109 0.6175327897 - 0.6542485952 0.6404178143 0.6183897257 - 0.6550374031 0.6412314177 0.6192467213 - 0.6558262706 0.6420449018 0.6201035976 - 0.6566126943 0.6428549886 0.6209588051 - 0.6573941112 0.6436582804 0.6218106151 - 0.6581755280 0.6444615722 0.6226623058 - 0.6589568853 0.6452649236 0.6235139966 - 0.6597383022 0.6460682154 0.6243656874 - 0.6605197191 0.6468715072 0.6252173781 - 0.6613010168 0.6476747990 0.6260691881 - 0.6620833874 0.6484764218 0.6269180179 - 0.6628664136 0.6492767930 0.6277649999 - 0.6636493206 0.6500772834 0.6286119819 - 0.6644322872 0.6508777738 0.6294590831 - 0.6652151942 0.6516783237 0.6303061247 - 0.6659982204 0.6524788141 0.6311531067 - 0.6667811275 0.6532791853 0.6320002079 - 0.6675598025 0.6540803909 0.6328403950 - 0.6683375835 0.6548817754 0.6336792111 - 0.6691154242 0.6556832194 0.6345179081 - 0.6698932052 0.6564844847 0.6353567243 - 0.6706709862 0.6572859287 0.6361954212 - 0.6714488268 0.6580871940 0.6370341778 - 0.6722266078 0.6588885784 0.6378728747 - 0.6729933023 0.6596770287 0.6387121081 - 0.6737592816 0.6604645252 0.6395512223 - 0.6745253205 0.6612520814 0.6403903961 - 0.6752912998 0.6620395780 0.6412295103 - 0.6760572791 0.6628271937 0.6420686841 - 0.6768233180 0.6636146903 0.6429077983 - 0.6775892973 0.6644023061 0.6437469721 - 0.6783525944 0.6651803851 0.6445764899 - 0.6791155934 0.6659579873 0.6454054117 - 0.6798787117 0.6667355895 0.6462342739 - 0.6806417704 0.6675131917 0.6470631957 - 0.6814048886 0.6682907939 0.6478921175 - 0.6821680069 0.6690683961 0.6487209797 - 0.6829311252 0.6698459983 0.6495499015 - 0.6836892962 0.6706212759 0.6503704190 - 0.6844468117 0.6713961959 0.6511895061 - 0.6852043271 0.6721711755 0.6520085931 - 0.6859617829 0.6729462147 0.6528277993 - 0.6867192984 0.6737210751 0.6536468863 - 0.6874766946 0.6744961143 0.6544659734 - 0.6882342100 0.6752710938 0.6552851796 - 0.6889876127 0.6760479808 0.6560990810 - 0.6897391081 0.6768258810 0.6569107175 - 0.6904906034 0.6776037216 0.6577221751 - 0.6912420988 0.6783815026 0.6585338116 - 0.6919935942 0.6791594028 0.6593453884 - 0.6927450895 0.6799371839 0.6601569057 - 0.6934965849 0.6807150841 0.6609684825 - 0.6942445040 0.6814894080 0.6617767215 - 0.6949872971 0.6822587848 0.6625800729 - 0.6957302094 0.6830282211 0.6633834243 - 0.6964730024 0.6837977171 0.6641867757 - 0.6972159147 0.6845670938 0.6649901867 - 0.6979587078 0.6853365898 0.6657935977 - 0.6987016201 0.6861060262 0.6665970087 - 0.6994444132 0.6868749261 0.6674001813 - 0.7001873255 0.6876338124 0.6682003736 - 0.7009301782 0.6883925796 0.6690005064 - 0.7016730905 0.6891514063 0.6698006988 - 0.7024160028 0.6899102926 0.6706007719 - 0.7031589150 0.6906691194 0.6714010239 - 0.7039018273 0.6914280057 0.6722010970 - 0.7046446800 0.6921867728 0.6730012894 - 0.7053861022 0.6929436922 0.6738013029 - 0.7061262727 0.6936991215 0.6746010780 - 0.7068666220 0.6944546103 0.6754009128 - 0.7076067924 0.6952099800 0.6762006879 - 0.7083470821 0.6959655285 0.6770005226 - 0.7090873718 0.6967208982 0.6778002977 - 0.7098276019 0.6974763870 0.6786001921 - 0.7105678916 0.6982318759 0.6794000268 - 0.7113052011 0.6989892721 0.6801937222 - 0.7120423913 0.6997467875 0.6809874177 - 0.7127797008 0.7005043030 0.6817811728 - 0.7135170102 0.7012618184 0.6825749278 - 0.7142542005 0.7020192742 0.6833686233 - 0.7149915099 0.7027767897 0.6841623187 - 0.7157288194 0.7035343051 0.6849560142 - 0.7164642215 0.7042888999 0.6857504845 - 0.7171959281 0.7050374746 0.6865466833 - 0.7179275155 0.7057859898 0.6873428822 - 0.7186592221 0.7065346241 0.6881390810 - 0.7193908095 0.7072831988 0.6889352798 - 0.7201225162 0.7080317736 0.6897314191 - 0.7208541036 0.7087804079 0.6905276179 - 0.7215858102 0.7095289826 0.6913238168 - 0.7223190069 0.7102715969 0.6921185255 - 0.7230535150 0.7110090256 0.6929119229 - 0.7237880230 0.7117465138 0.6937052011 - 0.7245225906 0.7124840021 0.6944985986 - 0.7252570987 0.7132214904 0.6952919960 - 0.7259916067 0.7139589787 0.6960853934 - 0.7267261147 0.7146965265 0.6968787909 - 0.7274606228 0.7154340148 0.6976721883 - 0.7281941175 0.7161688209 0.6984621882 - 0.7289270163 0.7169021964 0.6992504001 - 0.7296599150 0.7176355720 0.7000386715 - 0.7303928137 0.7183690071 0.7008268833 - 0.7311257720 0.7191023827 0.7016150951 - 0.7318586707 0.7198358178 0.7024034262 - 0.7325916290 0.7205691934 0.7031915784 - 0.7333245277 0.7213026285 0.7039797902 - 0.7340542078 0.7220379114 0.7047694921 - 0.7347822189 0.7227740884 0.7055600286 - 0.7355102897 0.7235103846 0.7063505054 - 0.7362383008 0.7242466211 0.7071409822 - 0.7369663715 0.7249829173 0.7079313993 - 0.7376943827 0.7257190943 0.7087218761 - 0.7384225130 0.7264553905 0.7095124125 - 0.7391505241 0.7271916270 0.7103028893 - 0.7398774028 0.7279284000 0.7110937238 - 0.7406033874 0.7286654115 0.7118847966 - 0.7413293719 0.7294024825 0.7126759291 - 0.7420554161 0.7301396132 0.7134671211 - 0.7427814007 0.7308766842 0.7142581940 - 0.7435073853 0.7316136956 0.7150493264 - 0.7442334294 0.7323508263 0.7158403993 - 0.7449594140 0.7330878973 0.7166314721 - 0.7456837893 0.7338215709 0.7174229026 - 0.7464053035 0.7345489264 0.7182146907 - 0.7471268177 0.7352762222 0.7190064788 - 0.7478482127 0.7360035181 0.7197983265 - 0.7485697269 0.7367308140 0.7205901146 - 0.7492911816 0.7374581099 0.7213820219 - 0.7500126958 0.7381855249 0.7221738100 - 0.7507342100 0.7389128208 0.7229655981 - 0.7514556050 0.7396399975 0.7237573266 - 0.7521721721 0.7403625846 0.7245445251 - 0.7528889179 0.7410852909 0.7253317833 - 0.7536056042 0.7418079972 0.7261189818 - 0.7543221712 0.7425305843 0.7269061804 - 0.7550389171 0.7432532907 0.7276934981 - 0.7557556033 0.7439759970 0.7284806967 - 0.7564721704 0.7446985841 0.7292678952 - 0.7571889162 0.7454212904 0.7300552130 - 0.7579010725 0.7461451888 0.7308400273 - 0.7586101890 0.7468699813 0.7316231132 - 0.7593191862 0.7475947738 0.7324061990 - 0.7600283027 0.7483196259 0.7331892848 - 0.7607374191 0.7490442991 0.7339723706 - 0.7614464164 0.7497690916 0.7347555161 - 0.7621554732 0.7504938841 0.7355386019 - 0.7628644705 0.7512186766 0.7363216877 - 0.7635735869 0.7519437075 0.7371047735 - 0.7642832994 0.7526741028 0.7378858924 - 0.7649930120 0.7534046173 0.7386670709 - 0.7657027245 0.7541350126 0.7394481897 - 0.7664124966 0.7548654079 0.7402294278 - 0.7671222091 0.7555958033 0.7410104871 - 0.7678319216 0.7563263178 0.7417917252 - 0.7685415745 0.7570567131 0.7425727844 - 0.7692512870 0.7577871084 0.7433540225 - 0.7699596882 0.7585147023 0.7441341877 - 0.7706660032 0.7592375278 0.7449126244 - 0.7713723183 0.7599601746 0.7456911206 - 0.7720785737 0.7606830001 0.7464696169 - 0.7727848887 0.7614058256 0.7472481132 - 0.7734912038 0.7621284723 0.7480264902 - 0.7741973996 0.7628512979 0.7488049865 - 0.7749037147 0.7635740042 0.7495834827 - 0.7756100297 0.7642968297 0.7503619790 - 0.7763124108 0.7650169730 0.7511404753 - 0.7770121098 0.7657353282 0.7519189715 - 0.7777118087 0.7664536238 0.7526975274 - 0.7784113884 0.7671719790 0.7534760833 - 0.7791110873 0.7678902745 0.7542545795 - 0.7798107266 0.7686086297 0.7550330758 - 0.7805104256 0.7693269849 0.7558116913 - 0.7812101245 0.7700452805 0.7565901875 - 0.7819097042 0.7707635760 0.7573688030 - 0.7826066017 0.7714834213 0.7581411004 - 0.7833021283 0.7722039223 0.7589104176 - 0.7839977145 0.7729244232 0.7596796751 - 0.7846931815 0.7736448050 0.7604489923 - 0.7853888273 0.7743653059 0.7612183094 - 0.7860842943 0.7750858068 0.7619876266 - 0.7867798805 0.7758061886 0.7627568841 - 0.7874754071 0.7765266895 0.7635262012 - 0.7881708741 0.7772471905 0.7642955184 - 0.7888624072 0.7779653072 0.7650622725 - 0.7895516157 0.7786822915 0.7658277750 - 0.7902408838 0.7793992162 0.7665932775 - 0.7909300923 0.7801160812 0.7673587799 - 0.7916194201 0.7808331251 0.7681242824 - 0.7923086286 0.7815499902 0.7688897848 - 0.7929978967 0.7822669148 0.7696552873 - 0.7936871052 0.7829838991 0.7704207897 - 0.7943763733 0.7837008238 0.7711862922 - 0.7950640917 0.7844138741 0.7719494104 - 0.7957503200 0.7851232290 0.7727103233 - 0.7964366078 0.7858325243 0.7734711170 - 0.7971227765 0.7865418196 0.7742319703 - 0.7978091240 0.7872511744 0.7749928236 - 0.7984952927 0.7879605293 0.7757536769 - 0.7991815209 0.7886698246 0.7765144706 - 0.7998678088 0.7893791199 0.7772753835 - 0.8005539775 0.7900884151 0.7780361772 - 0.8012402058 0.7907962799 0.7787957788 - 0.8019264936 0.7914993763 0.7795513272 - 0.8026127219 0.7922024131 0.7803066969 - 0.8032988906 0.7929055095 0.7810621262 - 0.8039851189 0.7936086059 0.7818176150 - 0.8046712875 0.7943117023 0.7825729847 - 0.8053575754 0.7950147986 0.7833284140 - 0.8060438037 0.7957177758 0.7840839028 - 0.8067299724 0.7964208722 0.7848392725 - 0.8074162006 0.7971240282 0.7855947018 - 0.8081002235 0.7978271842 0.7863516212 - 0.8087838292 0.7985303998 0.7871088982 - 0.8094673753 0.7992336154 0.7878661156 - 0.8101509809 0.7999367714 0.7886233926 - 0.8108345866 0.8006399870 0.7893806100 - 0.8115181923 0.8013432026 0.7901378274 - 0.8122016788 0.8020464778 0.7908951044 - 0.8128852844 0.8027496934 0.7916523218 - 0.8135688901 0.8034529090 0.7924095988 - 0.8142508864 0.8041560054 0.7931652069 - 0.8149294853 0.8048589826 0.7939171791 - 0.8156080842 0.8055620790 0.7946692705 - 0.8162866831 0.8062651157 0.7954213023 - 0.8169652820 0.8069680929 0.7961732745 - 0.8176438808 0.8076711297 0.7969254255 - 0.8183224797 0.8083741069 0.7976773977 - 0.8190010190 0.8090770841 0.7984294295 - 0.8196796179 0.8097801208 0.7991815209 - 0.8203582168 0.8104832172 0.7999334931 - 0.8210343719 0.8111814260 0.8006806970 - 0.8217092156 0.8118773103 0.8014255166 - 0.8223841190 0.8125730753 0.8021702170 - 0.8230590224 0.8132688999 0.8029149175 - 0.8237338066 0.8139647245 0.8036596775 - 0.8244087100 0.8146606088 0.8044043779 - 0.8250836134 0.8153563738 0.8051490784 - 0.8257583976 0.8160521984 0.8058938980 - 0.8264333010 0.8167480230 0.8066385984 - 0.8271082044 0.8174437881 0.8073834181 - 0.8277801871 0.8181331158 0.8081256151 - 0.8284516931 0.8188214898 0.8088673949 - 0.8291233182 0.8195099235 0.8096091747 - 0.8297948837 0.8201981783 0.8103510141 - 0.8304665089 0.8208866119 0.8110929132 - 0.8311380148 0.8215749860 0.8118346930 - 0.8318095803 0.8222633004 0.8125764728 - 0.8324812055 0.8229516745 0.8133183122 - 0.8331527710 0.8236399889 0.8140602112 - 0.8338242769 0.8243284225 0.8148019910 - 0.8344954848 0.8250157833 0.8155404925 - 0.8351665735 0.8257030845 0.8162788153 - 0.8358377218 0.8263903856 0.8170171976 - 0.8365089297 0.8270778060 0.8177555203 - 0.8371800184 0.8277651072 0.8184939027 - 0.8378511071 0.8284524083 0.8192322254 - 0.8385221958 0.8291397095 0.8199704885 - 0.8391934037 0.8298270702 0.8207088709 - 0.8398644924 0.8305143714 0.8214471936 - 0.8405355811 0.8312016726 0.8221855760 - 0.8412041068 0.8318877816 0.8229212165 - 0.8418722749 0.8325737119 0.8236564994 - 0.8425405025 0.8332597017 0.8243917823 - 0.8432086706 0.8339456916 0.8251271248 - 0.8438770175 0.8346316218 0.8258624077 - 0.8445451856 0.8353176117 0.8265976906 - 0.8452134132 0.8360034823 0.8273329735 - 0.8458815813 0.8366894722 0.8280683160 - 0.8465498090 0.8373754025 0.8288035989 - 0.8472179770 0.8380613923 0.8295388818 - 0.8478839993 0.8387414813 0.8302717209 - 0.8485488892 0.8394191861 0.8310034275 - 0.8492138982 0.8400970101 0.8317350745 - 0.8498789072 0.8407747149 0.8324669003 - 0.8505439162 0.8414524198 0.8331986070 - 0.8512089252 0.8421301842 0.8339303136 - 0.8518738747 0.8428078890 0.8346620202 - 0.8525388837 0.8434855938 0.8353937864 - 0.8532038927 0.8441634178 0.8361254930 - 0.8538689017 0.8448411226 0.8368571997 - 0.8545317054 0.8455154896 0.8375867009 - 0.8551911116 0.8461846709 0.8383126259 - 0.8558505177 0.8468537927 0.8390386105 - 0.8565099835 0.8475229740 0.8397644758 - 0.8571693897 0.8481922150 0.8404904008 - 0.8578287959 0.8488612771 0.8412163258 - 0.8584882021 0.8495305181 0.8419421911 - 0.8591476083 0.8501995802 0.8426681161 - 0.8598070145 0.8508688211 0.8433941007 - 0.8604664207 0.8515380025 0.8441200256 - 0.8611258268 0.8522071242 0.8448458910 - 0.8617823720 0.8528741002 0.8455688953 - 0.8624386787 0.8535410166 0.8462917209 - 0.8630949855 0.8542078137 0.8470144272 - 0.8637512922 0.8548746109 0.8477371931 - 0.8644075990 0.8555414081 0.8484598994 - 0.8650639057 0.8562082052 0.8491827250 - 0.8657202125 0.8568751216 0.8499054909 - 0.8663765192 0.8575419188 0.8506281972 - 0.8670328259 0.8582087159 0.8513510227 - 0.8676890731 0.8588755131 0.8520737290 - 0.8683446050 0.8595426083 0.8527960777 - 0.8689982891 0.8602101803 0.8535174727 - 0.8696519136 0.8608778119 0.8542389870 - 0.8703055978 0.8615455031 0.8549603820 - 0.8709592223 0.8622130752 0.8556818962 - 0.8716129065 0.8628807068 0.8564032912 - 0.8722665906 0.8635482788 0.8571248055 - 0.8729202151 0.8642160296 0.8578462005 - 0.8735738993 0.8648836017 0.8585677147 - 0.8742275238 0.8655511737 0.8592891097 - 0.8748812079 0.8662188053 0.8600106239 - 0.8755341172 0.8668799996 0.8607301712 - 0.8761864901 0.8675361276 0.8614482284 - 0.8768388033 0.8681921959 0.8621662855 - 0.8774911761 0.8688483238 0.8628842831 - 0.8781434894 0.8695043921 0.8636023998 - 0.8787959218 0.8701604009 0.8643205166 - 0.8794481754 0.8708165288 0.8650385141 - 0.8801006079 0.8714725971 0.8657565713 - 0.8807529211 0.8721287251 0.8664746284 - 0.8814052939 0.8727847934 0.8671926856 - 0.8820576072 0.8734409213 0.8679106832 - 0.8827077150 0.8740913868 0.8686279058 - 0.8833565116 0.8747391105 0.8693444729 - 0.8840054274 0.8753867745 0.8700612187 - 0.8846542835 0.8760343790 0.8707777858 - 0.8853030801 0.8766821027 0.8714944720 - 0.8859519958 0.8773298264 0.8722112179 - 0.8866009116 0.8779774904 0.8729277849 - 0.8872497082 0.8786250949 0.8736444712 - 0.8878986239 0.8792728186 0.8743610978 - 0.8885474205 0.8799204826 0.8750777841 - 0.8891962767 0.8805680871 0.8757944107 - 0.8898416162 0.8812143803 0.8765113950 - 0.8904845715 0.8818597794 0.8772286177 - 0.8911275268 0.8825051785 0.8779457808 - 0.8917704821 0.8831505775 0.8786630034 - 0.8924134970 0.8837959766 0.8793802857 - 0.8930563927 0.8844413757 0.8800975084 - 0.8936994076 0.8850867748 0.8808146715 - 0.8943423033 0.8857321143 0.8815318942 - 0.8949853182 0.8863775134 0.8822491169 - 0.8956282735 0.8870229125 0.8829662800 - 0.8962712288 0.8876683116 0.8836835027 - 0.8969116211 0.8883140087 0.8844001293 - 0.8975483775 0.8889601827 0.8851156831 - 0.8981850743 0.8896064162 0.8858312964 - 0.8988217711 0.8902524710 0.8865469098 - 0.8994585276 0.8908987045 0.8872625232 - 0.9000952244 0.8915448785 0.8879780769 - 0.9007319212 0.8921909928 0.8886936903 - 0.9013686180 0.8928372264 0.8894093037 - 0.9020053148 0.8934834003 0.8901249170 - 0.9026420116 0.8941295147 0.8908404112 - 0.9032788277 0.8947756886 0.8915560246 - 0.9039149880 0.8954212070 0.8922712803 - 0.9045432806 0.8960561156 0.8929808736 - 0.9051715732 0.8966909051 0.8936905265 - 0.9057999253 0.8973258138 0.8944001794 - 0.9064282179 0.8979606032 0.8951097727 - 0.9070565104 0.8985955119 0.8958194256 - 0.9076849222 0.8992303014 0.8965290189 - 0.9083132148 0.8998652101 0.8972386718 - 0.9089415073 0.9004999995 0.8979483247 - 0.9095697999 0.9011349082 0.8986579180 - 0.9101980925 0.9017696977 0.8993675113 - 0.9108263850 0.9024046063 0.9000772238 - 0.9114509225 0.9030348063 0.9007843137 - 0.9120721817 0.9036613703 0.9014893770 - 0.9126935005 0.9042879939 0.9021945000 - 0.9133148193 0.9049146175 0.9028996229 - 0.9139360785 0.9055411816 0.9036046267 - 0.9145573974 0.9061678052 0.9043096900 - 0.9151787162 0.9067944288 0.9050148129 - 0.9157999754 0.9074209929 0.9057198763 - 0.9164212942 0.9080476165 0.9064249992 - 0.9170426130 0.9086741805 0.9071301222 - 0.9176638722 0.9093006849 0.9078351855 - 0.9182853103 0.9099273086 0.9085403085 - 0.9188956022 0.9105505943 0.9092364907 - 0.9195045829 0.9111735225 0.9099317789 - 0.9201136827 0.9117963910 0.9106270075 - 0.9207227826 0.9124193192 0.9113222957 - 0.9213318229 0.9130421281 0.9120175242 - 0.9219409227 0.9136649966 0.9127128124 - 0.9225499034 0.9142879248 0.9134079814 - 0.9231590033 0.9149107933 0.9141033292 - 0.9237679839 0.9155337214 0.9147984982 - 0.9243770838 0.9161565900 0.9154937863 - 0.9249861240 0.9167795181 0.9161890149 - 0.9255942106 0.9174020886 0.9168831706 - 0.9261904955 0.9180212021 0.9175652862 - 0.9267867804 0.9186403751 0.9182474017 - 0.9273831248 0.9192596078 0.9189295173 - 0.9279794097 0.9198787808 0.9196115732 - 0.9285756946 0.9204980135 0.9202936888 - 0.9291719794 0.9211171269 0.9209758043 - 0.9297683239 0.9217363000 0.9216579199 - 0.9303646088 0.9223554730 0.9223399758 - 0.9309607744 0.9229747057 0.9230220914 - 0.9315571189 0.9235938787 0.9237042069 - 0.9321534038 0.9242129922 0.9243863225 - 0.9327489734 0.9248315096 0.9250671268 - 0.9333375096 0.9254437089 0.9257367849 - 0.9339259863 0.9260557890 0.9264063835 - 0.9345145226 0.9266679287 0.9270759821 - 0.9351029992 0.9272801280 0.9277455807 - 0.9356915951 0.9278922081 0.9284152985 - 0.9362800717 0.9285042882 0.9290848970 - 0.9368686080 0.9291164875 0.9297544956 - 0.9374570847 0.9297286272 0.9304240942 - 0.9380456209 0.9303408265 0.9310936928 - 0.9386342168 0.9309529066 0.9317634106 - 0.9392226934 0.9315649867 0.9324330091 - 0.9398112297 0.9321771860 0.9331026077 - 0.9403957725 0.9327827096 0.9337632060 - 0.9409803152 0.9333879948 0.9344233274 - 0.9415647984 0.9339932799 0.9350835085 - 0.9421492815 0.9345986247 0.9357436895 - 0.9427338243 0.9352039099 0.9364038110 - 0.9433181882 0.9358091950 0.9370639920 - 0.9439026713 0.9364144802 0.9377241731 - 0.9444872141 0.9370198250 0.9383842945 - 0.9450716972 0.9376251101 0.9390444756 - 0.9456561804 0.9382303953 0.9397047162 - 0.9462407231 0.9388356805 0.9403647780 - 0.9468250871 0.9394410253 0.9410250187 - 0.9474024773 0.9400426149 0.9416800141 - 0.9479761124 0.9406421781 0.9423323274 - 0.9485496879 0.9412418008 0.9429845214 - 0.9491233230 0.9418414831 0.9436367750 - 0.9496968985 0.9424411058 0.9442890882 - 0.9502704740 0.9430407286 0.9449412823 - 0.9508441091 0.9436402917 0.9455935955 - 0.9514176846 0.9442399740 0.9462457895 - 0.9519913197 0.9448395967 0.9468981028 - 0.9525648952 0.9454392195 0.9475504160 - 0.9531384706 0.9460387826 0.9482026100 - 0.9537121058 0.9466385245 0.9488549232 - 0.9542835951 0.9472373724 0.9495049119 - 0.9548454881 0.9478332996 0.9501445293 - 0.9554073811 0.9484291077 0.9507842064 - 0.9559693933 0.9490249753 0.9514238238 - 0.9565312862 0.9496209025 0.9520635009 - 0.9570931792 0.9502167106 0.9527031779 - 0.9576550722 0.9508125782 0.9533427954 - 0.9582170248 0.9514083862 0.9539824724 - 0.9587789178 0.9520043135 0.9546220899 - 0.9593408108 0.9526001811 0.9552618265 - 0.9599027038 0.9531959891 0.9559013844 - 0.9604645967 0.9537919164 0.9565411210 - 0.9610264897 0.9543877244 0.9571806788 - 0.9615818262 0.9549801946 0.9578106999 - 0.9621313214 0.9555696845 0.9584323764 - 0.9626806974 0.9561592937 0.9590541124 - 0.9632301927 0.9567487836 0.9596757293 - 0.9637796879 0.9573382735 0.9602974057 - 0.9643291831 0.9579278827 0.9609190822 - 0.9648786783 0.9585173726 0.9615408182 - 0.9654281735 0.9591069221 0.9621623755 - 0.9659777284 0.9596964717 0.9627841115 - 0.9665271044 0.9602860212 0.9634057879 - 0.9670765996 0.9608755112 0.9640274048 - 0.9676260948 0.9614651203 0.9646490812 - 0.9681755900 0.9620546103 0.9652708173 - 0.9687122703 0.9626389146 0.9658780098 - 0.9692441821 0.9632211924 0.9664797187 - 0.9697760940 0.9638035297 0.9670814276 - 0.9703080058 0.9643858075 0.9676830769 - 0.9708399177 0.9649680853 0.9682847857 - 0.9713717103 0.9655504227 0.9688864946 - 0.9719036222 0.9661327004 0.9694882035 - 0.9724354744 0.9667149782 0.9700899124 - 0.9729673862 0.9672973156 0.9706916213 - 0.9734992981 0.9678795934 0.9712932706 - 0.9740310907 0.9684618711 0.9718949795 - 0.9745630026 0.9690440893 0.9724966288 - 0.9750949144 0.9696264267 0.9730982780 - 0.9756121039 0.9702054262 0.9736818075 - 0.9761241078 0.9707831144 0.9742587805 - 0.9766359925 0.9713609219 0.9748356938 - 0.9771479964 0.9719387293 0.9754127264 - 0.9776600003 0.9725164175 0.9759895802 - 0.9781718850 0.9730942249 0.9765666127 - 0.9786838889 0.9736719131 0.9771435857 - 0.9791958928 0.9742497206 0.9777204990 - 0.9797077775 0.9748274088 0.9782974720 - 0.9802197814 0.9754052162 0.9788743854 - 0.9807317853 0.9759829044 0.9794514179 - 0.9812437296 0.9765607119 0.9800282717 - 0.9817556739 0.9771384001 0.9806053042 - 0.9822546244 0.9777116776 0.9811592102 - 0.9827439189 0.9782817960 0.9816957712 - 0.9832330942 0.9788517952 0.9822325110 - 0.9837223291 0.9794217944 0.9827690721 - 0.9842115045 0.9799917936 0.9833058119 - 0.9847007990 0.9805617929 0.9838423729 - 0.9851899743 0.9811317921 0.9843789935 - 0.9856792092 0.9817017913 0.9849156737 - 0.9861685038 0.9822717905 0.9854522943 - 0.9866576791 0.9828417897 0.9859889746 - 0.9871469140 0.9834119081 0.9865255952 - 0.9876360893 0.9839819074 0.9870622754 - 0.9881253839 0.9845519066 0.9875988960 - 0.9886041284 0.9851186275 0.9881225228 - 0.9890480042 0.9856745005 0.9886025786 - 0.9894918799 0.9862303734 0.9890826941 - 0.9899358153 0.9867863059 0.9895628095 - 0.9903796911 0.9873421788 0.9900428057 - 0.9908236265 0.9878981113 0.9905229211 - 0.9912675023 0.9884539843 0.9910029769 - 0.9917113781 0.9890099168 0.9914829731 - 0.9921553135 0.9895657897 0.9919630885 - 0.9925991893 0.9901217222 0.9924432039 - 0.9930431247 0.9906775951 0.9929233193 - 0.9934868813 0.9912334085 0.9934033155 - 0.9939308167 0.9917892814 0.9938833714 - 0.9943746924 0.9923452139 0.9943634868 - 0.9947882295 0.9928944707 0.9947875738 - 0.9951890707 0.9934411049 0.9951885939 - 0.9955899715 0.9939876795 0.9955894947 - 0.9959908724 0.9945343137 0.9959905148 - 0.9963918924 0.9950808287 0.9963914156 - 0.9967927933 0.9956274033 0.9967923760 - 0.9971936941 0.9961739779 0.9971932769 - 0.9975945950 0.9967206120 0.9975942969 - 0.9979954958 0.9972671270 0.9979951978 - 0.9983963966 0.9978137016 0.9983962178 - 0.9987972975 0.9983602762 0.9987971187 - 0.9991981983 0.9989069104 0.9991980791 - 0.9995990992 0.9994534254 0.9995989799 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0086291339 0.0078005488 0.0061553409 + 0.0166279394 0.0149212098 0.0115477303 + 0.0234502498 0.0210635699 0.0162159093 + 0.0294977706 0.0264640693 0.0204165298 + 0.0349041112 0.0313926600 0.0242713001 + 0.0398604982 0.0359628201 0.0279128905 + 0.0445825197 0.0402758084 0.0313868709 + 0.0489878207 0.0443142913 0.0346947797 + 0.0532177314 0.0482137911 0.0378837399 + 0.0572355017 0.0519315116 0.0409737006 + 0.0610651597 0.0555156991 0.0439543799 + 0.0647805408 0.0590067990 0.0468649715 + 0.0684034899 0.0623898692 0.0496960990 + 0.0719185919 0.0656935796 0.0524532497 + 0.0753464922 0.0688895881 0.0551457889 + 0.0786892101 0.0719943792 0.0577957407 + 0.0819205269 0.0749978498 0.0603696108 + 0.0850563273 0.0779183879 0.0628900677 + 0.0881082267 0.0807836503 0.0653679967 + 0.0910997614 0.0836165026 0.0678165331 + 0.0940506533 0.0864003077 0.0702505037 + 0.0969373211 0.0891356766 0.0726604089 + 0.0997503623 0.0918305814 0.0750340968 + 0.1025146991 0.0944881588 0.0773660317 + 0.1052661017 0.0971041918 0.0796465725 + 0.1079823002 0.0997008383 0.0818920732 + 0.1106742024 0.1022529975 0.0840972289 + 0.1133475006 0.1047334000 0.0862670168 + 0.1159679964 0.1071522981 0.0884032026 + 0.1185553968 0.1095331982 0.0905269608 + 0.1210843995 0.1118893027 0.0926406905 + 0.1235641018 0.1142370030 0.0947492719 + 0.1260133982 0.1165516973 0.0968295410 + 0.1284331977 0.1188732013 0.0988966897 + 0.1308183968 0.1211699992 0.1009425968 + 0.1331772059 0.1234538034 0.1029729024 + 0.1355085969 0.1256996989 0.1049892977 + 0.1378096938 0.1279380023 0.1069732010 + 0.1400966942 0.1301369965 0.1089397967 + 0.1423414052 0.1323242038 0.1109104976 + 0.1445591003 0.1344671994 0.1128705963 + 0.1467431933 0.1365955025 0.1148163974 + 0.1489122063 0.1386837959 0.1167598963 + 0.1510476023 0.1407471001 0.1186747029 + 0.1531807929 0.1428090930 0.1205610037 + 0.1553075016 0.1448387057 0.1224154979 + 0.1574216038 0.1468652934 0.1242436990 + 0.1595198959 0.1488717943 0.1260575950 + 0.1615947932 0.1508519948 0.1278496981 + 0.1636703014 0.1528411955 0.1296253949 + 0.1657373011 0.1548041999 0.1313930005 + 0.1677850038 0.1567573994 0.1331461966 + 0.1698163003 0.1587090939 0.1349010020 + 0.1718430966 0.1606369019 0.1366534978 + 0.1738559008 0.1625535041 0.1383929998 + 0.1758597046 0.1644703001 0.1401329041 + 0.1778582931 0.1663694978 0.1418718994 + 0.1798356026 0.1682510972 0.1436001062 + 0.1817937940 0.1701292992 0.1453143060 + 0.1837290972 0.1719716042 0.1470246017 + 0.1856500059 0.1737944931 0.1487206072 + 0.1875600070 0.1756183952 0.1504009068 + 0.1894560009 0.1774290055 0.1520728022 + 0.1913280934 0.1792113930 0.1537348032 + 0.1931733042 0.1809861064 0.1554002017 + 0.1949937940 0.1827580035 0.1570577025 + 0.1968041956 0.1845048070 0.1587098986 + 0.1985995024 0.1862314939 0.1603520066 + 0.2003758997 0.1879574060 0.1619874984 + 0.2021435946 0.1896872967 0.1636175960 + 0.2039009035 0.1914020032 0.1652458012 + 0.2056314051 0.1931017041 0.1668601930 + 0.2073460072 0.1948022991 0.1684632003 + 0.2090656012 0.1965142041 0.1700568050 + 0.2107871026 0.1982153952 0.1716396064 + 0.2125017941 0.1999046952 0.1732196957 + 0.2142132074 0.2015919983 0.1747861952 + 0.2159194946 0.2032641023 0.1763374954 + 0.2176184952 0.2049120069 0.1778807938 + 0.2193064988 0.2065432072 0.1794188023 + 0.2209898978 0.2081688046 0.1809532940 + 0.2226741016 0.2097914070 0.1824879944 + 0.2243521959 0.2114066035 0.1840219945 + 0.2260199040 0.2130115032 0.1855459064 + 0.2276818007 0.2146048993 0.1870635003 + 0.2293322980 0.2161996961 0.1885785013 + 0.2309723049 0.2177951932 0.1900880039 + 0.2325980961 0.2193817049 0.1915943027 + 0.2342166007 0.2209558040 0.1930999011 + 0.2358343005 0.2225205004 0.1946053058 + 0.2374477983 0.2240840048 0.1961077005 + 0.2390588969 0.2256534994 0.1975979954 + 0.2406685948 0.2272066027 0.1990832984 + 0.2422688007 0.2287465930 0.2005632073 + 0.2438556999 0.2302782983 0.2020380944 + 0.2454296947 0.2318089008 0.2035095990 + 0.2469906062 0.2333444953 0.2049811035 + 0.2485449016 0.2348676026 0.2064442933 + 0.2500948012 0.2363775969 0.2079003006 + 0.2516424954 0.2378765941 0.2093524039 + 0.2531889081 0.2393742949 0.2107995003 + 0.2547358871 0.2408725023 0.2122382969 + 0.2562828064 0.2423699051 0.2136691064 + 0.2578260899 0.2438611984 0.2150931954 + 0.2593621910 0.2453383952 0.2165144980 + 0.2608925104 0.2468091995 0.2179322988 + 0.2624158859 0.2482766062 0.2193447053 + 0.2639254928 0.2497424036 0.2207508981 + 0.2654292881 0.2512080073 0.2221533954 + 0.2669275999 0.2526736856 0.2235517055 + 0.2684175968 0.2541224957 0.2249393016 + 0.2699044943 0.2555668056 0.2263229042 + 0.2713881135 0.2570067048 0.2277022004 + 0.2728621960 0.2584454119 0.2290780991 + 0.2743291855 0.2598778009 0.2304508984 + 0.2757858038 0.2613000870 0.2318188995 + 0.2772283852 0.2627094984 0.2331835032 + 0.2786627114 0.2641144991 0.2345474958 + 0.2800801098 0.2655147016 0.2359123975 + 0.2814942896 0.2669171095 0.2372698933 + 0.2829064131 0.2683207989 0.2386226058 + 0.2843183875 0.2697081864 0.2399670929 + 0.2857283056 0.2710885108 0.2413100004 + 0.2871338129 0.2724551857 0.2426497042 + 0.2885296047 0.2738170922 0.2439855933 + 0.2899203002 0.2751764059 0.2453195006 + 0.2913027108 0.2765411139 0.2466553003 + 0.2926810980 0.2779048085 0.2479909062 + 0.2940480113 0.2792651057 0.2493253052 + 0.2954030931 0.2806189060 0.2506527901 + 0.2967463136 0.2819663882 0.2519730926 + 0.2980836928 0.2833054066 0.2532927990 + 0.2994191051 0.2846412957 0.2546122074 + 0.3007619083 0.2859781981 0.2559292912 + 0.3021047115 0.2873162031 0.2572456896 + 0.3034440875 0.2886605859 0.2585586011 + 0.3047791123 0.2899999022 0.2598730028 + 0.3061031103 0.2913272977 0.2611911893 + 0.3074271083 0.2926500142 0.2625049949 + 0.3087511957 0.2939665914 0.2638128996 + 0.3100728095 0.2952839136 0.2651130855 + 0.3113921881 0.2966020107 0.2664063871 + 0.3127081990 0.2979249954 0.2676930130 + 0.3140223026 0.2992509007 0.2689754069 + 0.3153294921 0.3005655110 0.2702546120 + 0.3166336119 0.3018752038 0.2715322971 + 0.3179413080 0.3031772077 0.2728073001 + 0.3192504048 0.3044762909 0.2740811110 + 0.3205552101 0.3057742119 0.2753461003 + 0.3218581975 0.3070715070 0.2766075134 + 0.3231492043 0.3083544075 0.2778604031 + 0.3244349062 0.3096314073 0.2791095972 + 0.3257190883 0.3108929992 0.2803491056 + 0.3270024955 0.3121466935 0.2815833986 + 0.3282819092 0.3133935034 0.2828159034 + 0.3295584023 0.3146354854 0.2840468884 + 0.3308312893 0.3158772886 0.2852747142 + 0.3321002126 0.3171189129 0.2864989042 + 0.3333649933 0.3183605075 0.2877179980 + 0.3346219063 0.3196018934 0.2889271975 + 0.3358792067 0.3208408952 0.2901360095 + 0.3371385932 0.3220691979 0.2913432121 + 0.3383980095 0.3232975006 0.2925504148 + 0.3396492004 0.3245128095 0.2937470973 + 0.3409003019 0.3257280886 0.2949436903 + 0.3421368003 0.3269419074 0.2961353958 + 0.3433690071 0.3281553984 0.2973257005 + 0.3445987105 0.3293735981 0.2985163927 + 0.3458260894 0.3305962980 0.2997077107 + 0.3470515013 0.3318206072 0.3008984029 + 0.3482699096 0.3330497146 0.3020871878 + 0.3494882882 0.3342789114 0.3032760024 + 0.3507004976 0.3354977071 0.3044616878 + 0.3519122005 0.3367156088 0.3056469858 + 0.3531202972 0.3379318118 0.3068349957 + 0.3543255925 0.3391467035 0.3080250025 + 0.3555307984 0.3403621018 0.3092128932 + 0.3567352891 0.3415792882 0.3103924990 + 0.3579396904 0.3427965045 0.3115719855 + 0.3591400981 0.3439978957 0.3127532005 + 0.3603394926 0.3451952934 0.3139348030 + 0.3615379930 0.3463875949 0.3151153028 + 0.3627347052 0.3475709856 0.3162938058 + 0.3639313877 0.3487544060 0.3174723089 + 0.3651260138 0.3499352038 0.3186487854 + 0.3663201928 0.3511157036 0.3198249936 + 0.3675152957 0.3522970974 0.3210014999 + 0.3687118888 0.3534801006 0.3221786916 + 0.3699085116 0.3546631038 0.3233557940 + 0.3711037934 0.3558301926 0.3245329857 + 0.3722988069 0.3569942117 0.3257102966 + 0.3734931946 0.3581548035 0.3268885911 + 0.3746860027 0.3593055010 0.3280701935 + 0.3758786917 0.3604561985 0.3292517960 + 0.3770669103 0.3616054058 0.3304269016 + 0.3782525063 0.3627535999 0.3315981925 + 0.3794381022 0.3639019132 0.3327696025 + 0.3806130886 0.3650487065 0.3339313865 + 0.3817881048 0.3661954105 0.3350929916 + 0.3829604089 0.3673394918 0.3362526000 + 0.3841269910 0.3684779108 0.3374074101 + 0.3852936029 0.3696162999 0.3385623097 + 0.3864552975 0.3707447052 0.3397139013 + 0.3876137137 0.3718662858 0.3408631980 + 0.3887721896 0.3729878962 0.3420126140 + 0.3899284899 0.3741044998 0.3431645036 + 0.3910844922 0.3752200007 0.3443169892 + 0.3922401071 0.3763357997 0.3454687893 + 0.3933893144 0.3774558902 0.3466030955 + 0.3945386112 0.3785760105 0.3477374911 + 0.3956862092 0.3796963990 0.3488697112 + 0.3968279064 0.3808178902 0.3499937057 + 0.3979696035 0.3819392920 0.3511176109 + 0.3991076052 0.3830555975 0.3522385061 + 0.4002385139 0.3841623068 0.3533534110 + 0.4013693929 0.3852689862 0.3544682860 + 0.4024933875 0.3863726854 0.3555805981 + 0.4036085010 0.3874725103 0.3566895127 + 0.4047234952 0.3885723054 0.3577983975 + 0.4058330059 0.3896723092 0.3589071929 + 0.4069365859 0.3907727003 0.3600156903 + 0.4080401957 0.3918730021 0.3611243069 + 0.4091409147 0.3929738104 0.3622342050 + 0.4102385938 0.3940750062 0.3633455932 + 0.4113362134 0.3951762021 0.3644570112 + 0.4124296904 0.3962723017 0.3655647933 + 0.4135183096 0.3973628879 0.3666686118 + 0.4146068990 0.3984535933 0.3677723110 + 0.4156936109 0.3995419145 0.3688744903 + 0.4167774022 0.4006268978 0.3699742854 + 0.4178611040 0.4017120004 0.3710741103 + 0.4189420044 0.4027974904 0.3721716106 + 0.4200159013 0.4038844109 0.3732632101 + 0.4210897088 0.4049712121 0.3743548989 + 0.4221613109 0.4060561061 0.3754456043 + 0.4232189953 0.4071291983 0.3765301108 + 0.4242765903 0.4082024097 0.3776144981 + 0.4253341854 0.4092755914 0.3786988854 + 0.4263879955 0.4103333056 0.3797815144 + 0.4274415076 0.4113903046 0.3808639050 + 0.4284951091 0.4124472141 0.3819462955 + 0.4295487106 0.4134993851 0.3830277026 + 0.4306023121 0.4145495892 0.3841085136 + 0.4316559136 0.4155999124 0.3851892948 + 0.4327076077 0.4166513085 0.3862665892 + 0.4337567091 0.4177044034 0.3873386979 + 0.4348058105 0.4187574983 0.3884108961 + 0.4358538985 0.4198100865 0.3894824088 + 0.4368919134 0.4208576977 0.3905462921 + 0.4379299879 0.4219053984 0.3916102946 + 0.4389680028 0.4229530990 0.3926742971 + 0.4400067031 0.4239928126 0.3937354982 + 0.4410456121 0.4250294864 0.3947955072 + 0.4420844913 0.4260661900 0.3958554864 + 0.4431211054 0.4271000028 0.3969140053 + 0.4441522956 0.4281272888 0.3979691863 + 0.4451833963 0.4291546047 0.3990243077 + 0.4462144971 0.4301818907 0.4000793993 + 0.4472317100 0.4312092960 0.4011329114 + 0.4482460916 0.4322367013 0.4021860957 + 0.4492605031 0.4332641959 0.4032391906 + 0.4502733946 0.4342932105 0.4042918980 + 0.4512828887 0.4353255928 0.4053437114 + 0.4522925019 0.4363580048 0.4063954949 + 0.4533020854 0.4373905063 0.4074473083 + 0.4543111026 0.4384124875 0.4084874094 + 0.4553197920 0.4394313991 0.4095237851 + 0.4563286006 0.4404503107 0.4105601907 + 0.4573364854 0.4414677918 0.4115959108 + 0.4583398104 0.4424777925 0.4126274884 + 0.4593431056 0.4434877038 0.4136590958 + 0.4603464007 0.4444975853 0.4146907032 + 0.4613440037 0.4455075860 0.4157181084 + 0.4623357058 0.4465177059 0.4167410135 + 0.4633274078 0.4475277066 0.4177640080 + 0.4643191993 0.4485377967 0.4187869132 + 0.4653109908 0.4495514035 0.4198037088 + 0.4663026929 0.4505659938 0.4208188057 + 0.4672945142 0.4515804946 0.4218339026 + 0.4682861865 0.4525944889 0.4228489995 + 0.4692744911 0.4535953999 0.4238614142 + 0.4702627957 0.4545961916 0.4248737097 + 0.4712511003 0.4555971026 0.4258860946 + 0.4722385108 0.4565958977 0.4268980026 + 0.4732227027 0.4575876892 0.4279080927 + 0.4742070138 0.4585795999 0.4289183021 + 0.4751912951 0.4595713913 0.4299283922 + 0.4761728942 0.4605633020 0.4309360981 + 0.4771496952 0.4615552127 0.4319396913 + 0.4781264961 0.4625470936 0.4329431951 + 0.4791032970 0.4635390043 0.4339466989 + 0.4800817966 0.4645307064 0.4349519014 + 0.4810624123 0.4655222893 0.4359593987 + 0.4820429981 0.4665139019 0.4369668067 + 0.4830236137 0.4675053954 0.4379743040 + 0.4840019941 0.4684920907 0.4389804900 + 0.4849779904 0.4694738090 0.4399853051 + 0.4859539866 0.4704554975 0.4409902096 + 0.4869301021 0.4714370966 0.4419949949 + 0.4879001081 0.4724138081 0.4429973066 + 0.4888631999 0.4733850062 0.4439963996 + 0.4898262918 0.4743562043 0.4449956119 + 0.4907892942 0.4753274024 0.4459947944 + 0.4917517006 0.4762980044 0.4469918907 + 0.4927130938 0.4772675931 0.4479857981 + 0.4936743975 0.4782372117 0.4489797056 + 0.4946357012 0.4792068005 0.4499736130 + 0.4955956042 0.4801762998 0.4509660006 + 0.4965516925 0.4811457098 0.4519538879 + 0.4975079000 0.4821150899 0.4529418051 + 0.4984639883 0.4830844998 0.4539296925 + 0.4994196892 0.4840534031 0.4549174011 + 0.5003700852 0.4850172102 0.4559034109 + 0.5013206005 0.4859808981 0.4568893015 + 0.5022711158 0.4869445860 0.4578751922 + 0.5032215118 0.4879083037 0.4588611126 + 0.5041645169 0.4888612032 0.4598459005 + 0.5051059127 0.4898119867 0.4608305097 + 0.5060474277 0.4907627106 0.4618149996 + 0.5069888234 0.4917134941 0.4627996087 + 0.5079312921 0.4926620126 0.4637798965 + 0.5088745952 0.4936085045 0.4647563100 + 0.5098178983 0.4945549965 0.4657326937 + 0.5107612014 0.4955014884 0.4667089880 + 0.5117039084 0.4964486957 0.4676840901 + 0.5126433969 0.4973995090 0.4686524868 + 0.5135828853 0.4983502924 0.4696210027 + 0.5145223737 0.4993009865 0.4705893993 + 0.5154619217 0.5002518296 0.4715577960 + 0.5163952708 0.5012043118 0.4725238979 + 0.5173267126 0.5021573901 0.4734891057 + 0.5182582140 0.5031105280 0.4744544029 + 0.5191895962 0.5040636063 0.4754197001 + 0.5201197863 0.5050135255 0.4763843119 + 0.5210466981 0.5059555173 0.4773474038 + 0.5219736099 0.5068973899 0.4783104062 + 0.5229005218 0.5078393817 0.4792734981 + 0.5238274932 0.5087813735 0.4802365899 + 0.5247486830 0.5097197294 0.4811953008 + 0.5256680846 0.5106567144 0.4821526110 + 0.5265874267 0.5115938187 0.4831098914 + 0.5275068283 0.5125308037 0.4840672016 + 0.5284252763 0.5134680271 0.4850249887 + 0.5293390155 0.5144063234 0.4859859049 + 0.5302528143 0.5153445005 0.4869467914 + 0.5311664939 0.5162826777 0.4879077077 + 0.5320801735 0.5172209740 0.4888687134 + 0.5329914093 0.5181534290 0.4898253977 + 0.5338997245 0.5190796256 0.4907774925 + 0.5348079801 0.5200058222 0.4917295873 + 0.5357164145 0.5209320784 0.4926817119 + 0.5366247296 0.5218582749 0.4936338067 + 0.5375316739 0.5227739215 0.4945797026 + 0.5384383202 0.5236862898 0.4955236018 + 0.5393447876 0.5245985985 0.4964675009 + 0.5402513742 0.5255110264 0.4974114001 + 0.5411579013 0.5264232755 0.4983552992 + 0.5420578122 0.5273302197 0.4992960095 + 0.5429574251 0.5282369256 0.5002363920 + 0.5438570976 0.5291436911 0.5011768937 + 0.5447567105 0.5300503969 0.5021173954 + 0.5456550717 0.5309572220 0.5030577779 + 0.5465437174 0.5318645835 0.5039975047 + 0.5474323034 0.5327720046 0.5049371123 + 0.5483208895 0.5336794257 0.5058767796 + 0.5492094755 0.5345867872 0.5068165064 + 0.5500965118 0.5354917049 0.5077555180 + 0.5509771109 0.5363870263 0.5086914897 + 0.5518578291 0.5372822881 0.5096275210 + 0.5527384281 0.5381776094 0.5105633736 + 0.5536190271 0.5390728712 0.5114994049 + 0.5545002222 0.5399649739 0.5124346018 + 0.5553833246 0.5408456922 0.5133668780 + 0.5562664866 0.5417262912 0.5142992139 + 0.5571495891 0.5426068902 0.5152313709 + 0.5580328107 0.5434876084 0.5161637068 + 0.5589159131 0.5443671942 0.5170946121 + 0.5597988963 0.5452414751 0.5180175900 + 0.5606818795 0.5461158156 0.5189406872 + 0.5615649223 0.5469899774 0.5198637843 + 0.5624479055 0.5478643179 0.5207868218 + 0.5633308887 0.5487385988 0.5217097998 + 0.5642132163 0.5496147871 0.5226305127 + 0.5650954247 0.5504909158 0.5235512257 + 0.5659775734 0.5513671041 0.5244718790 + 0.5668599010 0.5522431731 0.5253925920 + 0.5677421093 0.5531194210 0.5263133049 + 0.5686208010 0.5539948940 0.5272328854 + 0.5694987178 0.5548701882 0.5281524062 + 0.5703766942 0.5557454824 0.5290718079 + 0.5712546110 0.5566208959 0.5299912095 + 0.5721325874 0.5574961901 0.5309106112 + 0.5730063915 0.5583639145 0.5318285227 + 0.5738772154 0.5592259765 0.5327451229 + 0.5747479200 0.5600880980 0.5336617231 + 0.5756186843 0.5609502196 0.5345783234 + 0.5764895082 0.5618122816 0.5354949236 + 0.5773584247 0.5626726747 0.5364094973 + 0.5782213807 0.5635278821 0.5373175144 + 0.5790842772 0.5643829703 0.5382254124 + 0.5799472928 0.5652381182 0.5391334295 + 0.5808103085 0.5660933256 0.5400413871 + 0.5816733241 0.5669484138 0.5409492850 + 0.5825272799 0.5678043962 0.5418502092 + 0.5833795071 0.5686606169 0.5427495241 + 0.5842316747 0.5695167184 0.5436487794 + 0.5850839019 0.5703728795 0.5445482135 + 0.5859361291 0.5712291002 0.5454475284 + 0.5867881775 0.5720869899 0.5463449955 + 0.5876401067 0.5729479790 0.5472388864 + 0.5884919167 0.5738090873 0.5481327772 + 0.5893437862 0.5746700764 0.5490267277 + 0.5901955962 0.5755311847 0.5499206781 + 0.5910475254 0.5763922930 0.5508146286 + 0.5918961763 0.5772445202 0.5517038107 + 0.5927441716 0.5780944824 0.5525919199 + 0.5935921073 0.5789445043 0.5534799099 + 0.5944401026 0.5797945857 0.5543678999 + 0.5952879786 0.5806446075 0.5552558899 + 0.5961351991 0.5814933777 0.5561434031 + 0.5969787836 0.5823364854 0.5570282936 + 0.5978223085 0.5831795931 0.5579131246 + 0.5986657739 0.5840227008 0.5587978959 + 0.5995092988 0.5848656893 0.5596827865 + 0.6003528833 0.5857087970 0.5605676174 + 0.6011931896 0.5865523219 0.5614522099 + 0.6020298004 0.5873963237 0.5623363853 + 0.6028664112 0.5882403255 0.5632206202 + 0.6037030220 0.5890843272 0.5641047955 + 0.6045395732 0.5899283290 0.5649890900 + 0.6053761840 0.5907722712 0.5658733249 + 0.6062102914 0.5916194916 0.5667579174 + 0.6070430875 0.5924683213 0.5676426291 + 0.6078760028 0.5933170915 0.5685272813 + 0.6087087989 0.5941658020 0.5694121122 + 0.6095417142 0.5950145721 0.5702968240 + 0.6103746295 0.5958634019 0.5711815953 + 0.6112043858 0.5967034101 0.5720716119 + 0.6120336056 0.5975413918 0.5729628205 + 0.6128627062 0.5983793736 0.5738540292 + 0.6136919260 0.5992174149 0.5747451782 + 0.6145210862 0.6000553966 0.5756365061 + 0.6153501868 0.6008933783 0.5765277147 + 0.6161777973 0.6017255187 0.5774099231 + 0.6170051098 0.6025568247 0.5782908201 + 0.6178324223 0.6033880115 0.5791717172 + 0.6186597943 0.6042193174 0.5800524950 + 0.6194871068 0.6050505042 0.5809333920 + 0.6203144193 0.6058818102 0.5818142295 + 0.6211380959 0.6067138910 0.5826920271 + 0.6219611764 0.6075462103 0.5835691094 + 0.6227841973 0.6083784103 0.5844463110 + 0.6236072779 0.6092107296 0.5853233933 + 0.6244302988 0.6100429893 0.5862005949 + 0.6252533793 0.6108751893 0.5870776772 + 0.6260753274 0.6117079258 0.5879529119 + 0.6268969178 0.6125406027 0.5888274908 + 0.6277183890 0.6133733988 0.5897021294 + 0.6285399199 0.6142061949 0.5905765891 + 0.6293615103 0.6150388718 0.5914512277 + 0.6301829815 0.6158717275 0.5923258066 + 0.6310017109 0.6166999936 0.5932002068 + 0.6318179965 0.6175248027 0.5940744281 + 0.6326344013 0.6183496118 0.5949487090 + 0.6334508061 0.6191744208 0.5958228707 + 0.6342670918 0.6199991703 0.5966970921 + 0.6350834966 0.6208239794 0.5975713730 + 0.6358969808 0.6216465235 0.5984457135 + 0.6367033720 0.6224632263 0.5993201137 + 0.6375098228 0.6232798100 0.6001945138 + 0.6383162141 0.6240965128 0.6010689735 + 0.6391226053 0.6249132156 0.6019433737 + 0.6399289966 0.6257297993 0.6028177738 + 0.6407353878 0.6265465021 0.6036921740 + 0.6415356994 0.6273627281 0.6045618057 + 0.6423354745 0.6281787753 0.6054310799 + 0.6431353092 0.6289948821 0.6063002944 + 0.6439350843 0.6298109889 0.6071695089 + 0.6447349191 0.6306270957 0.6080387235 + 0.6455346942 0.6314432025 0.6089078784 + 0.6463314891 0.6322597861 0.6097748876 + 0.6471250057 0.6330770850 0.6106392741 + 0.6479185820 0.6338943243 0.6115037799 + 0.6487122178 0.6347116232 0.6123682261 + 0.6495056748 0.6355288029 0.6132326722 + 0.6502993107 0.6363461018 0.6140971184 + 0.6510928869 0.6371632814 0.6149616241 + 0.6518819928 0.6379770041 0.6158189178 + 0.6526708007 0.6387906075 0.6166757941 + 0.6534597278 0.6396042109 0.6175327897 + 0.6542485952 0.6404178143 0.6183897257 + 0.6550374031 0.6412314177 0.6192467213 + 0.6558262706 0.6420449018 0.6201035976 + 0.6566126943 0.6428549886 0.6209588051 + 0.6573941112 0.6436582804 0.6218106151 + 0.6581755280 0.6444615722 0.6226623058 + 0.6589568853 0.6452649236 0.6235139966 + 0.6597383022 0.6460682154 0.6243656874 + 0.6605197191 0.6468715072 0.6252173781 + 0.6613010168 0.6476747990 0.6260691881 + 0.6620833874 0.6484764218 0.6269180179 + 0.6628664136 0.6492767930 0.6277649999 + 0.6636493206 0.6500772834 0.6286119819 + 0.6644322872 0.6508777738 0.6294590831 + 0.6652151942 0.6516783237 0.6303061247 + 0.6659982204 0.6524788141 0.6311531067 + 0.6667811275 0.6532791853 0.6320002079 + 0.6675598025 0.6540803909 0.6328403950 + 0.6683375835 0.6548817754 0.6336792111 + 0.6691154242 0.6556832194 0.6345179081 + 0.6698932052 0.6564844847 0.6353567243 + 0.6706709862 0.6572859287 0.6361954212 + 0.6714488268 0.6580871940 0.6370341778 + 0.6722266078 0.6588885784 0.6378728747 + 0.6729933023 0.6596770287 0.6387121081 + 0.6737592816 0.6604645252 0.6395512223 + 0.6745253205 0.6612520814 0.6403903961 + 0.6752912998 0.6620395780 0.6412295103 + 0.6760572791 0.6628271937 0.6420686841 + 0.6768233180 0.6636146903 0.6429077983 + 0.6775892973 0.6644023061 0.6437469721 + 0.6783525944 0.6651803851 0.6445764899 + 0.6791155934 0.6659579873 0.6454054117 + 0.6798787117 0.6667355895 0.6462342739 + 0.6806417704 0.6675131917 0.6470631957 + 0.6814048886 0.6682907939 0.6478921175 + 0.6821680069 0.6690683961 0.6487209797 + 0.6829311252 0.6698459983 0.6495499015 + 0.6836892962 0.6706212759 0.6503704190 + 0.6844468117 0.6713961959 0.6511895061 + 0.6852043271 0.6721711755 0.6520085931 + 0.6859617829 0.6729462147 0.6528277993 + 0.6867192984 0.6737210751 0.6536468863 + 0.6874766946 0.6744961143 0.6544659734 + 0.6882342100 0.6752710938 0.6552851796 + 0.6889876127 0.6760479808 0.6560990810 + 0.6897391081 0.6768258810 0.6569107175 + 0.6904906034 0.6776037216 0.6577221751 + 0.6912420988 0.6783815026 0.6585338116 + 0.6919935942 0.6791594028 0.6593453884 + 0.6927450895 0.6799371839 0.6601569057 + 0.6934965849 0.6807150841 0.6609684825 + 0.6942445040 0.6814894080 0.6617767215 + 0.6949872971 0.6822587848 0.6625800729 + 0.6957302094 0.6830282211 0.6633834243 + 0.6964730024 0.6837977171 0.6641867757 + 0.6972159147 0.6845670938 0.6649901867 + 0.6979587078 0.6853365898 0.6657935977 + 0.6987016201 0.6861060262 0.6665970087 + 0.6994444132 0.6868749261 0.6674001813 + 0.7001873255 0.6876338124 0.6682003736 + 0.7009301782 0.6883925796 0.6690005064 + 0.7016730905 0.6891514063 0.6698006988 + 0.7024160028 0.6899102926 0.6706007719 + 0.7031589150 0.6906691194 0.6714010239 + 0.7039018273 0.6914280057 0.6722010970 + 0.7046446800 0.6921867728 0.6730012894 + 0.7053861022 0.6929436922 0.6738013029 + 0.7061262727 0.6936991215 0.6746010780 + 0.7068666220 0.6944546103 0.6754009128 + 0.7076067924 0.6952099800 0.6762006879 + 0.7083470821 0.6959655285 0.6770005226 + 0.7090873718 0.6967208982 0.6778002977 + 0.7098276019 0.6974763870 0.6786001921 + 0.7105678916 0.6982318759 0.6794000268 + 0.7113052011 0.6989892721 0.6801937222 + 0.7120423913 0.6997467875 0.6809874177 + 0.7127797008 0.7005043030 0.6817811728 + 0.7135170102 0.7012618184 0.6825749278 + 0.7142542005 0.7020192742 0.6833686233 + 0.7149915099 0.7027767897 0.6841623187 + 0.7157288194 0.7035343051 0.6849560142 + 0.7164642215 0.7042888999 0.6857504845 + 0.7171959281 0.7050374746 0.6865466833 + 0.7179275155 0.7057859898 0.6873428822 + 0.7186592221 0.7065346241 0.6881390810 + 0.7193908095 0.7072831988 0.6889352798 + 0.7201225162 0.7080317736 0.6897314191 + 0.7208541036 0.7087804079 0.6905276179 + 0.7215858102 0.7095289826 0.6913238168 + 0.7223190069 0.7102715969 0.6921185255 + 0.7230535150 0.7110090256 0.6929119229 + 0.7237880230 0.7117465138 0.6937052011 + 0.7245225906 0.7124840021 0.6944985986 + 0.7252570987 0.7132214904 0.6952919960 + 0.7259916067 0.7139589787 0.6960853934 + 0.7267261147 0.7146965265 0.6968787909 + 0.7274606228 0.7154340148 0.6976721883 + 0.7281941175 0.7161688209 0.6984621882 + 0.7289270163 0.7169021964 0.6992504001 + 0.7296599150 0.7176355720 0.7000386715 + 0.7303928137 0.7183690071 0.7008268833 + 0.7311257720 0.7191023827 0.7016150951 + 0.7318586707 0.7198358178 0.7024034262 + 0.7325916290 0.7205691934 0.7031915784 + 0.7333245277 0.7213026285 0.7039797902 + 0.7340542078 0.7220379114 0.7047694921 + 0.7347822189 0.7227740884 0.7055600286 + 0.7355102897 0.7235103846 0.7063505054 + 0.7362383008 0.7242466211 0.7071409822 + 0.7369663715 0.7249829173 0.7079313993 + 0.7376943827 0.7257190943 0.7087218761 + 0.7384225130 0.7264553905 0.7095124125 + 0.7391505241 0.7271916270 0.7103028893 + 0.7398774028 0.7279284000 0.7110937238 + 0.7406033874 0.7286654115 0.7118847966 + 0.7413293719 0.7294024825 0.7126759291 + 0.7420554161 0.7301396132 0.7134671211 + 0.7427814007 0.7308766842 0.7142581940 + 0.7435073853 0.7316136956 0.7150493264 + 0.7442334294 0.7323508263 0.7158403993 + 0.7449594140 0.7330878973 0.7166314721 + 0.7456837893 0.7338215709 0.7174229026 + 0.7464053035 0.7345489264 0.7182146907 + 0.7471268177 0.7352762222 0.7190064788 + 0.7478482127 0.7360035181 0.7197983265 + 0.7485697269 0.7367308140 0.7205901146 + 0.7492911816 0.7374581099 0.7213820219 + 0.7500126958 0.7381855249 0.7221738100 + 0.7507342100 0.7389128208 0.7229655981 + 0.7514556050 0.7396399975 0.7237573266 + 0.7521721721 0.7403625846 0.7245445251 + 0.7528889179 0.7410852909 0.7253317833 + 0.7536056042 0.7418079972 0.7261189818 + 0.7543221712 0.7425305843 0.7269061804 + 0.7550389171 0.7432532907 0.7276934981 + 0.7557556033 0.7439759970 0.7284806967 + 0.7564721704 0.7446985841 0.7292678952 + 0.7571889162 0.7454212904 0.7300552130 + 0.7579010725 0.7461451888 0.7308400273 + 0.7586101890 0.7468699813 0.7316231132 + 0.7593191862 0.7475947738 0.7324061990 + 0.7600283027 0.7483196259 0.7331892848 + 0.7607374191 0.7490442991 0.7339723706 + 0.7614464164 0.7497690916 0.7347555161 + 0.7621554732 0.7504938841 0.7355386019 + 0.7628644705 0.7512186766 0.7363216877 + 0.7635735869 0.7519437075 0.7371047735 + 0.7642832994 0.7526741028 0.7378858924 + 0.7649930120 0.7534046173 0.7386670709 + 0.7657027245 0.7541350126 0.7394481897 + 0.7664124966 0.7548654079 0.7402294278 + 0.7671222091 0.7555958033 0.7410104871 + 0.7678319216 0.7563263178 0.7417917252 + 0.7685415745 0.7570567131 0.7425727844 + 0.7692512870 0.7577871084 0.7433540225 + 0.7699596882 0.7585147023 0.7441341877 + 0.7706660032 0.7592375278 0.7449126244 + 0.7713723183 0.7599601746 0.7456911206 + 0.7720785737 0.7606830001 0.7464696169 + 0.7727848887 0.7614058256 0.7472481132 + 0.7734912038 0.7621284723 0.7480264902 + 0.7741973996 0.7628512979 0.7488049865 + 0.7749037147 0.7635740042 0.7495834827 + 0.7756100297 0.7642968297 0.7503619790 + 0.7763124108 0.7650169730 0.7511404753 + 0.7770121098 0.7657353282 0.7519189715 + 0.7777118087 0.7664536238 0.7526975274 + 0.7784113884 0.7671719790 0.7534760833 + 0.7791110873 0.7678902745 0.7542545795 + 0.7798107266 0.7686086297 0.7550330758 + 0.7805104256 0.7693269849 0.7558116913 + 0.7812101245 0.7700452805 0.7565901875 + 0.7819097042 0.7707635760 0.7573688030 + 0.7826066017 0.7714834213 0.7581411004 + 0.7833021283 0.7722039223 0.7589104176 + 0.7839977145 0.7729244232 0.7596796751 + 0.7846931815 0.7736448050 0.7604489923 + 0.7853888273 0.7743653059 0.7612183094 + 0.7860842943 0.7750858068 0.7619876266 + 0.7867798805 0.7758061886 0.7627568841 + 0.7874754071 0.7765266895 0.7635262012 + 0.7881708741 0.7772471905 0.7642955184 + 0.7888624072 0.7779653072 0.7650622725 + 0.7895516157 0.7786822915 0.7658277750 + 0.7902408838 0.7793992162 0.7665932775 + 0.7909300923 0.7801160812 0.7673587799 + 0.7916194201 0.7808331251 0.7681242824 + 0.7923086286 0.7815499902 0.7688897848 + 0.7929978967 0.7822669148 0.7696552873 + 0.7936871052 0.7829838991 0.7704207897 + 0.7943763733 0.7837008238 0.7711862922 + 0.7950640917 0.7844138741 0.7719494104 + 0.7957503200 0.7851232290 0.7727103233 + 0.7964366078 0.7858325243 0.7734711170 + 0.7971227765 0.7865418196 0.7742319703 + 0.7978091240 0.7872511744 0.7749928236 + 0.7984952927 0.7879605293 0.7757536769 + 0.7991815209 0.7886698246 0.7765144706 + 0.7998678088 0.7893791199 0.7772753835 + 0.8005539775 0.7900884151 0.7780361772 + 0.8012402058 0.7907962799 0.7787957788 + 0.8019264936 0.7914993763 0.7795513272 + 0.8026127219 0.7922024131 0.7803066969 + 0.8032988906 0.7929055095 0.7810621262 + 0.8039851189 0.7936086059 0.7818176150 + 0.8046712875 0.7943117023 0.7825729847 + 0.8053575754 0.7950147986 0.7833284140 + 0.8060438037 0.7957177758 0.7840839028 + 0.8067299724 0.7964208722 0.7848392725 + 0.8074162006 0.7971240282 0.7855947018 + 0.8081002235 0.7978271842 0.7863516212 + 0.8087838292 0.7985303998 0.7871088982 + 0.8094673753 0.7992336154 0.7878661156 + 0.8101509809 0.7999367714 0.7886233926 + 0.8108345866 0.8006399870 0.7893806100 + 0.8115181923 0.8013432026 0.7901378274 + 0.8122016788 0.8020464778 0.7908951044 + 0.8128852844 0.8027496934 0.7916523218 + 0.8135688901 0.8034529090 0.7924095988 + 0.8142508864 0.8041560054 0.7931652069 + 0.8149294853 0.8048589826 0.7939171791 + 0.8156080842 0.8055620790 0.7946692705 + 0.8162866831 0.8062651157 0.7954213023 + 0.8169652820 0.8069680929 0.7961732745 + 0.8176438808 0.8076711297 0.7969254255 + 0.8183224797 0.8083741069 0.7976773977 + 0.8190010190 0.8090770841 0.7984294295 + 0.8196796179 0.8097801208 0.7991815209 + 0.8203582168 0.8104832172 0.7999334931 + 0.8210343719 0.8111814260 0.8006806970 + 0.8217092156 0.8118773103 0.8014255166 + 0.8223841190 0.8125730753 0.8021702170 + 0.8230590224 0.8132688999 0.8029149175 + 0.8237338066 0.8139647245 0.8036596775 + 0.8244087100 0.8146606088 0.8044043779 + 0.8250836134 0.8153563738 0.8051490784 + 0.8257583976 0.8160521984 0.8058938980 + 0.8264333010 0.8167480230 0.8066385984 + 0.8271082044 0.8174437881 0.8073834181 + 0.8277801871 0.8181331158 0.8081256151 + 0.8284516931 0.8188214898 0.8088673949 + 0.8291233182 0.8195099235 0.8096091747 + 0.8297948837 0.8201981783 0.8103510141 + 0.8304665089 0.8208866119 0.8110929132 + 0.8311380148 0.8215749860 0.8118346930 + 0.8318095803 0.8222633004 0.8125764728 + 0.8324812055 0.8229516745 0.8133183122 + 0.8331527710 0.8236399889 0.8140602112 + 0.8338242769 0.8243284225 0.8148019910 + 0.8344954848 0.8250157833 0.8155404925 + 0.8351665735 0.8257030845 0.8162788153 + 0.8358377218 0.8263903856 0.8170171976 + 0.8365089297 0.8270778060 0.8177555203 + 0.8371800184 0.8277651072 0.8184939027 + 0.8378511071 0.8284524083 0.8192322254 + 0.8385221958 0.8291397095 0.8199704885 + 0.8391934037 0.8298270702 0.8207088709 + 0.8398644924 0.8305143714 0.8214471936 + 0.8405355811 0.8312016726 0.8221855760 + 0.8412041068 0.8318877816 0.8229212165 + 0.8418722749 0.8325737119 0.8236564994 + 0.8425405025 0.8332597017 0.8243917823 + 0.8432086706 0.8339456916 0.8251271248 + 0.8438770175 0.8346316218 0.8258624077 + 0.8445451856 0.8353176117 0.8265976906 + 0.8452134132 0.8360034823 0.8273329735 + 0.8458815813 0.8366894722 0.8280683160 + 0.8465498090 0.8373754025 0.8288035989 + 0.8472179770 0.8380613923 0.8295388818 + 0.8478839993 0.8387414813 0.8302717209 + 0.8485488892 0.8394191861 0.8310034275 + 0.8492138982 0.8400970101 0.8317350745 + 0.8498789072 0.8407747149 0.8324669003 + 0.8505439162 0.8414524198 0.8331986070 + 0.8512089252 0.8421301842 0.8339303136 + 0.8518738747 0.8428078890 0.8346620202 + 0.8525388837 0.8434855938 0.8353937864 + 0.8532038927 0.8441634178 0.8361254930 + 0.8538689017 0.8448411226 0.8368571997 + 0.8545317054 0.8455154896 0.8375867009 + 0.8551911116 0.8461846709 0.8383126259 + 0.8558505177 0.8468537927 0.8390386105 + 0.8565099835 0.8475229740 0.8397644758 + 0.8571693897 0.8481922150 0.8404904008 + 0.8578287959 0.8488612771 0.8412163258 + 0.8584882021 0.8495305181 0.8419421911 + 0.8591476083 0.8501995802 0.8426681161 + 0.8598070145 0.8508688211 0.8433941007 + 0.8604664207 0.8515380025 0.8441200256 + 0.8611258268 0.8522071242 0.8448458910 + 0.8617823720 0.8528741002 0.8455688953 + 0.8624386787 0.8535410166 0.8462917209 + 0.8630949855 0.8542078137 0.8470144272 + 0.8637512922 0.8548746109 0.8477371931 + 0.8644075990 0.8555414081 0.8484598994 + 0.8650639057 0.8562082052 0.8491827250 + 0.8657202125 0.8568751216 0.8499054909 + 0.8663765192 0.8575419188 0.8506281972 + 0.8670328259 0.8582087159 0.8513510227 + 0.8676890731 0.8588755131 0.8520737290 + 0.8683446050 0.8595426083 0.8527960777 + 0.8689982891 0.8602101803 0.8535174727 + 0.8696519136 0.8608778119 0.8542389870 + 0.8703055978 0.8615455031 0.8549603820 + 0.8709592223 0.8622130752 0.8556818962 + 0.8716129065 0.8628807068 0.8564032912 + 0.8722665906 0.8635482788 0.8571248055 + 0.8729202151 0.8642160296 0.8578462005 + 0.8735738993 0.8648836017 0.8585677147 + 0.8742275238 0.8655511737 0.8592891097 + 0.8748812079 0.8662188053 0.8600106239 + 0.8755341172 0.8668799996 0.8607301712 + 0.8761864901 0.8675361276 0.8614482284 + 0.8768388033 0.8681921959 0.8621662855 + 0.8774911761 0.8688483238 0.8628842831 + 0.8781434894 0.8695043921 0.8636023998 + 0.8787959218 0.8701604009 0.8643205166 + 0.8794481754 0.8708165288 0.8650385141 + 0.8801006079 0.8714725971 0.8657565713 + 0.8807529211 0.8721287251 0.8664746284 + 0.8814052939 0.8727847934 0.8671926856 + 0.8820576072 0.8734409213 0.8679106832 + 0.8827077150 0.8740913868 0.8686279058 + 0.8833565116 0.8747391105 0.8693444729 + 0.8840054274 0.8753867745 0.8700612187 + 0.8846542835 0.8760343790 0.8707777858 + 0.8853030801 0.8766821027 0.8714944720 + 0.8859519958 0.8773298264 0.8722112179 + 0.8866009116 0.8779774904 0.8729277849 + 0.8872497082 0.8786250949 0.8736444712 + 0.8878986239 0.8792728186 0.8743610978 + 0.8885474205 0.8799204826 0.8750777841 + 0.8891962767 0.8805680871 0.8757944107 + 0.8898416162 0.8812143803 0.8765113950 + 0.8904845715 0.8818597794 0.8772286177 + 0.8911275268 0.8825051785 0.8779457808 + 0.8917704821 0.8831505775 0.8786630034 + 0.8924134970 0.8837959766 0.8793802857 + 0.8930563927 0.8844413757 0.8800975084 + 0.8936994076 0.8850867748 0.8808146715 + 0.8943423033 0.8857321143 0.8815318942 + 0.8949853182 0.8863775134 0.8822491169 + 0.8956282735 0.8870229125 0.8829662800 + 0.8962712288 0.8876683116 0.8836835027 + 0.8969116211 0.8883140087 0.8844001293 + 0.8975483775 0.8889601827 0.8851156831 + 0.8981850743 0.8896064162 0.8858312964 + 0.8988217711 0.8902524710 0.8865469098 + 0.8994585276 0.8908987045 0.8872625232 + 0.9000952244 0.8915448785 0.8879780769 + 0.9007319212 0.8921909928 0.8886936903 + 0.9013686180 0.8928372264 0.8894093037 + 0.9020053148 0.8934834003 0.8901249170 + 0.9026420116 0.8941295147 0.8908404112 + 0.9032788277 0.8947756886 0.8915560246 + 0.9039149880 0.8954212070 0.8922712803 + 0.9045432806 0.8960561156 0.8929808736 + 0.9051715732 0.8966909051 0.8936905265 + 0.9057999253 0.8973258138 0.8944001794 + 0.9064282179 0.8979606032 0.8951097727 + 0.9070565104 0.8985955119 0.8958194256 + 0.9076849222 0.8992303014 0.8965290189 + 0.9083132148 0.8998652101 0.8972386718 + 0.9089415073 0.9004999995 0.8979483247 + 0.9095697999 0.9011349082 0.8986579180 + 0.9101980925 0.9017696977 0.8993675113 + 0.9108263850 0.9024046063 0.9000772238 + 0.9114509225 0.9030348063 0.9007843137 + 0.9120721817 0.9036613703 0.9014893770 + 0.9126935005 0.9042879939 0.9021945000 + 0.9133148193 0.9049146175 0.9028996229 + 0.9139360785 0.9055411816 0.9036046267 + 0.9145573974 0.9061678052 0.9043096900 + 0.9151787162 0.9067944288 0.9050148129 + 0.9157999754 0.9074209929 0.9057198763 + 0.9164212942 0.9080476165 0.9064249992 + 0.9170426130 0.9086741805 0.9071301222 + 0.9176638722 0.9093006849 0.9078351855 + 0.9182853103 0.9099273086 0.9085403085 + 0.9188956022 0.9105505943 0.9092364907 + 0.9195045829 0.9111735225 0.9099317789 + 0.9201136827 0.9117963910 0.9106270075 + 0.9207227826 0.9124193192 0.9113222957 + 0.9213318229 0.9130421281 0.9120175242 + 0.9219409227 0.9136649966 0.9127128124 + 0.9225499034 0.9142879248 0.9134079814 + 0.9231590033 0.9149107933 0.9141033292 + 0.9237679839 0.9155337214 0.9147984982 + 0.9243770838 0.9161565900 0.9154937863 + 0.9249861240 0.9167795181 0.9161890149 + 0.9255942106 0.9174020886 0.9168831706 + 0.9261904955 0.9180212021 0.9175652862 + 0.9267867804 0.9186403751 0.9182474017 + 0.9273831248 0.9192596078 0.9189295173 + 0.9279794097 0.9198787808 0.9196115732 + 0.9285756946 0.9204980135 0.9202936888 + 0.9291719794 0.9211171269 0.9209758043 + 0.9297683239 0.9217363000 0.9216579199 + 0.9303646088 0.9223554730 0.9223399758 + 0.9309607744 0.9229747057 0.9230220914 + 0.9315571189 0.9235938787 0.9237042069 + 0.9321534038 0.9242129922 0.9243863225 + 0.9327489734 0.9248315096 0.9250671268 + 0.9333375096 0.9254437089 0.9257367849 + 0.9339259863 0.9260557890 0.9264063835 + 0.9345145226 0.9266679287 0.9270759821 + 0.9351029992 0.9272801280 0.9277455807 + 0.9356915951 0.9278922081 0.9284152985 + 0.9362800717 0.9285042882 0.9290848970 + 0.9368686080 0.9291164875 0.9297544956 + 0.9374570847 0.9297286272 0.9304240942 + 0.9380456209 0.9303408265 0.9310936928 + 0.9386342168 0.9309529066 0.9317634106 + 0.9392226934 0.9315649867 0.9324330091 + 0.9398112297 0.9321771860 0.9331026077 + 0.9403957725 0.9327827096 0.9337632060 + 0.9409803152 0.9333879948 0.9344233274 + 0.9415647984 0.9339932799 0.9350835085 + 0.9421492815 0.9345986247 0.9357436895 + 0.9427338243 0.9352039099 0.9364038110 + 0.9433181882 0.9358091950 0.9370639920 + 0.9439026713 0.9364144802 0.9377241731 + 0.9444872141 0.9370198250 0.9383842945 + 0.9450716972 0.9376251101 0.9390444756 + 0.9456561804 0.9382303953 0.9397047162 + 0.9462407231 0.9388356805 0.9403647780 + 0.9468250871 0.9394410253 0.9410250187 + 0.9474024773 0.9400426149 0.9416800141 + 0.9479761124 0.9406421781 0.9423323274 + 0.9485496879 0.9412418008 0.9429845214 + 0.9491233230 0.9418414831 0.9436367750 + 0.9496968985 0.9424411058 0.9442890882 + 0.9502704740 0.9430407286 0.9449412823 + 0.9508441091 0.9436402917 0.9455935955 + 0.9514176846 0.9442399740 0.9462457895 + 0.9519913197 0.9448395967 0.9468981028 + 0.9525648952 0.9454392195 0.9475504160 + 0.9531384706 0.9460387826 0.9482026100 + 0.9537121058 0.9466385245 0.9488549232 + 0.9542835951 0.9472373724 0.9495049119 + 0.9548454881 0.9478332996 0.9501445293 + 0.9554073811 0.9484291077 0.9507842064 + 0.9559693933 0.9490249753 0.9514238238 + 0.9565312862 0.9496209025 0.9520635009 + 0.9570931792 0.9502167106 0.9527031779 + 0.9576550722 0.9508125782 0.9533427954 + 0.9582170248 0.9514083862 0.9539824724 + 0.9587789178 0.9520043135 0.9546220899 + 0.9593408108 0.9526001811 0.9552618265 + 0.9599027038 0.9531959891 0.9559013844 + 0.9604645967 0.9537919164 0.9565411210 + 0.9610264897 0.9543877244 0.9571806788 + 0.9615818262 0.9549801946 0.9578106999 + 0.9621313214 0.9555696845 0.9584323764 + 0.9626806974 0.9561592937 0.9590541124 + 0.9632301927 0.9567487836 0.9596757293 + 0.9637796879 0.9573382735 0.9602974057 + 0.9643291831 0.9579278827 0.9609190822 + 0.9648786783 0.9585173726 0.9615408182 + 0.9654281735 0.9591069221 0.9621623755 + 0.9659777284 0.9596964717 0.9627841115 + 0.9665271044 0.9602860212 0.9634057879 + 0.9670765996 0.9608755112 0.9640274048 + 0.9676260948 0.9614651203 0.9646490812 + 0.9681755900 0.9620546103 0.9652708173 + 0.9687122703 0.9626389146 0.9658780098 + 0.9692441821 0.9632211924 0.9664797187 + 0.9697760940 0.9638035297 0.9670814276 + 0.9703080058 0.9643858075 0.9676830769 + 0.9708399177 0.9649680853 0.9682847857 + 0.9713717103 0.9655504227 0.9688864946 + 0.9719036222 0.9661327004 0.9694882035 + 0.9724354744 0.9667149782 0.9700899124 + 0.9729673862 0.9672973156 0.9706916213 + 0.9734992981 0.9678795934 0.9712932706 + 0.9740310907 0.9684618711 0.9718949795 + 0.9745630026 0.9690440893 0.9724966288 + 0.9750949144 0.9696264267 0.9730982780 + 0.9756121039 0.9702054262 0.9736818075 + 0.9761241078 0.9707831144 0.9742587805 + 0.9766359925 0.9713609219 0.9748356938 + 0.9771479964 0.9719387293 0.9754127264 + 0.9776600003 0.9725164175 0.9759895802 + 0.9781718850 0.9730942249 0.9765666127 + 0.9786838889 0.9736719131 0.9771435857 + 0.9791958928 0.9742497206 0.9777204990 + 0.9797077775 0.9748274088 0.9782974720 + 0.9802197814 0.9754052162 0.9788743854 + 0.9807317853 0.9759829044 0.9794514179 + 0.9812437296 0.9765607119 0.9800282717 + 0.9817556739 0.9771384001 0.9806053042 + 0.9822546244 0.9777116776 0.9811592102 + 0.9827439189 0.9782817960 0.9816957712 + 0.9832330942 0.9788517952 0.9822325110 + 0.9837223291 0.9794217944 0.9827690721 + 0.9842115045 0.9799917936 0.9833058119 + 0.9847007990 0.9805617929 0.9838423729 + 0.9851899743 0.9811317921 0.9843789935 + 0.9856792092 0.9817017913 0.9849156737 + 0.9861685038 0.9822717905 0.9854522943 + 0.9866576791 0.9828417897 0.9859889746 + 0.9871469140 0.9834119081 0.9865255952 + 0.9876360893 0.9839819074 0.9870622754 + 0.9881253839 0.9845519066 0.9875988960 + 0.9886041284 0.9851186275 0.9881225228 + 0.9890480042 0.9856745005 0.9886025786 + 0.9894918799 0.9862303734 0.9890826941 + 0.9899358153 0.9867863059 0.9895628095 + 0.9903796911 0.9873421788 0.9900428057 + 0.9908236265 0.9878981113 0.9905229211 + 0.9912675023 0.9884539843 0.9910029769 + 0.9917113781 0.9890099168 0.9914829731 + 0.9921553135 0.9895657897 0.9919630885 + 0.9925991893 0.9901217222 0.9924432039 + 0.9930431247 0.9906775951 0.9929233193 + 0.9934868813 0.9912334085 0.9934033155 + 0.9939308167 0.9917892814 0.9938833714 + 0.9943746924 0.9923452139 0.9943634868 + 0.9947882295 0.9928944707 0.9947875738 + 0.9951890707 0.9934411049 0.9951885939 + 0.9955899715 0.9939876795 0.9955894947 + 0.9959908724 0.9945343137 0.9959905148 + 0.9963918924 0.9950808287 0.9963914156 + 0.9967927933 0.9956274033 0.9967923760 + 0.9971936941 0.9961739779 0.9971932769 + 0.9975945950 0.9967206120 0.9975942969 + 0.9979954958 0.9972671270 0.9979951978 + 0.9983963966 0.9978137016 0.9983962178 + 0.9987972975 0.9983602762 0.9987971187 + 0.9991981983 0.9989069104 0.9991980791 + 0.9995990992 0.9994534254 0.9995989799 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_800.spi1d b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_800.spi1d index 754cb9b7..960c2dda 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_800.spi1d +++ b/RenderPipelineFile/rpplugins/color_correction/resources/film_luts_raw/kodak_portra_800.spi1d @@ -1,1030 +1,1030 @@ -Version 1 -From 0.0 1.0 -Length 1024 -Components 3 -{ - 0.0000000000 0.0000000000 0.0000000000 - 0.0088124285 0.0079260534 0.0079260534 - 0.0173182599 0.0154689904 0.0154689904 - 0.0250097904 0.0223134402 0.0223134402 - 0.0318675414 0.0285592694 0.0285592694 - 0.0382458083 0.0343292207 0.0343292207 - 0.0440907888 0.0397338495 0.0397338495 - 0.0496913791 0.0447938815 0.0447938815 - 0.0549593382 0.0496262200 0.0496262200 - 0.0600188002 0.0542288311 0.0542288311 - 0.0648257434 0.0586788617 0.0586788617 - 0.0694660619 0.0629591122 0.0629591122 - 0.0739658996 0.0670659617 0.0670659617 - 0.0782863200 0.0710892603 0.0710892603 - 0.0824507698 0.0749912113 0.0749912113 - 0.0864929035 0.0788405612 0.0788405612 - 0.0904697403 0.0825625733 0.0825625733 - 0.0943273082 0.0861979574 0.0861979574 - 0.0981024578 0.0897707865 0.0897707865 - 0.1018401012 0.0932211131 0.0932211131 - 0.1054691002 0.0966563523 0.0966563523 - 0.1090489030 0.1000294015 0.1000294015 - 0.1125440001 0.1033191979 0.1033191979 - 0.1159221008 0.1065331027 0.1065331027 - 0.1192713007 0.1097005978 0.1097005978 - 0.1225581989 0.1128138974 0.1128138974 - 0.1257860959 0.1158875003 0.1158875003 - 0.1289588958 0.1189555973 0.1189555973 - 0.1321441978 0.1219801009 0.1219801009 - 0.1352636069 0.1249566972 0.1249566972 - 0.1383159012 0.1278752983 0.1278752983 - 0.1413581967 0.1307422966 0.1307422966 - 0.1443576962 0.1336016953 0.1336016953 - 0.1472717971 0.1364331990 0.1364331990 - 0.1501350999 0.1392025054 0.1392025054 - 0.1529815942 0.1419408023 0.1419408023 - 0.1558008045 0.1446657926 0.1446657926 - 0.1586032957 0.1473688930 0.1473688930 - 0.1613503993 0.1500454992 0.1500454992 - 0.1640668958 0.1526847035 0.1526847035 - 0.1667777002 0.1552831978 0.1552831978 - 0.1694519967 0.1578582972 0.1578582972 - 0.1720923930 0.1604107022 0.1604107022 - 0.1746968031 0.1629472971 0.1629472971 - 0.1772591025 0.1654641926 0.1654641926 - 0.1797759980 0.1679515988 0.1679515988 - 0.1822800934 0.1704009026 0.1704009026 - 0.1847687066 0.1728225946 0.1728225946 - 0.1872310042 0.1752288938 0.1752288938 - 0.1896561980 0.1776168942 0.1776168942 - 0.1920384020 0.1799840033 0.1799840033 - 0.1944015026 0.1823205948 0.1823205948 - 0.1967525929 0.1846179962 0.1846179962 - 0.1990911067 0.1869053990 0.1869053990 - 0.2014010996 0.1891890019 0.1891890019 - 0.2037014961 0.1914498061 0.1914498061 - 0.2060168982 0.1936779022 0.1936779022 - 0.2083324045 0.1958771050 0.1958771050 - 0.2106439024 0.1980604976 0.1980604976 - 0.2129282951 0.2002373040 0.2002373040 - 0.2151844054 0.2024063021 0.2024063021 - 0.2174350023 0.2045633942 0.2045633942 - 0.2196764946 0.2067063004 0.2067063004 - 0.2219022959 0.2088321000 0.2088321000 - 0.2241204977 0.2109334022 0.2109334022 - 0.2263171971 0.2130157053 0.2130157053 - 0.2284786999 0.2150927931 0.2150927931 - 0.2306168973 0.2171598971 0.2171598971 - 0.2327332944 0.2192052007 0.2192052007 - 0.2348272949 0.2212346047 0.2212346047 - 0.2369115055 0.2232697010 0.2232697010 - 0.2389767021 0.2253111005 0.2253111005 - 0.2410169989 0.2273571938 0.2273571938 - 0.2430370003 0.2294019014 0.2294019014 - 0.2450459003 0.2314381003 0.2314381003 - 0.2470474988 0.2334637046 0.2334637046 - 0.2490427047 0.2354702950 0.2354702950 - 0.2510350049 0.2374535054 0.2374535054 - 0.2530182004 0.2394181937 0.2394181937 - 0.2549867928 0.2413693964 0.2413693964 - 0.2569330931 0.2433117926 0.2433117926 - 0.2588599026 0.2452446073 0.2452446073 - 0.2607747018 0.2471683025 0.2471683025 - 0.2626813948 0.2490822971 0.2490822971 - 0.2645846903 0.2509844899 0.2509844899 - 0.2664898038 0.2528660893 0.2528660893 - 0.2683837116 0.2547389865 0.2547389865 - 0.2702631950 0.2566035986 0.2566035986 - 0.2721140981 0.2584480941 0.2584480941 - 0.2739478052 0.2602775097 0.2602775097 - 0.2757706046 0.2620961070 0.2620961070 - 0.2775853872 0.2639020085 0.2639020085 - 0.2794038057 0.2656953037 0.2656953037 - 0.2812230885 0.2674824893 0.2674824893 - 0.2830418050 0.2692643106 0.2692643106 - 0.2848351002 0.2710475922 0.2710475922 - 0.2866136134 0.2728300095 0.2728300095 - 0.2883686125 0.2746050954 0.2746050954 - 0.2901135087 0.2763741016 0.2763741016 - 0.2918516994 0.2781391144 0.2781391144 - 0.2935769856 0.2798882127 0.2798882127 - 0.2953031063 0.2816232145 0.2816232145 - 0.2970305085 0.2833353877 0.2833353877 - 0.2987627089 0.2850272954 0.2850272954 - 0.3004961908 0.2867107093 0.2867107093 - 0.3022179008 0.2883763909 0.2883763909 - 0.3039326966 0.2900390029 0.2900390029 - 0.3056339025 0.2916957140 0.2916957140 - 0.3073236048 0.2933523953 0.2933523953 - 0.3090057075 0.2950091958 0.2950091958 - 0.3106757104 0.2966647148 0.2966647148 - 0.3123432994 0.2983194888 0.2983194888 - 0.3140107989 0.2999661863 0.2999661863 - 0.3156793118 0.3016082942 0.3016082942 - 0.3173511028 0.3032361865 0.3032361865 - 0.3190175891 0.3048543036 0.3048543036 - 0.3206757009 0.3064570129 0.3064570129 - 0.3223229051 0.3080512881 0.3080512881 - 0.3239594102 0.3096374869 0.3096374869 - 0.3255836964 0.3112207055 0.3112207055 - 0.3271996081 0.3128018975 0.3128018975 - 0.3288047910 0.3143788874 0.3143788874 - 0.3304041922 0.3159537017 0.3159537017 - 0.3320013881 0.3175298870 0.3175298870 - 0.3335976005 0.3191067874 0.3191067874 - 0.3351888955 0.3206779063 0.3206779063 - 0.3367778063 0.3222463131 0.3222463131 - 0.3383550048 0.3238070011 0.3238070011 - 0.3399255872 0.3253633976 0.3253633976 - 0.3414827883 0.3269123137 0.3269123137 - 0.3430297971 0.3284555078 0.3284555078 - 0.3445636928 0.3299894929 0.3299894929 - 0.3460822105 0.3315128088 0.3315128088 - 0.3475958109 0.3330293894 0.3330293894 - 0.3490990102 0.3345321119 0.3345321119 - 0.3506017923 0.3360328078 0.3360328078 - 0.3521026969 0.3375233114 0.3375233114 - 0.3536036015 0.3390137851 0.3390137851 - 0.3551059067 0.3404915035 0.3404915035 - 0.3566082120 0.3419683874 0.3419683874 - 0.3580994904 0.3434371054 0.3434371054 - 0.3595857024 0.3449020982 0.3449020982 - 0.3610610068 0.3463630974 0.3463630974 - 0.3625203967 0.3478182852 0.3478182852 - 0.3639777005 0.3492721915 0.3492721915 - 0.3654133976 0.3507139981 0.3507139981 - 0.3668490946 0.3521556854 0.3521556854 - 0.3682768941 0.3535880148 0.3535880148 - 0.3697017133 0.3550168872 0.3550168872 - 0.3711271882 0.3564417064 0.3564417064 - 0.3725540042 0.3578588068 0.3578588068 - 0.3739807904 0.3592759073 0.3592759073 - 0.3754133880 0.3606815040 0.3606815040 - 0.3768466115 0.3620859981 0.3620859981 - 0.3782753050 0.3634850979 0.3634850979 - 0.3796978891 0.3648771048 0.3648771048 - 0.3811204135 0.3662689924 0.3662689924 - 0.3825325072 0.3676514030 0.3676514030 - 0.3839435875 0.3690330088 0.3690330088 - 0.3853504956 0.3704110980 0.3704110980 - 0.3867498934 0.3717832863 0.3717832863 - 0.3881492913 0.3731555045 0.3731555045 - 0.3895345032 0.3745233119 0.3745233119 - 0.3909151852 0.3758895993 0.3758895993 - 0.3922944069 0.3772548139 0.3772548139 - 0.3936628103 0.3786112964 0.3786112964 - 0.3950310946 0.3799678087 0.3799678087 - 0.3963941932 0.3813194036 0.3813194036 - 0.3977501988 0.3826650083 0.3826650083 - 0.3991062939 0.3840106130 0.3840106130 - 0.4004457891 0.3853495121 0.3853495121 - 0.4017783105 0.3866856992 0.3866856992 - 0.4031107128 0.3880218863 0.3880218863 - 0.4044226110 0.3893507123 0.3893507123 - 0.4057329893 0.3906790018 0.3906790018 - 0.4070420861 0.3920061886 0.3920061886 - 0.4083415866 0.3933256865 0.3933256865 - 0.4096410871 0.3946452141 0.3946452141 - 0.4109367132 0.3959634900 0.3959634900 - 0.4122210145 0.3972786963 0.3972786963 - 0.4135052860 0.3985939026 0.3985939026 - 0.4147841930 0.3999078870 0.3999078870 - 0.4160529971 0.4012196958 0.4012196958 - 0.4173217118 0.4025315046 0.4025315046 - 0.4185839891 0.4038358033 0.4038358033 - 0.4198360145 0.4051288962 0.4051288962 - 0.4210878909 0.4064218998 0.4064218998 - 0.4223288000 0.4077093899 0.4077093899 - 0.4235515893 0.4089880884 0.4089880884 - 0.4247742891 0.4102669060 0.4102669060 - 0.4259923995 0.4115405083 0.4115405083 - 0.4272009134 0.4128041863 0.4128041863 - 0.4284093082 0.4140678942 0.4140678942 - 0.4296151102 0.4153267145 0.4153267145 - 0.4308117032 0.4165697992 0.4165697992 - 0.4320083857 0.4178129137 0.4178129137 - 0.4332041144 0.4190546870 0.4190546870 - 0.4343877137 0.4202808142 0.4202808142 - 0.4355713129 0.4215069115 0.4215069115 - 0.4367547929 0.4227330089 0.4227330089 - 0.4379391968 0.4239530861 0.4239530861 - 0.4391236901 0.4251722991 0.4251722991 - 0.4403083026 0.4263913929 0.4263913929 - 0.4414936900 0.4276081026 0.4276081026 - 0.4426796138 0.4288231134 0.4288231134 - 0.4438655972 0.4300382137 0.4300382137 - 0.4450480044 0.4312498868 0.4312498868 - 0.4462217093 0.4324539900 0.4324539900 - 0.4473954141 0.4336580038 0.4336580038 - 0.4485690892 0.4348619878 0.4348619878 - 0.4497332871 0.4360530078 0.4360530078 - 0.4508967996 0.4372431040 0.4372431040 - 0.4520604014 0.4384332895 0.4384332895 - 0.4532183111 0.4396133125 0.4396133125 - 0.4543708861 0.4407838881 0.4407838881 - 0.4555234909 0.4419544041 0.4419544041 - 0.4566758871 0.4431242943 0.4431242943 - 0.4578221142 0.4442783892 0.4442783892 - 0.4589683115 0.4454325140 0.4454325140 - 0.4601145089 0.4465866089 0.4465866089 - 0.4612562060 0.4477371871 0.4477371871 - 0.4623934925 0.4488846064 0.4488846064 - 0.4635308087 0.4500319064 0.4500319064 - 0.4646682143 0.4511792958 0.4511792958 - 0.4657953978 0.4523159862 0.4523159862 - 0.4669215977 0.4534518123 0.4534518123 - 0.4680477977 0.4545876086 0.4545876086 - 0.4691722989 0.4557209909 0.4557209909 - 0.4702917933 0.4568476975 0.4568476975 - 0.4714111984 0.4579744935 0.4579744935 - 0.4725306928 0.4591012895 0.4591012895 - 0.4736500978 0.4602225125 0.4602225125 - 0.4747695029 0.4613394141 0.4613394141 - 0.4758889973 0.4624564052 0.4624564052 - 0.4770084023 0.4635733068 0.4635733068 - 0.4781275094 0.4646795094 0.4646795094 - 0.4792464077 0.4657829106 0.4657829106 - 0.4803653955 0.4668863118 0.4668863118 - 0.4814842939 0.4679895937 0.4679895937 - 0.4825977981 0.4690910876 0.4690910876 - 0.4837110043 0.4701924026 0.4701924026 - 0.4848242104 0.4712938070 0.4712938070 - 0.4859367907 0.4723945856 0.4723945856 - 0.4870401919 0.4734885097 0.4734885097 - 0.4881435037 0.4745824933 0.4745824933 - 0.4892467856 0.4756763875 0.4756763875 - 0.4903483987 0.4767695963 0.4767695963 - 0.4914352894 0.4778566062 0.4778566062 - 0.4925222099 0.4789434969 0.4789434969 - 0.4936090112 0.4800305068 0.4800305068 - 0.4946955144 0.4811173975 0.4811173975 - 0.4957770109 0.4822036028 0.4822036028 - 0.4968583882 0.4832898080 0.4832898080 - 0.4979399145 0.4843761027 0.4843761027 - 0.4990214109 0.4854621887 0.4854621887 - 0.5000975728 0.4865370989 0.4865370989 - 0.5011737943 0.4876120090 0.4876120090 - 0.5022500157 0.4886868894 0.4886868894 - 0.5033261776 0.4897617996 0.4897617996 - 0.5043833256 0.4908238053 0.4908238053 - 0.5054367781 0.4918834865 0.4918834865 - 0.5064904094 0.4929431975 0.4929431975 - 0.5075439811 0.4940029085 0.4940029085 - 0.5085870028 0.4950586855 0.4950586855 - 0.5096237063 0.4961124063 0.4961124063 - 0.5106604099 0.4971660078 0.4971660078 - 0.5116971135 0.4982196093 0.4982196093 - 0.5127326250 0.4992741942 0.4992741942 - 0.5137655139 0.5003306270 0.5003306270 - 0.5147984028 0.5013868809 0.5013868809 - 0.5158312917 0.5024433136 0.5024433136 - 0.5168641806 0.5034996271 0.5034996271 - 0.5178936124 0.5045477748 0.5045477748 - 0.5189229250 0.5055958033 0.5055958033 - 0.5199521780 0.5066437721 0.5066437721 - 0.5209816098 0.5076918006 0.5076918006 - 0.5220068097 0.5087348819 0.5087348819 - 0.5230284929 0.5097740889 0.5097740889 - 0.5240502954 0.5108131766 0.5108131766 - 0.5250719786 0.5118523240 0.5118523240 - 0.5260933042 0.5128911138 0.5128911138 - 0.5271025896 0.5139240026 0.5139240026 - 0.5281118751 0.5149567723 0.5149567723 - 0.5291212201 0.5159897208 0.5159897208 - 0.5301305056 0.5170226097 0.5170226097 - 0.5311338902 0.5180504918 0.5180504918 - 0.5321304202 0.5190727711 0.5190727711 - 0.5331268907 0.5200952291 0.5200952291 - 0.5341234207 0.5211176276 0.5211176276 - 0.5351197720 0.5221399069 0.5221399069 - 0.5361112952 0.5231561065 0.5231561065 - 0.5371016860 0.5241708755 0.5241708755 - 0.5380920172 0.5251858234 0.5251858234 - 0.5390822887 0.5262005925 0.5262005925 - 0.5400725007 0.5272145271 0.5272145271 - 0.5410608053 0.5282195807 0.5282195807 - 0.5420491099 0.5292246938 0.5292246938 - 0.5430374742 0.5302299261 0.5302299261 - 0.5440257788 0.5312349796 0.5312349796 - 0.5450109243 0.5322359800 0.5322359800 - 0.5459877849 0.5332267880 0.5332267880 - 0.5469647050 0.5342177153 0.5342177153 - 0.5479416251 0.5352085233 0.5352085233 - 0.5489184856 0.5361993909 0.5361993909 - 0.5498880744 0.5371838212 0.5371838212 - 0.5508465767 0.5381588936 0.5381588936 - 0.5518051982 0.5391340256 0.5391340256 - 0.5527637005 0.5401090980 0.5401090980 - 0.5537223220 0.5410841703 0.5410841703 - 0.5546782017 0.5420532823 0.5420532823 - 0.5556306839 0.5430148244 0.5430148244 - 0.5565832853 0.5439764261 0.5439764261 - 0.5575358272 0.5449379086 0.5449379086 - 0.5584883094 0.5458995104 0.5458995104 - 0.5594409704 0.5468540192 0.5468540192 - 0.5603938103 0.5477982163 0.5477982163 - 0.5613465905 0.5487424135 0.5487424135 - 0.5622994900 0.5496866107 0.5496866107 - 0.5632522702 0.5506308079 0.5506308079 - 0.5642018914 0.5515701175 0.5515701175 - 0.5651431084 0.5524966717 0.5524966717 - 0.5660842061 0.5534232855 0.5534232855 - 0.5670254230 0.5543497801 0.5543497801 - 0.5679665804 0.5552763939 0.5552763939 - 0.5689064860 0.5562021732 0.5562021732 - 0.5698277950 0.5571172237 0.5571172237 - 0.5707491040 0.5580322146 0.5580322146 - 0.5716704130 0.5589473248 0.5589473248 - 0.5725917220 0.5598623157 0.5598623157 - 0.5735129714 0.5607773066 0.5607773066 - 0.5744280815 0.5616865754 0.5616865754 - 0.5753415227 0.5625941753 0.5625941753 - 0.5762547851 0.5635017753 0.5635017753 - 0.5771682262 0.5644093752 0.5644093752 - 0.5780814886 0.5653169751 0.5653169751 - 0.5789892077 0.5662230849 0.5662230849 - 0.5798879862 0.5671266913 0.5671266913 - 0.5807867050 0.5680304170 0.5680304170 - 0.5816854835 0.5689340234 0.5689340234 - 0.5825843215 0.5698376894 0.5698376894 - 0.5834829807 0.5707412958 0.5707412958 - 0.5843644738 0.5716376901 0.5716376901 - 0.5852444768 0.5725334883 0.5725334883 - 0.5861244798 0.5734294057 0.5734294057 - 0.5870044827 0.5743252039 0.5743252039 - 0.5878844857 0.5752210021 0.5752210021 - 0.5887588263 0.5761132240 0.5761132240 - 0.5896230936 0.5769993067 0.5769993067 - 0.5904874802 0.5778853893 0.5778853893 - 0.5913518071 0.5787714124 0.5787714124 - 0.5922161937 0.5796574950 0.5796574950 - 0.5930805206 0.5805435777 0.5805435777 - 0.5939381719 0.5814265013 0.5814265013 - 0.5947932005 0.5823081732 0.5823081732 - 0.5956482291 0.5831899047 0.5831899047 - 0.5965033174 0.5840715170 0.5840715170 - 0.5973582864 0.5849531889 0.5849531889 - 0.5982133150 0.5858349204 0.5858349204 - 0.5990638733 0.5867123008 0.5867123008 - 0.5999143124 0.5875896811 0.5875896811 - 0.6007646918 0.5884671211 0.5884671211 - 0.6016150713 0.5893443823 0.5893443823 - 0.6024655104 0.5902218223 0.5902218223 - 0.6033152938 0.5910989046 0.5910989046 - 0.6041615009 0.5919741988 0.5919741988 - 0.6050077081 0.5928496122 0.5928496122 - 0.6058537960 0.5937249064 0.5937249064 - 0.6067000031 0.5946002007 0.5946002007 - 0.6075462103 0.5954756141 0.5954756141 - 0.6083909273 0.5963500142 0.5963500142 - 0.6092299223 0.5972210169 0.5972210169 - 0.6100689769 0.5980920792 0.5980920792 - 0.6109080911 0.5989630818 0.5989630818 - 0.6117472053 0.5998342037 0.5998342037 - 0.6125863194 0.6007053256 0.6007053256 - 0.6134225726 0.6015741229 0.6015741229 - 0.6142470241 0.6024342179 0.6024342179 - 0.6150714755 0.6032943130 0.6032943130 - 0.6158959866 0.6041544080 0.6041544080 - 0.6167204976 0.6050143838 0.6050143838 - 0.6175450087 0.6058744788 0.6058744788 - 0.6183692813 0.6067342162 0.6067342162 - 0.6191921234 0.6075887084 0.6075887084 - 0.6200149059 0.6084432006 0.6084432006 - 0.6208376884 0.6092978120 0.6092978120 - 0.6216604710 0.6101523042 0.6101523042 - 0.6224833131 0.6110067964 0.6110067964 - 0.6233062148 0.6118612885 0.6118612885 - 0.6241316199 0.6127145290 0.6127145290 - 0.6249575019 0.6135674119 0.6135674119 - 0.6257833242 0.6144202948 0.6144202948 - 0.6266092062 0.6152731776 0.6152731776 - 0.6274350882 0.6161261201 0.6161261201 - 0.6282610297 0.6169791222 0.6169791222 - 0.6290842295 0.6178271174 0.6178271174 - 0.6299049258 0.6186708808 0.6186708808 - 0.6307256222 0.6195145845 0.6195145845 - 0.6315463781 0.6203582883 0.6203582883 - 0.6323670745 0.6212021112 0.6212021112 - 0.6331877708 0.6220458150 0.6220458150 - 0.6340078712 0.6228886247 0.6228886247 - 0.6348218918 0.6237223148 0.6237223148 - 0.6356359124 0.6245561242 0.6245561242 - 0.6364498734 0.6253898740 0.6253898740 - 0.6372638941 0.6262236238 0.6262236238 - 0.6380779147 0.6270573735 0.6270573735 - 0.6388918757 0.6278911829 0.6278911829 - 0.6397082210 0.6287242174 0.6287242174 - 0.6405261755 0.6295565963 0.6295565963 - 0.6413441896 0.6303889751 0.6303889751 - 0.6421622038 0.6312214136 0.6312214136 - 0.6429802775 0.6320537925 0.6320537925 - 0.6437982917 0.6328861713 0.6328861713 - 0.6446163058 0.6337186098 0.6337186098 - 0.6454324722 0.6345450282 0.6345450282 - 0.6462485194 0.6353709102 0.6353709102 - 0.6470645070 0.6361969113 0.6361969113 - 0.6478806138 0.6370227933 0.6370227933 - 0.6486966014 0.6378486753 0.6378486753 - 0.6495125890 0.6386746168 0.6386746168 - 0.6503264904 0.6394987106 0.6394987106 - 0.6511304975 0.6403145790 0.6403145790 - 0.6519343853 0.6411305070 0.6411305070 - 0.6527382731 0.6419463754 0.6419463754 - 0.6535422206 0.6427623034 0.6427623034 - 0.6543462276 0.6435781717 0.6435781717 - 0.6551501155 0.6443940997 0.6443940997 - 0.6559516191 0.6452056170 0.6452056170 - 0.6567478776 0.6460080147 0.6460080147 - 0.6575443149 0.6468102932 0.6468102932 - 0.6583405733 0.6476125717 0.6476125717 - 0.6591368914 0.6484150290 0.6484150290 - 0.6599332094 0.6492173076 0.6492173076 - 0.6607295871 0.6500195861 0.6500195861 - 0.6615231037 0.6508170962 0.6508170962 - 0.6623113751 0.6516057849 0.6516057849 - 0.6630997062 0.6523944736 0.6523944736 - 0.6638879776 0.6531832218 0.6531832218 - 0.6646761894 0.6539719105 0.6539719105 - 0.6654645205 0.6547605991 0.6547605991 - 0.6662527919 0.6555492878 0.6555492878 - 0.6670367718 0.6563370228 0.6563370228 - 0.6678087711 0.6571220756 0.6571220756 - 0.6685808897 0.6579073071 0.6579073071 - 0.6693528891 0.6586924195 0.6586924195 - 0.6701248884 0.6594774723 0.6594774723 - 0.6708970070 0.6602627039 0.6602627039 - 0.6716690063 0.6610478163 0.6610478163 - 0.6724401712 0.6618329883 0.6618329883 - 0.6731992960 0.6626194119 0.6626194119 - 0.6739584208 0.6634057760 0.6634057760 - 0.6747174859 0.6641921997 0.6641921997 - 0.6754766703 0.6649786234 0.6649786234 - 0.6762357950 0.6657649875 0.6657649875 - 0.6769949198 0.6665514112 0.6665514112 - 0.6777541041 0.6673377752 0.6673377752 - 0.6785085201 0.6681154966 0.6681154966 - 0.6792613268 0.6688904762 0.6688904762 - 0.6800141931 0.6696655154 0.6696655154 - 0.6807669997 0.6704404950 0.6704404950 - 0.6815198064 0.6712154746 0.6712154746 - 0.6822726727 0.6719905138 0.6719905138 - 0.6830254793 0.6727654934 0.6727654934 - 0.6837748289 0.6735368967 0.6735368967 - 0.6845164895 0.6743007898 0.6743007898 - 0.6852582097 0.6750648022 0.6750648022 - 0.6859998703 0.6758286953 0.6758286953 - 0.6867415905 0.6765927076 0.6765927076 - 0.6874831915 0.6773566008 0.6773566008 - 0.6882249117 0.6781204939 0.6781204939 - 0.6889665723 0.6788845062 0.6788845062 - 0.6897013187 0.6796460152 0.6796460152 - 0.6904339194 0.6804066896 0.6804066896 - 0.6911664009 0.6811674833 0.6811674833 - 0.6918990016 0.6819282770 0.6819282770 - 0.6926314831 0.6826890111 0.6826890111 - 0.6933640242 0.6834498048 0.6834498048 - 0.6940966249 0.6842104793 0.6842104793 - 0.6948286295 0.6849713922 0.6849713922 - 0.6955537796 0.6857334971 0.6857334971 - 0.6962791085 0.6864956021 0.6864956021 - 0.6970043778 0.6872575879 0.6872575879 - 0.6977297068 0.6880196929 0.6880196929 - 0.6984549761 0.6887817979 0.6887817979 - 0.6991803050 0.6895439029 0.6895439029 - 0.6999055743 0.6903060079 0.6903060079 - 0.7006272078 0.6910654902 0.6910654902 - 0.7013388276 0.6918187141 0.6918187141 - 0.7020503879 0.6925718188 0.6925718188 - 0.7027621269 0.6933249235 0.6933249235 - 0.7034736872 0.6940780282 0.6940780282 - 0.7041853070 0.6948311925 0.6948311925 - 0.7048969865 0.6955842972 0.6955842972 - 0.7056086063 0.6963374019 0.6963374019 - 0.7063143849 0.6970837116 0.6970837116 - 0.7070090175 0.6978172064 0.6978172064 - 0.7077037096 0.6985507011 0.6985507011 - 0.7083982825 0.6992840767 0.6992840767 - 0.7090929151 0.7000175714 0.7000175714 - 0.7097874880 0.7007511258 0.7007511258 - 0.7104821205 0.7014846206 0.7014846206 - 0.7111766934 0.7022181153 0.7022181153 - 0.7118694186 0.7029485703 0.7029485703 - 0.7125568986 0.7036715150 0.7036715150 - 0.7132444978 0.7043945193 0.7043945193 - 0.7139320970 0.7051174045 0.7051174045 - 0.7146195769 0.7058404088 0.7058404088 - 0.7153071761 0.7065634131 0.7065634131 - 0.7159947753 0.7072862983 0.7072862983 - 0.7166823745 0.7080093026 0.7080093026 - 0.7173694968 0.7087321281 0.7087321281 - 0.7180529237 0.7094535828 0.7094535828 - 0.7187362909 0.7101749778 0.7101749778 - 0.7194197178 0.7108964920 0.7108964920 - 0.7201030254 0.7116180062 0.7116180062 - 0.7207863927 0.7123395205 0.7123395205 - 0.7214698195 0.7130609155 0.7130609155 - 0.7221531272 0.7137824297 0.7137824297 - 0.7228364944 0.7145038843 0.7145038843 - 0.7235093117 0.7152239084 0.7152239084 - 0.7241787910 0.7159435153 0.7159435153 - 0.7248482704 0.7166631222 0.7166631222 - 0.7255179286 0.7173827291 0.7173827291 - 0.7261874080 0.7181022763 0.7181022763 - 0.7268568873 0.7188218832 0.7188218832 - 0.7275264263 0.7195414901 0.7195414901 - 0.7281959057 0.7202610970 0.7202610970 - 0.7288634181 0.7209777236 0.7209777236 - 0.7295259833 0.7216871977 0.7216871977 - 0.7301887274 0.7223966718 0.7223966718 - 0.7308512926 0.7231062055 0.7231062055 - 0.7315139771 0.7238156796 0.7238156796 - 0.7321766019 0.7245252132 0.7245252132 - 0.7328392863 0.7252346873 0.7252346873 - 0.7335019112 0.7259442210 0.7259442210 - 0.7341645956 0.7266536951 0.7266536951 - 0.7348304987 0.7273570895 0.7273570895 - 0.7354977727 0.7280579209 0.7280579209 - 0.7361652255 0.7287588120 0.7287588120 - 0.7368324995 0.7294595838 0.7294595838 - 0.7374997735 0.7301604152 0.7301604152 - 0.7381672263 0.7308611870 0.7308611870 - 0.7388345003 0.7315620184 0.7315620184 - 0.7395017743 0.7322629094 0.7322629094 - 0.7401691079 0.7329636812 0.7329636812 - 0.7408332229 0.7336601019 0.7336601019 - 0.7414970994 0.7343564034 0.7343564034 - 0.7421609163 0.7350527048 0.7350527048 - 0.7428247929 0.7357490063 0.7357490063 - 0.7434887290 0.7364453077 0.7364453077 - 0.7441526055 0.7371416092 0.7371416092 - 0.7448164225 0.7378379107 0.7378379107 - 0.7454802990 0.7385342121 0.7385342121 - 0.7461434007 0.7392303944 0.7392303944 - 0.7467958927 0.7399262190 0.7399262190 - 0.7474485040 0.7406219244 0.7406219244 - 0.7481011152 0.7413176894 0.7413176894 - 0.7487536073 0.7420133948 0.7420133948 - 0.7494062185 0.7427091002 0.7427091002 - 0.7500588298 0.7434049249 0.7434049249 - 0.7507113218 0.7441005707 0.7441005707 - 0.7513638735 0.7447963953 0.7447963953 - 0.7520164847 0.7454916835 0.7454916835 - 0.7526692152 0.7461783290 0.7461783290 - 0.7533218861 0.7468649745 0.7468649745 - 0.7539746165 0.7475516796 0.7475516796 - 0.7546272874 0.7482383251 0.7482383251 - 0.7552800179 0.7489249706 0.7489249706 - 0.7559326887 0.7496116757 0.7496116757 - 0.7565854192 0.7502983212 0.7502983212 - 0.7572380900 0.7509850264 0.7509850264 - 0.7578908205 0.7516716719 0.7516716719 - 0.7585409880 0.7523459196 0.7523459196 - 0.7591909766 0.7530183792 0.7530183792 - 0.7598409057 0.7536908984 0.7536908984 - 0.7604907751 0.7543634176 0.7543634176 - 0.7611407042 0.7550358772 0.7550358772 - 0.7617905736 0.7557083964 0.7557083964 - 0.7624406219 0.7563807964 0.7563807964 - 0.7630904913 0.7570533156 0.7570533156 - 0.7637404203 0.7577257752 0.7577257752 - 0.7643836737 0.7583953738 0.7583953738 - 0.7650216222 0.7590625286 0.7590625286 - 0.7656595111 0.7597296238 0.7597296238 - 0.7662972808 0.7603967190 0.7603967190 - 0.7669352293 0.7610638738 0.7610638738 - 0.7675729990 0.7617310286 0.7617310286 - 0.7682108879 0.7623981237 0.7623981237 - 0.7688487768 0.7630652785 0.7630652785 - 0.7694866061 0.7637323737 0.7637323737 - 0.7701240778 0.7643994093 0.7643994093 - 0.7707566023 0.7650648952 0.7650648952 - 0.7713890076 0.7657303810 0.7657303810 - 0.7720214725 0.7663959265 0.7663959265 - 0.7726539969 0.7670614123 0.7670614123 - 0.7732864022 0.7677268982 0.7677268982 - 0.7739189267 0.7683923841 0.7683923841 - 0.7745512724 0.7690579295 0.7690579295 - 0.7751837969 0.7697234154 0.7697234154 - 0.7758163214 0.7703889012 0.7703889012 - 0.7764492035 0.7710499763 0.7710499763 - 0.7770828009 0.7717056870 0.7717056870 - 0.7777163982 0.7723612785 0.7723612785 - 0.7783499956 0.7730169892 0.7730169892 - 0.7789835930 0.7736726999 0.7736726999 - 0.7796171904 0.7743282914 0.7743282914 - 0.7802507877 0.7749840021 0.7749840021 - 0.7808843851 0.7756395936 0.7756395936 - 0.7815179825 0.7762953043 0.7762953043 - 0.7821515799 0.7769508958 0.7769508958 - 0.7827799916 0.7775998712 0.7775998712 - 0.7834056020 0.7782453895 0.7782453895 - 0.7840312719 0.7788907886 0.7788907886 - 0.7846568823 0.7795363069 0.7795363069 - 0.7852826118 0.7801818252 0.7801818252 - 0.7859082222 0.7808272243 0.7808272243 - 0.7865338922 0.7814726830 0.7814726830 - 0.7871595025 0.7821182013 0.7821182013 - 0.7877851725 0.7827636003 0.7827636003 - 0.7884107828 0.7834091187 0.7834091187 - 0.7890290022 0.7840545177 0.7840545177 - 0.7896440029 0.7846997976 0.7846997976 - 0.7902588844 0.7853450775 0.7853450775 - 0.7908738852 0.7859904170 0.7859904170 - 0.7914888859 0.7866356969 0.7866356969 - 0.7921038866 0.7872809768 0.7872809768 - 0.7927188873 0.7879263163 0.7879263163 - 0.7933338881 0.7885715961 0.7885715961 - 0.7939488888 0.7892168760 0.7892168760 - 0.7945638895 0.7898622155 0.7898622155 - 0.7951788902 0.7905039191 0.7905039191 - 0.7957940102 0.7911430001 0.7911430001 - 0.7964090705 0.7917820811 0.7917820811 - 0.7970241904 0.7924212813 0.7924212813 - 0.7976393104 0.7930604219 0.7930604219 - 0.7982543707 0.7936995029 0.7936995029 - 0.7988696098 0.7943387032 0.7943387032 - 0.7994847298 0.7949777842 0.7949777842 - 0.8000997901 0.7956169248 0.7956169248 - 0.8007149100 0.7962561250 0.7962561250 - 0.8013291955 0.7968928814 0.7968928814 - 0.8019415736 0.7975248098 0.7975248098 - 0.8025540709 0.7981566787 0.7981566787 - 0.8031666279 0.7987886071 0.7987886071 - 0.8037791252 0.7994204760 0.7994204760 - 0.8043916225 0.8000524044 0.8000524044 - 0.8050041199 0.8006842732 0.8006842732 - 0.8056166172 0.8013160825 0.8013160825 - 0.8062291145 0.8019480109 0.8019480109 - 0.8068416119 0.8025798798 0.8025798798 - 0.8074541092 0.8032118082 0.8032118082 - 0.8080565929 0.8038352728 0.8038352728 - 0.8086578250 0.8044574857 0.8044574857 - 0.8092588782 0.8050796986 0.8050796986 - 0.8098599911 0.8057019114 0.8057019114 - 0.8104611039 0.8063241839 0.8063241839 - 0.8110622764 0.8069463968 0.8069463968 - 0.8116633892 0.8075686097 0.8075686097 - 0.8122645020 0.8081908226 0.8081908226 - 0.8128656745 0.8088130951 0.8088130951 - 0.8134667873 0.8094353080 0.8094353080 - 0.8140665889 0.8100557923 0.8100557923 - 0.8146625757 0.8106715083 0.8106715083 - 0.8152586818 0.8112871051 0.8112871051 - 0.8158547282 0.8119028211 0.8119028211 - 0.8164507151 0.8125184178 0.8125184178 - 0.8170468211 0.8131340742 0.8131340742 - 0.8176428080 0.8137496710 0.8137496710 - 0.8182387948 0.8143653870 0.8143653870 - 0.8188349009 0.8149809837 0.8149809837 - 0.8194308877 0.8155966997 0.8155966997 - 0.8200268745 0.8162122965 0.8162122965 - 0.8206235170 0.8168265820 0.8168265820 - 0.8212205768 0.8174393773 0.8174393773 - 0.8218176961 0.8180521131 0.8180521131 - 0.8224148154 0.8186649084 0.8186649084 - 0.8230118752 0.8192775846 0.8192775846 - 0.8236089945 0.8198903799 0.8198903799 - 0.8242061138 0.8205031157 0.8205031157 - 0.8248031735 0.8211159110 0.8211159110 - 0.8254002929 0.8217287064 0.8217287064 - 0.8259974122 0.8223413825 0.8223413825 - 0.8265944719 0.8229541779 0.8229541779 - 0.8271877766 0.8235660791 0.8235660791 - 0.8277773857 0.8241772056 0.8241772056 - 0.8283669949 0.8247882724 0.8247882724 - 0.8289566040 0.8253993988 0.8253993988 - 0.8295462728 0.8260105252 0.8260105252 - 0.8301358819 0.8266214728 0.8266214728 - 0.8307254910 0.8272325993 0.8272325993 - 0.8313151002 0.8278437257 0.8278437257 - 0.8319047093 0.8284547925 0.8284547925 - 0.8324943185 0.8290659189 0.8290659189 - 0.8330839276 0.8296769857 0.8296769857 - 0.8336697817 0.8302866220 0.8302866220 - 0.8342493773 0.8308938146 0.8308938146 - 0.8348289728 0.8315008879 0.8315008879 - 0.8354085088 0.8321080208 0.8321080208 - 0.8359881043 0.8327152133 0.8327152133 - 0.8365676999 0.8333222866 0.8333222866 - 0.8371472955 0.8339294195 0.8339294195 - 0.8377267718 0.8345366120 0.8345366120 - 0.8383064270 0.8351436853 0.8351436853 - 0.8388860226 0.8357508183 0.8357508183 - 0.8394654989 0.8363580108 0.8363580108 - 0.8400450945 0.8369646072 0.8369646072 - 0.8406248093 0.8375620246 0.8375620246 - 0.8412045240 0.8381593823 0.8381593823 - 0.8417841196 0.8387567997 0.8387567997 - 0.8423637748 0.8393542171 0.8393542171 - 0.8429434896 0.8399515748 0.8399515748 - 0.8435232043 0.8405489922 0.8405489922 - 0.8441029191 0.8411465287 0.8411465287 - 0.8446825743 0.8417438865 0.8417438865 - 0.8452622294 0.8423413038 0.8423413038 - 0.8458418846 0.8429387212 0.8429387212 - 0.8464215994 0.8435360789 0.8435360789 - 0.8469976187 0.8441309929 0.8441309929 - 0.8475704789 0.8447238207 0.8447238207 - 0.8481433988 0.8453165889 0.8453165889 - 0.8487163186 0.8459094167 0.8459094167 - 0.8492891788 0.8465021849 0.8465021849 - 0.8498620987 0.8470950127 0.8470950127 - 0.8504350185 0.8476877809 0.8476877809 - 0.8510079980 0.8482806087 0.8482806087 - 0.8515809178 0.8488733768 0.8488733768 - 0.8521537781 0.8494662046 0.8494662046 - 0.8527266979 0.8500589728 0.8500589728 - 0.8532996178 0.8506518006 0.8506518006 - 0.8538609147 0.8512412906 0.8512412906 - 0.8544201851 0.8518301845 0.8518301845 - 0.8549793959 0.8524190784 0.8524190784 - 0.8555387259 0.8530079126 0.8530079126 - 0.8560978770 0.8535968065 0.8535968065 - 0.8566572070 0.8541857004 0.8541857004 - 0.8572164178 0.8547745943 0.8547745943 - 0.8577756882 0.8553634882 0.8553634882 - 0.8583348989 0.8559523821 0.8559523821 - 0.8588942289 0.8565412760 0.8565412760 - 0.8594533801 0.8571302295 0.8571302295 - 0.8600127101 0.8577190042 0.8577190042 - 0.8605682254 0.8583009243 0.8583009243 - 0.8611236215 0.8588824272 0.8588824272 - 0.8616790771 0.8594639897 0.8594639897 - 0.8622344732 0.8600456119 0.8600456119 - 0.8627899289 0.8606271148 0.8606271148 - 0.8633453250 0.8612086773 0.8612086773 - 0.8639007211 0.8617901802 0.8617901802 - 0.8644561172 0.8623718023 0.8623718023 - 0.8650115132 0.8629534245 0.8629534245 - 0.8655669093 0.8635349274 0.8635349274 - 0.8661223054 0.8641164899 0.8641164899 - 0.8666777015 0.8646979928 0.8646979928 - 0.8672348261 0.8652762175 0.8652762175 - 0.8677921295 0.8658539057 0.8658539057 - 0.8683493733 0.8664317131 0.8664317131 - 0.8689066768 0.8670094013 0.8670094013 - 0.8694639206 0.8675870895 0.8675870895 - 0.8700212240 0.8681647778 0.8681647778 - 0.8705785275 0.8687425852 0.8687425852 - 0.8711357713 0.8693202734 0.8693202734 - 0.8716930747 0.8698980212 0.8698980212 - 0.8722503781 0.8704758286 0.8704758286 - 0.8728076816 0.8710535169 0.8710535169 - 0.8733649850 0.8716312051 0.8716312051 - 0.8739185929 0.8722062111 0.8722062111 - 0.8744698763 0.8727794290 0.8727794290 - 0.8750212193 0.8733525276 0.8733525276 - 0.8755723834 0.8739256859 0.8739256859 - 0.8761237264 0.8744989038 0.8744989038 - 0.8766748905 0.8750720024 0.8750720024 - 0.8772261739 0.8756452203 0.8756452203 - 0.8777773976 0.8762183785 0.8762183785 - 0.8783286810 0.8767915964 0.8767915964 - 0.8788799047 0.8773646951 0.8773646951 - 0.8794311881 0.8779379129 0.8779379129 - 0.8799824119 0.8785110712 0.8785110712 - 0.8805326223 0.8790833950 0.8790833950 - 0.8810753822 0.8796498775 0.8796498775 - 0.8816182017 0.8802164197 0.8802164197 - 0.8821610212 0.8807829022 0.8807829022 - 0.8827037811 0.8813493252 0.8813493252 - 0.8832466006 0.8819158077 0.8819158077 - 0.8837893009 0.8824822903 0.8824822903 - 0.8843321204 0.8830487728 0.8830487728 - 0.8848748803 0.8836153150 0.8836153150 - 0.8854176998 0.8841817975 0.8841817975 - 0.8859605193 0.8847482204 0.8847482204 - 0.8865032792 0.8853147030 0.8853147030 - 0.8870459795 0.8858811855 0.8858811855 - 0.8875892162 0.8864449859 0.8864449859 - 0.8881329894 0.8870056272 0.8870056272 - 0.8886767030 0.8875662088 0.8875662088 - 0.8892204762 0.8881267905 0.8881267905 - 0.8897641897 0.8886873126 0.8886873126 - 0.8903079033 0.8892478943 0.8892478943 - 0.8908516765 0.8898084760 0.8898084760 - 0.8913953900 0.8903689981 0.8903689981 - 0.8919392228 0.8909295797 0.8909295797 - 0.8924828768 0.8914902210 0.8914902210 - 0.8930265903 0.8920506835 0.8920506835 - 0.8935704231 0.8926113248 0.8926113248 - 0.8941140771 0.8931719065 0.8931719065 - 0.8946548104 0.8937309980 0.8937309980 - 0.8951931000 0.8942890167 0.8942890167 - 0.8957313895 0.8948469162 0.8948469162 - 0.8962696791 0.8954048753 0.8954048753 - 0.8968080282 0.8959628940 0.8959628940 - 0.8973463774 0.8965209126 0.8965209126 - 0.8978847265 0.8970788121 0.8970788121 - 0.8984230161 0.8976367712 0.8976367712 - 0.8989613056 0.8981947899 0.8981947899 - 0.8994995952 0.8987526894 0.8987526894 - 0.9000378847 0.8993107080 0.8993107080 - 0.9005761743 0.8998687267 0.8998687267 - 0.9011145234 0.9004266262 0.9004266262 - 0.9016473293 0.9009829760 0.9009829760 - 0.9021736979 0.9015374184 0.9015374184 - 0.9027001858 0.9020918012 0.9020918012 - 0.9032266140 0.9026463032 0.9026463032 - 0.9037529826 0.9032006860 0.9032006860 - 0.9042794108 0.9037551284 0.9037551284 - 0.9048057795 0.9043095112 0.9043095112 - 0.9053322077 0.9048638940 0.9048638940 - 0.9058586955 0.9054183960 0.9054183960 - 0.9063851237 0.9059727788 0.9059727788 - 0.9069114923 0.9065272212 0.9065272212 - 0.9074379206 0.9070816040 0.9070816040 - 0.9079642892 0.9076361060 0.9076361060 - 0.9084903002 0.9081891179 0.9081891179 - 0.9090139866 0.9087343812 0.9087343812 - 0.9095376730 0.9092797041 0.9092797041 - 0.9100614190 0.9098249078 0.9098249078 - 0.9105851054 0.9103701711 0.9103701711 - 0.9111087918 0.9109154940 0.9109154940 - 0.9116324782 0.9114606977 0.9114606977 - 0.9121562243 0.9120060205 0.9120060205 - 0.9126799107 0.9125512838 0.9125512838 - 0.9132035971 0.9130964875 0.9130964875 - 0.9137272835 0.9136418104 0.9136418104 - 0.9142510295 0.9141870737 0.9141870737 - 0.9147747755 0.9147322774 0.9147322774 - 0.9152985215 0.9152776003 0.9152776003 - 0.9158238173 0.9158192277 0.9158192277 - 0.9163501263 0.9163584709 0.9163584709 - 0.9168763757 0.9168977737 0.9168977737 - 0.9174026847 0.9174370766 0.9174370766 - 0.9179289937 0.9179763794 0.9179763794 - 0.9184553027 0.9185156226 0.9185156226 - 0.9189816117 0.9190549254 0.9190549254 - 0.9195079207 0.9195942283 0.9195942283 - 0.9200342298 0.9201334715 0.9201334715 - 0.9205604792 0.9206727743 0.9206727743 - 0.9210867882 0.9212120771 0.9212120771 - 0.9216129780 0.9217513800 0.9217513800 - 0.9221392870 0.9222906828 0.9222906828 - 0.9226655960 0.9228299856 0.9228299856 - 0.9231876731 0.9233682752 0.9233682752 - 0.9237092137 0.9239063859 0.9239063859 - 0.9242306948 0.9244444966 0.9244444966 - 0.9247521162 0.9249826074 0.9249826074 - 0.9252735972 0.9255207777 0.9255207777 - 0.9257950783 0.9260588884 0.9260588884 - 0.9263164997 0.9265969992 0.9265969992 - 0.9268379807 0.9271351099 0.9271351099 - 0.9273594022 0.9276732206 0.9276732206 - 0.9278808832 0.9282112718 0.9282112718 - 0.9284024239 0.9287495017 0.9287495017 - 0.9289237857 0.9292876124 0.9292876124 - 0.9294453263 0.9298257232 0.9298257232 - 0.9299668074 0.9303637743 0.9303637743 - 0.9304789901 0.9308947921 0.9308947921 - 0.9309902191 0.9314250946 0.9314250946 - 0.9315013885 0.9319552779 0.9319552779 - 0.9320126176 0.9324855804 0.9324855804 - 0.9325237274 0.9330158234 0.9330158234 - 0.9330348969 0.9335461259 0.9335461259 - 0.9335461259 0.9340763092 0.9340763092 - 0.9340572953 0.9346066117 0.9346066117 - 0.9345685244 0.9351367950 0.9351367950 - 0.9350796938 0.9356669784 0.9356669784 - 0.9355909228 0.9361972809 0.9361972809 - 0.9361019731 0.9367275238 0.9367275238 - 0.9366132021 0.9372578263 0.9372578263 - 0.9371243715 0.9377880096 0.9377880096 - 0.9376301169 0.9383096099 0.9383096099 - 0.9381334186 0.9388273954 0.9388273954 - 0.9386367202 0.9393453002 0.9393453002 - 0.9391400218 0.9398630857 0.9398630857 - 0.9396432042 0.9403809905 0.9403809905 - 0.9401465058 0.9408987761 0.9408987761 - 0.9406498075 0.9414166808 0.9414166808 - 0.9411531091 0.9419345260 0.9419345260 - 0.9416564107 0.9424523115 0.9424523115 - 0.9421597123 0.9429702163 0.9429702163 - 0.9426630139 0.9434880018 0.9434880018 - 0.9431663156 0.9440059066 0.9440059066 - 0.9436694980 0.9445236921 0.9445236921 - 0.9441727996 0.9450414777 0.9450414777 - 0.9446740746 0.9455562234 0.9455562234 - 0.9451695085 0.9460619092 0.9460619092 - 0.9456648827 0.9465677142 0.9465677142 - 0.9461603761 0.9470734000 0.9470734000 - 0.9466558099 0.9475792050 0.9475792050 - 0.9471513033 0.9480848908 0.9480848908 - 0.9476466775 0.9485906959 0.9485906959 - 0.9481421709 0.9490963817 0.9490963817 - 0.9486376047 0.9496021867 0.9496021867 - 0.9491330981 0.9501078725 0.9501078725 - 0.9496284723 0.9506136775 0.9506136775 - 0.9501240253 0.9511194229 0.9511194229 - 0.9506193995 0.9516252279 0.9516252279 - 0.9511148930 0.9521309137 0.9521309137 - 0.9516103268 0.9526367188 0.9526367188 - 0.9520999789 0.9531362057 0.9531362057 - 0.9525858164 0.9536315799 0.9536315799 - 0.9530715942 0.9541268945 0.9541268945 - 0.9535574913 0.9546223283 0.9546223283 - 0.9540433288 0.9551175833 0.9551175833 - 0.9545291066 0.9556128979 0.9556128979 - 0.9550148845 0.9561082721 0.9561082721 - 0.9555007219 0.9566035867 0.9566035867 - 0.9559866190 0.9570990205 0.9570990205 - 0.9564723969 0.9575942755 0.9575942755 - 0.9569581747 0.9580897093 0.9580897093 - 0.9574440122 0.9585850239 0.9585850239 - 0.9579297900 0.9590802789 0.9590802789 - 0.9584156871 0.9595757127 0.9595757127 - 0.9589015245 0.9600710273 0.9600710273 - 0.9593812823 0.9605569243 0.9605569243 - 0.9598584771 0.9610384107 0.9610384107 - 0.9603356123 0.9615200162 0.9615200162 - 0.9608128071 0.9620016217 0.9620016217 - 0.9612900019 0.9624832273 0.9624832273 - 0.9617670774 0.9629647136 0.9629647136 - 0.9622442722 0.9634463191 0.9634463191 - 0.9627214074 0.9639279246 0.9639279246 - 0.9631986022 0.9644094706 0.9644094706 - 0.9636756778 0.9648910165 0.9648910165 - 0.9641528726 0.9653726220 0.9653726220 - 0.9646300077 0.9658542275 0.9658542275 - 0.9651072025 0.9663357735 0.9663357735 - 0.9655842781 0.9668173194 0.9668173194 - 0.9660614729 0.9672989249 0.9672989249 - 0.9665307999 0.9677714705 0.9677714705 - 0.9669935107 0.9682365060 0.9682365060 - 0.9674562216 0.9687014818 0.9687014818 - 0.9679188728 0.9691665173 0.9691665173 - 0.9683815837 0.9696314931 0.9696314931 - 0.9688442945 0.9700965285 0.9700965285 - 0.9693070054 0.9705615044 0.9705615044 - 0.9697697163 0.9710264802 0.9710264802 - 0.9702324271 0.9714915156 0.9714915156 - 0.9706950188 0.9719564915 0.9719564915 - 0.9711577296 0.9724215269 0.9724215269 - 0.9716203809 0.9728865027 0.9728865027 - 0.9720830917 0.9733514786 0.9733514786 - 0.9725458026 0.9738165140 0.9738165140 - 0.9730085135 0.9742814898 0.9742814898 - 0.9734690189 0.9747437239 0.9747437239 - 0.9739159942 0.9751892090 0.9751892090 - 0.9743630886 0.9756346941 0.9756346941 - 0.9748101830 0.9760801792 0.9760801792 - 0.9752572775 0.9765257835 0.9765257835 - 0.9757043719 0.9769713283 0.9769713283 - 0.9761515260 0.9774168134 0.9774168134 - 0.9765985012 0.9778622985 0.9778622985 - 0.9770455956 0.9783079028 0.9783079028 - 0.9774926901 0.9787533879 0.9787533879 - 0.9779397845 0.9791988730 0.9791988730 - 0.9783868790 0.9796444178 0.9796444178 - 0.9788339734 0.9800899029 0.9800899029 - 0.9792810082 0.9805355072 0.9805355072 - 0.9797281027 0.9809809923 0.9809809923 - 0.9801751971 0.9814264774 0.9814264774 - 0.9806160927 0.9818611741 0.9818611741 - 0.9810506105 0.9822847247 0.9822847247 - 0.9814851284 0.9827082753 0.9827082753 - 0.9819195271 0.9831318855 0.9831318855 - 0.9823539853 0.9835553765 0.9835553765 - 0.9827885032 0.9839789867 0.9839789867 - 0.9832230210 0.9844024777 0.9844024777 - 0.9836574793 0.9848260880 0.9848260880 - 0.9840919971 0.9852495790 0.9852495790 - 0.9845265150 0.9856731892 0.9856731892 - 0.9849609733 0.9860966802 0.9860966802 - 0.9853954911 0.9865202904 0.9865202904 - 0.9858298898 0.9869437814 0.9869437814 - 0.9862644076 0.9873673916 0.9873673916 - 0.9866989255 0.9877910018 0.9877910018 - 0.9871333838 0.9882144928 0.9882144928 - 0.9875543714 0.9886164069 0.9886164069 - 0.9879658818 0.9890031219 0.9890031219 - 0.9883775115 0.9893897772 0.9893897772 - 0.9887890220 0.9897766113 0.9897766113 - 0.9892005920 0.9901633263 0.9901633263 - 0.9896121025 0.9905501008 0.9905501008 - 0.9900236726 0.9909368157 0.9909368157 - 0.9904351830 0.9913235903 0.9913235903 - 0.9908468127 0.9917103052 0.9917103052 - 0.9912583232 0.9920970201 0.9920970201 - 0.9916698933 0.9924837947 0.9924837947 - 0.9920814037 0.9928705096 0.9928705096 - 0.9924929738 0.9932572842 0.9932572842 - 0.9929044843 0.9936439991 0.9936439991 - 0.9933161139 0.9940307736 0.9940307736 - 0.9937276244 0.9944174886 0.9944174886 - 0.9941204190 0.9947785139 0.9947785139 - 0.9944878221 0.9951047897 0.9951047897 - 0.9948552847 0.9954311848 0.9954311848 - 0.9952228069 0.9957575202 0.9957575202 - 0.9955903292 0.9960837960 0.9960837960 - 0.9959577918 0.9964101911 0.9964101911 - 0.9963251948 0.9967365265 0.9967365265 - 0.9966927171 0.9970629215 0.9970629215 - 0.9970601797 0.9973891973 0.9973891973 - 0.9974277020 0.9977155924 0.9977155924 - 0.9977951050 0.9980419278 0.9980419278 - 0.9981626272 0.9983683228 0.9983683228 - 0.9985300899 0.9986945987 0.9986945987 - 0.9988976121 0.9990209937 0.9990209937 - 0.9992650151 0.9993473291 0.9993473291 - 0.9996324778 0.9996737242 0.9996737242 - 1.0000000000 1.0000000000 1.0000000000 -} +Version 1 +From 0.0 1.0 +Length 1024 +Components 3 +{ + 0.0000000000 0.0000000000 0.0000000000 + 0.0088124285 0.0079260534 0.0079260534 + 0.0173182599 0.0154689904 0.0154689904 + 0.0250097904 0.0223134402 0.0223134402 + 0.0318675414 0.0285592694 0.0285592694 + 0.0382458083 0.0343292207 0.0343292207 + 0.0440907888 0.0397338495 0.0397338495 + 0.0496913791 0.0447938815 0.0447938815 + 0.0549593382 0.0496262200 0.0496262200 + 0.0600188002 0.0542288311 0.0542288311 + 0.0648257434 0.0586788617 0.0586788617 + 0.0694660619 0.0629591122 0.0629591122 + 0.0739658996 0.0670659617 0.0670659617 + 0.0782863200 0.0710892603 0.0710892603 + 0.0824507698 0.0749912113 0.0749912113 + 0.0864929035 0.0788405612 0.0788405612 + 0.0904697403 0.0825625733 0.0825625733 + 0.0943273082 0.0861979574 0.0861979574 + 0.0981024578 0.0897707865 0.0897707865 + 0.1018401012 0.0932211131 0.0932211131 + 0.1054691002 0.0966563523 0.0966563523 + 0.1090489030 0.1000294015 0.1000294015 + 0.1125440001 0.1033191979 0.1033191979 + 0.1159221008 0.1065331027 0.1065331027 + 0.1192713007 0.1097005978 0.1097005978 + 0.1225581989 0.1128138974 0.1128138974 + 0.1257860959 0.1158875003 0.1158875003 + 0.1289588958 0.1189555973 0.1189555973 + 0.1321441978 0.1219801009 0.1219801009 + 0.1352636069 0.1249566972 0.1249566972 + 0.1383159012 0.1278752983 0.1278752983 + 0.1413581967 0.1307422966 0.1307422966 + 0.1443576962 0.1336016953 0.1336016953 + 0.1472717971 0.1364331990 0.1364331990 + 0.1501350999 0.1392025054 0.1392025054 + 0.1529815942 0.1419408023 0.1419408023 + 0.1558008045 0.1446657926 0.1446657926 + 0.1586032957 0.1473688930 0.1473688930 + 0.1613503993 0.1500454992 0.1500454992 + 0.1640668958 0.1526847035 0.1526847035 + 0.1667777002 0.1552831978 0.1552831978 + 0.1694519967 0.1578582972 0.1578582972 + 0.1720923930 0.1604107022 0.1604107022 + 0.1746968031 0.1629472971 0.1629472971 + 0.1772591025 0.1654641926 0.1654641926 + 0.1797759980 0.1679515988 0.1679515988 + 0.1822800934 0.1704009026 0.1704009026 + 0.1847687066 0.1728225946 0.1728225946 + 0.1872310042 0.1752288938 0.1752288938 + 0.1896561980 0.1776168942 0.1776168942 + 0.1920384020 0.1799840033 0.1799840033 + 0.1944015026 0.1823205948 0.1823205948 + 0.1967525929 0.1846179962 0.1846179962 + 0.1990911067 0.1869053990 0.1869053990 + 0.2014010996 0.1891890019 0.1891890019 + 0.2037014961 0.1914498061 0.1914498061 + 0.2060168982 0.1936779022 0.1936779022 + 0.2083324045 0.1958771050 0.1958771050 + 0.2106439024 0.1980604976 0.1980604976 + 0.2129282951 0.2002373040 0.2002373040 + 0.2151844054 0.2024063021 0.2024063021 + 0.2174350023 0.2045633942 0.2045633942 + 0.2196764946 0.2067063004 0.2067063004 + 0.2219022959 0.2088321000 0.2088321000 + 0.2241204977 0.2109334022 0.2109334022 + 0.2263171971 0.2130157053 0.2130157053 + 0.2284786999 0.2150927931 0.2150927931 + 0.2306168973 0.2171598971 0.2171598971 + 0.2327332944 0.2192052007 0.2192052007 + 0.2348272949 0.2212346047 0.2212346047 + 0.2369115055 0.2232697010 0.2232697010 + 0.2389767021 0.2253111005 0.2253111005 + 0.2410169989 0.2273571938 0.2273571938 + 0.2430370003 0.2294019014 0.2294019014 + 0.2450459003 0.2314381003 0.2314381003 + 0.2470474988 0.2334637046 0.2334637046 + 0.2490427047 0.2354702950 0.2354702950 + 0.2510350049 0.2374535054 0.2374535054 + 0.2530182004 0.2394181937 0.2394181937 + 0.2549867928 0.2413693964 0.2413693964 + 0.2569330931 0.2433117926 0.2433117926 + 0.2588599026 0.2452446073 0.2452446073 + 0.2607747018 0.2471683025 0.2471683025 + 0.2626813948 0.2490822971 0.2490822971 + 0.2645846903 0.2509844899 0.2509844899 + 0.2664898038 0.2528660893 0.2528660893 + 0.2683837116 0.2547389865 0.2547389865 + 0.2702631950 0.2566035986 0.2566035986 + 0.2721140981 0.2584480941 0.2584480941 + 0.2739478052 0.2602775097 0.2602775097 + 0.2757706046 0.2620961070 0.2620961070 + 0.2775853872 0.2639020085 0.2639020085 + 0.2794038057 0.2656953037 0.2656953037 + 0.2812230885 0.2674824893 0.2674824893 + 0.2830418050 0.2692643106 0.2692643106 + 0.2848351002 0.2710475922 0.2710475922 + 0.2866136134 0.2728300095 0.2728300095 + 0.2883686125 0.2746050954 0.2746050954 + 0.2901135087 0.2763741016 0.2763741016 + 0.2918516994 0.2781391144 0.2781391144 + 0.2935769856 0.2798882127 0.2798882127 + 0.2953031063 0.2816232145 0.2816232145 + 0.2970305085 0.2833353877 0.2833353877 + 0.2987627089 0.2850272954 0.2850272954 + 0.3004961908 0.2867107093 0.2867107093 + 0.3022179008 0.2883763909 0.2883763909 + 0.3039326966 0.2900390029 0.2900390029 + 0.3056339025 0.2916957140 0.2916957140 + 0.3073236048 0.2933523953 0.2933523953 + 0.3090057075 0.2950091958 0.2950091958 + 0.3106757104 0.2966647148 0.2966647148 + 0.3123432994 0.2983194888 0.2983194888 + 0.3140107989 0.2999661863 0.2999661863 + 0.3156793118 0.3016082942 0.3016082942 + 0.3173511028 0.3032361865 0.3032361865 + 0.3190175891 0.3048543036 0.3048543036 + 0.3206757009 0.3064570129 0.3064570129 + 0.3223229051 0.3080512881 0.3080512881 + 0.3239594102 0.3096374869 0.3096374869 + 0.3255836964 0.3112207055 0.3112207055 + 0.3271996081 0.3128018975 0.3128018975 + 0.3288047910 0.3143788874 0.3143788874 + 0.3304041922 0.3159537017 0.3159537017 + 0.3320013881 0.3175298870 0.3175298870 + 0.3335976005 0.3191067874 0.3191067874 + 0.3351888955 0.3206779063 0.3206779063 + 0.3367778063 0.3222463131 0.3222463131 + 0.3383550048 0.3238070011 0.3238070011 + 0.3399255872 0.3253633976 0.3253633976 + 0.3414827883 0.3269123137 0.3269123137 + 0.3430297971 0.3284555078 0.3284555078 + 0.3445636928 0.3299894929 0.3299894929 + 0.3460822105 0.3315128088 0.3315128088 + 0.3475958109 0.3330293894 0.3330293894 + 0.3490990102 0.3345321119 0.3345321119 + 0.3506017923 0.3360328078 0.3360328078 + 0.3521026969 0.3375233114 0.3375233114 + 0.3536036015 0.3390137851 0.3390137851 + 0.3551059067 0.3404915035 0.3404915035 + 0.3566082120 0.3419683874 0.3419683874 + 0.3580994904 0.3434371054 0.3434371054 + 0.3595857024 0.3449020982 0.3449020982 + 0.3610610068 0.3463630974 0.3463630974 + 0.3625203967 0.3478182852 0.3478182852 + 0.3639777005 0.3492721915 0.3492721915 + 0.3654133976 0.3507139981 0.3507139981 + 0.3668490946 0.3521556854 0.3521556854 + 0.3682768941 0.3535880148 0.3535880148 + 0.3697017133 0.3550168872 0.3550168872 + 0.3711271882 0.3564417064 0.3564417064 + 0.3725540042 0.3578588068 0.3578588068 + 0.3739807904 0.3592759073 0.3592759073 + 0.3754133880 0.3606815040 0.3606815040 + 0.3768466115 0.3620859981 0.3620859981 + 0.3782753050 0.3634850979 0.3634850979 + 0.3796978891 0.3648771048 0.3648771048 + 0.3811204135 0.3662689924 0.3662689924 + 0.3825325072 0.3676514030 0.3676514030 + 0.3839435875 0.3690330088 0.3690330088 + 0.3853504956 0.3704110980 0.3704110980 + 0.3867498934 0.3717832863 0.3717832863 + 0.3881492913 0.3731555045 0.3731555045 + 0.3895345032 0.3745233119 0.3745233119 + 0.3909151852 0.3758895993 0.3758895993 + 0.3922944069 0.3772548139 0.3772548139 + 0.3936628103 0.3786112964 0.3786112964 + 0.3950310946 0.3799678087 0.3799678087 + 0.3963941932 0.3813194036 0.3813194036 + 0.3977501988 0.3826650083 0.3826650083 + 0.3991062939 0.3840106130 0.3840106130 + 0.4004457891 0.3853495121 0.3853495121 + 0.4017783105 0.3866856992 0.3866856992 + 0.4031107128 0.3880218863 0.3880218863 + 0.4044226110 0.3893507123 0.3893507123 + 0.4057329893 0.3906790018 0.3906790018 + 0.4070420861 0.3920061886 0.3920061886 + 0.4083415866 0.3933256865 0.3933256865 + 0.4096410871 0.3946452141 0.3946452141 + 0.4109367132 0.3959634900 0.3959634900 + 0.4122210145 0.3972786963 0.3972786963 + 0.4135052860 0.3985939026 0.3985939026 + 0.4147841930 0.3999078870 0.3999078870 + 0.4160529971 0.4012196958 0.4012196958 + 0.4173217118 0.4025315046 0.4025315046 + 0.4185839891 0.4038358033 0.4038358033 + 0.4198360145 0.4051288962 0.4051288962 + 0.4210878909 0.4064218998 0.4064218998 + 0.4223288000 0.4077093899 0.4077093899 + 0.4235515893 0.4089880884 0.4089880884 + 0.4247742891 0.4102669060 0.4102669060 + 0.4259923995 0.4115405083 0.4115405083 + 0.4272009134 0.4128041863 0.4128041863 + 0.4284093082 0.4140678942 0.4140678942 + 0.4296151102 0.4153267145 0.4153267145 + 0.4308117032 0.4165697992 0.4165697992 + 0.4320083857 0.4178129137 0.4178129137 + 0.4332041144 0.4190546870 0.4190546870 + 0.4343877137 0.4202808142 0.4202808142 + 0.4355713129 0.4215069115 0.4215069115 + 0.4367547929 0.4227330089 0.4227330089 + 0.4379391968 0.4239530861 0.4239530861 + 0.4391236901 0.4251722991 0.4251722991 + 0.4403083026 0.4263913929 0.4263913929 + 0.4414936900 0.4276081026 0.4276081026 + 0.4426796138 0.4288231134 0.4288231134 + 0.4438655972 0.4300382137 0.4300382137 + 0.4450480044 0.4312498868 0.4312498868 + 0.4462217093 0.4324539900 0.4324539900 + 0.4473954141 0.4336580038 0.4336580038 + 0.4485690892 0.4348619878 0.4348619878 + 0.4497332871 0.4360530078 0.4360530078 + 0.4508967996 0.4372431040 0.4372431040 + 0.4520604014 0.4384332895 0.4384332895 + 0.4532183111 0.4396133125 0.4396133125 + 0.4543708861 0.4407838881 0.4407838881 + 0.4555234909 0.4419544041 0.4419544041 + 0.4566758871 0.4431242943 0.4431242943 + 0.4578221142 0.4442783892 0.4442783892 + 0.4589683115 0.4454325140 0.4454325140 + 0.4601145089 0.4465866089 0.4465866089 + 0.4612562060 0.4477371871 0.4477371871 + 0.4623934925 0.4488846064 0.4488846064 + 0.4635308087 0.4500319064 0.4500319064 + 0.4646682143 0.4511792958 0.4511792958 + 0.4657953978 0.4523159862 0.4523159862 + 0.4669215977 0.4534518123 0.4534518123 + 0.4680477977 0.4545876086 0.4545876086 + 0.4691722989 0.4557209909 0.4557209909 + 0.4702917933 0.4568476975 0.4568476975 + 0.4714111984 0.4579744935 0.4579744935 + 0.4725306928 0.4591012895 0.4591012895 + 0.4736500978 0.4602225125 0.4602225125 + 0.4747695029 0.4613394141 0.4613394141 + 0.4758889973 0.4624564052 0.4624564052 + 0.4770084023 0.4635733068 0.4635733068 + 0.4781275094 0.4646795094 0.4646795094 + 0.4792464077 0.4657829106 0.4657829106 + 0.4803653955 0.4668863118 0.4668863118 + 0.4814842939 0.4679895937 0.4679895937 + 0.4825977981 0.4690910876 0.4690910876 + 0.4837110043 0.4701924026 0.4701924026 + 0.4848242104 0.4712938070 0.4712938070 + 0.4859367907 0.4723945856 0.4723945856 + 0.4870401919 0.4734885097 0.4734885097 + 0.4881435037 0.4745824933 0.4745824933 + 0.4892467856 0.4756763875 0.4756763875 + 0.4903483987 0.4767695963 0.4767695963 + 0.4914352894 0.4778566062 0.4778566062 + 0.4925222099 0.4789434969 0.4789434969 + 0.4936090112 0.4800305068 0.4800305068 + 0.4946955144 0.4811173975 0.4811173975 + 0.4957770109 0.4822036028 0.4822036028 + 0.4968583882 0.4832898080 0.4832898080 + 0.4979399145 0.4843761027 0.4843761027 + 0.4990214109 0.4854621887 0.4854621887 + 0.5000975728 0.4865370989 0.4865370989 + 0.5011737943 0.4876120090 0.4876120090 + 0.5022500157 0.4886868894 0.4886868894 + 0.5033261776 0.4897617996 0.4897617996 + 0.5043833256 0.4908238053 0.4908238053 + 0.5054367781 0.4918834865 0.4918834865 + 0.5064904094 0.4929431975 0.4929431975 + 0.5075439811 0.4940029085 0.4940029085 + 0.5085870028 0.4950586855 0.4950586855 + 0.5096237063 0.4961124063 0.4961124063 + 0.5106604099 0.4971660078 0.4971660078 + 0.5116971135 0.4982196093 0.4982196093 + 0.5127326250 0.4992741942 0.4992741942 + 0.5137655139 0.5003306270 0.5003306270 + 0.5147984028 0.5013868809 0.5013868809 + 0.5158312917 0.5024433136 0.5024433136 + 0.5168641806 0.5034996271 0.5034996271 + 0.5178936124 0.5045477748 0.5045477748 + 0.5189229250 0.5055958033 0.5055958033 + 0.5199521780 0.5066437721 0.5066437721 + 0.5209816098 0.5076918006 0.5076918006 + 0.5220068097 0.5087348819 0.5087348819 + 0.5230284929 0.5097740889 0.5097740889 + 0.5240502954 0.5108131766 0.5108131766 + 0.5250719786 0.5118523240 0.5118523240 + 0.5260933042 0.5128911138 0.5128911138 + 0.5271025896 0.5139240026 0.5139240026 + 0.5281118751 0.5149567723 0.5149567723 + 0.5291212201 0.5159897208 0.5159897208 + 0.5301305056 0.5170226097 0.5170226097 + 0.5311338902 0.5180504918 0.5180504918 + 0.5321304202 0.5190727711 0.5190727711 + 0.5331268907 0.5200952291 0.5200952291 + 0.5341234207 0.5211176276 0.5211176276 + 0.5351197720 0.5221399069 0.5221399069 + 0.5361112952 0.5231561065 0.5231561065 + 0.5371016860 0.5241708755 0.5241708755 + 0.5380920172 0.5251858234 0.5251858234 + 0.5390822887 0.5262005925 0.5262005925 + 0.5400725007 0.5272145271 0.5272145271 + 0.5410608053 0.5282195807 0.5282195807 + 0.5420491099 0.5292246938 0.5292246938 + 0.5430374742 0.5302299261 0.5302299261 + 0.5440257788 0.5312349796 0.5312349796 + 0.5450109243 0.5322359800 0.5322359800 + 0.5459877849 0.5332267880 0.5332267880 + 0.5469647050 0.5342177153 0.5342177153 + 0.5479416251 0.5352085233 0.5352085233 + 0.5489184856 0.5361993909 0.5361993909 + 0.5498880744 0.5371838212 0.5371838212 + 0.5508465767 0.5381588936 0.5381588936 + 0.5518051982 0.5391340256 0.5391340256 + 0.5527637005 0.5401090980 0.5401090980 + 0.5537223220 0.5410841703 0.5410841703 + 0.5546782017 0.5420532823 0.5420532823 + 0.5556306839 0.5430148244 0.5430148244 + 0.5565832853 0.5439764261 0.5439764261 + 0.5575358272 0.5449379086 0.5449379086 + 0.5584883094 0.5458995104 0.5458995104 + 0.5594409704 0.5468540192 0.5468540192 + 0.5603938103 0.5477982163 0.5477982163 + 0.5613465905 0.5487424135 0.5487424135 + 0.5622994900 0.5496866107 0.5496866107 + 0.5632522702 0.5506308079 0.5506308079 + 0.5642018914 0.5515701175 0.5515701175 + 0.5651431084 0.5524966717 0.5524966717 + 0.5660842061 0.5534232855 0.5534232855 + 0.5670254230 0.5543497801 0.5543497801 + 0.5679665804 0.5552763939 0.5552763939 + 0.5689064860 0.5562021732 0.5562021732 + 0.5698277950 0.5571172237 0.5571172237 + 0.5707491040 0.5580322146 0.5580322146 + 0.5716704130 0.5589473248 0.5589473248 + 0.5725917220 0.5598623157 0.5598623157 + 0.5735129714 0.5607773066 0.5607773066 + 0.5744280815 0.5616865754 0.5616865754 + 0.5753415227 0.5625941753 0.5625941753 + 0.5762547851 0.5635017753 0.5635017753 + 0.5771682262 0.5644093752 0.5644093752 + 0.5780814886 0.5653169751 0.5653169751 + 0.5789892077 0.5662230849 0.5662230849 + 0.5798879862 0.5671266913 0.5671266913 + 0.5807867050 0.5680304170 0.5680304170 + 0.5816854835 0.5689340234 0.5689340234 + 0.5825843215 0.5698376894 0.5698376894 + 0.5834829807 0.5707412958 0.5707412958 + 0.5843644738 0.5716376901 0.5716376901 + 0.5852444768 0.5725334883 0.5725334883 + 0.5861244798 0.5734294057 0.5734294057 + 0.5870044827 0.5743252039 0.5743252039 + 0.5878844857 0.5752210021 0.5752210021 + 0.5887588263 0.5761132240 0.5761132240 + 0.5896230936 0.5769993067 0.5769993067 + 0.5904874802 0.5778853893 0.5778853893 + 0.5913518071 0.5787714124 0.5787714124 + 0.5922161937 0.5796574950 0.5796574950 + 0.5930805206 0.5805435777 0.5805435777 + 0.5939381719 0.5814265013 0.5814265013 + 0.5947932005 0.5823081732 0.5823081732 + 0.5956482291 0.5831899047 0.5831899047 + 0.5965033174 0.5840715170 0.5840715170 + 0.5973582864 0.5849531889 0.5849531889 + 0.5982133150 0.5858349204 0.5858349204 + 0.5990638733 0.5867123008 0.5867123008 + 0.5999143124 0.5875896811 0.5875896811 + 0.6007646918 0.5884671211 0.5884671211 + 0.6016150713 0.5893443823 0.5893443823 + 0.6024655104 0.5902218223 0.5902218223 + 0.6033152938 0.5910989046 0.5910989046 + 0.6041615009 0.5919741988 0.5919741988 + 0.6050077081 0.5928496122 0.5928496122 + 0.6058537960 0.5937249064 0.5937249064 + 0.6067000031 0.5946002007 0.5946002007 + 0.6075462103 0.5954756141 0.5954756141 + 0.6083909273 0.5963500142 0.5963500142 + 0.6092299223 0.5972210169 0.5972210169 + 0.6100689769 0.5980920792 0.5980920792 + 0.6109080911 0.5989630818 0.5989630818 + 0.6117472053 0.5998342037 0.5998342037 + 0.6125863194 0.6007053256 0.6007053256 + 0.6134225726 0.6015741229 0.6015741229 + 0.6142470241 0.6024342179 0.6024342179 + 0.6150714755 0.6032943130 0.6032943130 + 0.6158959866 0.6041544080 0.6041544080 + 0.6167204976 0.6050143838 0.6050143838 + 0.6175450087 0.6058744788 0.6058744788 + 0.6183692813 0.6067342162 0.6067342162 + 0.6191921234 0.6075887084 0.6075887084 + 0.6200149059 0.6084432006 0.6084432006 + 0.6208376884 0.6092978120 0.6092978120 + 0.6216604710 0.6101523042 0.6101523042 + 0.6224833131 0.6110067964 0.6110067964 + 0.6233062148 0.6118612885 0.6118612885 + 0.6241316199 0.6127145290 0.6127145290 + 0.6249575019 0.6135674119 0.6135674119 + 0.6257833242 0.6144202948 0.6144202948 + 0.6266092062 0.6152731776 0.6152731776 + 0.6274350882 0.6161261201 0.6161261201 + 0.6282610297 0.6169791222 0.6169791222 + 0.6290842295 0.6178271174 0.6178271174 + 0.6299049258 0.6186708808 0.6186708808 + 0.6307256222 0.6195145845 0.6195145845 + 0.6315463781 0.6203582883 0.6203582883 + 0.6323670745 0.6212021112 0.6212021112 + 0.6331877708 0.6220458150 0.6220458150 + 0.6340078712 0.6228886247 0.6228886247 + 0.6348218918 0.6237223148 0.6237223148 + 0.6356359124 0.6245561242 0.6245561242 + 0.6364498734 0.6253898740 0.6253898740 + 0.6372638941 0.6262236238 0.6262236238 + 0.6380779147 0.6270573735 0.6270573735 + 0.6388918757 0.6278911829 0.6278911829 + 0.6397082210 0.6287242174 0.6287242174 + 0.6405261755 0.6295565963 0.6295565963 + 0.6413441896 0.6303889751 0.6303889751 + 0.6421622038 0.6312214136 0.6312214136 + 0.6429802775 0.6320537925 0.6320537925 + 0.6437982917 0.6328861713 0.6328861713 + 0.6446163058 0.6337186098 0.6337186098 + 0.6454324722 0.6345450282 0.6345450282 + 0.6462485194 0.6353709102 0.6353709102 + 0.6470645070 0.6361969113 0.6361969113 + 0.6478806138 0.6370227933 0.6370227933 + 0.6486966014 0.6378486753 0.6378486753 + 0.6495125890 0.6386746168 0.6386746168 + 0.6503264904 0.6394987106 0.6394987106 + 0.6511304975 0.6403145790 0.6403145790 + 0.6519343853 0.6411305070 0.6411305070 + 0.6527382731 0.6419463754 0.6419463754 + 0.6535422206 0.6427623034 0.6427623034 + 0.6543462276 0.6435781717 0.6435781717 + 0.6551501155 0.6443940997 0.6443940997 + 0.6559516191 0.6452056170 0.6452056170 + 0.6567478776 0.6460080147 0.6460080147 + 0.6575443149 0.6468102932 0.6468102932 + 0.6583405733 0.6476125717 0.6476125717 + 0.6591368914 0.6484150290 0.6484150290 + 0.6599332094 0.6492173076 0.6492173076 + 0.6607295871 0.6500195861 0.6500195861 + 0.6615231037 0.6508170962 0.6508170962 + 0.6623113751 0.6516057849 0.6516057849 + 0.6630997062 0.6523944736 0.6523944736 + 0.6638879776 0.6531832218 0.6531832218 + 0.6646761894 0.6539719105 0.6539719105 + 0.6654645205 0.6547605991 0.6547605991 + 0.6662527919 0.6555492878 0.6555492878 + 0.6670367718 0.6563370228 0.6563370228 + 0.6678087711 0.6571220756 0.6571220756 + 0.6685808897 0.6579073071 0.6579073071 + 0.6693528891 0.6586924195 0.6586924195 + 0.6701248884 0.6594774723 0.6594774723 + 0.6708970070 0.6602627039 0.6602627039 + 0.6716690063 0.6610478163 0.6610478163 + 0.6724401712 0.6618329883 0.6618329883 + 0.6731992960 0.6626194119 0.6626194119 + 0.6739584208 0.6634057760 0.6634057760 + 0.6747174859 0.6641921997 0.6641921997 + 0.6754766703 0.6649786234 0.6649786234 + 0.6762357950 0.6657649875 0.6657649875 + 0.6769949198 0.6665514112 0.6665514112 + 0.6777541041 0.6673377752 0.6673377752 + 0.6785085201 0.6681154966 0.6681154966 + 0.6792613268 0.6688904762 0.6688904762 + 0.6800141931 0.6696655154 0.6696655154 + 0.6807669997 0.6704404950 0.6704404950 + 0.6815198064 0.6712154746 0.6712154746 + 0.6822726727 0.6719905138 0.6719905138 + 0.6830254793 0.6727654934 0.6727654934 + 0.6837748289 0.6735368967 0.6735368967 + 0.6845164895 0.6743007898 0.6743007898 + 0.6852582097 0.6750648022 0.6750648022 + 0.6859998703 0.6758286953 0.6758286953 + 0.6867415905 0.6765927076 0.6765927076 + 0.6874831915 0.6773566008 0.6773566008 + 0.6882249117 0.6781204939 0.6781204939 + 0.6889665723 0.6788845062 0.6788845062 + 0.6897013187 0.6796460152 0.6796460152 + 0.6904339194 0.6804066896 0.6804066896 + 0.6911664009 0.6811674833 0.6811674833 + 0.6918990016 0.6819282770 0.6819282770 + 0.6926314831 0.6826890111 0.6826890111 + 0.6933640242 0.6834498048 0.6834498048 + 0.6940966249 0.6842104793 0.6842104793 + 0.6948286295 0.6849713922 0.6849713922 + 0.6955537796 0.6857334971 0.6857334971 + 0.6962791085 0.6864956021 0.6864956021 + 0.6970043778 0.6872575879 0.6872575879 + 0.6977297068 0.6880196929 0.6880196929 + 0.6984549761 0.6887817979 0.6887817979 + 0.6991803050 0.6895439029 0.6895439029 + 0.6999055743 0.6903060079 0.6903060079 + 0.7006272078 0.6910654902 0.6910654902 + 0.7013388276 0.6918187141 0.6918187141 + 0.7020503879 0.6925718188 0.6925718188 + 0.7027621269 0.6933249235 0.6933249235 + 0.7034736872 0.6940780282 0.6940780282 + 0.7041853070 0.6948311925 0.6948311925 + 0.7048969865 0.6955842972 0.6955842972 + 0.7056086063 0.6963374019 0.6963374019 + 0.7063143849 0.6970837116 0.6970837116 + 0.7070090175 0.6978172064 0.6978172064 + 0.7077037096 0.6985507011 0.6985507011 + 0.7083982825 0.6992840767 0.6992840767 + 0.7090929151 0.7000175714 0.7000175714 + 0.7097874880 0.7007511258 0.7007511258 + 0.7104821205 0.7014846206 0.7014846206 + 0.7111766934 0.7022181153 0.7022181153 + 0.7118694186 0.7029485703 0.7029485703 + 0.7125568986 0.7036715150 0.7036715150 + 0.7132444978 0.7043945193 0.7043945193 + 0.7139320970 0.7051174045 0.7051174045 + 0.7146195769 0.7058404088 0.7058404088 + 0.7153071761 0.7065634131 0.7065634131 + 0.7159947753 0.7072862983 0.7072862983 + 0.7166823745 0.7080093026 0.7080093026 + 0.7173694968 0.7087321281 0.7087321281 + 0.7180529237 0.7094535828 0.7094535828 + 0.7187362909 0.7101749778 0.7101749778 + 0.7194197178 0.7108964920 0.7108964920 + 0.7201030254 0.7116180062 0.7116180062 + 0.7207863927 0.7123395205 0.7123395205 + 0.7214698195 0.7130609155 0.7130609155 + 0.7221531272 0.7137824297 0.7137824297 + 0.7228364944 0.7145038843 0.7145038843 + 0.7235093117 0.7152239084 0.7152239084 + 0.7241787910 0.7159435153 0.7159435153 + 0.7248482704 0.7166631222 0.7166631222 + 0.7255179286 0.7173827291 0.7173827291 + 0.7261874080 0.7181022763 0.7181022763 + 0.7268568873 0.7188218832 0.7188218832 + 0.7275264263 0.7195414901 0.7195414901 + 0.7281959057 0.7202610970 0.7202610970 + 0.7288634181 0.7209777236 0.7209777236 + 0.7295259833 0.7216871977 0.7216871977 + 0.7301887274 0.7223966718 0.7223966718 + 0.7308512926 0.7231062055 0.7231062055 + 0.7315139771 0.7238156796 0.7238156796 + 0.7321766019 0.7245252132 0.7245252132 + 0.7328392863 0.7252346873 0.7252346873 + 0.7335019112 0.7259442210 0.7259442210 + 0.7341645956 0.7266536951 0.7266536951 + 0.7348304987 0.7273570895 0.7273570895 + 0.7354977727 0.7280579209 0.7280579209 + 0.7361652255 0.7287588120 0.7287588120 + 0.7368324995 0.7294595838 0.7294595838 + 0.7374997735 0.7301604152 0.7301604152 + 0.7381672263 0.7308611870 0.7308611870 + 0.7388345003 0.7315620184 0.7315620184 + 0.7395017743 0.7322629094 0.7322629094 + 0.7401691079 0.7329636812 0.7329636812 + 0.7408332229 0.7336601019 0.7336601019 + 0.7414970994 0.7343564034 0.7343564034 + 0.7421609163 0.7350527048 0.7350527048 + 0.7428247929 0.7357490063 0.7357490063 + 0.7434887290 0.7364453077 0.7364453077 + 0.7441526055 0.7371416092 0.7371416092 + 0.7448164225 0.7378379107 0.7378379107 + 0.7454802990 0.7385342121 0.7385342121 + 0.7461434007 0.7392303944 0.7392303944 + 0.7467958927 0.7399262190 0.7399262190 + 0.7474485040 0.7406219244 0.7406219244 + 0.7481011152 0.7413176894 0.7413176894 + 0.7487536073 0.7420133948 0.7420133948 + 0.7494062185 0.7427091002 0.7427091002 + 0.7500588298 0.7434049249 0.7434049249 + 0.7507113218 0.7441005707 0.7441005707 + 0.7513638735 0.7447963953 0.7447963953 + 0.7520164847 0.7454916835 0.7454916835 + 0.7526692152 0.7461783290 0.7461783290 + 0.7533218861 0.7468649745 0.7468649745 + 0.7539746165 0.7475516796 0.7475516796 + 0.7546272874 0.7482383251 0.7482383251 + 0.7552800179 0.7489249706 0.7489249706 + 0.7559326887 0.7496116757 0.7496116757 + 0.7565854192 0.7502983212 0.7502983212 + 0.7572380900 0.7509850264 0.7509850264 + 0.7578908205 0.7516716719 0.7516716719 + 0.7585409880 0.7523459196 0.7523459196 + 0.7591909766 0.7530183792 0.7530183792 + 0.7598409057 0.7536908984 0.7536908984 + 0.7604907751 0.7543634176 0.7543634176 + 0.7611407042 0.7550358772 0.7550358772 + 0.7617905736 0.7557083964 0.7557083964 + 0.7624406219 0.7563807964 0.7563807964 + 0.7630904913 0.7570533156 0.7570533156 + 0.7637404203 0.7577257752 0.7577257752 + 0.7643836737 0.7583953738 0.7583953738 + 0.7650216222 0.7590625286 0.7590625286 + 0.7656595111 0.7597296238 0.7597296238 + 0.7662972808 0.7603967190 0.7603967190 + 0.7669352293 0.7610638738 0.7610638738 + 0.7675729990 0.7617310286 0.7617310286 + 0.7682108879 0.7623981237 0.7623981237 + 0.7688487768 0.7630652785 0.7630652785 + 0.7694866061 0.7637323737 0.7637323737 + 0.7701240778 0.7643994093 0.7643994093 + 0.7707566023 0.7650648952 0.7650648952 + 0.7713890076 0.7657303810 0.7657303810 + 0.7720214725 0.7663959265 0.7663959265 + 0.7726539969 0.7670614123 0.7670614123 + 0.7732864022 0.7677268982 0.7677268982 + 0.7739189267 0.7683923841 0.7683923841 + 0.7745512724 0.7690579295 0.7690579295 + 0.7751837969 0.7697234154 0.7697234154 + 0.7758163214 0.7703889012 0.7703889012 + 0.7764492035 0.7710499763 0.7710499763 + 0.7770828009 0.7717056870 0.7717056870 + 0.7777163982 0.7723612785 0.7723612785 + 0.7783499956 0.7730169892 0.7730169892 + 0.7789835930 0.7736726999 0.7736726999 + 0.7796171904 0.7743282914 0.7743282914 + 0.7802507877 0.7749840021 0.7749840021 + 0.7808843851 0.7756395936 0.7756395936 + 0.7815179825 0.7762953043 0.7762953043 + 0.7821515799 0.7769508958 0.7769508958 + 0.7827799916 0.7775998712 0.7775998712 + 0.7834056020 0.7782453895 0.7782453895 + 0.7840312719 0.7788907886 0.7788907886 + 0.7846568823 0.7795363069 0.7795363069 + 0.7852826118 0.7801818252 0.7801818252 + 0.7859082222 0.7808272243 0.7808272243 + 0.7865338922 0.7814726830 0.7814726830 + 0.7871595025 0.7821182013 0.7821182013 + 0.7877851725 0.7827636003 0.7827636003 + 0.7884107828 0.7834091187 0.7834091187 + 0.7890290022 0.7840545177 0.7840545177 + 0.7896440029 0.7846997976 0.7846997976 + 0.7902588844 0.7853450775 0.7853450775 + 0.7908738852 0.7859904170 0.7859904170 + 0.7914888859 0.7866356969 0.7866356969 + 0.7921038866 0.7872809768 0.7872809768 + 0.7927188873 0.7879263163 0.7879263163 + 0.7933338881 0.7885715961 0.7885715961 + 0.7939488888 0.7892168760 0.7892168760 + 0.7945638895 0.7898622155 0.7898622155 + 0.7951788902 0.7905039191 0.7905039191 + 0.7957940102 0.7911430001 0.7911430001 + 0.7964090705 0.7917820811 0.7917820811 + 0.7970241904 0.7924212813 0.7924212813 + 0.7976393104 0.7930604219 0.7930604219 + 0.7982543707 0.7936995029 0.7936995029 + 0.7988696098 0.7943387032 0.7943387032 + 0.7994847298 0.7949777842 0.7949777842 + 0.8000997901 0.7956169248 0.7956169248 + 0.8007149100 0.7962561250 0.7962561250 + 0.8013291955 0.7968928814 0.7968928814 + 0.8019415736 0.7975248098 0.7975248098 + 0.8025540709 0.7981566787 0.7981566787 + 0.8031666279 0.7987886071 0.7987886071 + 0.8037791252 0.7994204760 0.7994204760 + 0.8043916225 0.8000524044 0.8000524044 + 0.8050041199 0.8006842732 0.8006842732 + 0.8056166172 0.8013160825 0.8013160825 + 0.8062291145 0.8019480109 0.8019480109 + 0.8068416119 0.8025798798 0.8025798798 + 0.8074541092 0.8032118082 0.8032118082 + 0.8080565929 0.8038352728 0.8038352728 + 0.8086578250 0.8044574857 0.8044574857 + 0.8092588782 0.8050796986 0.8050796986 + 0.8098599911 0.8057019114 0.8057019114 + 0.8104611039 0.8063241839 0.8063241839 + 0.8110622764 0.8069463968 0.8069463968 + 0.8116633892 0.8075686097 0.8075686097 + 0.8122645020 0.8081908226 0.8081908226 + 0.8128656745 0.8088130951 0.8088130951 + 0.8134667873 0.8094353080 0.8094353080 + 0.8140665889 0.8100557923 0.8100557923 + 0.8146625757 0.8106715083 0.8106715083 + 0.8152586818 0.8112871051 0.8112871051 + 0.8158547282 0.8119028211 0.8119028211 + 0.8164507151 0.8125184178 0.8125184178 + 0.8170468211 0.8131340742 0.8131340742 + 0.8176428080 0.8137496710 0.8137496710 + 0.8182387948 0.8143653870 0.8143653870 + 0.8188349009 0.8149809837 0.8149809837 + 0.8194308877 0.8155966997 0.8155966997 + 0.8200268745 0.8162122965 0.8162122965 + 0.8206235170 0.8168265820 0.8168265820 + 0.8212205768 0.8174393773 0.8174393773 + 0.8218176961 0.8180521131 0.8180521131 + 0.8224148154 0.8186649084 0.8186649084 + 0.8230118752 0.8192775846 0.8192775846 + 0.8236089945 0.8198903799 0.8198903799 + 0.8242061138 0.8205031157 0.8205031157 + 0.8248031735 0.8211159110 0.8211159110 + 0.8254002929 0.8217287064 0.8217287064 + 0.8259974122 0.8223413825 0.8223413825 + 0.8265944719 0.8229541779 0.8229541779 + 0.8271877766 0.8235660791 0.8235660791 + 0.8277773857 0.8241772056 0.8241772056 + 0.8283669949 0.8247882724 0.8247882724 + 0.8289566040 0.8253993988 0.8253993988 + 0.8295462728 0.8260105252 0.8260105252 + 0.8301358819 0.8266214728 0.8266214728 + 0.8307254910 0.8272325993 0.8272325993 + 0.8313151002 0.8278437257 0.8278437257 + 0.8319047093 0.8284547925 0.8284547925 + 0.8324943185 0.8290659189 0.8290659189 + 0.8330839276 0.8296769857 0.8296769857 + 0.8336697817 0.8302866220 0.8302866220 + 0.8342493773 0.8308938146 0.8308938146 + 0.8348289728 0.8315008879 0.8315008879 + 0.8354085088 0.8321080208 0.8321080208 + 0.8359881043 0.8327152133 0.8327152133 + 0.8365676999 0.8333222866 0.8333222866 + 0.8371472955 0.8339294195 0.8339294195 + 0.8377267718 0.8345366120 0.8345366120 + 0.8383064270 0.8351436853 0.8351436853 + 0.8388860226 0.8357508183 0.8357508183 + 0.8394654989 0.8363580108 0.8363580108 + 0.8400450945 0.8369646072 0.8369646072 + 0.8406248093 0.8375620246 0.8375620246 + 0.8412045240 0.8381593823 0.8381593823 + 0.8417841196 0.8387567997 0.8387567997 + 0.8423637748 0.8393542171 0.8393542171 + 0.8429434896 0.8399515748 0.8399515748 + 0.8435232043 0.8405489922 0.8405489922 + 0.8441029191 0.8411465287 0.8411465287 + 0.8446825743 0.8417438865 0.8417438865 + 0.8452622294 0.8423413038 0.8423413038 + 0.8458418846 0.8429387212 0.8429387212 + 0.8464215994 0.8435360789 0.8435360789 + 0.8469976187 0.8441309929 0.8441309929 + 0.8475704789 0.8447238207 0.8447238207 + 0.8481433988 0.8453165889 0.8453165889 + 0.8487163186 0.8459094167 0.8459094167 + 0.8492891788 0.8465021849 0.8465021849 + 0.8498620987 0.8470950127 0.8470950127 + 0.8504350185 0.8476877809 0.8476877809 + 0.8510079980 0.8482806087 0.8482806087 + 0.8515809178 0.8488733768 0.8488733768 + 0.8521537781 0.8494662046 0.8494662046 + 0.8527266979 0.8500589728 0.8500589728 + 0.8532996178 0.8506518006 0.8506518006 + 0.8538609147 0.8512412906 0.8512412906 + 0.8544201851 0.8518301845 0.8518301845 + 0.8549793959 0.8524190784 0.8524190784 + 0.8555387259 0.8530079126 0.8530079126 + 0.8560978770 0.8535968065 0.8535968065 + 0.8566572070 0.8541857004 0.8541857004 + 0.8572164178 0.8547745943 0.8547745943 + 0.8577756882 0.8553634882 0.8553634882 + 0.8583348989 0.8559523821 0.8559523821 + 0.8588942289 0.8565412760 0.8565412760 + 0.8594533801 0.8571302295 0.8571302295 + 0.8600127101 0.8577190042 0.8577190042 + 0.8605682254 0.8583009243 0.8583009243 + 0.8611236215 0.8588824272 0.8588824272 + 0.8616790771 0.8594639897 0.8594639897 + 0.8622344732 0.8600456119 0.8600456119 + 0.8627899289 0.8606271148 0.8606271148 + 0.8633453250 0.8612086773 0.8612086773 + 0.8639007211 0.8617901802 0.8617901802 + 0.8644561172 0.8623718023 0.8623718023 + 0.8650115132 0.8629534245 0.8629534245 + 0.8655669093 0.8635349274 0.8635349274 + 0.8661223054 0.8641164899 0.8641164899 + 0.8666777015 0.8646979928 0.8646979928 + 0.8672348261 0.8652762175 0.8652762175 + 0.8677921295 0.8658539057 0.8658539057 + 0.8683493733 0.8664317131 0.8664317131 + 0.8689066768 0.8670094013 0.8670094013 + 0.8694639206 0.8675870895 0.8675870895 + 0.8700212240 0.8681647778 0.8681647778 + 0.8705785275 0.8687425852 0.8687425852 + 0.8711357713 0.8693202734 0.8693202734 + 0.8716930747 0.8698980212 0.8698980212 + 0.8722503781 0.8704758286 0.8704758286 + 0.8728076816 0.8710535169 0.8710535169 + 0.8733649850 0.8716312051 0.8716312051 + 0.8739185929 0.8722062111 0.8722062111 + 0.8744698763 0.8727794290 0.8727794290 + 0.8750212193 0.8733525276 0.8733525276 + 0.8755723834 0.8739256859 0.8739256859 + 0.8761237264 0.8744989038 0.8744989038 + 0.8766748905 0.8750720024 0.8750720024 + 0.8772261739 0.8756452203 0.8756452203 + 0.8777773976 0.8762183785 0.8762183785 + 0.8783286810 0.8767915964 0.8767915964 + 0.8788799047 0.8773646951 0.8773646951 + 0.8794311881 0.8779379129 0.8779379129 + 0.8799824119 0.8785110712 0.8785110712 + 0.8805326223 0.8790833950 0.8790833950 + 0.8810753822 0.8796498775 0.8796498775 + 0.8816182017 0.8802164197 0.8802164197 + 0.8821610212 0.8807829022 0.8807829022 + 0.8827037811 0.8813493252 0.8813493252 + 0.8832466006 0.8819158077 0.8819158077 + 0.8837893009 0.8824822903 0.8824822903 + 0.8843321204 0.8830487728 0.8830487728 + 0.8848748803 0.8836153150 0.8836153150 + 0.8854176998 0.8841817975 0.8841817975 + 0.8859605193 0.8847482204 0.8847482204 + 0.8865032792 0.8853147030 0.8853147030 + 0.8870459795 0.8858811855 0.8858811855 + 0.8875892162 0.8864449859 0.8864449859 + 0.8881329894 0.8870056272 0.8870056272 + 0.8886767030 0.8875662088 0.8875662088 + 0.8892204762 0.8881267905 0.8881267905 + 0.8897641897 0.8886873126 0.8886873126 + 0.8903079033 0.8892478943 0.8892478943 + 0.8908516765 0.8898084760 0.8898084760 + 0.8913953900 0.8903689981 0.8903689981 + 0.8919392228 0.8909295797 0.8909295797 + 0.8924828768 0.8914902210 0.8914902210 + 0.8930265903 0.8920506835 0.8920506835 + 0.8935704231 0.8926113248 0.8926113248 + 0.8941140771 0.8931719065 0.8931719065 + 0.8946548104 0.8937309980 0.8937309980 + 0.8951931000 0.8942890167 0.8942890167 + 0.8957313895 0.8948469162 0.8948469162 + 0.8962696791 0.8954048753 0.8954048753 + 0.8968080282 0.8959628940 0.8959628940 + 0.8973463774 0.8965209126 0.8965209126 + 0.8978847265 0.8970788121 0.8970788121 + 0.8984230161 0.8976367712 0.8976367712 + 0.8989613056 0.8981947899 0.8981947899 + 0.8994995952 0.8987526894 0.8987526894 + 0.9000378847 0.8993107080 0.8993107080 + 0.9005761743 0.8998687267 0.8998687267 + 0.9011145234 0.9004266262 0.9004266262 + 0.9016473293 0.9009829760 0.9009829760 + 0.9021736979 0.9015374184 0.9015374184 + 0.9027001858 0.9020918012 0.9020918012 + 0.9032266140 0.9026463032 0.9026463032 + 0.9037529826 0.9032006860 0.9032006860 + 0.9042794108 0.9037551284 0.9037551284 + 0.9048057795 0.9043095112 0.9043095112 + 0.9053322077 0.9048638940 0.9048638940 + 0.9058586955 0.9054183960 0.9054183960 + 0.9063851237 0.9059727788 0.9059727788 + 0.9069114923 0.9065272212 0.9065272212 + 0.9074379206 0.9070816040 0.9070816040 + 0.9079642892 0.9076361060 0.9076361060 + 0.9084903002 0.9081891179 0.9081891179 + 0.9090139866 0.9087343812 0.9087343812 + 0.9095376730 0.9092797041 0.9092797041 + 0.9100614190 0.9098249078 0.9098249078 + 0.9105851054 0.9103701711 0.9103701711 + 0.9111087918 0.9109154940 0.9109154940 + 0.9116324782 0.9114606977 0.9114606977 + 0.9121562243 0.9120060205 0.9120060205 + 0.9126799107 0.9125512838 0.9125512838 + 0.9132035971 0.9130964875 0.9130964875 + 0.9137272835 0.9136418104 0.9136418104 + 0.9142510295 0.9141870737 0.9141870737 + 0.9147747755 0.9147322774 0.9147322774 + 0.9152985215 0.9152776003 0.9152776003 + 0.9158238173 0.9158192277 0.9158192277 + 0.9163501263 0.9163584709 0.9163584709 + 0.9168763757 0.9168977737 0.9168977737 + 0.9174026847 0.9174370766 0.9174370766 + 0.9179289937 0.9179763794 0.9179763794 + 0.9184553027 0.9185156226 0.9185156226 + 0.9189816117 0.9190549254 0.9190549254 + 0.9195079207 0.9195942283 0.9195942283 + 0.9200342298 0.9201334715 0.9201334715 + 0.9205604792 0.9206727743 0.9206727743 + 0.9210867882 0.9212120771 0.9212120771 + 0.9216129780 0.9217513800 0.9217513800 + 0.9221392870 0.9222906828 0.9222906828 + 0.9226655960 0.9228299856 0.9228299856 + 0.9231876731 0.9233682752 0.9233682752 + 0.9237092137 0.9239063859 0.9239063859 + 0.9242306948 0.9244444966 0.9244444966 + 0.9247521162 0.9249826074 0.9249826074 + 0.9252735972 0.9255207777 0.9255207777 + 0.9257950783 0.9260588884 0.9260588884 + 0.9263164997 0.9265969992 0.9265969992 + 0.9268379807 0.9271351099 0.9271351099 + 0.9273594022 0.9276732206 0.9276732206 + 0.9278808832 0.9282112718 0.9282112718 + 0.9284024239 0.9287495017 0.9287495017 + 0.9289237857 0.9292876124 0.9292876124 + 0.9294453263 0.9298257232 0.9298257232 + 0.9299668074 0.9303637743 0.9303637743 + 0.9304789901 0.9308947921 0.9308947921 + 0.9309902191 0.9314250946 0.9314250946 + 0.9315013885 0.9319552779 0.9319552779 + 0.9320126176 0.9324855804 0.9324855804 + 0.9325237274 0.9330158234 0.9330158234 + 0.9330348969 0.9335461259 0.9335461259 + 0.9335461259 0.9340763092 0.9340763092 + 0.9340572953 0.9346066117 0.9346066117 + 0.9345685244 0.9351367950 0.9351367950 + 0.9350796938 0.9356669784 0.9356669784 + 0.9355909228 0.9361972809 0.9361972809 + 0.9361019731 0.9367275238 0.9367275238 + 0.9366132021 0.9372578263 0.9372578263 + 0.9371243715 0.9377880096 0.9377880096 + 0.9376301169 0.9383096099 0.9383096099 + 0.9381334186 0.9388273954 0.9388273954 + 0.9386367202 0.9393453002 0.9393453002 + 0.9391400218 0.9398630857 0.9398630857 + 0.9396432042 0.9403809905 0.9403809905 + 0.9401465058 0.9408987761 0.9408987761 + 0.9406498075 0.9414166808 0.9414166808 + 0.9411531091 0.9419345260 0.9419345260 + 0.9416564107 0.9424523115 0.9424523115 + 0.9421597123 0.9429702163 0.9429702163 + 0.9426630139 0.9434880018 0.9434880018 + 0.9431663156 0.9440059066 0.9440059066 + 0.9436694980 0.9445236921 0.9445236921 + 0.9441727996 0.9450414777 0.9450414777 + 0.9446740746 0.9455562234 0.9455562234 + 0.9451695085 0.9460619092 0.9460619092 + 0.9456648827 0.9465677142 0.9465677142 + 0.9461603761 0.9470734000 0.9470734000 + 0.9466558099 0.9475792050 0.9475792050 + 0.9471513033 0.9480848908 0.9480848908 + 0.9476466775 0.9485906959 0.9485906959 + 0.9481421709 0.9490963817 0.9490963817 + 0.9486376047 0.9496021867 0.9496021867 + 0.9491330981 0.9501078725 0.9501078725 + 0.9496284723 0.9506136775 0.9506136775 + 0.9501240253 0.9511194229 0.9511194229 + 0.9506193995 0.9516252279 0.9516252279 + 0.9511148930 0.9521309137 0.9521309137 + 0.9516103268 0.9526367188 0.9526367188 + 0.9520999789 0.9531362057 0.9531362057 + 0.9525858164 0.9536315799 0.9536315799 + 0.9530715942 0.9541268945 0.9541268945 + 0.9535574913 0.9546223283 0.9546223283 + 0.9540433288 0.9551175833 0.9551175833 + 0.9545291066 0.9556128979 0.9556128979 + 0.9550148845 0.9561082721 0.9561082721 + 0.9555007219 0.9566035867 0.9566035867 + 0.9559866190 0.9570990205 0.9570990205 + 0.9564723969 0.9575942755 0.9575942755 + 0.9569581747 0.9580897093 0.9580897093 + 0.9574440122 0.9585850239 0.9585850239 + 0.9579297900 0.9590802789 0.9590802789 + 0.9584156871 0.9595757127 0.9595757127 + 0.9589015245 0.9600710273 0.9600710273 + 0.9593812823 0.9605569243 0.9605569243 + 0.9598584771 0.9610384107 0.9610384107 + 0.9603356123 0.9615200162 0.9615200162 + 0.9608128071 0.9620016217 0.9620016217 + 0.9612900019 0.9624832273 0.9624832273 + 0.9617670774 0.9629647136 0.9629647136 + 0.9622442722 0.9634463191 0.9634463191 + 0.9627214074 0.9639279246 0.9639279246 + 0.9631986022 0.9644094706 0.9644094706 + 0.9636756778 0.9648910165 0.9648910165 + 0.9641528726 0.9653726220 0.9653726220 + 0.9646300077 0.9658542275 0.9658542275 + 0.9651072025 0.9663357735 0.9663357735 + 0.9655842781 0.9668173194 0.9668173194 + 0.9660614729 0.9672989249 0.9672989249 + 0.9665307999 0.9677714705 0.9677714705 + 0.9669935107 0.9682365060 0.9682365060 + 0.9674562216 0.9687014818 0.9687014818 + 0.9679188728 0.9691665173 0.9691665173 + 0.9683815837 0.9696314931 0.9696314931 + 0.9688442945 0.9700965285 0.9700965285 + 0.9693070054 0.9705615044 0.9705615044 + 0.9697697163 0.9710264802 0.9710264802 + 0.9702324271 0.9714915156 0.9714915156 + 0.9706950188 0.9719564915 0.9719564915 + 0.9711577296 0.9724215269 0.9724215269 + 0.9716203809 0.9728865027 0.9728865027 + 0.9720830917 0.9733514786 0.9733514786 + 0.9725458026 0.9738165140 0.9738165140 + 0.9730085135 0.9742814898 0.9742814898 + 0.9734690189 0.9747437239 0.9747437239 + 0.9739159942 0.9751892090 0.9751892090 + 0.9743630886 0.9756346941 0.9756346941 + 0.9748101830 0.9760801792 0.9760801792 + 0.9752572775 0.9765257835 0.9765257835 + 0.9757043719 0.9769713283 0.9769713283 + 0.9761515260 0.9774168134 0.9774168134 + 0.9765985012 0.9778622985 0.9778622985 + 0.9770455956 0.9783079028 0.9783079028 + 0.9774926901 0.9787533879 0.9787533879 + 0.9779397845 0.9791988730 0.9791988730 + 0.9783868790 0.9796444178 0.9796444178 + 0.9788339734 0.9800899029 0.9800899029 + 0.9792810082 0.9805355072 0.9805355072 + 0.9797281027 0.9809809923 0.9809809923 + 0.9801751971 0.9814264774 0.9814264774 + 0.9806160927 0.9818611741 0.9818611741 + 0.9810506105 0.9822847247 0.9822847247 + 0.9814851284 0.9827082753 0.9827082753 + 0.9819195271 0.9831318855 0.9831318855 + 0.9823539853 0.9835553765 0.9835553765 + 0.9827885032 0.9839789867 0.9839789867 + 0.9832230210 0.9844024777 0.9844024777 + 0.9836574793 0.9848260880 0.9848260880 + 0.9840919971 0.9852495790 0.9852495790 + 0.9845265150 0.9856731892 0.9856731892 + 0.9849609733 0.9860966802 0.9860966802 + 0.9853954911 0.9865202904 0.9865202904 + 0.9858298898 0.9869437814 0.9869437814 + 0.9862644076 0.9873673916 0.9873673916 + 0.9866989255 0.9877910018 0.9877910018 + 0.9871333838 0.9882144928 0.9882144928 + 0.9875543714 0.9886164069 0.9886164069 + 0.9879658818 0.9890031219 0.9890031219 + 0.9883775115 0.9893897772 0.9893897772 + 0.9887890220 0.9897766113 0.9897766113 + 0.9892005920 0.9901633263 0.9901633263 + 0.9896121025 0.9905501008 0.9905501008 + 0.9900236726 0.9909368157 0.9909368157 + 0.9904351830 0.9913235903 0.9913235903 + 0.9908468127 0.9917103052 0.9917103052 + 0.9912583232 0.9920970201 0.9920970201 + 0.9916698933 0.9924837947 0.9924837947 + 0.9920814037 0.9928705096 0.9928705096 + 0.9924929738 0.9932572842 0.9932572842 + 0.9929044843 0.9936439991 0.9936439991 + 0.9933161139 0.9940307736 0.9940307736 + 0.9937276244 0.9944174886 0.9944174886 + 0.9941204190 0.9947785139 0.9947785139 + 0.9944878221 0.9951047897 0.9951047897 + 0.9948552847 0.9954311848 0.9954311848 + 0.9952228069 0.9957575202 0.9957575202 + 0.9955903292 0.9960837960 0.9960837960 + 0.9959577918 0.9964101911 0.9964101911 + 0.9963251948 0.9967365265 0.9967365265 + 0.9966927171 0.9970629215 0.9970629215 + 0.9970601797 0.9973891973 0.9973891973 + 0.9974277020 0.9977155924 0.9977155924 + 0.9977951050 0.9980419278 0.9980419278 + 0.9981626272 0.9983683228 0.9983683228 + 0.9985300899 0.9986945987 0.9986945987 + 0.9988976121 0.9990209937 0.9990209937 + 0.9992650151 0.9993473291 0.9993473291 + 0.9996324778 0.9996737242 0.9996737242 + 1.0000000000 1.0000000000 1.0000000000 +} diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/generate_default_lut.py b/RenderPipelineFile/rpplugins/color_correction/resources/generate_default_lut.py index 67b3cff8..7aa27928 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/generate_default_lut.py +++ b/RenderPipelineFile/rpplugins/color_correction/resources/generate_default_lut.py @@ -1,52 +1,52 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" -from __future__ import division, print_function - -from panda3d.core import PNMImage - -lut_size = 64 -lut_cols = 8 -lut_rows = (lut_size + lut_cols - 1) // lut_cols - -img = PNMImage(lut_size * lut_cols, lut_size * lut_rows, 3, 2**16 - 1) - - -def to_linear(v): - return float(v) / float(lut_size - 1) - - -def to_linear_inv(v): - return 1 - to_linear(v) - -for r in range(lut_size): - for g in range(lut_size): - for b in range(lut_size): - slice_offset_x = (b % lut_cols) * lut_size - slice_offset_y = (b // lut_cols) * lut_size - img.set_xel(r + slice_offset_x, g + slice_offset_y, - to_linear(r), to_linear_inv(g), to_linear(b)) - -img.write("default_lut.png") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +from __future__ import division, print_function + +from panda3d.core import PNMImage + +lut_size = 64 +lut_cols = 8 +lut_rows = (lut_size + lut_cols - 1) // lut_cols + +img = PNMImage(lut_size * lut_cols, lut_size * lut_rows, 3, 2**16 - 1) + + +def to_linear(v): + return float(v) / float(lut_size - 1) + + +def to_linear_inv(v): + return 1 - to_linear(v) + +for r in range(lut_size): + for g in range(lut_size): + for b in range(lut_size): + slice_offset_x = (b % lut_cols) * lut_size + slice_offset_y = (b // lut_cols) * lut_size + img.set_xel(r + slice_offset_x, g + slice_offset_y, + to_linear(r), to_linear_inv(g), to_linear(b)) + +img.write("default_lut.png") diff --git a/RenderPipelineFile/rpplugins/color_correction/resources/generate_film_luts.py b/RenderPipelineFile/rpplugins/color_correction/resources/generate_film_luts.py index 96159a37..bc2279c7 100644 --- a/RenderPipelineFile/rpplugins/color_correction/resources/generate_film_luts.py +++ b/RenderPipelineFile/rpplugins/color_correction/resources/generate_film_luts.py @@ -1,79 +1,79 @@ -""" - -Script to convert spi1d files to luts - -""" - -from __future__ import print_function - -import os -from panda3d.core import PNMImage - -for f in os.listdir("film_luts_raw"): - if not f.endswith(".spi1d"): - continue - - input_file = "film_luts_raw/" + f - - print("Processing", input_file) - output_name = input_file.replace("\\", "/").split("/")[-1].replace(".spi1d", "") - - with open(input_file, "r") as handle: - content = [i.strip() for i in handle.readlines()] - - header = [ - "Version 1", - "From 0.0 1.0", - "Length 1024", - "Components 3", - "{", - ] - - for i, line in enumerate(header): - if content[i] != line: - print("Invalid header! Expected", line, "but got", content[i]) - break - else: - - content = content[len(header):-1] - - values = [[], [], []] - - def srgb_to_linear(channel): - if channel <= 0.04045: - return channel / 12.92 - else: - return pow((channel + 0.055) / (1.0 + 0.055), 2.4) - - for i in xrange(1024): - line = [float(k) for k in content[i].split()] - - for k in xrange(3): - values[k].append(srgb_to_linear(line[k])) - - def to_linear(v): - return float(v) / float(64 - 1) - - def to_linear_inv(v): - return 1 - to_linear(v) - - def lookup_value(v, values): - return values[int(v * (len(values) - 1))] - - # Generate lut - img = PNMImage(64 * 8, 64 * 8, 3, 2**16 - 1) - for r in xrange(64): - for g in xrange(64): - for b in xrange(64): - slice_offset_x = (b % 8) * 64 - slice_offset_y = (b // 8) * 64 - - fr, fg, fb = to_linear(r), to_linear_inv(g), to_linear(b) - - fr = lookup_value(fr, values[0]) - fg = lookup_value(fg, values[1]) - fb = lookup_value(fb, values[2]) - - img.set_xel(r + slice_offset_x, g + slice_offset_y, fr, fg, fb) - - img.write("film_luts/" + output_name + ".png") +""" + +Script to convert spi1d files to luts + +""" + +from __future__ import print_function + +import os +from panda3d.core import PNMImage + +for f in os.listdir("film_luts_raw"): + if not f.endswith(".spi1d"): + continue + + input_file = "film_luts_raw/" + f + + print("Processing", input_file) + output_name = input_file.replace("\\", "/").split("/")[-1].replace(".spi1d", "") + + with open(input_file, "r") as handle: + content = [i.strip() for i in handle.readlines()] + + header = [ + "Version 1", + "From 0.0 1.0", + "Length 1024", + "Components 3", + "{", + ] + + for i, line in enumerate(header): + if content[i] != line: + print("Invalid header! Expected", line, "but got", content[i]) + break + else: + + content = content[len(header):-1] + + values = [[], [], []] + + def srgb_to_linear(channel): + if channel <= 0.04045: + return channel / 12.92 + else: + return pow((channel + 0.055) / (1.0 + 0.055), 2.4) + + for i in xrange(1024): + line = [float(k) for k in content[i].split()] + + for k in xrange(3): + values[k].append(srgb_to_linear(line[k])) + + def to_linear(v): + return float(v) / float(64 - 1) + + def to_linear_inv(v): + return 1 - to_linear(v) + + def lookup_value(v, values): + return values[int(v * (len(values) - 1))] + + # Generate lut + img = PNMImage(64 * 8, 64 * 8, 3, 2**16 - 1) + for r in xrange(64): + for g in xrange(64): + for b in xrange(64): + slice_offset_x = (b % 8) * 64 + slice_offset_y = (b // 8) * 64 + + fr, fg, fb = to_linear(r), to_linear_inv(g), to_linear(b) + + fr = lookup_value(fr, values[0]) + fg = lookup_value(fg, values[1]) + fb = lookup_value(fb, values[2]) + + img.set_xel(r + slice_offset_x, g + slice_offset_y, fr, fg, fb) + + img.write("film_luts/" + output_name + ".png") diff --git a/RenderPipelineFile/rpplugins/color_correction/shader/analyze_brightness.frag.glsl b/RenderPipelineFile/rpplugins/color_correction/shader/analyze_brightness.frag.glsl index 04cc79ff..8b64310d 100644 --- a/RenderPipelineFile/rpplugins/color_correction/shader/analyze_brightness.frag.glsl +++ b/RenderPipelineFile/rpplugins/color_correction/shader/analyze_brightness.frag.glsl @@ -1,85 +1,85 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#define USE_TIME_OF_DAY 1 -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/tonemapping.inc.glsl" - -layout(r16f) uniform imageBuffer RESTRICT ExposureStorage; -uniform sampler2D DownscaledTex; - -void main() { - - // Manually do the last downscale step - ivec2 texsize = textureSize(DownscaledTex, 0).xy; - float avg_luminance = 0.0; - for (int x = 0; x < texsize.x; ++x) { - for (int y = 0; y < texsize.y; ++y) { - avg_luminance += texelFetch(DownscaledTex, ivec2(x, y), 0).x; - } - } - - avg_luminance /= float(texsize.x * texsize.y); - avg_luminance = avg_luminance / (1 - avg_luminance); - - #if 0 - float exposure_val = computeEV100FromAvgLuminance(avg_luminance); - float exposure = convertEV100ToExposure(exposure_val); - #else - // Same as a above - just factored out. - float exposure = 0.1041666 * (1.0 / avg_luminance); - #endif - - float min_ev = GET_SETTING(color_correction, min_exposure_value); - float max_ev = GET_SETTING(color_correction, max_exposure_value); - float exposure_scale = GET_SETTING(color_correction, exposure_scale); - - // XXX: Without a multiplier of two, the image gets way too dark - not sure - // why, but most likely an issue in the exposure calculation. However, - // this is not physically correct. - exposure *= exposure_scale * 2.0; - - // Clamp to min and max exposure value - exposure = clamp(exposure, min_ev, max_ev); - - // Transition between the last and current value smoothly - float curr_exposure = imageLoad(ExposureStorage, 0).x; - float adaption_rate = GET_SETTING(color_correction, brightness_adaption_rate); - - if (curr_exposure < exposure) { - adaption_rate = GET_SETTING(color_correction, darkness_adaption_rate); - } - - const float adaption_speed = 0.8; - adaption_rate *= adaption_speed; - - float adjustment = saturate(MainSceneData.smooth_frame_delta * adaption_rate); - float new_luminance = mix(curr_exposure, exposure, adjustment); - new_luminance = clamp(new_luminance, 0.0, 1e4); - imageStore(ExposureStorage, 0, vec4(new_luminance)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#define USE_TIME_OF_DAY 1 +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/tonemapping.inc.glsl" + +layout(r16f) uniform imageBuffer RESTRICT ExposureStorage; +uniform sampler2D DownscaledTex; + +void main() { + + // Manually do the last downscale step + ivec2 texsize = textureSize(DownscaledTex, 0).xy; + float avg_luminance = 0.0; + for (int x = 0; x < texsize.x; ++x) { + for (int y = 0; y < texsize.y; ++y) { + avg_luminance += texelFetch(DownscaledTex, ivec2(x, y), 0).x; + } + } + + avg_luminance /= float(texsize.x * texsize.y); + avg_luminance = avg_luminance / (1 - avg_luminance); + + #if 0 + float exposure_val = computeEV100FromAvgLuminance(avg_luminance); + float exposure = convertEV100ToExposure(exposure_val); + #else + // Same as a above - just factored out. + float exposure = 0.1041666 * (1.0 / avg_luminance); + #endif + + float min_ev = GET_SETTING(color_correction, min_exposure_value); + float max_ev = GET_SETTING(color_correction, max_exposure_value); + float exposure_scale = GET_SETTING(color_correction, exposure_scale); + + // XXX: Without a multiplier of two, the image gets way too dark - not sure + // why, but most likely an issue in the exposure calculation. However, + // this is not physically correct. + exposure *= exposure_scale * 2.0; + + // Clamp to min and max exposure value + exposure = clamp(exposure, min_ev, max_ev); + + // Transition between the last and current value smoothly + float curr_exposure = imageLoad(ExposureStorage, 0).x; + float adaption_rate = GET_SETTING(color_correction, brightness_adaption_rate); + + if (curr_exposure < exposure) { + adaption_rate = GET_SETTING(color_correction, darkness_adaption_rate); + } + + const float adaption_speed = 0.8; + adaption_rate *= adaption_speed; + + float adjustment = saturate(MainSceneData.smooth_frame_delta * adaption_rate); + float new_luminance = mix(curr_exposure, exposure, adjustment); + new_luminance = clamp(new_luminance, 0.0, 1e4); + imageStore(ExposureStorage, 0, vec4(new_luminance)); +} diff --git a/RenderPipelineFile/rpplugins/color_correction/shader/apply_exposure.frag.glsl b/RenderPipelineFile/rpplugins/color_correction/shader/apply_exposure.frag.glsl index b0be751d..bcecdbfe 100644 --- a/RenderPipelineFile/rpplugins/color_correction/shader/apply_exposure.frag.glsl +++ b/RenderPipelineFile/rpplugins/color_correction/shader/apply_exposure.frag.glsl @@ -1,46 +1,46 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" - -uniform sampler2D ShadedScene; -uniform samplerBuffer Exposure; - -out vec3 result; - -void main() { - ivec2 coord = ivec2(gl_FragCoord.xy); - vec3 scene_color = texelFetch(ShadedScene, coord, 0).xyz; - - #if !DEBUG_MODE - float exposure = texelFetch(Exposure, 0).x; - scene_color *= exposure; - #endif - - result = scene_color; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" + +uniform sampler2D ShadedScene; +uniform samplerBuffer Exposure; + +out vec3 result; + +void main() { + ivec2 coord = ivec2(gl_FragCoord.xy); + vec3 scene_color = texelFetch(ShadedScene, coord, 0).xyz; + + #if !DEBUG_MODE + float exposure = texelFetch(Exposure, 0).x; + scene_color *= exposure; + #endif + + result = scene_color; +} diff --git a/RenderPipelineFile/rpplugins/color_correction/shader/apply_tonemap.frag.glsl b/RenderPipelineFile/rpplugins/color_correction/shader/apply_tonemap.frag.glsl index 6f2332b5..e8a72f8a 100644 --- a/RenderPipelineFile/rpplugins/color_correction/shader/apply_tonemap.frag.glsl +++ b/RenderPipelineFile/rpplugins/color_correction/shader/apply_tonemap.frag.glsl @@ -1,61 +1,61 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#define USE_TIME_OF_DAY 1 -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/tonemapping.inc.glsl" - -uniform sampler2D ShadedScene; -uniform sampler3D ColorLUT; - -out vec4 result; - -vec3 apply_lut(vec3 color) { - // We have a gradient from 0.5 / lut_size to 1 - 0.5 / lut_size - // so we need to transform from 0 .. 1 to that gradient (hardcoded lut size for now) - const float lut_start = 0.5 / 64.0; - const float lut_end = 1.0 - lut_start; - color = color * (lut_end - lut_start) + lut_start; - return textureLod(ColorLUT, color, 0).xyz; -} - -void main() { - vec2 texcoord = get_texcoord(); - - #if !DEBUG_MODE - vec3 scene_color = textureLod(ShadedScene, texcoord, 0).xyz; - scene_color = do_tonemapping(scene_color); - scene_color = apply_lut(scene_color); - #else - vec3 scene_color = textureLod(ShadedScene, texcoord, 0).xyz; - #endif // !DEBUG_MODE - - scene_color = saturate(scene_color); - - result = vec4(scene_color, 1); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#define USE_TIME_OF_DAY 1 +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/tonemapping.inc.glsl" + +uniform sampler2D ShadedScene; +uniform sampler3D ColorLUT; + +out vec4 result; + +vec3 apply_lut(vec3 color) { + // We have a gradient from 0.5 / lut_size to 1 - 0.5 / lut_size + // so we need to transform from 0 .. 1 to that gradient (hardcoded lut size for now) + const float lut_start = 0.5 / 64.0; + const float lut_end = 1.0 - lut_start; + color = color * (lut_end - lut_start) + lut_start; + return textureLod(ColorLUT, color, 0).xyz; +} + +void main() { + vec2 texcoord = get_texcoord(); + + #if !DEBUG_MODE + vec3 scene_color = textureLod(ShadedScene, texcoord, 0).xyz; + scene_color = do_tonemapping(scene_color); + scene_color = apply_lut(scene_color); + #else + vec3 scene_color = textureLod(ShadedScene, texcoord, 0).xyz; + #endif // !DEBUG_MODE + + scene_color = saturate(scene_color); + + result = vec4(scene_color, 1); +} diff --git a/RenderPipelineFile/rpplugins/color_correction/shader/chromatic_aberration.inc.glsl b/RenderPipelineFile/rpplugins/color_correction/shader/chromatic_aberration.inc.glsl index 52627ae4..cbc1ade0 100644 --- a/RenderPipelineFile/rpplugins/color_correction/shader/chromatic_aberration.inc.glsl +++ b/RenderPipelineFile/rpplugins/color_correction/shader/chromatic_aberration.inc.glsl @@ -1,60 +1,60 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -#pragma optionNV (unroll all) - -vec3 do_chromatic_aberration(sampler2D colortex, vec2 texcoord, float factor) { - - vec2 mid_coord = texcoord * 2.0 - 1.0; - vec3 blurred = vec3(0); - const int num_samples = GET_SETTING(color_correction, chromatic_aberration_samples); - - float factor_base = saturate(factor) * - GET_SETTING(color_correction, chromatic_aberration_strength); - float factor_strength = 25.0; - - vec2 mid_coord_r = mid_coord; - vec2 mid_coord_g = mid_coord * (1 - factor_base / factor_strength * 0.5); - vec2 mid_coord_b = mid_coord * (1 - factor_base / factor_strength); - - vec2 blur_dir = normalize(mid_coord + 1e-5) / SCREEN_SIZE * factor_strength * factor_base; - - for (int i = -num_samples; i <= num_samples; ++i) { - vec2 offset = float(i) / num_samples * blur_dir; - vec2 offcord_r = mid_coord_r + offset; - vec2 offcord_g = mid_coord_g + offset; - vec2 offcord_b = mid_coord_b + offset; - blurred.r += textureLod(colortex, offcord_r * 0.5 + 0.5, 0).r; - blurred.g += textureLod(colortex, offcord_g * 0.5 + 0.5, 0).g; - blurred.b += textureLod(colortex, offcord_b * 0.5 + 0.5, 0).b; - } - - blurred /= fma(float(num_samples), 2.0, 1.0); - - return blurred; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +#pragma optionNV (unroll all) + +vec3 do_chromatic_aberration(sampler2D colortex, vec2 texcoord, float factor) { + + vec2 mid_coord = texcoord * 2.0 - 1.0; + vec3 blurred = vec3(0); + const int num_samples = GET_SETTING(color_correction, chromatic_aberration_samples); + + float factor_base = saturate(factor) * + GET_SETTING(color_correction, chromatic_aberration_strength); + float factor_strength = 25.0; + + vec2 mid_coord_r = mid_coord; + vec2 mid_coord_g = mid_coord * (1 - factor_base / factor_strength * 0.5); + vec2 mid_coord_b = mid_coord * (1 - factor_base / factor_strength); + + vec2 blur_dir = normalize(mid_coord + 1e-5) / SCREEN_SIZE * factor_strength * factor_base; + + for (int i = -num_samples; i <= num_samples; ++i) { + vec2 offset = float(i) / num_samples * blur_dir; + vec2 offcord_r = mid_coord_r + offset; + vec2 offcord_g = mid_coord_g + offset; + vec2 offcord_b = mid_coord_b + offset; + blurred.r += textureLod(colortex, offcord_r * 0.5 + 0.5, 0).r; + blurred.g += textureLod(colortex, offcord_g * 0.5 + 0.5, 0).g; + blurred.b += textureLod(colortex, offcord_b * 0.5 + 0.5, 0).b; + } + + blurred /= fma(float(num_samples), 2.0, 1.0); + + return blurred; +} diff --git a/RenderPipelineFile/rpplugins/color_correction/shader/downscale_luminance.frag.glsl b/RenderPipelineFile/rpplugins/color_correction/shader/downscale_luminance.frag.glsl index 087aff7b..f6baae3d 100644 --- a/RenderPipelineFile/rpplugins/color_correction/shader/downscale_luminance.frag.glsl +++ b/RenderPipelineFile/rpplugins/color_correction/shader/downscale_luminance.frag.glsl @@ -1,44 +1,44 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -uniform sampler2D SourceTex; -out vec4 result; - -void main() { - vec2 texsize = vec2(textureSize(SourceTex, 0).xy); - ivec2 coord_screen = ivec2(gl_FragCoord.xy) * 4; - vec2 local_coord = (coord_screen + 1.0) / texsize; - vec2 pixel_offset = 2.0 / texsize; - - float lum = textureLod(SourceTex, local_coord, 0).x; - lum += textureLod(SourceTex, local_coord + vec2(pixel_offset.x, 0), 0).x; - lum += textureLod(SourceTex, local_coord + vec2(0, pixel_offset.y), 0).x; - lum += textureLod(SourceTex, local_coord + pixel_offset.xy, 0).x; - - result = vec4(lum * 0.25); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +uniform sampler2D SourceTex; +out vec4 result; + +void main() { + vec2 texsize = vec2(textureSize(SourceTex, 0).xy); + ivec2 coord_screen = ivec2(gl_FragCoord.xy) * 4; + vec2 local_coord = (coord_screen + 1.0) / texsize; + vec2 pixel_offset = 2.0 / texsize; + + float lum = textureLod(SourceTex, local_coord, 0).x; + lum += textureLod(SourceTex, local_coord + vec2(pixel_offset.x, 0), 0).x; + lum += textureLod(SourceTex, local_coord + vec2(0, pixel_offset.y), 0).x; + lum += textureLod(SourceTex, local_coord + pixel_offset.xy, 0).x; + + result = vec4(lum * 0.25); +} diff --git a/RenderPipelineFile/rpplugins/color_correction/shader/generate_luminance.frag.glsl b/RenderPipelineFile/rpplugins/color_correction/shader/generate_luminance.frag.glsl index 17f33530..bf070472 100644 --- a/RenderPipelineFile/rpplugins/color_correction/shader/generate_luminance.frag.glsl +++ b/RenderPipelineFile/rpplugins/color_correction/shader/generate_luminance.frag.glsl @@ -1,62 +1,62 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/color_spaces.inc.glsl" - -// Converts the scene color to a luminance - -uniform sampler2D ShadedScene; -out float result; - -float get_log_luminance(vec3 color) { - float lum = get_luminance(color); - return lum / (1 + lum); -} - -void main() { - ivec2 coord_screen = ivec2(gl_FragCoord.xy) * 4; - vec2 local_coord = (coord_screen + 1.0) / SCREEN_SIZE; - vec2 pixel_offset = 2.0 / SCREEN_SIZE; - - // Weight luminance based on distance to the borders - this is because - // pixels in the center of the screen are more visually important - float weight = 1.05 - 0.2 * distance(local_coord, vec2(0.5, 0.5)); - - vec4 luminances = vec4( - get_log_luminance(weight * - textureLod(ShadedScene, local_coord, 0).xyz), - get_log_luminance(weight * - textureLod(ShadedScene, local_coord + vec2(pixel_offset.x, 0), 0).xyz), - get_log_luminance(weight * - textureLod(ShadedScene, local_coord + vec2(0, pixel_offset.y), 0).xyz), - get_log_luminance(weight * - textureLod(ShadedScene, local_coord + pixel_offset.xy, 0).xyz) - ); - - result = dot(luminances, vec4(0.25)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/color_spaces.inc.glsl" + +// Converts the scene color to a luminance + +uniform sampler2D ShadedScene; +out float result; + +float get_log_luminance(vec3 color) { + float lum = get_luminance(color); + return lum / (1 + lum); +} + +void main() { + ivec2 coord_screen = ivec2(gl_FragCoord.xy) * 4; + vec2 local_coord = (coord_screen + 1.0) / SCREEN_SIZE; + vec2 pixel_offset = 2.0 / SCREEN_SIZE; + + // Weight luminance based on distance to the borders - this is because + // pixels in the center of the screen are more visually important + float weight = 1.05 - 0.2 * distance(local_coord, vec2(0.5, 0.5)); + + vec4 luminances = vec4( + get_log_luminance(weight * + textureLod(ShadedScene, local_coord, 0).xyz), + get_log_luminance(weight * + textureLod(ShadedScene, local_coord + vec2(pixel_offset.x, 0), 0).xyz), + get_log_luminance(weight * + textureLod(ShadedScene, local_coord + vec2(0, pixel_offset.y), 0).xyz), + get_log_luminance(weight * + textureLod(ShadedScene, local_coord + pixel_offset.xy, 0).xyz) + ); + + result = dot(luminances, vec4(0.25)); +} diff --git a/RenderPipelineFile/rpplugins/color_correction/shader/manual_exposure.frag.glsl b/RenderPipelineFile/rpplugins/color_correction/shader/manual_exposure.frag.glsl index 33de3f02..4c02b25a 100644 --- a/RenderPipelineFile/rpplugins/color_correction/shader/manual_exposure.frag.glsl +++ b/RenderPipelineFile/rpplugins/color_correction/shader/manual_exposure.frag.glsl @@ -1,49 +1,49 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#define USE_TIME_OF_DAY 1 -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/tonemapping.inc.glsl" - -uniform sampler2D ShadedScene; - -out vec3 result; - -// Applies manual camera parameters - -void main() { - vec2 texcoord = get_texcoord(); - - float exposure_val = computeEV100( - TimeOfDay.color_correction.camera_aperture, - TimeOfDay.color_correction.camera_shutter, - TimeOfDay.color_correction.camera_iso); - - float exposure = convertEV100ToExposure(exposure_val); - result = textureLod(ShadedScene, texcoord, 0).xyz * exposure; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#define USE_TIME_OF_DAY 1 +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/tonemapping.inc.glsl" + +uniform sampler2D ShadedScene; + +out vec3 result; + +// Applies manual camera parameters + +void main() { + vec2 texcoord = get_texcoord(); + + float exposure_val = computeEV100( + TimeOfDay.color_correction.camera_aperture, + TimeOfDay.color_correction.camera_shutter, + TimeOfDay.color_correction.camera_iso); + + float exposure = convertEV100ToExposure(exposure_val); + result = textureLod(ShadedScene, texcoord, 0).xyz * exposure; +} diff --git a/RenderPipelineFile/rpplugins/color_correction/shader/post_fx.frag.glsl b/RenderPipelineFile/rpplugins/color_correction/shader/post_fx.frag.glsl index e2201017..3a5b070a 100644 --- a/RenderPipelineFile/rpplugins/color_correction/shader/post_fx.frag.glsl +++ b/RenderPipelineFile/rpplugins/color_correction/shader/post_fx.frag.glsl @@ -1,79 +1,79 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#define USE_TIME_OF_DAY 1 -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/transforms.inc.glsl" -#pragma include "includes/noise.inc.glsl" - -#pragma include "chromatic_aberration.inc.glsl" - -uniform sampler2D ShadedScene; - -out vec4 result; - - -void main() { - - vec2 texcoord = get_texcoord(); - - #if !DEBUG_MODE - - - vec2 ccord = (texcoord - 0.5) * vec2(1.0, ASPECT_RATIO); - float vignette = 1 - saturate(length(ccord)); - - // Chromatic abberation - #if GET_SETTING(color_correction, use_chromatic_aberration) - vec3 scene_color = do_chromatic_aberration(ShadedScene, texcoord, 1-vignette); - #else - vec3 scene_color = textureLod(ShadedScene, texcoord, 0).xyz; - #endif - - // Compute film grain - float film_grain = grain(MainSceneData.frame_time); - vec3 blended_color = blend_soft_light(scene_color, vec3(film_grain)); - - // Blend film grain - float grain_factor = GET_SETTING(color_correction, film_grain_strength); - scene_color = mix(scene_color, blended_color, grain_factor); - - // const float saturation = 0.00; - // scene_color = max(vec3(0), (scene_color - saturation) / (1 - saturation)); - - // Apply the vignette based on the vignette strength - scene_color *= mix(1.0, vignette, GET_SETTING(color_correction, vignette_strength)); - - - #else - vec3 scene_color = textureLod(ShadedScene, texcoord, 0).xyz; - #endif // !DEBUG_MODE - - - result = vec4(scene_color, 1); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#define USE_TIME_OF_DAY 1 +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/transforms.inc.glsl" +#pragma include "includes/noise.inc.glsl" + +#pragma include "chromatic_aberration.inc.glsl" + +uniform sampler2D ShadedScene; + +out vec4 result; + + +void main() { + + vec2 texcoord = get_texcoord(); + + #if !DEBUG_MODE + + + vec2 ccord = (texcoord - 0.5) * vec2(1.0, ASPECT_RATIO); + float vignette = 1 - saturate(length(ccord)); + + // Chromatic abberation + #if GET_SETTING(color_correction, use_chromatic_aberration) + vec3 scene_color = do_chromatic_aberration(ShadedScene, texcoord, 1-vignette); + #else + vec3 scene_color = textureLod(ShadedScene, texcoord, 0).xyz; + #endif + + // Compute film grain + float film_grain = grain(MainSceneData.frame_time); + vec3 blended_color = blend_soft_light(scene_color, vec3(film_grain)); + + // Blend film grain + float grain_factor = GET_SETTING(color_correction, film_grain_strength); + scene_color = mix(scene_color, blended_color, grain_factor); + + // const float saturation = 0.00; + // scene_color = max(vec3(0), (scene_color - saturation) / (1 - saturation)); + + // Apply the vignette based on the vignette strength + scene_color *= mix(1.0, vignette, GET_SETTING(color_correction, vignette_strength)); + + + #else + vec3 scene_color = textureLod(ShadedScene, texcoord, 0).xyz; + #endif // !DEBUG_MODE + + + result = vec4(scene_color, 1); +} diff --git a/RenderPipelineFile/rpplugins/color_correction/shader/sharpen.frag.glsl b/RenderPipelineFile/rpplugins/color_correction/shader/sharpen.frag.glsl index 1aea9d51..a846014a 100644 --- a/RenderPipelineFile/rpplugins/color_correction/shader/sharpen.frag.glsl +++ b/RenderPipelineFile/rpplugins/color_correction/shader/sharpen.frag.glsl @@ -1,60 +1,60 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/color_spaces.inc.glsl" - -uniform sampler2D ShadedScene; -out vec3 result; - -void main() { - - vec2 texcoord = (ivec2(gl_FragCoord.xy) + 0.5) / NATIVE_SCREEN_SIZE; - vec3 scene_color = textureLod(ShadedScene, texcoord, 0).xyz; - - // Compute the sharpen strength for each individual channel - const float sharpen_strength = GET_SETTING(color_correction, sharpen_strength); - - vec2 pixel_size = 1.0 / NATIVE_SCREEN_SIZE; - const float r = 0.8; - - // Blur arround the current pixel - vec3 blur_sum = vec3(0); - blur_sum += textureLod(ShadedScene, texcoord + vec2(r, -r) * pixel_size, 0).rgb; - blur_sum += textureLod(ShadedScene, texcoord + vec2(-r, -r) * pixel_size, 0).rgb; - blur_sum += textureLod(ShadedScene, texcoord + vec2(r, r) * pixel_size, 0).rgb; - blur_sum += textureLod(ShadedScene, texcoord + vec2(-r, r) * pixel_size, 0).rgb; - - vec3 pixel_diff = scene_color - blur_sum * 0.25; - - // Apply the sharpening - #if !DEBUG_MODE - scene_color += dot(pixel_diff, LUMA_COEFFS * sharpen_strength); - #endif - result = scene_color; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/color_spaces.inc.glsl" + +uniform sampler2D ShadedScene; +out vec3 result; + +void main() { + + vec2 texcoord = (ivec2(gl_FragCoord.xy) + 0.5) / NATIVE_SCREEN_SIZE; + vec3 scene_color = textureLod(ShadedScene, texcoord, 0).xyz; + + // Compute the sharpen strength for each individual channel + const float sharpen_strength = GET_SETTING(color_correction, sharpen_strength); + + vec2 pixel_size = 1.0 / NATIVE_SCREEN_SIZE; + const float r = 0.8; + + // Blur arround the current pixel + vec3 blur_sum = vec3(0); + blur_sum += textureLod(ShadedScene, texcoord + vec2(r, -r) * pixel_size, 0).rgb; + blur_sum += textureLod(ShadedScene, texcoord + vec2(-r, -r) * pixel_size, 0).rgb; + blur_sum += textureLod(ShadedScene, texcoord + vec2(r, r) * pixel_size, 0).rgb; + blur_sum += textureLod(ShadedScene, texcoord + vec2(-r, r) * pixel_size, 0).rgb; + + vec3 pixel_diff = scene_color - blur_sum * 0.25; + + // Apply the sharpening + #if !DEBUG_MODE + scene_color += dot(pixel_diff, LUMA_COEFFS * sharpen_strength); + #endif + result = scene_color; +} diff --git a/RenderPipelineFile/rpplugins/color_correction/sharpen_stage.py b/RenderPipelineFile/rpplugins/color_correction/sharpen_stage.py index 553ad601..d2607dc6 100644 --- a/RenderPipelineFile/rpplugins/color_correction/sharpen_stage.py +++ b/RenderPipelineFile/rpplugins/color_correction/sharpen_stage.py @@ -1,73 +1,73 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import division - -from rplibs.six import itervalues - -from rpcore.globals import Globals -from rpcore.render_stage import RenderStage - - -class SharpenStage(RenderStage): - - required_inputs = [] - required_pipes = ["ShadedScene"] - - def __init__(self, pipeline): - RenderStage.__init__(self, pipeline) - self.sharpen_twice = True - - @property - def produced_pipes(self): - if self.sharpen_twice: - return {"ShadedScene": self.target2.color_tex} - else: - return {"ShadedScene": self.target.color_tex} - - def create(self): - native_size = Globals.native_resolution.x, Globals.native_resolution.y - self.target = self.create_target("Sharpen") - self.target.size = native_size - self.target.add_color_attachment(bits=16) - self.target.prepare_buffer() - - if self.sharpen_twice: - self.target2 = self.create_target("Sharpen2") - self.target2.size = native_size - self.target2.add_color_attachment(bits=16) - self.target2.prepare_buffer() - self.target2.set_shader_input("ShadedScene", self.target.color_tex) - - def set_dimensions(self): - self.target.size = Globals.native_resolution.x, Globals.native_resolution.y - if self.sharpen_twice: - self.target2.size = Globals.native_resolution.x, Globals.native_resolution.y - - def reload_shaders(self): - sharpen_shader = self.load_plugin_shader("sharpen.frag.glsl") - for target in itervalues(self._targets): - target.shader = sharpen_shader +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import division + +from rplibs.six import itervalues + +from rpcore.globals import Globals +from rpcore.render_stage import RenderStage + + +class SharpenStage(RenderStage): + + required_inputs = [] + required_pipes = ["ShadedScene"] + + def __init__(self, pipeline): + RenderStage.__init__(self, pipeline) + self.sharpen_twice = True + + @property + def produced_pipes(self): + if self.sharpen_twice: + return {"ShadedScene": self.target2.color_tex} + else: + return {"ShadedScene": self.target.color_tex} + + def create(self): + native_size = Globals.native_resolution.x, Globals.native_resolution.y + self.target = self.create_target("Sharpen") + self.target.size = native_size + self.target.add_color_attachment(bits=16) + self.target.prepare_buffer() + + if self.sharpen_twice: + self.target2 = self.create_target("Sharpen2") + self.target2.size = native_size + self.target2.add_color_attachment(bits=16) + self.target2.prepare_buffer() + self.target2.set_shader_input("ShadedScene", self.target.color_tex) + + def set_dimensions(self): + self.target.size = Globals.native_resolution.x, Globals.native_resolution.y + if self.sharpen_twice: + self.target2.size = Globals.native_resolution.x, Globals.native_resolution.y + + def reload_shaders(self): + sharpen_shader = self.load_plugin_shader("sharpen.frag.glsl") + for target in itervalues(self._targets): + target.shader = sharpen_shader diff --git a/RenderPipelineFile/rpplugins/color_correction/tonemapping_stage.py b/RenderPipelineFile/rpplugins/color_correction/tonemapping_stage.py index f2f54a4b..4d78cda0 100644 --- a/RenderPipelineFile/rpplugins/color_correction/tonemapping_stage.py +++ b/RenderPipelineFile/rpplugins/color_correction/tonemapping_stage.py @@ -1,45 +1,45 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.render_stage import RenderStage - - -class TonemappingStage(RenderStage): - - required_inputs = [] - required_pipes = ["ShadedScene"] - - @property - def produced_pipes(self): - return {"ShadedScene": self.target.color_tex} - - def create(self): - self.target = self.create_target("Tonemap") - self.target.add_color_attachment(bits=16) - self.target.prepare_buffer() - - def reload_shaders(self): - self.target.shader = self.load_plugin_shader("apply_tonemap.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.render_stage import RenderStage + + +class TonemappingStage(RenderStage): + + required_inputs = [] + required_pipes = ["ShadedScene"] + + @property + def produced_pipes(self): + return {"ShadedScene": self.target.color_tex} + + def create(self): + self.target = self.create_target("Tonemap") + self.target.add_color_attachment(bits=16) + self.target.prepare_buffer() + + def reload_shaders(self): + self.target.shader = self.load_plugin_shader("apply_tonemap.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/dof/config.yaml b/RenderPipelineFile/rpplugins/dof/config.yaml index adb4ee95..26f608c9 100644 --- a/RenderPipelineFile/rpplugins/dof/config.yaml +++ b/RenderPipelineFile/rpplugins/dof/config.yaml @@ -1,42 +1,42 @@ - -settings: !!omap - - - focal_point: - type: float - range: [0.0, 10000.0] - default: 10.0 - shader_runtime: true - label: Focal point - description: > - The focal point distance to the camera. - - - focal_size: - type: float - range: [0.0, 5000.0] - default: 2.5 - label: Focal size - shader_runtime: true - description: > - Size of the focal region. - - - blur_strength: - type: float - range: [0.0, 1.0] - default: 0.5 - label: Blur strength - shader_runtime: true - description: > - Strength of the blur - Will get replaced by a physically based - parameter soon! - - - near_blur_strength: - type: float - range: [0.0, 1.0] - default: 0.5 - label: Near Blur strength - shader_runtime: true - description: > - Strength of the near-field blur - Will get replaced by a physically based - parameter soon! - -daytime_settings: + +settings: !!omap + + - focal_point: + type: float + range: [0.0, 10000.0] + default: 10.0 + shader_runtime: true + label: Focal point + description: > + The focal point distance to the camera. + + - focal_size: + type: float + range: [0.0, 5000.0] + default: 2.5 + label: Focal size + shader_runtime: true + description: > + Size of the focal region. + + - blur_strength: + type: float + range: [0.0, 1.0] + default: 0.5 + label: Blur strength + shader_runtime: true + description: > + Strength of the blur - Will get replaced by a physically based + parameter soon! + + - near_blur_strength: + type: float + range: [0.0, 1.0] + default: 0.5 + label: Near Blur strength + shader_runtime: true + description: > + Strength of the near-field blur - Will get replaced by a physically based + parameter soon! + +daytime_settings: diff --git a/RenderPipelineFile/rpplugins/dof/dof_stage.py b/RenderPipelineFile/rpplugins/dof/dof_stage.py index fef2631f..541978a7 100644 --- a/RenderPipelineFile/rpplugins/dof/dof_stage.py +++ b/RenderPipelineFile/rpplugins/dof/dof_stage.py @@ -1,75 +1,75 @@ -from rpcore.render_stage import RenderStage - - -class DoFStage(RenderStage): - - """ This stage does the DoF pass """ - - required_inputs = [] - required_pipes = ["ShadedScene", "GBuffer", "DownscaledDepth"] - - @property - def produced_pipes(self): - return {"ShadedScene": self.target_merge.color_tex} - - def create(self): - - self.target_prefilter = self.create_target("PrefilterDoF") - # self.target_prefilter.size = -2 - self.target_prefilter.add_color_attachment(bits=16, alpha=True) - self.target_prefilter.prepare_buffer() - - self.tile_size = 32 - self.tile_target = self.create_target("FetchVertDOF") - self.tile_target.size = -1, -self.tile_size - self.tile_target.add_color_attachment(bits=(16, 16, 0)) - self.tile_target.prepare_buffer() - - self.tile_target.set_shader_input("PrecomputedCoC", self.target_prefilter.color_tex) - - self.tile_target_horiz = self.create_target("FetchHorizDOF") - self.tile_target_horiz.size = -self.tile_size - self.tile_target_horiz.add_color_attachment(bits=(16, 16, 0)) - self.tile_target_horiz.prepare_buffer() - self.tile_target_horiz.set_shader_input("SourceTex", self.tile_target.color_tex) - - self.minmax_target = self.create_target("DoFNeighborMinMax") - self.minmax_target.size = -self.tile_size - self.minmax_target.add_color_attachment(bits=(16, 16, 0)) - self.minmax_target.prepare_buffer() - self.minmax_target.set_shader_input("TileMinMax", self.tile_target_horiz.color_tex) - - self.presort_target = self.create_target("DoFPresort") - self.presort_target.add_color_attachment(bits=(11, 11, 10)) - self.presort_target.prepare_buffer() - self.presort_target.set_shader_inputs( - TileMinMax=self.minmax_target.color_tex, - PrecomputedCoC=self.target_prefilter.color_tex) - - self.target = self.create_target("ComputeDoF") - # self.target.size = -2 - self.target.add_color_attachment(bits=16, alpha=True) - self.target.prepare_buffer() - self.target.set_shader_inputs( - PresortResult=self.presort_target.color_tex, - PrecomputedCoC=self.target_prefilter.color_tex, - TileMinMax=self.minmax_target.color_tex) - - self.target_merge = self.create_target("MergeDoF") - self.target_merge.add_color_attachment(bits=16) - self.target_merge.prepare_buffer() - self.target_merge.set_shader_input("SourceTex", self.target.color_tex) - - # self.target_upscale.set_shader_input("SourceTex", self.target.color_tex) - # self.target_upscale.set_shader_input("upscaleWeights", Vec2(0.001, 0.001)) - - def reload_shaders(self): - self.tile_target.shader = self.load_plugin_shader("fetch_dof_minmax.frag.glsl") - self.tile_target_horiz.shader = self.load_plugin_shader("fetch_dof_minmax_horiz.frag.glsl") - self.minmax_target.shader = self.load_plugin_shader("fetch_dof_tile_neighbors.frag.glsl") - self.presort_target.shader = self.load_plugin_shader("dof_presort.frag.glsl") - self.target_prefilter.shader = self.load_plugin_shader("prefilter_dof.frag.glsl") - self.target.shader = self.load_plugin_shader("compute_dof.frag.glsl") - self.target_merge.shader = self.load_plugin_shader("merge_dof.frag.glsl") - # self.target_upscale.shader = self.load_plugin_shader( - # "/$$rp/shader/bilateral_upscale.frag.glsl") +from rpcore.render_stage import RenderStage + + +class DoFStage(RenderStage): + + """ This stage does the DoF pass """ + + required_inputs = [] + required_pipes = ["ShadedScene", "GBuffer", "DownscaledDepth"] + + @property + def produced_pipes(self): + return {"ShadedScene": self.target_merge.color_tex} + + def create(self): + + self.target_prefilter = self.create_target("PrefilterDoF") + # self.target_prefilter.size = -2 + self.target_prefilter.add_color_attachment(bits=16, alpha=True) + self.target_prefilter.prepare_buffer() + + self.tile_size = 32 + self.tile_target = self.create_target("FetchVertDOF") + self.tile_target.size = -1, -self.tile_size + self.tile_target.add_color_attachment(bits=(16, 16, 0)) + self.tile_target.prepare_buffer() + + self.tile_target.set_shader_input("PrecomputedCoC", self.target_prefilter.color_tex) + + self.tile_target_horiz = self.create_target("FetchHorizDOF") + self.tile_target_horiz.size = -self.tile_size + self.tile_target_horiz.add_color_attachment(bits=(16, 16, 0)) + self.tile_target_horiz.prepare_buffer() + self.tile_target_horiz.set_shader_input("SourceTex", self.tile_target.color_tex) + + self.minmax_target = self.create_target("DoFNeighborMinMax") + self.minmax_target.size = -self.tile_size + self.minmax_target.add_color_attachment(bits=(16, 16, 0)) + self.minmax_target.prepare_buffer() + self.minmax_target.set_shader_input("TileMinMax", self.tile_target_horiz.color_tex) + + self.presort_target = self.create_target("DoFPresort") + self.presort_target.add_color_attachment(bits=(11, 11, 10)) + self.presort_target.prepare_buffer() + self.presort_target.set_shader_inputs( + TileMinMax=self.minmax_target.color_tex, + PrecomputedCoC=self.target_prefilter.color_tex) + + self.target = self.create_target("ComputeDoF") + # self.target.size = -2 + self.target.add_color_attachment(bits=16, alpha=True) + self.target.prepare_buffer() + self.target.set_shader_inputs( + PresortResult=self.presort_target.color_tex, + PrecomputedCoC=self.target_prefilter.color_tex, + TileMinMax=self.minmax_target.color_tex) + + self.target_merge = self.create_target("MergeDoF") + self.target_merge.add_color_attachment(bits=16) + self.target_merge.prepare_buffer() + self.target_merge.set_shader_input("SourceTex", self.target.color_tex) + + # self.target_upscale.set_shader_input("SourceTex", self.target.color_tex) + # self.target_upscale.set_shader_input("upscaleWeights", Vec2(0.001, 0.001)) + + def reload_shaders(self): + self.tile_target.shader = self.load_plugin_shader("fetch_dof_minmax.frag.glsl") + self.tile_target_horiz.shader = self.load_plugin_shader("fetch_dof_minmax_horiz.frag.glsl") + self.minmax_target.shader = self.load_plugin_shader("fetch_dof_tile_neighbors.frag.glsl") + self.presort_target.shader = self.load_plugin_shader("dof_presort.frag.glsl") + self.target_prefilter.shader = self.load_plugin_shader("prefilter_dof.frag.glsl") + self.target.shader = self.load_plugin_shader("compute_dof.frag.glsl") + self.target_merge.shader = self.load_plugin_shader("merge_dof.frag.glsl") + # self.target_upscale.shader = self.load_plugin_shader( + # "/$$rp/shader/bilateral_upscale.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/dof/plugin.py b/RenderPipelineFile/rpplugins/dof/plugin.py index 7dd00bc8..e0fd2317 100644 --- a/RenderPipelineFile/rpplugins/dof/plugin.py +++ b/RenderPipelineFile/rpplugins/dof/plugin.py @@ -1,22 +1,22 @@ -from rpcore.pluginbase.base_plugin import BasePlugin - -from .dof_stage import DoFStage - - -class Plugin(BasePlugin): - - name = "Depth of Field" - author = "tobspr " - description = ("Adds support for depth of field") - version = "alpha (!)" - - def on_stage_setup(self): - self._stage = self.create_stage(DoFStage) - - def on_pipeline_created(self): - pass - - def update_some_setting(self): - """ This method gets called when the setting "some_setting" - of your plugin gets called. You should do all work to update required - inputs etc. yourself. """ +from rpcore.pluginbase.base_plugin import BasePlugin + +from .dof_stage import DoFStage + + +class Plugin(BasePlugin): + + name = "Depth of Field" + author = "tobspr " + description = ("Adds support for depth of field") + version = "alpha (!)" + + def on_stage_setup(self): + self._stage = self.create_stage(DoFStage) + + def on_pipeline_created(self): + pass + + def update_some_setting(self): + """ This method gets called when the setting "some_setting" + of your plugin gets called. You should do all work to update required + inputs etc. yourself. """ diff --git a/RenderPipelineFile/rpplugins/dof/shader/compute_dof.frag.glsl b/RenderPipelineFile/rpplugins/dof/shader/compute_dof.frag.glsl index e7f25c2c..2e439df6 100644 --- a/RenderPipelineFile/rpplugins/dof/shader/compute_dof.frag.glsl +++ b/RenderPipelineFile/rpplugins/dof/shader/compute_dof.frag.glsl @@ -1,124 +1,124 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "dof.inc.glsl" -#pragma include "includes/noise.inc.glsl" - -uniform sampler2D ShadedScene; -uniform sampler2D PresortResult; -uniform sampler2D PrecomputedCoC; -uniform sampler2D TileMinMax; -out vec4 result; - -/* - -WORK IN PROGRESS - -This code is code in progress and such not formatted very nicely nor commented! - -*/ - -float intersect_circle(vec2 d, float radius) { - float latt = saturate(length(d * vec2(1, 1)) / radius); - latt = pow(latt, 10.0); - latt = 1 - latt; - return latt; - - if (length(d) <= radius) { - return 1.0; - } - return 0.0; -} - -void main() { - - vec2 texcoord = get_texcoord(); - ivec2 tile_coord = ivec2(ivec2(gl_FragCoord.xy) / vec2(tile_size)); - vec2 tile_data = texelFetch(TileMinMax, tile_coord, 0).xy; - float tile_max_depth = tile_data.x; - float tile_max_coc = tile_data.y; - - vec3 presort_result = textureLod(PresortResult, texcoord, 0).xyz; - float mid_coc = presort_result.x; - - if (tile_max_coc <= 1e-4) { - result = textureLod(ShadedScene, texcoord, 0); - result.w = 0; - } - - vec2 max_radius = 16.0 / SCREEN_SIZE; - - vec3 jitter = rand_rgb(texcoord); - - const int num_ring_samples = 4; - float max_length = length(max_radius * vec2(1, 1)); - - int num_samples = 0; - - vec3 scene_color = textureLod(ShadedScene, texcoord, 0).xyz; - - float alpha = 0.0; - vec4 foreground = vec4(scene_color, 1) * 1e-4; - vec4 background = vec4(scene_color, 1) * 1e-4; - - for (int ring = 0; ring <= num_ring_samples; ++ring) { - int n_samples = max(1, 8 * ring); - vec2 r = max_radius * ring / float(num_ring_samples) * (tile_max_coc); - // vec2 r = max_radius * ring / float(num_ring_samples); - - for (int i = 0; i < n_samples; ++i) { - ++num_samples; - float phi = i / float(n_samples) * TWO_PI; - float x_offs = sin(phi); - float y_offs = cos(phi); - - vec2 tcoord = texcoord + vec2(x_offs, y_offs) * r; - // tcoord += jitter.xy * 0.1 * r; - - // XXX: Instead of manual clamping, use a near filtered texture - tcoord = truncate_coordinate(tcoord); - - vec3 sample_data = textureLod(PresortResult, tcoord, 0).xyz; - vec3 color_data = textureLod(PrecomputedCoC, tcoord, 0).xyz; - - float coc_weight = intersect_circle(r, sample_data.x * max_length); - coc_weight *= 1.0 / max(1e-9, sample_data.x * sample_data.x); - foreground += vec4(color_data, 1) * coc_weight; - // foreground += vec4(color_data, 1) * coc_weight * sample_data.y; - // background += vec4(color_data, 1) * coc_weight * sample_data.z; - } - } - - foreground.xyz /= max(1e-5, foreground.w); - // background.xyz /= max(1e-5, background.w); - - // result = mix(background, foreground, saturate(2 * foreground.w / num_samples)); - result = foreground; - result.w = mid_coc; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "dof.inc.glsl" +#pragma include "includes/noise.inc.glsl" + +uniform sampler2D ShadedScene; +uniform sampler2D PresortResult; +uniform sampler2D PrecomputedCoC; +uniform sampler2D TileMinMax; +out vec4 result; + +/* + +WORK IN PROGRESS + +This code is code in progress and such not formatted very nicely nor commented! + +*/ + +float intersect_circle(vec2 d, float radius) { + float latt = saturate(length(d * vec2(1, 1)) / radius); + latt = pow(latt, 10.0); + latt = 1 - latt; + return latt; + + if (length(d) <= radius) { + return 1.0; + } + return 0.0; +} + +void main() { + + vec2 texcoord = get_texcoord(); + ivec2 tile_coord = ivec2(ivec2(gl_FragCoord.xy) / vec2(tile_size)); + vec2 tile_data = texelFetch(TileMinMax, tile_coord, 0).xy; + float tile_max_depth = tile_data.x; + float tile_max_coc = tile_data.y; + + vec3 presort_result = textureLod(PresortResult, texcoord, 0).xyz; + float mid_coc = presort_result.x; + + if (tile_max_coc <= 1e-4) { + result = textureLod(ShadedScene, texcoord, 0); + result.w = 0; + } + + vec2 max_radius = 16.0 / SCREEN_SIZE; + + vec3 jitter = rand_rgb(texcoord); + + const int num_ring_samples = 4; + float max_length = length(max_radius * vec2(1, 1)); + + int num_samples = 0; + + vec3 scene_color = textureLod(ShadedScene, texcoord, 0).xyz; + + float alpha = 0.0; + vec4 foreground = vec4(scene_color, 1) * 1e-4; + vec4 background = vec4(scene_color, 1) * 1e-4; + + for (int ring = 0; ring <= num_ring_samples; ++ring) { + int n_samples = max(1, 8 * ring); + vec2 r = max_radius * ring / float(num_ring_samples) * (tile_max_coc); + // vec2 r = max_radius * ring / float(num_ring_samples); + + for (int i = 0; i < n_samples; ++i) { + ++num_samples; + float phi = i / float(n_samples) * TWO_PI; + float x_offs = sin(phi); + float y_offs = cos(phi); + + vec2 tcoord = texcoord + vec2(x_offs, y_offs) * r; + // tcoord += jitter.xy * 0.1 * r; + + // XXX: Instead of manual clamping, use a near filtered texture + tcoord = truncate_coordinate(tcoord); + + vec3 sample_data = textureLod(PresortResult, tcoord, 0).xyz; + vec3 color_data = textureLod(PrecomputedCoC, tcoord, 0).xyz; + + float coc_weight = intersect_circle(r, sample_data.x * max_length); + coc_weight *= 1.0 / max(1e-9, sample_data.x * sample_data.x); + foreground += vec4(color_data, 1) * coc_weight; + // foreground += vec4(color_data, 1) * coc_weight * sample_data.y; + // background += vec4(color_data, 1) * coc_weight * sample_data.z; + } + } + + foreground.xyz /= max(1e-5, foreground.w); + // background.xyz /= max(1e-5, background.w); + + // result = mix(background, foreground, saturate(2 * foreground.w / num_samples)); + result = foreground; + result.w = mid_coc; +} diff --git a/RenderPipelineFile/rpplugins/dof/shader/dof.inc.glsl b/RenderPipelineFile/rpplugins/dof/shader/dof.inc.glsl index 5900462c..b31078b9 100644 --- a/RenderPipelineFile/rpplugins/dof/shader/dof.inc.glsl +++ b/RenderPipelineFile/rpplugins/dof/shader/dof.inc.glsl @@ -1,48 +1,48 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -const int tile_size = 32; - -#define DOF_DEPTH_SCALE_FOREGROUND 250.0 -#define DOF_SINGLE_PIXEL_RADIUS 1.0 -// #define DOF_SINGLE_PIXEL_RADIUS 1.0 - -vec2 compare_depth(float depth, float tile_max_depth) -{ - float d = DOF_DEPTH_SCALE_FOREGROUND * (tile_max_depth - depth); - d -= 0.7; - vec2 depth_cmp = vec2(0); - depth_cmp.x = smoothstep(0.0, 1.0, saturate(d)); - // depth_cmp.x = step(0.01, d); - depth_cmp.y = 1.0 - depth_cmp.x; - return depth_cmp; -} - -float sample_alpha(float sampleCoc) -{ - return min(1.0 / (M_PI * sampleCoc * sampleCoc), M_PI * - DOF_SINGLE_PIXEL_RADIUS * DOF_SINGLE_PIXEL_RADIUS); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +const int tile_size = 32; + +#define DOF_DEPTH_SCALE_FOREGROUND 250.0 +#define DOF_SINGLE_PIXEL_RADIUS 1.0 +// #define DOF_SINGLE_PIXEL_RADIUS 1.0 + +vec2 compare_depth(float depth, float tile_max_depth) +{ + float d = DOF_DEPTH_SCALE_FOREGROUND * (tile_max_depth - depth); + d -= 0.7; + vec2 depth_cmp = vec2(0); + depth_cmp.x = smoothstep(0.0, 1.0, saturate(d)); + // depth_cmp.x = step(0.01, d); + depth_cmp.y = 1.0 - depth_cmp.x; + return depth_cmp; +} + +float sample_alpha(float sampleCoc) +{ + return min(1.0 / (M_PI * sampleCoc * sampleCoc), M_PI * + DOF_SINGLE_PIXEL_RADIUS * DOF_SINGLE_PIXEL_RADIUS); +} diff --git a/RenderPipelineFile/rpplugins/dof/shader/dof_presort.frag.glsl b/RenderPipelineFile/rpplugins/dof/shader/dof_presort.frag.glsl index 40f7e7ea..30547e76 100644 --- a/RenderPipelineFile/rpplugins/dof/shader/dof_presort.frag.glsl +++ b/RenderPipelineFile/rpplugins/dof/shader/dof_presort.frag.glsl @@ -1,66 +1,66 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "dof.inc.glsl" - -uniform sampler2D PrecomputedCoC; -uniform sampler2D TileMinMax; -out vec3 result; - - -/* - -WORK IN PROGRESS - -This code is code in progress and such not formatted very nicely nor commented! - -*/ - -void main() { - - vec2 texcoord = get_texcoord(); - ivec2 tile_coord = ivec2(ivec2(gl_FragCoord.xy) / vec2(tile_size)); - vec2 tile_data = texelFetch(TileMinMax, tile_coord, 0).xy; - float tile_min_depth = tile_data.x; - float tile_max_coc = tile_data.y; - - float pixel_depth = get_depth_at(texcoord); - float pixel_coc = textureLod(PrecomputedCoC, texcoord, 0).w; - - float alpha = sample_alpha(pixel_coc); // XXX: Returns just strange values - // float alpha = sample_alpha(tile_max_coc * 24.0); // XXX: Returns just strange values - alpha = 1.0; - vec2 depth_weights = compare_depth(pixel_depth, tile_min_depth); - // Depth weights.x = Foreground - // Depth weights.y = Background - result = vec3(pixel_coc, depth_weights.xy * alpha); - // result = vec3(pow(tile_min_depth, 100.0)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "dof.inc.glsl" + +uniform sampler2D PrecomputedCoC; +uniform sampler2D TileMinMax; +out vec3 result; + + +/* + +WORK IN PROGRESS + +This code is code in progress and such not formatted very nicely nor commented! + +*/ + +void main() { + + vec2 texcoord = get_texcoord(); + ivec2 tile_coord = ivec2(ivec2(gl_FragCoord.xy) / vec2(tile_size)); + vec2 tile_data = texelFetch(TileMinMax, tile_coord, 0).xy; + float tile_min_depth = tile_data.x; + float tile_max_coc = tile_data.y; + + float pixel_depth = get_depth_at(texcoord); + float pixel_coc = textureLod(PrecomputedCoC, texcoord, 0).w; + + float alpha = sample_alpha(pixel_coc); // XXX: Returns just strange values + // float alpha = sample_alpha(tile_max_coc * 24.0); // XXX: Returns just strange values + alpha = 1.0; + vec2 depth_weights = compare_depth(pixel_depth, tile_min_depth); + // Depth weights.x = Foreground + // Depth weights.y = Background + result = vec3(pixel_coc, depth_weights.xy * alpha); + // result = vec3(pow(tile_min_depth, 100.0)); +} diff --git a/RenderPipelineFile/rpplugins/dof/shader/fetch_dof_minmax.frag.glsl b/RenderPipelineFile/rpplugins/dof/shader/fetch_dof_minmax.frag.glsl index 0343cb7f..49c5011b 100644 --- a/RenderPipelineFile/rpplugins/dof/shader/fetch_dof_minmax.frag.glsl +++ b/RenderPipelineFile/rpplugins/dof/shader/fetch_dof_minmax.frag.glsl @@ -1,60 +1,60 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma optionNV (unroll all) - -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "dof.inc.glsl" - - -uniform sampler2D PrecomputedCoC; - -out vec2 result; - -void main() { - ivec2 coord = ivec2(gl_FragCoord.xy); - ivec2 screen_coord = ivec2(coord.x, coord.y * tile_size); - - float max_depth = 0; - float max_coc = 0; - - for (int y = 0; y <= tile_size; y += 1) { - float coc = texelFetch(PrecomputedCoC, screen_coord + ivec2(0, y), 0).w; - float depth = texelFetch(GBuffer.Depth, screen_coord, 0).x; - - if (screen_coord.y + y >= WINDOW_HEIGHT) continue; - - max_depth = max(max_depth, depth); - max_coc = max(max_coc, coc); - - } - - result = vec2(max_depth, max_coc); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma optionNV (unroll all) + +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "dof.inc.glsl" + + +uniform sampler2D PrecomputedCoC; + +out vec2 result; + +void main() { + ivec2 coord = ivec2(gl_FragCoord.xy); + ivec2 screen_coord = ivec2(coord.x, coord.y * tile_size); + + float max_depth = 0; + float max_coc = 0; + + for (int y = 0; y <= tile_size; y += 1) { + float coc = texelFetch(PrecomputedCoC, screen_coord + ivec2(0, y), 0).w; + float depth = texelFetch(GBuffer.Depth, screen_coord, 0).x; + + if (screen_coord.y + y >= WINDOW_HEIGHT) continue; + + max_depth = max(max_depth, depth); + max_coc = max(max_coc, coc); + + } + + result = vec2(max_depth, max_coc); +} diff --git a/RenderPipelineFile/rpplugins/dof/shader/fetch_dof_minmax_horiz.frag.glsl b/RenderPipelineFile/rpplugins/dof/shader/fetch_dof_minmax_horiz.frag.glsl index 34706fad..11103417 100644 --- a/RenderPipelineFile/rpplugins/dof/shader/fetch_dof_minmax_horiz.frag.glsl +++ b/RenderPipelineFile/rpplugins/dof/shader/fetch_dof_minmax_horiz.frag.glsl @@ -1,57 +1,57 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma optionNV (unroll all) - -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "dof.inc.glsl" - -uniform sampler2D SourceTex; - -out vec2 result; - -void main() { - ivec2 coord = ivec2(gl_FragCoord.xy); - ivec2 source_coord = ivec2(coord.x * tile_size, coord.y); - - float max_depth = 0; - float max_coc = 0; - - for (int x = 0; x <= tile_size; x += 1) { - int x_coord = clamp(source_coord.x + x, 0, WINDOW_WIDTH - 1); - vec2 data = texelFetch(SourceTex, ivec2(x_coord, source_coord.y), 0).xy; - - max_depth = max(max_depth, data.x); - max_coc = max(max_coc, data.y); - - } - - result = vec2(max_depth, max_coc); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma optionNV (unroll all) + +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "dof.inc.glsl" + +uniform sampler2D SourceTex; + +out vec2 result; + +void main() { + ivec2 coord = ivec2(gl_FragCoord.xy); + ivec2 source_coord = ivec2(coord.x * tile_size, coord.y); + + float max_depth = 0; + float max_coc = 0; + + for (int x = 0; x <= tile_size; x += 1) { + int x_coord = clamp(source_coord.x + x, 0, WINDOW_WIDTH - 1); + vec2 data = texelFetch(SourceTex, ivec2(x_coord, source_coord.y), 0).xy; + + max_depth = max(max_depth, data.x); + max_coc = max(max_coc, data.y); + + } + + result = vec2(max_depth, max_coc); +} diff --git a/RenderPipelineFile/rpplugins/dof/shader/fetch_dof_tile_neighbors.frag.glsl b/RenderPipelineFile/rpplugins/dof/shader/fetch_dof_tile_neighbors.frag.glsl index 290d9958..0e05f53e 100644 --- a/RenderPipelineFile/rpplugins/dof/shader/fetch_dof_tile_neighbors.frag.glsl +++ b/RenderPipelineFile/rpplugins/dof/shader/fetch_dof_tile_neighbors.frag.glsl @@ -1,57 +1,57 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "dof.inc.glsl" - -uniform sampler2D TileMinMax; -out vec2 result; - -void main() { - - ivec2 tile = ivec2(gl_FragCoord.xy); - ivec2 screen_coord = tile * tile_size; - ivec2 max_tiles = textureSize(TileMinMax, 0) - 1; - - float max_depth = 0; - float max_coc = 0; - - const int filter_size = 1; - - for (int x = -filter_size; x <= filter_size; ++x) { - for (int y = -filter_size; y <= filter_size; ++y) { - ivec2 neighbor_coord = clamp(tile + ivec2(x, y), ivec2(0), ivec2(max_tiles)); - vec2 neighbor_data = texelFetch(TileMinMax, neighbor_coord, 0).xy; - max_depth = max(max_depth, neighbor_data.x); - max_coc = max(max_coc, neighbor_data.y); - } - } - - result = vec2(max_depth, max_coc); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "dof.inc.glsl" + +uniform sampler2D TileMinMax; +out vec2 result; + +void main() { + + ivec2 tile = ivec2(gl_FragCoord.xy); + ivec2 screen_coord = tile * tile_size; + ivec2 max_tiles = textureSize(TileMinMax, 0) - 1; + + float max_depth = 0; + float max_coc = 0; + + const int filter_size = 1; + + for (int x = -filter_size; x <= filter_size; ++x) { + for (int y = -filter_size; y <= filter_size; ++y) { + ivec2 neighbor_coord = clamp(tile + ivec2(x, y), ivec2(0), ivec2(max_tiles)); + vec2 neighbor_data = texelFetch(TileMinMax, neighbor_coord, 0).xy; + max_depth = max(max_depth, neighbor_data.x); + max_coc = max(max_coc, neighbor_data.y); + } + } + + result = vec2(max_depth, max_coc); +} diff --git a/RenderPipelineFile/rpplugins/dof/shader/merge_dof.frag.glsl b/RenderPipelineFile/rpplugins/dof/shader/merge_dof.frag.glsl index a8168a8c..e957f25d 100644 --- a/RenderPipelineFile/rpplugins/dof/shader/merge_dof.frag.glsl +++ b/RenderPipelineFile/rpplugins/dof/shader/merge_dof.frag.glsl @@ -1,56 +1,56 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/color_spaces.inc.glsl" - -uniform sampler2D ShadedScene; -uniform sampler2D SourceTex; -out vec3 result; - -void main() { - const float sharpness = 0.5; - - vec2 texcoord = get_texcoord(); - vec4 dof_result = textureLod(SourceTex, texcoord, 0); - vec3 scene_color = textureLod(ShadedScene, texcoord, 0).xyz; - - // Reconstruct original color - // dof_result.xyz = dof_result.xyz / (1 - (1 - sharpness) * get_luminance(dof_result.xyz)); - float dof_weight = saturate(dof_result.w * 10.0); - - #if DEBUG_MODE - dof_weight = 0; - #endif - - // result = mix(scene_color * 0, dof_result.xyz, dof_weight); - // result = mix(vec3(1, 0, 0), dof_result.xyz, dof_weight); - // result.xyz = vec3(dof_result.w); - result.xyz = dof_result.xyz; - // result.xyz = scene_color; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/color_spaces.inc.glsl" + +uniform sampler2D ShadedScene; +uniform sampler2D SourceTex; +out vec3 result; + +void main() { + const float sharpness = 0.5; + + vec2 texcoord = get_texcoord(); + vec4 dof_result = textureLod(SourceTex, texcoord, 0); + vec3 scene_color = textureLod(ShadedScene, texcoord, 0).xyz; + + // Reconstruct original color + // dof_result.xyz = dof_result.xyz / (1 - (1 - sharpness) * get_luminance(dof_result.xyz)); + float dof_weight = saturate(dof_result.w * 10.0); + + #if DEBUG_MODE + dof_weight = 0; + #endif + + // result = mix(scene_color * 0, dof_result.xyz, dof_weight); + // result = mix(vec3(1, 0, 0), dof_result.xyz, dof_weight); + // result.xyz = vec3(dof_result.w); + result.xyz = dof_result.xyz; + // result.xyz = scene_color; +} diff --git a/RenderPipelineFile/rpplugins/dof/shader/prefilter_dof.frag.glsl b/RenderPipelineFile/rpplugins/dof/shader/prefilter_dof.frag.glsl index 27c0f27c..b3814d3b 100644 --- a/RenderPipelineFile/rpplugins/dof/shader/prefilter_dof.frag.glsl +++ b/RenderPipelineFile/rpplugins/dof/shader/prefilter_dof.frag.glsl @@ -1,108 +1,108 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "includes/transforms.inc.glsl" -#pragma include "includes/color_spaces.inc.glsl" - -uniform sampler2D ShadedScene; - -out vec4 result; - -/* - -WORK IN PROGRESS - -This code is code in progress and such not formatted very nicely nor commented! - -*/ - - -vec3 karis_average(vec3 color) { - const float sharpness = 0.0; - return color / (1 + (1 - sharpness) * get_luminance(color)); -} - -void main() { - vec2 texcoord = get_texcoord(); - float mid_depth = textureLod(GBuffer.Depth, texcoord, 0).x; - - vec3 mid_color = textureLod(ShadedScene, texcoord, 0).xyz; - - vec3 accum = karis_average(mid_color.xyz) * 0; - float weights = 1.0 * 0.0; - - const float scale = 0.1 * GET_SETTING(dof, blur_strength); // XXX: Todo, make it physically based - const float focus_plane = GET_SETTING(dof, focal_point); - const float focus_size = GET_SETTING(dof, focal_size); - const float near_scale = GET_SETTING(dof, near_blur_strength) / - max(0.0, focus_plane - focus_size - CAMERA_NEAR); - float dist = get_linear_z_from_z(mid_depth); - - float coc = (dist - focus_plane); - - if (coc >= 0) { - coc -= focus_size; - coc *= scale; - } else { - coc += focus_size; - coc *= -near_scale; - } - - coc = clamp(coc, 0.0001, 1.0); - - const int kernel_size = 2; - for (int x = -kernel_size; x <= kernel_size; ++x) { - for (int y = -kernel_size; y <= kernel_size; ++y) { - // skip center sample - // if (x == 0 && y == 0) continue; - vec2 offcoord = texcoord + vec2(x, y) / SCREEN_SIZE; - vec3 sample_data = textureLod(ShadedScene, offcoord, 0).xyz; - float sample_depth = textureLod(GBuffer.Depth, offcoord, 0).x; - sample_data = karis_average(sample_data); - - float weight = 1 - saturate(abs(mid_depth - sample_depth) / 0.005); - // float weight = 1; - accum += sample_data * weight; - weights += weight; - - } - } - accum /= max(0.001, weights); - - accum = mid_color; - - if (coc < 0.01) { - // accum = karis_average(mid_color); - } - // accum = karis_average(mid_color); - - result = vec4(accum, coc); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "includes/transforms.inc.glsl" +#pragma include "includes/color_spaces.inc.glsl" + +uniform sampler2D ShadedScene; + +out vec4 result; + +/* + +WORK IN PROGRESS + +This code is code in progress and such not formatted very nicely nor commented! + +*/ + + +vec3 karis_average(vec3 color) { + const float sharpness = 0.0; + return color / (1 + (1 - sharpness) * get_luminance(color)); +} + +void main() { + vec2 texcoord = get_texcoord(); + float mid_depth = textureLod(GBuffer.Depth, texcoord, 0).x; + + vec3 mid_color = textureLod(ShadedScene, texcoord, 0).xyz; + + vec3 accum = karis_average(mid_color.xyz) * 0; + float weights = 1.0 * 0.0; + + const float scale = 0.1 * GET_SETTING(dof, blur_strength); // XXX: Todo, make it physically based + const float focus_plane = GET_SETTING(dof, focal_point); + const float focus_size = GET_SETTING(dof, focal_size); + const float near_scale = GET_SETTING(dof, near_blur_strength) / + max(0.0, focus_plane - focus_size - CAMERA_NEAR); + float dist = get_linear_z_from_z(mid_depth); + + float coc = (dist - focus_plane); + + if (coc >= 0) { + coc -= focus_size; + coc *= scale; + } else { + coc += focus_size; + coc *= -near_scale; + } + + coc = clamp(coc, 0.0001, 1.0); + + const int kernel_size = 2; + for (int x = -kernel_size; x <= kernel_size; ++x) { + for (int y = -kernel_size; y <= kernel_size; ++y) { + // skip center sample + // if (x == 0 && y == 0) continue; + vec2 offcoord = texcoord + vec2(x, y) / SCREEN_SIZE; + vec3 sample_data = textureLod(ShadedScene, offcoord, 0).xyz; + float sample_depth = textureLod(GBuffer.Depth, offcoord, 0).x; + sample_data = karis_average(sample_data); + + float weight = 1 - saturate(abs(mid_depth - sample_depth) / 0.005); + // float weight = 1; + accum += sample_data * weight; + weights += weight; + + } + } + accum /= max(0.001, weights); + + accum = mid_color; + + if (coc < 0.01) { + // accum = karis_average(mid_color); + } + // accum = karis_average(mid_color); + + result = vec4(accum, coc); +} diff --git a/RenderPipelineFile/rpplugins/env_probes/apply_envprobes_stage.py b/RenderPipelineFile/rpplugins/env_probes/apply_envprobes_stage.py index 149c798d..57c14d15 100644 --- a/RenderPipelineFile/rpplugins/env_probes/apply_envprobes_stage.py +++ b/RenderPipelineFile/rpplugins/env_probes/apply_envprobes_stage.py @@ -1,53 +1,53 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.render_stage import RenderStage -from rpcore.stages.ambient_stage import AmbientStage - - -class ApplyEnvprobesStage(RenderStage): - - """ This stage takes the per-cell environment probes and samples them """ - - required_inputs = ["EnvProbes"] - required_pipes = ["GBuffer", "PerCellProbes", "CellIndices"] - - @property - def produced_pipes(self): - return { - "EnvmapAmbientSpec": self.target.color_tex, - "EnvmapAmbientDiff": self.target.aux_tex[0] - } - - def create(self): - self.target = self.create_target("ApplyEnvmap") - self.target.add_color_attachment(bits=16, alpha=True) - self.target.add_aux_attachment(bits=16) - self.target.prepare_buffer() - AmbientStage.required_pipes += ["EnvmapAmbientSpec", "EnvmapAmbientDiff"] - - def reload_shaders(self): - self.target.shader = self.load_plugin_shader("apply_envprobes.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.render_stage import RenderStage +from rpcore.stages.ambient_stage import AmbientStage + + +class ApplyEnvprobesStage(RenderStage): + + """ This stage takes the per-cell environment probes and samples them """ + + required_inputs = ["EnvProbes"] + required_pipes = ["GBuffer", "PerCellProbes", "CellIndices"] + + @property + def produced_pipes(self): + return { + "EnvmapAmbientSpec": self.target.color_tex, + "EnvmapAmbientDiff": self.target.aux_tex[0] + } + + def create(self): + self.target = self.create_target("ApplyEnvmap") + self.target.add_color_attachment(bits=16, alpha=True) + self.target.add_aux_attachment(bits=16) + self.target.prepare_buffer() + AmbientStage.required_pipes += ["EnvmapAmbientSpec", "EnvmapAmbientDiff"] + + def reload_shaders(self): + self.target.shader = self.load_plugin_shader("apply_envprobes.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/env_probes/config.yaml b/RenderPipelineFile/rpplugins/env_probes/config.yaml index 6cc36caf..a11b81fe 100644 --- a/RenderPipelineFile/rpplugins/env_probes/config.yaml +++ b/RenderPipelineFile/rpplugins/env_probes/config.yaml @@ -1,72 +1,72 @@ - -settings: !!omap - - - probe_resolution: - type: power_of_two - range: [16, 512] - default: 128 - label: Specular cubemap resolution - description: > - Resolution of the specular cubemaps in pixels. This heavily impacts - the VRAM usage, so you shouldn't set it too high. - - - diffuse_probe_resolution: - type: power_of_two - range: [1, 32] - default: 2 - label: Diffuse cubemap resolution - description: > - Resolution of the diffuse cubemap in pixels, this can usually be quite low. - - - max_probes: - type: int - range: [1, 341] - default: 16 - label: Maximum probe count - description: > - Maximum amount of enviroment probes, try to set this as low - as possible, since it affects VRAM usage by a lot. The maximum - limit of this setting is caused by hardware. - - - max_probes_per_cell: - type: int - range: [1, 16] - default: 3 - label: Max overlapping probes - description: > - Controlls how many probes can overlay at a given location. - If you get artifacts at probe transitions, try increasing this. - -daytime_settings: !!omap - - - ambient_scale: - type: scalar - range: [0.0, 2.0] - default: 0.2 - label: Ambient scale - description: > - Controls the ambient scale for the enviroment probes, since no - full rendering is performed, this is an approximation - - - sky_ambient_scale: - type: scalar - range: [0.0, 2.0] - default: 0.1 - label: Sky Ambient scale - description: > - Controls the sky ambient scale for the enviroment probes, since no - full rendering is performed, this is an approximation - - - sun_ambient_scale: - type: scalar - range: [0.0, 2.0] - default: 0.1 - label: Sun Ambient scale - description: > - Controls the sun ambient scale for the enviroment probes, since no - full rendering is performed, this is an approximation - - - - - + +settings: !!omap + + - probe_resolution: + type: power_of_two + range: [16, 512] + default: 128 + label: Specular cubemap resolution + description: > + Resolution of the specular cubemaps in pixels. This heavily impacts + the VRAM usage, so you shouldn't set it too high. + + - diffuse_probe_resolution: + type: power_of_two + range: [1, 32] + default: 2 + label: Diffuse cubemap resolution + description: > + Resolution of the diffuse cubemap in pixels, this can usually be quite low. + + - max_probes: + type: int + range: [1, 341] + default: 16 + label: Maximum probe count + description: > + Maximum amount of enviroment probes, try to set this as low + as possible, since it affects VRAM usage by a lot. The maximum + limit of this setting is caused by hardware. + + - max_probes_per_cell: + type: int + range: [1, 16] + default: 3 + label: Max overlapping probes + description: > + Controlls how many probes can overlay at a given location. + If you get artifacts at probe transitions, try increasing this. + +daytime_settings: !!omap + + - ambient_scale: + type: scalar + range: [0.0, 2.0] + default: 0.2 + label: Ambient scale + description: > + Controls the ambient scale for the enviroment probes, since no + full rendering is performed, this is an approximation + + - sky_ambient_scale: + type: scalar + range: [0.0, 2.0] + default: 0.1 + label: Sky Ambient scale + description: > + Controls the sky ambient scale for the enviroment probes, since no + full rendering is performed, this is an approximation + + - sun_ambient_scale: + type: scalar + range: [0.0, 2.0] + default: 0.1 + label: Sun Ambient scale + description: > + Controls the sun ambient scale for the enviroment probes, since no + full rendering is performed, this is an approximation + + + + + diff --git a/RenderPipelineFile/rpplugins/env_probes/cull_probes_stage.py b/RenderPipelineFile/rpplugins/env_probes/cull_probes_stage.py index 403d38ac..dbc0ca1b 100644 --- a/RenderPipelineFile/rpplugins/env_probes/cull_probes_stage.py +++ b/RenderPipelineFile/rpplugins/env_probes/cull_probes_stage.py @@ -1,74 +1,74 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -import math - -from rpcore.render_stage import RenderStage -from rpcore.image import Image - - -class CullProbesStage(RenderStage): - - """ This stage takes the list of used cells and creates a list of environment - probes for each cell """ - - required_inputs = ["EnvProbes"] - required_pipes = ["CellListBuffer"] - - def __init__(self, pipeline): - RenderStage.__init__(self, pipeline) - self.max_probes_per_cell = 4 - self.slice_width = pipeline.settings["lighting.culling_slice_width"] - - @property - def produced_pipes(self): - return {"PerCellProbes": self.per_cell_probes} - - @property - def produced_defines(self): - return {"MAX_PROBES_PER_CELL": self.max_probes_per_cell} - - def create(self): - # TODO: Use no oversized triangle in this stage - self.target = self.create_target("CullProbes") - self.target.size = 0, 0 - self.target.prepare_buffer() - - self.per_cell_probes = Image.create_buffer("PerCellProbes", 0, "R32I") - self.per_cell_probes.clear_image() - self.target.set_shader_inputs( - PerCellProbes=self.per_cell_probes, - threadCount=1) - - def set_dimensions(self): - max_cells = self._pipeline.light_mgr.total_tiles - num_rows = int(math.ceil(max_cells / float(self.slice_width))) - self.per_cell_probes.set_x_size(max_cells * self.max_probes_per_cell) - self.target.size = self.slice_width, num_rows - - def reload_shaders(self): - self.target.shader = self.load_plugin_shader( - "/$$rp/shader/tiled_culling.vert.glsl", "cull_probes.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +import math + +from rpcore.render_stage import RenderStage +from rpcore.image import Image + + +class CullProbesStage(RenderStage): + + """ This stage takes the list of used cells and creates a list of environment + probes for each cell """ + + required_inputs = ["EnvProbes"] + required_pipes = ["CellListBuffer"] + + def __init__(self, pipeline): + RenderStage.__init__(self, pipeline) + self.max_probes_per_cell = 4 + self.slice_width = pipeline.settings["lighting.culling_slice_width"] + + @property + def produced_pipes(self): + return {"PerCellProbes": self.per_cell_probes} + + @property + def produced_defines(self): + return {"MAX_PROBES_PER_CELL": self.max_probes_per_cell} + + def create(self): + # TODO: Use no oversized triangle in this stage + self.target = self.create_target("CullProbes") + self.target.size = 0, 0 + self.target.prepare_buffer() + + self.per_cell_probes = Image.create_buffer("PerCellProbes", 0, "R32I") + self.per_cell_probes.clear_image() + self.target.set_shader_inputs( + PerCellProbes=self.per_cell_probes, + threadCount=1) + + def set_dimensions(self): + max_cells = self._pipeline.light_mgr.total_tiles + num_rows = int(math.ceil(max_cells / float(self.slice_width))) + self.per_cell_probes.set_x_size(max_cells * self.max_probes_per_cell) + self.target.size = self.slice_width, num_rows + + def reload_shaders(self): + self.target.shader = self.load_plugin_shader( + "/$$rp/shader/tiled_culling.vert.glsl", "cull_probes.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/env_probes/environment_capture_stage.py b/RenderPipelineFile/rpplugins/env_probes/environment_capture_stage.py index b075b91c..e0bcd64d 100644 --- a/RenderPipelineFile/rpplugins/env_probes/environment_capture_stage.py +++ b/RenderPipelineFile/rpplugins/env_probes/environment_capture_stage.py @@ -1,193 +1,193 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import division -from six.moves import range # pylint: disable=import-error -from rplibs.six import itervalues - -from panda3d.core import Camera, PerspectiveLens, Vec4, Vec3, PTAInt - -from rpcore.globals import Globals -from rpcore.image import Image -from rpcore.render_stage import RenderStage - - -class EnvironmentCaptureStage(RenderStage): - - """ This stage renders the scene to a cubemap """ - - required_inputs = ["DefaultEnvmap", "AllLightsData", "maxLightIndex", "IESDatasetTex"] - required_pipes = [] - - def __init__(self, pipeline): - RenderStage.__init__(self, pipeline) - self.resolution = 128 - self.diffuse_resolution = 4 - self.regions = [] - self.cameras = [] - self.rig_node = Globals.render.attach_new_node("EnvmapCamRig") - self.pta_index = PTAInt.empty_array(1) - self.storage_tex = None - self.storage_tex_diffuse = None - - def create(self): - self.target = self.create_target("CaptureScene") - self.target.size = self.resolution * 6, self.resolution - self.target.add_depth_attachment(bits=16) - self.target.add_color_attachment(bits=16, alpha=True) - self.target.prepare_render(None) - - # Remove all unused display regions - internal_buffer = self.target.internal_buffer - internal_buffer.remove_all_display_regions() - internal_buffer.disable_clears() - internal_buffer.get_overlay_display_region().disable_clears() - - self._setup_camera_rig() - self._create_store_targets() - self._create_filter_targets() - - def _setup_camera_rig(self): - """ Setups the cameras to render a cubemap """ - directions = (Vec3(1, 0, 0), Vec3(-1, 0, 0), Vec3(0, 1, 0), - Vec3(0, -1, 0), Vec3(0, 0, 1), Vec3(0, 0, -1)) - - # Prepare the display regions - for i in range(6): - region = self.target.internal_buffer.make_display_region( - i / 6, i / 6 + 1 / 6, 0, 1) - region.set_sort(25 + i) - region.set_active(True) - region.disable_clears() - - # Set the correct clears - region.set_clear_depth_active(True) - region.set_clear_depth(1.0) - region.set_clear_color_active(True) - region.set_clear_color(Vec4(0)) - - lens = PerspectiveLens() - lens.set_fov(90) - lens.set_near_far(0.001, 1.0) - camera = Camera("EnvmapCam-" + str(i), lens) - camera_np = self.rig_node.attach_new_node(camera) - camera_np.look_at(camera_np, directions[i]) - region.set_camera(camera_np) - self.regions.append(region) - self.cameras.append(camera_np) - - self.cameras[0].set_r(90) - self.cameras[1].set_r(-90) - self.cameras[3].set_r(180) - self.cameras[5].set_r(180) - - # Register cameras - for camera_np in self.cameras: - self._pipeline.tag_mgr.register_camera("envmap", camera_np.node()) - - def _create_store_targets(self): - """ Creates the targets which copy the result texture into the actual storage """ - self.target_store = self.create_target("StoreCubemap") - self.target_store.size = self.resolution * 6, self.resolution - self.target_store.prepare_buffer() - self.target_store.set_shader_inputs( - SourceTex=self.target.color_tex, - DestTex=self.storage_tex, - currentIndex=self.pta_index) - - self.temporary_diffuse_map = Image.create_cube("DiffuseTemp", self.resolution, "RGBA16") - self.target_store_diff = self.create_target("StoreCubemapDiffuse") - self.target_store_diff.size = self.resolution * 6, self.resolution - self.target_store_diff.prepare_buffer() - self.target_store_diff.set_shader_inputs( - SourceTex=self.target.color_tex, - DestTex=self.temporary_diffuse_map, - currentIndex=self.pta_index) - - def _create_filter_targets(self): - """ Generates the targets which filter the specular cubemap """ - self.filter_targets = [] - mip = 0 - size = self.resolution - while size > 1: - size = size // 2 - mip += 1 - target = self.create_target("FilterCubemap:{0}-{1}x{1}".format(mip, size)) - target.size = size * 6, size - target.prepare_buffer() - target.set_shader_inputs( - currentIndex=self.pta_index, - currentMip=mip, - SourceTex=self.storage_tex) - target.set_shader_input("DestTex", self.storage_tex, False, True, -1, mip, 0) - self.filter_targets.append(target) - - # Target to filter the diffuse cubemap - self.filter_diffuse_target = self.create_target("FilterCubemapDiffuse") - self.filter_diffuse_target.size = self.diffuse_resolution * 6, self.diffuse_resolution - self.filter_diffuse_target.prepare_buffer() - self.filter_diffuse_target.set_shader_inputs( - SourceTex=self.temporary_diffuse_map, - DestTex=self.storage_tex_diffuse, - currentIndex=self.pta_index) - - def set_probe(self, probe): - self.rig_node.set_mat(probe.matrix) - self.pta_index[0] = probe.index - - def update(self): - # First, disable all targets - for target in itervalues(self._targets): - target.active = False - - # Check for updated faces - for i in range(6): - if self._pipeline.task_scheduler.is_scheduled("envprobes_capture_envmap_face" + str(i)): - self.regions[i].set_active(True) - - # Check for filtering - if self._pipeline.task_scheduler.is_scheduled("envprobes_filter_and_store_envmap"): - self.target_store.active = True - self.target_store_diff.active = True - self.filter_diffuse_target.active = True - for target in self.filter_targets: - target.active = True - - def set_shader_input(self, *args): - Globals.render.set_shader_input(*args) - - def set_shader_inputs(self, **kwargs): - Globals.render.set_shader_inputs(**kwargs) - - def reload_shaders(self): - self.target_store.shader = self.load_plugin_shader( - "store_cubemap.frag.glsl") - self.target_store_diff.shader = self.load_plugin_shader( - "store_cubemap_diffuse.frag.glsl") - self.filter_diffuse_target.shader = self.load_plugin_shader( - "filter_cubemap_diffuse.frag.glsl") - for i, target in enumerate(self.filter_targets): - target.shader = self.load_plugin_shader("mips/{}.autogen.glsl".format(i)) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import division +from six.moves import range # pylint: disable=import-error +from rplibs.six import itervalues + +from panda3d.core import Camera, PerspectiveLens, Vec4, Vec3, PTAInt + +from rpcore.globals import Globals +from rpcore.image import Image +from rpcore.render_stage import RenderStage + + +class EnvironmentCaptureStage(RenderStage): + + """ This stage renders the scene to a cubemap """ + + required_inputs = ["DefaultEnvmap", "AllLightsData", "maxLightIndex", "IESDatasetTex"] + required_pipes = [] + + def __init__(self, pipeline): + RenderStage.__init__(self, pipeline) + self.resolution = 128 + self.diffuse_resolution = 4 + self.regions = [] + self.cameras = [] + self.rig_node = Globals.render.attach_new_node("EnvmapCamRig") + self.pta_index = PTAInt.empty_array(1) + self.storage_tex = None + self.storage_tex_diffuse = None + + def create(self): + self.target = self.create_target("CaptureScene") + self.target.size = self.resolution * 6, self.resolution + self.target.add_depth_attachment(bits=16) + self.target.add_color_attachment(bits=16, alpha=True) + self.target.prepare_render(None) + + # Remove all unused display regions + internal_buffer = self.target.internal_buffer + internal_buffer.remove_all_display_regions() + internal_buffer.disable_clears() + internal_buffer.get_overlay_display_region().disable_clears() + + self._setup_camera_rig() + self._create_store_targets() + self._create_filter_targets() + + def _setup_camera_rig(self): + """ Setups the cameras to render a cubemap """ + directions = (Vec3(1, 0, 0), Vec3(-1, 0, 0), Vec3(0, 1, 0), + Vec3(0, -1, 0), Vec3(0, 0, 1), Vec3(0, 0, -1)) + + # Prepare the display regions + for i in range(6): + region = self.target.internal_buffer.make_display_region( + i / 6, i / 6 + 1 / 6, 0, 1) + region.set_sort(25 + i) + region.set_active(True) + region.disable_clears() + + # Set the correct clears + region.set_clear_depth_active(True) + region.set_clear_depth(1.0) + region.set_clear_color_active(True) + region.set_clear_color(Vec4(0)) + + lens = PerspectiveLens() + lens.set_fov(90) + lens.set_near_far(0.001, 1.0) + camera = Camera("EnvmapCam-" + str(i), lens) + camera_np = self.rig_node.attach_new_node(camera) + camera_np.look_at(camera_np, directions[i]) + region.set_camera(camera_np) + self.regions.append(region) + self.cameras.append(camera_np) + + self.cameras[0].set_r(90) + self.cameras[1].set_r(-90) + self.cameras[3].set_r(180) + self.cameras[5].set_r(180) + + # Register cameras + for camera_np in self.cameras: + self._pipeline.tag_mgr.register_camera("envmap", camera_np.node()) + + def _create_store_targets(self): + """ Creates the targets which copy the result texture into the actual storage """ + self.target_store = self.create_target("StoreCubemap") + self.target_store.size = self.resolution * 6, self.resolution + self.target_store.prepare_buffer() + self.target_store.set_shader_inputs( + SourceTex=self.target.color_tex, + DestTex=self.storage_tex, + currentIndex=self.pta_index) + + self.temporary_diffuse_map = Image.create_cube("DiffuseTemp", self.resolution, "RGBA16") + self.target_store_diff = self.create_target("StoreCubemapDiffuse") + self.target_store_diff.size = self.resolution * 6, self.resolution + self.target_store_diff.prepare_buffer() + self.target_store_diff.set_shader_inputs( + SourceTex=self.target.color_tex, + DestTex=self.temporary_diffuse_map, + currentIndex=self.pta_index) + + def _create_filter_targets(self): + """ Generates the targets which filter the specular cubemap """ + self.filter_targets = [] + mip = 0 + size = self.resolution + while size > 1: + size = size // 2 + mip += 1 + target = self.create_target("FilterCubemap:{0}-{1}x{1}".format(mip, size)) + target.size = size * 6, size + target.prepare_buffer() + target.set_shader_inputs( + currentIndex=self.pta_index, + currentMip=mip, + SourceTex=self.storage_tex) + target.set_shader_input("DestTex", self.storage_tex, False, True, -1, mip, 0) + self.filter_targets.append(target) + + # Target to filter the diffuse cubemap + self.filter_diffuse_target = self.create_target("FilterCubemapDiffuse") + self.filter_diffuse_target.size = self.diffuse_resolution * 6, self.diffuse_resolution + self.filter_diffuse_target.prepare_buffer() + self.filter_diffuse_target.set_shader_inputs( + SourceTex=self.temporary_diffuse_map, + DestTex=self.storage_tex_diffuse, + currentIndex=self.pta_index) + + def set_probe(self, probe): + self.rig_node.set_mat(probe.matrix) + self.pta_index[0] = probe.index + + def update(self): + # First, disable all targets + for target in itervalues(self._targets): + target.active = False + + # Check for updated faces + for i in range(6): + if self._pipeline.task_scheduler.is_scheduled("envprobes_capture_envmap_face" + str(i)): + self.regions[i].set_active(True) + + # Check for filtering + if self._pipeline.task_scheduler.is_scheduled("envprobes_filter_and_store_envmap"): + self.target_store.active = True + self.target_store_diff.active = True + self.filter_diffuse_target.active = True + for target in self.filter_targets: + target.active = True + + def set_shader_input(self, *args): + Globals.render.set_shader_input(*args) + + def set_shader_inputs(self, **kwargs): + Globals.render.set_shader_inputs(**kwargs) + + def reload_shaders(self): + self.target_store.shader = self.load_plugin_shader( + "store_cubemap.frag.glsl") + self.target_store_diff.shader = self.load_plugin_shader( + "store_cubemap_diffuse.frag.glsl") + self.filter_diffuse_target.shader = self.load_plugin_shader( + "filter_cubemap_diffuse.frag.glsl") + for i, target in enumerate(self.filter_targets): + target.shader = self.load_plugin_shader("mips/{}.autogen.glsl".format(i)) diff --git a/RenderPipelineFile/rpplugins/env_probes/environment_probe.py b/RenderPipelineFile/rpplugins/env_probes/environment_probe.py index 4643f2e9..2fd9656f 100644 --- a/RenderPipelineFile/rpplugins/env_probes/environment_probe.py +++ b/RenderPipelineFile/rpplugins/env_probes/environment_probe.py @@ -1,133 +1,133 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -import struct -from panda3d.core import TransformState, Vec3, Mat4, BoundingSphere - -from rpcore.rpobject import RPObject - - -class EnvironmentProbe(RPObject): - """ Simple class, representing an environment probe """ - - def __init__(self): - """ Inits a new environment probe """ - RPObject.__init__(self) - self.index = -1 - self.last_update = -1 - self._transform = TransformState.make_identity() - self._bounds = BoundingSphere(Vec3(0), 1.0) - self._modified = True - self._parallax_correction = True - self._border_smoothness = 0.1 - - @property - def modified(self): - """ Returns whether the probe was modified since the last write """ - return self._modified - - @property - def bounds(self): - """ Returns the probes worldspace bounds """ - return self._bounds - - @property - def parallax_correction(self): - """ Returns whether parallax correction is enabled for this probe """ - return self._parallax_correction - - @parallax_correction.setter - def parallax_correction(self, value): - """ Sets whether parallax correction is enabled for this probe """ - self._parallax_correction = value - self._modified = True - - @property - def border_smoothness(self): - """ Returns the border smoothness factor """ - return self._border_smoothness - - @border_smoothness.setter - def border_smoothness(self, value): - """ Sets the border smoothness factor """ - self._border_smoothness = value - self._modified = True - - def set_pos(self, *args): - """ Sets the probe position """ - self._transform = self._transform.set_pos(Vec3(*args)) - self.update_bounds() - - def set_hpr(self, *args): - """ Sets the probe rotation """ - self._transform = self._transform.set_hpr(Vec3(*args)) - self.update_bounds() - - def set_scale(self, *args): - """ Sets the probe scale """ - self._transform = self._transform.set_scale(Vec3(*args)) - self.update_bounds() - - def set_mat(self, matrix): - """ Sets the probes matrix, overrides all other transforms """ - self._transform = TransformState.make_mat(matrix) - self.update_bounds() - - def update_bounds(self): - """ Updates the spheres bounds """ - mat = self._transform.get_mat() - mid_point = mat.xform_point(Vec3(0, 0, 0)) - max_point = mat.xform_point(Vec3(1, 1, 1)) - radius = (mid_point - max_point).length() - self._bounds = BoundingSphere(mid_point, radius) - self._modified = True - - @property - def matrix(self): - """ Returns the matrix of the probe """ - return self._transform.get_mat() - - def write_to_buffer(self, buffer_ptr): - """ Writes the probe to a given byte buffer """ - data, mat = [], Mat4(self._transform.get_mat()) - mat.invert_in_place() - for i in range(4): - for j in range(3): - data.append(mat.get_cell(i, j)) - data += (float(self.index), - 1.0 if self._parallax_correction else 0.0, - self._border_smoothness, 0) - data.append(self._bounds.get_center().x) - data.append(self._bounds.get_center().y) - data.append(self._bounds.get_center().z) - data.append(self._bounds.get_radius()) - byte_data = struct.pack("f" * 20, *data) - - # 4 = sizeof float, 20 = floats per cubemap - bytes_per_probe = 4 * 20 - buffer_ptr.set_subdata(self.index * bytes_per_probe, bytes_per_probe, byte_data) - - self._modified = False +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +import struct +from panda3d.core import TransformState, Vec3, Mat4, BoundingSphere + +from rpcore.rpobject import RPObject + + +class EnvironmentProbe(RPObject): + """ Simple class, representing an environment probe """ + + def __init__(self): + """ Inits a new environment probe """ + RPObject.__init__(self) + self.index = -1 + self.last_update = -1 + self._transform = TransformState.make_identity() + self._bounds = BoundingSphere(Vec3(0), 1.0) + self._modified = True + self._parallax_correction = True + self._border_smoothness = 0.1 + + @property + def modified(self): + """ Returns whether the probe was modified since the last write """ + return self._modified + + @property + def bounds(self): + """ Returns the probes worldspace bounds """ + return self._bounds + + @property + def parallax_correction(self): + """ Returns whether parallax correction is enabled for this probe """ + return self._parallax_correction + + @parallax_correction.setter + def parallax_correction(self, value): + """ Sets whether parallax correction is enabled for this probe """ + self._parallax_correction = value + self._modified = True + + @property + def border_smoothness(self): + """ Returns the border smoothness factor """ + return self._border_smoothness + + @border_smoothness.setter + def border_smoothness(self, value): + """ Sets the border smoothness factor """ + self._border_smoothness = value + self._modified = True + + def set_pos(self, *args): + """ Sets the probe position """ + self._transform = self._transform.set_pos(Vec3(*args)) + self.update_bounds() + + def set_hpr(self, *args): + """ Sets the probe rotation """ + self._transform = self._transform.set_hpr(Vec3(*args)) + self.update_bounds() + + def set_scale(self, *args): + """ Sets the probe scale """ + self._transform = self._transform.set_scale(Vec3(*args)) + self.update_bounds() + + def set_mat(self, matrix): + """ Sets the probes matrix, overrides all other transforms """ + self._transform = TransformState.make_mat(matrix) + self.update_bounds() + + def update_bounds(self): + """ Updates the spheres bounds """ + mat = self._transform.get_mat() + mid_point = mat.xform_point(Vec3(0, 0, 0)) + max_point = mat.xform_point(Vec3(1, 1, 1)) + radius = (mid_point - max_point).length() + self._bounds = BoundingSphere(mid_point, radius) + self._modified = True + + @property + def matrix(self): + """ Returns the matrix of the probe """ + return self._transform.get_mat() + + def write_to_buffer(self, buffer_ptr): + """ Writes the probe to a given byte buffer """ + data, mat = [], Mat4(self._transform.get_mat()) + mat.invert_in_place() + for i in range(4): + for j in range(3): + data.append(mat.get_cell(i, j)) + data += (float(self.index), + 1.0 if self._parallax_correction else 0.0, + self._border_smoothness, 0) + data.append(self._bounds.get_center().x) + data.append(self._bounds.get_center().y) + data.append(self._bounds.get_center().z) + data.append(self._bounds.get_radius()) + byte_data = struct.pack("f" * 20, *data) + + # 4 = sizeof float, 20 = floats per cubemap + bytes_per_probe = 4 * 20 + buffer_ptr.set_subdata(self.index * bytes_per_probe, bytes_per_probe, byte_data) + + self._modified = False diff --git a/RenderPipelineFile/rpplugins/env_probes/plugin.py b/RenderPipelineFile/rpplugins/env_probes/plugin.py index 7205158d..aa8cc005 100644 --- a/RenderPipelineFile/rpplugins/env_probes/plugin.py +++ b/RenderPipelineFile/rpplugins/env_probes/plugin.py @@ -1,111 +1,111 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import PTAInt - -from rpcore.globals import Globals -from rpcore.util.shader_input_blocks import SimpleInputBlock -from rpcore.pluginbase.base_plugin import BasePlugin -from rpcore.stages.cull_lights_stage import CullLightsStage - -from .probe_manager import ProbeManager -from .environment_capture_stage import EnvironmentCaptureStage -from .apply_envprobes_stage import ApplyEnvprobesStage -from .cull_probes_stage import CullProbesStage - - -class Plugin(BasePlugin): - - name = "Environment Probes" - author = "tobspr " - description = ("This plugin adds support for environment probes, containing " - "diffuse and specular information. This enables accurate " - "reflections, and can also be used to simulate GI.") - version = "beta (!)" - - def on_stage_setup(self): - self.probe_mgr = ProbeManager() - self.probe_mgr.resolution = self.get_setting("probe_resolution") - self.probe_mgr.diffuse_resolution = self.get_setting("diffuse_probe_resolution") - self.probe_mgr.max_probes = self.get_setting("max_probes") - self.probe_mgr.init() - self._setup_stages() - - def _setup_stages(self): - """ Setups all stages """ - # Create the stage to generate and update the cubemaps - self.capture_stage = self.create_stage(EnvironmentCaptureStage) - self.capture_stage.resolution = self.probe_mgr.resolution - self.capture_stage.diffuse_resolution = self.probe_mgr.diffuse_resolution - self.capture_stage.storage_tex = self.probe_mgr.cubemap_storage - self.capture_stage.storage_tex_diffuse = self.probe_mgr.diffuse_storage - - # Create the stage to cull the cubemaps - self.cull_stage = self.create_stage(CullProbesStage) - - # Create the stage to apply the cubemaps - self.apply_stage = self.create_stage(ApplyEnvprobesStage) - - if self.is_plugin_enabled("scattering"): - self.capture_stage.required_pipes += ["ScatteringIBLSpecular", "ScatteringIBLDiffuse"] - - if self.is_plugin_enabled("pssm"): - self.capture_stage.required_pipes += ["PSSMSceneSunShadowMapPCF"] - self.capture_stage.required_inputs += ["PSSMSceneSunShadowMVP"] - - self._setup_inputs() - - def _setup_inputs(self): - """ Sets all required inputs """ - self.pta_probes = PTAInt.empty_array(1) - - # Construct the UBO which stores all environment probe data - self.data_ubo = SimpleInputBlock("EnvProbes") - self.data_ubo.add_input("num_probes", self.pta_probes) - self.data_ubo.add_input("cubemaps", self.probe_mgr.cubemap_storage) - self.data_ubo.add_input("diffuse_cubemaps", self.probe_mgr.diffuse_storage) - self.data_ubo.add_input("dataset", self.probe_mgr.dataset_storage) - self._pipeline.stage_mgr.input_blocks.append(self.data_ubo) - - # Use the UBO in light culling - CullLightsStage.required_inputs.append("EnvProbes") - - def on_pre_render_update(self): - if self._pipeline.task_scheduler.is_scheduled("envprobes_select_and_cull"): - self.probe_mgr.update() - self.pta_probes[0] = self.probe_mgr.num_probes - probe = self.probe_mgr.find_probe_to_update() - if probe: - probe.last_update = Globals.clock.get_frame_count() - self.capture_stage.active = True - self.capture_stage.set_probe(probe) - - if self.is_plugin_enabled("pssm"): - self.get_plugin_instance("pssm").scene_shadow_stage.request_focus( - probe.bounds.get_center(), probe.bounds.get_radius() - ) - else: - self.capture_stage.active = False +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import PTAInt + +from rpcore.globals import Globals +from rpcore.util.shader_input_blocks import SimpleInputBlock +from rpcore.pluginbase.base_plugin import BasePlugin +from rpcore.stages.cull_lights_stage import CullLightsStage + +from .probe_manager import ProbeManager +from .environment_capture_stage import EnvironmentCaptureStage +from .apply_envprobes_stage import ApplyEnvprobesStage +from .cull_probes_stage import CullProbesStage + + +class Plugin(BasePlugin): + + name = "Environment Probes" + author = "tobspr " + description = ("This plugin adds support for environment probes, containing " + "diffuse and specular information. This enables accurate " + "reflections, and can also be used to simulate GI.") + version = "beta (!)" + + def on_stage_setup(self): + self.probe_mgr = ProbeManager() + self.probe_mgr.resolution = self.get_setting("probe_resolution") + self.probe_mgr.diffuse_resolution = self.get_setting("diffuse_probe_resolution") + self.probe_mgr.max_probes = self.get_setting("max_probes") + self.probe_mgr.init() + self._setup_stages() + + def _setup_stages(self): + """ Setups all stages """ + # Create the stage to generate and update the cubemaps + self.capture_stage = self.create_stage(EnvironmentCaptureStage) + self.capture_stage.resolution = self.probe_mgr.resolution + self.capture_stage.diffuse_resolution = self.probe_mgr.diffuse_resolution + self.capture_stage.storage_tex = self.probe_mgr.cubemap_storage + self.capture_stage.storage_tex_diffuse = self.probe_mgr.diffuse_storage + + # Create the stage to cull the cubemaps + self.cull_stage = self.create_stage(CullProbesStage) + + # Create the stage to apply the cubemaps + self.apply_stage = self.create_stage(ApplyEnvprobesStage) + + if self.is_plugin_enabled("scattering"): + self.capture_stage.required_pipes += ["ScatteringIBLSpecular", "ScatteringIBLDiffuse"] + + if self.is_plugin_enabled("pssm"): + self.capture_stage.required_pipes += ["PSSMSceneSunShadowMapPCF"] + self.capture_stage.required_inputs += ["PSSMSceneSunShadowMVP"] + + self._setup_inputs() + + def _setup_inputs(self): + """ Sets all required inputs """ + self.pta_probes = PTAInt.empty_array(1) + + # Construct the UBO which stores all environment probe data + self.data_ubo = SimpleInputBlock("EnvProbes") + self.data_ubo.add_input("num_probes", self.pta_probes) + self.data_ubo.add_input("cubemaps", self.probe_mgr.cubemap_storage) + self.data_ubo.add_input("diffuse_cubemaps", self.probe_mgr.diffuse_storage) + self.data_ubo.add_input("dataset", self.probe_mgr.dataset_storage) + self._pipeline.stage_mgr.input_blocks.append(self.data_ubo) + + # Use the UBO in light culling + CullLightsStage.required_inputs.append("EnvProbes") + + def on_pre_render_update(self): + if self._pipeline.task_scheduler.is_scheduled("envprobes_select_and_cull"): + self.probe_mgr.update() + self.pta_probes[0] = self.probe_mgr.num_probes + probe = self.probe_mgr.find_probe_to_update() + if probe: + probe.last_update = Globals.clock.get_frame_count() + self.capture_stage.active = True + self.capture_stage.set_probe(probe) + + if self.is_plugin_enabled("pssm"): + self.get_plugin_instance("pssm").scene_shadow_stage.request_focus( + probe.bounds.get_center(), probe.bounds.get_radius() + ) + else: + self.capture_stage.active = False diff --git a/RenderPipelineFile/rpplugins/env_probes/probe_manager.py b/RenderPipelineFile/rpplugins/env_probes/probe_manager.py index d48583ba..a35fa076 100644 --- a/RenderPipelineFile/rpplugins/env_probes/probe_manager.py +++ b/RenderPipelineFile/rpplugins/env_probes/probe_manager.py @@ -1,103 +1,103 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import Vec4, SamplerState, BoundingVolume - -from rpcore.globals import Globals -from rpcore.rpobject import RPObject -from rpcore.image import Image - - -class ProbeManager(RPObject): - """ Manages all environment probes """ - - def __init__(self): - """ Initializes a new probe manager """ - RPObject.__init__(self) - self.probes = [] - self.max_probes = 3 - self.resolution = 128 - self.diffuse_resolution = 4 - - def init(self): - """ Creates the cubemap storage """ - - # Storage for the specular components (with mipmaps) - self.cubemap_storage = Image.create_cube_array( - "EnvmapStorage", self.resolution, self.max_probes, "RGBA16") - self.cubemap_storage.set_minfilter(SamplerState.FT_linear_mipmap_linear) - self.cubemap_storage.set_magfilter(SamplerState.FT_linear) - self.cubemap_storage.set_clear_color(Vec4(1.0, 0.0, 0.1, 1.0)) - self.cubemap_storage.clear_image() - - # Storage for the diffuse component - self.diffuse_storage = Image.create_cube_array( - "EnvmapDiffStorage", self.diffuse_resolution, self.max_probes, "RGBA16") - self.diffuse_storage.set_clear_color(Vec4(1, 0, 0.2, 1.0)) - self.diffuse_storage.clear_image() - - # Data-storage to store all cubemap properties - self.dataset_storage = Image.create_buffer( - "EnvmapData", self.max_probes * 5, "RGBA32") - self.dataset_storage.set_clear_color(Vec4(0)) - self.dataset_storage.clear_image() - - def add_probe(self, probe): - """ Adds a new probe """ - if len(self.probes) >= self.max_probes: - self.error("Cannot attach probe, out of slots!") - return False - probe.last_update = -1 - probe.index = len(self.probes) - self.probes.append(probe) - return True - - def update(self): - """ Updates the manager, updating all probes """ - ptr = self.dataset_storage.modify_ram_image() - for probe in self.probes: - if probe.modified: - probe.write_to_buffer(ptr) - - @property - def num_probes(self): - return len(self.probes) - - def find_probe_to_update(self): - """ Finds the next probe which requires an update, or returns None """ - if not self.probes: - return None - - view_frustum = Globals.base.camLens.make_bounds() - view_frustum.xform(Globals.base.cam.get_transform(Globals.base.render).get_mat()) - - def rating(probe): - return probe.last_update - for candidate in sorted(self.probes, key=rating): - if view_frustum.contains(candidate.bounds) == BoundingVolume.IF_no_intersection: - continue - return candidate - return None +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import Vec4, SamplerState, BoundingVolume + +from rpcore.globals import Globals +from rpcore.rpobject import RPObject +from rpcore.image import Image + + +class ProbeManager(RPObject): + """ Manages all environment probes """ + + def __init__(self): + """ Initializes a new probe manager """ + RPObject.__init__(self) + self.probes = [] + self.max_probes = 3 + self.resolution = 128 + self.diffuse_resolution = 4 + + def init(self): + """ Creates the cubemap storage """ + + # Storage for the specular components (with mipmaps) + self.cubemap_storage = Image.create_cube_array( + "EnvmapStorage", self.resolution, self.max_probes, "RGBA16") + self.cubemap_storage.set_minfilter(SamplerState.FT_linear_mipmap_linear) + self.cubemap_storage.set_magfilter(SamplerState.FT_linear) + self.cubemap_storage.set_clear_color(Vec4(1.0, 0.0, 0.1, 1.0)) + self.cubemap_storage.clear_image() + + # Storage for the diffuse component + self.diffuse_storage = Image.create_cube_array( + "EnvmapDiffStorage", self.diffuse_resolution, self.max_probes, "RGBA16") + self.diffuse_storage.set_clear_color(Vec4(1, 0, 0.2, 1.0)) + self.diffuse_storage.clear_image() + + # Data-storage to store all cubemap properties + self.dataset_storage = Image.create_buffer( + "EnvmapData", self.max_probes * 5, "RGBA32") + self.dataset_storage.set_clear_color(Vec4(0)) + self.dataset_storage.clear_image() + + def add_probe(self, probe): + """ Adds a new probe """ + if len(self.probes) >= self.max_probes: + self.error("Cannot attach probe, out of slots!") + return False + probe.last_update = -1 + probe.index = len(self.probes) + self.probes.append(probe) + return True + + def update(self): + """ Updates the manager, updating all probes """ + ptr = self.dataset_storage.modify_ram_image() + for probe in self.probes: + if probe.modified: + probe.write_to_buffer(ptr) + + @property + def num_probes(self): + return len(self.probes) + + def find_probe_to_update(self): + """ Finds the next probe which requires an update, or returns None """ + if not self.probes: + return None + + view_frustum = Globals.base.camLens.make_bounds() + view_frustum.xform(Globals.base.cam.get_transform(Globals.base.render).get_mat()) + + def rating(probe): + return probe.last_update + for candidate in sorted(self.probes, key=rating): + if view_frustum.contains(candidate.bounds) == BoundingVolume.IF_no_intersection: + continue + return candidate + return None diff --git a/RenderPipelineFile/rpplugins/env_probes/shader/apply_envprobes.frag.glsl b/RenderPipelineFile/rpplugins/env_probes/shader/apply_envprobes.frag.glsl index 43b0c2df..5291e443 100644 --- a/RenderPipelineFile/rpplugins/env_probes/shader/apply_envprobes.frag.glsl +++ b/RenderPipelineFile/rpplugins/env_probes/shader/apply_envprobes.frag.glsl @@ -1,43 +1,43 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" - -#define APPLY_ENVPROBES_PASS 1 -#pragma include "includes/envprobes.inc.glsl" - -layout(location = 0) out vec4 result_spec; -layout(location = 1) out vec4 result_diff; - -void main() { - vec2 texcoord = get_texcoord(); - Material m = unpack_material(GBuffer, texcoord); - apply_all_probes(m, result_spec, result_diff); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" + +#define APPLY_ENVPROBES_PASS 1 +#pragma include "includes/envprobes.inc.glsl" + +layout(location = 0) out vec4 result_spec; +layout(location = 1) out vec4 result_diff; + +void main() { + vec2 texcoord = get_texcoord(); + Material m = unpack_material(GBuffer, texcoord); + apply_all_probes(m, result_spec, result_diff); +} diff --git a/RenderPipelineFile/rpplugins/env_probes/shader/cull_probes.frag.glsl b/RenderPipelineFile/rpplugins/env_probes/shader/cull_probes.frag.glsl index a9e9c261..417c30a7 100644 --- a/RenderPipelineFile/rpplugins/env_probes/shader/cull_probes.frag.glsl +++ b/RenderPipelineFile/rpplugins/env_probes/shader/cull_probes.frag.glsl @@ -1,103 +1,103 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/envprobes.inc.glsl" -#pragma include "includes/transforms.inc.glsl" -#pragma include "includes/light_culling.inc.glsl" - -out vec4 result; - -uniform isamplerBuffer CellListBuffer; -uniform writeonly iimageBuffer RESTRICT PerCellProbes; - -void main() { - ivec2 coord = ivec2(gl_FragCoord.xy); - - // Find the index of the cell we are about to process - int idx = 1 + coord.x + coord.y * LC_CULLING_SLICE_WIDTH; - int num_total_cells = texelFetch(CellListBuffer, 0).x; - ivec2 precompute_size = MainSceneData.lc_tile_count; - - // If we found no remaining cell, we are done, so just return and hope for - // good coherency. - if (idx > num_total_cells) { - discard; - } - - // Fetch the cell data, this contains the cells position - int packed_cell_data = texelFetch(CellListBuffer, idx).x; - int cell_x, cell_y, cell_slice; - unpack_cell_data(packed_cell_data, cell_x, cell_y, cell_slice); - - float distance_bias = 0.01; - float bsphere_bias = 0.05; - // Find the tiles minimum and maximum distance - float min_distance = get_distance_from_slice(cell_slice) - distance_bias; - float max_distance = get_distance_from_slice(cell_slice + 1) + distance_bias; - - // Compute sample directions - vec3 local_ray_dirs[num_raydirs] = get_raydirs(cell_x, cell_y, precompute_size); - - int storage_offset = idx * MAX_PROBES_PER_CELL; - int probes_written = 0; - - // Iterate over all probes - for(int i = 0; i < EnvProbes.num_probes && probes_written < MAX_PROBES_PER_CELL; ++i) { - Cubemap map = get_cubemap(i); - vec4 pos_view = MainSceneData.view_mat_z_up * vec4(map.bounding_sphere_center, 1); - - bool visible = false; - - Sphere sphere; - sphere.pos = pos_view.xyz; - sphere.radius = map.bounding_sphere_radius + bsphere_bias; - - // Check for visibility - for (int k = 0; k < num_raydirs; ++k) { - visible = visible || viewspace_ray_sphere_distance_intersection( - sphere, local_ray_dirs[k], min_distance, max_distance); - } - - if (visible) { - imageStore(PerCellProbes, storage_offset + probes_written, ivec4(1 + i)); - ++probes_written; - } - } - - // Append zero byte - // if (probes_written < MAX_PROBES_PER_CELL) { - // imageStore(PerCellProbes, storage_offset + probes_written, ivec4(0)); - // } - - for (int i = probes_written; i < MAX_PROBES_PER_CELL; ++i) { - imageStore(PerCellProbes, storage_offset + i, ivec4(0)); - } - - result = vec4(cell_x % 2, 0.6, 1.0, 1.0); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/envprobes.inc.glsl" +#pragma include "includes/transforms.inc.glsl" +#pragma include "includes/light_culling.inc.glsl" + +out vec4 result; + +uniform isamplerBuffer CellListBuffer; +uniform writeonly iimageBuffer RESTRICT PerCellProbes; + +void main() { + ivec2 coord = ivec2(gl_FragCoord.xy); + + // Find the index of the cell we are about to process + int idx = 1 + coord.x + coord.y * LC_CULLING_SLICE_WIDTH; + int num_total_cells = texelFetch(CellListBuffer, 0).x; + ivec2 precompute_size = MainSceneData.lc_tile_count; + + // If we found no remaining cell, we are done, so just return and hope for + // good coherency. + if (idx > num_total_cells) { + discard; + } + + // Fetch the cell data, this contains the cells position + int packed_cell_data = texelFetch(CellListBuffer, idx).x; + int cell_x, cell_y, cell_slice; + unpack_cell_data(packed_cell_data, cell_x, cell_y, cell_slice); + + float distance_bias = 0.01; + float bsphere_bias = 0.05; + // Find the tiles minimum and maximum distance + float min_distance = get_distance_from_slice(cell_slice) - distance_bias; + float max_distance = get_distance_from_slice(cell_slice + 1) + distance_bias; + + // Compute sample directions + vec3 local_ray_dirs[num_raydirs] = get_raydirs(cell_x, cell_y, precompute_size); + + int storage_offset = idx * MAX_PROBES_PER_CELL; + int probes_written = 0; + + // Iterate over all probes + for(int i = 0; i < EnvProbes.num_probes && probes_written < MAX_PROBES_PER_CELL; ++i) { + Cubemap map = get_cubemap(i); + vec4 pos_view = MainSceneData.view_mat_z_up * vec4(map.bounding_sphere_center, 1); + + bool visible = false; + + Sphere sphere; + sphere.pos = pos_view.xyz; + sphere.radius = map.bounding_sphere_radius + bsphere_bias; + + // Check for visibility + for (int k = 0; k < num_raydirs; ++k) { + visible = visible || viewspace_ray_sphere_distance_intersection( + sphere, local_ray_dirs[k], min_distance, max_distance); + } + + if (visible) { + imageStore(PerCellProbes, storage_offset + probes_written, ivec4(1 + i)); + ++probes_written; + } + } + + // Append zero byte + // if (probes_written < MAX_PROBES_PER_CELL) { + // imageStore(PerCellProbes, storage_offset + probes_written, ivec4(0)); + // } + + for (int i = probes_written; i < MAX_PROBES_PER_CELL; ++i) { + imageStore(PerCellProbes, storage_offset + i, ivec4(0)); + } + + result = vec4(cell_x % 2, 0.6, 1.0, 1.0); +} diff --git a/RenderPipelineFile/rpplugins/env_probes/shader/filter_cubemap.frag.glsl b/RenderPipelineFile/rpplugins/env_probes/shader/filter_cubemap.frag.glsl index f12ee5d4..d05e76f1 100644 --- a/RenderPipelineFile/rpplugins/env_probes/shader/filter_cubemap.frag.glsl +++ b/RenderPipelineFile/rpplugins/env_probes/shader/filter_cubemap.frag.glsl @@ -1,75 +1,75 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/importance_sampling.inc.glsl" -#pragma include "includes/poisson_disk.inc.glsl" -#pragma include "includes/brdf.inc.glsl" -#pragma include "includes/noise.inc.glsl" - -// #pragma optionNV (unroll all) - -uniform samplerCubeArray SourceTex; -uniform writeonly imageCubeArray RESTRICT DestTex; -uniform int currentMip; -uniform int currentIndex; -out vec4 result; - -void main() { - const uint num_samples = SHADER_NUM_SAMPLES; - - // Get cubemap coordinate - int texsize = textureSize(SourceTex, currentMip).x; - ivec2 coord = ivec2(gl_FragCoord.xy); - ivec2 clamped_coord; int face; - vec3 n = texcoord_to_cubemap(texsize, coord, clamped_coord, face); - - // Determine target roughenss - float sample_roughness = 1e-10 + (currentMip + 1) / 12.0 - 0.04; - - // Get tangent and binormal - vec3 tangent, binormal; - find_arbitrary_tangent(n, tangent, binormal); - - vec4 accum = vec4(0.0); - for (uint i = 0; i < num_samples; ++i) { - vec2 Xi = hammersley(i, num_samples); - vec3 h = importance_sample_ggx(Xi, sample_roughness); - h = normalize(h.x * tangent + h.y * binormal + h.z * n); - accum += textureLod(SourceTex, vec4(h, currentIndex), currentMip - 1); - } - - accum /= num_samples; - - // It seems we are having some precision issues here. To make sure that - // no sky-cubemap leaks in, increase the weight by a small amount. - accum *= 1 + 1e-7; - accum.w = saturate(accum.w); - - imageStore(DestTex, ivec3(clamped_coord, currentIndex * 6 + face), accum); - result = accum; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/importance_sampling.inc.glsl" +#pragma include "includes/poisson_disk.inc.glsl" +#pragma include "includes/brdf.inc.glsl" +#pragma include "includes/noise.inc.glsl" + +// #pragma optionNV (unroll all) + +uniform samplerCubeArray SourceTex; +uniform writeonly imageCubeArray RESTRICT DestTex; +uniform int currentMip; +uniform int currentIndex; +out vec4 result; + +void main() { + const uint num_samples = SHADER_NUM_SAMPLES; + + // Get cubemap coordinate + int texsize = textureSize(SourceTex, currentMip).x; + ivec2 coord = ivec2(gl_FragCoord.xy); + ivec2 clamped_coord; int face; + vec3 n = texcoord_to_cubemap(texsize, coord, clamped_coord, face); + + // Determine target roughenss + float sample_roughness = 1e-10 + (currentMip + 1) / 12.0 - 0.04; + + // Get tangent and binormal + vec3 tangent, binormal; + find_arbitrary_tangent(n, tangent, binormal); + + vec4 accum = vec4(0.0); + for (uint i = 0; i < num_samples; ++i) { + vec2 Xi = hammersley(i, num_samples); + vec3 h = importance_sample_ggx(Xi, sample_roughness); + h = normalize(h.x * tangent + h.y * binormal + h.z * n); + accum += textureLod(SourceTex, vec4(h, currentIndex), currentMip - 1); + } + + accum /= num_samples; + + // It seems we are having some precision issues here. To make sure that + // no sky-cubemap leaks in, increase the weight by a small amount. + accum *= 1 + 1e-7; + accum.w = saturate(accum.w); + + imageStore(DestTex, ivec3(clamped_coord, currentIndex * 6 + face), accum); + result = accum; +} diff --git a/RenderPipelineFile/rpplugins/env_probes/shader/filter_cubemap_diffuse.frag.glsl b/RenderPipelineFile/rpplugins/env_probes/shader/filter_cubemap_diffuse.frag.glsl index b9667516..548e9f0e 100644 --- a/RenderPipelineFile/rpplugins/env_probes/shader/filter_cubemap_diffuse.frag.glsl +++ b/RenderPipelineFile/rpplugins/env_probes/shader/filter_cubemap_diffuse.frag.glsl @@ -1,61 +1,61 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/importance_sampling.inc.glsl" - -// #pragma optionNV (unroll all) - -uniform samplerCube SourceTex; -uniform writeonly imageCubeArray RESTRICT DestTex; -uniform int currentIndex; -out vec4 result; - -void main() { - // Get cubemap coordinate - const int texsize = GET_SETTING(env_probes, diffuse_probe_resolution); - ivec2 coord = ivec2(gl_FragCoord.xy); - ivec2 clamped_coord; int face; - vec3 n = texcoord_to_cubemap(texsize, coord, clamped_coord, face); - - // Get tangent and binormal - vec3 tangent, binormal; - find_arbitrary_tangent(n, tangent, binormal); - - const int num_samples = 256; - vec4 accum = vec4(0.0); - for (uint i = 0; i < num_samples; ++i) { - vec2 Xi = hammersley(i, num_samples); - vec3 h = importance_sample_lambert(Xi); - h = normalize(h.x * tangent + h.y * binormal + h.z * n); - accum += textureLod(SourceTex, h, 0); - } - accum /= num_samples; - imageStore(DestTex, ivec3(clamped_coord, currentIndex * 6 + face), accum); - result = accum; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/importance_sampling.inc.glsl" + +// #pragma optionNV (unroll all) + +uniform samplerCube SourceTex; +uniform writeonly imageCubeArray RESTRICT DestTex; +uniform int currentIndex; +out vec4 result; + +void main() { + // Get cubemap coordinate + const int texsize = GET_SETTING(env_probes, diffuse_probe_resolution); + ivec2 coord = ivec2(gl_FragCoord.xy); + ivec2 clamped_coord; int face; + vec3 n = texcoord_to_cubemap(texsize, coord, clamped_coord, face); + + // Get tangent and binormal + vec3 tangent, binormal; + find_arbitrary_tangent(n, tangent, binormal); + + const int num_samples = 256; + vec4 accum = vec4(0.0); + for (uint i = 0; i < num_samples; ++i) { + vec2 Xi = hammersley(i, num_samples); + vec3 h = importance_sample_lambert(Xi); + h = normalize(h.x * tangent + h.y * binormal + h.z * n); + accum += textureLod(SourceTex, h, 0); + } + accum /= num_samples; + imageStore(DestTex, ivec3(clamped_coord, currentIndex * 6 + face), accum); + result = accum; +} diff --git a/RenderPipelineFile/rpplugins/env_probes/shader/generate_mip_shaders.py b/RenderPipelineFile/rpplugins/env_probes/shader/generate_mip_shaders.py index 5c7a6af2..9aaef008 100644 --- a/RenderPipelineFile/rpplugins/env_probes/shader/generate_mip_shaders.py +++ b/RenderPipelineFile/rpplugins/env_probes/shader/generate_mip_shaders.py @@ -1,17 +1,17 @@ - -import os -from os.path import dirname, realpath, isdir -os.chdir(dirname(realpath(__file__))) - -if not isdir("mips"): - os.makedirs("mips") - -for i in range(12): - num_samples = 256 + i * 12 - if i == 0: - num_samples = 128 - with open('mips/{}.autogen.glsl'.format(i), 'w') as handle: - handle.write('// Autogenerated, do not edit\n') - handle.write('#version 430\n') - handle.write('#define SHADER_NUM_SAMPLES {}\n'.format(num_samples)) - handle.write('#pragma include "../filter_cubemap.frag.glsl"\n') + +import os +from os.path import dirname, realpath, isdir +os.chdir(dirname(realpath(__file__))) + +if not isdir("mips"): + os.makedirs("mips") + +for i in range(12): + num_samples = 256 + i * 12 + if i == 0: + num_samples = 128 + with open('mips/{}.autogen.glsl'.format(i), 'w') as handle: + handle.write('// Autogenerated, do not edit\n') + handle.write('#version 430\n') + handle.write('#define SHADER_NUM_SAMPLES {}\n'.format(num_samples)) + handle.write('#pragma include "../filter_cubemap.frag.glsl"\n') diff --git a/RenderPipelineFile/rpplugins/env_probes/shader/store_cubemap.frag.glsl b/RenderPipelineFile/rpplugins/env_probes/shader/store_cubemap.frag.glsl index 761343fd..2901cb82 100644 --- a/RenderPipelineFile/rpplugins/env_probes/shader/store_cubemap.frag.glsl +++ b/RenderPipelineFile/rpplugins/env_probes/shader/store_cubemap.frag.glsl @@ -1,46 +1,46 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" - -uniform sampler2D SourceTex; -uniform writeonly imageCubeArray RESTRICT DestTex; -uniform int currentIndex; - -void main() { - ivec2 coord = ivec2(gl_FragCoord.xy); - int size = textureSize(SourceTex, 0).y; - vec4 source_data = texelFetch(SourceTex, coord, 0); - - // Convert to local cubemap coordinate - int offset = coord.x / size; - coord.x = coord.x % size; - - // Store color in cubemap array - imageStore(DestTex, ivec3(coord.x, coord.y, currentIndex * 6 + offset), source_data); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" + +uniform sampler2D SourceTex; +uniform writeonly imageCubeArray RESTRICT DestTex; +uniform int currentIndex; + +void main() { + ivec2 coord = ivec2(gl_FragCoord.xy); + int size = textureSize(SourceTex, 0).y; + vec4 source_data = texelFetch(SourceTex, coord, 0); + + // Convert to local cubemap coordinate + int offset = coord.x / size; + coord.x = coord.x % size; + + // Store color in cubemap array + imageStore(DestTex, ivec3(coord.x, coord.y, currentIndex * 6 + offset), source_data); +} diff --git a/RenderPipelineFile/rpplugins/env_probes/shader/store_cubemap_diffuse.frag.glsl b/RenderPipelineFile/rpplugins/env_probes/shader/store_cubemap_diffuse.frag.glsl index 006f0f9e..31f9cae4 100644 --- a/RenderPipelineFile/rpplugins/env_probes/shader/store_cubemap_diffuse.frag.glsl +++ b/RenderPipelineFile/rpplugins/env_probes/shader/store_cubemap_diffuse.frag.glsl @@ -1,45 +1,45 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" - -uniform sampler2D SourceTex; -uniform writeonly imageCube RESTRICT DestTex; - -void main() { - ivec2 coord = ivec2(gl_FragCoord.xy); - int size = textureSize(SourceTex, 0).y; - vec4 source_data = texelFetch(SourceTex, coord, 0); - - // Convert to local cubemap coordinate - int offset = coord.x / size; - coord.x = coord.x % size; - - // Store color in diffuse cubemap array - imageStore(DestTex, ivec3(coord.x, coord.y, offset), source_data); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" + +uniform sampler2D SourceTex; +uniform writeonly imageCube RESTRICT DestTex; + +void main() { + ivec2 coord = ivec2(gl_FragCoord.xy); + int size = textureSize(SourceTex, 0).y; + vec4 source_data = texelFetch(SourceTex, coord, 0); + + // Convert to local cubemap coordinate + int offset = coord.x / size; + coord.x = coord.x % size; + + // Store color in diffuse cubemap array + imageStore(DestTex, ivec3(coord.x, coord.y, offset), source_data); +} diff --git a/RenderPipelineFile/rpplugins/forward_shading/config.yaml b/RenderPipelineFile/rpplugins/forward_shading/config.yaml index 87e3f9e6..35c61158 100644 --- a/RenderPipelineFile/rpplugins/forward_shading/config.yaml +++ b/RenderPipelineFile/rpplugins/forward_shading/config.yaml @@ -1,3 +1,3 @@ - -settings: -daytime_settings: + +settings: +daytime_settings: diff --git a/RenderPipelineFile/rpplugins/forward_shading/forward_stage.py b/RenderPipelineFile/rpplugins/forward_shading/forward_stage.py index c79477e0..75ee95c5 100644 --- a/RenderPipelineFile/rpplugins/forward_shading/forward_stage.py +++ b/RenderPipelineFile/rpplugins/forward_shading/forward_stage.py @@ -1,74 +1,74 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import Camera - -from rpcore.globals import Globals -from rpcore.render_stage import RenderStage - - -class ForwardStage(RenderStage): - - """ Forward shading stage, which first renders all forward objects, - and then merges them with the scene """ - - required_inputs = ["DefaultEnvmap", "PrefilteredBRDF", "PrefilteredCoatBRDF"] - required_pipes = ["SceneDepth", "ShadedScene", "CellIndices"] - - @property - def produced_pipes(self): - return {"ShadedScene": self.target_merge.color_tex} - - def create(self): - self.forward_cam = Camera("ForwardShadingCam") - self.forward_cam.set_lens(Globals.base.camLens) - self.forward_cam_np = Globals.base.camera.attach_new_node(self.forward_cam) - - self.target = self.create_target("ForwardShading") - self.target.add_color_attachment(bits=16, alpha=True) - self.target.add_depth_attachment(bits=32) - self.target.prepare_render(self.forward_cam_np) - self.target.set_clear_color(0, 0, 0, 0) - - self._pipeline.tag_mgr.register_camera("forward", self.forward_cam) - - self.target_merge = self.create_target("MergeWithDeferred") - self.target_merge.add_color_attachment(bits=16) - self.target_merge.prepare_buffer() - self.target_merge.set_shader_inputs( - ForwardDepth=self.target.depth_tex, - ForwardColor=self.target.color_tex) - - def set_shader_input(self, *args): - Globals.base.render.set_shader_input(*args) - RenderStage.set_shader_input(self, *args) - - def set_shader_inputs(self, **kwargs): - Globals.base.render.set_shader_inputs(**kwargs) - RenderStage.set_shader_inputs(self, **kwargs) - - def reload_shaders(self): - self.target_merge.shader = self.load_plugin_shader("merge_with_deferred.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import Camera + +from rpcore.globals import Globals +from rpcore.render_stage import RenderStage + + +class ForwardStage(RenderStage): + + """ Forward shading stage, which first renders all forward objects, + and then merges them with the scene """ + + required_inputs = ["DefaultEnvmap", "PrefilteredBRDF", "PrefilteredCoatBRDF"] + required_pipes = ["SceneDepth", "ShadedScene", "CellIndices"] + + @property + def produced_pipes(self): + return {"ShadedScene": self.target_merge.color_tex} + + def create(self): + self.forward_cam = Camera("ForwardShadingCam") + self.forward_cam.set_lens(Globals.base.camLens) + self.forward_cam_np = Globals.base.camera.attach_new_node(self.forward_cam) + + self.target = self.create_target("ForwardShading") + self.target.add_color_attachment(bits=16, alpha=True) + self.target.add_depth_attachment(bits=32) + self.target.prepare_render(self.forward_cam_np) + self.target.set_clear_color(0, 0, 0, 0) + + self._pipeline.tag_mgr.register_camera("forward", self.forward_cam) + + self.target_merge = self.create_target("MergeWithDeferred") + self.target_merge.add_color_attachment(bits=16) + self.target_merge.prepare_buffer() + self.target_merge.set_shader_inputs( + ForwardDepth=self.target.depth_tex, + ForwardColor=self.target.color_tex) + + def set_shader_input(self, *args): + Globals.base.render.set_shader_input(*args) + RenderStage.set_shader_input(self, *args) + + def set_shader_inputs(self, **kwargs): + Globals.base.render.set_shader_inputs(**kwargs) + RenderStage.set_shader_inputs(self, **kwargs) + + def reload_shaders(self): + self.target_merge.shader = self.load_plugin_shader("merge_with_deferred.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/forward_shading/plugin.py b/RenderPipelineFile/rpplugins/forward_shading/plugin.py index ef50452f..f8d1163d 100644 --- a/RenderPipelineFile/rpplugins/forward_shading/plugin.py +++ b/RenderPipelineFile/rpplugins/forward_shading/plugin.py @@ -1,55 +1,55 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.pluginbase.base_plugin import BasePlugin - -from .forward_stage import ForwardStage - - -class Plugin(BasePlugin): - - name = "Forward Rendering" - author = "tobspr " - description = ("This plugin adds support for an additional forward rendering " - "pass. This is mainly useful for transparency.") - version = "0.1 alpha (!)" - - def on_stage_setup(self): - self.stage = self.create_stage(ForwardStage) - - if self.is_plugin_enabled("scattering"): - self.stage.required_pipes += ["ScatteringIBLSpecular", "ScatteringIBLDiffuse"] - - if self.is_plugin_enabled("pssm"): - self.stage.required_pipes += ["PSSMSceneSunShadowMapPCF"] - self.stage.required_inputs += ["PSSMSceneSunShadowMVP"] - - if self.is_plugin_enabled("env_probes"): - self.stage.required_pipes += ["PerCellProbes"] - self.stage.required_inputs += ["EnvProbes"] - - def on_pipeline_created(self): - pass +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.pluginbase.base_plugin import BasePlugin + +from .forward_stage import ForwardStage + + +class Plugin(BasePlugin): + + name = "Forward Rendering" + author = "tobspr " + description = ("This plugin adds support for an additional forward rendering " + "pass. This is mainly useful for transparency.") + version = "0.1 alpha (!)" + + def on_stage_setup(self): + self.stage = self.create_stage(ForwardStage) + + if self.is_plugin_enabled("scattering"): + self.stage.required_pipes += ["ScatteringIBLSpecular", "ScatteringIBLDiffuse"] + + if self.is_plugin_enabled("pssm"): + self.stage.required_pipes += ["PSSMSceneSunShadowMapPCF"] + self.stage.required_inputs += ["PSSMSceneSunShadowMVP"] + + if self.is_plugin_enabled("env_probes"): + self.stage.required_pipes += ["PerCellProbes"] + self.stage.required_inputs += ["EnvProbes"] + + def on_pipeline_created(self): + pass diff --git a/RenderPipelineFile/rpplugins/forward_shading/shader/merge_with_deferred.frag.glsl b/RenderPipelineFile/rpplugins/forward_shading/shader/merge_with_deferred.frag.glsl index e7475369..c4f0efc3 100644 --- a/RenderPipelineFile/rpplugins/forward_shading/shader/merge_with_deferred.frag.glsl +++ b/RenderPipelineFile/rpplugins/forward_shading/shader/merge_with_deferred.frag.glsl @@ -1,49 +1,49 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" - -uniform sampler2D ShadedScene; -uniform sampler2D SceneDepth; -uniform sampler2D ForwardDepth; -uniform sampler2D ForwardColor; - -out vec3 result; - -void main() { - vec2 texcoord = get_texcoord(); - - vec3 deferred_result = textureLod(ShadedScene, texcoord, 0).xyz; - vec4 forward_result = textureLod(ForwardColor, texcoord, 0); - - float deferred_depth = textureLod(ForwardDepth, texcoord, 0).x; - float forward_depth = textureLod(SceneDepth, texcoord, 0).x; - forward_result.xyz = forward_result.xyz * forward_result.w + - deferred_result * (1 - forward_result.w); - result = deferred_depth > forward_depth ? deferred_result : forward_result.xyz; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" + +uniform sampler2D ShadedScene; +uniform sampler2D SceneDepth; +uniform sampler2D ForwardDepth; +uniform sampler2D ForwardColor; + +out vec3 result; + +void main() { + vec2 texcoord = get_texcoord(); + + vec3 deferred_result = textureLod(ShadedScene, texcoord, 0).xyz; + vec4 forward_result = textureLod(ForwardColor, texcoord, 0); + + float deferred_depth = textureLod(ForwardDepth, texcoord, 0).x; + float forward_depth = textureLod(SceneDepth, texcoord, 0).x; + forward_result.xyz = forward_result.xyz * forward_result.w + + deferred_result * (1 - forward_result.w); + result = deferred_depth > forward_depth ? deferred_result : forward_result.xyz; +} diff --git a/RenderPipelineFile/rpplugins/fxaa/config.yaml b/RenderPipelineFile/rpplugins/fxaa/config.yaml index 10f030b5..678c8191 100644 --- a/RenderPipelineFile/rpplugins/fxaa/config.yaml +++ b/RenderPipelineFile/rpplugins/fxaa/config.yaml @@ -1,41 +1,41 @@ - -settings: !!omap - - - quality: - type: enum - values: ["low", "medium", "high", "ultra"] - default: "ultra" - label: FXAA Quality - shader_runtime: true - description: > - This controls the quality of the FXAA algorithm, higher presets - produce better antialiasing but also consume more performance. - - - subpixel_quality: - type: float - range: [0, 1] - default: 0.5 - label: Subpixel quality - description: This controls the subpixel quality. - shader_runtime: true - - - edge_threshold: - type: float - range: [0.063, 0.333] - default: 0.166 - label: Edge threshold - shader_runtime: true - description: > - This controls the edge detection. Lower values lead to a better - detection, but also require more performace. - - - min_threshold: - type: float - range: [0.312, 0.833] - default: 0.833 - shader_runtime: true - label: Minimum threshold - description: > - Minimum edge threshold, improves detection in dark scenes. - -daytime_settings: + +settings: !!omap + + - quality: + type: enum + values: ["low", "medium", "high", "ultra"] + default: "ultra" + label: FXAA Quality + shader_runtime: true + description: > + This controls the quality of the FXAA algorithm, higher presets + produce better antialiasing but also consume more performance. + + - subpixel_quality: + type: float + range: [0, 1] + default: 0.5 + label: Subpixel quality + description: This controls the subpixel quality. + shader_runtime: true + + - edge_threshold: + type: float + range: [0.063, 0.333] + default: 0.166 + label: Edge threshold + shader_runtime: true + description: > + This controls the edge detection. Lower values lead to a better + detection, but also require more performace. + + - min_threshold: + type: float + range: [0.312, 0.833] + default: 0.833 + shader_runtime: true + label: Minimum threshold + description: > + Minimum edge threshold, improves detection in dark scenes. + +daytime_settings: diff --git a/RenderPipelineFile/rpplugins/fxaa/fxaa_stage.py b/RenderPipelineFile/rpplugins/fxaa/fxaa_stage.py index 48aed42d..86571e30 100644 --- a/RenderPipelineFile/rpplugins/fxaa/fxaa_stage.py +++ b/RenderPipelineFile/rpplugins/fxaa/fxaa_stage.py @@ -1,53 +1,53 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.render_stage import RenderStage - - -class FXAAStage(RenderStage): - - """ This is the main stage used by the FXAA plugin """ - - required_pipes = ["ShadedScene", "GBuffer"] - required_inputs = [] - - @property - def produced_pipes(self): - return {"ShadedScene": self.target.color_tex} - - def create(self): - self.luma_target = self.create_target("FXAAWriteLuma") - self.luma_target.add_color_attachment(bits=16, alpha=True) - self.luma_target.prepare_buffer() - - self.target = self.create_target("FXAA") - self.target.add_color_attachment(bits=16) - self.target.prepare_buffer() - self.target.set_shader_input("SourceTex", self.luma_target.color_tex) - - def reload_shaders(self): - self.target.shader = self.load_plugin_shader("fxaa_stage.frag.glsl") - self.luma_target.shader = self.load_plugin_shader("write_luma.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.render_stage import RenderStage + + +class FXAAStage(RenderStage): + + """ This is the main stage used by the FXAA plugin """ + + required_pipes = ["ShadedScene", "GBuffer"] + required_inputs = [] + + @property + def produced_pipes(self): + return {"ShadedScene": self.target.color_tex} + + def create(self): + self.luma_target = self.create_target("FXAAWriteLuma") + self.luma_target.add_color_attachment(bits=16, alpha=True) + self.luma_target.prepare_buffer() + + self.target = self.create_target("FXAA") + self.target.add_color_attachment(bits=16) + self.target.prepare_buffer() + self.target.set_shader_input("SourceTex", self.luma_target.color_tex) + + def reload_shaders(self): + self.target.shader = self.load_plugin_shader("fxaa_stage.frag.glsl") + self.luma_target.shader = self.load_plugin_shader("write_luma.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/fxaa/plugin.py b/RenderPipelineFile/rpplugins/fxaa/plugin.py index f56abc3e..893d300b 100644 --- a/RenderPipelineFile/rpplugins/fxaa/plugin.py +++ b/RenderPipelineFile/rpplugins/fxaa/plugin.py @@ -1,42 +1,42 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.pluginbase.base_plugin import BasePlugin - -from .fxaa_stage import FXAAStage - - -class Plugin(BasePlugin): - - name = "FXAA (Antialiasing)" - author = "tobspr " - description = ("This plugin adds support for the FXAA Antialiasing technique. " - "However, Please use SMAA! This plugin is just here for completeness, " - "but FXAA is really inferior to SMAA.") - version = "1.0" - - def on_stage_setup(self): - self._stage = self.create_stage(FXAAStage) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.pluginbase.base_plugin import BasePlugin + +from .fxaa_stage import FXAAStage + + +class Plugin(BasePlugin): + + name = "FXAA (Antialiasing)" + author = "tobspr " + description = ("This plugin adds support for the FXAA Antialiasing technique. " + "However, Please use SMAA! This plugin is just here for completeness, " + "but FXAA is really inferior to SMAA.") + version = "1.0" + + def on_stage_setup(self): + self._stage = self.create_stage(FXAAStage) diff --git a/RenderPipelineFile/rpplugins/fxaa/shader/FXAA.inc.glsl b/RenderPipelineFile/rpplugins/fxaa/shader/FXAA.inc.glsl index d48e829d..aeb403b9 100644 --- a/RenderPipelineFile/rpplugins/fxaa/shader/FXAA.inc.glsl +++ b/RenderPipelineFile/rpplugins/fxaa/shader/FXAA.inc.glsl @@ -1,1248 +1,1248 @@ -#pragma once - -/*============================================================================ - - - NVIDIA FXAA 3.11 by TIMOTHY LOTTES - - ------------------------------------------------------------------------------- -COPYRIGHT (C) 2010, 2011 NVIDIA CORPORATION. ALL RIGHTS RESERVED. ------------------------------------------------------------------------------- -TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THIS SOFTWARE IS PROVIDED -*AS IS* AND NVIDIA AND ITS SUPPLIERS DISCLAIM ALL WARRANTIES, EITHER EXPRESS -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT SHALL NVIDIA -OR ITS SUPPLIERS BE LIABLE FOR ANY SPECIAL, INCIDENTAL, INDIRECT, OR -CONSEQUENTIAL DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR -LOSS OF BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, -OR ANY OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE -THIS SOFTWARE, EVEN IF NVIDIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. - ------------------------------------------------------------------------------- - INTEGRATION CHECKLIST ------------------------------------------------------------------------------- -(1.) -In the shader source, setup defines for the desired configuration. -When providing multiple shaders (for different presets), -simply setup the defines differently in multiple files. -Example, - - #define FXAA_PC 1 - #define FXAA_HLSL_5 1 - #define FXAA_QUALITY__PRESET 12 - -Or, - - #define FXAA_360 1 - -Or, - - #define FXAA_PS3 1 - -Etc. - -(2.) -Then include this file, - - #include "Fxaa3_11.h" - -(3.) -Then call the FXAA pixel shader from within your desired shader. -Look at the FXAA Quality FxaaPixelShader() for docs on inputs. -As for FXAA 3.11 all inputs for all shaders are the same -to enable easy porting between platforms. - - return FxaaPixelShader(...); - -(4.) -Insure pass prior to FXAA outputs RGBL (see next section). -Or use, - - #define FXAA_GREEN_AS_LUMA 1 - -(5.) -Setup engine to provide the following constants -which are used in the FxaaPixelShader() inputs, - - FxaaFloat2 fxaaQualityRcpFrame, - FxaaFloat4 fxaaConsoleRcpFrameOpt, - FxaaFloat4 fxaaConsoleRcpFrameOpt2, - FxaaFloat4 fxaaConsole360RcpFrameOpt2, - FxaaFloat fxaaQualitySubpix, - FxaaFloat fxaaQualityEdgeThreshold, - FxaaFloat fxaaQualityEdgeThresholdMin, - FxaaFloat fxaaConsoleEdgeSharpness, - FxaaFloat fxaaConsoleEdgeThreshold, - FxaaFloat fxaaConsoleEdgeThresholdMin, - FxaaFloat4 fxaaConsole360ConstDir - -Look at the FXAA Quality FxaaPixelShader() for docs on inputs. - -(6.) -Have FXAA vertex shader run as a full screen triangle, -and output "pos" and "fxaaConsolePosPos" -such that inputs in the pixel shader provide, - - // {xy} = center of pixel - FxaaFloat2 pos, - - // {xy__} = upper left of pixel - // {__zw} = lower right of pixel - FxaaFloat4 fxaaConsolePosPos, - -(7.) -Insure the texture sampler(s) used by FXAA are set to bilinear filtering. - - ------------------------------------------------------------------------------- - INTEGRATION - RGBL AND COLORSPACE ------------------------------------------------------------------------------- -FXAA3 requires RGBL as input unless the following is set, - - #define FXAA_GREEN_AS_LUMA 1 - -In which case the engine uses green in place of luma, -and requires RGB input is in a non-linear colorspace. - -RGB should be LDR (low dynamic range). -Specifically do FXAA after tonemapping. - -RGB data as returned by a texture fetch can be non-linear, -or linear when FXAA_GREEN_AS_LUMA is not set. -Note an "sRGB format" texture counts as linear, -because the result of a texture fetch is linear data. -Regular "RGBA8" textures in the sRGB colorspace are non-linear. - -If FXAA_GREEN_AS_LUMA is not set, -luma must be stored in the alpha channel prior to running FXAA. -This luma should be in a perceptual space (could be gamma 2.0). -Example pass before FXAA where output is gamma 2.0 encoded, - - color.rgb = ToneMap(color.rgb); // linear color output - color.rgb = sqrt(color.rgb); // gamma 2.0 color output - return color; - -To use FXAA, - - color.rgb = ToneMap(color.rgb); // linear color output - color.rgb = sqrt(color.rgb); // gamma 2.0 color output - color.a = dot(color.rgb, FxaaFloat3(0.299, 0.587, 0.114)); // compute luma - return color; - -Another example where output is linear encoded, -say for instance writing to an sRGB formated render target, -where the render target does the conversion back to sRGB after blending, - - color.rgb = ToneMap(color.rgb); // linear color output - return color; - -To use FXAA, - - color.rgb = ToneMap(color.rgb); // linear color output - color.a = sqrt(dot(color.rgb, FxaaFloat3(0.299, 0.587, 0.114))); // compute luma - return color; - -Getting luma correct is required for the algorithm to work correctly. - - ------------------------------------------------------------------------------- - BEING LINEARLY CORRECT? ------------------------------------------------------------------------------- -Applying FXAA to a framebuffer with linear RGB color will look worse. -This is very counter intuitive, but happends to be true in this case. -The reason is because dithering artifacts will be more visiable -in a linear colorspace. - - ------------------------------------------------------------------------------- - COMPLEX INTEGRATION ------------------------------------------------------------------------------- -Q. What if the engine is blending into RGB before wanting to run FXAA? - -A. In the last opaque pass prior to FXAA, - have the pass write out luma into alpha. - Then blend into RGB only. - FXAA should be able to run ok - assuming the blending pass did not any add aliasing. - This should be the common case for particles and common blending passes. - -A. Or use FXAA_GREEN_AS_LUMA. - -============================================================================*/ - -/*============================================================================ - - INTEGRATION KNOBS - -============================================================================*/ -// -// FXAA_PS3 and FXAA_360 choose the console algorithm (FXAA3 CONSOLE). -// FXAA_360_OPT is a prototype for the new optimized 360 version. -// -// 1 = Use API. -// 0 = Don't use API. -// -/*--------------------------------------------------------------------------*/ -#ifndef FXAA_PS3 - #define FXAA_PS3 0 -#endif -/*--------------------------------------------------------------------------*/ -#ifndef FXAA_360 - #define FXAA_360 0 -#endif -/*--------------------------------------------------------------------------*/ -#ifndef FXAA_360_OPT - #define FXAA_360_OPT 0 -#endif -/*==========================================================================*/ -#ifndef FXAA_PC - // - // FXAA Quality - // The high quality PC algorithm. - // - #define FXAA_PC 0 -#endif -/*--------------------------------------------------------------------------*/ -#ifndef FXAA_PC_CONSOLE - // - // The console algorithm for PC is included - // for developers targeting really low spec machines. - // Likely better to just run FXAA_PC, and use a really low preset. - // - #define FXAA_PC_CONSOLE 0 -#endif -/*--------------------------------------------------------------------------*/ -#ifndef FXAA_GLSL_120 - #define FXAA_GLSL_120 0 -#endif -/*--------------------------------------------------------------------------*/ -#ifndef FXAA_GLSL_130 - #define FXAA_GLSL_130 0 -#endif -/*--------------------------------------------------------------------------*/ -#ifndef FXAA_HLSL_3 - #define FXAA_HLSL_3 0 -#endif -/*--------------------------------------------------------------------------*/ -#ifndef FXAA_HLSL_4 - #define FXAA_HLSL_4 0 -#endif -/*--------------------------------------------------------------------------*/ -#ifndef FXAA_HLSL_5 - #define FXAA_HLSL_5 0 -#endif -/*==========================================================================*/ -#ifndef FXAA_GREEN_AS_LUMA - // - // For those using non-linear color, - // and either not able to get luma in alpha, or not wanting to, - // this enables FXAA to run using green as a proxy for luma. - // So with this enabled, no need to pack luma in alpha. - // - // This will turn off AA on anything which lacks some amount of green. - // Pure red and blue or combination of only R and B, will get no AA. - // - // Might want to lower the settings for both, - // fxaaConsoleEdgeThresholdMin - // fxaaQualityEdgeThresholdMin - // In order to insure AA does not get turned off on colors - // which contain a minor amount of green. - // - // 1 = On. - // 0 = Off. - // - #define FXAA_GREEN_AS_LUMA 0 -#endif -/*--------------------------------------------------------------------------*/ -#ifndef FXAA_EARLY_EXIT - // - // Controls algorithm's early exit path. - // On PS3 turning this ON adds 2 cycles to the shader. - // On 360 turning this OFF adds 10ths of a millisecond to the shader. - // Turning this off on console will result in a more blurry image. - // So this defaults to on. - // - // 1 = On. - // 0 = Off. - // - #define FXAA_EARLY_EXIT 1 -#endif -/*--------------------------------------------------------------------------*/ -#ifndef FXAA_DISCARD - // - // Only valid for PC OpenGL currently. - // Probably will not work when FXAA_GREEN_AS_LUMA = 1. - // - // 1 = Use discard on pixels which don't need AA. - // For APIs which enable concurrent TEX+ROP from same surface. - // 0 = Return unchanged color on pixels which don't need AA. - // - #define FXAA_DISCARD 0 -#endif -/*--------------------------------------------------------------------------*/ -#ifndef FXAA_FAST_PIXEL_OFFSET - // - // Used for GLSL 120 only. - // - // 1 = GL API supports fast pixel offsets - // 0 = do not use fast pixel offsets - // - #ifdef GL_EXT_gpu_shader4 - #define FXAA_FAST_PIXEL_OFFSET 1 - #endif - #ifdef GL_NV_gpu_shader5 - #define FXAA_FAST_PIXEL_OFFSET 1 - #endif - #ifdef GL_ARB_gpu_shader5 - #define FXAA_FAST_PIXEL_OFFSET 1 - #endif - #ifndef FXAA_FAST_PIXEL_OFFSET - #define FXAA_FAST_PIXEL_OFFSET 0 - #endif -#endif -/*--------------------------------------------------------------------------*/ -#ifndef FXAA_GATHER4_ALPHA - // - // 1 = API supports gather4 on alpha channel. - // 0 = API does not support gather4 on alpha channel. - // - #if (FXAA_HLSL_5 == 1) - #define FXAA_GATHER4_ALPHA 1 - #endif - #ifdef GL_ARB_gpu_shader5 - #define FXAA_GATHER4_ALPHA 1 - #endif - #ifdef GL_NV_gpu_shader5 - #define FXAA_GATHER4_ALPHA 1 - #endif - #ifndef FXAA_GATHER4_ALPHA - #define FXAA_GATHER4_ALPHA 0 - #endif -#endif - -/*============================================================================ - FXAA CONSOLE PS3 - TUNING KNOBS -============================================================================*/ -#ifndef FXAA_CONSOLE__PS3_EDGE_SHARPNESS - // - // Consoles the sharpness of edges on PS3 only. - // Non-PS3 tuning is done with shader input. - // - // Due to the PS3 being ALU bound, - // there are only two safe values here: 4 and 8. - // These options use the shaders ability to a free *|/ by 2|4|8. - // - // 8.0 is sharper - // 4.0 is softer - // 2.0 is really soft (good for vector graphics inputs) - // - #if 1 - #define FXAA_CONSOLE__PS3_EDGE_SHARPNESS 8.0 - #endif - #if 0 - #define FXAA_CONSOLE__PS3_EDGE_SHARPNESS 4.0 - #endif - #if 0 - #define FXAA_CONSOLE__PS3_EDGE_SHARPNESS 2.0 - #endif -#endif -/*--------------------------------------------------------------------------*/ -#ifndef FXAA_CONSOLE__PS3_EDGE_THRESHOLD - // - // Only effects PS3. - // Non-PS3 tuning is done with shader input. - // - // The minimum amount of local contrast required to apply algorithm. - // The console setting has a different mapping than the quality setting. - // - // This only applies when FXAA_EARLY_EXIT is 1. - // - // Due to the PS3 being ALU bound, - // there are only two safe values here: 0.25 and 0.125. - // These options use the shaders ability to a free *|/ by 2|4|8. - // - // 0.125 leaves less aliasing, but is softer - // 0.25 leaves more aliasing, and is sharper - // - #if 1 - #define FXAA_CONSOLE__PS3_EDGE_THRESHOLD 0.125 - #else - #define FXAA_CONSOLE__PS3_EDGE_THRESHOLD 0.25 - #endif -#endif - -/*============================================================================ - FXAA QUALITY - TUNING KNOBS ------------------------------------------------------------------------------- -NOTE the other tuning knobs are now in the shader function inputs! -============================================================================*/ -#ifndef FXAA_QUALITY__PRESET - // - // Choose the quality preset. - // This needs to be compiled into the shader as it effects code. - // Best option to include multiple presets is to - // in each shader define the preset, then include this file. - // - // OPTIONS - // ----------------------------------------------------------------------- - // 10 to 15 - default medium dither (10=fastest, 15=highest quality) - // 20 to 29 - less dither, more expensive (20=fastest, 29=highest quality) - // 39 - no dither, very expensive - // - // NOTES - // ----------------------------------------------------------------------- - // 12 = slightly faster then FXAA 3.9 and higher edge quality (default) - // 13 = about same speed as FXAA 3.9 and better than 12 - // 23 = closest to FXAA 3.9 visually and performance wise - // _ = the lowest digit is directly related to performance - // _ = the highest digit is directly related to style - // - #define FXAA_QUALITY__PRESET 12 -#endif - - -/*============================================================================ - - FXAA QUALITY - PRESETS - -============================================================================*/ - -/*============================================================================ - FXAA QUALITY - MEDIUM DITHER PRESETS -============================================================================*/ -#if (FXAA_QUALITY__PRESET == 10) - #define FXAA_QUALITY__PS 3 - #define FXAA_QUALITY__P0 1.5 - #define FXAA_QUALITY__P1 3.0 - #define FXAA_QUALITY__P2 12.0 -#endif -/*--------------------------------------------------------------------------*/ -#if (FXAA_QUALITY__PRESET == 11) - #define FXAA_QUALITY__PS 4 - #define FXAA_QUALITY__P0 1.0 - #define FXAA_QUALITY__P1 1.5 - #define FXAA_QUALITY__P2 3.0 - #define FXAA_QUALITY__P3 12.0 -#endif -/*--------------------------------------------------------------------------*/ -#if (FXAA_QUALITY__PRESET == 12) - #define FXAA_QUALITY__PS 5 - #define FXAA_QUALITY__P0 1.0 - #define FXAA_QUALITY__P1 1.5 - #define FXAA_QUALITY__P2 2.0 - #define FXAA_QUALITY__P3 4.0 - #define FXAA_QUALITY__P4 12.0 -#endif -/*--------------------------------------------------------------------------*/ -#if (FXAA_QUALITY__PRESET == 13) - #define FXAA_QUALITY__PS 6 - #define FXAA_QUALITY__P0 1.0 - #define FXAA_QUALITY__P1 1.5 - #define FXAA_QUALITY__P2 2.0 - #define FXAA_QUALITY__P3 2.0 - #define FXAA_QUALITY__P4 4.0 - #define FXAA_QUALITY__P5 12.0 -#endif -/*--------------------------------------------------------------------------*/ -#if (FXAA_QUALITY__PRESET == 14) - #define FXAA_QUALITY__PS 7 - #define FXAA_QUALITY__P0 1.0 - #define FXAA_QUALITY__P1 1.5 - #define FXAA_QUALITY__P2 2.0 - #define FXAA_QUALITY__P3 2.0 - #define FXAA_QUALITY__P4 2.0 - #define FXAA_QUALITY__P5 4.0 - #define FXAA_QUALITY__P6 12.0 -#endif -/*--------------------------------------------------------------------------*/ -#if (FXAA_QUALITY__PRESET == 15) - #define FXAA_QUALITY__PS 8 - #define FXAA_QUALITY__P0 1.0 - #define FXAA_QUALITY__P1 1.5 - #define FXAA_QUALITY__P2 2.0 - #define FXAA_QUALITY__P3 2.0 - #define FXAA_QUALITY__P4 2.0 - #define FXAA_QUALITY__P5 2.0 - #define FXAA_QUALITY__P6 4.0 - #define FXAA_QUALITY__P7 12.0 -#endif - -/*============================================================================ - FXAA QUALITY - LOW DITHER PRESETS -============================================================================*/ -#if (FXAA_QUALITY__PRESET == 20) - #define FXAA_QUALITY__PS 3 - #define FXAA_QUALITY__P0 1.5 - #define FXAA_QUALITY__P1 2.0 - #define FXAA_QUALITY__P2 8.0 -#endif -/*--------------------------------------------------------------------------*/ -#if (FXAA_QUALITY__PRESET == 21) - #define FXAA_QUALITY__PS 4 - #define FXAA_QUALITY__P0 1.0 - #define FXAA_QUALITY__P1 1.5 - #define FXAA_QUALITY__P2 2.0 - #define FXAA_QUALITY__P3 8.0 -#endif -/*--------------------------------------------------------------------------*/ -#if (FXAA_QUALITY__PRESET == 22) - #define FXAA_QUALITY__PS 5 - #define FXAA_QUALITY__P0 1.0 - #define FXAA_QUALITY__P1 1.5 - #define FXAA_QUALITY__P2 2.0 - #define FXAA_QUALITY__P3 2.0 - #define FXAA_QUALITY__P4 8.0 -#endif -/*--------------------------------------------------------------------------*/ -#if (FXAA_QUALITY__PRESET == 23) - #define FXAA_QUALITY__PS 6 - #define FXAA_QUALITY__P0 1.0 - #define FXAA_QUALITY__P1 1.5 - #define FXAA_QUALITY__P2 2.0 - #define FXAA_QUALITY__P3 2.0 - #define FXAA_QUALITY__P4 2.0 - #define FXAA_QUALITY__P5 8.0 -#endif -/*--------------------------------------------------------------------------*/ -#if (FXAA_QUALITY__PRESET == 24) - #define FXAA_QUALITY__PS 7 - #define FXAA_QUALITY__P0 1.0 - #define FXAA_QUALITY__P1 1.5 - #define FXAA_QUALITY__P2 2.0 - #define FXAA_QUALITY__P3 2.0 - #define FXAA_QUALITY__P4 2.0 - #define FXAA_QUALITY__P5 3.0 - #define FXAA_QUALITY__P6 8.0 -#endif -/*--------------------------------------------------------------------------*/ -#if (FXAA_QUALITY__PRESET == 25) - #define FXAA_QUALITY__PS 8 - #define FXAA_QUALITY__P0 1.0 - #define FXAA_QUALITY__P1 1.5 - #define FXAA_QUALITY__P2 2.0 - #define FXAA_QUALITY__P3 2.0 - #define FXAA_QUALITY__P4 2.0 - #define FXAA_QUALITY__P5 2.0 - #define FXAA_QUALITY__P6 4.0 - #define FXAA_QUALITY__P7 8.0 -#endif -/*--------------------------------------------------------------------------*/ -#if (FXAA_QUALITY__PRESET == 26) - #define FXAA_QUALITY__PS 9 - #define FXAA_QUALITY__P0 1.0 - #define FXAA_QUALITY__P1 1.5 - #define FXAA_QUALITY__P2 2.0 - #define FXAA_QUALITY__P3 2.0 - #define FXAA_QUALITY__P4 2.0 - #define FXAA_QUALITY__P5 2.0 - #define FXAA_QUALITY__P6 2.0 - #define FXAA_QUALITY__P7 4.0 - #define FXAA_QUALITY__P8 8.0 -#endif -/*--------------------------------------------------------------------------*/ -#if (FXAA_QUALITY__PRESET == 27) - #define FXAA_QUALITY__PS 10 - #define FXAA_QUALITY__P0 1.0 - #define FXAA_QUALITY__P1 1.5 - #define FXAA_QUALITY__P2 2.0 - #define FXAA_QUALITY__P3 2.0 - #define FXAA_QUALITY__P4 2.0 - #define FXAA_QUALITY__P5 2.0 - #define FXAA_QUALITY__P6 2.0 - #define FXAA_QUALITY__P7 2.0 - #define FXAA_QUALITY__P8 4.0 - #define FXAA_QUALITY__P9 8.0 -#endif -/*--------------------------------------------------------------------------*/ -#if (FXAA_QUALITY__PRESET == 28) - #define FXAA_QUALITY__PS 11 - #define FXAA_QUALITY__P0 1.0 - #define FXAA_QUALITY__P1 1.5 - #define FXAA_QUALITY__P2 2.0 - #define FXAA_QUALITY__P3 2.0 - #define FXAA_QUALITY__P4 2.0 - #define FXAA_QUALITY__P5 2.0 - #define FXAA_QUALITY__P6 2.0 - #define FXAA_QUALITY__P7 2.0 - #define FXAA_QUALITY__P8 2.0 - #define FXAA_QUALITY__P9 4.0 - #define FXAA_QUALITY__P10 8.0 -#endif -/*--------------------------------------------------------------------------*/ -#if (FXAA_QUALITY__PRESET == 29) - #define FXAA_QUALITY__PS 12 - #define FXAA_QUALITY__P0 1.0 - #define FXAA_QUALITY__P1 1.5 - #define FXAA_QUALITY__P2 2.0 - #define FXAA_QUALITY__P3 2.0 - #define FXAA_QUALITY__P4 2.0 - #define FXAA_QUALITY__P5 2.0 - #define FXAA_QUALITY__P6 2.0 - #define FXAA_QUALITY__P7 2.0 - #define FXAA_QUALITY__P8 2.0 - #define FXAA_QUALITY__P9 2.0 - #define FXAA_QUALITY__P10 4.0 - #define FXAA_QUALITY__P11 8.0 -#endif - -/*============================================================================ - FXAA QUALITY - EXTREME QUALITY -============================================================================*/ -#if (FXAA_QUALITY__PRESET == 39) - #define FXAA_QUALITY__PS 12 - #define FXAA_QUALITY__P0 1.0 - #define FXAA_QUALITY__P1 1.0 - #define FXAA_QUALITY__P2 1.0 - #define FXAA_QUALITY__P3 1.0 - #define FXAA_QUALITY__P4 1.0 - #define FXAA_QUALITY__P5 1.5 - #define FXAA_QUALITY__P6 2.0 - #define FXAA_QUALITY__P7 2.0 - #define FXAA_QUALITY__P8 2.0 - #define FXAA_QUALITY__P9 2.0 - #define FXAA_QUALITY__P10 4.0 - #define FXAA_QUALITY__P11 8.0 -#endif - - - -/*============================================================================ - - API PORTING - -============================================================================*/ -#if (FXAA_GLSL_120 == 1) || (FXAA_GLSL_130 == 1) - #define FxaaBool bool - #define FxaaDiscard discard - #define FxaaFloat float - #define FxaaFloat2 vec2 - #define FxaaFloat3 vec3 - #define FxaaFloat4 vec4 - #define FxaaHalf float - #define FxaaHalf2 vec2 - #define FxaaHalf3 vec3 - #define FxaaHalf4 vec4 - #define FxaaInt2 ivec2 - #define FxaaSat(x) clamp(x, 0.0, 1.0) - #define FxaaTex sampler2D -#else - #define FxaaBool bool - #define FxaaDiscard clip(-1) - #define FxaaFloat float - #define FxaaFloat2 float2 - #define FxaaFloat3 float3 - #define FxaaFloat4 float4 - #define FxaaHalf half - #define FxaaHalf2 half2 - #define FxaaHalf3 half3 - #define FxaaHalf4 half4 - #define FxaaSat(x) saturate(x) -#endif -/*--------------------------------------------------------------------------*/ -#if (FXAA_GLSL_120 == 1) - // Requires, - // #version 120 - // And at least, - // #extension GL_EXT_gpu_shader4 : enable - // (or set FXAA_FAST_PIXEL_OFFSET 1 to work like DX9) - #define FxaaTexTop(t, p) texture2DLod(t, p, 0.0) - #if (FXAA_FAST_PIXEL_OFFSET == 1) - #define FxaaTexOff(t, p, o, r) texture2DLodOffset(t, p, 0.0, o) - #else - #define FxaaTexOff(t, p, o, r) texture2DLod(t, p + (o * r), 0.0) - #endif - #if (FXAA_GATHER4_ALPHA == 1) - // use #extension GL_ARB_gpu_shader5 : enable - #define FxaaTexAlpha4(t, p) textureGather(t, p, 3) - #define FxaaTexOffAlpha4(t, p, o) textureGatherOffset(t, p, o, 3) - #define FxaaTexGreen4(t, p) textureGather(t, p, 1) - #define FxaaTexOffGreen4(t, p, o) textureGatherOffset(t, p, o, 1) - #endif -#endif -/*--------------------------------------------------------------------------*/ -#if (FXAA_GLSL_130 == 1) - // Requires "#version 130" or better - #define FxaaTexTop(t, p) textureLod(t, p, 0.0) - #define FxaaTexOff(t, p, o, r) textureLodOffset(t, p, 0.0, o) - #if (FXAA_GATHER4_ALPHA == 1) - // use #extension GL_ARB_gpu_shader5 : enable - #define FxaaTexAlpha4(t, p) textureGather(t, p, 3) - #define FxaaTexOffAlpha4(t, p, o) textureGatherOffset(t, p, o, 3) - #define FxaaTexGreen4(t, p) textureGather(t, p, 1) - #define FxaaTexOffGreen4(t, p, o) textureGatherOffset(t, p, o, 1) - #endif -#endif -/*--------------------------------------------------------------------------*/ -#if (FXAA_HLSL_3 == 1) || (FXAA_360 == 1) || (FXAA_PS3 == 1) - #define FxaaInt2 float2 - #define FxaaTex sampler2D - #define FxaaTexTop(t, p) tex2Dlod(t, float4(p, 0.0, 0.0)) - #define FxaaTexOff(t, p, o, r) tex2Dlod(t, float4(p + (o * r), 0, 0)) -#endif -/*--------------------------------------------------------------------------*/ -#if (FXAA_HLSL_4 == 1) - #define FxaaInt2 int2 - struct FxaaTex { SamplerState smpl; Texture2D tex; }; - #define FxaaTexTop(t, p) t.tex.SampleLevel(t.smpl, p, 0.0) - #define FxaaTexOff(t, p, o, r) t.tex.SampleLevel(t.smpl, p, 0.0, o) -#endif -/*--------------------------------------------------------------------------*/ -#if (FXAA_HLSL_5 == 1) - #define FxaaInt2 int2 - struct FxaaTex { SamplerState smpl; Texture2D tex; }; - #define FxaaTexTop(t, p) t.tex.SampleLevel(t.smpl, p, 0.0) - #define FxaaTexOff(t, p, o, r) t.tex.SampleLevel(t.smpl, p, 0.0, o) - #define FxaaTexAlpha4(t, p) t.tex.GatherAlpha(t.smpl, p) - #define FxaaTexOffAlpha4(t, p, o) t.tex.GatherAlpha(t.smpl, p, o) - #define FxaaTexGreen4(t, p) t.tex.GatherGreen(t.smpl, p) - #define FxaaTexOffGreen4(t, p, o) t.tex.GatherGreen(t.smpl, p, o) -#endif - - -/*============================================================================ - GREEN AS LUMA OPTION SUPPORT FUNCTION -============================================================================*/ -#if (FXAA_GREEN_AS_LUMA == 0) - FxaaFloat FxaaLuma(FxaaFloat4 rgba) { return rgba.w; } -#else - FxaaFloat FxaaLuma(FxaaFloat4 rgba) { return rgba.y; } -#endif - - - - -/*============================================================================ - - FXAA3 QUALITY - PC - -============================================================================*/ -#if (FXAA_PC == 1) -/*--------------------------------------------------------------------------*/ -FxaaFloat4 FxaaPixelShader( - // - // Use noperspective interpolation here (turn off perspective interpolation). - // {xy} = center of pixel - FxaaFloat2 pos, - // - // Used only for FXAA Console, and not used on the 360 version. - // Use noperspective interpolation here (turn off perspective interpolation). - // {xy__} = upper left of pixel - // {__zw} = lower right of pixel - FxaaFloat4 fxaaConsolePosPos, - // - // Input color texture. - // {rgb_} = color in linear or perceptual color space - // if (FXAA_GREEN_AS_LUMA == 0) - // {___a} = luma in perceptual color space (not linear) - FxaaTex tex, - // - // Only used on the optimized 360 version of FXAA Console. - // For everything but 360, just use the same input here as for "tex". - // For 360, same texture, just alias with a 2nd sampler. - // This sampler needs to have an exponent bias of -1. - FxaaTex fxaaConsole360TexExpBiasNegOne, - // - // Only used on the optimized 360 version of FXAA Console. - // For everything but 360, just use the same input here as for "tex". - // For 360, same texture, just alias with a 3nd sampler. - // This sampler needs to have an exponent bias of -2. - FxaaTex fxaaConsole360TexExpBiasNegTwo, - // - // Only used on FXAA Quality. - // This must be from a constant/uniform. - // {x_} = 1.0/screenWidthInPixels - // {_y} = 1.0/screenHeightInPixels - FxaaFloat2 fxaaQualityRcpFrame, - // - // Only used on FXAA Console. - // This must be from a constant/uniform. - // This effects sub-pixel AA quality and inversely sharpness. - // Where N ranges between, - // N = 0.50 (default) - // N = 0.33 (sharper) - // {x___} = -N/screenWidthInPixels - // {_y__} = -N/screenHeightInPixels - // {__z_} = N/screenWidthInPixels - // {___w} = N/screenHeightInPixels - FxaaFloat4 fxaaConsoleRcpFrameOpt, - // - // Only used on FXAA Console. - // Not used on 360, but used on PS3 and PC. - // This must be from a constant/uniform. - // {x___} = -2.0/screenWidthInPixels - // {_y__} = -2.0/screenHeightInPixels - // {__z_} = 2.0/screenWidthInPixels - // {___w} = 2.0/screenHeightInPixels - FxaaFloat4 fxaaConsoleRcpFrameOpt2, - // - // Only used on FXAA Console. - // Only used on 360 in place of fxaaConsoleRcpFrameOpt2. - // This must be from a constant/uniform. - // {x___} = 8.0/screenWidthInPixels - // {_y__} = 8.0/screenHeightInPixels - // {__z_} = -4.0/screenWidthInPixels - // {___w} = -4.0/screenHeightInPixels - FxaaFloat4 fxaaConsole360RcpFrameOpt2, - // - // Only used on FXAA Quality. - // This used to be the FXAA_QUALITY__SUBPIX define. - // It is here now to allow easier tuning. - // Choose the amount of sub-pixel aliasing removal. - // This can effect sharpness. - // 1.00 - upper limit (softer) - // 0.75 - default amount of filtering - // 0.50 - lower limit (sharper, less sub-pixel aliasing removal) - // 0.25 - almost off - // 0.00 - completely off - FxaaFloat fxaaQualitySubpix, - // - // Only used on FXAA Quality. - // This used to be the FXAA_QUALITY__EDGE_THRESHOLD define. - // It is here now to allow easier tuning. - // The minimum amount of local contrast required to apply algorithm. - // 0.333 - too little (faster) - // 0.250 - low quality - // 0.166 - default - // 0.125 - high quality - // 0.063 - overkill (slower) - FxaaFloat fxaaQualityEdgeThreshold, - // - // Only used on FXAA Quality. - // This used to be the FXAA_QUALITY__EDGE_THRESHOLD_MIN define. - // It is here now to allow easier tuning. - // Trims the algorithm from processing darks. - // 0.0833 - upper limit (default, the start of visible unfiltered edges) - // 0.0625 - high quality (faster) - // 0.0312 - visible limit (slower) - // Special notes when using FXAA_GREEN_AS_LUMA, - // Likely want to set this to zero. - // As colors that are mostly not-green - // will appear very dark in the green channel! - // Tune by looking at mostly non-green content, - // then start at zero and increase until aliasing is a problem. - FxaaFloat fxaaQualityEdgeThresholdMin, - // - // Only used on FXAA Console. - // This used to be the FXAA_CONSOLE__EDGE_SHARPNESS define. - // It is here now to allow easier tuning. - // This does not effect PS3, as this needs to be compiled in. - // Use FXAA_CONSOLE__PS3_EDGE_SHARPNESS for PS3. - // Due to the PS3 being ALU bound, - // there are only three safe values here: 2 and 4 and 8. - // These options use the shaders ability to a free *|/ by 2|4|8. - // For all other platforms can be a non-power of two. - // 8.0 is sharper (default!!!) - // 4.0 is softer - // 2.0 is really soft (good only for vector graphics inputs) - FxaaFloat fxaaConsoleEdgeSharpness, - // - // Only used on FXAA Console. - // This used to be the FXAA_CONSOLE__EDGE_THRESHOLD define. - // It is here now to allow easier tuning. - // This does not effect PS3, as this needs to be compiled in. - // Use FXAA_CONSOLE__PS3_EDGE_THRESHOLD for PS3. - // Due to the PS3 being ALU bound, - // there are only two safe values here: 1/4 and 1/8. - // These options use the shaders ability to a free *|/ by 2|4|8. - // The console setting has a different mapping than the quality setting. - // Other platforms can use other values. - // 0.125 leaves less aliasing, but is softer (default!!!) - // 0.25 leaves more aliasing, and is sharper - FxaaFloat fxaaConsoleEdgeThreshold, - // - // Only used on FXAA Console. - // This used to be the FXAA_CONSOLE__EDGE_THRESHOLD_MIN define. - // It is here now to allow easier tuning. - // Trims the algorithm from processing darks. - // The console setting has a different mapping than the quality setting. - // This only applies when FXAA_EARLY_EXIT is 1. - // This does not apply to PS3, - // PS3 was simplified to avoid more shader instructions. - // 0.06 - faster but more aliasing in darks - // 0.05 - default - // 0.04 - slower and less aliasing in darks - // Special notes when using FXAA_GREEN_AS_LUMA, - // Likely want to set this to zero. - // As colors that are mostly not-green - // will appear very dark in the green channel! - // Tune by looking at mostly non-green content, - // then start at zero and increase until aliasing is a problem. - FxaaFloat fxaaConsoleEdgeThresholdMin, - // - // Extra constants for 360 FXAA Console only. - // Use zeros or anything else for other platforms. - // These must be in physical constant registers and NOT immedates. - // Immedates will result in compiler un-optimizing. - // {xyzw} = float4(1.0, -1.0, 0.25, -0.25) - FxaaFloat4 fxaaConsole360ConstDir -) { -/*--------------------------------------------------------------------------*/ - FxaaFloat2 posM; - posM.x = pos.x; - posM.y = pos.y; - #if (FXAA_GATHER4_ALPHA == 1) - #if (FXAA_DISCARD == 0) - FxaaFloat4 rgbyM = FxaaTexTop(tex, posM); - #if (FXAA_GREEN_AS_LUMA == 0) - #define lumaM rgbyM.w - #else - #define lumaM rgbyM.y - #endif - #endif - #if (FXAA_GREEN_AS_LUMA == 0) - FxaaFloat4 luma4A = FxaaTexAlpha4(tex, posM); - FxaaFloat4 luma4B = FxaaTexOffAlpha4(tex, posM, FxaaInt2(-1, -1)); - #else - FxaaFloat4 luma4A = FxaaTexGreen4(tex, posM); - FxaaFloat4 luma4B = FxaaTexOffGreen4(tex, posM, FxaaInt2(-1, -1)); - #endif - #if (FXAA_DISCARD == 1) - #define lumaM luma4A.w - #endif - #define lumaE luma4A.z - #define lumaS luma4A.x - #define lumaSE luma4A.y - #define lumaNW luma4B.w - #define lumaN luma4B.z - #define lumaW luma4B.x - #else - FxaaFloat4 rgbyM = FxaaTexTop(tex, posM); - #if (FXAA_GREEN_AS_LUMA == 0) - #define lumaM rgbyM.w - #else - #define lumaM rgbyM.y - #endif - FxaaFloat lumaS = FxaaLuma(FxaaTexOff(tex, posM, FxaaInt2(0, 1), fxaaQualityRcpFrame.xy)); - FxaaFloat lumaE = FxaaLuma(FxaaTexOff(tex, posM, FxaaInt2(1, 0), fxaaQualityRcpFrame.xy)); - FxaaFloat lumaN = FxaaLuma(FxaaTexOff(tex, posM, FxaaInt2(0, -1), fxaaQualityRcpFrame.xy)); - FxaaFloat lumaW = FxaaLuma(FxaaTexOff(tex, posM, FxaaInt2(-1, 0), fxaaQualityRcpFrame.xy)); - #endif -/*--------------------------------------------------------------------------*/ - FxaaFloat maxSM = max(lumaS, lumaM); - FxaaFloat minSM = min(lumaS, lumaM); - FxaaFloat maxESM = max(lumaE, maxSM); - FxaaFloat minESM = min(lumaE, minSM); - FxaaFloat maxWN = max(lumaN, lumaW); - FxaaFloat minWN = min(lumaN, lumaW); - FxaaFloat rangeMax = max(maxWN, maxESM); - FxaaFloat rangeMin = min(minWN, minESM); - FxaaFloat rangeMaxScaled = rangeMax * fxaaQualityEdgeThreshold; - FxaaFloat range = rangeMax - rangeMin; - FxaaFloat rangeMaxClamped = max(fxaaQualityEdgeThresholdMin, rangeMaxScaled); - FxaaBool earlyExit = range < rangeMaxClamped; -/*--------------------------------------------------------------------------*/ - if(earlyExit) - #if (FXAA_DISCARD == 1) - FxaaDiscard; - #else - return rgbyM; - #endif -/*--------------------------------------------------------------------------*/ - #if (FXAA_GATHER4_ALPHA == 0) - FxaaFloat lumaNW = FxaaLuma( - FxaaTexOff(tex, posM, FxaaInt2(-1, -1), fxaaQualityRcpFrame.xy)); - FxaaFloat lumaSE = FxaaLuma( - FxaaTexOff(tex, posM, FxaaInt2(1, 1), fxaaQualityRcpFrame.xy)); - FxaaFloat lumaNE = FxaaLuma( - FxaaTexOff(tex, posM, FxaaInt2(1, -1), fxaaQualityRcpFrame.xy)); - FxaaFloat lumaSW = FxaaLuma( - FxaaTexOff(tex, posM, FxaaInt2(-1, 1), fxaaQualityRcpFrame.xy)); - #else - FxaaFloat lumaNE = FxaaLuma( - FxaaTexOff(tex, posM, FxaaInt2(1, -1), fxaaQualityRcpFrame.xy)); - FxaaFloat lumaSW = FxaaLuma( - FxaaTexOff(tex, posM, FxaaInt2(-1, 1), fxaaQualityRcpFrame.xy)); - #endif -/*--------------------------------------------------------------------------*/ - FxaaFloat lumaNS = lumaN + lumaS; - FxaaFloat lumaWE = lumaW + lumaE; - FxaaFloat subpixRcpRange = 1.0 / range; - FxaaFloat subpixNSWE = lumaNS + lumaWE; - FxaaFloat edgeHorz1 = (-2.0 * lumaM) + lumaNS; - FxaaFloat edgeVert1 = (-2.0 * lumaM) + lumaWE; -/*--------------------------------------------------------------------------*/ - FxaaFloat lumaNESE = lumaNE + lumaSE; - FxaaFloat lumaNWNE = lumaNW + lumaNE; - FxaaFloat edgeHorz2 = (-2.0 * lumaE) + lumaNESE; - FxaaFloat edgeVert2 = (-2.0 * lumaN) + lumaNWNE; -/*--------------------------------------------------------------------------*/ - FxaaFloat lumaNWSW = lumaNW + lumaSW; - FxaaFloat lumaSWSE = lumaSW + lumaSE; - FxaaFloat edgeHorz4 = (abs(edgeHorz1) * 2.0) + abs(edgeHorz2); - FxaaFloat edgeVert4 = (abs(edgeVert1) * 2.0) + abs(edgeVert2); - FxaaFloat edgeHorz3 = (-2.0 * lumaW) + lumaNWSW; - FxaaFloat edgeVert3 = (-2.0 * lumaS) + lumaSWSE; - FxaaFloat edgeHorz = abs(edgeHorz3) + edgeHorz4; - FxaaFloat edgeVert = abs(edgeVert3) + edgeVert4; -/*--------------------------------------------------------------------------*/ - FxaaFloat subpixNWSWNESE = lumaNWSW + lumaNESE; - FxaaFloat lengthSign = fxaaQualityRcpFrame.x; - FxaaBool horzSpan = edgeHorz >= edgeVert; - FxaaFloat subpixA = subpixNSWE * 2.0 + subpixNWSWNESE; -/*--------------------------------------------------------------------------*/ - if (!horzSpan) lumaN = lumaW; - if (!horzSpan) lumaS = lumaE; - if (horzSpan) lengthSign = fxaaQualityRcpFrame.y; - FxaaFloat subpixB = (subpixA * (1.0 / 12.0)) - lumaM; -/*--------------------------------------------------------------------------*/ - FxaaFloat gradientN = lumaN - lumaM; - FxaaFloat gradientS = lumaS - lumaM; - FxaaFloat lumaNN = lumaN + lumaM; - FxaaFloat lumaSS = lumaS + lumaM; - FxaaBool pairN = abs(gradientN) >= abs(gradientS); - FxaaFloat gradient = max(abs(gradientN), abs(gradientS)); - if (pairN) lengthSign = -lengthSign; - FxaaFloat subpixC = FxaaSat(abs(subpixB) * subpixRcpRange); -/*--------------------------------------------------------------------------*/ - FxaaFloat2 posB; - posB.x = posM.x; - posB.y = posM.y; - FxaaFloat2 offNP; - offNP.x = (!horzSpan) ? 0.0 : fxaaQualityRcpFrame.x; - offNP.y = (horzSpan) ? 0.0 : fxaaQualityRcpFrame.y; - if (!horzSpan) posB.x += lengthSign * 0.5; - if (horzSpan) posB.y += lengthSign * 0.5; -/*--------------------------------------------------------------------------*/ - FxaaFloat2 posN; - posN.x = posB.x - offNP.x * FXAA_QUALITY__P0; - posN.y = posB.y - offNP.y * FXAA_QUALITY__P0; - FxaaFloat2 posP; - posP.x = posB.x + offNP.x * FXAA_QUALITY__P0; - posP.y = posB.y + offNP.y * FXAA_QUALITY__P0; - FxaaFloat subpixD = (-2.0 * subpixC) + 3.0; - FxaaFloat lumaEndN = FxaaLuma(FxaaTexTop(tex, posN)); - FxaaFloat subpixE = subpixC * subpixC; - FxaaFloat lumaEndP = FxaaLuma(FxaaTexTop(tex, posP)); -/*--------------------------------------------------------------------------*/ - if(!pairN) lumaNN = lumaSS; - FxaaFloat gradientScaled = gradient * 1.0 / 4.0; - FxaaFloat lumaMM = lumaM - lumaNN * 0.5; - FxaaFloat subpixF = subpixD * subpixE; - FxaaBool lumaMLTZero = lumaMM < 0.0; -/*--------------------------------------------------------------------------*/ - lumaEndN -= lumaNN * 0.5; - lumaEndP -= lumaNN * 0.5; - FxaaBool doneN = abs(lumaEndN) >= gradientScaled; - FxaaBool doneP = abs(lumaEndP) >= gradientScaled; - if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P1; - if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P1; - FxaaBool doneNP = (!doneN) || (!doneP); - if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P1; - if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P1; -/*--------------------------------------------------------------------------*/ - if(doneNP) { - if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); - if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); - if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; - if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; - doneN = abs(lumaEndN) >= gradientScaled; - doneP = abs(lumaEndP) >= gradientScaled; - if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P2; - if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P2; - doneNP = (!doneN) || (!doneP); - if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P2; - if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P2; -/*--------------------------------------------------------------------------*/ - #if (FXAA_QUALITY__PS > 3) - if(doneNP) { - if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); - if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); - if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; - if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; - doneN = abs(lumaEndN) >= gradientScaled; - doneP = abs(lumaEndP) >= gradientScaled; - if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P3; - if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P3; - doneNP = (!doneN) || (!doneP); - if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P3; - if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P3; -/*--------------------------------------------------------------------------*/ - #if (FXAA_QUALITY__PS > 4) - if(doneNP) { - if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); - if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); - if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; - if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; - doneN = abs(lumaEndN) >= gradientScaled; - doneP = abs(lumaEndP) >= gradientScaled; - if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P4; - if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P4; - doneNP = (!doneN) || (!doneP); - if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P4; - if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P4; -/*--------------------------------------------------------------------------*/ - #if (FXAA_QUALITY__PS > 5) - if(doneNP) { - if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); - if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); - if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; - if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; - doneN = abs(lumaEndN) >= gradientScaled; - doneP = abs(lumaEndP) >= gradientScaled; - if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P5; - if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P5; - doneNP = (!doneN) || (!doneP); - if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P5; - if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P5; -/*--------------------------------------------------------------------------*/ - #if (FXAA_QUALITY__PS > 6) - if(doneNP) { - if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); - if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); - if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; - if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; - doneN = abs(lumaEndN) >= gradientScaled; - doneP = abs(lumaEndP) >= gradientScaled; - if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P6; - if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P6; - doneNP = (!doneN) || (!doneP); - if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P6; - if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P6; -/*--------------------------------------------------------------------------*/ - #if (FXAA_QUALITY__PS > 7) - if(doneNP) { - if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); - if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); - if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; - if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; - doneN = abs(lumaEndN) >= gradientScaled; - doneP = abs(lumaEndP) >= gradientScaled; - if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P7; - if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P7; - doneNP = (!doneN) || (!doneP); - if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P7; - if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P7; -/*--------------------------------------------------------------------------*/ - #if (FXAA_QUALITY__PS > 8) - if(doneNP) { - if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); - if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); - if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; - if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; - doneN = abs(lumaEndN) >= gradientScaled; - doneP = abs(lumaEndP) >= gradientScaled; - if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P8; - if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P8; - doneNP = (!doneN) || (!doneP); - if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P8; - if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P8; -/*--------------------------------------------------------------------------*/ - #if (FXAA_QUALITY__PS > 9) - if(doneNP) { - if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); - if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); - if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; - if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; - doneN = abs(lumaEndN) >= gradientScaled; - doneP = abs(lumaEndP) >= gradientScaled; - if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P9; - if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P9; - doneNP = (!doneN) || (!doneP); - if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P9; - if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P9; -/*--------------------------------------------------------------------------*/ - #if (FXAA_QUALITY__PS > 10) - if(doneNP) { - if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); - if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); - if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; - if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; - doneN = abs(lumaEndN) >= gradientScaled; - doneP = abs(lumaEndP) >= gradientScaled; - if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P10; - if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P10; - doneNP = (!doneN) || (!doneP); - if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P10; - if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P10; -/*--------------------------------------------------------------------------*/ - #if (FXAA_QUALITY__PS > 11) - if(doneNP) { - if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); - if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); - if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; - if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; - doneN = abs(lumaEndN) >= gradientScaled; - doneP = abs(lumaEndP) >= gradientScaled; - if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P11; - if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P11; - doneNP = (!doneN) || (!doneP); - if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P11; - if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P11; -/*--------------------------------------------------------------------------*/ - #if (FXAA_QUALITY__PS > 12) - if(doneNP) { - if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); - if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); - if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; - if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; - doneN = abs(lumaEndN) >= gradientScaled; - doneP = abs(lumaEndP) >= gradientScaled; - if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P12; - if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P12; - doneNP = (!doneN) || (!doneP); - if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P12; - if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P12; -/*--------------------------------------------------------------------------*/ - } - #endif -/*--------------------------------------------------------------------------*/ - } - #endif -/*--------------------------------------------------------------------------*/ - } - #endif -/*--------------------------------------------------------------------------*/ - } - #endif -/*--------------------------------------------------------------------------*/ - } - #endif -/*--------------------------------------------------------------------------*/ - } - #endif -/*--------------------------------------------------------------------------*/ - } - #endif -/*--------------------------------------------------------------------------*/ - } - #endif -/*--------------------------------------------------------------------------*/ - } - #endif -/*--------------------------------------------------------------------------*/ - } - #endif -/*--------------------------------------------------------------------------*/ - } -/*--------------------------------------------------------------------------*/ - FxaaFloat dstN = posM.x - posN.x; - FxaaFloat dstP = posP.x - posM.x; - if(!horzSpan) dstN = posM.y - posN.y; - if(!horzSpan) dstP = posP.y - posM.y; -/*--------------------------------------------------------------------------*/ - FxaaBool goodSpanN = (lumaEndN < 0.0) != lumaMLTZero; - FxaaFloat spanLength = (dstP + dstN); - FxaaBool goodSpanP = (lumaEndP < 0.0) != lumaMLTZero; - FxaaFloat spanLengthRcp = 1.0 / spanLength; -/*--------------------------------------------------------------------------*/ - FxaaBool directionN = dstN < dstP; - FxaaFloat dst = min(dstN, dstP); - FxaaBool goodSpan = directionN ? goodSpanN : goodSpanP; - FxaaFloat subpixG = subpixF * subpixF; - FxaaFloat pixelOffset = (dst * (-spanLengthRcp)) + 0.5; - FxaaFloat subpixH = subpixG * fxaaQualitySubpix; -/*--------------------------------------------------------------------------*/ - FxaaFloat pixelOffsetGood = goodSpan ? pixelOffset : 0.0; - FxaaFloat pixelOffsetSubpix = max(pixelOffsetGood, subpixH); - if(!horzSpan) posM.x += pixelOffsetSubpix * lengthSign; - if(horzSpan) posM.y += pixelOffsetSubpix * lengthSign; - #if (FXAA_DISCARD == 1) - return FxaaTexTop(tex, posM); - #else - return FxaaFloat4(FxaaTexTop(tex, posM).xyz, lumaM); - #endif -} -/*==========================================================================*/ -#endif +#pragma once + +/*============================================================================ + + + NVIDIA FXAA 3.11 by TIMOTHY LOTTES + + +------------------------------------------------------------------------------ +COPYRIGHT (C) 2010, 2011 NVIDIA CORPORATION. ALL RIGHTS RESERVED. +------------------------------------------------------------------------------ +TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THIS SOFTWARE IS PROVIDED +*AS IS* AND NVIDIA AND ITS SUPPLIERS DISCLAIM ALL WARRANTIES, EITHER EXPRESS +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT SHALL NVIDIA +OR ITS SUPPLIERS BE LIABLE FOR ANY SPECIAL, INCIDENTAL, INDIRECT, OR +CONSEQUENTIAL DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR +LOSS OF BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, +OR ANY OTHER PECUNIARY LOSS) ARISING OUT OF THE USE OF OR INABILITY TO USE +THIS SOFTWARE, EVEN IF NVIDIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + +------------------------------------------------------------------------------ + INTEGRATION CHECKLIST +------------------------------------------------------------------------------ +(1.) +In the shader source, setup defines for the desired configuration. +When providing multiple shaders (for different presets), +simply setup the defines differently in multiple files. +Example, + + #define FXAA_PC 1 + #define FXAA_HLSL_5 1 + #define FXAA_QUALITY__PRESET 12 + +Or, + + #define FXAA_360 1 + +Or, + + #define FXAA_PS3 1 + +Etc. + +(2.) +Then include this file, + + #include "Fxaa3_11.h" + +(3.) +Then call the FXAA pixel shader from within your desired shader. +Look at the FXAA Quality FxaaPixelShader() for docs on inputs. +As for FXAA 3.11 all inputs for all shaders are the same +to enable easy porting between platforms. + + return FxaaPixelShader(...); + +(4.) +Insure pass prior to FXAA outputs RGBL (see next section). +Or use, + + #define FXAA_GREEN_AS_LUMA 1 + +(5.) +Setup engine to provide the following constants +which are used in the FxaaPixelShader() inputs, + + FxaaFloat2 fxaaQualityRcpFrame, + FxaaFloat4 fxaaConsoleRcpFrameOpt, + FxaaFloat4 fxaaConsoleRcpFrameOpt2, + FxaaFloat4 fxaaConsole360RcpFrameOpt2, + FxaaFloat fxaaQualitySubpix, + FxaaFloat fxaaQualityEdgeThreshold, + FxaaFloat fxaaQualityEdgeThresholdMin, + FxaaFloat fxaaConsoleEdgeSharpness, + FxaaFloat fxaaConsoleEdgeThreshold, + FxaaFloat fxaaConsoleEdgeThresholdMin, + FxaaFloat4 fxaaConsole360ConstDir + +Look at the FXAA Quality FxaaPixelShader() for docs on inputs. + +(6.) +Have FXAA vertex shader run as a full screen triangle, +and output "pos" and "fxaaConsolePosPos" +such that inputs in the pixel shader provide, + + // {xy} = center of pixel + FxaaFloat2 pos, + + // {xy__} = upper left of pixel + // {__zw} = lower right of pixel + FxaaFloat4 fxaaConsolePosPos, + +(7.) +Insure the texture sampler(s) used by FXAA are set to bilinear filtering. + + +------------------------------------------------------------------------------ + INTEGRATION - RGBL AND COLORSPACE +------------------------------------------------------------------------------ +FXAA3 requires RGBL as input unless the following is set, + + #define FXAA_GREEN_AS_LUMA 1 + +In which case the engine uses green in place of luma, +and requires RGB input is in a non-linear colorspace. + +RGB should be LDR (low dynamic range). +Specifically do FXAA after tonemapping. + +RGB data as returned by a texture fetch can be non-linear, +or linear when FXAA_GREEN_AS_LUMA is not set. +Note an "sRGB format" texture counts as linear, +because the result of a texture fetch is linear data. +Regular "RGBA8" textures in the sRGB colorspace are non-linear. + +If FXAA_GREEN_AS_LUMA is not set, +luma must be stored in the alpha channel prior to running FXAA. +This luma should be in a perceptual space (could be gamma 2.0). +Example pass before FXAA where output is gamma 2.0 encoded, + + color.rgb = ToneMap(color.rgb); // linear color output + color.rgb = sqrt(color.rgb); // gamma 2.0 color output + return color; + +To use FXAA, + + color.rgb = ToneMap(color.rgb); // linear color output + color.rgb = sqrt(color.rgb); // gamma 2.0 color output + color.a = dot(color.rgb, FxaaFloat3(0.299, 0.587, 0.114)); // compute luma + return color; + +Another example where output is linear encoded, +say for instance writing to an sRGB formated render target, +where the render target does the conversion back to sRGB after blending, + + color.rgb = ToneMap(color.rgb); // linear color output + return color; + +To use FXAA, + + color.rgb = ToneMap(color.rgb); // linear color output + color.a = sqrt(dot(color.rgb, FxaaFloat3(0.299, 0.587, 0.114))); // compute luma + return color; + +Getting luma correct is required for the algorithm to work correctly. + + +------------------------------------------------------------------------------ + BEING LINEARLY CORRECT? +------------------------------------------------------------------------------ +Applying FXAA to a framebuffer with linear RGB color will look worse. +This is very counter intuitive, but happends to be true in this case. +The reason is because dithering artifacts will be more visiable +in a linear colorspace. + + +------------------------------------------------------------------------------ + COMPLEX INTEGRATION +------------------------------------------------------------------------------ +Q. What if the engine is blending into RGB before wanting to run FXAA? + +A. In the last opaque pass prior to FXAA, + have the pass write out luma into alpha. + Then blend into RGB only. + FXAA should be able to run ok + assuming the blending pass did not any add aliasing. + This should be the common case for particles and common blending passes. + +A. Or use FXAA_GREEN_AS_LUMA. + +============================================================================*/ + +/*============================================================================ + + INTEGRATION KNOBS + +============================================================================*/ +// +// FXAA_PS3 and FXAA_360 choose the console algorithm (FXAA3 CONSOLE). +// FXAA_360_OPT is a prototype for the new optimized 360 version. +// +// 1 = Use API. +// 0 = Don't use API. +// +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_PS3 + #define FXAA_PS3 0 +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_360 + #define FXAA_360 0 +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_360_OPT + #define FXAA_360_OPT 0 +#endif +/*==========================================================================*/ +#ifndef FXAA_PC + // + // FXAA Quality + // The high quality PC algorithm. + // + #define FXAA_PC 0 +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_PC_CONSOLE + // + // The console algorithm for PC is included + // for developers targeting really low spec machines. + // Likely better to just run FXAA_PC, and use a really low preset. + // + #define FXAA_PC_CONSOLE 0 +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_GLSL_120 + #define FXAA_GLSL_120 0 +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_GLSL_130 + #define FXAA_GLSL_130 0 +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_HLSL_3 + #define FXAA_HLSL_3 0 +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_HLSL_4 + #define FXAA_HLSL_4 0 +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_HLSL_5 + #define FXAA_HLSL_5 0 +#endif +/*==========================================================================*/ +#ifndef FXAA_GREEN_AS_LUMA + // + // For those using non-linear color, + // and either not able to get luma in alpha, or not wanting to, + // this enables FXAA to run using green as a proxy for luma. + // So with this enabled, no need to pack luma in alpha. + // + // This will turn off AA on anything which lacks some amount of green. + // Pure red and blue or combination of only R and B, will get no AA. + // + // Might want to lower the settings for both, + // fxaaConsoleEdgeThresholdMin + // fxaaQualityEdgeThresholdMin + // In order to insure AA does not get turned off on colors + // which contain a minor amount of green. + // + // 1 = On. + // 0 = Off. + // + #define FXAA_GREEN_AS_LUMA 0 +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_EARLY_EXIT + // + // Controls algorithm's early exit path. + // On PS3 turning this ON adds 2 cycles to the shader. + // On 360 turning this OFF adds 10ths of a millisecond to the shader. + // Turning this off on console will result in a more blurry image. + // So this defaults to on. + // + // 1 = On. + // 0 = Off. + // + #define FXAA_EARLY_EXIT 1 +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_DISCARD + // + // Only valid for PC OpenGL currently. + // Probably will not work when FXAA_GREEN_AS_LUMA = 1. + // + // 1 = Use discard on pixels which don't need AA. + // For APIs which enable concurrent TEX+ROP from same surface. + // 0 = Return unchanged color on pixels which don't need AA. + // + #define FXAA_DISCARD 0 +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_FAST_PIXEL_OFFSET + // + // Used for GLSL 120 only. + // + // 1 = GL API supports fast pixel offsets + // 0 = do not use fast pixel offsets + // + #ifdef GL_EXT_gpu_shader4 + #define FXAA_FAST_PIXEL_OFFSET 1 + #endif + #ifdef GL_NV_gpu_shader5 + #define FXAA_FAST_PIXEL_OFFSET 1 + #endif + #ifdef GL_ARB_gpu_shader5 + #define FXAA_FAST_PIXEL_OFFSET 1 + #endif + #ifndef FXAA_FAST_PIXEL_OFFSET + #define FXAA_FAST_PIXEL_OFFSET 0 + #endif +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_GATHER4_ALPHA + // + // 1 = API supports gather4 on alpha channel. + // 0 = API does not support gather4 on alpha channel. + // + #if (FXAA_HLSL_5 == 1) + #define FXAA_GATHER4_ALPHA 1 + #endif + #ifdef GL_ARB_gpu_shader5 + #define FXAA_GATHER4_ALPHA 1 + #endif + #ifdef GL_NV_gpu_shader5 + #define FXAA_GATHER4_ALPHA 1 + #endif + #ifndef FXAA_GATHER4_ALPHA + #define FXAA_GATHER4_ALPHA 0 + #endif +#endif + +/*============================================================================ + FXAA CONSOLE PS3 - TUNING KNOBS +============================================================================*/ +#ifndef FXAA_CONSOLE__PS3_EDGE_SHARPNESS + // + // Consoles the sharpness of edges on PS3 only. + // Non-PS3 tuning is done with shader input. + // + // Due to the PS3 being ALU bound, + // there are only two safe values here: 4 and 8. + // These options use the shaders ability to a free *|/ by 2|4|8. + // + // 8.0 is sharper + // 4.0 is softer + // 2.0 is really soft (good for vector graphics inputs) + // + #if 1 + #define FXAA_CONSOLE__PS3_EDGE_SHARPNESS 8.0 + #endif + #if 0 + #define FXAA_CONSOLE__PS3_EDGE_SHARPNESS 4.0 + #endif + #if 0 + #define FXAA_CONSOLE__PS3_EDGE_SHARPNESS 2.0 + #endif +#endif +/*--------------------------------------------------------------------------*/ +#ifndef FXAA_CONSOLE__PS3_EDGE_THRESHOLD + // + // Only effects PS3. + // Non-PS3 tuning is done with shader input. + // + // The minimum amount of local contrast required to apply algorithm. + // The console setting has a different mapping than the quality setting. + // + // This only applies when FXAA_EARLY_EXIT is 1. + // + // Due to the PS3 being ALU bound, + // there are only two safe values here: 0.25 and 0.125. + // These options use the shaders ability to a free *|/ by 2|4|8. + // + // 0.125 leaves less aliasing, but is softer + // 0.25 leaves more aliasing, and is sharper + // + #if 1 + #define FXAA_CONSOLE__PS3_EDGE_THRESHOLD 0.125 + #else + #define FXAA_CONSOLE__PS3_EDGE_THRESHOLD 0.25 + #endif +#endif + +/*============================================================================ + FXAA QUALITY - TUNING KNOBS +------------------------------------------------------------------------------ +NOTE the other tuning knobs are now in the shader function inputs! +============================================================================*/ +#ifndef FXAA_QUALITY__PRESET + // + // Choose the quality preset. + // This needs to be compiled into the shader as it effects code. + // Best option to include multiple presets is to + // in each shader define the preset, then include this file. + // + // OPTIONS + // ----------------------------------------------------------------------- + // 10 to 15 - default medium dither (10=fastest, 15=highest quality) + // 20 to 29 - less dither, more expensive (20=fastest, 29=highest quality) + // 39 - no dither, very expensive + // + // NOTES + // ----------------------------------------------------------------------- + // 12 = slightly faster then FXAA 3.9 and higher edge quality (default) + // 13 = about same speed as FXAA 3.9 and better than 12 + // 23 = closest to FXAA 3.9 visually and performance wise + // _ = the lowest digit is directly related to performance + // _ = the highest digit is directly related to style + // + #define FXAA_QUALITY__PRESET 12 +#endif + + +/*============================================================================ + + FXAA QUALITY - PRESETS + +============================================================================*/ + +/*============================================================================ + FXAA QUALITY - MEDIUM DITHER PRESETS +============================================================================*/ +#if (FXAA_QUALITY__PRESET == 10) + #define FXAA_QUALITY__PS 3 + #define FXAA_QUALITY__P0 1.5 + #define FXAA_QUALITY__P1 3.0 + #define FXAA_QUALITY__P2 12.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 11) + #define FXAA_QUALITY__PS 4 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 3.0 + #define FXAA_QUALITY__P3 12.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 12) + #define FXAA_QUALITY__PS 5 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 4.0 + #define FXAA_QUALITY__P4 12.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 13) + #define FXAA_QUALITY__PS 6 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 4.0 + #define FXAA_QUALITY__P5 12.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 14) + #define FXAA_QUALITY__PS 7 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 2.0 + #define FXAA_QUALITY__P5 4.0 + #define FXAA_QUALITY__P6 12.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 15) + #define FXAA_QUALITY__PS 8 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 2.0 + #define FXAA_QUALITY__P5 2.0 + #define FXAA_QUALITY__P6 4.0 + #define FXAA_QUALITY__P7 12.0 +#endif + +/*============================================================================ + FXAA QUALITY - LOW DITHER PRESETS +============================================================================*/ +#if (FXAA_QUALITY__PRESET == 20) + #define FXAA_QUALITY__PS 3 + #define FXAA_QUALITY__P0 1.5 + #define FXAA_QUALITY__P1 2.0 + #define FXAA_QUALITY__P2 8.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 21) + #define FXAA_QUALITY__PS 4 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 8.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 22) + #define FXAA_QUALITY__PS 5 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 8.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 23) + #define FXAA_QUALITY__PS 6 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 2.0 + #define FXAA_QUALITY__P5 8.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 24) + #define FXAA_QUALITY__PS 7 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 2.0 + #define FXAA_QUALITY__P5 3.0 + #define FXAA_QUALITY__P6 8.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 25) + #define FXAA_QUALITY__PS 8 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 2.0 + #define FXAA_QUALITY__P5 2.0 + #define FXAA_QUALITY__P6 4.0 + #define FXAA_QUALITY__P7 8.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 26) + #define FXAA_QUALITY__PS 9 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 2.0 + #define FXAA_QUALITY__P5 2.0 + #define FXAA_QUALITY__P6 2.0 + #define FXAA_QUALITY__P7 4.0 + #define FXAA_QUALITY__P8 8.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 27) + #define FXAA_QUALITY__PS 10 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 2.0 + #define FXAA_QUALITY__P5 2.0 + #define FXAA_QUALITY__P6 2.0 + #define FXAA_QUALITY__P7 2.0 + #define FXAA_QUALITY__P8 4.0 + #define FXAA_QUALITY__P9 8.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 28) + #define FXAA_QUALITY__PS 11 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 2.0 + #define FXAA_QUALITY__P5 2.0 + #define FXAA_QUALITY__P6 2.0 + #define FXAA_QUALITY__P7 2.0 + #define FXAA_QUALITY__P8 2.0 + #define FXAA_QUALITY__P9 4.0 + #define FXAA_QUALITY__P10 8.0 +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_QUALITY__PRESET == 29) + #define FXAA_QUALITY__PS 12 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.5 + #define FXAA_QUALITY__P2 2.0 + #define FXAA_QUALITY__P3 2.0 + #define FXAA_QUALITY__P4 2.0 + #define FXAA_QUALITY__P5 2.0 + #define FXAA_QUALITY__P6 2.0 + #define FXAA_QUALITY__P7 2.0 + #define FXAA_QUALITY__P8 2.0 + #define FXAA_QUALITY__P9 2.0 + #define FXAA_QUALITY__P10 4.0 + #define FXAA_QUALITY__P11 8.0 +#endif + +/*============================================================================ + FXAA QUALITY - EXTREME QUALITY +============================================================================*/ +#if (FXAA_QUALITY__PRESET == 39) + #define FXAA_QUALITY__PS 12 + #define FXAA_QUALITY__P0 1.0 + #define FXAA_QUALITY__P1 1.0 + #define FXAA_QUALITY__P2 1.0 + #define FXAA_QUALITY__P3 1.0 + #define FXAA_QUALITY__P4 1.0 + #define FXAA_QUALITY__P5 1.5 + #define FXAA_QUALITY__P6 2.0 + #define FXAA_QUALITY__P7 2.0 + #define FXAA_QUALITY__P8 2.0 + #define FXAA_QUALITY__P9 2.0 + #define FXAA_QUALITY__P10 4.0 + #define FXAA_QUALITY__P11 8.0 +#endif + + + +/*============================================================================ + + API PORTING + +============================================================================*/ +#if (FXAA_GLSL_120 == 1) || (FXAA_GLSL_130 == 1) + #define FxaaBool bool + #define FxaaDiscard discard + #define FxaaFloat float + #define FxaaFloat2 vec2 + #define FxaaFloat3 vec3 + #define FxaaFloat4 vec4 + #define FxaaHalf float + #define FxaaHalf2 vec2 + #define FxaaHalf3 vec3 + #define FxaaHalf4 vec4 + #define FxaaInt2 ivec2 + #define FxaaSat(x) clamp(x, 0.0, 1.0) + #define FxaaTex sampler2D +#else + #define FxaaBool bool + #define FxaaDiscard clip(-1) + #define FxaaFloat float + #define FxaaFloat2 float2 + #define FxaaFloat3 float3 + #define FxaaFloat4 float4 + #define FxaaHalf half + #define FxaaHalf2 half2 + #define FxaaHalf3 half3 + #define FxaaHalf4 half4 + #define FxaaSat(x) saturate(x) +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_GLSL_120 == 1) + // Requires, + // #version 120 + // And at least, + // #extension GL_EXT_gpu_shader4 : enable + // (or set FXAA_FAST_PIXEL_OFFSET 1 to work like DX9) + #define FxaaTexTop(t, p) texture2DLod(t, p, 0.0) + #if (FXAA_FAST_PIXEL_OFFSET == 1) + #define FxaaTexOff(t, p, o, r) texture2DLodOffset(t, p, 0.0, o) + #else + #define FxaaTexOff(t, p, o, r) texture2DLod(t, p + (o * r), 0.0) + #endif + #if (FXAA_GATHER4_ALPHA == 1) + // use #extension GL_ARB_gpu_shader5 : enable + #define FxaaTexAlpha4(t, p) textureGather(t, p, 3) + #define FxaaTexOffAlpha4(t, p, o) textureGatherOffset(t, p, o, 3) + #define FxaaTexGreen4(t, p) textureGather(t, p, 1) + #define FxaaTexOffGreen4(t, p, o) textureGatherOffset(t, p, o, 1) + #endif +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_GLSL_130 == 1) + // Requires "#version 130" or better + #define FxaaTexTop(t, p) textureLod(t, p, 0.0) + #define FxaaTexOff(t, p, o, r) textureLodOffset(t, p, 0.0, o) + #if (FXAA_GATHER4_ALPHA == 1) + // use #extension GL_ARB_gpu_shader5 : enable + #define FxaaTexAlpha4(t, p) textureGather(t, p, 3) + #define FxaaTexOffAlpha4(t, p, o) textureGatherOffset(t, p, o, 3) + #define FxaaTexGreen4(t, p) textureGather(t, p, 1) + #define FxaaTexOffGreen4(t, p, o) textureGatherOffset(t, p, o, 1) + #endif +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_HLSL_3 == 1) || (FXAA_360 == 1) || (FXAA_PS3 == 1) + #define FxaaInt2 float2 + #define FxaaTex sampler2D + #define FxaaTexTop(t, p) tex2Dlod(t, float4(p, 0.0, 0.0)) + #define FxaaTexOff(t, p, o, r) tex2Dlod(t, float4(p + (o * r), 0, 0)) +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_HLSL_4 == 1) + #define FxaaInt2 int2 + struct FxaaTex { SamplerState smpl; Texture2D tex; }; + #define FxaaTexTop(t, p) t.tex.SampleLevel(t.smpl, p, 0.0) + #define FxaaTexOff(t, p, o, r) t.tex.SampleLevel(t.smpl, p, 0.0, o) +#endif +/*--------------------------------------------------------------------------*/ +#if (FXAA_HLSL_5 == 1) + #define FxaaInt2 int2 + struct FxaaTex { SamplerState smpl; Texture2D tex; }; + #define FxaaTexTop(t, p) t.tex.SampleLevel(t.smpl, p, 0.0) + #define FxaaTexOff(t, p, o, r) t.tex.SampleLevel(t.smpl, p, 0.0, o) + #define FxaaTexAlpha4(t, p) t.tex.GatherAlpha(t.smpl, p) + #define FxaaTexOffAlpha4(t, p, o) t.tex.GatherAlpha(t.smpl, p, o) + #define FxaaTexGreen4(t, p) t.tex.GatherGreen(t.smpl, p) + #define FxaaTexOffGreen4(t, p, o) t.tex.GatherGreen(t.smpl, p, o) +#endif + + +/*============================================================================ + GREEN AS LUMA OPTION SUPPORT FUNCTION +============================================================================*/ +#if (FXAA_GREEN_AS_LUMA == 0) + FxaaFloat FxaaLuma(FxaaFloat4 rgba) { return rgba.w; } +#else + FxaaFloat FxaaLuma(FxaaFloat4 rgba) { return rgba.y; } +#endif + + + + +/*============================================================================ + + FXAA3 QUALITY - PC + +============================================================================*/ +#if (FXAA_PC == 1) +/*--------------------------------------------------------------------------*/ +FxaaFloat4 FxaaPixelShader( + // + // Use noperspective interpolation here (turn off perspective interpolation). + // {xy} = center of pixel + FxaaFloat2 pos, + // + // Used only for FXAA Console, and not used on the 360 version. + // Use noperspective interpolation here (turn off perspective interpolation). + // {xy__} = upper left of pixel + // {__zw} = lower right of pixel + FxaaFloat4 fxaaConsolePosPos, + // + // Input color texture. + // {rgb_} = color in linear or perceptual color space + // if (FXAA_GREEN_AS_LUMA == 0) + // {___a} = luma in perceptual color space (not linear) + FxaaTex tex, + // + // Only used on the optimized 360 version of FXAA Console. + // For everything but 360, just use the same input here as for "tex". + // For 360, same texture, just alias with a 2nd sampler. + // This sampler needs to have an exponent bias of -1. + FxaaTex fxaaConsole360TexExpBiasNegOne, + // + // Only used on the optimized 360 version of FXAA Console. + // For everything but 360, just use the same input here as for "tex". + // For 360, same texture, just alias with a 3nd sampler. + // This sampler needs to have an exponent bias of -2. + FxaaTex fxaaConsole360TexExpBiasNegTwo, + // + // Only used on FXAA Quality. + // This must be from a constant/uniform. + // {x_} = 1.0/screenWidthInPixels + // {_y} = 1.0/screenHeightInPixels + FxaaFloat2 fxaaQualityRcpFrame, + // + // Only used on FXAA Console. + // This must be from a constant/uniform. + // This effects sub-pixel AA quality and inversely sharpness. + // Where N ranges between, + // N = 0.50 (default) + // N = 0.33 (sharper) + // {x___} = -N/screenWidthInPixels + // {_y__} = -N/screenHeightInPixels + // {__z_} = N/screenWidthInPixels + // {___w} = N/screenHeightInPixels + FxaaFloat4 fxaaConsoleRcpFrameOpt, + // + // Only used on FXAA Console. + // Not used on 360, but used on PS3 and PC. + // This must be from a constant/uniform. + // {x___} = -2.0/screenWidthInPixels + // {_y__} = -2.0/screenHeightInPixels + // {__z_} = 2.0/screenWidthInPixels + // {___w} = 2.0/screenHeightInPixels + FxaaFloat4 fxaaConsoleRcpFrameOpt2, + // + // Only used on FXAA Console. + // Only used on 360 in place of fxaaConsoleRcpFrameOpt2. + // This must be from a constant/uniform. + // {x___} = 8.0/screenWidthInPixels + // {_y__} = 8.0/screenHeightInPixels + // {__z_} = -4.0/screenWidthInPixels + // {___w} = -4.0/screenHeightInPixels + FxaaFloat4 fxaaConsole360RcpFrameOpt2, + // + // Only used on FXAA Quality. + // This used to be the FXAA_QUALITY__SUBPIX define. + // It is here now to allow easier tuning. + // Choose the amount of sub-pixel aliasing removal. + // This can effect sharpness. + // 1.00 - upper limit (softer) + // 0.75 - default amount of filtering + // 0.50 - lower limit (sharper, less sub-pixel aliasing removal) + // 0.25 - almost off + // 0.00 - completely off + FxaaFloat fxaaQualitySubpix, + // + // Only used on FXAA Quality. + // This used to be the FXAA_QUALITY__EDGE_THRESHOLD define. + // It is here now to allow easier tuning. + // The minimum amount of local contrast required to apply algorithm. + // 0.333 - too little (faster) + // 0.250 - low quality + // 0.166 - default + // 0.125 - high quality + // 0.063 - overkill (slower) + FxaaFloat fxaaQualityEdgeThreshold, + // + // Only used on FXAA Quality. + // This used to be the FXAA_QUALITY__EDGE_THRESHOLD_MIN define. + // It is here now to allow easier tuning. + // Trims the algorithm from processing darks. + // 0.0833 - upper limit (default, the start of visible unfiltered edges) + // 0.0625 - high quality (faster) + // 0.0312 - visible limit (slower) + // Special notes when using FXAA_GREEN_AS_LUMA, + // Likely want to set this to zero. + // As colors that are mostly not-green + // will appear very dark in the green channel! + // Tune by looking at mostly non-green content, + // then start at zero and increase until aliasing is a problem. + FxaaFloat fxaaQualityEdgeThresholdMin, + // + // Only used on FXAA Console. + // This used to be the FXAA_CONSOLE__EDGE_SHARPNESS define. + // It is here now to allow easier tuning. + // This does not effect PS3, as this needs to be compiled in. + // Use FXAA_CONSOLE__PS3_EDGE_SHARPNESS for PS3. + // Due to the PS3 being ALU bound, + // there are only three safe values here: 2 and 4 and 8. + // These options use the shaders ability to a free *|/ by 2|4|8. + // For all other platforms can be a non-power of two. + // 8.0 is sharper (default!!!) + // 4.0 is softer + // 2.0 is really soft (good only for vector graphics inputs) + FxaaFloat fxaaConsoleEdgeSharpness, + // + // Only used on FXAA Console. + // This used to be the FXAA_CONSOLE__EDGE_THRESHOLD define. + // It is here now to allow easier tuning. + // This does not effect PS3, as this needs to be compiled in. + // Use FXAA_CONSOLE__PS3_EDGE_THRESHOLD for PS3. + // Due to the PS3 being ALU bound, + // there are only two safe values here: 1/4 and 1/8. + // These options use the shaders ability to a free *|/ by 2|4|8. + // The console setting has a different mapping than the quality setting. + // Other platforms can use other values. + // 0.125 leaves less aliasing, but is softer (default!!!) + // 0.25 leaves more aliasing, and is sharper + FxaaFloat fxaaConsoleEdgeThreshold, + // + // Only used on FXAA Console. + // This used to be the FXAA_CONSOLE__EDGE_THRESHOLD_MIN define. + // It is here now to allow easier tuning. + // Trims the algorithm from processing darks. + // The console setting has a different mapping than the quality setting. + // This only applies when FXAA_EARLY_EXIT is 1. + // This does not apply to PS3, + // PS3 was simplified to avoid more shader instructions. + // 0.06 - faster but more aliasing in darks + // 0.05 - default + // 0.04 - slower and less aliasing in darks + // Special notes when using FXAA_GREEN_AS_LUMA, + // Likely want to set this to zero. + // As colors that are mostly not-green + // will appear very dark in the green channel! + // Tune by looking at mostly non-green content, + // then start at zero and increase until aliasing is a problem. + FxaaFloat fxaaConsoleEdgeThresholdMin, + // + // Extra constants for 360 FXAA Console only. + // Use zeros or anything else for other platforms. + // These must be in physical constant registers and NOT immedates. + // Immedates will result in compiler un-optimizing. + // {xyzw} = float4(1.0, -1.0, 0.25, -0.25) + FxaaFloat4 fxaaConsole360ConstDir +) { +/*--------------------------------------------------------------------------*/ + FxaaFloat2 posM; + posM.x = pos.x; + posM.y = pos.y; + #if (FXAA_GATHER4_ALPHA == 1) + #if (FXAA_DISCARD == 0) + FxaaFloat4 rgbyM = FxaaTexTop(tex, posM); + #if (FXAA_GREEN_AS_LUMA == 0) + #define lumaM rgbyM.w + #else + #define lumaM rgbyM.y + #endif + #endif + #if (FXAA_GREEN_AS_LUMA == 0) + FxaaFloat4 luma4A = FxaaTexAlpha4(tex, posM); + FxaaFloat4 luma4B = FxaaTexOffAlpha4(tex, posM, FxaaInt2(-1, -1)); + #else + FxaaFloat4 luma4A = FxaaTexGreen4(tex, posM); + FxaaFloat4 luma4B = FxaaTexOffGreen4(tex, posM, FxaaInt2(-1, -1)); + #endif + #if (FXAA_DISCARD == 1) + #define lumaM luma4A.w + #endif + #define lumaE luma4A.z + #define lumaS luma4A.x + #define lumaSE luma4A.y + #define lumaNW luma4B.w + #define lumaN luma4B.z + #define lumaW luma4B.x + #else + FxaaFloat4 rgbyM = FxaaTexTop(tex, posM); + #if (FXAA_GREEN_AS_LUMA == 0) + #define lumaM rgbyM.w + #else + #define lumaM rgbyM.y + #endif + FxaaFloat lumaS = FxaaLuma(FxaaTexOff(tex, posM, FxaaInt2(0, 1), fxaaQualityRcpFrame.xy)); + FxaaFloat lumaE = FxaaLuma(FxaaTexOff(tex, posM, FxaaInt2(1, 0), fxaaQualityRcpFrame.xy)); + FxaaFloat lumaN = FxaaLuma(FxaaTexOff(tex, posM, FxaaInt2(0, -1), fxaaQualityRcpFrame.xy)); + FxaaFloat lumaW = FxaaLuma(FxaaTexOff(tex, posM, FxaaInt2(-1, 0), fxaaQualityRcpFrame.xy)); + #endif +/*--------------------------------------------------------------------------*/ + FxaaFloat maxSM = max(lumaS, lumaM); + FxaaFloat minSM = min(lumaS, lumaM); + FxaaFloat maxESM = max(lumaE, maxSM); + FxaaFloat minESM = min(lumaE, minSM); + FxaaFloat maxWN = max(lumaN, lumaW); + FxaaFloat minWN = min(lumaN, lumaW); + FxaaFloat rangeMax = max(maxWN, maxESM); + FxaaFloat rangeMin = min(minWN, minESM); + FxaaFloat rangeMaxScaled = rangeMax * fxaaQualityEdgeThreshold; + FxaaFloat range = rangeMax - rangeMin; + FxaaFloat rangeMaxClamped = max(fxaaQualityEdgeThresholdMin, rangeMaxScaled); + FxaaBool earlyExit = range < rangeMaxClamped; +/*--------------------------------------------------------------------------*/ + if(earlyExit) + #if (FXAA_DISCARD == 1) + FxaaDiscard; + #else + return rgbyM; + #endif +/*--------------------------------------------------------------------------*/ + #if (FXAA_GATHER4_ALPHA == 0) + FxaaFloat lumaNW = FxaaLuma( + FxaaTexOff(tex, posM, FxaaInt2(-1, -1), fxaaQualityRcpFrame.xy)); + FxaaFloat lumaSE = FxaaLuma( + FxaaTexOff(tex, posM, FxaaInt2(1, 1), fxaaQualityRcpFrame.xy)); + FxaaFloat lumaNE = FxaaLuma( + FxaaTexOff(tex, posM, FxaaInt2(1, -1), fxaaQualityRcpFrame.xy)); + FxaaFloat lumaSW = FxaaLuma( + FxaaTexOff(tex, posM, FxaaInt2(-1, 1), fxaaQualityRcpFrame.xy)); + #else + FxaaFloat lumaNE = FxaaLuma( + FxaaTexOff(tex, posM, FxaaInt2(1, -1), fxaaQualityRcpFrame.xy)); + FxaaFloat lumaSW = FxaaLuma( + FxaaTexOff(tex, posM, FxaaInt2(-1, 1), fxaaQualityRcpFrame.xy)); + #endif +/*--------------------------------------------------------------------------*/ + FxaaFloat lumaNS = lumaN + lumaS; + FxaaFloat lumaWE = lumaW + lumaE; + FxaaFloat subpixRcpRange = 1.0 / range; + FxaaFloat subpixNSWE = lumaNS + lumaWE; + FxaaFloat edgeHorz1 = (-2.0 * lumaM) + lumaNS; + FxaaFloat edgeVert1 = (-2.0 * lumaM) + lumaWE; +/*--------------------------------------------------------------------------*/ + FxaaFloat lumaNESE = lumaNE + lumaSE; + FxaaFloat lumaNWNE = lumaNW + lumaNE; + FxaaFloat edgeHorz2 = (-2.0 * lumaE) + lumaNESE; + FxaaFloat edgeVert2 = (-2.0 * lumaN) + lumaNWNE; +/*--------------------------------------------------------------------------*/ + FxaaFloat lumaNWSW = lumaNW + lumaSW; + FxaaFloat lumaSWSE = lumaSW + lumaSE; + FxaaFloat edgeHorz4 = (abs(edgeHorz1) * 2.0) + abs(edgeHorz2); + FxaaFloat edgeVert4 = (abs(edgeVert1) * 2.0) + abs(edgeVert2); + FxaaFloat edgeHorz3 = (-2.0 * lumaW) + lumaNWSW; + FxaaFloat edgeVert3 = (-2.0 * lumaS) + lumaSWSE; + FxaaFloat edgeHorz = abs(edgeHorz3) + edgeHorz4; + FxaaFloat edgeVert = abs(edgeVert3) + edgeVert4; +/*--------------------------------------------------------------------------*/ + FxaaFloat subpixNWSWNESE = lumaNWSW + lumaNESE; + FxaaFloat lengthSign = fxaaQualityRcpFrame.x; + FxaaBool horzSpan = edgeHorz >= edgeVert; + FxaaFloat subpixA = subpixNSWE * 2.0 + subpixNWSWNESE; +/*--------------------------------------------------------------------------*/ + if (!horzSpan) lumaN = lumaW; + if (!horzSpan) lumaS = lumaE; + if (horzSpan) lengthSign = fxaaQualityRcpFrame.y; + FxaaFloat subpixB = (subpixA * (1.0 / 12.0)) - lumaM; +/*--------------------------------------------------------------------------*/ + FxaaFloat gradientN = lumaN - lumaM; + FxaaFloat gradientS = lumaS - lumaM; + FxaaFloat lumaNN = lumaN + lumaM; + FxaaFloat lumaSS = lumaS + lumaM; + FxaaBool pairN = abs(gradientN) >= abs(gradientS); + FxaaFloat gradient = max(abs(gradientN), abs(gradientS)); + if (pairN) lengthSign = -lengthSign; + FxaaFloat subpixC = FxaaSat(abs(subpixB) * subpixRcpRange); +/*--------------------------------------------------------------------------*/ + FxaaFloat2 posB; + posB.x = posM.x; + posB.y = posM.y; + FxaaFloat2 offNP; + offNP.x = (!horzSpan) ? 0.0 : fxaaQualityRcpFrame.x; + offNP.y = (horzSpan) ? 0.0 : fxaaQualityRcpFrame.y; + if (!horzSpan) posB.x += lengthSign * 0.5; + if (horzSpan) posB.y += lengthSign * 0.5; +/*--------------------------------------------------------------------------*/ + FxaaFloat2 posN; + posN.x = posB.x - offNP.x * FXAA_QUALITY__P0; + posN.y = posB.y - offNP.y * FXAA_QUALITY__P0; + FxaaFloat2 posP; + posP.x = posB.x + offNP.x * FXAA_QUALITY__P0; + posP.y = posB.y + offNP.y * FXAA_QUALITY__P0; + FxaaFloat subpixD = (-2.0 * subpixC) + 3.0; + FxaaFloat lumaEndN = FxaaLuma(FxaaTexTop(tex, posN)); + FxaaFloat subpixE = subpixC * subpixC; + FxaaFloat lumaEndP = FxaaLuma(FxaaTexTop(tex, posP)); +/*--------------------------------------------------------------------------*/ + if(!pairN) lumaNN = lumaSS; + FxaaFloat gradientScaled = gradient * 1.0 / 4.0; + FxaaFloat lumaMM = lumaM - lumaNN * 0.5; + FxaaFloat subpixF = subpixD * subpixE; + FxaaBool lumaMLTZero = lumaMM < 0.0; +/*--------------------------------------------------------------------------*/ + lumaEndN -= lumaNN * 0.5; + lumaEndP -= lumaNN * 0.5; + FxaaBool doneN = abs(lumaEndN) >= gradientScaled; + FxaaBool doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P1; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P1; + FxaaBool doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P1; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P1; +/*--------------------------------------------------------------------------*/ + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P2; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P2; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P2; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P2; +/*--------------------------------------------------------------------------*/ + #if (FXAA_QUALITY__PS > 3) + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P3; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P3; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P3; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P3; +/*--------------------------------------------------------------------------*/ + #if (FXAA_QUALITY__PS > 4) + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P4; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P4; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P4; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P4; +/*--------------------------------------------------------------------------*/ + #if (FXAA_QUALITY__PS > 5) + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P5; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P5; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P5; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P5; +/*--------------------------------------------------------------------------*/ + #if (FXAA_QUALITY__PS > 6) + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P6; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P6; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P6; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P6; +/*--------------------------------------------------------------------------*/ + #if (FXAA_QUALITY__PS > 7) + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P7; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P7; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P7; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P7; +/*--------------------------------------------------------------------------*/ + #if (FXAA_QUALITY__PS > 8) + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P8; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P8; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P8; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P8; +/*--------------------------------------------------------------------------*/ + #if (FXAA_QUALITY__PS > 9) + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P9; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P9; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P9; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P9; +/*--------------------------------------------------------------------------*/ + #if (FXAA_QUALITY__PS > 10) + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P10; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P10; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P10; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P10; +/*--------------------------------------------------------------------------*/ + #if (FXAA_QUALITY__PS > 11) + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P11; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P11; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P11; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P11; +/*--------------------------------------------------------------------------*/ + #if (FXAA_QUALITY__PS > 12) + if(doneNP) { + if(!doneN) lumaEndN = FxaaLuma(FxaaTexTop(tex, posN.xy)); + if(!doneP) lumaEndP = FxaaLuma(FxaaTexTop(tex, posP.xy)); + if(!doneN) lumaEndN = lumaEndN - lumaNN * 0.5; + if(!doneP) lumaEndP = lumaEndP - lumaNN * 0.5; + doneN = abs(lumaEndN) >= gradientScaled; + doneP = abs(lumaEndP) >= gradientScaled; + if(!doneN) posN.x -= offNP.x * FXAA_QUALITY__P12; + if(!doneN) posN.y -= offNP.y * FXAA_QUALITY__P12; + doneNP = (!doneN) || (!doneP); + if(!doneP) posP.x += offNP.x * FXAA_QUALITY__P12; + if(!doneP) posP.y += offNP.y * FXAA_QUALITY__P12; +/*--------------------------------------------------------------------------*/ + } + #endif +/*--------------------------------------------------------------------------*/ + } + #endif +/*--------------------------------------------------------------------------*/ + } + #endif +/*--------------------------------------------------------------------------*/ + } + #endif +/*--------------------------------------------------------------------------*/ + } + #endif +/*--------------------------------------------------------------------------*/ + } + #endif +/*--------------------------------------------------------------------------*/ + } + #endif +/*--------------------------------------------------------------------------*/ + } + #endif +/*--------------------------------------------------------------------------*/ + } + #endif +/*--------------------------------------------------------------------------*/ + } + #endif +/*--------------------------------------------------------------------------*/ + } +/*--------------------------------------------------------------------------*/ + FxaaFloat dstN = posM.x - posN.x; + FxaaFloat dstP = posP.x - posM.x; + if(!horzSpan) dstN = posM.y - posN.y; + if(!horzSpan) dstP = posP.y - posM.y; +/*--------------------------------------------------------------------------*/ + FxaaBool goodSpanN = (lumaEndN < 0.0) != lumaMLTZero; + FxaaFloat spanLength = (dstP + dstN); + FxaaBool goodSpanP = (lumaEndP < 0.0) != lumaMLTZero; + FxaaFloat spanLengthRcp = 1.0 / spanLength; +/*--------------------------------------------------------------------------*/ + FxaaBool directionN = dstN < dstP; + FxaaFloat dst = min(dstN, dstP); + FxaaBool goodSpan = directionN ? goodSpanN : goodSpanP; + FxaaFloat subpixG = subpixF * subpixF; + FxaaFloat pixelOffset = (dst * (-spanLengthRcp)) + 0.5; + FxaaFloat subpixH = subpixG * fxaaQualitySubpix; +/*--------------------------------------------------------------------------*/ + FxaaFloat pixelOffsetGood = goodSpan ? pixelOffset : 0.0; + FxaaFloat pixelOffsetSubpix = max(pixelOffsetGood, subpixH); + if(!horzSpan) posM.x += pixelOffsetSubpix * lengthSign; + if(horzSpan) posM.y += pixelOffsetSubpix * lengthSign; + #if (FXAA_DISCARD == 1) + return FxaaTexTop(tex, posM); + #else + return FxaaFloat4(FxaaTexTop(tex, posM).xyz, lumaM); + #endif +} +/*==========================================================================*/ +#endif diff --git a/RenderPipelineFile/rpplugins/fxaa/shader/fxaa_stage.frag.glsl b/RenderPipelineFile/rpplugins/fxaa/shader/fxaa_stage.frag.glsl index e2b764ee..5759dcd1 100644 --- a/RenderPipelineFile/rpplugins/fxaa/shader/fxaa_stage.frag.glsl +++ b/RenderPipelineFile/rpplugins/fxaa/shader/fxaa_stage.frag.glsl @@ -1,86 +1,86 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" - -#define FXAA_PC 1 -#define FXAA_GLSL_130 1 - -// Define quality level -#if ENUM_V_ACTIVE(fxaa, quality, low) - #define FXAA_QUALITY_PRESET 12 -#elif ENUM_V_ACTIVE(fxaa, quality, medium) - #define FXAA_QUALITY_PRESET 20 -#elif ENUM_V_ACTIVE(fxaa, quality, high) - #define FXAA_QUALITY_PRESET 29 -#elif ENUM_V_ACTIVE(fxaa, quality, ultra) - #define FXAA_QUALITY_PRESET 39 -#else - #error Unkown fxaa quality -#endif - -#pragma include "FXAA.inc.glsl" - -uniform sampler2D SourceTex; -out vec4 color; - -void main() { - - vec2 texcoord = get_texcoord(); - vec2 pixel_size = vec2(1.0) / SCREEN_SIZE; - - vec2 upper_left = texcoord + vec2(-0.5, 0.5) * pixel_size; - vec2 lower_right = texcoord + vec2(0.5, -0.5) * pixel_size; - - const float sharp = 0.5; // unused in the pc version - const float edge_threshold = GET_SETTING(fxaa, edge_threshold); - const float edge_min_threshold = 0.1 * GET_SETTING(fxaa, min_threshold); - const float edge_sharpness = 8.0; // power of two, unused in the pc version - vec4 size_twice = vec4(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_WIDTH, WINDOW_HEIGHT); - - vec4 result = FxaaPixelShader( - texcoord, // pos - vec4(upper_left, lower_right), // fxaaConsolePosPos - SourceTex, // tex - SourceTex, // fxaaConsole360TexExpBiasNegOne - SourceTex, // fxaaConsole360TexExpBiasNegTwo - vec2(1.0) / SCREEN_SIZE, // fxaaQualityRcpFrame - vec4(-sharp, -sharp, sharp, sharp) / size_twice, // fxaaConsoleRcpFrameOpt - vec4(-2, -2, 2, 2) / size_twice, // fxaaConsoleRcpFrameOpt2 - vec4(8, 8, -4, -4) / size_twice, // fxaaConsole360RcpFrameOpt2 - GET_SETTING(fxaa, subpixel_quality), // fxaaQualitySubpix - edge_threshold, // fxaaQualityEdgeThreshold - edge_min_threshold, // fxaaQualityEdgeThresholdMin - edge_sharpness, // fxaaConsoleEdgeSharpness - edge_threshold, // fxaaConsoleEdgeThreshold - edge_min_threshold, // fxaaConsoleEdgeThresholdMin - vec4(1.0, -1.0, 0.25, -0.25) // fxaaConsole360ConstDir - ); - - color = result; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" + +#define FXAA_PC 1 +#define FXAA_GLSL_130 1 + +// Define quality level +#if ENUM_V_ACTIVE(fxaa, quality, low) + #define FXAA_QUALITY_PRESET 12 +#elif ENUM_V_ACTIVE(fxaa, quality, medium) + #define FXAA_QUALITY_PRESET 20 +#elif ENUM_V_ACTIVE(fxaa, quality, high) + #define FXAA_QUALITY_PRESET 29 +#elif ENUM_V_ACTIVE(fxaa, quality, ultra) + #define FXAA_QUALITY_PRESET 39 +#else + #error Unkown fxaa quality +#endif + +#pragma include "FXAA.inc.glsl" + +uniform sampler2D SourceTex; +out vec4 color; + +void main() { + + vec2 texcoord = get_texcoord(); + vec2 pixel_size = vec2(1.0) / SCREEN_SIZE; + + vec2 upper_left = texcoord + vec2(-0.5, 0.5) * pixel_size; + vec2 lower_right = texcoord + vec2(0.5, -0.5) * pixel_size; + + const float sharp = 0.5; // unused in the pc version + const float edge_threshold = GET_SETTING(fxaa, edge_threshold); + const float edge_min_threshold = 0.1 * GET_SETTING(fxaa, min_threshold); + const float edge_sharpness = 8.0; // power of two, unused in the pc version + vec4 size_twice = vec4(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_WIDTH, WINDOW_HEIGHT); + + vec4 result = FxaaPixelShader( + texcoord, // pos + vec4(upper_left, lower_right), // fxaaConsolePosPos + SourceTex, // tex + SourceTex, // fxaaConsole360TexExpBiasNegOne + SourceTex, // fxaaConsole360TexExpBiasNegTwo + vec2(1.0) / SCREEN_SIZE, // fxaaQualityRcpFrame + vec4(-sharp, -sharp, sharp, sharp) / size_twice, // fxaaConsoleRcpFrameOpt + vec4(-2, -2, 2, 2) / size_twice, // fxaaConsoleRcpFrameOpt2 + vec4(8, 8, -4, -4) / size_twice, // fxaaConsole360RcpFrameOpt2 + GET_SETTING(fxaa, subpixel_quality), // fxaaQualitySubpix + edge_threshold, // fxaaQualityEdgeThreshold + edge_min_threshold, // fxaaQualityEdgeThresholdMin + edge_sharpness, // fxaaConsoleEdgeSharpness + edge_threshold, // fxaaConsoleEdgeThreshold + edge_min_threshold, // fxaaConsoleEdgeThresholdMin + vec4(1.0, -1.0, 0.25, -0.25) // fxaaConsole360ConstDir + ); + + color = result; +} diff --git a/RenderPipelineFile/rpplugins/fxaa/shader/write_luma.frag.glsl b/RenderPipelineFile/rpplugins/fxaa/shader/write_luma.frag.glsl index 21f4dfdf..aae4da37 100644 --- a/RenderPipelineFile/rpplugins/fxaa/shader/write_luma.frag.glsl +++ b/RenderPipelineFile/rpplugins/fxaa/shader/write_luma.frag.glsl @@ -1,37 +1,37 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -uniform sampler2D ShadedScene; -out vec4 color; - -void main() { - vec2 texcoord = get_texcoord(); - color.xyz = textureLod(ShadedScene, texcoord, 0).xyz; - color.w = dot(color.xyz, vec3(0.299, 0.587, 0.114)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +uniform sampler2D ShadedScene; +out vec4 color; + +void main() { + vec2 texcoord = get_texcoord(); + color.xyz = textureLod(ShadedScene, texcoord, 0).xyz; + color.w = dot(color.xyz, vec3(0.299, 0.587, 0.114)); +} diff --git a/RenderPipelineFile/rpplugins/motion_blur/config.yaml b/RenderPipelineFile/rpplugins/motion_blur/config.yaml index a722921b..03565ccc 100644 --- a/RenderPipelineFile/rpplugins/motion_blur/config.yaml +++ b/RenderPipelineFile/rpplugins/motion_blur/config.yaml @@ -1,80 +1,80 @@ - -settings: !!omap - - - num_camera_samples: - type: int - range: [2, 16] - default: 6 - label: Camera Blur sample count - shader_runtime: true - description: > - Controls the amount of samples for camera motion blur, more samples - mean a smoother blur but also require more performance. - - - camera_blur_factor: - type: float - range: [0.0, 2.0] - default: 0.4 - label: Camera Blur factor - shader_runtime: true - description: > - Controls the camera blur factor, a factor of 1 means the full blur - is applied, a factor of 0 means no blur at all. You should adjust - this so the blur is not too strong, otherwise motion sickness can happen. - - - enable_object_blur: - type: bool - default: false - label: Enable object blur - description: > - Whether to enable per object blur, this only works when the per - object velocity vector patch was applied. Otherwise, this just - costs more performance. - - - blur_factor: - display_if: {enable_object_blur: true} - type: float - range: [0.0, 1.0] - default: 0.5 - label: Object Blur factor - shader_runtime: true - description: > - Controls the blur factor of moving objects, a factor of 1 means the - full blur is applied, a factor of 0 means no blur at all. - - - tile_size: - display_if: {enable_object_blur: true} - type: int - range: [8, 64] - default: 32 - label: Object Blur Tile Size - description: > - Size of the blur tiles, this also has influence on the maximum - blur radius. Setting this too high will cause artifacts when objects - with different motion vectors overlap. - - - - max_blur_radius: - display_if: {enable_object_blur: true} - type: float - range: [1.0, 64.0] - default: 10.0 - label: Object Blur limit - shader_runtime: true - description: > - Controls the maximum possible blur size. Try to set this as low as - possible without getting artifacts. - - - num_samples: - display_if: {enable_object_blur: true} - type: int - range: [2, 64] - default: 12 - label: Object Blur sample count - shader_runtime: true - description: > - Controls the amount of samples, more samples mean a smoother blur - but also require more performance. - - -daytime_settings: + +settings: !!omap + + - num_camera_samples: + type: int + range: [2, 16] + default: 6 + label: Camera Blur sample count + shader_runtime: true + description: > + Controls the amount of samples for camera motion blur, more samples + mean a smoother blur but also require more performance. + + - camera_blur_factor: + type: float + range: [0.0, 2.0] + default: 0.4 + label: Camera Blur factor + shader_runtime: true + description: > + Controls the camera blur factor, a factor of 1 means the full blur + is applied, a factor of 0 means no blur at all. You should adjust + this so the blur is not too strong, otherwise motion sickness can happen. + + - enable_object_blur: + type: bool + default: false + label: Enable object blur + description: > + Whether to enable per object blur, this only works when the per + object velocity vector patch was applied. Otherwise, this just + costs more performance. + + - blur_factor: + display_if: {enable_object_blur: true} + type: float + range: [0.0, 1.0] + default: 0.5 + label: Object Blur factor + shader_runtime: true + description: > + Controls the blur factor of moving objects, a factor of 1 means the + full blur is applied, a factor of 0 means no blur at all. + + - tile_size: + display_if: {enable_object_blur: true} + type: int + range: [8, 64] + default: 32 + label: Object Blur Tile Size + description: > + Size of the blur tiles, this also has influence on the maximum + blur radius. Setting this too high will cause artifacts when objects + with different motion vectors overlap. + + + - max_blur_radius: + display_if: {enable_object_blur: true} + type: float + range: [1.0, 64.0] + default: 10.0 + label: Object Blur limit + shader_runtime: true + description: > + Controls the maximum possible blur size. Try to set this as low as + possible without getting artifacts. + + - num_samples: + display_if: {enable_object_blur: true} + type: int + range: [2, 64] + default: 12 + label: Object Blur sample count + shader_runtime: true + description: > + Controls the amount of samples, more samples mean a smoother blur + but also require more performance. + + +daytime_settings: diff --git a/RenderPipelineFile/rpplugins/motion_blur/motion_blur_stage.py b/RenderPipelineFile/rpplugins/motion_blur/motion_blur_stage.py index b8fcb1f1..1476a150 100644 --- a/RenderPipelineFile/rpplugins/motion_blur/motion_blur_stage.py +++ b/RenderPipelineFile/rpplugins/motion_blur/motion_blur_stage.py @@ -1,97 +1,97 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import SamplerState - -from rpcore.render_stage import RenderStage - - -class MotionBlurStage(RenderStage): - - """ This stage applies the motion blur, using the screen space velocity vectors """ - - required_inputs = [] - required_pipes = ["ShadedScene", "GBuffer", "DownscaledDepth", "CombinedVelocity"] - - @property - def produced_pipes(self): - return {"ShadedScene": self.target_cam_blur.color_tex} - - def create(self): - - if self.per_object_blur: - self.tile_target = self.create_target("FetchVertDominantVelocity") - self.tile_target.size = -1, -self.tile_size - self.tile_target.add_color_attachment(bits=(16, 16, 0)) - self.tile_target.prepare_buffer() - - self.tile_target_horiz = self.create_target("FetchHorizDominantVelocity") - self.tile_target_horiz.size = -self.tile_size - self.tile_target_horiz.add_color_attachment(bits=(16, 16, 0)) - self.tile_target_horiz.prepare_buffer() - self.tile_target_horiz.set_shader_input("SourceTex", self.tile_target.color_tex) - - self.minmax_target = self.create_target("NeighborMinMax") - self.minmax_target.size = -self.tile_size - self.minmax_target.add_color_attachment(bits=(16, 16, 0)) - self.minmax_target.prepare_buffer() - self.minmax_target.set_shader_input("TileMinMax", self.tile_target_horiz.color_tex) - - self.pack_target = self.create_target("PackBlurData") - self.pack_target.add_color_attachment(bits=(16, 16, 0)) - self.pack_target.prepare_buffer() - - self.target = self.create_target("ObjectMotionBlur") - self.target.add_color_attachment(bits=16) - self.target.prepare_buffer() - self.target.set_shader_inputs( - NeighborMinMax=self.minmax_target.color_tex, - PackedSceneData=self.pack_target.color_tex) - - self.target.color_tex.set_wrap_u(SamplerState.WM_clamp) - self.target.color_tex.set_wrap_v(SamplerState.WM_clamp) - - self.target_cam_blur = self.create_target("CameraMotionBlur") - self.target_cam_blur.add_color_attachment(bits=16) - self.target_cam_blur.prepare_buffer() - - if self.per_object_blur: - self.target_cam_blur.set_shader_input("SourceTex", self.target.color_tex) - - def reload_shaders(self): - if self.per_object_blur: - self.tile_target.shader = self.load_plugin_shader( - "fetch_dominant_velocity.frag.glsl") - self.tile_target_horiz.shader = self.load_plugin_shader( - "fetch_dominant_velocity_horiz.frag.glsl") - self.minmax_target.shader = self.load_plugin_shader( - "neighbor_minmax.frag.glsl") - self.pack_target.shader = self.load_plugin_shader( - "pack_blur_data.frag.glsl") - self.target.shader = self.load_plugin_shader( - "apply_motion_blur.frag.glsl") - self.target_cam_blur.shader = self.load_plugin_shader( - "camera_motion_blur.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import SamplerState + +from rpcore.render_stage import RenderStage + + +class MotionBlurStage(RenderStage): + + """ This stage applies the motion blur, using the screen space velocity vectors """ + + required_inputs = [] + required_pipes = ["ShadedScene", "GBuffer", "DownscaledDepth", "CombinedVelocity"] + + @property + def produced_pipes(self): + return {"ShadedScene": self.target_cam_blur.color_tex} + + def create(self): + + if self.per_object_blur: + self.tile_target = self.create_target("FetchVertDominantVelocity") + self.tile_target.size = -1, -self.tile_size + self.tile_target.add_color_attachment(bits=(16, 16, 0)) + self.tile_target.prepare_buffer() + + self.tile_target_horiz = self.create_target("FetchHorizDominantVelocity") + self.tile_target_horiz.size = -self.tile_size + self.tile_target_horiz.add_color_attachment(bits=(16, 16, 0)) + self.tile_target_horiz.prepare_buffer() + self.tile_target_horiz.set_shader_input("SourceTex", self.tile_target.color_tex) + + self.minmax_target = self.create_target("NeighborMinMax") + self.minmax_target.size = -self.tile_size + self.minmax_target.add_color_attachment(bits=(16, 16, 0)) + self.minmax_target.prepare_buffer() + self.minmax_target.set_shader_input("TileMinMax", self.tile_target_horiz.color_tex) + + self.pack_target = self.create_target("PackBlurData") + self.pack_target.add_color_attachment(bits=(16, 16, 0)) + self.pack_target.prepare_buffer() + + self.target = self.create_target("ObjectMotionBlur") + self.target.add_color_attachment(bits=16) + self.target.prepare_buffer() + self.target.set_shader_inputs( + NeighborMinMax=self.minmax_target.color_tex, + PackedSceneData=self.pack_target.color_tex) + + self.target.color_tex.set_wrap_u(SamplerState.WM_clamp) + self.target.color_tex.set_wrap_v(SamplerState.WM_clamp) + + self.target_cam_blur = self.create_target("CameraMotionBlur") + self.target_cam_blur.add_color_attachment(bits=16) + self.target_cam_blur.prepare_buffer() + + if self.per_object_blur: + self.target_cam_blur.set_shader_input("SourceTex", self.target.color_tex) + + def reload_shaders(self): + if self.per_object_blur: + self.tile_target.shader = self.load_plugin_shader( + "fetch_dominant_velocity.frag.glsl") + self.tile_target_horiz.shader = self.load_plugin_shader( + "fetch_dominant_velocity_horiz.frag.glsl") + self.minmax_target.shader = self.load_plugin_shader( + "neighbor_minmax.frag.glsl") + self.pack_target.shader = self.load_plugin_shader( + "pack_blur_data.frag.glsl") + self.target.shader = self.load_plugin_shader( + "apply_motion_blur.frag.glsl") + self.target_cam_blur.shader = self.load_plugin_shader( + "camera_motion_blur.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/motion_blur/plugin.py b/RenderPipelineFile/rpplugins/motion_blur/plugin.py index 551eb68a..214c9ef3 100644 --- a/RenderPipelineFile/rpplugins/motion_blur/plugin.py +++ b/RenderPipelineFile/rpplugins/motion_blur/plugin.py @@ -1,43 +1,43 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.pluginbase.base_plugin import BasePlugin - -from .motion_blur_stage import MotionBlurStage - - -class Plugin(BasePlugin): - - name = "Motion blur" - author = "tobspr " - description = ("This plugin adds support for motion blur. This includes " - "per-object motion blur aswell as camera-motion blur.") - version = "1.0" - - def on_stage_setup(self): - self.stage = self.create_stage(MotionBlurStage) - self.stage.tile_size = self.get_setting("tile_size") - self.stage.per_object_blur = self.get_setting("enable_object_blur") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.pluginbase.base_plugin import BasePlugin + +from .motion_blur_stage import MotionBlurStage + + +class Plugin(BasePlugin): + + name = "Motion blur" + author = "tobspr " + description = ("This plugin adds support for motion blur. This includes " + "per-object motion blur aswell as camera-motion blur.") + version = "1.0" + + def on_stage_setup(self): + self.stage = self.create_stage(MotionBlurStage) + self.stage.tile_size = self.get_setting("tile_size") + self.stage.per_object_blur = self.get_setting("enable_object_blur") diff --git a/RenderPipelineFile/rpplugins/motion_blur/shader/apply_motion_blur.frag.glsl b/RenderPipelineFile/rpplugins/motion_blur/shader/apply_motion_blur.frag.glsl index 46514e65..4f17b720 100644 --- a/RenderPipelineFile/rpplugins/motion_blur/shader/apply_motion_blur.frag.glsl +++ b/RenderPipelineFile/rpplugins/motion_blur/shader/apply_motion_blur.frag.glsl @@ -1,93 +1,93 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "includes/noise.inc.glsl" - -#pragma include "motion_blur.inc.glsl" - -uniform sampler2D ShadedScene; -uniform sampler2D PackedSceneData; -uniform sampler2D NeighborMinMax; - -out vec3 result; - -const int num_samples = GET_SETTING(motion_blur, num_samples); - - -void main() { - - vec2 texcoord = get_texcoord(); - ivec2 coord = ivec2(gl_FragCoord.xy); - ivec2 tile = coord / tile_size; - vec3 center_color = texelFetch(ShadedScene, coord, 0).xyz; - - vec2 tile_velocity = texelFetch(NeighborMinMax, tile, 0).xy; // max_v - const float weight_step = 1.0 / float(num_samples - 1); - - // Early out - if (length(tile_velocity) < 0.5 / WINDOW_WIDTH) { - result = max(vec3(0.0), center_color); - return; - } - - // Get current pixel offset - vec2 vx = texelFetch(PackedSceneData, coord, 0).xy; - vec2 blur_step = tile_velocity * weight_step * 2.0; - vec2 start_tc = texcoord - tile_velocity; - - float min_len_xy = -length(tile_velocity.xy); - float len_xy_step = (-min_len_xy) * weight_step * 2.0; - - float initial_weight = saturate(float(num_samples / 40.0)); - initial_weight *= 1.0 / (max(1.0, vx.x)); - - float jitter = rand(vec2(coord)); - - float weight_accum = initial_weight; - vec3 accum = center_color * initial_weight; - - for (int i = 0; i < num_samples; ++i) { - - vec2 tc = start_tc + blur_step * (i + jitter); - vec2 vy = textureLod(PackedSceneData, tc, 0).xy; - float len_xy = abs(min_len_xy + len_xy_step * (i + jitter)); - - vec2 cmp_softz = soft_depth_cmp(vec2(vx.y, vy.y), vec2(vy.y, vx.y)); - vec4 cmp_batch = batch_cmp(len_xy, max(vec2(1e-6), vec2(vy.x, vx.x))); - float w = dot(cmp_softz, cmp_batch.xy) + (cmp_batch.z * cmp_batch.w) * 2.0; - - accum += saturate(textureLod(ShadedScene, tc, 0).xyz) * w; - weight_accum += w; - - } - - result = max(vec3(0.0), accum / max(0.001, weight_accum)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "includes/noise.inc.glsl" + +#pragma include "motion_blur.inc.glsl" + +uniform sampler2D ShadedScene; +uniform sampler2D PackedSceneData; +uniform sampler2D NeighborMinMax; + +out vec3 result; + +const int num_samples = GET_SETTING(motion_blur, num_samples); + + +void main() { + + vec2 texcoord = get_texcoord(); + ivec2 coord = ivec2(gl_FragCoord.xy); + ivec2 tile = coord / tile_size; + vec3 center_color = texelFetch(ShadedScene, coord, 0).xyz; + + vec2 tile_velocity = texelFetch(NeighborMinMax, tile, 0).xy; // max_v + const float weight_step = 1.0 / float(num_samples - 1); + + // Early out + if (length(tile_velocity) < 0.5 / WINDOW_WIDTH) { + result = max(vec3(0.0), center_color); + return; + } + + // Get current pixel offset + vec2 vx = texelFetch(PackedSceneData, coord, 0).xy; + vec2 blur_step = tile_velocity * weight_step * 2.0; + vec2 start_tc = texcoord - tile_velocity; + + float min_len_xy = -length(tile_velocity.xy); + float len_xy_step = (-min_len_xy) * weight_step * 2.0; + + float initial_weight = saturate(float(num_samples / 40.0)); + initial_weight *= 1.0 / (max(1.0, vx.x)); + + float jitter = rand(vec2(coord)); + + float weight_accum = initial_weight; + vec3 accum = center_color * initial_weight; + + for (int i = 0; i < num_samples; ++i) { + + vec2 tc = start_tc + blur_step * (i + jitter); + vec2 vy = textureLod(PackedSceneData, tc, 0).xy; + float len_xy = abs(min_len_xy + len_xy_step * (i + jitter)); + + vec2 cmp_softz = soft_depth_cmp(vec2(vx.y, vy.y), vec2(vy.y, vx.y)); + vec4 cmp_batch = batch_cmp(len_xy, max(vec2(1e-6), vec2(vy.x, vx.x))); + float w = dot(cmp_softz, cmp_batch.xy) + (cmp_batch.z * cmp_batch.w) * 2.0; + + accum += saturate(textureLod(ShadedScene, tc, 0).xyz) * w; + weight_accum += w; + + } + + result = max(vec3(0.0), accum / max(0.001, weight_accum)); +} diff --git a/RenderPipelineFile/rpplugins/motion_blur/shader/camera_motion_blur.frag.glsl b/RenderPipelineFile/rpplugins/motion_blur/shader/camera_motion_blur.frag.glsl index 23ace798..395bcabd 100644 --- a/RenderPipelineFile/rpplugins/motion_blur/shader/camera_motion_blur.frag.glsl +++ b/RenderPipelineFile/rpplugins/motion_blur/shader/camera_motion_blur.frag.glsl @@ -1,111 +1,111 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "includes/noise.inc.glsl" - -#pragma optionNV (unroll all) - -#if GET_SETTING(motion_blur, enable_object_blur) -uniform sampler2D SourceTex; -#define SOURCE SourceTex -#else -uniform sampler2D ShadedScene; -#define SOURCE ShadedScene -#endif - - -uniform sampler2D CombinedVelocity; -out vec3 result; - -const int num_samples = GET_SETTING(motion_blur, num_camera_samples) * 2; -float max_velocity = 70.0 / WINDOW_WIDTH; -float min_velocity = 0.5 / WINDOW_WIDTH; - -void main() { - - vec2 texcoord = get_texcoord(); - ivec2 coord = ivec2(gl_FragCoord.xy); - - #if DEBUG_MODE - result = textureLod(SOURCE, texcoord, 0).xyz; - return; - #endif - - // Reconstruct last frame texcoord - vec2 film_offset_bias = MainSceneData.current_film_offset * vec2(1.0, 1.0 / ASPECT_RATIO); - vec3 pos = get_world_pos_at(texcoord - film_offset_bias); - vec4 last_proj = MainSceneData.last_view_proj_mat_no_jitter * vec4(pos, 1); - vec2 last_coord = fma(last_proj.xy / last_proj.w, vec2(0.5), vec2(0.5)); - - // Compute velocity in screen space - vec2 velocity = last_coord - texcoord; - - // Make sure that when we have low-fps, we reduce motion blur, and when we - // have higher fps, we increase it - this way it perceptually always stays - // the same (otherwise it feels really laggy at low FPS) - const float target_fps = 60.0; - velocity *= (1.0 / target_fps) / MainSceneData.frame_delta; - velocity *= GET_SETTING(motion_blur, camera_blur_factor); - - float velocity_len = length(velocity); - - // We can abort early when no velocity is present - if (velocity_len < min_velocity) { - result = textureLod(SOURCE, texcoord, 0).xyz; - return; - } - - if (velocity_len > max_velocity) { - float scale_factor = max_velocity / velocity_len; - velocity *= scale_factor; - velocity_len *= scale_factor; - } - - // Weight the center sample by a small bit to make sure we always have a weight. - // However, we don't weight it too much to make the blur not look weird. - float weights = 1e-3; - vec3 accum = textureLod(SOURCE, texcoord, 0).xyz * weights; - float jitter = rand(texcoord); - - // Blur in both directions - for (int i = -num_samples + 1; i < num_samples; ++i) { - vec2 offs = (i + 0.5 * jitter) / float(num_samples) * velocity; - - // Prevent bleeding when rotating - that is, objects moving into different directions - vec2 sample_velocity = textureLod(CombinedVelocity, texcoord + offs, 0).xy; - float weight = saturate(dot(sample_velocity, velocity) * WINDOW_WIDTH * 3); - accum += textureLod(SOURCE, texcoord + offs, 0).xyz * weight; - weights += weight; - } - - accum /= weights; - result = max(vec3(0.0), accum); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "includes/noise.inc.glsl" + +#pragma optionNV (unroll all) + +#if GET_SETTING(motion_blur, enable_object_blur) +uniform sampler2D SourceTex; +#define SOURCE SourceTex +#else +uniform sampler2D ShadedScene; +#define SOURCE ShadedScene +#endif + + +uniform sampler2D CombinedVelocity; +out vec3 result; + +const int num_samples = GET_SETTING(motion_blur, num_camera_samples) * 2; +float max_velocity = 70.0 / WINDOW_WIDTH; +float min_velocity = 0.5 / WINDOW_WIDTH; + +void main() { + + vec2 texcoord = get_texcoord(); + ivec2 coord = ivec2(gl_FragCoord.xy); + + #if DEBUG_MODE + result = textureLod(SOURCE, texcoord, 0).xyz; + return; + #endif + + // Reconstruct last frame texcoord + vec2 film_offset_bias = MainSceneData.current_film_offset * vec2(1.0, 1.0 / ASPECT_RATIO); + vec3 pos = get_world_pos_at(texcoord - film_offset_bias); + vec4 last_proj = MainSceneData.last_view_proj_mat_no_jitter * vec4(pos, 1); + vec2 last_coord = fma(last_proj.xy / last_proj.w, vec2(0.5), vec2(0.5)); + + // Compute velocity in screen space + vec2 velocity = last_coord - texcoord; + + // Make sure that when we have low-fps, we reduce motion blur, and when we + // have higher fps, we increase it - this way it perceptually always stays + // the same (otherwise it feels really laggy at low FPS) + const float target_fps = 60.0; + velocity *= (1.0 / target_fps) / MainSceneData.frame_delta; + velocity *= GET_SETTING(motion_blur, camera_blur_factor); + + float velocity_len = length(velocity); + + // We can abort early when no velocity is present + if (velocity_len < min_velocity) { + result = textureLod(SOURCE, texcoord, 0).xyz; + return; + } + + if (velocity_len > max_velocity) { + float scale_factor = max_velocity / velocity_len; + velocity *= scale_factor; + velocity_len *= scale_factor; + } + + // Weight the center sample by a small bit to make sure we always have a weight. + // However, we don't weight it too much to make the blur not look weird. + float weights = 1e-3; + vec3 accum = textureLod(SOURCE, texcoord, 0).xyz * weights; + float jitter = rand(texcoord); + + // Blur in both directions + for (int i = -num_samples + 1; i < num_samples; ++i) { + vec2 offs = (i + 0.5 * jitter) / float(num_samples) * velocity; + + // Prevent bleeding when rotating - that is, objects moving into different directions + vec2 sample_velocity = textureLod(CombinedVelocity, texcoord + offs, 0).xy; + float weight = saturate(dot(sample_velocity, velocity) * WINDOW_WIDTH * 3); + accum += textureLod(SOURCE, texcoord + offs, 0).xyz * weight; + weights += weight; + } + + accum /= weights; + result = max(vec3(0.0), accum); +} diff --git a/RenderPipelineFile/rpplugins/motion_blur/shader/fetch_dominant_velocity.frag.glsl b/RenderPipelineFile/rpplugins/motion_blur/shader/fetch_dominant_velocity.frag.glsl index c3221421..8ac278b7 100644 --- a/RenderPipelineFile/rpplugins/motion_blur/shader/fetch_dominant_velocity.frag.glsl +++ b/RenderPipelineFile/rpplugins/motion_blur/shader/fetch_dominant_velocity.frag.glsl @@ -1,58 +1,58 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma optionNV (unroll all) - -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "motion_blur.inc.glsl" - -out vec2 result; - -void main() { - ivec2 coord = ivec2(gl_FragCoord.xy); - ivec2 screen_coord = ivec2(coord.x, coord.y * tile_size); - - vec2 max_velocity = vec2(0); - float max_velocity_len_sq = 0.0; - - // Find the longest vector in the tile - for (int y = 0; y <= tile_size; y += 4) { - vec2 velocity = get_gbuffer_object_velocity(GBuffer, ivec2(coord.x, screen_coord.y + y)); - float len_sq = dot(velocity, velocity); - - // Check if the vector is longer than the current longest vector - if (len_sq > max_velocity_len_sq) { - max_velocity_len_sq = len_sq; - max_velocity = velocity; - } - } - - result = adjust_velocity(max_velocity); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma optionNV (unroll all) + +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "motion_blur.inc.glsl" + +out vec2 result; + +void main() { + ivec2 coord = ivec2(gl_FragCoord.xy); + ivec2 screen_coord = ivec2(coord.x, coord.y * tile_size); + + vec2 max_velocity = vec2(0); + float max_velocity_len_sq = 0.0; + + // Find the longest vector in the tile + for (int y = 0; y <= tile_size; y += 4) { + vec2 velocity = get_gbuffer_object_velocity(GBuffer, ivec2(coord.x, screen_coord.y + y)); + float len_sq = dot(velocity, velocity); + + // Check if the vector is longer than the current longest vector + if (len_sq > max_velocity_len_sq) { + max_velocity_len_sq = len_sq; + max_velocity = velocity; + } + } + + result = adjust_velocity(max_velocity); +} diff --git a/RenderPipelineFile/rpplugins/motion_blur/shader/fetch_dominant_velocity_horiz.frag.glsl b/RenderPipelineFile/rpplugins/motion_blur/shader/fetch_dominant_velocity_horiz.frag.glsl index cab721a2..db70c078 100644 --- a/RenderPipelineFile/rpplugins/motion_blur/shader/fetch_dominant_velocity_horiz.frag.glsl +++ b/RenderPipelineFile/rpplugins/motion_blur/shader/fetch_dominant_velocity_horiz.frag.glsl @@ -1,60 +1,60 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma optionNV (unroll all) - -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "motion_blur.inc.glsl" - -uniform sampler2D SourceTex; -out vec2 result; - -void main() { - ivec2 coord = ivec2(gl_FragCoord.xy); - ivec2 source_coord = ivec2(coord.x * tile_size, coord.y); - - vec2 max_velocity = vec2(0); - float max_velocity_len_sq = 0.0; - - // Find the longest vector in the tile - for (int x = 0; x <= tile_size; x += 4) { - int x_coord = clamp(source_coord.x + x, 0, WINDOW_WIDTH - 1); - vec2 velocity = texelFetch(SourceTex, ivec2(x_coord, source_coord.y), 0).xy; - float len_sq = dot(velocity, velocity); - - // Check if the vector is longer than the current longest vector - if (len_sq > max_velocity_len_sq) { - max_velocity_len_sq = len_sq; - max_velocity = velocity; - } - } - - result = max_velocity; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma optionNV (unroll all) + +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "motion_blur.inc.glsl" + +uniform sampler2D SourceTex; +out vec2 result; + +void main() { + ivec2 coord = ivec2(gl_FragCoord.xy); + ivec2 source_coord = ivec2(coord.x * tile_size, coord.y); + + vec2 max_velocity = vec2(0); + float max_velocity_len_sq = 0.0; + + // Find the longest vector in the tile + for (int x = 0; x <= tile_size; x += 4) { + int x_coord = clamp(source_coord.x + x, 0, WINDOW_WIDTH - 1); + vec2 velocity = texelFetch(SourceTex, ivec2(x_coord, source_coord.y), 0).xy; + float len_sq = dot(velocity, velocity); + + // Check if the vector is longer than the current longest vector + if (len_sq > max_velocity_len_sq) { + max_velocity_len_sq = len_sq; + max_velocity = velocity; + } + } + + result = max_velocity; +} diff --git a/RenderPipelineFile/rpplugins/motion_blur/shader/motion_blur.inc.glsl b/RenderPipelineFile/rpplugins/motion_blur/shader/motion_blur.inc.glsl index 6f47d12f..3ebadd2a 100644 --- a/RenderPipelineFile/rpplugins/motion_blur/shader/motion_blur.inc.glsl +++ b/RenderPipelineFile/rpplugins/motion_blur/shader/motion_blur.inc.glsl @@ -1,55 +1,55 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -const int tile_size = GET_SETTING(motion_blur, tile_size); -float blur_factor = GET_SETTING(motion_blur, blur_factor) * 0.5; -float max_velocity_len = GET_SETTING(motion_blur, max_blur_radius) * - tile_size / WINDOW_WIDTH * 0.2; -const vec2 soft_depth_factor = vec2(10.0); - -vec2 adjust_velocity(vec2 velocity) { - velocity *= blur_factor; - - // Make sure the velocity does not exceed the maximum length - float vel_len = length(velocity); - if (vel_len > max_velocity_len) { - velocity *= max_velocity_len / vel_len; - } - return velocity; -} - - -vec2 soft_depth_cmp(vec2 z0, vec2 z1) -{ - return saturate(fma(z0, soft_depth_factor, vec2(1.0)) - z1 * soft_depth_factor); -} - -vec4 batch_cmp(float len_xy_sq, vec2 velocities) -{ - return saturate(vec4((1 - len_xy_sq / (velocities.xyxy)) + vec4(0.0, 0.0, 0.95, 0.95))); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +const int tile_size = GET_SETTING(motion_blur, tile_size); +float blur_factor = GET_SETTING(motion_blur, blur_factor) * 0.5; +float max_velocity_len = GET_SETTING(motion_blur, max_blur_radius) * + tile_size / WINDOW_WIDTH * 0.2; +const vec2 soft_depth_factor = vec2(10.0); + +vec2 adjust_velocity(vec2 velocity) { + velocity *= blur_factor; + + // Make sure the velocity does not exceed the maximum length + float vel_len = length(velocity); + if (vel_len > max_velocity_len) { + velocity *= max_velocity_len / vel_len; + } + return velocity; +} + + +vec2 soft_depth_cmp(vec2 z0, vec2 z1) +{ + return saturate(fma(z0, soft_depth_factor, vec2(1.0)) - z1 * soft_depth_factor); +} + +vec4 batch_cmp(float len_xy_sq, vec2 velocities) +{ + return saturate(vec4((1 - len_xy_sq / (velocities.xyxy)) + vec4(0.0, 0.0, 0.95, 0.95))); +} diff --git a/RenderPipelineFile/rpplugins/motion_blur/shader/neighbor_minmax.frag.glsl b/RenderPipelineFile/rpplugins/motion_blur/shader/neighbor_minmax.frag.glsl index 59ae2a0b..1d713f07 100644 --- a/RenderPipelineFile/rpplugins/motion_blur/shader/neighbor_minmax.frag.glsl +++ b/RenderPipelineFile/rpplugins/motion_blur/shader/neighbor_minmax.frag.glsl @@ -1,71 +1,71 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "motion_blur.inc.glsl" - -uniform sampler2D TileMinMax; -out vec2 result; - -// Based on: -// http://graphics.cs.williams.edu/papers/MotionBlurI3D12/ - -void main() { - - ivec2 tile = ivec2(gl_FragCoord.xy); - ivec2 screen_coord = tile * tile_size; - ivec2 max_tiles = textureSize(TileMinMax, 0) - 1; - - vec2 max_velocity = vec2(0.0); - float largest_magnitude = -1.0; - - const int filter_size = 2; - - for (int x = -filter_size; x <= filter_size; ++x) { - for (int y = -filter_size; y <= filter_size; ++y) { - ivec2 neighbor_coord = clamp(tile + ivec2(x, y), ivec2(0), ivec2(max_tiles)); - vec2 vmax_neighbor = texelFetch(TileMinMax, neighbor_coord, 0).xy; - - float magnitude_neighbor = dot(vmax_neighbor, vmax_neighbor); - - if (magnitude_neighbor > largest_magnitude) { - vec2 direction_of_velocity = vmax_neighbor; - int displacement = abs(x) + abs(y); - ivec2 point = ivec2(sign(vec2(x, y) * direction_of_velocity)); - float dist = point.x + point.y; - if (abs(dist) == displacement) { - max_velocity = vmax_neighbor; - largest_magnitude = magnitude_neighbor; - } - } - } - } - - result = max_velocity; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "motion_blur.inc.glsl" + +uniform sampler2D TileMinMax; +out vec2 result; + +// Based on: +// http://graphics.cs.williams.edu/papers/MotionBlurI3D12/ + +void main() { + + ivec2 tile = ivec2(gl_FragCoord.xy); + ivec2 screen_coord = tile * tile_size; + ivec2 max_tiles = textureSize(TileMinMax, 0) - 1; + + vec2 max_velocity = vec2(0.0); + float largest_magnitude = -1.0; + + const int filter_size = 2; + + for (int x = -filter_size; x <= filter_size; ++x) { + for (int y = -filter_size; y <= filter_size; ++y) { + ivec2 neighbor_coord = clamp(tile + ivec2(x, y), ivec2(0), ivec2(max_tiles)); + vec2 vmax_neighbor = texelFetch(TileMinMax, neighbor_coord, 0).xy; + + float magnitude_neighbor = dot(vmax_neighbor, vmax_neighbor); + + if (magnitude_neighbor > largest_magnitude) { + vec2 direction_of_velocity = vmax_neighbor; + int displacement = abs(x) + abs(y); + ivec2 point = ivec2(sign(vec2(x, y) * direction_of_velocity)); + float dist = point.x + point.y; + if (abs(dist) == displacement) { + max_velocity = vmax_neighbor; + largest_magnitude = magnitude_neighbor; + } + } + } + } + + result = max_velocity; +} diff --git a/RenderPipelineFile/rpplugins/motion_blur/shader/pack_blur_data.frag.glsl b/RenderPipelineFile/rpplugins/motion_blur/shader/pack_blur_data.frag.glsl index a260bdf8..085af04d 100644 --- a/RenderPipelineFile/rpplugins/motion_blur/shader/pack_blur_data.frag.glsl +++ b/RenderPipelineFile/rpplugins/motion_blur/shader/pack_blur_data.frag.glsl @@ -1,44 +1,44 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// Packs the velocity length and scene depth into a RG16 target - -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "motion_blur.inc.glsl" - -out vec2 result; - -void main() { - vec2 texcoord = get_texcoord(); - float depth = get_depth_at(texcoord); - vec2 velocity = get_object_velocity_at(texcoord); - velocity = adjust_velocity(velocity); - result = vec2(length(velocity), depth); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// Packs the velocity length and scene depth into a RG16 target + +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "motion_blur.inc.glsl" + +out vec2 result; + +void main() { + vec2 texcoord = get_texcoord(); + float depth = get_depth_at(texcoord); + vec2 velocity = get_object_velocity_at(texcoord); + velocity = adjust_velocity(velocity); + result = vec2(length(velocity), depth); +} diff --git a/RenderPipelineFile/rpplugins/plugin_prefab/config.yaml b/RenderPipelineFile/rpplugins/plugin_prefab/config.yaml index 2d71d0b8..f98265f0 100644 --- a/RenderPipelineFile/rpplugins/plugin_prefab/config.yaml +++ b/RenderPipelineFile/rpplugins/plugin_prefab/config.yaml @@ -1,48 +1,48 @@ - -settings: !!omap - - - some_int_setting: - type: int - range: [0, 10] - default: 5 - label: Some integer setting - description: Description of the setting. - - - some_resolution_setting: - type: power_of_two - range: [4, 8192] - default: 32 - label: Some resolution setting, must be power of two. - description: Description of the setting. - - - some_float_setting: - type: float - range: [-5.3, 2.9] - default: 2.5 - label: Some float setting - description: Description of the setting. - runtime: true - - - some_enum: - type: enum - values: [value1, value2, value3] - default: value2 - label: Some enum setting - description: Description of the setting. - - - some_bool: - type: bool - default: false - label: Some boolean setting - description: Description of the setting. - -daytime_settings: !!omap - - - some_scalar: - type: scalar - unit: percent - range: [0, 1] - default: 0.5 - label: Some Scalar - description: Description of the scalar. - + +settings: !!omap + + - some_int_setting: + type: int + range: [0, 10] + default: 5 + label: Some integer setting + description: Description of the setting. + + - some_resolution_setting: + type: power_of_two + range: [4, 8192] + default: 32 + label: Some resolution setting, must be power of two. + description: Description of the setting. + + - some_float_setting: + type: float + range: [-5.3, 2.9] + default: 2.5 + label: Some float setting + description: Description of the setting. + runtime: true + + - some_enum: + type: enum + values: [value1, value2, value3] + default: value2 + label: Some enum setting + description: Description of the setting. + + - some_bool: + type: bool + default: false + label: Some boolean setting + description: Description of the setting. + +daytime_settings: !!omap + + - some_scalar: + type: scalar + unit: percent + range: [0, 1] + default: 0.5 + label: Some Scalar + description: Description of the scalar. + diff --git a/RenderPipelineFile/rpplugins/plugin_prefab/demo_stage.py b/RenderPipelineFile/rpplugins/plugin_prefab/demo_stage.py index 8faa32a3..537030b4 100644 --- a/RenderPipelineFile/rpplugins/plugin_prefab/demo_stage.py +++ b/RenderPipelineFile/rpplugins/plugin_prefab/demo_stage.py @@ -1,47 +1,47 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.render_stage import RenderStage - - -class DemoStage(RenderStage): - - """ This is a demo stage which does .. nothing """ - - required_inputs = [] - required_pipes = ["ShadedScene"] - - @property - def produced_pipes(self): - return {"ShadedScene": self.target.color_tex} - - def create(self): - self.target = self.create_target("FancyEffect") - self.target.add_color_texture(bits=16) - self.target.prepare_buffer() - - def reload_shaders(self): - self.target.shader = self.load_plugin_shader("fancy_effect.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.render_stage import RenderStage + + +class DemoStage(RenderStage): + + """ This is a demo stage which does .. nothing """ + + required_inputs = [] + required_pipes = ["ShadedScene"] + + @property + def produced_pipes(self): + return {"ShadedScene": self.target.color_tex} + + def create(self): + self.target = self.create_target("FancyEffect") + self.target.add_color_texture(bits=16) + self.target.prepare_buffer() + + def reload_shaders(self): + self.target.shader = self.load_plugin_shader("fancy_effect.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/plugin_prefab/plugin.py b/RenderPipelineFile/rpplugins/plugin_prefab/plugin.py index d288548b..1383e82b 100644 --- a/RenderPipelineFile/rpplugins/plugin_prefab/plugin.py +++ b/RenderPipelineFile/rpplugins/plugin_prefab/plugin.py @@ -1,55 +1,55 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" -# Load the base plugin class -from rpcore.pluginbase.base_plugin import BasePlugin - -# Load your additional plugin classes here, if required -from .demo_stage import DemoStage - - -class Plugin(BasePlugin): - - name = "Plugin Prefab" - author = "tobspr " - description = ("This is the most basic structure of a plugin. You can copy " - "it to produce your own plugins") - version = "1.0" - - def on_stage_setup(self): - """ This method gets called when the pipeline setups the render - stages. You should create your custom stages here """ - - # Setup a demo stage - self.stage = self.create_stage(DemoStage) - - def on_pipeline_created(self): - """ This method gets called after the pipeline finished the setup, - and is about to start rendering """ - - def update_some_setting(self): - """ This method gets called when the setting "some_setting" - of your plugin gets called. You should do all work to update required - inputs etc. yourself. """ +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +# Load the base plugin class +from rpcore.pluginbase.base_plugin import BasePlugin + +# Load your additional plugin classes here, if required +from .demo_stage import DemoStage + + +class Plugin(BasePlugin): + + name = "Plugin Prefab" + author = "tobspr " + description = ("This is the most basic structure of a plugin. You can copy " + "it to produce your own plugins") + version = "1.0" + + def on_stage_setup(self): + """ This method gets called when the pipeline setups the render + stages. You should create your custom stages here """ + + # Setup a demo stage + self.stage = self.create_stage(DemoStage) + + def on_pipeline_created(self): + """ This method gets called after the pipeline finished the setup, + and is about to start rendering """ + + def update_some_setting(self): + """ This method gets called when the setting "some_setting" + of your plugin gets called. You should do all work to update required + inputs etc. yourself. """ diff --git a/RenderPipelineFile/rpplugins/plugin_prefab/resources/README.txt b/RenderPipelineFile/rpplugins/plugin_prefab/resources/README.txt index df8455e5..1d6d114f 100644 --- a/RenderPipelineFile/rpplugins/plugin_prefab/resources/README.txt +++ b/RenderPipelineFile/rpplugins/plugin_prefab/resources/README.txt @@ -1,4 +1,4 @@ -Put your textures / models and other resources into this folder. - -You can access them with get_resource("resource_name.ext") - +Put your textures / models and other resources into this folder. + +You can access them with get_resource("resource_name.ext") + diff --git a/RenderPipelineFile/rpplugins/plugin_prefab/shader/README.txt b/RenderPipelineFile/rpplugins/plugin_prefab/shader/README.txt index 4a0dc3a9..927c2e38 100644 --- a/RenderPipelineFile/rpplugins/plugin_prefab/shader/README.txt +++ b/RenderPipelineFile/rpplugins/plugin_prefab/shader/README.txt @@ -1,12 +1,12 @@ -Put your shaders into this directory. - --- - -You can access shader files with self.get_shader_resource("shader_name.frag.glsl") - --- - -You can access load shader files inside of a RenderStage with: - -self.load_plugin_shader("shader_name.frag.glsl") - +Put your shaders into this directory. + +-- + +You can access shader files with self.get_shader_resource("shader_name.frag.glsl") + +-- + +You can access load shader files inside of a RenderStage with: + +self.load_plugin_shader("shader_name.frag.glsl") + diff --git a/RenderPipelineFile/rpplugins/pssm/config.yaml b/RenderPipelineFile/rpplugins/pssm/config.yaml index f84901d0..1d54aa73 100644 --- a/RenderPipelineFile/rpplugins/pssm/config.yaml +++ b/RenderPipelineFile/rpplugins/pssm/config.yaml @@ -1,254 +1,254 @@ - -settings: !!omap - - - max_distance: - type: float - range: [2.0, 6000.0] - default: 100.0 - runtime: true - shader_runtime: true - label: Maximum shadow distance - description: > - This controls the maximum distance up to where shadows - are rendered. A higher distance means further shadows, - but also a worse shadow quality. - - - logarithmic_factor: - type: float - range: [0.01, 3.0] - default: 2.4 - runtime: true - shader_runtime: true - label: Logarithmic Factor - description: > - Controls the logarithmic factor of the splitting scheme. Lower - values mean a more linear distribution, while higher values will - move the splits closer to the camera. - - - sun_distance: - type: float - range: [100.0, 10000.0] - default: 100.0 - label: Sun distance - runtime: true - description: > - This controls how far the sun is away from the scene, to avoid - clipping objects outside of the frustum which still cast shadows. - Basically you don't need to adjust this setting, but in - case you get artifacts, try increasing this setting. - - - split_count: - type: int - range: [1, 20] - default: 5 - label: Amount of PSSM Splits / Cascades - description: > - This controls how many splits are used for the PSSM. - A higher amount of splits increases the shadow map - detail and quality, but also increases the performance - cost by a lot. For every split, the scene has to get - rendered again, so be careful when choosing high amount - of splits. - - - resolution: - type: power_of_two - range: [128, 4096] - default: 1024 - label: Shadowmap resolution - description: > - This controls the resolution of the individual shadowmaps - of the splits. If you choose a resolution of 512 for example, - and you have 10 splits, then that makes a total resolution - of 5120 * 512 for the shadow map. Be careful when choosing high - defaults, the shadow map will soon take a huge amount of VRam. - - - border_bias: - type: float - range: [0.0, 0.35] - default: 0.058 - label: Border Padding - runtime: true - shader_runtime: true - description: > - Increases the shadow map size of each cascade by a small factor, to - ensure seamless filtering between cascades. If you encounter artifacts - between cascades, increase this value. - - - use_pcf: - type: bool - default: true - label: Use hardware filtering (PCF) - description: > - This setting turns Percentage Closer Filtering on or off (A simple - 2x2 bilinear filter provided by the GPU). - It provides a better shadow quality, and theoretically a better - performance. However, not all GPU's seem to support it. - - - filter_sequence: - type: sample_sequence - dimension: 2 - default: halton_2D_16 - shader_runtime: true - label: Filtering sequence - description: > - Amount and Type of samples to take for filtering the shadow. Higher - values provide a better shadow quality, but also are more expensive. - - - filter_radius: - type: float - range: [0.05, 10.0] - default: 0.55 - shader_runtime: true - label: Filtering radius - description: > - This setting controls the filter size, higher values mean a more - smooth penumbra. Too high values might (and will) produce artifacts. - This setting has almost no measureable impact on performance. - - - fixed_bias: - type: float - range: [0.0, 2.0] - default: 0.48 - shader_runtime: true - label: Depth Bias - description: > - Depth-Based bias. This avoids self-shadowing issues (also known) - as Shadow-Acne. Too high values will produce gaps in the shadows, - so try to find the lowest possible value. - - - slope_bias: - type: float - range: [0.0, 5.0] - default: 0.11 - shader_runtime: true - label: Slope Bias - description: > - Slope-Based bias. This avoid self-shadowing issues as well, but instead - of the fixed bias it adds bias based on the angle between the light - and the surface. - - - normal_bias: - type: float - range: [0.0, 5.0] - default: 0.39 - shader_runtime: true - label: Normal Bias - description: > - Normal-Based bias. This moves the sample position by a small amount - into the direction of the surface normal, to avoid self shadowing. - - # PCSS settings - - - use_pcss: - type: bool - default: false - shader_runtime: true - label: Use PCSS - description: > - PCSS adds support for variable penumbra size shadows. Without PCSS, - all shadows have the same penumbra size at all places. With PCSS, - the shadow penumbra size increases based on the distance. However, - PCSS requires additionally computations, and thus is slower than - regular filtering. - - - pcss_sequence: - display_if: {use_pcss: true} - type: sample_sequence - dimension: 2 - default: halton_2D_16 - shader_runtime: true - label: PCSS Penumbra Samples - description: > - Amount and Type of samples to take for finding the penumbra size. - Higher values might produce better results at the cost of performance. - - - pcss_penumbra_size: - display_if: {use_pcss: true} - type: float - range: [0.1, 300.0] - default: 11.0 - shader_runtime: true - label: PCSS Penumbra scale - description: > - Scales the penumbra by a fixed factor. This is an artistic setting, - and does not affect performance much. Lower settings will cause a - smaller penumbra, while higher settings produce a bigger one. Too - high values might cause artifacts in the filtering. - - - pcss_min_penumbra_size: - display_if: {use_pcss: true} - type: float - range: [0.1, 20.0] - default: 5.0 - shader_runtime: true - label: PCSS Min. Penumbra scale - description: > - Specifies the minimum penumbra/filter size to avoid blocky shadows. - - - use_distant_shadows: - type: bool - default: true - label: Render distant shadows - description: > - When this setting is enabled, an additional shadow map is generated - to render distant shadows, e.g. for terrain. This makes it possible - to set the pssm distance very small, but still recieve shadows from - big objects. If your scene is not big, this may not be required. - - - dist_shadow_resolution: - display_if: {use_distant_shadows: true} - type: power_of_two - range: [128, 8192] - default: 1024 - label: Distant Shadow Resolution - description: > - Controls the size of distant shadow map in pixels. - - - dist_shadow_clipsize: - display_if: {use_distant_shadows: true} - type: float - range: [100.0, 20000.0] - default: 1000.0 - runtime: true - label: Distant Shadow Range - description: > - Controls the range of the distant shadow map. Bigger values cover - a bigger part of the scene, but also decrease the effective resolution. - - - dist_shadow_sundist: - display_if: {use_distant_shadows: true} - type: float - range: [100.0, 20000.0] - default: 500.0 - label: Distant Shadow Sun Distance - runtime: true - description: > - Same as the Sun Distance, but for the distant shadow map. - - - scene_shadow_resolution: - type: power_of_two - range: [128, 4096] - default: 512 - label: Scene shadow Resolution - description: > - Controls the size of the shadow map which is used when rendering - environment probes. - - - scene_shadow_sundist: - type: float - range: [100.0, 20000.0] - default: 100.0 - label: Scene Shadow Sun Distance - runtime: true - description: > - Same as the Sun Distance, but for the scene shadow map. - -daytime_settings: - - - - - - - - + +settings: !!omap + + - max_distance: + type: float + range: [2.0, 6000.0] + default: 100.0 + runtime: true + shader_runtime: true + label: Maximum shadow distance + description: > + This controls the maximum distance up to where shadows + are rendered. A higher distance means further shadows, + but also a worse shadow quality. + + - logarithmic_factor: + type: float + range: [0.01, 3.0] + default: 2.4 + runtime: true + shader_runtime: true + label: Logarithmic Factor + description: > + Controls the logarithmic factor of the splitting scheme. Lower + values mean a more linear distribution, while higher values will + move the splits closer to the camera. + + - sun_distance: + type: float + range: [100.0, 10000.0] + default: 100.0 + label: Sun distance + runtime: true + description: > + This controls how far the sun is away from the scene, to avoid + clipping objects outside of the frustum which still cast shadows. + Basically you don't need to adjust this setting, but in + case you get artifacts, try increasing this setting. + + - split_count: + type: int + range: [1, 20] + default: 5 + label: Amount of PSSM Splits / Cascades + description: > + This controls how many splits are used for the PSSM. + A higher amount of splits increases the shadow map + detail and quality, but also increases the performance + cost by a lot. For every split, the scene has to get + rendered again, so be careful when choosing high amount + of splits. + + - resolution: + type: power_of_two + range: [128, 4096] + default: 1024 + label: Shadowmap resolution + description: > + This controls the resolution of the individual shadowmaps + of the splits. If you choose a resolution of 512 for example, + and you have 10 splits, then that makes a total resolution + of 5120 * 512 for the shadow map. Be careful when choosing high + defaults, the shadow map will soon take a huge amount of VRam. + + - border_bias: + type: float + range: [0.0, 0.35] + default: 0.058 + label: Border Padding + runtime: true + shader_runtime: true + description: > + Increases the shadow map size of each cascade by a small factor, to + ensure seamless filtering between cascades. If you encounter artifacts + between cascades, increase this value. + + - use_pcf: + type: bool + default: true + label: Use hardware filtering (PCF) + description: > + This setting turns Percentage Closer Filtering on or off (A simple + 2x2 bilinear filter provided by the GPU). + It provides a better shadow quality, and theoretically a better + performance. However, not all GPU's seem to support it. + + - filter_sequence: + type: sample_sequence + dimension: 2 + default: halton_2D_16 + shader_runtime: true + label: Filtering sequence + description: > + Amount and Type of samples to take for filtering the shadow. Higher + values provide a better shadow quality, but also are more expensive. + + - filter_radius: + type: float + range: [0.05, 10.0] + default: 0.55 + shader_runtime: true + label: Filtering radius + description: > + This setting controls the filter size, higher values mean a more + smooth penumbra. Too high values might (and will) produce artifacts. + This setting has almost no measureable impact on performance. + + - fixed_bias: + type: float + range: [0.0, 2.0] + default: 0.48 + shader_runtime: true + label: Depth Bias + description: > + Depth-Based bias. This avoids self-shadowing issues (also known) + as Shadow-Acne. Too high values will produce gaps in the shadows, + so try to find the lowest possible value. + + - slope_bias: + type: float + range: [0.0, 5.0] + default: 0.11 + shader_runtime: true + label: Slope Bias + description: > + Slope-Based bias. This avoid self-shadowing issues as well, but instead + of the fixed bias it adds bias based on the angle between the light + and the surface. + + - normal_bias: + type: float + range: [0.0, 5.0] + default: 0.39 + shader_runtime: true + label: Normal Bias + description: > + Normal-Based bias. This moves the sample position by a small amount + into the direction of the surface normal, to avoid self shadowing. + + # PCSS settings + + - use_pcss: + type: bool + default: false + shader_runtime: true + label: Use PCSS + description: > + PCSS adds support for variable penumbra size shadows. Without PCSS, + all shadows have the same penumbra size at all places. With PCSS, + the shadow penumbra size increases based on the distance. However, + PCSS requires additionally computations, and thus is slower than + regular filtering. + + - pcss_sequence: + display_if: {use_pcss: true} + type: sample_sequence + dimension: 2 + default: halton_2D_16 + shader_runtime: true + label: PCSS Penumbra Samples + description: > + Amount and Type of samples to take for finding the penumbra size. + Higher values might produce better results at the cost of performance. + + - pcss_penumbra_size: + display_if: {use_pcss: true} + type: float + range: [0.1, 300.0] + default: 11.0 + shader_runtime: true + label: PCSS Penumbra scale + description: > + Scales the penumbra by a fixed factor. This is an artistic setting, + and does not affect performance much. Lower settings will cause a + smaller penumbra, while higher settings produce a bigger one. Too + high values might cause artifacts in the filtering. + + - pcss_min_penumbra_size: + display_if: {use_pcss: true} + type: float + range: [0.1, 20.0] + default: 5.0 + shader_runtime: true + label: PCSS Min. Penumbra scale + description: > + Specifies the minimum penumbra/filter size to avoid blocky shadows. + + - use_distant_shadows: + type: bool + default: true + label: Render distant shadows + description: > + When this setting is enabled, an additional shadow map is generated + to render distant shadows, e.g. for terrain. This makes it possible + to set the pssm distance very small, but still recieve shadows from + big objects. If your scene is not big, this may not be required. + + - dist_shadow_resolution: + display_if: {use_distant_shadows: true} + type: power_of_two + range: [128, 8192] + default: 1024 + label: Distant Shadow Resolution + description: > + Controls the size of distant shadow map in pixels. + + - dist_shadow_clipsize: + display_if: {use_distant_shadows: true} + type: float + range: [100.0, 20000.0] + default: 1000.0 + runtime: true + label: Distant Shadow Range + description: > + Controls the range of the distant shadow map. Bigger values cover + a bigger part of the scene, but also decrease the effective resolution. + + - dist_shadow_sundist: + display_if: {use_distant_shadows: true} + type: float + range: [100.0, 20000.0] + default: 500.0 + label: Distant Shadow Sun Distance + runtime: true + description: > + Same as the Sun Distance, but for the distant shadow map. + + - scene_shadow_resolution: + type: power_of_two + range: [128, 4096] + default: 512 + label: Scene shadow Resolution + description: > + Controls the size of the shadow map which is used when rendering + environment probes. + + - scene_shadow_sundist: + type: float + range: [100.0, 20000.0] + default: 100.0 + label: Scene Shadow Sun Distance + runtime: true + description: > + Same as the Sun Distance, but for the scene shadow map. + +daytime_settings: + + + + + + + + diff --git a/RenderPipelineFile/rpplugins/pssm/plugin.py b/RenderPipelineFile/rpplugins/pssm/plugin.py index ed5a9df2..bb0770d5 100644 --- a/RenderPipelineFile/rpplugins/pssm/plugin.py +++ b/RenderPipelineFile/rpplugins/pssm/plugin.py @@ -1,182 +1,182 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" -from __future__ import print_function - -from six.moves import range # pylint: disable=import-error -from panda3d.core import PTAVecBase3f - -from rpcore.globals import Globals -from rpcore.pluginbase.base_plugin import BasePlugin -from rpcore.native import PSSMCameraRig, NATIVE_CXX_LOADED - -from .pssm_stage import PSSMStage -from .pssm_shadow_stage import PSSMShadowStage -from .pssm_scene_shadow_stage import PSSMSceneShadowStage -from .pssm_dist_shadow_stage import PSSMDistShadowStage - - -class Plugin(BasePlugin): - - name = "Sun Shadows" - author = "tobspr " - description = ("This plugin adds support for Parallel Split Shadow Maps " - "(PSSM), and also sun lighting.") - version = "1.2" - required_plugins = ("scattering",) - - def on_stage_setup(self): - - if not NATIVE_CXX_LOADED: - self.debug("Setting max splits to 1 since python is used") - self._pipeline.plugin_mgr.settings["pssm"]["split_count"].set_value(1) - - self.update_enabled = True - self.pta_sun_vector = PTAVecBase3f.empty_array(1) - self.last_cache_reset = 0 - - self.shadow_stage = self.create_stage(PSSMShadowStage) - self.pssm_stage = self.create_stage(PSSMStage) - - self.shadow_stage.num_splits = self.get_setting("split_count") - self.shadow_stage.split_resolution = self.get_setting("resolution") - - self.scene_shadow_stage = self.create_stage(PSSMSceneShadowStage) - self.scene_shadow_stage.resolution = self.get_setting("scene_shadow_resolution") - self.scene_shadow_stage.sun_distance = self.get_setting("scene_shadow_sundist") - - # Enable distant shadow map if specified - if self.get_setting("use_distant_shadows"): - self.dist_shadow_stage = self.create_stage(PSSMDistShadowStage) - self.dist_shadow_stage.resolution = self.get_setting("dist_shadow_resolution") - self.dist_shadow_stage.clip_size = self.get_setting("dist_shadow_clipsize") - self.dist_shadow_stage.sun_distance = self.get_setting("dist_shadow_sundist") - - self.pssm_stage.required_pipes.append("PSSMDistSunShadowMap") - self.pssm_stage.required_inputs.append("PSSMDistSunShadowMapMVP") - - def on_pipeline_created(self): - self.debug("Initializing pssm ..") - # Construct a dummy node to parent the rig to - self.node = Globals.base.render.attach_new_node("PSSMCameraRig") - self.node.hide() - - # Construct the actual PSSM rig - self.camera_rig = PSSMCameraRig(self.get_setting("split_count")) # noqa # pylint: disable=undefined-variable - self.camera_rig.set_sun_distance(self.get_setting("sun_distance")) - self.camera_rig.set_pssm_distance(self.get_setting("max_distance")) - self.camera_rig.set_logarithmic_factor(self.get_setting("logarithmic_factor")) - self.camera_rig.set_border_bias(self.get_setting("border_bias")) - self.camera_rig.set_use_stable_csm(True) - self.camera_rig.set_use_fixed_film_size(True) - self.camera_rig.set_resolution(self.get_setting("resolution")) - self.camera_rig.reparent_to(self.node) - - # Attach the cameras to the shadow stage - for i in range(self.get_setting("split_count")): - camera_np = self.camera_rig.get_camera(i) - camera_np.node().set_scene(Globals.base.render) - region = self.shadow_stage.split_regions[i] - region.set_camera(camera_np) - # camera_np.node().show_frustum() - - # Make sure the pipeline knows about our camera, so it can apply - # the correct bitmasks - self._pipeline.tag_mgr.register_camera("shadow", camera_np.node()) - - # Accept a shortcut to enable / disable the update of PSSM - Globals.base.accept("u", self.toggle_update_enabled) - - # Set inputs - self.pssm_stage.set_shader_inputs( - pssm_mvps=self.camera_rig.get_mvp_array(), - pssm_nearfar=self.camera_rig.get_nearfar_array()) - - if self.is_plugin_enabled("volumetrics"): - handle = self.get_plugin_instance("volumetrics") - handle.stage.set_shader_inputs( - pssm_mvps=self.camera_rig.get_mvp_array(), - pssm_nearfar=self.camera_rig.get_nearfar_array()) - - - def on_pre_render_update(self): - sun_vector = self.get_plugin_instance("scattering").sun_vector - - if sun_vector.z < 0.0: - self.shadow_stage.active = False - self.scene_shadow_stage.active = False - self.pssm_stage.set_render_shadows(False) - - if self.get_setting("use_distant_shadows"): - self.dist_shadow_stage.active = False - - # Return, no need to update the pssm splits - return - else: - self.shadow_stage.active = True - self.scene_shadow_stage.active = True - self.pssm_stage.set_render_shadows(True) - - if self.get_setting("use_distant_shadows"): - self.dist_shadow_stage.active = True - - if self.update_enabled: - self.camera_rig.update(Globals.base.camera, sun_vector) - - # Eventually reset cache - cache_diff = Globals.clock.get_frame_time() - self.last_cache_reset - if cache_diff > 5.0: - self.last_cache_reset = Globals.clock.get_frame_time() - self.camera_rig.reset_film_size_cache() - - self.scene_shadow_stage.sun_vector = sun_vector - - if self.get_setting("use_distant_shadows"): - self.dist_shadow_stage.sun_vector = sun_vector - - def update_max_distance(self): - self.camera_rig.set_pssm_distance(self.get_setting("max_distance")) - - def update_logarithmic_factor(self): - self.camera_rig.set_logarithmic_factor(self.get_setting("logarithmic_factor")) - - def update_border_bias(self): - self.camera_rig.set_border_bias(self.get_setting("border_bias")) - - def update_sun_distance(self): - self.camera_rig.set_sun_distance(self.get_setting("sun_distance")) - - def update_dist_shadow_clipsize(self): - self.dist_shadow_stage.clip_size = self.get_setting("dist_shadow_clipsize") - - def update_dist_shadow_sundist(self): - self.dist_shadow_stage.sun_distance = self.get_setting("dist_shadow_sundist") - - def update_scene_shadow_sundist(self): - self.scene_shadow_stage.sun_distance = self.get_setting("scene_shadow_sundist") - - def toggle_update_enabled(self): - self.update_enabled = not self.update_enabled - self.debug("Update enabled:", self.update_enabled) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +from __future__ import print_function + +from six.moves import range # pylint: disable=import-error +from panda3d.core import PTAVecBase3f + +from rpcore.globals import Globals +from rpcore.pluginbase.base_plugin import BasePlugin +from rpcore.native import PSSMCameraRig, NATIVE_CXX_LOADED + +from .pssm_stage import PSSMStage +from .pssm_shadow_stage import PSSMShadowStage +from .pssm_scene_shadow_stage import PSSMSceneShadowStage +from .pssm_dist_shadow_stage import PSSMDistShadowStage + + +class Plugin(BasePlugin): + + name = "Sun Shadows" + author = "tobspr " + description = ("This plugin adds support for Parallel Split Shadow Maps " + "(PSSM), and also sun lighting.") + version = "1.2" + required_plugins = ("scattering",) + + def on_stage_setup(self): + + if not NATIVE_CXX_LOADED: + self.debug("Setting max splits to 1 since python is used") + self._pipeline.plugin_mgr.settings["pssm"]["split_count"].set_value(1) + + self.update_enabled = True + self.pta_sun_vector = PTAVecBase3f.empty_array(1) + self.last_cache_reset = 0 + + self.shadow_stage = self.create_stage(PSSMShadowStage) + self.pssm_stage = self.create_stage(PSSMStage) + + self.shadow_stage.num_splits = self.get_setting("split_count") + self.shadow_stage.split_resolution = self.get_setting("resolution") + + self.scene_shadow_stage = self.create_stage(PSSMSceneShadowStage) + self.scene_shadow_stage.resolution = self.get_setting("scene_shadow_resolution") + self.scene_shadow_stage.sun_distance = self.get_setting("scene_shadow_sundist") + + # Enable distant shadow map if specified + if self.get_setting("use_distant_shadows"): + self.dist_shadow_stage = self.create_stage(PSSMDistShadowStage) + self.dist_shadow_stage.resolution = self.get_setting("dist_shadow_resolution") + self.dist_shadow_stage.clip_size = self.get_setting("dist_shadow_clipsize") + self.dist_shadow_stage.sun_distance = self.get_setting("dist_shadow_sundist") + + self.pssm_stage.required_pipes.append("PSSMDistSunShadowMap") + self.pssm_stage.required_inputs.append("PSSMDistSunShadowMapMVP") + + def on_pipeline_created(self): + self.debug("Initializing pssm ..") + # Construct a dummy node to parent the rig to + self.node = Globals.base.render.attach_new_node("PSSMCameraRig") + self.node.hide() + + # Construct the actual PSSM rig + self.camera_rig = PSSMCameraRig(self.get_setting("split_count")) # noqa # pylint: disable=undefined-variable + self.camera_rig.set_sun_distance(self.get_setting("sun_distance")) + self.camera_rig.set_pssm_distance(self.get_setting("max_distance")) + self.camera_rig.set_logarithmic_factor(self.get_setting("logarithmic_factor")) + self.camera_rig.set_border_bias(self.get_setting("border_bias")) + self.camera_rig.set_use_stable_csm(True) + self.camera_rig.set_use_fixed_film_size(True) + self.camera_rig.set_resolution(self.get_setting("resolution")) + self.camera_rig.reparent_to(self.node) + + # Attach the cameras to the shadow stage + for i in range(self.get_setting("split_count")): + camera_np = self.camera_rig.get_camera(i) + camera_np.node().set_scene(Globals.base.render) + region = self.shadow_stage.split_regions[i] + region.set_camera(camera_np) + # camera_np.node().show_frustum() + + # Make sure the pipeline knows about our camera, so it can apply + # the correct bitmasks + self._pipeline.tag_mgr.register_camera("shadow", camera_np.node()) + + # Accept a shortcut to enable / disable the update of PSSM + Globals.base.accept("u", self.toggle_update_enabled) + + # Set inputs + self.pssm_stage.set_shader_inputs( + pssm_mvps=self.camera_rig.get_mvp_array(), + pssm_nearfar=self.camera_rig.get_nearfar_array()) + + if self.is_plugin_enabled("volumetrics"): + handle = self.get_plugin_instance("volumetrics") + handle.stage.set_shader_inputs( + pssm_mvps=self.camera_rig.get_mvp_array(), + pssm_nearfar=self.camera_rig.get_nearfar_array()) + + + def on_pre_render_update(self): + sun_vector = self.get_plugin_instance("scattering").sun_vector + + if sun_vector.z < 0.0: + self.shadow_stage.active = False + self.scene_shadow_stage.active = False + self.pssm_stage.set_render_shadows(False) + + if self.get_setting("use_distant_shadows"): + self.dist_shadow_stage.active = False + + # Return, no need to update the pssm splits + return + else: + self.shadow_stage.active = True + self.scene_shadow_stage.active = True + self.pssm_stage.set_render_shadows(True) + + if self.get_setting("use_distant_shadows"): + self.dist_shadow_stage.active = True + + if self.update_enabled: + self.camera_rig.update(Globals.base.camera, sun_vector) + + # Eventually reset cache + cache_diff = Globals.clock.get_frame_time() - self.last_cache_reset + if cache_diff > 5.0: + self.last_cache_reset = Globals.clock.get_frame_time() + self.camera_rig.reset_film_size_cache() + + self.scene_shadow_stage.sun_vector = sun_vector + + if self.get_setting("use_distant_shadows"): + self.dist_shadow_stage.sun_vector = sun_vector + + def update_max_distance(self): + self.camera_rig.set_pssm_distance(self.get_setting("max_distance")) + + def update_logarithmic_factor(self): + self.camera_rig.set_logarithmic_factor(self.get_setting("logarithmic_factor")) + + def update_border_bias(self): + self.camera_rig.set_border_bias(self.get_setting("border_bias")) + + def update_sun_distance(self): + self.camera_rig.set_sun_distance(self.get_setting("sun_distance")) + + def update_dist_shadow_clipsize(self): + self.dist_shadow_stage.clip_size = self.get_setting("dist_shadow_clipsize") + + def update_dist_shadow_sundist(self): + self.dist_shadow_stage.sun_distance = self.get_setting("dist_shadow_sundist") + + def update_scene_shadow_sundist(self): + self.scene_shadow_stage.sun_distance = self.get_setting("scene_shadow_sundist") + + def toggle_update_enabled(self): + self.update_enabled = not self.update_enabled + self.debug("Update enabled:", self.update_enabled) diff --git a/RenderPipelineFile/rpplugins/pssm/pssm_dist_shadow_stage.py b/RenderPipelineFile/rpplugins/pssm/pssm_dist_shadow_stage.py index 5a59c5aa..c3bca193 100644 --- a/RenderPipelineFile/rpplugins/pssm/pssm_dist_shadow_stage.py +++ b/RenderPipelineFile/rpplugins/pssm/pssm_dist_shadow_stage.py @@ -1,140 +1,140 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" -from __future__ import division - -from panda3d.core import Vec3, Camera, OrthographicLens, PTAMat4 -from panda3d.core import LVecBase2i - -from rpcore.globals import Globals -from rpcore.render_stage import RenderStage -from rpcore.util.generic import snap_shadow_map - - -class PSSMDistShadowStage(RenderStage): - - """ This stage generates a depth map using Variance Shadow Maps for very - distant objects. """ - - required_inputs = [] - - def __init__(self, pipeline): - RenderStage.__init__(self, pipeline) - self.resolution = 2048 - self.clip_size = 500 - self.sun_distance = 8000 - self.sun_vector = Vec3(0, 0, 1) - self.pta_mvp = PTAMat4.empty_array(1) - - @property - def produced_inputs(self): - return {"PSSMDistSunShadowMapMVP": self.pta_mvp} - - @property - def produced_pipes(self): - return {"PSSMDistSunShadowMap": self.target_blur_h.color_tex} - - @property - def mvp(self): - return Globals.base.render.get_transform(self.cam_node).get_mat() * \ - self.cam_lens.get_projection_mat() - - def update(self): - self.target.active = False - self.target_convert.active = False - self.target_blur_v.active = False - self.target_blur_h.active = False - - # Query scheduled tasks - if self._pipeline.task_scheduler.is_scheduled("pssm_distant_shadows"): - - self.target.active = True - - # Reposition camera before we capture the scene - cam_pos = Globals.base.cam.get_pos(Globals.base.render) - self.cam_node.set_pos(cam_pos + self.sun_vector * self.sun_distance) - self.cam_node.look_at(cam_pos) - self.cam_lens.set_film_size(self.clip_size, self.clip_size) - - snap_shadow_map(self.mvp, self.cam_node, self.resolution) - - if self._pipeline.task_scheduler.is_scheduled("pssm_convert_distant_to_esm"): - self.target_convert.active = True - if self._pipeline.task_scheduler.is_scheduled("pssm_blur_distant_vert"): - self.target_blur_v.active = True - if self._pipeline.task_scheduler.is_scheduled("pssm_blur_distant_horiz"): - self.target_blur_h.active = True - - # Only update the MVP as soon as the shadow map is available - self.pta_mvp[0] = self.mvp - - def create(self): - self.camera = Camera("PSSMDistShadowsESM") - self.cam_lens = OrthographicLens() - self.cam_lens.set_film_size(12000, 12000) - self.cam_lens.set_near_far(10.0, self.sun_distance * 2) - self.camera.set_lens(self.cam_lens) - self.cam_node = Globals.base.render.attach_new_node(self.camera) - - self.target = self.create_target("ShadowMap") - self.target.size = self.resolution - self.target.add_depth_attachment(bits=32) - self.target.prepare_render(self.cam_node) - - self.target_convert = self.create_target("ConvertToESM") - self.target_convert.size = self.resolution - self.target_convert.add_color_attachment(bits=(32, 0, 0, 0)) - self.target_convert.prepare_buffer() - self.target_convert.set_shader_input("SourceTex", self.target.depth_tex) - - self.target_blur_v = self.create_target("BlurVert") - self.target_blur_v.size = self.resolution - self.target_blur_v.add_color_attachment(bits=(32, 0, 0, 0)) - self.target_blur_v.prepare_buffer() - self.target_blur_v.set_shader_inputs( - SourceTex=self.target_convert.color_tex, - direction=LVecBase2i(1, 0)) - - self.target_blur_h = self.create_target("BlurHoriz") - self.target_blur_h.size = self.resolution - self.target_blur_h.add_color_attachment(bits=(32, 0, 0, 0)) - self.target_blur_h.prepare_buffer() - self.target_blur_h.set_shader_inputs( - SourceTex=self.target_blur_v.color_tex, - direction=LVecBase2i(0, 1)) - - # Register shadow camera - self._pipeline.tag_mgr.register_camera("shadow", self.camera) - - def reload_shaders(self): - self.target_convert.shader = self.load_plugin_shader("convert_to_esm.frag.glsl") - self.target_blur_v.shader = self.load_plugin_shader("blur_esm.frag.glsl") - self.target_blur_h.shader = self.load_plugin_shader("blur_esm.frag.glsl") - - def set_shader_input(self, *args): - Globals.render.set_shader_input(*args) - - def set_shader_inputs(self, **kwargs): - Globals.render.set_shader_inputs(**kwargs) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +from __future__ import division + +from panda3d.core import Vec3, Camera, OrthographicLens, PTAMat4 +from panda3d.core import LVecBase2i + +from rpcore.globals import Globals +from rpcore.render_stage import RenderStage +from rpcore.util.generic import snap_shadow_map + + +class PSSMDistShadowStage(RenderStage): + + """ This stage generates a depth map using Variance Shadow Maps for very + distant objects. """ + + required_inputs = [] + + def __init__(self, pipeline): + RenderStage.__init__(self, pipeline) + self.resolution = 2048 + self.clip_size = 500 + self.sun_distance = 8000 + self.sun_vector = Vec3(0, 0, 1) + self.pta_mvp = PTAMat4.empty_array(1) + + @property + def produced_inputs(self): + return {"PSSMDistSunShadowMapMVP": self.pta_mvp} + + @property + def produced_pipes(self): + return {"PSSMDistSunShadowMap": self.target_blur_h.color_tex} + + @property + def mvp(self): + return Globals.base.render.get_transform(self.cam_node).get_mat() * \ + self.cam_lens.get_projection_mat() + + def update(self): + self.target.active = False + self.target_convert.active = False + self.target_blur_v.active = False + self.target_blur_h.active = False + + # Query scheduled tasks + if self._pipeline.task_scheduler.is_scheduled("pssm_distant_shadows"): + + self.target.active = True + + # Reposition camera before we capture the scene + cam_pos = Globals.base.cam.get_pos(Globals.base.render) + self.cam_node.set_pos(cam_pos + self.sun_vector * self.sun_distance) + self.cam_node.look_at(cam_pos) + self.cam_lens.set_film_size(self.clip_size, self.clip_size) + + snap_shadow_map(self.mvp, self.cam_node, self.resolution) + + if self._pipeline.task_scheduler.is_scheduled("pssm_convert_distant_to_esm"): + self.target_convert.active = True + if self._pipeline.task_scheduler.is_scheduled("pssm_blur_distant_vert"): + self.target_blur_v.active = True + if self._pipeline.task_scheduler.is_scheduled("pssm_blur_distant_horiz"): + self.target_blur_h.active = True + + # Only update the MVP as soon as the shadow map is available + self.pta_mvp[0] = self.mvp + + def create(self): + self.camera = Camera("PSSMDistShadowsESM") + self.cam_lens = OrthographicLens() + self.cam_lens.set_film_size(12000, 12000) + self.cam_lens.set_near_far(10.0, self.sun_distance * 2) + self.camera.set_lens(self.cam_lens) + self.cam_node = Globals.base.render.attach_new_node(self.camera) + + self.target = self.create_target("ShadowMap") + self.target.size = self.resolution + self.target.add_depth_attachment(bits=32) + self.target.prepare_render(self.cam_node) + + self.target_convert = self.create_target("ConvertToESM") + self.target_convert.size = self.resolution + self.target_convert.add_color_attachment(bits=(32, 0, 0, 0)) + self.target_convert.prepare_buffer() + self.target_convert.set_shader_input("SourceTex", self.target.depth_tex) + + self.target_blur_v = self.create_target("BlurVert") + self.target_blur_v.size = self.resolution + self.target_blur_v.add_color_attachment(bits=(32, 0, 0, 0)) + self.target_blur_v.prepare_buffer() + self.target_blur_v.set_shader_inputs( + SourceTex=self.target_convert.color_tex, + direction=LVecBase2i(1, 0)) + + self.target_blur_h = self.create_target("BlurHoriz") + self.target_blur_h.size = self.resolution + self.target_blur_h.add_color_attachment(bits=(32, 0, 0, 0)) + self.target_blur_h.prepare_buffer() + self.target_blur_h.set_shader_inputs( + SourceTex=self.target_blur_v.color_tex, + direction=LVecBase2i(0, 1)) + + # Register shadow camera + self._pipeline.tag_mgr.register_camera("shadow", self.camera) + + def reload_shaders(self): + self.target_convert.shader = self.load_plugin_shader("convert_to_esm.frag.glsl") + self.target_blur_v.shader = self.load_plugin_shader("blur_esm.frag.glsl") + self.target_blur_h.shader = self.load_plugin_shader("blur_esm.frag.glsl") + + def set_shader_input(self, *args): + Globals.render.set_shader_input(*args) + + def set_shader_inputs(self, **kwargs): + Globals.render.set_shader_inputs(**kwargs) diff --git a/RenderPipelineFile/rpplugins/pssm/pssm_scene_shadow_stage.py b/RenderPipelineFile/rpplugins/pssm/pssm_scene_shadow_stage.py index 6bfde176..faa59503 100644 --- a/RenderPipelineFile/rpplugins/pssm/pssm_scene_shadow_stage.py +++ b/RenderPipelineFile/rpplugins/pssm/pssm_scene_shadow_stage.py @@ -1,121 +1,121 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" -from __future__ import division - -from panda3d.core import Vec3, Camera, OrthographicLens, PTAMat4, SamplerState - -from rpcore.globals import Globals -from rpcore.render_stage import RenderStage -from rpcore.util.generic import snap_shadow_map - - -class PSSMSceneShadowStage(RenderStage): - - """ This stage creates the shadow map which covers the whole important part - of the scene. This is required because the shadow cascades only cover the - view frustum, but many plugins (VXGI, EnvMaps) require a shadow map. """ - - required_inputs = [] - required_pipes = [] - - def __init__(self, pipeline): - RenderStage.__init__(self, pipeline) - self.resolution = 2048 - self.sun_vector = Vec3(0, 0, 1) - self.sun_distance = 10.0 - self.pta_mvp = PTAMat4.empty_array(1) - self.focus = None - - # Store last focus entirely for the purpose of being able to see - # it in the debugger - self.last_focus = None - - @property - def produced_inputs(self): - return {"PSSMSceneSunShadowMVP": self.pta_mvp} - - @property - def produced_pipes(self): - return {"PSSMSceneSunShadowMapPCF": (self.target.depth_tex, self.make_pcf_state())} - - def make_pcf_state(self): - state = SamplerState() - state.set_minfilter(SamplerState.FT_shadow) - state.set_magfilter(SamplerState.FT_shadow) - return state - - def request_focus(self, focus_point, focus_size): - self.focus = (focus_point, focus_size) - self.last_focus = self.focus - - @property - def mvp(self): - return Globals.base.render.get_transform(self.cam_node).get_mat() * \ - self.cam_lens.get_projection_mat() - - def update(self): - if self._pipeline.task_scheduler.is_scheduled("pssm_scene_shadows"): - if self.focus is None: - # When no focus is set, there is no point in rendering the shadow map - self.target.active = False - else: - focus_point, focus_size = self.focus - - self.cam_lens.set_near_far(0.0, 2 * (focus_size + self.sun_distance)) - self.cam_lens.set_film_size(2 * focus_size, 2 * focus_size) - self.cam_node.set_pos( - focus_point + self.sun_vector * (self.sun_distance + focus_size)) - self.cam_node.look_at(focus_point) - - snap_shadow_map(self.mvp, self.cam_node, self.resolution) - self.target.active = True - self.pta_mvp[0] = self.mvp - - self.focus = None - else: - self.target.active = False - - def create(self): - self.camera = Camera("PSSMSceneSunShadowCam") - self.cam_lens = OrthographicLens() - self.cam_lens.set_film_size(400, 400) - self.cam_lens.set_near_far(100.0, 1800.0) - self.camera.set_lens(self.cam_lens) - self.cam_node = Globals.base.render.attach_new_node(self.camera) - - self.target = self.create_target("ShadowMap") - self.target.size = self.resolution - self.target.add_depth_attachment(bits=32) - self.target.prepare_render(self.cam_node) - - # Register shadow camera - self._pipeline.tag_mgr.register_camera("shadow", self.camera) - - def set_shader_input(self, *args): - Globals.render.set_shader_input(*args) - - def set_shader_inputs(self, **kwargs): - Globals.render.set_shader_inputs(**kwargs) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +from __future__ import division + +from panda3d.core import Vec3, Camera, OrthographicLens, PTAMat4, SamplerState + +from rpcore.globals import Globals +from rpcore.render_stage import RenderStage +from rpcore.util.generic import snap_shadow_map + + +class PSSMSceneShadowStage(RenderStage): + + """ This stage creates the shadow map which covers the whole important part + of the scene. This is required because the shadow cascades only cover the + view frustum, but many plugins (VXGI, EnvMaps) require a shadow map. """ + + required_inputs = [] + required_pipes = [] + + def __init__(self, pipeline): + RenderStage.__init__(self, pipeline) + self.resolution = 2048 + self.sun_vector = Vec3(0, 0, 1) + self.sun_distance = 10.0 + self.pta_mvp = PTAMat4.empty_array(1) + self.focus = None + + # Store last focus entirely for the purpose of being able to see + # it in the debugger + self.last_focus = None + + @property + def produced_inputs(self): + return {"PSSMSceneSunShadowMVP": self.pta_mvp} + + @property + def produced_pipes(self): + return {"PSSMSceneSunShadowMapPCF": (self.target.depth_tex, self.make_pcf_state())} + + def make_pcf_state(self): + state = SamplerState() + state.set_minfilter(SamplerState.FT_shadow) + state.set_magfilter(SamplerState.FT_shadow) + return state + + def request_focus(self, focus_point, focus_size): + self.focus = (focus_point, focus_size) + self.last_focus = self.focus + + @property + def mvp(self): + return Globals.base.render.get_transform(self.cam_node).get_mat() * \ + self.cam_lens.get_projection_mat() + + def update(self): + if self._pipeline.task_scheduler.is_scheduled("pssm_scene_shadows"): + if self.focus is None: + # When no focus is set, there is no point in rendering the shadow map + self.target.active = False + else: + focus_point, focus_size = self.focus + + self.cam_lens.set_near_far(0.0, 2 * (focus_size + self.sun_distance)) + self.cam_lens.set_film_size(2 * focus_size, 2 * focus_size) + self.cam_node.set_pos( + focus_point + self.sun_vector * (self.sun_distance + focus_size)) + self.cam_node.look_at(focus_point) + + snap_shadow_map(self.mvp, self.cam_node, self.resolution) + self.target.active = True + self.pta_mvp[0] = self.mvp + + self.focus = None + else: + self.target.active = False + + def create(self): + self.camera = Camera("PSSMSceneSunShadowCam") + self.cam_lens = OrthographicLens() + self.cam_lens.set_film_size(400, 400) + self.cam_lens.set_near_far(100.0, 1800.0) + self.camera.set_lens(self.cam_lens) + self.cam_node = Globals.base.render.attach_new_node(self.camera) + + self.target = self.create_target("ShadowMap") + self.target.size = self.resolution + self.target.add_depth_attachment(bits=32) + self.target.prepare_render(self.cam_node) + + # Register shadow camera + self._pipeline.tag_mgr.register_camera("shadow", self.camera) + + def set_shader_input(self, *args): + Globals.render.set_shader_input(*args) + + def set_shader_inputs(self, **kwargs): + Globals.render.set_shader_inputs(**kwargs) diff --git a/RenderPipelineFile/rpplugins/pssm/pssm_shadow_stage.py b/RenderPipelineFile/rpplugins/pssm/pssm_shadow_stage.py index 0148b9bd..c4906b97 100644 --- a/RenderPipelineFile/rpplugins/pssm/pssm_shadow_stage.py +++ b/RenderPipelineFile/rpplugins/pssm/pssm_shadow_stage.py @@ -1,93 +1,93 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" -from __future__ import division -from six.moves import range # pylint: disable=import-error - -from panda3d.core import SamplerState - -from rpcore.globals import Globals -from rpcore.render_stage import RenderStage - - -class PSSMShadowStage(RenderStage): - - """ This stage generates the depth-maps used for rendering PSSM """ - - required_inputs = [] - - def __init__(self, pipeline): - RenderStage.__init__(self, pipeline) - self.num_splits = 3 - self.split_resolution = 512 - self.split_regions = [] - - @property - def produced_pipes(self): - return { - "PSSMShadowAtlas": self.target.depth_tex, - "PSSMShadowAtlasPCF": (self.target.depth_tex, self.make_pcf_state()), - } - - def make_pcf_state(self): - state = SamplerState() - state.set_minfilter(SamplerState.FT_shadow) - state.set_magfilter(SamplerState.FT_shadow) - return state - - def get_shadow_tex(self): - return self.target["depth"] - - def create(self): - self.target = self.create_target("ShadowMap") - self.target.size = self.split_resolution * self.num_splits, self.split_resolution - self.target.add_depth_attachment(bits=32) - self.target.prepare_render(None) - - # Remove all unused display regions - internal_buffer = self.target.internal_buffer - internal_buffer.remove_all_display_regions() - internal_buffer.get_display_region(0).set_active(False) - internal_buffer.disable_clears() - - # Set a clear on the buffer instead on all regions - internal_buffer.set_clear_depth(1) - internal_buffer.set_clear_depth_active(True) - - # Prepare the display regions - for i in range(self.num_splits): - region = internal_buffer.make_display_region( - i / self.num_splits, - i / self.num_splits + 1 / self.num_splits, 0, 1) - region.set_sort(25 + i) - region.disable_clears() - region.set_active(True) - self.split_regions.append(region) - - def set_shader_input(self, *args): - Globals.render.set_shader_input(*args) - - def set_shader_inputs(self, **kwargs): - Globals.render.set_shader_inputs(**kwargs) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +from __future__ import division +from six.moves import range # pylint: disable=import-error + +from panda3d.core import SamplerState + +from rpcore.globals import Globals +from rpcore.render_stage import RenderStage + + +class PSSMShadowStage(RenderStage): + + """ This stage generates the depth-maps used for rendering PSSM """ + + required_inputs = [] + + def __init__(self, pipeline): + RenderStage.__init__(self, pipeline) + self.num_splits = 3 + self.split_resolution = 512 + self.split_regions = [] + + @property + def produced_pipes(self): + return { + "PSSMShadowAtlas": self.target.depth_tex, + "PSSMShadowAtlasPCF": (self.target.depth_tex, self.make_pcf_state()), + } + + def make_pcf_state(self): + state = SamplerState() + state.set_minfilter(SamplerState.FT_shadow) + state.set_magfilter(SamplerState.FT_shadow) + return state + + def get_shadow_tex(self): + return self.target["depth"] + + def create(self): + self.target = self.create_target("ShadowMap") + self.target.size = self.split_resolution * self.num_splits, self.split_resolution + self.target.add_depth_attachment(bits=32) + self.target.prepare_render(None) + + # Remove all unused display regions + internal_buffer = self.target.internal_buffer + internal_buffer.remove_all_display_regions() + internal_buffer.get_display_region(0).set_active(False) + internal_buffer.disable_clears() + + # Set a clear on the buffer instead on all regions + internal_buffer.set_clear_depth(1) + internal_buffer.set_clear_depth_active(True) + + # Prepare the display regions + for i in range(self.num_splits): + region = internal_buffer.make_display_region( + i / self.num_splits, + i / self.num_splits + 1 / self.num_splits, 0, 1) + region.set_sort(25 + i) + region.disable_clears() + region.set_active(True) + self.split_regions.append(region) + + def set_shader_input(self, *args): + Globals.render.set_shader_input(*args) + + def set_shader_inputs(self, **kwargs): + Globals.render.set_shader_inputs(**kwargs) diff --git a/RenderPipelineFile/rpplugins/pssm/pssm_stage.py b/RenderPipelineFile/rpplugins/pssm/pssm_stage.py index 4f637476..5e9df366 100644 --- a/RenderPipelineFile/rpplugins/pssm/pssm_stage.py +++ b/RenderPipelineFile/rpplugins/pssm/pssm_stage.py @@ -1,65 +1,65 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import Vec4 -from rpcore.render_stage import RenderStage - - -class PSSMStage(RenderStage): - - """ This stage uses the PSSM Shadow map to render the shadows """ - - required_inputs = [] - required_pipes = ["ShadedScene", "PSSMShadowAtlas", "GBuffer", "PSSMShadowAtlasPCF"] - - @property - def produced_pipes(self): - return {"ShadedScene": self.target.color_tex} - - def create(self): - self.enabled = True - self.target_shadows = self.create_target("FilterPSSM") - self.target_shadows.add_color_attachment(bits=(8, 0, 0, 0)) - self.target_shadows.prepare_buffer() - self.target_shadows.color_tex.set_clear_color(Vec4(0)) - - self.target = self.create_target("ApplyPSSMShadows") - self.target.add_color_attachment(bits=16) - self.target.prepare_buffer() - - self.target.set_shader_input("PrefilteredShadows", self.target_shadows.color_tex) - - def set_render_shadows(self, enabled): - """ Toggle whether to render shadows or whether to just pass through - the scene color """ - self.target_shadows.active = enabled - if enabled != self.enabled: - self.target_shadows.color_tex.clear_image() - self.enabled = enabled - - def reload_shaders(self): - self.target_shadows.shader = self.load_plugin_shader("filter_pssm_shadows.frag.glsl") - self.target.shader = self.load_plugin_shader("apply_sun_shading.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import Vec4 +from rpcore.render_stage import RenderStage + + +class PSSMStage(RenderStage): + + """ This stage uses the PSSM Shadow map to render the shadows """ + + required_inputs = [] + required_pipes = ["ShadedScene", "PSSMShadowAtlas", "GBuffer", "PSSMShadowAtlasPCF"] + + @property + def produced_pipes(self): + return {"ShadedScene": self.target.color_tex} + + def create(self): + self.enabled = True + self.target_shadows = self.create_target("FilterPSSM") + self.target_shadows.add_color_attachment(bits=(8, 0, 0, 0)) + self.target_shadows.prepare_buffer() + self.target_shadows.color_tex.set_clear_color(Vec4(0)) + + self.target = self.create_target("ApplyPSSMShadows") + self.target.add_color_attachment(bits=16) + self.target.prepare_buffer() + + self.target.set_shader_input("PrefilteredShadows", self.target_shadows.color_tex) + + def set_render_shadows(self, enabled): + """ Toggle whether to render shadows or whether to just pass through + the scene color """ + self.target_shadows.active = enabled + if enabled != self.enabled: + self.target_shadows.color_tex.clear_image() + self.enabled = enabled + + def reload_shaders(self): + self.target_shadows.shader = self.load_plugin_shader("filter_pssm_shadows.frag.glsl") + self.target.shader = self.load_plugin_shader("apply_sun_shading.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/pssm/shader/apply_sun_shading.frag.glsl b/RenderPipelineFile/rpplugins/pssm/shader/apply_sun_shading.frag.glsl index de0a8660..80714042 100644 --- a/RenderPipelineFile/rpplugins/pssm/shader/apply_sun_shading.frag.glsl +++ b/RenderPipelineFile/rpplugins/pssm/shader/apply_sun_shading.frag.glsl @@ -1,103 +1,103 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma optionNV (unroll all) - -#define USE_TIME_OF_DAY 1 -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/transforms.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "includes/lighting_pipeline.inc.glsl" -#pragma include "includes/lights.inc.glsl" -#pragma include "includes/poisson_disk.inc.glsl" -#pragma include "includes/shadows.inc.glsl" -#pragma include "includes/noise.inc.glsl" -#pragma include "includes/skin_shading.inc.glsl" - -out vec4 result; - -uniform GBufferData GBuffer; -uniform sampler2D PrefilteredShadows; -uniform sampler2D ShadedScene; - -void main() { - - vec3 sun_vector = get_sun_vector(); - - // Get current scene color - vec2 texcoord = get_texcoord(); - ivec2 coord = ivec2(gl_FragCoord.xy); - vec4 scene_color = textureLod(ShadedScene, texcoord, 0); - - float prefiltered_shadow = textureLod(PrefilteredShadows, texcoord, 0).x; - - // Early out - if (prefiltered_shadow < 1e-5) { - result = scene_color; - return; - } - - // Get the material data - Material m = unpack_material(GBuffer); - vec3 transmittance = vec3(1); - - // Compute the sun lighting - vec3 v = normalize(MainSceneData.camera_pos - m.position); - vec3 l = sun_vector; - vec3 sun_color = get_sun_color() * get_sun_color_scale(sun_vector); - - { - vec3 reflected_dir = reflect(-v, m.normal); - const float sun_angular_radius = degree_to_radians(0.54); - const float r = sin(sun_angular_radius); // Disk radius - const float d = cos(sun_angular_radius); // Distance to disk - - // Closest point to a disk (since the radius is small, this is - // a good approximation) - float DdotR = dot(sun_vector, reflected_dir); - vec3 S = reflected_dir - DdotR * sun_vector; - l = DdotR < d ? normalize(d * sun_vector + normalize(S) * r) : reflected_dir; - } - - vec3 lighting_result = apply_light(m, v, l, sun_color, 1.0, prefiltered_shadow, transmittance); - - // Backface shading for foliage, not physically correct but a nice - // approximation - float foliage_factor = m.shading_model == SHADING_MODEL_FOLIAGE ? 1.0 : 0.0; - lighting_result += foliage_factor * prefiltered_shadow * sun_color * m.basecolor * 0.12 * saturate(3.0 * dot(v, -sun_vector)); - - #if DEBUG_MODE - lighting_result *= 0; - #endif - - #if MODE_ACTIVE(PSSM_SPLITS) - lighting_result = vec3(prefiltered_shadow); - #endif - - result = max(vec4(0), scene_color) * 1 + vec4(lighting_result, 0); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma optionNV (unroll all) + +#define USE_TIME_OF_DAY 1 +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/transforms.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "includes/lighting_pipeline.inc.glsl" +#pragma include "includes/lights.inc.glsl" +#pragma include "includes/poisson_disk.inc.glsl" +#pragma include "includes/shadows.inc.glsl" +#pragma include "includes/noise.inc.glsl" +#pragma include "includes/skin_shading.inc.glsl" + +out vec4 result; + +uniform GBufferData GBuffer; +uniform sampler2D PrefilteredShadows; +uniform sampler2D ShadedScene; + +void main() { + + vec3 sun_vector = get_sun_vector(); + + // Get current scene color + vec2 texcoord = get_texcoord(); + ivec2 coord = ivec2(gl_FragCoord.xy); + vec4 scene_color = textureLod(ShadedScene, texcoord, 0); + + float prefiltered_shadow = textureLod(PrefilteredShadows, texcoord, 0).x; + + // Early out + if (prefiltered_shadow < 1e-5) { + result = scene_color; + return; + } + + // Get the material data + Material m = unpack_material(GBuffer); + vec3 transmittance = vec3(1); + + // Compute the sun lighting + vec3 v = normalize(MainSceneData.camera_pos - m.position); + vec3 l = sun_vector; + vec3 sun_color = get_sun_color() * get_sun_color_scale(sun_vector); + + { + vec3 reflected_dir = reflect(-v, m.normal); + const float sun_angular_radius = degree_to_radians(0.54); + const float r = sin(sun_angular_radius); // Disk radius + const float d = cos(sun_angular_radius); // Distance to disk + + // Closest point to a disk (since the radius is small, this is + // a good approximation) + float DdotR = dot(sun_vector, reflected_dir); + vec3 S = reflected_dir - DdotR * sun_vector; + l = DdotR < d ? normalize(d * sun_vector + normalize(S) * r) : reflected_dir; + } + + vec3 lighting_result = apply_light(m, v, l, sun_color, 1.0, prefiltered_shadow, transmittance); + + // Backface shading for foliage, not physically correct but a nice + // approximation + float foliage_factor = m.shading_model == SHADING_MODEL_FOLIAGE ? 1.0 : 0.0; + lighting_result += foliage_factor * prefiltered_shadow * sun_color * m.basecolor * 0.12 * saturate(3.0 * dot(v, -sun_vector)); + + #if DEBUG_MODE + lighting_result *= 0; + #endif + + #if MODE_ACTIVE(PSSM_SPLITS) + lighting_result = vec3(prefiltered_shadow); + #endif + + result = max(vec4(0), scene_color) * 1 + vec4(lighting_result, 0); +} diff --git a/RenderPipelineFile/rpplugins/pssm/shader/blur_esm.frag.glsl b/RenderPipelineFile/rpplugins/pssm/shader/blur_esm.frag.glsl index 7738af66..d367ce17 100644 --- a/RenderPipelineFile/rpplugins/pssm/shader/blur_esm.frag.glsl +++ b/RenderPipelineFile/rpplugins/pssm/shader/blur_esm.frag.glsl @@ -1,57 +1,57 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma optionNV (unroll all) - -// Blurs the ESM - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gaussian_weights.inc.glsl" - -uniform sampler2D SourceTex; -uniform ivec2 direction; - -out float result; - -void main() { - vec2 texsize = textureSize(SourceTex, 0).xy; - vec2 texcoord = gl_FragCoord.xy / texsize; - - float accum = 0; - - // Make sure you also adjust the offsets and weight arrays when you change the - // amount of samples - const int num_steps = 2; - - for (int i = 0; i < num_steps; ++i) { - vec2 offcoord = texcoord + vec2(opt_gaussian_offsets_2[i]) * direction / texsize; - accum += textureLod(SourceTex, offcoord, 0).x * opt_gaussian_weights_2[i]; - } - - result = accum; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma optionNV (unroll all) + +// Blurs the ESM + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gaussian_weights.inc.glsl" + +uniform sampler2D SourceTex; +uniform ivec2 direction; + +out float result; + +void main() { + vec2 texsize = textureSize(SourceTex, 0).xy; + vec2 texcoord = gl_FragCoord.xy / texsize; + + float accum = 0; + + // Make sure you also adjust the offsets and weight arrays when you change the + // amount of samples + const int num_steps = 2; + + for (int i = 0; i < num_steps; ++i) { + vec2 offcoord = texcoord + vec2(opt_gaussian_offsets_2[i]) * direction / texsize; + accum += textureLod(SourceTex, offcoord, 0).x * opt_gaussian_weights_2[i]; + } + + result = accum; +} diff --git a/RenderPipelineFile/rpplugins/pssm/shader/convert_to_esm.frag.glsl b/RenderPipelineFile/rpplugins/pssm/shader/convert_to_esm.frag.glsl index e0ad044a..29e89634 100644 --- a/RenderPipelineFile/rpplugins/pssm/shader/convert_to_esm.frag.glsl +++ b/RenderPipelineFile/rpplugins/pssm/shader/convert_to_esm.frag.glsl @@ -1,41 +1,41 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// Converts a depth texture to a ESM - -uniform sampler2D SourceTex; - -out float result; - -void main() { - ivec2 coord = ivec2(gl_FragCoord.xy); - float depth = texelFetch(SourceTex, coord, 0).x; - - const float esm_factor = 5.0; - result = exp(esm_factor * depth); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// Converts a depth texture to a ESM + +uniform sampler2D SourceTex; + +out float result; + +void main() { + ivec2 coord = ivec2(gl_FragCoord.xy); + float depth = texelFetch(SourceTex, coord, 0).x; + + const float esm_factor = 5.0; + result = exp(esm_factor * depth); +} diff --git a/RenderPipelineFile/rpplugins/pssm/shader/filter_pssm.inc.glsl b/RenderPipelineFile/rpplugins/pssm/shader/filter_pssm.inc.glsl index 1f569a75..430464a9 100644 --- a/RenderPipelineFile/rpplugins/pssm/shader/filter_pssm.inc.glsl +++ b/RenderPipelineFile/rpplugins/pssm/shader/filter_pssm.inc.glsl @@ -1,63 +1,63 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - -#pragma once - - -vec2 get_split_coord(vec2 local_coord, int split_index) { - local_coord.x = (local_coord.x + split_index) / float(GET_SETTING(pssm, split_count)); - return local_coord; -} - -float get_shadow(vec2 coord, float refz) { - #if GET_SETTING(pssm, use_pcf) - return textureLod(PSSMShadowAtlasPCF, vec3(coord, refz), 0); - #else - float depth_sample = textureLod(PSSMShadowAtlas, coord, 0).x; - return step(refz, depth_sample); - #endif -} - -float get_fixed_bias(int split) { - return GET_SETTING(pssm, fixed_bias) * 0.001 * (1 + 1.5 * split); -} - -vec3 get_pssm_split_biased_position(vec3 pos, vec3 normal, vec3 sun_vector, int split) { - - // Compute the biased position based on the normal and slope scaled - // bias. - float slope_bias = GET_SETTING(pssm, slope_bias) * 0.1 * (1 + 0.2 * split); - const float normal_bias = GET_SETTING(pssm, normal_bias) * 0.1; - - // Compute the biased position based on the normal and slope scaled - // bias. - vec3 biased_pos = get_biased_position(pos, slope_bias, normal_bias, normal, sun_vector); - - return biased_pos; -} - - +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + +#pragma once + + +vec2 get_split_coord(vec2 local_coord, int split_index) { + local_coord.x = (local_coord.x + split_index) / float(GET_SETTING(pssm, split_count)); + return local_coord; +} + +float get_shadow(vec2 coord, float refz) { + #if GET_SETTING(pssm, use_pcf) + return textureLod(PSSMShadowAtlasPCF, vec3(coord, refz), 0); + #else + float depth_sample = textureLod(PSSMShadowAtlas, coord, 0).x; + return step(refz, depth_sample); + #endif +} + +float get_fixed_bias(int split) { + return GET_SETTING(pssm, fixed_bias) * 0.001 * (1 + 1.5 * split); +} + +vec3 get_pssm_split_biased_position(vec3 pos, vec3 normal, vec3 sun_vector, int split) { + + // Compute the biased position based on the normal and slope scaled + // bias. + float slope_bias = GET_SETTING(pssm, slope_bias) * 0.1 * (1 + 0.2 * split); + const float normal_bias = GET_SETTING(pssm, normal_bias) * 0.1; + + // Compute the biased position based on the normal and slope scaled + // bias. + vec3 biased_pos = get_biased_position(pos, slope_bias, normal_bias, normal, sun_vector); + + return biased_pos; +} + + diff --git a/RenderPipelineFile/rpplugins/pssm/shader/filter_pssm_shadows.frag.glsl b/RenderPipelineFile/rpplugins/pssm/shader/filter_pssm_shadows.frag.glsl index 88c54e81..886853c1 100644 --- a/RenderPipelineFile/rpplugins/pssm/shader/filter_pssm_shadows.frag.glsl +++ b/RenderPipelineFile/rpplugins/pssm/shader/filter_pssm_shadows.frag.glsl @@ -1,291 +1,291 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma optionNV (unroll all) - -#define USE_TIME_OF_DAY 1 -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/transforms.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "includes/sampling_sequences.inc.glsl" -#pragma include "includes/shadows.inc.glsl" -#pragma include "includes/noise.inc.glsl" - -out float result; - -uniform GBufferData GBuffer; - -#if GET_SETTING(pssm, use_pcf) - uniform sampler2DShadow PSSMShadowAtlasPCF; -#endif - -uniform sampler2D PSSMShadowAtlas; - -uniform mat4 pssm_mvps[GET_SETTING(pssm, split_count)]; -uniform vec2 pssm_nearfar[GET_SETTING(pssm, split_count)]; - -#if GET_SETTING(pssm, use_distant_shadows) - uniform mat4 PSSMDistSunShadowMapMVP; - uniform sampler2D PSSMDistSunShadowMap; -#endif - -#pragma include "filter_pssm.inc.glsl" - - -void main() { - vec3 sun_vector = get_sun_vector(); - - // Get current scene color - vec2 texcoord = get_texcoord(); - ivec2 coord = ivec2(gl_FragCoord.xy); - - // Get the material data - Material m = unpack_material(GBuffer); - - // Early out, different optimizations - bool early_out = is_skybox(m) || sun_vector.z < SUN_VECTOR_HORIZON; - early_out = early_out || - (m.shading_model != SHADING_MODEL_FOLIAGE && dot(m.normal, sun_vector) <= 1e-7); - - if (early_out) { - result = 0.0; - return; - } - - // Variables to accumulate the shadows - float shadow_factor = 1.0; - - // Compute distant shadows - #if GET_SETTING(pssm, use_distant_shadows) - // #if 0 - { - // Compute the biased position based on the normal and slope scaled - // bias. - const float slope_bias = GET_SETTING(pssm, slope_bias) * 0.1 * 2; - const float normal_bias = GET_SETTING(pssm, normal_bias) * 0.1; - const float fixed_bias = 0.0005; - vec3 biased_pos = get_biased_position(m.position, - slope_bias, normal_bias, m.normal, sun_vector); - vec3 proj = project(PSSMDistSunShadowMapMVP, biased_pos); - proj.z -= fixed_bias; - - if (!out_of_unit_box(proj)) { - const float esm_factor = 5.0; - float depth_sample = textureLod(PSSMDistSunShadowMap, proj.xy, 0).x; - shadow_factor = saturate(exp(-esm_factor * proj.z) * depth_sample); - shadow_factor = pow(shadow_factor, 1e2); - } - } - #endif - - // Find lowest split in range - const int split_count = GET_SETTING(pssm, split_count); - int split = 99; - float border_bias = 0.5 - (0.5 / (1.0 + GET_SETTING(pssm, border_bias))); - - // Find the first matching split - for (int i = 0; i < split_count; ++i) { - vec3 coord = project(pssm_mvps[i], m.position); - if (coord.x >= border_bias && coord.x <= 1-border_bias && - coord.y >= border_bias && coord.y <= 1-border_bias && - coord.z >= 0.0 && coord.z <= 1.0) { - split = i; - break; - } - } - - // Compute the shadowing factor - if (split < GET_SETTING(pssm, split_count)) { - - // Get the MVP for the current split - mat4 mvp = pssm_mvps[split]; - - - float rotation = interleaved_gradient_noise( - gl_FragCoord.xy + (MainSceneData.frame_index % 4) / 3.0 ); - - // XXX: Pretty noisy - // mat2 rotation_mat = make_rotation_mat(rotation); - mat2 rotation_mat = mat2(1, 0, 0, 1); - - const float filter_radius = GET_SETTING(pssm, filter_radius) / - GET_SETTING(pssm, resolution); - - vec3 biased_pos = get_pssm_split_biased_position(m.position, m.normal, sun_vector, split); - - // Project the current pixel to the view of the light - vec3 projected = project(mvp, biased_pos); - vec2 projected_coord = get_split_coord(projected.xy, split); - - // Compute the fixed bias - float ref_depth = projected.z - get_fixed_bias(split); - - // Find filter size - vec2 filter_size = find_filter_size(mvp, sun_vector, filter_radius); - - // Increase filter size in the distance, to get better cache usage - filter_size *= (1.0 + 10.5 * distance(m.position, MainSceneData.camera_pos) / 100.0); - // vec2 filter_size = vec2(0.5 * filter_radius) * (1 / (1 + 0.7 * split)); - - #if GET_SETTING(pssm, use_pcss) - - { - /* - - PCSS Kernel - - Scan the region of the pixel for blockers, penumbra size is - amount of blockers compared to non-blockers. - - */ - - float num_blockers = 0.0; - float sum_blockers = 0.0; - - START_ITERATE_SEQUENCE(pssm, pcss_sequence, vec2 offset) - - offset = rotation_mat * offset; - - // Find depth at sample location - float sampled_depth = textureLod(PSSMShadowAtlas, - projected_coord + offset * filter_size * 4.0, 0).x; - - // Compare the depth with the pixel depth, in case its smaller, - // we found a blocker - float factor = step(sampled_depth, ref_depth); - num_blockers += factor; - sum_blockers += sampled_depth * factor; - - END_ITERATE_SEQUENCE(); - - // Examine ratio between blockers and non-blockers - float avg_blocker_depth = sum_blockers / num_blockers; - - // Penumbra size also takes average blocker depth into account - float penumbra_size = max(GET_SETTING(pssm, pcss_min_penumbra_size) * 0.03, ref_depth - avg_blocker_depth) / - ref_depth * GET_SETTING(pssm, pcss_penumbra_size); - - // Apply penumbra size - filter_size *= penumbra_size; - } - - #endif - - float local_shadow_factor = 0.0; - - - // Do the actual shadow map filtering - START_ITERATE_SEQUENCE(pssm, filter_sequence, vec2 offset) - - local_shadow_factor += get_shadow( - projected_coord + (rotation_mat * offset) * filter_size, ref_depth); - - END_ITERATE_SEQUENCE(); - NORMALIZE_SEQUENCE(pssm, filter_sequence, local_shadow_factor); - - - if (split >= GET_SETTING(pssm, split_count) - 1) { - // Smoothly fade in distant shadows - const float fade_size = 0.09; - const float depth_fade = 0.01; - float cascade_factor = 1; - cascade_factor *= saturate(projected.x / fade_size); - cascade_factor *= saturate(projected.y / fade_size); - cascade_factor *= saturate((1 - projected.x) / fade_size); - cascade_factor *= saturate((1 - projected.y) / fade_size); - cascade_factor *= saturate(projected.z / depth_fade); - cascade_factor *= saturate((1 - projected.z) / depth_fade); - shadow_factor = mix(shadow_factor, local_shadow_factor, cascade_factor); - } else { - shadow_factor = local_shadow_factor; - } - - // OPTIONAL: Transmittance - however this looks a bit buggy right now - - // might look better on surfaces like ice and so on - - // #if HAVE_PLUGIN(skin_shading) - // // Get the current split near and far planes - // vec2 split_near_far = pssm_nearfar[split]; - - // // Bias to move the position "into" the object, prevents artifacts - // float skin_border_factor = 0.025; - - // // Project the biased position to light space - // vec3 projected_skin = project(mvp, m.position - m.normal * skin_border_factor); - // vec2 projected_skin_coord = get_split_coord(projected_skin.xy, split); - // float skin_ref_depth = projected_skin.z; - - // // Get the shadow sample - // float shadow_sample = textureLod(PSSMShadowAtlas, projected_skin_coord, 0).x; - - // // Reconstruct intersection position - // // TODO: This is totally slow! Pass it as input - // mat4 inverse_mvp = inverse(mvp); - // vec3 intersection_pos = calculate_surface_pos_ortho(shadow_sample, projected_skin.xy, split_near_far.x, split_near_far.y, inverse_mvp); - - // // Get the distance the light traveled through the medium - // float distance_through_medium = distance(m.position, intersection_pos.xyz); - - // // TODO: Maybe we can remove this branch - // if (skin_ref_depth < shadow_sample) distance_through_medium = 0.0; - - // // Fetch the skin transmittance - // // transmittance = skin_transmittance(distance_through_medium); - // transmittance = vec3(1); - - // #endif - - } - - // Raymarch clouds - // CloudVoxels - #if HAVE_PLUGIN(clouds) - { - // vec3 start_coord = vec3(m.position.xy / 800.0, 0); - // vec3 end_coord = start_coord + vec3(0, 0, 1); - // const int num_steps = 32; - // vec3 step_dir = (end_coord - start_coord) / num_steps; - // float cloud_factor = 0.0; - // for (int i = 0; i < num_steps; ++i) { - // float cloud_sample = textureLod(CloudVoxels, start_coord, 0).w; - // cloud_factor += cloud_sample; - // start_coord += step_dir; - // } - // cloud_factor /= num_steps; - // cloud_factor *= 10.0; - // shadow_factor *= saturate(1.0 - cloud_factor); - } - #endif - - #if MODE_ACTIVE(PSSM_SPLITS) - float factor = float(split) / GET_SETTING(pssm, split_count); - shadow_factor = saturate(shadow_factor+0.5) * factor; - #endif - - result = shadow_factor; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma optionNV (unroll all) + +#define USE_TIME_OF_DAY 1 +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/transforms.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "includes/sampling_sequences.inc.glsl" +#pragma include "includes/shadows.inc.glsl" +#pragma include "includes/noise.inc.glsl" + +out float result; + +uniform GBufferData GBuffer; + +#if GET_SETTING(pssm, use_pcf) + uniform sampler2DShadow PSSMShadowAtlasPCF; +#endif + +uniform sampler2D PSSMShadowAtlas; + +uniform mat4 pssm_mvps[GET_SETTING(pssm, split_count)]; +uniform vec2 pssm_nearfar[GET_SETTING(pssm, split_count)]; + +#if GET_SETTING(pssm, use_distant_shadows) + uniform mat4 PSSMDistSunShadowMapMVP; + uniform sampler2D PSSMDistSunShadowMap; +#endif + +#pragma include "filter_pssm.inc.glsl" + + +void main() { + vec3 sun_vector = get_sun_vector(); + + // Get current scene color + vec2 texcoord = get_texcoord(); + ivec2 coord = ivec2(gl_FragCoord.xy); + + // Get the material data + Material m = unpack_material(GBuffer); + + // Early out, different optimizations + bool early_out = is_skybox(m) || sun_vector.z < SUN_VECTOR_HORIZON; + early_out = early_out || + (m.shading_model != SHADING_MODEL_FOLIAGE && dot(m.normal, sun_vector) <= 1e-7); + + if (early_out) { + result = 0.0; + return; + } + + // Variables to accumulate the shadows + float shadow_factor = 1.0; + + // Compute distant shadows + #if GET_SETTING(pssm, use_distant_shadows) + // #if 0 + { + // Compute the biased position based on the normal and slope scaled + // bias. + const float slope_bias = GET_SETTING(pssm, slope_bias) * 0.1 * 2; + const float normal_bias = GET_SETTING(pssm, normal_bias) * 0.1; + const float fixed_bias = 0.0005; + vec3 biased_pos = get_biased_position(m.position, + slope_bias, normal_bias, m.normal, sun_vector); + vec3 proj = project(PSSMDistSunShadowMapMVP, biased_pos); + proj.z -= fixed_bias; + + if (!out_of_unit_box(proj)) { + const float esm_factor = 5.0; + float depth_sample = textureLod(PSSMDistSunShadowMap, proj.xy, 0).x; + shadow_factor = saturate(exp(-esm_factor * proj.z) * depth_sample); + shadow_factor = pow(shadow_factor, 1e2); + } + } + #endif + + // Find lowest split in range + const int split_count = GET_SETTING(pssm, split_count); + int split = 99; + float border_bias = 0.5 - (0.5 / (1.0 + GET_SETTING(pssm, border_bias))); + + // Find the first matching split + for (int i = 0; i < split_count; ++i) { + vec3 coord = project(pssm_mvps[i], m.position); + if (coord.x >= border_bias && coord.x <= 1-border_bias && + coord.y >= border_bias && coord.y <= 1-border_bias && + coord.z >= 0.0 && coord.z <= 1.0) { + split = i; + break; + } + } + + // Compute the shadowing factor + if (split < GET_SETTING(pssm, split_count)) { + + // Get the MVP for the current split + mat4 mvp = pssm_mvps[split]; + + + float rotation = interleaved_gradient_noise( + gl_FragCoord.xy + (MainSceneData.frame_index % 4) / 3.0 ); + + // XXX: Pretty noisy + // mat2 rotation_mat = make_rotation_mat(rotation); + mat2 rotation_mat = mat2(1, 0, 0, 1); + + const float filter_radius = GET_SETTING(pssm, filter_radius) / + GET_SETTING(pssm, resolution); + + vec3 biased_pos = get_pssm_split_biased_position(m.position, m.normal, sun_vector, split); + + // Project the current pixel to the view of the light + vec3 projected = project(mvp, biased_pos); + vec2 projected_coord = get_split_coord(projected.xy, split); + + // Compute the fixed bias + float ref_depth = projected.z - get_fixed_bias(split); + + // Find filter size + vec2 filter_size = find_filter_size(mvp, sun_vector, filter_radius); + + // Increase filter size in the distance, to get better cache usage + filter_size *= (1.0 + 10.5 * distance(m.position, MainSceneData.camera_pos) / 100.0); + // vec2 filter_size = vec2(0.5 * filter_radius) * (1 / (1 + 0.7 * split)); + + #if GET_SETTING(pssm, use_pcss) + + { + /* + + PCSS Kernel + + Scan the region of the pixel for blockers, penumbra size is + amount of blockers compared to non-blockers. + + */ + + float num_blockers = 0.0; + float sum_blockers = 0.0; + + START_ITERATE_SEQUENCE(pssm, pcss_sequence, vec2 offset) + + offset = rotation_mat * offset; + + // Find depth at sample location + float sampled_depth = textureLod(PSSMShadowAtlas, + projected_coord + offset * filter_size * 4.0, 0).x; + + // Compare the depth with the pixel depth, in case its smaller, + // we found a blocker + float factor = step(sampled_depth, ref_depth); + num_blockers += factor; + sum_blockers += sampled_depth * factor; + + END_ITERATE_SEQUENCE(); + + // Examine ratio between blockers and non-blockers + float avg_blocker_depth = sum_blockers / num_blockers; + + // Penumbra size also takes average blocker depth into account + float penumbra_size = max(GET_SETTING(pssm, pcss_min_penumbra_size) * 0.03, ref_depth - avg_blocker_depth) / + ref_depth * GET_SETTING(pssm, pcss_penumbra_size); + + // Apply penumbra size + filter_size *= penumbra_size; + } + + #endif + + float local_shadow_factor = 0.0; + + + // Do the actual shadow map filtering + START_ITERATE_SEQUENCE(pssm, filter_sequence, vec2 offset) + + local_shadow_factor += get_shadow( + projected_coord + (rotation_mat * offset) * filter_size, ref_depth); + + END_ITERATE_SEQUENCE(); + NORMALIZE_SEQUENCE(pssm, filter_sequence, local_shadow_factor); + + + if (split >= GET_SETTING(pssm, split_count) - 1) { + // Smoothly fade in distant shadows + const float fade_size = 0.09; + const float depth_fade = 0.01; + float cascade_factor = 1; + cascade_factor *= saturate(projected.x / fade_size); + cascade_factor *= saturate(projected.y / fade_size); + cascade_factor *= saturate((1 - projected.x) / fade_size); + cascade_factor *= saturate((1 - projected.y) / fade_size); + cascade_factor *= saturate(projected.z / depth_fade); + cascade_factor *= saturate((1 - projected.z) / depth_fade); + shadow_factor = mix(shadow_factor, local_shadow_factor, cascade_factor); + } else { + shadow_factor = local_shadow_factor; + } + + // OPTIONAL: Transmittance - however this looks a bit buggy right now - + // might look better on surfaces like ice and so on + + // #if HAVE_PLUGIN(skin_shading) + // // Get the current split near and far planes + // vec2 split_near_far = pssm_nearfar[split]; + + // // Bias to move the position "into" the object, prevents artifacts + // float skin_border_factor = 0.025; + + // // Project the biased position to light space + // vec3 projected_skin = project(mvp, m.position - m.normal * skin_border_factor); + // vec2 projected_skin_coord = get_split_coord(projected_skin.xy, split); + // float skin_ref_depth = projected_skin.z; + + // // Get the shadow sample + // float shadow_sample = textureLod(PSSMShadowAtlas, projected_skin_coord, 0).x; + + // // Reconstruct intersection position + // // TODO: This is totally slow! Pass it as input + // mat4 inverse_mvp = inverse(mvp); + // vec3 intersection_pos = calculate_surface_pos_ortho(shadow_sample, projected_skin.xy, split_near_far.x, split_near_far.y, inverse_mvp); + + // // Get the distance the light traveled through the medium + // float distance_through_medium = distance(m.position, intersection_pos.xyz); + + // // TODO: Maybe we can remove this branch + // if (skin_ref_depth < shadow_sample) distance_through_medium = 0.0; + + // // Fetch the skin transmittance + // // transmittance = skin_transmittance(distance_through_medium); + // transmittance = vec3(1); + + // #endif + + } + + // Raymarch clouds + // CloudVoxels + #if HAVE_PLUGIN(clouds) + { + // vec3 start_coord = vec3(m.position.xy / 800.0, 0); + // vec3 end_coord = start_coord + vec3(0, 0, 1); + // const int num_steps = 32; + // vec3 step_dir = (end_coord - start_coord) / num_steps; + // float cloud_factor = 0.0; + // for (int i = 0; i < num_steps; ++i) { + // float cloud_sample = textureLod(CloudVoxels, start_coord, 0).w; + // cloud_factor += cloud_sample; + // start_coord += step_dir; + // } + // cloud_factor /= num_steps; + // cloud_factor *= 10.0; + // shadow_factor *= saturate(1.0 - cloud_factor); + } + #endif + + #if MODE_ACTIVE(PSSM_SPLITS) + float factor = float(split) / GET_SETTING(pssm, split_count); + shadow_factor = saturate(shadow_factor+0.5) * factor; + #endif + + result = shadow_factor; +} diff --git a/RenderPipelineFile/rpplugins/scattering/config.yaml b/RenderPipelineFile/rpplugins/scattering/config.yaml index 88358b1a..14d7fad2 100644 --- a/RenderPipelineFile/rpplugins/scattering/config.yaml +++ b/RenderPipelineFile/rpplugins/scattering/config.yaml @@ -1,142 +1,142 @@ - -settings: !!omap - - - scattering_method: - type: enum - values: ["eric_bruneton", "hosek_wilkie"] - default: "eric_bruneton" - label: Scattering Method - description: > - Chooses the scattering algorithm. eric_bruneton is the Precomputed - Atmospheric Scattering method proposed by Eric Bruneton. hosek_wilkie - denotes the sky model proposed by Lukas Hosek and Alexander Wilkie. - - - ground_reflectance: - display_if: {scattering_method: "eric_bruneton"} - type: float - range: [0.01, 1.0] - default: 0.1 - shader_runtime: true - label: Ground Reflectance - description: > - Average ground reflectance, controls how much light the earth reflects. - - - rayleigh_factor: - display_if: {scattering_method: "eric_bruneton"} - type: float - range: [0.05, 1.0] - default: 0.5 - shader_runtime: true - label: Rayleigh factor - description: > - Controls the the rayleigh factor, lower values cause a more bluish sky. - - - rayleigh_height_scale: - display_if: {scattering_method: "eric_bruneton"} - type: float - range: [1.0, 16.0] - default: 8.0 - shader_runtime: true - label: Rayleigh height scale - description: > - Rayleigh height scale, higher values cause a more bluish sky. - - - mie_height_scale: - display_if: {scattering_method: "eric_bruneton"} - type: float - range: [0.5, 5.0] - default: 1.3 - shader_runtime: true - label: Mie height scale - description: > - Mie height scale, higher values cause a more reddish sky. - - - mie_phase_factor: - display_if: {scattering_method: "eric_bruneton"} - type: float - range: [0.1, 0.9999] - default: 0.3 - shader_runtime: true - label: Mie Phase - description: > - Mie phase factor, this controls the apperance of the sun highlight - in the sky, higher values produce a smaller (but brighter) sun-shape. - - - beta_mie_scattering: - display_if: {scattering_method: "eric_bruneton"} - type: float - range: [1, 30] - default: 4 - shader_runtime: true - label: Beta Mie Scattering - description: > - Beta Mie Scattering factor - - - enable_godrays: - type: bool - default: false - label: Enable Godrays (experimental!) - description: > - This enables the experimental support for godrays. - - - atmosphere_start: - type: float - range: [25.0, 2000.0] - default: 100 - label: Atmosphere Start - shader_runtime: true - description: > - Start of the the atmosphere, this controls where - z = 0 lies inside the atmosphere. A value of 0 would mean - sea level for example. - - -daytime_settings: !!omap - - - sun_intensity: - type: scalar - unit: klux - range: [0.0, 150.0] - default: 100.0 - label: Sun Intensity - description: > - Controls the intensity of the sun in K-LUX (1000 lux) - - - sun_color: - type: color - default: [126, 255, 100] - label: Sun Color - description: > - Color of the sun, only affects direct lighting, has no impact on scattering. - - - sun_azimuth: - type: scalar - unit: degree - range: [0.0, 360.0] - default: 180 - label: Sun Azimuth - description: > - Horizontal angle of the sun, this basically controls from which - direction the sun comes - - - sun_altitude: - type: scalar - unit: degree - range: [-60, 60.0] - default: 30 - label: Sun Altidude - description: > - Vertical angle of the sun, this controls the height of the sun. - - - extinction: - type: scalar - unit: meter - range: [0.5, 50000.0] - default: 2000 - label: Extinction - logarithmic_factor: 2.0 - description: > - Extinction factor, this controls how much the fog darkens the - objects in the distance. - - + +settings: !!omap + + - scattering_method: + type: enum + values: ["eric_bruneton", "hosek_wilkie"] + default: "eric_bruneton" + label: Scattering Method + description: > + Chooses the scattering algorithm. eric_bruneton is the Precomputed + Atmospheric Scattering method proposed by Eric Bruneton. hosek_wilkie + denotes the sky model proposed by Lukas Hosek and Alexander Wilkie. + + - ground_reflectance: + display_if: {scattering_method: "eric_bruneton"} + type: float + range: [0.01, 1.0] + default: 0.1 + shader_runtime: true + label: Ground Reflectance + description: > + Average ground reflectance, controls how much light the earth reflects. + + - rayleigh_factor: + display_if: {scattering_method: "eric_bruneton"} + type: float + range: [0.05, 1.0] + default: 0.5 + shader_runtime: true + label: Rayleigh factor + description: > + Controls the the rayleigh factor, lower values cause a more bluish sky. + + - rayleigh_height_scale: + display_if: {scattering_method: "eric_bruneton"} + type: float + range: [1.0, 16.0] + default: 8.0 + shader_runtime: true + label: Rayleigh height scale + description: > + Rayleigh height scale, higher values cause a more bluish sky. + + - mie_height_scale: + display_if: {scattering_method: "eric_bruneton"} + type: float + range: [0.5, 5.0] + default: 1.3 + shader_runtime: true + label: Mie height scale + description: > + Mie height scale, higher values cause a more reddish sky. + + - mie_phase_factor: + display_if: {scattering_method: "eric_bruneton"} + type: float + range: [0.1, 0.9999] + default: 0.3 + shader_runtime: true + label: Mie Phase + description: > + Mie phase factor, this controls the apperance of the sun highlight + in the sky, higher values produce a smaller (but brighter) sun-shape. + + - beta_mie_scattering: + display_if: {scattering_method: "eric_bruneton"} + type: float + range: [1, 30] + default: 4 + shader_runtime: true + label: Beta Mie Scattering + description: > + Beta Mie Scattering factor + + - enable_godrays: + type: bool + default: false + label: Enable Godrays (experimental!) + description: > + This enables the experimental support for godrays. + + - atmosphere_start: + type: float + range: [25.0, 2000.0] + default: 100 + label: Atmosphere Start + shader_runtime: true + description: > + Start of the the atmosphere, this controls where + z = 0 lies inside the atmosphere. A value of 0 would mean + sea level for example. + + +daytime_settings: !!omap + + - sun_intensity: + type: scalar + unit: klux + range: [0.0, 150.0] + default: 100.0 + label: Sun Intensity + description: > + Controls the intensity of the sun in K-LUX (1000 lux) + + - sun_color: + type: color + default: [126, 255, 100] + label: Sun Color + description: > + Color of the sun, only affects direct lighting, has no impact on scattering. + + - sun_azimuth: + type: scalar + unit: degree + range: [0.0, 360.0] + default: 180 + label: Sun Azimuth + description: > + Horizontal angle of the sun, this basically controls from which + direction the sun comes + + - sun_altitude: + type: scalar + unit: degree + range: [-60, 60.0] + default: 30 + label: Sun Altidude + description: > + Vertical angle of the sun, this controls the height of the sun. + + - extinction: + type: scalar + unit: meter + range: [0.5, 50000.0] + default: 2000 + label: Extinction + logarithmic_factor: 2.0 + description: > + Extinction factor, this controls how much the fog darkens the + objects in the distance. + + diff --git a/RenderPipelineFile/rpplugins/scattering/godray_stage.py b/RenderPipelineFile/rpplugins/scattering/godray_stage.py index 82797ba3..50bd4086 100644 --- a/RenderPipelineFile/rpplugins/scattering/godray_stage.py +++ b/RenderPipelineFile/rpplugins/scattering/godray_stage.py @@ -1,49 +1,49 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import division - -from rpcore.render_stage import RenderStage - - -class GodrayStage(RenderStage): - - """ This stage renders the suns godrays """ - - required_inputs = [] - required_pipes = ["ShadedScene", "GBuffer"] - - @property - def produced_pipes(self): - return {"ShadedScene": self._target.color_tex} - - def create(self): - self._target = self.create_target("ComputeGodrays") - self._target.add_color_attachment(bits=16) - self._target.prepare_buffer() - - def reload_shaders(self): - self._target.shader = self.load_plugin_shader("compute_godrays.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import division + +from rpcore.render_stage import RenderStage + + +class GodrayStage(RenderStage): + + """ This stage renders the suns godrays """ + + required_inputs = [] + required_pipes = ["ShadedScene", "GBuffer"] + + @property + def produced_pipes(self): + return {"ShadedScene": self._target.color_tex} + + def create(self): + self._target = self.create_target("ComputeGodrays") + self._target.add_color_attachment(bits=16) + self._target.prepare_buffer() + + def reload_shaders(self): + self._target.shader = self.load_plugin_shader("compute_godrays.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/scattering/plugin.py b/RenderPipelineFile/rpplugins/scattering/plugin.py index e864f1ae..82993dc9 100644 --- a/RenderPipelineFile/rpplugins/scattering/plugin.py +++ b/RenderPipelineFile/rpplugins/scattering/plugin.py @@ -1,89 +1,89 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -import math - -from panda3d.core import Vec3 - -from rpcore.pluginbase.base_plugin import BasePlugin - -from .scattering_stage import ScatteringStage -from .scattering_envmap_stage import ScatteringEnvmapStage -from .godray_stage import GodrayStage - - -class Plugin(BasePlugin): - name = "Atmospheric Scattering" - author = "tobspr " - description = ("This plugin adds support for Atmospheric Scattering, and a " - "single sun, based on the work from Eric Bruneton. It also " - "adds support for atmospheric fog.") - version = "1.2" - - def on_pipeline_created(self): - self.scattering_model.load() - self.scattering_model.compute() - - def on_stage_setup(self): - self.display_stage = self.create_stage(ScatteringStage) - self.envmap_stage = self.create_stage(ScatteringEnvmapStage) - - if self.get_setting("enable_godrays"): - self.godray_stage = self.create_stage(GodrayStage) - - # Load scattering method - method = self.get_setting("scattering_method") - - self.debug("Loading scattering method for '" + method + "'") - - if method == "eric_bruneton": - from .scattering_methods import ScatteringMethodEricBruneton - self.scattering_model = ScatteringMethodEricBruneton(self) - elif method == "hosek_wilkie": - from .scattering_methods import ScatteringMethodHosekWilkie - self.scattering_model = ScatteringMethodHosekWilkie(self) # noqa # pylint: disable=redefined-variable-type - else: - self.error("Unrecognized scattering method!") - - @property - def sun_vector(self): - """ Returns the sun vector """ - sun_altitude = self.get_daytime_setting("sun_altitude") - sun_azimuth = self.get_daytime_setting("sun_azimuth") - theta = (90 - sun_altitude) / 180.0 * math.pi - phi = sun_azimuth / 180.0 * math.pi - sun_vector = Vec3( - math.sin(theta) * math.cos(phi), - math.sin(theta) * math.sin(phi), - math.cos(theta)) - return sun_vector - - def on_pre_render_update(self): - self.envmap_stage.active = self._pipeline.task_scheduler.is_scheduled( - "scattering_update_envmap") - - def on_shader_reload(self): - self.scattering_model.compute() +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +import math + +from panda3d.core import Vec3 + +from rpcore.pluginbase.base_plugin import BasePlugin + +from .scattering_stage import ScatteringStage +from .scattering_envmap_stage import ScatteringEnvmapStage +from .godray_stage import GodrayStage + + +class Plugin(BasePlugin): + name = "Atmospheric Scattering" + author = "tobspr " + description = ("This plugin adds support for Atmospheric Scattering, and a " + "single sun, based on the work from Eric Bruneton. It also " + "adds support for atmospheric fog.") + version = "1.2" + + def on_pipeline_created(self): + self.scattering_model.load() + self.scattering_model.compute() + + def on_stage_setup(self): + self.display_stage = self.create_stage(ScatteringStage) + self.envmap_stage = self.create_stage(ScatteringEnvmapStage) + + if self.get_setting("enable_godrays"): + self.godray_stage = self.create_stage(GodrayStage) + + # Load scattering method + method = self.get_setting("scattering_method") + + self.debug("Loading scattering method for '" + method + "'") + + if method == "eric_bruneton": + from .scattering_methods import ScatteringMethodEricBruneton + self.scattering_model = ScatteringMethodEricBruneton(self) + elif method == "hosek_wilkie": + from .scattering_methods import ScatteringMethodHosekWilkie + self.scattering_model = ScatteringMethodHosekWilkie(self) # noqa # pylint: disable=redefined-variable-type + else: + self.error("Unrecognized scattering method!") + + @property + def sun_vector(self): + """ Returns the sun vector """ + sun_altitude = self.get_daytime_setting("sun_altitude") + sun_azimuth = self.get_daytime_setting("sun_azimuth") + theta = (90 - sun_altitude) / 180.0 * math.pi + phi = sun_azimuth / 180.0 * math.pi + sun_vector = Vec3( + math.sin(theta) * math.cos(phi), + math.sin(theta) * math.sin(phi), + math.cos(theta)) + return sun_vector + + def on_pre_render_update(self): + self.envmap_stage.active = self._pipeline.task_scheduler.is_scheduled( + "scattering_update_envmap") + + def on_shader_reload(self): + self.scattering_model.compute() diff --git a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/.gitignore b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/.gitignore index a2c97840..197e239b 100644 --- a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/.gitignore +++ b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/.gitignore @@ -1,19 +1,19 @@ - -# Prefab gitignore which can be used by the user to ignore the files generated -# by the module builder - -*.pyd -*.pyc -*.so -*.pdb - -# Various output names -win_*/ -linux_*/ -source/interrogate* - -# Source and script files -scripts/ -CMakeLists.txt -build.py -LICENSE + +# Prefab gitignore which can be used by the user to ignore the files generated +# by the module builder + +*.pyd +*.pyc +*.so +*.pdb + +# Various output names +win_*/ +linux_*/ +source/interrogate* + +# Source and script files +scripts/ +CMakeLists.txt +build.py +LICENSE diff --git a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/README.md b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/README.md index 638ea42d..b3ae6085 100644 --- a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/README.md +++ b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/README.md @@ -1,70 +1,70 @@ - -# Hosek & Wilkie Skylight Model - -This uses the precomputed scattering model proposed by: - -http://cgg.mff.cuni.cz/projects/SkylightModelling/ - -It generates a LUT evaluating the model offline, which then -will be used in the scattering shader later on. - -### Usage - -In case you want to generate the lookup texture yourself, you need to first -compile the code (see below), and then run the program. -The program will ask you for a turbidity and ground albedo, using -`turbidity=3.0` and `groundAlbedo=0.1` is a good starting point. - -### Compilation - -Just run `python update_module_builder.py`, after that you should see a `build.py`, -run it with `python build.py` to generate the module. When the compilation succeeded, -run `python generate_table.py` to generate the LUT. After that you can use the -scattering method in the pipeline. - - -### License - -``` - - "An Analytic Model for Full Spectral Sky-Dome Radiance" - "Adding a Solar Radiance Function to the Hosek Skylight Model" - - both by - - Lukas Hosek and Alexander Wilkie - Charles University in Prague, Czech Republic - - Version: 1.4a, February 22nd, 2013 - - -This source is published under the following 3-clause BSD license. - -Copyright (c) 2012, Lukas Hosek and Alexander Wilkie -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * None of the names of the contributors may be used to endorse or promote - products derived from this software without specific prior written - permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -``` - + +# Hosek & Wilkie Skylight Model + +This uses the precomputed scattering model proposed by: + +http://cgg.mff.cuni.cz/projects/SkylightModelling/ + +It generates a LUT evaluating the model offline, which then +will be used in the scattering shader later on. + +### Usage + +In case you want to generate the lookup texture yourself, you need to first +compile the code (see below), and then run the program. +The program will ask you for a turbidity and ground albedo, using +`turbidity=3.0` and `groundAlbedo=0.1` is a good starting point. + +### Compilation + +Just run `python update_module_builder.py`, after that you should see a `build.py`, +run it with `python build.py` to generate the module. When the compilation succeeded, +run `python generate_table.py` to generate the LUT. After that you can use the +scattering method in the pipeline. + + +### License + +``` + + "An Analytic Model for Full Spectral Sky-Dome Radiance" + "Adding a Solar Radiance Function to the Hosek Skylight Model" + + both by + + Lukas Hosek and Alexander Wilkie + Charles University in Prague, Czech Republic + + Version: 1.4a, February 22nd, 2013 + + +This source is published under the following 3-clause BSD license. + +Copyright (c) 2012, Lukas Hosek and Alexander Wilkie +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * None of the names of the contributors may be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +``` + diff --git a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/config.ini b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/config.ini index e31ca541..6d049657 100644 --- a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/config.ini +++ b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/config.ini @@ -1,4 +1,4 @@ -generate_pdb=0 -module_name=HSWKScattering -use_lib_eigen=1 -vc_version=Visual Studio 10 2010 +generate_pdb=0 +module_name=HSWKScattering +use_lib_eigen=1 +vc_version=Visual Studio 10 2010 diff --git a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/generate_table.py b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/generate_table.py index 16aac24f..c19473c5 100644 --- a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/generate_table.py +++ b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/generate_table.py @@ -1,27 +1,27 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" -import HSWKScattering -HSWKScattering.generate_table() +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +import HSWKScattering +HSWKScattering.generate_table() diff --git a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/ArHosekSkyModel.cpp b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/ArHosekSkyModel.cpp index 7aa517b7..e0016026 100644 --- a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/ArHosekSkyModel.cpp +++ b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/ArHosekSkyModel.cpp @@ -1,503 +1,503 @@ -/* -This source is published under the following 3-clause BSD license. - -Copyright (c) 2012 - 2013, Lukas Hosek and Alexander Wilkie -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * None of the names of the contributors may be used to endorse or promote - products derived from this software without specific prior written - permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -/* ============================================================================ - -This file is part of a sample implementation of the analytical skylight and -solar radiance models presented in the SIGGRAPH 2012 paper - - - "An Analytic Model for Full Spectral Sky-Dome Radiance" - -and the 2013 IEEE CG&A paper - - "Adding a Solar Radiance Function to the Hosek Skylight Model" - - both by - - Lukas Hosek and Alexander Wilkie - Charles University in Prague, Czech Republic - - - Version: 1.4a, February 22nd, 2013 - -Version history: - -1.4a February 22nd, 2013 - Removed unnecessary and counter-intuitive solar radius parameters - from the interface of the colourspace sky dome initialisation functions. - -1.4 February 11th, 2013 - Fixed a bug which caused the relative brightness of the solar disc - and the sky dome to be off by a factor of about 6. The sun was too - bright: this affected both normal and alien sun scenarios. The - coefficients of the solar radiance function were changed to fix this. - -1.3 January 21st, 2013 (not released to the public) - Added support for solar discs that are not exactly the same size as - the terrestrial sun. Also added support for suns with a different - emission spectrum ("Alien World" functionality). - -1.2a December 18th, 2012 - Fixed a mistake and some inaccuracies in the solar radiance function - explanations found in ArHosekSkyModel.h. The actual source code is - unchanged compared to version 1.2. - -1.2 December 17th, 2012 - Native RGB data and a solar radiance function that matches the turbidity - conditions were added. - -1.1 September 2012 - The coefficients of the spectral model are now scaled so that the output - is given in physical units: W / (m^-2 * sr * nm). Also, the output of the - XYZ model is now no longer scaled to the range [0...1]. Instead, it is - the result of a simple conversion from spectral data via the CIE 2 degree - standard observer matching functions. Therefore, after multiplication - with 683 lm / W, the Y channel now corresponds to luminance in lm. - -1.0 May 11th, 2012 - Initial release. - - -Please visit http://cgg.mff.cuni.cz/projects/SkylightModelling/ to check if -an updated version of this code has been published! - -============================================================================ */ - -/* - -All instructions on how to use this code are in the accompanying header file. - -*/ - -#include "ArHosekSkyModel.h" -#include "ArHosekSkyModelData_CIEXYZ.data" -#include "ArHosekSkyModelData_RGB.data" -#include -#include -#include -#include - -// Some macro definitions that occur elsewhere in ART, and that have to be -// replicated to make this a stand-alone module. - -#ifndef NIL -#define NIL 0 -#endif - -#ifndef MATH_PI -#define MATH_PI 3.141592653589793 -#endif - -#ifndef MATH_DEG_TO_RAD -#define MATH_DEG_TO_RAD ( MATH_PI / 180.0 ) -#endif - -#ifndef MATH_RAD_TO_DEG -#define MATH_RAD_TO_DEG ( 180.0 / MATH_PI ) -#endif - -#ifndef DEGREES -#define DEGREES * MATH_DEG_TO_RAD -#endif - -#ifndef TERRESTRIAL_SOLAR_RADIUS -#define TERRESTRIAL_SOLAR_RADIUS ( ( 0.51 DEGREES ) / 2.0 ) -#endif - -#ifndef ALLOC -#define ALLOC(_struct) ((_struct *)malloc(sizeof(_struct))) -#endif - -// internal definitions - -typedef double *ArHosekSkyModel_Dataset; -typedef double *ArHosekSkyModel_Radiance_Dataset; - -// internal functions - -void ArHosekSkyModel_CookConfiguration( - ArHosekSkyModel_Dataset dataset, - ArHosekSkyModelConfiguration config, - double turbidity, - double albedo, - double solar_elevation - ) -{ - double * elev_matrix; - - int int_turbidity = (int)turbidity; - double turbidity_rem = turbidity - (double)int_turbidity; - - solar_elevation = pow(solar_elevation / (MATH_PI / 2.0), (1.0 / 3.0)); - - // alb 0 low turb - - elev_matrix = dataset + ( 9 * 6 * (int_turbidity-1) ); - - - for( unsigned int i = 0; i < 9; ++i ) - { - //(1-t).^3* A1 + 3*(1-t).^2.*t * A2 + 3*(1-t) .* t .^ 2 * A3 + t.^3 * A4; - config[i] = - (1.0-albedo) * (1.0 - turbidity_rem) - * ( pow(1.0-solar_elevation, 5.0) * elev_matrix[i] + - 5.0 * pow(1.0-solar_elevation, 4.0) * solar_elevation * elev_matrix[i+9] + - 10.0*pow(1.0-solar_elevation, 3.0)*pow(solar_elevation, 2.0) * elev_matrix[i+18] + - 10.0*pow(1.0-solar_elevation, 2.0)*pow(solar_elevation, 3.0) * elev_matrix[i+27] + - 5.0*(1.0-solar_elevation)*pow(solar_elevation, 4.0) * elev_matrix[i+36] + - pow(solar_elevation, 5.0) * elev_matrix[i+45]); - } - - // alb 1 low turb - elev_matrix = dataset + (9*6*10 + 9*6*(int_turbidity-1)); - for(unsigned int i = 0; i < 9; ++i) - { - //(1-t).^3* A1 + 3*(1-t).^2.*t * A2 + 3*(1-t) .* t .^ 2 * A3 + t.^3 * A4; - config[i] += - (albedo) * (1.0 - turbidity_rem) - * ( pow(1.0-solar_elevation, 5.0) * elev_matrix[i] + - 5.0 * pow(1.0-solar_elevation, 4.0) * solar_elevation * elev_matrix[i+9] + - 10.0*pow(1.0-solar_elevation, 3.0)*pow(solar_elevation, 2.0) * elev_matrix[i+18] + - 10.0*pow(1.0-solar_elevation, 2.0)*pow(solar_elevation, 3.0) * elev_matrix[i+27] + - 5.0*(1.0-solar_elevation)*pow(solar_elevation, 4.0) * elev_matrix[i+36] + - pow(solar_elevation, 5.0) * elev_matrix[i+45]); - } - - if(int_turbidity == 10) - return; - - // alb 0 high turb - elev_matrix = dataset + (9*6*(int_turbidity)); - for(unsigned int i = 0; i < 9; ++i) - { - //(1-t).^3* A1 + 3*(1-t).^2.*t * A2 + 3*(1-t) .* t .^ 2 * A3 + t.^3 * A4; - config[i] += - (1.0-albedo) * (turbidity_rem) - * ( pow(1.0-solar_elevation, 5.0) * elev_matrix[i] + - 5.0 * pow(1.0-solar_elevation, 4.0) * solar_elevation * elev_matrix[i+9] + - 10.0*pow(1.0-solar_elevation, 3.0)*pow(solar_elevation, 2.0) * elev_matrix[i+18] + - 10.0*pow(1.0-solar_elevation, 2.0)*pow(solar_elevation, 3.0) * elev_matrix[i+27] + - 5.0*(1.0-solar_elevation)*pow(solar_elevation, 4.0) * elev_matrix[i+36] + - pow(solar_elevation, 5.0) * elev_matrix[i+45]); - } - - // alb 1 high turb - elev_matrix = dataset + (9*6*10 + 9*6*(int_turbidity)); - for(unsigned int i = 0; i < 9; ++i) - { - //(1-t).^3* A1 + 3*(1-t).^2.*t * A2 + 3*(1-t) .* t .^ 2 * A3 + t.^3 * A4; - config[i] += - (albedo) * (turbidity_rem) - * ( pow(1.0-solar_elevation, 5.0) * elev_matrix[i] + - 5.0 * pow(1.0-solar_elevation, 4.0) * solar_elevation * elev_matrix[i+9] + - 10.0*pow(1.0-solar_elevation, 3.0)*pow(solar_elevation, 2.0) * elev_matrix[i+18] + - 10.0*pow(1.0-solar_elevation, 2.0)*pow(solar_elevation, 3.0) * elev_matrix[i+27] + - 5.0*(1.0-solar_elevation)*pow(solar_elevation, 4.0) * elev_matrix[i+36] + - pow(solar_elevation, 5.0) * elev_matrix[i+45]); - } -} - -double ArHosekSkyModel_CookRadianceConfiguration( - ArHosekSkyModel_Radiance_Dataset dataset, - double turbidity, - double albedo, - double solar_elevation - ) -{ - double* elev_matrix; - - int int_turbidity = (int)turbidity; - double turbidity_rem = turbidity - (double)int_turbidity; - double res; - solar_elevation = pow(solar_elevation / (MATH_PI / 2.0), (1.0 / 3.0)); - - // alb 0 low turb - elev_matrix = dataset + (6*(int_turbidity-1)); - //(1-t).^3* A1 + 3*(1-t).^2.*t * A2 + 3*(1-t) .* t .^ 2 * A3 + t.^3 * A4; - res = (1.0-albedo) * (1.0 - turbidity_rem) * - ( pow(1.0-solar_elevation, 5.0) * elev_matrix[0] + - 5.0*pow(1.0-solar_elevation, 4.0)*solar_elevation * elev_matrix[1] + - 10.0*pow(1.0-solar_elevation, 3.0)*pow(solar_elevation, 2.0) * elev_matrix[2] + - 10.0*pow(1.0-solar_elevation, 2.0)*pow(solar_elevation, 3.0) * elev_matrix[3] + - 5.0*(1.0-solar_elevation)*pow(solar_elevation, 4.0) * elev_matrix[4] + - pow(solar_elevation, 5.0) * elev_matrix[5]); - - // alb 1 low turb - elev_matrix = dataset + (6*10 + 6*(int_turbidity-1)); - //(1-t).^3* A1 + 3*(1-t).^2.*t * A2 + 3*(1-t) .* t .^ 2 * A3 + t.^3 * A4; - res += (albedo) * (1.0 - turbidity_rem) * - ( pow(1.0-solar_elevation, 5.0) * elev_matrix[0] + - 5.0*pow(1.0-solar_elevation, 4.0)*solar_elevation * elev_matrix[1] + - 10.0*pow(1.0-solar_elevation, 3.0)*pow(solar_elevation, 2.0) * elev_matrix[2] + - 10.0*pow(1.0-solar_elevation, 2.0)*pow(solar_elevation, 3.0) * elev_matrix[3] + - 5.0*(1.0-solar_elevation)*pow(solar_elevation, 4.0) * elev_matrix[4] + - pow(solar_elevation, 5.0) * elev_matrix[5]); - if(int_turbidity == 10) - return res; - - // alb 0 high turb - elev_matrix = dataset + (6*(int_turbidity)); - //(1-t).^3* A1 + 3*(1-t).^2.*t * A2 + 3*(1-t) .* t .^ 2 * A3 + t.^3 * A4; - res += (1.0-albedo) * (turbidity_rem) * - ( pow(1.0-solar_elevation, 5.0) * elev_matrix[0] + - 5.0*pow(1.0-solar_elevation, 4.0)*solar_elevation * elev_matrix[1] + - 10.0*pow(1.0-solar_elevation, 3.0)*pow(solar_elevation, 2.0) * elev_matrix[2] + - 10.0*pow(1.0-solar_elevation, 2.0)*pow(solar_elevation, 3.0) * elev_matrix[3] + - 5.0*(1.0-solar_elevation)*pow(solar_elevation, 4.0) * elev_matrix[4] + - pow(solar_elevation, 5.0) * elev_matrix[5]); - - // alb 1 high turb - elev_matrix = dataset + (6*10 + 6*(int_turbidity)); - //(1-t).^3* A1 + 3*(1-t).^2.*t * A2 + 3*(1-t) .* t .^ 2 * A3 + t.^3 * A4; - res += (albedo) * (turbidity_rem) * - ( pow(1.0-solar_elevation, 5.0) * elev_matrix[0] + - 5.0*pow(1.0-solar_elevation, 4.0)*solar_elevation * elev_matrix[1] + - 10.0*pow(1.0-solar_elevation, 3.0)*pow(solar_elevation, 2.0) * elev_matrix[2] + - 10.0*pow(1.0-solar_elevation, 2.0)*pow(solar_elevation, 3.0) * elev_matrix[3] + - 5.0*(1.0-solar_elevation)*pow(solar_elevation, 4.0) * elev_matrix[4] + - pow(solar_elevation, 5.0) * elev_matrix[5]); - return res; -} - -double ArHosekSkyModel_GetRadianceInternal( - ArHosekSkyModelConfiguration configuration, - double theta, - double gamma - ) -{ - const double expM = exp(configuration[4] * gamma); - const double rayM = cos(gamma)*cos(gamma); - const double mieM = (1.0 + cos(gamma)*cos(gamma)) / pow((1.0 + configuration[8]*configuration[8] - 2.0*configuration[8]*cos(gamma)), 1.5); - const double zenith = sqrt(cos(theta)); - - return (1.0 + configuration[0] * exp(configuration[1] / (cos(theta) + 0.01))) * - (configuration[2] + configuration[3] * expM + configuration[5] * rayM + configuration[6] * mieM + configuration[7] * zenith); -} - - -// 'blackbody_scaling_factor' -// -// Fudge factor, computed in Mathematica, to scale the results of the -// following function to match the solar radiance spectrum used in the -// original simulation. The scaling is done so their integrals over the -// range from 380.0 to 720.0 nanometers match for a blackbody temperature -// of 5800 K. -// Which leaves the original spectrum being less bright overall than the 5.8k -// blackbody radiation curve if the ultra-violet part of the spectrum is -// also considered. But the visible brightness should be very similar. - -const double blackbody_scaling_factor = 3.19992 * 10E-11; - -// 'art_blackbody_dd_value()' function -// -// Blackbody radiance, Planck's formula - -double art_blackbody_dd_value( - const double temperature, - const double lambda - ) -{ - double c1 = 3.74177 * 10E-17; - double c2 = 0.0143878; - double value; - - value = ( c1 / ( pow( lambda, 5.0 ) ) ) - * ( 1.0 / ( exp( c2 / ( lambda * temperature ) ) - 1.0 ) ); - - return value; -} - -// 'originalSolarRadianceTable[]' -// -// The solar spectrum incident at the top of the atmosphere, as it was used -// in the brute force path tracer that generated the reference results the -// model was fitted to. We need this as the yardstick to compare any altered -// Blackbody emission spectra for alien world stars to. - -// This is just the data from the Preetham paper, extended into the UV range. - -const double originalSolarRadianceTable[] = -{ - 7500.0, - 12500.0, - 21127.5, - 26760.5, - 30663.7, - 27825.0, - 25503.8, - 25134.2, - 23212.1, - 21526.7, - 19870.8 -}; - - -void arhosekskymodelstate_free( - ArHosekSkyModelState * state - ) -{ - free(state); -} - -double arhosekskymodel_radiance( - ArHosekSkyModelState * state, - double theta, - double gamma, - double wavelength - ) -{ - /// @@@ Added explicit cast to int to avoid warning - int low_wl = (int)((wavelength - 320.0 ) / 40.0); - - if ( low_wl < 0 || low_wl >= 11 ) - return 0.0f; - - double interp = fmod((wavelength - 320.0 ) / 40.0, 1.0); - - double val_low = - ArHosekSkyModel_GetRadianceInternal( - state->configs[low_wl], - theta, - gamma - ) - * state->radiances[low_wl] - * state->emission_correction_factor_sky[low_wl]; - - if ( interp < 1e-6 ) - return val_low; - - double result = ( 1.0 - interp ) * val_low; - - if ( low_wl+1 < 11 ) - { - result += - interp - * ArHosekSkyModel_GetRadianceInternal( - state->configs[low_wl+1], - theta, - gamma - ) - * state->radiances[low_wl+1] - * state->emission_correction_factor_sky[low_wl+1]; - } - - return result; -} - - -// xyz and rgb versions - -ArHosekSkyModelState * arhosek_xyz_skymodelstate_alloc_init( - const double turbidity, - const double albedo, - const double elevation - ) -{ - ArHosekSkyModelState * state = ALLOC(ArHosekSkyModelState); - - state->solar_radius = TERRESTRIAL_SOLAR_RADIUS; - state->turbidity = turbidity; - state->albedo = albedo; - state->elevation = elevation; - - for( unsigned int channel = 0; channel < 3; ++channel ) - { - ArHosekSkyModel_CookConfiguration( - datasetsXYZ[channel], - state->configs[channel], - turbidity, - albedo, - elevation - ); - - state->radiances[channel] = - ArHosekSkyModel_CookRadianceConfiguration( - datasetsXYZRad[channel], - turbidity, - albedo, - elevation - ); - } - - return state; -} - - -ArHosekSkyModelState * arhosek_rgb_skymodelstate_alloc_init( - const double turbidity, - const double albedo, - const double elevation - ) -{ - ArHosekSkyModelState* state = ALLOC(ArHosekSkyModelState); - - state->solar_radius = TERRESTRIAL_SOLAR_RADIUS; - state->turbidity = turbidity; - state->albedo = albedo; - state->elevation = elevation; - - for( unsigned int channel = 0; channel < 3; ++channel ) - { - ArHosekSkyModel_CookConfiguration( - datasetsRGB[channel], - state->configs[channel], - turbidity, - albedo, - elevation - ); - - state->radiances[channel] = - ArHosekSkyModel_CookRadianceConfiguration( - datasetsRGBRad[channel], - turbidity, - albedo, - elevation - ); - } - - return state; -} - -double arhosek_tristim_skymodel_radiance( - ArHosekSkyModelState * state, - double theta, - double gamma, - int channel - ) -{ - return - ArHosekSkyModel_GetRadianceInternal( - state->configs[channel], - theta, - gamma - ) - * state->radiances[channel]; -} +/* +This source is published under the following 3-clause BSD license. + +Copyright (c) 2012 - 2013, Lukas Hosek and Alexander Wilkie +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * None of the names of the contributors may be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/* ============================================================================ + +This file is part of a sample implementation of the analytical skylight and +solar radiance models presented in the SIGGRAPH 2012 paper + + + "An Analytic Model for Full Spectral Sky-Dome Radiance" + +and the 2013 IEEE CG&A paper + + "Adding a Solar Radiance Function to the Hosek Skylight Model" + + both by + + Lukas Hosek and Alexander Wilkie + Charles University in Prague, Czech Republic + + + Version: 1.4a, February 22nd, 2013 + +Version history: + +1.4a February 22nd, 2013 + Removed unnecessary and counter-intuitive solar radius parameters + from the interface of the colourspace sky dome initialisation functions. + +1.4 February 11th, 2013 + Fixed a bug which caused the relative brightness of the solar disc + and the sky dome to be off by a factor of about 6. The sun was too + bright: this affected both normal and alien sun scenarios. The + coefficients of the solar radiance function were changed to fix this. + +1.3 January 21st, 2013 (not released to the public) + Added support for solar discs that are not exactly the same size as + the terrestrial sun. Also added support for suns with a different + emission spectrum ("Alien World" functionality). + +1.2a December 18th, 2012 + Fixed a mistake and some inaccuracies in the solar radiance function + explanations found in ArHosekSkyModel.h. The actual source code is + unchanged compared to version 1.2. + +1.2 December 17th, 2012 + Native RGB data and a solar radiance function that matches the turbidity + conditions were added. + +1.1 September 2012 + The coefficients of the spectral model are now scaled so that the output + is given in physical units: W / (m^-2 * sr * nm). Also, the output of the + XYZ model is now no longer scaled to the range [0...1]. Instead, it is + the result of a simple conversion from spectral data via the CIE 2 degree + standard observer matching functions. Therefore, after multiplication + with 683 lm / W, the Y channel now corresponds to luminance in lm. + +1.0 May 11th, 2012 + Initial release. + + +Please visit http://cgg.mff.cuni.cz/projects/SkylightModelling/ to check if +an updated version of this code has been published! + +============================================================================ */ + +/* + +All instructions on how to use this code are in the accompanying header file. + +*/ + +#include "ArHosekSkyModel.h" +#include "ArHosekSkyModelData_CIEXYZ.data" +#include "ArHosekSkyModelData_RGB.data" +#include +#include +#include +#include + +// Some macro definitions that occur elsewhere in ART, and that have to be +// replicated to make this a stand-alone module. + +#ifndef NIL +#define NIL 0 +#endif + +#ifndef MATH_PI +#define MATH_PI 3.141592653589793 +#endif + +#ifndef MATH_DEG_TO_RAD +#define MATH_DEG_TO_RAD ( MATH_PI / 180.0 ) +#endif + +#ifndef MATH_RAD_TO_DEG +#define MATH_RAD_TO_DEG ( 180.0 / MATH_PI ) +#endif + +#ifndef DEGREES +#define DEGREES * MATH_DEG_TO_RAD +#endif + +#ifndef TERRESTRIAL_SOLAR_RADIUS +#define TERRESTRIAL_SOLAR_RADIUS ( ( 0.51 DEGREES ) / 2.0 ) +#endif + +#ifndef ALLOC +#define ALLOC(_struct) ((_struct *)malloc(sizeof(_struct))) +#endif + +// internal definitions + +typedef double *ArHosekSkyModel_Dataset; +typedef double *ArHosekSkyModel_Radiance_Dataset; + +// internal functions + +void ArHosekSkyModel_CookConfiguration( + ArHosekSkyModel_Dataset dataset, + ArHosekSkyModelConfiguration config, + double turbidity, + double albedo, + double solar_elevation + ) +{ + double * elev_matrix; + + int int_turbidity = (int)turbidity; + double turbidity_rem = turbidity - (double)int_turbidity; + + solar_elevation = pow(solar_elevation / (MATH_PI / 2.0), (1.0 / 3.0)); + + // alb 0 low turb + + elev_matrix = dataset + ( 9 * 6 * (int_turbidity-1) ); + + + for( unsigned int i = 0; i < 9; ++i ) + { + //(1-t).^3* A1 + 3*(1-t).^2.*t * A2 + 3*(1-t) .* t .^ 2 * A3 + t.^3 * A4; + config[i] = + (1.0-albedo) * (1.0 - turbidity_rem) + * ( pow(1.0-solar_elevation, 5.0) * elev_matrix[i] + + 5.0 * pow(1.0-solar_elevation, 4.0) * solar_elevation * elev_matrix[i+9] + + 10.0*pow(1.0-solar_elevation, 3.0)*pow(solar_elevation, 2.0) * elev_matrix[i+18] + + 10.0*pow(1.0-solar_elevation, 2.0)*pow(solar_elevation, 3.0) * elev_matrix[i+27] + + 5.0*(1.0-solar_elevation)*pow(solar_elevation, 4.0) * elev_matrix[i+36] + + pow(solar_elevation, 5.0) * elev_matrix[i+45]); + } + + // alb 1 low turb + elev_matrix = dataset + (9*6*10 + 9*6*(int_turbidity-1)); + for(unsigned int i = 0; i < 9; ++i) + { + //(1-t).^3* A1 + 3*(1-t).^2.*t * A2 + 3*(1-t) .* t .^ 2 * A3 + t.^3 * A4; + config[i] += + (albedo) * (1.0 - turbidity_rem) + * ( pow(1.0-solar_elevation, 5.0) * elev_matrix[i] + + 5.0 * pow(1.0-solar_elevation, 4.0) * solar_elevation * elev_matrix[i+9] + + 10.0*pow(1.0-solar_elevation, 3.0)*pow(solar_elevation, 2.0) * elev_matrix[i+18] + + 10.0*pow(1.0-solar_elevation, 2.0)*pow(solar_elevation, 3.0) * elev_matrix[i+27] + + 5.0*(1.0-solar_elevation)*pow(solar_elevation, 4.0) * elev_matrix[i+36] + + pow(solar_elevation, 5.0) * elev_matrix[i+45]); + } + + if(int_turbidity == 10) + return; + + // alb 0 high turb + elev_matrix = dataset + (9*6*(int_turbidity)); + for(unsigned int i = 0; i < 9; ++i) + { + //(1-t).^3* A1 + 3*(1-t).^2.*t * A2 + 3*(1-t) .* t .^ 2 * A3 + t.^3 * A4; + config[i] += + (1.0-albedo) * (turbidity_rem) + * ( pow(1.0-solar_elevation, 5.0) * elev_matrix[i] + + 5.0 * pow(1.0-solar_elevation, 4.0) * solar_elevation * elev_matrix[i+9] + + 10.0*pow(1.0-solar_elevation, 3.0)*pow(solar_elevation, 2.0) * elev_matrix[i+18] + + 10.0*pow(1.0-solar_elevation, 2.0)*pow(solar_elevation, 3.0) * elev_matrix[i+27] + + 5.0*(1.0-solar_elevation)*pow(solar_elevation, 4.0) * elev_matrix[i+36] + + pow(solar_elevation, 5.0) * elev_matrix[i+45]); + } + + // alb 1 high turb + elev_matrix = dataset + (9*6*10 + 9*6*(int_turbidity)); + for(unsigned int i = 0; i < 9; ++i) + { + //(1-t).^3* A1 + 3*(1-t).^2.*t * A2 + 3*(1-t) .* t .^ 2 * A3 + t.^3 * A4; + config[i] += + (albedo) * (turbidity_rem) + * ( pow(1.0-solar_elevation, 5.0) * elev_matrix[i] + + 5.0 * pow(1.0-solar_elevation, 4.0) * solar_elevation * elev_matrix[i+9] + + 10.0*pow(1.0-solar_elevation, 3.0)*pow(solar_elevation, 2.0) * elev_matrix[i+18] + + 10.0*pow(1.0-solar_elevation, 2.0)*pow(solar_elevation, 3.0) * elev_matrix[i+27] + + 5.0*(1.0-solar_elevation)*pow(solar_elevation, 4.0) * elev_matrix[i+36] + + pow(solar_elevation, 5.0) * elev_matrix[i+45]); + } +} + +double ArHosekSkyModel_CookRadianceConfiguration( + ArHosekSkyModel_Radiance_Dataset dataset, + double turbidity, + double albedo, + double solar_elevation + ) +{ + double* elev_matrix; + + int int_turbidity = (int)turbidity; + double turbidity_rem = turbidity - (double)int_turbidity; + double res; + solar_elevation = pow(solar_elevation / (MATH_PI / 2.0), (1.0 / 3.0)); + + // alb 0 low turb + elev_matrix = dataset + (6*(int_turbidity-1)); + //(1-t).^3* A1 + 3*(1-t).^2.*t * A2 + 3*(1-t) .* t .^ 2 * A3 + t.^3 * A4; + res = (1.0-albedo) * (1.0 - turbidity_rem) * + ( pow(1.0-solar_elevation, 5.0) * elev_matrix[0] + + 5.0*pow(1.0-solar_elevation, 4.0)*solar_elevation * elev_matrix[1] + + 10.0*pow(1.0-solar_elevation, 3.0)*pow(solar_elevation, 2.0) * elev_matrix[2] + + 10.0*pow(1.0-solar_elevation, 2.0)*pow(solar_elevation, 3.0) * elev_matrix[3] + + 5.0*(1.0-solar_elevation)*pow(solar_elevation, 4.0) * elev_matrix[4] + + pow(solar_elevation, 5.0) * elev_matrix[5]); + + // alb 1 low turb + elev_matrix = dataset + (6*10 + 6*(int_turbidity-1)); + //(1-t).^3* A1 + 3*(1-t).^2.*t * A2 + 3*(1-t) .* t .^ 2 * A3 + t.^3 * A4; + res += (albedo) * (1.0 - turbidity_rem) * + ( pow(1.0-solar_elevation, 5.0) * elev_matrix[0] + + 5.0*pow(1.0-solar_elevation, 4.0)*solar_elevation * elev_matrix[1] + + 10.0*pow(1.0-solar_elevation, 3.0)*pow(solar_elevation, 2.0) * elev_matrix[2] + + 10.0*pow(1.0-solar_elevation, 2.0)*pow(solar_elevation, 3.0) * elev_matrix[3] + + 5.0*(1.0-solar_elevation)*pow(solar_elevation, 4.0) * elev_matrix[4] + + pow(solar_elevation, 5.0) * elev_matrix[5]); + if(int_turbidity == 10) + return res; + + // alb 0 high turb + elev_matrix = dataset + (6*(int_turbidity)); + //(1-t).^3* A1 + 3*(1-t).^2.*t * A2 + 3*(1-t) .* t .^ 2 * A3 + t.^3 * A4; + res += (1.0-albedo) * (turbidity_rem) * + ( pow(1.0-solar_elevation, 5.0) * elev_matrix[0] + + 5.0*pow(1.0-solar_elevation, 4.0)*solar_elevation * elev_matrix[1] + + 10.0*pow(1.0-solar_elevation, 3.0)*pow(solar_elevation, 2.0) * elev_matrix[2] + + 10.0*pow(1.0-solar_elevation, 2.0)*pow(solar_elevation, 3.0) * elev_matrix[3] + + 5.0*(1.0-solar_elevation)*pow(solar_elevation, 4.0) * elev_matrix[4] + + pow(solar_elevation, 5.0) * elev_matrix[5]); + + // alb 1 high turb + elev_matrix = dataset + (6*10 + 6*(int_turbidity)); + //(1-t).^3* A1 + 3*(1-t).^2.*t * A2 + 3*(1-t) .* t .^ 2 * A3 + t.^3 * A4; + res += (albedo) * (turbidity_rem) * + ( pow(1.0-solar_elevation, 5.0) * elev_matrix[0] + + 5.0*pow(1.0-solar_elevation, 4.0)*solar_elevation * elev_matrix[1] + + 10.0*pow(1.0-solar_elevation, 3.0)*pow(solar_elevation, 2.0) * elev_matrix[2] + + 10.0*pow(1.0-solar_elevation, 2.0)*pow(solar_elevation, 3.0) * elev_matrix[3] + + 5.0*(1.0-solar_elevation)*pow(solar_elevation, 4.0) * elev_matrix[4] + + pow(solar_elevation, 5.0) * elev_matrix[5]); + return res; +} + +double ArHosekSkyModel_GetRadianceInternal( + ArHosekSkyModelConfiguration configuration, + double theta, + double gamma + ) +{ + const double expM = exp(configuration[4] * gamma); + const double rayM = cos(gamma)*cos(gamma); + const double mieM = (1.0 + cos(gamma)*cos(gamma)) / pow((1.0 + configuration[8]*configuration[8] - 2.0*configuration[8]*cos(gamma)), 1.5); + const double zenith = sqrt(cos(theta)); + + return (1.0 + configuration[0] * exp(configuration[1] / (cos(theta) + 0.01))) * + (configuration[2] + configuration[3] * expM + configuration[5] * rayM + configuration[6] * mieM + configuration[7] * zenith); +} + + +// 'blackbody_scaling_factor' +// +// Fudge factor, computed in Mathematica, to scale the results of the +// following function to match the solar radiance spectrum used in the +// original simulation. The scaling is done so their integrals over the +// range from 380.0 to 720.0 nanometers match for a blackbody temperature +// of 5800 K. +// Which leaves the original spectrum being less bright overall than the 5.8k +// blackbody radiation curve if the ultra-violet part of the spectrum is +// also considered. But the visible brightness should be very similar. + +const double blackbody_scaling_factor = 3.19992 * 10E-11; + +// 'art_blackbody_dd_value()' function +// +// Blackbody radiance, Planck's formula + +double art_blackbody_dd_value( + const double temperature, + const double lambda + ) +{ + double c1 = 3.74177 * 10E-17; + double c2 = 0.0143878; + double value; + + value = ( c1 / ( pow( lambda, 5.0 ) ) ) + * ( 1.0 / ( exp( c2 / ( lambda * temperature ) ) - 1.0 ) ); + + return value; +} + +// 'originalSolarRadianceTable[]' +// +// The solar spectrum incident at the top of the atmosphere, as it was used +// in the brute force path tracer that generated the reference results the +// model was fitted to. We need this as the yardstick to compare any altered +// Blackbody emission spectra for alien world stars to. + +// This is just the data from the Preetham paper, extended into the UV range. + +const double originalSolarRadianceTable[] = +{ + 7500.0, + 12500.0, + 21127.5, + 26760.5, + 30663.7, + 27825.0, + 25503.8, + 25134.2, + 23212.1, + 21526.7, + 19870.8 +}; + + +void arhosekskymodelstate_free( + ArHosekSkyModelState * state + ) +{ + free(state); +} + +double arhosekskymodel_radiance( + ArHosekSkyModelState * state, + double theta, + double gamma, + double wavelength + ) +{ + /// @@@ Added explicit cast to int to avoid warning + int low_wl = (int)((wavelength - 320.0 ) / 40.0); + + if ( low_wl < 0 || low_wl >= 11 ) + return 0.0f; + + double interp = fmod((wavelength - 320.0 ) / 40.0, 1.0); + + double val_low = + ArHosekSkyModel_GetRadianceInternal( + state->configs[low_wl], + theta, + gamma + ) + * state->radiances[low_wl] + * state->emission_correction_factor_sky[low_wl]; + + if ( interp < 1e-6 ) + return val_low; + + double result = ( 1.0 - interp ) * val_low; + + if ( low_wl+1 < 11 ) + { + result += + interp + * ArHosekSkyModel_GetRadianceInternal( + state->configs[low_wl+1], + theta, + gamma + ) + * state->radiances[low_wl+1] + * state->emission_correction_factor_sky[low_wl+1]; + } + + return result; +} + + +// xyz and rgb versions + +ArHosekSkyModelState * arhosek_xyz_skymodelstate_alloc_init( + const double turbidity, + const double albedo, + const double elevation + ) +{ + ArHosekSkyModelState * state = ALLOC(ArHosekSkyModelState); + + state->solar_radius = TERRESTRIAL_SOLAR_RADIUS; + state->turbidity = turbidity; + state->albedo = albedo; + state->elevation = elevation; + + for( unsigned int channel = 0; channel < 3; ++channel ) + { + ArHosekSkyModel_CookConfiguration( + datasetsXYZ[channel], + state->configs[channel], + turbidity, + albedo, + elevation + ); + + state->radiances[channel] = + ArHosekSkyModel_CookRadianceConfiguration( + datasetsXYZRad[channel], + turbidity, + albedo, + elevation + ); + } + + return state; +} + + +ArHosekSkyModelState * arhosek_rgb_skymodelstate_alloc_init( + const double turbidity, + const double albedo, + const double elevation + ) +{ + ArHosekSkyModelState* state = ALLOC(ArHosekSkyModelState); + + state->solar_radius = TERRESTRIAL_SOLAR_RADIUS; + state->turbidity = turbidity; + state->albedo = albedo; + state->elevation = elevation; + + for( unsigned int channel = 0; channel < 3; ++channel ) + { + ArHosekSkyModel_CookConfiguration( + datasetsRGB[channel], + state->configs[channel], + turbidity, + albedo, + elevation + ); + + state->radiances[channel] = + ArHosekSkyModel_CookRadianceConfiguration( + datasetsRGBRad[channel], + turbidity, + albedo, + elevation + ); + } + + return state; +} + +double arhosek_tristim_skymodel_radiance( + ArHosekSkyModelState * state, + double theta, + double gamma, + int channel + ) +{ + return + ArHosekSkyModel_GetRadianceInternal( + state->configs[channel], + theta, + gamma + ) + * state->radiances[channel]; +} diff --git a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/ArHosekSkyModel.h b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/ArHosekSkyModel.h index e1c3e2d9..48bd898e 100644 --- a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/ArHosekSkyModel.h +++ b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/ArHosekSkyModel.h @@ -1,451 +1,451 @@ -/* -This source is published under the following 3-clause BSD license. - -Copyright (c) 2012 - 2013, Lukas Hosek and Alexander Wilkie -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * None of the names of the contributors may be used to endorse or promote - products derived from this software without specific prior written - permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - - -/* ============================================================================ - -This file is part of a sample implementation of the analytical skylight and -solar radiance models presented in the SIGGRAPH 2012 paper - - - "An Analytic Model for Full Spectral Sky-Dome Radiance" - -and the 2013 IEEE CG&A paper - - "Adding a Solar Radiance Function to the Hosek Skylight Model" - - both by - - Lukas Hosek and Alexander Wilkie - Charles University in Prague, Czech Republic - - - Version: 1.4a, February 22nd, 2013 - -Version history: - -1.4a February 22nd, 2013 - Removed unnecessary and counter-intuitive solar radius parameters - from the interface of the colourspace sky dome initialisation functions. - -1.4 February 11th, 2013 - Fixed a bug which caused the relative brightness of the solar disc - and the sky dome to be off by a factor of about 6. The sun was too - bright: this affected both normal and alien sun scenarios. The - coefficients of the solar radiance function were changed to fix this. - -1.3 January 21st, 2013 (not released to the public) - Added support for solar discs that are not exactly the same size as - the terrestrial sun. Also added support for suns with a different - emission spectrum ("Alien World" functionality). - -1.2a December 18th, 2012 - Fixed a mistake and some inaccuracies in the solar radiance function - explanations found in ArHosekSkyModel.h. The actual source code is - unchanged compared to version 1.2. - -1.2 December 17th, 2012 - Native RGB data and a solar radiance function that matches the turbidity - conditions were added. - -1.1 September 2012 - The coefficients of the spectral model are now scaled so that the output - is given in physical units: W / (m^-2 * sr * nm). Also, the output of the - XYZ model is now no longer scaled to the range [0...1]. Instead, it is - the result of a simple conversion from spectral data via the CIE 2 degree - standard observer matching functions. Therefore, after multiplication - with 683 lm / W, the Y channel now corresponds to luminance in lm. - -1.0 May 11th, 2012 - Initial release. - - -Please visit http://cgg.mff.cuni.cz/projects/SkylightModelling/ to check if -an updated version of this code has been published! - -============================================================================ */ - - -/* - -This code is taken from ART, a rendering research system written in a -mix of C99 / Objective C. Since ART is not a small system and is intended to -be inter-operable with other libraries, and since C does not have namespaces, -the structures and functions in ART all have to have somewhat wordy -canonical names that begin with Ar.../ar..., like those seen in this example. - -Usage information: -================== - - -Model initialisation --------------------- - -A separate ArHosekSkyModelState has to be maintained for each spectral -band you want to use the model for. So in a renderer with 'num_channels' -bands, you would need something like - - ArHosekSkyModelState * skymodel_state[num_channels]; - -You then have to allocate and initialise these states. In the following code -snippet, we assume that 'albedo' is defined as - - double albedo[num_channels]; - -with a ground albedo value between [0,1] for each channel. The solar elevation -is given in radians. - - for ( unsigned int i = 0; i < num_channels; i++ ) - skymodel_state[i] = - arhosekskymodelstate_alloc_init( - turbidity, - albedo[i], - solarElevation - ); - -Note that starting with version 1.3, there is also a second initialisation -function which generates skydome states for different solar emission spectra -and solar radii: 'arhosekskymodelstate_alienworld_alloc_init()'. - -See the notes about the "Alien World" functionality provided further down for a -discussion of the usefulness and limits of that second initalisation function. -Sky model states that have been initialised with either function behave in a -completely identical fashion during use and cleanup. - -Using the model to generate skydome samples -------------------------------------------- - -Generating a skydome radiance spectrum "skydome_result" for a given location -on the skydome determined via the angles theta and gamma works as follows: - - double skydome_result[num_channels]; - - for ( unsigned int i = 0; i < num_channels; i++ ) - skydome_result[i] = - arhosekskymodel_radiance( - skymodel_state[i], - theta, - gamma, - channel_center[i] - ); - -The variable "channel_center" is assumed to hold the channel center wavelengths -for each of the num_channels samples of the spectrum we are building. - - -Cleanup after use ------------------ - -After rendering is complete, the content of the sky model states should be -disposed of via - - for ( unsigned int i = 0; i < num_channels; i++ ) - arhosekskymodelstate_free( skymodel_state[i] ); - - -CIE XYZ Version of the Model ----------------------------- - -Usage of the CIE XYZ version of the model is exactly the same, except that -num_channels is of course always 3, and that ArHosekTristimSkyModelState and -arhosek_tristim_skymodel_radiance() have to be used instead of their spectral -counterparts. - -RGB Version of the Model ------------------------- - -The RGB version uses sRGB primaries with a linear gamma ramp. The same set of -functions as with the XYZ data is used, except the model is initialized -by calling arhosek_rgb_skymodelstate_alloc_init. - -Solar Radiance Function ------------------------ - -For each position on the solar disc, this function returns the entire radiance -one sees - direct emission, as well as in-scattered light in the area of the -solar disc. The latter is important for low solar elevations - nice images of -the setting sun would not be possible without this. This is also the reason why -this function, just like the regular sky dome model evaluation function, needs -access to the sky dome data structures, as these provide information on -in-scattered radiance. - -CAVEAT #1: in this release, this function is only provided in spectral form! - RGB/XYZ versions to follow at a later date. - -CAVEAT #2: (fixed from release 1.3 onwards) - -CAVEAT #3: limb darkening renders the brightness of the solar disc - inhomogeneous even for high solar elevations - only taking a single - sample at the centre of the sun will yield an incorrect power - estimate for the solar disc! Always take multiple random samples - across the entire solar disc to estimate its power! - -CAVEAT #4: in this version, the limb darkening calculations still use a fairly - computationally expensive 5th order polynomial that was directly - taken from astronomical literature. For the purposes of Computer - Graphics, this is needlessly accurate, though, and will be replaced - by a cheaper approximation in a future release. - -"Alien World" functionality ---------------------------- - -The Hosek sky model can be used to roughly (!) predict the appearance of -outdoor scenes on earth-like planets, i.e. planets of a similar size and -atmospheric make-up. Since the spectral version of our model predicts sky dome -luminance patterns and solar radiance independently for each waveband, and -since the intensity of each waveband is solely dependent on the input radiance -from the star that the world in question is orbiting, it is trivial to re-scale -the wavebands to match a different star radiance. - -At least in theory, the spectral version of the model has always been capable -of this sort of thing, and the actual sky dome and solar radiance models were -actually not altered at all in this release. All we did was to add some support -functionality for doing this more easily with the existing data and functions, -and to add some explanations. - -Just use 'arhosekskymodelstate_alienworld_alloc_init()' to initialise the sky -model states (you will have to provide values for star temperature and solar -intensity compared to the terrestrial sun), and do everything else as you -did before. - -CAVEAT #1: we assume the emission of the star that illuminates the alien world - to be a perfect blackbody emission spectrum. This is never entirely - realistic - real star emission spectra are considerably more complex - than this, mainly due to absorption effects in the outer layers of - stars. However, blackbody spectra are a reasonable first assumption - in a usage scenario like this, where 100% accuracy is simply not - necessary: for rendering purposes, there are likely no visible - differences between a highly accurate solution based on a more - involved simulation, and this approximation. - -CAVEAT #2: we always use limb darkening data from our own sun to provide this - "appearance feature", even for suns of strongly different - temperature. Which is presumably not very realistic, but (as with - the unaltered blackbody spectrum from caveat #1) probably not a bad - first guess, either. If you need more accuracy than we provide here, - please make inquiries with a friendly astro-physicst of your choice. - -CAVEAT #3: you have to provide a value for the solar intensity of the star - which illuminates the alien world. For this, please bear in mind - that there is very likely a comparatively tight range of absolute - solar irradiance values for which an earth-like planet with an - atmosphere like the one we assume in our model can exist in the - first place! - - Too much irradiance, and the atmosphere probably boils off into - space, too little, it freezes. Which means that stars of - considerably different emission colour than our sun will have to be - fairly different in size from it, to still provide a reasonable and - inhabitable amount of irradiance. Red stars will need to be much - larger than our sun, while white or blue stars will have to be - comparatively tiny. The initialisation function handles this and - computes a plausible solar radius for a given emission spectrum. In - terms of absolute radiometric values, you should probably not stray - all too far from a solar intensity value of 1.0. - -CAVEAT #4: although we now support different solar radii for the actual solar - disc, the sky dome luminance patterns are *not* parameterised by - this value - i.e. the patterns stay exactly the same for different - solar radii! Which is of course not correct. But in our experience, - solar discs up to several degrees in diameter (! - our own sun is - half a degree across) do not cause the luminance patterns on the sky - to change perceptibly. The reason we know this is that we initially - used unrealistically large suns in our brute force path tracer, in - order to improve convergence speeds (which in the beginning were - abysmal). Later, we managed to do the reference renderings much - faster even with realistically small suns, and found that there was - no real difference in skydome appearance anyway. - Conclusion: changing the solar radius should not be over-done, so - close orbits around red supergiants are a no-no. But for the - purposes of getting a fairly credible first impression of what an - alien world with a reasonably sized sun would look like, what we are - doing here is probably still o.k. - -HINT #1: if you want to model the sky of an earth-like planet that orbits - a binary star, just super-impose two of these models with solar - intensity of ~0.5 each, and closely spaced solar positions. Light is - additive, after all. Tattooine, here we come... :-) - - P.S. according to Star Wars canon, Tattooine orbits a binary - that is made up of a G and K class star, respectively. - So ~5500K and ~4200K should be good first guesses for their - temperature. Just in case you were wondering, after reading the - previous paragraph. -*/ - - -#ifndef _ARHOSEK_SKYMODEL_H_ -#define _ARHOSEK_SKYMODEL_H_ - -typedef double ArHosekSkyModelConfiguration[9]; - - -// Spectral version of the model - -/* ---------------------------------------------------------------------------- - - ArHosekSkyModelState struct - --------------------------- - - This struct holds the pre-computation data for one particular albedo value. - Most fields are self-explanatory, but users should never directly - manipulate any of them anyway. The only consistent way to manipulate such - structs is via the functions 'arhosekskymodelstate_alloc_init' and - 'arhosekskymodelstate_free'. - - 'emission_correction_factor_sky' - 'emission_correction_factor_sun' - - The original model coefficients were fitted against the emission of - our local sun. If a different solar emission is desired (i.e. if the - model is being used to predict skydome appearance for an earth-like - planet that orbits a different star), these correction factors, which - are determined during the alloc_init step, are applied to each waveband - separately (they default to 1.0 in normal usage). This is the simplest - way to retrofit this sort of capability to the existing model. The - different factors for sky and sun are needed since the solar disc may - be of a different size compared to the terrestrial sun. - ----------------------------------------------------------------------------- */ - -typedef struct ArHosekSkyModelState -{ - ArHosekSkyModelConfiguration configs[11]; - double radiances[11]; - double turbidity; - double solar_radius; - double emission_correction_factor_sky[11]; - double emission_correction_factor_sun[11]; - double albedo; - double elevation; -} -ArHosekSkyModelState; - -/* ---------------------------------------------------------------------------- - - arhosekskymodelstate_alloc_init() function - ------------------------------------------ - - Initialises an ArHosekSkyModelState struct for a terrestrial setting. - ----------------------------------------------------------------------------- */ - -ArHosekSkyModelState * arhosekskymodelstate_alloc_init( - const double solar_elevation, - const double atmospheric_turbidity, - const double ground_albedo - ); - - -/* ---------------------------------------------------------------------------- - - arhosekskymodelstate_alienworld_alloc_init() function - ----------------------------------------------------- - - Initialises an ArHosekSkyModelState struct for an "alien world" setting - with a sun of a surface temperature given in 'kelvin'. The parameter - 'solar_intensity' controls the overall brightness of the sky, relative - to the solar irradiance on Earth. A value of 1.0 yields a sky dome that - is, on average over the wavelenghts covered in the model (!), as bright - as the terrestrial sky in radiometric terms. - - Which means that the solar radius has to be adjusted, since the - emissivity of a solar surface with a given temperature is more or less - fixed. So hotter suns have to be smaller to be equally bright as the - terrestrial sun, while cooler suns have to be larger. Note that there are - limits to the validity of the luminance patterns of the underlying model: - see the discussion above for more on this. In particular, an alien sun with - a surface temperature of only 2000 Kelvin has to be very large if it is - to be as bright as the terrestrial sun - so large that the luminance - patterns are no longer a really good fit in that case. - - If you need information about the solar radius that the model computes - for a given temperature (say, for light source sampling purposes), you - have to query the 'solar_radius' variable of the sky model state returned - *after* running this function. - ----------------------------------------------------------------------------- */ - -ArHosekSkyModelState * arhosekskymodelstate_alienworld_alloc_init( - const double solar_elevation, - const double solar_intensity, - const double solar_surface_temperature_kelvin, - const double atmospheric_turbidity, - const double ground_albedo - ); - -void arhosekskymodelstate_free( - ArHosekSkyModelState * state - ); - -double arhosekskymodel_radiance( - ArHosekSkyModelState * state, - double theta, - double gamma, - double wavelength - ); - -// CIE XYZ and RGB versions - - -ArHosekSkyModelState * arhosek_xyz_skymodelstate_alloc_init( - const double turbidity, - const double albedo, - const double elevation - ); - - -ArHosekSkyModelState * arhosek_rgb_skymodelstate_alloc_init( - const double turbidity, - const double albedo, - const double elevation - ); - - -double arhosek_tristim_skymodel_radiance( - ArHosekSkyModelState * state, - double theta, - double gamma, - int channel - ); - -// Delivers the complete function: sky + sun, including limb darkening. -// Please read the above description before using this - there are several -// caveats! - -double arhosekskymodel_solar_radiance( - ArHosekSkyModelState * state, - double theta, - double gamma, - double wavelength - ); - - -#endif // _ARHOSEK_SKYMODEL_H_ +/* +This source is published under the following 3-clause BSD license. + +Copyright (c) 2012 - 2013, Lukas Hosek and Alexander Wilkie +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * None of the names of the contributors may be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +/* ============================================================================ + +This file is part of a sample implementation of the analytical skylight and +solar radiance models presented in the SIGGRAPH 2012 paper + + + "An Analytic Model for Full Spectral Sky-Dome Radiance" + +and the 2013 IEEE CG&A paper + + "Adding a Solar Radiance Function to the Hosek Skylight Model" + + both by + + Lukas Hosek and Alexander Wilkie + Charles University in Prague, Czech Republic + + + Version: 1.4a, February 22nd, 2013 + +Version history: + +1.4a February 22nd, 2013 + Removed unnecessary and counter-intuitive solar radius parameters + from the interface of the colourspace sky dome initialisation functions. + +1.4 February 11th, 2013 + Fixed a bug which caused the relative brightness of the solar disc + and the sky dome to be off by a factor of about 6. The sun was too + bright: this affected both normal and alien sun scenarios. The + coefficients of the solar radiance function were changed to fix this. + +1.3 January 21st, 2013 (not released to the public) + Added support for solar discs that are not exactly the same size as + the terrestrial sun. Also added support for suns with a different + emission spectrum ("Alien World" functionality). + +1.2a December 18th, 2012 + Fixed a mistake and some inaccuracies in the solar radiance function + explanations found in ArHosekSkyModel.h. The actual source code is + unchanged compared to version 1.2. + +1.2 December 17th, 2012 + Native RGB data and a solar radiance function that matches the turbidity + conditions were added. + +1.1 September 2012 + The coefficients of the spectral model are now scaled so that the output + is given in physical units: W / (m^-2 * sr * nm). Also, the output of the + XYZ model is now no longer scaled to the range [0...1]. Instead, it is + the result of a simple conversion from spectral data via the CIE 2 degree + standard observer matching functions. Therefore, after multiplication + with 683 lm / W, the Y channel now corresponds to luminance in lm. + +1.0 May 11th, 2012 + Initial release. + + +Please visit http://cgg.mff.cuni.cz/projects/SkylightModelling/ to check if +an updated version of this code has been published! + +============================================================================ */ + + +/* + +This code is taken from ART, a rendering research system written in a +mix of C99 / Objective C. Since ART is not a small system and is intended to +be inter-operable with other libraries, and since C does not have namespaces, +the structures and functions in ART all have to have somewhat wordy +canonical names that begin with Ar.../ar..., like those seen in this example. + +Usage information: +================== + + +Model initialisation +-------------------- + +A separate ArHosekSkyModelState has to be maintained for each spectral +band you want to use the model for. So in a renderer with 'num_channels' +bands, you would need something like + + ArHosekSkyModelState * skymodel_state[num_channels]; + +You then have to allocate and initialise these states. In the following code +snippet, we assume that 'albedo' is defined as + + double albedo[num_channels]; + +with a ground albedo value between [0,1] for each channel. The solar elevation +is given in radians. + + for ( unsigned int i = 0; i < num_channels; i++ ) + skymodel_state[i] = + arhosekskymodelstate_alloc_init( + turbidity, + albedo[i], + solarElevation + ); + +Note that starting with version 1.3, there is also a second initialisation +function which generates skydome states for different solar emission spectra +and solar radii: 'arhosekskymodelstate_alienworld_alloc_init()'. + +See the notes about the "Alien World" functionality provided further down for a +discussion of the usefulness and limits of that second initalisation function. +Sky model states that have been initialised with either function behave in a +completely identical fashion during use and cleanup. + +Using the model to generate skydome samples +------------------------------------------- + +Generating a skydome radiance spectrum "skydome_result" for a given location +on the skydome determined via the angles theta and gamma works as follows: + + double skydome_result[num_channels]; + + for ( unsigned int i = 0; i < num_channels; i++ ) + skydome_result[i] = + arhosekskymodel_radiance( + skymodel_state[i], + theta, + gamma, + channel_center[i] + ); + +The variable "channel_center" is assumed to hold the channel center wavelengths +for each of the num_channels samples of the spectrum we are building. + + +Cleanup after use +----------------- + +After rendering is complete, the content of the sky model states should be +disposed of via + + for ( unsigned int i = 0; i < num_channels; i++ ) + arhosekskymodelstate_free( skymodel_state[i] ); + + +CIE XYZ Version of the Model +---------------------------- + +Usage of the CIE XYZ version of the model is exactly the same, except that +num_channels is of course always 3, and that ArHosekTristimSkyModelState and +arhosek_tristim_skymodel_radiance() have to be used instead of their spectral +counterparts. + +RGB Version of the Model +------------------------ + +The RGB version uses sRGB primaries with a linear gamma ramp. The same set of +functions as with the XYZ data is used, except the model is initialized +by calling arhosek_rgb_skymodelstate_alloc_init. + +Solar Radiance Function +----------------------- + +For each position on the solar disc, this function returns the entire radiance +one sees - direct emission, as well as in-scattered light in the area of the +solar disc. The latter is important for low solar elevations - nice images of +the setting sun would not be possible without this. This is also the reason why +this function, just like the regular sky dome model evaluation function, needs +access to the sky dome data structures, as these provide information on +in-scattered radiance. + +CAVEAT #1: in this release, this function is only provided in spectral form! + RGB/XYZ versions to follow at a later date. + +CAVEAT #2: (fixed from release 1.3 onwards) + +CAVEAT #3: limb darkening renders the brightness of the solar disc + inhomogeneous even for high solar elevations - only taking a single + sample at the centre of the sun will yield an incorrect power + estimate for the solar disc! Always take multiple random samples + across the entire solar disc to estimate its power! + +CAVEAT #4: in this version, the limb darkening calculations still use a fairly + computationally expensive 5th order polynomial that was directly + taken from astronomical literature. For the purposes of Computer + Graphics, this is needlessly accurate, though, and will be replaced + by a cheaper approximation in a future release. + +"Alien World" functionality +--------------------------- + +The Hosek sky model can be used to roughly (!) predict the appearance of +outdoor scenes on earth-like planets, i.e. planets of a similar size and +atmospheric make-up. Since the spectral version of our model predicts sky dome +luminance patterns and solar radiance independently for each waveband, and +since the intensity of each waveband is solely dependent on the input radiance +from the star that the world in question is orbiting, it is trivial to re-scale +the wavebands to match a different star radiance. + +At least in theory, the spectral version of the model has always been capable +of this sort of thing, and the actual sky dome and solar radiance models were +actually not altered at all in this release. All we did was to add some support +functionality for doing this more easily with the existing data and functions, +and to add some explanations. + +Just use 'arhosekskymodelstate_alienworld_alloc_init()' to initialise the sky +model states (you will have to provide values for star temperature and solar +intensity compared to the terrestrial sun), and do everything else as you +did before. + +CAVEAT #1: we assume the emission of the star that illuminates the alien world + to be a perfect blackbody emission spectrum. This is never entirely + realistic - real star emission spectra are considerably more complex + than this, mainly due to absorption effects in the outer layers of + stars. However, blackbody spectra are a reasonable first assumption + in a usage scenario like this, where 100% accuracy is simply not + necessary: for rendering purposes, there are likely no visible + differences between a highly accurate solution based on a more + involved simulation, and this approximation. + +CAVEAT #2: we always use limb darkening data from our own sun to provide this + "appearance feature", even for suns of strongly different + temperature. Which is presumably not very realistic, but (as with + the unaltered blackbody spectrum from caveat #1) probably not a bad + first guess, either. If you need more accuracy than we provide here, + please make inquiries with a friendly astro-physicst of your choice. + +CAVEAT #3: you have to provide a value for the solar intensity of the star + which illuminates the alien world. For this, please bear in mind + that there is very likely a comparatively tight range of absolute + solar irradiance values for which an earth-like planet with an + atmosphere like the one we assume in our model can exist in the + first place! + + Too much irradiance, and the atmosphere probably boils off into + space, too little, it freezes. Which means that stars of + considerably different emission colour than our sun will have to be + fairly different in size from it, to still provide a reasonable and + inhabitable amount of irradiance. Red stars will need to be much + larger than our sun, while white or blue stars will have to be + comparatively tiny. The initialisation function handles this and + computes a plausible solar radius for a given emission spectrum. In + terms of absolute radiometric values, you should probably not stray + all too far from a solar intensity value of 1.0. + +CAVEAT #4: although we now support different solar radii for the actual solar + disc, the sky dome luminance patterns are *not* parameterised by + this value - i.e. the patterns stay exactly the same for different + solar radii! Which is of course not correct. But in our experience, + solar discs up to several degrees in diameter (! - our own sun is + half a degree across) do not cause the luminance patterns on the sky + to change perceptibly. The reason we know this is that we initially + used unrealistically large suns in our brute force path tracer, in + order to improve convergence speeds (which in the beginning were + abysmal). Later, we managed to do the reference renderings much + faster even with realistically small suns, and found that there was + no real difference in skydome appearance anyway. + Conclusion: changing the solar radius should not be over-done, so + close orbits around red supergiants are a no-no. But for the + purposes of getting a fairly credible first impression of what an + alien world with a reasonably sized sun would look like, what we are + doing here is probably still o.k. + +HINT #1: if you want to model the sky of an earth-like planet that orbits + a binary star, just super-impose two of these models with solar + intensity of ~0.5 each, and closely spaced solar positions. Light is + additive, after all. Tattooine, here we come... :-) + + P.S. according to Star Wars canon, Tattooine orbits a binary + that is made up of a G and K class star, respectively. + So ~5500K and ~4200K should be good first guesses for their + temperature. Just in case you were wondering, after reading the + previous paragraph. +*/ + + +#ifndef _ARHOSEK_SKYMODEL_H_ +#define _ARHOSEK_SKYMODEL_H_ + +typedef double ArHosekSkyModelConfiguration[9]; + + +// Spectral version of the model + +/* ---------------------------------------------------------------------------- + + ArHosekSkyModelState struct + --------------------------- + + This struct holds the pre-computation data for one particular albedo value. + Most fields are self-explanatory, but users should never directly + manipulate any of them anyway. The only consistent way to manipulate such + structs is via the functions 'arhosekskymodelstate_alloc_init' and + 'arhosekskymodelstate_free'. + + 'emission_correction_factor_sky' + 'emission_correction_factor_sun' + + The original model coefficients were fitted against the emission of + our local sun. If a different solar emission is desired (i.e. if the + model is being used to predict skydome appearance for an earth-like + planet that orbits a different star), these correction factors, which + are determined during the alloc_init step, are applied to each waveband + separately (they default to 1.0 in normal usage). This is the simplest + way to retrofit this sort of capability to the existing model. The + different factors for sky and sun are needed since the solar disc may + be of a different size compared to the terrestrial sun. + +---------------------------------------------------------------------------- */ + +typedef struct ArHosekSkyModelState +{ + ArHosekSkyModelConfiguration configs[11]; + double radiances[11]; + double turbidity; + double solar_radius; + double emission_correction_factor_sky[11]; + double emission_correction_factor_sun[11]; + double albedo; + double elevation; +} +ArHosekSkyModelState; + +/* ---------------------------------------------------------------------------- + + arhosekskymodelstate_alloc_init() function + ------------------------------------------ + + Initialises an ArHosekSkyModelState struct for a terrestrial setting. + +---------------------------------------------------------------------------- */ + +ArHosekSkyModelState * arhosekskymodelstate_alloc_init( + const double solar_elevation, + const double atmospheric_turbidity, + const double ground_albedo + ); + + +/* ---------------------------------------------------------------------------- + + arhosekskymodelstate_alienworld_alloc_init() function + ----------------------------------------------------- + + Initialises an ArHosekSkyModelState struct for an "alien world" setting + with a sun of a surface temperature given in 'kelvin'. The parameter + 'solar_intensity' controls the overall brightness of the sky, relative + to the solar irradiance on Earth. A value of 1.0 yields a sky dome that + is, on average over the wavelenghts covered in the model (!), as bright + as the terrestrial sky in radiometric terms. + + Which means that the solar radius has to be adjusted, since the + emissivity of a solar surface with a given temperature is more or less + fixed. So hotter suns have to be smaller to be equally bright as the + terrestrial sun, while cooler suns have to be larger. Note that there are + limits to the validity of the luminance patterns of the underlying model: + see the discussion above for more on this. In particular, an alien sun with + a surface temperature of only 2000 Kelvin has to be very large if it is + to be as bright as the terrestrial sun - so large that the luminance + patterns are no longer a really good fit in that case. + + If you need information about the solar radius that the model computes + for a given temperature (say, for light source sampling purposes), you + have to query the 'solar_radius' variable of the sky model state returned + *after* running this function. + +---------------------------------------------------------------------------- */ + +ArHosekSkyModelState * arhosekskymodelstate_alienworld_alloc_init( + const double solar_elevation, + const double solar_intensity, + const double solar_surface_temperature_kelvin, + const double atmospheric_turbidity, + const double ground_albedo + ); + +void arhosekskymodelstate_free( + ArHosekSkyModelState * state + ); + +double arhosekskymodel_radiance( + ArHosekSkyModelState * state, + double theta, + double gamma, + double wavelength + ); + +// CIE XYZ and RGB versions + + +ArHosekSkyModelState * arhosek_xyz_skymodelstate_alloc_init( + const double turbidity, + const double albedo, + const double elevation + ); + + +ArHosekSkyModelState * arhosek_rgb_skymodelstate_alloc_init( + const double turbidity, + const double albedo, + const double elevation + ); + + +double arhosek_tristim_skymodel_radiance( + ArHosekSkyModelState * state, + double theta, + double gamma, + int channel + ); + +// Delivers the complete function: sky + sun, including limb darkening. +// Please read the above description before using this - there are several +// caveats! + +double arhosekskymodel_solar_radiance( + ArHosekSkyModelState * state, + double theta, + double gamma, + double wavelength + ); + + +#endif // _ARHOSEK_SKYMODEL_H_ diff --git a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/ArHosekSkyModelData_CIEXYZ.data b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/ArHosekSkyModelData_CIEXYZ.data index a5f3ce41..c6b85cf0 100644 --- a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/ArHosekSkyModelData_CIEXYZ.data +++ b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/ArHosekSkyModelData_CIEXYZ.data @@ -1,3863 +1,3863 @@ -/* -This source is published under the following 3-clause BSD license. - -Copyright (c) 2012 - 2013, Lukas Hosek and Alexander Wilkie -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * None of the names of the contributors may be used to endorse or promote - products derived from this software without specific prior written - permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - - -/* ============================================================================ - -This file is part of a sample implementation of the analytical skylight and -solar radiance models presented in the SIGGRAPH 2012 paper - - - "An Analytic Model for Full Spectral Sky-Dome Radiance" - -and the 2013 IEEE CG&A paper - - "Adding a Solar Radiance Function to the Hosek Skylight Model" - - both by - - Lukas Hosek and Alexander Wilkie - Charles University in Prague, Czech Republic - - - Version: 1.4a, February 22nd, 2013 - -Version history: - -1.4a February 22nd, 2013 - Removed unnecessary and counter-intuitive solar radius parameters - from the interface of the colourspace sky dome initialisation functions. - -1.4 February 11th, 2013 - Fixed a bug which caused the relative brightness of the solar disc - and the sky dome to be off by a factor of about 6. The sun was too - bright: this affected both normal and alien sun scenarios. The - coefficients of the solar radiance function were changed to fix this. - -1.3 January 21st, 2013 (not released to the public) - Added support for solar discs that are not exactly the same size as - the terrestrial sun. Also added support for suns with a different - emission spectrum ("Alien World" functionality). - -1.2a December 18th, 2012 - Fixed a mistake and some inaccuracies in the solar radiance function - explanations found in ArHosekSkyModel.h. The actual source code is - unchanged compared to version 1.2. - -1.2 December 17th, 2012 - Native RGB data and a solar radiance function that matches the turbidity - conditions were added. - -1.1 September 2012 - The coefficients of the spectral model are now scaled so that the output - is given in physical units: W / (m^-2 * sr * nm). Also, the output of the - XYZ model is now no longer scaled to the range [0...1]. Instead, it is - the result of a simple conversion from spectral data via the CIE 2 degree - standard observer matching functions. Therefore, after multiplication - with 683 lm / W, the Y channel now corresponds to luminance in lm. - -1.0 May 11th, 2012 - Initial release. - - -Please visit http://cgg.mff.cuni.cz/projects/SkylightModelling/ to check if -an updated version of this code has been published! - -============================================================================ */ - - -/* - -This file contains the coefficient data for the XYZ colour space version of -the model. - -*/ - -// Uses Sep 9 pattern / Aug 23 mean dataset - -double datasetXYZ1[] = -{ - // albedo 0, turbidity 1 - -1.117001e+000, - -1.867262e-001, - -1.113505e+001, - 1.259865e+001, - -3.937339e-002, - 1.167571e+000, - 7.100686e-003, - 3.592678e+000, - 6.083296e-001, - -1.152006e+000, - -1.926669e-001, - 6.152049e+000, - -4.770802e+000, - -8.704701e-002, - 7.483626e-001, - 3.372718e-002, - 4.464592e+000, - 4.036546e-001, - -1.072371e+000, - -2.696632e-001, - 2.816168e-001, - 1.820571e+000, - -3.742666e-001, - 2.080607e+000, - -7.675295e-002, - -2.835366e+000, - 1.129329e+000, - -1.109935e+000, - -1.532764e-001, - 1.198787e+000, - -9.015183e-001, - 5.173015e-003, - 5.749178e-001, - 1.075633e-001, - 4.387949e+000, - 2.650413e-001, - -1.052297e+000, - -2.229452e-001, - 1.952347e+000, - 5.727205e-001, - -4.885070e+000, - 1.984016e+000, - -1.106197e-001, - -4.898361e-001, - 8.907873e-001, - -1.070108e+000, - -1.600465e-001, - 1.593886e+000, - -4.479251e-005, - -3.306541e+000, - 9.390193e-001, - 9.513168e-002, - 2.343583e+000, - 5.335404e-001, - // albedo 0, turbidity 2 - -1.113253e+000, - -1.699600e-001, - -1.038822e+001, - 1.137513e+001, - -4.040911e-002, - 1.037455e+000, - 4.991792e-002, - 4.801919e+000, - 6.302710e-001, - -1.135747e+000, - -1.678594e-001, - 4.970755e+000, - -4.430230e+000, - -6.657408e-002, - 3.636161e-001, - 1.558009e-001, - 6.013370e+000, - 3.959601e-001, - -1.095892e+000, - -2.732595e-001, - 7.666496e-001, - 1.350731e+000, - -4.401401e-001, - 2.470135e+000, - -1.707929e-001, - -3.260793e+000, - 1.170337e+000, - -1.073668e+000, - -2.603929e-002, - -1.944589e-001, - 4.575207e-001, - 6.878164e-001, - -1.390770e-001, - 3.690299e-001, - 7.885781e+000, - 1.877694e-001, - -1.070091e+000, - -2.798957e-001, - 2.338478e+000, - -2.647221e+000, - -7.387808e+000, - 2.329210e+000, - -1.644639e-001, - -2.003710e+000, - 9.874527e-001, - -1.067120e+000, - -1.418866e-001, - 1.254090e+000, - 6.053048e+000, - -2.918892e+000, - 5.322812e-001, - 1.613053e-001, - 3.018161e+000, - 5.274090e-001, - // albedo 0, turbidity 3 - -1.129483e+000, - -1.890619e-001, - -9.065101e+000, - 9.659923e+000, - -3.607819e-002, - 8.314359e-001, - 8.181661e-002, - 4.768868e+000, - 6.339777e-001, - -1.146420e+000, - -1.883579e-001, - 3.309173e+000, - -3.127882e+000, - -6.938176e-002, - 3.987113e-001, - 1.400581e-001, - 6.283042e+000, - 5.267076e-001, - -1.128348e+000, - -2.641305e-001, - 1.223176e+000, - 5.514952e-002, - -3.490649e-001, - 1.997784e+000, - -4.123709e-002, - -2.251251e+000, - 9.483466e-001, - -1.025820e+000, - 1.404690e-002, - -1.187406e+000, - 2.729900e+000, - 5.877588e-001, - -2.761140e-001, - 4.602633e-001, - 8.305125e+000, - 3.945001e-001, - -1.083957e+000, - -2.606679e-001, - 2.207108e+000, - -7.202803e+000, - -5.968103e+000, - 2.129455e+000, - -7.789512e-002, - -1.137688e+000, - 8.871769e-001, - -1.062465e+000, - -1.512189e-001, - 1.042881e+000, - 1.427839e+001, - -4.242214e+000, - 4.038100e-001, - 1.997780e-001, - 2.814449e+000, - 5.803196e-001, - // albedo 0, turbidity 4 - -1.175099e+000, - -2.410789e-001, - -1.108587e+001, - 1.133404e+001, - -1.819300e-002, - 6.772942e-001, - 9.605043e-002, - 4.231166e+000, - 6.239972e-001, - -1.224207e+000, - -2.883527e-001, - 3.002206e+000, - -2.649612e+000, - -4.795418e-002, - 4.984398e-001, - 3.251434e-002, - 4.851611e+000, - 6.551019e-001, - -1.136955e+000, - -2.423048e-001, - 1.058823e+000, - -2.489236e-001, - -2.462179e-001, - 1.933140e+000, - 9.106828e-002, - -1.905869e-001, - 8.171065e-001, - -1.014535e+000, - -8.262500e-003, - -1.448017e+000, - 2.295788e+000, - 3.510334e-001, - -1.477418e+000, - 5.432449e-001, - 5.762796e+000, - 4.908751e-001, - -1.070666e+000, - -2.379780e-001, - 1.844589e+000, - -5.442448e+000, - -4.012768e+000, - 2.945275e+000, - 9.854725e-003, - 8.455959e-002, - 8.145030e-001, - -1.071525e+000, - -1.777132e-001, - 8.076590e-001, - 9.925865e+000, - -3.324623e+000, - -6.367437e-001, - 2.844581e-001, - 2.248384e+000, - 6.544022e-001, - // albedo 0, turbidity 5 - -1.218818e+000, - -2.952382e-001, - -1.345975e+001, - 1.347153e+001, - -6.814585e-003, - 5.079068e-001, - 1.197230e-001, - 3.776949e+000, - 5.836961e-001, - -1.409868e+000, - -5.114330e-001, - 2.776539e+000, - -2.039001e+000, - -2.673769e-002, - 4.145288e-001, - 7.829342e-004, - 2.275883e+000, - 6.629691e-001, - -1.069151e+000, - -9.434247e-002, - 7.293972e-001, - -1.222473e+000, - -1.533461e-001, - 2.160357e+000, - 4.626837e-002, - 3.852415e+000, - 8.593570e-001, - -1.021306e+000, - -1.149551e-001, - -1.108414e+000, - 4.178343e+000, - 4.013665e-001, - -2.222814e+000, - 6.929462e-001, - 1.392652e+000, - 4.401662e-001, - -1.074251e+000, - -2.224002e-001, - 1.372356e+000, - -8.858704e+000, - -3.922660e+000, - 3.020018e+000, - -1.458724e-002, - 1.511186e+000, - 8.288064e-001, - -1.062048e+000, - -1.526582e-001, - 4.921067e-001, - 1.485522e+001, - -3.229936e+000, - -8.426604e-001, - 3.916243e-001, - 2.678994e+000, - 6.689264e-001, - // albedo 0, turbidity 6 - -1.257023e+000, - -3.364700e-001, - -1.527795e+001, - 1.504223e+001, - 2.717715e-003, - 3.029910e-001, - 1.636851e-001, - 3.561663e+000, - 5.283161e-001, - -1.635124e+000, - -7.329993e-001, - 3.523939e+000, - -2.566337e+000, - -1.902543e-002, - 5.505483e-001, - -6.242176e-002, - 1.065992e+000, - 6.654236e-001, - -9.295823e-001, - 4.845834e-002, - -2.992990e-001, - -2.001327e-001, - -8.019339e-002, - 1.807806e+000, - 9.020277e-002, - 5.095372e+000, - 8.639936e-001, - -1.093740e+000, - -2.148608e-001, - -5.216240e-001, - 2.119777e+000, - 9.506454e-002, - -1.831439e+000, - 6.961204e-001, - 1.102084e-001, - 4.384319e-001, - -1.044181e+000, - -1.849257e-001, - 9.071246e-001, - -4.648901e+000, - -2.279385e+000, - 2.356502e+000, - -4.169147e-002, - 1.932557e+000, - 8.296550e-001, - -1.061451e+000, - -1.458745e-001, - 2.952267e-001, - 8.967214e+000, - -3.726228e+000, - -5.022316e-001, - 5.684877e-001, - 3.102347e+000, - 6.658443e-001, - // albedo 0, turbidity 7 - -1.332391e+000, - -4.127769e-001, - -9.328643e+000, - 9.046194e+000, - 3.457775e-003, - 3.377425e-001, - 1.530909e-001, - 3.301209e+000, - 4.997917e-001, - -1.932002e+000, - -9.947777e-001, - -2.042329e+000, - 3.586940e+000, - -5.642182e-002, - 8.130478e-001, - -8.195988e-002, - 1.118294e-001, - 5.617231e-001, - -8.707374e-001, - 1.286999e-001, - 1.820054e+000, - -4.674706e+000, - 3.317471e-003, - 5.919018e-001, - 1.975278e-001, - 6.686519e+000, - 9.631727e-001, - -1.070378e+000, - -3.030579e-001, - -9.041938e-001, - 6.200201e+000, - 1.232207e-001, - -3.650628e-001, - 5.029403e-001, - -2.903162e+000, - 3.811408e-001, - -1.063035e+000, - -1.637545e-001, - 5.853072e-001, - -7.889906e+000, - -1.200641e+000, - 1.035018e+000, - 1.192093e-001, - 3.267054e+000, - 8.416151e-001, - -1.053655e+000, - -1.562286e-001, - 2.423683e-001, - 1.128575e+001, - -4.363262e+000, - -7.314160e-002, - 5.642088e-001, - 2.514023e+000, - 6.670457e-001, - // albedo 0, turbidity 8 - -1.366112e+000, - -4.718287e-001, - -7.876222e+000, - 7.746900e+000, - -9.182309e-003, - 4.716076e-001, - 8.320252e-002, - 3.165603e+000, - 5.392334e-001, - -2.468204e+000, - -1.336340e+000, - -5.386723e+000, - 7.072672e+000, - -8.329266e-002, - 8.636876e-001, - -1.978177e-002, - -1.326218e-001, - 2.979222e-001, - -9.653522e-001, - -2.373416e-002, - 1.810250e+000, - -6.467262e+000, - 1.410706e-001, - -4.753717e-001, - 3.003095e-001, - 6.551163e+000, - 1.151083e+000, - -8.943186e-001, - -2.487152e-001, - -2.308960e-001, - 8.512648e+000, - 1.298402e-001, - 1.034705e+000, - 2.303509e-001, - -3.924095e+000, - 2.982717e-001, - -1.146999e+000, - -2.318784e-001, - 8.992419e-002, - -9.933614e+000, - -8.860920e-001, - -3.071656e-002, - 2.852012e-001, - 3.046199e+000, - 8.599001e-001, - -1.032399e+000, - -1.645145e-001, - 2.683599e-001, - 1.327701e+001, - -4.407670e+000, - 7.709869e-002, - 4.951727e-001, - 1.957277e+000, - 6.630943e-001, - // albedo 0, turbidity 9 - -1.469070e+000, - -6.135092e-001, - -6.506263e+000, - 6.661315e+000, - -3.835383e-002, - 7.150413e-001, - 7.784318e-003, - 2.820577e+000, - 6.756784e-001, - -2.501583e+000, - -1.247404e+000, - -1.523462e+001, - 1.633191e+001, - -1.204803e-002, - 5.896471e-001, - -2.002023e-002, - 1.144647e+000, - 6.177874e-002, - -2.438672e+000, - -1.127291e+000, - 5.731172e+000, - -1.021350e+001, - 6.165610e-002, - -7.752641e-001, - 4.708254e-001, - 4.176847e+000, - 1.200881e+000, - -1.513427e-001, - 9.792731e-002, - -1.612349e+000, - 9.814289e+000, - 5.188921e-002, - 1.716403e+000, - -7.039255e-002, - -2.815115e+000, - 3.291874e-001, - -1.318511e+000, - -3.650554e-001, - 4.221268e-001, - -9.294529e+000, - -4.397520e-002, - -8.100625e-001, - 3.742719e-001, - 1.834166e+000, - 8.223450e-001, - -1.016009e+000, - -1.820264e-001, - 1.278426e-001, - 1.182696e+001, - -4.801528e+000, - 4.947899e-001, - 4.660378e-001, - 1.601254e+000, - 6.702359e-001, - // albedo 0, turbidity 10 - -1.841310e+000, - -9.781779e-001, - -4.610903e+000, - 4.824662e+000, - -5.100806e-002, - 6.463776e-001, - -6.377724e-006, - 2.216875e+000, - 8.618530e-001, - -2.376373e+000, - -1.108657e+000, - -1.489799e+001, - 1.546458e+001, - 4.091025e-002, - 9.761780e-002, - -1.048958e-002, - 2.165834e+000, - -1.609171e-001, - -4.710318e+000, - -2.261963e+000, - 6.947327e+000, - -1.034828e+001, - -1.325542e-001, - 7.508674e-001, - 2.247553e-001, - 2.873142e+000, - 1.297100e+000, - 2.163750e-001, - -1.944345e-001, - -2.437860e+000, - 1.011314e+001, - 4.450500e-001, - 3.111492e-001, - 2.751323e-001, - -1.627906e+000, - 2.531213e-001, - -1.258794e+000, - -3.524641e-001, - 8.425444e-001, - -1.085313e+001, - -1.154381e+000, - -4.638014e-001, - -2.781115e-003, - 4.344498e-001, - 8.507091e-001, - -1.018938e+000, - -1.804153e-001, - -6.354054e-002, - 1.573150e+001, - -4.386999e+000, - 6.211115e-001, - 5.294648e-001, - 1.580749e+000, - 6.586655e-001, - // albedo 1, turbidity 1 - -1.116416e+000, - -1.917524e-001, - -1.068233e+001, - 1.222221e+001, - -3.668978e-002, - 1.054022e+000, - 1.592132e-002, - 3.180583e+000, - 5.627370e-001, - -1.132341e+000, - -1.671286e-001, - 5.976499e+000, - -4.227366e+000, - -9.542489e-002, - 8.664938e-001, - 8.351793e-003, - 4.876068e+000, - 4.492779e-001, - -1.087635e+000, - -3.173679e-001, - 4.314407e-001, - 1.100555e+000, - -4.410057e-001, - 1.677253e+000, - -3.005925e-002, - -4.201249e+000, - 1.070902e+000, - -1.083031e+000, - -8.847705e-002, - 1.291773e+000, - 4.546776e-001, - 3.091894e-001, - 7.261760e-001, - 4.203659e-002, - 5.990615e+000, - 3.704756e-001, - -1.057899e+000, - -2.246706e-001, - 2.329563e+000, - -1.219656e+000, - -5.335260e+000, - 8.545378e-001, - -3.906209e-002, - -9.025499e-001, - 7.797348e-001, - -1.073305e+000, - -1.522553e-001, - 1.767063e+000, - 1.904280e+000, - -3.101673e+000, - 3.995856e-001, - 2.905192e-002, - 2.563977e+000, - 5.753067e-001, - // albedo 1, turbidity 2 - -1.113674e+000, - -1.759694e-001, - -9.754125e+000, - 1.087391e+001, - -3.841093e-002, - 9.524272e-001, - 5.680219e-002, - 4.227034e+000, - 6.029571e-001, - -1.126496e+000, - -1.680281e-001, - 5.332352e+000, - -4.575579e+000, - -6.761755e-002, - 3.295335e-001, - 1.194896e-001, - 5.570901e+000, - 4.536185e-001, - -1.103074e+000, - -2.681801e-001, - 6.571479e-002, - 2.396522e+000, - -4.551280e-001, - 2.466331e+000, - -1.232022e-001, - -3.023201e+000, - 1.086379e+000, - -1.053299e+000, - -2.697173e-002, - 8.379121e-001, - -9.681458e-001, - 5.890692e-001, - -4.872027e-001, - 2.936929e-001, - 7.510139e+000, - 3.079122e-001, - -1.079553e+000, - -2.710448e-001, - 2.462379e+000, - -3.713554e-001, - -8.534512e+000, - 1.828242e+000, - -1.686398e-001, - -1.961340e+000, - 8.941077e-001, - -1.069741e+000, - -1.396394e-001, - 1.657868e+000, - 3.236313e+000, - -2.706344e+000, - -2.948122e-001, - 1.314816e-001, - 2.868457e+000, - 5.413403e-001, - // albedo 1, turbidity 3 - -1.131649e+000, - -1.954455e-001, - -7.751595e+000, - 8.685861e+000, - -4.910871e-002, - 8.992952e-001, - 4.710143e-002, - 4.254818e+000, - 6.821116e-001, - -1.156689e+000, - -1.884324e-001, - 3.163519e+000, - -3.091522e+000, - -6.613927e-002, - -2.575883e-002, - 1.640065e-001, - 6.073643e+000, - 4.453468e-001, - -1.079224e+000, - -2.621389e-001, - 9.446437e-001, - 1.448479e+000, - -3.969384e-001, - 2.626638e+000, - -8.101186e-002, - -3.016355e+000, - 1.076295e+000, - -1.080832e+000, - 1.033057e-002, - -3.500156e-001, - -3.281419e-002, - 5.655512e-001, - -1.156742e+000, - 4.534710e-001, - 8.774122e+000, - 2.772869e-001, - -1.051202e+000, - -2.679975e-001, - 2.719109e+000, - -2.190316e+000, - -6.878798e+000, - 2.250481e+000, - -2.030252e-001, - -2.026527e+000, - 9.701096e-001, - -1.089849e+000, - -1.598589e-001, - 1.564748e+000, - 6.869187e+000, - -3.053670e+000, - -6.110435e-001, - 1.644472e-001, - 2.370452e+000, - 5.511770e-001, - // albedo 1, turbidity 4 - -1.171419e+000, - -2.429746e-001, - -8.991334e+000, - 9.571216e+000, - -2.772861e-002, - 6.688262e-001, - 7.683478e-002, - 3.785611e+000, - 6.347635e-001, - -1.228554e+000, - -2.917562e-001, - 2.753986e+000, - -2.491780e+000, - -4.663434e-002, - 3.118303e-001, - 7.546506e-002, - 4.463096e+000, - 5.955071e-001, - -1.093124e+000, - -2.447767e-001, - 9.097406e-001, - 5.448296e-001, - -2.957824e-001, - 2.024167e+000, - -5.152333e-004, - -1.069081e+000, - 9.369565e-001, - -1.056994e+000, - 1.569507e-002, - -8.217491e-001, - 1.870818e+000, - 7.061930e-001, - -1.483928e+000, - 5.978206e-001, - 6.864902e+000, - 3.673332e-001, - -1.054871e+000, - -2.758129e-001, - 2.712807e+000, - -5.950110e+000, - -6.554039e+000, - 2.447523e+000, - -1.895171e-001, - -1.454292e+000, - 9.131738e-001, - -1.100218e+000, - -1.746241e-001, - 1.438505e+000, - 1.115481e+001, - -3.266076e+000, - -8.837357e-001, - 1.970100e-001, - 1.991595e+000, - 5.907821e-001, - // albedo 1, turbidity 5 - -1.207267e+000, - -2.913610e-001, - -1.103767e+001, - 1.140724e+001, - -1.416800e-002, - 5.564047e-001, - 8.476262e-002, - 3.371255e+000, - 6.221335e-001, - -1.429698e+000, - -5.374218e-001, - 2.837524e+000, - -2.221936e+000, - -2.422337e-002, - 9.313758e-002, - 7.190250e-002, - 1.869022e+000, - 5.609035e-001, - -1.002274e+000, - -6.972810e-002, - 4.031308e-001, - -3.932997e-001, - -1.521923e-001, - 2.390646e+000, - -6.893990e-002, - 2.999661e+000, - 1.017843e+000, - -1.081168e+000, - -1.178666e-001, - -4.968080e-001, - 3.919299e+000, - 6.046866e-001, - -2.440615e+000, - 7.891538e-001, - 2.140835e+000, - 2.740470e-001, - -1.050727e+000, - -2.307688e-001, - 2.276396e+000, - -9.454407e+000, - -5.505176e+000, - 2.992620e+000, - -2.450942e-001, - 6.078372e-001, - 9.606765e-001, - -1.103752e+000, - -1.810202e-001, - 1.375044e+000, - 1.589095e+001, - -3.438954e+000, - -1.265669e+000, - 2.475172e-001, - 1.680768e+000, - 5.978056e-001, - // albedo 1, turbidity 6 - -1.244324e+000, - -3.378542e-001, - -1.111001e+001, - 1.137784e+001, - -7.896794e-003, - 4.808023e-001, - 9.249904e-002, - 3.025816e+000, - 5.880239e-001, - -1.593165e+000, - -7.027621e-001, - 2.220896e+000, - -1.437709e+000, - -1.534738e-002, - 6.286958e-002, - 6.644555e-002, - 1.091727e+000, - 5.470080e-001, - -9.136506e-001, - 1.344874e-002, - 7.772636e-001, - -1.209396e+000, - -1.408978e-001, - 2.433718e+000, - -1.041938e-001, - 3.791244e+000, - 1.037916e+000, - -1.134968e+000, - -1.803315e-001, - -9.267335e-001, - 4.576670e+000, - 6.851928e-001, - -2.805000e+000, - 8.687208e-001, - 1.161483e+000, - 2.571688e-001, - -1.017037e+000, - -2.053943e-001, - 2.361640e+000, - -9.887818e+000, - -5.122889e+000, - 3.287088e+000, - -2.594102e-001, - 8.578927e-001, - 9.592340e-001, - -1.118723e+000, - -1.934942e-001, - 1.226023e+000, - 1.674140e+001, - -3.277335e+000, - -1.629809e+000, - 2.765232e-001, - 1.637713e+000, - 6.113963e-001, - // albedo 1, turbidity 7 - -1.314779e+000, - -4.119915e-001, - -1.241150e+001, - 1.241578e+001, - 2.344284e-003, - 2.980837e-001, - 1.414613e-001, - 2.781731e+000, - 4.998556e-001, - -1.926199e+000, - -1.020038e+000, - 2.569200e+000, - -1.081159e+000, - -2.266833e-002, - 3.588668e-001, - 8.750078e-003, - -2.452171e-001, - 4.796758e-001, - -7.780002e-001, - 1.850647e-001, - 4.445456e-002, - -2.409297e+000, - -7.816346e-002, - 1.546790e+000, - -2.807227e-002, - 5.998176e+000, - 1.132396e+000, - -1.179326e+000, - -3.578330e-001, - -2.392933e-001, - 6.467883e+000, - 5.904596e-001, - -1.869975e+000, - 8.045839e-001, - -2.498121e+000, - 1.610633e-001, - -1.009956e+000, - -1.311896e-001, - 1.726577e+000, - -1.219356e+001, - -3.466239e+000, - 2.343602e+000, - -2.252205e-001, - 2.573681e+000, - 1.027109e+000, - -1.112460e+000, - -2.063093e-001, - 1.233051e+000, - 2.058946e+001, - -4.578074e+000, - -1.145643e+000, - 3.160192e-001, - 1.420159e+000, - 5.860212e-001, - // albedo 1, turbidity 8 - -1.371689e+000, - -4.914196e-001, - -1.076610e+001, - 1.107405e+001, - -1.485077e-002, - 5.936218e-001, - 3.685482e-002, - 2.599968e+000, - 6.002204e-001, - -2.436997e+000, - -1.377939e+000, - 2.130141e-002, - 1.079593e+000, - -1.796232e-002, - -3.933248e-002, - 1.610711e-001, - -6.901181e-001, - 1.206416e-001, - -8.743368e-001, - 7.331370e-002, - 8.734259e-001, - -3.743126e+000, - -3.151167e-002, - 1.297596e+000, - -7.634926e-002, - 6.532873e+000, - 1.435737e+000, - -9.810197e-001, - -3.521634e-001, - -2.855205e-001, - 7.134674e+000, - 6.839748e-001, - -1.394841e+000, - 6.952036e-001, - -4.633104e+000, - -2.173401e-002, - -1.122958e+000, - -1.691536e-001, - 1.382360e+000, - -1.102913e+001, - -2.608171e+000, - 1.865111e+000, - -1.345154e-001, - 3.112342e+000, - 1.094134e+000, - -1.075586e+000, - -2.077415e-001, - 1.171477e+000, - 1.793270e+001, - -4.656858e+000, - -1.036839e+000, - 3.338295e-001, - 1.042793e+000, - 5.739374e-001, - // albedo 1, turbidity 9 - -1.465871e+000, - -6.364486e-001, - -8.833718e+000, - 9.343650e+000, - -3.223600e-002, - 7.552848e-001, - -3.121341e-006, - 2.249164e+000, - 8.094662e-001, - -2.448924e+000, - -1.270878e+000, - -4.823703e+000, - 5.853058e+000, - -2.149127e-002, - 3.581132e-002, - -1.230276e-003, - 4.892553e-001, - -1.597657e-001, - -2.419809e+000, - -1.071337e+000, - 1.575648e+000, - -4.983580e+000, - 9.545185e-003, - 5.032615e-001, - 4.186266e-001, - 4.634147e+000, - 1.433517e+000, - -1.383278e-001, - -2.797095e-002, - -1.943067e-001, - 6.679623e+000, - 4.118280e-001, - -2.744289e-001, - -2.118722e-002, - -4.337025e+000, - 1.505072e-001, - -1.341872e+000, - -2.518572e-001, - 1.027009e+000, - -6.527103e+000, - -1.081271e+000, - 1.015465e+000, - 2.845789e-001, - 2.470371e+000, - 9.278120e-001, - -1.040640e+000, - -2.367454e-001, - 1.100744e+000, - 8.827253e+000, - -4.560794e+000, - -7.287017e-001, - 2.842503e-001, - 6.336593e-001, - 6.327335e-001, - // albedo 1, turbidity 10 - -1.877993e+000, - -1.025135e+000, - -4.311037e+000, - 4.715016e+000, - -4.711631e-002, - 6.335844e-001, - -7.665398e-006, - 1.788017e+000, - 9.001409e-001, - -2.281540e+000, - -1.137668e+000, - -1.036869e+001, - 1.136254e+001, - 1.961739e-002, - -9.836174e-002, - -6.734567e-003, - 1.320918e+000, - -2.400807e-001, - -4.904054e+000, - -2.315781e+000, - 5.735999e+000, - -8.626257e+000, - -1.255643e-001, - 1.545446e+000, - 1.396860e-001, - 2.972897e+000, - 1.429934e+000, - 4.077067e-001, - -1.833688e-001, - -2.450939e+000, - 9.119433e+000, - 4.505361e-001, - -1.340828e+000, - 3.973690e-001, - -1.785370e+000, - 9.628711e-002, - -1.296052e+000, - -3.250526e-001, - 1.813294e+000, - -1.031485e+001, - -1.388690e+000, - 1.239733e+000, - -8.989196e-002, - -3.389637e-001, - 9.639560e-001, - -1.062181e+000, - -2.423444e-001, - 7.577592e-001, - 1.566938e+001, - -4.462264e+000, - -5.742810e-001, - 3.262259e-001, - 9.461672e-001, - 6.232887e-001, -}; - -double datasetXYZRad1[] = -{ - // albedo 0, turbidity 1 - 1.560219e+000, - 1.417388e+000, - 1.206927e+000, - 1.091949e+001, - 5.931416e+000, - 7.304788e+000, - // albedo 0, turbidity 2 - 1.533049e+000, - 1.560532e+000, - 3.685059e-001, - 1.355040e+001, - 5.543711e+000, - 7.792189e+000, - // albedo 0, turbidity 3 - 1.471043e+000, - 1.746088e+000, - -9.299697e-001, - 1.720362e+001, - 5.473384e+000, - 8.336416e+000, - // albedo 0, turbidity 4 - 1.355991e+000, - 2.109348e+000, - -3.295855e+000, - 2.264843e+001, - 5.454607e+000, - 9.304656e+000, - // albedo 0, turbidity 5 - 1.244963e+000, - 2.547533e+000, - -5.841485e+000, - 2.756879e+001, - 5.576104e+000, - 1.043287e+001, - // albedo 0, turbidity 6 - 1.175532e+000, - 2.784634e+000, - -7.212225e+000, - 2.975347e+001, - 6.472980e+000, - 1.092331e+001, - // albedo 0, turbidity 7 - 1.082973e+000, - 3.118094e+000, - -8.934293e+000, - 3.186879e+001, - 8.473885e+000, - 1.174019e+001, - // albedo 0, turbidity 8 - 9.692500e-001, - 3.349574e+000, - -1.003810e+001, - 3.147654e+001, - 1.338931e+001, - 1.272547e+001, - // albedo 0, turbidity 9 - 8.547044e-001, - 3.151538e+000, - -9.095567e+000, - 2.554995e+001, - 2.273219e+001, - 1.410398e+001, - // albedo 0, turbidity 10 - 7.580340e-001, - 2.311153e+000, - -5.170814e+000, - 1.229669e+001, - 3.686529e+001, - 1.598882e+001, - // albedo 1, turbidity 1 - 1.664273e+000, - 1.574468e+000, - 1.422078e+000, - 9.768247e+000, - 1.447338e+001, - 1.644988e+001, - // albedo 1, turbidity 2 - 1.638295e+000, - 1.719586e+000, - 5.786675e-001, - 1.239846e+001, - 1.415419e+001, - 1.728605e+001, - // albedo 1, turbidity 3 - 1.572623e+000, - 1.921559e+000, - -7.714802e-001, - 1.609246e+001, - 1.420954e+001, - 1.825908e+001, - // albedo 1, turbidity 4 - 1.468395e+000, - 2.211970e+000, - -2.845869e+000, - 2.075027e+001, - 1.524822e+001, - 1.937622e+001, - // albedo 1, turbidity 5 - 1.355047e+000, - 2.556469e+000, - -4.960920e+000, - 2.460237e+001, - 1.648360e+001, - 2.065648e+001, - // albedo 1, turbidity 6 - 1.291642e+000, - 2.742036e+000, - -6.061967e+000, - 2.602002e+001, - 1.819144e+001, - 2.116712e+001, - // albedo 1, turbidity 7 - 1.194565e+000, - 2.972120e+000, - -7.295779e+000, - 2.691805e+001, - 2.124880e+001, - 2.201819e+001, - // albedo 1, turbidity 8 - 1.083631e+000, - 3.047021e+000, - -7.766096e+000, - 2.496261e+001, - 2.744264e+001, - 2.291875e+001, - // albedo 1, turbidity 9 - 9.707994e-001, - 2.736459e+000, - -6.308284e+000, - 1.760860e+001, - 3.776291e+001, - 2.392150e+001, - // albedo 1, turbidity 10 - 8.574294e-001, - 1.865155e+000, - -2.364707e+000, - 4.337793e+000, - 5.092831e+001, - 2.523432e+001, -}; - -double datasetXYZ2[] = -{ - // albedo 0, turbidity 1 - -1.127942e+000, - -1.905548e-001, - -1.252356e+001, - 1.375799e+001, - -3.624732e-002, - 1.055453e+000, - 1.385036e-002, - 4.176970e+000, - 5.928345e-001, - -1.155260e+000, - -1.778135e-001, - 6.216056e+000, - -5.254116e+000, - -8.787445e-002, - 8.434621e-001, - 4.025734e-002, - 6.195322e+000, - 3.111856e-001, - -1.125624e+000, - -3.217593e-001, - 5.043919e-001, - 1.686284e+000, - -3.536071e-001, - 1.476321e+000, - -7.899019e-002, - -4.522531e+000, - 1.271691e+000, - -1.081801e+000, - -1.033234e-001, - 9.995550e-001, - 7.482946e-003, - -6.776018e-002, - 1.463141e+000, - 9.492021e-002, - 5.612723e+000, - 1.298846e-001, - -1.075320e+000, - -2.402711e-001, - 2.141284e+000, - -1.203359e+000, - -4.945188e+000, - 1.437221e+000, - -8.096750e-002, - -1.028378e+000, - 1.004164e+000, - -1.073337e+000, - -1.516517e-001, - 1.639379e+000, - 2.304669e+000, - -3.214244e+000, - 1.286245e+000, - 5.613957e-002, - 2.480902e+000, - 4.999363e-001, - // albedo 0, turbidity 2 - -1.128399e+000, - -1.857793e-001, - -1.089863e+001, - 1.172984e+001, - -3.768099e-002, - 9.439285e-001, - 4.869335e-002, - 4.845114e+000, - 6.119211e-001, - -1.114002e+000, - -1.399280e-001, - 4.963800e+000, - -4.685500e+000, - -7.780879e-002, - 4.049736e-001, - 1.586297e-001, - 7.770264e+000, - 3.449006e-001, - -1.185472e+000, - -3.403543e-001, - 6.588322e-001, - 1.133713e+000, - -4.118674e-001, - 2.061191e+000, - -1.882768e-001, - -4.372586e+000, - 1.223530e+000, - -1.002272e+000, - 2.000703e-002, - 7.073269e-002, - 1.485075e+000, - 5.005589e-001, - 4.301494e-001, - 3.626541e-001, - 7.921098e+000, - 1.574766e-001, - -1.121006e+000, - -3.007777e-001, - 2.242051e+000, - -4.571561e+000, - -7.761071e+000, - 2.053404e+000, - -1.524018e-001, - -1.886162e+000, - 1.018208e+000, - -1.058864e+000, - -1.358673e-001, - 1.389667e+000, - 8.633409e+000, - -3.437249e+000, - 7.295429e-001, - 1.514700e-001, - 2.842513e+000, - 5.014325e-001, - // albedo 0, turbidity 3 - -1.144464e+000, - -2.043799e-001, - -1.020188e+001, - 1.071247e+001, - -3.256693e-002, - 7.860205e-001, - 6.872719e-002, - 4.824771e+000, - 6.259836e-001, - -1.170104e+000, - -2.118626e-001, - 4.391405e+000, - -4.198900e+000, - -7.111559e-002, - 3.890442e-001, - 1.024831e-001, - 6.282535e+000, - 5.365688e-001, - -1.129171e+000, - -2.552880e-001, - 2.238298e-001, - 7.314295e-001, - -3.562730e-001, - 1.881931e+000, - -3.078716e-002, - -1.039120e+000, - 9.096301e-001, - -1.042294e+000, - 4.450203e-003, - -5.116033e-001, - 2.627589e+000, - 6.098996e-001, - -1.264638e-001, - 4.325281e-001, - 7.080503e+000, - 4.583646e-001, - -1.082293e+000, - -2.723056e-001, - 2.065076e+000, - -8.143133e+000, - -7.892212e+000, - 2.142231e+000, - -7.106240e-002, - -1.122398e+000, - 8.338505e-001, - -1.071715e+000, - -1.426568e-001, - 1.095351e+000, - 1.729783e+001, - -3.851931e+000, - 4.360514e-001, - 2.114440e-001, - 2.970832e+000, - 5.944389e-001, - // albedo 0, turbidity 4 - -1.195909e+000, - -2.590449e-001, - -1.191037e+001, - 1.207947e+001, - -1.589842e-002, - 6.297846e-001, - 9.054772e-002, - 4.285959e+000, - 5.933752e-001, - -1.245763e+000, - -3.316637e-001, - 4.293660e+000, - -3.694011e+000, - -4.699947e-002, - 4.843684e-001, - 2.130425e-002, - 4.097549e+000, - 6.530809e-001, - -1.148742e+000, - -1.902509e-001, - -2.393233e-001, - -2.441254e-001, - -2.610918e-001, - 1.846988e+000, - 3.532866e-002, - 2.660106e+000, - 8.358294e-001, - -1.016080e+000, - -7.444960e-002, - -5.053436e-001, - 4.388855e+000, - 6.054987e-001, - -1.208300e+000, - 5.817215e-001, - 2.543570e+000, - 4.726568e-001, - -1.072027e+000, - -2.101440e-001, - 1.518378e+000, - -1.060119e+001, - -6.016546e+000, - 2.649475e+000, - -5.166992e-002, - 1.571269e+000, - 8.344622e-001, - -1.072365e+000, - -1.511201e-001, - 7.478010e-001, - 1.900732e+001, - -3.950387e+000, - -3.473907e-001, - 3.797211e-001, - 2.782949e+000, - 6.296808e-001, - // albedo 0, turbidity 5 - -1.239423e+000, - -3.136289e-001, - -1.351100e+001, - 1.349468e+001, - -7.070423e-003, - 5.012315e-001, - 1.106008e-001, - 3.803619e+000, - 5.577948e-001, - -1.452524e+000, - -5.676944e-001, - 2.993153e+000, - -2.277288e+000, - -2.168954e-002, - 3.056720e-001, - 1.152338e-002, - 1.852697e+000, - 6.427228e-001, - -1.061421e+000, - -4.590521e-002, - 6.057022e-001, - -1.096835e+000, - -1.504952e-001, - 2.344921e+000, - -5.491832e-002, - 5.268322e+000, - 9.082253e-001, - -1.042373e+000, - -1.769498e-001, - -1.075388e+000, - 3.831712e+000, - 3.154140e-001, - -2.416458e+000, - 7.909032e-001, - -1.492892e-002, - 3.854049e-001, - -1.064159e+000, - -1.892684e-001, - 1.438685e+000, - -8.166362e+000, - -3.616364e+000, - 3.275206e+000, - -1.203825e-001, - 2.039491e+000, - 8.688057e-001, - -1.070120e+000, - -1.569508e-001, - 4.124760e-001, - 1.399683e+001, - -3.547085e+000, - -1.046326e+000, - 4.973825e-001, - 2.791231e+000, - 6.503286e-001, - // albedo 0, turbidity 6 - -1.283579e+000, - -3.609518e-001, - -1.335397e+001, - 1.315248e+001, - -4.431938e-004, - 3.769526e-001, - 1.429824e-001, - 3.573613e+000, - 4.998696e-001, - -1.657952e+000, - -7.627948e-001, - 1.958222e+000, - -7.949816e-001, - -2.882837e-002, - 5.356149e-001, - -5.191946e-002, - 8.869955e-001, - 6.263320e-001, - -9.527600e-001, - 6.494189e-002, - 5.361303e-001, - -2.129590e+000, - -9.258630e-002, - 1.604776e+000, - 5.067770e-002, - 6.376055e+000, - 9.138052e-001, - -1.080827e+000, - -2.523120e-001, - -7.154262e-001, - 4.120085e+000, - 1.878228e-001, - -1.492158e+000, - 6.881655e-001, - -1.446611e+000, - 4.040631e-001, - -1.054075e+000, - -1.665498e-001, - 9.191052e-001, - -6.636943e+000, - -1.894826e+000, - 2.107810e+000, - -3.680499e-002, - 2.655452e+000, - 8.413840e-001, - -1.061127e+000, - -1.448849e-001, - 2.667493e-001, - 1.034103e+001, - -4.285769e+000, - -3.874504e-001, - 5.998752e-001, - 3.132426e+000, - 6.652753e-001, - // albedo 0, turbidity 7 - -1.347345e+000, - -4.287832e-001, - -9.305553e+000, - 9.133813e+000, - -3.173527e-003, - 3.977564e-001, - 1.151420e-001, - 3.320564e+000, - 4.998134e-001, - -1.927296e+000, - -9.901372e-001, - -2.593499e+000, - 4.087421e+000, - -5.833993e-002, - 8.158929e-001, - -4.681279e-002, - 2.423716e-001, - 4.938052e-001, - -9.470092e-001, - 7.325237e-002, - 2.064735e+000, - -5.167540e+000, - -1.313751e-002, - 4.832169e-001, - 1.126295e-001, - 6.970522e+000, - 1.035022e+000, - -1.022557e+000, - -2.762616e-001, - -9.375748e-001, - 6.696739e+000, - 2.200765e-001, - -1.133253e-001, - 5.492505e-001, - -3.109391e+000, - 3.321914e-001, - -1.087444e+000, - -1.836263e-001, - 6.225024e-001, - -8.576765e+000, - -1.107637e+000, - 7.859427e-001, - 9.910909e-002, - 3.112938e+000, - 8.596261e-001, - -1.051544e+000, - -1.546262e-001, - 2.371731e-001, - 1.200502e+001, - -4.527291e+000, - 7.268862e-002, - 5.571478e-001, - 2.532873e+000, - 6.662000e-001, - // albedo 0, turbidity 8 - -1.375576e+000, - -4.840019e-001, - -8.121290e+000, - 8.058140e+000, - -1.445661e-002, - 5.123314e-001, - 5.813321e-002, - 3.203219e+000, - 5.442318e-001, - -2.325221e+000, - -1.241463e+000, - -7.063430e+000, - 8.741369e+000, - -7.829950e-002, - 8.844273e-001, - -3.471106e-002, - 1.740583e-001, - 2.814079e-001, - -1.228700e+000, - -2.013412e-001, - 2.949042e+000, - -7.371945e+000, - 1.071753e-001, - -2.491970e-001, - 2.265223e-001, - 6.391504e+000, - 1.172389e+000, - -7.601786e-001, - -1.680631e-001, - -7.584444e-001, - 8.541356e+000, - 8.222291e-002, - 6.729633e-001, - 3.206615e-001, - -3.700940e+000, - 2.710054e-001, - -1.191166e+000, - -2.672347e-001, - 2.927498e-001, - -9.713613e+000, - -4.783721e-001, - 2.352803e-001, - 2.161949e-001, - 2.691481e+000, - 8.745447e-001, - -1.030135e+000, - -1.653301e-001, - 2.263443e-001, - 1.296157e+001, - -4.650644e+000, - 7.055709e-003, - 5.091975e-001, - 2.000370e+000, - 6.603839e-001, - // albedo 0, turbidity 9 - -1.508018e+000, - -6.460933e-001, - -6.402745e+000, - 6.545995e+000, - -3.750320e-002, - 6.921803e-001, - 3.309819e-003, - 2.797527e+000, - 6.978446e-001, - -2.333308e+000, - -1.167837e+000, - -1.746787e+001, - 1.868630e+001, - -8.948229e-003, - 5.621946e-001, - -3.402626e-002, - 1.217943e+000, - 1.149865e-002, - -2.665953e+000, - -1.226307e+000, - 7.169725e+000, - -1.159434e+001, - 3.583420e-002, - -3.074378e-001, - 3.412248e-001, - 4.422122e+000, - 1.283791e+000, - -9.705116e-002, - 8.312991e-002, - -2.160462e+000, - 1.028235e+001, - 3.543357e-002, - 1.032049e+000, - 1.058310e-001, - -2.972898e+000, - 2.418628e-001, - -1.329617e+000, - -3.699557e-001, - 5.560117e-001, - -9.730113e+000, - 9.938865e-002, - -3.071488e-001, - 2.510691e-001, - 1.777111e+000, - 8.705142e-001, - -1.019387e+000, - -1.893247e-001, - 1.194079e-001, - 1.239436e+001, - -4.799224e+000, - 2.940213e-001, - 4.841268e-001, - 1.529724e+000, - 6.582615e-001, - // albedo 0, turbidity 10 - -1.896737e+000, - -1.005442e+000, - -6.411032e+000, - 6.548220e+000, - -3.227596e-002, - 5.717262e-001, - -8.115192e-006, - 2.296704e+000, - 9.000749e-001, - -2.411116e+000, - -1.225587e+000, - -1.753629e+001, - 1.829393e+001, - 1.247555e-002, - 2.364616e-001, - -5.114637e-003, - 1.603778e+000, - -2.224156e-001, - -4.707121e+000, - -2.074977e+000, - 7.942300e+000, - -1.132407e+001, - -5.415654e-002, - 5.446811e-001, - 1.032493e-001, - 4.010235e+000, - 1.369802e+000, - 1.010482e-001, - -4.013305e-001, - -2.674579e+000, - 9.779409e+000, - 1.782506e-001, - 7.053045e-001, - 4.200002e-001, - -2.400671e+000, - 1.953165e-001, - -1.243526e+000, - -3.391255e-001, - 8.848882e-001, - -9.789025e+000, - -3.997324e-001, - -9.546227e-001, - -1.044017e-001, - 6.010593e-001, - 8.714462e-001, - -1.014633e+000, - -1.730009e-001, - -7.738934e-002, - 1.390903e+001, - -4.847307e+000, - 1.076059e+000, - 5.685743e-001, - 1.572992e+000, - 6.561432e-001, - // albedo 1, turbidity 1 - -1.122998e+000, - -1.881183e-001, - -1.030709e+001, - 1.158932e+001, - -4.079495e-002, - 9.603774e-001, - 3.079436e-002, - 4.009235e+000, - 5.060745e-001, - -1.134790e+000, - -1.539688e-001, - 5.478405e+000, - -4.217270e+000, - -1.043858e-001, - 7.165008e-001, - 1.524765e-002, - 6.473623e+000, - 4.207882e-001, - -1.134957e+000, - -3.513318e-001, - 7.393837e-001, - 1.354415e+000, - -4.764078e-001, - 1.690441e+000, - -5.492640e-002, - -5.563523e+000, - 1.145743e+000, - -1.058344e+000, - -5.758503e-002, - 1.168230e+000, - 3.269824e-001, - 1.795193e-001, - 7.849011e-001, - 7.441853e-002, - 6.904804e+000, - 2.818790e-001, - -1.075194e+000, - -2.355813e-001, - 2.463685e+000, - -1.536505e+000, - -7.505771e+000, - 9.619712e-001, - -6.465851e-002, - -1.355492e+000, - 8.489847e-001, - -1.079030e+000, - -1.465328e-001, - 1.773838e+000, - 2.310131e+000, - -3.136065e+000, - 3.507952e-001, - 4.435014e-002, - 2.819225e+000, - 5.689008e-001, - // albedo 1, turbidity 2 - -1.125833e+000, - -1.870849e-001, - -9.555833e+000, - 1.059713e+001, - -4.225402e-002, - 9.164663e-001, - 4.338796e-002, - 4.400980e+000, - 6.056119e-001, - -1.127440e+000, - -1.551891e-001, - 4.755621e+000, - -4.408806e+000, - -7.851763e-002, - 2.268284e-001, - 1.460070e-001, - 7.048003e+000, - 3.525997e-001, - -1.143788e+000, - -3.170178e-001, - 5.480669e-001, - 2.041830e+000, - -4.532139e-001, - 2.302233e+000, - -1.887419e-001, - -4.489221e+000, - 1.250967e+000, - -1.032849e+000, - 7.376031e-003, - 5.666073e-001, - -2.312203e-001, - 4.862894e-001, - -1.748294e-001, - 3.572870e-001, - 8.380522e+000, - 1.302333e-001, - -1.093728e+000, - -2.786977e-001, - 2.641272e+000, - -1.507494e+000, - -8.731243e+000, - 1.684055e+000, - -2.023377e-001, - -2.176398e+000, - 1.013249e+000, - -1.076578e+000, - -1.456205e-001, - 1.693935e+000, - 2.945003e+000, - -2.822673e+000, - -2.520033e-001, - 1.517034e-001, - 2.649109e+000, - 5.179094e-001, - // albedo 1, turbidity 3 - -1.146417e+000, - -2.119353e-001, - -7.187525e+000, - 8.058599e+000, - -5.256438e-002, - 8.375733e-001, - 3.887093e-002, - 4.222111e+000, - 6.695347e-001, - -1.173674e+000, - -2.067025e-001, - 2.899359e+000, - -2.804918e+000, - -8.473899e-002, - 3.944225e-003, - 1.340641e-001, - 6.160887e+000, - 4.527141e-001, - -1.090098e+000, - -2.599633e-001, - 9.180856e-001, - 1.092710e+000, - -4.215019e-001, - 2.427660e+000, - -9.277667e-002, - -2.123523e+000, - 1.058159e+000, - -1.084460e+000, - 8.056181e-003, - -2.453510e-001, - 6.619567e-001, - 4.668118e-001, - -9.526719e-001, - 4.648454e-001, - 8.001572e+000, - 3.054194e-001, - -1.053728e+000, - -2.765784e-001, - 2.792388e+000, - -3.489517e+000, - -8.150535e+000, - 2.195757e+000, - -2.017234e-001, - -2.128017e+000, - 9.326589e-001, - -1.099348e+000, - -1.593939e-001, - 1.568292e+000, - 7.247853e+000, - -2.933000e+000, - -5.890481e-001, - 1.724440e-001, - 2.433484e+000, - 5.736558e-001, - // albedo 1, turbidity 4 - -1.185983e+000, - -2.581184e-001, - -7.761056e+000, - 8.317053e+000, - -3.351773e-002, - 6.676667e-001, - 5.941733e-002, - 3.820727e+000, - 6.324032e-001, - -1.268591e+000, - -3.398067e-001, - 2.348503e+000, - -2.023779e+000, - -5.368458e-002, - 1.083282e-001, - 8.402858e-002, - 3.910254e+000, - 5.577481e-001, - -1.071353e+000, - -1.992459e-001, - 7.878387e-001, - 1.974702e-001, - -3.033058e-001, - 2.335298e+000, - -8.205259e-002, - 7.954454e-001, - 9.972312e-001, - -1.089513e+000, - -3.104364e-002, - -5.995746e-001, - 2.330281e+000, - 6.581939e-001, - -1.821467e+000, - 6.679973e-001, - 5.090195e+000, - 3.125161e-001, - -1.040214e+000, - -2.570934e-001, - 2.660489e+000, - -6.506045e+000, - -7.053586e+000, - 2.763153e+000, - -2.433632e-001, - -7.648176e-001, - 9.452937e-001, - -1.116052e+000, - -1.831993e-001, - 1.457694e+000, - 1.163608e+001, - -3.216426e+000, - -1.045594e+000, - 2.285002e-001, - 1.817407e+000, - 5.810396e-001, - // albedo 1, turbidity 5 - -1.230134e+000, - -3.136264e-001, - -8.909301e+000, - 9.145006e+000, - -1.055387e-002, - 4.467317e-001, - 1.016826e-001, - 3.342964e+000, - 5.633840e-001, - -1.442907e+000, - -5.593147e-001, - 2.156447e+000, - -1.241657e+000, - -3.512130e-002, - 3.050274e-001, - 1.797175e-002, - 1.742358e+000, - 5.977153e-001, - -1.027627e+000, - -6.481539e-002, - 4.351975e-001, - -1.051677e+000, - -2.030672e-001, - 1.942684e+000, - -3.615993e-002, - 4.050266e+000, - 9.801624e-001, - -1.082110e+000, - -1.578209e-001, - -3.397511e-001, - 4.163851e+000, - 6.650368e-001, - -1.841730e+000, - 7.062544e-001, - 6.789881e-001, - 3.172623e-001, - -1.047447e+000, - -1.977560e-001, - 2.183364e+000, - -8.805249e+000, - -5.483962e+000, - 2.551309e+000, - -1.779640e-001, - 1.519501e+000, - 9.212536e-001, - -1.111853e+000, - -1.935736e-001, - 1.394408e+000, - 1.392405e+001, - -3.465430e+000, - -1.068432e+000, - 2.388671e-001, - 1.455336e+000, - 6.233425e-001, - // albedo 1, turbidity 6 - -1.262238e+000, - -3.546341e-001, - -1.008703e+001, - 1.020084e+001, - -1.852187e-003, - 3.537580e-001, - 1.239199e-001, - 3.056093e+000, - 5.132052e-001, - -1.613810e+000, - -7.355585e-001, - 2.760123e+000, - -1.685253e+000, - -2.517552e-002, - 2.914258e-001, - 4.743448e-003, - 8.689596e-001, - 5.674192e-001, - -9.462336e-001, - 2.950767e-002, - -2.613816e-001, - -7.398653e-001, - -1.315558e-001, - 1.901042e+000, - -6.447844e-002, - 4.969341e+000, - 1.027342e+000, - -1.111481e+000, - -2.194054e-001, - -9.004538e-002, - 3.983442e+000, - 4.871278e-001, - -1.965315e+000, - 7.956121e-001, - -2.363225e-001, - 2.718037e-001, - -1.036397e+000, - -1.827106e-001, - 1.964747e+000, - -8.870759e+000, - -4.208011e+000, - 2.461215e+000, - -2.158905e-001, - 1.561676e+000, - 9.436866e-001, - -1.113769e+000, - -1.947819e-001, - 1.300720e+000, - 1.516476e+001, - -4.088732e+000, - -1.069384e+000, - 2.836434e-001, - 1.671451e+000, - 6.229612e-001, - // albedo 1, turbidity 7 - -1.328069e+000, - -4.244047e-001, - -8.417040e+000, - 8.552244e+000, - -6.813504e-003, - 4.127422e-001, - 9.619897e-002, - 2.854227e+000, - 5.059880e-001, - -1.927552e+000, - -1.025290e+000, - 9.529576e-001, - 4.255950e-001, - -3.738779e-002, - 2.584586e-001, - 4.911004e-002, - -2.640913e-001, - 4.138626e-001, - -8.488094e-001, - 1.435988e-001, - 6.356807e-001, - -2.895732e+000, - -8.473961e-002, - 1.701305e+000, - -1.323908e-001, - 6.499338e+000, - 1.210928e+000, - -1.128313e+000, - -3.397048e-001, - -4.043140e-001, - 6.265097e+000, - 5.482395e-001, - -2.057614e+000, - 8.884087e-001, - -2.943879e+000, - 9.760301e-002, - -1.039764e+000, - -1.494772e-001, - 1.781915e+000, - -1.153012e+001, - -3.379232e+000, - 2.517231e+000, - -2.764393e-001, - 2.588849e+000, - 1.052120e+000, - -1.108447e+000, - -2.012251e-001, - 1.198640e+000, - 1.925331e+001, - -4.423892e+000, - -1.257122e+000, - 3.395690e-001, - 1.481220e+000, - 5.880175e-001, - // albedo 1, turbidity 8 - -1.374185e+000, - -4.967434e-001, - -7.401318e+000, - 7.724021e+000, - -2.345723e-002, - 5.979653e-001, - 2.436346e-002, - 2.658970e+000, - 6.014891e-001, - -2.310933e+000, - -1.290290e+000, - -1.301909e+000, - 2.557806e+000, - -3.744449e-002, - 8.982861e-002, - 1.090613e-001, - -4.398363e-001, - 1.184329e-001, - -1.124730e+000, - -9.921830e-002, - 1.366902e+000, - -4.172489e+000, - -5.078016e-002, - 1.393597e+000, - -9.323843e-002, - 6.452721e+000, - 1.435913e+000, - -8.468477e-001, - -2.744819e-001, - -4.347200e-001, - 6.713362e+000, - 6.127133e-001, - -1.685634e+000, - 7.360941e-001, - -4.535502e+000, - -2.920866e-002, - -1.165242e+000, - -2.008697e-001, - 1.438778e+000, - -1.008936e+001, - -2.214771e+000, - 2.102909e+000, - -1.763085e-001, - 2.859075e+000, - 1.093470e+000, - -1.074614e+000, - -2.066374e-001, - 1.131891e+000, - 1.630063e+001, - -4.801441e+000, - -1.112590e+000, - 3.595785e-001, - 1.122227e+000, - 5.794610e-001, - // albedo 1, turbidity 9 - -1.521515e+000, - -6.835604e-001, - -5.571044e+000, - 6.028774e+000, - -4.253715e-002, - 6.875746e-001, - -5.279456e-006, - 2.180150e+000, - 8.487705e-001, - -2.240415e+000, - -1.171166e+000, - -7.182771e+000, - 8.417068e+000, - -1.932866e-002, - 1.101887e-001, - -1.098862e-002, - 6.242195e-001, - -2.393875e-001, - -2.712354e+000, - -1.198830e+000, - 3.180200e+000, - -6.768130e+000, - -2.563386e-003, - 7.984607e-001, - 2.764376e-001, - 4.695358e+000, - 1.557045e+000, - -3.655172e-002, - -2.142321e-002, - -9.138120e-001, - 7.932786e+000, - 3.516542e-001, - -7.994343e-001, - 1.786761e-001, - -4.208399e+000, - 1.820576e-002, - -1.368610e+000, - -2.656212e-001, - 1.249397e+000, - -8.317818e+000, - -8.962772e-001, - 1.423249e+000, - 1.478381e-001, - 2.191660e+000, - 1.007748e+000, - -1.041753e+000, - -2.453366e-001, - 1.061102e+000, - 1.130172e+001, - -4.739312e+000, - -9.223334e-001, - 2.982776e-001, - 6.162931e-001, - 6.080302e-001, - // albedo 1, turbidity 10 - -1.989159e+000, - -1.095160e+000, - -2.915550e+000, - 3.275339e+000, - -5.735765e-002, - 5.742174e-001, - -7.683288e-006, - 1.763400e+000, - 9.001342e-001, - -2.070020e+000, - -1.086338e+000, - -1.095898e+001, - 1.206960e+001, - 3.780123e-002, - -1.774699e-002, - -5.881348e-004, - 1.333819e+000, - -2.605423e-001, - -5.249653e+000, - -2.383040e+000, - 6.160406e+000, - -9.097138e+000, - -1.955319e-001, - 1.651785e+000, - 6.016463e-004, - 3.021824e+000, - 1.493574e+000, - 4.685432e-001, - -2.358662e-001, - -2.666433e+000, - 9.685763e+000, - 5.804928e-001, - -1.521875e+000, - 5.668989e-001, - -1.548136e+000, - 1.688642e-002, - -1.296891e+000, - -3.449031e-001, - 1.928548e+000, - -1.167560e+001, - -1.627615e+000, - 1.355603e+000, - -1.929074e-001, - -6.568952e-001, - 1.009774e+000, - -1.067288e+000, - -2.410392e-001, - 7.147961e-001, - 1.783840e+001, - -4.374399e+000, - -6.588777e-001, - 3.329831e-001, - 1.012066e+000, - 6.118645e-001, -}; - -double datasetXYZRad2[] = -{ - // albedo 0, turbidity 1 - 1.632341e+000, - 1.395230e+000, - 1.375634e+000, - 1.238193e+001, - 5.921102e+000, - 7.766508e+000, - // albedo 0, turbidity 2 - 1.597115e+000, - 1.554617e+000, - 3.932382e-001, - 1.505284e+001, - 5.725234e+000, - 8.158155e+000, - // albedo 0, turbidity 3 - 1.522034e+000, - 1.844545e+000, - -1.322862e+000, - 1.918382e+001, - 5.440769e+000, - 8.837119e+000, - // albedo 0, turbidity 4 - 1.403048e+000, - 2.290852e+000, - -4.013792e+000, - 2.485100e+001, - 5.521888e+000, - 9.845547e+000, - // albedo 0, turbidity 5 - 1.286364e+000, - 2.774498e+000, - -6.648221e+000, - 2.964151e+001, - 5.923777e+000, - 1.097075e+001, - // albedo 0, turbidity 6 - 1.213544e+000, - 3.040195e+000, - -8.092676e+000, - 3.186082e+001, - 6.789782e+000, - 1.158899e+001, - // albedo 0, turbidity 7 - 1.122622e+000, - 3.347465e+000, - -9.649016e+000, - 3.343824e+001, - 9.347715e+000, - 1.231374e+001, - // albedo 0, turbidity 8 - 1.007356e+000, - 3.543858e+000, - -1.053520e+001, - 3.239842e+001, - 1.483962e+001, - 1.331718e+001, - // albedo 0, turbidity 9 - 8.956642e-001, - 3.278700e+000, - -9.254933e+000, - 2.557923e+001, - 2.489677e+001, - 1.476166e+001, - // albedo 0, turbidity 10 - 7.985143e-001, - 2.340404e+000, - -4.928274e+000, - 1.141787e+001, - 3.961501e+001, - 1.682448e+001, - // albedo 1, turbidity 1 - 1.745162e+000, - 1.639467e+000, - 1.342721e+000, - 1.166033e+001, - 1.490124e+001, - 1.774031e+001, - // albedo 1, turbidity 2 - 1.708439e+000, - 1.819144e+000, - 2.834399e-001, - 1.448066e+001, - 1.459214e+001, - 1.858679e+001, - // albedo 1, turbidity 3 - 1.631720e+000, - 2.094799e+000, - -1.378825e+000, - 1.843198e+001, - 1.463173e+001, - 1.962881e+001, - // albedo 1, turbidity 4 - 1.516536e+000, - 2.438729e+000, - -3.624121e+000, - 2.298621e+001, - 1.599782e+001, - 2.070027e+001, - // albedo 1, turbidity 5 - 1.405863e+000, - 2.785191e+000, - -5.705236e+000, - 2.645121e+001, - 1.768330e+001, - 2.191903e+001, - // albedo 1, turbidity 6 - 1.344052e+000, - 2.951807e+000, - -6.683851e+000, - 2.744271e+001, - 1.985706e+001, - 2.229452e+001, - // albedo 1, turbidity 7 - 1.245827e+000, - 3.182923e+000, - -7.822960e+000, - 2.791395e+001, - 2.327254e+001, - 2.315910e+001, - // albedo 1, turbidity 8 - 1.132305e+000, - 3.202593e+000, - -8.008429e+000, - 2.521093e+001, - 3.000014e+001, - 2.405306e+001, - // albedo 1, turbidity 9 - 1.020330e+000, - 2.820556e+000, - -6.238704e+000, - 1.709276e+001, - 4.077916e+001, - 2.509949e+001, - // albedo 1, turbidity 10 - 9.031570e-001, - 1.863917e+000, - -1.955738e+000, - 3.032665e+000, - 5.434290e+001, - 2.641780e+001, -}; - -double datasetXYZ3[] = -{ - // albedo 0, turbidity 1 - -1.310023e+000, - -4.407658e-001, - -3.640340e+001, - 3.683292e+001, - -8.124762e-003, - 5.297961e-001, - 1.188633e-002, - 3.138320e+000, - 5.134778e-001, - -1.424100e+000, - -5.501606e-001, - -1.753510e+001, - 1.822769e+001, - -1.539272e-002, - 6.366826e-001, - 2.661996e-003, - 2.659915e+000, - 4.071138e-001, - -1.103436e+000, - -1.884105e-001, - 6.425322e+000, - -6.910579e+000, - -2.019861e-002, - 3.553271e-001, - -1.589061e-002, - 5.345985e+000, - 8.790218e-001, - -1.186200e+000, - -4.307514e-001, - -3.957947e+000, - 5.979352e+000, - -5.348869e-002, - 1.736117e+000, - 3.491346e-002, - -2.692261e+000, - 5.610506e-001, - -1.006038e+000, - -1.305995e-001, - 4.473513e+000, - -3.806719e+000, - 1.419407e-001, - -2.148238e-002, - -5.081185e-002, - 3.735362e+000, - 5.358280e-001, - -1.078507e+000, - -1.633754e-001, - -3.812368e+000, - 4.381700e+000, - 2.988122e-002, - 1.754224e+000, - 1.472376e-001, - 3.722798e+000, - 4.999157e-001, - // albedo 0, turbidity 2 - -1.333582e+000, - -4.649908e-001, - -3.359528e+001, - 3.404375e+001, - -9.384242e-003, - 5.587511e-001, - 5.726310e-003, - 3.073145e+000, - 5.425529e-001, - -1.562624e+000, - -7.107068e-001, - -1.478170e+001, - 1.559839e+001, - -1.462375e-002, - 5.050133e-001, - 2.516017e-002, - 1.604696e+000, - 2.902403e-001, - -8.930158e-001, - 4.068077e-002, - 1.373481e+000, - -2.342752e+000, - -2.098058e-002, - 6.248686e-001, - -5.258363e-002, - 7.058214e+000, - 1.150373e+000, - -1.262823e+000, - -4.818353e-001, - 8.892610e-004, - 1.923120e+000, - -4.979718e-002, - 1.040693e+000, - 1.558103e-001, - -2.852480e+000, - 2.420691e-001, - -9.968383e-001, - -1.200648e-001, - 1.324342e+000, - -9.430889e-001, - 1.931098e-001, - 4.436916e-001, - -7.320456e-002, - 4.215931e+000, - 7.898019e-001, - -1.078185e+000, - -1.718192e-001, - -1.720191e+000, - 2.358918e+000, - 2.765637e-002, - 1.260245e+000, - 2.021941e-001, - 3.395483e+000, - 5.173628e-001, - // albedo 0, turbidity 3 - -1.353023e+000, - -4.813523e-001, - -3.104920e+001, - 3.140156e+001, - -9.510741e-003, - 5.542030e-001, - 8.135471e-003, - 3.136646e+000, - 5.215989e-001, - -1.624704e+000, - -7.990201e-001, - -2.167125e+001, - 2.246341e+001, - -1.163533e-002, - 5.415746e-001, - 2.618378e-002, - 1.139214e+000, - 3.444357e-001, - -7.983610e-001, - 1.417476e-001, - 9.914841e+000, - -1.081503e+001, - -1.218845e-002, - 3.411392e-001, - -6.137698e-002, - 7.445848e+000, - 1.180080e+000, - -1.266679e+000, - -4.288977e-001, - -5.818701e+000, - 6.986437e+000, - -8.180711e-002, - 1.397403e+000, - 2.016916e-001, - -1.275731e+000, - 2.592773e-001, - -1.009707e+000, - -1.537754e-001, - 3.496378e+000, - -3.013726e+000, - 2.421150e-001, - -2.831925e-001, - 3.003395e-002, - 3.702862e+000, - 7.746320e-001, - -1.075646e+000, - -1.768747e-001, - -1.347762e+000, - 1.989004e+000, - 1.375836e-002, - 1.764810e+000, - 1.330018e-001, - 3.230864e+000, - 6.626210e-001, - // albedo 0, turbidity 4 - -1.375269e+000, - -5.103569e-001, - -3.442661e+001, - 3.478703e+001, - -8.460009e-003, - 5.408643e-001, - 4.813323e-003, - 3.016078e+000, - 5.062069e-001, - -1.821679e+000, - -9.766461e-001, - -1.926488e+001, - 1.997912e+001, - -9.822567e-003, - 3.649556e-001, - 4.316092e-002, - 8.930190e-001, - 4.166527e-001, - -6.633542e-001, - 1.997841e-001, - 2.395592e+000, - -3.117175e+000, - -1.080884e-002, - 8.983814e-001, - -1.375825e-001, - 6.673463e+000, - 1.115663e+000, - -1.303240e+000, - -3.612712e-001, - 8.292959e-002, - 3.381364e-001, - -6.078648e-002, - 3.229247e-001, - 3.680987e-001, - 7.046755e-001, - 3.144924e-001, - -9.952598e-001, - -2.039076e-001, - 4.026851e-001, - 2.686684e-001, - 1.640712e-001, - 5.186341e-001, - -1.205520e-002, - 2.659613e+000, - 8.030394e-001, - -1.098579e+000, - -2.151992e-001, - 6.558198e-001, - -7.436900e-004, - -1.421817e-003, - 1.073701e+000, - 1.886875e-001, - 2.536857e+000, - 6.673923e-001, - // albedo 0, turbidity 5 - -1.457986e+000, - -5.906842e-001, - -3.812464e+001, - 3.838539e+001, - -6.024357e-003, - 4.741484e-001, - 1.209223e-002, - 2.818432e+000, - 5.012433e-001, - -1.835728e+000, - -1.003405e+000, - -6.848129e+000, - 7.601943e+000, - -1.277375e-002, - 4.785598e-001, - 3.366853e-002, - 1.097701e+000, - 4.636635e-001, - -8.491348e-001, - 9.466365e-003, - -2.685226e+000, - 2.004060e+000, - -1.168708e-002, - 6.752316e-001, - -1.543371e-001, - 5.674759e+000, - 1.039534e+000, - -1.083379e+000, - -1.506790e-001, - 7.328236e-001, - -5.095568e-001, - -8.609153e-002, - 4.448820e-001, - 4.174662e-001, - 1.481556e+000, - 3.942551e-001, - -1.117089e+000, - -3.337605e-001, - 2.502281e-001, - 4.036323e-001, - 2.673899e-001, - 2.829817e-001, - 2.242450e-002, - 2.043207e+000, - 7.706902e-001, - -1.071648e+000, - -2.126200e-001, - 6.069466e-001, - -1.456290e-003, - -5.515960e-001, - 1.046755e+000, - 1.985021e-001, - 2.290245e+000, - 6.876058e-001, - // albedo 0, turbidity 6 - -1.483903e+000, - -6.309647e-001, - -4.380213e+001, - 4.410537e+001, - -5.712161e-003, - 5.195992e-001, - 2.028428e-003, - 2.687114e+000, - 5.098321e-001, - -2.053976e+000, - -1.141473e+000, - 5.109183e-001, - 8.060391e-002, - -1.033983e-002, - 4.066532e-001, - 4.869627e-002, - 1.161722e+000, - 4.039525e-001, - -6.348185e-001, - 7.651292e-002, - -1.031327e+001, - 1.007598e+001, - -2.083688e-002, - 7.359516e-001, - -2.029459e-001, - 5.013257e+000, - 1.077649e+000, - -1.228630e+000, - -1.650496e-001, - 4.077157e-002, - -7.189167e-001, - -5.092220e-002, - 2.959814e-001, - 5.111496e-001, - 2.540433e+000, - 3.615330e-001, - -1.041883e+000, - -3.278413e-001, - -6.691911e-002, - 1.307364e+000, - 2.166663e-001, - 3.000595e-001, - -3.157136e-003, - 1.389208e+000, - 7.999026e-001, - -1.103556e+000, - -2.443602e-001, - 4.705347e-001, - -9.296482e-004, - -5.309920e-001, - 9.654511e-001, - 2.142587e-001, - 2.244723e+000, - 6.839976e-001, - // albedo 0, turbidity 7 - -1.555684e+000, - -6.962113e-001, - -4.647983e+001, - 4.674270e+001, - -5.034895e-003, - 4.755090e-001, - -9.502561e-007, - 2.626569e+000, - 5.056194e-001, - -1.998288e+000, - -1.124720e+000, - -1.629586e+000, - 2.187993e+000, - -8.284384e-003, - 3.845258e-001, - 5.726240e-002, - 1.185644e+000, - 4.255812e-001, - -1.032570e+000, - -2.513850e-001, - -3.721112e+000, - 3.506967e+000, - -2.186561e-002, - 9.436049e-001, - -2.451412e-001, - 4.725724e+000, - 1.039256e+000, - -8.597532e-001, - 9.073332e-002, - -2.553741e+000, - 1.993237e+000, - -4.390891e-002, - -2.046928e-001, - 5.515623e-001, - 1.909127e+000, - 3.948212e-001, - -1.210482e+000, - -4.477622e-001, - -2.267805e-001, - 1.219488e+000, - 1.336186e-001, - 6.866897e-001, - 2.808997e-002, - 1.600403e+000, - 7.816409e-001, - -1.078168e+000, - -2.699261e-001, - 2.537282e-001, - 3.820684e-001, - -4.425103e-001, - 5.298235e-001, - 2.185217e-001, - 1.728679e+000, - 6.882743e-001, - // albedo 0, turbidity 8 - -1.697968e+000, - -8.391488e-001, - -5.790105e+001, - 5.814120e+001, - -3.404760e-003, - 4.265140e-001, - -1.796301e-006, - 2.368442e+000, - 5.324429e-001, - -2.141552e+000, - -1.172230e+000, - 1.677872e+001, - -1.641470e+001, - -5.732425e-003, - 2.002199e-001, - 6.841834e-002, - 1.485338e+000, - 3.215763e-001, - -1.442946e+000, - -7.264245e-001, - -9.503706e+000, - 9.650462e+000, - -2.120995e-002, - 1.419263e+000, - -2.893098e-001, - 3.860731e+000, - 1.120857e+000, - -5.696752e-001, - 3.411279e-001, - -2.931035e-001, - -6.512552e-001, - -1.068437e-001, - -1.085661e+000, - 6.107549e-001, - 1.459503e+000, - 3.210336e-001, - -1.313839e+000, - -5.921371e-001, - -2.332222e-001, - 1.648196e+000, - 2.492787e-001, - 1.381033e+000, - -1.993392e-002, - 9.812560e-001, - 8.316329e-001, - -1.087464e+000, - -3.195534e-001, - 2.902095e-001, - 3.383709e-001, - -8.798482e-001, - 1.494668e-002, - 2.529703e-001, - 1.452644e+000, - 6.693870e-001, - // albedo 0, turbidity 9 - -2.068582e+000, - -1.118605e+000, - -5.081598e+001, - 5.097486e+001, - -3.280669e-003, - 4.067371e-001, - -2.544951e-006, - 2.179497e+000, - 5.778017e-001, - -1.744693e+000, - -8.537207e-001, - 2.234361e+001, - -2.208318e+001, - -5.932616e-003, - 1.035049e-001, - 5.742772e-002, - 1.977880e+000, - 2.124846e-001, - -3.287515e+000, - -2.140268e+000, - -1.249566e+001, - 1.240091e+001, - -2.409349e-002, - 1.397821e+000, - -2.371627e-001, - 2.771192e+000, - 1.170496e+000, - 5.502311e-001, - 1.046630e+000, - 2.193517e+000, - -2.220400e+000, - -1.064394e-001, - -1.017926e+000, - 4.795457e-001, - 1.030644e+000, - 3.177516e-001, - -1.719734e+000, - -9.536198e-001, - -6.586821e-001, - 1.386361e+000, - -2.513065e-002, - 1.187011e+000, - 6.542539e-002, - 5.296055e-001, - 8.082660e-001, - -1.005700e+000, - -3.028096e-001, - 4.470957e-002, - 1.007760e+000, - -8.119016e-001, - 3.153338e-002, - 2.311321e-001, - 1.182208e+000, - 6.824758e-001, - // albedo 0, turbidity 10 - -2.728867e+000, - -1.580388e+000, - -3.079627e+001, - 3.092586e+001, - -4.197673e-003, - 3.154759e-001, - -3.897675e-006, - 1.920567e+000, - 6.664791e-001, - -1.322495e+000, - -7.249275e-001, - 1.477660e+001, - -1.468154e+001, - -9.044857e-003, - 5.624314e-002, - 6.498392e-002, - 2.047389e+000, - 6.367540e-002, - -6.102376e+000, - -3.473018e+000, - -9.926071e+000, - 9.637797e+000, - -1.097909e-002, - 1.103498e+000, - -2.424521e-001, - 2.520748e+000, - 1.240260e+000, - 1.351796e+000, - 1.018588e+000, - 2.009081e+000, - -1.333394e+000, - -1.979125e-001, - -3.318292e-001, - 4.476624e-001, - 9.095235e-001, - 2.955611e-001, - -1.774467e+000, - -1.079880e+000, - -8.084680e-002, - 2.577697e-001, - -1.149295e-001, - 4.975303e-001, - 2.931611e-003, - -3.803171e-001, - 8.002794e-001, - -9.898401e-001, - -2.542513e-001, - -7.530911e-002, - 1.870355e+000, - -1.521918e+000, - 2.405164e-001, - 2.964615e-001, - 1.334800e+000, - 6.789053e-001, - // albedo 1, turbidity 1 - -1.279730e+000, - -4.290674e-001, - -4.277972e+001, - 4.343305e+001, - -6.541826e-003, - 4.945086e-001, - 1.425338e-002, - 2.685244e+000, - 5.011313e-001, - -1.449506e+000, - -5.766374e-001, - -1.688496e+001, - 1.781118e+001, - -1.121649e-002, - 3.545020e-001, - 2.287338e-002, - 1.904281e+000, - 4.936998e-001, - -1.021980e+000, - -1.897574e-001, - 2.482462e+000, - -2.941725e+000, - -1.570448e-002, - 7.532578e-001, - -4.256800e-002, - 5.239660e+000, - 4.983116e-001, - -1.162608e+000, - -3.428049e-001, - 3.974358e+000, - -1.527935e+000, - -3.919201e-002, - 8.758593e-001, - 7.291363e-002, - -3.455257e+000, - 8.007426e-001, - -9.929985e-001, - -8.712006e-002, - -7.397313e-001, - 1.348372e+000, - 9.511685e-002, - 3.233584e-001, - -7.549148e-002, - 5.806452e+000, - 4.990042e-001, - -1.084996e+000, - -1.739767e-001, - 1.580475e-001, - 9.088180e-001, - 6.871433e-002, - 5.933079e-001, - 1.188921e-001, - 3.074079e+000, - 4.999327e-001, - // albedo 1, turbidity 2 - -1.317009e+000, - -4.661946e-001, - -4.255347e+001, - 4.312782e+001, - -5.727235e-003, - 4.285447e-001, - 2.189854e-002, - 2.608310e+000, - 5.190700e-001, - -1.469236e+000, - -6.282139e-001, - -1.241404e+001, - 1.348765e+001, - -1.204770e-002, - 5.070285e-001, - -7.280216e-004, - 1.491533e+000, - 3.635064e-001, - -9.713808e-001, - -8.138038e-002, - 3.709854e-001, - -1.041174e+000, - -1.814075e-002, - 5.060860e-001, - -2.053756e-002, - 6.161431e+000, - 1.093736e+000, - -1.159057e+000, - -3.698074e-001, - 2.711209e+000, - -6.006479e-001, - -4.896926e-002, - 9.273957e-001, - 1.137712e-001, - -3.496828e+000, - 2.867109e-001, - -1.011601e+000, - -8.201890e-002, - 2.105725e-001, - 4.597520e-001, - 1.478925e-001, - 2.138940e-001, - -5.660670e-002, - 6.057755e+000, - 7.859121e-001, - -1.078020e+000, - -1.811580e-001, - 1.646622e-001, - 8.348426e-001, - 1.149064e-001, - 4.985738e-001, - 1.376605e-001, - 2.746607e+000, - 4.999626e-001, - // albedo 1, turbidity 3 - -1.325672e+000, - -4.769313e-001, - -4.111215e+001, - 4.168293e+001, - -6.274997e-003, - 4.649469e-001, - 1.119411e-002, - 2.631267e+000, - 5.234546e-001, - -1.619391e+000, - -8.000253e-001, - -1.534098e+001, - 1.632706e+001, - -1.012023e-002, - 4.242255e-001, - 2.931597e-002, - 8.925807e-001, - 3.314765e-001, - -7.356979e-001, - 1.368406e-001, - 2.972579e+000, - -3.535359e+000, - -1.318948e-002, - 4.607620e-001, - -7.182778e-002, - 6.254100e+000, - 1.236299e+000, - -1.316217e+000, - -4.194427e-001, - 3.489902e-002, - 1.289849e+000, - -4.755960e-002, - 1.138222e+000, - 1.975992e-001, - -8.991542e-001, - 2.290572e-001, - -9.502188e-001, - -1.172703e-001, - 1.405202e+000, - -3.061919e-001, - 1.058772e-001, - -3.760592e-001, - -1.983179e-002, - 3.562353e+000, - 7.895959e-001, - -1.100117e+000, - -1.900567e-001, - 4.925030e-001, - 5.250225e-001, - 1.576804e-001, - 1.042701e+000, - 7.330743e-002, - 2.796064e+000, - 6.749783e-001, - // albedo 1, turbidity 4 - -1.354183e+000, - -5.130625e-001, - -4.219268e+001, - 4.271772e+001, - -5.365373e-003, - 4.136743e-001, - 1.235172e-002, - 2.520122e+000, - 5.187269e-001, - -1.741434e+000, - -9.589761e-001, - -8.230339e+000, - 9.296799e+000, - -9.600162e-003, - 4.994969e-001, - 2.955452e-002, - 3.667099e-001, - 3.526999e-001, - -6.917347e-001, - 2.154887e-001, - -8.760264e-001, - 2.334121e-001, - -1.909621e-002, - 4.748033e-001, - -1.138514e-001, - 6.515360e+000, - 1.225097e+000, - -1.293189e+000, - -4.218700e-001, - 1.620952e+000, - -7.858597e-001, - -3.769410e-002, - 6.636786e-001, - 3.364945e-001, - -5.341017e-001, - 2.128347e-001, - -9.735521e-001, - -1.325495e-001, - 1.007517e+000, - 2.598258e-001, - 6.762169e-002, - 1.421018e-003, - -6.915987e-002, - 3.185897e+000, - 8.641956e-001, - -1.094800e+000, - -1.962062e-001, - 5.755591e-001, - 2.906259e-001, - 2.625748e-001, - 7.644049e-001, - 1.347492e-001, - 2.677126e+000, - 6.465460e-001, - // albedo 1, turbidity 5 - -1.393063e+000, - -5.578338e-001, - -4.185249e+001, - 4.233504e+001, - -5.435640e-003, - 4.743765e-001, - 7.422477e-003, - 2.442801e+000, - 5.211707e-001, - -1.939487e+000, - -1.128509e+000, - -8.974257e+000, - 9.978383e+000, - -7.965597e-003, - 2.948830e-001, - 4.436763e-002, - 2.839868e-001, - 3.440424e-001, - -6.011562e-001, - 2.354877e-001, - -3.079820e+000, - 2.585094e+000, - -2.002701e-002, - 7.793909e-001, - -1.598414e-001, - 5.834678e+000, - 1.202856e+000, - -1.315676e+000, - -3.903446e-001, - 1.701900e+000, - -1.304609e+000, - -1.045121e-002, - 2.747707e-001, - 4.143967e-001, - 3.197102e-001, - 2.637580e-001, - -9.618628e-001, - -1.625841e-001, - 1.187138e+000, - 1.497802e-001, - -5.590954e-006, - 3.178475e-002, - -4.153145e-002, - 2.496096e+000, - 8.195082e-001, - -1.111554e+000, - -2.365546e-001, - 7.831875e-001, - 2.018684e-001, - 2.074369e-001, - 7.395978e-001, - 1.225730e-001, - 1.876478e+000, - 6.821167e-001, - // albedo 1, turbidity 6 - -1.427879e+000, - -5.994879e-001, - -3.531016e+001, - 3.581581e+001, - -6.431497e-003, - 4.554192e-001, - 7.348731e-004, - 2.334619e+000, - 5.233377e-001, - -1.998177e+000, - -1.206633e+000, - -2.146510e+001, - 2.242237e+001, - -5.857596e-003, - 2.755663e-001, - 6.384795e-002, - 1.358244e-001, - 3.328437e-001, - -6.440630e-001, - 2.058571e-001, - 2.155499e+000, - -2.587968e+000, - -1.840023e-002, - 8.826555e-001, - -2.222452e-001, - 5.847073e+000, - 1.228387e+000, - -1.229071e+000, - -3.360441e-001, - -3.429599e-001, - 6.179469e-001, - 2.029610e-003, - 8.899319e-002, - 5.041624e-001, - 1.882964e-001, - 2.252040e-001, - -1.022905e+000, - -2.101621e-001, - 1.915689e+000, - -6.498794e-001, - -3.463651e-002, - 8.954605e-002, - -6.797854e-002, - 2.417705e+000, - 8.568618e-001, - -1.082538e+000, - -2.007723e-001, - 4.731009e-001, - 4.077267e-001, - 1.324289e-001, - 6.514880e-001, - 1.702912e-001, - 2.309383e+000, - 6.600895e-001, - // albedo 1, turbidity 7 - -1.472139e+000, - -6.499815e-001, - -3.428465e+001, - 3.469659e+001, - -5.747023e-003, - 4.174167e-001, - 1.688597e-003, - 2.323046e+000, - 5.395191e-001, - -2.161176e+000, - -1.353089e+000, - -2.226827e+001, - 2.329138e+001, - -5.583808e-003, - 2.364793e-001, - 6.096656e-002, - 1.944666e-003, - 2.861624e-001, - -6.593044e-001, - 1.393558e-001, - 4.698373e+000, - -5.193883e+000, - -1.998390e-002, - 1.095635e+000, - -2.391254e-001, - 5.598103e+000, - 1.236193e+000, - -1.195717e+000, - -2.972715e-001, - 4.648953e-002, - 3.024588e-001, - 5.003313e-003, - -3.754741e-001, - 5.247265e-001, - -1.381312e-001, - 2.493896e-001, - -1.020139e+000, - -2.253524e-001, - 3.548437e-001, - 7.030485e-001, - -2.107076e-002, - 4.581395e-001, - -3.243757e-002, - 2.453259e+000, - 8.323623e-001, - -1.098770e+000, - -2.435780e-001, - 8.761614e-001, - 1.941613e-001, - -1.990692e-001, - 3.761139e-001, - 1.657412e-001, - 1.590503e+000, - 6.741417e-001, - // albedo 1, turbidity 8 - -1.648007e+000, - -8.205121e-001, - -4.435106e+001, - 4.479801e+001, - -4.181353e-003, - 3.854830e-001, - -1.842385e-006, - 2.000281e+000, - 5.518363e-001, - -2.140986e+000, - -1.282239e+000, - -3.979213e+000, - 4.672459e+000, - -5.008582e-003, - 2.421920e-001, - 6.253602e-002, - 6.612713e-001, - 2.555851e-001, - -1.300502e+000, - -5.137898e-001, - 5.179821e-001, - -4.032341e-001, - -2.066785e-002, - 1.087929e+000, - -2.615309e-001, - 4.225887e+000, - 1.229237e+000, - -6.963340e-001, - 9.241060e-002, - 6.936356e-002, - -3.588571e-001, - -5.461843e-002, - -5.616643e-001, - 5.484166e-001, - -4.776267e-002, - 2.414935e-001, - -1.233179e+000, - -4.325498e-001, - 6.479813e-001, - 8.368356e-001, - 2.458875e-001, - 6.464752e-001, - -2.897097e-002, - 1.561773e+000, - 8.518598e-001, - -1.051023e+000, - -2.533690e-001, - 1.004294e+000, - 3.028083e-001, - -1.520108e+000, - 1.607013e-001, - 1.619975e-001, - 1.131094e+000, - 6.706655e-001, - // albedo 1, turbidity 9 - -1.948249e+000, - -1.097383e+000, - -4.453697e+001, - 4.494902e+001, - -3.579939e-003, - 3.491605e-001, - -2.500253e-006, - 1.740442e+000, - 6.188022e-001, - -2.154253e+000, - -1.209559e+000, - 4.144894e+000, - -3.562411e+000, - -5.638843e-003, - 1.067169e-001, - 7.594858e-002, - 1.005280e+000, - 1.072543e-001, - -2.513259e+000, - -1.507208e+000, - -1.602979e+000, - 1.404154e+000, - -5.560750e-003, - 1.240490e+000, - -2.852117e-001, - 3.485252e+000, - 1.349321e+000, - -7.832214e-002, - 3.655626e-001, - 3.856288e-001, - 6.867894e-001, - -1.609523e-001, - -6.704306e-001, - 5.357301e-001, - -6.457935e-001, - 1.479503e-001, - -1.354784e+000, - -5.454375e-001, - 8.797469e-001, - -1.466514e+000, - 7.134420e-001, - 5.934903e-001, - -2.911178e-002, - 8.643737e-001, - 9.030724e-001, - -1.048324e+000, - -2.738736e-001, - 8.783074e-001, - 3.246188e+000, - -4.435369e+000, - 1.251791e-001, - 1.783486e-001, - 1.064657e+000, - 6.522878e-001, - // albedo 1, turbidity 10 - -2.770408e+000, - -1.618911e+000, - -2.504031e+001, - 2.531674e+001, - -4.239279e-003, - 3.241013e-001, - -3.764484e-006, - 1.586843e+000, - 7.035906e-001, - -1.913500e+000, - -1.144014e+000, - -1.080587e+001, - 1.153677e+001, - -1.003197e-002, - 1.577515e-001, - 5.217789e-002, - 1.225278e+000, - 5.172771e-003, - -5.293208e+000, - -2.876463e+000, - 2.087053e+000, - -3.201552e+000, - 3.892964e-003, - 5.323930e-001, - -2.034512e-001, - 2.617760e+000, - 1.273597e+000, - 9.060340e-001, - 3.773409e-001, - -6.399945e-001, - 3.213979e+000, - -9.112172e-002, - 6.494055e-001, - 3.953280e-001, - 5.047796e-001, - 2.998695e-001, - -1.482179e+000, - -6.778310e-001, - 1.161775e+000, - -3.004872e+000, - 4.774797e-001, - -4.969248e-001, - -3.512074e-003, - -1.307190e+000, - 7.927378e-001, - -9.863181e-001, - -1.803364e-001, - 5.810824e-001, - 4.580570e+000, - -3.863454e+000, - 5.328174e-001, - 2.272821e-001, - 1.771114e+000, - 6.791814e-001, -}; - -double datasetXYZRad3[] = -{ - // albedo 0, turbidity 1 - 1.168084e+000, - 2.156455e+000, - -3.980314e+000, - 1.989302e+001, - 1.328335e+001, - 1.435621e+001, - // albedo 0, turbidity 2 - 1.135488e+000, - 2.294701e+000, - -4.585886e+000, - 2.090208e+001, - 1.347840e+001, - 1.467658e+001, - // albedo 0, turbidity 3 - 1.107408e+000, - 2.382765e+000, - -5.112357e+000, - 2.147823e+001, - 1.493128e+001, - 1.460882e+001, - // albedo 0, turbidity 4 - 1.054193e+000, - 2.592891e+000, - -6.115000e+000, - 2.268967e+001, - 1.635672e+001, - 1.518999e+001, - // albedo 0, turbidity 5 - 1.006946e+000, - 2.705420e+000, - -6.698930e+000, - 2.291830e+001, - 1.834324e+001, - 1.570651e+001, - // albedo 0, turbidity 6 - 9.794044e-001, - 2.742440e+000, - -6.805283e+000, - 2.225271e+001, - 2.050797e+001, - 1.563130e+001, - // albedo 0, turbidity 7 - 9.413577e-001, - 2.722009e+000, - -6.760707e+000, - 2.098242e+001, - 2.342588e+001, - 1.605011e+001, - // albedo 0, turbidity 8 - 8.917923e-001, - 2.592780e+000, - -6.152635e+000, - 1.774141e+001, - 2.858324e+001, - 1.657910e+001, - // albedo 0, turbidity 9 - 8.288391e-001, - 2.153434e+000, - -4.118327e+000, - 1.078118e+001, - 3.681710e+001, - 1.738139e+001, - // albedo 0, turbidity 10 - 7.623528e-001, - 1.418187e+000, - -8.845235e-001, - 7.590129e-001, - 4.629859e+001, - 1.921657e+001, - // albedo 1, turbidity 1 - 1.352858e+000, - 2.048862e+000, - -2.053393e+000, - 1.405874e+001, - 3.045344e+001, - 3.044430e+001, - // albedo 1, turbidity 2 - 1.330497e+000, - 2.126497e+000, - -2.466296e+000, - 1.467559e+001, - 3.090738e+001, - 3.069707e+001, - // albedo 1, turbidity 3 - 1.286344e+000, - 2.200436e+000, - -2.877228e+000, - 1.492701e+001, - 3.236288e+001, - 3.077223e+001, - // albedo 1, turbidity 4 - 1.234428e+000, - 2.289628e+000, - -3.404699e+000, - 1.499436e+001, - 3.468390e+001, - 3.084842e+001, - // albedo 1, turbidity 5 - 1.178660e+000, - 2.306071e+000, - -3.549159e+000, - 1.411006e+001, - 3.754188e+001, - 3.079730e+001, - // albedo 1, turbidity 6 - 1.151366e+000, - 2.333005e+000, - -3.728627e+000, - 1.363374e+001, - 3.905894e+001, - 3.092599e+001, - // albedo 1, turbidity 7 - 1.101593e+000, - 2.299422e+000, - -3.565787e+000, - 1.196745e+001, - 4.188472e+001, - 3.102755e+001, - // albedo 1, turbidity 8 - 1.038322e+000, - 2.083539e+000, - -2.649585e+000, - 8.037389e+000, - 4.700869e+001, - 3.065948e+001, - // albedo 1, turbidity 9 - 9.596146e-001, - 1.671470e+000, - -8.751538e-001, - 1.679772e+000, - 5.345784e+001, - 3.054520e+001, - // albedo 1, turbidity 10 - 8.640731e-001, - 9.858301e-001, - 1.854956e+000, - -6.798097e+000, - 5.936468e+001, - 3.110255e+001, -}; - - - -double* datasetsXYZ[] = -{ - datasetXYZ1, - datasetXYZ2, - datasetXYZ3 -}; - -double* datasetsXYZRad[] = -{ - datasetXYZRad1, - datasetXYZRad2, - datasetXYZRad3 -}; +/* +This source is published under the following 3-clause BSD license. + +Copyright (c) 2012 - 2013, Lukas Hosek and Alexander Wilkie +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * None of the names of the contributors may be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +/* ============================================================================ + +This file is part of a sample implementation of the analytical skylight and +solar radiance models presented in the SIGGRAPH 2012 paper + + + "An Analytic Model for Full Spectral Sky-Dome Radiance" + +and the 2013 IEEE CG&A paper + + "Adding a Solar Radiance Function to the Hosek Skylight Model" + + both by + + Lukas Hosek and Alexander Wilkie + Charles University in Prague, Czech Republic + + + Version: 1.4a, February 22nd, 2013 + +Version history: + +1.4a February 22nd, 2013 + Removed unnecessary and counter-intuitive solar radius parameters + from the interface of the colourspace sky dome initialisation functions. + +1.4 February 11th, 2013 + Fixed a bug which caused the relative brightness of the solar disc + and the sky dome to be off by a factor of about 6. The sun was too + bright: this affected both normal and alien sun scenarios. The + coefficients of the solar radiance function were changed to fix this. + +1.3 January 21st, 2013 (not released to the public) + Added support for solar discs that are not exactly the same size as + the terrestrial sun. Also added support for suns with a different + emission spectrum ("Alien World" functionality). + +1.2a December 18th, 2012 + Fixed a mistake and some inaccuracies in the solar radiance function + explanations found in ArHosekSkyModel.h. The actual source code is + unchanged compared to version 1.2. + +1.2 December 17th, 2012 + Native RGB data and a solar radiance function that matches the turbidity + conditions were added. + +1.1 September 2012 + The coefficients of the spectral model are now scaled so that the output + is given in physical units: W / (m^-2 * sr * nm). Also, the output of the + XYZ model is now no longer scaled to the range [0...1]. Instead, it is + the result of a simple conversion from spectral data via the CIE 2 degree + standard observer matching functions. Therefore, after multiplication + with 683 lm / W, the Y channel now corresponds to luminance in lm. + +1.0 May 11th, 2012 + Initial release. + + +Please visit http://cgg.mff.cuni.cz/projects/SkylightModelling/ to check if +an updated version of this code has been published! + +============================================================================ */ + + +/* + +This file contains the coefficient data for the XYZ colour space version of +the model. + +*/ + +// Uses Sep 9 pattern / Aug 23 mean dataset + +double datasetXYZ1[] = +{ + // albedo 0, turbidity 1 + -1.117001e+000, + -1.867262e-001, + -1.113505e+001, + 1.259865e+001, + -3.937339e-002, + 1.167571e+000, + 7.100686e-003, + 3.592678e+000, + 6.083296e-001, + -1.152006e+000, + -1.926669e-001, + 6.152049e+000, + -4.770802e+000, + -8.704701e-002, + 7.483626e-001, + 3.372718e-002, + 4.464592e+000, + 4.036546e-001, + -1.072371e+000, + -2.696632e-001, + 2.816168e-001, + 1.820571e+000, + -3.742666e-001, + 2.080607e+000, + -7.675295e-002, + -2.835366e+000, + 1.129329e+000, + -1.109935e+000, + -1.532764e-001, + 1.198787e+000, + -9.015183e-001, + 5.173015e-003, + 5.749178e-001, + 1.075633e-001, + 4.387949e+000, + 2.650413e-001, + -1.052297e+000, + -2.229452e-001, + 1.952347e+000, + 5.727205e-001, + -4.885070e+000, + 1.984016e+000, + -1.106197e-001, + -4.898361e-001, + 8.907873e-001, + -1.070108e+000, + -1.600465e-001, + 1.593886e+000, + -4.479251e-005, + -3.306541e+000, + 9.390193e-001, + 9.513168e-002, + 2.343583e+000, + 5.335404e-001, + // albedo 0, turbidity 2 + -1.113253e+000, + -1.699600e-001, + -1.038822e+001, + 1.137513e+001, + -4.040911e-002, + 1.037455e+000, + 4.991792e-002, + 4.801919e+000, + 6.302710e-001, + -1.135747e+000, + -1.678594e-001, + 4.970755e+000, + -4.430230e+000, + -6.657408e-002, + 3.636161e-001, + 1.558009e-001, + 6.013370e+000, + 3.959601e-001, + -1.095892e+000, + -2.732595e-001, + 7.666496e-001, + 1.350731e+000, + -4.401401e-001, + 2.470135e+000, + -1.707929e-001, + -3.260793e+000, + 1.170337e+000, + -1.073668e+000, + -2.603929e-002, + -1.944589e-001, + 4.575207e-001, + 6.878164e-001, + -1.390770e-001, + 3.690299e-001, + 7.885781e+000, + 1.877694e-001, + -1.070091e+000, + -2.798957e-001, + 2.338478e+000, + -2.647221e+000, + -7.387808e+000, + 2.329210e+000, + -1.644639e-001, + -2.003710e+000, + 9.874527e-001, + -1.067120e+000, + -1.418866e-001, + 1.254090e+000, + 6.053048e+000, + -2.918892e+000, + 5.322812e-001, + 1.613053e-001, + 3.018161e+000, + 5.274090e-001, + // albedo 0, turbidity 3 + -1.129483e+000, + -1.890619e-001, + -9.065101e+000, + 9.659923e+000, + -3.607819e-002, + 8.314359e-001, + 8.181661e-002, + 4.768868e+000, + 6.339777e-001, + -1.146420e+000, + -1.883579e-001, + 3.309173e+000, + -3.127882e+000, + -6.938176e-002, + 3.987113e-001, + 1.400581e-001, + 6.283042e+000, + 5.267076e-001, + -1.128348e+000, + -2.641305e-001, + 1.223176e+000, + 5.514952e-002, + -3.490649e-001, + 1.997784e+000, + -4.123709e-002, + -2.251251e+000, + 9.483466e-001, + -1.025820e+000, + 1.404690e-002, + -1.187406e+000, + 2.729900e+000, + 5.877588e-001, + -2.761140e-001, + 4.602633e-001, + 8.305125e+000, + 3.945001e-001, + -1.083957e+000, + -2.606679e-001, + 2.207108e+000, + -7.202803e+000, + -5.968103e+000, + 2.129455e+000, + -7.789512e-002, + -1.137688e+000, + 8.871769e-001, + -1.062465e+000, + -1.512189e-001, + 1.042881e+000, + 1.427839e+001, + -4.242214e+000, + 4.038100e-001, + 1.997780e-001, + 2.814449e+000, + 5.803196e-001, + // albedo 0, turbidity 4 + -1.175099e+000, + -2.410789e-001, + -1.108587e+001, + 1.133404e+001, + -1.819300e-002, + 6.772942e-001, + 9.605043e-002, + 4.231166e+000, + 6.239972e-001, + -1.224207e+000, + -2.883527e-001, + 3.002206e+000, + -2.649612e+000, + -4.795418e-002, + 4.984398e-001, + 3.251434e-002, + 4.851611e+000, + 6.551019e-001, + -1.136955e+000, + -2.423048e-001, + 1.058823e+000, + -2.489236e-001, + -2.462179e-001, + 1.933140e+000, + 9.106828e-002, + -1.905869e-001, + 8.171065e-001, + -1.014535e+000, + -8.262500e-003, + -1.448017e+000, + 2.295788e+000, + 3.510334e-001, + -1.477418e+000, + 5.432449e-001, + 5.762796e+000, + 4.908751e-001, + -1.070666e+000, + -2.379780e-001, + 1.844589e+000, + -5.442448e+000, + -4.012768e+000, + 2.945275e+000, + 9.854725e-003, + 8.455959e-002, + 8.145030e-001, + -1.071525e+000, + -1.777132e-001, + 8.076590e-001, + 9.925865e+000, + -3.324623e+000, + -6.367437e-001, + 2.844581e-001, + 2.248384e+000, + 6.544022e-001, + // albedo 0, turbidity 5 + -1.218818e+000, + -2.952382e-001, + -1.345975e+001, + 1.347153e+001, + -6.814585e-003, + 5.079068e-001, + 1.197230e-001, + 3.776949e+000, + 5.836961e-001, + -1.409868e+000, + -5.114330e-001, + 2.776539e+000, + -2.039001e+000, + -2.673769e-002, + 4.145288e-001, + 7.829342e-004, + 2.275883e+000, + 6.629691e-001, + -1.069151e+000, + -9.434247e-002, + 7.293972e-001, + -1.222473e+000, + -1.533461e-001, + 2.160357e+000, + 4.626837e-002, + 3.852415e+000, + 8.593570e-001, + -1.021306e+000, + -1.149551e-001, + -1.108414e+000, + 4.178343e+000, + 4.013665e-001, + -2.222814e+000, + 6.929462e-001, + 1.392652e+000, + 4.401662e-001, + -1.074251e+000, + -2.224002e-001, + 1.372356e+000, + -8.858704e+000, + -3.922660e+000, + 3.020018e+000, + -1.458724e-002, + 1.511186e+000, + 8.288064e-001, + -1.062048e+000, + -1.526582e-001, + 4.921067e-001, + 1.485522e+001, + -3.229936e+000, + -8.426604e-001, + 3.916243e-001, + 2.678994e+000, + 6.689264e-001, + // albedo 0, turbidity 6 + -1.257023e+000, + -3.364700e-001, + -1.527795e+001, + 1.504223e+001, + 2.717715e-003, + 3.029910e-001, + 1.636851e-001, + 3.561663e+000, + 5.283161e-001, + -1.635124e+000, + -7.329993e-001, + 3.523939e+000, + -2.566337e+000, + -1.902543e-002, + 5.505483e-001, + -6.242176e-002, + 1.065992e+000, + 6.654236e-001, + -9.295823e-001, + 4.845834e-002, + -2.992990e-001, + -2.001327e-001, + -8.019339e-002, + 1.807806e+000, + 9.020277e-002, + 5.095372e+000, + 8.639936e-001, + -1.093740e+000, + -2.148608e-001, + -5.216240e-001, + 2.119777e+000, + 9.506454e-002, + -1.831439e+000, + 6.961204e-001, + 1.102084e-001, + 4.384319e-001, + -1.044181e+000, + -1.849257e-001, + 9.071246e-001, + -4.648901e+000, + -2.279385e+000, + 2.356502e+000, + -4.169147e-002, + 1.932557e+000, + 8.296550e-001, + -1.061451e+000, + -1.458745e-001, + 2.952267e-001, + 8.967214e+000, + -3.726228e+000, + -5.022316e-001, + 5.684877e-001, + 3.102347e+000, + 6.658443e-001, + // albedo 0, turbidity 7 + -1.332391e+000, + -4.127769e-001, + -9.328643e+000, + 9.046194e+000, + 3.457775e-003, + 3.377425e-001, + 1.530909e-001, + 3.301209e+000, + 4.997917e-001, + -1.932002e+000, + -9.947777e-001, + -2.042329e+000, + 3.586940e+000, + -5.642182e-002, + 8.130478e-001, + -8.195988e-002, + 1.118294e-001, + 5.617231e-001, + -8.707374e-001, + 1.286999e-001, + 1.820054e+000, + -4.674706e+000, + 3.317471e-003, + 5.919018e-001, + 1.975278e-001, + 6.686519e+000, + 9.631727e-001, + -1.070378e+000, + -3.030579e-001, + -9.041938e-001, + 6.200201e+000, + 1.232207e-001, + -3.650628e-001, + 5.029403e-001, + -2.903162e+000, + 3.811408e-001, + -1.063035e+000, + -1.637545e-001, + 5.853072e-001, + -7.889906e+000, + -1.200641e+000, + 1.035018e+000, + 1.192093e-001, + 3.267054e+000, + 8.416151e-001, + -1.053655e+000, + -1.562286e-001, + 2.423683e-001, + 1.128575e+001, + -4.363262e+000, + -7.314160e-002, + 5.642088e-001, + 2.514023e+000, + 6.670457e-001, + // albedo 0, turbidity 8 + -1.366112e+000, + -4.718287e-001, + -7.876222e+000, + 7.746900e+000, + -9.182309e-003, + 4.716076e-001, + 8.320252e-002, + 3.165603e+000, + 5.392334e-001, + -2.468204e+000, + -1.336340e+000, + -5.386723e+000, + 7.072672e+000, + -8.329266e-002, + 8.636876e-001, + -1.978177e-002, + -1.326218e-001, + 2.979222e-001, + -9.653522e-001, + -2.373416e-002, + 1.810250e+000, + -6.467262e+000, + 1.410706e-001, + -4.753717e-001, + 3.003095e-001, + 6.551163e+000, + 1.151083e+000, + -8.943186e-001, + -2.487152e-001, + -2.308960e-001, + 8.512648e+000, + 1.298402e-001, + 1.034705e+000, + 2.303509e-001, + -3.924095e+000, + 2.982717e-001, + -1.146999e+000, + -2.318784e-001, + 8.992419e-002, + -9.933614e+000, + -8.860920e-001, + -3.071656e-002, + 2.852012e-001, + 3.046199e+000, + 8.599001e-001, + -1.032399e+000, + -1.645145e-001, + 2.683599e-001, + 1.327701e+001, + -4.407670e+000, + 7.709869e-002, + 4.951727e-001, + 1.957277e+000, + 6.630943e-001, + // albedo 0, turbidity 9 + -1.469070e+000, + -6.135092e-001, + -6.506263e+000, + 6.661315e+000, + -3.835383e-002, + 7.150413e-001, + 7.784318e-003, + 2.820577e+000, + 6.756784e-001, + -2.501583e+000, + -1.247404e+000, + -1.523462e+001, + 1.633191e+001, + -1.204803e-002, + 5.896471e-001, + -2.002023e-002, + 1.144647e+000, + 6.177874e-002, + -2.438672e+000, + -1.127291e+000, + 5.731172e+000, + -1.021350e+001, + 6.165610e-002, + -7.752641e-001, + 4.708254e-001, + 4.176847e+000, + 1.200881e+000, + -1.513427e-001, + 9.792731e-002, + -1.612349e+000, + 9.814289e+000, + 5.188921e-002, + 1.716403e+000, + -7.039255e-002, + -2.815115e+000, + 3.291874e-001, + -1.318511e+000, + -3.650554e-001, + 4.221268e-001, + -9.294529e+000, + -4.397520e-002, + -8.100625e-001, + 3.742719e-001, + 1.834166e+000, + 8.223450e-001, + -1.016009e+000, + -1.820264e-001, + 1.278426e-001, + 1.182696e+001, + -4.801528e+000, + 4.947899e-001, + 4.660378e-001, + 1.601254e+000, + 6.702359e-001, + // albedo 0, turbidity 10 + -1.841310e+000, + -9.781779e-001, + -4.610903e+000, + 4.824662e+000, + -5.100806e-002, + 6.463776e-001, + -6.377724e-006, + 2.216875e+000, + 8.618530e-001, + -2.376373e+000, + -1.108657e+000, + -1.489799e+001, + 1.546458e+001, + 4.091025e-002, + 9.761780e-002, + -1.048958e-002, + 2.165834e+000, + -1.609171e-001, + -4.710318e+000, + -2.261963e+000, + 6.947327e+000, + -1.034828e+001, + -1.325542e-001, + 7.508674e-001, + 2.247553e-001, + 2.873142e+000, + 1.297100e+000, + 2.163750e-001, + -1.944345e-001, + -2.437860e+000, + 1.011314e+001, + 4.450500e-001, + 3.111492e-001, + 2.751323e-001, + -1.627906e+000, + 2.531213e-001, + -1.258794e+000, + -3.524641e-001, + 8.425444e-001, + -1.085313e+001, + -1.154381e+000, + -4.638014e-001, + -2.781115e-003, + 4.344498e-001, + 8.507091e-001, + -1.018938e+000, + -1.804153e-001, + -6.354054e-002, + 1.573150e+001, + -4.386999e+000, + 6.211115e-001, + 5.294648e-001, + 1.580749e+000, + 6.586655e-001, + // albedo 1, turbidity 1 + -1.116416e+000, + -1.917524e-001, + -1.068233e+001, + 1.222221e+001, + -3.668978e-002, + 1.054022e+000, + 1.592132e-002, + 3.180583e+000, + 5.627370e-001, + -1.132341e+000, + -1.671286e-001, + 5.976499e+000, + -4.227366e+000, + -9.542489e-002, + 8.664938e-001, + 8.351793e-003, + 4.876068e+000, + 4.492779e-001, + -1.087635e+000, + -3.173679e-001, + 4.314407e-001, + 1.100555e+000, + -4.410057e-001, + 1.677253e+000, + -3.005925e-002, + -4.201249e+000, + 1.070902e+000, + -1.083031e+000, + -8.847705e-002, + 1.291773e+000, + 4.546776e-001, + 3.091894e-001, + 7.261760e-001, + 4.203659e-002, + 5.990615e+000, + 3.704756e-001, + -1.057899e+000, + -2.246706e-001, + 2.329563e+000, + -1.219656e+000, + -5.335260e+000, + 8.545378e-001, + -3.906209e-002, + -9.025499e-001, + 7.797348e-001, + -1.073305e+000, + -1.522553e-001, + 1.767063e+000, + 1.904280e+000, + -3.101673e+000, + 3.995856e-001, + 2.905192e-002, + 2.563977e+000, + 5.753067e-001, + // albedo 1, turbidity 2 + -1.113674e+000, + -1.759694e-001, + -9.754125e+000, + 1.087391e+001, + -3.841093e-002, + 9.524272e-001, + 5.680219e-002, + 4.227034e+000, + 6.029571e-001, + -1.126496e+000, + -1.680281e-001, + 5.332352e+000, + -4.575579e+000, + -6.761755e-002, + 3.295335e-001, + 1.194896e-001, + 5.570901e+000, + 4.536185e-001, + -1.103074e+000, + -2.681801e-001, + 6.571479e-002, + 2.396522e+000, + -4.551280e-001, + 2.466331e+000, + -1.232022e-001, + -3.023201e+000, + 1.086379e+000, + -1.053299e+000, + -2.697173e-002, + 8.379121e-001, + -9.681458e-001, + 5.890692e-001, + -4.872027e-001, + 2.936929e-001, + 7.510139e+000, + 3.079122e-001, + -1.079553e+000, + -2.710448e-001, + 2.462379e+000, + -3.713554e-001, + -8.534512e+000, + 1.828242e+000, + -1.686398e-001, + -1.961340e+000, + 8.941077e-001, + -1.069741e+000, + -1.396394e-001, + 1.657868e+000, + 3.236313e+000, + -2.706344e+000, + -2.948122e-001, + 1.314816e-001, + 2.868457e+000, + 5.413403e-001, + // albedo 1, turbidity 3 + -1.131649e+000, + -1.954455e-001, + -7.751595e+000, + 8.685861e+000, + -4.910871e-002, + 8.992952e-001, + 4.710143e-002, + 4.254818e+000, + 6.821116e-001, + -1.156689e+000, + -1.884324e-001, + 3.163519e+000, + -3.091522e+000, + -6.613927e-002, + -2.575883e-002, + 1.640065e-001, + 6.073643e+000, + 4.453468e-001, + -1.079224e+000, + -2.621389e-001, + 9.446437e-001, + 1.448479e+000, + -3.969384e-001, + 2.626638e+000, + -8.101186e-002, + -3.016355e+000, + 1.076295e+000, + -1.080832e+000, + 1.033057e-002, + -3.500156e-001, + -3.281419e-002, + 5.655512e-001, + -1.156742e+000, + 4.534710e-001, + 8.774122e+000, + 2.772869e-001, + -1.051202e+000, + -2.679975e-001, + 2.719109e+000, + -2.190316e+000, + -6.878798e+000, + 2.250481e+000, + -2.030252e-001, + -2.026527e+000, + 9.701096e-001, + -1.089849e+000, + -1.598589e-001, + 1.564748e+000, + 6.869187e+000, + -3.053670e+000, + -6.110435e-001, + 1.644472e-001, + 2.370452e+000, + 5.511770e-001, + // albedo 1, turbidity 4 + -1.171419e+000, + -2.429746e-001, + -8.991334e+000, + 9.571216e+000, + -2.772861e-002, + 6.688262e-001, + 7.683478e-002, + 3.785611e+000, + 6.347635e-001, + -1.228554e+000, + -2.917562e-001, + 2.753986e+000, + -2.491780e+000, + -4.663434e-002, + 3.118303e-001, + 7.546506e-002, + 4.463096e+000, + 5.955071e-001, + -1.093124e+000, + -2.447767e-001, + 9.097406e-001, + 5.448296e-001, + -2.957824e-001, + 2.024167e+000, + -5.152333e-004, + -1.069081e+000, + 9.369565e-001, + -1.056994e+000, + 1.569507e-002, + -8.217491e-001, + 1.870818e+000, + 7.061930e-001, + -1.483928e+000, + 5.978206e-001, + 6.864902e+000, + 3.673332e-001, + -1.054871e+000, + -2.758129e-001, + 2.712807e+000, + -5.950110e+000, + -6.554039e+000, + 2.447523e+000, + -1.895171e-001, + -1.454292e+000, + 9.131738e-001, + -1.100218e+000, + -1.746241e-001, + 1.438505e+000, + 1.115481e+001, + -3.266076e+000, + -8.837357e-001, + 1.970100e-001, + 1.991595e+000, + 5.907821e-001, + // albedo 1, turbidity 5 + -1.207267e+000, + -2.913610e-001, + -1.103767e+001, + 1.140724e+001, + -1.416800e-002, + 5.564047e-001, + 8.476262e-002, + 3.371255e+000, + 6.221335e-001, + -1.429698e+000, + -5.374218e-001, + 2.837524e+000, + -2.221936e+000, + -2.422337e-002, + 9.313758e-002, + 7.190250e-002, + 1.869022e+000, + 5.609035e-001, + -1.002274e+000, + -6.972810e-002, + 4.031308e-001, + -3.932997e-001, + -1.521923e-001, + 2.390646e+000, + -6.893990e-002, + 2.999661e+000, + 1.017843e+000, + -1.081168e+000, + -1.178666e-001, + -4.968080e-001, + 3.919299e+000, + 6.046866e-001, + -2.440615e+000, + 7.891538e-001, + 2.140835e+000, + 2.740470e-001, + -1.050727e+000, + -2.307688e-001, + 2.276396e+000, + -9.454407e+000, + -5.505176e+000, + 2.992620e+000, + -2.450942e-001, + 6.078372e-001, + 9.606765e-001, + -1.103752e+000, + -1.810202e-001, + 1.375044e+000, + 1.589095e+001, + -3.438954e+000, + -1.265669e+000, + 2.475172e-001, + 1.680768e+000, + 5.978056e-001, + // albedo 1, turbidity 6 + -1.244324e+000, + -3.378542e-001, + -1.111001e+001, + 1.137784e+001, + -7.896794e-003, + 4.808023e-001, + 9.249904e-002, + 3.025816e+000, + 5.880239e-001, + -1.593165e+000, + -7.027621e-001, + 2.220896e+000, + -1.437709e+000, + -1.534738e-002, + 6.286958e-002, + 6.644555e-002, + 1.091727e+000, + 5.470080e-001, + -9.136506e-001, + 1.344874e-002, + 7.772636e-001, + -1.209396e+000, + -1.408978e-001, + 2.433718e+000, + -1.041938e-001, + 3.791244e+000, + 1.037916e+000, + -1.134968e+000, + -1.803315e-001, + -9.267335e-001, + 4.576670e+000, + 6.851928e-001, + -2.805000e+000, + 8.687208e-001, + 1.161483e+000, + 2.571688e-001, + -1.017037e+000, + -2.053943e-001, + 2.361640e+000, + -9.887818e+000, + -5.122889e+000, + 3.287088e+000, + -2.594102e-001, + 8.578927e-001, + 9.592340e-001, + -1.118723e+000, + -1.934942e-001, + 1.226023e+000, + 1.674140e+001, + -3.277335e+000, + -1.629809e+000, + 2.765232e-001, + 1.637713e+000, + 6.113963e-001, + // albedo 1, turbidity 7 + -1.314779e+000, + -4.119915e-001, + -1.241150e+001, + 1.241578e+001, + 2.344284e-003, + 2.980837e-001, + 1.414613e-001, + 2.781731e+000, + 4.998556e-001, + -1.926199e+000, + -1.020038e+000, + 2.569200e+000, + -1.081159e+000, + -2.266833e-002, + 3.588668e-001, + 8.750078e-003, + -2.452171e-001, + 4.796758e-001, + -7.780002e-001, + 1.850647e-001, + 4.445456e-002, + -2.409297e+000, + -7.816346e-002, + 1.546790e+000, + -2.807227e-002, + 5.998176e+000, + 1.132396e+000, + -1.179326e+000, + -3.578330e-001, + -2.392933e-001, + 6.467883e+000, + 5.904596e-001, + -1.869975e+000, + 8.045839e-001, + -2.498121e+000, + 1.610633e-001, + -1.009956e+000, + -1.311896e-001, + 1.726577e+000, + -1.219356e+001, + -3.466239e+000, + 2.343602e+000, + -2.252205e-001, + 2.573681e+000, + 1.027109e+000, + -1.112460e+000, + -2.063093e-001, + 1.233051e+000, + 2.058946e+001, + -4.578074e+000, + -1.145643e+000, + 3.160192e-001, + 1.420159e+000, + 5.860212e-001, + // albedo 1, turbidity 8 + -1.371689e+000, + -4.914196e-001, + -1.076610e+001, + 1.107405e+001, + -1.485077e-002, + 5.936218e-001, + 3.685482e-002, + 2.599968e+000, + 6.002204e-001, + -2.436997e+000, + -1.377939e+000, + 2.130141e-002, + 1.079593e+000, + -1.796232e-002, + -3.933248e-002, + 1.610711e-001, + -6.901181e-001, + 1.206416e-001, + -8.743368e-001, + 7.331370e-002, + 8.734259e-001, + -3.743126e+000, + -3.151167e-002, + 1.297596e+000, + -7.634926e-002, + 6.532873e+000, + 1.435737e+000, + -9.810197e-001, + -3.521634e-001, + -2.855205e-001, + 7.134674e+000, + 6.839748e-001, + -1.394841e+000, + 6.952036e-001, + -4.633104e+000, + -2.173401e-002, + -1.122958e+000, + -1.691536e-001, + 1.382360e+000, + -1.102913e+001, + -2.608171e+000, + 1.865111e+000, + -1.345154e-001, + 3.112342e+000, + 1.094134e+000, + -1.075586e+000, + -2.077415e-001, + 1.171477e+000, + 1.793270e+001, + -4.656858e+000, + -1.036839e+000, + 3.338295e-001, + 1.042793e+000, + 5.739374e-001, + // albedo 1, turbidity 9 + -1.465871e+000, + -6.364486e-001, + -8.833718e+000, + 9.343650e+000, + -3.223600e-002, + 7.552848e-001, + -3.121341e-006, + 2.249164e+000, + 8.094662e-001, + -2.448924e+000, + -1.270878e+000, + -4.823703e+000, + 5.853058e+000, + -2.149127e-002, + 3.581132e-002, + -1.230276e-003, + 4.892553e-001, + -1.597657e-001, + -2.419809e+000, + -1.071337e+000, + 1.575648e+000, + -4.983580e+000, + 9.545185e-003, + 5.032615e-001, + 4.186266e-001, + 4.634147e+000, + 1.433517e+000, + -1.383278e-001, + -2.797095e-002, + -1.943067e-001, + 6.679623e+000, + 4.118280e-001, + -2.744289e-001, + -2.118722e-002, + -4.337025e+000, + 1.505072e-001, + -1.341872e+000, + -2.518572e-001, + 1.027009e+000, + -6.527103e+000, + -1.081271e+000, + 1.015465e+000, + 2.845789e-001, + 2.470371e+000, + 9.278120e-001, + -1.040640e+000, + -2.367454e-001, + 1.100744e+000, + 8.827253e+000, + -4.560794e+000, + -7.287017e-001, + 2.842503e-001, + 6.336593e-001, + 6.327335e-001, + // albedo 1, turbidity 10 + -1.877993e+000, + -1.025135e+000, + -4.311037e+000, + 4.715016e+000, + -4.711631e-002, + 6.335844e-001, + -7.665398e-006, + 1.788017e+000, + 9.001409e-001, + -2.281540e+000, + -1.137668e+000, + -1.036869e+001, + 1.136254e+001, + 1.961739e-002, + -9.836174e-002, + -6.734567e-003, + 1.320918e+000, + -2.400807e-001, + -4.904054e+000, + -2.315781e+000, + 5.735999e+000, + -8.626257e+000, + -1.255643e-001, + 1.545446e+000, + 1.396860e-001, + 2.972897e+000, + 1.429934e+000, + 4.077067e-001, + -1.833688e-001, + -2.450939e+000, + 9.119433e+000, + 4.505361e-001, + -1.340828e+000, + 3.973690e-001, + -1.785370e+000, + 9.628711e-002, + -1.296052e+000, + -3.250526e-001, + 1.813294e+000, + -1.031485e+001, + -1.388690e+000, + 1.239733e+000, + -8.989196e-002, + -3.389637e-001, + 9.639560e-001, + -1.062181e+000, + -2.423444e-001, + 7.577592e-001, + 1.566938e+001, + -4.462264e+000, + -5.742810e-001, + 3.262259e-001, + 9.461672e-001, + 6.232887e-001, +}; + +double datasetXYZRad1[] = +{ + // albedo 0, turbidity 1 + 1.560219e+000, + 1.417388e+000, + 1.206927e+000, + 1.091949e+001, + 5.931416e+000, + 7.304788e+000, + // albedo 0, turbidity 2 + 1.533049e+000, + 1.560532e+000, + 3.685059e-001, + 1.355040e+001, + 5.543711e+000, + 7.792189e+000, + // albedo 0, turbidity 3 + 1.471043e+000, + 1.746088e+000, + -9.299697e-001, + 1.720362e+001, + 5.473384e+000, + 8.336416e+000, + // albedo 0, turbidity 4 + 1.355991e+000, + 2.109348e+000, + -3.295855e+000, + 2.264843e+001, + 5.454607e+000, + 9.304656e+000, + // albedo 0, turbidity 5 + 1.244963e+000, + 2.547533e+000, + -5.841485e+000, + 2.756879e+001, + 5.576104e+000, + 1.043287e+001, + // albedo 0, turbidity 6 + 1.175532e+000, + 2.784634e+000, + -7.212225e+000, + 2.975347e+001, + 6.472980e+000, + 1.092331e+001, + // albedo 0, turbidity 7 + 1.082973e+000, + 3.118094e+000, + -8.934293e+000, + 3.186879e+001, + 8.473885e+000, + 1.174019e+001, + // albedo 0, turbidity 8 + 9.692500e-001, + 3.349574e+000, + -1.003810e+001, + 3.147654e+001, + 1.338931e+001, + 1.272547e+001, + // albedo 0, turbidity 9 + 8.547044e-001, + 3.151538e+000, + -9.095567e+000, + 2.554995e+001, + 2.273219e+001, + 1.410398e+001, + // albedo 0, turbidity 10 + 7.580340e-001, + 2.311153e+000, + -5.170814e+000, + 1.229669e+001, + 3.686529e+001, + 1.598882e+001, + // albedo 1, turbidity 1 + 1.664273e+000, + 1.574468e+000, + 1.422078e+000, + 9.768247e+000, + 1.447338e+001, + 1.644988e+001, + // albedo 1, turbidity 2 + 1.638295e+000, + 1.719586e+000, + 5.786675e-001, + 1.239846e+001, + 1.415419e+001, + 1.728605e+001, + // albedo 1, turbidity 3 + 1.572623e+000, + 1.921559e+000, + -7.714802e-001, + 1.609246e+001, + 1.420954e+001, + 1.825908e+001, + // albedo 1, turbidity 4 + 1.468395e+000, + 2.211970e+000, + -2.845869e+000, + 2.075027e+001, + 1.524822e+001, + 1.937622e+001, + // albedo 1, turbidity 5 + 1.355047e+000, + 2.556469e+000, + -4.960920e+000, + 2.460237e+001, + 1.648360e+001, + 2.065648e+001, + // albedo 1, turbidity 6 + 1.291642e+000, + 2.742036e+000, + -6.061967e+000, + 2.602002e+001, + 1.819144e+001, + 2.116712e+001, + // albedo 1, turbidity 7 + 1.194565e+000, + 2.972120e+000, + -7.295779e+000, + 2.691805e+001, + 2.124880e+001, + 2.201819e+001, + // albedo 1, turbidity 8 + 1.083631e+000, + 3.047021e+000, + -7.766096e+000, + 2.496261e+001, + 2.744264e+001, + 2.291875e+001, + // albedo 1, turbidity 9 + 9.707994e-001, + 2.736459e+000, + -6.308284e+000, + 1.760860e+001, + 3.776291e+001, + 2.392150e+001, + // albedo 1, turbidity 10 + 8.574294e-001, + 1.865155e+000, + -2.364707e+000, + 4.337793e+000, + 5.092831e+001, + 2.523432e+001, +}; + +double datasetXYZ2[] = +{ + // albedo 0, turbidity 1 + -1.127942e+000, + -1.905548e-001, + -1.252356e+001, + 1.375799e+001, + -3.624732e-002, + 1.055453e+000, + 1.385036e-002, + 4.176970e+000, + 5.928345e-001, + -1.155260e+000, + -1.778135e-001, + 6.216056e+000, + -5.254116e+000, + -8.787445e-002, + 8.434621e-001, + 4.025734e-002, + 6.195322e+000, + 3.111856e-001, + -1.125624e+000, + -3.217593e-001, + 5.043919e-001, + 1.686284e+000, + -3.536071e-001, + 1.476321e+000, + -7.899019e-002, + -4.522531e+000, + 1.271691e+000, + -1.081801e+000, + -1.033234e-001, + 9.995550e-001, + 7.482946e-003, + -6.776018e-002, + 1.463141e+000, + 9.492021e-002, + 5.612723e+000, + 1.298846e-001, + -1.075320e+000, + -2.402711e-001, + 2.141284e+000, + -1.203359e+000, + -4.945188e+000, + 1.437221e+000, + -8.096750e-002, + -1.028378e+000, + 1.004164e+000, + -1.073337e+000, + -1.516517e-001, + 1.639379e+000, + 2.304669e+000, + -3.214244e+000, + 1.286245e+000, + 5.613957e-002, + 2.480902e+000, + 4.999363e-001, + // albedo 0, turbidity 2 + -1.128399e+000, + -1.857793e-001, + -1.089863e+001, + 1.172984e+001, + -3.768099e-002, + 9.439285e-001, + 4.869335e-002, + 4.845114e+000, + 6.119211e-001, + -1.114002e+000, + -1.399280e-001, + 4.963800e+000, + -4.685500e+000, + -7.780879e-002, + 4.049736e-001, + 1.586297e-001, + 7.770264e+000, + 3.449006e-001, + -1.185472e+000, + -3.403543e-001, + 6.588322e-001, + 1.133713e+000, + -4.118674e-001, + 2.061191e+000, + -1.882768e-001, + -4.372586e+000, + 1.223530e+000, + -1.002272e+000, + 2.000703e-002, + 7.073269e-002, + 1.485075e+000, + 5.005589e-001, + 4.301494e-001, + 3.626541e-001, + 7.921098e+000, + 1.574766e-001, + -1.121006e+000, + -3.007777e-001, + 2.242051e+000, + -4.571561e+000, + -7.761071e+000, + 2.053404e+000, + -1.524018e-001, + -1.886162e+000, + 1.018208e+000, + -1.058864e+000, + -1.358673e-001, + 1.389667e+000, + 8.633409e+000, + -3.437249e+000, + 7.295429e-001, + 1.514700e-001, + 2.842513e+000, + 5.014325e-001, + // albedo 0, turbidity 3 + -1.144464e+000, + -2.043799e-001, + -1.020188e+001, + 1.071247e+001, + -3.256693e-002, + 7.860205e-001, + 6.872719e-002, + 4.824771e+000, + 6.259836e-001, + -1.170104e+000, + -2.118626e-001, + 4.391405e+000, + -4.198900e+000, + -7.111559e-002, + 3.890442e-001, + 1.024831e-001, + 6.282535e+000, + 5.365688e-001, + -1.129171e+000, + -2.552880e-001, + 2.238298e-001, + 7.314295e-001, + -3.562730e-001, + 1.881931e+000, + -3.078716e-002, + -1.039120e+000, + 9.096301e-001, + -1.042294e+000, + 4.450203e-003, + -5.116033e-001, + 2.627589e+000, + 6.098996e-001, + -1.264638e-001, + 4.325281e-001, + 7.080503e+000, + 4.583646e-001, + -1.082293e+000, + -2.723056e-001, + 2.065076e+000, + -8.143133e+000, + -7.892212e+000, + 2.142231e+000, + -7.106240e-002, + -1.122398e+000, + 8.338505e-001, + -1.071715e+000, + -1.426568e-001, + 1.095351e+000, + 1.729783e+001, + -3.851931e+000, + 4.360514e-001, + 2.114440e-001, + 2.970832e+000, + 5.944389e-001, + // albedo 0, turbidity 4 + -1.195909e+000, + -2.590449e-001, + -1.191037e+001, + 1.207947e+001, + -1.589842e-002, + 6.297846e-001, + 9.054772e-002, + 4.285959e+000, + 5.933752e-001, + -1.245763e+000, + -3.316637e-001, + 4.293660e+000, + -3.694011e+000, + -4.699947e-002, + 4.843684e-001, + 2.130425e-002, + 4.097549e+000, + 6.530809e-001, + -1.148742e+000, + -1.902509e-001, + -2.393233e-001, + -2.441254e-001, + -2.610918e-001, + 1.846988e+000, + 3.532866e-002, + 2.660106e+000, + 8.358294e-001, + -1.016080e+000, + -7.444960e-002, + -5.053436e-001, + 4.388855e+000, + 6.054987e-001, + -1.208300e+000, + 5.817215e-001, + 2.543570e+000, + 4.726568e-001, + -1.072027e+000, + -2.101440e-001, + 1.518378e+000, + -1.060119e+001, + -6.016546e+000, + 2.649475e+000, + -5.166992e-002, + 1.571269e+000, + 8.344622e-001, + -1.072365e+000, + -1.511201e-001, + 7.478010e-001, + 1.900732e+001, + -3.950387e+000, + -3.473907e-001, + 3.797211e-001, + 2.782949e+000, + 6.296808e-001, + // albedo 0, turbidity 5 + -1.239423e+000, + -3.136289e-001, + -1.351100e+001, + 1.349468e+001, + -7.070423e-003, + 5.012315e-001, + 1.106008e-001, + 3.803619e+000, + 5.577948e-001, + -1.452524e+000, + -5.676944e-001, + 2.993153e+000, + -2.277288e+000, + -2.168954e-002, + 3.056720e-001, + 1.152338e-002, + 1.852697e+000, + 6.427228e-001, + -1.061421e+000, + -4.590521e-002, + 6.057022e-001, + -1.096835e+000, + -1.504952e-001, + 2.344921e+000, + -5.491832e-002, + 5.268322e+000, + 9.082253e-001, + -1.042373e+000, + -1.769498e-001, + -1.075388e+000, + 3.831712e+000, + 3.154140e-001, + -2.416458e+000, + 7.909032e-001, + -1.492892e-002, + 3.854049e-001, + -1.064159e+000, + -1.892684e-001, + 1.438685e+000, + -8.166362e+000, + -3.616364e+000, + 3.275206e+000, + -1.203825e-001, + 2.039491e+000, + 8.688057e-001, + -1.070120e+000, + -1.569508e-001, + 4.124760e-001, + 1.399683e+001, + -3.547085e+000, + -1.046326e+000, + 4.973825e-001, + 2.791231e+000, + 6.503286e-001, + // albedo 0, turbidity 6 + -1.283579e+000, + -3.609518e-001, + -1.335397e+001, + 1.315248e+001, + -4.431938e-004, + 3.769526e-001, + 1.429824e-001, + 3.573613e+000, + 4.998696e-001, + -1.657952e+000, + -7.627948e-001, + 1.958222e+000, + -7.949816e-001, + -2.882837e-002, + 5.356149e-001, + -5.191946e-002, + 8.869955e-001, + 6.263320e-001, + -9.527600e-001, + 6.494189e-002, + 5.361303e-001, + -2.129590e+000, + -9.258630e-002, + 1.604776e+000, + 5.067770e-002, + 6.376055e+000, + 9.138052e-001, + -1.080827e+000, + -2.523120e-001, + -7.154262e-001, + 4.120085e+000, + 1.878228e-001, + -1.492158e+000, + 6.881655e-001, + -1.446611e+000, + 4.040631e-001, + -1.054075e+000, + -1.665498e-001, + 9.191052e-001, + -6.636943e+000, + -1.894826e+000, + 2.107810e+000, + -3.680499e-002, + 2.655452e+000, + 8.413840e-001, + -1.061127e+000, + -1.448849e-001, + 2.667493e-001, + 1.034103e+001, + -4.285769e+000, + -3.874504e-001, + 5.998752e-001, + 3.132426e+000, + 6.652753e-001, + // albedo 0, turbidity 7 + -1.347345e+000, + -4.287832e-001, + -9.305553e+000, + 9.133813e+000, + -3.173527e-003, + 3.977564e-001, + 1.151420e-001, + 3.320564e+000, + 4.998134e-001, + -1.927296e+000, + -9.901372e-001, + -2.593499e+000, + 4.087421e+000, + -5.833993e-002, + 8.158929e-001, + -4.681279e-002, + 2.423716e-001, + 4.938052e-001, + -9.470092e-001, + 7.325237e-002, + 2.064735e+000, + -5.167540e+000, + -1.313751e-002, + 4.832169e-001, + 1.126295e-001, + 6.970522e+000, + 1.035022e+000, + -1.022557e+000, + -2.762616e-001, + -9.375748e-001, + 6.696739e+000, + 2.200765e-001, + -1.133253e-001, + 5.492505e-001, + -3.109391e+000, + 3.321914e-001, + -1.087444e+000, + -1.836263e-001, + 6.225024e-001, + -8.576765e+000, + -1.107637e+000, + 7.859427e-001, + 9.910909e-002, + 3.112938e+000, + 8.596261e-001, + -1.051544e+000, + -1.546262e-001, + 2.371731e-001, + 1.200502e+001, + -4.527291e+000, + 7.268862e-002, + 5.571478e-001, + 2.532873e+000, + 6.662000e-001, + // albedo 0, turbidity 8 + -1.375576e+000, + -4.840019e-001, + -8.121290e+000, + 8.058140e+000, + -1.445661e-002, + 5.123314e-001, + 5.813321e-002, + 3.203219e+000, + 5.442318e-001, + -2.325221e+000, + -1.241463e+000, + -7.063430e+000, + 8.741369e+000, + -7.829950e-002, + 8.844273e-001, + -3.471106e-002, + 1.740583e-001, + 2.814079e-001, + -1.228700e+000, + -2.013412e-001, + 2.949042e+000, + -7.371945e+000, + 1.071753e-001, + -2.491970e-001, + 2.265223e-001, + 6.391504e+000, + 1.172389e+000, + -7.601786e-001, + -1.680631e-001, + -7.584444e-001, + 8.541356e+000, + 8.222291e-002, + 6.729633e-001, + 3.206615e-001, + -3.700940e+000, + 2.710054e-001, + -1.191166e+000, + -2.672347e-001, + 2.927498e-001, + -9.713613e+000, + -4.783721e-001, + 2.352803e-001, + 2.161949e-001, + 2.691481e+000, + 8.745447e-001, + -1.030135e+000, + -1.653301e-001, + 2.263443e-001, + 1.296157e+001, + -4.650644e+000, + 7.055709e-003, + 5.091975e-001, + 2.000370e+000, + 6.603839e-001, + // albedo 0, turbidity 9 + -1.508018e+000, + -6.460933e-001, + -6.402745e+000, + 6.545995e+000, + -3.750320e-002, + 6.921803e-001, + 3.309819e-003, + 2.797527e+000, + 6.978446e-001, + -2.333308e+000, + -1.167837e+000, + -1.746787e+001, + 1.868630e+001, + -8.948229e-003, + 5.621946e-001, + -3.402626e-002, + 1.217943e+000, + 1.149865e-002, + -2.665953e+000, + -1.226307e+000, + 7.169725e+000, + -1.159434e+001, + 3.583420e-002, + -3.074378e-001, + 3.412248e-001, + 4.422122e+000, + 1.283791e+000, + -9.705116e-002, + 8.312991e-002, + -2.160462e+000, + 1.028235e+001, + 3.543357e-002, + 1.032049e+000, + 1.058310e-001, + -2.972898e+000, + 2.418628e-001, + -1.329617e+000, + -3.699557e-001, + 5.560117e-001, + -9.730113e+000, + 9.938865e-002, + -3.071488e-001, + 2.510691e-001, + 1.777111e+000, + 8.705142e-001, + -1.019387e+000, + -1.893247e-001, + 1.194079e-001, + 1.239436e+001, + -4.799224e+000, + 2.940213e-001, + 4.841268e-001, + 1.529724e+000, + 6.582615e-001, + // albedo 0, turbidity 10 + -1.896737e+000, + -1.005442e+000, + -6.411032e+000, + 6.548220e+000, + -3.227596e-002, + 5.717262e-001, + -8.115192e-006, + 2.296704e+000, + 9.000749e-001, + -2.411116e+000, + -1.225587e+000, + -1.753629e+001, + 1.829393e+001, + 1.247555e-002, + 2.364616e-001, + -5.114637e-003, + 1.603778e+000, + -2.224156e-001, + -4.707121e+000, + -2.074977e+000, + 7.942300e+000, + -1.132407e+001, + -5.415654e-002, + 5.446811e-001, + 1.032493e-001, + 4.010235e+000, + 1.369802e+000, + 1.010482e-001, + -4.013305e-001, + -2.674579e+000, + 9.779409e+000, + 1.782506e-001, + 7.053045e-001, + 4.200002e-001, + -2.400671e+000, + 1.953165e-001, + -1.243526e+000, + -3.391255e-001, + 8.848882e-001, + -9.789025e+000, + -3.997324e-001, + -9.546227e-001, + -1.044017e-001, + 6.010593e-001, + 8.714462e-001, + -1.014633e+000, + -1.730009e-001, + -7.738934e-002, + 1.390903e+001, + -4.847307e+000, + 1.076059e+000, + 5.685743e-001, + 1.572992e+000, + 6.561432e-001, + // albedo 1, turbidity 1 + -1.122998e+000, + -1.881183e-001, + -1.030709e+001, + 1.158932e+001, + -4.079495e-002, + 9.603774e-001, + 3.079436e-002, + 4.009235e+000, + 5.060745e-001, + -1.134790e+000, + -1.539688e-001, + 5.478405e+000, + -4.217270e+000, + -1.043858e-001, + 7.165008e-001, + 1.524765e-002, + 6.473623e+000, + 4.207882e-001, + -1.134957e+000, + -3.513318e-001, + 7.393837e-001, + 1.354415e+000, + -4.764078e-001, + 1.690441e+000, + -5.492640e-002, + -5.563523e+000, + 1.145743e+000, + -1.058344e+000, + -5.758503e-002, + 1.168230e+000, + 3.269824e-001, + 1.795193e-001, + 7.849011e-001, + 7.441853e-002, + 6.904804e+000, + 2.818790e-001, + -1.075194e+000, + -2.355813e-001, + 2.463685e+000, + -1.536505e+000, + -7.505771e+000, + 9.619712e-001, + -6.465851e-002, + -1.355492e+000, + 8.489847e-001, + -1.079030e+000, + -1.465328e-001, + 1.773838e+000, + 2.310131e+000, + -3.136065e+000, + 3.507952e-001, + 4.435014e-002, + 2.819225e+000, + 5.689008e-001, + // albedo 1, turbidity 2 + -1.125833e+000, + -1.870849e-001, + -9.555833e+000, + 1.059713e+001, + -4.225402e-002, + 9.164663e-001, + 4.338796e-002, + 4.400980e+000, + 6.056119e-001, + -1.127440e+000, + -1.551891e-001, + 4.755621e+000, + -4.408806e+000, + -7.851763e-002, + 2.268284e-001, + 1.460070e-001, + 7.048003e+000, + 3.525997e-001, + -1.143788e+000, + -3.170178e-001, + 5.480669e-001, + 2.041830e+000, + -4.532139e-001, + 2.302233e+000, + -1.887419e-001, + -4.489221e+000, + 1.250967e+000, + -1.032849e+000, + 7.376031e-003, + 5.666073e-001, + -2.312203e-001, + 4.862894e-001, + -1.748294e-001, + 3.572870e-001, + 8.380522e+000, + 1.302333e-001, + -1.093728e+000, + -2.786977e-001, + 2.641272e+000, + -1.507494e+000, + -8.731243e+000, + 1.684055e+000, + -2.023377e-001, + -2.176398e+000, + 1.013249e+000, + -1.076578e+000, + -1.456205e-001, + 1.693935e+000, + 2.945003e+000, + -2.822673e+000, + -2.520033e-001, + 1.517034e-001, + 2.649109e+000, + 5.179094e-001, + // albedo 1, turbidity 3 + -1.146417e+000, + -2.119353e-001, + -7.187525e+000, + 8.058599e+000, + -5.256438e-002, + 8.375733e-001, + 3.887093e-002, + 4.222111e+000, + 6.695347e-001, + -1.173674e+000, + -2.067025e-001, + 2.899359e+000, + -2.804918e+000, + -8.473899e-002, + 3.944225e-003, + 1.340641e-001, + 6.160887e+000, + 4.527141e-001, + -1.090098e+000, + -2.599633e-001, + 9.180856e-001, + 1.092710e+000, + -4.215019e-001, + 2.427660e+000, + -9.277667e-002, + -2.123523e+000, + 1.058159e+000, + -1.084460e+000, + 8.056181e-003, + -2.453510e-001, + 6.619567e-001, + 4.668118e-001, + -9.526719e-001, + 4.648454e-001, + 8.001572e+000, + 3.054194e-001, + -1.053728e+000, + -2.765784e-001, + 2.792388e+000, + -3.489517e+000, + -8.150535e+000, + 2.195757e+000, + -2.017234e-001, + -2.128017e+000, + 9.326589e-001, + -1.099348e+000, + -1.593939e-001, + 1.568292e+000, + 7.247853e+000, + -2.933000e+000, + -5.890481e-001, + 1.724440e-001, + 2.433484e+000, + 5.736558e-001, + // albedo 1, turbidity 4 + -1.185983e+000, + -2.581184e-001, + -7.761056e+000, + 8.317053e+000, + -3.351773e-002, + 6.676667e-001, + 5.941733e-002, + 3.820727e+000, + 6.324032e-001, + -1.268591e+000, + -3.398067e-001, + 2.348503e+000, + -2.023779e+000, + -5.368458e-002, + 1.083282e-001, + 8.402858e-002, + 3.910254e+000, + 5.577481e-001, + -1.071353e+000, + -1.992459e-001, + 7.878387e-001, + 1.974702e-001, + -3.033058e-001, + 2.335298e+000, + -8.205259e-002, + 7.954454e-001, + 9.972312e-001, + -1.089513e+000, + -3.104364e-002, + -5.995746e-001, + 2.330281e+000, + 6.581939e-001, + -1.821467e+000, + 6.679973e-001, + 5.090195e+000, + 3.125161e-001, + -1.040214e+000, + -2.570934e-001, + 2.660489e+000, + -6.506045e+000, + -7.053586e+000, + 2.763153e+000, + -2.433632e-001, + -7.648176e-001, + 9.452937e-001, + -1.116052e+000, + -1.831993e-001, + 1.457694e+000, + 1.163608e+001, + -3.216426e+000, + -1.045594e+000, + 2.285002e-001, + 1.817407e+000, + 5.810396e-001, + // albedo 1, turbidity 5 + -1.230134e+000, + -3.136264e-001, + -8.909301e+000, + 9.145006e+000, + -1.055387e-002, + 4.467317e-001, + 1.016826e-001, + 3.342964e+000, + 5.633840e-001, + -1.442907e+000, + -5.593147e-001, + 2.156447e+000, + -1.241657e+000, + -3.512130e-002, + 3.050274e-001, + 1.797175e-002, + 1.742358e+000, + 5.977153e-001, + -1.027627e+000, + -6.481539e-002, + 4.351975e-001, + -1.051677e+000, + -2.030672e-001, + 1.942684e+000, + -3.615993e-002, + 4.050266e+000, + 9.801624e-001, + -1.082110e+000, + -1.578209e-001, + -3.397511e-001, + 4.163851e+000, + 6.650368e-001, + -1.841730e+000, + 7.062544e-001, + 6.789881e-001, + 3.172623e-001, + -1.047447e+000, + -1.977560e-001, + 2.183364e+000, + -8.805249e+000, + -5.483962e+000, + 2.551309e+000, + -1.779640e-001, + 1.519501e+000, + 9.212536e-001, + -1.111853e+000, + -1.935736e-001, + 1.394408e+000, + 1.392405e+001, + -3.465430e+000, + -1.068432e+000, + 2.388671e-001, + 1.455336e+000, + 6.233425e-001, + // albedo 1, turbidity 6 + -1.262238e+000, + -3.546341e-001, + -1.008703e+001, + 1.020084e+001, + -1.852187e-003, + 3.537580e-001, + 1.239199e-001, + 3.056093e+000, + 5.132052e-001, + -1.613810e+000, + -7.355585e-001, + 2.760123e+000, + -1.685253e+000, + -2.517552e-002, + 2.914258e-001, + 4.743448e-003, + 8.689596e-001, + 5.674192e-001, + -9.462336e-001, + 2.950767e-002, + -2.613816e-001, + -7.398653e-001, + -1.315558e-001, + 1.901042e+000, + -6.447844e-002, + 4.969341e+000, + 1.027342e+000, + -1.111481e+000, + -2.194054e-001, + -9.004538e-002, + 3.983442e+000, + 4.871278e-001, + -1.965315e+000, + 7.956121e-001, + -2.363225e-001, + 2.718037e-001, + -1.036397e+000, + -1.827106e-001, + 1.964747e+000, + -8.870759e+000, + -4.208011e+000, + 2.461215e+000, + -2.158905e-001, + 1.561676e+000, + 9.436866e-001, + -1.113769e+000, + -1.947819e-001, + 1.300720e+000, + 1.516476e+001, + -4.088732e+000, + -1.069384e+000, + 2.836434e-001, + 1.671451e+000, + 6.229612e-001, + // albedo 1, turbidity 7 + -1.328069e+000, + -4.244047e-001, + -8.417040e+000, + 8.552244e+000, + -6.813504e-003, + 4.127422e-001, + 9.619897e-002, + 2.854227e+000, + 5.059880e-001, + -1.927552e+000, + -1.025290e+000, + 9.529576e-001, + 4.255950e-001, + -3.738779e-002, + 2.584586e-001, + 4.911004e-002, + -2.640913e-001, + 4.138626e-001, + -8.488094e-001, + 1.435988e-001, + 6.356807e-001, + -2.895732e+000, + -8.473961e-002, + 1.701305e+000, + -1.323908e-001, + 6.499338e+000, + 1.210928e+000, + -1.128313e+000, + -3.397048e-001, + -4.043140e-001, + 6.265097e+000, + 5.482395e-001, + -2.057614e+000, + 8.884087e-001, + -2.943879e+000, + 9.760301e-002, + -1.039764e+000, + -1.494772e-001, + 1.781915e+000, + -1.153012e+001, + -3.379232e+000, + 2.517231e+000, + -2.764393e-001, + 2.588849e+000, + 1.052120e+000, + -1.108447e+000, + -2.012251e-001, + 1.198640e+000, + 1.925331e+001, + -4.423892e+000, + -1.257122e+000, + 3.395690e-001, + 1.481220e+000, + 5.880175e-001, + // albedo 1, turbidity 8 + -1.374185e+000, + -4.967434e-001, + -7.401318e+000, + 7.724021e+000, + -2.345723e-002, + 5.979653e-001, + 2.436346e-002, + 2.658970e+000, + 6.014891e-001, + -2.310933e+000, + -1.290290e+000, + -1.301909e+000, + 2.557806e+000, + -3.744449e-002, + 8.982861e-002, + 1.090613e-001, + -4.398363e-001, + 1.184329e-001, + -1.124730e+000, + -9.921830e-002, + 1.366902e+000, + -4.172489e+000, + -5.078016e-002, + 1.393597e+000, + -9.323843e-002, + 6.452721e+000, + 1.435913e+000, + -8.468477e-001, + -2.744819e-001, + -4.347200e-001, + 6.713362e+000, + 6.127133e-001, + -1.685634e+000, + 7.360941e-001, + -4.535502e+000, + -2.920866e-002, + -1.165242e+000, + -2.008697e-001, + 1.438778e+000, + -1.008936e+001, + -2.214771e+000, + 2.102909e+000, + -1.763085e-001, + 2.859075e+000, + 1.093470e+000, + -1.074614e+000, + -2.066374e-001, + 1.131891e+000, + 1.630063e+001, + -4.801441e+000, + -1.112590e+000, + 3.595785e-001, + 1.122227e+000, + 5.794610e-001, + // albedo 1, turbidity 9 + -1.521515e+000, + -6.835604e-001, + -5.571044e+000, + 6.028774e+000, + -4.253715e-002, + 6.875746e-001, + -5.279456e-006, + 2.180150e+000, + 8.487705e-001, + -2.240415e+000, + -1.171166e+000, + -7.182771e+000, + 8.417068e+000, + -1.932866e-002, + 1.101887e-001, + -1.098862e-002, + 6.242195e-001, + -2.393875e-001, + -2.712354e+000, + -1.198830e+000, + 3.180200e+000, + -6.768130e+000, + -2.563386e-003, + 7.984607e-001, + 2.764376e-001, + 4.695358e+000, + 1.557045e+000, + -3.655172e-002, + -2.142321e-002, + -9.138120e-001, + 7.932786e+000, + 3.516542e-001, + -7.994343e-001, + 1.786761e-001, + -4.208399e+000, + 1.820576e-002, + -1.368610e+000, + -2.656212e-001, + 1.249397e+000, + -8.317818e+000, + -8.962772e-001, + 1.423249e+000, + 1.478381e-001, + 2.191660e+000, + 1.007748e+000, + -1.041753e+000, + -2.453366e-001, + 1.061102e+000, + 1.130172e+001, + -4.739312e+000, + -9.223334e-001, + 2.982776e-001, + 6.162931e-001, + 6.080302e-001, + // albedo 1, turbidity 10 + -1.989159e+000, + -1.095160e+000, + -2.915550e+000, + 3.275339e+000, + -5.735765e-002, + 5.742174e-001, + -7.683288e-006, + 1.763400e+000, + 9.001342e-001, + -2.070020e+000, + -1.086338e+000, + -1.095898e+001, + 1.206960e+001, + 3.780123e-002, + -1.774699e-002, + -5.881348e-004, + 1.333819e+000, + -2.605423e-001, + -5.249653e+000, + -2.383040e+000, + 6.160406e+000, + -9.097138e+000, + -1.955319e-001, + 1.651785e+000, + 6.016463e-004, + 3.021824e+000, + 1.493574e+000, + 4.685432e-001, + -2.358662e-001, + -2.666433e+000, + 9.685763e+000, + 5.804928e-001, + -1.521875e+000, + 5.668989e-001, + -1.548136e+000, + 1.688642e-002, + -1.296891e+000, + -3.449031e-001, + 1.928548e+000, + -1.167560e+001, + -1.627615e+000, + 1.355603e+000, + -1.929074e-001, + -6.568952e-001, + 1.009774e+000, + -1.067288e+000, + -2.410392e-001, + 7.147961e-001, + 1.783840e+001, + -4.374399e+000, + -6.588777e-001, + 3.329831e-001, + 1.012066e+000, + 6.118645e-001, +}; + +double datasetXYZRad2[] = +{ + // albedo 0, turbidity 1 + 1.632341e+000, + 1.395230e+000, + 1.375634e+000, + 1.238193e+001, + 5.921102e+000, + 7.766508e+000, + // albedo 0, turbidity 2 + 1.597115e+000, + 1.554617e+000, + 3.932382e-001, + 1.505284e+001, + 5.725234e+000, + 8.158155e+000, + // albedo 0, turbidity 3 + 1.522034e+000, + 1.844545e+000, + -1.322862e+000, + 1.918382e+001, + 5.440769e+000, + 8.837119e+000, + // albedo 0, turbidity 4 + 1.403048e+000, + 2.290852e+000, + -4.013792e+000, + 2.485100e+001, + 5.521888e+000, + 9.845547e+000, + // albedo 0, turbidity 5 + 1.286364e+000, + 2.774498e+000, + -6.648221e+000, + 2.964151e+001, + 5.923777e+000, + 1.097075e+001, + // albedo 0, turbidity 6 + 1.213544e+000, + 3.040195e+000, + -8.092676e+000, + 3.186082e+001, + 6.789782e+000, + 1.158899e+001, + // albedo 0, turbidity 7 + 1.122622e+000, + 3.347465e+000, + -9.649016e+000, + 3.343824e+001, + 9.347715e+000, + 1.231374e+001, + // albedo 0, turbidity 8 + 1.007356e+000, + 3.543858e+000, + -1.053520e+001, + 3.239842e+001, + 1.483962e+001, + 1.331718e+001, + // albedo 0, turbidity 9 + 8.956642e-001, + 3.278700e+000, + -9.254933e+000, + 2.557923e+001, + 2.489677e+001, + 1.476166e+001, + // albedo 0, turbidity 10 + 7.985143e-001, + 2.340404e+000, + -4.928274e+000, + 1.141787e+001, + 3.961501e+001, + 1.682448e+001, + // albedo 1, turbidity 1 + 1.745162e+000, + 1.639467e+000, + 1.342721e+000, + 1.166033e+001, + 1.490124e+001, + 1.774031e+001, + // albedo 1, turbidity 2 + 1.708439e+000, + 1.819144e+000, + 2.834399e-001, + 1.448066e+001, + 1.459214e+001, + 1.858679e+001, + // albedo 1, turbidity 3 + 1.631720e+000, + 2.094799e+000, + -1.378825e+000, + 1.843198e+001, + 1.463173e+001, + 1.962881e+001, + // albedo 1, turbidity 4 + 1.516536e+000, + 2.438729e+000, + -3.624121e+000, + 2.298621e+001, + 1.599782e+001, + 2.070027e+001, + // albedo 1, turbidity 5 + 1.405863e+000, + 2.785191e+000, + -5.705236e+000, + 2.645121e+001, + 1.768330e+001, + 2.191903e+001, + // albedo 1, turbidity 6 + 1.344052e+000, + 2.951807e+000, + -6.683851e+000, + 2.744271e+001, + 1.985706e+001, + 2.229452e+001, + // albedo 1, turbidity 7 + 1.245827e+000, + 3.182923e+000, + -7.822960e+000, + 2.791395e+001, + 2.327254e+001, + 2.315910e+001, + // albedo 1, turbidity 8 + 1.132305e+000, + 3.202593e+000, + -8.008429e+000, + 2.521093e+001, + 3.000014e+001, + 2.405306e+001, + // albedo 1, turbidity 9 + 1.020330e+000, + 2.820556e+000, + -6.238704e+000, + 1.709276e+001, + 4.077916e+001, + 2.509949e+001, + // albedo 1, turbidity 10 + 9.031570e-001, + 1.863917e+000, + -1.955738e+000, + 3.032665e+000, + 5.434290e+001, + 2.641780e+001, +}; + +double datasetXYZ3[] = +{ + // albedo 0, turbidity 1 + -1.310023e+000, + -4.407658e-001, + -3.640340e+001, + 3.683292e+001, + -8.124762e-003, + 5.297961e-001, + 1.188633e-002, + 3.138320e+000, + 5.134778e-001, + -1.424100e+000, + -5.501606e-001, + -1.753510e+001, + 1.822769e+001, + -1.539272e-002, + 6.366826e-001, + 2.661996e-003, + 2.659915e+000, + 4.071138e-001, + -1.103436e+000, + -1.884105e-001, + 6.425322e+000, + -6.910579e+000, + -2.019861e-002, + 3.553271e-001, + -1.589061e-002, + 5.345985e+000, + 8.790218e-001, + -1.186200e+000, + -4.307514e-001, + -3.957947e+000, + 5.979352e+000, + -5.348869e-002, + 1.736117e+000, + 3.491346e-002, + -2.692261e+000, + 5.610506e-001, + -1.006038e+000, + -1.305995e-001, + 4.473513e+000, + -3.806719e+000, + 1.419407e-001, + -2.148238e-002, + -5.081185e-002, + 3.735362e+000, + 5.358280e-001, + -1.078507e+000, + -1.633754e-001, + -3.812368e+000, + 4.381700e+000, + 2.988122e-002, + 1.754224e+000, + 1.472376e-001, + 3.722798e+000, + 4.999157e-001, + // albedo 0, turbidity 2 + -1.333582e+000, + -4.649908e-001, + -3.359528e+001, + 3.404375e+001, + -9.384242e-003, + 5.587511e-001, + 5.726310e-003, + 3.073145e+000, + 5.425529e-001, + -1.562624e+000, + -7.107068e-001, + -1.478170e+001, + 1.559839e+001, + -1.462375e-002, + 5.050133e-001, + 2.516017e-002, + 1.604696e+000, + 2.902403e-001, + -8.930158e-001, + 4.068077e-002, + 1.373481e+000, + -2.342752e+000, + -2.098058e-002, + 6.248686e-001, + -5.258363e-002, + 7.058214e+000, + 1.150373e+000, + -1.262823e+000, + -4.818353e-001, + 8.892610e-004, + 1.923120e+000, + -4.979718e-002, + 1.040693e+000, + 1.558103e-001, + -2.852480e+000, + 2.420691e-001, + -9.968383e-001, + -1.200648e-001, + 1.324342e+000, + -9.430889e-001, + 1.931098e-001, + 4.436916e-001, + -7.320456e-002, + 4.215931e+000, + 7.898019e-001, + -1.078185e+000, + -1.718192e-001, + -1.720191e+000, + 2.358918e+000, + 2.765637e-002, + 1.260245e+000, + 2.021941e-001, + 3.395483e+000, + 5.173628e-001, + // albedo 0, turbidity 3 + -1.353023e+000, + -4.813523e-001, + -3.104920e+001, + 3.140156e+001, + -9.510741e-003, + 5.542030e-001, + 8.135471e-003, + 3.136646e+000, + 5.215989e-001, + -1.624704e+000, + -7.990201e-001, + -2.167125e+001, + 2.246341e+001, + -1.163533e-002, + 5.415746e-001, + 2.618378e-002, + 1.139214e+000, + 3.444357e-001, + -7.983610e-001, + 1.417476e-001, + 9.914841e+000, + -1.081503e+001, + -1.218845e-002, + 3.411392e-001, + -6.137698e-002, + 7.445848e+000, + 1.180080e+000, + -1.266679e+000, + -4.288977e-001, + -5.818701e+000, + 6.986437e+000, + -8.180711e-002, + 1.397403e+000, + 2.016916e-001, + -1.275731e+000, + 2.592773e-001, + -1.009707e+000, + -1.537754e-001, + 3.496378e+000, + -3.013726e+000, + 2.421150e-001, + -2.831925e-001, + 3.003395e-002, + 3.702862e+000, + 7.746320e-001, + -1.075646e+000, + -1.768747e-001, + -1.347762e+000, + 1.989004e+000, + 1.375836e-002, + 1.764810e+000, + 1.330018e-001, + 3.230864e+000, + 6.626210e-001, + // albedo 0, turbidity 4 + -1.375269e+000, + -5.103569e-001, + -3.442661e+001, + 3.478703e+001, + -8.460009e-003, + 5.408643e-001, + 4.813323e-003, + 3.016078e+000, + 5.062069e-001, + -1.821679e+000, + -9.766461e-001, + -1.926488e+001, + 1.997912e+001, + -9.822567e-003, + 3.649556e-001, + 4.316092e-002, + 8.930190e-001, + 4.166527e-001, + -6.633542e-001, + 1.997841e-001, + 2.395592e+000, + -3.117175e+000, + -1.080884e-002, + 8.983814e-001, + -1.375825e-001, + 6.673463e+000, + 1.115663e+000, + -1.303240e+000, + -3.612712e-001, + 8.292959e-002, + 3.381364e-001, + -6.078648e-002, + 3.229247e-001, + 3.680987e-001, + 7.046755e-001, + 3.144924e-001, + -9.952598e-001, + -2.039076e-001, + 4.026851e-001, + 2.686684e-001, + 1.640712e-001, + 5.186341e-001, + -1.205520e-002, + 2.659613e+000, + 8.030394e-001, + -1.098579e+000, + -2.151992e-001, + 6.558198e-001, + -7.436900e-004, + -1.421817e-003, + 1.073701e+000, + 1.886875e-001, + 2.536857e+000, + 6.673923e-001, + // albedo 0, turbidity 5 + -1.457986e+000, + -5.906842e-001, + -3.812464e+001, + 3.838539e+001, + -6.024357e-003, + 4.741484e-001, + 1.209223e-002, + 2.818432e+000, + 5.012433e-001, + -1.835728e+000, + -1.003405e+000, + -6.848129e+000, + 7.601943e+000, + -1.277375e-002, + 4.785598e-001, + 3.366853e-002, + 1.097701e+000, + 4.636635e-001, + -8.491348e-001, + 9.466365e-003, + -2.685226e+000, + 2.004060e+000, + -1.168708e-002, + 6.752316e-001, + -1.543371e-001, + 5.674759e+000, + 1.039534e+000, + -1.083379e+000, + -1.506790e-001, + 7.328236e-001, + -5.095568e-001, + -8.609153e-002, + 4.448820e-001, + 4.174662e-001, + 1.481556e+000, + 3.942551e-001, + -1.117089e+000, + -3.337605e-001, + 2.502281e-001, + 4.036323e-001, + 2.673899e-001, + 2.829817e-001, + 2.242450e-002, + 2.043207e+000, + 7.706902e-001, + -1.071648e+000, + -2.126200e-001, + 6.069466e-001, + -1.456290e-003, + -5.515960e-001, + 1.046755e+000, + 1.985021e-001, + 2.290245e+000, + 6.876058e-001, + // albedo 0, turbidity 6 + -1.483903e+000, + -6.309647e-001, + -4.380213e+001, + 4.410537e+001, + -5.712161e-003, + 5.195992e-001, + 2.028428e-003, + 2.687114e+000, + 5.098321e-001, + -2.053976e+000, + -1.141473e+000, + 5.109183e-001, + 8.060391e-002, + -1.033983e-002, + 4.066532e-001, + 4.869627e-002, + 1.161722e+000, + 4.039525e-001, + -6.348185e-001, + 7.651292e-002, + -1.031327e+001, + 1.007598e+001, + -2.083688e-002, + 7.359516e-001, + -2.029459e-001, + 5.013257e+000, + 1.077649e+000, + -1.228630e+000, + -1.650496e-001, + 4.077157e-002, + -7.189167e-001, + -5.092220e-002, + 2.959814e-001, + 5.111496e-001, + 2.540433e+000, + 3.615330e-001, + -1.041883e+000, + -3.278413e-001, + -6.691911e-002, + 1.307364e+000, + 2.166663e-001, + 3.000595e-001, + -3.157136e-003, + 1.389208e+000, + 7.999026e-001, + -1.103556e+000, + -2.443602e-001, + 4.705347e-001, + -9.296482e-004, + -5.309920e-001, + 9.654511e-001, + 2.142587e-001, + 2.244723e+000, + 6.839976e-001, + // albedo 0, turbidity 7 + -1.555684e+000, + -6.962113e-001, + -4.647983e+001, + 4.674270e+001, + -5.034895e-003, + 4.755090e-001, + -9.502561e-007, + 2.626569e+000, + 5.056194e-001, + -1.998288e+000, + -1.124720e+000, + -1.629586e+000, + 2.187993e+000, + -8.284384e-003, + 3.845258e-001, + 5.726240e-002, + 1.185644e+000, + 4.255812e-001, + -1.032570e+000, + -2.513850e-001, + -3.721112e+000, + 3.506967e+000, + -2.186561e-002, + 9.436049e-001, + -2.451412e-001, + 4.725724e+000, + 1.039256e+000, + -8.597532e-001, + 9.073332e-002, + -2.553741e+000, + 1.993237e+000, + -4.390891e-002, + -2.046928e-001, + 5.515623e-001, + 1.909127e+000, + 3.948212e-001, + -1.210482e+000, + -4.477622e-001, + -2.267805e-001, + 1.219488e+000, + 1.336186e-001, + 6.866897e-001, + 2.808997e-002, + 1.600403e+000, + 7.816409e-001, + -1.078168e+000, + -2.699261e-001, + 2.537282e-001, + 3.820684e-001, + -4.425103e-001, + 5.298235e-001, + 2.185217e-001, + 1.728679e+000, + 6.882743e-001, + // albedo 0, turbidity 8 + -1.697968e+000, + -8.391488e-001, + -5.790105e+001, + 5.814120e+001, + -3.404760e-003, + 4.265140e-001, + -1.796301e-006, + 2.368442e+000, + 5.324429e-001, + -2.141552e+000, + -1.172230e+000, + 1.677872e+001, + -1.641470e+001, + -5.732425e-003, + 2.002199e-001, + 6.841834e-002, + 1.485338e+000, + 3.215763e-001, + -1.442946e+000, + -7.264245e-001, + -9.503706e+000, + 9.650462e+000, + -2.120995e-002, + 1.419263e+000, + -2.893098e-001, + 3.860731e+000, + 1.120857e+000, + -5.696752e-001, + 3.411279e-001, + -2.931035e-001, + -6.512552e-001, + -1.068437e-001, + -1.085661e+000, + 6.107549e-001, + 1.459503e+000, + 3.210336e-001, + -1.313839e+000, + -5.921371e-001, + -2.332222e-001, + 1.648196e+000, + 2.492787e-001, + 1.381033e+000, + -1.993392e-002, + 9.812560e-001, + 8.316329e-001, + -1.087464e+000, + -3.195534e-001, + 2.902095e-001, + 3.383709e-001, + -8.798482e-001, + 1.494668e-002, + 2.529703e-001, + 1.452644e+000, + 6.693870e-001, + // albedo 0, turbidity 9 + -2.068582e+000, + -1.118605e+000, + -5.081598e+001, + 5.097486e+001, + -3.280669e-003, + 4.067371e-001, + -2.544951e-006, + 2.179497e+000, + 5.778017e-001, + -1.744693e+000, + -8.537207e-001, + 2.234361e+001, + -2.208318e+001, + -5.932616e-003, + 1.035049e-001, + 5.742772e-002, + 1.977880e+000, + 2.124846e-001, + -3.287515e+000, + -2.140268e+000, + -1.249566e+001, + 1.240091e+001, + -2.409349e-002, + 1.397821e+000, + -2.371627e-001, + 2.771192e+000, + 1.170496e+000, + 5.502311e-001, + 1.046630e+000, + 2.193517e+000, + -2.220400e+000, + -1.064394e-001, + -1.017926e+000, + 4.795457e-001, + 1.030644e+000, + 3.177516e-001, + -1.719734e+000, + -9.536198e-001, + -6.586821e-001, + 1.386361e+000, + -2.513065e-002, + 1.187011e+000, + 6.542539e-002, + 5.296055e-001, + 8.082660e-001, + -1.005700e+000, + -3.028096e-001, + 4.470957e-002, + 1.007760e+000, + -8.119016e-001, + 3.153338e-002, + 2.311321e-001, + 1.182208e+000, + 6.824758e-001, + // albedo 0, turbidity 10 + -2.728867e+000, + -1.580388e+000, + -3.079627e+001, + 3.092586e+001, + -4.197673e-003, + 3.154759e-001, + -3.897675e-006, + 1.920567e+000, + 6.664791e-001, + -1.322495e+000, + -7.249275e-001, + 1.477660e+001, + -1.468154e+001, + -9.044857e-003, + 5.624314e-002, + 6.498392e-002, + 2.047389e+000, + 6.367540e-002, + -6.102376e+000, + -3.473018e+000, + -9.926071e+000, + 9.637797e+000, + -1.097909e-002, + 1.103498e+000, + -2.424521e-001, + 2.520748e+000, + 1.240260e+000, + 1.351796e+000, + 1.018588e+000, + 2.009081e+000, + -1.333394e+000, + -1.979125e-001, + -3.318292e-001, + 4.476624e-001, + 9.095235e-001, + 2.955611e-001, + -1.774467e+000, + -1.079880e+000, + -8.084680e-002, + 2.577697e-001, + -1.149295e-001, + 4.975303e-001, + 2.931611e-003, + -3.803171e-001, + 8.002794e-001, + -9.898401e-001, + -2.542513e-001, + -7.530911e-002, + 1.870355e+000, + -1.521918e+000, + 2.405164e-001, + 2.964615e-001, + 1.334800e+000, + 6.789053e-001, + // albedo 1, turbidity 1 + -1.279730e+000, + -4.290674e-001, + -4.277972e+001, + 4.343305e+001, + -6.541826e-003, + 4.945086e-001, + 1.425338e-002, + 2.685244e+000, + 5.011313e-001, + -1.449506e+000, + -5.766374e-001, + -1.688496e+001, + 1.781118e+001, + -1.121649e-002, + 3.545020e-001, + 2.287338e-002, + 1.904281e+000, + 4.936998e-001, + -1.021980e+000, + -1.897574e-001, + 2.482462e+000, + -2.941725e+000, + -1.570448e-002, + 7.532578e-001, + -4.256800e-002, + 5.239660e+000, + 4.983116e-001, + -1.162608e+000, + -3.428049e-001, + 3.974358e+000, + -1.527935e+000, + -3.919201e-002, + 8.758593e-001, + 7.291363e-002, + -3.455257e+000, + 8.007426e-001, + -9.929985e-001, + -8.712006e-002, + -7.397313e-001, + 1.348372e+000, + 9.511685e-002, + 3.233584e-001, + -7.549148e-002, + 5.806452e+000, + 4.990042e-001, + -1.084996e+000, + -1.739767e-001, + 1.580475e-001, + 9.088180e-001, + 6.871433e-002, + 5.933079e-001, + 1.188921e-001, + 3.074079e+000, + 4.999327e-001, + // albedo 1, turbidity 2 + -1.317009e+000, + -4.661946e-001, + -4.255347e+001, + 4.312782e+001, + -5.727235e-003, + 4.285447e-001, + 2.189854e-002, + 2.608310e+000, + 5.190700e-001, + -1.469236e+000, + -6.282139e-001, + -1.241404e+001, + 1.348765e+001, + -1.204770e-002, + 5.070285e-001, + -7.280216e-004, + 1.491533e+000, + 3.635064e-001, + -9.713808e-001, + -8.138038e-002, + 3.709854e-001, + -1.041174e+000, + -1.814075e-002, + 5.060860e-001, + -2.053756e-002, + 6.161431e+000, + 1.093736e+000, + -1.159057e+000, + -3.698074e-001, + 2.711209e+000, + -6.006479e-001, + -4.896926e-002, + 9.273957e-001, + 1.137712e-001, + -3.496828e+000, + 2.867109e-001, + -1.011601e+000, + -8.201890e-002, + 2.105725e-001, + 4.597520e-001, + 1.478925e-001, + 2.138940e-001, + -5.660670e-002, + 6.057755e+000, + 7.859121e-001, + -1.078020e+000, + -1.811580e-001, + 1.646622e-001, + 8.348426e-001, + 1.149064e-001, + 4.985738e-001, + 1.376605e-001, + 2.746607e+000, + 4.999626e-001, + // albedo 1, turbidity 3 + -1.325672e+000, + -4.769313e-001, + -4.111215e+001, + 4.168293e+001, + -6.274997e-003, + 4.649469e-001, + 1.119411e-002, + 2.631267e+000, + 5.234546e-001, + -1.619391e+000, + -8.000253e-001, + -1.534098e+001, + 1.632706e+001, + -1.012023e-002, + 4.242255e-001, + 2.931597e-002, + 8.925807e-001, + 3.314765e-001, + -7.356979e-001, + 1.368406e-001, + 2.972579e+000, + -3.535359e+000, + -1.318948e-002, + 4.607620e-001, + -7.182778e-002, + 6.254100e+000, + 1.236299e+000, + -1.316217e+000, + -4.194427e-001, + 3.489902e-002, + 1.289849e+000, + -4.755960e-002, + 1.138222e+000, + 1.975992e-001, + -8.991542e-001, + 2.290572e-001, + -9.502188e-001, + -1.172703e-001, + 1.405202e+000, + -3.061919e-001, + 1.058772e-001, + -3.760592e-001, + -1.983179e-002, + 3.562353e+000, + 7.895959e-001, + -1.100117e+000, + -1.900567e-001, + 4.925030e-001, + 5.250225e-001, + 1.576804e-001, + 1.042701e+000, + 7.330743e-002, + 2.796064e+000, + 6.749783e-001, + // albedo 1, turbidity 4 + -1.354183e+000, + -5.130625e-001, + -4.219268e+001, + 4.271772e+001, + -5.365373e-003, + 4.136743e-001, + 1.235172e-002, + 2.520122e+000, + 5.187269e-001, + -1.741434e+000, + -9.589761e-001, + -8.230339e+000, + 9.296799e+000, + -9.600162e-003, + 4.994969e-001, + 2.955452e-002, + 3.667099e-001, + 3.526999e-001, + -6.917347e-001, + 2.154887e-001, + -8.760264e-001, + 2.334121e-001, + -1.909621e-002, + 4.748033e-001, + -1.138514e-001, + 6.515360e+000, + 1.225097e+000, + -1.293189e+000, + -4.218700e-001, + 1.620952e+000, + -7.858597e-001, + -3.769410e-002, + 6.636786e-001, + 3.364945e-001, + -5.341017e-001, + 2.128347e-001, + -9.735521e-001, + -1.325495e-001, + 1.007517e+000, + 2.598258e-001, + 6.762169e-002, + 1.421018e-003, + -6.915987e-002, + 3.185897e+000, + 8.641956e-001, + -1.094800e+000, + -1.962062e-001, + 5.755591e-001, + 2.906259e-001, + 2.625748e-001, + 7.644049e-001, + 1.347492e-001, + 2.677126e+000, + 6.465460e-001, + // albedo 1, turbidity 5 + -1.393063e+000, + -5.578338e-001, + -4.185249e+001, + 4.233504e+001, + -5.435640e-003, + 4.743765e-001, + 7.422477e-003, + 2.442801e+000, + 5.211707e-001, + -1.939487e+000, + -1.128509e+000, + -8.974257e+000, + 9.978383e+000, + -7.965597e-003, + 2.948830e-001, + 4.436763e-002, + 2.839868e-001, + 3.440424e-001, + -6.011562e-001, + 2.354877e-001, + -3.079820e+000, + 2.585094e+000, + -2.002701e-002, + 7.793909e-001, + -1.598414e-001, + 5.834678e+000, + 1.202856e+000, + -1.315676e+000, + -3.903446e-001, + 1.701900e+000, + -1.304609e+000, + -1.045121e-002, + 2.747707e-001, + 4.143967e-001, + 3.197102e-001, + 2.637580e-001, + -9.618628e-001, + -1.625841e-001, + 1.187138e+000, + 1.497802e-001, + -5.590954e-006, + 3.178475e-002, + -4.153145e-002, + 2.496096e+000, + 8.195082e-001, + -1.111554e+000, + -2.365546e-001, + 7.831875e-001, + 2.018684e-001, + 2.074369e-001, + 7.395978e-001, + 1.225730e-001, + 1.876478e+000, + 6.821167e-001, + // albedo 1, turbidity 6 + -1.427879e+000, + -5.994879e-001, + -3.531016e+001, + 3.581581e+001, + -6.431497e-003, + 4.554192e-001, + 7.348731e-004, + 2.334619e+000, + 5.233377e-001, + -1.998177e+000, + -1.206633e+000, + -2.146510e+001, + 2.242237e+001, + -5.857596e-003, + 2.755663e-001, + 6.384795e-002, + 1.358244e-001, + 3.328437e-001, + -6.440630e-001, + 2.058571e-001, + 2.155499e+000, + -2.587968e+000, + -1.840023e-002, + 8.826555e-001, + -2.222452e-001, + 5.847073e+000, + 1.228387e+000, + -1.229071e+000, + -3.360441e-001, + -3.429599e-001, + 6.179469e-001, + 2.029610e-003, + 8.899319e-002, + 5.041624e-001, + 1.882964e-001, + 2.252040e-001, + -1.022905e+000, + -2.101621e-001, + 1.915689e+000, + -6.498794e-001, + -3.463651e-002, + 8.954605e-002, + -6.797854e-002, + 2.417705e+000, + 8.568618e-001, + -1.082538e+000, + -2.007723e-001, + 4.731009e-001, + 4.077267e-001, + 1.324289e-001, + 6.514880e-001, + 1.702912e-001, + 2.309383e+000, + 6.600895e-001, + // albedo 1, turbidity 7 + -1.472139e+000, + -6.499815e-001, + -3.428465e+001, + 3.469659e+001, + -5.747023e-003, + 4.174167e-001, + 1.688597e-003, + 2.323046e+000, + 5.395191e-001, + -2.161176e+000, + -1.353089e+000, + -2.226827e+001, + 2.329138e+001, + -5.583808e-003, + 2.364793e-001, + 6.096656e-002, + 1.944666e-003, + 2.861624e-001, + -6.593044e-001, + 1.393558e-001, + 4.698373e+000, + -5.193883e+000, + -1.998390e-002, + 1.095635e+000, + -2.391254e-001, + 5.598103e+000, + 1.236193e+000, + -1.195717e+000, + -2.972715e-001, + 4.648953e-002, + 3.024588e-001, + 5.003313e-003, + -3.754741e-001, + 5.247265e-001, + -1.381312e-001, + 2.493896e-001, + -1.020139e+000, + -2.253524e-001, + 3.548437e-001, + 7.030485e-001, + -2.107076e-002, + 4.581395e-001, + -3.243757e-002, + 2.453259e+000, + 8.323623e-001, + -1.098770e+000, + -2.435780e-001, + 8.761614e-001, + 1.941613e-001, + -1.990692e-001, + 3.761139e-001, + 1.657412e-001, + 1.590503e+000, + 6.741417e-001, + // albedo 1, turbidity 8 + -1.648007e+000, + -8.205121e-001, + -4.435106e+001, + 4.479801e+001, + -4.181353e-003, + 3.854830e-001, + -1.842385e-006, + 2.000281e+000, + 5.518363e-001, + -2.140986e+000, + -1.282239e+000, + -3.979213e+000, + 4.672459e+000, + -5.008582e-003, + 2.421920e-001, + 6.253602e-002, + 6.612713e-001, + 2.555851e-001, + -1.300502e+000, + -5.137898e-001, + 5.179821e-001, + -4.032341e-001, + -2.066785e-002, + 1.087929e+000, + -2.615309e-001, + 4.225887e+000, + 1.229237e+000, + -6.963340e-001, + 9.241060e-002, + 6.936356e-002, + -3.588571e-001, + -5.461843e-002, + -5.616643e-001, + 5.484166e-001, + -4.776267e-002, + 2.414935e-001, + -1.233179e+000, + -4.325498e-001, + 6.479813e-001, + 8.368356e-001, + 2.458875e-001, + 6.464752e-001, + -2.897097e-002, + 1.561773e+000, + 8.518598e-001, + -1.051023e+000, + -2.533690e-001, + 1.004294e+000, + 3.028083e-001, + -1.520108e+000, + 1.607013e-001, + 1.619975e-001, + 1.131094e+000, + 6.706655e-001, + // albedo 1, turbidity 9 + -1.948249e+000, + -1.097383e+000, + -4.453697e+001, + 4.494902e+001, + -3.579939e-003, + 3.491605e-001, + -2.500253e-006, + 1.740442e+000, + 6.188022e-001, + -2.154253e+000, + -1.209559e+000, + 4.144894e+000, + -3.562411e+000, + -5.638843e-003, + 1.067169e-001, + 7.594858e-002, + 1.005280e+000, + 1.072543e-001, + -2.513259e+000, + -1.507208e+000, + -1.602979e+000, + 1.404154e+000, + -5.560750e-003, + 1.240490e+000, + -2.852117e-001, + 3.485252e+000, + 1.349321e+000, + -7.832214e-002, + 3.655626e-001, + 3.856288e-001, + 6.867894e-001, + -1.609523e-001, + -6.704306e-001, + 5.357301e-001, + -6.457935e-001, + 1.479503e-001, + -1.354784e+000, + -5.454375e-001, + 8.797469e-001, + -1.466514e+000, + 7.134420e-001, + 5.934903e-001, + -2.911178e-002, + 8.643737e-001, + 9.030724e-001, + -1.048324e+000, + -2.738736e-001, + 8.783074e-001, + 3.246188e+000, + -4.435369e+000, + 1.251791e-001, + 1.783486e-001, + 1.064657e+000, + 6.522878e-001, + // albedo 1, turbidity 10 + -2.770408e+000, + -1.618911e+000, + -2.504031e+001, + 2.531674e+001, + -4.239279e-003, + 3.241013e-001, + -3.764484e-006, + 1.586843e+000, + 7.035906e-001, + -1.913500e+000, + -1.144014e+000, + -1.080587e+001, + 1.153677e+001, + -1.003197e-002, + 1.577515e-001, + 5.217789e-002, + 1.225278e+000, + 5.172771e-003, + -5.293208e+000, + -2.876463e+000, + 2.087053e+000, + -3.201552e+000, + 3.892964e-003, + 5.323930e-001, + -2.034512e-001, + 2.617760e+000, + 1.273597e+000, + 9.060340e-001, + 3.773409e-001, + -6.399945e-001, + 3.213979e+000, + -9.112172e-002, + 6.494055e-001, + 3.953280e-001, + 5.047796e-001, + 2.998695e-001, + -1.482179e+000, + -6.778310e-001, + 1.161775e+000, + -3.004872e+000, + 4.774797e-001, + -4.969248e-001, + -3.512074e-003, + -1.307190e+000, + 7.927378e-001, + -9.863181e-001, + -1.803364e-001, + 5.810824e-001, + 4.580570e+000, + -3.863454e+000, + 5.328174e-001, + 2.272821e-001, + 1.771114e+000, + 6.791814e-001, +}; + +double datasetXYZRad3[] = +{ + // albedo 0, turbidity 1 + 1.168084e+000, + 2.156455e+000, + -3.980314e+000, + 1.989302e+001, + 1.328335e+001, + 1.435621e+001, + // albedo 0, turbidity 2 + 1.135488e+000, + 2.294701e+000, + -4.585886e+000, + 2.090208e+001, + 1.347840e+001, + 1.467658e+001, + // albedo 0, turbidity 3 + 1.107408e+000, + 2.382765e+000, + -5.112357e+000, + 2.147823e+001, + 1.493128e+001, + 1.460882e+001, + // albedo 0, turbidity 4 + 1.054193e+000, + 2.592891e+000, + -6.115000e+000, + 2.268967e+001, + 1.635672e+001, + 1.518999e+001, + // albedo 0, turbidity 5 + 1.006946e+000, + 2.705420e+000, + -6.698930e+000, + 2.291830e+001, + 1.834324e+001, + 1.570651e+001, + // albedo 0, turbidity 6 + 9.794044e-001, + 2.742440e+000, + -6.805283e+000, + 2.225271e+001, + 2.050797e+001, + 1.563130e+001, + // albedo 0, turbidity 7 + 9.413577e-001, + 2.722009e+000, + -6.760707e+000, + 2.098242e+001, + 2.342588e+001, + 1.605011e+001, + // albedo 0, turbidity 8 + 8.917923e-001, + 2.592780e+000, + -6.152635e+000, + 1.774141e+001, + 2.858324e+001, + 1.657910e+001, + // albedo 0, turbidity 9 + 8.288391e-001, + 2.153434e+000, + -4.118327e+000, + 1.078118e+001, + 3.681710e+001, + 1.738139e+001, + // albedo 0, turbidity 10 + 7.623528e-001, + 1.418187e+000, + -8.845235e-001, + 7.590129e-001, + 4.629859e+001, + 1.921657e+001, + // albedo 1, turbidity 1 + 1.352858e+000, + 2.048862e+000, + -2.053393e+000, + 1.405874e+001, + 3.045344e+001, + 3.044430e+001, + // albedo 1, turbidity 2 + 1.330497e+000, + 2.126497e+000, + -2.466296e+000, + 1.467559e+001, + 3.090738e+001, + 3.069707e+001, + // albedo 1, turbidity 3 + 1.286344e+000, + 2.200436e+000, + -2.877228e+000, + 1.492701e+001, + 3.236288e+001, + 3.077223e+001, + // albedo 1, turbidity 4 + 1.234428e+000, + 2.289628e+000, + -3.404699e+000, + 1.499436e+001, + 3.468390e+001, + 3.084842e+001, + // albedo 1, turbidity 5 + 1.178660e+000, + 2.306071e+000, + -3.549159e+000, + 1.411006e+001, + 3.754188e+001, + 3.079730e+001, + // albedo 1, turbidity 6 + 1.151366e+000, + 2.333005e+000, + -3.728627e+000, + 1.363374e+001, + 3.905894e+001, + 3.092599e+001, + // albedo 1, turbidity 7 + 1.101593e+000, + 2.299422e+000, + -3.565787e+000, + 1.196745e+001, + 4.188472e+001, + 3.102755e+001, + // albedo 1, turbidity 8 + 1.038322e+000, + 2.083539e+000, + -2.649585e+000, + 8.037389e+000, + 4.700869e+001, + 3.065948e+001, + // albedo 1, turbidity 9 + 9.596146e-001, + 1.671470e+000, + -8.751538e-001, + 1.679772e+000, + 5.345784e+001, + 3.054520e+001, + // albedo 1, turbidity 10 + 8.640731e-001, + 9.858301e-001, + 1.854956e+000, + -6.798097e+000, + 5.936468e+001, + 3.110255e+001, +}; + + + +double* datasetsXYZ[] = +{ + datasetXYZ1, + datasetXYZ2, + datasetXYZ3 +}; + +double* datasetsXYZRad[] = +{ + datasetXYZRad1, + datasetXYZRad2, + datasetXYZRad3 +}; diff --git a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/ArHosekSkyModelData_RGB.data b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/ArHosekSkyModelData_RGB.data index 3cd1b90a..247bfe2b 100644 --- a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/ArHosekSkyModelData_RGB.data +++ b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/ArHosekSkyModelData_RGB.data @@ -1,3861 +1,3861 @@ -/* -This source is published under the following 3-clause BSD license. - -Copyright (c) 2012 - 2013, Lukas Hosek and Alexander Wilkie -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * None of the names of the contributors may be used to endorse or promote - products derived from this software without specific prior written - permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - - -/* ============================================================================ - -This file is part of a sample implementation of the analytical skylight and -solar radiance models presented in the SIGGRAPH 2012 paper - - - "An Analytic Model for Full Spectral Sky-Dome Radiance" - -and the 2013 IEEE CG&A paper - - "Adding a Solar Radiance Function to the Hosek Skylight Model" - - both by - - Lukas Hosek and Alexander Wilkie - Charles University in Prague, Czech Republic - - - Version: 1.4a, February 22nd, 2013 - -Version history: - -1.4a February 22nd, 2013 - Removed unnecessary and counter-intuitive solar radius parameters - from the interface of the colourspace sky dome initialisation functions. - -1.4 February 11th, 2013 - Fixed a bug which caused the relative brightness of the solar disc - and the sky dome to be off by a factor of about 6. The sun was too - bright: this affected both normal and alien sun scenarios. The - coefficients of the solar radiance function were changed to fix this. - -1.3 January 21st, 2013 (not released to the public) - Added support for solar discs that are not exactly the same size as - the terrestrial sun. Also added support for suns with a different - emission spectrum ("Alien World" functionality). - -1.2a December 18th, 2012 - Fixed a mistake and some inaccuracies in the solar radiance function - explanations found in ArHosekSkyModel.h. The actual source code is - unchanged compared to version 1.2. - -1.2 December 17th, 2012 - Native RGB data and a solar radiance function that matches the turbidity - conditions were added. - -1.1 September 2012 - The coefficients of the spectral model are now scaled so that the output - is given in physical units: W / (m^-2 * sr * nm). Also, the output of the - XYZ model is now no longer scaled to the range [0...1]. Instead, it is - the result of a simple conversion from spectral data via the CIE 2 degree - standard observer matching functions. Therefore, after multiplication - with 683 lm / W, the Y channel now corresponds to luminance in lm. - -1.0 May 11th, 2012 - Initial release. - - -Please visit http://cgg.mff.cuni.cz/projects/SkylightModelling/ to check if -an updated version of this code has been published! - -============================================================================ */ - - -/* - -This file contains the coefficient data for the RGB colour space version of -the model. - -*/ - -// uses Aug 23 dataset - -double datasetRGB1[] = -{ - // albedo 0, turbidity 1 - -1.099459e+000, - -1.335146e-001, - -4.083223e+000, - 5.919603e+000, - -1.104166e-001, - 1.600158e+000, - -1.326538e-006, - 4.917807e+000, - 5.127716e-001, - -1.169858e+000, - -1.832793e-001, - 9.694744e-001, - 9.495762e-002, - -4.738918e-002, - 2.194171e-001, - 1.095749e-001, - 3.603604e+000, - 3.815119e-001, - -9.665225e-001, - -1.403888e-001, - 5.194457e+000, - -1.107607e+000, - -8.135181e-001, - 4.969661e+000, - -2.300508e-001, - -2.489350e+000, - 1.279158e+000, - -1.292508e+000, - -1.299552e-001, - -2.071404e+000, - -4.752482e-002, - 1.215598e+000, - -1.904179e+000, - 3.027985e-001, - 8.707768e+000, - 6.332446e-002, - -9.264666e-001, - -1.696780e-001, - 4.574070e+000, - -4.232936e-001, - -7.575833e+000, - 5.079755e+000, - -2.576343e-001, - -4.506805e+000, - 6.908129e-001, - -1.139072e+000, - -1.796056e-001, - 1.923311e+000, - 6.788529e+000, - -2.364389e+000, - -1.064041e+000, - 1.717010e-001, - 1.534681e+000, - 5.015810e-001, - // albedo 0, turbidity 2 - -1.107257e+000, - -1.384411e-001, - -4.285744e+000, - 5.713157e+000, - -1.015992e-001, - 1.372638e+000, - 6.555893e-002, - 5.127514e+000, - 6.550471e-001, - -1.187337e+000, - -1.969013e-001, - 8.551048e-001, - 5.289708e-002, - -7.626406e-002, - 1.733153e-002, - 1.779454e-001, - 3.801038e+000, - 4.742709e-001, - -9.685321e-001, - -1.553308e-001, - 4.732492e+000, - -1.178935e+000, - -7.852791e-001, - 4.604492e+000, - -2.666518e-001, - -2.367663e+000, - 1.177527e+000, - -1.252817e+000, - -5.129949e-002, - -2.800433e+000, - -1.295992e-002, - 1.308964e+000, - -2.204331e+000, - 7.276011e-001, - 8.699265e+000, - 1.188388e-001, - -9.459509e-001, - -2.322133e-001, - 4.375041e+000, - -1.712018e-001, - -7.451681e+000, - 5.078019e+000, - -4.223538e-001, - -4.595561e+000, - 1.074719e+000, - -1.125092e+000, - -1.796750e-001, - 1.626399e+000, - 6.989743e+000, - -2.406382e+000, - -9.060383e-001, - 2.961611e-001, - 1.337715e+000, - 5.438140e-001, - // albedo 0, turbidity 3 - -1.135338e+000, - -1.716160e-001, - -1.499253e+000, - 2.373491e+000, - -1.654023e-001, - 9.566404e-001, - 1.113453e-001, - 4.528473e+000, - 6.579439e-001, - -1.132780e+000, - -1.456214e-001, - -1.736672e+000, - 1.756589e+000, - -1.087003e-001, - 3.757927e-001, - 2.525070e-001, - 7.178513e+000, - 5.003814e-001, - -1.167176e+000, - -2.927225e-001, - 5.727667e+000, - -3.139244e+000, - -6.425204e-001, - 2.822634e+000, - -1.457812e-001, - -6.787080e+000, - 1.017072e+000, - -1.042529e+000, - 4.110823e-002, - -4.000629e+000, - 4.362364e+000, - 1.090540e+000, - -1.338674e+000, - 8.246964e-001, - 1.095249e+001, - 2.912211e-001, - -1.061598e+000, - -2.096143e-001, - 3.803155e+000, - -7.977069e+000, - -3.637880e+000, - 3.707671e+000, - -1.903128e-001, - -3.397953e+000, - 9.971500e-001, - -1.073560e+000, - -2.077964e-001, - 1.492052e+000, - 1.626322e+001, - -5.015304e+000, - -4.059889e-001, - 2.659782e-001, - 6.395380e-001, - 5.634436e-001, - // albedo 0, turbidity 4 - -1.172794e+000, - -2.111186e-001, - -1.360013e+000, - 1.604080e+000, - -8.473723e-002, - 7.217312e-001, - 1.548030e-001, - 4.257010e+000, - 6.328974e-001, - -1.238374e+000, - -2.670827e-001, - 3.247678e-001, - 5.466311e-001, - -7.425952e-001, - 5.276440e-001, - 2.678026e-002, - 5.484169e+000, - 6.814734e-001, - -1.176923e+000, - -2.574586e-001, - 2.304045e+000, - -2.797678e+000, - 1.464405e+000, - 1.998552e+000, - 2.550559e-001, - -4.199772e+000, - 7.544892e-001, - -1.003284e+000, - 1.943984e-002, - -2.145066e+000, - 1.030924e+001, - -1.525413e+001, - -2.023010e+000, - 5.448699e-001, - 8.159497e+000, - 5.539148e-001, - -1.060017e+000, - -2.037206e-001, - 2.483018e+000, - -4.595459e+000, - 6.526991e+000, - 4.031804e+000, - 1.206513e-001, - -2.586527e+000, - 7.875752e-001, - -1.081141e+000, - -2.123302e-001, - 1.092275e+000, - 2.683841e+000, - -4.166938e+000, - -1.396582e+000, - 4.371205e-001, - 1.030233e+000, - 6.664862e-001, - // albedo 0, turbidity 5 - -1.222392e+000, - -2.651924e-001, - -4.625037e-001, - 3.521964e-001, - 2.148855e-002, - 5.078494e-001, - 1.791590e-001, - 3.852516e+000, - 5.998216e-001, - -1.424610e+000, - -4.710155e-001, - -1.826815e-001, - 1.786277e+000, - -1.952442e+000, - 5.277612e-001, - -1.773629e-002, - 2.415874e+000, - 6.701272e-001, - -1.130655e+000, - -1.358609e-001, - 9.171203e-001, - -4.660394e+000, - 6.251162e+000, - 1.904529e+000, - 2.639668e-001, - 1.856130e+000, - 8.228440e-001, - -9.739015e-001, - -6.674749e-002, - -4.768897e-001, - 1.248589e+001, - -1.994688e+001, - -2.353043e+000, - 5.885575e-001, - 1.287251e+000, - 4.830135e-001, - -1.082178e+000, - -1.974495e-001, - 1.050245e+000, - -4.792855e+000, - 8.663406e+000, - 3.246969e+000, - 1.556731e-001, - 8.117442e-001, - 8.050376e-001, - -1.063354e+000, - -1.727108e-001, - 9.681592e-001, - 2.736077e+000, - -4.969269e+000, - -8.360570e-001, - 5.994612e-001, - 1.024039e+000, - 6.786935e-001, - // albedo 0, turbidity 6 - -1.261936e+000, - -3.053676e-001, - -4.262222e-001, - 4.000196e-001, - -2.059388e-002, - 4.721802e-001, - 1.480028e-001, - 3.505343e+000, - 6.121337e-001, - -1.681088e+000, - -6.971919e-001, - -1.105652e-001, - 7.437426e-001, - -6.594399e-001, - 2.254221e-001, - 8.710195e-002, - 1.263913e+000, - 5.681865e-001, - -9.453001e-001, - 3.460388e-002, - 6.067038e-001, - -1.985128e+000, - 3.457236e+000, - 2.655483e+000, - -1.162354e-002, - 3.304716e+000, - 1.001950e+000, - -1.086609e+000, - -2.029011e-001, - -6.399170e-001, - 6.926885e+000, - -1.512189e+001, - -3.793051e+000, - 9.456120e-001, - 2.222222e-001, - 2.893725e-001, - -1.041259e+000, - -1.388790e-001, - 1.147331e+000, - 6.282086e+000, - 3.679836e+000, - 4.398314e+000, - -1.355232e-001, - 1.031134e+000, - 9.273509e-001, - -1.063473e+000, - -1.916051e-001, - 6.556979e-001, - -3.371891e-003, - -3.699664e+000, - -1.926783e+000, - 7.371154e-001, - 1.179975e+000, - 6.367068e-001, - // albedo 0, turbidity 7 - -1.336390e+000, - -3.778927e-001, - -7.259477e-001, - 2.270247e-001, - 4.627513e-001, - 1.366459e-001, - 2.637347e-001, - 3.292059e+000, - 4.998211e-001, - -2.119878e+000, - -1.055472e+000, - 5.422052e-001, - 7.826648e-001, - -1.286065e+000, - 9.517905e-001, - -1.432358e-001, - -2.379816e-001, - 5.910513e-001, - -7.761432e-001, - 2.124336e-001, - -6.845184e-001, - -9.812342e-001, - 4.347257e+000, - 9.671980e-001, - 3.773150e-001, - 5.789529e+000, - 9.646598e-001, - -1.118734e+000, - -3.513815e-001, - 5.500918e-001, - 9.449627e-001, - -1.262070e+001, - -1.825280e+000, - 4.731260e-001, - -3.326892e+000, - 3.568768e-001, - -1.026437e+000, - -8.257946e-002, - 3.221701e-001, - 1.198372e+001, - 1.555130e+000, - 2.560304e+000, - 1.406465e-001, - 2.912858e+000, - 8.643181e-001, - -1.069949e+000, - -2.029607e-001, - 5.825042e-001, - -2.398595e-003, - -3.278335e+000, - -1.349882e+000, - 7.208433e-001, - 8.505164e-001, - 6.625391e-001, - // albedo 0, turbidity 8 - -1.392309e+000, - -4.454945e-001, - -5.664000e-001, - 6.283393e-001, - -3.761727e-001, - 6.949802e-001, - 7.748178e-002, - 3.192797e+000, - 5.968661e-001, - -2.713405e+000, - -1.395112e+000, - 2.029230e-001, - 1.877272e-001, - -3.715859e-001, - -1.652929e-001, - 2.385861e-001, - -4.150768e-001, - 1.375467e-001, - -9.588644e-001, - 2.433900e-002, - -1.527493e+000, - -9.632874e-001, - 5.496269e+000, - 1.094931e+000, - 2.004044e-001, - 6.084554e+000, - 1.369604e+000, - -8.028546e-001, - -2.473563e-001, - 1.617898e+000, - 2.073591e+000, - -1.149446e+001, - -8.394131e-001, - 2.726847e-001, - -4.634538e+000, - 1.367293e-001, - -1.198326e+000, - -1.804865e-001, - -3.565414e-001, - 4.073200e+000, - 1.662086e+000, - 1.239770e+000, - 3.367978e-001, - 2.997402e+000, - 9.360383e-001, - -1.013531e+000, - -1.859060e-001, - 5.799857e-001, - 1.331883e+001, - -4.346873e+000, - -1.113820e+000, - 5.275714e-001, - 8.045177e-001, - 6.496373e-001, - // albedo 0, turbidity 9 - -1.530103e+000, - -6.107468e-001, - -3.841771e-001, - 1.881508e+000, - -1.464807e+000, - 6.654690e-001, - -5.950797e-006, - 2.738912e+000, - 8.101012e-001, - -2.415469e+000, - -1.057499e+000, - -4.161968e-001, - -2.357548e+000, - 6.300296e-001, - 6.224915e-001, - 1.545048e-002, - 2.038561e+000, - -1.339415e-001, - -3.096796e+000, - -1.465688e+000, - -1.199232e+000, - 4.567061e+000, - 3.260980e+000, - -9.794907e-001, - 8.950491e-001, - 2.049235e+000, - 1.331015e+000, - 2.713904e-001, - 2.852852e-001, - 1.202090e+000, - -8.206784e+000, - -5.805762e+000, - 1.804431e+000, - -6.090648e-001, - -1.990902e+000, - 3.288858e-001, - -1.456580e+000, - -3.455960e-001, - -6.409257e-002, - 1.667697e+001, - -2.311094e+000, - -9.771104e-001, - 6.759863e-001, - 1.245136e+000, - 7.911932e-001, - -9.860389e-001, - -2.099564e-001, - 2.946650e-001, - -3.547800e-003, - -2.268313e+000, - -6.205647e-002, - 4.705185e-001, - 8.657995e-001, - 6.856284e-001, - // albedo 0, turbidity 10 - -1.971736e+000, - -9.414047e-001, - -3.400557e-001, - 1.468763e+000, - -1.474284e+000, - 5.501062e-001, - -1.109750e-005, - 2.356370e+000, - 9.001702e-001, - -1.589845e+000, - -7.797079e-001, - -5.582240e-001, - -8.137376e-001, - 5.846617e-001, - 1.129459e-001, - -2.658005e-002, - 2.707248e+000, - -2.112486e-001, - -6.940173e+000, - -2.823963e+000, - -1.620848e+000, - 1.090696e+000, - 2.391730e+000, - 1.370047e+000, - 5.890462e-001, - 1.728400e+000, - 1.331253e+000, - 1.293144e+000, - -1.919778e-003, - 1.644206e+000, - -8.666967e-001, - -7.161953e+000, - -1.385018e+000, - -1.505374e-001, - -1.388643e+000, - 2.530122e-001, - -1.488880e+000, - -2.495496e-001, - -2.377137e-001, - 1.167714e+001, - -8.617124e-001, - 1.053828e+000, - 1.992744e-001, - 3.633564e-001, - 8.553304e-001, - -1.060891e+000, - -4.035829e-001, - 2.823207e-001, - -2.369798e-003, - -1.876577e+000, - -5.950265e-001, - 4.241017e-001, - 3.140802e-001, - 6.631669e-001, - // albedo 1, turbidity 1 - -1.101204e+000, - -1.351353e-001, - -4.030882e+000, - 6.096353e+000, - -1.148599e-001, - 1.606507e+000, - -1.555474e-006, - 4.436084e+000, - 5.973715e-001, - -1.154597e+000, - -1.923378e-001, - 8.512132e-001, - 2.934895e-001, - -6.522777e-002, - 1.389077e-001, - 9.091469e-002, - 3.133307e+000, - 2.108541e-001, - -1.031588e+000, - -1.546804e-001, - 5.266214e+000, - -9.491390e-001, - -7.184867e-001, - 4.875626e+000, - -1.911907e-001, - -2.865642e+000, - 1.087895e+000, - -1.159454e+000, - -9.546699e-002, - -1.508146e+000, - -2.031411e-002, - 1.040653e+000, - -2.333508e+000, - 2.540592e-001, - 8.594981e+000, - 9.316770e-002, - -1.035940e+000, - -2.021151e-001, - 4.719343e+000, - -9.019318e-001, - -7.858046e+000, - 3.901234e+000, - -2.233137e-001, - -4.344739e+000, - 6.550733e-001, - -1.096669e+000, - -1.558196e-001, - 2.057553e+000, - 6.274495e+000, - -2.678352e+000, - -1.814927e+000, - 1.550676e-001, - 1.903276e+000, - 4.998989e-001, - // albedo 1, turbidity 2 - -1.114209e+000, - -1.473531e-001, - -7.602914e+000, - 8.973685e+000, - -4.980074e-002, - 1.289198e+000, - 8.366906e-002, - 4.557987e+000, - 6.118757e-001, - -1.149397e+000, - -1.981628e-001, - 4.914096e+000, - -3.498986e+000, - -6.257090e-002, - 1.667401e-001, - 1.048980e-001, - 2.284689e+000, - 5.935965e-001, - -1.056121e+000, - -1.456172e-001, - 4.272656e-001, - 2.912649e+000, - -5.501745e-001, - 4.406542e+000, - -1.387680e-001, - 1.245555e+000, - 9.733011e-001, - -1.125047e+000, - -4.003662e-002, - 1.058457e+000, - -3.462236e+000, - 4.395278e-001, - -2.395805e+000, - 5.177589e-001, - 4.866247e+000, - 4.253189e-001, - -1.051444e+000, - -2.804541e-001, - 3.364668e+000, - 3.293787e+000, - -1.015741e+001, - 3.807407e+000, - -3.592377e-001, - -3.367415e+000, - 7.900825e-001, - -1.093847e+000, - -1.436965e-001, - 2.384780e+000, - 5.787070e+000, - -2.445987e+000, - -1.311171e+000, - 2.326563e-001, - 1.158439e+000, - 5.555416e-001, - // albedo 1, turbidity 3 - -1.134824e+000, - -1.680468e-001, - -3.325620e+000, - 4.458596e+000, - -1.135063e-001, - 1.104500e+000, - 7.794544e-002, - 4.609952e+000, - 6.854854e-001, - -1.143017e+000, - -1.565926e-001, - 3.014687e-001, - -1.763027e-001, - -3.557925e-002, - -2.342406e-001, - 2.528705e-001, - 5.884085e+000, - 4.750602e-001, - -1.136801e+000, - -2.907502e-001, - 3.682423e+000, - -4.061202e-001, - -8.728159e-001, - 4.001510e+000, - -1.522202e-001, - -5.528713e+000, - 1.044847e+000, - -1.063652e+000, - 7.808107e-002, - -1.983678e+000, - 3.648078e-001, - 2.102276e+000, - -3.065050e+000, - 8.431951e-001, - 1.038830e+001, - 2.662834e-001, - -1.061015e+000, - -2.859814e-001, - 4.223615e+000, - -2.290138e+000, - -8.314010e+000, - 4.405718e+000, - -4.613627e-001, - -4.502910e+000, - 1.008383e+000, - -1.106302e+000, - -1.697123e-001, - 2.087196e+000, - 8.238929e+000, - -2.992416e+000, - -1.821776e+000, - 3.434859e-001, - 7.755179e-001, - 5.341190e-001, - // albedo 1, turbidity 4 - -1.171110e+000, - -2.106304e-001, - -1.614361e+000, - 2.378103e+000, - -1.625969e-001, - 8.504483e-001, - 1.059312e-001, - 4.046256e+000, - 6.618227e-001, - -1.200480e+000, - -2.235733e-001, - 1.014390e+000, - -1.174074e+000, - -4.440180e-001, - 2.262406e-001, - 1.665868e-001, - 5.461829e+000, - 5.676310e-001, - -1.223587e+000, - -3.502622e-001, - 1.699106e+000, - 6.724266e-001, - 1.268567e+000, - 2.135102e+000, - 8.039374e-004, - -5.221111e+000, - 9.445690e-001, - -9.452673e-001, - 1.468459e-001, - -1.335034e+000, - 4.346628e+000, - -1.285652e+001, - -1.807046e+000, - 8.175243e-001, - 9.301065e+000, - 3.656798e-001, - -1.134681e+000, - -3.310951e-001, - 3.571244e+000, - -2.208948e+000, - 6.041580e+000, - 3.107577e+000, - -3.112127e-001, - -4.186351e+000, - 9.188333e-001, - -1.083237e+000, - -1.831394e-001, - 2.062654e+000, - 1.385424e+000, - -5.004950e+000, - -1.332669e+000, - 3.627352e-001, - 3.323150e-001, - 6.191181e-001, - // albedo 1, turbidity 5 - -1.211527e+000, - -2.590617e-001, - -1.660874e-001, - 3.627905e-001, - -1.039258e-001, - 4.697924e-001, - 1.671653e-001, - 3.507497e+000, - 6.022506e-001, - -1.433017e+000, - -4.733592e-001, - 1.724445e-001, - 9.953236e-001, - -1.874457e+000, - 4.432099e-001, - 1.715810e-002, - 2.339272e+000, - 6.441470e-001, - -1.084920e+000, - -1.587903e-001, - 8.999585e-001, - -2.537516e+000, - 5.877859e+000, - 2.014554e+000, - 9.689141e-002, - 3.177242e-001, - 9.030399e-001, - -1.008242e+000, - 2.793030e-003, - -3.507469e-001, - 1.028300e+001, - -2.080454e+001, - -2.781026e+000, - 8.995090e-001, - 3.366951e+000, - 3.473867e-001, - -1.103151e+000, - -2.799598e-001, - 2.525791e+000, - -4.255704e+000, - 9.903388e+000, - 3.722668e+000, - -3.603941e-001, - -1.303292e+000, - 9.369454e-001, - -1.102235e+000, - -2.025061e-001, - 2.085660e+000, - 1.686787e+000, - -5.010957e+000, - -1.656458e+000, - 4.584029e-001, - -2.751759e-001, - 6.184162e-001, - // albedo 1, turbidity 6 - -1.256130e+000, - -3.104904e-001, - 1.639350e-001, - 1.315502e-001, - -7.297583e-001, - 4.778480e-001, - 1.259265e-001, - 3.012108e+000, - 6.202728e-001, - -1.620114e+000, - -6.552670e-001, - -2.877157e-001, - 1.094371e+000, - 2.818914e-001, - 3.696830e-001, - 9.428521e-002, - 1.450951e+000, - 5.681308e-001, - -9.686204e-001, - -3.755647e-002, - 1.469980e+000, - -3.103414e+000, - 2.856583e+000, - 1.883209e+000, - -5.746099e-002, - 1.286383e+000, - 1.001751e+000, - -1.089377e+000, - -1.023062e-001, - -1.498891e+000, - 1.066455e+001, - -1.720184e+001, - -2.759314e+000, - 1.061258e+000, - 2.910211e+000, - 2.624701e-001, - -1.044681e+000, - -2.156857e-001, - 3.230136e+000, - -5.863862e-001, - 6.096640e+000, - 3.550019e+000, - -4.255773e-001, - -1.500033e+000, - 9.687696e-001, - -1.133658e+000, - -2.505101e-001, - 1.717840e+000, - 8.480428e-003, - -5.011789e+000, - -1.740989e+000, - 4.983430e-001, - -2.081829e-001, - 6.088641e-001, - // albedo 1, turbidity 7 - -1.335366e+000, - -3.863319e-001, - -5.279971e-001, - 3.638324e-001, - 3.230699e-001, - 8.339707e-002, - 2.483293e-001, - 2.678646e+000, - 4.998346e-001, - -2.004511e+000, - -9.957121e-001, - 1.250807e+000, - 1.625025e-002, - -3.410754e-001, - 7.858244e-001, - -9.506757e-002, - 2.651876e-002, - 5.788643e-001, - -8.714157e-001, - 1.192051e-001, - -8.486879e-001, - -3.702497e-001, - 1.818277e+000, - 1.103427e+000, - 2.454866e-001, - 3.841575e+000, - 9.847350e-001, - -1.042618e+000, - -2.285793e-001, - 3.620175e-001, - 2.983368e+000, - -9.776844e+000, - -1.971587e+000, - 6.691674e-001, - -7.901947e-001, - 3.213200e-001, - -1.099112e+000, - -1.869868e-001, - 2.044065e+000, - 2.062964e+000, - 1.265668e+000, - 2.710130e+000, - -1.099443e-001, - 2.179353e-001, - 9.024108e-001, - -1.106985e+000, - -2.396881e-001, - 1.809807e+000, - 8.523319e+000, - -5.011788e+000, - -1.590086e+000, - 3.248449e-001, - -1.003187e-001, - 6.550606e-001, - // albedo 1, turbidity 8 - -1.421285e+000, - -4.767024e-001, - -3.885004e-001, - 8.274590e-001, - -3.644229e-001, - 6.999513e-001, - 5.196710e-002, - 2.578431e+000, - 6.246310e-001, - -2.611217e+000, - -1.398846e+000, - 4.527425e-001, - -5.932142e-001, - 2.224617e-001, - -5.593581e-001, - 3.389633e-001, - -7.767112e-001, - 6.536004e-002, - -9.881543e-001, - 4.684782e-002, - -8.616613e-001, - 8.799807e-001, - 4.003130e+000, - 1.739543e+000, - -8.098378e-002, - 5.524802e+000, - 1.499673e+000, - -7.544759e-001, - -2.314808e-001, - 8.125770e-001, - -7.724135e-001, - -9.577645e+000, - -1.629433e+000, - 6.790832e-001, - -4.193895e+000, - -2.526624e-002, - -1.273719e+000, - -2.187030e-001, - 1.401798e+000, - 5.231832e+000, - 7.405093e-001, - 1.775166e+000, - -7.269476e-002, - 1.996087e+000, - 1.057450e+000, - -1.046864e+000, - -2.247559e-001, - 1.679449e+000, - 1.140057e+001, - -4.948829e+000, - -1.182664e+000, - 3.241038e-001, - -2.470012e-001, - 6.115900e-001, - // albedo 1, turbidity 9 - -1.514607e+000, - -5.985430e-001, - -1.877610e-001, - 1.756930e+000, - -1.314206e+000, - 6.115810e-001, - -5.970460e-006, - 2.412975e+000, - 8.124304e-001, - -2.308414e+000, - -1.083797e+000, - -1.179959e-001, - -1.728246e+000, - 7.784742e-001, - 5.494505e-001, - 6.203168e-003, - 9.326251e-001, - -1.419518e-001, - -3.230837e+000, - -1.438670e+000, - -9.868286e-001, - 2.974393e+000, - 1.949339e+000, - -6.337857e-001, - 8.160271e-001, - 3.278606e+000, - 1.354373e+000, - 5.149378e-001, - 2.754789e-001, - 1.040965e+000, - -4.501186e+000, - -3.399057e+000, - 9.661861e-001, - -4.736173e-001, - -4.037574e+000, - 2.794847e-001, - -1.621870e+000, - -3.192763e-001, - 8.786242e-001, - 9.785565e+000, - -2.727652e+000, - 1.903691e-002, - 5.521261e-001, - 2.138764e+000, - 8.419871e-001, - -9.951701e-001, - -2.550607e-001, - 1.498952e+000, - -2.737197e-003, - -3.101832e+000, - -5.921329e-001, - 2.864422e-001, - -4.405218e-001, - 6.631410e-001, - // albedo 1, turbidity 10 - -1.902954e+000, - -9.056918e-001, - -2.069570e-001, - 1.191499e+000, - -1.092577e+000, - 5.849556e-001, - -9.649602e-006, - 2.048407e+000, - 9.001527e-001, - -1.271627e+000, - -7.193923e-001, - -1.136606e-002, - -1.167951e-001, - 3.286175e-003, - -5.262827e-002, - -2.473874e-002, - 1.716125e+000, - -2.187133e-001, - -7.647175e+000, - -3.114129e+000, - -1.490128e+000, - -5.266488e-001, - 3.063090e+000, - 1.474262e+000, - 5.481458e-001, - 2.052174e+000, - 1.353089e+000, - 2.191403e+000, - 3.421120e-001, - 1.446510e+000, - 2.170943e+000, - -7.768187e+000, - -1.471207e+000, - -1.456708e-001, - -1.753574e+000, - 2.310576e-001, - -1.932296e+000, - -3.814739e-001, - 6.245422e-001, - 6.748294e+000, - -3.060171e-001, - 1.067747e+000, - 2.500671e-001, - -1.252596e-001, - 8.614611e-001, - -9.471101e-001, - -4.052640e-001, - 1.300174e+000, - -3.951536e-003, - -1.908284e+000, - -5.385721e-001, - 2.133578e-001, - -6.250292e-001, - 6.658012e-001, -}; - -double datasetRGBRad1[] = -{ - // albedo 0, turbidity 1 - 1.962684e+000, - 1.159831e+000, - 4.450588e+000, - 5.079633e+000, - 4.437388e+000, - 4.324573e+000, - // albedo 0, turbidity 2 - 1.946487e+000, - 1.287515e+000, - 3.703696e+000, - 8.782833e+000, - 3.440437e+000, - 5.160333e+000, - // albedo 0, turbidity 3 - 1.882170e+000, - 1.335878e+000, - 2.648641e+000, - 1.358368e+001, - 3.105473e+000, - 5.907387e+000, - // albedo 0, turbidity 4 - 1.738159e+000, - 1.624289e+000, - -8.786695e-003, - 2.118253e+001, - 2.770255e+000, - 7.055672e+000, - // albedo 0, turbidity 5 - 1.571896e+000, - 2.301786e+000, - -4.028545e+000, - 2.966806e+001, - 1.630876e+000, - 8.711031e+000, - // albedo 0, turbidity 6 - 1.475048e+000, - 2.679086e+000, - -6.311315e+000, - 3.377896e+001, - 2.140975e+000, - 9.385283e+000, - // albedo 0, turbidity 7 - 1.326174e+000, - 3.378759e+000, - -9.831444e+000, - 3.942061e+001, - 2.852702e+000, - 1.082542e+001, - // albedo 0, turbidity 8 - 1.153344e+000, - 3.967771e+000, - -1.265181e+001, - 4.195016e+001, - 7.468239e+000, - 1.221350e+001, - // albedo 0, turbidity 9 - 9.746081e-001, - 4.051626e+000, - -1.298454e+001, - 3.754964e+001, - 1.749232e+001, - 1.420619e+001, - // albedo 0, turbidity 10 - 8.448016e-001, - 3.181809e+000, - -8.757338e+000, - 2.197962e+001, - 3.524033e+001, - 1.639549e+001, - // albedo 1, turbidity 1 - 2.029623e+000, - 1.364434e+000, - 4.201529e+000, - 5.415099e+000, - 9.825839e+000, - 1.063328e+001, - // albedo 1, turbidity 2 - 2.023126e+000, - 1.494728e+000, - 3.420413e+000, - 9.072178e+000, - 9.205157e+000, - 1.186639e+001, - // albedo 1, turbidity 3 - 1.956307e+000, - 1.648665e+000, - 2.039712e+000, - 1.430239e+001, - 9.039526e+000, - 1.330453e+001, - // albedo 1, turbidity 4 - 1.825053e+000, - 1.985022e+000, - -8.036307e-001, - 2.202493e+001, - 9.415361e+000, - 1.517659e+001, - // albedo 1, turbidity 5 - 1.650367e+000, - 2.593201e+000, - -4.469328e+000, - 2.969817e+001, - 9.410977e+000, - 1.744850e+001, - // albedo 1, turbidity 6 - 1.555202e+000, - 2.962925e+000, - -6.608170e+000, - 3.329887e+001, - 1.064559e+001, - 1.850816e+001, - // albedo 1, turbidity 7 - 1.412478e+000, - 3.439403e+000, - -9.196616e+000, - 3.685077e+001, - 1.345341e+001, - 2.003128e+001, - // albedo 1, turbidity 8 - 1.252990e+000, - 3.820805e+000, - -1.115338e+001, - 3.721593e+001, - 2.014916e+001, - 2.182320e+001, - // albedo 1, turbidity 9 - 1.091952e+000, - 3.663027e+000, - -1.031330e+001, - 2.978985e+001, - 3.296835e+001, - 2.375450e+001, - // albedo 1, turbidity 10 - 9.501691e-001, - 2.664579e+000, - -5.545167e+000, - 1.281159e+001, - 5.154768e+001, - 2.574284e+001, -}; - -double datasetRGB2[] = -{ - // albedo 0, turbidity 1 - -1.140530e+000, - -1.982747e-001, - -7.512730e+000, - 8.403899e+000, - -5.699038e-002, - 9.015907e-001, - 3.392161e-002, - 4.772522e+000, - 5.111184e-001, - -1.165117e+000, - -1.852955e-001, - 2.963684e+000, - -2.262274e+000, - -1.571683e-001, - 6.339974e-001, - 4.977879e-002, - 7.243307e+000, - 4.220053e-001, - -1.169936e+000, - -3.357429e-001, - 1.911291e+000, - -2.391074e-001, - -4.791643e-001, - 1.446113e+000, - -9.178108e-002, - -4.700239e+000, - 8.096219e-001, - -1.060246e+000, - -1.051633e-001, - 5.013829e-001, - 2.832309e+000, - -3.707855e-001, - 1.523131e+000, - 9.163749e-002, - 5.604183e+000, - 7.208566e-001, - -1.089753e+000, - -2.382167e-001, - 2.360312e+000, - -5.902562e+000, - -8.799894e+000, - 1.377692e+000, - -6.131633e-002, - -1.415472e+000, - 6.124057e-001, - -1.075481e+000, - -1.242391e-001, - 1.425781e+000, - 8.810319e+000, - -2.922646e+000, - 1.486520e+000, - 3.270580e-002, - 3.889783e+000, - 4.999482e-001, - // albedo 0, turbidity 2 - -1.149342e+000, - -2.076337e-001, - -7.446587e+000, - 8.014559e+000, - -4.866227e-002, - 8.203043e-001, - 6.386483e-002, - 4.894198e+000, - 5.452051e-001, - -1.120531e+000, - -1.513311e-001, - 2.735504e+000, - -2.417591e+000, - -1.361114e-001, - 4.296342e-001, - 9.427488e-002, - 8.171403e+000, - 4.102448e-001, - -1.226964e+000, - -3.516378e-001, - 1.308298e+000, - -5.097487e-002, - -4.846783e-001, - 1.654619e+000, - -1.134940e-001, - -3.347854e+000, - 1.131147e+000, - -9.664377e-001, - 2.767589e-002, - 1.658235e-001, - 2.407439e+000, - -1.300304e-001, - 9.170958e-001, - 2.742895e-001, - 6.642633e+000, - 2.550064e-001, - -1.153358e+000, - -3.126223e-001, - 2.078934e+000, - -5.857733e+000, - -8.659848e+000, - 1.758505e+000, - -9.616094e-002, - -1.230863e+000, - 9.663832e-001, - -1.053850e+000, - -1.330743e-001, - 1.481738e+000, - 1.049485e+001, - -3.528854e+000, - 9.142363e-001, - 1.244880e-001, - 2.644615e+000, - 5.001048e-001, - // albedo 0, turbidity 3 - -1.173687e+000, - -2.360362e-001, - -3.741454e+000, - 4.088507e+000, - -7.528205e-002, - 6.645237e-001, - 7.718265e-002, - 4.651220e+000, - 5.586318e-001, - -1.213757e+000, - -2.589561e-001, - 7.132551e-001, - -4.259327e-001, - -1.980821e-001, - 3.627815e-001, - 4.666560e-002, - 5.807984e+000, - 5.847377e-001, - -1.108794e+000, - -2.259870e-001, - 1.574179e+000, - -3.753731e-001, - -5.984743e-001, - 1.659414e+000, - -1.681021e-002, - 6.785219e-001, - 8.647325e-001, - -1.060896e+000, - -1.346690e-002, - -7.529656e-001, - 1.711319e+000, - -9.792435e-001, - 2.022433e-001, - 3.826487e-001, - 5.725157e+000, - 5.290714e-001, - -1.085145e+000, - -2.840715e-001, - 2.088029e+000, - -4.935097e+000, - -9.056542e+000, - 1.976149e+000, - -3.912485e-002, - -8.636064e-001, - 7.452125e-001, - -1.077983e+000, - -1.416633e-001, - 1.100848e+000, - 1.015875e+001, - -2.943712e+000, - 5.255135e-001, - 2.164224e-001, - 2.941143e+000, - 6.699937e-001, - // albedo 0, turbidity 4 - -1.223293e+000, - -2.867444e-001, - -1.624136e+000, - 1.668299e+000, - -9.537589e-002, - 5.015947e-001, - 1.130741e-001, - 4.244812e+000, - 5.082152e-001, - -1.325342e+000, - -4.280991e-001, - 4.705490e-001, - 6.926592e-002, - -4.572587e-001, - 5.344144e-001, - -2.554192e-002, - 3.093939e+000, - 6.639401e-001, - -1.113581e+000, - -1.192133e-001, - 4.011536e-001, - 7.011889e-001, - 2.052842e-001, - 9.880724e-001, - 1.807533e-002, - 4.690160e+000, - 8.576240e-001, - -1.016063e+000, - -1.038138e-001, - -2.280391e-001, - 7.898918e-001, - -1.127333e+001, - 2.074545e-001, - 5.388182e-001, - 1.364263e+000, - 4.660455e-001, - -1.099582e+000, - -2.228607e-001, - 1.332648e+000, - 5.135188e+000, - 1.653152e+000, - 1.417020e+000, - -1.087532e-001, - 1.809275e+000, - 8.080874e-001, - -1.064357e+000, - -1.520775e-001, - 8.207368e-001, - -1.323565e-003, - -5.009523e+000, - 3.946298e-001, - 4.337902e-001, - 2.593198e+000, - 6.719172e-001, - // albedo 0, turbidity 5 - -1.278702e+000, - -3.512866e-001, - -4.511055e-001, - 3.895760e-001, - -2.429672e-001, - 4.270577e-001, - 1.135348e-001, - 3.719130e+000, - 4.998867e-001, - -1.580069e+000, - -7.095475e-001, - -3.198904e-001, - 1.715748e+000, - -1.185915e+000, - 4.523161e-001, - -1.026159e-002, - 7.927188e-001, - 5.538350e-001, - -9.474023e-001, - 1.173703e-001, - 4.881381e-001, - -2.618684e+000, - 3.251661e+000, - 1.213931e+000, - -1.736274e-002, - 8.000768e+000, - 1.025998e+000, - -1.129091e+000, - -3.287694e-001, - -3.524077e-001, - 3.352892e+000, - -1.416073e+001, - -8.485617e-001, - 6.560766e-001, - -2.820937e+000, - 3.111303e-001, - -1.030884e+000, - -1.137581e-001, - 1.109855e+000, - 8.082276e+000, - 1.519214e+000, - 2.112433e+000, - -1.592299e-001, - 3.675905e+000, - 8.703367e-001, - -1.075192e+000, - -1.627166e-001, - 3.514910e-001, - 1.168164e+000, - -4.255822e+000, - -6.015348e-001, - 6.265776e-001, - 2.884818e+000, - 6.548384e-001, - // albedo 0, turbidity 6 - -1.316017e+000, - -3.889652e-001, - -5.030854e-001, - 4.488704e-001, - -3.186800e-001, - 4.570763e-001, - 8.909201e-002, - 3.659274e+000, - 5.011746e-001, - -1.731876e+000, - -8.493806e-001, - 1.194871e-001, - 2.002781e+000, - -2.006547e+000, - 4.872233e-001, - -2.854606e-002, - 2.662137e-001, - 4.611629e-001, - -9.273680e-001, - 1.380954e-001, - -3.302179e-001, - -3.553265e+000, - 4.633345e+000, - 9.696729e-001, - 8.799775e-002, - 8.291129e+000, - 1.094451e+000, - -1.099377e+000, - -3.325392e-001, - 2.501063e-001, - 2.613712e+000, - -1.328142e+001, - -5.579527e-001, - 4.992081e-001, - -3.504402e+000, - 3.022924e-001, - -1.048420e+000, - -1.227773e-001, - 5.845373e-001, - 1.105869e+001, - 3.813151e-002, - 1.330409e+000, - 1.978131e-002, - 3.959430e+000, - 8.396439e-001, - -1.063233e+000, - -1.560639e-001, - 2.840033e-001, - 8.751565e-001, - -3.411820e+000, - -1.436564e-001, - 5.846580e-001, - 2.899292e+000, - 6.799095e-001, - // albedo 0, turbidity 7 - -1.376715e+000, - -4.541567e-001, - -1.445491e+000, - 1.569898e+000, - -1.390627e-001, - 5.558270e-001, - 4.109877e-002, - 3.349451e+000, - 5.516123e-001, - -1.953391e+000, - -1.035869e+000, - 1.690563e+000, - -1.964690e-001, - -7.787096e-001, - 5.799605e-001, - 2.945626e-002, - 4.217906e-002, - 2.451373e-001, - -1.012422e+000, - 7.136451e-002, - -1.862534e+000, - -7.228653e-001, - 1.947997e-001, - 2.091805e-001, - 6.399233e-002, - 7.928994e+000, - 1.290733e+000, - -9.706708e-001, - -2.880950e-001, - 1.107797e+000, - -2.731734e+000, - -8.445995e+000, - 4.296774e-001, - 5.117648e-001, - -3.824277e+000, - 1.761207e-001, - -1.110611e+000, - -1.789409e-001, - 2.108488e-001, - 2.071430e+001, - -1.763174e+000, - 9.554695e-002, - -2.943103e-002, - 3.422079e+000, - 8.815496e-001, - -1.048334e+000, - -1.614087e-001, - 2.475184e-001, - 2.146938e-002, - -2.983901e+000, - 2.538224e-001, - 5.601370e-001, - 2.461925e+000, - 6.777394e-001, - // albedo 0, turbidity 8 - -1.393719e+000, - -5.002724e-001, - -2.408940e+000, - 2.680983e+000, - -1.362825e-001, - 7.395067e-001, - -3.300343e-006, - 3.260889e+000, - 8.132057e-001, - -2.128663e+000, - -1.151182e+000, - 2.923026e+000, - -1.931838e+000, - -4.426170e-001, - 2.309983e-001, - -5.485890e-003, - 3.279529e-001, - -2.229467e-001, - -1.618022e+000, - -3.766490e-001, - -3.163544e+000, - 1.611608e+000, - -3.967476e-001, - 3.933680e-001, - 3.006742e-001, - 6.835177e+000, - 1.613765e+000, - -5.669064e-001, - -1.481749e-001, - 2.071817e+000, - -8.157422e+000, - -5.988088e+000, - 2.387202e-001, - 1.447191e-001, - -4.296385e+000, - 5.011258e-002, - -1.241724e+000, - -2.519348e-001, - -1.908609e-001, - 2.952235e+001, - -3.333660e+000, - -1.837651e-002, - 1.022249e-001, - 2.929320e+000, - 8.867262e-001, - -1.021670e+000, - -1.667327e-001, - 1.789771e-001, - -2.178108e-003, - -2.641572e+000, - -5.641484e-002, - 5.303758e-001, - 2.138196e+000, - 6.780350e-001, - // albedo 0, turbidity 9 - -1.669332e+000, - -7.588708e-001, - -2.993557e+000, - 3.178760e+000, - -8.066442e-002, - 6.544672e-001, - -8.089880e-006, - 2.628924e+000, - 9.001272e-001, - -1.755806e+000, - -8.735348e-001, - 3.258881e+000, - -2.504785e+000, - -3.300791e-001, - 1.180565e-001, - -9.315982e-003, - 1.785154e+000, - -3.205824e-001, - -3.720277e+000, - -1.733350e+000, - -3.332272e+000, - 1.515869e+000, - 1.734218e-001, - 8.011956e-001, - 1.995440e-001, - 3.817666e+000, - 1.638502e+000, - 4.724641e-001, - 3.209828e-001, - 2.051443e+000, - -5.105574e+000, - -6.509139e+000, - -4.232041e-001, - 2.598931e-001, - -2.151756e+000, - -3.493910e-003, - -1.525600e+000, - -4.897606e-001, - -9.891121e-002, - 2.346818e+001, - -2.278152e+000, - 1.681219e-001, - -4.469389e-002, - 1.051000e+000, - 9.294666e-001, - -9.908649e-001, - -2.008182e-001, - 1.605143e-001, - -2.463113e-003, - -2.477349e+000, - -1.218647e-001, - 4.750121e-001, - 1.460813e+000, - 6.661364e-001, - // albedo 0, turbidity 10 - -2.122119e+000, - -1.125475e+000, - -3.066599e+000, - 3.145078e+000, - -5.411593e-002, - 5.133628e-001, - -7.823408e-006, - 2.268448e+000, - 9.001416e-001, - -1.528158e+000, - -9.370249e-001, - 2.567559e+000, - -1.591439e+000, - -3.634460e-001, - 1.763256e-001, - 1.119624e-003, - 1.811848e+000, - -2.637929e-001, - -6.524387e+000, - -2.673507e+000, - -2.940472e+000, - -6.025609e-001, - 7.852067e-001, - 1.073499e+000, - -3.540435e-002, - 3.517416e+000, - 1.490466e+000, - 8.886026e-001, - -9.681828e-002, - 1.430554e+000, - 4.993717e+000, - -6.071355e+000, - -6.053986e-001, - 5.092997e-001, - -1.273010e+000, - 7.491329e-002, - -1.481997e+000, - -5.897282e-001, - 2.659264e-001, - 1.267239e+000, - -5.741291e-001, - 5.983011e-002, - -2.217312e-001, - -3.016452e-001, - 9.260830e-001, - -1.010943e+000, - -2.075134e-001, - 5.066749e-002, - 1.470708e+001, - -3.780501e+000, - 7.253223e-002, - 4.045458e-001, - 1.320164e+000, - 6.559925e-001, - // albedo 1, turbidity 1 - -1.129907e+000, - -1.884011e-001, - -8.047670e+000, - 9.035776e+000, - -5.539419e-002, - 8.823349e-001, - 3.197135e-002, - 4.839388e+000, - 5.042822e-001, - -1.133821e+000, - -1.510781e-001, - 3.362822e+000, - -2.453381e+000, - -1.463925e-001, - 4.728708e-001, - 5.958140e-002, - 7.636300e+000, - 4.805162e-001, - -1.176518e+000, - -3.549902e-001, - 1.729044e+000, - -2.160966e-001, - -5.075865e-001, - 1.675584e+000, - -8.906902e-002, - -5.386842e+000, - 5.452218e-001, - -1.043563e+000, - -7.520975e-002, - 8.750644e-001, - 2.510518e+000, - 7.584882e-003, - 9.361250e-001, - 7.889083e-002, - 6.066644e+000, - 5.813108e-001, - -1.081304e+000, - -2.222253e-001, - 2.517638e+000, - -4.453820e+000, - -8.663691e+000, - 8.662558e-001, - -4.802657e-002, - -8.965449e-001, - 4.886656e-001, - -1.083774e+000, - -1.375469e-001, - 1.685818e+000, - 5.631120e+000, - -3.100752e+000, - 4.045941e-001, - 2.346895e-002, - 3.390321e+000, - 5.008309e-001, - // albedo 1, turbidity 2 - -1.143158e+000, - -2.058334e-001, - -9.660198e+000, - 1.062394e+001, - -4.434119e-002, - 8.607615e-001, - 3.177325e-002, - 4.416481e+000, - 5.918162e-001, - -1.146773e+000, - -1.727385e-001, - 4.626048e+000, - -4.684602e+000, - -8.307137e-002, - 1.619616e-001, - 1.484866e-001, - 7.572868e+000, - 2.681126e-001, - -1.151324e+000, - -3.099303e-001, - 4.125596e-001, - 2.340752e+000, - -4.214444e-001, - 1.987375e+000, - -1.913410e-001, - -3.845978e+000, - 1.337311e+000, - -1.034258e+000, - -7.778759e-003, - 7.050094e-001, - -8.036369e-001, - 3.138570e-001, - 2.469452e-001, - 3.559970e-001, - 7.485917e+000, - 4.790329e-002, - -1.096568e+000, - -2.673169e-001, - 2.575654e+000, - -8.057121e-001, - -8.884928e+000, - 1.416170e+000, - -2.091315e-001, - -1.543494e+000, - 1.065445e+000, - -1.083304e+000, - -1.528265e-001, - 1.697727e+000, - 2.503702e+000, - -2.885296e+000, - -1.298500e-001, - 1.548870e-001, - 2.479652e+000, - 5.066496e-001, - // albedo 1, turbidity 3 - -1.165736e+000, - -2.329945e-001, - -5.967964e+000, - 6.705959e+000, - -5.931355e-002, - 7.485638e-001, - 3.913878e-002, - 4.221591e+000, - 6.183926e-001, - -1.212422e+000, - -2.545910e-001, - 2.418626e+000, - -2.266104e+000, - -1.102014e-001, - 1.363887e-002, - 1.055411e-001, - 5.648062e+000, - 4.557412e-001, - -1.070436e+000, - -2.163341e-001, - 7.098718e-001, - 7.843075e-001, - -4.323930e-001, - 2.109823e+000, - -9.589700e-002, - -1.985193e-001, - 1.060428e+000, - -1.104879e+000, - -3.013622e-002, - 2.976276e-002, - 1.069707e+000, - 1.410000e-001, - -4.880020e-001, - 4.452288e-001, - 6.418590e+000, - 3.195986e-001, - -1.048969e+000, - -2.655317e-001, - 2.689426e+000, - -3.941038e+000, - -9.506461e+000, - 1.837119e+000, - -1.892124e-001, - -1.562146e+000, - 9.043414e-001, - -1.106145e+000, - -1.601642e-001, - 1.544544e+000, - 7.388492e+000, - -2.924600e+000, - -4.328453e-001, - 1.763161e-001, - 2.523111e+000, - 5.851902e-001, - // albedo 1, turbidity 4 - -1.203666e+000, - -2.776587e-001, - -2.084286e+000, - 2.450840e+000, - -8.746613e-002, - 5.258507e-001, - 7.983316e-002, - 3.860055e+000, - 5.486167e-001, - -1.340448e+000, - -4.230590e-001, - 3.462849e-001, - 4.707607e-001, - -2.512626e-001, - 1.530746e-001, - 2.724218e-002, - 3.035216e+000, - 5.876133e-001, - -1.014554e+000, - -1.168790e-001, - 9.477794e-001, - -1.061218e+000, - -4.196730e-001, - 2.058832e+000, - -5.989624e-002, - 3.058168e+000, - 9.763861e-001, - -1.137388e+000, - -9.854030e-002, - -2.984893e-001, - 3.647820e+000, - -6.585571e-001, - -1.479180e+000, - 6.102932e-001, - 3.265914e+000, - 3.480333e-001, - -1.021816e+000, - -2.344957e-001, - 2.463671e+000, - -7.240685e+000, - -8.862697e+000, - 2.514058e+000, - -2.122768e-001, - -3.313968e-002, - 9.028136e-001, - -1.126581e+000, - -1.874347e-001, - 1.454154e+000, - 1.034398e+001, - -3.237393e+000, - -8.654927e-001, - 2.457248e-001, - 1.845769e+000, - 6.002482e-001, - // albedo 1, turbidity 5 - -1.263727e+000, - -3.439354e-001, - -1.786388e-001, - 3.980166e-001, - -3.349517e-001, - 3.825166e-001, - 1.029225e-001, - 3.331096e+000, - 4.998955e-001, - -1.530010e+000, - -6.879698e-001, - 2.380415e-001, - 1.608216e+000, - -1.682679e+000, - 3.546360e-001, - -3.915220e-003, - 4.517655e-001, - 5.128605e-001, - -9.685659e-001, - 9.480403e-002, - 6.076844e-002, - -3.217561e+000, - 4.568074e+000, - 1.069299e+000, - 2.083638e-002, - 7.301088e+000, - 1.072165e+000, - -1.113925e+000, - -3.112382e-001, - 3.954133e-001, - 5.105907e+000, - -1.456866e+001, - -4.917378e-001, - 5.289909e-001, - -2.678374e+000, - 3.014709e-001, - -1.046864e+000, - -1.215754e-001, - 1.778308e+000, - 4.661489e+000, - 2.565583e-001, - 1.353680e+000, - -1.175767e-001, - 3.415972e+000, - 8.457746e-001, - -1.104480e+000, - -1.940913e-001, - 1.343668e+000, - -1.759206e-003, - -5.009204e+000, - -4.186951e-001, - 3.125710e-001, - 1.628183e+000, - 6.720408e-001, - // albedo 1, turbidity 6 - -1.286902e+000, - -3.781238e-001, - -8.977253e-002, - 3.545393e-001, - -4.866515e-001, - 3.843664e-001, - 8.281675e-002, - 3.122231e+000, - 5.046991e-001, - -1.712597e+000, - -8.549112e-001, - 4.809286e-001, - 1.515398e+000, - -2.212211e+000, - 2.539029e-001, - 2.335997e-002, - -6.089466e-002, - 4.268444e-001, - -8.807283e-001, - 1.646097e-001, - -4.437898e-001, - -3.188247e+000, - 5.984417e+000, - 1.334779e+000, - -4.026975e-002, - 7.546431e+000, - 1.175751e+000, - -1.147253e+000, - -3.538199e-001, - 6.101836e-001, - 4.437780e+000, - -1.559813e+001, - -1.103222e+000, - 6.242039e-001, - -3.091472e+000, - 2.174290e-001, - -1.038230e+000, - -1.213475e-001, - 1.547505e+000, - 5.893176e+000, - 1.368738e+000, - 1.663127e+000, - -1.377130e-001, - 3.185279e+000, - 8.736453e-001, - -1.101026e+000, - -1.874907e-001, - 1.272667e+000, - 3.596524e+000, - -5.007243e+000, - -6.352483e-001, - 3.048985e-001, - 1.931613e+000, - 6.788844e-001, - // albedo 1, turbidity 7 - -1.342753e+000, - -4.384971e-001, - -1.213491e+000, - 1.621399e+000, - -1.551441e-001, - 5.614218e-001, - 2.591739e-002, - 2.958967e+000, - 5.782132e-001, - -1.937684e+000, - -1.066019e+000, - 1.913336e+000, - -7.347719e-001, - -5.916167e-001, - 1.587590e-001, - 1.092568e-001, - -6.275002e-001, - 1.599071e-001, - -9.302391e-001, - 1.486187e-001, - -1.603835e+000, - 1.783713e-001, - 1.100461e+000, - 1.174181e+000, - -1.602361e-001, - 7.868331e+000, - 1.468971e+000, - -1.053631e+000, - -3.727050e-001, - 1.114117e+000, - -9.603286e-001, - -1.062469e+001, - -1.162140e+000, - 7.952797e-001, - -4.478765e+000, - -4.440862e-002, - -1.083629e+000, - -1.261405e-001, - 1.229344e+000, - 1.127825e+001, - 1.319010e-001, - 1.624729e+000, - -2.825898e-001, - 3.661082e+000, - 1.036911e+000, - -1.093950e+000, - -2.067455e-001, - 1.258035e+000, - 7.548645e+000, - -4.598387e+000, - -8.944932e-001, - 3.292634e-001, - 1.311304e+000, - 6.291871e-001, - // albedo 1, turbidity 8 - -1.385867e+000, - -5.068139e-001, - -1.486490e+000, - 1.969049e+000, - -1.698025e-001, - 6.629167e-001, - -5.289365e-006, - 2.760315e+000, - 8.644368e-001, - -2.107367e+000, - -1.175639e+000, - 2.313241e+000, - -1.001653e+000, - -4.843139e-001, - 1.124485e-001, - 3.901494e-005, - -3.502469e-001, - -3.204780e-001, - -1.475244e+000, - -2.833055e-001, - -2.085824e+000, - 1.192563e+000, - -7.645200e-001, - 8.380081e-001, - 2.203580e-001, - 7.157885e+000, - 1.753702e+000, - -6.644372e-001, - -2.549735e-001, - 1.600273e+000, - -8.589034e+000, - -6.144718e+000, - -7.599731e-001, - 2.898370e-001, - -5.770923e+000, - -9.656242e-002, - -1.211687e+000, - -1.653494e-001, - 8.393400e-001, - 2.792988e+001, - -3.395461e+000, - 9.933752e-001, - -3.976877e-002, - 3.776659e+000, - 9.546526e-001, - -1.063757e+000, - -2.037563e-001, - 1.117207e+000, - -1.252806e-003, - -3.332330e+000, - -6.971409e-001, - 3.388719e-001, - 1.311398e+000, - 6.635171e-001, - // albedo 1, turbidity 9 - -1.678889e+000, - -7.992295e-001, - -2.421687e+000, - 2.871029e+000, - -7.662842e-002, - 6.046208e-001, - -7.598099e-006, - 2.002314e+000, - 9.001307e-001, - -1.692144e+000, - -8.804250e-001, - 3.060895e+000, - -2.000009e+000, - -3.183563e-001, - 8.385862e-002, - -6.326713e-003, - 1.206639e+000, - -3.369967e-001, - -3.676795e+000, - -1.719207e+000, - -2.534697e+000, - 1.005285e+000, - 1.550407e-001, - 1.072910e+000, - 1.318094e-001, - 3.717018e+000, - 1.689191e+000, - 5.424542e-001, - 3.263528e-001, - 1.551055e+000, - -3.841058e+000, - -6.598996e+000, - -1.201779e+000, - 3.530669e-001, - -2.542945e+000, - -6.482523e-002, - -1.553849e+000, - -4.576860e-001, - 9.324676e-001, - 1.950982e+001, - -2.344516e+000, - 1.121020e+000, - -1.221537e-001, - 7.285496e-001, - 9.582816e-001, - -1.020650e+000, - -2.215797e-001, - 1.009774e+000, - -2.056855e-003, - -2.740338e+000, - -8.122355e-001, - 3.328967e-001, - 8.982766e-001, - 6.594676e-001, - // albedo 1, turbidity 10 - -2.247360e+000, - -1.221267e+000, - -3.072346e+000, - 3.385139e+000, - -4.387559e-002, - 5.084887e-001, - -7.418833e-006, - 1.750107e+000, - 9.001401e-001, - -1.248499e+000, - -8.442718e-001, - 3.062611e+000, - -2.020314e+000, - -2.815341e-001, - 5.254745e-002, - 3.345008e-003, - 1.433225e+000, - -2.835911e-001, - -7.004119e+000, - -2.927978e+000, - -2.649852e+000, - 7.971894e-001, - 5.466893e-001, - 1.442667e+000, - -6.063912e-002, - 2.806194e+000, - 1.547429e+000, - 1.434882e+000, - 9.114639e-002, - 1.170089e+000, - 3.512808e-002, - -5.861915e+000, - -1.411843e+000, - 5.400486e-001, - -7.746522e-001, - 2.386984e-002, - -1.559053e+000, - -5.502302e-001, - 1.200396e+000, - 1.347741e+001, - -2.344397e+000, - 8.868907e-001, - -3.292661e-001, - -1.362105e+000, - 9.217826e-001, - -1.044436e+000, - -2.360719e-001, - 7.054471e-001, - -2.904518e-003, - -2.092829e+000, - -5.119668e-001, - 4.174861e-001, - 9.687435e-001, - 6.588427e-001, -}; - -double datasetRGBRad2[] = -{ - // albedo 0, turbidity 1 - 1.590330e+000, - 1.355401e+000, - 1.151412e+000, - 1.359116e+001, - 5.857714e+000, - 8.090833e+000, - // albedo 0, turbidity 2 - 1.552540e+000, - 1.510040e+000, - 1.276413e-001, - 1.604643e+001, - 5.912162e+000, - 8.350009e+000, - // albedo 0, turbidity 3 - 1.470871e+000, - 1.880464e+000, - -1.865398e+000, - 2.030808e+001, - 5.471461e+000, - 9.109834e+000, - // albedo 0, turbidity 4 - 1.356563e+000, - 2.373866e+000, - -4.653245e+000, - 2.570922e+001, - 5.686009e+000, - 1.009480e+001, - // albedo 0, turbidity 5 - 1.244232e+000, - 2.851519e+000, - -7.130942e+000, - 2.993449e+001, - 6.382120e+000, - 1.114578e+001, - // albedo 0, turbidity 6 - 1.173693e+000, - 3.120604e+000, - -8.491886e+000, - 3.187393e+001, - 7.290615e+000, - 1.180066e+001, - // albedo 0, turbidity 7 - 1.091845e+000, - 3.368888e+000, - -9.722083e+000, - 3.268508e+001, - 1.032424e+001, - 1.236508e+001, - // albedo 0, turbidity 8 - 9.858985e-001, - 3.500541e+000, - -1.026328e+001, - 3.092956e+001, - 1.610881e+001, - 1.331222e+001, - // albedo 0, turbidity 9 - 8.864993e-001, - 3.172888e+000, - -8.687550e+000, - 2.362161e+001, - 2.621851e+001, - 1.474967e+001, - // albedo 0, turbidity 10 - 7.946973e-001, - 2.189355e+000, - -4.207953e+000, - 9.399091e+000, - 4.062849e+001, - 1.681753e+001, - // albedo 1, turbidity 1 - 1.711696e+000, - 1.657311e+000, - 9.328021e-001, - 1.317880e+001, - 1.506751e+001, - 1.863556e+001, - // albedo 1, turbidity 2 - 1.666968e+000, - 1.849993e+000, - -2.088601e-001, - 1.586653e+001, - 1.486880e+001, - 1.940719e+001, - // albedo 1, turbidity 3 - 1.584846e+000, - 2.170022e+000, - -2.019597e+000, - 1.970826e+001, - 1.490684e+001, - 2.045055e+001, - // albedo 1, turbidity 4 - 1.469412e+000, - 2.524017e+000, - -4.197267e+000, - 2.365249e+001, - 1.664588e+001, - 2.134477e+001, - // albedo 1, turbidity 5 - 1.369714e+000, - 2.843548e+000, - -6.059031e+000, - 2.634993e+001, - 1.881361e+001, - 2.232186e+001, - // albedo 1, turbidity 6 - 1.310477e+000, - 2.984444e+000, - -6.831686e+000, - 2.682340e+001, - 2.123267e+001, - 2.259755e+001, - // albedo 1, turbidity 7 - 1.222552e+000, - 3.176523e+000, - -7.731496e+000, - 2.671760e+001, - 2.484358e+001, - 2.336863e+001, - // albedo 1, turbidity 8 - 1.115781e+000, - 3.130635e+000, - -7.581744e+000, - 2.336531e+001, - 3.171048e+001, - 2.413859e+001, - // albedo 1, turbidity 9 - 1.013181e+000, - 2.699342e+000, - -5.602709e+000, - 1.500158e+001, - 4.217613e+001, - 2.515957e+001, - // albedo 1, turbidity 10 - 8.976323e-001, - 1.726948e+000, - -1.296120e+000, - 1.183675e+000, - 5.503215e+001, - 2.643066e+001, -}; - -double datasetRGB3[] = -{ - // albedo 0, turbidity 1 - -1.372629e+000, - -4.905585e-001, - -4.100789e+001, - 4.122169e+001, - -7.389360e-003, - 4.839359e-001, - 6.474757e-003, - 3.471755e+000, - 5.092936e-001, - -1.523025e+000, - -6.497084e-001, - 6.249857e+000, - -5.662543e+000, - -1.908402e-002, - 5.512810e-001, - -2.181049e-005, - 2.507663e+000, - 4.339598e-001, - -1.035567e+000, - -7.478740e-002, - 9.221030e-001, - -2.140047e+000, - -2.374146e-002, - 3.795517e-001, - -1.769134e-002, - 7.479831e+000, - 7.729303e-001, - -1.271086e+000, - -5.588190e-001, - 6.908023e-001, - 2.096832e+000, - -2.453967e-001, - 1.410648e+000, - 4.475036e-002, - -4.719115e+000, - 5.741186e-001, - -9.712598e-001, - -7.033926e-002, - 9.167274e-001, - -9.502097e-001, - 3.004684e-001, - 4.547054e-001, - -5.929017e-002, - 5.266196e+000, - 7.204135e-001, - -1.087457e+000, - -1.888896e-001, - 8.156686e-001, - 3.101712e-001, - -2.155419e+000, - 1.422205e+000, - 9.692261e-002, - 3.122404e+000, - 4.999430e-001, - // albedo 0, turbidity 2 - -1.425280e+000, - -5.413508e-001, - -3.454883e+001, - 3.481142e+001, - -8.686975e-003, - 4.914268e-001, - -2.479243e-006, - 3.239879e+000, - 6.094201e-001, - -1.688557e+000, - -8.070865e-001, - 7.018459e+000, - -6.244574e+000, - -2.149341e-002, - 3.993971e-001, - 1.252502e-002, - 1.630662e+000, - 1.097860e-001, - -8.664152e-001, - 7.869125e-002, - -5.236535e-001, - -1.218960e+000, - -2.059093e-002, - 6.684898e-001, - -5.584112e-002, - 8.602299e+000, - 1.410496e+000, - -1.319763e+000, - -5.985323e-001, - 1.253918e+000, - 1.914706e+000, - -3.216739e-001, - 9.011213e-001, - 1.324845e-001, - -5.252749e+000, - 6.231252e-002, - -9.706008e-001, - -5.914059e-002, - 5.693150e-001, - -1.175362e+000, - 5.221644e-001, - 7.518213e-001, - -8.247655e-002, - 5.875635e+000, - 9.850863e-001, - -1.085330e+000, - -1.956105e-001, - 8.019605e-001, - 5.338101e-001, - -3.423464e+000, - 1.110444e+000, - 1.507923e-001, - 2.864942e+000, - 4.999481e-001, - // albedo 0, turbidity 3 - -1.431967e+000, - -5.478935e-001, - -3.286288e+001, - 3.305288e+001, - -8.380797e-003, - 4.772050e-001, - -3.044274e-006, - 3.289973e+000, - 5.976303e-001, - -1.801361e+000, - -9.315889e-001, - 5.391756e+000, - -4.588592e+000, - -2.040076e-002, - 4.144684e-001, - 1.814534e-002, - 1.051795e+000, - 1.145651e-001, - -7.905357e-001, - 1.451332e-001, - -1.605661e-001, - -1.592174e+000, - 4.561348e-004, - 3.380323e-001, - -7.770275e-002, - 8.775384e+000, - 1.489512e+000, - -1.308575e+000, - -5.539232e-001, - 9.184133e-001, - 2.011479e+000, - -3.842472e-001, - 1.432274e+000, - 1.637153e-001, - -4.408856e+000, - 5.272957e-002, - -9.829872e-001, - -8.183048e-002, - 4.464556e-001, - -1.442716e+000, - 1.029641e+000, - -6.991617e-002, - 8.702356e-003, - 5.706417e+000, - 9.116452e-001, - -1.087130e+000, - -2.038013e-001, - 7.260801e-001, - 9.164376e-001, - -5.006183e+000, - 1.511271e+000, - 1.257134e-001, - 2.715439e+000, - 6.201652e-001, - // albedo 0, turbidity 4 - -1.448662e+000, - -5.799075e-001, - -2.833268e+001, - 2.858023e+001, - -9.134061e-003, - 4.404783e-001, - -2.709026e-006, - 3.029357e+000, - 5.540071e-001, - -2.061772e+000, - -1.145190e+000, - 7.918478e+000, - -7.212525e+000, - -2.020760e-002, - 2.962715e-001, - 4.689670e-002, - 8.517209e-001, - 2.334587e-001, - -6.413755e-001, - 1.780425e-001, - -2.412919e+000, - 1.064484e+000, - -1.949986e-002, - 6.769741e-001, - -1.752760e-001, - 7.262714e+000, - 1.325869e+000, - -1.304871e+000, - -3.975581e-001, - 1.219002e+000, - 7.285178e-001, - -2.710105e-001, - 7.779727e-001, - 3.247139e-001, - -8.818168e-001, - 1.839517e-001, - -1.001104e+000, - -1.994801e-001, - 3.676742e-001, - -1.409737e+000, - 2.901555e-001, - 2.506940e-001, - 2.468899e-003, - 3.398923e+000, - 8.584645e-001, - -1.111552e+000, - -2.487204e-001, - 7.410842e-001, - 1.703749e+000, - -5.007855e+000, - 1.057763e+000, - 1.354511e-001, - 2.088715e+000, - 6.600013e-001, - // albedo 0, turbidity 5 - -1.547227e+000, - -6.679466e-001, - -1.861465e+001, - 1.884045e+001, - -1.242210e-002, - 4.157339e-001, - -2.432805e-006, - 2.812423e+000, - 5.446957e-001, - -2.043890e+000, - -1.149081e+000, - 2.304118e+000, - -1.715757e+000, - -2.433628e-002, - 2.816836e-001, - 7.185458e-002, - 1.064860e+000, - 2.706789e-001, - -9.040720e-001, - -8.274472e-002, - -2.555676e-001, - -6.326215e-001, - -2.770880e-002, - 6.676024e-001, - -2.513532e-001, - 5.903839e+000, - 1.241452e+000, - -1.000013e+000, - -1.010774e-001, - 3.699166e-001, - 8.774526e-001, - -3.042007e-001, - 6.951053e-001, - 4.361813e-001, - 6.793421e-001, - 2.573892e-001, - -1.171332e+000, - -3.768188e-001, - 3.701377e-001, - -1.470757e+000, - 5.525942e-001, - 2.991456e-002, - 1.581823e-002, - 2.365233e+000, - 8.214514e-001, - -1.068667e+000, - -2.326330e-001, - 6.725059e-001, - 2.243733e+000, - -4.614370e+000, - 1.033677e+000, - 1.376291e-001, - 2.013334e+000, - 6.865304e-001, - // albedo 0, turbidity 6 - -1.592991e+000, - -7.246948e-001, - -2.598204e+001, - 2.621960e+001, - -8.365176e-003, - 4.207571e-001, - -2.742772e-006, - 2.623735e+000, - 5.873190e-001, - -2.271349e+000, - -1.280884e+000, - 6.308739e+000, - -5.758350e+000, - -1.977049e-002, - 3.671835e-001, - 6.698038e-002, - 1.150597e+000, - 1.759218e-001, - -6.368620e-001, - -7.436052e-003, - -2.230026e+000, - 1.640997e+000, - -1.548497e-002, - 3.145331e-001, - -2.492644e-001, - 5.083843e+000, - 1.260215e+000, - -1.177925e+000, - -9.628114e-002, - 3.051152e-001, - -3.749544e-002, - -2.713209e-001, - 1.164226e+000, - 4.559969e-001, - 2.175429e+000, - 2.874284e-001, - -1.078500e+000, - -3.801779e-001, - 4.788906e-001, - -4.795969e-001, - 5.977621e-001, - -4.488535e-001, - 3.386874e-002, - 1.538143e+000, - 8.062054e-001, - -1.108028e+000, - -2.596892e-001, - 5.162202e-001, - 1.557081e+000, - -4.265039e+000, - 1.182535e+000, - 1.563762e-001, - 2.095084e+000, - 6.883383e-001, - // albedo 0, turbidity 7 - -1.668427e+000, - -7.908511e-001, - -2.779690e+001, - 2.799746e+001, - -7.186935e-003, - 3.757766e-001, - -3.326858e-006, - 2.563421e+000, - 5.439687e-001, - -2.156175e+000, - -1.220004e+000, - 3.585732e+000, - -3.235988e+000, - -1.086239e-002, - 1.846143e-001, - 1.046017e-001, - 1.234427e+000, - 2.842191e-001, - -1.117051e+000, - -4.101627e-001, - -8.463730e-001, - 7.671472e-001, - -2.226609e-002, - 8.574943e-001, - -3.434124e-001, - 4.475715e+000, - 1.154824e+000, - -7.444840e-001, - 2.312078e-001, - -5.393724e-001, - 1.574213e-001, - -1.763914e-001, - 2.751692e-001, - 5.564200e-001, - 2.217672e+000, - 3.483932e-001, - -1.273036e+000, - -5.275562e-001, - 4.902512e-001, - -4.498436e-002, - 4.339366e-001, - 2.386682e-001, - 2.380879e-002, - 1.413444e+000, - 7.855923e-001, - -1.084192e+000, - -2.936753e-001, - 4.719432e-001, - 1.384436e+000, - -3.257789e+000, - 6.119543e-001, - 1.681884e-001, - 1.650441e+000, - 6.936631e-001, - // albedo 0, turbidity 8 - -1.848490e+000, - -9.512670e-001, - -3.005251e+001, - 3.024315e+001, - -5.635304e-003, - 3.447780e-001, - -2.782999e-006, - 2.309422e+000, - 5.643559e-001, - -2.300008e+000, - -1.252335e+000, - -1.218876e+000, - 1.493730e+000, - -6.107100e-003, - 7.974860e-002, - 1.023449e-001, - 1.505934e+000, - 2.360948e-001, - -1.483705e+000, - -8.547575e-001, - -7.797146e-001, - 6.447971e-001, - -2.678052e-002, - 1.091263e+000, - -3.344889e-001, - 3.830416e+000, - 1.189425e+000, - -5.348005e-001, - 3.982733e-001, - -4.071573e-001, - 3.265569e-001, - -8.658789e-002, - -2.370892e-001, - 5.369097e-001, - 1.478279e+000, - 3.143303e-001, - -1.320401e+000, - -6.043247e-001, - 3.019196e-001, - -7.732911e-002, - 4.768381e-001, - 6.745764e-001, - 3.694098e-002, - 1.158234e+000, - 8.169056e-001, - -1.101040e+000, - -3.420019e-001, - 3.775661e-001, - 1.769338e+000, - -2.990515e+000, - 1.649529e-001, - 1.970125e-001, - 1.453355e+000, - 6.759757e-001, - // albedo 0, turbidity 9 - -2.251946e+000, - -1.229349e+000, - -3.271808e+001, - 3.283114e+001, - -4.252027e-003, - 3.372289e-001, - -3.001937e-006, - 2.154046e+000, - 5.842674e-001, - -1.867834e+000, - -9.531252e-001, - -1.229365e+001, - 1.269149e+001, - -6.844772e-003, - 1.185107e-001, - 7.539587e-002, - 1.846381e+000, - 1.899412e-001, - -3.398629e+000, - -2.180862e+000, - 2.335213e+000, - -3.382823e+000, - -8.613985e-003, - 8.431602e-001, - -2.393567e-001, - 3.112460e+000, - 1.218556e+000, - 5.708381e-001, - 9.406030e-001, - -6.890113e-001, - 2.746233e+000, - -5.772068e-002, - 1.096005e-001, - 3.491978e-001, - 7.281453e-001, - 3.212049e-001, - -1.705909e+000, - -8.517224e-001, - 1.131160e-001, - -2.141434e+000, - 4.274043e-001, - 3.397600e-001, - 1.786490e-001, - 9.026101e-001, - 7.882800e-001, - -1.012865e+000, - -3.495551e-001, - 3.369038e-001, - 3.724205e+000, - -3.089586e+000, - 1.266964e-001, - 1.461790e-001, - 1.170199e+000, - 6.931052e-001, - // albedo 0, turbidity 10 - -2.890318e+000, - -1.665573e+000, - -3.493756e+001, - 3.500369e+001, - -2.984251e-003, - 2.622419e-001, - -4.259360e-006, - 1.947681e+000, - 6.905752e-001, - -1.956022e+000, - -1.062900e+000, - -1.919714e+001, - 1.975164e+001, - -8.865396e-003, - 2.165540e-001, - 5.475637e-002, - 1.761134e+000, - 3.164249e-003, - -5.612198e+000, - -3.101371e+000, - 4.098034e+000, - -6.144001e+000, - 9.944958e-003, - 2.905472e-001, - -1.707110e-001, - 3.199107e+000, - 1.337660e+000, - 8.353756e-001, - 4.855943e-001, - -1.243589e+000, - 5.147385e+000, - -7.013963e-002, - 9.380410e-001, - 2.335714e-001, - 1.727744e-001, - 2.802696e-001, - -1.524329e+000, - -7.388547e-001, - 3.259025e-001, - -4.050634e+000, - 4.058549e-001, - -2.591384e-001, - 1.898299e-001, - 3.556071e-001, - 7.884126e-001, - -1.070371e+000, - -4.207858e-001, - 1.739862e-001, - 5.293410e+000, - -3.136757e+000, - 2.323856e-001, - 1.673706e-001, - 1.007227e+000, - 6.844287e-001, - // albedo 1, turbidity 1 - -1.341720e+000, - -4.834889e-001, - -4.633447e+001, - 4.682148e+001, - -6.137296e-003, - 4.599216e-001, - 7.047323e-003, - 2.895798e+000, - 4.999398e-001, - -1.529104e+000, - -6.498631e-001, - 1.534103e+001, - -1.450675e+001, - -1.531439e-002, - 3.280082e-001, - 1.682926e-002, - 1.901587e+000, - 5.013227e-001, - -1.014776e+000, - -1.454495e-001, - -4.071085e+000, - 2.954982e+000, - -2.630348e-002, - 5.681531e-001, - -3.016505e-002, - 6.773854e+000, - 5.003504e-001, - -1.172413e+000, - -4.026320e-001, - 2.960428e+000, - 2.020710e-001, - -2.004947e-001, - 9.375572e-001, - 5.998168e-002, - -4.945934e+000, - 4.502898e-001, - -9.898161e-001, - -5.772814e-002, - 4.470024e-001, - -5.786656e-001, - 1.158168e-001, - 3.468040e-001, - -5.043360e-002, - 6.867947e+000, - 8.012363e-001, - -1.085111e+000, - -1.882675e-001, - 1.223748e+000, - 3.565495e-001, - -3.688357e+000, - 5.653723e-001, - 6.727646e-002, - 2.690130e+000, - 4.999400e-001, - // albedo 1, turbidity 2 - -1.389119e+000, - -5.290250e-001, - -4.055774e+001, - 4.105972e+001, - -7.062577e-003, - 4.560060e-001, - -1.736334e-006, - 2.775512e+000, - 6.671455e-001, - -1.584641e+000, - -7.200619e-001, - 1.248067e+001, - -1.156028e+001, - -1.659568e-002, - 3.050029e-001, - 1.099895e-002, - 1.438927e+000, - -2.138015e-002, - -9.826068e-001, - -8.887254e-002, - -2.960031e+000, - 1.808816e+000, - -2.478159e-002, - 6.035733e-001, - -4.868441e-002, - 7.347705e+000, - 1.584739e+000, - -1.150423e+000, - -4.073793e-001, - 2.412991e+000, - 4.870840e-001, - -2.337902e-001, - 8.295114e-001, - 1.129914e-001, - -5.150045e+000, - -9.016643e-002, - -1.016933e+000, - -6.311501e-002, - 5.218937e-001, - -5.716430e-001, - 1.250993e-001, - 3.601524e-001, - -5.497586e-002, - 7.060139e+000, - 1.018333e+000, - -1.073151e+000, - -1.845444e-001, - 1.155394e+000, - 3.004486e-001, - -3.431711e+000, - 4.657031e-001, - 9.401223e-002, - 2.688620e+000, - 4.999544e-001, - // albedo 1, turbidity 3 - -1.391257e+000, - -5.365815e-001, - -4.255881e+001, - 4.299132e+001, - -5.838466e-003, - 4.229134e-001, - -2.760038e-006, - 2.775531e+000, - 6.234597e-001, - -1.780062e+000, - -9.228880e-001, - 1.376172e+001, - -1.260946e+001, - -1.507526e-002, - 3.117435e-001, - 2.205045e-002, - 6.093731e-001, - 3.463446e-002, - -7.388169e-001, - 1.275670e-001, - -3.999528e+000, - 2.223993e+000, - -1.856853e-002, - 5.439310e-001, - -8.834054e-002, - 8.037139e+000, - 1.645951e+000, - -1.322387e+000, - -5.320143e-001, - 2.659359e+000, - 1.086712e+000, - -2.129712e-001, - 8.704649e-001, - 1.800315e-001, - -4.967241e+000, - -1.383720e-001, - -9.378288e-001, - -1.599895e-002, - 3.607555e-001, - -1.980561e+000, - 3.791456e-001, - 1.212268e-001, - -2.845992e-002, - 6.825542e+000, - 1.059139e+000, - -1.100832e+000, - -2.172313e-001, - 1.211561e+000, - 2.002721e+000, - -5.010011e+000, - 5.717583e-001, - 6.777702e-002, - 2.160006e+000, - 5.676392e-001, - // albedo 1, turbidity 4 - -1.409373e+000, - -5.708751e-001, - -3.034974e+001, - 3.079809e+001, - -7.280715e-003, - 3.723304e-001, - -2.436279e-006, - 2.577348e+000, - 5.913377e-001, - -1.954312e+000, - -1.116510e+000, - 5.399148e+000, - -4.299553e+000, - -1.724739e-002, - 3.742824e-001, - 4.187077e-002, - 1.044883e-001, - 1.232727e-001, - -6.772215e-001, - 2.001396e-001, - -3.670523e-001, - -1.014628e+000, - -3.497152e-003, - 4.099858e-001, - -1.584633e-001, - 7.750400e+000, - 1.514559e+000, - -1.291600e+000, - -4.977437e-001, - 9.641914e-001, - 1.562420e+000, - -3.227782e-001, - 9.055427e-001, - 3.046444e-001, - -3.385619e+000, - 9.546291e-003, - -9.750857e-001, - -8.770560e-002, - 9.054256e-001, - -1.429236e+000, - 8.974777e-001, - -1.217961e-001, - -5.194608e-002, - 4.909409e+000, - 9.589153e-001, - -1.088007e+000, - -1.959301e-001, - 9.745799e-001, - 1.260761e+000, - -5.008864e+000, - 7.271248e-001, - 1.096661e-001, - 2.717295e+000, - 6.340731e-001, - // albedo 1, turbidity 5 - -1.456050e+000, - -6.223072e-001, - -2.228088e+001, - 2.269604e+001, - -9.340812e-003, - 4.118308e-001, - -2.418083e-006, - 2.442117e+000, - 5.589638e-001, - -2.176449e+000, - -1.302416e+000, - 2.222836e+000, - -1.222730e+000, - -1.728051e-002, - 1.323513e-001, - 7.027731e-002, - 4.835745e-002, - 2.093351e-001, - -5.789641e-001, - 2.215407e-001, - 2.142291e-001, - -1.201725e+000, - -1.185728e-002, - 8.122982e-001, - -2.380420e-001, - 6.706841e+000, - 1.404146e+000, - -1.307463e+000, - -4.515174e-001, - 6.447827e-001, - 1.223841e+000, - -2.902391e-001, - 4.986588e-001, - 4.073652e-001, - -1.706696e+000, - 1.060885e-001, - -9.698678e-001, - -1.307094e-001, - 9.389347e-001, - -1.522852e+000, - 7.768797e-001, - -1.368595e-001, - -3.857426e-002, - 3.676935e+000, - 8.980966e-001, - -1.104349e+000, - -2.380323e-001, - 1.047043e+000, - 1.865421e+000, - -5.011664e+000, - 7.014954e-001, - 9.622701e-002, - 1.891360e+000, - 6.687354e-001, - // albedo 1, turbidity 6 - -1.502249e+000, - -6.724523e-001, - -2.888092e+001, - 2.930360e+001, - -6.685766e-003, - 3.685464e-001, - -2.469442e-006, - 2.310797e+000, - 5.566754e-001, - -2.217125e+000, - -1.364924e+000, - 4.048243e+000, - -3.111333e+000, - -1.317747e-002, - 1.921948e-001, - 8.627702e-002, - 1.981769e-003, - 2.213689e-001, - -6.215757e-001, - 1.687995e-001, - -5.949131e-001, - -1.551293e-001, - 3.356129e-004, - 6.897657e-001, - -2.855053e-001, - 6.271042e+000, - 1.363084e+000, - -1.216317e+000, - -3.489429e-001, - 7.566226e-001, - 5.409809e-001, - -2.830843e-001, - 6.191825e-001, - 4.755163e-001, - -9.131387e-001, - 1.383909e-001, - -1.030437e+000, - -2.034064e-001, - 8.335995e-001, - -1.050947e+000, - 8.689093e-001, - -3.672310e-001, - -4.056183e-002, - 3.111269e+000, - 8.856842e-001, - -1.078984e+000, - -2.070549e-001, - 9.683145e-001, - 1.497022e+000, - -5.007653e+000, - 7.702541e-001, - 1.285822e-001, - 2.225188e+000, - 6.587911e-001, - // albedo 1, turbidity 7 - -1.559291e+000, - -7.374039e-001, - -3.596311e+001, - 3.634470e+001, - -4.667132e-003, - 3.277964e-001, - -2.487945e-006, - 2.215652e+000, - 5.764681e-001, - -2.356929e+000, - -1.444755e+000, - 6.244526e+000, - -5.540162e+000, - -8.794510e-003, - 1.792100e-001, - 9.578517e-002, - 3.737676e-001, - 1.922194e-001, - -6.589752e-001, - -2.926910e-002, - -1.831779e+000, - 1.869962e+000, - -2.030095e-003, - 7.552089e-001, - -3.168157e-001, - 4.632196e+000, - 1.294054e+000, - -1.161046e+000, - -1.472506e-001, - 6.494138e-001, - -8.327174e-001, - -2.320724e-001, - 3.391212e-001, - 5.269637e-001, - 9.376341e-001, - 2.458573e-001, - -1.034427e+000, - -3.062504e-001, - 8.975634e-001, - 3.203531e-001, - 8.565142e-001, - -1.250162e-001, - -4.094017e-002, - 1.861304e+000, - 8.223468e-001, - -1.109954e+000, - -2.740277e-001, - 1.063811e+000, - 7.077398e-001, - -4.695734e+000, - 5.621696e-001, - 1.248956e-001, - 1.297723e+000, - 6.789720e-001, - // albedo 1, turbidity 8 - -1.788293e+000, - -9.368751e-001, - -4.382980e+001, - 4.424963e+001, - -3.652530e-003, - 3.094331e-001, - -2.810503e-006, - 1.904402e+000, - 5.861599e-001, - -2.268206e+000, - -1.312676e+000, - 2.863082e+000, - -2.373727e+000, - -5.144980e-003, - 1.711072e-001, - 9.316041e-002, - 9.309598e-001, - 1.791683e-001, - -1.376966e+000, - -7.418582e-001, - -1.349589e+000, - 1.563419e+000, - -3.124219e-003, - 6.967139e-001, - -3.061887e-001, - 3.602731e+000, - 1.255669e+000, - -6.017540e-001, - 2.815928e-001, - 5.424052e-001, - -6.885450e-001, - -1.620001e-001, - 2.980046e-001, - 4.995571e-001, - 7.371203e-001, - 2.812466e-001, - -1.278853e+000, - -5.245326e-001, - 7.870520e-001, - 3.125067e-001, - 7.748105e-001, - -7.788581e-002, - 3.490956e-003, - 1.283748e+000, - 8.130190e-001, - -1.050930e+000, - -2.786331e-001, - 1.056344e+000, - 1.053002e+000, - -4.047789e+000, - 4.432174e-001, - 1.169077e-001, - 9.532621e-001, - 6.806764e-001, - // albedo 1, turbidity 9 - -2.084927e+000, - -1.203954e+000, - -4.881638e+001, - 4.920160e+001, - -2.896045e-003, - 2.882977e-001, - -3.073517e-006, - 1.702211e+000, - 6.374180e-001, - -2.328567e+000, - -1.238023e+000, - -1.891019e+000, - 2.451520e+000, - -5.847581e-003, - 2.084702e-001, - 7.848130e-002, - 1.211048e+000, - 8.095008e-002, - -2.634632e+000, - -1.789460e+000, - -1.370558e-001, - -3.326435e-001, - 2.783737e-003, - 5.239451e-001, - -2.548881e-001, - 2.896327e+000, - 1.324116e+000, - 6.882616e-002, - 5.997821e-001, - 1.535398e-001, - 1.375209e+000, - -1.267285e-001, - 4.239743e-001, - 4.013122e-001, - 1.794675e-001, - 2.395382e-001, - -1.430918e+000, - -6.439041e-001, - 8.325980e-001, - -1.705612e+000, - 7.236426e-001, - -5.567593e-002, - 6.408718e-002, - 6.836524e-001, - 8.388887e-001, - -1.037956e+000, - -3.215402e-001, - 9.457349e-001, - 3.178114e+000, - -4.152156e+000, - 2.230992e-001, - 1.156198e-001, - 7.606223e-001, - 6.656923e-001, - // albedo 1, turbidity 10 - -2.967314e+000, - -1.728778e+000, - -3.730988e+001, - 3.755578e+001, - -2.588835e-003, - 2.927966e-001, - -3.935038e-006, - 1.592161e+000, - 6.868694e-001, - -2.123311e+000, - -1.175148e+000, - -1.314988e+001, - 1.386882e+001, - -7.828537e-003, - 1.852026e-001, - 5.481038e-002, - 1.294309e+000, - 2.428177e-002, - -5.443597e+000, - -3.156344e+000, - 2.110838e+000, - -3.421556e+000, - 1.181890e-002, - 1.196951e-001, - -1.742902e-001, - 2.404353e+000, - 1.272805e+000, - 1.029898e+000, - 5.912521e-001, - -3.983531e-001, - 3.286069e+000, - -9.252065e-002, - 1.331381e+000, - 2.560642e-001, - 8.001754e-001, - 3.624178e-001, - -1.547574e+000, - -7.881604e-001, - 1.020902e+000, - -2.897069e+000, - 5.213470e-001, - -9.242315e-001, - 1.185594e-001, - -1.150721e+000, - 7.317211e-001, - -9.621043e-001, - -1.991406e-001, - 6.531287e-001, - 3.925839e+000, - -3.596904e+000, - 6.317332e-001, - 1.531334e-001, - 1.457846e+000, - 6.966285e-001, -}; - -double datasetRGBRad3[] = -{ - // albedo 0, turbidity 1 - 9.926518e-001, - 1.999494e+000, - -4.136109e+000, - 1.856270e+001, - 1.351028e+001, - 1.390238e+001, - // albedo 0, turbidity 2 - 9.634366e-001, - 2.119694e+000, - -4.614523e+000, - 1.919701e+001, - 1.376644e+001, - 1.418731e+001, - // albedo 0, turbidity 3 - 9.446537e-001, - 2.171610e+000, - -4.915556e+000, - 1.918240e+001, - 1.537135e+001, - 1.400530e+001, - // albedo 0, turbidity 4 - 9.073074e-001, - 2.330536e+000, - -5.577596e+000, - 1.961615e+001, - 1.688365e+001, - 1.446955e+001, - // albedo 0, turbidity 5 - 8.739124e-001, - 2.388682e+000, - -5.842995e+000, - 1.923265e+001, - 1.887735e+001, - 1.485698e+001, - // albedo 0, turbidity 6 - 8.563688e-001, - 2.391534e+000, - -5.769133e+000, - 1.828709e+001, - 2.097209e+001, - 1.469587e+001, - // albedo 0, turbidity 7 - 8.270533e-001, - 2.342790e+000, - -5.558071e+000, - 1.684993e+001, - 2.356498e+001, - 1.505975e+001, - // albedo 0, turbidity 8 - 7.908339e-001, - 2.190341e+000, - -4.852571e+000, - 1.374862e+001, - 2.806846e+001, - 1.548444e+001, - // albedo 0, turbidity 9 - 7.403619e-001, - 1.783998e+000, - -2.983854e+000, - 7.622563e+000, - 3.507610e+001, - 1.615805e+001, - // albedo 0, turbidity 10 - 6.840111e-001, - 1.154457e+000, - -2.393830e-001, - -7.896893e-001, - 4.282765e+001, - 1.779469e+001, - // albedo 1, turbidity 1 - 1.168300e+000, - 1.860993e+000, - -2.129074e+000, - 1.251952e+001, - 3.032499e+001, - 2.938716e+001, - // albedo 1, turbidity 2 - 1.150338e+000, - 1.918813e+000, - -2.413527e+000, - 1.274862e+001, - 3.087134e+001, - 2.951432e+001, - // albedo 1, turbidity 3 - 1.114719e+000, - 1.964689e+000, - -2.625423e+000, - 1.247837e+001, - 3.237949e+001, - 2.943596e+001, - // albedo 1, turbidity 4 - 1.077948e+000, - 2.006292e+000, - -2.846934e+000, - 1.190195e+001, - 3.459293e+001, - 2.937492e+001, - // albedo 1, turbidity 5 - 1.035143e+000, - 1.986681e+000, - -2.752584e+000, - 1.060972e+001, - 3.722185e+001, - 2.918594e+001, - // albedo 1, turbidity 6 - 1.015992e+000, - 1.992054e+000, - -2.812626e+000, - 1.001416e+001, - 3.847300e+001, - 2.924624e+001, - // albedo 1, turbidity 7 - 9.756887e-001, - 1.939897e+000, - -2.533281e+000, - 8.319176e+000, - 4.083907e+001, - 2.925586e+001, - // albedo 1, turbidity 8 - 9.264164e-001, - 1.716454e+000, - -1.597044e+000, - 4.739725e+000, - 4.507683e+001, - 2.878915e+001, - // albedo 1, turbidity 9 - 8.595191e-001, - 1.346034e+000, - -2.801895e-002, - -6.582906e-001, - 5.017523e+001, - 2.852953e+001, - // albedo 1, turbidity 10 - 7.754116e-001, - 7.709245e-001, - 2.200201e+000, - -7.487661e+000, - 5.436622e+001, - 2.893432e+001, -}; - -double* datasetsRGB[] = -{ - datasetRGB1, - datasetRGB2, - datasetRGB3 -}; - -double* datasetsRGBRad[] = -{ - datasetRGBRad1, - datasetRGBRad2, - datasetRGBRad3 -}; +/* +This source is published under the following 3-clause BSD license. + +Copyright (c) 2012 - 2013, Lukas Hosek and Alexander Wilkie +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * None of the names of the contributors may be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + + +/* ============================================================================ + +This file is part of a sample implementation of the analytical skylight and +solar radiance models presented in the SIGGRAPH 2012 paper + + + "An Analytic Model for Full Spectral Sky-Dome Radiance" + +and the 2013 IEEE CG&A paper + + "Adding a Solar Radiance Function to the Hosek Skylight Model" + + both by + + Lukas Hosek and Alexander Wilkie + Charles University in Prague, Czech Republic + + + Version: 1.4a, February 22nd, 2013 + +Version history: + +1.4a February 22nd, 2013 + Removed unnecessary and counter-intuitive solar radius parameters + from the interface of the colourspace sky dome initialisation functions. + +1.4 February 11th, 2013 + Fixed a bug which caused the relative brightness of the solar disc + and the sky dome to be off by a factor of about 6. The sun was too + bright: this affected both normal and alien sun scenarios. The + coefficients of the solar radiance function were changed to fix this. + +1.3 January 21st, 2013 (not released to the public) + Added support for solar discs that are not exactly the same size as + the terrestrial sun. Also added support for suns with a different + emission spectrum ("Alien World" functionality). + +1.2a December 18th, 2012 + Fixed a mistake and some inaccuracies in the solar radiance function + explanations found in ArHosekSkyModel.h. The actual source code is + unchanged compared to version 1.2. + +1.2 December 17th, 2012 + Native RGB data and a solar radiance function that matches the turbidity + conditions were added. + +1.1 September 2012 + The coefficients of the spectral model are now scaled so that the output + is given in physical units: W / (m^-2 * sr * nm). Also, the output of the + XYZ model is now no longer scaled to the range [0...1]. Instead, it is + the result of a simple conversion from spectral data via the CIE 2 degree + standard observer matching functions. Therefore, after multiplication + with 683 lm / W, the Y channel now corresponds to luminance in lm. + +1.0 May 11th, 2012 + Initial release. + + +Please visit http://cgg.mff.cuni.cz/projects/SkylightModelling/ to check if +an updated version of this code has been published! + +============================================================================ */ + + +/* + +This file contains the coefficient data for the RGB colour space version of +the model. + +*/ + +// uses Aug 23 dataset + +double datasetRGB1[] = +{ + // albedo 0, turbidity 1 + -1.099459e+000, + -1.335146e-001, + -4.083223e+000, + 5.919603e+000, + -1.104166e-001, + 1.600158e+000, + -1.326538e-006, + 4.917807e+000, + 5.127716e-001, + -1.169858e+000, + -1.832793e-001, + 9.694744e-001, + 9.495762e-002, + -4.738918e-002, + 2.194171e-001, + 1.095749e-001, + 3.603604e+000, + 3.815119e-001, + -9.665225e-001, + -1.403888e-001, + 5.194457e+000, + -1.107607e+000, + -8.135181e-001, + 4.969661e+000, + -2.300508e-001, + -2.489350e+000, + 1.279158e+000, + -1.292508e+000, + -1.299552e-001, + -2.071404e+000, + -4.752482e-002, + 1.215598e+000, + -1.904179e+000, + 3.027985e-001, + 8.707768e+000, + 6.332446e-002, + -9.264666e-001, + -1.696780e-001, + 4.574070e+000, + -4.232936e-001, + -7.575833e+000, + 5.079755e+000, + -2.576343e-001, + -4.506805e+000, + 6.908129e-001, + -1.139072e+000, + -1.796056e-001, + 1.923311e+000, + 6.788529e+000, + -2.364389e+000, + -1.064041e+000, + 1.717010e-001, + 1.534681e+000, + 5.015810e-001, + // albedo 0, turbidity 2 + -1.107257e+000, + -1.384411e-001, + -4.285744e+000, + 5.713157e+000, + -1.015992e-001, + 1.372638e+000, + 6.555893e-002, + 5.127514e+000, + 6.550471e-001, + -1.187337e+000, + -1.969013e-001, + 8.551048e-001, + 5.289708e-002, + -7.626406e-002, + 1.733153e-002, + 1.779454e-001, + 3.801038e+000, + 4.742709e-001, + -9.685321e-001, + -1.553308e-001, + 4.732492e+000, + -1.178935e+000, + -7.852791e-001, + 4.604492e+000, + -2.666518e-001, + -2.367663e+000, + 1.177527e+000, + -1.252817e+000, + -5.129949e-002, + -2.800433e+000, + -1.295992e-002, + 1.308964e+000, + -2.204331e+000, + 7.276011e-001, + 8.699265e+000, + 1.188388e-001, + -9.459509e-001, + -2.322133e-001, + 4.375041e+000, + -1.712018e-001, + -7.451681e+000, + 5.078019e+000, + -4.223538e-001, + -4.595561e+000, + 1.074719e+000, + -1.125092e+000, + -1.796750e-001, + 1.626399e+000, + 6.989743e+000, + -2.406382e+000, + -9.060383e-001, + 2.961611e-001, + 1.337715e+000, + 5.438140e-001, + // albedo 0, turbidity 3 + -1.135338e+000, + -1.716160e-001, + -1.499253e+000, + 2.373491e+000, + -1.654023e-001, + 9.566404e-001, + 1.113453e-001, + 4.528473e+000, + 6.579439e-001, + -1.132780e+000, + -1.456214e-001, + -1.736672e+000, + 1.756589e+000, + -1.087003e-001, + 3.757927e-001, + 2.525070e-001, + 7.178513e+000, + 5.003814e-001, + -1.167176e+000, + -2.927225e-001, + 5.727667e+000, + -3.139244e+000, + -6.425204e-001, + 2.822634e+000, + -1.457812e-001, + -6.787080e+000, + 1.017072e+000, + -1.042529e+000, + 4.110823e-002, + -4.000629e+000, + 4.362364e+000, + 1.090540e+000, + -1.338674e+000, + 8.246964e-001, + 1.095249e+001, + 2.912211e-001, + -1.061598e+000, + -2.096143e-001, + 3.803155e+000, + -7.977069e+000, + -3.637880e+000, + 3.707671e+000, + -1.903128e-001, + -3.397953e+000, + 9.971500e-001, + -1.073560e+000, + -2.077964e-001, + 1.492052e+000, + 1.626322e+001, + -5.015304e+000, + -4.059889e-001, + 2.659782e-001, + 6.395380e-001, + 5.634436e-001, + // albedo 0, turbidity 4 + -1.172794e+000, + -2.111186e-001, + -1.360013e+000, + 1.604080e+000, + -8.473723e-002, + 7.217312e-001, + 1.548030e-001, + 4.257010e+000, + 6.328974e-001, + -1.238374e+000, + -2.670827e-001, + 3.247678e-001, + 5.466311e-001, + -7.425952e-001, + 5.276440e-001, + 2.678026e-002, + 5.484169e+000, + 6.814734e-001, + -1.176923e+000, + -2.574586e-001, + 2.304045e+000, + -2.797678e+000, + 1.464405e+000, + 1.998552e+000, + 2.550559e-001, + -4.199772e+000, + 7.544892e-001, + -1.003284e+000, + 1.943984e-002, + -2.145066e+000, + 1.030924e+001, + -1.525413e+001, + -2.023010e+000, + 5.448699e-001, + 8.159497e+000, + 5.539148e-001, + -1.060017e+000, + -2.037206e-001, + 2.483018e+000, + -4.595459e+000, + 6.526991e+000, + 4.031804e+000, + 1.206513e-001, + -2.586527e+000, + 7.875752e-001, + -1.081141e+000, + -2.123302e-001, + 1.092275e+000, + 2.683841e+000, + -4.166938e+000, + -1.396582e+000, + 4.371205e-001, + 1.030233e+000, + 6.664862e-001, + // albedo 0, turbidity 5 + -1.222392e+000, + -2.651924e-001, + -4.625037e-001, + 3.521964e-001, + 2.148855e-002, + 5.078494e-001, + 1.791590e-001, + 3.852516e+000, + 5.998216e-001, + -1.424610e+000, + -4.710155e-001, + -1.826815e-001, + 1.786277e+000, + -1.952442e+000, + 5.277612e-001, + -1.773629e-002, + 2.415874e+000, + 6.701272e-001, + -1.130655e+000, + -1.358609e-001, + 9.171203e-001, + -4.660394e+000, + 6.251162e+000, + 1.904529e+000, + 2.639668e-001, + 1.856130e+000, + 8.228440e-001, + -9.739015e-001, + -6.674749e-002, + -4.768897e-001, + 1.248589e+001, + -1.994688e+001, + -2.353043e+000, + 5.885575e-001, + 1.287251e+000, + 4.830135e-001, + -1.082178e+000, + -1.974495e-001, + 1.050245e+000, + -4.792855e+000, + 8.663406e+000, + 3.246969e+000, + 1.556731e-001, + 8.117442e-001, + 8.050376e-001, + -1.063354e+000, + -1.727108e-001, + 9.681592e-001, + 2.736077e+000, + -4.969269e+000, + -8.360570e-001, + 5.994612e-001, + 1.024039e+000, + 6.786935e-001, + // albedo 0, turbidity 6 + -1.261936e+000, + -3.053676e-001, + -4.262222e-001, + 4.000196e-001, + -2.059388e-002, + 4.721802e-001, + 1.480028e-001, + 3.505343e+000, + 6.121337e-001, + -1.681088e+000, + -6.971919e-001, + -1.105652e-001, + 7.437426e-001, + -6.594399e-001, + 2.254221e-001, + 8.710195e-002, + 1.263913e+000, + 5.681865e-001, + -9.453001e-001, + 3.460388e-002, + 6.067038e-001, + -1.985128e+000, + 3.457236e+000, + 2.655483e+000, + -1.162354e-002, + 3.304716e+000, + 1.001950e+000, + -1.086609e+000, + -2.029011e-001, + -6.399170e-001, + 6.926885e+000, + -1.512189e+001, + -3.793051e+000, + 9.456120e-001, + 2.222222e-001, + 2.893725e-001, + -1.041259e+000, + -1.388790e-001, + 1.147331e+000, + 6.282086e+000, + 3.679836e+000, + 4.398314e+000, + -1.355232e-001, + 1.031134e+000, + 9.273509e-001, + -1.063473e+000, + -1.916051e-001, + 6.556979e-001, + -3.371891e-003, + -3.699664e+000, + -1.926783e+000, + 7.371154e-001, + 1.179975e+000, + 6.367068e-001, + // albedo 0, turbidity 7 + -1.336390e+000, + -3.778927e-001, + -7.259477e-001, + 2.270247e-001, + 4.627513e-001, + 1.366459e-001, + 2.637347e-001, + 3.292059e+000, + 4.998211e-001, + -2.119878e+000, + -1.055472e+000, + 5.422052e-001, + 7.826648e-001, + -1.286065e+000, + 9.517905e-001, + -1.432358e-001, + -2.379816e-001, + 5.910513e-001, + -7.761432e-001, + 2.124336e-001, + -6.845184e-001, + -9.812342e-001, + 4.347257e+000, + 9.671980e-001, + 3.773150e-001, + 5.789529e+000, + 9.646598e-001, + -1.118734e+000, + -3.513815e-001, + 5.500918e-001, + 9.449627e-001, + -1.262070e+001, + -1.825280e+000, + 4.731260e-001, + -3.326892e+000, + 3.568768e-001, + -1.026437e+000, + -8.257946e-002, + 3.221701e-001, + 1.198372e+001, + 1.555130e+000, + 2.560304e+000, + 1.406465e-001, + 2.912858e+000, + 8.643181e-001, + -1.069949e+000, + -2.029607e-001, + 5.825042e-001, + -2.398595e-003, + -3.278335e+000, + -1.349882e+000, + 7.208433e-001, + 8.505164e-001, + 6.625391e-001, + // albedo 0, turbidity 8 + -1.392309e+000, + -4.454945e-001, + -5.664000e-001, + 6.283393e-001, + -3.761727e-001, + 6.949802e-001, + 7.748178e-002, + 3.192797e+000, + 5.968661e-001, + -2.713405e+000, + -1.395112e+000, + 2.029230e-001, + 1.877272e-001, + -3.715859e-001, + -1.652929e-001, + 2.385861e-001, + -4.150768e-001, + 1.375467e-001, + -9.588644e-001, + 2.433900e-002, + -1.527493e+000, + -9.632874e-001, + 5.496269e+000, + 1.094931e+000, + 2.004044e-001, + 6.084554e+000, + 1.369604e+000, + -8.028546e-001, + -2.473563e-001, + 1.617898e+000, + 2.073591e+000, + -1.149446e+001, + -8.394131e-001, + 2.726847e-001, + -4.634538e+000, + 1.367293e-001, + -1.198326e+000, + -1.804865e-001, + -3.565414e-001, + 4.073200e+000, + 1.662086e+000, + 1.239770e+000, + 3.367978e-001, + 2.997402e+000, + 9.360383e-001, + -1.013531e+000, + -1.859060e-001, + 5.799857e-001, + 1.331883e+001, + -4.346873e+000, + -1.113820e+000, + 5.275714e-001, + 8.045177e-001, + 6.496373e-001, + // albedo 0, turbidity 9 + -1.530103e+000, + -6.107468e-001, + -3.841771e-001, + 1.881508e+000, + -1.464807e+000, + 6.654690e-001, + -5.950797e-006, + 2.738912e+000, + 8.101012e-001, + -2.415469e+000, + -1.057499e+000, + -4.161968e-001, + -2.357548e+000, + 6.300296e-001, + 6.224915e-001, + 1.545048e-002, + 2.038561e+000, + -1.339415e-001, + -3.096796e+000, + -1.465688e+000, + -1.199232e+000, + 4.567061e+000, + 3.260980e+000, + -9.794907e-001, + 8.950491e-001, + 2.049235e+000, + 1.331015e+000, + 2.713904e-001, + 2.852852e-001, + 1.202090e+000, + -8.206784e+000, + -5.805762e+000, + 1.804431e+000, + -6.090648e-001, + -1.990902e+000, + 3.288858e-001, + -1.456580e+000, + -3.455960e-001, + -6.409257e-002, + 1.667697e+001, + -2.311094e+000, + -9.771104e-001, + 6.759863e-001, + 1.245136e+000, + 7.911932e-001, + -9.860389e-001, + -2.099564e-001, + 2.946650e-001, + -3.547800e-003, + -2.268313e+000, + -6.205647e-002, + 4.705185e-001, + 8.657995e-001, + 6.856284e-001, + // albedo 0, turbidity 10 + -1.971736e+000, + -9.414047e-001, + -3.400557e-001, + 1.468763e+000, + -1.474284e+000, + 5.501062e-001, + -1.109750e-005, + 2.356370e+000, + 9.001702e-001, + -1.589845e+000, + -7.797079e-001, + -5.582240e-001, + -8.137376e-001, + 5.846617e-001, + 1.129459e-001, + -2.658005e-002, + 2.707248e+000, + -2.112486e-001, + -6.940173e+000, + -2.823963e+000, + -1.620848e+000, + 1.090696e+000, + 2.391730e+000, + 1.370047e+000, + 5.890462e-001, + 1.728400e+000, + 1.331253e+000, + 1.293144e+000, + -1.919778e-003, + 1.644206e+000, + -8.666967e-001, + -7.161953e+000, + -1.385018e+000, + -1.505374e-001, + -1.388643e+000, + 2.530122e-001, + -1.488880e+000, + -2.495496e-001, + -2.377137e-001, + 1.167714e+001, + -8.617124e-001, + 1.053828e+000, + 1.992744e-001, + 3.633564e-001, + 8.553304e-001, + -1.060891e+000, + -4.035829e-001, + 2.823207e-001, + -2.369798e-003, + -1.876577e+000, + -5.950265e-001, + 4.241017e-001, + 3.140802e-001, + 6.631669e-001, + // albedo 1, turbidity 1 + -1.101204e+000, + -1.351353e-001, + -4.030882e+000, + 6.096353e+000, + -1.148599e-001, + 1.606507e+000, + -1.555474e-006, + 4.436084e+000, + 5.973715e-001, + -1.154597e+000, + -1.923378e-001, + 8.512132e-001, + 2.934895e-001, + -6.522777e-002, + 1.389077e-001, + 9.091469e-002, + 3.133307e+000, + 2.108541e-001, + -1.031588e+000, + -1.546804e-001, + 5.266214e+000, + -9.491390e-001, + -7.184867e-001, + 4.875626e+000, + -1.911907e-001, + -2.865642e+000, + 1.087895e+000, + -1.159454e+000, + -9.546699e-002, + -1.508146e+000, + -2.031411e-002, + 1.040653e+000, + -2.333508e+000, + 2.540592e-001, + 8.594981e+000, + 9.316770e-002, + -1.035940e+000, + -2.021151e-001, + 4.719343e+000, + -9.019318e-001, + -7.858046e+000, + 3.901234e+000, + -2.233137e-001, + -4.344739e+000, + 6.550733e-001, + -1.096669e+000, + -1.558196e-001, + 2.057553e+000, + 6.274495e+000, + -2.678352e+000, + -1.814927e+000, + 1.550676e-001, + 1.903276e+000, + 4.998989e-001, + // albedo 1, turbidity 2 + -1.114209e+000, + -1.473531e-001, + -7.602914e+000, + 8.973685e+000, + -4.980074e-002, + 1.289198e+000, + 8.366906e-002, + 4.557987e+000, + 6.118757e-001, + -1.149397e+000, + -1.981628e-001, + 4.914096e+000, + -3.498986e+000, + -6.257090e-002, + 1.667401e-001, + 1.048980e-001, + 2.284689e+000, + 5.935965e-001, + -1.056121e+000, + -1.456172e-001, + 4.272656e-001, + 2.912649e+000, + -5.501745e-001, + 4.406542e+000, + -1.387680e-001, + 1.245555e+000, + 9.733011e-001, + -1.125047e+000, + -4.003662e-002, + 1.058457e+000, + -3.462236e+000, + 4.395278e-001, + -2.395805e+000, + 5.177589e-001, + 4.866247e+000, + 4.253189e-001, + -1.051444e+000, + -2.804541e-001, + 3.364668e+000, + 3.293787e+000, + -1.015741e+001, + 3.807407e+000, + -3.592377e-001, + -3.367415e+000, + 7.900825e-001, + -1.093847e+000, + -1.436965e-001, + 2.384780e+000, + 5.787070e+000, + -2.445987e+000, + -1.311171e+000, + 2.326563e-001, + 1.158439e+000, + 5.555416e-001, + // albedo 1, turbidity 3 + -1.134824e+000, + -1.680468e-001, + -3.325620e+000, + 4.458596e+000, + -1.135063e-001, + 1.104500e+000, + 7.794544e-002, + 4.609952e+000, + 6.854854e-001, + -1.143017e+000, + -1.565926e-001, + 3.014687e-001, + -1.763027e-001, + -3.557925e-002, + -2.342406e-001, + 2.528705e-001, + 5.884085e+000, + 4.750602e-001, + -1.136801e+000, + -2.907502e-001, + 3.682423e+000, + -4.061202e-001, + -8.728159e-001, + 4.001510e+000, + -1.522202e-001, + -5.528713e+000, + 1.044847e+000, + -1.063652e+000, + 7.808107e-002, + -1.983678e+000, + 3.648078e-001, + 2.102276e+000, + -3.065050e+000, + 8.431951e-001, + 1.038830e+001, + 2.662834e-001, + -1.061015e+000, + -2.859814e-001, + 4.223615e+000, + -2.290138e+000, + -8.314010e+000, + 4.405718e+000, + -4.613627e-001, + -4.502910e+000, + 1.008383e+000, + -1.106302e+000, + -1.697123e-001, + 2.087196e+000, + 8.238929e+000, + -2.992416e+000, + -1.821776e+000, + 3.434859e-001, + 7.755179e-001, + 5.341190e-001, + // albedo 1, turbidity 4 + -1.171110e+000, + -2.106304e-001, + -1.614361e+000, + 2.378103e+000, + -1.625969e-001, + 8.504483e-001, + 1.059312e-001, + 4.046256e+000, + 6.618227e-001, + -1.200480e+000, + -2.235733e-001, + 1.014390e+000, + -1.174074e+000, + -4.440180e-001, + 2.262406e-001, + 1.665868e-001, + 5.461829e+000, + 5.676310e-001, + -1.223587e+000, + -3.502622e-001, + 1.699106e+000, + 6.724266e-001, + 1.268567e+000, + 2.135102e+000, + 8.039374e-004, + -5.221111e+000, + 9.445690e-001, + -9.452673e-001, + 1.468459e-001, + -1.335034e+000, + 4.346628e+000, + -1.285652e+001, + -1.807046e+000, + 8.175243e-001, + 9.301065e+000, + 3.656798e-001, + -1.134681e+000, + -3.310951e-001, + 3.571244e+000, + -2.208948e+000, + 6.041580e+000, + 3.107577e+000, + -3.112127e-001, + -4.186351e+000, + 9.188333e-001, + -1.083237e+000, + -1.831394e-001, + 2.062654e+000, + 1.385424e+000, + -5.004950e+000, + -1.332669e+000, + 3.627352e-001, + 3.323150e-001, + 6.191181e-001, + // albedo 1, turbidity 5 + -1.211527e+000, + -2.590617e-001, + -1.660874e-001, + 3.627905e-001, + -1.039258e-001, + 4.697924e-001, + 1.671653e-001, + 3.507497e+000, + 6.022506e-001, + -1.433017e+000, + -4.733592e-001, + 1.724445e-001, + 9.953236e-001, + -1.874457e+000, + 4.432099e-001, + 1.715810e-002, + 2.339272e+000, + 6.441470e-001, + -1.084920e+000, + -1.587903e-001, + 8.999585e-001, + -2.537516e+000, + 5.877859e+000, + 2.014554e+000, + 9.689141e-002, + 3.177242e-001, + 9.030399e-001, + -1.008242e+000, + 2.793030e-003, + -3.507469e-001, + 1.028300e+001, + -2.080454e+001, + -2.781026e+000, + 8.995090e-001, + 3.366951e+000, + 3.473867e-001, + -1.103151e+000, + -2.799598e-001, + 2.525791e+000, + -4.255704e+000, + 9.903388e+000, + 3.722668e+000, + -3.603941e-001, + -1.303292e+000, + 9.369454e-001, + -1.102235e+000, + -2.025061e-001, + 2.085660e+000, + 1.686787e+000, + -5.010957e+000, + -1.656458e+000, + 4.584029e-001, + -2.751759e-001, + 6.184162e-001, + // albedo 1, turbidity 6 + -1.256130e+000, + -3.104904e-001, + 1.639350e-001, + 1.315502e-001, + -7.297583e-001, + 4.778480e-001, + 1.259265e-001, + 3.012108e+000, + 6.202728e-001, + -1.620114e+000, + -6.552670e-001, + -2.877157e-001, + 1.094371e+000, + 2.818914e-001, + 3.696830e-001, + 9.428521e-002, + 1.450951e+000, + 5.681308e-001, + -9.686204e-001, + -3.755647e-002, + 1.469980e+000, + -3.103414e+000, + 2.856583e+000, + 1.883209e+000, + -5.746099e-002, + 1.286383e+000, + 1.001751e+000, + -1.089377e+000, + -1.023062e-001, + -1.498891e+000, + 1.066455e+001, + -1.720184e+001, + -2.759314e+000, + 1.061258e+000, + 2.910211e+000, + 2.624701e-001, + -1.044681e+000, + -2.156857e-001, + 3.230136e+000, + -5.863862e-001, + 6.096640e+000, + 3.550019e+000, + -4.255773e-001, + -1.500033e+000, + 9.687696e-001, + -1.133658e+000, + -2.505101e-001, + 1.717840e+000, + 8.480428e-003, + -5.011789e+000, + -1.740989e+000, + 4.983430e-001, + -2.081829e-001, + 6.088641e-001, + // albedo 1, turbidity 7 + -1.335366e+000, + -3.863319e-001, + -5.279971e-001, + 3.638324e-001, + 3.230699e-001, + 8.339707e-002, + 2.483293e-001, + 2.678646e+000, + 4.998346e-001, + -2.004511e+000, + -9.957121e-001, + 1.250807e+000, + 1.625025e-002, + -3.410754e-001, + 7.858244e-001, + -9.506757e-002, + 2.651876e-002, + 5.788643e-001, + -8.714157e-001, + 1.192051e-001, + -8.486879e-001, + -3.702497e-001, + 1.818277e+000, + 1.103427e+000, + 2.454866e-001, + 3.841575e+000, + 9.847350e-001, + -1.042618e+000, + -2.285793e-001, + 3.620175e-001, + 2.983368e+000, + -9.776844e+000, + -1.971587e+000, + 6.691674e-001, + -7.901947e-001, + 3.213200e-001, + -1.099112e+000, + -1.869868e-001, + 2.044065e+000, + 2.062964e+000, + 1.265668e+000, + 2.710130e+000, + -1.099443e-001, + 2.179353e-001, + 9.024108e-001, + -1.106985e+000, + -2.396881e-001, + 1.809807e+000, + 8.523319e+000, + -5.011788e+000, + -1.590086e+000, + 3.248449e-001, + -1.003187e-001, + 6.550606e-001, + // albedo 1, turbidity 8 + -1.421285e+000, + -4.767024e-001, + -3.885004e-001, + 8.274590e-001, + -3.644229e-001, + 6.999513e-001, + 5.196710e-002, + 2.578431e+000, + 6.246310e-001, + -2.611217e+000, + -1.398846e+000, + 4.527425e-001, + -5.932142e-001, + 2.224617e-001, + -5.593581e-001, + 3.389633e-001, + -7.767112e-001, + 6.536004e-002, + -9.881543e-001, + 4.684782e-002, + -8.616613e-001, + 8.799807e-001, + 4.003130e+000, + 1.739543e+000, + -8.098378e-002, + 5.524802e+000, + 1.499673e+000, + -7.544759e-001, + -2.314808e-001, + 8.125770e-001, + -7.724135e-001, + -9.577645e+000, + -1.629433e+000, + 6.790832e-001, + -4.193895e+000, + -2.526624e-002, + -1.273719e+000, + -2.187030e-001, + 1.401798e+000, + 5.231832e+000, + 7.405093e-001, + 1.775166e+000, + -7.269476e-002, + 1.996087e+000, + 1.057450e+000, + -1.046864e+000, + -2.247559e-001, + 1.679449e+000, + 1.140057e+001, + -4.948829e+000, + -1.182664e+000, + 3.241038e-001, + -2.470012e-001, + 6.115900e-001, + // albedo 1, turbidity 9 + -1.514607e+000, + -5.985430e-001, + -1.877610e-001, + 1.756930e+000, + -1.314206e+000, + 6.115810e-001, + -5.970460e-006, + 2.412975e+000, + 8.124304e-001, + -2.308414e+000, + -1.083797e+000, + -1.179959e-001, + -1.728246e+000, + 7.784742e-001, + 5.494505e-001, + 6.203168e-003, + 9.326251e-001, + -1.419518e-001, + -3.230837e+000, + -1.438670e+000, + -9.868286e-001, + 2.974393e+000, + 1.949339e+000, + -6.337857e-001, + 8.160271e-001, + 3.278606e+000, + 1.354373e+000, + 5.149378e-001, + 2.754789e-001, + 1.040965e+000, + -4.501186e+000, + -3.399057e+000, + 9.661861e-001, + -4.736173e-001, + -4.037574e+000, + 2.794847e-001, + -1.621870e+000, + -3.192763e-001, + 8.786242e-001, + 9.785565e+000, + -2.727652e+000, + 1.903691e-002, + 5.521261e-001, + 2.138764e+000, + 8.419871e-001, + -9.951701e-001, + -2.550607e-001, + 1.498952e+000, + -2.737197e-003, + -3.101832e+000, + -5.921329e-001, + 2.864422e-001, + -4.405218e-001, + 6.631410e-001, + // albedo 1, turbidity 10 + -1.902954e+000, + -9.056918e-001, + -2.069570e-001, + 1.191499e+000, + -1.092577e+000, + 5.849556e-001, + -9.649602e-006, + 2.048407e+000, + 9.001527e-001, + -1.271627e+000, + -7.193923e-001, + -1.136606e-002, + -1.167951e-001, + 3.286175e-003, + -5.262827e-002, + -2.473874e-002, + 1.716125e+000, + -2.187133e-001, + -7.647175e+000, + -3.114129e+000, + -1.490128e+000, + -5.266488e-001, + 3.063090e+000, + 1.474262e+000, + 5.481458e-001, + 2.052174e+000, + 1.353089e+000, + 2.191403e+000, + 3.421120e-001, + 1.446510e+000, + 2.170943e+000, + -7.768187e+000, + -1.471207e+000, + -1.456708e-001, + -1.753574e+000, + 2.310576e-001, + -1.932296e+000, + -3.814739e-001, + 6.245422e-001, + 6.748294e+000, + -3.060171e-001, + 1.067747e+000, + 2.500671e-001, + -1.252596e-001, + 8.614611e-001, + -9.471101e-001, + -4.052640e-001, + 1.300174e+000, + -3.951536e-003, + -1.908284e+000, + -5.385721e-001, + 2.133578e-001, + -6.250292e-001, + 6.658012e-001, +}; + +double datasetRGBRad1[] = +{ + // albedo 0, turbidity 1 + 1.962684e+000, + 1.159831e+000, + 4.450588e+000, + 5.079633e+000, + 4.437388e+000, + 4.324573e+000, + // albedo 0, turbidity 2 + 1.946487e+000, + 1.287515e+000, + 3.703696e+000, + 8.782833e+000, + 3.440437e+000, + 5.160333e+000, + // albedo 0, turbidity 3 + 1.882170e+000, + 1.335878e+000, + 2.648641e+000, + 1.358368e+001, + 3.105473e+000, + 5.907387e+000, + // albedo 0, turbidity 4 + 1.738159e+000, + 1.624289e+000, + -8.786695e-003, + 2.118253e+001, + 2.770255e+000, + 7.055672e+000, + // albedo 0, turbidity 5 + 1.571896e+000, + 2.301786e+000, + -4.028545e+000, + 2.966806e+001, + 1.630876e+000, + 8.711031e+000, + // albedo 0, turbidity 6 + 1.475048e+000, + 2.679086e+000, + -6.311315e+000, + 3.377896e+001, + 2.140975e+000, + 9.385283e+000, + // albedo 0, turbidity 7 + 1.326174e+000, + 3.378759e+000, + -9.831444e+000, + 3.942061e+001, + 2.852702e+000, + 1.082542e+001, + // albedo 0, turbidity 8 + 1.153344e+000, + 3.967771e+000, + -1.265181e+001, + 4.195016e+001, + 7.468239e+000, + 1.221350e+001, + // albedo 0, turbidity 9 + 9.746081e-001, + 4.051626e+000, + -1.298454e+001, + 3.754964e+001, + 1.749232e+001, + 1.420619e+001, + // albedo 0, turbidity 10 + 8.448016e-001, + 3.181809e+000, + -8.757338e+000, + 2.197962e+001, + 3.524033e+001, + 1.639549e+001, + // albedo 1, turbidity 1 + 2.029623e+000, + 1.364434e+000, + 4.201529e+000, + 5.415099e+000, + 9.825839e+000, + 1.063328e+001, + // albedo 1, turbidity 2 + 2.023126e+000, + 1.494728e+000, + 3.420413e+000, + 9.072178e+000, + 9.205157e+000, + 1.186639e+001, + // albedo 1, turbidity 3 + 1.956307e+000, + 1.648665e+000, + 2.039712e+000, + 1.430239e+001, + 9.039526e+000, + 1.330453e+001, + // albedo 1, turbidity 4 + 1.825053e+000, + 1.985022e+000, + -8.036307e-001, + 2.202493e+001, + 9.415361e+000, + 1.517659e+001, + // albedo 1, turbidity 5 + 1.650367e+000, + 2.593201e+000, + -4.469328e+000, + 2.969817e+001, + 9.410977e+000, + 1.744850e+001, + // albedo 1, turbidity 6 + 1.555202e+000, + 2.962925e+000, + -6.608170e+000, + 3.329887e+001, + 1.064559e+001, + 1.850816e+001, + // albedo 1, turbidity 7 + 1.412478e+000, + 3.439403e+000, + -9.196616e+000, + 3.685077e+001, + 1.345341e+001, + 2.003128e+001, + // albedo 1, turbidity 8 + 1.252990e+000, + 3.820805e+000, + -1.115338e+001, + 3.721593e+001, + 2.014916e+001, + 2.182320e+001, + // albedo 1, turbidity 9 + 1.091952e+000, + 3.663027e+000, + -1.031330e+001, + 2.978985e+001, + 3.296835e+001, + 2.375450e+001, + // albedo 1, turbidity 10 + 9.501691e-001, + 2.664579e+000, + -5.545167e+000, + 1.281159e+001, + 5.154768e+001, + 2.574284e+001, +}; + +double datasetRGB2[] = +{ + // albedo 0, turbidity 1 + -1.140530e+000, + -1.982747e-001, + -7.512730e+000, + 8.403899e+000, + -5.699038e-002, + 9.015907e-001, + 3.392161e-002, + 4.772522e+000, + 5.111184e-001, + -1.165117e+000, + -1.852955e-001, + 2.963684e+000, + -2.262274e+000, + -1.571683e-001, + 6.339974e-001, + 4.977879e-002, + 7.243307e+000, + 4.220053e-001, + -1.169936e+000, + -3.357429e-001, + 1.911291e+000, + -2.391074e-001, + -4.791643e-001, + 1.446113e+000, + -9.178108e-002, + -4.700239e+000, + 8.096219e-001, + -1.060246e+000, + -1.051633e-001, + 5.013829e-001, + 2.832309e+000, + -3.707855e-001, + 1.523131e+000, + 9.163749e-002, + 5.604183e+000, + 7.208566e-001, + -1.089753e+000, + -2.382167e-001, + 2.360312e+000, + -5.902562e+000, + -8.799894e+000, + 1.377692e+000, + -6.131633e-002, + -1.415472e+000, + 6.124057e-001, + -1.075481e+000, + -1.242391e-001, + 1.425781e+000, + 8.810319e+000, + -2.922646e+000, + 1.486520e+000, + 3.270580e-002, + 3.889783e+000, + 4.999482e-001, + // albedo 0, turbidity 2 + -1.149342e+000, + -2.076337e-001, + -7.446587e+000, + 8.014559e+000, + -4.866227e-002, + 8.203043e-001, + 6.386483e-002, + 4.894198e+000, + 5.452051e-001, + -1.120531e+000, + -1.513311e-001, + 2.735504e+000, + -2.417591e+000, + -1.361114e-001, + 4.296342e-001, + 9.427488e-002, + 8.171403e+000, + 4.102448e-001, + -1.226964e+000, + -3.516378e-001, + 1.308298e+000, + -5.097487e-002, + -4.846783e-001, + 1.654619e+000, + -1.134940e-001, + -3.347854e+000, + 1.131147e+000, + -9.664377e-001, + 2.767589e-002, + 1.658235e-001, + 2.407439e+000, + -1.300304e-001, + 9.170958e-001, + 2.742895e-001, + 6.642633e+000, + 2.550064e-001, + -1.153358e+000, + -3.126223e-001, + 2.078934e+000, + -5.857733e+000, + -8.659848e+000, + 1.758505e+000, + -9.616094e-002, + -1.230863e+000, + 9.663832e-001, + -1.053850e+000, + -1.330743e-001, + 1.481738e+000, + 1.049485e+001, + -3.528854e+000, + 9.142363e-001, + 1.244880e-001, + 2.644615e+000, + 5.001048e-001, + // albedo 0, turbidity 3 + -1.173687e+000, + -2.360362e-001, + -3.741454e+000, + 4.088507e+000, + -7.528205e-002, + 6.645237e-001, + 7.718265e-002, + 4.651220e+000, + 5.586318e-001, + -1.213757e+000, + -2.589561e-001, + 7.132551e-001, + -4.259327e-001, + -1.980821e-001, + 3.627815e-001, + 4.666560e-002, + 5.807984e+000, + 5.847377e-001, + -1.108794e+000, + -2.259870e-001, + 1.574179e+000, + -3.753731e-001, + -5.984743e-001, + 1.659414e+000, + -1.681021e-002, + 6.785219e-001, + 8.647325e-001, + -1.060896e+000, + -1.346690e-002, + -7.529656e-001, + 1.711319e+000, + -9.792435e-001, + 2.022433e-001, + 3.826487e-001, + 5.725157e+000, + 5.290714e-001, + -1.085145e+000, + -2.840715e-001, + 2.088029e+000, + -4.935097e+000, + -9.056542e+000, + 1.976149e+000, + -3.912485e-002, + -8.636064e-001, + 7.452125e-001, + -1.077983e+000, + -1.416633e-001, + 1.100848e+000, + 1.015875e+001, + -2.943712e+000, + 5.255135e-001, + 2.164224e-001, + 2.941143e+000, + 6.699937e-001, + // albedo 0, turbidity 4 + -1.223293e+000, + -2.867444e-001, + -1.624136e+000, + 1.668299e+000, + -9.537589e-002, + 5.015947e-001, + 1.130741e-001, + 4.244812e+000, + 5.082152e-001, + -1.325342e+000, + -4.280991e-001, + 4.705490e-001, + 6.926592e-002, + -4.572587e-001, + 5.344144e-001, + -2.554192e-002, + 3.093939e+000, + 6.639401e-001, + -1.113581e+000, + -1.192133e-001, + 4.011536e-001, + 7.011889e-001, + 2.052842e-001, + 9.880724e-001, + 1.807533e-002, + 4.690160e+000, + 8.576240e-001, + -1.016063e+000, + -1.038138e-001, + -2.280391e-001, + 7.898918e-001, + -1.127333e+001, + 2.074545e-001, + 5.388182e-001, + 1.364263e+000, + 4.660455e-001, + -1.099582e+000, + -2.228607e-001, + 1.332648e+000, + 5.135188e+000, + 1.653152e+000, + 1.417020e+000, + -1.087532e-001, + 1.809275e+000, + 8.080874e-001, + -1.064357e+000, + -1.520775e-001, + 8.207368e-001, + -1.323565e-003, + -5.009523e+000, + 3.946298e-001, + 4.337902e-001, + 2.593198e+000, + 6.719172e-001, + // albedo 0, turbidity 5 + -1.278702e+000, + -3.512866e-001, + -4.511055e-001, + 3.895760e-001, + -2.429672e-001, + 4.270577e-001, + 1.135348e-001, + 3.719130e+000, + 4.998867e-001, + -1.580069e+000, + -7.095475e-001, + -3.198904e-001, + 1.715748e+000, + -1.185915e+000, + 4.523161e-001, + -1.026159e-002, + 7.927188e-001, + 5.538350e-001, + -9.474023e-001, + 1.173703e-001, + 4.881381e-001, + -2.618684e+000, + 3.251661e+000, + 1.213931e+000, + -1.736274e-002, + 8.000768e+000, + 1.025998e+000, + -1.129091e+000, + -3.287694e-001, + -3.524077e-001, + 3.352892e+000, + -1.416073e+001, + -8.485617e-001, + 6.560766e-001, + -2.820937e+000, + 3.111303e-001, + -1.030884e+000, + -1.137581e-001, + 1.109855e+000, + 8.082276e+000, + 1.519214e+000, + 2.112433e+000, + -1.592299e-001, + 3.675905e+000, + 8.703367e-001, + -1.075192e+000, + -1.627166e-001, + 3.514910e-001, + 1.168164e+000, + -4.255822e+000, + -6.015348e-001, + 6.265776e-001, + 2.884818e+000, + 6.548384e-001, + // albedo 0, turbidity 6 + -1.316017e+000, + -3.889652e-001, + -5.030854e-001, + 4.488704e-001, + -3.186800e-001, + 4.570763e-001, + 8.909201e-002, + 3.659274e+000, + 5.011746e-001, + -1.731876e+000, + -8.493806e-001, + 1.194871e-001, + 2.002781e+000, + -2.006547e+000, + 4.872233e-001, + -2.854606e-002, + 2.662137e-001, + 4.611629e-001, + -9.273680e-001, + 1.380954e-001, + -3.302179e-001, + -3.553265e+000, + 4.633345e+000, + 9.696729e-001, + 8.799775e-002, + 8.291129e+000, + 1.094451e+000, + -1.099377e+000, + -3.325392e-001, + 2.501063e-001, + 2.613712e+000, + -1.328142e+001, + -5.579527e-001, + 4.992081e-001, + -3.504402e+000, + 3.022924e-001, + -1.048420e+000, + -1.227773e-001, + 5.845373e-001, + 1.105869e+001, + 3.813151e-002, + 1.330409e+000, + 1.978131e-002, + 3.959430e+000, + 8.396439e-001, + -1.063233e+000, + -1.560639e-001, + 2.840033e-001, + 8.751565e-001, + -3.411820e+000, + -1.436564e-001, + 5.846580e-001, + 2.899292e+000, + 6.799095e-001, + // albedo 0, turbidity 7 + -1.376715e+000, + -4.541567e-001, + -1.445491e+000, + 1.569898e+000, + -1.390627e-001, + 5.558270e-001, + 4.109877e-002, + 3.349451e+000, + 5.516123e-001, + -1.953391e+000, + -1.035869e+000, + 1.690563e+000, + -1.964690e-001, + -7.787096e-001, + 5.799605e-001, + 2.945626e-002, + 4.217906e-002, + 2.451373e-001, + -1.012422e+000, + 7.136451e-002, + -1.862534e+000, + -7.228653e-001, + 1.947997e-001, + 2.091805e-001, + 6.399233e-002, + 7.928994e+000, + 1.290733e+000, + -9.706708e-001, + -2.880950e-001, + 1.107797e+000, + -2.731734e+000, + -8.445995e+000, + 4.296774e-001, + 5.117648e-001, + -3.824277e+000, + 1.761207e-001, + -1.110611e+000, + -1.789409e-001, + 2.108488e-001, + 2.071430e+001, + -1.763174e+000, + 9.554695e-002, + -2.943103e-002, + 3.422079e+000, + 8.815496e-001, + -1.048334e+000, + -1.614087e-001, + 2.475184e-001, + 2.146938e-002, + -2.983901e+000, + 2.538224e-001, + 5.601370e-001, + 2.461925e+000, + 6.777394e-001, + // albedo 0, turbidity 8 + -1.393719e+000, + -5.002724e-001, + -2.408940e+000, + 2.680983e+000, + -1.362825e-001, + 7.395067e-001, + -3.300343e-006, + 3.260889e+000, + 8.132057e-001, + -2.128663e+000, + -1.151182e+000, + 2.923026e+000, + -1.931838e+000, + -4.426170e-001, + 2.309983e-001, + -5.485890e-003, + 3.279529e-001, + -2.229467e-001, + -1.618022e+000, + -3.766490e-001, + -3.163544e+000, + 1.611608e+000, + -3.967476e-001, + 3.933680e-001, + 3.006742e-001, + 6.835177e+000, + 1.613765e+000, + -5.669064e-001, + -1.481749e-001, + 2.071817e+000, + -8.157422e+000, + -5.988088e+000, + 2.387202e-001, + 1.447191e-001, + -4.296385e+000, + 5.011258e-002, + -1.241724e+000, + -2.519348e-001, + -1.908609e-001, + 2.952235e+001, + -3.333660e+000, + -1.837651e-002, + 1.022249e-001, + 2.929320e+000, + 8.867262e-001, + -1.021670e+000, + -1.667327e-001, + 1.789771e-001, + -2.178108e-003, + -2.641572e+000, + -5.641484e-002, + 5.303758e-001, + 2.138196e+000, + 6.780350e-001, + // albedo 0, turbidity 9 + -1.669332e+000, + -7.588708e-001, + -2.993557e+000, + 3.178760e+000, + -8.066442e-002, + 6.544672e-001, + -8.089880e-006, + 2.628924e+000, + 9.001272e-001, + -1.755806e+000, + -8.735348e-001, + 3.258881e+000, + -2.504785e+000, + -3.300791e-001, + 1.180565e-001, + -9.315982e-003, + 1.785154e+000, + -3.205824e-001, + -3.720277e+000, + -1.733350e+000, + -3.332272e+000, + 1.515869e+000, + 1.734218e-001, + 8.011956e-001, + 1.995440e-001, + 3.817666e+000, + 1.638502e+000, + 4.724641e-001, + 3.209828e-001, + 2.051443e+000, + -5.105574e+000, + -6.509139e+000, + -4.232041e-001, + 2.598931e-001, + -2.151756e+000, + -3.493910e-003, + -1.525600e+000, + -4.897606e-001, + -9.891121e-002, + 2.346818e+001, + -2.278152e+000, + 1.681219e-001, + -4.469389e-002, + 1.051000e+000, + 9.294666e-001, + -9.908649e-001, + -2.008182e-001, + 1.605143e-001, + -2.463113e-003, + -2.477349e+000, + -1.218647e-001, + 4.750121e-001, + 1.460813e+000, + 6.661364e-001, + // albedo 0, turbidity 10 + -2.122119e+000, + -1.125475e+000, + -3.066599e+000, + 3.145078e+000, + -5.411593e-002, + 5.133628e-001, + -7.823408e-006, + 2.268448e+000, + 9.001416e-001, + -1.528158e+000, + -9.370249e-001, + 2.567559e+000, + -1.591439e+000, + -3.634460e-001, + 1.763256e-001, + 1.119624e-003, + 1.811848e+000, + -2.637929e-001, + -6.524387e+000, + -2.673507e+000, + -2.940472e+000, + -6.025609e-001, + 7.852067e-001, + 1.073499e+000, + -3.540435e-002, + 3.517416e+000, + 1.490466e+000, + 8.886026e-001, + -9.681828e-002, + 1.430554e+000, + 4.993717e+000, + -6.071355e+000, + -6.053986e-001, + 5.092997e-001, + -1.273010e+000, + 7.491329e-002, + -1.481997e+000, + -5.897282e-001, + 2.659264e-001, + 1.267239e+000, + -5.741291e-001, + 5.983011e-002, + -2.217312e-001, + -3.016452e-001, + 9.260830e-001, + -1.010943e+000, + -2.075134e-001, + 5.066749e-002, + 1.470708e+001, + -3.780501e+000, + 7.253223e-002, + 4.045458e-001, + 1.320164e+000, + 6.559925e-001, + // albedo 1, turbidity 1 + -1.129907e+000, + -1.884011e-001, + -8.047670e+000, + 9.035776e+000, + -5.539419e-002, + 8.823349e-001, + 3.197135e-002, + 4.839388e+000, + 5.042822e-001, + -1.133821e+000, + -1.510781e-001, + 3.362822e+000, + -2.453381e+000, + -1.463925e-001, + 4.728708e-001, + 5.958140e-002, + 7.636300e+000, + 4.805162e-001, + -1.176518e+000, + -3.549902e-001, + 1.729044e+000, + -2.160966e-001, + -5.075865e-001, + 1.675584e+000, + -8.906902e-002, + -5.386842e+000, + 5.452218e-001, + -1.043563e+000, + -7.520975e-002, + 8.750644e-001, + 2.510518e+000, + 7.584882e-003, + 9.361250e-001, + 7.889083e-002, + 6.066644e+000, + 5.813108e-001, + -1.081304e+000, + -2.222253e-001, + 2.517638e+000, + -4.453820e+000, + -8.663691e+000, + 8.662558e-001, + -4.802657e-002, + -8.965449e-001, + 4.886656e-001, + -1.083774e+000, + -1.375469e-001, + 1.685818e+000, + 5.631120e+000, + -3.100752e+000, + 4.045941e-001, + 2.346895e-002, + 3.390321e+000, + 5.008309e-001, + // albedo 1, turbidity 2 + -1.143158e+000, + -2.058334e-001, + -9.660198e+000, + 1.062394e+001, + -4.434119e-002, + 8.607615e-001, + 3.177325e-002, + 4.416481e+000, + 5.918162e-001, + -1.146773e+000, + -1.727385e-001, + 4.626048e+000, + -4.684602e+000, + -8.307137e-002, + 1.619616e-001, + 1.484866e-001, + 7.572868e+000, + 2.681126e-001, + -1.151324e+000, + -3.099303e-001, + 4.125596e-001, + 2.340752e+000, + -4.214444e-001, + 1.987375e+000, + -1.913410e-001, + -3.845978e+000, + 1.337311e+000, + -1.034258e+000, + -7.778759e-003, + 7.050094e-001, + -8.036369e-001, + 3.138570e-001, + 2.469452e-001, + 3.559970e-001, + 7.485917e+000, + 4.790329e-002, + -1.096568e+000, + -2.673169e-001, + 2.575654e+000, + -8.057121e-001, + -8.884928e+000, + 1.416170e+000, + -2.091315e-001, + -1.543494e+000, + 1.065445e+000, + -1.083304e+000, + -1.528265e-001, + 1.697727e+000, + 2.503702e+000, + -2.885296e+000, + -1.298500e-001, + 1.548870e-001, + 2.479652e+000, + 5.066496e-001, + // albedo 1, turbidity 3 + -1.165736e+000, + -2.329945e-001, + -5.967964e+000, + 6.705959e+000, + -5.931355e-002, + 7.485638e-001, + 3.913878e-002, + 4.221591e+000, + 6.183926e-001, + -1.212422e+000, + -2.545910e-001, + 2.418626e+000, + -2.266104e+000, + -1.102014e-001, + 1.363887e-002, + 1.055411e-001, + 5.648062e+000, + 4.557412e-001, + -1.070436e+000, + -2.163341e-001, + 7.098718e-001, + 7.843075e-001, + -4.323930e-001, + 2.109823e+000, + -9.589700e-002, + -1.985193e-001, + 1.060428e+000, + -1.104879e+000, + -3.013622e-002, + 2.976276e-002, + 1.069707e+000, + 1.410000e-001, + -4.880020e-001, + 4.452288e-001, + 6.418590e+000, + 3.195986e-001, + -1.048969e+000, + -2.655317e-001, + 2.689426e+000, + -3.941038e+000, + -9.506461e+000, + 1.837119e+000, + -1.892124e-001, + -1.562146e+000, + 9.043414e-001, + -1.106145e+000, + -1.601642e-001, + 1.544544e+000, + 7.388492e+000, + -2.924600e+000, + -4.328453e-001, + 1.763161e-001, + 2.523111e+000, + 5.851902e-001, + // albedo 1, turbidity 4 + -1.203666e+000, + -2.776587e-001, + -2.084286e+000, + 2.450840e+000, + -8.746613e-002, + 5.258507e-001, + 7.983316e-002, + 3.860055e+000, + 5.486167e-001, + -1.340448e+000, + -4.230590e-001, + 3.462849e-001, + 4.707607e-001, + -2.512626e-001, + 1.530746e-001, + 2.724218e-002, + 3.035216e+000, + 5.876133e-001, + -1.014554e+000, + -1.168790e-001, + 9.477794e-001, + -1.061218e+000, + -4.196730e-001, + 2.058832e+000, + -5.989624e-002, + 3.058168e+000, + 9.763861e-001, + -1.137388e+000, + -9.854030e-002, + -2.984893e-001, + 3.647820e+000, + -6.585571e-001, + -1.479180e+000, + 6.102932e-001, + 3.265914e+000, + 3.480333e-001, + -1.021816e+000, + -2.344957e-001, + 2.463671e+000, + -7.240685e+000, + -8.862697e+000, + 2.514058e+000, + -2.122768e-001, + -3.313968e-002, + 9.028136e-001, + -1.126581e+000, + -1.874347e-001, + 1.454154e+000, + 1.034398e+001, + -3.237393e+000, + -8.654927e-001, + 2.457248e-001, + 1.845769e+000, + 6.002482e-001, + // albedo 1, turbidity 5 + -1.263727e+000, + -3.439354e-001, + -1.786388e-001, + 3.980166e-001, + -3.349517e-001, + 3.825166e-001, + 1.029225e-001, + 3.331096e+000, + 4.998955e-001, + -1.530010e+000, + -6.879698e-001, + 2.380415e-001, + 1.608216e+000, + -1.682679e+000, + 3.546360e-001, + -3.915220e-003, + 4.517655e-001, + 5.128605e-001, + -9.685659e-001, + 9.480403e-002, + 6.076844e-002, + -3.217561e+000, + 4.568074e+000, + 1.069299e+000, + 2.083638e-002, + 7.301088e+000, + 1.072165e+000, + -1.113925e+000, + -3.112382e-001, + 3.954133e-001, + 5.105907e+000, + -1.456866e+001, + -4.917378e-001, + 5.289909e-001, + -2.678374e+000, + 3.014709e-001, + -1.046864e+000, + -1.215754e-001, + 1.778308e+000, + 4.661489e+000, + 2.565583e-001, + 1.353680e+000, + -1.175767e-001, + 3.415972e+000, + 8.457746e-001, + -1.104480e+000, + -1.940913e-001, + 1.343668e+000, + -1.759206e-003, + -5.009204e+000, + -4.186951e-001, + 3.125710e-001, + 1.628183e+000, + 6.720408e-001, + // albedo 1, turbidity 6 + -1.286902e+000, + -3.781238e-001, + -8.977253e-002, + 3.545393e-001, + -4.866515e-001, + 3.843664e-001, + 8.281675e-002, + 3.122231e+000, + 5.046991e-001, + -1.712597e+000, + -8.549112e-001, + 4.809286e-001, + 1.515398e+000, + -2.212211e+000, + 2.539029e-001, + 2.335997e-002, + -6.089466e-002, + 4.268444e-001, + -8.807283e-001, + 1.646097e-001, + -4.437898e-001, + -3.188247e+000, + 5.984417e+000, + 1.334779e+000, + -4.026975e-002, + 7.546431e+000, + 1.175751e+000, + -1.147253e+000, + -3.538199e-001, + 6.101836e-001, + 4.437780e+000, + -1.559813e+001, + -1.103222e+000, + 6.242039e-001, + -3.091472e+000, + 2.174290e-001, + -1.038230e+000, + -1.213475e-001, + 1.547505e+000, + 5.893176e+000, + 1.368738e+000, + 1.663127e+000, + -1.377130e-001, + 3.185279e+000, + 8.736453e-001, + -1.101026e+000, + -1.874907e-001, + 1.272667e+000, + 3.596524e+000, + -5.007243e+000, + -6.352483e-001, + 3.048985e-001, + 1.931613e+000, + 6.788844e-001, + // albedo 1, turbidity 7 + -1.342753e+000, + -4.384971e-001, + -1.213491e+000, + 1.621399e+000, + -1.551441e-001, + 5.614218e-001, + 2.591739e-002, + 2.958967e+000, + 5.782132e-001, + -1.937684e+000, + -1.066019e+000, + 1.913336e+000, + -7.347719e-001, + -5.916167e-001, + 1.587590e-001, + 1.092568e-001, + -6.275002e-001, + 1.599071e-001, + -9.302391e-001, + 1.486187e-001, + -1.603835e+000, + 1.783713e-001, + 1.100461e+000, + 1.174181e+000, + -1.602361e-001, + 7.868331e+000, + 1.468971e+000, + -1.053631e+000, + -3.727050e-001, + 1.114117e+000, + -9.603286e-001, + -1.062469e+001, + -1.162140e+000, + 7.952797e-001, + -4.478765e+000, + -4.440862e-002, + -1.083629e+000, + -1.261405e-001, + 1.229344e+000, + 1.127825e+001, + 1.319010e-001, + 1.624729e+000, + -2.825898e-001, + 3.661082e+000, + 1.036911e+000, + -1.093950e+000, + -2.067455e-001, + 1.258035e+000, + 7.548645e+000, + -4.598387e+000, + -8.944932e-001, + 3.292634e-001, + 1.311304e+000, + 6.291871e-001, + // albedo 1, turbidity 8 + -1.385867e+000, + -5.068139e-001, + -1.486490e+000, + 1.969049e+000, + -1.698025e-001, + 6.629167e-001, + -5.289365e-006, + 2.760315e+000, + 8.644368e-001, + -2.107367e+000, + -1.175639e+000, + 2.313241e+000, + -1.001653e+000, + -4.843139e-001, + 1.124485e-001, + 3.901494e-005, + -3.502469e-001, + -3.204780e-001, + -1.475244e+000, + -2.833055e-001, + -2.085824e+000, + 1.192563e+000, + -7.645200e-001, + 8.380081e-001, + 2.203580e-001, + 7.157885e+000, + 1.753702e+000, + -6.644372e-001, + -2.549735e-001, + 1.600273e+000, + -8.589034e+000, + -6.144718e+000, + -7.599731e-001, + 2.898370e-001, + -5.770923e+000, + -9.656242e-002, + -1.211687e+000, + -1.653494e-001, + 8.393400e-001, + 2.792988e+001, + -3.395461e+000, + 9.933752e-001, + -3.976877e-002, + 3.776659e+000, + 9.546526e-001, + -1.063757e+000, + -2.037563e-001, + 1.117207e+000, + -1.252806e-003, + -3.332330e+000, + -6.971409e-001, + 3.388719e-001, + 1.311398e+000, + 6.635171e-001, + // albedo 1, turbidity 9 + -1.678889e+000, + -7.992295e-001, + -2.421687e+000, + 2.871029e+000, + -7.662842e-002, + 6.046208e-001, + -7.598099e-006, + 2.002314e+000, + 9.001307e-001, + -1.692144e+000, + -8.804250e-001, + 3.060895e+000, + -2.000009e+000, + -3.183563e-001, + 8.385862e-002, + -6.326713e-003, + 1.206639e+000, + -3.369967e-001, + -3.676795e+000, + -1.719207e+000, + -2.534697e+000, + 1.005285e+000, + 1.550407e-001, + 1.072910e+000, + 1.318094e-001, + 3.717018e+000, + 1.689191e+000, + 5.424542e-001, + 3.263528e-001, + 1.551055e+000, + -3.841058e+000, + -6.598996e+000, + -1.201779e+000, + 3.530669e-001, + -2.542945e+000, + -6.482523e-002, + -1.553849e+000, + -4.576860e-001, + 9.324676e-001, + 1.950982e+001, + -2.344516e+000, + 1.121020e+000, + -1.221537e-001, + 7.285496e-001, + 9.582816e-001, + -1.020650e+000, + -2.215797e-001, + 1.009774e+000, + -2.056855e-003, + -2.740338e+000, + -8.122355e-001, + 3.328967e-001, + 8.982766e-001, + 6.594676e-001, + // albedo 1, turbidity 10 + -2.247360e+000, + -1.221267e+000, + -3.072346e+000, + 3.385139e+000, + -4.387559e-002, + 5.084887e-001, + -7.418833e-006, + 1.750107e+000, + 9.001401e-001, + -1.248499e+000, + -8.442718e-001, + 3.062611e+000, + -2.020314e+000, + -2.815341e-001, + 5.254745e-002, + 3.345008e-003, + 1.433225e+000, + -2.835911e-001, + -7.004119e+000, + -2.927978e+000, + -2.649852e+000, + 7.971894e-001, + 5.466893e-001, + 1.442667e+000, + -6.063912e-002, + 2.806194e+000, + 1.547429e+000, + 1.434882e+000, + 9.114639e-002, + 1.170089e+000, + 3.512808e-002, + -5.861915e+000, + -1.411843e+000, + 5.400486e-001, + -7.746522e-001, + 2.386984e-002, + -1.559053e+000, + -5.502302e-001, + 1.200396e+000, + 1.347741e+001, + -2.344397e+000, + 8.868907e-001, + -3.292661e-001, + -1.362105e+000, + 9.217826e-001, + -1.044436e+000, + -2.360719e-001, + 7.054471e-001, + -2.904518e-003, + -2.092829e+000, + -5.119668e-001, + 4.174861e-001, + 9.687435e-001, + 6.588427e-001, +}; + +double datasetRGBRad2[] = +{ + // albedo 0, turbidity 1 + 1.590330e+000, + 1.355401e+000, + 1.151412e+000, + 1.359116e+001, + 5.857714e+000, + 8.090833e+000, + // albedo 0, turbidity 2 + 1.552540e+000, + 1.510040e+000, + 1.276413e-001, + 1.604643e+001, + 5.912162e+000, + 8.350009e+000, + // albedo 0, turbidity 3 + 1.470871e+000, + 1.880464e+000, + -1.865398e+000, + 2.030808e+001, + 5.471461e+000, + 9.109834e+000, + // albedo 0, turbidity 4 + 1.356563e+000, + 2.373866e+000, + -4.653245e+000, + 2.570922e+001, + 5.686009e+000, + 1.009480e+001, + // albedo 0, turbidity 5 + 1.244232e+000, + 2.851519e+000, + -7.130942e+000, + 2.993449e+001, + 6.382120e+000, + 1.114578e+001, + // albedo 0, turbidity 6 + 1.173693e+000, + 3.120604e+000, + -8.491886e+000, + 3.187393e+001, + 7.290615e+000, + 1.180066e+001, + // albedo 0, turbidity 7 + 1.091845e+000, + 3.368888e+000, + -9.722083e+000, + 3.268508e+001, + 1.032424e+001, + 1.236508e+001, + // albedo 0, turbidity 8 + 9.858985e-001, + 3.500541e+000, + -1.026328e+001, + 3.092956e+001, + 1.610881e+001, + 1.331222e+001, + // albedo 0, turbidity 9 + 8.864993e-001, + 3.172888e+000, + -8.687550e+000, + 2.362161e+001, + 2.621851e+001, + 1.474967e+001, + // albedo 0, turbidity 10 + 7.946973e-001, + 2.189355e+000, + -4.207953e+000, + 9.399091e+000, + 4.062849e+001, + 1.681753e+001, + // albedo 1, turbidity 1 + 1.711696e+000, + 1.657311e+000, + 9.328021e-001, + 1.317880e+001, + 1.506751e+001, + 1.863556e+001, + // albedo 1, turbidity 2 + 1.666968e+000, + 1.849993e+000, + -2.088601e-001, + 1.586653e+001, + 1.486880e+001, + 1.940719e+001, + // albedo 1, turbidity 3 + 1.584846e+000, + 2.170022e+000, + -2.019597e+000, + 1.970826e+001, + 1.490684e+001, + 2.045055e+001, + // albedo 1, turbidity 4 + 1.469412e+000, + 2.524017e+000, + -4.197267e+000, + 2.365249e+001, + 1.664588e+001, + 2.134477e+001, + // albedo 1, turbidity 5 + 1.369714e+000, + 2.843548e+000, + -6.059031e+000, + 2.634993e+001, + 1.881361e+001, + 2.232186e+001, + // albedo 1, turbidity 6 + 1.310477e+000, + 2.984444e+000, + -6.831686e+000, + 2.682340e+001, + 2.123267e+001, + 2.259755e+001, + // albedo 1, turbidity 7 + 1.222552e+000, + 3.176523e+000, + -7.731496e+000, + 2.671760e+001, + 2.484358e+001, + 2.336863e+001, + // albedo 1, turbidity 8 + 1.115781e+000, + 3.130635e+000, + -7.581744e+000, + 2.336531e+001, + 3.171048e+001, + 2.413859e+001, + // albedo 1, turbidity 9 + 1.013181e+000, + 2.699342e+000, + -5.602709e+000, + 1.500158e+001, + 4.217613e+001, + 2.515957e+001, + // albedo 1, turbidity 10 + 8.976323e-001, + 1.726948e+000, + -1.296120e+000, + 1.183675e+000, + 5.503215e+001, + 2.643066e+001, +}; + +double datasetRGB3[] = +{ + // albedo 0, turbidity 1 + -1.372629e+000, + -4.905585e-001, + -4.100789e+001, + 4.122169e+001, + -7.389360e-003, + 4.839359e-001, + 6.474757e-003, + 3.471755e+000, + 5.092936e-001, + -1.523025e+000, + -6.497084e-001, + 6.249857e+000, + -5.662543e+000, + -1.908402e-002, + 5.512810e-001, + -2.181049e-005, + 2.507663e+000, + 4.339598e-001, + -1.035567e+000, + -7.478740e-002, + 9.221030e-001, + -2.140047e+000, + -2.374146e-002, + 3.795517e-001, + -1.769134e-002, + 7.479831e+000, + 7.729303e-001, + -1.271086e+000, + -5.588190e-001, + 6.908023e-001, + 2.096832e+000, + -2.453967e-001, + 1.410648e+000, + 4.475036e-002, + -4.719115e+000, + 5.741186e-001, + -9.712598e-001, + -7.033926e-002, + 9.167274e-001, + -9.502097e-001, + 3.004684e-001, + 4.547054e-001, + -5.929017e-002, + 5.266196e+000, + 7.204135e-001, + -1.087457e+000, + -1.888896e-001, + 8.156686e-001, + 3.101712e-001, + -2.155419e+000, + 1.422205e+000, + 9.692261e-002, + 3.122404e+000, + 4.999430e-001, + // albedo 0, turbidity 2 + -1.425280e+000, + -5.413508e-001, + -3.454883e+001, + 3.481142e+001, + -8.686975e-003, + 4.914268e-001, + -2.479243e-006, + 3.239879e+000, + 6.094201e-001, + -1.688557e+000, + -8.070865e-001, + 7.018459e+000, + -6.244574e+000, + -2.149341e-002, + 3.993971e-001, + 1.252502e-002, + 1.630662e+000, + 1.097860e-001, + -8.664152e-001, + 7.869125e-002, + -5.236535e-001, + -1.218960e+000, + -2.059093e-002, + 6.684898e-001, + -5.584112e-002, + 8.602299e+000, + 1.410496e+000, + -1.319763e+000, + -5.985323e-001, + 1.253918e+000, + 1.914706e+000, + -3.216739e-001, + 9.011213e-001, + 1.324845e-001, + -5.252749e+000, + 6.231252e-002, + -9.706008e-001, + -5.914059e-002, + 5.693150e-001, + -1.175362e+000, + 5.221644e-001, + 7.518213e-001, + -8.247655e-002, + 5.875635e+000, + 9.850863e-001, + -1.085330e+000, + -1.956105e-001, + 8.019605e-001, + 5.338101e-001, + -3.423464e+000, + 1.110444e+000, + 1.507923e-001, + 2.864942e+000, + 4.999481e-001, + // albedo 0, turbidity 3 + -1.431967e+000, + -5.478935e-001, + -3.286288e+001, + 3.305288e+001, + -8.380797e-003, + 4.772050e-001, + -3.044274e-006, + 3.289973e+000, + 5.976303e-001, + -1.801361e+000, + -9.315889e-001, + 5.391756e+000, + -4.588592e+000, + -2.040076e-002, + 4.144684e-001, + 1.814534e-002, + 1.051795e+000, + 1.145651e-001, + -7.905357e-001, + 1.451332e-001, + -1.605661e-001, + -1.592174e+000, + 4.561348e-004, + 3.380323e-001, + -7.770275e-002, + 8.775384e+000, + 1.489512e+000, + -1.308575e+000, + -5.539232e-001, + 9.184133e-001, + 2.011479e+000, + -3.842472e-001, + 1.432274e+000, + 1.637153e-001, + -4.408856e+000, + 5.272957e-002, + -9.829872e-001, + -8.183048e-002, + 4.464556e-001, + -1.442716e+000, + 1.029641e+000, + -6.991617e-002, + 8.702356e-003, + 5.706417e+000, + 9.116452e-001, + -1.087130e+000, + -2.038013e-001, + 7.260801e-001, + 9.164376e-001, + -5.006183e+000, + 1.511271e+000, + 1.257134e-001, + 2.715439e+000, + 6.201652e-001, + // albedo 0, turbidity 4 + -1.448662e+000, + -5.799075e-001, + -2.833268e+001, + 2.858023e+001, + -9.134061e-003, + 4.404783e-001, + -2.709026e-006, + 3.029357e+000, + 5.540071e-001, + -2.061772e+000, + -1.145190e+000, + 7.918478e+000, + -7.212525e+000, + -2.020760e-002, + 2.962715e-001, + 4.689670e-002, + 8.517209e-001, + 2.334587e-001, + -6.413755e-001, + 1.780425e-001, + -2.412919e+000, + 1.064484e+000, + -1.949986e-002, + 6.769741e-001, + -1.752760e-001, + 7.262714e+000, + 1.325869e+000, + -1.304871e+000, + -3.975581e-001, + 1.219002e+000, + 7.285178e-001, + -2.710105e-001, + 7.779727e-001, + 3.247139e-001, + -8.818168e-001, + 1.839517e-001, + -1.001104e+000, + -1.994801e-001, + 3.676742e-001, + -1.409737e+000, + 2.901555e-001, + 2.506940e-001, + 2.468899e-003, + 3.398923e+000, + 8.584645e-001, + -1.111552e+000, + -2.487204e-001, + 7.410842e-001, + 1.703749e+000, + -5.007855e+000, + 1.057763e+000, + 1.354511e-001, + 2.088715e+000, + 6.600013e-001, + // albedo 0, turbidity 5 + -1.547227e+000, + -6.679466e-001, + -1.861465e+001, + 1.884045e+001, + -1.242210e-002, + 4.157339e-001, + -2.432805e-006, + 2.812423e+000, + 5.446957e-001, + -2.043890e+000, + -1.149081e+000, + 2.304118e+000, + -1.715757e+000, + -2.433628e-002, + 2.816836e-001, + 7.185458e-002, + 1.064860e+000, + 2.706789e-001, + -9.040720e-001, + -8.274472e-002, + -2.555676e-001, + -6.326215e-001, + -2.770880e-002, + 6.676024e-001, + -2.513532e-001, + 5.903839e+000, + 1.241452e+000, + -1.000013e+000, + -1.010774e-001, + 3.699166e-001, + 8.774526e-001, + -3.042007e-001, + 6.951053e-001, + 4.361813e-001, + 6.793421e-001, + 2.573892e-001, + -1.171332e+000, + -3.768188e-001, + 3.701377e-001, + -1.470757e+000, + 5.525942e-001, + 2.991456e-002, + 1.581823e-002, + 2.365233e+000, + 8.214514e-001, + -1.068667e+000, + -2.326330e-001, + 6.725059e-001, + 2.243733e+000, + -4.614370e+000, + 1.033677e+000, + 1.376291e-001, + 2.013334e+000, + 6.865304e-001, + // albedo 0, turbidity 6 + -1.592991e+000, + -7.246948e-001, + -2.598204e+001, + 2.621960e+001, + -8.365176e-003, + 4.207571e-001, + -2.742772e-006, + 2.623735e+000, + 5.873190e-001, + -2.271349e+000, + -1.280884e+000, + 6.308739e+000, + -5.758350e+000, + -1.977049e-002, + 3.671835e-001, + 6.698038e-002, + 1.150597e+000, + 1.759218e-001, + -6.368620e-001, + -7.436052e-003, + -2.230026e+000, + 1.640997e+000, + -1.548497e-002, + 3.145331e-001, + -2.492644e-001, + 5.083843e+000, + 1.260215e+000, + -1.177925e+000, + -9.628114e-002, + 3.051152e-001, + -3.749544e-002, + -2.713209e-001, + 1.164226e+000, + 4.559969e-001, + 2.175429e+000, + 2.874284e-001, + -1.078500e+000, + -3.801779e-001, + 4.788906e-001, + -4.795969e-001, + 5.977621e-001, + -4.488535e-001, + 3.386874e-002, + 1.538143e+000, + 8.062054e-001, + -1.108028e+000, + -2.596892e-001, + 5.162202e-001, + 1.557081e+000, + -4.265039e+000, + 1.182535e+000, + 1.563762e-001, + 2.095084e+000, + 6.883383e-001, + // albedo 0, turbidity 7 + -1.668427e+000, + -7.908511e-001, + -2.779690e+001, + 2.799746e+001, + -7.186935e-003, + 3.757766e-001, + -3.326858e-006, + 2.563421e+000, + 5.439687e-001, + -2.156175e+000, + -1.220004e+000, + 3.585732e+000, + -3.235988e+000, + -1.086239e-002, + 1.846143e-001, + 1.046017e-001, + 1.234427e+000, + 2.842191e-001, + -1.117051e+000, + -4.101627e-001, + -8.463730e-001, + 7.671472e-001, + -2.226609e-002, + 8.574943e-001, + -3.434124e-001, + 4.475715e+000, + 1.154824e+000, + -7.444840e-001, + 2.312078e-001, + -5.393724e-001, + 1.574213e-001, + -1.763914e-001, + 2.751692e-001, + 5.564200e-001, + 2.217672e+000, + 3.483932e-001, + -1.273036e+000, + -5.275562e-001, + 4.902512e-001, + -4.498436e-002, + 4.339366e-001, + 2.386682e-001, + 2.380879e-002, + 1.413444e+000, + 7.855923e-001, + -1.084192e+000, + -2.936753e-001, + 4.719432e-001, + 1.384436e+000, + -3.257789e+000, + 6.119543e-001, + 1.681884e-001, + 1.650441e+000, + 6.936631e-001, + // albedo 0, turbidity 8 + -1.848490e+000, + -9.512670e-001, + -3.005251e+001, + 3.024315e+001, + -5.635304e-003, + 3.447780e-001, + -2.782999e-006, + 2.309422e+000, + 5.643559e-001, + -2.300008e+000, + -1.252335e+000, + -1.218876e+000, + 1.493730e+000, + -6.107100e-003, + 7.974860e-002, + 1.023449e-001, + 1.505934e+000, + 2.360948e-001, + -1.483705e+000, + -8.547575e-001, + -7.797146e-001, + 6.447971e-001, + -2.678052e-002, + 1.091263e+000, + -3.344889e-001, + 3.830416e+000, + 1.189425e+000, + -5.348005e-001, + 3.982733e-001, + -4.071573e-001, + 3.265569e-001, + -8.658789e-002, + -2.370892e-001, + 5.369097e-001, + 1.478279e+000, + 3.143303e-001, + -1.320401e+000, + -6.043247e-001, + 3.019196e-001, + -7.732911e-002, + 4.768381e-001, + 6.745764e-001, + 3.694098e-002, + 1.158234e+000, + 8.169056e-001, + -1.101040e+000, + -3.420019e-001, + 3.775661e-001, + 1.769338e+000, + -2.990515e+000, + 1.649529e-001, + 1.970125e-001, + 1.453355e+000, + 6.759757e-001, + // albedo 0, turbidity 9 + -2.251946e+000, + -1.229349e+000, + -3.271808e+001, + 3.283114e+001, + -4.252027e-003, + 3.372289e-001, + -3.001937e-006, + 2.154046e+000, + 5.842674e-001, + -1.867834e+000, + -9.531252e-001, + -1.229365e+001, + 1.269149e+001, + -6.844772e-003, + 1.185107e-001, + 7.539587e-002, + 1.846381e+000, + 1.899412e-001, + -3.398629e+000, + -2.180862e+000, + 2.335213e+000, + -3.382823e+000, + -8.613985e-003, + 8.431602e-001, + -2.393567e-001, + 3.112460e+000, + 1.218556e+000, + 5.708381e-001, + 9.406030e-001, + -6.890113e-001, + 2.746233e+000, + -5.772068e-002, + 1.096005e-001, + 3.491978e-001, + 7.281453e-001, + 3.212049e-001, + -1.705909e+000, + -8.517224e-001, + 1.131160e-001, + -2.141434e+000, + 4.274043e-001, + 3.397600e-001, + 1.786490e-001, + 9.026101e-001, + 7.882800e-001, + -1.012865e+000, + -3.495551e-001, + 3.369038e-001, + 3.724205e+000, + -3.089586e+000, + 1.266964e-001, + 1.461790e-001, + 1.170199e+000, + 6.931052e-001, + // albedo 0, turbidity 10 + -2.890318e+000, + -1.665573e+000, + -3.493756e+001, + 3.500369e+001, + -2.984251e-003, + 2.622419e-001, + -4.259360e-006, + 1.947681e+000, + 6.905752e-001, + -1.956022e+000, + -1.062900e+000, + -1.919714e+001, + 1.975164e+001, + -8.865396e-003, + 2.165540e-001, + 5.475637e-002, + 1.761134e+000, + 3.164249e-003, + -5.612198e+000, + -3.101371e+000, + 4.098034e+000, + -6.144001e+000, + 9.944958e-003, + 2.905472e-001, + -1.707110e-001, + 3.199107e+000, + 1.337660e+000, + 8.353756e-001, + 4.855943e-001, + -1.243589e+000, + 5.147385e+000, + -7.013963e-002, + 9.380410e-001, + 2.335714e-001, + 1.727744e-001, + 2.802696e-001, + -1.524329e+000, + -7.388547e-001, + 3.259025e-001, + -4.050634e+000, + 4.058549e-001, + -2.591384e-001, + 1.898299e-001, + 3.556071e-001, + 7.884126e-001, + -1.070371e+000, + -4.207858e-001, + 1.739862e-001, + 5.293410e+000, + -3.136757e+000, + 2.323856e-001, + 1.673706e-001, + 1.007227e+000, + 6.844287e-001, + // albedo 1, turbidity 1 + -1.341720e+000, + -4.834889e-001, + -4.633447e+001, + 4.682148e+001, + -6.137296e-003, + 4.599216e-001, + 7.047323e-003, + 2.895798e+000, + 4.999398e-001, + -1.529104e+000, + -6.498631e-001, + 1.534103e+001, + -1.450675e+001, + -1.531439e-002, + 3.280082e-001, + 1.682926e-002, + 1.901587e+000, + 5.013227e-001, + -1.014776e+000, + -1.454495e-001, + -4.071085e+000, + 2.954982e+000, + -2.630348e-002, + 5.681531e-001, + -3.016505e-002, + 6.773854e+000, + 5.003504e-001, + -1.172413e+000, + -4.026320e-001, + 2.960428e+000, + 2.020710e-001, + -2.004947e-001, + 9.375572e-001, + 5.998168e-002, + -4.945934e+000, + 4.502898e-001, + -9.898161e-001, + -5.772814e-002, + 4.470024e-001, + -5.786656e-001, + 1.158168e-001, + 3.468040e-001, + -5.043360e-002, + 6.867947e+000, + 8.012363e-001, + -1.085111e+000, + -1.882675e-001, + 1.223748e+000, + 3.565495e-001, + -3.688357e+000, + 5.653723e-001, + 6.727646e-002, + 2.690130e+000, + 4.999400e-001, + // albedo 1, turbidity 2 + -1.389119e+000, + -5.290250e-001, + -4.055774e+001, + 4.105972e+001, + -7.062577e-003, + 4.560060e-001, + -1.736334e-006, + 2.775512e+000, + 6.671455e-001, + -1.584641e+000, + -7.200619e-001, + 1.248067e+001, + -1.156028e+001, + -1.659568e-002, + 3.050029e-001, + 1.099895e-002, + 1.438927e+000, + -2.138015e-002, + -9.826068e-001, + -8.887254e-002, + -2.960031e+000, + 1.808816e+000, + -2.478159e-002, + 6.035733e-001, + -4.868441e-002, + 7.347705e+000, + 1.584739e+000, + -1.150423e+000, + -4.073793e-001, + 2.412991e+000, + 4.870840e-001, + -2.337902e-001, + 8.295114e-001, + 1.129914e-001, + -5.150045e+000, + -9.016643e-002, + -1.016933e+000, + -6.311501e-002, + 5.218937e-001, + -5.716430e-001, + 1.250993e-001, + 3.601524e-001, + -5.497586e-002, + 7.060139e+000, + 1.018333e+000, + -1.073151e+000, + -1.845444e-001, + 1.155394e+000, + 3.004486e-001, + -3.431711e+000, + 4.657031e-001, + 9.401223e-002, + 2.688620e+000, + 4.999544e-001, + // albedo 1, turbidity 3 + -1.391257e+000, + -5.365815e-001, + -4.255881e+001, + 4.299132e+001, + -5.838466e-003, + 4.229134e-001, + -2.760038e-006, + 2.775531e+000, + 6.234597e-001, + -1.780062e+000, + -9.228880e-001, + 1.376172e+001, + -1.260946e+001, + -1.507526e-002, + 3.117435e-001, + 2.205045e-002, + 6.093731e-001, + 3.463446e-002, + -7.388169e-001, + 1.275670e-001, + -3.999528e+000, + 2.223993e+000, + -1.856853e-002, + 5.439310e-001, + -8.834054e-002, + 8.037139e+000, + 1.645951e+000, + -1.322387e+000, + -5.320143e-001, + 2.659359e+000, + 1.086712e+000, + -2.129712e-001, + 8.704649e-001, + 1.800315e-001, + -4.967241e+000, + -1.383720e-001, + -9.378288e-001, + -1.599895e-002, + 3.607555e-001, + -1.980561e+000, + 3.791456e-001, + 1.212268e-001, + -2.845992e-002, + 6.825542e+000, + 1.059139e+000, + -1.100832e+000, + -2.172313e-001, + 1.211561e+000, + 2.002721e+000, + -5.010011e+000, + 5.717583e-001, + 6.777702e-002, + 2.160006e+000, + 5.676392e-001, + // albedo 1, turbidity 4 + -1.409373e+000, + -5.708751e-001, + -3.034974e+001, + 3.079809e+001, + -7.280715e-003, + 3.723304e-001, + -2.436279e-006, + 2.577348e+000, + 5.913377e-001, + -1.954312e+000, + -1.116510e+000, + 5.399148e+000, + -4.299553e+000, + -1.724739e-002, + 3.742824e-001, + 4.187077e-002, + 1.044883e-001, + 1.232727e-001, + -6.772215e-001, + 2.001396e-001, + -3.670523e-001, + -1.014628e+000, + -3.497152e-003, + 4.099858e-001, + -1.584633e-001, + 7.750400e+000, + 1.514559e+000, + -1.291600e+000, + -4.977437e-001, + 9.641914e-001, + 1.562420e+000, + -3.227782e-001, + 9.055427e-001, + 3.046444e-001, + -3.385619e+000, + 9.546291e-003, + -9.750857e-001, + -8.770560e-002, + 9.054256e-001, + -1.429236e+000, + 8.974777e-001, + -1.217961e-001, + -5.194608e-002, + 4.909409e+000, + 9.589153e-001, + -1.088007e+000, + -1.959301e-001, + 9.745799e-001, + 1.260761e+000, + -5.008864e+000, + 7.271248e-001, + 1.096661e-001, + 2.717295e+000, + 6.340731e-001, + // albedo 1, turbidity 5 + -1.456050e+000, + -6.223072e-001, + -2.228088e+001, + 2.269604e+001, + -9.340812e-003, + 4.118308e-001, + -2.418083e-006, + 2.442117e+000, + 5.589638e-001, + -2.176449e+000, + -1.302416e+000, + 2.222836e+000, + -1.222730e+000, + -1.728051e-002, + 1.323513e-001, + 7.027731e-002, + 4.835745e-002, + 2.093351e-001, + -5.789641e-001, + 2.215407e-001, + 2.142291e-001, + -1.201725e+000, + -1.185728e-002, + 8.122982e-001, + -2.380420e-001, + 6.706841e+000, + 1.404146e+000, + -1.307463e+000, + -4.515174e-001, + 6.447827e-001, + 1.223841e+000, + -2.902391e-001, + 4.986588e-001, + 4.073652e-001, + -1.706696e+000, + 1.060885e-001, + -9.698678e-001, + -1.307094e-001, + 9.389347e-001, + -1.522852e+000, + 7.768797e-001, + -1.368595e-001, + -3.857426e-002, + 3.676935e+000, + 8.980966e-001, + -1.104349e+000, + -2.380323e-001, + 1.047043e+000, + 1.865421e+000, + -5.011664e+000, + 7.014954e-001, + 9.622701e-002, + 1.891360e+000, + 6.687354e-001, + // albedo 1, turbidity 6 + -1.502249e+000, + -6.724523e-001, + -2.888092e+001, + 2.930360e+001, + -6.685766e-003, + 3.685464e-001, + -2.469442e-006, + 2.310797e+000, + 5.566754e-001, + -2.217125e+000, + -1.364924e+000, + 4.048243e+000, + -3.111333e+000, + -1.317747e-002, + 1.921948e-001, + 8.627702e-002, + 1.981769e-003, + 2.213689e-001, + -6.215757e-001, + 1.687995e-001, + -5.949131e-001, + -1.551293e-001, + 3.356129e-004, + 6.897657e-001, + -2.855053e-001, + 6.271042e+000, + 1.363084e+000, + -1.216317e+000, + -3.489429e-001, + 7.566226e-001, + 5.409809e-001, + -2.830843e-001, + 6.191825e-001, + 4.755163e-001, + -9.131387e-001, + 1.383909e-001, + -1.030437e+000, + -2.034064e-001, + 8.335995e-001, + -1.050947e+000, + 8.689093e-001, + -3.672310e-001, + -4.056183e-002, + 3.111269e+000, + 8.856842e-001, + -1.078984e+000, + -2.070549e-001, + 9.683145e-001, + 1.497022e+000, + -5.007653e+000, + 7.702541e-001, + 1.285822e-001, + 2.225188e+000, + 6.587911e-001, + // albedo 1, turbidity 7 + -1.559291e+000, + -7.374039e-001, + -3.596311e+001, + 3.634470e+001, + -4.667132e-003, + 3.277964e-001, + -2.487945e-006, + 2.215652e+000, + 5.764681e-001, + -2.356929e+000, + -1.444755e+000, + 6.244526e+000, + -5.540162e+000, + -8.794510e-003, + 1.792100e-001, + 9.578517e-002, + 3.737676e-001, + 1.922194e-001, + -6.589752e-001, + -2.926910e-002, + -1.831779e+000, + 1.869962e+000, + -2.030095e-003, + 7.552089e-001, + -3.168157e-001, + 4.632196e+000, + 1.294054e+000, + -1.161046e+000, + -1.472506e-001, + 6.494138e-001, + -8.327174e-001, + -2.320724e-001, + 3.391212e-001, + 5.269637e-001, + 9.376341e-001, + 2.458573e-001, + -1.034427e+000, + -3.062504e-001, + 8.975634e-001, + 3.203531e-001, + 8.565142e-001, + -1.250162e-001, + -4.094017e-002, + 1.861304e+000, + 8.223468e-001, + -1.109954e+000, + -2.740277e-001, + 1.063811e+000, + 7.077398e-001, + -4.695734e+000, + 5.621696e-001, + 1.248956e-001, + 1.297723e+000, + 6.789720e-001, + // albedo 1, turbidity 8 + -1.788293e+000, + -9.368751e-001, + -4.382980e+001, + 4.424963e+001, + -3.652530e-003, + 3.094331e-001, + -2.810503e-006, + 1.904402e+000, + 5.861599e-001, + -2.268206e+000, + -1.312676e+000, + 2.863082e+000, + -2.373727e+000, + -5.144980e-003, + 1.711072e-001, + 9.316041e-002, + 9.309598e-001, + 1.791683e-001, + -1.376966e+000, + -7.418582e-001, + -1.349589e+000, + 1.563419e+000, + -3.124219e-003, + 6.967139e-001, + -3.061887e-001, + 3.602731e+000, + 1.255669e+000, + -6.017540e-001, + 2.815928e-001, + 5.424052e-001, + -6.885450e-001, + -1.620001e-001, + 2.980046e-001, + 4.995571e-001, + 7.371203e-001, + 2.812466e-001, + -1.278853e+000, + -5.245326e-001, + 7.870520e-001, + 3.125067e-001, + 7.748105e-001, + -7.788581e-002, + 3.490956e-003, + 1.283748e+000, + 8.130190e-001, + -1.050930e+000, + -2.786331e-001, + 1.056344e+000, + 1.053002e+000, + -4.047789e+000, + 4.432174e-001, + 1.169077e-001, + 9.532621e-001, + 6.806764e-001, + // albedo 1, turbidity 9 + -2.084927e+000, + -1.203954e+000, + -4.881638e+001, + 4.920160e+001, + -2.896045e-003, + 2.882977e-001, + -3.073517e-006, + 1.702211e+000, + 6.374180e-001, + -2.328567e+000, + -1.238023e+000, + -1.891019e+000, + 2.451520e+000, + -5.847581e-003, + 2.084702e-001, + 7.848130e-002, + 1.211048e+000, + 8.095008e-002, + -2.634632e+000, + -1.789460e+000, + -1.370558e-001, + -3.326435e-001, + 2.783737e-003, + 5.239451e-001, + -2.548881e-001, + 2.896327e+000, + 1.324116e+000, + 6.882616e-002, + 5.997821e-001, + 1.535398e-001, + 1.375209e+000, + -1.267285e-001, + 4.239743e-001, + 4.013122e-001, + 1.794675e-001, + 2.395382e-001, + -1.430918e+000, + -6.439041e-001, + 8.325980e-001, + -1.705612e+000, + 7.236426e-001, + -5.567593e-002, + 6.408718e-002, + 6.836524e-001, + 8.388887e-001, + -1.037956e+000, + -3.215402e-001, + 9.457349e-001, + 3.178114e+000, + -4.152156e+000, + 2.230992e-001, + 1.156198e-001, + 7.606223e-001, + 6.656923e-001, + // albedo 1, turbidity 10 + -2.967314e+000, + -1.728778e+000, + -3.730988e+001, + 3.755578e+001, + -2.588835e-003, + 2.927966e-001, + -3.935038e-006, + 1.592161e+000, + 6.868694e-001, + -2.123311e+000, + -1.175148e+000, + -1.314988e+001, + 1.386882e+001, + -7.828537e-003, + 1.852026e-001, + 5.481038e-002, + 1.294309e+000, + 2.428177e-002, + -5.443597e+000, + -3.156344e+000, + 2.110838e+000, + -3.421556e+000, + 1.181890e-002, + 1.196951e-001, + -1.742902e-001, + 2.404353e+000, + 1.272805e+000, + 1.029898e+000, + 5.912521e-001, + -3.983531e-001, + 3.286069e+000, + -9.252065e-002, + 1.331381e+000, + 2.560642e-001, + 8.001754e-001, + 3.624178e-001, + -1.547574e+000, + -7.881604e-001, + 1.020902e+000, + -2.897069e+000, + 5.213470e-001, + -9.242315e-001, + 1.185594e-001, + -1.150721e+000, + 7.317211e-001, + -9.621043e-001, + -1.991406e-001, + 6.531287e-001, + 3.925839e+000, + -3.596904e+000, + 6.317332e-001, + 1.531334e-001, + 1.457846e+000, + 6.966285e-001, +}; + +double datasetRGBRad3[] = +{ + // albedo 0, turbidity 1 + 9.926518e-001, + 1.999494e+000, + -4.136109e+000, + 1.856270e+001, + 1.351028e+001, + 1.390238e+001, + // albedo 0, turbidity 2 + 9.634366e-001, + 2.119694e+000, + -4.614523e+000, + 1.919701e+001, + 1.376644e+001, + 1.418731e+001, + // albedo 0, turbidity 3 + 9.446537e-001, + 2.171610e+000, + -4.915556e+000, + 1.918240e+001, + 1.537135e+001, + 1.400530e+001, + // albedo 0, turbidity 4 + 9.073074e-001, + 2.330536e+000, + -5.577596e+000, + 1.961615e+001, + 1.688365e+001, + 1.446955e+001, + // albedo 0, turbidity 5 + 8.739124e-001, + 2.388682e+000, + -5.842995e+000, + 1.923265e+001, + 1.887735e+001, + 1.485698e+001, + // albedo 0, turbidity 6 + 8.563688e-001, + 2.391534e+000, + -5.769133e+000, + 1.828709e+001, + 2.097209e+001, + 1.469587e+001, + // albedo 0, turbidity 7 + 8.270533e-001, + 2.342790e+000, + -5.558071e+000, + 1.684993e+001, + 2.356498e+001, + 1.505975e+001, + // albedo 0, turbidity 8 + 7.908339e-001, + 2.190341e+000, + -4.852571e+000, + 1.374862e+001, + 2.806846e+001, + 1.548444e+001, + // albedo 0, turbidity 9 + 7.403619e-001, + 1.783998e+000, + -2.983854e+000, + 7.622563e+000, + 3.507610e+001, + 1.615805e+001, + // albedo 0, turbidity 10 + 6.840111e-001, + 1.154457e+000, + -2.393830e-001, + -7.896893e-001, + 4.282765e+001, + 1.779469e+001, + // albedo 1, turbidity 1 + 1.168300e+000, + 1.860993e+000, + -2.129074e+000, + 1.251952e+001, + 3.032499e+001, + 2.938716e+001, + // albedo 1, turbidity 2 + 1.150338e+000, + 1.918813e+000, + -2.413527e+000, + 1.274862e+001, + 3.087134e+001, + 2.951432e+001, + // albedo 1, turbidity 3 + 1.114719e+000, + 1.964689e+000, + -2.625423e+000, + 1.247837e+001, + 3.237949e+001, + 2.943596e+001, + // albedo 1, turbidity 4 + 1.077948e+000, + 2.006292e+000, + -2.846934e+000, + 1.190195e+001, + 3.459293e+001, + 2.937492e+001, + // albedo 1, turbidity 5 + 1.035143e+000, + 1.986681e+000, + -2.752584e+000, + 1.060972e+001, + 3.722185e+001, + 2.918594e+001, + // albedo 1, turbidity 6 + 1.015992e+000, + 1.992054e+000, + -2.812626e+000, + 1.001416e+001, + 3.847300e+001, + 2.924624e+001, + // albedo 1, turbidity 7 + 9.756887e-001, + 1.939897e+000, + -2.533281e+000, + 8.319176e+000, + 4.083907e+001, + 2.925586e+001, + // albedo 1, turbidity 8 + 9.264164e-001, + 1.716454e+000, + -1.597044e+000, + 4.739725e+000, + 4.507683e+001, + 2.878915e+001, + // albedo 1, turbidity 9 + 8.595191e-001, + 1.346034e+000, + -2.801895e-002, + -6.582906e-001, + 5.017523e+001, + 2.852953e+001, + // albedo 1, turbidity 10 + 7.754116e-001, + 7.709245e-001, + 2.200201e+000, + -7.487661e+000, + 5.436622e+001, + 2.893432e+001, +}; + +double* datasetsRGB[] = +{ + datasetRGB1, + datasetRGB2, + datasetRGB3 +}; + +double* datasetsRGBRad[] = +{ + datasetRGBRad1, + datasetRGBRad2, + datasetRGBRad3 +}; diff --git a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/config_module.cpp b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/config_module.cpp index 1597710f..6beeb621 100644 --- a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/config_module.cpp +++ b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/config_module.cpp @@ -1,34 +1,34 @@ - -#include "config_module.h" - -/* - -Include all your dynamically typed classes here, e.g. -#include "my_dynamic_class.h" - -*/ - -#include "dconfig.h" - - -Configure( config_mymodule ); -NotifyCategoryDef( mymodule , ""); - -ConfigureFn( config_mymodule ) { - init_libmymodule(); -} - -void -init_libmymodule() { - static bool initialized = false; - if (initialized) { - return; - } - initialized = true; - - // Init your dynamic types here, e.g.: - // MyDynamicClass::init_type(); - - return; -} - + +#include "config_module.h" + +/* + +Include all your dynamically typed classes here, e.g. +#include "my_dynamic_class.h" + +*/ + +#include "dconfig.h" + + +Configure( config_mymodule ); +NotifyCategoryDef( mymodule , ""); + +ConfigureFn( config_mymodule ) { + init_libmymodule(); +} + +void +init_libmymodule() { + static bool initialized = false; + if (initialized) { + return; + } + initialized = true; + + // Init your dynamic types here, e.g.: + // MyDynamicClass::init_type(); + + return; +} + diff --git a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/config_module.h b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/config_module.h index b0aa4e5b..6b4ae33d 100644 --- a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/config_module.h +++ b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/config_module.h @@ -1,12 +1,12 @@ -#pragma once - -#include "pandabase.h" -#include "notifyCategoryProxy.h" -#include "configVariableDouble.h" -#include "configVariableString.h" -#include "configVariableInt.h" - - -NotifyCategoryDecl(mymodule, EXPORT_CLASS, EXPORT_TEMPL); - -extern void init_libmymodule(); +#pragma once + +#include "pandabase.h" +#include "notifyCategoryProxy.h" +#include "configVariableDouble.h" +#include "configVariableString.h" +#include "configVariableInt.h" + + +NotifyCategoryDecl(mymodule, EXPORT_CLASS, EXPORT_TEMPL); + +extern void init_libmymodule(); diff --git a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/main.h b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/main.h index 3ace4617..a3f65113 100644 --- a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/main.h +++ b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/source/main.h @@ -1,101 +1,101 @@ - - -#include "ArHosekSkyModel.h" -#include "PNMImage.h" -#include "Texture.h" - -#include -using namespace std; - -#define ONE_PI 3.14159265359 -#define TWO_PI 6.28318530718 - -double srgb_clamp(double v) { - v = max(0.0, min(1.0, v / 255.0)); - return v; -} - - -BEGIN_PUBLISH -INLINE void generate_table() -{ - - const double min_elevation = 0.0; // degree - const double max_elevation = 40.0; // degree - double turbidity = 3.0; - double albedo = 0.2; - - cout << "Scattering Precomputation v0.1" << endl; - cout << "Turbidity (double from 0 to 10): "; - cin >> turbidity; - cout << "Ground Albedo (double from 0 to 1): "; - cin >> albedo; - - const double sun_azimuth = ONE_PI * 0.25; - - PNMImage img(512 * 10, 128 * 10, 3, 65535); - - for (size_t elev = 0; elev < 100; ++elev) { - if (elev % 10 == 0) - cout << elev << " percent" << endl; - - double factor = ((double)elev) / 99.0; - double angle = factor * (max_elevation - min_elevation) + min_elevation; - - const double sun_elevation = angle / 360.0 * TWO_PI; - const int num_channels = 3; - - ArHosekSkyModelState* skymodel_state = arhosek_rgb_skymodelstate_alloc_init( - turbidity, albedo, sun_elevation); - - double skydome_result[num_channels]; - - for (size_t i = 0; i < 512; ++i) { - for (size_t k = 0; k < 128; ++k) { - - double factor_x = ((double)i) / 512.0; - double factor_y = ((double)k) / 128.0; - - double theta = 0, gamma = 0; - - if (true) { - // write out raw angles - gamma = factor_x * 2.0 * ONE_PI; - theta = factor_y * 0.5 * ONE_PI; - } - else { - // Write out transformed angles - double coord_azimuth = factor_x * TWO_PI; - theta = factor_y * ONE_PI * 0.5; - double neg_solar_elevation = 0.5 * ONE_PI - sun_elevation; - - double cosGamma = cos(theta) * cos(neg_solar_elevation) - + sin(theta) * sin(neg_solar_elevation) - * cos(coord_azimuth - sun_azimuth); - - gamma = acos(cosGamma); - } - - for (size_t r = 0; r < num_channels; r++) { - skydome_result[r] = srgb_clamp(arhosek_tristim_skymodel_radiance( - skymodel_state, theta, gamma, r)); - } - - img.set_xel(i + (elev % 10) * 512, k + (elev / 10) * 128, - skydome_result[0], skydome_result[1], skydome_result[2]); - } - } - - arhosekskymodelstate_free(skymodel_state); - } - - cout << "Writing data ..." << endl; - // img.write("scattering_lut.png"); - - Texture t; - t.setup_2d_texture(img.get_x_size(), img.get_y_size(), Texture::T_float, Texture::F_rgb16); - t.load(img); - t.write("scattering_lut.txo.pz"); -} - -END_PUBLISH + + +#include "ArHosekSkyModel.h" +#include "PNMImage.h" +#include "Texture.h" + +#include +using namespace std; + +#define ONE_PI 3.14159265359 +#define TWO_PI 6.28318530718 + +double srgb_clamp(double v) { + v = max(0.0, min(1.0, v / 255.0)); + return v; +} + + +BEGIN_PUBLISH +INLINE void generate_table() +{ + + const double min_elevation = 0.0; // degree + const double max_elevation = 40.0; // degree + double turbidity = 3.0; + double albedo = 0.2; + + cout << "Scattering Precomputation v0.1" << endl; + cout << "Turbidity (double from 0 to 10): "; + cin >> turbidity; + cout << "Ground Albedo (double from 0 to 1): "; + cin >> albedo; + + const double sun_azimuth = ONE_PI * 0.25; + + PNMImage img(512 * 10, 128 * 10, 3, 65535); + + for (size_t elev = 0; elev < 100; ++elev) { + if (elev % 10 == 0) + cout << elev << " percent" << endl; + + double factor = ((double)elev) / 99.0; + double angle = factor * (max_elevation - min_elevation) + min_elevation; + + const double sun_elevation = angle / 360.0 * TWO_PI; + const int num_channels = 3; + + ArHosekSkyModelState* skymodel_state = arhosek_rgb_skymodelstate_alloc_init( + turbidity, albedo, sun_elevation); + + double skydome_result[num_channels]; + + for (size_t i = 0; i < 512; ++i) { + for (size_t k = 0; k < 128; ++k) { + + double factor_x = ((double)i) / 512.0; + double factor_y = ((double)k) / 128.0; + + double theta = 0, gamma = 0; + + if (true) { + // write out raw angles + gamma = factor_x * 2.0 * ONE_PI; + theta = factor_y * 0.5 * ONE_PI; + } + else { + // Write out transformed angles + double coord_azimuth = factor_x * TWO_PI; + theta = factor_y * ONE_PI * 0.5; + double neg_solar_elevation = 0.5 * ONE_PI - sun_elevation; + + double cosGamma = cos(theta) * cos(neg_solar_elevation) + + sin(theta) * sin(neg_solar_elevation) + * cos(coord_azimuth - sun_azimuth); + + gamma = acos(cosGamma); + } + + for (size_t r = 0; r < num_channels; r++) { + skydome_result[r] = srgb_clamp(arhosek_tristim_skymodel_radiance( + skymodel_state, theta, gamma, r)); + } + + img.set_xel(i + (elev % 10) * 512, k + (elev / 10) * 128, + skydome_result[0], skydome_result[1], skydome_result[2]); + } + } + + arhosekskymodelstate_free(skymodel_state); + } + + cout << "Writing data ..." << endl; + // img.write("scattering_lut.png"); + + Texture t; + t.setup_2d_texture(img.get_x_size(), img.get_y_size(), Texture::T_float, Texture::F_rgb16); + t.load(img); + t.write("scattering_lut.txo.pz"); +} + +END_PUBLISH diff --git a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/update_module_builder.py b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/update_module_builder.py index ab2d79e3..722d319b 100644 --- a/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/update_module_builder.py +++ b/RenderPipelineFile/rpplugins/scattering/resources/hosek_wilkie_scattering/update_module_builder.py @@ -1,57 +1,57 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -# This script downloads and updates the module builder. - -# pylint: skip-file - -if __name__ == "__main__": - import os - import sys - from os.path import dirname, realpath, join, isfile - - # Files which are skipped from the downloaded zip - files_to_ignore = "__init__.py LICENSE README.md config.ini config_module".split() - - current_dir = dirname(realpath(__file__)) - os.chdir(current_dir) - - # Import thirdparty packages - sys.path.insert(0, "../" * 4 + "rpcore/util") - sys.path.insert(0, "../" * 4) - - # Download the module - from submodule_downloader import download_submodule - download_submodule("tobspr", "P3DModuleBuilder", current_dir, files_to_ignore) - - # Make an init script so we can import the code - with open("scripts/__init__.py", "w") as handle: - handle.write("# empty file to be able to import the scripts") - - # Update the gitignore using the suggested version from the module builder - if isfile(".gitignore"): - os.remove(".gitignore") - os.rename("prefab.gitignore", ".gitignore") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +# This script downloads and updates the module builder. + +# pylint: skip-file + +if __name__ == "__main__": + import os + import sys + from os.path import dirname, realpath, join, isfile + + # Files which are skipped from the downloaded zip + files_to_ignore = "__init__.py LICENSE README.md config.ini config_module".split() + + current_dir = dirname(realpath(__file__)) + os.chdir(current_dir) + + # Import thirdparty packages + sys.path.insert(0, "../" * 4 + "rpcore/util") + sys.path.insert(0, "../" * 4) + + # Download the module + from submodule_downloader import download_submodule + download_submodule("tobspr", "P3DModuleBuilder", current_dir, files_to_ignore) + + # Make an init script so we can import the code + with open("scripts/__init__.py", "w") as handle: + handle.write("# empty file to be able to import the scripts") + + # Update the gitignore using the suggested version from the module builder + if isfile(".gitignore"): + os.remove(".gitignore") + os.rename("prefab.gitignore", ".gitignore") diff --git a/RenderPipelineFile/rpplugins/scattering/scattering_envmap_stage.py b/RenderPipelineFile/rpplugins/scattering/scattering_envmap_stage.py index 26a0c834..a461f209 100644 --- a/RenderPipelineFile/rpplugins/scattering/scattering_envmap_stage.py +++ b/RenderPipelineFile/rpplugins/scattering/scattering_envmap_stage.py @@ -1,68 +1,68 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import division - -from rpcore.render_stage import RenderStage -from rpcore.util.cubemap_filter import CubemapFilter - -from rpcore.stages.ambient_stage import AmbientStage -from rpcore.stages.gbuffer_stage import GBufferStage - - -class ScatteringEnvmapStage(RenderStage): - - """ This stage uses the precomputed data to make a cubemap containing the - scattering """ - - required_pipes = [] - required_inputs = ["DefaultSkydome", "DefaultEnvmap"] - - @property - def produced_pipes(self): - return { - "ScatteringIBLDiffuse": self.cubemap_filter.diffuse_cubemap, - "ScatteringIBLSpecular": self.cubemap_filter.specular_cubemap, - } - - def create(self): - self.cubemap_filter = CubemapFilter(self, "ScatEnvCub") - - self.target_cube = self.create_target("ComputeScattering") - self.target_cube.size = self.cubemap_filter.size * 6, self.cubemap_filter.size - self.target_cube.prepare_buffer() - self.target_cube.set_shader_input("DestCubemap", self.cubemap_filter.target_cubemap) - - self.cubemap_filter.create() - - # Make the stages use our cubemap textures - for stage in (AmbientStage, GBufferStage): - stage.required_pipes.append("ScatteringIBLDiffuse") - stage.required_pipes.append("ScatteringIBLSpecular") - - def reload_shaders(self): - self.target_cube.shader = self.load_plugin_shader("scattering_envmap.frag.glsl") - self.cubemap_filter.reload_shaders() +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import division + +from rpcore.render_stage import RenderStage +from rpcore.util.cubemap_filter import CubemapFilter + +from rpcore.stages.ambient_stage import AmbientStage +from rpcore.stages.gbuffer_stage import GBufferStage + + +class ScatteringEnvmapStage(RenderStage): + + """ This stage uses the precomputed data to make a cubemap containing the + scattering """ + + required_pipes = [] + required_inputs = ["DefaultSkydome", "DefaultEnvmap"] + + @property + def produced_pipes(self): + return { + "ScatteringIBLDiffuse": self.cubemap_filter.diffuse_cubemap, + "ScatteringIBLSpecular": self.cubemap_filter.specular_cubemap, + } + + def create(self): + self.cubemap_filter = CubemapFilter(self, "ScatEnvCub") + + self.target_cube = self.create_target("ComputeScattering") + self.target_cube.size = self.cubemap_filter.size * 6, self.cubemap_filter.size + self.target_cube.prepare_buffer() + self.target_cube.set_shader_input("DestCubemap", self.cubemap_filter.target_cubemap) + + self.cubemap_filter.create() + + # Make the stages use our cubemap textures + for stage in (AmbientStage, GBufferStage): + stage.required_pipes.append("ScatteringIBLDiffuse") + stage.required_pipes.append("ScatteringIBLSpecular") + + def reload_shaders(self): + self.target_cube.shader = self.load_plugin_shader("scattering_envmap.frag.glsl") + self.cubemap_filter.reload_shaders() diff --git a/RenderPipelineFile/rpplugins/scattering/scattering_methods.py b/RenderPipelineFile/rpplugins/scattering/scattering_methods.py index 47dc2c70..6f431385 100644 --- a/RenderPipelineFile/rpplugins/scattering/scattering_methods.py +++ b/RenderPipelineFile/rpplugins/scattering/scattering_methods.py @@ -1,254 +1,254 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -import math - -from six.moves import range # pylint: disable=import-error -from rplibs.six import iteritems, itervalues - -from direct.stdpy.file import listdir, isfile, join -from panda3d.core import SamplerState, ShaderAttrib, NodePath - -from rpcore.globals import Globals -from rpcore.rpobject import RPObject -from rpcore.image import Image -from rpcore.loader import RPLoader - - -class ScatteringMethod(RPObject): - - """ Base class for all scattering methods """ - - def __init__(self, plugin_handle): - RPObject.__init__(self) - self.handle = plugin_handle - - def load(self): - """ Should load all required resources here """ - raise NotImplementedError() - - def compute(self): - """ Should compute the model here if necessary """ - raise NotImplementedError() - - -class ScatteringMethodHosekWilkie(ScatteringMethod): - - """ Scattering as suggested by Hosek and Wilkie """ - - def load(self): - """ Loads the scattering method """ - lut_src = self.handle.get_resource( - "hosek_wilkie_scattering/scattering_lut.txo") - - if not isfile(lut_src): - self.error("Could not find precompiled LUT for the Hosek Wilkie " - "Scattering! Make sure you compiled the algorithm code!") - return - - lut_tex = RPLoader.load_sliced_3d_texture(lut_src, 512, 128, 100) - lut_tex.set_wrap_u(SamplerState.WM_repeat) - lut_tex.set_wrap_v(SamplerState.WM_clamp) - lut_tex.set_wrap_w(SamplerState.WM_clamp) - lut_tex.set_minfilter(SamplerState.FT_linear) - lut_tex.set_magfilter(SamplerState.FT_linear) - - # Setting the format explicitely shouldn't be necessary - # lut_tex.set_format(Image.F_rgb16) - - self.handle.display_stage.set_shader_input("ScatteringLUT", lut_tex) - self.handle.envmap_stage.set_shader_input("ScatteringLUT", lut_tex) - - def compute(self): - """ Computes the scattering method, not required since we use a precomputed - LUT """ - pass - - -class ScatteringMethodEricBruneton(ScatteringMethod): - - """ Precomputed atmospheric scattering by Eric Bruneton """ - - def load(self): - """ Inits parameters, those should match with the ones specified in common.glsl """ - self.use_32_bit = False - self.trans_w, self.trans_h = 256 * 4, 64 * 4 - self.sky_w, self.sky_h = 64 * 4, 16 * 4 - self.res_r, self.res_mu, self.res_mu_s, self.res_nu = 32, 128, 32, 8 - self.res_mu_s_nu = self.res_mu_s * self.res_nu - - self.create_shaders() - self.create_textures() - - def create_textures(self): - """ Creates all textures required for the scattering """ - - tex_format = "RGBA32" if self.use_32_bit else "RGBA16" - img_2d, img_3d = Image.create_2d, Image.create_3d - - self.textures = { - "transmittance": img_2d("scat-trans", self.trans_w, self.trans_h, tex_format), - "irradiance": img_2d("scat-irrad", self.sky_w, self.sky_h, tex_format), - "inscatter": img_3d("scat-inscat", self.res_mu_s_nu, self.res_mu, self.res_r, tex_format), # noqa # pylint: disable=line-too-long - "delta_e": img_2d("scat-dx-e", self.sky_w, self.sky_h, tex_format), - "delta_sr": img_3d("scat-dx-sr", self.res_mu_s_nu, self.res_mu, self.res_r, tex_format), - "delta_sm": img_3d("scat-dx-sm", self.res_mu_s_nu, self.res_mu, self.res_r, tex_format), - "delta_j": img_3d("scat-dx-j", self.res_mu_s_nu, self.res_mu, self.res_r, tex_format), - } - - for img in itervalues(self.textures): - img.set_minfilter(SamplerState.FT_linear) - img.set_magfilter(SamplerState.FT_linear) - img.set_wrap_u(SamplerState.WM_clamp) - img.set_wrap_v(SamplerState.WM_clamp) - img.set_wrap_w(SamplerState.WM_clamp) - - def create_shaders(self): - """ Creates all the shaders used for precomputing """ - self.shaders = {} - resource_path = self.handle.get_shader_resource("eric_bruneton") - for fname in listdir(resource_path): - fpath = join(resource_path, fname) - if isfile(fpath) and fname.endswith(".compute.glsl"): - shader_name = fname.split(".")[0] - shader_obj = RPLoader.load_shader(fpath) - self.shaders[shader_name] = shader_obj - - def exec_compute_shader(self, shader_obj, shader_inputs, exec_size, - workgroup_size=(16, 16, 1)): - """ Executes a compute shader. The shader object should be a shader - loaded with Shader.load_compute, the shader inputs should be a dict where - the keys are the names of the shader inputs and the values are the - inputs. The workgroup_size has to match the size defined in the - compute shader """ - ntx = int(math.ceil(exec_size[0] / workgroup_size[0])) - nty = int(math.ceil(exec_size[1] / workgroup_size[1])) - ntz = int(math.ceil(exec_size[2] / workgroup_size[2])) - - nodepath = NodePath("shader") - nodepath.set_shader(shader_obj) - nodepath.set_shader_inputs(**shader_inputs) - - attr = nodepath.get_attrib(ShaderAttrib) - Globals.base.graphicsEngine.dispatch_compute( - (ntx, nty, ntz), attr, Globals.base.win.gsg) - - def compute(self): - """ Precomputes the scattering """ - - self.debug("Precomputing ...") - exec_cshader = self.exec_compute_shader - - # Transmittance - exec_cshader( - self.shaders["transmittance"], { - "dest": self.textures["transmittance"] - }, (self.trans_w, self.trans_h, 1)) - - # Delta E - exec_cshader( - self.shaders["delta_e"], { - "transmittanceSampler": self.textures["transmittance"], - "dest": self.textures["delta_e"] - }, (self.sky_w, self.sky_h, 1)) - - # Delta S - exec_cshader( - self.shaders["delta_sm_sr"], { - "transmittanceSampler": self.textures["transmittance"], - "destDeltaSR": self.textures["delta_sr"], - "destDeltaSM": self.textures["delta_sm"] - }, (self.res_mu_s_nu, self.res_mu, self.res_r), (8, 8, 8)) - - # Copy deltaE to irradiance texture - exec_cshader( - self.shaders["copy_irradiance"], { - "k": 0.0, - "deltaESampler": self.textures["delta_e"], - "dest": self.textures["irradiance"] - }, (self.sky_w, self.sky_h, 1)) - - # Copy delta s into inscatter texture - exec_cshader( - self.shaders["copy_inscatter"], { - "deltaSRSampler": self.textures["delta_sr"], - "deltaSMSampler": self.textures["delta_sm"], - "dest": self.textures["inscatter"] - }, (self.res_mu_s_nu, self.res_mu, self.res_r), (8, 8, 8)) - - for order in range(2, 5): - first = order == 2 - - # Delta J - exec_cshader( - self.shaders["delta_j"], { - "transmittanceSampler": self.textures["transmittance"], - "deltaSRSampler": self.textures["delta_sr"], - "deltaSMSampler": self.textures["delta_sm"], - "deltaESampler": self.textures["delta_e"], - "dest": self.textures["delta_j"], - "first": first - }, (self.res_mu_s_nu, self.res_mu, self.res_r), (8, 8, 8)) - - # Delta E - exec_cshader( - self.shaders["irradiance_n"], { - "transmittanceSampler": self.textures["transmittance"], - "deltaSRSampler": self.textures["delta_sr"], - "deltaSMSampler": self.textures["delta_sm"], - "dest": self.textures["delta_e"], - "first": first - }, (self.sky_w, self.sky_h, 1)) - - # Delta Sr - exec_cshader( - self.shaders["delta_sr"], { - "transmittanceSampler": self.textures["transmittance"], - "deltaJSampler": self.textures["delta_j"], - "dest": self.textures["delta_sr"], - "first": first - }, (self.res_mu_s_nu, self.res_mu, self.res_r), (8, 8, 8)) - - # Add delta E to irradiance - exec_cshader( - self.shaders["add_delta_e"], { - "deltaESampler": self.textures["delta_e"], - "dest": self.textures["irradiance"], - }, (self.sky_w, self.sky_h, 1)) - - # Add deltaSr to inscatter texture - exec_cshader( - self.shaders["add_delta_sr"], { - "deltaSSampler": self.textures["delta_sr"], - "dest": self.textures["inscatter"] - }, (self.res_mu_s_nu, self.res_mu, self.res_r), (8, 8, 8)) - - # Make stages available - for stage in [self.handle.display_stage, self.handle.envmap_stage]: - stage.set_shader_inputs( - InscatterSampler=self.textures["inscatter"], - transmittanceSampler=self.textures["transmittance"], - IrradianceSampler=self.textures["irradiance"]) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +import math + +from six.moves import range # pylint: disable=import-error +from rplibs.six import iteritems, itervalues + +from direct.stdpy.file import listdir, isfile, join +from panda3d.core import SamplerState, ShaderAttrib, NodePath + +from rpcore.globals import Globals +from rpcore.rpobject import RPObject +from rpcore.image import Image +from rpcore.loader import RPLoader + + +class ScatteringMethod(RPObject): + + """ Base class for all scattering methods """ + + def __init__(self, plugin_handle): + RPObject.__init__(self) + self.handle = plugin_handle + + def load(self): + """ Should load all required resources here """ + raise NotImplementedError() + + def compute(self): + """ Should compute the model here if necessary """ + raise NotImplementedError() + + +class ScatteringMethodHosekWilkie(ScatteringMethod): + + """ Scattering as suggested by Hosek and Wilkie """ + + def load(self): + """ Loads the scattering method """ + lut_src = self.handle.get_resource( + "hosek_wilkie_scattering/scattering_lut.txo") + + if not isfile(lut_src): + self.error("Could not find precompiled LUT for the Hosek Wilkie " + "Scattering! Make sure you compiled the algorithm code!") + return + + lut_tex = RPLoader.load_sliced_3d_texture(lut_src, 512, 128, 100) + lut_tex.set_wrap_u(SamplerState.WM_repeat) + lut_tex.set_wrap_v(SamplerState.WM_clamp) + lut_tex.set_wrap_w(SamplerState.WM_clamp) + lut_tex.set_minfilter(SamplerState.FT_linear) + lut_tex.set_magfilter(SamplerState.FT_linear) + + # Setting the format explicitely shouldn't be necessary + # lut_tex.set_format(Image.F_rgb16) + + self.handle.display_stage.set_shader_input("ScatteringLUT", lut_tex) + self.handle.envmap_stage.set_shader_input("ScatteringLUT", lut_tex) + + def compute(self): + """ Computes the scattering method, not required since we use a precomputed + LUT """ + pass + + +class ScatteringMethodEricBruneton(ScatteringMethod): + + """ Precomputed atmospheric scattering by Eric Bruneton """ + + def load(self): + """ Inits parameters, those should match with the ones specified in common.glsl """ + self.use_32_bit = False + self.trans_w, self.trans_h = 256 * 4, 64 * 4 + self.sky_w, self.sky_h = 64 * 4, 16 * 4 + self.res_r, self.res_mu, self.res_mu_s, self.res_nu = 32, 128, 32, 8 + self.res_mu_s_nu = self.res_mu_s * self.res_nu + + self.create_shaders() + self.create_textures() + + def create_textures(self): + """ Creates all textures required for the scattering """ + + tex_format = "RGBA32" if self.use_32_bit else "RGBA16" + img_2d, img_3d = Image.create_2d, Image.create_3d + + self.textures = { + "transmittance": img_2d("scat-trans", self.trans_w, self.trans_h, tex_format), + "irradiance": img_2d("scat-irrad", self.sky_w, self.sky_h, tex_format), + "inscatter": img_3d("scat-inscat", self.res_mu_s_nu, self.res_mu, self.res_r, tex_format), # noqa # pylint: disable=line-too-long + "delta_e": img_2d("scat-dx-e", self.sky_w, self.sky_h, tex_format), + "delta_sr": img_3d("scat-dx-sr", self.res_mu_s_nu, self.res_mu, self.res_r, tex_format), + "delta_sm": img_3d("scat-dx-sm", self.res_mu_s_nu, self.res_mu, self.res_r, tex_format), + "delta_j": img_3d("scat-dx-j", self.res_mu_s_nu, self.res_mu, self.res_r, tex_format), + } + + for img in itervalues(self.textures): + img.set_minfilter(SamplerState.FT_linear) + img.set_magfilter(SamplerState.FT_linear) + img.set_wrap_u(SamplerState.WM_clamp) + img.set_wrap_v(SamplerState.WM_clamp) + img.set_wrap_w(SamplerState.WM_clamp) + + def create_shaders(self): + """ Creates all the shaders used for precomputing """ + self.shaders = {} + resource_path = self.handle.get_shader_resource("eric_bruneton") + for fname in listdir(resource_path): + fpath = join(resource_path, fname) + if isfile(fpath) and fname.endswith(".compute.glsl"): + shader_name = fname.split(".")[0] + shader_obj = RPLoader.load_shader(fpath) + self.shaders[shader_name] = shader_obj + + def exec_compute_shader(self, shader_obj, shader_inputs, exec_size, + workgroup_size=(16, 16, 1)): + """ Executes a compute shader. The shader object should be a shader + loaded with Shader.load_compute, the shader inputs should be a dict where + the keys are the names of the shader inputs and the values are the + inputs. The workgroup_size has to match the size defined in the + compute shader """ + ntx = int(math.ceil(exec_size[0] / workgroup_size[0])) + nty = int(math.ceil(exec_size[1] / workgroup_size[1])) + ntz = int(math.ceil(exec_size[2] / workgroup_size[2])) + + nodepath = NodePath("shader") + nodepath.set_shader(shader_obj) + nodepath.set_shader_inputs(**shader_inputs) + + attr = nodepath.get_attrib(ShaderAttrib) + Globals.base.graphicsEngine.dispatch_compute( + (ntx, nty, ntz), attr, Globals.base.win.gsg) + + def compute(self): + """ Precomputes the scattering """ + + self.debug("Precomputing ...") + exec_cshader = self.exec_compute_shader + + # Transmittance + exec_cshader( + self.shaders["transmittance"], { + "dest": self.textures["transmittance"] + }, (self.trans_w, self.trans_h, 1)) + + # Delta E + exec_cshader( + self.shaders["delta_e"], { + "transmittanceSampler": self.textures["transmittance"], + "dest": self.textures["delta_e"] + }, (self.sky_w, self.sky_h, 1)) + + # Delta S + exec_cshader( + self.shaders["delta_sm_sr"], { + "transmittanceSampler": self.textures["transmittance"], + "destDeltaSR": self.textures["delta_sr"], + "destDeltaSM": self.textures["delta_sm"] + }, (self.res_mu_s_nu, self.res_mu, self.res_r), (8, 8, 8)) + + # Copy deltaE to irradiance texture + exec_cshader( + self.shaders["copy_irradiance"], { + "k": 0.0, + "deltaESampler": self.textures["delta_e"], + "dest": self.textures["irradiance"] + }, (self.sky_w, self.sky_h, 1)) + + # Copy delta s into inscatter texture + exec_cshader( + self.shaders["copy_inscatter"], { + "deltaSRSampler": self.textures["delta_sr"], + "deltaSMSampler": self.textures["delta_sm"], + "dest": self.textures["inscatter"] + }, (self.res_mu_s_nu, self.res_mu, self.res_r), (8, 8, 8)) + + for order in range(2, 5): + first = order == 2 + + # Delta J + exec_cshader( + self.shaders["delta_j"], { + "transmittanceSampler": self.textures["transmittance"], + "deltaSRSampler": self.textures["delta_sr"], + "deltaSMSampler": self.textures["delta_sm"], + "deltaESampler": self.textures["delta_e"], + "dest": self.textures["delta_j"], + "first": first + }, (self.res_mu_s_nu, self.res_mu, self.res_r), (8, 8, 8)) + + # Delta E + exec_cshader( + self.shaders["irradiance_n"], { + "transmittanceSampler": self.textures["transmittance"], + "deltaSRSampler": self.textures["delta_sr"], + "deltaSMSampler": self.textures["delta_sm"], + "dest": self.textures["delta_e"], + "first": first + }, (self.sky_w, self.sky_h, 1)) + + # Delta Sr + exec_cshader( + self.shaders["delta_sr"], { + "transmittanceSampler": self.textures["transmittance"], + "deltaJSampler": self.textures["delta_j"], + "dest": self.textures["delta_sr"], + "first": first + }, (self.res_mu_s_nu, self.res_mu, self.res_r), (8, 8, 8)) + + # Add delta E to irradiance + exec_cshader( + self.shaders["add_delta_e"], { + "deltaESampler": self.textures["delta_e"], + "dest": self.textures["irradiance"], + }, (self.sky_w, self.sky_h, 1)) + + # Add deltaSr to inscatter texture + exec_cshader( + self.shaders["add_delta_sr"], { + "deltaSSampler": self.textures["delta_sr"], + "dest": self.textures["inscatter"] + }, (self.res_mu_s_nu, self.res_mu, self.res_r), (8, 8, 8)) + + # Make stages available + for stage in [self.handle.display_stage, self.handle.envmap_stage]: + stage.set_shader_inputs( + InscatterSampler=self.textures["inscatter"], + transmittanceSampler=self.textures["transmittance"], + IrradianceSampler=self.textures["irradiance"]) diff --git a/RenderPipelineFile/rpplugins/scattering/scattering_stage.py b/RenderPipelineFile/rpplugins/scattering/scattering_stage.py index 908e5b3c..9048dd55 100644 --- a/RenderPipelineFile/rpplugins/scattering/scattering_stage.py +++ b/RenderPipelineFile/rpplugins/scattering/scattering_stage.py @@ -1,49 +1,49 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import division - -from rpcore.render_stage import RenderStage - - -class ScatteringStage(RenderStage): - - """ This stage uses the precomputed data to display the scattering """ - - required_pipes = ["ShadedScene", "GBuffer"] - required_inputs = ["DefaultSkydome"] - - @property - def produced_pipes(self): - return {"ShadedScene": self.target.color_tex} - - def create(self): - self.target = self.create_target("ApplyScattering") - self.target.add_color_attachment(bits=16, alpha=True) - self.target.prepare_buffer() - - def reload_shaders(self): - self.target.shader = self.load_plugin_shader("apply_scattering.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import division + +from rpcore.render_stage import RenderStage + + +class ScatteringStage(RenderStage): + + """ This stage uses the precomputed data to display the scattering """ + + required_pipes = ["ShadedScene", "GBuffer"] + required_inputs = ["DefaultSkydome"] + + @property + def produced_pipes(self): + return {"ShadedScene": self.target.color_tex} + + def create(self): + self.target = self.create_target("ApplyScattering") + self.target.add_color_attachment(bits=16, alpha=True) + self.target.prepare_buffer() + + def reload_shaders(self): + self.target.shader = self.load_plugin_shader("apply_scattering.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/scattering/shader/LICENSE.txt b/RenderPipelineFile/rpplugins/scattering/shader/LICENSE.txt index 2e6526eb..910af6a1 100644 --- a/RenderPipelineFile/rpplugins/scattering/shader/LICENSE.txt +++ b/RenderPipelineFile/rpplugins/scattering/shader/LICENSE.txt @@ -1,37 +1,37 @@ - -Some parts of the shaders in the plugin contain code from a -publication from Eric Bruneton: - -https://hal.inria.fr/inria-00288758/document - -Below is the license for this code: - - -Precomputed Atmospheric Scattering -Copyright (c) 2008 INRIA -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: -1. Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright -notice, this list of conditions and the following disclaimer in the -documentation and/or other materials provided with the distribution. -3. Neither the name of the copyright holders nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF -THE POSSIBILITY OF SUCH DAMAGE. - + +Some parts of the shaders in the plugin contain code from a +publication from Eric Bruneton: + +https://hal.inria.fr/inria-00288758/document + +Below is the license for this code: + + +Precomputed Atmospheric Scattering +Copyright (c) 2008 INRIA +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holders nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/RenderPipelineFile/rpplugins/scattering/shader/apply_scattering.frag.glsl b/RenderPipelineFile/rpplugins/scattering/shader/apply_scattering.frag.glsl index 63d802b3..47d33c0e 100644 --- a/RenderPipelineFile/rpplugins/scattering/shader/apply_scattering.frag.glsl +++ b/RenderPipelineFile/rpplugins/scattering/shader/apply_scattering.frag.glsl @@ -1,101 +1,101 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#define USE_TIME_OF_DAY 1 -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" - -uniform sampler2D ShadedScene; -uniform sampler2D DefaultSkydome; - -uniform GBufferData GBuffer; - -out vec4 result; - -#pragma include "scattering_method.inc.glsl" - -void main() { - - vec2 texcoord = get_texcoord(); - - // Get material data - Material m = unpack_material(GBuffer); - vec3 view_vector = normalize(m.position - MainSceneData.camera_pos); - vec3 orig_view_vector = view_vector; - - // Fetch scattering - float sky_clip = 0.0; - - // Prevent a way too dark horizon by clamping the view vector - if (is_skybox(m)) { - view_vector.z = max(view_vector.z, 0.05); - } - - vec3 inscattered_light = DoScattering(m.position, view_vector, sky_clip) - * TimeOfDay.scattering.sun_intensity; - - result.xyz = textureLod(ShadedScene, texcoord, 0).xyz; - result.w = 1; - - // Cloud color - if (is_skybox(m)) { - - inscattered_light *= M_PI; // XXX: This makes it look better, but has no physical background. - - #if !HAVE_PLUGIN(clouds) - vec3 cloud_color = textureLod(DefaultSkydome, get_skydome_coord(orig_view_vector), 0).xyz; - // inscattered_light = 15 * cloud_color * M_PI; - cloud_color = mix(vec3(0.5), cloud_color, saturate(orig_view_vector.z / 0.05)); - inscattered_light *= 0.5 + 1 * cloud_color; - #endif - - // Sun disk - vec3 silhouette_col = vec3(TimeOfDay.scattering.sun_intensity) * - inscattered_light * sky_clip; - silhouette_col *= 2.0; - float disk_factor = pow(saturate(dot(orig_view_vector, sun_vector) + 0.000069), 23.0 * 1e5); - float upper_disk_factor = smoothstep(0, 1, (orig_view_vector.z + 0.045) * 1.0); - inscattered_light += vec3(1, 0.3, 0.1) * disk_factor * upper_disk_factor * - silhouette_col * 3.0 * 1e3; - - } else { - inscattered_light *= 70.0; - float dist = distance(m.position, MainSceneData.camera_pos); - float extinction = saturate(dist / TimeOfDay.scattering.extinction * 10.0); - inscattered_light *= extinction; - } - - #if !DEBUG_MODE - result.xyz += inscattered_light; - #endif - - #if MODE_ACTIVE(SCATTERING) - result.xyz = inscattered_light / (1 + inscattered_light); - #endif - -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#define USE_TIME_OF_DAY 1 +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" + +uniform sampler2D ShadedScene; +uniform sampler2D DefaultSkydome; + +uniform GBufferData GBuffer; + +out vec4 result; + +#pragma include "scattering_method.inc.glsl" + +void main() { + + vec2 texcoord = get_texcoord(); + + // Get material data + Material m = unpack_material(GBuffer); + vec3 view_vector = normalize(m.position - MainSceneData.camera_pos); + vec3 orig_view_vector = view_vector; + + // Fetch scattering + float sky_clip = 0.0; + + // Prevent a way too dark horizon by clamping the view vector + if (is_skybox(m)) { + view_vector.z = max(view_vector.z, 0.05); + } + + vec3 inscattered_light = DoScattering(m.position, view_vector, sky_clip) + * TimeOfDay.scattering.sun_intensity; + + result.xyz = textureLod(ShadedScene, texcoord, 0).xyz; + result.w = 1; + + // Cloud color + if (is_skybox(m)) { + + inscattered_light *= M_PI; // XXX: This makes it look better, but has no physical background. + + #if !HAVE_PLUGIN(clouds) + vec3 cloud_color = textureLod(DefaultSkydome, get_skydome_coord(orig_view_vector), 0).xyz; + // inscattered_light = 15 * cloud_color * M_PI; + cloud_color = mix(vec3(0.5), cloud_color, saturate(orig_view_vector.z / 0.05)); + inscattered_light *= 0.5 + 1 * cloud_color; + #endif + + // Sun disk + vec3 silhouette_col = vec3(TimeOfDay.scattering.sun_intensity) * + inscattered_light * sky_clip; + silhouette_col *= 2.0; + float disk_factor = pow(saturate(dot(orig_view_vector, sun_vector) + 0.000069), 23.0 * 1e5); + float upper_disk_factor = smoothstep(0, 1, (orig_view_vector.z + 0.045) * 1.0); + inscattered_light += vec3(1, 0.3, 0.1) * disk_factor * upper_disk_factor * + silhouette_col * 3.0 * 1e3; + + } else { + inscattered_light *= 70.0; + float dist = distance(m.position, MainSceneData.camera_pos); + float extinction = saturate(dist / TimeOfDay.scattering.extinction * 10.0); + inscattered_light *= extinction; + } + + #if !DEBUG_MODE + result.xyz += inscattered_light; + #endif + + #if MODE_ACTIVE(SCATTERING) + result.xyz = inscattered_light / (1 + inscattered_light); + #endif + +} diff --git a/RenderPipelineFile/rpplugins/scattering/shader/compute_godrays.frag.glsl b/RenderPipelineFile/rpplugins/scattering/shader/compute_godrays.frag.glsl index 0cc58cf0..a710ffaf 100644 --- a/RenderPipelineFile/rpplugins/scattering/shader/compute_godrays.frag.glsl +++ b/RenderPipelineFile/rpplugins/scattering/shader/compute_godrays.frag.glsl @@ -1,80 +1,80 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#define USE_TIME_OF_DAY 1 -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "includes/color_spaces.inc.glsl" -#pragma include "includes/noise.inc.glsl" - -uniform sampler2D ShadedScene; -out vec3 result; - - -void main() { - vec2 texcoord = get_texcoord(); - Material m = unpack_material(GBuffer); - - // compute sun position on screen - // TODO: could move to vertex shader - vec3 sun_vector = get_sun_vector(); - vec3 sun_pos = sun_vector * 1e5; - vec4 sun_proj = MainSceneData.view_proj_mat_no_jitter * vec4(sun_pos, 1); - sun_proj.xyz /= sun_proj.w; - if (sun_proj.w < 0.0) { - result = textureLod(ShadedScene, texcoord, 0).xyz; - return; - } - sun_proj.xy = sun_proj.xy * 0.5 + 0.5; - - // raymarch to sun and collect .. whatever - float jitter = rand(texcoord) * 0.9; - - const int num_samples = 32; - vec3 accum = vec3(0); - for (int i = 0; i < num_samples; ++i) { - float t = (i + jitter) / float(num_samples - 1); - - vec2 sample_coord = mix(texcoord, sun_proj.xy, pow(t, 1.0)); - vec3 sample_data = textureLod(ShadedScene, sample_coord, 0).xyz; - - // float weight = step(get_luminance(sample_data), 1.0); - float weight = step(1.0, get_luminance(sample_data)); - // float weight = 1.0; - accum += sample_data * weight * saturate(5 * (1 - t)) * 1; - // accum += sample_data * weight; - } - - accum /= num_samples; - accum *= 0.001; - - accum += textureLod(ShadedScene, texcoord, 0).xyz; - result = vec3(accum); - -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#define USE_TIME_OF_DAY 1 +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "includes/color_spaces.inc.glsl" +#pragma include "includes/noise.inc.glsl" + +uniform sampler2D ShadedScene; +out vec3 result; + + +void main() { + vec2 texcoord = get_texcoord(); + Material m = unpack_material(GBuffer); + + // compute sun position on screen + // TODO: could move to vertex shader + vec3 sun_vector = get_sun_vector(); + vec3 sun_pos = sun_vector * 1e5; + vec4 sun_proj = MainSceneData.view_proj_mat_no_jitter * vec4(sun_pos, 1); + sun_proj.xyz /= sun_proj.w; + if (sun_proj.w < 0.0) { + result = textureLod(ShadedScene, texcoord, 0).xyz; + return; + } + sun_proj.xy = sun_proj.xy * 0.5 + 0.5; + + // raymarch to sun and collect .. whatever + float jitter = rand(texcoord) * 0.9; + + const int num_samples = 32; + vec3 accum = vec3(0); + for (int i = 0; i < num_samples; ++i) { + float t = (i + jitter) / float(num_samples - 1); + + vec2 sample_coord = mix(texcoord, sun_proj.xy, pow(t, 1.0)); + vec3 sample_data = textureLod(ShadedScene, sample_coord, 0).xyz; + + // float weight = step(get_luminance(sample_data), 1.0); + float weight = step(1.0, get_luminance(sample_data)); + // float weight = 1.0; + accum += sample_data * weight * saturate(5 * (1 - t)) * 1; + // accum += sample_data * weight; + } + + accum /= num_samples; + accum *= 0.001; + + accum += textureLod(ShadedScene, texcoord, 0).xyz; + result = vec3(accum); + +} diff --git a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/add_delta_e.compute.glsl b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/add_delta_e.compute.glsl index 7b5229d7..fe0e1c72 100644 --- a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/add_delta_e.compute.glsl +++ b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/add_delta_e.compute.glsl @@ -1,46 +1,46 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "scattering_common.glsl" - -layout(local_size_x = 16, local_size_y = 16) in; - -uniform sampler2D deltaESampler; - -#if SCATTERING_USE_32_BIT -layout(rgba32f) uniform image2D RESTRICT dest; -#else -layout(rgba16f) uniform image2D RESTRICT dest; -#endif - -void main() { - ivec2 coord = ivec2(gl_GlobalInvocationID.xy); - vec3 delta_e_val = texelFetch(deltaESampler, coord, 0).xyz; - vec4 irradiance_val = imageLoad(dest, coord); - imageStore(dest, coord, vec4(delta_e_val + irradiance_val.xyz, irradiance_val.w)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "scattering_common.glsl" + +layout(local_size_x = 16, local_size_y = 16) in; + +uniform sampler2D deltaESampler; + +#if SCATTERING_USE_32_BIT +layout(rgba32f) uniform image2D RESTRICT dest; +#else +layout(rgba16f) uniform image2D RESTRICT dest; +#endif + +void main() { + ivec2 coord = ivec2(gl_GlobalInvocationID.xy); + vec3 delta_e_val = texelFetch(deltaESampler, coord, 0).xyz; + vec4 irradiance_val = imageLoad(dest, coord); + imageStore(dest, coord, vec4(delta_e_val + irradiance_val.xyz, irradiance_val.w)); +} diff --git a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/add_delta_sr.compute.glsl b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/add_delta_sr.compute.glsl index 2d95e28b..3c8fc420 100644 --- a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/add_delta_sr.compute.glsl +++ b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/add_delta_sr.compute.glsl @@ -1,56 +1,56 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "scattering_common.glsl" - -layout(local_size_x = 8, local_size_y = 8, local_size_z = 8) in; - -uniform sampler3D deltaSSampler; - -#if SCATTERING_USE_32_BIT -layout(rgba32f) uniform image3D RESTRICT dest; -#else -layout(rgba16f) uniform image3D RESTRICT dest; -#endif - -void main() { - - ivec3 coord = ivec3(gl_GlobalInvocationID.xyz); - - float r; - vec4 dhdH; - get_r_dhdh(coord.z, r, dhdH); - - float mu, muS, nu; - getMuMuSNu(r, dhdH, mu, muS, nu); - - vec4 orig_val = imageLoad(dest, coord); - vec4 new_val = texelFetch(deltaSSampler, coord, 0) / phaseFunctionR(nu); - imageStore(dest, coord, vec4(orig_val + new_val)); - -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "scattering_common.glsl" + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 8) in; + +uniform sampler3D deltaSSampler; + +#if SCATTERING_USE_32_BIT +layout(rgba32f) uniform image3D RESTRICT dest; +#else +layout(rgba16f) uniform image3D RESTRICT dest; +#endif + +void main() { + + ivec3 coord = ivec3(gl_GlobalInvocationID.xyz); + + float r; + vec4 dhdH; + get_r_dhdh(coord.z, r, dhdH); + + float mu, muS, nu; + getMuMuSNu(r, dhdH, mu, muS, nu); + + vec4 orig_val = imageLoad(dest, coord); + vec4 new_val = texelFetch(deltaSSampler, coord, 0) / phaseFunctionR(nu); + imageStore(dest, coord, vec4(orig_val + new_val)); + +} diff --git a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/compute_scattering.inc.glsl b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/compute_scattering.inc.glsl index ba182c0e..250d67a8 100644 --- a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/compute_scattering.inc.glsl +++ b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/compute_scattering.inc.glsl @@ -1,237 +1,237 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - - - -// Include local scattering code -#define NO_COMPUTE_SHADER 1 -#pragma include "scattering_common.glsl" -#pragma include "includes/gbuffer.inc.glsl" - -uniform sampler3D InscatterSampler; -uniform sampler2D IrradianceSampler; - -vec3 sun_vector = get_sun_vector(); -/* - - -Some parts of this code are taken from the master thesis of Stefan Sperlhofer, -which unfortunately does not seem to be online anymore, so I can't link to it. - - -*/ - -const float EPSILON_ATMOSPHERE = 0.002f; -const float EPSILON_INSCATTER = 0.01f; - - -// input - d: view ray in world space -// output - offset: distance to atmosphere or 0 if within atmosphere -// output - max_path_length: distance traversed within atmosphere -// output - return value: intersection occurred true/false -bool intersect_atmosphere(vec3 cam_pos, vec3 d, out float offset, out float max_path_length) -{ - offset = 0.0f; - max_path_length = 0.0f; - - // vector from ray origin to center of the sphere - vec3 l = -cam_pos; - float l2 = dot(l, l); - float s = dot(l, d); - - // adjust top atmosphere boundary by small epsilon to prevent artifacts - float r = Rt - EPSILON_ATMOSPHERE; - float r2 = r * r; - if(l2 <= r2) - { - // ray origin inside sphere, hit is ensured - float m2 = l2 - (s * s); - float q = sqrt(r2 - m2); - max_path_length = s + q; - return true; - } - else if(s >= 0) - { - // ray starts outside in front of sphere, hit is possible - float m2 = l2 - (s * s); - if(m2 <= r2) - { - // ray hits atmosphere definitely - float q = sqrt(r2 - m2); - offset = s - q; - max_path_length = (s + q) - offset; - return true; - } - } - return false; -} - - -// Transforms a point from world-space to atmospheric-space. -// This is required because we use different coordinate systems for normal -// rendering and atmospheric scattering. -vec3 worldspace_to_atmosphere(vec3 pos) { - pos.z += GET_SETTING(scattering, atmosphere_start); - pos /= 1000.0; - pos.z = max(0, pos.z); - pos.z += Rg; - return pos; -} - -vec3 get_inscattered_light(vec3 surface_pos, vec3 view_dir, inout vec3 attenuation, - inout float irradiance_factor) -{ - vec3 inscattered_light = vec3(0); - float offset; - float max_path_length; - - vec3 cam_pos = MainSceneData.camera_pos; - // cam_pos.z = max(cam_pos.z, surface_pos.z); - cam_pos = worldspace_to_atmosphere(cam_pos); - - if (is_skybox(surface_pos)) { - surface_pos = worldspace_to_atmosphere(surface_pos * 1e10); - } else { - surface_pos = worldspace_to_atmosphere(surface_pos); - } - - if (intersect_atmosphere(cam_pos, view_dir, offset, max_path_length)) { - float path_length = distance(cam_pos, surface_pos); - - // check if object occludes atmosphere - if (path_length > offset) { - - // offsetting camera - vec3 start_pos = cam_pos + offset * view_dir; - float start_pos_height = length(start_pos); - path_length -= offset; - - // starting position of path is now ensured to be inside atmosphere - // was either originally there or has been moved to top boundary - float mustart_pos = dot(start_pos, view_dir) / start_pos_height; - float nustart_pos = dot(view_dir, sun_vector); - float musstart_pos = dot(start_pos, sun_vector) / start_pos_height; - - // in-scattering for infinite ray (light in-scattered when - // no surface hit or object behind atmosphere) - vec4 inscatter = max(texture4D(InscatterSampler, start_pos_height, - mustart_pos, musstart_pos, nustart_pos), 0.0f); - float surface_pos_height = length(surface_pos); - float musEndPos = dot(surface_pos, sun_vector) / surface_pos_height; - - // check if object hit is inside atmosphere - if (path_length < max_path_length) { - - // reduce total in-scattered light when surface hit - // within atmosphere - // fíx described in chapter 5.1.1 - attenuation = analyticTransmittance(start_pos_height, mustart_pos, path_length); - float muEndPos = dot(surface_pos, view_dir) / surface_pos_height; - vec4 inscatterSurface = texture4D(InscatterSampler, surface_pos_height, - muEndPos, musEndPos, nustart_pos); - inscatter = max(inscatter - attenuation.rgbr * inscatterSurface, 0.0f); - irradiance_factor = 1.0f; - } else { - // retrieve extinction factor for infinite ray - // fíx described in chapter 5.1.1 - attenuation = analyticTransmittance(start_pos_height, mustart_pos, path_length); - } - - // avoids imprecision problems near horizon by interpolating between - // two points above and below horizon - // fíx described in chapter 5.1.2 - - #if 0 - float muHorizon = -sqrt(1.0 - (Rg / start_pos_height) * (Rg / start_pos_height)); - if (abs(mustart_pos - muHorizon) < EPSILON_INSCATTER) - { - float mu = muHorizon - EPSILON_INSCATTER; - float samplePosHeight = sqrt(start_pos_height * start_pos_height - + path_length * path_length + 2.0f * start_pos_height * - path_length * mu); - float muSamplePos = (start_pos_height * mu + path_length) / samplePosHeight; - vec4 inScatter0 = texture4D(InscatterSampler, start_pos_height, mu, - musstart_pos, nustart_pos); - vec4 inScatter1 = texture4D(InscatterSampler, samplePosHeight, - muSamplePos, musEndPos, nustart_pos); - vec4 inScatterA = max(inScatter0 - attenuation.rgbr * inScatter1, 0.0); - mu = muHorizon + EPSILON_INSCATTER; - samplePosHeight = sqrt(start_pos_height * start_pos_height - + path_length * path_length + 2.0f * - start_pos_height * path_length * mu); - muSamplePos = (start_pos_height * mu + path_length) / samplePosHeight; - inScatter0 = texture4D(InscatterSampler, start_pos_height, mu, - musstart_pos, nustart_pos); - inScatter1 = texture4D(InscatterSampler, samplePosHeight, muSamplePos, - musEndPos, nustart_pos); - vec4 inScatterB = max(inScatter0 - attenuation.rgbr * inScatter1, - 0.0f); - float t = ((mustart_pos - muHorizon) + EPSILON_INSCATTER) / - (2.0 * EPSILON_INSCATTER); - inscatter = mix(inScatterA, inScatterB, t); - } - #endif - - // avoids imprecision problems in Mie scattering when sun is below - //horizon - // fíx described in chapter 5.1.3 - inscatter.w *= smoothstep(0.00, 0.02, musstart_pos); - float phaseR = phaseFunctionR(nustart_pos); - float phaseM = phaseFunctionM(nustart_pos); - - phaseR *= GET_SETTING(scattering, rayleigh_factor); - - inscattered_light = max(inscatter.rgb * phaseR + getMie(inscatter) * phaseM, 0.0f); - - } - } - - - - #if !HAVE_PLUGIN(color_correction) - // Reduce scattering, otherwise its way too bright without automatic - // exposure - // inscattered_light /= 7.0; - #endif - - return inscattered_light; -} - - - -vec3 DoScattering(vec3 surface_pos, vec3 view_dir, out float sky_clip) -{ - vec3 attenuation = vec3(0); - float irradiance_factor = 0.0; - vec3 scattering = get_inscattered_light(surface_pos, view_dir, attenuation, irradiance_factor); - - sky_clip = is_skybox(surface_pos) ? 1.0 : 0.0; - - // Reduce scattering in the near to avoid artifacts due to low precision - scattering *= saturate(distance(surface_pos, MainSceneData.camera_pos) / 500.0 - 0.0); - - return scattering; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + + + +// Include local scattering code +#define NO_COMPUTE_SHADER 1 +#pragma include "scattering_common.glsl" +#pragma include "includes/gbuffer.inc.glsl" + +uniform sampler3D InscatterSampler; +uniform sampler2D IrradianceSampler; + +vec3 sun_vector = get_sun_vector(); +/* + + +Some parts of this code are taken from the master thesis of Stefan Sperlhofer, +which unfortunately does not seem to be online anymore, so I can't link to it. + + +*/ + +const float EPSILON_ATMOSPHERE = 0.002f; +const float EPSILON_INSCATTER = 0.01f; + + +// input - d: view ray in world space +// output - offset: distance to atmosphere or 0 if within atmosphere +// output - max_path_length: distance traversed within atmosphere +// output - return value: intersection occurred true/false +bool intersect_atmosphere(vec3 cam_pos, vec3 d, out float offset, out float max_path_length) +{ + offset = 0.0f; + max_path_length = 0.0f; + + // vector from ray origin to center of the sphere + vec3 l = -cam_pos; + float l2 = dot(l, l); + float s = dot(l, d); + + // adjust top atmosphere boundary by small epsilon to prevent artifacts + float r = Rt - EPSILON_ATMOSPHERE; + float r2 = r * r; + if(l2 <= r2) + { + // ray origin inside sphere, hit is ensured + float m2 = l2 - (s * s); + float q = sqrt(r2 - m2); + max_path_length = s + q; + return true; + } + else if(s >= 0) + { + // ray starts outside in front of sphere, hit is possible + float m2 = l2 - (s * s); + if(m2 <= r2) + { + // ray hits atmosphere definitely + float q = sqrt(r2 - m2); + offset = s - q; + max_path_length = (s + q) - offset; + return true; + } + } + return false; +} + + +// Transforms a point from world-space to atmospheric-space. +// This is required because we use different coordinate systems for normal +// rendering and atmospheric scattering. +vec3 worldspace_to_atmosphere(vec3 pos) { + pos.z += GET_SETTING(scattering, atmosphere_start); + pos /= 1000.0; + pos.z = max(0, pos.z); + pos.z += Rg; + return pos; +} + +vec3 get_inscattered_light(vec3 surface_pos, vec3 view_dir, inout vec3 attenuation, + inout float irradiance_factor) +{ + vec3 inscattered_light = vec3(0); + float offset; + float max_path_length; + + vec3 cam_pos = MainSceneData.camera_pos; + // cam_pos.z = max(cam_pos.z, surface_pos.z); + cam_pos = worldspace_to_atmosphere(cam_pos); + + if (is_skybox(surface_pos)) { + surface_pos = worldspace_to_atmosphere(surface_pos * 1e10); + } else { + surface_pos = worldspace_to_atmosphere(surface_pos); + } + + if (intersect_atmosphere(cam_pos, view_dir, offset, max_path_length)) { + float path_length = distance(cam_pos, surface_pos); + + // check if object occludes atmosphere + if (path_length > offset) { + + // offsetting camera + vec3 start_pos = cam_pos + offset * view_dir; + float start_pos_height = length(start_pos); + path_length -= offset; + + // starting position of path is now ensured to be inside atmosphere + // was either originally there or has been moved to top boundary + float mustart_pos = dot(start_pos, view_dir) / start_pos_height; + float nustart_pos = dot(view_dir, sun_vector); + float musstart_pos = dot(start_pos, sun_vector) / start_pos_height; + + // in-scattering for infinite ray (light in-scattered when + // no surface hit or object behind atmosphere) + vec4 inscatter = max(texture4D(InscatterSampler, start_pos_height, + mustart_pos, musstart_pos, nustart_pos), 0.0f); + float surface_pos_height = length(surface_pos); + float musEndPos = dot(surface_pos, sun_vector) / surface_pos_height; + + // check if object hit is inside atmosphere + if (path_length < max_path_length) { + + // reduce total in-scattered light when surface hit + // within atmosphere + // fíx described in chapter 5.1.1 + attenuation = analyticTransmittance(start_pos_height, mustart_pos, path_length); + float muEndPos = dot(surface_pos, view_dir) / surface_pos_height; + vec4 inscatterSurface = texture4D(InscatterSampler, surface_pos_height, + muEndPos, musEndPos, nustart_pos); + inscatter = max(inscatter - attenuation.rgbr * inscatterSurface, 0.0f); + irradiance_factor = 1.0f; + } else { + // retrieve extinction factor for infinite ray + // fíx described in chapter 5.1.1 + attenuation = analyticTransmittance(start_pos_height, mustart_pos, path_length); + } + + // avoids imprecision problems near horizon by interpolating between + // two points above and below horizon + // fíx described in chapter 5.1.2 + + #if 0 + float muHorizon = -sqrt(1.0 - (Rg / start_pos_height) * (Rg / start_pos_height)); + if (abs(mustart_pos - muHorizon) < EPSILON_INSCATTER) + { + float mu = muHorizon - EPSILON_INSCATTER; + float samplePosHeight = sqrt(start_pos_height * start_pos_height + + path_length * path_length + 2.0f * start_pos_height * + path_length * mu); + float muSamplePos = (start_pos_height * mu + path_length) / samplePosHeight; + vec4 inScatter0 = texture4D(InscatterSampler, start_pos_height, mu, + musstart_pos, nustart_pos); + vec4 inScatter1 = texture4D(InscatterSampler, samplePosHeight, + muSamplePos, musEndPos, nustart_pos); + vec4 inScatterA = max(inScatter0 - attenuation.rgbr * inScatter1, 0.0); + mu = muHorizon + EPSILON_INSCATTER; + samplePosHeight = sqrt(start_pos_height * start_pos_height + + path_length * path_length + 2.0f * + start_pos_height * path_length * mu); + muSamplePos = (start_pos_height * mu + path_length) / samplePosHeight; + inScatter0 = texture4D(InscatterSampler, start_pos_height, mu, + musstart_pos, nustart_pos); + inScatter1 = texture4D(InscatterSampler, samplePosHeight, muSamplePos, + musEndPos, nustart_pos); + vec4 inScatterB = max(inScatter0 - attenuation.rgbr * inScatter1, + 0.0f); + float t = ((mustart_pos - muHorizon) + EPSILON_INSCATTER) / + (2.0 * EPSILON_INSCATTER); + inscatter = mix(inScatterA, inScatterB, t); + } + #endif + + // avoids imprecision problems in Mie scattering when sun is below + //horizon + // fíx described in chapter 5.1.3 + inscatter.w *= smoothstep(0.00, 0.02, musstart_pos); + float phaseR = phaseFunctionR(nustart_pos); + float phaseM = phaseFunctionM(nustart_pos); + + phaseR *= GET_SETTING(scattering, rayleigh_factor); + + inscattered_light = max(inscatter.rgb * phaseR + getMie(inscatter) * phaseM, 0.0f); + + } + } + + + + #if !HAVE_PLUGIN(color_correction) + // Reduce scattering, otherwise its way too bright without automatic + // exposure + // inscattered_light /= 7.0; + #endif + + return inscattered_light; +} + + + +vec3 DoScattering(vec3 surface_pos, vec3 view_dir, out float sky_clip) +{ + vec3 attenuation = vec3(0); + float irradiance_factor = 0.0; + vec3 scattering = get_inscattered_light(surface_pos, view_dir, attenuation, irradiance_factor); + + sky_clip = is_skybox(surface_pos) ? 1.0 : 0.0; + + // Reduce scattering in the near to avoid artifacts due to low precision + scattering *= saturate(distance(surface_pos, MainSceneData.camera_pos) / 500.0 - 0.0); + + return scattering; +} diff --git a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/copy_inscatter.compute.glsl b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/copy_inscatter.compute.glsl index 4672bbd8..a2c0aa6b 100644 --- a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/copy_inscatter.compute.glsl +++ b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/copy_inscatter.compute.glsl @@ -1,44 +1,44 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "scattering_common.glsl" - -layout(local_size_x = 8, local_size_y = 8, local_size_z = 8) in; - -uniform sampler3D deltaSRSampler; -uniform sampler3D deltaSMSampler; -uniform writeonly image3D RESTRICT dest; - -void main() { - ivec2 coord = ivec2(gl_GlobalInvocationID.xy); - int layer = int(gl_GlobalInvocationID.z); - - vec3 ray = texelFetch(deltaSRSampler, ivec3(coord, layer), 0).xyz; - vec3 mie = texelFetch(deltaSMSampler, ivec3(coord, layer), 0).xyz; - imageStore(dest, ivec3(coord, layer), vec4(ray.xyz, mie.x)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "scattering_common.glsl" + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 8) in; + +uniform sampler3D deltaSRSampler; +uniform sampler3D deltaSMSampler; +uniform writeonly image3D RESTRICT dest; + +void main() { + ivec2 coord = ivec2(gl_GlobalInvocationID.xy); + int layer = int(gl_GlobalInvocationID.z); + + vec3 ray = texelFetch(deltaSRSampler, ivec3(coord, layer), 0).xyz; + vec3 mie = texelFetch(deltaSMSampler, ivec3(coord, layer), 0).xyz; + imageStore(dest, ivec3(coord, layer), vec4(ray.xyz, mie.x)); +} diff --git a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/copy_irradiance.compute.glsl b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/copy_irradiance.compute.glsl index 89a8b5ce..49746e01 100644 --- a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/copy_irradiance.compute.glsl +++ b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/copy_irradiance.compute.glsl @@ -1,42 +1,42 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "scattering_common.glsl" - -layout(local_size_x = 16, local_size_y = 16) in; - -uniform float k; -uniform sampler2D deltaESampler; - -uniform writeonly image2D RESTRICT dest; - -void main() { - ivec2 coord = ivec2(gl_GlobalInvocationID.xy); - vec4 delta_e_val = texelFetch(deltaESampler, coord, 0); - imageStore(dest, coord, vec4(k * delta_e_val)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "scattering_common.glsl" + +layout(local_size_x = 16, local_size_y = 16) in; + +uniform float k; +uniform sampler2D deltaESampler; + +uniform writeonly image2D RESTRICT dest; + +void main() { + ivec2 coord = ivec2(gl_GlobalInvocationID.xy); + vec4 delta_e_val = texelFetch(deltaESampler, coord, 0); + imageStore(dest, coord, vec4(k * delta_e_val)); +} diff --git a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/delta_e.compute.glsl b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/delta_e.compute.glsl index 967288a7..3115f9d7 100644 --- a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/delta_e.compute.glsl +++ b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/delta_e.compute.glsl @@ -1,39 +1,39 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "scattering_common.glsl" - -layout(local_size_x = 16, local_size_y = 16) in; -uniform writeonly image2D RESTRICT dest; - -void main() { - ivec2 coord = ivec2(gl_GlobalInvocationID.xy); - float r, muS; - getIrradianceRMuS(r, muS); - imageStore(dest, coord, vec4(transmittance(r, muS) * max(muS, 0.0), SCAT_DEBUG_ALPHA)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "scattering_common.glsl" + +layout(local_size_x = 16, local_size_y = 16) in; +uniform writeonly image2D RESTRICT dest; + +void main() { + ivec2 coord = ivec2(gl_GlobalInvocationID.xy); + float r, muS; + getIrradianceRMuS(r, muS); + imageStore(dest, coord, vec4(transmittance(r, muS) * max(muS, 0.0), SCAT_DEBUG_ALPHA)); +} diff --git a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/delta_j.compute.glsl b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/delta_j.compute.glsl index 5fb6d1ed..815bc19e 100644 --- a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/delta_j.compute.glsl +++ b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/delta_j.compute.glsl @@ -1,130 +1,130 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "scattering_common.glsl" - -layout(local_size_x = 8, local_size_y = 8, local_size_z = 8) in; - -uniform sampler2D deltaESampler; -uniform sampler3D deltaSRSampler; -uniform sampler3D deltaSMSampler; -uniform bool first; - -uniform writeonly image3D RESTRICT dest; - -const float dphi = M_PI / float(INSCATTER_SPHERICAL_INTEGRAL_SAMPLES); -const float dtheta = M_PI / float(INSCATTER_SPHERICAL_INTEGRAL_SAMPLES); - -void inscatter(float r, float mu, float muS, float nu, out vec3 raymie) { - r = clamp(r, Rg, Rt); - mu = clamp(mu, -1.0, 1.0); - muS = clamp(muS, -1.0, 1.0); - float var = sqrt(1.0 - mu * mu) * sqrt(1.0 - muS * muS); - nu = clamp(nu, muS * mu - var, muS * mu + var); - - float cthetamin = -sqrt(1.0 - (Rg / r) * (Rg / r)); - - vec3 v = vec3(sqrt(1.0 - mu * mu), 0.0, mu); - float sx = v.x == 0.0 ? 0.0 : (nu - muS * mu) / v.x; - vec3 s = vec3(sx, sqrt(max(0.0, 1.0 - sx * sx - muS * muS)), muS); - - raymie = vec3(0.0); - - // integral over 4.PI around x with two nested loops over w directions (theta,phi) -- Eq (7) - for (int itheta = 0; itheta < INSCATTER_SPHERICAL_INTEGRAL_SAMPLES; ++itheta) { - float theta = (float(itheta) + 0.5) * dtheta; - float ctheta = cos(theta); - - float greflectance = 0.0; - float dground = 0.0; - vec3 gtransp = vec3(0.0); - if (ctheta < cthetamin) { // if ground visible in direction w - // compute transparency gtransp between x and ground - greflectance = AVERAGE_GROUND_REFLECTANCE / M_PI; - dground = -r * ctheta - sqrt(r * r * (ctheta * ctheta - 1.0) + Rg * Rg); - gtransp = transmittance(Rg, -(r * ctheta + dground) / Rg, dground); - } - - for (int iphi = 0; iphi < 2 * INSCATTER_SPHERICAL_INTEGRAL_SAMPLES; ++iphi) { - float phi = (float(iphi) + 0.5) * dphi; - float dw = dtheta * dphi * sin(theta); - vec3 w = vec3(cos(phi) * sin(theta), sin(phi) * sin(theta), ctheta); - - float nu1 = dot(s, w); - float nu2 = dot(v, w); - float pr2 = phaseFunctionR(nu2); - float pm2 = phaseFunctionM(nu2); - - // compute irradiance received at ground in direction w (if ground visible) =deltaE - vec3 gnormal = (vec3(0.0, 0.0, r) + dground * w) / Rg; - vec3 girradiance = irradiance(deltaESampler, Rg, dot(gnormal, s)); - - vec3 raymie1; // light arriving at x from direction w - - // first term = light reflected from the ground and attenuated before reaching x, =T.alpha/PI.deltaE - raymie1 = greflectance * girradiance * gtransp; - - // second term = inscattered light, =deltaS - if (first) { - // first iteration is special because Rayleigh and Mie were stored separately, - // without the phase functions factors; they must be reintroduced here - float pr1 = phaseFunctionR(nu1); - float pm1 = phaseFunctionM(nu1); - vec3 ray1 = texture4D(deltaSRSampler, r, w.z, muS, nu1).rgb; - vec3 mie1 = texture4D(deltaSMSampler, r, w.z, muS, nu1).rgb; - raymie1 += ray1 * pr1 + mie1 * pm1; - } else { - raymie1 += texture4D(deltaSRSampler, r, w.z, muS, nu1).rgb; - } - - // light coming from direction w and scattered in direction v - // = light arriving at x from direction w (raymie1) * SUM(scattering coefficient * phaseFunction) - // see Eq (7) - raymie += raymie1 * (betaR * exp(-(r - Rg) / HR) * pr2 + betaMSca * - exp(-(r - Rg) / HM) * pm2) * dw; - } - } - - // output raymie = J[T.alpha/PI.deltaE + deltaS] (line 7 in algorithm 4.1) -} - -void main() { - ivec2 coord = ivec2(gl_GlobalInvocationID.xy); - int layer = int(gl_GlobalInvocationID.z); - - float r; - vec4 dhdH; - get_r_dhdh(layer, r, dhdH); - - vec3 raymie; - float mu, muS, nu; - getMuMuSNu(r, dhdH, mu, muS, nu); - inscatter(r, mu, muS, nu, raymie); - - imageStore(dest, ivec3(coord, layer), vec4(raymie, SCAT_DEBUG_ALPHA)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "scattering_common.glsl" + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 8) in; + +uniform sampler2D deltaESampler; +uniform sampler3D deltaSRSampler; +uniform sampler3D deltaSMSampler; +uniform bool first; + +uniform writeonly image3D RESTRICT dest; + +const float dphi = M_PI / float(INSCATTER_SPHERICAL_INTEGRAL_SAMPLES); +const float dtheta = M_PI / float(INSCATTER_SPHERICAL_INTEGRAL_SAMPLES); + +void inscatter(float r, float mu, float muS, float nu, out vec3 raymie) { + r = clamp(r, Rg, Rt); + mu = clamp(mu, -1.0, 1.0); + muS = clamp(muS, -1.0, 1.0); + float var = sqrt(1.0 - mu * mu) * sqrt(1.0 - muS * muS); + nu = clamp(nu, muS * mu - var, muS * mu + var); + + float cthetamin = -sqrt(1.0 - (Rg / r) * (Rg / r)); + + vec3 v = vec3(sqrt(1.0 - mu * mu), 0.0, mu); + float sx = v.x == 0.0 ? 0.0 : (nu - muS * mu) / v.x; + vec3 s = vec3(sx, sqrt(max(0.0, 1.0 - sx * sx - muS * muS)), muS); + + raymie = vec3(0.0); + + // integral over 4.PI around x with two nested loops over w directions (theta,phi) -- Eq (7) + for (int itheta = 0; itheta < INSCATTER_SPHERICAL_INTEGRAL_SAMPLES; ++itheta) { + float theta = (float(itheta) + 0.5) * dtheta; + float ctheta = cos(theta); + + float greflectance = 0.0; + float dground = 0.0; + vec3 gtransp = vec3(0.0); + if (ctheta < cthetamin) { // if ground visible in direction w + // compute transparency gtransp between x and ground + greflectance = AVERAGE_GROUND_REFLECTANCE / M_PI; + dground = -r * ctheta - sqrt(r * r * (ctheta * ctheta - 1.0) + Rg * Rg); + gtransp = transmittance(Rg, -(r * ctheta + dground) / Rg, dground); + } + + for (int iphi = 0; iphi < 2 * INSCATTER_SPHERICAL_INTEGRAL_SAMPLES; ++iphi) { + float phi = (float(iphi) + 0.5) * dphi; + float dw = dtheta * dphi * sin(theta); + vec3 w = vec3(cos(phi) * sin(theta), sin(phi) * sin(theta), ctheta); + + float nu1 = dot(s, w); + float nu2 = dot(v, w); + float pr2 = phaseFunctionR(nu2); + float pm2 = phaseFunctionM(nu2); + + // compute irradiance received at ground in direction w (if ground visible) =deltaE + vec3 gnormal = (vec3(0.0, 0.0, r) + dground * w) / Rg; + vec3 girradiance = irradiance(deltaESampler, Rg, dot(gnormal, s)); + + vec3 raymie1; // light arriving at x from direction w + + // first term = light reflected from the ground and attenuated before reaching x, =T.alpha/PI.deltaE + raymie1 = greflectance * girradiance * gtransp; + + // second term = inscattered light, =deltaS + if (first) { + // first iteration is special because Rayleigh and Mie were stored separately, + // without the phase functions factors; they must be reintroduced here + float pr1 = phaseFunctionR(nu1); + float pm1 = phaseFunctionM(nu1); + vec3 ray1 = texture4D(deltaSRSampler, r, w.z, muS, nu1).rgb; + vec3 mie1 = texture4D(deltaSMSampler, r, w.z, muS, nu1).rgb; + raymie1 += ray1 * pr1 + mie1 * pm1; + } else { + raymie1 += texture4D(deltaSRSampler, r, w.z, muS, nu1).rgb; + } + + // light coming from direction w and scattered in direction v + // = light arriving at x from direction w (raymie1) * SUM(scattering coefficient * phaseFunction) + // see Eq (7) + raymie += raymie1 * (betaR * exp(-(r - Rg) / HR) * pr2 + betaMSca * + exp(-(r - Rg) / HM) * pm2) * dw; + } + } + + // output raymie = J[T.alpha/PI.deltaE + deltaS] (line 7 in algorithm 4.1) +} + +void main() { + ivec2 coord = ivec2(gl_GlobalInvocationID.xy); + int layer = int(gl_GlobalInvocationID.z); + + float r; + vec4 dhdH; + get_r_dhdh(layer, r, dhdH); + + vec3 raymie; + float mu, muS, nu; + getMuMuSNu(r, dhdH, mu, muS, nu); + inscatter(r, mu, muS, nu, raymie); + + imageStore(dest, ivec3(coord, layer), vec4(raymie, SCAT_DEBUG_ALPHA)); +} diff --git a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/delta_sm_sr.compute.glsl b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/delta_sm_sr.compute.glsl index 84da095c..f7822158 100644 --- a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/delta_sm_sr.compute.glsl +++ b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/delta_sm_sr.compute.glsl @@ -1,93 +1,93 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "scattering_common.glsl" - -layout(local_size_x = 8, local_size_y = 8, local_size_z = 8) in; - -uniform writeonly image3D RESTRICT destDeltaSR; -uniform writeonly image3D RESTRICT destDeltaSM; - -void integrand(float r, float mu, float muS, float nu, float t, out vec3 ray, out vec3 mie) { - ray = vec3(0.0); - mie = vec3(0.0); - float ri = sqrt(r * r + t * t + 2.0 * r * mu * t); - float muSi = (nu * t + muS * r) / ri; - ri = max(Rg, ri); - if (muSi >= -sqrt(1.0 - Rg * Rg / (ri * ri))) { - vec3 ti = transmittance(r, mu, t) * transmittance(ri, muSi); - ray = exp(-(ri - Rg) / HR) * ti; - mie = exp(-(ri - Rg) / HM) * ti; - } -} - - -void inscatter(float r, float mu, float muS, float nu, out vec3 ray, out vec3 mie) { - ray = vec3(0.0); - mie = vec3(0.0); - float dx = limit(r, mu) / float(INSCATTER_INTEGRAL_SAMPLES); - float xi = 0.0; - vec3 rayi; - vec3 miei; - integrand(r, mu, muS, nu, 0.0, rayi, miei); - for (int i = 1; i <= INSCATTER_INTEGRAL_SAMPLES; ++i) { - float xj = float(i) * dx; - vec3 rayj; - vec3 miej; - integrand(r, mu, muS, nu, xj, rayj, miej); - ray += (rayi + rayj) / 2.0 * dx; - mie += (miei + miej) / 2.0 * dx; - xi = xj; - rayi = rayj; - miei = miej; - } - ray *= betaR; - mie *= betaMSca; -} - - -void main() { - ivec2 coord = ivec2(gl_GlobalInvocationID.xy); - int layer = int(gl_GlobalInvocationID.z); - - float r; - vec4 dhdH; - get_r_dhdh(layer, r, dhdH); - - vec3 ray; - vec3 mie; - float mu, muS, nu; - getMuMuSNu(r, dhdH, mu, muS, nu); - inscatter(r, mu, muS, nu, ray, mie); - // store separately Rayleigh and Mie contributions, WITHOUT the phase function factor - // (cf "Angular precision") - - imageStore(destDeltaSR, ivec3(coord, layer), vec4(ray, SCAT_DEBUG_ALPHA)); - imageStore(destDeltaSM, ivec3(coord, layer), vec4(mie, SCAT_DEBUG_ALPHA)); - -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "scattering_common.glsl" + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 8) in; + +uniform writeonly image3D RESTRICT destDeltaSR; +uniform writeonly image3D RESTRICT destDeltaSM; + +void integrand(float r, float mu, float muS, float nu, float t, out vec3 ray, out vec3 mie) { + ray = vec3(0.0); + mie = vec3(0.0); + float ri = sqrt(r * r + t * t + 2.0 * r * mu * t); + float muSi = (nu * t + muS * r) / ri; + ri = max(Rg, ri); + if (muSi >= -sqrt(1.0 - Rg * Rg / (ri * ri))) { + vec3 ti = transmittance(r, mu, t) * transmittance(ri, muSi); + ray = exp(-(ri - Rg) / HR) * ti; + mie = exp(-(ri - Rg) / HM) * ti; + } +} + + +void inscatter(float r, float mu, float muS, float nu, out vec3 ray, out vec3 mie) { + ray = vec3(0.0); + mie = vec3(0.0); + float dx = limit(r, mu) / float(INSCATTER_INTEGRAL_SAMPLES); + float xi = 0.0; + vec3 rayi; + vec3 miei; + integrand(r, mu, muS, nu, 0.0, rayi, miei); + for (int i = 1; i <= INSCATTER_INTEGRAL_SAMPLES; ++i) { + float xj = float(i) * dx; + vec3 rayj; + vec3 miej; + integrand(r, mu, muS, nu, xj, rayj, miej); + ray += (rayi + rayj) / 2.0 * dx; + mie += (miei + miej) / 2.0 * dx; + xi = xj; + rayi = rayj; + miei = miej; + } + ray *= betaR; + mie *= betaMSca; +} + + +void main() { + ivec2 coord = ivec2(gl_GlobalInvocationID.xy); + int layer = int(gl_GlobalInvocationID.z); + + float r; + vec4 dhdH; + get_r_dhdh(layer, r, dhdH); + + vec3 ray; + vec3 mie; + float mu, muS, nu; + getMuMuSNu(r, dhdH, mu, muS, nu); + inscatter(r, mu, muS, nu, ray, mie); + // store separately Rayleigh and Mie contributions, WITHOUT the phase function factor + // (cf "Angular precision") + + imageStore(destDeltaSR, ivec3(coord, layer), vec4(ray, SCAT_DEBUG_ALPHA)); + imageStore(destDeltaSM, ivec3(coord, layer), vec4(mie, SCAT_DEBUG_ALPHA)); + +} diff --git a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/delta_sr.compute.glsl b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/delta_sr.compute.glsl index dcd21db8..13cd8195 100644 --- a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/delta_sr.compute.glsl +++ b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/delta_sr.compute.glsl @@ -1,70 +1,70 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "scattering_common.glsl" - -layout(local_size_x = 8, local_size_y = 8, local_size_z = 8) in; - -uniform sampler3D deltaJSampler; -uniform writeonly image3D RESTRICT dest; - -vec3 integrand(float r, float mu, float muS, float nu, float t) { - float ri = sqrt(r * r + t * t + 2.0 * r * mu * t); - float mui = (r * mu + t) / ri; - float muSi = (nu * t + muS * r) / ri; - return texture4D(deltaJSampler, ri, mui, muSi, nu).rgb * transmittance(r, mu, t); -} - -vec3 inscatter(float r, float mu, float muS, float nu) { - vec3 raymie = vec3(0.0); - float dx = limit(r, mu) / float(INSCATTER_INTEGRAL_SAMPLES); - float xi = 0.0; - vec3 raymiei = integrand(r, mu, muS, nu, 0.0); - for (int i = 1; i <= INSCATTER_INTEGRAL_SAMPLES; ++i) { - float xj = float(i) * dx; - vec3 raymiej = integrand(r, mu, muS, nu, xj); - raymie += (raymiei + raymiej) / 2.0 * dx; - xi = xj; - raymiei = raymiej; - } - return raymie; -} - -void main() { - ivec2 coord = ivec2(gl_GlobalInvocationID.xy); - int layer = int(gl_GlobalInvocationID.z); - - float r; - vec4 dhdH; - get_r_dhdh(layer, r, dhdH); - - float mu, muS, nu; - getMuMuSNu(r, dhdH, mu, muS, nu); - vec3 result = inscatter(r, mu, muS, nu); - imageStore(dest, ivec3(coord, layer), vec4(result, SCAT_DEBUG_ALPHA)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "scattering_common.glsl" + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 8) in; + +uniform sampler3D deltaJSampler; +uniform writeonly image3D RESTRICT dest; + +vec3 integrand(float r, float mu, float muS, float nu, float t) { + float ri = sqrt(r * r + t * t + 2.0 * r * mu * t); + float mui = (r * mu + t) / ri; + float muSi = (nu * t + muS * r) / ri; + return texture4D(deltaJSampler, ri, mui, muSi, nu).rgb * transmittance(r, mu, t); +} + +vec3 inscatter(float r, float mu, float muS, float nu) { + vec3 raymie = vec3(0.0); + float dx = limit(r, mu) / float(INSCATTER_INTEGRAL_SAMPLES); + float xi = 0.0; + vec3 raymiei = integrand(r, mu, muS, nu, 0.0); + for (int i = 1; i <= INSCATTER_INTEGRAL_SAMPLES; ++i) { + float xj = float(i) * dx; + vec3 raymiej = integrand(r, mu, muS, nu, xj); + raymie += (raymiei + raymiej) / 2.0 * dx; + xi = xj; + raymiei = raymiej; + } + return raymie; +} + +void main() { + ivec2 coord = ivec2(gl_GlobalInvocationID.xy); + int layer = int(gl_GlobalInvocationID.z); + + float r; + vec4 dhdH; + get_r_dhdh(layer, r, dhdH); + + float mu, muS, nu; + getMuMuSNu(r, dhdH, mu, muS, nu); + vec3 result = inscatter(r, mu, muS, nu); + imageStore(dest, ivec3(coord, layer), vec4(result, SCAT_DEBUG_ALPHA)); +} diff --git a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/irradiance_n.compute.glsl b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/irradiance_n.compute.glsl index db4f56bd..ba29b5b5 100644 --- a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/irradiance_n.compute.glsl +++ b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/irradiance_n.compute.glsl @@ -1,72 +1,72 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "scattering_common.glsl" - -layout(local_size_x = 16, local_size_y = 16) in; - -uniform sampler3D deltaSRSampler; -uniform sampler3D deltaSMSampler; -uniform bool first; - -uniform writeonly image2D RESTRICT dest; - -const float dphi = M_PI / float(IRRADIANCE_INTEGRAL_SAMPLES); -const float dtheta = M_PI / float(IRRADIANCE_INTEGRAL_SAMPLES); - -void main() { - ivec2 coord = ivec2(gl_GlobalInvocationID.xy); - - float r, muS; - getIrradianceRMuS(r, muS); - vec3 s = vec3(max(sqrt(1.0 - muS * muS), 0.0), 0.0, muS); - - vec3 result = vec3(0.0); - // integral over 2.PI around x with two nested loops over w directions (theta,phi) -- Eq (15) - for (int iphi = 0; iphi < 2 * IRRADIANCE_INTEGRAL_SAMPLES; ++iphi) { - float phi = (float(iphi) + 0.5) * dphi; - for (int itheta = 0; itheta < IRRADIANCE_INTEGRAL_SAMPLES / 2; ++itheta) { - float theta = (float(itheta) + 0.5) * dtheta; - float dw = dtheta * dphi * sin(theta); - vec3 w = vec3(cos(phi) * sin(theta), sin(phi) * sin(theta), cos(theta)); - float nu = dot(s, w); - if (first) { - // first iteration is special because Rayleigh and Mie were stored separately, - // without the phase functions factors; they must be reintroduced here - float pr1 = phaseFunctionR(nu); - float pm1 = phaseFunctionM(nu); - vec3 ray1 = texture4D(deltaSRSampler, r, w.z, muS, nu).rgb; - vec3 mie1 = texture4D(deltaSMSampler, r, w.z, muS, nu).rgb; - result += (ray1 * pr1 + mie1 * pm1) * w.z * dw; - } else { - result += texture4D(deltaSRSampler, r, w.z, muS, nu).rgb * w.z * dw; - } - } - } - imageStore(dest, coord, vec4(result, SCAT_DEBUG_ALPHA)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "scattering_common.glsl" + +layout(local_size_x = 16, local_size_y = 16) in; + +uniform sampler3D deltaSRSampler; +uniform sampler3D deltaSMSampler; +uniform bool first; + +uniform writeonly image2D RESTRICT dest; + +const float dphi = M_PI / float(IRRADIANCE_INTEGRAL_SAMPLES); +const float dtheta = M_PI / float(IRRADIANCE_INTEGRAL_SAMPLES); + +void main() { + ivec2 coord = ivec2(gl_GlobalInvocationID.xy); + + float r, muS; + getIrradianceRMuS(r, muS); + vec3 s = vec3(max(sqrt(1.0 - muS * muS), 0.0), 0.0, muS); + + vec3 result = vec3(0.0); + // integral over 2.PI around x with two nested loops over w directions (theta,phi) -- Eq (15) + for (int iphi = 0; iphi < 2 * IRRADIANCE_INTEGRAL_SAMPLES; ++iphi) { + float phi = (float(iphi) + 0.5) * dphi; + for (int itheta = 0; itheta < IRRADIANCE_INTEGRAL_SAMPLES / 2; ++itheta) { + float theta = (float(itheta) + 0.5) * dtheta; + float dw = dtheta * dphi * sin(theta); + vec3 w = vec3(cos(phi) * sin(theta), sin(phi) * sin(theta), cos(theta)); + float nu = dot(s, w); + if (first) { + // first iteration is special because Rayleigh and Mie were stored separately, + // without the phase functions factors; they must be reintroduced here + float pr1 = phaseFunctionR(nu); + float pm1 = phaseFunctionM(nu); + vec3 ray1 = texture4D(deltaSRSampler, r, w.z, muS, nu).rgb; + vec3 mie1 = texture4D(deltaSMSampler, r, w.z, muS, nu).rgb; + result += (ray1 * pr1 + mie1 * pm1) * w.z * dw; + } else { + result += texture4D(deltaSRSampler, r, w.z, muS, nu).rgb * w.z * dw; + } + } + } + imageStore(dest, coord, vec4(result, SCAT_DEBUG_ALPHA)); +} diff --git a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/scattering_common.glsl b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/scattering_common.glsl index d68055ab..41718771 100644 --- a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/scattering_common.glsl +++ b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/scattering_common.glsl @@ -1,343 +1,343 @@ -#pragma once - -// This file includes defines and functions used for the scattering - - -// #define SCATTERING_USE_32_BIT 1 - -#pragma include "render_pipeline_base.inc.glsl" - -#extension GL_ARB_shading_language_420pack : enable - - -// These can be overridden depending on if the shader is a compute shader -// or a regular fragment shader -#ifndef NO_COMPUTE_SHADER -#define PIXEL_X gl_GlobalInvocationID.x -#define PIXEL_Y gl_GlobalInvocationID.y -#else -#define PIXEL_X gl_FragCoord.x -#define PIXEL_Y gl_FragCoord.x -#endif - - -// This define can be set to 1.0 when writing out the scattering textures, so -// the alpha channel is not 0.0 and the texture cannot get displayed -#define SCAT_DEBUG_ALPHA 0.0 - - -// Actual include: - -/** - * Precomputed Atmospheric Scattering - * Copyright (c) 2008 INRIA - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of the copyright holders nor the names of its - * contributors may be used to endorse or promote products derived from - * this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF - * THE POSSIBILITY OF SUCH DAMAGE. - - * Author: Eric Bruneton - */ - -// Constants -const float Rg = 6360.0; -const float Rt = 6420.0; -const float RL = 6421.0; -const int TRANSMITTANCE_W = 256 * 4; -const int TRANSMITTANCE_H = 64 * 4; -const int SKY_W = 64 * 4; -const int SKY_H = 16 * 4; - -const int RES_R = 32; -const int RES_MU = 128; -const int RES_MU_S = 32; -const int RES_NU = 8; - - - -// ---------------------------------------------------------------------------- -// PHYSICAL MODEL PARAMETERS -// ---------------------------------------------------------------------------- - -const float AVERAGE_GROUND_REFLECTANCE = GET_SETTING(scattering, ground_reflectance); - -// Rayleigh -const float HR = GET_SETTING(scattering, rayleigh_height_scale); -const vec3 betaR = vec3(5.8e-3, 1.35e-2, 3.31e-2); - -// Mie - -const float HM = GET_SETTING(scattering, mie_height_scale); -const vec3 betaMSca = vec3(GET_SETTING(scattering, beta_mie_scattering) * 1e-3); -const float mieG = GET_SETTING(scattering, mie_phase_factor); -// CLEAR SKY -// const float HM = 1.2; -// const vec3 betaMSca = vec3(20e-3); -// const float mieG = 0.76; -// PARTLY CLOUDY -/*const float HM = 3.0; -const vec3 betaMSca = vec3(3e-3); -const float mieG = 0.65;*/ - -// Beta Mie Extinction -const vec3 betaMEx = betaMSca / 0.9; - -// ---------------------------------------------------------------------------- -// NUMERICAL INTEGRATION PARAMETERS -// ---------------------------------------------------------------------------- - -const int TRANSMITTANCE_INTEGRAL_SAMPLES = 500; -const int INSCATTER_INTEGRAL_SAMPLES = 50; -const int IRRADIANCE_INTEGRAL_SAMPLES = 32; - -// Needs to be a low value, leads to driver crashes otherwise -const int INSCATTER_SPHERICAL_INTEGRAL_SAMPLES = 8; - -// ---------------------------------------------------------------------------- -// PARAMETERIZATION OPTIONS -// ---------------------------------------------------------------------------- - -#define TRANSMITTANCE_NON_LINEAR -#define INSCATTER_NON_LINEAR - -// ---------------------------------------------------------------------------- -// PARAMETERIZATION FUNCTIONS -// ---------------------------------------------------------------------------- - -uniform sampler2D transmittanceSampler; - - -void get_r_dhdh(int layer, out float r, out vec4 dhdh) { - r = float(layer) / float(RES_R - 1.0); - r = r * r; - r = sqrt(Rg * Rg + r * (Rt * Rt - Rg * Rg)) + - (layer == 0 ? 0.01 : (layer == RES_R - 1 ? -0.001 : 0.0)); - float dmin = Rt - r; - float dmax = sqrt(r * r - Rg * Rg) + sqrt(Rt * Rt - Rg * Rg); - float dminp = r - Rg; - float dmaxp = sqrt(r * r - Rg * Rg); - dhdh = vec4(dmin, dmax, dminp, dmaxp); -} - - - - -vec2 getTransmittanceUV(float r, float mu) { - float uR, uMu; -#ifdef TRANSMITTANCE_NON_LINEAR - uR = sqrt((r - Rg) / (Rt - Rg)); - uMu = atan((mu + 0.15) / (1.0 + 0.15) * tan(1.5)) / 1.5; -#else - uR = (r - Rg) / (Rt - Rg); - uMu = (mu + 0.15) / (1.0 + 0.15); -#endif - return vec2(uMu, uR); -} - -void getTransmittanceRMu(out float r, out float muS) { - r = PIXEL_Y / float(TRANSMITTANCE_H); - muS = PIXEL_X / float(TRANSMITTANCE_W); -#ifdef TRANSMITTANCE_NON_LINEAR - r = Rg + (r * r) * (Rt - Rg); - muS = -0.15 + tan(1.5 * muS) / tan(1.5) * (1.0 + 0.15); -#else - r = Rg + r * (Rt - Rg); - muS = -0.15 + muS * (1.0 + 0.15); -#endif -} - -vec2 getIrradianceUV(float r, float muS) { - float uR = (r - Rg) / (Rt - Rg); - float uMuS = (muS + 0.2) / (1.0 + 0.2); - return vec2(uMuS, uR); -} - -void getIrradianceRMuS(out float r, out float muS) { - // POSSIBLE PROBLEMS int(coord) does not match gl_FragCoord ? - r = Rg + int(PIXEL_Y) / (float(SKY_H) - 1.0) * (Rt - Rg); - muS = -0.2 + int(PIXEL_X) / (float(SKY_W) - 1.0) * (1.0 + 0.2); -} - -vec4 texture4D(sampler3D table, float r, float mu, float muS, float nu) -{ - float Rg_sq = Rg * Rg; - float H = sqrt(Rt * Rt - Rg_sq); - float rho = sqrt(r * r - Rg_sq); -#ifdef INSCATTER_NON_LINEAR - float rmu = r * mu; - float delta = rmu * rmu - r * r + Rg_sq; - vec4 cst = rmu < 0.0 && delta > 0.0 ? - vec4(1.0, 0.0, 0.0, 0.5 - 0.5 / float(RES_MU)) : - vec4(-1.0, H * H, H, 0.5 + 0.5 / float(RES_MU)); - float uR = 0.5 / float(RES_R) + rho / H * (1.0 - 1.0 / float(RES_R)); - float uMu = cst.w + (rmu * cst.x + sqrt(delta + cst.y)) / (rho + cst.z) * - (0.5 - 1.0 / float(RES_MU)); - // paper formula - // float uMuS = 0.5 / float(RES_MU_S) + max((1.0 - exp(-3.0 * muS - 0.6)) / (1.0 - exp(-3.6)), 0.0) * (1.0 - 1.0 / float(RES_MU_S)); - // better formula - float uMuS = 0.5 / float(RES_MU_S) + (atan(max(muS, -0.1975) * tan(1.26 * 1.1)) / 1.1 + - (1.0 - 0.26)) * 0.5 * (1.0 - 1.0 / float(RES_MU_S)); -#else - float uR = 0.5 / float(RES_R) + rho / H * (1.0 - 1.0 / float(RES_R)); - float uMu = 0.5 / float(RES_MU) + (mu + 1.0) / 2.0 * (1.0 - 1.0 / float(RES_MU)); - float uMuS = 0.5 / float(RES_MU_S) + max(muS + 0.2, 0.0) / 1.2 * (1.0 - 1.0 / float(RES_MU_S)); -#endif - float lerp = (nu + 1.0) / 2.0 * (float(RES_NU) - 1.0); - float uNu = floor(lerp); - lerp = lerp - uNu; - return textureLod(table, vec3((uNu + uMuS) / float(RES_NU), uMu, uR), 0) * (1.0 - lerp) + - textureLod(table, vec3((uNu + uMuS + 1.0) / float(RES_NU), uMu, uR), 0) * lerp; -} - -void getMuMuSNu(float r, vec4 dhdH, out float mu, out float muS, out float nu) { - float x = int(PIXEL_X); - float y = int(PIXEL_Y); -#ifdef INSCATTER_NON_LINEAR - if (y < float(RES_MU) / 2.0) { - float d = 1.0 - y / (float(RES_MU) / 2.0 - 1.0); - d = min(max(dhdH.z, d * dhdH.w), dhdH.w * 0.999); - mu = (Rg * Rg - r * r - d * d) / (2.0 * r * d); - mu = min(mu, -sqrt(1.0 - (Rg / r) * (Rg / r)) - 0.001); - } else { - float d = (y - float(RES_MU) / 2.0) / (float(RES_MU) / 2.0 - 1.0); - d = min(max(dhdH.x, d * dhdH.y), dhdH.y * 0.999); - mu = (Rt * Rt - r * r - d * d) / (2.0 * r * d); - } - muS = mod(x, float(RES_MU_S)) / (float(RES_MU_S) - 1.0); - // paper formula - //muS = -(0.6 + log(1.0 - muS * (1.0 - exp(-3.6)))) / 3.0; - // better formula - muS = tan((2.0 * muS - 1.0 + 0.26) * 1.1) / tan(1.26 * 1.1); - nu = -1.0 + floor(x / float(RES_MU_S)) / (float(RES_NU) - 1.0) * 2.0; -#else - mu = -1.0 + 2.0 * y / (float(RES_MU) - 1.0); - muS = mod(x, float(RES_MU_S)) / (float(RES_MU_S) - 1.0); - muS = -0.2 + muS * 1.2; - nu = -1.0 + floor(x / float(RES_MU_S)) / (float(RES_NU) - 1.0) * 2.0; -#endif -} - -// ---------------------------------------------------------------------------- -// UTILITY FUNCTIONS -// ---------------------------------------------------------------------------- - -// nearest intersection of ray r,mu with ground or top atmosphere boundary -// mu=cos(ray zenith angle at ray origin) -float limit(float r, float mu) { - float dout = -r * mu + sqrt(r * r * (mu * mu - 1.0) + RL * RL); - float delta2 = r * r * (mu * mu - 1.0) + Rg * Rg; - if (delta2 >= 0.0) { - float din = -r * mu - sqrt(delta2); - if (din >= 0.0) { - dout = min(dout, din); - } - } - return dout; -} - -// transmittance(=transparency) of atmosphere for infinite ray (r,mu) -// (mu=cos(view zenith angle)), intersections with ground ignored -vec3 transmittance(float r, float mu) { - vec2 uv = getTransmittanceUV(r, mu); - return textureLod(transmittanceSampler, uv, 0).rgb; -} - -// transmittance(=transparency) of atmosphere for infinite ray (r,mu) -// (mu=cos(view zenith angle)), or zero if ray intersects ground -vec3 transmittanceWithShadow(float r, float mu) { - return mu < -sqrt(1.0 - (Rg / r) * (Rg / r)) ? vec3(0.0) : transmittance(r, mu); -} - -// transmittance(=transparency) of atmosphere between x and x0 -// assume segment x,x0 not intersecting ground -// r=||x||, mu=cos(zenith angle of [x,x0) ray at x), v=unit direction vector of [x,x0) ray -vec3 transmittance(float r, float mu, vec3 v, vec3 x0) { - vec3 result; - float r1 = length(x0); - float mu1 = dot(x0, v) / r; - if (mu > 0.0) { - result = min(transmittance(r, mu) / transmittance(r1, mu1), 1.0); - } else { - result = min(transmittance(r1, -mu1) / transmittance(r, -mu), 1.0); - } - return result; -} - -// optical depth for ray (r,mu) of length d, using analytic formula -// (mu=cos(view zenith angle)), intersections with ground ignored -// H=height scale of exponential density function -float opticalDepth(float H, float r, float mu, float d) { - float a = sqrt((0.5 / H) * r); - vec2 a01 = a * vec2(mu, mu + d / r); - vec2 a01s = sign(a01); - vec2 a01sq = a01 * a01; - float x = a01s.y > a01s.x ? exp(a01sq.x) : 0.0; - vec2 y = a01s / (2.3193 * abs(a01) + sqrt(1.52 * a01sq + 4.0)) * - vec2(1.0, exp(-d / H * (d / (2.0 * r) + mu))); - return sqrt((6.2831 * H) * r) * exp((Rg - r) / H) * (x + dot(y, vec2(1.0, -1.0))); -} - -// transmittance(=transparency) of atmosphere for ray (r,mu) of length d -// (mu=cos(view zenith angle)), intersections with ground ignored -// uses analytic formula instead of transmittance texture -vec3 analyticTransmittance(float r, float mu, float d) { - return exp(- betaR * opticalDepth(HR, r, mu, d) - betaMEx * opticalDepth(HM, r, mu, d)); -} - -// transmittance(=transparency) of atmosphere between x and x0 -// assume segment x,x0 not intersecting ground -// d = distance between x and x0, mu=cos(zenith angle of [x,x0) ray at x) -vec3 transmittance(float r, float mu, float d) { - vec3 result; - float r1 = sqrt(r * r + d * d + 2.0 * r * mu * d); - float mu1 = (r * mu + d) / r1; - if (mu > 0.0) { - result = min(transmittance(r, mu) / transmittance(r1, mu1), 1.0); - } else { - result = min(transmittance(r1, -mu1) / transmittance(r, -mu), 1.0); - } - return result; -} - -vec3 irradiance(sampler2D sampler, float r, float muS) { - vec2 uv = getIrradianceUV(r, muS); - return textureLod(sampler, uv, 0).rgb; -} - -// Rayleigh phase function -float phaseFunctionR(float mu) { - return (3.0 / (16.0 * M_PI)) * (1.0 + mu * mu); -} - -// Mie phase function -float phaseFunctionM(float mu) { - return 1.5 * 1.0 / (4.0 * M_PI) * (1.0 - mieG * mieG) * - pow(1.0 + (mieG * mieG) - 2.0 * mieG * mu, -3.0 / 2.0) * - (1.0 + mu * mu) / (2.0 + mieG * mieG); -} - -// approximated single Mie scattering (cf. approximate Cm in paragraph "Angular precision") -vec3 getMie(vec4 rayMie) { // rayMie.rgb=C*, rayMie.w=Cm,r - return rayMie.rgb * rayMie.w / max(rayMie.r, 1e-4) * (betaR.r / betaR); -} +#pragma once + +// This file includes defines and functions used for the scattering + + +// #define SCATTERING_USE_32_BIT 1 + +#pragma include "render_pipeline_base.inc.glsl" + +#extension GL_ARB_shading_language_420pack : enable + + +// These can be overridden depending on if the shader is a compute shader +// or a regular fragment shader +#ifndef NO_COMPUTE_SHADER +#define PIXEL_X gl_GlobalInvocationID.x +#define PIXEL_Y gl_GlobalInvocationID.y +#else +#define PIXEL_X gl_FragCoord.x +#define PIXEL_Y gl_FragCoord.x +#endif + + +// This define can be set to 1.0 when writing out the scattering textures, so +// the alpha channel is not 0.0 and the texture cannot get displayed +#define SCAT_DEBUG_ALPHA 0.0 + + +// Actual include: + +/** + * Precomputed Atmospheric Scattering + * Copyright (c) 2008 INRIA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holders nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + * THE POSSIBILITY OF SUCH DAMAGE. + + * Author: Eric Bruneton + */ + +// Constants +const float Rg = 6360.0; +const float Rt = 6420.0; +const float RL = 6421.0; +const int TRANSMITTANCE_W = 256 * 4; +const int TRANSMITTANCE_H = 64 * 4; +const int SKY_W = 64 * 4; +const int SKY_H = 16 * 4; + +const int RES_R = 32; +const int RES_MU = 128; +const int RES_MU_S = 32; +const int RES_NU = 8; + + + +// ---------------------------------------------------------------------------- +// PHYSICAL MODEL PARAMETERS +// ---------------------------------------------------------------------------- + +const float AVERAGE_GROUND_REFLECTANCE = GET_SETTING(scattering, ground_reflectance); + +// Rayleigh +const float HR = GET_SETTING(scattering, rayleigh_height_scale); +const vec3 betaR = vec3(5.8e-3, 1.35e-2, 3.31e-2); + +// Mie + +const float HM = GET_SETTING(scattering, mie_height_scale); +const vec3 betaMSca = vec3(GET_SETTING(scattering, beta_mie_scattering) * 1e-3); +const float mieG = GET_SETTING(scattering, mie_phase_factor); +// CLEAR SKY +// const float HM = 1.2; +// const vec3 betaMSca = vec3(20e-3); +// const float mieG = 0.76; +// PARTLY CLOUDY +/*const float HM = 3.0; +const vec3 betaMSca = vec3(3e-3); +const float mieG = 0.65;*/ + +// Beta Mie Extinction +const vec3 betaMEx = betaMSca / 0.9; + +// ---------------------------------------------------------------------------- +// NUMERICAL INTEGRATION PARAMETERS +// ---------------------------------------------------------------------------- + +const int TRANSMITTANCE_INTEGRAL_SAMPLES = 500; +const int INSCATTER_INTEGRAL_SAMPLES = 50; +const int IRRADIANCE_INTEGRAL_SAMPLES = 32; + +// Needs to be a low value, leads to driver crashes otherwise +const int INSCATTER_SPHERICAL_INTEGRAL_SAMPLES = 8; + +// ---------------------------------------------------------------------------- +// PARAMETERIZATION OPTIONS +// ---------------------------------------------------------------------------- + +#define TRANSMITTANCE_NON_LINEAR +#define INSCATTER_NON_LINEAR + +// ---------------------------------------------------------------------------- +// PARAMETERIZATION FUNCTIONS +// ---------------------------------------------------------------------------- + +uniform sampler2D transmittanceSampler; + + +void get_r_dhdh(int layer, out float r, out vec4 dhdh) { + r = float(layer) / float(RES_R - 1.0); + r = r * r; + r = sqrt(Rg * Rg + r * (Rt * Rt - Rg * Rg)) + + (layer == 0 ? 0.01 : (layer == RES_R - 1 ? -0.001 : 0.0)); + float dmin = Rt - r; + float dmax = sqrt(r * r - Rg * Rg) + sqrt(Rt * Rt - Rg * Rg); + float dminp = r - Rg; + float dmaxp = sqrt(r * r - Rg * Rg); + dhdh = vec4(dmin, dmax, dminp, dmaxp); +} + + + + +vec2 getTransmittanceUV(float r, float mu) { + float uR, uMu; +#ifdef TRANSMITTANCE_NON_LINEAR + uR = sqrt((r - Rg) / (Rt - Rg)); + uMu = atan((mu + 0.15) / (1.0 + 0.15) * tan(1.5)) / 1.5; +#else + uR = (r - Rg) / (Rt - Rg); + uMu = (mu + 0.15) / (1.0 + 0.15); +#endif + return vec2(uMu, uR); +} + +void getTransmittanceRMu(out float r, out float muS) { + r = PIXEL_Y / float(TRANSMITTANCE_H); + muS = PIXEL_X / float(TRANSMITTANCE_W); +#ifdef TRANSMITTANCE_NON_LINEAR + r = Rg + (r * r) * (Rt - Rg); + muS = -0.15 + tan(1.5 * muS) / tan(1.5) * (1.0 + 0.15); +#else + r = Rg + r * (Rt - Rg); + muS = -0.15 + muS * (1.0 + 0.15); +#endif +} + +vec2 getIrradianceUV(float r, float muS) { + float uR = (r - Rg) / (Rt - Rg); + float uMuS = (muS + 0.2) / (1.0 + 0.2); + return vec2(uMuS, uR); +} + +void getIrradianceRMuS(out float r, out float muS) { + // POSSIBLE PROBLEMS int(coord) does not match gl_FragCoord ? + r = Rg + int(PIXEL_Y) / (float(SKY_H) - 1.0) * (Rt - Rg); + muS = -0.2 + int(PIXEL_X) / (float(SKY_W) - 1.0) * (1.0 + 0.2); +} + +vec4 texture4D(sampler3D table, float r, float mu, float muS, float nu) +{ + float Rg_sq = Rg * Rg; + float H = sqrt(Rt * Rt - Rg_sq); + float rho = sqrt(r * r - Rg_sq); +#ifdef INSCATTER_NON_LINEAR + float rmu = r * mu; + float delta = rmu * rmu - r * r + Rg_sq; + vec4 cst = rmu < 0.0 && delta > 0.0 ? + vec4(1.0, 0.0, 0.0, 0.5 - 0.5 / float(RES_MU)) : + vec4(-1.0, H * H, H, 0.5 + 0.5 / float(RES_MU)); + float uR = 0.5 / float(RES_R) + rho / H * (1.0 - 1.0 / float(RES_R)); + float uMu = cst.w + (rmu * cst.x + sqrt(delta + cst.y)) / (rho + cst.z) * + (0.5 - 1.0 / float(RES_MU)); + // paper formula + // float uMuS = 0.5 / float(RES_MU_S) + max((1.0 - exp(-3.0 * muS - 0.6)) / (1.0 - exp(-3.6)), 0.0) * (1.0 - 1.0 / float(RES_MU_S)); + // better formula + float uMuS = 0.5 / float(RES_MU_S) + (atan(max(muS, -0.1975) * tan(1.26 * 1.1)) / 1.1 + + (1.0 - 0.26)) * 0.5 * (1.0 - 1.0 / float(RES_MU_S)); +#else + float uR = 0.5 / float(RES_R) + rho / H * (1.0 - 1.0 / float(RES_R)); + float uMu = 0.5 / float(RES_MU) + (mu + 1.0) / 2.0 * (1.0 - 1.0 / float(RES_MU)); + float uMuS = 0.5 / float(RES_MU_S) + max(muS + 0.2, 0.0) / 1.2 * (1.0 - 1.0 / float(RES_MU_S)); +#endif + float lerp = (nu + 1.0) / 2.0 * (float(RES_NU) - 1.0); + float uNu = floor(lerp); + lerp = lerp - uNu; + return textureLod(table, vec3((uNu + uMuS) / float(RES_NU), uMu, uR), 0) * (1.0 - lerp) + + textureLod(table, vec3((uNu + uMuS + 1.0) / float(RES_NU), uMu, uR), 0) * lerp; +} + +void getMuMuSNu(float r, vec4 dhdH, out float mu, out float muS, out float nu) { + float x = int(PIXEL_X); + float y = int(PIXEL_Y); +#ifdef INSCATTER_NON_LINEAR + if (y < float(RES_MU) / 2.0) { + float d = 1.0 - y / (float(RES_MU) / 2.0 - 1.0); + d = min(max(dhdH.z, d * dhdH.w), dhdH.w * 0.999); + mu = (Rg * Rg - r * r - d * d) / (2.0 * r * d); + mu = min(mu, -sqrt(1.0 - (Rg / r) * (Rg / r)) - 0.001); + } else { + float d = (y - float(RES_MU) / 2.0) / (float(RES_MU) / 2.0 - 1.0); + d = min(max(dhdH.x, d * dhdH.y), dhdH.y * 0.999); + mu = (Rt * Rt - r * r - d * d) / (2.0 * r * d); + } + muS = mod(x, float(RES_MU_S)) / (float(RES_MU_S) - 1.0); + // paper formula + //muS = -(0.6 + log(1.0 - muS * (1.0 - exp(-3.6)))) / 3.0; + // better formula + muS = tan((2.0 * muS - 1.0 + 0.26) * 1.1) / tan(1.26 * 1.1); + nu = -1.0 + floor(x / float(RES_MU_S)) / (float(RES_NU) - 1.0) * 2.0; +#else + mu = -1.0 + 2.0 * y / (float(RES_MU) - 1.0); + muS = mod(x, float(RES_MU_S)) / (float(RES_MU_S) - 1.0); + muS = -0.2 + muS * 1.2; + nu = -1.0 + floor(x / float(RES_MU_S)) / (float(RES_NU) - 1.0) * 2.0; +#endif +} + +// ---------------------------------------------------------------------------- +// UTILITY FUNCTIONS +// ---------------------------------------------------------------------------- + +// nearest intersection of ray r,mu with ground or top atmosphere boundary +// mu=cos(ray zenith angle at ray origin) +float limit(float r, float mu) { + float dout = -r * mu + sqrt(r * r * (mu * mu - 1.0) + RL * RL); + float delta2 = r * r * (mu * mu - 1.0) + Rg * Rg; + if (delta2 >= 0.0) { + float din = -r * mu - sqrt(delta2); + if (din >= 0.0) { + dout = min(dout, din); + } + } + return dout; +} + +// transmittance(=transparency) of atmosphere for infinite ray (r,mu) +// (mu=cos(view zenith angle)), intersections with ground ignored +vec3 transmittance(float r, float mu) { + vec2 uv = getTransmittanceUV(r, mu); + return textureLod(transmittanceSampler, uv, 0).rgb; +} + +// transmittance(=transparency) of atmosphere for infinite ray (r,mu) +// (mu=cos(view zenith angle)), or zero if ray intersects ground +vec3 transmittanceWithShadow(float r, float mu) { + return mu < -sqrt(1.0 - (Rg / r) * (Rg / r)) ? vec3(0.0) : transmittance(r, mu); +} + +// transmittance(=transparency) of atmosphere between x and x0 +// assume segment x,x0 not intersecting ground +// r=||x||, mu=cos(zenith angle of [x,x0) ray at x), v=unit direction vector of [x,x0) ray +vec3 transmittance(float r, float mu, vec3 v, vec3 x0) { + vec3 result; + float r1 = length(x0); + float mu1 = dot(x0, v) / r; + if (mu > 0.0) { + result = min(transmittance(r, mu) / transmittance(r1, mu1), 1.0); + } else { + result = min(transmittance(r1, -mu1) / transmittance(r, -mu), 1.0); + } + return result; +} + +// optical depth for ray (r,mu) of length d, using analytic formula +// (mu=cos(view zenith angle)), intersections with ground ignored +// H=height scale of exponential density function +float opticalDepth(float H, float r, float mu, float d) { + float a = sqrt((0.5 / H) * r); + vec2 a01 = a * vec2(mu, mu + d / r); + vec2 a01s = sign(a01); + vec2 a01sq = a01 * a01; + float x = a01s.y > a01s.x ? exp(a01sq.x) : 0.0; + vec2 y = a01s / (2.3193 * abs(a01) + sqrt(1.52 * a01sq + 4.0)) * + vec2(1.0, exp(-d / H * (d / (2.0 * r) + mu))); + return sqrt((6.2831 * H) * r) * exp((Rg - r) / H) * (x + dot(y, vec2(1.0, -1.0))); +} + +// transmittance(=transparency) of atmosphere for ray (r,mu) of length d +// (mu=cos(view zenith angle)), intersections with ground ignored +// uses analytic formula instead of transmittance texture +vec3 analyticTransmittance(float r, float mu, float d) { + return exp(- betaR * opticalDepth(HR, r, mu, d) - betaMEx * opticalDepth(HM, r, mu, d)); +} + +// transmittance(=transparency) of atmosphere between x and x0 +// assume segment x,x0 not intersecting ground +// d = distance between x and x0, mu=cos(zenith angle of [x,x0) ray at x) +vec3 transmittance(float r, float mu, float d) { + vec3 result; + float r1 = sqrt(r * r + d * d + 2.0 * r * mu * d); + float mu1 = (r * mu + d) / r1; + if (mu > 0.0) { + result = min(transmittance(r, mu) / transmittance(r1, mu1), 1.0); + } else { + result = min(transmittance(r1, -mu1) / transmittance(r, -mu), 1.0); + } + return result; +} + +vec3 irradiance(sampler2D sampler, float r, float muS) { + vec2 uv = getIrradianceUV(r, muS); + return textureLod(sampler, uv, 0).rgb; +} + +// Rayleigh phase function +float phaseFunctionR(float mu) { + return (3.0 / (16.0 * M_PI)) * (1.0 + mu * mu); +} + +// Mie phase function +float phaseFunctionM(float mu) { + return 1.5 * 1.0 / (4.0 * M_PI) * (1.0 - mieG * mieG) * + pow(1.0 + (mieG * mieG) - 2.0 * mieG * mu, -3.0 / 2.0) * + (1.0 + mu * mu) / (2.0 + mieG * mieG); +} + +// approximated single Mie scattering (cf. approximate Cm in paragraph "Angular precision") +vec3 getMie(vec4 rayMie) { // rayMie.rgb=C*, rayMie.w=Cm,r + return rayMie.rgb * rayMie.w / max(rayMie.r, 1e-4) * (betaR.r / betaR); +} diff --git a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/transmittance.compute.glsl b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/transmittance.compute.glsl index c5cf63d5..a56ca3f6 100644 --- a/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/transmittance.compute.glsl +++ b/RenderPipelineFile/rpplugins/scattering/shader/eric_bruneton/transmittance.compute.glsl @@ -1,58 +1,58 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "scattering_common.glsl" - - -layout(local_size_x = 16, local_size_y = 16) in; - -uniform writeonly image2D RESTRICT dest; - -float opticalDepth(float H, float r, float mu) { - float result = 0.0; - float dx = limit(r, mu) / float(TRANSMITTANCE_INTEGRAL_SAMPLES); - float xi = 0.0; - float yi = exp(-(r - Rg) / H); - for (int i = 1; i <= TRANSMITTANCE_INTEGRAL_SAMPLES; ++i) { - float xj = float(i) * dx; - float yj = exp(-(sqrt(r * r + xj * xj + 2.0 * xj * r * mu) - Rg) / H); - result += (yi + yj) / 2.0 * dx; - xi = xj; - yi = yj; - } - return mu < -sqrt(1.0 - (Rg / r) * (Rg / r)) ? 1e9 : result; -} - - -void main() { - ivec2 coord = ivec2(gl_GlobalInvocationID.xy); - float r, muS; - getTransmittanceRMu(r, muS); - vec3 depth = betaR * opticalDepth(HR, r, muS) + betaMEx * opticalDepth(HM, r, muS); - imageStore(dest, coord, vec4(exp(-depth), SCAT_DEBUG_ALPHA)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "scattering_common.glsl" + + +layout(local_size_x = 16, local_size_y = 16) in; + +uniform writeonly image2D RESTRICT dest; + +float opticalDepth(float H, float r, float mu) { + float result = 0.0; + float dx = limit(r, mu) / float(TRANSMITTANCE_INTEGRAL_SAMPLES); + float xi = 0.0; + float yi = exp(-(r - Rg) / H); + for (int i = 1; i <= TRANSMITTANCE_INTEGRAL_SAMPLES; ++i) { + float xj = float(i) * dx; + float yj = exp(-(sqrt(r * r + xj * xj + 2.0 * xj * r * mu) - Rg) / H); + result += (yi + yj) / 2.0 * dx; + xi = xj; + yi = yj; + } + return mu < -sqrt(1.0 - (Rg / r) * (Rg / r)) ? 1e9 : result; +} + + +void main() { + ivec2 coord = ivec2(gl_GlobalInvocationID.xy); + float r, muS; + getTransmittanceRMu(r, muS); + vec3 depth = betaR * opticalDepth(HR, r, muS) + betaMEx * opticalDepth(HM, r, muS); + imageStore(dest, coord, vec4(exp(-depth), SCAT_DEBUG_ALPHA)); +} diff --git a/RenderPipelineFile/rpplugins/scattering/shader/hosek_wilkie/compute_scattering.inc.glsl b/RenderPipelineFile/rpplugins/scattering/shader/hosek_wilkie/compute_scattering.inc.glsl index 08591a40..6d563fe8 100644 --- a/RenderPipelineFile/rpplugins/scattering/shader/hosek_wilkie/compute_scattering.inc.glsl +++ b/RenderPipelineFile/rpplugins/scattering/shader/hosek_wilkie/compute_scattering.inc.glsl @@ -1,125 +1,125 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - - -vec3 sun_vector = get_sun_vector(); - -uniform sampler3D ScatteringLUT; - - -// Fetches the scattering value from the LUT -vec3 get_scattering(vec3 surface_pos) { - surface_pos = normalize(surface_pos); - - float elevation, theta, radius; - vector_to_spherical(surface_pos, theta, elevation, radius); - - float sun_elevation = TimeOfDay.scattering.sun_altitude / 180.0 * M_PI; - float sun_azimuth = TimeOfDay.scattering.sun_azimuth / 180.0 * M_PI; - - float night_factor = saturate(sun_elevation * 30.0); - - float max_angle_factor = 40.0 / 180.0 * M_PI; - float elevation_factor = sun_elevation / max_angle_factor; - - float slice_index = 0.5 / 100.0 + elevation_factor; - - float factor_x = theta; - float factor_y = elevation; - - float neg_solar_elevation = HALF_PI - sun_elevation; - - float cos_gamma = cos(elevation) * cos(neg_solar_elevation) - + sin(elevation) * sin(neg_solar_elevation) - * cos(theta - sun_azimuth + M_PI); - - float gamma = acos(cos_gamma); - - elevation *= 0.95; - - vec2 lut_coord = vec2(gamma / TWO_PI, 1 - (elevation / HALF_PI)); - vec3 value = textureLod(ScatteringLUT, vec3(lut_coord, slice_index), 0).xyz ; - value *= 25.0; - value *= night_factor; - return vec3(value); - -} - -// Fetches the scattering value at a given surface position -vec3 get_scattering_at_surface(vec3 surface_pos) { - vec3 vec_to_cam = vec3(surface_pos - MainSceneData.camera_pos); - surface_pos.xy = vec2(vec_to_cam.xy * 0.6); - return get_scattering(surface_pos); -} - -vec3 DoScattering(vec3 surface_pos, vec3 view_dir, out float fog_factor) -{ - - // Move surface pos above ocean level - if (surface_pos.z < -0.01) { - vec3 v2s = surface_pos - MainSceneData.camera_pos; - float z_factor = abs(MainSceneData.camera_pos.z) / abs(v2s.z); - surface_pos = MainSceneData.camera_pos + v2s * z_factor; - view_dir = normalize(surface_pos - MainSceneData.camera_pos); - } - - float path_length = distance(surface_pos, MainSceneData.camera_pos); - vec3 inscatter = get_scattering(surface_pos); - fog_factor = 1.0; - - - // Check if the ray is finite - if (path_length < 20000.0) { - - // Integrate scattering - const int num_steps = 6; - float curr_h = MainSceneData.camera_pos.z; - - curr_h *= 1.0 - saturate(path_length / 30000.0); - - float h_step = (surface_pos.z - MainSceneData.camera_pos.z) / num_steps; - vec3 accum = vec3(0); - for (int i = 0; i < num_steps; ++i) { - curr_h += h_step; - accum += get_scattering_at_surface(vec3(surface_pos.xy, curr_h)); - } - - accum /= float(num_steps); - - // Exponential fog - float fog_ramp = TimeOfDay.scattering.fog_ramp_size; - fog_factor = saturate(1.0 - exp(-path_length / (0.6 * fog_ramp))); - - // Exponential height fog - fog_factor *= exp(-pow(max(0, surface_pos.z), 1.2) / (5.0 * 4000.0)); - inscatter = accum * 0.8; - fog_factor = saturate(1.1 * fog_factor); - } - - return inscatter; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + + +vec3 sun_vector = get_sun_vector(); + +uniform sampler3D ScatteringLUT; + + +// Fetches the scattering value from the LUT +vec3 get_scattering(vec3 surface_pos) { + surface_pos = normalize(surface_pos); + + float elevation, theta, radius; + vector_to_spherical(surface_pos, theta, elevation, radius); + + float sun_elevation = TimeOfDay.scattering.sun_altitude / 180.0 * M_PI; + float sun_azimuth = TimeOfDay.scattering.sun_azimuth / 180.0 * M_PI; + + float night_factor = saturate(sun_elevation * 30.0); + + float max_angle_factor = 40.0 / 180.0 * M_PI; + float elevation_factor = sun_elevation / max_angle_factor; + + float slice_index = 0.5 / 100.0 + elevation_factor; + + float factor_x = theta; + float factor_y = elevation; + + float neg_solar_elevation = HALF_PI - sun_elevation; + + float cos_gamma = cos(elevation) * cos(neg_solar_elevation) + + sin(elevation) * sin(neg_solar_elevation) + * cos(theta - sun_azimuth + M_PI); + + float gamma = acos(cos_gamma); + + elevation *= 0.95; + + vec2 lut_coord = vec2(gamma / TWO_PI, 1 - (elevation / HALF_PI)); + vec3 value = textureLod(ScatteringLUT, vec3(lut_coord, slice_index), 0).xyz ; + value *= 25.0; + value *= night_factor; + return vec3(value); + +} + +// Fetches the scattering value at a given surface position +vec3 get_scattering_at_surface(vec3 surface_pos) { + vec3 vec_to_cam = vec3(surface_pos - MainSceneData.camera_pos); + surface_pos.xy = vec2(vec_to_cam.xy * 0.6); + return get_scattering(surface_pos); +} + +vec3 DoScattering(vec3 surface_pos, vec3 view_dir, out float fog_factor) +{ + + // Move surface pos above ocean level + if (surface_pos.z < -0.01) { + vec3 v2s = surface_pos - MainSceneData.camera_pos; + float z_factor = abs(MainSceneData.camera_pos.z) / abs(v2s.z); + surface_pos = MainSceneData.camera_pos + v2s * z_factor; + view_dir = normalize(surface_pos - MainSceneData.camera_pos); + } + + float path_length = distance(surface_pos, MainSceneData.camera_pos); + vec3 inscatter = get_scattering(surface_pos); + fog_factor = 1.0; + + + // Check if the ray is finite + if (path_length < 20000.0) { + + // Integrate scattering + const int num_steps = 6; + float curr_h = MainSceneData.camera_pos.z; + + curr_h *= 1.0 - saturate(path_length / 30000.0); + + float h_step = (surface_pos.z - MainSceneData.camera_pos.z) / num_steps; + vec3 accum = vec3(0); + for (int i = 0; i < num_steps; ++i) { + curr_h += h_step; + accum += get_scattering_at_surface(vec3(surface_pos.xy, curr_h)); + } + + accum /= float(num_steps); + + // Exponential fog + float fog_ramp = TimeOfDay.scattering.fog_ramp_size; + fog_factor = saturate(1.0 - exp(-path_length / (0.6 * fog_ramp))); + + // Exponential height fog + fog_factor *= exp(-pow(max(0, surface_pos.z), 1.2) / (5.0 * 4000.0)); + inscatter = accum * 0.8; + fog_factor = saturate(1.1 * fog_factor); + } + + return inscatter; +} diff --git a/RenderPipelineFile/rpplugins/scattering/shader/scattering_envmap.frag.glsl b/RenderPipelineFile/rpplugins/scattering/shader/scattering_envmap.frag.glsl index eefe1bb1..a85b825b 100644 --- a/RenderPipelineFile/rpplugins/scattering/shader/scattering_envmap.frag.glsl +++ b/RenderPipelineFile/rpplugins/scattering/shader/scattering_envmap.frag.glsl @@ -1,75 +1,75 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#define USE_TIME_OF_DAY 1 -#pragma include "render_pipeline_base.inc.glsl" - -layout(r11f_g11f_b10f) uniform imageCube RESTRICT DestCubemap; -uniform sampler2D DefaultSkydome; -uniform samplerCube DefaultEnvmap; - -#pragma include "scattering_method.inc.glsl" - -void main() { - - // Get cubemap coordinate - int texsize = imageSize(DestCubemap).x; - ivec2 coord = ivec2(gl_FragCoord.xy); - - // Get cubemap view_vector - ivec2 clamped_coord; int face; - vec3 view_vector = texcoord_to_cubemap(texsize, coord, clamped_coord, face); - - // Store horizon - float horizon = view_vector.z; - float sky_clip = 0.0; - - // Get inscattered light - vec3 inscattered_light = DoScattering(view_vector * 1e10, view_vector, sky_clip) - * TimeOfDay.scattering.sun_intensity; - - if (horizon > 0.0) { - inscattered_light *= 2.0; // XXX: This makes it look better, but has no physical background. - - // Render clouds to provide more variance for the cubemap - vec3 cloud_color = textureLod(DefaultSkydome, get_skydome_coord(view_vector), 0).xyz; - // inscattered_light = cloud_color * 15; - } else { - // Blend ambient cubemap at the bottom - vec3 sun_vector = get_sun_vector(); - vec3 color_scale = get_sun_color_scale(sun_vector) * TimeOfDay.scattering.sun_color; - inscattered_light = textureLod(DefaultEnvmap, fix_cubemap_coord(view_vector), 0).xyz - * TimeOfDay.scattering.sun_intensity / M_PI * color_scale; - - #if !HAVE_PLUGIN(color_correction) - // inscattered_light *= 0.1; - #endif - } - - imageStore(DestCubemap, ivec3(clamped_coord, face), vec4(inscattered_light, 1.0)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#define USE_TIME_OF_DAY 1 +#pragma include "render_pipeline_base.inc.glsl" + +layout(r11f_g11f_b10f) uniform imageCube RESTRICT DestCubemap; +uniform sampler2D DefaultSkydome; +uniform samplerCube DefaultEnvmap; + +#pragma include "scattering_method.inc.glsl" + +void main() { + + // Get cubemap coordinate + int texsize = imageSize(DestCubemap).x; + ivec2 coord = ivec2(gl_FragCoord.xy); + + // Get cubemap view_vector + ivec2 clamped_coord; int face; + vec3 view_vector = texcoord_to_cubemap(texsize, coord, clamped_coord, face); + + // Store horizon + float horizon = view_vector.z; + float sky_clip = 0.0; + + // Get inscattered light + vec3 inscattered_light = DoScattering(view_vector * 1e10, view_vector, sky_clip) + * TimeOfDay.scattering.sun_intensity; + + if (horizon > 0.0) { + inscattered_light *= 2.0; // XXX: This makes it look better, but has no physical background. + + // Render clouds to provide more variance for the cubemap + vec3 cloud_color = textureLod(DefaultSkydome, get_skydome_coord(view_vector), 0).xyz; + // inscattered_light = cloud_color * 15; + } else { + // Blend ambient cubemap at the bottom + vec3 sun_vector = get_sun_vector(); + vec3 color_scale = get_sun_color_scale(sun_vector) * TimeOfDay.scattering.sun_color; + inscattered_light = textureLod(DefaultEnvmap, fix_cubemap_coord(view_vector), 0).xyz + * TimeOfDay.scattering.sun_intensity / M_PI * color_scale; + + #if !HAVE_PLUGIN(color_correction) + // inscattered_light *= 0.1; + #endif + } + + imageStore(DestCubemap, ivec3(clamped_coord, face), vec4(inscattered_light, 1.0)); +} diff --git a/RenderPipelineFile/rpplugins/scattering/shader/scattering_method.inc.glsl b/RenderPipelineFile/rpplugins/scattering/shader/scattering_method.inc.glsl index 4767eb53..c88d7add 100644 --- a/RenderPipelineFile/rpplugins/scattering/shader/scattering_method.inc.glsl +++ b/RenderPipelineFile/rpplugins/scattering/shader/scattering_method.inc.glsl @@ -1,39 +1,39 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -// This file just includes the scattering kernel based on the selected scattering -// method - - -#if ENUM_V_ACTIVE(scattering, scattering_method, eric_bruneton) - #pragma include "eric_bruneton/compute_scattering.inc.glsl" -#elif ENUM_V_ACTIVE(scattering, scattering_method, hosek_wilkie) - #pragma include "hosek_wilkie/compute_scattering.inc.glsl" -#else - #error Unkown Scattering Method! -#endif +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +// This file just includes the scattering kernel based on the selected scattering +// method + + +#if ENUM_V_ACTIVE(scattering, scattering_method, eric_bruneton) + #pragma include "eric_bruneton/compute_scattering.inc.glsl" +#elif ENUM_V_ACTIVE(scattering, scattering_method, hosek_wilkie) + #pragma include "hosek_wilkie/compute_scattering.inc.glsl" +#else + #error Unkown Scattering Method! +#endif diff --git a/RenderPipelineFile/rpplugins/skin_shading/config.yaml b/RenderPipelineFile/rpplugins/skin_shading/config.yaml index 01deba1b..e98bfcd0 100644 --- a/RenderPipelineFile/rpplugins/skin_shading/config.yaml +++ b/RenderPipelineFile/rpplugins/skin_shading/config.yaml @@ -1,23 +1,23 @@ - -settings: !!omap - - - quality: - type: enum - values: ["low", "medium", "high"] - default: "medium" - label: Quality - shader_runtime: true - description: > - Controls the quality of the subsurface scattering, higher - values provide smoother results but also consume more performance. - - - blur_scale: - type: float - range: [0.1, 3.0] - default: 0.43 - shader_runtime: true - label: Filter Radius - description: Radius of the subsurface scattering filter, higher values - produce smoother results but might also produce artifacts. - -daytime_settings: + +settings: !!omap + + - quality: + type: enum + values: ["low", "medium", "high"] + default: "medium" + label: Quality + shader_runtime: true + description: > + Controls the quality of the subsurface scattering, higher + values provide smoother results but also consume more performance. + + - blur_scale: + type: float + range: [0.1, 3.0] + default: 0.43 + shader_runtime: true + label: Filter Radius + description: Radius of the subsurface scattering filter, higher values + produce smoother results but might also produce artifacts. + +daytime_settings: diff --git a/RenderPipelineFile/rpplugins/skin_shading/plugin.py b/RenderPipelineFile/rpplugins/skin_shading/plugin.py index b56cefa9..d5f15d61 100644 --- a/RenderPipelineFile/rpplugins/skin_shading/plugin.py +++ b/RenderPipelineFile/rpplugins/skin_shading/plugin.py @@ -1,41 +1,41 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.pluginbase.base_plugin import BasePlugin - -from .skin_shading_stage import SkinShadingStage - - -class Plugin(BasePlugin): - - name = "Skin Shading" - author = "tobspr " - description = ("This plugin adds support for Seperable Screen Space Subsurface " - "Scattering (SSSSS). This improves skin rendering.") - version = "1.0" - - def on_stage_setup(self): - self.stage = self.create_stage(SkinShadingStage) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.pluginbase.base_plugin import BasePlugin + +from .skin_shading_stage import SkinShadingStage + + +class Plugin(BasePlugin): + + name = "Skin Shading" + author = "tobspr " + description = ("This plugin adds support for Seperable Screen Space Subsurface " + "Scattering (SSSSS). This improves skin rendering.") + version = "1.0" + + def on_stage_setup(self): + self.stage = self.create_stage(SkinShadingStage) diff --git a/RenderPipelineFile/rpplugins/skin_shading/shader/SeperableSSS.inc.glsl b/RenderPipelineFile/rpplugins/skin_shading/shader/SeperableSSS.inc.glsl index 94f3f8f6..11e87a00 100644 --- a/RenderPipelineFile/rpplugins/skin_shading/shader/SeperableSSS.inc.glsl +++ b/RenderPipelineFile/rpplugins/skin_shading/shader/SeperableSSS.inc.glsl @@ -1,173 +1,173 @@ -/** - * - * NOTICE: Modified by tobspr for GLSL 4.0, removed all unnecessary stuff. - * Original copyright below. - * - * - * Copyright (C) 2012 Jorge Jimenez (jorge@iryoku.com) - * Copyright (C) 2012 Diego Gutierrez (diegog@unizar.es) - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the following disclaimer - * in the documentation and/or other materials provided with the - * distribution: - * - * "Uses Separable SSS. Copyright (C) 2012 by Jorge Jimenez and Diego - * Gutierrez." - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS - * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, - * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS - * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - * The views and conclusions contained in the software and documentation are - * those of the authors and should not be interpreted as representing official - * policies, either expressed or implied, of the copyright holders. - */ - -#pragma optionNV (unroll all) - -#if ENUM_V_ACTIVE(skin_shading, quality, low) - #define SSSS_QUALITY 0 -#elif ENUM_V_ACTIVE(skin_shading, quality, medium) - #define SSSS_QUALITY 1 -#elif ENUM_V_ACTIVE(skin_shading, quality, high) - #define SSSS_QUALITY 2 -#else - #error Invalid skin shading quality level -#endif - -#define SSSS_FOLLOW_SURFACE 1 - - -#if SSSS_QUALITY == 2 -#define SSSS_N_SAMPLES 25 -vec4 kernel[] = { - vec4(0.530605, 0.613514, 0.739601, 0), - vec4(0.000973794, 1.11862e-005, 9.43437e-007, -3), - vec4(0.00333804, 7.85443e-005, 1.2945e-005, -2.52083), - vec4(0.00500364, 0.00020094, 5.28848e-005, -2.08333), - vec4(0.00700976, 0.00049366, 0.000151938, -1.6875), - vec4(0.0094389, 0.00139119, 0.000416598, -1.33333), - vec4(0.0128496, 0.00356329, 0.00132016, -1.02083), - vec4(0.017924, 0.00711691, 0.00347194, -0.75), - vec4(0.0263642, 0.0119715, 0.00684598, -0.520833), - vec4(0.0410172, 0.0199899, 0.0118481, -0.333333), - vec4(0.0493588, 0.0367726, 0.0219485, -0.1875), - vec4(0.0402784, 0.0657244, 0.04631, -0.0833333), - vec4(0.0211412, 0.0459286, 0.0378196, -0.0208333), - vec4(0.0211412, 0.0459286, 0.0378196, 0.0208333), - vec4(0.0402784, 0.0657244, 0.04631, 0.0833333), - vec4(0.0493588, 0.0367726, 0.0219485, 0.1875), - vec4(0.0410172, 0.0199899, 0.0118481, 0.333333), - vec4(0.0263642, 0.0119715, 0.00684598, 0.520833), - vec4(0.017924, 0.00711691, 0.00347194, 0.75), - vec4(0.0128496, 0.00356329, 0.00132016, 1.02083), - vec4(0.0094389, 0.00139119, 0.000416598, 1.33333), - vec4(0.00700976, 0.00049366, 0.000151938, 1.6875), - vec4(0.00500364, 0.00020094, 5.28848e-005, 2.08333), - vec4(0.00333804, 7.85443e-005, 1.2945e-005, 2.52083), - vec4(0.000973794, 1.11862e-005, 9.43437e-007, 3), -}; -#elif SSSS_QUALITY == 1 -#define SSSS_N_SAMPLES 17 -vec4 kernel[] = { - vec4(0.536343, 0.624624, 0.748867, 0), - vec4(0.00317394, 0.000134823, 3.77269e-005, -2), - vec4(0.0100386, 0.000914679, 0.000275702, -1.53125), - vec4(0.0144609, 0.00317269, 0.00106399, -1.125), - vec4(0.0216301, 0.00794618, 0.00376991, -0.78125), - vec4(0.0347317, 0.0151085, 0.00871983, -0.5), - vec4(0.0571056, 0.0287432, 0.0172844, -0.28125), - vec4(0.0582416, 0.0659959, 0.0411329, -0.125), - vec4(0.0324462, 0.0656718, 0.0532821, -0.03125), - vec4(0.0324462, 0.0656718, 0.0532821, 0.03125), - vec4(0.0582416, 0.0659959, 0.0411329, 0.125), - vec4(0.0571056, 0.0287432, 0.0172844, 0.28125), - vec4(0.0347317, 0.0151085, 0.00871983, 0.5), - vec4(0.0216301, 0.00794618, 0.00376991, 0.78125), - vec4(0.0144609, 0.00317269, 0.00106399, 1.125), - vec4(0.0100386, 0.000914679, 0.000275702, 1.53125), - vec4(0.00317394, 0.000134823, 3.77269e-005, 2), -}; -#elif SSSS_QUALITY == 0 -#define SSSS_N_SAMPLES 11 -vec4 kernel[] = { - vec4(0.560479, 0.669086, 0.784728, 0), - vec4(0.00471691, 0.000184771, 5.07566e-005, -2), - vec4(0.0192831, 0.00282018, 0.00084214, -1.28), - vec4(0.03639, 0.0130999, 0.00643685, -0.72), - vec4(0.0821904, 0.0358608, 0.0209261, -0.32), - vec4(0.0771802, 0.113491, 0.0793803, -0.08), - vec4(0.0771802, 0.113491, 0.0793803, 0.08), - vec4(0.0821904, 0.0358608, 0.0209261, 0.32), - vec4(0.03639, 0.0130999, 0.00643685, 0.72), - vec4(0.0192831, 0.00282018, 0.00084214, 1.28), - vec4(0.00471691, 0.000184771, 5.07565e-005, 2), -}; -#else -#error Quality must be one of {0, 1, 2} -#endif - - -vec4 SSSSBlurPS( - vec2 texcoord, - sampler2D colorTex, - sampler2D depthTex, - float sssWidth, - float sssScale, - vec2 dir) { - - // Fetch color of current pixel: - vec4 colorM = textureLod(colorTex, texcoord, 0); - - // Fetch linear depth of current pixel: - float depthM = textureLod(depthTex, texcoord, 0).r; - - // Calculate the sssWidth scale (1.0 for a unit plane sitting on the - // projection window): - float distanceToProjectionWindow = 10.0; - float scale = distanceToProjectionWindow / depthM; - - // Calculate the final step to fetch the surrounding pixels: - vec2 finalStep = sssWidth * scale * dir; - finalStep *= sssScale; - finalStep *= 1.0 / 3.0; // Divide by 3 as the kernels range from -3 to 3. - - // Accumulate the center sample: - vec4 colorBlurred = colorM; - colorBlurred.rgb *= kernel[0].rgb; - - // Accumulate the other samples: - for (int i = 1; i < SSSS_N_SAMPLES; i++) { - // Fetch color and depth for current sample: - vec2 offset = texcoord + kernel[i].a * finalStep; - vec4 color = textureLod(colorTex, offset, 0); - - #if SSSS_FOLLOW_SURFACE == 1 - // If the difference in depth is huge, we lerp color back to "colorM": - float depth = textureLod(depthTex, offset, 0).r; - float s = saturate(1000.0f * distanceToProjectionWindow * - sssWidth * abs(depthM - depth)); - color.rgb = mix(color.rgb, colorM.rgb, s); - #endif - - // Accumulate: - colorBlurred.rgb += kernel[i].rgb * color.rgb; - } - - return colorBlurred; -} +/** + * + * NOTICE: Modified by tobspr for GLSL 4.0, removed all unnecessary stuff. + * Original copyright below. + * + * + * Copyright (C) 2012 Jorge Jimenez (jorge@iryoku.com) + * Copyright (C) 2012 Diego Gutierrez (diegog@unizar.es) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the following disclaimer + * in the documentation and/or other materials provided with the + * distribution: + * + * "Uses Separable SSS. Copyright (C) 2012 by Jorge Jimenez and Diego + * Gutierrez." + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS + * IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are + * those of the authors and should not be interpreted as representing official + * policies, either expressed or implied, of the copyright holders. + */ + +#pragma optionNV (unroll all) + +#if ENUM_V_ACTIVE(skin_shading, quality, low) + #define SSSS_QUALITY 0 +#elif ENUM_V_ACTIVE(skin_shading, quality, medium) + #define SSSS_QUALITY 1 +#elif ENUM_V_ACTIVE(skin_shading, quality, high) + #define SSSS_QUALITY 2 +#else + #error Invalid skin shading quality level +#endif + +#define SSSS_FOLLOW_SURFACE 1 + + +#if SSSS_QUALITY == 2 +#define SSSS_N_SAMPLES 25 +vec4 kernel[] = { + vec4(0.530605, 0.613514, 0.739601, 0), + vec4(0.000973794, 1.11862e-005, 9.43437e-007, -3), + vec4(0.00333804, 7.85443e-005, 1.2945e-005, -2.52083), + vec4(0.00500364, 0.00020094, 5.28848e-005, -2.08333), + vec4(0.00700976, 0.00049366, 0.000151938, -1.6875), + vec4(0.0094389, 0.00139119, 0.000416598, -1.33333), + vec4(0.0128496, 0.00356329, 0.00132016, -1.02083), + vec4(0.017924, 0.00711691, 0.00347194, -0.75), + vec4(0.0263642, 0.0119715, 0.00684598, -0.520833), + vec4(0.0410172, 0.0199899, 0.0118481, -0.333333), + vec4(0.0493588, 0.0367726, 0.0219485, -0.1875), + vec4(0.0402784, 0.0657244, 0.04631, -0.0833333), + vec4(0.0211412, 0.0459286, 0.0378196, -0.0208333), + vec4(0.0211412, 0.0459286, 0.0378196, 0.0208333), + vec4(0.0402784, 0.0657244, 0.04631, 0.0833333), + vec4(0.0493588, 0.0367726, 0.0219485, 0.1875), + vec4(0.0410172, 0.0199899, 0.0118481, 0.333333), + vec4(0.0263642, 0.0119715, 0.00684598, 0.520833), + vec4(0.017924, 0.00711691, 0.00347194, 0.75), + vec4(0.0128496, 0.00356329, 0.00132016, 1.02083), + vec4(0.0094389, 0.00139119, 0.000416598, 1.33333), + vec4(0.00700976, 0.00049366, 0.000151938, 1.6875), + vec4(0.00500364, 0.00020094, 5.28848e-005, 2.08333), + vec4(0.00333804, 7.85443e-005, 1.2945e-005, 2.52083), + vec4(0.000973794, 1.11862e-005, 9.43437e-007, 3), +}; +#elif SSSS_QUALITY == 1 +#define SSSS_N_SAMPLES 17 +vec4 kernel[] = { + vec4(0.536343, 0.624624, 0.748867, 0), + vec4(0.00317394, 0.000134823, 3.77269e-005, -2), + vec4(0.0100386, 0.000914679, 0.000275702, -1.53125), + vec4(0.0144609, 0.00317269, 0.00106399, -1.125), + vec4(0.0216301, 0.00794618, 0.00376991, -0.78125), + vec4(0.0347317, 0.0151085, 0.00871983, -0.5), + vec4(0.0571056, 0.0287432, 0.0172844, -0.28125), + vec4(0.0582416, 0.0659959, 0.0411329, -0.125), + vec4(0.0324462, 0.0656718, 0.0532821, -0.03125), + vec4(0.0324462, 0.0656718, 0.0532821, 0.03125), + vec4(0.0582416, 0.0659959, 0.0411329, 0.125), + vec4(0.0571056, 0.0287432, 0.0172844, 0.28125), + vec4(0.0347317, 0.0151085, 0.00871983, 0.5), + vec4(0.0216301, 0.00794618, 0.00376991, 0.78125), + vec4(0.0144609, 0.00317269, 0.00106399, 1.125), + vec4(0.0100386, 0.000914679, 0.000275702, 1.53125), + vec4(0.00317394, 0.000134823, 3.77269e-005, 2), +}; +#elif SSSS_QUALITY == 0 +#define SSSS_N_SAMPLES 11 +vec4 kernel[] = { + vec4(0.560479, 0.669086, 0.784728, 0), + vec4(0.00471691, 0.000184771, 5.07566e-005, -2), + vec4(0.0192831, 0.00282018, 0.00084214, -1.28), + vec4(0.03639, 0.0130999, 0.00643685, -0.72), + vec4(0.0821904, 0.0358608, 0.0209261, -0.32), + vec4(0.0771802, 0.113491, 0.0793803, -0.08), + vec4(0.0771802, 0.113491, 0.0793803, 0.08), + vec4(0.0821904, 0.0358608, 0.0209261, 0.32), + vec4(0.03639, 0.0130999, 0.00643685, 0.72), + vec4(0.0192831, 0.00282018, 0.00084214, 1.28), + vec4(0.00471691, 0.000184771, 5.07565e-005, 2), +}; +#else +#error Quality must be one of {0, 1, 2} +#endif + + +vec4 SSSSBlurPS( + vec2 texcoord, + sampler2D colorTex, + sampler2D depthTex, + float sssWidth, + float sssScale, + vec2 dir) { + + // Fetch color of current pixel: + vec4 colorM = textureLod(colorTex, texcoord, 0); + + // Fetch linear depth of current pixel: + float depthM = textureLod(depthTex, texcoord, 0).r; + + // Calculate the sssWidth scale (1.0 for a unit plane sitting on the + // projection window): + float distanceToProjectionWindow = 10.0; + float scale = distanceToProjectionWindow / depthM; + + // Calculate the final step to fetch the surrounding pixels: + vec2 finalStep = sssWidth * scale * dir; + finalStep *= sssScale; + finalStep *= 1.0 / 3.0; // Divide by 3 as the kernels range from -3 to 3. + + // Accumulate the center sample: + vec4 colorBlurred = colorM; + colorBlurred.rgb *= kernel[0].rgb; + + // Accumulate the other samples: + for (int i = 1; i < SSSS_N_SAMPLES; i++) { + // Fetch color and depth for current sample: + vec2 offset = texcoord + kernel[i].a * finalStep; + vec4 color = textureLod(colorTex, offset, 0); + + #if SSSS_FOLLOW_SURFACE == 1 + // If the difference in depth is huge, we lerp color back to "colorM": + float depth = textureLod(depthTex, offset, 0).r; + float s = saturate(1000.0f * distanceToProjectionWindow * + sssWidth * abs(depthM - depth)); + color.rgb = mix(color.rgb, colorM.rgb, s); + #endif + + // Accumulate: + colorBlurred.rgb += kernel[i].rgb * color.rgb; + } + + return colorBlurred; +} diff --git a/RenderPipelineFile/rpplugins/skin_shading/shader/sssss_blur.frag.glsl b/RenderPipelineFile/rpplugins/skin_shading/shader/sssss_blur.frag.glsl index 101c89bc..df841ed2 100644 --- a/RenderPipelineFile/rpplugins/skin_shading/shader/sssss_blur.frag.glsl +++ b/RenderPipelineFile/rpplugins/skin_shading/shader/sssss_blur.frag.glsl @@ -1,63 +1,63 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma optionNV (unroll all) - -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" - -#pragma include "SeperableSSS.inc.glsl" - -uniform sampler2D ShadedScene; -uniform ivec2 direction; - -out vec3 color; - -// Performs the subsurface scattering blur - -void main() { - vec2 texcoord = get_texcoord(); - int shading_model = get_gbuffer_shading_model(GBuffer, texcoord); - - // Early out - if (shading_model != SHADING_MODEL_SKIN) { - color = textureLod(ShadedScene, texcoord, 0).xyz; - return; - } - - const float sss_width = 0.01 * GET_SETTING(skin_shading, blur_scale); - vec4 blur_result = SSSSBlurPS(texcoord, ShadedScene, GBuffer.Depth, sss_width, - 1, vec2(direction)); - color = blur_result.xyz; - - #if DEBUG_MODE - color = textureLod(ShadedScene, texcoord, 0).xyz; - #endif - -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma optionNV (unroll all) + +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" + +#pragma include "SeperableSSS.inc.glsl" + +uniform sampler2D ShadedScene; +uniform ivec2 direction; + +out vec3 color; + +// Performs the subsurface scattering blur + +void main() { + vec2 texcoord = get_texcoord(); + int shading_model = get_gbuffer_shading_model(GBuffer, texcoord); + + // Early out + if (shading_model != SHADING_MODEL_SKIN) { + color = textureLod(ShadedScene, texcoord, 0).xyz; + return; + } + + const float sss_width = 0.01 * GET_SETTING(skin_shading, blur_scale); + vec4 blur_result = SSSSBlurPS(texcoord, ShadedScene, GBuffer.Depth, sss_width, + 1, vec2(direction)); + color = blur_result.xyz; + + #if DEBUG_MODE + color = textureLod(ShadedScene, texcoord, 0).xyz; + #endif + +} diff --git a/RenderPipelineFile/rpplugins/skin_shading/skin_shading_stage.py b/RenderPipelineFile/rpplugins/skin_shading/skin_shading_stage.py index eeab0321..9e540b6e 100644 --- a/RenderPipelineFile/rpplugins/skin_shading/skin_shading_stage.py +++ b/RenderPipelineFile/rpplugins/skin_shading/skin_shading_stage.py @@ -1,66 +1,66 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import LVecBase2i - -from six.moves import range # pylint: disable=import-error -from rpcore.render_stage import RenderStage - - -class SkinShadingStage(RenderStage): - - """ This is the main stage used by the SkinShadingStage plugin """ - - required_pipes = ["ShadedScene", "GBuffer"] - - @property - def produced_pipes(self): - return {"ShadedScene": self.final_tex} - - def create(self): - current_tex = None - self.blur_targets = [] - for i in range(3): - target_h = self.create_target("BlurH-" + str(i)) - target_h.add_color_attachment(bits=16) - target_h.prepare_buffer() - target_h.set_shader_input("direction", LVecBase2i(1, 0)) - if current_tex is not None: - target_h.set_shader_input("ShadedScene", current_tex, override=True) - current_tex = target_h.color_tex - target_v = self.create_target("BlurV-" + str(i)) - target_v.add_color_attachment(bits=16) - target_v.prepare_buffer() - target_v.set_shader_input("ShadedScene", current_tex, override=True) - target_v.set_shader_input("direction", LVecBase2i(0, 1)) - current_tex = target_v.color_tex - self.blur_targets += [target_h, target_v] - self.final_tex = current_tex - - def reload_shaders(self): - blur_shader = self.load_plugin_shader("sssss_blur.frag.glsl") - for target in self.blur_targets: - target.shader = blur_shader +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import LVecBase2i + +from six.moves import range # pylint: disable=import-error +from rpcore.render_stage import RenderStage + + +class SkinShadingStage(RenderStage): + + """ This is the main stage used by the SkinShadingStage plugin """ + + required_pipes = ["ShadedScene", "GBuffer"] + + @property + def produced_pipes(self): + return {"ShadedScene": self.final_tex} + + def create(self): + current_tex = None + self.blur_targets = [] + for i in range(3): + target_h = self.create_target("BlurH-" + str(i)) + target_h.add_color_attachment(bits=16) + target_h.prepare_buffer() + target_h.set_shader_input("direction", LVecBase2i(1, 0)) + if current_tex is not None: + target_h.set_shader_input("ShadedScene", current_tex, override=True) + current_tex = target_h.color_tex + target_v = self.create_target("BlurV-" + str(i)) + target_v.add_color_attachment(bits=16) + target_v.prepare_buffer() + target_v.set_shader_input("ShadedScene", current_tex, override=True) + target_v.set_shader_input("direction", LVecBase2i(0, 1)) + current_tex = target_v.color_tex + self.blur_targets += [target_h, target_v] + self.final_tex = current_tex + + def reload_shaders(self): + blur_shader = self.load_plugin_shader("sssss_blur.frag.glsl") + for target in self.blur_targets: + target.shader = blur_shader diff --git a/RenderPipelineFile/rpplugins/sky_ao/ao_stage.py b/RenderPipelineFile/rpplugins/sky_ao/ao_stage.py index a1ff37fd..a4468a7c 100644 --- a/RenderPipelineFile/rpplugins/sky_ao/ao_stage.py +++ b/RenderPipelineFile/rpplugins/sky_ao/ao_stage.py @@ -1,62 +1,62 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import Vec2 - -from rpcore.render_stage import RenderStage - - -class SkyAOStage(RenderStage): - - """ This is the main stage used by the Sky AO plugin, and computes - the sky occlusion term """ - - required_inputs = ["SkyAOCapturePosition"] - required_pipes = ["SkyAOHeight", "GBuffer"] - - @property - def produced_pipes(self): - return {"SkyAO": self.target_upscale.color_tex} - - def create(self): - self.target = self.create_target("ComputeSkyAO") - self.target.size = -2 - self.target.add_color_attachment(bits=(16, 0, 0, 0)) - self.target.prepare_buffer() - - self.target_upscale = self.create_target("UpscaleSkyAO") - self.target_upscale.add_color_attachment(bits=(16, 0, 0, 0)) - self.target_upscale.prepare_buffer() - - self.target_upscale.set_shader_inputs( - SourceTex=self.target.color_tex, - upscaleWeights=Vec2(0.001, 0.001)) - - def reload_shaders(self): - self.target.shader = self.load_plugin_shader( - "compute_sky_ao.frag.glsl") - self.target_upscale.shader = self.load_plugin_shader( - "/$$rp/shader/bilateral_upscale.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import Vec2 + +from rpcore.render_stage import RenderStage + + +class SkyAOStage(RenderStage): + + """ This is the main stage used by the Sky AO plugin, and computes + the sky occlusion term """ + + required_inputs = ["SkyAOCapturePosition"] + required_pipes = ["SkyAOHeight", "GBuffer"] + + @property + def produced_pipes(self): + return {"SkyAO": self.target_upscale.color_tex} + + def create(self): + self.target = self.create_target("ComputeSkyAO") + self.target.size = -2 + self.target.add_color_attachment(bits=(16, 0, 0, 0)) + self.target.prepare_buffer() + + self.target_upscale = self.create_target("UpscaleSkyAO") + self.target_upscale.add_color_attachment(bits=(16, 0, 0, 0)) + self.target_upscale.prepare_buffer() + + self.target_upscale.set_shader_inputs( + SourceTex=self.target.color_tex, + upscaleWeights=Vec2(0.001, 0.001)) + + def reload_shaders(self): + self.target.shader = self.load_plugin_shader( + "compute_sky_ao.frag.glsl") + self.target_upscale.shader = self.load_plugin_shader( + "/$$rp/shader/bilateral_upscale.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/sky_ao/capture_stage.py b/RenderPipelineFile/rpplugins/sky_ao/capture_stage.py index 314f18e9..cfae74ac 100644 --- a/RenderPipelineFile/rpplugins/sky_ao/capture_stage.py +++ b/RenderPipelineFile/rpplugins/sky_ao/capture_stage.py @@ -1,94 +1,94 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import division - -from panda3d.core import Camera, OrthographicLens, PTALVecBase3f - -from rpcore.globals import Globals -from rpcore.render_stage import RenderStage - - -class SkyAOCaptureStage(RenderStage): - - """ This stage captures the sky ao by rendering the scene from above """ - - required_inputs = [] - required_pipes = [] - - @property - def produced_pipes(self): - return {"SkyAOHeight": self.target_convert.color_tex} - - @property - def produced_inputs(self): - return {"SkyAOCapturePosition": self.pta_position} - - def __init__(self, pipeline): - RenderStage.__init__(self, pipeline) - self.pta_position = PTALVecBase3f.empty_array(1) - self.resolution = 512 - self.capture_height = 100.0 - self.max_radius = 100.0 - - def create(self): - self.camera = Camera("SkyAOCaptureCam") - self.cam_lens = OrthographicLens() - self.cam_lens.set_film_size(self.max_radius, self.max_radius) - self.cam_lens.set_near_far(0, self.capture_height) - self.camera.set_lens(self.cam_lens) - self.cam_node = Globals.base.render.attach_new_node(self.camera) - self.cam_node.look_at(0, 0, -1) - self.cam_node.set_r(0) - - self.target = self.create_target("SkyAOCapture") - self.target.size = self.resolution - self.target.add_depth_attachment(bits=16) - self.target.prepare_render(self.cam_node) - - self.target_convert = self.create_target("ConvertDepth") - self.target_convert.size = self.resolution - self.target_convert.add_color_attachment(bits=(16, 0, 0, 0)) - self.target_convert.prepare_buffer() - - self.target_convert.set_shader_inputs( - DepthTex=self.target.depth_tex, - position=self.pta_position) - - # Register camera - self._pipeline.tag_mgr.register_camera("shadow", self.camera) - - def update(self): - snap_size = self.max_radius / self.resolution - cam_pos = Globals.base.camera.get_pos(Globals.base.render) - self.cam_node.set_pos( - cam_pos.x - cam_pos.x % snap_size, - cam_pos.y - cam_pos.y % snap_size, - self.capture_height / 2.0) - self.pta_position[0] = self.cam_node.get_pos() - - def reload_shaders(self): - self.target_convert.shader = self.load_plugin_shader("convert_depth.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import division + +from panda3d.core import Camera, OrthographicLens, PTALVecBase3f + +from rpcore.globals import Globals +from rpcore.render_stage import RenderStage + + +class SkyAOCaptureStage(RenderStage): + + """ This stage captures the sky ao by rendering the scene from above """ + + required_inputs = [] + required_pipes = [] + + @property + def produced_pipes(self): + return {"SkyAOHeight": self.target_convert.color_tex} + + @property + def produced_inputs(self): + return {"SkyAOCapturePosition": self.pta_position} + + def __init__(self, pipeline): + RenderStage.__init__(self, pipeline) + self.pta_position = PTALVecBase3f.empty_array(1) + self.resolution = 512 + self.capture_height = 100.0 + self.max_radius = 100.0 + + def create(self): + self.camera = Camera("SkyAOCaptureCam") + self.cam_lens = OrthographicLens() + self.cam_lens.set_film_size(self.max_radius, self.max_radius) + self.cam_lens.set_near_far(0, self.capture_height) + self.camera.set_lens(self.cam_lens) + self.cam_node = Globals.base.render.attach_new_node(self.camera) + self.cam_node.look_at(0, 0, -1) + self.cam_node.set_r(0) + + self.target = self.create_target("SkyAOCapture") + self.target.size = self.resolution + self.target.add_depth_attachment(bits=16) + self.target.prepare_render(self.cam_node) + + self.target_convert = self.create_target("ConvertDepth") + self.target_convert.size = self.resolution + self.target_convert.add_color_attachment(bits=(16, 0, 0, 0)) + self.target_convert.prepare_buffer() + + self.target_convert.set_shader_inputs( + DepthTex=self.target.depth_tex, + position=self.pta_position) + + # Register camera + self._pipeline.tag_mgr.register_camera("shadow", self.camera) + + def update(self): + snap_size = self.max_radius / self.resolution + cam_pos = Globals.base.camera.get_pos(Globals.base.render) + self.cam_node.set_pos( + cam_pos.x - cam_pos.x % snap_size, + cam_pos.y - cam_pos.y % snap_size, + self.capture_height / 2.0) + self.pta_position[0] = self.cam_node.get_pos() + + def reload_shaders(self): + self.target_convert.shader = self.load_plugin_shader("convert_depth.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/sky_ao/config.yaml b/RenderPipelineFile/rpplugins/sky_ao/config.yaml index 1f0f272d..b84d38d5 100644 --- a/RenderPipelineFile/rpplugins/sky_ao/config.yaml +++ b/RenderPipelineFile/rpplugins/sky_ao/config.yaml @@ -1,79 +1,79 @@ - -settings: !!omap - - - sample_radius: - type: float - range: [0.1, 50.0] - default: 6.0 - shader_runtime: true - label: Sample radius - description: > - Controls the ao sample radius, higher values produce blurrier results, - but might cost more performance, and cause more jittering. - - - max_radius: - type: float - range: [10, 2000.0] - default: 200.0 - label: Maximum Radius - description: > - Controls how far the sky ao is computed, in world space. The higher - the maximum radius is, the further sky ao reaches, but it also reduces - the precision. - - - resolution: - type: power_of_two - range: [32, 8192] - default: 1024 - label: Resolution - description: > - A higher resolution provides more details, but also costs more - performance and memory. - - - sample_sequence: - type: sample_sequence - shader_runtime: true - dimension: 2 - default: poisson_2D_32 - label: Sample Sequence - description: > - Amout and Type of samples, higher values cost more performance - but provide smoother results. - - - ao_multiplier: - type: float - shader_runtime: true - range: [0.05, 5.0] - default: 0.25 - label: AO Multiplier - description: > - Controls the ao strength, higher values produce a darker ao. - - - ao_bias: - shader_runtime: true - type: float - range: [0.0, 1.0] - default: 0.3 - label: AO Bias - description: > - Miminum ambient occlusion term, to prevent total dark spots. - - - blend_factor: - type: float - shader_runtime: true - range: [0.01, 0.5] - default: 0.05 - label: Border Blending - description: > - Controls how smooth the sky ao fades out at distance (the maximum radius) - - - capture_height: - type: float - range: [10, 1500.0] - default: 250.0 - label: Capture height - description: > - Controls from which height the sky ao is captured. This should be the - height of your highest object, e.g. maximum terrain height. - -daytime_settings: + +settings: !!omap + + - sample_radius: + type: float + range: [0.1, 50.0] + default: 6.0 + shader_runtime: true + label: Sample radius + description: > + Controls the ao sample radius, higher values produce blurrier results, + but might cost more performance, and cause more jittering. + + - max_radius: + type: float + range: [10, 2000.0] + default: 200.0 + label: Maximum Radius + description: > + Controls how far the sky ao is computed, in world space. The higher + the maximum radius is, the further sky ao reaches, but it also reduces + the precision. + + - resolution: + type: power_of_two + range: [32, 8192] + default: 1024 + label: Resolution + description: > + A higher resolution provides more details, but also costs more + performance and memory. + + - sample_sequence: + type: sample_sequence + shader_runtime: true + dimension: 2 + default: poisson_2D_32 + label: Sample Sequence + description: > + Amout and Type of samples, higher values cost more performance + but provide smoother results. + + - ao_multiplier: + type: float + shader_runtime: true + range: [0.05, 5.0] + default: 0.25 + label: AO Multiplier + description: > + Controls the ao strength, higher values produce a darker ao. + + - ao_bias: + shader_runtime: true + type: float + range: [0.0, 1.0] + default: 0.3 + label: AO Bias + description: > + Miminum ambient occlusion term, to prevent total dark spots. + + - blend_factor: + type: float + shader_runtime: true + range: [0.01, 0.5] + default: 0.05 + label: Border Blending + description: > + Controls how smooth the sky ao fades out at distance (the maximum radius) + + - capture_height: + type: float + range: [10, 1500.0] + default: 250.0 + label: Capture height + description: > + Controls from which height the sky ao is captured. This should be the + height of your highest object, e.g. maximum terrain height. + +daytime_settings: diff --git a/RenderPipelineFile/rpplugins/sky_ao/plugin.py b/RenderPipelineFile/rpplugins/sky_ao/plugin.py index 5ddf96b6..1566522c 100644 --- a/RenderPipelineFile/rpplugins/sky_ao/plugin.py +++ b/RenderPipelineFile/rpplugins/sky_ao/plugin.py @@ -1,60 +1,60 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.pluginbase.base_plugin import BasePlugin -from rpcore.stages.ambient_stage import AmbientStage - -from .capture_stage import SkyAOCaptureStage -from .ao_stage import SkyAOStage - - -class Plugin(BasePlugin): - - name = "Sky Occlusion" - author = "tobspr " - description = ("This plugin adds support for sky occlusion, computing " - "occlusion based on the sky visibility.") - version = "0.2 beta" - - def on_stage_setup(self): - self.capture_stage = self.create_stage(SkyAOCaptureStage) - self.ao_stage = self.create_stage(SkyAOStage) - - self.capture_stage.resolution = self.get_setting("resolution") - self.capture_stage.max_radius = self.get_setting("max_radius") - self.capture_stage.capture_height = self.get_setting("capture_height") - - # Make the stages use our output - AmbientStage.required_pipes.append("SkyAO") - - def on_post_stage_setup(self): - if self.is_plugin_enabled("env_probes"): - self.get_plugin_instance("env_probes").capture_stage.required_inputs.append("SkyAOCapturePosition") - self.get_plugin_instance("env_probes").capture_stage.required_pipes.append("SkyAOHeight") - - - def on_pipeline_created(self): - pass +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.pluginbase.base_plugin import BasePlugin +from rpcore.stages.ambient_stage import AmbientStage + +from .capture_stage import SkyAOCaptureStage +from .ao_stage import SkyAOStage + + +class Plugin(BasePlugin): + + name = "Sky Occlusion" + author = "tobspr " + description = ("This plugin adds support for sky occlusion, computing " + "occlusion based on the sky visibility.") + version = "0.2 beta" + + def on_stage_setup(self): + self.capture_stage = self.create_stage(SkyAOCaptureStage) + self.ao_stage = self.create_stage(SkyAOStage) + + self.capture_stage.resolution = self.get_setting("resolution") + self.capture_stage.max_radius = self.get_setting("max_radius") + self.capture_stage.capture_height = self.get_setting("capture_height") + + # Make the stages use our output + AmbientStage.required_pipes.append("SkyAO") + + def on_post_stage_setup(self): + if self.is_plugin_enabled("env_probes"): + self.get_plugin_instance("env_probes").capture_stage.required_inputs.append("SkyAOCapturePosition") + self.get_plugin_instance("env_probes").capture_stage.required_pipes.append("SkyAOHeight") + + + def on_pipeline_created(self): + pass diff --git a/RenderPipelineFile/rpplugins/sky_ao/shader/compute_sky_ao.frag.glsl b/RenderPipelineFile/rpplugins/sky_ao/shader/compute_sky_ao.frag.glsl index 48474679..da436178 100644 --- a/RenderPipelineFile/rpplugins/sky_ao/shader/compute_sky_ao.frag.glsl +++ b/RenderPipelineFile/rpplugins/sky_ao/shader/compute_sky_ao.frag.glsl @@ -1,42 +1,42 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" - -#pragma include "sky_ao.inc.glsl" - -out vec4 result; - -void main() { - - vec2 texcoord = get_half_texcoord(); - Material m = unpack_material(GBuffer, texcoord); - result = vec4(compute_sky_ao(m.position, m.normal)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" + +#pragma include "sky_ao.inc.glsl" + +out vec4 result; + +void main() { + + vec2 texcoord = get_half_texcoord(); + Material m = unpack_material(GBuffer, texcoord); + result = vec4(compute_sky_ao(m.position, m.normal)); +} diff --git a/RenderPipelineFile/rpplugins/sky_ao/shader/convert_depth.frag.glsl b/RenderPipelineFile/rpplugins/sky_ao/shader/convert_depth.frag.glsl index 8fa983bd..7fcbcc44 100644 --- a/RenderPipelineFile/rpplugins/sky_ao/shader/convert_depth.frag.glsl +++ b/RenderPipelineFile/rpplugins/sky_ao/shader/convert_depth.frag.glsl @@ -1,47 +1,47 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/transforms.inc.glsl" - -uniform sampler2D DepthTex; -uniform vec3 position; - -out vec4 result; - -void main() { - ivec2 coord = ivec2(gl_FragCoord.xy); - - float depth_val = texelFetch(DepthTex, coord, 0).x; - - const float far_plane = GET_SETTING(sky_ao, capture_height); - - float linear_depth = depth_val * far_plane; - float z_pos = position.z - linear_depth; - result = vec4(z_pos); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/transforms.inc.glsl" + +uniform sampler2D DepthTex; +uniform vec3 position; + +out vec4 result; + +void main() { + ivec2 coord = ivec2(gl_FragCoord.xy); + + float depth_val = texelFetch(DepthTex, coord, 0).x; + + const float far_plane = GET_SETTING(sky_ao, capture_height); + + float linear_depth = depth_val * far_plane; + float z_pos = position.z - linear_depth; + result = vec4(z_pos); +} diff --git a/RenderPipelineFile/rpplugins/sky_ao/shader/sky_ao.inc.glsl b/RenderPipelineFile/rpplugins/sky_ao/shader/sky_ao.inc.glsl index 673e21a2..3ef912c1 100644 --- a/RenderPipelineFile/rpplugins/sky_ao/shader/sky_ao.inc.glsl +++ b/RenderPipelineFile/rpplugins/sky_ao/shader/sky_ao.inc.glsl @@ -1,98 +1,98 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ -#pragma once - -#pragma include "includes/sampling_sequences.inc.glsl" -#pragma include "includes/noise.inc.glsl" - -uniform sampler2D SkyAOHeight; -uniform vec3 SkyAOCapturePosition; - -float compute_sky_ao(vec3 ws_position, vec3 ws_normal) { - - // Constants - const int ao_map_resolution = GET_SETTING(sky_ao, resolution); - const float radius = GET_SETTING(sky_ao, sample_radius); - const float ao_multiplier = GET_SETTING(sky_ao, ao_multiplier); - const float ao_bias = GET_SETTING(sky_ao, ao_bias); - const float fade_scale = GET_SETTING(sky_ao, blend_factor); - const float ao_map_scale = GET_SETTING(sky_ao, max_radius); // world space - const float film_size = ao_map_scale * 0.5; - - - vec2 local_coord = (ws_position.xy - SkyAOCapturePosition.xy) / film_size * 0.5 + 0.5; - - if (out_of_screen(local_coord)) { - return 1.0; - } - - float blend = 1.0; - blend *= saturate(min(local_coord.x, local_coord.y) / fade_scale); - blend *= saturate(min(1 - local_coord.x, 1 - local_coord.y) / fade_scale); - - - // Factor to convert from tex-space to world-space - const float tc_to_ws = ao_map_scale; - - - float accum = 0.0; - vec2 jitter = rand_rgb(ivec2(gl_FragCoord.xy) % 2).xy; - - // Capture samples - - // vec3 avg_unoccluded_normal = vec3(0); - - START_ITERATE_SEQUENCE(sky_ao, sample_sequence, vec2 offset) - - offset = (offset * 0.9 + 0.1 * jitter) / ao_map_resolution * radius; - vec2 offcoord = local_coord + offset; - - float sample_z = textureLod(SkyAOHeight, offcoord, 0).x; - - - accum += saturate(1.0 * (sample_z - ws_position.z)); - // if (sample_z > ws_position.z) - // accum += 1.0; - // Project sample in normal direction - // float dist = length(offset) * tc_to_ws; - // float proj_z = ws_position.z + dist * ws_normal.z; - - - // accum += saturate((sample_z - proj_z) * 2.0); - // if (proj_z >= sample_z) { - // accum += 1.0; - // } - - END_ITERATE_SEQUENCE() - - NORMALIZE_SEQUENCE(sky_ao, sample_sequence, accum); - - accum = saturate(1 - accum + ao_bias); - accum = pow(accum, 0.25 * ao_multiplier); - accum = mix(1, accum, blend); - - return accum; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ +#pragma once + +#pragma include "includes/sampling_sequences.inc.glsl" +#pragma include "includes/noise.inc.glsl" + +uniform sampler2D SkyAOHeight; +uniform vec3 SkyAOCapturePosition; + +float compute_sky_ao(vec3 ws_position, vec3 ws_normal) { + + // Constants + const int ao_map_resolution = GET_SETTING(sky_ao, resolution); + const float radius = GET_SETTING(sky_ao, sample_radius); + const float ao_multiplier = GET_SETTING(sky_ao, ao_multiplier); + const float ao_bias = GET_SETTING(sky_ao, ao_bias); + const float fade_scale = GET_SETTING(sky_ao, blend_factor); + const float ao_map_scale = GET_SETTING(sky_ao, max_radius); // world space + const float film_size = ao_map_scale * 0.5; + + + vec2 local_coord = (ws_position.xy - SkyAOCapturePosition.xy) / film_size * 0.5 + 0.5; + + if (out_of_screen(local_coord)) { + return 1.0; + } + + float blend = 1.0; + blend *= saturate(min(local_coord.x, local_coord.y) / fade_scale); + blend *= saturate(min(1 - local_coord.x, 1 - local_coord.y) / fade_scale); + + + // Factor to convert from tex-space to world-space + const float tc_to_ws = ao_map_scale; + + + float accum = 0.0; + vec2 jitter = rand_rgb(ivec2(gl_FragCoord.xy) % 2).xy; + + // Capture samples + + // vec3 avg_unoccluded_normal = vec3(0); + + START_ITERATE_SEQUENCE(sky_ao, sample_sequence, vec2 offset) + + offset = (offset * 0.9 + 0.1 * jitter) / ao_map_resolution * radius; + vec2 offcoord = local_coord + offset; + + float sample_z = textureLod(SkyAOHeight, offcoord, 0).x; + + + accum += saturate(1.0 * (sample_z - ws_position.z)); + // if (sample_z > ws_position.z) + // accum += 1.0; + // Project sample in normal direction + // float dist = length(offset) * tc_to_ws; + // float proj_z = ws_position.z + dist * ws_normal.z; + + + // accum += saturate((sample_z - proj_z) * 2.0); + // if (proj_z >= sample_z) { + // accum += 1.0; + // } + + END_ITERATE_SEQUENCE() + + NORMALIZE_SEQUENCE(sky_ao, sample_sequence, accum); + + accum = saturate(1 - accum + ao_bias); + accum = pow(accum, 0.25 * ao_multiplier); + accum = mix(1, accum, blend); + + return accum; +} diff --git a/RenderPipelineFile/rpplugins/smaa/LICENSE.txt b/RenderPipelineFile/rpplugins/smaa/LICENSE.txt index fa509891..a757682d 100644 --- a/RenderPipelineFile/rpplugins/smaa/LICENSE.txt +++ b/RenderPipelineFile/rpplugins/smaa/LICENSE.txt @@ -1,34 +1,34 @@ - -This plugin uses the SMAA code from: - -https://github.com/iryoku/smaa - -You can find the license below: - ----- - -Copyright (C) 2013 Jorge Jimenez (jorge@iryoku.com) -Copyright (C) 2013 Jose I. Echevarria (joseignacioechevarria@gmail.com) -Copyright (C) 2013 Belen Masia (bmasia@unizar.es) -Copyright (C) 2013 Fernando Navarro (fernandn@microsoft.com) -Copyright (C) 2013 Diego Gutierrez (diegog@unizar.es) - -Permission is hereby granted, free of charge, to any person obtaining a copy -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. As clarification, there is no -requirement that the copyright notice and permission be included in binary -distributions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - + +This plugin uses the SMAA code from: + +https://github.com/iryoku/smaa + +You can find the license below: + +---- + +Copyright (C) 2013 Jorge Jimenez (jorge@iryoku.com) +Copyright (C) 2013 Jose I. Echevarria (joseignacioechevarria@gmail.com) +Copyright (C) 2013 Belen Masia (bmasia@unizar.es) +Copyright (C) 2013 Fernando Navarro (fernandn@microsoft.com) +Copyright (C) 2013 Diego Gutierrez (diegog@unizar.es) + +Permission is hereby granted, free of charge, to any person obtaining a copy +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. As clarification, there is no +requirement that the copyright notice and permission be included in binary +distributions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/RenderPipelineFile/rpplugins/smaa/config.yaml b/RenderPipelineFile/rpplugins/smaa/config.yaml index b5ae3a0e..f8fa5aa4 100644 --- a/RenderPipelineFile/rpplugins/smaa/config.yaml +++ b/RenderPipelineFile/rpplugins/smaa/config.yaml @@ -1,61 +1,61 @@ - -settings: !!omap - - - use_reprojection: - type: bool - default: true - label: Use temporal antialiasing (SMAA T2) - description: > - This setting enables temporal antialiasing, which increases the - antialiasing quality a lot. Basically the scene gets rendered a - little different every frame, and the results of the current and - the last frame get merged. This way, flickering is reduced by a lot, - and the image becomes more smooth. - - - smaa_quality: - type: enum - default: ultra - values: ["low", "medium", "high", "ultra"] - label: SMAA Quality Preset - shader_runtime: true - description: > - The quality of the smaa algorithm. Usually you should choose "ultra", - since it does not have a significant higher cost, but provides way - better quality. However, on low-end systems, it might be desireable - to turn this value down. - - - jitter_pattern: - type: enum - default: halton8 - values: ["halton8", "skew_butterly", "uniform4", "halton256", "rotated4_helix2", "uniform2", - "halton16", "still", "rotated4", "rotated4_helix", "poisson10", "halton32", - "uniform4_helix", "uniform4_double_helix", "pentagram"] - runtime: true - label: Jitter sequence - description: > - Controls the jitter pattern to use - - - history_length: - display_if: { use_reprojection: True } - type: int - range: [1, 16] - default: 8 - shader_runtime: true - label: History Length - description: > - History buffer size. This controls how long pixels will stay - in the history buffer, greater values lead to smoother results - but also might produce more artifacts. - - - jitter_amount: - display_if: { use_reprojection: True } - type: float - range: [0.05, 2.0] - default: 0.3 - runtime: true - label: Jitter Amount - description: > - Controls how much each pixel is jittered, higher values might - preserve better details but also introduce flickering. - -daytime_settings: + +settings: !!omap + + - use_reprojection: + type: bool + default: true + label: Use temporal antialiasing (SMAA T2) + description: > + This setting enables temporal antialiasing, which increases the + antialiasing quality a lot. Basically the scene gets rendered a + little different every frame, and the results of the current and + the last frame get merged. This way, flickering is reduced by a lot, + and the image becomes more smooth. + + - smaa_quality: + type: enum + default: ultra + values: ["low", "medium", "high", "ultra"] + label: SMAA Quality Preset + shader_runtime: true + description: > + The quality of the smaa algorithm. Usually you should choose "ultra", + since it does not have a significant higher cost, but provides way + better quality. However, on low-end systems, it might be desireable + to turn this value down. + + - jitter_pattern: + type: enum + default: halton8 + values: ["halton8", "skew_butterly", "uniform4", "halton256", "rotated4_helix2", "uniform2", + "halton16", "still", "rotated4", "rotated4_helix", "poisson10", "halton32", + "uniform4_helix", "uniform4_double_helix", "pentagram"] + runtime: true + label: Jitter sequence + description: > + Controls the jitter pattern to use + + - history_length: + display_if: { use_reprojection: True } + type: int + range: [1, 16] + default: 8 + shader_runtime: true + label: History Length + description: > + History buffer size. This controls how long pixels will stay + in the history buffer, greater values lead to smoother results + but also might produce more artifacts. + + - jitter_amount: + display_if: { use_reprojection: True } + type: float + range: [0.05, 2.0] + default: 0.3 + runtime: true + label: Jitter Amount + description: > + Controls how much each pixel is jittered, higher values might + preserve better details but also introduce flickering. + +daytime_settings: diff --git a/RenderPipelineFile/rpplugins/smaa/jitters.py b/RenderPipelineFile/rpplugins/smaa/jitters.py index 32be2c85..ecb35357 100644 --- a/RenderPipelineFile/rpplugins/smaa/jitters.py +++ b/RenderPipelineFile/rpplugins/smaa/jitters.py @@ -1,140 +1,140 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -# pylint: skip-file -# flake8: noqa - -# Jitters from -# https://github.com/playdeadgames/temporal/blob/master/Assets/Scripts/FrustumJitter.cs - -from __future__ import division -from panda3d.core import Vec2 - -__all__ = ("JITTERS",) - -JITTERS = { - - "still": [ - Vec2(0.0, 0.0), - ], - - "uniform2": [ - Vec2(-0.25, -0.25), # ll - Vec2( 0.25, 0.25), # ur - ], - - "uniform4": [ - Vec2(-0.25, -0.25), # ll - Vec2( 0.25, -0.25), # lr - Vec2( 0.25, 0.25), # ur - Vec2(-0.25, 0.25), # ul - ], - - "uniform4_helix": [ - Vec2(-0.25, -0.25), # ll 3 1 - Vec2( 0.25, 0.25), # ur \/| - Vec2( 0.25, -0.25), # lr /\| - Vec2(-0.25, 0.25), # ul 0 2 - ], - - "uniform4_double_helix": [ - Vec2(-0.25, -0.25), # ll 3 1 - Vec2( 0.25, 0.25), # ur \/| - Vec2( 0.25, -0.25), # lr /\| - Vec2(-0.25, 0.25), # ul 0 2 - Vec2(-0.25, -0.25), # ll 6--7 - Vec2( 0.25, -0.25), # lr \ - Vec2(-0.25, 0.25), # ul \ - Vec2( 0.25, 0.25), # ur 4--5 - ], - - "skew_butterly": [ - Vec2(-0.250, -0.250), - Vec2( 0.250, 0.250), - Vec2( 0.125, -0.125), - Vec2(-0.125, 0.125), - ], - - "rotated4": [ - Vec2(-0.125, -0.375), # ll - Vec2( 0.375, -0.125), # lr - Vec2( 0.125, 0.375), # ur - Vec2(-0.375, 0.125), # ul - ], - - "rotated4_helix": [ - Vec2(-0.125, -0.375), # ll 3 1 - Vec2( 0.125, 0.375), # ur \/| - Vec2( 0.375, -0.125), # lr /\| - Vec2(-0.375, 0.125), # ul 0 2 - ], - - "rotated4_helix2": [ - Vec2(-0.125, -0.375), # ll 2--1 - Vec2( 0.125, 0.375), # ur \/ - Vec2(-0.375, 0.125), # ul /\ - Vec2( 0.375, -0.125), # lr 0 3 - ], - - "poisson10": [ - Vec2(-0.16795960 * 0.25, 0.65544910 * 0.25), - Vec2(-0.69096030 * 0.25, 0.59015970 * 0.25), - Vec2( 0.49843820 * 0.25, 0.83099720 * 0.25), - Vec2( 0.17230150 * 0.25, -0.03882703 * 0.25), - Vec2(-0.60772670 * 0.25, -0.06013587 * 0.25), - Vec2( 0.65606390 * 0.25, 0.24007600 * 0.25), - Vec2( 0.80348370 * 0.25, -0.48096900 * 0.25), - Vec2( 0.33436540 * 0.25, -0.73007030 * 0.25), - Vec2(-0.47839520 * 0.25, -0.56005300 * 0.25), - Vec2(-0.12388120 * 0.25, -0.96633990 * 0.25), - ], - - "pentagram": [ - Vec2( 0.000000 * 0.5, 0.525731 * 0.5), # head - Vec2(-0.309017 * 0.5, -0.425325 * 0.5), # lleg - Vec2( 0.500000 * 0.5, 0.162460 * 0.5), # rarm - Vec2(-0.500000 * 0.5, 0.162460 * 0.5), # larm - Vec2( 0.309017 * 0.5, -0.425325 * 0.5), # rleg - ], -} - - -def halton_seq(prime, index=1): - """ Low discrepancy halton sequence with a given prime """ - r, f, i = 0.0, 1.0, index - while i > 0: - f /= prime - r += f * (i % prime) - i = int(i / float(prime)) - return r - -# Initialize halton sequences -for seq_num in [8, 16, 32, 256]: - sequence = [] - for seq_iter in range(seq_num): - point = Vec2(halton_seq(2, seq_iter + 1) - 0.5, halton_seq(3, seq_iter + 1) - 0.5) - sequence.append(point) - JITTERS["halton" + str(seq_num)] = sequence +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +# pylint: skip-file +# flake8: noqa + +# Jitters from +# https://github.com/playdeadgames/temporal/blob/master/Assets/Scripts/FrustumJitter.cs + +from __future__ import division +from panda3d.core import Vec2 + +__all__ = ("JITTERS",) + +JITTERS = { + + "still": [ + Vec2(0.0, 0.0), + ], + + "uniform2": [ + Vec2(-0.25, -0.25), # ll + Vec2( 0.25, 0.25), # ur + ], + + "uniform4": [ + Vec2(-0.25, -0.25), # ll + Vec2( 0.25, -0.25), # lr + Vec2( 0.25, 0.25), # ur + Vec2(-0.25, 0.25), # ul + ], + + "uniform4_helix": [ + Vec2(-0.25, -0.25), # ll 3 1 + Vec2( 0.25, 0.25), # ur \/| + Vec2( 0.25, -0.25), # lr /\| + Vec2(-0.25, 0.25), # ul 0 2 + ], + + "uniform4_double_helix": [ + Vec2(-0.25, -0.25), # ll 3 1 + Vec2( 0.25, 0.25), # ur \/| + Vec2( 0.25, -0.25), # lr /\| + Vec2(-0.25, 0.25), # ul 0 2 + Vec2(-0.25, -0.25), # ll 6--7 + Vec2( 0.25, -0.25), # lr \ + Vec2(-0.25, 0.25), # ul \ + Vec2( 0.25, 0.25), # ur 4--5 + ], + + "skew_butterly": [ + Vec2(-0.250, -0.250), + Vec2( 0.250, 0.250), + Vec2( 0.125, -0.125), + Vec2(-0.125, 0.125), + ], + + "rotated4": [ + Vec2(-0.125, -0.375), # ll + Vec2( 0.375, -0.125), # lr + Vec2( 0.125, 0.375), # ur + Vec2(-0.375, 0.125), # ul + ], + + "rotated4_helix": [ + Vec2(-0.125, -0.375), # ll 3 1 + Vec2( 0.125, 0.375), # ur \/| + Vec2( 0.375, -0.125), # lr /\| + Vec2(-0.375, 0.125), # ul 0 2 + ], + + "rotated4_helix2": [ + Vec2(-0.125, -0.375), # ll 2--1 + Vec2( 0.125, 0.375), # ur \/ + Vec2(-0.375, 0.125), # ul /\ + Vec2( 0.375, -0.125), # lr 0 3 + ], + + "poisson10": [ + Vec2(-0.16795960 * 0.25, 0.65544910 * 0.25), + Vec2(-0.69096030 * 0.25, 0.59015970 * 0.25), + Vec2( 0.49843820 * 0.25, 0.83099720 * 0.25), + Vec2( 0.17230150 * 0.25, -0.03882703 * 0.25), + Vec2(-0.60772670 * 0.25, -0.06013587 * 0.25), + Vec2( 0.65606390 * 0.25, 0.24007600 * 0.25), + Vec2( 0.80348370 * 0.25, -0.48096900 * 0.25), + Vec2( 0.33436540 * 0.25, -0.73007030 * 0.25), + Vec2(-0.47839520 * 0.25, -0.56005300 * 0.25), + Vec2(-0.12388120 * 0.25, -0.96633990 * 0.25), + ], + + "pentagram": [ + Vec2( 0.000000 * 0.5, 0.525731 * 0.5), # head + Vec2(-0.309017 * 0.5, -0.425325 * 0.5), # lleg + Vec2( 0.500000 * 0.5, 0.162460 * 0.5), # rarm + Vec2(-0.500000 * 0.5, 0.162460 * 0.5), # larm + Vec2( 0.309017 * 0.5, -0.425325 * 0.5), # rleg + ], +} + + +def halton_seq(prime, index=1): + """ Low discrepancy halton sequence with a given prime """ + r, f, i = 0.0, 1.0, index + while i > 0: + f /= prime + r += f * (i % prime) + i = int(i / float(prime)) + return r + +# Initialize halton sequences +for seq_num in [8, 16, 32, 256]: + sequence = [] + for seq_iter in range(seq_num): + point = Vec2(halton_seq(2, seq_iter + 1) - 0.5, halton_seq(3, seq_iter + 1) - 0.5) + sequence.append(point) + JITTERS["halton" + str(seq_num)] = sequence diff --git a/RenderPipelineFile/rpplugins/smaa/plugin.py b/RenderPipelineFile/rpplugins/smaa/plugin.py index 466ef011..37e7b983 100644 --- a/RenderPipelineFile/rpplugins/smaa/plugin.py +++ b/RenderPipelineFile/rpplugins/smaa/plugin.py @@ -1,108 +1,108 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import SamplerState - -from rpcore.globals import Globals -from rpcore.loader import RPLoader -from rpcore.pluginbase.base_plugin import BasePlugin - -from .smaa_stage import SMAAStage -from .jitters import JITTERS - - -class Plugin(BasePlugin): - - name = "SMAA (Antialiasing)" - author = "tobspr " - description = ("This plugin adds support for SMAA, a post process " - "antialiasing technique.") - version = "1.5" - - def on_stage_setup(self): - if self.get_setting("use_reprojection"): - self._compute_jitters() - - self._smaa_stage = self.create_stage(SMAAStage) - self._smaa_stage.use_reprojection = self.get_setting("use_reprojection") - self._load_textures() - - def on_pre_render_update(self): - # Apply jitter for temporal aa - if self.get_setting("use_reprojection"): - jitter_scale = self.get_setting("jitter_amount") - jitter = self._jitters[self._jitter_index] - jitter = jitter[0] * jitter_scale, jitter[1] * jitter_scale - - Globals.base.camLens.set_film_offset(jitter) - self._smaa_stage.set_jitter_index(self._jitter_index) - - # Increase jitter index - self._jitter_index += 1 - if self._jitter_index >= len(self._jitters): - self._jitter_index = 0 - - def _compute_jitters(self): - """ Internal method to compute the SMAA sub-pixel frame offsets """ - self._jitters = [] - self._jitter_index = 0 - scale = 1.0 / float(Globals.native_resolution.x) - - # Reduce jittering to 35% to avoid flickering - scale *= 0.35 - - for x, y in JITTERS[self.get_setting("jitter_pattern")]: - jitter_x = (x * 2 - 1) * scale * 0.5 - jitter_y = (y * 2 - 1) * scale * 0.5 - self._jitters.append((jitter_x, jitter_y)) - - @property - def history_length(self): - if self.get_setting("use_reprojection"): - return len(self._jitters) - return 1 - - def update_jitter_pattern(self): - """ Updates the jitter pattern when the setting was changed """ - self._compute_jitters() - - def on_window_resized(self): - """ Updates the jitter pattern when the window size was changed """ - self._compute_jitters() - - def _load_textures(self): - """ Loads all required textures """ - search_tex = RPLoader.load_texture(self.get_resource("search_tex.png")) - area_tex = RPLoader.load_texture(self.get_resource("area_tex.png")) - - for tex in [search_tex, area_tex]: - tex.set_minfilter(SamplerState.FT_linear) - tex.set_magfilter(SamplerState.FT_linear) - tex.set_wrap_u(SamplerState.WM_clamp) - tex.set_wrap_v(SamplerState.WM_clamp) - - self._smaa_stage.area_tex = area_tex - self._smaa_stage.search_tex = search_tex +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import SamplerState + +from rpcore.globals import Globals +from rpcore.loader import RPLoader +from rpcore.pluginbase.base_plugin import BasePlugin + +from .smaa_stage import SMAAStage +from .jitters import JITTERS + + +class Plugin(BasePlugin): + + name = "SMAA (Antialiasing)" + author = "tobspr " + description = ("This plugin adds support for SMAA, a post process " + "antialiasing technique.") + version = "1.5" + + def on_stage_setup(self): + if self.get_setting("use_reprojection"): + self._compute_jitters() + + self._smaa_stage = self.create_stage(SMAAStage) + self._smaa_stage.use_reprojection = self.get_setting("use_reprojection") + self._load_textures() + + def on_pre_render_update(self): + # Apply jitter for temporal aa + if self.get_setting("use_reprojection"): + jitter_scale = self.get_setting("jitter_amount") + jitter = self._jitters[self._jitter_index] + jitter = jitter[0] * jitter_scale, jitter[1] * jitter_scale + + Globals.base.camLens.set_film_offset(jitter) + self._smaa_stage.set_jitter_index(self._jitter_index) + + # Increase jitter index + self._jitter_index += 1 + if self._jitter_index >= len(self._jitters): + self._jitter_index = 0 + + def _compute_jitters(self): + """ Internal method to compute the SMAA sub-pixel frame offsets """ + self._jitters = [] + self._jitter_index = 0 + scale = 1.0 / float(Globals.native_resolution.x) + + # Reduce jittering to 35% to avoid flickering + scale *= 0.35 + + for x, y in JITTERS[self.get_setting("jitter_pattern")]: + jitter_x = (x * 2 - 1) * scale * 0.5 + jitter_y = (y * 2 - 1) * scale * 0.5 + self._jitters.append((jitter_x, jitter_y)) + + @property + def history_length(self): + if self.get_setting("use_reprojection"): + return len(self._jitters) + return 1 + + def update_jitter_pattern(self): + """ Updates the jitter pattern when the setting was changed """ + self._compute_jitters() + + def on_window_resized(self): + """ Updates the jitter pattern when the window size was changed """ + self._compute_jitters() + + def _load_textures(self): + """ Loads all required textures """ + search_tex = RPLoader.load_texture(self.get_resource("search_tex.png")) + area_tex = RPLoader.load_texture(self.get_resource("area_tex.png")) + + for tex in [search_tex, area_tex]: + tex.set_minfilter(SamplerState.FT_linear) + tex.set_magfilter(SamplerState.FT_linear) + tex.set_wrap_u(SamplerState.WM_clamp) + tex.set_wrap_v(SamplerState.WM_clamp) + + self._smaa_stage.area_tex = area_tex + self._smaa_stage.search_tex = search_tex diff --git a/RenderPipelineFile/rpplugins/smaa/shader/SMAA.inc.glsl b/RenderPipelineFile/rpplugins/smaa/shader/SMAA.inc.glsl index c5ed9d3f..509c862f 100644 --- a/RenderPipelineFile/rpplugins/smaa/shader/SMAA.inc.glsl +++ b/RenderPipelineFile/rpplugins/smaa/shader/SMAA.inc.glsl @@ -1,1309 +1,1309 @@ - -// lint: skip-file - - -/** - * Copyright (C) 2013 Jorge Jimenez (jorge@iryoku.com) - * Copyright (C) 2013 Jose I. Echevarria (joseignacioechevarria@gmail.com) - * Copyright (C) 2013 Belen Masia (bmasia@unizar.es) - * Copyright (C) 2013 Fernando Navarro (fernandn@microsoft.com) - * Copyright (C) 2013 Diego Gutierrez (diegog@unizar.es) - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to - * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies - * of the Software, and to permit persons to whom the Software is furnished to - * do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. As clarification, there - * is no requirement that the copyright notice and permission be included in - * binary distributions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - - -/** - * _______ ___ ___ ___ ___ - * / || \/ | / \ / \ - * | (---- | \ / | / ^ \ / ^ \ - * \ \ | |\/| | / /_\ \ / /_\ \ - * ----) | | | | | / _____ \ / _____ \ - * |_______/ |__| |__| /__/ \__\ /__/ \__\ - * - * E N H A N C E D - * S U B P I X E L M O R P H O L O G I C A L A N T I A L I A S I N G - * - * http://www.iryoku.com/smaa/ - * - * Hi, welcome aboard! - * - * Here you'll find instructions to get the shader up and running as fast as - * possible. - * - * IMPORTANTE NOTICE: when updating, remember to update both this file and the - * precomputed textures! They may change from version to version. - * - * The shader has three passes, chained together as follows: - * - * |input|------------------ - * v | - * [ SMAA*EdgeDetection ] | - * v | - * |edgesTex| | - * v | - * [ SMAABlendingWeightCalculation ] | - * v | - * |blendTex| | - * v | - * [ SMAANeighborhoodBlending ] <------ - * v - * |output| - * - * Note that each [pass] has its own vertex and pixel shader. Remember to use - * oversized triangles instead of quads to avoid overshading along the - * diagonal. - * - * You've three edge detection methods to choose from: luma, color or depth. - * They represent different quality/performance and anti-aliasing/sharpness - * tradeoffs, so our recommendation is for you to choose the one that best - * suits your particular scenario: - * - * - Depth edge detection is usually the fastest but it may miss some edges. - * - * - Luma edge detection is usually more expensive than depth edge detection, - * but catches visible edges that depth edge detection can miss. - * - * - Color edge detection is usually the most expensive one but catches - * chroma-only edges. - * - * For quickstarters: just use luma edge detection. - * - * The general advice is to not rush the integration process and ensure each - * step is done correctly (don't try to integrate SMAA T2x with predicated edge - * detection from the start!). Ok then, let's go! - * - * 1. The first step is to create two RGBA temporal render targets for holding - * |edgesTex| and |blendTex|. - * - * In DX10 or DX11, you can use a RG render target for the edges texture. - * In the case of NVIDIA GPUs, using RG render targets seems to actually be - * slower. - * - * On the Xbox 360, you can use the same render target for resolving both - * |edgesTex| and |blendTex|, as they aren't needed simultaneously. - * - * 2. Both temporal render targets |edgesTex| and |blendTex| must be cleared - * each frame. Do not forget to clear the alpha channel! - * - * 3. The next step is loading the two supporting precalculated textures, - * 'areaTex' and 'searchTex'. You'll find them in the 'Textures' folder as - * C++ headers, and also as regular DDS files. They'll be needed for the - * 'SMAABlendingWeightCalculation' pass. - * - * If you use the C++ headers, be sure to load them in the format specified - * inside of them. - * - * You can also compress 'areaTex' and 'searchTex' using BC5 and BC4 - * respectively, if you have that option in your content processor pipeline. - * When compressing then, you get a non-perceptible quality decrease, and a - * marginal performance increase. - * - * 4. All samplers must be set to linear filtering and clamp. - * - * After you get the technique working, remember that 64-bit inputs have - * half-rate linear filtering on GCN. - * - * If SMAA is applied to 64-bit color buffers, switching to point filtering - * when accesing them will increase the performance. Search for - * 'SMAASamplePoint' to see which textures may benefit from point - * filtering, and where (which is basically the color input in the edge - * detection and resolve passes). - * - * 5. All texture reads and buffer writes must be non-sRGB, with the exception - * of the input read and the output write in - * 'SMAANeighborhoodBlending' (and only in this pass!). If sRGB reads in - * this last pass are not possible, the technique will work anyway, but - * will perform antialiasing in gamma space. - * - * IMPORTANT: for best results the input read for the color/luma edge - * detection should *NOT* be sRGB. - * - * 6. Before including SMAA.h you'll have to setup the render target metrics, - * the target and any optional configuration defines. Optionally you can - * use a preset. - * - * You have the following targets available: - * SMAA_HLSL_3 - * SMAA_HLSL_4 - * SMAA_HLSL_4_1 - * SMAA_GLSL_3 * - * SMAA_GLSL_4 * - * - * * (See SMAA_INCLUDE_VS and SMAA_INCLUDE_PS below). - * - * And four presets: - * SMAA_PRESET_LOW (%60 of the quality) - * SMAA_PRESET_MEDIUM (%80 of the quality) - * SMAA_PRESET_HIGH (%95 of the quality) - * SMAA_PRESET_ULTRA (%99 of the quality) - * - * For example: - * #define SMAA_RT_METRICS float4(1.0 / 1280.0, 1.0 / 720.0, 1280.0, 720.0) - * #define SMAA_HLSL_4 - * #define SMAA_PRESET_HIGH - * #include "SMAA.h" - * - * Note that SMAA_RT_METRICS doesn't need to be a macro, it can be a - * uniform variable. The code is designed to minimize the impact of not - * using a constant value, but it is still better to hardcode it. - * - * Depending on how you encoded 'areaTex' and 'searchTex', you may have to - * add (and customize) the following defines before including SMAA.h: - * #define SMAA_AREATEX_SELECT(sample) sample.rg - * #define SMAA_SEARCHTEX_SELECT(sample) sample.r - * - * If your engine is already using porting macros, you can define - * SMAA_CUSTOM_SL, and define the porting functions by yourself. - * - * 7. Then, you'll have to setup the passes as indicated in the scheme above. - * You can take a look into SMAA.fx, to see how we did it for our demo. - * Checkout the function wrappers, you may want to copy-paste them! - * - * 8. It's recommended to validate the produced |edgesTex| and |blendTex|. - * You can use a screenshot from your engine to compare the |edgesTex| - * and |blendTex| produced inside of the engine with the results obtained - * with the reference demo. - * - * 9. After you get the last pass to work, it's time to optimize. You'll have - * to initialize a stencil buffer in the first pass (discard is already in - * the code), then mask execution by using it the second pass. The last - * pass should be executed in all pixels. - * - * - * After this point you can choose to enable predicated thresholding, - * temporal supersampling and motion blur integration: - * - * a) If you want to use predicated thresholding, take a look into - * SMAA_PREDICATION; you'll need to pass an extra texture in the edge - * detection pass. - * - * b) If you want to enable temporal supersampling (SMAA T2x): - * - * 1. The first step is to render using subpixel jitters. I won't go into - * detail, but it's as simple as moving each vertex position in the - * vertex shader, you can check how we do it in our DX10 demo. - * - * 2. Then, you must setup the temporal resolve. You may want to take a look - * into SMAAResolve for resolving 2x modes. After you get it working, you'll - * probably see ghosting everywhere. But fear not, you can enable the - * CryENGINE temporal reprojection by setting the SMAA_REPROJECTION macro. - * Check out SMAA_DECODE_VELOCITY if your velocity buffer is encoded. - * - * 3. The next step is to apply SMAA to each subpixel jittered frame, just as - * done for 1x. - * - * 4. At this point you should already have something usable, but for best - * results the proper area textures must be set depending on current jitter. - * For this, the parameter 'subsampleIndices' of - * 'SMAABlendingWeightCalculationPS' must be set as follows, for our T2x - * mode: - * - * @SUBSAMPLE_INDICES - * - * | S# | Camera Jitter | subsampleIndices | - * +----+------------------+---------------------+ - * | 0 | ( 0.25, -0.25) | float4(1, 1, 1, 0) | - * | 1 | (-0.25, 0.25) | float4(2, 2, 2, 0) | - * - * These jitter positions assume a bottom-to-top y axis. S# stands for the - * sample number. - * - * More information about temporal supersampling here: - * http://iryoku.com/aacourse/downloads/13-Anti-Aliasing-Methods-in-CryENGINE-3.pdf - * - * c) If you want to enable spatial multisampling (SMAA S2x): - * - * 1. The scene must be rendered using MSAA 2x. The MSAA 2x buffer must be - * created with: - * - DX10: see below (*) - * - DX10.1: D3D10_STANDARD_MULTISAMPLE_PATTERN or - * - DX11: D3D11_STANDARD_MULTISAMPLE_PATTERN - * - * This allows to ensure that the subsample order matches the table in - * @SUBSAMPLE_INDICES. - * - * (*) In the case of DX10, we refer the reader to: - * - SMAA::detectMSAAOrder and - * - SMAA::msaaReorder - * - * These functions allow to match the standard multisample patterns by - * detecting the subsample order for a specific GPU, and reordering - * them appropriately. - * - * 2. A shader must be run to output each subsample into a separate buffer - * (DX10 is required). You can use SMAASeparate for this purpose, or just do - * it in an existing pass (for example, in the tone mapping pass, which has - * the advantage of feeding tone mapped subsamples to SMAA, which will yield - * better results). - * - * 3. The full SMAA 1x pipeline must be run for each separated buffer, storing - * the results in the final buffer. The second run should alpha blend with - * the existing final buffer using a blending factor of 0.5. - * 'subsampleIndices' must be adjusted as in the SMAA T2x case (see point - * b). - * - * d) If you want to enable temporal supersampling on top of SMAA S2x - * (which actually is SMAA 4x): - * - * 1. SMAA 4x consists on temporally jittering SMAA S2x, so the first step is - * to calculate SMAA S2x for current frame. In this case, 'subsampleIndices' - * must be set as follows: - * - * | F# | S# | Camera Jitter | Net Jitter | subsampleIndices | - * +----+----+--------------------+-------------------+----------------------+ - * | 0 | 0 | ( 0.125, 0.125) | ( 0.375, -0.125) | float4(5, 3, 1, 3) | - * | 0 | 1 | ( 0.125, 0.125) | (-0.125, 0.375) | float4(4, 6, 2, 3) | - * +----+----+--------------------+-------------------+----------------------+ - * | 1 | 2 | (-0.125, -0.125) | ( 0.125, -0.375) | float4(3, 5, 1, 4) | - * | 1 | 3 | (-0.125, -0.125) | (-0.375, 0.125) | float4(6, 4, 2, 4) | - * - * These jitter positions assume a bottom-to-top y axis. F# stands for the - * frame number. S# stands for the sample number. - * - * 2. After calculating SMAA S2x for current frame (with the new subsample - * indices), previous frame must be reprojected as in SMAA T2x mode (see - * point b). - * - * e) If motion blur is used, you may want to do the edge detection pass - * together with motion blur. This has two advantages: - * - * 1. Pixels under heavy motion can be omitted from the edge detection process. - * For these pixels we can just store "no edge", as motion blur will take - * care of them. - * 2. The center pixel tap is reused. - * - * Note that in this case depth testing should be used instead of stenciling, - * as we have to write all the pixels in the motion blur pass. - * - * That's it! - */ - -//----------------------------------------------------------------------------- -// SMAA Presets - -/** - * Note that if you use one of these presets, the following configuration - * macros will be ignored if set in the "Configurable Defines" section. - */ - -#if defined(SMAA_PRESET_LOW) -#define SMAA_THRESHOLD 0.15 -#define SMAA_MAX_SEARCH_STEPS 4 -#define SMAA_DISABLE_DIAG_DETECTION -#define SMAA_DISABLE_CORNER_DETECTION -#elif defined(SMAA_PRESET_MEDIUM) -#define SMAA_THRESHOLD 0.1 -#define SMAA_MAX_SEARCH_STEPS 8 -#define SMAA_DISABLE_DIAG_DETECTION -#define SMAA_DISABLE_CORNER_DETECTION -#elif defined(SMAA_PRESET_HIGH) -#define SMAA_THRESHOLD 0.1 -#define SMAA_MAX_SEARCH_STEPS 16 -#define SMAA_MAX_SEARCH_STEPS_DIAG 8 -#define SMAA_CORNER_ROUNDING 25 -#elif defined(SMAA_PRESET_ULTRA) -#define SMAA_THRESHOLD 0.05 -#define SMAA_MAX_SEARCH_STEPS 32 -#define SMAA_MAX_SEARCH_STEPS_DIAG 16 -#define SMAA_CORNER_ROUNDING 25 -#endif - -//----------------------------------------------------------------------------- -// Configurable Defines - -/** - * SMAA_THRESHOLD specifies the threshold or sensitivity to edges. - * Lowering this value you will be able to detect more edges at the expense of - * performance. - * - * Range: [0, 0.5] - * 0.1 is a reasonable value, and allows to catch most visible edges. - * 0.05 is a rather overkill value, that allows to catch 'em all. - * - * If temporal supersampling is used, 0.2 could be a reasonable value, as low - * contrast edges are properly filtered by just 2x. - */ -#ifndef SMAA_THRESHOLD -#define SMAA_THRESHOLD 0.1 -#endif - -/** - * SMAA_DEPTH_THRESHOLD specifies the threshold for depth edge detection. - * - * Range: depends on the depth range of the scene. - */ -#ifndef SMAA_DEPTH_THRESHOLD -#define SMAA_DEPTH_THRESHOLD (0.1 * SMAA_THRESHOLD) -#endif - -/** - * SMAA_MAX_SEARCH_STEPS specifies the maximum steps performed in the - * horizontal/vertical pattern searches, at each side of the pixel. - * - * In number of pixels, it's actually the double. So the maximum line length - * perfectly handled by, for example 16, is 64 (by perfectly, we meant that - * longer lines won't look as good, but still antialiased). - * - * Range: [0, 112] - */ -#ifndef SMAA_MAX_SEARCH_STEPS -#define SMAA_MAX_SEARCH_STEPS 16 -#endif - -/** - * SMAA_MAX_SEARCH_STEPS_DIAG specifies the maximum steps performed in the - * diagonal pattern searches, at each side of the pixel. In this case we jump - * one pixel at time, instead of two. - * - * Range: [0, 20] - * - * On high-end machines it is cheap (between a 0.8x and 0.9x slower for 16 - * steps), but it can have a significant impact on older machines. - * - * Define SMAA_DISABLE_DIAG_DETECTION to disable diagonal processing. - */ -#ifndef SMAA_MAX_SEARCH_STEPS_DIAG -#define SMAA_MAX_SEARCH_STEPS_DIAG 8 -#endif - -/** - * SMAA_CORNER_ROUNDING specifies how much sharp corners will be rounded. - * - * Range: [0, 100] - * - * Define SMAA_DISABLE_CORNER_DETECTION to disable corner processing. - */ -#ifndef SMAA_CORNER_ROUNDING -#define SMAA_CORNER_ROUNDING 25 -#endif - -/** - * If there is an neighbor edge that has SMAA_LOCAL_CONTRAST_FACTOR times - * bigger contrast than current edge, current edge will be discarded. - * - * This allows to eliminate spurious crossing edges, and is based on the fact - * that, if there is too much contrast in a direction, that will hide - * perceptually contrast in the other neighbors. - */ -#ifndef SMAA_LOCAL_CONTRAST_ADAPTATION_FACTOR -#define SMAA_LOCAL_CONTRAST_ADAPTATION_FACTOR 2.0 -#endif - -/** - * Predicated thresholding allows to better preserve texture details and to - * improve performance, by decreasing the number of detected edges using an - * additional buffer like the light accumulation buffer, object ids or even the - * depth buffer (the depth buffer usage may be limited to indoor or short range - * scenes). - * - * It locally decreases the luma or color threshold if an edge is found in an - * additional buffer (so the global threshold can be higher). - * - * This method was developed by Playstation EDGE MLAA team, and used in - * Killzone 3, by using the light accumulation buffer. More information here: - * http://iryoku.com/aacourse/downloads/06-MLAA-on-PS3.pptx - */ -#ifndef SMAA_PREDICATION -#define SMAA_PREDICATION 0 -#endif - -/** - * Threshold to be used in the additional predication buffer. - * - * Range: depends on the input, so you'll have to find the magic number that - * works for you. - */ -#ifndef SMAA_PREDICATION_THRESHOLD -#define SMAA_PREDICATION_THRESHOLD 0.01 -#endif - -/** - * How much to scale the global threshold used for luma or color edge - * detection when using predication. - * - * Range: [1, 5] - */ -#ifndef SMAA_PREDICATION_SCALE -#define SMAA_PREDICATION_SCALE 2.0 -#endif - -/** - * How much to locally decrease the threshold. - * - * Range: [0, 1] - */ -#ifndef SMAA_PREDICATION_STRENGTH -#define SMAA_PREDICATION_STRENGTH 0.4 -#endif - -/** - * Temporal reprojection allows to remove ghosting artifacts when using - * temporal supersampling. We use the CryEngine 3 method which also introduces - * velocity weighting. This feature is of extreme importance for totally - * removing ghosting. More information here: - * http://iryoku.com/aacourse/downloads/13-Anti-Aliasing-Methods-in-CryENGINE-3.pdf - * - * Note that you'll need to setup a velocity buffer for enabling reprojection. - * For static geometry, saving the previous depth buffer is a viable - * alternative. - */ -#ifndef SMAA_REPROJECTION -#define SMAA_REPROJECTION 0 -#endif - -/** - * SMAA_REPROJECTION_WEIGHT_SCALE controls the velocity weighting. It allows to - * remove ghosting trails behind the moving object, which are not removed by - * just using reprojection. Using low values will exhibit ghosting, while using - * high values will disable temporal supersampling under motion. - * - * Behind the scenes, velocity weighting removes temporal supersampling when - * the velocity of the subsamples differs (meaning they are different objects). - * - * Range: [0, 80] - */ -#ifndef SMAA_REPROJECTION_WEIGHT_SCALE -#define SMAA_REPROJECTION_WEIGHT_SCALE 30.0 -#endif - -/** - * On some compilers, discard cannot be used in vertex shaders. Thus, they need - * to be compiled separately. - */ -#ifndef SMAA_INCLUDE_VS -#define SMAA_INCLUDE_VS 1 -#endif -#ifndef SMAA_INCLUDE_PS -#define SMAA_INCLUDE_PS 1 -#endif - -//----------------------------------------------------------------------------- -// Texture Access Defines - -#ifndef SMAA_AREATEX_SELECT -#if defined(SMAA_HLSL_3) -#define SMAA_AREATEX_SELECT(sample) sample.ra -#else -#define SMAA_AREATEX_SELECT(sample) sample.rg -#endif -#endif - -#ifndef SMAA_SEARCHTEX_SELECT -#define SMAA_SEARCHTEX_SELECT(sample) sample.r -#endif - -#ifndef SMAA_DECODE_VELOCITY -#define SMAA_DECODE_VELOCITY(sample) sample.rg -#endif - -//----------------------------------------------------------------------------- -// Non-Configurable Defines - -#define SMAA_AREATEX_MAX_DISTANCE 16 -#define SMAA_AREATEX_MAX_DISTANCE_DIAG 20 -#define SMAA_AREATEX_PIXEL_SIZE (1.0 / float2(160.0, 560.0)) -#define SMAA_AREATEX_SUBTEX_SIZE (1.0 / 7.0) -#define SMAA_SEARCHTEX_SIZE float2(66.0, 33.0) -#define SMAA_SEARCHTEX_PACKED_SIZE float2(64.0, 16.0) -#define SMAA_CORNER_ROUNDING_NORM (float(SMAA_CORNER_ROUNDING) / 100.0) - -//----------------------------------------------------------------------------- -// Porting Functions - -#if defined(SMAA_HLSL_3) -#define SMAATexture2D(tex) sampler2D tex -#define SMAATexturePass2D(tex) tex -#define SMAASampleLevelZero(tex, coord) tex2Dlod(tex, float4(coord, 0.0, 0.0)) -#define SMAASampleLevelZeroPoint(tex, coord) tex2Dlod(tex, float4(coord, 0.0, 0.0)) -#define SMAASampleLevelZeroOffset(tex, coord, offset) tex2Dlod(tex, float4(coord + offset * SMAA_RT_METRICS.xy, 0.0, 0.0)) -#define SMAASample(tex, coord) tex2D(tex, coord) -#define SMAASamplePoint(tex, coord) tex2D(tex, coord) -#define SMAASampleOffset(tex, coord, offset) tex2D(tex, coord + offset * SMAA_RT_METRICS.xy) -#define SMAA_FLATTEN [flatten] -#define SMAA_BRANCH [branch] -#endif -#if defined(SMAA_HLSL_4) || defined(SMAA_HLSL_4_1) -SamplerState LinearSampler { Filter = MIN_MAG_LINEAR_MIP_POINT; AddressU = Clamp; AddressV = Clamp; }; -SamplerState PointSampler { Filter = MIN_MAG_MIP_POINT; AddressU = Clamp; AddressV = Clamp; }; -#define SMAATexture2D(tex) Texture2D tex -#define SMAATexturePass2D(tex) tex -#define SMAASampleLevelZero(tex, coord) tex.SampleLevel(LinearSampler, coord, 0) -#define SMAASampleLevelZeroPoint(tex, coord) tex.SampleLevel(PointSampler, coord, 0) -#define SMAASampleLevelZeroOffset(tex, coord, offset) tex.SampleLevel(LinearSampler, coord, 0, offset) -#define SMAASample(tex, coord) tex.Sample(LinearSampler, coord) -#define SMAASamplePoint(tex, coord) tex.Sample(PointSampler, coord) -#define SMAASampleOffset(tex, coord, offset) tex.Sample(LinearSampler, coord, offset) -#define SMAA_FLATTEN [flatten] -#define SMAA_BRANCH [branch] -#define SMAATexture2DMS2(tex) Texture2DMS tex -#define SMAALoad(tex, pos, sample) tex.Load(pos, sample) -#if defined(SMAA_HLSL_4_1) -#define SMAAGather(tex, coord) tex.Gather(LinearSampler, coord, 0) -#endif -#endif -#if defined(SMAA_GLSL_3) || defined(SMAA_GLSL_4) -#define SMAATexture2D(tex) sampler2D tex -#define SMAATexturePass2D(tex) tex -#define SMAASampleLevelZero(tex, coord) textureLod(tex, coord, 0.0) -#define SMAASampleLevelZeroPoint(tex, coord) textureLod(tex, coord, 0.0) -#define SMAASampleLevelZeroOffset(tex, coord, offset) textureLodOffset(tex, coord, 0.0, offset) -#define SMAASample(tex, coord) textureLod(tex, coord, 0) -#define SMAASamplePoint(tex, coord) textureLod(tex, coord, 0) -#define SMAASampleOffset(tex, coord, offset) texture(tex, coord, offset) -#define SMAA_FLATTEN -#define SMAA_BRANCH -#define lerp(a, b, t) mix(a, b, t) -#define saturate(a) clamp(a, 0.0, 1.0) -#if defined(SMAA_GLSL_4) -#define mad(a, b, c) fma(a, b, c) -#define SMAAGather(tex, coord) textureGather(tex, coord) -#else -#define mad(a, b, c) (a * b + c) -#endif -#define float2 vec2 -#define float3 vec3 -#define float4 vec4 -#define int2 ivec2 -#define int3 ivec3 -#define int4 ivec4 -#define bool2 bvec2 -#define bool3 bvec3 -#define bool4 bvec4 -#endif - -#if !defined(SMAA_HLSL_3) && !defined(SMAA_HLSL_4) && !defined(SMAA_HLSL_4_1) && !defined(SMAA_GLSL_3) && !defined(SMAA_GLSL_4) && !defined(SMAA_CUSTOM_SL) -#error you must define the shading language: SMAA_HLSL_*, SMAA_GLSL_* or SMAA_CUSTOM_SL -#endif - -//----------------------------------------------------------------------------- -// Misc functions - -/** - * Gathers current pixel, and the top-left neighbors. - */ -float3 SMAAGatherNeighbours(float2 texcoord, - float4 offset[3], - SMAATexture2D(tex)) { - #ifdef SMAAGather - return SMAAGather(tex, texcoord + SMAA_RT_METRICS.xy * float2(-0.5, -0.5)).grb; - #else - float P = SMAASamplePoint(tex, texcoord).r; - float Pleft = SMAASamplePoint(tex, offset[0].xy).r; - float Ptop = SMAASamplePoint(tex, offset[0].zw).r; - return float3(P, Pleft, Ptop); - #endif -} - -/** - * Adjusts the threshold by means of predication. - */ -float2 SMAACalculatePredicatedThreshold(float2 texcoord, - float4 offset[3], - SMAATexture2D(predicationTex)) { - float3 neighbours = SMAAGatherNeighbours(texcoord, offset, SMAATexturePass2D(predicationTex)); - float2 delta = abs(neighbours.xx - neighbours.yz); - float2 edges = step(SMAA_PREDICATION_THRESHOLD, delta); - return SMAA_PREDICATION_SCALE * SMAA_THRESHOLD * (1.0 - SMAA_PREDICATION_STRENGTH * edges); -} - -/** - * Conditional move: - */ -void SMAAMovc(bool2 cond, inout float2 variable, float2 value) { - SMAA_FLATTEN if (cond.x) variable.x = value.x; - SMAA_FLATTEN if (cond.y) variable.y = value.y; -} - -void SMAAMovc(bool4 cond, inout float4 variable, float4 value) { - SMAAMovc(cond.xy, variable.xy, value.xy); - SMAAMovc(cond.zw, variable.zw, value.zw); -} - - -#if SMAA_INCLUDE_VS -//----------------------------------------------------------------------------- -// Vertex Shaders - -/** - * Edge Detection Vertex Shader - */ -void SMAAEdgeDetectionVS(float2 texcoord, - out float4 offset[3]) { - offset[0] = mad(SMAA_RT_METRICS.xyxy, float4(-1.0, 0.0, 0.0, -1.0), texcoord.xyxy); - offset[1] = mad(SMAA_RT_METRICS.xyxy, float4( 1.0, 0.0, 0.0, 1.0), texcoord.xyxy); - offset[2] = mad(SMAA_RT_METRICS.xyxy, float4(-2.0, 0.0, 0.0, -2.0), texcoord.xyxy); -} - -/** - * Blend Weight Calculation Vertex Shader - */ -void SMAABlendingWeightCalculationVS(float2 texcoord, - out float2 pixcoord, - out float4 offset[3]) { - pixcoord = texcoord * SMAA_RT_METRICS.zw; - - // We will use these offsets for the searches later on (see @PSEUDO_GATHER4): - offset[0] = mad(SMAA_RT_METRICS.xyxy, float4(-0.25, -0.125, 1.25, -0.125), texcoord.xyxy); - offset[1] = mad(SMAA_RT_METRICS.xyxy, float4(-0.125, -0.25, -0.125, 1.25), texcoord.xyxy); - - // And these for the searches, they indicate the ends of the loops: - offset[2] = mad(SMAA_RT_METRICS.xxyy, - float4(-2.0, 2.0, -2.0, 2.0) * float(SMAA_MAX_SEARCH_STEPS), - float4(offset[0].xz, offset[1].yw)); -} - -/** - * Neighborhood Blending Vertex Shader - */ -void SMAANeighborhoodBlendingVS(float2 texcoord, - out float4 offset) { - offset = mad(SMAA_RT_METRICS.xyxy, float4( 1.0, 0.0, 0.0, 1.0), texcoord.xyxy); -} -#endif // SMAA_INCLUDE_VS - -#if SMAA_INCLUDE_PS -//----------------------------------------------------------------------------- -// Edge Detection Pixel Shaders (First Pass) - -/** - * Luma Edge Detection - * - * IMPORTANT NOTICE: luma edge detection requires gamma-corrected colors, and - * thus 'colorTex' should be a non-sRGB texture. - */ -float2 SMAALumaEdgeDetectionPS(float2 texcoord, - float4 offset[3], - SMAATexture2D(colorTex) - #if SMAA_PREDICATION - , SMAATexture2D(predicationTex) - #endif - ) { - // Calculate the threshold: - #if SMAA_PREDICATION - float2 threshold = SMAACalculatePredicatedThreshold(texcoord, offset, SMAATexturePass2D(predicationTex)); - #else - float2 threshold = float2(SMAA_THRESHOLD, SMAA_THRESHOLD); - #endif - - // Calculate lumas: - float3 weights = float3(0.2126, 0.7152, 0.0722); - float L = dot(SMAASamplePoint(colorTex, texcoord).rgb, weights); - - float Lleft = dot(SMAASamplePoint(colorTex, offset[0].xy).rgb, weights); - float Ltop = dot(SMAASamplePoint(colorTex, offset[0].zw).rgb, weights); - - // We do the usual threshold: - float4 delta; - delta.xy = abs(L - float2(Lleft, Ltop)); - float2 edges = step(threshold, delta.xy); - - // Then discard if there is no edge: - if (dot(edges, float2(1.0, 1.0)) == 0.0) - discard; - - // Calculate right and bottom deltas: - float Lright = dot(SMAASamplePoint(colorTex, offset[1].xy).rgb, weights); - float Lbottom = dot(SMAASamplePoint(colorTex, offset[1].zw).rgb, weights); - delta.zw = abs(L - float2(Lright, Lbottom)); - - // Calculate the maximum delta in the direct neighborhood: - float2 maxDelta = max(delta.xy, delta.zw); - - // Calculate left-left and top-top deltas: - float Lleftleft = dot(SMAASamplePoint(colorTex, offset[2].xy).rgb, weights); - float Ltoptop = dot(SMAASamplePoint(colorTex, offset[2].zw).rgb, weights); - delta.zw = abs(float2(Lleft, Ltop) - float2(Lleftleft, Ltoptop)); - - // Calculate the final maximum delta: - maxDelta = max(maxDelta.xy, delta.zw); - float finalDelta = max(maxDelta.x, maxDelta.y); - - // Local contrast adaptation: - edges.xy *= step(finalDelta, SMAA_LOCAL_CONTRAST_ADAPTATION_FACTOR * delta.xy); - - return edges; -} - -/** - * Color Edge Detection - * - * IMPORTANT NOTICE: color edge detection requires gamma-corrected colors, and - * thus 'colorTex' should be a non-sRGB texture. - */ -float2 SMAAColorEdgeDetectionPS(float2 texcoord, - float4 offset[3], - SMAATexture2D(colorTex) - #if SMAA_PREDICATION - , SMAATexture2D(predicationTex) - #endif - ) { - // Calculate the threshold: - #if SMAA_PREDICATION - float2 threshold = SMAACalculatePredicatedThreshold(texcoord, offset, predicationTex); - #else - float2 threshold = float2(SMAA_THRESHOLD, SMAA_THRESHOLD); - #endif - - // Calculate color deltas: - float4 delta; - float3 C = SMAA_GET_COLOR(SMAASamplePoint(colorTex, texcoord).rgb); - - float3 Cleft = SMAA_GET_COLOR(SMAASamplePoint(colorTex, offset[0].xy).rgb); - float3 t = abs(C - Cleft); - delta.x = max(max(t.r, t.g), t.b); - - float3 Ctop = SMAA_GET_COLOR(SMAASamplePoint(colorTex, offset[0].zw).rgb); - t = abs(C - Ctop); - delta.y = max(max(t.r, t.g), t.b); - - // We do the usual threshold: - float2 edges = step(threshold, delta.xy); - - // Then discard if there is no edge: - if (dot(edges, float2(1.0, 1.0)) == 0.0) - discard; - - // Calculate right and bottom deltas: - float3 Cright = SMAA_GET_COLOR(SMAASamplePoint(colorTex, offset[1].xy).rgb); - t = abs(C - Cright); - delta.z = max(max(t.r, t.g), t.b); - - float3 Cbottom = SMAA_GET_COLOR(SMAASamplePoint(colorTex, offset[1].zw).rgb); - t = abs(C - Cbottom); - delta.w = max(max(t.r, t.g), t.b); - - // Calculate the maximum delta in the direct neighborhood: - float2 maxDelta = max(delta.xy, delta.zw); - - // Calculate left-left and top-top deltas: - float3 Cleftleft = SMAA_GET_COLOR(SMAASamplePoint(colorTex, offset[2].xy).rgb); - t = abs(C - Cleftleft); - delta.z = max(max(t.r, t.g), t.b); - - float3 Ctoptop = SMAA_GET_COLOR(SMAASamplePoint(colorTex, offset[2].zw).rgb); - t = abs(C - Ctoptop); - delta.w = max(max(t.r, t.g), t.b); - - // Calculate the final maximum delta: - maxDelta = max(maxDelta.xy, delta.zw); - float finalDelta = max(maxDelta.x, maxDelta.y); - - // Local contrast adaptation: - edges.xy *= step(finalDelta, SMAA_LOCAL_CONTRAST_ADAPTATION_FACTOR * delta.xy); - - return edges; -} - -/** - * Depth Edge Detection - */ -float2 SMAADepthEdgeDetectionPS(float2 texcoord, - float4 offset[3], - SMAATexture2D(depthTex)) { - float3 neighbours = SMAAGatherNeighbours(texcoord, offset, SMAATexturePass2D(depthTex)); - float2 delta = abs(neighbours.xx - float2(neighbours.y, neighbours.z)); - float2 edges = step(SMAA_DEPTH_THRESHOLD, delta); - - if (dot(edges, float2(1.0, 1.0)) == 0.0) - discard; - - return edges; -} - -//----------------------------------------------------------------------------- -// Diagonal Search Functions - -#if !defined(SMAA_DISABLE_DIAG_DETECTION) - -/** - * Allows to decode two binary values from a bilinear-filtered access. - */ -float2 SMAADecodeDiagBilinearAccess(float2 e) { - // Bilinear access for fetching 'e' have a 0.25 offset, and we are - // interested in the R and G edges: - // - // +---G---+-------+ - // | x o R x | - // +-------+-------+ - // - // Then, if one of these edge is enabled: - // Red: (0.75 * X + 0.25 * 1) => 0.25 or 1.0 - // Green: (0.75 * 1 + 0.25 * X) => 0.75 or 1.0 - // - // This function will unpack the values (mad + mul + round): - // wolframalpha.com: round(x * abs(5 * x - 5 * 0.75)) plot 0 to 1 - e.r = e.r * abs(5.0 * e.r - 5.0 * 0.75); - return round(e); -} - -float4 SMAADecodeDiagBilinearAccess(float4 e) { - e.rb = e.rb * abs(5.0 * e.rb - 5.0 * 0.75); - return round(e); -} - -/** - * These functions allows to perform diagonal pattern searches. - */ -float2 SMAASearchDiag1(SMAATexture2D(edgesTex), float2 texcoord, float2 dir, out float2 e) { - float4 coord = float4(texcoord, -1.0, 1.0); - float3 t = float3(SMAA_RT_METRICS.xy, 1.0); - while (coord.z < float(SMAA_MAX_SEARCH_STEPS_DIAG - 1) && - coord.w > 0.9) { - coord.xyz = mad(t, float3(dir, 1.0), coord.xyz); - e = SMAASampleLevelZero(edgesTex, coord.xy).rg; - coord.w = dot(e, float2(0.5, 0.5)); - } - return coord.zw; -} - -float2 SMAASearchDiag2(SMAATexture2D(edgesTex), float2 texcoord, float2 dir, out float2 e) { - float4 coord = float4(texcoord, -1.0, 1.0); - coord.x += 0.25 * SMAA_RT_METRICS.x; // See @SearchDiag2Optimization - float3 t = float3(SMAA_RT_METRICS.xy, 1.0); - while (coord.z < float(SMAA_MAX_SEARCH_STEPS_DIAG - 1) && - coord.w > 0.9) { - coord.xyz = mad(t, float3(dir, 1.0), coord.xyz); - - // @SearchDiag2Optimization - // Fetch both edges at once using bilinear filtering: - e = SMAASampleLevelZero(edgesTex, coord.xy).rg; - e = SMAADecodeDiagBilinearAccess(e); - - // Non-optimized version: - // e.g = SMAASampleLevelZero(edgesTex, coord.xy).g; - // e.r = SMAASampleLevelZeroOffset(edgesTex, coord.xy, int2(1, 0)).r; - - coord.w = dot(e, float2(0.5, 0.5)); - } - return coord.zw; -} - -/** - * Similar to SMAAArea, this calculates the area corresponding to a certain - * diagonal distance and crossing edges 'e'. - */ -float2 SMAAAreaDiag(SMAATexture2D(areaTex), float2 dist, float2 e, float offset) { - float2 texcoord = mad(float2(SMAA_AREATEX_MAX_DISTANCE_DIAG, SMAA_AREATEX_MAX_DISTANCE_DIAG), e, dist); - - // We do a scale and bias for mapping to texel space: - texcoord = mad(SMAA_AREATEX_PIXEL_SIZE, texcoord, 0.5 * SMAA_AREATEX_PIXEL_SIZE); - - // Diagonal areas are on the second half of the texture: - texcoord.x += 0.5; - - // Move to proper place, according to the subpixel offset: - texcoord.y += SMAA_AREATEX_SUBTEX_SIZE * offset; - - // Do it! - return SMAA_AREATEX_SELECT(SMAASampleLevelZero(areaTex, texcoord)); -} - -/** - * This searches for diagonal patterns and returns the corresponding weights. - */ -float2 SMAACalculateDiagWeights(SMAATexture2D(edgesTex), SMAATexture2D(areaTex), float2 texcoord, float2 e, float4 subsampleIndices) { - float2 weights = float2(0.0, 0.0); - - // Search for the line ends: - float4 d; - float2 end; - if (e.r > 0.0) { - d.xz = SMAASearchDiag1(SMAATexturePass2D(edgesTex), texcoord, float2(-1.0, 1.0), end); - d.x += float(end.y > 0.9); - } else - d.xz = float2(0.0, 0.0); - d.yw = SMAASearchDiag1(SMAATexturePass2D(edgesTex), texcoord, float2(1.0, -1.0), end); - - SMAA_BRANCH - if (d.x + d.y > 2.0) { // d.x + d.y + 1 > 3 - // Fetch the crossing edges: - float4 coords = mad(float4(-d.x + 0.25, d.x, d.y, -d.y - 0.25), SMAA_RT_METRICS.xyxy, texcoord.xyxy); - float4 c; - c.xy = SMAASampleLevelZeroOffset(edgesTex, coords.xy, int2(-1, 0)).rg; - c.zw = SMAASampleLevelZeroOffset(edgesTex, coords.zw, int2( 1, 0)).rg; - c.yxwz = SMAADecodeDiagBilinearAccess(c.xyzw); - - // Non-optimized version: - // float4 coords = mad(float4(-d.x, d.x, d.y, -d.y), SMAA_RT_METRICS.xyxy, texcoord.xyxy); - // float4 c; - // c.x = SMAASampleLevelZeroOffset(edgesTex, coords.xy, int2(-1, 0)).g; - // c.y = SMAASampleLevelZeroOffset(edgesTex, coords.xy, int2( 0, 0)).r; - // c.z = SMAASampleLevelZeroOffset(edgesTex, coords.zw, int2( 1, 0)).g; - // c.w = SMAASampleLevelZeroOffset(edgesTex, coords.zw, int2( 1, -1)).r; - - // Merge crossing edges at each side into a single value: - float2 cc = mad(float2(2.0, 2.0), c.xz, c.yw); - - // Remove the crossing edge if we didn't found the end of the line: - SMAAMovc(bool2(step(0.9, d.zw)), cc, float2(0.0, 0.0)); - - // Fetch the areas for this line: - weights += SMAAAreaDiag(SMAATexturePass2D(areaTex), d.xy, cc, subsampleIndices.z); - } - - // Search for the line ends: - d.xz = SMAASearchDiag2(SMAATexturePass2D(edgesTex), texcoord, float2(-1.0, -1.0), end); - if (SMAASampleLevelZeroOffset(edgesTex, texcoord, int2(1, 0)).r > 0.0) { - d.yw = SMAASearchDiag2(SMAATexturePass2D(edgesTex), texcoord, float2(1.0, 1.0), end); - d.y += float(end.y > 0.9); - } else - d.yw = float2(0.0, 0.0); - - SMAA_BRANCH - if (d.x + d.y > 2.0) { // d.x + d.y + 1 > 3 - // Fetch the crossing edges: - float4 coords = mad(float4(-d.x, -d.x, d.y, d.y), SMAA_RT_METRICS.xyxy, texcoord.xyxy); - float4 c; - c.x = SMAASampleLevelZeroOffset(edgesTex, coords.xy, int2(-1, 0)).g; - c.y = SMAASampleLevelZeroOffset(edgesTex, coords.xy, int2( 0, -1)).r; - c.zw = SMAASampleLevelZeroOffset(edgesTex, coords.zw, int2( 1, 0)).gr; - float2 cc = mad(float2(2.0, 2.0), c.xz, c.yw); - - // Remove the crossing edge if we didn't found the end of the line: - SMAAMovc(bool2(step(0.9, d.zw)), cc, float2(0.0, 0.0)); - - // Fetch the areas for this line: - weights += SMAAAreaDiag(SMAATexturePass2D(areaTex), d.xy, cc, subsampleIndices.w).gr; - } - - return weights; -} -#endif - -//----------------------------------------------------------------------------- -// Horizontal/Vertical Search Functions - -/** - * This allows to determine how much length should we add in the last step - * of the searches. It takes the bilinearly interpolated edge (see - * @PSEUDO_GATHER4), and adds 0, 1 or 2, depending on which edges and - * crossing edges are active. - */ -float SMAASearchLength(SMAATexture2D(searchTex), float2 e, float offset) { - // The texture is flipped vertically, with left and right cases taking half - // of the space horizontally: - float2 scale = SMAA_SEARCHTEX_SIZE * float2(0.5, -1.0); - float2 bias = SMAA_SEARCHTEX_SIZE * float2(offset, 1.0); - - // Scale and bias to access texel centers: - scale += float2(-1.0, 1.0); - bias += float2( 0.5, -0.5); - - // Convert from pixel coordinates to texcoords: - // (We use SMAA_SEARCHTEX_PACKED_SIZE because the texture is cropped) - scale *= 1.0 / SMAA_SEARCHTEX_PACKED_SIZE; - bias *= 1.0 / SMAA_SEARCHTEX_PACKED_SIZE; - - // Lookup the search texture: - return SMAA_SEARCHTEX_SELECT(SMAASampleLevelZero(searchTex, mad(scale, e, bias))); -} - -/** - * Horizontal/vertical search functions for the 2nd pass. - */ -float SMAASearchXLeft(SMAATexture2D(edgesTex), SMAATexture2D(searchTex), float2 texcoord, float end) { - /** - * @PSEUDO_GATHER4 - * This texcoord has been offset by (-0.25, -0.125) in the vertex shader to - * sample between edge, thus fetching four edges in a row. - * Sampling with different offsets in each direction allows to disambiguate - * which edges are active from the four fetched ones. - */ - float2 e = float2(0.0, 1.0); - while (texcoord.x > end && - e.g > 0.8281 && // Is there some edge not activated? - e.r == 0.0) { // Or is there a crossing edge that breaks the line? - e = SMAASampleLevelZero(edgesTex, texcoord).rg; - texcoord = mad(-float2(2.0, 0.0), SMAA_RT_METRICS.xy, texcoord); - } - - float offset = mad(-(255.0 / 127.0), SMAASearchLength(SMAATexturePass2D(searchTex), e, 0.0), 3.25); - return mad(SMAA_RT_METRICS.x, offset, texcoord.x); - - // Non-optimized version: - // We correct the previous (-0.25, -0.125) offset we applied: - // texcoord.x += 0.25 * SMAA_RT_METRICS.x; - - // The searches are bias by 1, so adjust the coords accordingly: - // texcoord.x += SMAA_RT_METRICS.x; - - // Disambiguate the length added by the last step: - // texcoord.x += 2.0 * SMAA_RT_METRICS.x; // Undo last step - // texcoord.x -= SMAA_RT_METRICS.x * (255.0 / 127.0) * SMAASearchLength(SMAATexturePass2D(searchTex), e, 0.0); - // return mad(SMAA_RT_METRICS.x, offset, texcoord.x); -} - -float SMAASearchXRight(SMAATexture2D(edgesTex), SMAATexture2D(searchTex), float2 texcoord, float end) { - float2 e = float2(0.0, 1.0); - while (texcoord.x < end && - e.g > 0.8281 && // Is there some edge not activated? - e.r == 0.0) { // Or is there a crossing edge that breaks the line? - e = SMAASampleLevelZero(edgesTex, texcoord).rg; - texcoord = mad(float2(2.0, 0.0), SMAA_RT_METRICS.xy, texcoord); - } - float offset = mad(-(255.0 / 127.0), SMAASearchLength(SMAATexturePass2D(searchTex), e, 0.5), 3.25); - return mad(-SMAA_RT_METRICS.x, offset, texcoord.x); -} - -float SMAASearchYUp(SMAATexture2D(edgesTex), SMAATexture2D(searchTex), float2 texcoord, float end) { - float2 e = float2(1.0, 0.0); - while (texcoord.y > end && - e.r > 0.8281 && // Is there some edge not activated? - e.g == 0.0) { // Or is there a crossing edge that breaks the line? - e = SMAASampleLevelZero(edgesTex, texcoord).rg; - texcoord = mad(-float2(0.0, 2.0), SMAA_RT_METRICS.xy, texcoord); - } - float offset = mad(-(255.0 / 127.0), SMAASearchLength(SMAATexturePass2D(searchTex), e.gr, 0.0), 3.25); - return mad(SMAA_RT_METRICS.y, offset, texcoord.y); -} - -float SMAASearchYDown(SMAATexture2D(edgesTex), SMAATexture2D(searchTex), float2 texcoord, float end) { - float2 e = float2(1.0, 0.0); - while (texcoord.y < end && - e.r > 0.8281 && // Is there some edge not activated? - e.g == 0.0) { // Or is there a crossing edge that breaks the line? - e = SMAASampleLevelZero(edgesTex, texcoord).rg; - texcoord = mad(float2(0.0, 2.0), SMAA_RT_METRICS.xy, texcoord); - } - float offset = mad(-(255.0 / 127.0), SMAASearchLength(SMAATexturePass2D(searchTex), e.gr, 0.5), 3.25); - return mad(-SMAA_RT_METRICS.y, offset, texcoord.y); -} - -/** - * Ok, we have the distance and both crossing edges. So, what are the areas - * at each side of current edge? - */ -float2 SMAAArea(SMAATexture2D(areaTex), float2 dist, float e1, float e2, float offset) { - // Rounding prevents precision errors of bilinear filtering: - float2 texcoord = mad(float2(SMAA_AREATEX_MAX_DISTANCE, SMAA_AREATEX_MAX_DISTANCE), round(4.0 * float2(e1, e2)), dist); - - // We do a scale and bias for mapping to texel space: - texcoord = mad(SMAA_AREATEX_PIXEL_SIZE, texcoord, 0.5 * SMAA_AREATEX_PIXEL_SIZE); - - // Move to proper place, according to the subpixel offset: - texcoord.y = mad(SMAA_AREATEX_SUBTEX_SIZE, offset, texcoord.y); - - // Do it! - return SMAA_AREATEX_SELECT(SMAASampleLevelZero(areaTex, texcoord)); -} - -//----------------------------------------------------------------------------- -// Corner Detection Functions - -void SMAADetectHorizontalCornerPattern(SMAATexture2D(edgesTex), inout float2 weights, float4 texcoord, float2 d) { - #if !defined(SMAA_DISABLE_CORNER_DETECTION) - float2 leftRight = step(d.xy, d.yx); - float2 rounding = (1.0 - SMAA_CORNER_ROUNDING_NORM) * leftRight; - - rounding /= leftRight.x + leftRight.y; // Reduce blending for pixels in the center of a line. - - float2 factor = float2(1.0, 1.0); - factor.x -= rounding.x * SMAASampleLevelZeroOffset(edgesTex, texcoord.xy, int2(0, 1)).r; - factor.x -= rounding.y * SMAASampleLevelZeroOffset(edgesTex, texcoord.zw, int2(1, 1)).r; - factor.y -= rounding.x * SMAASampleLevelZeroOffset(edgesTex, texcoord.xy, int2(0, -2)).r; - factor.y -= rounding.y * SMAASampleLevelZeroOffset(edgesTex, texcoord.zw, int2(1, -2)).r; - - weights *= saturate(factor); - #endif -} - -void SMAADetectVerticalCornerPattern(SMAATexture2D(edgesTex), inout float2 weights, float4 texcoord, float2 d) { - #if !defined(SMAA_DISABLE_CORNER_DETECTION) - float2 leftRight = step(d.xy, d.yx); - float2 rounding = (1.0 - SMAA_CORNER_ROUNDING_NORM) * leftRight; - - rounding /= leftRight.x + leftRight.y; - - float2 factor = float2(1.0, 1.0); - factor.x -= rounding.x * SMAASampleLevelZeroOffset(edgesTex, texcoord.xy, int2( 1, 0)).g; - factor.x -= rounding.y * SMAASampleLevelZeroOffset(edgesTex, texcoord.zw, int2( 1, 1)).g; - factor.y -= rounding.x * SMAASampleLevelZeroOffset(edgesTex, texcoord.xy, int2(-2, 0)).g; - factor.y -= rounding.y * SMAASampleLevelZeroOffset(edgesTex, texcoord.zw, int2(-2, 1)).g; - - weights *= saturate(factor); - #endif -} - -//----------------------------------------------------------------------------- -// Blending Weight Calculation Pixel Shader (Second Pass) - -float4 SMAABlendingWeightCalculationPS(float2 texcoord, - float2 pixcoord, - float4 offset[3], - SMAATexture2D(edgesTex), - SMAATexture2D(areaTex), - SMAATexture2D(searchTex), - float4 subsampleIndices) { // Just pass zero for SMAA 1x, see @SUBSAMPLE_INDICES. - float4 weights = float4(0.0, 0.0, 0.0, 0.0); - - float2 e = SMAASample(edgesTex, texcoord).rg; - - SMAA_BRANCH - if (e.g > 0.0) { // Edge at north - #if !defined(SMAA_DISABLE_DIAG_DETECTION) - // Diagonals have both north and west edges, so searching for them in - // one of the boundaries is enough. - weights.rg = SMAACalculateDiagWeights(SMAATexturePass2D(edgesTex), SMAATexturePass2D(areaTex), texcoord, e, subsampleIndices); - - // We give priority to diagonals, so if we find a diagonal we skip - // horizontal/vertical processing. - SMAA_BRANCH - if (weights.r == -weights.g) { // weights.r + weights.g == 0.0 - #endif - - float2 d; - - // Find the distance to the left: - float3 coords; - coords.x = SMAASearchXLeft(SMAATexturePass2D(edgesTex), SMAATexturePass2D(searchTex), offset[0].xy, offset[2].x); - coords.y = offset[1].y; // offset[1].y = texcoord.y - 0.25 * SMAA_RT_METRICS.y (@CROSSING_OFFSET) - d.x = coords.x; - - // Now fetch the left crossing edges, two at a time using bilinear - // filtering. Sampling at -0.25 (see @CROSSING_OFFSET) enables to - // discern what value each edge has: - float e1 = SMAASampleLevelZero(edgesTex, coords.xy).r; - - // Find the distance to the right: - coords.z = SMAASearchXRight(SMAATexturePass2D(edgesTex), SMAATexturePass2D(searchTex), offset[0].zw, offset[2].y); - d.y = coords.z; - - // We want the distances to be in pixel units (doing this here allow to - // better interleave arithmetic and memory accesses): - d = abs(round(mad(SMAA_RT_METRICS.zz, d, -pixcoord.xx))); - - // SMAAArea below needs a sqrt, as the areas texture is compressed - // quadratically: - float2 sqrt_d = sqrt(d); - - // Fetch the right crossing edges: - float e2 = SMAASampleLevelZeroOffset(edgesTex, coords.zy, int2(1, 0)).r; - - // Ok, we know how this pattern looks like, now it is time for getting - // the actual area: - weights.rg = SMAAArea(SMAATexturePass2D(areaTex), sqrt_d, e1, e2, subsampleIndices.y); - - // Fix corners: - coords.y = texcoord.y; - SMAADetectHorizontalCornerPattern(SMAATexturePass2D(edgesTex), weights.rg, coords.xyzy, d); - - #if !defined(SMAA_DISABLE_DIAG_DETECTION) - } else - e.r = 0.0; // Skip vertical processing. - #endif - } - - SMAA_BRANCH - if (e.r > 0.0) { // Edge at west - float2 d; - - // Find the distance to the top: - float3 coords; - coords.y = SMAASearchYUp(SMAATexturePass2D(edgesTex), SMAATexturePass2D(searchTex), offset[1].xy, offset[2].z); - coords.x = offset[0].x; // offset[1].x = texcoord.x - 0.25 * SMAA_RT_METRICS.x; - d.x = coords.y; - - // Fetch the top crossing edges: - float e1 = SMAASampleLevelZero(edgesTex, coords.xy).g; - - // Find the distance to the bottom: - coords.z = SMAASearchYDown(SMAATexturePass2D(edgesTex), SMAATexturePass2D(searchTex), offset[1].zw, offset[2].w); - d.y = coords.z; - - // We want the distances to be in pixel units: - d = abs(round(mad(SMAA_RT_METRICS.ww, d, -pixcoord.yy))); - - // SMAAArea below needs a sqrt, as the areas texture is compressed - // quadratically: - float2 sqrt_d = sqrt(d); - - // Fetch the bottom crossing edges: - float e2 = SMAASampleLevelZeroOffset(edgesTex, coords.xz, int2(0, 1)).g; - - // Get the area for this direction: - weights.ba = SMAAArea(SMAATexturePass2D(areaTex), sqrt_d, e1, e2, subsampleIndices.x); - - // Fix corners: - coords.x = texcoord.x; - SMAADetectVerticalCornerPattern(SMAATexturePass2D(edgesTex), weights.ba, coords.xyxz, d); - } - - return weights; -} - -//----------------------------------------------------------------------------- -// Neighborhood Blending Pixel Shader (Third Pass) - -float4 SMAANeighborhoodBlendingPS(float2 texcoord, - float4 offset, - SMAATexture2D(colorTex), - SMAATexture2D(blendTex) - ) { - // Fetch the blending weights for current pixel: - float4 a; - a.x = SMAASample(blendTex, offset.xy).a; // Right - a.y = SMAASample(blendTex, offset.zw).g; // Top - a.wz = SMAASample(blendTex, texcoord).xz; // Bottom / Left - - // Is there any blending weight with a value greater than 0.0? - SMAA_BRANCH - if (dot(a, float4(1.0, 1.0, 1.0, 1.0)) < 1e-5) { - float4 color = SMAA_GET_COLOR(SMAASampleLevelZero(colorTex, texcoord)); - return color; - } else { - bool h = max(a.x, a.z) > max(a.y, a.w); // max(horizontal) > max(vertical) - - // Calculate the blending offsets: - float4 blendingOffset = float4(0.0, a.y, 0.0, a.w); - float2 blendingWeight = a.yw; - SMAAMovc(bool4(h, h, h, h), blendingOffset, float4(a.x, 0.0, a.z, 0.0)); - SMAAMovc(bool2(h, h), blendingWeight, a.xz); - blendingWeight /= dot(blendingWeight, float2(1.0, 1.0)); - - // Calculate the texture coordinates: - float4 blendingCoord = mad(blendingOffset, float4(SMAA_RT_METRICS.xy, -SMAA_RT_METRICS.xy), texcoord.xyxy); - - // We exploit bilinear filtering to mix current pixel with the chosen - // neighbor: - float4 color = blendingWeight.x * SMAA_GET_COLOR(SMAASampleLevelZero(colorTex, blendingCoord.xy)); - color += blendingWeight.y * SMAA_GET_COLOR(SMAASampleLevelZero(colorTex, blendingCoord.zw)); - return color; - } -} - -//----------------------------------------------------------------------------- -// Separate Multisamples Pixel Shader (Optional Pass) - -#ifdef SMAALoad -void SMAASeparatePS(float4 position, - float2 texcoord, - out float4 target0, - out float4 target1, - SMAATexture2DMS2(colorTexMS)) { - int2 pos = int2(position.xy); - target0 = SMAALoad(colorTexMS, pos, 0); - target1 = SMAALoad(colorTexMS, pos, 1); -} -#endif - -//----------------------------------------------------------------------------- -#endif // SMAA_INCLUDE_PS + +// lint: skip-file + + +/** + * Copyright (C) 2013 Jorge Jimenez (jorge@iryoku.com) + * Copyright (C) 2013 Jose I. Echevarria (joseignacioechevarria@gmail.com) + * Copyright (C) 2013 Belen Masia (bmasia@unizar.es) + * Copyright (C) 2013 Fernando Navarro (fernandn@microsoft.com) + * Copyright (C) 2013 Diego Gutierrez (diegog@unizar.es) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to + * do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. As clarification, there + * is no requirement that the copyright notice and permission be included in + * binary distributions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + + +/** + * _______ ___ ___ ___ ___ + * / || \/ | / \ / \ + * | (---- | \ / | / ^ \ / ^ \ + * \ \ | |\/| | / /_\ \ / /_\ \ + * ----) | | | | | / _____ \ / _____ \ + * |_______/ |__| |__| /__/ \__\ /__/ \__\ + * + * E N H A N C E D + * S U B P I X E L M O R P H O L O G I C A L A N T I A L I A S I N G + * + * http://www.iryoku.com/smaa/ + * + * Hi, welcome aboard! + * + * Here you'll find instructions to get the shader up and running as fast as + * possible. + * + * IMPORTANTE NOTICE: when updating, remember to update both this file and the + * precomputed textures! They may change from version to version. + * + * The shader has three passes, chained together as follows: + * + * |input|------------------ + * v | + * [ SMAA*EdgeDetection ] | + * v | + * |edgesTex| | + * v | + * [ SMAABlendingWeightCalculation ] | + * v | + * |blendTex| | + * v | + * [ SMAANeighborhoodBlending ] <------ + * v + * |output| + * + * Note that each [pass] has its own vertex and pixel shader. Remember to use + * oversized triangles instead of quads to avoid overshading along the + * diagonal. + * + * You've three edge detection methods to choose from: luma, color or depth. + * They represent different quality/performance and anti-aliasing/sharpness + * tradeoffs, so our recommendation is for you to choose the one that best + * suits your particular scenario: + * + * - Depth edge detection is usually the fastest but it may miss some edges. + * + * - Luma edge detection is usually more expensive than depth edge detection, + * but catches visible edges that depth edge detection can miss. + * + * - Color edge detection is usually the most expensive one but catches + * chroma-only edges. + * + * For quickstarters: just use luma edge detection. + * + * The general advice is to not rush the integration process and ensure each + * step is done correctly (don't try to integrate SMAA T2x with predicated edge + * detection from the start!). Ok then, let's go! + * + * 1. The first step is to create two RGBA temporal render targets for holding + * |edgesTex| and |blendTex|. + * + * In DX10 or DX11, you can use a RG render target for the edges texture. + * In the case of NVIDIA GPUs, using RG render targets seems to actually be + * slower. + * + * On the Xbox 360, you can use the same render target for resolving both + * |edgesTex| and |blendTex|, as they aren't needed simultaneously. + * + * 2. Both temporal render targets |edgesTex| and |blendTex| must be cleared + * each frame. Do not forget to clear the alpha channel! + * + * 3. The next step is loading the two supporting precalculated textures, + * 'areaTex' and 'searchTex'. You'll find them in the 'Textures' folder as + * C++ headers, and also as regular DDS files. They'll be needed for the + * 'SMAABlendingWeightCalculation' pass. + * + * If you use the C++ headers, be sure to load them in the format specified + * inside of them. + * + * You can also compress 'areaTex' and 'searchTex' using BC5 and BC4 + * respectively, if you have that option in your content processor pipeline. + * When compressing then, you get a non-perceptible quality decrease, and a + * marginal performance increase. + * + * 4. All samplers must be set to linear filtering and clamp. + * + * After you get the technique working, remember that 64-bit inputs have + * half-rate linear filtering on GCN. + * + * If SMAA is applied to 64-bit color buffers, switching to point filtering + * when accesing them will increase the performance. Search for + * 'SMAASamplePoint' to see which textures may benefit from point + * filtering, and where (which is basically the color input in the edge + * detection and resolve passes). + * + * 5. All texture reads and buffer writes must be non-sRGB, with the exception + * of the input read and the output write in + * 'SMAANeighborhoodBlending' (and only in this pass!). If sRGB reads in + * this last pass are not possible, the technique will work anyway, but + * will perform antialiasing in gamma space. + * + * IMPORTANT: for best results the input read for the color/luma edge + * detection should *NOT* be sRGB. + * + * 6. Before including SMAA.h you'll have to setup the render target metrics, + * the target and any optional configuration defines. Optionally you can + * use a preset. + * + * You have the following targets available: + * SMAA_HLSL_3 + * SMAA_HLSL_4 + * SMAA_HLSL_4_1 + * SMAA_GLSL_3 * + * SMAA_GLSL_4 * + * + * * (See SMAA_INCLUDE_VS and SMAA_INCLUDE_PS below). + * + * And four presets: + * SMAA_PRESET_LOW (%60 of the quality) + * SMAA_PRESET_MEDIUM (%80 of the quality) + * SMAA_PRESET_HIGH (%95 of the quality) + * SMAA_PRESET_ULTRA (%99 of the quality) + * + * For example: + * #define SMAA_RT_METRICS float4(1.0 / 1280.0, 1.0 / 720.0, 1280.0, 720.0) + * #define SMAA_HLSL_4 + * #define SMAA_PRESET_HIGH + * #include "SMAA.h" + * + * Note that SMAA_RT_METRICS doesn't need to be a macro, it can be a + * uniform variable. The code is designed to minimize the impact of not + * using a constant value, but it is still better to hardcode it. + * + * Depending on how you encoded 'areaTex' and 'searchTex', you may have to + * add (and customize) the following defines before including SMAA.h: + * #define SMAA_AREATEX_SELECT(sample) sample.rg + * #define SMAA_SEARCHTEX_SELECT(sample) sample.r + * + * If your engine is already using porting macros, you can define + * SMAA_CUSTOM_SL, and define the porting functions by yourself. + * + * 7. Then, you'll have to setup the passes as indicated in the scheme above. + * You can take a look into SMAA.fx, to see how we did it for our demo. + * Checkout the function wrappers, you may want to copy-paste them! + * + * 8. It's recommended to validate the produced |edgesTex| and |blendTex|. + * You can use a screenshot from your engine to compare the |edgesTex| + * and |blendTex| produced inside of the engine with the results obtained + * with the reference demo. + * + * 9. After you get the last pass to work, it's time to optimize. You'll have + * to initialize a stencil buffer in the first pass (discard is already in + * the code), then mask execution by using it the second pass. The last + * pass should be executed in all pixels. + * + * + * After this point you can choose to enable predicated thresholding, + * temporal supersampling and motion blur integration: + * + * a) If you want to use predicated thresholding, take a look into + * SMAA_PREDICATION; you'll need to pass an extra texture in the edge + * detection pass. + * + * b) If you want to enable temporal supersampling (SMAA T2x): + * + * 1. The first step is to render using subpixel jitters. I won't go into + * detail, but it's as simple as moving each vertex position in the + * vertex shader, you can check how we do it in our DX10 demo. + * + * 2. Then, you must setup the temporal resolve. You may want to take a look + * into SMAAResolve for resolving 2x modes. After you get it working, you'll + * probably see ghosting everywhere. But fear not, you can enable the + * CryENGINE temporal reprojection by setting the SMAA_REPROJECTION macro. + * Check out SMAA_DECODE_VELOCITY if your velocity buffer is encoded. + * + * 3. The next step is to apply SMAA to each subpixel jittered frame, just as + * done for 1x. + * + * 4. At this point you should already have something usable, but for best + * results the proper area textures must be set depending on current jitter. + * For this, the parameter 'subsampleIndices' of + * 'SMAABlendingWeightCalculationPS' must be set as follows, for our T2x + * mode: + * + * @SUBSAMPLE_INDICES + * + * | S# | Camera Jitter | subsampleIndices | + * +----+------------------+---------------------+ + * | 0 | ( 0.25, -0.25) | float4(1, 1, 1, 0) | + * | 1 | (-0.25, 0.25) | float4(2, 2, 2, 0) | + * + * These jitter positions assume a bottom-to-top y axis. S# stands for the + * sample number. + * + * More information about temporal supersampling here: + * http://iryoku.com/aacourse/downloads/13-Anti-Aliasing-Methods-in-CryENGINE-3.pdf + * + * c) If you want to enable spatial multisampling (SMAA S2x): + * + * 1. The scene must be rendered using MSAA 2x. The MSAA 2x buffer must be + * created with: + * - DX10: see below (*) + * - DX10.1: D3D10_STANDARD_MULTISAMPLE_PATTERN or + * - DX11: D3D11_STANDARD_MULTISAMPLE_PATTERN + * + * This allows to ensure that the subsample order matches the table in + * @SUBSAMPLE_INDICES. + * + * (*) In the case of DX10, we refer the reader to: + * - SMAA::detectMSAAOrder and + * - SMAA::msaaReorder + * + * These functions allow to match the standard multisample patterns by + * detecting the subsample order for a specific GPU, and reordering + * them appropriately. + * + * 2. A shader must be run to output each subsample into a separate buffer + * (DX10 is required). You can use SMAASeparate for this purpose, or just do + * it in an existing pass (for example, in the tone mapping pass, which has + * the advantage of feeding tone mapped subsamples to SMAA, which will yield + * better results). + * + * 3. The full SMAA 1x pipeline must be run for each separated buffer, storing + * the results in the final buffer. The second run should alpha blend with + * the existing final buffer using a blending factor of 0.5. + * 'subsampleIndices' must be adjusted as in the SMAA T2x case (see point + * b). + * + * d) If you want to enable temporal supersampling on top of SMAA S2x + * (which actually is SMAA 4x): + * + * 1. SMAA 4x consists on temporally jittering SMAA S2x, so the first step is + * to calculate SMAA S2x for current frame. In this case, 'subsampleIndices' + * must be set as follows: + * + * | F# | S# | Camera Jitter | Net Jitter | subsampleIndices | + * +----+----+--------------------+-------------------+----------------------+ + * | 0 | 0 | ( 0.125, 0.125) | ( 0.375, -0.125) | float4(5, 3, 1, 3) | + * | 0 | 1 | ( 0.125, 0.125) | (-0.125, 0.375) | float4(4, 6, 2, 3) | + * +----+----+--------------------+-------------------+----------------------+ + * | 1 | 2 | (-0.125, -0.125) | ( 0.125, -0.375) | float4(3, 5, 1, 4) | + * | 1 | 3 | (-0.125, -0.125) | (-0.375, 0.125) | float4(6, 4, 2, 4) | + * + * These jitter positions assume a bottom-to-top y axis. F# stands for the + * frame number. S# stands for the sample number. + * + * 2. After calculating SMAA S2x for current frame (with the new subsample + * indices), previous frame must be reprojected as in SMAA T2x mode (see + * point b). + * + * e) If motion blur is used, you may want to do the edge detection pass + * together with motion blur. This has two advantages: + * + * 1. Pixels under heavy motion can be omitted from the edge detection process. + * For these pixels we can just store "no edge", as motion blur will take + * care of them. + * 2. The center pixel tap is reused. + * + * Note that in this case depth testing should be used instead of stenciling, + * as we have to write all the pixels in the motion blur pass. + * + * That's it! + */ + +//----------------------------------------------------------------------------- +// SMAA Presets + +/** + * Note that if you use one of these presets, the following configuration + * macros will be ignored if set in the "Configurable Defines" section. + */ + +#if defined(SMAA_PRESET_LOW) +#define SMAA_THRESHOLD 0.15 +#define SMAA_MAX_SEARCH_STEPS 4 +#define SMAA_DISABLE_DIAG_DETECTION +#define SMAA_DISABLE_CORNER_DETECTION +#elif defined(SMAA_PRESET_MEDIUM) +#define SMAA_THRESHOLD 0.1 +#define SMAA_MAX_SEARCH_STEPS 8 +#define SMAA_DISABLE_DIAG_DETECTION +#define SMAA_DISABLE_CORNER_DETECTION +#elif defined(SMAA_PRESET_HIGH) +#define SMAA_THRESHOLD 0.1 +#define SMAA_MAX_SEARCH_STEPS 16 +#define SMAA_MAX_SEARCH_STEPS_DIAG 8 +#define SMAA_CORNER_ROUNDING 25 +#elif defined(SMAA_PRESET_ULTRA) +#define SMAA_THRESHOLD 0.05 +#define SMAA_MAX_SEARCH_STEPS 32 +#define SMAA_MAX_SEARCH_STEPS_DIAG 16 +#define SMAA_CORNER_ROUNDING 25 +#endif + +//----------------------------------------------------------------------------- +// Configurable Defines + +/** + * SMAA_THRESHOLD specifies the threshold or sensitivity to edges. + * Lowering this value you will be able to detect more edges at the expense of + * performance. + * + * Range: [0, 0.5] + * 0.1 is a reasonable value, and allows to catch most visible edges. + * 0.05 is a rather overkill value, that allows to catch 'em all. + * + * If temporal supersampling is used, 0.2 could be a reasonable value, as low + * contrast edges are properly filtered by just 2x. + */ +#ifndef SMAA_THRESHOLD +#define SMAA_THRESHOLD 0.1 +#endif + +/** + * SMAA_DEPTH_THRESHOLD specifies the threshold for depth edge detection. + * + * Range: depends on the depth range of the scene. + */ +#ifndef SMAA_DEPTH_THRESHOLD +#define SMAA_DEPTH_THRESHOLD (0.1 * SMAA_THRESHOLD) +#endif + +/** + * SMAA_MAX_SEARCH_STEPS specifies the maximum steps performed in the + * horizontal/vertical pattern searches, at each side of the pixel. + * + * In number of pixels, it's actually the double. So the maximum line length + * perfectly handled by, for example 16, is 64 (by perfectly, we meant that + * longer lines won't look as good, but still antialiased). + * + * Range: [0, 112] + */ +#ifndef SMAA_MAX_SEARCH_STEPS +#define SMAA_MAX_SEARCH_STEPS 16 +#endif + +/** + * SMAA_MAX_SEARCH_STEPS_DIAG specifies the maximum steps performed in the + * diagonal pattern searches, at each side of the pixel. In this case we jump + * one pixel at time, instead of two. + * + * Range: [0, 20] + * + * On high-end machines it is cheap (between a 0.8x and 0.9x slower for 16 + * steps), but it can have a significant impact on older machines. + * + * Define SMAA_DISABLE_DIAG_DETECTION to disable diagonal processing. + */ +#ifndef SMAA_MAX_SEARCH_STEPS_DIAG +#define SMAA_MAX_SEARCH_STEPS_DIAG 8 +#endif + +/** + * SMAA_CORNER_ROUNDING specifies how much sharp corners will be rounded. + * + * Range: [0, 100] + * + * Define SMAA_DISABLE_CORNER_DETECTION to disable corner processing. + */ +#ifndef SMAA_CORNER_ROUNDING +#define SMAA_CORNER_ROUNDING 25 +#endif + +/** + * If there is an neighbor edge that has SMAA_LOCAL_CONTRAST_FACTOR times + * bigger contrast than current edge, current edge will be discarded. + * + * This allows to eliminate spurious crossing edges, and is based on the fact + * that, if there is too much contrast in a direction, that will hide + * perceptually contrast in the other neighbors. + */ +#ifndef SMAA_LOCAL_CONTRAST_ADAPTATION_FACTOR +#define SMAA_LOCAL_CONTRAST_ADAPTATION_FACTOR 2.0 +#endif + +/** + * Predicated thresholding allows to better preserve texture details and to + * improve performance, by decreasing the number of detected edges using an + * additional buffer like the light accumulation buffer, object ids or even the + * depth buffer (the depth buffer usage may be limited to indoor or short range + * scenes). + * + * It locally decreases the luma or color threshold if an edge is found in an + * additional buffer (so the global threshold can be higher). + * + * This method was developed by Playstation EDGE MLAA team, and used in + * Killzone 3, by using the light accumulation buffer. More information here: + * http://iryoku.com/aacourse/downloads/06-MLAA-on-PS3.pptx + */ +#ifndef SMAA_PREDICATION +#define SMAA_PREDICATION 0 +#endif + +/** + * Threshold to be used in the additional predication buffer. + * + * Range: depends on the input, so you'll have to find the magic number that + * works for you. + */ +#ifndef SMAA_PREDICATION_THRESHOLD +#define SMAA_PREDICATION_THRESHOLD 0.01 +#endif + +/** + * How much to scale the global threshold used for luma or color edge + * detection when using predication. + * + * Range: [1, 5] + */ +#ifndef SMAA_PREDICATION_SCALE +#define SMAA_PREDICATION_SCALE 2.0 +#endif + +/** + * How much to locally decrease the threshold. + * + * Range: [0, 1] + */ +#ifndef SMAA_PREDICATION_STRENGTH +#define SMAA_PREDICATION_STRENGTH 0.4 +#endif + +/** + * Temporal reprojection allows to remove ghosting artifacts when using + * temporal supersampling. We use the CryEngine 3 method which also introduces + * velocity weighting. This feature is of extreme importance for totally + * removing ghosting. More information here: + * http://iryoku.com/aacourse/downloads/13-Anti-Aliasing-Methods-in-CryENGINE-3.pdf + * + * Note that you'll need to setup a velocity buffer for enabling reprojection. + * For static geometry, saving the previous depth buffer is a viable + * alternative. + */ +#ifndef SMAA_REPROJECTION +#define SMAA_REPROJECTION 0 +#endif + +/** + * SMAA_REPROJECTION_WEIGHT_SCALE controls the velocity weighting. It allows to + * remove ghosting trails behind the moving object, which are not removed by + * just using reprojection. Using low values will exhibit ghosting, while using + * high values will disable temporal supersampling under motion. + * + * Behind the scenes, velocity weighting removes temporal supersampling when + * the velocity of the subsamples differs (meaning they are different objects). + * + * Range: [0, 80] + */ +#ifndef SMAA_REPROJECTION_WEIGHT_SCALE +#define SMAA_REPROJECTION_WEIGHT_SCALE 30.0 +#endif + +/** + * On some compilers, discard cannot be used in vertex shaders. Thus, they need + * to be compiled separately. + */ +#ifndef SMAA_INCLUDE_VS +#define SMAA_INCLUDE_VS 1 +#endif +#ifndef SMAA_INCLUDE_PS +#define SMAA_INCLUDE_PS 1 +#endif + +//----------------------------------------------------------------------------- +// Texture Access Defines + +#ifndef SMAA_AREATEX_SELECT +#if defined(SMAA_HLSL_3) +#define SMAA_AREATEX_SELECT(sample) sample.ra +#else +#define SMAA_AREATEX_SELECT(sample) sample.rg +#endif +#endif + +#ifndef SMAA_SEARCHTEX_SELECT +#define SMAA_SEARCHTEX_SELECT(sample) sample.r +#endif + +#ifndef SMAA_DECODE_VELOCITY +#define SMAA_DECODE_VELOCITY(sample) sample.rg +#endif + +//----------------------------------------------------------------------------- +// Non-Configurable Defines + +#define SMAA_AREATEX_MAX_DISTANCE 16 +#define SMAA_AREATEX_MAX_DISTANCE_DIAG 20 +#define SMAA_AREATEX_PIXEL_SIZE (1.0 / float2(160.0, 560.0)) +#define SMAA_AREATEX_SUBTEX_SIZE (1.0 / 7.0) +#define SMAA_SEARCHTEX_SIZE float2(66.0, 33.0) +#define SMAA_SEARCHTEX_PACKED_SIZE float2(64.0, 16.0) +#define SMAA_CORNER_ROUNDING_NORM (float(SMAA_CORNER_ROUNDING) / 100.0) + +//----------------------------------------------------------------------------- +// Porting Functions + +#if defined(SMAA_HLSL_3) +#define SMAATexture2D(tex) sampler2D tex +#define SMAATexturePass2D(tex) tex +#define SMAASampleLevelZero(tex, coord) tex2Dlod(tex, float4(coord, 0.0, 0.0)) +#define SMAASampleLevelZeroPoint(tex, coord) tex2Dlod(tex, float4(coord, 0.0, 0.0)) +#define SMAASampleLevelZeroOffset(tex, coord, offset) tex2Dlod(tex, float4(coord + offset * SMAA_RT_METRICS.xy, 0.0, 0.0)) +#define SMAASample(tex, coord) tex2D(tex, coord) +#define SMAASamplePoint(tex, coord) tex2D(tex, coord) +#define SMAASampleOffset(tex, coord, offset) tex2D(tex, coord + offset * SMAA_RT_METRICS.xy) +#define SMAA_FLATTEN [flatten] +#define SMAA_BRANCH [branch] +#endif +#if defined(SMAA_HLSL_4) || defined(SMAA_HLSL_4_1) +SamplerState LinearSampler { Filter = MIN_MAG_LINEAR_MIP_POINT; AddressU = Clamp; AddressV = Clamp; }; +SamplerState PointSampler { Filter = MIN_MAG_MIP_POINT; AddressU = Clamp; AddressV = Clamp; }; +#define SMAATexture2D(tex) Texture2D tex +#define SMAATexturePass2D(tex) tex +#define SMAASampleLevelZero(tex, coord) tex.SampleLevel(LinearSampler, coord, 0) +#define SMAASampleLevelZeroPoint(tex, coord) tex.SampleLevel(PointSampler, coord, 0) +#define SMAASampleLevelZeroOffset(tex, coord, offset) tex.SampleLevel(LinearSampler, coord, 0, offset) +#define SMAASample(tex, coord) tex.Sample(LinearSampler, coord) +#define SMAASamplePoint(tex, coord) tex.Sample(PointSampler, coord) +#define SMAASampleOffset(tex, coord, offset) tex.Sample(LinearSampler, coord, offset) +#define SMAA_FLATTEN [flatten] +#define SMAA_BRANCH [branch] +#define SMAATexture2DMS2(tex) Texture2DMS tex +#define SMAALoad(tex, pos, sample) tex.Load(pos, sample) +#if defined(SMAA_HLSL_4_1) +#define SMAAGather(tex, coord) tex.Gather(LinearSampler, coord, 0) +#endif +#endif +#if defined(SMAA_GLSL_3) || defined(SMAA_GLSL_4) +#define SMAATexture2D(tex) sampler2D tex +#define SMAATexturePass2D(tex) tex +#define SMAASampleLevelZero(tex, coord) textureLod(tex, coord, 0.0) +#define SMAASampleLevelZeroPoint(tex, coord) textureLod(tex, coord, 0.0) +#define SMAASampleLevelZeroOffset(tex, coord, offset) textureLodOffset(tex, coord, 0.0, offset) +#define SMAASample(tex, coord) textureLod(tex, coord, 0) +#define SMAASamplePoint(tex, coord) textureLod(tex, coord, 0) +#define SMAASampleOffset(tex, coord, offset) texture(tex, coord, offset) +#define SMAA_FLATTEN +#define SMAA_BRANCH +#define lerp(a, b, t) mix(a, b, t) +#define saturate(a) clamp(a, 0.0, 1.0) +#if defined(SMAA_GLSL_4) +#define mad(a, b, c) fma(a, b, c) +#define SMAAGather(tex, coord) textureGather(tex, coord) +#else +#define mad(a, b, c) (a * b + c) +#endif +#define float2 vec2 +#define float3 vec3 +#define float4 vec4 +#define int2 ivec2 +#define int3 ivec3 +#define int4 ivec4 +#define bool2 bvec2 +#define bool3 bvec3 +#define bool4 bvec4 +#endif + +#if !defined(SMAA_HLSL_3) && !defined(SMAA_HLSL_4) && !defined(SMAA_HLSL_4_1) && !defined(SMAA_GLSL_3) && !defined(SMAA_GLSL_4) && !defined(SMAA_CUSTOM_SL) +#error you must define the shading language: SMAA_HLSL_*, SMAA_GLSL_* or SMAA_CUSTOM_SL +#endif + +//----------------------------------------------------------------------------- +// Misc functions + +/** + * Gathers current pixel, and the top-left neighbors. + */ +float3 SMAAGatherNeighbours(float2 texcoord, + float4 offset[3], + SMAATexture2D(tex)) { + #ifdef SMAAGather + return SMAAGather(tex, texcoord + SMAA_RT_METRICS.xy * float2(-0.5, -0.5)).grb; + #else + float P = SMAASamplePoint(tex, texcoord).r; + float Pleft = SMAASamplePoint(tex, offset[0].xy).r; + float Ptop = SMAASamplePoint(tex, offset[0].zw).r; + return float3(P, Pleft, Ptop); + #endif +} + +/** + * Adjusts the threshold by means of predication. + */ +float2 SMAACalculatePredicatedThreshold(float2 texcoord, + float4 offset[3], + SMAATexture2D(predicationTex)) { + float3 neighbours = SMAAGatherNeighbours(texcoord, offset, SMAATexturePass2D(predicationTex)); + float2 delta = abs(neighbours.xx - neighbours.yz); + float2 edges = step(SMAA_PREDICATION_THRESHOLD, delta); + return SMAA_PREDICATION_SCALE * SMAA_THRESHOLD * (1.0 - SMAA_PREDICATION_STRENGTH * edges); +} + +/** + * Conditional move: + */ +void SMAAMovc(bool2 cond, inout float2 variable, float2 value) { + SMAA_FLATTEN if (cond.x) variable.x = value.x; + SMAA_FLATTEN if (cond.y) variable.y = value.y; +} + +void SMAAMovc(bool4 cond, inout float4 variable, float4 value) { + SMAAMovc(cond.xy, variable.xy, value.xy); + SMAAMovc(cond.zw, variable.zw, value.zw); +} + + +#if SMAA_INCLUDE_VS +//----------------------------------------------------------------------------- +// Vertex Shaders + +/** + * Edge Detection Vertex Shader + */ +void SMAAEdgeDetectionVS(float2 texcoord, + out float4 offset[3]) { + offset[0] = mad(SMAA_RT_METRICS.xyxy, float4(-1.0, 0.0, 0.0, -1.0), texcoord.xyxy); + offset[1] = mad(SMAA_RT_METRICS.xyxy, float4( 1.0, 0.0, 0.0, 1.0), texcoord.xyxy); + offset[2] = mad(SMAA_RT_METRICS.xyxy, float4(-2.0, 0.0, 0.0, -2.0), texcoord.xyxy); +} + +/** + * Blend Weight Calculation Vertex Shader + */ +void SMAABlendingWeightCalculationVS(float2 texcoord, + out float2 pixcoord, + out float4 offset[3]) { + pixcoord = texcoord * SMAA_RT_METRICS.zw; + + // We will use these offsets for the searches later on (see @PSEUDO_GATHER4): + offset[0] = mad(SMAA_RT_METRICS.xyxy, float4(-0.25, -0.125, 1.25, -0.125), texcoord.xyxy); + offset[1] = mad(SMAA_RT_METRICS.xyxy, float4(-0.125, -0.25, -0.125, 1.25), texcoord.xyxy); + + // And these for the searches, they indicate the ends of the loops: + offset[2] = mad(SMAA_RT_METRICS.xxyy, + float4(-2.0, 2.0, -2.0, 2.0) * float(SMAA_MAX_SEARCH_STEPS), + float4(offset[0].xz, offset[1].yw)); +} + +/** + * Neighborhood Blending Vertex Shader + */ +void SMAANeighborhoodBlendingVS(float2 texcoord, + out float4 offset) { + offset = mad(SMAA_RT_METRICS.xyxy, float4( 1.0, 0.0, 0.0, 1.0), texcoord.xyxy); +} +#endif // SMAA_INCLUDE_VS + +#if SMAA_INCLUDE_PS +//----------------------------------------------------------------------------- +// Edge Detection Pixel Shaders (First Pass) + +/** + * Luma Edge Detection + * + * IMPORTANT NOTICE: luma edge detection requires gamma-corrected colors, and + * thus 'colorTex' should be a non-sRGB texture. + */ +float2 SMAALumaEdgeDetectionPS(float2 texcoord, + float4 offset[3], + SMAATexture2D(colorTex) + #if SMAA_PREDICATION + , SMAATexture2D(predicationTex) + #endif + ) { + // Calculate the threshold: + #if SMAA_PREDICATION + float2 threshold = SMAACalculatePredicatedThreshold(texcoord, offset, SMAATexturePass2D(predicationTex)); + #else + float2 threshold = float2(SMAA_THRESHOLD, SMAA_THRESHOLD); + #endif + + // Calculate lumas: + float3 weights = float3(0.2126, 0.7152, 0.0722); + float L = dot(SMAASamplePoint(colorTex, texcoord).rgb, weights); + + float Lleft = dot(SMAASamplePoint(colorTex, offset[0].xy).rgb, weights); + float Ltop = dot(SMAASamplePoint(colorTex, offset[0].zw).rgb, weights); + + // We do the usual threshold: + float4 delta; + delta.xy = abs(L - float2(Lleft, Ltop)); + float2 edges = step(threshold, delta.xy); + + // Then discard if there is no edge: + if (dot(edges, float2(1.0, 1.0)) == 0.0) + discard; + + // Calculate right and bottom deltas: + float Lright = dot(SMAASamplePoint(colorTex, offset[1].xy).rgb, weights); + float Lbottom = dot(SMAASamplePoint(colorTex, offset[1].zw).rgb, weights); + delta.zw = abs(L - float2(Lright, Lbottom)); + + // Calculate the maximum delta in the direct neighborhood: + float2 maxDelta = max(delta.xy, delta.zw); + + // Calculate left-left and top-top deltas: + float Lleftleft = dot(SMAASamplePoint(colorTex, offset[2].xy).rgb, weights); + float Ltoptop = dot(SMAASamplePoint(colorTex, offset[2].zw).rgb, weights); + delta.zw = abs(float2(Lleft, Ltop) - float2(Lleftleft, Ltoptop)); + + // Calculate the final maximum delta: + maxDelta = max(maxDelta.xy, delta.zw); + float finalDelta = max(maxDelta.x, maxDelta.y); + + // Local contrast adaptation: + edges.xy *= step(finalDelta, SMAA_LOCAL_CONTRAST_ADAPTATION_FACTOR * delta.xy); + + return edges; +} + +/** + * Color Edge Detection + * + * IMPORTANT NOTICE: color edge detection requires gamma-corrected colors, and + * thus 'colorTex' should be a non-sRGB texture. + */ +float2 SMAAColorEdgeDetectionPS(float2 texcoord, + float4 offset[3], + SMAATexture2D(colorTex) + #if SMAA_PREDICATION + , SMAATexture2D(predicationTex) + #endif + ) { + // Calculate the threshold: + #if SMAA_PREDICATION + float2 threshold = SMAACalculatePredicatedThreshold(texcoord, offset, predicationTex); + #else + float2 threshold = float2(SMAA_THRESHOLD, SMAA_THRESHOLD); + #endif + + // Calculate color deltas: + float4 delta; + float3 C = SMAA_GET_COLOR(SMAASamplePoint(colorTex, texcoord).rgb); + + float3 Cleft = SMAA_GET_COLOR(SMAASamplePoint(colorTex, offset[0].xy).rgb); + float3 t = abs(C - Cleft); + delta.x = max(max(t.r, t.g), t.b); + + float3 Ctop = SMAA_GET_COLOR(SMAASamplePoint(colorTex, offset[0].zw).rgb); + t = abs(C - Ctop); + delta.y = max(max(t.r, t.g), t.b); + + // We do the usual threshold: + float2 edges = step(threshold, delta.xy); + + // Then discard if there is no edge: + if (dot(edges, float2(1.0, 1.0)) == 0.0) + discard; + + // Calculate right and bottom deltas: + float3 Cright = SMAA_GET_COLOR(SMAASamplePoint(colorTex, offset[1].xy).rgb); + t = abs(C - Cright); + delta.z = max(max(t.r, t.g), t.b); + + float3 Cbottom = SMAA_GET_COLOR(SMAASamplePoint(colorTex, offset[1].zw).rgb); + t = abs(C - Cbottom); + delta.w = max(max(t.r, t.g), t.b); + + // Calculate the maximum delta in the direct neighborhood: + float2 maxDelta = max(delta.xy, delta.zw); + + // Calculate left-left and top-top deltas: + float3 Cleftleft = SMAA_GET_COLOR(SMAASamplePoint(colorTex, offset[2].xy).rgb); + t = abs(C - Cleftleft); + delta.z = max(max(t.r, t.g), t.b); + + float3 Ctoptop = SMAA_GET_COLOR(SMAASamplePoint(colorTex, offset[2].zw).rgb); + t = abs(C - Ctoptop); + delta.w = max(max(t.r, t.g), t.b); + + // Calculate the final maximum delta: + maxDelta = max(maxDelta.xy, delta.zw); + float finalDelta = max(maxDelta.x, maxDelta.y); + + // Local contrast adaptation: + edges.xy *= step(finalDelta, SMAA_LOCAL_CONTRAST_ADAPTATION_FACTOR * delta.xy); + + return edges; +} + +/** + * Depth Edge Detection + */ +float2 SMAADepthEdgeDetectionPS(float2 texcoord, + float4 offset[3], + SMAATexture2D(depthTex)) { + float3 neighbours = SMAAGatherNeighbours(texcoord, offset, SMAATexturePass2D(depthTex)); + float2 delta = abs(neighbours.xx - float2(neighbours.y, neighbours.z)); + float2 edges = step(SMAA_DEPTH_THRESHOLD, delta); + + if (dot(edges, float2(1.0, 1.0)) == 0.0) + discard; + + return edges; +} + +//----------------------------------------------------------------------------- +// Diagonal Search Functions + +#if !defined(SMAA_DISABLE_DIAG_DETECTION) + +/** + * Allows to decode two binary values from a bilinear-filtered access. + */ +float2 SMAADecodeDiagBilinearAccess(float2 e) { + // Bilinear access for fetching 'e' have a 0.25 offset, and we are + // interested in the R and G edges: + // + // +---G---+-------+ + // | x o R x | + // +-------+-------+ + // + // Then, if one of these edge is enabled: + // Red: (0.75 * X + 0.25 * 1) => 0.25 or 1.0 + // Green: (0.75 * 1 + 0.25 * X) => 0.75 or 1.0 + // + // This function will unpack the values (mad + mul + round): + // wolframalpha.com: round(x * abs(5 * x - 5 * 0.75)) plot 0 to 1 + e.r = e.r * abs(5.0 * e.r - 5.0 * 0.75); + return round(e); +} + +float4 SMAADecodeDiagBilinearAccess(float4 e) { + e.rb = e.rb * abs(5.0 * e.rb - 5.0 * 0.75); + return round(e); +} + +/** + * These functions allows to perform diagonal pattern searches. + */ +float2 SMAASearchDiag1(SMAATexture2D(edgesTex), float2 texcoord, float2 dir, out float2 e) { + float4 coord = float4(texcoord, -1.0, 1.0); + float3 t = float3(SMAA_RT_METRICS.xy, 1.0); + while (coord.z < float(SMAA_MAX_SEARCH_STEPS_DIAG - 1) && + coord.w > 0.9) { + coord.xyz = mad(t, float3(dir, 1.0), coord.xyz); + e = SMAASampleLevelZero(edgesTex, coord.xy).rg; + coord.w = dot(e, float2(0.5, 0.5)); + } + return coord.zw; +} + +float2 SMAASearchDiag2(SMAATexture2D(edgesTex), float2 texcoord, float2 dir, out float2 e) { + float4 coord = float4(texcoord, -1.0, 1.0); + coord.x += 0.25 * SMAA_RT_METRICS.x; // See @SearchDiag2Optimization + float3 t = float3(SMAA_RT_METRICS.xy, 1.0); + while (coord.z < float(SMAA_MAX_SEARCH_STEPS_DIAG - 1) && + coord.w > 0.9) { + coord.xyz = mad(t, float3(dir, 1.0), coord.xyz); + + // @SearchDiag2Optimization + // Fetch both edges at once using bilinear filtering: + e = SMAASampleLevelZero(edgesTex, coord.xy).rg; + e = SMAADecodeDiagBilinearAccess(e); + + // Non-optimized version: + // e.g = SMAASampleLevelZero(edgesTex, coord.xy).g; + // e.r = SMAASampleLevelZeroOffset(edgesTex, coord.xy, int2(1, 0)).r; + + coord.w = dot(e, float2(0.5, 0.5)); + } + return coord.zw; +} + +/** + * Similar to SMAAArea, this calculates the area corresponding to a certain + * diagonal distance and crossing edges 'e'. + */ +float2 SMAAAreaDiag(SMAATexture2D(areaTex), float2 dist, float2 e, float offset) { + float2 texcoord = mad(float2(SMAA_AREATEX_MAX_DISTANCE_DIAG, SMAA_AREATEX_MAX_DISTANCE_DIAG), e, dist); + + // We do a scale and bias for mapping to texel space: + texcoord = mad(SMAA_AREATEX_PIXEL_SIZE, texcoord, 0.5 * SMAA_AREATEX_PIXEL_SIZE); + + // Diagonal areas are on the second half of the texture: + texcoord.x += 0.5; + + // Move to proper place, according to the subpixel offset: + texcoord.y += SMAA_AREATEX_SUBTEX_SIZE * offset; + + // Do it! + return SMAA_AREATEX_SELECT(SMAASampleLevelZero(areaTex, texcoord)); +} + +/** + * This searches for diagonal patterns and returns the corresponding weights. + */ +float2 SMAACalculateDiagWeights(SMAATexture2D(edgesTex), SMAATexture2D(areaTex), float2 texcoord, float2 e, float4 subsampleIndices) { + float2 weights = float2(0.0, 0.0); + + // Search for the line ends: + float4 d; + float2 end; + if (e.r > 0.0) { + d.xz = SMAASearchDiag1(SMAATexturePass2D(edgesTex), texcoord, float2(-1.0, 1.0), end); + d.x += float(end.y > 0.9); + } else + d.xz = float2(0.0, 0.0); + d.yw = SMAASearchDiag1(SMAATexturePass2D(edgesTex), texcoord, float2(1.0, -1.0), end); + + SMAA_BRANCH + if (d.x + d.y > 2.0) { // d.x + d.y + 1 > 3 + // Fetch the crossing edges: + float4 coords = mad(float4(-d.x + 0.25, d.x, d.y, -d.y - 0.25), SMAA_RT_METRICS.xyxy, texcoord.xyxy); + float4 c; + c.xy = SMAASampleLevelZeroOffset(edgesTex, coords.xy, int2(-1, 0)).rg; + c.zw = SMAASampleLevelZeroOffset(edgesTex, coords.zw, int2( 1, 0)).rg; + c.yxwz = SMAADecodeDiagBilinearAccess(c.xyzw); + + // Non-optimized version: + // float4 coords = mad(float4(-d.x, d.x, d.y, -d.y), SMAA_RT_METRICS.xyxy, texcoord.xyxy); + // float4 c; + // c.x = SMAASampleLevelZeroOffset(edgesTex, coords.xy, int2(-1, 0)).g; + // c.y = SMAASampleLevelZeroOffset(edgesTex, coords.xy, int2( 0, 0)).r; + // c.z = SMAASampleLevelZeroOffset(edgesTex, coords.zw, int2( 1, 0)).g; + // c.w = SMAASampleLevelZeroOffset(edgesTex, coords.zw, int2( 1, -1)).r; + + // Merge crossing edges at each side into a single value: + float2 cc = mad(float2(2.0, 2.0), c.xz, c.yw); + + // Remove the crossing edge if we didn't found the end of the line: + SMAAMovc(bool2(step(0.9, d.zw)), cc, float2(0.0, 0.0)); + + // Fetch the areas for this line: + weights += SMAAAreaDiag(SMAATexturePass2D(areaTex), d.xy, cc, subsampleIndices.z); + } + + // Search for the line ends: + d.xz = SMAASearchDiag2(SMAATexturePass2D(edgesTex), texcoord, float2(-1.0, -1.0), end); + if (SMAASampleLevelZeroOffset(edgesTex, texcoord, int2(1, 0)).r > 0.0) { + d.yw = SMAASearchDiag2(SMAATexturePass2D(edgesTex), texcoord, float2(1.0, 1.0), end); + d.y += float(end.y > 0.9); + } else + d.yw = float2(0.0, 0.0); + + SMAA_BRANCH + if (d.x + d.y > 2.0) { // d.x + d.y + 1 > 3 + // Fetch the crossing edges: + float4 coords = mad(float4(-d.x, -d.x, d.y, d.y), SMAA_RT_METRICS.xyxy, texcoord.xyxy); + float4 c; + c.x = SMAASampleLevelZeroOffset(edgesTex, coords.xy, int2(-1, 0)).g; + c.y = SMAASampleLevelZeroOffset(edgesTex, coords.xy, int2( 0, -1)).r; + c.zw = SMAASampleLevelZeroOffset(edgesTex, coords.zw, int2( 1, 0)).gr; + float2 cc = mad(float2(2.0, 2.0), c.xz, c.yw); + + // Remove the crossing edge if we didn't found the end of the line: + SMAAMovc(bool2(step(0.9, d.zw)), cc, float2(0.0, 0.0)); + + // Fetch the areas for this line: + weights += SMAAAreaDiag(SMAATexturePass2D(areaTex), d.xy, cc, subsampleIndices.w).gr; + } + + return weights; +} +#endif + +//----------------------------------------------------------------------------- +// Horizontal/Vertical Search Functions + +/** + * This allows to determine how much length should we add in the last step + * of the searches. It takes the bilinearly interpolated edge (see + * @PSEUDO_GATHER4), and adds 0, 1 or 2, depending on which edges and + * crossing edges are active. + */ +float SMAASearchLength(SMAATexture2D(searchTex), float2 e, float offset) { + // The texture is flipped vertically, with left and right cases taking half + // of the space horizontally: + float2 scale = SMAA_SEARCHTEX_SIZE * float2(0.5, -1.0); + float2 bias = SMAA_SEARCHTEX_SIZE * float2(offset, 1.0); + + // Scale and bias to access texel centers: + scale += float2(-1.0, 1.0); + bias += float2( 0.5, -0.5); + + // Convert from pixel coordinates to texcoords: + // (We use SMAA_SEARCHTEX_PACKED_SIZE because the texture is cropped) + scale *= 1.0 / SMAA_SEARCHTEX_PACKED_SIZE; + bias *= 1.0 / SMAA_SEARCHTEX_PACKED_SIZE; + + // Lookup the search texture: + return SMAA_SEARCHTEX_SELECT(SMAASampleLevelZero(searchTex, mad(scale, e, bias))); +} + +/** + * Horizontal/vertical search functions for the 2nd pass. + */ +float SMAASearchXLeft(SMAATexture2D(edgesTex), SMAATexture2D(searchTex), float2 texcoord, float end) { + /** + * @PSEUDO_GATHER4 + * This texcoord has been offset by (-0.25, -0.125) in the vertex shader to + * sample between edge, thus fetching four edges in a row. + * Sampling with different offsets in each direction allows to disambiguate + * which edges are active from the four fetched ones. + */ + float2 e = float2(0.0, 1.0); + while (texcoord.x > end && + e.g > 0.8281 && // Is there some edge not activated? + e.r == 0.0) { // Or is there a crossing edge that breaks the line? + e = SMAASampleLevelZero(edgesTex, texcoord).rg; + texcoord = mad(-float2(2.0, 0.0), SMAA_RT_METRICS.xy, texcoord); + } + + float offset = mad(-(255.0 / 127.0), SMAASearchLength(SMAATexturePass2D(searchTex), e, 0.0), 3.25); + return mad(SMAA_RT_METRICS.x, offset, texcoord.x); + + // Non-optimized version: + // We correct the previous (-0.25, -0.125) offset we applied: + // texcoord.x += 0.25 * SMAA_RT_METRICS.x; + + // The searches are bias by 1, so adjust the coords accordingly: + // texcoord.x += SMAA_RT_METRICS.x; + + // Disambiguate the length added by the last step: + // texcoord.x += 2.0 * SMAA_RT_METRICS.x; // Undo last step + // texcoord.x -= SMAA_RT_METRICS.x * (255.0 / 127.0) * SMAASearchLength(SMAATexturePass2D(searchTex), e, 0.0); + // return mad(SMAA_RT_METRICS.x, offset, texcoord.x); +} + +float SMAASearchXRight(SMAATexture2D(edgesTex), SMAATexture2D(searchTex), float2 texcoord, float end) { + float2 e = float2(0.0, 1.0); + while (texcoord.x < end && + e.g > 0.8281 && // Is there some edge not activated? + e.r == 0.0) { // Or is there a crossing edge that breaks the line? + e = SMAASampleLevelZero(edgesTex, texcoord).rg; + texcoord = mad(float2(2.0, 0.0), SMAA_RT_METRICS.xy, texcoord); + } + float offset = mad(-(255.0 / 127.0), SMAASearchLength(SMAATexturePass2D(searchTex), e, 0.5), 3.25); + return mad(-SMAA_RT_METRICS.x, offset, texcoord.x); +} + +float SMAASearchYUp(SMAATexture2D(edgesTex), SMAATexture2D(searchTex), float2 texcoord, float end) { + float2 e = float2(1.0, 0.0); + while (texcoord.y > end && + e.r > 0.8281 && // Is there some edge not activated? + e.g == 0.0) { // Or is there a crossing edge that breaks the line? + e = SMAASampleLevelZero(edgesTex, texcoord).rg; + texcoord = mad(-float2(0.0, 2.0), SMAA_RT_METRICS.xy, texcoord); + } + float offset = mad(-(255.0 / 127.0), SMAASearchLength(SMAATexturePass2D(searchTex), e.gr, 0.0), 3.25); + return mad(SMAA_RT_METRICS.y, offset, texcoord.y); +} + +float SMAASearchYDown(SMAATexture2D(edgesTex), SMAATexture2D(searchTex), float2 texcoord, float end) { + float2 e = float2(1.0, 0.0); + while (texcoord.y < end && + e.r > 0.8281 && // Is there some edge not activated? + e.g == 0.0) { // Or is there a crossing edge that breaks the line? + e = SMAASampleLevelZero(edgesTex, texcoord).rg; + texcoord = mad(float2(0.0, 2.0), SMAA_RT_METRICS.xy, texcoord); + } + float offset = mad(-(255.0 / 127.0), SMAASearchLength(SMAATexturePass2D(searchTex), e.gr, 0.5), 3.25); + return mad(-SMAA_RT_METRICS.y, offset, texcoord.y); +} + +/** + * Ok, we have the distance and both crossing edges. So, what are the areas + * at each side of current edge? + */ +float2 SMAAArea(SMAATexture2D(areaTex), float2 dist, float e1, float e2, float offset) { + // Rounding prevents precision errors of bilinear filtering: + float2 texcoord = mad(float2(SMAA_AREATEX_MAX_DISTANCE, SMAA_AREATEX_MAX_DISTANCE), round(4.0 * float2(e1, e2)), dist); + + // We do a scale and bias for mapping to texel space: + texcoord = mad(SMAA_AREATEX_PIXEL_SIZE, texcoord, 0.5 * SMAA_AREATEX_PIXEL_SIZE); + + // Move to proper place, according to the subpixel offset: + texcoord.y = mad(SMAA_AREATEX_SUBTEX_SIZE, offset, texcoord.y); + + // Do it! + return SMAA_AREATEX_SELECT(SMAASampleLevelZero(areaTex, texcoord)); +} + +//----------------------------------------------------------------------------- +// Corner Detection Functions + +void SMAADetectHorizontalCornerPattern(SMAATexture2D(edgesTex), inout float2 weights, float4 texcoord, float2 d) { + #if !defined(SMAA_DISABLE_CORNER_DETECTION) + float2 leftRight = step(d.xy, d.yx); + float2 rounding = (1.0 - SMAA_CORNER_ROUNDING_NORM) * leftRight; + + rounding /= leftRight.x + leftRight.y; // Reduce blending for pixels in the center of a line. + + float2 factor = float2(1.0, 1.0); + factor.x -= rounding.x * SMAASampleLevelZeroOffset(edgesTex, texcoord.xy, int2(0, 1)).r; + factor.x -= rounding.y * SMAASampleLevelZeroOffset(edgesTex, texcoord.zw, int2(1, 1)).r; + factor.y -= rounding.x * SMAASampleLevelZeroOffset(edgesTex, texcoord.xy, int2(0, -2)).r; + factor.y -= rounding.y * SMAASampleLevelZeroOffset(edgesTex, texcoord.zw, int2(1, -2)).r; + + weights *= saturate(factor); + #endif +} + +void SMAADetectVerticalCornerPattern(SMAATexture2D(edgesTex), inout float2 weights, float4 texcoord, float2 d) { + #if !defined(SMAA_DISABLE_CORNER_DETECTION) + float2 leftRight = step(d.xy, d.yx); + float2 rounding = (1.0 - SMAA_CORNER_ROUNDING_NORM) * leftRight; + + rounding /= leftRight.x + leftRight.y; + + float2 factor = float2(1.0, 1.0); + factor.x -= rounding.x * SMAASampleLevelZeroOffset(edgesTex, texcoord.xy, int2( 1, 0)).g; + factor.x -= rounding.y * SMAASampleLevelZeroOffset(edgesTex, texcoord.zw, int2( 1, 1)).g; + factor.y -= rounding.x * SMAASampleLevelZeroOffset(edgesTex, texcoord.xy, int2(-2, 0)).g; + factor.y -= rounding.y * SMAASampleLevelZeroOffset(edgesTex, texcoord.zw, int2(-2, 1)).g; + + weights *= saturate(factor); + #endif +} + +//----------------------------------------------------------------------------- +// Blending Weight Calculation Pixel Shader (Second Pass) + +float4 SMAABlendingWeightCalculationPS(float2 texcoord, + float2 pixcoord, + float4 offset[3], + SMAATexture2D(edgesTex), + SMAATexture2D(areaTex), + SMAATexture2D(searchTex), + float4 subsampleIndices) { // Just pass zero for SMAA 1x, see @SUBSAMPLE_INDICES. + float4 weights = float4(0.0, 0.0, 0.0, 0.0); + + float2 e = SMAASample(edgesTex, texcoord).rg; + + SMAA_BRANCH + if (e.g > 0.0) { // Edge at north + #if !defined(SMAA_DISABLE_DIAG_DETECTION) + // Diagonals have both north and west edges, so searching for them in + // one of the boundaries is enough. + weights.rg = SMAACalculateDiagWeights(SMAATexturePass2D(edgesTex), SMAATexturePass2D(areaTex), texcoord, e, subsampleIndices); + + // We give priority to diagonals, so if we find a diagonal we skip + // horizontal/vertical processing. + SMAA_BRANCH + if (weights.r == -weights.g) { // weights.r + weights.g == 0.0 + #endif + + float2 d; + + // Find the distance to the left: + float3 coords; + coords.x = SMAASearchXLeft(SMAATexturePass2D(edgesTex), SMAATexturePass2D(searchTex), offset[0].xy, offset[2].x); + coords.y = offset[1].y; // offset[1].y = texcoord.y - 0.25 * SMAA_RT_METRICS.y (@CROSSING_OFFSET) + d.x = coords.x; + + // Now fetch the left crossing edges, two at a time using bilinear + // filtering. Sampling at -0.25 (see @CROSSING_OFFSET) enables to + // discern what value each edge has: + float e1 = SMAASampleLevelZero(edgesTex, coords.xy).r; + + // Find the distance to the right: + coords.z = SMAASearchXRight(SMAATexturePass2D(edgesTex), SMAATexturePass2D(searchTex), offset[0].zw, offset[2].y); + d.y = coords.z; + + // We want the distances to be in pixel units (doing this here allow to + // better interleave arithmetic and memory accesses): + d = abs(round(mad(SMAA_RT_METRICS.zz, d, -pixcoord.xx))); + + // SMAAArea below needs a sqrt, as the areas texture is compressed + // quadratically: + float2 sqrt_d = sqrt(d); + + // Fetch the right crossing edges: + float e2 = SMAASampleLevelZeroOffset(edgesTex, coords.zy, int2(1, 0)).r; + + // Ok, we know how this pattern looks like, now it is time for getting + // the actual area: + weights.rg = SMAAArea(SMAATexturePass2D(areaTex), sqrt_d, e1, e2, subsampleIndices.y); + + // Fix corners: + coords.y = texcoord.y; + SMAADetectHorizontalCornerPattern(SMAATexturePass2D(edgesTex), weights.rg, coords.xyzy, d); + + #if !defined(SMAA_DISABLE_DIAG_DETECTION) + } else + e.r = 0.0; // Skip vertical processing. + #endif + } + + SMAA_BRANCH + if (e.r > 0.0) { // Edge at west + float2 d; + + // Find the distance to the top: + float3 coords; + coords.y = SMAASearchYUp(SMAATexturePass2D(edgesTex), SMAATexturePass2D(searchTex), offset[1].xy, offset[2].z); + coords.x = offset[0].x; // offset[1].x = texcoord.x - 0.25 * SMAA_RT_METRICS.x; + d.x = coords.y; + + // Fetch the top crossing edges: + float e1 = SMAASampleLevelZero(edgesTex, coords.xy).g; + + // Find the distance to the bottom: + coords.z = SMAASearchYDown(SMAATexturePass2D(edgesTex), SMAATexturePass2D(searchTex), offset[1].zw, offset[2].w); + d.y = coords.z; + + // We want the distances to be in pixel units: + d = abs(round(mad(SMAA_RT_METRICS.ww, d, -pixcoord.yy))); + + // SMAAArea below needs a sqrt, as the areas texture is compressed + // quadratically: + float2 sqrt_d = sqrt(d); + + // Fetch the bottom crossing edges: + float e2 = SMAASampleLevelZeroOffset(edgesTex, coords.xz, int2(0, 1)).g; + + // Get the area for this direction: + weights.ba = SMAAArea(SMAATexturePass2D(areaTex), sqrt_d, e1, e2, subsampleIndices.x); + + // Fix corners: + coords.x = texcoord.x; + SMAADetectVerticalCornerPattern(SMAATexturePass2D(edgesTex), weights.ba, coords.xyxz, d); + } + + return weights; +} + +//----------------------------------------------------------------------------- +// Neighborhood Blending Pixel Shader (Third Pass) + +float4 SMAANeighborhoodBlendingPS(float2 texcoord, + float4 offset, + SMAATexture2D(colorTex), + SMAATexture2D(blendTex) + ) { + // Fetch the blending weights for current pixel: + float4 a; + a.x = SMAASample(blendTex, offset.xy).a; // Right + a.y = SMAASample(blendTex, offset.zw).g; // Top + a.wz = SMAASample(blendTex, texcoord).xz; // Bottom / Left + + // Is there any blending weight with a value greater than 0.0? + SMAA_BRANCH + if (dot(a, float4(1.0, 1.0, 1.0, 1.0)) < 1e-5) { + float4 color = SMAA_GET_COLOR(SMAASampleLevelZero(colorTex, texcoord)); + return color; + } else { + bool h = max(a.x, a.z) > max(a.y, a.w); // max(horizontal) > max(vertical) + + // Calculate the blending offsets: + float4 blendingOffset = float4(0.0, a.y, 0.0, a.w); + float2 blendingWeight = a.yw; + SMAAMovc(bool4(h, h, h, h), blendingOffset, float4(a.x, 0.0, a.z, 0.0)); + SMAAMovc(bool2(h, h), blendingWeight, a.xz); + blendingWeight /= dot(blendingWeight, float2(1.0, 1.0)); + + // Calculate the texture coordinates: + float4 blendingCoord = mad(blendingOffset, float4(SMAA_RT_METRICS.xy, -SMAA_RT_METRICS.xy), texcoord.xyxy); + + // We exploit bilinear filtering to mix current pixel with the chosen + // neighbor: + float4 color = blendingWeight.x * SMAA_GET_COLOR(SMAASampleLevelZero(colorTex, blendingCoord.xy)); + color += blendingWeight.y * SMAA_GET_COLOR(SMAASampleLevelZero(colorTex, blendingCoord.zw)); + return color; + } +} + +//----------------------------------------------------------------------------- +// Separate Multisamples Pixel Shader (Optional Pass) + +#ifdef SMAALoad +void SMAASeparatePS(float4 position, + float2 texcoord, + out float4 target0, + out float4 target1, + SMAATexture2DMS2(colorTexMS)) { + int2 pos = int2(position.xy); + target0 = SMAALoad(colorTexMS, pos, 0); + target1 = SMAALoad(colorTexMS, pos, 1); +} +#endif + +//----------------------------------------------------------------------------- +#endif // SMAA_INCLUDE_PS diff --git a/RenderPipelineFile/rpplugins/smaa/shader/blending_weights.frag.glsl b/RenderPipelineFile/rpplugins/smaa/shader/blending_weights.frag.glsl index 43a19196..abf72f24 100644 --- a/RenderPipelineFile/rpplugins/smaa/shader/blending_weights.frag.glsl +++ b/RenderPipelineFile/rpplugins/smaa/shader/blending_weights.frag.glsl @@ -1,60 +1,60 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "smaa_wrapper.inc.glsl" - -uniform sampler2D EdgeTex; -uniform sampler2D AreaTex; -uniform sampler2D SearchTex; -out vec4 result; - -void main() { - - vec2 texcoord = get_texcoord(); - - // "Vertex shader" - vec4 offset[3]; - vec2 pixcoord; - SMAABlendingWeightCalculationVS(texcoord, pixcoord, offset); - - // XXX: Find the right indices - // #if GET_SETTING(smaa, use_reprojection) - // vec4 subsampleIndices = MainSceneData.frame_index % 2 == 1 ? vec4(1, 1, 1, 0) : vec4(2, 2, 2, 0); - // #else - vec4 subsampleIndices = vec4(0); - // #endif - - // subsampleIndices = vec4(0); - - // subsampleIndices = vec4(2, 2, 2, 0); - - // Actual Fragment shader - result = SMAABlendingWeightCalculationPS(texcoord, pixcoord, offset, - EdgeTex, AreaTex, SearchTex, subsampleIndices); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "smaa_wrapper.inc.glsl" + +uniform sampler2D EdgeTex; +uniform sampler2D AreaTex; +uniform sampler2D SearchTex; +out vec4 result; + +void main() { + + vec2 texcoord = get_texcoord(); + + // "Vertex shader" + vec4 offset[3]; + vec2 pixcoord; + SMAABlendingWeightCalculationVS(texcoord, pixcoord, offset); + + // XXX: Find the right indices + // #if GET_SETTING(smaa, use_reprojection) + // vec4 subsampleIndices = MainSceneData.frame_index % 2 == 1 ? vec4(1, 1, 1, 0) : vec4(2, 2, 2, 0); + // #else + vec4 subsampleIndices = vec4(0); + // #endif + + // subsampleIndices = vec4(0); + + // subsampleIndices = vec4(2, 2, 2, 0); + + // Actual Fragment shader + result = SMAABlendingWeightCalculationPS(texcoord, pixcoord, offset, + EdgeTex, AreaTex, SearchTex, subsampleIndices); +} diff --git a/RenderPipelineFile/rpplugins/smaa/shader/edge_detection.frag.glsl b/RenderPipelineFile/rpplugins/smaa/shader/edge_detection.frag.glsl index f9455640..6e296728 100644 --- a/RenderPipelineFile/rpplugins/smaa/shader/edge_detection.frag.glsl +++ b/RenderPipelineFile/rpplugins/smaa/shader/edge_detection.frag.glsl @@ -1,50 +1,50 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" - -#pragma include "smaa_wrapper.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" - -uniform GBufferData GBuffer; -uniform sampler2D ShadedScene; -out vec4 result; - -void main() { - - vec2 texcoord = get_texcoord(); - - // "Vertex shader" - vec4 offset[3]; - SMAAEdgeDetectionVS(texcoord, offset); - - // Actual Fragment shader - result = vec4(0); - result.xy = SMAAColorEdgeDetectionPS(texcoord, offset, ShadedScene); - result.w = 1.0; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" + +#pragma include "smaa_wrapper.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" + +uniform GBufferData GBuffer; +uniform sampler2D ShadedScene; +out vec4 result; + +void main() { + + vec2 texcoord = get_texcoord(); + + // "Vertex shader" + vec4 offset[3]; + SMAAEdgeDetectionVS(texcoord, offset); + + // Actual Fragment shader + result = vec4(0); + result.xy = SMAAColorEdgeDetectionPS(texcoord, offset, ShadedScene); + result.w = 1.0; +} diff --git a/RenderPipelineFile/rpplugins/smaa/shader/neighborhood_blending.frag.glsl b/RenderPipelineFile/rpplugins/smaa/shader/neighborhood_blending.frag.glsl index d7b55632..4e38809f 100644 --- a/RenderPipelineFile/rpplugins/smaa/shader/neighborhood_blending.frag.glsl +++ b/RenderPipelineFile/rpplugins/smaa/shader/neighborhood_blending.frag.glsl @@ -1,47 +1,47 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "smaa_wrapper.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" - -uniform sampler2D BlendTex; -uniform sampler2D ShadedScene; - -out vec4 result; - -void main() { - vec2 texcoord = get_texcoord(); - - // "Vertex shader" - vec4 offset; - SMAANeighborhoodBlendingVS(texcoord, offset); - - // Actual Fragment shader - result = SMAANeighborhoodBlendingPS(texcoord, offset, ShadedScene, BlendTex); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "smaa_wrapper.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" + +uniform sampler2D BlendTex; +uniform sampler2D ShadedScene; + +out vec4 result; + +void main() { + vec2 texcoord = get_texcoord(); + + // "Vertex shader" + vec4 offset; + SMAANeighborhoodBlendingVS(texcoord, offset); + + // Actual Fragment shader + result = SMAANeighborhoodBlendingPS(texcoord, offset, ShadedScene, BlendTex); +} diff --git a/RenderPipelineFile/rpplugins/smaa/shader/resolve_smaa.frag.glsl b/RenderPipelineFile/rpplugins/smaa/shader/resolve_smaa.frag.glsl index 95ba0b2d..63eee0b1 100644 --- a/RenderPipelineFile/rpplugins/smaa/shader/resolve_smaa.frag.glsl +++ b/RenderPipelineFile/rpplugins/smaa/shader/resolve_smaa.frag.glsl @@ -1,52 +1,52 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" - -#define RS_MAX_CLIP_DIST 0.4 -#define RS_DISTANCE_SCALE 1.5 -#define RS_KEEP_GOOD_DURATION float(GET_SETTING(smaa, history_length)) -#define RS_KEEP_BAD_DURATION RS_KEEP_GOOD_DURATION * 0.5 - -#pragma include "includes/temporal_resolve.inc.glsl" - -uniform sampler2D CurrentTex; -uniform sampler2D Previous_SMAAPostResolve; -uniform sampler2D CombinedVelocity; - -uniform int jitterIndex; - -out vec3 result; - -void main() { - vec2 texcoord = get_texcoord(); - vec2 velocity = textureLod(CombinedVelocity, texcoord, 0).xy; - vec2 last_coord = texcoord + velocity; - - result = resolve_temporal(CurrentTex, Previous_SMAAPostResolve, texcoord, last_coord).xyz; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" + +#define RS_MAX_CLIP_DIST 0.4 +#define RS_DISTANCE_SCALE 1.5 +#define RS_KEEP_GOOD_DURATION float(GET_SETTING(smaa, history_length)) +#define RS_KEEP_BAD_DURATION RS_KEEP_GOOD_DURATION * 0.5 + +#pragma include "includes/temporal_resolve.inc.glsl" + +uniform sampler2D CurrentTex; +uniform sampler2D Previous_SMAAPostResolve; +uniform sampler2D CombinedVelocity; + +uniform int jitterIndex; + +out vec3 result; + +void main() { + vec2 texcoord = get_texcoord(); + vec2 velocity = textureLod(CombinedVelocity, texcoord, 0).xy; + vec2 last_coord = texcoord + velocity; + + result = resolve_temporal(CurrentTex, Previous_SMAAPostResolve, texcoord, last_coord).xyz; +} diff --git a/RenderPipelineFile/rpplugins/smaa/shader/smaa_wrapper.inc.glsl b/RenderPipelineFile/rpplugins/smaa/shader/smaa_wrapper.inc.glsl index 97c46647..c939079f 100644 --- a/RenderPipelineFile/rpplugins/smaa/shader/smaa_wrapper.inc.glsl +++ b/RenderPipelineFile/rpplugins/smaa/shader/smaa_wrapper.inc.glsl @@ -1,74 +1,74 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -// This file just sets a few defines and then includes the SMAA header - -#define SMAA_GLSL_4 -#define SMAA_RT_METRICS vec4(1.0 / WINDOW_WIDTH, 1.0 / WINDOW_HEIGHT, WINDOW_WIDTH, WINDOW_HEIGHT) - - -// Get seleted SMAA quality -#if ENUM_V_ACTIVE(smaa, smaa_quality, low) - #define SMAA_PRESET_LOW -#elif ENUM_V_ACTIVE(smaa, smaa_quality, medium) - #define SMAA_PRESET_MEDIUM -#elif ENUM_V_ACTIVE(smaa, smaa_quality, high) - #define SMAA_PRESET_HIGH -#elif ENUM_V_ACTIVE(smaa, smaa_quality, ultra) - #define SMAA_PRESET_ULTRA -#else - #error Unkown smaa quality value! -#endif - - -// Include both Pixel and Vertex shader, because we do the vertex shader logic -// in the pixel shader. -#define SMAA_INCLUDE_VS 1 -#define SMAA_INCLUDE_PS 1 -#define SMAA_DECODE_VELOCITY(_sample) error, custom resolve pass - -vec3 SMAA_GET_COLOR(vec3 _color) { - return _color; -} - -vec4 SMAA_GET_COLOR(vec4 _color) { - return _color; -} - -// Optionally enable smaa predication -// #define SMAA_PREDICATION 1 -// #define SMAA_PREDICATION_SCALE 3.0 -// #define SMAA_PREDICATION_THRESHOLD 0.00001 - -// SMAA defines its own saturate, make sure we don't run into conflicts -#ifdef saturate - #undef saturate -#endif - -// Include the actual smaa header -#pragma include "SMAA.inc.glsl" +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +// This file just sets a few defines and then includes the SMAA header + +#define SMAA_GLSL_4 +#define SMAA_RT_METRICS vec4(1.0 / WINDOW_WIDTH, 1.0 / WINDOW_HEIGHT, WINDOW_WIDTH, WINDOW_HEIGHT) + + +// Get seleted SMAA quality +#if ENUM_V_ACTIVE(smaa, smaa_quality, low) + #define SMAA_PRESET_LOW +#elif ENUM_V_ACTIVE(smaa, smaa_quality, medium) + #define SMAA_PRESET_MEDIUM +#elif ENUM_V_ACTIVE(smaa, smaa_quality, high) + #define SMAA_PRESET_HIGH +#elif ENUM_V_ACTIVE(smaa, smaa_quality, ultra) + #define SMAA_PRESET_ULTRA +#else + #error Unkown smaa quality value! +#endif + + +// Include both Pixel and Vertex shader, because we do the vertex shader logic +// in the pixel shader. +#define SMAA_INCLUDE_VS 1 +#define SMAA_INCLUDE_PS 1 +#define SMAA_DECODE_VELOCITY(_sample) error, custom resolve pass + +vec3 SMAA_GET_COLOR(vec3 _color) { + return _color; +} + +vec4 SMAA_GET_COLOR(vec4 _color) { + return _color; +} + +// Optionally enable smaa predication +// #define SMAA_PREDICATION 1 +// #define SMAA_PREDICATION_SCALE 3.0 +// #define SMAA_PREDICATION_THRESHOLD 0.00001 + +// SMAA defines its own saturate, make sure we don't run into conflicts +#ifdef saturate + #undef saturate +#endif + +// Include the actual smaa header +#pragma include "SMAA.inc.glsl" diff --git a/RenderPipelineFile/rpplugins/smaa/smaa_stage.py b/RenderPipelineFile/rpplugins/smaa/smaa_stage.py index e0b60ba3..c5eed817 100644 --- a/RenderPipelineFile/rpplugins/smaa/smaa_stage.py +++ b/RenderPipelineFile/rpplugins/smaa/smaa_stage.py @@ -1,106 +1,106 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" -from __future__ import division - -from panda3d.core import PTAInt - -from rpcore.render_stage import RenderStage - - -class SMAAStage(RenderStage): - - """ This stage does the actual SMAA """ - - required_inputs = [] - - def __init__(self, pipeline): - RenderStage.__init__(self, pipeline) - self.area_tex = None - self.search_tex = None - self.use_reprojection = True - self._jitter_index = PTAInt.empty_array(1) - - def set_jitter_index(self, idx): - """ Sets the current jitter index """ - self._jitter_index[0] = idx - - @property - def required_pipes(self): - pipes = ["ShadedScene", "GBuffer", "CombinedVelocity"] - if self.use_reprojection: - pipes.append("PreviousFrame::SMAAPostResolve") - return pipes - - @property - def produced_pipes(self): - if self.use_reprojection: - return { - "ShadedScene": self.resolve_target.color_tex, - "SMAAPostResolve": self.resolve_target.color_tex - } - else: - return {"ShadedScene": self.neighbor_target.color_tex} - - def create(self): - # Edge detection - self.edge_target = self.create_target("EdgeDetection") - self.edge_target.add_color_attachment() - self.edge_target.prepare_buffer() - self.edge_target.set_clear_color(0, 0, 0, 0) - - # Weight blending - self.blend_target = self.create_target("BlendWeights") - self.blend_target.add_color_attachment(alpha=True) - self.blend_target.prepare_buffer() - - self.blend_target.set_shader_inputs( - EdgeTex=self.edge_target.color_tex, - AreaTex=self.area_tex, - SearchTex=self.search_tex, - jitterIndex=self._jitter_index) - - # Neighbor blending - self.neighbor_target = self.create_target("NeighborBlending") - self.neighbor_target.add_color_attachment(bits=16) - self.neighbor_target.prepare_buffer() - self.neighbor_target.set_shader_input("BlendTex", self.blend_target.color_tex) - - # Resolving - if self.use_reprojection: - self.resolve_target = self.create_target("Resolve") - self.resolve_target.add_color_attachment(bits=16) - self.resolve_target.prepare_buffer() - self.resolve_target.set_shader_inputs( - jitterIndex=self._jitter_index, - # Set initial textures - CurrentTex=self.neighbor_target.color_tex) - - def reload_shaders(self): - self.edge_target.shader = self.load_plugin_shader("edge_detection.frag.glsl") - self.blend_target.shader = self.load_plugin_shader("blending_weights.frag.glsl") - self.neighbor_target.shader = self.load_plugin_shader("neighborhood_blending.frag.glsl") - if self.use_reprojection: - self.resolve_target.shader = self.load_plugin_shader("resolve_smaa.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +from __future__ import division + +from panda3d.core import PTAInt + +from rpcore.render_stage import RenderStage + + +class SMAAStage(RenderStage): + + """ This stage does the actual SMAA """ + + required_inputs = [] + + def __init__(self, pipeline): + RenderStage.__init__(self, pipeline) + self.area_tex = None + self.search_tex = None + self.use_reprojection = True + self._jitter_index = PTAInt.empty_array(1) + + def set_jitter_index(self, idx): + """ Sets the current jitter index """ + self._jitter_index[0] = idx + + @property + def required_pipes(self): + pipes = ["ShadedScene", "GBuffer", "CombinedVelocity"] + if self.use_reprojection: + pipes.append("PreviousFrame::SMAAPostResolve") + return pipes + + @property + def produced_pipes(self): + if self.use_reprojection: + return { + "ShadedScene": self.resolve_target.color_tex, + "SMAAPostResolve": self.resolve_target.color_tex + } + else: + return {"ShadedScene": self.neighbor_target.color_tex} + + def create(self): + # Edge detection + self.edge_target = self.create_target("EdgeDetection") + self.edge_target.add_color_attachment() + self.edge_target.prepare_buffer() + self.edge_target.set_clear_color(0, 0, 0, 0) + + # Weight blending + self.blend_target = self.create_target("BlendWeights") + self.blend_target.add_color_attachment(alpha=True) + self.blend_target.prepare_buffer() + + self.blend_target.set_shader_inputs( + EdgeTex=self.edge_target.color_tex, + AreaTex=self.area_tex, + SearchTex=self.search_tex, + jitterIndex=self._jitter_index) + + # Neighbor blending + self.neighbor_target = self.create_target("NeighborBlending") + self.neighbor_target.add_color_attachment(bits=16) + self.neighbor_target.prepare_buffer() + self.neighbor_target.set_shader_input("BlendTex", self.blend_target.color_tex) + + # Resolving + if self.use_reprojection: + self.resolve_target = self.create_target("Resolve") + self.resolve_target.add_color_attachment(bits=16) + self.resolve_target.prepare_buffer() + self.resolve_target.set_shader_inputs( + jitterIndex=self._jitter_index, + # Set initial textures + CurrentTex=self.neighbor_target.color_tex) + + def reload_shaders(self): + self.edge_target.shader = self.load_plugin_shader("edge_detection.frag.glsl") + self.blend_target.shader = self.load_plugin_shader("blending_weights.frag.glsl") + self.neighbor_target.shader = self.load_plugin_shader("neighborhood_blending.frag.glsl") + if self.use_reprojection: + self.resolve_target.shader = self.load_plugin_shader("resolve_smaa.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/ssr/config.yaml b/RenderPipelineFile/rpplugins/ssr/config.yaml index b306cfe6..0f2c514e 100644 --- a/RenderPipelineFile/rpplugins/ssr/config.yaml +++ b/RenderPipelineFile/rpplugins/ssr/config.yaml @@ -1,103 +1,103 @@ - -settings: !!omap - - - - effect_scale: - type: float - range: [0.0, 1.0] - default: 1.0 - label: SSR Strength - shader_runtime: true - description: > - Controls the strength of the reflections, lower values will reduce - the influence of ssr, increasing the influence of other plugins - like environment probes - - - trace_steps: - type: int - range: [4, 512] - default: 64 - label: Trace Steps - shader_runtime: true - description: > - Amount of trace steps. A higher amount will lead to less missed - details, although it will also be more computationally expensive. - - - history_length: - type: int - range: [1, 32] - default: 16 - shader_runtime: true - label: History Length - description: > - History buffer size. This controls how long pixels will stay - in the history buffer, greater values lead to smoother results - but also might produce more artifacts. - - - abort_on_object_infront: - type: bool - default: true - label: Abort on invalid hit - shader_runtime: true - description: > - Aborts the trace when an object is found in front of the ray. - This might lead to artifacts on floating objects, but might - also improve the quality. - - - intial_bias: - type: float - range: [0.05, 10.0] - default: 0.1 - label: Initial ray bias - shader_runtime: true - description: > - Offsets the ray a bit in the tracing direction to avoid self- - intersection. A too small value will cause artifacts due to self - intersection, a too high value will miss details near to objects. - - - hit_tolerance: - type: float - range: [0.05, 10.0] - default: 0.1 - label: Hit tolerance - shader_runtime: true - description: > - Higher values will produce more intersections, but also produce - false positives. If you set this value too low, artifacts and noise - will occur. - - - roughness_fade: - type: float - range: [0.1, 1.0] - default: 0.72 - shader_runtime: true - label: Roughness Fade - description: > - Fades out the reflections at high roughness values, so that other - solutions like environment probes have more influence. This is - to prevent noise, and controls the maximum roughness where ssr is - active. - - - skip_invalid_samples: - type: bool - default: false - shader_runtime: true - label: Skip Invalid Pixels - description: > - Whether to skip pixels which could not successfully be reprojected - from the last frames color texture. This affects the effect when - moving. Setting this to true will produce more accurate but also - more noisy results. Setting this to false might produce artifacts. - - - border_fade: - type: float - range: [0.001, 1.0] - default: 0.005 - shader_runtime: true - label: Border Fade - description: > - Fades out the effect at the screen borders to avoid rough transitions. - A value of 0.0 means no transitions while a value of 1.0 means a - very smooth transition. - -daytime_settings: + +settings: !!omap + + + - effect_scale: + type: float + range: [0.0, 1.0] + default: 1.0 + label: SSR Strength + shader_runtime: true + description: > + Controls the strength of the reflections, lower values will reduce + the influence of ssr, increasing the influence of other plugins + like environment probes + + - trace_steps: + type: int + range: [4, 512] + default: 64 + label: Trace Steps + shader_runtime: true + description: > + Amount of trace steps. A higher amount will lead to less missed + details, although it will also be more computationally expensive. + + - history_length: + type: int + range: [1, 32] + default: 16 + shader_runtime: true + label: History Length + description: > + History buffer size. This controls how long pixels will stay + in the history buffer, greater values lead to smoother results + but also might produce more artifacts. + + - abort_on_object_infront: + type: bool + default: true + label: Abort on invalid hit + shader_runtime: true + description: > + Aborts the trace when an object is found in front of the ray. + This might lead to artifacts on floating objects, but might + also improve the quality. + + - intial_bias: + type: float + range: [0.05, 10.0] + default: 0.1 + label: Initial ray bias + shader_runtime: true + description: > + Offsets the ray a bit in the tracing direction to avoid self- + intersection. A too small value will cause artifacts due to self + intersection, a too high value will miss details near to objects. + + - hit_tolerance: + type: float + range: [0.05, 10.0] + default: 0.1 + label: Hit tolerance + shader_runtime: true + description: > + Higher values will produce more intersections, but also produce + false positives. If you set this value too low, artifacts and noise + will occur. + + - roughness_fade: + type: float + range: [0.1, 1.0] + default: 0.72 + shader_runtime: true + label: Roughness Fade + description: > + Fades out the reflections at high roughness values, so that other + solutions like environment probes have more influence. This is + to prevent noise, and controls the maximum roughness where ssr is + active. + + - skip_invalid_samples: + type: bool + default: false + shader_runtime: true + label: Skip Invalid Pixels + description: > + Whether to skip pixels which could not successfully be reprojected + from the last frames color texture. This affects the effect when + moving. Setting this to true will produce more accurate but also + more noisy results. Setting this to false might produce artifacts. + + - border_fade: + type: float + range: [0.001, 1.0] + default: 0.005 + shader_runtime: true + label: Border Fade + description: > + Fades out the effect at the screen borders to avoid rough transitions. + A value of 0.0 means no transitions while a value of 1.0 means a + very smooth transition. + +daytime_settings: diff --git a/RenderPipelineFile/rpplugins/ssr/plugin.py b/RenderPipelineFile/rpplugins/ssr/plugin.py index 35f3ee8d..38d489c7 100644 --- a/RenderPipelineFile/rpplugins/ssr/plugin.py +++ b/RenderPipelineFile/rpplugins/ssr/plugin.py @@ -1,50 +1,50 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.pluginbase.base_plugin import BasePlugin - -from .ssr_stage import SSRStage - - -class Plugin(BasePlugin): - - name = "Screen Space Reflections" - author = "tobspr " - description = ("This plugin adds support for stochastik screen space reflections " - "and is still very experimental!") - version = "beta (!)" - - def on_stage_setup(self): - self.ssr_stage = self.create_stage(SSRStage) - - if self.is_plugin_enabled("color_correction"): - self.ssr_stage.required_pipes.append("FuturePipe::Exposure") - - def reload_shaders(self): - BasePlugin.reload_shaders(self) - - # Also update the ambient stage - self._pipeline.stage_mgr.get_stage("AmbientStage").reload_shaders() +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.pluginbase.base_plugin import BasePlugin + +from .ssr_stage import SSRStage + + +class Plugin(BasePlugin): + + name = "Screen Space Reflections" + author = "tobspr " + description = ("This plugin adds support for stochastik screen space reflections " + "and is still very experimental!") + version = "beta (!)" + + def on_stage_setup(self): + self.ssr_stage = self.create_stage(SSRStage) + + if self.is_plugin_enabled("color_correction"): + self.ssr_stage.required_pipes.append("FuturePipe::Exposure") + + def reload_shaders(self): + BasePlugin.reload_shaders(self) + + # Also update the ambient stage + self._pipeline.stage_mgr.get_stage("AmbientStage").reload_shaders() diff --git a/RenderPipelineFile/rpplugins/ssr/shader/reflection_velocity.frag.glsl b/RenderPipelineFile/rpplugins/ssr/shader/reflection_velocity.frag.glsl index 29a63484..22651b1c 100644 --- a/RenderPipelineFile/rpplugins/ssr/shader/reflection_velocity.frag.glsl +++ b/RenderPipelineFile/rpplugins/ssr/shader/reflection_velocity.frag.glsl @@ -1,92 +1,92 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// Copies the reflection velocity vector - -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/color_spaces.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "includes/transforms.inc.glsl" - -uniform sampler2D TraceResult; -out vec2 velocity; - -void main() { - vec2 texcoord = get_texcoord(); - - // Get bilateral start coordinates - ivec2 coord = ivec2(gl_FragCoord.xy); - ivec2 bil_start_coord = get_bilateral_coord(coord); - float mid_depth = get_depth_at(texcoord); - - // Find the position of the closest fragment - const int kernel_size = 0; - vec2 best_result = vec2(0); - float best_score = 0.0; - - for (int i = -kernel_size; i < 2 + kernel_size; ++i) { - for (int j = -kernel_size; j < 2 + kernel_size; ++j) { - - ivec2 source_coord = bil_start_coord + ivec2(i, j); - ivec2 screen_coord = source_coord * 2; - - vec2 trace_result = texelFetch(TraceResult, source_coord, 0).xy; - float trace_depth = get_depth_at(screen_coord); - float trace_score = max(0.0, 1.0 - 10.0 * abs(trace_depth - mid_depth)); - - // Weight the sample, so that depths closer to the viewer have a greater weight. - // Also scale by the fade factor to make sure a sample is present - trace_score = (1 - trace_depth) * (trace_result.x > 1e-5 ? 1.0 : 0.0); - - if (trace_score > best_score) { - best_result = trace_result; - best_score = trace_score; - } - } - } - - // In case no sample was found, fall back to the current pixels coordinate - if (length_squared(best_result) < 1e-4) { - best_result = texcoord; - } - - best_result = truncate_coordinate(best_result); // xxx - - // Find depth of intersection - float intersection_depth = get_depth_at(best_result); - - // Reconstruct last frame texcoord - vec2 film_offset_bias = MainSceneData.current_film_offset * vec2(1.0, 1.0 / ASPECT_RATIO); - vec3 pos = calculate_surface_pos(intersection_depth, texcoord - film_offset_bias); - vec4 last_proj = MainSceneData.last_view_proj_mat_no_jitter * vec4(pos, 1); - vec2 last_coord = fma(last_proj.xy / last_proj.w, vec2(0.5), vec2(0.5)); - - // Compute velocity - velocity = last_coord - texcoord; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// Copies the reflection velocity vector + +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/color_spaces.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "includes/transforms.inc.glsl" + +uniform sampler2D TraceResult; +out vec2 velocity; + +void main() { + vec2 texcoord = get_texcoord(); + + // Get bilateral start coordinates + ivec2 coord = ivec2(gl_FragCoord.xy); + ivec2 bil_start_coord = get_bilateral_coord(coord); + float mid_depth = get_depth_at(texcoord); + + // Find the position of the closest fragment + const int kernel_size = 0; + vec2 best_result = vec2(0); + float best_score = 0.0; + + for (int i = -kernel_size; i < 2 + kernel_size; ++i) { + for (int j = -kernel_size; j < 2 + kernel_size; ++j) { + + ivec2 source_coord = bil_start_coord + ivec2(i, j); + ivec2 screen_coord = source_coord * 2; + + vec2 trace_result = texelFetch(TraceResult, source_coord, 0).xy; + float trace_depth = get_depth_at(screen_coord); + float trace_score = max(0.0, 1.0 - 10.0 * abs(trace_depth - mid_depth)); + + // Weight the sample, so that depths closer to the viewer have a greater weight. + // Also scale by the fade factor to make sure a sample is present + trace_score = (1 - trace_depth) * (trace_result.x > 1e-5 ? 1.0 : 0.0); + + if (trace_score > best_score) { + best_result = trace_result; + best_score = trace_score; + } + } + } + + // In case no sample was found, fall back to the current pixels coordinate + if (length_squared(best_result) < 1e-4) { + best_result = texcoord; + } + + best_result = truncate_coordinate(best_result); // xxx + + // Find depth of intersection + float intersection_depth = get_depth_at(best_result); + + // Reconstruct last frame texcoord + vec2 film_offset_bias = MainSceneData.current_film_offset * vec2(1.0, 1.0 / ASPECT_RATIO); + vec3 pos = calculate_surface_pos(intersection_depth, texcoord - film_offset_bias); + vec4 last_proj = MainSceneData.last_view_proj_mat_no_jitter * vec4(pos, 1); + vec2 last_coord = fma(last_proj.xy / last_proj.w, vec2(0.5), vec2(0.5)); + + // Compute velocity + velocity = last_coord - texcoord; +} diff --git a/RenderPipelineFile/rpplugins/ssr/shader/reproject_lighting.frag.glsl b/RenderPipelineFile/rpplugins/ssr/shader/reproject_lighting.frag.glsl index b39c1af7..59a6d28c 100644 --- a/RenderPipelineFile/rpplugins/ssr/shader/reproject_lighting.frag.glsl +++ b/RenderPipelineFile/rpplugins/ssr/shader/reproject_lighting.frag.glsl @@ -1,96 +1,96 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -// Copies the previous scene color to the first mipmap. -// Also outputs the current frame intersection depth - -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/color_spaces.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "includes/transforms.inc.glsl" - -uniform sampler2D CombinedVelocity; -uniform sampler2D Previous_PostAmbientScene; -uniform sampler2D Previous_SceneDepth; - -#if HAVE_PLUGIN(color_correction) - uniform samplerBuffer Exposure; -#endif - -out vec4 result; - -void main() { - vec2 texcoord = get_texcoord(); - vec2 velocity = textureLod(CombinedVelocity, texcoord, 0).xy; - vec2 last_coord = texcoord + velocity; - - // Out of screen, can early out - if (out_of_screen(last_coord)) { - result = vec4(0); - return; - } - - float fade = 1.0; - - // Check if reprojected position matches - float curr_depth = get_depth_at(texcoord); - - #if GET_SETTING(ssr, skip_invalid_samples) - // Skip samples which are invalid due to a position change or due to being - // occluded in the last frame. - - // TODO: Should probably use the 3x3 AABB for this, but might be too - // performance heavy. I think this should work out well. - vec3 curr_pos = calculate_surface_pos(curr_depth, texcoord); - float last_depth = textureLod(Previous_SceneDepth, last_coord, 0).x; - - vec3 last_pos = calculate_surface_pos(last_depth, last_coord, - MainSceneData.last_inv_view_proj_mat_no_jitter); - - if (distance(curr_pos, last_pos) > 0.9) { - // fade = 0.0; - } - #endif - - vec3 intersected_color = textureLod(Previous_PostAmbientScene, last_coord, 0).xyz; - - #if HAVE_PLUGIN(color_correction) - // Prevent super bright spots by clamping to a reasonable high color. - // Otherwise very bright highlights lead to artifacts. - // Since this heavily depends on the scene brightness, we base this on - // the exposure. Basically the color is only allowed to be twice as bright - // as the average screen brightness. - - float current_ev = 1.0 / texelFetch(Exposure, 0).x; - intersected_color = clamp(intersected_color, 0.0, 2 * current_ev); - #endif - - // Finally store the result in the mip-chain - result = vec4(intersected_color, 1) * fade; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +// Copies the previous scene color to the first mipmap. +// Also outputs the current frame intersection depth + +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/color_spaces.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "includes/transforms.inc.glsl" + +uniform sampler2D CombinedVelocity; +uniform sampler2D Previous_PostAmbientScene; +uniform sampler2D Previous_SceneDepth; + +#if HAVE_PLUGIN(color_correction) + uniform samplerBuffer Exposure; +#endif + +out vec4 result; + +void main() { + vec2 texcoord = get_texcoord(); + vec2 velocity = textureLod(CombinedVelocity, texcoord, 0).xy; + vec2 last_coord = texcoord + velocity; + + // Out of screen, can early out + if (out_of_screen(last_coord)) { + result = vec4(0); + return; + } + + float fade = 1.0; + + // Check if reprojected position matches + float curr_depth = get_depth_at(texcoord); + + #if GET_SETTING(ssr, skip_invalid_samples) + // Skip samples which are invalid due to a position change or due to being + // occluded in the last frame. + + // TODO: Should probably use the 3x3 AABB for this, but might be too + // performance heavy. I think this should work out well. + vec3 curr_pos = calculate_surface_pos(curr_depth, texcoord); + float last_depth = textureLod(Previous_SceneDepth, last_coord, 0).x; + + vec3 last_pos = calculate_surface_pos(last_depth, last_coord, + MainSceneData.last_inv_view_proj_mat_no_jitter); + + if (distance(curr_pos, last_pos) > 0.9) { + // fade = 0.0; + } + #endif + + vec3 intersected_color = textureLod(Previous_PostAmbientScene, last_coord, 0).xyz; + + #if HAVE_PLUGIN(color_correction) + // Prevent super bright spots by clamping to a reasonable high color. + // Otherwise very bright highlights lead to artifacts. + // Since this heavily depends on the scene brightness, we base this on + // the exposure. Basically the color is only allowed to be twice as bright + // as the average screen brightness. + + float current_ev = 1.0 / texelFetch(Exposure, 0).x; + intersected_color = clamp(intersected_color, 0.0, 2 * current_ev); + #endif + + // Finally store the result in the mip-chain + result = vec4(intersected_color, 1) * fade; +} diff --git a/RenderPipelineFile/rpplugins/ssr/shader/resolve_ssr.frag.glsl b/RenderPipelineFile/rpplugins/ssr/shader/resolve_ssr.frag.glsl index 900484a5..15a264f7 100644 --- a/RenderPipelineFile/rpplugins/ssr/shader/resolve_ssr.frag.glsl +++ b/RenderPipelineFile/rpplugins/ssr/shader/resolve_ssr.frag.glsl @@ -1,55 +1,55 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/transforms.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" - -uniform sampler2D CurrentTex; -uniform sampler2D VelocityTex; -uniform sampler2D Previous_SSRSpecular; - -#define RS_MAX_CLIP_DIST 5.0 -#define RS_DISTANCE_SCALE 0.2 -#define RS_KEEP_GOOD_DURATION float(GET_SETTING(ssr, history_length)) -#define RS_KEEP_BAD_DURATION float(GET_SETTING(ssr, history_length)) -#define RS_AABB_SIZE 1.5 -#define RS_USE_SMOOTH_TECHNIQUE 1 - - -#pragma include "includes/temporal_resolve.inc.glsl" - -out vec4 result; - -void main() { - vec2 texcoord = get_texcoord(); - vec2 velocity = textureLod(VelocityTex, texcoord, 0).xy; - vec2 last_coord = texcoord + velocity; - result = resolve_temporal(CurrentTex, Previous_SSRSpecular, texcoord, last_coord); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/transforms.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" + +uniform sampler2D CurrentTex; +uniform sampler2D VelocityTex; +uniform sampler2D Previous_SSRSpecular; + +#define RS_MAX_CLIP_DIST 5.0 +#define RS_DISTANCE_SCALE 0.2 +#define RS_KEEP_GOOD_DURATION float(GET_SETTING(ssr, history_length)) +#define RS_KEEP_BAD_DURATION float(GET_SETTING(ssr, history_length)) +#define RS_AABB_SIZE 1.5 +#define RS_USE_SMOOTH_TECHNIQUE 1 + + +#pragma include "includes/temporal_resolve.inc.glsl" + +out vec4 result; + +void main() { + vec2 texcoord = get_texcoord(); + vec2 velocity = textureLod(VelocityTex, texcoord, 0).xy; + vec2 last_coord = texcoord + velocity; + result = resolve_temporal(CurrentTex, Previous_SSRSpecular, texcoord, last_coord); +} diff --git a/RenderPipelineFile/rpplugins/ssr/shader/ssr_trace.frag.glsl b/RenderPipelineFile/rpplugins/ssr/shader/ssr_trace.frag.glsl index f67629cb..0bf3c8a3 100644 --- a/RenderPipelineFile/rpplugins/ssr/shader/ssr_trace.frag.glsl +++ b/RenderPipelineFile/rpplugins/ssr/shader/ssr_trace.frag.glsl @@ -1,244 +1,244 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "includes/noise.inc.glsl" -#pragma include "includes/brdf.inc.glsl" -#pragma include "includes/transforms.inc.glsl" -#pragma include "includes/importance_sampling.inc.glsl" - -uniform sampler2D DownscaledDepth; - -out vec2 result; - -#define USE_LINEAR_DEPTH 0 -#define NUM_RAYDIR_RETRIES 3 - -const int num_steps = GET_SETTING(ssr, trace_steps); -const float hit_tolerance_ws = 0.00001 * GET_SETTING(ssr, hit_tolerance); -const float hit_tolerance_backface = 0.00001; - -bool point_between_planes(float z, float z_a, float z_b, float trace_length, out bool hit_factor) { - - // This traces correct, but looks weird because gaps are not filled - // return z + hit_tolerance_ws >= min(z_a, z_b) - 0.00015 && z - hit_tolerance_ws <= max(z_a, z_b); - - hit_factor = false; - - // This traces "incorrect", but looks better because gaps are getting filled then - if (z - hit_tolerance_ws * trace_length <= max(z_a, z_b)) { - - #if GET_SETTING(ssr, abort_on_object_infront) - hit_factor = (z + hit_tolerance_ws * trace_length) >= - min(z_a, z_b) - hit_tolerance_backface; - #else - hit_factor = true; - #endif - - return true; - } - - return false; -} - - -void main() -{ - ivec2 coord = ivec2(gl_FragCoord.xy); - vec2 texcoord = get_half_texcoord(); - - vec3 normal_vs = get_view_normal(texcoord); - Material m = unpack_material(GBuffer, texcoord); - normal_vs = world_normal_to_view(m.normal); - - vec3 ray_start_vs = get_view_pos_at(texcoord); - float pixeldist = distance(m.position, MainSceneData.camera_pos); - - // Skip skybox and distant pixels - if (pixeldist > 3000) { - result = vec2(0); - return; - } - - // Important: For clearcoat we trace the outer layer (with low roughness) - // instead of the high roughness layer - float roughness = get_effective_roughness(m); - - // Skip pixels with too high roughness - if (roughness > GET_SETTING(ssr, roughness_fade)) { - result = vec2(0); - return; - } - - - // Get ray start - vec3 view_dir = normalize(ray_start_vs); - vec3 ray_dir = normalize(reflect(view_dir, normal_vs)); - - // Get tangent and binormal - vec3 tangent, binormal; - find_arbitrary_tangent(ray_dir, tangent, binormal); - - vec3 importance_ray_dir = vec3(0); - - - // Generate ray directions until we find a value which is valid - int retry = 0; - for (;retry < NUM_RAYDIR_RETRIES; ++retry) { - - vec2 seed = texcoord + 1.3123 * retry + 0.176445 * (MainSceneData.frame_index % 32); - - // Get random sequence, should probably use halton or so - vec2 xi = clamp(abs(rand_rgb(seed).xy), vec2(0.01), vec2(0.99)); - - // Clamp brdf tail, see frostbite slides for details - const float brdf_bias = 0.8; - xi.y = mix(xi.y, 0.0, brdf_bias); - - // Get importance sampled directory - vec3 rho = importance_sample_ggx(xi, clamp(sqrt(roughness), 0.0001, 1.0)); - importance_ray_dir = normalize(1e-5 + rho.x * tangent + rho.y * binormal + rho.z * ray_dir); - - // If the ray dir is fine, abort, otherwise continue - if (dot(importance_ray_dir, normal_vs) > 0.005) { - break; - } - } - - // Still didn't find a good ray dir - if (retry >= NUM_RAYDIR_RETRIES - 1) { - result = vec2(0); - return; - } - - ray_dir = importance_ray_dir; - - // Ray not in view - if (dot(ray_dir, view_dir) < 1e-5) { - result = vec2(0); - return; - } - - float max_ray_len = 1.0 * pixeldist; - - // Clip ray to near plane - #if 1 - float ray_len = ((ray_start_vs.z + ray_dir.z * max_ray_len) > CAMERA_NEAR) ? - (CAMERA_NEAR - ray_start_vs.z) / ray_dir.z : - max_ray_len; - #else - float ray_len = max_ray_len; - #endif - - // Convert start and end pos from view to screen space - vec3 ray_start_screen = view_to_screen(ray_start_vs); - vec3 ray_end_screen = view_to_screen(ray_start_vs + ray_len * ray_dir); - - vec3 ray_pos = ray_start_screen; - vec3 ray_dir_screen = ray_end_screen - ray_start_screen; - - // Make sure the ray does not leave the screen - float scale_max_x = min(1, 0.99 * (1.0 - ray_start_screen.x) / max(1e-5, ray_dir_screen.x)); - float scale_max_y = min(1, 0.99 * (1.0 - ray_start_screen.y) / max(1e-5, ray_dir_screen.y)); - float scale_min_x = min(1, 0.99 * ray_start_screen.x / max(1e-5, -ray_dir_screen.x)); - float scale_min_y = min(1, 0.99 * ray_start_screen.y / max(1e-5, -ray_dir_screen.y)); - ray_dir_screen *= min(scale_max_x, scale_max_y); - ray_dir_screen *= min(scale_min_x, scale_min_y); - - #if USE_LINEAR_DEPTH - // Linearize depth so we can interpolate it - ray_start_screen.z = get_linear_z_from_z(ray_start_screen.z); - ray_end_screen.z = get_linear_z_from_z(ray_end_screen.z); - #endif - - vec3 ray_step = (ray_end_screen - ray_start_screen) / num_steps; - - float distance_scale = 1.0 + 0.00001 * pixeldist; - - // Initial ray bias to avoid self intersection - // ray_pos += (15.0 + roughness * 0.0) * ray_step * float(num_steps) / 512.0 / clamp(dot(normal_vs, -view_dir), 0.1, 1.0) * GET_SETTING(ssr, intial_bias) * distance_scale; - - vec2 intersection = vec2(-1); - - // Jitter ray position to make sure we catch all details - float jitter = abs(rand(ivec2(gl_FragCoord.xy) % 8 + - (MainSceneData.frame_index % GET_SETTING(ssr, history_length)) * 0.1)); - - // Rough sourfaces need more jitter - jitter *= max(0.5, m.roughness); - - ray_pos += jitter * ray_step; - - int i = 0; - float intersection_weight = 0.0; - bool hit_factor = false; - - for (i = 1; i < num_steps; ++i) { - ray_pos += ray_step; - - // Current coordinate is in the mid of two samples, not at the end, so - // substract half of a step - vec2 curr_coord = ray_pos.xy - 0.5 * ray_step.xy; - - - // Increase ray bias as we advance the ray - float trace_len = GET_SETTING(ssr, intial_bias) * 10.0 + - 100.0 * distance_squared(curr_coord, texcoord); - trace_len *= 1.0 + 1.0 * roughness; - - // Check for intersection - #if USE_LINEAR_DEPTH - float depth_sample = textureLod(DownscaledDepth, curr_coord, 0).x; - #else - float depth_sample = textureLod(GBuffer.Depth, curr_coord, 0).x; - #endif - - if (point_between_planes(depth_sample, ray_pos.z, - ray_pos.z - ray_step.z, trace_len, hit_factor)) { - intersection = curr_coord; - break; - } - } - - // Make sure we hit exactly one pixel - intersection = truncate_coordinate(intersection); - - // Check if we hit something - if (min(intersection.x, intersection.y) <= 0.0 || out_of_screen(intersection)) { - result = vec2(0); - return; - } - - if (!hit_factor) { - result = vec2(0); - return; - } - result = intersection; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "includes/noise.inc.glsl" +#pragma include "includes/brdf.inc.glsl" +#pragma include "includes/transforms.inc.glsl" +#pragma include "includes/importance_sampling.inc.glsl" + +uniform sampler2D DownscaledDepth; + +out vec2 result; + +#define USE_LINEAR_DEPTH 0 +#define NUM_RAYDIR_RETRIES 3 + +const int num_steps = GET_SETTING(ssr, trace_steps); +const float hit_tolerance_ws = 0.00001 * GET_SETTING(ssr, hit_tolerance); +const float hit_tolerance_backface = 0.00001; + +bool point_between_planes(float z, float z_a, float z_b, float trace_length, out bool hit_factor) { + + // This traces correct, but looks weird because gaps are not filled + // return z + hit_tolerance_ws >= min(z_a, z_b) - 0.00015 && z - hit_tolerance_ws <= max(z_a, z_b); + + hit_factor = false; + + // This traces "incorrect", but looks better because gaps are getting filled then + if (z - hit_tolerance_ws * trace_length <= max(z_a, z_b)) { + + #if GET_SETTING(ssr, abort_on_object_infront) + hit_factor = (z + hit_tolerance_ws * trace_length) >= + min(z_a, z_b) - hit_tolerance_backface; + #else + hit_factor = true; + #endif + + return true; + } + + return false; +} + + +void main() +{ + ivec2 coord = ivec2(gl_FragCoord.xy); + vec2 texcoord = get_half_texcoord(); + + vec3 normal_vs = get_view_normal(texcoord); + Material m = unpack_material(GBuffer, texcoord); + normal_vs = world_normal_to_view(m.normal); + + vec3 ray_start_vs = get_view_pos_at(texcoord); + float pixeldist = distance(m.position, MainSceneData.camera_pos); + + // Skip skybox and distant pixels + if (pixeldist > 3000) { + result = vec2(0); + return; + } + + // Important: For clearcoat we trace the outer layer (with low roughness) + // instead of the high roughness layer + float roughness = get_effective_roughness(m); + + // Skip pixels with too high roughness + if (roughness > GET_SETTING(ssr, roughness_fade)) { + result = vec2(0); + return; + } + + + // Get ray start + vec3 view_dir = normalize(ray_start_vs); + vec3 ray_dir = normalize(reflect(view_dir, normal_vs)); + + // Get tangent and binormal + vec3 tangent, binormal; + find_arbitrary_tangent(ray_dir, tangent, binormal); + + vec3 importance_ray_dir = vec3(0); + + + // Generate ray directions until we find a value which is valid + int retry = 0; + for (;retry < NUM_RAYDIR_RETRIES; ++retry) { + + vec2 seed = texcoord + 1.3123 * retry + 0.176445 * (MainSceneData.frame_index % 32); + + // Get random sequence, should probably use halton or so + vec2 xi = clamp(abs(rand_rgb(seed).xy), vec2(0.01), vec2(0.99)); + + // Clamp brdf tail, see frostbite slides for details + const float brdf_bias = 0.8; + xi.y = mix(xi.y, 0.0, brdf_bias); + + // Get importance sampled directory + vec3 rho = importance_sample_ggx(xi, clamp(sqrt(roughness), 0.0001, 1.0)); + importance_ray_dir = normalize(1e-5 + rho.x * tangent + rho.y * binormal + rho.z * ray_dir); + + // If the ray dir is fine, abort, otherwise continue + if (dot(importance_ray_dir, normal_vs) > 0.005) { + break; + } + } + + // Still didn't find a good ray dir + if (retry >= NUM_RAYDIR_RETRIES - 1) { + result = vec2(0); + return; + } + + ray_dir = importance_ray_dir; + + // Ray not in view + if (dot(ray_dir, view_dir) < 1e-5) { + result = vec2(0); + return; + } + + float max_ray_len = 1.0 * pixeldist; + + // Clip ray to near plane + #if 1 + float ray_len = ((ray_start_vs.z + ray_dir.z * max_ray_len) > CAMERA_NEAR) ? + (CAMERA_NEAR - ray_start_vs.z) / ray_dir.z : + max_ray_len; + #else + float ray_len = max_ray_len; + #endif + + // Convert start and end pos from view to screen space + vec3 ray_start_screen = view_to_screen(ray_start_vs); + vec3 ray_end_screen = view_to_screen(ray_start_vs + ray_len * ray_dir); + + vec3 ray_pos = ray_start_screen; + vec3 ray_dir_screen = ray_end_screen - ray_start_screen; + + // Make sure the ray does not leave the screen + float scale_max_x = min(1, 0.99 * (1.0 - ray_start_screen.x) / max(1e-5, ray_dir_screen.x)); + float scale_max_y = min(1, 0.99 * (1.0 - ray_start_screen.y) / max(1e-5, ray_dir_screen.y)); + float scale_min_x = min(1, 0.99 * ray_start_screen.x / max(1e-5, -ray_dir_screen.x)); + float scale_min_y = min(1, 0.99 * ray_start_screen.y / max(1e-5, -ray_dir_screen.y)); + ray_dir_screen *= min(scale_max_x, scale_max_y); + ray_dir_screen *= min(scale_min_x, scale_min_y); + + #if USE_LINEAR_DEPTH + // Linearize depth so we can interpolate it + ray_start_screen.z = get_linear_z_from_z(ray_start_screen.z); + ray_end_screen.z = get_linear_z_from_z(ray_end_screen.z); + #endif + + vec3 ray_step = (ray_end_screen - ray_start_screen) / num_steps; + + float distance_scale = 1.0 + 0.00001 * pixeldist; + + // Initial ray bias to avoid self intersection + // ray_pos += (15.0 + roughness * 0.0) * ray_step * float(num_steps) / 512.0 / clamp(dot(normal_vs, -view_dir), 0.1, 1.0) * GET_SETTING(ssr, intial_bias) * distance_scale; + + vec2 intersection = vec2(-1); + + // Jitter ray position to make sure we catch all details + float jitter = abs(rand(ivec2(gl_FragCoord.xy) % 8 + + (MainSceneData.frame_index % GET_SETTING(ssr, history_length)) * 0.1)); + + // Rough sourfaces need more jitter + jitter *= max(0.5, m.roughness); + + ray_pos += jitter * ray_step; + + int i = 0; + float intersection_weight = 0.0; + bool hit_factor = false; + + for (i = 1; i < num_steps; ++i) { + ray_pos += ray_step; + + // Current coordinate is in the mid of two samples, not at the end, so + // substract half of a step + vec2 curr_coord = ray_pos.xy - 0.5 * ray_step.xy; + + + // Increase ray bias as we advance the ray + float trace_len = GET_SETTING(ssr, intial_bias) * 10.0 + + 100.0 * distance_squared(curr_coord, texcoord); + trace_len *= 1.0 + 1.0 * roughness; + + // Check for intersection + #if USE_LINEAR_DEPTH + float depth_sample = textureLod(DownscaledDepth, curr_coord, 0).x; + #else + float depth_sample = textureLod(GBuffer.Depth, curr_coord, 0).x; + #endif + + if (point_between_planes(depth_sample, ray_pos.z, + ray_pos.z - ray_step.z, trace_len, hit_factor)) { + intersection = curr_coord; + break; + } + } + + // Make sure we hit exactly one pixel + intersection = truncate_coordinate(intersection); + + // Check if we hit something + if (min(intersection.x, intersection.y) <= 0.0 || out_of_screen(intersection)) { + result = vec2(0); + return; + } + + if (!hit_factor) { + result = vec2(0); + return; + } + result = intersection; +} diff --git a/RenderPipelineFile/rpplugins/ssr/shader/upscale_bilateral_brdf.frag.glsl b/RenderPipelineFile/rpplugins/ssr/shader/upscale_bilateral_brdf.frag.glsl index a383c339..a4dc80c3 100644 --- a/RenderPipelineFile/rpplugins/ssr/shader/upscale_bilateral_brdf.frag.glsl +++ b/RenderPipelineFile/rpplugins/ssr/shader/upscale_bilateral_brdf.frag.glsl @@ -1,138 +1,138 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma optionNV (unroll all) - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "includes/brdf.inc.glsl" -#pragma include "includes/color_spaces.inc.glsl" - -layout(location = 0) out vec4 result; - -uniform sampler2D SourceTex; -uniform sampler2D LastFrameColor; -uniform GBufferData GBuffer; - -// Fades out a coordinate on the screen edges -float get_border_fade(vec2 coord) { - const float border_fade = max(1e-5, GET_SETTING(ssr, border_fade)); - float fade = 1.0; - fade *= saturate(coord.x / border_fade) * saturate(coord.y / border_fade); - fade *= saturate((1 - coord.x) / border_fade) * saturate((1 - coord.y) / border_fade); - return fade; -} - -void main() { - - // Get bilateral sample coordinates - ivec2 coord = ivec2(gl_FragCoord.xy); - ivec2 bil_start_coord = get_bilateral_coord(coord); - vec2 texcoord = ((bil_start_coord * 2) + 0.5) / SCREEN_SIZE; - - // Get current pixel data - float mid_depth = get_gbuffer_depth(GBuffer, coord); - vec3 ws_normal = get_gbuffer_normal(GBuffer, coord); - - Material m = unpack_material(GBuffer, texcoord); - - // Early out on skybox - if (is_skybox(m.position)) { - result = vec4(0); - return; - } - - const float max_depth_diff = 0.002; - - float weights = 0.0; - vec4 accum = vec4(0); - - // Controls how many other pixels should be taken into account, besides - // of the 4 direct neighbors. - const int search_radius = 0; - - vec3 view_vector = normalize(MainSceneData.camera_pos - m.position); - vec4 avg_position = vec4(0); - - // Get reflection directory - vec3 reflected_dir = get_reflection_vector(m, -view_vector); - float roughness = get_effective_roughness(m); - - // Accumulate all samples - for (int x = -search_radius; x < 2 + search_radius; ++x) { - for (int y = -search_radius; y < 2 + search_radius; ++y) { - - // Get sample coordinate - ivec2 source_coord = bil_start_coord + ivec2(x, y); - ivec2 screen_coord = source_coord * 2; - vec2 intersection = texelFetch(SourceTex, source_coord, 0).xy; - float intersection_weight = intersection.x > 1e-5 ? 1 : 0.0; - - // Skip empty samples - if (intersection_weight < 0.5) { - weights += 1.0; - continue; - } - - float sample_depth = get_gbuffer_depth(GBuffer, screen_coord); - - // Find depth of intersection pixel - float intersection_depth = get_gbuffer_depth(GBuffer, intersection); - - // Get world space position of intersection pixel - vec3 ws_intersection = calculate_surface_pos(intersection_depth, intersection); - - // Find distance and vector to intersection based on the pixels position - float distance_to_intersection = 1e-9 + distance(ws_intersection, m.position); - vec3 l = (ws_intersection - m.position) / distance_to_intersection; - - // Get the halfway vector - vec3 h = normalize(view_vector + l); - float NxH = max(1e-5, dot(m.normal, h)); - - float weight = clamp(brdf_distribution_ggx(NxH, 0.05 + sqrt(roughness)), 0.0, 1.0); - weight *= 1 - saturate(abs(mid_depth - sample_depth) / max_depth_diff); - vec4 color_sample = textureLod(LastFrameColor, intersection, 0); - - // Fade out samples at the screen border - // XXX: Do we really have to this per sample? - color_sample *= get_border_fade(intersection); - - accum += color_sample * weight; - weights += weight; - } - } - - if (weights < 0.4) { - accum = vec4(0); - } else { - accum /= weights; - } - - result = accum; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma optionNV (unroll all) + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "includes/brdf.inc.glsl" +#pragma include "includes/color_spaces.inc.glsl" + +layout(location = 0) out vec4 result; + +uniform sampler2D SourceTex; +uniform sampler2D LastFrameColor; +uniform GBufferData GBuffer; + +// Fades out a coordinate on the screen edges +float get_border_fade(vec2 coord) { + const float border_fade = max(1e-5, GET_SETTING(ssr, border_fade)); + float fade = 1.0; + fade *= saturate(coord.x / border_fade) * saturate(coord.y / border_fade); + fade *= saturate((1 - coord.x) / border_fade) * saturate((1 - coord.y) / border_fade); + return fade; +} + +void main() { + + // Get bilateral sample coordinates + ivec2 coord = ivec2(gl_FragCoord.xy); + ivec2 bil_start_coord = get_bilateral_coord(coord); + vec2 texcoord = ((bil_start_coord * 2) + 0.5) / SCREEN_SIZE; + + // Get current pixel data + float mid_depth = get_gbuffer_depth(GBuffer, coord); + vec3 ws_normal = get_gbuffer_normal(GBuffer, coord); + + Material m = unpack_material(GBuffer, texcoord); + + // Early out on skybox + if (is_skybox(m.position)) { + result = vec4(0); + return; + } + + const float max_depth_diff = 0.002; + + float weights = 0.0; + vec4 accum = vec4(0); + + // Controls how many other pixels should be taken into account, besides + // of the 4 direct neighbors. + const int search_radius = 0; + + vec3 view_vector = normalize(MainSceneData.camera_pos - m.position); + vec4 avg_position = vec4(0); + + // Get reflection directory + vec3 reflected_dir = get_reflection_vector(m, -view_vector); + float roughness = get_effective_roughness(m); + + // Accumulate all samples + for (int x = -search_radius; x < 2 + search_radius; ++x) { + for (int y = -search_radius; y < 2 + search_radius; ++y) { + + // Get sample coordinate + ivec2 source_coord = bil_start_coord + ivec2(x, y); + ivec2 screen_coord = source_coord * 2; + vec2 intersection = texelFetch(SourceTex, source_coord, 0).xy; + float intersection_weight = intersection.x > 1e-5 ? 1 : 0.0; + + // Skip empty samples + if (intersection_weight < 0.5) { + weights += 1.0; + continue; + } + + float sample_depth = get_gbuffer_depth(GBuffer, screen_coord); + + // Find depth of intersection pixel + float intersection_depth = get_gbuffer_depth(GBuffer, intersection); + + // Get world space position of intersection pixel + vec3 ws_intersection = calculate_surface_pos(intersection_depth, intersection); + + // Find distance and vector to intersection based on the pixels position + float distance_to_intersection = 1e-9 + distance(ws_intersection, m.position); + vec3 l = (ws_intersection - m.position) / distance_to_intersection; + + // Get the halfway vector + vec3 h = normalize(view_vector + l); + float NxH = max(1e-5, dot(m.normal, h)); + + float weight = clamp(brdf_distribution_ggx(NxH, 0.05 + sqrt(roughness)), 0.0, 1.0); + weight *= 1 - saturate(abs(mid_depth - sample_depth) / max_depth_diff); + vec4 color_sample = textureLod(LastFrameColor, intersection, 0); + + // Fade out samples at the screen border + // XXX: Do we really have to this per sample? + color_sample *= get_border_fade(intersection); + + accum += color_sample * weight; + weights += weight; + } + } + + if (weights < 0.4) { + accum = vec4(0); + } else { + accum /= weights; + } + + result = accum; +} diff --git a/RenderPipelineFile/rpplugins/ssr/ssr_stage.py b/RenderPipelineFile/rpplugins/ssr/ssr_stage.py index 90b702c4..857224b5 100644 --- a/RenderPipelineFile/rpplugins/ssr/ssr_stage.py +++ b/RenderPipelineFile/rpplugins/ssr/ssr_stage.py @@ -1,90 +1,90 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import SamplerState - -from rpcore.render_stage import RenderStage -from rpcore.stages.ambient_stage import AmbientStage - - -class SSRStage(RenderStage): - - """ This stage does the SSR pass """ - - required_inputs = [] - required_pipes = ["ShadedScene", "CombinedVelocity", "GBuffer", - "DownscaledDepth", "PreviousFrame::PostAmbientScene", - "PreviousFrame::SSRSpecular", "PreviousFrame::SceneDepth"] - - @property - def produced_pipes(self): - return {"SSRSpecular": self.target_resolve.color_tex} - - def create(self): - self.target = self.create_target("ComputeSSR") - self.target.size = -2 - self.target.add_color_attachment(bits=(16, 16, 0, 0)) - self.target.prepare_buffer() - - self.target.color_tex.set_minfilter(SamplerState.FT_nearest) - self.target.color_tex.set_magfilter(SamplerState.FT_nearest) - - self.target_velocity = self.create_target("ReflectionVelocity") - self.target_velocity.add_color_attachment(bits=(16, 16, 0, 0)) - self.target_velocity.prepare_buffer() - self.target_velocity.set_shader_input("TraceResult", self.target.color_tex) - - self.target_reproject_lighting = self.create_target("CopyLighting") - self.target_reproject_lighting.add_color_attachment(bits=16, alpha=True) - self.target_reproject_lighting.prepare_buffer() - - self.target_upscale = self.create_target("UpscaleSSR") - self.target_upscale.add_color_attachment(bits=16, alpha=True) - self.target_upscale.prepare_buffer() - self.target_upscale.set_shader_inputs( - SourceTex=self.target.color_tex, - LastFrameColor=self.target_reproject_lighting.color_tex) - - self.target_resolve = self.create_target("ResolveSSR") - self.target_resolve.add_color_attachment(bits=16, alpha=True) - self.target_resolve.prepare_buffer() - self.target_resolve.set_shader_inputs( - CurrentTex=self.target_upscale.color_tex, - VelocityTex=self.target_velocity.color_tex) - - AmbientStage.required_pipes.append("SSRSpecular") - - def reload_shaders(self): - self.target.shader = self.load_plugin_shader( - "ssr_trace.frag.glsl") - self.target_velocity.shader = self.load_plugin_shader( - "reflection_velocity.frag.glsl") - self.target_reproject_lighting.shader = self.load_plugin_shader( - "reproject_lighting.frag.glsl") - self.target_upscale.shader = self.load_plugin_shader( - "upscale_bilateral_brdf.frag.glsl") - self.target_resolve.shader = self.load_plugin_shader( - "resolve_ssr.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import SamplerState + +from rpcore.render_stage import RenderStage +from rpcore.stages.ambient_stage import AmbientStage + + +class SSRStage(RenderStage): + + """ This stage does the SSR pass """ + + required_inputs = [] + required_pipes = ["ShadedScene", "CombinedVelocity", "GBuffer", + "DownscaledDepth", "PreviousFrame::PostAmbientScene", + "PreviousFrame::SSRSpecular", "PreviousFrame::SceneDepth"] + + @property + def produced_pipes(self): + return {"SSRSpecular": self.target_resolve.color_tex} + + def create(self): + self.target = self.create_target("ComputeSSR") + self.target.size = -2 + self.target.add_color_attachment(bits=(16, 16, 0, 0)) + self.target.prepare_buffer() + + self.target.color_tex.set_minfilter(SamplerState.FT_nearest) + self.target.color_tex.set_magfilter(SamplerState.FT_nearest) + + self.target_velocity = self.create_target("ReflectionVelocity") + self.target_velocity.add_color_attachment(bits=(16, 16, 0, 0)) + self.target_velocity.prepare_buffer() + self.target_velocity.set_shader_input("TraceResult", self.target.color_tex) + + self.target_reproject_lighting = self.create_target("CopyLighting") + self.target_reproject_lighting.add_color_attachment(bits=16, alpha=True) + self.target_reproject_lighting.prepare_buffer() + + self.target_upscale = self.create_target("UpscaleSSR") + self.target_upscale.add_color_attachment(bits=16, alpha=True) + self.target_upscale.prepare_buffer() + self.target_upscale.set_shader_inputs( + SourceTex=self.target.color_tex, + LastFrameColor=self.target_reproject_lighting.color_tex) + + self.target_resolve = self.create_target("ResolveSSR") + self.target_resolve.add_color_attachment(bits=16, alpha=True) + self.target_resolve.prepare_buffer() + self.target_resolve.set_shader_inputs( + CurrentTex=self.target_upscale.color_tex, + VelocityTex=self.target_velocity.color_tex) + + AmbientStage.required_pipes.append("SSRSpecular") + + def reload_shaders(self): + self.target.shader = self.load_plugin_shader( + "ssr_trace.frag.glsl") + self.target_velocity.shader = self.load_plugin_shader( + "reflection_velocity.frag.glsl") + self.target_reproject_lighting.shader = self.load_plugin_shader( + "reproject_lighting.frag.glsl") + self.target_upscale.shader = self.load_plugin_shader( + "upscale_bilateral_brdf.frag.glsl") + self.target_resolve.shader = self.load_plugin_shader( + "resolve_ssr.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/volumetrics/config.yaml b/RenderPipelineFile/rpplugins/volumetrics/config.yaml index defda349..f93af72e 100644 --- a/RenderPipelineFile/rpplugins/volumetrics/config.yaml +++ b/RenderPipelineFile/rpplugins/volumetrics/config.yaml @@ -1,114 +1,114 @@ - -settings: !!omap - - - enable_volumetric_shadows: - type: bool - default: true - label: Volumetric Shadows - description: > - Whether to enable volumetric shadows - - - volumetric_shadow_intensity: - display_if: { enable_volumetric_shadows: true } - type: float - range: [0.1, 10.0] - default: 1.0 - shader_runtime: true - label: Intensity - description: > - Controls the intensity/density of the volumetric shadows - - - volumetric_shadow_brightness: - display_if: { enable_volumetric_shadows: true } - type: float - range: [0.1, 10.0] - default: 1.0 - shader_runtime: true - label: Brightness - description: > - Controls the brightness of the volumetric shadows. - - - volumetric_shadow_pow: - display_if: { enable_volumetric_shadows: true } - type: float - range: [0.25, 4.0] - default: 1.0 - shader_runtime: true - label: Detail - description: > - Decreases/Increases the detail of the volumetric shadows. The shadow is computed as pow(shadow, detail). - - - volumetric_max_distance: - display_if: { enable_volumetric_shadows: true } - type: float - range: [1.0, 500.0] - default: 150.0 - shader_runtime: true - label: Maximum distance - description: > - Maximum distance until volumetric shadows is computed, after that - distance no "godrays" are visible. Lower values provide more details. - - - volumetric_shadow_fadein_distance: - display_if: { enable_volumetric_shadows: true } - type: float - range: [1.0, 100.0] - default: 15.0 - shader_runtime: true - label: Fade-In Distance - description: > - Volumetric shadows are faded out near to the camera, this controls - the distance at which this happens. - - - - volumetric_num_steps: - display_if: { enable_volumetric_shadows: true} - type: int - range: [4, 128] - default: 32 - shader_runtime: true - label: Sample count - description: > - Controls how many samples to take along the path, higher values require - more performance but produce more detailed results. - - -daytime_settings: !!omap - - - fog_ramp_size: - type: scalar - unit: meter - range: [0.5, 30000.0] - default: 2000 - label: Fog Ramp - logarithmic_factor: 2.0 - description: > - Size of the transition from no fog to fog in world space units. - Higher values produce a smoother transition from no fog to fog, - while lower values will produce a more sharp transition. - - - fog_color: - type: color - default: [127, 127, 127] - label: Fog Color - description: > - Color of the fog, usually should match the atmosphere color - - - fog_brightness: - type: scalar - unit: klux - range: [0.0, 20.0] - logarithmic_factor: 2.0 - default: 12.0 - label: Fog Brightness - description: > - Controls the brightness of the fog, should match the sun intensity. - - - fog_intensity: - type: scalar - unit: none - range: [0.01, 50.0] - default: 30.0 - label: Fog Intensity - description: > - Controls the intensity/amount of the volumetric height fog. + +settings: !!omap + + - enable_volumetric_shadows: + type: bool + default: true + label: Volumetric Shadows + description: > + Whether to enable volumetric shadows + + - volumetric_shadow_intensity: + display_if: { enable_volumetric_shadows: true } + type: float + range: [0.1, 10.0] + default: 1.0 + shader_runtime: true + label: Intensity + description: > + Controls the intensity/density of the volumetric shadows + + - volumetric_shadow_brightness: + display_if: { enable_volumetric_shadows: true } + type: float + range: [0.1, 10.0] + default: 1.0 + shader_runtime: true + label: Brightness + description: > + Controls the brightness of the volumetric shadows. + + - volumetric_shadow_pow: + display_if: { enable_volumetric_shadows: true } + type: float + range: [0.25, 4.0] + default: 1.0 + shader_runtime: true + label: Detail + description: > + Decreases/Increases the detail of the volumetric shadows. The shadow is computed as pow(shadow, detail). + + - volumetric_max_distance: + display_if: { enable_volumetric_shadows: true } + type: float + range: [1.0, 500.0] + default: 150.0 + shader_runtime: true + label: Maximum distance + description: > + Maximum distance until volumetric shadows is computed, after that + distance no "godrays" are visible. Lower values provide more details. + + - volumetric_shadow_fadein_distance: + display_if: { enable_volumetric_shadows: true } + type: float + range: [1.0, 100.0] + default: 15.0 + shader_runtime: true + label: Fade-In Distance + description: > + Volumetric shadows are faded out near to the camera, this controls + the distance at which this happens. + + + - volumetric_num_steps: + display_if: { enable_volumetric_shadows: true} + type: int + range: [4, 128] + default: 32 + shader_runtime: true + label: Sample count + description: > + Controls how many samples to take along the path, higher values require + more performance but produce more detailed results. + + +daytime_settings: !!omap + + - fog_ramp_size: + type: scalar + unit: meter + range: [0.5, 30000.0] + default: 2000 + label: Fog Ramp + logarithmic_factor: 2.0 + description: > + Size of the transition from no fog to fog in world space units. + Higher values produce a smoother transition from no fog to fog, + while lower values will produce a more sharp transition. + + - fog_color: + type: color + default: [127, 127, 127] + label: Fog Color + description: > + Color of the fog, usually should match the atmosphere color + + - fog_brightness: + type: scalar + unit: klux + range: [0.0, 20.0] + logarithmic_factor: 2.0 + default: 12.0 + label: Fog Brightness + description: > + Controls the brightness of the fog, should match the sun intensity. + + - fog_intensity: + type: scalar + unit: none + range: [0.01, 50.0] + default: 30.0 + label: Fog Intensity + description: > + Controls the intensity/amount of the volumetric height fog. diff --git a/RenderPipelineFile/rpplugins/volumetrics/plugin.py b/RenderPipelineFile/rpplugins/volumetrics/plugin.py index cf856a92..f65b66ee 100644 --- a/RenderPipelineFile/rpplugins/volumetrics/plugin.py +++ b/RenderPipelineFile/rpplugins/volumetrics/plugin.py @@ -1,47 +1,47 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from rpcore.pluginbase.base_plugin import BasePlugin -from .volumetrics_stage import VolumetricsStage - - -class Plugin(BasePlugin): - - name = "Volumetric Lighting" - author = "tobspr " - description = ("This plugins adds support for volumetric lighting") - version = "0.1 alpha (!)" - - required_plugins = ("pssm",) - - def on_stage_setup(self): - self.stage = self.create_stage(VolumetricsStage) - self.stage.enable_volumetric_shadows = self.get_setting("enable_volumetric_shadows") - - if self.get_plugin_instance("pssm").get_setting("use_pcf"): - self.stage.required_pipes.append("PSSMShadowAtlasPCF") - else: - self.stage.required_pipes.append("PSSMShadowAtlas") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from rpcore.pluginbase.base_plugin import BasePlugin +from .volumetrics_stage import VolumetricsStage + + +class Plugin(BasePlugin): + + name = "Volumetric Lighting" + author = "tobspr " + description = ("This plugins adds support for volumetric lighting") + version = "0.1 alpha (!)" + + required_plugins = ("pssm",) + + def on_stage_setup(self): + self.stage = self.create_stage(VolumetricsStage) + self.stage.enable_volumetric_shadows = self.get_setting("enable_volumetric_shadows") + + if self.get_plugin_instance("pssm").get_setting("use_pcf"): + self.stage.required_pipes.append("PSSMShadowAtlasPCF") + else: + self.stage.required_pipes.append("PSSMShadowAtlas") diff --git a/RenderPipelineFile/rpplugins/volumetrics/shader/apply_volumetrics.frag.glsl b/RenderPipelineFile/rpplugins/volumetrics/shader/apply_volumetrics.frag.glsl index e3f0db00..f32db98e 100644 --- a/RenderPipelineFile/rpplugins/volumetrics/shader/apply_volumetrics.frag.glsl +++ b/RenderPipelineFile/rpplugins/volumetrics/shader/apply_volumetrics.frag.glsl @@ -1,83 +1,83 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#define USE_TIME_OF_DAY 1 -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" - -uniform sampler2D ShadedScene; - -#if GET_SETTING(volumetrics, enable_volumetric_shadows) - uniform sampler2D VolumetricsTex; -#endif - -out vec3 result; - -float compute_fog(vec3 ray_start, vec3 ray_end) -{ - float dist = length(ray_start - ray_end); - vec3 ray_dir = (ray_start - ray_end) / dist; - const float c = 1.0 * TimeOfDay.volumetrics.fog_intensity; - const float b = 0.2 / TimeOfDay.volumetrics.fog_ramp_size; - ray_dir.z = -ray_dir.z; - if (abs(ray_dir.z) < 1e-2) - ray_dir.z = 1e-2; - return saturate(c * exp(-ray_start.z * b) * (1.0 - exp(-dist * ray_dir.z * b)) / ray_dir.z); -} - -void main() { - vec2 texcoord = get_texcoord(); - - float depth = get_depth_at(texcoord); - vec3 surface_pos = calculate_surface_pos(depth, texcoord); - - #if GET_SETTING(volumetrics, enable_volumetric_shadows) - vec4 volumetrics = textureLod(VolumetricsTex, texcoord, 0); - #else - vec4 volumetrics = vec4(0); - #endif - - vec3 scene_color = textureLod(ShadedScene, texcoord, 0).xyz; - - - vec3 fog_color = TimeOfDay.volumetrics.fog_color * 4.5 * TimeOfDay.volumetrics.fog_brightness * TimeOfDay.scattering.sun_intensity; - - vec3 ray_dir = normalize(MainSceneData.camera_pos - surface_pos); - - float fog_weight = compute_fog(MainSceneData.camera_pos, surface_pos); - - vec3 merged_color = scene_color; - #if !DEBUG_MODE - merged_color = mix(scene_color, fog_color, fog_weight); - merged_color = mix(merged_color, volumetrics.xyz, volumetrics.w); - #endif - - - result = merged_color; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#define USE_TIME_OF_DAY 1 +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" + +uniform sampler2D ShadedScene; + +#if GET_SETTING(volumetrics, enable_volumetric_shadows) + uniform sampler2D VolumetricsTex; +#endif + +out vec3 result; + +float compute_fog(vec3 ray_start, vec3 ray_end) +{ + float dist = length(ray_start - ray_end); + vec3 ray_dir = (ray_start - ray_end) / dist; + const float c = 1.0 * TimeOfDay.volumetrics.fog_intensity; + const float b = 0.2 / TimeOfDay.volumetrics.fog_ramp_size; + ray_dir.z = -ray_dir.z; + if (abs(ray_dir.z) < 1e-2) + ray_dir.z = 1e-2; + return saturate(c * exp(-ray_start.z * b) * (1.0 - exp(-dist * ray_dir.z * b)) / ray_dir.z); +} + +void main() { + vec2 texcoord = get_texcoord(); + + float depth = get_depth_at(texcoord); + vec3 surface_pos = calculate_surface_pos(depth, texcoord); + + #if GET_SETTING(volumetrics, enable_volumetric_shadows) + vec4 volumetrics = textureLod(VolumetricsTex, texcoord, 0); + #else + vec4 volumetrics = vec4(0); + #endif + + vec3 scene_color = textureLod(ShadedScene, texcoord, 0).xyz; + + + vec3 fog_color = TimeOfDay.volumetrics.fog_color * 4.5 * TimeOfDay.volumetrics.fog_brightness * TimeOfDay.scattering.sun_intensity; + + vec3 ray_dir = normalize(MainSceneData.camera_pos - surface_pos); + + float fog_weight = compute_fog(MainSceneData.camera_pos, surface_pos); + + vec3 merged_color = scene_color; + #if !DEBUG_MODE + merged_color = mix(scene_color, fog_color, fog_weight); + merged_color = mix(merged_color, volumetrics.xyz, volumetrics.w); + #endif + + + result = merged_color; +} diff --git a/RenderPipelineFile/rpplugins/volumetrics/shader/compute_volumetric_shadows.frag.glsl b/RenderPipelineFile/rpplugins/volumetrics/shader/compute_volumetric_shadows.frag.glsl index 1c95388d..2b7cf40b 100644 --- a/RenderPipelineFile/rpplugins/volumetrics/shader/compute_volumetric_shadows.frag.glsl +++ b/RenderPipelineFile/rpplugins/volumetrics/shader/compute_volumetric_shadows.frag.glsl @@ -1,134 +1,134 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#define USE_TIME_OF_DAY 1 -#define USE_GBUFFER_EXTENSIONS -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "includes/shadows.inc.glsl" -#pragma include "includes/noise.inc.glsl" - - -#if GET_SETTING(pssm, use_pcf) - uniform sampler2DShadow PSSMShadowAtlasPCF; -#else - uniform sampler2D PSSMShadowAtlas; -#endif - - -#pragma include "/$$rp/rpplugins/pssm/shader/filter_pssm.inc.glsl" - -uniform sampler2D ShadedScene; - -uniform sampler2D PSSMShadowAtlas; -uniform mat4 pssm_mvps[GET_SETTING(pssm, split_count)]; -uniform vec2 pssm_nearfar[GET_SETTING(pssm, split_count)]; - -out vec4 result; - - -void main() { - - #if DEBUG_MODE - result = vec4(0); - return; - #endif - - vec2 texcoord = get_half_texcoord(); - - vec3 start_pos = MainSceneData.camera_pos; - vec3 end_pos = get_gbuffer_position(GBuffer, texcoord); - - // Looks weird - // if (is_skybox(end_pos, MainSceneData.camera_pos)) - // { - // result = vec4(0); - // return; - // } - - float max_distance = GET_SETTING(volumetrics, volumetric_max_distance); - - vec3 step_vector = end_pos - start_pos; - if (length(step_vector) > max_distance) { - step_vector = normalize(step_vector) * max_distance; - } - - float total_distance = length(step_vector); - - end_pos = start_pos + step_vector; - - float jitter = rand(ivec2(gl_FragCoord.xy) % 2); - jitter = 0; - - const int num_steps = GET_SETTING(volumetrics, volumetric_num_steps); - vec3 step_offs = step_vector / num_steps; - - float volumetrics = 0.0; - - vec3 sun_vector = get_sun_vector(); - vec3 sun_color = get_sun_color() * get_sun_color_scale(sun_vector); - - const float distance_fade = GET_SETTING(volumetrics, volumetric_shadow_fadein_distance); - - int start_split = 0; - const float fixed_bias = 0.0005; - - for (int i = 0; i < num_steps; ++i) { - vec3 pos = start_pos + (i + jitter) * step_offs; - vec3 proj = project(pssm_mvps[start_split], pos); - - // Check if out of split - while (out_of_screen(proj.xy) && start_split < GET_SETTING(pssm, split_count) - 1) { - ++start_split; - proj = project(pssm_mvps[start_split], pos); - } - - if (out_of_screen(proj.xy)) { - // Out of pssm range - break; - } - - float sun_influence = get_shadow(get_split_coord(proj.xy, start_split), proj.z - get_fixed_bias(start_split)); - - // Apply distance fade - float d = float(i) / float(num_steps) * total_distance; - volumetrics += 0.02 * sun_influence * smoothstep(0, 1, d / distance_fade) * (1 - volumetrics); - } - - - // volumetrics /= float(num_steps); - volumetrics = saturate(volumetrics); - volumetrics = pow(volumetrics, GET_SETTING(volumetrics, volumetric_shadow_pow)); - - vec3 color = sun_color * 0.5 * GET_SETTING(volumetrics, volumetric_shadow_brightness); - result = vec4(color, 0.01 * GET_SETTING(volumetrics, volumetric_shadow_intensity)) * volumetrics; - result.w = saturate(result.w); - - - -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#define USE_TIME_OF_DAY 1 +#define USE_GBUFFER_EXTENSIONS +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "includes/shadows.inc.glsl" +#pragma include "includes/noise.inc.glsl" + + +#if GET_SETTING(pssm, use_pcf) + uniform sampler2DShadow PSSMShadowAtlasPCF; +#else + uniform sampler2D PSSMShadowAtlas; +#endif + + +#pragma include "/$$rp/rpplugins/pssm/shader/filter_pssm.inc.glsl" + +uniform sampler2D ShadedScene; + +uniform sampler2D PSSMShadowAtlas; +uniform mat4 pssm_mvps[GET_SETTING(pssm, split_count)]; +uniform vec2 pssm_nearfar[GET_SETTING(pssm, split_count)]; + +out vec4 result; + + +void main() { + + #if DEBUG_MODE + result = vec4(0); + return; + #endif + + vec2 texcoord = get_half_texcoord(); + + vec3 start_pos = MainSceneData.camera_pos; + vec3 end_pos = get_gbuffer_position(GBuffer, texcoord); + + // Looks weird + // if (is_skybox(end_pos, MainSceneData.camera_pos)) + // { + // result = vec4(0); + // return; + // } + + float max_distance = GET_SETTING(volumetrics, volumetric_max_distance); + + vec3 step_vector = end_pos - start_pos; + if (length(step_vector) > max_distance) { + step_vector = normalize(step_vector) * max_distance; + } + + float total_distance = length(step_vector); + + end_pos = start_pos + step_vector; + + float jitter = rand(ivec2(gl_FragCoord.xy) % 2); + jitter = 0; + + const int num_steps = GET_SETTING(volumetrics, volumetric_num_steps); + vec3 step_offs = step_vector / num_steps; + + float volumetrics = 0.0; + + vec3 sun_vector = get_sun_vector(); + vec3 sun_color = get_sun_color() * get_sun_color_scale(sun_vector); + + const float distance_fade = GET_SETTING(volumetrics, volumetric_shadow_fadein_distance); + + int start_split = 0; + const float fixed_bias = 0.0005; + + for (int i = 0; i < num_steps; ++i) { + vec3 pos = start_pos + (i + jitter) * step_offs; + vec3 proj = project(pssm_mvps[start_split], pos); + + // Check if out of split + while (out_of_screen(proj.xy) && start_split < GET_SETTING(pssm, split_count) - 1) { + ++start_split; + proj = project(pssm_mvps[start_split], pos); + } + + if (out_of_screen(proj.xy)) { + // Out of pssm range + break; + } + + float sun_influence = get_shadow(get_split_coord(proj.xy, start_split), proj.z - get_fixed_bias(start_split)); + + // Apply distance fade + float d = float(i) / float(num_steps) * total_distance; + volumetrics += 0.02 * sun_influence * smoothstep(0, 1, d / distance_fade) * (1 - volumetrics); + } + + + // volumetrics /= float(num_steps); + volumetrics = saturate(volumetrics); + volumetrics = pow(volumetrics, GET_SETTING(volumetrics, volumetric_shadow_pow)); + + vec3 color = sun_color * 0.5 * GET_SETTING(volumetrics, volumetric_shadow_brightness); + result = vec4(color, 0.01 * GET_SETTING(volumetrics, volumetric_shadow_intensity)) * volumetrics; + result.w = saturate(result.w); + + + +} diff --git a/RenderPipelineFile/rpplugins/volumetrics/volumetrics_stage.py b/RenderPipelineFile/rpplugins/volumetrics/volumetrics_stage.py index d1801225..5a926645 100644 --- a/RenderPipelineFile/rpplugins/volumetrics/volumetrics_stage.py +++ b/RenderPipelineFile/rpplugins/volumetrics/volumetrics_stage.py @@ -1,74 +1,74 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from panda3d.core import Vec2 -from rpcore.render_stage import RenderStage - - -class VolumetricsStage(RenderStage): - - """ This stage applies the volumetric lighting """ - - required_inputs = [] - required_pipes = ["ShadedScene", "GBuffer"] - - def __init__(self, pipeline): - RenderStage.__init__(self, pipeline) - self.enable_volumetric_shadows = False - - @property - def produced_pipes(self): - return {"ShadedScene": self.target_combine.color_tex} - - def create(self): - - if self.enable_volumetric_shadows: - self.target = self.create_target("ComputeVolumetrics") - self.target.size = -2 - self.target.add_color_attachment(bits=16, alpha=True) - self.target.prepare_buffer() - - self.target_upscale = self.create_target("Upscale") - self.target_upscale.add_color_attachment(bits=16, alpha=True) - self.target_upscale.prepare_buffer() - - self.target_upscale.set_shader_inputs( - SourceTex=self.target.color_tex, - upscaleWeights=Vec2(0.001, 0.001)) - - self.target_combine = self.create_target("CombineVolumetrics") - self.target_combine.add_color_attachment(bits=16) - self.target_combine.prepare_buffer() - - if self.enable_volumetric_shadows: - self.target_combine.set_shader_input("VolumetricsTex", self.target_upscale.color_tex) - - def reload_shaders(self): - if self.enable_volumetric_shadows: - self.target.shader = self.load_plugin_shader("compute_volumetric_shadows.frag.glsl") - self.target_upscale.shader = self.load_plugin_shader( - "/$$rp/shader/bilateral_upscale.frag.glsl") - self.target_combine.shader = self.load_plugin_shader("apply_volumetrics.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from panda3d.core import Vec2 +from rpcore.render_stage import RenderStage + + +class VolumetricsStage(RenderStage): + + """ This stage applies the volumetric lighting """ + + required_inputs = [] + required_pipes = ["ShadedScene", "GBuffer"] + + def __init__(self, pipeline): + RenderStage.__init__(self, pipeline) + self.enable_volumetric_shadows = False + + @property + def produced_pipes(self): + return {"ShadedScene": self.target_combine.color_tex} + + def create(self): + + if self.enable_volumetric_shadows: + self.target = self.create_target("ComputeVolumetrics") + self.target.size = -2 + self.target.add_color_attachment(bits=16, alpha=True) + self.target.prepare_buffer() + + self.target_upscale = self.create_target("Upscale") + self.target_upscale.add_color_attachment(bits=16, alpha=True) + self.target_upscale.prepare_buffer() + + self.target_upscale.set_shader_inputs( + SourceTex=self.target.color_tex, + upscaleWeights=Vec2(0.001, 0.001)) + + self.target_combine = self.create_target("CombineVolumetrics") + self.target_combine.add_color_attachment(bits=16) + self.target_combine.prepare_buffer() + + if self.enable_volumetric_shadows: + self.target_combine.set_shader_input("VolumetricsTex", self.target_upscale.color_tex) + + def reload_shaders(self): + if self.enable_volumetric_shadows: + self.target.shader = self.load_plugin_shader("compute_volumetric_shadows.frag.glsl") + self.target_upscale.shader = self.load_plugin_shader( + "/$$rp/shader/bilateral_upscale.frag.glsl") + self.target_combine.shader = self.load_plugin_shader("apply_volumetrics.frag.glsl") diff --git a/RenderPipelineFile/rpplugins/vxgi/config.yaml b/RenderPipelineFile/rpplugins/vxgi/config.yaml index 2530ffef..bb6e6d63 100644 --- a/RenderPipelineFile/rpplugins/vxgi/config.yaml +++ b/RenderPipelineFile/rpplugins/vxgi/config.yaml @@ -1,48 +1,48 @@ - -settings: !!omap - - - grid_resolution: - type: power_of_two - range: [32, 384] - default: 256 - label: Voxel Grid Resolution - description: > - Resolution of the voxel grid. This highly impacts performance and - memory requirements. A grid of 32^3 almost requires no memory, while - a grid of 512^3 requires about 1,0 GB of VRAM. However, the higher - the grid resolution, the more detail will be visible in the GI. - Power-of-two sizes are preferred. - - - grid_ws_size: - type: float - range: [5.0, 150.0] - default: 100.0 - label: Worlspace Grid Size - description: > - This controls the dimensions of the voxel grid. The voxel grid is - centered arround the camera, and has an equal size in each dimension. - This setting controls the size of the grid in world space. A size of - 40.0 for example makes the grid 80x80x80 world-space units big. - - - diffuse_cone_steps: - type: int - range: [2, 32] - default: 32 - shader_runtime: true - label: Diffuse Cone Steps - description: > - Amount of steps to trace for each diffuse cone. A higher amount will - lead to more color bleeding, however it also affects performance. - - - specular_cone_steps: - type: int - range: [2, 150] - default: 150 - shader_runtime: true - label: Specular Cone Steps - description: > - Amount of steps to trace for the specular cone. A higher amount will - cause reflections to range further, however it also affects performance. - - -daytime_settings: + +settings: !!omap + + - grid_resolution: + type: power_of_two + range: [32, 384] + default: 256 + label: Voxel Grid Resolution + description: > + Resolution of the voxel grid. This highly impacts performance and + memory requirements. A grid of 32^3 almost requires no memory, while + a grid of 512^3 requires about 1,0 GB of VRAM. However, the higher + the grid resolution, the more detail will be visible in the GI. + Power-of-two sizes are preferred. + + - grid_ws_size: + type: float + range: [5.0, 150.0] + default: 100.0 + label: Worlspace Grid Size + description: > + This controls the dimensions of the voxel grid. The voxel grid is + centered arround the camera, and has an equal size in each dimension. + This setting controls the size of the grid in world space. A size of + 40.0 for example makes the grid 80x80x80 world-space units big. + + - diffuse_cone_steps: + type: int + range: [2, 32] + default: 32 + shader_runtime: true + label: Diffuse Cone Steps + description: > + Amount of steps to trace for each diffuse cone. A higher amount will + lead to more color bleeding, however it also affects performance. + + - specular_cone_steps: + type: int + range: [2, 150] + default: 150 + shader_runtime: true + label: Specular Cone Steps + description: > + Amount of steps to trace for the specular cone. A higher amount will + cause reflections to range further, however it also affects performance. + + +daytime_settings: diff --git a/RenderPipelineFile/rpplugins/vxgi/plugin.py b/RenderPipelineFile/rpplugins/vxgi/plugin.py index 0bcc9546..3fe210fe 100644 --- a/RenderPipelineFile/rpplugins/vxgi/plugin.py +++ b/RenderPipelineFile/rpplugins/vxgi/plugin.py @@ -1,97 +1,97 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import division - -import collections - -from rpcore.globals import Globals -from rpcore.pluginbase.base_plugin import BasePlugin - -from .voxelization_stage import VoxelizationStage -from .vxgi_stage import VXGIStage - - -class Plugin(BasePlugin): - - name = "Voxel Global Illumination" - author = "tobspr " - description = ("Provides Global Illumination using Voxel Cone Tracing. This " - "technique is still very unoptimized and experimental!") - version = "alpha (!)" - - def on_stage_setup(self): - self._voxel_stage = self.create_stage(VoxelizationStage) - self._vxgi_stage = self.create_stage(VXGIStage) - - self._voxel_stage.voxel_resolution = self.get_setting("grid_resolution") - self._voxel_stage.voxel_world_size = self.get_setting("grid_ws_size") - - if self.is_plugin_enabled("pssm"): - # Add shadow map as requirement - self._voxel_stage.required_pipes.append("PSSMSceneSunShadowMapPCF") - self._voxel_stage.required_inputs.append("PSSMSceneSunShadowMVP") - - def on_pre_render_update(self): - task = self._queue[0] - self._queue.rotate(-1) - task() - - def on_pipeline_created(self): - self._queue = collections.deque() - self._queue.extend([self._voxelize_x, self._voxelize_y, self._voxelize_z]) - self._queue.extend([self._generate_mipmaps]) - - def _set_grid_pos(self): - """ Finds the new voxel grid position """ - grid_pos = Globals.base.camera.get_pos(Globals.base.render) - - # Snap the voxel grid - voxel_size = 2.0 * self.get_setting("grid_ws_size") / self.get_setting("grid_resolution") - snap_size = voxel_size * 2**4 - - for dimension in range(3): - cell_val = grid_pos.get_cell(dimension) - grid_pos.set_cell(dimension, cell_val - cell_val % snap_size) - - self._voxel_stage.set_grid_position(grid_pos) - - def _voxelize_x(self): - """ Voxelizes the scene from the x axis """ - self._set_grid_pos() - self._voxel_stage.state = VoxelizationStage.S_voxelize_x - - def _voxelize_y(self): - """ Voxelizes the scene from the y axis """ - self._voxel_stage.state = VoxelizationStage.S_voxelize_y - - def _voxelize_z(self): - """ Voxelizes the scene from the z axis """ - self._voxel_stage.state = VoxelizationStage.S_voxelize_z - - def _generate_mipmaps(self): - """ Generates the mipmaps for the voxel grid """ - self._voxel_stage.state = VoxelizationStage.S_gen_mipmaps +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import division + +import collections + +from rpcore.globals import Globals +from rpcore.pluginbase.base_plugin import BasePlugin + +from .voxelization_stage import VoxelizationStage +from .vxgi_stage import VXGIStage + + +class Plugin(BasePlugin): + + name = "Voxel Global Illumination" + author = "tobspr " + description = ("Provides Global Illumination using Voxel Cone Tracing. This " + "technique is still very unoptimized and experimental!") + version = "alpha (!)" + + def on_stage_setup(self): + self._voxel_stage = self.create_stage(VoxelizationStage) + self._vxgi_stage = self.create_stage(VXGIStage) + + self._voxel_stage.voxel_resolution = self.get_setting("grid_resolution") + self._voxel_stage.voxel_world_size = self.get_setting("grid_ws_size") + + if self.is_plugin_enabled("pssm"): + # Add shadow map as requirement + self._voxel_stage.required_pipes.append("PSSMSceneSunShadowMapPCF") + self._voxel_stage.required_inputs.append("PSSMSceneSunShadowMVP") + + def on_pre_render_update(self): + task = self._queue[0] + self._queue.rotate(-1) + task() + + def on_pipeline_created(self): + self._queue = collections.deque() + self._queue.extend([self._voxelize_x, self._voxelize_y, self._voxelize_z]) + self._queue.extend([self._generate_mipmaps]) + + def _set_grid_pos(self): + """ Finds the new voxel grid position """ + grid_pos = Globals.base.camera.get_pos(Globals.base.render) + + # Snap the voxel grid + voxel_size = 2.0 * self.get_setting("grid_ws_size") / self.get_setting("grid_resolution") + snap_size = voxel_size * 2**4 + + for dimension in range(3): + cell_val = grid_pos.get_cell(dimension) + grid_pos.set_cell(dimension, cell_val - cell_val % snap_size) + + self._voxel_stage.set_grid_position(grid_pos) + + def _voxelize_x(self): + """ Voxelizes the scene from the x axis """ + self._set_grid_pos() + self._voxel_stage.state = VoxelizationStage.S_voxelize_x + + def _voxelize_y(self): + """ Voxelizes the scene from the y axis """ + self._voxel_stage.state = VoxelizationStage.S_voxelize_y + + def _voxelize_z(self): + """ Voxelizes the scene from the z axis """ + self._voxel_stage.state = VoxelizationStage.S_voxelize_z + + def _generate_mipmaps(self): + """ Generates the mipmaps for the voxel grid """ + self._voxel_stage.state = VoxelizationStage.S_gen_mipmaps diff --git a/RenderPipelineFile/rpplugins/vxgi/shader/copy_voxels.frag.glsl b/RenderPipelineFile/rpplugins/vxgi/shader/copy_voxels.frag.glsl index c3513367..55d93059 100644 --- a/RenderPipelineFile/rpplugins/vxgi/shader/copy_voxels.frag.glsl +++ b/RenderPipelineFile/rpplugins/vxgi/shader/copy_voxels.frag.glsl @@ -1,39 +1,39 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" - -uniform sampler3D SourceTex; -uniform writeonly image3D RESTRICT DestTex; - -flat in int instance_id; - -void main() { - ivec3 coord = ivec3(gl_FragCoord.xy, instance_id); - imageStore(DestTex, coord, texelFetch(SourceTex, coord, 0)); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" + +uniform sampler3D SourceTex; +uniform writeonly image3D RESTRICT DestTex; + +flat in int instance_id; + +void main() { + ivec3 coord = ivec3(gl_FragCoord.xy, instance_id); + imageStore(DestTex, coord, texelFetch(SourceTex, coord, 0)); +} diff --git a/RenderPipelineFile/rpplugins/vxgi/shader/generate_mipmaps.frag.glsl b/RenderPipelineFile/rpplugins/vxgi/shader/generate_mipmaps.frag.glsl index dc718035..ba7b35ed 100644 --- a/RenderPipelineFile/rpplugins/vxgi/shader/generate_mipmaps.frag.glsl +++ b/RenderPipelineFile/rpplugins/vxgi/shader/generate_mipmaps.frag.glsl @@ -1,54 +1,54 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" - -flat in int instance_id; - -uniform int sourceMip; -uniform sampler3D SourceTex; -uniform writeonly image3D RESTRICT DestTex; - -void main() { - ivec3 coord = ivec3(gl_FragCoord.xy, instance_id); - - ivec3 parent_coord = coord * 2; - - vec4 accum = vec4(0); - accum += texelFetch(SourceTex, parent_coord + ivec3(0, 0, 0), sourceMip); - accum += texelFetch(SourceTex, parent_coord + ivec3(1, 0, 0), sourceMip); - accum += texelFetch(SourceTex, parent_coord + ivec3(0, 1, 0), sourceMip); - accum += texelFetch(SourceTex, parent_coord + ivec3(1, 1, 0), sourceMip); - - accum += texelFetch(SourceTex, parent_coord + ivec3(0, 0, 1), sourceMip); - accum += texelFetch(SourceTex, parent_coord + ivec3(1, 0, 1), sourceMip); - accum += texelFetch(SourceTex, parent_coord + ivec3(0, 1, 1), sourceMip); - accum += texelFetch(SourceTex, parent_coord + ivec3(1, 1, 1), sourceMip); - accum /= 8.0; - imageStore(DestTex, coord, accum); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" + +flat in int instance_id; + +uniform int sourceMip; +uniform sampler3D SourceTex; +uniform writeonly image3D RESTRICT DestTex; + +void main() { + ivec3 coord = ivec3(gl_FragCoord.xy, instance_id); + + ivec3 parent_coord = coord * 2; + + vec4 accum = vec4(0); + accum += texelFetch(SourceTex, parent_coord + ivec3(0, 0, 0), sourceMip); + accum += texelFetch(SourceTex, parent_coord + ivec3(1, 0, 0), sourceMip); + accum += texelFetch(SourceTex, parent_coord + ivec3(0, 1, 0), sourceMip); + accum += texelFetch(SourceTex, parent_coord + ivec3(1, 1, 0), sourceMip); + + accum += texelFetch(SourceTex, parent_coord + ivec3(0, 0, 1), sourceMip); + accum += texelFetch(SourceTex, parent_coord + ivec3(1, 0, 1), sourceMip); + accum += texelFetch(SourceTex, parent_coord + ivec3(0, 1, 1), sourceMip); + accum += texelFetch(SourceTex, parent_coord + ivec3(1, 1, 1), sourceMip); + accum /= 8.0; + imageStore(DestTex, coord, accum); +} diff --git a/RenderPipelineFile/rpplugins/vxgi/shader/resolve_vxgi.frag.glsl b/RenderPipelineFile/rpplugins/vxgi/shader/resolve_vxgi.frag.glsl index 7d9a11c8..a4bb4a1c 100644 --- a/RenderPipelineFile/rpplugins/vxgi/shader/resolve_vxgi.frag.glsl +++ b/RenderPipelineFile/rpplugins/vxgi/shader/resolve_vxgi.frag.glsl @@ -1,50 +1,50 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" - - -#define RS_MAX_CLIP_DIST 1.5 -#define RS_DISTANCE_SCALE 1.0 -#define RS_KEEP_GOOD_DURATION 64.0 -#define RS_USE_POSITION_TECHNIQUE 1 -#pragma include "includes/temporal_resolve.inc.glsl" - -uniform sampler2D CurrentTex; -uniform sampler2D CombinedVelocity; -uniform sampler2D Previous_VXGIPostSample; - -out vec4 result; - -void main() { - vec2 texcoord = get_texcoord(); - vec2 velocity = textureLod(CombinedVelocity, texcoord, 0).xy; - vec2 last_coord = texcoord + velocity; - - result = resolve_temporal(CurrentTex, Previous_VXGIPostSample, texcoord, last_coord); -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" + + +#define RS_MAX_CLIP_DIST 1.5 +#define RS_DISTANCE_SCALE 1.0 +#define RS_KEEP_GOOD_DURATION 64.0 +#define RS_USE_POSITION_TECHNIQUE 1 +#pragma include "includes/temporal_resolve.inc.glsl" + +uniform sampler2D CurrentTex; +uniform sampler2D CombinedVelocity; +uniform sampler2D Previous_VXGIPostSample; + +out vec4 result; + +void main() { + vec2 texcoord = get_texcoord(); + vec2 velocity = textureLod(CombinedVelocity, texcoord, 0).xy; + vec2 last_coord = texcoord + velocity; + + result = resolve_temporal(CurrentTex, Previous_VXGIPostSample, texcoord, last_coord); +} diff --git a/RenderPipelineFile/rpplugins/vxgi/shader/vxgi.inc.glsl b/RenderPipelineFile/rpplugins/vxgi/shader/vxgi.inc.glsl index ecf844ce..58adffbf 100644 --- a/RenderPipelineFile/rpplugins/vxgi/shader/vxgi.inc.glsl +++ b/RenderPipelineFile/rpplugins/vxgi/shader/vxgi.inc.glsl @@ -1,92 +1,92 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#pragma once - -// Required inputs -uniform sampler3D SceneVoxels; -uniform samplerCube ScatteringIBLSpecular; -uniform samplerCube ScatteringIBLDiffuse; - -// Voxel grid parameters -uniform vec3 voxelGridPosition; -uniform float voxelGridSize; -uniform int voxelGridResolution; - - -vec3 worldspace_to_voxelspace(vec3 worldspace) { - vec3 voxel_coord = (worldspace - voxelGridPosition) / GET_SETTING(vxgi, grid_ws_size); - return fma(voxel_coord, vec3(0.5), vec3(0.5)); -} - -float get_mipmap_from_cone_radius(float cone_radius) { - return log2(cone_radius * GET_SETTING(vxgi, grid_resolution) * 0.6) - 1; -} - -vec4 trace_cone(vec3 start_pos, vec3 nrm, vec3 direction, int max_steps, - bool is_specular, float cone_grow_factor, float seed) { - - // Find initial cone radius - float cone_radius = (1.0 + 5.0 * cone_grow_factor) / GET_SETTING(vxgi, grid_resolution); - - // Offset start position to avoid self intersection - start_pos += nrm * 1.5 / GET_SETTING(vxgi, grid_resolution); - start_pos += direction * 5.5 / GET_SETTING(vxgi, grid_resolution); - - if (!is_specular) { - // start_pos += nrm * 1.5 / GET_SETTING(vxgi, grid_resolution); - } - - // Cone parameters - vec3 current_pos = start_pos; - vec4 accum = vec4(0); - float mipmap = 0.0; - - current_pos += direction * cone_radius * seed; - - // Trace the cone over the voxel grid - for (int i = 0; i < max_steps; ++i) { - mipmap = get_mipmap_from_cone_radius(cone_radius); - vec4 sampled = textureLod(SceneVoxels, current_pos, mipmap); - sampled.w *= 2.0; - sampled.w = saturate(sampled.w); - accum += sampled * (1.0 - accum.w); - current_pos += direction * cone_radius / 2.25; - cone_radius *= 1.01 + cone_grow_factor; - } - - // Unpack packed color, since we use 8 bit targets only - accum.xyz = accum.xyz / (1 - accum.xyz); - // accum.xyz *= 15.0; - - if (is_specular) { - // accum.xyz += textureLod(ScatteringIBLSpecular, direction, mipmap).xyz * (1-accum.w); - } else { - // accum.xyz += textureLod(ScatteringIBLDiffuse, direction, 0).xyz * (1-accum.w) * 0.1; - } - - return accum; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#pragma once + +// Required inputs +uniform sampler3D SceneVoxels; +uniform samplerCube ScatteringIBLSpecular; +uniform samplerCube ScatteringIBLDiffuse; + +// Voxel grid parameters +uniform vec3 voxelGridPosition; +uniform float voxelGridSize; +uniform int voxelGridResolution; + + +vec3 worldspace_to_voxelspace(vec3 worldspace) { + vec3 voxel_coord = (worldspace - voxelGridPosition) / GET_SETTING(vxgi, grid_ws_size); + return fma(voxel_coord, vec3(0.5), vec3(0.5)); +} + +float get_mipmap_from_cone_radius(float cone_radius) { + return log2(cone_radius * GET_SETTING(vxgi, grid_resolution) * 0.6) - 1; +} + +vec4 trace_cone(vec3 start_pos, vec3 nrm, vec3 direction, int max_steps, + bool is_specular, float cone_grow_factor, float seed) { + + // Find initial cone radius + float cone_radius = (1.0 + 5.0 * cone_grow_factor) / GET_SETTING(vxgi, grid_resolution); + + // Offset start position to avoid self intersection + start_pos += nrm * 1.5 / GET_SETTING(vxgi, grid_resolution); + start_pos += direction * 5.5 / GET_SETTING(vxgi, grid_resolution); + + if (!is_specular) { + // start_pos += nrm * 1.5 / GET_SETTING(vxgi, grid_resolution); + } + + // Cone parameters + vec3 current_pos = start_pos; + vec4 accum = vec4(0); + float mipmap = 0.0; + + current_pos += direction * cone_radius * seed; + + // Trace the cone over the voxel grid + for (int i = 0; i < max_steps; ++i) { + mipmap = get_mipmap_from_cone_radius(cone_radius); + vec4 sampled = textureLod(SceneVoxels, current_pos, mipmap); + sampled.w *= 2.0; + sampled.w = saturate(sampled.w); + accum += sampled * (1.0 - accum.w); + current_pos += direction * cone_radius / 2.25; + cone_radius *= 1.01 + cone_grow_factor; + } + + // Unpack packed color, since we use 8 bit targets only + accum.xyz = accum.xyz / (1 - accum.xyz); + // accum.xyz *= 15.0; + + if (is_specular) { + // accum.xyz += textureLod(ScatteringIBLSpecular, direction, mipmap).xyz * (1-accum.w); + } else { + // accum.xyz += textureLod(ScatteringIBLDiffuse, direction, 0).xyz * (1-accum.w) * 0.1; + } + + return accum; +} diff --git a/RenderPipelineFile/rpplugins/vxgi/shader/vxgi_diffuse.frag.glsl b/RenderPipelineFile/rpplugins/vxgi/shader/vxgi_diffuse.frag.glsl index c0de3075..0bce4f2e 100644 --- a/RenderPipelineFile/rpplugins/vxgi/shader/vxgi_diffuse.frag.glsl +++ b/RenderPipelineFile/rpplugins/vxgi/shader/vxgi_diffuse.frag.glsl @@ -1,89 +1,89 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "includes/poisson_disk.inc.glsl" -#pragma include "includes/noise.inc.glsl" -#pragma include "vxgi.inc.glsl" - -uniform sampler2D ShadedScene; -uniform GBufferData GBuffer; -out vec4 result; - -void main() { - // Get texture coordinate - ivec2 coord = ivec2(gl_FragCoord.xy) * 2; - vec2 texcoord = (coord + 0.5) / SCREEN_SIZE; - vec3 noise_vec = vec3(0, 0, 0); - - // Get material data - Material m = unpack_material(GBuffer, texcoord); - vec3 voxel_coord = worldspace_to_voxelspace(m.position); - - // Get view vector - vec3 view_vector = normalize(MainSceneData.camera_pos - m.position); - vec3 reflected_dir = reflect(-view_vector, m.normal); - - if (out_of_unit_box(voxel_coord)) - { - result = textureLod(ScatteringIBLDiffuse, m.normal, 0); - return; - } - - // Trace diffuse cones - vec4 accum = vec4(0); - - // float jitter = 16.0 / (MainSceneData.frame_index % 512); - float jitter = 0.0; - - for (int i = 0; i < 8; ++i) { - vec3 direction = rand_rgb(vec2(texcoord + i * 0.001 + - 0.134 * (MainSceneData.frame_index % 1024))); - - direction = normalize(direction); - direction = face_forward(direction, m.normal); - float weight = max(0.0, dot(m.normal, direction)); // Guaranteed to be > 0 - // weight = 1.0; - vec4 cone = trace_cone( - voxel_coord, - m.normal, - direction, - // GET_SETTING(vxgi, diffuse_cone_steps), - 32, - false, - 0.05, - jitter); - accum.xyz += cone.xyz * weight; - accum.w += weight; - } - accum /= max(1e-3, accum.w); - accum *= 1.0; - accum = clamp(accum, vec4(0.0), vec4(100.0)); - result = accum; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "includes/poisson_disk.inc.glsl" +#pragma include "includes/noise.inc.glsl" +#pragma include "vxgi.inc.glsl" + +uniform sampler2D ShadedScene; +uniform GBufferData GBuffer; +out vec4 result; + +void main() { + // Get texture coordinate + ivec2 coord = ivec2(gl_FragCoord.xy) * 2; + vec2 texcoord = (coord + 0.5) / SCREEN_SIZE; + vec3 noise_vec = vec3(0, 0, 0); + + // Get material data + Material m = unpack_material(GBuffer, texcoord); + vec3 voxel_coord = worldspace_to_voxelspace(m.position); + + // Get view vector + vec3 view_vector = normalize(MainSceneData.camera_pos - m.position); + vec3 reflected_dir = reflect(-view_vector, m.normal); + + if (out_of_unit_box(voxel_coord)) + { + result = textureLod(ScatteringIBLDiffuse, m.normal, 0); + return; + } + + // Trace diffuse cones + vec4 accum = vec4(0); + + // float jitter = 16.0 / (MainSceneData.frame_index % 512); + float jitter = 0.0; + + for (int i = 0; i < 8; ++i) { + vec3 direction = rand_rgb(vec2(texcoord + i * 0.001 + + 0.134 * (MainSceneData.frame_index % 1024))); + + direction = normalize(direction); + direction = face_forward(direction, m.normal); + float weight = max(0.0, dot(m.normal, direction)); // Guaranteed to be > 0 + // weight = 1.0; + vec4 cone = trace_cone( + voxel_coord, + m.normal, + direction, + // GET_SETTING(vxgi, diffuse_cone_steps), + 32, + false, + 0.05, + jitter); + accum.xyz += cone.xyz * weight; + accum.w += weight; + } + accum /= max(1e-3, accum.w); + accum *= 1.0; + accum = clamp(accum, vec4(0.0), vec4(100.0)); + result = accum; +} diff --git a/RenderPipelineFile/rpplugins/vxgi/shader/vxgi_specular.frag.glsl b/RenderPipelineFile/rpplugins/vxgi/shader/vxgi_specular.frag.glsl index 392f8068..e748a0c9 100644 --- a/RenderPipelineFile/rpplugins/vxgi/shader/vxgi_specular.frag.glsl +++ b/RenderPipelineFile/rpplugins/vxgi/shader/vxgi_specular.frag.glsl @@ -1,70 +1,70 @@ -/** - * - * RenderPipeline - * - * Copyright (c) 2014-2016 tobspr - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -#version 430 - - -#pragma include "render_pipeline_base.inc.glsl" -#pragma include "includes/gbuffer.inc.glsl" -#pragma include "includes/poisson_disk.inc.glsl" -#pragma include "vxgi.inc.glsl" - -uniform sampler2D ShadedScene; -uniform GBufferData GBuffer; -out vec4 result; - -void main() { - - Material m = unpack_material(GBuffer); - vec3 voxel_coord = worldspace_to_voxelspace(m.position); - - // Get view vector - vec3 view_vector = normalize(MainSceneData.camera_pos - m.position); - vec3 reflected_dir = reflect(-view_vector, m.normal); - - if (out_of_unit_box(voxel_coord)) - { - result = textureLod(ScatteringIBLSpecular, reflected_dir, 7) * 0.5; - return; - } - - // Trace specular cone - vec4 specular = trace_cone( - voxel_coord, - m.normal, - reflected_dir, - GET_SETTING(vxgi, specular_cone_steps), - true, - m.roughness * 0.17); - - // specular *= 0.1; - // specular.xyz = pow(specular.xyz, vec3(2.2)); - // specular.xyz *= 0.01; - // specular.xyz = specular.xyz / (1 - specular.xyz); - specular.xyz = max(vec3(0.0), specular.xyz); - - result = specular; -} +/** + * + * RenderPipeline + * + * Copyright (c) 2014-2016 tobspr + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#version 430 + + +#pragma include "render_pipeline_base.inc.glsl" +#pragma include "includes/gbuffer.inc.glsl" +#pragma include "includes/poisson_disk.inc.glsl" +#pragma include "vxgi.inc.glsl" + +uniform sampler2D ShadedScene; +uniform GBufferData GBuffer; +out vec4 result; + +void main() { + + Material m = unpack_material(GBuffer); + vec3 voxel_coord = worldspace_to_voxelspace(m.position); + + // Get view vector + vec3 view_vector = normalize(MainSceneData.camera_pos - m.position); + vec3 reflected_dir = reflect(-view_vector, m.normal); + + if (out_of_unit_box(voxel_coord)) + { + result = textureLod(ScatteringIBLSpecular, reflected_dir, 7) * 0.5; + return; + } + + // Trace specular cone + vec4 specular = trace_cone( + voxel_coord, + m.normal, + reflected_dir, + GET_SETTING(vxgi, specular_cone_steps), + true, + m.roughness * 0.17); + + // specular *= 0.1; + // specular.xyz = pow(specular.xyz, vec3(2.2)); + // specular.xyz *= 0.01; + // specular.xyz = specular.xyz / (1 - specular.xyz); + specular.xyz = max(vec3(0.0), specular.xyz); + + result = specular; +} diff --git a/RenderPipelineFile/rpplugins/vxgi/voxelization_stage.py b/RenderPipelineFile/rpplugins/vxgi/voxelization_stage.py index 07839f4b..86ff478b 100644 --- a/RenderPipelineFile/rpplugins/vxgi/voxelization_stage.py +++ b/RenderPipelineFile/rpplugins/vxgi/voxelization_stage.py @@ -1,203 +1,203 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import division - -from rpcore.globals import Globals -from rpcore.image import Image -from rpcore.render_stage import RenderStage - -from panda3d.core import Camera, OrthographicLens, NodePath, CullFaceAttrib -from panda3d.core import DepthTestAttrib, Vec4, PTALVecBase3, Vec3, SamplerState -from panda3d.core import ColorWriteAttrib - - -class VoxelizationStage(RenderStage): - - """ This stage voxelizes the whole scene """ - - required_inputs = ["DefaultEnvmap", "AllLightsData", "maxLightIndex"] - required_pipes = [] - - # The different states of voxelization - S_disabled = 0 - S_voxelize_x = 1 - S_voxelize_y = 2 - S_voxelize_z = 3 - S_gen_mipmaps = 4 - - def __init__(self, pipeline): - RenderStage.__init__(self, pipeline) - self.voxel_resolution = 256 - self.voxel_world_size = -1 - self.state = self.S_disabled - self.create_ptas() - - def set_grid_position(self, pos): - self.pta_next_grid_pos[0] = pos - - def create_ptas(self): - self.pta_next_grid_pos = PTALVecBase3.empty_array(1) - self.pta_grid_pos = PTALVecBase3.empty_array(1) - - @property - def produced_inputs(self): - return {"voxelGridPosition": self.pta_grid_pos} - - @property - def produced_pipes(self): - return {"SceneVoxels": self.voxel_grid} - - def create(self): - # Create the voxel grid used to generate the voxels - self.voxel_temp_grid = Image.create_3d( - "VoxelsTemp", self.voxel_resolution, self.voxel_resolution, - self.voxel_resolution, "RGBA8") - self.voxel_temp_grid.set_clear_color(Vec4(0)) - self.voxel_temp_nrm_grid = Image.create_3d( - "VoxelsTemp", self.voxel_resolution, self.voxel_resolution, - self.voxel_resolution, "R11G11B10") - self.voxel_temp_nrm_grid.set_clear_color(Vec4(0)) - - # Create the voxel grid which is a copy of the temporary grid, but stable - self.voxel_grid = Image.create_3d( - "Voxels", self.voxel_resolution, self.voxel_resolution, self.voxel_resolution, "RGBA8") - self.voxel_grid.set_clear_color(Vec4(0)) - self.voxel_grid.set_minfilter(SamplerState.FT_linear_mipmap_linear) - - # Create the camera for voxelization - self.voxel_cam = Camera("VoxelizeCam") - self.voxel_cam.set_camera_mask(self._pipeline.tag_mgr.get_mask("voxelize")) - self.voxel_cam_lens = OrthographicLens() - self.voxel_cam_lens.set_film_size( - -2.0 * self.voxel_world_size, 2.0 * self.voxel_world_size) - self.voxel_cam_lens.set_near_far(0.0, 2.0 * self.voxel_world_size) - self.voxel_cam.set_lens(self.voxel_cam_lens) - self.voxel_cam_np = Globals.base.render.attach_new_node(self.voxel_cam) - self._pipeline.tag_mgr.register_camera("voxelize", self.voxel_cam) - - # Create the voxelization target - self.voxel_target = self.create_target("VoxelizeScene") - self.voxel_target.size = self.voxel_resolution - self.voxel_target.prepare_render(self.voxel_cam_np) - - # Create the target which copies the voxel grid - self.copy_target = self.create_target("CopyVoxels") - self.copy_target.size = self.voxel_resolution - self.copy_target.prepare_buffer() - - # TODO! Does not work with the new render target yet - maybe add option - # to post process region for instances? - self.copy_target.instance_count = self.voxel_resolution - self.copy_target.set_shader_inputs( - SourceTex=self.voxel_temp_grid, - DestTex=self.voxel_grid) - - # Create the target which generates the mipmaps - self.mip_targets = [] - mip_size, mip = self.voxel_resolution, 0 - while mip_size > 1: - mip_size, mip = mip_size // 2, mip + 1 - mip_target = self.create_target("GenMipmaps:" + str(mip)) - mip_target.size = mip_size - mip_target.prepare_buffer() - mip_target.instance_count = mip_size - mip_target.set_shader_inputs( - SourceTex=self.voxel_grid, - sourceMip=(mip - 1)) - mip_target.set_shader_input("DestTex", self.voxel_grid, False, True, -1, mip, 0) - self.mip_targets.append(mip_target) - - # Create the initial state used for rendering voxels - initial_state = NodePath("VXGIInitialState") - initial_state.set_attrib(CullFaceAttrib.make(CullFaceAttrib.M_cull_none), 100000) - initial_state.set_attrib(DepthTestAttrib.make(DepthTestAttrib.M_none), 100000) - initial_state.set_attrib(ColorWriteAttrib.make(ColorWriteAttrib.C_off), 100000) - self.voxel_cam.set_initial_state(initial_state.get_state()) - - Globals.base.render.set_shader_inputs( - voxelGridPosition=self.pta_next_grid_pos, - VoxelGridDest=self.voxel_temp_grid) - - def update(self): - self.voxel_cam_np.show() - self.voxel_target.active = True - self.copy_target.active = False - - for target in self.mip_targets: - target.active = False - - # Voxelization disable - if self.state == self.S_disabled: - self.voxel_cam_np.hide() - self.voxel_target.active = False - - # Voxelization from X-Axis - elif self.state == self.S_voxelize_x: - # Clear voxel grid - self.voxel_temp_grid.clear_image() - self.voxel_cam_np.set_pos( - self.pta_next_grid_pos[0] + Vec3(self.voxel_world_size, 0, 0)) - self.voxel_cam_np.look_at(self.pta_next_grid_pos[0]) - - # Voxelization from Y-Axis - elif self.state == self.S_voxelize_y: - self.voxel_cam_np.set_pos( - self.pta_next_grid_pos[0] + Vec3(0, self.voxel_world_size, 0)) - self.voxel_cam_np.look_at(self.pta_next_grid_pos[0]) - - # Voxelization from Z-Axis - elif self.state == self.S_voxelize_z: - self.voxel_cam_np.set_pos( - self.pta_next_grid_pos[0] + Vec3(0, 0, self.voxel_world_size)) - self.voxel_cam_np.look_at(self.pta_next_grid_pos[0]) - - # Generate mipmaps - elif self.state == self.S_gen_mipmaps: - self.voxel_target.active = False - self.copy_target.active = True - self.voxel_cam_np.hide() - - for target in self.mip_targets: - target.active = True - - # As soon as we generate the mipmaps, we need to update the grid position - # as well - self.pta_grid_pos[0] = self.pta_next_grid_pos[0] - - def reload_shaders(self): - self.copy_target.shader = self.load_plugin_shader( - "/$$rp/shader/default_post_process_instanced.vert.glsl", "copy_voxels.frag.glsl") - mip_shader = self.load_plugin_shader( - "/$$rp/shader/default_post_process_instanced.vert.glsl", "generate_mipmaps.frag.glsl") - for target in self.mip_targets: - target.shader = mip_shader - - def set_shader_input(self, *args): - Globals.render.set_shader_input(*args) - - def set_shader_inputs(self, **kwargs): - Globals.render.set_shader_inputs(**kwargs) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import division + +from rpcore.globals import Globals +from rpcore.image import Image +from rpcore.render_stage import RenderStage + +from panda3d.core import Camera, OrthographicLens, NodePath, CullFaceAttrib +from panda3d.core import DepthTestAttrib, Vec4, PTALVecBase3, Vec3, SamplerState +from panda3d.core import ColorWriteAttrib + + +class VoxelizationStage(RenderStage): + + """ This stage voxelizes the whole scene """ + + required_inputs = ["DefaultEnvmap", "AllLightsData", "maxLightIndex"] + required_pipes = [] + + # The different states of voxelization + S_disabled = 0 + S_voxelize_x = 1 + S_voxelize_y = 2 + S_voxelize_z = 3 + S_gen_mipmaps = 4 + + def __init__(self, pipeline): + RenderStage.__init__(self, pipeline) + self.voxel_resolution = 256 + self.voxel_world_size = -1 + self.state = self.S_disabled + self.create_ptas() + + def set_grid_position(self, pos): + self.pta_next_grid_pos[0] = pos + + def create_ptas(self): + self.pta_next_grid_pos = PTALVecBase3.empty_array(1) + self.pta_grid_pos = PTALVecBase3.empty_array(1) + + @property + def produced_inputs(self): + return {"voxelGridPosition": self.pta_grid_pos} + + @property + def produced_pipes(self): + return {"SceneVoxels": self.voxel_grid} + + def create(self): + # Create the voxel grid used to generate the voxels + self.voxel_temp_grid = Image.create_3d( + "VoxelsTemp", self.voxel_resolution, self.voxel_resolution, + self.voxel_resolution, "RGBA8") + self.voxel_temp_grid.set_clear_color(Vec4(0)) + self.voxel_temp_nrm_grid = Image.create_3d( + "VoxelsTemp", self.voxel_resolution, self.voxel_resolution, + self.voxel_resolution, "R11G11B10") + self.voxel_temp_nrm_grid.set_clear_color(Vec4(0)) + + # Create the voxel grid which is a copy of the temporary grid, but stable + self.voxel_grid = Image.create_3d( + "Voxels", self.voxel_resolution, self.voxel_resolution, self.voxel_resolution, "RGBA8") + self.voxel_grid.set_clear_color(Vec4(0)) + self.voxel_grid.set_minfilter(SamplerState.FT_linear_mipmap_linear) + + # Create the camera for voxelization + self.voxel_cam = Camera("VoxelizeCam") + self.voxel_cam.set_camera_mask(self._pipeline.tag_mgr.get_mask("voxelize")) + self.voxel_cam_lens = OrthographicLens() + self.voxel_cam_lens.set_film_size( + -2.0 * self.voxel_world_size, 2.0 * self.voxel_world_size) + self.voxel_cam_lens.set_near_far(0.0, 2.0 * self.voxel_world_size) + self.voxel_cam.set_lens(self.voxel_cam_lens) + self.voxel_cam_np = Globals.base.render.attach_new_node(self.voxel_cam) + self._pipeline.tag_mgr.register_camera("voxelize", self.voxel_cam) + + # Create the voxelization target + self.voxel_target = self.create_target("VoxelizeScene") + self.voxel_target.size = self.voxel_resolution + self.voxel_target.prepare_render(self.voxel_cam_np) + + # Create the target which copies the voxel grid + self.copy_target = self.create_target("CopyVoxels") + self.copy_target.size = self.voxel_resolution + self.copy_target.prepare_buffer() + + # TODO! Does not work with the new render target yet - maybe add option + # to post process region for instances? + self.copy_target.instance_count = self.voxel_resolution + self.copy_target.set_shader_inputs( + SourceTex=self.voxel_temp_grid, + DestTex=self.voxel_grid) + + # Create the target which generates the mipmaps + self.mip_targets = [] + mip_size, mip = self.voxel_resolution, 0 + while mip_size > 1: + mip_size, mip = mip_size // 2, mip + 1 + mip_target = self.create_target("GenMipmaps:" + str(mip)) + mip_target.size = mip_size + mip_target.prepare_buffer() + mip_target.instance_count = mip_size + mip_target.set_shader_inputs( + SourceTex=self.voxel_grid, + sourceMip=(mip - 1)) + mip_target.set_shader_input("DestTex", self.voxel_grid, False, True, -1, mip, 0) + self.mip_targets.append(mip_target) + + # Create the initial state used for rendering voxels + initial_state = NodePath("VXGIInitialState") + initial_state.set_attrib(CullFaceAttrib.make(CullFaceAttrib.M_cull_none), 100000) + initial_state.set_attrib(DepthTestAttrib.make(DepthTestAttrib.M_none), 100000) + initial_state.set_attrib(ColorWriteAttrib.make(ColorWriteAttrib.C_off), 100000) + self.voxel_cam.set_initial_state(initial_state.get_state()) + + Globals.base.render.set_shader_inputs( + voxelGridPosition=self.pta_next_grid_pos, + VoxelGridDest=self.voxel_temp_grid) + + def update(self): + self.voxel_cam_np.show() + self.voxel_target.active = True + self.copy_target.active = False + + for target in self.mip_targets: + target.active = False + + # Voxelization disable + if self.state == self.S_disabled: + self.voxel_cam_np.hide() + self.voxel_target.active = False + + # Voxelization from X-Axis + elif self.state == self.S_voxelize_x: + # Clear voxel grid + self.voxel_temp_grid.clear_image() + self.voxel_cam_np.set_pos( + self.pta_next_grid_pos[0] + Vec3(self.voxel_world_size, 0, 0)) + self.voxel_cam_np.look_at(self.pta_next_grid_pos[0]) + + # Voxelization from Y-Axis + elif self.state == self.S_voxelize_y: + self.voxel_cam_np.set_pos( + self.pta_next_grid_pos[0] + Vec3(0, self.voxel_world_size, 0)) + self.voxel_cam_np.look_at(self.pta_next_grid_pos[0]) + + # Voxelization from Z-Axis + elif self.state == self.S_voxelize_z: + self.voxel_cam_np.set_pos( + self.pta_next_grid_pos[0] + Vec3(0, 0, self.voxel_world_size)) + self.voxel_cam_np.look_at(self.pta_next_grid_pos[0]) + + # Generate mipmaps + elif self.state == self.S_gen_mipmaps: + self.voxel_target.active = False + self.copy_target.active = True + self.voxel_cam_np.hide() + + for target in self.mip_targets: + target.active = True + + # As soon as we generate the mipmaps, we need to update the grid position + # as well + self.pta_grid_pos[0] = self.pta_next_grid_pos[0] + + def reload_shaders(self): + self.copy_target.shader = self.load_plugin_shader( + "/$$rp/shader/default_post_process_instanced.vert.glsl", "copy_voxels.frag.glsl") + mip_shader = self.load_plugin_shader( + "/$$rp/shader/default_post_process_instanced.vert.glsl", "generate_mipmaps.frag.glsl") + for target in self.mip_targets: + target.shader = mip_shader + + def set_shader_input(self, *args): + Globals.render.set_shader_input(*args) + + def set_shader_inputs(self, **kwargs): + Globals.render.set_shader_inputs(**kwargs) diff --git a/RenderPipelineFile/rpplugins/vxgi/vxgi_stage.py b/RenderPipelineFile/rpplugins/vxgi/vxgi_stage.py index 4a49caa7..aeca0e68 100644 --- a/RenderPipelineFile/rpplugins/vxgi/vxgi_stage.py +++ b/RenderPipelineFile/rpplugins/vxgi/vxgi_stage.py @@ -1,105 +1,105 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" -from __future__ import division - -from panda3d.core import LVecBase2i, Vec2 - -from rpcore.render_stage import RenderStage -from rpcore.stages.ambient_stage import AmbientStage - - -class VXGIStage(RenderStage): - - required_inputs = ["voxelGridPosition"] - required_pipes = ["ShadedScene", "SceneVoxels", "GBuffer", "ScatteringIBLSpecular", - "ScatteringIBLDiffuse", "PreviousFrame::VXGIPostSample", - "CombinedVelocity", "PreviousFrame::SceneDepth"] - - @property - def produced_pipes(self): - return { - # "VXGISpecular": self.target_spec.color_tex, - "VXGIDiffuse": self.target_resolve.color_tex, - "VXGIPostSample": self.target_resolve.color_tex - } - - def create(self): - # Create a target for the specular GI - # self.target_spec = self.create_target("SpecularGI") - # self.target_spec.add_color_attachment(bits=16, alpha=True) - # self.target_spec.prepare_buffer() - - # Create a target for the diffuse GI - self.target_diff = self.create_target("DiffuseGI") - self.target_diff.size = -2 - self.target_diff.add_color_attachment(bits=16) - self.target_diff.prepare_buffer() - - # Create the target which blurs the diffuse result - self.target_blur_v = self.create_target("BlurV") - self.target_blur_v.size = -2 - self.target_blur_v.add_color_attachment(bits=16) - self.target_blur_v.has_color_alpha = True - self.target_blur_v.prepare_buffer() - self.target_blur_v.set_shader_input("SourceTex", self.target_diff.color_tex) - - self.target_blur_h = self.create_target("BlurH") - self.target_blur_h.size = -2 - self.target_blur_h.add_color_attachment(bits=16) - self.target_blur_h.has_color_alpha = True - self.target_blur_h.prepare_buffer() - self.target_blur_h.set_shader_input("SourceTex", self.target_blur_v.color_tex) - - # Set blur parameters - self.target_blur_v.set_shader_input("blur_direction", LVecBase2i(0, 1)) - self.target_blur_h.set_shader_input("blur_direction", LVecBase2i(1, 0)) - - # Create the target which bilateral upsamples the diffuse target - self.target_upscale_diff = self.create_target("UpscaleDiffuse") - self.target_upscale_diff.add_color_attachment(bits=16) - self.target_upscale_diff.prepare_buffer() - self.target_upscale_diff.set_shader_inputs( - SourceTex=self.target_blur_h.color_tex, - upscaleWeights=Vec2(0.0001, 0.001)) - - self.target_resolve = self.create_target("ResolveVXGI") - self.target_resolve.add_color_attachment(bits=16) - self.target_resolve.prepare_buffer() - self.target_resolve.set_shader_input("CurrentTex", self.target_upscale_diff.color_tex) - - # Make the ambient stage use the GI result - AmbientStage.required_pipes += ["VXGIDiffuse"] - - def reload_shaders(self): - # self.target_spec.shader = self.load_plugin_shader("vxgi_specular.frag.glsl") - self.target_diff.shader = self.load_plugin_shader("vxgi_diffuse.frag.glsl") - self.target_upscale_diff.shader = self.load_plugin_shader( - "/$$rp/shader/bilateral_upscale.frag.glsl") - blur_shader = self.load_plugin_shader( - "/$$rp/shader/bilateral_halfres_blur.frag.glsl") - self.target_blur_v.shader = blur_shader - self.target_blur_h.shader = blur_shader - self.target_resolve.shader = self.load_plugin_shader("resolve_vxgi.frag.glsl") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" +from __future__ import division + +from panda3d.core import LVecBase2i, Vec2 + +from rpcore.render_stage import RenderStage +from rpcore.stages.ambient_stage import AmbientStage + + +class VXGIStage(RenderStage): + + required_inputs = ["voxelGridPosition"] + required_pipes = ["ShadedScene", "SceneVoxels", "GBuffer", "ScatteringIBLSpecular", + "ScatteringIBLDiffuse", "PreviousFrame::VXGIPostSample", + "CombinedVelocity", "PreviousFrame::SceneDepth"] + + @property + def produced_pipes(self): + return { + # "VXGISpecular": self.target_spec.color_tex, + "VXGIDiffuse": self.target_resolve.color_tex, + "VXGIPostSample": self.target_resolve.color_tex + } + + def create(self): + # Create a target for the specular GI + # self.target_spec = self.create_target("SpecularGI") + # self.target_spec.add_color_attachment(bits=16, alpha=True) + # self.target_spec.prepare_buffer() + + # Create a target for the diffuse GI + self.target_diff = self.create_target("DiffuseGI") + self.target_diff.size = -2 + self.target_diff.add_color_attachment(bits=16) + self.target_diff.prepare_buffer() + + # Create the target which blurs the diffuse result + self.target_blur_v = self.create_target("BlurV") + self.target_blur_v.size = -2 + self.target_blur_v.add_color_attachment(bits=16) + self.target_blur_v.has_color_alpha = True + self.target_blur_v.prepare_buffer() + self.target_blur_v.set_shader_input("SourceTex", self.target_diff.color_tex) + + self.target_blur_h = self.create_target("BlurH") + self.target_blur_h.size = -2 + self.target_blur_h.add_color_attachment(bits=16) + self.target_blur_h.has_color_alpha = True + self.target_blur_h.prepare_buffer() + self.target_blur_h.set_shader_input("SourceTex", self.target_blur_v.color_tex) + + # Set blur parameters + self.target_blur_v.set_shader_input("blur_direction", LVecBase2i(0, 1)) + self.target_blur_h.set_shader_input("blur_direction", LVecBase2i(1, 0)) + + # Create the target which bilateral upsamples the diffuse target + self.target_upscale_diff = self.create_target("UpscaleDiffuse") + self.target_upscale_diff.add_color_attachment(bits=16) + self.target_upscale_diff.prepare_buffer() + self.target_upscale_diff.set_shader_inputs( + SourceTex=self.target_blur_h.color_tex, + upscaleWeights=Vec2(0.0001, 0.001)) + + self.target_resolve = self.create_target("ResolveVXGI") + self.target_resolve.add_color_attachment(bits=16) + self.target_resolve.prepare_buffer() + self.target_resolve.set_shader_input("CurrentTex", self.target_upscale_diff.color_tex) + + # Make the ambient stage use the GI result + AmbientStage.required_pipes += ["VXGIDiffuse"] + + def reload_shaders(self): + # self.target_spec.shader = self.load_plugin_shader("vxgi_specular.frag.glsl") + self.target_diff.shader = self.load_plugin_shader("vxgi_diffuse.frag.glsl") + self.target_upscale_diff.shader = self.load_plugin_shader( + "/$$rp/shader/bilateral_upscale.frag.glsl") + blur_shader = self.load_plugin_shader( + "/$$rp/shader/bilateral_halfres_blur.frag.glsl") + self.target_blur_v.shader = blur_shader + self.target_blur_h.shader = blur_shader + self.target_resolve.shader = self.load_plugin_shader("resolve_vxgi.frag.glsl") diff --git a/RenderPipelineFile/setup.py b/RenderPipelineFile/setup.py index 1fbad3fa..cd185b0c 100644 --- a/RenderPipelineFile/setup.py +++ b/RenderPipelineFile/setup.py @@ -1,312 +1,312 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -# Setup script to install everything required to run the pipeline. - -# pylint: skip-file -# flake8: noqa - -from __future__ import print_function -import os -import sys -import subprocess -import webbrowser -import gzip -import shutil -import argparse - -sys.dont_write_bytecode = True - -DEVNULL = open(os.path.devnull, "w") -SETUP_DIR = os.path.dirname(os.path.realpath(__file__)) -CURRENT_STEP = 0 - -os.chdir(SETUP_DIR) -sys.path.insert(0, ".") - -from six.moves import input # pylint: disable=import-error - -# Load and init colorama, used to color the output -from rplibs.colorama import init as init_colorama -from rplibs.colorama import Fore, Style -init_colorama() - -def parse_cmd_args(): - """ Parses the command line arguments """ - parser = argparse.ArgumentParser(description="Render Pipeline Setup") - parser.add_argument( - "--clean", help="Clean rebuild of the native modules", action="store_true") - parser.add_argument( - "--verbose", help="Output additional debug information", action="store_true") - parser.add_argument( - "--skip-update", help="Skip updating the module builder to avoid overriding changes", action="store_true") - parser.add_argument( - "--skip-native", help="Skip native module compilation", action="store_true") - parser.add_argument( - "--ci-build", help="Skip setup steps requiring gpu drivers, only for travis ci", action="store_true") - return parser.parse_args() - -CMD_ARGS = parse_cmd_args() - -def color(string, col): - """ Colors a string """ - return col + string + Style.RESET_ALL - -def error(msg): - """ Prints an error message and then exists the program """ - print("\n" + color("Setup failed:\t", Fore.RED + Style.BRIGHT), msg) - print(color("\nPlease fix the above errors and then restart the setup.\n", - Fore.RED + Style.BRIGHT)) - sys.exit(-1) - -def print_step(title): - """ Prints a new section """ - global CURRENT_STEP - CURRENT_STEP += 1 - print("\n\n[", str(CURRENT_STEP).zfill(2), "] ", color(title, Fore.CYAN + Style.BRIGHT)) - -def ask_for_troubleshoot(url): - if CMD_ARGS.ci_build: - print("\nSETUP FAILED!") - return - - if not url: - print("\nSorry, no troubleshooting options are available.\n") - return - - query = "\nIt seems the setup failed, do you want to open the troubleshooting page for this step? (y/n):" - if get_user_choice(query): - print("OK, opening", url,"\n") - webbrowser.open(url, new=2) - -def exec_python_file(pth, args=None, troubleshoot=None): - """ Executes a python file and checks the return value """ - basedir = os.path.dirname(os.path.abspath(os.path.join(SETUP_DIR, pth))) + "/" - print("\tRunning script:", Fore.YELLOW + Style.BRIGHT + pth + Style.RESET_ALL) - pth = os.path.basename(pth) - os.chdir(basedir) - cmd = [sys.executable, "-B", pth] + (args or []) - if CMD_ARGS.verbose: - print("Executing", ' '.join(cmd)) - print("CWD is", basedir) - try: - output = subprocess.check_output(cmd, stderr=sys.stderr) - except subprocess.CalledProcessError as msg: - print(color("Failed to execute '" + pth + "'", Fore.YELLOW + Style.BRIGHT)) - print("Output:", msg, "\n", msg.output.decode("utf-8", errors="ignore")) - ask_for_troubleshoot(troubleshoot) - error("Python script didn't return properly!") - except IOError as msg: - print("Python script error:", msg) - ask_for_troubleshoot(troubleshoot) - error("Error during script execution") - if CMD_ARGS.verbose: - print(output.decode("utf-8", errors="ignore")) - os.chdir(SETUP_DIR) - -def extract_gz_files(pth): - """ Extract all gz files in the given path recursively """ - files = os.listdir(pth) - for fname in files: - fullpath = os.path.join(pth, fname) - if os.path.isfile(fullpath) and fname.endswith(".gz"): - print("\tExtracting", fname) - try: - with open(fullpath[:-3], 'wb') as dest, gzip.open(fullpath, 'rb') as src: - shutil.copyfileobj(src, dest) - except IOError as msg: - error("Failed to extract file '" + fname + "': " + str(msg)) - elif os.path.isdir(fullpath): - extract_gz_files(fullpath) - -def check_file_exists(fpath): - """ Checks if the given file exists """ - return os.path.isfile(os.path.join(SETUP_DIR, fpath)) - -def ask_download_samples(): - """ Asks the user if he wants to download the samples """ - query = "\nDo you want to download the Render Pipeline samples (~450MB)? (y/n):" - - if get_user_choice(query): - print_step("Downloading samples (Might take a while, depending on your " - "internet connection) ...") - exec_python_file("samples/download_samples.py") - -def get_user_choice(query): - """ Asks the user a boolean question """ - print("\n") - query = color(query, Fore.GREEN + Style.BRIGHT) + " " - - while True: - print(query, end="") - user_choice = input().strip().lower() - - if user_choice in ["y", "yes", "1"]: - return True - - if user_choice in ["n", "no", "0"]: - return False - - print(color("Invalid input: '" + user_choice + "'", Fore.RED + Style.BRIGHT)) - -def write_flag(flag_location, flag_value): - """ Writes a binary flag """ - flag_location = os.path.join(SETUP_DIR, flag_location) - try: - with open(flag_location, "w") as handle: - handle.write("1" if flag_value else "0") - except IOError as msg: - error("Failed to write flag to "+ flag_location + ", reason: " + str(msg)) - -def check_cmake(): - """ Checks if cmake is installed """ - try: - subprocess.call(["cmake", "--version"], stdout=subprocess.PIPE) - except Exception as msg: - print("\n") - print(color("Could not find cmake!", Fore.RED + Style.BRIGHT)) - print("It seems that cmake is not installed on this system, or not on") - print("your path. Please install cmake and make sure it is on your path.") - print("You can ensure this by running 'cmake --version' in a command line.") - print("Full error message:") - print(msg) - error("cmake missing") - -def check_panda_version(): - """ Checks whether the Panda3D version used is up to date. This is important - when using the C++ modules """ - - from panda3d.core import Texture - - if not hasattr(Texture, "F_r16i"): - print("\n") - print("It seems your Panda3D version is outdated. Please get the newest version ") - print("from", color("https://github.com/panda3d/panda3d", Fore.MAGENTA + Style.BRIGHT), - "(you can also build from source).") - error("Panda3D version outdated") - -def check_panda_rplight(): - """ Checks whether Panda3D provides the rpcore native module. """ - - try: - from panda3d import _rplight - return True - except ImportError: - pass - - return False - -def setup(): - """ Main setup routine """ - - print("-" * 79) - print("\nRender Pipeline Setup 1.3\n") - print("-" * 79) - - - if CMD_ARGS.ci_build: - print() - print(color("Running CI setup without steps requiring a GPU", Fore.BLUE + Style.BRIGHT)) - - print_step("Checking Panda3D Modules") - # Make sure this python build is using Panda3D - try: - from panda3d.core import NodePath - except ImportError: - print("\n") - print("Could not import Panda3D modules! Please make sure they are ") - print("on your path, and you are using the correct python version!") - error("Failed to import Panda3D modules") - - check_panda_version() - - if not CMD_ARGS.ci_build: - print_step("Checking requirements ..") - exec_python_file("data/setup/check_requirements.py", - troubleshoot="https://github.com/tobspr/RenderPipeline/wiki/Setup-Troubleshooting#requirements-check") - - if check_panda_rplight(): - write_flag("rpcore/native/use_cxx.flag", True) - - elif not CMD_ARGS.skip_native: - query = ("The C++ modules of the pipeline are faster and produce better \n" - "results, but we will have to compile them. As alternative, \n" - "a Python fallback is used, which is slower and produces worse \n" - "results. Also some plugins only partially work with the python \n" - "fallback (e.g. PSSM). Do you want to compile the C++ modules? (y/n):") - - # Dont install the c++ modules when using travis - if CMD_ARGS.ci_build or get_user_choice(query): - check_cmake() - write_flag("rpcore/native/use_cxx.flag", True) - - if not CMD_ARGS.skip_update: - print_step("Downloading the module builder ...") - exec_python_file("rpcore/native/update_module_builder.py", - troubleshoot="https://github.com/tobspr/RenderPipeline/wiki/Setup-Troubleshooting#downloading-module-builder") - - print_step("Building the native code .. (This might take a while!)") - exec_python_file("rpcore/native/build.py", ["--clean"] if CMD_ARGS.clean else [], - troubleshoot="https://github.com/tobspr/RenderPipeline/wiki/Setup-Troubleshooting#building-the-native-code") - - else: - write_flag("rpcore/native/use_cxx.flag", False) - - print_step("Generating .txo files ...") - exec_python_file("data/generate_txo_files.py", - troubleshoot="https://github.com/tobspr/RenderPipeline/wiki/Setup-Troubleshooting#extracting-txo-files") - - if not CMD_ARGS.ci_build: - print_step("Filtering default cubemap ..") - exec_python_file("data/default_cubemap/filter.py", - troubleshoot="https://github.com/tobspr/RenderPipeline/wiki/Setup-Troubleshooting#filtering-default-cubemap") - - print_step("Precomputing film grain .. ") - exec_python_file("data/film_grain/generate.py", - troubleshoot="https://github.com/tobspr/RenderPipeline/wiki/Setup-Troubleshooting#precomputing-film-grain") - - print_step("Running shader scripts .. ") - exec_python_file("rpplugins/env_probes/shader/generate_mip_shaders.py", - troubleshoot="https://github.com/tobspr/RenderPipeline/wiki/Setup-Troubleshooting#running-shader-scripts") - - - if not CMD_ARGS.ci_build: - print_step("Precomputing clouds ..") - exec_python_file("rpplugins/clouds/resources/precompute.py", - troubleshoot="https://github.com/tobspr/RenderPipeline/wiki/Setup-Troubleshooting#precomputing-clouds") - - write_flag("data/install.flag", True) - - # -- Further setup code follows here -- - - print(color("\n\n-- Setup finished sucessfully! --", Fore.GREEN + Style.BRIGHT)) - - if not CMD_ARGS.ci_build: - ask_download_samples() - - -if __name__ == "__main__": - setup() +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +# Setup script to install everything required to run the pipeline. + +# pylint: skip-file +# flake8: noqa + +from __future__ import print_function +import os +import sys +import subprocess +import webbrowser +import gzip +import shutil +import argparse + +sys.dont_write_bytecode = True + +DEVNULL = open(os.path.devnull, "w") +SETUP_DIR = os.path.dirname(os.path.realpath(__file__)) +CURRENT_STEP = 0 + +os.chdir(SETUP_DIR) +sys.path.insert(0, ".") + +from six.moves import input # pylint: disable=import-error + +# Load and init colorama, used to color the output +from rplibs.colorama import init as init_colorama +from rplibs.colorama import Fore, Style +init_colorama() + +def parse_cmd_args(): + """ Parses the command line arguments """ + parser = argparse.ArgumentParser(description="Render Pipeline Setup") + parser.add_argument( + "--clean", help="Clean rebuild of the native modules", action="store_true") + parser.add_argument( + "--verbose", help="Output additional debug information", action="store_true") + parser.add_argument( + "--skip-update", help="Skip updating the module builder to avoid overriding changes", action="store_true") + parser.add_argument( + "--skip-native", help="Skip native module compilation", action="store_true") + parser.add_argument( + "--ci-build", help="Skip setup steps requiring gpu drivers, only for travis ci", action="store_true") + return parser.parse_args() + +CMD_ARGS = parse_cmd_args() + +def color(string, col): + """ Colors a string """ + return col + string + Style.RESET_ALL + +def error(msg): + """ Prints an error message and then exists the program """ + print("\n" + color("Setup failed:\t", Fore.RED + Style.BRIGHT), msg) + print(color("\nPlease fix the above errors and then restart the setup.\n", + Fore.RED + Style.BRIGHT)) + sys.exit(-1) + +def print_step(title): + """ Prints a new section """ + global CURRENT_STEP + CURRENT_STEP += 1 + print("\n\n[", str(CURRENT_STEP).zfill(2), "] ", color(title, Fore.CYAN + Style.BRIGHT)) + +def ask_for_troubleshoot(url): + if CMD_ARGS.ci_build: + print("\nSETUP FAILED!") + return + + if not url: + print("\nSorry, no troubleshooting options are available.\n") + return + + query = "\nIt seems the setup failed, do you want to open the troubleshooting page for this step? (y/n):" + if get_user_choice(query): + print("OK, opening", url,"\n") + webbrowser.open(url, new=2) + +def exec_python_file(pth, args=None, troubleshoot=None): + """ Executes a python file and checks the return value """ + basedir = os.path.dirname(os.path.abspath(os.path.join(SETUP_DIR, pth))) + "/" + print("\tRunning script:", Fore.YELLOW + Style.BRIGHT + pth + Style.RESET_ALL) + pth = os.path.basename(pth) + os.chdir(basedir) + cmd = [sys.executable, "-B", pth] + (args or []) + if CMD_ARGS.verbose: + print("Executing", ' '.join(cmd)) + print("CWD is", basedir) + try: + output = subprocess.check_output(cmd, stderr=sys.stderr) + except subprocess.CalledProcessError as msg: + print(color("Failed to execute '" + pth + "'", Fore.YELLOW + Style.BRIGHT)) + print("Output:", msg, "\n", msg.output.decode("utf-8", errors="ignore")) + ask_for_troubleshoot(troubleshoot) + error("Python script didn't return properly!") + except IOError as msg: + print("Python script error:", msg) + ask_for_troubleshoot(troubleshoot) + error("Error during script execution") + if CMD_ARGS.verbose: + print(output.decode("utf-8", errors="ignore")) + os.chdir(SETUP_DIR) + +def extract_gz_files(pth): + """ Extract all gz files in the given path recursively """ + files = os.listdir(pth) + for fname in files: + fullpath = os.path.join(pth, fname) + if os.path.isfile(fullpath) and fname.endswith(".gz"): + print("\tExtracting", fname) + try: + with open(fullpath[:-3], 'wb') as dest, gzip.open(fullpath, 'rb') as src: + shutil.copyfileobj(src, dest) + except IOError as msg: + error("Failed to extract file '" + fname + "': " + str(msg)) + elif os.path.isdir(fullpath): + extract_gz_files(fullpath) + +def check_file_exists(fpath): + """ Checks if the given file exists """ + return os.path.isfile(os.path.join(SETUP_DIR, fpath)) + +def ask_download_samples(): + """ Asks the user if he wants to download the samples """ + query = "\nDo you want to download the Render Pipeline samples (~450MB)? (y/n):" + + if get_user_choice(query): + print_step("Downloading samples (Might take a while, depending on your " + "internet connection) ...") + exec_python_file("samples/download_samples.py") + +def get_user_choice(query): + """ Asks the user a boolean question """ + print("\n") + query = color(query, Fore.GREEN + Style.BRIGHT) + " " + + while True: + print(query, end="") + user_choice = input().strip().lower() + + if user_choice in ["y", "yes", "1"]: + return True + + if user_choice in ["n", "no", "0"]: + return False + + print(color("Invalid input: '" + user_choice + "'", Fore.RED + Style.BRIGHT)) + +def write_flag(flag_location, flag_value): + """ Writes a binary flag """ + flag_location = os.path.join(SETUP_DIR, flag_location) + try: + with open(flag_location, "w") as handle: + handle.write("1" if flag_value else "0") + except IOError as msg: + error("Failed to write flag to "+ flag_location + ", reason: " + str(msg)) + +def check_cmake(): + """ Checks if cmake is installed """ + try: + subprocess.call(["cmake", "--version"], stdout=subprocess.PIPE) + except Exception as msg: + print("\n") + print(color("Could not find cmake!", Fore.RED + Style.BRIGHT)) + print("It seems that cmake is not installed on this system, or not on") + print("your path. Please install cmake and make sure it is on your path.") + print("You can ensure this by running 'cmake --version' in a command line.") + print("Full error message:") + print(msg) + error("cmake missing") + +def check_panda_version(): + """ Checks whether the Panda3D version used is up to date. This is important + when using the C++ modules """ + + from panda3d.core import Texture + + if not hasattr(Texture, "F_r16i"): + print("\n") + print("It seems your Panda3D version is outdated. Please get the newest version ") + print("from", color("https://github.com/panda3d/panda3d", Fore.MAGENTA + Style.BRIGHT), + "(you can also build from source).") + error("Panda3D version outdated") + +def check_panda_rplight(): + """ Checks whether Panda3D provides the rpcore native module. """ + + try: + from panda3d import _rplight + return True + except ImportError: + pass + + return False + +def setup(): + """ Main setup routine """ + + print("-" * 79) + print("\nRender Pipeline Setup 1.3\n") + print("-" * 79) + + + if CMD_ARGS.ci_build: + print() + print(color("Running CI setup without steps requiring a GPU", Fore.BLUE + Style.BRIGHT)) + + print_step("Checking Panda3D Modules") + # Make sure this python build is using Panda3D + try: + from panda3d.core import NodePath + except ImportError: + print("\n") + print("Could not import Panda3D modules! Please make sure they are ") + print("on your path, and you are using the correct python version!") + error("Failed to import Panda3D modules") + + check_panda_version() + + if not CMD_ARGS.ci_build: + print_step("Checking requirements ..") + exec_python_file("data/setup/check_requirements.py", + troubleshoot="https://github.com/tobspr/RenderPipeline/wiki/Setup-Troubleshooting#requirements-check") + + if check_panda_rplight(): + write_flag("rpcore/native/use_cxx.flag", True) + + elif not CMD_ARGS.skip_native: + query = ("The C++ modules of the pipeline are faster and produce better \n" + "results, but we will have to compile them. As alternative, \n" + "a Python fallback is used, which is slower and produces worse \n" + "results. Also some plugins only partially work with the python \n" + "fallback (e.g. PSSM). Do you want to compile the C++ modules? (y/n):") + + # Dont install the c++ modules when using travis + if CMD_ARGS.ci_build or get_user_choice(query): + check_cmake() + write_flag("rpcore/native/use_cxx.flag", True) + + if not CMD_ARGS.skip_update: + print_step("Downloading the module builder ...") + exec_python_file("rpcore/native/update_module_builder.py", + troubleshoot="https://github.com/tobspr/RenderPipeline/wiki/Setup-Troubleshooting#downloading-module-builder") + + print_step("Building the native code .. (This might take a while!)") + exec_python_file("rpcore/native/build.py", ["--clean"] if CMD_ARGS.clean else [], + troubleshoot="https://github.com/tobspr/RenderPipeline/wiki/Setup-Troubleshooting#building-the-native-code") + + else: + write_flag("rpcore/native/use_cxx.flag", False) + + print_step("Generating .txo files ...") + exec_python_file("data/generate_txo_files.py", + troubleshoot="https://github.com/tobspr/RenderPipeline/wiki/Setup-Troubleshooting#extracting-txo-files") + + if not CMD_ARGS.ci_build: + print_step("Filtering default cubemap ..") + exec_python_file("data/default_cubemap/filter.py", + troubleshoot="https://github.com/tobspr/RenderPipeline/wiki/Setup-Troubleshooting#filtering-default-cubemap") + + print_step("Precomputing film grain .. ") + exec_python_file("data/film_grain/generate.py", + troubleshoot="https://github.com/tobspr/RenderPipeline/wiki/Setup-Troubleshooting#precomputing-film-grain") + + print_step("Running shader scripts .. ") + exec_python_file("rpplugins/env_probes/shader/generate_mip_shaders.py", + troubleshoot="https://github.com/tobspr/RenderPipeline/wiki/Setup-Troubleshooting#running-shader-scripts") + + + if not CMD_ARGS.ci_build: + print_step("Precomputing clouds ..") + exec_python_file("rpplugins/clouds/resources/precompute.py", + troubleshoot="https://github.com/tobspr/RenderPipeline/wiki/Setup-Troubleshooting#precomputing-clouds") + + write_flag("data/install.flag", True) + + # -- Further setup code follows here -- + + print(color("\n\n-- Setup finished sucessfully! --", Fore.GREEN + Style.BRIGHT)) + + if not CMD_ARGS.ci_build: + ask_download_samples() + + +if __name__ == "__main__": + setup() diff --git a/RenderPipelineFile/start_daytime_editor.py b/RenderPipelineFile/start_daytime_editor.py index 356b9857..c82ca49a 100644 --- a/RenderPipelineFile/start_daytime_editor.py +++ b/RenderPipelineFile/start_daytime_editor.py @@ -1,12 +1,12 @@ -#!/usr/bin/env python - -import os -import sys -import subprocess -this_dir = os.path.dirname(os.path.realpath(__file__)) - -if not os.path.isfile(os.path.join(this_dir, "data", "install.flag")): - print("Please install the pipeline first by running setup.py") - sys.exit(-1) -to_execute = os.path.join(this_dir, "toolkit", "day_time_editor", "main.py") -subprocess.call([sys.executable, to_execute]) +#!/usr/bin/env python + +import os +import sys +import subprocess +this_dir = os.path.dirname(os.path.realpath(__file__)) + +if not os.path.isfile(os.path.join(this_dir, "data", "install.flag")): + print("Please install the pipeline first by running setup.py") + sys.exit(-1) +to_execute = os.path.join(this_dir, "toolkit", "day_time_editor", "main.py") +subprocess.call([sys.executable, to_execute]) diff --git a/RenderPipelineFile/start_plugin_configurator.py b/RenderPipelineFile/start_plugin_configurator.py index cbfd9ec5..85195263 100644 --- a/RenderPipelineFile/start_plugin_configurator.py +++ b/RenderPipelineFile/start_plugin_configurator.py @@ -1,13 +1,13 @@ -#!/usr/bin/env python - -import os -import sys -import subprocess -this_dir = os.path.dirname(os.path.realpath(__file__)) - -if not os.path.isfile(os.path.join(this_dir, "data", "install.flag")): - print("Please install the pipeline first by running setup.py") - sys.exit(-1) -to_execute = os.path.join(this_dir, "toolkit", "plugin_configurator", "main.py") -# 使用相对路径而不是绝对路径 +#!/usr/bin/env python + +import os +import sys +import subprocess +this_dir = os.path.dirname(os.path.realpath(__file__)) + +if not os.path.isfile(os.path.join(this_dir, "data", "install.flag")): + print("Please install the pipeline first by running setup.py") + sys.exit(-1) +to_execute = os.path.join(this_dir, "toolkit", "plugin_configurator", "main.py") +# 使用相对路径而不是绝对路径 subprocess.call([sys.executable, to_execute], cwd=this_dir) \ No newline at end of file diff --git a/RenderPipelineFile/toolkit/bake_gi/bake.py b/RenderPipelineFile/toolkit/bake_gi/bake.py index dd246018..2aed7fc9 100644 --- a/RenderPipelineFile/toolkit/bake_gi/bake.py +++ b/RenderPipelineFile/toolkit/bake_gi/bake.py @@ -1,264 +1,264 @@ -""" - -Precomputed GI using spherical harmonics. - -""" - -from __future__ import division, print_function - -import os -import sys - -os.chdir(os.path.realpath(os.path.dirname(__file__))) -sys.path.insert(0, "../../") - -# Shouldn't import everything, but saves a lot of code -from panda3d.core import * -from direct.showbase.ShowBase import ShowBase - -from rpcore.globals import Globals -from rpcore.render_target import RenderTarget -from rplibs.progressbar import ETA, ProgressBar, Percentage, Bar, Counter, Rate - -class Application(ShowBase): - - def __init__(self): - - # Load settings - load_prc_file_data("", """ - win-size 10 10 - window-type offscreen - win-title GI Bake - textures-power-2 none - gl-coordinate-system default - sync-video #f - support-stencil #f - framebuffer-stencil #f - framebuffer-multisample #f - multisamples 0 - gl-cube-map-seamless #t - gl-force-fbo-color #f - """) - - ShowBase.__init__(self) - Globals.load(self) - Globals.resolution = LVecBase2i(1600, 900) - sun_vector = Vec3(0.2, 0.5, 1.2).normalized() - capture_resolution = 32 - sun_shadow_map_resolution = 2048 - num_probes = LVecBase3i(64) - divisor = 128 - num_bakers = 8 - padding = 0.5 - - for region in self.win.get_display_regions(): - region.set_active(False) - - print("Loading scene ...") - # model = loader.load_model("resources/test-scene.bam") - model = loader.load_model("scene/scene.bam") - # model = loader.load_model("scene/LivingRoom.egg") - model.reparent_to(render) - model.flatten_strong() - - start_point, end_point = model.get_tight_bounds() - start_point -= padding - end_point += padding - diameter = (start_point - end_point).length() * 0.5 - model_center = (start_point + end_point) * 0.5 - model_size = end_point - start_point - - print("Rendering sun shadow map ..") - sun_shadow_cam = Camera("SunShadowCamera") - sun_shadow_lens = OrthographicLens() - sun_shadow_lens.set_film_size(diameter * 2, diameter * 2) - sun_shadow_lens.set_near_far(0, 2 * diameter) - sun_shadow_cam.set_lens(sun_shadow_lens) - sun_shadow_cam_np = render.attach_new_node(sun_shadow_cam) - sun_shadow_cam_np.set_pos(model_center + sun_vector * diameter) - sun_shadow_cam_np.look_at(model_center) - - sun_shadow_target = RenderTarget() - sun_shadow_target.size = sun_shadow_map_resolution - sun_shadow_target.add_depth_attachment(bits=32) - sun_shadow_target.prepare_render(sun_shadow_cam_np) - - self.render_frame() - sun_shadow_target.active = False - shadow_mvp = self.get_mvp(sun_shadow_cam_np) - - print("Computing first bounce ..") - - # Target to store all results - max_probes = num_probes.x * num_probes.y * num_probes.z - - if max_probes % divisor != 0: - print("WARNING: Bad divisor:", divisor, "for", max_probes) - - num_rows = (max_probes + divisor - 1) // divisor - final_data = Texture("FinalProbeResult") - final_data.setup_2d_texture(6 * divisor, num_rows, Texture.T_float, Texture.F_rgba16) - final_data.set_clear_color(Vec4(1.0, 0.6, 0.2, 1.0)) - - worker_handles = [] - - store_shader = Shader.load(Shader.SL_GLSL, "resources/default.vert.glsl", "resources/copy_cubemap.frag.glsl") - convolute_shader = Shader.load(Shader.SL_GLSL, "resources/default.vert.glsl", "resources/convolute.frag.glsl") - - for worked_id in range(num_bakers): - probe_position = Vec3(0, 0, 4) - capture_target = RenderTarget() - capture_target.size = capture_resolution * 6, capture_resolution - capture_target.add_depth_attachment(bits=16) - capture_target.add_color_attachment(bits=16, alpha=True) - capture_target.prepare_render(None) - - # Remove all unused display regions - internal_buffer = capture_target.internal_buffer - internal_buffer.remove_all_display_regions() - internal_buffer.disable_clears() - internal_buffer.get_overlay_display_region().disable_clears() - - # Setup the cubemap capture rig - directions = (Vec3(1, 0, 0), Vec3(-1, 0, 0), Vec3(0, 1, 0), - Vec3(0, -1, 0), Vec3(0, 0, 1), Vec3(0, 0, -1)) - capture_regions = [] - capture_cams = [] - capture_rig = render.attach_new_node("CaptureRig") - capture_rig.set_pos(probe_position) - - # Prepare the display regions - for i in range(6): - region = capture_target.internal_buffer.make_display_region( - i / 6, i / 6 + 1 / 6, 0, 1) - region.set_sort(25 + i) - region.set_active(True) - region.disable_clears() - - # Set the correct clears - region.set_clear_depth_active(True) - region.set_clear_depth(1.0) - region.set_clear_color_active(True) - region.set_clear_color(Vec4(0.0, 0.0, 0.0, 0.0)) - - lens = PerspectiveLens() - lens.set_fov(90) - lens.set_near_far(0.05, 2 * diameter) - camera = Camera("CaptureCam-" + str(i), lens) - camera_np = capture_rig.attach_new_node(camera) - camera_np.look_at(camera_np, directions[i]) - region.set_camera(camera_np) - capture_regions.append(region) - capture_cams.append(camera_np) - - capture_cams[0].set_r(90) - capture_cams[1].set_r(-90) - capture_cams[3].set_r(180) - capture_cams[5].set_r(180) - - destination_cubemap = Texture("TemporaryCubemap") - destination_cubemap.setup_cube_map(capture_resolution, Texture.T_float, Texture.F_rgba16) - - # Target to convert the FBO to a cubemap - target_store_cubemap = RenderTarget() - target_store_cubemap.size = capture_resolution * 6, capture_resolution - target_store_cubemap.prepare_buffer() - target_store_cubemap.set_shader_inputs( - SourceTex=capture_target.color_tex, - DestTex=destination_cubemap) - - target_store_cubemap.shader = store_shader - - # Target to filter the data - store_pta = PTALVecBase2i.empty_array(1) - target_convolute = RenderTarget() - target_convolute.size = 6, 1 - # target_convolute.add_color_attachment(bits=16) - target_convolute.prepare_buffer() - target_convolute.set_shader_inputs( - SourceTex=destination_cubemap, - DestTex=final_data, - storeCoord=store_pta) - target_convolute.shader = convolute_shader - - # Set initial shader - shader = Shader.load(Shader.SL_GLSL, - "resources/first-bounce.vert.glsl", "resources/first-bounce.frag.glsl") - render.set_shader(shader) - render.set_shader_inputs( - ShadowMap=sun_shadow_target.depth_tex, - shadowMVP=shadow_mvp, - sunVector=sun_vector) - - worker_handles.append((capture_rig, store_pta)) - - print("Preparing to render", max_probes, "probes ..") - widgets = [Counter(), " ", Bar(), " ", Percentage(), " ", ETA(), " ", Rate()] - progressbar = ProgressBar(widgets=widgets, maxval=max_probes).start() - progressbar.update(0) - - work_queue = [] - - for z_pos in range(num_probes.z): - for y_pos in range(num_probes.y): - for x_pos in range(num_probes.x): - index = x_pos + y_pos * num_probes.x + z_pos * num_probes.y * num_probes.x - # print("Baking", index, "out of", max_probes) - offs_x = start_point.x + x_pos / (num_probes.x + 0.5) * model_size.x - offs_y = start_point.y + y_pos / (num_probes.y + 0.5) * model_size.y - offs_z = start_point.z + z_pos / (num_probes.z + 0.5) * model_size.z - - store_x = index % divisor - store_y = index // divisor - - work_queue.append((Vec3(offs_x, offs_y, offs_z), LVecBase2i(store_x, store_y))) - - for i, (pos, store) in enumerate(work_queue): - worker_handles[i%num_bakers][0].set_pos(pos) - worker_handles[i%num_bakers][1][0] = store - if i % num_bakers == num_bakers - 1: - self.render_frame() - progressbar.update(i) - - - progressbar.finish() - self.render_frame() - - - print("Writing out data ..") - self.graphicsEngine.extract_texture_data(final_data, self.win.gsg) - final_data.write("raw-bake.png") - - print("Writing out configuration") - with open("_bake_params.py", "w") as handle: - handle.write("#Autogenerated\n") - handle.write("from panda3d.core import LPoint3f, LVecBase3i, LVector3f\n") - handle.write("BAKE_MESH_START = " + str(start_point) + "\n") - handle.write("BAKE_MESH_END = " + str(end_point) + "\n") - handle.write("BAKE_MESH_PROBECOUNT = " + str(num_probes) + "\n") - handle.write("BAKE_DIVISOR = " + str(divisor) + "\n") - handle.write("BAKE_SUN_VECTOR = " + str(sun_vector) + "\n") - - with open("_bake_params.glsl", "w") as handle: - handle.write("// Autogenerated\n") - handle.write("#define LPoint3f vec3\n") - handle.write("#define LVector3f vec3\n") - handle.write("#define LVecBase3i ivec3\n") - handle.write("const vec3 bake_mesh_start = " + str(start_point) + ";\n") - handle.write("const vec3 bake_mesh_end = " + str(end_point) + ";\n") - handle.write("const ivec3 bake_mesh_probecount = " + str(num_probes) + ";\n") - handle.write("const int bake_divisor = " + str(divisor) + ";\n") - handle.write("const vec3 sun_vector = " + str(sun_vector) + ";\n") - - def render_frame(self): - """ Convenience function to render a frame """ - self.graphicsEngine.render_frame() - - def get_mvp(self, cam_node): - """ Computes the view-projection matrix of a camera """ - return render.get_transform(cam_node).get_mat() * cam_node.node().get_lens().get_projection_mat() - - -Application() - - +""" + +Precomputed GI using spherical harmonics. + +""" + +from __future__ import division, print_function + +import os +import sys + +os.chdir(os.path.realpath(os.path.dirname(__file__))) +sys.path.insert(0, "../../") + +# Shouldn't import everything, but saves a lot of code +from panda3d.core import * +from direct.showbase.ShowBase import ShowBase + +from rpcore.globals import Globals +from rpcore.render_target import RenderTarget +from rplibs.progressbar import ETA, ProgressBar, Percentage, Bar, Counter, Rate + +class Application(ShowBase): + + def __init__(self): + + # Load settings + load_prc_file_data("", """ + win-size 10 10 + window-type offscreen + win-title GI Bake + textures-power-2 none + gl-coordinate-system default + sync-video #f + support-stencil #f + framebuffer-stencil #f + framebuffer-multisample #f + multisamples 0 + gl-cube-map-seamless #t + gl-force-fbo-color #f + """) + + ShowBase.__init__(self) + Globals.load(self) + Globals.resolution = LVecBase2i(1600, 900) + sun_vector = Vec3(0.2, 0.5, 1.2).normalized() + capture_resolution = 32 + sun_shadow_map_resolution = 2048 + num_probes = LVecBase3i(64) + divisor = 128 + num_bakers = 8 + padding = 0.5 + + for region in self.win.get_display_regions(): + region.set_active(False) + + print("Loading scene ...") + # model = loader.load_model("resources/test-scene.bam") + model = loader.load_model("scene/scene.bam") + # model = loader.load_model("scene/LivingRoom.egg") + model.reparent_to(render) + model.flatten_strong() + + start_point, end_point = model.get_tight_bounds() + start_point -= padding + end_point += padding + diameter = (start_point - end_point).length() * 0.5 + model_center = (start_point + end_point) * 0.5 + model_size = end_point - start_point + + print("Rendering sun shadow map ..") + sun_shadow_cam = Camera("SunShadowCamera") + sun_shadow_lens = OrthographicLens() + sun_shadow_lens.set_film_size(diameter * 2, diameter * 2) + sun_shadow_lens.set_near_far(0, 2 * diameter) + sun_shadow_cam.set_lens(sun_shadow_lens) + sun_shadow_cam_np = render.attach_new_node(sun_shadow_cam) + sun_shadow_cam_np.set_pos(model_center + sun_vector * diameter) + sun_shadow_cam_np.look_at(model_center) + + sun_shadow_target = RenderTarget() + sun_shadow_target.size = sun_shadow_map_resolution + sun_shadow_target.add_depth_attachment(bits=32) + sun_shadow_target.prepare_render(sun_shadow_cam_np) + + self.render_frame() + sun_shadow_target.active = False + shadow_mvp = self.get_mvp(sun_shadow_cam_np) + + print("Computing first bounce ..") + + # Target to store all results + max_probes = num_probes.x * num_probes.y * num_probes.z + + if max_probes % divisor != 0: + print("WARNING: Bad divisor:", divisor, "for", max_probes) + + num_rows = (max_probes + divisor - 1) // divisor + final_data = Texture("FinalProbeResult") + final_data.setup_2d_texture(6 * divisor, num_rows, Texture.T_float, Texture.F_rgba16) + final_data.set_clear_color(Vec4(1.0, 0.6, 0.2, 1.0)) + + worker_handles = [] + + store_shader = Shader.load(Shader.SL_GLSL, "resources/default.vert.glsl", "resources/copy_cubemap.frag.glsl") + convolute_shader = Shader.load(Shader.SL_GLSL, "resources/default.vert.glsl", "resources/convolute.frag.glsl") + + for worked_id in range(num_bakers): + probe_position = Vec3(0, 0, 4) + capture_target = RenderTarget() + capture_target.size = capture_resolution * 6, capture_resolution + capture_target.add_depth_attachment(bits=16) + capture_target.add_color_attachment(bits=16, alpha=True) + capture_target.prepare_render(None) + + # Remove all unused display regions + internal_buffer = capture_target.internal_buffer + internal_buffer.remove_all_display_regions() + internal_buffer.disable_clears() + internal_buffer.get_overlay_display_region().disable_clears() + + # Setup the cubemap capture rig + directions = (Vec3(1, 0, 0), Vec3(-1, 0, 0), Vec3(0, 1, 0), + Vec3(0, -1, 0), Vec3(0, 0, 1), Vec3(0, 0, -1)) + capture_regions = [] + capture_cams = [] + capture_rig = render.attach_new_node("CaptureRig") + capture_rig.set_pos(probe_position) + + # Prepare the display regions + for i in range(6): + region = capture_target.internal_buffer.make_display_region( + i / 6, i / 6 + 1 / 6, 0, 1) + region.set_sort(25 + i) + region.set_active(True) + region.disable_clears() + + # Set the correct clears + region.set_clear_depth_active(True) + region.set_clear_depth(1.0) + region.set_clear_color_active(True) + region.set_clear_color(Vec4(0.0, 0.0, 0.0, 0.0)) + + lens = PerspectiveLens() + lens.set_fov(90) + lens.set_near_far(0.05, 2 * diameter) + camera = Camera("CaptureCam-" + str(i), lens) + camera_np = capture_rig.attach_new_node(camera) + camera_np.look_at(camera_np, directions[i]) + region.set_camera(camera_np) + capture_regions.append(region) + capture_cams.append(camera_np) + + capture_cams[0].set_r(90) + capture_cams[1].set_r(-90) + capture_cams[3].set_r(180) + capture_cams[5].set_r(180) + + destination_cubemap = Texture("TemporaryCubemap") + destination_cubemap.setup_cube_map(capture_resolution, Texture.T_float, Texture.F_rgba16) + + # Target to convert the FBO to a cubemap + target_store_cubemap = RenderTarget() + target_store_cubemap.size = capture_resolution * 6, capture_resolution + target_store_cubemap.prepare_buffer() + target_store_cubemap.set_shader_inputs( + SourceTex=capture_target.color_tex, + DestTex=destination_cubemap) + + target_store_cubemap.shader = store_shader + + # Target to filter the data + store_pta = PTALVecBase2i.empty_array(1) + target_convolute = RenderTarget() + target_convolute.size = 6, 1 + # target_convolute.add_color_attachment(bits=16) + target_convolute.prepare_buffer() + target_convolute.set_shader_inputs( + SourceTex=destination_cubemap, + DestTex=final_data, + storeCoord=store_pta) + target_convolute.shader = convolute_shader + + # Set initial shader + shader = Shader.load(Shader.SL_GLSL, + "resources/first-bounce.vert.glsl", "resources/first-bounce.frag.glsl") + render.set_shader(shader) + render.set_shader_inputs( + ShadowMap=sun_shadow_target.depth_tex, + shadowMVP=shadow_mvp, + sunVector=sun_vector) + + worker_handles.append((capture_rig, store_pta)) + + print("Preparing to render", max_probes, "probes ..") + widgets = [Counter(), " ", Bar(), " ", Percentage(), " ", ETA(), " ", Rate()] + progressbar = ProgressBar(widgets=widgets, maxval=max_probes).start() + progressbar.update(0) + + work_queue = [] + + for z_pos in range(num_probes.z): + for y_pos in range(num_probes.y): + for x_pos in range(num_probes.x): + index = x_pos + y_pos * num_probes.x + z_pos * num_probes.y * num_probes.x + # print("Baking", index, "out of", max_probes) + offs_x = start_point.x + x_pos / (num_probes.x + 0.5) * model_size.x + offs_y = start_point.y + y_pos / (num_probes.y + 0.5) * model_size.y + offs_z = start_point.z + z_pos / (num_probes.z + 0.5) * model_size.z + + store_x = index % divisor + store_y = index // divisor + + work_queue.append((Vec3(offs_x, offs_y, offs_z), LVecBase2i(store_x, store_y))) + + for i, (pos, store) in enumerate(work_queue): + worker_handles[i%num_bakers][0].set_pos(pos) + worker_handles[i%num_bakers][1][0] = store + if i % num_bakers == num_bakers - 1: + self.render_frame() + progressbar.update(i) + + + progressbar.finish() + self.render_frame() + + + print("Writing out data ..") + self.graphicsEngine.extract_texture_data(final_data, self.win.gsg) + final_data.write("raw-bake.png") + + print("Writing out configuration") + with open("_bake_params.py", "w") as handle: + handle.write("#Autogenerated\n") + handle.write("from panda3d.core import LPoint3f, LVecBase3i, LVector3f\n") + handle.write("BAKE_MESH_START = " + str(start_point) + "\n") + handle.write("BAKE_MESH_END = " + str(end_point) + "\n") + handle.write("BAKE_MESH_PROBECOUNT = " + str(num_probes) + "\n") + handle.write("BAKE_DIVISOR = " + str(divisor) + "\n") + handle.write("BAKE_SUN_VECTOR = " + str(sun_vector) + "\n") + + with open("_bake_params.glsl", "w") as handle: + handle.write("// Autogenerated\n") + handle.write("#define LPoint3f vec3\n") + handle.write("#define LVector3f vec3\n") + handle.write("#define LVecBase3i ivec3\n") + handle.write("const vec3 bake_mesh_start = " + str(start_point) + ";\n") + handle.write("const vec3 bake_mesh_end = " + str(end_point) + ";\n") + handle.write("const ivec3 bake_mesh_probecount = " + str(num_probes) + ";\n") + handle.write("const int bake_divisor = " + str(divisor) + ";\n") + handle.write("const vec3 sun_vector = " + str(sun_vector) + ";\n") + + def render_frame(self): + """ Convenience function to render a frame """ + self.graphicsEngine.render_frame() + + def get_mvp(self, cam_node): + """ Computes the view-projection matrix of a camera """ + return render.get_transform(cam_node).get_mat() * cam_node.node().get_lens().get_projection_mat() + + +Application() + + diff --git a/RenderPipelineFile/toolkit/bake_gi/display.py b/RenderPipelineFile/toolkit/bake_gi/display.py index 41d91337..e087be3c 100644 --- a/RenderPipelineFile/toolkit/bake_gi/display.py +++ b/RenderPipelineFile/toolkit/bake_gi/display.py @@ -1,100 +1,100 @@ -""" - -Test-script to view the baked GI - -""" - -import os -import sys - -os.chdir(os.path.realpath(os.path.dirname(__file__))) -sys.path.insert(0, "../../") - -from _bake_params import * - -from panda3d.core import * -from direct.showbase.ShowBase import ShowBase - -from rpcore.globals import Globals -from rpcore.render_target import RenderTarget - -class Application(ShowBase): - - def __init__(self): - - load_prc_file_data("", """ - textures-power-2 none - win-size 1600 900 - """) - - ShowBase.__init__(self) - Globals.load(self) - Globals.resolution = LVecBase2i(1600, 900) - sun_vector = Vec3(BAKE_SUN_VECTOR).normalized() - - diameter = (BAKE_MESH_END - BAKE_MESH_START).length() - model_center = (BAKE_MESH_START + BAKE_MESH_END) * 0.5 - sun_shadow_map_resolution = 8192 - - - # model = loader.load_model("resources/test-scene.bam") - model = loader.load_model("scene/scene.bam") - model.reparent_to(render) - model.flatten_strong() - - - print("Rendering sun shadow map ..") - sun_shadow_cam = Camera("SunShadowCamera") - sun_shadow_lens = OrthographicLens() - sun_shadow_lens.set_film_size(diameter * 2, diameter * 2) - sun_shadow_lens.set_near_far(0, 2 * diameter) - sun_shadow_cam.set_lens(sun_shadow_lens) - sun_shadow_cam_np = render.attach_new_node(sun_shadow_cam) - sun_shadow_cam_np.set_pos(model_center + sun_vector * diameter) - sun_shadow_cam_np.look_at(model_center) - - sun_shadow_target = RenderTarget() - sun_shadow_target.size = sun_shadow_map_resolution - sun_shadow_target.add_depth_attachment(bits=32) - sun_shadow_target.prepare_render(sun_shadow_cam_np) - - self.graphicsEngine.render_frame() - sun_shadow_target.active = False - shadow_mvp = self.get_mvp(sun_shadow_cam_np) - - # Load the dataset - dataset = loader.load_texture("raw-bake.png") - render.set_shader_input("GIDataTexture", dataset) - - # Load the display shader - shader = Shader.load(Shader.SL_GLSL, "resources/display.vert.glsl", "resources/display.frag.glsl") - render.set_shader(shader) - - render.set_shader_inputs( - ShadowMap=sun_shadow_target.depth_tex, - shadowMVP=shadow_mvp, - sunVector=sun_vector) - - # Render spheres distributed over the mesh - mesh_size = BAKE_MESH_END - BAKE_MESH_START - - for i in range(11): - for j in range(11): - for k in range(11): - offs_x = i / 10.0 * mesh_size.x + BAKE_MESH_START.x - offs_y = j / 10.0 * mesh_size.y + BAKE_MESH_START.y - offs_z = k / 10.0 * mesh_size.z + BAKE_MESH_START.z - - sphere = loader.load_model("resources/sphere.bam") - sphere.reparent_to(render) - sphere.set_scale(0.02) - sphere.set_pos(offs_x, offs_y, offs_z) - - - def get_mvp(self, cam_node): - """ Computes the view-projection matrix of a camera """ - return render.get_transform(cam_node).get_mat() * cam_node.node().get_lens().get_projection_mat() - - -Application().run() - +""" + +Test-script to view the baked GI + +""" + +import os +import sys + +os.chdir(os.path.realpath(os.path.dirname(__file__))) +sys.path.insert(0, "../../") + +from _bake_params import * + +from panda3d.core import * +from direct.showbase.ShowBase import ShowBase + +from rpcore.globals import Globals +from rpcore.render_target import RenderTarget + +class Application(ShowBase): + + def __init__(self): + + load_prc_file_data("", """ + textures-power-2 none + win-size 1600 900 + """) + + ShowBase.__init__(self) + Globals.load(self) + Globals.resolution = LVecBase2i(1600, 900) + sun_vector = Vec3(BAKE_SUN_VECTOR).normalized() + + diameter = (BAKE_MESH_END - BAKE_MESH_START).length() + model_center = (BAKE_MESH_START + BAKE_MESH_END) * 0.5 + sun_shadow_map_resolution = 8192 + + + # model = loader.load_model("resources/test-scene.bam") + model = loader.load_model("scene/scene.bam") + model.reparent_to(render) + model.flatten_strong() + + + print("Rendering sun shadow map ..") + sun_shadow_cam = Camera("SunShadowCamera") + sun_shadow_lens = OrthographicLens() + sun_shadow_lens.set_film_size(diameter * 2, diameter * 2) + sun_shadow_lens.set_near_far(0, 2 * diameter) + sun_shadow_cam.set_lens(sun_shadow_lens) + sun_shadow_cam_np = render.attach_new_node(sun_shadow_cam) + sun_shadow_cam_np.set_pos(model_center + sun_vector * diameter) + sun_shadow_cam_np.look_at(model_center) + + sun_shadow_target = RenderTarget() + sun_shadow_target.size = sun_shadow_map_resolution + sun_shadow_target.add_depth_attachment(bits=32) + sun_shadow_target.prepare_render(sun_shadow_cam_np) + + self.graphicsEngine.render_frame() + sun_shadow_target.active = False + shadow_mvp = self.get_mvp(sun_shadow_cam_np) + + # Load the dataset + dataset = loader.load_texture("raw-bake.png") + render.set_shader_input("GIDataTexture", dataset) + + # Load the display shader + shader = Shader.load(Shader.SL_GLSL, "resources/display.vert.glsl", "resources/display.frag.glsl") + render.set_shader(shader) + + render.set_shader_inputs( + ShadowMap=sun_shadow_target.depth_tex, + shadowMVP=shadow_mvp, + sunVector=sun_vector) + + # Render spheres distributed over the mesh + mesh_size = BAKE_MESH_END - BAKE_MESH_START + + for i in range(11): + for j in range(11): + for k in range(11): + offs_x = i / 10.0 * mesh_size.x + BAKE_MESH_START.x + offs_y = j / 10.0 * mesh_size.y + BAKE_MESH_START.y + offs_z = k / 10.0 * mesh_size.z + BAKE_MESH_START.z + + sphere = loader.load_model("resources/sphere.bam") + sphere.reparent_to(render) + sphere.set_scale(0.02) + sphere.set_pos(offs_x, offs_y, offs_z) + + + def get_mvp(self, cam_node): + """ Computes the view-projection matrix of a camera """ + return render.get_transform(cam_node).get_mat() * cam_node.node().get_lens().get_projection_mat() + + +Application().run() + diff --git a/RenderPipelineFile/toolkit/bake_gi/resources/convolute.frag.glsl b/RenderPipelineFile/toolkit/bake_gi/resources/convolute.frag.glsl index c66f93d3..a949c069 100644 --- a/RenderPipelineFile/toolkit/bake_gi/resources/convolute.frag.glsl +++ b/RenderPipelineFile/toolkit/bake_gi/resources/convolute.frag.glsl @@ -1,65 +1,65 @@ -#version 430 - -uniform samplerCube SourceTex; -layout(rgba16f) uniform image2D DestTex; -uniform ivec2 storeCoord; -// out vec3 color; - -#define TWO_PI 6.2831853071795864769252867 - -vec2 hammersley(uint i, uint N) { - return vec2(float(i) / float(N), float(bitfieldReverse(i)) * 2.3283064365386963e-10); -} - -vec3 importance_sample_lambert(vec2 xi) { - float phi = TWO_PI * xi.x; - float cos_theta = sqrt(xi.y); - float sin_theta = sqrt(1 - cos_theta * cos_theta); - vec3 H; - H.x = sin_theta * cos(phi); - H.y = sin_theta * sin(phi); - H.z = cos_theta; - return vec3(H); -} - -vec3 get_sky_color(vec3 v) { - return vec3(51, 137, 233) / 255.0 * max(0, v.z) * 0.5; -} - -void main() { - int offs = int(gl_FragCoord.x); - vec3 direction = vec3(0); - switch(offs) { - case 0: direction = vec3(1, 0, 0); break; - case 1: direction = vec3(-1, 0, 0); break; - case 2: direction = vec3(0, 1, 0); break; - case 3: direction = vec3(0, -1, 0); break; - case 4: direction = vec3(0, 0, 1); break; - case 5: direction = vec3(0, 0, -1); break; - } - - const int num_samples = 512; - - // Find tangent / binormal - vec3 v0 = abs(direction.z) < 0.999 ? vec3(0, 0, 1) : vec3(0, 1, 0); - vec3 tangent = normalize(cross(v0, direction)); - vec3 bitangent = normalize(cross(tangent, direction)); - - vec3 accum = vec3(0); - - for (uint i = 0; i < num_samples; ++i) { - vec2 xi = hammersley(i, num_samples); - vec3 h = importance_sample_lambert(xi); - h = normalize(h.x * tangent + h.y * bitangent + h.z * direction); - - // vec3 l = -reflect(direction, h); - vec4 sampled_color = textureLod(SourceTex, h, 0); - accum += sampled_color.xyz * sampled_color.w + get_sky_color(h) * (1 - sampled_color.w); - } - - accum /= num_samples; - - // color = vec3(accum); - - imageStore(DestTex, ivec2(storeCoord.x * 6 + gl_FragCoord.x, storeCoord.y), vec4(accum, 1)); -} +#version 430 + +uniform samplerCube SourceTex; +layout(rgba16f) uniform image2D DestTex; +uniform ivec2 storeCoord; +// out vec3 color; + +#define TWO_PI 6.2831853071795864769252867 + +vec2 hammersley(uint i, uint N) { + return vec2(float(i) / float(N), float(bitfieldReverse(i)) * 2.3283064365386963e-10); +} + +vec3 importance_sample_lambert(vec2 xi) { + float phi = TWO_PI * xi.x; + float cos_theta = sqrt(xi.y); + float sin_theta = sqrt(1 - cos_theta * cos_theta); + vec3 H; + H.x = sin_theta * cos(phi); + H.y = sin_theta * sin(phi); + H.z = cos_theta; + return vec3(H); +} + +vec3 get_sky_color(vec3 v) { + return vec3(51, 137, 233) / 255.0 * max(0, v.z) * 0.5; +} + +void main() { + int offs = int(gl_FragCoord.x); + vec3 direction = vec3(0); + switch(offs) { + case 0: direction = vec3(1, 0, 0); break; + case 1: direction = vec3(-1, 0, 0); break; + case 2: direction = vec3(0, 1, 0); break; + case 3: direction = vec3(0, -1, 0); break; + case 4: direction = vec3(0, 0, 1); break; + case 5: direction = vec3(0, 0, -1); break; + } + + const int num_samples = 512; + + // Find tangent / binormal + vec3 v0 = abs(direction.z) < 0.999 ? vec3(0, 0, 1) : vec3(0, 1, 0); + vec3 tangent = normalize(cross(v0, direction)); + vec3 bitangent = normalize(cross(tangent, direction)); + + vec3 accum = vec3(0); + + for (uint i = 0; i < num_samples; ++i) { + vec2 xi = hammersley(i, num_samples); + vec3 h = importance_sample_lambert(xi); + h = normalize(h.x * tangent + h.y * bitangent + h.z * direction); + + // vec3 l = -reflect(direction, h); + vec4 sampled_color = textureLod(SourceTex, h, 0); + accum += sampled_color.xyz * sampled_color.w + get_sky_color(h) * (1 - sampled_color.w); + } + + accum /= num_samples; + + // color = vec3(accum); + + imageStore(DestTex, ivec2(storeCoord.x * 6 + gl_FragCoord.x, storeCoord.y), vec4(accum, 1)); +} diff --git a/RenderPipelineFile/toolkit/bake_gi/resources/copy_cubemap.frag.glsl b/RenderPipelineFile/toolkit/bake_gi/resources/copy_cubemap.frag.glsl index db13dcbc..79584d16 100644 --- a/RenderPipelineFile/toolkit/bake_gi/resources/copy_cubemap.frag.glsl +++ b/RenderPipelineFile/toolkit/bake_gi/resources/copy_cubemap.frag.glsl @@ -1,16 +1,16 @@ -#version 430 - -uniform sampler2D SourceTex; -uniform writeonly imageCube DestTex; - -void main() { - ivec2 coord = ivec2(gl_FragCoord.xy); - int size = textureSize(SourceTex, 0).y; - vec4 source_data = texelFetch(SourceTex, coord, 0); - - // Convert to local cubemap coordinate - int offset = coord.x / size; - coord.x = coord.x % size; - - imageStore(DestTex, ivec3(coord.x, coord.y, offset), source_data); -} +#version 430 + +uniform sampler2D SourceTex; +uniform writeonly imageCube DestTex; + +void main() { + ivec2 coord = ivec2(gl_FragCoord.xy); + int size = textureSize(SourceTex, 0).y; + vec4 source_data = texelFetch(SourceTex, coord, 0); + + // Convert to local cubemap coordinate + int offset = coord.x / size; + coord.x = coord.x % size; + + imageStore(DestTex, ivec3(coord.x, coord.y, offset), source_data); +} diff --git a/RenderPipelineFile/toolkit/bake_gi/resources/default.vert.glsl b/RenderPipelineFile/toolkit/bake_gi/resources/default.vert.glsl index 4b469e0c..57b168fa 100644 --- a/RenderPipelineFile/toolkit/bake_gi/resources/default.vert.glsl +++ b/RenderPipelineFile/toolkit/bake_gi/resources/default.vert.glsl @@ -1,6 +1,6 @@ -#version 430 -in vec4 p3d_Vertex; - -void main() { - gl_Position = vec4(p3d_Vertex.xz, 0, 1); -} +#version 430 +in vec4 p3d_Vertex; + +void main() { + gl_Position = vec4(p3d_Vertex.xz, 0, 1); +} diff --git a/RenderPipelineFile/toolkit/bake_gi/resources/display.frag.glsl b/RenderPipelineFile/toolkit/bake_gi/resources/display.frag.glsl index 1a5dc1e9..710fda02 100644 --- a/RenderPipelineFile/toolkit/bake_gi/resources/display.frag.glsl +++ b/RenderPipelineFile/toolkit/bake_gi/resources/display.frag.glsl @@ -1,93 +1,93 @@ -#version 430 - -#pragma include "../_bake_params.glsl" - -in vec3 ws_normal; -in vec3 ws_position; -in vec2 ms_uv; - -uniform sampler2D p3d_Texture0; -uniform sampler2D GIDataTexture; - -uniform sampler2D ShadowMap; -uniform mat4 shadowMVP; -uniform vec3 sunVector; - -out vec3 color; - -vec3 get_probe_at(ivec3 coord, vec3 nrm) { - int offs = coord.x + coord.y * bake_mesh_probecount.x + - coord.z * bake_mesh_probecount.y * bake_mesh_probecount.x; - if (offs > bake_mesh_probecount.x * bake_mesh_probecount.y * bake_mesh_probecount.z || - offs < 0) { - return vec3(1, 0, 0); - } - - ivec2 base_coord = ivec2((offs % bake_divisor) * 6, offs / bake_divisor); - vec3 baked = vec3(0); - baked += texelFetch( - GIDataTexture, ivec2(0, 0) + base_coord, 0).xyz * max(0, dot(nrm, vec3(1, 0, 0))); - baked += texelFetch( - GIDataTexture, ivec2(1, 0) + base_coord, 0).xyz * max(0, dot(nrm, vec3(-1, 0, 0))); - baked += texelFetch( - GIDataTexture, ivec2(2, 0) + base_coord, 0).xyz * max(0, dot(nrm, vec3(0, 1, 0))); - baked += texelFetch( - GIDataTexture, ivec2(3, 0) + base_coord, 0).xyz * max(0, dot(nrm, vec3(0, -1, 0))); - baked += texelFetch( - GIDataTexture, ivec2(4, 0) + base_coord, 0).xyz * max(0, dot(nrm, vec3(0, 0, 1))); - baked += texelFetch( - GIDataTexture, ivec2(5, 0) + base_coord, 0).xyz * max(0, dot(nrm, vec3(0, 0, -1))); - - return baked; -} - -void main() { - - vec3 biased_position = ws_position + 0.4 * ws_normal; - vec3 local_coord = (biased_position - bake_mesh_start) / (bake_mesh_end - bake_mesh_start); - - ivec3 local_index = ivec3(local_coord * bake_mesh_probecount); - vec3 fract_coord = (local_coord * bake_mesh_probecount - local_index); - - // 3D Lerp - vec3 probe_000 = get_probe_at(local_index + ivec3(0, 0, 0), ws_normal); - vec3 probe_001 = get_probe_at(local_index + ivec3(0, 0, 1), ws_normal); - vec3 probe_010 = get_probe_at(local_index + ivec3(0, 1, 0), ws_normal); - vec3 probe_011 = get_probe_at(local_index + ivec3(0, 1, 1), ws_normal); - vec3 probe_100 = get_probe_at(local_index + ivec3(1, 0, 0), ws_normal); - vec3 probe_101 = get_probe_at(local_index + ivec3(1, 0, 1), ws_normal); - vec3 probe_110 = get_probe_at(local_index + ivec3(1, 1, 0), ws_normal); - vec3 probe_111 = get_probe_at(local_index + ivec3(1, 1, 1), ws_normal); - - vec3 lerp_bottom = mix( - mix(probe_000, probe_100, fract_coord.x), - mix(probe_010, probe_110, fract_coord.x), - fract_coord.y - ); - - vec3 lerp_top = mix( - mix(probe_001, probe_101, fract_coord.x), - mix(probe_011, probe_111, fract_coord.x), - fract_coord.y - ); - - vec3 precomputed_gi = mix(lerp_bottom, lerp_top, fract_coord.z); - - vec3 basecolor = textureLod(p3d_Texture0, ms_uv, 0).xyz; - // basecolor = vec3(0.8); - - - // Shadows - vec4 projected = shadowMVP * vec4(ws_position, 1); - vec3 shadow_space_pos = projected.xyz / projected.w * 0.5 + 0.5; - - float actual_depth = textureLod(ShadowMap, shadow_space_pos.xy, 0).x; - - const float bias = 0.001; - float shadow = actual_depth >= shadow_space_pos.z - bias ? 1.0 : 0.0; - float NxL = max(0, dot(sunVector, ws_normal)); - const vec3 sun_color = vec3(1.4, 1.2, 1.0); - - color = vec3(basecolor * precomputed_gi + basecolor * shadow * sun_color * NxL); - color = pow(color, vec3(1.0 / 2.2)); -} +#version 430 + +#pragma include "../_bake_params.glsl" + +in vec3 ws_normal; +in vec3 ws_position; +in vec2 ms_uv; + +uniform sampler2D p3d_Texture0; +uniform sampler2D GIDataTexture; + +uniform sampler2D ShadowMap; +uniform mat4 shadowMVP; +uniform vec3 sunVector; + +out vec3 color; + +vec3 get_probe_at(ivec3 coord, vec3 nrm) { + int offs = coord.x + coord.y * bake_mesh_probecount.x + + coord.z * bake_mesh_probecount.y * bake_mesh_probecount.x; + if (offs > bake_mesh_probecount.x * bake_mesh_probecount.y * bake_mesh_probecount.z || + offs < 0) { + return vec3(1, 0, 0); + } + + ivec2 base_coord = ivec2((offs % bake_divisor) * 6, offs / bake_divisor); + vec3 baked = vec3(0); + baked += texelFetch( + GIDataTexture, ivec2(0, 0) + base_coord, 0).xyz * max(0, dot(nrm, vec3(1, 0, 0))); + baked += texelFetch( + GIDataTexture, ivec2(1, 0) + base_coord, 0).xyz * max(0, dot(nrm, vec3(-1, 0, 0))); + baked += texelFetch( + GIDataTexture, ivec2(2, 0) + base_coord, 0).xyz * max(0, dot(nrm, vec3(0, 1, 0))); + baked += texelFetch( + GIDataTexture, ivec2(3, 0) + base_coord, 0).xyz * max(0, dot(nrm, vec3(0, -1, 0))); + baked += texelFetch( + GIDataTexture, ivec2(4, 0) + base_coord, 0).xyz * max(0, dot(nrm, vec3(0, 0, 1))); + baked += texelFetch( + GIDataTexture, ivec2(5, 0) + base_coord, 0).xyz * max(0, dot(nrm, vec3(0, 0, -1))); + + return baked; +} + +void main() { + + vec3 biased_position = ws_position + 0.4 * ws_normal; + vec3 local_coord = (biased_position - bake_mesh_start) / (bake_mesh_end - bake_mesh_start); + + ivec3 local_index = ivec3(local_coord * bake_mesh_probecount); + vec3 fract_coord = (local_coord * bake_mesh_probecount - local_index); + + // 3D Lerp + vec3 probe_000 = get_probe_at(local_index + ivec3(0, 0, 0), ws_normal); + vec3 probe_001 = get_probe_at(local_index + ivec3(0, 0, 1), ws_normal); + vec3 probe_010 = get_probe_at(local_index + ivec3(0, 1, 0), ws_normal); + vec3 probe_011 = get_probe_at(local_index + ivec3(0, 1, 1), ws_normal); + vec3 probe_100 = get_probe_at(local_index + ivec3(1, 0, 0), ws_normal); + vec3 probe_101 = get_probe_at(local_index + ivec3(1, 0, 1), ws_normal); + vec3 probe_110 = get_probe_at(local_index + ivec3(1, 1, 0), ws_normal); + vec3 probe_111 = get_probe_at(local_index + ivec3(1, 1, 1), ws_normal); + + vec3 lerp_bottom = mix( + mix(probe_000, probe_100, fract_coord.x), + mix(probe_010, probe_110, fract_coord.x), + fract_coord.y + ); + + vec3 lerp_top = mix( + mix(probe_001, probe_101, fract_coord.x), + mix(probe_011, probe_111, fract_coord.x), + fract_coord.y + ); + + vec3 precomputed_gi = mix(lerp_bottom, lerp_top, fract_coord.z); + + vec3 basecolor = textureLod(p3d_Texture0, ms_uv, 0).xyz; + // basecolor = vec3(0.8); + + + // Shadows + vec4 projected = shadowMVP * vec4(ws_position, 1); + vec3 shadow_space_pos = projected.xyz / projected.w * 0.5 + 0.5; + + float actual_depth = textureLod(ShadowMap, shadow_space_pos.xy, 0).x; + + const float bias = 0.001; + float shadow = actual_depth >= shadow_space_pos.z - bias ? 1.0 : 0.0; + float NxL = max(0, dot(sunVector, ws_normal)); + const vec3 sun_color = vec3(1.4, 1.2, 1.0); + + color = vec3(basecolor * precomputed_gi + basecolor * shadow * sun_color * NxL); + color = pow(color, vec3(1.0 / 2.2)); +} diff --git a/RenderPipelineFile/toolkit/bake_gi/resources/display.vert.glsl b/RenderPipelineFile/toolkit/bake_gi/resources/display.vert.glsl index 0c7f497b..7188a4e7 100644 --- a/RenderPipelineFile/toolkit/bake_gi/resources/display.vert.glsl +++ b/RenderPipelineFile/toolkit/bake_gi/resources/display.vert.glsl @@ -1,20 +1,20 @@ -#version 430 - -in vec4 p3d_Vertex; -in vec3 p3d_Normal; -in vec2 p3d_MultiTexCoord0; - -uniform mat4 p3d_ViewProjectionMatrix; -uniform mat4 p3d_ModelMatrix; -uniform mat3 tpose_world_to_model; - -out vec3 ws_position; -out vec3 ws_normal; -out vec2 ms_uv; - -void main() { - ms_uv = p3d_MultiTexCoord0; - ws_normal = normalize(tpose_world_to_model * p3d_Normal).xyz; - ws_position = (p3d_ModelMatrix * p3d_Vertex).xyz; - gl_Position = p3d_ViewProjectionMatrix * vec4(ws_position, 1); -} +#version 430 + +in vec4 p3d_Vertex; +in vec3 p3d_Normal; +in vec2 p3d_MultiTexCoord0; + +uniform mat4 p3d_ViewProjectionMatrix; +uniform mat4 p3d_ModelMatrix; +uniform mat3 tpose_world_to_model; + +out vec3 ws_position; +out vec3 ws_normal; +out vec2 ms_uv; + +void main() { + ms_uv = p3d_MultiTexCoord0; + ws_normal = normalize(tpose_world_to_model * p3d_Normal).xyz; + ws_position = (p3d_ModelMatrix * p3d_Vertex).xyz; + gl_Position = p3d_ViewProjectionMatrix * vec4(ws_position, 1); +} diff --git a/RenderPipelineFile/toolkit/bake_gi/resources/first-bounce.frag.glsl b/RenderPipelineFile/toolkit/bake_gi/resources/first-bounce.frag.glsl index e5d58997..379b3cbc 100644 --- a/RenderPipelineFile/toolkit/bake_gi/resources/first-bounce.frag.glsl +++ b/RenderPipelineFile/toolkit/bake_gi/resources/first-bounce.frag.glsl @@ -1,32 +1,32 @@ -#version 430 - -in vec3 ws_position; -in vec3 ws_normal; -in vec2 ws_uv; - -uniform sampler2D p3d_Texture0; -uniform sampler2D ShadowMap; -uniform mat4 shadowMVP; -uniform vec3 sunVector; - -out vec4 color; - -const vec3 sun_color = vec3(1.4, 1.1, 1.0) * 1; - -void main() { - vec4 projected = shadowMVP * vec4(ws_position, 1); - vec3 shadow_space_pos = projected.xyz / projected.w * 0.5 + 0.5; - - float actual_depth = textureLod(ShadowMap, shadow_space_pos.xy, 0).x; - - const float bias = 0.001; - float shadow = actual_depth >= shadow_space_pos.z - bias ? 1.0 : 0.0; - - float NxL = max(0.0, dot(ws_normal, sunVector)); - vec3 basecolor = texture(p3d_Texture0, ws_uv).xyz; - // basecolor = vec3(0.8); - - vec3 ambient = vec3(0.0001) * basecolor; - - color = vec4(basecolor * NxL * shadow * sun_color + ambient, 1); -} +#version 430 + +in vec3 ws_position; +in vec3 ws_normal; +in vec2 ws_uv; + +uniform sampler2D p3d_Texture0; +uniform sampler2D ShadowMap; +uniform mat4 shadowMVP; +uniform vec3 sunVector; + +out vec4 color; + +const vec3 sun_color = vec3(1.4, 1.1, 1.0) * 1; + +void main() { + vec4 projected = shadowMVP * vec4(ws_position, 1); + vec3 shadow_space_pos = projected.xyz / projected.w * 0.5 + 0.5; + + float actual_depth = textureLod(ShadowMap, shadow_space_pos.xy, 0).x; + + const float bias = 0.001; + float shadow = actual_depth >= shadow_space_pos.z - bias ? 1.0 : 0.0; + + float NxL = max(0.0, dot(ws_normal, sunVector)); + vec3 basecolor = texture(p3d_Texture0, ws_uv).xyz; + // basecolor = vec3(0.8); + + vec3 ambient = vec3(0.0001) * basecolor; + + color = vec4(basecolor * NxL * shadow * sun_color + ambient, 1); +} diff --git a/RenderPipelineFile/toolkit/bake_gi/resources/first-bounce.vert.glsl b/RenderPipelineFile/toolkit/bake_gi/resources/first-bounce.vert.glsl index 0c7f497b..7188a4e7 100644 --- a/RenderPipelineFile/toolkit/bake_gi/resources/first-bounce.vert.glsl +++ b/RenderPipelineFile/toolkit/bake_gi/resources/first-bounce.vert.glsl @@ -1,20 +1,20 @@ -#version 430 - -in vec4 p3d_Vertex; -in vec3 p3d_Normal; -in vec2 p3d_MultiTexCoord0; - -uniform mat4 p3d_ViewProjectionMatrix; -uniform mat4 p3d_ModelMatrix; -uniform mat3 tpose_world_to_model; - -out vec3 ws_position; -out vec3 ws_normal; -out vec2 ms_uv; - -void main() { - ms_uv = p3d_MultiTexCoord0; - ws_normal = normalize(tpose_world_to_model * p3d_Normal).xyz; - ws_position = (p3d_ModelMatrix * p3d_Vertex).xyz; - gl_Position = p3d_ViewProjectionMatrix * vec4(ws_position, 1); -} +#version 430 + +in vec4 p3d_Vertex; +in vec3 p3d_Normal; +in vec2 p3d_MultiTexCoord0; + +uniform mat4 p3d_ViewProjectionMatrix; +uniform mat4 p3d_ModelMatrix; +uniform mat3 tpose_world_to_model; + +out vec3 ws_position; +out vec3 ws_normal; +out vec2 ms_uv; + +void main() { + ms_uv = p3d_MultiTexCoord0; + ws_normal = normalize(tpose_world_to_model * p3d_Normal).xyz; + ws_position = (p3d_ModelMatrix * p3d_Vertex).xyz; + gl_Position = p3d_ViewProjectionMatrix * vec4(ws_position, 1); +} diff --git a/RenderPipelineFile/toolkit/day_time_editor/curve_widget.py b/RenderPipelineFile/toolkit/day_time_editor/curve_widget.py index 44bfbd88..c7f07a04 100644 --- a/RenderPipelineFile/toolkit/day_time_editor/curve_widget.py +++ b/RenderPipelineFile/toolkit/day_time_editor/curve_widget.py @@ -1,344 +1,344 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -from __future__ import print_function - -# from PyQt5 import Qt -# from PyQt5.QtGui import QPainter, QColor, QPen -# from PyQt5.QtWidgets import QWidget -from six.moves import range # pylint: disable=import-error - -import math - -# 修复导入路径问题 -from rplibs.pyqt_imports import * # noqa - - -class CurveWidget(QWidget): - - """ This is a resizeable Widget which shows an editable curve which can - be modified. """ - - def __init__(self, parent): - """ Constructs the CurveWidget, we start with an initial curve """ - QWidget.__init__(self, parent) - self.setFocusPolicy(Qt.ClickFocus) - self._curves = [] - - # Store current display time - self._current_time = 0.5 - - # Widget render constants - self._cv_point_size = 3 - self._legend_border = 52 - self._bar_h = 30 - - # Currently dragged control point, format is: - # (CurveIndex, PointIndex, Drag-Offset (x,y)) - self._drag_point = None - self._drag_time = 0.0 - - # Currently selected control point, format is: - # (CurveIndex, PointIndex) - self._selected_point = None - - self._unit_processor = lambda v: str(round(v, 2)) - self._change_handler = lambda: None - - def set_unit_processor(self, proc): - """ Sets the function which gets called to map values from 0 .. 1 to - values like 10% to 30% """ - self._unit_processor = proc - - def set_change_handler(self, handler): - """ Sets a function which gets called when the data in this widget got - edited """ - self._change_handler = handler - - def paintEvent(self, e): # noqa - """ Internal QT paint event, draws the entire widget """ - qp = QPainter() - qp.begin(self) - self._draw(qp) - qp.end() - - def mousePressEvent(self, QMouseEvent): # noqa - """ Internal mouse-press handler """ - self._drag_point = None - self._selected_point = None - mouse_pos = QMouseEvent.pos() - mouse_x = mouse_pos.x() - self._legend_border - mouse_y = mouse_pos.y() - self._bar_h - - for index, curve in enumerate(self._curves): - - # Check for clicks on control points - for cv_index, (x, y) in enumerate(curve.control_points): - point_x = self._get_x_value_for(x) - point_y = self._get_y_value_for(y) - self._bar_h - if abs(point_x - mouse_x) < self._cv_point_size + 6: - if (abs(point_y - mouse_y)) < self._cv_point_size + 6: - drag_x_offset = mouse_x - point_x - drag_y_offset = mouse_y - point_y - mpos_relx = float(mouse_x) / (self.width() - self._legend_border) - self._drag_point = (index, cv_index, (drag_x_offset, drag_y_offset)) - self._drag_time = mpos_relx - self._selected_point = (index, cv_index) - - # If still no intersection, check if we clicked a curve - if mouse_x > 0 and mouse_x < self.width() - self._legend_border: - if mouse_y > 0 and mouse_y < self.height() - self._legend_border: - mpos_relx = float(mouse_x) / (self.width() - self._legend_border) - curve_py = curve.get_value(mpos_relx) - curve_offy = self._get_y_value_for(curve_py) - self._bar_h - - if abs(curve_offy - mouse_y) < 8 and self._selected_point is None: - # Clicked on curve, spawn new point - cv_index = curve.append_cv(mpos_relx, curve_py) - - self._selected_point = (index, cv_index) - self._drag_point = (index, cv_index, (0, 0)) - self._drag_time = mpos_relx - self._change_handler() - self.update() - - def mouseReleaseEvent(self, event): # noqa - """ Internal mouse-release handler """ - self._drag_point = None - self._drag_time = -1 - self.update() - - def mouseMoveEvent(self, event): # noqa - """ Internal mouse-move handler """ - if len(self._curves) < 1: - return - - if self._drag_point is not None: - mouse_x = event.pos().x() - self._drag_point[2][0] - self._legend_border - mouse_y = event.pos().y() - self._drag_point[2][1] - self._bar_h - - # Convert to local coordinate - local_x = max(0, min(1, mouse_x / float(self.width() - self._legend_border))) - local_y = mouse_y / float(self.height() - self._legend_border - self._bar_h) - local_y = 1 - max(0, min(1, local_y)) - - self._drag_time = local_x - - # Set new point data - self._curves[self._drag_point[0]].set_cv_value(self._drag_point[1], local_x, local_y) - - # Redraw curve - self._curves[self._drag_point[0]].build_curve() - self.update() - self._change_handler() - - def keyPressEvent(self, event): # noqa - """ Internal keypress handler """ - # Delete anchor point - if event.key() == Qt.Key_Delete: - self.delete_current_point() - - def delete_current_point(self): - """ Deletes the currently selected point """ - if self._selected_point is not None: - self._curves[self._selected_point[0]].remove_cv(self._selected_point[1]) - self._selected_point = None - self._drag_point = None - self.update() - self._change_handler() - - def set_curves(self, curves): - """ Sets the list of displayed curves """ - self._selected_point = None - self._drag_point = None - self._curves = curves - self.update() - - def _get_y_value_for(self, local_value): - """ Converts a value from 0 to 1 to a value from 0 .. canvas height """ - local_value = max(0, min(1.0, 1.0 - local_value)) - local_value *= self.height() - self._legend_border - self._bar_h - local_value += self._bar_h - return local_value - - def _get_x_value_for(self, local_value): - """ Converts a value from 0 to 1 to a value from 0 .. canvas width """ - local_value = max(0, min(1.0, local_value)) - local_value *= self.width() - self._legend_border - return local_value - - def set_current_time(self, local_time): - """ Sets the current displayed time, should range from 0 to 1 """ - self._current_time = max(0.0, min(1.0, local_time)) - self.update() - - def _draw(self, painter): - """ Internal method to draw the widget """ - - canvas_width = self.width() - self._legend_border - canvas_height = self.height() - self._legend_border - self._bar_h - - # Draw field background - # painter.setPen(QColor(200, 200, 200)) - # painter.setBrush(QColor(230, 230, 230)) - # painter.drawRect(0, 0, self.width() - 1, self.height() - 1) - - # Draw legend - - # Compute amount of horizontal / vertical lines - num_vert_lines = 12 # 24 / 12 = 2, one entry per 2 hours - line_spacing_x = (self.width() - self._legend_border) / num_vert_lines - line_spacing_y = (self.height() - self._legend_border - self._bar_h) / 20.0 - num_horiz_lines = int(math.ceil(canvas_height / float(line_spacing_y)) + 1) - - # Draw vertical lines - painter.setPen(QColor(200, 200, 200)) - for i in range(num_vert_lines + 1): - line_pos = i * line_spacing_x + self._legend_border - 1 - painter.drawLine(int(line_pos), self._bar_h, int(line_pos), canvas_height + self._bar_h) - - # Draw horizontal lines - painter.setPen(QColor(200, 200, 200)) - for i in range(num_horiz_lines): - line_pos = canvas_height - i * line_spacing_y + self._bar_h - painter.drawLine(self._legend_border, int(line_pos), self.width(), int(line_pos)) - - # Draw vetical legend labels - painter.setPen(QColor(120, 120, 120)) - for i in range(num_horiz_lines): - line_pos = canvas_height - i * line_spacing_y + self._bar_h - # painter.drawText(6, line_pos + 3, str(round(float(i) / (num_horiz_lines-1), 2))) - painter.drawText( - 6, int(line_pos + 3), self._unit_processor(float(i) / (num_horiz_lines - 1))) - - # Draw horizontal legend labels - for i in range(num_vert_lines + 1): - line_pos = i * line_spacing_x + self._legend_border - offpos_x = -14 - if i == 0: - offpos_x = -2 - elif i == num_vert_lines: - offpos_x = -27 - time_string = str(int(float(i) / num_vert_lines * 24)).zfill(2) + ":00" - painter.drawText(int(line_pos + offpos_x), canvas_height + self._bar_h + 18, time_string) - - # Draw curve - for index, curve in enumerate(self._curves): - painter.setPen(QColor(*curve.color)) - last_value = 0 - for i in range(canvas_width): - rel_offset = i / (canvas_width - 1.0) - curve_height = self._get_y_value_for(curve.get_value(rel_offset)) - - if i == 0: - last_value = curve_height - - painter.drawLine( - int(self._legend_border + i - 1), int(last_value), int(self._legend_border + i), int(curve_height)) - last_value = curve_height - - # Draw the CV points of the curve - painter.setBrush(QColor(240, 240, 240)) - - for cv_index, (x, y) in enumerate(curve.control_points): - offs_x = x * canvas_width + self._legend_border - offs_y = (1 - y) * canvas_height + self._bar_h - - if (self._selected_point and self._selected_point[0] == index and - self._selected_point[1] == cv_index): - painter.setPen(QColor(255, 0, 0)) - else: - painter.setPen(QColor(100, 100, 100)) - painter.drawRect( - int(offs_x - self._cv_point_size), int(offs_y - self._cv_point_size), - 2 * self._cv_point_size, 2 * self._cv_point_size) - - # Draw bar background - bar_half_height = 4 - bar_top_pos = 10 - - painter.setBrush(QColor(255, 0, 0)) - painter.setPen(QColor(110, 110, 110)) - - painter.drawRect( - self._legend_border - 1, bar_top_pos - 1, - self.width() - self._legend_border, 2 * bar_half_height + 2) - - # Draw bar - if len(self._curves) == 0: - return - - if len(self._curves) == 1: - bar_curve = self._curves[0] - else: - bar_curve = self._curves[0:3] - - for i in range(canvas_width - 1): - xpos = self._legend_border + i - relv = float(i) / float(canvas_width) - - if len(self._curves) == 1: - val = max(0, min(255, int(bar_curve.get_value(relv) * 255.0))) - painter.setPen(QColor(val, val, val)) - else: - r = max(0, min(255, int(bar_curve[0].get_value(relv) * 255.0))) - g = max(0, min(255, int(bar_curve[1].get_value(relv) * 255.0))) - b = max(0, min(255, int(bar_curve[2].get_value(relv) * 255.0))) - painter.setPen(QColor(r, g, b)) - painter.drawLine(int(xpos), bar_top_pos, int(xpos), bar_top_pos + 2 * bar_half_height) - - # Draw selected time - if self._drag_point: - painter.setBrush(QColor(200, 200, 200)) - painter.setPen(QColor(90, 90, 90)) - offs_x = max(0, min( - canvas_width + 10, self._drag_time * canvas_width + self._legend_border - 19)) - offs_y = self.height() - self._legend_border - minutes = int(self._drag_time * 24 * 60) - hours = int(minutes / 60) - minutes = minutes % 60 - painter.drawRect(int(offs_x), self.height() - self._legend_border + 5, 40, 20) - painter.drawText(int(offs_x + 7), offs_y + 20, "{:02}:{:02}".format(int(hours), int(minutes))) - - painter.setPen(QColor(150, 150, 150)) - painter.drawLine( - int(offs_x) + 19, bar_top_pos + 15, int(offs_x + 19), self.height() - self._legend_border + 5) - - # Display current time - pen = QPen() - pen.setColor(QColor(255, 100, 100)) - pen.setStyle(Qt.DashLine) - painter.setPen(pen) - - xoffs = self._legend_border + self._current_time * (canvas_width - 1) - painter.drawLine(int(xoffs), self._bar_h, int(xoffs), self._bar_h + canvas_height) - - # Draw usage hints - painter.setPen(QColor(100, 100, 100)) - painter.drawText( - 5, self.height() - 2, - "Click on the curve to add new control points, click and drag " - "existing points to move them.") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +from __future__ import print_function + +# from PyQt5 import Qt +# from PyQt5.QtGui import QPainter, QColor, QPen +# from PyQt5.QtWidgets import QWidget +from six.moves import range # pylint: disable=import-error + +import math + +# 修复导入路径问题 +from rplibs.pyqt_imports import * # noqa + + +class CurveWidget(QWidget): + + """ This is a resizeable Widget which shows an editable curve which can + be modified. """ + + def __init__(self, parent): + """ Constructs the CurveWidget, we start with an initial curve """ + QWidget.__init__(self, parent) + self.setFocusPolicy(Qt.ClickFocus) + self._curves = [] + + # Store current display time + self._current_time = 0.5 + + # Widget render constants + self._cv_point_size = 3 + self._legend_border = 52 + self._bar_h = 30 + + # Currently dragged control point, format is: + # (CurveIndex, PointIndex, Drag-Offset (x,y)) + self._drag_point = None + self._drag_time = 0.0 + + # Currently selected control point, format is: + # (CurveIndex, PointIndex) + self._selected_point = None + + self._unit_processor = lambda v: str(round(v, 2)) + self._change_handler = lambda: None + + def set_unit_processor(self, proc): + """ Sets the function which gets called to map values from 0 .. 1 to + values like 10% to 30% """ + self._unit_processor = proc + + def set_change_handler(self, handler): + """ Sets a function which gets called when the data in this widget got + edited """ + self._change_handler = handler + + def paintEvent(self, e): # noqa + """ Internal QT paint event, draws the entire widget """ + qp = QPainter() + qp.begin(self) + self._draw(qp) + qp.end() + + def mousePressEvent(self, QMouseEvent): # noqa + """ Internal mouse-press handler """ + self._drag_point = None + self._selected_point = None + mouse_pos = QMouseEvent.pos() + mouse_x = mouse_pos.x() - self._legend_border + mouse_y = mouse_pos.y() - self._bar_h + + for index, curve in enumerate(self._curves): + + # Check for clicks on control points + for cv_index, (x, y) in enumerate(curve.control_points): + point_x = self._get_x_value_for(x) + point_y = self._get_y_value_for(y) - self._bar_h + if abs(point_x - mouse_x) < self._cv_point_size + 6: + if (abs(point_y - mouse_y)) < self._cv_point_size + 6: + drag_x_offset = mouse_x - point_x + drag_y_offset = mouse_y - point_y + mpos_relx = float(mouse_x) / (self.width() - self._legend_border) + self._drag_point = (index, cv_index, (drag_x_offset, drag_y_offset)) + self._drag_time = mpos_relx + self._selected_point = (index, cv_index) + + # If still no intersection, check if we clicked a curve + if mouse_x > 0 and mouse_x < self.width() - self._legend_border: + if mouse_y > 0 and mouse_y < self.height() - self._legend_border: + mpos_relx = float(mouse_x) / (self.width() - self._legend_border) + curve_py = curve.get_value(mpos_relx) + curve_offy = self._get_y_value_for(curve_py) - self._bar_h + + if abs(curve_offy - mouse_y) < 8 and self._selected_point is None: + # Clicked on curve, spawn new point + cv_index = curve.append_cv(mpos_relx, curve_py) + + self._selected_point = (index, cv_index) + self._drag_point = (index, cv_index, (0, 0)) + self._drag_time = mpos_relx + self._change_handler() + self.update() + + def mouseReleaseEvent(self, event): # noqa + """ Internal mouse-release handler """ + self._drag_point = None + self._drag_time = -1 + self.update() + + def mouseMoveEvent(self, event): # noqa + """ Internal mouse-move handler """ + if len(self._curves) < 1: + return + + if self._drag_point is not None: + mouse_x = event.pos().x() - self._drag_point[2][0] - self._legend_border + mouse_y = event.pos().y() - self._drag_point[2][1] - self._bar_h + + # Convert to local coordinate + local_x = max(0, min(1, mouse_x / float(self.width() - self._legend_border))) + local_y = mouse_y / float(self.height() - self._legend_border - self._bar_h) + local_y = 1 - max(0, min(1, local_y)) + + self._drag_time = local_x + + # Set new point data + self._curves[self._drag_point[0]].set_cv_value(self._drag_point[1], local_x, local_y) + + # Redraw curve + self._curves[self._drag_point[0]].build_curve() + self.update() + self._change_handler() + + def keyPressEvent(self, event): # noqa + """ Internal keypress handler """ + # Delete anchor point + if event.key() == Qt.Key_Delete: + self.delete_current_point() + + def delete_current_point(self): + """ Deletes the currently selected point """ + if self._selected_point is not None: + self._curves[self._selected_point[0]].remove_cv(self._selected_point[1]) + self._selected_point = None + self._drag_point = None + self.update() + self._change_handler() + + def set_curves(self, curves): + """ Sets the list of displayed curves """ + self._selected_point = None + self._drag_point = None + self._curves = curves + self.update() + + def _get_y_value_for(self, local_value): + """ Converts a value from 0 to 1 to a value from 0 .. canvas height """ + local_value = max(0, min(1.0, 1.0 - local_value)) + local_value *= self.height() - self._legend_border - self._bar_h + local_value += self._bar_h + return local_value + + def _get_x_value_for(self, local_value): + """ Converts a value from 0 to 1 to a value from 0 .. canvas width """ + local_value = max(0, min(1.0, local_value)) + local_value *= self.width() - self._legend_border + return local_value + + def set_current_time(self, local_time): + """ Sets the current displayed time, should range from 0 to 1 """ + self._current_time = max(0.0, min(1.0, local_time)) + self.update() + + def _draw(self, painter): + """ Internal method to draw the widget """ + + canvas_width = self.width() - self._legend_border + canvas_height = self.height() - self._legend_border - self._bar_h + + # Draw field background + # painter.setPen(QColor(200, 200, 200)) + # painter.setBrush(QColor(230, 230, 230)) + # painter.drawRect(0, 0, self.width() - 1, self.height() - 1) + + # Draw legend + + # Compute amount of horizontal / vertical lines + num_vert_lines = 12 # 24 / 12 = 2, one entry per 2 hours + line_spacing_x = (self.width() - self._legend_border) / num_vert_lines + line_spacing_y = (self.height() - self._legend_border - self._bar_h) / 20.0 + num_horiz_lines = int(math.ceil(canvas_height / float(line_spacing_y)) + 1) + + # Draw vertical lines + painter.setPen(QColor(200, 200, 200)) + for i in range(num_vert_lines + 1): + line_pos = i * line_spacing_x + self._legend_border - 1 + painter.drawLine(int(line_pos), self._bar_h, int(line_pos), canvas_height + self._bar_h) + + # Draw horizontal lines + painter.setPen(QColor(200, 200, 200)) + for i in range(num_horiz_lines): + line_pos = canvas_height - i * line_spacing_y + self._bar_h + painter.drawLine(self._legend_border, int(line_pos), self.width(), int(line_pos)) + + # Draw vetical legend labels + painter.setPen(QColor(120, 120, 120)) + for i in range(num_horiz_lines): + line_pos = canvas_height - i * line_spacing_y + self._bar_h + # painter.drawText(6, line_pos + 3, str(round(float(i) / (num_horiz_lines-1), 2))) + painter.drawText( + 6, int(line_pos + 3), self._unit_processor(float(i) / (num_horiz_lines - 1))) + + # Draw horizontal legend labels + for i in range(num_vert_lines + 1): + line_pos = i * line_spacing_x + self._legend_border + offpos_x = -14 + if i == 0: + offpos_x = -2 + elif i == num_vert_lines: + offpos_x = -27 + time_string = str(int(float(i) / num_vert_lines * 24)).zfill(2) + ":00" + painter.drawText(int(line_pos + offpos_x), canvas_height + self._bar_h + 18, time_string) + + # Draw curve + for index, curve in enumerate(self._curves): + painter.setPen(QColor(*curve.color)) + last_value = 0 + for i in range(canvas_width): + rel_offset = i / (canvas_width - 1.0) + curve_height = self._get_y_value_for(curve.get_value(rel_offset)) + + if i == 0: + last_value = curve_height + + painter.drawLine( + int(self._legend_border + i - 1), int(last_value), int(self._legend_border + i), int(curve_height)) + last_value = curve_height + + # Draw the CV points of the curve + painter.setBrush(QColor(240, 240, 240)) + + for cv_index, (x, y) in enumerate(curve.control_points): + offs_x = x * canvas_width + self._legend_border + offs_y = (1 - y) * canvas_height + self._bar_h + + if (self._selected_point and self._selected_point[0] == index and + self._selected_point[1] == cv_index): + painter.setPen(QColor(255, 0, 0)) + else: + painter.setPen(QColor(100, 100, 100)) + painter.drawRect( + int(offs_x - self._cv_point_size), int(offs_y - self._cv_point_size), + 2 * self._cv_point_size, 2 * self._cv_point_size) + + # Draw bar background + bar_half_height = 4 + bar_top_pos = 10 + + painter.setBrush(QColor(255, 0, 0)) + painter.setPen(QColor(110, 110, 110)) + + painter.drawRect( + self._legend_border - 1, bar_top_pos - 1, + self.width() - self._legend_border, 2 * bar_half_height + 2) + + # Draw bar + if len(self._curves) == 0: + return + + if len(self._curves) == 1: + bar_curve = self._curves[0] + else: + bar_curve = self._curves[0:3] + + for i in range(canvas_width - 1): + xpos = self._legend_border + i + relv = float(i) / float(canvas_width) + + if len(self._curves) == 1: + val = max(0, min(255, int(bar_curve.get_value(relv) * 255.0))) + painter.setPen(QColor(val, val, val)) + else: + r = max(0, min(255, int(bar_curve[0].get_value(relv) * 255.0))) + g = max(0, min(255, int(bar_curve[1].get_value(relv) * 255.0))) + b = max(0, min(255, int(bar_curve[2].get_value(relv) * 255.0))) + painter.setPen(QColor(r, g, b)) + painter.drawLine(int(xpos), bar_top_pos, int(xpos), bar_top_pos + 2 * bar_half_height) + + # Draw selected time + if self._drag_point: + painter.setBrush(QColor(200, 200, 200)) + painter.setPen(QColor(90, 90, 90)) + offs_x = max(0, min( + canvas_width + 10, self._drag_time * canvas_width + self._legend_border - 19)) + offs_y = self.height() - self._legend_border + minutes = int(self._drag_time * 24 * 60) + hours = int(minutes / 60) + minutes = minutes % 60 + painter.drawRect(int(offs_x), self.height() - self._legend_border + 5, 40, 20) + painter.drawText(int(offs_x + 7), offs_y + 20, "{:02}:{:02}".format(int(hours), int(minutes))) + + painter.setPen(QColor(150, 150, 150)) + painter.drawLine( + int(offs_x) + 19, bar_top_pos + 15, int(offs_x + 19), self.height() - self._legend_border + 5) + + # Display current time + pen = QPen() + pen.setColor(QColor(255, 100, 100)) + pen.setStyle(Qt.DashLine) + painter.setPen(pen) + + xoffs = self._legend_border + self._current_time * (canvas_width - 1) + painter.drawLine(int(xoffs), self._bar_h, int(xoffs), self._bar_h + canvas_height) + + # Draw usage hints + painter.setPen(QColor(100, 100, 100)) + painter.drawText( + 5, self.height() - 2, + "Click on the curve to add new control points, click and drag " + "existing points to move them.") diff --git a/RenderPipelineFile/toolkit/day_time_editor/main.py b/RenderPipelineFile/toolkit/day_time_editor/main.py index 82a68231..a13256c3 100644 --- a/RenderPipelineFile/toolkit/day_time_editor/main.py +++ b/RenderPipelineFile/toolkit/day_time_editor/main.py @@ -1,299 +1,299 @@ -#!/usr/bin/env python - -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -# This tool offers an interface to configure the time of day settings - - -from __future__ import print_function, division - -import os -import sys -import time -from threading import Thread - -# Change to the current directory -os.chdir(os.path.join(os.path.dirname(os.path.realpath(__file__)))) - -# Add the render pipeline to the path -sys.path.insert(0, "../../") - -# 修复导入路径问题 -from rplibs.six import iteritems # noqa -from rplibs.pyqt_imports import * # noqa - -from curve_widget import CurveWidget # noqa - -from rpcore.pluginbase.manager import PluginManager # noqa -from rpcore.mount_manager import MountManager # noqa -from rpcore.util.network_communication import NetworkCommunication # noqa - -from ui.main_window_generated import Ui_MainWindow # noqa -from ui.point_insert_dialog_generated import Ui_Dialog as Ui_PointDialog # noqa - -class PointDialog(QDialog, Ui_PointDialog): - - def __init__(self, parent): - QDialog.__init__(self, parent) - self.setupUi(self) - qt_connect(self.btn_insert, "clicked()", lambda: self.done(1)) - - def get_value(self): - time = self.ipt_time.time() - val = self.ipt_value.value() - return time, val - - -class DayTimeEditor(QMainWindow, Ui_MainWindow): - - """ This is the main editor class which handles the user interface """ - - def __init__(self): - - # Init mounts - self._mount_mgr = MountManager(None) - self._mount_mgr.mount() - - self._plugin_mgr = PluginManager(None) - self._plugin_mgr.load() - - QMainWindow.__init__(self) - self.setupUi() - self._tree_widgets = [] - self._cmd_queue = set() - - self._selected_setting_handle = None - self._selected_setting = None - self._selected_plugin = None - self._current_time = 0.5 - - self._update_settings_list() - self._on_time_changed(self.time_slider.value()) - - self._bg_thread = Thread(target=self.updateThread) - self._bg_thread.start() - - def set_settings_visible(self, visibility): - if not visibility: - self.frame_current_setting.hide() - else: - self.frame_current_setting.show() - - def closeEvent(self, event): # noqa - event.accept() - import os - os._exit(1) - - def updateThread(self): # noqa - """ Seperate update thread """ - - while True: - if self._cmd_queue: - cmd = self._cmd_queue.pop() - if cmd == "settime": - NetworkCommunication.send_async( - NetworkCommunication.DAYTIME_PORT, "settime " + str(self._current_time)) - continue - elif cmd == "write_settings": - self._plugin_mgr.save_daytime_overrides("/$$rpconfig/daytime.yaml") - NetworkCommunication.send_async( - NetworkCommunication.DAYTIME_PORT, "loadconf") - else: - print("Unkown cmd:", cmd) - - time.sleep(0.1) - - def setupUi(self): # noqa - """ Setups the UI Components """ - Ui_MainWindow.setupUi(self, self) - self.settings_tree.setColumnWidth(0, 160) - self.settings_tree.expandAll() - - self.edit_widget = CurveWidget(self) - self.edit_widget.set_change_handler(self._on_curve_edited) - self.prefab_edit_widget.addWidget(self.edit_widget) - - qt_connect(self.time_slider, "valueChanged(int)", self._on_time_changed) - qt_connect(self.settings_tree, "itemSelectionChanged()", self._on_setting_selected) - qt_connect(self.btn_insert_point, "clicked()", self._insert_point) - qt_connect(self.btn_reset, "clicked()", self._reset_settings) - - def _reset_settings(self): - """ Resets the current plugins settings """ - # QMessageBox.warning(self, "Houston, we have a problem!", - # "This functionality is not yet implemented! Blame tobspr if you need it.\n\n" - # "On a more serious note, you can still hand-edit config/daytime.yaml.", - # QMessageBox.Ok, QMessageBox.Ok) - - # Ask the user if he's really sure about it - msg = "Are you sure you want to reset the control points of '" +\ - self._selected_setting_handle.label + "'?\n" - msg += "!! This cannot be undone !! They will be lost forever (a long time!)." - reply = QMessageBox.question( - self, "Warning", msg, QMessageBox.Yes, QMessageBox.No) - if reply == QMessageBox.Yes: - - QMessageBox.information(self, "Success", "Control points have been reset!") - default = self._selected_setting_handle.default - self._selected_setting_handle.curves[0].set_single_value(default) - self._update_settings_list() - self._cmd_queue.add("write_settings") - - def _insert_point(self): - """ Asks the user to insert a new point """ - dialog = PointDialog(self) - if dialog.exec_(): - time, val = dialog.get_value() - minutes = (time.hour() * 60 + time.minute()) / (24 * 60) - - if (val < self._selected_setting_handle.minvalue or - val > self._selected_setting_handle.maxvalue): - QMessageBox.information( - self, "Invalid Value", "Value is out of setting range!", QMessageBox.Ok) - return - - val_linear = self._selected_setting_handle.get_linear_value(val) - self._selected_setting_handle.curves[0].append_cv(minutes, val_linear) - self._cmd_queue.add("write_settings") - - def _update_tree_widgets(self): - """ Updates the tree widgets """ - for setting_handle, widget in self._tree_widgets: - value = setting_handle.get_scaled_value_at(self._current_time) - formatted = setting_handle.format(value) - widget.setText(1, formatted) - - if setting_handle.type == "color": - color_values = [int(v*255)for v in value] - widget.setBackground(1, QBrush(QColor(*color_values))) - # else: - # widget.setBackground(1,QBrush(QColor(*value))) - - def _on_curve_edited(self): - """ Called when the curve got edited in the curve widget """ - self._cmd_queue.add("write_settings") - self._update_tree_widgets() - - def _on_setting_selected(self): - """ Called when a setting got selected in the settings tree """ - selected = self.settings_tree.selectedItems() - if len(selected) != 1: - self._selected_setting = None - self._selected_plugin = None - self._selected_setting_handle = None - self.edit_widget.set_curves([]) - self.set_settings_visible(False) - else: - selected = selected[0] - - self._selected_plugin = selected._plugin_id - self._selected_setting = selected._setting_id - self._selected_setting_handle = selected._setting_handle - - self.lbl_current_setting.setText(self._selected_setting_handle.label) - self.lbl_setting_desc.setText(self._selected_setting_handle.description) - - self.edit_widget.set_curves(self._selected_setting_handle.curves) - - if self._selected_setting_handle.type == "color": - self.edit_widget.set_unit_processor(lambda x: str(int(x * 255))) - self.btn_insert_point.hide() - else: - self.edit_widget.set_unit_processor( - lambda x: self._selected_setting_handle.format( - self._selected_setting_handle.get_scaled_value(x))) - self.btn_insert_point.show() - - self.set_settings_visible(True) - self._update_tree_widgets() - - def _on_time_changed(self, val): - """ Handler when the time slider got moved """ - hour = val // (60 * 60 * 60) - minute = (val // (60 * 60)) % 60 - ftime = float(val) / (24 * 60 * 60 * 60) - - self.time_label.setText(str(hour).zfill(2) + ":" + str(minute).zfill(2)) - self.time_float_label.setText("{:1.4f}".format(ftime)) - self.edit_widget.set_current_time(ftime) - self._current_time = ftime - self._update_tree_widgets() - self._cmd_queue.add("settime") - - def _update_settings_list(self): - """ Updates the list of visible settings """ - - self.settings_tree.clear() - self._tree_widgets = [] - - first_item = None - - for plugin_id, plugin in iteritems(self._plugin_mgr.instances): - - daytime_settings = self._plugin_mgr.day_settings[plugin_id] - - if not daytime_settings: - # Skip plugins with empty settings - continue - - plugin_head = QTreeWidgetItem(self.settings_tree) - plugin_head.setText(0, plugin.name) - plugin_head.setFlags(Qt.ItemIsEnabled) - font = QFont() - font.setBold(True) - if not self._plugin_mgr.is_plugin_enabled(plugin_id): - plugin_head.setText(0, plugin.name) - plugin_head.setFont(0, font) - - # Display all settings - for setting, setting_handle in iteritems(daytime_settings): - setting_item = QTreeWidgetItem(plugin_head) - setting_item.setText(0, setting_handle.label) - if PYQT_VERSION == 4: - setting_item.setTextColor(0, QColor(150, 150, 150)) - else: - setting_item.setForeground(0, QColor(150, 150, 150)) - setting_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) - setting_item._setting_id = setting - setting_item._setting_handle = setting_handle - setting_item._plugin_id = plugin_id - setting_item.setToolTip(0, setting_handle.description) - setting_item.setToolTip(1, setting_handle.description) - self._tree_widgets.append((setting_handle, setting_item)) - if not first_item: - first_item = setting_item - - self.settings_tree.expandAll() - if first_item: - self.settings_tree.setCurrentItem(first_item) - -# Start application -app = QApplication(sys.argv) -qt_register_fonts() -editor = DayTimeEditor() -editor.show() -app.exec_() +#!/usr/bin/env python + +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +# This tool offers an interface to configure the time of day settings + + +from __future__ import print_function, division + +import os +import sys +import time +from threading import Thread + +# Change to the current directory +os.chdir(os.path.join(os.path.dirname(os.path.realpath(__file__)))) + +# Add the render pipeline to the path +sys.path.insert(0, "../../") + +# 修复导入路径问题 +from rplibs.six import iteritems # noqa +from rplibs.pyqt_imports import * # noqa + +from curve_widget import CurveWidget # noqa + +from rpcore.pluginbase.manager import PluginManager # noqa +from rpcore.mount_manager import MountManager # noqa +from rpcore.util.network_communication import NetworkCommunication # noqa + +from ui.main_window_generated import Ui_MainWindow # noqa +from ui.point_insert_dialog_generated import Ui_Dialog as Ui_PointDialog # noqa + +class PointDialog(QDialog, Ui_PointDialog): + + def __init__(self, parent): + QDialog.__init__(self, parent) + self.setupUi(self) + qt_connect(self.btn_insert, "clicked()", lambda: self.done(1)) + + def get_value(self): + time = self.ipt_time.time() + val = self.ipt_value.value() + return time, val + + +class DayTimeEditor(QMainWindow, Ui_MainWindow): + + """ This is the main editor class which handles the user interface """ + + def __init__(self): + + # Init mounts + self._mount_mgr = MountManager(None) + self._mount_mgr.mount() + + self._plugin_mgr = PluginManager(None) + self._plugin_mgr.load() + + QMainWindow.__init__(self) + self.setupUi() + self._tree_widgets = [] + self._cmd_queue = set() + + self._selected_setting_handle = None + self._selected_setting = None + self._selected_plugin = None + self._current_time = 0.5 + + self._update_settings_list() + self._on_time_changed(self.time_slider.value()) + + self._bg_thread = Thread(target=self.updateThread) + self._bg_thread.start() + + def set_settings_visible(self, visibility): + if not visibility: + self.frame_current_setting.hide() + else: + self.frame_current_setting.show() + + def closeEvent(self, event): # noqa + event.accept() + import os + os._exit(1) + + def updateThread(self): # noqa + """ Seperate update thread """ + + while True: + if self._cmd_queue: + cmd = self._cmd_queue.pop() + if cmd == "settime": + NetworkCommunication.send_async( + NetworkCommunication.DAYTIME_PORT, "settime " + str(self._current_time)) + continue + elif cmd == "write_settings": + self._plugin_mgr.save_daytime_overrides("/$$rpconfig/daytime.yaml") + NetworkCommunication.send_async( + NetworkCommunication.DAYTIME_PORT, "loadconf") + else: + print("Unkown cmd:", cmd) + + time.sleep(0.1) + + def setupUi(self): # noqa + """ Setups the UI Components """ + Ui_MainWindow.setupUi(self, self) + self.settings_tree.setColumnWidth(0, 160) + self.settings_tree.expandAll() + + self.edit_widget = CurveWidget(self) + self.edit_widget.set_change_handler(self._on_curve_edited) + self.prefab_edit_widget.addWidget(self.edit_widget) + + qt_connect(self.time_slider, "valueChanged(int)", self._on_time_changed) + qt_connect(self.settings_tree, "itemSelectionChanged()", self._on_setting_selected) + qt_connect(self.btn_insert_point, "clicked()", self._insert_point) + qt_connect(self.btn_reset, "clicked()", self._reset_settings) + + def _reset_settings(self): + """ Resets the current plugins settings """ + # QMessageBox.warning(self, "Houston, we have a problem!", + # "This functionality is not yet implemented! Blame tobspr if you need it.\n\n" + # "On a more serious note, you can still hand-edit config/daytime.yaml.", + # QMessageBox.Ok, QMessageBox.Ok) + + # Ask the user if he's really sure about it + msg = "Are you sure you want to reset the control points of '" +\ + self._selected_setting_handle.label + "'?\n" + msg += "!! This cannot be undone !! They will be lost forever (a long time!)." + reply = QMessageBox.question( + self, "Warning", msg, QMessageBox.Yes, QMessageBox.No) + if reply == QMessageBox.Yes: + + QMessageBox.information(self, "Success", "Control points have been reset!") + default = self._selected_setting_handle.default + self._selected_setting_handle.curves[0].set_single_value(default) + self._update_settings_list() + self._cmd_queue.add("write_settings") + + def _insert_point(self): + """ Asks the user to insert a new point """ + dialog = PointDialog(self) + if dialog.exec_(): + time, val = dialog.get_value() + minutes = (time.hour() * 60 + time.minute()) / (24 * 60) + + if (val < self._selected_setting_handle.minvalue or + val > self._selected_setting_handle.maxvalue): + QMessageBox.information( + self, "Invalid Value", "Value is out of setting range!", QMessageBox.Ok) + return + + val_linear = self._selected_setting_handle.get_linear_value(val) + self._selected_setting_handle.curves[0].append_cv(minutes, val_linear) + self._cmd_queue.add("write_settings") + + def _update_tree_widgets(self): + """ Updates the tree widgets """ + for setting_handle, widget in self._tree_widgets: + value = setting_handle.get_scaled_value_at(self._current_time) + formatted = setting_handle.format(value) + widget.setText(1, formatted) + + if setting_handle.type == "color": + color_values = [int(v*255)for v in value] + widget.setBackground(1, QBrush(QColor(*color_values))) + # else: + # widget.setBackground(1,QBrush(QColor(*value))) + + def _on_curve_edited(self): + """ Called when the curve got edited in the curve widget """ + self._cmd_queue.add("write_settings") + self._update_tree_widgets() + + def _on_setting_selected(self): + """ Called when a setting got selected in the settings tree """ + selected = self.settings_tree.selectedItems() + if len(selected) != 1: + self._selected_setting = None + self._selected_plugin = None + self._selected_setting_handle = None + self.edit_widget.set_curves([]) + self.set_settings_visible(False) + else: + selected = selected[0] + + self._selected_plugin = selected._plugin_id + self._selected_setting = selected._setting_id + self._selected_setting_handle = selected._setting_handle + + self.lbl_current_setting.setText(self._selected_setting_handle.label) + self.lbl_setting_desc.setText(self._selected_setting_handle.description) + + self.edit_widget.set_curves(self._selected_setting_handle.curves) + + if self._selected_setting_handle.type == "color": + self.edit_widget.set_unit_processor(lambda x: str(int(x * 255))) + self.btn_insert_point.hide() + else: + self.edit_widget.set_unit_processor( + lambda x: self._selected_setting_handle.format( + self._selected_setting_handle.get_scaled_value(x))) + self.btn_insert_point.show() + + self.set_settings_visible(True) + self._update_tree_widgets() + + def _on_time_changed(self, val): + """ Handler when the time slider got moved """ + hour = val // (60 * 60 * 60) + minute = (val // (60 * 60)) % 60 + ftime = float(val) / (24 * 60 * 60 * 60) + + self.time_label.setText(str(hour).zfill(2) + ":" + str(minute).zfill(2)) + self.time_float_label.setText("{:1.4f}".format(ftime)) + self.edit_widget.set_current_time(ftime) + self._current_time = ftime + self._update_tree_widgets() + self._cmd_queue.add("settime") + + def _update_settings_list(self): + """ Updates the list of visible settings """ + + self.settings_tree.clear() + self._tree_widgets = [] + + first_item = None + + for plugin_id, plugin in iteritems(self._plugin_mgr.instances): + + daytime_settings = self._plugin_mgr.day_settings[plugin_id] + + if not daytime_settings: + # Skip plugins with empty settings + continue + + plugin_head = QTreeWidgetItem(self.settings_tree) + plugin_head.setText(0, plugin.name) + plugin_head.setFlags(Qt.ItemIsEnabled) + font = QFont() + font.setBold(True) + if not self._plugin_mgr.is_plugin_enabled(plugin_id): + plugin_head.setText(0, plugin.name) + plugin_head.setFont(0, font) + + # Display all settings + for setting, setting_handle in iteritems(daytime_settings): + setting_item = QTreeWidgetItem(plugin_head) + setting_item.setText(0, setting_handle.label) + if PYQT_VERSION == 4: + setting_item.setTextColor(0, QColor(150, 150, 150)) + else: + setting_item.setForeground(0, QColor(150, 150, 150)) + setting_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable) + setting_item._setting_id = setting + setting_item._setting_handle = setting_handle + setting_item._plugin_id = plugin_id + setting_item.setToolTip(0, setting_handle.description) + setting_item.setToolTip(1, setting_handle.description) + self._tree_widgets.append((setting_handle, setting_item)) + if not first_item: + first_item = setting_item + + self.settings_tree.expandAll() + if first_item: + self.settings_tree.setCurrentItem(first_item) + +# Start application +app = QApplication(sys.argv) +qt_register_fonts() +editor = DayTimeEditor() +editor.show() +app.exec_() diff --git a/RenderPipelineFile/toolkit/day_time_editor/ui/compile_ui_qt4.bat b/RenderPipelineFile/toolkit/day_time_editor/ui/compile_ui_qt4.bat index aba27fdf..2cf78f10 100644 --- a/RenderPipelineFile/toolkit/day_time_editor/ui/compile_ui_qt4.bat +++ b/RenderPipelineFile/toolkit/day_time_editor/ui/compile_ui_qt4.bat @@ -1,10 +1,10 @@ -@echo off - -rem Adjust the path to match your PyQt4 installation -SET PYQTPATH="C:\Projekte\Panda3D\built_x64\python\Lib\site-packages\PyQt4" - -python -m PyQt4.uic.pyuic --from-imports main_window.ui -o main_window_generated.py -python -m PyQt4.uic.pyuic --from-imports point_insert.ui -o point_insert_dialog_generated.py -"%PYQTPATH%\pyrcc4.exe" -py3 resources.qrc -o resources_rc.py - -pause +@echo off + +rem Adjust the path to match your PyQt4 installation +SET PYQTPATH="C:\Projekte\Panda3D\built_x64\python\Lib\site-packages\PyQt4" + +python -m PyQt4.uic.pyuic --from-imports main_window.ui -o main_window_generated.py +python -m PyQt4.uic.pyuic --from-imports point_insert.ui -o point_insert_dialog_generated.py +"%PYQTPATH%\pyrcc4.exe" -py3 resources.qrc -o resources_rc.py + +pause diff --git a/RenderPipelineFile/toolkit/day_time_editor/ui/compile_ui_qt5.bat b/RenderPipelineFile/toolkit/day_time_editor/ui/compile_ui_qt5.bat index 0ac03bfe..724f2638 100644 --- a/RenderPipelineFile/toolkit/day_time_editor/ui/compile_ui_qt5.bat +++ b/RenderPipelineFile/toolkit/day_time_editor/ui/compile_ui_qt5.bat @@ -1,7 +1,7 @@ -@echo off - -python -m PyQt5.uic.pyuic --from-imports main_window.ui -o main_window_generated.py -python -m PyQt5.uic.pyuic --from-imports point_insert.ui -o point_insert_dialog_generated.py -python -m PyQt5.pyrcc_main resources.qrc -o resources_rc.py - -pause +@echo off + +python -m PyQt5.uic.pyuic --from-imports main_window.ui -o main_window_generated.py +python -m PyQt5.uic.pyuic --from-imports point_insert.ui -o point_insert_dialog_generated.py +python -m PyQt5.pyrcc_main resources.qrc -o resources_rc.py + +pause diff --git a/RenderPipelineFile/toolkit/day_time_editor/ui/main_window.ui b/RenderPipelineFile/toolkit/day_time_editor/ui/main_window.ui index a249f516..b8d94efc 100644 --- a/RenderPipelineFile/toolkit/day_time_editor/ui/main_window.ui +++ b/RenderPipelineFile/toolkit/day_time_editor/ui/main_window.ui @@ -1,1107 +1,1107 @@ - - - MainWindow - - - - 0 - 0 - 973 - 632 - - - - - 973 - 632 - - - - - 973 - 632 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 63 - 63 - 63 - - - - - - - 52 - 52 - 52 - - - - - - - 21 - 21 - 21 - - - - - - - 28 - 28 - 28 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 21 - 21 - 21 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 63 - 63 - 63 - - - - - - - 52 - 52 - 52 - - - - - - - 21 - 21 - 21 - - - - - - - 28 - 28 - 28 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 21 - 21 - 21 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - - - 21 - 21 - 21 - - - - - - - 255 - 255 - 255 - - - - - - - 63 - 63 - 63 - - - - - - - 52 - 52 - 52 - - - - - - - 21 - 21 - 21 - - - - - - - 28 - 28 - 28 - - - - - - - 21 - 21 - 21 - - - - - - - 255 - 255 - 255 - - - - - - - 21 - 21 - 21 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 42 - 42 - 42 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - - Day Time Editor - - - - :/res/res/icon.png:/res/res/icon.png - - - false - - - QMainWindow { background: #fff;} -*, QLabel { font-family: Roboto; font-weight: 300; } - -QScrollBar { - background: #eee; -} - - -QScrollBar:vertical { - width: 9px; - margin: 0; - } - - QScrollBar::handle:vertical { - min-height: 15px; - background: #aaa; - - } - - - QScrollBar::handle:vertical:hover { - - background: #999; - - } - QScrollBar::add-line:vertical { - } - - QScrollBar::sub-line:vertical { - } - - - QScrollBar::add-page:vertical { - background: #ddd; - - } - QScrollBar::sub-page:vertical { - background: #ddd; - } - - - - - - - 350 - 80 - 641 - 551 - - - - QFrame { background: #e5e5e5;} - - - QFrame::NoFrame - - - QFrame::Plain - - - - - 20 - 16 - 221 - 31 - - - - - Roboto - -1 - 37 - false - - - - color: #555; border: 0; font-size: 15px; text-transform: uppercase; - - - Occlusion Strength - - - - - - 500 - 14 - 101 - 31 - - - - QPushButton { -color: #eee; -background: #666; -border: 0; -} - -QPushButton:hover { - background: #555; -} - -QPushButton:pressed { - background: #111; -} - - - Reset curve - - - - - - 20 - 94 - 581 - 441 - - - - QFrame { -} - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - 0 - 0 - 571 - 441 - - - - - 0 - - - - - - - - 20 - 50 - 571 - 41 - - - - - Roboto - 37 - false - - - - color: #888; - - - Description: Some Description about the setting, to give the user a rough idea what this does. This description may be so long, it can even go into the next line. - - - true - - - - - - 368 - 14 - 131 - 31 - - - - QPushButton { -color: #eee; -background: #666; -border: 0; -} - -QPushButton:hover { - background: #555; -} - -QPushButton:pressed { - background: #111; -} - - - Insert point from data - - - - - - - -1 - 80 - 351 - 551 - - - - background: #eee; - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - 14 - 20 - 311 - 511 - - - - QTreeWidget::item { -padding: 7px 7px 6px; -background: #ddd; -outline: 0 !important; -margin-bottom: 1px; -margin-right: 0px; -color: #777; -border: 0; -border-radius: 0; -} - -QTreeWidget::item:hover { -background: #ccc; -} - -QTreeWidget::item:selected { -background: #555; -color: #eee; -} - -QTreeWidget { -padding: 0; -color: #eee; -background: transparent; - -} - -* { -outline: 0; -} - - - - - QFrame::NoFrame - - - Qt::ScrollBarAsNeeded - - - Qt::ScrollBarAsNeeded - - - QAbstractItemView::NoEditTriggers - - - false - - - false - - - QAbstractItemView::SelectRows - - - QAbstractItemView::ScrollPerPixel - - - 18 - - - true - - - false - - - true - - - false - - - true - - - false - - - true - - - true - - - false - - - false - - - 150 - - - false - - - - Setting - - - - - Value - - - - - Scattering - - - - Sun Height - - - 0,4 - - - - - Sun Color - - - [128, 255, 100] - - - - - Sun Angle - - - 180 ° - - - - - - Ambient Occlusion - - - - - - ItemIsSelectable|ItemIsDragEnabled|ItemIsUserCheckable|ItemIsEnabled|ItemIsTristate - - - - Occlusion Strength - - - 0.5 - - - - - - - - - 420 - 10 - 551 - 61 - - - - QFrame { - background: #fff; -} - - - - QFrame::NoFrame - - - QFrame::Plain - - - - - 10 - 11 - 521 - 20 - - - - 5184000 - - - 3600 - - - 3600 - - - 2592000 - - - true - - - Qt::Horizontal - - - false - - - false - - - QSlider::TicksBelow - - - 432000 - - - - - - 254 - 30 - 32 - 32 - - - - - - - :/res/res/12_00.png - - - - - - 341 - 30 - 32 - 32 - - - - - - - :/res/res/16_00.png - - - - - - 0 - 30 - 32 - 32 - - - - - - - :/res/res/0_00.png - - - - - - 84 - 30 - 32 - 32 - - - - - - - :/res/res/4_00.png - - - - - - 170 - 30 - 32 - 32 - - - - - - - :/res/res/8_00.png - - - - - - 422 - 30 - 32 - 32 - - - - - - - :/res/res/20_00.png - - - - - - 510 - 30 - 32 - 32 - - - - - - - :/res/res/0_00.png - - - - - - - 20 - 35 - 261 - 31 - - - - font-size: 20px; color: #2aa877; -font-weight: 100; - - - TIME OF DAY EDITOR - - - - - - 350 - 47 - 41 - 18 - - - - - Roboto - -1 - 37 - false - - - - color: #777; font-size: 12px; - - - 0.486 - - - - - - 350 - 20 - 81 - 31 - - - - - Roboto - -1 - 37 - false - - - - color: #444; font-size: 22px; - - - 11:15 - - - - - - 20 - 16 - 261 - 31 - - - - font-size: 13px; -color: #aaa; -font-weight: 600; - - - RENDER PIPELINE - - - frame_2 - frame_current_setting - frame_4 - label - time_float_label - time_label - label_9 - - - - - - - + + + MainWindow + + + + 0 + 0 + 973 + 632 + + + + + 973 + 632 + + + + + 973 + 632 + + + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 63 + 63 + 63 + + + + + + + 52 + 52 + 52 + + + + + + + 21 + 21 + 21 + + + + + + + 28 + 28 + 28 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 0 + 0 + + + + + + + 21 + 21 + 21 + + + + + + + 255 + 255 + 220 + + + + + + + 0 + 0 + 0 + + + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 63 + 63 + 63 + + + + + + + 52 + 52 + 52 + + + + + + + 21 + 21 + 21 + + + + + + + 28 + 28 + 28 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 0 + 0 + + + + + + + 21 + 21 + 21 + + + + + + + 255 + 255 + 220 + + + + + + + 0 + 0 + 0 + + + + + + + + + 21 + 21 + 21 + + + + + + + 255 + 255 + 255 + + + + + + + 63 + 63 + 63 + + + + + + + 52 + 52 + 52 + + + + + + + 21 + 21 + 21 + + + + + + + 28 + 28 + 28 + + + + + + + 21 + 21 + 21 + + + + + + + 255 + 255 + 255 + + + + + + + 21 + 21 + 21 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 0 + 0 + + + + + + + 42 + 42 + 42 + + + + + + + 255 + 255 + 220 + + + + + + + 0 + 0 + 0 + + + + + + + + Day Time Editor + + + + :/res/res/icon.png:/res/res/icon.png + + + false + + + QMainWindow { background: #fff;} +*, QLabel { font-family: Roboto; font-weight: 300; } + +QScrollBar { + background: #eee; +} + + +QScrollBar:vertical { + width: 9px; + margin: 0; + } + + QScrollBar::handle:vertical { + min-height: 15px; + background: #aaa; + + } + + + QScrollBar::handle:vertical:hover { + + background: #999; + + } + QScrollBar::add-line:vertical { + } + + QScrollBar::sub-line:vertical { + } + + + QScrollBar::add-page:vertical { + background: #ddd; + + } + QScrollBar::sub-page:vertical { + background: #ddd; + } + + + + + + + 350 + 80 + 641 + 551 + + + + QFrame { background: #e5e5e5;} + + + QFrame::NoFrame + + + QFrame::Plain + + + + + 20 + 16 + 221 + 31 + + + + + Roboto + -1 + 37 + false + + + + color: #555; border: 0; font-size: 15px; text-transform: uppercase; + + + Occlusion Strength + + + + + + 500 + 14 + 101 + 31 + + + + QPushButton { +color: #eee; +background: #666; +border: 0; +} + +QPushButton:hover { + background: #555; +} + +QPushButton:pressed { + background: #111; +} + + + Reset curve + + + + + + 20 + 94 + 581 + 441 + + + + QFrame { +} + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + 0 + 0 + 571 + 441 + + + + + 0 + + + + + + + + 20 + 50 + 571 + 41 + + + + + Roboto + 37 + false + + + + color: #888; + + + Description: Some Description about the setting, to give the user a rough idea what this does. This description may be so long, it can even go into the next line. + + + true + + + + + + 368 + 14 + 131 + 31 + + + + QPushButton { +color: #eee; +background: #666; +border: 0; +} + +QPushButton:hover { + background: #555; +} + +QPushButton:pressed { + background: #111; +} + + + Insert point from data + + + + + + + -1 + 80 + 351 + 551 + + + + background: #eee; + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + 14 + 20 + 311 + 511 + + + + QTreeWidget::item { +padding: 7px 7px 6px; +background: #ddd; +outline: 0 !important; +margin-bottom: 1px; +margin-right: 0px; +color: #777; +border: 0; +border-radius: 0; +} + +QTreeWidget::item:hover { +background: #ccc; +} + +QTreeWidget::item:selected { +background: #555; +color: #eee; +} + +QTreeWidget { +padding: 0; +color: #eee; +background: transparent; + +} + +* { +outline: 0; +} + + + + + QFrame::NoFrame + + + Qt::ScrollBarAsNeeded + + + Qt::ScrollBarAsNeeded + + + QAbstractItemView::NoEditTriggers + + + false + + + false + + + QAbstractItemView::SelectRows + + + QAbstractItemView::ScrollPerPixel + + + 18 + + + true + + + false + + + true + + + false + + + true + + + false + + + true + + + true + + + false + + + false + + + 150 + + + false + + + + Setting + + + + + Value + + + + + Scattering + + + + Sun Height + + + 0,4 + + + + + Sun Color + + + [128, 255, 100] + + + + + Sun Angle + + + 180 ° + + + + + + Ambient Occlusion + + + + + + ItemIsSelectable|ItemIsDragEnabled|ItemIsUserCheckable|ItemIsEnabled|ItemIsTristate + + + + Occlusion Strength + + + 0.5 + + + + + + + + + 420 + 10 + 551 + 61 + + + + QFrame { + background: #fff; +} + + + + QFrame::NoFrame + + + QFrame::Plain + + + + + 10 + 11 + 521 + 20 + + + + 5184000 + + + 3600 + + + 3600 + + + 2592000 + + + true + + + Qt::Horizontal + + + false + + + false + + + QSlider::TicksBelow + + + 432000 + + + + + + 254 + 30 + 32 + 32 + + + + + + + :/res/res/12_00.png + + + + + + 341 + 30 + 32 + 32 + + + + + + + :/res/res/16_00.png + + + + + + 0 + 30 + 32 + 32 + + + + + + + :/res/res/0_00.png + + + + + + 84 + 30 + 32 + 32 + + + + + + + :/res/res/4_00.png + + + + + + 170 + 30 + 32 + 32 + + + + + + + :/res/res/8_00.png + + + + + + 422 + 30 + 32 + 32 + + + + + + + :/res/res/20_00.png + + + + + + 510 + 30 + 32 + 32 + + + + + + + :/res/res/0_00.png + + + + + + + 20 + 35 + 261 + 31 + + + + font-size: 20px; color: #2aa877; +font-weight: 100; + + + TIME OF DAY EDITOR + + + + + + 350 + 47 + 41 + 18 + + + + + Roboto + -1 + 37 + false + + + + color: #777; font-size: 12px; + + + 0.486 + + + + + + 350 + 20 + 81 + 31 + + + + + Roboto + -1 + 37 + false + + + + color: #444; font-size: 22px; + + + 11:15 + + + + + + 20 + 16 + 261 + 31 + + + + font-size: 13px; +color: #aaa; +font-weight: 600; + + + RENDER PIPELINE + + + frame_2 + frame_current_setting + frame_4 + label + time_float_label + time_label + label_9 + + + + + + + diff --git a/RenderPipelineFile/toolkit/day_time_editor/ui/main_window_generated.py b/RenderPipelineFile/toolkit/day_time_editor/ui/main_window_generated.py index cbdd91e9..ae5f8a90 100644 --- a/RenderPipelineFile/toolkit/day_time_editor/ui/main_window_generated.py +++ b/RenderPipelineFile/toolkit/day_time_editor/ui/main_window_generated.py @@ -1,468 +1,468 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'main_window.ui' -# -# Created by: PyQt5 UI code generator 5.7 -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore, QtGui, QtWidgets - -class Ui_MainWindow(object): - def setupUi(self, MainWindow): - MainWindow.setObjectName("MainWindow") - MainWindow.resize(973, 632) - MainWindow.setMinimumSize(QtCore.QSize(973, 632)) - MainWindow.setMaximumSize(QtCore.QSize(973, 632)) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush) - brush = QtGui.QBrush(QtGui.QColor(63, 63, 63)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Light, brush) - brush = QtGui.QBrush(QtGui.QColor(52, 52, 52)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Midlight, brush) - brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Dark, brush) - brush = QtGui.QBrush(QtGui.QColor(28, 28, 28)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Mid, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.BrightText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Shadow, brush) - brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.AlternateBase, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ToolTipBase, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ToolTipText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush) - brush = QtGui.QBrush(QtGui.QColor(63, 63, 63)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Light, brush) - brush = QtGui.QBrush(QtGui.QColor(52, 52, 52)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Midlight, brush) - brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Dark, brush) - brush = QtGui.QBrush(QtGui.QColor(28, 28, 28)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Mid, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.BrightText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Shadow, brush) - brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.AlternateBase, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipBase, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipText, brush) - brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush) - brush = QtGui.QBrush(QtGui.QColor(63, 63, 63)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Light, brush) - brush = QtGui.QBrush(QtGui.QColor(52, 52, 52)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Midlight, brush) - brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Dark, brush) - brush = QtGui.QBrush(QtGui.QColor(28, 28, 28)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Mid, brush) - brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.BrightText, brush) - brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Shadow, brush) - brush = QtGui.QBrush(QtGui.QColor(42, 42, 42)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.AlternateBase, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipBase, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipText, brush) - MainWindow.setPalette(palette) - icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(":/res/res/icon.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - MainWindow.setWindowIcon(icon) - MainWindow.setAutoFillBackground(False) - MainWindow.setStyleSheet("QMainWindow { background: #fff;}\n" -"*, QLabel { font-family: Roboto; font-weight: 300; }\n" -"\n" -"QScrollBar {\n" -" background: #eee;\n" -"}\n" -"\n" -"\n" -"QScrollBar:vertical {\n" -" width: 9px;\n" -" margin: 0;\n" -" }\n" -"\n" -" QScrollBar::handle:vertical {\n" -" min-height: 15px;\n" -" background: #aaa;\n" -"\n" -" }\n" -"\n" -"\n" -" QScrollBar::handle:vertical:hover {\n" -" \n" -" background: #999;\n" -"\n" -" }\n" -" QScrollBar::add-line:vertical {\n" -" }\n" -"\n" -" QScrollBar::sub-line:vertical {\n" -" }\n" -"\n" -"\n" -" QScrollBar::add-page:vertical {\n" -" background: #ddd;\n" -"\n" -" }\n" -" QScrollBar::sub-page:vertical {\n" -" background: #ddd;\n" -" }\n" -"") - self.centralwidget = QtWidgets.QWidget(MainWindow) - self.centralwidget.setObjectName("centralwidget") - self.frame_current_setting = QtWidgets.QFrame(self.centralwidget) - self.frame_current_setting.setGeometry(QtCore.QRect(350, 80, 641, 551)) - self.frame_current_setting.setStyleSheet("QFrame { background: #e5e5e5;}") - self.frame_current_setting.setFrameShape(QtWidgets.QFrame.NoFrame) - self.frame_current_setting.setFrameShadow(QtWidgets.QFrame.Plain) - self.frame_current_setting.setObjectName("frame_current_setting") - self.lbl_current_setting = QtWidgets.QLabel(self.frame_current_setting) - self.lbl_current_setting.setGeometry(QtCore.QRect(20, 16, 221, 31)) - font = QtGui.QFont() - font.setFamily("Roboto") - font.setPointSize(-1) - font.setBold(False) - font.setWeight(37) - self.lbl_current_setting.setFont(font) - self.lbl_current_setting.setStyleSheet("color: #555; border: 0; font-size: 15px; text-transform: uppercase;") - self.lbl_current_setting.setObjectName("lbl_current_setting") - self.btn_reset = QtWidgets.QPushButton(self.frame_current_setting) - self.btn_reset.setGeometry(QtCore.QRect(500, 14, 101, 31)) - self.btn_reset.setStyleSheet("QPushButton {\n" -"color: #eee;\n" -"background: #666;\n" -"border: 0; \n" -"}\n" -"\n" -"QPushButton:hover {\n" -" background: #555;\n" -"}\n" -"\n" -"QPushButton:pressed {\n" -" background: #111;\n" -"}") - self.btn_reset.setObjectName("btn_reset") - self.frame_3 = QtWidgets.QFrame(self.frame_current_setting) - self.frame_3.setGeometry(QtCore.QRect(20, 94, 581, 441)) - self.frame_3.setStyleSheet("QFrame {\n" -"}") - self.frame_3.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.frame_3.setFrameShadow(QtWidgets.QFrame.Raised) - self.frame_3.setObjectName("frame_3") - self.verticalLayoutWidget = QtWidgets.QWidget(self.frame_3) - self.verticalLayoutWidget.setGeometry(QtCore.QRect(0, 0, 571, 441)) - self.verticalLayoutWidget.setObjectName("verticalLayoutWidget") - self.prefab_edit_widget = QtWidgets.QVBoxLayout(self.verticalLayoutWidget) - self.prefab_edit_widget.setContentsMargins(0, 0, 0, 0) - self.prefab_edit_widget.setSpacing(0) - self.prefab_edit_widget.setObjectName("prefab_edit_widget") - self.lbl_setting_desc = QtWidgets.QLabel(self.frame_current_setting) - self.lbl_setting_desc.setGeometry(QtCore.QRect(20, 50, 571, 41)) - font = QtGui.QFont() - font.setFamily("Roboto") - font.setBold(False) - font.setWeight(37) - self.lbl_setting_desc.setFont(font) - self.lbl_setting_desc.setStyleSheet("color: #888;") - self.lbl_setting_desc.setWordWrap(True) - self.lbl_setting_desc.setObjectName("lbl_setting_desc") - self.btn_insert_point = QtWidgets.QPushButton(self.frame_current_setting) - self.btn_insert_point.setGeometry(QtCore.QRect(368, 14, 131, 31)) - self.btn_insert_point.setStyleSheet("QPushButton {\n" -"color: #eee;\n" -"background: #666;\n" -"border: 0; \n" -"}\n" -"\n" -"QPushButton:hover {\n" -" background: #555;\n" -"}\n" -"\n" -"QPushButton:pressed {\n" -" background: #111;\n" -"}") - self.btn_insert_point.setObjectName("btn_insert_point") - self.frame_2 = QtWidgets.QFrame(self.centralwidget) - self.frame_2.setGeometry(QtCore.QRect(-1, 80, 351, 551)) - self.frame_2.setStyleSheet("background: #eee;") - self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised) - self.frame_2.setObjectName("frame_2") - self.settings_tree = QtWidgets.QTreeWidget(self.frame_2) - self.settings_tree.setGeometry(QtCore.QRect(14, 20, 311, 511)) - self.settings_tree.setStyleSheet("QTreeWidget::item {\n" -"padding: 7px 7px 6px;\n" -"background: #ddd;\n" -"outline: 0 !important;\n" -"margin-bottom: 1px;\n" -"margin-right: 0px;\n" -"color: #777;\n" -"border: 0;\n" -"border-radius: 0;\n" -"}\n" -"\n" -"QTreeWidget::item:hover {\n" -"background: #ccc;\n" -"}\n" -"\n" -"QTreeWidget::item:selected {\n" -"background: #555;\n" -"color: #eee;\n" -"}\n" -"\n" -"QTreeWidget {\n" -"padding: 0;\n" -"color: #eee;\n" -"background: transparent;\n" -"\n" -"}\n" -"\n" -"* {\n" -"outline: 0;\n" -"}\n" -"\n" -"") - self.settings_tree.setFrameShape(QtWidgets.QFrame.NoFrame) - self.settings_tree.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.settings_tree.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.settings_tree.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) - self.settings_tree.setProperty("showDropIndicator", False) - self.settings_tree.setAlternatingRowColors(False) - self.settings_tree.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) - self.settings_tree.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) - self.settings_tree.setIndentation(18) - self.settings_tree.setRootIsDecorated(True) - self.settings_tree.setUniformRowHeights(False) - self.settings_tree.setItemsExpandable(True) - self.settings_tree.setAnimated(True) - self.settings_tree.setAllColumnsShowFocus(False) - self.settings_tree.setWordWrap(True) - self.settings_tree.setHeaderHidden(True) - self.settings_tree.setObjectName("settings_tree") - item_0 = QtWidgets.QTreeWidgetItem(self.settings_tree) - item_1 = QtWidgets.QTreeWidgetItem(item_0) - item_1 = QtWidgets.QTreeWidgetItem(item_0) - item_1 = QtWidgets.QTreeWidgetItem(item_0) - item_0 = QtWidgets.QTreeWidgetItem(self.settings_tree) - item_0.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsDragEnabled|QtCore.Qt.ItemIsUserCheckable|QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsTristate) - item_1 = QtWidgets.QTreeWidgetItem(item_0) - self.settings_tree.header().setVisible(False) - self.settings_tree.header().setCascadingSectionResizes(False) - self.settings_tree.header().setDefaultSectionSize(150) - self.settings_tree.header().setStretchLastSection(False) - self.frame_4 = QtWidgets.QFrame(self.centralwidget) - self.frame_4.setGeometry(QtCore.QRect(420, 10, 551, 61)) - self.frame_4.setStyleSheet("QFrame {\n" -" background: #fff;\n" -"}\n" -"") - self.frame_4.setFrameShape(QtWidgets.QFrame.NoFrame) - self.frame_4.setFrameShadow(QtWidgets.QFrame.Plain) - self.frame_4.setObjectName("frame_4") - self.time_slider = QtWidgets.QSlider(self.frame_4) - self.time_slider.setGeometry(QtCore.QRect(10, 11, 521, 20)) - self.time_slider.setMaximum(5184000) - self.time_slider.setSingleStep(3600) - self.time_slider.setPageStep(3600) - self.time_slider.setProperty("value", 2592000) - self.time_slider.setTracking(True) - self.time_slider.setOrientation(QtCore.Qt.Horizontal) - self.time_slider.setInvertedAppearance(False) - self.time_slider.setInvertedControls(False) - self.time_slider.setTickPosition(QtWidgets.QSlider.TicksBelow) - self.time_slider.setTickInterval(432000) - self.time_slider.setObjectName("time_slider") - self.label_2 = QtWidgets.QLabel(self.frame_4) - self.label_2.setGeometry(QtCore.QRect(254, 30, 32, 32)) - self.label_2.setText("") - self.label_2.setPixmap(QtGui.QPixmap(":/res/res/12_00.png")) - self.label_2.setObjectName("label_2") - self.label_3 = QtWidgets.QLabel(self.frame_4) - self.label_3.setGeometry(QtCore.QRect(341, 30, 32, 32)) - self.label_3.setText("") - self.label_3.setPixmap(QtGui.QPixmap(":/res/res/16_00.png")) - self.label_3.setObjectName("label_3") - self.label_4 = QtWidgets.QLabel(self.frame_4) - self.label_4.setGeometry(QtCore.QRect(0, 30, 32, 32)) - self.label_4.setText("") - self.label_4.setPixmap(QtGui.QPixmap(":/res/res/0_00.png")) - self.label_4.setObjectName("label_4") - self.label_5 = QtWidgets.QLabel(self.frame_4) - self.label_5.setGeometry(QtCore.QRect(84, 30, 32, 32)) - self.label_5.setText("") - self.label_5.setPixmap(QtGui.QPixmap(":/res/res/4_00.png")) - self.label_5.setObjectName("label_5") - self.label_6 = QtWidgets.QLabel(self.frame_4) - self.label_6.setGeometry(QtCore.QRect(170, 30, 32, 32)) - self.label_6.setText("") - self.label_6.setPixmap(QtGui.QPixmap(":/res/res/8_00.png")) - self.label_6.setObjectName("label_6") - self.label_7 = QtWidgets.QLabel(self.frame_4) - self.label_7.setGeometry(QtCore.QRect(422, 30, 32, 32)) - self.label_7.setText("") - self.label_7.setPixmap(QtGui.QPixmap(":/res/res/20_00.png")) - self.label_7.setObjectName("label_7") - self.label_8 = QtWidgets.QLabel(self.frame_4) - self.label_8.setGeometry(QtCore.QRect(510, 30, 32, 32)) - self.label_8.setText("") - self.label_8.setPixmap(QtGui.QPixmap(":/res/res/0_00.png")) - self.label_8.setObjectName("label_8") - self.label = QtWidgets.QLabel(self.centralwidget) - self.label.setGeometry(QtCore.QRect(20, 35, 261, 31)) - self.label.setStyleSheet("font-size: 20px; color: #2aa877;\n" -"font-weight: 100;") - self.label.setObjectName("label") - self.time_float_label = QtWidgets.QLabel(self.centralwidget) - self.time_float_label.setGeometry(QtCore.QRect(350, 47, 41, 18)) - font = QtGui.QFont() - font.setFamily("Roboto") - font.setPointSize(-1) - font.setBold(False) - font.setWeight(37) - self.time_float_label.setFont(font) - self.time_float_label.setStyleSheet("color: #777; font-size: 12px;") - self.time_float_label.setObjectName("time_float_label") - self.time_label = QtWidgets.QLabel(self.centralwidget) - self.time_label.setGeometry(QtCore.QRect(350, 20, 81, 31)) - font = QtGui.QFont() - font.setFamily("Roboto") - font.setPointSize(-1) - font.setBold(False) - font.setWeight(37) - self.time_label.setFont(font) - self.time_label.setStyleSheet("color: #444; font-size: 22px;") - self.time_label.setObjectName("time_label") - self.label_9 = QtWidgets.QLabel(self.centralwidget) - self.label_9.setGeometry(QtCore.QRect(20, 16, 261, 31)) - self.label_9.setStyleSheet("font-size: 13px;\n" -"color: #aaa;\n" -"font-weight: 600;") - self.label_9.setObjectName("label_9") - self.frame_2.raise_() - self.frame_current_setting.raise_() - self.frame_4.raise_() - self.label.raise_() - self.time_float_label.raise_() - self.time_label.raise_() - self.label_9.raise_() - MainWindow.setCentralWidget(self.centralwidget) - - self.retranslateUi(MainWindow) - QtCore.QMetaObject.connectSlotsByName(MainWindow) - - def retranslateUi(self, MainWindow): - _translate = QtCore.QCoreApplication.translate - MainWindow.setWindowTitle(_translate("MainWindow", "Day Time Editor")) - self.lbl_current_setting.setText(_translate("MainWindow", "Occlusion Strength")) - self.btn_reset.setText(_translate("MainWindow", "Reset curve")) - self.lbl_setting_desc.setText(_translate("MainWindow", "Description: Some Description about the setting, to give the user a rough idea what this does. This description may be so long, it can even go into the next line.")) - self.btn_insert_point.setText(_translate("MainWindow", "Insert point from data")) - self.settings_tree.setSortingEnabled(False) - self.settings_tree.headerItem().setText(0, _translate("MainWindow", "Setting")) - self.settings_tree.headerItem().setText(1, _translate("MainWindow", "Value")) - __sortingEnabled = self.settings_tree.isSortingEnabled() - self.settings_tree.setSortingEnabled(False) - self.settings_tree.topLevelItem(0).setText(0, _translate("MainWindow", "Scattering")) - self.settings_tree.topLevelItem(0).child(0).setText(0, _translate("MainWindow", "Sun Height")) - self.settings_tree.topLevelItem(0).child(0).setText(1, _translate("MainWindow", "0,4")) - self.settings_tree.topLevelItem(0).child(1).setText(0, _translate("MainWindow", "Sun Color")) - self.settings_tree.topLevelItem(0).child(1).setText(1, _translate("MainWindow", "[128, 255, 100]")) - self.settings_tree.topLevelItem(0).child(2).setText(0, _translate("MainWindow", "Sun Angle")) - self.settings_tree.topLevelItem(0).child(2).setText(1, _translate("MainWindow", "180 °")) - self.settings_tree.topLevelItem(1).setText(0, _translate("MainWindow", "Ambient Occlusion")) - self.settings_tree.topLevelItem(1).child(0).setText(0, _translate("MainWindow", "Occlusion Strength")) - self.settings_tree.topLevelItem(1).child(0).setText(1, _translate("MainWindow", "0.5")) - self.settings_tree.setSortingEnabled(__sortingEnabled) - self.label.setText(_translate("MainWindow", "TIME OF DAY EDITOR")) - self.time_float_label.setText(_translate("MainWindow", "0.486")) - self.time_label.setText(_translate("MainWindow", "11:15")) - self.label_9.setText(_translate("MainWindow", "RENDER PIPELINE")) - -from . import resources_rc +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'main_window.ui' +# +# Created by: PyQt5 UI code generator 5.7 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(973, 632) + MainWindow.setMinimumSize(QtCore.QSize(973, 632)) + MainWindow.setMaximumSize(QtCore.QSize(973, 632)) + palette = QtGui.QPalette() + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush) + brush = QtGui.QBrush(QtGui.QColor(63, 63, 63)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Light, brush) + brush = QtGui.QBrush(QtGui.QColor(52, 52, 52)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Midlight, brush) + brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Dark, brush) + brush = QtGui.QBrush(QtGui.QColor(28, 28, 28)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Mid, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.BrightText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ButtonText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Shadow, brush) + brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.AlternateBase, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ToolTipBase, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ToolTipText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush) + brush = QtGui.QBrush(QtGui.QColor(63, 63, 63)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Light, brush) + brush = QtGui.QBrush(QtGui.QColor(52, 52, 52)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Midlight, brush) + brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Dark, brush) + brush = QtGui.QBrush(QtGui.QColor(28, 28, 28)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Mid, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.BrightText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ButtonText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Shadow, brush) + brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.AlternateBase, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipBase, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipText, brush) + brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush) + brush = QtGui.QBrush(QtGui.QColor(63, 63, 63)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Light, brush) + brush = QtGui.QBrush(QtGui.QColor(52, 52, 52)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Midlight, brush) + brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Dark, brush) + brush = QtGui.QBrush(QtGui.QColor(28, 28, 28)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Mid, brush) + brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.BrightText, brush) + brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Shadow, brush) + brush = QtGui.QBrush(QtGui.QColor(42, 42, 42)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.AlternateBase, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipBase, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipText, brush) + MainWindow.setPalette(palette) + icon = QtGui.QIcon() + icon.addPixmap(QtGui.QPixmap(":/res/res/icon.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + MainWindow.setWindowIcon(icon) + MainWindow.setAutoFillBackground(False) + MainWindow.setStyleSheet("QMainWindow { background: #fff;}\n" +"*, QLabel { font-family: Roboto; font-weight: 300; }\n" +"\n" +"QScrollBar {\n" +" background: #eee;\n" +"}\n" +"\n" +"\n" +"QScrollBar:vertical {\n" +" width: 9px;\n" +" margin: 0;\n" +" }\n" +"\n" +" QScrollBar::handle:vertical {\n" +" min-height: 15px;\n" +" background: #aaa;\n" +"\n" +" }\n" +"\n" +"\n" +" QScrollBar::handle:vertical:hover {\n" +" \n" +" background: #999;\n" +"\n" +" }\n" +" QScrollBar::add-line:vertical {\n" +" }\n" +"\n" +" QScrollBar::sub-line:vertical {\n" +" }\n" +"\n" +"\n" +" QScrollBar::add-page:vertical {\n" +" background: #ddd;\n" +"\n" +" }\n" +" QScrollBar::sub-page:vertical {\n" +" background: #ddd;\n" +" }\n" +"") + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.frame_current_setting = QtWidgets.QFrame(self.centralwidget) + self.frame_current_setting.setGeometry(QtCore.QRect(350, 80, 641, 551)) + self.frame_current_setting.setStyleSheet("QFrame { background: #e5e5e5;}") + self.frame_current_setting.setFrameShape(QtWidgets.QFrame.NoFrame) + self.frame_current_setting.setFrameShadow(QtWidgets.QFrame.Plain) + self.frame_current_setting.setObjectName("frame_current_setting") + self.lbl_current_setting = QtWidgets.QLabel(self.frame_current_setting) + self.lbl_current_setting.setGeometry(QtCore.QRect(20, 16, 221, 31)) + font = QtGui.QFont() + font.setFamily("Roboto") + font.setPointSize(-1) + font.setBold(False) + font.setWeight(37) + self.lbl_current_setting.setFont(font) + self.lbl_current_setting.setStyleSheet("color: #555; border: 0; font-size: 15px; text-transform: uppercase;") + self.lbl_current_setting.setObjectName("lbl_current_setting") + self.btn_reset = QtWidgets.QPushButton(self.frame_current_setting) + self.btn_reset.setGeometry(QtCore.QRect(500, 14, 101, 31)) + self.btn_reset.setStyleSheet("QPushButton {\n" +"color: #eee;\n" +"background: #666;\n" +"border: 0; \n" +"}\n" +"\n" +"QPushButton:hover {\n" +" background: #555;\n" +"}\n" +"\n" +"QPushButton:pressed {\n" +" background: #111;\n" +"}") + self.btn_reset.setObjectName("btn_reset") + self.frame_3 = QtWidgets.QFrame(self.frame_current_setting) + self.frame_3.setGeometry(QtCore.QRect(20, 94, 581, 441)) + self.frame_3.setStyleSheet("QFrame {\n" +"}") + self.frame_3.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame_3.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame_3.setObjectName("frame_3") + self.verticalLayoutWidget = QtWidgets.QWidget(self.frame_3) + self.verticalLayoutWidget.setGeometry(QtCore.QRect(0, 0, 571, 441)) + self.verticalLayoutWidget.setObjectName("verticalLayoutWidget") + self.prefab_edit_widget = QtWidgets.QVBoxLayout(self.verticalLayoutWidget) + self.prefab_edit_widget.setContentsMargins(0, 0, 0, 0) + self.prefab_edit_widget.setSpacing(0) + self.prefab_edit_widget.setObjectName("prefab_edit_widget") + self.lbl_setting_desc = QtWidgets.QLabel(self.frame_current_setting) + self.lbl_setting_desc.setGeometry(QtCore.QRect(20, 50, 571, 41)) + font = QtGui.QFont() + font.setFamily("Roboto") + font.setBold(False) + font.setWeight(37) + self.lbl_setting_desc.setFont(font) + self.lbl_setting_desc.setStyleSheet("color: #888;") + self.lbl_setting_desc.setWordWrap(True) + self.lbl_setting_desc.setObjectName("lbl_setting_desc") + self.btn_insert_point = QtWidgets.QPushButton(self.frame_current_setting) + self.btn_insert_point.setGeometry(QtCore.QRect(368, 14, 131, 31)) + self.btn_insert_point.setStyleSheet("QPushButton {\n" +"color: #eee;\n" +"background: #666;\n" +"border: 0; \n" +"}\n" +"\n" +"QPushButton:hover {\n" +" background: #555;\n" +"}\n" +"\n" +"QPushButton:pressed {\n" +" background: #111;\n" +"}") + self.btn_insert_point.setObjectName("btn_insert_point") + self.frame_2 = QtWidgets.QFrame(self.centralwidget) + self.frame_2.setGeometry(QtCore.QRect(-1, 80, 351, 551)) + self.frame_2.setStyleSheet("background: #eee;") + self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame_2.setObjectName("frame_2") + self.settings_tree = QtWidgets.QTreeWidget(self.frame_2) + self.settings_tree.setGeometry(QtCore.QRect(14, 20, 311, 511)) + self.settings_tree.setStyleSheet("QTreeWidget::item {\n" +"padding: 7px 7px 6px;\n" +"background: #ddd;\n" +"outline: 0 !important;\n" +"margin-bottom: 1px;\n" +"margin-right: 0px;\n" +"color: #777;\n" +"border: 0;\n" +"border-radius: 0;\n" +"}\n" +"\n" +"QTreeWidget::item:hover {\n" +"background: #ccc;\n" +"}\n" +"\n" +"QTreeWidget::item:selected {\n" +"background: #555;\n" +"color: #eee;\n" +"}\n" +"\n" +"QTreeWidget {\n" +"padding: 0;\n" +"color: #eee;\n" +"background: transparent;\n" +"\n" +"}\n" +"\n" +"* {\n" +"outline: 0;\n" +"}\n" +"\n" +"") + self.settings_tree.setFrameShape(QtWidgets.QFrame.NoFrame) + self.settings_tree.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.settings_tree.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.settings_tree.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + self.settings_tree.setProperty("showDropIndicator", False) + self.settings_tree.setAlternatingRowColors(False) + self.settings_tree.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + self.settings_tree.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) + self.settings_tree.setIndentation(18) + self.settings_tree.setRootIsDecorated(True) + self.settings_tree.setUniformRowHeights(False) + self.settings_tree.setItemsExpandable(True) + self.settings_tree.setAnimated(True) + self.settings_tree.setAllColumnsShowFocus(False) + self.settings_tree.setWordWrap(True) + self.settings_tree.setHeaderHidden(True) + self.settings_tree.setObjectName("settings_tree") + item_0 = QtWidgets.QTreeWidgetItem(self.settings_tree) + item_1 = QtWidgets.QTreeWidgetItem(item_0) + item_1 = QtWidgets.QTreeWidgetItem(item_0) + item_1 = QtWidgets.QTreeWidgetItem(item_0) + item_0 = QtWidgets.QTreeWidgetItem(self.settings_tree) + item_0.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsDragEnabled|QtCore.Qt.ItemIsUserCheckable|QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsTristate) + item_1 = QtWidgets.QTreeWidgetItem(item_0) + self.settings_tree.header().setVisible(False) + self.settings_tree.header().setCascadingSectionResizes(False) + self.settings_tree.header().setDefaultSectionSize(150) + self.settings_tree.header().setStretchLastSection(False) + self.frame_4 = QtWidgets.QFrame(self.centralwidget) + self.frame_4.setGeometry(QtCore.QRect(420, 10, 551, 61)) + self.frame_4.setStyleSheet("QFrame {\n" +" background: #fff;\n" +"}\n" +"") + self.frame_4.setFrameShape(QtWidgets.QFrame.NoFrame) + self.frame_4.setFrameShadow(QtWidgets.QFrame.Plain) + self.frame_4.setObjectName("frame_4") + self.time_slider = QtWidgets.QSlider(self.frame_4) + self.time_slider.setGeometry(QtCore.QRect(10, 11, 521, 20)) + self.time_slider.setMaximum(5184000) + self.time_slider.setSingleStep(3600) + self.time_slider.setPageStep(3600) + self.time_slider.setProperty("value", 2592000) + self.time_slider.setTracking(True) + self.time_slider.setOrientation(QtCore.Qt.Horizontal) + self.time_slider.setInvertedAppearance(False) + self.time_slider.setInvertedControls(False) + self.time_slider.setTickPosition(QtWidgets.QSlider.TicksBelow) + self.time_slider.setTickInterval(432000) + self.time_slider.setObjectName("time_slider") + self.label_2 = QtWidgets.QLabel(self.frame_4) + self.label_2.setGeometry(QtCore.QRect(254, 30, 32, 32)) + self.label_2.setText("") + self.label_2.setPixmap(QtGui.QPixmap(":/res/res/12_00.png")) + self.label_2.setObjectName("label_2") + self.label_3 = QtWidgets.QLabel(self.frame_4) + self.label_3.setGeometry(QtCore.QRect(341, 30, 32, 32)) + self.label_3.setText("") + self.label_3.setPixmap(QtGui.QPixmap(":/res/res/16_00.png")) + self.label_3.setObjectName("label_3") + self.label_4 = QtWidgets.QLabel(self.frame_4) + self.label_4.setGeometry(QtCore.QRect(0, 30, 32, 32)) + self.label_4.setText("") + self.label_4.setPixmap(QtGui.QPixmap(":/res/res/0_00.png")) + self.label_4.setObjectName("label_4") + self.label_5 = QtWidgets.QLabel(self.frame_4) + self.label_5.setGeometry(QtCore.QRect(84, 30, 32, 32)) + self.label_5.setText("") + self.label_5.setPixmap(QtGui.QPixmap(":/res/res/4_00.png")) + self.label_5.setObjectName("label_5") + self.label_6 = QtWidgets.QLabel(self.frame_4) + self.label_6.setGeometry(QtCore.QRect(170, 30, 32, 32)) + self.label_6.setText("") + self.label_6.setPixmap(QtGui.QPixmap(":/res/res/8_00.png")) + self.label_6.setObjectName("label_6") + self.label_7 = QtWidgets.QLabel(self.frame_4) + self.label_7.setGeometry(QtCore.QRect(422, 30, 32, 32)) + self.label_7.setText("") + self.label_7.setPixmap(QtGui.QPixmap(":/res/res/20_00.png")) + self.label_7.setObjectName("label_7") + self.label_8 = QtWidgets.QLabel(self.frame_4) + self.label_8.setGeometry(QtCore.QRect(510, 30, 32, 32)) + self.label_8.setText("") + self.label_8.setPixmap(QtGui.QPixmap(":/res/res/0_00.png")) + self.label_8.setObjectName("label_8") + self.label = QtWidgets.QLabel(self.centralwidget) + self.label.setGeometry(QtCore.QRect(20, 35, 261, 31)) + self.label.setStyleSheet("font-size: 20px; color: #2aa877;\n" +"font-weight: 100;") + self.label.setObjectName("label") + self.time_float_label = QtWidgets.QLabel(self.centralwidget) + self.time_float_label.setGeometry(QtCore.QRect(350, 47, 41, 18)) + font = QtGui.QFont() + font.setFamily("Roboto") + font.setPointSize(-1) + font.setBold(False) + font.setWeight(37) + self.time_float_label.setFont(font) + self.time_float_label.setStyleSheet("color: #777; font-size: 12px;") + self.time_float_label.setObjectName("time_float_label") + self.time_label = QtWidgets.QLabel(self.centralwidget) + self.time_label.setGeometry(QtCore.QRect(350, 20, 81, 31)) + font = QtGui.QFont() + font.setFamily("Roboto") + font.setPointSize(-1) + font.setBold(False) + font.setWeight(37) + self.time_label.setFont(font) + self.time_label.setStyleSheet("color: #444; font-size: 22px;") + self.time_label.setObjectName("time_label") + self.label_9 = QtWidgets.QLabel(self.centralwidget) + self.label_9.setGeometry(QtCore.QRect(20, 16, 261, 31)) + self.label_9.setStyleSheet("font-size: 13px;\n" +"color: #aaa;\n" +"font-weight: 600;") + self.label_9.setObjectName("label_9") + self.frame_2.raise_() + self.frame_current_setting.raise_() + self.frame_4.raise_() + self.label.raise_() + self.time_float_label.raise_() + self.time_label.raise_() + self.label_9.raise_() + MainWindow.setCentralWidget(self.centralwidget) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "Day Time Editor")) + self.lbl_current_setting.setText(_translate("MainWindow", "Occlusion Strength")) + self.btn_reset.setText(_translate("MainWindow", "Reset curve")) + self.lbl_setting_desc.setText(_translate("MainWindow", "Description: Some Description about the setting, to give the user a rough idea what this does. This description may be so long, it can even go into the next line.")) + self.btn_insert_point.setText(_translate("MainWindow", "Insert point from data")) + self.settings_tree.setSortingEnabled(False) + self.settings_tree.headerItem().setText(0, _translate("MainWindow", "Setting")) + self.settings_tree.headerItem().setText(1, _translate("MainWindow", "Value")) + __sortingEnabled = self.settings_tree.isSortingEnabled() + self.settings_tree.setSortingEnabled(False) + self.settings_tree.topLevelItem(0).setText(0, _translate("MainWindow", "Scattering")) + self.settings_tree.topLevelItem(0).child(0).setText(0, _translate("MainWindow", "Sun Height")) + self.settings_tree.topLevelItem(0).child(0).setText(1, _translate("MainWindow", "0,4")) + self.settings_tree.topLevelItem(0).child(1).setText(0, _translate("MainWindow", "Sun Color")) + self.settings_tree.topLevelItem(0).child(1).setText(1, _translate("MainWindow", "[128, 255, 100]")) + self.settings_tree.topLevelItem(0).child(2).setText(0, _translate("MainWindow", "Sun Angle")) + self.settings_tree.topLevelItem(0).child(2).setText(1, _translate("MainWindow", "180 °")) + self.settings_tree.topLevelItem(1).setText(0, _translate("MainWindow", "Ambient Occlusion")) + self.settings_tree.topLevelItem(1).child(0).setText(0, _translate("MainWindow", "Occlusion Strength")) + self.settings_tree.topLevelItem(1).child(0).setText(1, _translate("MainWindow", "0.5")) + self.settings_tree.setSortingEnabled(__sortingEnabled) + self.label.setText(_translate("MainWindow", "TIME OF DAY EDITOR")) + self.time_float_label.setText(_translate("MainWindow", "0.486")) + self.time_label.setText(_translate("MainWindow", "11:15")) + self.label_9.setText(_translate("MainWindow", "RENDER PIPELINE")) + +from . import resources_rc diff --git a/RenderPipelineFile/toolkit/day_time_editor/ui/point_insert.ui b/RenderPipelineFile/toolkit/day_time_editor/ui/point_insert.ui index 0c5b7baa..3cbe70d7 100644 --- a/RenderPipelineFile/toolkit/day_time_editor/ui/point_insert.ui +++ b/RenderPipelineFile/toolkit/day_time_editor/ui/point_insert.ui @@ -1,124 +1,124 @@ - - - Dialog - - - - 0 - 0 - 342 - 197 - - - - - 342 - 197 - - - - - 342 - 197 - - - - Insert Point - - - - - 10 - 10 - 301 - 71 - - - - Enter the values of the point you want to insert. - -Did you know: If you dont want to enter concrete values, just click and drag anywhere on the curve to insert a new point. - - - true - - - - - - 10 - 100 - 46 - 21 - - - - Time: - - - - - - 10 - 130 - 46 - 21 - - - - Value: - - - - - - 70 - 130 - 121 - 22 - - - - 99999.990000000005239 - - - - - - 70 - 100 - 121 - 22 - - - - HH:mm - - - - - - 240 - 160 - 75 - 23 - - - - Qt::TabFocus - - - Insert - - - - - ipt_time - ipt_value - btn_insert - - - - - - + + + Dialog + + + + 0 + 0 + 342 + 197 + + + + + 342 + 197 + + + + + 342 + 197 + + + + Insert Point + + + + + 10 + 10 + 301 + 71 + + + + Enter the values of the point you want to insert. + +Did you know: If you dont want to enter concrete values, just click and drag anywhere on the curve to insert a new point. + + + true + + + + + + 10 + 100 + 46 + 21 + + + + Time: + + + + + + 10 + 130 + 46 + 21 + + + + Value: + + + + + + 70 + 130 + 121 + 22 + + + + 99999.990000000005239 + + + + + + 70 + 100 + 121 + 22 + + + + HH:mm + + + + + + 240 + 160 + 75 + 23 + + + + Qt::TabFocus + + + Insert + + + + + ipt_time + ipt_value + btn_insert + + + + + + diff --git a/RenderPipelineFile/toolkit/day_time_editor/ui/point_insert_dialog_generated.py b/RenderPipelineFile/toolkit/day_time_editor/ui/point_insert_dialog_generated.py index ba19fa01..e2e1277e 100644 --- a/RenderPipelineFile/toolkit/day_time_editor/ui/point_insert_dialog_generated.py +++ b/RenderPipelineFile/toolkit/day_time_editor/ui/point_insert_dialog_generated.py @@ -1,55 +1,55 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'point_insert.ui' -# -# Created by: PyQt5 UI code generator 5.7 -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore, QtGui, QtWidgets - -class Ui_Dialog(object): - def setupUi(self, Dialog): - Dialog.setObjectName("Dialog") - Dialog.resize(342, 197) - Dialog.setMinimumSize(QtCore.QSize(342, 197)) - Dialog.setMaximumSize(QtCore.QSize(342, 197)) - self.label = QtWidgets.QLabel(Dialog) - self.label.setGeometry(QtCore.QRect(10, 10, 301, 71)) - self.label.setWordWrap(True) - self.label.setObjectName("label") - self.label_2 = QtWidgets.QLabel(Dialog) - self.label_2.setGeometry(QtCore.QRect(10, 100, 46, 21)) - self.label_2.setObjectName("label_2") - self.label_3 = QtWidgets.QLabel(Dialog) - self.label_3.setGeometry(QtCore.QRect(10, 130, 46, 21)) - self.label_3.setObjectName("label_3") - self.ipt_value = QtWidgets.QDoubleSpinBox(Dialog) - self.ipt_value.setGeometry(QtCore.QRect(70, 130, 121, 22)) - self.ipt_value.setMaximum(99999.99) - self.ipt_value.setObjectName("ipt_value") - self.ipt_time = QtWidgets.QTimeEdit(Dialog) - self.ipt_time.setGeometry(QtCore.QRect(70, 100, 121, 22)) - self.ipt_time.setObjectName("ipt_time") - self.btn_insert = QtWidgets.QPushButton(Dialog) - self.btn_insert.setGeometry(QtCore.QRect(240, 160, 75, 23)) - self.btn_insert.setFocusPolicy(QtCore.Qt.TabFocus) - self.btn_insert.setObjectName("btn_insert") - - self.retranslateUi(Dialog) - QtCore.QMetaObject.connectSlotsByName(Dialog) - Dialog.setTabOrder(self.ipt_time, self.ipt_value) - Dialog.setTabOrder(self.ipt_value, self.btn_insert) - - def retranslateUi(self, Dialog): - _translate = QtCore.QCoreApplication.translate - Dialog.setWindowTitle(_translate("Dialog", "Insert Point")) - self.label.setText(_translate("Dialog", "Enter the values of the point you want to insert. \n" -"\n" -"Did you know: If you dont want to enter concrete values, just click and drag anywhere on the curve to insert a new point.")) - self.label_2.setText(_translate("Dialog", "Time:")) - self.label_3.setText(_translate("Dialog", "Value:")) - self.ipt_time.setDisplayFormat(_translate("Dialog", "HH:mm")) - self.btn_insert.setText(_translate("Dialog", "Insert")) - -from . import resources_rc +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'point_insert.ui' +# +# Created by: PyQt5 UI code generator 5.7 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_Dialog(object): + def setupUi(self, Dialog): + Dialog.setObjectName("Dialog") + Dialog.resize(342, 197) + Dialog.setMinimumSize(QtCore.QSize(342, 197)) + Dialog.setMaximumSize(QtCore.QSize(342, 197)) + self.label = QtWidgets.QLabel(Dialog) + self.label.setGeometry(QtCore.QRect(10, 10, 301, 71)) + self.label.setWordWrap(True) + self.label.setObjectName("label") + self.label_2 = QtWidgets.QLabel(Dialog) + self.label_2.setGeometry(QtCore.QRect(10, 100, 46, 21)) + self.label_2.setObjectName("label_2") + self.label_3 = QtWidgets.QLabel(Dialog) + self.label_3.setGeometry(QtCore.QRect(10, 130, 46, 21)) + self.label_3.setObjectName("label_3") + self.ipt_value = QtWidgets.QDoubleSpinBox(Dialog) + self.ipt_value.setGeometry(QtCore.QRect(70, 130, 121, 22)) + self.ipt_value.setMaximum(99999.99) + self.ipt_value.setObjectName("ipt_value") + self.ipt_time = QtWidgets.QTimeEdit(Dialog) + self.ipt_time.setGeometry(QtCore.QRect(70, 100, 121, 22)) + self.ipt_time.setObjectName("ipt_time") + self.btn_insert = QtWidgets.QPushButton(Dialog) + self.btn_insert.setGeometry(QtCore.QRect(240, 160, 75, 23)) + self.btn_insert.setFocusPolicy(QtCore.Qt.TabFocus) + self.btn_insert.setObjectName("btn_insert") + + self.retranslateUi(Dialog) + QtCore.QMetaObject.connectSlotsByName(Dialog) + Dialog.setTabOrder(self.ipt_time, self.ipt_value) + Dialog.setTabOrder(self.ipt_value, self.btn_insert) + + def retranslateUi(self, Dialog): + _translate = QtCore.QCoreApplication.translate + Dialog.setWindowTitle(_translate("Dialog", "Insert Point")) + self.label.setText(_translate("Dialog", "Enter the values of the point you want to insert. \n" +"\n" +"Did you know: If you dont want to enter concrete values, just click and drag anywhere on the curve to insert a new point.")) + self.label_2.setText(_translate("Dialog", "Time:")) + self.label_3.setText(_translate("Dialog", "Value:")) + self.ipt_time.setDisplayFormat(_translate("Dialog", "HH:mm")) + self.btn_insert.setText(_translate("Dialog", "Insert")) + +from . import resources_rc diff --git a/RenderPipelineFile/toolkit/day_time_editor/ui/resources.qrc b/RenderPipelineFile/toolkit/day_time_editor/ui/resources.qrc index 29cbc9ea..eb9aafa4 100644 --- a/RenderPipelineFile/toolkit/day_time_editor/ui/resources.qrc +++ b/RenderPipelineFile/toolkit/day_time_editor/ui/resources.qrc @@ -1,14 +1,14 @@ - - - res/0_00.png - res/4_00.png - res/8_00.png - res/12_00.png - res/16_00.png - res/20_00.png - res/icon.png - res/12_00.png - res/daytime_editor_logo.png - res/icon.png - - + + + res/0_00.png + res/4_00.png + res/8_00.png + res/12_00.png + res/16_00.png + res/20_00.png + res/icon.png + res/12_00.png + res/daytime_editor_logo.png + res/icon.png + + diff --git a/RenderPipelineFile/toolkit/day_time_editor/ui/resources_rc.py b/RenderPipelineFile/toolkit/day_time_editor/ui/resources_rc.py index 86834013..61a3f95e 100644 --- a/RenderPipelineFile/toolkit/day_time_editor/ui/resources_rc.py +++ b/RenderPipelineFile/toolkit/day_time_editor/ui/resources_rc.py @@ -1,1392 +1,1392 @@ -# -*- coding: utf-8 -*- - -# Resource object code -# -# Created by: The Resource Compiler for PyQt5 (Qt v5.7.0) -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore - -qt_resource_data = b"\ -\x00\x00\x03\x92\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ -\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ -\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ -\x01\x00\x9a\x9c\x18\x00\x00\x03\x34\x49\x44\x41\x54\x58\x85\xed\ -\x95\x4f\x68\x14\x57\x1c\xc7\xbf\xbf\x37\x33\x49\x30\x71\xff\xcc\ -\xee\xcb\x8e\x2b\x71\xb2\xd9\xf1\x60\x1a\xa1\xd0\x43\x29\x68\x05\ -\xb1\xd4\xb3\x14\xc4\xe0\x29\x94\x56\x45\x54\x54\xea\xc9\x96\x82\ -\x50\x3d\x14\x52\x28\x2d\xbd\x04\x7b\x08\x85\x7a\x16\x3c\xa8\x54\ -\xa4\x39\xf8\x07\xdb\x6a\x25\x98\x75\x45\xbb\x26\x6b\x66\xb2\x6b\ -\x74\x5d\xb3\x66\xe7\xfd\x7a\x90\x15\xdd\x64\xb3\xb3\x58\x2c\xa5\ -\xf9\xde\x66\xde\xf7\x7d\x7f\x9f\x79\xef\xf7\xe6\x01\xcb\x5a\xd6\ -\x7f\x59\x8e\x69\x86\xfa\xa5\xec\x7a\x9d\x0c\xf1\x3a\x93\x59\x17\ -\x7b\xe7\x04\x76\xbf\x11\x80\x3e\xab\x7b\x57\xaa\x3b\x36\xf8\x0a\ -\x80\x80\x00\xa0\xbd\xfc\x2e\x95\x88\x0f\xa5\x12\xf1\xa1\x7f\x1c\ -\x40\x43\xf5\x02\x20\x4e\x38\xa6\x19\x7a\x01\xc0\x98\x24\xc2\x64\ -\xed\xd9\xb6\xc3\x11\x41\x74\x8c\x7c\x5c\x0c\x9e\x1b\x50\x85\xd2\ -\x53\xcf\xec\xea\x5c\x4d\xba\x16\x2e\x94\x9e\xfc\x01\x00\x0f\x9f\ -\x94\xaf\x15\x4b\xe5\xdf\x6b\x9e\x58\x47\x68\x07\x11\xe7\xb2\xd3\ -\xde\xcf\x41\x73\x69\xa9\x31\xc7\x34\x57\x66\x0a\x85\x47\xf5\x03\ -\xa9\x55\xf1\x8d\xc4\xd8\xce\xa0\xb7\x00\x80\x14\xdf\x24\xa6\xd1\ -\xdb\xae\x3b\x56\xef\x75\x4c\x33\xb4\x58\x46\x53\x80\xb5\x96\xb9\ -\x4e\x81\xce\x33\x8b\x61\xb1\x32\x3c\x9c\xc9\x64\x2a\x7d\xd1\x68\ -\x98\xdb\xb4\x93\xed\x6d\x3c\xb0\x73\x73\xe7\xe4\x86\x81\x8e\x18\ -\x00\x5c\xbc\x5e\x29\x8c\x9e\x2b\x25\x2b\xbe\xb8\x32\x47\xfa\xd0\ -\xd4\xd4\x54\xd9\x71\x9c\x76\x7e\x3c\x7b\x10\xa4\xf6\x71\x95\xde\ -\xcf\x7a\xde\x44\xab\x2b\x80\x74\x32\xda\x03\xa5\x9f\x60\xa8\x0b\ -\xd1\xfc\xcc\x48\xc1\x8a\xfd\xb2\x71\x7d\xfb\xfc\xb7\xfb\x23\x1b\ -\xda\x84\xf6\xca\xf6\x3d\xf3\x7d\xff\x93\xaf\x67\xc7\x2e\x8f\x57\ -\x2a\xb7\xf3\xde\xd6\x74\x42\xee\x01\xf0\xae\x30\xfc\x23\x13\xb9\ -\xc2\xfd\x96\x57\x60\x01\x8c\x25\x0f\xf7\x74\x6b\xdb\xce\x1c\x97\ -\xef\x2d\xe5\xdb\xf2\xd9\x83\xcb\x79\x8f\x4f\x66\xf2\xee\x77\x41\ -\x72\x83\xff\x07\x14\xef\xfd\x7e\x5f\x64\x75\x33\xdb\xf0\x2e\x53\ -\x32\x70\x20\x68\xec\xa2\x00\xef\x00\x86\x6d\x87\x23\xb6\x1d\x8e\ -\x00\x10\xe9\x64\xb4\xc7\x30\xc0\xa9\xa4\xb1\xa6\x59\xe0\x40\x9f\ -\xd1\xab\x69\xe8\xb0\xa5\xb4\x00\x68\xb5\x9c\x4d\x80\x1e\x18\xa0\ -\x68\xc5\x8f\xe8\x15\x7d\x5c\xaf\xe8\xe3\x7d\xdd\xf1\xed\xfe\xbc\ -\x6e\x76\x18\x78\x1c\xf4\xab\x42\x5d\x98\x68\xd3\xfc\x68\xda\x92\ -\x83\xb5\x9c\xbf\x12\xf2\xd0\x62\xde\x40\x3d\x60\x4b\x69\x19\x1a\ -\x5d\xbd\x31\x92\x48\x06\xf1\xaf\xff\x78\x72\xfa\xa9\x98\x5f\x97\ -\xcb\x3d\x2a\x34\xf3\x06\xea\x81\xbb\xae\x9b\xf7\xa1\x8a\xbf\xdd\ -\x7a\x36\xde\xcc\xfb\xeb\x8d\xb9\xeb\xaa\x2a\xee\x05\x29\x0e\x34\ -\x58\x01\xc7\x92\x7b\x18\xea\xf3\x7a\x2f\x13\x48\x23\xc0\x8a\xea\ -\x57\x4f\x1f\x8f\xdd\xda\xf1\x65\x71\x40\x37\x58\x8d\x1e\x8d\xff\ -\xf9\xc1\x21\xb7\xdf\x9b\xad\xbe\xed\x33\x40\x0c\x06\xc0\x2f\x4f\ -\x66\xe0\x8b\x6c\x7e\xe6\x87\x40\x00\x00\x44\xbf\x94\x2b\x1a\x51\ -\x87\x5c\xd7\x77\x2d\x39\xc2\xc0\x0a\x02\x34\xc5\x3c\xa3\x1e\x78\ -\x9f\x76\x4a\xb9\x68\xa3\x01\xc0\x4d\xd7\x2d\x03\x50\x41\x01\x00\ -\x00\xbd\x96\x65\x0b\xae\x2e\x3c\xf7\x44\x83\x0c\x94\xd7\xe4\xdd\ -\x9d\xf7\x1d\x47\x53\xa5\xd9\x53\xc4\x5c\x65\x60\xe1\x1d\x20\xfc\ -\xb1\xec\x54\xf1\x5e\xa3\x1a\x0d\x89\x9f\x0f\x56\x7b\x15\xd1\x96\ -\x3a\x62\x62\xf0\xa5\x3b\x79\xef\xab\x3b\x80\x8f\x4c\xa6\xba\x09\ -\xd8\x96\x4b\xc8\xa3\x44\xea\x43\x86\xf0\xeb\x52\x72\x00\x1a\x02\ -\xfc\xeb\x0a\x74\x0c\xd3\x96\x3c\xcc\xe0\x8f\x5a\x4a\x56\xf8\x29\ -\x3b\xed\x7d\xd3\xcc\xb6\xe4\x16\xd4\x44\xd0\x7e\x14\xc2\x3f\xdb\ -\x4a\x7d\x22\x3d\xd7\x8a\x7f\x59\xcb\xfa\xff\xea\x6f\xcd\x62\x28\ -\x7d\x4b\x8a\x77\x07\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\ -\x82\ -\x00\x00\x03\xe9\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ -\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ -\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ -\x01\x00\x9a\x9c\x18\x00\x00\x03\x8b\x49\x44\x41\x54\x58\x85\xed\ -\x96\x5f\x68\xdb\x55\x14\xc7\xbf\xe7\xde\xdf\x2f\xc9\x7e\x69\x9b\ -\xa6\x69\xf2\x6b\xcc\xdc\xaf\x26\x6d\x61\x6b\x29\xcc\x8d\x81\x5b\ -\x5d\xa7\x3e\x14\x06\xb3\xe2\x98\x7f\x86\xa8\x0c\xe6\x54\x9c\xc5\ -\x09\x13\x8a\x8c\x22\x4c\xd0\x47\xc5\xfa\xa0\x20\x0a\x4a\x19\x3e\ -\x29\xc8\x64\x0c\x54\xc4\x3d\x0d\x1f\xca\xaa\xb0\xa6\x69\xba\x48\ -\xda\x25\xb4\x4b\xb7\xa6\x49\x93\xdf\x3d\x3e\x8c\xb4\x51\x97\x3f\ -\x85\x20\x88\xfd\xbe\xdd\x73\xbe\xf7\x9c\x0f\x87\x7b\x2f\x17\xd8\ -\xd2\x96\xfe\x83\x22\xcb\xb2\x5c\x8d\x2a\xa6\x6d\xc6\x1c\xf1\xfb\ -\xf7\x93\xc4\x88\x5a\xcb\xfe\x01\xe0\x4c\x23\x00\x44\xdd\xcd\x83\ -\xed\x27\xa5\x26\x2e\xb8\x74\xd9\x23\x18\x57\xee\xe9\x31\xfd\xa7\ -\xc3\xa6\x19\x68\x38\x40\x24\x10\x18\xd6\xa5\x36\xfa\xed\xc5\x57\ -\x65\x51\xa0\x89\x6d\xfc\xfa\xcf\xe6\xbe\x43\xcc\x78\x85\xa9\xf8\ -\x76\x43\x01\xc2\x5e\xaf\x87\x89\x3f\xfa\xfc\x8b\x17\x0a\xa1\x90\ -\xa7\xa3\xb8\xc6\x06\x49\x99\x29\xaf\x11\x31\xfd\xaf\x41\xc8\xaf\ -\xde\x79\xef\xfd\x65\xa7\xd3\x75\x38\x62\xfa\xc6\xc2\xa6\xbb\xae\ -\x49\xd4\x9e\x80\x43\x7b\xe9\xc1\xdd\xf7\x47\x7b\xfb\x83\xdd\x00\ -\xe0\x74\xd0\x9d\x02\x15\x7c\xa5\xb4\xe5\xf7\x07\x98\xd4\x49\xab\ -\x33\x1c\x3d\x76\xfc\xf8\xde\xd3\x23\x6f\x26\x41\x34\x20\x94\x71\ -\xb0\x1e\x80\x9a\x87\x90\x18\x4f\xbf\x71\xf6\x91\xa6\xd2\x7a\xfb\ -\x0e\x6f\x6a\xfa\x7a\x7a\x1f\x80\xdf\x01\x20\x9e\x4a\xcd\x5b\x96\ -\x67\x30\x36\x33\xfd\xd9\xd0\xc0\x81\x2b\x73\x37\x62\x21\x56\x72\ -\x38\x96\xba\x39\x59\x0f\x40\xd5\x09\x0c\x02\x1a\x4b\x74\xf7\xee\ -\x0a\xf6\x94\x62\xcf\x3c\xbb\x4f\x03\xf8\xb9\x72\x5f\x3c\x9e\xb9\ -\x65\x3b\x0b\x27\xe2\x37\x66\xbd\x60\x1a\x8d\xdd\xac\xaf\x79\x4d\ -\x80\x64\x30\xd8\x2a\x20\xee\x68\x9a\x46\xa5\xd8\xf0\x13\x7d\x7b\ -\x75\x87\x1e\x0e\x07\x02\x8f\x95\x7b\xe7\xe6\x32\x4b\x94\xb7\x0f\ -\xcc\x2c\xa4\x27\xea\x6d\x5e\x13\xe0\x36\x90\x85\x62\xa3\x3c\xe6\ -\x70\xe8\xf2\xc3\x0f\x9e\x5a\x85\xe4\x73\x3b\x5b\x5b\xad\xf2\xdc\ -\xcc\xd2\x52\x06\x9b\x54\xf5\x09\x24\x93\x59\x66\x95\x89\xc7\x6f\ -\x25\xca\xe3\x0f\x0d\x3c\xd0\x37\xfa\xd6\x90\xcc\xbb\xb4\x89\x48\ -\xd0\x3f\xb4\xd9\xa6\xe5\x92\xb5\x0c\x5e\xb7\xd1\x9d\xcf\x15\xe4\ -\xa1\x47\x7b\x3a\xcb\xe3\x7d\xfd\xf7\xed\xd8\xb3\xdb\xca\x5c\xfa\ -\xfe\xda\x91\x66\xe7\xb6\xa3\x6d\x2d\x86\xee\x6b\x31\xb8\xd9\x68\ -\x76\x05\x3c\x46\xa7\xc7\x30\x0e\xb7\xb9\x8d\x13\xfd\x2b\xd9\x4b\ -\x71\x40\x55\xaa\x4f\x95\x12\x25\x75\x77\xb4\xed\x54\xa4\x5d\xfe\ -\xe9\xe7\x33\x6e\x8f\xc7\xd5\xf2\xf7\x7c\xa1\xc0\xea\xbb\x8b\x93\ -\x57\x27\xbe\xbc\x9a\x8b\xcd\xa6\xda\xec\x9c\x32\x34\x97\xc8\xda\ -\xb6\x9d\xcf\xe5\xec\x6b\x33\xc9\xf4\xf3\xd5\xea\x57\x04\x18\x2c\ -\xbb\xa2\x89\x8e\xf6\xf3\xdb\x43\xde\x87\xbf\xfe\xe6\xe5\x3d\x12\ -\xaa\xe6\xd4\x62\xb3\x4b\xf1\x63\x4f\x7e\xea\x14\xb6\x3a\x12\x4a\ -\xa7\x27\x01\xe0\x47\xa0\x58\x37\x40\xd8\x6c\x7f\x1d\xc4\xef\xde\ -\x35\xd0\x2f\xc4\xac\x6c\xd0\xea\xdd\x2c\xbb\x88\xa8\x22\x84\x6d\ -\x53\x4e\x03\x08\x82\x0b\x4c\x68\x02\x78\x3f\x00\x08\x88\xb3\xd3\ -\xf3\xa9\xf1\x5a\xf0\xff\xba\xaa\x9e\x81\x88\x69\xf6\x2a\x52\x07\ -\x89\xd8\xb9\xbe\x81\x49\x03\xf3\x5c\x74\x21\x7d\xe1\x2f\x5e\x9f\ -\xef\x71\xe8\xd4\xc5\xb4\x71\xe0\x98\x29\xaf\xc1\xfe\xe1\xfa\xfc\ -\xe2\x6f\x95\x7a\x54\x7d\x8a\x99\x8a\xdb\x24\x09\x0f\x83\xd6\x3f\ -\x20\x24\x88\x55\x11\x47\x23\xa6\x7f\x57\x74\x21\x35\x06\x00\xe1\ -\x8e\xc0\x39\x56\xdc\x2b\x04\x4d\x11\x00\x28\x30\x00\x56\xc2\xd6\ -\x19\xa2\xea\xe7\xa5\xe6\x2d\xb8\x97\x2c\xcb\xd3\xaa\x67\x1d\x1f\ -\x2b\x0d\x51\xa1\x00\x26\x44\x68\xcd\x3e\x35\xbd\xb8\xb8\xbc\xd9\ -\x5a\x75\x01\x44\x4c\xff\x18\x98\x5f\x5c\x0f\x08\xba\xec\xb4\x31\ -\x92\x23\x1e\x27\x52\xc2\x0d\xfd\xd4\x6d\xd8\xe3\x82\x31\xb8\xe1\ -\xc1\x27\xd1\xf9\xf4\xf9\x86\x00\x74\x75\x75\x39\xc5\xca\x4a\x73\ -\x69\x9d\x95\x72\x25\x91\x48\xac\x62\xe3\x21\xb3\x83\xc1\xa0\xd1\ -\x0c\xac\x3f\xdb\x9a\x37\xb9\x3c\x35\x85\xb5\x7a\xea\x6f\x69\x4b\ -\xff\x6f\xfd\x09\xec\x59\x33\x5e\xbb\x11\x8b\x6a\x00\x00\x00\x00\ -\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x17\x50\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x03\xe8\x00\x00\x00\x44\x08\x06\x00\x00\x00\x12\x95\x1f\xec\ -\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ -\x01\x00\x9a\x9c\x18\x00\x00\x0a\x4f\x69\x43\x43\x50\x50\x68\x6f\ -\x74\x6f\x73\x68\x6f\x70\x20\x49\x43\x43\x20\x70\x72\x6f\x66\x69\ -\x6c\x65\x00\x00\x78\xda\x9d\x53\x67\x54\x53\xe9\x16\x3d\xf7\xde\ -\xf4\x42\x4b\x88\x80\x94\x4b\x6f\x52\x15\x08\x20\x52\x42\x8b\x80\ -\x14\x91\x26\x2a\x21\x09\x10\x4a\x88\x21\xa1\xd9\x15\x51\xc1\x11\ -\x45\x45\x04\x1b\xc8\xa0\x88\x03\x8e\x8e\x80\x8c\x15\x51\x2c\x0c\ -\x8a\x0a\xd8\x07\xe4\x21\xa2\x8e\x83\xa3\x88\x8a\xca\xfb\xe1\x7b\ -\xa3\x6b\xd6\xbc\xf7\xe6\xcd\xfe\xb5\xd7\x3e\xe7\xac\xf3\x9d\xb3\ -\xcf\x07\xc0\x08\x0c\x96\x48\x33\x51\x35\x80\x0c\xa9\x42\x1e\x11\ -\xe0\x83\xc7\xc4\xc6\xe1\xe4\x2e\x40\x81\x0a\x24\x70\x00\x10\x08\ -\xb3\x64\x21\x73\xfd\x23\x01\x00\xf8\x7e\x3c\x3c\x2b\x22\xc0\x07\ -\xbe\x00\x01\x78\xd3\x0b\x08\x00\xc0\x4d\x9b\xc0\x30\x1c\x87\xff\ -\x0f\xea\x42\x99\x5c\x01\x80\x84\x01\xc0\x74\x91\x38\x4b\x08\x80\ -\x14\x00\x40\x7a\x8e\x42\xa6\x00\x40\x46\x01\x80\x9d\x98\x26\x53\ -\x00\xa0\x04\x00\x60\xcb\x63\x62\xe3\x00\x50\x2d\x00\x60\x27\x7f\ -\xe6\xd3\x00\x80\x9d\xf8\x99\x7b\x01\x00\x5b\x94\x21\x15\x01\xa0\ -\x91\x00\x20\x13\x65\x88\x44\x00\x68\x3b\x00\xac\xcf\x56\x8a\x45\ -\x00\x58\x30\x00\x14\x66\x4b\xc4\x39\x00\xd8\x2d\x00\x30\x49\x57\ -\x66\x48\x00\xb0\xb7\x00\xc0\xce\x10\x0b\xb2\x00\x08\x0c\x00\x30\ -\x51\x88\x85\x29\x00\x04\x7b\x00\x60\xc8\x23\x23\x78\x00\x84\x99\ -\x00\x14\x46\xf2\x57\x3c\xf1\x2b\xae\x10\xe7\x2a\x00\x00\x78\x99\ -\xb2\x3c\xb9\x24\x39\x45\x81\x5b\x08\x2d\x71\x07\x57\x57\x2e\x1e\ -\x28\xce\x49\x17\x2b\x14\x36\x61\x02\x61\x9a\x40\x2e\xc2\x79\x99\ -\x19\x32\x81\x34\x0f\xe0\xf3\xcc\x00\x00\xa0\x91\x15\x11\xe0\x83\ -\xf3\xfd\x78\xce\x0e\xae\xce\xce\x36\x8e\xb6\x0e\x5f\x2d\xea\xbf\ -\x06\xff\x22\x62\x62\xe3\xfe\xe5\xcf\xab\x70\x40\x00\x00\xe1\x74\ -\x7e\xd1\xfe\x2c\x2f\xb3\x1a\x80\x3b\x06\x80\x6d\xfe\xa2\x25\xee\ -\x04\x68\x5e\x0b\xa0\x75\xf7\x8b\x66\xb2\x0f\x40\xb5\x00\xa0\xe9\ -\xda\x57\xf3\x70\xf8\x7e\x3c\x3c\x45\xa1\x90\xb9\xd9\xd9\xe5\xe4\ -\xe4\xd8\x4a\xc4\x42\x5b\x61\xca\x57\x7d\xfe\x67\xc2\x5f\xc0\x57\ -\xfd\x6c\xf9\x7e\x3c\xfc\xf7\xf5\xe0\xbe\xe2\x24\x81\x32\x5d\x81\ -\x47\x04\xf8\xe0\xc2\xcc\xf4\x4c\xa5\x1c\xcf\x92\x09\x84\x62\xdc\ -\xe6\x8f\x47\xfc\xb7\x0b\xff\xfc\x1d\xd3\x22\xc4\x49\x62\xb9\x58\ -\x2a\x14\xe3\x51\x12\x71\x8e\x44\x9a\x8c\xf3\x32\xa5\x22\x89\x42\ -\x92\x29\xc5\x25\xd2\xff\x64\xe2\xdf\x2c\xfb\x03\x3e\xdf\x35\x00\ -\xb0\x6a\x3e\x01\x7b\x91\x2d\xa8\x5d\x63\x03\xf6\x4b\x27\x10\x58\ -\x74\xc0\xe2\xf7\x00\x00\xf2\xbb\x6f\xc1\xd4\x28\x08\x03\x80\x68\ -\x83\xe1\xcf\x77\xff\xef\x3f\xfd\x47\xa0\x25\x00\x80\x66\x49\x92\ -\x71\x00\x00\x5e\x44\x24\x2e\x54\xca\xb3\x3f\xc7\x08\x00\x00\x44\ -\xa0\x81\x2a\xb0\x41\x1b\xf4\xc1\x18\x2c\xc0\x06\x1c\xc1\x05\xdc\ -\xc1\x0b\xfc\x60\x36\x84\x42\x24\xc4\xc2\x42\x10\x42\x0a\x64\x80\ -\x1c\x72\x60\x29\xac\x82\x42\x28\x86\xcd\xb0\x1d\x2a\x60\x2f\xd4\ -\x40\x1d\x34\xc0\x51\x68\x86\x93\x70\x0e\x2e\xc2\x55\xb8\x0e\x3d\ -\x70\x0f\xfa\x61\x08\x9e\xc1\x28\xbc\x81\x09\x04\x41\xc8\x08\x13\ -\x61\x21\xda\x88\x01\x62\x8a\x58\x23\x8e\x08\x17\x99\x85\xf8\x21\ -\xc1\x48\x04\x12\x8b\x24\x20\xc9\x88\x14\x51\x22\x4b\x91\x35\x48\ -\x31\x52\x8a\x54\x20\x55\x48\x1d\xf2\x3d\x72\x02\x39\x87\x5c\x46\ -\xba\x91\x3b\xc8\x00\x32\x82\xfc\x86\xbc\x47\x31\x94\x81\xb2\x51\ -\x3d\xd4\x0c\xb5\x43\xb9\xa8\x37\x1a\x84\x46\xa2\x0b\xd0\x64\x74\ -\x31\x9a\x8f\x16\xa0\x9b\xd0\x72\xb4\x1a\x3d\x8c\x36\xa1\xe7\xd0\ -\xab\x68\x0f\xda\x8f\x3e\x43\xc7\x30\xc0\xe8\x18\x07\x33\xc4\x6c\ -\x30\x2e\xc6\xc3\x42\xb1\x38\x2c\x09\x93\x63\xcb\xb1\x22\xac\x0c\ -\xab\xc6\x1a\xb0\x56\xac\x03\xbb\x89\xf5\x63\xcf\xb1\x77\x04\x12\ -\x81\x45\xc0\x09\x36\x04\x77\x42\x20\x61\x1e\x41\x48\x58\x4c\x58\ -\x4e\xd8\x48\xa8\x20\x1c\x24\x34\x11\xda\x09\x37\x09\x03\x84\x51\ -\xc2\x27\x22\x93\xa8\x4b\xb4\x26\xba\x11\xf9\xc4\x18\x62\x32\x31\ -\x87\x58\x48\x2c\x23\xd6\x12\x8f\x13\x2f\x10\x7b\x88\x43\xc4\x37\ -\x24\x12\x89\x43\x32\x27\xb9\x90\x02\x49\xb1\xa4\x54\xd2\x12\xd2\ -\x46\xd2\x6e\x52\x23\xe9\x2c\xa9\x9b\x34\x48\x1a\x23\x93\xc9\xda\ -\x64\x6b\xb2\x07\x39\x94\x2c\x20\x2b\xc8\x85\xe4\x9d\xe4\xc3\xe4\ -\x33\xe4\x1b\xe4\x21\xf2\x5b\x0a\x9d\x62\x40\x71\xa4\xf8\x53\xe2\ -\x28\x52\xca\x6a\x4a\x19\xe5\x10\xe5\x34\xe5\x06\x65\x98\x32\x41\ -\x55\xa3\x9a\x52\xdd\xa8\xa1\x54\x11\x35\x8f\x5a\x42\xad\xa1\xb6\ -\x52\xaf\x51\x87\xa8\x13\x34\x75\x9a\x39\xcd\x83\x16\x49\x4b\xa5\ -\xad\xa2\x95\xd3\x1a\x68\x17\x68\xf7\x69\xaf\xe8\x74\xba\x11\xdd\ -\x95\x1e\x4e\x97\xd0\x57\xd2\xcb\xe9\x47\xe8\x97\xe8\x03\xf4\x77\ -\x0c\x0d\x86\x15\x83\xc7\x88\x67\x28\x19\x9b\x18\x07\x18\x67\x19\ -\x77\x18\xaf\x98\x4c\xa6\x19\xd3\x8b\x19\xc7\x54\x30\x37\x31\xeb\ -\x98\xe7\x99\x0f\x99\x6f\x55\x58\x2a\xb6\x2a\x7c\x15\x91\xca\x0a\ -\x95\x4a\x95\x26\x95\x1b\x2a\x2f\x54\xa9\xaa\xa6\xaa\xde\xaa\x0b\ -\x55\xf3\x55\xcb\x54\x8f\xa9\x5e\x53\x7d\xae\x46\x55\x33\x53\xe3\ -\xa9\x09\xd4\x96\xab\x55\xaa\x9d\x50\xeb\x53\x1b\x53\x67\xa9\x3b\ -\xa8\x87\xaa\x67\xa8\x6f\x54\x3f\xa4\x7e\x59\xfd\x89\x06\x59\xc3\ -\x4c\xc3\x4f\x43\xa4\x51\xa0\xb1\x5f\xe3\xbc\xc6\x20\x0b\x63\x19\ -\xb3\x78\x2c\x21\x6b\x0d\xab\x86\x75\x81\x35\xc4\x26\xb1\xcd\xd9\ -\x7c\x76\x2a\xbb\x98\xfd\x1d\xbb\x8b\x3d\xaa\xa9\xa1\x39\x43\x33\ -\x4a\x33\x57\xb3\x52\xf3\x94\x66\x3f\x07\xe3\x98\x71\xf8\x9c\x74\ -\x4e\x09\xe7\x28\xa7\x97\xf3\x7e\x8a\xde\x14\xef\x29\xe2\x29\x1b\ -\xa6\x34\x4c\xb9\x31\x65\x5c\x6b\xaa\x96\x97\x96\x58\xab\x48\xab\ -\x51\xab\x47\xeb\xbd\x36\xae\xed\xa7\x9d\xa6\xbd\x45\xbb\x59\xfb\ -\x81\x0e\x41\xc7\x4a\x27\x5c\x27\x47\x67\x8f\xce\x05\x9d\xe7\x53\ -\xd9\x53\xdd\xa7\x0a\xa7\x16\x4d\x3d\x3a\xf5\xae\x2e\xaa\x6b\xa5\ -\x1b\xa1\xbb\x44\x77\xbf\x6e\xa7\xee\x98\x9e\xbe\x5e\x80\x9e\x4c\ -\x6f\xa7\xde\x79\xbd\xe7\xfa\x1c\x7d\x2f\xfd\x54\xfd\x6d\xfa\xa7\ -\xf5\x47\x0c\x58\x06\xb3\x0c\x24\x06\xdb\x0c\xce\x18\x3c\xc5\x35\ -\x71\x6f\x3c\x1d\x2f\xc7\xdb\xf1\x51\x43\x5d\xc3\x40\x43\xa5\x61\ -\x95\x61\x97\xe1\x84\x91\xb9\xd1\x3c\xa3\xd5\x46\x8d\x46\x0f\x8c\ -\x69\xc6\x5c\xe3\x24\xe3\x6d\xc6\x6d\xc6\xa3\x26\x06\x26\x21\x26\ -\x4b\x4d\xea\x4d\xee\x9a\x52\x4d\xb9\xa6\x29\xa6\x3b\x4c\x3b\x4c\ -\xc7\xcd\xcc\xcd\xa2\xcd\xd6\x99\x35\x9b\x3d\x31\xd7\x32\xe7\x9b\ -\xe7\x9b\xd7\x9b\xdf\xb7\x60\x5a\x78\x5a\x2c\xb6\xa8\xb6\xb8\x65\ -\x49\xb2\xe4\x5a\xa6\x59\xee\xb6\xbc\x6e\x85\x5a\x39\x59\xa5\x58\ -\x55\x5a\x5d\xb3\x46\xad\x9d\xad\x25\xd6\xbb\xad\xbb\xa7\x11\xa7\ -\xb9\x4e\x93\x4e\xab\x9e\xd6\x67\xc3\xb0\xf1\xb6\xc9\xb6\xa9\xb7\ -\x19\xb0\xe5\xd8\x06\xdb\xae\xb6\x6d\xb6\x7d\x61\x67\x62\x17\x67\ -\xb7\xc5\xae\xc3\xee\x93\xbd\x93\x7d\xba\x7d\x8d\xfd\x3d\x07\x0d\ -\x87\xd9\x0e\xab\x1d\x5a\x1d\x7e\x73\xb4\x72\x14\x3a\x56\x3a\xde\ -\x9a\xce\x9c\xee\x3f\x7d\xc5\xf4\x96\xe9\x2f\x67\x58\xcf\x10\xcf\ -\xd8\x33\xe3\xb6\x13\xcb\x29\xc4\x69\x9d\x53\x9b\xd3\x47\x67\x17\ -\x67\xb9\x73\x83\xf3\x88\x8b\x89\x4b\x82\xcb\x2e\x97\x3e\x2e\x9b\ -\x1b\xc6\xdd\xc8\xbd\xe4\x4a\x74\xf5\x71\x5d\xe1\x7a\xd2\xf5\x9d\ -\x9b\xb3\x9b\xc2\xed\xa8\xdb\xaf\xee\x36\xee\x69\xee\x87\xdc\x9f\ -\xcc\x34\x9f\x29\x9e\x59\x33\x73\xd0\xc3\xc8\x43\xe0\x51\xe5\xd1\ -\x3f\x0b\x9f\x95\x30\x6b\xdf\xac\x7e\x4f\x43\x4f\x81\x67\xb5\xe7\ -\x23\x2f\x63\x2f\x91\x57\xad\xd7\xb0\xb7\xa5\x77\xaa\xf7\x61\xef\ -\x17\x3e\xf6\x3e\x72\x9f\xe3\x3e\xe3\x3c\x37\xde\x32\xde\x59\x5f\ -\xcc\x37\xc0\xb7\xc8\xb7\xcb\x4f\xc3\x6f\x9e\x5f\x85\xdf\x43\x7f\ -\x23\xff\x64\xff\x7a\xff\xd1\x00\xa7\x80\x25\x01\x67\x03\x89\x81\ -\x41\x81\x5b\x02\xfb\xf8\x7a\x7c\x21\xbf\x8e\x3f\x3a\xdb\x65\xf6\ -\xb2\xd9\xed\x41\x8c\xa0\xb9\x41\x15\x41\x8f\x82\xad\x82\xe5\xc1\ -\xad\x21\x68\xc8\xec\x90\xad\x21\xf7\xe7\x98\xce\x91\xce\x69\x0e\ -\x85\x50\x7e\xe8\xd6\xd0\x07\x61\xe6\x61\x8b\xc3\x7e\x0c\x27\x85\ -\x87\x85\x57\x86\x3f\x8e\x70\x88\x58\x1a\xd1\x31\x97\x35\x77\xd1\ -\xdc\x43\x73\xdf\x44\xfa\x44\x96\x44\xde\x9b\x67\x31\x4f\x39\xaf\ -\x2d\x4a\x35\x2a\x3e\xaa\x2e\x6a\x3c\xda\x37\xba\x34\xba\x3f\xc6\ -\x2e\x66\x59\xcc\xd5\x58\x9d\x58\x49\x6c\x4b\x1c\x39\x2e\x2a\xae\ -\x36\x6e\x6c\xbe\xdf\xfc\xed\xf3\x87\xe2\x9d\xe2\x0b\xe3\x7b\x17\ -\x98\x2f\xc8\x5d\x70\x79\xa1\xce\xc2\xf4\x85\xa7\x16\xa9\x2e\x12\ -\x2c\x3a\x96\x40\x4c\x88\x4e\x38\x94\xf0\x41\x10\x2a\xa8\x16\x8c\ -\x25\xf2\x13\x77\x25\x8e\x0a\x79\xc2\x1d\xc2\x67\x22\x2f\xd1\x36\ -\xd1\x88\xd8\x43\x5c\x2a\x1e\x4e\xf2\x48\x2a\x4d\x7a\x92\xec\x91\ -\xbc\x35\x79\x24\xc5\x33\xa5\x2c\xe5\xb9\x84\x27\xa9\x90\xbc\x4c\ -\x0d\x4c\xdd\x9b\x3a\x9e\x16\x9a\x76\x20\x6d\x32\x3d\x3a\xbd\x31\ -\x83\x92\x91\x90\x71\x42\xaa\x21\x4d\x93\xb6\x67\xea\x67\xe6\x66\ -\x76\xcb\xac\x65\x85\xb2\xfe\xc5\x6e\x8b\xb7\x2f\x1e\x95\x07\xc9\ -\x6b\xb3\x90\xac\x05\x59\x2d\x0a\xb6\x42\xa6\xe8\x54\x5a\x28\xd7\ -\x2a\x07\xb2\x67\x65\x57\x66\xbf\xcd\x89\xca\x39\x96\xab\x9e\x2b\ -\xcd\xed\xcc\xb3\xca\xdb\x90\x37\x9c\xef\x9f\xff\xed\x12\xc2\x12\ -\xe1\x92\xb6\xa5\x86\x4b\x57\x2d\x1d\x58\xe6\xbd\xac\x6a\x39\xb2\ -\x3c\x71\x79\xdb\x0a\xe3\x15\x05\x2b\x86\x56\x06\xac\x3c\xb8\x8a\ -\xb6\x2a\x6d\xd5\x4f\xab\xed\x57\x97\xae\x7e\xbd\x26\x7a\x4d\x6b\ -\x81\x5e\xc1\xca\x82\xc1\xb5\x01\x6b\xeb\x0b\x55\x0a\xe5\x85\x7d\ -\xeb\xdc\xd7\xed\x5d\x4f\x58\x2f\x59\xdf\xb5\x61\xfa\x86\x9d\x1b\ -\x3e\x15\x89\x8a\xae\x14\xdb\x17\x97\x15\x7f\xd8\x28\xdc\x78\xe5\ -\x1b\x87\x6f\xca\xbf\x99\xdc\x94\xb4\xa9\xab\xc4\xb9\x64\xcf\x66\ -\xd2\x66\xe9\xe6\xde\x2d\x9e\x5b\x0e\x96\xaa\x97\xe6\x97\x0e\x6e\ -\x0d\xd9\xda\xb4\x0d\xdf\x56\xb4\xed\xf5\xf6\x45\xdb\x2f\x97\xcd\ -\x28\xdb\xbb\x83\xb6\x43\xb9\xa3\xbf\x3c\xb8\xbc\x65\xa7\xc9\xce\ -\xcd\x3b\x3f\x54\xa4\x54\xf4\x54\xfa\x54\x36\xee\xd2\xdd\xb5\x61\ -\xd7\xf8\x6e\xd1\xee\x1b\x7b\xbc\xf6\x34\xec\xd5\xdb\x5b\xbc\xf7\ -\xfd\x3e\xc9\xbe\xdb\x55\x01\x55\x4d\xd5\x66\xd5\x65\xfb\x49\xfb\ -\xb3\xf7\x3f\xae\x89\xaa\xe9\xf8\x96\xfb\x6d\x5d\xad\x4e\x6d\x71\ -\xed\xc7\x03\xd2\x03\xfd\x07\x23\x0e\xb6\xd7\xb9\xd4\xd5\x1d\xd2\ -\x3d\x54\x52\x8f\xd6\x2b\xeb\x47\x0e\xc7\x1f\xbe\xfe\x9d\xef\x77\ -\x2d\x0d\x36\x0d\x55\x8d\x9c\xc6\xe2\x23\x70\x44\x79\xe4\xe9\xf7\ -\x09\xdf\xf7\x1e\x0d\x3a\xda\x76\x8c\x7b\xac\xe1\x07\xd3\x1f\x76\ -\x1d\x67\x1d\x2f\x6a\x42\x9a\xf2\x9a\x46\x9b\x53\x9a\xfb\x5b\x62\ -\x5b\xba\x4f\xcc\x3e\xd1\xd6\xea\xde\x7a\xfc\x47\xdb\x1f\x0f\x9c\ -\x34\x3c\x59\x79\x4a\xf3\x54\xc9\x69\xda\xe9\x82\xd3\x93\x67\xf2\ -\xcf\x8c\x9d\x95\x9d\x7d\x7e\x2e\xf9\xdc\x60\xdb\xa2\xb6\x7b\xe7\ -\x63\xce\xdf\x6a\x0f\x6f\xef\xba\x10\x74\xe1\xd2\x45\xff\x8b\xe7\ -\x3b\xbc\x3b\xce\x5c\xf2\xb8\x74\xf2\xb2\xdb\xe5\x13\x57\xb8\x57\ -\x9a\xaf\x3a\x5f\x6d\xea\x74\xea\x3c\xfe\x93\xd3\x4f\xc7\xbb\x9c\ -\xbb\x9a\xae\xb9\x5c\x6b\xb9\xee\x7a\xbd\xb5\x7b\x66\xf7\xe9\x1b\ -\x9e\x37\xce\xdd\xf4\xbd\x79\xf1\x16\xff\xd6\xd5\x9e\x39\x3d\xdd\ -\xbd\xf3\x7a\x6f\xf7\xc5\xf7\xf5\xdf\x16\xdd\x7e\x72\x27\xfd\xce\ -\xcb\xbb\xd9\x77\x27\xee\xad\xbc\x4f\xbc\x5f\xf4\x40\xed\x41\xd9\ -\x43\xdd\x87\xd5\x3f\x5b\xfe\xdc\xd8\xef\xdc\x7f\x6a\xc0\x77\xa0\ -\xf3\xd1\xdc\x47\xf7\x06\x85\x83\xcf\xfe\x91\xf5\x8f\x0f\x43\x05\ -\x8f\x99\x8f\xcb\x86\x0d\x86\xeb\x9e\x38\x3e\x39\x39\xe2\x3f\x72\ -\xfd\xe9\xfc\xa7\x43\xcf\x64\xcf\x26\x9e\x17\xfe\xa2\xfe\xcb\xae\ -\x17\x16\x2f\x7e\xf8\xd5\xeb\xd7\xce\xd1\x98\xd1\xa1\x97\xf2\x97\ -\x93\xbf\x6d\x7c\xa5\xfd\xea\xc0\xeb\x19\xaf\xdb\xc6\xc2\xc6\x1e\ -\xbe\xc9\x78\x33\x31\x5e\xf4\x56\xfb\xed\xc1\x77\xdc\x77\x1d\xef\ -\xa3\xdf\x0f\x4f\xe4\x7c\x20\x7f\x28\xff\x68\xf9\xb1\xf5\x53\xd0\ -\xa7\xfb\x93\x19\x93\x93\xff\x04\x03\x98\xf3\xfc\x63\x33\x2d\xdb\ -\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\x25\x00\x00\x80\x83\ -\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00\x75\x30\x00\x00\xea\x60\ -\x00\x00\x3a\x98\x00\x00\x17\x6f\x92\x5f\xc5\x46\x00\x00\x0c\x7b\ -\x49\x44\x41\x54\x78\xda\xec\xdd\x5b\x6c\xd4\x55\x02\xc7\xf1\xdf\ -\x74\xee\x33\x5c\x86\xd6\x34\x81\x8c\x98\x5e\xd4\xd4\xaa\x54\xdb\ -\x05\xd2\x58\x62\x25\x1b\x21\x2b\x98\xac\x84\xb0\x66\xab\xae\xc9\ -\xee\x1a\x62\xb2\x64\xb3\x3e\xe8\x83\xc9\xf2\x20\xd9\x68\x8c\x24\ -\xae\x9a\xdd\x07\xc4\xcb\x66\xd7\x60\xb2\xca\xee\x56\xa2\x02\x01\ -\x43\x50\x41\x51\xac\x44\x43\x5b\x02\x0d\x93\x6d\xb6\x30\x40\xa7\ -\x73\xeb\x74\xf6\x61\x98\xff\xdc\xaf\x2d\x9d\x52\xbe\x9f\x84\x84\ -\xd2\x99\xff\xe5\x9c\x33\x13\x7e\xe7\x9c\xff\x39\xa6\xce\xce\xce\ -\xb8\x00\x00\xd3\x16\x0c\x06\x29\x04\x00\x00\x00\x54\xad\x8e\x22\ -\x00\x00\x00\x00\x00\x80\x80\x0e\x00\x00\x00\x00\x00\x08\xe8\x00\ -\x00\x00\x00\x00\x10\xd0\x01\x00\x00\x00\x00\x00\x01\x1d\x00\x00\ -\x00\x00\x00\x02\x3a\x00\x00\x00\x00\x00\x20\xa0\x03\x00\x00\x00\ -\x00\x40\x40\x07\x00\x00\x00\x00\x00\x04\x74\x00\x00\x00\x00\x00\ -\x08\xe8\x00\x00\x00\x00\x00\x80\x80\x0e\x00\x00\x00\x00\x00\x01\ -\x1d\x00\x00\x00\x00\x00\x10\xd0\x01\x00\x00\x00\x00\x20\xa0\x03\ -\x00\x00\x00\x00\x00\x02\x3a\x00\x00\x00\x00\x00\x04\x74\x00\x00\ -\x00\x00\x00\x40\x40\x07\x00\x00\x00\x00\x80\x80\x0e\x00\x00\x00\ -\x00\x00\x08\xe8\x00\x00\x00\x00\x00\x10\xd0\x01\x00\x00\x00\x00\ -\x00\x01\x1d\x00\x00\x00\x00\x00\x02\x3a\x00\x00\x00\x00\x00\x20\ -\xa0\x03\x00\x00\x00\x00\x40\x40\x07\x00\x00\x00\x00\x00\x04\x74\ -\x00\x00\x00\x00\x00\x08\xe8\x00\x00\x00\x00\x00\xa0\x0a\x96\x6b\ -\x76\x60\x6f\x87\x4c\x8e\x85\x15\xbd\x27\x1e\xba\xa2\xc9\x91\x13\ -\xc6\xcf\xe6\x86\x26\xd5\x2d\xf1\x4a\x92\x26\xcf\x0f\x28\x3e\x71\ -\x21\xe3\xf5\x26\x57\xbd\x2c\xcb\xda\x8d\x9f\xa3\xa7\x0f\x97\x75\ -\x9e\xf4\xe3\x66\x9f\xb3\x9a\xeb\x2f\x74\x8c\x72\x98\x5c\xf5\xb2\ -\x36\xad\x92\xa5\xb1\x49\x92\x34\x75\x79\x54\x93\xff\x3d\x5d\xd6\ -\xf1\xac\xad\x3d\x65\x9f\x67\xea\xe2\x88\x62\x63\xc3\x65\x5f\x53\ -\x7a\xb9\xce\xd4\xbd\x26\xcb\xbd\xd4\x31\xd2\xcf\x5f\xce\xf9\xd2\ -\xeb\x2a\x5f\x3b\x99\x89\x36\x9a\x7d\x1d\xc5\xca\xa8\x92\xb2\x4e\ -\xd6\x61\xf6\x7b\x2a\xa9\xdb\x72\xce\x9b\xaf\x9d\x45\xcf\x1c\x2f\ -\x79\x9d\xc5\xee\x73\xba\xed\x01\x00\x00\x00\xc0\x2c\x05\x74\xf7\ -\x9a\x27\x64\x69\x6c\xae\xe8\x3d\x93\xa3\x43\xba\xf4\xb7\x6d\xc6\ -\xcf\xf6\xbb\xd7\xcb\xb9\x62\x9d\x24\xe9\xf2\xbf\x5e\xcc\x09\xe0\ -\x96\x65\xed\x5a\xf4\xd0\x33\xc6\xcf\x97\xf6\x94\x17\x16\x5c\xbd\ -\xbf\x91\xcd\xdb\x9e\xf7\x9c\xd5\x5c\x7f\xa1\x63\x94\xbe\x8e\xad\ -\x72\xdc\x71\xbf\x4c\x56\x47\xde\x63\x06\x0e\xbd\x59\xf4\x7e\xd2\ -\xef\xbd\x94\xe0\x37\x1f\x69\xe2\xc0\x6b\xe5\x35\x8a\xac\x72\xcd\ -\x09\x65\xd1\x90\x22\x67\xbf\x53\xf0\xc8\xdb\x65\x07\x51\x49\x5a\ -\xf4\xf3\x3f\xaa\xce\xed\x51\x3c\x1a\xd2\x85\x3f\x6f\x2e\x7c\xfc\ -\x89\x0b\x5a\xd0\xfb\x6b\xd5\xb9\x3d\x92\x24\xff\xdb\xbf\x2b\x1a\ -\x3c\x17\x3d\xfc\x9c\x4c\x56\x87\xe2\xd1\x90\x2e\xee\x7a\xea\x9a\ -\xb4\xd1\xec\x3a\x2e\xb7\x8c\x02\x9f\xbe\x5a\xb4\xc3\x20\x79\x8c\ -\xec\xfa\xa9\xa4\x6e\x8b\xd5\xb1\xc9\x55\x2f\x57\xcf\x93\x72\xb4\ -\xad\xc9\x7d\x43\xcf\x63\x9a\x1c\x1d\xd2\xf8\xbe\x9d\x05\xcb\xb7\ -\xd4\x7d\x56\x72\xaf\x00\x00\x00\x00\x6a\x14\xd0\x27\xc7\x46\x72\ -\xc3\x94\xcd\x29\xb3\x67\xa9\x24\x29\xe6\xf7\x29\x1e\x09\x96\x7c\ -\x4f\x25\x1c\xf7\x6c\xd4\x78\x89\x80\x6e\x6e\x68\x32\xc2\x79\x39\ -\xe2\xd1\x90\x62\x17\xcf\x57\x7c\xaf\xa5\x2c\xdc\xb4\x23\xe3\x3a\ -\x26\x47\x87\x24\x49\x75\xee\x7a\xd5\xb9\x3d\xb2\x34\x36\x6b\xd1\ -\xc3\xcf\xe9\xf2\x07\x2f\x94\xec\x74\x98\x0a\xf8\x35\x15\x28\x1e\ -\x8c\xa6\x2e\x8f\x56\x55\xa6\xe9\xf5\x94\xac\x3f\x93\xd5\x21\x7b\ -\x4b\x97\x6c\xcb\xef\xd4\xf8\xc1\x5d\x8a\x0c\xf4\x97\x6e\x68\xde\ -\x0e\x23\x70\x9b\xac\x0e\x39\x3a\x37\x29\x74\x7c\x4f\xe1\xb0\xf9\ -\xd5\x87\x72\xf7\x3c\x26\x49\x72\x76\xf7\x69\x7c\xef\xf6\xbc\xaf\ -\x73\xae\xda\x62\x74\x70\x84\xbe\x3f\x58\x55\x40\x9c\x6e\x1d\x17\ -\x2f\xa3\x37\xca\xaa\xc3\x7c\x1d\x02\xd9\x92\x6d\xa3\xd0\xef\xb3\ -\xeb\xd8\xe4\xaa\xd7\xe2\xcd\x3b\x8c\xcf\x5c\xfa\x7d\x9a\x97\x2c\ -\x93\xc9\xea\x90\xa5\xb1\x59\x8b\xb7\xfc\xa9\xac\x6b\xcc\xfe\xcc\ -\x26\xaf\x27\x79\xaf\x96\x86\x1d\xba\xf4\xde\xb3\x84\x74\x00\x00\ -\x00\x60\xae\x05\xf4\xc0\xbe\x97\x72\xfe\xcd\xda\xda\x63\x8c\xc6\ -\x05\x3e\x7b\xa7\xec\x29\xe9\xe5\xb2\x2d\xbf\xb3\xf4\x6b\x6e\x5f\ -\x53\x59\x40\xbd\x78\xbe\xaa\xd1\xf1\x62\x5c\xbd\x5b\x8d\x70\x1e\ -\x19\x19\xd0\xf8\x7f\x5e\xcc\x08\x35\x8e\xce\x4d\x72\xad\xde\x2c\ -\x93\xd5\xa1\x45\x0f\x3f\xa7\x8b\xbb\x9e\x2a\x1a\x7a\xc2\xa7\x8f\ -\x96\x3d\x3a\x5e\x71\x3d\x66\xd5\x93\xc9\x55\x2f\x47\xc7\x06\x39\ -\xef\xf9\x99\x4c\x56\x87\x16\xdc\xff\x2b\x5d\xbe\xe4\x2b\x19\xee\ -\x1c\xf7\x6c\xcc\x6c\x0b\x4d\x9d\x45\x03\x7a\xe8\xf8\x1e\x39\xef\ -\xdd\xa8\x3a\xb7\x47\xf6\x96\x2e\x05\x1b\x9a\x72\x46\x79\x4d\xae\ -\x7a\x39\xee\xb8\xdf\xe8\xa4\xa8\xb6\x0c\xa6\x5b\xc7\xf9\xca\xc8\ -\xb9\x6a\x8b\x9c\x2b\xd6\x19\x75\x58\x69\x48\xcf\x77\x3d\xae\xde\ -\xad\xc6\x8c\x92\x72\xae\xd7\xbd\xf6\x69\x23\x9c\x87\x07\x8f\xe5\ -\x8c\x70\x3b\xbb\x1f\x97\x6b\xe5\x23\x32\x59\x1d\x5a\xb8\xfe\xf7\ -\xf2\xbf\xbb\xad\x68\x3b\xcb\xf7\x99\x35\xb9\xea\xe5\x5e\xfb\xb4\ -\xec\x2d\x5d\x32\x7b\x96\xca\xd5\xf3\x64\xde\xcf\x3e\x00\x00\x00\ -\x80\xd2\xe6\xd5\x22\x71\xc9\x91\xd9\xa2\x41\xb1\x7d\x6d\x6d\xaf\ -\xd1\x55\x6f\x84\xac\x98\xdf\xa7\x2b\x7b\x72\x47\x1c\x43\xc7\xf7\ -\x68\xfc\xe0\x2e\xe3\x9e\xdc\x6b\x9f\x9e\x33\x65\x1c\x9f\xb8\xa0\ -\xe0\x91\xdd\xba\xfc\xc1\x0b\xa9\xeb\x5b\xf3\x44\xc9\x7b\x4e\x76\ -\x9e\xc4\xfc\xbe\x44\x47\x89\xb7\x5d\xe6\x86\xa6\xa2\xef\x1b\x3f\ -\xf0\xd7\xb4\x30\xd9\x97\xf3\xfb\xf4\xd1\xf3\xe0\x57\x1f\xce\xa9\ -\x32\x9a\x38\xf0\x9a\x02\x87\xdf\x2a\xbb\x8c\x66\x9a\xc5\xdb\x21\ -\x7b\x4b\x97\xa4\xab\x9d\x40\x7b\xb7\xe7\xb4\xb3\xe0\x91\xdd\x9a\ -\xf8\xe2\xfd\xc4\x17\x81\xdb\x23\xe7\xaa\x2d\x55\xb6\x87\xb7\x53\ -\x1d\x2f\x4b\x6f\xe5\x5b\x15\x00\x00\x00\xb8\xd1\x03\x7a\x64\x64\ -\x40\x92\x64\xbf\xfd\xbe\x82\xaf\xb1\xb6\xf6\x18\x53\x84\x93\xaf\ -\x9f\x6d\xf6\xb6\x07\x8c\xbf\x4f\x7c\xf9\xcf\xc2\xf7\x33\xd0\x6f\ -\x4c\x63\x2e\x67\x66\xc0\x6c\x9b\x1c\x39\xa1\xf0\xe0\xb1\x44\x18\ -\x6c\x6c\x96\xc5\xdb\x51\xf4\x9e\x93\x41\x7a\xfc\x93\xd7\x15\x8f\ -\x86\x12\xf7\x55\x62\x36\x43\xf4\xf4\x61\xa3\x0c\xec\x2d\x5d\x19\ -\x81\xde\xdc\xd0\x64\x8c\x9e\xc7\xfc\xbe\xa2\xa3\xf1\xb5\x12\x3a\ -\xbe\x27\xa3\x8c\x4a\x75\x48\xcc\x24\xdb\xad\xdd\xa9\x20\x7e\xf4\ -\x1f\x05\x5f\x17\x3c\xb2\x5b\x53\x01\x7f\xe2\x3d\xb7\xac\xa8\xea\ -\x5c\xb1\xb1\x61\xa3\x9e\x92\x23\xf6\x00\x00\x00\x00\x6e\xe0\x80\ -\x1e\x3e\x75\xa8\x64\x10\xb2\xb7\xf5\x1a\x81\x2e\x36\x76\xae\x26\ -\xd7\x69\x59\x76\x47\x46\x08\x2f\x1a\x50\x7d\x3f\x4a\x4a\x8c\xc0\ -\x16\x0b\xc0\x35\x0b\xa0\x5f\x7f\x98\x37\x10\x66\x73\xdc\xf5\xd3\ -\xc4\xfd\x8e\x0c\x68\x72\xe4\x84\x22\x67\xbf\x4b\xfc\x7b\x19\xb3\ -\x19\x92\x23\xbc\x52\xe6\x28\xba\xb3\xbb\xcf\x08\xfd\x81\xcf\xde\ -\x99\xc3\xed\xf2\x40\xaa\x8c\x2a\x7c\xbc\x62\x3a\xac\x4b\x6f\x93\ -\x94\x98\xfa\x5f\x6a\x6a\x7d\xe4\xec\xb7\x33\x16\xae\xf3\x3d\x1b\ -\x0f\x00\x00\x00\xe0\x06\x0b\xe8\xf1\xf0\xb8\x31\x12\xe8\xe8\x7a\ -\x24\xe7\xf7\x26\x57\xbd\x31\xe5\x37\x74\xf2\x63\x23\xc0\xcc\x36\ -\xf3\xc2\x7a\x23\x38\x95\x12\x3d\x77\x32\x75\xfd\x15\x6e\x59\x37\ -\x1b\xca\x79\xa6\xda\xdc\xd0\x64\x04\xbf\xe8\xf0\xf1\x44\x20\x1c\ -\xfa\x32\xd1\xf8\xdc\x9e\x92\xdb\x89\x45\x4f\x1f\x4e\xcd\x8e\xb8\ -\x3a\x8a\x6e\x6e\x68\x32\xea\x72\x72\x74\x68\xc6\xd7\x32\x98\x49\ -\x19\xcf\xa6\xdb\xdd\xb3\x7e\xfe\x52\x8b\x07\x4a\x52\x3c\x32\x91\ -\x51\x5f\x95\xb2\x78\x3b\x8c\xd5\xf0\x23\x67\xbe\xe6\x5b\x15\x00\ -\x00\x00\xa8\x92\x65\x3e\xdd\x4c\xf8\xf4\x51\x39\x57\xac\x93\xbd\ -\x75\xa5\x02\xfb\x32\x7f\xe7\xe8\xd8\x90\x08\x23\xd1\x90\xc2\xa7\ -\xf6\x17\x9d\x0a\x9f\x51\x40\x8d\xcd\x6a\xd8\x56\xf8\xf9\xe6\x4a\ -\xb6\x2f\xab\x34\x38\x95\xcb\xba\xf4\x36\xb9\x7a\xb7\x16\x2e\x97\ -\x6f\xfb\x2b\xda\x0e\x6d\x26\x25\x3b\x4b\xe2\xd1\x90\x31\x0d\x3d\ -\x32\xd0\xaf\xa9\xee\x5f\x24\x16\x80\x6b\xeb\x2d\x19\xb0\x27\x0e\ -\xfc\x45\xb6\xbe\x9d\x92\x72\x9f\x45\x4f\x1f\x61\xaf\xfa\x43\x70\ -\x8d\xea\xf8\x7a\x54\xb7\xc4\x5b\xb0\xad\xd8\xdb\x7a\x65\xbd\xf9\ -\xae\xcc\xd7\x2f\xb8\xc9\x78\x04\x63\x72\x74\x48\xa1\x13\x7b\xf9\ -\x56\x05\x00\x00\x00\x08\xe8\x89\x20\x9a\x5c\x39\xdb\xd6\xbe\x3e\ -\x63\x0a\x79\x72\x3a\x75\xe4\xec\x77\xf3\x6e\x1b\x28\x4b\x63\x73\ -\xd1\xfd\xbc\xa3\xe7\x4e\xd6\x2c\xa0\xdb\x5b\x57\x1a\xe5\x9e\x2e\ -\x72\xf6\x5b\x39\xda\xd6\xc8\xb6\xfc\x4e\x99\x5c\xf5\x45\xeb\x24\ -\x36\x36\xac\xf0\xe0\x31\xd9\x5b\xba\x8c\x91\x73\x29\xb1\x32\xf9\ -\x5c\x1e\x3d\x9f\x6f\xd2\xcb\x3e\x5b\xf6\x3e\xf1\x00\x00\x00\x00\ -\x6e\xf0\x80\x1e\x1b\x1b\x56\x64\x64\x40\x36\x6f\xbb\xec\x6d\x6b\ -\x8c\x80\x9e\xbe\x07\x77\xfa\x73\xd3\x65\x1d\xd3\xef\x2b\xfa\x8c\ -\xf3\xd4\xc5\x91\x9a\xdf\xf7\xe4\xe8\x90\xf1\xbc\xfa\x5c\xba\x46\ -\x5b\xfb\x7a\xe3\x39\xf1\xf4\x67\xb1\x25\x29\x3c\xf0\x89\x1c\x6d\ -\x6b\x12\x7b\x68\xb7\x3d\x50\x72\x91\xb7\xc0\xa7\xaf\xca\xb6\xfc\ -\x0d\xe3\x78\x92\x32\x56\x0f\x9f\x56\xbb\xb9\x0e\xea\x78\x4e\x7c\ -\xbe\xb2\xf6\x41\x4f\xee\xf9\x2e\x25\x3a\x89\x16\x6c\x78\x3e\x67\ -\x2b\x37\x00\x00\x00\x00\x37\x68\x40\x4f\x04\xc1\x43\xb2\x79\xdb\ -\x8d\x6d\xbc\x62\x63\xc3\xc6\x1e\xdc\x31\xbf\xaf\xa2\xbd\xa8\x25\ -\x29\x1e\x09\x5e\x93\x51\x5a\xf3\x92\x65\x33\x76\xac\xa8\xef\xc7\ -\x39\x39\x05\xdb\xde\x96\x5a\x14\xcd\x7a\xf3\x5d\x39\xd3\xa3\xe3\ -\xd1\x50\x22\xa0\xdf\x7e\x5f\xc9\x80\x1e\x9f\xb8\xa0\xd0\xf7\x07\ -\x8d\x2d\xea\x42\xa7\x0e\xcd\xd8\xac\x80\x6b\x55\xc7\x52\x62\xed\ -\x83\xeb\xc9\xe4\xf9\xc2\xbb\x1b\xe4\xdb\x07\xdd\xdc\xd0\x24\x67\ -\x77\x9f\x31\xbb\x21\x1e\x61\x1f\x74\x00\x00\x00\xa0\x5a\x75\xf3\ -\xed\x86\x22\x03\xfd\xc6\x02\x6c\xf6\xbb\xd7\x67\xec\xc1\x1d\x3a\ -\xf9\x71\xed\x03\xd0\x58\x62\x34\xd6\x64\x75\x94\x0c\x6f\xe9\x81\ -\x76\x2e\x8e\xe2\xa6\x2f\xf0\x16\x0f\x07\x72\x82\x9b\xcd\xdb\x6e\ -\xfc\xec\x5c\xb1\x2e\xe7\x4f\x72\x34\xbc\xdc\x2d\xc8\xd2\x17\xcd\ -\x8b\x0c\x7e\x7e\x5d\xb4\x47\x6b\xd3\xaa\x54\x1d\x5e\x1e\x9d\xf5\ -\xf3\x97\xd3\x11\x54\xb7\xe0\xa6\x54\x3d\x56\x38\xfa\x1d\x1b\x1b\ -\xce\x98\xc9\x60\x69\xf0\xf2\xad\x0a\x00\x00\x00\x10\xd0\x53\xc2\ -\xa7\x8f\x26\x02\x7a\xeb\x6a\x63\x0f\xee\xe4\xe2\x70\xb5\x16\xfb\ -\xdf\x19\xe3\xef\xe9\x7b\xa2\xe7\x63\x6f\x5d\x9d\x08\x4d\xd1\x50\ -\xcd\x9e\x21\x2f\x7a\x7d\x57\xb7\xad\x93\xa4\xc8\x0f\x87\x32\x7f\ -\x77\xf7\xfa\x54\xa7\xc4\xe8\x50\xc1\x3f\xf9\x5e\x3f\x9f\x38\x57\ -\x3c\x98\xea\x60\x38\x73\x7c\xd6\xce\x9b\x5c\x4d\xdd\x64\x75\x14\ -\x5d\x29\x3f\xbd\x03\x2b\xe6\xf7\x55\xd7\xa6\xc7\x86\x8d\xf7\x16\ -\x5b\x0b\x01\x00\x00\x00\x40\x71\x96\xf9\x78\x53\xc9\xc5\xe2\xea\ -\xdc\x1e\xb9\x7b\x1e\x4b\x04\x96\x39\xb2\x38\x5c\xf8\xd4\x7e\xb9\ -\x56\x6f\x96\xc9\xea\x90\xf3\xde\x8d\x0a\x9f\xda\x9f\xf7\xba\x9c\ -\xdd\x8f\xa7\x9e\x9b\xff\xfe\xe0\x9c\x2b\x63\x47\xe7\xa6\x8c\xad\ -\xce\xb2\x3b\x10\x92\x9d\x0b\x31\xbf\xaf\xe8\xe2\x61\x8b\x1f\x7d\ -\x45\x96\xc6\x66\xd9\x5b\x57\xcf\xbb\x95\xd2\x1d\x9d\x9b\x8c\xc0\ -\x1a\x1e\x3c\x36\xab\x9d\x2c\x91\x1f\x0e\xc9\xb5\x32\xb1\x82\xbe\ -\xfb\xbe\x5f\xca\x5f\x60\x0a\xbf\xab\xe7\x49\x63\x26\xc3\x74\x66\ -\x98\xa4\x3f\x9b\x0e\x00\x00\x00\xa0\x3a\xf3\x72\x04\x3d\xb9\x58\ -\x5c\xba\x99\x5a\x50\x6c\xba\xe2\x13\x17\x14\xfc\xfa\xdf\x89\xc2\ -\x77\x7b\xb4\x78\xf3\x8e\x8c\xe9\xdd\x26\x57\xbd\x5c\xbd\x5b\x8d\ -\x70\x35\x15\xf0\x2b\xf8\xf9\xdf\xe7\x4c\xd9\x5a\xbc\x1d\x5a\xb0\ -\xe1\x79\xa3\xe3\x23\x1e\x0d\x69\x7c\xdf\xce\x8c\xd7\x58\x5b\x7b\ -\x52\x9d\x0b\x25\x42\x5f\xf8\x87\xcf\x8c\xb2\xb0\xb5\xcf\x8f\x51\ -\xf4\x7c\x65\x14\xf8\xf4\xd5\x59\xff\x0c\x84\x4e\x25\x66\x35\x98\ -\x3d\x4b\xb5\xf8\xd1\x57\x72\xda\x99\xfb\xc1\x3f\xc8\x71\x75\x9d\ -\x80\x98\xdf\x57\x72\x1d\x80\x62\xa6\x08\xe8\x00\x00\x00\xc0\xf4\ -\xb3\xc4\x7c\xbd\xb1\xe4\x62\x71\x52\xfe\x11\xde\xb2\x0b\xa8\xc4\ -\x1e\xd9\xc9\xe3\x57\xb2\xc5\x54\xf0\xc8\x6e\x99\x1b\x6e\x91\xbd\ -\xa5\x4b\x66\xcf\x52\x79\xfa\x76\x6a\x2a\xe0\x57\x3c\x1a\x34\x56\ -\xc5\x4e\x06\xbb\x2b\xfd\x2f\xd7\x74\xe4\x7f\xd1\x43\xcf\x48\x7a\ -\x26\x7f\x28\x0b\xf8\x75\xa5\xff\xe5\xdc\xd1\xf3\xb4\xa9\xef\xa5\ -\x1e\x2b\x08\x9f\xda\x6f\x04\x59\x5b\xf3\x4f\x32\xb6\xc6\x9b\xb5\ -\x0f\xc1\x34\xeb\xb8\x58\x19\xc5\xfc\x3e\x8d\x7f\xf2\x7a\x4d\xea\ -\x30\xb0\xef\x25\x99\x6c\x2e\xd9\x5b\xba\x64\x69\x6c\x96\xa7\x6f\ -\xa7\x31\x15\x3d\xbd\x9d\xc5\xfc\x3e\x5d\x7a\xef\xd9\x69\x76\x08\ -\x9c\x93\xae\x7e\xde\x92\x8b\x33\x02\x00\x00\x00\xa8\x4c\xdd\x7c\ -\xbd\xb1\xf4\xc5\xe2\x92\xa3\xb4\x73\xc9\xf8\xde\xed\x0a\x1c\x7e\ -\xcb\xb8\xc6\x3a\xb7\x27\x23\x34\x85\x07\x8f\xe9\xe2\xae\xa7\x2a\ -\x5e\x75\x7e\x36\xc4\xfc\x3e\x4d\x7c\xf1\xbe\xfc\xef\x6e\xcb\xb9\ -\x3e\x93\xab\xde\x98\xfa\x1e\x1e\x3c\x56\x32\x98\xc6\x27\x2e\x28\ -\x3c\x78\x2c\x11\xec\x5b\xba\xae\xbb\x55\xcf\x8b\x95\x51\xe0\xf0\ -\x5b\xf2\xbf\xf9\xdb\x9a\xd6\xe1\xf8\xde\xed\x0a\x7e\xf3\x91\xe2\ -\xd1\x90\x11\xcc\x93\xed\x2c\x1e\x0d\x29\xf8\xcd\x47\xba\xf4\xde\ -\xb3\xd3\xee\x40\x48\x5f\xc0\xcf\xd9\xdd\xc7\x37\x2b\x00\x00\x00\ -\x50\x05\x53\x67\x67\x67\x9c\x62\xa8\x2d\x8b\xb7\x43\x26\xc7\xc2\ -\x54\xd8\xb9\x46\x5b\x7e\x81\x76\x46\x3b\xbb\xb6\x82\x41\xa6\xfa\ -\x03\x00\x00\x60\x1a\xff\x67\xa7\x08\x6a\x6f\x2e\x8e\x92\x83\x76\ -\x06\x00\x00\x00\x60\x76\xd5\x51\x04\x00\x00\x00\x00\x00\x10\xd0\ -\x01\x00\x00\x00\x00\x00\x01\x1d\x00\x00\x00\x00\x00\x02\x3a\x00\ -\x00\x00\x00\x00\x20\xa0\x03\x00\x00\x00\x00\x40\x40\x07\x00\x00\ -\x00\x00\x00\x04\x74\x00\x00\x00\x00\x00\x08\xe8\x00\x00\x00\x00\ -\x00\x80\x80\x0e\x00\x00\x00\x00\x00\x01\x1d\x00\x00\x00\x00\x00\ -\x10\xd0\x01\x00\x00\x00\x00\x20\xa0\x03\x00\x00\x00\x00\x00\x02\ -\x3a\x00\x00\x00\x00\x00\x04\x74\x00\x00\x00\x00\x00\x40\x40\x07\ -\x00\x00\x00\x00\x80\x80\x0e\x00\x00\x00\x00\x00\x08\xe8\x00\x00\ -\x00\x00\x00\x10\xd0\x01\x00\x00\x00\x00\x00\x01\x1d\x00\x00\x00\ -\x00\x00\x02\x3a\x00\x00\x00\x00\x00\x20\xa0\x03\x00\x00\x00\x00\ -\x70\xfd\xf8\x3f\x00\x00\x00\xff\xff\x03\x00\xce\x28\x15\x65\x5d\ -\xb8\xdc\xe1\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x03\xe6\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ -\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ -\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ -\x01\x00\x9a\x9c\x18\x00\x00\x03\x88\x49\x44\x41\x54\x58\x85\xed\ -\x56\x5d\x68\x1c\x55\x18\x3d\xf7\xde\xd9\x9f\xce\x76\xb3\xdd\x24\ -\x33\x93\x61\x63\x36\xee\x66\x5b\x34\x6d\xa5\xb6\x14\x6c\x63\x53\ -\xf5\xc1\x12\x29\x11\x4b\xfd\x09\xd2\x4a\xa1\xc5\x8a\x21\xd8\x8a\ -\xd0\x22\x12\x84\x3c\x28\xe2\x83\x22\x14\x14\x8a\xc1\x9f\x22\x82\ -\x60\x41\x2a\x45\x50\x11\xed\x4b\x11\x0c\xfd\xa1\x4d\x76\x77\x6a\ -\xcb\xa6\xd9\x8d\xe9\xd6\x64\xb3\xc9\xce\xdc\xcf\x87\xb0\x76\x50\ -\xb3\x3f\xb0\x3e\x88\x39\x6f\x73\xbe\xef\x9e\x73\xf8\xf8\xe6\x72\ -\x81\x15\xac\xe0\x3f\x08\x16\x8d\x46\xfd\x8d\x12\x53\xea\x69\x8e\ -\x6b\xda\x36\x26\x30\x24\x17\x0b\x37\x00\x1c\x69\x44\x00\x5e\xb3\ -\xb9\xd9\x7a\x50\x28\xfc\x33\xbf\x57\xac\xe3\x84\x9f\xfe\xb1\xc7\ -\xd0\x06\x63\x86\xa1\x37\x3c\x40\x5c\xd7\xfb\x3d\x42\x39\x7e\xfa\ -\xcc\x0b\xc2\x66\x08\x90\x83\x9f\xff\x6e\xde\xb2\x93\x08\x87\x89\ -\xd9\xaf\x36\x34\x40\x2c\x1c\x0e\x11\xa3\xf7\x3e\x1c\xdd\x5f\x8a\ -\x44\x42\x6d\xf6\x22\xa9\x4c\x88\xbc\x5b\x23\x6e\x68\x2f\x82\x8b\ -\x4f\x5e\x7f\xe3\xcd\xdb\x3e\x9f\xbf\x2f\x6e\x68\xc3\x31\x23\x50\ -\xd3\x24\xaa\x4f\xc0\xab\x1c\xba\x7f\xd3\x5d\x13\xdd\x1b\xcd\x04\ -\x00\xf8\xbc\x6c\xb6\xc4\x4a\x2d\xe5\x72\x54\xd3\x74\x62\xf2\x60\ -\x47\x67\x74\x62\xef\xc0\xc0\x96\xc1\xa1\xa3\x19\x87\x51\x0f\x97\ -\xea\x8e\x5a\x02\x54\x5d\x42\x46\x78\xea\xa5\x57\x1e\x5a\x5d\xfe\ -\x6e\xef\x08\x67\xc7\xaf\xe6\xb6\x02\xb8\x0c\x00\x56\x36\x3b\x19\ -\x8d\x86\x7a\xd3\xc9\xd4\xc9\x5d\x3d\xdb\xcf\x59\xbf\xa6\x22\x4c\ -\x8a\xfe\xf1\xec\xd4\x58\x2d\x01\x2a\x4e\xa0\x17\x50\x48\x20\xd1\ -\x7d\xaf\xb9\xb6\xcc\x3d\xfd\xcc\x56\x05\xa0\x67\xdd\x7d\x96\x95\ -\xbf\xe5\xf8\x4a\x07\xd2\xe9\xf4\x1a\x10\x3b\x9e\x9a\xaa\xcd\xbc\ -\x6a\x80\x8c\x69\xae\xe1\xe0\xb3\x8a\xa2\xb0\x32\xd7\xff\xf8\xfa\ -\x2d\x1e\xaf\x27\x16\xd3\xf5\x47\xdc\xbd\xd7\xae\xe5\x67\x98\xed\ -\x6c\x4f\xde\xcc\x9d\xaa\xd5\xbc\x6a\x80\xdf\x81\x02\x24\xa9\x6e\ -\xce\xeb\xf5\x88\x77\xdf\x79\x72\x1e\x02\xaf\xad\x0b\x85\x3a\xdd\ -\xb5\xe4\xcc\x4c\x1e\x75\xa2\xf2\x04\x32\x99\x02\x91\xcc\x5b\xd6\ -\xad\xeb\x6e\xfe\x81\x9e\xbb\xd7\x1f\x3b\xb6\x8b\x97\x54\xcf\xa7\ -\x71\x53\x7b\xb4\x5e\x53\x37\x44\xb5\x86\x70\x40\x4d\x2c\x14\x4b\ -\x62\xe7\xc3\x6b\x3b\xdd\xfc\x86\x0d\x66\xc7\xe6\x4d\xd1\xfc\xd9\ -\xaf\x2f\xec\x0e\xfa\x56\xed\x69\x6e\x52\x3d\x2d\x4d\x2a\x05\xd5\ -\xa0\x5f\x0f\xa9\x9d\x21\x55\xed\x6b\x0e\xa8\x07\x36\xce\x15\xce\ -\x5a\x80\x5c\x4e\x9f\x2d\x57\x28\x23\xd1\xd6\x7c\x8f\x64\xca\x37\ -\xdf\xff\x70\x24\x10\x0a\xf9\x9b\xfe\x5a\x2f\x95\x48\x7e\x75\x66\ -\xec\xfc\xa9\x8f\xcf\x17\x53\xe9\x6c\xb3\x53\x94\xaa\xe2\xe7\x05\ -\xe9\x60\x61\xbe\xb8\x78\x21\x99\xc9\xed\xab\xa4\xbf\x6c\x80\x5e\ -\xd7\x2f\x7a\xbd\xad\x75\xa4\x3d\x12\x7e\xf0\xf3\x2f\x9f\xdf\x2c\ -\x20\x05\x17\x8c\xbb\x17\x13\x00\x6c\xdb\x26\xe9\x90\x04\x80\x54\ -\x7a\xc6\xda\xfb\xc4\x07\x3e\xee\xc8\xdd\x91\x5c\x6e\x0c\x00\xbe\ -\x03\xec\x9a\x03\xc4\xf4\xd6\x21\x70\x1a\x59\x6a\x60\x3f\x32\x22\ -\xe9\x80\xcd\x2f\x55\xc9\xdf\xa6\x07\x6f\x7c\x71\xfa\x70\x7a\xff\ -\xc0\xc9\xfb\x98\x87\xcb\xd1\xd1\x7d\xbf\x3c\xd6\x77\x22\x3e\x3d\ -\x3d\xdb\xee\x38\xac\xa8\x00\x0c\x9c\x4a\xc4\xb0\x1a\xa0\x6d\x4b\ -\xe7\xc4\xcb\xc9\xc9\xa9\x13\x35\x4f\xa0\x12\x4c\xd3\x54\x3d\xb6\ -\xfd\xb6\xc2\x31\xe7\x30\xf2\x0a\x4e\x7e\xc7\x33\x3d\x68\x59\x28\ -\xd6\xab\x55\x31\x40\xdc\x30\xba\x25\x93\x3b\x18\x23\xdf\x9f\xa4\ -\x03\x02\x9c\x04\x13\xca\xf4\x44\x26\x3b\x0c\x00\x31\x43\x7b\x0b\ -\x44\x7e\x00\x57\x20\xee\x68\x12\xb1\x05\x05\xce\xb7\x57\x27\x7f\ -\xbb\xb4\x9c\x47\xc5\xab\x98\x98\xbd\x8a\x18\x6f\x22\xe2\xde\x32\ -\xc7\x39\xc0\x25\x9d\x1b\xcf\x64\x3f\x2a\x73\xc9\x9b\xd9\xa3\x09\ -\xbd\xf5\x90\xc3\x58\x07\x93\x77\x36\x9e\x38\x9f\x27\x46\x0d\x7b\ -\xbc\xfc\x2b\xa8\x69\x07\xe2\x86\x36\x0c\xa2\xe7\xea\x52\xe6\x78\ -\x7f\x62\x32\x37\xd2\x90\x00\x5d\x5d\x5d\x3e\x3e\x37\x17\xac\xc7\ -\x5f\x09\x67\x6e\x5f\xbc\x88\xc5\x7a\xce\xac\x60\x05\xff\x4f\xfc\ -\x01\xa0\xdd\x38\x94\xa3\x9e\x46\x4d\x00\x00\x00\x00\x49\x45\x4e\ -\x44\xae\x42\x60\x82\ -\x00\x00\x03\x6b\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ -\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ -\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ -\x01\x00\x9a\x9c\x18\x00\x00\x03\x0d\x49\x44\x41\x54\x58\x85\xed\ -\x96\x6f\x48\x13\x71\x18\xc7\x9f\xbb\x6d\x77\xbb\xbb\xdd\x36\x37\ -\xb7\xdb\xf9\x1f\x9d\x6d\x5d\x26\x65\x89\x15\x92\x26\x24\x1a\xea\ -\x0b\x49\x05\x5f\x28\x91\x98\x41\x6f\x82\xe8\x4d\xbd\xea\x65\x84\ -\xa0\x44\x68\xf9\x22\xb2\x08\x7c\x11\x41\xf8\x46\x51\x22\x03\x2b\ -\x41\xca\x3f\x24\xfe\x29\x06\xba\x9b\x4d\x45\xa7\xde\xfe\xdd\xee\ -\x7a\xa5\xcc\xbf\xdb\x42\xa2\x60\x9f\x77\xf7\x7b\xbe\xf7\x3c\x1f\ -\xee\x39\xb8\x03\x88\x13\x27\xce\xff\x08\xc7\x01\x76\x54\xbd\xd0\ -\x58\xc2\xd9\x0c\x53\x90\x65\x31\xc9\xe2\xaa\xb9\xff\xaf\x0b\x70\ -\x19\x6c\xb3\x46\x8b\x0f\xe0\xb8\x52\x0a\x85\xa4\x27\xfb\x0a\x26\ -\x9b\x6f\x1c\x63\xd9\xc4\x23\x17\xb0\x26\x31\x95\x14\x89\xb5\xf6\ -\xbc\xb9\x4e\xe9\xf5\xe4\xba\x1c\x82\xd1\xdd\x99\x4c\xd6\x54\x28\ -\x85\xe4\x0e\x0c\xc7\x1f\x1c\xa9\x80\xd5\x60\xd0\xe2\x6a\xc5\x8b\ -\x8e\x67\xf5\x14\xcb\xea\xc0\xbb\x19\x50\xa0\x4a\xe5\x6a\x58\x04\ -\xb1\x27\xb3\xcd\x24\xa9\x79\xd7\xd6\xf1\x14\xb4\x09\xba\x06\x5b\ -\x0a\x3b\x90\x91\x64\xb4\x1d\x89\x00\x46\xa9\x6e\x96\x97\x72\xb8\ -\xcd\xc6\x00\x00\x00\xa9\xc5\xc5\x20\x12\x34\x6e\xd5\xd3\x4d\x26\ -\x26\x18\x12\x3b\xf3\x0b\x0a\x34\xe5\x95\x55\x70\xf7\xde\x7d\x4a\ -\x14\xc5\x12\x54\x42\x1b\xa2\x11\x50\x46\x0a\xa8\x09\xbc\xa9\xb6\ -\xfe\x0c\xb1\x75\x9d\x73\x82\x41\x5c\xce\xb5\x7c\x00\x98\x02\x00\ -\x70\xb8\xdd\x2e\x7b\x52\x52\xe2\xe8\x97\x91\x9e\xc6\xba\x9a\x8b\ -\x63\xa3\xa3\x6b\x80\xc8\xa5\x3f\xf8\xa5\x3d\x6b\xda\x8f\x43\x9f\ -\x40\x11\x80\xd2\xeb\xf5\xa7\xda\xed\x96\xed\xb3\x2b\x15\xb9\x3a\ -\x8a\xc6\x5b\xc2\x73\x53\x4e\xe7\xb2\x5f\xf0\xd6\x0e\x7f\x1c\x52\ -\xfa\xfc\xbe\xdb\x73\x51\x0e\x8f\x28\xc0\xb3\xac\x9e\x24\xb0\x20\ -\x8a\x22\xdb\x67\xc5\xc5\xd9\x40\x12\xaa\x93\x59\x8c\xf1\xd2\x6e\ -\x09\x05\xa2\x34\x4d\x2f\xb8\xba\xa3\x1d\x1e\x51\x60\x1d\x40\xf0\ -\x07\xc4\x1d\x6b\x52\x28\x50\x78\xf8\xa8\x9a\x06\x04\x1d\xb4\x9a\ -\xcd\xe7\xc2\x6b\xd3\x3c\xbf\x14\xcb\xf0\x88\x02\x3c\xcf\x0b\x08\ -\x0a\x82\xcb\xe5\xd9\x71\x9e\x97\x97\x0a\x65\x65\x9c\x24\xa3\xf2\ -\x70\x26\x63\x2c\x89\x75\x68\x38\x8a\x48\x01\x8b\x51\xcf\xe9\x74\ -\x78\xee\xa9\xd3\xa9\x48\xf8\xf9\xe5\xd2\xe3\xc8\x85\xf3\x99\xf0\ -\xf9\xd3\xcf\x6a\x83\x56\x53\x45\xaa\x70\xd1\x40\xd1\x62\x62\x02\ -\x8e\xe9\x09\x6d\x7a\x02\x4d\x96\x9b\xf4\x54\x67\x8e\x47\x78\xee\ -\x00\x90\x0e\xea\x8f\x1c\x54\xd8\xc2\x6a\x31\x70\x14\x4d\x8d\xf4\ -\xf5\xdf\x22\x29\x0d\xbe\xa7\x2e\x49\x32\x0c\x7d\x98\x85\xde\xde\ -\x09\xcf\xf7\x09\x67\xc8\x23\xf8\x15\x34\xa1\x96\xdc\xcb\x1b\x5a\ -\x9f\x37\x88\xce\xb9\xdc\x87\xce\x88\x28\x00\x00\x90\x6b\x4d\x7e\ -\x9c\x77\x36\xfd\x5a\x7b\x7b\x0d\x19\xfe\x42\x1e\xc4\xfc\xfc\x2a\ -\xd4\x5d\xed\xda\x14\x04\x7f\xe1\x8c\xd3\xfd\xf5\xb0\x6c\xc4\x15\ -\x00\x00\xd8\x57\xd6\xfb\x1c\x3e\xb9\x64\xfc\xdb\x82\xb9\xa8\x38\ -\x1b\x53\xa9\x0e\xbe\x6d\x72\x92\x87\xa6\xc6\xee\x0d\xc1\xeb\x6f\ -\x99\x5e\x70\x47\xfc\x68\x45\x25\xe0\x00\x90\x6c\x2b\x9e\x57\x33\ -\x02\x58\x5e\xbf\x1c\xe1\x50\x14\x51\x30\x16\x1a\xa5\x69\x35\x00\ -\x00\x84\x44\x09\xc6\xc7\x9c\xd0\xd6\x36\xb8\xd9\xde\xfa\x7e\x7d\ -\xcd\x13\xa8\x9d\xe5\x7f\xbd\x8d\xa6\x77\x54\x2b\x08\xc7\x6a\x31\ -\x70\x04\x45\xdc\x91\x25\xa8\x90\xc4\x90\x16\xc3\x94\xa2\xe0\x0b\ -\xa8\x08\x8d\x6a\xce\x2b\x04\xbb\xd4\x01\xa4\x73\x6c\x71\x71\x33\ -\xd6\xbe\x7f\x44\x4a\x4a\x0a\x91\x96\xa6\x4b\x80\x18\xff\x2b\xe2\ -\xc4\x89\xf3\x4f\xf1\x1b\x22\xca\x00\x6f\xd0\x10\xcf\x18\x00\x00\ -\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x0f\x88\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x40\x00\x00\x00\x40\x08\x06\x00\x00\x00\xaa\x69\x71\xde\ -\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ -\x01\x00\x9a\x9c\x18\x00\x00\x0a\x4f\x69\x43\x43\x50\x50\x68\x6f\ -\x74\x6f\x73\x68\x6f\x70\x20\x49\x43\x43\x20\x70\x72\x6f\x66\x69\ -\x6c\x65\x00\x00\x78\xda\x9d\x53\x67\x54\x53\xe9\x16\x3d\xf7\xde\ -\xf4\x42\x4b\x88\x80\x94\x4b\x6f\x52\x15\x08\x20\x52\x42\x8b\x80\ -\x14\x91\x26\x2a\x21\x09\x10\x4a\x88\x21\xa1\xd9\x15\x51\xc1\x11\ -\x45\x45\x04\x1b\xc8\xa0\x88\x03\x8e\x8e\x80\x8c\x15\x51\x2c\x0c\ -\x8a\x0a\xd8\x07\xe4\x21\xa2\x8e\x83\xa3\x88\x8a\xca\xfb\xe1\x7b\ -\xa3\x6b\xd6\xbc\xf7\xe6\xcd\xfe\xb5\xd7\x3e\xe7\xac\xf3\x9d\xb3\ -\xcf\x07\xc0\x08\x0c\x96\x48\x33\x51\x35\x80\x0c\xa9\x42\x1e\x11\ -\xe0\x83\xc7\xc4\xc6\xe1\xe4\x2e\x40\x81\x0a\x24\x70\x00\x10\x08\ -\xb3\x64\x21\x73\xfd\x23\x01\x00\xf8\x7e\x3c\x3c\x2b\x22\xc0\x07\ -\xbe\x00\x01\x78\xd3\x0b\x08\x00\xc0\x4d\x9b\xc0\x30\x1c\x87\xff\ -\x0f\xea\x42\x99\x5c\x01\x80\x84\x01\xc0\x74\x91\x38\x4b\x08\x80\ -\x14\x00\x40\x7a\x8e\x42\xa6\x00\x40\x46\x01\x80\x9d\x98\x26\x53\ -\x00\xa0\x04\x00\x60\xcb\x63\x62\xe3\x00\x50\x2d\x00\x60\x27\x7f\ -\xe6\xd3\x00\x80\x9d\xf8\x99\x7b\x01\x00\x5b\x94\x21\x15\x01\xa0\ -\x91\x00\x20\x13\x65\x88\x44\x00\x68\x3b\x00\xac\xcf\x56\x8a\x45\ -\x00\x58\x30\x00\x14\x66\x4b\xc4\x39\x00\xd8\x2d\x00\x30\x49\x57\ -\x66\x48\x00\xb0\xb7\x00\xc0\xce\x10\x0b\xb2\x00\x08\x0c\x00\x30\ -\x51\x88\x85\x29\x00\x04\x7b\x00\x60\xc8\x23\x23\x78\x00\x84\x99\ -\x00\x14\x46\xf2\x57\x3c\xf1\x2b\xae\x10\xe7\x2a\x00\x00\x78\x99\ -\xb2\x3c\xb9\x24\x39\x45\x81\x5b\x08\x2d\x71\x07\x57\x57\x2e\x1e\ -\x28\xce\x49\x17\x2b\x14\x36\x61\x02\x61\x9a\x40\x2e\xc2\x79\x99\ -\x19\x32\x81\x34\x0f\xe0\xf3\xcc\x00\x00\xa0\x91\x15\x11\xe0\x83\ -\xf3\xfd\x78\xce\x0e\xae\xce\xce\x36\x8e\xb6\x0e\x5f\x2d\xea\xbf\ -\x06\xff\x22\x62\x62\xe3\xfe\xe5\xcf\xab\x70\x40\x00\x00\xe1\x74\ -\x7e\xd1\xfe\x2c\x2f\xb3\x1a\x80\x3b\x06\x80\x6d\xfe\xa2\x25\xee\ -\x04\x68\x5e\x0b\xa0\x75\xf7\x8b\x66\xb2\x0f\x40\xb5\x00\xa0\xe9\ -\xda\x57\xf3\x70\xf8\x7e\x3c\x3c\x45\xa1\x90\xb9\xd9\xd9\xe5\xe4\ -\xe4\xd8\x4a\xc4\x42\x5b\x61\xca\x57\x7d\xfe\x67\xc2\x5f\xc0\x57\ -\xfd\x6c\xf9\x7e\x3c\xfc\xf7\xf5\xe0\xbe\xe2\x24\x81\x32\x5d\x81\ -\x47\x04\xf8\xe0\xc2\xcc\xf4\x4c\xa5\x1c\xcf\x92\x09\x84\x62\xdc\ -\xe6\x8f\x47\xfc\xb7\x0b\xff\xfc\x1d\xd3\x22\xc4\x49\x62\xb9\x58\ -\x2a\x14\xe3\x51\x12\x71\x8e\x44\x9a\x8c\xf3\x32\xa5\x22\x89\x42\ -\x92\x29\xc5\x25\xd2\xff\x64\xe2\xdf\x2c\xfb\x03\x3e\xdf\x35\x00\ -\xb0\x6a\x3e\x01\x7b\x91\x2d\xa8\x5d\x63\x03\xf6\x4b\x27\x10\x58\ -\x74\xc0\xe2\xf7\x00\x00\xf2\xbb\x6f\xc1\xd4\x28\x08\x03\x80\x68\ -\x83\xe1\xcf\x77\xff\xef\x3f\xfd\x47\xa0\x25\x00\x80\x66\x49\x92\ -\x71\x00\x00\x5e\x44\x24\x2e\x54\xca\xb3\x3f\xc7\x08\x00\x00\x44\ -\xa0\x81\x2a\xb0\x41\x1b\xf4\xc1\x18\x2c\xc0\x06\x1c\xc1\x05\xdc\ -\xc1\x0b\xfc\x60\x36\x84\x42\x24\xc4\xc2\x42\x10\x42\x0a\x64\x80\ -\x1c\x72\x60\x29\xac\x82\x42\x28\x86\xcd\xb0\x1d\x2a\x60\x2f\xd4\ -\x40\x1d\x34\xc0\x51\x68\x86\x93\x70\x0e\x2e\xc2\x55\xb8\x0e\x3d\ -\x70\x0f\xfa\x61\x08\x9e\xc1\x28\xbc\x81\x09\x04\x41\xc8\x08\x13\ -\x61\x21\xda\x88\x01\x62\x8a\x58\x23\x8e\x08\x17\x99\x85\xf8\x21\ -\xc1\x48\x04\x12\x8b\x24\x20\xc9\x88\x14\x51\x22\x4b\x91\x35\x48\ -\x31\x52\x8a\x54\x20\x55\x48\x1d\xf2\x3d\x72\x02\x39\x87\x5c\x46\ -\xba\x91\x3b\xc8\x00\x32\x82\xfc\x86\xbc\x47\x31\x94\x81\xb2\x51\ -\x3d\xd4\x0c\xb5\x43\xb9\xa8\x37\x1a\x84\x46\xa2\x0b\xd0\x64\x74\ -\x31\x9a\x8f\x16\xa0\x9b\xd0\x72\xb4\x1a\x3d\x8c\x36\xa1\xe7\xd0\ -\xab\x68\x0f\xda\x8f\x3e\x43\xc7\x30\xc0\xe8\x18\x07\x33\xc4\x6c\ -\x30\x2e\xc6\xc3\x42\xb1\x38\x2c\x09\x93\x63\xcb\xb1\x22\xac\x0c\ -\xab\xc6\x1a\xb0\x56\xac\x03\xbb\x89\xf5\x63\xcf\xb1\x77\x04\x12\ -\x81\x45\xc0\x09\x36\x04\x77\x42\x20\x61\x1e\x41\x48\x58\x4c\x58\ -\x4e\xd8\x48\xa8\x20\x1c\x24\x34\x11\xda\x09\x37\x09\x03\x84\x51\ -\xc2\x27\x22\x93\xa8\x4b\xb4\x26\xba\x11\xf9\xc4\x18\x62\x32\x31\ -\x87\x58\x48\x2c\x23\xd6\x12\x8f\x13\x2f\x10\x7b\x88\x43\xc4\x37\ -\x24\x12\x89\x43\x32\x27\xb9\x90\x02\x49\xb1\xa4\x54\xd2\x12\xd2\ -\x46\xd2\x6e\x52\x23\xe9\x2c\xa9\x9b\x34\x48\x1a\x23\x93\xc9\xda\ -\x64\x6b\xb2\x07\x39\x94\x2c\x20\x2b\xc8\x85\xe4\x9d\xe4\xc3\xe4\ -\x33\xe4\x1b\xe4\x21\xf2\x5b\x0a\x9d\x62\x40\x71\xa4\xf8\x53\xe2\ -\x28\x52\xca\x6a\x4a\x19\xe5\x10\xe5\x34\xe5\x06\x65\x98\x32\x41\ -\x55\xa3\x9a\x52\xdd\xa8\xa1\x54\x11\x35\x8f\x5a\x42\xad\xa1\xb6\ -\x52\xaf\x51\x87\xa8\x13\x34\x75\x9a\x39\xcd\x83\x16\x49\x4b\xa5\ -\xad\xa2\x95\xd3\x1a\x68\x17\x68\xf7\x69\xaf\xe8\x74\xba\x11\xdd\ -\x95\x1e\x4e\x97\xd0\x57\xd2\xcb\xe9\x47\xe8\x97\xe8\x03\xf4\x77\ -\x0c\x0d\x86\x15\x83\xc7\x88\x67\x28\x19\x9b\x18\x07\x18\x67\x19\ -\x77\x18\xaf\x98\x4c\xa6\x19\xd3\x8b\x19\xc7\x54\x30\x37\x31\xeb\ -\x98\xe7\x99\x0f\x99\x6f\x55\x58\x2a\xb6\x2a\x7c\x15\x91\xca\x0a\ -\x95\x4a\x95\x26\x95\x1b\x2a\x2f\x54\xa9\xaa\xa6\xaa\xde\xaa\x0b\ -\x55\xf3\x55\xcb\x54\x8f\xa9\x5e\x53\x7d\xae\x46\x55\x33\x53\xe3\ -\xa9\x09\xd4\x96\xab\x55\xaa\x9d\x50\xeb\x53\x1b\x53\x67\xa9\x3b\ -\xa8\x87\xaa\x67\xa8\x6f\x54\x3f\xa4\x7e\x59\xfd\x89\x06\x59\xc3\ -\x4c\xc3\x4f\x43\xa4\x51\xa0\xb1\x5f\xe3\xbc\xc6\x20\x0b\x63\x19\ -\xb3\x78\x2c\x21\x6b\x0d\xab\x86\x75\x81\x35\xc4\x26\xb1\xcd\xd9\ -\x7c\x76\x2a\xbb\x98\xfd\x1d\xbb\x8b\x3d\xaa\xa9\xa1\x39\x43\x33\ -\x4a\x33\x57\xb3\x52\xf3\x94\x66\x3f\x07\xe3\x98\x71\xf8\x9c\x74\ -\x4e\x09\xe7\x28\xa7\x97\xf3\x7e\x8a\xde\x14\xef\x29\xe2\x29\x1b\ -\xa6\x34\x4c\xb9\x31\x65\x5c\x6b\xaa\x96\x97\x96\x58\xab\x48\xab\ -\x51\xab\x47\xeb\xbd\x36\xae\xed\xa7\x9d\xa6\xbd\x45\xbb\x59\xfb\ -\x81\x0e\x41\xc7\x4a\x27\x5c\x27\x47\x67\x8f\xce\x05\x9d\xe7\x53\ -\xd9\x53\xdd\xa7\x0a\xa7\x16\x4d\x3d\x3a\xf5\xae\x2e\xaa\x6b\xa5\ -\x1b\xa1\xbb\x44\x77\xbf\x6e\xa7\xee\x98\x9e\xbe\x5e\x80\x9e\x4c\ -\x6f\xa7\xde\x79\xbd\xe7\xfa\x1c\x7d\x2f\xfd\x54\xfd\x6d\xfa\xa7\ -\xf5\x47\x0c\x58\x06\xb3\x0c\x24\x06\xdb\x0c\xce\x18\x3c\xc5\x35\ -\x71\x6f\x3c\x1d\x2f\xc7\xdb\xf1\x51\x43\x5d\xc3\x40\x43\xa5\x61\ -\x95\x61\x97\xe1\x84\x91\xb9\xd1\x3c\xa3\xd5\x46\x8d\x46\x0f\x8c\ -\x69\xc6\x5c\xe3\x24\xe3\x6d\xc6\x6d\xc6\xa3\x26\x06\x26\x21\x26\ -\x4b\x4d\xea\x4d\xee\x9a\x52\x4d\xb9\xa6\x29\xa6\x3b\x4c\x3b\x4c\ -\xc7\xcd\xcc\xcd\xa2\xcd\xd6\x99\x35\x9b\x3d\x31\xd7\x32\xe7\x9b\ -\xe7\x9b\xd7\x9b\xdf\xb7\x60\x5a\x78\x5a\x2c\xb6\xa8\xb6\xb8\x65\ -\x49\xb2\xe4\x5a\xa6\x59\xee\xb6\xbc\x6e\x85\x5a\x39\x59\xa5\x58\ -\x55\x5a\x5d\xb3\x46\xad\x9d\xad\x25\xd6\xbb\xad\xbb\xa7\x11\xa7\ -\xb9\x4e\x93\x4e\xab\x9e\xd6\x67\xc3\xb0\xf1\xb6\xc9\xb6\xa9\xb7\ -\x19\xb0\xe5\xd8\x06\xdb\xae\xb6\x6d\xb6\x7d\x61\x67\x62\x17\x67\ -\xb7\xc5\xae\xc3\xee\x93\xbd\x93\x7d\xba\x7d\x8d\xfd\x3d\x07\x0d\ -\x87\xd9\x0e\xab\x1d\x5a\x1d\x7e\x73\xb4\x72\x14\x3a\x56\x3a\xde\ -\x9a\xce\x9c\xee\x3f\x7d\xc5\xf4\x96\xe9\x2f\x67\x58\xcf\x10\xcf\ -\xd8\x33\xe3\xb6\x13\xcb\x29\xc4\x69\x9d\x53\x9b\xd3\x47\x67\x17\ -\x67\xb9\x73\x83\xf3\x88\x8b\x89\x4b\x82\xcb\x2e\x97\x3e\x2e\x9b\ -\x1b\xc6\xdd\xc8\xbd\xe4\x4a\x74\xf5\x71\x5d\xe1\x7a\xd2\xf5\x9d\ -\x9b\xb3\x9b\xc2\xed\xa8\xdb\xaf\xee\x36\xee\x69\xee\x87\xdc\x9f\ -\xcc\x34\x9f\x29\x9e\x59\x33\x73\xd0\xc3\xc8\x43\xe0\x51\xe5\xd1\ -\x3f\x0b\x9f\x95\x30\x6b\xdf\xac\x7e\x4f\x43\x4f\x81\x67\xb5\xe7\ -\x23\x2f\x63\x2f\x91\x57\xad\xd7\xb0\xb7\xa5\x77\xaa\xf7\x61\xef\ -\x17\x3e\xf6\x3e\x72\x9f\xe3\x3e\xe3\x3c\x37\xde\x32\xde\x59\x5f\ -\xcc\x37\xc0\xb7\xc8\xb7\xcb\x4f\xc3\x6f\x9e\x5f\x85\xdf\x43\x7f\ -\x23\xff\x64\xff\x7a\xff\xd1\x00\xa7\x80\x25\x01\x67\x03\x89\x81\ -\x41\x81\x5b\x02\xfb\xf8\x7a\x7c\x21\xbf\x8e\x3f\x3a\xdb\x65\xf6\ -\xb2\xd9\xed\x41\x8c\xa0\xb9\x41\x15\x41\x8f\x82\xad\x82\xe5\xc1\ -\xad\x21\x68\xc8\xec\x90\xad\x21\xf7\xe7\x98\xce\x91\xce\x69\x0e\ -\x85\x50\x7e\xe8\xd6\xd0\x07\x61\xe6\x61\x8b\xc3\x7e\x0c\x27\x85\ -\x87\x85\x57\x86\x3f\x8e\x70\x88\x58\x1a\xd1\x31\x97\x35\x77\xd1\ -\xdc\x43\x73\xdf\x44\xfa\x44\x96\x44\xde\x9b\x67\x31\x4f\x39\xaf\ -\x2d\x4a\x35\x2a\x3e\xaa\x2e\x6a\x3c\xda\x37\xba\x34\xba\x3f\xc6\ -\x2e\x66\x59\xcc\xd5\x58\x9d\x58\x49\x6c\x4b\x1c\x39\x2e\x2a\xae\ -\x36\x6e\x6c\xbe\xdf\xfc\xed\xf3\x87\xe2\x9d\xe2\x0b\xe3\x7b\x17\ -\x98\x2f\xc8\x5d\x70\x79\xa1\xce\xc2\xf4\x85\xa7\x16\xa9\x2e\x12\ -\x2c\x3a\x96\x40\x4c\x88\x4e\x38\x94\xf0\x41\x10\x2a\xa8\x16\x8c\ -\x25\xf2\x13\x77\x25\x8e\x0a\x79\xc2\x1d\xc2\x67\x22\x2f\xd1\x36\ -\xd1\x88\xd8\x43\x5c\x2a\x1e\x4e\xf2\x48\x2a\x4d\x7a\x92\xec\x91\ -\xbc\x35\x79\x24\xc5\x33\xa5\x2c\xe5\xb9\x84\x27\xa9\x90\xbc\x4c\ -\x0d\x4c\xdd\x9b\x3a\x9e\x16\x9a\x76\x20\x6d\x32\x3d\x3a\xbd\x31\ -\x83\x92\x91\x90\x71\x42\xaa\x21\x4d\x93\xb6\x67\xea\x67\xe6\x66\ -\x76\xcb\xac\x65\x85\xb2\xfe\xc5\x6e\x8b\xb7\x2f\x1e\x95\x07\xc9\ -\x6b\xb3\x90\xac\x05\x59\x2d\x0a\xb6\x42\xa6\xe8\x54\x5a\x28\xd7\ -\x2a\x07\xb2\x67\x65\x57\x66\xbf\xcd\x89\xca\x39\x96\xab\x9e\x2b\ -\xcd\xed\xcc\xb3\xca\xdb\x90\x37\x9c\xef\x9f\xff\xed\x12\xc2\x12\ -\xe1\x92\xb6\xa5\x86\x4b\x57\x2d\x1d\x58\xe6\xbd\xac\x6a\x39\xb2\ -\x3c\x71\x79\xdb\x0a\xe3\x15\x05\x2b\x86\x56\x06\xac\x3c\xb8\x8a\ -\xb6\x2a\x6d\xd5\x4f\xab\xed\x57\x97\xae\x7e\xbd\x26\x7a\x4d\x6b\ -\x81\x5e\xc1\xca\x82\xc1\xb5\x01\x6b\xeb\x0b\x55\x0a\xe5\x85\x7d\ -\xeb\xdc\xd7\xed\x5d\x4f\x58\x2f\x59\xdf\xb5\x61\xfa\x86\x9d\x1b\ -\x3e\x15\x89\x8a\xae\x14\xdb\x17\x97\x15\x7f\xd8\x28\xdc\x78\xe5\ -\x1b\x87\x6f\xca\xbf\x99\xdc\x94\xb4\xa9\xab\xc4\xb9\x64\xcf\x66\ -\xd2\x66\xe9\xe6\xde\x2d\x9e\x5b\x0e\x96\xaa\x97\xe6\x97\x0e\x6e\ -\x0d\xd9\xda\xb4\x0d\xdf\x56\xb4\xed\xf5\xf6\x45\xdb\x2f\x97\xcd\ -\x28\xdb\xbb\x83\xb6\x43\xb9\xa3\xbf\x3c\xb8\xbc\x65\xa7\xc9\xce\ -\xcd\x3b\x3f\x54\xa4\x54\xf4\x54\xfa\x54\x36\xee\xd2\xdd\xb5\x61\ -\xd7\xf8\x6e\xd1\xee\x1b\x7b\xbc\xf6\x34\xec\xd5\xdb\x5b\xbc\xf7\ -\xfd\x3e\xc9\xbe\xdb\x55\x01\x55\x4d\xd5\x66\xd5\x65\xfb\x49\xfb\ -\xb3\xf7\x3f\xae\x89\xaa\xe9\xf8\x96\xfb\x6d\x5d\xad\x4e\x6d\x71\ -\xed\xc7\x03\xd2\x03\xfd\x07\x23\x0e\xb6\xd7\xb9\xd4\xd5\x1d\xd2\ -\x3d\x54\x52\x8f\xd6\x2b\xeb\x47\x0e\xc7\x1f\xbe\xfe\x9d\xef\x77\ -\x2d\x0d\x36\x0d\x55\x8d\x9c\xc6\xe2\x23\x70\x44\x79\xe4\xe9\xf7\ -\x09\xdf\xf7\x1e\x0d\x3a\xda\x76\x8c\x7b\xac\xe1\x07\xd3\x1f\x76\ -\x1d\x67\x1d\x2f\x6a\x42\x9a\xf2\x9a\x46\x9b\x53\x9a\xfb\x5b\x62\ -\x5b\xba\x4f\xcc\x3e\xd1\xd6\xea\xde\x7a\xfc\x47\xdb\x1f\x0f\x9c\ -\x34\x3c\x59\x79\x4a\xf3\x54\xc9\x69\xda\xe9\x82\xd3\x93\x67\xf2\ -\xcf\x8c\x9d\x95\x9d\x7d\x7e\x2e\xf9\xdc\x60\xdb\xa2\xb6\x7b\xe7\ -\x63\xce\xdf\x6a\x0f\x6f\xef\xba\x10\x74\xe1\xd2\x45\xff\x8b\xe7\ -\x3b\xbc\x3b\xce\x5c\xf2\xb8\x74\xf2\xb2\xdb\xe5\x13\x57\xb8\x57\ -\x9a\xaf\x3a\x5f\x6d\xea\x74\xea\x3c\xfe\x93\xd3\x4f\xc7\xbb\x9c\ -\xbb\x9a\xae\xb9\x5c\x6b\xb9\xee\x7a\xbd\xb5\x7b\x66\xf7\xe9\x1b\ -\x9e\x37\xce\xdd\xf4\xbd\x79\xf1\x16\xff\xd6\xd5\x9e\x39\x3d\xdd\ -\xbd\xf3\x7a\x6f\xf7\xc5\xf7\xf5\xdf\x16\xdd\x7e\x72\x27\xfd\xce\ -\xcb\xbb\xd9\x77\x27\xee\xad\xbc\x4f\xbc\x5f\xf4\x40\xed\x41\xd9\ -\x43\xdd\x87\xd5\x3f\x5b\xfe\xdc\xd8\xef\xdc\x7f\x6a\xc0\x77\xa0\ -\xf3\xd1\xdc\x47\xf7\x06\x85\x83\xcf\xfe\x91\xf5\x8f\x0f\x43\x05\ -\x8f\x99\x8f\xcb\x86\x0d\x86\xeb\x9e\x38\x3e\x39\x39\xe2\x3f\x72\ -\xfd\xe9\xfc\xa7\x43\xcf\x64\xcf\x26\x9e\x17\xfe\xa2\xfe\xcb\xae\ -\x17\x16\x2f\x7e\xf8\xd5\xeb\xd7\xce\xd1\x98\xd1\xa1\x97\xf2\x97\ -\x93\xbf\x6d\x7c\xa5\xfd\xea\xc0\xeb\x19\xaf\xdb\xc6\xc2\xc6\x1e\ -\xbe\xc9\x78\x33\x31\x5e\xf4\x56\xfb\xed\xc1\x77\xdc\x77\x1d\xef\ -\xa3\xdf\x0f\x4f\xe4\x7c\x20\x7f\x28\xff\x68\xf9\xb1\xf5\x53\xd0\ -\xa7\xfb\x93\x19\x93\x93\xff\x04\x03\x98\xf3\xfc\x63\x33\x2d\xdb\ -\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\x25\x00\x00\x80\x83\ -\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00\x75\x30\x00\x00\xea\x60\ -\x00\x00\x3a\x98\x00\x00\x17\x6f\x92\x5f\xc5\x46\x00\x00\x04\xb3\ -\x49\x44\x41\x54\x78\xda\xec\x9b\x5d\x4c\x5b\x65\x18\xc7\xff\x6f\ -\x4b\x19\x8e\xca\x12\xc0\x36\x2d\x6b\x9a\x8d\x2e\xbb\x11\x24\x54\ -\xbe\x12\x68\x71\x4b\xb6\x25\xf3\x6e\x8a\x5c\x98\x5d\x90\x6c\x6e\ -\x44\x2f\xd8\x00\xfb\xe1\x16\xa6\x59\x84\x69\x24\x21\x82\xcb\xa2\ -\x2e\x61\x12\x58\xa2\x89\x37\x66\x03\xa7\x6e\xf8\x31\x3e\xa6\x2b\ -\xf3\xaa\x4a\xa6\xb6\x7c\xa4\xf5\x60\x32\x53\x3e\xd2\x7a\xce\xf1\ -\x02\x06\x6b\x7b\xca\xd6\xd3\x73\x0e\xa7\xe9\x79\xae\xda\x73\xd2\ -\xf3\xbc\xef\xef\x3c\xef\xf3\x3e\x1f\x6f\x09\x4e\x7d\xcd\x22\x83\ -\x45\x85\x0c\x17\x05\x80\x02\x40\x01\xa0\x00\x50\x00\x6c\x85\x38\ -\x6c\x06\x50\x6e\x2b\x28\xb7\x15\x6d\x75\x86\xcc\x03\xd0\x5a\x67\ -\x84\x8a\x00\x2a\x02\xbc\x69\x33\x66\x1e\x00\x15\xe1\xfe\xac\xf8\ -\x00\x05\x80\x02\x20\x33\x00\x30\x2c\xf7\xe7\xb4\x00\xf0\x7a\x8d\ -\x1e\x01\x67\x39\x82\xae\x72\x9c\xdd\x57\xc4\x4b\xf1\x07\x3f\xcc\ -\x83\x61\x57\x27\xdf\x35\x3a\xc7\xeb\x19\x1d\xfb\x77\x22\xe8\x2a\ -\x47\xc0\x59\x8e\xe6\x2a\x3d\xaf\x67\x10\x3e\xe9\x70\xc0\x55\x0e\ -\xcd\x23\xae\xfb\xd2\x64\x10\x8e\x61\xbf\xa4\x6f\xae\xeb\xa0\x09\ -\xc7\x2a\x74\xeb\xdf\x23\x34\x0b\xfd\xbb\xbf\x48\x63\x01\xb1\xbb\ -\xd6\xf1\x0a\x1d\xce\xed\xdf\x29\xd9\xe4\xcf\x1f\x88\x9e\x3c\x00\ -\x10\x22\xe1\x12\xe8\x1b\x0f\xc6\x5d\x7b\xa3\x46\x0f\x87\x4d\xfc\ -\x88\xce\x65\x37\xe2\x64\xa5\x2e\xee\xfa\x87\xb7\x03\xd2\x01\xe8\ -\xf8\x66\x06\x97\x26\xe3\x21\x34\x57\xeb\x45\x07\x70\x92\x63\xad\ -\x7f\x34\x11\xc4\xdb\xdf\xcd\xf2\x7a\x5e\x16\xef\x58\x7e\xd8\x0f\ -\x02\x44\x99\xe2\x52\x84\xd9\x74\xd9\x1c\xab\xd0\xa1\xa1\x24\x1f\ -\x96\x82\x1c\x68\xb3\xd5\xeb\x11\x20\xc3\x02\xa1\x30\x8d\xe9\x85\ -\x15\x0c\xdd\xfb\x07\x9f\xdc\x09\x22\x91\x63\x5a\xfe\x8f\x41\x6e\ -\xf6\xc6\x7b\xbb\x38\x11\x84\x7b\x84\xbf\xff\x21\xa9\xd6\x04\xdd\ -\xf5\x46\xbc\x56\xa9\xc3\x72\x84\xc1\xd1\xcf\xef\x63\xdc\x1f\x8a\ -\xba\xaf\x26\x04\xdd\x87\xcd\x68\x28\xc9\x47\xb6\xfa\xc9\x16\x6a\ -\x98\x66\x71\xf5\xd7\x05\x9c\xfa\xca\x07\x9a\x8d\x1e\x5e\xb5\x49\ -\x8b\xfe\x97\x8a\x91\xa3\x21\xe8\x1b\x0f\xa2\xf3\xd6\x5c\x4a\x16\ -\x45\xc4\x2c\x8a\x1e\xb0\xec\xc0\xa7\x47\x76\x63\xbb\x86\x5f\xb8\ -\xb1\x14\x61\xd0\xf4\xc5\x7d\x8c\x4c\x3f\x48\xbf\x40\xa8\xad\xce\ -\x80\xa1\x46\x0b\xef\xc9\x03\xc0\x76\x8d\x0a\x43\x8d\x16\x51\xd3\ -\x65\x51\x00\x9c\xae\x35\xc0\x69\x17\x2e\xc5\x75\xda\x8d\x38\x5d\ -\x6b\x48\x0f\x00\x2f\xec\xce\x83\xbb\x5e\xf8\xfc\xde\x5d\x6f\x84\ -\x7d\x57\x9e\xbc\x01\x64\xa9\x08\xae\xbc\x5c\x2c\x9a\xb9\x0e\x34\ -\x14\x43\x4d\x88\x7c\x01\xf4\xbc\x68\x4e\x69\xcd\x3f\x89\x4f\xe8\ -\x3e\x6c\x96\x27\x00\x15\x01\x8e\x3c\x9b\x2f\x7a\x20\xf4\x4a\x49\ -\x3e\x88\x1c\x01\x34\x59\x75\x51\x09\x92\x58\xa2\x51\x13\x34\x59\ -\x9f\x91\x1f\x80\xc6\x52\xf1\xdf\xfe\x86\xae\x02\xf9\x01\xb0\x14\ -\xe4\x48\x06\x60\x4f\xa1\x70\xba\x12\xe6\x02\x0e\x9b\x61\xbd\x74\ -\x1d\x5b\xc9\xe9\x1a\x9d\xc3\x7b\xdf\xcf\x47\x5d\xd7\x66\xab\x25\ -\x03\xc0\xa5\x2b\xd9\xf1\x3e\xd6\x02\xb8\x1e\xf6\xd0\xd9\x71\xd5\ -\xf1\xa5\x2c\x6d\x73\xe9\x4a\x76\xbc\xa2\x87\xc2\xe9\x22\x09\x01\ -\x3c\xac\xd9\xc5\x4a\xa2\x1a\x9e\x94\x85\x4d\x2e\x5d\xc9\x8e\x57\ -\xf0\x6c\xf0\xcf\xb6\x32\xe4\x6d\x93\xc6\x0f\x3c\x58\xa1\xb1\xeb\ -\x7d\x8f\xbc\x76\x01\x2f\xb5\x22\x99\x05\x78\xa9\x65\xf9\x6d\x83\ -\x83\x53\x94\x64\x00\x06\xa7\x16\xe4\x07\xa0\xff\x2e\x85\x08\x2d\ -\xbe\x23\x88\xd0\x2c\xfa\xef\x52\xf2\x03\xc0\xb0\xc0\x80\x04\x56\ -\x30\x30\x45\x41\x48\xcc\x82\x6e\x83\xad\xd7\x7c\x58\x0c\x33\xa2\ -\x4d\x3e\x14\xa6\xd1\x7a\xcd\x27\xdf\x74\x98\x61\x81\xc6\xab\xbf\ -\x43\x8c\x85\xc0\x02\x68\x18\x9c\x16\x7c\xbb\x15\x3c\x10\xfa\xf1\ -\xaf\x10\xde\x1a\x11\xbe\x4d\xe6\x1c\xf6\x63\x2c\xa6\xe2\x2c\x4b\ -\x00\xc0\x6a\xa3\xc2\x31\xec\x17\xc4\x12\x58\x00\xed\xd7\xfd\x9c\ -\x8d\x18\xd9\x02\x00\x56\x1b\xa6\x87\x2e\x7b\x37\x6d\x96\x3c\x4e\ -\x96\x22\x0c\x0e\x5d\xf6\xe2\xe3\x3b\x41\xd1\xfc\x4a\xca\x00\x5c\ -\x76\x23\x7c\xed\x65\xf0\xb6\x94\xa2\xca\xa4\x8d\xba\x37\x39\x1b\ -\x82\xf9\x82\x07\x9f\x79\x16\x10\x4e\x62\x8b\x0c\xd3\x2c\xae\x78\ -\x28\x98\x2f\x78\x30\x39\x1b\x6d\xf6\xd5\x26\x2d\xbc\x2d\xa5\xf0\ -\xb5\x97\xc1\x21\x40\xe5\x39\xa5\x50\xb8\xf3\xa0\x09\xc7\x1f\x69\ -\x8d\xfd\xbd\x18\xc1\xde\xee\x7b\x09\x14\x09\xd3\x1a\xfb\xad\xe5\ -\x39\x14\xe6\x6e\x64\xf1\x17\x27\x82\x70\xa5\xe0\x73\xb2\x84\x9a\ -\x3c\x00\x3c\xb5\x49\x41\x94\x5d\x5b\x16\xa9\xae\xe5\x58\x1d\x27\ -\xd6\x3a\xc5\x7c\x21\xf0\x5a\x02\x67\xf7\x15\xc5\x4d\x1e\x00\xfa\ -\xc6\x02\x10\x5b\x7a\xc7\xe3\x75\x9c\xa8\xd4\xa1\x83\xe7\xf9\x04\ -\x7e\x47\x64\x38\xda\xe0\xbd\x63\x01\x74\x8e\xce\x8b\x0e\xa0\xf3\ -\xd6\x1c\x7a\x39\x40\x37\x57\xe9\xa4\x03\x10\xd3\xb0\x45\xcf\xed\ -\x00\xce\xdc\x98\x91\x2c\x19\x3a\x73\x63\x06\x3d\x31\x07\x22\x58\ -\x9e\x9e\x4c\x8d\x9a\xa3\x1d\xc9\xfe\x68\x85\x66\x50\x6b\x7e\x1a\ -\xec\xda\xe4\xcf\x7d\x9b\xfc\xe1\x04\x87\xcd\x80\x2f\x5f\xdd\x8b\ -\x76\x9b\x11\x2c\x80\x9f\x7c\xc9\x05\x39\x37\xff\xf8\x17\xdb\xb2\ -\x54\x78\xbe\x28\x17\x34\x03\xbc\x73\x73\x16\x13\x33\x8b\xd2\xee\ -\x02\xa9\x08\xe5\xb6\x46\xed\x02\x85\xe7\x7f\xde\x8a\x61\x28\x67\ -\x85\x95\xa2\xa8\x02\x40\x01\xb0\x35\x92\xd6\x67\x85\x85\x10\x21\ -\xce\x0a\x0b\x21\x44\xf9\xeb\xac\xe2\x03\x14\x00\x0a\x80\x4c\x96\ -\xff\x07\x00\x85\xfa\xad\x10\xc5\x6e\x22\x46\x00\x00\x00\x00\x49\ -\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x0f\x88\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x40\x00\x00\x00\x40\x08\x06\x00\x00\x00\xaa\x69\x71\xde\ -\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ -\x01\x00\x9a\x9c\x18\x00\x00\x0a\x4f\x69\x43\x43\x50\x50\x68\x6f\ -\x74\x6f\x73\x68\x6f\x70\x20\x49\x43\x43\x20\x70\x72\x6f\x66\x69\ -\x6c\x65\x00\x00\x78\xda\x9d\x53\x67\x54\x53\xe9\x16\x3d\xf7\xde\ -\xf4\x42\x4b\x88\x80\x94\x4b\x6f\x52\x15\x08\x20\x52\x42\x8b\x80\ -\x14\x91\x26\x2a\x21\x09\x10\x4a\x88\x21\xa1\xd9\x15\x51\xc1\x11\ -\x45\x45\x04\x1b\xc8\xa0\x88\x03\x8e\x8e\x80\x8c\x15\x51\x2c\x0c\ -\x8a\x0a\xd8\x07\xe4\x21\xa2\x8e\x83\xa3\x88\x8a\xca\xfb\xe1\x7b\ -\xa3\x6b\xd6\xbc\xf7\xe6\xcd\xfe\xb5\xd7\x3e\xe7\xac\xf3\x9d\xb3\ -\xcf\x07\xc0\x08\x0c\x96\x48\x33\x51\x35\x80\x0c\xa9\x42\x1e\x11\ -\xe0\x83\xc7\xc4\xc6\xe1\xe4\x2e\x40\x81\x0a\x24\x70\x00\x10\x08\ -\xb3\x64\x21\x73\xfd\x23\x01\x00\xf8\x7e\x3c\x3c\x2b\x22\xc0\x07\ -\xbe\x00\x01\x78\xd3\x0b\x08\x00\xc0\x4d\x9b\xc0\x30\x1c\x87\xff\ -\x0f\xea\x42\x99\x5c\x01\x80\x84\x01\xc0\x74\x91\x38\x4b\x08\x80\ -\x14\x00\x40\x7a\x8e\x42\xa6\x00\x40\x46\x01\x80\x9d\x98\x26\x53\ -\x00\xa0\x04\x00\x60\xcb\x63\x62\xe3\x00\x50\x2d\x00\x60\x27\x7f\ -\xe6\xd3\x00\x80\x9d\xf8\x99\x7b\x01\x00\x5b\x94\x21\x15\x01\xa0\ -\x91\x00\x20\x13\x65\x88\x44\x00\x68\x3b\x00\xac\xcf\x56\x8a\x45\ -\x00\x58\x30\x00\x14\x66\x4b\xc4\x39\x00\xd8\x2d\x00\x30\x49\x57\ -\x66\x48\x00\xb0\xb7\x00\xc0\xce\x10\x0b\xb2\x00\x08\x0c\x00\x30\ -\x51\x88\x85\x29\x00\x04\x7b\x00\x60\xc8\x23\x23\x78\x00\x84\x99\ -\x00\x14\x46\xf2\x57\x3c\xf1\x2b\xae\x10\xe7\x2a\x00\x00\x78\x99\ -\xb2\x3c\xb9\x24\x39\x45\x81\x5b\x08\x2d\x71\x07\x57\x57\x2e\x1e\ -\x28\xce\x49\x17\x2b\x14\x36\x61\x02\x61\x9a\x40\x2e\xc2\x79\x99\ -\x19\x32\x81\x34\x0f\xe0\xf3\xcc\x00\x00\xa0\x91\x15\x11\xe0\x83\ -\xf3\xfd\x78\xce\x0e\xae\xce\xce\x36\x8e\xb6\x0e\x5f\x2d\xea\xbf\ -\x06\xff\x22\x62\x62\xe3\xfe\xe5\xcf\xab\x70\x40\x00\x00\xe1\x74\ -\x7e\xd1\xfe\x2c\x2f\xb3\x1a\x80\x3b\x06\x80\x6d\xfe\xa2\x25\xee\ -\x04\x68\x5e\x0b\xa0\x75\xf7\x8b\x66\xb2\x0f\x40\xb5\x00\xa0\xe9\ -\xda\x57\xf3\x70\xf8\x7e\x3c\x3c\x45\xa1\x90\xb9\xd9\xd9\xe5\xe4\ -\xe4\xd8\x4a\xc4\x42\x5b\x61\xca\x57\x7d\xfe\x67\xc2\x5f\xc0\x57\ -\xfd\x6c\xf9\x7e\x3c\xfc\xf7\xf5\xe0\xbe\xe2\x24\x81\x32\x5d\x81\ -\x47\x04\xf8\xe0\xc2\xcc\xf4\x4c\xa5\x1c\xcf\x92\x09\x84\x62\xdc\ -\xe6\x8f\x47\xfc\xb7\x0b\xff\xfc\x1d\xd3\x22\xc4\x49\x62\xb9\x58\ -\x2a\x14\xe3\x51\x12\x71\x8e\x44\x9a\x8c\xf3\x32\xa5\x22\x89\x42\ -\x92\x29\xc5\x25\xd2\xff\x64\xe2\xdf\x2c\xfb\x03\x3e\xdf\x35\x00\ -\xb0\x6a\x3e\x01\x7b\x91\x2d\xa8\x5d\x63\x03\xf6\x4b\x27\x10\x58\ -\x74\xc0\xe2\xf7\x00\x00\xf2\xbb\x6f\xc1\xd4\x28\x08\x03\x80\x68\ -\x83\xe1\xcf\x77\xff\xef\x3f\xfd\x47\xa0\x25\x00\x80\x66\x49\x92\ -\x71\x00\x00\x5e\x44\x24\x2e\x54\xca\xb3\x3f\xc7\x08\x00\x00\x44\ -\xa0\x81\x2a\xb0\x41\x1b\xf4\xc1\x18\x2c\xc0\x06\x1c\xc1\x05\xdc\ -\xc1\x0b\xfc\x60\x36\x84\x42\x24\xc4\xc2\x42\x10\x42\x0a\x64\x80\ -\x1c\x72\x60\x29\xac\x82\x42\x28\x86\xcd\xb0\x1d\x2a\x60\x2f\xd4\ -\x40\x1d\x34\xc0\x51\x68\x86\x93\x70\x0e\x2e\xc2\x55\xb8\x0e\x3d\ -\x70\x0f\xfa\x61\x08\x9e\xc1\x28\xbc\x81\x09\x04\x41\xc8\x08\x13\ -\x61\x21\xda\x88\x01\x62\x8a\x58\x23\x8e\x08\x17\x99\x85\xf8\x21\ -\xc1\x48\x04\x12\x8b\x24\x20\xc9\x88\x14\x51\x22\x4b\x91\x35\x48\ -\x31\x52\x8a\x54\x20\x55\x48\x1d\xf2\x3d\x72\x02\x39\x87\x5c\x46\ -\xba\x91\x3b\xc8\x00\x32\x82\xfc\x86\xbc\x47\x31\x94\x81\xb2\x51\ -\x3d\xd4\x0c\xb5\x43\xb9\xa8\x37\x1a\x84\x46\xa2\x0b\xd0\x64\x74\ -\x31\x9a\x8f\x16\xa0\x9b\xd0\x72\xb4\x1a\x3d\x8c\x36\xa1\xe7\xd0\ -\xab\x68\x0f\xda\x8f\x3e\x43\xc7\x30\xc0\xe8\x18\x07\x33\xc4\x6c\ -\x30\x2e\xc6\xc3\x42\xb1\x38\x2c\x09\x93\x63\xcb\xb1\x22\xac\x0c\ -\xab\xc6\x1a\xb0\x56\xac\x03\xbb\x89\xf5\x63\xcf\xb1\x77\x04\x12\ -\x81\x45\xc0\x09\x36\x04\x77\x42\x20\x61\x1e\x41\x48\x58\x4c\x58\ -\x4e\xd8\x48\xa8\x20\x1c\x24\x34\x11\xda\x09\x37\x09\x03\x84\x51\ -\xc2\x27\x22\x93\xa8\x4b\xb4\x26\xba\x11\xf9\xc4\x18\x62\x32\x31\ -\x87\x58\x48\x2c\x23\xd6\x12\x8f\x13\x2f\x10\x7b\x88\x43\xc4\x37\ -\x24\x12\x89\x43\x32\x27\xb9\x90\x02\x49\xb1\xa4\x54\xd2\x12\xd2\ -\x46\xd2\x6e\x52\x23\xe9\x2c\xa9\x9b\x34\x48\x1a\x23\x93\xc9\xda\ -\x64\x6b\xb2\x07\x39\x94\x2c\x20\x2b\xc8\x85\xe4\x9d\xe4\xc3\xe4\ -\x33\xe4\x1b\xe4\x21\xf2\x5b\x0a\x9d\x62\x40\x71\xa4\xf8\x53\xe2\ -\x28\x52\xca\x6a\x4a\x19\xe5\x10\xe5\x34\xe5\x06\x65\x98\x32\x41\ -\x55\xa3\x9a\x52\xdd\xa8\xa1\x54\x11\x35\x8f\x5a\x42\xad\xa1\xb6\ -\x52\xaf\x51\x87\xa8\x13\x34\x75\x9a\x39\xcd\x83\x16\x49\x4b\xa5\ -\xad\xa2\x95\xd3\x1a\x68\x17\x68\xf7\x69\xaf\xe8\x74\xba\x11\xdd\ -\x95\x1e\x4e\x97\xd0\x57\xd2\xcb\xe9\x47\xe8\x97\xe8\x03\xf4\x77\ -\x0c\x0d\x86\x15\x83\xc7\x88\x67\x28\x19\x9b\x18\x07\x18\x67\x19\ -\x77\x18\xaf\x98\x4c\xa6\x19\xd3\x8b\x19\xc7\x54\x30\x37\x31\xeb\ -\x98\xe7\x99\x0f\x99\x6f\x55\x58\x2a\xb6\x2a\x7c\x15\x91\xca\x0a\ -\x95\x4a\x95\x26\x95\x1b\x2a\x2f\x54\xa9\xaa\xa6\xaa\xde\xaa\x0b\ -\x55\xf3\x55\xcb\x54\x8f\xa9\x5e\x53\x7d\xae\x46\x55\x33\x53\xe3\ -\xa9\x09\xd4\x96\xab\x55\xaa\x9d\x50\xeb\x53\x1b\x53\x67\xa9\x3b\ -\xa8\x87\xaa\x67\xa8\x6f\x54\x3f\xa4\x7e\x59\xfd\x89\x06\x59\xc3\ -\x4c\xc3\x4f\x43\xa4\x51\xa0\xb1\x5f\xe3\xbc\xc6\x20\x0b\x63\x19\ -\xb3\x78\x2c\x21\x6b\x0d\xab\x86\x75\x81\x35\xc4\x26\xb1\xcd\xd9\ -\x7c\x76\x2a\xbb\x98\xfd\x1d\xbb\x8b\x3d\xaa\xa9\xa1\x39\x43\x33\ -\x4a\x33\x57\xb3\x52\xf3\x94\x66\x3f\x07\xe3\x98\x71\xf8\x9c\x74\ -\x4e\x09\xe7\x28\xa7\x97\xf3\x7e\x8a\xde\x14\xef\x29\xe2\x29\x1b\ -\xa6\x34\x4c\xb9\x31\x65\x5c\x6b\xaa\x96\x97\x96\x58\xab\x48\xab\ -\x51\xab\x47\xeb\xbd\x36\xae\xed\xa7\x9d\xa6\xbd\x45\xbb\x59\xfb\ -\x81\x0e\x41\xc7\x4a\x27\x5c\x27\x47\x67\x8f\xce\x05\x9d\xe7\x53\ -\xd9\x53\xdd\xa7\x0a\xa7\x16\x4d\x3d\x3a\xf5\xae\x2e\xaa\x6b\xa5\ -\x1b\xa1\xbb\x44\x77\xbf\x6e\xa7\xee\x98\x9e\xbe\x5e\x80\x9e\x4c\ -\x6f\xa7\xde\x79\xbd\xe7\xfa\x1c\x7d\x2f\xfd\x54\xfd\x6d\xfa\xa7\ -\xf5\x47\x0c\x58\x06\xb3\x0c\x24\x06\xdb\x0c\xce\x18\x3c\xc5\x35\ -\x71\x6f\x3c\x1d\x2f\xc7\xdb\xf1\x51\x43\x5d\xc3\x40\x43\xa5\x61\ -\x95\x61\x97\xe1\x84\x91\xb9\xd1\x3c\xa3\xd5\x46\x8d\x46\x0f\x8c\ -\x69\xc6\x5c\xe3\x24\xe3\x6d\xc6\x6d\xc6\xa3\x26\x06\x26\x21\x26\ -\x4b\x4d\xea\x4d\xee\x9a\x52\x4d\xb9\xa6\x29\xa6\x3b\x4c\x3b\x4c\ -\xc7\xcd\xcc\xcd\xa2\xcd\xd6\x99\x35\x9b\x3d\x31\xd7\x32\xe7\x9b\ -\xe7\x9b\xd7\x9b\xdf\xb7\x60\x5a\x78\x5a\x2c\xb6\xa8\xb6\xb8\x65\ -\x49\xb2\xe4\x5a\xa6\x59\xee\xb6\xbc\x6e\x85\x5a\x39\x59\xa5\x58\ -\x55\x5a\x5d\xb3\x46\xad\x9d\xad\x25\xd6\xbb\xad\xbb\xa7\x11\xa7\ -\xb9\x4e\x93\x4e\xab\x9e\xd6\x67\xc3\xb0\xf1\xb6\xc9\xb6\xa9\xb7\ -\x19\xb0\xe5\xd8\x06\xdb\xae\xb6\x6d\xb6\x7d\x61\x67\x62\x17\x67\ -\xb7\xc5\xae\xc3\xee\x93\xbd\x93\x7d\xba\x7d\x8d\xfd\x3d\x07\x0d\ -\x87\xd9\x0e\xab\x1d\x5a\x1d\x7e\x73\xb4\x72\x14\x3a\x56\x3a\xde\ -\x9a\xce\x9c\xee\x3f\x7d\xc5\xf4\x96\xe9\x2f\x67\x58\xcf\x10\xcf\ -\xd8\x33\xe3\xb6\x13\xcb\x29\xc4\x69\x9d\x53\x9b\xd3\x47\x67\x17\ -\x67\xb9\x73\x83\xf3\x88\x8b\x89\x4b\x82\xcb\x2e\x97\x3e\x2e\x9b\ -\x1b\xc6\xdd\xc8\xbd\xe4\x4a\x74\xf5\x71\x5d\xe1\x7a\xd2\xf5\x9d\ -\x9b\xb3\x9b\xc2\xed\xa8\xdb\xaf\xee\x36\xee\x69\xee\x87\xdc\x9f\ -\xcc\x34\x9f\x29\x9e\x59\x33\x73\xd0\xc3\xc8\x43\xe0\x51\xe5\xd1\ -\x3f\x0b\x9f\x95\x30\x6b\xdf\xac\x7e\x4f\x43\x4f\x81\x67\xb5\xe7\ -\x23\x2f\x63\x2f\x91\x57\xad\xd7\xb0\xb7\xa5\x77\xaa\xf7\x61\xef\ -\x17\x3e\xf6\x3e\x72\x9f\xe3\x3e\xe3\x3c\x37\xde\x32\xde\x59\x5f\ -\xcc\x37\xc0\xb7\xc8\xb7\xcb\x4f\xc3\x6f\x9e\x5f\x85\xdf\x43\x7f\ -\x23\xff\x64\xff\x7a\xff\xd1\x00\xa7\x80\x25\x01\x67\x03\x89\x81\ -\x41\x81\x5b\x02\xfb\xf8\x7a\x7c\x21\xbf\x8e\x3f\x3a\xdb\x65\xf6\ -\xb2\xd9\xed\x41\x8c\xa0\xb9\x41\x15\x41\x8f\x82\xad\x82\xe5\xc1\ -\xad\x21\x68\xc8\xec\x90\xad\x21\xf7\xe7\x98\xce\x91\xce\x69\x0e\ -\x85\x50\x7e\xe8\xd6\xd0\x07\x61\xe6\x61\x8b\xc3\x7e\x0c\x27\x85\ -\x87\x85\x57\x86\x3f\x8e\x70\x88\x58\x1a\xd1\x31\x97\x35\x77\xd1\ -\xdc\x43\x73\xdf\x44\xfa\x44\x96\x44\xde\x9b\x67\x31\x4f\x39\xaf\ -\x2d\x4a\x35\x2a\x3e\xaa\x2e\x6a\x3c\xda\x37\xba\x34\xba\x3f\xc6\ -\x2e\x66\x59\xcc\xd5\x58\x9d\x58\x49\x6c\x4b\x1c\x39\x2e\x2a\xae\ -\x36\x6e\x6c\xbe\xdf\xfc\xed\xf3\x87\xe2\x9d\xe2\x0b\xe3\x7b\x17\ -\x98\x2f\xc8\x5d\x70\x79\xa1\xce\xc2\xf4\x85\xa7\x16\xa9\x2e\x12\ -\x2c\x3a\x96\x40\x4c\x88\x4e\x38\x94\xf0\x41\x10\x2a\xa8\x16\x8c\ -\x25\xf2\x13\x77\x25\x8e\x0a\x79\xc2\x1d\xc2\x67\x22\x2f\xd1\x36\ -\xd1\x88\xd8\x43\x5c\x2a\x1e\x4e\xf2\x48\x2a\x4d\x7a\x92\xec\x91\ -\xbc\x35\x79\x24\xc5\x33\xa5\x2c\xe5\xb9\x84\x27\xa9\x90\xbc\x4c\ -\x0d\x4c\xdd\x9b\x3a\x9e\x16\x9a\x76\x20\x6d\x32\x3d\x3a\xbd\x31\ -\x83\x92\x91\x90\x71\x42\xaa\x21\x4d\x93\xb6\x67\xea\x67\xe6\x66\ -\x76\xcb\xac\x65\x85\xb2\xfe\xc5\x6e\x8b\xb7\x2f\x1e\x95\x07\xc9\ -\x6b\xb3\x90\xac\x05\x59\x2d\x0a\xb6\x42\xa6\xe8\x54\x5a\x28\xd7\ -\x2a\x07\xb2\x67\x65\x57\x66\xbf\xcd\x89\xca\x39\x96\xab\x9e\x2b\ -\xcd\xed\xcc\xb3\xca\xdb\x90\x37\x9c\xef\x9f\xff\xed\x12\xc2\x12\ -\xe1\x92\xb6\xa5\x86\x4b\x57\x2d\x1d\x58\xe6\xbd\xac\x6a\x39\xb2\ -\x3c\x71\x79\xdb\x0a\xe3\x15\x05\x2b\x86\x56\x06\xac\x3c\xb8\x8a\ -\xb6\x2a\x6d\xd5\x4f\xab\xed\x57\x97\xae\x7e\xbd\x26\x7a\x4d\x6b\ -\x81\x5e\xc1\xca\x82\xc1\xb5\x01\x6b\xeb\x0b\x55\x0a\xe5\x85\x7d\ -\xeb\xdc\xd7\xed\x5d\x4f\x58\x2f\x59\xdf\xb5\x61\xfa\x86\x9d\x1b\ -\x3e\x15\x89\x8a\xae\x14\xdb\x17\x97\x15\x7f\xd8\x28\xdc\x78\xe5\ -\x1b\x87\x6f\xca\xbf\x99\xdc\x94\xb4\xa9\xab\xc4\xb9\x64\xcf\x66\ -\xd2\x66\xe9\xe6\xde\x2d\x9e\x5b\x0e\x96\xaa\x97\xe6\x97\x0e\x6e\ -\x0d\xd9\xda\xb4\x0d\xdf\x56\xb4\xed\xf5\xf6\x45\xdb\x2f\x97\xcd\ -\x28\xdb\xbb\x83\xb6\x43\xb9\xa3\xbf\x3c\xb8\xbc\x65\xa7\xc9\xce\ -\xcd\x3b\x3f\x54\xa4\x54\xf4\x54\xfa\x54\x36\xee\xd2\xdd\xb5\x61\ -\xd7\xf8\x6e\xd1\xee\x1b\x7b\xbc\xf6\x34\xec\xd5\xdb\x5b\xbc\xf7\ -\xfd\x3e\xc9\xbe\xdb\x55\x01\x55\x4d\xd5\x66\xd5\x65\xfb\x49\xfb\ -\xb3\xf7\x3f\xae\x89\xaa\xe9\xf8\x96\xfb\x6d\x5d\xad\x4e\x6d\x71\ -\xed\xc7\x03\xd2\x03\xfd\x07\x23\x0e\xb6\xd7\xb9\xd4\xd5\x1d\xd2\ -\x3d\x54\x52\x8f\xd6\x2b\xeb\x47\x0e\xc7\x1f\xbe\xfe\x9d\xef\x77\ -\x2d\x0d\x36\x0d\x55\x8d\x9c\xc6\xe2\x23\x70\x44\x79\xe4\xe9\xf7\ -\x09\xdf\xf7\x1e\x0d\x3a\xda\x76\x8c\x7b\xac\xe1\x07\xd3\x1f\x76\ -\x1d\x67\x1d\x2f\x6a\x42\x9a\xf2\x9a\x46\x9b\x53\x9a\xfb\x5b\x62\ -\x5b\xba\x4f\xcc\x3e\xd1\xd6\xea\xde\x7a\xfc\x47\xdb\x1f\x0f\x9c\ -\x34\x3c\x59\x79\x4a\xf3\x54\xc9\x69\xda\xe9\x82\xd3\x93\x67\xf2\ -\xcf\x8c\x9d\x95\x9d\x7d\x7e\x2e\xf9\xdc\x60\xdb\xa2\xb6\x7b\xe7\ -\x63\xce\xdf\x6a\x0f\x6f\xef\xba\x10\x74\xe1\xd2\x45\xff\x8b\xe7\ -\x3b\xbc\x3b\xce\x5c\xf2\xb8\x74\xf2\xb2\xdb\xe5\x13\x57\xb8\x57\ -\x9a\xaf\x3a\x5f\x6d\xea\x74\xea\x3c\xfe\x93\xd3\x4f\xc7\xbb\x9c\ -\xbb\x9a\xae\xb9\x5c\x6b\xb9\xee\x7a\xbd\xb5\x7b\x66\xf7\xe9\x1b\ -\x9e\x37\xce\xdd\xf4\xbd\x79\xf1\x16\xff\xd6\xd5\x9e\x39\x3d\xdd\ -\xbd\xf3\x7a\x6f\xf7\xc5\xf7\xf5\xdf\x16\xdd\x7e\x72\x27\xfd\xce\ -\xcb\xbb\xd9\x77\x27\xee\xad\xbc\x4f\xbc\x5f\xf4\x40\xed\x41\xd9\ -\x43\xdd\x87\xd5\x3f\x5b\xfe\xdc\xd8\xef\xdc\x7f\x6a\xc0\x77\xa0\ -\xf3\xd1\xdc\x47\xf7\x06\x85\x83\xcf\xfe\x91\xf5\x8f\x0f\x43\x05\ -\x8f\x99\x8f\xcb\x86\x0d\x86\xeb\x9e\x38\x3e\x39\x39\xe2\x3f\x72\ -\xfd\xe9\xfc\xa7\x43\xcf\x64\xcf\x26\x9e\x17\xfe\xa2\xfe\xcb\xae\ -\x17\x16\x2f\x7e\xf8\xd5\xeb\xd7\xce\xd1\x98\xd1\xa1\x97\xf2\x97\ -\x93\xbf\x6d\x7c\xa5\xfd\xea\xc0\xeb\x19\xaf\xdb\xc6\xc2\xc6\x1e\ -\xbe\xc9\x78\x33\x31\x5e\xf4\x56\xfb\xed\xc1\x77\xdc\x77\x1d\xef\ -\xa3\xdf\x0f\x4f\xe4\x7c\x20\x7f\x28\xff\x68\xf9\xb1\xf5\x53\xd0\ -\xa7\xfb\x93\x19\x93\x93\xff\x04\x03\x98\xf3\xfc\x63\x33\x2d\xdb\ -\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\x25\x00\x00\x80\x83\ -\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00\x75\x30\x00\x00\xea\x60\ -\x00\x00\x3a\x98\x00\x00\x17\x6f\x92\x5f\xc5\x46\x00\x00\x04\xb3\ -\x49\x44\x41\x54\x78\xda\xec\x9b\x5d\x4c\x5b\x65\x18\xc7\xff\x6f\ -\x4b\x19\x8e\xca\x12\xc0\x36\x2d\x6b\x9a\x8d\x2e\xbb\x11\x24\x54\ -\xbe\x12\x68\x71\x4b\xb6\x25\xf3\x6e\x8a\x5c\x98\x5d\x90\x6c\x6e\ -\x44\x2f\xd8\x00\xfb\xe1\x16\xa6\x59\x84\x69\x24\x21\x82\xcb\xa2\ -\x2e\x61\x12\x58\xa2\x89\x37\x66\x03\xa7\x6e\xf8\x31\x3e\xa6\x2b\ -\xf3\xaa\x4a\xa6\xb6\x7c\xa4\xf5\x60\x32\x53\x3e\xd2\x7a\xce\xf1\ -\x02\x06\x6b\x7b\xca\xd6\xd3\x73\x0e\xa7\xe9\x79\xae\xda\x73\xd2\ -\xf3\xbc\xef\xef\x3c\xef\xf3\x3e\x1f\x6f\x09\x4e\x7d\xcd\x22\x83\ -\x45\x85\x0c\x17\x05\x80\x02\x40\x01\xa0\x00\x50\x00\x6c\x85\x38\ -\x6c\x06\x50\x6e\x2b\x28\xb7\x15\x6d\x75\x86\xcc\x03\xd0\x5a\x67\ -\x84\x8a\x00\x2a\x02\xbc\x69\x33\x66\x1e\x00\x15\xe1\xfe\xac\xf8\ -\x00\x05\x80\x02\x20\x33\x00\x30\x2c\xf7\xe7\xb4\x00\xf0\x7a\x8d\ -\x1e\x01\x67\x39\x82\xae\x72\x9c\xdd\x57\xc4\x4b\xf1\x07\x3f\xcc\ -\x83\x61\x57\x27\xdf\x35\x3a\xc7\xeb\x19\x1d\xfb\x77\x22\xe8\x2a\ -\x47\xc0\x59\x8e\xe6\x2a\x3d\xaf\x67\x10\x3e\xe9\x70\xc0\x55\x0e\ -\xcd\x23\xae\xfb\xd2\x64\x10\x8e\x61\xbf\xa4\x6f\xae\xeb\xa0\x09\ -\xc7\x2a\x74\xeb\xdf\x23\x34\x0b\xfd\xbb\xbf\x48\x63\x01\xb1\xbb\ -\xd6\xf1\x0a\x1d\xce\xed\xdf\x29\xd9\xe4\xcf\x1f\x88\x9e\x3c\x00\ -\x10\x22\xe1\x12\xe8\x1b\x0f\xc6\x5d\x7b\xa3\x46\x0f\x87\x4d\xfc\ -\x88\xce\x65\x37\xe2\x64\xa5\x2e\xee\xfa\x87\xb7\x03\xd2\x01\xe8\ -\xf8\x66\x06\x97\x26\xe3\x21\x34\x57\xeb\x45\x07\x70\x92\x63\xad\ -\x7f\x34\x11\xc4\xdb\xdf\xcd\xf2\x7a\x5e\x16\xef\x58\x7e\xd8\x0f\ -\x02\x44\x99\xe2\x52\x84\xd9\x74\xd9\x1c\xab\xd0\xa1\xa1\x24\x1f\ -\x96\x82\x1c\x68\xb3\xd5\xeb\x11\x20\xc3\x02\xa1\x30\x8d\xe9\x85\ -\x15\x0c\xdd\xfb\x07\x9f\xdc\x09\x22\x91\x63\x5a\xfe\x8f\x41\x6e\ -\xf6\xc6\x7b\xbb\x38\x11\x84\x7b\x84\xbf\xff\x21\xa9\xd6\x04\xdd\ -\xf5\x46\xbc\x56\xa9\xc3\x72\x84\xc1\xd1\xcf\xef\x63\xdc\x1f\x8a\ -\xba\xaf\x26\x04\xdd\x87\xcd\x68\x28\xc9\x47\xb6\xfa\xc9\x16\x6a\ -\x98\x66\x71\xf5\xd7\x05\x9c\xfa\xca\x07\x9a\x8d\x1e\x5e\xb5\x49\ -\x8b\xfe\x97\x8a\x91\xa3\x21\xe8\x1b\x0f\xa2\xf3\xd6\x5c\x4a\x16\ -\x45\xc4\x2c\x8a\x1e\xb0\xec\xc0\xa7\x47\x76\x63\xbb\x86\x5f\xb8\ -\xb1\x14\x61\xd0\xf4\xc5\x7d\x8c\x4c\x3f\x48\xbf\x40\xa8\xad\xce\ -\x80\xa1\x46\x0b\xef\xc9\x03\xc0\x76\x8d\x0a\x43\x8d\x16\x51\xd3\ -\x65\x51\x00\x9c\xae\x35\xc0\x69\x17\x2e\xc5\x75\xda\x8d\x38\x5d\ -\x6b\x48\x0f\x00\x2f\xec\xce\x83\xbb\x5e\xf8\xfc\xde\x5d\x6f\x84\ -\x7d\x57\x9e\xbc\x01\x64\xa9\x08\xae\xbc\x5c\x2c\x9a\xb9\x0e\x34\ -\x14\x43\x4d\x88\x7c\x01\xf4\xbc\x68\x4e\x69\xcd\x3f\x89\x4f\xe8\ -\x3e\x6c\x96\x27\x00\x15\x01\x8e\x3c\x9b\x2f\x7a\x20\xf4\x4a\x49\ -\x3e\x88\x1c\x01\x34\x59\x75\x51\x09\x92\x58\xa2\x51\x13\x34\x59\ -\x9f\x91\x1f\x80\xc6\x52\xf1\xdf\xfe\x86\xae\x02\xf9\x01\xb0\x14\ -\xe4\x48\x06\x60\x4f\xa1\x70\xba\x12\xe6\x02\x0e\x9b\x61\xbd\x74\ -\x1d\x5b\xc9\xe9\x1a\x9d\xc3\x7b\xdf\xcf\x47\x5d\xd7\x66\xab\x25\ -\x03\xc0\xa5\x2b\xd9\xf1\x3e\xd6\x02\xb8\x1e\xf6\xd0\xd9\x71\xd5\ -\xf1\xa5\x2c\x6d\x73\xe9\x4a\x76\xbc\xa2\x87\xc2\xe9\x22\x09\x01\ -\x3c\xac\xd9\xc5\x4a\xa2\x1a\x9e\x94\x85\x4d\x2e\x5d\xc9\x8e\x57\ -\xf0\x6c\xf0\xcf\xb6\x32\xe4\x6d\x93\xc6\x0f\x3c\x58\xa1\xb1\xeb\ -\x7d\x8f\xbc\x76\x01\x2f\xb5\x22\x99\x05\x78\xa9\x65\xf9\x6d\x83\ -\x83\x53\x94\x64\x00\x06\xa7\x16\xe4\x07\xa0\xff\x2e\x85\x08\x2d\ -\xbe\x23\x88\xd0\x2c\xfa\xef\x52\xf2\x03\xc0\xb0\xc0\x80\x04\x56\ -\x30\x30\x45\x41\x48\xcc\x82\x6e\x83\xad\xd7\x7c\x58\x0c\x33\xa2\ -\x4d\x3e\x14\xa6\xd1\x7a\xcd\x27\xdf\x74\x98\x61\x81\xc6\xab\xbf\ -\x43\x8c\x85\xc0\x02\x68\x18\x9c\x16\x7c\xbb\x15\x3c\x10\xfa\xf1\ -\xaf\x10\xde\x1a\x11\xbe\x4d\xe6\x1c\xf6\x63\x2c\xa6\xe2\x2c\x4b\ -\x00\xc0\x6a\xa3\xc2\x31\xec\x17\xc4\x12\x58\x00\xed\xd7\xfd\x9c\ -\x8d\x18\xd9\x02\x00\x56\x1b\xa6\x87\x2e\x7b\x37\x6d\x96\x3c\x4e\ -\x96\x22\x0c\x0e\x5d\xf6\xe2\xe3\x3b\x41\xd1\xfc\x4a\xca\x00\x5c\ -\x76\x23\x7c\xed\x65\xf0\xb6\x94\xa2\xca\xa4\x8d\xba\x37\x39\x1b\ -\x82\xf9\x82\x07\x9f\x79\x16\x10\x4e\x62\x8b\x0c\xd3\x2c\xae\x78\ -\x28\x98\x2f\x78\x30\x39\x1b\x6d\xf6\xd5\x26\x2d\xbc\x2d\xa5\xf0\ -\xb5\x97\xc1\x21\x40\xe5\x39\xa5\x50\xb8\xf3\xa0\x09\xc7\x1f\x69\ -\x8d\xfd\xbd\x18\xc1\xde\xee\x7b\x09\x14\x09\xd3\x1a\xfb\xad\xe5\ -\x39\x14\xe6\x6e\x64\xf1\x17\x27\x82\x70\xa5\xe0\x73\xb2\x84\x9a\ -\x3c\x00\x3c\xb5\x49\x41\x94\x5d\x5b\x16\xa9\xae\xe5\x58\x1d\x27\ -\xd6\x3a\xc5\x7c\x21\xf0\x5a\x02\x67\xf7\x15\xc5\x4d\x1e\x00\xfa\ -\xc6\x02\x10\x5b\x7a\xc7\xe3\x75\x9c\xa8\xd4\xa1\x83\xe7\xf9\x04\ -\x7e\x47\x64\x38\xda\xe0\xbd\x63\x01\x74\x8e\xce\x8b\x0e\xa0\xf3\ -\xd6\x1c\x7a\x39\x40\x37\x57\xe9\xa4\x03\x10\xd3\xb0\x45\xcf\xed\ -\x00\xce\xdc\x98\x91\x2c\x19\x3a\x73\x63\x06\x3d\x31\x07\x22\x58\ -\x9e\x9e\x4c\x8d\x9a\xa3\x1d\xc9\xfe\x68\x85\x66\x50\x6b\x7e\x1a\ -\xec\xda\xe4\xcf\x7d\x9b\xfc\xe1\x04\x87\xcd\x80\x2f\x5f\xdd\x8b\ -\x76\x9b\x11\x2c\x80\x9f\x7c\xc9\x05\x39\x37\xff\xf8\x17\xdb\xb2\ -\x54\x78\xbe\x28\x17\x34\x03\xbc\x73\x73\x16\x13\x33\x8b\xd2\xee\ -\x02\xa9\x08\xe5\xb6\x46\xed\x02\x85\xe7\x7f\xde\x8a\x61\x28\x67\ -\x85\x95\xa2\xa8\x02\x40\x01\xb0\x35\x92\xd6\x67\x85\x85\x10\x21\ -\xce\x0a\x0b\x21\x44\xf9\xeb\xac\xe2\x03\x14\x00\x0a\x80\x4c\x96\ -\xff\x07\x00\x85\xfa\xad\x10\xc5\x6e\x22\x46\x00\x00\x00\x00\x49\ -\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x03\xd7\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ -\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ -\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ -\x01\x00\x9a\x9c\x18\x00\x00\x03\x79\x49\x44\x41\x54\x58\x85\xed\ -\x56\x5f\x68\x5b\x75\x14\xfe\xce\xcd\xcd\xbf\xf6\xa6\x59\x96\x7b\ -\x93\xdb\x88\x6b\x93\x9b\xd2\x91\xcd\x0e\x11\x1f\x2a\x6c\x63\x22\ -\x0a\xb3\x74\x22\xfa\x30\x1f\xf6\xb0\x37\xeb\x7c\xe8\x60\x0a\xa2\ -\x20\x88\xff\xc0\xaa\xb5\xe2\x9c\x6f\xfa\x30\x11\x61\x9b\x4e\x41\ -\x11\x04\x61\xa0\xf5\xcf\x1c\x6c\xa6\xf6\xbf\x12\x5d\x93\x26\x59\ -\x48\x73\x13\xdb\xdc\xe4\x1e\x5f\x8c\xa4\x35\x49\x6f\xdb\x21\x0c\ -\xfa\xbd\xfd\xce\xf9\xee\x77\x3e\xce\xbd\xe7\xdc\x1f\xb0\x8d\x6d\ -\xdc\xca\x88\xc5\xe0\xb8\x0b\xb0\x6f\x45\xc3\xb6\x95\x87\x3d\x82\ -\x72\x6a\xc5\xe3\xbe\x3b\xa7\xff\xf5\xed\x66\x35\x04\xab\xc4\x68\ -\x48\xbe\x53\x53\x95\x37\x57\x05\x89\x45\x66\x61\x55\x07\x22\xaa\ -\x3c\xa6\x05\x83\x7b\x6e\xba\x01\xb4\xed\x88\x9b\x30\x07\xb4\x40\ -\xa0\xbf\xce\x41\x82\x05\x33\x51\x3b\xf5\xa8\xf2\x01\x32\xe9\xbe\ -\x1d\xa9\xd4\x94\x65\xdd\x8d\x20\x1c\x94\x1f\xd4\x82\xca\x0b\xcd\ -\xf2\x9a\x2a\xbf\xaa\x75\x2a\x0f\x6c\x44\x93\x5a\x25\xbb\x15\x65\ -\x5f\x9b\x92\x9e\x88\xc7\x51\xae\x0b\x0b\xd1\x4e\xff\x51\xaf\x24\ -\x3e\xb1\x5c\xe6\xdd\x00\xe0\xb2\xd3\xaf\x4b\xa5\xea\x5b\xd3\xd7\ -\x33\x1f\x01\xe0\x1a\xb1\xab\xab\xcb\xe5\x30\x4a\xbb\xa7\xaf\xa7\ -\xaf\x6c\xca\xc0\x3f\xef\xfc\xb0\xc9\x3c\x3c\x9f\xca\x7c\xbe\x6b\ -\x97\xd7\xe7\x13\x9c\x5f\x45\x42\x62\xef\xd0\x11\x49\xea\x0b\x3b\ -\x00\x00\x3f\xcf\x96\xf1\xce\x27\xba\xfe\xfb\x62\xf9\xda\x72\x9e\ -\xef\x9f\xcc\x66\x0b\x5a\xc0\x3f\x68\x0a\x78\x83\x98\xce\xcd\xa5\ -\x32\xa7\x36\x65\x00\x00\xb4\x40\xa0\x9f\x05\x1e\x98\x4b\xa6\x9f\ -\xeb\xeb\x0e\x8c\x1f\x3d\xe4\xde\x37\xfc\x68\xc7\x7f\x46\x8f\x19\ -\x78\xe9\xc3\x7c\xf9\xe2\xa5\xe5\xef\xaf\xfc\xb6\xb8\x5f\x53\xe5\ -\x97\x05\xb6\x5d\x98\x4e\xa5\xc6\x5b\xe9\xaf\x6b\xa0\x86\x68\xa7\ -\xff\xb1\xbe\xb0\xf3\xcc\xd9\x67\xfd\x52\x33\x0e\x33\xf0\xc8\xf3\ -\x19\x7d\x6a\xa1\x7a\x6c\xfa\x8f\xc5\xf3\x56\x74\x2d\x4f\x81\x57\ -\x12\x4f\x0c\x3d\x24\x35\x2d\x0e\x00\x44\xc0\xe3\x47\x3c\x92\xd7\ -\xc5\x4f\x5a\xd5\x15\x1b\x05\x63\x31\x38\x96\xb3\xca\x49\x10\x8b\ -\x00\x25\xe6\x92\xe9\xf7\x0d\xc3\xec\xdd\xdb\xbd\xfe\xd2\xdb\xdb\ -\x2d\x22\xa7\xe3\x10\x00\x84\x83\xf2\x71\x22\x84\xc0\x64\xdc\x9e\ -\x4a\x8f\x7c\x03\x54\xd6\xf2\x1b\x76\xc0\x1d\x07\x13\x99\x06\x98\ -\x2a\x4c\x66\x05\x00\xaa\x4c\xc4\xdc\x88\xbd\x1a\xf5\x14\x81\xa8\ -\x02\xa6\x0a\x11\x1b\xcd\xf8\x0d\x3b\xf0\x13\x60\x20\x99\x1d\xa9\ -\x8f\x39\xed\x34\x71\x75\xde\xb8\xe7\x40\x9f\xb3\xa5\x81\xab\xf3\ -\x06\x7c\x12\xbe\x06\x80\xd9\x64\xfa\x83\x5a\x7c\xb6\x09\xdf\xf2\ -\x37\x50\x28\x56\xdf\x7e\xef\x33\xbd\xd0\x8a\xc3\x0c\xbc\xfb\xa9\ -\xae\x17\x4a\xe6\xa8\x55\xdd\x75\x7f\x46\xe1\x4e\x79\xbf\x5f\x6a\ -\x1b\x9a\x49\x66\x46\xed\xa2\x73\x30\xaf\x9b\x81\xfe\x98\xcb\x46\ -\x6b\xe6\x87\x19\x78\xf1\x6c\xbe\x7c\x79\xb2\xfc\xdd\xb5\x44\xe6\ -\x99\x48\x50\x19\xf1\x75\xb4\x17\x73\x7a\x29\xd1\x58\xd9\x82\x81\ -\x88\x2a\x8f\x09\x4c\xc3\x10\xe8\x74\x4e\x2f\xcd\x78\xc8\xf5\xf1\ -\xec\x82\x79\xef\x17\x3f\xae\xf8\x76\x76\xd8\x1c\xed\x2e\x42\x69\ -\x85\x31\x3e\x61\xe0\xa9\x33\xf9\xc2\xe5\xa9\xf2\x0f\xd5\x92\x30\ -\x90\x2a\x16\x0d\x9f\xa7\x4d\x24\xa6\xd3\x3e\xa9\x3d\x92\x2b\x96\ -\xbe\x6c\x56\xa3\xe5\x1e\x08\x07\x02\x77\xb8\xe5\xc5\xc9\x35\xab\ -\x18\x3d\xb7\x29\x0f\x7b\xdd\x38\x51\xa9\xf0\x9e\x2a\x40\x4e\x3b\ -\xfd\x92\x2f\x61\x6c\xfa\xcf\xf4\xb9\x7a\x5e\x2c\x06\x47\xf9\xc6\ -\xce\xe8\x4c\xf2\x46\xbc\x55\x1d\xcb\xd0\x02\xfe\x41\x4d\x95\x5f\ -\x69\x96\x8f\x04\x95\xd7\x22\xaa\x7c\x78\x23\x9a\xd6\xef\x03\xd1\ -\xa8\xd3\x14\xf0\xba\xc0\xb6\x7f\x37\x5c\x38\x28\x1f\xd7\x54\xe5\ -\x58\x9d\xda\x05\x06\x8f\xc6\x62\x70\xdc\x74\x03\xd5\xa5\xa5\x5e\ -\x62\x3a\x5f\xbf\xdb\x89\x10\x62\x46\xa8\x76\x9e\x5b\x48\x5f\x22\ -\x16\x2e\xae\x64\x83\x3d\x56\x75\xb7\x84\x48\x50\x79\x5a\x53\xe5\ -\x93\xff\x4b\xb1\x46\x38\x08\x88\x07\x9b\x2c\xb3\x6d\x6c\xe3\x96\ -\xc1\xdf\xd0\x8a\x3c\x4e\x76\x81\x84\x65\x00\x00\x00\x00\x49\x45\ -\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x03\xd7\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ -\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ -\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ -\x01\x00\x9a\x9c\x18\x00\x00\x03\x79\x49\x44\x41\x54\x58\x85\xed\ -\x56\x5f\x68\x5b\x75\x14\xfe\xce\xcd\xcd\xbf\xf6\xa6\x59\x96\x7b\ -\x93\xdb\x88\x6b\x93\x9b\xd2\x91\xcd\x0e\x11\x1f\x2a\x6c\x63\x22\ -\x0a\xb3\x74\x22\xfa\x30\x1f\xf6\xb0\x37\xeb\x7c\xe8\x60\x0a\xa2\ -\x20\x88\xff\xc0\xaa\xb5\xe2\x9c\x6f\xfa\x30\x11\x61\x9b\x4e\x41\ -\x11\x04\x61\xa0\xf5\xcf\x1c\x6c\xa6\xf6\xbf\x12\x5d\x93\x26\x59\ -\x48\x73\x13\xdb\xdc\xe4\x1e\x5f\x8c\xa4\x35\x49\x6f\xdb\x21\x0c\ -\xfa\xbd\xfd\xce\xf9\xee\x77\x3e\xce\xbd\xe7\xdc\x1f\xb0\x8d\x6d\ -\xdc\xca\x88\xc5\xe0\xb8\x0b\xb0\x6f\x45\xc3\xb6\x95\x87\x3d\x82\ -\x72\x6a\xc5\xe3\xbe\x3b\xa7\xff\xf5\xed\x66\x35\x04\xab\xc4\x68\ -\x48\xbe\x53\x53\x95\x37\x57\x05\x89\x45\x66\x61\x55\x07\x22\xaa\ -\x3c\xa6\x05\x83\x7b\x6e\xba\x01\xb4\xed\x88\x9b\x30\x07\xb4\x40\ -\xa0\xbf\xce\x41\x82\x05\x33\x51\x3b\xf5\xa8\xf2\x01\x32\xe9\xbe\ -\x1d\xa9\xd4\x94\x65\xdd\x8d\x20\x1c\x94\x1f\xd4\x82\xca\x0b\xcd\ -\xf2\x9a\x2a\xbf\xaa\x75\x2a\x0f\x6c\x44\x93\x5a\x25\xbb\x15\x65\ -\x5f\x9b\x92\x9e\x88\xc7\x51\xae\x0b\x0b\xd1\x4e\xff\x51\xaf\x24\ -\x3e\xb1\x5c\xe6\xdd\x00\xe0\xb2\xd3\xaf\x4b\xa5\xea\x5b\xd3\xd7\ -\x33\x1f\x01\xe0\x1a\xb1\xab\xab\xcb\xe5\x30\x4a\xbb\xa7\xaf\xa7\ -\xaf\x6c\xca\xc0\x3f\xef\xfc\xb0\xc9\x3c\x3c\x9f\xca\x7c\xbe\x6b\ -\x97\xd7\xe7\x13\x9c\x5f\x45\x42\x62\xef\xd0\x11\x49\xea\x0b\x3b\ -\x00\x00\x3f\xcf\x96\xf1\xce\x27\xba\xfe\xfb\x62\xf9\xda\x72\x9e\ -\xef\x9f\xcc\x66\x0b\x5a\xc0\x3f\x68\x0a\x78\x83\x98\xce\xcd\xa5\ -\x32\xa7\x36\x65\x00\x00\xb4\x40\xa0\x9f\x05\x1e\x98\x4b\xa6\x9f\ -\xeb\xeb\x0e\x8c\x1f\x3d\xe4\xde\x37\xfc\x68\xc7\x7f\x46\x8f\x19\ -\x78\xe9\xc3\x7c\xf9\xe2\xa5\xe5\xef\xaf\xfc\xb6\xb8\x5f\x53\xe5\ -\x97\x05\xb6\x5d\x98\x4e\xa5\xc6\x5b\xe9\xaf\x6b\xa0\x86\x68\xa7\ -\xff\xb1\xbe\xb0\xf3\xcc\xd9\x67\xfd\x52\x33\x0e\x33\xf0\xc8\xf3\ -\x19\x7d\x6a\xa1\x7a\x6c\xfa\x8f\xc5\xf3\x56\x74\x2d\x4f\x81\x57\ -\x12\x4f\x0c\x3d\x24\x35\x2d\x0e\x00\x44\xc0\xe3\x47\x3c\x92\xd7\ -\xc5\x4f\x5a\xd5\x15\x1b\x05\x63\x31\x38\x96\xb3\xca\x49\x10\x8b\ -\x00\x25\xe6\x92\xe9\xf7\x0d\xc3\xec\xdd\xdb\xbd\xfe\xd2\xdb\xdb\ -\x2d\x22\xa7\xe3\x10\x00\x84\x83\xf2\x71\x22\x84\xc0\x64\xdc\x9e\ -\x4a\x8f\x7c\x03\x54\xd6\xf2\x1b\x76\xc0\x1d\x07\x13\x99\x06\x98\ -\x2a\x4c\x66\x05\x00\xaa\x4c\xc4\xdc\x88\xbd\x1a\xf5\x14\x81\xa8\ -\x02\xa6\x0a\x11\x1b\xcd\xf8\x0d\x3b\xf0\x13\x60\x20\x99\x1d\xa9\ -\x8f\x39\xed\x34\x71\x75\xde\xb8\xe7\x40\x9f\xb3\xa5\x81\xab\xf3\ -\x06\x7c\x12\xbe\x06\x80\xd9\x64\xfa\x83\x5a\x7c\xb6\x09\xdf\xf2\ -\x37\x50\x28\x56\xdf\x7e\xef\x33\xbd\xd0\x8a\xc3\x0c\xbc\xfb\xa9\ -\xae\x17\x4a\xe6\xa8\x55\xdd\x75\x7f\x46\xe1\x4e\x79\xbf\x5f\x6a\ -\x1b\x9a\x49\x66\x46\xed\xa2\x73\x30\xaf\x9b\x81\xfe\x98\xcb\x46\ -\x6b\xe6\x87\x19\x78\xf1\x6c\xbe\x7c\x79\xb2\xfc\xdd\xb5\x44\xe6\ -\x99\x48\x50\x19\xf1\x75\xb4\x17\x73\x7a\x29\xd1\x58\xd9\x82\x81\ -\x88\x2a\x8f\x09\x4c\xc3\x10\xe8\x74\x4e\x2f\xcd\x78\xc8\xf5\xf1\ -\xec\x82\x79\xef\x17\x3f\xae\xf8\x76\x76\xd8\x1c\xed\x2e\x42\x69\ -\x85\x31\x3e\x61\xe0\xa9\x33\xf9\xc2\xe5\xa9\xf2\x0f\xd5\x92\x30\ -\x90\x2a\x16\x0d\x9f\xa7\x4d\x24\xa6\xd3\x3e\xa9\x3d\x92\x2b\x96\ -\xbe\x6c\x56\xa3\xe5\x1e\x08\x07\x02\x77\xb8\xe5\xc5\xc9\x35\xab\ -\x18\x3d\xb7\x29\x0f\x7b\xdd\x38\x51\xa9\xf0\x9e\x2a\x40\x4e\x3b\ -\xfd\x92\x2f\x61\x6c\xfa\xcf\xf4\xb9\x7a\x5e\x2c\x06\x47\xf9\xc6\ -\xce\xe8\x4c\xf2\x46\xbc\x55\x1d\xcb\xd0\x02\xfe\x41\x4d\x95\x5f\ -\x69\x96\x8f\x04\x95\xd7\x22\xaa\x7c\x78\x23\x9a\xd6\xef\x03\xd1\ -\xa8\xd3\x14\xf0\xba\xc0\xb6\x7f\x37\x5c\x38\x28\x1f\xd7\x54\xe5\ -\x58\x9d\xda\x05\x06\x8f\xc6\x62\x70\xdc\x74\x03\xd5\xa5\xa5\x5e\ -\x62\x3a\x5f\xbf\xdb\x89\x10\x62\x46\xa8\x76\x9e\x5b\x48\x5f\x22\ -\x16\x2e\xae\x64\x83\x3d\x56\x75\xb7\x84\x48\x50\x79\x5a\x53\xe5\ -\x93\xff\x4b\xb1\x46\x38\x08\x88\x07\x9b\x2c\xb3\x6d\x6c\xe3\x96\ -\xc1\xdf\xd0\x8a\x3c\x4e\x76\x81\x84\x65\x00\x00\x00\x00\x49\x45\ -\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x03\xdd\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ -\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ -\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ -\x01\x00\x9a\x9c\x18\x00\x00\x03\x7f\x49\x44\x41\x54\x58\x85\xed\ -\x94\x5f\x68\x5b\x55\x1c\xc7\xbf\xbf\x73\xee\x6d\xb3\xa6\x4d\x72\ -\x6f\x72\x9b\x9b\xcc\x2e\x6b\x13\x27\x9d\x54\x10\x87\x68\xd9\x10\ -\x44\xc1\xc7\x22\xc2\xd0\x87\x0d\x64\x32\xa6\x75\x0a\x56\xf6\xe6\ -\xde\xa4\x3e\xf8\x20\x1b\x0e\x5f\xe6\x8b\xa2\xf8\xe8\x83\xf8\xe8\ -\xbf\xee\x41\x2b\x2a\xce\x51\xba\xae\xb0\x9a\x36\xb1\x37\x49\x75\ -\xc6\xb4\xb7\xc9\x39\x3f\x1f\x5c\xb2\xb6\x24\xed\x1d\x1b\x82\xd8\ -\xef\xd3\xf9\xf3\x3d\xdf\xdf\xe7\x1c\xce\x39\xc0\xae\x76\xf5\x5f\ -\x56\xce\xb6\x23\x07\x1d\xa7\xf7\x4e\x32\xc4\x9d\x2c\x66\x43\x8c\ -\xaf\x09\x9c\xfa\x57\x00\x86\xdc\xf8\xc9\xac\xeb\x1c\xdb\x04\x20\ -\x20\x00\xc8\xcd\x3e\xe7\xf8\x60\x32\xf1\xfc\x5d\x07\x90\xe0\xaf\ -\x00\x4c\x0e\x59\x56\xb4\x05\xc0\x58\x22\xc2\x52\xb3\x9f\xc9\x44\ -\x63\x00\xde\x24\x85\xaf\x83\xe7\x06\x54\xa5\xba\x5a\xb2\xc2\xe1\ -\x14\x19\xc2\x5a\xa9\xd6\x7e\x02\x80\xdf\xff\xaa\xfd\xd0\x6c\x03\ -\x40\x3c\x14\x79\x96\x88\xf3\xf3\xcb\xa5\x4f\x82\xe6\xd2\x76\x73\ -\x39\xdb\xee\x9b\xab\x54\x6e\x6c\x9d\x18\x4c\x25\x8e\x10\xe3\x28\ -\x83\xee\x07\x00\xd2\x7c\x85\x98\x3e\xbc\xe6\x79\x97\xb6\x7a\x73\ -\xb6\x1d\x69\x97\xd1\x54\xc7\x13\xb8\xd7\xb5\x87\x59\xd2\xb4\x15\ -\xee\x15\xa9\x4c\x6d\xda\xf3\xa0\x86\x2c\x2b\x1a\x8b\xf6\x7e\xdc\ -\x6d\xf2\x0b\xc7\x9f\x0c\xf7\x9c\x1e\xeb\x4b\x8c\x8d\xee\x09\xdb\ -\x11\x11\xf9\x65\xc1\x3f\xd6\x17\xee\x1d\xdd\xd3\x17\xfd\xac\x5a\ -\xad\xd6\x73\xb9\x5c\x77\xcc\x94\xaf\xc3\xc0\x07\x56\x28\xfc\xe9\ -\x4a\xad\x56\xb9\xdd\x13\x40\x36\x6d\x0d\x40\x1b\x6f\x31\xf4\x97\ -\x56\xb1\x7c\xb1\xe2\xc6\xbf\x38\x32\xd2\x5d\x3f\xff\x4a\xec\x70\ -\x97\x90\x9b\xe0\xd7\x95\x52\x27\xdf\xfe\xe3\xd2\xb7\x33\xbe\x7f\ -\xad\x58\x7a\x2a\x9b\x74\x4e\x01\x78\x44\x98\xea\xcc\xd5\x7c\x65\ -\xb1\x53\x8d\x6d\x01\x36\xc1\xb8\xce\xc4\x40\xbf\x7c\xfa\xf3\x49\ -\xe7\xd1\xed\x7c\x4f\x4c\x94\xa6\x8b\x95\xc6\xfb\x73\x45\xef\xdd\ -\x20\xb9\xc1\xff\x01\xcd\xe3\x17\x4e\xc7\xf6\xee\x64\x3b\xf7\x52\ -\xa4\x9f\x81\x57\x83\xc6\xb6\x05\x78\x08\x30\x33\x99\x68\xec\xe6\ -\xb3\x12\xd9\xb4\x35\x60\x9a\xe0\xc1\xb4\xb9\x6f\xa7\xc0\xe1\xc1\ -\xae\x7d\x52\x22\x94\x71\x1c\x17\x80\x6c\xe6\x3c\x06\x18\x81\x01\ -\x56\xdc\xc4\x19\xc3\x37\x66\x0c\xdf\x98\x19\xea\x4f\x1c\x55\x75\ -\xc3\x0e\x99\xf8\x33\xe8\xae\xac\x1e\x39\x67\x4a\x65\x67\x5d\xe7\ -\xb9\x66\xce\xaf\x49\xe7\xb5\x76\xde\x40\x77\x20\xe3\x38\xae\x29\ -\xe9\xfb\xcb\x17\x93\xe9\x20\xfe\x91\x13\x4b\xcb\xab\xa2\x3e\x9c\ -\xcf\xdf\x68\x7b\xf3\x37\x2a\xd0\x1d\xb8\xee\x79\x45\x05\xbd\xf2\ -\xe3\xec\xfa\xcc\x4e\xde\xa9\xcb\x6b\x3f\xeb\x86\x58\x08\x52\x1c\ -\xe8\x70\x02\x39\xd7\x79\x91\xa1\xdf\xb8\xc9\x38\xcd\x00\x43\xeb\ -\x35\x69\x8a\x2e\x43\x52\x88\xb4\x6e\x0b\xde\x10\x42\x0b\xcd\xfe\ -\xaa\xd2\xbe\x64\x11\x22\x80\x00\x7d\x08\x00\x18\x38\x3b\x5f\x2c\ -\xbf\x17\x08\x00\x80\x38\xe8\x38\x3d\x00\x50\x16\x82\xe3\x5a\x93\ -\x21\x04\x37\xb4\xa6\x66\xbf\xdd\xa2\xb2\x10\x9c\x6c\xc2\x6c\xf1\ -\x5e\xf1\xbc\x1a\x00\x1d\x14\x00\x00\xb0\xdf\x75\x33\x04\x75\x88\ -\x98\xcd\xd6\x02\x29\x59\xb2\x58\x9c\x2d\x14\xbe\xd9\xe8\x3d\x90\ -\x4a\x1d\x66\xa8\x01\xa5\x35\x37\xc7\x98\xa8\x4e\xd4\xf8\x6e\xbe\ -\xb0\xb2\xd0\xa9\x46\xdb\xa7\x71\x6b\xb2\xb1\x5f\x13\x3f\x08\x2d\ -\xbb\x5b\x83\x0d\xe6\x3a\xfb\x63\x07\x9c\xf8\xc8\xac\x57\xbe\xf0\ -\x4f\xf1\xc4\x89\x75\xf6\x1f\x97\x30\xf2\x80\x68\x6d\x8a\x48\xf9\ -\x80\xf1\x1b\x80\x8e\x00\x81\x7f\xc2\x8d\xba\x2f\x1e\x4f\xfb\x06\ -\x9d\x93\x24\xa6\x08\x60\xad\xd5\xa8\x62\xf1\xf2\x75\xcf\x2b\xde\ -\x6e\x56\x20\x80\xac\xeb\x4c\x30\xf8\x99\xd6\x00\xd3\x94\x20\x39\ -\xa9\xb5\x3a\x4f\xac\xb4\x69\x86\xc6\x7d\xed\x9f\x25\xd0\xc3\x2d\ -\x8f\xc6\x47\xf3\xcb\xa5\x77\xee\x0a\x40\xce\x75\x1d\x12\xea\xd6\ -\x37\x2c\x94\x77\x35\x5f\x59\x7c\x20\x99\x0c\x7b\x42\x70\xa1\x50\ -\xa8\xe5\xf6\xda\xf7\x10\xcb\x44\x2b\x98\x8d\xfc\x6c\xa1\x50\x0a\ -\x92\xbf\xab\x5d\xfd\xbf\xf5\x37\xb2\xeb\x52\xa6\x18\x05\x8f\xcd\ -\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -" - -qt_resource_name = b"\ -\x00\x03\ -\x00\x00\x78\xc3\ -\x00\x72\ -\x00\x65\x00\x73\ -\x00\x08\ -\x02\x33\x50\x87\ -\x00\x38\ -\x00\x5f\x00\x30\x00\x30\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x08\ -\x02\x33\x50\x07\ -\x00\x34\ -\x00\x5f\x00\x30\x00\x30\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x17\ -\x07\x37\x30\x67\ -\x00\x64\ -\x00\x61\x00\x79\x00\x74\x00\x69\x00\x6d\x00\x65\x00\x5f\x00\x65\x00\x64\x00\x69\x00\x74\x00\x6f\x00\x72\x00\x5f\x00\x6c\x00\x6f\ -\x00\x67\x00\x6f\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x09\ -\x02\x33\x3d\x87\ -\x00\x32\ -\x00\x30\x00\x5f\x00\x30\x00\x30\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x08\ -\x02\x33\x51\x87\ -\x00\x30\ -\x00\x5f\x00\x30\x00\x30\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x08\ -\x0a\x61\x5a\xa7\ -\x00\x69\ -\x00\x63\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x09\ -\x02\x33\x3e\x47\ -\x00\x31\ -\x00\x32\x00\x5f\x00\x30\x00\x30\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x09\ -\x02\x33\x3e\xc7\ -\x00\x31\ -\x00\x36\x00\x5f\x00\x30\x00\x30\x00\x2e\x00\x70\x00\x6e\x00\x67\ -" - -qt_resource_struct = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x03\ -\x00\x00\x00\x6c\x00\x00\x00\x00\x00\x01\x00\x00\x1e\xd7\ -\x00\x00\x00\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x49\x23\ -\x00\x00\x00\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x45\x48\ -\x00\x00\x00\xc8\x00\x00\x00\x00\x00\x01\x00\x00\x4c\xfe\ -\x00\x00\x00\x22\x00\x00\x00\x00\x00\x01\x00\x00\x03\x96\ -\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x00\x84\x00\x00\x00\x00\x00\x01\x00\x00\x22\xc1\ -\x00\x00\x00\x38\x00\x00\x00\x00\x00\x01\x00\x00\x07\x83\ -\x00\x00\x00\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x35\xbc\ -\x00\x00\x00\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x26\x30\ -" - -def qInitResources(): - QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) - -def qCleanupResources(): - QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) - -qInitResources() +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created by: The Resource Compiler for PyQt5 (Qt v5.7.0) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + +qt_resource_data = b"\ +\x00\x00\x03\x92\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ +\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ +\x01\x00\x9a\x9c\x18\x00\x00\x03\x34\x49\x44\x41\x54\x58\x85\xed\ +\x95\x4f\x68\x14\x57\x1c\xc7\xbf\xbf\x37\x33\x49\x30\x71\xff\xcc\ +\xee\xcb\x8e\x2b\x71\xb2\xd9\xf1\x60\x1a\xa1\xd0\x43\x29\x68\x05\ +\xb1\xd4\xb3\x14\xc4\xe0\x29\x94\x56\x45\x54\x54\xea\xc9\x96\x82\ +\x50\x3d\x14\x52\x28\x2d\xbd\x04\x7b\x08\x85\x7a\x16\x3c\xa8\x54\ +\xa4\x39\xf8\x07\xdb\x6a\x25\x98\x75\x45\xbb\x26\x6b\x66\xb2\x6b\ +\x74\x5d\xb3\x66\xe7\xfd\x7a\x90\x15\xdd\x64\xb3\xb3\x58\x2c\xa5\ +\xf9\xde\x66\xde\xf7\x7d\x7f\x9f\x79\xef\xf7\xe6\x01\xcb\x5a\xd6\ +\x7f\x59\x8e\x69\x86\xfa\xa5\xec\x7a\x9d\x0c\xf1\x3a\x93\x59\x17\ +\x7b\xe7\x04\x76\xbf\x11\x80\x3e\xab\x7b\x57\xaa\x3b\x36\xf8\x0a\ +\x80\x80\x00\xa0\xbd\xfc\x2e\x95\x88\x0f\xa5\x12\xf1\xa1\x7f\x1c\ +\x40\x43\xf5\x02\x20\x4e\x38\xa6\x19\x7a\x01\xc0\x98\x24\xc2\x64\ +\xed\xd9\xb6\xc3\x11\x41\x74\x8c\x7c\x5c\x0c\x9e\x1b\x50\x85\xd2\ +\x53\xcf\xec\xea\x5c\x4d\xba\x16\x2e\x94\x9e\xfc\x01\x00\x0f\x9f\ +\x94\xaf\x15\x4b\xe5\xdf\x6b\x9e\x58\x47\x68\x07\x11\xe7\xb2\xd3\ +\xde\xcf\x41\x73\x69\xa9\x31\xc7\x34\x57\x66\x0a\x85\x47\xf5\x03\ +\xa9\x55\xf1\x8d\xc4\xd8\xce\xa0\xb7\x00\x80\x14\xdf\x24\xa6\xd1\ +\xdb\xae\x3b\x56\xef\x75\x4c\x33\xb4\x58\x46\x53\x80\xb5\x96\xb9\ +\x4e\x81\xce\x33\x8b\x61\xb1\x32\x3c\x9c\xc9\x64\x2a\x7d\xd1\x68\ +\x98\xdb\xb4\x93\xed\x6d\x3c\xb0\x73\x73\xe7\xe4\x86\x81\x8e\x18\ +\x00\x5c\xbc\x5e\x29\x8c\x9e\x2b\x25\x2b\xbe\xb8\x32\x47\xfa\xd0\ +\xd4\xd4\x54\xd9\x71\x9c\x76\x7e\x3c\x7b\x10\xa4\xf6\x71\x95\xde\ +\xcf\x7a\xde\x44\xab\x2b\x80\x74\x32\xda\x03\xa5\x9f\x60\xa8\x0b\ +\xd1\xfc\xcc\x48\xc1\x8a\xfd\xb2\x71\x7d\xfb\xfc\xb7\xfb\x23\x1b\ +\xda\x84\xf6\xca\xf6\x3d\xf3\x7d\xff\x93\xaf\x67\xc7\x2e\x8f\x57\ +\x2a\xb7\xf3\xde\xd6\x74\x42\xee\x01\xf0\xae\x30\xfc\x23\x13\xb9\ +\xc2\xfd\x96\x57\x60\x01\x8c\x25\x0f\xf7\x74\x6b\xdb\xce\x1c\x97\ +\xef\x2d\xe5\xdb\xf2\xd9\x83\xcb\x79\x8f\x4f\x66\xf2\xee\x77\x41\ +\x72\x83\xff\x07\x14\xef\xfd\x7e\x5f\x64\x75\x33\xdb\xf0\x2e\x53\ +\x32\x70\x20\x68\xec\xa2\x00\xef\x00\x86\x6d\x87\x23\xb6\x1d\x8e\ +\x00\x10\xe9\x64\xb4\xc7\x30\xc0\xa9\xa4\xb1\xa6\x59\xe0\x40\x9f\ +\xd1\xab\x69\xe8\xb0\xa5\xb4\x00\x68\xb5\x9c\x4d\x80\x1e\x18\xa0\ +\x68\xc5\x8f\xe8\x15\x7d\x5c\xaf\xe8\xe3\x7d\xdd\xf1\xed\xfe\xbc\ +\x6e\x76\x18\x78\x1c\xf4\xab\x42\x5d\x98\x68\xd3\xfc\x68\xda\x92\ +\x83\xb5\x9c\xbf\x12\xf2\xd0\x62\xde\x40\x3d\x60\x4b\x69\x19\x1a\ +\x5d\xbd\x31\x92\x48\x06\xf1\xaf\xff\x78\x72\xfa\xa9\x98\x5f\x97\ +\xcb\x3d\x2a\x34\xf3\x06\xea\x81\xbb\xae\x9b\xf7\xa1\x8a\xbf\xdd\ +\x7a\x36\xde\xcc\xfb\xeb\x8d\xb9\xeb\xaa\x2a\xee\x05\x29\x0e\x34\ +\x58\x01\xc7\x92\x7b\x18\xea\xf3\x7a\x2f\x13\x48\x23\xc0\x8a\xea\ +\x57\x4f\x1f\x8f\xdd\xda\xf1\x65\x71\x40\x37\x58\x8d\x1e\x8d\xff\ +\xf9\xc1\x21\xb7\xdf\x9b\xad\xbe\xed\x33\x40\x0c\x06\xc0\x2f\x4f\ +\x66\xe0\x8b\x6c\x7e\xe6\x87\x40\x00\x00\x44\xbf\x94\x2b\x1a\x51\ +\x87\x5c\xd7\x77\x2d\x39\xc2\xc0\x0a\x02\x34\xc5\x3c\xa3\x1e\x78\ +\x9f\x76\x4a\xb9\x68\xa3\x01\xc0\x4d\xd7\x2d\x03\x50\x41\x01\x00\ +\x00\xbd\x96\x65\x0b\xae\x2e\x3c\xf7\x44\x83\x0c\x94\xd7\xe4\xdd\ +\x9d\xf7\x1d\x47\x53\xa5\xd9\x53\xc4\x5c\x65\x60\xe1\x1d\x20\xfc\ +\xb1\xec\x54\xf1\x5e\xa3\x1a\x0d\x89\x9f\x0f\x56\x7b\x15\xd1\x96\ +\x3a\x62\x62\xf0\xa5\x3b\x79\xef\xab\x3b\x80\x8f\x4c\xa6\xba\x09\ +\xd8\x96\x4b\xc8\xa3\x44\xea\x43\x86\xf0\xeb\x52\x72\x00\x1a\x02\ +\xfc\xeb\x0a\x74\x0c\xd3\x96\x3c\xcc\xe0\x8f\x5a\x4a\x56\xf8\x29\ +\x3b\xed\x7d\xd3\xcc\xb6\xe4\x16\xd4\x44\xd0\x7e\x14\xc2\x3f\xdb\ +\x4a\x7d\x22\x3d\xd7\x8a\x7f\x59\xcb\xfa\xff\xea\x6f\xcd\x62\x28\ +\x7d\x4b\x8a\x77\x07\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\ +\x82\ +\x00\x00\x03\xe9\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ +\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ +\x01\x00\x9a\x9c\x18\x00\x00\x03\x8b\x49\x44\x41\x54\x58\x85\xed\ +\x96\x5f\x68\xdb\x55\x14\xc7\xbf\xe7\xde\xdf\x2f\xc9\x7e\x69\x9b\ +\xa6\x69\xf2\x6b\xcc\xdc\xaf\x26\x6d\x61\x6b\x29\xcc\x8d\x81\x5b\ +\x5d\xa7\x3e\x14\x06\xb3\xe2\x98\x7f\x86\xa8\x0c\xe6\x54\x9c\xc5\ +\x09\x13\x8a\x8c\x22\x4c\xd0\x47\xc5\xfa\xa0\x20\x0a\x4a\x19\x3e\ +\x29\xc8\x64\x0c\x54\xc4\x3d\x0d\x1f\xca\xaa\xb0\xa6\x69\xba\x48\ +\xda\x25\xb4\x4b\xb7\xa6\x49\x93\xdf\x3d\x3e\x8c\xb4\x51\x97\x3f\ +\x85\x20\x88\xfd\xbe\xdd\x73\xbe\xf7\x9c\x0f\x87\x7b\x2f\x17\xd8\ +\xd2\x96\xfe\x83\x22\xcb\xb2\x5c\x8d\x2a\xa6\x6d\xc6\x1c\xf1\xfb\ +\xf7\x93\xc4\x88\x5a\xcb\xfe\x01\xe0\x4c\x23\x00\x44\xdd\xcd\x83\ +\xed\x27\xa5\x26\x2e\xb8\x74\xd9\x23\x18\x57\xee\xe9\x31\xfd\xa7\ +\xc3\xa6\x19\x68\x38\x40\x24\x10\x18\xd6\xa5\x36\xfa\xed\xc5\x57\ +\x65\x51\xa0\x89\x6d\xfc\xfa\xcf\xe6\xbe\x43\xcc\x78\x85\xa9\xf8\ +\x76\x43\x01\xc2\x5e\xaf\x87\x89\x3f\xfa\xfc\x8b\x17\x0a\xa1\x90\ +\xa7\xa3\xb8\xc6\x06\x49\x99\x29\xaf\x11\x31\xfd\xaf\x41\xc8\xaf\ +\xde\x79\xef\xfd\x65\xa7\xd3\x75\x38\x62\xfa\xc6\xc2\xa6\xbb\xae\ +\x49\xd4\x9e\x80\x43\x7b\xe9\xc1\xdd\xf7\x47\x7b\xfb\x83\xdd\x00\ +\xe0\x74\xd0\x9d\x02\x15\x7c\xa5\xb4\xe5\xf7\x07\x98\xd4\x49\xab\ +\x33\x1c\x3d\x76\xfc\xf8\xde\xd3\x23\x6f\x26\x41\x34\x20\x94\x71\ +\xb0\x1e\x80\x9a\x87\x90\x18\x4f\xbf\x71\xf6\x91\xa6\xd2\x7a\xfb\ +\x0e\x6f\x6a\xfa\x7a\x7a\x1f\x80\xdf\x01\x20\x9e\x4a\xcd\x5b\x96\ +\x67\x30\x36\x33\xfd\xd9\xd0\xc0\x81\x2b\x73\x37\x62\x21\x56\x72\ +\x38\x96\xba\x39\x59\x0f\x40\xd5\x09\x0c\x02\x1a\x4b\x74\xf7\xee\ +\x0a\xf6\x94\x62\xcf\x3c\xbb\x4f\x03\xf8\xb9\x72\x5f\x3c\x9e\xb9\ +\x65\x3b\x0b\x27\xe2\x37\x66\xbd\x60\x1a\x8d\xdd\xac\xaf\x79\x4d\ +\x80\x64\x30\xd8\x2a\x20\xee\x68\x9a\x46\xa5\xd8\xf0\x13\x7d\x7b\ +\x75\x87\x1e\x0e\x07\x02\x8f\x95\x7b\xe7\xe6\x32\x4b\x94\xb7\x0f\ +\xcc\x2c\xa4\x27\xea\x6d\x5e\x13\xe0\x36\x90\x85\x62\xa3\x3c\xe6\ +\x70\xe8\xf2\xc3\x0f\x9e\x5a\x85\xe4\x73\x3b\x5b\x5b\xad\xf2\xdc\ +\xcc\xd2\x52\x06\x9b\x54\xf5\x09\x24\x93\x59\x66\x95\x89\xc7\x6f\ +\x25\xca\xe3\x0f\x0d\x3c\xd0\x37\xfa\xd6\x90\xcc\xbb\xb4\x89\x48\ +\xd0\x3f\xb4\xd9\xa6\xe5\x92\xb5\x0c\x5e\xb7\xd1\x9d\xcf\x15\xe4\ +\xa1\x47\x7b\x3a\xcb\xe3\x7d\xfd\xf7\xed\xd8\xb3\xdb\xca\x5c\xfa\ +\xfe\xda\x91\x66\xe7\xb6\xa3\x6d\x2d\x86\xee\x6b\x31\xb8\xd9\x68\ +\x76\x05\x3c\x46\xa7\xc7\x30\x0e\xb7\xb9\x8d\x13\xfd\x2b\xd9\x4b\ +\x71\x40\x55\xaa\x4f\x95\x12\x25\x75\x77\xb4\xed\x54\xa4\x5d\xfe\ +\xe9\xe7\x33\x6e\x8f\xc7\xd5\xf2\xf7\x7c\xa1\xc0\xea\xbb\x8b\x93\ +\x57\x27\xbe\xbc\x9a\x8b\xcd\xa6\xda\xec\x9c\x32\x34\x97\xc8\xda\ +\xb6\x9d\xcf\xe5\xec\x6b\x33\xc9\xf4\xf3\xd5\xea\x57\x04\x18\x2c\ +\xbb\xa2\x89\x8e\xf6\xf3\xdb\x43\xde\x87\xbf\xfe\xe6\xe5\x3d\x12\ +\xaa\xe6\xd4\x62\xb3\x4b\xf1\x63\x4f\x7e\xea\x14\xb6\x3a\x12\x4a\ +\xa7\x27\x01\xe0\x47\xa0\x58\x37\x40\xd8\x6c\x7f\x1d\xc4\xef\xde\ +\x35\xd0\x2f\xc4\xac\x6c\xd0\xea\xdd\x2c\xbb\x88\xa8\x22\x84\x6d\ +\x53\x4e\x03\x08\x82\x0b\x4c\x68\x02\x78\x3f\x00\x08\x88\xb3\xd3\ +\xf3\xa9\xf1\x5a\xf0\xff\xba\xaa\x9e\x81\x88\x69\xf6\x2a\x52\x07\ +\x89\xd8\xb9\xbe\x81\x49\x03\xf3\x5c\x74\x21\x7d\xe1\x2f\x5e\x9f\ +\xef\x71\xe8\xd4\xc5\xb4\x71\xe0\x98\x29\xaf\xc1\xfe\xe1\xfa\xfc\ +\xe2\x6f\x95\x7a\x54\x7d\x8a\x99\x8a\xdb\x24\x09\x0f\x83\xd6\x3f\ +\x20\x24\x88\x55\x11\x47\x23\xa6\x7f\x57\x74\x21\x35\x06\x00\xe1\ +\x8e\xc0\x39\x56\xdc\x2b\x04\x4d\x11\x00\x28\x30\x00\x56\xc2\xd6\ +\x19\xa2\xea\xe7\xa5\xe6\x2d\xb8\x97\x2c\xcb\xd3\xaa\x67\x1d\x1f\ +\x2b\x0d\x51\xa1\x00\x26\x44\x68\xcd\x3e\x35\xbd\xb8\xb8\xbc\xd9\ +\x5a\x75\x01\x44\x4c\xff\x18\x98\x5f\x5c\x0f\x08\xba\xec\xb4\x31\ +\x92\x23\x1e\x27\x52\xc2\x0d\xfd\xd4\x6d\xd8\xe3\x82\x31\xb8\xe1\ +\xc1\x27\xd1\xf9\xf4\xf9\x86\x00\x74\x75\x75\x39\xc5\xca\x4a\x73\ +\x69\x9d\x95\x72\x25\x91\x48\xac\x62\xe3\x21\xb3\x83\xc1\xa0\xd1\ +\x0c\xac\x3f\xdb\x9a\x37\xb9\x3c\x35\x85\xb5\x7a\xea\x6f\x69\x4b\ +\xff\x6f\xfd\x09\xec\x59\x33\x5e\xbb\x11\x8b\x6a\x00\x00\x00\x00\ +\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x17\x50\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x03\xe8\x00\x00\x00\x44\x08\x06\x00\x00\x00\x12\x95\x1f\xec\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ +\x01\x00\x9a\x9c\x18\x00\x00\x0a\x4f\x69\x43\x43\x50\x50\x68\x6f\ +\x74\x6f\x73\x68\x6f\x70\x20\x49\x43\x43\x20\x70\x72\x6f\x66\x69\ +\x6c\x65\x00\x00\x78\xda\x9d\x53\x67\x54\x53\xe9\x16\x3d\xf7\xde\ +\xf4\x42\x4b\x88\x80\x94\x4b\x6f\x52\x15\x08\x20\x52\x42\x8b\x80\ +\x14\x91\x26\x2a\x21\x09\x10\x4a\x88\x21\xa1\xd9\x15\x51\xc1\x11\ +\x45\x45\x04\x1b\xc8\xa0\x88\x03\x8e\x8e\x80\x8c\x15\x51\x2c\x0c\ +\x8a\x0a\xd8\x07\xe4\x21\xa2\x8e\x83\xa3\x88\x8a\xca\xfb\xe1\x7b\ +\xa3\x6b\xd6\xbc\xf7\xe6\xcd\xfe\xb5\xd7\x3e\xe7\xac\xf3\x9d\xb3\ +\xcf\x07\xc0\x08\x0c\x96\x48\x33\x51\x35\x80\x0c\xa9\x42\x1e\x11\ +\xe0\x83\xc7\xc4\xc6\xe1\xe4\x2e\x40\x81\x0a\x24\x70\x00\x10\x08\ +\xb3\x64\x21\x73\xfd\x23\x01\x00\xf8\x7e\x3c\x3c\x2b\x22\xc0\x07\ +\xbe\x00\x01\x78\xd3\x0b\x08\x00\xc0\x4d\x9b\xc0\x30\x1c\x87\xff\ +\x0f\xea\x42\x99\x5c\x01\x80\x84\x01\xc0\x74\x91\x38\x4b\x08\x80\ +\x14\x00\x40\x7a\x8e\x42\xa6\x00\x40\x46\x01\x80\x9d\x98\x26\x53\ +\x00\xa0\x04\x00\x60\xcb\x63\x62\xe3\x00\x50\x2d\x00\x60\x27\x7f\ +\xe6\xd3\x00\x80\x9d\xf8\x99\x7b\x01\x00\x5b\x94\x21\x15\x01\xa0\ +\x91\x00\x20\x13\x65\x88\x44\x00\x68\x3b\x00\xac\xcf\x56\x8a\x45\ +\x00\x58\x30\x00\x14\x66\x4b\xc4\x39\x00\xd8\x2d\x00\x30\x49\x57\ +\x66\x48\x00\xb0\xb7\x00\xc0\xce\x10\x0b\xb2\x00\x08\x0c\x00\x30\ +\x51\x88\x85\x29\x00\x04\x7b\x00\x60\xc8\x23\x23\x78\x00\x84\x99\ +\x00\x14\x46\xf2\x57\x3c\xf1\x2b\xae\x10\xe7\x2a\x00\x00\x78\x99\ +\xb2\x3c\xb9\x24\x39\x45\x81\x5b\x08\x2d\x71\x07\x57\x57\x2e\x1e\ +\x28\xce\x49\x17\x2b\x14\x36\x61\x02\x61\x9a\x40\x2e\xc2\x79\x99\ +\x19\x32\x81\x34\x0f\xe0\xf3\xcc\x00\x00\xa0\x91\x15\x11\xe0\x83\ +\xf3\xfd\x78\xce\x0e\xae\xce\xce\x36\x8e\xb6\x0e\x5f\x2d\xea\xbf\ +\x06\xff\x22\x62\x62\xe3\xfe\xe5\xcf\xab\x70\x40\x00\x00\xe1\x74\ +\x7e\xd1\xfe\x2c\x2f\xb3\x1a\x80\x3b\x06\x80\x6d\xfe\xa2\x25\xee\ +\x04\x68\x5e\x0b\xa0\x75\xf7\x8b\x66\xb2\x0f\x40\xb5\x00\xa0\xe9\ +\xda\x57\xf3\x70\xf8\x7e\x3c\x3c\x45\xa1\x90\xb9\xd9\xd9\xe5\xe4\ +\xe4\xd8\x4a\xc4\x42\x5b\x61\xca\x57\x7d\xfe\x67\xc2\x5f\xc0\x57\ +\xfd\x6c\xf9\x7e\x3c\xfc\xf7\xf5\xe0\xbe\xe2\x24\x81\x32\x5d\x81\ +\x47\x04\xf8\xe0\xc2\xcc\xf4\x4c\xa5\x1c\xcf\x92\x09\x84\x62\xdc\ +\xe6\x8f\x47\xfc\xb7\x0b\xff\xfc\x1d\xd3\x22\xc4\x49\x62\xb9\x58\ +\x2a\x14\xe3\x51\x12\x71\x8e\x44\x9a\x8c\xf3\x32\xa5\x22\x89\x42\ +\x92\x29\xc5\x25\xd2\xff\x64\xe2\xdf\x2c\xfb\x03\x3e\xdf\x35\x00\ +\xb0\x6a\x3e\x01\x7b\x91\x2d\xa8\x5d\x63\x03\xf6\x4b\x27\x10\x58\ +\x74\xc0\xe2\xf7\x00\x00\xf2\xbb\x6f\xc1\xd4\x28\x08\x03\x80\x68\ +\x83\xe1\xcf\x77\xff\xef\x3f\xfd\x47\xa0\x25\x00\x80\x66\x49\x92\ +\x71\x00\x00\x5e\x44\x24\x2e\x54\xca\xb3\x3f\xc7\x08\x00\x00\x44\ +\xa0\x81\x2a\xb0\x41\x1b\xf4\xc1\x18\x2c\xc0\x06\x1c\xc1\x05\xdc\ +\xc1\x0b\xfc\x60\x36\x84\x42\x24\xc4\xc2\x42\x10\x42\x0a\x64\x80\ +\x1c\x72\x60\x29\xac\x82\x42\x28\x86\xcd\xb0\x1d\x2a\x60\x2f\xd4\ +\x40\x1d\x34\xc0\x51\x68\x86\x93\x70\x0e\x2e\xc2\x55\xb8\x0e\x3d\ +\x70\x0f\xfa\x61\x08\x9e\xc1\x28\xbc\x81\x09\x04\x41\xc8\x08\x13\ +\x61\x21\xda\x88\x01\x62\x8a\x58\x23\x8e\x08\x17\x99\x85\xf8\x21\ +\xc1\x48\x04\x12\x8b\x24\x20\xc9\x88\x14\x51\x22\x4b\x91\x35\x48\ +\x31\x52\x8a\x54\x20\x55\x48\x1d\xf2\x3d\x72\x02\x39\x87\x5c\x46\ +\xba\x91\x3b\xc8\x00\x32\x82\xfc\x86\xbc\x47\x31\x94\x81\xb2\x51\ +\x3d\xd4\x0c\xb5\x43\xb9\xa8\x37\x1a\x84\x46\xa2\x0b\xd0\x64\x74\ +\x31\x9a\x8f\x16\xa0\x9b\xd0\x72\xb4\x1a\x3d\x8c\x36\xa1\xe7\xd0\ +\xab\x68\x0f\xda\x8f\x3e\x43\xc7\x30\xc0\xe8\x18\x07\x33\xc4\x6c\ +\x30\x2e\xc6\xc3\x42\xb1\x38\x2c\x09\x93\x63\xcb\xb1\x22\xac\x0c\ +\xab\xc6\x1a\xb0\x56\xac\x03\xbb\x89\xf5\x63\xcf\xb1\x77\x04\x12\ +\x81\x45\xc0\x09\x36\x04\x77\x42\x20\x61\x1e\x41\x48\x58\x4c\x58\ +\x4e\xd8\x48\xa8\x20\x1c\x24\x34\x11\xda\x09\x37\x09\x03\x84\x51\ +\xc2\x27\x22\x93\xa8\x4b\xb4\x26\xba\x11\xf9\xc4\x18\x62\x32\x31\ +\x87\x58\x48\x2c\x23\xd6\x12\x8f\x13\x2f\x10\x7b\x88\x43\xc4\x37\ +\x24\x12\x89\x43\x32\x27\xb9\x90\x02\x49\xb1\xa4\x54\xd2\x12\xd2\ +\x46\xd2\x6e\x52\x23\xe9\x2c\xa9\x9b\x34\x48\x1a\x23\x93\xc9\xda\ +\x64\x6b\xb2\x07\x39\x94\x2c\x20\x2b\xc8\x85\xe4\x9d\xe4\xc3\xe4\ +\x33\xe4\x1b\xe4\x21\xf2\x5b\x0a\x9d\x62\x40\x71\xa4\xf8\x53\xe2\ +\x28\x52\xca\x6a\x4a\x19\xe5\x10\xe5\x34\xe5\x06\x65\x98\x32\x41\ +\x55\xa3\x9a\x52\xdd\xa8\xa1\x54\x11\x35\x8f\x5a\x42\xad\xa1\xb6\ +\x52\xaf\x51\x87\xa8\x13\x34\x75\x9a\x39\xcd\x83\x16\x49\x4b\xa5\ +\xad\xa2\x95\xd3\x1a\x68\x17\x68\xf7\x69\xaf\xe8\x74\xba\x11\xdd\ +\x95\x1e\x4e\x97\xd0\x57\xd2\xcb\xe9\x47\xe8\x97\xe8\x03\xf4\x77\ +\x0c\x0d\x86\x15\x83\xc7\x88\x67\x28\x19\x9b\x18\x07\x18\x67\x19\ +\x77\x18\xaf\x98\x4c\xa6\x19\xd3\x8b\x19\xc7\x54\x30\x37\x31\xeb\ +\x98\xe7\x99\x0f\x99\x6f\x55\x58\x2a\xb6\x2a\x7c\x15\x91\xca\x0a\ +\x95\x4a\x95\x26\x95\x1b\x2a\x2f\x54\xa9\xaa\xa6\xaa\xde\xaa\x0b\ +\x55\xf3\x55\xcb\x54\x8f\xa9\x5e\x53\x7d\xae\x46\x55\x33\x53\xe3\ +\xa9\x09\xd4\x96\xab\x55\xaa\x9d\x50\xeb\x53\x1b\x53\x67\xa9\x3b\ +\xa8\x87\xaa\x67\xa8\x6f\x54\x3f\xa4\x7e\x59\xfd\x89\x06\x59\xc3\ +\x4c\xc3\x4f\x43\xa4\x51\xa0\xb1\x5f\xe3\xbc\xc6\x20\x0b\x63\x19\ +\xb3\x78\x2c\x21\x6b\x0d\xab\x86\x75\x81\x35\xc4\x26\xb1\xcd\xd9\ +\x7c\x76\x2a\xbb\x98\xfd\x1d\xbb\x8b\x3d\xaa\xa9\xa1\x39\x43\x33\ +\x4a\x33\x57\xb3\x52\xf3\x94\x66\x3f\x07\xe3\x98\x71\xf8\x9c\x74\ +\x4e\x09\xe7\x28\xa7\x97\xf3\x7e\x8a\xde\x14\xef\x29\xe2\x29\x1b\ +\xa6\x34\x4c\xb9\x31\x65\x5c\x6b\xaa\x96\x97\x96\x58\xab\x48\xab\ +\x51\xab\x47\xeb\xbd\x36\xae\xed\xa7\x9d\xa6\xbd\x45\xbb\x59\xfb\ +\x81\x0e\x41\xc7\x4a\x27\x5c\x27\x47\x67\x8f\xce\x05\x9d\xe7\x53\ +\xd9\x53\xdd\xa7\x0a\xa7\x16\x4d\x3d\x3a\xf5\xae\x2e\xaa\x6b\xa5\ +\x1b\xa1\xbb\x44\x77\xbf\x6e\xa7\xee\x98\x9e\xbe\x5e\x80\x9e\x4c\ +\x6f\xa7\xde\x79\xbd\xe7\xfa\x1c\x7d\x2f\xfd\x54\xfd\x6d\xfa\xa7\ +\xf5\x47\x0c\x58\x06\xb3\x0c\x24\x06\xdb\x0c\xce\x18\x3c\xc5\x35\ +\x71\x6f\x3c\x1d\x2f\xc7\xdb\xf1\x51\x43\x5d\xc3\x40\x43\xa5\x61\ +\x95\x61\x97\xe1\x84\x91\xb9\xd1\x3c\xa3\xd5\x46\x8d\x46\x0f\x8c\ +\x69\xc6\x5c\xe3\x24\xe3\x6d\xc6\x6d\xc6\xa3\x26\x06\x26\x21\x26\ +\x4b\x4d\xea\x4d\xee\x9a\x52\x4d\xb9\xa6\x29\xa6\x3b\x4c\x3b\x4c\ +\xc7\xcd\xcc\xcd\xa2\xcd\xd6\x99\x35\x9b\x3d\x31\xd7\x32\xe7\x9b\ +\xe7\x9b\xd7\x9b\xdf\xb7\x60\x5a\x78\x5a\x2c\xb6\xa8\xb6\xb8\x65\ +\x49\xb2\xe4\x5a\xa6\x59\xee\xb6\xbc\x6e\x85\x5a\x39\x59\xa5\x58\ +\x55\x5a\x5d\xb3\x46\xad\x9d\xad\x25\xd6\xbb\xad\xbb\xa7\x11\xa7\ +\xb9\x4e\x93\x4e\xab\x9e\xd6\x67\xc3\xb0\xf1\xb6\xc9\xb6\xa9\xb7\ +\x19\xb0\xe5\xd8\x06\xdb\xae\xb6\x6d\xb6\x7d\x61\x67\x62\x17\x67\ +\xb7\xc5\xae\xc3\xee\x93\xbd\x93\x7d\xba\x7d\x8d\xfd\x3d\x07\x0d\ +\x87\xd9\x0e\xab\x1d\x5a\x1d\x7e\x73\xb4\x72\x14\x3a\x56\x3a\xde\ +\x9a\xce\x9c\xee\x3f\x7d\xc5\xf4\x96\xe9\x2f\x67\x58\xcf\x10\xcf\ +\xd8\x33\xe3\xb6\x13\xcb\x29\xc4\x69\x9d\x53\x9b\xd3\x47\x67\x17\ +\x67\xb9\x73\x83\xf3\x88\x8b\x89\x4b\x82\xcb\x2e\x97\x3e\x2e\x9b\ +\x1b\xc6\xdd\xc8\xbd\xe4\x4a\x74\xf5\x71\x5d\xe1\x7a\xd2\xf5\x9d\ +\x9b\xb3\x9b\xc2\xed\xa8\xdb\xaf\xee\x36\xee\x69\xee\x87\xdc\x9f\ +\xcc\x34\x9f\x29\x9e\x59\x33\x73\xd0\xc3\xc8\x43\xe0\x51\xe5\xd1\ +\x3f\x0b\x9f\x95\x30\x6b\xdf\xac\x7e\x4f\x43\x4f\x81\x67\xb5\xe7\ +\x23\x2f\x63\x2f\x91\x57\xad\xd7\xb0\xb7\xa5\x77\xaa\xf7\x61\xef\ +\x17\x3e\xf6\x3e\x72\x9f\xe3\x3e\xe3\x3c\x37\xde\x32\xde\x59\x5f\ +\xcc\x37\xc0\xb7\xc8\xb7\xcb\x4f\xc3\x6f\x9e\x5f\x85\xdf\x43\x7f\ +\x23\xff\x64\xff\x7a\xff\xd1\x00\xa7\x80\x25\x01\x67\x03\x89\x81\ +\x41\x81\x5b\x02\xfb\xf8\x7a\x7c\x21\xbf\x8e\x3f\x3a\xdb\x65\xf6\ +\xb2\xd9\xed\x41\x8c\xa0\xb9\x41\x15\x41\x8f\x82\xad\x82\xe5\xc1\ +\xad\x21\x68\xc8\xec\x90\xad\x21\xf7\xe7\x98\xce\x91\xce\x69\x0e\ +\x85\x50\x7e\xe8\xd6\xd0\x07\x61\xe6\x61\x8b\xc3\x7e\x0c\x27\x85\ +\x87\x85\x57\x86\x3f\x8e\x70\x88\x58\x1a\xd1\x31\x97\x35\x77\xd1\ +\xdc\x43\x73\xdf\x44\xfa\x44\x96\x44\xde\x9b\x67\x31\x4f\x39\xaf\ +\x2d\x4a\x35\x2a\x3e\xaa\x2e\x6a\x3c\xda\x37\xba\x34\xba\x3f\xc6\ +\x2e\x66\x59\xcc\xd5\x58\x9d\x58\x49\x6c\x4b\x1c\x39\x2e\x2a\xae\ +\x36\x6e\x6c\xbe\xdf\xfc\xed\xf3\x87\xe2\x9d\xe2\x0b\xe3\x7b\x17\ +\x98\x2f\xc8\x5d\x70\x79\xa1\xce\xc2\xf4\x85\xa7\x16\xa9\x2e\x12\ +\x2c\x3a\x96\x40\x4c\x88\x4e\x38\x94\xf0\x41\x10\x2a\xa8\x16\x8c\ +\x25\xf2\x13\x77\x25\x8e\x0a\x79\xc2\x1d\xc2\x67\x22\x2f\xd1\x36\ +\xd1\x88\xd8\x43\x5c\x2a\x1e\x4e\xf2\x48\x2a\x4d\x7a\x92\xec\x91\ +\xbc\x35\x79\x24\xc5\x33\xa5\x2c\xe5\xb9\x84\x27\xa9\x90\xbc\x4c\ +\x0d\x4c\xdd\x9b\x3a\x9e\x16\x9a\x76\x20\x6d\x32\x3d\x3a\xbd\x31\ +\x83\x92\x91\x90\x71\x42\xaa\x21\x4d\x93\xb6\x67\xea\x67\xe6\x66\ +\x76\xcb\xac\x65\x85\xb2\xfe\xc5\x6e\x8b\xb7\x2f\x1e\x95\x07\xc9\ +\x6b\xb3\x90\xac\x05\x59\x2d\x0a\xb6\x42\xa6\xe8\x54\x5a\x28\xd7\ +\x2a\x07\xb2\x67\x65\x57\x66\xbf\xcd\x89\xca\x39\x96\xab\x9e\x2b\ +\xcd\xed\xcc\xb3\xca\xdb\x90\x37\x9c\xef\x9f\xff\xed\x12\xc2\x12\ +\xe1\x92\xb6\xa5\x86\x4b\x57\x2d\x1d\x58\xe6\xbd\xac\x6a\x39\xb2\ +\x3c\x71\x79\xdb\x0a\xe3\x15\x05\x2b\x86\x56\x06\xac\x3c\xb8\x8a\ +\xb6\x2a\x6d\xd5\x4f\xab\xed\x57\x97\xae\x7e\xbd\x26\x7a\x4d\x6b\ +\x81\x5e\xc1\xca\x82\xc1\xb5\x01\x6b\xeb\x0b\x55\x0a\xe5\x85\x7d\ +\xeb\xdc\xd7\xed\x5d\x4f\x58\x2f\x59\xdf\xb5\x61\xfa\x86\x9d\x1b\ +\x3e\x15\x89\x8a\xae\x14\xdb\x17\x97\x15\x7f\xd8\x28\xdc\x78\xe5\ +\x1b\x87\x6f\xca\xbf\x99\xdc\x94\xb4\xa9\xab\xc4\xb9\x64\xcf\x66\ +\xd2\x66\xe9\xe6\xde\x2d\x9e\x5b\x0e\x96\xaa\x97\xe6\x97\x0e\x6e\ +\x0d\xd9\xda\xb4\x0d\xdf\x56\xb4\xed\xf5\xf6\x45\xdb\x2f\x97\xcd\ +\x28\xdb\xbb\x83\xb6\x43\xb9\xa3\xbf\x3c\xb8\xbc\x65\xa7\xc9\xce\ +\xcd\x3b\x3f\x54\xa4\x54\xf4\x54\xfa\x54\x36\xee\xd2\xdd\xb5\x61\ +\xd7\xf8\x6e\xd1\xee\x1b\x7b\xbc\xf6\x34\xec\xd5\xdb\x5b\xbc\xf7\ +\xfd\x3e\xc9\xbe\xdb\x55\x01\x55\x4d\xd5\x66\xd5\x65\xfb\x49\xfb\ +\xb3\xf7\x3f\xae\x89\xaa\xe9\xf8\x96\xfb\x6d\x5d\xad\x4e\x6d\x71\ +\xed\xc7\x03\xd2\x03\xfd\x07\x23\x0e\xb6\xd7\xb9\xd4\xd5\x1d\xd2\ +\x3d\x54\x52\x8f\xd6\x2b\xeb\x47\x0e\xc7\x1f\xbe\xfe\x9d\xef\x77\ +\x2d\x0d\x36\x0d\x55\x8d\x9c\xc6\xe2\x23\x70\x44\x79\xe4\xe9\xf7\ +\x09\xdf\xf7\x1e\x0d\x3a\xda\x76\x8c\x7b\xac\xe1\x07\xd3\x1f\x76\ +\x1d\x67\x1d\x2f\x6a\x42\x9a\xf2\x9a\x46\x9b\x53\x9a\xfb\x5b\x62\ +\x5b\xba\x4f\xcc\x3e\xd1\xd6\xea\xde\x7a\xfc\x47\xdb\x1f\x0f\x9c\ +\x34\x3c\x59\x79\x4a\xf3\x54\xc9\x69\xda\xe9\x82\xd3\x93\x67\xf2\ +\xcf\x8c\x9d\x95\x9d\x7d\x7e\x2e\xf9\xdc\x60\xdb\xa2\xb6\x7b\xe7\ +\x63\xce\xdf\x6a\x0f\x6f\xef\xba\x10\x74\xe1\xd2\x45\xff\x8b\xe7\ +\x3b\xbc\x3b\xce\x5c\xf2\xb8\x74\xf2\xb2\xdb\xe5\x13\x57\xb8\x57\ +\x9a\xaf\x3a\x5f\x6d\xea\x74\xea\x3c\xfe\x93\xd3\x4f\xc7\xbb\x9c\ +\xbb\x9a\xae\xb9\x5c\x6b\xb9\xee\x7a\xbd\xb5\x7b\x66\xf7\xe9\x1b\ +\x9e\x37\xce\xdd\xf4\xbd\x79\xf1\x16\xff\xd6\xd5\x9e\x39\x3d\xdd\ +\xbd\xf3\x7a\x6f\xf7\xc5\xf7\xf5\xdf\x16\xdd\x7e\x72\x27\xfd\xce\ +\xcb\xbb\xd9\x77\x27\xee\xad\xbc\x4f\xbc\x5f\xf4\x40\xed\x41\xd9\ +\x43\xdd\x87\xd5\x3f\x5b\xfe\xdc\xd8\xef\xdc\x7f\x6a\xc0\x77\xa0\ +\xf3\xd1\xdc\x47\xf7\x06\x85\x83\xcf\xfe\x91\xf5\x8f\x0f\x43\x05\ +\x8f\x99\x8f\xcb\x86\x0d\x86\xeb\x9e\x38\x3e\x39\x39\xe2\x3f\x72\ +\xfd\xe9\xfc\xa7\x43\xcf\x64\xcf\x26\x9e\x17\xfe\xa2\xfe\xcb\xae\ +\x17\x16\x2f\x7e\xf8\xd5\xeb\xd7\xce\xd1\x98\xd1\xa1\x97\xf2\x97\ +\x93\xbf\x6d\x7c\xa5\xfd\xea\xc0\xeb\x19\xaf\xdb\xc6\xc2\xc6\x1e\ +\xbe\xc9\x78\x33\x31\x5e\xf4\x56\xfb\xed\xc1\x77\xdc\x77\x1d\xef\ +\xa3\xdf\x0f\x4f\xe4\x7c\x20\x7f\x28\xff\x68\xf9\xb1\xf5\x53\xd0\ +\xa7\xfb\x93\x19\x93\x93\xff\x04\x03\x98\xf3\xfc\x63\x33\x2d\xdb\ +\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\x25\x00\x00\x80\x83\ +\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00\x75\x30\x00\x00\xea\x60\ +\x00\x00\x3a\x98\x00\x00\x17\x6f\x92\x5f\xc5\x46\x00\x00\x0c\x7b\ +\x49\x44\x41\x54\x78\xda\xec\xdd\x5b\x6c\xd4\x55\x02\xc7\xf1\xdf\ +\x74\xee\x33\x5c\x86\xd6\x34\x81\x8c\x98\x5e\xd4\xd4\xaa\x54\xdb\ +\x05\xd2\x58\x62\x25\x1b\x21\x2b\x98\xac\x84\xb0\x66\xab\xae\xc9\ +\xee\x1a\x62\xb2\x64\xb3\x3e\xe8\x83\xc9\xf2\x20\xd9\x68\x8c\x24\ +\xae\x9a\xdd\x07\xc4\xcb\x66\xd7\x60\xb2\xca\xee\x56\xa2\x02\x01\ +\x43\x50\x41\x51\xac\x44\x43\x5b\x02\x0d\x93\x6d\xb6\x30\x40\xa7\ +\x73\xeb\x74\xf6\x61\x98\xff\xdc\xaf\x2d\x9d\x52\xbe\x9f\x84\x84\ +\xd2\x99\xff\xe5\x9c\x33\x13\x7e\xe7\x9c\xff\x39\xa6\xce\xce\xce\ +\xb8\x00\x00\xd3\x16\x0c\x06\x29\x04\x00\x00\x00\x54\xad\x8e\x22\ +\x00\x00\x00\x00\x00\x80\x80\x0e\x00\x00\x00\x00\x00\x08\xe8\x00\ +\x00\x00\x00\x00\x10\xd0\x01\x00\x00\x00\x00\x00\x01\x1d\x00\x00\ +\x00\x00\x00\x02\x3a\x00\x00\x00\x00\x00\x20\xa0\x03\x00\x00\x00\ +\x00\x40\x40\x07\x00\x00\x00\x00\x00\x04\x74\x00\x00\x00\x00\x00\ +\x08\xe8\x00\x00\x00\x00\x00\x80\x80\x0e\x00\x00\x00\x00\x00\x01\ +\x1d\x00\x00\x00\x00\x00\x10\xd0\x01\x00\x00\x00\x00\x20\xa0\x03\ +\x00\x00\x00\x00\x00\x02\x3a\x00\x00\x00\x00\x00\x04\x74\x00\x00\ +\x00\x00\x00\x40\x40\x07\x00\x00\x00\x00\x80\x80\x0e\x00\x00\x00\ +\x00\x00\x08\xe8\x00\x00\x00\x00\x00\x10\xd0\x01\x00\x00\x00\x00\ +\x00\x01\x1d\x00\x00\x00\x00\x00\x02\x3a\x00\x00\x00\x00\x00\x20\ +\xa0\x03\x00\x00\x00\x00\x40\x40\x07\x00\x00\x00\x00\x00\x04\x74\ +\x00\x00\x00\x00\x00\x08\xe8\x00\x00\x00\x00\x00\xa0\x0a\x96\x6b\ +\x76\x60\x6f\x87\x4c\x8e\x85\x15\xbd\x27\x1e\xba\xa2\xc9\x91\x13\ +\xc6\xcf\xe6\x86\x26\xd5\x2d\xf1\x4a\x92\x26\xcf\x0f\x28\x3e\x71\ +\x21\xe3\xf5\x26\x57\xbd\x2c\xcb\xda\x8d\x9f\xa3\xa7\x0f\x97\x75\ +\x9e\xf4\xe3\x66\x9f\xb3\x9a\xeb\x2f\x74\x8c\x72\x98\x5c\xf5\xb2\ +\x36\xad\x92\xa5\xb1\x49\x92\x34\x75\x79\x54\x93\xff\x3d\x5d\xd6\ +\xf1\xac\xad\x3d\x65\x9f\x67\xea\xe2\x88\x62\x63\xc3\x65\x5f\x53\ +\x7a\xb9\xce\xd4\xbd\x26\xcb\xbd\xd4\x31\xd2\xcf\x5f\xce\xf9\xd2\ +\xeb\x2a\x5f\x3b\x99\x89\x36\x9a\x7d\x1d\xc5\xca\xa8\x92\xb2\x4e\ +\xd6\x61\xf6\x7b\x2a\xa9\xdb\x72\xce\x9b\xaf\x9d\x45\xcf\x1c\x2f\ +\x79\x9d\xc5\xee\x73\xba\xed\x01\x00\x00\x00\xc0\x2c\x05\x74\xf7\ +\x9a\x27\x64\x69\x6c\xae\xe8\x3d\x93\xa3\x43\xba\xf4\xb7\x6d\xc6\ +\xcf\xf6\xbb\xd7\xcb\xb9\x62\x9d\x24\xe9\xf2\xbf\x5e\xcc\x09\xe0\ +\x96\x65\xed\x5a\xf4\xd0\x33\xc6\xcf\x97\xf6\x94\x17\x16\x5c\xbd\ +\xbf\x91\xcd\xdb\x9e\xf7\x9c\xd5\x5c\x7f\xa1\x63\x94\xbe\x8e\xad\ +\x72\xdc\x71\xbf\x4c\x56\x47\xde\x63\x06\x0e\xbd\x59\xf4\x7e\xd2\ +\xef\xbd\x94\xe0\x37\x1f\x69\xe2\xc0\x6b\xe5\x35\x8a\xac\x72\xcd\ +\x09\x65\xd1\x90\x22\x67\xbf\x53\xf0\xc8\xdb\x65\x07\x51\x49\x5a\ +\xf4\xf3\x3f\xaa\xce\xed\x51\x3c\x1a\xd2\x85\x3f\x6f\x2e\x7c\xfc\ +\x89\x0b\x5a\xd0\xfb\x6b\xd5\xb9\x3d\x92\x24\xff\xdb\xbf\x2b\x1a\ +\x3c\x17\x3d\xfc\x9c\x4c\x56\x87\xe2\xd1\x90\x2e\xee\x7a\xea\x9a\ +\xb4\xd1\xec\x3a\x2e\xb7\x8c\x02\x9f\xbe\x5a\xb4\xc3\x20\x79\x8c\ +\xec\xfa\xa9\xa4\x6e\x8b\xd5\xb1\xc9\x55\x2f\x57\xcf\x93\x72\xb4\ +\xad\xc9\x7d\x43\xcf\x63\x9a\x1c\x1d\xd2\xf8\xbe\x9d\x05\xcb\xb7\ +\xd4\x7d\x56\x72\xaf\x00\x00\x00\x00\x6a\x14\xd0\x27\xc7\x46\x72\ +\xc3\x94\xcd\x29\xb3\x67\xa9\x24\x29\xe6\xf7\x29\x1e\x09\x96\x7c\ +\x4f\x25\x1c\xf7\x6c\xd4\x78\x89\x80\x6e\x6e\x68\x32\xc2\x79\x39\ +\xe2\xd1\x90\x62\x17\xcf\x57\x7c\xaf\xa5\x2c\xdc\xb4\x23\xe3\x3a\ +\x26\x47\x87\x24\x49\x75\xee\x7a\xd5\xb9\x3d\xb2\x34\x36\x6b\xd1\ +\xc3\xcf\xe9\xf2\x07\x2f\x94\xec\x74\x98\x0a\xf8\x35\x15\x28\x1e\ +\x8c\xa6\x2e\x8f\x56\x55\xa6\xe9\xf5\x94\xac\x3f\x93\xd5\x21\x7b\ +\x4b\x97\x6c\xcb\xef\xd4\xf8\xc1\x5d\x8a\x0c\xf4\x97\x6e\x68\xde\ +\x0e\x23\x70\x9b\xac\x0e\x39\x3a\x37\x29\x74\x7c\x4f\xe1\xb0\xf9\ +\xd5\x87\x72\xf7\x3c\x26\x49\x72\x76\xf7\x69\x7c\xef\xf6\xbc\xaf\ +\x73\xae\xda\x62\x74\x70\x84\xbe\x3f\x58\x55\x40\x9c\x6e\x1d\x17\ +\x2f\xa3\x37\xca\xaa\xc3\x7c\x1d\x02\xd9\x92\x6d\xa3\xd0\xef\xb3\ +\xeb\xd8\xe4\xaa\xd7\xe2\xcd\x3b\x8c\xcf\x5c\xfa\x7d\x9a\x97\x2c\ +\x93\xc9\xea\x90\xa5\xb1\x59\x8b\xb7\xfc\xa9\xac\x6b\xcc\xfe\xcc\ +\x26\xaf\x27\x79\xaf\x96\x86\x1d\xba\xf4\xde\xb3\x84\x74\x00\x00\ +\x00\x60\xae\x05\xf4\xc0\xbe\x97\x72\xfe\xcd\xda\xda\x63\x8c\xc6\ +\x05\x3e\x7b\xa7\xec\x29\xe9\xe5\xb2\x2d\xbf\xb3\xf4\x6b\x6e\x5f\ +\x53\x59\x40\xbd\x78\xbe\xaa\xd1\xf1\x62\x5c\xbd\x5b\x8d\x70\x1e\ +\x19\x19\xd0\xf8\x7f\x5e\xcc\x08\x35\x8e\xce\x4d\x72\xad\xde\x2c\ +\x93\xd5\xa1\x45\x0f\x3f\xa7\x8b\xbb\x9e\x2a\x1a\x7a\xc2\xa7\x8f\ +\x96\x3d\x3a\x5e\x71\x3d\x66\xd5\x93\xc9\x55\x2f\x47\xc7\x06\x39\ +\xef\xf9\x99\x4c\x56\x87\x16\xdc\xff\x2b\x5d\xbe\xe4\x2b\x19\xee\ +\x1c\xf7\x6c\xcc\x6c\x0b\x4d\x9d\x45\x03\x7a\xe8\xf8\x1e\x39\xef\ +\xdd\xa8\x3a\xb7\x47\xf6\x96\x2e\x05\x1b\x9a\x72\x46\x79\x4d\xae\ +\x7a\x39\xee\xb8\xdf\xe8\xa4\xa8\xb6\x0c\xa6\x5b\xc7\xf9\xca\xc8\ +\xb9\x6a\x8b\x9c\x2b\xd6\x19\x75\x58\x69\x48\xcf\x77\x3d\xae\xde\ +\xad\xc6\x8c\x92\x72\xae\xd7\xbd\xf6\x69\x23\x9c\x87\x07\x8f\xe5\ +\x8c\x70\x3b\xbb\x1f\x97\x6b\xe5\x23\x32\x59\x1d\x5a\xb8\xfe\xf7\ +\xf2\xbf\xbb\xad\x68\x3b\xcb\xf7\x99\x35\xb9\xea\xe5\x5e\xfb\xb4\ +\xec\x2d\x5d\x32\x7b\x96\xca\xd5\xf3\x64\xde\xcf\x3e\x00\x00\x00\ +\x80\xd2\xe6\xd5\x22\x71\xc9\x91\xd9\xa2\x41\xb1\x7d\x6d\x6d\xaf\ +\xd1\x55\x6f\x84\xac\x98\xdf\xa7\x2b\x7b\x72\x47\x1c\x43\xc7\xf7\ +\x68\xfc\xe0\x2e\xe3\x9e\xdc\x6b\x9f\x9e\x33\x65\x1c\x9f\xb8\xa0\ +\xe0\x91\xdd\xba\xfc\xc1\x0b\xa9\xeb\x5b\xf3\x44\xc9\x7b\x4e\x76\ +\x9e\xc4\xfc\xbe\x44\x47\x89\xb7\x5d\xe6\x86\xa6\xa2\xef\x1b\x3f\ +\xf0\xd7\xb4\x30\xd9\x97\xf3\xfb\xf4\xd1\xf3\xe0\x57\x1f\xce\xa9\ +\x32\x9a\x38\xf0\x9a\x02\x87\xdf\x2a\xbb\x8c\x66\x9a\xc5\xdb\x21\ +\x7b\x4b\x97\xa4\xab\x9d\x40\x7b\xb7\xe7\xb4\xb3\xe0\x91\xdd\x9a\ +\xf8\xe2\xfd\xc4\x17\x81\xdb\x23\xe7\xaa\x2d\x55\xb6\x87\xb7\x53\ +\x1d\x2f\x4b\x6f\xe5\x5b\x15\x00\x00\x00\xb8\xd1\x03\x7a\x64\x64\ +\x40\x92\x64\xbf\xfd\xbe\x82\xaf\xb1\xb6\xf6\x18\x53\x84\x93\xaf\ +\x9f\x6d\xf6\xb6\x07\x8c\xbf\x4f\x7c\xf9\xcf\xc2\xf7\x33\xd0\x6f\ +\x4c\x63\x2e\x67\x66\xc0\x6c\x9b\x1c\x39\xa1\xf0\xe0\xb1\x44\x18\ +\x6c\x6c\x96\xc5\xdb\x51\xf4\x9e\x93\x41\x7a\xfc\x93\xd7\x15\x8f\ +\x86\x12\xf7\x55\x62\x36\x43\xf4\xf4\x61\xa3\x0c\xec\x2d\x5d\x19\ +\x81\xde\xdc\xd0\x64\x8c\x9e\xc7\xfc\xbe\xa2\xa3\xf1\xb5\x12\x3a\ +\xbe\x27\xa3\x8c\x4a\x75\x48\xcc\x24\xdb\xad\xdd\xa9\x20\x7e\xf4\ +\x1f\x05\x5f\x17\x3c\xb2\x5b\x53\x01\x7f\xe2\x3d\xb7\xac\xa8\xea\ +\x5c\xb1\xb1\x61\xa3\x9e\x92\x23\xf6\x00\x00\x00\x00\x6e\xe0\x80\ +\x1e\x3e\x75\xa8\x64\x10\xb2\xb7\xf5\x1a\x81\x2e\x36\x76\xae\x26\ +\xd7\x69\x59\x76\x47\x46\x08\x2f\x1a\x50\x7d\x3f\x4a\x4a\x8c\xc0\ +\x16\x0b\xc0\x35\x0b\xa0\x5f\x7f\x98\x37\x10\x66\x73\xdc\xf5\xd3\ +\xc4\xfd\x8e\x0c\x68\x72\xe4\x84\x22\x67\xbf\x4b\xfc\x7b\x19\xb3\ +\x19\x92\x23\xbc\x52\xe6\x28\xba\xb3\xbb\xcf\x08\xfd\x81\xcf\xde\ +\x99\xc3\xed\xf2\x40\xaa\x8c\x2a\x7c\xbc\x62\x3a\xac\x4b\x6f\x93\ +\x94\x98\xfa\x5f\x6a\x6a\x7d\xe4\xec\xb7\x33\x16\xae\xf3\x3d\x1b\ +\x0f\x00\x00\x00\xe0\x06\x0b\xe8\xf1\xf0\xb8\x31\x12\xe8\xe8\x7a\ +\x24\xe7\xf7\x26\x57\xbd\x31\xe5\x37\x74\xf2\x63\x23\xc0\xcc\x36\ +\xf3\xc2\x7a\x23\x38\x95\x12\x3d\x77\x32\x75\xfd\x15\x6e\x59\x37\ +\x1b\xca\x79\xa6\xda\xdc\xd0\x64\x04\xbf\xe8\xf0\xf1\x44\x20\x1c\ +\xfa\x32\xd1\xf8\xdc\x9e\x92\xdb\x89\x45\x4f\x1f\x4e\xcd\x8e\xb8\ +\x3a\x8a\x6e\x6e\x68\x32\xea\x72\x72\x74\x68\xc6\xd7\x32\x98\x49\ +\x19\xcf\xa6\xdb\xdd\xb3\x7e\xfe\x52\x8b\x07\x4a\x52\x3c\x32\x91\ +\x51\x5f\x95\xb2\x78\x3b\x8c\xd5\xf0\x23\x67\xbe\xe6\x5b\x15\x00\ +\x00\x00\xa8\x92\x65\x3e\xdd\x4c\xf8\xf4\x51\x39\x57\xac\x93\xbd\ +\x75\xa5\x02\xfb\x32\x7f\xe7\xe8\xd8\x90\x08\x23\xd1\x90\xc2\xa7\ +\xf6\x17\x9d\x0a\x9f\x51\x40\x8d\xcd\x6a\xd8\x56\xf8\xf9\xe6\x4a\ +\xb6\x2f\xab\x34\x38\x95\xcb\xba\xf4\x36\xb9\x7a\xb7\x16\x2e\x97\ +\x6f\xfb\x2b\xda\x0e\x6d\x26\x25\x3b\x4b\xe2\xd1\x90\x31\x0d\x3d\ +\x32\xd0\xaf\xa9\xee\x5f\x24\x16\x80\x6b\xeb\x2d\x19\xb0\x27\x0e\ +\xfc\x45\xb6\xbe\x9d\x92\x72\x9f\x45\x4f\x1f\x61\xaf\xfa\x43\x70\ +\x8d\xea\xf8\x7a\x54\xb7\xc4\x5b\xb0\xad\xd8\xdb\x7a\x65\xbd\xf9\ +\xae\xcc\xd7\x2f\xb8\xc9\x78\x04\x63\x72\x74\x48\xa1\x13\x7b\xf9\ +\x56\x05\x00\x00\x00\x08\xe8\x89\x20\x9a\x5c\x39\xdb\xd6\xbe\x3e\ +\x63\x0a\x79\x72\x3a\x75\xe4\xec\x77\xf3\x6e\x1b\x28\x4b\x63\x73\ +\xd1\xfd\xbc\xa3\xe7\x4e\xd6\x2c\xa0\xdb\x5b\x57\x1a\xe5\x9e\x2e\ +\x72\xf6\x5b\x39\xda\xd6\xc8\xb6\xfc\x4e\x99\x5c\xf5\x45\xeb\x24\ +\x36\x36\xac\xf0\xe0\x31\xd9\x5b\xba\x8c\x91\x73\x29\xb1\x32\xf9\ +\x5c\x1e\x3d\x9f\x6f\xd2\xcb\x3e\x5b\xf6\x3e\xf1\x00\x00\x00\x00\ +\x6e\xf0\x80\x1e\x1b\x1b\x56\x64\x64\x40\x36\x6f\xbb\xec\x6d\x6b\ +\x8c\x80\x9e\xbe\x07\x77\xfa\x73\xd3\x65\x1d\xd3\xef\x2b\xfa\x8c\ +\xf3\xd4\xc5\x91\x9a\xdf\xf7\xe4\xe8\x90\xf1\xbc\xfa\x5c\xba\x46\ +\x5b\xfb\x7a\xe3\x39\xf1\xf4\x67\xb1\x25\x29\x3c\xf0\x89\x1c\x6d\ +\x6b\x12\x7b\x68\xb7\x3d\x50\x72\x91\xb7\xc0\xa7\xaf\xca\xb6\xfc\ +\x0d\xe3\x78\x92\x32\x56\x0f\x9f\x56\xbb\xb9\x0e\xea\x78\x4e\x7c\ +\xbe\xb2\xf6\x41\x4f\xee\xf9\x2e\x25\x3a\x89\x16\x6c\x78\x3e\x67\ +\x2b\x37\x00\x00\x00\x00\x37\x68\x40\x4f\x04\xc1\x43\xb2\x79\xdb\ +\x8d\x6d\xbc\x62\x63\xc3\xc6\x1e\xdc\x31\xbf\xaf\xa2\xbd\xa8\x25\ +\x29\x1e\x09\x5e\x93\x51\x5a\xf3\x92\x65\x33\x76\xac\xa8\xef\xc7\ +\x39\x39\x05\xdb\xde\x96\x5a\x14\xcd\x7a\xf3\x5d\x39\xd3\xa3\xe3\ +\xd1\x50\x22\xa0\xdf\x7e\x5f\xc9\x80\x1e\x9f\xb8\xa0\xd0\xf7\x07\ +\x8d\x2d\xea\x42\xa7\x0e\xcd\xd8\xac\x80\x6b\x55\xc7\x52\x62\xed\ +\x83\xeb\xc9\xe4\xf9\xc2\xbb\x1b\xe4\xdb\x07\xdd\xdc\xd0\x24\x67\ +\x77\x9f\x31\xbb\x21\x1e\x61\x1f\x74\x00\x00\x00\xa0\x5a\x75\xf3\ +\xed\x86\x22\x03\xfd\xc6\x02\x6c\xf6\xbb\xd7\x67\xec\xc1\x1d\x3a\ +\xf9\x71\xed\x03\xd0\x58\x62\x34\xd6\x64\x75\x94\x0c\x6f\xe9\x81\ +\x76\x2e\x8e\xe2\xa6\x2f\xf0\x16\x0f\x07\x72\x82\x9b\xcd\xdb\x6e\ +\xfc\xec\x5c\xb1\x2e\xe7\x4f\x72\x34\xbc\xdc\x2d\xc8\xd2\x17\xcd\ +\x8b\x0c\x7e\x7e\x5d\xb4\x47\x6b\xd3\xaa\x54\x1d\x5e\x1e\x9d\xf5\ +\xf3\x97\xd3\x11\x54\xb7\xe0\xa6\x54\x3d\x56\x38\xfa\x1d\x1b\x1b\ +\xce\x98\xc9\x60\x69\xf0\xf2\xad\x0a\x00\x00\x00\x10\xd0\x53\xc2\ +\xa7\x8f\x26\x02\x7a\xeb\x6a\x63\x0f\xee\xe4\xe2\x70\xb5\x16\xfb\ +\xdf\x19\xe3\xef\xe9\x7b\xa2\xe7\x63\x6f\x5d\x9d\x08\x4d\xd1\x50\ +\xcd\x9e\x21\x2f\x7a\x7d\x57\xb7\xad\x93\xa4\xc8\x0f\x87\x32\x7f\ +\x77\xf7\xfa\x54\xa7\xc4\xe8\x50\xc1\x3f\xf9\x5e\x3f\x9f\x38\x57\ +\x3c\x98\xea\x60\x38\x73\x7c\xd6\xce\x9b\x5c\x4d\xdd\x64\x75\x14\ +\x5d\x29\x3f\xbd\x03\x2b\xe6\xf7\x55\xd7\xa6\xc7\x86\x8d\xf7\x16\ +\x5b\x0b\x01\x00\x00\x00\x40\x71\x96\xf9\x78\x53\xc9\xc5\xe2\xea\ +\xdc\x1e\xb9\x7b\x1e\x4b\x04\x96\x39\xb2\x38\x5c\xf8\xd4\x7e\xb9\ +\x56\x6f\x96\xc9\xea\x90\xf3\xde\x8d\x0a\x9f\xda\x9f\xf7\xba\x9c\ +\xdd\x8f\xa7\x9e\x9b\xff\xfe\xe0\x9c\x2b\x63\x47\xe7\xa6\x8c\xad\ +\xce\xb2\x3b\x10\x92\x9d\x0b\x31\xbf\xaf\xe8\xe2\x61\x8b\x1f\x7d\ +\x45\x96\xc6\x66\xd9\x5b\x57\xcf\xbb\x95\xd2\x1d\x9d\x9b\x8c\xc0\ +\x1a\x1e\x3c\x36\xab\x9d\x2c\x91\x1f\x0e\xc9\xb5\x32\xb1\x82\xbe\ +\xfb\xbe\x5f\xca\x5f\x60\x0a\xbf\xab\xe7\x49\x63\x26\xc3\x74\x66\ +\x98\xa4\x3f\x9b\x0e\x00\x00\x00\xa0\x3a\xf3\x72\x04\x3d\xb9\x58\ +\x5c\xba\x99\x5a\x50\x6c\xba\xe2\x13\x17\x14\xfc\xfa\xdf\x89\xc2\ +\x77\x7b\xb4\x78\xf3\x8e\x8c\xe9\xdd\x26\x57\xbd\x5c\xbd\x5b\x8d\ +\x70\x35\x15\xf0\x2b\xf8\xf9\xdf\xe7\x4c\xd9\x5a\xbc\x1d\x5a\xb0\ +\xe1\x79\xa3\xe3\x23\x1e\x0d\x69\x7c\xdf\xce\x8c\xd7\x58\x5b\x7b\ +\x52\x9d\x0b\x25\x42\x5f\xf8\x87\xcf\x8c\xb2\xb0\xb5\xcf\x8f\x51\ +\xf4\x7c\x65\x14\xf8\xf4\xd5\x59\xff\x0c\x84\x4e\x25\x66\x35\x98\ +\x3d\x4b\xb5\xf8\xd1\x57\x72\xda\x99\xfb\xc1\x3f\xc8\x71\x75\x9d\ +\x80\x98\xdf\x57\x72\x1d\x80\x62\xa6\x08\xe8\x00\x00\x00\xc0\xf4\ +\xb3\xc4\x7c\xbd\xb1\xe4\x62\x71\x52\xfe\x11\xde\xb2\x0b\xa8\xc4\ +\x1e\xd9\xc9\xe3\x57\xb2\xc5\x54\xf0\xc8\x6e\x99\x1b\x6e\x91\xbd\ +\xa5\x4b\x66\xcf\x52\x79\xfa\x76\x6a\x2a\xe0\x57\x3c\x1a\x34\x56\ +\xc5\x4e\x06\xbb\x2b\xfd\x2f\xd7\x74\xe4\x7f\xd1\x43\xcf\x48\x7a\ +\x26\x7f\x28\x0b\xf8\x75\xa5\xff\xe5\xdc\xd1\xf3\xb4\xa9\xef\xa5\ +\x1e\x2b\x08\x9f\xda\x6f\x04\x59\x5b\xf3\x4f\x32\xb6\xc6\x9b\xb5\ +\x0f\xc1\x34\xeb\xb8\x58\x19\xc5\xfc\x3e\x8d\x7f\xf2\x7a\x4d\xea\ +\x30\xb0\xef\x25\x99\x6c\x2e\xd9\x5b\xba\x64\x69\x6c\x96\xa7\x6f\ +\xa7\x31\x15\x3d\xbd\x9d\xc5\xfc\x3e\x5d\x7a\xef\xd9\x69\x76\x08\ +\x9c\x93\xae\x7e\xde\x92\x8b\x33\x02\x00\x00\x00\xa8\x4c\xdd\x7c\ +\xbd\xb1\xf4\xc5\xe2\x92\xa3\xb4\x73\xc9\xf8\xde\xed\x0a\x1c\x7e\ +\xcb\xb8\xc6\x3a\xb7\x27\x23\x34\x85\x07\x8f\xe9\xe2\xae\xa7\x2a\ +\x5e\x75\x7e\x36\xc4\xfc\x3e\x4d\x7c\xf1\xbe\xfc\xef\x6e\xcb\xb9\ +\x3e\x93\xab\xde\x98\xfa\x1e\x1e\x3c\x56\x32\x98\xc6\x27\x2e\x28\ +\x3c\x78\x2c\x11\xec\x5b\xba\xae\xbb\x55\xcf\x8b\x95\x51\xe0\xf0\ +\x5b\xf2\xbf\xf9\xdb\x9a\xd6\xe1\xf8\xde\xed\x0a\x7e\xf3\x91\xe2\ +\xd1\x90\x11\xcc\x93\xed\x2c\x1e\x0d\x29\xf8\xcd\x47\xba\xf4\xde\ +\xb3\xd3\xee\x40\x48\x5f\xc0\xcf\xd9\xdd\xc7\x37\x2b\x00\x00\x00\ +\x50\x05\x53\x67\x67\x67\x9c\x62\xa8\x2d\x8b\xb7\x43\x26\xc7\xc2\ +\x54\xd8\xb9\x46\x5b\x7e\x81\x76\x46\x3b\xbb\xb6\x82\x41\xa6\xfa\ +\x03\x00\x00\x60\x1a\xff\x67\xa7\x08\x6a\x6f\x2e\x8e\x92\x83\x76\ +\x06\x00\x00\x00\x60\x76\xd5\x51\x04\x00\x00\x00\x00\x00\x10\xd0\ +\x01\x00\x00\x00\x00\x00\x01\x1d\x00\x00\x00\x00\x00\x02\x3a\x00\ +\x00\x00\x00\x00\x20\xa0\x03\x00\x00\x00\x00\x40\x40\x07\x00\x00\ +\x00\x00\x00\x04\x74\x00\x00\x00\x00\x00\x08\xe8\x00\x00\x00\x00\ +\x00\x80\x80\x0e\x00\x00\x00\x00\x00\x01\x1d\x00\x00\x00\x00\x00\ +\x10\xd0\x01\x00\x00\x00\x00\x20\xa0\x03\x00\x00\x00\x00\x00\x02\ +\x3a\x00\x00\x00\x00\x00\x04\x74\x00\x00\x00\x00\x00\x40\x40\x07\ +\x00\x00\x00\x00\x80\x80\x0e\x00\x00\x00\x00\x00\x08\xe8\x00\x00\ +\x00\x00\x00\x10\xd0\x01\x00\x00\x00\x00\x00\x01\x1d\x00\x00\x00\ +\x00\x00\x02\x3a\x00\x00\x00\x00\x00\x20\xa0\x03\x00\x00\x00\x00\ +\x70\xfd\xf8\x3f\x00\x00\x00\xff\xff\x03\x00\xce\x28\x15\x65\x5d\ +\xb8\xdc\xe1\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x03\xe6\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ +\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ +\x01\x00\x9a\x9c\x18\x00\x00\x03\x88\x49\x44\x41\x54\x58\x85\xed\ +\x56\x5d\x68\x1c\x55\x18\x3d\xf7\xde\xd9\x9f\xce\x76\xb3\xdd\x24\ +\x33\x93\x61\x63\x36\xee\x66\x5b\x34\x6d\xa5\xb6\x14\x6c\x63\x53\ +\xf5\xc1\x12\x29\x11\x4b\xfd\x09\xd2\x4a\xa1\xc5\x8a\x21\xd8\x8a\ +\xd0\x22\x12\x84\x3c\x28\xe2\x83\x22\x14\x14\x8a\xc1\x9f\x22\x82\ +\x60\x41\x2a\x45\x50\x11\xed\x4b\x11\x0c\xfd\xa1\x4d\x76\x77\x6a\ +\xcb\xa6\xd9\x8d\xe9\xd6\x64\xb3\xc9\xce\xdc\xcf\x87\xb0\x76\x50\ +\xb3\x3f\xb0\x3e\x88\x39\x6f\x73\xbe\xef\x9e\x73\xf8\xf8\xe6\x72\ +\x81\x15\xac\xe0\x3f\x08\x16\x8d\x46\xfd\x8d\x12\x53\xea\x69\x8e\ +\x6b\xda\x36\x26\x30\x24\x17\x0b\x37\x00\x1c\x69\x44\x00\x5e\xb3\ +\xb9\xd9\x7a\x50\x28\xfc\x33\xbf\x57\xac\xe3\x84\x9f\xfe\xb1\xc7\ +\xd0\x06\x63\x86\xa1\x37\x3c\x40\x5c\xd7\xfb\x3d\x42\x39\x7e\xfa\ +\xcc\x0b\xc2\x66\x08\x90\x83\x9f\xff\x6e\xde\xb2\x93\x08\x87\x89\ +\xd9\xaf\x36\x34\x40\x2c\x1c\x0e\x11\xa3\xf7\x3e\x1c\xdd\x5f\x8a\ +\x44\x42\x6d\xf6\x22\xa9\x4c\x88\xbc\x5b\x23\x6e\x68\x2f\x82\x8b\ +\x4f\x5e\x7f\xe3\xcd\xdb\x3e\x9f\xbf\x2f\x6e\x68\xc3\x31\x23\x50\ +\xd3\x24\xaa\x4f\xc0\xab\x1c\xba\x7f\xd3\x5d\x13\xdd\x1b\xcd\x04\ +\x00\xf8\xbc\x6c\xb6\xc4\x4a\x2d\xe5\x72\x54\xd3\x74\x62\xf2\x60\ +\x47\x67\x74\x62\xef\xc0\xc0\x96\xc1\xa1\xa3\x19\x87\x51\x0f\x97\ +\xea\x8e\x5a\x02\x54\x5d\x42\x46\x78\xea\xa5\x57\x1e\x5a\x5d\xfe\ +\x6e\xef\x08\x67\xc7\xaf\xe6\xb6\x02\xb8\x0c\x00\x56\x36\x3b\x19\ +\x8d\x86\x7a\xd3\xc9\xd4\xc9\x5d\x3d\xdb\xcf\x59\xbf\xa6\x22\x4c\ +\x8a\xfe\xf1\xec\xd4\x58\x2d\x01\x2a\x4e\xa0\x17\x50\x48\x20\xd1\ +\x7d\xaf\xb9\xb6\xcc\x3d\xfd\xcc\x56\x05\xa0\x67\xdd\x7d\x96\x95\ +\xbf\xe5\xf8\x4a\x07\xd2\xe9\xf4\x1a\x10\x3b\x9e\x9a\xaa\xcd\xbc\ +\x6a\x80\x8c\x69\xae\xe1\xe0\xb3\x8a\xa2\xb0\x32\xd7\xff\xf8\xfa\ +\x2d\x1e\xaf\x27\x16\xd3\xf5\x47\xdc\xbd\xd7\xae\xe5\x67\x98\xed\ +\x6c\x4f\xde\xcc\x9d\xaa\xd5\xbc\x6a\x80\xdf\x81\x02\x24\xa9\x6e\ +\xce\xeb\xf5\x88\x77\xdf\x79\x72\x1e\x02\xaf\xad\x0b\x85\x3a\xdd\ +\xb5\xe4\xcc\x4c\x1e\x75\xa2\xf2\x04\x32\x99\x02\x91\xcc\x5b\xd6\ +\xad\xeb\x6e\xfe\x81\x9e\xbb\xd7\x1f\x3b\xb6\x8b\x97\x54\xcf\xa7\ +\x71\x53\x7b\xb4\x5e\x53\x37\x44\xb5\x86\x70\x40\x4d\x2c\x14\x4b\ +\x62\xe7\xc3\x6b\x3b\xdd\xfc\x86\x0d\x66\xc7\xe6\x4d\xd1\xfc\xd9\ +\xaf\x2f\xec\x0e\xfa\x56\xed\x69\x6e\x52\x3d\x2d\x4d\x2a\x05\xd5\ +\xa0\x5f\x0f\xa9\x9d\x21\x55\xed\x6b\x0e\xa8\x07\x36\xce\x15\xce\ +\x5a\x80\x5c\x4e\x9f\x2d\x57\x28\x23\xd1\xd6\x7c\x8f\x64\xca\x37\ +\xdf\xff\x70\x24\x10\x0a\xf9\x9b\xfe\x5a\x2f\x95\x48\x7e\x75\x66\ +\xec\xfc\xa9\x8f\xcf\x17\x53\xe9\x6c\xb3\x53\x94\xaa\xe2\xe7\x05\ +\xe9\x60\x61\xbe\xb8\x78\x21\x99\xc9\xed\xab\xa4\xbf\x6c\x80\x5e\ +\xd7\x2f\x7a\xbd\xad\x75\xa4\x3d\x12\x7e\xf0\xf3\x2f\x9f\xdf\x2c\ +\x20\x05\x17\x8c\xbb\x17\x13\x00\x6c\xdb\x26\xe9\x90\x04\x80\x54\ +\x7a\xc6\xda\xfb\xc4\x07\x3e\xee\xc8\xdd\x91\x5c\x6e\x0c\x00\xbe\ +\x03\xec\x9a\x03\xc4\xf4\xd6\x21\x70\x1a\x59\x6a\x60\x3f\x32\x22\ +\xe9\x80\xcd\x2f\x55\xc9\xdf\xa6\x07\x6f\x7c\x71\xfa\x70\x7a\xff\ +\xc0\xc9\xfb\x98\x87\xcb\xd1\xd1\x7d\xbf\x3c\xd6\x77\x22\x3e\x3d\ +\x3d\xdb\xee\x38\xac\xa8\x00\x0c\x9c\x4a\xc4\xb0\x1a\xa0\x6d\x4b\ +\xe7\xc4\xcb\xc9\xc9\xa9\x13\x35\x4f\xa0\x12\x4c\xd3\x54\x3d\xb6\ +\xfd\xb6\xc2\x31\xe7\x30\xf2\x0a\x4e\x7e\xc7\x33\x3d\x68\x59\x28\ +\xd6\xab\x55\x31\x40\xdc\x30\xba\x25\x93\x3b\x18\x23\xdf\x9f\xa4\ +\x03\x02\x9c\x04\x13\xca\xf4\x44\x26\x3b\x0c\x00\x31\x43\x7b\x0b\ +\x44\x7e\x00\x57\x20\xee\x68\x12\xb1\x05\x05\xce\xb7\x57\x27\x7f\ +\xbb\xb4\x9c\x47\xc5\xab\x98\x98\xbd\x8a\x18\x6f\x22\xe2\xde\x32\ +\xc7\x39\xc0\x25\x9d\x1b\xcf\x64\x3f\x2a\x73\xc9\x9b\xd9\xa3\x09\ +\xbd\xf5\x90\xc3\x58\x07\x93\x77\x36\x9e\x38\x9f\x27\x46\x0d\x7b\ +\xbc\xfc\x2b\xa8\x69\x07\xe2\x86\x36\x0c\xa2\xe7\xea\x52\xe6\x78\ +\x7f\x62\x32\x37\xd2\x90\x00\x5d\x5d\x5d\x3e\x3e\x37\x17\xac\xc7\ +\x5f\x09\x67\x6e\x5f\xbc\x88\xc5\x7a\xce\xac\x60\x05\xff\x4f\xfc\ +\x01\xa0\xdd\x38\x94\xa3\x9e\x46\x4d\x00\x00\x00\x00\x49\x45\x4e\ +\x44\xae\x42\x60\x82\ +\x00\x00\x03\x6b\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ +\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ +\x01\x00\x9a\x9c\x18\x00\x00\x03\x0d\x49\x44\x41\x54\x58\x85\xed\ +\x96\x6f\x48\x13\x71\x18\xc7\x9f\xbb\x6d\x77\xbb\xbb\xdd\x36\x37\ +\xb7\xdb\xf9\x1f\x9d\x6d\x5d\x26\x65\x89\x15\x92\x26\x24\x1a\xea\ +\x0b\x49\x05\x5f\x28\x91\x98\x41\x6f\x82\xe8\x4d\xbd\xea\x65\x84\ +\xa0\x44\x68\xf9\x22\xb2\x08\x7c\x11\x41\xf8\x46\x51\x22\x03\x2b\ +\x41\xca\x3f\x24\xfe\x29\x06\xba\x9b\x4d\x45\xa7\xde\xfe\xdd\xee\ +\x7a\xa5\xcc\xbf\xdb\x42\xa2\x60\x9f\x77\xf7\x7b\xbe\xf7\x3c\x1f\ +\xee\x39\xb8\x03\x88\x13\x27\xce\xff\x08\xc7\x01\x76\x54\xbd\xd0\ +\x58\xc2\xd9\x0c\x53\x90\x65\x31\xc9\xe2\xaa\xb9\xff\xaf\x0b\x70\ +\x19\x6c\xb3\x46\x8b\x0f\xe0\xb8\x52\x0a\x85\xa4\x27\xfb\x0a\x26\ +\x9b\x6f\x1c\x63\xd9\xc4\x23\x17\xb0\x26\x31\x95\x14\x89\xb5\xf6\ +\xbc\xb9\x4e\xe9\xf5\xe4\xba\x1c\x82\xd1\xdd\x99\x4c\xd6\x54\x28\ +\x85\xe4\x0e\x0c\xc7\x1f\x1c\xa9\x80\xd5\x60\xd0\xe2\x6a\xc5\x8b\ +\x8e\x67\xf5\x14\xcb\xea\xc0\xbb\x19\x50\xa0\x4a\xe5\x6a\x58\x04\ +\xb1\x27\xb3\xcd\x24\xa9\x79\xd7\xd6\xf1\x14\xb4\x09\xba\x06\x5b\ +\x0a\x3b\x90\x91\x64\xb4\x1d\x89\x00\x46\xa9\x6e\x96\x97\x72\xb8\ +\xcd\xc6\x00\x00\x00\xa9\xc5\xc5\x20\x12\x34\x6e\xd5\xd3\x4d\x26\ +\x26\x18\x12\x3b\xf3\x0b\x0a\x34\xe5\x95\x55\x70\xf7\xde\x7d\x4a\ +\x14\xc5\x12\x54\x42\x1b\xa2\x11\x50\x46\x0a\xa8\x09\xbc\xa9\xb6\ +\xfe\x0c\xb1\x75\x9d\x73\x82\x41\x5c\xce\xb5\x7c\x00\x98\x02\x00\ +\x70\xb8\xdd\x2e\x7b\x52\x52\xe2\xe8\x97\x91\x9e\xc6\xba\x9a\x8b\ +\x63\xa3\xa3\x6b\x80\xc8\xa5\x3f\xf8\xa5\x3d\x6b\xda\x8f\x43\x9f\ +\x40\x11\x80\xd2\xeb\xf5\xa7\xda\xed\x96\xed\xb3\x2b\x15\xb9\x3a\ +\x8a\xc6\x5b\xc2\x73\x53\x4e\xe7\xb2\x5f\xf0\xd6\x0e\x7f\x1c\x52\ +\xfa\xfc\xbe\xdb\x73\x51\x0e\x8f\x28\xc0\xb3\xac\x9e\x24\xb0\x20\ +\x8a\x22\xdb\x67\xc5\xc5\xd9\x40\x12\xaa\x93\x59\x8c\xf1\xd2\x6e\ +\x09\x05\xa2\x34\x4d\x2f\xb8\xba\xa3\x1d\x1e\x51\x60\x1d\x40\xf0\ +\x07\xc4\x1d\x6b\x52\x28\x50\x78\xf8\xa8\x9a\x06\x04\x1d\xb4\x9a\ +\xcd\xe7\xc2\x6b\xd3\x3c\xbf\x14\xcb\xf0\x88\x02\x3c\xcf\x0b\x08\ +\x0a\x82\xcb\xe5\xd9\x71\x9e\x97\x97\x0a\x65\x65\x9c\x24\xa3\xf2\ +\x70\x26\x63\x2c\x89\x75\x68\x38\x8a\x48\x01\x8b\x51\xcf\xe9\x74\ +\x78\xee\xa9\xd3\xa9\x48\xf8\xf9\xe5\xd2\xe3\xc8\x85\xf3\x99\xf0\ +\xf9\xd3\xcf\x6a\x83\x56\x53\x45\xaa\x70\xd1\x40\xd1\x62\x62\x02\ +\x8e\xe9\x09\x6d\x7a\x02\x4d\x96\x9b\xf4\x54\x67\x8e\x47\x78\xee\ +\x00\x90\x0e\xea\x8f\x1c\x54\xd8\xc2\x6a\x31\x70\x14\x4d\x8d\xf4\ +\xf5\xdf\x22\x29\x0d\xbe\xa7\x2e\x49\x32\x0c\x7d\x98\x85\xde\xde\ +\x09\xcf\xf7\x09\x67\xc8\x23\xf8\x15\x34\xa1\x96\xdc\xcb\x1b\x5a\ +\x9f\x37\x88\xce\xb9\xdc\x87\xce\x88\x28\x00\x00\x90\x6b\x4d\x7e\ +\x9c\x77\x36\xfd\x5a\x7b\x7b\x0d\x19\xfe\x42\x1e\xc4\xfc\xfc\x2a\ +\xd4\x5d\xed\xda\x14\x04\x7f\xe1\x8c\xd3\xfd\xf5\xb0\x6c\xc4\x15\ +\x00\x00\xd8\x57\xd6\xfb\x1c\x3e\xb9\x64\xfc\xdb\x82\xb9\xa8\x38\ +\x1b\x53\xa9\x0e\xbe\x6d\x72\x92\x87\xa6\xc6\xee\x0d\xc1\xeb\x6f\ +\x99\x5e\x70\x47\xfc\x68\x45\x25\xe0\x00\x90\x6c\x2b\x9e\x57\x33\ +\x02\x58\x5e\xbf\x1c\xe1\x50\x14\x51\x30\x16\x1a\xa5\x69\x35\x00\ +\x00\x84\x44\x09\xc6\xc7\x9c\xd0\xd6\x36\xb8\xd9\xde\xfa\x7e\x7d\ +\xcd\x13\xa8\x9d\xe5\x7f\xbd\x8d\xa6\x77\x54\x2b\x08\xc7\x6a\x31\ +\x70\x04\x45\xdc\x91\x25\xa8\x90\xc4\x90\x16\xc3\x94\xa2\xe0\x0b\ +\xa8\x08\x8d\x6a\xce\x2b\x04\xbb\xd4\x01\xa4\x73\x6c\x71\x71\x33\ +\xd6\xbe\x7f\x44\x4a\x4a\x0a\x91\x96\xa6\x4b\x80\x18\xff\x2b\xe2\ +\xc4\x89\xf3\x4f\xf1\x1b\x22\xca\x00\x6f\xd0\x10\xcf\x18\x00\x00\ +\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x0f\x88\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x40\x00\x00\x00\x40\x08\x06\x00\x00\x00\xaa\x69\x71\xde\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ +\x01\x00\x9a\x9c\x18\x00\x00\x0a\x4f\x69\x43\x43\x50\x50\x68\x6f\ +\x74\x6f\x73\x68\x6f\x70\x20\x49\x43\x43\x20\x70\x72\x6f\x66\x69\ +\x6c\x65\x00\x00\x78\xda\x9d\x53\x67\x54\x53\xe9\x16\x3d\xf7\xde\ +\xf4\x42\x4b\x88\x80\x94\x4b\x6f\x52\x15\x08\x20\x52\x42\x8b\x80\ +\x14\x91\x26\x2a\x21\x09\x10\x4a\x88\x21\xa1\xd9\x15\x51\xc1\x11\ +\x45\x45\x04\x1b\xc8\xa0\x88\x03\x8e\x8e\x80\x8c\x15\x51\x2c\x0c\ +\x8a\x0a\xd8\x07\xe4\x21\xa2\x8e\x83\xa3\x88\x8a\xca\xfb\xe1\x7b\ +\xa3\x6b\xd6\xbc\xf7\xe6\xcd\xfe\xb5\xd7\x3e\xe7\xac\xf3\x9d\xb3\ +\xcf\x07\xc0\x08\x0c\x96\x48\x33\x51\x35\x80\x0c\xa9\x42\x1e\x11\ +\xe0\x83\xc7\xc4\xc6\xe1\xe4\x2e\x40\x81\x0a\x24\x70\x00\x10\x08\ +\xb3\x64\x21\x73\xfd\x23\x01\x00\xf8\x7e\x3c\x3c\x2b\x22\xc0\x07\ +\xbe\x00\x01\x78\xd3\x0b\x08\x00\xc0\x4d\x9b\xc0\x30\x1c\x87\xff\ +\x0f\xea\x42\x99\x5c\x01\x80\x84\x01\xc0\x74\x91\x38\x4b\x08\x80\ +\x14\x00\x40\x7a\x8e\x42\xa6\x00\x40\x46\x01\x80\x9d\x98\x26\x53\ +\x00\xa0\x04\x00\x60\xcb\x63\x62\xe3\x00\x50\x2d\x00\x60\x27\x7f\ +\xe6\xd3\x00\x80\x9d\xf8\x99\x7b\x01\x00\x5b\x94\x21\x15\x01\xa0\ +\x91\x00\x20\x13\x65\x88\x44\x00\x68\x3b\x00\xac\xcf\x56\x8a\x45\ +\x00\x58\x30\x00\x14\x66\x4b\xc4\x39\x00\xd8\x2d\x00\x30\x49\x57\ +\x66\x48\x00\xb0\xb7\x00\xc0\xce\x10\x0b\xb2\x00\x08\x0c\x00\x30\ +\x51\x88\x85\x29\x00\x04\x7b\x00\x60\xc8\x23\x23\x78\x00\x84\x99\ +\x00\x14\x46\xf2\x57\x3c\xf1\x2b\xae\x10\xe7\x2a\x00\x00\x78\x99\ +\xb2\x3c\xb9\x24\x39\x45\x81\x5b\x08\x2d\x71\x07\x57\x57\x2e\x1e\ +\x28\xce\x49\x17\x2b\x14\x36\x61\x02\x61\x9a\x40\x2e\xc2\x79\x99\ +\x19\x32\x81\x34\x0f\xe0\xf3\xcc\x00\x00\xa0\x91\x15\x11\xe0\x83\ +\xf3\xfd\x78\xce\x0e\xae\xce\xce\x36\x8e\xb6\x0e\x5f\x2d\xea\xbf\ +\x06\xff\x22\x62\x62\xe3\xfe\xe5\xcf\xab\x70\x40\x00\x00\xe1\x74\ +\x7e\xd1\xfe\x2c\x2f\xb3\x1a\x80\x3b\x06\x80\x6d\xfe\xa2\x25\xee\ +\x04\x68\x5e\x0b\xa0\x75\xf7\x8b\x66\xb2\x0f\x40\xb5\x00\xa0\xe9\ +\xda\x57\xf3\x70\xf8\x7e\x3c\x3c\x45\xa1\x90\xb9\xd9\xd9\xe5\xe4\ +\xe4\xd8\x4a\xc4\x42\x5b\x61\xca\x57\x7d\xfe\x67\xc2\x5f\xc0\x57\ +\xfd\x6c\xf9\x7e\x3c\xfc\xf7\xf5\xe0\xbe\xe2\x24\x81\x32\x5d\x81\ +\x47\x04\xf8\xe0\xc2\xcc\xf4\x4c\xa5\x1c\xcf\x92\x09\x84\x62\xdc\ +\xe6\x8f\x47\xfc\xb7\x0b\xff\xfc\x1d\xd3\x22\xc4\x49\x62\xb9\x58\ +\x2a\x14\xe3\x51\x12\x71\x8e\x44\x9a\x8c\xf3\x32\xa5\x22\x89\x42\ +\x92\x29\xc5\x25\xd2\xff\x64\xe2\xdf\x2c\xfb\x03\x3e\xdf\x35\x00\ +\xb0\x6a\x3e\x01\x7b\x91\x2d\xa8\x5d\x63\x03\xf6\x4b\x27\x10\x58\ +\x74\xc0\xe2\xf7\x00\x00\xf2\xbb\x6f\xc1\xd4\x28\x08\x03\x80\x68\ +\x83\xe1\xcf\x77\xff\xef\x3f\xfd\x47\xa0\x25\x00\x80\x66\x49\x92\ +\x71\x00\x00\x5e\x44\x24\x2e\x54\xca\xb3\x3f\xc7\x08\x00\x00\x44\ +\xa0\x81\x2a\xb0\x41\x1b\xf4\xc1\x18\x2c\xc0\x06\x1c\xc1\x05\xdc\ +\xc1\x0b\xfc\x60\x36\x84\x42\x24\xc4\xc2\x42\x10\x42\x0a\x64\x80\ +\x1c\x72\x60\x29\xac\x82\x42\x28\x86\xcd\xb0\x1d\x2a\x60\x2f\xd4\ +\x40\x1d\x34\xc0\x51\x68\x86\x93\x70\x0e\x2e\xc2\x55\xb8\x0e\x3d\ +\x70\x0f\xfa\x61\x08\x9e\xc1\x28\xbc\x81\x09\x04\x41\xc8\x08\x13\ +\x61\x21\xda\x88\x01\x62\x8a\x58\x23\x8e\x08\x17\x99\x85\xf8\x21\ +\xc1\x48\x04\x12\x8b\x24\x20\xc9\x88\x14\x51\x22\x4b\x91\x35\x48\ +\x31\x52\x8a\x54\x20\x55\x48\x1d\xf2\x3d\x72\x02\x39\x87\x5c\x46\ +\xba\x91\x3b\xc8\x00\x32\x82\xfc\x86\xbc\x47\x31\x94\x81\xb2\x51\ +\x3d\xd4\x0c\xb5\x43\xb9\xa8\x37\x1a\x84\x46\xa2\x0b\xd0\x64\x74\ +\x31\x9a\x8f\x16\xa0\x9b\xd0\x72\xb4\x1a\x3d\x8c\x36\xa1\xe7\xd0\ +\xab\x68\x0f\xda\x8f\x3e\x43\xc7\x30\xc0\xe8\x18\x07\x33\xc4\x6c\ +\x30\x2e\xc6\xc3\x42\xb1\x38\x2c\x09\x93\x63\xcb\xb1\x22\xac\x0c\ +\xab\xc6\x1a\xb0\x56\xac\x03\xbb\x89\xf5\x63\xcf\xb1\x77\x04\x12\ +\x81\x45\xc0\x09\x36\x04\x77\x42\x20\x61\x1e\x41\x48\x58\x4c\x58\ +\x4e\xd8\x48\xa8\x20\x1c\x24\x34\x11\xda\x09\x37\x09\x03\x84\x51\ +\xc2\x27\x22\x93\xa8\x4b\xb4\x26\xba\x11\xf9\xc4\x18\x62\x32\x31\ +\x87\x58\x48\x2c\x23\xd6\x12\x8f\x13\x2f\x10\x7b\x88\x43\xc4\x37\ +\x24\x12\x89\x43\x32\x27\xb9\x90\x02\x49\xb1\xa4\x54\xd2\x12\xd2\ +\x46\xd2\x6e\x52\x23\xe9\x2c\xa9\x9b\x34\x48\x1a\x23\x93\xc9\xda\ +\x64\x6b\xb2\x07\x39\x94\x2c\x20\x2b\xc8\x85\xe4\x9d\xe4\xc3\xe4\ +\x33\xe4\x1b\xe4\x21\xf2\x5b\x0a\x9d\x62\x40\x71\xa4\xf8\x53\xe2\ +\x28\x52\xca\x6a\x4a\x19\xe5\x10\xe5\x34\xe5\x06\x65\x98\x32\x41\ +\x55\xa3\x9a\x52\xdd\xa8\xa1\x54\x11\x35\x8f\x5a\x42\xad\xa1\xb6\ +\x52\xaf\x51\x87\xa8\x13\x34\x75\x9a\x39\xcd\x83\x16\x49\x4b\xa5\ +\xad\xa2\x95\xd3\x1a\x68\x17\x68\xf7\x69\xaf\xe8\x74\xba\x11\xdd\ +\x95\x1e\x4e\x97\xd0\x57\xd2\xcb\xe9\x47\xe8\x97\xe8\x03\xf4\x77\ +\x0c\x0d\x86\x15\x83\xc7\x88\x67\x28\x19\x9b\x18\x07\x18\x67\x19\ +\x77\x18\xaf\x98\x4c\xa6\x19\xd3\x8b\x19\xc7\x54\x30\x37\x31\xeb\ +\x98\xe7\x99\x0f\x99\x6f\x55\x58\x2a\xb6\x2a\x7c\x15\x91\xca\x0a\ +\x95\x4a\x95\x26\x95\x1b\x2a\x2f\x54\xa9\xaa\xa6\xaa\xde\xaa\x0b\ +\x55\xf3\x55\xcb\x54\x8f\xa9\x5e\x53\x7d\xae\x46\x55\x33\x53\xe3\ +\xa9\x09\xd4\x96\xab\x55\xaa\x9d\x50\xeb\x53\x1b\x53\x67\xa9\x3b\ +\xa8\x87\xaa\x67\xa8\x6f\x54\x3f\xa4\x7e\x59\xfd\x89\x06\x59\xc3\ +\x4c\xc3\x4f\x43\xa4\x51\xa0\xb1\x5f\xe3\xbc\xc6\x20\x0b\x63\x19\ +\xb3\x78\x2c\x21\x6b\x0d\xab\x86\x75\x81\x35\xc4\x26\xb1\xcd\xd9\ +\x7c\x76\x2a\xbb\x98\xfd\x1d\xbb\x8b\x3d\xaa\xa9\xa1\x39\x43\x33\ +\x4a\x33\x57\xb3\x52\xf3\x94\x66\x3f\x07\xe3\x98\x71\xf8\x9c\x74\ +\x4e\x09\xe7\x28\xa7\x97\xf3\x7e\x8a\xde\x14\xef\x29\xe2\x29\x1b\ +\xa6\x34\x4c\xb9\x31\x65\x5c\x6b\xaa\x96\x97\x96\x58\xab\x48\xab\ +\x51\xab\x47\xeb\xbd\x36\xae\xed\xa7\x9d\xa6\xbd\x45\xbb\x59\xfb\ +\x81\x0e\x41\xc7\x4a\x27\x5c\x27\x47\x67\x8f\xce\x05\x9d\xe7\x53\ +\xd9\x53\xdd\xa7\x0a\xa7\x16\x4d\x3d\x3a\xf5\xae\x2e\xaa\x6b\xa5\ +\x1b\xa1\xbb\x44\x77\xbf\x6e\xa7\xee\x98\x9e\xbe\x5e\x80\x9e\x4c\ +\x6f\xa7\xde\x79\xbd\xe7\xfa\x1c\x7d\x2f\xfd\x54\xfd\x6d\xfa\xa7\ +\xf5\x47\x0c\x58\x06\xb3\x0c\x24\x06\xdb\x0c\xce\x18\x3c\xc5\x35\ +\x71\x6f\x3c\x1d\x2f\xc7\xdb\xf1\x51\x43\x5d\xc3\x40\x43\xa5\x61\ +\x95\x61\x97\xe1\x84\x91\xb9\xd1\x3c\xa3\xd5\x46\x8d\x46\x0f\x8c\ +\x69\xc6\x5c\xe3\x24\xe3\x6d\xc6\x6d\xc6\xa3\x26\x06\x26\x21\x26\ +\x4b\x4d\xea\x4d\xee\x9a\x52\x4d\xb9\xa6\x29\xa6\x3b\x4c\x3b\x4c\ +\xc7\xcd\xcc\xcd\xa2\xcd\xd6\x99\x35\x9b\x3d\x31\xd7\x32\xe7\x9b\ +\xe7\x9b\xd7\x9b\xdf\xb7\x60\x5a\x78\x5a\x2c\xb6\xa8\xb6\xb8\x65\ +\x49\xb2\xe4\x5a\xa6\x59\xee\xb6\xbc\x6e\x85\x5a\x39\x59\xa5\x58\ +\x55\x5a\x5d\xb3\x46\xad\x9d\xad\x25\xd6\xbb\xad\xbb\xa7\x11\xa7\ +\xb9\x4e\x93\x4e\xab\x9e\xd6\x67\xc3\xb0\xf1\xb6\xc9\xb6\xa9\xb7\ +\x19\xb0\xe5\xd8\x06\xdb\xae\xb6\x6d\xb6\x7d\x61\x67\x62\x17\x67\ +\xb7\xc5\xae\xc3\xee\x93\xbd\x93\x7d\xba\x7d\x8d\xfd\x3d\x07\x0d\ +\x87\xd9\x0e\xab\x1d\x5a\x1d\x7e\x73\xb4\x72\x14\x3a\x56\x3a\xde\ +\x9a\xce\x9c\xee\x3f\x7d\xc5\xf4\x96\xe9\x2f\x67\x58\xcf\x10\xcf\ +\xd8\x33\xe3\xb6\x13\xcb\x29\xc4\x69\x9d\x53\x9b\xd3\x47\x67\x17\ +\x67\xb9\x73\x83\xf3\x88\x8b\x89\x4b\x82\xcb\x2e\x97\x3e\x2e\x9b\ +\x1b\xc6\xdd\xc8\xbd\xe4\x4a\x74\xf5\x71\x5d\xe1\x7a\xd2\xf5\x9d\ +\x9b\xb3\x9b\xc2\xed\xa8\xdb\xaf\xee\x36\xee\x69\xee\x87\xdc\x9f\ +\xcc\x34\x9f\x29\x9e\x59\x33\x73\xd0\xc3\xc8\x43\xe0\x51\xe5\xd1\ +\x3f\x0b\x9f\x95\x30\x6b\xdf\xac\x7e\x4f\x43\x4f\x81\x67\xb5\xe7\ +\x23\x2f\x63\x2f\x91\x57\xad\xd7\xb0\xb7\xa5\x77\xaa\xf7\x61\xef\ +\x17\x3e\xf6\x3e\x72\x9f\xe3\x3e\xe3\x3c\x37\xde\x32\xde\x59\x5f\ +\xcc\x37\xc0\xb7\xc8\xb7\xcb\x4f\xc3\x6f\x9e\x5f\x85\xdf\x43\x7f\ +\x23\xff\x64\xff\x7a\xff\xd1\x00\xa7\x80\x25\x01\x67\x03\x89\x81\ +\x41\x81\x5b\x02\xfb\xf8\x7a\x7c\x21\xbf\x8e\x3f\x3a\xdb\x65\xf6\ +\xb2\xd9\xed\x41\x8c\xa0\xb9\x41\x15\x41\x8f\x82\xad\x82\xe5\xc1\ +\xad\x21\x68\xc8\xec\x90\xad\x21\xf7\xe7\x98\xce\x91\xce\x69\x0e\ +\x85\x50\x7e\xe8\xd6\xd0\x07\x61\xe6\x61\x8b\xc3\x7e\x0c\x27\x85\ +\x87\x85\x57\x86\x3f\x8e\x70\x88\x58\x1a\xd1\x31\x97\x35\x77\xd1\ +\xdc\x43\x73\xdf\x44\xfa\x44\x96\x44\xde\x9b\x67\x31\x4f\x39\xaf\ +\x2d\x4a\x35\x2a\x3e\xaa\x2e\x6a\x3c\xda\x37\xba\x34\xba\x3f\xc6\ +\x2e\x66\x59\xcc\xd5\x58\x9d\x58\x49\x6c\x4b\x1c\x39\x2e\x2a\xae\ +\x36\x6e\x6c\xbe\xdf\xfc\xed\xf3\x87\xe2\x9d\xe2\x0b\xe3\x7b\x17\ +\x98\x2f\xc8\x5d\x70\x79\xa1\xce\xc2\xf4\x85\xa7\x16\xa9\x2e\x12\ +\x2c\x3a\x96\x40\x4c\x88\x4e\x38\x94\xf0\x41\x10\x2a\xa8\x16\x8c\ +\x25\xf2\x13\x77\x25\x8e\x0a\x79\xc2\x1d\xc2\x67\x22\x2f\xd1\x36\ +\xd1\x88\xd8\x43\x5c\x2a\x1e\x4e\xf2\x48\x2a\x4d\x7a\x92\xec\x91\ +\xbc\x35\x79\x24\xc5\x33\xa5\x2c\xe5\xb9\x84\x27\xa9\x90\xbc\x4c\ +\x0d\x4c\xdd\x9b\x3a\x9e\x16\x9a\x76\x20\x6d\x32\x3d\x3a\xbd\x31\ +\x83\x92\x91\x90\x71\x42\xaa\x21\x4d\x93\xb6\x67\xea\x67\xe6\x66\ +\x76\xcb\xac\x65\x85\xb2\xfe\xc5\x6e\x8b\xb7\x2f\x1e\x95\x07\xc9\ +\x6b\xb3\x90\xac\x05\x59\x2d\x0a\xb6\x42\xa6\xe8\x54\x5a\x28\xd7\ +\x2a\x07\xb2\x67\x65\x57\x66\xbf\xcd\x89\xca\x39\x96\xab\x9e\x2b\ +\xcd\xed\xcc\xb3\xca\xdb\x90\x37\x9c\xef\x9f\xff\xed\x12\xc2\x12\ +\xe1\x92\xb6\xa5\x86\x4b\x57\x2d\x1d\x58\xe6\xbd\xac\x6a\x39\xb2\ +\x3c\x71\x79\xdb\x0a\xe3\x15\x05\x2b\x86\x56\x06\xac\x3c\xb8\x8a\ +\xb6\x2a\x6d\xd5\x4f\xab\xed\x57\x97\xae\x7e\xbd\x26\x7a\x4d\x6b\ +\x81\x5e\xc1\xca\x82\xc1\xb5\x01\x6b\xeb\x0b\x55\x0a\xe5\x85\x7d\ +\xeb\xdc\xd7\xed\x5d\x4f\x58\x2f\x59\xdf\xb5\x61\xfa\x86\x9d\x1b\ +\x3e\x15\x89\x8a\xae\x14\xdb\x17\x97\x15\x7f\xd8\x28\xdc\x78\xe5\ +\x1b\x87\x6f\xca\xbf\x99\xdc\x94\xb4\xa9\xab\xc4\xb9\x64\xcf\x66\ +\xd2\x66\xe9\xe6\xde\x2d\x9e\x5b\x0e\x96\xaa\x97\xe6\x97\x0e\x6e\ +\x0d\xd9\xda\xb4\x0d\xdf\x56\xb4\xed\xf5\xf6\x45\xdb\x2f\x97\xcd\ +\x28\xdb\xbb\x83\xb6\x43\xb9\xa3\xbf\x3c\xb8\xbc\x65\xa7\xc9\xce\ +\xcd\x3b\x3f\x54\xa4\x54\xf4\x54\xfa\x54\x36\xee\xd2\xdd\xb5\x61\ +\xd7\xf8\x6e\xd1\xee\x1b\x7b\xbc\xf6\x34\xec\xd5\xdb\x5b\xbc\xf7\ +\xfd\x3e\xc9\xbe\xdb\x55\x01\x55\x4d\xd5\x66\xd5\x65\xfb\x49\xfb\ +\xb3\xf7\x3f\xae\x89\xaa\xe9\xf8\x96\xfb\x6d\x5d\xad\x4e\x6d\x71\ +\xed\xc7\x03\xd2\x03\xfd\x07\x23\x0e\xb6\xd7\xb9\xd4\xd5\x1d\xd2\ +\x3d\x54\x52\x8f\xd6\x2b\xeb\x47\x0e\xc7\x1f\xbe\xfe\x9d\xef\x77\ +\x2d\x0d\x36\x0d\x55\x8d\x9c\xc6\xe2\x23\x70\x44\x79\xe4\xe9\xf7\ +\x09\xdf\xf7\x1e\x0d\x3a\xda\x76\x8c\x7b\xac\xe1\x07\xd3\x1f\x76\ +\x1d\x67\x1d\x2f\x6a\x42\x9a\xf2\x9a\x46\x9b\x53\x9a\xfb\x5b\x62\ +\x5b\xba\x4f\xcc\x3e\xd1\xd6\xea\xde\x7a\xfc\x47\xdb\x1f\x0f\x9c\ +\x34\x3c\x59\x79\x4a\xf3\x54\xc9\x69\xda\xe9\x82\xd3\x93\x67\xf2\ +\xcf\x8c\x9d\x95\x9d\x7d\x7e\x2e\xf9\xdc\x60\xdb\xa2\xb6\x7b\xe7\ +\x63\xce\xdf\x6a\x0f\x6f\xef\xba\x10\x74\xe1\xd2\x45\xff\x8b\xe7\ +\x3b\xbc\x3b\xce\x5c\xf2\xb8\x74\xf2\xb2\xdb\xe5\x13\x57\xb8\x57\ +\x9a\xaf\x3a\x5f\x6d\xea\x74\xea\x3c\xfe\x93\xd3\x4f\xc7\xbb\x9c\ +\xbb\x9a\xae\xb9\x5c\x6b\xb9\xee\x7a\xbd\xb5\x7b\x66\xf7\xe9\x1b\ +\x9e\x37\xce\xdd\xf4\xbd\x79\xf1\x16\xff\xd6\xd5\x9e\x39\x3d\xdd\ +\xbd\xf3\x7a\x6f\xf7\xc5\xf7\xf5\xdf\x16\xdd\x7e\x72\x27\xfd\xce\ +\xcb\xbb\xd9\x77\x27\xee\xad\xbc\x4f\xbc\x5f\xf4\x40\xed\x41\xd9\ +\x43\xdd\x87\xd5\x3f\x5b\xfe\xdc\xd8\xef\xdc\x7f\x6a\xc0\x77\xa0\ +\xf3\xd1\xdc\x47\xf7\x06\x85\x83\xcf\xfe\x91\xf5\x8f\x0f\x43\x05\ +\x8f\x99\x8f\xcb\x86\x0d\x86\xeb\x9e\x38\x3e\x39\x39\xe2\x3f\x72\ +\xfd\xe9\xfc\xa7\x43\xcf\x64\xcf\x26\x9e\x17\xfe\xa2\xfe\xcb\xae\ +\x17\x16\x2f\x7e\xf8\xd5\xeb\xd7\xce\xd1\x98\xd1\xa1\x97\xf2\x97\ +\x93\xbf\x6d\x7c\xa5\xfd\xea\xc0\xeb\x19\xaf\xdb\xc6\xc2\xc6\x1e\ +\xbe\xc9\x78\x33\x31\x5e\xf4\x56\xfb\xed\xc1\x77\xdc\x77\x1d\xef\ +\xa3\xdf\x0f\x4f\xe4\x7c\x20\x7f\x28\xff\x68\xf9\xb1\xf5\x53\xd0\ +\xa7\xfb\x93\x19\x93\x93\xff\x04\x03\x98\xf3\xfc\x63\x33\x2d\xdb\ +\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\x25\x00\x00\x80\x83\ +\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00\x75\x30\x00\x00\xea\x60\ +\x00\x00\x3a\x98\x00\x00\x17\x6f\x92\x5f\xc5\x46\x00\x00\x04\xb3\ +\x49\x44\x41\x54\x78\xda\xec\x9b\x5d\x4c\x5b\x65\x18\xc7\xff\x6f\ +\x4b\x19\x8e\xca\x12\xc0\x36\x2d\x6b\x9a\x8d\x2e\xbb\x11\x24\x54\ +\xbe\x12\x68\x71\x4b\xb6\x25\xf3\x6e\x8a\x5c\x98\x5d\x90\x6c\x6e\ +\x44\x2f\xd8\x00\xfb\xe1\x16\xa6\x59\x84\x69\x24\x21\x82\xcb\xa2\ +\x2e\x61\x12\x58\xa2\x89\x37\x66\x03\xa7\x6e\xf8\x31\x3e\xa6\x2b\ +\xf3\xaa\x4a\xa6\xb6\x7c\xa4\xf5\x60\x32\x53\x3e\xd2\x7a\xce\xf1\ +\x02\x06\x6b\x7b\xca\xd6\xd3\x73\x0e\xa7\xe9\x79\xae\xda\x73\xd2\ +\xf3\xbc\xef\xef\x3c\xef\xf3\x3e\x1f\x6f\x09\x4e\x7d\xcd\x22\x83\ +\x45\x85\x0c\x17\x05\x80\x02\x40\x01\xa0\x00\x50\x00\x6c\x85\x38\ +\x6c\x06\x50\x6e\x2b\x28\xb7\x15\x6d\x75\x86\xcc\x03\xd0\x5a\x67\ +\x84\x8a\x00\x2a\x02\xbc\x69\x33\x66\x1e\x00\x15\xe1\xfe\xac\xf8\ +\x00\x05\x80\x02\x20\x33\x00\x30\x2c\xf7\xe7\xb4\x00\xf0\x7a\x8d\ +\x1e\x01\x67\x39\x82\xae\x72\x9c\xdd\x57\xc4\x4b\xf1\x07\x3f\xcc\ +\x83\x61\x57\x27\xdf\x35\x3a\xc7\xeb\x19\x1d\xfb\x77\x22\xe8\x2a\ +\x47\xc0\x59\x8e\xe6\x2a\x3d\xaf\x67\x10\x3e\xe9\x70\xc0\x55\x0e\ +\xcd\x23\xae\xfb\xd2\x64\x10\x8e\x61\xbf\xa4\x6f\xae\xeb\xa0\x09\ +\xc7\x2a\x74\xeb\xdf\x23\x34\x0b\xfd\xbb\xbf\x48\x63\x01\xb1\xbb\ +\xd6\xf1\x0a\x1d\xce\xed\xdf\x29\xd9\xe4\xcf\x1f\x88\x9e\x3c\x00\ +\x10\x22\xe1\x12\xe8\x1b\x0f\xc6\x5d\x7b\xa3\x46\x0f\x87\x4d\xfc\ +\x88\xce\x65\x37\xe2\x64\xa5\x2e\xee\xfa\x87\xb7\x03\xd2\x01\xe8\ +\xf8\x66\x06\x97\x26\xe3\x21\x34\x57\xeb\x45\x07\x70\x92\x63\xad\ +\x7f\x34\x11\xc4\xdb\xdf\xcd\xf2\x7a\x5e\x16\xef\x58\x7e\xd8\x0f\ +\x02\x44\x99\xe2\x52\x84\xd9\x74\xd9\x1c\xab\xd0\xa1\xa1\x24\x1f\ +\x96\x82\x1c\x68\xb3\xd5\xeb\x11\x20\xc3\x02\xa1\x30\x8d\xe9\x85\ +\x15\x0c\xdd\xfb\x07\x9f\xdc\x09\x22\x91\x63\x5a\xfe\x8f\x41\x6e\ +\xf6\xc6\x7b\xbb\x38\x11\x84\x7b\x84\xbf\xff\x21\xa9\xd6\x04\xdd\ +\xf5\x46\xbc\x56\xa9\xc3\x72\x84\xc1\xd1\xcf\xef\x63\xdc\x1f\x8a\ +\xba\xaf\x26\x04\xdd\x87\xcd\x68\x28\xc9\x47\xb6\xfa\xc9\x16\x6a\ +\x98\x66\x71\xf5\xd7\x05\x9c\xfa\xca\x07\x9a\x8d\x1e\x5e\xb5\x49\ +\x8b\xfe\x97\x8a\x91\xa3\x21\xe8\x1b\x0f\xa2\xf3\xd6\x5c\x4a\x16\ +\x45\xc4\x2c\x8a\x1e\xb0\xec\xc0\xa7\x47\x76\x63\xbb\x86\x5f\xb8\ +\xb1\x14\x61\xd0\xf4\xc5\x7d\x8c\x4c\x3f\x48\xbf\x40\xa8\xad\xce\ +\x80\xa1\x46\x0b\xef\xc9\x03\xc0\x76\x8d\x0a\x43\x8d\x16\x51\xd3\ +\x65\x51\x00\x9c\xae\x35\xc0\x69\x17\x2e\xc5\x75\xda\x8d\x38\x5d\ +\x6b\x48\x0f\x00\x2f\xec\xce\x83\xbb\x5e\xf8\xfc\xde\x5d\x6f\x84\ +\x7d\x57\x9e\xbc\x01\x64\xa9\x08\xae\xbc\x5c\x2c\x9a\xb9\x0e\x34\ +\x14\x43\x4d\x88\x7c\x01\xf4\xbc\x68\x4e\x69\xcd\x3f\x89\x4f\xe8\ +\x3e\x6c\x96\x27\x00\x15\x01\x8e\x3c\x9b\x2f\x7a\x20\xf4\x4a\x49\ +\x3e\x88\x1c\x01\x34\x59\x75\x51\x09\x92\x58\xa2\x51\x13\x34\x59\ +\x9f\x91\x1f\x80\xc6\x52\xf1\xdf\xfe\x86\xae\x02\xf9\x01\xb0\x14\ +\xe4\x48\x06\x60\x4f\xa1\x70\xba\x12\xe6\x02\x0e\x9b\x61\xbd\x74\ +\x1d\x5b\xc9\xe9\x1a\x9d\xc3\x7b\xdf\xcf\x47\x5d\xd7\x66\xab\x25\ +\x03\xc0\xa5\x2b\xd9\xf1\x3e\xd6\x02\xb8\x1e\xf6\xd0\xd9\x71\xd5\ +\xf1\xa5\x2c\x6d\x73\xe9\x4a\x76\xbc\xa2\x87\xc2\xe9\x22\x09\x01\ +\x3c\xac\xd9\xc5\x4a\xa2\x1a\x9e\x94\x85\x4d\x2e\x5d\xc9\x8e\x57\ +\xf0\x6c\xf0\xcf\xb6\x32\xe4\x6d\x93\xc6\x0f\x3c\x58\xa1\xb1\xeb\ +\x7d\x8f\xbc\x76\x01\x2f\xb5\x22\x99\x05\x78\xa9\x65\xf9\x6d\x83\ +\x83\x53\x94\x64\x00\x06\xa7\x16\xe4\x07\xa0\xff\x2e\x85\x08\x2d\ +\xbe\x23\x88\xd0\x2c\xfa\xef\x52\xf2\x03\xc0\xb0\xc0\x80\x04\x56\ +\x30\x30\x45\x41\x48\xcc\x82\x6e\x83\xad\xd7\x7c\x58\x0c\x33\xa2\ +\x4d\x3e\x14\xa6\xd1\x7a\xcd\x27\xdf\x74\x98\x61\x81\xc6\xab\xbf\ +\x43\x8c\x85\xc0\x02\x68\x18\x9c\x16\x7c\xbb\x15\x3c\x10\xfa\xf1\ +\xaf\x10\xde\x1a\x11\xbe\x4d\xe6\x1c\xf6\x63\x2c\xa6\xe2\x2c\x4b\ +\x00\xc0\x6a\xa3\xc2\x31\xec\x17\xc4\x12\x58\x00\xed\xd7\xfd\x9c\ +\x8d\x18\xd9\x02\x00\x56\x1b\xa6\x87\x2e\x7b\x37\x6d\x96\x3c\x4e\ +\x96\x22\x0c\x0e\x5d\xf6\xe2\xe3\x3b\x41\xd1\xfc\x4a\xca\x00\x5c\ +\x76\x23\x7c\xed\x65\xf0\xb6\x94\xa2\xca\xa4\x8d\xba\x37\x39\x1b\ +\x82\xf9\x82\x07\x9f\x79\x16\x10\x4e\x62\x8b\x0c\xd3\x2c\xae\x78\ +\x28\x98\x2f\x78\x30\x39\x1b\x6d\xf6\xd5\x26\x2d\xbc\x2d\xa5\xf0\ +\xb5\x97\xc1\x21\x40\xe5\x39\xa5\x50\xb8\xf3\xa0\x09\xc7\x1f\x69\ +\x8d\xfd\xbd\x18\xc1\xde\xee\x7b\x09\x14\x09\xd3\x1a\xfb\xad\xe5\ +\x39\x14\xe6\x6e\x64\xf1\x17\x27\x82\x70\xa5\xe0\x73\xb2\x84\x9a\ +\x3c\x00\x3c\xb5\x49\x41\x94\x5d\x5b\x16\xa9\xae\xe5\x58\x1d\x27\ +\xd6\x3a\xc5\x7c\x21\xf0\x5a\x02\x67\xf7\x15\xc5\x4d\x1e\x00\xfa\ +\xc6\x02\x10\x5b\x7a\xc7\xe3\x75\x9c\xa8\xd4\xa1\x83\xe7\xf9\x04\ +\x7e\x47\x64\x38\xda\xe0\xbd\x63\x01\x74\x8e\xce\x8b\x0e\xa0\xf3\ +\xd6\x1c\x7a\x39\x40\x37\x57\xe9\xa4\x03\x10\xd3\xb0\x45\xcf\xed\ +\x00\xce\xdc\x98\x91\x2c\x19\x3a\x73\x63\x06\x3d\x31\x07\x22\x58\ +\x9e\x9e\x4c\x8d\x9a\xa3\x1d\xc9\xfe\x68\x85\x66\x50\x6b\x7e\x1a\ +\xec\xda\xe4\xcf\x7d\x9b\xfc\xe1\x04\x87\xcd\x80\x2f\x5f\xdd\x8b\ +\x76\x9b\x11\x2c\x80\x9f\x7c\xc9\x05\x39\x37\xff\xf8\x17\xdb\xb2\ +\x54\x78\xbe\x28\x17\x34\x03\xbc\x73\x73\x16\x13\x33\x8b\xd2\xee\ +\x02\xa9\x08\xe5\xb6\x46\xed\x02\x85\xe7\x7f\xde\x8a\x61\x28\x67\ +\x85\x95\xa2\xa8\x02\x40\x01\xb0\x35\x92\xd6\x67\x85\x85\x10\x21\ +\xce\x0a\x0b\x21\x44\xf9\xeb\xac\xe2\x03\x14\x00\x0a\x80\x4c\x96\ +\xff\x07\x00\x85\xfa\xad\x10\xc5\x6e\x22\x46\x00\x00\x00\x00\x49\ +\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x0f\x88\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x40\x00\x00\x00\x40\x08\x06\x00\x00\x00\xaa\x69\x71\xde\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ +\x01\x00\x9a\x9c\x18\x00\x00\x0a\x4f\x69\x43\x43\x50\x50\x68\x6f\ +\x74\x6f\x73\x68\x6f\x70\x20\x49\x43\x43\x20\x70\x72\x6f\x66\x69\ +\x6c\x65\x00\x00\x78\xda\x9d\x53\x67\x54\x53\xe9\x16\x3d\xf7\xde\ +\xf4\x42\x4b\x88\x80\x94\x4b\x6f\x52\x15\x08\x20\x52\x42\x8b\x80\ +\x14\x91\x26\x2a\x21\x09\x10\x4a\x88\x21\xa1\xd9\x15\x51\xc1\x11\ +\x45\x45\x04\x1b\xc8\xa0\x88\x03\x8e\x8e\x80\x8c\x15\x51\x2c\x0c\ +\x8a\x0a\xd8\x07\xe4\x21\xa2\x8e\x83\xa3\x88\x8a\xca\xfb\xe1\x7b\ +\xa3\x6b\xd6\xbc\xf7\xe6\xcd\xfe\xb5\xd7\x3e\xe7\xac\xf3\x9d\xb3\ +\xcf\x07\xc0\x08\x0c\x96\x48\x33\x51\x35\x80\x0c\xa9\x42\x1e\x11\ +\xe0\x83\xc7\xc4\xc6\xe1\xe4\x2e\x40\x81\x0a\x24\x70\x00\x10\x08\ +\xb3\x64\x21\x73\xfd\x23\x01\x00\xf8\x7e\x3c\x3c\x2b\x22\xc0\x07\ +\xbe\x00\x01\x78\xd3\x0b\x08\x00\xc0\x4d\x9b\xc0\x30\x1c\x87\xff\ +\x0f\xea\x42\x99\x5c\x01\x80\x84\x01\xc0\x74\x91\x38\x4b\x08\x80\ +\x14\x00\x40\x7a\x8e\x42\xa6\x00\x40\x46\x01\x80\x9d\x98\x26\x53\ +\x00\xa0\x04\x00\x60\xcb\x63\x62\xe3\x00\x50\x2d\x00\x60\x27\x7f\ +\xe6\xd3\x00\x80\x9d\xf8\x99\x7b\x01\x00\x5b\x94\x21\x15\x01\xa0\ +\x91\x00\x20\x13\x65\x88\x44\x00\x68\x3b\x00\xac\xcf\x56\x8a\x45\ +\x00\x58\x30\x00\x14\x66\x4b\xc4\x39\x00\xd8\x2d\x00\x30\x49\x57\ +\x66\x48\x00\xb0\xb7\x00\xc0\xce\x10\x0b\xb2\x00\x08\x0c\x00\x30\ +\x51\x88\x85\x29\x00\x04\x7b\x00\x60\xc8\x23\x23\x78\x00\x84\x99\ +\x00\x14\x46\xf2\x57\x3c\xf1\x2b\xae\x10\xe7\x2a\x00\x00\x78\x99\ +\xb2\x3c\xb9\x24\x39\x45\x81\x5b\x08\x2d\x71\x07\x57\x57\x2e\x1e\ +\x28\xce\x49\x17\x2b\x14\x36\x61\x02\x61\x9a\x40\x2e\xc2\x79\x99\ +\x19\x32\x81\x34\x0f\xe0\xf3\xcc\x00\x00\xa0\x91\x15\x11\xe0\x83\ +\xf3\xfd\x78\xce\x0e\xae\xce\xce\x36\x8e\xb6\x0e\x5f\x2d\xea\xbf\ +\x06\xff\x22\x62\x62\xe3\xfe\xe5\xcf\xab\x70\x40\x00\x00\xe1\x74\ +\x7e\xd1\xfe\x2c\x2f\xb3\x1a\x80\x3b\x06\x80\x6d\xfe\xa2\x25\xee\ +\x04\x68\x5e\x0b\xa0\x75\xf7\x8b\x66\xb2\x0f\x40\xb5\x00\xa0\xe9\ +\xda\x57\xf3\x70\xf8\x7e\x3c\x3c\x45\xa1\x90\xb9\xd9\xd9\xe5\xe4\ +\xe4\xd8\x4a\xc4\x42\x5b\x61\xca\x57\x7d\xfe\x67\xc2\x5f\xc0\x57\ +\xfd\x6c\xf9\x7e\x3c\xfc\xf7\xf5\xe0\xbe\xe2\x24\x81\x32\x5d\x81\ +\x47\x04\xf8\xe0\xc2\xcc\xf4\x4c\xa5\x1c\xcf\x92\x09\x84\x62\xdc\ +\xe6\x8f\x47\xfc\xb7\x0b\xff\xfc\x1d\xd3\x22\xc4\x49\x62\xb9\x58\ +\x2a\x14\xe3\x51\x12\x71\x8e\x44\x9a\x8c\xf3\x32\xa5\x22\x89\x42\ +\x92\x29\xc5\x25\xd2\xff\x64\xe2\xdf\x2c\xfb\x03\x3e\xdf\x35\x00\ +\xb0\x6a\x3e\x01\x7b\x91\x2d\xa8\x5d\x63\x03\xf6\x4b\x27\x10\x58\ +\x74\xc0\xe2\xf7\x00\x00\xf2\xbb\x6f\xc1\xd4\x28\x08\x03\x80\x68\ +\x83\xe1\xcf\x77\xff\xef\x3f\xfd\x47\xa0\x25\x00\x80\x66\x49\x92\ +\x71\x00\x00\x5e\x44\x24\x2e\x54\xca\xb3\x3f\xc7\x08\x00\x00\x44\ +\xa0\x81\x2a\xb0\x41\x1b\xf4\xc1\x18\x2c\xc0\x06\x1c\xc1\x05\xdc\ +\xc1\x0b\xfc\x60\x36\x84\x42\x24\xc4\xc2\x42\x10\x42\x0a\x64\x80\ +\x1c\x72\x60\x29\xac\x82\x42\x28\x86\xcd\xb0\x1d\x2a\x60\x2f\xd4\ +\x40\x1d\x34\xc0\x51\x68\x86\x93\x70\x0e\x2e\xc2\x55\xb8\x0e\x3d\ +\x70\x0f\xfa\x61\x08\x9e\xc1\x28\xbc\x81\x09\x04\x41\xc8\x08\x13\ +\x61\x21\xda\x88\x01\x62\x8a\x58\x23\x8e\x08\x17\x99\x85\xf8\x21\ +\xc1\x48\x04\x12\x8b\x24\x20\xc9\x88\x14\x51\x22\x4b\x91\x35\x48\ +\x31\x52\x8a\x54\x20\x55\x48\x1d\xf2\x3d\x72\x02\x39\x87\x5c\x46\ +\xba\x91\x3b\xc8\x00\x32\x82\xfc\x86\xbc\x47\x31\x94\x81\xb2\x51\ +\x3d\xd4\x0c\xb5\x43\xb9\xa8\x37\x1a\x84\x46\xa2\x0b\xd0\x64\x74\ +\x31\x9a\x8f\x16\xa0\x9b\xd0\x72\xb4\x1a\x3d\x8c\x36\xa1\xe7\xd0\ +\xab\x68\x0f\xda\x8f\x3e\x43\xc7\x30\xc0\xe8\x18\x07\x33\xc4\x6c\ +\x30\x2e\xc6\xc3\x42\xb1\x38\x2c\x09\x93\x63\xcb\xb1\x22\xac\x0c\ +\xab\xc6\x1a\xb0\x56\xac\x03\xbb\x89\xf5\x63\xcf\xb1\x77\x04\x12\ +\x81\x45\xc0\x09\x36\x04\x77\x42\x20\x61\x1e\x41\x48\x58\x4c\x58\ +\x4e\xd8\x48\xa8\x20\x1c\x24\x34\x11\xda\x09\x37\x09\x03\x84\x51\ +\xc2\x27\x22\x93\xa8\x4b\xb4\x26\xba\x11\xf9\xc4\x18\x62\x32\x31\ +\x87\x58\x48\x2c\x23\xd6\x12\x8f\x13\x2f\x10\x7b\x88\x43\xc4\x37\ +\x24\x12\x89\x43\x32\x27\xb9\x90\x02\x49\xb1\xa4\x54\xd2\x12\xd2\ +\x46\xd2\x6e\x52\x23\xe9\x2c\xa9\x9b\x34\x48\x1a\x23\x93\xc9\xda\ +\x64\x6b\xb2\x07\x39\x94\x2c\x20\x2b\xc8\x85\xe4\x9d\xe4\xc3\xe4\ +\x33\xe4\x1b\xe4\x21\xf2\x5b\x0a\x9d\x62\x40\x71\xa4\xf8\x53\xe2\ +\x28\x52\xca\x6a\x4a\x19\xe5\x10\xe5\x34\xe5\x06\x65\x98\x32\x41\ +\x55\xa3\x9a\x52\xdd\xa8\xa1\x54\x11\x35\x8f\x5a\x42\xad\xa1\xb6\ +\x52\xaf\x51\x87\xa8\x13\x34\x75\x9a\x39\xcd\x83\x16\x49\x4b\xa5\ +\xad\xa2\x95\xd3\x1a\x68\x17\x68\xf7\x69\xaf\xe8\x74\xba\x11\xdd\ +\x95\x1e\x4e\x97\xd0\x57\xd2\xcb\xe9\x47\xe8\x97\xe8\x03\xf4\x77\ +\x0c\x0d\x86\x15\x83\xc7\x88\x67\x28\x19\x9b\x18\x07\x18\x67\x19\ +\x77\x18\xaf\x98\x4c\xa6\x19\xd3\x8b\x19\xc7\x54\x30\x37\x31\xeb\ +\x98\xe7\x99\x0f\x99\x6f\x55\x58\x2a\xb6\x2a\x7c\x15\x91\xca\x0a\ +\x95\x4a\x95\x26\x95\x1b\x2a\x2f\x54\xa9\xaa\xa6\xaa\xde\xaa\x0b\ +\x55\xf3\x55\xcb\x54\x8f\xa9\x5e\x53\x7d\xae\x46\x55\x33\x53\xe3\ +\xa9\x09\xd4\x96\xab\x55\xaa\x9d\x50\xeb\x53\x1b\x53\x67\xa9\x3b\ +\xa8\x87\xaa\x67\xa8\x6f\x54\x3f\xa4\x7e\x59\xfd\x89\x06\x59\xc3\ +\x4c\xc3\x4f\x43\xa4\x51\xa0\xb1\x5f\xe3\xbc\xc6\x20\x0b\x63\x19\ +\xb3\x78\x2c\x21\x6b\x0d\xab\x86\x75\x81\x35\xc4\x26\xb1\xcd\xd9\ +\x7c\x76\x2a\xbb\x98\xfd\x1d\xbb\x8b\x3d\xaa\xa9\xa1\x39\x43\x33\ +\x4a\x33\x57\xb3\x52\xf3\x94\x66\x3f\x07\xe3\x98\x71\xf8\x9c\x74\ +\x4e\x09\xe7\x28\xa7\x97\xf3\x7e\x8a\xde\x14\xef\x29\xe2\x29\x1b\ +\xa6\x34\x4c\xb9\x31\x65\x5c\x6b\xaa\x96\x97\x96\x58\xab\x48\xab\ +\x51\xab\x47\xeb\xbd\x36\xae\xed\xa7\x9d\xa6\xbd\x45\xbb\x59\xfb\ +\x81\x0e\x41\xc7\x4a\x27\x5c\x27\x47\x67\x8f\xce\x05\x9d\xe7\x53\ +\xd9\x53\xdd\xa7\x0a\xa7\x16\x4d\x3d\x3a\xf5\xae\x2e\xaa\x6b\xa5\ +\x1b\xa1\xbb\x44\x77\xbf\x6e\xa7\xee\x98\x9e\xbe\x5e\x80\x9e\x4c\ +\x6f\xa7\xde\x79\xbd\xe7\xfa\x1c\x7d\x2f\xfd\x54\xfd\x6d\xfa\xa7\ +\xf5\x47\x0c\x58\x06\xb3\x0c\x24\x06\xdb\x0c\xce\x18\x3c\xc5\x35\ +\x71\x6f\x3c\x1d\x2f\xc7\xdb\xf1\x51\x43\x5d\xc3\x40\x43\xa5\x61\ +\x95\x61\x97\xe1\x84\x91\xb9\xd1\x3c\xa3\xd5\x46\x8d\x46\x0f\x8c\ +\x69\xc6\x5c\xe3\x24\xe3\x6d\xc6\x6d\xc6\xa3\x26\x06\x26\x21\x26\ +\x4b\x4d\xea\x4d\xee\x9a\x52\x4d\xb9\xa6\x29\xa6\x3b\x4c\x3b\x4c\ +\xc7\xcd\xcc\xcd\xa2\xcd\xd6\x99\x35\x9b\x3d\x31\xd7\x32\xe7\x9b\ +\xe7\x9b\xd7\x9b\xdf\xb7\x60\x5a\x78\x5a\x2c\xb6\xa8\xb6\xb8\x65\ +\x49\xb2\xe4\x5a\xa6\x59\xee\xb6\xbc\x6e\x85\x5a\x39\x59\xa5\x58\ +\x55\x5a\x5d\xb3\x46\xad\x9d\xad\x25\xd6\xbb\xad\xbb\xa7\x11\xa7\ +\xb9\x4e\x93\x4e\xab\x9e\xd6\x67\xc3\xb0\xf1\xb6\xc9\xb6\xa9\xb7\ +\x19\xb0\xe5\xd8\x06\xdb\xae\xb6\x6d\xb6\x7d\x61\x67\x62\x17\x67\ +\xb7\xc5\xae\xc3\xee\x93\xbd\x93\x7d\xba\x7d\x8d\xfd\x3d\x07\x0d\ +\x87\xd9\x0e\xab\x1d\x5a\x1d\x7e\x73\xb4\x72\x14\x3a\x56\x3a\xde\ +\x9a\xce\x9c\xee\x3f\x7d\xc5\xf4\x96\xe9\x2f\x67\x58\xcf\x10\xcf\ +\xd8\x33\xe3\xb6\x13\xcb\x29\xc4\x69\x9d\x53\x9b\xd3\x47\x67\x17\ +\x67\xb9\x73\x83\xf3\x88\x8b\x89\x4b\x82\xcb\x2e\x97\x3e\x2e\x9b\ +\x1b\xc6\xdd\xc8\xbd\xe4\x4a\x74\xf5\x71\x5d\xe1\x7a\xd2\xf5\x9d\ +\x9b\xb3\x9b\xc2\xed\xa8\xdb\xaf\xee\x36\xee\x69\xee\x87\xdc\x9f\ +\xcc\x34\x9f\x29\x9e\x59\x33\x73\xd0\xc3\xc8\x43\xe0\x51\xe5\xd1\ +\x3f\x0b\x9f\x95\x30\x6b\xdf\xac\x7e\x4f\x43\x4f\x81\x67\xb5\xe7\ +\x23\x2f\x63\x2f\x91\x57\xad\xd7\xb0\xb7\xa5\x77\xaa\xf7\x61\xef\ +\x17\x3e\xf6\x3e\x72\x9f\xe3\x3e\xe3\x3c\x37\xde\x32\xde\x59\x5f\ +\xcc\x37\xc0\xb7\xc8\xb7\xcb\x4f\xc3\x6f\x9e\x5f\x85\xdf\x43\x7f\ +\x23\xff\x64\xff\x7a\xff\xd1\x00\xa7\x80\x25\x01\x67\x03\x89\x81\ +\x41\x81\x5b\x02\xfb\xf8\x7a\x7c\x21\xbf\x8e\x3f\x3a\xdb\x65\xf6\ +\xb2\xd9\xed\x41\x8c\xa0\xb9\x41\x15\x41\x8f\x82\xad\x82\xe5\xc1\ +\xad\x21\x68\xc8\xec\x90\xad\x21\xf7\xe7\x98\xce\x91\xce\x69\x0e\ +\x85\x50\x7e\xe8\xd6\xd0\x07\x61\xe6\x61\x8b\xc3\x7e\x0c\x27\x85\ +\x87\x85\x57\x86\x3f\x8e\x70\x88\x58\x1a\xd1\x31\x97\x35\x77\xd1\ +\xdc\x43\x73\xdf\x44\xfa\x44\x96\x44\xde\x9b\x67\x31\x4f\x39\xaf\ +\x2d\x4a\x35\x2a\x3e\xaa\x2e\x6a\x3c\xda\x37\xba\x34\xba\x3f\xc6\ +\x2e\x66\x59\xcc\xd5\x58\x9d\x58\x49\x6c\x4b\x1c\x39\x2e\x2a\xae\ +\x36\x6e\x6c\xbe\xdf\xfc\xed\xf3\x87\xe2\x9d\xe2\x0b\xe3\x7b\x17\ +\x98\x2f\xc8\x5d\x70\x79\xa1\xce\xc2\xf4\x85\xa7\x16\xa9\x2e\x12\ +\x2c\x3a\x96\x40\x4c\x88\x4e\x38\x94\xf0\x41\x10\x2a\xa8\x16\x8c\ +\x25\xf2\x13\x77\x25\x8e\x0a\x79\xc2\x1d\xc2\x67\x22\x2f\xd1\x36\ +\xd1\x88\xd8\x43\x5c\x2a\x1e\x4e\xf2\x48\x2a\x4d\x7a\x92\xec\x91\ +\xbc\x35\x79\x24\xc5\x33\xa5\x2c\xe5\xb9\x84\x27\xa9\x90\xbc\x4c\ +\x0d\x4c\xdd\x9b\x3a\x9e\x16\x9a\x76\x20\x6d\x32\x3d\x3a\xbd\x31\ +\x83\x92\x91\x90\x71\x42\xaa\x21\x4d\x93\xb6\x67\xea\x67\xe6\x66\ +\x76\xcb\xac\x65\x85\xb2\xfe\xc5\x6e\x8b\xb7\x2f\x1e\x95\x07\xc9\ +\x6b\xb3\x90\xac\x05\x59\x2d\x0a\xb6\x42\xa6\xe8\x54\x5a\x28\xd7\ +\x2a\x07\xb2\x67\x65\x57\x66\xbf\xcd\x89\xca\x39\x96\xab\x9e\x2b\ +\xcd\xed\xcc\xb3\xca\xdb\x90\x37\x9c\xef\x9f\xff\xed\x12\xc2\x12\ +\xe1\x92\xb6\xa5\x86\x4b\x57\x2d\x1d\x58\xe6\xbd\xac\x6a\x39\xb2\ +\x3c\x71\x79\xdb\x0a\xe3\x15\x05\x2b\x86\x56\x06\xac\x3c\xb8\x8a\ +\xb6\x2a\x6d\xd5\x4f\xab\xed\x57\x97\xae\x7e\xbd\x26\x7a\x4d\x6b\ +\x81\x5e\xc1\xca\x82\xc1\xb5\x01\x6b\xeb\x0b\x55\x0a\xe5\x85\x7d\ +\xeb\xdc\xd7\xed\x5d\x4f\x58\x2f\x59\xdf\xb5\x61\xfa\x86\x9d\x1b\ +\x3e\x15\x89\x8a\xae\x14\xdb\x17\x97\x15\x7f\xd8\x28\xdc\x78\xe5\ +\x1b\x87\x6f\xca\xbf\x99\xdc\x94\xb4\xa9\xab\xc4\xb9\x64\xcf\x66\ +\xd2\x66\xe9\xe6\xde\x2d\x9e\x5b\x0e\x96\xaa\x97\xe6\x97\x0e\x6e\ +\x0d\xd9\xda\xb4\x0d\xdf\x56\xb4\xed\xf5\xf6\x45\xdb\x2f\x97\xcd\ +\x28\xdb\xbb\x83\xb6\x43\xb9\xa3\xbf\x3c\xb8\xbc\x65\xa7\xc9\xce\ +\xcd\x3b\x3f\x54\xa4\x54\xf4\x54\xfa\x54\x36\xee\xd2\xdd\xb5\x61\ +\xd7\xf8\x6e\xd1\xee\x1b\x7b\xbc\xf6\x34\xec\xd5\xdb\x5b\xbc\xf7\ +\xfd\x3e\xc9\xbe\xdb\x55\x01\x55\x4d\xd5\x66\xd5\x65\xfb\x49\xfb\ +\xb3\xf7\x3f\xae\x89\xaa\xe9\xf8\x96\xfb\x6d\x5d\xad\x4e\x6d\x71\ +\xed\xc7\x03\xd2\x03\xfd\x07\x23\x0e\xb6\xd7\xb9\xd4\xd5\x1d\xd2\ +\x3d\x54\x52\x8f\xd6\x2b\xeb\x47\x0e\xc7\x1f\xbe\xfe\x9d\xef\x77\ +\x2d\x0d\x36\x0d\x55\x8d\x9c\xc6\xe2\x23\x70\x44\x79\xe4\xe9\xf7\ +\x09\xdf\xf7\x1e\x0d\x3a\xda\x76\x8c\x7b\xac\xe1\x07\xd3\x1f\x76\ +\x1d\x67\x1d\x2f\x6a\x42\x9a\xf2\x9a\x46\x9b\x53\x9a\xfb\x5b\x62\ +\x5b\xba\x4f\xcc\x3e\xd1\xd6\xea\xde\x7a\xfc\x47\xdb\x1f\x0f\x9c\ +\x34\x3c\x59\x79\x4a\xf3\x54\xc9\x69\xda\xe9\x82\xd3\x93\x67\xf2\ +\xcf\x8c\x9d\x95\x9d\x7d\x7e\x2e\xf9\xdc\x60\xdb\xa2\xb6\x7b\xe7\ +\x63\xce\xdf\x6a\x0f\x6f\xef\xba\x10\x74\xe1\xd2\x45\xff\x8b\xe7\ +\x3b\xbc\x3b\xce\x5c\xf2\xb8\x74\xf2\xb2\xdb\xe5\x13\x57\xb8\x57\ +\x9a\xaf\x3a\x5f\x6d\xea\x74\xea\x3c\xfe\x93\xd3\x4f\xc7\xbb\x9c\ +\xbb\x9a\xae\xb9\x5c\x6b\xb9\xee\x7a\xbd\xb5\x7b\x66\xf7\xe9\x1b\ +\x9e\x37\xce\xdd\xf4\xbd\x79\xf1\x16\xff\xd6\xd5\x9e\x39\x3d\xdd\ +\xbd\xf3\x7a\x6f\xf7\xc5\xf7\xf5\xdf\x16\xdd\x7e\x72\x27\xfd\xce\ +\xcb\xbb\xd9\x77\x27\xee\xad\xbc\x4f\xbc\x5f\xf4\x40\xed\x41\xd9\ +\x43\xdd\x87\xd5\x3f\x5b\xfe\xdc\xd8\xef\xdc\x7f\x6a\xc0\x77\xa0\ +\xf3\xd1\xdc\x47\xf7\x06\x85\x83\xcf\xfe\x91\xf5\x8f\x0f\x43\x05\ +\x8f\x99\x8f\xcb\x86\x0d\x86\xeb\x9e\x38\x3e\x39\x39\xe2\x3f\x72\ +\xfd\xe9\xfc\xa7\x43\xcf\x64\xcf\x26\x9e\x17\xfe\xa2\xfe\xcb\xae\ +\x17\x16\x2f\x7e\xf8\xd5\xeb\xd7\xce\xd1\x98\xd1\xa1\x97\xf2\x97\ +\x93\xbf\x6d\x7c\xa5\xfd\xea\xc0\xeb\x19\xaf\xdb\xc6\xc2\xc6\x1e\ +\xbe\xc9\x78\x33\x31\x5e\xf4\x56\xfb\xed\xc1\x77\xdc\x77\x1d\xef\ +\xa3\xdf\x0f\x4f\xe4\x7c\x20\x7f\x28\xff\x68\xf9\xb1\xf5\x53\xd0\ +\xa7\xfb\x93\x19\x93\x93\xff\x04\x03\x98\xf3\xfc\x63\x33\x2d\xdb\ +\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\x25\x00\x00\x80\x83\ +\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00\x75\x30\x00\x00\xea\x60\ +\x00\x00\x3a\x98\x00\x00\x17\x6f\x92\x5f\xc5\x46\x00\x00\x04\xb3\ +\x49\x44\x41\x54\x78\xda\xec\x9b\x5d\x4c\x5b\x65\x18\xc7\xff\x6f\ +\x4b\x19\x8e\xca\x12\xc0\x36\x2d\x6b\x9a\x8d\x2e\xbb\x11\x24\x54\ +\xbe\x12\x68\x71\x4b\xb6\x25\xf3\x6e\x8a\x5c\x98\x5d\x90\x6c\x6e\ +\x44\x2f\xd8\x00\xfb\xe1\x16\xa6\x59\x84\x69\x24\x21\x82\xcb\xa2\ +\x2e\x61\x12\x58\xa2\x89\x37\x66\x03\xa7\x6e\xf8\x31\x3e\xa6\x2b\ +\xf3\xaa\x4a\xa6\xb6\x7c\xa4\xf5\x60\x32\x53\x3e\xd2\x7a\xce\xf1\ +\x02\x06\x6b\x7b\xca\xd6\xd3\x73\x0e\xa7\xe9\x79\xae\xda\x73\xd2\ +\xf3\xbc\xef\xef\x3c\xef\xf3\x3e\x1f\x6f\x09\x4e\x7d\xcd\x22\x83\ +\x45\x85\x0c\x17\x05\x80\x02\x40\x01\xa0\x00\x50\x00\x6c\x85\x38\ +\x6c\x06\x50\x6e\x2b\x28\xb7\x15\x6d\x75\x86\xcc\x03\xd0\x5a\x67\ +\x84\x8a\x00\x2a\x02\xbc\x69\x33\x66\x1e\x00\x15\xe1\xfe\xac\xf8\ +\x00\x05\x80\x02\x20\x33\x00\x30\x2c\xf7\xe7\xb4\x00\xf0\x7a\x8d\ +\x1e\x01\x67\x39\x82\xae\x72\x9c\xdd\x57\xc4\x4b\xf1\x07\x3f\xcc\ +\x83\x61\x57\x27\xdf\x35\x3a\xc7\xeb\x19\x1d\xfb\x77\x22\xe8\x2a\ +\x47\xc0\x59\x8e\xe6\x2a\x3d\xaf\x67\x10\x3e\xe9\x70\xc0\x55\x0e\ +\xcd\x23\xae\xfb\xd2\x64\x10\x8e\x61\xbf\xa4\x6f\xae\xeb\xa0\x09\ +\xc7\x2a\x74\xeb\xdf\x23\x34\x0b\xfd\xbb\xbf\x48\x63\x01\xb1\xbb\ +\xd6\xf1\x0a\x1d\xce\xed\xdf\x29\xd9\xe4\xcf\x1f\x88\x9e\x3c\x00\ +\x10\x22\xe1\x12\xe8\x1b\x0f\xc6\x5d\x7b\xa3\x46\x0f\x87\x4d\xfc\ +\x88\xce\x65\x37\xe2\x64\xa5\x2e\xee\xfa\x87\xb7\x03\xd2\x01\xe8\ +\xf8\x66\x06\x97\x26\xe3\x21\x34\x57\xeb\x45\x07\x70\x92\x63\xad\ +\x7f\x34\x11\xc4\xdb\xdf\xcd\xf2\x7a\x5e\x16\xef\x58\x7e\xd8\x0f\ +\x02\x44\x99\xe2\x52\x84\xd9\x74\xd9\x1c\xab\xd0\xa1\xa1\x24\x1f\ +\x96\x82\x1c\x68\xb3\xd5\xeb\x11\x20\xc3\x02\xa1\x30\x8d\xe9\x85\ +\x15\x0c\xdd\xfb\x07\x9f\xdc\x09\x22\x91\x63\x5a\xfe\x8f\x41\x6e\ +\xf6\xc6\x7b\xbb\x38\x11\x84\x7b\x84\xbf\xff\x21\xa9\xd6\x04\xdd\ +\xf5\x46\xbc\x56\xa9\xc3\x72\x84\xc1\xd1\xcf\xef\x63\xdc\x1f\x8a\ +\xba\xaf\x26\x04\xdd\x87\xcd\x68\x28\xc9\x47\xb6\xfa\xc9\x16\x6a\ +\x98\x66\x71\xf5\xd7\x05\x9c\xfa\xca\x07\x9a\x8d\x1e\x5e\xb5\x49\ +\x8b\xfe\x97\x8a\x91\xa3\x21\xe8\x1b\x0f\xa2\xf3\xd6\x5c\x4a\x16\ +\x45\xc4\x2c\x8a\x1e\xb0\xec\xc0\xa7\x47\x76\x63\xbb\x86\x5f\xb8\ +\xb1\x14\x61\xd0\xf4\xc5\x7d\x8c\x4c\x3f\x48\xbf\x40\xa8\xad\xce\ +\x80\xa1\x46\x0b\xef\xc9\x03\xc0\x76\x8d\x0a\x43\x8d\x16\x51\xd3\ +\x65\x51\x00\x9c\xae\x35\xc0\x69\x17\x2e\xc5\x75\xda\x8d\x38\x5d\ +\x6b\x48\x0f\x00\x2f\xec\xce\x83\xbb\x5e\xf8\xfc\xde\x5d\x6f\x84\ +\x7d\x57\x9e\xbc\x01\x64\xa9\x08\xae\xbc\x5c\x2c\x9a\xb9\x0e\x34\ +\x14\x43\x4d\x88\x7c\x01\xf4\xbc\x68\x4e\x69\xcd\x3f\x89\x4f\xe8\ +\x3e\x6c\x96\x27\x00\x15\x01\x8e\x3c\x9b\x2f\x7a\x20\xf4\x4a\x49\ +\x3e\x88\x1c\x01\x34\x59\x75\x51\x09\x92\x58\xa2\x51\x13\x34\x59\ +\x9f\x91\x1f\x80\xc6\x52\xf1\xdf\xfe\x86\xae\x02\xf9\x01\xb0\x14\ +\xe4\x48\x06\x60\x4f\xa1\x70\xba\x12\xe6\x02\x0e\x9b\x61\xbd\x74\ +\x1d\x5b\xc9\xe9\x1a\x9d\xc3\x7b\xdf\xcf\x47\x5d\xd7\x66\xab\x25\ +\x03\xc0\xa5\x2b\xd9\xf1\x3e\xd6\x02\xb8\x1e\xf6\xd0\xd9\x71\xd5\ +\xf1\xa5\x2c\x6d\x73\xe9\x4a\x76\xbc\xa2\x87\xc2\xe9\x22\x09\x01\ +\x3c\xac\xd9\xc5\x4a\xa2\x1a\x9e\x94\x85\x4d\x2e\x5d\xc9\x8e\x57\ +\xf0\x6c\xf0\xcf\xb6\x32\xe4\x6d\x93\xc6\x0f\x3c\x58\xa1\xb1\xeb\ +\x7d\x8f\xbc\x76\x01\x2f\xb5\x22\x99\x05\x78\xa9\x65\xf9\x6d\x83\ +\x83\x53\x94\x64\x00\x06\xa7\x16\xe4\x07\xa0\xff\x2e\x85\x08\x2d\ +\xbe\x23\x88\xd0\x2c\xfa\xef\x52\xf2\x03\xc0\xb0\xc0\x80\x04\x56\ +\x30\x30\x45\x41\x48\xcc\x82\x6e\x83\xad\xd7\x7c\x58\x0c\x33\xa2\ +\x4d\x3e\x14\xa6\xd1\x7a\xcd\x27\xdf\x74\x98\x61\x81\xc6\xab\xbf\ +\x43\x8c\x85\xc0\x02\x68\x18\x9c\x16\x7c\xbb\x15\x3c\x10\xfa\xf1\ +\xaf\x10\xde\x1a\x11\xbe\x4d\xe6\x1c\xf6\x63\x2c\xa6\xe2\x2c\x4b\ +\x00\xc0\x6a\xa3\xc2\x31\xec\x17\xc4\x12\x58\x00\xed\xd7\xfd\x9c\ +\x8d\x18\xd9\x02\x00\x56\x1b\xa6\x87\x2e\x7b\x37\x6d\x96\x3c\x4e\ +\x96\x22\x0c\x0e\x5d\xf6\xe2\xe3\x3b\x41\xd1\xfc\x4a\xca\x00\x5c\ +\x76\x23\x7c\xed\x65\xf0\xb6\x94\xa2\xca\xa4\x8d\xba\x37\x39\x1b\ +\x82\xf9\x82\x07\x9f\x79\x16\x10\x4e\x62\x8b\x0c\xd3\x2c\xae\x78\ +\x28\x98\x2f\x78\x30\x39\x1b\x6d\xf6\xd5\x26\x2d\xbc\x2d\xa5\xf0\ +\xb5\x97\xc1\x21\x40\xe5\x39\xa5\x50\xb8\xf3\xa0\x09\xc7\x1f\x69\ +\x8d\xfd\xbd\x18\xc1\xde\xee\x7b\x09\x14\x09\xd3\x1a\xfb\xad\xe5\ +\x39\x14\xe6\x6e\x64\xf1\x17\x27\x82\x70\xa5\xe0\x73\xb2\x84\x9a\ +\x3c\x00\x3c\xb5\x49\x41\x94\x5d\x5b\x16\xa9\xae\xe5\x58\x1d\x27\ +\xd6\x3a\xc5\x7c\x21\xf0\x5a\x02\x67\xf7\x15\xc5\x4d\x1e\x00\xfa\ +\xc6\x02\x10\x5b\x7a\xc7\xe3\x75\x9c\xa8\xd4\xa1\x83\xe7\xf9\x04\ +\x7e\x47\x64\x38\xda\xe0\xbd\x63\x01\x74\x8e\xce\x8b\x0e\xa0\xf3\ +\xd6\x1c\x7a\x39\x40\x37\x57\xe9\xa4\x03\x10\xd3\xb0\x45\xcf\xed\ +\x00\xce\xdc\x98\x91\x2c\x19\x3a\x73\x63\x06\x3d\x31\x07\x22\x58\ +\x9e\x9e\x4c\x8d\x9a\xa3\x1d\xc9\xfe\x68\x85\x66\x50\x6b\x7e\x1a\ +\xec\xda\xe4\xcf\x7d\x9b\xfc\xe1\x04\x87\xcd\x80\x2f\x5f\xdd\x8b\ +\x76\x9b\x11\x2c\x80\x9f\x7c\xc9\x05\x39\x37\xff\xf8\x17\xdb\xb2\ +\x54\x78\xbe\x28\x17\x34\x03\xbc\x73\x73\x16\x13\x33\x8b\xd2\xee\ +\x02\xa9\x08\xe5\xb6\x46\xed\x02\x85\xe7\x7f\xde\x8a\x61\x28\x67\ +\x85\x95\xa2\xa8\x02\x40\x01\xb0\x35\x92\xd6\x67\x85\x85\x10\x21\ +\xce\x0a\x0b\x21\x44\xf9\xeb\xac\xe2\x03\x14\x00\x0a\x80\x4c\x96\ +\xff\x07\x00\x85\xfa\xad\x10\xc5\x6e\x22\x46\x00\x00\x00\x00\x49\ +\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x03\xd7\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ +\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ +\x01\x00\x9a\x9c\x18\x00\x00\x03\x79\x49\x44\x41\x54\x58\x85\xed\ +\x56\x5f\x68\x5b\x75\x14\xfe\xce\xcd\xcd\xbf\xf6\xa6\x59\x96\x7b\ +\x93\xdb\x88\x6b\x93\x9b\xd2\x91\xcd\x0e\x11\x1f\x2a\x6c\x63\x22\ +\x0a\xb3\x74\x22\xfa\x30\x1f\xf6\xb0\x37\xeb\x7c\xe8\x60\x0a\xa2\ +\x20\x88\xff\xc0\xaa\xb5\xe2\x9c\x6f\xfa\x30\x11\x61\x9b\x4e\x41\ +\x11\x04\x61\xa0\xf5\xcf\x1c\x6c\xa6\xf6\xbf\x12\x5d\x93\x26\x59\ +\x48\x73\x13\xdb\xdc\xe4\x1e\x5f\x8c\xa4\x35\x49\x6f\xdb\x21\x0c\ +\xfa\xbd\xfd\xce\xf9\xee\x77\x3e\xce\xbd\xe7\xdc\x1f\xb0\x8d\x6d\ +\xdc\xca\x88\xc5\xe0\xb8\x0b\xb0\x6f\x45\xc3\xb6\x95\x87\x3d\x82\ +\x72\x6a\xc5\xe3\xbe\x3b\xa7\xff\xf5\xed\x66\x35\x04\xab\xc4\x68\ +\x48\xbe\x53\x53\x95\x37\x57\x05\x89\x45\x66\x61\x55\x07\x22\xaa\ +\x3c\xa6\x05\x83\x7b\x6e\xba\x01\xb4\xed\x88\x9b\x30\x07\xb4\x40\ +\xa0\xbf\xce\x41\x82\x05\x33\x51\x3b\xf5\xa8\xf2\x01\x32\xe9\xbe\ +\x1d\xa9\xd4\x94\x65\xdd\x8d\x20\x1c\x94\x1f\xd4\x82\xca\x0b\xcd\ +\xf2\x9a\x2a\xbf\xaa\x75\x2a\x0f\x6c\x44\x93\x5a\x25\xbb\x15\x65\ +\x5f\x9b\x92\x9e\x88\xc7\x51\xae\x0b\x0b\xd1\x4e\xff\x51\xaf\x24\ +\x3e\xb1\x5c\xe6\xdd\x00\xe0\xb2\xd3\xaf\x4b\xa5\xea\x5b\xd3\xd7\ +\x33\x1f\x01\xe0\x1a\xb1\xab\xab\xcb\xe5\x30\x4a\xbb\xa7\xaf\xa7\ +\xaf\x6c\xca\xc0\x3f\xef\xfc\xb0\xc9\x3c\x3c\x9f\xca\x7c\xbe\x6b\ +\x97\xd7\xe7\x13\x9c\x5f\x45\x42\x62\xef\xd0\x11\x49\xea\x0b\x3b\ +\x00\x00\x3f\xcf\x96\xf1\xce\x27\xba\xfe\xfb\x62\xf9\xda\x72\x9e\ +\xef\x9f\xcc\x66\x0b\x5a\xc0\x3f\x68\x0a\x78\x83\x98\xce\xcd\xa5\ +\x32\xa7\x36\x65\x00\x00\xb4\x40\xa0\x9f\x05\x1e\x98\x4b\xa6\x9f\ +\xeb\xeb\x0e\x8c\x1f\x3d\xe4\xde\x37\xfc\x68\xc7\x7f\x46\x8f\x19\ +\x78\xe9\xc3\x7c\xf9\xe2\xa5\xe5\xef\xaf\xfc\xb6\xb8\x5f\x53\xe5\ +\x97\x05\xb6\x5d\x98\x4e\xa5\xc6\x5b\xe9\xaf\x6b\xa0\x86\x68\xa7\ +\xff\xb1\xbe\xb0\xf3\xcc\xd9\x67\xfd\x52\x33\x0e\x33\xf0\xc8\xf3\ +\x19\x7d\x6a\xa1\x7a\x6c\xfa\x8f\xc5\xf3\x56\x74\x2d\x4f\x81\x57\ +\x12\x4f\x0c\x3d\x24\x35\x2d\x0e\x00\x44\xc0\xe3\x47\x3c\x92\xd7\ +\xc5\x4f\x5a\xd5\x15\x1b\x05\x63\x31\x38\x96\xb3\xca\x49\x10\x8b\ +\x00\x25\xe6\x92\xe9\xf7\x0d\xc3\xec\xdd\xdb\xbd\xfe\xd2\xdb\xdb\ +\x2d\x22\xa7\xe3\x10\x00\x84\x83\xf2\x71\x22\x84\xc0\x64\xdc\x9e\ +\x4a\x8f\x7c\x03\x54\xd6\xf2\x1b\x76\xc0\x1d\x07\x13\x99\x06\x98\ +\x2a\x4c\x66\x05\x00\xaa\x4c\xc4\xdc\x88\xbd\x1a\xf5\x14\x81\xa8\ +\x02\xa6\x0a\x11\x1b\xcd\xf8\x0d\x3b\xf0\x13\x60\x20\x99\x1d\xa9\ +\x8f\x39\xed\x34\x71\x75\xde\xb8\xe7\x40\x9f\xb3\xa5\x81\xab\xf3\ +\x06\x7c\x12\xbe\x06\x80\xd9\x64\xfa\x83\x5a\x7c\xb6\x09\xdf\xf2\ +\x37\x50\x28\x56\xdf\x7e\xef\x33\xbd\xd0\x8a\xc3\x0c\xbc\xfb\xa9\ +\xae\x17\x4a\xe6\xa8\x55\xdd\x75\x7f\x46\xe1\x4e\x79\xbf\x5f\x6a\ +\x1b\x9a\x49\x66\x46\xed\xa2\x73\x30\xaf\x9b\x81\xfe\x98\xcb\x46\ +\x6b\xe6\x87\x19\x78\xf1\x6c\xbe\x7c\x79\xb2\xfc\xdd\xb5\x44\xe6\ +\x99\x48\x50\x19\xf1\x75\xb4\x17\x73\x7a\x29\xd1\x58\xd9\x82\x81\ +\x88\x2a\x8f\x09\x4c\xc3\x10\xe8\x74\x4e\x2f\xcd\x78\xc8\xf5\xf1\ +\xec\x82\x79\xef\x17\x3f\xae\xf8\x76\x76\xd8\x1c\xed\x2e\x42\x69\ +\x85\x31\x3e\x61\xe0\xa9\x33\xf9\xc2\xe5\xa9\xf2\x0f\xd5\x92\x30\ +\x90\x2a\x16\x0d\x9f\xa7\x4d\x24\xa6\xd3\x3e\xa9\x3d\x92\x2b\x96\ +\xbe\x6c\x56\xa3\xe5\x1e\x08\x07\x02\x77\xb8\xe5\xc5\xc9\x35\xab\ +\x18\x3d\xb7\x29\x0f\x7b\xdd\x38\x51\xa9\xf0\x9e\x2a\x40\x4e\x3b\ +\xfd\x92\x2f\x61\x6c\xfa\xcf\xf4\xb9\x7a\x5e\x2c\x06\x47\xf9\xc6\ +\xce\xe8\x4c\xf2\x46\xbc\x55\x1d\xcb\xd0\x02\xfe\x41\x4d\x95\x5f\ +\x69\x96\x8f\x04\x95\xd7\x22\xaa\x7c\x78\x23\x9a\xd6\xef\x03\xd1\ +\xa8\xd3\x14\xf0\xba\xc0\xb6\x7f\x37\x5c\x38\x28\x1f\xd7\x54\xe5\ +\x58\x9d\xda\x05\x06\x8f\xc6\x62\x70\xdc\x74\x03\xd5\xa5\xa5\x5e\ +\x62\x3a\x5f\xbf\xdb\x89\x10\x62\x46\xa8\x76\x9e\x5b\x48\x5f\x22\ +\x16\x2e\xae\x64\x83\x3d\x56\x75\xb7\x84\x48\x50\x79\x5a\x53\xe5\ +\x93\xff\x4b\xb1\x46\x38\x08\x88\x07\x9b\x2c\xb3\x6d\x6c\xe3\x96\ +\xc1\xdf\xd0\x8a\x3c\x4e\x76\x81\x84\x65\x00\x00\x00\x00\x49\x45\ +\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x03\xd7\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ +\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ +\x01\x00\x9a\x9c\x18\x00\x00\x03\x79\x49\x44\x41\x54\x58\x85\xed\ +\x56\x5f\x68\x5b\x75\x14\xfe\xce\xcd\xcd\xbf\xf6\xa6\x59\x96\x7b\ +\x93\xdb\x88\x6b\x93\x9b\xd2\x91\xcd\x0e\x11\x1f\x2a\x6c\x63\x22\ +\x0a\xb3\x74\x22\xfa\x30\x1f\xf6\xb0\x37\xeb\x7c\xe8\x60\x0a\xa2\ +\x20\x88\xff\xc0\xaa\xb5\xe2\x9c\x6f\xfa\x30\x11\x61\x9b\x4e\x41\ +\x11\x04\x61\xa0\xf5\xcf\x1c\x6c\xa6\xf6\xbf\x12\x5d\x93\x26\x59\ +\x48\x73\x13\xdb\xdc\xe4\x1e\x5f\x8c\xa4\x35\x49\x6f\xdb\x21\x0c\ +\xfa\xbd\xfd\xce\xf9\xee\x77\x3e\xce\xbd\xe7\xdc\x1f\xb0\x8d\x6d\ +\xdc\xca\x88\xc5\xe0\xb8\x0b\xb0\x6f\x45\xc3\xb6\x95\x87\x3d\x82\ +\x72\x6a\xc5\xe3\xbe\x3b\xa7\xff\xf5\xed\x66\x35\x04\xab\xc4\x68\ +\x48\xbe\x53\x53\x95\x37\x57\x05\x89\x45\x66\x61\x55\x07\x22\xaa\ +\x3c\xa6\x05\x83\x7b\x6e\xba\x01\xb4\xed\x88\x9b\x30\x07\xb4\x40\ +\xa0\xbf\xce\x41\x82\x05\x33\x51\x3b\xf5\xa8\xf2\x01\x32\xe9\xbe\ +\x1d\xa9\xd4\x94\x65\xdd\x8d\x20\x1c\x94\x1f\xd4\x82\xca\x0b\xcd\ +\xf2\x9a\x2a\xbf\xaa\x75\x2a\x0f\x6c\x44\x93\x5a\x25\xbb\x15\x65\ +\x5f\x9b\x92\x9e\x88\xc7\x51\xae\x0b\x0b\xd1\x4e\xff\x51\xaf\x24\ +\x3e\xb1\x5c\xe6\xdd\x00\xe0\xb2\xd3\xaf\x4b\xa5\xea\x5b\xd3\xd7\ +\x33\x1f\x01\xe0\x1a\xb1\xab\xab\xcb\xe5\x30\x4a\xbb\xa7\xaf\xa7\ +\xaf\x6c\xca\xc0\x3f\xef\xfc\xb0\xc9\x3c\x3c\x9f\xca\x7c\xbe\x6b\ +\x97\xd7\xe7\x13\x9c\x5f\x45\x42\x62\xef\xd0\x11\x49\xea\x0b\x3b\ +\x00\x00\x3f\xcf\x96\xf1\xce\x27\xba\xfe\xfb\x62\xf9\xda\x72\x9e\ +\xef\x9f\xcc\x66\x0b\x5a\xc0\x3f\x68\x0a\x78\x83\x98\xce\xcd\xa5\ +\x32\xa7\x36\x65\x00\x00\xb4\x40\xa0\x9f\x05\x1e\x98\x4b\xa6\x9f\ +\xeb\xeb\x0e\x8c\x1f\x3d\xe4\xde\x37\xfc\x68\xc7\x7f\x46\x8f\x19\ +\x78\xe9\xc3\x7c\xf9\xe2\xa5\xe5\xef\xaf\xfc\xb6\xb8\x5f\x53\xe5\ +\x97\x05\xb6\x5d\x98\x4e\xa5\xc6\x5b\xe9\xaf\x6b\xa0\x86\x68\xa7\ +\xff\xb1\xbe\xb0\xf3\xcc\xd9\x67\xfd\x52\x33\x0e\x33\xf0\xc8\xf3\ +\x19\x7d\x6a\xa1\x7a\x6c\xfa\x8f\xc5\xf3\x56\x74\x2d\x4f\x81\x57\ +\x12\x4f\x0c\x3d\x24\x35\x2d\x0e\x00\x44\xc0\xe3\x47\x3c\x92\xd7\ +\xc5\x4f\x5a\xd5\x15\x1b\x05\x63\x31\x38\x96\xb3\xca\x49\x10\x8b\ +\x00\x25\xe6\x92\xe9\xf7\x0d\xc3\xec\xdd\xdb\xbd\xfe\xd2\xdb\xdb\ +\x2d\x22\xa7\xe3\x10\x00\x84\x83\xf2\x71\x22\x84\xc0\x64\xdc\x9e\ +\x4a\x8f\x7c\x03\x54\xd6\xf2\x1b\x76\xc0\x1d\x07\x13\x99\x06\x98\ +\x2a\x4c\x66\x05\x00\xaa\x4c\xc4\xdc\x88\xbd\x1a\xf5\x14\x81\xa8\ +\x02\xa6\x0a\x11\x1b\xcd\xf8\x0d\x3b\xf0\x13\x60\x20\x99\x1d\xa9\ +\x8f\x39\xed\x34\x71\x75\xde\xb8\xe7\x40\x9f\xb3\xa5\x81\xab\xf3\ +\x06\x7c\x12\xbe\x06\x80\xd9\x64\xfa\x83\x5a\x7c\xb6\x09\xdf\xf2\ +\x37\x50\x28\x56\xdf\x7e\xef\x33\xbd\xd0\x8a\xc3\x0c\xbc\xfb\xa9\ +\xae\x17\x4a\xe6\xa8\x55\xdd\x75\x7f\x46\xe1\x4e\x79\xbf\x5f\x6a\ +\x1b\x9a\x49\x66\x46\xed\xa2\x73\x30\xaf\x9b\x81\xfe\x98\xcb\x46\ +\x6b\xe6\x87\x19\x78\xf1\x6c\xbe\x7c\x79\xb2\xfc\xdd\xb5\x44\xe6\ +\x99\x48\x50\x19\xf1\x75\xb4\x17\x73\x7a\x29\xd1\x58\xd9\x82\x81\ +\x88\x2a\x8f\x09\x4c\xc3\x10\xe8\x74\x4e\x2f\xcd\x78\xc8\xf5\xf1\ +\xec\x82\x79\xef\x17\x3f\xae\xf8\x76\x76\xd8\x1c\xed\x2e\x42\x69\ +\x85\x31\x3e\x61\xe0\xa9\x33\xf9\xc2\xe5\xa9\xf2\x0f\xd5\x92\x30\ +\x90\x2a\x16\x0d\x9f\xa7\x4d\x24\xa6\xd3\x3e\xa9\x3d\x92\x2b\x96\ +\xbe\x6c\x56\xa3\xe5\x1e\x08\x07\x02\x77\xb8\xe5\xc5\xc9\x35\xab\ +\x18\x3d\xb7\x29\x0f\x7b\xdd\x38\x51\xa9\xf0\x9e\x2a\x40\x4e\x3b\ +\xfd\x92\x2f\x61\x6c\xfa\xcf\xf4\xb9\x7a\x5e\x2c\x06\x47\xf9\xc6\ +\xce\xe8\x4c\xf2\x46\xbc\x55\x1d\xcb\xd0\x02\xfe\x41\x4d\x95\x5f\ +\x69\x96\x8f\x04\x95\xd7\x22\xaa\x7c\x78\x23\x9a\xd6\xef\x03\xd1\ +\xa8\xd3\x14\xf0\xba\xc0\xb6\x7f\x37\x5c\x38\x28\x1f\xd7\x54\xe5\ +\x58\x9d\xda\x05\x06\x8f\xc6\x62\x70\xdc\x74\x03\xd5\xa5\xa5\x5e\ +\x62\x3a\x5f\xbf\xdb\x89\x10\x62\x46\xa8\x76\x9e\x5b\x48\x5f\x22\ +\x16\x2e\xae\x64\x83\x3d\x56\x75\xb7\x84\x48\x50\x79\x5a\x53\xe5\ +\x93\xff\x4b\xb1\x46\x38\x08\x88\x07\x9b\x2c\xb3\x6d\x6c\xe3\x96\ +\xc1\xdf\xd0\x8a\x3c\x4e\x76\x81\x84\x65\x00\x00\x00\x00\x49\x45\ +\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x03\xdd\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x20\x00\x00\x00\x20\x08\x06\x00\x00\x00\x73\x7a\x7a\xf4\ +\x00\x00\x00\x04\x73\x42\x49\x54\x08\x08\x08\x08\x7c\x08\x64\x88\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ +\x01\x00\x9a\x9c\x18\x00\x00\x03\x7f\x49\x44\x41\x54\x58\x85\xed\ +\x94\x5f\x68\x5b\x55\x1c\xc7\xbf\xbf\x73\xee\x6d\xb3\xa6\x4d\x72\ +\x6f\x72\x9b\x9b\xcc\x2e\x6b\x13\x27\x9d\x54\x10\x87\x68\xd9\x10\ +\x44\xc1\xc7\x22\xc2\xd0\x87\x0d\x64\x32\xa6\x75\x0a\x56\xf6\xe6\ +\xde\xa4\x3e\xf8\x20\x1b\x0e\x5f\xe6\x8b\xa2\xf8\xe8\x83\xf8\xe8\ +\xbf\xee\x41\x2b\x2a\xce\x51\xba\xae\xb0\x9a\x36\xb1\x37\x49\x75\ +\xc6\xb4\xb7\xc9\x39\x3f\x1f\x5c\xb2\xb6\x24\xed\x1d\x1b\x82\xd8\ +\xef\xd3\xf9\xf3\x3d\xdf\xdf\xe7\x1c\xce\x39\xc0\xae\x76\xf5\x5f\ +\x56\xce\xb6\x23\x07\x1d\xa7\xf7\x4e\x32\xc4\x9d\x2c\x66\x43\x8c\ +\xaf\x09\x9c\xfa\x57\x00\x86\xdc\xf8\xc9\xac\xeb\x1c\xdb\x04\x20\ +\x20\x00\xc8\xcd\x3e\xe7\xf8\x60\x32\xf1\xfc\x5d\x07\x90\xe0\xaf\ +\x00\x4c\x0e\x59\x56\xb4\x05\xc0\x58\x22\xc2\x52\xb3\x9f\xc9\x44\ +\x63\x00\xde\x24\x85\xaf\x83\xe7\x06\x54\xa5\xba\x5a\xb2\xc2\xe1\ +\x14\x19\xc2\x5a\xa9\xd6\x7e\x02\x80\xdf\xff\xaa\xfd\xd0\x6c\x03\ +\x40\x3c\x14\x79\x96\x88\xf3\xf3\xcb\xa5\x4f\x82\xe6\xd2\x76\x73\ +\x39\xdb\xee\x9b\xab\x54\x6e\x6c\x9d\x18\x4c\x25\x8e\x10\xe3\x28\ +\x83\xee\x07\x00\xd2\x7c\x85\x98\x3e\xbc\xe6\x79\x97\xb6\x7a\x73\ +\xb6\x1d\x69\x97\xd1\x54\xc7\x13\xb8\xd7\xb5\x87\x59\xd2\xb4\x15\ +\xee\x15\xa9\x4c\x6d\xda\xf3\xa0\x86\x2c\x2b\x1a\x8b\xf6\x7e\xdc\ +\x6d\xf2\x0b\xc7\x9f\x0c\xf7\x9c\x1e\xeb\x4b\x8c\x8d\xee\x09\xdb\ +\x11\x11\xf9\x65\xc1\x3f\xd6\x17\xee\x1d\xdd\xd3\x17\xfd\xac\x5a\ +\xad\xd6\x73\xb9\x5c\x77\xcc\x94\xaf\xc3\xc0\x07\x56\x28\xfc\xe9\ +\x4a\xad\x56\xb9\xdd\x13\x40\x36\x6d\x0d\x40\x1b\x6f\x31\xf4\x97\ +\x56\xb1\x7c\xb1\xe2\xc6\xbf\x38\x32\xd2\x5d\x3f\xff\x4a\xec\x70\ +\x97\x90\x9b\xe0\xd7\x95\x52\x27\xdf\xfe\xe3\xd2\xb7\x33\xbe\x7f\ +\xad\x58\x7a\x2a\x9b\x74\x4e\x01\x78\x44\x98\xea\xcc\xd5\x7c\x65\ +\xb1\x53\x8d\x6d\x01\x36\xc1\xb8\xce\xc4\x40\xbf\x7c\xfa\xf3\x49\ +\xe7\xd1\xed\x7c\x4f\x4c\x94\xa6\x8b\x95\xc6\xfb\x73\x45\xef\xdd\ +\x20\xb9\xc1\xff\x01\xcd\xe3\x17\x4e\xc7\xf6\xee\x64\x3b\xf7\x52\ +\xa4\x9f\x81\x57\x83\xc6\xb6\x05\x78\x08\x30\x33\x99\x68\xec\xe6\ +\xb3\x12\xd9\xb4\x35\x60\x9a\xe0\xc1\xb4\xb9\x6f\xa7\xc0\xe1\xc1\ +\xae\x7d\x52\x22\x94\x71\x1c\x17\x80\x6c\xe6\x3c\x06\x18\x81\x01\ +\x56\xdc\xc4\x19\xc3\x37\x66\x0c\xdf\x98\x19\xea\x4f\x1c\x55\x75\ +\xc3\x0e\x99\xf8\x33\xe8\xae\xac\x1e\x39\x67\x4a\x65\x67\x5d\xe7\ +\xb9\x66\xce\xaf\x49\xe7\xb5\x76\xde\x40\x77\x20\xe3\x38\xae\x29\ +\xe9\xfb\xcb\x17\x93\xe9\x20\xfe\x91\x13\x4b\xcb\xab\xa2\x3e\x9c\ +\xcf\xdf\x68\x7b\xf3\x37\x2a\xd0\x1d\xb8\xee\x79\x45\x05\xbd\xf2\ +\xe3\xec\xfa\xcc\x4e\xde\xa9\xcb\x6b\x3f\xeb\x86\x58\x08\x52\x1c\ +\xe8\x70\x02\x39\xd7\x79\x91\xa1\xdf\xb8\xc9\x38\xcd\x00\x43\xeb\ +\x35\x69\x8a\x2e\x43\x52\x88\xb4\x6e\x0b\xde\x10\x42\x0b\xcd\xfe\ +\xaa\xd2\xbe\x64\x11\x22\x80\x00\x7d\x08\x00\x18\x38\x3b\x5f\x2c\ +\xbf\x17\x08\x00\x80\x38\xe8\x38\x3d\x00\x50\x16\x82\xe3\x5a\x93\ +\x21\x04\x37\xb4\xa6\x66\xbf\xdd\xa2\xb2\x10\x9c\x6c\xc2\x6c\xf1\ +\x5e\xf1\xbc\x1a\x00\x1d\x14\x00\x00\xb0\xdf\x75\x33\x04\x75\x88\ +\x98\xcd\xd6\x02\x29\x59\xb2\x58\x9c\x2d\x14\xbe\xd9\xe8\x3d\x90\ +\x4a\x1d\x66\xa8\x01\xa5\x35\x37\xc7\x98\xa8\x4e\xd4\xf8\x6e\xbe\ +\xb0\xb2\xd0\xa9\x46\xdb\xa7\x71\x6b\xb2\xb1\x5f\x13\x3f\x08\x2d\ +\xbb\x5b\x83\x0d\xe6\x3a\xfb\x63\x07\x9c\xf8\xc8\xac\x57\xbe\xf0\ +\x4f\xf1\xc4\x89\x75\xf6\x1f\x97\x30\xf2\x80\x68\x6d\x8a\x48\xf9\ +\x80\xf1\x1b\x80\x8e\x00\x81\x7f\xc2\x8d\xba\x2f\x1e\x4f\xfb\x06\ +\x9d\x93\x24\xa6\x08\x60\xad\xd5\xa8\x62\xf1\xf2\x75\xcf\x2b\xde\ +\x6e\x56\x20\x80\xac\xeb\x4c\x30\xf8\x99\xd6\x00\xd3\x94\x20\x39\ +\xa9\xb5\x3a\x4f\xac\xb4\x69\x86\xc6\x7d\xed\x9f\x25\xd0\xc3\x2d\ +\x8f\xc6\x47\xf3\xcb\xa5\x77\xee\x0a\x40\xce\x75\x1d\x12\xea\xd6\ +\x37\x2c\x94\x77\x35\x5f\x59\x7c\x20\x99\x0c\x7b\x42\x70\xa1\x50\ +\xa8\xe5\xf6\xda\xf7\x10\xcb\x44\x2b\x98\x8d\xfc\x6c\xa1\x50\x0a\ +\x92\xbf\xab\x5d\xfd\xbf\xf5\x37\xb2\xeb\x52\xa6\x18\x05\x8f\xcd\ +\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +" + +qt_resource_name = b"\ +\x00\x03\ +\x00\x00\x78\xc3\ +\x00\x72\ +\x00\x65\x00\x73\ +\x00\x08\ +\x02\x33\x50\x87\ +\x00\x38\ +\x00\x5f\x00\x30\x00\x30\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x08\ +\x02\x33\x50\x07\ +\x00\x34\ +\x00\x5f\x00\x30\x00\x30\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x17\ +\x07\x37\x30\x67\ +\x00\x64\ +\x00\x61\x00\x79\x00\x74\x00\x69\x00\x6d\x00\x65\x00\x5f\x00\x65\x00\x64\x00\x69\x00\x74\x00\x6f\x00\x72\x00\x5f\x00\x6c\x00\x6f\ +\x00\x67\x00\x6f\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x09\ +\x02\x33\x3d\x87\ +\x00\x32\ +\x00\x30\x00\x5f\x00\x30\x00\x30\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x08\ +\x02\x33\x51\x87\ +\x00\x30\ +\x00\x5f\x00\x30\x00\x30\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x08\ +\x0a\x61\x5a\xa7\ +\x00\x69\ +\x00\x63\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x09\ +\x02\x33\x3e\x47\ +\x00\x31\ +\x00\x32\x00\x5f\x00\x30\x00\x30\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x09\ +\x02\x33\x3e\xc7\ +\x00\x31\ +\x00\x36\x00\x5f\x00\x30\x00\x30\x00\x2e\x00\x70\x00\x6e\x00\x67\ +" + +qt_resource_struct = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x03\ +\x00\x00\x00\x6c\x00\x00\x00\x00\x00\x01\x00\x00\x1e\xd7\ +\x00\x00\x00\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x49\x23\ +\x00\x00\x00\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x45\x48\ +\x00\x00\x00\xc8\x00\x00\x00\x00\x00\x01\x00\x00\x4c\xfe\ +\x00\x00\x00\x22\x00\x00\x00\x00\x00\x01\x00\x00\x03\x96\ +\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x00\x84\x00\x00\x00\x00\x00\x01\x00\x00\x22\xc1\ +\x00\x00\x00\x38\x00\x00\x00\x00\x00\x01\x00\x00\x07\x83\ +\x00\x00\x00\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x35\xbc\ +\x00\x00\x00\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x26\x30\ +" + +def qInitResources(): + QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources() diff --git a/RenderPipelineFile/toolkit/import_sun_data/README.md b/RenderPipelineFile/toolkit/import_sun_data/README.md index 732c6841..ae0bfde3 100644 --- a/RenderPipelineFile/toolkit/import_sun_data/README.md +++ b/RenderPipelineFile/toolkit/import_sun_data/README.md @@ -1,14 +1,14 @@ - -## Importing real-world sun data - -This script imports real world sun position data into the pipeline. -Edit `import_data.py` so it contains the date and location where the data -should be fetched for. - -The script will then download the sun data for different times of the day -and configure the render pipeline to match those locations. - - -### Requirements - -- Internet connection + +## Importing real-world sun data + +This script imports real world sun position data into the pipeline. +Edit `import_data.py` so it contains the date and location where the data +should be fetched for. + +The script will then download the sun data for different times of the day +and configure the render pipeline to match those locations. + + +### Requirements + +- Internet connection diff --git a/RenderPipelineFile/toolkit/import_sun_data/import_data.py b/RenderPipelineFile/toolkit/import_sun_data/import_data.py index 7119d21d..a43ce52d 100644 --- a/RenderPipelineFile/toolkit/import_sun_data/import_data.py +++ b/RenderPipelineFile/toolkit/import_sun_data/import_data.py @@ -1,108 +1,108 @@ -""" - -This script downloads real world sun position data and imports them into the -pipelines time of day system. - -(c) 2016 tobpsr - -""" - -from __future__ import print_function, division - -# You can get this data from http://sunpath.azurewebsites.net/ -CONFIG = { - "year": 2016, - "month": 7, - "day": 21, - "latitude": 50.778228759765625, - "longitude": 6.088640213012695, - "height": 167, - "precision": 6 -} - -if __name__ == "__main__": - - import sys - import math - - sys.path.insert(0, "../../") - from rplibs.six.moves import urllib # pylint: disable=import-error - - CONFIG["end_day"] = CONFIG["day"] - - API = "http://www.nrel.gov/midc/apps/spa.pl?syear={year}&smonth={month}&sday={day}&eyear={year}&emonth={month}&eday={end_day}&step={precision}&stepunit=1&latitude={latitude}&longitude={longitude}&timezone=%2B1&elev={height}&press=1013&temp=20&dut1=0.0&deltat=64.797&azmrot=180&slope=0&refract=0.5667&field=2&field=35&field=36&field=38&zip=0" # noqa - - url = API.format(**CONFIG) - - query_str = ("Are you sure you want to import the data? This overrides your " - "current sun azimuth, altitude and intensity settings! (y/n): ") - if sys.version_info.major >= 3: - query_result = input(query_str) - else: - query_result = raw_input(query_str) - - if query_result.strip().lower() not in ["y", "yes", "j"]: - print("Aborted.") - sys.exit(0) - - print("Querying api ..") - handle = urllib.request.urlopen(url) - data = handle.read().decode("utf-8") - handle.close() - - # Extract data - print("Extracting data ..") - lines = data.replace("\r", "").split("\n")[1:] - - # Load render pipeline api - print("Loading plugin api ..") - sys.path.insert(0, "../../") - from rpcore.pluginbase.manager import PluginManager - from rpcore.mount_manager import MountManager - - mount_mgr = MountManager(None) - mount_mgr.mount() - - plugin_mgr = PluginManager(None) - plugin_mgr.load() - - convert_to_linear = plugin_mgr.day_settings["scattering"]["sun_intensity"].get_linear_value - - hour, minutes = 0, 0 - - data_points_azimuth = [] - data_points_altitude = [] - data_points_intensity = [] - - for line in lines: - if not line: - break - date, time, azim_angle, declination, ascension, elevation = line.split(",") - - float_azim = (float(azim_angle) + 180) / 360 - float_elev = (float(elevation) / 60) * 0.5 + 0.5 - - float_time = (minutes + hour * 60) / (24 * 60) - - data_points_azimuth.append((float_time, float_azim)) - data_points_altitude.append((float_time, float_elev)) - - # Approximated intensity - approx_intensity = 2.0 * (1 - math.cos(math.pi * max(0, 8.0 + float(elevation)) / 60.0)) - approx_intensity = max(0, min(150.0, approx_intensity * 0.25)) - data_points_intensity.append((float_time, approx_intensity)) - - minutes += CONFIG["precision"] - if minutes >= 60: - hour += 1 - minutes = minutes % 60 - - print("Saving ..") - settings = plugin_mgr.day_settings["scattering"] - settings["sun_azimuth"].curves[0].control_points = data_points_azimuth - settings["sun_altitude"].curves[0].control_points = data_points_altitude - settings["sun_intensity"].curves[0].control_points = data_points_intensity - - plugin_mgr.save_daytime_overrides("/$$rpconfig/daytime.yaml") - - print("Done!") +""" + +This script downloads real world sun position data and imports them into the +pipelines time of day system. + +(c) 2016 tobpsr + +""" + +from __future__ import print_function, division + +# You can get this data from http://sunpath.azurewebsites.net/ +CONFIG = { + "year": 2016, + "month": 7, + "day": 21, + "latitude": 50.778228759765625, + "longitude": 6.088640213012695, + "height": 167, + "precision": 6 +} + +if __name__ == "__main__": + + import sys + import math + + sys.path.insert(0, "../../") + from rplibs.six.moves import urllib # pylint: disable=import-error + + CONFIG["end_day"] = CONFIG["day"] + + API = "http://www.nrel.gov/midc/apps/spa.pl?syear={year}&smonth={month}&sday={day}&eyear={year}&emonth={month}&eday={end_day}&step={precision}&stepunit=1&latitude={latitude}&longitude={longitude}&timezone=%2B1&elev={height}&press=1013&temp=20&dut1=0.0&deltat=64.797&azmrot=180&slope=0&refract=0.5667&field=2&field=35&field=36&field=38&zip=0" # noqa + + url = API.format(**CONFIG) + + query_str = ("Are you sure you want to import the data? This overrides your " + "current sun azimuth, altitude and intensity settings! (y/n): ") + if sys.version_info.major >= 3: + query_result = input(query_str) + else: + query_result = raw_input(query_str) + + if query_result.strip().lower() not in ["y", "yes", "j"]: + print("Aborted.") + sys.exit(0) + + print("Querying api ..") + handle = urllib.request.urlopen(url) + data = handle.read().decode("utf-8") + handle.close() + + # Extract data + print("Extracting data ..") + lines = data.replace("\r", "").split("\n")[1:] + + # Load render pipeline api + print("Loading plugin api ..") + sys.path.insert(0, "../../") + from rpcore.pluginbase.manager import PluginManager + from rpcore.mount_manager import MountManager + + mount_mgr = MountManager(None) + mount_mgr.mount() + + plugin_mgr = PluginManager(None) + plugin_mgr.load() + + convert_to_linear = plugin_mgr.day_settings["scattering"]["sun_intensity"].get_linear_value + + hour, minutes = 0, 0 + + data_points_azimuth = [] + data_points_altitude = [] + data_points_intensity = [] + + for line in lines: + if not line: + break + date, time, azim_angle, declination, ascension, elevation = line.split(",") + + float_azim = (float(azim_angle) + 180) / 360 + float_elev = (float(elevation) / 60) * 0.5 + 0.5 + + float_time = (minutes + hour * 60) / (24 * 60) + + data_points_azimuth.append((float_time, float_azim)) + data_points_altitude.append((float_time, float_elev)) + + # Approximated intensity + approx_intensity = 2.0 * (1 - math.cos(math.pi * max(0, 8.0 + float(elevation)) / 60.0)) + approx_intensity = max(0, min(150.0, approx_intensity * 0.25)) + data_points_intensity.append((float_time, approx_intensity)) + + minutes += CONFIG["precision"] + if minutes >= 60: + hour += 1 + minutes = minutes % 60 + + print("Saving ..") + settings = plugin_mgr.day_settings["scattering"] + settings["sun_azimuth"].curves[0].control_points = data_points_azimuth + settings["sun_altitude"].curves[0].control_points = data_points_altitude + settings["sun_intensity"].curves[0].control_points = data_points_intensity + + plugin_mgr.save_daytime_overrides("/$$rpconfig/daytime.yaml") + + print("Done!") diff --git a/RenderPipelineFile/toolkit/material_editor/main.py b/RenderPipelineFile/toolkit/material_editor/main.py index dd383451..b1570868 100644 --- a/RenderPipelineFile/toolkit/material_editor/main.py +++ b/RenderPipelineFile/toolkit/material_editor/main.py @@ -1,329 +1,329 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -# This tool offers an interface to modify materials -# Hacked together in a hour, I'm sorry. - -from __future__ import print_function - -import os -import sys -import time -import math -import tempfile -import colorsys -from threading import Thread -from functools import partial - -os.chdir(os.path.join(os.path.dirname(os.path.realpath(__file__)))) -sys.path.insert(0, os.getcwd()) -sys.path.insert(0, "../../") - -from rplibs.six import iteritems # noqa -from rplibs.pyqt_imports import * # noqa - -from RenderPipelineFile.toolkit.material_editor.ui.main_window_generated import Ui_MainWindow # noqa -from rpcore.util.network_communication import NetworkCommunication # noqa - -# Allow working with an older version of the material DB. -# ONLY for debugging the viewer. -ALLOW_OUTDATED_MATERIALS = False - - -class MaterialData: - - def __init__(self): - self.name = "" - self.shading_model = 0 - self.metallic = False - self.roughness = 0.0 - self.specular = 0.0 - self.normal_strength = 0.0 - self.shading_model_param1 = 0.0 - self.shading_model_param2 = 0.0 - self.basecolor_r = 0.6 - self.basecolor_g = 0.6 - self.basecolor_b = 0.6 - - -class MaterialEditor(QMainWindow, Ui_MainWindow): - - """ Interface to change the plugin settings """ - - SHADING_MODELS = [ - ("Default", 0, None), - ("Emissive", 1, None), - ("Clearcoat", 2, None), - ("Transparent", 3, "Transparency"), - ("Skin", 4, None), - ("Foliage", 5, None), - ] - - def __init__(self): - QMainWindow.__init__(self) - Ui_MainWindow.__init__(self) - - self.in_update = False - self.materials = [] - self.material = MaterialData() - - self.setupUi(self) - self.init_shading_models() - self.init_bindings() - self.update_material_list() - self.on_material_selected() - - def init_bindings(self): - qt_connect(self.cb_shading_model, "currentIndexChanged", self.read_from_ui) - qt_connect(self.cb_material, "currentIndexChanged", self.on_material_selected) - qt_connect(self.cb_metallic, "stateChanged", self.read_from_ui) - - self.sliders = [ - (self.slider_roughness, self.lbl_roughness, 0.0, 1.0, "roughness"), - (self.slider_specular, self.lbl_specular, 1.0, 2.51, "specular"), - (self.slider_normal, self.lbl_normal, 0.0, 1.0, "normal_strength"), - (self.slider_param1, self.lbl_param1, 0.0, 1.0, "shading_model_param1"), - ] - - for slider, lbl, start, end, prop in self.sliders: - qt_connect(slider, "valueChanged", self.read_from_ui) - - qt_connect(self.basecolor_1, "valueChanged", self.read_from_ui) - qt_connect(self.basecolor_2, "valueChanged", self.read_from_ui) - qt_connect(self.basecolor_3, "valueChanged", self.read_from_ui) - - for cb in (self.cb_rgb, self.cb_srgb, self.cb_hsv): - qt_connect(cb, "toggled", self.write_to_ui) - - def update_ui(self): - # Basecolor - labels = "R", "G", "B" - if self.cb_hsv.isChecked(): - labels = "H", "S", "V" - self.lbl_basecolor1.setText(labels[0]) - self.lbl_basecolor2.setText(labels[1]) - self.lbl_basecolor3.setText(labels[2]) - - a, b, c = (self.basecolor_1.value() / 100.0, - self.basecolor_2.value() / 100.0, - self.basecolor_3.value() / 100.0) - rgb = self.tuple_to_basecolor(a, b, c) - self.lbl_basecolor_val1.setText("{:0.2f}".format(a)) - self.lbl_basecolor_val2.setText("{:0.2f}".format(b)) - self.lbl_basecolor_val3.setText("{:0.2f}".format(c)) - self.lbl_color_preview.setStyleSheet("background: rgb({}, {}, {});".format( - int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255))) - - # Shading model - self._update_shading_model() - - def read_from_ui(self): - if self.in_update: - return - - # Rest of sliders - for slider, lbl, start, end, prop in self.sliders: - val = (slider.value() / 100.0) * (end - start) + start - lbl.setText("{:0.2f}".format(val)) - setattr(self.material, prop, val) - - # Basecolor - rgb = self._get_ui_basecolor_rgb() - self.material.basecolor_r = rgb[0] - self.material.basecolor_g = rgb[1] - self.material.basecolor_b = rgb[2] - - # Metallic - self.material.metallic = self.cb_metallic.isChecked() - - # Shading model - self.material.shading_model = self.cb_shading_model.currentIndex() - - self.update_ui() - self.send_update() - - def write_to_ui(self): - self.in_update = True - - # Basecolor - values = self.basecolor_to_tuple(self.material) - self.basecolor_1.setValue(int(values[0] * 100.0)) - self.basecolor_2.setValue(int(values[1] * 100.0)) - self.basecolor_3.setValue(int(values[2] * 100.0)) - - # Shading model - self.cb_shading_model.setCurrentIndex(self.material.shading_model) - - # Metallics - self.cb_metallic.setChecked(self.material.metallic) - self.slider_specular.setEnabled(not self.material.metallic) - - # Rest of sliders - for slider, lbl, start, end, prop in self.sliders: - val = getattr(self.material, prop) - slider.setValue(int((val - start) / (end - start) * 100.0)) - - self.in_update = False - self.update_ui() - - def _get_ui_basecolor_rgb(self): - """ Extracts the RGB color which is currently edited in the UI """ - a, b, c = (self.basecolor_1.value() / 100.0, - self.basecolor_2.value() / 100.0, - self.basecolor_3.value() / 100.0) - return self.tuple_to_basecolor(a, b, c) - - def update_material_list(self): - temp_path = os.path.join(tempfile.gettempdir(), "rp_materials.data") - print("Waiting for creation of", temp_path) - if not ALLOW_OUTDATED_MATERIALS: - try: - os.remove(temp_path) - except: - pass - NetworkCommunication.send_async(NetworkCommunication.MATERIAL_PORT, "dump_materials " + temp_path) - start_time = time.time() - while not os.path.isfile(temp_path) and time.time() - start_time < 5.0: - time.sleep(0.5) - if not os.path.isfile(temp_path): - QMessageBox.critical( - self, "Error", "Render Pipeline not responding! Make sure a render pipeline application is running, and try again later.") - sys.exit(-1) - if not ALLOW_OUTDATED_MATERIALS: - time.sleep(0.5) - self._load_material_list(temp_path) - - def _load_material_list(self, pth): - self.materials = [] - self.cb_material.clear() - with open(pth) as handle: - for line in handle.readlines(): - parts = line.strip().split(" ") - material = self._read_in_material(parts) - self.materials.append(material) - self.cb_material.addItem(material.name) - - def _read_in_material(self, parts): - material = MaterialData() - material.name = parts[0] - material.basecolor_r = float(parts[1]) - material.basecolor_g = float(parts[2]) - material.basecolor_b = float(parts[3]) - material.roughness = float(parts[4]) - material.specular = float(parts[5]) - material.metallic = float(parts[6]) > 0.5 - material.shading_model = int(float(parts[7])) - material.normal_strength = float(parts[8]) - material.shading_model_param1 = float(parts[9]) - material.shading_model_param2 = float(parts[10]) - return material - - def basecolor_to_tuple(self, mat): - def to_srgb(v): return math.pow(v, 1.0 / 2.2) - if self.cb_rgb.isChecked(): - return mat.basecolor_r, mat.basecolor_g, mat.basecolor_b - elif self.cb_srgb.isChecked(): - return to_srgb(mat.basecolor_r), to_srgb(mat.basecolor_g), to_srgb(mat.basecolor_b) - elif self.cb_hsv.isChecked(): - return colorsys.rgb_to_hsv(mat.basecolor_r, mat.basecolor_g, mat.basecolor_b) - else: - assert False - - def tuple_to_basecolor(self, a, b, c): - def from_srgb(v): return math.pow(v, 2.2) - if self.cb_rgb.isChecked(): - return a, b, c - elif self.cb_srgb.isChecked(): - return from_srgb(a), from_srgb(b), from_srgb(c) - elif self.cb_hsv.isChecked(): - return colorsys.hsv_to_rgb(a, b, c) - else: - assert False - - def on_material_selected(self): - index = self.cb_material.currentIndex() - if index < 0 or index >= len(self.materials): - print("Invalid material with index", index, "only have", len(self.materials), "materials") - return - self.material = self.materials[index] - print("Loaded material", self.material.name) - self.write_to_ui() - - def send_update(self): - serialized = ("{} " * 11).format( - self.material.name, - self.material.basecolor_r, - self.material.basecolor_g, - self.material.basecolor_b, - self.material.roughness, - self.material.specular, - 1.0 if self.material.metallic else 0.0, - self.material.shading_model, - self.material.normal_strength, - self.material.shading_model_param1, - self.material.shading_model_param2, - ) - NetworkCommunication.send_async(NetworkCommunication.MATERIAL_PORT, "update_material " + serialized) - - def _update_shading_model(self): - name, val, optional_param = self.SHADING_MODELS[self.cb_shading_model.currentIndex()] - if optional_param is None: - self.slider_param1.setEnabled(False) - self.lbl_shading_model_param1.setText("Unused") - else: - self.slider_param1.setEnabled(True) - self.lbl_shading_model_param1.setText(optional_param) - - self.cb_metallic.show() - self.slider_roughness.setEnabled(True) - self.slider_normal.setEnabled(True) - - if name == "Emissive": - self.cb_metallic.hide() - self.slider_roughness.setEnabled(False) - self.slider_specular.setEnabled(False) - self.slider_normal.setEnabled(False) - elif name == "Clearcoat": - self.cb_metallic.hide() - self.slider_specular.setEnabled(False) - elif name == "Transparent": - pass - elif name == "Skin": - self.cb_metallic.hide() - elif name == "Foliage": - self.cb_metallic.hide() - - def init_shading_models(self): - self.cb_shading_model.clear() - for name, val, optional_param in self.SHADING_MODELS: - self.cb_shading_model.addItem(name) - -# Start application -app = QApplication(sys.argv) -qt_register_fonts() -editor = MaterialEditor() -editor.show() -app.exec_() +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +# This tool offers an interface to modify materials +# Hacked together in a hour, I'm sorry. + +from __future__ import print_function + +import os +import sys +import time +import math +import tempfile +import colorsys +from threading import Thread +from functools import partial + +os.chdir(os.path.join(os.path.dirname(os.path.realpath(__file__)))) +sys.path.insert(0, os.getcwd()) +sys.path.insert(0, "../../") + +from rplibs.six import iteritems # noqa +from rplibs.pyqt_imports import * # noqa + +from RenderPipelineFile.toolkit.material_editor.ui.main_window_generated import Ui_MainWindow # noqa +from rpcore.util.network_communication import NetworkCommunication # noqa + +# Allow working with an older version of the material DB. +# ONLY for debugging the viewer. +ALLOW_OUTDATED_MATERIALS = False + + +class MaterialData: + + def __init__(self): + self.name = "" + self.shading_model = 0 + self.metallic = False + self.roughness = 0.0 + self.specular = 0.0 + self.normal_strength = 0.0 + self.shading_model_param1 = 0.0 + self.shading_model_param2 = 0.0 + self.basecolor_r = 0.6 + self.basecolor_g = 0.6 + self.basecolor_b = 0.6 + + +class MaterialEditor(QMainWindow, Ui_MainWindow): + + """ Interface to change the plugin settings """ + + SHADING_MODELS = [ + ("Default", 0, None), + ("Emissive", 1, None), + ("Clearcoat", 2, None), + ("Transparent", 3, "Transparency"), + ("Skin", 4, None), + ("Foliage", 5, None), + ] + + def __init__(self): + QMainWindow.__init__(self) + Ui_MainWindow.__init__(self) + + self.in_update = False + self.materials = [] + self.material = MaterialData() + + self.setupUi(self) + self.init_shading_models() + self.init_bindings() + self.update_material_list() + self.on_material_selected() + + def init_bindings(self): + qt_connect(self.cb_shading_model, "currentIndexChanged", self.read_from_ui) + qt_connect(self.cb_material, "currentIndexChanged", self.on_material_selected) + qt_connect(self.cb_metallic, "stateChanged", self.read_from_ui) + + self.sliders = [ + (self.slider_roughness, self.lbl_roughness, 0.0, 1.0, "roughness"), + (self.slider_specular, self.lbl_specular, 1.0, 2.51, "specular"), + (self.slider_normal, self.lbl_normal, 0.0, 1.0, "normal_strength"), + (self.slider_param1, self.lbl_param1, 0.0, 1.0, "shading_model_param1"), + ] + + for slider, lbl, start, end, prop in self.sliders: + qt_connect(slider, "valueChanged", self.read_from_ui) + + qt_connect(self.basecolor_1, "valueChanged", self.read_from_ui) + qt_connect(self.basecolor_2, "valueChanged", self.read_from_ui) + qt_connect(self.basecolor_3, "valueChanged", self.read_from_ui) + + for cb in (self.cb_rgb, self.cb_srgb, self.cb_hsv): + qt_connect(cb, "toggled", self.write_to_ui) + + def update_ui(self): + # Basecolor + labels = "R", "G", "B" + if self.cb_hsv.isChecked(): + labels = "H", "S", "V" + self.lbl_basecolor1.setText(labels[0]) + self.lbl_basecolor2.setText(labels[1]) + self.lbl_basecolor3.setText(labels[2]) + + a, b, c = (self.basecolor_1.value() / 100.0, + self.basecolor_2.value() / 100.0, + self.basecolor_3.value() / 100.0) + rgb = self.tuple_to_basecolor(a, b, c) + self.lbl_basecolor_val1.setText("{:0.2f}".format(a)) + self.lbl_basecolor_val2.setText("{:0.2f}".format(b)) + self.lbl_basecolor_val3.setText("{:0.2f}".format(c)) + self.lbl_color_preview.setStyleSheet("background: rgb({}, {}, {});".format( + int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255))) + + # Shading model + self._update_shading_model() + + def read_from_ui(self): + if self.in_update: + return + + # Rest of sliders + for slider, lbl, start, end, prop in self.sliders: + val = (slider.value() / 100.0) * (end - start) + start + lbl.setText("{:0.2f}".format(val)) + setattr(self.material, prop, val) + + # Basecolor + rgb = self._get_ui_basecolor_rgb() + self.material.basecolor_r = rgb[0] + self.material.basecolor_g = rgb[1] + self.material.basecolor_b = rgb[2] + + # Metallic + self.material.metallic = self.cb_metallic.isChecked() + + # Shading model + self.material.shading_model = self.cb_shading_model.currentIndex() + + self.update_ui() + self.send_update() + + def write_to_ui(self): + self.in_update = True + + # Basecolor + values = self.basecolor_to_tuple(self.material) + self.basecolor_1.setValue(int(values[0] * 100.0)) + self.basecolor_2.setValue(int(values[1] * 100.0)) + self.basecolor_3.setValue(int(values[2] * 100.0)) + + # Shading model + self.cb_shading_model.setCurrentIndex(self.material.shading_model) + + # Metallics + self.cb_metallic.setChecked(self.material.metallic) + self.slider_specular.setEnabled(not self.material.metallic) + + # Rest of sliders + for slider, lbl, start, end, prop in self.sliders: + val = getattr(self.material, prop) + slider.setValue(int((val - start) / (end - start) * 100.0)) + + self.in_update = False + self.update_ui() + + def _get_ui_basecolor_rgb(self): + """ Extracts the RGB color which is currently edited in the UI """ + a, b, c = (self.basecolor_1.value() / 100.0, + self.basecolor_2.value() / 100.0, + self.basecolor_3.value() / 100.0) + return self.tuple_to_basecolor(a, b, c) + + def update_material_list(self): + temp_path = os.path.join(tempfile.gettempdir(), "rp_materials.data") + print("Waiting for creation of", temp_path) + if not ALLOW_OUTDATED_MATERIALS: + try: + os.remove(temp_path) + except: + pass + NetworkCommunication.send_async(NetworkCommunication.MATERIAL_PORT, "dump_materials " + temp_path) + start_time = time.time() + while not os.path.isfile(temp_path) and time.time() - start_time < 5.0: + time.sleep(0.5) + if not os.path.isfile(temp_path): + QMessageBox.critical( + self, "Error", "Render Pipeline not responding! Make sure a render pipeline application is running, and try again later.") + sys.exit(-1) + if not ALLOW_OUTDATED_MATERIALS: + time.sleep(0.5) + self._load_material_list(temp_path) + + def _load_material_list(self, pth): + self.materials = [] + self.cb_material.clear() + with open(pth) as handle: + for line in handle.readlines(): + parts = line.strip().split(" ") + material = self._read_in_material(parts) + self.materials.append(material) + self.cb_material.addItem(material.name) + + def _read_in_material(self, parts): + material = MaterialData() + material.name = parts[0] + material.basecolor_r = float(parts[1]) + material.basecolor_g = float(parts[2]) + material.basecolor_b = float(parts[3]) + material.roughness = float(parts[4]) + material.specular = float(parts[5]) + material.metallic = float(parts[6]) > 0.5 + material.shading_model = int(float(parts[7])) + material.normal_strength = float(parts[8]) + material.shading_model_param1 = float(parts[9]) + material.shading_model_param2 = float(parts[10]) + return material + + def basecolor_to_tuple(self, mat): + def to_srgb(v): return math.pow(v, 1.0 / 2.2) + if self.cb_rgb.isChecked(): + return mat.basecolor_r, mat.basecolor_g, mat.basecolor_b + elif self.cb_srgb.isChecked(): + return to_srgb(mat.basecolor_r), to_srgb(mat.basecolor_g), to_srgb(mat.basecolor_b) + elif self.cb_hsv.isChecked(): + return colorsys.rgb_to_hsv(mat.basecolor_r, mat.basecolor_g, mat.basecolor_b) + else: + assert False + + def tuple_to_basecolor(self, a, b, c): + def from_srgb(v): return math.pow(v, 2.2) + if self.cb_rgb.isChecked(): + return a, b, c + elif self.cb_srgb.isChecked(): + return from_srgb(a), from_srgb(b), from_srgb(c) + elif self.cb_hsv.isChecked(): + return colorsys.hsv_to_rgb(a, b, c) + else: + assert False + + def on_material_selected(self): + index = self.cb_material.currentIndex() + if index < 0 or index >= len(self.materials): + print("Invalid material with index", index, "only have", len(self.materials), "materials") + return + self.material = self.materials[index] + print("Loaded material", self.material.name) + self.write_to_ui() + + def send_update(self): + serialized = ("{} " * 11).format( + self.material.name, + self.material.basecolor_r, + self.material.basecolor_g, + self.material.basecolor_b, + self.material.roughness, + self.material.specular, + 1.0 if self.material.metallic else 0.0, + self.material.shading_model, + self.material.normal_strength, + self.material.shading_model_param1, + self.material.shading_model_param2, + ) + NetworkCommunication.send_async(NetworkCommunication.MATERIAL_PORT, "update_material " + serialized) + + def _update_shading_model(self): + name, val, optional_param = self.SHADING_MODELS[self.cb_shading_model.currentIndex()] + if optional_param is None: + self.slider_param1.setEnabled(False) + self.lbl_shading_model_param1.setText("Unused") + else: + self.slider_param1.setEnabled(True) + self.lbl_shading_model_param1.setText(optional_param) + + self.cb_metallic.show() + self.slider_roughness.setEnabled(True) + self.slider_normal.setEnabled(True) + + if name == "Emissive": + self.cb_metallic.hide() + self.slider_roughness.setEnabled(False) + self.slider_specular.setEnabled(False) + self.slider_normal.setEnabled(False) + elif name == "Clearcoat": + self.cb_metallic.hide() + self.slider_specular.setEnabled(False) + elif name == "Transparent": + pass + elif name == "Skin": + self.cb_metallic.hide() + elif name == "Foliage": + self.cb_metallic.hide() + + def init_shading_models(self): + self.cb_shading_model.clear() + for name, val, optional_param in self.SHADING_MODELS: + self.cb_shading_model.addItem(name) + +# Start application +app = QApplication(sys.argv) +qt_register_fonts() +editor = MaterialEditor() +editor.show() +app.exec_() diff --git a/RenderPipelineFile/toolkit/material_editor/ui/compile_ui_qt4.bat b/RenderPipelineFile/toolkit/material_editor/ui/compile_ui_qt4.bat index 2392f6a6..dca9a251 100644 --- a/RenderPipelineFile/toolkit/material_editor/ui/compile_ui_qt4.bat +++ b/RenderPipelineFile/toolkit/material_editor/ui/compile_ui_qt4.bat @@ -1,9 +1,9 @@ -@echo off - -rem Adjust the path to match your PyQt4 installation -SET PYQTPATH="C:\Projekte\Panda3D\built_x64\python\Lib\site-packages\PyQt4" - -python -m PyQt4.uic.pyuic --from-imports main_window.ui -o main_window_generated.py -"%PYQTPATH%\pyrcc4.exe" -py3 resources.qrc -o resources_rc.py - -pause +@echo off + +rem Adjust the path to match your PyQt4 installation +SET PYQTPATH="C:\Projekte\Panda3D\built_x64\python\Lib\site-packages\PyQt4" + +python -m PyQt4.uic.pyuic --from-imports main_window.ui -o main_window_generated.py +"%PYQTPATH%\pyrcc4.exe" -py3 resources.qrc -o resources_rc.py + +pause diff --git a/RenderPipelineFile/toolkit/material_editor/ui/compile_ui_qt5.bat b/RenderPipelineFile/toolkit/material_editor/ui/compile_ui_qt5.bat index 202b5149..28cd9ee3 100644 --- a/RenderPipelineFile/toolkit/material_editor/ui/compile_ui_qt5.bat +++ b/RenderPipelineFile/toolkit/material_editor/ui/compile_ui_qt5.bat @@ -1,6 +1,6 @@ -@echo off - -python -m PyQt5.uic.pyuic --from-imports main_window.ui -o main_window_generated.py -python -m PyQt5.pyrcc_main resources.qrc -o resources_rc.py - -pause +@echo off + +python -m PyQt5.uic.pyuic --from-imports main_window.ui -o main_window_generated.py +python -m PyQt5.pyrcc_main resources.qrc -o resources_rc.py + +pause diff --git a/RenderPipelineFile/toolkit/material_editor/ui/main_window.ui b/RenderPipelineFile/toolkit/material_editor/ui/main_window.ui index 02d4fcfb..859e8c84 100644 --- a/RenderPipelineFile/toolkit/material_editor/ui/main_window.ui +++ b/RenderPipelineFile/toolkit/material_editor/ui/main_window.ui @@ -1,807 +1,807 @@ - - - MainWindow - - - - 0 - 0 - 262 - 608 - - - - - 262 - 608 - - - - - 262 - 5560 - - - - Render Pipeline Material Editor - - - QMainWindow { background: #fff;} -*, QLabel { font-family: Roboto; font-weight: 300; } - -QSlider::groove:horizontal { - border: 0; - height: 4px; - background: rgba(0, 0, 0, 15); -} - -QSlider::handle:horizonal { - background: #666; - width: 8px; - margin: -6px 0; -} - -QSlider::handle:horizontal:disabled { - background: #ccc; -} -QSlider::groove:horizontal:disabled { - background: rgba(0, 0, 0, 8); -} - -QRadioButton { - background: #fafafa; - padding: 4px 6px 3px; - font-size: 12px; - color: #333; -} - -QRadioButton::indicator { - width: 15px; - height: 15px; - margin-top: -1px; -} - -QRadioButton:checked { -background: #eee; -} - -QRadioButton:hover { -background: #eaeaea; -} - -QComboBox { -border: 0; -background: #fff; -padding: 7px 10px 3px; -font-family: Roboto; -color: #777; -font-size: 12px; -} - -QComboBox::down-arrow { - border: 0; - margin: -3px; - background: #ddd; - margin-left: -16px; -margin-bottom: -4px; - image: url(":/img/down-arrow.png"); -} - -QCheckBox::indicator { - border: 1px solid #aaa; - background: #39f; - width: 14px; - height: 14px; - image: url(":/img/checked.png"); -} - -QCheckBox { -text-transform: uppercase; -color: #555; -} - -QCheckBox::indicator:unchecked { - background: #fff; - image: none; -} - - - - - 0 - - - 0 - - - - - - 0 - 65 - - - - - 16777215 - 65 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - 20 - 26 - 261 - 31 - - - - font-size: 20px; color: #03C03C; -font-weight: 100; - - - MATERIAL EDITOR - - - - - - 20 - 6 - 261 - 31 - - - - font-size: 13px; -color: #aaa; -font-weight: 600; - - - RENDER PIPELINE - - - - - - - - 0 - - - - - - 0 - 0 - - - - - 999999 - 16777215 - - - - QFrame { background: #eee; } QFrame>QLabel { text-transform: uppercase; color: #888; background: transparent; } - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - 20 - 20 - 61 - 16 - - - - Material - - - - - - 20 - 40 - 221 - 30 - - - - - tree01 - - - - - - - 20 - 80 - 141 - 16 - - - - Shading model - - - - - - 20 - 100 - 221 - 30 - - - - - Default - - - - - Emissive - - - - - Transparent - - - - - - - 20 - 140 - 81 - 17 - - - - Metallic - - - - - - 30 - 240 - 201 - 73 - - - - - 6 - - - - - - - - 30 - 0 - - - - - 30 - 16777215 - - - - background: transparent; - - - 0.43 - - - Qt::AlignCenter - - - - - - - 100 - - - 53 - - - Qt::Horizontal - - - - - - - 100 - - - 12 - - - Qt::Horizontal - - - - - - - 100 - - - 43 - - - Qt::Horizontal - - - - - - - - 30 - 0 - - - - - 30 - 16777215 - - - - background: transparent; - - - 0.53 - - - Qt::AlignCenter - - - - - - - - 30 - 0 - - - - - 30 - 16777215 - - - - background: transparent; - - - 0.12 - - - Qt::AlignCenter - - - - - - - - 11 - 0 - - - - background: transparent; color: #555; - - - R - - - - - - - - 11 - 0 - - - - background: transparent; color: #555; - - - G - - - - - - - - 11 - 0 - - - - background: transparent; color: #555; - - - B - - - - - - - - - - 30 - 0 - - - - - 30 - 16777215 - - - - background: rgba(60, 120, 255, 1); - - - - - - - - - - - - 20 - 180 - 131 - 16 - - - - Basecolor - - - - - - 20 - 340 - 131 - 16 - - - - Roughness - - - - - - 20 - 360 - 191 - 19 - - - - 100 - - - 32 - - - Qt::Horizontal - - - - - - 20 - 390 - 131 - 16 - - - - Specular IOR - - - - - - 20 - 410 - 191 - 19 - - - - 100 - - - 20 - - - Qt::Horizontal - - - - - - 20 - 440 - 131 - 16 - - - - Normal-Strength - - - - - - 20 - 460 - 191 - 19 - - - - 100 - - - 80 - - - Qt::Horizontal - - - - - - 220 - 360 - 31 - 20 - - - - color: #333; - - - 0.32 - - - - - - 220 - 410 - 31 - 20 - - - - color: #333; - - - 1.43 - - - - - - 220 - 459 - 31 - 20 - - - - color: #333; - - - 0.80 - - - - - - -10 - 166 - 311 - 561 - - - - QFrame { background: #fff; } - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - 230 - 339 - 31 - 20 - - - - color: #333; - - - 0.20 - - - - - false - - - - 30 - 340 - 191 - 19 - - - - 100 - - - 80 - - - Qt::Horizontal - - - - - - 30 - 320 - 141 - 16 - - - - Shading-model-param - - - - - - 30 - 34 - 60 - 27 - - - - RGB - - - true - - - - - - 92 - 34 - 71 - 27 - - - - SRGB - - - - - - 165 - 34 - 62 - 27 - - - - HSV - - - - - - 30 - 59 - 220 - 96 - - - - QFrame { background: #eee; } - - - QFrame::StyledPanel - - - QFrame::Raised - - - lbl_param1 - slider_param1 - lbl_shading_model_param1 - cb_rgb - cb_srgb - lbl_basecolor_val1 - cb_hsv - frame_3 - - frame_2 - label_2 - cb_material - label_14 - cb_shading_model - cb_metallic - horizontalLayoutWidget_3 - label_15 - label_16 - slider_roughness - label_17 - slider_specular - label_18 - slider_normal - lbl_roughness - lbl_specular - lbl_normal - - - - - - - - - - - - + + + MainWindow + + + + 0 + 0 + 262 + 608 + + + + + 262 + 608 + + + + + 262 + 5560 + + + + Render Pipeline Material Editor + + + QMainWindow { background: #fff;} +*, QLabel { font-family: Roboto; font-weight: 300; } + +QSlider::groove:horizontal { + border: 0; + height: 4px; + background: rgba(0, 0, 0, 15); +} + +QSlider::handle:horizonal { + background: #666; + width: 8px; + margin: -6px 0; +} + +QSlider::handle:horizontal:disabled { + background: #ccc; +} +QSlider::groove:horizontal:disabled { + background: rgba(0, 0, 0, 8); +} + +QRadioButton { + background: #fafafa; + padding: 4px 6px 3px; + font-size: 12px; + color: #333; +} + +QRadioButton::indicator { + width: 15px; + height: 15px; + margin-top: -1px; +} + +QRadioButton:checked { +background: #eee; +} + +QRadioButton:hover { +background: #eaeaea; +} + +QComboBox { +border: 0; +background: #fff; +padding: 7px 10px 3px; +font-family: Roboto; +color: #777; +font-size: 12px; +} + +QComboBox::down-arrow { + border: 0; + margin: -3px; + background: #ddd; + margin-left: -16px; +margin-bottom: -4px; + image: url(":/img/down-arrow.png"); +} + +QCheckBox::indicator { + border: 1px solid #aaa; + background: #39f; + width: 14px; + height: 14px; + image: url(":/img/checked.png"); +} + +QCheckBox { +text-transform: uppercase; +color: #555; +} + +QCheckBox::indicator:unchecked { + background: #fff; + image: none; +} + + + + + 0 + + + 0 + + + + + + 0 + 65 + + + + + 16777215 + 65 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + 20 + 26 + 261 + 31 + + + + font-size: 20px; color: #03C03C; +font-weight: 100; + + + MATERIAL EDITOR + + + + + + 20 + 6 + 261 + 31 + + + + font-size: 13px; +color: #aaa; +font-weight: 600; + + + RENDER PIPELINE + + + + + + + + 0 + + + + + + 0 + 0 + + + + + 999999 + 16777215 + + + + QFrame { background: #eee; } QFrame>QLabel { text-transform: uppercase; color: #888; background: transparent; } + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + 20 + 20 + 61 + 16 + + + + Material + + + + + + 20 + 40 + 221 + 30 + + + + + tree01 + + + + + + + 20 + 80 + 141 + 16 + + + + Shading model + + + + + + 20 + 100 + 221 + 30 + + + + + Default + + + + + Emissive + + + + + Transparent + + + + + + + 20 + 140 + 81 + 17 + + + + Metallic + + + + + + 30 + 240 + 201 + 73 + + + + + 6 + + + + + + + + 30 + 0 + + + + + 30 + 16777215 + + + + background: transparent; + + + 0.43 + + + Qt::AlignCenter + + + + + + + 100 + + + 53 + + + Qt::Horizontal + + + + + + + 100 + + + 12 + + + Qt::Horizontal + + + + + + + 100 + + + 43 + + + Qt::Horizontal + + + + + + + + 30 + 0 + + + + + 30 + 16777215 + + + + background: transparent; + + + 0.53 + + + Qt::AlignCenter + + + + + + + + 30 + 0 + + + + + 30 + 16777215 + + + + background: transparent; + + + 0.12 + + + Qt::AlignCenter + + + + + + + + 11 + 0 + + + + background: transparent; color: #555; + + + R + + + + + + + + 11 + 0 + + + + background: transparent; color: #555; + + + G + + + + + + + + 11 + 0 + + + + background: transparent; color: #555; + + + B + + + + + + + + + + 30 + 0 + + + + + 30 + 16777215 + + + + background: rgba(60, 120, 255, 1); + + + + + + + + + + + + 20 + 180 + 131 + 16 + + + + Basecolor + + + + + + 20 + 340 + 131 + 16 + + + + Roughness + + + + + + 20 + 360 + 191 + 19 + + + + 100 + + + 32 + + + Qt::Horizontal + + + + + + 20 + 390 + 131 + 16 + + + + Specular IOR + + + + + + 20 + 410 + 191 + 19 + + + + 100 + + + 20 + + + Qt::Horizontal + + + + + + 20 + 440 + 131 + 16 + + + + Normal-Strength + + + + + + 20 + 460 + 191 + 19 + + + + 100 + + + 80 + + + Qt::Horizontal + + + + + + 220 + 360 + 31 + 20 + + + + color: #333; + + + 0.32 + + + + + + 220 + 410 + 31 + 20 + + + + color: #333; + + + 1.43 + + + + + + 220 + 459 + 31 + 20 + + + + color: #333; + + + 0.80 + + + + + + -10 + 166 + 311 + 561 + + + + QFrame { background: #fff; } + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + 230 + 339 + 31 + 20 + + + + color: #333; + + + 0.20 + + + + + false + + + + 30 + 340 + 191 + 19 + + + + 100 + + + 80 + + + Qt::Horizontal + + + + + + 30 + 320 + 141 + 16 + + + + Shading-model-param + + + + + + 30 + 34 + 60 + 27 + + + + RGB + + + true + + + + + + 92 + 34 + 71 + 27 + + + + SRGB + + + + + + 165 + 34 + 62 + 27 + + + + HSV + + + + + + 30 + 59 + 220 + 96 + + + + QFrame { background: #eee; } + + + QFrame::StyledPanel + + + QFrame::Raised + + + lbl_param1 + slider_param1 + lbl_shading_model_param1 + cb_rgb + cb_srgb + lbl_basecolor_val1 + cb_hsv + frame_3 + + frame_2 + label_2 + cb_material + label_14 + cb_shading_model + cb_metallic + horizontalLayoutWidget_3 + label_15 + label_16 + slider_roughness + label_17 + slider_specular + label_18 + slider_normal + lbl_roughness + lbl_specular + lbl_normal + + + + + + + + + + + + diff --git a/RenderPipelineFile/toolkit/material_editor/ui/main_window_generated.py b/RenderPipelineFile/toolkit/material_editor/ui/main_window_generated.py index afc9e5aa..79c160c9 100644 --- a/RenderPipelineFile/toolkit/material_editor/ui/main_window_generated.py +++ b/RenderPipelineFile/toolkit/material_editor/ui/main_window_generated.py @@ -1,360 +1,360 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'main_window.ui' -# -# Created by: PyQt5 UI code generator 5.7 -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore, QtGui, QtWidgets - -class Ui_MainWindow(object): - def setupUi(self, MainWindow): - MainWindow.setObjectName("MainWindow") - MainWindow.resize(262, 608) - MainWindow.setMinimumSize(QtCore.QSize(262, 608)) - MainWindow.setMaximumSize(QtCore.QSize(262, 5560)) - MainWindow.setStyleSheet("QMainWindow { background: #fff;}\n" -"*, QLabel { font-family: Roboto; font-weight: 300; }\n" -"\n" -"QSlider::groove:horizontal {\n" -" border: 0;\n" -" height: 4px;\n" -" background: rgba(0, 0, 0, 15);\n" -"}\n" -"\n" -"QSlider::handle:horizonal {\n" -" background: #666;\n" -" width: 8px;\n" -" margin: -6px 0;\n" -"}\n" -"\n" -"QSlider::handle:horizontal:disabled {\n" -" background: #ccc;\n" -"}\n" -"QSlider::groove:horizontal:disabled {\n" -" background: rgba(0, 0, 0, 8);\n" -"}\n" -"\n" -"QRadioButton {\n" -" background: #fafafa;\n" -" padding: 4px 6px 3px;\n" -" font-size: 12px;\n" -" color: #333;\n" -"}\n" -"\n" -"QRadioButton::indicator {\n" -" width: 15px;\n" -" height: 15px;\n" -" margin-top: -1px;\n" -"}\n" -"\n" -"QRadioButton:checked {\n" -"background: #eee;\n" -"}\n" -"\n" -"QRadioButton:hover {\n" -"background: #eaeaea;\n" -"}\n" -"\n" -"QComboBox {\n" -"border: 0;\n" -"background: #fff;\n" -"padding: 7px 10px 3px;\n" -"font-family: Roboto;\n" -"color: #777;\n" -"font-size: 12px;\n" -"}\n" -"\n" -"QComboBox::down-arrow {\n" -" border: 0;\n" -" margin: -3px;\n" -" background: #ddd;\n" -" margin-left: -16px;\n" -"margin-bottom: -4px;\n" -" image: url(\":/img/down-arrow.png\");\n" -"}\n" -"\n" -"QCheckBox::indicator {\n" -" border: 1px solid #aaa;\n" -" background: #39f;\n" -" width: 14px;\n" -" height: 14px;\n" -" image: url(\":/img/checked.png\");\n" -"}\n" -"\n" -"QCheckBox {\n" -"text-transform: uppercase;\n" -"color: #555;\n" -"}\n" -"\n" -"QCheckBox::indicator:unchecked {\n" -" background: #fff;\n" -" image: none;\n" -"}") - self.centralwidget = QtWidgets.QWidget(MainWindow) - self.centralwidget.setObjectName("centralwidget") - self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.verticalLayout.setSpacing(0) - self.verticalLayout.setObjectName("verticalLayout") - self.frame = QtWidgets.QFrame(self.centralwidget) - self.frame.setMinimumSize(QtCore.QSize(0, 65)) - self.frame.setMaximumSize(QtCore.QSize(16777215, 65)) - self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.frame.setFrameShadow(QtWidgets.QFrame.Raised) - self.frame.setObjectName("frame") - self.label = QtWidgets.QLabel(self.frame) - self.label.setGeometry(QtCore.QRect(20, 26, 261, 31)) - self.label.setStyleSheet("font-size: 20px; color: #03C03C;\n" -"font-weight: 100;") - self.label.setObjectName("label") - self.label_9 = QtWidgets.QLabel(self.frame) - self.label_9.setGeometry(QtCore.QRect(20, 6, 261, 31)) - self.label_9.setStyleSheet("font-size: 13px;\n" -"color: #aaa;\n" -"font-weight: 600;") - self.label_9.setObjectName("label_9") - self.verticalLayout.addWidget(self.frame) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setSpacing(0) - self.horizontalLayout.setObjectName("horizontalLayout") - self.sidebar_material = QtWidgets.QFrame(self.centralwidget) - self.sidebar_material.setMinimumSize(QtCore.QSize(0, 0)) - self.sidebar_material.setMaximumSize(QtCore.QSize(999999, 16777215)) - self.sidebar_material.setStyleSheet("QFrame { background: #eee; } QFrame>QLabel { text-transform: uppercase; color: #888; background: transparent; }") - self.sidebar_material.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.sidebar_material.setFrameShadow(QtWidgets.QFrame.Raised) - self.sidebar_material.setObjectName("sidebar_material") - self.label_2 = QtWidgets.QLabel(self.sidebar_material) - self.label_2.setGeometry(QtCore.QRect(20, 20, 61, 16)) - self.label_2.setObjectName("label_2") - self.cb_material = QtWidgets.QComboBox(self.sidebar_material) - self.cb_material.setGeometry(QtCore.QRect(20, 40, 221, 30)) - self.cb_material.setObjectName("cb_material") - self.cb_material.addItem("") - self.label_14 = QtWidgets.QLabel(self.sidebar_material) - self.label_14.setGeometry(QtCore.QRect(20, 80, 141, 16)) - self.label_14.setObjectName("label_14") - self.cb_shading_model = QtWidgets.QComboBox(self.sidebar_material) - self.cb_shading_model.setGeometry(QtCore.QRect(20, 100, 221, 30)) - self.cb_shading_model.setObjectName("cb_shading_model") - self.cb_shading_model.addItem("") - self.cb_shading_model.addItem("") - self.cb_shading_model.addItem("") - self.cb_metallic = QtWidgets.QCheckBox(self.sidebar_material) - self.cb_metallic.setGeometry(QtCore.QRect(20, 140, 81, 17)) - self.cb_metallic.setObjectName("cb_metallic") - self.horizontalLayoutWidget_3 = QtWidgets.QWidget(self.sidebar_material) - self.horizontalLayoutWidget_3.setGeometry(QtCore.QRect(30, 240, 201, 73)) - self.horizontalLayoutWidget_3.setObjectName("horizontalLayoutWidget_3") - self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_3) - self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0) - self.horizontalLayout_4.setSpacing(6) - self.horizontalLayout_4.setObjectName("horizontalLayout_4") - self.gridLayout = QtWidgets.QGridLayout() - self.gridLayout.setObjectName("gridLayout") - self.lbl_basecolor_val2 = QtWidgets.QLabel(self.horizontalLayoutWidget_3) - self.lbl_basecolor_val2.setMinimumSize(QtCore.QSize(30, 0)) - self.lbl_basecolor_val2.setMaximumSize(QtCore.QSize(30, 16777215)) - self.lbl_basecolor_val2.setStyleSheet("background: transparent;") - self.lbl_basecolor_val2.setAlignment(QtCore.Qt.AlignCenter) - self.lbl_basecolor_val2.setObjectName("lbl_basecolor_val2") - self.gridLayout.addWidget(self.lbl_basecolor_val2, 1, 2, 1, 1) - self.basecolor_3 = QtWidgets.QSlider(self.horizontalLayoutWidget_3) - self.basecolor_3.setMaximum(100) - self.basecolor_3.setProperty("value", 53) - self.basecolor_3.setOrientation(QtCore.Qt.Horizontal) - self.basecolor_3.setObjectName("basecolor_3") - self.gridLayout.addWidget(self.basecolor_3, 2, 1, 1, 1) - self.basecolor_1 = QtWidgets.QSlider(self.horizontalLayoutWidget_3) - self.basecolor_1.setMaximum(100) - self.basecolor_1.setProperty("value", 12) - self.basecolor_1.setOrientation(QtCore.Qt.Horizontal) - self.basecolor_1.setObjectName("basecolor_1") - self.gridLayout.addWidget(self.basecolor_1, 0, 1, 1, 1) - self.basecolor_2 = QtWidgets.QSlider(self.horizontalLayoutWidget_3) - self.basecolor_2.setMaximum(100) - self.basecolor_2.setProperty("value", 43) - self.basecolor_2.setOrientation(QtCore.Qt.Horizontal) - self.basecolor_2.setObjectName("basecolor_2") - self.gridLayout.addWidget(self.basecolor_2, 1, 1, 1, 1) - self.lbl_basecolor_val3 = QtWidgets.QLabel(self.horizontalLayoutWidget_3) - self.lbl_basecolor_val3.setMinimumSize(QtCore.QSize(30, 0)) - self.lbl_basecolor_val3.setMaximumSize(QtCore.QSize(30, 16777215)) - self.lbl_basecolor_val3.setStyleSheet("background: transparent;") - self.lbl_basecolor_val3.setAlignment(QtCore.Qt.AlignCenter) - self.lbl_basecolor_val3.setObjectName("lbl_basecolor_val3") - self.gridLayout.addWidget(self.lbl_basecolor_val3, 2, 2, 1, 1) - self.lbl_basecolor_val1 = QtWidgets.QLabel(self.horizontalLayoutWidget_3) - self.lbl_basecolor_val1.setMinimumSize(QtCore.QSize(30, 0)) - self.lbl_basecolor_val1.setMaximumSize(QtCore.QSize(30, 16777215)) - self.lbl_basecolor_val1.setStyleSheet("background: transparent;") - self.lbl_basecolor_val1.setAlignment(QtCore.Qt.AlignCenter) - self.lbl_basecolor_val1.setObjectName("lbl_basecolor_val1") - self.gridLayout.addWidget(self.lbl_basecolor_val1, 0, 2, 1, 1) - self.lbl_basecolor1 = QtWidgets.QLabel(self.horizontalLayoutWidget_3) - self.lbl_basecolor1.setMinimumSize(QtCore.QSize(11, 0)) - self.lbl_basecolor1.setStyleSheet("background: transparent; color: #555; ") - self.lbl_basecolor1.setObjectName("lbl_basecolor1") - self.gridLayout.addWidget(self.lbl_basecolor1, 0, 0, 1, 1) - self.lbl_basecolor2 = QtWidgets.QLabel(self.horizontalLayoutWidget_3) - self.lbl_basecolor2.setMinimumSize(QtCore.QSize(11, 0)) - self.lbl_basecolor2.setStyleSheet("background: transparent; color: #555; ") - self.lbl_basecolor2.setObjectName("lbl_basecolor2") - self.gridLayout.addWidget(self.lbl_basecolor2, 1, 0, 1, 1) - self.lbl_basecolor3 = QtWidgets.QLabel(self.horizontalLayoutWidget_3) - self.lbl_basecolor3.setMinimumSize(QtCore.QSize(11, 0)) - self.lbl_basecolor3.setStyleSheet("background: transparent; color: #555; ") - self.lbl_basecolor3.setObjectName("lbl_basecolor3") - self.gridLayout.addWidget(self.lbl_basecolor3, 2, 0, 1, 1) - self.horizontalLayout_4.addLayout(self.gridLayout) - self.lbl_color_preview = QtWidgets.QLabel(self.horizontalLayoutWidget_3) - self.lbl_color_preview.setMinimumSize(QtCore.QSize(30, 0)) - self.lbl_color_preview.setMaximumSize(QtCore.QSize(30, 16777215)) - self.lbl_color_preview.setStyleSheet("background: rgba(60, 120, 255);") - self.lbl_color_preview.setText("") - self.lbl_color_preview.setObjectName("lbl_color_preview") - self.horizontalLayout_4.addWidget(self.lbl_color_preview) - self.label_15 = QtWidgets.QLabel(self.sidebar_material) - self.label_15.setGeometry(QtCore.QRect(20, 180, 131, 16)) - self.label_15.setObjectName("label_15") - self.label_16 = QtWidgets.QLabel(self.sidebar_material) - self.label_16.setGeometry(QtCore.QRect(20, 340, 131, 16)) - self.label_16.setObjectName("label_16") - self.slider_roughness = QtWidgets.QSlider(self.sidebar_material) - self.slider_roughness.setGeometry(QtCore.QRect(20, 360, 191, 19)) - self.slider_roughness.setMaximum(100) - self.slider_roughness.setProperty("value", 32) - self.slider_roughness.setOrientation(QtCore.Qt.Horizontal) - self.slider_roughness.setObjectName("slider_roughness") - self.label_17 = QtWidgets.QLabel(self.sidebar_material) - self.label_17.setGeometry(QtCore.QRect(20, 390, 131, 16)) - self.label_17.setObjectName("label_17") - self.slider_specular = QtWidgets.QSlider(self.sidebar_material) - self.slider_specular.setGeometry(QtCore.QRect(20, 410, 191, 19)) - self.slider_specular.setMaximum(100) - self.slider_specular.setSliderPosition(20) - self.slider_specular.setOrientation(QtCore.Qt.Horizontal) - self.slider_specular.setObjectName("slider_specular") - self.label_18 = QtWidgets.QLabel(self.sidebar_material) - self.label_18.setGeometry(QtCore.QRect(20, 440, 131, 16)) - self.label_18.setObjectName("label_18") - self.slider_normal = QtWidgets.QSlider(self.sidebar_material) - self.slider_normal.setGeometry(QtCore.QRect(20, 460, 191, 19)) - self.slider_normal.setMaximum(100) - self.slider_normal.setSliderPosition(80) - self.slider_normal.setOrientation(QtCore.Qt.Horizontal) - self.slider_normal.setObjectName("slider_normal") - self.lbl_roughness = QtWidgets.QLabel(self.sidebar_material) - self.lbl_roughness.setGeometry(QtCore.QRect(220, 360, 31, 20)) - self.lbl_roughness.setStyleSheet("color: #333;") - self.lbl_roughness.setObjectName("lbl_roughness") - self.lbl_specular = QtWidgets.QLabel(self.sidebar_material) - self.lbl_specular.setGeometry(QtCore.QRect(220, 410, 31, 20)) - self.lbl_specular.setStyleSheet("color: #333;") - self.lbl_specular.setObjectName("lbl_specular") - self.lbl_normal = QtWidgets.QLabel(self.sidebar_material) - self.lbl_normal.setGeometry(QtCore.QRect(220, 459, 31, 20)) - self.lbl_normal.setStyleSheet("color: #333;") - self.lbl_normal.setObjectName("lbl_normal") - self.frame_2 = QtWidgets.QFrame(self.sidebar_material) - self.frame_2.setGeometry(QtCore.QRect(-10, 166, 311, 561)) - self.frame_2.setStyleSheet("QFrame { background: #fff; }") - self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised) - self.frame_2.setObjectName("frame_2") - self.lbl_param1 = QtWidgets.QLabel(self.frame_2) - self.lbl_param1.setGeometry(QtCore.QRect(230, 339, 31, 20)) - self.lbl_param1.setStyleSheet("color: #333;") - self.lbl_param1.setObjectName("lbl_param1") - self.slider_param1 = QtWidgets.QSlider(self.frame_2) - self.slider_param1.setEnabled(False) - self.slider_param1.setGeometry(QtCore.QRect(30, 340, 191, 19)) - self.slider_param1.setMaximum(100) - self.slider_param1.setSliderPosition(80) - self.slider_param1.setOrientation(QtCore.Qt.Horizontal) - self.slider_param1.setObjectName("slider_param1") - self.lbl_shading_model_param1 = QtWidgets.QLabel(self.frame_2) - self.lbl_shading_model_param1.setGeometry(QtCore.QRect(30, 320, 141, 16)) - self.lbl_shading_model_param1.setObjectName("lbl_shading_model_param1") - self.cb_rgb = QtWidgets.QRadioButton(self.frame_2) - self.cb_rgb.setGeometry(QtCore.QRect(30, 34, 60, 27)) - self.cb_rgb.setChecked(True) - self.cb_rgb.setObjectName("cb_rgb") - self.cb_srgb = QtWidgets.QRadioButton(self.frame_2) - self.cb_srgb.setGeometry(QtCore.QRect(92, 34, 71, 27)) - self.cb_srgb.setObjectName("cb_srgb") - self.cb_hsv = QtWidgets.QRadioButton(self.frame_2) - self.cb_hsv.setGeometry(QtCore.QRect(165, 34, 62, 27)) - self.cb_hsv.setObjectName("cb_hsv") - self.frame_3 = QtWidgets.QFrame(self.frame_2) - self.frame_3.setGeometry(QtCore.QRect(30, 59, 220, 96)) - self.frame_3.setStyleSheet("QFrame { background: #eee; }") - self.frame_3.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.frame_3.setFrameShadow(QtWidgets.QFrame.Raised) - self.frame_3.setObjectName("frame_3") - self.lbl_param1.raise_() - self.slider_param1.raise_() - self.lbl_shading_model_param1.raise_() - self.cb_rgb.raise_() - self.cb_srgb.raise_() - self.lbl_basecolor_val1.raise_() - self.cb_hsv.raise_() - self.frame_3.raise_() - self.frame_2.raise_() - self.label_2.raise_() - self.cb_material.raise_() - self.label_14.raise_() - self.cb_shading_model.raise_() - self.cb_metallic.raise_() - self.horizontalLayoutWidget_3.raise_() - self.label_15.raise_() - self.label_16.raise_() - self.slider_roughness.raise_() - self.label_17.raise_() - self.slider_specular.raise_() - self.label_18.raise_() - self.slider_normal.raise_() - self.lbl_roughness.raise_() - self.lbl_specular.raise_() - self.lbl_normal.raise_() - self.horizontalLayout.addWidget(self.sidebar_material) - self.verticalLayout.addLayout(self.horizontalLayout) - MainWindow.setCentralWidget(self.centralwidget) - - self.retranslateUi(MainWindow) - QtCore.QMetaObject.connectSlotsByName(MainWindow) - - def retranslateUi(self, MainWindow): - _translate = QtCore.QCoreApplication.translate - MainWindow.setWindowTitle(_translate("MainWindow", "Render Pipeline Material Editor")) - self.label.setText(_translate("MainWindow", "MATERIAL EDITOR")) - self.label_9.setText(_translate("MainWindow", "RENDER PIPELINE")) - self.label_2.setText(_translate("MainWindow", "Material")) - self.cb_material.setItemText(0, _translate("MainWindow", "tree01")) - self.label_14.setText(_translate("MainWindow", "Shading model")) - self.cb_shading_model.setItemText(0, _translate("MainWindow", "Default")) - self.cb_shading_model.setItemText(1, _translate("MainWindow", "Emissive")) - self.cb_shading_model.setItemText(2, _translate("MainWindow", "Transparent")) - self.cb_metallic.setText(_translate("MainWindow", "Metallic")) - self.lbl_basecolor_val2.setText(_translate("MainWindow", "0.43")) - self.lbl_basecolor_val3.setText(_translate("MainWindow", "0.53")) - self.lbl_basecolor_val1.setText(_translate("MainWindow", "0.12")) - self.lbl_basecolor1.setText(_translate("MainWindow", "R")) - self.lbl_basecolor2.setText(_translate("MainWindow", "G")) - self.lbl_basecolor3.setText(_translate("MainWindow", "B")) - self.label_15.setText(_translate("MainWindow", "Basecolor")) - self.label_16.setText(_translate("MainWindow", "Roughness")) - self.label_17.setText(_translate("MainWindow", "Specular IOR")) - self.label_18.setText(_translate("MainWindow", "Normal-Strength")) - self.lbl_roughness.setText(_translate("MainWindow", "0.32")) - self.lbl_specular.setText(_translate("MainWindow", "1.43")) - self.lbl_normal.setText(_translate("MainWindow", "0.80")) - self.lbl_param1.setText(_translate("MainWindow", "0.20")) - self.lbl_shading_model_param1.setText(_translate("MainWindow", "Shading-model-param")) - self.cb_rgb.setText(_translate("MainWindow", "RGB")) - self.cb_srgb.setText(_translate("MainWindow", "SRGB")) - self.cb_hsv.setText(_translate("MainWindow", "HSV")) - -#from . import resources_rc +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'main_window.ui' +# +# Created by: PyQt5 UI code generator 5.7 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(262, 608) + MainWindow.setMinimumSize(QtCore.QSize(262, 608)) + MainWindow.setMaximumSize(QtCore.QSize(262, 5560)) + MainWindow.setStyleSheet("QMainWindow { background: #fff;}\n" +"*, QLabel { font-family: Roboto; font-weight: 300; }\n" +"\n" +"QSlider::groove:horizontal {\n" +" border: 0;\n" +" height: 4px;\n" +" background: rgba(0, 0, 0, 15);\n" +"}\n" +"\n" +"QSlider::handle:horizonal {\n" +" background: #666;\n" +" width: 8px;\n" +" margin: -6px 0;\n" +"}\n" +"\n" +"QSlider::handle:horizontal:disabled {\n" +" background: #ccc;\n" +"}\n" +"QSlider::groove:horizontal:disabled {\n" +" background: rgba(0, 0, 0, 8);\n" +"}\n" +"\n" +"QRadioButton {\n" +" background: #fafafa;\n" +" padding: 4px 6px 3px;\n" +" font-size: 12px;\n" +" color: #333;\n" +"}\n" +"\n" +"QRadioButton::indicator {\n" +" width: 15px;\n" +" height: 15px;\n" +" margin-top: -1px;\n" +"}\n" +"\n" +"QRadioButton:checked {\n" +"background: #eee;\n" +"}\n" +"\n" +"QRadioButton:hover {\n" +"background: #eaeaea;\n" +"}\n" +"\n" +"QComboBox {\n" +"border: 0;\n" +"background: #fff;\n" +"padding: 7px 10px 3px;\n" +"font-family: Roboto;\n" +"color: #777;\n" +"font-size: 12px;\n" +"}\n" +"\n" +"QComboBox::down-arrow {\n" +" border: 0;\n" +" margin: -3px;\n" +" background: #ddd;\n" +" margin-left: -16px;\n" +"margin-bottom: -4px;\n" +" image: url(\":/img/down-arrow.png\");\n" +"}\n" +"\n" +"QCheckBox::indicator {\n" +" border: 1px solid #aaa;\n" +" background: #39f;\n" +" width: 14px;\n" +" height: 14px;\n" +" image: url(\":/img/checked.png\");\n" +"}\n" +"\n" +"QCheckBox {\n" +"text-transform: uppercase;\n" +"color: #555;\n" +"}\n" +"\n" +"QCheckBox::indicator:unchecked {\n" +" background: #fff;\n" +" image: none;\n" +"}") + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setSpacing(0) + self.verticalLayout.setObjectName("verticalLayout") + self.frame = QtWidgets.QFrame(self.centralwidget) + self.frame.setMinimumSize(QtCore.QSize(0, 65)) + self.frame.setMaximumSize(QtCore.QSize(16777215, 65)) + self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame.setObjectName("frame") + self.label = QtWidgets.QLabel(self.frame) + self.label.setGeometry(QtCore.QRect(20, 26, 261, 31)) + self.label.setStyleSheet("font-size: 20px; color: #03C03C;\n" +"font-weight: 100;") + self.label.setObjectName("label") + self.label_9 = QtWidgets.QLabel(self.frame) + self.label_9.setGeometry(QtCore.QRect(20, 6, 261, 31)) + self.label_9.setStyleSheet("font-size: 13px;\n" +"color: #aaa;\n" +"font-weight: 600;") + self.label_9.setObjectName("label_9") + self.verticalLayout.addWidget(self.frame) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setSpacing(0) + self.horizontalLayout.setObjectName("horizontalLayout") + self.sidebar_material = QtWidgets.QFrame(self.centralwidget) + self.sidebar_material.setMinimumSize(QtCore.QSize(0, 0)) + self.sidebar_material.setMaximumSize(QtCore.QSize(999999, 16777215)) + self.sidebar_material.setStyleSheet("QFrame { background: #eee; } QFrame>QLabel { text-transform: uppercase; color: #888; background: transparent; }") + self.sidebar_material.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.sidebar_material.setFrameShadow(QtWidgets.QFrame.Raised) + self.sidebar_material.setObjectName("sidebar_material") + self.label_2 = QtWidgets.QLabel(self.sidebar_material) + self.label_2.setGeometry(QtCore.QRect(20, 20, 61, 16)) + self.label_2.setObjectName("label_2") + self.cb_material = QtWidgets.QComboBox(self.sidebar_material) + self.cb_material.setGeometry(QtCore.QRect(20, 40, 221, 30)) + self.cb_material.setObjectName("cb_material") + self.cb_material.addItem("") + self.label_14 = QtWidgets.QLabel(self.sidebar_material) + self.label_14.setGeometry(QtCore.QRect(20, 80, 141, 16)) + self.label_14.setObjectName("label_14") + self.cb_shading_model = QtWidgets.QComboBox(self.sidebar_material) + self.cb_shading_model.setGeometry(QtCore.QRect(20, 100, 221, 30)) + self.cb_shading_model.setObjectName("cb_shading_model") + self.cb_shading_model.addItem("") + self.cb_shading_model.addItem("") + self.cb_shading_model.addItem("") + self.cb_metallic = QtWidgets.QCheckBox(self.sidebar_material) + self.cb_metallic.setGeometry(QtCore.QRect(20, 140, 81, 17)) + self.cb_metallic.setObjectName("cb_metallic") + self.horizontalLayoutWidget_3 = QtWidgets.QWidget(self.sidebar_material) + self.horizontalLayoutWidget_3.setGeometry(QtCore.QRect(30, 240, 201, 73)) + self.horizontalLayoutWidget_3.setObjectName("horizontalLayoutWidget_3") + self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_3) + self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout_4.setSpacing(6) + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.gridLayout = QtWidgets.QGridLayout() + self.gridLayout.setObjectName("gridLayout") + self.lbl_basecolor_val2 = QtWidgets.QLabel(self.horizontalLayoutWidget_3) + self.lbl_basecolor_val2.setMinimumSize(QtCore.QSize(30, 0)) + self.lbl_basecolor_val2.setMaximumSize(QtCore.QSize(30, 16777215)) + self.lbl_basecolor_val2.setStyleSheet("background: transparent;") + self.lbl_basecolor_val2.setAlignment(QtCore.Qt.AlignCenter) + self.lbl_basecolor_val2.setObjectName("lbl_basecolor_val2") + self.gridLayout.addWidget(self.lbl_basecolor_val2, 1, 2, 1, 1) + self.basecolor_3 = QtWidgets.QSlider(self.horizontalLayoutWidget_3) + self.basecolor_3.setMaximum(100) + self.basecolor_3.setProperty("value", 53) + self.basecolor_3.setOrientation(QtCore.Qt.Horizontal) + self.basecolor_3.setObjectName("basecolor_3") + self.gridLayout.addWidget(self.basecolor_3, 2, 1, 1, 1) + self.basecolor_1 = QtWidgets.QSlider(self.horizontalLayoutWidget_3) + self.basecolor_1.setMaximum(100) + self.basecolor_1.setProperty("value", 12) + self.basecolor_1.setOrientation(QtCore.Qt.Horizontal) + self.basecolor_1.setObjectName("basecolor_1") + self.gridLayout.addWidget(self.basecolor_1, 0, 1, 1, 1) + self.basecolor_2 = QtWidgets.QSlider(self.horizontalLayoutWidget_3) + self.basecolor_2.setMaximum(100) + self.basecolor_2.setProperty("value", 43) + self.basecolor_2.setOrientation(QtCore.Qt.Horizontal) + self.basecolor_2.setObjectName("basecolor_2") + self.gridLayout.addWidget(self.basecolor_2, 1, 1, 1, 1) + self.lbl_basecolor_val3 = QtWidgets.QLabel(self.horizontalLayoutWidget_3) + self.lbl_basecolor_val3.setMinimumSize(QtCore.QSize(30, 0)) + self.lbl_basecolor_val3.setMaximumSize(QtCore.QSize(30, 16777215)) + self.lbl_basecolor_val3.setStyleSheet("background: transparent;") + self.lbl_basecolor_val3.setAlignment(QtCore.Qt.AlignCenter) + self.lbl_basecolor_val3.setObjectName("lbl_basecolor_val3") + self.gridLayout.addWidget(self.lbl_basecolor_val3, 2, 2, 1, 1) + self.lbl_basecolor_val1 = QtWidgets.QLabel(self.horizontalLayoutWidget_3) + self.lbl_basecolor_val1.setMinimumSize(QtCore.QSize(30, 0)) + self.lbl_basecolor_val1.setMaximumSize(QtCore.QSize(30, 16777215)) + self.lbl_basecolor_val1.setStyleSheet("background: transparent;") + self.lbl_basecolor_val1.setAlignment(QtCore.Qt.AlignCenter) + self.lbl_basecolor_val1.setObjectName("lbl_basecolor_val1") + self.gridLayout.addWidget(self.lbl_basecolor_val1, 0, 2, 1, 1) + self.lbl_basecolor1 = QtWidgets.QLabel(self.horizontalLayoutWidget_3) + self.lbl_basecolor1.setMinimumSize(QtCore.QSize(11, 0)) + self.lbl_basecolor1.setStyleSheet("background: transparent; color: #555; ") + self.lbl_basecolor1.setObjectName("lbl_basecolor1") + self.gridLayout.addWidget(self.lbl_basecolor1, 0, 0, 1, 1) + self.lbl_basecolor2 = QtWidgets.QLabel(self.horizontalLayoutWidget_3) + self.lbl_basecolor2.setMinimumSize(QtCore.QSize(11, 0)) + self.lbl_basecolor2.setStyleSheet("background: transparent; color: #555; ") + self.lbl_basecolor2.setObjectName("lbl_basecolor2") + self.gridLayout.addWidget(self.lbl_basecolor2, 1, 0, 1, 1) + self.lbl_basecolor3 = QtWidgets.QLabel(self.horizontalLayoutWidget_3) + self.lbl_basecolor3.setMinimumSize(QtCore.QSize(11, 0)) + self.lbl_basecolor3.setStyleSheet("background: transparent; color: #555; ") + self.lbl_basecolor3.setObjectName("lbl_basecolor3") + self.gridLayout.addWidget(self.lbl_basecolor3, 2, 0, 1, 1) + self.horizontalLayout_4.addLayout(self.gridLayout) + self.lbl_color_preview = QtWidgets.QLabel(self.horizontalLayoutWidget_3) + self.lbl_color_preview.setMinimumSize(QtCore.QSize(30, 0)) + self.lbl_color_preview.setMaximumSize(QtCore.QSize(30, 16777215)) + self.lbl_color_preview.setStyleSheet("background: rgba(60, 120, 255);") + self.lbl_color_preview.setText("") + self.lbl_color_preview.setObjectName("lbl_color_preview") + self.horizontalLayout_4.addWidget(self.lbl_color_preview) + self.label_15 = QtWidgets.QLabel(self.sidebar_material) + self.label_15.setGeometry(QtCore.QRect(20, 180, 131, 16)) + self.label_15.setObjectName("label_15") + self.label_16 = QtWidgets.QLabel(self.sidebar_material) + self.label_16.setGeometry(QtCore.QRect(20, 340, 131, 16)) + self.label_16.setObjectName("label_16") + self.slider_roughness = QtWidgets.QSlider(self.sidebar_material) + self.slider_roughness.setGeometry(QtCore.QRect(20, 360, 191, 19)) + self.slider_roughness.setMaximum(100) + self.slider_roughness.setProperty("value", 32) + self.slider_roughness.setOrientation(QtCore.Qt.Horizontal) + self.slider_roughness.setObjectName("slider_roughness") + self.label_17 = QtWidgets.QLabel(self.sidebar_material) + self.label_17.setGeometry(QtCore.QRect(20, 390, 131, 16)) + self.label_17.setObjectName("label_17") + self.slider_specular = QtWidgets.QSlider(self.sidebar_material) + self.slider_specular.setGeometry(QtCore.QRect(20, 410, 191, 19)) + self.slider_specular.setMaximum(100) + self.slider_specular.setSliderPosition(20) + self.slider_specular.setOrientation(QtCore.Qt.Horizontal) + self.slider_specular.setObjectName("slider_specular") + self.label_18 = QtWidgets.QLabel(self.sidebar_material) + self.label_18.setGeometry(QtCore.QRect(20, 440, 131, 16)) + self.label_18.setObjectName("label_18") + self.slider_normal = QtWidgets.QSlider(self.sidebar_material) + self.slider_normal.setGeometry(QtCore.QRect(20, 460, 191, 19)) + self.slider_normal.setMaximum(100) + self.slider_normal.setSliderPosition(80) + self.slider_normal.setOrientation(QtCore.Qt.Horizontal) + self.slider_normal.setObjectName("slider_normal") + self.lbl_roughness = QtWidgets.QLabel(self.sidebar_material) + self.lbl_roughness.setGeometry(QtCore.QRect(220, 360, 31, 20)) + self.lbl_roughness.setStyleSheet("color: #333;") + self.lbl_roughness.setObjectName("lbl_roughness") + self.lbl_specular = QtWidgets.QLabel(self.sidebar_material) + self.lbl_specular.setGeometry(QtCore.QRect(220, 410, 31, 20)) + self.lbl_specular.setStyleSheet("color: #333;") + self.lbl_specular.setObjectName("lbl_specular") + self.lbl_normal = QtWidgets.QLabel(self.sidebar_material) + self.lbl_normal.setGeometry(QtCore.QRect(220, 459, 31, 20)) + self.lbl_normal.setStyleSheet("color: #333;") + self.lbl_normal.setObjectName("lbl_normal") + self.frame_2 = QtWidgets.QFrame(self.sidebar_material) + self.frame_2.setGeometry(QtCore.QRect(-10, 166, 311, 561)) + self.frame_2.setStyleSheet("QFrame { background: #fff; }") + self.frame_2.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame_2.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame_2.setObjectName("frame_2") + self.lbl_param1 = QtWidgets.QLabel(self.frame_2) + self.lbl_param1.setGeometry(QtCore.QRect(230, 339, 31, 20)) + self.lbl_param1.setStyleSheet("color: #333;") + self.lbl_param1.setObjectName("lbl_param1") + self.slider_param1 = QtWidgets.QSlider(self.frame_2) + self.slider_param1.setEnabled(False) + self.slider_param1.setGeometry(QtCore.QRect(30, 340, 191, 19)) + self.slider_param1.setMaximum(100) + self.slider_param1.setSliderPosition(80) + self.slider_param1.setOrientation(QtCore.Qt.Horizontal) + self.slider_param1.setObjectName("slider_param1") + self.lbl_shading_model_param1 = QtWidgets.QLabel(self.frame_2) + self.lbl_shading_model_param1.setGeometry(QtCore.QRect(30, 320, 141, 16)) + self.lbl_shading_model_param1.setObjectName("lbl_shading_model_param1") + self.cb_rgb = QtWidgets.QRadioButton(self.frame_2) + self.cb_rgb.setGeometry(QtCore.QRect(30, 34, 60, 27)) + self.cb_rgb.setChecked(True) + self.cb_rgb.setObjectName("cb_rgb") + self.cb_srgb = QtWidgets.QRadioButton(self.frame_2) + self.cb_srgb.setGeometry(QtCore.QRect(92, 34, 71, 27)) + self.cb_srgb.setObjectName("cb_srgb") + self.cb_hsv = QtWidgets.QRadioButton(self.frame_2) + self.cb_hsv.setGeometry(QtCore.QRect(165, 34, 62, 27)) + self.cb_hsv.setObjectName("cb_hsv") + self.frame_3 = QtWidgets.QFrame(self.frame_2) + self.frame_3.setGeometry(QtCore.QRect(30, 59, 220, 96)) + self.frame_3.setStyleSheet("QFrame { background: #eee; }") + self.frame_3.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame_3.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame_3.setObjectName("frame_3") + self.lbl_param1.raise_() + self.slider_param1.raise_() + self.lbl_shading_model_param1.raise_() + self.cb_rgb.raise_() + self.cb_srgb.raise_() + self.lbl_basecolor_val1.raise_() + self.cb_hsv.raise_() + self.frame_3.raise_() + self.frame_2.raise_() + self.label_2.raise_() + self.cb_material.raise_() + self.label_14.raise_() + self.cb_shading_model.raise_() + self.cb_metallic.raise_() + self.horizontalLayoutWidget_3.raise_() + self.label_15.raise_() + self.label_16.raise_() + self.slider_roughness.raise_() + self.label_17.raise_() + self.slider_specular.raise_() + self.label_18.raise_() + self.slider_normal.raise_() + self.lbl_roughness.raise_() + self.lbl_specular.raise_() + self.lbl_normal.raise_() + self.horizontalLayout.addWidget(self.sidebar_material) + self.verticalLayout.addLayout(self.horizontalLayout) + MainWindow.setCentralWidget(self.centralwidget) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "Render Pipeline Material Editor")) + self.label.setText(_translate("MainWindow", "MATERIAL EDITOR")) + self.label_9.setText(_translate("MainWindow", "RENDER PIPELINE")) + self.label_2.setText(_translate("MainWindow", "Material")) + self.cb_material.setItemText(0, _translate("MainWindow", "tree01")) + self.label_14.setText(_translate("MainWindow", "Shading model")) + self.cb_shading_model.setItemText(0, _translate("MainWindow", "Default")) + self.cb_shading_model.setItemText(1, _translate("MainWindow", "Emissive")) + self.cb_shading_model.setItemText(2, _translate("MainWindow", "Transparent")) + self.cb_metallic.setText(_translate("MainWindow", "Metallic")) + self.lbl_basecolor_val2.setText(_translate("MainWindow", "0.43")) + self.lbl_basecolor_val3.setText(_translate("MainWindow", "0.53")) + self.lbl_basecolor_val1.setText(_translate("MainWindow", "0.12")) + self.lbl_basecolor1.setText(_translate("MainWindow", "R")) + self.lbl_basecolor2.setText(_translate("MainWindow", "G")) + self.lbl_basecolor3.setText(_translate("MainWindow", "B")) + self.label_15.setText(_translate("MainWindow", "Basecolor")) + self.label_16.setText(_translate("MainWindow", "Roughness")) + self.label_17.setText(_translate("MainWindow", "Specular IOR")) + self.label_18.setText(_translate("MainWindow", "Normal-Strength")) + self.lbl_roughness.setText(_translate("MainWindow", "0.32")) + self.lbl_specular.setText(_translate("MainWindow", "1.43")) + self.lbl_normal.setText(_translate("MainWindow", "0.80")) + self.lbl_param1.setText(_translate("MainWindow", "0.20")) + self.lbl_shading_model_param1.setText(_translate("MainWindow", "Shading-model-param")) + self.cb_rgb.setText(_translate("MainWindow", "RGB")) + self.cb_srgb.setText(_translate("MainWindow", "SRGB")) + self.cb_hsv.setText(_translate("MainWindow", "HSV")) + +#from . import resources_rc diff --git a/RenderPipelineFile/toolkit/material_editor/ui/resources.qrc b/RenderPipelineFile/toolkit/material_editor/ui/resources.qrc index 2d0a50d6..47eaa06f 100644 --- a/RenderPipelineFile/toolkit/material_editor/ui/resources.qrc +++ b/RenderPipelineFile/toolkit/material_editor/ui/resources.qrc @@ -1,6 +1,6 @@ - - - checked.png - down-arrow.png - - + + + checked.png + down-arrow.png + + diff --git a/RenderPipelineFile/toolkit/material_editor/ui/resources_rc.py b/RenderPipelineFile/toolkit/material_editor/ui/resources_rc.py index 9251ba38..8f092def 100644 --- a/RenderPipelineFile/toolkit/material_editor/ui/resources_rc.py +++ b/RenderPipelineFile/toolkit/material_editor/ui/resources_rc.py @@ -1,88 +1,88 @@ -# -*- coding: utf-8 -*- - -# Resource object code -# -# Created by: The Resource Compiler for PyQt5 (Qt v5.7.0) -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore - -qt_resource_data = b"\ -\x00\x00\x01\xb0\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x10\x00\x00\x00\x10\x08\x04\x00\x00\x00\xb5\xfa\x37\xea\ -\x00\x00\x00\x04\x67\x41\x4d\x41\x00\x00\xb1\x8f\x0b\xfc\x61\x05\ -\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\x26\x00\x00\x80\x84\ -\x00\x00\xfa\x00\x00\x00\x80\xe8\x00\x00\x75\x30\x00\x00\xea\x60\ -\x00\x00\x3a\x98\x00\x00\x17\x70\x9c\xba\x51\x3c\x00\x00\x00\x02\ -\x62\x4b\x47\x44\x00\x00\xaa\x8d\x23\x32\x00\x00\x00\x09\x70\x48\ -\x59\x73\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01\x42\x28\x9b\x78\x00\ -\x00\x00\x07\x74\x49\x4d\x45\x07\xe0\x0b\x13\x14\x13\x01\xe2\x2b\ -\x02\xea\x00\x00\x00\x7e\x49\x44\x41\x54\x28\xcf\x63\x60\x18\x78\ -\xc0\xd8\xcd\xfd\x75\x2e\x43\x56\xc3\x3b\x4c\xa9\x06\x11\x86\xe9\ -\x92\x51\x4c\xa5\x5f\x19\x9e\x33\xec\x69\x10\xc2\x22\xbd\x8f\xe1\ -\x4e\xfa\x6f\x26\x06\x86\x86\x42\x86\x83\xe8\x4a\xda\x84\x19\xf6\ -\x32\x6c\x6d\xa8\x64\x60\x60\x84\xaa\xef\x67\xb0\x67\x70\x81\x59\ -\xd4\x26\xfc\x6b\x2f\xc3\xf6\x86\x4a\x06\x06\xb8\x02\x64\x25\x0d\ -\x42\x0c\x7b\x19\x76\x40\xa4\x91\x14\xc0\x94\x30\x30\x20\x4b\xa3\ -\x28\x80\x2a\x61\x60\xd8\x89\x90\xc6\xf4\x5a\x6b\x63\xfd\x40\x87\ -\x3c\x26\x00\x00\x8c\x5b\x28\xe0\xab\xb2\x1c\xe7\x00\x00\x00\x25\ -\x74\x45\x58\x74\x64\x61\x74\x65\x3a\x63\x72\x65\x61\x74\x65\x00\ -\x32\x30\x31\x36\x2d\x31\x31\x2d\x31\x39\x54\x32\x30\x3a\x31\x39\ -\x3a\x30\x31\x2b\x30\x31\x3a\x30\x30\x22\xee\x27\xba\x00\x00\x00\ -\x25\x74\x45\x58\x74\x64\x61\x74\x65\x3a\x6d\x6f\x64\x69\x66\x79\ -\x00\x32\x30\x31\x36\x2d\x31\x31\x2d\x31\x39\x54\x32\x30\x3a\x31\ -\x39\x3a\x30\x31\x2b\x30\x31\x3a\x30\x30\x53\xb3\x9f\x06\x00\x00\ -\x00\x19\x74\x45\x58\x74\x53\x6f\x66\x74\x77\x61\x72\x65\x00\x77\ -\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\x2e\x6f\x72\x67\x9b\ -\xee\x3c\x1a\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x00\xe7\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x10\x00\x00\x00\x10\x04\x03\x00\x00\x00\xed\xdd\xe2\x52\ -\x00\x00\x00\x03\x73\x42\x49\x54\x08\x08\x08\xdb\xe1\x4f\xe0\x00\ -\x00\x00\x09\x70\x48\x59\x73\x00\x00\x03\xd1\x00\x00\x03\xd1\x01\ -\x98\x8a\x89\xb4\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\x74\ -\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\ -\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x30\x50\x4c\x54\ -\x45\x8b\x92\x9e\x97\x9d\xa8\x99\xa0\xab\xa0\xa7\xb1\xa4\xab\xb4\ -\xa6\xac\xb5\xa7\xad\xb6\xb3\xb9\xc0\xb5\xbb\xc2\xb6\xbc\xc3\xc1\ -\xc7\xcc\xc3\xc9\xcf\xd0\xd5\xd9\xe3\xe8\xea\xe6\xeb\xec\xe7\xec\ -\xed\x53\xe8\xb6\xd9\x00\x00\x00\x29\x49\x44\x41\x54\x08\x5b\x63\ -\xf8\x0f\x05\x0c\x44\x33\xae\x43\x19\xff\x78\xa0\x8c\x8f\xfb\xc1\ -\x0c\xaf\xff\x91\x10\x35\x8f\x22\xf2\xa1\x8a\x27\x23\x74\x11\xc9\ -\x00\x00\xa9\xd1\x7a\xfc\xeb\x17\x98\xb3\x00\x00\x00\x00\x49\x45\ -\x4e\x44\xae\x42\x60\x82\ -" - -qt_resource_name = b"\ -\x00\x03\ -\x00\x00\x70\x37\ -\x00\x69\ -\x00\x6d\x00\x67\ -\x00\x0e\ -\x04\xac\x3c\xa7\ -\x00\x64\ -\x00\x6f\x00\x77\x00\x6e\x00\x2d\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x0b\ -\x01\x64\x80\x07\ -\x00\x63\ -\x00\x68\x00\x65\x00\x63\x00\x6b\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ -" - -qt_resource_struct = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\ -\x00\x00\x00\x2e\x00\x00\x00\x00\x00\x01\x00\x00\x01\xb4\ -\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -" - -def qInitResources(): - QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) - -def qCleanupResources(): - QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) - -qInitResources() +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created by: The Resource Compiler for PyQt5 (Qt v5.7.0) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + +qt_resource_data = b"\ +\x00\x00\x01\xb0\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x10\x00\x00\x00\x10\x08\x04\x00\x00\x00\xb5\xfa\x37\xea\ +\x00\x00\x00\x04\x67\x41\x4d\x41\x00\x00\xb1\x8f\x0b\xfc\x61\x05\ +\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\x26\x00\x00\x80\x84\ +\x00\x00\xfa\x00\x00\x00\x80\xe8\x00\x00\x75\x30\x00\x00\xea\x60\ +\x00\x00\x3a\x98\x00\x00\x17\x70\x9c\xba\x51\x3c\x00\x00\x00\x02\ +\x62\x4b\x47\x44\x00\x00\xaa\x8d\x23\x32\x00\x00\x00\x09\x70\x48\ +\x59\x73\x00\x00\x0d\xd7\x00\x00\x0d\xd7\x01\x42\x28\x9b\x78\x00\ +\x00\x00\x07\x74\x49\x4d\x45\x07\xe0\x0b\x13\x14\x13\x01\xe2\x2b\ +\x02\xea\x00\x00\x00\x7e\x49\x44\x41\x54\x28\xcf\x63\x60\x18\x78\ +\xc0\xd8\xcd\xfd\x75\x2e\x43\x56\xc3\x3b\x4c\xa9\x06\x11\x86\xe9\ +\x92\x51\x4c\xa5\x5f\x19\x9e\x33\xec\x69\x10\xc2\x22\xbd\x8f\xe1\ +\x4e\xfa\x6f\x26\x06\x86\x86\x42\x86\x83\xe8\x4a\xda\x84\x19\xf6\ +\x32\x6c\x6d\xa8\x64\x60\x60\x84\xaa\xef\x67\xb0\x67\x70\x81\x59\ +\xd4\x26\xfc\x6b\x2f\xc3\xf6\x86\x4a\x06\x06\xb8\x02\x64\x25\x0d\ +\x42\x0c\x7b\x19\x76\x40\xa4\x91\x14\xc0\x94\x30\x30\x20\x4b\xa3\ +\x28\x80\x2a\x61\x60\xd8\x89\x90\xc6\xf4\x5a\x6b\x63\xfd\x40\x87\ +\x3c\x26\x00\x00\x8c\x5b\x28\xe0\xab\xb2\x1c\xe7\x00\x00\x00\x25\ +\x74\x45\x58\x74\x64\x61\x74\x65\x3a\x63\x72\x65\x61\x74\x65\x00\ +\x32\x30\x31\x36\x2d\x31\x31\x2d\x31\x39\x54\x32\x30\x3a\x31\x39\ +\x3a\x30\x31\x2b\x30\x31\x3a\x30\x30\x22\xee\x27\xba\x00\x00\x00\ +\x25\x74\x45\x58\x74\x64\x61\x74\x65\x3a\x6d\x6f\x64\x69\x66\x79\ +\x00\x32\x30\x31\x36\x2d\x31\x31\x2d\x31\x39\x54\x32\x30\x3a\x31\ +\x39\x3a\x30\x31\x2b\x30\x31\x3a\x30\x30\x53\xb3\x9f\x06\x00\x00\ +\x00\x19\x74\x45\x58\x74\x53\x6f\x66\x74\x77\x61\x72\x65\x00\x77\ +\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\x65\x2e\x6f\x72\x67\x9b\ +\xee\x3c\x1a\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x00\xe7\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x10\x00\x00\x00\x10\x04\x03\x00\x00\x00\xed\xdd\xe2\x52\ +\x00\x00\x00\x03\x73\x42\x49\x54\x08\x08\x08\xdb\xe1\x4f\xe0\x00\ +\x00\x00\x09\x70\x48\x59\x73\x00\x00\x03\xd1\x00\x00\x03\xd1\x01\ +\x98\x8a\x89\xb4\x00\x00\x00\x19\x74\x45\x58\x74\x53\x6f\x66\x74\ +\x77\x61\x72\x65\x00\x77\x77\x77\x2e\x69\x6e\x6b\x73\x63\x61\x70\ +\x65\x2e\x6f\x72\x67\x9b\xee\x3c\x1a\x00\x00\x00\x30\x50\x4c\x54\ +\x45\x8b\x92\x9e\x97\x9d\xa8\x99\xa0\xab\xa0\xa7\xb1\xa4\xab\xb4\ +\xa6\xac\xb5\xa7\xad\xb6\xb3\xb9\xc0\xb5\xbb\xc2\xb6\xbc\xc3\xc1\ +\xc7\xcc\xc3\xc9\xcf\xd0\xd5\xd9\xe3\xe8\xea\xe6\xeb\xec\xe7\xec\ +\xed\x53\xe8\xb6\xd9\x00\x00\x00\x29\x49\x44\x41\x54\x08\x5b\x63\ +\xf8\x0f\x05\x0c\x44\x33\xae\x43\x19\xff\x78\xa0\x8c\x8f\xfb\xc1\ +\x0c\xaf\xff\x91\x10\x35\x8f\x22\xf2\xa1\x8a\x27\x23\x74\x11\xc9\ +\x00\x00\xa9\xd1\x7a\xfc\xeb\x17\x98\xb3\x00\x00\x00\x00\x49\x45\ +\x4e\x44\xae\x42\x60\x82\ +" + +qt_resource_name = b"\ +\x00\x03\ +\x00\x00\x70\x37\ +\x00\x69\ +\x00\x6d\x00\x67\ +\x00\x0e\ +\x04\xac\x3c\xa7\ +\x00\x64\ +\x00\x6f\x00\x77\x00\x6e\x00\x2d\x00\x61\x00\x72\x00\x72\x00\x6f\x00\x77\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x0b\ +\x01\x64\x80\x07\ +\x00\x63\ +\x00\x68\x00\x65\x00\x63\x00\x6b\x00\x65\x00\x64\x00\x2e\x00\x70\x00\x6e\x00\x67\ +" + +qt_resource_struct = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\ +\x00\x00\x00\x2e\x00\x00\x00\x00\x00\x01\x00\x00\x01\xb4\ +\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +" + +def qInitResources(): + QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources() diff --git a/RenderPipelineFile/toolkit/pathtracing_reference/README.md b/RenderPipelineFile/toolkit/pathtracing_reference/README.md index cb9cdd21..d95442dc 100644 --- a/RenderPipelineFile/toolkit/pathtracing_reference/README.md +++ b/RenderPipelineFile/toolkit/pathtracing_reference/README.md @@ -1,9 +1,9 @@ - -# Pathtracer Reference - -This is a utility tool to generate a pathtracer reference. -You need to get a copy of the Mitsuba renderer, and copy it to `C:/mitsuba`. - -The Mitsuba renderer can be acquired from: - -https://www.mitsuba-renderer.org/download.html + +# Pathtracer Reference + +This is a utility tool to generate a pathtracer reference. +You need to get a copy of the Mitsuba renderer, and copy it to `C:/mitsuba`. + +The Mitsuba renderer can be acquired from: + +https://www.mitsuba-renderer.org/download.html diff --git a/RenderPipelineFile/toolkit/pathtracing_reference/batch_compare.py b/RenderPipelineFile/toolkit/pathtracing_reference/batch_compare.py index d0b76aa9..5c8c85bd 100644 --- a/RenderPipelineFile/toolkit/pathtracing_reference/batch_compare.py +++ b/RenderPipelineFile/toolkit/pathtracing_reference/batch_compare.py @@ -1,208 +1,208 @@ -""" - -Compares various results - -""" - -from __future__ import print_function - -import os -import sys - -from panda3d.core import PNMImage - -try: - os.makedirs("batch_compare") -except: - pass - -overlay = PNMImage("res/overlay.png") - -materials_to_test = [] - -GOLD_F0 = (1, 0.867136, 0.358654) - -# materials_to_test.append({ -# "name": "Gold", -# "basecolor": GOLD_F0, -# "roughness": 0.1, -# "type": "metallic", -# "material_src": "Au" -# }) - -# materials_to_test.append({ -# "name": "Foliage", -# "basecolor": (0.6, 0.9, 0.6), -# "roughness": 0.6, -# "type": "foliage" -# }) - -# materials_to_test.append({ -# "name": "GoldRough", -# "basecolor": GOLD_F0, -# "roughness": 0.4, -# "type": "metallic", -# "material_src": "Au" -# }) - - -# materials_to_test.append({ -# "name": "Test-R0", -# "basecolor": (0, 0, 0), -# "ior": 1.51, -# "roughness": 0.3 -# }) - -# materials_to_test.append({ -# "name": "Test-IOR1.1", -# "basecolor": (0, 0, 1), -# "ior": 1.1, -# "roughness": 0.0 -# }) - -# materials_to_test.append({ -# "name": "Test-IOR2", -# "basecolor": (0, 0, 1), -# "ior": 2.0, -# "roughness": 0.0 -# }) - -# materials_to_test.append({ -# "name": "Test-IOR2.5", -# "basecolor": (0, 0, 1), -# "ior": 2.5, -# "roughness": 0.0 -# }) - - -# materials_to_test.append({ -# "name": "Test-SpecOnly", -# "basecolor": (0, 0, 0), -# "ior": 1.5, -# "roughness": 0.0 -# }) - -# materials_to_test.append({ -# "name": "Test-SpecOnly-R0.3", -# "basecolor": (0, 0, 0), -# "ior": 1.5, -# "roughness": 0.3 -# }) - -# materials_to_test.append({ -# "name": "Nonrefractive-Diffuse", -# "basecolor": (0.5, 0.5, 0.5), -# "ior": 1.0, -# "roughness": 1.0 -# }) - -# materials_to_test.append({ -# "name": "Rough-Diffuse", -# "basecolor": (0.5, 0.5, 0.5), -# "ior": 1.5, -# "roughness": 0.8 -# }) -# materials_to_test.append({ -# "name": "Normal-Diffuse", -# "basecolor": (00.5, 0.5, 0.5), -# "ior": 1.5, -# "roughness": 0.4 -# }) - - -for i in range(11): - roughness = i / 10.0 - - materials_to_test.append({ - "name": "Plastic-R" + str(roughness), - "basecolor": (1, 0, 0), - "ior": 1.51, - "roughness": roughness - }) - - # materials_to_test.append({ - # "name": "Gold-R" + str(roughness), - # "basecolor": GOLD_F0, - # "ior": 1.51, - # "type": "metallic", - # "material_src": "Au", - # "roughness": roughness - # }) - - # materials_to_test.append({ - # "name": "Clearcoat-R" + str(roughness), - # "basecolor": (1, 0.85, 0.345), - # "material_src": "Au", - # "type": "clearcoat", - # "roughness": roughness - # }) - - # materials_to_test.append({ - # "name": "Diffuse-R" + str(roughness), - # "basecolor": (0.8, 0.8, 0.8), - # "ior": 1.16, - # "roughness": roughness - # }) - - -for material in materials_to_test: - - print("Testing material", material["name"]) - - # Write material def - with open("_tmp_material.py", "w") as handle: - handle.write("# Autogenerated\n") - handle.write("name = '{}'".format(material["name"]) + "\n") - handle.write("roughness = {}".format(material["roughness"]) + "\n") - handle.write("ior = {}".format(material.get("ior", 1.51)) + "\n") - handle.write("basecolor = {}".format(material["basecolor"]) + "\n") - handle.write("mat_type = '{}'".format(material.get("type", "default")) + "\n") - - # run rp - print(" Running RP ..") - cmd = '"{}"'.format(sys.executable) + " -B run_renderpipeline.py silent > nul" - os.system(cmd) - - print(" Running mitsuba ..") - # run mitsuba - with open("res/scene.templ.xml", "r") as handle: - content = handle.read() - - mat_type = material.get("type", "default") - - use_default = mat_type == "default" - use_metallic = mat_type == "metallic" - - for shading_type in (("default", "metallic", "clearcoat")): - def_name = shading_type.upper() - if mat_type == shading_type: - content = content.replace("%IF_" + def_name + "%", "") - content = content.replace("%ENDIF_" + def_name + "%", "") - else: - content = content.replace("%IF_" + def_name + "%", "") - - content = content.replace("%ALPHA%", str(material["roughness"] * material["roughness"])) - content = content.replace("%IOR%", str(material.get("ior", 1.51))) - content = content.replace("%BASECOLOR%", str(material["basecolor"]).strip("()")) - content = content.replace("%MATERIAL_SRC%", material.get("material_src", "").strip("()")) - - with open("res/scene.xml", "w") as handle: - handle.write(content) - - os.system("run_mitsuba.bat > nul") - - print(" Writing result ..") - img = PNMImage("scene-rp.png") - img_ref = PNMImage("scene.png") - - img.copy_sub_image(img_ref, 256, 0, 256, 0, 256, 512) - img.mult_sub_image(overlay, 0, 0) - img.write("batch_compare/" + material["name"] + ".png") - -try: - os.remove("_tmp_material.py") -except: - pass - -print("Done!") +""" + +Compares various results + +""" + +from __future__ import print_function + +import os +import sys + +from panda3d.core import PNMImage + +try: + os.makedirs("batch_compare") +except: + pass + +overlay = PNMImage("res/overlay.png") + +materials_to_test = [] + +GOLD_F0 = (1, 0.867136, 0.358654) + +# materials_to_test.append({ +# "name": "Gold", +# "basecolor": GOLD_F0, +# "roughness": 0.1, +# "type": "metallic", +# "material_src": "Au" +# }) + +# materials_to_test.append({ +# "name": "Foliage", +# "basecolor": (0.6, 0.9, 0.6), +# "roughness": 0.6, +# "type": "foliage" +# }) + +# materials_to_test.append({ +# "name": "GoldRough", +# "basecolor": GOLD_F0, +# "roughness": 0.4, +# "type": "metallic", +# "material_src": "Au" +# }) + + +# materials_to_test.append({ +# "name": "Test-R0", +# "basecolor": (0, 0, 0), +# "ior": 1.51, +# "roughness": 0.3 +# }) + +# materials_to_test.append({ +# "name": "Test-IOR1.1", +# "basecolor": (0, 0, 1), +# "ior": 1.1, +# "roughness": 0.0 +# }) + +# materials_to_test.append({ +# "name": "Test-IOR2", +# "basecolor": (0, 0, 1), +# "ior": 2.0, +# "roughness": 0.0 +# }) + +# materials_to_test.append({ +# "name": "Test-IOR2.5", +# "basecolor": (0, 0, 1), +# "ior": 2.5, +# "roughness": 0.0 +# }) + + +# materials_to_test.append({ +# "name": "Test-SpecOnly", +# "basecolor": (0, 0, 0), +# "ior": 1.5, +# "roughness": 0.0 +# }) + +# materials_to_test.append({ +# "name": "Test-SpecOnly-R0.3", +# "basecolor": (0, 0, 0), +# "ior": 1.5, +# "roughness": 0.3 +# }) + +# materials_to_test.append({ +# "name": "Nonrefractive-Diffuse", +# "basecolor": (0.5, 0.5, 0.5), +# "ior": 1.0, +# "roughness": 1.0 +# }) + +# materials_to_test.append({ +# "name": "Rough-Diffuse", +# "basecolor": (0.5, 0.5, 0.5), +# "ior": 1.5, +# "roughness": 0.8 +# }) +# materials_to_test.append({ +# "name": "Normal-Diffuse", +# "basecolor": (00.5, 0.5, 0.5), +# "ior": 1.5, +# "roughness": 0.4 +# }) + + +for i in range(11): + roughness = i / 10.0 + + materials_to_test.append({ + "name": "Plastic-R" + str(roughness), + "basecolor": (1, 0, 0), + "ior": 1.51, + "roughness": roughness + }) + + # materials_to_test.append({ + # "name": "Gold-R" + str(roughness), + # "basecolor": GOLD_F0, + # "ior": 1.51, + # "type": "metallic", + # "material_src": "Au", + # "roughness": roughness + # }) + + # materials_to_test.append({ + # "name": "Clearcoat-R" + str(roughness), + # "basecolor": (1, 0.85, 0.345), + # "material_src": "Au", + # "type": "clearcoat", + # "roughness": roughness + # }) + + # materials_to_test.append({ + # "name": "Diffuse-R" + str(roughness), + # "basecolor": (0.8, 0.8, 0.8), + # "ior": 1.16, + # "roughness": roughness + # }) + + +for material in materials_to_test: + + print("Testing material", material["name"]) + + # Write material def + with open("_tmp_material.py", "w") as handle: + handle.write("# Autogenerated\n") + handle.write("name = '{}'".format(material["name"]) + "\n") + handle.write("roughness = {}".format(material["roughness"]) + "\n") + handle.write("ior = {}".format(material.get("ior", 1.51)) + "\n") + handle.write("basecolor = {}".format(material["basecolor"]) + "\n") + handle.write("mat_type = '{}'".format(material.get("type", "default")) + "\n") + + # run rp + print(" Running RP ..") + cmd = '"{}"'.format(sys.executable) + " -B run_renderpipeline.py silent > nul" + os.system(cmd) + + print(" Running mitsuba ..") + # run mitsuba + with open("res/scene.templ.xml", "r") as handle: + content = handle.read() + + mat_type = material.get("type", "default") + + use_default = mat_type == "default" + use_metallic = mat_type == "metallic" + + for shading_type in (("default", "metallic", "clearcoat")): + def_name = shading_type.upper() + if mat_type == shading_type: + content = content.replace("%IF_" + def_name + "%", "") + content = content.replace("%ENDIF_" + def_name + "%", "") + else: + content = content.replace("%IF_" + def_name + "%", "") + + content = content.replace("%ALPHA%", str(material["roughness"] * material["roughness"])) + content = content.replace("%IOR%", str(material.get("ior", 1.51))) + content = content.replace("%BASECOLOR%", str(material["basecolor"]).strip("()")) + content = content.replace("%MATERIAL_SRC%", material.get("material_src", "").strip("()")) + + with open("res/scene.xml", "w") as handle: + handle.write(content) + + os.system("run_mitsuba.bat > nul") + + print(" Writing result ..") + img = PNMImage("scene-rp.png") + img_ref = PNMImage("scene.png") + + img.copy_sub_image(img_ref, 256, 0, 256, 0, 256, 512) + img.mult_sub_image(overlay, 0, 0) + img.write("batch_compare/" + material["name"] + ".png") + +try: + os.remove("_tmp_material.py") +except: + pass + +print("Done!") diff --git a/RenderPipelineFile/toolkit/pathtracing_reference/config/daytime.yaml b/RenderPipelineFile/toolkit/pathtracing_reference/config/daytime.yaml index 90e061b4..ffc8d922 100644 --- a/RenderPipelineFile/toolkit/pathtracing_reference/config/daytime.yaml +++ b/RenderPipelineFile/toolkit/pathtracing_reference/config/daytime.yaml @@ -1,24 +1,24 @@ - -# Render Pipeline Time Of Day Configuration -# Instead of editing this file, prefer to use the Time Of Day Editor -# Any formatting and comments will be lost - -control_points: - clouds: - cloud_brightness: [[[0.5000000000,0.5000000000]]] - cloud_intensity: [[[0.5,0.5]]] - cumulus_amount: [[[0.5,1.0000000000]]] - stratus_amount: [[[0.1103700000,1.0000000000]]] - soft_amount: [[[0.5,1.0000000000]]] - color_correction: - camera_iso: [[[0.5345489443,0.5136513820]]] - camera_shutter: [[[0.5095969290,0.1128516483]]] - camera_aperture: [[[0.5019193858,0.4838709677]]] - scattering: - sun_intensity: [[[0.5000000000,0.5000000000]]] - sun_color: [[[0.4971900000,0.5763000000],[0.0433100000,0.8999700000],[0.8597400000,0.9130000000],[0.1785000000,0.8973600000],[0.8099800000,0.8651100000],[0.2360800000,0.7712700000],[0.6602700000,0.6451700000],[0.1286000000,0.9530800000]],[[0.5058900000,0.5160300000],[0.0572700000,0.6541600000],[0.2395000000,0.5976800000],[0.8104600000,0.6009000000],[0.6967400000,0.5483900000]],[[0.0862400000,0.4257800000],[0.4955600000,0.4033000000],[0.8234200000,0.4340200000]]] - sun_azimuth: [[[0.5000000000,0.5000000000]]] - sun_altitude: [[[0.5000000000,0.5000000000]]] - fog_ramp_size: [[[0.6468330134,0.9178885631]]] - dof: - some_scalar: [[[0.5000000000,0.5000000000]]] + +# Render Pipeline Time Of Day Configuration +# Instead of editing this file, prefer to use the Time Of Day Editor +# Any formatting and comments will be lost + +control_points: + clouds: + cloud_brightness: [[[0.5000000000,0.5000000000]]] + cloud_intensity: [[[0.5,0.5]]] + cumulus_amount: [[[0.5,1.0000000000]]] + stratus_amount: [[[0.1103700000,1.0000000000]]] + soft_amount: [[[0.5,1.0000000000]]] + color_correction: + camera_iso: [[[0.5345489443,0.5136513820]]] + camera_shutter: [[[0.5095969290,0.1128516483]]] + camera_aperture: [[[0.5019193858,0.4838709677]]] + scattering: + sun_intensity: [[[0.5000000000,0.5000000000]]] + sun_color: [[[0.4971900000,0.5763000000],[0.0433100000,0.8999700000],[0.8597400000,0.9130000000],[0.1785000000,0.8973600000],[0.8099800000,0.8651100000],[0.2360800000,0.7712700000],[0.6602700000,0.6451700000],[0.1286000000,0.9530800000]],[[0.5058900000,0.5160300000],[0.0572700000,0.6541600000],[0.2395000000,0.5976800000],[0.8104600000,0.6009000000],[0.6967400000,0.5483900000]],[[0.0862400000,0.4257800000],[0.4955600000,0.4033000000],[0.8234200000,0.4340200000]]] + sun_azimuth: [[[0.5000000000,0.5000000000]]] + sun_altitude: [[[0.5000000000,0.5000000000]]] + fog_ramp_size: [[[0.6468330134,0.9178885631]]] + dof: + some_scalar: [[[0.5000000000,0.5000000000]]] diff --git a/RenderPipelineFile/toolkit/pathtracing_reference/config/debugging.yaml b/RenderPipelineFile/toolkit/pathtracing_reference/config/debugging.yaml index fa9cfc3e..5980b9cd 100644 --- a/RenderPipelineFile/toolkit/pathtracing_reference/config/debugging.yaml +++ b/RenderPipelineFile/toolkit/pathtracing_reference/config/debugging.yaml @@ -1,17 +1,17 @@ - -# This file contains all available debug modes. -# You can add or remove debug modes here, aswell as -# disable or enable them, whatever fits to you. - -# NOTICE: This file only has effect if debugging is enabled -# in the pipeline.yaml (settings.pipeline.display_debuger == True) - -render_modes: - - { name: "Diffuse", key: "DIFFUSE" } - - { name: "Roughness", key: "ROUGHNESS" } - - { name: "Specular", key: "SPECULAR" } - - { name: "Normal", key: "NORMAL" } - - { name: "Metallic", key: "METALLIC" } - - { name: "Translucency", key: "TRANSLUCENCY" } - - { name: "PSSM Splits", key: "PSSM_SPLITS", requires: "pssm", cxx_only: true } - - { name: "Ambient Occlusion", key: "OCCLUSION", requires: "ao" } + +# This file contains all available debug modes. +# You can add or remove debug modes here, aswell as +# disable or enable them, whatever fits to you. + +# NOTICE: This file only has effect if debugging is enabled +# in the pipeline.yaml (settings.pipeline.display_debuger == True) + +render_modes: + - { name: "Diffuse", key: "DIFFUSE" } + - { name: "Roughness", key: "ROUGHNESS" } + - { name: "Specular", key: "SPECULAR" } + - { name: "Normal", key: "NORMAL" } + - { name: "Metallic", key: "METALLIC" } + - { name: "Translucency", key: "TRANSLUCENCY" } + - { name: "PSSM Splits", key: "PSSM_SPLITS", requires: "pssm", cxx_only: true } + - { name: "Ambient Occlusion", key: "OCCLUSION", requires: "ao" } diff --git a/RenderPipelineFile/toolkit/pathtracing_reference/config/panda3d-config.prc b/RenderPipelineFile/toolkit/pathtracing_reference/config/panda3d-config.prc index dfdb0fe3..b83cc1fa 100644 --- a/RenderPipelineFile/toolkit/pathtracing_reference/config/panda3d-config.prc +++ b/RenderPipelineFile/toolkit/pathtracing_reference/config/panda3d-config.prc @@ -1,172 +1,172 @@ - - -# This is the config file used to configure basic settings for Panda3D. -# The pipeline loads it at startup to ensure the environment is setup properly. - -# -------------- Debugging options -------------- - -# gl-dump-compiled-shaders #t -# notify-level-glgsg debug -# notify-level-glgsg warning -# notify-level-gobj debug -pstats-gpu-timing #t -pstats-max-rate 200 -gl-debug #t -gl-debug-object-labels #t -sync-video #f - -# ----------------- Misc Settings ----------------- - -# No stack trace on assertion, set this to true to make panda crash on assertions -# (which will allow to debug it) -assert-abort #t - -# File system should be case sensitive -# NOTICE: Set this to #f if you are using tempfile, since it returns -# wrong cased directory paths -vfs-case-sensitive #t - -# Enable state cache, this seems to actually help the performance by a lot -state-cache #t -transform-cache #t - -# Frame rate meter style -frame-rate-meter-milliseconds #t -frame-rate-meter-update-interval 1.0 -frame-rate-meter-text-pattern %0.2f fps -frame-rate-meter-ms-text-pattern %0.3f ms -frame-rate-meter-layer-sort 1000 -frame-rate-meter-scale 0.036 -frame-rate-meter-side-margins 0.4 -show-frame-rate-meter #f - -# Set text settings -text-minfilter linear -text-magfilter linear -text-page-size 512 512 - -# Better text performance since rdb's patch -text-flatten 0 -text-dynamic-merge 1 - -# For smoother animations -# even-animation #t - -# Threading, really buggy! -#threading-model App/Cull/Draw - -support-stencil #f -framebuffer-stencil #f - -# Don't use srgb correction, we do that ourself -framebuffer-srgb #f - -# Don't use multisamples -framebuffer-multisample #f -multisamples 0 - -# Don't rescale textures which are no power-of-2 -textures-power-2 none - -# This is required, the pipeline does not support resizing yet -win-fixed-size #t - -# Set default texture filters -texture-anisotropic-degree 8 -texture-magfilter linear -texture-minfilter linear -texture-quality-level fastest - -# Enable seamless cubemap filtering, thats important for environment filtering -gl-cube-map-seamless #t - -# Set model cache dir -model-cache-dir $USER_APPDATA/Panda3D-1.9/cache -model-cache-textures #f - -# Disable the annoying SRGB warning from pnmimage -notify-level-pnmimage error - -# Disable the buffer viewer, we have our own -show-buffers #f - -# Use the default coordinate system, this makes our matrix transformations -# faster because we don't have have to transform them to a different coordinate -# system before -gl-coordinate-system default - -# This makes animations smoother, especially if they were exported at 30 FPS -# and are played at 60 FPS -interpolate-frames #t - -# Disable workarround in panda which causes our shadow atlas to take twice -# the amount of vram it should, due to an intel driver bug. -gl-force-fbo-color #f - -# ----------- OpenGL / Performance Settings ------------ - -# Animations on the gpu. The default shader has to get adjusted to support this -# feature before this option can be turned on. -# hardware-animated-vertices #t - -# Try this options for performance -# uniquify-matrix #t -# uniquify-transforms #t -# uniquify-states #t -# uniquify-attribs #f - -# Enable garbarge collection -garbage-collect-states #t -# garbage-collect-states-rate 0.2 - -# Compress textures on the drivers? -# driver-compress-textures #t - -# Faster animations? (Have to test) -# matrix-palette #t -# display-list-animation #t - -# Better GL performance by not using gl-finish and so on -gl-finish #f -gl-force-no-error #t -gl-check-errors #f -gl-force-no-flush #t -gl-force-no-scissor #t - -# Eventually disable memory barriers, have to check if this is faster -gl-enable-memory-barriers #f - -# Disable threading -lock-to-one-cpu #t -support-threads #f - -# Let the driver generate the mipmaps -driver-generate-mipmaps #t -#gl-ignore-mipmaps #t - -# Use immutable texture storage, it is *supposed* to be faster, but might not be -gl-immutable-texture-storage #t - -# auto-flip #f - -# Default window settings -# depth-bits 0 -color-bits 0 - -framebuffer-depth #f - -gl-fixed-vertex-attrib-locations #f - -# Disable the fragment shader performance warning -gl-validate-shaders #f -gl-skip-shader-recompilation-warnings #t - -alpha-scale-via-texture #f -bounds-type best # best/fastest/sphere/box -pstats-name Render Pipeline Stats -rescale-normals #f -screenshot-extension png - -# Required for correct velocity -always-store-prev-transform #t -allow-incomplete-render #t + + +# This is the config file used to configure basic settings for Panda3D. +# The pipeline loads it at startup to ensure the environment is setup properly. + +# -------------- Debugging options -------------- + +# gl-dump-compiled-shaders #t +# notify-level-glgsg debug +# notify-level-glgsg warning +# notify-level-gobj debug +pstats-gpu-timing #t +pstats-max-rate 200 +gl-debug #t +gl-debug-object-labels #t +sync-video #f + +# ----------------- Misc Settings ----------------- + +# No stack trace on assertion, set this to true to make panda crash on assertions +# (which will allow to debug it) +assert-abort #t + +# File system should be case sensitive +# NOTICE: Set this to #f if you are using tempfile, since it returns +# wrong cased directory paths +vfs-case-sensitive #t + +# Enable state cache, this seems to actually help the performance by a lot +state-cache #t +transform-cache #t + +# Frame rate meter style +frame-rate-meter-milliseconds #t +frame-rate-meter-update-interval 1.0 +frame-rate-meter-text-pattern %0.2f fps +frame-rate-meter-ms-text-pattern %0.3f ms +frame-rate-meter-layer-sort 1000 +frame-rate-meter-scale 0.036 +frame-rate-meter-side-margins 0.4 +show-frame-rate-meter #f + +# Set text settings +text-minfilter linear +text-magfilter linear +text-page-size 512 512 + +# Better text performance since rdb's patch +text-flatten 0 +text-dynamic-merge 1 + +# For smoother animations +# even-animation #t + +# Threading, really buggy! +#threading-model App/Cull/Draw + +support-stencil #f +framebuffer-stencil #f + +# Don't use srgb correction, we do that ourself +framebuffer-srgb #f + +# Don't use multisamples +framebuffer-multisample #f +multisamples 0 + +# Don't rescale textures which are no power-of-2 +textures-power-2 none + +# This is required, the pipeline does not support resizing yet +win-fixed-size #t + +# Set default texture filters +texture-anisotropic-degree 8 +texture-magfilter linear +texture-minfilter linear +texture-quality-level fastest + +# Enable seamless cubemap filtering, thats important for environment filtering +gl-cube-map-seamless #t + +# Set model cache dir +model-cache-dir $USER_APPDATA/Panda3D-1.9/cache +model-cache-textures #f + +# Disable the annoying SRGB warning from pnmimage +notify-level-pnmimage error + +# Disable the buffer viewer, we have our own +show-buffers #f + +# Use the default coordinate system, this makes our matrix transformations +# faster because we don't have have to transform them to a different coordinate +# system before +gl-coordinate-system default + +# This makes animations smoother, especially if they were exported at 30 FPS +# and are played at 60 FPS +interpolate-frames #t + +# Disable workarround in panda which causes our shadow atlas to take twice +# the amount of vram it should, due to an intel driver bug. +gl-force-fbo-color #f + +# ----------- OpenGL / Performance Settings ------------ + +# Animations on the gpu. The default shader has to get adjusted to support this +# feature before this option can be turned on. +# hardware-animated-vertices #t + +# Try this options for performance +# uniquify-matrix #t +# uniquify-transforms #t +# uniquify-states #t +# uniquify-attribs #f + +# Enable garbarge collection +garbage-collect-states #t +# garbage-collect-states-rate 0.2 + +# Compress textures on the drivers? +# driver-compress-textures #t + +# Faster animations? (Have to test) +# matrix-palette #t +# display-list-animation #t + +# Better GL performance by not using gl-finish and so on +gl-finish #f +gl-force-no-error #t +gl-check-errors #f +gl-force-no-flush #t +gl-force-no-scissor #t + +# Eventually disable memory barriers, have to check if this is faster +gl-enable-memory-barriers #f + +# Disable threading +lock-to-one-cpu #t +support-threads #f + +# Let the driver generate the mipmaps +driver-generate-mipmaps #t +#gl-ignore-mipmaps #t + +# Use immutable texture storage, it is *supposed* to be faster, but might not be +gl-immutable-texture-storage #t + +# auto-flip #f + +# Default window settings +# depth-bits 0 +color-bits 0 + +framebuffer-depth #f + +gl-fixed-vertex-attrib-locations #f + +# Disable the fragment shader performance warning +gl-validate-shaders #f +gl-skip-shader-recompilation-warnings #t + +alpha-scale-via-texture #f +bounds-type best # best/fastest/sphere/box +pstats-name Render Pipeline Stats +rescale-normals #f +screenshot-extension png + +# Required for correct velocity +always-store-prev-transform #t +allow-incomplete-render #t diff --git a/RenderPipelineFile/toolkit/pathtracing_reference/config/pipeline.yaml b/RenderPipelineFile/toolkit/pathtracing_reference/config/pipeline.yaml index d87cbaa4..aba3f270 100644 --- a/RenderPipelineFile/toolkit/pathtracing_reference/config/pipeline.yaml +++ b/RenderPipelineFile/toolkit/pathtracing_reference/config/pipeline.yaml @@ -1,19 +1,19 @@ - -pipeline: - display_debugger: false - use_r11_g11_b10: false - resolution_scale: 2.0 - reference_mode: true - -lighting: - culling_grid_size_x: 32 - culling_grid_size_y: 16 - culling_grid_slices: 32 - culling_max_distance: 50.0 - culling_slice_width: 256 - max_lights_per_cell: 64 - -shadows: - atlas_size: 4096 - max_updates: 40 - max_update_distance: 150.0 + +pipeline: + display_debugger: false + use_r11_g11_b10: false + resolution_scale: 2.0 + reference_mode: true + +lighting: + culling_grid_size_x: 32 + culling_grid_size_y: 16 + culling_grid_slices: 32 + culling_max_distance: 50.0 + culling_slice_width: 256 + max_lights_per_cell: 64 + +shadows: + atlas_size: 4096 + max_updates: 40 + max_update_distance: 150.0 diff --git a/RenderPipelineFile/toolkit/pathtracing_reference/config/plugins.yaml b/RenderPipelineFile/toolkit/pathtracing_reference/config/plugins.yaml index 728beb50..b2726e5e 100644 --- a/RenderPipelineFile/toolkit/pathtracing_reference/config/plugins.yaml +++ b/RenderPipelineFile/toolkit/pathtracing_reference/config/plugins.yaml @@ -1,155 +1,155 @@ - -# Render Pipeline Plugin Configuration -# Instead of editing this file, prefer to use the Plugin Configurator -# Any formatting and comments will be lost - -enabled: - # - ao - # - bloom - # - color_correction - # - env_probes - # - pssm - # - scattering - # - smaa - # - ssr - # - clouds - # - dof - # - fxaa - # - motion_blur - # - skin_shading - # - volumetrics - # - vxgi - - -overrides: - ao: - blur_normal_factor: 4.94 - blur_depth_factor: 4.83 - occlusion_strength: 1.21 - technique: SSAO - ssao_sample_radius: 79.61 - ssao_sample_count: 8 - ssao_bias: 0.0333 - ssao_max_distance: 6.89 - hbao_sample_radius: 119.11 - hbao_ray_count: 4 - hbao_ray_steps: 3 - hbao_tangent_bias: 0.5889 - hbao_max_distance: 4.9 - ssvo_sample_count: 4 - ssvo_sphere_radius: 7.86 - ssvo_max_distance: 0.6 - alchemy_sample_radius: 127.8901 - alchemy_num_samples: 8 - alchemy_max_distance: 6.07 - ue4ao_sample_radius: 110.33 - ue4ao_sample_count: 8 - ue4ao_max_distance: 1.87 - - bloom: - num_mipmaps: 8 - bloom_strength: 0.45 - remove_fireflies: False - lens_dirt_factor: 0.7111 - - clouds: - cloud_decay: 5.78 - raymarch_steps: 64 - - color_correction: - tonemap_operator: optimized - reinhard_version: rgb - exponential_factor: 1.4 - uc2t_shoulder_strength: 0.22 - uc2t_linear_strength: 0.3 - uc2t_linear_angle: 0.1 - uc2t_toe_strength: 0.2 - uc2t_toe_numerator: 0.01 - uc2t_toe_denumerator: 0.3 - uc2t_reference_white: 11.2 - vignette_strength: 0.4444 - film_grain_strength: 0.2889 - color_lut: color_lut.png - use_chromatic_aberration: True - chromatic_aberration_strength: 0.22 - chromatic_aberration_samples: 2 - manual_camera_parameters: False - min_exposure_value: 0.012 - max_exposure_value: 1.0 - exposure_bias: 0.0 - brightness_adaption_rate: 2.67 - darkness_adaption_rate: 0.79 - use_sharpen: False - sharpen_strength: 0.5 - sharpen_twice: False - - dof: - - env_probes: - probe_resolution: 128 - diffuse_probe_resolution: 4 - max_probes: 16 - max_probes_per_cell: 5 - - fxaa: - quality: ultra - subpixel_quality: 0.5 - edge_threshold: 0.075 - min_threshold: 0.7114 - - motion_blur: - num_camera_samples: 6 - camera_blur_factor: 0.5778 - blur_factor: 0.5 - tile_size: 32 - max_blur_radius: 10.0 - num_samples: 12 - - pssm: - max_distance: 45.0 - logarithmic_factor: 2.67 - sun_distance: 1145.0 - split_count: 3 - resolution: 2048 - border_bias: 0.01943 - use_pcf: True - filter_sample_count: 32 - filter_radius: 9.89 - fixed_bias: 0.0889 - slope_bias: 0.39 - normal_bias: 0.0 - use_pcss: False - pcss_search_samples: 32 - pcss_penumbra_size: 300.0 - use_distant_shadows: True - dist_shadow_resolution: 1024 - dist_shadow_clipsize: 150.0 - scene_shadow_resolution: 1024 - - scattering: - scattering_method: eric_bruneton - ground_reflectance: 0.1 - rayleigh_height_scale: 8.0 - mie_height_scale: 1.45 - mie_phase_factor: 0.21 - beta_mie_scattering: 4.0 - enable_godrays: False - - skin_shading: - quality: high - blur_scale: 1.0 - - smaa: - use_reprojection: True - smaa_quality: ultra - - ssr: - - volumetrics: - - vxgi: - grid_resolution: 256 - grid_ws_size: 20.0 - diffuse_cone_steps: 32 - specular_cone_steps: 150 - + +# Render Pipeline Plugin Configuration +# Instead of editing this file, prefer to use the Plugin Configurator +# Any formatting and comments will be lost + +enabled: + # - ao + # - bloom + # - color_correction + # - env_probes + # - pssm + # - scattering + # - smaa + # - ssr + # - clouds + # - dof + # - fxaa + # - motion_blur + # - skin_shading + # - volumetrics + # - vxgi + + +overrides: + ao: + blur_normal_factor: 4.94 + blur_depth_factor: 4.83 + occlusion_strength: 1.21 + technique: SSAO + ssao_sample_radius: 79.61 + ssao_sample_count: 8 + ssao_bias: 0.0333 + ssao_max_distance: 6.89 + hbao_sample_radius: 119.11 + hbao_ray_count: 4 + hbao_ray_steps: 3 + hbao_tangent_bias: 0.5889 + hbao_max_distance: 4.9 + ssvo_sample_count: 4 + ssvo_sphere_radius: 7.86 + ssvo_max_distance: 0.6 + alchemy_sample_radius: 127.8901 + alchemy_num_samples: 8 + alchemy_max_distance: 6.07 + ue4ao_sample_radius: 110.33 + ue4ao_sample_count: 8 + ue4ao_max_distance: 1.87 + + bloom: + num_mipmaps: 8 + bloom_strength: 0.45 + remove_fireflies: False + lens_dirt_factor: 0.7111 + + clouds: + cloud_decay: 5.78 + raymarch_steps: 64 + + color_correction: + tonemap_operator: optimized + reinhard_version: rgb + exponential_factor: 1.4 + uc2t_shoulder_strength: 0.22 + uc2t_linear_strength: 0.3 + uc2t_linear_angle: 0.1 + uc2t_toe_strength: 0.2 + uc2t_toe_numerator: 0.01 + uc2t_toe_denumerator: 0.3 + uc2t_reference_white: 11.2 + vignette_strength: 0.4444 + film_grain_strength: 0.2889 + color_lut: color_lut.png + use_chromatic_aberration: True + chromatic_aberration_strength: 0.22 + chromatic_aberration_samples: 2 + manual_camera_parameters: False + min_exposure_value: 0.012 + max_exposure_value: 1.0 + exposure_bias: 0.0 + brightness_adaption_rate: 2.67 + darkness_adaption_rate: 0.79 + use_sharpen: False + sharpen_strength: 0.5 + sharpen_twice: False + + dof: + + env_probes: + probe_resolution: 128 + diffuse_probe_resolution: 4 + max_probes: 16 + max_probes_per_cell: 5 + + fxaa: + quality: ultra + subpixel_quality: 0.5 + edge_threshold: 0.075 + min_threshold: 0.7114 + + motion_blur: + num_camera_samples: 6 + camera_blur_factor: 0.5778 + blur_factor: 0.5 + tile_size: 32 + max_blur_radius: 10.0 + num_samples: 12 + + pssm: + max_distance: 45.0 + logarithmic_factor: 2.67 + sun_distance: 1145.0 + split_count: 3 + resolution: 2048 + border_bias: 0.01943 + use_pcf: True + filter_sample_count: 32 + filter_radius: 9.89 + fixed_bias: 0.0889 + slope_bias: 0.39 + normal_bias: 0.0 + use_pcss: False + pcss_search_samples: 32 + pcss_penumbra_size: 300.0 + use_distant_shadows: True + dist_shadow_resolution: 1024 + dist_shadow_clipsize: 150.0 + scene_shadow_resolution: 1024 + + scattering: + scattering_method: eric_bruneton + ground_reflectance: 0.1 + rayleigh_height_scale: 8.0 + mie_height_scale: 1.45 + mie_phase_factor: 0.21 + beta_mie_scattering: 4.0 + enable_godrays: False + + skin_shading: + quality: high + blur_scale: 1.0 + + smaa: + use_reprojection: True + smaa_quality: ultra + + ssr: + + volumetrics: + + vxgi: + grid_resolution: 256 + grid_ws_size: 20.0 + diffuse_cone_steps: 32 + specular_cone_steps: 150 + diff --git a/RenderPipelineFile/toolkit/pathtracing_reference/config/stages.yaml b/RenderPipelineFile/toolkit/pathtracing_reference/config/stages.yaml index c76ef0ae..8701377f 100644 --- a/RenderPipelineFile/toolkit/pathtracing_reference/config/stages.yaml +++ b/RenderPipelineFile/toolkit/pathtracing_reference/config/stages.yaml @@ -1,61 +1,61 @@ - -# This file controls the order of all render stages. -# Usually you do not have to modify this, except if you are adding new plugins. -# If you move stages, you should know what you do. -# Commenting out a stage won't disable it, but instead produce an error when -# it is created. - -global_stage_order: - - # Shadows, Environment and Voxelization - - PSSMShadowStage - - PSSMDistShadowStage - - PSSMSceneShadowStage - - ScatteringEnvmapStage - - EnvironmentCaptureStage - - VoxelizationStage - - # Main scene - - GBufferStage - - ShadowStage - - DownscaleZStage - - CombineVelocityStage - - # Light(-culling) - - FlagUsedCellsStage - - CollectUsedCellsStage - - CullLightsStage - - CullProbesStage - - # Plugins - - CloudVoxelStage - - AOStage - - ApplyLightsStage - - PSSMStage - - ScatteringStage - - ApplyEnvprobesStage - - VXGIStage - - SSRStage - - SSSRStage - - AmbientStage - - ApplyCloudsStage - - GodrayStage - - VolumetricsStage - - AutoExposureStage - - ManualExposureStage - - # Post AA & Effects - - BloomStage - - TonemappingStage - - SMAAStage - - FXAAStage - - DoFStage - - MotionBlurStage - - SkinShadingStage - - SharpenStage - - ColorCorrectionStage - - # Finishing stages, do not insert anything below - - UpscaleStage - - FinalStage - - UpdatePreviousPipesStage + +# This file controls the order of all render stages. +# Usually you do not have to modify this, except if you are adding new plugins. +# If you move stages, you should know what you do. +# Commenting out a stage won't disable it, but instead produce an error when +# it is created. + +global_stage_order: + + # Shadows, Environment and Voxelization + - PSSMShadowStage + - PSSMDistShadowStage + - PSSMSceneShadowStage + - ScatteringEnvmapStage + - EnvironmentCaptureStage + - VoxelizationStage + + # Main scene + - GBufferStage + - ShadowStage + - DownscaleZStage + - CombineVelocityStage + + # Light(-culling) + - FlagUsedCellsStage + - CollectUsedCellsStage + - CullLightsStage + - CullProbesStage + + # Plugins + - CloudVoxelStage + - AOStage + - ApplyLightsStage + - PSSMStage + - ScatteringStage + - ApplyEnvprobesStage + - VXGIStage + - SSRStage + - SSSRStage + - AmbientStage + - ApplyCloudsStage + - GodrayStage + - VolumetricsStage + - AutoExposureStage + - ManualExposureStage + + # Post AA & Effects + - BloomStage + - TonemappingStage + - SMAAStage + - FXAAStage + - DoFStage + - MotionBlurStage + - SkinShadingStage + - SharpenStage + - ColorCorrectionStage + + # Finishing stages, do not insert anything below + - UpscaleStage + - FinalStage + - UpdatePreviousPipesStage diff --git a/RenderPipelineFile/toolkit/pathtracing_reference/config/task-scheduler.yaml b/RenderPipelineFile/toolkit/pathtracing_reference/config/task-scheduler.yaml index 23d6c7f3..b5007a59 100644 --- a/RenderPipelineFile/toolkit/pathtracing_reference/config/task-scheduler.yaml +++ b/RenderPipelineFile/toolkit/pathtracing_reference/config/task-scheduler.yaml @@ -1,33 +1,33 @@ - -# This file controls which tasks are allowed to run each frame. -# Usually you do not have to edit this file, except when developing plugins. - -frame_cycles: !!omap - - - frame1: - - envprobes_select_and_cull - - pssm_scene_shadows - - - frame2: - - envprobes_capture_envmap_face0 - - pssm_distant_shadows - - - frame3: - - envprobes_capture_envmap_face1 - - pssm_convert_distant_to_esm - - - frame4: - - envprobes_capture_envmap_face2 - - pssm_blur_distant_vert - - - frame5: - - pssm_blur_distant_horiz - - envprobes_capture_envmap_face3 - - envprobes_capture_envmap_face4 - - - frame6: - - envprobes_capture_envmap_face5 - - scattering_update_envmap - - - frame7: - - envprobes_filter_and_store_envmap + +# This file controls which tasks are allowed to run each frame. +# Usually you do not have to edit this file, except when developing plugins. + +frame_cycles: !!omap + + - frame1: + - envprobes_select_and_cull + - pssm_scene_shadows + + - frame2: + - envprobes_capture_envmap_face0 + - pssm_distant_shadows + + - frame3: + - envprobes_capture_envmap_face1 + - pssm_convert_distant_to_esm + + - frame4: + - envprobes_capture_envmap_face2 + - pssm_blur_distant_vert + + - frame5: + - pssm_blur_distant_horiz + - envprobes_capture_envmap_face3 + - envprobes_capture_envmap_face4 + + - frame6: + - envprobes_capture_envmap_face5 + - scattering_update_envmap + + - frame7: + - envprobes_filter_and_store_envmap diff --git a/RenderPipelineFile/toolkit/pathtracing_reference/generate_difference.py b/RenderPipelineFile/toolkit/pathtracing_reference/generate_difference.py index 8088e7ef..284e6bc7 100644 --- a/RenderPipelineFile/toolkit/pathtracing_reference/generate_difference.py +++ b/RenderPipelineFile/toolkit/pathtracing_reference/generate_difference.py @@ -1,42 +1,42 @@ -""" - -Compares both renders - -""" - -from __future__ import print_function - -from panda3d.core import PNMImage - -source_a = "scene.png" -source_b = "scene-rp.png" - -write_diff_img = False - -img_a = PNMImage(source_a) -img_b = PNMImage(source_b) - -w, h = img_a.get_x_size(), img_a.get_y_size() -img_dest = PNMImage(w, h, 3) - -error_scale = 10.0 - -total_diff = 0.0 - -for x in xrange(w): - for y in xrange(h): - val_a = img_a.get_xel(x, y) - val_b = img_b.get_xel(x, y) - - abs_diff = (val_a - val_b) * error_scale - r, g, b = abs(abs_diff.x), abs(abs_diff.y), abs(abs_diff.z) - - img_dest.set_xel(x, y, r, g, b) - total_diff += r + g + b - -total_diff /= float(w * h) -total_diff /= error_scale - -print("Average difference: ", total_diff, " in RGB: ", total_diff * 255) - -img_dest.write("difference.png") +""" + +Compares both renders + +""" + +from __future__ import print_function + +from panda3d.core import PNMImage + +source_a = "scene.png" +source_b = "scene-rp.png" + +write_diff_img = False + +img_a = PNMImage(source_a) +img_b = PNMImage(source_b) + +w, h = img_a.get_x_size(), img_a.get_y_size() +img_dest = PNMImage(w, h, 3) + +error_scale = 10.0 + +total_diff = 0.0 + +for x in xrange(w): + for y in xrange(h): + val_a = img_a.get_xel(x, y) + val_b = img_b.get_xel(x, y) + + abs_diff = (val_a - val_b) * error_scale + r, g, b = abs(abs_diff.x), abs(abs_diff.y), abs(abs_diff.z) + + img_dest.set_xel(x, y, r, g, b) + total_diff += r + g + b + +total_diff /= float(w * h) +total_diff /= error_scale + +print("Average difference: ", total_diff, " in RGB: ", total_diff * 255) + +img_dest.write("difference.png") diff --git a/RenderPipelineFile/toolkit/pathtracing_reference/res/scene.templ.xml b/RenderPipelineFile/toolkit/pathtracing_reference/res/scene.templ.xml index 5ab5a7d5..a87f02cd 100644 --- a/RenderPipelineFile/toolkit/pathtracing_reference/res/scene.templ.xml +++ b/RenderPipelineFile/toolkit/pathtracing_reference/res/scene.templ.xml @@ -1,119 +1,119 @@ - - - - - - - - - - - - - - %IF_DEFAULT% - - - - - - - - %ENDIF_DEFAULT% - - %IF_METALLIC% - - - - - - - - %ENDIF_METALLIC% - - %IF_CLEARCOAT% - - - - - - - - - - - - - - - - %ENDIF_CLEARCOAT% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + %IF_DEFAULT% + + + + + + + + %ENDIF_DEFAULT% + + %IF_METALLIC% + + + + + + + + %ENDIF_METALLIC% + + %IF_CLEARCOAT% + + + + + + + + + + + + + + + + %ENDIF_CLEARCOAT% + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RenderPipelineFile/toolkit/pathtracing_reference/res/scene.xml b/RenderPipelineFile/toolkit/pathtracing_reference/res/scene.xml index 187d5d1c..d9e569f2 100644 --- a/RenderPipelineFile/toolkit/pathtracing_reference/res/scene.xml +++ b/RenderPipelineFile/toolkit/pathtracing_reference/res/scene.xml @@ -1,119 +1,119 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/RenderPipelineFile/toolkit/pathtracing_reference/res/tex/convert_cubemap.py b/RenderPipelineFile/toolkit/pathtracing_reference/res/tex/convert_cubemap.py index 0e3b921e..24e0555b 100644 --- a/RenderPipelineFile/toolkit/pathtracing_reference/res/tex/convert_cubemap.py +++ b/RenderPipelineFile/toolkit/pathtracing_reference/res/tex/convert_cubemap.py @@ -1,66 +1,66 @@ -""" -Converts the cubemap to a spherical one -""" - -from __future__ import division, print_function - -from panda3d.core import * # noqa -load_prc_file_data("", "textures-power-2 none") - -import direct.directbase.DirectStart # noqa - -cubemap = loader.load_cube_map("../../../../data/default_cubemap/source/#.jpg") -w, h = 4096, 2048 - -cshader = Shader.make_compute(Shader.SL_GLSL, """ -#version 430 -layout (local_size_x = 16, local_size_y = 16, local_size_z = 1) in; - -uniform samplerCube SourceTex; -uniform writeonly image2D DestTex; -#define M_PI 3.1415926535897932384626433 -#define TWO_PI 6.2831853071795864769252867 - -// Converts a normalized spherical coordinate (r = 1) to cartesian coordinates -vec3 spherical_to_vector(float theta, float phi) { - float sin_theta = sin(theta); - return normalize(vec3( - sin_theta * cos(phi), - sin_theta * sin(phi), - cos(theta) - )); -} - -// Fixes the cubemap direction -vec3 fix_cubemap_coord(vec3 coord) { - return normalize(coord.xzy * vec3(1,-1,1)); -} - -void main() { - ivec2 dimensions = imageSize(DestTex).xy; - ivec2 coord = ivec2(gl_GlobalInvocationID.xy); - float theta = (coord.x + 0.5) / float(dimensions.x) * TWO_PI; - float phi = (dimensions.y - coord.y - 0.5) / float(dimensions.y) * M_PI; - vec3 v = spherical_to_vector(phi, theta); - v = fix_cubemap_coord(v); - vec4 color = texture(SourceTex, v); - imageStore(DestTex, coord, vec4(color)); -} -""") - -dest_tex = Texture("") -dest_tex.setup_2d_texture(w, h, Texture.T_float, Texture.F_rgba16) - -print("Converting to spherical coordinates ..") -np = NodePath("np") -np.set_shader(cshader) -np.set_shader_input("SourceTex", cubemap) -np.set_shader_input("DestTex", dest_tex) -attr = np.get_attrib(ShaderAttrib) -base.graphicsEngine.dispatch_compute((w // 16, h // 16, 1), attr, base.win.gsg) - -print("Extracting data ..") -base.graphicsEngine.extract_texture_data(dest_tex, base.win.gsg) - -print("Writing texture ..") -dest_tex.write("envmap.png") +""" +Converts the cubemap to a spherical one +""" + +from __future__ import division, print_function + +from panda3d.core import * # noqa +load_prc_file_data("", "textures-power-2 none") + +import direct.directbase.DirectStart # noqa + +cubemap = loader.load_cube_map("../../../../data/default_cubemap/source/#.jpg") +w, h = 4096, 2048 + +cshader = Shader.make_compute(Shader.SL_GLSL, """ +#version 430 +layout (local_size_x = 16, local_size_y = 16, local_size_z = 1) in; + +uniform samplerCube SourceTex; +uniform writeonly image2D DestTex; +#define M_PI 3.1415926535897932384626433 +#define TWO_PI 6.2831853071795864769252867 + +// Converts a normalized spherical coordinate (r = 1) to cartesian coordinates +vec3 spherical_to_vector(float theta, float phi) { + float sin_theta = sin(theta); + return normalize(vec3( + sin_theta * cos(phi), + sin_theta * sin(phi), + cos(theta) + )); +} + +// Fixes the cubemap direction +vec3 fix_cubemap_coord(vec3 coord) { + return normalize(coord.xzy * vec3(1,-1,1)); +} + +void main() { + ivec2 dimensions = imageSize(DestTex).xy; + ivec2 coord = ivec2(gl_GlobalInvocationID.xy); + float theta = (coord.x + 0.5) / float(dimensions.x) * TWO_PI; + float phi = (dimensions.y - coord.y - 0.5) / float(dimensions.y) * M_PI; + vec3 v = spherical_to_vector(phi, theta); + v = fix_cubemap_coord(v); + vec4 color = texture(SourceTex, v); + imageStore(DestTex, coord, vec4(color)); +} +""") + +dest_tex = Texture("") +dest_tex.setup_2d_texture(w, h, Texture.T_float, Texture.F_rgba16) + +print("Converting to spherical coordinates ..") +np = NodePath("np") +np.set_shader(cshader) +np.set_shader_input("SourceTex", cubemap) +np.set_shader_input("DestTex", dest_tex) +attr = np.get_attrib(ShaderAttrib) +base.graphicsEngine.dispatch_compute((w // 16, h // 16, 1), attr, base.win.gsg) + +print("Extracting data ..") +base.graphicsEngine.extract_texture_data(dest_tex, base.win.gsg) + +print("Writing texture ..") +dest_tex.write("envmap.png") diff --git a/RenderPipelineFile/toolkit/pathtracing_reference/run_mitsuba.bat b/RenderPipelineFile/toolkit/pathtracing_reference/run_mitsuba.bat index 3219f7e4..cd8add76 100644 --- a/RenderPipelineFile/toolkit/pathtracing_reference/run_mitsuba.bat +++ b/RenderPipelineFile/toolkit/pathtracing_reference/run_mitsuba.bat @@ -1,12 +1,12 @@ -@echo off - -"C:/mitsuba/mitsuba" -p 6 res/scene.xml -echo "" -echo Rendering done -echo "" -echo "" -echo "" -"C:/mitsuba/mtsutil" tonemap res/scene.exr -copy res\scene.png scene.png -del res\scene.exr -del mitsuba.*.log +@echo off + +"C:/mitsuba/mitsuba" -p 6 res/scene.xml +echo "" +echo Rendering done +echo "" +echo "" +echo "" +"C:/mitsuba/mtsutil" tonemap res/scene.exr +copy res\scene.png scene.png +del res\scene.exr +del mitsuba.*.log diff --git a/RenderPipelineFile/toolkit/pathtracing_reference/run_renderpipeline.py b/RenderPipelineFile/toolkit/pathtracing_reference/run_renderpipeline.py index f8e8104b..746cda68 100644 --- a/RenderPipelineFile/toolkit/pathtracing_reference/run_renderpipeline.py +++ b/RenderPipelineFile/toolkit/pathtracing_reference/run_renderpipeline.py @@ -1,94 +1,94 @@ -""" - -Renders the sphere using the render pipeline - -""" - -import _tmp_material as material - -import sys -from panda3d.core import load_prc_file_data, Vec4 -from direct.showbase.ShowBase import ShowBase - - -class Application(ShowBase): - - def __init__(self): - sys.path.insert(0, "../../") - load_prc_file_data("", "win-size 512 512") - load_prc_file_data("", "textures-power-2 none") - load_prc_file_data("", "print-pipe-types #f") - load_prc_file_data("", "notify-level-glgsg error") - # load_prc_file_data("", "win-size 1024 1024") - - from rpcore import RenderPipeline, PointLight - - self.render_pipeline = RenderPipeline() - self.render_pipeline.mount_mgr.config_dir = "config/" - self.render_pipeline.create(self) - - sphere = self.loader.loadModel("res/sphere.bam") - sphere.reparent_to(self.render) - - self.disableMouse() - self.camLens.setFov(40) - self.camLens.setNearFar(0.03, 2000.0) - self.camera.set_pos(0, -3.5, 0) - self.camera.look_at(0, -2.5, 0) - - self.render2d.hide() - self.aspect2d.hide() - - light = PointLight() - light.pos = 10, -10, 10 - light.radius = 1e20 - light.color = (1, 1, 1) - light.inner_radius = 4.0 - light.energy = 3 - self.render_pipeline.add_light(light) - - light = PointLight() - light.pos = -10, -10, 10 - light.radius = 1e20 - light.color = (1, 1, 1) - light.inner_radius = 4.0 - light.energy = 3 - self.render_pipeline.add_light(light) - - for mat in sphere.find_all_materials(): - mat.roughness = material.roughness - mat.base_color = Vec4(*(list(material.basecolor) + [1])) - mat.refractive_index = material.ior - - mat.metallic = 1.0 if material.mat_type == "metallic" else 0.0 - - if material.mat_type == "clearcoat": - mat.emission = (2, 0, 0, 0) - mat.metallic = 1.0 - mat.refractive_index = 1.51 - - if material.mat_type == "foliage": - mat.emission = (5, 0, 0, 0) - mat.metallic = 0.0 - mat.refractive_index = 1.51 - - for i in range(10): - self.taskMgr.step() - - self.win.save_screenshot("scene-rp.png") - - self.accept("r", self.reload) - - def reload(self): - print("Reloading") - self.render_pipeline.reload_shaders() - - for i in range(4): - self.taskMgr.step() - - self.win.save_screenshot("scene-rp.png") - -if len(sys.argv) <= 1: - Application().run() -else: - Application() +""" + +Renders the sphere using the render pipeline + +""" + +import _tmp_material as material + +import sys +from panda3d.core import load_prc_file_data, Vec4 +from direct.showbase.ShowBase import ShowBase + + +class Application(ShowBase): + + def __init__(self): + sys.path.insert(0, "../../") + load_prc_file_data("", "win-size 512 512") + load_prc_file_data("", "textures-power-2 none") + load_prc_file_data("", "print-pipe-types #f") + load_prc_file_data("", "notify-level-glgsg error") + # load_prc_file_data("", "win-size 1024 1024") + + from rpcore import RenderPipeline, PointLight + + self.render_pipeline = RenderPipeline() + self.render_pipeline.mount_mgr.config_dir = "config/" + self.render_pipeline.create(self) + + sphere = self.loader.loadModel("res/sphere.bam") + sphere.reparent_to(self.render) + + self.disableMouse() + self.camLens.setFov(40) + self.camLens.setNearFar(0.03, 2000.0) + self.camera.set_pos(0, -3.5, 0) + self.camera.look_at(0, -2.5, 0) + + self.render2d.hide() + self.aspect2d.hide() + + light = PointLight() + light.pos = 10, -10, 10 + light.radius = 1e20 + light.color = (1, 1, 1) + light.inner_radius = 4.0 + light.energy = 3 + self.render_pipeline.add_light(light) + + light = PointLight() + light.pos = -10, -10, 10 + light.radius = 1e20 + light.color = (1, 1, 1) + light.inner_radius = 4.0 + light.energy = 3 + self.render_pipeline.add_light(light) + + for mat in sphere.find_all_materials(): + mat.roughness = material.roughness + mat.base_color = Vec4(*(list(material.basecolor) + [1])) + mat.refractive_index = material.ior + + mat.metallic = 1.0 if material.mat_type == "metallic" else 0.0 + + if material.mat_type == "clearcoat": + mat.emission = (2, 0, 0, 0) + mat.metallic = 1.0 + mat.refractive_index = 1.51 + + if material.mat_type == "foliage": + mat.emission = (5, 0, 0, 0) + mat.metallic = 0.0 + mat.refractive_index = 1.51 + + for i in range(10): + self.taskMgr.step() + + self.win.save_screenshot("scene-rp.png") + + self.accept("r", self.reload) + + def reload(self): + print("Reloading") + self.render_pipeline.reload_shaders() + + for i in range(4): + self.taskMgr.step() + + self.win.save_screenshot("scene-rp.png") + +if len(sys.argv) <= 1: + Application().run() +else: + Application() diff --git a/RenderPipelineFile/toolkit/plugin_configurator/main.py b/RenderPipelineFile/toolkit/plugin_configurator/main.py index ba0ff055..2fe4c272 100644 --- a/RenderPipelineFile/toolkit/plugin_configurator/main.py +++ b/RenderPipelineFile/toolkit/plugin_configurator/main.py @@ -1,596 +1,596 @@ -#!/usr/bin/env python - -import os -import sys -import time -from threading import Thread -from functools import partial - -# Change to the current directory -os.chdir(os.path.join(os.path.dirname(os.path.realpath(__file__)))) - -# Append the current directory to the path -sys.path.insert(0, os.getcwd()) - -# Add the render pipeline to the path -sys.path.insert(0, "../../") - -from rplibs.six import iteritems # noqa -from rplibs.pyqt_imports import * #noqa - -# Load the generated UI Layout -from ui.main_window_generated import Ui_MainWindow # noqa - -# 创建一个简单的UniversalMessageDialog类,避免复杂的导入问题 -class UniversalMessageDialog(QDialog): - @staticmethod - def show_success(parent, title, message, show_cancel=True, confirm_text="确认"): - msg_box = QMessageBox(QMessageBox.Information, title, message, QMessageBox.Ok, parent) - if show_cancel: - msg_box.addButton(QMessageBox.Cancel) - msg_box.button(QMessageBox.Ok).setText(confirm_text) - return msg_box.exec_() - - @staticmethod - def show_warning(parent, title, message, show_cancel=True, confirm_text="确认"): - msg_box = QMessageBox(QMessageBox.Warning, title, message, QMessageBox.Ok, parent) - if show_cancel: - msg_box.addButton(QMessageBox.Cancel) - msg_box.button(QMessageBox.Ok).setText(confirm_text) - return msg_box.exec_() - - @staticmethod - def show_error(parent, title, message, show_cancel=True, confirm_text="确认"): - msg_box = QMessageBox(QMessageBox.Critical, title, message, QMessageBox.Ok, parent) - if show_cancel: - msg_box.addButton(QMessageBox.Cancel) - msg_box.button(QMessageBox.Ok).setText(confirm_text) - return msg_box.exec_() - - @staticmethod - def show_info(parent, title, message, show_cancel=True, confirm_text="确认"): - msg_box = QMessageBox(QMessageBox.Information, title, message, QMessageBox.Ok, parent) - if show_cancel: - msg_box.addButton(QMessageBox.Cancel) - msg_box.button(QMessageBox.Ok).setText(confirm_text) - return msg_box.exec_() - -from rpcore.pluginbase.manager import PluginManager # noqa -from rpcore.util.network_communication import NetworkCommunication # noqa -from rpcore.mount_manager import MountManager # noqa - - -class PluginConfigurator(QMainWindow, Ui_MainWindow): - - """ Interface to change the plugin settings """ - - def __init__(self): - - - # Init mounts - self._mount_mgr = MountManager(None) - self._mount_mgr.mount() - - self._plugin_mgr = PluginManager(None) - self._plugin_mgr.requires_daytime_settings = False - - QMainWindow.__init__(self) - Ui_MainWindow.__init__(self) - self.setupUi(self) - - - - self._current_plugin = None - self._current_plugin_instance = None - self.lbl_restart_pipeline.hide() - self._set_settings_visible(False) - self._update_queue = list() - - qt_connect(self.lst_plugins, "itemSelectionChanged()", - self.on_plugin_selected) - qt_connect(self.lst_plugins, "itemChanged(QListWidgetItem*)", - self.on_plugin_state_changed) - qt_connect(self.btn_reset_plugin_settings, "clicked()", - self.on_reset_plugin_settings) - - self._load_plugin_list() - - # Adjust column widths - self.table_plugin_settings.setColumnWidth(0, 140) - self.table_plugin_settings.setColumnWidth(1, 105) - self.table_plugin_settings.setColumnWidth(2, 160) - - update_thread = Thread(target=self.update_thread, args=()) - update_thread.start() - - # 启动通信文件监听线程 - comm_thread = Thread(target=self.communication_thread, args=()) - comm_thread.daemon = True # 设置为守护线 程,主程序退出时自动结束 - comm_thread.start() - - def closeEvent(self, event): # noqa - event.accept() - # 通知主程序配置可能已更改 - try: - # 发送一个信号或者写入一个标记文件来通知主程序 - marker_file = "/tmp/rp_plugin_config_changed.flag" - with open(marker_file, "w") as f: - f.write("Plugin configuration may have changed") - except Exception as e: - print(f"无法创建配置更改标记文件: {e}") - - import os - os._exit(1) - - def on_reset_plugin_settings(self): - """ Gets called when the user wants to reset settings of a plugin """ - - # Ask the user if he's really sure about it - msg = "Are you sure you want to reset the settings of '" - msg += self._current_plugin_instance.name + "'?\n" - msg += "This does not reset the Time of Day settings of this plugin.\n\n" - msg += "!! This cannot be undone !! They will be lost forever (a long time!)." - reply = QMessageBox.question( - self, "Warning", msg, QMessageBox.Yes, QMessageBox.No) - if reply == QMessageBox.Yes: - - UniversalMessageDialog.show_success( - self, - "Success", - "Settings have been reset! You may have to restart the pipeline.", - show_cancel=False, - confirm_text="OK" - ) - self._plugin_mgr.reset_plugin_settings(self._current_plugin) - - # Save config - self._plugin_mgr.save_overrides("/$$rpconfig/plugins.yaml") - - # Always show the restart hint, even if its not always required - self._show_restart_hint() - - # Re-render everything - self._load_plugin_list() - - def on_plugin_state_changed(self, item): - """ Handler when a plugin got activated/deactivated """ - plugin_id = item._plugin_id - state = item.checkState() == Qt.Checked - self._plugin_mgr.set_plugin_enabled(plugin_id, state) - self._rewrite_plugin_config() - self._show_restart_hint() - - def on_plugin_selected(self): - """ Gets called when a plugin got selected in the plugin list """ - selected_item = self.lst_plugins.selectedItems() - if not selected_item: - self._current_plugin = None - self._current_plugin_instance = None - self._set_settings_visible(False) - return - assert len(selected_item) == 1 - selected_item = selected_item[0] - self._current_plugin = selected_item._plugin_id - self._current_plugin_instance = self._plugin_mgr.instances[self._current_plugin] - assert(self._current_plugin_instance is not None) - self._render_current_plugin() - self._set_settings_visible(True) - - def update_thread(self): - """ Internal update thread """ - while True: - if len(self._update_queue) > 0: - item = self._update_queue.pop(-1) - NetworkCommunication.send_async(NetworkCommunication.CONFIG_PORT, item) - - if item.startswith("setval "): - setting_id = item.split()[1] - for entry in list(self._update_queue): - if entry.split()[1] == setting_id: - self._update_queue.remove(entry) - time.sleep(0.2) - - def _rewrite_plugin_config(self): - """ Rewrites the plugin configuration """ - self._plugin_mgr.save_overrides("/$$rpconfig/plugins.yaml") - - def _render_current_plugin(self): - """ Displays the currently selected plugin """ - self.lbl_plugin_name.setText(self._current_plugin_instance.name.upper() + " [" + self._current_plugin_instance.plugin_id + "]") - - version_str = "Version " + self._current_plugin_instance.version - version_str += " by " + self._current_plugin_instance.author - - self.lbl_plugin_version.setText(version_str) - self.lbl_plugin_desc.setText(self._current_plugin_instance.description) - - self._render_current_settings() - - def _show_restart_hint(self): - """ Shows a hint to restart the pipeline """ - self.lbl_restart_pipeline.show() - - def _render_current_settings(self): - """ Renders the current plugin settings """ - settings = self._plugin_mgr.settings[self._current_plugin] - - # remove all rows - while self.table_plugin_settings.rowCount() > 0: - self.table_plugin_settings.removeRow(0) - - label_font = QFont() - label_font.setPointSize(10) - label_font.setFamily("Roboto") - - desc_font = QFont() - desc_font.setPointSize(8) - desc_font.setFamily("Roboto") - - for index, (name, handle) in enumerate(iteritems(settings)): - if not handle.should_be_visible(settings): - continue - - row_index = self.table_plugin_settings.rowCount() - - # Increase row count - self.table_plugin_settings.insertRow(self.table_plugin_settings.rowCount()) - - label = QLabel() - label.setText(handle.label) - label.setWordWrap(True) - label.setFont(label_font) - - if not (handle.shader_runtime or handle.runtime ): - label.setStyleSheet("color: #999;") - - if handle.display_conditions: - label.setStyleSheet(label.styleSheet() + "padding-left: 10px;") - - label.setMargin(10) - - self.table_plugin_settings.setCellWidget(row_index, 0, label) - - item_default = QTableWidgetItem() - item_default.setText(str(handle.default)) - item_default.setTextAlignment(Qt.AlignCenter) - self.table_plugin_settings.setItem(row_index, 1, item_default) - - setting_widget = self._get_widget_for_setting(name, handle) - self.table_plugin_settings.setCellWidget(row_index, 2, setting_widget) - - label_desc = QLabel() - label_desc.setText(handle.description) - label_desc.setWordWrap(True) - label_desc.setFont(desc_font) - label_desc.setStyleSheet("color: #555;padding: 5px;") - - self.table_plugin_settings.setCellWidget(row_index, 3, label_desc) - - def _do_update_setting(self, setting_id, value): - """ Updates a setting of the current plugin """ - - # Check whether the setting is a runtime setting - setting_handle = self._plugin_mgr.get_setting_handle( - self._current_plugin, setting_id) - - # Skip the setting in case the value is equal - if setting_handle.value == value: - return - - # Otherwise set the new value - setting_handle.set_value(value) - self._rewrite_plugin_config() - - if not setting_handle.runtime and not setting_handle.shader_runtime: - self._show_restart_hint() - else: - # In case the setting is dynamic, notice the pipeline about it: - # print("Sending reload packet ...") - self._update_queue.append("setval {}.{} {}".format( - self._current_plugin, setting_id, value)) - - # Update GUI, but only in case of enum and bool values, since they can trigger - # display conditions: - if setting_handle.type == "enum" or setting_handle.type == "bool": - self._render_current_settings() - - def _on_setting_bool_changed(self, setting_id, value): - self._do_update_setting(setting_id, value == Qt.Checked) - - def _on_setting_scalar_changed(self, setting_id, value): - self._do_update_setting(setting_id, value) - - def _on_setting_enum_changed(self, setting_id, value): - self._do_update_setting(setting_id, value) - - def _on_setting_power_of_two_changed(self, setting_id, value): - self._do_update_setting(setting_id, value) - - def _on_setting_slider_changed(self, setting_id, setting_type, bound_objs, value): - if setting_type == "float": - value /= 100000.0 - self._do_update_setting(setting_id, value) - - for obj in bound_objs: - obj.setValue(value) - - def _on_setting_spinbox_changed(self, setting_id, setting_type, bound_objs, value): - self._do_update_setting(setting_id, value) - # Assume objects are sliders, so we need to rescale the value - for obj in bound_objs: - obj.setValue(value * 100000.0 if setting_type == "float" else value) - - def _choose_path(self, setting_id, setting_handle, bound_objs): - """ Shows a file chooser to show an path from """ - - this_dir = os.path.dirname(os.path.realpath(__file__)) - plugin_dir = os.path.join(this_dir, "../../rpplugins/" + self._current_plugin, "resources") - plugin_dir = os.path.abspath(plugin_dir) - search_dir = os.path.join(plugin_dir, setting_handle.base_path) - - print("Plugin dir =", plugin_dir) - print("Search dir =", search_dir) - - current_file = setting_handle.value.replace("\\", "/").split("/")[-1] - print("Current file =", current_file) - file_dlg = QFileDialog(self, "Choose File ..", search_dir, setting_handle.file_type) - file_dlg.selectFile(current_file) - # file_dlg.setViewMode(QFileDialog.Detail) - - if file_dlg.exec_(): - filename = file_dlg.selectedFiles() - filename = filename[-1] - print("QT selected files returned:", filename) - - filename = os.path.relpath(str(filename), plugin_dir) - filename = filename.replace("\\", "/") - print("Relative path is", filename) - self._do_update_setting(setting_id, filename) - - display_file = filename.split("/")[-1] - for obj in bound_objs: - obj.setText(display_file) - - def _get_widget_for_setting(self, setting_id, setting): - """ Returns an appropriate widget to control the given setting """ - - widget = QWidget() - layout = QHBoxLayout() - layout.setAlignment(Qt.AlignCenter) - widget.setLayout(layout) - - - if setting.type == "bool": - box = QCheckBox() - box.setChecked(Qt.Checked if setting.value else Qt.Unchecked) - qt_connect(box, "stateChanged(int)", - partial(self._on_setting_bool_changed, setting_id)) - layout.addWidget(box) - - elif setting.type == "float" or setting.type == "int": - - if setting.type == "float": - box = QDoubleSpinBox() - - if setting.maxval - setting.minval <= 2.0: - box.setDecimals(4) - else: - box = QSpinBox() - box.setMinimum(setting.minval) - box.setMaximum(setting.maxval) - box.setValue(setting.value) - - box.setAlignment(Qt.AlignCenter) - - slider = QSlider() - slider.setOrientation(Qt.Horizontal) - - if setting.type == "float": - box.setSingleStep(abs(setting.maxval - setting.minval) / 100.0) - slider.setMinimum(int(setting.minval * 100000.0)) - slider.setMaximum(int(setting.maxval * 100000.0)) - slider.setValue(int(setting.value * 100000.0)) - elif setting.type == "int": - box.setSingleStep(int(max(1, (setting.maxval - setting.minval) / 32))) - slider.setMinimum(int(setting.minval)) - slider.setMaximum(int(setting.maxval)) - slider.setValue(int(setting.value)) - - layout.addWidget(box) - layout.addWidget(slider) - - qt_connect(slider, "valueChanged(int)", - partial(self._on_setting_slider_changed, setting_id, setting.type, [box])) - - value_type = "int" if setting.type == "int" else "double" - - qt_connect(box, "valueChanged(" + value_type + ")", - partial(self._on_setting_spinbox_changed, setting_id, setting.type, [slider])) - - elif setting.type == "enum": - box = QComboBox() - for value in setting.values: - box.addItem(value) - qt_connect(box, "currentIndexChanged(QString)", - partial(self._on_setting_enum_changed, setting_id)) - box.setCurrentIndex(setting.values.index(setting.value)) - box.setMinimumWidth(145) - - layout.addWidget(box) - - elif setting.type == "power_of_two": - - box = QComboBox() - resolutions = [str(2**i) for i in range(1, 32) if 2**i >= setting.minval and 2**i <= setting.maxval] - for value in resolutions: - box.addItem(value) - qt_connect(box, "currentIndexChanged(QString)", - partial(self._on_setting_power_of_two_changed, setting_id)) - box.setCurrentIndex(resolutions.index(str(setting.value))) - box.setMinimumWidth(145) - layout.addWidget(box) - - elif setting.type == "sample_sequence": - - box = QComboBox() - for value in setting.sequences: - box.addItem(value) - qt_connect(box, "currentIndexChanged(QString)", - partial(self._on_setting_enum_changed, setting_id)) - box.setCurrentIndex(setting.sequences.index(str(setting.value))) - box.setMinimumWidth(145) - layout.addWidget(box) - - elif setting.type == "path": - - label = QLabel() - display_file = setting.value.replace("\\", "/").split("/")[-1] - - desc_font = QFont() - desc_font.setPointSize(7) - desc_font.setFamily("Roboto") - - label.setText(display_file) - label.setFont(desc_font) - - button = QPushButton() - button.setText("Choose File ...") - qt_connect(button, "clicked()", partial( - self._choose_path, setting_id, setting, (label,))) - - layout.addWidget(label) - layout.addWidget(button) - - else: - print("ERROR: Unkown setting type:", setting.type) - - return widget - - def _set_settings_visible(self, flag): - """ Sets whether the settings panel is visible or not """ - if flag: - self.frame_details.show() - else: - self.frame_details.hide() - - def _load_plugin_list(self): - """ Reloads the whole plugin list """ - print("Loading plugin list") - - # Reset selection - self._current_plugin = None - self._current_plugin_instance = None - self._set_settings_visible(False) - - # Plugins are all plugins in the plugins directory - self._plugin_mgr.unload() - self._plugin_mgr.load() - - self.lst_plugins.clear() - plugins = sorted(iteritems(self._plugin_mgr.instances), key=lambda plg: plg[1].name) - - - item_font = QFont() - item_font.setBold(False) - item_font.setPointSize(10) - - for plugin_id, instance in plugins: - - item = QListWidgetItem() - item.setText(" " + instance.name) - item.setFont(item_font) - - if self._plugin_mgr.is_plugin_enabled(plugin_id): - item.setCheckState(Qt.Checked) - else: - item.setCheckState(Qt.Unchecked) - - item._plugin_id = plugin_id - self.lst_plugins.addItem(item) - - self.lst_plugins.setCurrentRow(0) - - def communication_thread(self): - """监听来自主程序的通信文件""" - import json - import tempfile - - comm_dir = os.path.join(tempfile.gettempdir(), "daytime_editor_comm") - comm_file = os.path.join(comm_dir, "sun_azimuth_command.json") - - print("🔄 Day Time Editor通信监听已启动") - print(f" 监听文件: {comm_file}") - - last_timestamp = 0 - - while True: - try: - if os.path.exists(comm_file): - with open(comm_file, 'r') as f: - command = json.load(f) - - # 检查是否是新命令(避免重复执行) - if command.get('timestamp', 0) > last_timestamp: - last_timestamp = command.get('timestamp', 0) - - if command.get('command') == 'set_sun_azimuth': - azimuth_value = command.get('value', 0) - print(f"📨 收到Sun Azimuth命令: {azimuth_value}°") - - # 在主线程中执行UI更新 - QTimer.singleShot(0, partial(self.apply_sun_azimuth, azimuth_value)) - - time.sleep(0.1) # 100ms检查一次 - - except Exception as e: - print(f"⚠️ 通信监听错误: {e}") - time.sleep(1) # 出错时等待1秒再重试 - - def apply_sun_azimuth(self, azimuth_degrees): - """应用Sun Azimuth值到Day Time Editor""" - try: - print(f"🌞 正在应用Sun Azimuth: {azimuth_degrees}°") - - # 查找Sun Azimuth相关的控件 - # 这里需要根据实际的UI结构来实现 - # 先尝试找到可能的控件 - - # 方法1:查找包含"azimuth"的控件 - for widget_name in dir(self): - widget = getattr(self, widget_name, None) - if widget and hasattr(widget, 'setValue'): - if 'azimuth' in widget_name.lower() or 'sun' in widget_name.lower(): - print(f" 尝试设置控件: {widget_name}") - try: - widget.setValue(azimuth_degrees) - print(f"✅ 成功设置 {widget_name} = {azimuth_degrees}°") - return - except Exception as e: - print(f" 设置失败: {e}") - - # 方法2:查找表格中的Sun Azimuth设置 - if hasattr(self, 'table_plugin_settings'): - table = self.table_plugin_settings - for row in range(table.rowCount()): - item = table.item(row, 0) # 第一列通常是设置名称 - if item and 'azimuth' in item.text().lower(): - # 找到Sun Azimuth行,设置第二列的值 - value_item = table.item(row, 1) - if value_item: - value_item.setText(str(azimuth_degrees)) - print(f"✅ 在表格中设置Sun Azimuth = {azimuth_degrees}°") - return - - print("⚠️ 未找到Sun Azimuth控件,请检查UI结构") - - except Exception as e: - print(f"❌ 应用Sun Azimuth失败: {e}") - import traceback - traceback.print_exc() - -# Start application -app = QApplication(sys.argv) -qt_register_fonts() -configurator = PluginConfigurator() -configurator.show() -app.exec_() +#!/usr/bin/env python + +import os +import sys +import time +from threading import Thread +from functools import partial + +# Change to the current directory +os.chdir(os.path.join(os.path.dirname(os.path.realpath(__file__)))) + +# Append the current directory to the path +sys.path.insert(0, os.getcwd()) + +# Add the render pipeline to the path +sys.path.insert(0, "../../") + +from rplibs.six import iteritems # noqa +from rplibs.pyqt_imports import * #noqa + +# Load the generated UI Layout +from ui.main_window_generated import Ui_MainWindow # noqa + +# 创建一个简单的UniversalMessageDialog类,避免复杂的导入问题 +class UniversalMessageDialog(QDialog): + @staticmethod + def show_success(parent, title, message, show_cancel=True, confirm_text="确认"): + msg_box = QMessageBox(QMessageBox.Information, title, message, QMessageBox.Ok, parent) + if show_cancel: + msg_box.addButton(QMessageBox.Cancel) + msg_box.button(QMessageBox.Ok).setText(confirm_text) + return msg_box.exec_() + + @staticmethod + def show_warning(parent, title, message, show_cancel=True, confirm_text="确认"): + msg_box = QMessageBox(QMessageBox.Warning, title, message, QMessageBox.Ok, parent) + if show_cancel: + msg_box.addButton(QMessageBox.Cancel) + msg_box.button(QMessageBox.Ok).setText(confirm_text) + return msg_box.exec_() + + @staticmethod + def show_error(parent, title, message, show_cancel=True, confirm_text="确认"): + msg_box = QMessageBox(QMessageBox.Critical, title, message, QMessageBox.Ok, parent) + if show_cancel: + msg_box.addButton(QMessageBox.Cancel) + msg_box.button(QMessageBox.Ok).setText(confirm_text) + return msg_box.exec_() + + @staticmethod + def show_info(parent, title, message, show_cancel=True, confirm_text="确认"): + msg_box = QMessageBox(QMessageBox.Information, title, message, QMessageBox.Ok, parent) + if show_cancel: + msg_box.addButton(QMessageBox.Cancel) + msg_box.button(QMessageBox.Ok).setText(confirm_text) + return msg_box.exec_() + +from rpcore.pluginbase.manager import PluginManager # noqa +from rpcore.util.network_communication import NetworkCommunication # noqa +from rpcore.mount_manager import MountManager # noqa + + +class PluginConfigurator(QMainWindow, Ui_MainWindow): + + """ Interface to change the plugin settings """ + + def __init__(self): + + + # Init mounts + self._mount_mgr = MountManager(None) + self._mount_mgr.mount() + + self._plugin_mgr = PluginManager(None) + self._plugin_mgr.requires_daytime_settings = False + + QMainWindow.__init__(self) + Ui_MainWindow.__init__(self) + self.setupUi(self) + + + + self._current_plugin = None + self._current_plugin_instance = None + self.lbl_restart_pipeline.hide() + self._set_settings_visible(False) + self._update_queue = list() + + qt_connect(self.lst_plugins, "itemSelectionChanged()", + self.on_plugin_selected) + qt_connect(self.lst_plugins, "itemChanged(QListWidgetItem*)", + self.on_plugin_state_changed) + qt_connect(self.btn_reset_plugin_settings, "clicked()", + self.on_reset_plugin_settings) + + self._load_plugin_list() + + # Adjust column widths + self.table_plugin_settings.setColumnWidth(0, 140) + self.table_plugin_settings.setColumnWidth(1, 105) + self.table_plugin_settings.setColumnWidth(2, 160) + + update_thread = Thread(target=self.update_thread, args=()) + update_thread.start() + + # 启动通信文件监听线程 + comm_thread = Thread(target=self.communication_thread, args=()) + comm_thread.daemon = True # 设置为守护线 程,主程序退出时自动结束 + comm_thread.start() + + def closeEvent(self, event): # noqa + event.accept() + # 通知主程序配置可能已更改 + try: + # 发送一个信号或者写入一个标记文件来通知主程序 + marker_file = "/tmp/rp_plugin_config_changed.flag" + with open(marker_file, "w") as f: + f.write("Plugin configuration may have changed") + except Exception as e: + print(f"无法创建配置更改标记文件: {e}") + + import os + os._exit(1) + + def on_reset_plugin_settings(self): + """ Gets called when the user wants to reset settings of a plugin """ + + # Ask the user if he's really sure about it + msg = "Are you sure you want to reset the settings of '" + msg += self._current_plugin_instance.name + "'?\n" + msg += "This does not reset the Time of Day settings of this plugin.\n\n" + msg += "!! This cannot be undone !! They will be lost forever (a long time!)." + reply = QMessageBox.question( + self, "Warning", msg, QMessageBox.Yes, QMessageBox.No) + if reply == QMessageBox.Yes: + + UniversalMessageDialog.show_success( + self, + "Success", + "Settings have been reset! You may have to restart the pipeline.", + show_cancel=False, + confirm_text="OK" + ) + self._plugin_mgr.reset_plugin_settings(self._current_plugin) + + # Save config + self._plugin_mgr.save_overrides("/$$rpconfig/plugins.yaml") + + # Always show the restart hint, even if its not always required + self._show_restart_hint() + + # Re-render everything + self._load_plugin_list() + + def on_plugin_state_changed(self, item): + """ Handler when a plugin got activated/deactivated """ + plugin_id = item._plugin_id + state = item.checkState() == Qt.Checked + self._plugin_mgr.set_plugin_enabled(plugin_id, state) + self._rewrite_plugin_config() + self._show_restart_hint() + + def on_plugin_selected(self): + """ Gets called when a plugin got selected in the plugin list """ + selected_item = self.lst_plugins.selectedItems() + if not selected_item: + self._current_plugin = None + self._current_plugin_instance = None + self._set_settings_visible(False) + return + assert len(selected_item) == 1 + selected_item = selected_item[0] + self._current_plugin = selected_item._plugin_id + self._current_plugin_instance = self._plugin_mgr.instances[self._current_plugin] + assert(self._current_plugin_instance is not None) + self._render_current_plugin() + self._set_settings_visible(True) + + def update_thread(self): + """ Internal update thread """ + while True: + if len(self._update_queue) > 0: + item = self._update_queue.pop(-1) + NetworkCommunication.send_async(NetworkCommunication.CONFIG_PORT, item) + + if item.startswith("setval "): + setting_id = item.split()[1] + for entry in list(self._update_queue): + if entry.split()[1] == setting_id: + self._update_queue.remove(entry) + time.sleep(0.2) + + def _rewrite_plugin_config(self): + """ Rewrites the plugin configuration """ + self._plugin_mgr.save_overrides("/$$rpconfig/plugins.yaml") + + def _render_current_plugin(self): + """ Displays the currently selected plugin """ + self.lbl_plugin_name.setText(self._current_plugin_instance.name.upper() + " [" + self._current_plugin_instance.plugin_id + "]") + + version_str = "Version " + self._current_plugin_instance.version + version_str += " by " + self._current_plugin_instance.author + + self.lbl_plugin_version.setText(version_str) + self.lbl_plugin_desc.setText(self._current_plugin_instance.description) + + self._render_current_settings() + + def _show_restart_hint(self): + """ Shows a hint to restart the pipeline """ + self.lbl_restart_pipeline.show() + + def _render_current_settings(self): + """ Renders the current plugin settings """ + settings = self._plugin_mgr.settings[self._current_plugin] + + # remove all rows + while self.table_plugin_settings.rowCount() > 0: + self.table_plugin_settings.removeRow(0) + + label_font = QFont() + label_font.setPointSize(10) + label_font.setFamily("Roboto") + + desc_font = QFont() + desc_font.setPointSize(8) + desc_font.setFamily("Roboto") + + for index, (name, handle) in enumerate(iteritems(settings)): + if not handle.should_be_visible(settings): + continue + + row_index = self.table_plugin_settings.rowCount() + + # Increase row count + self.table_plugin_settings.insertRow(self.table_plugin_settings.rowCount()) + + label = QLabel() + label.setText(handle.label) + label.setWordWrap(True) + label.setFont(label_font) + + if not (handle.shader_runtime or handle.runtime ): + label.setStyleSheet("color: #999;") + + if handle.display_conditions: + label.setStyleSheet(label.styleSheet() + "padding-left: 10px;") + + label.setMargin(10) + + self.table_plugin_settings.setCellWidget(row_index, 0, label) + + item_default = QTableWidgetItem() + item_default.setText(str(handle.default)) + item_default.setTextAlignment(Qt.AlignCenter) + self.table_plugin_settings.setItem(row_index, 1, item_default) + + setting_widget = self._get_widget_for_setting(name, handle) + self.table_plugin_settings.setCellWidget(row_index, 2, setting_widget) + + label_desc = QLabel() + label_desc.setText(handle.description) + label_desc.setWordWrap(True) + label_desc.setFont(desc_font) + label_desc.setStyleSheet("color: #555;padding: 5px;") + + self.table_plugin_settings.setCellWidget(row_index, 3, label_desc) + + def _do_update_setting(self, setting_id, value): + """ Updates a setting of the current plugin """ + + # Check whether the setting is a runtime setting + setting_handle = self._plugin_mgr.get_setting_handle( + self._current_plugin, setting_id) + + # Skip the setting in case the value is equal + if setting_handle.value == value: + return + + # Otherwise set the new value + setting_handle.set_value(value) + self._rewrite_plugin_config() + + if not setting_handle.runtime and not setting_handle.shader_runtime: + self._show_restart_hint() + else: + # In case the setting is dynamic, notice the pipeline about it: + # print("Sending reload packet ...") + self._update_queue.append("setval {}.{} {}".format( + self._current_plugin, setting_id, value)) + + # Update GUI, but only in case of enum and bool values, since they can trigger + # display conditions: + if setting_handle.type == "enum" or setting_handle.type == "bool": + self._render_current_settings() + + def _on_setting_bool_changed(self, setting_id, value): + self._do_update_setting(setting_id, value == Qt.Checked) + + def _on_setting_scalar_changed(self, setting_id, value): + self._do_update_setting(setting_id, value) + + def _on_setting_enum_changed(self, setting_id, value): + self._do_update_setting(setting_id, value) + + def _on_setting_power_of_two_changed(self, setting_id, value): + self._do_update_setting(setting_id, value) + + def _on_setting_slider_changed(self, setting_id, setting_type, bound_objs, value): + if setting_type == "float": + value /= 100000.0 + self._do_update_setting(setting_id, value) + + for obj in bound_objs: + obj.setValue(value) + + def _on_setting_spinbox_changed(self, setting_id, setting_type, bound_objs, value): + self._do_update_setting(setting_id, value) + # Assume objects are sliders, so we need to rescale the value + for obj in bound_objs: + obj.setValue(value * 100000.0 if setting_type == "float" else value) + + def _choose_path(self, setting_id, setting_handle, bound_objs): + """ Shows a file chooser to show an path from """ + + this_dir = os.path.dirname(os.path.realpath(__file__)) + plugin_dir = os.path.join(this_dir, "../../rpplugins/" + self._current_plugin, "resources") + plugin_dir = os.path.abspath(plugin_dir) + search_dir = os.path.join(plugin_dir, setting_handle.base_path) + + print("Plugin dir =", plugin_dir) + print("Search dir =", search_dir) + + current_file = setting_handle.value.replace("\\", "/").split("/")[-1] + print("Current file =", current_file) + file_dlg = QFileDialog(self, "Choose File ..", search_dir, setting_handle.file_type) + file_dlg.selectFile(current_file) + # file_dlg.setViewMode(QFileDialog.Detail) + + if file_dlg.exec_(): + filename = file_dlg.selectedFiles() + filename = filename[-1] + print("QT selected files returned:", filename) + + filename = os.path.relpath(str(filename), plugin_dir) + filename = filename.replace("\\", "/") + print("Relative path is", filename) + self._do_update_setting(setting_id, filename) + + display_file = filename.split("/")[-1] + for obj in bound_objs: + obj.setText(display_file) + + def _get_widget_for_setting(self, setting_id, setting): + """ Returns an appropriate widget to control the given setting """ + + widget = QWidget() + layout = QHBoxLayout() + layout.setAlignment(Qt.AlignCenter) + widget.setLayout(layout) + + + if setting.type == "bool": + box = QCheckBox() + box.setChecked(Qt.Checked if setting.value else Qt.Unchecked) + qt_connect(box, "stateChanged(int)", + partial(self._on_setting_bool_changed, setting_id)) + layout.addWidget(box) + + elif setting.type == "float" or setting.type == "int": + + if setting.type == "float": + box = QDoubleSpinBox() + + if setting.maxval - setting.minval <= 2.0: + box.setDecimals(4) + else: + box = QSpinBox() + box.setMinimum(setting.minval) + box.setMaximum(setting.maxval) + box.setValue(setting.value) + + box.setAlignment(Qt.AlignCenter) + + slider = QSlider() + slider.setOrientation(Qt.Horizontal) + + if setting.type == "float": + box.setSingleStep(abs(setting.maxval - setting.minval) / 100.0) + slider.setMinimum(int(setting.minval * 100000.0)) + slider.setMaximum(int(setting.maxval * 100000.0)) + slider.setValue(int(setting.value * 100000.0)) + elif setting.type == "int": + box.setSingleStep(int(max(1, (setting.maxval - setting.minval) / 32))) + slider.setMinimum(int(setting.minval)) + slider.setMaximum(int(setting.maxval)) + slider.setValue(int(setting.value)) + + layout.addWidget(box) + layout.addWidget(slider) + + qt_connect(slider, "valueChanged(int)", + partial(self._on_setting_slider_changed, setting_id, setting.type, [box])) + + value_type = "int" if setting.type == "int" else "double" + + qt_connect(box, "valueChanged(" + value_type + ")", + partial(self._on_setting_spinbox_changed, setting_id, setting.type, [slider])) + + elif setting.type == "enum": + box = QComboBox() + for value in setting.values: + box.addItem(value) + qt_connect(box, "currentIndexChanged(QString)", + partial(self._on_setting_enum_changed, setting_id)) + box.setCurrentIndex(setting.values.index(setting.value)) + box.setMinimumWidth(145) + + layout.addWidget(box) + + elif setting.type == "power_of_two": + + box = QComboBox() + resolutions = [str(2**i) for i in range(1, 32) if 2**i >= setting.minval and 2**i <= setting.maxval] + for value in resolutions: + box.addItem(value) + qt_connect(box, "currentIndexChanged(QString)", + partial(self._on_setting_power_of_two_changed, setting_id)) + box.setCurrentIndex(resolutions.index(str(setting.value))) + box.setMinimumWidth(145) + layout.addWidget(box) + + elif setting.type == "sample_sequence": + + box = QComboBox() + for value in setting.sequences: + box.addItem(value) + qt_connect(box, "currentIndexChanged(QString)", + partial(self._on_setting_enum_changed, setting_id)) + box.setCurrentIndex(setting.sequences.index(str(setting.value))) + box.setMinimumWidth(145) + layout.addWidget(box) + + elif setting.type == "path": + + label = QLabel() + display_file = setting.value.replace("\\", "/").split("/")[-1] + + desc_font = QFont() + desc_font.setPointSize(7) + desc_font.setFamily("Roboto") + + label.setText(display_file) + label.setFont(desc_font) + + button = QPushButton() + button.setText("Choose File ...") + qt_connect(button, "clicked()", partial( + self._choose_path, setting_id, setting, (label,))) + + layout.addWidget(label) + layout.addWidget(button) + + else: + print("ERROR: Unkown setting type:", setting.type) + + return widget + + def _set_settings_visible(self, flag): + """ Sets whether the settings panel is visible or not """ + if flag: + self.frame_details.show() + else: + self.frame_details.hide() + + def _load_plugin_list(self): + """ Reloads the whole plugin list """ + print("Loading plugin list") + + # Reset selection + self._current_plugin = None + self._current_plugin_instance = None + self._set_settings_visible(False) + + # Plugins are all plugins in the plugins directory + self._plugin_mgr.unload() + self._plugin_mgr.load() + + self.lst_plugins.clear() + plugins = sorted(iteritems(self._plugin_mgr.instances), key=lambda plg: plg[1].name) + + + item_font = QFont() + item_font.setBold(False) + item_font.setPointSize(10) + + for plugin_id, instance in plugins: + + item = QListWidgetItem() + item.setText(" " + instance.name) + item.setFont(item_font) + + if self._plugin_mgr.is_plugin_enabled(plugin_id): + item.setCheckState(Qt.Checked) + else: + item.setCheckState(Qt.Unchecked) + + item._plugin_id = plugin_id + self.lst_plugins.addItem(item) + + self.lst_plugins.setCurrentRow(0) + + def communication_thread(self): + """监听来自主程序的通信文件""" + import json + import tempfile + + comm_dir = os.path.join(tempfile.gettempdir(), "daytime_editor_comm") + comm_file = os.path.join(comm_dir, "sun_azimuth_command.json") + + print("🔄 Day Time Editor通信监听已启动") + print(f" 监听文件: {comm_file}") + + last_timestamp = 0 + + while True: + try: + if os.path.exists(comm_file): + with open(comm_file, 'r') as f: + command = json.load(f) + + # 检查是否是新命令(避免重复执行) + if command.get('timestamp', 0) > last_timestamp: + last_timestamp = command.get('timestamp', 0) + + if command.get('command') == 'set_sun_azimuth': + azimuth_value = command.get('value', 0) + print(f"📨 收到Sun Azimuth命令: {azimuth_value}°") + + # 在主线程中执行UI更新 + QTimer.singleShot(0, partial(self.apply_sun_azimuth, azimuth_value)) + + time.sleep(0.1) # 100ms检查一次 + + except Exception as e: + print(f"⚠️ 通信监听错误: {e}") + time.sleep(1) # 出错时等待1秒再重试 + + def apply_sun_azimuth(self, azimuth_degrees): + """应用Sun Azimuth值到Day Time Editor""" + try: + print(f"🌞 正在应用Sun Azimuth: {azimuth_degrees}°") + + # 查找Sun Azimuth相关的控件 + # 这里需要根据实际的UI结构来实现 + # 先尝试找到可能的控件 + + # 方法1:查找包含"azimuth"的控件 + for widget_name in dir(self): + widget = getattr(self, widget_name, None) + if widget and hasattr(widget, 'setValue'): + if 'azimuth' in widget_name.lower() or 'sun' in widget_name.lower(): + print(f" 尝试设置控件: {widget_name}") + try: + widget.setValue(azimuth_degrees) + print(f"✅ 成功设置 {widget_name} = {azimuth_degrees}°") + return + except Exception as e: + print(f" 设置失败: {e}") + + # 方法2:查找表格中的Sun Azimuth设置 + if hasattr(self, 'table_plugin_settings'): + table = self.table_plugin_settings + for row in range(table.rowCount()): + item = table.item(row, 0) # 第一列通常是设置名称 + if item and 'azimuth' in item.text().lower(): + # 找到Sun Azimuth行,设置第二列的值 + value_item = table.item(row, 1) + if value_item: + value_item.setText(str(azimuth_degrees)) + print(f"✅ 在表格中设置Sun Azimuth = {azimuth_degrees}°") + return + + print("⚠️ 未找到Sun Azimuth控件,请检查UI结构") + + except Exception as e: + print(f"❌ 应用Sun Azimuth失败: {e}") + import traceback + traceback.print_exc() + +# Start application +app = QApplication(sys.argv) +qt_register_fonts() +configurator = PluginConfigurator() +configurator.show() +app.exec_() diff --git a/RenderPipelineFile/toolkit/plugin_configurator/ui/compile_ui_qt4.bat b/RenderPipelineFile/toolkit/plugin_configurator/ui/compile_ui_qt4.bat index 2392f6a6..dca9a251 100644 --- a/RenderPipelineFile/toolkit/plugin_configurator/ui/compile_ui_qt4.bat +++ b/RenderPipelineFile/toolkit/plugin_configurator/ui/compile_ui_qt4.bat @@ -1,9 +1,9 @@ -@echo off - -rem Adjust the path to match your PyQt4 installation -SET PYQTPATH="C:\Projekte\Panda3D\built_x64\python\Lib\site-packages\PyQt4" - -python -m PyQt4.uic.pyuic --from-imports main_window.ui -o main_window_generated.py -"%PYQTPATH%\pyrcc4.exe" -py3 resources.qrc -o resources_rc.py - -pause +@echo off + +rem Adjust the path to match your PyQt4 installation +SET PYQTPATH="C:\Projekte\Panda3D\built_x64\python\Lib\site-packages\PyQt4" + +python -m PyQt4.uic.pyuic --from-imports main_window.ui -o main_window_generated.py +"%PYQTPATH%\pyrcc4.exe" -py3 resources.qrc -o resources_rc.py + +pause diff --git a/RenderPipelineFile/toolkit/plugin_configurator/ui/compile_ui_qt5.bat b/RenderPipelineFile/toolkit/plugin_configurator/ui/compile_ui_qt5.bat index 202b5149..28cd9ee3 100644 --- a/RenderPipelineFile/toolkit/plugin_configurator/ui/compile_ui_qt5.bat +++ b/RenderPipelineFile/toolkit/plugin_configurator/ui/compile_ui_qt5.bat @@ -1,6 +1,6 @@ -@echo off - -python -m PyQt5.uic.pyuic --from-imports main_window.ui -o main_window_generated.py -python -m PyQt5.pyrcc_main resources.qrc -o resources_rc.py - -pause +@echo off + +python -m PyQt5.uic.pyuic --from-imports main_window.ui -o main_window_generated.py +python -m PyQt5.pyrcc_main resources.qrc -o resources_rc.py + +pause diff --git a/RenderPipelineFile/toolkit/plugin_configurator/ui/main_window.ui b/RenderPipelineFile/toolkit/plugin_configurator/ui/main_window.ui index baec7a1b..1358ab1d 100644 --- a/RenderPipelineFile/toolkit/plugin_configurator/ui/main_window.ui +++ b/RenderPipelineFile/toolkit/plugin_configurator/ui/main_window.ui @@ -1,1845 +1,1845 @@ - - - MainWindow - - - - 0 - 0 - 1000 - 680 - - - - - 0 - 0 - - - - - 1000 - 680 - - - - - 1000 - 680 - - - - Plugin Configurator - - - - :/icon/res/icon.png:/icon/res/icon.png - - - QMainWindow { background: #fff;} -*, QLabel { font-family: Roboto; font-weight: 300; } - -QScrollBar { - background: #eee; -} - - -QScrollBar:vertical { - width: 9px; - margin: 0; - } - - QScrollBar::handle:vertical { - min-height: 15px; - background: #aaa; - - } - - - QScrollBar::handle:vertical:hover { - - background: #999; - - } - QScrollBar::add-line:vertical { - } - - QScrollBar::sub-line:vertical { - } - - - QScrollBar::add-page:vertical { - background: #ddd; - - } - QScrollBar::sub-page:vertical { - background: #ddd; - } - - - - - - - 20 - 80 - 251 - 531 - - - - QListWidget::item { -padding: 8px 7px 5px; -background: #ddd; -outline: 0 !important; -margin-bottom: 1px; -margin-right: 10px; -color: #777; -font-weight: 500; -border: 0; -border-radius: 0; -} - -QListWidget::item:hover { -background: #ccc; -} - -QListWidget::item:selected { -background: #555; -color: #eee; -} - -QListWidget { -padding: 0; -color: #eee; -background: transparent; -/* -background: #222; -padding: 5px 8px; -border: 1px solid #444; -*/ -} - -* { -outline: 0; -} - - - - - QFrame::NoFrame - - - 0 - - - - Ambient Occlusion - - - - - Another Ite - - - - - Bloom - - - - - Volumetric Clouds - - - - - Depth of Field - - - - - Environment Probes - - - - - FXAA (Antialiasing) - - - - - SKAA - - - - - Motion Blur - - - - - PSSM Shadows - - - - - Atmospheric Scattering - - - - - Skin Shading - - - - - SMAA (Antialiasing) - - - - - Screen Space Reflections - - - - - Volumetric Lighting - - - - - VXGI - - - - - Item 346 - - - - - Other Item - - - - - Other Item to force scrollbars - - - - - - - 281 - 60 - 721 - 621 - - - - - - - - - 255 - 255 - 255 - - - - - - - 229 - 229 - 229 - - - - - - - 63 - 63 - 63 - - - - - - - 52 - 52 - 52 - - - - - - - 21 - 21 - 21 - - - - - - - 28 - 28 - 28 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 229 - 229 - 229 - - - - - - - 229 - 229 - 229 - - - - - - - 0 - 0 - 0 - - - - - - - 21 - 21 - 21 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - - - 255 - 255 - 255 - - - - - - - 229 - 229 - 229 - - - - - - - 63 - 63 - 63 - - - - - - - 52 - 52 - 52 - - - - - - - 21 - 21 - 21 - - - - - - - 28 - 28 - 28 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 229 - 229 - 229 - - - - - - - 229 - 229 - 229 - - - - - - - 0 - 0 - 0 - - - - - - - 21 - 21 - 21 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - - - 21 - 21 - 21 - - - - - - - 229 - 229 - 229 - - - - - - - 63 - 63 - 63 - - - - - - - 52 - 52 - 52 - - - - - - - 21 - 21 - 21 - - - - - - - 28 - 28 - 28 - - - - - - - 21 - 21 - 21 - - - - - - - 255 - 255 - 255 - - - - - - - 21 - 21 - 21 - - - - - - - 229 - 229 - 229 - - - - - - - 229 - 229 - 229 - - - - - - - 0 - 0 - 0 - - - - - - - 42 - 42 - 42 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - - false - - - #frame_details { background: #e5e5e5; } - - - QFrame::NoFrame - - - QFrame::Plain - - - 1 - - - - - 20 - 40 - 391 - 21 - - - - - - - - - 255 - 151 - 25 - - - - - - - 236 - 236 - 236 - - - - - - - 255 - 151 - 25 - - - - - - - 255 - 151 - 25 - - - - - - - 236 - 236 - 236 - - - - - - - 236 - 236 - 236 - - - - - - - - - 255 - 151 - 25 - - - - - - - 236 - 236 - 236 - - - - - - - 255 - 151 - 25 - - - - - - - 255 - 151 - 25 - - - - - - - 236 - 236 - 236 - - - - - - - 236 - 236 - 236 - - - - - - - - - 255 - 151 - 25 - - - - - - - 236 - 236 - 236 - - - - - - - 255 - 151 - 25 - - - - - - - 255 - 151 - 25 - - - - - - - 236 - 236 - 236 - - - - - - - 236 - 236 - 236 - - - - - - - - - Roboto - 75 - true - - - - color: #ff9719; border: 0; border-radius: 4px; font-weight: bold; - - - version 0.1 by some author - - - Qt::PlainText - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - - - - 20 - 60 - 441 - 51 - - - - color: #777; border: 0; font-size: 11px; - - - This is a fancy description -It shows information about the plugin and maybe -a website or so. - - - Qt::PlainText - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop - - - true - - - - - - 20 - 110 - 681 - 491 - - - - - - - - - 0 - 0 - 0 - - - - - - - 240 - 240 - 240 - - - - - - - 255 - 255 - 255 - - - - - - - 247 - 247 - 247 - - - - - - - 120 - 120 - 120 - - - - - - - 160 - 160 - 160 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 240 - 240 - 240 - - - - - - - 0 - 0 - 0 - - - - - - - 247 - 247 - 247 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - - - 0 - 0 - 0 - - - - - - - 240 - 240 - 240 - - - - - - - 255 - 255 - 255 - - - - - - - 247 - 247 - 247 - - - - - - - 120 - 120 - 120 - - - - - - - 160 - 160 - 160 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 240 - 240 - 240 - - - - - - - 0 - 0 - 0 - - - - - - - 247 - 247 - 247 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - - - 120 - 120 - 120 - - - - - - - 240 - 240 - 240 - - - - - - - 255 - 255 - 255 - - - - - - - 247 - 247 - 247 - - - - - - - 120 - 120 - 120 - - - - - - - 160 - 160 - 160 - - - - - - - 120 - 120 - 120 - - - - - - - 255 - 255 - 255 - - - - - - - 120 - 120 - 120 - - - - - - - 240 - 240 - 240 - - - - - - - 240 - 240 - 240 - - - - - - - 0 - 0 - 0 - - - - - - - 240 - 240 - 240 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - - - Roboto - -1 - 37 - false - - - - QTableWidget { border: 1px solid #ccc; font-family: Roboto; font-size: 11px; } - - - QFrame::StyledPanel - - - Qt::ScrollBarAsNeeded - - - Qt::ScrollBarAlwaysOff - - - QAbstractItemView::NoEditTriggers - - - false - - - false - - - true - - - QAbstractItemView::NoSelection - - - QAbstractItemView::SelectRows - - - Qt::ElideNone - - - QAbstractItemView::ScrollPerPixel - - - QAbstractItemView::ScrollPerPixel - - - false - - - Qt::NoPen - - - true - - - true - - - true - - - false - - - true - - - false - - - true - - - false - - - false - - - 45 - - - false - - - false - - - - Row - - - AlignHCenter|AlignVCenter|AlignCenter - - - - - Row2 - - - - - Setting - - - - - Default - - - AlignHCenter|AlignVCenter|AlignCenter - - - - - Current Value - - - - - Description - - - - - Long setting name which requires - - - - - DefaultRow0 - - - - - CurrentRow0 - - - - - DescRow0 - - - - - DefaultRow1 - - - - - CurrentRow1 - - - - - aasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfg - - - - - - - 20 - 20 - 391 - 21 - - - - - Roboto - -1 - 37 - false - - - - color: #555; border: 0; font-size: 15px; - - - Ambient Occlusion - - - Qt::RichText - - - - - - 520 - 20 - 180 - 31 - - - - QPushButton { -color: #eee; -background: #666; -border: 0; -} - -QPushButton:hover { - background: #555; -} - -QPushButton:pressed { - background: #111; -} - - - Reset Settings of this Plugin - - - - - - - 0 - 60 - 281 - 621 - - - - false - - - QFrame {background: #eee;} - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - 20 - 560 - 251 - 51 - - - - - - - - - 153 - 153 - 153 - - - - - - - 238 - 238 - 238 - - - - - - - 153 - 153 - 153 - - - - - - - 153 - 153 - 153 - - - - - - - 238 - 238 - 238 - - - - - - - 238 - 238 - 238 - - - - - - - - - 153 - 153 - 153 - - - - - - - 238 - 238 - 238 - - - - - - - 153 - 153 - 153 - - - - - - - 153 - 153 - 153 - - - - - - - 238 - 238 - 238 - - - - - - - 238 - 238 - 238 - - - - - - - - - 153 - 153 - 153 - - - - - - - 238 - 238 - 238 - - - - - - - 153 - 153 - 153 - - - - - - - 153 - 153 - 153 - - - - - - - 238 - 238 - 238 - - - - - - - 238 - 238 - 238 - - - - - - - - - Roboto - 37 - false - - - - border-top: 1px dotted #aaa; color: #999; padding: 0; - - - Hint: Settings with a gray color require a pipeline restart when changed. - - - Qt::PlainText - - - false - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - 687 - 18 - 294 - 28 - - - - - Roboto - -1 - 37 - false - - - - false - - - background: #ff9719; color: #eee; padding: 5px; border: 0; font-size: 12px; - - - Pipeline needs to be restarted to apply all changes! - - - Qt::AlignCenter - - - - - - 20 - 25 - 261 - 31 - - - - font-size: 20px; color: #ff9719; -font-weight: 100; - - - PLUGIN CONFIGURATOR - - - - - - 20 - 6 - 261 - 31 - - - - font-size: 13px; -color: #aaa; -font-weight: 600; - - - RENDER PIPELINE - - - frame - lst_plugins - lbl_restart_pipeline - frame_details - label - label_9 - - - - - - - + + + MainWindow + + + + 0 + 0 + 1000 + 680 + + + + + 0 + 0 + + + + + 1000 + 680 + + + + + 1000 + 680 + + + + Plugin Configurator + + + + :/icon/res/icon.png:/icon/res/icon.png + + + QMainWindow { background: #fff;} +*, QLabel { font-family: Roboto; font-weight: 300; } + +QScrollBar { + background: #eee; +} + + +QScrollBar:vertical { + width: 9px; + margin: 0; + } + + QScrollBar::handle:vertical { + min-height: 15px; + background: #aaa; + + } + + + QScrollBar::handle:vertical:hover { + + background: #999; + + } + QScrollBar::add-line:vertical { + } + + QScrollBar::sub-line:vertical { + } + + + QScrollBar::add-page:vertical { + background: #ddd; + + } + QScrollBar::sub-page:vertical { + background: #ddd; + } + + + + + + + 20 + 80 + 251 + 531 + + + + QListWidget::item { +padding: 8px 7px 5px; +background: #ddd; +outline: 0 !important; +margin-bottom: 1px; +margin-right: 10px; +color: #777; +font-weight: 500; +border: 0; +border-radius: 0; +} + +QListWidget::item:hover { +background: #ccc; +} + +QListWidget::item:selected { +background: #555; +color: #eee; +} + +QListWidget { +padding: 0; +color: #eee; +background: transparent; +/* +background: #222; +padding: 5px 8px; +border: 1px solid #444; +*/ +} + +* { +outline: 0; +} + + + + + QFrame::NoFrame + + + 0 + + + + Ambient Occlusion + + + + + Another Ite + + + + + Bloom + + + + + Volumetric Clouds + + + + + Depth of Field + + + + + Environment Probes + + + + + FXAA (Antialiasing) + + + + + SKAA + + + + + Motion Blur + + + + + PSSM Shadows + + + + + Atmospheric Scattering + + + + + Skin Shading + + + + + SMAA (Antialiasing) + + + + + Screen Space Reflections + + + + + Volumetric Lighting + + + + + VXGI + + + + + Item 346 + + + + + Other Item + + + + + Other Item to force scrollbars + + + + + + + 281 + 60 + 721 + 621 + + + + + + + + + 255 + 255 + 255 + + + + + + + 229 + 229 + 229 + + + + + + + 63 + 63 + 63 + + + + + + + 52 + 52 + 52 + + + + + + + 21 + 21 + 21 + + + + + + + 28 + 28 + 28 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 229 + 229 + 229 + + + + + + + 229 + 229 + 229 + + + + + + + 0 + 0 + 0 + + + + + + + 21 + 21 + 21 + + + + + + + 255 + 255 + 220 + + + + + + + 0 + 0 + 0 + + + + + + + + + 255 + 255 + 255 + + + + + + + 229 + 229 + 229 + + + + + + + 63 + 63 + 63 + + + + + + + 52 + 52 + 52 + + + + + + + 21 + 21 + 21 + + + + + + + 28 + 28 + 28 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 229 + 229 + 229 + + + + + + + 229 + 229 + 229 + + + + + + + 0 + 0 + 0 + + + + + + + 21 + 21 + 21 + + + + + + + 255 + 255 + 220 + + + + + + + 0 + 0 + 0 + + + + + + + + + 21 + 21 + 21 + + + + + + + 229 + 229 + 229 + + + + + + + 63 + 63 + 63 + + + + + + + 52 + 52 + 52 + + + + + + + 21 + 21 + 21 + + + + + + + 28 + 28 + 28 + + + + + + + 21 + 21 + 21 + + + + + + + 255 + 255 + 255 + + + + + + + 21 + 21 + 21 + + + + + + + 229 + 229 + 229 + + + + + + + 229 + 229 + 229 + + + + + + + 0 + 0 + 0 + + + + + + + 42 + 42 + 42 + + + + + + + 255 + 255 + 220 + + + + + + + 0 + 0 + 0 + + + + + + + + false + + + #frame_details { background: #e5e5e5; } + + + QFrame::NoFrame + + + QFrame::Plain + + + 1 + + + + + 20 + 40 + 391 + 21 + + + + + + + + + 255 + 151 + 25 + + + + + + + 236 + 236 + 236 + + + + + + + 255 + 151 + 25 + + + + + + + 255 + 151 + 25 + + + + + + + 236 + 236 + 236 + + + + + + + 236 + 236 + 236 + + + + + + + + + 255 + 151 + 25 + + + + + + + 236 + 236 + 236 + + + + + + + 255 + 151 + 25 + + + + + + + 255 + 151 + 25 + + + + + + + 236 + 236 + 236 + + + + + + + 236 + 236 + 236 + + + + + + + + + 255 + 151 + 25 + + + + + + + 236 + 236 + 236 + + + + + + + 255 + 151 + 25 + + + + + + + 255 + 151 + 25 + + + + + + + 236 + 236 + 236 + + + + + + + 236 + 236 + 236 + + + + + + + + + Roboto + 75 + true + + + + color: #ff9719; border: 0; border-radius: 4px; font-weight: bold; + + + version 0.1 by some author + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + 20 + 60 + 441 + 51 + + + + color: #777; border: 0; font-size: 11px; + + + This is a fancy description +It shows information about the plugin and maybe +a website or so. + + + Qt::PlainText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + 20 + 110 + 681 + 491 + + + + + + + + + 0 + 0 + 0 + + + + + + + 240 + 240 + 240 + + + + + + + 255 + 255 + 255 + + + + + + + 247 + 247 + 247 + + + + + + + 120 + 120 + 120 + + + + + + + 160 + 160 + 160 + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 255 + 255 + + + + + + + 240 + 240 + 240 + + + + + + + 0 + 0 + 0 + + + + + + + 247 + 247 + 247 + + + + + + + 255 + 255 + 220 + + + + + + + 0 + 0 + 0 + + + + + + + + + 0 + 0 + 0 + + + + + + + 240 + 240 + 240 + + + + + + + 255 + 255 + 255 + + + + + + + 247 + 247 + 247 + + + + + + + 120 + 120 + 120 + + + + + + + 160 + 160 + 160 + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 255 + 255 + + + + + + + 240 + 240 + 240 + + + + + + + 0 + 0 + 0 + + + + + + + 247 + 247 + 247 + + + + + + + 255 + 255 + 220 + + + + + + + 0 + 0 + 0 + + + + + + + + + 120 + 120 + 120 + + + + + + + 240 + 240 + 240 + + + + + + + 255 + 255 + 255 + + + + + + + 247 + 247 + 247 + + + + + + + 120 + 120 + 120 + + + + + + + 160 + 160 + 160 + + + + + + + 120 + 120 + 120 + + + + + + + 255 + 255 + 255 + + + + + + + 120 + 120 + 120 + + + + + + + 240 + 240 + 240 + + + + + + + 240 + 240 + 240 + + + + + + + 0 + 0 + 0 + + + + + + + 240 + 240 + 240 + + + + + + + 255 + 255 + 220 + + + + + + + 0 + 0 + 0 + + + + + + + + + Roboto + -1 + 37 + false + + + + QTableWidget { border: 1px solid #ccc; font-family: Roboto; font-size: 11px; } + + + QFrame::StyledPanel + + + Qt::ScrollBarAsNeeded + + + Qt::ScrollBarAlwaysOff + + + QAbstractItemView::NoEditTriggers + + + false + + + false + + + true + + + QAbstractItemView::NoSelection + + + QAbstractItemView::SelectRows + + + Qt::ElideNone + + + QAbstractItemView::ScrollPerPixel + + + QAbstractItemView::ScrollPerPixel + + + false + + + Qt::NoPen + + + true + + + true + + + true + + + false + + + true + + + false + + + true + + + false + + + false + + + 45 + + + false + + + false + + + + Row + + + AlignHCenter|AlignVCenter|AlignCenter + + + + + Row2 + + + + + Setting + + + + + Default + + + AlignHCenter|AlignVCenter|AlignCenter + + + + + Current Value + + + + + Description + + + + + Long setting name which requires + + + + + DefaultRow0 + + + + + CurrentRow0 + + + + + DescRow0 + + + + + DefaultRow1 + + + + + CurrentRow1 + + + + + aasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfgaasd\n\nsdf\n\ndfdfG\n\nfg + + + + + + + 20 + 20 + 391 + 21 + + + + + Roboto + -1 + 37 + false + + + + color: #555; border: 0; font-size: 15px; + + + Ambient Occlusion + + + Qt::RichText + + + + + + 520 + 20 + 180 + 31 + + + + QPushButton { +color: #eee; +background: #666; +border: 0; +} + +QPushButton:hover { + background: #555; +} + +QPushButton:pressed { + background: #111; +} + + + Reset Settings of this Plugin + + + + + + + 0 + 60 + 281 + 621 + + + + false + + + QFrame {background: #eee;} + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + 20 + 560 + 251 + 51 + + + + + + + + + 153 + 153 + 153 + + + + + + + 238 + 238 + 238 + + + + + + + 153 + 153 + 153 + + + + + + + 153 + 153 + 153 + + + + + + + 238 + 238 + 238 + + + + + + + 238 + 238 + 238 + + + + + + + + + 153 + 153 + 153 + + + + + + + 238 + 238 + 238 + + + + + + + 153 + 153 + 153 + + + + + + + 153 + 153 + 153 + + + + + + + 238 + 238 + 238 + + + + + + + 238 + 238 + 238 + + + + + + + + + 153 + 153 + 153 + + + + + + + 238 + 238 + 238 + + + + + + + 153 + 153 + 153 + + + + + + + 153 + 153 + 153 + + + + + + + 238 + 238 + 238 + + + + + + + 238 + 238 + 238 + + + + + + + + + Roboto + 37 + false + + + + border-top: 1px dotted #aaa; color: #999; padding: 0; + + + Hint: Settings with a gray color require a pipeline restart when changed. + + + Qt::PlainText + + + false + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + 687 + 18 + 294 + 28 + + + + + Roboto + -1 + 37 + false + + + + false + + + background: #ff9719; color: #eee; padding: 5px; border: 0; font-size: 12px; + + + Pipeline needs to be restarted to apply all changes! + + + Qt::AlignCenter + + + + + + 20 + 25 + 261 + 31 + + + + font-size: 20px; color: #ff9719; +font-weight: 100; + + + PLUGIN CONFIGURATOR + + + + + + 20 + 6 + 261 + 31 + + + + font-size: 13px; +color: #aaa; +font-weight: 600; + + + RENDER PIPELINE + + + frame + lst_plugins + lbl_restart_pipeline + frame_details + label + label_9 + + + + + + + diff --git a/RenderPipelineFile/toolkit/plugin_configurator/ui/main_window_generated.py b/RenderPipelineFile/toolkit/plugin_configurator/ui/main_window_generated.py index 3b8004c3..a5e6f8fc 100644 --- a/RenderPipelineFile/toolkit/plugin_configurator/ui/main_window_generated.py +++ b/RenderPipelineFile/toolkit/plugin_configurator/ui/main_window_generated.py @@ -1,790 +1,790 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'main_window.ui' -# -# Created by: PyQt5 UI code generator 5.7 -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore, QtGui, QtWidgets - -class Ui_MainWindow(object): - def setupUi(self, MainWindow): - MainWindow.setObjectName("MainWindow") - MainWindow.resize(1000, 680) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth()) - MainWindow.setSizePolicy(sizePolicy) - MainWindow.setMinimumSize(QtCore.QSize(1000, 680)) - MainWindow.setMaximumSize(QtCore.QSize(1000, 680)) - icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(":/icon/res/icon.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - MainWindow.setWindowIcon(icon) - MainWindow.setStyleSheet("QMainWindow { background: #fff;}\n" -"*, QLabel { font-family: Roboto; font-weight: 300; }\n" -"\n" -"QScrollBar {\n" -" background: #eee;\n" -"}\n" -"\n" -"\n" -"QScrollBar:vertical {\n" -" width: 9px;\n" -" margin: 0;\n" -" }\n" -"\n" -" QScrollBar::handle:vertical {\n" -" min-height: 15px;\n" -" background: #aaa;\n" -"\n" -" }\n" -"\n" -"\n" -" QScrollBar::handle:vertical:hover {\n" -" \n" -" background: #999;\n" -"\n" -" }\n" -" QScrollBar::add-line:vertical {\n" -" }\n" -"\n" -" QScrollBar::sub-line:vertical {\n" -" }\n" -"\n" -"\n" -" QScrollBar::add-page:vertical {\n" -" background: #ddd;\n" -"\n" -" }\n" -" QScrollBar::sub-page:vertical {\n" -" background: #ddd;\n" -" }\n" -"") - self.centralwidget = QtWidgets.QWidget(MainWindow) - self.centralwidget.setObjectName("centralwidget") - self.lst_plugins = QtWidgets.QListWidget(self.centralwidget) - self.lst_plugins.setGeometry(QtCore.QRect(20, 80, 251, 531)) - self.lst_plugins.setStyleSheet("QListWidget::item {\n" -"padding: 8px 7px 5px;\n" -"background: #ddd;\n" -"outline: 0 !important;\n" -"margin-bottom: 1px;\n" -"margin-right: 10px;\n" -"color: #777;\n" -"font-weight: 500;\n" -"border: 0;\n" -"border-radius: 0;\n" -"}\n" -"\n" -"QListWidget::item:hover {\n" -"background: #ccc;\n" -"}\n" -"\n" -"QListWidget::item:selected {\n" -"background: #555;\n" -"color: #eee;\n" -"}\n" -"\n" -"QListWidget {\n" -"padding: 0;\n" -"color: #eee;\n" -"background: transparent;\n" -"/*\n" -"background: #222;\n" -"padding: 5px 8px;\n" -"border: 1px solid #444;\n" -"*/\n" -"}\n" -"\n" -"* {\n" -"outline: 0;\n" -"}\n" -"\n" -"") - self.lst_plugins.setFrameShape(QtWidgets.QFrame.NoFrame) - self.lst_plugins.setLineWidth(0) - self.lst_plugins.setObjectName("lst_plugins") - item = QtWidgets.QListWidgetItem() - self.lst_plugins.addItem(item) - item = QtWidgets.QListWidgetItem() - self.lst_plugins.addItem(item) - item = QtWidgets.QListWidgetItem() - self.lst_plugins.addItem(item) - item = QtWidgets.QListWidgetItem() - self.lst_plugins.addItem(item) - item = QtWidgets.QListWidgetItem() - self.lst_plugins.addItem(item) - item = QtWidgets.QListWidgetItem() - self.lst_plugins.addItem(item) - item = QtWidgets.QListWidgetItem() - self.lst_plugins.addItem(item) - item = QtWidgets.QListWidgetItem() - self.lst_plugins.addItem(item) - item = QtWidgets.QListWidgetItem() - self.lst_plugins.addItem(item) - item = QtWidgets.QListWidgetItem() - self.lst_plugins.addItem(item) - item = QtWidgets.QListWidgetItem() - self.lst_plugins.addItem(item) - item = QtWidgets.QListWidgetItem() - self.lst_plugins.addItem(item) - item = QtWidgets.QListWidgetItem() - self.lst_plugins.addItem(item) - item = QtWidgets.QListWidgetItem() - self.lst_plugins.addItem(item) - item = QtWidgets.QListWidgetItem() - self.lst_plugins.addItem(item) - item = QtWidgets.QListWidgetItem() - self.lst_plugins.addItem(item) - item = QtWidgets.QListWidgetItem() - self.lst_plugins.addItem(item) - item = QtWidgets.QListWidgetItem() - self.lst_plugins.addItem(item) - item = QtWidgets.QListWidgetItem() - self.lst_plugins.addItem(item) - self.frame_details = QtWidgets.QFrame(self.centralwidget) - self.frame_details.setGeometry(QtCore.QRect(281, 60, 721, 621)) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(229, 229, 229)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush) - brush = QtGui.QBrush(QtGui.QColor(63, 63, 63)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Light, brush) - brush = QtGui.QBrush(QtGui.QColor(52, 52, 52)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Midlight, brush) - brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Dark, brush) - brush = QtGui.QBrush(QtGui.QColor(28, 28, 28)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Mid, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.BrightText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(229, 229, 229)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush) - brush = QtGui.QBrush(QtGui.QColor(229, 229, 229)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Shadow, brush) - brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.AlternateBase, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ToolTipBase, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ToolTipText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(229, 229, 229)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush) - brush = QtGui.QBrush(QtGui.QColor(63, 63, 63)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Light, brush) - brush = QtGui.QBrush(QtGui.QColor(52, 52, 52)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Midlight, brush) - brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Dark, brush) - brush = QtGui.QBrush(QtGui.QColor(28, 28, 28)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Mid, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.BrightText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(229, 229, 229)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush) - brush = QtGui.QBrush(QtGui.QColor(229, 229, 229)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Shadow, brush) - brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.AlternateBase, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipBase, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipText, brush) - brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(229, 229, 229)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush) - brush = QtGui.QBrush(QtGui.QColor(63, 63, 63)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Light, brush) - brush = QtGui.QBrush(QtGui.QColor(52, 52, 52)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Midlight, brush) - brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Dark, brush) - brush = QtGui.QBrush(QtGui.QColor(28, 28, 28)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Mid, brush) - brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.BrightText, brush) - brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(229, 229, 229)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush) - brush = QtGui.QBrush(QtGui.QColor(229, 229, 229)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Shadow, brush) - brush = QtGui.QBrush(QtGui.QColor(42, 42, 42)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.AlternateBase, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipBase, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipText, brush) - self.frame_details.setPalette(palette) - self.frame_details.setAutoFillBackground(False) - self.frame_details.setStyleSheet("#frame_details { background: #e5e5e5; }") - self.frame_details.setFrameShape(QtWidgets.QFrame.NoFrame) - self.frame_details.setFrameShadow(QtWidgets.QFrame.Plain) - self.frame_details.setLineWidth(1) - self.frame_details.setObjectName("frame_details") - self.lbl_plugin_version = QtWidgets.QLabel(self.frame_details) - self.lbl_plugin_version.setGeometry(QtCore.QRect(20, 40, 391, 21)) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(255, 151, 25)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(236, 236, 236)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 151, 25)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 151, 25)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(236, 236, 236)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush) - brush = QtGui.QBrush(QtGui.QColor(236, 236, 236)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 151, 25)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(236, 236, 236)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 151, 25)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 151, 25)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(236, 236, 236)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush) - brush = QtGui.QBrush(QtGui.QColor(236, 236, 236)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 151, 25)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(236, 236, 236)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 151, 25)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 151, 25)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(236, 236, 236)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush) - brush = QtGui.QBrush(QtGui.QColor(236, 236, 236)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush) - self.lbl_plugin_version.setPalette(palette) - font = QtGui.QFont() - font.setFamily("Roboto") - font.setBold(True) - font.setWeight(75) - self.lbl_plugin_version.setFont(font) - self.lbl_plugin_version.setStyleSheet("color: #ff9719; border: 0; border-radius: 4px; font-weight: bold;") - self.lbl_plugin_version.setTextFormat(QtCore.Qt.PlainText) - self.lbl_plugin_version.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) - self.lbl_plugin_version.setObjectName("lbl_plugin_version") - self.lbl_plugin_desc = QtWidgets.QLabel(self.frame_details) - self.lbl_plugin_desc.setGeometry(QtCore.QRect(20, 60, 441, 51)) - self.lbl_plugin_desc.setStyleSheet("color: #777; border: 0; font-size: 11px;") - self.lbl_plugin_desc.setTextFormat(QtCore.Qt.PlainText) - self.lbl_plugin_desc.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) - self.lbl_plugin_desc.setWordWrap(True) - self.lbl_plugin_desc.setObjectName("lbl_plugin_desc") - self.table_plugin_settings = QtWidgets.QTableWidget(self.frame_details) - self.table_plugin_settings.setGeometry(QtCore.QRect(20, 110, 681, 491)) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(240, 240, 240)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Light, brush) - brush = QtGui.QBrush(QtGui.QColor(247, 247, 247)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Midlight, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Dark, brush) - brush = QtGui.QBrush(QtGui.QColor(160, 160, 160)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Mid, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.BrightText, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush) - brush = QtGui.QBrush(QtGui.QColor(240, 240, 240)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Shadow, brush) - brush = QtGui.QBrush(QtGui.QColor(247, 247, 247)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.AlternateBase, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ToolTipBase, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ToolTipText, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(240, 240, 240)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Light, brush) - brush = QtGui.QBrush(QtGui.QColor(247, 247, 247)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Midlight, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Dark, brush) - brush = QtGui.QBrush(QtGui.QColor(160, 160, 160)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Mid, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.BrightText, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush) - brush = QtGui.QBrush(QtGui.QColor(240, 240, 240)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Shadow, brush) - brush = QtGui.QBrush(QtGui.QColor(247, 247, 247)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.AlternateBase, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipBase, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipText, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(240, 240, 240)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Light, brush) - brush = QtGui.QBrush(QtGui.QColor(247, 247, 247)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Midlight, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Dark, brush) - brush = QtGui.QBrush(QtGui.QColor(160, 160, 160)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Mid, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.BrightText, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(240, 240, 240)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush) - brush = QtGui.QBrush(QtGui.QColor(240, 240, 240)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Shadow, brush) - brush = QtGui.QBrush(QtGui.QColor(240, 240, 240)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.AlternateBase, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipBase, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipText, brush) - self.table_plugin_settings.setPalette(palette) - font = QtGui.QFont() - font.setFamily("Roboto") - font.setPointSize(-1) - font.setBold(False) - font.setWeight(37) - self.table_plugin_settings.setFont(font) - self.table_plugin_settings.setStyleSheet("QTableWidget { border: 1px solid #ccc; font-family: Roboto; font-size: 11px; }") - self.table_plugin_settings.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.table_plugin_settings.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.table_plugin_settings.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.table_plugin_settings.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) - self.table_plugin_settings.setProperty("showDropIndicator", False) - self.table_plugin_settings.setDragDropOverwriteMode(False) - self.table_plugin_settings.setAlternatingRowColors(True) - self.table_plugin_settings.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) - self.table_plugin_settings.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) - self.table_plugin_settings.setTextElideMode(QtCore.Qt.ElideNone) - self.table_plugin_settings.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) - self.table_plugin_settings.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) - self.table_plugin_settings.setShowGrid(False) - self.table_plugin_settings.setGridStyle(QtCore.Qt.NoPen) - self.table_plugin_settings.setWordWrap(True) - self.table_plugin_settings.setCornerButtonEnabled(True) - self.table_plugin_settings.setObjectName("table_plugin_settings") - self.table_plugin_settings.setColumnCount(4) - self.table_plugin_settings.setRowCount(2) - item = QtWidgets.QTableWidgetItem() - item.setTextAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter|QtCore.Qt.AlignCenter) - self.table_plugin_settings.setVerticalHeaderItem(0, item) - item = QtWidgets.QTableWidgetItem() - self.table_plugin_settings.setVerticalHeaderItem(1, item) - item = QtWidgets.QTableWidgetItem() - self.table_plugin_settings.setHorizontalHeaderItem(0, item) - item = QtWidgets.QTableWidgetItem() - item.setTextAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter|QtCore.Qt.AlignCenter) - self.table_plugin_settings.setHorizontalHeaderItem(1, item) - item = QtWidgets.QTableWidgetItem() - self.table_plugin_settings.setHorizontalHeaderItem(2, item) - item = QtWidgets.QTableWidgetItem() - self.table_plugin_settings.setHorizontalHeaderItem(3, item) - item = QtWidgets.QTableWidgetItem() - self.table_plugin_settings.setItem(0, 0, item) - item = QtWidgets.QTableWidgetItem() - self.table_plugin_settings.setItem(0, 1, item) - item = QtWidgets.QTableWidgetItem() - self.table_plugin_settings.setItem(0, 2, item) - item = QtWidgets.QTableWidgetItem() - self.table_plugin_settings.setItem(0, 3, item) - item = QtWidgets.QTableWidgetItem() - self.table_plugin_settings.setItem(1, 1, item) - item = QtWidgets.QTableWidgetItem() - self.table_plugin_settings.setItem(1, 2, item) - item = QtWidgets.QTableWidgetItem() - self.table_plugin_settings.setItem(1, 3, item) - self.table_plugin_settings.horizontalHeader().setVisible(True) - self.table_plugin_settings.horizontalHeader().setCascadingSectionResizes(False) - self.table_plugin_settings.horizontalHeader().setHighlightSections(True) - self.table_plugin_settings.horizontalHeader().setSortIndicatorShown(False) - self.table_plugin_settings.horizontalHeader().setStretchLastSection(True) - self.table_plugin_settings.verticalHeader().setVisible(False) - self.table_plugin_settings.verticalHeader().setCascadingSectionResizes(False) - self.table_plugin_settings.verticalHeader().setDefaultSectionSize(45) - self.table_plugin_settings.verticalHeader().setSortIndicatorShown(False) - self.table_plugin_settings.verticalHeader().setStretchLastSection(False) - self.lbl_plugin_name = QtWidgets.QLabel(self.frame_details) - self.lbl_plugin_name.setGeometry(QtCore.QRect(20, 20, 391, 21)) - font = QtGui.QFont() - font.setFamily("Roboto") - font.setPointSize(-1) - font.setBold(False) - font.setWeight(37) - self.lbl_plugin_name.setFont(font) - self.lbl_plugin_name.setStyleSheet("color: #555; border: 0; font-size: 15px;") - self.lbl_plugin_name.setTextFormat(QtCore.Qt.RichText) - self.lbl_plugin_name.setObjectName("lbl_plugin_name") - self.btn_reset_plugin_settings = QtWidgets.QPushButton(self.frame_details) - self.btn_reset_plugin_settings.setGeometry(QtCore.QRect(520, 60, 180, 31)) - self.btn_reset_plugin_settings.setStyleSheet("QPushButton {\n" -"color: #eee;\n" -"background: #666;\n" -"border: 0; \n" -"}\n" -"\n" -"QPushButton:hover {\n" -" background: #555;\n" -"}\n" -"\n" -"QPushButton:pressed {\n" -" background: #111;\n" -"}") - self.btn_reset_plugin_settings.setObjectName("btn_reset_plugin_settings") - self.frame = QtWidgets.QFrame(self.centralwidget) - self.frame.setGeometry(QtCore.QRect(0, 60, 281, 621)) - self.frame.setAutoFillBackground(False) - self.frame.setStyleSheet("QFrame {background: #eee;}") - self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) - self.frame.setFrameShadow(QtWidgets.QFrame.Raised) - self.frame.setObjectName("frame") - self.lbl_hint_restart = QtWidgets.QLabel(self.frame) - self.lbl_hint_restart.setGeometry(QtCore.QRect(20, 560, 251, 51)) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(153, 153, 153)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(238, 238, 238)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush) - brush = QtGui.QBrush(QtGui.QColor(153, 153, 153)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(153, 153, 153)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(238, 238, 238)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush) - brush = QtGui.QBrush(QtGui.QColor(238, 238, 238)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush) - brush = QtGui.QBrush(QtGui.QColor(153, 153, 153)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(238, 238, 238)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush) - brush = QtGui.QBrush(QtGui.QColor(153, 153, 153)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(153, 153, 153)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(238, 238, 238)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush) - brush = QtGui.QBrush(QtGui.QColor(238, 238, 238)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush) - brush = QtGui.QBrush(QtGui.QColor(153, 153, 153)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(238, 238, 238)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush) - brush = QtGui.QBrush(QtGui.QColor(153, 153, 153)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(153, 153, 153)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(238, 238, 238)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush) - brush = QtGui.QBrush(QtGui.QColor(238, 238, 238)) - brush.setStyle(QtCore.Qt.SolidPattern) - palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush) - self.lbl_hint_restart.setPalette(palette) - font = QtGui.QFont() - font.setFamily("Roboto") - font.setBold(False) - font.setWeight(37) - self.lbl_hint_restart.setFont(font) - self.lbl_hint_restart.setStyleSheet("border-top: 1px dotted #aaa; color: #999; padding: 0;") - self.lbl_hint_restart.setTextFormat(QtCore.Qt.PlainText) - self.lbl_hint_restart.setScaledContents(False) - self.lbl_hint_restart.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) - self.lbl_hint_restart.setWordWrap(True) - self.lbl_hint_restart.setObjectName("lbl_hint_restart") - self.lbl_restart_pipeline = QtWidgets.QLabel(self.centralwidget) - self.lbl_restart_pipeline.setGeometry(QtCore.QRect(687, 18, 294, 28)) - font = QtGui.QFont() - font.setFamily("Roboto") - font.setPointSize(-1) - font.setBold(False) - font.setWeight(37) - self.lbl_restart_pipeline.setFont(font) - self.lbl_restart_pipeline.setAutoFillBackground(False) - self.lbl_restart_pipeline.setStyleSheet("background: #ff9719; color: #eee; padding: 5px; border: 0; font-size: 12px;") - self.lbl_restart_pipeline.setAlignment(QtCore.Qt.AlignCenter) - self.lbl_restart_pipeline.setObjectName("lbl_restart_pipeline") - self.label = QtWidgets.QLabel(self.centralwidget) - self.label.setGeometry(QtCore.QRect(20, 25, 261, 31)) - self.label.setStyleSheet("font-size: 20px; color: #ff9719;\n" -"font-weight: 100;") - self.label.setObjectName("label") - self.label_9 = QtWidgets.QLabel(self.centralwidget) - self.label_9.setGeometry(QtCore.QRect(20, 6, 261, 31)) - self.label_9.setStyleSheet("font-size: 13px;\n" -"color: #aaa;\n" -"font-weight: 600;") - self.label_9.setObjectName("label_9") - self.frame.raise_() - self.lst_plugins.raise_() - self.lbl_restart_pipeline.raise_() - self.frame_details.raise_() - self.label.raise_() - self.label_9.raise_() - MainWindow.setCentralWidget(self.centralwidget) - - self.retranslateUi(MainWindow) - QtCore.QMetaObject.connectSlotsByName(MainWindow) - - def retranslateUi(self, MainWindow): - _translate = QtCore.QCoreApplication.translate - MainWindow.setWindowTitle(_translate("MainWindow", "Plugin Configurator")) - __sortingEnabled = self.lst_plugins.isSortingEnabled() - self.lst_plugins.setSortingEnabled(False) - item = self.lst_plugins.item(0) - item.setText(_translate("MainWindow", "Ambient Occlusion")) - item = self.lst_plugins.item(1) - item.setText(_translate("MainWindow", "Another Ite")) - item = self.lst_plugins.item(2) - item.setText(_translate("MainWindow", "Bloom")) - item = self.lst_plugins.item(3) - item.setText(_translate("MainWindow", "Volumetric Clouds")) - item = self.lst_plugins.item(4) - item.setText(_translate("MainWindow", "Depth of Field")) - item = self.lst_plugins.item(5) - item.setText(_translate("MainWindow", "Environment Probes")) - item = self.lst_plugins.item(6) - item.setText(_translate("MainWindow", "FXAA (Antialiasing)")) - item = self.lst_plugins.item(7) - item.setText(_translate("MainWindow", "SKAA")) - item = self.lst_plugins.item(8) - item.setText(_translate("MainWindow", "Motion Blur")) - item = self.lst_plugins.item(9) - item.setText(_translate("MainWindow", "PSSM Shadows")) - item = self.lst_plugins.item(10) - item.setText(_translate("MainWindow", "Atmospheric Scattering")) - item = self.lst_plugins.item(11) - item.setText(_translate("MainWindow", "Skin Shading")) - item = self.lst_plugins.item(12) - item.setText(_translate("MainWindow", "SMAA (Antialiasing)")) - item = self.lst_plugins.item(13) - item.setText(_translate("MainWindow", "Screen Space Reflections")) - item = self.lst_plugins.item(14) - item.setText(_translate("MainWindow", "Volumetric Lighting")) - item = self.lst_plugins.item(15) - item.setText(_translate("MainWindow", "VXGI")) - item = self.lst_plugins.item(16) - item.setText(_translate("MainWindow", "Item 346")) - item = self.lst_plugins.item(17) - item.setText(_translate("MainWindow", "Other Item")) - item = self.lst_plugins.item(18) - item.setText(_translate("MainWindow", "Other Item to force scrollbars")) - self.lst_plugins.setSortingEnabled(__sortingEnabled) - self.lbl_plugin_version.setText(_translate("MainWindow", "version 0.1 by some author")) - self.lbl_plugin_desc.setText(_translate("MainWindow", "This is a fancy description\n" -"It shows information about the plugin and maybe\n" -"a website or so.")) - item = self.table_plugin_settings.verticalHeaderItem(0) - item.setText(_translate("MainWindow", "Row")) - item = self.table_plugin_settings.verticalHeaderItem(1) - item.setText(_translate("MainWindow", "Row2")) - item = self.table_plugin_settings.horizontalHeaderItem(0) - item.setText(_translate("MainWindow", "Setting")) - item = self.table_plugin_settings.horizontalHeaderItem(1) - item.setText(_translate("MainWindow", "Default")) - item = self.table_plugin_settings.horizontalHeaderItem(2) - item.setText(_translate("MainWindow", "Current Value")) - item = self.table_plugin_settings.horizontalHeaderItem(3) - item.setText(_translate("MainWindow", "Description")) - __sortingEnabled = self.table_plugin_settings.isSortingEnabled() - self.table_plugin_settings.setSortingEnabled(False) - item = self.table_plugin_settings.item(0, 0) - item.setText(_translate("MainWindow", "Long setting name which requires")) - item = self.table_plugin_settings.item(0, 1) - item.setText(_translate("MainWindow", "DefaultRow0")) - item = self.table_plugin_settings.item(0, 2) - item.setText(_translate("MainWindow", "CurrentRow0")) - item = self.table_plugin_settings.item(0, 3) - item.setText(_translate("MainWindow", "DescRow0")) - item = self.table_plugin_settings.item(1, 1) - item.setText(_translate("MainWindow", "DefaultRow1")) - item = self.table_plugin_settings.item(1, 2) - item.setText(_translate("MainWindow", "CurrentRow1")) - item = self.table_plugin_settings.item(1, 3) - item.setText(_translate("MainWindow", "aasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfg")) - self.table_plugin_settings.setSortingEnabled(__sortingEnabled) - self.lbl_plugin_name.setText(_translate("MainWindow", "Ambient Occlusion")) - self.btn_reset_plugin_settings.setText(_translate("MainWindow", "Reset Settings of this Plugin")) - self.lbl_hint_restart.setText(_translate("MainWindow", "Hint: Settings with a gray color require a pipeline restart when changed.")) - self.lbl_restart_pipeline.setText(_translate("MainWindow", "Pipeline needs to be restarted to apply all changes!")) - self.label.setText(_translate("MainWindow", "PLUGIN CONFIGURATOR")) - self.label_9.setText(_translate("MainWindow", "RENDER PIPELINE")) - -from . import resources_rc +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'main_window.ui' +# +# Created by: PyQt5 UI code generator 5.7 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(1000, 680) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(MainWindow.sizePolicy().hasHeightForWidth()) + MainWindow.setSizePolicy(sizePolicy) + MainWindow.setMinimumSize(QtCore.QSize(1000, 680)) + MainWindow.setMaximumSize(QtCore.QSize(1000, 680)) + icon = QtGui.QIcon() + icon.addPixmap(QtGui.QPixmap(":/icon/res/icon.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + MainWindow.setWindowIcon(icon) + MainWindow.setStyleSheet("QMainWindow { background: #fff;}\n" +"*, QLabel { font-family: Roboto; font-weight: 300; }\n" +"\n" +"QScrollBar {\n" +" background: #eee;\n" +"}\n" +"\n" +"\n" +"QScrollBar:vertical {\n" +" width: 9px;\n" +" margin: 0;\n" +" }\n" +"\n" +" QScrollBar::handle:vertical {\n" +" min-height: 15px;\n" +" background: #aaa;\n" +"\n" +" }\n" +"\n" +"\n" +" QScrollBar::handle:vertical:hover {\n" +" \n" +" background: #999;\n" +"\n" +" }\n" +" QScrollBar::add-line:vertical {\n" +" }\n" +"\n" +" QScrollBar::sub-line:vertical {\n" +" }\n" +"\n" +"\n" +" QScrollBar::add-page:vertical {\n" +" background: #ddd;\n" +"\n" +" }\n" +" QScrollBar::sub-page:vertical {\n" +" background: #ddd;\n" +" }\n" +"") + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.lst_plugins = QtWidgets.QListWidget(self.centralwidget) + self.lst_plugins.setGeometry(QtCore.QRect(20, 80, 251, 531)) + self.lst_plugins.setStyleSheet("QListWidget::item {\n" +"padding: 8px 7px 5px;\n" +"background: #ddd;\n" +"outline: 0 !important;\n" +"margin-bottom: 1px;\n" +"margin-right: 10px;\n" +"color: #777;\n" +"font-weight: 500;\n" +"border: 0;\n" +"border-radius: 0;\n" +"}\n" +"\n" +"QListWidget::item:hover {\n" +"background: #ccc;\n" +"}\n" +"\n" +"QListWidget::item:selected {\n" +"background: #555;\n" +"color: #eee;\n" +"}\n" +"\n" +"QListWidget {\n" +"padding: 0;\n" +"color: #eee;\n" +"background: transparent;\n" +"/*\n" +"background: #222;\n" +"padding: 5px 8px;\n" +"border: 1px solid #444;\n" +"*/\n" +"}\n" +"\n" +"* {\n" +"outline: 0;\n" +"}\n" +"\n" +"") + self.lst_plugins.setFrameShape(QtWidgets.QFrame.NoFrame) + self.lst_plugins.setLineWidth(0) + self.lst_plugins.setObjectName("lst_plugins") + item = QtWidgets.QListWidgetItem() + self.lst_plugins.addItem(item) + item = QtWidgets.QListWidgetItem() + self.lst_plugins.addItem(item) + item = QtWidgets.QListWidgetItem() + self.lst_plugins.addItem(item) + item = QtWidgets.QListWidgetItem() + self.lst_plugins.addItem(item) + item = QtWidgets.QListWidgetItem() + self.lst_plugins.addItem(item) + item = QtWidgets.QListWidgetItem() + self.lst_plugins.addItem(item) + item = QtWidgets.QListWidgetItem() + self.lst_plugins.addItem(item) + item = QtWidgets.QListWidgetItem() + self.lst_plugins.addItem(item) + item = QtWidgets.QListWidgetItem() + self.lst_plugins.addItem(item) + item = QtWidgets.QListWidgetItem() + self.lst_plugins.addItem(item) + item = QtWidgets.QListWidgetItem() + self.lst_plugins.addItem(item) + item = QtWidgets.QListWidgetItem() + self.lst_plugins.addItem(item) + item = QtWidgets.QListWidgetItem() + self.lst_plugins.addItem(item) + item = QtWidgets.QListWidgetItem() + self.lst_plugins.addItem(item) + item = QtWidgets.QListWidgetItem() + self.lst_plugins.addItem(item) + item = QtWidgets.QListWidgetItem() + self.lst_plugins.addItem(item) + item = QtWidgets.QListWidgetItem() + self.lst_plugins.addItem(item) + item = QtWidgets.QListWidgetItem() + self.lst_plugins.addItem(item) + item = QtWidgets.QListWidgetItem() + self.lst_plugins.addItem(item) + self.frame_details = QtWidgets.QFrame(self.centralwidget) + self.frame_details.setGeometry(QtCore.QRect(281, 60, 721, 621)) + palette = QtGui.QPalette() + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(229, 229, 229)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush) + brush = QtGui.QBrush(QtGui.QColor(63, 63, 63)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Light, brush) + brush = QtGui.QBrush(QtGui.QColor(52, 52, 52)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Midlight, brush) + brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Dark, brush) + brush = QtGui.QBrush(QtGui.QColor(28, 28, 28)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Mid, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.BrightText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ButtonText, brush) + brush = QtGui.QBrush(QtGui.QColor(229, 229, 229)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush) + brush = QtGui.QBrush(QtGui.QColor(229, 229, 229)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Shadow, brush) + brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.AlternateBase, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ToolTipBase, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ToolTipText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(229, 229, 229)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush) + brush = QtGui.QBrush(QtGui.QColor(63, 63, 63)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Light, brush) + brush = QtGui.QBrush(QtGui.QColor(52, 52, 52)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Midlight, brush) + brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Dark, brush) + brush = QtGui.QBrush(QtGui.QColor(28, 28, 28)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Mid, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.BrightText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ButtonText, brush) + brush = QtGui.QBrush(QtGui.QColor(229, 229, 229)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush) + brush = QtGui.QBrush(QtGui.QColor(229, 229, 229)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Shadow, brush) + brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.AlternateBase, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipBase, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipText, brush) + brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(229, 229, 229)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush) + brush = QtGui.QBrush(QtGui.QColor(63, 63, 63)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Light, brush) + brush = QtGui.QBrush(QtGui.QColor(52, 52, 52)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Midlight, brush) + brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Dark, brush) + brush = QtGui.QBrush(QtGui.QColor(28, 28, 28)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Mid, brush) + brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.BrightText, brush) + brush = QtGui.QBrush(QtGui.QColor(21, 21, 21)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, brush) + brush = QtGui.QBrush(QtGui.QColor(229, 229, 229)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush) + brush = QtGui.QBrush(QtGui.QColor(229, 229, 229)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Shadow, brush) + brush = QtGui.QBrush(QtGui.QColor(42, 42, 42)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.AlternateBase, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipBase, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipText, brush) + self.frame_details.setPalette(palette) + self.frame_details.setAutoFillBackground(False) + self.frame_details.setStyleSheet("#frame_details { background: #e5e5e5; }") + self.frame_details.setFrameShape(QtWidgets.QFrame.NoFrame) + self.frame_details.setFrameShadow(QtWidgets.QFrame.Plain) + self.frame_details.setLineWidth(1) + self.frame_details.setObjectName("frame_details") + self.lbl_plugin_version = QtWidgets.QLabel(self.frame_details) + self.lbl_plugin_version.setGeometry(QtCore.QRect(20, 40, 391, 21)) + palette = QtGui.QPalette() + brush = QtGui.QBrush(QtGui.QColor(255, 151, 25)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(236, 236, 236)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 151, 25)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 151, 25)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ButtonText, brush) + brush = QtGui.QBrush(QtGui.QColor(236, 236, 236)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush) + brush = QtGui.QBrush(QtGui.QColor(236, 236, 236)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 151, 25)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(236, 236, 236)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 151, 25)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 151, 25)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ButtonText, brush) + brush = QtGui.QBrush(QtGui.QColor(236, 236, 236)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush) + brush = QtGui.QBrush(QtGui.QColor(236, 236, 236)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 151, 25)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(236, 236, 236)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 151, 25)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 151, 25)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, brush) + brush = QtGui.QBrush(QtGui.QColor(236, 236, 236)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush) + brush = QtGui.QBrush(QtGui.QColor(236, 236, 236)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush) + self.lbl_plugin_version.setPalette(palette) + font = QtGui.QFont() + font.setFamily("Roboto") + font.setBold(True) + font.setWeight(75) + self.lbl_plugin_version.setFont(font) + self.lbl_plugin_version.setStyleSheet("color: #ff9719; border: 0; border-radius: 4px; font-weight: bold;") + self.lbl_plugin_version.setTextFormat(QtCore.Qt.PlainText) + self.lbl_plugin_version.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self.lbl_plugin_version.setObjectName("lbl_plugin_version") + self.lbl_plugin_desc = QtWidgets.QLabel(self.frame_details) + self.lbl_plugin_desc.setGeometry(QtCore.QRect(20, 60, 441, 51)) + self.lbl_plugin_desc.setStyleSheet("color: #777; border: 0; font-size: 11px;") + self.lbl_plugin_desc.setTextFormat(QtCore.Qt.PlainText) + self.lbl_plugin_desc.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) + self.lbl_plugin_desc.setWordWrap(True) + self.lbl_plugin_desc.setObjectName("lbl_plugin_desc") + self.table_plugin_settings = QtWidgets.QTableWidget(self.frame_details) + self.table_plugin_settings.setGeometry(QtCore.QRect(20, 110, 681, 491)) + palette = QtGui.QPalette() + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(240, 240, 240)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Light, brush) + brush = QtGui.QBrush(QtGui.QColor(247, 247, 247)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Midlight, brush) + brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Dark, brush) + brush = QtGui.QBrush(QtGui.QColor(160, 160, 160)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Mid, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.BrightText, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ButtonText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush) + brush = QtGui.QBrush(QtGui.QColor(240, 240, 240)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Shadow, brush) + brush = QtGui.QBrush(QtGui.QColor(247, 247, 247)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.AlternateBase, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ToolTipBase, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ToolTipText, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(240, 240, 240)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Light, brush) + brush = QtGui.QBrush(QtGui.QColor(247, 247, 247)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Midlight, brush) + brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Dark, brush) + brush = QtGui.QBrush(QtGui.QColor(160, 160, 160)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Mid, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.BrightText, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ButtonText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush) + brush = QtGui.QBrush(QtGui.QColor(240, 240, 240)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Shadow, brush) + brush = QtGui.QBrush(QtGui.QColor(247, 247, 247)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.AlternateBase, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipBase, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ToolTipText, brush) + brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(240, 240, 240)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Light, brush) + brush = QtGui.QBrush(QtGui.QColor(247, 247, 247)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Midlight, brush) + brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Dark, brush) + brush = QtGui.QBrush(QtGui.QColor(160, 160, 160)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Mid, brush) + brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.BrightText, brush) + brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, brush) + brush = QtGui.QBrush(QtGui.QColor(240, 240, 240)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush) + brush = QtGui.QBrush(QtGui.QColor(240, 240, 240)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Shadow, brush) + brush = QtGui.QBrush(QtGui.QColor(240, 240, 240)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.AlternateBase, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipBase, brush) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ToolTipText, brush) + self.table_plugin_settings.setPalette(palette) + font = QtGui.QFont() + font.setFamily("Roboto") + font.setPointSize(-1) + font.setBold(False) + font.setWeight(37) + self.table_plugin_settings.setFont(font) + self.table_plugin_settings.setStyleSheet("QTableWidget { border: 1px solid #ccc; font-family: Roboto; font-size: 11px; }") + self.table_plugin_settings.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.table_plugin_settings.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) + self.table_plugin_settings.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.table_plugin_settings.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + self.table_plugin_settings.setProperty("showDropIndicator", False) + self.table_plugin_settings.setDragDropOverwriteMode(False) + self.table_plugin_settings.setAlternatingRowColors(True) + self.table_plugin_settings.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) + self.table_plugin_settings.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + self.table_plugin_settings.setTextElideMode(QtCore.Qt.ElideNone) + self.table_plugin_settings.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) + self.table_plugin_settings.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) + self.table_plugin_settings.setShowGrid(False) + self.table_plugin_settings.setGridStyle(QtCore.Qt.NoPen) + self.table_plugin_settings.setWordWrap(True) + self.table_plugin_settings.setCornerButtonEnabled(True) + self.table_plugin_settings.setObjectName("table_plugin_settings") + self.table_plugin_settings.setColumnCount(4) + self.table_plugin_settings.setRowCount(2) + item = QtWidgets.QTableWidgetItem() + item.setTextAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter|QtCore.Qt.AlignCenter) + self.table_plugin_settings.setVerticalHeaderItem(0, item) + item = QtWidgets.QTableWidgetItem() + self.table_plugin_settings.setVerticalHeaderItem(1, item) + item = QtWidgets.QTableWidgetItem() + self.table_plugin_settings.setHorizontalHeaderItem(0, item) + item = QtWidgets.QTableWidgetItem() + item.setTextAlignment(QtCore.Qt.AlignHCenter|QtCore.Qt.AlignVCenter|QtCore.Qt.AlignCenter) + self.table_plugin_settings.setHorizontalHeaderItem(1, item) + item = QtWidgets.QTableWidgetItem() + self.table_plugin_settings.setHorizontalHeaderItem(2, item) + item = QtWidgets.QTableWidgetItem() + self.table_plugin_settings.setHorizontalHeaderItem(3, item) + item = QtWidgets.QTableWidgetItem() + self.table_plugin_settings.setItem(0, 0, item) + item = QtWidgets.QTableWidgetItem() + self.table_plugin_settings.setItem(0, 1, item) + item = QtWidgets.QTableWidgetItem() + self.table_plugin_settings.setItem(0, 2, item) + item = QtWidgets.QTableWidgetItem() + self.table_plugin_settings.setItem(0, 3, item) + item = QtWidgets.QTableWidgetItem() + self.table_plugin_settings.setItem(1, 1, item) + item = QtWidgets.QTableWidgetItem() + self.table_plugin_settings.setItem(1, 2, item) + item = QtWidgets.QTableWidgetItem() + self.table_plugin_settings.setItem(1, 3, item) + self.table_plugin_settings.horizontalHeader().setVisible(True) + self.table_plugin_settings.horizontalHeader().setCascadingSectionResizes(False) + self.table_plugin_settings.horizontalHeader().setHighlightSections(True) + self.table_plugin_settings.horizontalHeader().setSortIndicatorShown(False) + self.table_plugin_settings.horizontalHeader().setStretchLastSection(True) + self.table_plugin_settings.verticalHeader().setVisible(False) + self.table_plugin_settings.verticalHeader().setCascadingSectionResizes(False) + self.table_plugin_settings.verticalHeader().setDefaultSectionSize(45) + self.table_plugin_settings.verticalHeader().setSortIndicatorShown(False) + self.table_plugin_settings.verticalHeader().setStretchLastSection(False) + self.lbl_plugin_name = QtWidgets.QLabel(self.frame_details) + self.lbl_plugin_name.setGeometry(QtCore.QRect(20, 20, 391, 21)) + font = QtGui.QFont() + font.setFamily("Roboto") + font.setPointSize(-1) + font.setBold(False) + font.setWeight(37) + self.lbl_plugin_name.setFont(font) + self.lbl_plugin_name.setStyleSheet("color: #555; border: 0; font-size: 15px;") + self.lbl_plugin_name.setTextFormat(QtCore.Qt.RichText) + self.lbl_plugin_name.setObjectName("lbl_plugin_name") + self.btn_reset_plugin_settings = QtWidgets.QPushButton(self.frame_details) + self.btn_reset_plugin_settings.setGeometry(QtCore.QRect(520, 60, 180, 31)) + self.btn_reset_plugin_settings.setStyleSheet("QPushButton {\n" +"color: #eee;\n" +"background: #666;\n" +"border: 0; \n" +"}\n" +"\n" +"QPushButton:hover {\n" +" background: #555;\n" +"}\n" +"\n" +"QPushButton:pressed {\n" +" background: #111;\n" +"}") + self.btn_reset_plugin_settings.setObjectName("btn_reset_plugin_settings") + self.frame = QtWidgets.QFrame(self.centralwidget) + self.frame.setGeometry(QtCore.QRect(0, 60, 281, 621)) + self.frame.setAutoFillBackground(False) + self.frame.setStyleSheet("QFrame {background: #eee;}") + self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) + self.frame.setFrameShadow(QtWidgets.QFrame.Raised) + self.frame.setObjectName("frame") + self.lbl_hint_restart = QtWidgets.QLabel(self.frame) + self.lbl_hint_restart.setGeometry(QtCore.QRect(20, 560, 251, 51)) + palette = QtGui.QPalette() + brush = QtGui.QBrush(QtGui.QColor(153, 153, 153)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(238, 238, 238)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Button, brush) + brush = QtGui.QBrush(QtGui.QColor(153, 153, 153)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(153, 153, 153)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.ButtonText, brush) + brush = QtGui.QBrush(QtGui.QColor(238, 238, 238)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Base, brush) + brush = QtGui.QBrush(QtGui.QColor(238, 238, 238)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Window, brush) + brush = QtGui.QBrush(QtGui.QColor(153, 153, 153)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(238, 238, 238)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Button, brush) + brush = QtGui.QBrush(QtGui.QColor(153, 153, 153)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(153, 153, 153)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.ButtonText, brush) + brush = QtGui.QBrush(QtGui.QColor(238, 238, 238)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Base, brush) + brush = QtGui.QBrush(QtGui.QColor(238, 238, 238)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, brush) + brush = QtGui.QBrush(QtGui.QColor(153, 153, 153)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(238, 238, 238)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Button, brush) + brush = QtGui.QBrush(QtGui.QColor(153, 153, 153)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(153, 153, 153)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.ButtonText, brush) + brush = QtGui.QBrush(QtGui.QColor(238, 238, 238)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Base, brush) + brush = QtGui.QBrush(QtGui.QColor(238, 238, 238)) + brush.setStyle(QtCore.Qt.SolidPattern) + palette.setBrush(QtGui.QPalette.Disabled, QtGui.QPalette.Window, brush) + self.lbl_hint_restart.setPalette(palette) + font = QtGui.QFont() + font.setFamily("Roboto") + font.setBold(False) + font.setWeight(37) + self.lbl_hint_restart.setFont(font) + self.lbl_hint_restart.setStyleSheet("border-top: 1px dotted #aaa; color: #999; padding: 0;") + self.lbl_hint_restart.setTextFormat(QtCore.Qt.PlainText) + self.lbl_hint_restart.setScaledContents(False) + self.lbl_hint_restart.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) + self.lbl_hint_restart.setWordWrap(True) + self.lbl_hint_restart.setObjectName("lbl_hint_restart") + self.lbl_restart_pipeline = QtWidgets.QLabel(self.centralwidget) + self.lbl_restart_pipeline.setGeometry(QtCore.QRect(687, 18, 294, 28)) + font = QtGui.QFont() + font.setFamily("Roboto") + font.setPointSize(-1) + font.setBold(False) + font.setWeight(37) + self.lbl_restart_pipeline.setFont(font) + self.lbl_restart_pipeline.setAutoFillBackground(False) + self.lbl_restart_pipeline.setStyleSheet("background: #ff9719; color: #eee; padding: 5px; border: 0; font-size: 12px;") + self.lbl_restart_pipeline.setAlignment(QtCore.Qt.AlignCenter) + self.lbl_restart_pipeline.setObjectName("lbl_restart_pipeline") + self.label = QtWidgets.QLabel(self.centralwidget) + self.label.setGeometry(QtCore.QRect(20, 25, 261, 31)) + self.label.setStyleSheet("font-size: 20px; color: #ff9719;\n" +"font-weight: 100;") + self.label.setObjectName("label") + self.label_9 = QtWidgets.QLabel(self.centralwidget) + self.label_9.setGeometry(QtCore.QRect(20, 6, 261, 31)) + self.label_9.setStyleSheet("font-size: 13px;\n" +"color: #aaa;\n" +"font-weight: 600;") + self.label_9.setObjectName("label_9") + self.frame.raise_() + self.lst_plugins.raise_() + self.lbl_restart_pipeline.raise_() + self.frame_details.raise_() + self.label.raise_() + self.label_9.raise_() + MainWindow.setCentralWidget(self.centralwidget) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "Plugin Configurator")) + __sortingEnabled = self.lst_plugins.isSortingEnabled() + self.lst_plugins.setSortingEnabled(False) + item = self.lst_plugins.item(0) + item.setText(_translate("MainWindow", "Ambient Occlusion")) + item = self.lst_plugins.item(1) + item.setText(_translate("MainWindow", "Another Ite")) + item = self.lst_plugins.item(2) + item.setText(_translate("MainWindow", "Bloom")) + item = self.lst_plugins.item(3) + item.setText(_translate("MainWindow", "Volumetric Clouds")) + item = self.lst_plugins.item(4) + item.setText(_translate("MainWindow", "Depth of Field")) + item = self.lst_plugins.item(5) + item.setText(_translate("MainWindow", "Environment Probes")) + item = self.lst_plugins.item(6) + item.setText(_translate("MainWindow", "FXAA (Antialiasing)")) + item = self.lst_plugins.item(7) + item.setText(_translate("MainWindow", "SKAA")) + item = self.lst_plugins.item(8) + item.setText(_translate("MainWindow", "Motion Blur")) + item = self.lst_plugins.item(9) + item.setText(_translate("MainWindow", "PSSM Shadows")) + item = self.lst_plugins.item(10) + item.setText(_translate("MainWindow", "Atmospheric Scattering")) + item = self.lst_plugins.item(11) + item.setText(_translate("MainWindow", "Skin Shading")) + item = self.lst_plugins.item(12) + item.setText(_translate("MainWindow", "SMAA (Antialiasing)")) + item = self.lst_plugins.item(13) + item.setText(_translate("MainWindow", "Screen Space Reflections")) + item = self.lst_plugins.item(14) + item.setText(_translate("MainWindow", "Volumetric Lighting")) + item = self.lst_plugins.item(15) + item.setText(_translate("MainWindow", "VXGI")) + item = self.lst_plugins.item(16) + item.setText(_translate("MainWindow", "Item 346")) + item = self.lst_plugins.item(17) + item.setText(_translate("MainWindow", "Other Item")) + item = self.lst_plugins.item(18) + item.setText(_translate("MainWindow", "Other Item to force scrollbars")) + self.lst_plugins.setSortingEnabled(__sortingEnabled) + self.lbl_plugin_version.setText(_translate("MainWindow", "version 0.1 by some author")) + self.lbl_plugin_desc.setText(_translate("MainWindow", "This is a fancy description\n" +"It shows information about the plugin and maybe\n" +"a website or so.")) + item = self.table_plugin_settings.verticalHeaderItem(0) + item.setText(_translate("MainWindow", "Row")) + item = self.table_plugin_settings.verticalHeaderItem(1) + item.setText(_translate("MainWindow", "Row2")) + item = self.table_plugin_settings.horizontalHeaderItem(0) + item.setText(_translate("MainWindow", "Setting")) + item = self.table_plugin_settings.horizontalHeaderItem(1) + item.setText(_translate("MainWindow", "Default")) + item = self.table_plugin_settings.horizontalHeaderItem(2) + item.setText(_translate("MainWindow", "Current Value")) + item = self.table_plugin_settings.horizontalHeaderItem(3) + item.setText(_translate("MainWindow", "Description")) + __sortingEnabled = self.table_plugin_settings.isSortingEnabled() + self.table_plugin_settings.setSortingEnabled(False) + item = self.table_plugin_settings.item(0, 0) + item.setText(_translate("MainWindow", "Long setting name which requires")) + item = self.table_plugin_settings.item(0, 1) + item.setText(_translate("MainWindow", "DefaultRow0")) + item = self.table_plugin_settings.item(0, 2) + item.setText(_translate("MainWindow", "CurrentRow0")) + item = self.table_plugin_settings.item(0, 3) + item.setText(_translate("MainWindow", "DescRow0")) + item = self.table_plugin_settings.item(1, 1) + item.setText(_translate("MainWindow", "DefaultRow1")) + item = self.table_plugin_settings.item(1, 2) + item.setText(_translate("MainWindow", "CurrentRow1")) + item = self.table_plugin_settings.item(1, 3) + item.setText(_translate("MainWindow", "aasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfgaasd\\n\\nsdf\\n\\ndfdfG\\n\\nfg")) + self.table_plugin_settings.setSortingEnabled(__sortingEnabled) + self.lbl_plugin_name.setText(_translate("MainWindow", "Ambient Occlusion")) + self.btn_reset_plugin_settings.setText(_translate("MainWindow", "Reset Settings of this Plugin")) + self.lbl_hint_restart.setText(_translate("MainWindow", "Hint: Settings with a gray color require a pipeline restart when changed.")) + self.lbl_restart_pipeline.setText(_translate("MainWindow", "Pipeline needs to be restarted to apply all changes!")) + self.label.setText(_translate("MainWindow", "PLUGIN CONFIGURATOR")) + self.label_9.setText(_translate("MainWindow", "RENDER PIPELINE")) + +from . import resources_rc diff --git a/RenderPipelineFile/toolkit/plugin_configurator/ui/resources.qrc b/RenderPipelineFile/toolkit/plugin_configurator/ui/resources.qrc index fe54e414..010f82e5 100644 --- a/RenderPipelineFile/toolkit/plugin_configurator/ui/resources.qrc +++ b/RenderPipelineFile/toolkit/plugin_configurator/ui/resources.qrc @@ -1,9 +1,9 @@ - - - res/pipeline_logo.png - res/icon.png - - - res/plugin_configurator_logo.png - - + + + res/pipeline_logo.png + res/icon.png + + + res/plugin_configurator_logo.png + + diff --git a/RenderPipelineFile/toolkit/plugin_configurator/ui/resources_rc.py b/RenderPipelineFile/toolkit/plugin_configurator/ui/resources_rc.py index 9234557b..83bf33a8 100644 --- a/RenderPipelineFile/toolkit/plugin_configurator/ui/resources_rc.py +++ b/RenderPipelineFile/toolkit/plugin_configurator/ui/resources_rc.py @@ -1,1061 +1,1061 @@ -# -*- coding: utf-8 -*- - -# Resource object code -# -# Created by: The Resource Compiler for PyQt5 (Qt v5.7.0) -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore - -qt_resource_data = b"\ -\x00\x00\x17\xe1\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x46\x00\x00\x00\x46\x08\x06\x00\x00\x00\x71\x2e\xe2\x84\ -\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ -\x01\x00\x9a\x9c\x18\x00\x00\x0a\x4f\x69\x43\x43\x50\x50\x68\x6f\ -\x74\x6f\x73\x68\x6f\x70\x20\x49\x43\x43\x20\x70\x72\x6f\x66\x69\ -\x6c\x65\x00\x00\x78\xda\x9d\x53\x67\x54\x53\xe9\x16\x3d\xf7\xde\ -\xf4\x42\x4b\x88\x80\x94\x4b\x6f\x52\x15\x08\x20\x52\x42\x8b\x80\ -\x14\x91\x26\x2a\x21\x09\x10\x4a\x88\x21\xa1\xd9\x15\x51\xc1\x11\ -\x45\x45\x04\x1b\xc8\xa0\x88\x03\x8e\x8e\x80\x8c\x15\x51\x2c\x0c\ -\x8a\x0a\xd8\x07\xe4\x21\xa2\x8e\x83\xa3\x88\x8a\xca\xfb\xe1\x7b\ -\xa3\x6b\xd6\xbc\xf7\xe6\xcd\xfe\xb5\xd7\x3e\xe7\xac\xf3\x9d\xb3\ -\xcf\x07\xc0\x08\x0c\x96\x48\x33\x51\x35\x80\x0c\xa9\x42\x1e\x11\ -\xe0\x83\xc7\xc4\xc6\xe1\xe4\x2e\x40\x81\x0a\x24\x70\x00\x10\x08\ -\xb3\x64\x21\x73\xfd\x23\x01\x00\xf8\x7e\x3c\x3c\x2b\x22\xc0\x07\ -\xbe\x00\x01\x78\xd3\x0b\x08\x00\xc0\x4d\x9b\xc0\x30\x1c\x87\xff\ -\x0f\xea\x42\x99\x5c\x01\x80\x84\x01\xc0\x74\x91\x38\x4b\x08\x80\ -\x14\x00\x40\x7a\x8e\x42\xa6\x00\x40\x46\x01\x80\x9d\x98\x26\x53\ -\x00\xa0\x04\x00\x60\xcb\x63\x62\xe3\x00\x50\x2d\x00\x60\x27\x7f\ -\xe6\xd3\x00\x80\x9d\xf8\x99\x7b\x01\x00\x5b\x94\x21\x15\x01\xa0\ -\x91\x00\x20\x13\x65\x88\x44\x00\x68\x3b\x00\xac\xcf\x56\x8a\x45\ -\x00\x58\x30\x00\x14\x66\x4b\xc4\x39\x00\xd8\x2d\x00\x30\x49\x57\ -\x66\x48\x00\xb0\xb7\x00\xc0\xce\x10\x0b\xb2\x00\x08\x0c\x00\x30\ -\x51\x88\x85\x29\x00\x04\x7b\x00\x60\xc8\x23\x23\x78\x00\x84\x99\ -\x00\x14\x46\xf2\x57\x3c\xf1\x2b\xae\x10\xe7\x2a\x00\x00\x78\x99\ -\xb2\x3c\xb9\x24\x39\x45\x81\x5b\x08\x2d\x71\x07\x57\x57\x2e\x1e\ -\x28\xce\x49\x17\x2b\x14\x36\x61\x02\x61\x9a\x40\x2e\xc2\x79\x99\ -\x19\x32\x81\x34\x0f\xe0\xf3\xcc\x00\x00\xa0\x91\x15\x11\xe0\x83\ -\xf3\xfd\x78\xce\x0e\xae\xce\xce\x36\x8e\xb6\x0e\x5f\x2d\xea\xbf\ -\x06\xff\x22\x62\x62\xe3\xfe\xe5\xcf\xab\x70\x40\x00\x00\xe1\x74\ -\x7e\xd1\xfe\x2c\x2f\xb3\x1a\x80\x3b\x06\x80\x6d\xfe\xa2\x25\xee\ -\x04\x68\x5e\x0b\xa0\x75\xf7\x8b\x66\xb2\x0f\x40\xb5\x00\xa0\xe9\ -\xda\x57\xf3\x70\xf8\x7e\x3c\x3c\x45\xa1\x90\xb9\xd9\xd9\xe5\xe4\ -\xe4\xd8\x4a\xc4\x42\x5b\x61\xca\x57\x7d\xfe\x67\xc2\x5f\xc0\x57\ -\xfd\x6c\xf9\x7e\x3c\xfc\xf7\xf5\xe0\xbe\xe2\x24\x81\x32\x5d\x81\ -\x47\x04\xf8\xe0\xc2\xcc\xf4\x4c\xa5\x1c\xcf\x92\x09\x84\x62\xdc\ -\xe6\x8f\x47\xfc\xb7\x0b\xff\xfc\x1d\xd3\x22\xc4\x49\x62\xb9\x58\ -\x2a\x14\xe3\x51\x12\x71\x8e\x44\x9a\x8c\xf3\x32\xa5\x22\x89\x42\ -\x92\x29\xc5\x25\xd2\xff\x64\xe2\xdf\x2c\xfb\x03\x3e\xdf\x35\x00\ -\xb0\x6a\x3e\x01\x7b\x91\x2d\xa8\x5d\x63\x03\xf6\x4b\x27\x10\x58\ -\x74\xc0\xe2\xf7\x00\x00\xf2\xbb\x6f\xc1\xd4\x28\x08\x03\x80\x68\ -\x83\xe1\xcf\x77\xff\xef\x3f\xfd\x47\xa0\x25\x00\x80\x66\x49\x92\ -\x71\x00\x00\x5e\x44\x24\x2e\x54\xca\xb3\x3f\xc7\x08\x00\x00\x44\ -\xa0\x81\x2a\xb0\x41\x1b\xf4\xc1\x18\x2c\xc0\x06\x1c\xc1\x05\xdc\ -\xc1\x0b\xfc\x60\x36\x84\x42\x24\xc4\xc2\x42\x10\x42\x0a\x64\x80\ -\x1c\x72\x60\x29\xac\x82\x42\x28\x86\xcd\xb0\x1d\x2a\x60\x2f\xd4\ -\x40\x1d\x34\xc0\x51\x68\x86\x93\x70\x0e\x2e\xc2\x55\xb8\x0e\x3d\ -\x70\x0f\xfa\x61\x08\x9e\xc1\x28\xbc\x81\x09\x04\x41\xc8\x08\x13\ -\x61\x21\xda\x88\x01\x62\x8a\x58\x23\x8e\x08\x17\x99\x85\xf8\x21\ -\xc1\x48\x04\x12\x8b\x24\x20\xc9\x88\x14\x51\x22\x4b\x91\x35\x48\ -\x31\x52\x8a\x54\x20\x55\x48\x1d\xf2\x3d\x72\x02\x39\x87\x5c\x46\ -\xba\x91\x3b\xc8\x00\x32\x82\xfc\x86\xbc\x47\x31\x94\x81\xb2\x51\ -\x3d\xd4\x0c\xb5\x43\xb9\xa8\x37\x1a\x84\x46\xa2\x0b\xd0\x64\x74\ -\x31\x9a\x8f\x16\xa0\x9b\xd0\x72\xb4\x1a\x3d\x8c\x36\xa1\xe7\xd0\ -\xab\x68\x0f\xda\x8f\x3e\x43\xc7\x30\xc0\xe8\x18\x07\x33\xc4\x6c\ -\x30\x2e\xc6\xc3\x42\xb1\x38\x2c\x09\x93\x63\xcb\xb1\x22\xac\x0c\ -\xab\xc6\x1a\xb0\x56\xac\x03\xbb\x89\xf5\x63\xcf\xb1\x77\x04\x12\ -\x81\x45\xc0\x09\x36\x04\x77\x42\x20\x61\x1e\x41\x48\x58\x4c\x58\ -\x4e\xd8\x48\xa8\x20\x1c\x24\x34\x11\xda\x09\x37\x09\x03\x84\x51\ -\xc2\x27\x22\x93\xa8\x4b\xb4\x26\xba\x11\xf9\xc4\x18\x62\x32\x31\ -\x87\x58\x48\x2c\x23\xd6\x12\x8f\x13\x2f\x10\x7b\x88\x43\xc4\x37\ -\x24\x12\x89\x43\x32\x27\xb9\x90\x02\x49\xb1\xa4\x54\xd2\x12\xd2\ -\x46\xd2\x6e\x52\x23\xe9\x2c\xa9\x9b\x34\x48\x1a\x23\x93\xc9\xda\ -\x64\x6b\xb2\x07\x39\x94\x2c\x20\x2b\xc8\x85\xe4\x9d\xe4\xc3\xe4\ -\x33\xe4\x1b\xe4\x21\xf2\x5b\x0a\x9d\x62\x40\x71\xa4\xf8\x53\xe2\ -\x28\x52\xca\x6a\x4a\x19\xe5\x10\xe5\x34\xe5\x06\x65\x98\x32\x41\ -\x55\xa3\x9a\x52\xdd\xa8\xa1\x54\x11\x35\x8f\x5a\x42\xad\xa1\xb6\ -\x52\xaf\x51\x87\xa8\x13\x34\x75\x9a\x39\xcd\x83\x16\x49\x4b\xa5\ -\xad\xa2\x95\xd3\x1a\x68\x17\x68\xf7\x69\xaf\xe8\x74\xba\x11\xdd\ -\x95\x1e\x4e\x97\xd0\x57\xd2\xcb\xe9\x47\xe8\x97\xe8\x03\xf4\x77\ -\x0c\x0d\x86\x15\x83\xc7\x88\x67\x28\x19\x9b\x18\x07\x18\x67\x19\ -\x77\x18\xaf\x98\x4c\xa6\x19\xd3\x8b\x19\xc7\x54\x30\x37\x31\xeb\ -\x98\xe7\x99\x0f\x99\x6f\x55\x58\x2a\xb6\x2a\x7c\x15\x91\xca\x0a\ -\x95\x4a\x95\x26\x95\x1b\x2a\x2f\x54\xa9\xaa\xa6\xaa\xde\xaa\x0b\ -\x55\xf3\x55\xcb\x54\x8f\xa9\x5e\x53\x7d\xae\x46\x55\x33\x53\xe3\ -\xa9\x09\xd4\x96\xab\x55\xaa\x9d\x50\xeb\x53\x1b\x53\x67\xa9\x3b\ -\xa8\x87\xaa\x67\xa8\x6f\x54\x3f\xa4\x7e\x59\xfd\x89\x06\x59\xc3\ -\x4c\xc3\x4f\x43\xa4\x51\xa0\xb1\x5f\xe3\xbc\xc6\x20\x0b\x63\x19\ -\xb3\x78\x2c\x21\x6b\x0d\xab\x86\x75\x81\x35\xc4\x26\xb1\xcd\xd9\ -\x7c\x76\x2a\xbb\x98\xfd\x1d\xbb\x8b\x3d\xaa\xa9\xa1\x39\x43\x33\ -\x4a\x33\x57\xb3\x52\xf3\x94\x66\x3f\x07\xe3\x98\x71\xf8\x9c\x74\ -\x4e\x09\xe7\x28\xa7\x97\xf3\x7e\x8a\xde\x14\xef\x29\xe2\x29\x1b\ -\xa6\x34\x4c\xb9\x31\x65\x5c\x6b\xaa\x96\x97\x96\x58\xab\x48\xab\ -\x51\xab\x47\xeb\xbd\x36\xae\xed\xa7\x9d\xa6\xbd\x45\xbb\x59\xfb\ -\x81\x0e\x41\xc7\x4a\x27\x5c\x27\x47\x67\x8f\xce\x05\x9d\xe7\x53\ -\xd9\x53\xdd\xa7\x0a\xa7\x16\x4d\x3d\x3a\xf5\xae\x2e\xaa\x6b\xa5\ -\x1b\xa1\xbb\x44\x77\xbf\x6e\xa7\xee\x98\x9e\xbe\x5e\x80\x9e\x4c\ -\x6f\xa7\xde\x79\xbd\xe7\xfa\x1c\x7d\x2f\xfd\x54\xfd\x6d\xfa\xa7\ -\xf5\x47\x0c\x58\x06\xb3\x0c\x24\x06\xdb\x0c\xce\x18\x3c\xc5\x35\ -\x71\x6f\x3c\x1d\x2f\xc7\xdb\xf1\x51\x43\x5d\xc3\x40\x43\xa5\x61\ -\x95\x61\x97\xe1\x84\x91\xb9\xd1\x3c\xa3\xd5\x46\x8d\x46\x0f\x8c\ -\x69\xc6\x5c\xe3\x24\xe3\x6d\xc6\x6d\xc6\xa3\x26\x06\x26\x21\x26\ -\x4b\x4d\xea\x4d\xee\x9a\x52\x4d\xb9\xa6\x29\xa6\x3b\x4c\x3b\x4c\ -\xc7\xcd\xcc\xcd\xa2\xcd\xd6\x99\x35\x9b\x3d\x31\xd7\x32\xe7\x9b\ -\xe7\x9b\xd7\x9b\xdf\xb7\x60\x5a\x78\x5a\x2c\xb6\xa8\xb6\xb8\x65\ -\x49\xb2\xe4\x5a\xa6\x59\xee\xb6\xbc\x6e\x85\x5a\x39\x59\xa5\x58\ -\x55\x5a\x5d\xb3\x46\xad\x9d\xad\x25\xd6\xbb\xad\xbb\xa7\x11\xa7\ -\xb9\x4e\x93\x4e\xab\x9e\xd6\x67\xc3\xb0\xf1\xb6\xc9\xb6\xa9\xb7\ -\x19\xb0\xe5\xd8\x06\xdb\xae\xb6\x6d\xb6\x7d\x61\x67\x62\x17\x67\ -\xb7\xc5\xae\xc3\xee\x93\xbd\x93\x7d\xba\x7d\x8d\xfd\x3d\x07\x0d\ -\x87\xd9\x0e\xab\x1d\x5a\x1d\x7e\x73\xb4\x72\x14\x3a\x56\x3a\xde\ -\x9a\xce\x9c\xee\x3f\x7d\xc5\xf4\x96\xe9\x2f\x67\x58\xcf\x10\xcf\ -\xd8\x33\xe3\xb6\x13\xcb\x29\xc4\x69\x9d\x53\x9b\xd3\x47\x67\x17\ -\x67\xb9\x73\x83\xf3\x88\x8b\x89\x4b\x82\xcb\x2e\x97\x3e\x2e\x9b\ -\x1b\xc6\xdd\xc8\xbd\xe4\x4a\x74\xf5\x71\x5d\xe1\x7a\xd2\xf5\x9d\ -\x9b\xb3\x9b\xc2\xed\xa8\xdb\xaf\xee\x36\xee\x69\xee\x87\xdc\x9f\ -\xcc\x34\x9f\x29\x9e\x59\x33\x73\xd0\xc3\xc8\x43\xe0\x51\xe5\xd1\ -\x3f\x0b\x9f\x95\x30\x6b\xdf\xac\x7e\x4f\x43\x4f\x81\x67\xb5\xe7\ -\x23\x2f\x63\x2f\x91\x57\xad\xd7\xb0\xb7\xa5\x77\xaa\xf7\x61\xef\ -\x17\x3e\xf6\x3e\x72\x9f\xe3\x3e\xe3\x3c\x37\xde\x32\xde\x59\x5f\ -\xcc\x37\xc0\xb7\xc8\xb7\xcb\x4f\xc3\x6f\x9e\x5f\x85\xdf\x43\x7f\ -\x23\xff\x64\xff\x7a\xff\xd1\x00\xa7\x80\x25\x01\x67\x03\x89\x81\ -\x41\x81\x5b\x02\xfb\xf8\x7a\x7c\x21\xbf\x8e\x3f\x3a\xdb\x65\xf6\ -\xb2\xd9\xed\x41\x8c\xa0\xb9\x41\x15\x41\x8f\x82\xad\x82\xe5\xc1\ -\xad\x21\x68\xc8\xec\x90\xad\x21\xf7\xe7\x98\xce\x91\xce\x69\x0e\ -\x85\x50\x7e\xe8\xd6\xd0\x07\x61\xe6\x61\x8b\xc3\x7e\x0c\x27\x85\ -\x87\x85\x57\x86\x3f\x8e\x70\x88\x58\x1a\xd1\x31\x97\x35\x77\xd1\ -\xdc\x43\x73\xdf\x44\xfa\x44\x96\x44\xde\x9b\x67\x31\x4f\x39\xaf\ -\x2d\x4a\x35\x2a\x3e\xaa\x2e\x6a\x3c\xda\x37\xba\x34\xba\x3f\xc6\ -\x2e\x66\x59\xcc\xd5\x58\x9d\x58\x49\x6c\x4b\x1c\x39\x2e\x2a\xae\ -\x36\x6e\x6c\xbe\xdf\xfc\xed\xf3\x87\xe2\x9d\xe2\x0b\xe3\x7b\x17\ -\x98\x2f\xc8\x5d\x70\x79\xa1\xce\xc2\xf4\x85\xa7\x16\xa9\x2e\x12\ -\x2c\x3a\x96\x40\x4c\x88\x4e\x38\x94\xf0\x41\x10\x2a\xa8\x16\x8c\ -\x25\xf2\x13\x77\x25\x8e\x0a\x79\xc2\x1d\xc2\x67\x22\x2f\xd1\x36\ -\xd1\x88\xd8\x43\x5c\x2a\x1e\x4e\xf2\x48\x2a\x4d\x7a\x92\xec\x91\ -\xbc\x35\x79\x24\xc5\x33\xa5\x2c\xe5\xb9\x84\x27\xa9\x90\xbc\x4c\ -\x0d\x4c\xdd\x9b\x3a\x9e\x16\x9a\x76\x20\x6d\x32\x3d\x3a\xbd\x31\ -\x83\x92\x91\x90\x71\x42\xaa\x21\x4d\x93\xb6\x67\xea\x67\xe6\x66\ -\x76\xcb\xac\x65\x85\xb2\xfe\xc5\x6e\x8b\xb7\x2f\x1e\x95\x07\xc9\ -\x6b\xb3\x90\xac\x05\x59\x2d\x0a\xb6\x42\xa6\xe8\x54\x5a\x28\xd7\ -\x2a\x07\xb2\x67\x65\x57\x66\xbf\xcd\x89\xca\x39\x96\xab\x9e\x2b\ -\xcd\xed\xcc\xb3\xca\xdb\x90\x37\x9c\xef\x9f\xff\xed\x12\xc2\x12\ -\xe1\x92\xb6\xa5\x86\x4b\x57\x2d\x1d\x58\xe6\xbd\xac\x6a\x39\xb2\ -\x3c\x71\x79\xdb\x0a\xe3\x15\x05\x2b\x86\x56\x06\xac\x3c\xb8\x8a\ -\xb6\x2a\x6d\xd5\x4f\xab\xed\x57\x97\xae\x7e\xbd\x26\x7a\x4d\x6b\ -\x81\x5e\xc1\xca\x82\xc1\xb5\x01\x6b\xeb\x0b\x55\x0a\xe5\x85\x7d\ -\xeb\xdc\xd7\xed\x5d\x4f\x58\x2f\x59\xdf\xb5\x61\xfa\x86\x9d\x1b\ -\x3e\x15\x89\x8a\xae\x14\xdb\x17\x97\x15\x7f\xd8\x28\xdc\x78\xe5\ -\x1b\x87\x6f\xca\xbf\x99\xdc\x94\xb4\xa9\xab\xc4\xb9\x64\xcf\x66\ -\xd2\x66\xe9\xe6\xde\x2d\x9e\x5b\x0e\x96\xaa\x97\xe6\x97\x0e\x6e\ -\x0d\xd9\xda\xb4\x0d\xdf\x56\xb4\xed\xf5\xf6\x45\xdb\x2f\x97\xcd\ -\x28\xdb\xbb\x83\xb6\x43\xb9\xa3\xbf\x3c\xb8\xbc\x65\xa7\xc9\xce\ -\xcd\x3b\x3f\x54\xa4\x54\xf4\x54\xfa\x54\x36\xee\xd2\xdd\xb5\x61\ -\xd7\xf8\x6e\xd1\xee\x1b\x7b\xbc\xf6\x34\xec\xd5\xdb\x5b\xbc\xf7\ -\xfd\x3e\xc9\xbe\xdb\x55\x01\x55\x4d\xd5\x66\xd5\x65\xfb\x49\xfb\ -\xb3\xf7\x3f\xae\x89\xaa\xe9\xf8\x96\xfb\x6d\x5d\xad\x4e\x6d\x71\ -\xed\xc7\x03\xd2\x03\xfd\x07\x23\x0e\xb6\xd7\xb9\xd4\xd5\x1d\xd2\ -\x3d\x54\x52\x8f\xd6\x2b\xeb\x47\x0e\xc7\x1f\xbe\xfe\x9d\xef\x77\ -\x2d\x0d\x36\x0d\x55\x8d\x9c\xc6\xe2\x23\x70\x44\x79\xe4\xe9\xf7\ -\x09\xdf\xf7\x1e\x0d\x3a\xda\x76\x8c\x7b\xac\xe1\x07\xd3\x1f\x76\ -\x1d\x67\x1d\x2f\x6a\x42\x9a\xf2\x9a\x46\x9b\x53\x9a\xfb\x5b\x62\ -\x5b\xba\x4f\xcc\x3e\xd1\xd6\xea\xde\x7a\xfc\x47\xdb\x1f\x0f\x9c\ -\x34\x3c\x59\x79\x4a\xf3\x54\xc9\x69\xda\xe9\x82\xd3\x93\x67\xf2\ -\xcf\x8c\x9d\x95\x9d\x7d\x7e\x2e\xf9\xdc\x60\xdb\xa2\xb6\x7b\xe7\ -\x63\xce\xdf\x6a\x0f\x6f\xef\xba\x10\x74\xe1\xd2\x45\xff\x8b\xe7\ -\x3b\xbc\x3b\xce\x5c\xf2\xb8\x74\xf2\xb2\xdb\xe5\x13\x57\xb8\x57\ -\x9a\xaf\x3a\x5f\x6d\xea\x74\xea\x3c\xfe\x93\xd3\x4f\xc7\xbb\x9c\ -\xbb\x9a\xae\xb9\x5c\x6b\xb9\xee\x7a\xbd\xb5\x7b\x66\xf7\xe9\x1b\ -\x9e\x37\xce\xdd\xf4\xbd\x79\xf1\x16\xff\xd6\xd5\x9e\x39\x3d\xdd\ -\xbd\xf3\x7a\x6f\xf7\xc5\xf7\xf5\xdf\x16\xdd\x7e\x72\x27\xfd\xce\ -\xcb\xbb\xd9\x77\x27\xee\xad\xbc\x4f\xbc\x5f\xf4\x40\xed\x41\xd9\ -\x43\xdd\x87\xd5\x3f\x5b\xfe\xdc\xd8\xef\xdc\x7f\x6a\xc0\x77\xa0\ -\xf3\xd1\xdc\x47\xf7\x06\x85\x83\xcf\xfe\x91\xf5\x8f\x0f\x43\x05\ -\x8f\x99\x8f\xcb\x86\x0d\x86\xeb\x9e\x38\x3e\x39\x39\xe2\x3f\x72\ -\xfd\xe9\xfc\xa7\x43\xcf\x64\xcf\x26\x9e\x17\xfe\xa2\xfe\xcb\xae\ -\x17\x16\x2f\x7e\xf8\xd5\xeb\xd7\xce\xd1\x98\xd1\xa1\x97\xf2\x97\ -\x93\xbf\x6d\x7c\xa5\xfd\xea\xc0\xeb\x19\xaf\xdb\xc6\xc2\xc6\x1e\ -\xbe\xc9\x78\x33\x31\x5e\xf4\x56\xfb\xed\xc1\x77\xdc\x77\x1d\xef\ -\xa3\xdf\x0f\x4f\xe4\x7c\x20\x7f\x28\xff\x68\xf9\xb1\xf5\x53\xd0\ -\xa7\xfb\x93\x19\x93\x93\xff\x04\x03\x98\xf3\xfc\x63\x33\x2d\xdb\ -\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\x25\x00\x00\x80\x83\ -\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00\x75\x30\x00\x00\xea\x60\ -\x00\x00\x3a\x98\x00\x00\x17\x6f\x92\x5f\xc5\x46\x00\x00\x0d\x0c\ -\x49\x44\x41\x54\x78\xda\xec\x9c\x7d\x70\x5c\xd5\x79\xc6\x7f\xe7\ -\xdc\xbb\xbb\x77\xbf\xbc\x5a\xc9\xb2\x8c\x31\xc6\xc6\x60\x6c\x62\ -\x13\x8c\x09\xb8\x21\x34\x04\x4c\xec\x60\xe3\xa4\x8d\x29\x6e\x61\ -\xa6\xb4\x49\xda\xd0\x4c\x9a\xe2\x36\x1d\xca\x30\x43\x4a\x81\x69\ -\xd2\xc9\x1f\x09\x90\xc1\xa4\x9d\x7c\x0c\x13\x3e\x12\xf3\x61\xc0\ -\xb2\x63\x17\x9c\x2a\xc4\x01\x83\xb1\xb1\x94\x20\x6c\xab\x76\x2d\ -\xdb\xb2\xf5\xb1\x5a\x69\xb5\xbb\x77\xf7\xde\x7b\xfa\xc7\xbd\x6b\ -\x7d\xee\x6a\x25\xaf\x84\xe2\xd1\x33\x73\xc6\x2b\xef\xbd\xe7\x9c\ -\xfb\xdc\xf7\x7d\xcf\xfb\x3e\xe7\x48\x42\x29\xc5\x34\x86\x43\x4e\ -\x53\x30\x4d\xcc\x34\x31\xd3\xc4\x4c\x13\x33\x4d\xcc\x94\x81\x5e\ -\xea\xcb\xa5\x4b\x97\x56\x7a\xbc\x38\x70\x39\xf0\x31\x60\x31\x70\ -\x31\x30\x0b\xa8\xf3\xbe\x13\xc0\x64\xe4\x0f\x01\xe0\x64\x63\x63\ -\xe3\xc7\xc6\x45\x4c\x05\xad\xf2\x0b\xc0\x17\x81\x6b\x80\x45\x53\ -\xc4\x28\x8c\x71\x5b\xcc\x39\x62\x2e\xb0\x11\xb8\xdb\xb3\x90\xa9\ -\x86\xee\xc9\x26\x26\x00\xfc\x2d\xf0\x2f\xc0\xec\x29\x1c\x46\xc4\ -\x64\x12\xb3\x16\xf8\x0f\x60\xc9\xf4\xaa\xd4\xcf\xfe\x77\x80\x57\ -\xcf\x07\x52\x2a\x65\x31\x71\xe0\x19\x60\xf5\x18\xee\xc9\x03\x47\ -\x81\xff\x05\xce\x00\x6d\x40\x87\xb7\x22\xd5\x78\x2e\x58\x0b\x2c\ -\xf0\x5a\xe0\x0f\x8d\x98\x39\xc0\x4b\xc0\x27\xca\xbc\xfe\xb7\xc0\ -\x76\xe0\x65\xa0\x19\xc8\x94\xb1\x72\x2c\x04\x6e\xf3\xdc\xf4\x53\ -\x7f\x08\xc4\x5c\x02\xfc\x0f\x70\x61\x19\xd7\xd6\x03\x8f\x03\xdb\ -\xc6\x38\x46\x16\x68\xf2\xda\xbf\x03\x37\x01\xf7\x00\x1b\xa6\x6a\ -\x8c\x99\x01\xbc\x58\x06\x29\xfb\x80\x35\xc0\xad\xe3\x20\x65\x24\ -\xbc\x0e\xdc\x0e\xfc\xb1\xf7\x79\x4a\x11\xa3\x7b\xa4\x5c\x39\xca\ -\x75\x0f\x7a\x2e\xb6\xe3\x9c\x27\x29\x20\xe7\x80\xe9\xb8\x9f\x81\ -\x06\xe0\x66\xe0\x1f\xa6\x92\x2b\x3d\xe8\x99\x74\x31\x38\xc0\x5d\ -\xc0\x33\x3d\x7d\x29\x6c\xdb\x41\x88\xfe\x94\xc1\x71\x1c\x22\xa1\ -\x10\x46\x20\x80\xe3\x38\x25\xc9\x68\x33\x15\x59\x5b\x91\x57\x30\ -\x2f\x28\xf1\x0b\x38\xd4\xe7\xe0\x13\x30\x3b\x20\x09\x6a\x7c\xcf\ -\x56\x1c\x02\x9e\x05\xa2\x1f\x25\x31\xab\x81\x07\x4a\x7c\xdf\x23\ -\x84\xd8\x90\xc9\x66\x77\x9a\xf9\x1c\x2b\x96\x2c\x25\x1c\x0a\x61\ -\x59\x56\x7f\x34\x0d\x04\xf8\xdd\x91\xc3\x1c\x3b\x75\x82\x68\x38\ -\x8c\x4f\xf7\x81\x52\x28\xc0\x16\x1a\x42\x08\xf2\x4a\xd1\x91\x53\ -\x6c\xbc\xc0\xc7\x45\x41\x41\x9b\xa9\x58\x35\x53\x23\x28\x05\x2f\ -\x9f\xb6\xb8\x20\x20\x78\xfe\x94\xc5\x87\x29\x87\xba\x80\xd8\x16\ -\xd5\xc5\xcd\xca\x0d\xe8\x17\x7c\x14\xc4\x68\xc0\xf7\x8b\xbe\x61\ -\x29\x73\x59\xd3\x5c\xdf\x95\x4c\xfe\x6a\xf9\x92\x2b\xb8\xe1\xea\ -\x6b\x98\x3f\x67\x2e\xa1\xa0\x81\x65\xdb\xfd\x69\xb1\xcf\xcf\xb2\ -\xcb\x16\xd1\xdd\x9b\xe2\xc3\xa3\x2d\xbc\x7f\xe8\x03\x42\xc1\x10\ -\x02\x85\xee\x58\xa4\x2c\xc5\xb2\xa8\xc6\xdf\xcc\xf3\x71\x63\x8d\ -\x8e\x61\x08\xd7\x06\x2d\x85\xa3\xe0\xd6\x39\x3e\x10\xb0\xba\xd6\ -\xa2\xc7\x56\xfc\xf8\x78\x9e\x57\xce\xd8\x7b\x17\x84\xc4\x2a\xa5\ -\x78\x13\xa8\xaa\x48\x62\x56\x4a\x0c\x1f\x52\x5d\x7f\xbd\x18\x31\ -\x42\x08\x27\x63\x9a\xb7\x1b\x7e\xff\x0b\x97\x2f\xb8\x84\x9b\xae\ -\x5d\xc9\xec\x9a\x5a\xba\x7a\x92\xd8\xb6\x3d\xcc\x95\xc2\xc1\x20\ -\xa1\x60\x90\x64\x6f\x2f\xbd\x7d\x29\x2c\x7f\x88\xa0\x95\x61\xdd\ -\xf1\x6d\xf8\xf2\x19\x74\xbf\xc1\xac\x90\x20\x65\x3a\x98\xf6\xd9\ -\xb8\xd2\xdf\x87\x82\x9a\x80\x00\x9f\xa0\x2b\xed\xf0\x97\xef\x65\ -\x69\x48\xd8\x5c\x1c\x14\x37\xd9\x8a\xd7\x46\x2b\x10\x3d\x9c\x6e\ -\x6c\x6c\x9c\x7d\xae\xc1\xb7\xd6\x8b\x2d\x23\x22\x97\xcf\x3f\x2c\ -\x85\x78\xe1\x8e\x35\x6b\xb9\x73\xed\x7a\x0c\x7f\x80\xf6\x44\x17\ -\x8e\xe3\x68\x62\x20\x2b\xae\x65\x91\x31\x4d\x3a\x12\x09\x1c\xa5\ -\x88\x45\xa2\x84\x82\x21\x22\xc1\x20\x35\x7e\xc9\x9c\x00\x84\x35\ -\xe8\xca\x38\xe4\x9d\xe1\xa4\x14\xe2\x4f\x22\xa7\xe8\xea\x73\x88\ -\xfb\x04\xcf\xac\x30\xf8\x54\x5c\xa3\x25\xad\x5e\x07\xfe\x69\x32\ -\x57\xa5\xbf\xf6\x32\xd2\x91\x5c\x68\x7f\x2a\x93\x7e\xf0\xd2\x79\ -\x17\xb3\xf0\xa2\x79\x9c\xe9\xec\x0c\x58\xb6\xfd\x25\x21\xc4\x76\ -\xe0\xf7\x5e\x7b\xc5\xab\xb4\xe5\x00\x2b\xc3\x71\x1c\xf2\x56\x1e\ -\xcb\xb2\xc8\x5b\x16\x59\x5b\x91\xb6\xdd\xd5\x47\x8c\x9c\x61\x7f\ -\x19\xd8\x0a\xbc\x05\xec\x16\xf0\x48\x97\xa9\x2e\x0f\x49\xc1\xb3\ -\xcb\x0d\x6e\xac\xd6\x68\xcd\x3a\x4f\xe8\x82\x9d\x93\x45\xcc\x97\ -\x8a\xb8\x10\x7d\x99\xf4\xa6\xcb\xe7\x2f\xe0\x8e\xd5\xb7\x62\xe6\ -\x72\xcb\x80\xdd\xc0\x7f\x7a\x81\xfa\x32\x4f\x98\x5a\xe7\x95\x0d\ -\xff\xed\xfd\x3c\x56\xac\x02\xde\x04\x7e\xe8\x65\xc1\xd7\x02\x9f\ -\x06\xee\x97\x82\xfd\x89\x9c\x7a\x24\xa2\x0b\x9e\x5e\x6e\x70\x43\ -\x5c\xe3\xb4\xa9\xbe\x29\x26\x81\x98\xf5\xde\x03\x0e\x43\x3a\x9b\ -\x79\xa3\xae\x66\xe6\x1b\x77\xaf\xff\x53\x7c\xba\x3e\x37\x6b\x9a\ -\x3b\x85\x10\x2b\x4b\xf4\x75\xa3\xf7\xc6\x6b\xc6\x30\xc7\xcf\x00\ -\x3b\x4b\x14\xa7\x86\x14\xdc\xdf\x6e\xaa\xef\xc6\x0c\xc1\xdd\x73\ -\xfd\x28\x38\x60\xbb\x4b\xf8\x84\x12\x73\x57\x11\x6b\x51\xe9\x4c\ -\xf6\x51\xdb\x76\xd0\x75\x9d\x64\x2a\xf5\x03\x29\x65\x5d\x19\xfd\ -\x2d\x02\xbe\x55\xe6\xfc\x22\xc0\x8f\xca\x5a\x5e\x25\x9b\xba\xd2\ -\xea\x96\x2f\xd4\xe9\x7c\x32\xae\xd1\x95\x53\x8f\x02\xe6\x44\x11\ -\x53\x55\xb4\x40\x54\x6a\x9f\xa6\xc9\x5d\x2b\xaf\xbc\x8a\xbc\x65\ -\x5f\x27\xa5\xbc\x6d\x0c\xe3\xde\x03\xcc\x2f\xe3\xba\xaf\xe2\xea\ -\xc2\xe5\x8a\x1f\x0f\x23\x20\xa2\x81\x80\x83\xc0\x1b\x13\x95\xc7\ -\x2c\x2e\xf6\x00\x66\x2e\xb7\x75\xe1\xdc\x79\x7c\x7c\xf1\x12\x94\ -\xe3\x8c\xb5\xea\xd5\xbc\xb2\xa2\xa5\xc4\x1c\x2c\xcf\xf5\xc6\x22\ -\x0a\x5d\x01\x5c\xf2\x8d\x4b\x02\x2d\x7b\x93\x19\x6c\xc5\xcb\x9a\ -\x60\xcd\x44\x10\x53\x6c\x9b\xc0\xee\xcb\x66\xb7\xac\xfc\xf8\x72\ -\x62\xe1\x08\x1d\xdd\x89\x99\xe3\x18\xfb\x2a\xaf\x55\x12\x06\x30\ -\x77\x86\x4e\x8b\x74\x6b\x93\x17\x81\x6f\x7b\x45\x6f\x45\x5d\xa9\ -\x58\xc0\x6b\x91\x42\x7c\x90\x31\xb3\x58\xb6\x05\xd0\x33\xce\x07\ -\x49\x79\xd2\xc2\x48\x2d\x35\x8e\xfe\x72\x40\x2a\xe7\x9c\xdd\x83\ -\x39\x0d\x34\x4e\x84\xc5\xcc\x2b\x46\x8c\x52\xca\xf6\xe9\x3a\x52\ -\x4a\x94\x52\xef\x0e\xc9\xe3\xca\xc1\x9d\x5e\x8e\xe3\x2f\xf1\x90\ -\xdf\x04\xfe\x7c\x0c\x7d\x76\x79\x7d\x0e\xc4\x51\xe0\x93\x95\x26\ -\x66\x56\x91\xff\x6f\xd7\xa4\xa4\xa7\x2f\x45\x2e\x9f\x47\x4a\xf9\ -\xba\x52\xea\xfd\x32\xa4\x88\x02\xb6\x02\x3f\x2b\xe3\xba\x47\xc7\ -\x48\xcc\x0f\x81\x4c\xb5\x4f\xa0\x09\xb0\x5c\xb3\x39\x3c\x11\xab\ -\x52\xb1\xe5\xf7\x64\x38\x14\xe2\x37\xef\xed\xa3\x2b\x99\x24\xe0\ -\xf7\x5b\x5e\x12\x68\x95\x31\xa6\x09\xfc\x73\x99\xf3\x6b\x04\x1e\ -\x2a\xf3\xda\xc3\xb8\x82\x3c\xbb\x3a\x2c\xd2\xf6\xd9\xec\xf9\xf4\ -\x44\x2d\xd7\x23\xa1\x53\x4a\x49\x3a\x9b\xe1\xed\x83\x07\x0a\xda\ -\xca\x3b\x5e\x56\xda\x59\xa2\xbf\x16\x2f\xe0\x36\x7b\x25\x2c\x28\ -\x41\x3e\x17\xc6\xce\x85\x30\x52\x0a\x3d\xaf\x50\x83\x67\xf5\xaf\ -\xc0\xbf\x8d\x32\xcf\x3d\xc0\xa7\x1d\x45\x56\xd3\x05\xcf\x9d\xcc\ -\xd3\x6d\xa9\x42\x9d\x65\x15\x5f\xc4\xc6\x4f\x4c\xe9\x9b\xa5\xe4\ -\xc0\x87\x1f\x70\xaa\xfd\x0c\x01\x7f\x00\x5c\xa1\xfb\x7a\xe0\x49\ -\xef\x6d\x77\x00\x27\x86\xf4\x77\xcc\xcd\x83\x04\xca\xd6\x41\xda\ -\xc4\x66\x1e\x26\x54\x7b\x94\xd6\xc5\x1a\x59\x43\xc3\x97\x55\x28\ -\x71\x76\x74\x07\x38\x30\x34\xe9\x06\xfa\x80\xf7\x81\x47\x3c\xe1\ -\xec\xa4\xa1\x43\x4b\xaf\x4d\xca\x76\x0b\xd1\x42\xc6\x55\x64\xfa\ -\xea\x5c\x88\x29\x76\x73\x1c\x20\x64\x04\x39\xdd\xd9\xc1\x3b\x8d\ -\x07\x89\x86\xc3\x85\xef\x9a\xbd\x04\x6e\x05\xb0\x12\xb8\x0e\xf8\ -\x2f\xef\xbb\x05\x08\xf5\x62\xa1\x5b\xc7\xf6\xa1\xf9\x4c\x2e\x5a\ -\xb4\x93\xda\x25\x0d\xbc\xbf\x4a\xe7\x9d\x9b\x23\xe8\x96\x22\x9a\ -\x70\x30\x7a\x15\xc0\x85\x4a\xf2\xd3\x01\x63\x6f\x04\xfe\xc8\x6b\ -\x2b\x3c\xe1\x2c\x0b\x10\xf6\x0b\x9e\x3e\x99\x67\x4f\xb7\x43\x95\ -\x4f\x8c\x16\x47\x73\x95\x96\x36\xc1\xdd\x36\x41\x29\x45\x4d\xac\ -\x8a\xfd\xcd\xbf\xe7\xda\x65\x57\x52\x1d\x8b\xd3\x97\x49\x0f\x1c\ -\xf8\x88\xf7\xf9\x3e\xe0\xcf\x84\xb4\xa3\x56\x2e\xb4\x1a\x25\x57\ -\xcd\x59\xfa\xcb\x5d\xbe\x60\x12\x3b\x6f\x60\x99\x11\x40\x11\xce\ -\x2a\x92\xb3\x24\xa7\x16\xea\x84\x93\x0e\x20\x88\xb7\xd9\x0f\xa4\ -\xa3\x22\xe4\xe8\x20\x1d\x5e\x53\xf0\x9c\x18\xe1\x75\xc5\xfd\x82\ -\xa3\xbd\x0e\x3f\x3f\x69\xb1\x20\x28\x70\xfa\xaf\x99\x59\x22\x55\ -\xa8\xa8\x18\x5e\xd0\x67\xdc\x14\x56\xd3\xe8\xed\x4b\xb1\xb7\xe9\ -\x20\x45\x97\x6c\xa1\x3a\x50\x7c\xcb\xec\x8b\xa3\x1c\x9d\xd8\x85\ -\x4d\xdf\x09\xc6\x4e\x09\x7f\xa8\x1b\x5f\xb0\x67\x90\xa3\xf9\xb3\ -\x8a\xc6\x1b\xfc\xfc\xfa\x8b\x06\x0d\xb7\x07\xae\x6f\x5b\xa8\x7d\ -\x35\xda\xe5\x10\x49\xa8\x3e\x7f\x5a\x7d\xcd\xe8\x53\xf8\xb3\xca\ -\x35\x3a\x31\xd8\x49\x7f\x76\x22\x4f\x9b\xe9\xe0\x97\xc3\x6a\xb3\ -\x91\xd0\x31\x11\xc4\xcc\x2f\xdc\xab\x94\x22\x1c\x0a\xb3\xe3\xcd\ -\x06\xf6\xec\xdf\x47\x4d\x55\xfc\xac\xc8\x6d\xe7\x42\x58\xd9\x28\ -\x56\x26\x8a\x99\xaa\xf9\x5e\xa4\xfa\x78\xd3\xfc\x4f\x3c\x4f\xdd\ -\xa2\x5f\x2f\xb7\x6d\xff\xbd\xb9\x74\x0c\xc7\xf2\x0f\xf3\x58\x3d\ -\xaf\x08\xa6\x14\x3e\x53\x7d\xff\xad\xf5\x06\x6f\xaf\x33\x48\xcc\ -\x96\x8f\x67\xa3\x1c\x4b\xc7\x04\xbd\xd5\x12\x5b\x07\xe1\xb8\x6a\ -\x5e\x95\x5f\xd0\x69\x2a\x7e\xda\x6a\x11\xd6\x07\x59\x0b\x25\x6a\ -\xad\xff\x9b\x08\x57\x5a\xe8\xb5\x43\x00\x9a\x94\x04\x8d\x20\xef\ -\xfe\xae\x91\x6b\x96\x2e\x23\x1a\x8e\x90\x4a\xa7\x89\xd4\x1d\x46\ -\xd3\x72\x28\x47\xc3\xb6\xfc\x76\xcd\xfc\x7d\x0f\x05\x22\x9d\xcf\ -\xe5\x52\x71\x94\x92\x0f\x20\x78\x02\x94\xe9\xc9\x1a\x17\x7b\x81\ -\xf6\xb0\x37\xe9\x0d\xfe\x0c\x57\x6b\xb6\x43\xeb\x62\xed\x54\xdb\ -\x02\xf9\x88\x6e\xbb\x14\xe6\xfd\x82\xeb\x5e\x31\xa9\x3e\x69\x13\ -\x89\x49\xda\x73\x8a\xaf\x1c\xc8\xd2\x63\x2b\xaa\x7d\x83\xac\xb6\ -\x86\xe2\x47\x50\x9a\x27\x82\x18\x3f\xf0\x27\x85\xbc\x41\x29\x45\ -\x2c\x12\xa1\xf5\x4c\x1b\x3f\x7a\x69\x0b\x1b\xd7\xac\x45\x23\x42\ -\x7c\xde\x7e\x8c\x19\x67\x70\xf2\x06\x08\x85\x9d\x0b\x3e\x9f\x4d\ -\xd5\x7e\x59\x08\xfb\x16\x84\x8a\x03\xcf\x01\xbd\xb8\x1b\x72\xd5\ -\xf4\xe7\x1d\x6f\x00\xd7\x2b\x09\xd2\x82\x48\x42\xfd\x7d\x3e\x40\ -\xef\x40\x43\xb0\x7d\x20\x24\xd8\x40\xfd\x69\x8b\xfa\x76\x8b\x25\ -\x11\x89\x3d\xd8\x5a\xd6\x0d\xe8\x77\x28\xde\x9b\x08\x57\x2a\x08\ -\x58\x83\x44\xee\xaa\xe8\x0c\x9a\x0e\x1f\x62\xd7\x9e\xbd\x5c\x70\ -\x61\x1e\xc7\x51\xe4\x33\x31\x2c\x33\x8c\x95\x8d\xa0\x1c\x0d\x21\ -\xec\xaf\x03\x49\xef\xb6\xcf\x7b\x7a\x4f\xf5\x90\xa4\x72\x23\x70\ -\x51\x21\x4b\x56\x82\x5f\xe8\x39\x38\xdb\xf2\x70\xe4\x0a\x9d\x50\ -\x48\xf2\x50\xb3\xc9\x7d\xcd\x26\x97\x85\x87\x91\x82\xf7\xf2\x46\ -\x42\xef\x68\x35\xd4\xb9\x10\xb3\xd2\x6b\x03\x24\x1a\x45\x75\x2c\ -\x46\xd3\x87\x27\x68\xcd\x6c\xa3\xa6\x26\x87\xca\xfb\x41\x89\xfe\ -\xe6\x9a\xf0\x4f\xc6\x30\xce\xd3\xc3\xcc\x3c\xaf\xf0\xcf\xd5\xd9\ -\x1a\x87\x6d\xc7\xf2\xc8\x91\x1f\xe4\x52\xe0\xb3\x45\xfa\x7c\xd7\ -\xab\xa1\x26\x84\x18\x0d\xb8\x7f\x58\xdd\x1f\x08\x70\xa6\xab\x9d\ -\xa3\xc7\xdb\x09\xf8\x43\x6e\x76\x3b\x5c\x1a\xb8\x76\x0c\xe3\x7c\ -\x65\xa8\xcb\x4b\x1b\x8c\x19\x92\x9d\x61\x38\xd2\x6d\x53\xeb\x17\ -\x23\x25\x5c\xf7\x01\xc1\x22\x7d\xbe\x5a\x29\x31\xbc\x18\x6e\xf3\ -\x12\xb8\x41\x2e\xe5\xf7\xf9\x99\x11\x35\x10\xc2\x61\x84\x6d\xab\ -\x1b\x87\x5a\xda\x28\xb8\x05\xb8\x7a\xd0\x18\x80\x50\x8a\xd9\x9a\ -\x20\xa4\x8b\x42\xb1\x38\x10\x97\xe1\x9e\xfd\x1b\x09\x09\x4f\xac\ -\x9f\x50\x62\xc0\x3d\x5a\x36\xd8\x94\x34\x48\x26\x1d\x7c\x3e\x81\ -\x1c\x3e\xc2\xb2\x71\x8c\x71\xc5\xe0\x52\x04\xb2\x59\x45\x22\xe1\ -\xa0\x69\x23\x5e\xff\x6d\xcf\xa2\x47\xc2\x2f\x06\xc4\xb8\x09\x25\ -\xe6\x06\x60\xd3\xa0\xca\xb3\x4a\xe3\x85\x2d\x7d\xbc\xf4\x52\x9a\ -\x48\x54\x0c\xb5\x9a\xe0\x38\xc6\x88\xf4\xc7\x31\x88\x44\x05\x1d\ -\x1d\x0e\xf5\xdb\xd2\xc4\x62\xc3\x9e\xff\x2f\x4a\x04\x5d\x87\x12\ -\xdb\xcc\x95\x26\xa6\xa0\x9b\xdc\x3c\xf4\x8d\xbe\xf6\x6a\x1f\x9a\ -\x14\x18\x86\x60\xc0\xc1\x86\xd6\x71\xf4\x7f\xcc\x75\x53\x30\x0c\ -\x81\x26\x05\x5b\xb6\xf4\x91\x4e\xab\xa1\x16\xb9\x1c\xd8\x5c\xa2\ -\x9f\x27\xcb\x55\xf4\x2a\x45\x4c\x00\xd8\x52\x30\x79\xa5\x20\x16\ -\x93\xb4\xb7\xdb\x6c\x7e\xb2\x97\x70\x58\x12\x0c\x9e\xb5\x9c\x06\ -\xdc\xf4\xa3\x5c\x24\x80\x77\x94\x82\x60\x50\x10\x0e\x4b\x36\x3f\ -\xd9\xcb\xf6\xfa\xb4\xbb\xe1\x3f\x58\x54\x7b\x61\xa0\x75\x0d\x41\ -\x4f\x19\xf2\x45\xc5\x89\x01\x88\x01\xbb\xf0\xb6\x5b\xa4\x84\x70\ -\x58\x52\x5f\x9f\xe6\xf1\xc7\x92\x18\x86\x20\x5e\x2d\x11\x82\x43\ -\x4a\xf1\xe3\x72\x3b\x55\x8a\xa7\x84\xe0\x54\xbc\x5a\x62\x18\x82\ -\xc7\x1f\x4b\x52\x5f\x9f\xa6\xae\x4e\x43\xd7\x07\x2d\xcd\x6f\x52\ -\x7a\x4b\xe6\x7e\xdc\x43\x90\x93\x4e\x0c\xb8\xe7\x53\x76\x01\x9f\ -\x2d\xc4\x95\x59\xb3\x34\x76\xec\xc8\xf0\xd4\xe6\x1e\x4e\xb4\xda\ -\x84\x42\x12\x21\xf8\x47\x2f\x97\x18\x0d\xdb\x85\xe0\x81\x50\x48\ -\x72\xa2\xd5\xe6\xa9\xcd\x3d\xec\xd8\x91\x61\xd6\x2c\xed\x6c\xbc\ -\xf1\xa4\x87\x9d\x1e\x39\xc5\xf0\x3c\xf0\x44\x25\x15\xbc\xf1\x60\ -\x06\xee\x79\xbb\x7b\x0a\x96\x53\x57\xa7\xb1\x7b\x77\x96\x4d\xf7\ -\x76\xd2\xd0\x90\x61\xf6\x6c\x2d\x19\x08\x88\x5b\x95\xe2\x99\x22\ -\x56\x82\xcf\x27\x36\x87\xc2\x72\x43\x5d\x9d\x66\x35\x34\x64\xd8\ -\x74\x6f\x27\xbb\x77\x67\xa9\xab\xd3\x06\xc6\x95\xbb\x70\xf7\xc3\ -\x4b\x59\xca\x51\xe0\x6b\x63\x7d\x88\xd1\xce\xc7\xb4\x95\xd0\x7d\ -\xcb\xc1\x2b\xc0\xc3\xc0\xdb\x42\x40\x26\xe3\x06\xcb\x4b\x2f\xf5\ -\xb1\xe1\xf6\x30\x2b\x56\xf8\xc9\x66\x59\xe3\xe5\x2a\x8b\xbc\xe4\ -\xb9\xc9\xe7\x63\x7b\x22\xe1\xfc\xaa\xa9\x29\xcf\xae\x9d\x69\x9a\ -\x9b\xf3\x38\x0e\x03\xe3\xd4\x95\x9e\x6e\x7c\xe7\x28\xe3\xf7\x7a\ -\x8a\xe2\xc1\x11\x05\xe5\xc6\xc6\x8a\x17\x91\x63\x49\x00\xd7\x01\ -\x4f\x2b\xc5\x0f\x42\x21\xf1\xdb\x5c\x4e\xf1\xd6\x5b\x26\xdd\xdd\ -\x0e\x6d\xa7\x42\x98\x39\xb5\x1d\xc5\xf6\x81\xaf\x27\x1a\x91\xec\ -\xdd\x9b\x65\xcf\x6f\x4c\x14\x10\x89\x9c\x5d\xd9\xae\xc2\xdd\xb6\ -\xfd\x2b\x8a\x6f\xbb\x14\x60\x79\xe3\x1f\x1c\xcf\xc4\x27\xda\x62\ -\x86\xe6\x10\xdb\x81\x6d\x52\xb2\xd5\x34\xd5\xc9\x64\xb7\x63\x0b\ -\x09\xca\x01\xdb\xe9\x4f\x0e\x1d\xc7\x0d\xdc\x91\xb0\x90\xca\x5d\ -\x6d\xd6\x7a\xed\x73\x94\x77\x5a\xaa\x13\xb8\xc3\x73\xb3\xe2\x5b\ -\x10\x25\x2c\x66\x32\x89\x19\x88\x6e\xe0\x03\xdc\x23\xf3\x47\x94\ -\xe2\xb4\x52\xee\xc9\x04\x21\xf0\x0b\x41\x2d\xee\x01\xeb\x05\xb8\ -\xfb\xe7\x63\xd9\x02\x3e\x82\xbb\x17\xb5\x77\xb4\x0b\x3f\x4a\x57\ -\x2a\x86\xaa\x81\xd5\xb9\x10\x6e\xab\x00\x9e\x05\xfe\xce\xcb\x7d\ -\xce\x09\xe7\xcb\xef\x44\xb6\x01\xdf\xf0\x2c\x25\x51\x89\x0e\xf5\ -\xf3\x80\x94\xc7\xbc\x42\xf6\x78\x25\x3b\x1d\x8d\x18\x35\x85\x2d\ -\xe4\x45\xdc\xd3\x56\x7b\x27\x62\x00\xbd\x8c\x58\x30\x55\xd0\x0d\ -\xbc\x0d\xfc\xd2\x53\x00\x3b\x26\x72\xb0\xd1\x88\x69\xc1\xdd\x5c\ -\x33\x27\x91\x00\xd3\x4b\xcc\x3a\xbd\xaa\xba\x19\xd8\xef\xfd\x7b\ -\x78\xb2\x26\x21\xa6\xff\x4c\xca\xf9\xbd\x2a\x4d\x13\x33\x4d\xcc\ -\x34\x31\xd3\xc4\x4c\x13\x73\x3e\xe0\xff\x07\x00\xd9\x01\xc4\xf4\ -\xd1\x48\x28\x2f\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\ -\x00\x00\x10\xfe\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x00\x40\x00\x00\x00\x40\x08\x06\x00\x00\x00\xaa\x69\x71\xde\ -\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ -\x01\x00\x9a\x9c\x18\x00\x00\x0a\x4f\x69\x43\x43\x50\x50\x68\x6f\ -\x74\x6f\x73\x68\x6f\x70\x20\x49\x43\x43\x20\x70\x72\x6f\x66\x69\ -\x6c\x65\x00\x00\x78\xda\x9d\x53\x67\x54\x53\xe9\x16\x3d\xf7\xde\ -\xf4\x42\x4b\x88\x80\x94\x4b\x6f\x52\x15\x08\x20\x52\x42\x8b\x80\ -\x14\x91\x26\x2a\x21\x09\x10\x4a\x88\x21\xa1\xd9\x15\x51\xc1\x11\ -\x45\x45\x04\x1b\xc8\xa0\x88\x03\x8e\x8e\x80\x8c\x15\x51\x2c\x0c\ -\x8a\x0a\xd8\x07\xe4\x21\xa2\x8e\x83\xa3\x88\x8a\xca\xfb\xe1\x7b\ -\xa3\x6b\xd6\xbc\xf7\xe6\xcd\xfe\xb5\xd7\x3e\xe7\xac\xf3\x9d\xb3\ -\xcf\x07\xc0\x08\x0c\x96\x48\x33\x51\x35\x80\x0c\xa9\x42\x1e\x11\ -\xe0\x83\xc7\xc4\xc6\xe1\xe4\x2e\x40\x81\x0a\x24\x70\x00\x10\x08\ -\xb3\x64\x21\x73\xfd\x23\x01\x00\xf8\x7e\x3c\x3c\x2b\x22\xc0\x07\ -\xbe\x00\x01\x78\xd3\x0b\x08\x00\xc0\x4d\x9b\xc0\x30\x1c\x87\xff\ -\x0f\xea\x42\x99\x5c\x01\x80\x84\x01\xc0\x74\x91\x38\x4b\x08\x80\ -\x14\x00\x40\x7a\x8e\x42\xa6\x00\x40\x46\x01\x80\x9d\x98\x26\x53\ -\x00\xa0\x04\x00\x60\xcb\x63\x62\xe3\x00\x50\x2d\x00\x60\x27\x7f\ -\xe6\xd3\x00\x80\x9d\xf8\x99\x7b\x01\x00\x5b\x94\x21\x15\x01\xa0\ -\x91\x00\x20\x13\x65\x88\x44\x00\x68\x3b\x00\xac\xcf\x56\x8a\x45\ -\x00\x58\x30\x00\x14\x66\x4b\xc4\x39\x00\xd8\x2d\x00\x30\x49\x57\ -\x66\x48\x00\xb0\xb7\x00\xc0\xce\x10\x0b\xb2\x00\x08\x0c\x00\x30\ -\x51\x88\x85\x29\x00\x04\x7b\x00\x60\xc8\x23\x23\x78\x00\x84\x99\ -\x00\x14\x46\xf2\x57\x3c\xf1\x2b\xae\x10\xe7\x2a\x00\x00\x78\x99\ -\xb2\x3c\xb9\x24\x39\x45\x81\x5b\x08\x2d\x71\x07\x57\x57\x2e\x1e\ -\x28\xce\x49\x17\x2b\x14\x36\x61\x02\x61\x9a\x40\x2e\xc2\x79\x99\ -\x19\x32\x81\x34\x0f\xe0\xf3\xcc\x00\x00\xa0\x91\x15\x11\xe0\x83\ -\xf3\xfd\x78\xce\x0e\xae\xce\xce\x36\x8e\xb6\x0e\x5f\x2d\xea\xbf\ -\x06\xff\x22\x62\x62\xe3\xfe\xe5\xcf\xab\x70\x40\x00\x00\xe1\x74\ -\x7e\xd1\xfe\x2c\x2f\xb3\x1a\x80\x3b\x06\x80\x6d\xfe\xa2\x25\xee\ -\x04\x68\x5e\x0b\xa0\x75\xf7\x8b\x66\xb2\x0f\x40\xb5\x00\xa0\xe9\ -\xda\x57\xf3\x70\xf8\x7e\x3c\x3c\x45\xa1\x90\xb9\xd9\xd9\xe5\xe4\ -\xe4\xd8\x4a\xc4\x42\x5b\x61\xca\x57\x7d\xfe\x67\xc2\x5f\xc0\x57\ -\xfd\x6c\xf9\x7e\x3c\xfc\xf7\xf5\xe0\xbe\xe2\x24\x81\x32\x5d\x81\ -\x47\x04\xf8\xe0\xc2\xcc\xf4\x4c\xa5\x1c\xcf\x92\x09\x84\x62\xdc\ -\xe6\x8f\x47\xfc\xb7\x0b\xff\xfc\x1d\xd3\x22\xc4\x49\x62\xb9\x58\ -\x2a\x14\xe3\x51\x12\x71\x8e\x44\x9a\x8c\xf3\x32\xa5\x22\x89\x42\ -\x92\x29\xc5\x25\xd2\xff\x64\xe2\xdf\x2c\xfb\x03\x3e\xdf\x35\x00\ -\xb0\x6a\x3e\x01\x7b\x91\x2d\xa8\x5d\x63\x03\xf6\x4b\x27\x10\x58\ -\x74\xc0\xe2\xf7\x00\x00\xf2\xbb\x6f\xc1\xd4\x28\x08\x03\x80\x68\ -\x83\xe1\xcf\x77\xff\xef\x3f\xfd\x47\xa0\x25\x00\x80\x66\x49\x92\ -\x71\x00\x00\x5e\x44\x24\x2e\x54\xca\xb3\x3f\xc7\x08\x00\x00\x44\ -\xa0\x81\x2a\xb0\x41\x1b\xf4\xc1\x18\x2c\xc0\x06\x1c\xc1\x05\xdc\ -\xc1\x0b\xfc\x60\x36\x84\x42\x24\xc4\xc2\x42\x10\x42\x0a\x64\x80\ -\x1c\x72\x60\x29\xac\x82\x42\x28\x86\xcd\xb0\x1d\x2a\x60\x2f\xd4\ -\x40\x1d\x34\xc0\x51\x68\x86\x93\x70\x0e\x2e\xc2\x55\xb8\x0e\x3d\ -\x70\x0f\xfa\x61\x08\x9e\xc1\x28\xbc\x81\x09\x04\x41\xc8\x08\x13\ -\x61\x21\xda\x88\x01\x62\x8a\x58\x23\x8e\x08\x17\x99\x85\xf8\x21\ -\xc1\x48\x04\x12\x8b\x24\x20\xc9\x88\x14\x51\x22\x4b\x91\x35\x48\ -\x31\x52\x8a\x54\x20\x55\x48\x1d\xf2\x3d\x72\x02\x39\x87\x5c\x46\ -\xba\x91\x3b\xc8\x00\x32\x82\xfc\x86\xbc\x47\x31\x94\x81\xb2\x51\ -\x3d\xd4\x0c\xb5\x43\xb9\xa8\x37\x1a\x84\x46\xa2\x0b\xd0\x64\x74\ -\x31\x9a\x8f\x16\xa0\x9b\xd0\x72\xb4\x1a\x3d\x8c\x36\xa1\xe7\xd0\ -\xab\x68\x0f\xda\x8f\x3e\x43\xc7\x30\xc0\xe8\x18\x07\x33\xc4\x6c\ -\x30\x2e\xc6\xc3\x42\xb1\x38\x2c\x09\x93\x63\xcb\xb1\x22\xac\x0c\ -\xab\xc6\x1a\xb0\x56\xac\x03\xbb\x89\xf5\x63\xcf\xb1\x77\x04\x12\ -\x81\x45\xc0\x09\x36\x04\x77\x42\x20\x61\x1e\x41\x48\x58\x4c\x58\ -\x4e\xd8\x48\xa8\x20\x1c\x24\x34\x11\xda\x09\x37\x09\x03\x84\x51\ -\xc2\x27\x22\x93\xa8\x4b\xb4\x26\xba\x11\xf9\xc4\x18\x62\x32\x31\ -\x87\x58\x48\x2c\x23\xd6\x12\x8f\x13\x2f\x10\x7b\x88\x43\xc4\x37\ -\x24\x12\x89\x43\x32\x27\xb9\x90\x02\x49\xb1\xa4\x54\xd2\x12\xd2\ -\x46\xd2\x6e\x52\x23\xe9\x2c\xa9\x9b\x34\x48\x1a\x23\x93\xc9\xda\ -\x64\x6b\xb2\x07\x39\x94\x2c\x20\x2b\xc8\x85\xe4\x9d\xe4\xc3\xe4\ -\x33\xe4\x1b\xe4\x21\xf2\x5b\x0a\x9d\x62\x40\x71\xa4\xf8\x53\xe2\ -\x28\x52\xca\x6a\x4a\x19\xe5\x10\xe5\x34\xe5\x06\x65\x98\x32\x41\ -\x55\xa3\x9a\x52\xdd\xa8\xa1\x54\x11\x35\x8f\x5a\x42\xad\xa1\xb6\ -\x52\xaf\x51\x87\xa8\x13\x34\x75\x9a\x39\xcd\x83\x16\x49\x4b\xa5\ -\xad\xa2\x95\xd3\x1a\x68\x17\x68\xf7\x69\xaf\xe8\x74\xba\x11\xdd\ -\x95\x1e\x4e\x97\xd0\x57\xd2\xcb\xe9\x47\xe8\x97\xe8\x03\xf4\x77\ -\x0c\x0d\x86\x15\x83\xc7\x88\x67\x28\x19\x9b\x18\x07\x18\x67\x19\ -\x77\x18\xaf\x98\x4c\xa6\x19\xd3\x8b\x19\xc7\x54\x30\x37\x31\xeb\ -\x98\xe7\x99\x0f\x99\x6f\x55\x58\x2a\xb6\x2a\x7c\x15\x91\xca\x0a\ -\x95\x4a\x95\x26\x95\x1b\x2a\x2f\x54\xa9\xaa\xa6\xaa\xde\xaa\x0b\ -\x55\xf3\x55\xcb\x54\x8f\xa9\x5e\x53\x7d\xae\x46\x55\x33\x53\xe3\ -\xa9\x09\xd4\x96\xab\x55\xaa\x9d\x50\xeb\x53\x1b\x53\x67\xa9\x3b\ -\xa8\x87\xaa\x67\xa8\x6f\x54\x3f\xa4\x7e\x59\xfd\x89\x06\x59\xc3\ -\x4c\xc3\x4f\x43\xa4\x51\xa0\xb1\x5f\xe3\xbc\xc6\x20\x0b\x63\x19\ -\xb3\x78\x2c\x21\x6b\x0d\xab\x86\x75\x81\x35\xc4\x26\xb1\xcd\xd9\ -\x7c\x76\x2a\xbb\x98\xfd\x1d\xbb\x8b\x3d\xaa\xa9\xa1\x39\x43\x33\ -\x4a\x33\x57\xb3\x52\xf3\x94\x66\x3f\x07\xe3\x98\x71\xf8\x9c\x74\ -\x4e\x09\xe7\x28\xa7\x97\xf3\x7e\x8a\xde\x14\xef\x29\xe2\x29\x1b\ -\xa6\x34\x4c\xb9\x31\x65\x5c\x6b\xaa\x96\x97\x96\x58\xab\x48\xab\ -\x51\xab\x47\xeb\xbd\x36\xae\xed\xa7\x9d\xa6\xbd\x45\xbb\x59\xfb\ -\x81\x0e\x41\xc7\x4a\x27\x5c\x27\x47\x67\x8f\xce\x05\x9d\xe7\x53\ -\xd9\x53\xdd\xa7\x0a\xa7\x16\x4d\x3d\x3a\xf5\xae\x2e\xaa\x6b\xa5\ -\x1b\xa1\xbb\x44\x77\xbf\x6e\xa7\xee\x98\x9e\xbe\x5e\x80\x9e\x4c\ -\x6f\xa7\xde\x79\xbd\xe7\xfa\x1c\x7d\x2f\xfd\x54\xfd\x6d\xfa\xa7\ -\xf5\x47\x0c\x58\x06\xb3\x0c\x24\x06\xdb\x0c\xce\x18\x3c\xc5\x35\ -\x71\x6f\x3c\x1d\x2f\xc7\xdb\xf1\x51\x43\x5d\xc3\x40\x43\xa5\x61\ -\x95\x61\x97\xe1\x84\x91\xb9\xd1\x3c\xa3\xd5\x46\x8d\x46\x0f\x8c\ -\x69\xc6\x5c\xe3\x24\xe3\x6d\xc6\x6d\xc6\xa3\x26\x06\x26\x21\x26\ -\x4b\x4d\xea\x4d\xee\x9a\x52\x4d\xb9\xa6\x29\xa6\x3b\x4c\x3b\x4c\ -\xc7\xcd\xcc\xcd\xa2\xcd\xd6\x99\x35\x9b\x3d\x31\xd7\x32\xe7\x9b\ -\xe7\x9b\xd7\x9b\xdf\xb7\x60\x5a\x78\x5a\x2c\xb6\xa8\xb6\xb8\x65\ -\x49\xb2\xe4\x5a\xa6\x59\xee\xb6\xbc\x6e\x85\x5a\x39\x59\xa5\x58\ -\x55\x5a\x5d\xb3\x46\xad\x9d\xad\x25\xd6\xbb\xad\xbb\xa7\x11\xa7\ -\xb9\x4e\x93\x4e\xab\x9e\xd6\x67\xc3\xb0\xf1\xb6\xc9\xb6\xa9\xb7\ -\x19\xb0\xe5\xd8\x06\xdb\xae\xb6\x6d\xb6\x7d\x61\x67\x62\x17\x67\ -\xb7\xc5\xae\xc3\xee\x93\xbd\x93\x7d\xba\x7d\x8d\xfd\x3d\x07\x0d\ -\x87\xd9\x0e\xab\x1d\x5a\x1d\x7e\x73\xb4\x72\x14\x3a\x56\x3a\xde\ -\x9a\xce\x9c\xee\x3f\x7d\xc5\xf4\x96\xe9\x2f\x67\x58\xcf\x10\xcf\ -\xd8\x33\xe3\xb6\x13\xcb\x29\xc4\x69\x9d\x53\x9b\xd3\x47\x67\x17\ -\x67\xb9\x73\x83\xf3\x88\x8b\x89\x4b\x82\xcb\x2e\x97\x3e\x2e\x9b\ -\x1b\xc6\xdd\xc8\xbd\xe4\x4a\x74\xf5\x71\x5d\xe1\x7a\xd2\xf5\x9d\ -\x9b\xb3\x9b\xc2\xed\xa8\xdb\xaf\xee\x36\xee\x69\xee\x87\xdc\x9f\ -\xcc\x34\x9f\x29\x9e\x59\x33\x73\xd0\xc3\xc8\x43\xe0\x51\xe5\xd1\ -\x3f\x0b\x9f\x95\x30\x6b\xdf\xac\x7e\x4f\x43\x4f\x81\x67\xb5\xe7\ -\x23\x2f\x63\x2f\x91\x57\xad\xd7\xb0\xb7\xa5\x77\xaa\xf7\x61\xef\ -\x17\x3e\xf6\x3e\x72\x9f\xe3\x3e\xe3\x3c\x37\xde\x32\xde\x59\x5f\ -\xcc\x37\xc0\xb7\xc8\xb7\xcb\x4f\xc3\x6f\x9e\x5f\x85\xdf\x43\x7f\ -\x23\xff\x64\xff\x7a\xff\xd1\x00\xa7\x80\x25\x01\x67\x03\x89\x81\ -\x41\x81\x5b\x02\xfb\xf8\x7a\x7c\x21\xbf\x8e\x3f\x3a\xdb\x65\xf6\ -\xb2\xd9\xed\x41\x8c\xa0\xb9\x41\x15\x41\x8f\x82\xad\x82\xe5\xc1\ -\xad\x21\x68\xc8\xec\x90\xad\x21\xf7\xe7\x98\xce\x91\xce\x69\x0e\ -\x85\x50\x7e\xe8\xd6\xd0\x07\x61\xe6\x61\x8b\xc3\x7e\x0c\x27\x85\ -\x87\x85\x57\x86\x3f\x8e\x70\x88\x58\x1a\xd1\x31\x97\x35\x77\xd1\ -\xdc\x43\x73\xdf\x44\xfa\x44\x96\x44\xde\x9b\x67\x31\x4f\x39\xaf\ -\x2d\x4a\x35\x2a\x3e\xaa\x2e\x6a\x3c\xda\x37\xba\x34\xba\x3f\xc6\ -\x2e\x66\x59\xcc\xd5\x58\x9d\x58\x49\x6c\x4b\x1c\x39\x2e\x2a\xae\ -\x36\x6e\x6c\xbe\xdf\xfc\xed\xf3\x87\xe2\x9d\xe2\x0b\xe3\x7b\x17\ -\x98\x2f\xc8\x5d\x70\x79\xa1\xce\xc2\xf4\x85\xa7\x16\xa9\x2e\x12\ -\x2c\x3a\x96\x40\x4c\x88\x4e\x38\x94\xf0\x41\x10\x2a\xa8\x16\x8c\ -\x25\xf2\x13\x77\x25\x8e\x0a\x79\xc2\x1d\xc2\x67\x22\x2f\xd1\x36\ -\xd1\x88\xd8\x43\x5c\x2a\x1e\x4e\xf2\x48\x2a\x4d\x7a\x92\xec\x91\ -\xbc\x35\x79\x24\xc5\x33\xa5\x2c\xe5\xb9\x84\x27\xa9\x90\xbc\x4c\ -\x0d\x4c\xdd\x9b\x3a\x9e\x16\x9a\x76\x20\x6d\x32\x3d\x3a\xbd\x31\ -\x83\x92\x91\x90\x71\x42\xaa\x21\x4d\x93\xb6\x67\xea\x67\xe6\x66\ -\x76\xcb\xac\x65\x85\xb2\xfe\xc5\x6e\x8b\xb7\x2f\x1e\x95\x07\xc9\ -\x6b\xb3\x90\xac\x05\x59\x2d\x0a\xb6\x42\xa6\xe8\x54\x5a\x28\xd7\ -\x2a\x07\xb2\x67\x65\x57\x66\xbf\xcd\x89\xca\x39\x96\xab\x9e\x2b\ -\xcd\xed\xcc\xb3\xca\xdb\x90\x37\x9c\xef\x9f\xff\xed\x12\xc2\x12\ -\xe1\x92\xb6\xa5\x86\x4b\x57\x2d\x1d\x58\xe6\xbd\xac\x6a\x39\xb2\ -\x3c\x71\x79\xdb\x0a\xe3\x15\x05\x2b\x86\x56\x06\xac\x3c\xb8\x8a\ -\xb6\x2a\x6d\xd5\x4f\xab\xed\x57\x97\xae\x7e\xbd\x26\x7a\x4d\x6b\ -\x81\x5e\xc1\xca\x82\xc1\xb5\x01\x6b\xeb\x0b\x55\x0a\xe5\x85\x7d\ -\xeb\xdc\xd7\xed\x5d\x4f\x58\x2f\x59\xdf\xb5\x61\xfa\x86\x9d\x1b\ -\x3e\x15\x89\x8a\xae\x14\xdb\x17\x97\x15\x7f\xd8\x28\xdc\x78\xe5\ -\x1b\x87\x6f\xca\xbf\x99\xdc\x94\xb4\xa9\xab\xc4\xb9\x64\xcf\x66\ -\xd2\x66\xe9\xe6\xde\x2d\x9e\x5b\x0e\x96\xaa\x97\xe6\x97\x0e\x6e\ -\x0d\xd9\xda\xb4\x0d\xdf\x56\xb4\xed\xf5\xf6\x45\xdb\x2f\x97\xcd\ -\x28\xdb\xbb\x83\xb6\x43\xb9\xa3\xbf\x3c\xb8\xbc\x65\xa7\xc9\xce\ -\xcd\x3b\x3f\x54\xa4\x54\xf4\x54\xfa\x54\x36\xee\xd2\xdd\xb5\x61\ -\xd7\xf8\x6e\xd1\xee\x1b\x7b\xbc\xf6\x34\xec\xd5\xdb\x5b\xbc\xf7\ -\xfd\x3e\xc9\xbe\xdb\x55\x01\x55\x4d\xd5\x66\xd5\x65\xfb\x49\xfb\ -\xb3\xf7\x3f\xae\x89\xaa\xe9\xf8\x96\xfb\x6d\x5d\xad\x4e\x6d\x71\ -\xed\xc7\x03\xd2\x03\xfd\x07\x23\x0e\xb6\xd7\xb9\xd4\xd5\x1d\xd2\ -\x3d\x54\x52\x8f\xd6\x2b\xeb\x47\x0e\xc7\x1f\xbe\xfe\x9d\xef\x77\ -\x2d\x0d\x36\x0d\x55\x8d\x9c\xc6\xe2\x23\x70\x44\x79\xe4\xe9\xf7\ -\x09\xdf\xf7\x1e\x0d\x3a\xda\x76\x8c\x7b\xac\xe1\x07\xd3\x1f\x76\ -\x1d\x67\x1d\x2f\x6a\x42\x9a\xf2\x9a\x46\x9b\x53\x9a\xfb\x5b\x62\ -\x5b\xba\x4f\xcc\x3e\xd1\xd6\xea\xde\x7a\xfc\x47\xdb\x1f\x0f\x9c\ -\x34\x3c\x59\x79\x4a\xf3\x54\xc9\x69\xda\xe9\x82\xd3\x93\x67\xf2\ -\xcf\x8c\x9d\x95\x9d\x7d\x7e\x2e\xf9\xdc\x60\xdb\xa2\xb6\x7b\xe7\ -\x63\xce\xdf\x6a\x0f\x6f\xef\xba\x10\x74\xe1\xd2\x45\xff\x8b\xe7\ -\x3b\xbc\x3b\xce\x5c\xf2\xb8\x74\xf2\xb2\xdb\xe5\x13\x57\xb8\x57\ -\x9a\xaf\x3a\x5f\x6d\xea\x74\xea\x3c\xfe\x93\xd3\x4f\xc7\xbb\x9c\ -\xbb\x9a\xae\xb9\x5c\x6b\xb9\xee\x7a\xbd\xb5\x7b\x66\xf7\xe9\x1b\ -\x9e\x37\xce\xdd\xf4\xbd\x79\xf1\x16\xff\xd6\xd5\x9e\x39\x3d\xdd\ -\xbd\xf3\x7a\x6f\xf7\xc5\xf7\xf5\xdf\x16\xdd\x7e\x72\x27\xfd\xce\ -\xcb\xbb\xd9\x77\x27\xee\xad\xbc\x4f\xbc\x5f\xf4\x40\xed\x41\xd9\ -\x43\xdd\x87\xd5\x3f\x5b\xfe\xdc\xd8\xef\xdc\x7f\x6a\xc0\x77\xa0\ -\xf3\xd1\xdc\x47\xf7\x06\x85\x83\xcf\xfe\x91\xf5\x8f\x0f\x43\x05\ -\x8f\x99\x8f\xcb\x86\x0d\x86\xeb\x9e\x38\x3e\x39\x39\xe2\x3f\x72\ -\xfd\xe9\xfc\xa7\x43\xcf\x64\xcf\x26\x9e\x17\xfe\xa2\xfe\xcb\xae\ -\x17\x16\x2f\x7e\xf8\xd5\xeb\xd7\xce\xd1\x98\xd1\xa1\x97\xf2\x97\ -\x93\xbf\x6d\x7c\xa5\xfd\xea\xc0\xeb\x19\xaf\xdb\xc6\xc2\xc6\x1e\ -\xbe\xc9\x78\x33\x31\x5e\xf4\x56\xfb\xed\xc1\x77\xdc\x77\x1d\xef\ -\xa3\xdf\x0f\x4f\xe4\x7c\x20\x7f\x28\xff\x68\xf9\xb1\xf5\x53\xd0\ -\xa7\xfb\x93\x19\x93\x93\xff\x04\x03\x98\xf3\xfc\x63\x33\x2d\xdb\ -\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\x25\x00\x00\x80\x83\ -\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00\x75\x30\x00\x00\xea\x60\ -\x00\x00\x3a\x98\x00\x00\x17\x6f\x92\x5f\xc5\x46\x00\x00\x06\x29\ -\x49\x44\x41\x54\x78\xda\xec\x9b\x6b\x4c\x14\x57\x14\xc7\xff\xb3\ -\x6f\x58\xc0\x05\xb3\xac\xb0\xca\x52\x77\xc1\x05\xe4\xb9\x01\x8a\ -\x3c\x8d\x58\xa5\x8d\x52\x51\x13\x8d\x69\x6b\x4d\x63\x6a\x6a\x1a\ -\xb5\x8d\x89\x69\xad\xad\xad\x36\xb5\x35\x4d\x5b\x5b\xfb\x48\x34\ -\x35\xa5\x26\x8d\x62\x51\x12\x4b\x7d\xd0\xa2\xc8\x43\xde\xe0\x22\ -\x2b\x20\xca\x4b\xc1\x07\x08\x0b\xfb\x98\x65\xfb\x41\x34\xe1\x51\ -\x76\x67\x76\x66\x43\xc2\x9c\x4f\x93\x99\x73\xce\xdc\xfb\xcb\xdc\ -\x73\xee\x3d\xf7\x0e\xb1\x61\x6f\x8a\x1d\xb3\x58\x78\x98\xe5\xc2\ -\x01\xe0\x00\x70\x00\x38\x00\x1c\x00\x0e\x00\x07\x80\x03\x30\x6b\ -\x45\xc0\xa6\xf3\xc0\xb9\x2a\x24\x47\xbe\x84\x68\x75\x22\x6e\xb4\ -\x57\xe1\xe4\xa5\xa3\x0e\x6d\xd6\xa4\x6e\x46\x8c\x26\x09\x4d\x77\ -\x6a\x50\x5c\x77\x1e\xdd\x0f\xef\xb0\x0a\x80\x60\x63\x2d\x10\xaf\ -\x4d\x47\x62\xd8\x52\x84\x07\xc7\x8d\xbb\xdf\xd0\x56\x81\xa3\xf9\ -\x9f\xfd\xaf\xdd\xe6\xac\x5d\x48\xd0\x66\x8c\xbb\xa7\x6f\xaf\x46\ -\x99\xfe\x32\x2a\x9b\x8b\x59\x01\xc0\x5f\x9c\x1e\xf4\x31\x93\x0e\ -\x37\x2d\xdf\x8e\xd5\xc9\xaf\x41\x2e\x0b\x98\xf4\x4c\xe1\xab\x84\ -\x5a\x19\x8e\x8a\x9b\xff\x4c\x7a\xb6\x75\xd5\x1e\xe8\x42\x53\x27\ -\xdd\x97\xcb\x02\x10\x1b\xb2\x04\x72\xdf\x00\xd4\xb6\x94\xce\xec\ -\x21\x40\x10\x04\x62\x35\x4b\xa6\xd5\xd1\x06\x45\x63\xf7\xc6\xc3\ -\x68\xbe\x5b\x07\x89\xc8\x13\x23\x66\x23\xd4\xca\x30\x68\x94\x11\ -\xd3\xda\xc5\x85\xa4\x20\xf7\xc2\x11\x58\x49\xcb\xcc\x05\xc0\xe7\ -\xf1\x61\x21\xcd\xf0\x84\xd7\xb4\x7a\x2a\x85\x06\x2a\x85\x86\x92\ -\x6f\x8b\xd5\x04\x01\x5f\xc8\x38\x00\x1e\xb3\x5f\x00\x0f\x04\x88\ -\xd9\x9b\x06\xad\xa4\x05\x7c\x3e\x3b\x89\x45\x20\x10\xc2\x4a\x9a\ -\x67\x36\x80\x79\x7e\x0b\xe0\x21\x96\xb2\x02\x40\x28\x10\x41\x2e\ -\x0b\x9c\xb9\x00\xfc\x7d\x95\xd8\xb1\xfe\x00\xf8\x3c\x3e\x3b\xf9\ -\x1a\x04\xde\xcd\xd9\x3f\x65\x76\x71\x3b\x00\x91\x50\x02\xa9\xc4\ -\x1b\x52\x89\x37\x7c\xa4\xbe\x50\x29\x34\xd8\xbd\xe1\x4b\xf8\x78\ -\xca\x58\x1d\xaf\x73\xbc\xfc\xb0\x7b\xe3\x57\x98\x2f\x7f\x01\x12\ -\x91\x27\x84\x02\x11\x84\x02\x91\x4b\x71\x87\xf2\x44\x68\xf3\xca\ -\x5d\xd0\xaa\x62\x20\x12\x88\xc6\x22\xbf\x00\xc2\xb1\x6b\x77\xca\ -\xd0\xc8\x13\xd8\x6c\x24\xec\xb0\xc3\x4a\x5a\x60\xe8\x6c\x40\xee\ -\x85\x23\xec\xa6\xc1\x68\xf5\x8b\x48\x08\xcb\x60\xac\x13\xa3\xf6\ -\x51\xf0\x08\x7a\xa3\xd0\xcb\xc3\x67\xd2\x84\xa9\xb1\xad\x12\x75\ -\xad\x65\xec\x01\xc8\xd4\xbd\xea\x52\x87\x8d\xa6\x41\x54\x35\x5f\ -\x85\xa1\xb3\x1e\xf7\x1f\x75\xc1\x64\x19\x86\x44\xe4\x09\x85\x9f\ -\x12\x8b\x16\x44\x21\x2e\x34\x05\x52\x89\x37\x6d\xff\xcb\x74\xd9\ -\xec\x01\xf0\x97\x05\x42\xad\x0c\xa7\xdd\xb8\x8b\x55\x67\x50\x58\ -\x71\x0a\x46\xd3\xe0\xa4\x67\x5d\x0f\xda\x51\x6d\x28\xc1\xd9\x92\ -\xdf\xb0\x22\x61\x3d\x6d\xd0\x1a\x65\x04\xe4\xb2\x79\xe8\xeb\xbf\ -\xc7\x7c\x10\x5c\x91\xb0\x8e\x76\xe7\x7f\x3c\x7b\x00\x79\xc5\xc7\ -\xa7\xec\xfc\xc4\x2f\x24\xaf\xf8\x18\x7e\x3a\x7b\x90\xf6\xbb\x32\ -\x75\x39\xec\x64\x01\x5d\x68\x0a\xad\x06\xfd\xf0\xe7\x7e\xd4\xb7\ -\x96\x53\xb2\xa9\x6b\x2d\xc3\xd1\xfc\x4f\x69\xbd\x2f\x91\x62\x8c\ -\x72\x1a\x40\x51\x6d\x01\xe5\xc6\x14\xd5\x9c\x43\xe3\xed\x4a\x5a\ -\x1d\x69\x68\xbb\x8e\xa2\x9a\x73\x94\xed\xca\x9b\x8a\xd8\x01\x90\ -\x7f\xf5\x04\x25\xe7\x26\xcb\x30\xf2\x8a\x8f\xb9\x14\x34\xcf\x5c\ -\x39\x0e\x93\x65\x84\x12\x34\x67\x8a\x2e\xb4\x27\x42\xbf\xfe\xf5\ -\xb5\xd3\x6b\xf2\x8a\xa6\x7f\x61\x1b\xb5\xb9\x04\x80\xb4\x91\xb8\ -\x3e\x45\xed\x60\x2a\x31\x74\x34\xd0\x1a\x36\x94\x93\xf0\xcf\xe7\ -\x3e\xc7\xad\xce\x46\x87\x7a\xfa\x3b\xd5\x8c\xcc\x15\xf4\xed\x35\ -\x0e\x75\xda\xef\x19\xf0\xcd\xa9\x0f\xdd\x37\x15\xbe\x79\xb7\xce\ -\xa1\x4e\x5f\x7f\x0f\x23\x00\x9c\xf1\x53\xdf\x5a\x0e\x3b\xec\xee\ -\x03\x30\x47\xea\xeb\x54\x01\x83\x09\x31\x5b\x1d\xc7\x00\x1f\xa9\ -\x9f\x7b\x17\x43\x4f\x86\xfb\x9d\x5a\x30\x31\x21\x62\xa1\x87\xe3\ -\xf6\x18\x1f\xb9\x0f\x00\x41\x10\x88\x5c\x18\xef\x50\x8f\xa9\x65\ -\xab\x33\x7e\xb4\xaa\x18\xf7\x01\xd8\xb9\xfe\x20\x54\x8a\x10\x87\ -\x7a\x11\x13\x4a\xe2\x74\x25\xdc\x09\x3f\xa1\xf3\x23\xb1\x75\xd5\ -\x1e\xf6\x01\xbc\xb3\x66\x9f\xc3\xea\xed\x33\x89\xd7\x66\xb8\x5c\ -\x1c\xe1\xf3\xf8\x88\xd7\xa6\x3b\xa5\x1b\xa3\x49\xc2\x1b\x2b\x77\ -\xb2\x07\x60\xd3\xf2\xed\x88\x08\xd6\x39\xed\x58\x22\xf2\x40\x4e\ -\xda\x16\x97\x00\xac\x4d\x7f\x0b\x12\x91\x87\xd3\xfa\x89\x61\x4b\ -\x91\x9d\xfc\x3a\x3b\x00\x26\xee\xd8\x38\x23\x4b\x63\x57\x39\x15\ -\x2f\xa6\x92\xc8\x85\x09\xc8\x88\x79\x85\xb2\x5d\x7a\xcc\xcb\xec\ -\x00\xa8\xa0\x38\xc7\x7e\x26\xdb\xb2\xf7\x22\x4a\x9d\x48\xc9\x26\ -\x46\x93\x84\x6d\xd9\xf4\x26\x36\x55\xcd\x57\xd8\x01\xf0\x77\x65\ -\x1e\xed\x4f\xf9\xed\xd5\x1f\x20\x27\xed\x4d\x87\xc5\x0e\xa9\xc4\ -\x1b\x6b\xd3\xb6\xd0\x0e\x68\x4f\xdb\x79\x9a\x9d\x82\x48\x5f\x7f\ -\x0f\x5a\xbb\xf4\xb4\x8b\x22\x99\xba\x35\x48\x8a\xc8\x44\xb5\xe1\ -\x2a\x9a\x3b\xea\xd1\xfb\xb8\x1b\x23\x66\x23\x3c\xc4\x52\xf8\xfb\ -\x06\x62\xd1\x82\x68\xc4\x85\x26\xbb\x54\x11\x32\x74\x36\x50\x2a\ -\x86\x50\x02\x00\x00\x97\xaa\xf3\x5d\xaa\x0a\x49\x25\xde\x48\x8d\ -\xca\x42\x6a\x54\x16\x00\xc0\x6e\xb7\x83\x20\x98\xdb\x49\xba\x54\ -\x95\x4f\xd9\x86\x12\x80\xda\x96\x52\x94\x34\x14\x22\x64\xfe\x62\ -\x08\x05\x62\x10\x04\xc0\xe7\x09\xe1\xed\x39\x87\x56\x83\x5d\xe9\ -\xbc\x85\x34\xc3\x66\x23\xc7\xae\x2d\xb8\x79\xb7\x16\x0d\x6d\x15\ -\xec\x02\x00\x80\xdc\x8b\xdf\x3f\x35\xe4\x0b\x9f\xe7\x6a\xb9\x2c\ -\x00\x3b\xd6\x1d\x80\xa7\xc4\x0b\x6c\x8b\xd1\x34\x88\x6f\x4f\x7f\ -\x84\xfe\xa1\x07\xcf\x97\xdb\x24\x69\x85\x85\xe6\xb6\x19\xed\x9d\ -\x21\xd2\x66\x05\x69\xb3\xc2\x6c\x35\xa1\xb3\xef\x36\x0e\x9d\x7c\ -\x1f\x03\x43\x8f\x58\xed\xfc\x80\xf1\x31\x0e\x9d\x7c\x0f\x1d\xbd\ -\xad\x18\x1c\x1e\xc0\xb0\x69\x08\xc3\xa6\x21\xda\x9d\x77\x09\xc0\ -\x44\xe9\xed\xef\xc6\x77\x79\xfb\x68\x2f\x4b\x9d\x29\x8e\x7c\xfd\ -\xc7\x1e\xca\x41\xce\x6d\x00\x00\xa0\x6f\xa0\x07\x24\x69\x65\x05\ -\xc0\x88\xd9\x88\xde\xfe\x6e\xc6\xfd\x32\x0a\x40\x24\x10\x83\x1c\ -\x0b\x4c\x4c\x8b\x6d\xd4\xca\xca\x16\x1c\xe3\xc7\xe4\x98\x4c\x6b\ -\x93\x21\xd8\x18\xf7\xc9\xe8\x69\x06\x2b\x69\x81\x48\x28\x76\xa8\ -\x77\xab\xb3\x11\xad\x5d\x7a\x48\xc4\x52\x98\xcc\x46\x84\x07\xc7\ -\x21\xc8\xc1\x91\x19\xa1\x40\x0c\xfb\xe8\xe8\xcc\x06\x60\x21\xcd\ -\xa8\x6a\xbe\x32\xed\x12\xb6\xe6\x56\x09\x7e\x29\xf8\x62\xdc\xbd\ -\x82\xd2\xdf\xb1\x3d\xe7\x13\x68\x83\xa2\xa7\xb1\xbb\xc6\x4a\x80\ -\x65\xfc\x98\x5c\x6d\x4b\x29\xee\x3f\xee\x84\x48\x28\x81\xff\x84\ -\x13\x1d\x65\xfa\xcb\x38\x7e\xfe\xf0\x24\x1b\x3b\xec\x28\x6f\x2a\ -\x42\x90\x42\x0d\x85\xaf\x72\xdc\xb3\x1b\xed\x55\x28\xb8\x96\x8b\ -\xc2\xeb\xa7\x58\x19\x56\x04\x9b\x3f\x4d\x05\xcc\x0d\x42\x5a\x74\ -\x16\xc2\x54\xb1\xa8\x31\x5c\x43\x7e\xc9\x09\x87\x36\x1b\x97\x6d\ -\x43\x78\xb0\x0e\x75\x2d\x65\x28\x69\x2c\x44\xcf\xc3\x0e\x56\xe7\ -\x16\x04\xf7\xd7\xd8\x2c\x17\x0e\x00\x07\x80\x03\xc0\x01\xe0\x00\ -\x70\x00\x38\x00\xb3\x57\xfe\x1b\x00\x50\x37\x09\xa9\xfa\xed\xb7\ -\x72\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ -\x00\x00\x15\x73\ -\x89\ -\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ -\x00\x03\xe8\x00\x00\x00\x44\x08\x06\x00\x00\x00\x12\x95\x1f\xec\ -\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ -\x01\x00\x9a\x9c\x18\x00\x00\x0a\x4f\x69\x43\x43\x50\x50\x68\x6f\ -\x74\x6f\x73\x68\x6f\x70\x20\x49\x43\x43\x20\x70\x72\x6f\x66\x69\ -\x6c\x65\x00\x00\x78\xda\x9d\x53\x67\x54\x53\xe9\x16\x3d\xf7\xde\ -\xf4\x42\x4b\x88\x80\x94\x4b\x6f\x52\x15\x08\x20\x52\x42\x8b\x80\ -\x14\x91\x26\x2a\x21\x09\x10\x4a\x88\x21\xa1\xd9\x15\x51\xc1\x11\ -\x45\x45\x04\x1b\xc8\xa0\x88\x03\x8e\x8e\x80\x8c\x15\x51\x2c\x0c\ -\x8a\x0a\xd8\x07\xe4\x21\xa2\x8e\x83\xa3\x88\x8a\xca\xfb\xe1\x7b\ -\xa3\x6b\xd6\xbc\xf7\xe6\xcd\xfe\xb5\xd7\x3e\xe7\xac\xf3\x9d\xb3\ -\xcf\x07\xc0\x08\x0c\x96\x48\x33\x51\x35\x80\x0c\xa9\x42\x1e\x11\ -\xe0\x83\xc7\xc4\xc6\xe1\xe4\x2e\x40\x81\x0a\x24\x70\x00\x10\x08\ -\xb3\x64\x21\x73\xfd\x23\x01\x00\xf8\x7e\x3c\x3c\x2b\x22\xc0\x07\ -\xbe\x00\x01\x78\xd3\x0b\x08\x00\xc0\x4d\x9b\xc0\x30\x1c\x87\xff\ -\x0f\xea\x42\x99\x5c\x01\x80\x84\x01\xc0\x74\x91\x38\x4b\x08\x80\ -\x14\x00\x40\x7a\x8e\x42\xa6\x00\x40\x46\x01\x80\x9d\x98\x26\x53\ -\x00\xa0\x04\x00\x60\xcb\x63\x62\xe3\x00\x50\x2d\x00\x60\x27\x7f\ -\xe6\xd3\x00\x80\x9d\xf8\x99\x7b\x01\x00\x5b\x94\x21\x15\x01\xa0\ -\x91\x00\x20\x13\x65\x88\x44\x00\x68\x3b\x00\xac\xcf\x56\x8a\x45\ -\x00\x58\x30\x00\x14\x66\x4b\xc4\x39\x00\xd8\x2d\x00\x30\x49\x57\ -\x66\x48\x00\xb0\xb7\x00\xc0\xce\x10\x0b\xb2\x00\x08\x0c\x00\x30\ -\x51\x88\x85\x29\x00\x04\x7b\x00\x60\xc8\x23\x23\x78\x00\x84\x99\ -\x00\x14\x46\xf2\x57\x3c\xf1\x2b\xae\x10\xe7\x2a\x00\x00\x78\x99\ -\xb2\x3c\xb9\x24\x39\x45\x81\x5b\x08\x2d\x71\x07\x57\x57\x2e\x1e\ -\x28\xce\x49\x17\x2b\x14\x36\x61\x02\x61\x9a\x40\x2e\xc2\x79\x99\ -\x19\x32\x81\x34\x0f\xe0\xf3\xcc\x00\x00\xa0\x91\x15\x11\xe0\x83\ -\xf3\xfd\x78\xce\x0e\xae\xce\xce\x36\x8e\xb6\x0e\x5f\x2d\xea\xbf\ -\x06\xff\x22\x62\x62\xe3\xfe\xe5\xcf\xab\x70\x40\x00\x00\xe1\x74\ -\x7e\xd1\xfe\x2c\x2f\xb3\x1a\x80\x3b\x06\x80\x6d\xfe\xa2\x25\xee\ -\x04\x68\x5e\x0b\xa0\x75\xf7\x8b\x66\xb2\x0f\x40\xb5\x00\xa0\xe9\ -\xda\x57\xf3\x70\xf8\x7e\x3c\x3c\x45\xa1\x90\xb9\xd9\xd9\xe5\xe4\ -\xe4\xd8\x4a\xc4\x42\x5b\x61\xca\x57\x7d\xfe\x67\xc2\x5f\xc0\x57\ -\xfd\x6c\xf9\x7e\x3c\xfc\xf7\xf5\xe0\xbe\xe2\x24\x81\x32\x5d\x81\ -\x47\x04\xf8\xe0\xc2\xcc\xf4\x4c\xa5\x1c\xcf\x92\x09\x84\x62\xdc\ -\xe6\x8f\x47\xfc\xb7\x0b\xff\xfc\x1d\xd3\x22\xc4\x49\x62\xb9\x58\ -\x2a\x14\xe3\x51\x12\x71\x8e\x44\x9a\x8c\xf3\x32\xa5\x22\x89\x42\ -\x92\x29\xc5\x25\xd2\xff\x64\xe2\xdf\x2c\xfb\x03\x3e\xdf\x35\x00\ -\xb0\x6a\x3e\x01\x7b\x91\x2d\xa8\x5d\x63\x03\xf6\x4b\x27\x10\x58\ -\x74\xc0\xe2\xf7\x00\x00\xf2\xbb\x6f\xc1\xd4\x28\x08\x03\x80\x68\ -\x83\xe1\xcf\x77\xff\xef\x3f\xfd\x47\xa0\x25\x00\x80\x66\x49\x92\ -\x71\x00\x00\x5e\x44\x24\x2e\x54\xca\xb3\x3f\xc7\x08\x00\x00\x44\ -\xa0\x81\x2a\xb0\x41\x1b\xf4\xc1\x18\x2c\xc0\x06\x1c\xc1\x05\xdc\ -\xc1\x0b\xfc\x60\x36\x84\x42\x24\xc4\xc2\x42\x10\x42\x0a\x64\x80\ -\x1c\x72\x60\x29\xac\x82\x42\x28\x86\xcd\xb0\x1d\x2a\x60\x2f\xd4\ -\x40\x1d\x34\xc0\x51\x68\x86\x93\x70\x0e\x2e\xc2\x55\xb8\x0e\x3d\ -\x70\x0f\xfa\x61\x08\x9e\xc1\x28\xbc\x81\x09\x04\x41\xc8\x08\x13\ -\x61\x21\xda\x88\x01\x62\x8a\x58\x23\x8e\x08\x17\x99\x85\xf8\x21\ -\xc1\x48\x04\x12\x8b\x24\x20\xc9\x88\x14\x51\x22\x4b\x91\x35\x48\ -\x31\x52\x8a\x54\x20\x55\x48\x1d\xf2\x3d\x72\x02\x39\x87\x5c\x46\ -\xba\x91\x3b\xc8\x00\x32\x82\xfc\x86\xbc\x47\x31\x94\x81\xb2\x51\ -\x3d\xd4\x0c\xb5\x43\xb9\xa8\x37\x1a\x84\x46\xa2\x0b\xd0\x64\x74\ -\x31\x9a\x8f\x16\xa0\x9b\xd0\x72\xb4\x1a\x3d\x8c\x36\xa1\xe7\xd0\ -\xab\x68\x0f\xda\x8f\x3e\x43\xc7\x30\xc0\xe8\x18\x07\x33\xc4\x6c\ -\x30\x2e\xc6\xc3\x42\xb1\x38\x2c\x09\x93\x63\xcb\xb1\x22\xac\x0c\ -\xab\xc6\x1a\xb0\x56\xac\x03\xbb\x89\xf5\x63\xcf\xb1\x77\x04\x12\ -\x81\x45\xc0\x09\x36\x04\x77\x42\x20\x61\x1e\x41\x48\x58\x4c\x58\ -\x4e\xd8\x48\xa8\x20\x1c\x24\x34\x11\xda\x09\x37\x09\x03\x84\x51\ -\xc2\x27\x22\x93\xa8\x4b\xb4\x26\xba\x11\xf9\xc4\x18\x62\x32\x31\ -\x87\x58\x48\x2c\x23\xd6\x12\x8f\x13\x2f\x10\x7b\x88\x43\xc4\x37\ -\x24\x12\x89\x43\x32\x27\xb9\x90\x02\x49\xb1\xa4\x54\xd2\x12\xd2\ -\x46\xd2\x6e\x52\x23\xe9\x2c\xa9\x9b\x34\x48\x1a\x23\x93\xc9\xda\ -\x64\x6b\xb2\x07\x39\x94\x2c\x20\x2b\xc8\x85\xe4\x9d\xe4\xc3\xe4\ -\x33\xe4\x1b\xe4\x21\xf2\x5b\x0a\x9d\x62\x40\x71\xa4\xf8\x53\xe2\ -\x28\x52\xca\x6a\x4a\x19\xe5\x10\xe5\x34\xe5\x06\x65\x98\x32\x41\ -\x55\xa3\x9a\x52\xdd\xa8\xa1\x54\x11\x35\x8f\x5a\x42\xad\xa1\xb6\ -\x52\xaf\x51\x87\xa8\x13\x34\x75\x9a\x39\xcd\x83\x16\x49\x4b\xa5\ -\xad\xa2\x95\xd3\x1a\x68\x17\x68\xf7\x69\xaf\xe8\x74\xba\x11\xdd\ -\x95\x1e\x4e\x97\xd0\x57\xd2\xcb\xe9\x47\xe8\x97\xe8\x03\xf4\x77\ -\x0c\x0d\x86\x15\x83\xc7\x88\x67\x28\x19\x9b\x18\x07\x18\x67\x19\ -\x77\x18\xaf\x98\x4c\xa6\x19\xd3\x8b\x19\xc7\x54\x30\x37\x31\xeb\ -\x98\xe7\x99\x0f\x99\x6f\x55\x58\x2a\xb6\x2a\x7c\x15\x91\xca\x0a\ -\x95\x4a\x95\x26\x95\x1b\x2a\x2f\x54\xa9\xaa\xa6\xaa\xde\xaa\x0b\ -\x55\xf3\x55\xcb\x54\x8f\xa9\x5e\x53\x7d\xae\x46\x55\x33\x53\xe3\ -\xa9\x09\xd4\x96\xab\x55\xaa\x9d\x50\xeb\x53\x1b\x53\x67\xa9\x3b\ -\xa8\x87\xaa\x67\xa8\x6f\x54\x3f\xa4\x7e\x59\xfd\x89\x06\x59\xc3\ -\x4c\xc3\x4f\x43\xa4\x51\xa0\xb1\x5f\xe3\xbc\xc6\x20\x0b\x63\x19\ -\xb3\x78\x2c\x21\x6b\x0d\xab\x86\x75\x81\x35\xc4\x26\xb1\xcd\xd9\ -\x7c\x76\x2a\xbb\x98\xfd\x1d\xbb\x8b\x3d\xaa\xa9\xa1\x39\x43\x33\ -\x4a\x33\x57\xb3\x52\xf3\x94\x66\x3f\x07\xe3\x98\x71\xf8\x9c\x74\ -\x4e\x09\xe7\x28\xa7\x97\xf3\x7e\x8a\xde\x14\xef\x29\xe2\x29\x1b\ -\xa6\x34\x4c\xb9\x31\x65\x5c\x6b\xaa\x96\x97\x96\x58\xab\x48\xab\ -\x51\xab\x47\xeb\xbd\x36\xae\xed\xa7\x9d\xa6\xbd\x45\xbb\x59\xfb\ -\x81\x0e\x41\xc7\x4a\x27\x5c\x27\x47\x67\x8f\xce\x05\x9d\xe7\x53\ -\xd9\x53\xdd\xa7\x0a\xa7\x16\x4d\x3d\x3a\xf5\xae\x2e\xaa\x6b\xa5\ -\x1b\xa1\xbb\x44\x77\xbf\x6e\xa7\xee\x98\x9e\xbe\x5e\x80\x9e\x4c\ -\x6f\xa7\xde\x79\xbd\xe7\xfa\x1c\x7d\x2f\xfd\x54\xfd\x6d\xfa\xa7\ -\xf5\x47\x0c\x58\x06\xb3\x0c\x24\x06\xdb\x0c\xce\x18\x3c\xc5\x35\ -\x71\x6f\x3c\x1d\x2f\xc7\xdb\xf1\x51\x43\x5d\xc3\x40\x43\xa5\x61\ -\x95\x61\x97\xe1\x84\x91\xb9\xd1\x3c\xa3\xd5\x46\x8d\x46\x0f\x8c\ -\x69\xc6\x5c\xe3\x24\xe3\x6d\xc6\x6d\xc6\xa3\x26\x06\x26\x21\x26\ -\x4b\x4d\xea\x4d\xee\x9a\x52\x4d\xb9\xa6\x29\xa6\x3b\x4c\x3b\x4c\ -\xc7\xcd\xcc\xcd\xa2\xcd\xd6\x99\x35\x9b\x3d\x31\xd7\x32\xe7\x9b\ -\xe7\x9b\xd7\x9b\xdf\xb7\x60\x5a\x78\x5a\x2c\xb6\xa8\xb6\xb8\x65\ -\x49\xb2\xe4\x5a\xa6\x59\xee\xb6\xbc\x6e\x85\x5a\x39\x59\xa5\x58\ -\x55\x5a\x5d\xb3\x46\xad\x9d\xad\x25\xd6\xbb\xad\xbb\xa7\x11\xa7\ -\xb9\x4e\x93\x4e\xab\x9e\xd6\x67\xc3\xb0\xf1\xb6\xc9\xb6\xa9\xb7\ -\x19\xb0\xe5\xd8\x06\xdb\xae\xb6\x6d\xb6\x7d\x61\x67\x62\x17\x67\ -\xb7\xc5\xae\xc3\xee\x93\xbd\x93\x7d\xba\x7d\x8d\xfd\x3d\x07\x0d\ -\x87\xd9\x0e\xab\x1d\x5a\x1d\x7e\x73\xb4\x72\x14\x3a\x56\x3a\xde\ -\x9a\xce\x9c\xee\x3f\x7d\xc5\xf4\x96\xe9\x2f\x67\x58\xcf\x10\xcf\ -\xd8\x33\xe3\xb6\x13\xcb\x29\xc4\x69\x9d\x53\x9b\xd3\x47\x67\x17\ -\x67\xb9\x73\x83\xf3\x88\x8b\x89\x4b\x82\xcb\x2e\x97\x3e\x2e\x9b\ -\x1b\xc6\xdd\xc8\xbd\xe4\x4a\x74\xf5\x71\x5d\xe1\x7a\xd2\xf5\x9d\ -\x9b\xb3\x9b\xc2\xed\xa8\xdb\xaf\xee\x36\xee\x69\xee\x87\xdc\x9f\ -\xcc\x34\x9f\x29\x9e\x59\x33\x73\xd0\xc3\xc8\x43\xe0\x51\xe5\xd1\ -\x3f\x0b\x9f\x95\x30\x6b\xdf\xac\x7e\x4f\x43\x4f\x81\x67\xb5\xe7\ -\x23\x2f\x63\x2f\x91\x57\xad\xd7\xb0\xb7\xa5\x77\xaa\xf7\x61\xef\ -\x17\x3e\xf6\x3e\x72\x9f\xe3\x3e\xe3\x3c\x37\xde\x32\xde\x59\x5f\ -\xcc\x37\xc0\xb7\xc8\xb7\xcb\x4f\xc3\x6f\x9e\x5f\x85\xdf\x43\x7f\ -\x23\xff\x64\xff\x7a\xff\xd1\x00\xa7\x80\x25\x01\x67\x03\x89\x81\ -\x41\x81\x5b\x02\xfb\xf8\x7a\x7c\x21\xbf\x8e\x3f\x3a\xdb\x65\xf6\ -\xb2\xd9\xed\x41\x8c\xa0\xb9\x41\x15\x41\x8f\x82\xad\x82\xe5\xc1\ -\xad\x21\x68\xc8\xec\x90\xad\x21\xf7\xe7\x98\xce\x91\xce\x69\x0e\ -\x85\x50\x7e\xe8\xd6\xd0\x07\x61\xe6\x61\x8b\xc3\x7e\x0c\x27\x85\ -\x87\x85\x57\x86\x3f\x8e\x70\x88\x58\x1a\xd1\x31\x97\x35\x77\xd1\ -\xdc\x43\x73\xdf\x44\xfa\x44\x96\x44\xde\x9b\x67\x31\x4f\x39\xaf\ -\x2d\x4a\x35\x2a\x3e\xaa\x2e\x6a\x3c\xda\x37\xba\x34\xba\x3f\xc6\ -\x2e\x66\x59\xcc\xd5\x58\x9d\x58\x49\x6c\x4b\x1c\x39\x2e\x2a\xae\ -\x36\x6e\x6c\xbe\xdf\xfc\xed\xf3\x87\xe2\x9d\xe2\x0b\xe3\x7b\x17\ -\x98\x2f\xc8\x5d\x70\x79\xa1\xce\xc2\xf4\x85\xa7\x16\xa9\x2e\x12\ -\x2c\x3a\x96\x40\x4c\x88\x4e\x38\x94\xf0\x41\x10\x2a\xa8\x16\x8c\ -\x25\xf2\x13\x77\x25\x8e\x0a\x79\xc2\x1d\xc2\x67\x22\x2f\xd1\x36\ -\xd1\x88\xd8\x43\x5c\x2a\x1e\x4e\xf2\x48\x2a\x4d\x7a\x92\xec\x91\ -\xbc\x35\x79\x24\xc5\x33\xa5\x2c\xe5\xb9\x84\x27\xa9\x90\xbc\x4c\ -\x0d\x4c\xdd\x9b\x3a\x9e\x16\x9a\x76\x20\x6d\x32\x3d\x3a\xbd\x31\ -\x83\x92\x91\x90\x71\x42\xaa\x21\x4d\x93\xb6\x67\xea\x67\xe6\x66\ -\x76\xcb\xac\x65\x85\xb2\xfe\xc5\x6e\x8b\xb7\x2f\x1e\x95\x07\xc9\ -\x6b\xb3\x90\xac\x05\x59\x2d\x0a\xb6\x42\xa6\xe8\x54\x5a\x28\xd7\ -\x2a\x07\xb2\x67\x65\x57\x66\xbf\xcd\x89\xca\x39\x96\xab\x9e\x2b\ -\xcd\xed\xcc\xb3\xca\xdb\x90\x37\x9c\xef\x9f\xff\xed\x12\xc2\x12\ -\xe1\x92\xb6\xa5\x86\x4b\x57\x2d\x1d\x58\xe6\xbd\xac\x6a\x39\xb2\ -\x3c\x71\x79\xdb\x0a\xe3\x15\x05\x2b\x86\x56\x06\xac\x3c\xb8\x8a\ -\xb6\x2a\x6d\xd5\x4f\xab\xed\x57\x97\xae\x7e\xbd\x26\x7a\x4d\x6b\ -\x81\x5e\xc1\xca\x82\xc1\xb5\x01\x6b\xeb\x0b\x55\x0a\xe5\x85\x7d\ -\xeb\xdc\xd7\xed\x5d\x4f\x58\x2f\x59\xdf\xb5\x61\xfa\x86\x9d\x1b\ -\x3e\x15\x89\x8a\xae\x14\xdb\x17\x97\x15\x7f\xd8\x28\xdc\x78\xe5\ -\x1b\x87\x6f\xca\xbf\x99\xdc\x94\xb4\xa9\xab\xc4\xb9\x64\xcf\x66\ -\xd2\x66\xe9\xe6\xde\x2d\x9e\x5b\x0e\x96\xaa\x97\xe6\x97\x0e\x6e\ -\x0d\xd9\xda\xb4\x0d\xdf\x56\xb4\xed\xf5\xf6\x45\xdb\x2f\x97\xcd\ -\x28\xdb\xbb\x83\xb6\x43\xb9\xa3\xbf\x3c\xb8\xbc\x65\xa7\xc9\xce\ -\xcd\x3b\x3f\x54\xa4\x54\xf4\x54\xfa\x54\x36\xee\xd2\xdd\xb5\x61\ -\xd7\xf8\x6e\xd1\xee\x1b\x7b\xbc\xf6\x34\xec\xd5\xdb\x5b\xbc\xf7\ -\xfd\x3e\xc9\xbe\xdb\x55\x01\x55\x4d\xd5\x66\xd5\x65\xfb\x49\xfb\ -\xb3\xf7\x3f\xae\x89\xaa\xe9\xf8\x96\xfb\x6d\x5d\xad\x4e\x6d\x71\ -\xed\xc7\x03\xd2\x03\xfd\x07\x23\x0e\xb6\xd7\xb9\xd4\xd5\x1d\xd2\ -\x3d\x54\x52\x8f\xd6\x2b\xeb\x47\x0e\xc7\x1f\xbe\xfe\x9d\xef\x77\ -\x2d\x0d\x36\x0d\x55\x8d\x9c\xc6\xe2\x23\x70\x44\x79\xe4\xe9\xf7\ -\x09\xdf\xf7\x1e\x0d\x3a\xda\x76\x8c\x7b\xac\xe1\x07\xd3\x1f\x76\ -\x1d\x67\x1d\x2f\x6a\x42\x9a\xf2\x9a\x46\x9b\x53\x9a\xfb\x5b\x62\ -\x5b\xba\x4f\xcc\x3e\xd1\xd6\xea\xde\x7a\xfc\x47\xdb\x1f\x0f\x9c\ -\x34\x3c\x59\x79\x4a\xf3\x54\xc9\x69\xda\xe9\x82\xd3\x93\x67\xf2\ -\xcf\x8c\x9d\x95\x9d\x7d\x7e\x2e\xf9\xdc\x60\xdb\xa2\xb6\x7b\xe7\ -\x63\xce\xdf\x6a\x0f\x6f\xef\xba\x10\x74\xe1\xd2\x45\xff\x8b\xe7\ -\x3b\xbc\x3b\xce\x5c\xf2\xb8\x74\xf2\xb2\xdb\xe5\x13\x57\xb8\x57\ -\x9a\xaf\x3a\x5f\x6d\xea\x74\xea\x3c\xfe\x93\xd3\x4f\xc7\xbb\x9c\ -\xbb\x9a\xae\xb9\x5c\x6b\xb9\xee\x7a\xbd\xb5\x7b\x66\xf7\xe9\x1b\ -\x9e\x37\xce\xdd\xf4\xbd\x79\xf1\x16\xff\xd6\xd5\x9e\x39\x3d\xdd\ -\xbd\xf3\x7a\x6f\xf7\xc5\xf7\xf5\xdf\x16\xdd\x7e\x72\x27\xfd\xce\ -\xcb\xbb\xd9\x77\x27\xee\xad\xbc\x4f\xbc\x5f\xf4\x40\xed\x41\xd9\ -\x43\xdd\x87\xd5\x3f\x5b\xfe\xdc\xd8\xef\xdc\x7f\x6a\xc0\x77\xa0\ -\xf3\xd1\xdc\x47\xf7\x06\x85\x83\xcf\xfe\x91\xf5\x8f\x0f\x43\x05\ -\x8f\x99\x8f\xcb\x86\x0d\x86\xeb\x9e\x38\x3e\x39\x39\xe2\x3f\x72\ -\xfd\xe9\xfc\xa7\x43\xcf\x64\xcf\x26\x9e\x17\xfe\xa2\xfe\xcb\xae\ -\x17\x16\x2f\x7e\xf8\xd5\xeb\xd7\xce\xd1\x98\xd1\xa1\x97\xf2\x97\ -\x93\xbf\x6d\x7c\xa5\xfd\xea\xc0\xeb\x19\xaf\xdb\xc6\xc2\xc6\x1e\ -\xbe\xc9\x78\x33\x31\x5e\xf4\x56\xfb\xed\xc1\x77\xdc\x77\x1d\xef\ -\xa3\xdf\x0f\x4f\xe4\x7c\x20\x7f\x28\xff\x68\xf9\xb1\xf5\x53\xd0\ -\xa7\xfb\x93\x19\x93\x93\xff\x04\x03\x98\xf3\xfc\x63\x33\x2d\xdb\ -\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\x25\x00\x00\x80\x83\ -\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00\x75\x30\x00\x00\xea\x60\ -\x00\x00\x3a\x98\x00\x00\x17\x6f\x92\x5f\xc5\x46\x00\x00\x0a\x9e\ -\x49\x44\x41\x54\x78\xda\xec\xdd\xdb\x6f\x5c\x57\xd9\x07\xe0\x77\ -\x4e\x7b\x66\x7c\xc0\x63\x37\x4e\x30\x10\x3e\x7d\x11\x4d\xa4\xba\ -\x84\x42\x50\x73\x81\x7a\x81\xb8\xe6\xaf\xe5\xf2\x13\xe2\xa2\xaa\ -\x3e\x15\x51\x28\x55\x83\x08\x95\x2c\xa0\xa5\x26\x24\xb5\x9d\xf8\ -\x30\xc7\x3d\x9b\x8b\xe9\x8c\xc7\xf6\x1c\xec\xb1\xad\xd8\xf1\xf3\ -\xdc\x45\x3b\xb3\x67\xf6\x5a\x2b\x52\x7e\x7b\x1d\xde\xdc\xa3\x47\ -\x8f\xb2\x00\xb8\x24\xf5\x7a\x5d\x23\x00\x00\xc0\x29\xe4\x35\x01\ -\x00\x00\x00\x08\xe8\x00\x00\x00\x80\x80\x0e\x00\x00\x00\x02\x3a\ -\x00\x00\x00\x20\xa0\x03\x00\x00\x80\x80\x0e\x00\x00\x00\x08\xe8\ -\x00\x00\x00\x20\xa0\x03\x00\x00\x00\x02\x3a\x00\x00\x00\x08\xe8\ -\x00\x00\x00\x80\x80\x0e\x00\x00\x00\x02\x3a\x00\x00\x00\x20\xa0\ -\x03\x00\x00\x80\x80\x0e\x00\x00\x00\x08\xe8\x00\x00\x00\x20\xa0\ -\x03\x00\x00\x00\x02\x3a\x00\x00\x00\x08\xe8\x00\x00\x00\x80\x80\ -\x0e\x00\x00\x00\x02\x3a\x00\x00\x00\x20\xa0\x03\x00\x00\x80\x80\ -\x0e\x00\x00\x00\x08\xe8\x00\x00\x00\x20\xa0\x03\x00\x00\x00\x02\ -\x3a\x00\x00\x00\x08\xe8\x00\x00\x00\xc0\x0c\x8a\x97\xfd\x05\x95\ -\x3b\xe9\xc4\xeb\xad\xed\x7c\x74\x5b\xb9\xb1\xd7\xf3\x49\x16\xc9\ -\x72\xf7\x54\x7f\xf7\xc8\x83\x2d\x64\x51\x9c\xef\x7d\xae\xf1\xac\ -\x70\xaa\xdf\xd8\xd9\xcf\x47\x67\x6f\xf2\xfd\x93\x95\x6e\x94\x57\ -\xd3\xc8\x17\xb3\x88\x88\x68\xbf\xca\x47\x6b\xbb\x30\xf5\x73\xd3\ -\x7e\xcf\xf0\xf5\x69\xcf\x99\xac\x74\x23\x5f\xca\xa2\xdb\xce\x45\ -\x6b\x6b\xf6\x77\x2c\xc5\x85\x2c\x92\xe5\x34\x4a\xdf\xe9\x7d\x6f\ -\xb7\x93\x8b\xe6\xf3\xc2\xd4\x7b\x4e\xeb\xd3\x61\xc3\x6d\x7a\x96\ -\xcf\x1d\xff\xfc\xf0\x38\x38\x8b\xe3\x6d\x39\xad\xaf\xfb\xd7\xa7\ -\xb5\xed\x59\xc7\xe5\xf1\x71\xd3\xa9\xe7\xa3\xf9\x9f\xe9\xe3\xe6\ -\x78\x7f\x95\x6f\xa7\x51\xac\x76\x67\xbe\x07\x00\x00\x70\x83\x03\ -\xfa\xad\xf7\xeb\xd3\x43\xd4\x4e\x21\x5e\xfd\x2d\x19\x19\x5c\x93\ -\xe5\xee\xe0\x1e\x2f\x7e\x5f\x9d\x1a\xb6\xfb\xe6\xff\xa7\x1d\x8b\ -\xf7\x5a\x11\x11\xf1\xd5\x6f\x16\x4e\xf5\x1b\x77\x37\x92\x78\xf9\ -\x24\x19\x1b\xb0\xde\x7a\xd4\x88\x42\x65\x74\x48\x3c\xd8\x2c\xc5\ -\xce\x67\xc9\xd8\xa0\x36\xed\xf7\x0c\x5f\x3f\xd8\x2c\xc5\xd6\x1f\ -\xca\x63\x7f\x6f\x6d\xbd\x19\x49\x2d\x8d\xd6\x4e\x21\xfe\xf3\x61\ -\xf5\xcc\x7d\x92\x4f\xb2\xa8\x3d\x6c\xc5\xdc\x5a\xfb\xe4\xc5\xfb\ -\xbd\xfe\xd8\x79\x52\x1e\x1b\x50\x4f\xd3\xa7\x7d\xc3\x6d\x7a\x96\ -\xcf\x1d\xff\xfc\xf0\x38\x38\x8b\xe3\x63\x66\x5a\x5f\x0f\x7f\xc7\ -\xbf\x7f\x37\x3f\x36\xfc\x9e\x76\x5c\x26\x2b\xdd\x41\x7f\x9d\xb0\ -\xde\x6b\xeb\xad\x3f\x55\x26\x86\xec\x7c\x92\xc5\xad\xc7\x8d\x73\ -\xdd\x03\x00\x00\x10\xd0\x07\xd2\x46\x3e\xd2\xc6\xd1\x00\x51\x5a\ -\xe8\x46\xae\x98\x45\x52\x4b\xe3\xd6\xfb\xf5\xd8\xfa\xb4\x12\x07\ -\x5f\x16\xaf\x5c\x23\xcd\xdd\xed\xc4\xca\x7b\x8d\xc1\x9f\xdb\x7b\ -\xbd\x59\xf3\x42\x35\x8b\xd2\x42\x37\x0a\x95\x6e\xcc\xad\xb5\xa3\ -\xb4\x98\xc6\xf3\x8f\xaa\xa7\x9e\xe5\x1f\xfb\x7d\x6b\xed\x38\xb8\ -\x53\x3c\xf5\xcb\x88\xb3\x86\xf3\xd5\x5f\xd4\xa3\xb4\xd0\x1d\xf4\ -\x4b\x73\xbb\x10\x59\x27\x7a\xb3\xe9\x0b\xdd\x48\x6a\x69\xac\x3e\ -\xae\xc7\xf3\x8f\xab\x13\x67\x91\x47\xf5\xe9\x71\x9d\x7a\xfe\xc8\ -\x8b\x98\xe3\x0a\x95\x6c\xf0\xd2\x63\xd4\xf5\xfe\xe7\xbb\xed\xdc\ -\xc8\xeb\xfd\x31\x94\x75\x72\xd1\xde\x3b\xf9\x5b\xbb\xed\xd9\xfb\ -\x62\xe5\xa7\x8d\x99\x5e\x80\x0c\x87\xf3\xd5\xc7\xf5\xc8\xf5\x57\ -\x5b\x8c\x18\x37\x49\x2d\x8d\x3b\x1f\x1c\xc4\xb3\x0f\xe7\x46\x06\ -\xec\x7c\x92\xc5\xda\xaf\x0e\x8e\xdc\xa3\xbd\xdb\x6b\x87\xd2\xe2\ -\x61\x7f\x4d\xba\x07\x00\x00\x20\xa0\x1f\x71\xf0\x75\x71\xe4\x8c\ -\xe5\x70\xf8\x5d\x7e\xb7\x19\x8d\x67\x85\x73\x07\xdc\x0b\x6d\xa0\ -\x85\x2c\x96\xdf\x6d\x46\x44\x44\xd6\xc9\xc5\x37\x7f\xac\x9c\x08\ -\xce\x2b\x3f\x6f\xf6\x02\xfa\x42\x37\x6a\x0f\x5b\x13\x67\xbf\x4f\ -\x6b\xf9\x61\x33\x36\xff\x6f\xee\xc2\x9f\x67\xf1\xed\xf6\x20\x9c\ -\x8f\x9a\xa9\xaf\xdc\xe9\xbd\x2c\xc9\x15\xb3\x58\xfe\x49\x23\x9e\ -\xfd\x6e\xee\xcc\x7d\x3a\xce\xa8\xb0\xbb\xb4\xde\x1a\xac\x1c\x98\ -\x14\x86\x5b\x5b\xf9\x91\xd7\x6f\x7f\x50\x8f\xa4\x96\x46\x7b\x2f\ -\x7f\xae\x30\x3d\x32\x60\xd7\xd2\x98\xbf\xd7\x89\xfd\x8d\xb3\xff\ -\x33\xc9\x27\xd9\x20\x9c\x67\x9d\x5c\x6c\x7f\x5e\x3e\xf1\xf2\x69\ -\xf9\xbd\x66\xcc\xdf\x6d\x47\xae\x98\x45\xed\xc7\xcd\x78\xf1\xff\ -\x95\x13\xf7\xa9\x3d\x6c\x0d\xc2\xf9\xa8\x59\xff\xf9\x7b\x9d\x58\ -\x5e\x6f\x44\xae\x98\x9d\xfb\x85\x02\x00\x00\xf0\x7a\xbd\xf6\x43\ -\xe2\x0e\xbe\x2c\xc6\xc1\x66\x29\x22\x22\x72\xc5\x6c\xa6\x7d\xca\ -\x97\x69\xf1\x47\x87\x01\x69\xfb\xf3\xf2\xc8\x59\xed\x9d\xcf\x0e\ -\x43\x53\x75\xb5\x73\x21\xdf\x5b\xa8\x74\x63\x69\xbd\x75\xe1\xcf\ -\x53\x5e\x39\x6c\xdf\x57\x7f\x3d\x19\xae\x1b\xcf\x0a\x83\x99\xea\ -\xd2\x42\x37\x92\x95\xee\x8d\xfe\x07\x52\x7b\xd0\x8c\x7c\x92\xcd\ -\xf4\x22\xa4\x3f\x6e\x5e\x6d\x24\x23\x57\x86\xbc\xfc\x4b\x12\x59\ -\xe7\xdb\xfd\xf9\xb7\x46\x8f\x9b\xfe\x78\xca\x3a\xb9\xd8\xfd\xa2\ -\x74\xe2\xfa\xfe\x46\x31\xd2\x46\x7e\xf0\x42\x61\x96\xdf\x0a\x00\ -\x00\x08\xe8\x47\x42\x61\x5f\xff\xc0\xb2\xab\xa2\xb2\xda\x0b\xb4\ -\xed\xbd\xfc\xd8\xe5\xf7\xdd\x56\xee\xc8\x4b\x86\xf3\xea\x07\xae\ -\xc5\x7b\xad\x28\x2e\x5c\x4e\xe0\x4a\x1b\xe3\x0f\xc4\xdb\xff\xd7\ -\x61\x10\xcc\x97\x6e\x66\xe0\xeb\xf7\x41\xae\xd8\xdb\xaf\x7f\xe6\ -\x71\x73\xbb\x33\xb8\xcf\xee\xd3\xd2\xd8\x71\xb3\xf7\xcf\xc3\x6b\ -\xa3\x5e\x4e\xf5\xc7\x53\xa7\x91\x1b\xbb\xb2\x64\xf8\x1e\xb3\x1c\ -\xa4\x07\x00\x00\x5c\x0d\x45\x4d\x30\xd9\x60\x7f\xf4\xf6\xe4\xfd\ -\xe0\x5b\x7f\x28\xc7\x56\x94\x2f\xe4\x3b\x5f\xfe\x35\x89\xe5\x77\ -\x9b\x97\xb2\x6c\xb9\xfb\xed\x8c\x6d\xa1\xd2\x1d\xbb\x7c\x7b\x7f\ -\xa3\x18\xfb\x1b\x0b\x37\xba\xdf\xd3\x46\x2e\x0e\xbe\x4e\x62\xf1\ -\x5e\x6b\xa6\x33\x01\xfa\xdb\x08\x9a\x53\xc6\xcd\xcb\x27\xc9\xc4\ -\x6d\x02\x69\x23\x1f\x85\x4a\x37\x4a\x0b\xdd\xa8\xdc\x49\x47\xfe\ -\x86\xdd\xa7\xa5\xb1\x2f\x01\x00\x00\x80\xeb\xe3\x4a\xcc\xa0\x0f\ -\xcf\x1c\x0e\x1f\x2a\x76\x95\x9c\xe7\xb0\xb1\x33\x7f\x57\x2b\x17\ -\x3b\x4f\x7b\x61\xbf\xbf\x0f\xfa\xa2\x1c\x7c\x75\x18\xc8\x6b\x0f\ -\x9a\xb1\xb4\x7e\x79\xb3\xf4\xd7\xdd\xee\x17\xa5\xc1\x4c\xfa\xd2\ -\x3b\xcd\xd9\x82\x7e\xfd\x7c\xe3\x66\x78\x76\xfc\xad\x9f\x35\x62\ -\x69\xbd\x65\x19\x3b\x00\x00\xbc\xa1\x5e\xfb\x0c\x7a\xe5\x4e\x7a\ -\x64\x9f\x6d\xfd\xab\x82\x5e\x89\xde\x2c\xf6\xfc\xf7\x0b\x91\xd4\ -\xd2\xa8\x3d\x68\x46\xfd\xab\x8b\x39\x3c\xef\xe0\xcb\x62\xe4\x4a\ -\x95\xc1\xc1\x62\x8b\xf7\x7a\x87\xb4\xf5\x4f\x18\xdf\xff\x67\xe9\ -\xd4\xb5\xd5\xfb\x9f\x1d\xe7\x2c\x65\xf1\xae\xa2\x6e\x2b\x17\xdb\ -\x9f\x95\xe3\xd6\xfb\xbd\x53\xef\x97\xd6\x5b\x67\x3a\x14\xef\x42\ -\x5e\x12\x3c\x2d\x45\xb1\xda\x1d\x1c\x26\xd7\x6f\xf3\xd6\x4e\x21\ -\x9a\x5b\x85\xd8\xff\x47\xc9\xc9\xed\x00\x00\x20\xa0\x9f\xcd\xdc\ -\xf7\x3a\x47\x0e\x28\x8b\x38\x2c\x91\xd5\xb7\xf3\xb4\x7c\xa5\x4e\ -\x70\x9f\xe6\xf6\x07\xa3\xeb\x72\x4f\xaa\x21\x7e\x16\x5b\x7f\xaa\ -\xc4\x77\x7f\xb9\x3f\xd8\x07\x7d\x11\xa7\xc3\xf7\xc3\x7f\xba\x5f\ -\x8d\xef\xdc\x6f\x0d\x6a\x6b\x97\x16\x7a\xcb\xa8\xe7\xef\xb6\xa7\ -\xd6\x41\xbf\x49\x1a\xcf\x0a\xd1\x78\x51\x8c\xca\xad\x4e\x2c\xfc\ -\xb0\x7d\x21\x81\xf8\xac\xe3\x66\xfb\xd3\x72\xd4\x37\x8b\x47\xfa\ -\x2b\xa9\xa5\x91\xd4\xd2\x41\x58\x57\x07\x1d\x00\x00\x04\xf4\x53\ -\x2b\x54\xba\x51\xa8\x8c\xbe\xd6\xda\x29\xc4\xab\xbf\x25\xd7\x6e\ -\xb6\xb5\x1f\x96\x8e\xbb\xa8\x83\xd5\x3a\x7b\xb9\xd8\xdd\x98\x7d\ -\x1f\xf4\xd4\xe0\xf9\xac\x1a\xc5\x85\x2c\xaa\xdf\xef\xbd\x3c\x29\ -\xd7\xd2\x41\x5d\xfa\xd3\xd4\x41\x3f\xd8\x2c\x4d\xac\x5b\xdf\xda\ -\x7e\x33\x02\xfe\xd6\x27\xe5\x58\xfb\x55\x7a\x61\x67\x02\xcc\x32\ -\x6e\x46\xf5\x57\xff\xe4\xf7\x7e\x1d\xf4\x69\xfd\x05\x00\x00\x08\ -\xe8\x13\xc3\x5c\x6b\x3b\x7f\xad\x66\xcd\x87\xed\x6e\x1c\x2e\x77\ -\x2e\x54\xb3\x98\x5b\x6b\x5f\xfc\x77\x7c\x51\x8a\xb9\xef\x75\xa2\ -\x50\xe9\x5e\x4a\x6d\xf4\xce\x5e\xae\x77\xc8\x58\x94\x22\x9f\x64\ -\xb1\xf8\x76\x3b\x16\xef\xf5\x4a\xcb\xad\x3e\xae\xc7\xe6\x6f\xe7\ -\xc6\xf6\x4f\x5a\xcf\x5d\xeb\x25\xec\xa7\xd5\x6d\xe5\xe2\xd5\x46\ -\x12\x4b\xf7\x9b\x91\xd4\xd2\x98\xbb\xdb\x39\xd7\x98\x3d\xcf\xb8\ -\x39\xde\x5f\x4b\xef\xb4\x06\xcb\xdf\xdf\x7a\xd4\xb8\xf0\xf1\x01\ -\x00\x00\xbc\x81\x01\xfd\xba\x87\xb9\x42\xf5\xe4\xec\xe6\xf0\x7e\ -\xe4\xca\x9d\xf4\x52\x02\xfa\xf0\x3e\xe8\xcb\xaa\x8d\x3e\xfc\x5d\ -\x2f\x9f\x24\x51\x5e\xe9\x2d\x9f\xee\xd7\xa5\x9f\x34\x4b\x7e\x53\ -\xec\x3e\x2d\x45\xf5\x76\x27\x92\x5a\x1a\xcb\xef\x36\x63\xfb\xf3\ -\xd3\x6d\x37\x18\x55\x36\xf0\xa2\xc6\x4d\xb7\x95\x8b\xed\x4f\xcb\ -\x51\x59\x4d\xbf\x5d\xa1\xd2\xab\x5b\x6f\x16\x1d\x00\x00\xae\xa7\ -\x1b\xf1\x3f\xf9\x51\xf5\xa5\x07\x0d\x30\xe5\x44\xec\xf6\x5e\xaf\ -\x89\xaa\xab\x9d\xd7\x76\x7a\x76\x7f\x1f\x74\x44\xef\x60\xb6\x42\ -\x25\x9b\xb9\x1d\x7e\xf0\xeb\xbd\xf8\xc1\xaf\xf7\x26\xb6\xc9\xde\ -\xdf\x4b\x13\x03\xe6\x4d\xb5\xf3\xa4\x17\xca\x73\xc5\x2c\x16\x7f\ -\x34\xf9\x45\x49\x6b\xa7\xf7\x32\xaa\x5c\x4b\x67\x1e\x37\x4b\xeb\ -\xad\x41\x7f\x4d\xba\xc7\xf0\x49\xef\x37\xb5\x6e\x3d\x00\x00\x08\ -\xe8\x57\x58\x7d\xf3\x70\xd6\x77\xee\xee\xf8\x32\x65\x8b\x6f\x1f\ -\xce\x5e\x8e\x2a\xf1\x76\xf0\x75\x69\x10\xca\x96\xde\x69\xbd\xb6\ -\xe7\xd9\xfa\xa4\x1c\xd9\x50\x0d\xf3\x59\x0c\x97\x8a\x2b\xdf\x4a\ -\x27\x06\xf9\x49\x6d\x72\x53\xb5\xb6\xf2\x83\xe5\xe9\xfd\x3a\xe7\ -\xe3\xec\xff\xeb\x70\xdc\xd4\x1e\xce\x36\x6e\xda\xaf\x0e\xdb\x7e\ -\xfe\x7f\xc7\x8f\xe1\xea\xed\x8e\xce\x01\x00\x00\x01\xfd\x6a\x87\ -\xa9\xfe\xec\xf7\xdc\x5a\x7b\xe4\xd2\xf0\xc5\x07\xed\x41\x99\xb0\ -\xb4\x91\x1f\x59\xe2\x6d\xf7\x69\x69\x70\x9f\xf9\xbb\xed\x58\x7e\ -\xaf\x79\x62\x36\xb3\x72\x27\x8d\xe5\x87\xcd\x4b\x7d\x9e\xfe\x3e\ -\xe8\xf3\xb6\x49\x7f\x66\x77\xf1\x5e\x6b\x64\x4d\xed\xe1\x25\xd7\ -\xca\xde\x9d\x34\x5c\x1b\x7d\x62\x40\xdf\x28\x1e\x19\x7f\x2b\x3f\ -\x3f\x39\x6e\x92\x95\xee\xc4\xfa\xea\x07\x5f\x16\x0f\xeb\xb0\xdf\ -\x6f\xc6\xe2\x83\xf6\xc8\x31\xdc\x3f\x74\x2e\xeb\xdc\x8c\x33\x01\ -\x00\x00\xe0\x4d\x75\xad\x36\x17\xdf\x7a\xbf\x3e\x39\x3c\x6d\x24\ -\x47\xf6\xf7\x6e\xff\xb9\x12\xab\x8f\xeb\x83\xfa\xd1\x0b\x3f\x6c\ -\x0f\x42\xd3\xf0\x49\xda\x59\x27\x17\xdf\x7c\x52\x19\x7b\xf0\xd7\ -\xf3\x8f\xaa\xb1\xfa\x8b\xfa\xa0\x0c\x59\xbf\x14\x59\xae\x98\x9d\ -\x98\x49\x3d\xd8\x2c\x5d\x5a\x48\x1a\xde\x07\x3d\xab\x17\x1f\x57\ -\x06\xcf\x32\x5c\x53\x3b\x22\xa2\x50\xc9\x8e\xcc\xce\x4f\x2b\x7b\ -\x37\xad\x0e\xfa\xa8\x3e\xb9\xee\x86\xcf\x04\x98\x66\x78\xdc\xcc\ -\xad\xb5\x63\x6e\xed\xec\xe3\xe6\x9b\x4f\x0e\xc7\xf0\xd2\xfd\x66\ -\x2c\xdd\x6f\x0e\xfa\x6b\xb8\x4c\x61\xd6\xc9\x9d\x7a\x5f\x3c\x00\ -\x00\x70\x35\xbd\xd1\xeb\x97\x5b\x5b\xf9\xd8\xfc\xed\x5c\x1c\x6c\ -\x1e\x2e\x37\xee\xd7\x8f\x1e\x0e\x46\xcf\x3e\x9c\x9b\x78\xb0\x56\ -\xb7\x95\x8b\xe7\x1f\x55\x63\x77\x23\x19\x2c\x33\x4f\x6a\xe9\x91\ -\x90\xd5\xde\xcb\xc7\xd6\xa7\x95\x0b\xab\x55\x3e\x4e\x7f\x1f\xf4\ -\x79\x02\xe6\xa8\x67\x49\x6a\xe9\x20\x9c\xb7\x76\x0a\xf1\xe2\xf7\ -\xd5\xd8\xdf\x70\x38\xdc\x28\xc3\x67\x02\xcc\xd2\xd6\xc7\xc7\xcd\ -\xf6\x93\xf1\xe3\xa6\x3f\x86\x47\xf5\x57\x3f\x9c\x37\x5e\x14\xe3\ -\xf9\xc7\x55\x87\xf9\x01\x00\xc0\x35\x97\x7b\xf4\xe8\xd1\x8d\x39\ -\x55\xea\xf8\xc1\x68\xb3\xce\x74\x1f\xbf\xcf\x75\x2e\x15\x97\xac\ -\x74\x8f\x1c\x2c\x76\x9d\x9f\xe5\xba\xb5\x75\x67\x3f\x1f\x9d\xbd\ -\xdc\x1b\x3f\xf6\xea\xf5\xba\xce\x07\x00\x80\x53\xb8\x51\x53\x6e\ -\x17\xb5\xf4\xfc\x4d\xda\xe7\xab\x24\xd7\xf5\x6a\x6b\x7b\xcc\x01\ -\x00\xe0\xcd\x25\x9d\x01\x00\x00\x80\x80\x0e\x00\x00\x00\x08\xe8\ -\x00\x00\x00\x20\xa0\x03\x00\x00\x00\x02\x3a\x00\x00\x00\x08\xe8\ -\x00\x00\x00\x80\x80\x0e\x00\x00\x00\x02\x3a\x00\x00\x00\x20\xa0\ -\x03\x00\x00\x80\x80\x0e\x00\x00\x00\x08\xe8\x00\x00\x00\x20\xa0\ -\x03\x00\x00\x00\x02\x3a\x00\x00\x00\x08\xe8\x00\x00\x00\x80\x80\ -\x0e\x00\x00\x00\x02\x3a\x00\x00\x00\x20\xa0\x03\x00\x00\x80\x80\ -\x0e\x00\x00\x00\x08\xe8\x00\x00\x00\x20\xa0\x03\x00\x00\x00\x02\ -\x3a\x00\x00\x00\x5c\x23\xff\x05\x00\x00\xff\xff\x03\x00\x78\x29\ -\xcd\x85\x61\x95\xed\x47\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\ -\x60\x82\ -" - -qt_resource_name = b"\ -\x00\x03\ -\x00\x00\x78\xc3\ -\x00\x72\ -\x00\x65\x00\x73\ -\x00\x04\ -\x00\x06\xfa\x5e\ -\x00\x69\ -\x00\x63\x00\x6f\x00\x6e\ -\x00\x11\ -\x01\xb7\x20\x27\ -\x00\x70\ -\x00\x69\x00\x70\x00\x65\x00\x6c\x00\x69\x00\x6e\x00\x65\x00\x5f\x00\x6c\x00\x6f\x00\x67\x00\x6f\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\ -\x00\x08\ -\x0a\x61\x5a\xa7\ -\x00\x69\ -\x00\x63\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ -\x00\x1c\ -\x08\x07\xe3\xc7\ -\x00\x70\ -\x00\x6c\x00\x75\x00\x67\x00\x69\x00\x6e\x00\x5f\x00\x63\x00\x6f\x00\x6e\x00\x66\x00\x69\x00\x67\x00\x75\x00\x72\x00\x61\x00\x74\ -\x00\x6f\x00\x72\x00\x5f\x00\x6c\x00\x6f\x00\x67\x00\x6f\x00\x2e\x00\x70\x00\x6e\x00\x67\ -" - -qt_resource_struct = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x06\ -\x00\x00\x00\x0c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x04\ -\x00\x00\x00\x1a\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x00\x42\x00\x00\x00\x00\x00\x01\x00\x00\x17\xe5\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x07\ -\x00\x00\x00\x58\x00\x00\x00\x00\x00\x01\x00\x00\x28\xe7\ -" - -def qInitResources(): - QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) - -def qCleanupResources(): - QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) - -qInitResources() +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created by: The Resource Compiler for PyQt5 (Qt v5.7.0) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + +qt_resource_data = b"\ +\x00\x00\x17\xe1\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x46\x00\x00\x00\x46\x08\x06\x00\x00\x00\x71\x2e\xe2\x84\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ +\x01\x00\x9a\x9c\x18\x00\x00\x0a\x4f\x69\x43\x43\x50\x50\x68\x6f\ +\x74\x6f\x73\x68\x6f\x70\x20\x49\x43\x43\x20\x70\x72\x6f\x66\x69\ +\x6c\x65\x00\x00\x78\xda\x9d\x53\x67\x54\x53\xe9\x16\x3d\xf7\xde\ +\xf4\x42\x4b\x88\x80\x94\x4b\x6f\x52\x15\x08\x20\x52\x42\x8b\x80\ +\x14\x91\x26\x2a\x21\x09\x10\x4a\x88\x21\xa1\xd9\x15\x51\xc1\x11\ +\x45\x45\x04\x1b\xc8\xa0\x88\x03\x8e\x8e\x80\x8c\x15\x51\x2c\x0c\ +\x8a\x0a\xd8\x07\xe4\x21\xa2\x8e\x83\xa3\x88\x8a\xca\xfb\xe1\x7b\ +\xa3\x6b\xd6\xbc\xf7\xe6\xcd\xfe\xb5\xd7\x3e\xe7\xac\xf3\x9d\xb3\ +\xcf\x07\xc0\x08\x0c\x96\x48\x33\x51\x35\x80\x0c\xa9\x42\x1e\x11\ +\xe0\x83\xc7\xc4\xc6\xe1\xe4\x2e\x40\x81\x0a\x24\x70\x00\x10\x08\ +\xb3\x64\x21\x73\xfd\x23\x01\x00\xf8\x7e\x3c\x3c\x2b\x22\xc0\x07\ +\xbe\x00\x01\x78\xd3\x0b\x08\x00\xc0\x4d\x9b\xc0\x30\x1c\x87\xff\ +\x0f\xea\x42\x99\x5c\x01\x80\x84\x01\xc0\x74\x91\x38\x4b\x08\x80\ +\x14\x00\x40\x7a\x8e\x42\xa6\x00\x40\x46\x01\x80\x9d\x98\x26\x53\ +\x00\xa0\x04\x00\x60\xcb\x63\x62\xe3\x00\x50\x2d\x00\x60\x27\x7f\ +\xe6\xd3\x00\x80\x9d\xf8\x99\x7b\x01\x00\x5b\x94\x21\x15\x01\xa0\ +\x91\x00\x20\x13\x65\x88\x44\x00\x68\x3b\x00\xac\xcf\x56\x8a\x45\ +\x00\x58\x30\x00\x14\x66\x4b\xc4\x39\x00\xd8\x2d\x00\x30\x49\x57\ +\x66\x48\x00\xb0\xb7\x00\xc0\xce\x10\x0b\xb2\x00\x08\x0c\x00\x30\ +\x51\x88\x85\x29\x00\x04\x7b\x00\x60\xc8\x23\x23\x78\x00\x84\x99\ +\x00\x14\x46\xf2\x57\x3c\xf1\x2b\xae\x10\xe7\x2a\x00\x00\x78\x99\ +\xb2\x3c\xb9\x24\x39\x45\x81\x5b\x08\x2d\x71\x07\x57\x57\x2e\x1e\ +\x28\xce\x49\x17\x2b\x14\x36\x61\x02\x61\x9a\x40\x2e\xc2\x79\x99\ +\x19\x32\x81\x34\x0f\xe0\xf3\xcc\x00\x00\xa0\x91\x15\x11\xe0\x83\ +\xf3\xfd\x78\xce\x0e\xae\xce\xce\x36\x8e\xb6\x0e\x5f\x2d\xea\xbf\ +\x06\xff\x22\x62\x62\xe3\xfe\xe5\xcf\xab\x70\x40\x00\x00\xe1\x74\ +\x7e\xd1\xfe\x2c\x2f\xb3\x1a\x80\x3b\x06\x80\x6d\xfe\xa2\x25\xee\ +\x04\x68\x5e\x0b\xa0\x75\xf7\x8b\x66\xb2\x0f\x40\xb5\x00\xa0\xe9\ +\xda\x57\xf3\x70\xf8\x7e\x3c\x3c\x45\xa1\x90\xb9\xd9\xd9\xe5\xe4\ +\xe4\xd8\x4a\xc4\x42\x5b\x61\xca\x57\x7d\xfe\x67\xc2\x5f\xc0\x57\ +\xfd\x6c\xf9\x7e\x3c\xfc\xf7\xf5\xe0\xbe\xe2\x24\x81\x32\x5d\x81\ +\x47\x04\xf8\xe0\xc2\xcc\xf4\x4c\xa5\x1c\xcf\x92\x09\x84\x62\xdc\ +\xe6\x8f\x47\xfc\xb7\x0b\xff\xfc\x1d\xd3\x22\xc4\x49\x62\xb9\x58\ +\x2a\x14\xe3\x51\x12\x71\x8e\x44\x9a\x8c\xf3\x32\xa5\x22\x89\x42\ +\x92\x29\xc5\x25\xd2\xff\x64\xe2\xdf\x2c\xfb\x03\x3e\xdf\x35\x00\ +\xb0\x6a\x3e\x01\x7b\x91\x2d\xa8\x5d\x63\x03\xf6\x4b\x27\x10\x58\ +\x74\xc0\xe2\xf7\x00\x00\xf2\xbb\x6f\xc1\xd4\x28\x08\x03\x80\x68\ +\x83\xe1\xcf\x77\xff\xef\x3f\xfd\x47\xa0\x25\x00\x80\x66\x49\x92\ +\x71\x00\x00\x5e\x44\x24\x2e\x54\xca\xb3\x3f\xc7\x08\x00\x00\x44\ +\xa0\x81\x2a\xb0\x41\x1b\xf4\xc1\x18\x2c\xc0\x06\x1c\xc1\x05\xdc\ +\xc1\x0b\xfc\x60\x36\x84\x42\x24\xc4\xc2\x42\x10\x42\x0a\x64\x80\ +\x1c\x72\x60\x29\xac\x82\x42\x28\x86\xcd\xb0\x1d\x2a\x60\x2f\xd4\ +\x40\x1d\x34\xc0\x51\x68\x86\x93\x70\x0e\x2e\xc2\x55\xb8\x0e\x3d\ +\x70\x0f\xfa\x61\x08\x9e\xc1\x28\xbc\x81\x09\x04\x41\xc8\x08\x13\ +\x61\x21\xda\x88\x01\x62\x8a\x58\x23\x8e\x08\x17\x99\x85\xf8\x21\ +\xc1\x48\x04\x12\x8b\x24\x20\xc9\x88\x14\x51\x22\x4b\x91\x35\x48\ +\x31\x52\x8a\x54\x20\x55\x48\x1d\xf2\x3d\x72\x02\x39\x87\x5c\x46\ +\xba\x91\x3b\xc8\x00\x32\x82\xfc\x86\xbc\x47\x31\x94\x81\xb2\x51\ +\x3d\xd4\x0c\xb5\x43\xb9\xa8\x37\x1a\x84\x46\xa2\x0b\xd0\x64\x74\ +\x31\x9a\x8f\x16\xa0\x9b\xd0\x72\xb4\x1a\x3d\x8c\x36\xa1\xe7\xd0\ +\xab\x68\x0f\xda\x8f\x3e\x43\xc7\x30\xc0\xe8\x18\x07\x33\xc4\x6c\ +\x30\x2e\xc6\xc3\x42\xb1\x38\x2c\x09\x93\x63\xcb\xb1\x22\xac\x0c\ +\xab\xc6\x1a\xb0\x56\xac\x03\xbb\x89\xf5\x63\xcf\xb1\x77\x04\x12\ +\x81\x45\xc0\x09\x36\x04\x77\x42\x20\x61\x1e\x41\x48\x58\x4c\x58\ +\x4e\xd8\x48\xa8\x20\x1c\x24\x34\x11\xda\x09\x37\x09\x03\x84\x51\ +\xc2\x27\x22\x93\xa8\x4b\xb4\x26\xba\x11\xf9\xc4\x18\x62\x32\x31\ +\x87\x58\x48\x2c\x23\xd6\x12\x8f\x13\x2f\x10\x7b\x88\x43\xc4\x37\ +\x24\x12\x89\x43\x32\x27\xb9\x90\x02\x49\xb1\xa4\x54\xd2\x12\xd2\ +\x46\xd2\x6e\x52\x23\xe9\x2c\xa9\x9b\x34\x48\x1a\x23\x93\xc9\xda\ +\x64\x6b\xb2\x07\x39\x94\x2c\x20\x2b\xc8\x85\xe4\x9d\xe4\xc3\xe4\ +\x33\xe4\x1b\xe4\x21\xf2\x5b\x0a\x9d\x62\x40\x71\xa4\xf8\x53\xe2\ +\x28\x52\xca\x6a\x4a\x19\xe5\x10\xe5\x34\xe5\x06\x65\x98\x32\x41\ +\x55\xa3\x9a\x52\xdd\xa8\xa1\x54\x11\x35\x8f\x5a\x42\xad\xa1\xb6\ +\x52\xaf\x51\x87\xa8\x13\x34\x75\x9a\x39\xcd\x83\x16\x49\x4b\xa5\ +\xad\xa2\x95\xd3\x1a\x68\x17\x68\xf7\x69\xaf\xe8\x74\xba\x11\xdd\ +\x95\x1e\x4e\x97\xd0\x57\xd2\xcb\xe9\x47\xe8\x97\xe8\x03\xf4\x77\ +\x0c\x0d\x86\x15\x83\xc7\x88\x67\x28\x19\x9b\x18\x07\x18\x67\x19\ +\x77\x18\xaf\x98\x4c\xa6\x19\xd3\x8b\x19\xc7\x54\x30\x37\x31\xeb\ +\x98\xe7\x99\x0f\x99\x6f\x55\x58\x2a\xb6\x2a\x7c\x15\x91\xca\x0a\ +\x95\x4a\x95\x26\x95\x1b\x2a\x2f\x54\xa9\xaa\xa6\xaa\xde\xaa\x0b\ +\x55\xf3\x55\xcb\x54\x8f\xa9\x5e\x53\x7d\xae\x46\x55\x33\x53\xe3\ +\xa9\x09\xd4\x96\xab\x55\xaa\x9d\x50\xeb\x53\x1b\x53\x67\xa9\x3b\ +\xa8\x87\xaa\x67\xa8\x6f\x54\x3f\xa4\x7e\x59\xfd\x89\x06\x59\xc3\ +\x4c\xc3\x4f\x43\xa4\x51\xa0\xb1\x5f\xe3\xbc\xc6\x20\x0b\x63\x19\ +\xb3\x78\x2c\x21\x6b\x0d\xab\x86\x75\x81\x35\xc4\x26\xb1\xcd\xd9\ +\x7c\x76\x2a\xbb\x98\xfd\x1d\xbb\x8b\x3d\xaa\xa9\xa1\x39\x43\x33\ +\x4a\x33\x57\xb3\x52\xf3\x94\x66\x3f\x07\xe3\x98\x71\xf8\x9c\x74\ +\x4e\x09\xe7\x28\xa7\x97\xf3\x7e\x8a\xde\x14\xef\x29\xe2\x29\x1b\ +\xa6\x34\x4c\xb9\x31\x65\x5c\x6b\xaa\x96\x97\x96\x58\xab\x48\xab\ +\x51\xab\x47\xeb\xbd\x36\xae\xed\xa7\x9d\xa6\xbd\x45\xbb\x59\xfb\ +\x81\x0e\x41\xc7\x4a\x27\x5c\x27\x47\x67\x8f\xce\x05\x9d\xe7\x53\ +\xd9\x53\xdd\xa7\x0a\xa7\x16\x4d\x3d\x3a\xf5\xae\x2e\xaa\x6b\xa5\ +\x1b\xa1\xbb\x44\x77\xbf\x6e\xa7\xee\x98\x9e\xbe\x5e\x80\x9e\x4c\ +\x6f\xa7\xde\x79\xbd\xe7\xfa\x1c\x7d\x2f\xfd\x54\xfd\x6d\xfa\xa7\ +\xf5\x47\x0c\x58\x06\xb3\x0c\x24\x06\xdb\x0c\xce\x18\x3c\xc5\x35\ +\x71\x6f\x3c\x1d\x2f\xc7\xdb\xf1\x51\x43\x5d\xc3\x40\x43\xa5\x61\ +\x95\x61\x97\xe1\x84\x91\xb9\xd1\x3c\xa3\xd5\x46\x8d\x46\x0f\x8c\ +\x69\xc6\x5c\xe3\x24\xe3\x6d\xc6\x6d\xc6\xa3\x26\x06\x26\x21\x26\ +\x4b\x4d\xea\x4d\xee\x9a\x52\x4d\xb9\xa6\x29\xa6\x3b\x4c\x3b\x4c\ +\xc7\xcd\xcc\xcd\xa2\xcd\xd6\x99\x35\x9b\x3d\x31\xd7\x32\xe7\x9b\ +\xe7\x9b\xd7\x9b\xdf\xb7\x60\x5a\x78\x5a\x2c\xb6\xa8\xb6\xb8\x65\ +\x49\xb2\xe4\x5a\xa6\x59\xee\xb6\xbc\x6e\x85\x5a\x39\x59\xa5\x58\ +\x55\x5a\x5d\xb3\x46\xad\x9d\xad\x25\xd6\xbb\xad\xbb\xa7\x11\xa7\ +\xb9\x4e\x93\x4e\xab\x9e\xd6\x67\xc3\xb0\xf1\xb6\xc9\xb6\xa9\xb7\ +\x19\xb0\xe5\xd8\x06\xdb\xae\xb6\x6d\xb6\x7d\x61\x67\x62\x17\x67\ +\xb7\xc5\xae\xc3\xee\x93\xbd\x93\x7d\xba\x7d\x8d\xfd\x3d\x07\x0d\ +\x87\xd9\x0e\xab\x1d\x5a\x1d\x7e\x73\xb4\x72\x14\x3a\x56\x3a\xde\ +\x9a\xce\x9c\xee\x3f\x7d\xc5\xf4\x96\xe9\x2f\x67\x58\xcf\x10\xcf\ +\xd8\x33\xe3\xb6\x13\xcb\x29\xc4\x69\x9d\x53\x9b\xd3\x47\x67\x17\ +\x67\xb9\x73\x83\xf3\x88\x8b\x89\x4b\x82\xcb\x2e\x97\x3e\x2e\x9b\ +\x1b\xc6\xdd\xc8\xbd\xe4\x4a\x74\xf5\x71\x5d\xe1\x7a\xd2\xf5\x9d\ +\x9b\xb3\x9b\xc2\xed\xa8\xdb\xaf\xee\x36\xee\x69\xee\x87\xdc\x9f\ +\xcc\x34\x9f\x29\x9e\x59\x33\x73\xd0\xc3\xc8\x43\xe0\x51\xe5\xd1\ +\x3f\x0b\x9f\x95\x30\x6b\xdf\xac\x7e\x4f\x43\x4f\x81\x67\xb5\xe7\ +\x23\x2f\x63\x2f\x91\x57\xad\xd7\xb0\xb7\xa5\x77\xaa\xf7\x61\xef\ +\x17\x3e\xf6\x3e\x72\x9f\xe3\x3e\xe3\x3c\x37\xde\x32\xde\x59\x5f\ +\xcc\x37\xc0\xb7\xc8\xb7\xcb\x4f\xc3\x6f\x9e\x5f\x85\xdf\x43\x7f\ +\x23\xff\x64\xff\x7a\xff\xd1\x00\xa7\x80\x25\x01\x67\x03\x89\x81\ +\x41\x81\x5b\x02\xfb\xf8\x7a\x7c\x21\xbf\x8e\x3f\x3a\xdb\x65\xf6\ +\xb2\xd9\xed\x41\x8c\xa0\xb9\x41\x15\x41\x8f\x82\xad\x82\xe5\xc1\ +\xad\x21\x68\xc8\xec\x90\xad\x21\xf7\xe7\x98\xce\x91\xce\x69\x0e\ +\x85\x50\x7e\xe8\xd6\xd0\x07\x61\xe6\x61\x8b\xc3\x7e\x0c\x27\x85\ +\x87\x85\x57\x86\x3f\x8e\x70\x88\x58\x1a\xd1\x31\x97\x35\x77\xd1\ +\xdc\x43\x73\xdf\x44\xfa\x44\x96\x44\xde\x9b\x67\x31\x4f\x39\xaf\ +\x2d\x4a\x35\x2a\x3e\xaa\x2e\x6a\x3c\xda\x37\xba\x34\xba\x3f\xc6\ +\x2e\x66\x59\xcc\xd5\x58\x9d\x58\x49\x6c\x4b\x1c\x39\x2e\x2a\xae\ +\x36\x6e\x6c\xbe\xdf\xfc\xed\xf3\x87\xe2\x9d\xe2\x0b\xe3\x7b\x17\ +\x98\x2f\xc8\x5d\x70\x79\xa1\xce\xc2\xf4\x85\xa7\x16\xa9\x2e\x12\ +\x2c\x3a\x96\x40\x4c\x88\x4e\x38\x94\xf0\x41\x10\x2a\xa8\x16\x8c\ +\x25\xf2\x13\x77\x25\x8e\x0a\x79\xc2\x1d\xc2\x67\x22\x2f\xd1\x36\ +\xd1\x88\xd8\x43\x5c\x2a\x1e\x4e\xf2\x48\x2a\x4d\x7a\x92\xec\x91\ +\xbc\x35\x79\x24\xc5\x33\xa5\x2c\xe5\xb9\x84\x27\xa9\x90\xbc\x4c\ +\x0d\x4c\xdd\x9b\x3a\x9e\x16\x9a\x76\x20\x6d\x32\x3d\x3a\xbd\x31\ +\x83\x92\x91\x90\x71\x42\xaa\x21\x4d\x93\xb6\x67\xea\x67\xe6\x66\ +\x76\xcb\xac\x65\x85\xb2\xfe\xc5\x6e\x8b\xb7\x2f\x1e\x95\x07\xc9\ +\x6b\xb3\x90\xac\x05\x59\x2d\x0a\xb6\x42\xa6\xe8\x54\x5a\x28\xd7\ +\x2a\x07\xb2\x67\x65\x57\x66\xbf\xcd\x89\xca\x39\x96\xab\x9e\x2b\ +\xcd\xed\xcc\xb3\xca\xdb\x90\x37\x9c\xef\x9f\xff\xed\x12\xc2\x12\ +\xe1\x92\xb6\xa5\x86\x4b\x57\x2d\x1d\x58\xe6\xbd\xac\x6a\x39\xb2\ +\x3c\x71\x79\xdb\x0a\xe3\x15\x05\x2b\x86\x56\x06\xac\x3c\xb8\x8a\ +\xb6\x2a\x6d\xd5\x4f\xab\xed\x57\x97\xae\x7e\xbd\x26\x7a\x4d\x6b\ +\x81\x5e\xc1\xca\x82\xc1\xb5\x01\x6b\xeb\x0b\x55\x0a\xe5\x85\x7d\ +\xeb\xdc\xd7\xed\x5d\x4f\x58\x2f\x59\xdf\xb5\x61\xfa\x86\x9d\x1b\ +\x3e\x15\x89\x8a\xae\x14\xdb\x17\x97\x15\x7f\xd8\x28\xdc\x78\xe5\ +\x1b\x87\x6f\xca\xbf\x99\xdc\x94\xb4\xa9\xab\xc4\xb9\x64\xcf\x66\ +\xd2\x66\xe9\xe6\xde\x2d\x9e\x5b\x0e\x96\xaa\x97\xe6\x97\x0e\x6e\ +\x0d\xd9\xda\xb4\x0d\xdf\x56\xb4\xed\xf5\xf6\x45\xdb\x2f\x97\xcd\ +\x28\xdb\xbb\x83\xb6\x43\xb9\xa3\xbf\x3c\xb8\xbc\x65\xa7\xc9\xce\ +\xcd\x3b\x3f\x54\xa4\x54\xf4\x54\xfa\x54\x36\xee\xd2\xdd\xb5\x61\ +\xd7\xf8\x6e\xd1\xee\x1b\x7b\xbc\xf6\x34\xec\xd5\xdb\x5b\xbc\xf7\ +\xfd\x3e\xc9\xbe\xdb\x55\x01\x55\x4d\xd5\x66\xd5\x65\xfb\x49\xfb\ +\xb3\xf7\x3f\xae\x89\xaa\xe9\xf8\x96\xfb\x6d\x5d\xad\x4e\x6d\x71\ +\xed\xc7\x03\xd2\x03\xfd\x07\x23\x0e\xb6\xd7\xb9\xd4\xd5\x1d\xd2\ +\x3d\x54\x52\x8f\xd6\x2b\xeb\x47\x0e\xc7\x1f\xbe\xfe\x9d\xef\x77\ +\x2d\x0d\x36\x0d\x55\x8d\x9c\xc6\xe2\x23\x70\x44\x79\xe4\xe9\xf7\ +\x09\xdf\xf7\x1e\x0d\x3a\xda\x76\x8c\x7b\xac\xe1\x07\xd3\x1f\x76\ +\x1d\x67\x1d\x2f\x6a\x42\x9a\xf2\x9a\x46\x9b\x53\x9a\xfb\x5b\x62\ +\x5b\xba\x4f\xcc\x3e\xd1\xd6\xea\xde\x7a\xfc\x47\xdb\x1f\x0f\x9c\ +\x34\x3c\x59\x79\x4a\xf3\x54\xc9\x69\xda\xe9\x82\xd3\x93\x67\xf2\ +\xcf\x8c\x9d\x95\x9d\x7d\x7e\x2e\xf9\xdc\x60\xdb\xa2\xb6\x7b\xe7\ +\x63\xce\xdf\x6a\x0f\x6f\xef\xba\x10\x74\xe1\xd2\x45\xff\x8b\xe7\ +\x3b\xbc\x3b\xce\x5c\xf2\xb8\x74\xf2\xb2\xdb\xe5\x13\x57\xb8\x57\ +\x9a\xaf\x3a\x5f\x6d\xea\x74\xea\x3c\xfe\x93\xd3\x4f\xc7\xbb\x9c\ +\xbb\x9a\xae\xb9\x5c\x6b\xb9\xee\x7a\xbd\xb5\x7b\x66\xf7\xe9\x1b\ +\x9e\x37\xce\xdd\xf4\xbd\x79\xf1\x16\xff\xd6\xd5\x9e\x39\x3d\xdd\ +\xbd\xf3\x7a\x6f\xf7\xc5\xf7\xf5\xdf\x16\xdd\x7e\x72\x27\xfd\xce\ +\xcb\xbb\xd9\x77\x27\xee\xad\xbc\x4f\xbc\x5f\xf4\x40\xed\x41\xd9\ +\x43\xdd\x87\xd5\x3f\x5b\xfe\xdc\xd8\xef\xdc\x7f\x6a\xc0\x77\xa0\ +\xf3\xd1\xdc\x47\xf7\x06\x85\x83\xcf\xfe\x91\xf5\x8f\x0f\x43\x05\ +\x8f\x99\x8f\xcb\x86\x0d\x86\xeb\x9e\x38\x3e\x39\x39\xe2\x3f\x72\ +\xfd\xe9\xfc\xa7\x43\xcf\x64\xcf\x26\x9e\x17\xfe\xa2\xfe\xcb\xae\ +\x17\x16\x2f\x7e\xf8\xd5\xeb\xd7\xce\xd1\x98\xd1\xa1\x97\xf2\x97\ +\x93\xbf\x6d\x7c\xa5\xfd\xea\xc0\xeb\x19\xaf\xdb\xc6\xc2\xc6\x1e\ +\xbe\xc9\x78\x33\x31\x5e\xf4\x56\xfb\xed\xc1\x77\xdc\x77\x1d\xef\ +\xa3\xdf\x0f\x4f\xe4\x7c\x20\x7f\x28\xff\x68\xf9\xb1\xf5\x53\xd0\ +\xa7\xfb\x93\x19\x93\x93\xff\x04\x03\x98\xf3\xfc\x63\x33\x2d\xdb\ +\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\x25\x00\x00\x80\x83\ +\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00\x75\x30\x00\x00\xea\x60\ +\x00\x00\x3a\x98\x00\x00\x17\x6f\x92\x5f\xc5\x46\x00\x00\x0d\x0c\ +\x49\x44\x41\x54\x78\xda\xec\x9c\x7d\x70\x5c\xd5\x79\xc6\x7f\xe7\ +\xdc\xbb\xbb\x77\xbf\xbc\x5a\xc9\xb2\x8c\x31\xc6\xc6\x60\x6c\x62\ +\x13\x8c\x09\xb8\x21\x34\x04\x4c\xec\x60\xe3\xa4\x8d\x29\x6e\x61\ +\xa6\xb4\x49\xda\xd0\x4c\x9a\xe2\x36\x1d\xca\x30\x43\x4a\x81\x69\ +\xd2\xc9\x1f\x09\x90\xc1\xa4\x9d\x7c\x0c\x13\x3e\x12\xf3\x61\xc0\ +\xb2\x63\x17\x9c\x2a\xc4\x01\x83\xb1\xb1\x94\x20\x6c\xab\x76\x2d\ +\xdb\xb2\xf5\xb1\x5a\x69\xb5\xbb\x77\xf7\xde\x7b\xfa\xc7\xbd\x6b\ +\x7d\xee\x6a\x25\xaf\x84\xe2\xd1\x33\x73\xc6\x2b\xef\xbd\xe7\x9c\ +\xfb\xdc\xf7\x7d\xcf\xfb\x3e\xe7\x48\x42\x29\xc5\x34\x86\x43\x4e\ +\x53\x30\x4d\xcc\x34\x31\xd3\xc4\x4c\x13\x33\x4d\xcc\x94\x81\x5e\ +\xea\xcb\xa5\x4b\x97\x56\x7a\xbc\x38\x70\x39\xf0\x31\x60\x31\x70\ +\x31\x30\x0b\xa8\xf3\xbe\x13\xc0\x64\xe4\x0f\x01\xe0\x64\x63\x63\ +\xe3\xc7\xc6\x45\x4c\x05\xad\xf2\x0b\xc0\x17\x81\x6b\x80\x45\x53\ +\xc4\x28\x8c\x71\x5b\xcc\x39\x62\x2e\xb0\x11\xb8\xdb\xb3\x90\xa9\ +\x86\xee\xc9\x26\x26\x00\xfc\x2d\xf0\x2f\xc0\xec\x29\x1c\x46\xc4\ +\x64\x12\xb3\x16\xf8\x0f\x60\xc9\xf4\xaa\xd4\xcf\xfe\x77\x80\x57\ +\xcf\x07\x52\x2a\x65\x31\x71\xe0\x19\x60\xf5\x18\xee\xc9\x03\x47\ +\x81\xff\x05\xce\x00\x6d\x40\x87\xb7\x22\xd5\x78\x2e\x58\x0b\x2c\ +\xf0\x5a\xe0\x0f\x8d\x98\x39\xc0\x4b\xc0\x27\xca\xbc\xfe\xb7\xc0\ +\x76\xe0\x65\xa0\x19\xc8\x94\xb1\x72\x2c\x04\x6e\xf3\xdc\xf4\x53\ +\x7f\x08\xc4\x5c\x02\xfc\x0f\x70\x61\x19\xd7\xd6\x03\x8f\x03\xdb\ +\xc6\x38\x46\x16\x68\xf2\xda\xbf\x03\x37\x01\xf7\x00\x1b\xa6\x6a\ +\x8c\x99\x01\xbc\x58\x06\x29\xfb\x80\x35\xc0\xad\xe3\x20\x65\x24\ +\xbc\x0e\xdc\x0e\xfc\xb1\xf7\x79\x4a\x11\xa3\x7b\xa4\x5c\x39\xca\ +\x75\x0f\x7a\x2e\xb6\xe3\x9c\x27\x29\x20\xe7\x80\xe9\xb8\x9f\x81\ +\x06\xe0\x66\xe0\x1f\xa6\x92\x2b\x3d\xe8\x99\x74\x31\x38\xc0\x5d\ +\xc0\x33\x3d\x7d\x29\x6c\xdb\x41\x88\xfe\x94\xc1\x71\x1c\x22\xa1\ +\x10\x46\x20\x80\xe3\x38\x25\xc9\x68\x33\x15\x59\x5b\x91\x57\x30\ +\x2f\x28\xf1\x0b\x38\xd4\xe7\xe0\x13\x30\x3b\x20\x09\x6a\x7c\xcf\ +\x56\x1c\x02\x9e\x05\xa2\x1f\x25\x31\xab\x81\x07\x4a\x7c\xdf\x23\ +\x84\xd8\x90\xc9\x66\x77\x9a\xf9\x1c\x2b\x96\x2c\x25\x1c\x0a\x61\ +\x59\x56\x7f\x34\x0d\x04\xf8\xdd\x91\xc3\x1c\x3b\x75\x82\x68\x38\ +\x8c\x4f\xf7\x81\x52\x28\xc0\x16\x1a\x42\x08\xf2\x4a\xd1\x91\x53\ +\x6c\xbc\xc0\xc7\x45\x41\x41\x9b\xa9\x58\x35\x53\x23\x28\x05\x2f\ +\x9f\xb6\xb8\x20\x20\x78\xfe\x94\xc5\x87\x29\x87\xba\x80\xd8\x16\ +\xd5\xc5\xcd\xca\x0d\xe8\x17\x7c\x14\xc4\x68\xc0\xf7\x8b\xbe\x61\ +\x29\x73\x59\xd3\x5c\xdf\x95\x4c\xfe\x6a\xf9\x92\x2b\xb8\xe1\xea\ +\x6b\x98\x3f\x67\x2e\xa1\xa0\x81\x65\xdb\xfd\x69\xb1\xcf\xcf\xb2\ +\xcb\x16\xd1\xdd\x9b\xe2\xc3\xa3\x2d\xbc\x7f\xe8\x03\x42\xc1\x10\ +\x02\x85\xee\x58\xa4\x2c\xc5\xb2\xa8\xc6\xdf\xcc\xf3\x71\x63\x8d\ +\x8e\x61\x08\xd7\x06\x2d\x85\xa3\xe0\xd6\x39\x3e\x10\xb0\xba\xd6\ +\xa2\xc7\x56\xfc\xf8\x78\x9e\x57\xce\xd8\x7b\x17\x84\xc4\x2a\xa5\ +\x78\x13\xa8\xaa\x48\x62\x56\x4a\x0c\x1f\x52\x5d\x7f\xbd\x18\x31\ +\x42\x08\x27\x63\x9a\xb7\x1b\x7e\xff\x0b\x97\x2f\xb8\x84\x9b\xae\ +\x5d\xc9\xec\x9a\x5a\xba\x7a\x92\xd8\xb6\x3d\xcc\x95\xc2\xc1\x20\ +\xa1\x60\x90\x64\x6f\x2f\xbd\x7d\x29\x2c\x7f\x88\xa0\x95\x61\xdd\ +\xf1\x6d\xf8\xf2\x19\x74\xbf\xc1\xac\x90\x20\x65\x3a\x98\xf6\xd9\ +\xb8\xd2\xdf\x87\x82\x9a\x80\x00\x9f\xa0\x2b\xed\xf0\x97\xef\x65\ +\x69\x48\xd8\x5c\x1c\x14\x37\xd9\x8a\xd7\x46\x2b\x10\x3d\x9c\x6e\ +\x6c\x6c\x9c\x7d\xae\xc1\xb7\xd6\x8b\x2d\x23\x22\x97\xcf\x3f\x2c\ +\x85\x78\xe1\x8e\x35\x6b\xb9\x73\xed\x7a\x0c\x7f\x80\xf6\x44\x17\ +\x8e\xe3\x68\x62\x20\x2b\xae\x65\x91\x31\x4d\x3a\x12\x09\x1c\xa5\ +\x88\x45\xa2\x84\x82\x21\x22\xc1\x20\x35\x7e\xc9\x9c\x00\x84\x35\ +\xe8\xca\x38\xe4\x9d\xe1\xa4\x14\xe2\x4f\x22\xa7\xe8\xea\x73\x88\ +\xfb\x04\xcf\xac\x30\xf8\x54\x5c\xa3\x25\xad\x5e\x07\xfe\x69\x32\ +\x57\xa5\xbf\xf6\x32\xd2\x91\x5c\x68\x7f\x2a\x93\x7e\xf0\xd2\x79\ +\x17\xb3\xf0\xa2\x79\x9c\xe9\xec\x0c\x58\xb6\xfd\x25\x21\xc4\x76\ +\xe0\xf7\x5e\x7b\xc5\xab\xb4\xe5\x00\x2b\xc3\x71\x1c\xf2\x56\x1e\ +\xcb\xb2\xc8\x5b\x16\x59\x5b\x91\xb6\xdd\xd5\x47\x8c\x9c\x61\x7f\ +\x19\xd8\x0a\xbc\x05\xec\x16\xf0\x48\x97\xa9\x2e\x0f\x49\xc1\xb3\ +\xcb\x0d\x6e\xac\xd6\x68\xcd\x3a\x4f\xe8\x82\x9d\x93\x45\xcc\x97\ +\x8a\xb8\x10\x7d\x99\xf4\xa6\xcb\xe7\x2f\xe0\x8e\xd5\xb7\x62\xe6\ +\x72\xcb\x80\xdd\xc0\x7f\x7a\x81\xfa\x32\x4f\x98\x5a\xe7\x95\x0d\ +\xff\xed\xfd\x3c\x56\xac\x02\xde\x04\x7e\xe8\x65\xc1\xd7\x02\x9f\ +\x06\xee\x97\x82\xfd\x89\x9c\x7a\x24\xa2\x0b\x9e\x5e\x6e\x70\x43\ +\x5c\xe3\xb4\xa9\xbe\x29\x26\x81\x98\xf5\xde\x03\x0e\x43\x3a\x9b\ +\x79\xa3\xae\x66\xe6\x1b\x77\xaf\xff\x53\x7c\xba\x3e\x37\x6b\x9a\ +\x3b\x85\x10\x2b\x4b\xf4\x75\xa3\xf7\xc6\x6b\xc6\x30\xc7\xcf\x00\ +\x3b\x4b\x14\xa7\x86\x14\xdc\xdf\x6e\xaa\xef\xc6\x0c\xc1\xdd\x73\ +\xfd\x28\x38\x60\xbb\x4b\xf8\x84\x12\x73\x57\x11\x6b\x51\xe9\x4c\ +\xf6\x51\xdb\x76\xd0\x75\x9d\x64\x2a\xf5\x03\x29\x65\x5d\x19\xfd\ +\x2d\x02\xbe\x55\xe6\xfc\x22\xc0\x8f\xca\x5a\x5e\x25\x9b\xba\xd2\ +\xea\x96\x2f\xd4\xe9\x7c\x32\xae\xd1\x95\x53\x8f\x02\xe6\x44\x11\ +\x53\x55\xb4\x40\x54\x6a\x9f\xa6\xc9\x5d\x2b\xaf\xbc\x8a\xbc\x65\ +\x5f\x27\xa5\xbc\x6d\x0c\xe3\xde\x03\xcc\x2f\xe3\xba\xaf\xe2\xea\ +\xc2\xe5\x8a\x1f\x0f\x23\x20\xa2\x81\x80\x83\xc0\x1b\x13\x95\xc7\ +\x2c\x2e\xf6\x00\x66\x2e\xb7\x75\xe1\xdc\x79\x7c\x7c\xf1\x12\x94\ +\xe3\x8c\xb5\xea\xd5\xbc\xb2\xa2\xa5\xc4\x1c\x2c\xcf\xf5\xc6\x22\ +\x0a\x5d\x01\x5c\xf2\x8d\x4b\x02\x2d\x7b\x93\x19\x6c\xc5\xcb\x9a\ +\x60\xcd\x44\x10\x53\x6c\x9b\xc0\xee\xcb\x66\xb7\xac\xfc\xf8\x72\ +\x62\xe1\x08\x1d\xdd\x89\x99\xe3\x18\xfb\x2a\xaf\x55\x12\x06\x30\ +\x77\x86\x4e\x8b\x74\x6b\x93\x17\x81\x6f\x7b\x45\x6f\x45\x5d\xa9\ +\x58\xc0\x6b\x91\x42\x7c\x90\x31\xb3\x58\xb6\x05\xd0\x33\xce\x07\ +\x49\x79\xd2\xc2\x48\x2d\x35\x8e\xfe\x72\x40\x2a\xe7\x9c\xdd\x83\ +\x39\x0d\x34\x4e\x84\xc5\xcc\x2b\x46\x8c\x52\xca\xf6\xe9\x3a\x52\ +\x4a\x94\x52\xef\x0e\xc9\xe3\xca\xc1\x9d\x5e\x8e\xe3\x2f\xf1\x90\ +\xdf\x04\xfe\x7c\x0c\x7d\x76\x79\x7d\x0e\xc4\x51\xe0\x93\x95\x26\ +\x66\x56\x91\xff\x6f\xd7\xa4\xa4\xa7\x2f\x45\x2e\x9f\x47\x4a\xf9\ +\xba\x52\xea\xfd\x32\xa4\x88\x02\xb6\x02\x3f\x2b\xe3\xba\x47\xc7\ +\x48\xcc\x0f\x81\x4c\xb5\x4f\xa0\x09\xb0\x5c\xb3\x39\x3c\x11\xab\ +\x52\xb1\xe5\xf7\x64\x38\x14\xe2\x37\xef\xed\xa3\x2b\x99\x24\xe0\ +\xf7\x5b\x5e\x12\x68\x95\x31\xa6\x09\xfc\x73\x99\xf3\x6b\x04\x1e\ +\x2a\xf3\xda\xc3\xb8\x82\x3c\xbb\x3a\x2c\xd2\xf6\xd9\xec\xf9\xf4\ +\x44\x2d\xd7\x23\xa1\x53\x4a\x49\x3a\x9b\xe1\xed\x83\x07\x0a\xda\ +\xca\x3b\x5e\x56\xda\x59\xa2\xbf\x16\x2f\xe0\x36\x7b\x25\x2c\x28\ +\x41\x3e\x17\xc6\xce\x85\x30\x52\x0a\x3d\xaf\x50\x83\x67\xf5\xaf\ +\xc0\xbf\x8d\x32\xcf\x3d\xc0\xa7\x1d\x45\x56\xd3\x05\xcf\x9d\xcc\ +\xd3\x6d\xa9\x42\x9d\x65\x15\x5f\xc4\xc6\x4f\x4c\xe9\x9b\xa5\xe4\ +\xc0\x87\x1f\x70\xaa\xfd\x0c\x01\x7f\x00\x5c\xa1\xfb\x7a\xe0\x49\ +\xef\x6d\x77\x00\x27\x86\xf4\x77\xcc\xcd\x83\x04\xca\xd6\x41\xda\ +\xc4\x66\x1e\x26\x54\x7b\x94\xd6\xc5\x1a\x59\x43\xc3\x97\x55\x28\ +\x71\x76\x74\x07\x38\x30\x34\xe9\x06\xfa\x80\xf7\x81\x47\x3c\xe1\ +\xec\xa4\xa1\x43\x4b\xaf\x4d\xca\x76\x0b\xd1\x42\xc6\x55\x64\xfa\ +\xea\x5c\x88\x29\x76\x73\x1c\x20\x64\x04\x39\xdd\xd9\xc1\x3b\x8d\ +\x07\x89\x86\xc3\x85\xef\x9a\xbd\x04\x6e\x05\xb0\x12\xb8\x0e\xf8\ +\x2f\xef\xbb\x05\x08\xf5\x62\xa1\x5b\xc7\xf6\xa1\xf9\x4c\x2e\x5a\ +\xb4\x93\xda\x25\x0d\xbc\xbf\x4a\xe7\x9d\x9b\x23\xe8\x96\x22\x9a\ +\x70\x30\x7a\x15\xc0\x85\x4a\xf2\xd3\x01\x63\x6f\x04\xfe\xc8\x6b\ +\x2b\x3c\xe1\x2c\x0b\x10\xf6\x0b\x9e\x3e\x99\x67\x4f\xb7\x43\x95\ +\x4f\x8c\x16\x47\x73\x95\x96\x36\xc1\xdd\x36\x41\x29\x45\x4d\xac\ +\x8a\xfd\xcd\xbf\xe7\xda\x65\x57\x52\x1d\x8b\xd3\x97\x49\x0f\x1c\ +\xf8\x88\xf7\xf9\x3e\xe0\xcf\x84\xb4\xa3\x56\x2e\xb4\x1a\x25\x57\ +\xcd\x59\xfa\xcb\x5d\xbe\x60\x12\x3b\x6f\x60\x99\x11\x40\x11\xce\ +\x2a\x92\xb3\x24\xa7\x16\xea\x84\x93\x0e\x20\x88\xb7\xd9\x0f\xa4\ +\xa3\x22\xe4\xe8\x20\x1d\x5e\x53\xf0\x9c\x18\xe1\x75\xc5\xfd\x82\ +\xa3\xbd\x0e\x3f\x3f\x69\xb1\x20\x28\x70\xfa\xaf\x99\x59\x22\x55\ +\xa8\xa8\x18\x5e\xd0\x67\xdc\x14\x56\xd3\xe8\xed\x4b\xb1\xb7\xe9\ +\x20\x45\x97\x6c\xa1\x3a\x50\x7c\xcb\xec\x8b\xa3\x1c\x9d\xd8\x85\ +\x4d\xdf\x09\xc6\x4e\x09\x7f\xa8\x1b\x5f\xb0\x67\x90\xa3\xf9\xb3\ +\x8a\xc6\x1b\xfc\xfc\xfa\x8b\x06\x0d\xb7\x07\xae\x6f\x5b\xa8\x7d\ +\x35\xda\xe5\x10\x49\xa8\x3e\x7f\x5a\x7d\xcd\xe8\x53\xf8\xb3\xca\ +\x35\x3a\x31\xd8\x49\x7f\x76\x22\x4f\x9b\xe9\xe0\x97\xc3\x6a\xb3\ +\x91\xd0\x31\x11\xc4\xcc\x2f\xdc\xab\x94\x22\x1c\x0a\xb3\xe3\xcd\ +\x06\xf6\xec\xdf\x47\x4d\x55\xfc\xac\xc8\x6d\xe7\x42\x58\xd9\x28\ +\x56\x26\x8a\x99\xaa\xf9\x5e\xa4\xfa\x78\xd3\xfc\x4f\x3c\x4f\xdd\ +\xa2\x5f\x2f\xb7\x6d\xff\xbd\xb9\x74\x0c\xc7\xf2\x0f\xf3\x58\x3d\ +\xaf\x08\xa6\x14\x3e\x53\x7d\xff\xad\xf5\x06\x6f\xaf\x33\x48\xcc\ +\x96\x8f\x67\xa3\x1c\x4b\xc7\x04\xbd\xd5\x12\x5b\x07\xe1\xb8\x6a\ +\x5e\x95\x5f\xd0\x69\x2a\x7e\xda\x6a\x11\xd6\x07\x59\x0b\x25\x6a\ +\xad\xff\x9b\x08\x57\x5a\xe8\xb5\x43\x00\x9a\x94\x04\x8d\x20\xef\ +\xfe\xae\x91\x6b\x96\x2e\x23\x1a\x8e\x90\x4a\xa7\x89\xd4\x1d\x46\ +\xd3\x72\x28\x47\xc3\xb6\xfc\x76\xcd\xfc\x7d\x0f\x05\x22\x9d\xcf\ +\xe5\x52\x71\x94\x92\x0f\x20\x78\x02\x94\xe9\xc9\x1a\x17\x7b\x81\ +\xf6\xb0\x37\xe9\x0d\xfe\x0c\x57\x6b\xb6\x43\xeb\x62\xed\x54\xdb\ +\x02\xf9\x88\x6e\xbb\x14\xe6\xfd\x82\xeb\x5e\x31\xa9\x3e\x69\x13\ +\x89\x49\xda\x73\x8a\xaf\x1c\xc8\xd2\x63\x2b\xaa\x7d\x83\xac\xb6\ +\x86\xe2\x47\x50\x9a\x27\x82\x18\x3f\xf0\x27\x85\xbc\x41\x29\x45\ +\x2c\x12\xa1\xf5\x4c\x1b\x3f\x7a\x69\x0b\x1b\xd7\xac\x45\x23\x42\ +\x7c\xde\x7e\x8c\x19\x67\x70\xf2\x06\x08\x85\x9d\x0b\x3e\x9f\x4d\ +\xd5\x7e\x59\x08\xfb\x16\x84\x8a\x03\xcf\x01\xbd\xb8\x1b\x72\xd5\ +\xf4\xe7\x1d\x6f\x00\xd7\x2b\x09\xd2\x82\x48\x42\xfd\x7d\x3e\x40\ +\xef\x40\x43\xb0\x7d\x20\x24\xd8\x40\xfd\x69\x8b\xfa\x76\x8b\x25\ +\x11\x89\x3d\xd8\x5a\xd6\x0d\xe8\x77\x28\xde\x9b\x08\x57\x2a\x08\ +\x58\x83\x44\xee\xaa\xe8\x0c\x9a\x0e\x1f\x62\xd7\x9e\xbd\x5c\x70\ +\x61\x1e\xc7\x51\xe4\x33\x31\x2c\x33\x8c\x95\x8d\xa0\x1c\x0d\x21\ +\xec\xaf\x03\x49\xef\xb6\xcf\x7b\x7a\x4f\xf5\x90\xa4\x72\x23\x70\ +\x51\x21\x4b\x56\x82\x5f\xe8\x39\x38\xdb\xf2\x70\xe4\x0a\x9d\x50\ +\x48\xf2\x50\xb3\xc9\x7d\xcd\x26\x97\x85\x87\x91\x82\xf7\xf2\x46\ +\x42\xef\x68\x35\xd4\xb9\x10\xb3\xd2\x6b\x03\x24\x1a\x45\x75\x2c\ +\x46\xd3\x87\x27\x68\xcd\x6c\xa3\xa6\x26\x87\xca\xfb\x41\x89\xfe\ +\xe6\x9a\xf0\x4f\xc6\x30\xce\xd3\xc3\xcc\x3c\xaf\xf0\xcf\xd5\xd9\ +\x1a\x87\x6d\xc7\xf2\xc8\x91\x1f\xe4\x52\xe0\xb3\x45\xfa\x7c\xd7\ +\xab\xa1\x26\x84\x18\x0d\xb8\x7f\x58\xdd\x1f\x08\x70\xa6\xab\x9d\ +\xa3\xc7\xdb\x09\xf8\x43\x6e\x76\x3b\x5c\x1a\xb8\x76\x0c\xe3\x7c\ +\x65\xa8\xcb\x4b\x1b\x8c\x19\x92\x9d\x61\x38\xd2\x6d\x53\xeb\x17\ +\x23\x25\x5c\xf7\x01\xc1\x22\x7d\xbe\x5a\x29\x31\xbc\x18\x6e\xf3\ +\x12\xb8\x41\x2e\xe5\xf7\xf9\x99\x11\x35\x10\xc2\x61\x84\x6d\xab\ +\x1b\x87\x5a\xda\x28\xb8\x05\xb8\x7a\xd0\x18\x80\x50\x8a\xd9\x9a\ +\x20\xa4\x8b\x42\xb1\x38\x10\x97\xe1\x9e\xfd\x1b\x09\x09\x4f\xac\ +\x9f\x50\x62\xc0\x3d\x5a\x36\xd8\x94\x34\x48\x26\x1d\x7c\x3e\x81\ +\x1c\x3e\xc2\xb2\x71\x8c\x71\xc5\xe0\x52\x04\xb2\x59\x45\x22\xe1\ +\xa0\x69\x23\x5e\xff\x6d\xcf\xa2\x47\xc2\x2f\x06\xc4\xb8\x09\x25\ +\xe6\x06\x60\xd3\xa0\xca\xb3\x4a\xe3\x85\x2d\x7d\xbc\xf4\x52\x9a\ +\x48\x54\x0c\xb5\x9a\xe0\x38\xc6\x88\xf4\xc7\x31\x88\x44\x05\x1d\ +\x1d\x0e\xf5\xdb\xd2\xc4\x62\xc3\x9e\xff\x2f\x4a\x04\x5d\x87\x12\ +\xdb\xcc\x95\x26\xa6\xa0\x9b\xdc\x3c\xf4\x8d\xbe\xf6\x6a\x1f\x9a\ +\x14\x18\x86\x60\xc0\xc1\x86\xd6\x71\xf4\x7f\xcc\x75\x53\x30\x0c\ +\x81\x26\x05\x5b\xb6\xf4\x91\x4e\xab\xa1\x16\xb9\x1c\xd8\x5c\xa2\ +\x9f\x27\xcb\x55\xf4\x2a\x45\x4c\x00\xd8\x52\x30\x79\xa5\x20\x16\ +\x93\xb4\xb7\xdb\x6c\x7e\xb2\x97\x70\x58\x12\x0c\x9e\xb5\x9c\x06\ +\xdc\xf4\xa3\x5c\x24\x80\x77\x94\x82\x60\x50\x10\x0e\x4b\x36\x3f\ +\xd9\xcb\xf6\xfa\xb4\xbb\xe1\x3f\x58\x54\x7b\x61\xa0\x75\x0d\x41\ +\x4f\x19\xf2\x45\xc5\x89\x01\x88\x01\xbb\xf0\xb6\x5b\xa4\x84\x70\ +\x58\x52\x5f\x9f\xe6\xf1\xc7\x92\x18\x86\x20\x5e\x2d\x11\x82\x43\ +\x4a\xf1\xe3\x72\x3b\x55\x8a\xa7\x84\xe0\x54\xbc\x5a\x62\x18\x82\ +\xc7\x1f\x4b\x52\x5f\x9f\xa6\xae\x4e\x43\xd7\x07\x2d\xcd\x6f\x52\ +\x7a\x4b\xe6\x7e\xdc\x43\x90\x93\x4e\x0c\xb8\xe7\x53\x76\x01\x9f\ +\x2d\xc4\x95\x59\xb3\x34\x76\xec\xc8\xf0\xd4\xe6\x1e\x4e\xb4\xda\ +\x84\x42\x12\x21\xf8\x47\x2f\x97\x18\x0d\xdb\x85\xe0\x81\x50\x48\ +\x72\xa2\xd5\xe6\xa9\xcd\x3d\xec\xd8\x91\x61\xd6\x2c\xed\x6c\xbc\ +\xf1\xa4\x87\x9d\x1e\x39\xc5\xf0\x3c\xf0\x44\x25\x15\xbc\xf1\x60\ +\x06\xee\x79\xbb\x7b\x0a\x96\x53\x57\xa7\xb1\x7b\x77\x96\x4d\xf7\ +\x76\xd2\xd0\x90\x61\xf6\x6c\x2d\x19\x08\x88\x5b\x95\xe2\x99\x22\ +\x56\x82\xcf\x27\x36\x87\xc2\x72\x43\x5d\x9d\x66\x35\x34\x64\xd8\ +\x74\x6f\x27\xbb\x77\x67\xa9\xab\xd3\x06\xc6\x95\xbb\x70\xf7\xc3\ +\x4b\x59\xca\x51\xe0\x6b\x63\x7d\x88\xd1\xce\xc7\xb4\x95\xd0\x7d\ +\xcb\xc1\x2b\xc0\xc3\xc0\xdb\x42\x40\x26\xe3\x06\xcb\x4b\x2f\xf5\ +\xb1\xe1\xf6\x30\x2b\x56\xf8\xc9\x66\x59\xe3\xe5\x2a\x8b\xbc\xe4\ +\xb9\xc9\xe7\x63\x7b\x22\xe1\xfc\xaa\xa9\x29\xcf\xae\x9d\x69\x9a\ +\x9b\xf3\x38\x0e\x03\xe3\xd4\x95\x9e\x6e\x7c\xe7\x28\xe3\xf7\x7a\ +\x8a\xe2\xc1\x11\x05\xe5\xc6\xc6\x8a\x17\x91\x63\x49\x00\xd7\x01\ +\x4f\x2b\xc5\x0f\x42\x21\xf1\xdb\x5c\x4e\xf1\xd6\x5b\x26\xdd\xdd\ +\x0e\x6d\xa7\x42\x98\x39\xb5\x1d\xc5\xf6\x81\xaf\x27\x1a\x91\xec\ +\xdd\x9b\x65\xcf\x6f\x4c\x14\x10\x89\x9c\x5d\xd9\xae\xc2\xdd\xb6\ +\xfd\x2b\x8a\x6f\xbb\x14\x60\x79\xe3\x1f\x1c\xcf\xc4\x27\xda\x62\ +\x86\xe6\x10\xdb\x81\x6d\x52\xb2\xd5\x34\xd5\xc9\x64\xb7\x63\x0b\ +\x09\xca\x01\xdb\xe9\x4f\x0e\x1d\xc7\x0d\xdc\x91\xb0\x90\xca\x5d\ +\x6d\xd6\x7a\xed\x73\x94\x77\x5a\xaa\x13\xb8\xc3\x73\xb3\xe2\x5b\ +\x10\x25\x2c\x66\x32\x89\x19\x88\x6e\xe0\x03\xdc\x23\xf3\x47\x94\ +\xe2\xb4\x52\xee\xc9\x04\x21\xf0\x0b\x41\x2d\xee\x01\xeb\x05\xb8\ +\xfb\xe7\x63\xd9\x02\x3e\x82\xbb\x17\xb5\x77\xb4\x0b\x3f\x4a\x57\ +\x2a\x86\xaa\x81\xd5\xb9\x10\x6e\xab\x00\x9e\x05\xfe\xce\xcb\x7d\ +\xce\x09\xe7\xcb\xef\x44\xb6\x01\xdf\xf0\x2c\x25\x51\x89\x0e\xf5\ +\xf3\x80\x94\xc7\xbc\x42\xf6\x78\x25\x3b\x1d\x8d\x18\x35\x85\x2d\ +\xe4\x45\xdc\xd3\x56\x7b\x27\x62\x00\xbd\x8c\x58\x30\x55\xd0\x0d\ +\xbc\x0d\xfc\xd2\x53\x00\x3b\x26\x72\xb0\xd1\x88\x69\xc1\xdd\x5c\ +\x33\x27\x91\x00\xd3\x4b\xcc\x3a\xbd\xaa\xba\x19\xd8\xef\xfd\x7b\ +\x78\xb2\x26\x21\xa6\xff\x4c\xca\xf9\xbd\x2a\x4d\x13\x33\x4d\xcc\ +\x34\x31\xd3\xc4\x4c\x13\x73\x3e\xe0\xff\x07\x00\xd9\x01\xc4\xf4\ +\xd1\x48\x28\x2f\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\ +\x00\x00\x10\xfe\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x40\x00\x00\x00\x40\x08\x06\x00\x00\x00\xaa\x69\x71\xde\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ +\x01\x00\x9a\x9c\x18\x00\x00\x0a\x4f\x69\x43\x43\x50\x50\x68\x6f\ +\x74\x6f\x73\x68\x6f\x70\x20\x49\x43\x43\x20\x70\x72\x6f\x66\x69\ +\x6c\x65\x00\x00\x78\xda\x9d\x53\x67\x54\x53\xe9\x16\x3d\xf7\xde\ +\xf4\x42\x4b\x88\x80\x94\x4b\x6f\x52\x15\x08\x20\x52\x42\x8b\x80\ +\x14\x91\x26\x2a\x21\x09\x10\x4a\x88\x21\xa1\xd9\x15\x51\xc1\x11\ +\x45\x45\x04\x1b\xc8\xa0\x88\x03\x8e\x8e\x80\x8c\x15\x51\x2c\x0c\ +\x8a\x0a\xd8\x07\xe4\x21\xa2\x8e\x83\xa3\x88\x8a\xca\xfb\xe1\x7b\ +\xa3\x6b\xd6\xbc\xf7\xe6\xcd\xfe\xb5\xd7\x3e\xe7\xac\xf3\x9d\xb3\ +\xcf\x07\xc0\x08\x0c\x96\x48\x33\x51\x35\x80\x0c\xa9\x42\x1e\x11\ +\xe0\x83\xc7\xc4\xc6\xe1\xe4\x2e\x40\x81\x0a\x24\x70\x00\x10\x08\ +\xb3\x64\x21\x73\xfd\x23\x01\x00\xf8\x7e\x3c\x3c\x2b\x22\xc0\x07\ +\xbe\x00\x01\x78\xd3\x0b\x08\x00\xc0\x4d\x9b\xc0\x30\x1c\x87\xff\ +\x0f\xea\x42\x99\x5c\x01\x80\x84\x01\xc0\x74\x91\x38\x4b\x08\x80\ +\x14\x00\x40\x7a\x8e\x42\xa6\x00\x40\x46\x01\x80\x9d\x98\x26\x53\ +\x00\xa0\x04\x00\x60\xcb\x63\x62\xe3\x00\x50\x2d\x00\x60\x27\x7f\ +\xe6\xd3\x00\x80\x9d\xf8\x99\x7b\x01\x00\x5b\x94\x21\x15\x01\xa0\ +\x91\x00\x20\x13\x65\x88\x44\x00\x68\x3b\x00\xac\xcf\x56\x8a\x45\ +\x00\x58\x30\x00\x14\x66\x4b\xc4\x39\x00\xd8\x2d\x00\x30\x49\x57\ +\x66\x48\x00\xb0\xb7\x00\xc0\xce\x10\x0b\xb2\x00\x08\x0c\x00\x30\ +\x51\x88\x85\x29\x00\x04\x7b\x00\x60\xc8\x23\x23\x78\x00\x84\x99\ +\x00\x14\x46\xf2\x57\x3c\xf1\x2b\xae\x10\xe7\x2a\x00\x00\x78\x99\ +\xb2\x3c\xb9\x24\x39\x45\x81\x5b\x08\x2d\x71\x07\x57\x57\x2e\x1e\ +\x28\xce\x49\x17\x2b\x14\x36\x61\x02\x61\x9a\x40\x2e\xc2\x79\x99\ +\x19\x32\x81\x34\x0f\xe0\xf3\xcc\x00\x00\xa0\x91\x15\x11\xe0\x83\ +\xf3\xfd\x78\xce\x0e\xae\xce\xce\x36\x8e\xb6\x0e\x5f\x2d\xea\xbf\ +\x06\xff\x22\x62\x62\xe3\xfe\xe5\xcf\xab\x70\x40\x00\x00\xe1\x74\ +\x7e\xd1\xfe\x2c\x2f\xb3\x1a\x80\x3b\x06\x80\x6d\xfe\xa2\x25\xee\ +\x04\x68\x5e\x0b\xa0\x75\xf7\x8b\x66\xb2\x0f\x40\xb5\x00\xa0\xe9\ +\xda\x57\xf3\x70\xf8\x7e\x3c\x3c\x45\xa1\x90\xb9\xd9\xd9\xe5\xe4\ +\xe4\xd8\x4a\xc4\x42\x5b\x61\xca\x57\x7d\xfe\x67\xc2\x5f\xc0\x57\ +\xfd\x6c\xf9\x7e\x3c\xfc\xf7\xf5\xe0\xbe\xe2\x24\x81\x32\x5d\x81\ +\x47\x04\xf8\xe0\xc2\xcc\xf4\x4c\xa5\x1c\xcf\x92\x09\x84\x62\xdc\ +\xe6\x8f\x47\xfc\xb7\x0b\xff\xfc\x1d\xd3\x22\xc4\x49\x62\xb9\x58\ +\x2a\x14\xe3\x51\x12\x71\x8e\x44\x9a\x8c\xf3\x32\xa5\x22\x89\x42\ +\x92\x29\xc5\x25\xd2\xff\x64\xe2\xdf\x2c\xfb\x03\x3e\xdf\x35\x00\ +\xb0\x6a\x3e\x01\x7b\x91\x2d\xa8\x5d\x63\x03\xf6\x4b\x27\x10\x58\ +\x74\xc0\xe2\xf7\x00\x00\xf2\xbb\x6f\xc1\xd4\x28\x08\x03\x80\x68\ +\x83\xe1\xcf\x77\xff\xef\x3f\xfd\x47\xa0\x25\x00\x80\x66\x49\x92\ +\x71\x00\x00\x5e\x44\x24\x2e\x54\xca\xb3\x3f\xc7\x08\x00\x00\x44\ +\xa0\x81\x2a\xb0\x41\x1b\xf4\xc1\x18\x2c\xc0\x06\x1c\xc1\x05\xdc\ +\xc1\x0b\xfc\x60\x36\x84\x42\x24\xc4\xc2\x42\x10\x42\x0a\x64\x80\ +\x1c\x72\x60\x29\xac\x82\x42\x28\x86\xcd\xb0\x1d\x2a\x60\x2f\xd4\ +\x40\x1d\x34\xc0\x51\x68\x86\x93\x70\x0e\x2e\xc2\x55\xb8\x0e\x3d\ +\x70\x0f\xfa\x61\x08\x9e\xc1\x28\xbc\x81\x09\x04\x41\xc8\x08\x13\ +\x61\x21\xda\x88\x01\x62\x8a\x58\x23\x8e\x08\x17\x99\x85\xf8\x21\ +\xc1\x48\x04\x12\x8b\x24\x20\xc9\x88\x14\x51\x22\x4b\x91\x35\x48\ +\x31\x52\x8a\x54\x20\x55\x48\x1d\xf2\x3d\x72\x02\x39\x87\x5c\x46\ +\xba\x91\x3b\xc8\x00\x32\x82\xfc\x86\xbc\x47\x31\x94\x81\xb2\x51\ +\x3d\xd4\x0c\xb5\x43\xb9\xa8\x37\x1a\x84\x46\xa2\x0b\xd0\x64\x74\ +\x31\x9a\x8f\x16\xa0\x9b\xd0\x72\xb4\x1a\x3d\x8c\x36\xa1\xe7\xd0\ +\xab\x68\x0f\xda\x8f\x3e\x43\xc7\x30\xc0\xe8\x18\x07\x33\xc4\x6c\ +\x30\x2e\xc6\xc3\x42\xb1\x38\x2c\x09\x93\x63\xcb\xb1\x22\xac\x0c\ +\xab\xc6\x1a\xb0\x56\xac\x03\xbb\x89\xf5\x63\xcf\xb1\x77\x04\x12\ +\x81\x45\xc0\x09\x36\x04\x77\x42\x20\x61\x1e\x41\x48\x58\x4c\x58\ +\x4e\xd8\x48\xa8\x20\x1c\x24\x34\x11\xda\x09\x37\x09\x03\x84\x51\ +\xc2\x27\x22\x93\xa8\x4b\xb4\x26\xba\x11\xf9\xc4\x18\x62\x32\x31\ +\x87\x58\x48\x2c\x23\xd6\x12\x8f\x13\x2f\x10\x7b\x88\x43\xc4\x37\ +\x24\x12\x89\x43\x32\x27\xb9\x90\x02\x49\xb1\xa4\x54\xd2\x12\xd2\ +\x46\xd2\x6e\x52\x23\xe9\x2c\xa9\x9b\x34\x48\x1a\x23\x93\xc9\xda\ +\x64\x6b\xb2\x07\x39\x94\x2c\x20\x2b\xc8\x85\xe4\x9d\xe4\xc3\xe4\ +\x33\xe4\x1b\xe4\x21\xf2\x5b\x0a\x9d\x62\x40\x71\xa4\xf8\x53\xe2\ +\x28\x52\xca\x6a\x4a\x19\xe5\x10\xe5\x34\xe5\x06\x65\x98\x32\x41\ +\x55\xa3\x9a\x52\xdd\xa8\xa1\x54\x11\x35\x8f\x5a\x42\xad\xa1\xb6\ +\x52\xaf\x51\x87\xa8\x13\x34\x75\x9a\x39\xcd\x83\x16\x49\x4b\xa5\ +\xad\xa2\x95\xd3\x1a\x68\x17\x68\xf7\x69\xaf\xe8\x74\xba\x11\xdd\ +\x95\x1e\x4e\x97\xd0\x57\xd2\xcb\xe9\x47\xe8\x97\xe8\x03\xf4\x77\ +\x0c\x0d\x86\x15\x83\xc7\x88\x67\x28\x19\x9b\x18\x07\x18\x67\x19\ +\x77\x18\xaf\x98\x4c\xa6\x19\xd3\x8b\x19\xc7\x54\x30\x37\x31\xeb\ +\x98\xe7\x99\x0f\x99\x6f\x55\x58\x2a\xb6\x2a\x7c\x15\x91\xca\x0a\ +\x95\x4a\x95\x26\x95\x1b\x2a\x2f\x54\xa9\xaa\xa6\xaa\xde\xaa\x0b\ +\x55\xf3\x55\xcb\x54\x8f\xa9\x5e\x53\x7d\xae\x46\x55\x33\x53\xe3\ +\xa9\x09\xd4\x96\xab\x55\xaa\x9d\x50\xeb\x53\x1b\x53\x67\xa9\x3b\ +\xa8\x87\xaa\x67\xa8\x6f\x54\x3f\xa4\x7e\x59\xfd\x89\x06\x59\xc3\ +\x4c\xc3\x4f\x43\xa4\x51\xa0\xb1\x5f\xe3\xbc\xc6\x20\x0b\x63\x19\ +\xb3\x78\x2c\x21\x6b\x0d\xab\x86\x75\x81\x35\xc4\x26\xb1\xcd\xd9\ +\x7c\x76\x2a\xbb\x98\xfd\x1d\xbb\x8b\x3d\xaa\xa9\xa1\x39\x43\x33\ +\x4a\x33\x57\xb3\x52\xf3\x94\x66\x3f\x07\xe3\x98\x71\xf8\x9c\x74\ +\x4e\x09\xe7\x28\xa7\x97\xf3\x7e\x8a\xde\x14\xef\x29\xe2\x29\x1b\ +\xa6\x34\x4c\xb9\x31\x65\x5c\x6b\xaa\x96\x97\x96\x58\xab\x48\xab\ +\x51\xab\x47\xeb\xbd\x36\xae\xed\xa7\x9d\xa6\xbd\x45\xbb\x59\xfb\ +\x81\x0e\x41\xc7\x4a\x27\x5c\x27\x47\x67\x8f\xce\x05\x9d\xe7\x53\ +\xd9\x53\xdd\xa7\x0a\xa7\x16\x4d\x3d\x3a\xf5\xae\x2e\xaa\x6b\xa5\ +\x1b\xa1\xbb\x44\x77\xbf\x6e\xa7\xee\x98\x9e\xbe\x5e\x80\x9e\x4c\ +\x6f\xa7\xde\x79\xbd\xe7\xfa\x1c\x7d\x2f\xfd\x54\xfd\x6d\xfa\xa7\ +\xf5\x47\x0c\x58\x06\xb3\x0c\x24\x06\xdb\x0c\xce\x18\x3c\xc5\x35\ +\x71\x6f\x3c\x1d\x2f\xc7\xdb\xf1\x51\x43\x5d\xc3\x40\x43\xa5\x61\ +\x95\x61\x97\xe1\x84\x91\xb9\xd1\x3c\xa3\xd5\x46\x8d\x46\x0f\x8c\ +\x69\xc6\x5c\xe3\x24\xe3\x6d\xc6\x6d\xc6\xa3\x26\x06\x26\x21\x26\ +\x4b\x4d\xea\x4d\xee\x9a\x52\x4d\xb9\xa6\x29\xa6\x3b\x4c\x3b\x4c\ +\xc7\xcd\xcc\xcd\xa2\xcd\xd6\x99\x35\x9b\x3d\x31\xd7\x32\xe7\x9b\ +\xe7\x9b\xd7\x9b\xdf\xb7\x60\x5a\x78\x5a\x2c\xb6\xa8\xb6\xb8\x65\ +\x49\xb2\xe4\x5a\xa6\x59\xee\xb6\xbc\x6e\x85\x5a\x39\x59\xa5\x58\ +\x55\x5a\x5d\xb3\x46\xad\x9d\xad\x25\xd6\xbb\xad\xbb\xa7\x11\xa7\ +\xb9\x4e\x93\x4e\xab\x9e\xd6\x67\xc3\xb0\xf1\xb6\xc9\xb6\xa9\xb7\ +\x19\xb0\xe5\xd8\x06\xdb\xae\xb6\x6d\xb6\x7d\x61\x67\x62\x17\x67\ +\xb7\xc5\xae\xc3\xee\x93\xbd\x93\x7d\xba\x7d\x8d\xfd\x3d\x07\x0d\ +\x87\xd9\x0e\xab\x1d\x5a\x1d\x7e\x73\xb4\x72\x14\x3a\x56\x3a\xde\ +\x9a\xce\x9c\xee\x3f\x7d\xc5\xf4\x96\xe9\x2f\x67\x58\xcf\x10\xcf\ +\xd8\x33\xe3\xb6\x13\xcb\x29\xc4\x69\x9d\x53\x9b\xd3\x47\x67\x17\ +\x67\xb9\x73\x83\xf3\x88\x8b\x89\x4b\x82\xcb\x2e\x97\x3e\x2e\x9b\ +\x1b\xc6\xdd\xc8\xbd\xe4\x4a\x74\xf5\x71\x5d\xe1\x7a\xd2\xf5\x9d\ +\x9b\xb3\x9b\xc2\xed\xa8\xdb\xaf\xee\x36\xee\x69\xee\x87\xdc\x9f\ +\xcc\x34\x9f\x29\x9e\x59\x33\x73\xd0\xc3\xc8\x43\xe0\x51\xe5\xd1\ +\x3f\x0b\x9f\x95\x30\x6b\xdf\xac\x7e\x4f\x43\x4f\x81\x67\xb5\xe7\ +\x23\x2f\x63\x2f\x91\x57\xad\xd7\xb0\xb7\xa5\x77\xaa\xf7\x61\xef\ +\x17\x3e\xf6\x3e\x72\x9f\xe3\x3e\xe3\x3c\x37\xde\x32\xde\x59\x5f\ +\xcc\x37\xc0\xb7\xc8\xb7\xcb\x4f\xc3\x6f\x9e\x5f\x85\xdf\x43\x7f\ +\x23\xff\x64\xff\x7a\xff\xd1\x00\xa7\x80\x25\x01\x67\x03\x89\x81\ +\x41\x81\x5b\x02\xfb\xf8\x7a\x7c\x21\xbf\x8e\x3f\x3a\xdb\x65\xf6\ +\xb2\xd9\xed\x41\x8c\xa0\xb9\x41\x15\x41\x8f\x82\xad\x82\xe5\xc1\ +\xad\x21\x68\xc8\xec\x90\xad\x21\xf7\xe7\x98\xce\x91\xce\x69\x0e\ +\x85\x50\x7e\xe8\xd6\xd0\x07\x61\xe6\x61\x8b\xc3\x7e\x0c\x27\x85\ +\x87\x85\x57\x86\x3f\x8e\x70\x88\x58\x1a\xd1\x31\x97\x35\x77\xd1\ +\xdc\x43\x73\xdf\x44\xfa\x44\x96\x44\xde\x9b\x67\x31\x4f\x39\xaf\ +\x2d\x4a\x35\x2a\x3e\xaa\x2e\x6a\x3c\xda\x37\xba\x34\xba\x3f\xc6\ +\x2e\x66\x59\xcc\xd5\x58\x9d\x58\x49\x6c\x4b\x1c\x39\x2e\x2a\xae\ +\x36\x6e\x6c\xbe\xdf\xfc\xed\xf3\x87\xe2\x9d\xe2\x0b\xe3\x7b\x17\ +\x98\x2f\xc8\x5d\x70\x79\xa1\xce\xc2\xf4\x85\xa7\x16\xa9\x2e\x12\ +\x2c\x3a\x96\x40\x4c\x88\x4e\x38\x94\xf0\x41\x10\x2a\xa8\x16\x8c\ +\x25\xf2\x13\x77\x25\x8e\x0a\x79\xc2\x1d\xc2\x67\x22\x2f\xd1\x36\ +\xd1\x88\xd8\x43\x5c\x2a\x1e\x4e\xf2\x48\x2a\x4d\x7a\x92\xec\x91\ +\xbc\x35\x79\x24\xc5\x33\xa5\x2c\xe5\xb9\x84\x27\xa9\x90\xbc\x4c\ +\x0d\x4c\xdd\x9b\x3a\x9e\x16\x9a\x76\x20\x6d\x32\x3d\x3a\xbd\x31\ +\x83\x92\x91\x90\x71\x42\xaa\x21\x4d\x93\xb6\x67\xea\x67\xe6\x66\ +\x76\xcb\xac\x65\x85\xb2\xfe\xc5\x6e\x8b\xb7\x2f\x1e\x95\x07\xc9\ +\x6b\xb3\x90\xac\x05\x59\x2d\x0a\xb6\x42\xa6\xe8\x54\x5a\x28\xd7\ +\x2a\x07\xb2\x67\x65\x57\x66\xbf\xcd\x89\xca\x39\x96\xab\x9e\x2b\ +\xcd\xed\xcc\xb3\xca\xdb\x90\x37\x9c\xef\x9f\xff\xed\x12\xc2\x12\ +\xe1\x92\xb6\xa5\x86\x4b\x57\x2d\x1d\x58\xe6\xbd\xac\x6a\x39\xb2\ +\x3c\x71\x79\xdb\x0a\xe3\x15\x05\x2b\x86\x56\x06\xac\x3c\xb8\x8a\ +\xb6\x2a\x6d\xd5\x4f\xab\xed\x57\x97\xae\x7e\xbd\x26\x7a\x4d\x6b\ +\x81\x5e\xc1\xca\x82\xc1\xb5\x01\x6b\xeb\x0b\x55\x0a\xe5\x85\x7d\ +\xeb\xdc\xd7\xed\x5d\x4f\x58\x2f\x59\xdf\xb5\x61\xfa\x86\x9d\x1b\ +\x3e\x15\x89\x8a\xae\x14\xdb\x17\x97\x15\x7f\xd8\x28\xdc\x78\xe5\ +\x1b\x87\x6f\xca\xbf\x99\xdc\x94\xb4\xa9\xab\xc4\xb9\x64\xcf\x66\ +\xd2\x66\xe9\xe6\xde\x2d\x9e\x5b\x0e\x96\xaa\x97\xe6\x97\x0e\x6e\ +\x0d\xd9\xda\xb4\x0d\xdf\x56\xb4\xed\xf5\xf6\x45\xdb\x2f\x97\xcd\ +\x28\xdb\xbb\x83\xb6\x43\xb9\xa3\xbf\x3c\xb8\xbc\x65\xa7\xc9\xce\ +\xcd\x3b\x3f\x54\xa4\x54\xf4\x54\xfa\x54\x36\xee\xd2\xdd\xb5\x61\ +\xd7\xf8\x6e\xd1\xee\x1b\x7b\xbc\xf6\x34\xec\xd5\xdb\x5b\xbc\xf7\ +\xfd\x3e\xc9\xbe\xdb\x55\x01\x55\x4d\xd5\x66\xd5\x65\xfb\x49\xfb\ +\xb3\xf7\x3f\xae\x89\xaa\xe9\xf8\x96\xfb\x6d\x5d\xad\x4e\x6d\x71\ +\xed\xc7\x03\xd2\x03\xfd\x07\x23\x0e\xb6\xd7\xb9\xd4\xd5\x1d\xd2\ +\x3d\x54\x52\x8f\xd6\x2b\xeb\x47\x0e\xc7\x1f\xbe\xfe\x9d\xef\x77\ +\x2d\x0d\x36\x0d\x55\x8d\x9c\xc6\xe2\x23\x70\x44\x79\xe4\xe9\xf7\ +\x09\xdf\xf7\x1e\x0d\x3a\xda\x76\x8c\x7b\xac\xe1\x07\xd3\x1f\x76\ +\x1d\x67\x1d\x2f\x6a\x42\x9a\xf2\x9a\x46\x9b\x53\x9a\xfb\x5b\x62\ +\x5b\xba\x4f\xcc\x3e\xd1\xd6\xea\xde\x7a\xfc\x47\xdb\x1f\x0f\x9c\ +\x34\x3c\x59\x79\x4a\xf3\x54\xc9\x69\xda\xe9\x82\xd3\x93\x67\xf2\ +\xcf\x8c\x9d\x95\x9d\x7d\x7e\x2e\xf9\xdc\x60\xdb\xa2\xb6\x7b\xe7\ +\x63\xce\xdf\x6a\x0f\x6f\xef\xba\x10\x74\xe1\xd2\x45\xff\x8b\xe7\ +\x3b\xbc\x3b\xce\x5c\xf2\xb8\x74\xf2\xb2\xdb\xe5\x13\x57\xb8\x57\ +\x9a\xaf\x3a\x5f\x6d\xea\x74\xea\x3c\xfe\x93\xd3\x4f\xc7\xbb\x9c\ +\xbb\x9a\xae\xb9\x5c\x6b\xb9\xee\x7a\xbd\xb5\x7b\x66\xf7\xe9\x1b\ +\x9e\x37\xce\xdd\xf4\xbd\x79\xf1\x16\xff\xd6\xd5\x9e\x39\x3d\xdd\ +\xbd\xf3\x7a\x6f\xf7\xc5\xf7\xf5\xdf\x16\xdd\x7e\x72\x27\xfd\xce\ +\xcb\xbb\xd9\x77\x27\xee\xad\xbc\x4f\xbc\x5f\xf4\x40\xed\x41\xd9\ +\x43\xdd\x87\xd5\x3f\x5b\xfe\xdc\xd8\xef\xdc\x7f\x6a\xc0\x77\xa0\ +\xf3\xd1\xdc\x47\xf7\x06\x85\x83\xcf\xfe\x91\xf5\x8f\x0f\x43\x05\ +\x8f\x99\x8f\xcb\x86\x0d\x86\xeb\x9e\x38\x3e\x39\x39\xe2\x3f\x72\ +\xfd\xe9\xfc\xa7\x43\xcf\x64\xcf\x26\x9e\x17\xfe\xa2\xfe\xcb\xae\ +\x17\x16\x2f\x7e\xf8\xd5\xeb\xd7\xce\xd1\x98\xd1\xa1\x97\xf2\x97\ +\x93\xbf\x6d\x7c\xa5\xfd\xea\xc0\xeb\x19\xaf\xdb\xc6\xc2\xc6\x1e\ +\xbe\xc9\x78\x33\x31\x5e\xf4\x56\xfb\xed\xc1\x77\xdc\x77\x1d\xef\ +\xa3\xdf\x0f\x4f\xe4\x7c\x20\x7f\x28\xff\x68\xf9\xb1\xf5\x53\xd0\ +\xa7\xfb\x93\x19\x93\x93\xff\x04\x03\x98\xf3\xfc\x63\x33\x2d\xdb\ +\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\x25\x00\x00\x80\x83\ +\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00\x75\x30\x00\x00\xea\x60\ +\x00\x00\x3a\x98\x00\x00\x17\x6f\x92\x5f\xc5\x46\x00\x00\x06\x29\ +\x49\x44\x41\x54\x78\xda\xec\x9b\x6b\x4c\x14\x57\x14\xc7\xff\xb3\ +\x6f\x58\xc0\x05\xb3\xac\xb0\xca\x52\x77\xc1\x05\xe4\xb9\x01\x8a\ +\x3c\x8d\x58\xa5\x8d\x52\x51\x13\x8d\x69\x6b\x4d\x63\x6a\x6a\x1a\ +\xb5\x8d\x89\x69\xad\xad\xad\x36\xb5\x35\x4d\x5b\x5b\xfb\x48\x34\ +\x35\xa5\x26\x8d\x62\x51\x12\x4b\x7d\xd0\xa2\xc8\x43\xde\xe0\x22\ +\x2b\x20\xca\x4b\xc1\x07\x08\x0b\xfb\x98\x65\xfb\x41\x34\xe1\x51\ +\x76\x67\x76\x66\x43\xc2\x9c\x4f\x93\x99\x73\xce\xdc\xfb\xcb\xdc\ +\x73\xee\x3d\xf7\x0e\xb1\x61\x6f\x8a\x1d\xb3\x58\x78\x98\xe5\xc2\ +\x01\xe0\x00\x70\x00\x38\x00\x1c\x00\x0e\x00\x07\x80\x03\x30\x6b\ +\x45\xc0\xa6\xf3\xc0\xb9\x2a\x24\x47\xbe\x84\x68\x75\x22\x6e\xb4\ +\x57\xe1\xe4\xa5\xa3\x0e\x6d\xd6\xa4\x6e\x46\x8c\x26\x09\x4d\x77\ +\x6a\x50\x5c\x77\x1e\xdd\x0f\xef\xb0\x0a\x80\x60\x63\x2d\x10\xaf\ +\x4d\x47\x62\xd8\x52\x84\x07\xc7\x8d\xbb\xdf\xd0\x56\x81\xa3\xf9\ +\x9f\xfd\xaf\xdd\xe6\xac\x5d\x48\xd0\x66\x8c\xbb\xa7\x6f\xaf\x46\ +\x99\xfe\x32\x2a\x9b\x8b\x59\x01\xc0\x5f\x9c\x1e\xf4\x31\x93\x0e\ +\x37\x2d\xdf\x8e\xd5\xc9\xaf\x41\x2e\x0b\x98\xf4\x4c\xe1\xab\x84\ +\x5a\x19\x8e\x8a\x9b\xff\x4c\x7a\xb6\x75\xd5\x1e\xe8\x42\x53\x27\ +\xdd\x97\xcb\x02\x10\x1b\xb2\x04\x72\xdf\x00\xd4\xb6\x94\xce\xec\ +\x21\x40\x10\x04\x62\x35\x4b\xa6\xd5\xd1\x06\x45\x63\xf7\xc6\xc3\ +\x68\xbe\x5b\x07\x89\xc8\x13\x23\x66\x23\xd4\xca\x30\x68\x94\x11\ +\xd3\xda\xc5\x85\xa4\x20\xf7\xc2\x11\x58\x49\xcb\xcc\x05\xc0\xe7\ +\xf1\x61\x21\xcd\xf0\x84\xd7\xb4\x7a\x2a\x85\x06\x2a\x85\x86\x92\ +\x6f\x8b\xd5\x04\x01\x5f\xc8\x38\x00\x1e\xb3\x5f\x00\x0f\x04\x88\ +\xd9\x9b\x06\xad\xa4\x05\x7c\x3e\x3b\x89\x45\x20\x10\xc2\x4a\x9a\ +\x67\x36\x80\x79\x7e\x0b\xe0\x21\x96\xb2\x02\x40\x28\x10\x41\x2e\ +\x0b\x9c\xb9\x00\xfc\x7d\x95\xd8\xb1\xfe\x00\xf8\x3c\x3e\x3b\xf9\ +\x1a\x04\xde\xcd\xd9\x3f\x65\x76\x71\x3b\x00\x91\x50\x02\xa9\xc4\ +\x1b\x52\x89\x37\x7c\xa4\xbe\x50\x29\x34\xd8\xbd\xe1\x4b\xf8\x78\ +\xca\x58\x1d\xaf\x73\xbc\xfc\xb0\x7b\xe3\x57\x98\x2f\x7f\x01\x12\ +\x91\x27\x84\x02\x11\x84\x02\x91\x4b\x71\x87\xf2\x44\x68\xf3\xca\ +\x5d\xd0\xaa\x62\x20\x12\x88\xc6\x22\xbf\x00\xc2\xb1\x6b\x77\xca\ +\xd0\xc8\x13\xd8\x6c\x24\xec\xb0\xc3\x4a\x5a\x60\xe8\x6c\x40\xee\ +\x85\x23\xec\xa6\xc1\x68\xf5\x8b\x48\x08\xcb\x60\xac\x13\xa3\xf6\ +\x51\xf0\x08\x7a\xa3\xd0\xcb\xc3\x67\xd2\x84\xa9\xb1\xad\x12\x75\ +\xad\x65\xec\x01\xc8\xd4\xbd\xea\x52\x87\x8d\xa6\x41\x54\x35\x5f\ +\x85\xa1\xb3\x1e\xf7\x1f\x75\xc1\x64\x19\x86\x44\xe4\x09\x85\x9f\ +\x12\x8b\x16\x44\x21\x2e\x34\x05\x52\x89\x37\x6d\xff\xcb\x74\xd9\ +\xec\x01\xf0\x97\x05\x42\xad\x0c\xa7\xdd\xb8\x8b\x55\x67\x50\x58\ +\x71\x0a\x46\xd3\xe0\xa4\x67\x5d\x0f\xda\x51\x6d\x28\xc1\xd9\x92\ +\xdf\xb0\x22\x61\x3d\x6d\xd0\x1a\x65\x04\xe4\xb2\x79\xe8\xeb\xbf\ +\xc7\x7c\x10\x5c\x91\xb0\x8e\x76\xe7\x7f\x3c\x7b\x00\x79\xc5\xc7\ +\xa7\xec\xfc\xc4\x2f\x24\xaf\xf8\x18\x7e\x3a\x7b\x90\xf6\xbb\x32\ +\x75\x39\xec\x64\x01\x5d\x68\x0a\xad\x06\xfd\xf0\xe7\x7e\xd4\xb7\ +\x96\x53\xb2\xa9\x6b\x2d\xc3\xd1\xfc\x4f\x69\xbd\x2f\x91\x62\x8c\ +\x72\x1a\x40\x51\x6d\x01\xe5\xc6\x14\xd5\x9c\x43\xe3\xed\x4a\x5a\ +\x1d\x69\x68\xbb\x8e\xa2\x9a\x73\x94\xed\xca\x9b\x8a\xd8\x01\x90\ +\x7f\xf5\x04\x25\xe7\x26\xcb\x30\xf2\x8a\x8f\xb9\x14\x34\xcf\x5c\ +\x39\x0e\x93\x65\x84\x12\x34\x67\x8a\x2e\xb4\x27\x42\xbf\xfe\xf5\ +\xb5\xd3\x6b\xf2\x8a\xa6\x7f\x61\x1b\xb5\xb9\x04\x80\xb4\x91\xb8\ +\x3e\x45\xed\x60\x2a\x31\x74\x34\xd0\x1a\x36\x94\x93\xf0\xcf\xe7\ +\x3e\xc7\xad\xce\x46\x87\x7a\xfa\x3b\xd5\x8c\xcc\x15\xf4\xed\x35\ +\x0e\x75\xda\xef\x19\xf0\xcd\xa9\x0f\xdd\x37\x15\xbe\x79\xb7\xce\ +\xa1\x4e\x5f\x7f\x0f\x23\x00\x9c\xf1\x53\xdf\x5a\x0e\x3b\xec\xee\ +\x03\x30\x47\xea\xeb\x54\x01\x83\x09\x31\x5b\x1d\xc7\x00\x1f\xa9\ +\x9f\x7b\x17\x43\x4f\x86\xfb\x9d\x5a\x30\x31\x21\x62\xa1\x87\xe3\ +\xf6\x18\x1f\xb9\x0f\x00\x41\x10\x88\x5c\x18\xef\x50\x8f\xa9\x65\ +\xab\x33\x7e\xb4\xaa\x18\xf7\x01\xd8\xb9\xfe\x20\x54\x8a\x10\x87\ +\x7a\x11\x13\x4a\xe2\x74\x25\xdc\x09\x3f\xa1\xf3\x23\xb1\x75\xd5\ +\x1e\xf6\x01\xbc\xb3\x66\x9f\xc3\xea\xed\x33\x89\xd7\x66\xb8\x5c\ +\x1c\xe1\xf3\xf8\x88\xd7\xa6\x3b\xa5\x1b\xa3\x49\xc2\x1b\x2b\x77\ +\xb2\x07\x60\xd3\xf2\xed\x88\x08\xd6\x39\xed\x58\x22\xf2\x40\x4e\ +\xda\x16\x97\x00\xac\x4d\x7f\x0b\x12\x91\x87\xd3\xfa\x89\x61\x4b\ +\x91\x9d\xfc\x3a\x3b\x00\x26\xee\xd8\x38\x23\x4b\x63\x57\x39\x15\ +\x2f\xa6\x92\xc8\x85\x09\xc8\x88\x79\x85\xb2\x5d\x7a\xcc\xcb\xec\ +\x00\xa8\xa0\x38\xc7\x7e\x26\xdb\xb2\xf7\x22\x4a\x9d\x48\xc9\x26\ +\x46\x93\x84\x6d\xd9\xf4\x26\x36\x55\xcd\x57\xd8\x01\xf0\x77\x65\ +\x1e\xed\x4f\xf9\xed\xd5\x1f\x20\x27\xed\x4d\x87\xc5\x0e\xa9\xc4\ +\x1b\x6b\xd3\xb6\xd0\x0e\x68\x4f\xdb\x79\x9a\x9d\x82\x48\x5f\x7f\ +\x0f\x5a\xbb\xf4\xb4\x8b\x22\x99\xba\x35\x48\x8a\xc8\x44\xb5\xe1\ +\x2a\x9a\x3b\xea\xd1\xfb\xb8\x1b\x23\x66\x23\x3c\xc4\x52\xf8\xfb\ +\x06\x62\xd1\x82\x68\xc4\x85\x26\xbb\x54\x11\x32\x74\x36\x50\x2a\ +\x86\x50\x02\x00\x00\x97\xaa\xf3\x5d\xaa\x0a\x49\x25\xde\x48\x8d\ +\xca\x42\x6a\x54\x16\x00\xc0\x6e\xb7\x83\x20\x98\xdb\x49\xba\x54\ +\x95\x4f\xd9\x86\x12\x80\xda\x96\x52\x94\x34\x14\x22\x64\xfe\x62\ +\x08\x05\x62\x10\x04\xc0\xe7\x09\xe1\xed\x39\x87\x56\x83\x5d\xe9\ +\xbc\x85\x34\xc3\x66\x23\xc7\xae\x2d\xb8\x79\xb7\x16\x0d\x6d\x15\ +\xec\x02\x00\x80\xdc\x8b\xdf\x3f\x35\xe4\x0b\x9f\xe7\x6a\xb9\x2c\ +\x00\x3b\xd6\x1d\x80\xa7\xc4\x0b\x6c\x8b\xd1\x34\x88\x6f\x4f\x7f\ +\x84\xfe\xa1\x07\xcf\x97\xdb\x24\x69\x85\x85\xe6\xb6\x19\xed\x9d\ +\x21\xd2\x66\x05\x69\xb3\xc2\x6c\x35\xa1\xb3\xef\x36\x0e\x9d\x7c\ +\x1f\x03\x43\x8f\x58\xed\xfc\x80\xf1\x31\x0e\x9d\x7c\x0f\x1d\xbd\ +\xad\x18\x1c\x1e\xc0\xb0\x69\x08\xc3\xa6\x21\xda\x9d\x77\x09\xc0\ +\x44\xe9\xed\xef\xc6\x77\x79\xfb\x68\x2f\x4b\x9d\x29\x8e\x7c\xfd\ +\xc7\x1e\xca\x41\xce\x6d\x00\x00\xa0\x6f\xa0\x07\x24\x69\x65\x05\ +\xc0\x88\xd9\x88\xde\xfe\x6e\xc6\xfd\x32\x0a\x40\x24\x10\x83\x1c\ +\x0b\x4c\x4c\x8b\x6d\xd4\xca\xca\x16\x1c\xe3\xc7\xe4\x98\x4c\x6b\ +\x93\x21\xd8\x18\xf7\xc9\xe8\x69\x06\x2b\x69\x81\x48\x28\x76\xa8\ +\x77\xab\xb3\x11\xad\x5d\x7a\x48\xc4\x52\x98\xcc\x46\x84\x07\xc7\ +\x21\xc8\xc1\x91\x19\xa1\x40\x0c\xfb\xe8\xe8\xcc\x06\x60\x21\xcd\ +\xa8\x6a\xbe\x32\xed\x12\xb6\xe6\x56\x09\x7e\x29\xf8\x62\xdc\xbd\ +\x82\xd2\xdf\xb1\x3d\xe7\x13\x68\x83\xa2\xa7\xb1\xbb\xc6\x4a\x80\ +\x65\xfc\x98\x5c\x6d\x4b\x29\xee\x3f\xee\x84\x48\x28\x81\xff\x84\ +\x13\x1d\x65\xfa\xcb\x38\x7e\xfe\xf0\x24\x1b\x3b\xec\x28\x6f\x2a\ +\x42\x90\x42\x0d\x85\xaf\x72\xdc\xb3\x1b\xed\x55\x28\xb8\x96\x8b\ +\xc2\xeb\xa7\x58\x19\x56\x04\x9b\x3f\x4d\x05\xcc\x0d\x42\x5a\x74\ +\x16\xc2\x54\xb1\xa8\x31\x5c\x43\x7e\xc9\x09\x87\x36\x1b\x97\x6d\ +\x43\x78\xb0\x0e\x75\x2d\x65\x28\x69\x2c\x44\xcf\xc3\x0e\x56\xe7\ +\x16\x04\xf7\xd7\xd8\x2c\x17\x0e\x00\x07\x80\x03\xc0\x01\xe0\x00\ +\x70\x00\x38\x00\xb3\x57\xfe\x1b\x00\x50\x37\x09\xa9\xfa\xed\xb7\ +\x72\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +\x00\x00\x15\x73\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x03\xe8\x00\x00\x00\x44\x08\x06\x00\x00\x00\x12\x95\x1f\xec\ +\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\ +\x01\x00\x9a\x9c\x18\x00\x00\x0a\x4f\x69\x43\x43\x50\x50\x68\x6f\ +\x74\x6f\x73\x68\x6f\x70\x20\x49\x43\x43\x20\x70\x72\x6f\x66\x69\ +\x6c\x65\x00\x00\x78\xda\x9d\x53\x67\x54\x53\xe9\x16\x3d\xf7\xde\ +\xf4\x42\x4b\x88\x80\x94\x4b\x6f\x52\x15\x08\x20\x52\x42\x8b\x80\ +\x14\x91\x26\x2a\x21\x09\x10\x4a\x88\x21\xa1\xd9\x15\x51\xc1\x11\ +\x45\x45\x04\x1b\xc8\xa0\x88\x03\x8e\x8e\x80\x8c\x15\x51\x2c\x0c\ +\x8a\x0a\xd8\x07\xe4\x21\xa2\x8e\x83\xa3\x88\x8a\xca\xfb\xe1\x7b\ +\xa3\x6b\xd6\xbc\xf7\xe6\xcd\xfe\xb5\xd7\x3e\xe7\xac\xf3\x9d\xb3\ +\xcf\x07\xc0\x08\x0c\x96\x48\x33\x51\x35\x80\x0c\xa9\x42\x1e\x11\ +\xe0\x83\xc7\xc4\xc6\xe1\xe4\x2e\x40\x81\x0a\x24\x70\x00\x10\x08\ +\xb3\x64\x21\x73\xfd\x23\x01\x00\xf8\x7e\x3c\x3c\x2b\x22\xc0\x07\ +\xbe\x00\x01\x78\xd3\x0b\x08\x00\xc0\x4d\x9b\xc0\x30\x1c\x87\xff\ +\x0f\xea\x42\x99\x5c\x01\x80\x84\x01\xc0\x74\x91\x38\x4b\x08\x80\ +\x14\x00\x40\x7a\x8e\x42\xa6\x00\x40\x46\x01\x80\x9d\x98\x26\x53\ +\x00\xa0\x04\x00\x60\xcb\x63\x62\xe3\x00\x50\x2d\x00\x60\x27\x7f\ +\xe6\xd3\x00\x80\x9d\xf8\x99\x7b\x01\x00\x5b\x94\x21\x15\x01\xa0\ +\x91\x00\x20\x13\x65\x88\x44\x00\x68\x3b\x00\xac\xcf\x56\x8a\x45\ +\x00\x58\x30\x00\x14\x66\x4b\xc4\x39\x00\xd8\x2d\x00\x30\x49\x57\ +\x66\x48\x00\xb0\xb7\x00\xc0\xce\x10\x0b\xb2\x00\x08\x0c\x00\x30\ +\x51\x88\x85\x29\x00\x04\x7b\x00\x60\xc8\x23\x23\x78\x00\x84\x99\ +\x00\x14\x46\xf2\x57\x3c\xf1\x2b\xae\x10\xe7\x2a\x00\x00\x78\x99\ +\xb2\x3c\xb9\x24\x39\x45\x81\x5b\x08\x2d\x71\x07\x57\x57\x2e\x1e\ +\x28\xce\x49\x17\x2b\x14\x36\x61\x02\x61\x9a\x40\x2e\xc2\x79\x99\ +\x19\x32\x81\x34\x0f\xe0\xf3\xcc\x00\x00\xa0\x91\x15\x11\xe0\x83\ +\xf3\xfd\x78\xce\x0e\xae\xce\xce\x36\x8e\xb6\x0e\x5f\x2d\xea\xbf\ +\x06\xff\x22\x62\x62\xe3\xfe\xe5\xcf\xab\x70\x40\x00\x00\xe1\x74\ +\x7e\xd1\xfe\x2c\x2f\xb3\x1a\x80\x3b\x06\x80\x6d\xfe\xa2\x25\xee\ +\x04\x68\x5e\x0b\xa0\x75\xf7\x8b\x66\xb2\x0f\x40\xb5\x00\xa0\xe9\ +\xda\x57\xf3\x70\xf8\x7e\x3c\x3c\x45\xa1\x90\xb9\xd9\xd9\xe5\xe4\ +\xe4\xd8\x4a\xc4\x42\x5b\x61\xca\x57\x7d\xfe\x67\xc2\x5f\xc0\x57\ +\xfd\x6c\xf9\x7e\x3c\xfc\xf7\xf5\xe0\xbe\xe2\x24\x81\x32\x5d\x81\ +\x47\x04\xf8\xe0\xc2\xcc\xf4\x4c\xa5\x1c\xcf\x92\x09\x84\x62\xdc\ +\xe6\x8f\x47\xfc\xb7\x0b\xff\xfc\x1d\xd3\x22\xc4\x49\x62\xb9\x58\ +\x2a\x14\xe3\x51\x12\x71\x8e\x44\x9a\x8c\xf3\x32\xa5\x22\x89\x42\ +\x92\x29\xc5\x25\xd2\xff\x64\xe2\xdf\x2c\xfb\x03\x3e\xdf\x35\x00\ +\xb0\x6a\x3e\x01\x7b\x91\x2d\xa8\x5d\x63\x03\xf6\x4b\x27\x10\x58\ +\x74\xc0\xe2\xf7\x00\x00\xf2\xbb\x6f\xc1\xd4\x28\x08\x03\x80\x68\ +\x83\xe1\xcf\x77\xff\xef\x3f\xfd\x47\xa0\x25\x00\x80\x66\x49\x92\ +\x71\x00\x00\x5e\x44\x24\x2e\x54\xca\xb3\x3f\xc7\x08\x00\x00\x44\ +\xa0\x81\x2a\xb0\x41\x1b\xf4\xc1\x18\x2c\xc0\x06\x1c\xc1\x05\xdc\ +\xc1\x0b\xfc\x60\x36\x84\x42\x24\xc4\xc2\x42\x10\x42\x0a\x64\x80\ +\x1c\x72\x60\x29\xac\x82\x42\x28\x86\xcd\xb0\x1d\x2a\x60\x2f\xd4\ +\x40\x1d\x34\xc0\x51\x68\x86\x93\x70\x0e\x2e\xc2\x55\xb8\x0e\x3d\ +\x70\x0f\xfa\x61\x08\x9e\xc1\x28\xbc\x81\x09\x04\x41\xc8\x08\x13\ +\x61\x21\xda\x88\x01\x62\x8a\x58\x23\x8e\x08\x17\x99\x85\xf8\x21\ +\xc1\x48\x04\x12\x8b\x24\x20\xc9\x88\x14\x51\x22\x4b\x91\x35\x48\ +\x31\x52\x8a\x54\x20\x55\x48\x1d\xf2\x3d\x72\x02\x39\x87\x5c\x46\ +\xba\x91\x3b\xc8\x00\x32\x82\xfc\x86\xbc\x47\x31\x94\x81\xb2\x51\ +\x3d\xd4\x0c\xb5\x43\xb9\xa8\x37\x1a\x84\x46\xa2\x0b\xd0\x64\x74\ +\x31\x9a\x8f\x16\xa0\x9b\xd0\x72\xb4\x1a\x3d\x8c\x36\xa1\xe7\xd0\ +\xab\x68\x0f\xda\x8f\x3e\x43\xc7\x30\xc0\xe8\x18\x07\x33\xc4\x6c\ +\x30\x2e\xc6\xc3\x42\xb1\x38\x2c\x09\x93\x63\xcb\xb1\x22\xac\x0c\ +\xab\xc6\x1a\xb0\x56\xac\x03\xbb\x89\xf5\x63\xcf\xb1\x77\x04\x12\ +\x81\x45\xc0\x09\x36\x04\x77\x42\x20\x61\x1e\x41\x48\x58\x4c\x58\ +\x4e\xd8\x48\xa8\x20\x1c\x24\x34\x11\xda\x09\x37\x09\x03\x84\x51\ +\xc2\x27\x22\x93\xa8\x4b\xb4\x26\xba\x11\xf9\xc4\x18\x62\x32\x31\ +\x87\x58\x48\x2c\x23\xd6\x12\x8f\x13\x2f\x10\x7b\x88\x43\xc4\x37\ +\x24\x12\x89\x43\x32\x27\xb9\x90\x02\x49\xb1\xa4\x54\xd2\x12\xd2\ +\x46\xd2\x6e\x52\x23\xe9\x2c\xa9\x9b\x34\x48\x1a\x23\x93\xc9\xda\ +\x64\x6b\xb2\x07\x39\x94\x2c\x20\x2b\xc8\x85\xe4\x9d\xe4\xc3\xe4\ +\x33\xe4\x1b\xe4\x21\xf2\x5b\x0a\x9d\x62\x40\x71\xa4\xf8\x53\xe2\ +\x28\x52\xca\x6a\x4a\x19\xe5\x10\xe5\x34\xe5\x06\x65\x98\x32\x41\ +\x55\xa3\x9a\x52\xdd\xa8\xa1\x54\x11\x35\x8f\x5a\x42\xad\xa1\xb6\ +\x52\xaf\x51\x87\xa8\x13\x34\x75\x9a\x39\xcd\x83\x16\x49\x4b\xa5\ +\xad\xa2\x95\xd3\x1a\x68\x17\x68\xf7\x69\xaf\xe8\x74\xba\x11\xdd\ +\x95\x1e\x4e\x97\xd0\x57\xd2\xcb\xe9\x47\xe8\x97\xe8\x03\xf4\x77\ +\x0c\x0d\x86\x15\x83\xc7\x88\x67\x28\x19\x9b\x18\x07\x18\x67\x19\ +\x77\x18\xaf\x98\x4c\xa6\x19\xd3\x8b\x19\xc7\x54\x30\x37\x31\xeb\ +\x98\xe7\x99\x0f\x99\x6f\x55\x58\x2a\xb6\x2a\x7c\x15\x91\xca\x0a\ +\x95\x4a\x95\x26\x95\x1b\x2a\x2f\x54\xa9\xaa\xa6\xaa\xde\xaa\x0b\ +\x55\xf3\x55\xcb\x54\x8f\xa9\x5e\x53\x7d\xae\x46\x55\x33\x53\xe3\ +\xa9\x09\xd4\x96\xab\x55\xaa\x9d\x50\xeb\x53\x1b\x53\x67\xa9\x3b\ +\xa8\x87\xaa\x67\xa8\x6f\x54\x3f\xa4\x7e\x59\xfd\x89\x06\x59\xc3\ +\x4c\xc3\x4f\x43\xa4\x51\xa0\xb1\x5f\xe3\xbc\xc6\x20\x0b\x63\x19\ +\xb3\x78\x2c\x21\x6b\x0d\xab\x86\x75\x81\x35\xc4\x26\xb1\xcd\xd9\ +\x7c\x76\x2a\xbb\x98\xfd\x1d\xbb\x8b\x3d\xaa\xa9\xa1\x39\x43\x33\ +\x4a\x33\x57\xb3\x52\xf3\x94\x66\x3f\x07\xe3\x98\x71\xf8\x9c\x74\ +\x4e\x09\xe7\x28\xa7\x97\xf3\x7e\x8a\xde\x14\xef\x29\xe2\x29\x1b\ +\xa6\x34\x4c\xb9\x31\x65\x5c\x6b\xaa\x96\x97\x96\x58\xab\x48\xab\ +\x51\xab\x47\xeb\xbd\x36\xae\xed\xa7\x9d\xa6\xbd\x45\xbb\x59\xfb\ +\x81\x0e\x41\xc7\x4a\x27\x5c\x27\x47\x67\x8f\xce\x05\x9d\xe7\x53\ +\xd9\x53\xdd\xa7\x0a\xa7\x16\x4d\x3d\x3a\xf5\xae\x2e\xaa\x6b\xa5\ +\x1b\xa1\xbb\x44\x77\xbf\x6e\xa7\xee\x98\x9e\xbe\x5e\x80\x9e\x4c\ +\x6f\xa7\xde\x79\xbd\xe7\xfa\x1c\x7d\x2f\xfd\x54\xfd\x6d\xfa\xa7\ +\xf5\x47\x0c\x58\x06\xb3\x0c\x24\x06\xdb\x0c\xce\x18\x3c\xc5\x35\ +\x71\x6f\x3c\x1d\x2f\xc7\xdb\xf1\x51\x43\x5d\xc3\x40\x43\xa5\x61\ +\x95\x61\x97\xe1\x84\x91\xb9\xd1\x3c\xa3\xd5\x46\x8d\x46\x0f\x8c\ +\x69\xc6\x5c\xe3\x24\xe3\x6d\xc6\x6d\xc6\xa3\x26\x06\x26\x21\x26\ +\x4b\x4d\xea\x4d\xee\x9a\x52\x4d\xb9\xa6\x29\xa6\x3b\x4c\x3b\x4c\ +\xc7\xcd\xcc\xcd\xa2\xcd\xd6\x99\x35\x9b\x3d\x31\xd7\x32\xe7\x9b\ +\xe7\x9b\xd7\x9b\xdf\xb7\x60\x5a\x78\x5a\x2c\xb6\xa8\xb6\xb8\x65\ +\x49\xb2\xe4\x5a\xa6\x59\xee\xb6\xbc\x6e\x85\x5a\x39\x59\xa5\x58\ +\x55\x5a\x5d\xb3\x46\xad\x9d\xad\x25\xd6\xbb\xad\xbb\xa7\x11\xa7\ +\xb9\x4e\x93\x4e\xab\x9e\xd6\x67\xc3\xb0\xf1\xb6\xc9\xb6\xa9\xb7\ +\x19\xb0\xe5\xd8\x06\xdb\xae\xb6\x6d\xb6\x7d\x61\x67\x62\x17\x67\ +\xb7\xc5\xae\xc3\xee\x93\xbd\x93\x7d\xba\x7d\x8d\xfd\x3d\x07\x0d\ +\x87\xd9\x0e\xab\x1d\x5a\x1d\x7e\x73\xb4\x72\x14\x3a\x56\x3a\xde\ +\x9a\xce\x9c\xee\x3f\x7d\xc5\xf4\x96\xe9\x2f\x67\x58\xcf\x10\xcf\ +\xd8\x33\xe3\xb6\x13\xcb\x29\xc4\x69\x9d\x53\x9b\xd3\x47\x67\x17\ +\x67\xb9\x73\x83\xf3\x88\x8b\x89\x4b\x82\xcb\x2e\x97\x3e\x2e\x9b\ +\x1b\xc6\xdd\xc8\xbd\xe4\x4a\x74\xf5\x71\x5d\xe1\x7a\xd2\xf5\x9d\ +\x9b\xb3\x9b\xc2\xed\xa8\xdb\xaf\xee\x36\xee\x69\xee\x87\xdc\x9f\ +\xcc\x34\x9f\x29\x9e\x59\x33\x73\xd0\xc3\xc8\x43\xe0\x51\xe5\xd1\ +\x3f\x0b\x9f\x95\x30\x6b\xdf\xac\x7e\x4f\x43\x4f\x81\x67\xb5\xe7\ +\x23\x2f\x63\x2f\x91\x57\xad\xd7\xb0\xb7\xa5\x77\xaa\xf7\x61\xef\ +\x17\x3e\xf6\x3e\x72\x9f\xe3\x3e\xe3\x3c\x37\xde\x32\xde\x59\x5f\ +\xcc\x37\xc0\xb7\xc8\xb7\xcb\x4f\xc3\x6f\x9e\x5f\x85\xdf\x43\x7f\ +\x23\xff\x64\xff\x7a\xff\xd1\x00\xa7\x80\x25\x01\x67\x03\x89\x81\ +\x41\x81\x5b\x02\xfb\xf8\x7a\x7c\x21\xbf\x8e\x3f\x3a\xdb\x65\xf6\ +\xb2\xd9\xed\x41\x8c\xa0\xb9\x41\x15\x41\x8f\x82\xad\x82\xe5\xc1\ +\xad\x21\x68\xc8\xec\x90\xad\x21\xf7\xe7\x98\xce\x91\xce\x69\x0e\ +\x85\x50\x7e\xe8\xd6\xd0\x07\x61\xe6\x61\x8b\xc3\x7e\x0c\x27\x85\ +\x87\x85\x57\x86\x3f\x8e\x70\x88\x58\x1a\xd1\x31\x97\x35\x77\xd1\ +\xdc\x43\x73\xdf\x44\xfa\x44\x96\x44\xde\x9b\x67\x31\x4f\x39\xaf\ +\x2d\x4a\x35\x2a\x3e\xaa\x2e\x6a\x3c\xda\x37\xba\x34\xba\x3f\xc6\ +\x2e\x66\x59\xcc\xd5\x58\x9d\x58\x49\x6c\x4b\x1c\x39\x2e\x2a\xae\ +\x36\x6e\x6c\xbe\xdf\xfc\xed\xf3\x87\xe2\x9d\xe2\x0b\xe3\x7b\x17\ +\x98\x2f\xc8\x5d\x70\x79\xa1\xce\xc2\xf4\x85\xa7\x16\xa9\x2e\x12\ +\x2c\x3a\x96\x40\x4c\x88\x4e\x38\x94\xf0\x41\x10\x2a\xa8\x16\x8c\ +\x25\xf2\x13\x77\x25\x8e\x0a\x79\xc2\x1d\xc2\x67\x22\x2f\xd1\x36\ +\xd1\x88\xd8\x43\x5c\x2a\x1e\x4e\xf2\x48\x2a\x4d\x7a\x92\xec\x91\ +\xbc\x35\x79\x24\xc5\x33\xa5\x2c\xe5\xb9\x84\x27\xa9\x90\xbc\x4c\ +\x0d\x4c\xdd\x9b\x3a\x9e\x16\x9a\x76\x20\x6d\x32\x3d\x3a\xbd\x31\ +\x83\x92\x91\x90\x71\x42\xaa\x21\x4d\x93\xb6\x67\xea\x67\xe6\x66\ +\x76\xcb\xac\x65\x85\xb2\xfe\xc5\x6e\x8b\xb7\x2f\x1e\x95\x07\xc9\ +\x6b\xb3\x90\xac\x05\x59\x2d\x0a\xb6\x42\xa6\xe8\x54\x5a\x28\xd7\ +\x2a\x07\xb2\x67\x65\x57\x66\xbf\xcd\x89\xca\x39\x96\xab\x9e\x2b\ +\xcd\xed\xcc\xb3\xca\xdb\x90\x37\x9c\xef\x9f\xff\xed\x12\xc2\x12\ +\xe1\x92\xb6\xa5\x86\x4b\x57\x2d\x1d\x58\xe6\xbd\xac\x6a\x39\xb2\ +\x3c\x71\x79\xdb\x0a\xe3\x15\x05\x2b\x86\x56\x06\xac\x3c\xb8\x8a\ +\xb6\x2a\x6d\xd5\x4f\xab\xed\x57\x97\xae\x7e\xbd\x26\x7a\x4d\x6b\ +\x81\x5e\xc1\xca\x82\xc1\xb5\x01\x6b\xeb\x0b\x55\x0a\xe5\x85\x7d\ +\xeb\xdc\xd7\xed\x5d\x4f\x58\x2f\x59\xdf\xb5\x61\xfa\x86\x9d\x1b\ +\x3e\x15\x89\x8a\xae\x14\xdb\x17\x97\x15\x7f\xd8\x28\xdc\x78\xe5\ +\x1b\x87\x6f\xca\xbf\x99\xdc\x94\xb4\xa9\xab\xc4\xb9\x64\xcf\x66\ +\xd2\x66\xe9\xe6\xde\x2d\x9e\x5b\x0e\x96\xaa\x97\xe6\x97\x0e\x6e\ +\x0d\xd9\xda\xb4\x0d\xdf\x56\xb4\xed\xf5\xf6\x45\xdb\x2f\x97\xcd\ +\x28\xdb\xbb\x83\xb6\x43\xb9\xa3\xbf\x3c\xb8\xbc\x65\xa7\xc9\xce\ +\xcd\x3b\x3f\x54\xa4\x54\xf4\x54\xfa\x54\x36\xee\xd2\xdd\xb5\x61\ +\xd7\xf8\x6e\xd1\xee\x1b\x7b\xbc\xf6\x34\xec\xd5\xdb\x5b\xbc\xf7\ +\xfd\x3e\xc9\xbe\xdb\x55\x01\x55\x4d\xd5\x66\xd5\x65\xfb\x49\xfb\ +\xb3\xf7\x3f\xae\x89\xaa\xe9\xf8\x96\xfb\x6d\x5d\xad\x4e\x6d\x71\ +\xed\xc7\x03\xd2\x03\xfd\x07\x23\x0e\xb6\xd7\xb9\xd4\xd5\x1d\xd2\ +\x3d\x54\x52\x8f\xd6\x2b\xeb\x47\x0e\xc7\x1f\xbe\xfe\x9d\xef\x77\ +\x2d\x0d\x36\x0d\x55\x8d\x9c\xc6\xe2\x23\x70\x44\x79\xe4\xe9\xf7\ +\x09\xdf\xf7\x1e\x0d\x3a\xda\x76\x8c\x7b\xac\xe1\x07\xd3\x1f\x76\ +\x1d\x67\x1d\x2f\x6a\x42\x9a\xf2\x9a\x46\x9b\x53\x9a\xfb\x5b\x62\ +\x5b\xba\x4f\xcc\x3e\xd1\xd6\xea\xde\x7a\xfc\x47\xdb\x1f\x0f\x9c\ +\x34\x3c\x59\x79\x4a\xf3\x54\xc9\x69\xda\xe9\x82\xd3\x93\x67\xf2\ +\xcf\x8c\x9d\x95\x9d\x7d\x7e\x2e\xf9\xdc\x60\xdb\xa2\xb6\x7b\xe7\ +\x63\xce\xdf\x6a\x0f\x6f\xef\xba\x10\x74\xe1\xd2\x45\xff\x8b\xe7\ +\x3b\xbc\x3b\xce\x5c\xf2\xb8\x74\xf2\xb2\xdb\xe5\x13\x57\xb8\x57\ +\x9a\xaf\x3a\x5f\x6d\xea\x74\xea\x3c\xfe\x93\xd3\x4f\xc7\xbb\x9c\ +\xbb\x9a\xae\xb9\x5c\x6b\xb9\xee\x7a\xbd\xb5\x7b\x66\xf7\xe9\x1b\ +\x9e\x37\xce\xdd\xf4\xbd\x79\xf1\x16\xff\xd6\xd5\x9e\x39\x3d\xdd\ +\xbd\xf3\x7a\x6f\xf7\xc5\xf7\xf5\xdf\x16\xdd\x7e\x72\x27\xfd\xce\ +\xcb\xbb\xd9\x77\x27\xee\xad\xbc\x4f\xbc\x5f\xf4\x40\xed\x41\xd9\ +\x43\xdd\x87\xd5\x3f\x5b\xfe\xdc\xd8\xef\xdc\x7f\x6a\xc0\x77\xa0\ +\xf3\xd1\xdc\x47\xf7\x06\x85\x83\xcf\xfe\x91\xf5\x8f\x0f\x43\x05\ +\x8f\x99\x8f\xcb\x86\x0d\x86\xeb\x9e\x38\x3e\x39\x39\xe2\x3f\x72\ +\xfd\xe9\xfc\xa7\x43\xcf\x64\xcf\x26\x9e\x17\xfe\xa2\xfe\xcb\xae\ +\x17\x16\x2f\x7e\xf8\xd5\xeb\xd7\xce\xd1\x98\xd1\xa1\x97\xf2\x97\ +\x93\xbf\x6d\x7c\xa5\xfd\xea\xc0\xeb\x19\xaf\xdb\xc6\xc2\xc6\x1e\ +\xbe\xc9\x78\x33\x31\x5e\xf4\x56\xfb\xed\xc1\x77\xdc\x77\x1d\xef\ +\xa3\xdf\x0f\x4f\xe4\x7c\x20\x7f\x28\xff\x68\xf9\xb1\xf5\x53\xd0\ +\xa7\xfb\x93\x19\x93\x93\xff\x04\x03\x98\xf3\xfc\x63\x33\x2d\xdb\ +\x00\x00\x00\x20\x63\x48\x52\x4d\x00\x00\x7a\x25\x00\x00\x80\x83\ +\x00\x00\xf9\xff\x00\x00\x80\xe9\x00\x00\x75\x30\x00\x00\xea\x60\ +\x00\x00\x3a\x98\x00\x00\x17\x6f\x92\x5f\xc5\x46\x00\x00\x0a\x9e\ +\x49\x44\x41\x54\x78\xda\xec\xdd\xdb\x6f\x5c\x57\xd9\x07\xe0\x77\ +\x4e\x7b\x66\x7c\xc0\x63\x37\x4e\x30\x10\x3e\x7d\x11\x4d\xa4\xba\ +\x84\x42\x50\x73\x81\x7a\x81\xb8\xe6\xaf\xe5\xf2\x13\xe2\xa2\xaa\ +\x3e\x15\x51\x28\x55\x83\x08\x95\x2c\xa0\xa5\x26\x24\xb5\x9d\xf8\ +\x30\xc7\x3d\x9b\x8b\xe9\x8c\xc7\xf6\x1c\xec\xb1\xad\xd8\xf1\xf3\ +\xdc\x45\x3b\xb3\x67\xf6\x5a\x2b\x52\x7e\x7b\x1d\xde\xdc\xa3\x47\ +\x8f\xb2\x00\xb8\x24\xf5\x7a\x5d\x23\x00\x00\xc0\x29\xe4\x35\x01\ +\x00\x00\x00\x08\xe8\x00\x00\x00\x80\x80\x0e\x00\x00\x00\x02\x3a\ +\x00\x00\x00\x20\xa0\x03\x00\x00\x80\x80\x0e\x00\x00\x00\x08\xe8\ +\x00\x00\x00\x20\xa0\x03\x00\x00\x00\x02\x3a\x00\x00\x00\x08\xe8\ +\x00\x00\x00\x80\x80\x0e\x00\x00\x00\x02\x3a\x00\x00\x00\x20\xa0\ +\x03\x00\x00\x80\x80\x0e\x00\x00\x00\x08\xe8\x00\x00\x00\x20\xa0\ +\x03\x00\x00\x00\x02\x3a\x00\x00\x00\x08\xe8\x00\x00\x00\x80\x80\ +\x0e\x00\x00\x00\x02\x3a\x00\x00\x00\x20\xa0\x03\x00\x00\x80\x80\ +\x0e\x00\x00\x00\x08\xe8\x00\x00\x00\x20\xa0\x03\x00\x00\x00\x02\ +\x3a\x00\x00\x00\x08\xe8\x00\x00\x00\xc0\x0c\x8a\x97\xfd\x05\x95\ +\x3b\xe9\xc4\xeb\xad\xed\x7c\x74\x5b\xb9\xb1\xd7\xf3\x49\x16\xc9\ +\x72\xf7\x54\x7f\xf7\xc8\x83\x2d\x64\x51\x9c\xef\x7d\xae\xf1\xac\ +\x70\xaa\xdf\xd8\xd9\xcf\x47\x67\x6f\xf2\xfd\x93\x95\x6e\x94\x57\ +\xd3\xc8\x17\xb3\x88\x88\x68\xbf\xca\x47\x6b\xbb\x30\xf5\x73\xd3\ +\x7e\xcf\xf0\xf5\x69\xcf\x99\xac\x74\x23\x5f\xca\xa2\xdb\xce\x45\ +\x6b\x6b\xf6\x77\x2c\xc5\x85\x2c\x92\xe5\x34\x4a\xdf\xe9\x7d\x6f\ +\xb7\x93\x8b\xe6\xf3\xc2\xd4\x7b\x4e\xeb\xd3\x61\xc3\x6d\x7a\x96\ +\xcf\x1d\xff\xfc\xf0\x38\x38\x8b\xe3\x6d\x39\xad\xaf\xfb\xd7\xa7\ +\xb5\xed\x59\xc7\xe5\xf1\x71\xd3\xa9\xe7\xa3\xf9\x9f\xe9\xe3\xe6\ +\x78\x7f\x95\x6f\xa7\x51\xac\x76\x67\xbe\x07\x00\x00\x70\x83\x03\ +\xfa\xad\xf7\xeb\xd3\x43\xd4\x4e\x21\x5e\xfd\x2d\x19\x19\x5c\x93\ +\xe5\xee\xe0\x1e\x2f\x7e\x5f\x9d\x1a\xb6\xfb\xe6\xff\xa7\x1d\x8b\ +\xf7\x5a\x11\x11\xf1\xd5\x6f\x16\x4e\xf5\x1b\x77\x37\x92\x78\xf9\ +\x24\x19\x1b\xb0\xde\x7a\xd4\x88\x42\x65\x74\x48\x3c\xd8\x2c\xc5\ +\xce\x67\xc9\xd8\xa0\x36\xed\xf7\x0c\x5f\x3f\xd8\x2c\xc5\xd6\x1f\ +\xca\x63\x7f\x6f\x6d\xbd\x19\x49\x2d\x8d\xd6\x4e\x21\xfe\xf3\x61\ +\xf5\xcc\x7d\x92\x4f\xb2\xa8\x3d\x6c\xc5\xdc\x5a\xfb\xe4\xc5\xfb\ +\xbd\xfe\xd8\x79\x52\x1e\x1b\x50\x4f\xd3\xa7\x7d\xc3\x6d\x7a\x96\ +\xcf\x1d\xff\xfc\xf0\x38\x38\x8b\xe3\x63\x66\x5a\x5f\x0f\x7f\xc7\ +\xbf\x7f\x37\x3f\x36\xfc\x9e\x76\x5c\x26\x2b\xdd\x41\x7f\x9d\xb0\ +\xde\x6b\xeb\xad\x3f\x55\x26\x86\xec\x7c\x92\xc5\xad\xc7\x8d\x73\ +\xdd\x03\x00\x00\x10\xd0\x07\xd2\x46\x3e\xd2\xc6\xd1\x00\x51\x5a\ +\xe8\x46\xae\x98\x45\x52\x4b\xe3\xd6\xfb\xf5\xd8\xfa\xb4\x12\x07\ +\x5f\x16\xaf\x5c\x23\xcd\xdd\xed\xc4\xca\x7b\x8d\xc1\x9f\xdb\x7b\ +\xbd\x59\xf3\x42\x35\x8b\xd2\x42\x37\x0a\x95\x6e\xcc\xad\xb5\xa3\ +\xb4\x98\xc6\xf3\x8f\xaa\xa7\x9e\xe5\x1f\xfb\x7d\x6b\xed\x38\xb8\ +\x53\x3c\xf5\xcb\x88\xb3\x86\xf3\xd5\x5f\xd4\xa3\xb4\xd0\x1d\xf4\ +\x4b\x73\xbb\x10\x59\x27\x7a\xb3\xe9\x0b\xdd\x48\x6a\x69\xac\x3e\ +\xae\xc7\xf3\x8f\xab\x13\x67\x91\x47\xf5\xe9\x71\x9d\x7a\xfe\xc8\ +\x8b\x98\xe3\x0a\x95\x6c\xf0\xd2\x63\xd4\xf5\xfe\xe7\xbb\xed\xdc\ +\xc8\xeb\xfd\x31\x94\x75\x72\xd1\xde\x3b\xf9\x5b\xbb\xed\xd9\xfb\ +\x62\xe5\xa7\x8d\x99\x5e\x80\x0c\x87\xf3\xd5\xc7\xf5\xc8\xf5\x57\ +\x5b\x8c\x18\x37\x49\x2d\x8d\x3b\x1f\x1c\xc4\xb3\x0f\xe7\x46\x06\ +\xec\x7c\x92\xc5\xda\xaf\x0e\x8e\xdc\xa3\xbd\xdb\x6b\x87\xd2\xe2\ +\x61\x7f\x4d\xba\x07\x00\x00\x20\xa0\x1f\x71\xf0\x75\x71\xe4\x8c\ +\xe5\x70\xf8\x5d\x7e\xb7\x19\x8d\x67\x85\x73\x07\xdc\x0b\x6d\xa0\ +\x85\x2c\x96\xdf\x6d\x46\x44\x44\xd6\xc9\xc5\x37\x7f\xac\x9c\x08\ +\xce\x2b\x3f\x6f\xf6\x02\xfa\x42\x37\x6a\x0f\x5b\x13\x67\xbf\x4f\ +\x6b\xf9\x61\x33\x36\xff\x6f\xee\xc2\x9f\x67\xf1\xed\xf6\x20\x9c\ +\x8f\x9a\xa9\xaf\xdc\xe9\xbd\x2c\xc9\x15\xb3\x58\xfe\x49\x23\x9e\ +\xfd\x6e\xee\xcc\x7d\x3a\xce\xa8\xb0\xbb\xb4\xde\x1a\xac\x1c\x98\ +\x14\x86\x5b\x5b\xf9\x91\xd7\x6f\x7f\x50\x8f\xa4\x96\x46\x7b\x2f\ +\x7f\xae\x30\x3d\x32\x60\xd7\xd2\x98\xbf\xd7\x89\xfd\x8d\xb3\xff\ +\x33\xc9\x27\xd9\x20\x9c\x67\x9d\x5c\x6c\x7f\x5e\x3e\xf1\xf2\x69\ +\xf9\xbd\x66\xcc\xdf\x6d\x47\xae\x98\x45\xed\xc7\xcd\x78\xf1\xff\ +\x95\x13\xf7\xa9\x3d\x6c\x0d\xc2\xf9\xa8\x59\xff\xf9\x7b\x9d\x58\ +\x5e\x6f\x44\xae\x98\x9d\xfb\x85\x02\x00\x00\xf0\x7a\xbd\xf6\x43\ +\xe2\x0e\xbe\x2c\xc6\xc1\x66\x29\x22\x22\x72\xc5\x6c\xa6\x7d\xca\ +\x97\x69\xf1\x47\x87\x01\x69\xfb\xf3\xf2\xc8\x59\xed\x9d\xcf\x0e\ +\x43\x53\x75\xb5\x73\x21\xdf\x5b\xa8\x74\x63\x69\xbd\x75\xe1\xcf\ +\x53\x5e\x39\x6c\xdf\x57\x7f\x3d\x19\xae\x1b\xcf\x0a\x83\x99\xea\ +\xd2\x42\x37\x92\x95\xee\x8d\xfe\x07\x52\x7b\xd0\x8c\x7c\x92\xcd\ +\xf4\x22\xa4\x3f\x6e\x5e\x6d\x24\x23\x57\x86\xbc\xfc\x4b\x12\x59\ +\xe7\xdb\xfd\xf9\xb7\x46\x8f\x9b\xfe\x78\xca\x3a\xb9\xd8\xfd\xa2\ +\x74\xe2\xfa\xfe\x46\x31\xd2\x46\x7e\xf0\x42\x61\x96\xdf\x0a\x00\ +\x00\x08\xe8\x47\x42\x61\x5f\xff\xc0\xb2\xab\xa2\xb2\xda\x0b\xb4\ +\xed\xbd\xfc\xd8\xe5\xf7\xdd\x56\xee\xc8\x4b\x86\xf3\xea\x07\xae\ +\xc5\x7b\xad\x28\x2e\x5c\x4e\xe0\x4a\x1b\xe3\x0f\xc4\xdb\xff\xd7\ +\x61\x10\xcc\x97\x6e\x66\xe0\xeb\xf7\x41\xae\xd8\xdb\xaf\x7f\xe6\ +\x71\x73\xbb\x33\xb8\xcf\xee\xd3\xd2\xd8\x71\xb3\xf7\xcf\xc3\x6b\ +\xa3\x5e\x4e\xf5\xc7\x53\xa7\x91\x1b\xbb\xb2\x64\xf8\x1e\xb3\x1c\ +\xa4\x07\x00\x00\x5c\x0d\x45\x4d\x30\xd9\x60\x7f\xf4\xf6\xe4\xfd\ +\xe0\x5b\x7f\x28\xc7\x56\x94\x2f\xe4\x3b\x5f\xfe\x35\x89\xe5\x77\ +\x9b\x97\xb2\x6c\xb9\xfb\xed\x8c\x6d\xa1\xd2\x1d\xbb\x7c\x7b\x7f\ +\xa3\x18\xfb\x1b\x0b\x37\xba\xdf\xd3\x46\x2e\x0e\xbe\x4e\x62\xf1\ +\x5e\x6b\xa6\x33\x01\xfa\xdb\x08\x9a\x53\xc6\xcd\xcb\x27\xc9\xc4\ +\x6d\x02\x69\x23\x1f\x85\x4a\x37\x4a\x0b\xdd\xa8\xdc\x49\x47\xfe\ +\x86\xdd\xa7\xa5\xb1\x2f\x01\x00\x00\x80\xeb\xe3\x4a\xcc\xa0\x0f\ +\xcf\x1c\x0e\x1f\x2a\x76\x95\x9c\xe7\xb0\xb1\x33\x7f\x57\x2b\x17\ +\x3b\x4f\x7b\x61\xbf\xbf\x0f\xfa\xa2\x1c\x7c\x75\x18\xc8\x6b\x0f\ +\x9a\xb1\xb4\x7e\x79\xb3\xf4\xd7\xdd\xee\x17\xa5\xc1\x4c\xfa\xd2\ +\x3b\xcd\xd9\x82\x7e\xfd\x7c\xe3\x66\x78\x76\xfc\xad\x9f\x35\x62\ +\x69\xbd\x65\x19\x3b\x00\x00\xbc\xa1\x5e\xfb\x0c\x7a\xe5\x4e\x7a\ +\x64\x9f\x6d\xfd\xab\x82\x5e\x89\xde\x2c\xf6\xfc\xf7\x0b\x91\xd4\ +\xd2\xa8\x3d\x68\x46\xfd\xab\x8b\x39\x3c\xef\xe0\xcb\x62\xe4\x4a\ +\x95\xc1\xc1\x62\x8b\xf7\x7a\x87\xb4\xf5\x4f\x18\xdf\xff\x67\xe9\ +\xd4\xb5\xd5\xfb\x9f\x1d\xe7\x2c\x65\xf1\xae\xa2\x6e\x2b\x17\xdb\ +\x9f\x95\xe3\xd6\xfb\xbd\x53\xef\x97\xd6\x5b\x67\x3a\x14\xef\x42\ +\x5e\x12\x3c\x2d\x45\xb1\xda\x1d\x1c\x26\xd7\x6f\xf3\xd6\x4e\x21\ +\x9a\x5b\x85\xd8\xff\x47\xc9\xc9\xed\x00\x00\x20\xa0\x9f\xcd\xdc\ +\xf7\x3a\x47\x0e\x28\x8b\x38\x2c\x91\xd5\xb7\xf3\xb4\x7c\xa5\x4e\ +\x70\x9f\xe6\xf6\x07\xa3\xeb\x72\x4f\xaa\x21\x7e\x16\x5b\x7f\xaa\ +\xc4\x77\x7f\xb9\x3f\xd8\x07\x7d\x11\xa7\xc3\xf7\xc3\x7f\xba\x5f\ +\x8d\xef\xdc\x6f\x0d\x6a\x6b\x97\x16\x7a\xcb\xa8\xe7\xef\xb6\xa7\ +\xd6\x41\xbf\x49\x1a\xcf\x0a\xd1\x78\x51\x8c\xca\xad\x4e\x2c\xfc\ +\xb0\x7d\x21\x81\xf8\xac\xe3\x66\xfb\xd3\x72\xd4\x37\x8b\x47\xfa\ +\x2b\xa9\xa5\x91\xd4\xd2\x41\x58\x57\x07\x1d\x00\x00\x04\xf4\x53\ +\x2b\x54\xba\x51\xa8\x8c\xbe\xd6\xda\x29\xc4\xab\xbf\x25\xd7\x6e\ +\xb6\xb5\x1f\x96\x8e\xbb\xa8\x83\xd5\x3a\x7b\xb9\xd8\xdd\x98\x7d\ +\x1f\xf4\xd4\xe0\xf9\xac\x1a\xc5\x85\x2c\xaa\xdf\xef\xbd\x3c\x29\ +\xd7\xd2\x41\x5d\xfa\xd3\xd4\x41\x3f\xd8\x2c\x4d\xac\x5b\xdf\xda\ +\x7e\x33\x02\xfe\xd6\x27\xe5\x58\xfb\x55\x7a\x61\x67\x02\xcc\x32\ +\x6e\x46\xf5\x57\xff\xe4\xf7\x7e\x1d\xf4\x69\xfd\x05\x00\x00\x08\ +\xe8\x13\xc3\x5c\x6b\x3b\x7f\xad\x66\xcd\x87\xed\x6e\x1c\x2e\x77\ +\x2e\x54\xb3\x98\x5b\x6b\x5f\xfc\x77\x7c\x51\x8a\xb9\xef\x75\xa2\ +\x50\xe9\x5e\x4a\x6d\xf4\xce\x5e\xae\x77\xc8\x58\x94\x22\x9f\x64\ +\xb1\xf8\x76\x3b\x16\xef\xf5\x4a\xcb\xad\x3e\xae\xc7\xe6\x6f\xe7\ +\xc6\xf6\x4f\x5a\xcf\x5d\xeb\x25\xec\xa7\xd5\x6d\xe5\xe2\xd5\x46\ +\x12\x4b\xf7\x9b\x91\xd4\xd2\x98\xbb\xdb\x39\xd7\x98\x3d\xcf\xb8\ +\x39\xde\x5f\x4b\xef\xb4\x06\xcb\xdf\xdf\x7a\xd4\xb8\xf0\xf1\x01\ +\x00\x00\xbc\x81\x01\xfd\xba\x87\xb9\x42\xf5\xe4\xec\xe6\xf0\x7e\ +\xe4\xca\x9d\xf4\x52\x02\xfa\xf0\x3e\xe8\xcb\xaa\x8d\x3e\xfc\x5d\ +\x2f\x9f\x24\x51\x5e\xe9\x2d\x9f\xee\xd7\xa5\x9f\x34\x4b\x7e\x53\ +\xec\x3e\x2d\x45\xf5\x76\x27\x92\x5a\x1a\xcb\xef\x36\x63\xfb\xf3\ +\xd3\x6d\x37\x18\x55\x36\xf0\xa2\xc6\x4d\xb7\x95\x8b\xed\x4f\xcb\ +\x51\x59\x4d\xbf\x5d\xa1\xd2\xab\x5b\x6f\x16\x1d\x00\x00\xae\xa7\ +\x1b\xf1\x3f\xf9\x51\xf5\xa5\x07\x0d\x30\xe5\x44\xec\xf6\x5e\xaf\ +\x89\xaa\xab\x9d\xd7\x76\x7a\x76\x7f\x1f\x74\x44\xef\x60\xb6\x42\ +\x25\x9b\xb9\x1d\x7e\xf0\xeb\xbd\xf8\xc1\xaf\xf7\x26\xb6\xc9\xde\ +\xdf\x4b\x13\x03\xe6\x4d\xb5\xf3\xa4\x17\xca\x73\xc5\x2c\x16\x7f\ +\x34\xf9\x45\x49\x6b\xa7\xf7\x32\xaa\x5c\x4b\x67\x1e\x37\x4b\xeb\ +\xad\x41\x7f\x4d\xba\xc7\xf0\x49\xef\x37\xb5\x6e\x3d\x00\x00\x08\ +\xe8\x57\x58\x7d\xf3\x70\xd6\x77\xee\xee\xf8\x32\x65\x8b\x6f\x1f\ +\xce\x5e\x8e\x2a\xf1\x76\xf0\x75\x69\x10\xca\x96\xde\x69\xbd\xb6\ +\xe7\xd9\xfa\xa4\x1c\xd9\x50\x0d\xf3\x59\x0c\x97\x8a\x2b\xdf\x4a\ +\x27\x06\xf9\x49\x6d\x72\x53\xb5\xb6\xf2\x83\xe5\xe9\xfd\x3a\xe7\ +\xe3\xec\xff\xeb\x70\xdc\xd4\x1e\xce\x36\x6e\xda\xaf\x0e\xdb\x7e\ +\xfe\x7f\xc7\x8f\xe1\xea\xed\x8e\xce\x01\x00\x00\x01\xfd\x6a\x87\ +\xa9\xfe\xec\xf7\xdc\x5a\x7b\xe4\xd2\xf0\xc5\x07\xed\x41\x99\xb0\ +\xb4\x91\x1f\x59\xe2\x6d\xf7\x69\x69\x70\x9f\xf9\xbb\xed\x58\x7e\ +\xaf\x79\x62\x36\xb3\x72\x27\x8d\xe5\x87\xcd\x4b\x7d\x9e\xfe\x3e\ +\xe8\xf3\xb6\x49\x7f\x66\x77\xf1\x5e\x6b\x64\x4d\xed\xe1\x25\xd7\ +\xca\xde\x9d\x34\x5c\x1b\x7d\x62\x40\xdf\x28\x1e\x19\x7f\x2b\x3f\ +\x3f\x39\x6e\x92\x95\xee\xc4\xfa\xea\x07\x5f\x16\x0f\xeb\xb0\xdf\ +\x6f\xc6\xe2\x83\xf6\xc8\x31\xdc\x3f\x74\x2e\xeb\xdc\x8c\x33\x01\ +\x00\x00\xe0\x4d\x75\xad\x36\x17\xdf\x7a\xbf\x3e\x39\x3c\x6d\x24\ +\x47\xf6\xf7\x6e\xff\xb9\x12\xab\x8f\xeb\x83\xfa\xd1\x0b\x3f\x6c\ +\x0f\x42\xd3\xf0\x49\xda\x59\x27\x17\xdf\x7c\x52\x19\x7b\xf0\xd7\ +\xf3\x8f\xaa\xb1\xfa\x8b\xfa\xa0\x0c\x59\xbf\x14\x59\xae\x98\x9d\ +\x98\x49\x3d\xd8\x2c\x5d\x5a\x48\x1a\xde\x07\x3d\xab\x17\x1f\x57\ +\x06\xcf\x32\x5c\x53\x3b\x22\xa2\x50\xc9\x8e\xcc\xce\x4f\x2b\x7b\ +\x37\xad\x0e\xfa\xa8\x3e\xb9\xee\x86\xcf\x04\x98\x66\x78\xdc\xcc\ +\xad\xb5\x63\x6e\xed\xec\xe3\xe6\x9b\x4f\x0e\xc7\xf0\xd2\xfd\x66\ +\x2c\xdd\x6f\x0e\xfa\x6b\xb8\x4c\x61\xd6\xc9\x9d\x7a\x5f\x3c\x00\ +\x00\x70\x35\xbd\xd1\xeb\x97\x5b\x5b\xf9\xd8\xfc\xed\x5c\x1c\x6c\ +\x1e\x2e\x37\xee\xd7\x8f\x1e\x0e\x46\xcf\x3e\x9c\x9b\x78\xb0\x56\ +\xb7\x95\x8b\xe7\x1f\x55\x63\x77\x23\x19\x2c\x33\x4f\x6a\xe9\x91\ +\x90\xd5\xde\xcb\xc7\xd6\xa7\x95\x0b\xab\x55\x3e\x4e\x7f\x1f\xf4\ +\x79\x02\xe6\xa8\x67\x49\x6a\xe9\x20\x9c\xb7\x76\x0a\xf1\xe2\xf7\ +\xd5\xd8\xdf\x70\x38\xdc\x28\xc3\x67\x02\xcc\xd2\xd6\xc7\xc7\xcd\ +\xf6\x93\xf1\xe3\xa6\x3f\x86\x47\xf5\x57\x3f\x9c\x37\x5e\x14\xe3\ +\xf9\xc7\x55\x87\xf9\x01\x00\xc0\x35\x97\x7b\xf4\xe8\xd1\x8d\x39\ +\x55\xea\xf8\xc1\x68\xb3\xce\x74\x1f\xbf\xcf\x75\x2e\x15\x97\xac\ +\x74\x8f\x1c\x2c\x76\x9d\x9f\xe5\xba\xb5\x75\x67\x3f\x1f\x9d\xbd\ +\xdc\x1b\x3f\xf6\xea\xf5\xba\xce\x07\x00\x80\x53\xb8\x51\x53\x6e\ +\x17\xb5\xf4\xfc\x4d\xda\xe7\xab\x24\xd7\xf5\x6a\x6b\x7b\xcc\x01\ +\x00\xe0\xcd\x25\x9d\x01\x00\x00\x80\x80\x0e\x00\x00\x00\x08\xe8\ +\x00\x00\x00\x20\xa0\x03\x00\x00\x00\x02\x3a\x00\x00\x00\x08\xe8\ +\x00\x00\x00\x80\x80\x0e\x00\x00\x00\x02\x3a\x00\x00\x00\x20\xa0\ +\x03\x00\x00\x80\x80\x0e\x00\x00\x00\x08\xe8\x00\x00\x00\x20\xa0\ +\x03\x00\x00\x00\x02\x3a\x00\x00\x00\x08\xe8\x00\x00\x00\x80\x80\ +\x0e\x00\x00\x00\x02\x3a\x00\x00\x00\x20\xa0\x03\x00\x00\x80\x80\ +\x0e\x00\x00\x00\x08\xe8\x00\x00\x00\x20\xa0\x03\x00\x00\x00\x02\ +\x3a\x00\x00\x00\x5c\x23\xff\x05\x00\x00\xff\xff\x03\x00\x78\x29\ +\xcd\x85\x61\x95\xed\x47\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\ +\x60\x82\ +" + +qt_resource_name = b"\ +\x00\x03\ +\x00\x00\x78\xc3\ +\x00\x72\ +\x00\x65\x00\x73\ +\x00\x04\ +\x00\x06\xfa\x5e\ +\x00\x69\ +\x00\x63\x00\x6f\x00\x6e\ +\x00\x11\ +\x01\xb7\x20\x27\ +\x00\x70\ +\x00\x69\x00\x70\x00\x65\x00\x6c\x00\x69\x00\x6e\x00\x65\x00\x5f\x00\x6c\x00\x6f\x00\x67\x00\x6f\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\ +\x00\x08\ +\x0a\x61\x5a\xa7\ +\x00\x69\ +\x00\x63\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ +\x00\x1c\ +\x08\x07\xe3\xc7\ +\x00\x70\ +\x00\x6c\x00\x75\x00\x67\x00\x69\x00\x6e\x00\x5f\x00\x63\x00\x6f\x00\x6e\x00\x66\x00\x69\x00\x67\x00\x75\x00\x72\x00\x61\x00\x74\ +\x00\x6f\x00\x72\x00\x5f\x00\x6c\x00\x6f\x00\x67\x00\x6f\x00\x2e\x00\x70\x00\x6e\x00\x67\ +" + +qt_resource_struct = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x06\ +\x00\x00\x00\x0c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x04\ +\x00\x00\x00\x1a\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x00\x42\x00\x00\x00\x00\x00\x01\x00\x00\x17\xe5\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x07\ +\x00\x00\x00\x58\x00\x00\x00\x00\x00\x01\x00\x00\x28\xe7\ +" + +def qInitResources(): + QtCore.qRegisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(0x01, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources() diff --git a/RenderPipelineFile/toolkit/poisson_disk_generator/.gitignore b/RenderPipelineFile/toolkit/poisson_disk_generator/.gitignore index a2c97840..197e239b 100644 --- a/RenderPipelineFile/toolkit/poisson_disk_generator/.gitignore +++ b/RenderPipelineFile/toolkit/poisson_disk_generator/.gitignore @@ -1,19 +1,19 @@ - -# Prefab gitignore which can be used by the user to ignore the files generated -# by the module builder - -*.pyd -*.pyc -*.so -*.pdb - -# Various output names -win_*/ -linux_*/ -source/interrogate* - -# Source and script files -scripts/ -CMakeLists.txt -build.py -LICENSE + +# Prefab gitignore which can be used by the user to ignore the files generated +# by the module builder + +*.pyd +*.pyc +*.so +*.pdb + +# Various output names +win_*/ +linux_*/ +source/interrogate* + +# Source and script files +scripts/ +CMakeLists.txt +build.py +LICENSE diff --git a/RenderPipelineFile/toolkit/poisson_disk_generator/config.ini b/RenderPipelineFile/toolkit/poisson_disk_generator/config.ini index f219245a..8bd36d56 100644 --- a/RenderPipelineFile/toolkit/poisson_disk_generator/config.ini +++ b/RenderPipelineFile/toolkit/poisson_disk_generator/config.ini @@ -1,5 +1,5 @@ -generate_pdb=0 -module_name=PoissonDiskGenerator -use_lib_eigen=0 -use_lib_freetype=0 -vc_version=Visual Studio 10 2010 +generate_pdb=0 +module_name=PoissonDiskGenerator +use_lib_eigen=0 +use_lib_freetype=0 +vc_version=Visual Studio 10 2010 diff --git a/RenderPipelineFile/toolkit/poisson_disk_generator/generate_poisson_disk.py b/RenderPipelineFile/toolkit/poisson_disk_generator/generate_poisson_disk.py index e97e8814..183ffab5 100644 --- a/RenderPipelineFile/toolkit/poisson_disk_generator/generate_poisson_disk.py +++ b/RenderPipelineFile/toolkit/poisson_disk_generator/generate_poisson_disk.py @@ -1,36 +1,36 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -import PoissonDiskGenerator - - -# XXX: Read options from stdin -sample_count = 64 -evaluations = 15000 -num_tries = 400 -is_3d = False - -PoissonDiskGenerator.PDGenerator(sample_count, evaluations, num_tries, is_3d) +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +import PoissonDiskGenerator + + +# XXX: Read options from stdin +sample_count = 64 +evaluations = 15000 +num_tries = 400 +is_3d = False + +PoissonDiskGenerator.PDGenerator(sample_count, evaluations, num_tries, is_3d) diff --git a/RenderPipelineFile/toolkit/poisson_disk_generator/source/PoissonDiskGenerator.h b/RenderPipelineFile/toolkit/poisson_disk_generator/source/PoissonDiskGenerator.h index f9dbd869..6adf6986 100644 --- a/RenderPipelineFile/toolkit/poisson_disk_generator/source/PoissonDiskGenerator.h +++ b/RenderPipelineFile/toolkit/poisson_disk_generator/source/PoissonDiskGenerator.h @@ -1,198 +1,198 @@ - -#include "pandabase.h" -#include "pnmImage.h" - -#include -#include -#include -#include -#include -#include -#include - -using namespace std; - -#define TWO_PI 6.28318530718f -#define square(x) ((x) * (x)) - -struct Vec { - float x, y, z; - inline float length_sq() const { - return x*x + y*y + z*z; - } - inline float dist_sq(const Vec& other) const { - return square(x - other.x) + square(y - other.y) + square(z - other.z); - } - - inline float angle_signed() const { - return atan2(y, x); - } - - friend inline ostream& operator<<(ostream& stream, const Vec& v) { - return stream << "Vec(" << v.x << ", " << v.y << ", " << v.z << ")"; - } -}; - -int compare_vec(const void * a, const void * b) -{ - return ((Vec*)a)->angle_signed() - ((Vec*)b)->angle_signed(); -} - -class PDGenerator -{ - -PUBLISHED: - - PDGenerator(int sample_count, int evaluations, int num_tries, int is_3d_) - { - is_3d = is_3d_; - rnd_device = new random_device(); - rnd_generator = new mt19937(rnd_device->operator()()); - rng = new uniform_real_distribution(-1.0f, 1.0f); - randvec = is_3d ? &PDGenerator::randvec_3d : &PDGenerator::randvec_2d; - - cout << "Sample count = " << sample_count << endl; - cout << "Evaluations = " << evaluations << endl; - cout << "Num tries = " << num_tries << endl; - - Vec* sample_points = new Vec[sample_count]; - Vec* best_points = new Vec[sample_count]; - - float best_score = -100000.0; - - for (size_t try_count = 0; try_count < num_tries; ++try_count) { - // Generate the initial sample point - sample_points[0] = randvec(this); - - // Fill the points - for (size_t i = 1; i < sample_count; ++i) { - Vec best_point; - float best_point_min_dist = 0.0f; - - // Find the point which has the greatest minimum distance to all current points - for (size_t k = 0; k < evaluations; ++k) { - Vec point = randvec(this); - float min_dist = 10000.0; - - // Get the minimum distance to all current points - for (size_t j = 0; j < i; ++j) { - min_dist = min(min_dist, point.dist_sq(sample_points[j])); - } - - if (min_dist > best_point_min_dist) { - best_point = point; - best_point_min_dist = min_dist; - } - } - - sample_points[i] = best_point; - } - - // Rate the points - float score = 0.0; - - // Check for all points - for (size_t i = 0; i < sample_count; ++i) { - - // Find minimum distance to all other points - // float mindist = 10000.0; - float mindist = 0.0; - Vec point = sample_points[i]; - for (size_t j = 0; j < sample_count; ++j) { - if (j == i) continue; - // mindist = min(mindist, point.dist_sq(sample_points[j])); - mindist += point.dist_sq(sample_points[j]); - } - score += mindist * mindist; - } - - if (try_count % 20 == 0) { - cout << "Try " << try_count << " has a score of " << score << endl; - } - - if (score >= best_score) { - memcpy(best_points, sample_points, sizeof(Vec) * sample_count); - best_score = score; - } - - } - - // Sort points based on their angle - qsort(best_points, sample_count, sizeof(Vec), compare_vec); - - // write results - string datatype = is_3d ? "vec3" : "vec2"; - ofstream output("disk.txt"); - - output << "CONST_ARRAY " << datatype << " poisson_disk_" << (is_3d ? "3D" : "2D") - << "_" << sample_count << "[" << sample_count << "] = " << datatype << "[](" << endl; - - // write out all points - for (size_t i = 0; i < sample_count; ++i) { - Vec v = best_points[i]; - output << " " << datatype << "(" << v.x << ", " << v.y; - if (is_3d) { - output << ", " << v.z; - } - output << ")" << (i == sample_count - 1 ? "" : ",") << endl; - } - - output << ");" << endl; - - output.close(); - - // Plot the points - PNMImage dest(256, 256); - - for (float rad = 0.0f; rad < TWO_PI; rad += 0.005f) { - size_t x = int(sin(rad) * 66.0f) + 127; - size_t y = int(cos(rad) * 66.0f) + 127; - if ( int(rad / TWO_PI * 64.0f) % 2 == 0) { - dest.set_xel(x, y, 0, 0.1, 0.1); - } - } - - for (size_t i = 0; i < sample_count; ++i) { - Vec point = best_points[i]; - int px = int(point.x * 64.0) + 127; - int py = int(point.y * 64.0) + 127; - float f = float(i / float(sample_count)) * 0.8 + 0.2; - dest.set_xel(px, py, f, f, f); - } - - dest.write("result.png"); - } - - inline float frand() { - return rng->operator()(*rnd_generator); - } - - inline Vec randvec_3d() { - float r = frand(); - - // this is so wrong that its almost correct again - float phi = frand() * TWO_PI * 0.5; - float theta = (frand() * 0.5 + 0.5) * TWO_PI; - float sin_phi = sin(phi); - Vec v = { sin_phi * cos(theta) * r, sin_phi * sin(theta) * r, - cos(phi) * r }; - return v; - } - - inline Vec randvec_2d() { - float r = frand() * 0.5 + 0.5; - float phi = (frand() * 0.5 + 0.5) * TWO_PI; - Vec v = { cos(phi) * r, sin(phi) * r, 0.0 }; - return v; - } - - bool is_3d; - -#ifndef INTERROGATE - std::function randvec; - std::random_device* rnd_device; - mt19937* rnd_generator; - uniform_real_distribution* rng; -#endif - -}; + +#include "pandabase.h" +#include "pnmImage.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +#define TWO_PI 6.28318530718f +#define square(x) ((x) * (x)) + +struct Vec { + float x, y, z; + inline float length_sq() const { + return x*x + y*y + z*z; + } + inline float dist_sq(const Vec& other) const { + return square(x - other.x) + square(y - other.y) + square(z - other.z); + } + + inline float angle_signed() const { + return atan2(y, x); + } + + friend inline ostream& operator<<(ostream& stream, const Vec& v) { + return stream << "Vec(" << v.x << ", " << v.y << ", " << v.z << ")"; + } +}; + +int compare_vec(const void * a, const void * b) +{ + return ((Vec*)a)->angle_signed() - ((Vec*)b)->angle_signed(); +} + +class PDGenerator +{ + +PUBLISHED: + + PDGenerator(int sample_count, int evaluations, int num_tries, int is_3d_) + { + is_3d = is_3d_; + rnd_device = new random_device(); + rnd_generator = new mt19937(rnd_device->operator()()); + rng = new uniform_real_distribution(-1.0f, 1.0f); + randvec = is_3d ? &PDGenerator::randvec_3d : &PDGenerator::randvec_2d; + + cout << "Sample count = " << sample_count << endl; + cout << "Evaluations = " << evaluations << endl; + cout << "Num tries = " << num_tries << endl; + + Vec* sample_points = new Vec[sample_count]; + Vec* best_points = new Vec[sample_count]; + + float best_score = -100000.0; + + for (size_t try_count = 0; try_count < num_tries; ++try_count) { + // Generate the initial sample point + sample_points[0] = randvec(this); + + // Fill the points + for (size_t i = 1; i < sample_count; ++i) { + Vec best_point; + float best_point_min_dist = 0.0f; + + // Find the point which has the greatest minimum distance to all current points + for (size_t k = 0; k < evaluations; ++k) { + Vec point = randvec(this); + float min_dist = 10000.0; + + // Get the minimum distance to all current points + for (size_t j = 0; j < i; ++j) { + min_dist = min(min_dist, point.dist_sq(sample_points[j])); + } + + if (min_dist > best_point_min_dist) { + best_point = point; + best_point_min_dist = min_dist; + } + } + + sample_points[i] = best_point; + } + + // Rate the points + float score = 0.0; + + // Check for all points + for (size_t i = 0; i < sample_count; ++i) { + + // Find minimum distance to all other points + // float mindist = 10000.0; + float mindist = 0.0; + Vec point = sample_points[i]; + for (size_t j = 0; j < sample_count; ++j) { + if (j == i) continue; + // mindist = min(mindist, point.dist_sq(sample_points[j])); + mindist += point.dist_sq(sample_points[j]); + } + score += mindist * mindist; + } + + if (try_count % 20 == 0) { + cout << "Try " << try_count << " has a score of " << score << endl; + } + + if (score >= best_score) { + memcpy(best_points, sample_points, sizeof(Vec) * sample_count); + best_score = score; + } + + } + + // Sort points based on their angle + qsort(best_points, sample_count, sizeof(Vec), compare_vec); + + // write results + string datatype = is_3d ? "vec3" : "vec2"; + ofstream output("disk.txt"); + + output << "CONST_ARRAY " << datatype << " poisson_disk_" << (is_3d ? "3D" : "2D") + << "_" << sample_count << "[" << sample_count << "] = " << datatype << "[](" << endl; + + // write out all points + for (size_t i = 0; i < sample_count; ++i) { + Vec v = best_points[i]; + output << " " << datatype << "(" << v.x << ", " << v.y; + if (is_3d) { + output << ", " << v.z; + } + output << ")" << (i == sample_count - 1 ? "" : ",") << endl; + } + + output << ");" << endl; + + output.close(); + + // Plot the points + PNMImage dest(256, 256); + + for (float rad = 0.0f; rad < TWO_PI; rad += 0.005f) { + size_t x = int(sin(rad) * 66.0f) + 127; + size_t y = int(cos(rad) * 66.0f) + 127; + if ( int(rad / TWO_PI * 64.0f) % 2 == 0) { + dest.set_xel(x, y, 0, 0.1, 0.1); + } + } + + for (size_t i = 0; i < sample_count; ++i) { + Vec point = best_points[i]; + int px = int(point.x * 64.0) + 127; + int py = int(point.y * 64.0) + 127; + float f = float(i / float(sample_count)) * 0.8 + 0.2; + dest.set_xel(px, py, f, f, f); + } + + dest.write("result.png"); + } + + inline float frand() { + return rng->operator()(*rnd_generator); + } + + inline Vec randvec_3d() { + float r = frand(); + + // this is so wrong that its almost correct again + float phi = frand() * TWO_PI * 0.5; + float theta = (frand() * 0.5 + 0.5) * TWO_PI; + float sin_phi = sin(phi); + Vec v = { sin_phi * cos(theta) * r, sin_phi * sin(theta) * r, + cos(phi) * r }; + return v; + } + + inline Vec randvec_2d() { + float r = frand() * 0.5 + 0.5; + float phi = (frand() * 0.5 + 0.5) * TWO_PI; + Vec v = { cos(phi) * r, sin(phi) * r, 0.0 }; + return v; + } + + bool is_3d; + +#ifndef INTERROGATE + std::function randvec; + std::random_device* rnd_device; + mt19937* rnd_generator; + uniform_real_distribution* rng; +#endif + +}; diff --git a/RenderPipelineFile/toolkit/poisson_disk_generator/update_module_builder.py b/RenderPipelineFile/toolkit/poisson_disk_generator/update_module_builder.py index a1d84a20..6874a7ab 100644 --- a/RenderPipelineFile/toolkit/poisson_disk_generator/update_module_builder.py +++ b/RenderPipelineFile/toolkit/poisson_disk_generator/update_module_builder.py @@ -1,57 +1,57 @@ -""" - -RenderPipeline - -Copyright (c) 2014-2016 tobspr - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -""" - -# This script downloads and updates the module builder. - -# pylint: skip-file - -if __name__ == "__main__": - import os - import sys - from os.path import dirname, realpath, isfile - - # Files which are skipped from the downloaded zip - files_to_ignore = "__init__.py LICENSE README.md config.ini".split() - - current_dir = dirname(realpath(__file__)) - os.chdir(current_dir) - - # Import thirdparty packages - sys.path.insert(0, "../../rpcore/util") - sys.path.insert(0, "../../") - - # Download the module - from submodule_downloader import download_submodule - download_submodule("tobspr", "P3DModuleBuilder", current_dir, files_to_ignore) - - # Make an init script so we can import the code - with open("scripts/__init__.py", "w") as handle: - handle.write("") - - # Update the gitignore using the suggested version from the module builder - if isfile(".gitignore"): - os.remove(".gitignore") - os.rename("prefab.gitignore", ".gitignore") +""" + +RenderPipeline + +Copyright (c) 2014-2016 tobspr + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +""" + +# This script downloads and updates the module builder. + +# pylint: skip-file + +if __name__ == "__main__": + import os + import sys + from os.path import dirname, realpath, isfile + + # Files which are skipped from the downloaded zip + files_to_ignore = "__init__.py LICENSE README.md config.ini".split() + + current_dir = dirname(realpath(__file__)) + os.chdir(current_dir) + + # Import thirdparty packages + sys.path.insert(0, "../../rpcore/util") + sys.path.insert(0, "../../") + + # Download the module + from submodule_downloader import download_submodule + download_submodule("tobspr", "P3DModuleBuilder", current_dir, files_to_ignore) + + # Make an init script so we can import the code + with open("scripts/__init__.py", "w") as handle: + handle.write("") + + # Update the gitignore using the suggested version from the module builder + if isfile(".gitignore"): + os.remove(".gitignore") + os.rename("prefab.gitignore", ".gitignore") diff --git a/RenderPipelineFile/toolkit/render_service/README.md b/RenderPipelineFile/toolkit/render_service/README.md index 2579b313..7782cee4 100644 --- a/RenderPipelineFile/toolkit/render_service/README.md +++ b/RenderPipelineFile/toolkit/render_service/README.md @@ -1,11 +1,11 @@ - -## Render service - -This is a tool to generate previews and renders of materials. -It keeps the render pipeline running in the background, and renders scenes -on demand. - -A render can be triggered over the network. Example code for triggering a render -and waiting for completion can be found in `example_usage.py` - - + +## Render service + +This is a tool to generate previews and renders of materials. +It keeps the render pipeline running in the background, and renders scenes +on demand. + +A render can be triggered over the network. Example code for triggering a render +and waiting for completion can be found in `example_usage.py` + + diff --git a/RenderPipelineFile/toolkit/render_service/config/daytime.yaml b/RenderPipelineFile/toolkit/render_service/config/daytime.yaml index cbea2358..dcab6d12 100644 --- a/RenderPipelineFile/toolkit/render_service/config/daytime.yaml +++ b/RenderPipelineFile/toolkit/render_service/config/daytime.yaml @@ -1,6 +1,6 @@ - -# Render Pipeline Time Of Day Configuration -# Instead of editing this file, prefer to use the Time Of Day Editor -# Any formatting and comments will be lost - -control_points: + +# Render Pipeline Time Of Day Configuration +# Instead of editing this file, prefer to use the Time Of Day Editor +# Any formatting and comments will be lost + +control_points: diff --git a/RenderPipelineFile/toolkit/render_service/config/debugging.yaml b/RenderPipelineFile/toolkit/render_service/config/debugging.yaml index fa9cfc3e..5980b9cd 100644 --- a/RenderPipelineFile/toolkit/render_service/config/debugging.yaml +++ b/RenderPipelineFile/toolkit/render_service/config/debugging.yaml @@ -1,17 +1,17 @@ - -# This file contains all available debug modes. -# You can add or remove debug modes here, aswell as -# disable or enable them, whatever fits to you. - -# NOTICE: This file only has effect if debugging is enabled -# in the pipeline.yaml (settings.pipeline.display_debuger == True) - -render_modes: - - { name: "Diffuse", key: "DIFFUSE" } - - { name: "Roughness", key: "ROUGHNESS" } - - { name: "Specular", key: "SPECULAR" } - - { name: "Normal", key: "NORMAL" } - - { name: "Metallic", key: "METALLIC" } - - { name: "Translucency", key: "TRANSLUCENCY" } - - { name: "PSSM Splits", key: "PSSM_SPLITS", requires: "pssm", cxx_only: true } - - { name: "Ambient Occlusion", key: "OCCLUSION", requires: "ao" } + +# This file contains all available debug modes. +# You can add or remove debug modes here, aswell as +# disable or enable them, whatever fits to you. + +# NOTICE: This file only has effect if debugging is enabled +# in the pipeline.yaml (settings.pipeline.display_debuger == True) + +render_modes: + - { name: "Diffuse", key: "DIFFUSE" } + - { name: "Roughness", key: "ROUGHNESS" } + - { name: "Specular", key: "SPECULAR" } + - { name: "Normal", key: "NORMAL" } + - { name: "Metallic", key: "METALLIC" } + - { name: "Translucency", key: "TRANSLUCENCY" } + - { name: "PSSM Splits", key: "PSSM_SPLITS", requires: "pssm", cxx_only: true } + - { name: "Ambient Occlusion", key: "OCCLUSION", requires: "ao" } diff --git a/RenderPipelineFile/toolkit/render_service/config/panda3d-config.prc b/RenderPipelineFile/toolkit/render_service/config/panda3d-config.prc index dfdb0fe3..b83cc1fa 100644 --- a/RenderPipelineFile/toolkit/render_service/config/panda3d-config.prc +++ b/RenderPipelineFile/toolkit/render_service/config/panda3d-config.prc @@ -1,172 +1,172 @@ - - -# This is the config file used to configure basic settings for Panda3D. -# The pipeline loads it at startup to ensure the environment is setup properly. - -# -------------- Debugging options -------------- - -# gl-dump-compiled-shaders #t -# notify-level-glgsg debug -# notify-level-glgsg warning -# notify-level-gobj debug -pstats-gpu-timing #t -pstats-max-rate 200 -gl-debug #t -gl-debug-object-labels #t -sync-video #f - -# ----------------- Misc Settings ----------------- - -# No stack trace on assertion, set this to true to make panda crash on assertions -# (which will allow to debug it) -assert-abort #t - -# File system should be case sensitive -# NOTICE: Set this to #f if you are using tempfile, since it returns -# wrong cased directory paths -vfs-case-sensitive #t - -# Enable state cache, this seems to actually help the performance by a lot -state-cache #t -transform-cache #t - -# Frame rate meter style -frame-rate-meter-milliseconds #t -frame-rate-meter-update-interval 1.0 -frame-rate-meter-text-pattern %0.2f fps -frame-rate-meter-ms-text-pattern %0.3f ms -frame-rate-meter-layer-sort 1000 -frame-rate-meter-scale 0.036 -frame-rate-meter-side-margins 0.4 -show-frame-rate-meter #f - -# Set text settings -text-minfilter linear -text-magfilter linear -text-page-size 512 512 - -# Better text performance since rdb's patch -text-flatten 0 -text-dynamic-merge 1 - -# For smoother animations -# even-animation #t - -# Threading, really buggy! -#threading-model App/Cull/Draw - -support-stencil #f -framebuffer-stencil #f - -# Don't use srgb correction, we do that ourself -framebuffer-srgb #f - -# Don't use multisamples -framebuffer-multisample #f -multisamples 0 - -# Don't rescale textures which are no power-of-2 -textures-power-2 none - -# This is required, the pipeline does not support resizing yet -win-fixed-size #t - -# Set default texture filters -texture-anisotropic-degree 8 -texture-magfilter linear -texture-minfilter linear -texture-quality-level fastest - -# Enable seamless cubemap filtering, thats important for environment filtering -gl-cube-map-seamless #t - -# Set model cache dir -model-cache-dir $USER_APPDATA/Panda3D-1.9/cache -model-cache-textures #f - -# Disable the annoying SRGB warning from pnmimage -notify-level-pnmimage error - -# Disable the buffer viewer, we have our own -show-buffers #f - -# Use the default coordinate system, this makes our matrix transformations -# faster because we don't have have to transform them to a different coordinate -# system before -gl-coordinate-system default - -# This makes animations smoother, especially if they were exported at 30 FPS -# and are played at 60 FPS -interpolate-frames #t - -# Disable workarround in panda which causes our shadow atlas to take twice -# the amount of vram it should, due to an intel driver bug. -gl-force-fbo-color #f - -# ----------- OpenGL / Performance Settings ------------ - -# Animations on the gpu. The default shader has to get adjusted to support this -# feature before this option can be turned on. -# hardware-animated-vertices #t - -# Try this options for performance -# uniquify-matrix #t -# uniquify-transforms #t -# uniquify-states #t -# uniquify-attribs #f - -# Enable garbarge collection -garbage-collect-states #t -# garbage-collect-states-rate 0.2 - -# Compress textures on the drivers? -# driver-compress-textures #t - -# Faster animations? (Have to test) -# matrix-palette #t -# display-list-animation #t - -# Better GL performance by not using gl-finish and so on -gl-finish #f -gl-force-no-error #t -gl-check-errors #f -gl-force-no-flush #t -gl-force-no-scissor #t - -# Eventually disable memory barriers, have to check if this is faster -gl-enable-memory-barriers #f - -# Disable threading -lock-to-one-cpu #t -support-threads #f - -# Let the driver generate the mipmaps -driver-generate-mipmaps #t -#gl-ignore-mipmaps #t - -# Use immutable texture storage, it is *supposed* to be faster, but might not be -gl-immutable-texture-storage #t - -# auto-flip #f - -# Default window settings -# depth-bits 0 -color-bits 0 - -framebuffer-depth #f - -gl-fixed-vertex-attrib-locations #f - -# Disable the fragment shader performance warning -gl-validate-shaders #f -gl-skip-shader-recompilation-warnings #t - -alpha-scale-via-texture #f -bounds-type best # best/fastest/sphere/box -pstats-name Render Pipeline Stats -rescale-normals #f -screenshot-extension png - -# Required for correct velocity -always-store-prev-transform #t -allow-incomplete-render #t + + +# This is the config file used to configure basic settings for Panda3D. +# The pipeline loads it at startup to ensure the environment is setup properly. + +# -------------- Debugging options -------------- + +# gl-dump-compiled-shaders #t +# notify-level-glgsg debug +# notify-level-glgsg warning +# notify-level-gobj debug +pstats-gpu-timing #t +pstats-max-rate 200 +gl-debug #t +gl-debug-object-labels #t +sync-video #f + +# ----------------- Misc Settings ----------------- + +# No stack trace on assertion, set this to true to make panda crash on assertions +# (which will allow to debug it) +assert-abort #t + +# File system should be case sensitive +# NOTICE: Set this to #f if you are using tempfile, since it returns +# wrong cased directory paths +vfs-case-sensitive #t + +# Enable state cache, this seems to actually help the performance by a lot +state-cache #t +transform-cache #t + +# Frame rate meter style +frame-rate-meter-milliseconds #t +frame-rate-meter-update-interval 1.0 +frame-rate-meter-text-pattern %0.2f fps +frame-rate-meter-ms-text-pattern %0.3f ms +frame-rate-meter-layer-sort 1000 +frame-rate-meter-scale 0.036 +frame-rate-meter-side-margins 0.4 +show-frame-rate-meter #f + +# Set text settings +text-minfilter linear +text-magfilter linear +text-page-size 512 512 + +# Better text performance since rdb's patch +text-flatten 0 +text-dynamic-merge 1 + +# For smoother animations +# even-animation #t + +# Threading, really buggy! +#threading-model App/Cull/Draw + +support-stencil #f +framebuffer-stencil #f + +# Don't use srgb correction, we do that ourself +framebuffer-srgb #f + +# Don't use multisamples +framebuffer-multisample #f +multisamples 0 + +# Don't rescale textures which are no power-of-2 +textures-power-2 none + +# This is required, the pipeline does not support resizing yet +win-fixed-size #t + +# Set default texture filters +texture-anisotropic-degree 8 +texture-magfilter linear +texture-minfilter linear +texture-quality-level fastest + +# Enable seamless cubemap filtering, thats important for environment filtering +gl-cube-map-seamless #t + +# Set model cache dir +model-cache-dir $USER_APPDATA/Panda3D-1.9/cache +model-cache-textures #f + +# Disable the annoying SRGB warning from pnmimage +notify-level-pnmimage error + +# Disable the buffer viewer, we have our own +show-buffers #f + +# Use the default coordinate system, this makes our matrix transformations +# faster because we don't have have to transform them to a different coordinate +# system before +gl-coordinate-system default + +# This makes animations smoother, especially if they were exported at 30 FPS +# and are played at 60 FPS +interpolate-frames #t + +# Disable workarround in panda which causes our shadow atlas to take twice +# the amount of vram it should, due to an intel driver bug. +gl-force-fbo-color #f + +# ----------- OpenGL / Performance Settings ------------ + +# Animations on the gpu. The default shader has to get adjusted to support this +# feature before this option can be turned on. +# hardware-animated-vertices #t + +# Try this options for performance +# uniquify-matrix #t +# uniquify-transforms #t +# uniquify-states #t +# uniquify-attribs #f + +# Enable garbarge collection +garbage-collect-states #t +# garbage-collect-states-rate 0.2 + +# Compress textures on the drivers? +# driver-compress-textures #t + +# Faster animations? (Have to test) +# matrix-palette #t +# display-list-animation #t + +# Better GL performance by not using gl-finish and so on +gl-finish #f +gl-force-no-error #t +gl-check-errors #f +gl-force-no-flush #t +gl-force-no-scissor #t + +# Eventually disable memory barriers, have to check if this is faster +gl-enable-memory-barriers #f + +# Disable threading +lock-to-one-cpu #t +support-threads #f + +# Let the driver generate the mipmaps +driver-generate-mipmaps #t +#gl-ignore-mipmaps #t + +# Use immutable texture storage, it is *supposed* to be faster, but might not be +gl-immutable-texture-storage #t + +# auto-flip #f + +# Default window settings +# depth-bits 0 +color-bits 0 + +framebuffer-depth #f + +gl-fixed-vertex-attrib-locations #f + +# Disable the fragment shader performance warning +gl-validate-shaders #f +gl-skip-shader-recompilation-warnings #t + +alpha-scale-via-texture #f +bounds-type best # best/fastest/sphere/box +pstats-name Render Pipeline Stats +rescale-normals #f +screenshot-extension png + +# Required for correct velocity +always-store-prev-transform #t +allow-incomplete-render #t diff --git a/RenderPipelineFile/toolkit/render_service/config/pipeline.yaml b/RenderPipelineFile/toolkit/render_service/config/pipeline.yaml index 4b3836ba..f9306866 100644 --- a/RenderPipelineFile/toolkit/render_service/config/pipeline.yaml +++ b/RenderPipelineFile/toolkit/render_service/config/pipeline.yaml @@ -1,19 +1,19 @@ - -pipeline: - display_debugger: false - use_r11_g11_b10: false - resolution_scale: 1.0 - reference_mode: true - -lighting: - culling_grid_size_x: 32 - culling_grid_size_y: 16 - culling_grid_slices: 32 - culling_max_distance: 50.0 - culling_slice_width: 256 - max_lights_per_cell: 64 - -shadows: - atlas_size: 4096 - max_updates: 40 - max_update_distance: 150.0 + +pipeline: + display_debugger: false + use_r11_g11_b10: false + resolution_scale: 1.0 + reference_mode: true + +lighting: + culling_grid_size_x: 32 + culling_grid_size_y: 16 + culling_grid_slices: 32 + culling_max_distance: 50.0 + culling_slice_width: 256 + max_lights_per_cell: 64 + +shadows: + atlas_size: 4096 + max_updates: 40 + max_update_distance: 150.0 diff --git a/RenderPipelineFile/toolkit/render_service/config/plugins.yaml b/RenderPipelineFile/toolkit/render_service/config/plugins.yaml index f76c7586..1fc99fd4 100644 --- a/RenderPipelineFile/toolkit/render_service/config/plugins.yaml +++ b/RenderPipelineFile/toolkit/render_service/config/plugins.yaml @@ -1,29 +1,29 @@ - -# Render Pipeline Plugin Configuration -# Instead of editing this file, prefer to use the Plugin Configurator -# Any formatting and comments will be lost - -enabled: - # - ao - # - bloom - # - color_correction - # - env_probes - # - pssm - # - scattering - # - smaa - # - ssr - # - clouds - # - dof - - fxaa - # - motion_blur - # - skin_shading - # - volumetrics - # - vxgi - - -overrides: - fxaa: - quality: ultra - subpixel_quality: 0.5 - edge_threshold: 0.075 - min_threshold: 0.7114 + +# Render Pipeline Plugin Configuration +# Instead of editing this file, prefer to use the Plugin Configurator +# Any formatting and comments will be lost + +enabled: + # - ao + # - bloom + # - color_correction + # - env_probes + # - pssm + # - scattering + # - smaa + # - ssr + # - clouds + # - dof + - fxaa + # - motion_blur + # - skin_shading + # - volumetrics + # - vxgi + + +overrides: + fxaa: + quality: ultra + subpixel_quality: 0.5 + edge_threshold: 0.075 + min_threshold: 0.7114 diff --git a/RenderPipelineFile/toolkit/render_service/config/stages.yaml b/RenderPipelineFile/toolkit/render_service/config/stages.yaml index c76ef0ae..8701377f 100644 --- a/RenderPipelineFile/toolkit/render_service/config/stages.yaml +++ b/RenderPipelineFile/toolkit/render_service/config/stages.yaml @@ -1,61 +1,61 @@ - -# This file controls the order of all render stages. -# Usually you do not have to modify this, except if you are adding new plugins. -# If you move stages, you should know what you do. -# Commenting out a stage won't disable it, but instead produce an error when -# it is created. - -global_stage_order: - - # Shadows, Environment and Voxelization - - PSSMShadowStage - - PSSMDistShadowStage - - PSSMSceneShadowStage - - ScatteringEnvmapStage - - EnvironmentCaptureStage - - VoxelizationStage - - # Main scene - - GBufferStage - - ShadowStage - - DownscaleZStage - - CombineVelocityStage - - # Light(-culling) - - FlagUsedCellsStage - - CollectUsedCellsStage - - CullLightsStage - - CullProbesStage - - # Plugins - - CloudVoxelStage - - AOStage - - ApplyLightsStage - - PSSMStage - - ScatteringStage - - ApplyEnvprobesStage - - VXGIStage - - SSRStage - - SSSRStage - - AmbientStage - - ApplyCloudsStage - - GodrayStage - - VolumetricsStage - - AutoExposureStage - - ManualExposureStage - - # Post AA & Effects - - BloomStage - - TonemappingStage - - SMAAStage - - FXAAStage - - DoFStage - - MotionBlurStage - - SkinShadingStage - - SharpenStage - - ColorCorrectionStage - - # Finishing stages, do not insert anything below - - UpscaleStage - - FinalStage - - UpdatePreviousPipesStage + +# This file controls the order of all render stages. +# Usually you do not have to modify this, except if you are adding new plugins. +# If you move stages, you should know what you do. +# Commenting out a stage won't disable it, but instead produce an error when +# it is created. + +global_stage_order: + + # Shadows, Environment and Voxelization + - PSSMShadowStage + - PSSMDistShadowStage + - PSSMSceneShadowStage + - ScatteringEnvmapStage + - EnvironmentCaptureStage + - VoxelizationStage + + # Main scene + - GBufferStage + - ShadowStage + - DownscaleZStage + - CombineVelocityStage + + # Light(-culling) + - FlagUsedCellsStage + - CollectUsedCellsStage + - CullLightsStage + - CullProbesStage + + # Plugins + - CloudVoxelStage + - AOStage + - ApplyLightsStage + - PSSMStage + - ScatteringStage + - ApplyEnvprobesStage + - VXGIStage + - SSRStage + - SSSRStage + - AmbientStage + - ApplyCloudsStage + - GodrayStage + - VolumetricsStage + - AutoExposureStage + - ManualExposureStage + + # Post AA & Effects + - BloomStage + - TonemappingStage + - SMAAStage + - FXAAStage + - DoFStage + - MotionBlurStage + - SkinShadingStage + - SharpenStage + - ColorCorrectionStage + + # Finishing stages, do not insert anything below + - UpscaleStage + - FinalStage + - UpdatePreviousPipesStage diff --git a/RenderPipelineFile/toolkit/render_service/config/task-scheduler.yaml b/RenderPipelineFile/toolkit/render_service/config/task-scheduler.yaml index ea5c9ed3..d95e286e 100644 --- a/RenderPipelineFile/toolkit/render_service/config/task-scheduler.yaml +++ b/RenderPipelineFile/toolkit/render_service/config/task-scheduler.yaml @@ -1,2 +1,2 @@ -frame_cycles: !!omap - - frame0: +frame_cycles: !!omap + - frame0: diff --git a/RenderPipelineFile/toolkit/render_service/example_usage.py b/RenderPipelineFile/toolkit/render_service/example_usage.py index 3ae01193..8a0601f7 100644 --- a/RenderPipelineFile/toolkit/render_service/example_usage.py +++ b/RenderPipelineFile/toolkit/render_service/example_usage.py @@ -1,58 +1,58 @@ -""" - -Example of using the render service to render a simple sphere. -The render service should be running. - -""" - -import sys -import pickle -import socket - -# Address of the render service -renderer_ip = ("127.0.0.1", 62360) - -# Port which the render service will ping when its done. -pingback_port = 62500 - -# Scene data -payload = pickle.dumps({ - "scene": "resources/preview.bam", - "dest": "demo-render.png", - "view_size_x": 512, - "view_size_y": 512, - "pingback_port": pingback_port -}, protocol=0) - -# Start rendering -print("Sending payload ...") -sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) -try: - sock.sendto(payload, renderer_ip) -except Exception as msg: - sock.close() - raise msg - -# Wait until the renderer finished -print("Waiting for response ...") -sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) -sock.settimeout(3.0) - -try: - sock.bind(("localhost", pingback_port)) -except Exception as msg: - print("Could not connect to pingback_port - maybe a render result is still waiting?") - print("Error message:", msg) - sys.exit(1) - -sock.listen(1) - -try: - conn, addr = sock.accept() -except socket.timeout: - print("No render pipeline response within timeout! Make sure the service is running.") - sock.close() - sys.exit(1) - -print("Rendering was done!") +""" + +Example of using the render service to render a simple sphere. +The render service should be running. + +""" + +import sys +import pickle +import socket + +# Address of the render service +renderer_ip = ("127.0.0.1", 62360) + +# Port which the render service will ping when its done. +pingback_port = 62500 + +# Scene data +payload = pickle.dumps({ + "scene": "resources/preview.bam", + "dest": "demo-render.png", + "view_size_x": 512, + "view_size_y": 512, + "pingback_port": pingback_port +}, protocol=0) + +# Start rendering +print("Sending payload ...") +sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +try: + sock.sendto(payload, renderer_ip) +except Exception as msg: + sock.close() + raise msg + +# Wait until the renderer finished +print("Waiting for response ...") +sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) +sock.settimeout(3.0) + +try: + sock.bind(("localhost", pingback_port)) +except Exception as msg: + print("Could not connect to pingback_port - maybe a render result is still waiting?") + print("Error message:", msg) + sys.exit(1) + +sock.listen(1) + +try: + conn, addr = sock.accept() +except socket.timeout: + print("No render pipeline response within timeout! Make sure the service is running.") + sock.close() + sys.exit(1) + +print("Rendering was done!") diff --git a/RenderPipelineFile/toolkit/render_service/service.py b/RenderPipelineFile/toolkit/render_service/service.py index a9609c5d..07bfd9e3 100644 --- a/RenderPipelineFile/toolkit/render_service/service.py +++ b/RenderPipelineFile/toolkit/render_service/service.py @@ -1,174 +1,174 @@ -""" - -Render service to render previews of materials - -""" - -from __future__ import print_function - -import sys -import socket -import time -import pickle - -from threading import Thread - -from panda3d.core import load_prc_file_data, Filename, Mat4 -from panda3d.core import CS_zup_right, CS_yup_right, BamCache -from direct.showbase.ShowBase import ShowBase - -sys.path.insert(0, "../../") -from rpcore import RenderPipeline, PointLight # noqa - - -class Application(ShowBase): - - ICOMING_PORT = 62360 - - def __init__(self): - load_prc_file_data("", "win-size 512 512") - load_prc_file_data("", "window-type offscreen") - load_prc_file_data("", "model-cache-dir") - load_prc_file_data("", "model-cache-textures #f") - load_prc_file_data("", "textures-power-2 none") - load_prc_file_data("", "alpha-bits 0") - load_prc_file_data("", "print-pipe-types #f") - - # Construct render pipeline - self.render_pipeline = RenderPipeline() - self.render_pipeline.mount_mgr.config_dir = "config/" - self.render_pipeline.create(self) - - self.setup_scene() - - # Disable model caching - BamCache.get_global_ptr().cache_models = False - - self.update_queue = [] - self.start_listen() - - # Render initial frames - for i in range(10): - self.taskMgr.step() - - last_update = 0.0 - self.scene_node = None - - # Wait for updates - while True: - - # Update once in a while - curr_time = time.time() - if curr_time > last_update + 1.0: - last_update = curr_time - self.taskMgr.step() - - if self.update_queue: - if self.scene_node: - self.scene_node.remove_node() - - # Only take the latest packet - payload = self.update_queue.pop(0) - print("RENDERING:", payload) - - scene = self.loader.loadModel(Filename.from_os_specific(payload["scene"])) - - for light in scene.find_all_matches("**/+PointLight"): - light.remove_node() - for light in scene.find_all_matches("**/+Spotlight"): - light.remove_node() - - # Find camera - main_cam = scene.find("**/Camera") - if main_cam: - transform_mat = main_cam.get_transform(self.render).get_mat() - transform_mat = Mat4.convert_mat(CS_zup_right, CS_yup_right) * transform_mat - self.camera.set_mat(transform_mat) - else: - print("WARNING: No camera found") - self.camera.set_pos(0, -3.5, 0) - self.camera.look_at(0, -2.5, 0) - - self.camLens.set_fov(64.0) - - self.scene_node = scene - scene.reparent_to(self.render) - - # Render scene - for i in range(8): - self.taskMgr.step() - - dest_path = Filename.from_os_specific(payload["dest"]) - print("Saving screenshot to", dest_path) - self.win.save_screenshot(dest_path) - self.notify_about_finish(int(payload["pingback_port"])) - - def start_listen(self): - """ Starts the listener thread """ - thread = Thread(target=self.listener_thread, args=(), name="ListenerThread") - thread.setDaemon(True) - thread.start() - return thread - - def listener_thread(self): - """ Thread which listens to incoming updates """ - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - print("Listening on 127.0.0.1:" + str(self.ICOMING_PORT)) - try: - sock.bind(("127.0.0.1", self.ICOMING_PORT)) - while True: - data, addr = sock.recvfrom(8192) - self.handle_data(data) - except Exception as msg: - print("Failed to bind to address! Reason:", msg) - finally: - sock.close() - - def handle_data(self, data): - """ Handles a new update """ - # print("Got:", data) - unpacked_data = pickle.loads(data) - # print("Data = ", unpacked_data) - self.update_queue.append(unpacked_data) - - def notify_about_finish(self, port): - """ Notifies the caller that the result finished """ - print("Sending finish result to localhost:" + str(port)) - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - try: - sock.connect(("localhost", port)) - except Exception as msg: - print("Could not send finish result: ", msg) - return - sock.sendall(b"done") - print("Sent done flag.") - sock.close() - - def setup_scene(self): - """ Setups the basic scene geometry """ - self.disableMouse() - self.render2d.hide() - self.aspect2d.hide() - - light = PointLight() - light.pos = 20.0, -0.85, -1.31 - light.radius = 100.0 - light.energy = 2500 - light.set_color_from_temperature(8000) - # self.render_pipeline.add_light(light) - - light = PointLight() - light.pos = -11.2, -13.84, -9.24 - light.radius = 1e20 - light.set_color_from_temperature(8000) - light.energy = 2500 - # self.render_pipeline.add_light(light) - - # envprobe = self.render_pipeline.add_environment_probe() - # envprobe.set_pos(0, -16.2, 4.4) - # envprobe.set_scale(40, 40, 40) - # envprobe.parallax_correction = False - -Application() +""" + +Render service to render previews of materials + +""" + +from __future__ import print_function + +import sys +import socket +import time +import pickle + +from threading import Thread + +from panda3d.core import load_prc_file_data, Filename, Mat4 +from panda3d.core import CS_zup_right, CS_yup_right, BamCache +from direct.showbase.ShowBase import ShowBase + +sys.path.insert(0, "../../") +from rpcore import RenderPipeline, PointLight # noqa + + +class Application(ShowBase): + + ICOMING_PORT = 62360 + + def __init__(self): + load_prc_file_data("", "win-size 512 512") + load_prc_file_data("", "window-type offscreen") + load_prc_file_data("", "model-cache-dir") + load_prc_file_data("", "model-cache-textures #f") + load_prc_file_data("", "textures-power-2 none") + load_prc_file_data("", "alpha-bits 0") + load_prc_file_data("", "print-pipe-types #f") + + # Construct render pipeline + self.render_pipeline = RenderPipeline() + self.render_pipeline.mount_mgr.config_dir = "config/" + self.render_pipeline.create(self) + + self.setup_scene() + + # Disable model caching + BamCache.get_global_ptr().cache_models = False + + self.update_queue = [] + self.start_listen() + + # Render initial frames + for i in range(10): + self.taskMgr.step() + + last_update = 0.0 + self.scene_node = None + + # Wait for updates + while True: + + # Update once in a while + curr_time = time.time() + if curr_time > last_update + 1.0: + last_update = curr_time + self.taskMgr.step() + + if self.update_queue: + if self.scene_node: + self.scene_node.remove_node() + + # Only take the latest packet + payload = self.update_queue.pop(0) + print("RENDERING:", payload) + + scene = self.loader.loadModel(Filename.from_os_specific(payload["scene"])) + + for light in scene.find_all_matches("**/+PointLight"): + light.remove_node() + for light in scene.find_all_matches("**/+Spotlight"): + light.remove_node() + + # Find camera + main_cam = scene.find("**/Camera") + if main_cam: + transform_mat = main_cam.get_transform(self.render).get_mat() + transform_mat = Mat4.convert_mat(CS_zup_right, CS_yup_right) * transform_mat + self.camera.set_mat(transform_mat) + else: + print("WARNING: No camera found") + self.camera.set_pos(0, -3.5, 0) + self.camera.look_at(0, -2.5, 0) + + self.camLens.set_fov(64.0) + + self.scene_node = scene + scene.reparent_to(self.render) + + # Render scene + for i in range(8): + self.taskMgr.step() + + dest_path = Filename.from_os_specific(payload["dest"]) + print("Saving screenshot to", dest_path) + self.win.save_screenshot(dest_path) + self.notify_about_finish(int(payload["pingback_port"])) + + def start_listen(self): + """ Starts the listener thread """ + thread = Thread(target=self.listener_thread, args=(), name="ListenerThread") + thread.setDaemon(True) + thread.start() + return thread + + def listener_thread(self): + """ Thread which listens to incoming updates """ + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + print("Listening on 127.0.0.1:" + str(self.ICOMING_PORT)) + try: + sock.bind(("127.0.0.1", self.ICOMING_PORT)) + while True: + data, addr = sock.recvfrom(8192) + self.handle_data(data) + except Exception as msg: + print("Failed to bind to address! Reason:", msg) + finally: + sock.close() + + def handle_data(self, data): + """ Handles a new update """ + # print("Got:", data) + unpacked_data = pickle.loads(data) + # print("Data = ", unpacked_data) + self.update_queue.append(unpacked_data) + + def notify_about_finish(self, port): + """ Notifies the caller that the result finished """ + print("Sending finish result to localhost:" + str(port)) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + try: + sock.connect(("localhost", port)) + except Exception as msg: + print("Could not send finish result: ", msg) + return + sock.sendall(b"done") + print("Sent done flag.") + sock.close() + + def setup_scene(self): + """ Setups the basic scene geometry """ + self.disableMouse() + self.render2d.hide() + self.aspect2d.hide() + + light = PointLight() + light.pos = 20.0, -0.85, -1.31 + light.radius = 100.0 + light.energy = 2500 + light.set_color_from_temperature(8000) + # self.render_pipeline.add_light(light) + + light = PointLight() + light.pos = -11.2, -13.84, -9.24 + light.radius = 1e20 + light.set_color_from_temperature(8000) + light.energy = 2500 + # self.render_pipeline.add_light(light) + + # envprobe = self.render_pipeline.add_environment_probe() + # envprobe.set_pos(0, -16.2, 4.4) + # envprobe.set_scale(40, 40, 40) + # envprobe.parallax_correction = False + +Application() diff --git a/RenderPipelineFile/toolkit/rp_distributor/README.md b/RenderPipelineFile/toolkit/rp_distributor/README.md index 93205486..cbb7c307 100644 --- a/RenderPipelineFile/toolkit/rp_distributor/README.md +++ b/RenderPipelineFile/toolkit/rp_distributor/README.md @@ -1,5 +1,5 @@ -## RP Distributor - -This makes a distributed copy of the render pipeline, with all not-necessary -files (like the whole toolkit/ folder) removed. - +## RP Distributor + +This makes a distributed copy of the render pipeline, with all not-necessary +files (like the whole toolkit/ folder) removed. + diff --git a/RenderPipelineFile/toolkit/rp_distributor/launch.templ.bat b/RenderPipelineFile/toolkit/rp_distributor/launch.templ.bat index d3c10078..e1ab1f9a 100644 --- a/RenderPipelineFile/toolkit/rp_distributor/launch.templ.bat +++ b/RenderPipelineFile/toolkit/rp_distributor/launch.templ.bat @@ -1,23 +1,23 @@ -@echo off - -rem set required paths -set PANDAPATH=%~dp0panda3d -set PATH=%PANDAPATH%\bin;%PANDAPATH%;%PATH% -set PYTHONPATH=%PANDAPATH%\;%PYTHONPATH% - -rem check for visual studio dlls -where msvcp140.dll >nul 2>nul -if ERRORLEVEL 1 ( - @echo This program requires the Microsoft Visual C++ 2015 Runtime. - @echo Hit a key to visit the download page. - pause - start https://www.microsoft.com/en-us/download/details.aspx?id=48145 - goto end -) - -rem execute actual program -@echo Running Panda3D from "%PANDAPATH%" -"%PANDAPATH%\python\ppython.exe" -B application/main.py -pause - -:end +@echo off + +rem set required paths +set PANDAPATH=%~dp0panda3d +set PATH=%PANDAPATH%\bin;%PANDAPATH%;%PATH% +set PYTHONPATH=%PANDAPATH%\;%PYTHONPATH% + +rem check for visual studio dlls +where msvcp140.dll >nul 2>nul +if ERRORLEVEL 1 ( + @echo This program requires the Microsoft Visual C++ 2015 Runtime. + @echo Hit a key to visit the download page. + pause + start https://www.microsoft.com/en-us/download/details.aspx?id=48145 + goto end +) + +rem execute actual program +@echo Running Panda3D from "%PANDAPATH%" +"%PANDAPATH%\python\ppython.exe" -B application/main.py +pause + +:end diff --git a/RenderPipelineFile/toolkit/rp_distributor/main.py b/RenderPipelineFile/toolkit/rp_distributor/main.py index e8959c3f..68828d12 100644 --- a/RenderPipelineFile/toolkit/rp_distributor/main.py +++ b/RenderPipelineFile/toolkit/rp_distributor/main.py @@ -1,154 +1,154 @@ -""" Tool to distribute the rp and the panda3d build. """ - -from __future__ import print_function - -import os -import sys -import shutil -from os.path import isdir, join, dirname, realpath, relpath - -base_dir = realpath(dirname(__file__)) -rp_dir = realpath(join(base_dir, "../../")) -os.chdir(base_dir) - -sys.path.insert(0, rp_dir) -from rplibs.six.moves import input # noqa # pylint: disable=import-error - -# TODO: Add option to skip gui folders if debugger is disabled - -rp_ignores = [ - - # data - "skybox-blend.zip", - "skybox.jpg", - "skybox-2.jpg", - "default_cubemap/source", - "default_cubemap/source_2", - "default_cubemap/filter.compute.glsl", - "default_cubemap/filter.py", - "data/generate_txo_files.py", - "README.md", - "environment_brdf/generate_reference.py", - "run_mitsuba.bat", - ".mip", - ".xml", - ".exr", - ".psd", - ".diff", - ".pyc", - ".pdb", - "__pycache__", - "environment_brdf/res/", - "film_grain/generate.py", - "film_grain/grain.compute.glsl", - "ies_profiles/PREVIEWS.jpg", - "loading_screen_bg.png", - - - # rpcore - "native/scripts", - "native/source", - "native/win_amd", - "native/win_i386", - "native/.gitignore", - "native/build.py", - "native/CMakeLists", - "native/update_module_builder.py", - "native/config.ini", - - # rpplugins - ".ffxml", - "bloom/resources/SOURCE.txt", - "bloom/resources/lens_dirt.png", - "clouds/resources/generate_", - "clouds/resources/noise.inc", - "clouds/resources/precompute.py", - "color_correction/resources/film_luts_raw", - "color_correction/resources/generate_", - "plugin_prefab", - "scattering/resources/hosek_wilkie_scattering", - - "toolkit", - -] - -panda_ignores = [ - ".pdb", - ".pyc", - "python/.vs", - "python/include", -] - -app_ignores = [ - ".pyc", - ".blend" -] - - -def copy_tree(source_dir, dest_dir, ignorelist, tree_pth): - source = join(source_dir, tree_pth) - dest = join(dest_dir, tree_pth) - for basepath, dirnames, files in os.walk(source): - for f in files: - abspath = realpath(join(basepath, f)) - abspath = abspath.replace("\\", "/") - - for ignore in ignorelist: - if ignore in abspath: - break - else: - local_pth = relpath(abspath, start=source) - dest_pth = join(dest, local_pth) - dname = dirname(dest_pth) - if not isdir(dname): - print("Creating", dname) - os.makedirs(dname) - shutil.copyfile(abspath, dest_pth) - - -def distribute(): - print("Render Pipeline Distributor v0.1") - print("") - print("Copying tree ..") - - dist_folder_name = "built" - - tmp_dir = join(base_dir, dist_folder_name, "render_pipeline") - if isdir(tmp_dir): - shutil.rmtree(tmp_dir) - os.makedirs(tmp_dir) - - for dname in ("config", "data", "rplibs", "effects", "rpcore", "rpplugins", "toolkit"): - copy_tree(rp_dir, tmp_dir, rp_ignores, dname) - - shutil.copyfile(join(rp_dir, "LICENSE.txt"), join(tmp_dir, "LICENSE.txt")) - - print("Copying Panda3D build ...") - - python_pth = realpath(dirname(sys.executable)) - panda_pth = realpath(join(python_pth, "..")) - - tmp_dir = join(base_dir, dist_folder_name, "panda3d") - if isdir(tmp_dir): - shutil.rmtree(tmp_dir) - os.makedirs(tmp_dir) - - for dname in ("direct", "etc", "models", "panda3d", "pandac", "python"): - copy_tree(panda_pth, tmp_dir, panda_ignores, dname) - - copy_tree(panda_pth, tmp_dir, panda_ignores + [".exe"], "bin") - - shutil.copyfile(join(panda_pth, "LICENSE"), join(tmp_dir, "LICENSE.txt")) - - # Copy launcher script - shutil.copyfile( - join(base_dir, "launch.templ.bat"), join(base_dir, dist_folder_name, "launch.bat")) - - # Copy application - app_pth = join(base_dir, "../../../RenderPipeline-Samples/01-Material-Demo/") - tmp_dir = join(base_dir, dist_folder_name, "application") - - copy_tree(app_pth, tmp_dir, app_ignores, ".") - -if __name__ == "__main__": - distribute() +""" Tool to distribute the rp and the panda3d build. """ + +from __future__ import print_function + +import os +import sys +import shutil +from os.path import isdir, join, dirname, realpath, relpath + +base_dir = realpath(dirname(__file__)) +rp_dir = realpath(join(base_dir, "../../")) +os.chdir(base_dir) + +sys.path.insert(0, rp_dir) +from rplibs.six.moves import input # noqa # pylint: disable=import-error + +# TODO: Add option to skip gui folders if debugger is disabled + +rp_ignores = [ + + # data + "skybox-blend.zip", + "skybox.jpg", + "skybox-2.jpg", + "default_cubemap/source", + "default_cubemap/source_2", + "default_cubemap/filter.compute.glsl", + "default_cubemap/filter.py", + "data/generate_txo_files.py", + "README.md", + "environment_brdf/generate_reference.py", + "run_mitsuba.bat", + ".mip", + ".xml", + ".exr", + ".psd", + ".diff", + ".pyc", + ".pdb", + "__pycache__", + "environment_brdf/res/", + "film_grain/generate.py", + "film_grain/grain.compute.glsl", + "ies_profiles/PREVIEWS.jpg", + "loading_screen_bg.png", + + + # rpcore + "native/scripts", + "native/source", + "native/win_amd", + "native/win_i386", + "native/.gitignore", + "native/build.py", + "native/CMakeLists", + "native/update_module_builder.py", + "native/config.ini", + + # rpplugins + ".ffxml", + "bloom/resources/SOURCE.txt", + "bloom/resources/lens_dirt.png", + "clouds/resources/generate_", + "clouds/resources/noise.inc", + "clouds/resources/precompute.py", + "color_correction/resources/film_luts_raw", + "color_correction/resources/generate_", + "plugin_prefab", + "scattering/resources/hosek_wilkie_scattering", + + "toolkit", + +] + +panda_ignores = [ + ".pdb", + ".pyc", + "python/.vs", + "python/include", +] + +app_ignores = [ + ".pyc", + ".blend" +] + + +def copy_tree(source_dir, dest_dir, ignorelist, tree_pth): + source = join(source_dir, tree_pth) + dest = join(dest_dir, tree_pth) + for basepath, dirnames, files in os.walk(source): + for f in files: + abspath = realpath(join(basepath, f)) + abspath = abspath.replace("\\", "/") + + for ignore in ignorelist: + if ignore in abspath: + break + else: + local_pth = relpath(abspath, start=source) + dest_pth = join(dest, local_pth) + dname = dirname(dest_pth) + if not isdir(dname): + print("Creating", dname) + os.makedirs(dname) + shutil.copyfile(abspath, dest_pth) + + +def distribute(): + print("Render Pipeline Distributor v0.1") + print("") + print("Copying tree ..") + + dist_folder_name = "built" + + tmp_dir = join(base_dir, dist_folder_name, "render_pipeline") + if isdir(tmp_dir): + shutil.rmtree(tmp_dir) + os.makedirs(tmp_dir) + + for dname in ("config", "data", "rplibs", "effects", "rpcore", "rpplugins", "toolkit"): + copy_tree(rp_dir, tmp_dir, rp_ignores, dname) + + shutil.copyfile(join(rp_dir, "LICENSE.txt"), join(tmp_dir, "LICENSE.txt")) + + print("Copying Panda3D build ...") + + python_pth = realpath(dirname(sys.executable)) + panda_pth = realpath(join(python_pth, "..")) + + tmp_dir = join(base_dir, dist_folder_name, "panda3d") + if isdir(tmp_dir): + shutil.rmtree(tmp_dir) + os.makedirs(tmp_dir) + + for dname in ("direct", "etc", "models", "panda3d", "pandac", "python"): + copy_tree(panda_pth, tmp_dir, panda_ignores, dname) + + copy_tree(panda_pth, tmp_dir, panda_ignores + [".exe"], "bin") + + shutil.copyfile(join(panda_pth, "LICENSE"), join(tmp_dir, "LICENSE.txt")) + + # Copy launcher script + shutil.copyfile( + join(base_dir, "launch.templ.bat"), join(base_dir, dist_folder_name, "launch.bat")) + + # Copy application + app_pth = join(base_dir, "../../../RenderPipeline-Samples/01-Material-Demo/") + tmp_dir = join(base_dir, dist_folder_name, "application") + + copy_tree(app_pth, tmp_dir, app_ignores, ".") + +if __name__ == "__main__": + distribute() diff --git a/Start_Run.py b/Start_Run.py index 81951385..8c6489d1 100644 --- a/Start_Run.py +++ b/Start_Run.py @@ -1,37 +1,37 @@ -import os -import sys - -# 添加项目根目录到 Python 路径 -project_root = os.path.dirname(os.path.abspath(__file__)) -sys.path.insert(0, project_root) - -# 设置工作目录为项目根目录 -os.chdir(project_root) - -# 添加 RenderPipeline 到路径(注意路径名称应与实际目录一致) -render_pipeline_path = os.path.join(project_root, "RenderPipeline") -sys.path.insert(0, render_pipeline_path) - -# 添加 RenderPipelineFile 路径 -render_pipeline_file_path = os.path.join(project_root, "RenderPipelineFile") -sys.path.insert(0, render_pipeline_file_path) - -# 添加 icons 目录到路径 -icons_path = os.path.join(project_root, "icons") -sys.path.insert(0, icons_path) - -# 现在可以导入并运行主程序 -if __name__ == "__main__": - args = sys.argv[1:] - # args = "/home/tiger/桌面/Test1" - # args = "C:/Users/29381/Desktop/1" - print(f'Path is {args}') - # 将整个列表转换为字符串(包括方括号) - args_str = ''.join(args) - - from main import run - if args: - run(args_str) - # run(args) - else: +import os +import sys + +# 添加项目根目录到 Python 路径 +project_root = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, project_root) + +# 设置工作目录为项目根目录 +os.chdir(project_root) + +# 添加 RenderPipeline 到路径(注意路径名称应与实际目录一致) +render_pipeline_path = os.path.join(project_root, "RenderPipeline") +sys.path.insert(0, render_pipeline_path) + +# 添加 RenderPipelineFile 路径 +render_pipeline_file_path = os.path.join(project_root, "RenderPipelineFile") +sys.path.insert(0, render_pipeline_file_path) + +# 添加 icons 目录到路径 +icons_path = os.path.join(project_root, "icons") +sys.path.insert(0, icons_path) + +# 现在可以导入并运行主程序 +if __name__ == "__main__": + args = sys.argv[1:] + # args = "/home/tiger/桌面/Test1" + # args = "C:/Users/29381/Desktop/1" + print(f'Path is {args}') + # 将整个列表转换为字符串(包括方括号) + args_str = ''.join(args) + + from main import run + if args: + run(args_str) + # run(args) + else: run() \ No newline at end of file diff --git a/VR_Manager 模块化拆分计划.md b/VR_Manager 模块化拆分计划.md index acd64458..22bb9c89 100644 --- a/VR_Manager 模块化拆分计划.md +++ b/VR_Manager 模块化拆分计划.md @@ -1,446 +1,446 @@ - VR Manager 模块化拆分计划 - - 📊 现状分析 - - 当前状态: - - 文件:core/vr_manager.py - - 行数:4736行(严重超标,标准为900行) - - 方法数:138个方法 - - 问题:典型的"上帝类"反模式,承担了过多职责 - - 主要职责识别: - 1. 性能监控和调试 (~900行) - 2. 测试模式系统 (~800行) - 3. 对象池和优化 (~300行) - 4. 姿态跟踪系统 (~900行) - 5. 设备管理 (~300行) - 6. 渲染缓冲管理 (~400行) - 7. 相机系统 (~350行) - 8. 合成器和提交 (~300行) - 9. RenderPipeline集成 (~250行) - 10. 核心生命周期 (~500行) - - --- - 🎯 拆分策略 - - 核心原则 - - 1. 渐进迭代:每次拆分一个模块,立即验证 - 2. 组合模式:新模块作为VRManager属性,保持接口不变 - 3. 依赖顺序:先拆分独立模块,后拆分核心模块 - 4. 向后兼容:所有公开API通过委托方法保持可用 - - 拆分顺序(按依赖关系) - - 阶段1:性能监控 → 最独立,零依赖 - 阶段2:测试调试 → 依赖少,可独立测试 - 阶段3:对象池优化 → 小而关键,性能核心 - 阶段4:跟踪系统 → 中等复杂度 - 阶段5:渲染系统 → 核心功能,最复杂 - 阶段6:核心管理器 → 整合所有子系统 - - --- - 📋 详细拆分计划 - - 🔷 阶段1:拆分性能监控系统 (2-3小时) - - 创建文件:core/vr/performance/monitoring.py (~900行) - - 迁移方法 (35个): - - 性能报告:_print_performance_report, _print_performance_recommendations, _print_brief_performance_report - - 性能监控:_init_performance_monitoring, _update_performance_metrics, _update_gpu_metrics, _track_frame_time - - GPU计时:_get_gpu_frame_timing, enable_gpu_timing_monitoring, disable_gpu_timing_monitoring - - 管线统计:_get_pipeline_stats, test_pipeline_monitoring, _start_timing, _end_timing - - 诊断工具:_print_render_callback_diagnostics, _check_rendering_optimizations, _diagnose_opengl_state - - 调试控制:enable_debug_output, disable_debug_output, set_debug_mode, toggle_debug_output, get_debug_status - - 配置方法:set_performance_check_interval, set_frame_time_history_size, set_performance_report_interval - - 查询方法:get_performance_stats, get_current_performance_summary, get_performance_monitoring_config - - 控制方法:enable_performance_monitoring, disable_performance_monitoring, force_performance_report, reset_performance_counters - - 状态查询:print_performance_monitoring_status, set_prediction_time - - 迁移属性 (~30个): - # 性能监控开关 - performance_monitoring, debug_output_enabled, debug_mode - enable_pipeline_monitoring, enable_gpu_timing - - # 性能数据 - cpu_usage, memory_usage, gpu_usage, gpu_memory_usage - frame_times, max_frame_time_history - wait_poses_time, left_render_time, right_render_time, submit_time - total_frame_time, vr_sync_wait_time - - # GPU计时 - gpu_scene_render_ms, gpu_pre_submit_ms, gpu_post_submit_ms - gpu_total_render_ms, gpu_compositor_render_ms - gpu_timing_history, gpu_timing_failure_count - - # 历史记录 - wait_poses_times, render_times, submit_times, sync_wait_times - pipeline_history_size - - 类结构: - class VRPerformanceMonitor: - """VR性能监控系统""" - def __init__(self, vr_manager): - self.vr_manager = vr_manager - # 初始化所有性能监控属性 - - VRManager集成: - # __init__ 中 - self.performance_monitor = VRPerformanceMonitor(self) - - # 委托方法(保持API兼容) - def enable_performance_monitoring(self): - return self.performance_monitor.enable_performance_monitoring() - - 验证步骤: - 1. ✅ 编译检查:python -m py_compile core/vr/performance/monitoring.py - 2. ✅ 导入测试:启动应用,确认无导入错误 - 3. ✅ 功能测试:调用 vr_manager.enable_performance_monitoring() - 4. ✅ 输出验证:检查性能报告正常生成 - 5. ✅ API测试:验证所有委托方法可用 - - --- - 🔷 阶段2:拆分测试调试系统 (2-3小时) - - 创建文件:core/vr/testing/test_mode.py (~800行) - - 迁移方法 (17个): - - 测试模式:enable_vr_test_mode, disable_vr_test_mode, switch_test_display_mode - - 纹理管理:_ensure_test_mode_textures, _create_cached_ovr_textures, _batch_submit_textures - - 显示系统:_initialize_test_display, _update_test_display, _create_stereo_display, _cleanup_test_display - - HUD系统:_initialize_test_performance_hud, _update_test_performance_hud, _cleanup_test_performance_hud - - 状态查询:get_test_mode_status, get_test_mode_features, set_test_mode_features - - 性能测试:run_vr_performance_test - - 迁移属性 (~15个): - vr_test_mode, test_display_mode - test_display_quad, test_right_quad, stereo_display_created - test_performance_hud, test_performance_text - test_mode_initialized - hud_update_counter, hud_update_interval - test_mode_submit_texture, test_mode_wait_poses - - 类结构: - class VRTestMode: - """VR测试模式系统""" - def __init__(self, vr_manager): - self.vr_manager = vr_manager - - 验证步骤: - 1. ✅ 启用测试模式:vr_manager.enable_vr_test_mode('stereo') - 2. ✅ 检查显示:验证测试quad显示正确 - 3. ✅ HUD验证:检查性能HUD显示 - 4. ✅ 切换模式:测试 left/right/stereo 模式切换 - 5. ✅ 禁用测试:vr_manager.disable_vr_test_mode() - - --- - 🔷 阶段3:拆分对象池和优化系统 (1-2小时) - - 创建文件:core/vr/performance/optimization.py (~300行) - - 迁移方法 (19个): - - 对象池:_initialize_object_pools, _get_pooled_matrix, _return_pooled_matrix - - GC控制:_manual_gc_control, enable_gc_control, disable_gc_control, set_manual_gc_interval, force_manual_gc - - 分辨率:set_resolution_scale, set_quality_preset, cycle_quality_preset, _apply_resolution_scale - - 查询方法:get_object_pool_status, get_resolution_info, print_resolution_info - - 性能模式:enable_performance_mode, disable_performance_mode, set_performance_mode_trigger_frame, get_performance_mode_status - - 迁移属性 (~20个): - # 对象池 - _matrix_pool, _matrix_pool_size - _cached_matrices, _controller_poses_cache - _left_ovr_texture, _right_ovr_texture - - # GC控制 - _gc_control_enabled, _gc_disabled - _manual_gc_interval, _last_manual_gc_frame - - # 分辨率 - resolution_scale, base_eye_width, base_eye_height - scaled_eye_width, scaled_eye_height - quality_presets, current_quality_preset - - # 性能模式 - performance_mode_enabled, performance_mode_trigger_frame - - 验证步骤: - 1. ✅ 对象池:检查Mat4对象池正常工作 - 2. ✅ GC控制:验证手动GC按预期触发 - 3. ✅ 分辨率:测试质量预设切换 - 4. ✅ 性能测试:运行30秒性能测试,确认优化生效 - - --- - 🔷 阶段4:拆分跟踪系统 (3-4小时) - - 创建文件1:core/vr/tracking/poses.py (~600行) - - 迁移方法 (12个): - - 姿态获取:_wait_get_poses, _wait_get_poses_immediate, _wait_get_poses_with_prediction - - 姿态缓存:_cache_poses_for_next_frame, _reset_waitgetposes_flag - - 姿态更新:_update_tracking_data, update_hmd, _update_camera_poses, _update_camera_poses_with_cache - - 矩阵转换:_convert_openvr_matrix_to_panda, _update_matrix_from_openvr, convert_mat - - 迁移属性: - hmd_pose, controller_poses, tracked_device_poses - poses, game_poses - tracking_space, hmd_anchor, left_eye_anchor, right_eye_anchor - coord_mat, coord_mat_inv - use_prediction_time, poses_updated_in_task - _waitgetposes_called_this_frame - _cached_render_poses, _first_frame - - 创建文件2:core/vr/tracking/devices.py (~300行) - - 迁移方法 (8个): - - 控制器:_initialize_controllers, _detect_controllers, get_controller_by_role - - 设备管理:_create_tracked_device_anchor, update_tracked_devices - - 状态查询:are_controllers_connected, get_connected_controllers - - 震动:trigger_controller_haptic - - 迁移属性: - left_controller, right_controller - controllers, tracked_device_anchors - - 创建文件3:core/vr/tracking/input_wrapper.py (~200行) - - 迁移方法 (12个): - - 按钮查询:is_trigger_pressed, is_trigger_just_pressed, is_grip_pressed, is_grip_just_pressed, is_menu_pressed - - 触摸板:is_trackpad_touched, get_trackpad_position - - 交互查询:get_selected_object, get_grabbed_object, is_grabbing_object - - 交互控制:force_release_all_grabs, add_interactable_object - - 类结构: - class VRPoseTracker: - """VR姿态跟踪系统""" - - class VRDeviceManager: - """VR设备管理系统""" - - class VRInputWrapper: - """VR输入包装层""" - - 验证步骤: - 1. ✅ 姿态跟踪:启动VR,检查头显跟踪正常 - 2. ✅ 控制器检测:验证控制器正确识别 - 3. ✅ 输入测试:测试按钮和触摸板输入 - 4. ✅ 震动测试:触发控制器震动反馈 - - --- - 🔷 阶段5:拆分渲染系统 (4-5小时) - - 创建文件1:core/vr/rendering/buffers.py (~400行) - - 迁移方法 (10个): - - 缓冲区创建:_create_vr_buffers, _create_vr_buffer, _create_vr_buffers_with_pipeline - - 纹理管理:_create_vr_texture, _prepare_and_cache_textures - - Pipeline效果:_apply_pipeline_vr_effects - - 天空盒:_check_skybox_status, _create_vr_skybox - - 诊断:_diagnose_buffer_performance - - 清理:_cleanup_vr_buffers - - 迁移属性: - vr_left_eye_buffer, vr_right_eye_buffer - vr_left_texture, vr_right_texture - left_texture_id, right_texture_id, textures_prepared - eye_width, eye_height, near_clip, far_clip - scaled_eye_width, scaled_eye_height - - 创建文件2:core/vr/rendering/cameras.py (~350行) - - 迁移方法 (6个): - - 相机设置:_setup_vr_cameras, _get_eye_offset - - 优化:_optimize_vr_rendering, _apply_lightweight_rendering, _disable_vr_buffer_extras - - 主相机:_disable_main_cam, _enable_main_cam - - 迁移属性: - vr_left_camera, vr_right_camera - - 创建文件3:core/vr/rendering/compositor.py (~300行) - - 迁移方法 (9个): - - 渲染回调:simple_left_cb, simple_right_cb - - 纹理提交:submit_texture - - GPU同步:_sync_gpu_if_needed, _smart_gpu_sync - - ATW控制:_disable_async_reprojection, enable_async_reprojection_disable, disable_async_reprojection_disable - - 迁移属性: - vr_compositor, submit_together - openvr_frame_id - left_eye_last_render_frame, right_eye_last_render_frame - disable_async_reprojection - - 创建文件4:core/vr/rendering/pipeline.py (~250行) - - 迁移方法 (4个): - - Pipeline集成:_create_vr_buffers_with_pipeline, _apply_pipeline_vr_effects - - 模式切换:set_vr_render_mode, get_vr_render_mode - - 迁移属性: - vr_render_mode, render_pipeline_enabled - vr_pipeline_left_target, vr_pipeline_right_target - pipeline_resolution_scale, vr_pipeline_controller - pipeline_vr_config - - 验证步骤: - 1. ✅ 缓冲区:检查左右眼缓冲区正确创建 - 2. ✅ 相机:验证双眼相机位置和视锥 - 3. ✅ 渲染:测试左右眼渲染回调 - 4. ✅ 提交:确认纹理正确提交到OpenVR - 5. ✅ Pipeline:测试RenderPipeline模式切换 - - --- - 🔷 阶段6:重构核心管理器 (2-3小时) - - 保留在 core/vr_manager.py (~500行) - - 保留方法 (10个核心方法): - - 生命周期:__init__, cleanup - - VR初始化:is_vr_available, initialize_vr - - VR控制:enable_vr, disable_vr - - 主循环:_start_vr_task, _update_vr - - 状态查询:get_vr_status - - 新增子系统属性(组合模式): - def __init__(self, world): - # ... 基础初始化 ... - - # 子系统初始化(按依赖顺序) - self.optimization = VROptimization(self) - self.performance_monitor = VRPerformanceMonitor(self) - self.test_mode = VRTestMode(self) - - self.pose_tracker = VRPoseTracker(self) - self.device_manager = VRDeviceManager(self) - self.input_wrapper = VRInputWrapper(self) - - self.buffer_manager = VRBufferManager(self) - self.camera_manager = VRCameraManager(self) - self.compositor = VRCompositor(self) - self.pipeline_manager = VRPipelineManager(self) - - 委托方法(保持API兼容): - # 性能监控委托 - def enable_performance_monitoring(self): - return self.performance_monitor.enable_performance_monitoring() - - # 测试模式委托 - def enable_vr_test_mode(self, display_mode='stereo'): - return self.test_mode.enable_vr_test_mode(display_mode) - - # 优化系统委托 - def set_resolution_scale(self, scale): - return self.optimization.set_resolution_scale(scale) - - # ... 其他委托方法 ... - - 验证步骤: - 1. ✅ 完整启动:启动应用,测试VR完整流程 - 2. ✅ API测试:验证所有公开API可用 - 3. ✅ 性能测试:运行性能测试,确认无回退 - 4. ✅ 集成测试:测试传送、交互、渲染等所有功能 - - --- - ✅ 每阶段验证清单 - - 静态验证 - - - python -m py_compile 检查语法 - - python main.py --help 验证模块导入 - - 运行 pylint 检查代码质量 - - 功能验证 - - - 启动应用:python main.py - - 检查VR可用性:vr_manager.is_vr_available() - - 测试VR启用:切换到VR模式 - - 测试头显跟踪:移动头显查看跟踪 - - 测试控制器:检测和交互 - - 测试传送:使用控制器传送 - - 测试性能监控:查看性能报告 - - 测试VR禁用:退出VR模式 - - 性能验证 - - - 运行性能测试:vr_manager.run_vr_performance_test(30) - - 检查帧率稳定在90fps - - 对比拆分前后性能指标 - - 确认无性能回退 - - --- - 🛡️ 风险控制 - - 备份策略 - - # 每个阶段开始前 - cp core/vr_manager.py core/vr_manager.py.backup.stage{N} - - 回滚方案 - - # 恢复备份 - cp core/vr_manager.py.backup.stage{N} core/vr_manager.py - - 循环依赖预防 - - - ✅ 所有子模块通过 self.vr_manager 访问其他子系统 - - ✅ 子模块间不直接引用 - - ✅ VRManager 作为中介协调所有子系统 - - 性能保护 - - - ✅ 热点路径(渲染回调)保持直接调用 - - ✅ 非关键路径使用委托模式 - - ✅ 对象池和缓存机制保持不变 - - --- - 📊 预期成果 - - 代码结构 - - core/vr_manager.py (500行) ✅ - core/vr/ - ├── performance/ - │ ├── monitoring.py (900行) ✅ - │ └── optimization.py (300行) ✅ - ├── testing/ - │ └── test_mode.py (800行) ✅ - ├── tracking/ - │ ├── poses.py (600行) ✅ - │ ├── devices.py (300行) ✅ - │ └── input_wrapper.py (200行) ✅ - └── rendering/ - ├── buffers.py (400行) ✅ - ├── cameras.py (350行) ✅ - ├── compositor.py (300行) ✅ - └── pipeline.py (250行) ✅ - - 改进指标 - - - ✅ 文件行数:4736行 → 最大900行(符合规范) - - ✅ 目录文件数:所有目录 ≤ 4个文件(远小于8个限制) - - ✅ 方法数:138个 → 每个类 ≤ 20个方法 - - ✅ 职责清晰:每个模块单一职责 - - ✅ 可维护性:大幅提升 - - ✅ 可测试性:模块化便于单元测试 - - ✅ 向后兼容:100%保持现有API - - 时间估算 - - - 阶段1: 2-3小时 - - 阶段2: 2-3小时 - - 阶段3: 1-2小时 - - 阶段4: 3-4小时 - - 阶段5: 4-5小时 - - 阶段6: 2-3小时 - - 总计:15-20小时,分6个阶段逐步完成 - - --- - 🚀 开始执行 - - 确认此计划后,将按以下顺序执行: - 1. 创建备份 - 2. 阶段1:拆分性能监控系统 - 3. 验证通过后,进入阶段2 - 4. 依次完成所有6个阶段 + VR Manager 模块化拆分计划 + + 📊 现状分析 + + 当前状态: + - 文件:core/vr_manager.py + - 行数:4736行(严重超标,标准为900行) + - 方法数:138个方法 + - 问题:典型的"上帝类"反模式,承担了过多职责 + + 主要职责识别: + 1. 性能监控和调试 (~900行) + 2. 测试模式系统 (~800行) + 3. 对象池和优化 (~300行) + 4. 姿态跟踪系统 (~900行) + 5. 设备管理 (~300行) + 6. 渲染缓冲管理 (~400行) + 7. 相机系统 (~350行) + 8. 合成器和提交 (~300行) + 9. RenderPipeline集成 (~250行) + 10. 核心生命周期 (~500行) + + --- + 🎯 拆分策略 + + 核心原则 + + 1. 渐进迭代:每次拆分一个模块,立即验证 + 2. 组合模式:新模块作为VRManager属性,保持接口不变 + 3. 依赖顺序:先拆分独立模块,后拆分核心模块 + 4. 向后兼容:所有公开API通过委托方法保持可用 + + 拆分顺序(按依赖关系) + + 阶段1:性能监控 → 最独立,零依赖 + 阶段2:测试调试 → 依赖少,可独立测试 + 阶段3:对象池优化 → 小而关键,性能核心 + 阶段4:跟踪系统 → 中等复杂度 + 阶段5:渲染系统 → 核心功能,最复杂 + 阶段6:核心管理器 → 整合所有子系统 + + --- + 📋 详细拆分计划 + + 🔷 阶段1:拆分性能监控系统 (2-3小时) + + 创建文件:core/vr/performance/monitoring.py (~900行) + + 迁移方法 (35个): + - 性能报告:_print_performance_report, _print_performance_recommendations, _print_brief_performance_report + - 性能监控:_init_performance_monitoring, _update_performance_metrics, _update_gpu_metrics, _track_frame_time + - GPU计时:_get_gpu_frame_timing, enable_gpu_timing_monitoring, disable_gpu_timing_monitoring + - 管线统计:_get_pipeline_stats, test_pipeline_monitoring, _start_timing, _end_timing + - 诊断工具:_print_render_callback_diagnostics, _check_rendering_optimizations, _diagnose_opengl_state + - 调试控制:enable_debug_output, disable_debug_output, set_debug_mode, toggle_debug_output, get_debug_status + - 配置方法:set_performance_check_interval, set_frame_time_history_size, set_performance_report_interval + - 查询方法:get_performance_stats, get_current_performance_summary, get_performance_monitoring_config + - 控制方法:enable_performance_monitoring, disable_performance_monitoring, force_performance_report, reset_performance_counters + - 状态查询:print_performance_monitoring_status, set_prediction_time + + 迁移属性 (~30个): + # 性能监控开关 + performance_monitoring, debug_output_enabled, debug_mode + enable_pipeline_monitoring, enable_gpu_timing + + # 性能数据 + cpu_usage, memory_usage, gpu_usage, gpu_memory_usage + frame_times, max_frame_time_history + wait_poses_time, left_render_time, right_render_time, submit_time + total_frame_time, vr_sync_wait_time + + # GPU计时 + gpu_scene_render_ms, gpu_pre_submit_ms, gpu_post_submit_ms + gpu_total_render_ms, gpu_compositor_render_ms + gpu_timing_history, gpu_timing_failure_count + + # 历史记录 + wait_poses_times, render_times, submit_times, sync_wait_times + pipeline_history_size + + 类结构: + class VRPerformanceMonitor: + """VR性能监控系统""" + def __init__(self, vr_manager): + self.vr_manager = vr_manager + # 初始化所有性能监控属性 + + VRManager集成: + # __init__ 中 + self.performance_monitor = VRPerformanceMonitor(self) + + # 委托方法(保持API兼容) + def enable_performance_monitoring(self): + return self.performance_monitor.enable_performance_monitoring() + + 验证步骤: + 1. ✅ 编译检查:python -m py_compile core/vr/performance/monitoring.py + 2. ✅ 导入测试:启动应用,确认无导入错误 + 3. ✅ 功能测试:调用 vr_manager.enable_performance_monitoring() + 4. ✅ 输出验证:检查性能报告正常生成 + 5. ✅ API测试:验证所有委托方法可用 + + --- + 🔷 阶段2:拆分测试调试系统 (2-3小时) + + 创建文件:core/vr/testing/test_mode.py (~800行) + + 迁移方法 (17个): + - 测试模式:enable_vr_test_mode, disable_vr_test_mode, switch_test_display_mode + - 纹理管理:_ensure_test_mode_textures, _create_cached_ovr_textures, _batch_submit_textures + - 显示系统:_initialize_test_display, _update_test_display, _create_stereo_display, _cleanup_test_display + - HUD系统:_initialize_test_performance_hud, _update_test_performance_hud, _cleanup_test_performance_hud + - 状态查询:get_test_mode_status, get_test_mode_features, set_test_mode_features + - 性能测试:run_vr_performance_test + + 迁移属性 (~15个): + vr_test_mode, test_display_mode + test_display_quad, test_right_quad, stereo_display_created + test_performance_hud, test_performance_text + test_mode_initialized + hud_update_counter, hud_update_interval + test_mode_submit_texture, test_mode_wait_poses + + 类结构: + class VRTestMode: + """VR测试模式系统""" + def __init__(self, vr_manager): + self.vr_manager = vr_manager + + 验证步骤: + 1. ✅ 启用测试模式:vr_manager.enable_vr_test_mode('stereo') + 2. ✅ 检查显示:验证测试quad显示正确 + 3. ✅ HUD验证:检查性能HUD显示 + 4. ✅ 切换模式:测试 left/right/stereo 模式切换 + 5. ✅ 禁用测试:vr_manager.disable_vr_test_mode() + + --- + 🔷 阶段3:拆分对象池和优化系统 (1-2小时) + + 创建文件:core/vr/performance/optimization.py (~300行) + + 迁移方法 (19个): + - 对象池:_initialize_object_pools, _get_pooled_matrix, _return_pooled_matrix + - GC控制:_manual_gc_control, enable_gc_control, disable_gc_control, set_manual_gc_interval, force_manual_gc + - 分辨率:set_resolution_scale, set_quality_preset, cycle_quality_preset, _apply_resolution_scale + - 查询方法:get_object_pool_status, get_resolution_info, print_resolution_info + - 性能模式:enable_performance_mode, disable_performance_mode, set_performance_mode_trigger_frame, get_performance_mode_status + + 迁移属性 (~20个): + # 对象池 + _matrix_pool, _matrix_pool_size + _cached_matrices, _controller_poses_cache + _left_ovr_texture, _right_ovr_texture + + # GC控制 + _gc_control_enabled, _gc_disabled + _manual_gc_interval, _last_manual_gc_frame + + # 分辨率 + resolution_scale, base_eye_width, base_eye_height + scaled_eye_width, scaled_eye_height + quality_presets, current_quality_preset + + # 性能模式 + performance_mode_enabled, performance_mode_trigger_frame + + 验证步骤: + 1. ✅ 对象池:检查Mat4对象池正常工作 + 2. ✅ GC控制:验证手动GC按预期触发 + 3. ✅ 分辨率:测试质量预设切换 + 4. ✅ 性能测试:运行30秒性能测试,确认优化生效 + + --- + 🔷 阶段4:拆分跟踪系统 (3-4小时) + + 创建文件1:core/vr/tracking/poses.py (~600行) + + 迁移方法 (12个): + - 姿态获取:_wait_get_poses, _wait_get_poses_immediate, _wait_get_poses_with_prediction + - 姿态缓存:_cache_poses_for_next_frame, _reset_waitgetposes_flag + - 姿态更新:_update_tracking_data, update_hmd, _update_camera_poses, _update_camera_poses_with_cache + - 矩阵转换:_convert_openvr_matrix_to_panda, _update_matrix_from_openvr, convert_mat + + 迁移属性: + hmd_pose, controller_poses, tracked_device_poses + poses, game_poses + tracking_space, hmd_anchor, left_eye_anchor, right_eye_anchor + coord_mat, coord_mat_inv + use_prediction_time, poses_updated_in_task + _waitgetposes_called_this_frame + _cached_render_poses, _first_frame + + 创建文件2:core/vr/tracking/devices.py (~300行) + + 迁移方法 (8个): + - 控制器:_initialize_controllers, _detect_controllers, get_controller_by_role + - 设备管理:_create_tracked_device_anchor, update_tracked_devices + - 状态查询:are_controllers_connected, get_connected_controllers + - 震动:trigger_controller_haptic + + 迁移属性: + left_controller, right_controller + controllers, tracked_device_anchors + + 创建文件3:core/vr/tracking/input_wrapper.py (~200行) + + 迁移方法 (12个): + - 按钮查询:is_trigger_pressed, is_trigger_just_pressed, is_grip_pressed, is_grip_just_pressed, is_menu_pressed + - 触摸板:is_trackpad_touched, get_trackpad_position + - 交互查询:get_selected_object, get_grabbed_object, is_grabbing_object + - 交互控制:force_release_all_grabs, add_interactable_object + + 类结构: + class VRPoseTracker: + """VR姿态跟踪系统""" + + class VRDeviceManager: + """VR设备管理系统""" + + class VRInputWrapper: + """VR输入包装层""" + + 验证步骤: + 1. ✅ 姿态跟踪:启动VR,检查头显跟踪正常 + 2. ✅ 控制器检测:验证控制器正确识别 + 3. ✅ 输入测试:测试按钮和触摸板输入 + 4. ✅ 震动测试:触发控制器震动反馈 + + --- + 🔷 阶段5:拆分渲染系统 (4-5小时) + + 创建文件1:core/vr/rendering/buffers.py (~400行) + + 迁移方法 (10个): + - 缓冲区创建:_create_vr_buffers, _create_vr_buffer, _create_vr_buffers_with_pipeline + - 纹理管理:_create_vr_texture, _prepare_and_cache_textures + - Pipeline效果:_apply_pipeline_vr_effects + - 天空盒:_check_skybox_status, _create_vr_skybox + - 诊断:_diagnose_buffer_performance + - 清理:_cleanup_vr_buffers + + 迁移属性: + vr_left_eye_buffer, vr_right_eye_buffer + vr_left_texture, vr_right_texture + left_texture_id, right_texture_id, textures_prepared + eye_width, eye_height, near_clip, far_clip + scaled_eye_width, scaled_eye_height + + 创建文件2:core/vr/rendering/cameras.py (~350行) + + 迁移方法 (6个): + - 相机设置:_setup_vr_cameras, _get_eye_offset + - 优化:_optimize_vr_rendering, _apply_lightweight_rendering, _disable_vr_buffer_extras + - 主相机:_disable_main_cam, _enable_main_cam + + 迁移属性: + vr_left_camera, vr_right_camera + + 创建文件3:core/vr/rendering/compositor.py (~300行) + + 迁移方法 (9个): + - 渲染回调:simple_left_cb, simple_right_cb + - 纹理提交:submit_texture + - GPU同步:_sync_gpu_if_needed, _smart_gpu_sync + - ATW控制:_disable_async_reprojection, enable_async_reprojection_disable, disable_async_reprojection_disable + + 迁移属性: + vr_compositor, submit_together + openvr_frame_id + left_eye_last_render_frame, right_eye_last_render_frame + disable_async_reprojection + + 创建文件4:core/vr/rendering/pipeline.py (~250行) + + 迁移方法 (4个): + - Pipeline集成:_create_vr_buffers_with_pipeline, _apply_pipeline_vr_effects + - 模式切换:set_vr_render_mode, get_vr_render_mode + + 迁移属性: + vr_render_mode, render_pipeline_enabled + vr_pipeline_left_target, vr_pipeline_right_target + pipeline_resolution_scale, vr_pipeline_controller + pipeline_vr_config + + 验证步骤: + 1. ✅ 缓冲区:检查左右眼缓冲区正确创建 + 2. ✅ 相机:验证双眼相机位置和视锥 + 3. ✅ 渲染:测试左右眼渲染回调 + 4. ✅ 提交:确认纹理正确提交到OpenVR + 5. ✅ Pipeline:测试RenderPipeline模式切换 + + --- + 🔷 阶段6:重构核心管理器 (2-3小时) + + 保留在 core/vr_manager.py (~500行) + + 保留方法 (10个核心方法): + - 生命周期:__init__, cleanup + - VR初始化:is_vr_available, initialize_vr + - VR控制:enable_vr, disable_vr + - 主循环:_start_vr_task, _update_vr + - 状态查询:get_vr_status + + 新增子系统属性(组合模式): + def __init__(self, world): + # ... 基础初始化 ... + + # 子系统初始化(按依赖顺序) + self.optimization = VROptimization(self) + self.performance_monitor = VRPerformanceMonitor(self) + self.test_mode = VRTestMode(self) + + self.pose_tracker = VRPoseTracker(self) + self.device_manager = VRDeviceManager(self) + self.input_wrapper = VRInputWrapper(self) + + self.buffer_manager = VRBufferManager(self) + self.camera_manager = VRCameraManager(self) + self.compositor = VRCompositor(self) + self.pipeline_manager = VRPipelineManager(self) + + 委托方法(保持API兼容): + # 性能监控委托 + def enable_performance_monitoring(self): + return self.performance_monitor.enable_performance_monitoring() + + # 测试模式委托 + def enable_vr_test_mode(self, display_mode='stereo'): + return self.test_mode.enable_vr_test_mode(display_mode) + + # 优化系统委托 + def set_resolution_scale(self, scale): + return self.optimization.set_resolution_scale(scale) + + # ... 其他委托方法 ... + + 验证步骤: + 1. ✅ 完整启动:启动应用,测试VR完整流程 + 2. ✅ API测试:验证所有公开API可用 + 3. ✅ 性能测试:运行性能测试,确认无回退 + 4. ✅ 集成测试:测试传送、交互、渲染等所有功能 + + --- + ✅ 每阶段验证清单 + + 静态验证 + + - python -m py_compile 检查语法 + - python main.py --help 验证模块导入 + - 运行 pylint 检查代码质量 + + 功能验证 + + - 启动应用:python main.py + - 检查VR可用性:vr_manager.is_vr_available() + - 测试VR启用:切换到VR模式 + - 测试头显跟踪:移动头显查看跟踪 + - 测试控制器:检测和交互 + - 测试传送:使用控制器传送 + - 测试性能监控:查看性能报告 + - 测试VR禁用:退出VR模式 + + 性能验证 + + - 运行性能测试:vr_manager.run_vr_performance_test(30) + - 检查帧率稳定在90fps + - 对比拆分前后性能指标 + - 确认无性能回退 + + --- + 🛡️ 风险控制 + + 备份策略 + + # 每个阶段开始前 + cp core/vr_manager.py core/vr_manager.py.backup.stage{N} + + 回滚方案 + + # 恢复备份 + cp core/vr_manager.py.backup.stage{N} core/vr_manager.py + + 循环依赖预防 + + - ✅ 所有子模块通过 self.vr_manager 访问其他子系统 + - ✅ 子模块间不直接引用 + - ✅ VRManager 作为中介协调所有子系统 + + 性能保护 + + - ✅ 热点路径(渲染回调)保持直接调用 + - ✅ 非关键路径使用委托模式 + - ✅ 对象池和缓存机制保持不变 + + --- + 📊 预期成果 + + 代码结构 + + core/vr_manager.py (500行) ✅ + core/vr/ + ├── performance/ + │ ├── monitoring.py (900行) ✅ + │ └── optimization.py (300行) ✅ + ├── testing/ + │ └── test_mode.py (800行) ✅ + ├── tracking/ + │ ├── poses.py (600行) ✅ + │ ├── devices.py (300行) ✅ + │ └── input_wrapper.py (200行) ✅ + └── rendering/ + ├── buffers.py (400行) ✅ + ├── cameras.py (350行) ✅ + ├── compositor.py (300行) ✅ + └── pipeline.py (250行) ✅ + + 改进指标 + + - ✅ 文件行数:4736行 → 最大900行(符合规范) + - ✅ 目录文件数:所有目录 ≤ 4个文件(远小于8个限制) + - ✅ 方法数:138个 → 每个类 ≤ 20个方法 + - ✅ 职责清晰:每个模块单一职责 + - ✅ 可维护性:大幅提升 + - ✅ 可测试性:模块化便于单元测试 + - ✅ 向后兼容:100%保持现有API + + 时间估算 + + - 阶段1: 2-3小时 + - 阶段2: 2-3小时 + - 阶段3: 1-2小时 + - 阶段4: 3-4小时 + - 阶段5: 4-5小时 + - 阶段6: 2-3小时 + + 总计:15-20小时,分6个阶段逐步完成 + + --- + 🚀 开始执行 + + 确认此计划后,将按以下顺序执行: + 1. 创建备份 + 2. 阶段1:拆分性能监控系统 + 3. 验证通过后,进入阶段2 + 4. 依次完成所有6个阶段 5. 最终整体测试和文档更新 \ No newline at end of file diff --git a/build_windows.bat b/build_windows.bat index e84fdabe..c323ff85 100755 --- a/build_windows.bat +++ b/build_windows.bat @@ -1,12 +1,12 @@ -@echo off -set PYTHONPATH=%~dp0_internal -pyinstaller --onedir --windowed --name=EG_Engine --exclude-module pyassimp --add-data="RenderPipelineFile;RenderPipelineFile" --add-data="QMeta3D;QMeta3D" --add-data="core;core" --add-data="gui;gui" --add-data="ui;ui" --add-data="scene;scene" --add-data="project;project" --add-data="demo;demo" --add-data="plugins;plugins" --add-data="scripts;scripts" --hidden-import=PyQt5.sip --hidden-import=panda3d.core main.py - -echo @echo off > dist\EG_Engine\run.bat -echo set PYTHONPATH=%%~dp0_internal >> dist\EG_Engine\run.bat -echo cd .. >> dist\EG_Engine\run.bat -echo cd .. >> dist\EG_Engine\run.bat -echo python main.py >> dist\EG_Engine\run.bat -echo pause >> dist\EG_Engine\run.bat - +@echo off +set PYTHONPATH=%~dp0_internal +pyinstaller --onedir --windowed --name=EG_Engine --exclude-module pyassimp --add-data="RenderPipelineFile;RenderPipelineFile" --add-data="QMeta3D;QMeta3D" --add-data="core;core" --add-data="gui;gui" --add-data="ui;ui" --add-data="scene;scene" --add-data="project;project" --add-data="demo;demo" --add-data="plugins;plugins" --add-data="scripts;scripts" --hidden-import=PyQt5.sip --hidden-import=panda3d.core main.py + +echo @echo off > dist\EG_Engine\run.bat +echo set PYTHONPATH=%%~dp0_internal >> dist\EG_Engine\run.bat +echo cd .. >> dist\EG_Engine\run.bat +echo cd .. >> dist\EG_Engine\run.bat +echo python main.py >> dist\EG_Engine\run.bat +echo pause >> dist\EG_Engine\run.bat + pause \ No newline at end of file diff --git a/config/vr_settings.json b/config/vr_settings.json index 902d31fb..1688b9bf 100644 --- a/config/vr_settings.json +++ b/config/vr_settings.json @@ -1,15 +1,15 @@ -{ - "render_mode": "render_pipeline", - "resolution_scale": 0.75, - "pipeline_resolution_scale": 0.75, - "quality_preset": "quality", - "pipeline_vr_config": { - "enable_shadows": true, - "enable_ao": true, - "enable_bloom": false, - "enable_motion_blur": false, - "enable_ssr": false, - "shadow_quality": "medium", - "ao_quality": "low" - } +{ + "render_mode": "render_pipeline", + "resolution_scale": 0.75, + "pipeline_resolution_scale": 0.75, + "quality_preset": "quality", + "pipeline_vr_config": { + "enable_shadows": true, + "enable_ao": true, + "enable_bloom": false, + "enable_motion_blur": false, + "enable_ssr": false, + "shadow_quality": "medium", + "ao_quality": "low" + } } \ No newline at end of file diff --git a/core/Command_System.py b/core/Command_System.py index fd8d245b..daaf60ed 100644 --- a/core/Command_System.py +++ b/core/Command_System.py @@ -1,648 +1,648 @@ -from abc import ABC, abstractmethod -from collections import deque -from typing import List -from panda3d.core import NodePath, Point3 - - -class Command(ABC): - """ - 抽象命令类,所有具体命令都需要继承此类 - """ - - @abstractmethod - def execute(self): - """ - 执行命令 - """ - pass - - @abstractmethod - def undo(self): - """ - 撤销命令 - """ - pass - - @abstractmethod - def redo(self): - """ - 重做命令 - """ - pass - - -class CommandManager: - """ - 命令管理器,负责管理命令的执行、撤销和重做 - """ - - def __init__(self, max_history: int = 100): - # 用于存储已执行的命令的历史记录 - self._undo_stack: deque = deque(maxlen=max_history) - # 用于存储已撤销的命令,支持重做 - self._redo_stack: deque = deque(maxlen=max_history) - # 最大历史记录数 - self._max_history = max_history - - def execute_command(self, command: Command): - """ - 执行命令,并将其添加到撤销栈中 - """ - try: - command.execute() - self._undo_stack.append(command) - # 清空重做栈,因为执行新命令后就无法重做之前的命令了 - self._redo_stack.clear() - except Exception as e: - print(f"执行命令时出错: {e}") - raise - - def undo(self) -> bool: - """ - 撤销上一个命令 - 返回是否成功撤销 - """ - if not self._undo_stack: - return False - - try: - command = self._undo_stack.pop() - command.undo() - self._redo_stack.append(command) - return True - except Exception as e: - print(f"撤销命令时出错: {e}") - # 如果撤销失败,将命令放回撤销栈 - self._undo_stack.append(command) - return False - - def redo(self) -> bool: - """ - 重做上一个被撤销的命令 - 返回是否成功重做 - """ - if not self._redo_stack: - return False - - try: - command = self._redo_stack.pop() - command.redo() - self._undo_stack.append(command) - return True - except Exception as e: - print(f"重做命令时出错: {e}") - # 如果重做失败,将命令放回重做栈 - self._redo_stack.append(command) - return False - - def can_undo(self) -> bool: - """ - 检查是否可以撤销 - """ - return len(self._undo_stack) > 0 - - def can_redo(self) -> bool: - """ - 检查是否可以重做 - """ - return len(self._redo_stack) > 0 - - def clear_history(self): - """ - 清空所有历史记录 - """ - self._undo_stack.clear() - self._redo_stack.clear() - - def get_undo_count(self) -> int: - """ - 获取可撤销的命令数量 - """ - return len(self._undo_stack) - - def get_redo_count(self) -> int: - """ - 获取可重做的命令数量 - """ - return len(self._redo_stack) - - -# 示例命令实现 -class MoveNodeCommand(Command): - """ - 移动节点命令示例 - """ - - def __init__(self, node: NodePath, old_pos, new_pos): - self.node = node - self.old_pos = old_pos - self.new_pos = new_pos - - def execute(self): - """ - 执行移动操作 - """ - self.node.setPos(self.new_pos) - - def undo(self): - """ - 撤销移动操作 - """ - self.node.setPos(self.old_pos) - - def redo(self): - """ - 重做移动操作 - """ - self.node.setPos(self.new_pos) - - -class DeleteNodeCommand(Command): - """ - 删除节点命令示例 - """ - - def __init__(self, node: NodePath, parent_node: NodePath,world=None): - self.node = node - self.parent_node = parent_node - self.world = world - - self.node_name = node.getName() - self.node_pos = node.getPos() - self.node_hpr = node.getHpr() - self.node_scale = node.getScale() - - # 保存节点类型信息 - self.node_type = "NODE" - if node.hasTag("tree_item_type"): - self.node_type = node.getTag("tree_item_type") - elif node.hasTag("gui_type"): - gui_type = node.getTag("gui_type") - if gui_type == "button": - self.node_type = "GUI_BUTTON" - elif gui_type == "label": - self.node_type = "GUI_LABEL" - elif gui_type == "entry": - self.node_type = "GUI_ENTRY" - elif gui_type == "2d_image": - self.node_type = "GUI_IMAGE" - elif gui_type == "3d_text": - self.node_type = "GUI_3DTEXT" - elif gui_type == "3d_image": - self.node_type = "GUI_3DIMAGE" - elif gui_type == "video_screen": - self.node_type = "GUI_VIDEO_SCREEN" - elif gui_type == "2d_video_screen": - self.node_type = "GUI_2D_VIDEO_SCREEN" - elif node.hasTag("light_type"): - self.node_type = "LIGHT_NODE" - elif node.hasTag("element_type") and node.getTag("element_type") == "cesium_tileset": - self.node_type = "CESIUM_TILESET_NODE" - elif node.hasTag("is_scene_element"): - self.node_type = "SCENE_NODE" - - self.node_tags = {} - if hasattr(node,'hasTag'): - for tag_key in node.getNetTag('tags').split(',') if node.hasTag('tags') else []: - if node.hasTag(tag_key): - self.node_tags[tag_key] = node.getTag(tag_key) - else: - try: - if hasattr(node,'getTag'): - common_tags = ['is_scene_element','tree_item_type','gui_type','light_type', - 'element_type','file','model_path','video_path','image_path', - 'gui_text','name','created_by_user'] - for tag in common_tags: - if node.hasTag(tag): - self.node_tags[tag] = node.getTag(tag) - except: - pass - - self.node_python_tags = {} - if hasattr(node,'getPythonTagKeys'): - try: - for tag_key in node.getPythonTagKeys(): - self.node_python_tags[tag_key] = node.getPythonTag(tag_key) - except Exception as e: - pass - - # 对于特定类型的节点,保存额外的数据 - self.extra_data = {} - if self.node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE", - "GUI_3DTEXT", "GUI_3DIMAGE", "GUI_VIDEO_SCREEN", "GUI_2D_VIDEO_SCREEN"]: - if node.hasTag("gui_text"): - self.extra_data["gui_text"] = node.getTag("gui_text") - if node.hasTag("video_path"): - self.extra_data["video_path"] = node.getTag("video_path") - if node.hasTag("image_path"): - self.extra_data["image_path"] = node.getTag("image_path") - elif self.node_type == "LIGHT_NODE": - if node.hasTag("light_type"): - self.extra_data["light_type"] = node.getTag("light_type") - rp_light = node.getPythonTag("rp_light_object") - if rp_light: - self.extra_data["light_data"] = { - 'energy': getattr(rp_light, 'energy', 5000), - 'radius': getattr(rp_light, 'radius', 1000), - 'fov': getattr(rp_light, 'fov', 70) if hasattr(rp_light, 'fov') else 70, - 'inner_radius': getattr(rp_light, 'inner_radius', 0.4) if hasattr(rp_light, - 'inner_radius') else 0.4, - 'casts_shadows': getattr(rp_light, 'casts_shadows', True), - 'shadow_map_resolution': getattr(rp_light, 'shadow_map_resolution', 256) - } - elif self.node_type == "CESIUM_TILESET_NODE": - if node.hasTag("tileset_url"): - self.extra_data["tileset_url"] = node.getTag("tileset_url") - - def execute(self): - """ - 执行删除操作 - """ - # 从world的相应列表中移除节点引用 - if self.world and hasattr(self.world, 'scene_manager'): - scene_manager = self.world.scene_manager - if self.node_type == "LIGHT_NODE": - if self.node.hasTag("light_type"): - light_type = self.node.getTag("light_type") - if light_type == "spot_light" and hasattr(scene_manager, - 'Spotlight') and self.node in scene_manager.Spotlight: - scene_manager.Spotlight.remove(self.node) - elif light_type == "point_light" and hasattr(scene_manager, - 'Pointlight') and self.node in scene_manager.Pointlight: - scene_manager.Pointlight.remove(self.node) - elif self.node_type == "IMPORTED_MODEL_NODE" and hasattr(scene_manager, - 'models') and self.node in scene_manager.models: - scene_manager.models.remove(self.node) - elif self.node_type.startswith("GUI_") and hasattr(self.world, - 'gui_elements') and self.node in self.world.gui_elements: - self.world.gui_elements.remove(self.node) - elif self.node_type == "CESIUM_TILESET_NODE": - # 从tilesets列表中移除 - if hasattr(scene_manager, 'tilesets'): - tilesets_to_remove = [] - for i, tileset_info in enumerate(scene_manager.tilesets): - if tileset_info.get('node') == self.node: - tilesets_to_remove.append(i) - for i in reversed(tilesets_to_remove): - del scene_manager.tilesets[i] - - # 从场景图中移除节点 - if self.node and not self.node.isEmpty(): - self.node.removeNode() - - def undo(self): - """ - 撤销删除操作(重新创建节点) - """ - try: - # 使用场景管理器重建节点 - if self.world and hasattr(self.world, 'scene_manager'): - scene_manager = self.world.scene_manager - - # 创建节点数据字典 - node_data = { - 'name': self.node_name, - 'node_type': self.node_type, - 'pos': (self.node_pos.x, self.node_pos.y, self.node_pos.z), - 'hpr': (self.node_hpr.x, self.node_hpr.y, self.node_hpr.z), - 'scale': (self.node_scale.x, self.node_scale.y, self.node_scale.z), - 'tags': self.node_tags - } - - # 添加额外数据 - if self.extra_data: - if self.node_type.startswith("GUI_"): - node_data['gui_data'] = self.extra_data - elif self.node_type == "LIGHT_NODE": - node_data['light_data'] = self.extra_data.get('light_data', {}) - elif self.node_type == "CESIUM_TILESET_NODE": - node_data['tileset_url'] = self.extra_data.get('tileset_url', '') - - # 重建节点 - new_node = scene_manager.recreateNodeFromData(node_data, self.parent_node) - - if new_node: - print(f"✅ 成功撤销删除操作,节点 {self.node_name} 已恢复") - # 更新节点引用 - self.node = new_node - else: - print(f"❌ 撤销删除操作失败,无法重建节点 {self.node_name}") - else: - print("❌ 无法撤销删除操作,缺少场景管理器引用") - - except Exception as e: - print(f"❌ 撤销删除操作时出错: {e}") - import traceback - traceback.print_exc() - - def redo(self): - """ - 重做删除操作 - """ - self.execute() - - -class RotateNodeCommand(Command): - """ - 旋转节点命令 - """ - - def __init__(self, node: NodePath, old_hpr, new_hpr): - self.node = node - self.old_hpr = old_hpr - self.new_hpr = new_hpr - - def execute(self): - """ - 执行旋转操作 - """ - self.node.setHpr(self.new_hpr) - - def undo(self): - """ - 撤销旋转操作 - """ - self.node.setHpr(self.old_hpr) - - def redo(self): - """ - 重做旋转操作 - """ - self.node.setHpr(self.new_hpr) - - -class ScaleNodeCommand(Command): - """ - 缩放节点命令 - """ - - def __init__(self, node: NodePath, old_scale, new_scale): - self.node = node - self.old_scale = old_scale - self.new_scale = new_scale - - def execute(self): - """ - 执行缩放操作 - """ - self.node.setScale(self.new_scale) - - def undo(self): - """ - 撤销缩放操作 - """ - self.node.setScale(self.old_scale) - - def redo(self): - """ - 重做缩放操作 - """ - self.node.setScale(self.new_scale) - - -class CreateNodeCommand(Command): - """ - 创建节点命令 - """ - - def __init__(self, node_creator_func,parent_node, *args, **kwargs): - self.node_creator_func = node_creator_func - self.parent_node = parent_node - self.args = args - self.kwargs = kwargs - self.created_node = None - - def execute(self): - """ - 执行创建节点操作 - """ - self.created_node = self.node_creator_func(self.parent_node,*self.args, **self.kwargs) - return self.created_node - - def undo(self): - """ - 撤销创建节点操作 - """ - if self.created_node: - self.created_node.removeNode() - - def redo(self): - """ - 重做创建节点操作 - """ - self.execute() - - -class ReparentNodeCommand(Command): - """ - 重新设置节点父子关系命令 - 增强版(同时处理Panda3D和Qt树) - """ - - def __init__(self, node: NodePath, old_parent: NodePath, new_parent: NodePath, - old_parent_item=None, new_parent_item=None, is_2d_gui=False, world=None): - self.node = node - self.old_parent = old_parent - self.new_parent = new_parent - self.old_parent_item = old_parent_item # Qt树中的旧父节点项 - self.new_parent_item = new_parent_item # Qt树中的新父节点项 - self.is_2d_gui = is_2d_gui - self.world = world - - # 保存节点在操作前的世界坐标和局部坐标,以便在撤销/重做时保持位置不变 - self.world_pos = node.getPos(self.world.render if self.world else node.getParent()) - self.world_hpr = node.getHpr(self.world.render if self.world else node.getParent()) - self.world_scale = node.getScale(self.world.render if self.world else node.getParent()) - # 同时保存局部坐标,因为在父节点改变后可能需要恢复 - self.local_pos = node.getPos() - self.local_hpr = node.getHpr() - self.local_scale = node.getScale() - - def _updateQtTree(self, node_item, new_parent_item): - """更新Qt树控件中的节点位置""" - if not node_item or not new_parent_item: - return - - # 从当前父节点中移除 - current_parent = node_item.parent() - if current_parent: - current_parent.removeChild(node_item) - else: - # 如果是顶级项目 - tree_widget = node_item.treeWidget() - if tree_widget: - index = tree_widget.indexOfTopLevelItem(node_item) - if index >= 0: - tree_widget.takeTopLevelItem(index) - - # 添加到新父节点 - new_parent_item.addChild(node_item) - - def execute(self): - """ - 执行重新父化操作 - """ - # 更新Panda3D节点父子关系 - if self.is_2d_gui and self.world: - # 2D GUI元素需要特殊处理 - if self.new_parent and not self.new_parent.isEmpty(): - if hasattr(self.new_parent, 'getTag') and self.new_parent.getTag("is_gui_element") == "1": - # 目标是GUI元素,直接重新父化 - self.node.wrtReparentTo(self.new_parent) - else: - # 目标是3D节点,保持GUI特性,重新父化到aspect2d - self.node.wrtReparentTo(self.world.aspect2d) - print(f"2D GUI元素保持在aspect2d下") - else: - # 如果新父节点为None,重新父化到aspect2d - self.node.wrtReparentTo(self.world.aspect2d) - print(f"2D GUI元素重新父化到aspect2d") - else: - # 普通3D节点的处理 - if self.new_parent and not self.new_parent.isEmpty(): - self.node.wrtReparentTo(self.new_parent) - else: - # 如果新父节点为空,将其父化到render节点 - if self.world: - self.node.wrtReparentTo(self.world.render) - else: - # 备用方案 - from panda3d.core import NodePath - self.node.wrtReparentTo(NodePath("render")) - - def undo(self): - """ - 撤销重新父化操作 - """ - # 在改变父节点前保存当前的缩放值 - current_scale = self.node.getScale() - - # 恢复Panda3D节点父子关系 - if self.is_2d_gui and self.world: - # 2D GUI元素需要特殊处理 - if self.old_parent and not self.old_parent.isEmpty(): - if hasattr(self.old_parent, 'getTag') and self.old_parent.getTag("is_gui_element") == "1": - # 原父节点是GUI元素,直接重新父化 - self.node.wrtReparentTo(self.old_parent) - else: - # 原父节点是3D节点,保持GUI特性,重新父化到aspect2d - self.node.wrtReparentTo(self.world.aspect2d) - print(f"2D GUI元素恢复到aspect2d下") - else: - # 如果原父节点为空,重新父化到aspect2d - self.node.wrtReparentTo(self.world.aspect2d) - print(f"2D GUI元素恢复到aspect2d") - else: - # 普通3D节点的处理 - if self.old_parent and not self.old_parent.isEmpty(): - self.node.wrtReparentTo(self.old_parent) - else: - # 如果原父节点为空,将其父化到render节点 - if self.world: - self.node.wrtReparentTo(self.world.render) - else: - # 备用方案 - from panda3d.core import NodePath - self.node.wrtReparentTo(NodePath("render")) - - # 恢复局部坐标(不是世界坐标),因为父节点已经改变 - self.node.setPos(self.local_pos) - self.node.setHpr(self.local_hpr) - # 特别处理缩放,确保GUI元素的缩放不会异常变化 - if not self.is_2d_gui or abs(current_scale.length() - self.local_scale.length()) > 0.001: - self.node.setScale(self.local_scale) - - def redo(self): - """ - 重做重新父化操作 - """ - # 在改变父节点前保存当前的缩放值 - current_scale = self.node.getScale() - - # 重新执行Panda3D节点父子关系更新 - if self.is_2d_gui and self.world: - # 2D GUI元素需要特殊处理 - if self.new_parent and not self.new_parent.isEmpty(): - if hasattr(self.new_parent, 'getTag') and self.new_parent.getTag("is_gui_element") == "1": - # 目标是GUI元素,直接重新父化 - self.node.wrtReparentTo(self.new_parent) - else: - # 目标是3D节点,保持GUI特性,重新父化到aspect2d - self.node.wrtReparentTo(self.world.aspect2d) - print(f"2D GUI元素保持在aspect2d下") - else: - # 如果新父节点为None,重新父化到aspect2d - self.node.wrtReparentTo(self.world.aspect2d) - print(f"2D GUI元素重新父化到aspect2d") - else: - # 普通3D节点的处理 - if self.new_parent and not self.new_parent.isEmpty(): - self.node.wrtReparentTo(self.new_parent) - else: - # 如果新父节点为空,将其父化到render节点 - if self.world: - self.node.wrtReparentTo(self.world.render) - else: - # 备用方案 - from panda3d.core import NodePath - self.node.wrtReparentTo(NodePath("render")) - - # 恢复局部坐标(不是世界坐标),因为父节点已经改变 - self.node.setPos(self.local_pos) - self.node.setHpr(self.local_hpr) - # 特别处理缩放,确保GUI元素的缩放不会异常变化 - if not self.is_2d_gui or abs(current_scale.length() - self.local_scale.length()) > 0.001: - self.node.setScale(self.local_scale) - - -class CompositeCommand(Command): - """ - 组合命令类,用于同时执行多个命令 - """ - def __init__(self,commands:List[Command]): - self.commands = commands - - def execute(self): - """ - 执行所有命令 - """ - for command in self.commands: - command.execute() - - def undo(self): - """ - 撤销所有命令(逆序执行) - """ - for command in reversed(self.commands): - command.undo() - - def redo(self): - """ - 重做所有命令 - """ - for command in self.commands: - command.redo() - -class MoveLightCommand(Command): - def __init__(self, node, old_pos, new_pos, light_object=None): - self.node = node - self.old_pos = Point3(old_pos) - self.new_pos = Point3(new_pos) - self.light_object = light_object - - def execute(self): # 将原来的 do() 改为 execute() - if self.light_object: - self.light_object.pos = self.new_pos - if self.node: - self.node.setPos(self.new_pos) - - def undo(self): - if self.light_object: - self.light_object.pos = self.old_pos - if self.node: - self.node.setPos(self.old_pos) - - def redo(self): - self.execute() # 调用 execute() 而不是 do() - - - +from abc import ABC, abstractmethod +from collections import deque +from typing import List +from panda3d.core import NodePath, Point3 + + +class Command(ABC): + """ + 抽象命令类,所有具体命令都需要继承此类 + """ + + @abstractmethod + def execute(self): + """ + 执行命令 + """ + pass + + @abstractmethod + def undo(self): + """ + 撤销命令 + """ + pass + + @abstractmethod + def redo(self): + """ + 重做命令 + """ + pass + + +class CommandManager: + """ + 命令管理器,负责管理命令的执行、撤销和重做 + """ + + def __init__(self, max_history: int = 100): + # 用于存储已执行的命令的历史记录 + self._undo_stack: deque = deque(maxlen=max_history) + # 用于存储已撤销的命令,支持重做 + self._redo_stack: deque = deque(maxlen=max_history) + # 最大历史记录数 + self._max_history = max_history + + def execute_command(self, command: Command): + """ + 执行命令,并将其添加到撤销栈中 + """ + try: + command.execute() + self._undo_stack.append(command) + # 清空重做栈,因为执行新命令后就无法重做之前的命令了 + self._redo_stack.clear() + except Exception as e: + print(f"执行命令时出错: {e}") + raise + + def undo(self) -> bool: + """ + 撤销上一个命令 + 返回是否成功撤销 + """ + if not self._undo_stack: + return False + + try: + command = self._undo_stack.pop() + command.undo() + self._redo_stack.append(command) + return True + except Exception as e: + print(f"撤销命令时出错: {e}") + # 如果撤销失败,将命令放回撤销栈 + self._undo_stack.append(command) + return False + + def redo(self) -> bool: + """ + 重做上一个被撤销的命令 + 返回是否成功重做 + """ + if not self._redo_stack: + return False + + try: + command = self._redo_stack.pop() + command.redo() + self._undo_stack.append(command) + return True + except Exception as e: + print(f"重做命令时出错: {e}") + # 如果重做失败,将命令放回重做栈 + self._redo_stack.append(command) + return False + + def can_undo(self) -> bool: + """ + 检查是否可以撤销 + """ + return len(self._undo_stack) > 0 + + def can_redo(self) -> bool: + """ + 检查是否可以重做 + """ + return len(self._redo_stack) > 0 + + def clear_history(self): + """ + 清空所有历史记录 + """ + self._undo_stack.clear() + self._redo_stack.clear() + + def get_undo_count(self) -> int: + """ + 获取可撤销的命令数量 + """ + return len(self._undo_stack) + + def get_redo_count(self) -> int: + """ + 获取可重做的命令数量 + """ + return len(self._redo_stack) + + +# 示例命令实现 +class MoveNodeCommand(Command): + """ + 移动节点命令示例 + """ + + def __init__(self, node: NodePath, old_pos, new_pos): + self.node = node + self.old_pos = old_pos + self.new_pos = new_pos + + def execute(self): + """ + 执行移动操作 + """ + self.node.setPos(self.new_pos) + + def undo(self): + """ + 撤销移动操作 + """ + self.node.setPos(self.old_pos) + + def redo(self): + """ + 重做移动操作 + """ + self.node.setPos(self.new_pos) + + +class DeleteNodeCommand(Command): + """ + 删除节点命令示例 + """ + + def __init__(self, node: NodePath, parent_node: NodePath,world=None): + self.node = node + self.parent_node = parent_node + self.world = world + + self.node_name = node.getName() + self.node_pos = node.getPos() + self.node_hpr = node.getHpr() + self.node_scale = node.getScale() + + # 保存节点类型信息 + self.node_type = "NODE" + if node.hasTag("tree_item_type"): + self.node_type = node.getTag("tree_item_type") + elif node.hasTag("gui_type"): + gui_type = node.getTag("gui_type") + if gui_type == "button": + self.node_type = "GUI_BUTTON" + elif gui_type == "label": + self.node_type = "GUI_LABEL" + elif gui_type == "entry": + self.node_type = "GUI_ENTRY" + elif gui_type == "2d_image": + self.node_type = "GUI_IMAGE" + elif gui_type == "3d_text": + self.node_type = "GUI_3DTEXT" + elif gui_type == "3d_image": + self.node_type = "GUI_3DIMAGE" + elif gui_type == "video_screen": + self.node_type = "GUI_VIDEO_SCREEN" + elif gui_type == "2d_video_screen": + self.node_type = "GUI_2D_VIDEO_SCREEN" + elif node.hasTag("light_type"): + self.node_type = "LIGHT_NODE" + elif node.hasTag("element_type") and node.getTag("element_type") == "cesium_tileset": + self.node_type = "CESIUM_TILESET_NODE" + elif node.hasTag("is_scene_element"): + self.node_type = "SCENE_NODE" + + self.node_tags = {} + if hasattr(node,'hasTag'): + for tag_key in node.getNetTag('tags').split(',') if node.hasTag('tags') else []: + if node.hasTag(tag_key): + self.node_tags[tag_key] = node.getTag(tag_key) + else: + try: + if hasattr(node,'getTag'): + common_tags = ['is_scene_element','tree_item_type','gui_type','light_type', + 'element_type','file','model_path','video_path','image_path', + 'gui_text','name','created_by_user'] + for tag in common_tags: + if node.hasTag(tag): + self.node_tags[tag] = node.getTag(tag) + except: + pass + + self.node_python_tags = {} + if hasattr(node,'getPythonTagKeys'): + try: + for tag_key in node.getPythonTagKeys(): + self.node_python_tags[tag_key] = node.getPythonTag(tag_key) + except Exception as e: + pass + + # 对于特定类型的节点,保存额外的数据 + self.extra_data = {} + if self.node_type in ["GUI_BUTTON", "GUI_LABEL", "GUI_ENTRY", "GUI_IMAGE", + "GUI_3DTEXT", "GUI_3DIMAGE", "GUI_VIDEO_SCREEN", "GUI_2D_VIDEO_SCREEN"]: + if node.hasTag("gui_text"): + self.extra_data["gui_text"] = node.getTag("gui_text") + if node.hasTag("video_path"): + self.extra_data["video_path"] = node.getTag("video_path") + if node.hasTag("image_path"): + self.extra_data["image_path"] = node.getTag("image_path") + elif self.node_type == "LIGHT_NODE": + if node.hasTag("light_type"): + self.extra_data["light_type"] = node.getTag("light_type") + rp_light = node.getPythonTag("rp_light_object") + if rp_light: + self.extra_data["light_data"] = { + 'energy': getattr(rp_light, 'energy', 5000), + 'radius': getattr(rp_light, 'radius', 1000), + 'fov': getattr(rp_light, 'fov', 70) if hasattr(rp_light, 'fov') else 70, + 'inner_radius': getattr(rp_light, 'inner_radius', 0.4) if hasattr(rp_light, + 'inner_radius') else 0.4, + 'casts_shadows': getattr(rp_light, 'casts_shadows', True), + 'shadow_map_resolution': getattr(rp_light, 'shadow_map_resolution', 256) + } + elif self.node_type == "CESIUM_TILESET_NODE": + if node.hasTag("tileset_url"): + self.extra_data["tileset_url"] = node.getTag("tileset_url") + + def execute(self): + """ + 执行删除操作 + """ + # 从world的相应列表中移除节点引用 + if self.world and hasattr(self.world, 'scene_manager'): + scene_manager = self.world.scene_manager + if self.node_type == "LIGHT_NODE": + if self.node.hasTag("light_type"): + light_type = self.node.getTag("light_type") + if light_type == "spot_light" and hasattr(scene_manager, + 'Spotlight') and self.node in scene_manager.Spotlight: + scene_manager.Spotlight.remove(self.node) + elif light_type == "point_light" and hasattr(scene_manager, + 'Pointlight') and self.node in scene_manager.Pointlight: + scene_manager.Pointlight.remove(self.node) + elif self.node_type == "IMPORTED_MODEL_NODE" and hasattr(scene_manager, + 'models') and self.node in scene_manager.models: + scene_manager.models.remove(self.node) + elif self.node_type.startswith("GUI_") and hasattr(self.world, + 'gui_elements') and self.node in self.world.gui_elements: + self.world.gui_elements.remove(self.node) + elif self.node_type == "CESIUM_TILESET_NODE": + # 从tilesets列表中移除 + if hasattr(scene_manager, 'tilesets'): + tilesets_to_remove = [] + for i, tileset_info in enumerate(scene_manager.tilesets): + if tileset_info.get('node') == self.node: + tilesets_to_remove.append(i) + for i in reversed(tilesets_to_remove): + del scene_manager.tilesets[i] + + # 从场景图中移除节点 + if self.node and not self.node.isEmpty(): + self.node.removeNode() + + def undo(self): + """ + 撤销删除操作(重新创建节点) + """ + try: + # 使用场景管理器重建节点 + if self.world and hasattr(self.world, 'scene_manager'): + scene_manager = self.world.scene_manager + + # 创建节点数据字典 + node_data = { + 'name': self.node_name, + 'node_type': self.node_type, + 'pos': (self.node_pos.x, self.node_pos.y, self.node_pos.z), + 'hpr': (self.node_hpr.x, self.node_hpr.y, self.node_hpr.z), + 'scale': (self.node_scale.x, self.node_scale.y, self.node_scale.z), + 'tags': self.node_tags + } + + # 添加额外数据 + if self.extra_data: + if self.node_type.startswith("GUI_"): + node_data['gui_data'] = self.extra_data + elif self.node_type == "LIGHT_NODE": + node_data['light_data'] = self.extra_data.get('light_data', {}) + elif self.node_type == "CESIUM_TILESET_NODE": + node_data['tileset_url'] = self.extra_data.get('tileset_url', '') + + # 重建节点 + new_node = scene_manager.recreateNodeFromData(node_data, self.parent_node) + + if new_node: + print(f"✅ 成功撤销删除操作,节点 {self.node_name} 已恢复") + # 更新节点引用 + self.node = new_node + else: + print(f"❌ 撤销删除操作失败,无法重建节点 {self.node_name}") + else: + print("❌ 无法撤销删除操作,缺少场景管理器引用") + + except Exception as e: + print(f"❌ 撤销删除操作时出错: {e}") + import traceback + traceback.print_exc() + + def redo(self): + """ + 重做删除操作 + """ + self.execute() + + +class RotateNodeCommand(Command): + """ + 旋转节点命令 + """ + + def __init__(self, node: NodePath, old_hpr, new_hpr): + self.node = node + self.old_hpr = old_hpr + self.new_hpr = new_hpr + + def execute(self): + """ + 执行旋转操作 + """ + self.node.setHpr(self.new_hpr) + + def undo(self): + """ + 撤销旋转操作 + """ + self.node.setHpr(self.old_hpr) + + def redo(self): + """ + 重做旋转操作 + """ + self.node.setHpr(self.new_hpr) + + +class ScaleNodeCommand(Command): + """ + 缩放节点命令 + """ + + def __init__(self, node: NodePath, old_scale, new_scale): + self.node = node + self.old_scale = old_scale + self.new_scale = new_scale + + def execute(self): + """ + 执行缩放操作 + """ + self.node.setScale(self.new_scale) + + def undo(self): + """ + 撤销缩放操作 + """ + self.node.setScale(self.old_scale) + + def redo(self): + """ + 重做缩放操作 + """ + self.node.setScale(self.new_scale) + + +class CreateNodeCommand(Command): + """ + 创建节点命令 + """ + + def __init__(self, node_creator_func,parent_node, *args, **kwargs): + self.node_creator_func = node_creator_func + self.parent_node = parent_node + self.args = args + self.kwargs = kwargs + self.created_node = None + + def execute(self): + """ + 执行创建节点操作 + """ + self.created_node = self.node_creator_func(self.parent_node,*self.args, **self.kwargs) + return self.created_node + + def undo(self): + """ + 撤销创建节点操作 + """ + if self.created_node: + self.created_node.removeNode() + + def redo(self): + """ + 重做创建节点操作 + """ + self.execute() + + +class ReparentNodeCommand(Command): + """ + 重新设置节点父子关系命令 - 增强版(同时处理Panda3D和Qt树) + """ + + def __init__(self, node: NodePath, old_parent: NodePath, new_parent: NodePath, + old_parent_item=None, new_parent_item=None, is_2d_gui=False, world=None): + self.node = node + self.old_parent = old_parent + self.new_parent = new_parent + self.old_parent_item = old_parent_item # Qt树中的旧父节点项 + self.new_parent_item = new_parent_item # Qt树中的新父节点项 + self.is_2d_gui = is_2d_gui + self.world = world + + # 保存节点在操作前的世界坐标和局部坐标,以便在撤销/重做时保持位置不变 + self.world_pos = node.getPos(self.world.render if self.world else node.getParent()) + self.world_hpr = node.getHpr(self.world.render if self.world else node.getParent()) + self.world_scale = node.getScale(self.world.render if self.world else node.getParent()) + # 同时保存局部坐标,因为在父节点改变后可能需要恢复 + self.local_pos = node.getPos() + self.local_hpr = node.getHpr() + self.local_scale = node.getScale() + + def _updateQtTree(self, node_item, new_parent_item): + """更新Qt树控件中的节点位置""" + if not node_item or not new_parent_item: + return + + # 从当前父节点中移除 + current_parent = node_item.parent() + if current_parent: + current_parent.removeChild(node_item) + else: + # 如果是顶级项目 + tree_widget = node_item.treeWidget() + if tree_widget: + index = tree_widget.indexOfTopLevelItem(node_item) + if index >= 0: + tree_widget.takeTopLevelItem(index) + + # 添加到新父节点 + new_parent_item.addChild(node_item) + + def execute(self): + """ + 执行重新父化操作 + """ + # 更新Panda3D节点父子关系 + if self.is_2d_gui and self.world: + # 2D GUI元素需要特殊处理 + if self.new_parent and not self.new_parent.isEmpty(): + if hasattr(self.new_parent, 'getTag') and self.new_parent.getTag("is_gui_element") == "1": + # 目标是GUI元素,直接重新父化 + self.node.wrtReparentTo(self.new_parent) + else: + # 目标是3D节点,保持GUI特性,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素保持在aspect2d下") + else: + # 如果新父节点为None,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素重新父化到aspect2d") + else: + # 普通3D节点的处理 + if self.new_parent and not self.new_parent.isEmpty(): + self.node.wrtReparentTo(self.new_parent) + else: + # 如果新父节点为空,将其父化到render节点 + if self.world: + self.node.wrtReparentTo(self.world.render) + else: + # 备用方案 + from panda3d.core import NodePath + self.node.wrtReparentTo(NodePath("render")) + + def undo(self): + """ + 撤销重新父化操作 + """ + # 在改变父节点前保存当前的缩放值 + current_scale = self.node.getScale() + + # 恢复Panda3D节点父子关系 + if self.is_2d_gui and self.world: + # 2D GUI元素需要特殊处理 + if self.old_parent and not self.old_parent.isEmpty(): + if hasattr(self.old_parent, 'getTag') and self.old_parent.getTag("is_gui_element") == "1": + # 原父节点是GUI元素,直接重新父化 + self.node.wrtReparentTo(self.old_parent) + else: + # 原父节点是3D节点,保持GUI特性,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素恢复到aspect2d下") + else: + # 如果原父节点为空,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素恢复到aspect2d") + else: + # 普通3D节点的处理 + if self.old_parent and not self.old_parent.isEmpty(): + self.node.wrtReparentTo(self.old_parent) + else: + # 如果原父节点为空,将其父化到render节点 + if self.world: + self.node.wrtReparentTo(self.world.render) + else: + # 备用方案 + from panda3d.core import NodePath + self.node.wrtReparentTo(NodePath("render")) + + # 恢复局部坐标(不是世界坐标),因为父节点已经改变 + self.node.setPos(self.local_pos) + self.node.setHpr(self.local_hpr) + # 特别处理缩放,确保GUI元素的缩放不会异常变化 + if not self.is_2d_gui or abs(current_scale.length() - self.local_scale.length()) > 0.001: + self.node.setScale(self.local_scale) + + def redo(self): + """ + 重做重新父化操作 + """ + # 在改变父节点前保存当前的缩放值 + current_scale = self.node.getScale() + + # 重新执行Panda3D节点父子关系更新 + if self.is_2d_gui and self.world: + # 2D GUI元素需要特殊处理 + if self.new_parent and not self.new_parent.isEmpty(): + if hasattr(self.new_parent, 'getTag') and self.new_parent.getTag("is_gui_element") == "1": + # 目标是GUI元素,直接重新父化 + self.node.wrtReparentTo(self.new_parent) + else: + # 目标是3D节点,保持GUI特性,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素保持在aspect2d下") + else: + # 如果新父节点为None,重新父化到aspect2d + self.node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素重新父化到aspect2d") + else: + # 普通3D节点的处理 + if self.new_parent and not self.new_parent.isEmpty(): + self.node.wrtReparentTo(self.new_parent) + else: + # 如果新父节点为空,将其父化到render节点 + if self.world: + self.node.wrtReparentTo(self.world.render) + else: + # 备用方案 + from panda3d.core import NodePath + self.node.wrtReparentTo(NodePath("render")) + + # 恢复局部坐标(不是世界坐标),因为父节点已经改变 + self.node.setPos(self.local_pos) + self.node.setHpr(self.local_hpr) + # 特别处理缩放,确保GUI元素的缩放不会异常变化 + if not self.is_2d_gui or abs(current_scale.length() - self.local_scale.length()) > 0.001: + self.node.setScale(self.local_scale) + + +class CompositeCommand(Command): + """ + 组合命令类,用于同时执行多个命令 + """ + def __init__(self,commands:List[Command]): + self.commands = commands + + def execute(self): + """ + 执行所有命令 + """ + for command in self.commands: + command.execute() + + def undo(self): + """ + 撤销所有命令(逆序执行) + """ + for command in reversed(self.commands): + command.undo() + + def redo(self): + """ + 重做所有命令 + """ + for command in self.commands: + command.redo() + +class MoveLightCommand(Command): + def __init__(self, node, old_pos, new_pos, light_object=None): + self.node = node + self.old_pos = Point3(old_pos) + self.new_pos = Point3(new_pos) + self.light_object = light_object + + def execute(self): # 将原来的 do() 改为 execute() + if self.light_object: + self.light_object.pos = self.new_pos + if self.node: + self.node.setPos(self.new_pos) + + def undo(self): + if self.light_object: + self.light_object.pos = self.old_pos + if self.node: + self.node.setPos(self.old_pos) + + def redo(self): + self.execute() # 调用 execute() 而不是 do() + + + diff --git a/core/InfoPanelManager.py b/core/InfoPanelManager.py index 71a43eb8..15ded92a 100644 --- a/core/InfoPanelManager.py +++ b/core/InfoPanelManager.py @@ -1,1693 +1,1693 @@ -# 修改后的 InfoPanelManager.py -from xml.sax.handler import property_encoding - -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QMessageBox -from direct.gui.DirectGui import DirectFrame, DirectLabel -from direct.showbase.ShowBaseGlobal import aspect2d -from panda3d.core import TextNode, Vec4, NodePath -from direct.showbase.DirectObject import DirectObject -import threading -import json -import time -from urllib.parse import urlparse -import requests - - -class InfoPanelManager(DirectObject): - """信息面板管理器,用于创建和管理2D信息面板""" - - def __init__(self, world): - """ - 初始化信息面板管理器 - parent: 父节点,默认为aspect2d - """ - DirectObject.__init__(self) - self.world= world - self.parent = aspect2d - self.panels = {} # 存储所有创建的面板 - self.data_sources = {} # 存储数据源 - self.data_callbacks = {} # 存储数据更新回调 - - def setParent(self, parent): - """设置父节点""" - self.parent = parent if parent else aspect2d - - def createInfoPanel(self, panel_id, position=(0, 0), size=(1.0, 0.6), - bg_color=(0.15, 0.15, 0.15, 0.9), border_color=(0.3, 0.3, 0.3, 1.0), - title_color=(1.0, 1.0, 1.0, 1.0), content_color=(0.9, 0.9, 0.9, 1.0), - visible=True, font=None, bg_image=None): - """ - 创建信息面板 - 返回 NodePath 实例 - """ - # 如果面板已存在,先移除它 - if panel_id in self.panels: - self.removePanel(panel_id) - - # 确保父节点存在 - parent_node = self.parent if self.parent else aspect2d - - # 根据面板ID确定标题和内容 - title, content = self._getPanelContent(panel_id) - - # 创建主节点,便于统一管理 - panel_node = parent_node.attachNewNode(f"{panel_id}") - panel_node.setPos(position[0], 0, position[1]) - - # 创建主面板框架(带边框效果) - panel = DirectFrame( - frameSize=(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2), - frameColor=bg_color, - parent=panel_node - ) - - # 添加背景图片(如果提供) - bg_image_node = None - if bg_image and hasattr(self.world, 'loader'): - try: - from panda3d.core import Texture, TransparencyAttrib - # 使用 world.loader 加载纹理 - texture = self.world.loader.loadTexture(bg_image) - if texture: - # 创建一个覆盖整个面板的图片 - bg_image_node = DirectFrame( - frameSize=(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2), - frameTexture=texture, - parent=panel_node, - sortOrder=-10 # 确保在最底层 - ) - bg_image_node.setTransparency(TransparencyAttrib.MAlpha) - except Exception as e: - print(f"加载背景图片失败: {e}") - - # 添加边框 - border_thickness = 0.005 - # 上边框 - top_border = DirectFrame( - frameSize=(-size[0] / 2 - border_thickness, size[0] / 2 + border_thickness, - size[1] / 2, size[1] / 2 + border_thickness), - frameColor=border_color, - parent=panel_node - ) - # 下边框 - bottom_border = DirectFrame( - frameSize=(-size[0] / 2 - border_thickness, size[0] / 2 + border_thickness, - -size[1] / 2 - border_thickness, -size[1] / 2), - frameColor=border_color, - parent=panel_node - ) - # 左边框 - left_border = DirectFrame( - frameSize=(-size[0] / 2 - border_thickness, -size[0] / 2, - -size[1] / 2, size[1] / 2), - frameColor=border_color, - parent=panel_node - ) - # 右边框 - right_border = DirectFrame( - frameSize=(size[0] / 2, size[0] / 2 + border_thickness, - -size[1] / 2, size[1] / 2), - frameColor=border_color, - parent=panel_node - ) - - # 创建标题栏背景 - title_bar_height = 0.08 - title_bar = DirectFrame( - frameSize=(-size[0] / 2 + border_thickness, size[0] / 2 - border_thickness, - size[1] / 2 - title_bar_height, size[1] / 2 - border_thickness), - frameColor=(0, 0, 0, 0), - parent=panel_node - ) - - # 创建标题 - title_label = DirectLabel( - text=title, - text_scale=0.06, - text_fg=title_color, - text_align=TextNode.ACenter, - pos=(0, 0, size[1] / 2 - title_bar_height / 2 - border_thickness), - parent=panel_node, - relief=None, - text_font=font or None - ) - - # 创建内容区域背景(稍微深一点) - content_bg = DirectFrame( - frameSize=(-size[0] / 2 + border_thickness * 2, size[0] / 2 - border_thickness * 2, - -size[1] / 2 + border_thickness * 2, size[1] / 2 - title_bar_height - border_thickness * 2), - frameColor=(0, 0, 0, 0), - parent=panel_node - ) - - # 创建内容 - 设置一个非常大的换行值,几乎不换行 - content_label = DirectLabel( - text=content, - text_scale=0.045, - text_fg=content_color, - text_align=TextNode.ALeft, - text_wordwrap=0, # 设置一个非常大的值,几乎不自动换行 - pos=(-size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05), - parent=panel_node, - relief=None, - text_font=font or None - ) - - # 保存引用 - self.panels[panel_id] = { - 'node': panel_node, - 'panel': panel, - 'title_label': title_label, - 'content_label': content_label, - 'title_bar': title_bar, - 'content_bg': content_bg, - 'borders': { - 'top': top_border, - 'bottom': bottom_border, - 'left': left_border, - 'right': right_border - }, - 'bg_image': bg_image_node, # 保存背景图片节点引用 - 'properties': { - 'size': size, - 'position': position, - 'bg_color': bg_color, - 'border_color': border_color, - 'title_color': title_color, - 'content_color': content_color, - 'font': font, - 'bg_image': bg_image # 保存背景图片路径 - } - } - - # 设置GUI类型标记和支持3D编辑的标记 - panel_node.setTag("gui_type", "info_panel") - panel_node.setTag("panel_id", panel_id) - panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 - panel_node.setTag("is_gui_element",'1') - panel_node.setTag("tree_item_type","INFO_PANEL") - panel_node.setTag("supports_3d_position_editing","1") - # 如果有背景图片,保存背景图片路径 - if bg_image: - panel_node.setTag("image_path", bg_image) - - - if not visible: - panel_node.hide() - - # 将面板添加到场景树 - #self._addPanelToSceneTree(panel_node, panel_id) - if hasattr(self.world, 'gui_elements'): - self.world.gui_elements.append(panel_node) - - return panel_node - - def _addPanelToSceneTree(self, panel_node, panel_id): - """ - 将信息面板添加到场景树中 - """ - try: - # 获取树形控件 - if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'): - tree_widget = self.world.interface_manager.treeWidget - if tree_widget: - # 查找根节点项 - root_item = None - for i in range(tree_widget.topLevelItemCount()): - item = tree_widget.topLevelItem(i) - if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render: - root_item = item - break - - if root_item: - # 使用现有的 add_node_to_tree_widget 方法添加节点 - qt_item = tree_widget.add_node_to_tree_widget(panel_node, root_item, "INFO_PANEL") - if qt_item: - print(f"✅ 信息面板 {panel_id} 已添加到场景树") - # 选中创建的节点 - tree_widget.setCurrentItem(qt_item) - # 更新选择和属性面板 - tree_widget.update_selection_and_properties(panel_node, qt_item) - else: - print(f"⚠️ 信息面板 {panel_id} 添加到场景树失败") - else: - print("⚠️ 未找到场景树根节点,无法添加信息面板") - else: - print("⚠️ 无法访问场景树控件,信息面板未添加到场景树") - except Exception as e: - print(f"❌ 添加信息面板到场景树时出错: {e}") - import traceback - traceback.print_exc() - - def setPanelBackgroundImage(self, panel_id, image_path): - """ - 为指定面板设置背景图片 - panel_id: 面板ID - image_path: 图片路径 - """ - if panel_id not in self.panels: - print(f"面板 {panel_id} 不存在") - return False - - try: - panel_data = self.panels[panel_id] - - # 如果image_path为None或空,清除现有的背景图片 - if not image_path: - if 'bg_image' in panel_data and panel_data['bg_image']: - panel_data['bg_image'].destroy() - panel_data['bg_image'] = None - panel_data['properties']['bg_image'] = None - # 清除节点标签 - if panel_data['node'].hasTag("bg_image_path"): - panel_data['node'].clearTag("bg_image_path") - return True - - # 使用 world.loader 加载新图片 - texture = self.world.loader.loadTexture(image_path) - if not texture: - print(f"无法加载图片: {image_path}") - return False - - # 获取面板大小 - size = panel_data['properties']['size'] - - # 如果已存在背景图片,先销毁它 - if 'bg_image' in panel_data and panel_data['bg_image']: - panel_data['bg_image'].destroy() - - from direct.gui.DirectGui import DirectFrame - from panda3d.core import TransparencyAttrib - - bg_image_node = DirectFrame( - frameSize=(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2), - frameTexture=texture, - parent=panel_data['node'], - sortOrder=-10 # 确保在最底层 - ) - - bg_image_node.setTransparency(TransparencyAttrib.MAlpha) - - # 保存引用 - panel_data['bg_image'] = bg_image_node - panel_data['properties']['bg_image'] = image_path - - # 更新节点标签 - panel_data['node'].setTag("bg_image_path", image_path) - - print(f"成功设置信息面板背景图片: {image_path}") - return True - except Exception as e: - print(f"设置背景图片失败: {e}") - import traceback - traceback.print_exc() - return False - - def _getPanelContent(self, panel_id): - """ - 根据面板ID获取固定的标题和内容 - """ - panel_contents = { - "scene_info": { - "title": "场景信息", - "content": "项目名称: 我的3D项目\n场景节点数: 15\n总材质数: 8\n光源数量: 3" - }, - "controls": { - "title": "控制说明", - "content": "WASD: 移动\n鼠标: 视角\n空格: 跳跃\nESC: 退出" - }, - "model_info": { - "title": "模型信息", - "content": "模型名称: Unknown\n子节点数: 0\n类型: Unknown" - }, - "terrain_info": { - "title": "地形信息", - "content": "地形名称: Unknown\n大小: 0 x 0\n分辨率: 0" - }, - "light_info": { - "title": "光源信息", - "content": "光源类型: Unknown\n投射阴影: Unknown\n阴影分辨率: Unknown" - }, - "sensor_data": { - "title": "传感器数据", - "content": "温度: --°C\n湿度: --%\n压力: -- hPa\n更新时间: --" - }, - "system_status": { - "title": "系统状态", - "content": "CPU使用率: --%\n内存使用: --MB\n网络状态: 断开\n运行时间: --" - }, - "realtime_data": { - "title": "实时数据", - "content": "数据加载中..." - }, - "default": { - "title": "信息面板", - "content": "这是一个信息面板" - } - } - - # 返回对应面板的内容,如果没有则返回默认内容 - panel_data = panel_contents.get(panel_id, panel_contents["default"]) - return panel_data["title"], panel_data["content"] - - def updatePanelContent(self, panel_id, title=None, content=None): - """ - 更新面板内容 - """ - if panel_id not in self.panels: - print(f"面板 {panel_id} 不存在") - return False - - panel_data = self.panels[panel_id] - - if title is not None and panel_data['title_label']: - panel_data['title_label'].setText(title) - - if content is not None and panel_data['content_label']: - panel_data['content_label'].setText(content) - - return True - - # 更新 registerDataSource 方法以更好地处理不同类型面板 - def registerDataSource(self, panel_id, data_callback, update_interval=1.0): - """ - 注册数据源,定期更新面板内容 - 改进版 - """ - if panel_id not in self.panels: - print(f"面板 {panel_id} 不存在") - return False - - # 停止现有的数据源(如果存在) - if panel_id in self.data_sources: - self.data_sources[panel_id]['stop'] = True - - # 创建数据源 - data_source = { - 'callback': data_callback, - 'interval': update_interval, - 'stop': False, - 'panel_type': '3d' if self._is3DPanel(panel_id) else '2d' if self._is2DPanel(panel_id) else 'unknown' - } - - self.data_sources[panel_id] = data_source - - # 启动数据更新线程 - thread = threading.Thread(target=self._updateDataThread, args=(panel_id,), daemon=True) - thread.start() - - return True - - # 在 InfoPanelManager 类中修复 _updateDataThread 方法 - def _updateDataThread(self, panel_id): - """ - 数据更新线程 - 最终修复版 - """ - while panel_id in self.data_sources and not self.data_sources[panel_id]['stop']: - try: - # 获取数据 - data = self.data_sources[panel_id]['callback']() - - # 更新面板内容 - if data and panel_id in self.panels: - panel_type = self.data_sources[panel_id].get('panel_type', 'unknown') - - if panel_type == '2d': - self.updatePanelContent(panel_id, content=data) - elif panel_type == '3d': - self.update3DPanelContent(panel_id, content=data) - else: - # 尝试自动检测 - if self._is2DPanel(panel_id): - self.updatePanelContent(panel_id, content=data) - elif self._is3DPanel(panel_id): - self.update3DPanelContent(panel_id, content=data) - - # 等待下次更新 - interval = self.data_sources[panel_id]['interval'] - time.sleep(interval) - - except Exception as e: - print(f"更新面板 {panel_id} 数据时出错: {e}") - import traceback - traceback.print_exc() - time.sleep(1.0) # 出错时等待1秒再重试 - - # 在 InfoPanelManager 类中添加以下方法 - def _is3DPanel(self, panel_id): - """ - 判断面板是否为3D面板 - """ - if panel_id not in self.panels: - return False - panel_data = self.panels[panel_id] - return 'content_node' in panel_data and 'content_label' not in panel_data - - def _is2DPanel(self, panel_id): - """ - 判断面板是否为2D面板 - """ - if panel_id not in self.panels: - return False - panel_data = self.panels[panel_id] - return 'content_label' in panel_data and 'content_node' not in panel_data - - def unregisterDataSource(self, panel_id): - """ - 注销数据源 - """ - if panel_id in self.data_sources: - self.data_sources[panel_id]['stop'] = True - del self.data_sources[panel_id] - return True - return False - - def updatePanelProperties(self, panel_id, **properties): - """ - 更新面板属性 - """ - if panel_id not in self.panels: - print(f"面板 {panel_id} 不存在") - return False - - panel_data = self.panels[panel_id] - props = panel_data['properties'] - - # 更新位置 - if 'position' in properties: - pos = properties['position'] - panel_data['node'].setPos(pos[0], 0, pos[1]) - props['position'] = pos - - # 更新大小 - if 'size' in properties: - size = properties['size'] - panel_data['panel']['frameSize'] = (-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2) - props['size'] = size - - # 更新边框 - border_thickness = 0.005 - title_bar_height = 0.08 - - # 更新边框位置和大小 - panel_data['borders']['top']['frameSize'] = ( - -size[0] / 2 - border_thickness, size[0] / 2 + border_thickness, - size[1] / 2, size[1] / 2 + border_thickness - ) - - panel_data['borders']['bottom']['frameSize'] = ( - -size[0] / 2 - border_thickness, size[0] / 2 + border_thickness, - -size[1] / 2 - border_thickness, -size[1] / 2 - ) - - panel_data['borders']['left']['frameSize'] = ( - -size[0] / 2 - border_thickness, -size[0] / 2, - -size[1] / 2, size[1] / 2 - ) - - panel_data['borders']['right']['frameSize'] = ( - size[0] / 2, size[0] / 2 + border_thickness, - -size[1] / 2, size[1] / 2 - ) - - # 更新标题栏 - panel_data['title_bar']['frameSize'] = ( - -size[0] / 2 + border_thickness, size[0] / 2 - border_thickness, - size[1] / 2 - title_bar_height, size[1] / 2 - border_thickness - ) - - # 更新标题位置 - panel_data['title_label'].setPos( - 0, 0, size[1] / 2 - title_bar_height / 2 - border_thickness - ) - - # 更新内容背景 - panel_data['content_bg']['frameSize'] = ( - -size[0] / 2 + border_thickness * 2, size[0] / 2 - border_thickness * 2, - -size[1] / 2 + border_thickness * 2, size[1] / 2 - title_bar_height - border_thickness * 2 - ) - - # 更新内容位置 - panel_data['content_label'].setPos( - -size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05 - ) - - panel_data['content_label']['text_wordwrap'] = 0 - - # 如果有背景图片,也需要更新其大小 - if 'bg_image' in panel_data and panel_data['bg_image']: - panel_data['bg_image']['frameSize'] = (-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2) - - # 更新背景颜色 - if 'bg_color' in properties: - panel_data['panel']['frameColor'] = properties['bg_color'] - props['bg_color'] = properties['bg_color'] - - # 更新边框颜色 - if 'border_color' in properties: - color = properties['border_color'] - for border in panel_data['borders'].values(): - border['frameColor'] = color - props['border_color'] = color - - # 更新标题颜色 - if 'title_color' in properties: - panel_data['title_label']['text_fg'] = properties['title_color'] - props['title_color'] = properties['title_color'] - - # 更新内容颜色 - if 'content_color' in properties: - panel_data['content_label']['text_fg'] = properties['content_color'] - props['content_color'] = properties['content_color'] - - # 更新标题 - if 'title' in properties: - panel_data['title_label'].setText(properties['title']) - - # 更新内容 - if 'content' in properties: - panel_data['content_label'].setText(properties['content']) - - # 更新字体大小 - if 'title_size' in properties: - panel_data['title_label']['text_scale'] = properties['title_size'] - props['title_size'] = properties['title_size'] - - if 'content_size' in properties: - panel_data['content_label']['text_scale'] = properties['content_size'] - props['content_size'] = properties['content_size'] - current_size = props.get('size',(1.0,0.6)) - # 当字体大小改变时,仍然保持较大的换行值 - panel_data['content_label']['text_wordwrap'] = 0 - - # 更新背景图片 - if 'bg_image' in properties: - image_path = properties['bg_image'] - # 无论image_path是None、空字符串还是有效路径,都调用setPanelBackgroundImage处理 - self.setPanelBackgroundImage(panel_id, image_path) - # 同步更新properties中的bg_image值 - props['bg_image'] = image_path - - return True - - def showPanel(self, panel_id): - """显示指定面板""" - if panel_id in self.panels: - self.panels[panel_id]['node'].show() - return True - return False - - def hidePanel(self, panel_id): - """隐藏指定面板""" - if panel_id in self.panels: - self.panels[panel_id]['node'].hide() - return True - return False - - def togglePanel(self, panel_id): - """切换面板显示/隐藏状态""" - if panel_id in self.panels: - node = self.panels[panel_id]['node'] - if node.isHidden(): - node.show() - else: - node.hide() - return True - return False - - def getPanelNode(self, panel_id): - """获取面板的节点""" - if panel_id in self.panels: - return self.panels[panel_id]['node'] - return None - - def removePanel(self, panel_id): - """移除指定面板""" - if panel_id in self.panels: - # 停止数据源更新 - self.unregisterDataSource(panel_id) - - panel_data = self.panels[panel_id] - # 清理所有子元素 - panel_data['node'].removeNode() - # 从字典中移除 - del self.panels[panel_id] - return True - return False - - def removeAllPanels(self): - """移除所有面板""" - panel_ids = list(self.panels.keys()) - for panel_id in panel_ids: - self.removePanel(panel_id) - - def createHTTPInfoPanel(self, panel_id, url, method="GET", headers=None, data=None, - position=(0, 0), size=(1.0, 0.6), - bg_color=(0.15, 0.15, 0.15, 0.9), - border_color=(0.3, 0.3, 0.3, 1.0), - title_color=(1.0, 1.0, 1.0, 1.0), - content_color=(0.9, 0.9, 0.9, 1.0), - update_interval=30.0, font=None): - """ - 创建HTTP信息面板 - panel_id: 面板ID - url: 请求的URL - method: HTTP方法 - headers: 请求头 - data: POST数据 - position: 位置 - size: 大小 - bg_color: 背景颜色 - border_color: 边框颜色 - title_color: 标题颜色 - content_color: 内容颜色 - update_interval: 更新间隔(秒) - font: 字体 - """ - # 创建面板 - domain = urlparse(url).netloc or url[:30] - title = f"HTTP数据: {domain}" - - panel_node = self.createInfoPanel( - panel_id=panel_id, - position=position, - size=size, - bg_color=bg_color, - border_color=border_color, - title_color=title_color, - content_color=content_color, - font=font - ) - - # 更新标题 - self.updatePanelContent(panel_id, title=title) - - # 立即获取并显示数据 - content = fetchHTTPData(url, method, headers, data) - self.updatePanelContent(panel_id, content=content) - - # 注册数据源,定期更新 - def http_data_callback(): - return fetchHTTPData(url, method, headers, data) - - self.registerDataSource(panel_id, http_data_callback, update_interval) - - # 保存HTTP请求信息,便于后续更新 - if panel_id not in self.data_sources: - self.data_sources[panel_id] = {} - self.data_sources[panel_id]['http_info'] = { - 'url': url, - 'method': method, - 'headers': headers, - 'data': data - } - - return panel_node - - def updateHTTPInfoPanel(self, panel_id, url=None, method=None, headers=None, data=None): - """ - 更新HTTP信息面板的请求参数 - """ - if panel_id not in self.panels: - print(f"面板 {panel_id} 不存在") - return False - - # 更新HTTP信息 - if panel_id in self.data_sources and 'http_info' in self.data_sources[panel_id]: - http_info = self.data_sources[panel_id]['http_info'] - if url is not None: - http_info['url'] = url - if method is not None: - http_info['method'] = method - if headers is not None: - http_info['headers'] = headers - if data is not None: - http_info['data'] = data - - # 更新面板标题 - if url is not None: - domain = urlparse(url).netloc or url[:30] - title = f"HTTP数据: {domain}" - self.updatePanelContent(panel_id, title=title) - - # 立即获取并显示新数据 - content = fetchHTTPData( - http_info['url'], - http_info['method'], - http_info['headers'], - http_info['data'] - ) - self.updatePanelContent(panel_id, content=content) - - return True - - def create3DInfoPanel(self, panel_id, position=(0, 0, 0), size=(1.0, 0.6), - bg_color=(0.15, 0.15, 0.15, 0.9), border_color=(0.3, 0.3, 0.3, 1.0), - title_color=(1.0, 1.0, 1.0, 1.0), content_color=(0.9, 0.9, 0.9, 1.0), - visible=True, font=None, bg_image=None): - """ - 创建简化版3D信息面板 - 只显示文字,无面板背景,避免闪烁 - """ - # 如果面板已存在,先移除它 - if panel_id in self.panels: - self.removePanel(panel_id) - - # 确保父节点存在 - parent_node = self.parent if self.parent else self.world.render - - # 根据面板ID确定标题和内容 - title, content = self._getPanelContent(panel_id) - - # 创建主节点,便于统一管理 - panel_node = parent_node.attachNewNode(f"{panel_id}") - panel_node.setPos(position[0], position[1], position[2]) - - # 直接创建文字节点,不创建面板背景和边框 - from panda3d.core import TextNode - - # 创建标题文本 - title_text_node = TextNode(f'title_{panel_id}') - title_text_node.setText(title) - title_text_node.setTextColor(*title_color) - title_text_node.setAlign(TextNode.ACenter) - if font: - title_text_node.setFont(font) - - title_text = panel_node.attachNewNode(title_text_node) - title_text.setScale(0.06) - title_text.setPos(0, 0, size[1] / 4) # 将标题放在上方 - - # 创建内容文本 - content_text_node = TextNode(f'content_{panel_id}') - content_text_node.setText(content) - content_text_node.setTextColor(*content_color) - content_text_node.setAlign(TextNode.ALeft) - content_text_node.setWordwrap(size[0] * 2) # 根据面板宽度设置换行 - if font: - content_text_node.setFont(font) - - content_text = panel_node.attachNewNode(content_text_node) - content_text.setScale(0.045) - content_text.setPos(-size[0] / 2, 0, size[1] / 4 - 0.1) # 将内容放在标题下方 - - # 保存引用 - self.panels[panel_id] = { - 'node': panel_node, - 'title_text': title_text, - 'content_text': content_text, - 'title_node': title_text_node, - 'content_node': content_text_node, - 'properties': { - 'size': size, - 'position': position, - 'title_color': title_color, - 'content_color': content_color, - 'font': font - } - } - - # 设置GUI类型标记和支持3D编辑的标记 - panel_node.setTag("gui_type", "info_panel_3d") - panel_node.setTag("panel_id", panel_id) - panel_node.setTag("is_gui_element", "1") # 添加此标记确保节点被识别为GUI元素 - panel_node.setTag("is_scene_element", "1") # 添加此标记确保节点被识别为场景元素 - panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 - panel_node.setTag("tree_item_type", "INFO_PANEL_3D") # 添加树节点类型标记 - - # 如果有背景图片,保存背景图片路径 - if bg_image: - panel_node.setTag("bg_image_path", bg_image) - - if not visible: - panel_node.hide() - - # 将面板添加到场景树 - #self._addPanelToSceneTree(panel_node, panel_id) - if hasattr(self.world, 'gui_elements'): - self.world.gui_elements.append(panel_node) - - return panel_node - - def update3DPanelContent(self, panel_id, title=None, content=None): - """ - 更新3D面板内容 - """ - if panel_id not in self.panels: - print(f"面板 {panel_id} 不存在") - return False - - panel_data = self.panels[panel_id] - - if title is not None and 'title_node' in panel_data: - panel_data['title_node'].setText(title) - - if content is not None and 'content_node' in panel_data: - panel_data['content_node'].setText(content) - - return True - - def update3DPanelProperties(self, panel_id, **properties): - """ - 更新3D面板属性 - """ - if panel_id not in self.panels: - print(f"面板 {panel_id} 不存在") - return False - - panel_data = self.panels[panel_id] - props = panel_data['properties'] - - # 更新位置 - if 'position' in properties: - pos = properties['position'] - panel_data['node'].setPos(pos[0], pos[1], pos[2]) - props['position'] = pos - - # 更新大小 - if 'size' in properties: - size = properties['size'] - props['size'] = size - - # 由于3D面板使用CardMaker创建,需要重新创建几何体 - print("注意:3D面板大小调整需要重新创建面板几何体") - - # 更新背景颜色 - if 'bg_color' in properties: - bg_color = properties['bg_color'] - if 'panel_bg' in panel_data: - from panda3d.core import Material - material = Material() - material.setDiffuse(Vec4(*bg_color)) - material.setAmbient(Vec4(*bg_color[:3], 1.0)) - panel_data['panel_bg'].setMaterial(material, 1) - props['bg_color'] = bg_color - - # 更新边框颜色 - if 'border_color' in properties: - border_color = properties['border_color'] - from panda3d.core import Material - border_mat = Material() - border_mat.setDiffuse(Vec4(*border_color)) - for border in panel_data['borders'].values(): - border.setMaterial(border_mat, 1) - props['border_color'] = border_color - - # 更新标题颜色 - if 'title_color' in properties: - title_color = properties['title_color'] - if 'title_node' in panel_data: - panel_data['title_node'].setTextColor(*title_color) - props['title_color'] = title_color - - # 更新内容颜色 - if 'content_color' in properties: - content_color = properties['content_color'] - if 'content_node' in panel_data: - panel_data['content_node'].setTextColor(*content_color) - props['content_color'] = content_color - - # 更新标题 - if 'title' in properties: - if 'title_node' in panel_data: - panel_data['title_node'].setText(properties['title']) - - # 更新内容 - if 'content' in properties: - if 'content_node' in panel_data: - panel_data['content_node'].setText(properties['content']) - - # 更新字体大小 - if 'title_size' in properties: - if 'title_text' in panel_data: - panel_data['title_text'].setScale(properties['title_size']) - props['title_size'] = properties['title_size'] - - if 'content_size' in properties: - if 'content_text' in panel_data: - panel_data['content_text'].setScale(properties['content_size']) - props['content_size'] = properties['content_size'] - - return True - - def create3DHTTPInfoPanel(self, panel_id, url, method="GET", headers=None, data=None, - position=(0, 0, 0), size=(1.0, 0.6), - bg_color=(0.15, 0.15, 0.15, 0.9), - border_color=(0.3, 0.3, 0.3, 1.0), - title_color=(1.0, 1.0, 1.0, 1.0), - content_color=(0.9, 0.9, 0.9, 1.0), - update_interval=30.0, font=None): - """ - 创建3D HTTP信息面板 - """ - # 创建面板 - domain = urlparse(url).netloc or url[:30] - title = f"HTTP数据: {domain}" - - panel_node = self.create3DInfoPanel( - panel_id=panel_id, - position=position, - size=size, - bg_color=bg_color, - border_color=border_color, - title_color=title_color, - content_color=content_color, - font=font - ) - - # 更新标题 - self.update3DPanelContent(panel_id, title=title) - - # 立即获取并显示数据 - content = fetchHTTPData(url, method, headers, data) - self.update3DPanelContent(panel_id, content=content) - - # 注册数据源,定期更新 - def http_data_callback(): - return fetchHTTPData(url, method, headers, data) - - self.registerDataSource(panel_id, http_data_callback, update_interval) - - # 保存HTTP请求信息,便于后续更新 - if panel_id not in self.data_sources: - self.data_sources[panel_id] = {} - self.data_sources[panel_id]['http_info'] = { - 'url': url, - 'method': method, - 'headers': headers, - 'data': data - } - - return panel_node - - # 在 add_methods_to_property_panel 函数中添加以下方法 - - def add_methods_to_property_panel(property_panel_instance): - # ... (原有代码保持不变) - import types - - def createHTTPInfoPanel(self, url, panel_id="http_info", method="GET", headers=None, data=None, - position=(0.8, 0.0), size=(0.4, 0.3), update_interval=30.0): - """ - 为属性面板创建HTTP信息面板 - url: 请求的URL - panel_id: 面板ID - method: HTTP方法 - headers: 请求头 - data: POST数据 - position: 位置 - size: 大小 - update_interval: 更新间隔(秒) - """ - try: - # 确保父节点已设置 - if self.info_panel_manager.parent is None and hasattr(self, 'world'): - self.info_panel_manager.setParent(self.world.render) - - # 创建HTTP信息面板 - panel_node = self.info_panel_manager.createHTTPInfoPanel( - panel_id=panel_id, - url=url, - method=method, - headers=headers, - data=data, - position=position, - size=size, - update_interval=update_interval, - bg_color=(0.15, 0.25, 0.35, 0.95), # 蓝色背景 - border_color=(0.3, 0.5, 0.7, 1.0), # 蓝色边框 - title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题 - content_color=(0.95, 0.95, 0.95, 1.0) - ) - - # 设置标签 - panel_node.setTag("element_type", "info_panel") - panel_node.setTag("is_scene_element", "1") - panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 - - print(f"✓ 已创建HTTP信息面板: {url}") - - return panel_node - - except Exception as e: - print(f"✗ 创建HTTP信息面板失败: {e}") - return None - - def updateHTTPInfoPanel(self, panel_id, url=None, method=None, headers=None, data=None): - """ - 更新HTTP信息面板 - """ - try: - if hasattr(self, 'info_panel_manager'): - return self.info_panel_manager.updateHTTPInfoPanel(panel_id, url, method, headers, data) - return False - except Exception as e: - print(f"✗ 更新HTTP信息面板失败: {e}") - return False - - def create3DRealtimeDataPanel(self, data_callback=None, update_interval=1.0): - """创建3D实时数据面板""" - try: - # 确保父节点已设置 - if self.info_panel_manager.parent is None and hasattr(self, 'world'): - self.info_panel_manager.setParent(self.world.render) - - # 创建3D实时数据面板 - panel_node = self.info_panel_manager.create3DInfoPanel( - panel_id="realtime_data_3d", - position=(0, 0, 0), - size=(0.35, 0.3), - bg_color=(0.15, 0.25, 0.35, 0.95), # 蓝色背景 - border_color=(0.3, 0.5, 0.7, 1.0), # 蓝色边框 - title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题 - content_color=(0.95, 0.95, 0.95, 1.0) - ) - - # 设置标签 - panel_node.setTag("element_type", "info_panel_3d") - panel_node.setTag("is_scene_element", "1") - panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 - - # 如果提供了数据回调函数,则注册数据源 - if data_callback: - # 立即显示初始数据 - initial_data = data_callback() - self.info_panel_manager.update3DPanelContent("realtime_data_3d", content=initial_data) - - # 注册数据源 - self.info_panel_manager.registerDataSource("realtime_data_3d", data_callback, update_interval) - else: - # 使用默认数据 - default_data = "等待数据..." - self.info_panel_manager.update3DPanelContent("realtime_data_3d", content=default_data) - - print("✓ 已创建3D实时数据面板") - - return panel_node - - except Exception as e: - print(f"✗ 创建3D实时数据面板失败: {e}") - return None - - def create3DModelInfoPanel(self, model): - """为模型创建3D信息面板""" - try: - # 确保父节点已设置 - if self.info_panel_manager.parent is None and hasattr(self, 'world'): - self.info_panel_manager.setParent(self.world.render) - - # 获取模型信息 - model_name = model.getName() if hasattr(model, 'getName') else 'Unknown' - num_children = model.getNumChildren() if hasattr(model, 'getNumChildren') else 0 - - # 创建面板内容 - content = f"模型名称: {model_name}\n子节点数: {num_children}\n类型: {type(model).__name__}" - - # 创建或更新面板 - panel_node = self.info_panel_manager.create3DInfoPanel( - panel_id="model_info_3d", - position=(2, 0, 0), # 默认放在模型旁边 - size=(0.35, 0.25), - bg_color=(0.15, 0.15, 0.25, 0.95), # 蓝紫色背景 - border_color=(0.3, 0.3, 0.7, 1.0), # 蓝色边框 - title_color=(0.5, 0.8, 1.0, 1.0), # 浅蓝色标题 - content_color=(0.95, 0.95, 0.95, 1.0) - ) - - # 更新面板内容为模型特定信息 - self.info_panel_manager.update3DPanelContent("model_info_3d", - title="模型信息", - content=content) - - # 设置标签 - panel_node.setTag("element_type", "info_panel_3d") - panel_node.setTag("is_scene_element", "1") - panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 - - print(f"✓ 已创建3D模型信息面板: {model_name}") - - return panel_node - - except Exception as e: - print(f"✗ 创建3D模型信息面板失败: {e}") - return None - - # 在绑定方法的部分添加3D面板方法 - property_panel_instance.create3DRealtimeDataPanel = types.MethodType(create3DRealtimeDataPanel, - property_panel_instance) - property_panel_instance.create3DModelInfoPanel = types.MethodType(create3DModelInfoPanel, - property_panel_instance) - - # 将新方法绑定到实例 - # ... (原有绑定保持不变) - property_panel_instance.createHTTPInfoPanel = types.MethodType(createHTTPInfoPanel, property_panel_instance) - property_panel_instance.updateHTTPInfoPanel = types.MethodType(updateHTTPInfoPanel, property_panel_instance) - - def serializePanelData(self, panel_id): - """序列化面板数据用于保存""" - if panel_id not in self.panels: - return None - - panel_data = self.panels[panel_id] - props = panel_data['properties'] - - # 获取面板类型 - panel_type = "2d" - if panel_data['node'].hasTag("gui_type"): - gui_type = panel_data['node'].getTag("gui_type") - if "3d" in gui_type.lower(): - panel_type = "3d" - - # 构建序列化数据 - serialized_data = { - 'panel_id': panel_id, - 'panel_type': panel_type, - 'position': props.get('position', (0, 0) if panel_type == "2d" else (0, 0, 0)), - 'size': props.get('size', (1.0, 0.6)), - 'bg_color': props.get('bg_color', (0.15, 0.15, 0.15, 0.9)), - 'border_color': props.get('border_color', (0.3, 0.3, 0.3, 1.0)), - 'title_color': props.get('title_color', (1.0, 1.0, 1.0, 1.0)), - 'content_color': props.get('content_color', (0.9, 0.9, 0.9, 1.0)), - 'title': panel_data['title_label'].getText() if 'title_label' in panel_data else "信息面板", - 'content': panel_data[ - 'content_label'].getText() if 'content_label' in panel_data else "" if 'content_node' not in panel_data else - panel_data['content_node'].getText(), - 'font_path': props.get('font', None), - 'bg_image': props.get('bg_image', None), - 'visible': not panel_data['node'].isHidden() - } - - # 添加HTTP面板特有数据 - if panel_id in self.data_sources and 'http_info' in self.data_sources[panel_id]: - serialized_data['http_info'] = self.data_sources[panel_id]['http_info'] - - return serialized_data - - def getAllPanelData(self): - """获取所有面板的序列化数据""" - panel_data_list = [] - for panel_id in self.panels: - data = self.serializePanelData(panel_id) - if data: - panel_data_list.append(data) - return panel_data_list - - def recreatePanelFromData(self, panel_data): - """从序列化数据重新创建面板""" - try: - panel_id = panel_data['panel_id'] - panel_type = panel_data['panel_type'] - position = panel_data['position'] - size = panel_data['size'] - - # 重建面板 - if panel_type == "3d": - panel_node = self.create3DInfoPanel( - panel_id=panel_id, - position=position, - size=size, - bg_color=panel_data.get('bg_color', (0.15, 0.15, 0.15, 0.9)), - border_color=panel_data.get('border_color', (0.3, 0.3, 0.3, 1.0)), - title_color=panel_data.get('title_color', (1.0, 1.0, 1.0, 1.0)), - content_color=panel_data.get('content_color', (0.9, 0.9, 0.9, 1.0)), - visible=panel_data.get('visible', True), - font=panel_data.get('font_path', None), - bg_image=panel_data.get('bg_image', None) - ) - - # 更新内容 - self.update3DPanelContent( - panel_id, - title=panel_data.get('title', '信息面板'), - content=panel_data.get('content', '') - ) - else: - panel_node = self.createInfoPanel( - panel_id=panel_id, - position=(position[0], position[1]) if len(position) >= 2 else (0, 0), - size=size, - bg_color=panel_data.get('bg_color', (0.15, 0.15, 0.15, 0.9)), - border_color=panel_data.get('border_color', (0.3, 0.3, 0.3, 1.0)), - title_color=panel_data.get('title_color', (1.0, 1.0, 1.0, 1.0)), - content_color=panel_data.get('content_color', (0.9, 0.9, 0.9, 1.0)), - visible=panel_data.get('visible', True), - font=panel_data.get('font_path', None), - bg_image=panel_data.get('bg_image', None) - ) - - # 更新内容 - self.updatePanelContent( - panel_id, - title=panel_data.get('title', '信息面板'), - content=panel_data.get('content', '') - ) - - # 设置标签 - if panel_node: - panel_node.setTag("element_type", "info_panel") - panel_node.setTag("is_scene_element", "1") - panel_node.setTag("supports_3d_position_editing", "1") - - # 如果是HTTP面板,重新注册数据源 - if 'http_info' in panel_data: - http_info = panel_data['http_info'] - self.createHTTPInfoPanel( - panel_id=panel_id, - url=http_info['url'], - method=http_info.get('method', 'GET'), - headers=http_info.get('headers', None), - data=http_info.get('data', None), - position=position, - size=size, - update_interval=self.data_sources.get(panel_id, {}).get('interval', - 30.0) if panel_id in self.data_sources else 30.0 - ) - - print(f"✓ 信息面板 {panel_id} 已重建") - return panel_node - - except Exception as e: - print(f"✗ 重建信息面板 {panel_data.get('panel_id', 'unknown')} 失败: {e}") - import traceback - traceback.print_exc() - return None - - def onCreateSampleInfoPanel(self): - """创建示例天气信息面板(模拟数据)""" - try: - # 获取中文字体 - from panda3d.core import TextNode - font = self.world.getChineseFont() if self.world.getChineseFont() else None - - # 使用唯一的面板ID - import time - unique_id = f"weather_info_{int(time.time())}" - - # 创建示例面板 - weather_panel = self.createInfoPanel( - panel_id=unique_id, # 使用唯一ID - position=(1.32, 0.68), - size=(1, 0.6), - bg_color=(0.15, 0.25, 0.35, 0), # 蓝色背景 - border_color=(0.3, 0.5, 0.7, 0), # 蓝色边框 - title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题 - content_color=(0.95, 0.95, 0.95, 1.0), - font=font, - bg_image="/home/tiger/图片/内部信息框2@2x.png" - ) - weather_panel.setTag("name",unique_id) - - # 更新面板标题 - self.updatePanelContent(unique_id, title="北京天气") - - self._addPanelToSceneTree(weather_panel, unique_id) - # 立即显示加载中信息 - self.updatePanelContent(unique_id, content="正在获取天气数据...") - - self.registerDataSource(unique_id, self.getRealWeatherData, update_interval=5.0) - - print("✓ 示例天气信息面板已创建") - - except Exception as e: - print(f"✗ 创建示例天气信息面板失败: {e}") - import traceback - traceback.print_exc() - QMessageBox.critical(self, "错误", f"创建示例天气信息面板时出错: {str(e)}") - - def getRealWeatherData(self): - """获取真实天气数据""" - try: - import requests - import json - from datetime import datetime - - # 请求天气数据 - url = "https://wttr.in/Beijing?format=j1" - response = requests.get(url, timeout=10) - response.raise_for_status() - - # 解析JSON数据 - weather_data = response.json() - - # 提取当前天气信息 - current_condition = weather_data['current_condition'][0] - weather_desc = current_condition['weatherDesc'][0]['value'] - temp_c = current_condition['temp_C'] - feels_like = current_condition['FeelsLikeC'] - humidity = current_condition['humidity'] - pressure = current_condition['pressure'] - visibility = current_condition['visibility'] - wind_speed = current_condition['windspeedKmph'] - wind_dir = current_condition['winddir16Point'] - - # 提取空气质量(如果可用) - air_quality = "N/A" - if 'air_quality' in weather_data and weather_data['air_quality']: - if 'us-epa-index' in current_condition: - air_quality_index = current_condition['air_quality_index'] - air_quality = f"指数: {air_quality_index}" - - # 获取更新时间 - update_time = datetime.now().strftime("%Y-%m-%d %H:%M") - - # 格式化显示内容 - content = f"天气状况: {weather_desc}\n温度: {temp_c}°C (体感 {feels_like}°C)\n湿度: {humidity}%\n气压: {pressure} hPa\n能见度: {visibility} km\n风速: {wind_speed} km/h ({wind_dir})\n空气质量: {air_quality}\n更新时间: {update_time}" - - return content - - except requests.exceptions.Timeout: - return "错误: 获取天气数据超时" - except requests.exceptions.ConnectionError: - return "错误: 网络连接失败" - except requests.exceptions.HTTPError as e: - return f"HTTP错误: {e}" - except json.JSONDecodeError: - return "错误: 无法解析天气数据" - except KeyError as e: - return f"错误: 天气数据格式不正确 (缺少字段: {e})" - except Exception as e: - return f"获取天气数据失败: {str(e)}" - - -# 示例数据源函数 -def getRealtimeData(): - """ - 获取实时数据的示例函数 - """ - import random - from datetime import datetime - - # 模拟实时数据 - value1 = round(random.uniform(10, 100), 2) - value2 = round(random.uniform(0, 50), 2) - timestamp = datetime.now().strftime("%H:%M:%S") - - return f"实时值1: {value1}\n实时值2: {value2}\n更新时间: {timestamp}" - - -# 使用示例函数 -def createSampleInfoPanel(parent_node): - """创建示例信息面板""" - - # 创建信息面板管理器 - info_manager = InfoPanelManager(parent_node) - - # 创建一个实时数据面板 - info_manager.createInfoPanel( - panel_id="realtime_data", - position=(0.8, 0.0), - size=(0.35, 0.3), - bg_color=(0.15, 0.25, 0.35, 0.95), # 蓝色背景 - border_color=(0.3, 0.5, 0.7, 1.0), # 蓝色边框 - title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题 - content_color=(0.95, 0.95, 0.95, 1.0) - ) - - # 立即显示初始数据 - initial_data = getRealtimeData() - info_manager.updatePanelContent("realtime_data", content=initial_data) - - # 注册数据源,每秒更新一次 - info_manager.registerDataSource("realtime_data", getRealtimeData, update_interval=1.0) - - return info_manager - - -# 集成到您的 property_panel.py 中的方法 -def add_methods_to_property_panel(property_panel_instance): - """ - 为 property_panel 实例添加 DirectGUI 信息面板支持 - """ - import types - - # 添加信息面板管理器作为类属性 - if not hasattr(property_panel_instance, 'info_panel_manager'): - if hasattr(property_panel_instance, 'world') and hasattr(property_panel_instance.world, 'render'): - property_panel_instance.info_panel_manager = InfoPanelManager(property_panel_instance.world.render) - else: - property_panel_instance.info_panel_manager = InfoPanelManager() - - def createRealtimeDataPanel(self, data_callback=None, update_interval=1.0): - """创建实时数据面板""" - try: - # 确保父节点已设置 - if self.info_panel_manager.parent is None and hasattr(self, 'world'): - self.info_panel_manager.setParent(self.world.render) - - # 创建实时数据面板 - panel_node = self.info_panel_manager.createInfoPanel( - panel_id="realtime_data", - position=(0.8, 0.0), - size=(0.35, 0.3), - bg_color=(0.15, 0.25, 0.35, 0.95), # 蓝色背景 - border_color=(0.3, 0.5, 0.7, 1.0), # 蓝色边框 - title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题 - content_color=(0.95, 0.95, 0.95, 1.0) - ) - - # 设置标签 - panel_node.setTag("element_type", "info_panel") - panel_node.setTag("is_scene_element", "1") - panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 - - # 如果提供了数据回调函数,则注册数据源 - if data_callback: - # 立即显示初始数据 - initial_data = data_callback() - self.info_panel_manager.updatePanelContent("realtime_data", content=initial_data) - - # 注册数据源 - self.info_panel_manager.registerDataSource("realtime_data", data_callback, update_interval) - else: - # 使用默认数据 - default_data = "等待数据..." - self.info_panel_manager.updatePanelContent("realtime_data", content=default_data) - - print("✓ 已创建实时数据面板") - - return panel_node - - except Exception as e: - print(f"✗ 创建实时数据面板失败: {e}") - return None - - - def createModelInfoPanel(self, model): - """为模型创建信息面板""" - try: - # 确保父节点已设置 - if self.info_panel_manager.parent is None and hasattr(self, 'world'): - self.info_panel_manager.setParent(self.world.render) - - # 获取模型信息 - model_name = model.getName() if hasattr(model, 'getName') else 'Unknown' - num_children = model.getNumChildren() if hasattr(model, 'getNumChildren') else 0 - - # 创建面板内容 - content = f"模型名称: {model_name}\n子节点数: {num_children}\n类型: {type(model).__name__}" - - # 创建或更新面板 - panel_node = self.info_panel_manager.createInfoPanel( - panel_id="model_info", - position=(0.8, 0.7), - size=(0.35, 0.25), - bg_color=(0.15, 0.15, 0.25, 0.95), # 蓝紫色背景 - border_color=(0.3, 0.3, 0.7, 1.0), # 蓝色边框 - title_color=(0.5, 0.8, 1.0, 1.0), # 浅蓝色标题 - content_color=(0.95, 0.95, 0.95, 1.0) - ) - - # 更新面板内容为模型特定信息 - self.info_panel_manager.updatePanelContent("model_info", - title="模型信息", - content=content) - - # 设置标签 - panel_node.setTag("element_type", "info_panel") - panel_node.setTag("is_scene_element", "1") - panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 - - print(f"✓ 已创建模型信息面板: {model_name}") - - return panel_node - - except Exception as e: - print(f"✗ 创建模型信息面板失败: {e}") - return None - - def createTerrainInfoPanel(self, terrain_info): - """为地形创建信息面板""" - try: - # 确保父节点已设置 - if self.info_panel_manager.parent is None and hasattr(self, 'world'): - self.info_panel_manager.setParent(self.world.render) - - # 获取地形信息 - terrain_name = terrain_info.get('name', 'Unknown') - width = terrain_info.get('width', 0) - height = terrain_info.get('height', 0) - resolution = terrain_info.get('resolution', 0) - - # 创建面板内容 - content = f"地形名称: {terrain_name}\n大小: {width} x {height}\n分辨率: {resolution}" - - # 创建或更新面板 - panel_node = self.info_panel_manager.createInfoPanel( - panel_id="terrain_info", - position=(0.8, 0.7), - size=(0.35, 0.25), - bg_color=(0.15, 0.25, 0.15, 0.95), # 绿色背景 - border_color=(0.3, 0.7, 0.3, 1.0), # 绿色边框 - title_color=(0.5, 1.0, 0.5, 1.0), # 浅绿色标题 - content_color=(0.95, 0.95, 0.95, 1.0) - ) - - # 更新面板内容为地形特定信息 - self.info_panel_manager.updatePanelContent("terrain_info", - title="地形信息", - content=content) - - panel_node.setTag("element_type", "info_panel") - panel_node.setTag("is_scene_element", "1") - panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 - - print(f"✓ 已创建地形信息面板: {terrain_name}") - - return panel_node - - except Exception as e: - print(f"✗ 创建地形信息面板失败: {e}") - return None - - def createLightInfoPanel(self, light_object): - """为光源创建信息面板""" - try: - # 确保父节点已设置 - if self.info_panel_manager.parent is None and hasattr(self, 'world'): - self.info_panel_manager.setParent(self.world.render) - - # 获取光源信息 - light_type = type(light_object).__name__ - casts_shadows = getattr(light_object, 'casts_shadows', 'Unknown') - shadow_res = getattr(light_object, 'shadow_map_resolution', 'Unknown') - - # 创建面板内容 - content = f"光源类型: {light_type}\n投射阴影: {casts_shadows}\n阴影分辨率: {shadow_res}" - - # 创建或更新面板 - panel_node = self.info_panel_manager.createInfoPanel( - panel_id="light_info", - position=(0.8, 0.7), - size=(0.35, 0.25), - bg_color=(0.25, 0.25, 0.15, 0.95), # 黄色背景 - border_color=(0.7, 0.7, 0.3, 1.0), # 黄色边框 - title_color=(1.0, 1.0, 0.5, 1.0), # 浅黄色标题 - content_color=(0.95, 0.95, 0.95, 1.0) - ) - - # 更新面板内容为光源特定信息 - self.info_panel_manager.updatePanelContent("light_info", - title="光源信息", - content=content) - - panel_node.setTag("element_type", "info_panel") - panel_node.setTag("is_scene_element", "1") - panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 - - print(f"✓ 已创建光源信息面板") - - return panel_node - - except Exception as e: - print(f"✗ 创建光源信息面板失败: {e}") - return None - - def removeInfoPanel(self, panel_id="model_info"): - """移除信息面板""" - if hasattr(self, 'info_panel_manager'): - self.info_panel_manager.removePanel(panel_id) - - def setupInfoPanelManager(self): - """初始化信息面板管理器""" - if not hasattr(self, 'info_panel_manager'): - if hasattr(self, 'world') and hasattr(self.world, 'render'): - self.info_panel_manager = InfoPanelManager(self.world.render) - else: - self.info_panel_manager = InfoPanelManager() - - # 将方法绑定到实例 - import types - property_panel_instance.createRealtimeDataPanel = types.MethodType(createRealtimeDataPanel, property_panel_instance) - property_panel_instance.createModelInfoPanel = types.MethodType(createModelInfoPanel, property_panel_instance) - property_panel_instance.createTerrainInfoPanel = types.MethodType(createTerrainInfoPanel, property_panel_instance) - property_panel_instance.createLightInfoPanel = types.MethodType(createLightInfoPanel, property_panel_instance) - property_panel_instance.removeInfoPanel = types.MethodType(removeInfoPanel, property_panel_instance) - property_panel_instance.setupInfoPanelManager = types.MethodType(setupInfoPanelManager, property_panel_instance) - -def fetchHTTPData(url, method="GET", headers=None, data=None, timeout=5): - """ - 获取HTTP数据的通用函数 - url: 请求的URL - method: HTTP方法 (GET, POST, etc.) - headers: 请求头 - data: POST数据 - timeout: 超时时间(秒) - """ - try: - if headers is None: - headers = { - 'User-Agent': 'InfoPanelManager/1.0', - 'Accept': 'application/json,text/plain,*/*' - } - - # 根据方法发送请求 - if method.upper() == "GET": - response = requests.get(url, headers=headers, timeout=timeout) - elif method.upper() == "POST": - response = requests.post(url, headers=headers, data=data, timeout=timeout) - else: - response = requests.request(method, url, headers=headers, data=data, timeout=timeout) - - # 检查响应状态 - response.raise_for_status() - - # 尝试解析JSON - try: - json_data = response.json() - return formatJSONData(json_data) - except: - # 如果不是JSON,返回文本内容 - return response.text[:500] # 限制长度 - - except requests.exceptions.Timeout: - return "错误: 请求超时" - except requests.exceptions.ConnectionError: - return "错误: 连接失败" - except requests.exceptions.HTTPError as e: - return f"HTTP错误: {e}" - except Exception as e: - return f"获取数据失败: {str(e)}" - - -def formatJSONData(data, indent=0): - """ - 格式化JSON数据为易读的文本 - """ - if isinstance(data, dict): - lines = [] - for key, value in data.items(): - if isinstance(value, (dict, list)): - lines.append(f"{' ' * indent}{key}:") - lines.append(formatJSONData(value, indent + 1)) - else: - lines.append(f"{' ' * indent}{key}: {value}") - return "\n".join(lines) - elif isinstance(data, list): - lines = [] - for i, item in enumerate(data): - if isinstance(item, (dict, list)): - lines.append(f"{' ' * indent}[{i}]:") - lines.append(formatJSONData(item, indent + 1)) - else: - lines.append(f"{' ' * indent}[{i}] {item}") - return "\n".join(lines) - else: - return str(data) +# 修改后的 InfoPanelManager.py +from xml.sax.handler import property_encoding + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QMessageBox +from direct.gui.DirectGui import DirectFrame, DirectLabel +from direct.showbase.ShowBaseGlobal import aspect2d +from panda3d.core import TextNode, Vec4, NodePath +from direct.showbase.DirectObject import DirectObject +import threading +import json +import time +from urllib.parse import urlparse +import requests + + +class InfoPanelManager(DirectObject): + """信息面板管理器,用于创建和管理2D信息面板""" + + def __init__(self, world): + """ + 初始化信息面板管理器 + parent: 父节点,默认为aspect2d + """ + DirectObject.__init__(self) + self.world= world + self.parent = aspect2d + self.panels = {} # 存储所有创建的面板 + self.data_sources = {} # 存储数据源 + self.data_callbacks = {} # 存储数据更新回调 + + def setParent(self, parent): + """设置父节点""" + self.parent = parent if parent else aspect2d + + def createInfoPanel(self, panel_id, position=(0, 0), size=(1.0, 0.6), + bg_color=(0.15, 0.15, 0.15, 0.9), border_color=(0.3, 0.3, 0.3, 1.0), + title_color=(1.0, 1.0, 1.0, 1.0), content_color=(0.9, 0.9, 0.9, 1.0), + visible=True, font=None, bg_image=None): + """ + 创建信息面板 - 返回 NodePath 实例 + """ + # 如果面板已存在,先移除它 + if panel_id in self.panels: + self.removePanel(panel_id) + + # 确保父节点存在 + parent_node = self.parent if self.parent else aspect2d + + # 根据面板ID确定标题和内容 + title, content = self._getPanelContent(panel_id) + + # 创建主节点,便于统一管理 + panel_node = parent_node.attachNewNode(f"{panel_id}") + panel_node.setPos(position[0], 0, position[1]) + + # 创建主面板框架(带边框效果) + panel = DirectFrame( + frameSize=(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2), + frameColor=bg_color, + parent=panel_node + ) + + # 添加背景图片(如果提供) + bg_image_node = None + if bg_image and hasattr(self.world, 'loader'): + try: + from panda3d.core import Texture, TransparencyAttrib + # 使用 world.loader 加载纹理 + texture = self.world.loader.loadTexture(bg_image) + if texture: + # 创建一个覆盖整个面板的图片 + bg_image_node = DirectFrame( + frameSize=(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2), + frameTexture=texture, + parent=panel_node, + sortOrder=-10 # 确保在最底层 + ) + bg_image_node.setTransparency(TransparencyAttrib.MAlpha) + except Exception as e: + print(f"加载背景图片失败: {e}") + + # 添加边框 + border_thickness = 0.005 + # 上边框 + top_border = DirectFrame( + frameSize=(-size[0] / 2 - border_thickness, size[0] / 2 + border_thickness, + size[1] / 2, size[1] / 2 + border_thickness), + frameColor=border_color, + parent=panel_node + ) + # 下边框 + bottom_border = DirectFrame( + frameSize=(-size[0] / 2 - border_thickness, size[0] / 2 + border_thickness, + -size[1] / 2 - border_thickness, -size[1] / 2), + frameColor=border_color, + parent=panel_node + ) + # 左边框 + left_border = DirectFrame( + frameSize=(-size[0] / 2 - border_thickness, -size[0] / 2, + -size[1] / 2, size[1] / 2), + frameColor=border_color, + parent=panel_node + ) + # 右边框 + right_border = DirectFrame( + frameSize=(size[0] / 2, size[0] / 2 + border_thickness, + -size[1] / 2, size[1] / 2), + frameColor=border_color, + parent=panel_node + ) + + # 创建标题栏背景 + title_bar_height = 0.08 + title_bar = DirectFrame( + frameSize=(-size[0] / 2 + border_thickness, size[0] / 2 - border_thickness, + size[1] / 2 - title_bar_height, size[1] / 2 - border_thickness), + frameColor=(0, 0, 0, 0), + parent=panel_node + ) + + # 创建标题 + title_label = DirectLabel( + text=title, + text_scale=0.06, + text_fg=title_color, + text_align=TextNode.ACenter, + pos=(0, 0, size[1] / 2 - title_bar_height / 2 - border_thickness), + parent=panel_node, + relief=None, + text_font=font or None + ) + + # 创建内容区域背景(稍微深一点) + content_bg = DirectFrame( + frameSize=(-size[0] / 2 + border_thickness * 2, size[0] / 2 - border_thickness * 2, + -size[1] / 2 + border_thickness * 2, size[1] / 2 - title_bar_height - border_thickness * 2), + frameColor=(0, 0, 0, 0), + parent=panel_node + ) + + # 创建内容 - 设置一个非常大的换行值,几乎不换行 + content_label = DirectLabel( + text=content, + text_scale=0.045, + text_fg=content_color, + text_align=TextNode.ALeft, + text_wordwrap=0, # 设置一个非常大的值,几乎不自动换行 + pos=(-size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05), + parent=panel_node, + relief=None, + text_font=font or None + ) + + # 保存引用 + self.panels[panel_id] = { + 'node': panel_node, + 'panel': panel, + 'title_label': title_label, + 'content_label': content_label, + 'title_bar': title_bar, + 'content_bg': content_bg, + 'borders': { + 'top': top_border, + 'bottom': bottom_border, + 'left': left_border, + 'right': right_border + }, + 'bg_image': bg_image_node, # 保存背景图片节点引用 + 'properties': { + 'size': size, + 'position': position, + 'bg_color': bg_color, + 'border_color': border_color, + 'title_color': title_color, + 'content_color': content_color, + 'font': font, + 'bg_image': bg_image # 保存背景图片路径 + } + } + + # 设置GUI类型标记和支持3D编辑的标记 + panel_node.setTag("gui_type", "info_panel") + panel_node.setTag("panel_id", panel_id) + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + panel_node.setTag("is_gui_element",'1') + panel_node.setTag("tree_item_type","INFO_PANEL") + panel_node.setTag("supports_3d_position_editing","1") + # 如果有背景图片,保存背景图片路径 + if bg_image: + panel_node.setTag("image_path", bg_image) + + + if not visible: + panel_node.hide() + + # 将面板添加到场景树 + #self._addPanelToSceneTree(panel_node, panel_id) + if hasattr(self.world, 'gui_elements'): + self.world.gui_elements.append(panel_node) + + return panel_node + + def _addPanelToSceneTree(self, panel_node, panel_id): + """ + 将信息面板添加到场景树中 + """ + try: + # 获取树形控件 + if hasattr(self.world, 'interface_manager') and hasattr(self.world.interface_manager, 'treeWidget'): + tree_widget = self.world.interface_manager.treeWidget + if tree_widget: + # 查找根节点项 + root_item = None + for i in range(tree_widget.topLevelItemCount()): + item = tree_widget.topLevelItem(i) + if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render: + root_item = item + break + + if root_item: + # 使用现有的 add_node_to_tree_widget 方法添加节点 + qt_item = tree_widget.add_node_to_tree_widget(panel_node, root_item, "INFO_PANEL") + if qt_item: + print(f"✅ 信息面板 {panel_id} 已添加到场景树") + # 选中创建的节点 + tree_widget.setCurrentItem(qt_item) + # 更新选择和属性面板 + tree_widget.update_selection_and_properties(panel_node, qt_item) + else: + print(f"⚠️ 信息面板 {panel_id} 添加到场景树失败") + else: + print("⚠️ 未找到场景树根节点,无法添加信息面板") + else: + print("⚠️ 无法访问场景树控件,信息面板未添加到场景树") + except Exception as e: + print(f"❌ 添加信息面板到场景树时出错: {e}") + import traceback + traceback.print_exc() + + def setPanelBackgroundImage(self, panel_id, image_path): + """ + 为指定面板设置背景图片 + panel_id: 面板ID + image_path: 图片路径 + """ + if panel_id not in self.panels: + print(f"面板 {panel_id} 不存在") + return False + + try: + panel_data = self.panels[panel_id] + + # 如果image_path为None或空,清除现有的背景图片 + if not image_path: + if 'bg_image' in panel_data and panel_data['bg_image']: + panel_data['bg_image'].destroy() + panel_data['bg_image'] = None + panel_data['properties']['bg_image'] = None + # 清除节点标签 + if panel_data['node'].hasTag("bg_image_path"): + panel_data['node'].clearTag("bg_image_path") + return True + + # 使用 world.loader 加载新图片 + texture = self.world.loader.loadTexture(image_path) + if not texture: + print(f"无法加载图片: {image_path}") + return False + + # 获取面板大小 + size = panel_data['properties']['size'] + + # 如果已存在背景图片,先销毁它 + if 'bg_image' in panel_data and panel_data['bg_image']: + panel_data['bg_image'].destroy() + + from direct.gui.DirectGui import DirectFrame + from panda3d.core import TransparencyAttrib + + bg_image_node = DirectFrame( + frameSize=(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2), + frameTexture=texture, + parent=panel_data['node'], + sortOrder=-10 # 确保在最底层 + ) + + bg_image_node.setTransparency(TransparencyAttrib.MAlpha) + + # 保存引用 + panel_data['bg_image'] = bg_image_node + panel_data['properties']['bg_image'] = image_path + + # 更新节点标签 + panel_data['node'].setTag("bg_image_path", image_path) + + print(f"成功设置信息面板背景图片: {image_path}") + return True + except Exception as e: + print(f"设置背景图片失败: {e}") + import traceback + traceback.print_exc() + return False + + def _getPanelContent(self, panel_id): + """ + 根据面板ID获取固定的标题和内容 + """ + panel_contents = { + "scene_info": { + "title": "场景信息", + "content": "项目名称: 我的3D项目\n场景节点数: 15\n总材质数: 8\n光源数量: 3" + }, + "controls": { + "title": "控制说明", + "content": "WASD: 移动\n鼠标: 视角\n空格: 跳跃\nESC: 退出" + }, + "model_info": { + "title": "模型信息", + "content": "模型名称: Unknown\n子节点数: 0\n类型: Unknown" + }, + "terrain_info": { + "title": "地形信息", + "content": "地形名称: Unknown\n大小: 0 x 0\n分辨率: 0" + }, + "light_info": { + "title": "光源信息", + "content": "光源类型: Unknown\n投射阴影: Unknown\n阴影分辨率: Unknown" + }, + "sensor_data": { + "title": "传感器数据", + "content": "温度: --°C\n湿度: --%\n压力: -- hPa\n更新时间: --" + }, + "system_status": { + "title": "系统状态", + "content": "CPU使用率: --%\n内存使用: --MB\n网络状态: 断开\n运行时间: --" + }, + "realtime_data": { + "title": "实时数据", + "content": "数据加载中..." + }, + "default": { + "title": "信息面板", + "content": "这是一个信息面板" + } + } + + # 返回对应面板的内容,如果没有则返回默认内容 + panel_data = panel_contents.get(panel_id, panel_contents["default"]) + return panel_data["title"], panel_data["content"] + + def updatePanelContent(self, panel_id, title=None, content=None): + """ + 更新面板内容 + """ + if panel_id not in self.panels: + print(f"面板 {panel_id} 不存在") + return False + + panel_data = self.panels[panel_id] + + if title is not None and panel_data['title_label']: + panel_data['title_label'].setText(title) + + if content is not None and panel_data['content_label']: + panel_data['content_label'].setText(content) + + return True + + # 更新 registerDataSource 方法以更好地处理不同类型面板 + def registerDataSource(self, panel_id, data_callback, update_interval=1.0): + """ + 注册数据源,定期更新面板内容 - 改进版 + """ + if panel_id not in self.panels: + print(f"面板 {panel_id} 不存在") + return False + + # 停止现有的数据源(如果存在) + if panel_id in self.data_sources: + self.data_sources[panel_id]['stop'] = True + + # 创建数据源 + data_source = { + 'callback': data_callback, + 'interval': update_interval, + 'stop': False, + 'panel_type': '3d' if self._is3DPanel(panel_id) else '2d' if self._is2DPanel(panel_id) else 'unknown' + } + + self.data_sources[panel_id] = data_source + + # 启动数据更新线程 + thread = threading.Thread(target=self._updateDataThread, args=(panel_id,), daemon=True) + thread.start() + + return True + + # 在 InfoPanelManager 类中修复 _updateDataThread 方法 + def _updateDataThread(self, panel_id): + """ + 数据更新线程 - 最终修复版 + """ + while panel_id in self.data_sources and not self.data_sources[panel_id]['stop']: + try: + # 获取数据 + data = self.data_sources[panel_id]['callback']() + + # 更新面板内容 + if data and panel_id in self.panels: + panel_type = self.data_sources[panel_id].get('panel_type', 'unknown') + + if panel_type == '2d': + self.updatePanelContent(panel_id, content=data) + elif panel_type == '3d': + self.update3DPanelContent(panel_id, content=data) + else: + # 尝试自动检测 + if self._is2DPanel(panel_id): + self.updatePanelContent(panel_id, content=data) + elif self._is3DPanel(panel_id): + self.update3DPanelContent(panel_id, content=data) + + # 等待下次更新 + interval = self.data_sources[panel_id]['interval'] + time.sleep(interval) + + except Exception as e: + print(f"更新面板 {panel_id} 数据时出错: {e}") + import traceback + traceback.print_exc() + time.sleep(1.0) # 出错时等待1秒再重试 + + # 在 InfoPanelManager 类中添加以下方法 + def _is3DPanel(self, panel_id): + """ + 判断面板是否为3D面板 + """ + if panel_id not in self.panels: + return False + panel_data = self.panels[panel_id] + return 'content_node' in panel_data and 'content_label' not in panel_data + + def _is2DPanel(self, panel_id): + """ + 判断面板是否为2D面板 + """ + if panel_id not in self.panels: + return False + panel_data = self.panels[panel_id] + return 'content_label' in panel_data and 'content_node' not in panel_data + + def unregisterDataSource(self, panel_id): + """ + 注销数据源 + """ + if panel_id in self.data_sources: + self.data_sources[panel_id]['stop'] = True + del self.data_sources[panel_id] + return True + return False + + def updatePanelProperties(self, panel_id, **properties): + """ + 更新面板属性 + """ + if panel_id not in self.panels: + print(f"面板 {panel_id} 不存在") + return False + + panel_data = self.panels[panel_id] + props = panel_data['properties'] + + # 更新位置 + if 'position' in properties: + pos = properties['position'] + panel_data['node'].setPos(pos[0], 0, pos[1]) + props['position'] = pos + + # 更新大小 + if 'size' in properties: + size = properties['size'] + panel_data['panel']['frameSize'] = (-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2) + props['size'] = size + + # 更新边框 + border_thickness = 0.005 + title_bar_height = 0.08 + + # 更新边框位置和大小 + panel_data['borders']['top']['frameSize'] = ( + -size[0] / 2 - border_thickness, size[0] / 2 + border_thickness, + size[1] / 2, size[1] / 2 + border_thickness + ) + + panel_data['borders']['bottom']['frameSize'] = ( + -size[0] / 2 - border_thickness, size[0] / 2 + border_thickness, + -size[1] / 2 - border_thickness, -size[1] / 2 + ) + + panel_data['borders']['left']['frameSize'] = ( + -size[0] / 2 - border_thickness, -size[0] / 2, + -size[1] / 2, size[1] / 2 + ) + + panel_data['borders']['right']['frameSize'] = ( + size[0] / 2, size[0] / 2 + border_thickness, + -size[1] / 2, size[1] / 2 + ) + + # 更新标题栏 + panel_data['title_bar']['frameSize'] = ( + -size[0] / 2 + border_thickness, size[0] / 2 - border_thickness, + size[1] / 2 - title_bar_height, size[1] / 2 - border_thickness + ) + + # 更新标题位置 + panel_data['title_label'].setPos( + 0, 0, size[1] / 2 - title_bar_height / 2 - border_thickness + ) + + # 更新内容背景 + panel_data['content_bg']['frameSize'] = ( + -size[0] / 2 + border_thickness * 2, size[0] / 2 - border_thickness * 2, + -size[1] / 2 + border_thickness * 2, size[1] / 2 - title_bar_height - border_thickness * 2 + ) + + # 更新内容位置 + panel_data['content_label'].setPos( + -size[0] / 2 + 0.03, 0, size[1] / 2 - title_bar_height - 0.05 + ) + + panel_data['content_label']['text_wordwrap'] = 0 + + # 如果有背景图片,也需要更新其大小 + if 'bg_image' in panel_data and panel_data['bg_image']: + panel_data['bg_image']['frameSize'] = (-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2) + + # 更新背景颜色 + if 'bg_color' in properties: + panel_data['panel']['frameColor'] = properties['bg_color'] + props['bg_color'] = properties['bg_color'] + + # 更新边框颜色 + if 'border_color' in properties: + color = properties['border_color'] + for border in panel_data['borders'].values(): + border['frameColor'] = color + props['border_color'] = color + + # 更新标题颜色 + if 'title_color' in properties: + panel_data['title_label']['text_fg'] = properties['title_color'] + props['title_color'] = properties['title_color'] + + # 更新内容颜色 + if 'content_color' in properties: + panel_data['content_label']['text_fg'] = properties['content_color'] + props['content_color'] = properties['content_color'] + + # 更新标题 + if 'title' in properties: + panel_data['title_label'].setText(properties['title']) + + # 更新内容 + if 'content' in properties: + panel_data['content_label'].setText(properties['content']) + + # 更新字体大小 + if 'title_size' in properties: + panel_data['title_label']['text_scale'] = properties['title_size'] + props['title_size'] = properties['title_size'] + + if 'content_size' in properties: + panel_data['content_label']['text_scale'] = properties['content_size'] + props['content_size'] = properties['content_size'] + current_size = props.get('size',(1.0,0.6)) + # 当字体大小改变时,仍然保持较大的换行值 + panel_data['content_label']['text_wordwrap'] = 0 + + # 更新背景图片 + if 'bg_image' in properties: + image_path = properties['bg_image'] + # 无论image_path是None、空字符串还是有效路径,都调用setPanelBackgroundImage处理 + self.setPanelBackgroundImage(panel_id, image_path) + # 同步更新properties中的bg_image值 + props['bg_image'] = image_path + + return True + + def showPanel(self, panel_id): + """显示指定面板""" + if panel_id in self.panels: + self.panels[panel_id]['node'].show() + return True + return False + + def hidePanel(self, panel_id): + """隐藏指定面板""" + if panel_id in self.panels: + self.panels[panel_id]['node'].hide() + return True + return False + + def togglePanel(self, panel_id): + """切换面板显示/隐藏状态""" + if panel_id in self.panels: + node = self.panels[panel_id]['node'] + if node.isHidden(): + node.show() + else: + node.hide() + return True + return False + + def getPanelNode(self, panel_id): + """获取面板的节点""" + if panel_id in self.panels: + return self.panels[panel_id]['node'] + return None + + def removePanel(self, panel_id): + """移除指定面板""" + if panel_id in self.panels: + # 停止数据源更新 + self.unregisterDataSource(panel_id) + + panel_data = self.panels[panel_id] + # 清理所有子元素 + panel_data['node'].removeNode() + # 从字典中移除 + del self.panels[panel_id] + return True + return False + + def removeAllPanels(self): + """移除所有面板""" + panel_ids = list(self.panels.keys()) + for panel_id in panel_ids: + self.removePanel(panel_id) + + def createHTTPInfoPanel(self, panel_id, url, method="GET", headers=None, data=None, + position=(0, 0), size=(1.0, 0.6), + bg_color=(0.15, 0.15, 0.15, 0.9), + border_color=(0.3, 0.3, 0.3, 1.0), + title_color=(1.0, 1.0, 1.0, 1.0), + content_color=(0.9, 0.9, 0.9, 1.0), + update_interval=30.0, font=None): + """ + 创建HTTP信息面板 + panel_id: 面板ID + url: 请求的URL + method: HTTP方法 + headers: 请求头 + data: POST数据 + position: 位置 + size: 大小 + bg_color: 背景颜色 + border_color: 边框颜色 + title_color: 标题颜色 + content_color: 内容颜色 + update_interval: 更新间隔(秒) + font: 字体 + """ + # 创建面板 + domain = urlparse(url).netloc or url[:30] + title = f"HTTP数据: {domain}" + + panel_node = self.createInfoPanel( + panel_id=panel_id, + position=position, + size=size, + bg_color=bg_color, + border_color=border_color, + title_color=title_color, + content_color=content_color, + font=font + ) + + # 更新标题 + self.updatePanelContent(panel_id, title=title) + + # 立即获取并显示数据 + content = fetchHTTPData(url, method, headers, data) + self.updatePanelContent(panel_id, content=content) + + # 注册数据源,定期更新 + def http_data_callback(): + return fetchHTTPData(url, method, headers, data) + + self.registerDataSource(panel_id, http_data_callback, update_interval) + + # 保存HTTP请求信息,便于后续更新 + if panel_id not in self.data_sources: + self.data_sources[panel_id] = {} + self.data_sources[panel_id]['http_info'] = { + 'url': url, + 'method': method, + 'headers': headers, + 'data': data + } + + return panel_node + + def updateHTTPInfoPanel(self, panel_id, url=None, method=None, headers=None, data=None): + """ + 更新HTTP信息面板的请求参数 + """ + if panel_id not in self.panels: + print(f"面板 {panel_id} 不存在") + return False + + # 更新HTTP信息 + if panel_id in self.data_sources and 'http_info' in self.data_sources[panel_id]: + http_info = self.data_sources[panel_id]['http_info'] + if url is not None: + http_info['url'] = url + if method is not None: + http_info['method'] = method + if headers is not None: + http_info['headers'] = headers + if data is not None: + http_info['data'] = data + + # 更新面板标题 + if url is not None: + domain = urlparse(url).netloc or url[:30] + title = f"HTTP数据: {domain}" + self.updatePanelContent(panel_id, title=title) + + # 立即获取并显示新数据 + content = fetchHTTPData( + http_info['url'], + http_info['method'], + http_info['headers'], + http_info['data'] + ) + self.updatePanelContent(panel_id, content=content) + + return True + + def create3DInfoPanel(self, panel_id, position=(0, 0, 0), size=(1.0, 0.6), + bg_color=(0.15, 0.15, 0.15, 0.9), border_color=(0.3, 0.3, 0.3, 1.0), + title_color=(1.0, 1.0, 1.0, 1.0), content_color=(0.9, 0.9, 0.9, 1.0), + visible=True, font=None, bg_image=None): + """ + 创建简化版3D信息面板 - 只显示文字,无面板背景,避免闪烁 + """ + # 如果面板已存在,先移除它 + if panel_id in self.panels: + self.removePanel(panel_id) + + # 确保父节点存在 + parent_node = self.parent if self.parent else self.world.render + + # 根据面板ID确定标题和内容 + title, content = self._getPanelContent(panel_id) + + # 创建主节点,便于统一管理 + panel_node = parent_node.attachNewNode(f"{panel_id}") + panel_node.setPos(position[0], position[1], position[2]) + + # 直接创建文字节点,不创建面板背景和边框 + from panda3d.core import TextNode + + # 创建标题文本 + title_text_node = TextNode(f'title_{panel_id}') + title_text_node.setText(title) + title_text_node.setTextColor(*title_color) + title_text_node.setAlign(TextNode.ACenter) + if font: + title_text_node.setFont(font) + + title_text = panel_node.attachNewNode(title_text_node) + title_text.setScale(0.06) + title_text.setPos(0, 0, size[1] / 4) # 将标题放在上方 + + # 创建内容文本 + content_text_node = TextNode(f'content_{panel_id}') + content_text_node.setText(content) + content_text_node.setTextColor(*content_color) + content_text_node.setAlign(TextNode.ALeft) + content_text_node.setWordwrap(size[0] * 2) # 根据面板宽度设置换行 + if font: + content_text_node.setFont(font) + + content_text = panel_node.attachNewNode(content_text_node) + content_text.setScale(0.045) + content_text.setPos(-size[0] / 2, 0, size[1] / 4 - 0.1) # 将内容放在标题下方 + + # 保存引用 + self.panels[panel_id] = { + 'node': panel_node, + 'title_text': title_text, + 'content_text': content_text, + 'title_node': title_text_node, + 'content_node': content_text_node, + 'properties': { + 'size': size, + 'position': position, + 'title_color': title_color, + 'content_color': content_color, + 'font': font + } + } + + # 设置GUI类型标记和支持3D编辑的标记 + panel_node.setTag("gui_type", "info_panel_3d") + panel_node.setTag("panel_id", panel_id) + panel_node.setTag("is_gui_element", "1") # 添加此标记确保节点被识别为GUI元素 + panel_node.setTag("is_scene_element", "1") # 添加此标记确保节点被识别为场景元素 + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + panel_node.setTag("tree_item_type", "INFO_PANEL_3D") # 添加树节点类型标记 + + # 如果有背景图片,保存背景图片路径 + if bg_image: + panel_node.setTag("bg_image_path", bg_image) + + if not visible: + panel_node.hide() + + # 将面板添加到场景树 + #self._addPanelToSceneTree(panel_node, panel_id) + if hasattr(self.world, 'gui_elements'): + self.world.gui_elements.append(panel_node) + + return panel_node + + def update3DPanelContent(self, panel_id, title=None, content=None): + """ + 更新3D面板内容 + """ + if panel_id not in self.panels: + print(f"面板 {panel_id} 不存在") + return False + + panel_data = self.panels[panel_id] + + if title is not None and 'title_node' in panel_data: + panel_data['title_node'].setText(title) + + if content is not None and 'content_node' in panel_data: + panel_data['content_node'].setText(content) + + return True + + def update3DPanelProperties(self, panel_id, **properties): + """ + 更新3D面板属性 + """ + if panel_id not in self.panels: + print(f"面板 {panel_id} 不存在") + return False + + panel_data = self.panels[panel_id] + props = panel_data['properties'] + + # 更新位置 + if 'position' in properties: + pos = properties['position'] + panel_data['node'].setPos(pos[0], pos[1], pos[2]) + props['position'] = pos + + # 更新大小 + if 'size' in properties: + size = properties['size'] + props['size'] = size + + # 由于3D面板使用CardMaker创建,需要重新创建几何体 + print("注意:3D面板大小调整需要重新创建面板几何体") + + # 更新背景颜色 + if 'bg_color' in properties: + bg_color = properties['bg_color'] + if 'panel_bg' in panel_data: + from panda3d.core import Material + material = Material() + material.setDiffuse(Vec4(*bg_color)) + material.setAmbient(Vec4(*bg_color[:3], 1.0)) + panel_data['panel_bg'].setMaterial(material, 1) + props['bg_color'] = bg_color + + # 更新边框颜色 + if 'border_color' in properties: + border_color = properties['border_color'] + from panda3d.core import Material + border_mat = Material() + border_mat.setDiffuse(Vec4(*border_color)) + for border in panel_data['borders'].values(): + border.setMaterial(border_mat, 1) + props['border_color'] = border_color + + # 更新标题颜色 + if 'title_color' in properties: + title_color = properties['title_color'] + if 'title_node' in panel_data: + panel_data['title_node'].setTextColor(*title_color) + props['title_color'] = title_color + + # 更新内容颜色 + if 'content_color' in properties: + content_color = properties['content_color'] + if 'content_node' in panel_data: + panel_data['content_node'].setTextColor(*content_color) + props['content_color'] = content_color + + # 更新标题 + if 'title' in properties: + if 'title_node' in panel_data: + panel_data['title_node'].setText(properties['title']) + + # 更新内容 + if 'content' in properties: + if 'content_node' in panel_data: + panel_data['content_node'].setText(properties['content']) + + # 更新字体大小 + if 'title_size' in properties: + if 'title_text' in panel_data: + panel_data['title_text'].setScale(properties['title_size']) + props['title_size'] = properties['title_size'] + + if 'content_size' in properties: + if 'content_text' in panel_data: + panel_data['content_text'].setScale(properties['content_size']) + props['content_size'] = properties['content_size'] + + return True + + def create3DHTTPInfoPanel(self, panel_id, url, method="GET", headers=None, data=None, + position=(0, 0, 0), size=(1.0, 0.6), + bg_color=(0.15, 0.15, 0.15, 0.9), + border_color=(0.3, 0.3, 0.3, 1.0), + title_color=(1.0, 1.0, 1.0, 1.0), + content_color=(0.9, 0.9, 0.9, 1.0), + update_interval=30.0, font=None): + """ + 创建3D HTTP信息面板 + """ + # 创建面板 + domain = urlparse(url).netloc or url[:30] + title = f"HTTP数据: {domain}" + + panel_node = self.create3DInfoPanel( + panel_id=panel_id, + position=position, + size=size, + bg_color=bg_color, + border_color=border_color, + title_color=title_color, + content_color=content_color, + font=font + ) + + # 更新标题 + self.update3DPanelContent(panel_id, title=title) + + # 立即获取并显示数据 + content = fetchHTTPData(url, method, headers, data) + self.update3DPanelContent(panel_id, content=content) + + # 注册数据源,定期更新 + def http_data_callback(): + return fetchHTTPData(url, method, headers, data) + + self.registerDataSource(panel_id, http_data_callback, update_interval) + + # 保存HTTP请求信息,便于后续更新 + if panel_id not in self.data_sources: + self.data_sources[panel_id] = {} + self.data_sources[panel_id]['http_info'] = { + 'url': url, + 'method': method, + 'headers': headers, + 'data': data + } + + return panel_node + + # 在 add_methods_to_property_panel 函数中添加以下方法 + + def add_methods_to_property_panel(property_panel_instance): + # ... (原有代码保持不变) + import types + + def createHTTPInfoPanel(self, url, panel_id="http_info", method="GET", headers=None, data=None, + position=(0.8, 0.0), size=(0.4, 0.3), update_interval=30.0): + """ + 为属性面板创建HTTP信息面板 + url: 请求的URL + panel_id: 面板ID + method: HTTP方法 + headers: 请求头 + data: POST数据 + position: 位置 + size: 大小 + update_interval: 更新间隔(秒) + """ + try: + # 确保父节点已设置 + if self.info_panel_manager.parent is None and hasattr(self, 'world'): + self.info_panel_manager.setParent(self.world.render) + + # 创建HTTP信息面板 + panel_node = self.info_panel_manager.createHTTPInfoPanel( + panel_id=panel_id, + url=url, + method=method, + headers=headers, + data=data, + position=position, + size=size, + update_interval=update_interval, + bg_color=(0.15, 0.25, 0.35, 0.95), # 蓝色背景 + border_color=(0.3, 0.5, 0.7, 1.0), # 蓝色边框 + title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题 + content_color=(0.95, 0.95, 0.95, 1.0) + ) + + # 设置标签 + panel_node.setTag("element_type", "info_panel") + panel_node.setTag("is_scene_element", "1") + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + + print(f"✓ 已创建HTTP信息面板: {url}") + + return panel_node + + except Exception as e: + print(f"✗ 创建HTTP信息面板失败: {e}") + return None + + def updateHTTPInfoPanel(self, panel_id, url=None, method=None, headers=None, data=None): + """ + 更新HTTP信息面板 + """ + try: + if hasattr(self, 'info_panel_manager'): + return self.info_panel_manager.updateHTTPInfoPanel(panel_id, url, method, headers, data) + return False + except Exception as e: + print(f"✗ 更新HTTP信息面板失败: {e}") + return False + + def create3DRealtimeDataPanel(self, data_callback=None, update_interval=1.0): + """创建3D实时数据面板""" + try: + # 确保父节点已设置 + if self.info_panel_manager.parent is None and hasattr(self, 'world'): + self.info_panel_manager.setParent(self.world.render) + + # 创建3D实时数据面板 + panel_node = self.info_panel_manager.create3DInfoPanel( + panel_id="realtime_data_3d", + position=(0, 0, 0), + size=(0.35, 0.3), + bg_color=(0.15, 0.25, 0.35, 0.95), # 蓝色背景 + border_color=(0.3, 0.5, 0.7, 1.0), # 蓝色边框 + title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题 + content_color=(0.95, 0.95, 0.95, 1.0) + ) + + # 设置标签 + panel_node.setTag("element_type", "info_panel_3d") + panel_node.setTag("is_scene_element", "1") + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + + # 如果提供了数据回调函数,则注册数据源 + if data_callback: + # 立即显示初始数据 + initial_data = data_callback() + self.info_panel_manager.update3DPanelContent("realtime_data_3d", content=initial_data) + + # 注册数据源 + self.info_panel_manager.registerDataSource("realtime_data_3d", data_callback, update_interval) + else: + # 使用默认数据 + default_data = "等待数据..." + self.info_panel_manager.update3DPanelContent("realtime_data_3d", content=default_data) + + print("✓ 已创建3D实时数据面板") + + return panel_node + + except Exception as e: + print(f"✗ 创建3D实时数据面板失败: {e}") + return None + + def create3DModelInfoPanel(self, model): + """为模型创建3D信息面板""" + try: + # 确保父节点已设置 + if self.info_panel_manager.parent is None and hasattr(self, 'world'): + self.info_panel_manager.setParent(self.world.render) + + # 获取模型信息 + model_name = model.getName() if hasattr(model, 'getName') else 'Unknown' + num_children = model.getNumChildren() if hasattr(model, 'getNumChildren') else 0 + + # 创建面板内容 + content = f"模型名称: {model_name}\n子节点数: {num_children}\n类型: {type(model).__name__}" + + # 创建或更新面板 + panel_node = self.info_panel_manager.create3DInfoPanel( + panel_id="model_info_3d", + position=(2, 0, 0), # 默认放在模型旁边 + size=(0.35, 0.25), + bg_color=(0.15, 0.15, 0.25, 0.95), # 蓝紫色背景 + border_color=(0.3, 0.3, 0.7, 1.0), # 蓝色边框 + title_color=(0.5, 0.8, 1.0, 1.0), # 浅蓝色标题 + content_color=(0.95, 0.95, 0.95, 1.0) + ) + + # 更新面板内容为模型特定信息 + self.info_panel_manager.update3DPanelContent("model_info_3d", + title="模型信息", + content=content) + + # 设置标签 + panel_node.setTag("element_type", "info_panel_3d") + panel_node.setTag("is_scene_element", "1") + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + + print(f"✓ 已创建3D模型信息面板: {model_name}") + + return panel_node + + except Exception as e: + print(f"✗ 创建3D模型信息面板失败: {e}") + return None + + # 在绑定方法的部分添加3D面板方法 + property_panel_instance.create3DRealtimeDataPanel = types.MethodType(create3DRealtimeDataPanel, + property_panel_instance) + property_panel_instance.create3DModelInfoPanel = types.MethodType(create3DModelInfoPanel, + property_panel_instance) + + # 将新方法绑定到实例 + # ... (原有绑定保持不变) + property_panel_instance.createHTTPInfoPanel = types.MethodType(createHTTPInfoPanel, property_panel_instance) + property_panel_instance.updateHTTPInfoPanel = types.MethodType(updateHTTPInfoPanel, property_panel_instance) + + def serializePanelData(self, panel_id): + """序列化面板数据用于保存""" + if panel_id not in self.panels: + return None + + panel_data = self.panels[panel_id] + props = panel_data['properties'] + + # 获取面板类型 + panel_type = "2d" + if panel_data['node'].hasTag("gui_type"): + gui_type = panel_data['node'].getTag("gui_type") + if "3d" in gui_type.lower(): + panel_type = "3d" + + # 构建序列化数据 + serialized_data = { + 'panel_id': panel_id, + 'panel_type': panel_type, + 'position': props.get('position', (0, 0) if panel_type == "2d" else (0, 0, 0)), + 'size': props.get('size', (1.0, 0.6)), + 'bg_color': props.get('bg_color', (0.15, 0.15, 0.15, 0.9)), + 'border_color': props.get('border_color', (0.3, 0.3, 0.3, 1.0)), + 'title_color': props.get('title_color', (1.0, 1.0, 1.0, 1.0)), + 'content_color': props.get('content_color', (0.9, 0.9, 0.9, 1.0)), + 'title': panel_data['title_label'].getText() if 'title_label' in panel_data else "信息面板", + 'content': panel_data[ + 'content_label'].getText() if 'content_label' in panel_data else "" if 'content_node' not in panel_data else + panel_data['content_node'].getText(), + 'font_path': props.get('font', None), + 'bg_image': props.get('bg_image', None), + 'visible': not panel_data['node'].isHidden() + } + + # 添加HTTP面板特有数据 + if panel_id in self.data_sources and 'http_info' in self.data_sources[panel_id]: + serialized_data['http_info'] = self.data_sources[panel_id]['http_info'] + + return serialized_data + + def getAllPanelData(self): + """获取所有面板的序列化数据""" + panel_data_list = [] + for panel_id in self.panels: + data = self.serializePanelData(panel_id) + if data: + panel_data_list.append(data) + return panel_data_list + + def recreatePanelFromData(self, panel_data): + """从序列化数据重新创建面板""" + try: + panel_id = panel_data['panel_id'] + panel_type = panel_data['panel_type'] + position = panel_data['position'] + size = panel_data['size'] + + # 重建面板 + if panel_type == "3d": + panel_node = self.create3DInfoPanel( + panel_id=panel_id, + position=position, + size=size, + bg_color=panel_data.get('bg_color', (0.15, 0.15, 0.15, 0.9)), + border_color=panel_data.get('border_color', (0.3, 0.3, 0.3, 1.0)), + title_color=panel_data.get('title_color', (1.0, 1.0, 1.0, 1.0)), + content_color=panel_data.get('content_color', (0.9, 0.9, 0.9, 1.0)), + visible=panel_data.get('visible', True), + font=panel_data.get('font_path', None), + bg_image=panel_data.get('bg_image', None) + ) + + # 更新内容 + self.update3DPanelContent( + panel_id, + title=panel_data.get('title', '信息面板'), + content=panel_data.get('content', '') + ) + else: + panel_node = self.createInfoPanel( + panel_id=panel_id, + position=(position[0], position[1]) if len(position) >= 2 else (0, 0), + size=size, + bg_color=panel_data.get('bg_color', (0.15, 0.15, 0.15, 0.9)), + border_color=panel_data.get('border_color', (0.3, 0.3, 0.3, 1.0)), + title_color=panel_data.get('title_color', (1.0, 1.0, 1.0, 1.0)), + content_color=panel_data.get('content_color', (0.9, 0.9, 0.9, 1.0)), + visible=panel_data.get('visible', True), + font=panel_data.get('font_path', None), + bg_image=panel_data.get('bg_image', None) + ) + + # 更新内容 + self.updatePanelContent( + panel_id, + title=panel_data.get('title', '信息面板'), + content=panel_data.get('content', '') + ) + + # 设置标签 + if panel_node: + panel_node.setTag("element_type", "info_panel") + panel_node.setTag("is_scene_element", "1") + panel_node.setTag("supports_3d_position_editing", "1") + + # 如果是HTTP面板,重新注册数据源 + if 'http_info' in panel_data: + http_info = panel_data['http_info'] + self.createHTTPInfoPanel( + panel_id=panel_id, + url=http_info['url'], + method=http_info.get('method', 'GET'), + headers=http_info.get('headers', None), + data=http_info.get('data', None), + position=position, + size=size, + update_interval=self.data_sources.get(panel_id, {}).get('interval', + 30.0) if panel_id in self.data_sources else 30.0 + ) + + print(f"✓ 信息面板 {panel_id} 已重建") + return panel_node + + except Exception as e: + print(f"✗ 重建信息面板 {panel_data.get('panel_id', 'unknown')} 失败: {e}") + import traceback + traceback.print_exc() + return None + + def onCreateSampleInfoPanel(self): + """创建示例天气信息面板(模拟数据)""" + try: + # 获取中文字体 + from panda3d.core import TextNode + font = self.world.getChineseFont() if self.world.getChineseFont() else None + + # 使用唯一的面板ID + import time + unique_id = f"weather_info_{int(time.time())}" + + # 创建示例面板 + weather_panel = self.createInfoPanel( + panel_id=unique_id, # 使用唯一ID + position=(1.32, 0.68), + size=(1, 0.6), + bg_color=(0.15, 0.25, 0.35, 0), # 蓝色背景 + border_color=(0.3, 0.5, 0.7, 0), # 蓝色边框 + title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题 + content_color=(0.95, 0.95, 0.95, 1.0), + font=font, + bg_image="/home/tiger/图片/内部信息框2@2x.png" + ) + weather_panel.setTag("name",unique_id) + + # 更新面板标题 + self.updatePanelContent(unique_id, title="北京天气") + + self._addPanelToSceneTree(weather_panel, unique_id) + # 立即显示加载中信息 + self.updatePanelContent(unique_id, content="正在获取天气数据...") + + self.registerDataSource(unique_id, self.getRealWeatherData, update_interval=5.0) + + print("✓ 示例天气信息面板已创建") + + except Exception as e: + print(f"✗ 创建示例天气信息面板失败: {e}") + import traceback + traceback.print_exc() + QMessageBox.critical(self, "错误", f"创建示例天气信息面板时出错: {str(e)}") + + def getRealWeatherData(self): + """获取真实天气数据""" + try: + import requests + import json + from datetime import datetime + + # 请求天气数据 + url = "https://wttr.in/Beijing?format=j1" + response = requests.get(url, timeout=10) + response.raise_for_status() + + # 解析JSON数据 + weather_data = response.json() + + # 提取当前天气信息 + current_condition = weather_data['current_condition'][0] + weather_desc = current_condition['weatherDesc'][0]['value'] + temp_c = current_condition['temp_C'] + feels_like = current_condition['FeelsLikeC'] + humidity = current_condition['humidity'] + pressure = current_condition['pressure'] + visibility = current_condition['visibility'] + wind_speed = current_condition['windspeedKmph'] + wind_dir = current_condition['winddir16Point'] + + # 提取空气质量(如果可用) + air_quality = "N/A" + if 'air_quality' in weather_data and weather_data['air_quality']: + if 'us-epa-index' in current_condition: + air_quality_index = current_condition['air_quality_index'] + air_quality = f"指数: {air_quality_index}" + + # 获取更新时间 + update_time = datetime.now().strftime("%Y-%m-%d %H:%M") + + # 格式化显示内容 + content = f"天气状况: {weather_desc}\n温度: {temp_c}°C (体感 {feels_like}°C)\n湿度: {humidity}%\n气压: {pressure} hPa\n能见度: {visibility} km\n风速: {wind_speed} km/h ({wind_dir})\n空气质量: {air_quality}\n更新时间: {update_time}" + + return content + + except requests.exceptions.Timeout: + return "错误: 获取天气数据超时" + except requests.exceptions.ConnectionError: + return "错误: 网络连接失败" + except requests.exceptions.HTTPError as e: + return f"HTTP错误: {e}" + except json.JSONDecodeError: + return "错误: 无法解析天气数据" + except KeyError as e: + return f"错误: 天气数据格式不正确 (缺少字段: {e})" + except Exception as e: + return f"获取天气数据失败: {str(e)}" + + +# 示例数据源函数 +def getRealtimeData(): + """ + 获取实时数据的示例函数 + """ + import random + from datetime import datetime + + # 模拟实时数据 + value1 = round(random.uniform(10, 100), 2) + value2 = round(random.uniform(0, 50), 2) + timestamp = datetime.now().strftime("%H:%M:%S") + + return f"实时值1: {value1}\n实时值2: {value2}\n更新时间: {timestamp}" + + +# 使用示例函数 +def createSampleInfoPanel(parent_node): + """创建示例信息面板""" + + # 创建信息面板管理器 + info_manager = InfoPanelManager(parent_node) + + # 创建一个实时数据面板 + info_manager.createInfoPanel( + panel_id="realtime_data", + position=(0.8, 0.0), + size=(0.35, 0.3), + bg_color=(0.15, 0.25, 0.35, 0.95), # 蓝色背景 + border_color=(0.3, 0.5, 0.7, 1.0), # 蓝色边框 + title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题 + content_color=(0.95, 0.95, 0.95, 1.0) + ) + + # 立即显示初始数据 + initial_data = getRealtimeData() + info_manager.updatePanelContent("realtime_data", content=initial_data) + + # 注册数据源,每秒更新一次 + info_manager.registerDataSource("realtime_data", getRealtimeData, update_interval=1.0) + + return info_manager + + +# 集成到您的 property_panel.py 中的方法 +def add_methods_to_property_panel(property_panel_instance): + """ + 为 property_panel 实例添加 DirectGUI 信息面板支持 + """ + import types + + # 添加信息面板管理器作为类属性 + if not hasattr(property_panel_instance, 'info_panel_manager'): + if hasattr(property_panel_instance, 'world') and hasattr(property_panel_instance.world, 'render'): + property_panel_instance.info_panel_manager = InfoPanelManager(property_panel_instance.world.render) + else: + property_panel_instance.info_panel_manager = InfoPanelManager() + + def createRealtimeDataPanel(self, data_callback=None, update_interval=1.0): + """创建实时数据面板""" + try: + # 确保父节点已设置 + if self.info_panel_manager.parent is None and hasattr(self, 'world'): + self.info_panel_manager.setParent(self.world.render) + + # 创建实时数据面板 + panel_node = self.info_panel_manager.createInfoPanel( + panel_id="realtime_data", + position=(0.8, 0.0), + size=(0.35, 0.3), + bg_color=(0.15, 0.25, 0.35, 0.95), # 蓝色背景 + border_color=(0.3, 0.5, 0.7, 1.0), # 蓝色边框 + title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题 + content_color=(0.95, 0.95, 0.95, 1.0) + ) + + # 设置标签 + panel_node.setTag("element_type", "info_panel") + panel_node.setTag("is_scene_element", "1") + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + + # 如果提供了数据回调函数,则注册数据源 + if data_callback: + # 立即显示初始数据 + initial_data = data_callback() + self.info_panel_manager.updatePanelContent("realtime_data", content=initial_data) + + # 注册数据源 + self.info_panel_manager.registerDataSource("realtime_data", data_callback, update_interval) + else: + # 使用默认数据 + default_data = "等待数据..." + self.info_panel_manager.updatePanelContent("realtime_data", content=default_data) + + print("✓ 已创建实时数据面板") + + return panel_node + + except Exception as e: + print(f"✗ 创建实时数据面板失败: {e}") + return None + + + def createModelInfoPanel(self, model): + """为模型创建信息面板""" + try: + # 确保父节点已设置 + if self.info_panel_manager.parent is None and hasattr(self, 'world'): + self.info_panel_manager.setParent(self.world.render) + + # 获取模型信息 + model_name = model.getName() if hasattr(model, 'getName') else 'Unknown' + num_children = model.getNumChildren() if hasattr(model, 'getNumChildren') else 0 + + # 创建面板内容 + content = f"模型名称: {model_name}\n子节点数: {num_children}\n类型: {type(model).__name__}" + + # 创建或更新面板 + panel_node = self.info_panel_manager.createInfoPanel( + panel_id="model_info", + position=(0.8, 0.7), + size=(0.35, 0.25), + bg_color=(0.15, 0.15, 0.25, 0.95), # 蓝紫色背景 + border_color=(0.3, 0.3, 0.7, 1.0), # 蓝色边框 + title_color=(0.5, 0.8, 1.0, 1.0), # 浅蓝色标题 + content_color=(0.95, 0.95, 0.95, 1.0) + ) + + # 更新面板内容为模型特定信息 + self.info_panel_manager.updatePanelContent("model_info", + title="模型信息", + content=content) + + # 设置标签 + panel_node.setTag("element_type", "info_panel") + panel_node.setTag("is_scene_element", "1") + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + + print(f"✓ 已创建模型信息面板: {model_name}") + + return panel_node + + except Exception as e: + print(f"✗ 创建模型信息面板失败: {e}") + return None + + def createTerrainInfoPanel(self, terrain_info): + """为地形创建信息面板""" + try: + # 确保父节点已设置 + if self.info_panel_manager.parent is None and hasattr(self, 'world'): + self.info_panel_manager.setParent(self.world.render) + + # 获取地形信息 + terrain_name = terrain_info.get('name', 'Unknown') + width = terrain_info.get('width', 0) + height = terrain_info.get('height', 0) + resolution = terrain_info.get('resolution', 0) + + # 创建面板内容 + content = f"地形名称: {terrain_name}\n大小: {width} x {height}\n分辨率: {resolution}" + + # 创建或更新面板 + panel_node = self.info_panel_manager.createInfoPanel( + panel_id="terrain_info", + position=(0.8, 0.7), + size=(0.35, 0.25), + bg_color=(0.15, 0.25, 0.15, 0.95), # 绿色背景 + border_color=(0.3, 0.7, 0.3, 1.0), # 绿色边框 + title_color=(0.5, 1.0, 0.5, 1.0), # 浅绿色标题 + content_color=(0.95, 0.95, 0.95, 1.0) + ) + + # 更新面板内容为地形特定信息 + self.info_panel_manager.updatePanelContent("terrain_info", + title="地形信息", + content=content) + + panel_node.setTag("element_type", "info_panel") + panel_node.setTag("is_scene_element", "1") + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + + print(f"✓ 已创建地形信息面板: {terrain_name}") + + return panel_node + + except Exception as e: + print(f"✗ 创建地形信息面板失败: {e}") + return None + + def createLightInfoPanel(self, light_object): + """为光源创建信息面板""" + try: + # 确保父节点已设置 + if self.info_panel_manager.parent is None and hasattr(self, 'world'): + self.info_panel_manager.setParent(self.world.render) + + # 获取光源信息 + light_type = type(light_object).__name__ + casts_shadows = getattr(light_object, 'casts_shadows', 'Unknown') + shadow_res = getattr(light_object, 'shadow_map_resolution', 'Unknown') + + # 创建面板内容 + content = f"光源类型: {light_type}\n投射阴影: {casts_shadows}\n阴影分辨率: {shadow_res}" + + # 创建或更新面板 + panel_node = self.info_panel_manager.createInfoPanel( + panel_id="light_info", + position=(0.8, 0.7), + size=(0.35, 0.25), + bg_color=(0.25, 0.25, 0.15, 0.95), # 黄色背景 + border_color=(0.7, 0.7, 0.3, 1.0), # 黄色边框 + title_color=(1.0, 1.0, 0.5, 1.0), # 浅黄色标题 + content_color=(0.95, 0.95, 0.95, 1.0) + ) + + # 更新面板内容为光源特定信息 + self.info_panel_manager.updatePanelContent("light_info", + title="光源信息", + content=content) + + panel_node.setTag("element_type", "info_panel") + panel_node.setTag("is_scene_element", "1") + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + + print(f"✓ 已创建光源信息面板") + + return panel_node + + except Exception as e: + print(f"✗ 创建光源信息面板失败: {e}") + return None + + def removeInfoPanel(self, panel_id="model_info"): + """移除信息面板""" + if hasattr(self, 'info_panel_manager'): + self.info_panel_manager.removePanel(panel_id) + + def setupInfoPanelManager(self): + """初始化信息面板管理器""" + if not hasattr(self, 'info_panel_manager'): + if hasattr(self, 'world') and hasattr(self.world, 'render'): + self.info_panel_manager = InfoPanelManager(self.world.render) + else: + self.info_panel_manager = InfoPanelManager() + + # 将方法绑定到实例 + import types + property_panel_instance.createRealtimeDataPanel = types.MethodType(createRealtimeDataPanel, property_panel_instance) + property_panel_instance.createModelInfoPanel = types.MethodType(createModelInfoPanel, property_panel_instance) + property_panel_instance.createTerrainInfoPanel = types.MethodType(createTerrainInfoPanel, property_panel_instance) + property_panel_instance.createLightInfoPanel = types.MethodType(createLightInfoPanel, property_panel_instance) + property_panel_instance.removeInfoPanel = types.MethodType(removeInfoPanel, property_panel_instance) + property_panel_instance.setupInfoPanelManager = types.MethodType(setupInfoPanelManager, property_panel_instance) + +def fetchHTTPData(url, method="GET", headers=None, data=None, timeout=5): + """ + 获取HTTP数据的通用函数 + url: 请求的URL + method: HTTP方法 (GET, POST, etc.) + headers: 请求头 + data: POST数据 + timeout: 超时时间(秒) + """ + try: + if headers is None: + headers = { + 'User-Agent': 'InfoPanelManager/1.0', + 'Accept': 'application/json,text/plain,*/*' + } + + # 根据方法发送请求 + if method.upper() == "GET": + response = requests.get(url, headers=headers, timeout=timeout) + elif method.upper() == "POST": + response = requests.post(url, headers=headers, data=data, timeout=timeout) + else: + response = requests.request(method, url, headers=headers, data=data, timeout=timeout) + + # 检查响应状态 + response.raise_for_status() + + # 尝试解析JSON + try: + json_data = response.json() + return formatJSONData(json_data) + except: + # 如果不是JSON,返回文本内容 + return response.text[:500] # 限制长度 + + except requests.exceptions.Timeout: + return "错误: 请求超时" + except requests.exceptions.ConnectionError: + return "错误: 连接失败" + except requests.exceptions.HTTPError as e: + return f"HTTP错误: {e}" + except Exception as e: + return f"获取数据失败: {str(e)}" + + +def formatJSONData(data, indent=0): + """ + 格式化JSON数据为易读的文本 + """ + if isinstance(data, dict): + lines = [] + for key, value in data.items(): + if isinstance(value, (dict, list)): + lines.append(f"{' ' * indent}{key}:") + lines.append(formatJSONData(value, indent + 1)) + else: + lines.append(f"{' ' * indent}{key}: {value}") + return "\n".join(lines) + elif isinstance(data, list): + lines = [] + for i, item in enumerate(data): + if isinstance(item, (dict, list)): + lines.append(f"{' ' * indent}[{i}]:") + lines.append(formatJSONData(item, indent + 1)) + else: + lines.append(f"{' ' * indent}[{i}] {item}") + return "\n".join(lines) + else: + return str(data) diff --git a/core/__init__.py b/core/__init__.py index d1b5ed41..8ee75f7d 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,26 +1,26 @@ -""" -Core package - 核心功能模块 - -包含引擎的核心功能: -- world.py: 基础世界功能(相机、光照、地板等) -- selection.py: 选择和变换系统 -- event_handler.py: 事件处理系统 -- tool_manager.py: 工具管理系统 -- script_system.py: 脚本系统 -""" - -from .world import CoreWorld -from .selection import SelectionSystem -from .event_handler import EventHandler -from .tool_manager import ToolManager -from .script_system import ScriptManager, ScriptBase, ScriptComponent - -__all__ = [ - 'CoreWorld', - 'SelectionSystem', - 'EventHandler', - 'ToolManager', - 'ScriptManager', - 'ScriptBase', - 'ScriptComponent' +""" +Core package - 核心功能模块 + +包含引擎的核心功能: +- world.py: 基础世界功能(相机、光照、地板等) +- selection.py: 选择和变换系统 +- event_handler.py: 事件处理系统 +- tool_manager.py: 工具管理系统 +- script_system.py: 脚本系统 +""" + +from .world import CoreWorld +from .selection import SelectionSystem +from .event_handler import EventHandler +from .tool_manager import ToolManager +from .script_system import ScriptManager, ScriptBase, ScriptComponent + +__all__ = [ + 'CoreWorld', + 'SelectionSystem', + 'EventHandler', + 'ToolManager', + 'ScriptManager', + 'ScriptBase', + 'ScriptComponent' ] \ No newline at end of file diff --git a/core/assembly_interaction.py b/core/assembly_interaction.py index c2917e8d..1a201ed4 100644 --- a/core/assembly_interaction.py +++ b/core/assembly_interaction.py @@ -1,2339 +1,2339 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import json -import os -from direct.showbase.DirectObject import DirectObject -from direct.task import Task -from panda3d.core import CollisionTraverser, CollisionNode, CollisionHandlerQueue, CollisionRay -from panda3d.core import BitMask32, Vec3, Point3, Plane -from PyQt5.QtWidgets import QMessageBox, QFileDialog, QDialog, QVBoxLayout, QHBoxLayout -from PyQt5.QtWidgets import QLabel, QPushButton, QTextEdit, QComboBox, QGroupBox -from PyQt5.QtCore import Qt - -# 导入维修系统GUI -try: - from core.maintenance_gui import MaintenanceGUI -except ImportError as e: - print(f"⚠️ 导入维修GUI失败: {e}") - MaintenanceGUI = None - - -class AssemblyInteractionManager(DirectObject): - """拆装交互管理器(优化版)""" - - def __init__(self, world): - DirectObject.__init__(self) - self.world = world - self.config_data = None - self.current_step = 0 - self.total_steps = 0 - self.is_active = False - - # --- 模式控制 --- - self.mode = "training" # 默认训练模式,可选 "training" 或 "exam" - - # --- 拖拽相关(优化) --- - self.dragging_model = None - self.drag_offset = Vec3(0, 0, 0) - self.drag_plane = Plane(Vec3(0, 1, 0), Point3(0, 0, 0)) # 初始化一个平面 - - # --- 安装/拆卸状态管理 --- - self.model_original_positions = {} # 记录模型的原始位置 - self.model_current_states = {} # 记录模型当前状态 ('installed' 或 'removed') - - # --- UI组件 --- - self.step_dialog = None - - # --- 维修系统GUI --- - self.maintenance_gui = None - if MaintenanceGUI: - self.maintenance_gui = MaintenanceGUI(world) - - # --- 操作权限控制 --- - self.operation_enabled = True # 是否允许进行操作 - - # --- 考核相关 --- - self.exam_score = 0 # 考核总分 - self.exam_max_score = 0 # 考核满分 - self.step_scores = {} # 每步得分记录 - - # --- 碰撞检测(优化) --- - self.picker_traverser = CollisionTraverser('picker_traverser') - self.collision_handler = CollisionHandlerQueue() - self.picker_ray_node = None - - print("拆装交互管理器初始化完成") - - def init_exam_mode(self): - """初始化考核模式""" - print("📝 初始化考核模式...") - - # 重置考核数据 - self.exam_score = 0 - self.exam_max_score = 0 - self.step_scores = {} - - # 计算总分 - steps = self.config_data.get('steps', []) - for i, step_data in enumerate(steps): - step_score = step_data.get('score', 10) # 默认每步10分 - self.exam_max_score += step_score - self.step_scores[i] = { - 'max_score': step_score, - 'current_score': step_score, # 初始满分,操作错误时扣分 - 'tool_error': False, # 是否有工具错误 - 'operation_attempts': 0 # 操作尝试次数 - } - - print(f"📝 考核模式初始化完成,总分: {self.exam_max_score} 分") - - def play_step_audio(self, step_data): - """播放步骤音频""" - try: - # 考核模式下不播放音频 - if self.mode == "exam": - print("🔇 考核模式,音频播放已禁用") - return - - # 检查是否启用了自动播放音频 - settings = self.config_data.get('settings', {}) - auto_play_audio = settings.get('auto_play_audio', True) - - if not auto_play_audio: - print("🔇 音频播放已禁用") - return - - # 获取音频文件路径 - audio_file = step_data.get('audio_file', '') - - if not audio_file or audio_file.strip() == '': - print("🔇 当前步骤没有配置音频文件") - return - - print(f"🔊 准备播放步骤音频: {audio_file}") - - import os - # 检查音频文件格式 - file_ext = os.path.splitext(audio_file)[1].lower() - print(f"🔊 音频文件格式: {file_ext}") - - # 对于MP3文件,优先使用pygame播放 - if file_ext in ['.mp3', '.m4a', '.aac']: - print("🔊 检测到MP3/M4A/AAC格式,优先尝试pygame播放") - if self.play_audio_with_pygame(audio_file): - return - print("🔊 pygame不可用,尝试备用播放方法") - self.play_audio_fallback(audio_file) - return - - # 检查音频文件是否存在 - import os - if not os.path.isfile(audio_file): - # 如果不是绝对路径,尝试在项目目录下查找 - if not os.path.isabs(audio_file): - # 尝试在当前目录、Resources目录等位置查找 - possible_paths = [ - audio_file, - os.path.join("Resources", audio_file), - os.path.join("Resources", "audio", audio_file), - os.path.join("audio", audio_file), - os.path.join("sounds", audio_file) - ] - - audio_file_found = None - for path in possible_paths: - if os.path.isfile(path): - audio_file_found = path - break - - if audio_file_found: - audio_file = audio_file_found - print(f"🔊 找到音频文件: {audio_file}") - else: - print(f"❌ 找不到音频文件: {audio_file}") - print(f" 尝试的路径: {possible_paths}") - return - else: - print(f"❌ 音频文件不存在: {audio_file}") - return - - # 使用Panda3D的音频系统播放音频 - if hasattr(self.world, 'loader') and self.world.loader: - try: - # 检查音频系统是否启用 - from panda3d.core import AudioManager - print(f"🔊 音频管理器状态: {AudioManager.createAudioManager()}") - - # 加载音频文件 - print(f"🔊 尝试加载音频文件: {audio_file}") - audio_sound = self.world.loader.loadSfx(audio_file) - - if audio_sound: - print(f"✅ 音频文件加载成功: {os.path.basename(audio_file)}") - print(f"🔊 音频状态: {audio_sound.status()}") - audio_length = audio_sound.length() - print(f"🔊 音频长度: {audio_length:.2f}秒") - - # 检查音频文件是否有效 - if audio_length <= 0: - print("⚠️ 音频长度为0,可能是格式不支持或文件损坏") - print("🔊 尝试使用pygame播放") - if not self.play_audio_with_pygame(audio_file): - self.play_audio_fallback(audio_file) - return - - # 设置音量(在播放前设置) - audio_sound.setVolume(0.8) - print(f"🔊 设置音量: 80%") - - # 播放音频 - audio_sound.play() - print(f"✅ 开始播放音频: {os.path.basename(audio_file)}") - - # 等待一小段时间让播放状态更新 - import time - time.sleep(0.1) - - final_status = audio_sound.status() - print(f"🔊 播放后状态: {final_status}") - - # 检查是否真的在播放 (状态码2表示PLAYING) - if final_status == 2: # PLAYING - print("✅ 音频正在播放中") - elif final_status == 1: # READY - print("⚠️ 音频处于就绪状态但未播放,尝试pygame") - if not self.play_audio_with_pygame(audio_file): - self.play_audio_fallback(audio_file) - else: - print(f"⚠️ 音频状态异常: {final_status}") - if not self.play_audio_with_pygame(audio_file): - self.play_audio_fallback(audio_file) - else: - print(f"❌ 无法加载音频文件: {audio_file}") - print("🔊 Panda3D加载失败,尝试pygame") - if not self.play_audio_with_pygame(audio_file): - self.play_audio_fallback(audio_file) - - except Exception as e: - print(f"❌ Panda3D音频播放失败: {e}") - import traceback - traceback.print_exc() - # 尝试使用pygame播放(备用方案) - print("🔊 Panda3D播放失败,尝试pygame") - if not self.play_audio_with_pygame(audio_file): - self.play_audio_fallback(audio_file) - else: - print("⚠️ 找不到Panda3D音频加载器,尝试pygame") - if not self.play_audio_with_pygame(audio_file): - self.play_audio_fallback(audio_file) - - except Exception as e: - print(f"❌ 音频播放失败: {e}") - import traceback - traceback.print_exc() - - def play_audio_with_pygame(self, audio_file): - """使用pygame播放音频(优先方法)""" - try: - import pygame - print("🔊 使用pygame播放音频") - - # 初始化pygame mixer - if not pygame.mixer.get_init(): - pygame.mixer.pre_init(frequency=22050, size=-16, channels=2, buffer=1024) - pygame.mixer.init() - print("🔊 pygame mixer初始化完成") - - # 停止当前播放的音频(如果有) - if pygame.mixer.music.get_busy(): - pygame.mixer.music.stop() - print("🔊 停止之前的音频") - - # 加载并播放音频文件 - pygame.mixer.music.load(audio_file) - pygame.mixer.music.set_volume(0.8) - pygame.mixer.music.play() - - print(f"✅ pygame开始播放音频: {os.path.basename(audio_file)}") - - # 在后台线程中监控播放完成 - def wait_for_pygame_completion(): - import time - while pygame.mixer.music.get_busy(): - time.sleep(0.1) - print(f"🎵 pygame音频播放完成: {os.path.basename(audio_file)}") - - import threading - threading.Thread(target=wait_for_pygame_completion, daemon=True).start() - - print("🎵 音频将在后台完整播放") - return True - - except ImportError: - print("⚠️ pygame未安装") - return False - except Exception as e: - print(f"⚠️ pygame播放失败: {e}") - import traceback - traceback.print_exc() - return False - - def play_audio_fallback(self, audio_file): - """备用音频播放方法""" - import os - print(f"🔊 尝试备用音频播放方法...") - - # 方法1: 使用系统默认播放器(最简单) - try: - print("🔊 尝试使用系统默认播放器") - if os.name == 'nt': # Windows - os.startfile(audio_file) - print(f"✅ 使用系统默认播放器播放: {os.path.basename(audio_file)}") - return - elif os.name == 'posix': # Linux/Mac - os.system(f'xdg-open "{audio_file}"') - print(f"✅ 使用系统默认播放器播放: {os.path.basename(audio_file)}") - return - except Exception as e: - print(f"⚠️ 系统默认播放器失败: {e}") - - # 方法2: 尝试使用Windows Media Player - try: - if os.name == 'nt': # Windows - print("🔊 尝试使用Windows Media Player") - cmd = f'start wmplayer "{audio_file}"' - os.system(cmd) - print(f"✅ 使用Windows Media Player播放音频: {os.path.basename(audio_file)}") - return - except Exception as e: - print(f"⚠️ Windows Media Player播放失败: {e}") - - # 方法3: 使用PowerShell播放WAV文件 - try: - if os.name == 'nt': - file_ext = os.path.splitext(audio_file)[1].lower() - if file_ext in ['.wav']: - print("🔊 尝试使用PowerShell播放WAV文件") - import subprocess - cmd = f'powershell -c "(New-Object Media.SoundPlayer \\"{audio_file}\\").PlaySync()"' - subprocess.run(cmd, shell=True) - print(f"✅ 使用PowerShell播放音频: {os.path.basename(audio_file)}") - return - except Exception as e: - print(f"⚠️ PowerShell播放失败: {e}") - - # 方法4: 简单的系统调用 - try: - print("🔊 尝试使用系统默认播放器") - if os.name == 'nt': # Windows - os.startfile(audio_file) - elif os.name == 'posix': # Linux/Mac - os.system(f'xdg-open "{audio_file}"') - print(f"✅ 使用系统默认播放器播放音频: {os.path.basename(audio_file)}") - except Exception as e: - print(f"❌ 所有备用音频播放方法都失败了: {e}") - print("💡 建议检查:") - print(" 1. 音频文件是否存在且格式正确") - print(" 2. 系统音频驱动是否正常") - print(" 3. 音频文件是否损坏") - - def set_operation_enabled(self, enabled): - """设置操作是否启用""" - self.operation_enabled = enabled - print(f"🔧 操作权限: {'启用' if enabled else '禁用'}") - - def check_operation_permission(self): - """检查操作权限""" - if not self.is_active: - print("⚠️ 交互模式未激活") - return False - - # 获取当前步骤数据 - if self.current_step >= self.total_steps: - print("⚠️ 所有步骤已完成") - return False - - step_data = self.config_data['steps'][self.current_step] - required_tool = step_data.get('required_tool', '无') - - # 获取当前选择的工具(优先从维修GUI获取) - current_tool = "无" - if self.maintenance_gui: - current_tool = self.maintenance_gui.get_current_tool() - elif hasattr(self, 'step_dialog') and self.step_dialog and hasattr(self.step_dialog, 'tool_combo'): - current_tool = self.step_dialog.tool_combo.currentText() - - # 使用 getChinese 处理工具名称 - if hasattr(self.world, 'getChinese'): - required_tool = self.world.getChinese(required_tool) - current_tool = self.world.getChinese(current_tool) - - print(f"🔧 工具检查: 需要'{required_tool}',当前'{current_tool}'") - - # 检查工具是否匹配 - tool_matches = self.check_tool_match(current_tool, required_tool) - - if self.mode == "training": - # 训练模式:显示GUI警告并阻止错误操作 - if not tool_matches: - print("⚠️ 训练模式 - 工具不匹配,无法进行操作") - - # 在维修GUI中显示警告 - if self.maintenance_gui: - warning_msg = f"请先选择 '{required_tool}' 工具!" - self.maintenance_gui.show_warning(warning_msg, 2.0) - # 也在Qt对话框中显示警告(仅在非维修系统模式下) - elif hasattr(self, 'step_dialog') and self.step_dialog: - QMessageBox.warning(self.step_dialog, "工具不匹配", - f"当前步骤需要使用 '{required_tool}' 工具,请先选择正确的工具!") - return False - return True - else: - # 考核模式:不显示提示,但记录工具错误并阻止操作 - if not tool_matches: - print(f"❌ 考核模式 - 工具错误:需要'{required_tool}',实际选择'{current_tool}',操作被阻止") - - # 记录工具错误(扣分) - self.record_tool_error() - return False - return True - - def check_tool_match(self, current_tool, required_tool): - """检查工具是否匹配""" - # 如果步骤不要求特定工具,任何工具都可以 - if required_tool == "无" or required_tool == "" or not required_tool: - return True - - # 精确匹配 - if current_tool == required_tool: - return True - - return False - - def record_tool_error(self): - """记录工具错误(考核模式下扣分)""" - if self.mode != "exam": - return - - # 获取当前步骤的考核记录 - if self.current_step in self.step_scores: - step_record = self.step_scores[self.current_step] - - # 如果是第一次工具错误,扣除一定分数 - if not step_record['tool_error']: - step_record['tool_error'] = True - # 扣除该步骤50%的分数作为工具错误惩罚 - # penalty = step_record['max_score'] * 0.5 - penalty = step_record['max_score'] - step_record['current_score'] = max(0, step_record['current_score'] - penalty) - - print(f"📝 考核记录:工具错误,扣除{penalty:.0f}分,当前步骤剩余{step_record['current_score']:.0f}分") - - # 增加操作尝试次数 - step_record['operation_attempts'] += 1 - - def start_interaction_mode(self, mode="training"): - """启动交互模式""" - try: - # 设置模式 - self.mode = mode - print(f"🎯 启动拆装交互模式: {self.mode}") - - # 如果没有配置数据,尝试加载 - if not self.config_data: - if not self.load_configuration(): - return False - - if not self.config_data or not self.config_data.get('steps'): - QMessageBox.warning(None, "警告", "没有找到有效的配置步骤!\n请先在'拆装配置'中设置步骤。") - return False - - self.total_steps = len(self.config_data['steps']) - self.current_step = 0 - self.is_active = True - - # 设置维修系统GUI - if self.maintenance_gui: - tools_list = self.config_data.get('tools', ['手']) - self.maintenance_gui.setup_gui(tools_list, mode) - self.maintenance_gui.show_gui() - print(f"✅ 维修系统GUI已启动,模式: {mode}") - - # 如果是考核模式,初始化考核相关数据 - - if self.mode == "exam": - self.init_exam_mode() - # 显示考核开始提示 - if self.maintenance_gui: - exam_start_msg = f"📝 考核模式已启动\n\n总分: {self.exam_max_score} 分\n步骤数: {self.total_steps}\n\n✅ 可以选择和切换工具\n❌ 不会显示步骤描述和语音提示\n❌ 使用错误工具会扣分" - self.maintenance_gui.update_step_info(exam_start_msg) - print("📝 考核开始提示已在GUI中显示") - - # 设置碰撞检测 - self.setup_picking() - - # 拦截Qt层面的鼠标事件处理 - print("🔄 拦截Qt层面的鼠标事件处理") - print(f"🔍 检查world对象: {type(self.world).__name__}") - print(f"🔍 是否有qtWidget属性: {hasattr(self.world, 'qtWidget')}") - if hasattr(self.world, 'qtWidget'): - print(f"🔍 qtWidget值: {self.world.qtWidget}") - print(f"🔍 qtWidget类型: {type(self.world.qtWidget)}") - - if hasattr(self.world, 'qtWidget') and self.world.qtWidget: - # 备份原有的鼠标事件处理方法 - self.original_mouse_press_event = self.world.qtWidget.mousePressEvent - self.original_mouse_release_event = self.world.qtWidget.mouseReleaseEvent - - print(f"🔍 原有鼠标事件方法: {self.original_mouse_press_event}") - - # 替换为我们的处理方法 - self.world.qtWidget.mousePressEvent = self.qt_mouse_press_event - self.world.qtWidget.mouseReleaseEvent = self.qt_mouse_release_event - print("✅ Qt鼠标事件已被拦截") - else: - print("⚠️ 找不到Qt部件,使用Panda3D事件系统") - # 作为备用方案,仍然绑定Panda3D事件 - self.accept("mouse1", self.on_mouse_down) - self.accept("mouse1-up", self.on_mouse_up) - - # 绑定键盘备用触发 - self.accept("space", self.trigger_drag_by_keyboard) - self.accept("s", self.show_models_status) # S键显示模型状态 - self.accept("r", self.record_current_positions) # R键记录当前位置为原始位置 - - print("✅ 拆装交互事件绑定完成") - print("💡 提示: 按S键可查看所有模型状态,按R键重新记录原始位置") - - print("拆装交互模式已启动,核心事件已绑定。") - print("请点击目标模型进行操作。") - - # 调试:列出场景中的所有模型 - self.list_scene_models() - - # 显示步骤指引界面(维修系统使用GUI界面,跳过Qt对话框) - if not self.maintenance_gui: - self.show_step_dialog() - - # 开始第一步 - self.start_current_step() - - print(f"共 {self.total_steps} 步") - return True - - except Exception as e: - QMessageBox.critical(None, "错误", f"启动交互模式失败: {str(e)}") - import traceback - traceback.print_exc() - return False - - def load_configuration(self): - """加载拆装配置""" - try: - file_path, _ = QFileDialog.getOpenFileName( - None, "选择拆装配置文件", "", - "JSON文件 (*.json);;所有文件 (*)" - ) - if not file_path: - return False - - with open(file_path, 'r', encoding='utf-8') as f: - self.config_data = json.load(f) - - self.total_steps = len(self.config_data.get('steps', [])) - self.current_step = 0 - - # 从配置文件中恢复原始位置信息 - self.load_original_positions_from_config() - - print(f"成功加载配置: {file_path}") - return True - - except Exception as e: - QMessageBox.critical(None, "错误", f"加载配置失败: {str(e)}") - return False - - def load_original_positions_from_config(self): - """从配置文件中加载模型的原始位置信息""" - print("📍 从配置文件恢复模型原始位置...") - - # 清空现有的位置记录 - self.model_original_positions.clear() - self.model_current_states.clear() - - # 从配置文件的models部分读取原始位置(models是数组格式) - models_config = self.config_data.get('models', []) - - for model_data in models_config: - model_name = model_data.get('name') - if not model_name: - continue - - # 尝试从配置文件中获取原始位置 - original_pos_data = model_data.get('original_pos') - - if original_pos_data and len(original_pos_data) >= 3: - # 如果配置文件中有原始位置,使用配置文件中的位置 - original_pos = Vec3(original_pos_data[0], original_pos_data[1], original_pos_data[2]) - self.model_original_positions[model_name] = original_pos - self.model_current_states[model_name] = 'installed' - print(f" 📍 {model_name}: 从配置文件恢复原始位置 {original_pos}") - else: - # 如果配置文件中没有原始位置,使用当前场景中的位置作为原始位置 - target_node = self.find_model_node(model_name) - if target_node and not target_node.isEmpty(): - current_pos = target_node.getPos() - self.model_original_positions[model_name] = current_pos - self.model_current_states[model_name] = 'installed' - print(f" 📍 {model_name}: 使用当前场景位置作为原始位置 {current_pos}") - else: - print(f" ❌ {model_name}: 在场景中找不到对应模型") - - # 如果配置文件中没有models部分,从步骤中提取模型信息 - if not models_config: - print(" 配置文件中没有models部分,从步骤中提取模型信息...") - for step_data in self.config_data.get('steps', []): - target_model_name = step_data.get('target_model') - if target_model_name and target_model_name not in self.model_original_positions: - target_node = self.find_model_node(target_model_name) - if target_node and not target_node.isEmpty(): - current_pos = target_node.getPos() - self.model_original_positions[target_model_name] = current_pos - self.model_current_states[target_model_name] = 'installed' - print(f" 📍 {target_model_name}: 从场景记录原始位置 {current_pos}") - - print(f"📍 原始位置恢复完成,共记录 {len(self.model_original_positions)} 个模型") - - # 检查配置文件的步骤完整性 - self.check_step_completeness() - - def check_step_completeness(self): - """检查配置文件的步骤完整性""" - print("\n🔍 检查步骤配置完整性...") - - steps = self.config_data.get('steps', []) - if not steps: - print("⚠️ 配置文件中没有步骤") - return - - # 统计每个模型的拆卸和安装步骤 - model_operations = {} - - for i, step in enumerate(steps): - target_model = step.get('target_model') - step_type = self.normalize_step_type(step) - - if target_model: - if target_model not in model_operations: - model_operations[target_model] = {'disassemble': [], 'assemble': []} - - model_operations[target_model][step_type].append(i + 1) - - # 检查每个模型是否有配对的拆卸和安装步骤 - incomplete_models = [] - for model_name, operations in model_operations.items(): - disassemble_count = len(operations['disassemble']) - assemble_count = len(operations['assemble']) - - print(f" 📋 {model_name}: 拆卸步骤 {disassemble_count} 个, 安装步骤 {assemble_count} 个") - - if disassemble_count > 0 and assemble_count == 0: - incomplete_models.append(model_name) - print(f" ⚠️ 只有拆卸步骤,缺少安装步骤") - elif disassemble_count != assemble_count: - print(f" ⚠️ 拆卸和安装步骤数量不匹配") - - if incomplete_models: - print(f"\n⚠️ 发现 {len(incomplete_models)} 个模型缺少安装步骤:") - for model in incomplete_models: - print(f" - {model}") - print("💡 建议为每个拆卸的模型添加对应的安装步骤") - else: - print("\n✅ 所有模型都有完整的拆卸和安装步骤配置") - - def setup_picking(self): - """设置用于鼠标拾取的碰撞检测系统""" - try: - # 创建一个碰撞射线 - picker_ray = CollisionRay() - - # 创建一个用于放置射线的碰撞节点 - picker_ray_node = CollisionNode('mouse_ray') - picker_ray_node.addSolid(picker_ray) - - # 设置碰撞掩码,确保它只与可拾取的物体碰撞 - # 这里我们假设可拾取的物体有第1位的碰撞比特 - picker_ray_node.setFromCollideMask(BitMask32.bit(1)) - picker_ray_node.setIntoCollideMask(BitMask32.allOff()) - - # 将碰撞节点附加到相机上,这样射线就会跟随相机移动 - self.picker_ray_node = self.world.camera.attachNewNode(picker_ray_node) - - # 将碰撞节点添加到遍历器中,并指定一个处理器来存储碰撞结果 - self.picker_traverser.addCollider(self.picker_ray_node, self.collision_handler) - - print("射线拾取系统设置完成") - - except Exception as e: - print(f"设置碰撞检测失败: {e}") - - def show_step_dialog(self): - """显示步骤指引对话框""" - if self.step_dialog: - self.step_dialog.close() - - # 获取主窗口作为父窗口 - parent_window = None - if hasattr(self.world, 'main_window') and self.world.main_window: - parent_window = self.world.main_window - - self.step_dialog = StepGuideDialog(self, parent_window) - self.step_dialog.show() - - def start_current_step(self): - """开始当前步骤""" - if self.current_step >= self.total_steps: - self.finish_interaction() - return - - step_data = self.config_data['steps'][self.current_step] - - # 兼容中文和英文配置格式 - operation_type = self.normalize_operation_type(step_data) - step_type = self.normalize_step_type(step_data) - target_model_name = step_data.get('target_model', '未设置') - - # 调试信息:显示字段映射结果 - print(f"🔄 字段映射: '{step_data.get('type', 'N/A')}' -> '{step_type}'") - print(f"🔄 操作映射: '{step_data.get('interaction_type', 'N/A')}' -> '{operation_type}'") - - # 使用 getChinese 处理步骤名称 - step_name = step_data.get('name', '未命名') - if hasattr(self.world, 'getChinese'): - step_name = self.world.getChinese(step_name) - target_model_name = self.world.getChinese(target_model_name) - - print(f"\n=== 开始第 {self.current_step + 1} 步: {step_name} ===") - print(f"目标模型: {target_model_name}") - print(f"操作类型: {operation_type}") - print(f"步骤类型: {step_type}") - - # 显示容差信息 - if step_type == 'assemble': - snap_tolerance = step_data.get('snap_tolerance') or step_data.get('tolerance', 5.0) - print(f"吸附容差: {snap_tolerance:.1f} 单位") - else: - disassemble_threshold = step_data.get('disassemble_threshold', 5.0) - print(f"拆卸阈值: {disassemble_threshold:.1f} 单位(距离原位)") - - # 检查模型的原始位置记录 - if target_model_name: - if target_model_name in self.model_original_positions: - original_pos = self.model_original_positions[target_model_name] - print(f"📍 模型 '{target_model_name}' 的原始位置: {original_pos}") - else: - # 如果没有记录,使用当前位置作为原始位置(但会给出警告) - target_node = self.find_model_node(target_model_name) - if target_node and not target_node.isEmpty(): - current_pos = target_node.getPos() - self.model_original_positions[target_model_name] = current_pos - self.model_current_states[target_model_name] = 'installed' - print(f"⚠️ 模型 '{target_model_name}' 没有预设原始位置,使用当前位置: {current_pos}") - print(" 建议在配置文件中预设模型的original_position以确保一致性") - - # 如果是安装步骤,确保模型显示 - if step_type == 'assemble' and target_model_name: - target_node = self.find_model_node(target_model_name) - if target_node and not target_node.isEmpty(): - if target_node.isHidden(): - print(f"👁️ 安装步骤开始 - 显示隐藏的模型: {target_model_name}") - target_node.show() - else: - print(f"✅ 模型 '{target_model_name}' 已经可见") - - # 播放步骤音频(如果有配置) - self.play_step_audio(step_data) - - # 更新维修系统GUI的步骤信息 - if self.maintenance_gui: - step_name = step_data.get('name', f'步骤 {self.current_step + 1}') - step_description = step_data.get('description', '') - - # 使用 getChinese 处理文本 - if hasattr(self.world, 'getChineseFont'): - step_name = self.world.getChineseFont() - step_description = self.world.getChineseFont() - - if self.mode == "exam": - # 考核模式:只显示步骤编号和名称,不显示描述 - step_info = f"Step {self.current_step + 1}/{self.total_steps}: {step_name}" - print(f"📝 考核模式步骤信息: {step_info}") - else: - # 训练模式:显示完整信息 - step_info = f"Step: {self.current_step + 1}/{self.total_steps}: {step_name}" - if step_description: - step_info += f"\n{step_description}" - print(f"📚 训练模式步骤信息: {step_info}") - - self.maintenance_gui.update_step_info(step_info) - - # 更新Qt步骤对话框(仅在非维修系统模式下) - if self.step_dialog and not self.maintenance_gui: - self.step_dialog.update_step_info(step_data, self.current_step + 1, self.total_steps) - - # 准备交互(主要是确保目标模型可以被拾取) - self.prepare_interaction(step_data) - - def prepare_interaction(self, step_data): - """准备交互,主要是设置目标模型的碰撞掩码""" - target_model_name = step_data.get('target_model') - if not target_model_name: - print("警告: 当前步骤没有指定目标模型") - return - - target_node = self.find_model_node(target_model_name) - if not target_node: - print(f"警告: 找不到目标模型 '{target_model_name}'") - return - - # 给目标模型设置碰撞掩码,使其可以被射线拾取 - # BitMask32.bit(1) 对应 setup_picking 中设置的 from_collide_mask - target_node.setCollideMask(BitMask32.bit(1)) - print(f"已为模型 '{target_model_name}' 设置碰撞掩码,等待用户点击。") - - def find_model_node(self, model_name): - """在场景中查找指定名称的模型节点""" - # find() 是Panda3D提供的更高效的节点查找方法 - return self.world.render.find(f"**/{model_name}") - - def list_scene_models(self): - """列出场景中的所有模型(调试用)""" - print("\n📋 场景中的模型列表:") - all_nodes = self.world.render.findAllMatches("**") - model_count = 0 - for node_path in all_nodes: - node_name = node_path.getName() - # 过滤掉系统节点和碰撞节点 - if (node_name and - node_name not in ['render', 'camera', 'aspect2d', 'pixel2d', 'cam'] and - not node_name.startswith("modelCollision_") and - not node_name.startswith("collision") and - not node_name.startswith("picker")): - print(f" - {node_name}") - model_count += 1 - print(f"总共找到 {model_count} 个模型节点\n") - - def show_models_status(self): - """显示所有模型的当前状态""" - if not self.model_original_positions: - print("📊 没有记录任何模型状态") - return - - print("\n📊 模型状态信息:") - for model_name, original_pos in self.model_original_positions.items(): - current_state = self.model_current_states.get(model_name, 'unknown') - model_node = self.find_model_node(model_name) - - if model_node and not model_node.isEmpty(): - current_pos = model_node.getPos() - distance_from_original = (current_pos - original_pos).length() - - status_icon = "🔧" if current_state == 'installed' else "📦" if current_state == 'removed' else "❓" - print(f" {status_icon} {model_name}:") - print(f" 状态: {current_state}") - print(f" 原始位置: {original_pos}") - print(f" 当前位置: {current_pos}") - print(f" 距离原位: {distance_from_original:.2f}") - else: - print(f" ❌ {model_name}: 找不到模型节点") - print() - - def record_current_positions(self): - """将当前所有模型的位置记录为原始位置""" - print("\n📍 重新记录所有模型的原始位置...") - - # 从步骤中提取所有模型 - all_models = set() - for step_data in self.config_data.get('steps', []): - target_model = step_data.get('target_model') - if target_model: - all_models.add(target_model) - - # 记录每个模型的当前位置为原始位置 - for model_name in all_models: - target_node = self.find_model_node(model_name) - if target_node and not target_node.isEmpty(): - current_pos = target_node.getPos() - self.model_original_positions[model_name] = current_pos - self.model_current_states[model_name] = 'installed' - print(f" 📍 {model_name}: 记录新的原始位置 {current_pos}") - else: - print(f" ❌ {model_name}: 在场景中找不到对应模型") - - print(f"📍 重新记录完成,共记录 {len(self.model_original_positions)} 个模型") - print("💡 提示: 如需永久保存这些位置,请在配置界面中重新保存配置文件") - - def normalize_step_type(self, step_data): - """标准化步骤类型,兼容中文和英文配置""" - # 优先使用英文字段 - step_type = step_data.get('step_type') - if step_type: - return step_type - - # 兼容中文字段 - type_field = step_data.get('type', '') - if type_field == '拆卸': - return 'disassemble' - elif type_field == '安装': - return 'assemble' - else: - return 'disassemble' # 默认值 - - def normalize_operation_type(self, step_data): - """标准化操作类型,兼容中文和英文配置""" - # 优先使用英文字段 - operation_type = step_data.get('operation_type') - if operation_type: - return operation_type - - # 兼容中文字段 - interaction_type = step_data.get('interaction_type', '') - if interaction_type == '鼠标拖拽': - return 'drag' - elif interaction_type == '点击触发': - return 'click' - else: - return 'drag' # 默认值 - - def analyze_final_state(self): - """分析最终状态,检查哪些模型没有正确回到原位""" - print("\n📊 最终状态分析:") - - if not self.model_original_positions: - print("没有模型状态记录") - return - - incorrect_models = [] - for model_name, original_pos in self.model_original_positions.items(): - model_node = self.find_model_node(model_name) - if model_node and not model_node.isEmpty(): - current_pos = model_node.getPos() - distance = (current_pos - original_pos).length() - current_state = self.model_current_states.get(model_name, 'unknown') - - # 检查是否应该在原位但实际不在原位的模型 - if current_state == 'installed' and distance > 1.0: # 容差1.0单位 - incorrect_models.append({ - 'name': model_name, - 'expected_pos': original_pos, - 'actual_pos': current_pos, - 'distance': distance - }) - print(f"❌ {model_name}: 应该在原位但偏离了 {distance:.2f} 单位") - print(f" 期望位置: {original_pos}") - print(f" 实际位置: {current_pos}") - elif current_state == 'installed' and distance <= 1.0: - print(f"✅ {model_name}: 正确安装在原位 (偏差 {distance:.2f})") - elif current_state == 'removed': - print(f"📦 {model_name}: 已拆卸 (距离原位 {distance:.2f})") - - if incorrect_models: - print(f"\n⚠️ 发现 {len(incorrect_models)} 个模型没有正确回到原位") - print("💡 建议检查安装步骤的配置和吸附容差设置") - else: - print("\n✅ 所有模型都处于正确状态") - - def qt_mouse_press_event(self, event): - """Qt鼠标按下事件处理(选择性拦截版)""" - from PyQt5.QtCore import Qt - - # 只拦截左键,其他按键使用原有处理方式 - if event.button() == Qt.LeftButton: - print('🖱️ 左键被拦截处理') - print(f'🖱️ 左键点击位置: ({event.x()}, {event.y()})') - - # 检查是否点击在GUI区域 - if self.maintenance_gui and self.is_click_in_gui_area(event.x(), event.y()): - print('🎯 点击在GUI区域,直接处理工具按钮点击') - # 直接处理工具按钮点击,不依赖DirectGUI事件 - button_clicked = self.handle_gui_button_click(event.x(), event.y()) - if button_clicked: - print(f'🎯 工具按钮点击已处理: {button_clicked}') - event.accept() - return - - # 如果不是按钮点击,让原有事件处理器处理 - if hasattr(self, 'original_mouse_press_event'): - self.original_mouse_press_event(event) - else: - event.ignore() - return - - # 点击在3D区域,进行拦截处理 - print('🎯 点击在3D区域,进行拦截处理') - self.handle_qt_mouse_click(event.x(), event.y()) - event.accept() - else: - # 其他按键使用原有处理方式 - if hasattr(self, 'original_mouse_press_event'): - self.original_mouse_press_event(event) - else: - event.ignore() - - def is_click_in_gui_area(self, click_x, click_y): - """检查鼠标点击是否在GUI区域(高精度版本)""" - try: - if not self.maintenance_gui: - return False - - # 获取窗口尺寸用于坐标转换 - if hasattr(self.world, 'qtWidget') and self.world.qtWidget: - widget_width = self.world.qtWidget.width() - widget_height = self.world.qtWidget.height() - else: - # 使用默认窗口尺寸 - widget_width = 1380 - widget_height = 750 - - # 使用与maintenance_gui完全相同的坐标转换算法 - aspect_x = (click_x / widget_width) * 2.67 - 1.33 - aspect_y = 1.0 - (click_y / widget_height) * 2.0 - - # 额外的坐标验证(与Panda3D标准坐标系对比) - # Panda3D aspect2d坐标系: x范围[-1.33, 1.33], y范围[-1.0, 1.0] - normalized_x = (click_x / widget_width) * 2.0 - 1.0 # 标准化到[-1, 1] - normalized_y = 1.0 - (click_y / widget_height) * 2.0 # 标准化到[-1, 1],Y轴翻转 - aspect_ratio = widget_width / widget_height - panda_x = normalized_x * aspect_ratio # 应用宽高比 - panda_y = normalized_y - - print(f"🔍 精确GUI区域检查: Qt({click_x}, {click_y}) -> aspect2d({aspect_x:.3f}, {aspect_y:.3f})") - print(f"🔍 验证坐标转换: 标准化({normalized_x:.3f}, {normalized_y:.3f}) -> Panda3D({panda_x:.3f}, {panda_y:.3f})") - print(f"📏 窗口信息: 尺寸({widget_width}x{widget_height}), 宽高比({aspect_ratio:.3f})") - - # 快速检查工具按钮区域 - if hasattr(self.maintenance_gui, 'available_tools') and self.maintenance_gui.available_tools: - # 工具按钮区域(底部条带) - tools_area_left = -1.1 - tools_area_right = 0.5 - tools_area_bottom = -1.0 - tools_area_top = -0.5 - - if (tools_area_left <= aspect_x <= tools_area_right and - tools_area_bottom <= aspect_y <= tools_area_top): - print(f"✅ 点击在工具按钮区域内") - return True - - # 检查其他GUI区域(步骤显示、当前工具、警告等) - other_gui_areas = [] - - # 步骤显示区域(顶部) - if hasattr(self.maintenance_gui, 'step_text') and self.maintenance_gui.step_text: - other_gui_areas.append({ - 'name': '步骤显示区域', - 'left': -1.8, 'right': 1.8, - 'bottom': 0.5, 'top': 1.1 - }) - - # 当前工具显示区域(右侧) - if hasattr(self.maintenance_gui, 'current_tool_text') and self.maintenance_gui.current_tool_text: - other_gui_areas.append({ - 'name': '当前工具显示区域', - 'left': 0.2, 'right': 1.8, - 'bottom': 0.6, 'top': 1.0 - }) - - # 警告显示区域(中央) - if hasattr(self.maintenance_gui, 'warning_text') and self.maintenance_gui.warning_text: - other_gui_areas.append({ - 'name': '警告显示区域', - 'left': -1.5, 'right': 1.5, - 'bottom': 0.0, 'top': 0.6 - }) - - # 检查其他GUI区域 - for area in other_gui_areas: - if (area['left'] <= aspect_x <= area['right'] and - area['bottom'] <= aspect_y <= area['top']): - print(f"✅ 点击在GUI区域内: {area['name']}") - return True - - print("❌ 点击不在任何GUI区域内") - return False - - except Exception as e: - print(f"❌ 检查GUI区域失败: {e}") - import traceback - traceback.print_exc() - return False - - def handle_gui_button_click(self, click_x, click_y): - """直接处理GUI按钮点击""" - try: - if not self.maintenance_gui: - return None - - # 获取窗口尺寸用于坐标转换 - if hasattr(self.world, 'qtWidget') and self.world.qtWidget: - widget_width = self.world.qtWidget.width() - widget_height = self.world.qtWidget.height() - else: - widget_width = 1380 - widget_height = 750 - - # 使用与maintenance_gui完全相同的坐标转换算法 - aspect_x = (click_x / widget_width) * 2.67 - 1.33 - aspect_y = 1.0 - (click_y / widget_height) * 2.0 - - print(f"🎯 直接处理按钮点击:Qt({click_x}, {click_y}) -> aspect2d({aspect_x:.3f}, {aspect_y:.3f})") - - # 检查工具按钮点击 - if hasattr(self.maintenance_gui, 'available_tools') and self.maintenance_gui.available_tools: - button_width = 0.4 - button_height = 0.25 - button_spacing = 0.45 - start_x = -0.8 - start_y = -0.75 - click_padding = 0.05 # 点击容差 - - for i, tool_data in enumerate(self.maintenance_gui.available_tools): - # 获取工具名称 - if isinstance(tool_data, dict): - tool_name = tool_data.get('name', str(tool_data)) - else: - tool_name = str(tool_data) - - # 计算按钮位置 - button_x = start_x + i * button_spacing - button_y = start_y - - # 计算按钮边界(包含容差) - left = button_x - (button_width/2 + click_padding) - right = button_x + (button_width/2 + click_padding) - bottom = button_y - (button_height/2 + click_padding) - top = button_y + (button_height/2 + click_padding) - - print(f"🔍 检查按钮{i} '{tool_name}': 范围x[{left:.3f}, {right:.3f}], y[{bottom:.3f}, {top:.3f}]") - - # 检查点击是否在按钮范围内 - if (left <= aspect_x <= right and bottom <= aspect_y <= top): - print(f"✅ 直接匹配按钮{i} '{tool_name}'!调用工具选择") - # 直接调用维修GUI的工具选择方法 - self.maintenance_gui.on_tool_selected(tool_name) - return tool_name - - print("❌ 点击不在任何按钮范围内") - return None - - except Exception as e: - print(f"❌ 直接处理按钮点击失败: {e}") - import traceback - traceback.print_exc() - return None - - def qt_mouse_release_event(self, event): - """Qt鼠标释放事件处理(维修系统优化版)""" - from PyQt5.QtCore import Qt - - # 只拦截左键释放,其他按键使用原有处理方式 - if event.button() == Qt.LeftButton: - print('🖱️ 左键释放被拦截处理') - - # 维修系统模式:先让原有处理器处理 - if self.maintenance_gui: - if hasattr(self, 'original_mouse_release_event'): - self.original_mouse_release_event(event) - - # 处理3D交互的鼠标释放 - self.on_mouse_up() - event.accept() - else: - # 其他按键释放使用原有处理方式 - if hasattr(self, 'original_mouse_release_event'): - self.original_mouse_release_event(event) - else: - event.ignore() - - def handle_qt_mouse_click(self, x, y): - """处理Qt鼠标点击(转换为3D世界坐标)""" - try: - if not self.is_active: - print("⚠️ 交互模式未激活") - return - - # 检查操作权限 - if not self.check_operation_permission(): - return - - # 获取当前步骤的目标模型 - if self.current_step >= self.total_steps: - print("⚠️ 所有步骤已完成") - return - - step_data = self.config_data['steps'][self.current_step] - target_model_name = step_data.get('target_model') - - if not target_model_name: - print("⚠️ 当前步骤没有指定目标模型") - return - - print(f'🎯 检测点击目标: {target_model_name}') - - # 获取窗口尺寸进行坐标转换 - if hasattr(self.world, 'qtWidget') and self.world.qtWidget: - widget = self.world.qtWidget - win_width = widget.width() - win_height = widget.height() - - # 转换为Panda3D的标准化坐标 - mx = 2.0 * x / float(win_width) - 1.0 - my = 1.0 - 2.0 * y / float(win_height) - - # 执行射线检测 - self.perform_ray_cast(mx, my, target_model_name) - else: - print("❌ 找不到Qt部件,无法进行坐标转换") - - except Exception as e: - print(f"❌ Qt鼠标点击处理失败: {e}") - import traceback - traceback.print_exc() - - def perform_ray_cast(self, mx, my, target_model_name): - """执行射线检测""" - try: - # 获取鼠标射线 - from panda3d.core import Point3, Point2, CollisionTraverser, CollisionHandlerQueue - from panda3d.core import CollisionNode, CollisionRay, BitMask32 - - near_point = Point3() - far_point = Point3() - self.world.cam.node().getLens().extrude(Point2(mx, my), near_point, far_point) - - # 转换到世界坐标系 - world_near = self.world.render.getRelativePoint(self.world.camera, near_point) - world_far = self.world.render.getRelativePoint(self.world.camera, far_point) - - # 进行真正的射线检测 - picker = CollisionTraverser() - queue = CollisionHandlerQueue() - - # 创建射线碰撞检测 - pickerNode = CollisionNode('mouseRay') - pickerNP = self.world.camera.attachNewNode(pickerNode) - - # 设置射线的碰撞掩码,匹配模型的碰撞掩码(第2位) - pickerNode.setFromCollideMask(BitMask32.bit(1)) - - # 使用相机坐标系的点创建射线 - direction = far_point - near_point - direction.normalize() - pickerNode.addSolid(CollisionRay(near_point, direction)) - - picker.addCollider(pickerNP, queue) - picker.traverse(self.world.render) - - # 检查是否点击到了目标模型 - hit_target_model = False - hit_point = None - - if queue.getNumEntries() > 0: - # 遍历所有碰撞结果 - for i in range(queue.getNumEntries()): - entry = queue.getEntry(i) - hit_node_path = entry.getIntoNodePath() - hit_pos = entry.getSurfacePoint(self.world.render) - - # 检查是否击中了目标模型或其相关的碰撞体 - if self.is_target_model_hit(hit_node_path, target_model_name): - print(f"✅ 击中目标模型: {hit_node_path.getName()}") - hit_target_model = True - hit_point = hit_pos - break - - # 清理碰撞检测节点 - pickerNP.removeNode() - - if hit_target_model: - # 查找目标模型节点 - target_node = self.find_model_node(target_model_name) - if target_node and not target_node.isEmpty(): - # 根据操作类型决定行为 - step_data = self.config_data['steps'][self.current_step] - operation_type = self.normalize_operation_type(step_data) - - if operation_type == 'drag': - print(f'✅ 开始拖拽目标模型: {target_node.getName()}') - self.start_dragging(target_node, hit_point) - elif operation_type == 'click': - print(f'✅ 点击触发位移模型: {target_node.getName()}') - self.trigger_click_movement(target_node, step_data) - else: - print(f"❌ 未知的操作类型: {operation_type}") - else: - print(f"❌ 找不到目标模型节点: {target_model_name}") - else: - print(f"❌ 没有点击到目标模型 '{target_model_name}',请点击正确的模型") - - except Exception as e: - print(f"❌ 射线检测失败: {e}") - import traceback - traceback.print_exc() - - def is_target_model_hit(self, hit_node_path, target_model_name): - """检查击中的节点是否是目标模型""" - hit_name = hit_node_path.getName() - - # 直接匹配模型名称 - if hit_name == target_model_name: - return True - - # 检查是否是模型的碰撞体(通常以modelCollision_开头) - if hit_name.startswith("modelCollision_"): - collision_model_name = hit_name.replace("modelCollision_", "", 1) - if collision_model_name == target_model_name: - return True - - # 检查父节点是否是目标模型 - parent = hit_node_path.getParent() - while parent and not parent.isEmpty() and parent != self.world.render: - parent_name = parent.getName() - if parent_name == target_model_name: - return True - parent = parent.getParent() - - # 检查子节点路径 - current = hit_node_path - while current and not current.isEmpty(): - if current.getName() == target_model_name: - return True - current = current.getParent() - - return False - - def on_mouse_down(self): - """鼠标按下事件(优化版 v2:处理兄弟节点结构的碰撞体)""" - if not self.is_active or not self.world.mouseWatcherNode.hasMouse(): - return - # 获取鼠标位置并发射射线 - mouse_pos = self.world.mouseWatcherNode.getMouse() - picker_ray = self.picker_ray_node.node().getSolid(0) - picker_ray.setFromLens(self.world.camNode, mouse_pos.getX(), mouse_pos.getY()) - - # 执行遍历 - self.picker_traverser.traverse(self.world.render) - - if self.collision_handler.getNumEntries() > 0: - # 获取最近的碰撞点 - self.collision_handler.sortEntries() - entry = self.collision_handler.getEntry(0) - - hit_node_path = entry.getIntoNodePath() - hit_point = entry.getSurfacePoint(self.world.render) - - # --- 这是新的逻辑 --- - # 打印被击中的碰撞体名字,用于调试 - print(f"射线击中了碰撞节点: {hit_node_path.getName()}") - - # 根据命名约定推导模型名称 - hit_name = hit_node_path.getName() - derived_model_name = "" - - # 假设碰撞节点的命名规则是 "前缀 + 模型名" - if hit_name.startswith("modelCollision_"): - derived_model_name = hit_name.replace("modelCollision_", "", 1) - else: - derived_model_name = hit_name - - print(f"根据命名约定,推导出的模型名称为: {derived_model_name}") - - # 获取当前步骤的目标模型名称 - step_data = self.config_data['steps'][self.current_step] - target_model_name = step_data.get('target_model') - - # 检查推导出的名称是否是当前步骤需要操作的目标 - if derived_model_name == target_model_name: - print(f"✅ 击中正确!目标是 '{target_model_name}'") - - # 根据推导出的正确名称,去场景中查找可以被拖动的那个节点 - model_to_drag = self.find_model_node(derived_model_name) - - if model_to_drag and not model_to_drag.isEmpty(): - self.start_dragging(model_to_drag, hit_point) - else: - print(f"❌ 错误:虽然名称匹配,但在场景中找不到名为 '{derived_model_name}' 的可拖动节点!") - else: - print(f"❌ 击中的不是当前目标模型 (需要 '{target_model_name}', 击中了 '{derived_model_name}')") - def start_dragging(self, model_node, hit_point): - """开始拖拽模型(优化版:计算偏移量)""" - # 再次检查操作权限(双重保险) - if not self.check_operation_permission(): - print("⚠️ 权限检查失败,无法开始拖拽") - return - - self.dragging_model = model_node - - # 1. 定义一个拖拽平面。这里我们使用一个平行于XZ的平面(法线为Y轴), - # 并且该平面穿过模型的初始中心点。 - # 这意味着模型将在其初始高度上水平移动。 - # 您可以根据需要更改法线,例如 Vec3(0, 0, 1) 表示在XY平面上拖拽。 - plane_normal = Vec3(0, 1, 0) - self.drag_plane = Plane(plane_normal, model_node.getPos()) - - # 2. 计算鼠标射线与拖拽平面的交点。 - # 这个交点是鼠标在“拖拽世界”中的3D位置。 - plane_intersection_point = Point3() - if self.drag_plane.intersectsLine(plane_intersection_point, - self.world.camera.getPos(self.world.render), - hit_point): - # 3. 计算模型中心点和这个交点之间的偏移量。 - # 在整个拖拽过程中,我们将维持这个偏移量,以避免模型跳跃。 - self.drag_offset = model_node.getPos() - plane_intersection_point - print(f"开始拖拽模型: {model_node.getName()}") - print(f"初始位置: {model_node.getPos()}") - - # 启动拖拽任务 - self.world.taskMgr.add(self.drag_task, "drag_model_task") - else: - print("警告:无法计算拖拽平面的交点,拖拽取消。") - self.dragging_model = None - - def drag_task(self, task): - """拖拽任务(优化版:应用偏移量)""" - if not self.dragging_model or not self.world.mouseWatcherNode.hasMouse(): - # 如果模型被置空或鼠标不在窗口内,则停止任务 - return task.done - - # 获取最新的鼠标位置 - mouse_pos = self.world.mouseWatcherNode.getMouse() - - # 再次从相机发射射线以获取鼠标在3D空间中的指向 - near_point = Point3() - far_point = Point3() - self.world.cam.node().getLens().extrude(mouse_pos, near_point, far_point) - - # 将近点和远点转换到世界坐标系 - origin_point = self.world.render.getRelativePoint(self.world.camera, near_point) - direction_point = self.world.render.getRelativePoint(self.world.camera, far_point) - - # 计算射线与我们之前定义的拖拽平面的交点 - intersection_point = Point3() - if self.drag_plane.intersectsLine(intersection_point, origin_point, direction_point): - # 新的位置 = 当前鼠标在拖拽平面上的3D位置 + 初始偏移量 - new_pos = intersection_point + self.drag_offset - self.dragging_model.setPos(new_pos) - - # 初始化距离打印时间(如果还没有) - if not hasattr(self, '_last_distance_print_time'): - self._last_distance_print_time = 0 - - # 在拖拽过程中,实时显示距离原始位置的距离 - if hasattr(self, 'config_data') and self.current_step < len(self.config_data.get('steps', [])): - step_data = self.config_data['steps'][self.current_step] - step_type = self.normalize_step_type(step_data) - target_model_name = step_data.get('target_model', '') - - if (target_model_name in self.model_original_positions and - hasattr(self, '_last_distance_print_time')): - - # 每0.5秒显示一次距离信息 - current_time = task.time - if current_time - getattr(self, '_last_distance_print_time', 0) > 0.5: - original_pos = self.model_original_positions[target_model_name] - distance = (new_pos - original_pos).length() - - if step_type == 'assemble': - snap_tolerance = step_data.get('snap_tolerance') or step_data.get('tolerance', 5.0) - if distance <= snap_tolerance * 1.2: # 只在接近时显示 - print(f"📏 安装拖拽中 - 距离原位: {distance:.2f} (需要 <= {snap_tolerance:.2f})") - else: # 拆卸模式 - disassemble_threshold = step_data.get('disassemble_threshold', 5.0) - if distance >= disassemble_threshold * 0.8: # 接近阈值时显示 - print(f"📏 拆卸拖拽中 - 距离原位: {distance:.2f} (需要 >= {disassemble_threshold:.2f})") - - self._last_distance_print_time = current_time - - return task.cont - - def stop_dragging(self): - """停止拖拽""" - if not self.dragging_model: - return - - print(f"停止拖拽模型: {self.dragging_model.getName()}") - print(f"最终位置: {self.dragging_model.getPos()}") - - # 停止拖拽任务 - self.world.taskMgr.remove("drag_model_task") - - # 检查是否完成了当前步骤 - self.check_step_completion() - - # 清理拖拽状态 - self.dragging_model = None - - def on_mouse_up(self): - """鼠标抬起事件""" - self.stop_dragging() - - def check_step_completion(self): - """检查步骤是否完成""" - if not self.dragging_model: - return - - step_data = self.config_data['steps'][self.current_step] - step_type = self.normalize_step_type(step_data) - print(f"检查步骤完成: {step_type} ----------------------------------------") - target_model_name = step_data.get('target_model', '') - current_pos = self.dragging_model.getPos() - - if step_type == 'assemble': - # 安装模式:检查是否接近原始位置 - if target_model_name in self.model_original_positions: - original_pos = self.model_original_positions[target_model_name] - distance_to_original = (original_pos - current_pos).length() - # 吸附容差:优先使用配置的snap_tolerance,否则使用拆卸时的tolerance,最后默认5.0 - snap_tolerance = step_data.get('snap_tolerance') or step_data.get('tolerance', 5.0) - - print(f"🔧 安装模式 - 距离原始位置: {distance_to_original:.2f}") - print(f"🎯 吸附容差: {snap_tolerance:.2f}") - print(f"🔍 吸附条件: {distance_to_original:.2f} <= {snap_tolerance:.2f} = {distance_to_original <= snap_tolerance}") - - if distance_to_original <= snap_tolerance: - # 自动吸附到原始位置 - print(f"✨ 自动吸附到原始位置!") - print(f"📍 精确位置: {original_pos}") - self.dragging_model.setPos(original_pos) - self.model_current_states[target_model_name] = 'installed' - print(f"步骤 {self.current_step + 1} 完成!模型已安装到原始位置") - self.complete_current_step() - return - else: - print(f"距离原始位置还有 {distance_to_original:.2f} 单位,需要更接近才能自动吸附") - else: - print("⚠️ 没有找到模型的原始位置记录") - - else: - # 拆卸模式:检查是否远离原始位置 - if target_model_name in self.model_original_positions: - original_pos = self.model_original_positions[target_model_name] - distance_from_original = (current_pos - original_pos).length() - disassemble_threshold = step_data.get('disassemble_threshold', 5.0) # 拆卸距离阈值 - - print(f"🔧 拆卸模式 - 距离原始位置: {distance_from_original:.2f}") - print(f"🎯 拆卸阈值: {disassemble_threshold:.2f}") - print(f"🔍 拆卸条件: {distance_from_original:.2f} >= {disassemble_threshold:.2f} = {distance_from_original >= disassemble_threshold}") - - if distance_from_original >= disassemble_threshold: - print(f"步骤 {self.current_step + 1} 完成!模型已拆卸,距离原位 {distance_from_original:.2f} >= {disassemble_threshold:.2f}") - if target_model_name: - self.model_current_states[target_model_name] = 'removed' - self.complete_current_step() - return - else: - print(f"需要将模型拖拽到距离原位至少 {disassemble_threshold:.2f} 单位(当前 {distance_from_original:.2f})") - else: - # 如果没有原始位置记录,回退到原来的目标位置模式 - print("⚠️ 没有找到模型的原始位置记录,使用目标位置模式") - target_pos_data = step_data.get('target_position') - - if target_pos_data is None: - # 如果没有定义目标位置,拆卸后即完成 - print("拆卸模式 - 没有定义目标位置,视为完成。") - if target_model_name: - self.model_current_states[target_model_name] = 'removed' - self.complete_current_step() - return - - target_pos = Vec3(*target_pos_data) - distance = (target_pos - current_pos).length() - tolerance = step_data.get('tolerance', 5) - - if distance <= tolerance: - print(f"步骤 {self.current_step + 1} 完成!拆卸到目标位置 距离 {distance:.2f} < 容差 {tolerance}") - if target_model_name: - self.model_current_states[target_model_name] = 'removed' - self.complete_current_step() - else: - print(f"距离目标位置还有 {distance:.2f} 单位,未达到容差 {tolerance}") - - def complete_current_step(self): - """完成当前步骤""" - # 记录操作完成(考核模式下) - if self.mode == "exam" and self.current_step in self.step_scores: - self.step_scores[self.current_step]['operation_attempts'] += 1 - print(f"📝 考核记录:步骤 {self.current_step + 1} 完成,操作次数: {self.step_scores[self.current_step]['operation_attempts']}") - - # 在进入下一步之前,取消当前模型的碰撞,避免干扰 - step_data = self.config_data['steps'][self.current_step] - target_model_name = step_data.get('target_model') - if target_model_name: - node = self.find_model_node(target_model_name) - if node: - node.setCollideMask(BitMask32.allOff()) - - # 显示当前所有模型的状态 - self.show_models_status() - - self.current_step += 1 - - if self.current_step >= self.total_steps: - self.finish_interaction() - else: - self.start_current_step() - - def finish_interaction(self): - """完成所有交互""" - print("\n🎉 所有拆装步骤已完成!") - self.is_active = False - self.ignoreAll() - - # 清理维修系统GUI - if self.maintenance_gui: - self.maintenance_gui.cleanup_gui() - print("✅ 维修系统GUI已清理") - - # 恢复原有的Qt鼠标事件处理 - print("🔄 恢复原有的Qt鼠标事件处理") - if hasattr(self, 'original_mouse_press_event') and hasattr(self.world, 'qtWidget') and self.world.qtWidget: - self.world.qtWidget.mousePressEvent = self.original_mouse_press_event - self.world.qtWidget.mouseReleaseEvent = self.original_mouse_release_event - print("✅ 原有Qt鼠标事件处理已恢复") - else: - print("⚠️ 没有找到备份的Qt事件处理方法") - - if self.step_dialog: - self.step_dialog.close() - self.step_dialog = None - - # 根据模式显示不同的完成结果 - if self.mode == "exam": - # 考核模式:显示详细的考核结果 - self.show_exam_results() - else: - # 训练模式:显示训练完成提示 - print("\n🎉 训练模式完成!") - completion_msg = "🎉 训练完成!\n\n所有维修步骤已完成!\n现在可以尝试考核模式检验学习成果。" - - if self.maintenance_gui: - self.maintenance_gui.update_step_info(completion_msg) - print("📋 训练完成信息已在GUI中显示") - else: - QMessageBox.information(None, "训练完成", completion_msg) - - def stop_interaction(self): - """用户主动停止交互""" - print("\n🛑 用户主动停止拆装交互") - - try: - # 设置为非活动状态 - self.is_active = False - self.ignoreAll() - - # 清理维修系统GUI - if self.maintenance_gui: - self.maintenance_gui.cleanup_gui() - print("✅ 维修系统GUI已清理") - - # 恢复原有的Qt鼠标事件处理 - print("🔄 恢复原有的Qt鼠标事件处理") - if hasattr(self, 'original_mouse_press_event') and hasattr(self.world, 'qtWidget') and self.world.qtWidget: - self.world.qtWidget.mousePressEvent = self.original_mouse_press_event - self.world.qtWidget.mouseReleaseEvent = self.original_mouse_release_event - print("✅ 原有Qt鼠标事件处理已恢复") - else: - print("⚠️ 没有找到备份的Qt事件处理方法") - - # 关闭步骤对话框 - if self.step_dialog: - self.step_dialog.close() - self.step_dialog = None - - # 显示停止确认信息 - if self.mode == "exam": - stop_msg = "🛑 考核已停止\n\n您的考核进度没有被保存。" - else: - stop_msg = "🛑 训练已停止\n\n您可以随时重新开始训练。" - - if self.maintenance_gui: - # 如果GUI还在,显示停止信息 - self.maintenance_gui.update_step_info(stop_msg) - else: - QMessageBox.information(None, "操作停止", stop_msg) - - print("✅ 拆装交互已停止") - - except Exception as e: - print(f"❌ 停止拆装交互失败: {e}") - import traceback - traceback.print_exc() - - def show_exam_results(self): - """显示考核结果""" - try: - # 计算最终得分 - final_score = 0 - total_operations = 0 - total_errors = 0 - - for step_record in self.step_scores.values(): - final_score += step_record['current_score'] - total_operations += step_record['operation_attempts'] - if step_record['tool_error']: - total_errors += 1 - - # 计算得分率和准确率 - score_rate = (final_score / self.exam_max_score * 100) if self.exam_max_score > 0 else 0 - accuracy_rate = ((self.total_steps - total_errors) / self.total_steps * 100) if self.total_steps > 0 else 100 - - # 生成考核报告 - print(f"\n{'='*50}") - print(f"🎓 维修技能考核结果报告") - print(f"{'='*50}") - print(f"📊 总体成绩:") - print(f" 总得分: {final_score:.0f} / {self.exam_max_score:.0f} 分") - print(f" 得分率: {score_rate:.1f}%") - print(f" 操作准确率: {accuracy_rate:.1f}%") - print(f" 总操作次数: {total_operations}") - print(f" 错误次数: {total_errors}") - - # 评级系统 - if score_rate >= 90: - grade = "优秀" - grade_icon = "🏆" - grade_desc = "表现卓越,技能熟练" - elif score_rate >= 80: - grade = "良好" - grade_icon = "🥈" - grade_desc = "表现良好,基本掌握" - elif score_rate >= 60: - grade = "及格" - grade_icon = "✅" - grade_desc = "达到基本要求" - else: - grade = "不及格" - grade_icon = "❌" - grade_desc = "需要加强练习" - - print(f"\n🏅 评级: {grade_icon} {grade}") - print(f" 评语: {grade_desc}") - - # 详细步骤分析 - print(f"\n📋 详细步骤分析:") - print(f"{'-'*40}") - - perfect_steps = 0 - for step_idx, step_record in self.step_scores.items(): - step_data = self.config_data['steps'][step_idx] - step_name = step_data.get('name', f'步骤{step_idx+1}') - max_score = step_record['max_score'] - current_score = step_record['current_score'] - tool_error = step_record['tool_error'] - attempts = step_record['operation_attempts'] - - # 判断步骤完成质量 - if current_score >= max_score: - step_status = "🟢 完美" - perfect_steps += 1 - elif current_score >= max_score * 0.8: - step_status = "🟡 良好" - elif current_score > 0: - step_status = "🟠 一般" - else: - step_status = "🔴 失败" - - print(f"{step_name}:") - print(f" 状态: {step_status}") - print(f" 得分: {current_score:.0f}/{max_score:.0f} 分") - print(f" 工具使用: {'❌ 错误' if tool_error else '✅ 正确'}") - print(f" 操作次数: {attempts}") - print() - - # 生成改进建议 - suggestions = [] - if total_errors > 0: - suggestions.append("• 注意选择正确的工具进行操作") - if total_operations > self.total_steps * 1.5: - suggestions.append("• 减少不必要的操作,提高操作效率") - if perfect_steps < self.total_steps * 0.5: - suggestions.append("• 多练习以提高操作的精确度") - if score_rate < 80: - suggestions.append("• 建议重新学习相关理论知识") - - if suggestions: - print("💡 改进建议:") - for suggestion in suggestions: - print(f" {suggestion}") - print() - - print(f"{'='*50}") - - # 为GUI显示准备简化版本的报告 - gui_report = f"🎓 考核完成!\n\n" - gui_report += f"📊 成绩总览:\n" - gui_report += f"总得分: {final_score:.0f}/{self.exam_max_score:.0f} 分 ({score_rate:.1f}%)\n" - gui_report += f"评级: {grade_icon} {grade}\n" - gui_report += f"操作准确率: {accuracy_rate:.1f}%\n\n" - gui_report += f"完美完成步骤: {perfect_steps}/{self.total_steps}\n" - gui_report += f"总操作次数: {total_operations}\n" - gui_report += f"错误次数: {total_errors}" - - # 在维修GUI中显示详细考核结果 - if self.maintenance_gui: - # 创建详细的考核结果数据 - exam_result_data = { - 'final_score': final_score, - 'max_score': self.exam_max_score, - 'score_rate': score_rate, - 'accuracy_rate': accuracy_rate, - 'grade': grade, - 'grade_icon': grade_icon, - 'grade_desc': grade_desc, - 'total_operations': total_operations, - 'total_errors': total_errors, - 'perfect_steps': perfect_steps, - 'total_steps': self.total_steps, - 'step_details': [], - 'suggestions': suggestions - } - - # 添加每步详情 - for step_idx, step_record in self.step_scores.items(): - step_data = self.config_data['steps'][step_idx] - step_name = step_data.get('name', f'步骤{step_idx+1}') - max_score = step_record['max_score'] - current_score = step_record['current_score'] - tool_error = step_record['tool_error'] - attempts = step_record['operation_attempts'] - - # 判断步骤完成质量 - if current_score >= max_score: - step_status = "🟢 完美" - elif current_score >= max_score * 0.8: - step_status = "🟡 良好" - elif current_score > 0: - step_status = "🟠 一般" - else: - step_status = "🔴 失败" - - exam_result_data['step_details'].append({ - 'name': step_name, - 'status': step_status, - 'current_score': current_score, - 'max_score': max_score, - 'tool_error': tool_error, - 'attempts': attempts - }) - - # 使用GUI显示考核结果 - self.maintenance_gui.show_exam_results(exam_result_data) - print("📋 考核结果已在GUI中显示") - else: - # 如果没有GUI,使用简单的控制台输出 - print("⚠️ 维修GUI不可用,考核结果仅在控制台显示") - - # 保存考核结果到文件(可选) - self.save_exam_results(final_score, score_rate, grade) - - except Exception as e: - print(f"❌ 显示考核结果失败: {e}") - import traceback - traceback.print_exc() - QMessageBox.critical(None, "错误", f"显示考核结果失败: {str(e)}") - - def save_exam_results(self, final_score, score_rate, grade): - """保存考核结果到文件""" - try: - import datetime - import json - - # 生成考核记录 - exam_record = { - "timestamp": datetime.datetime.now().isoformat(), - "mode": "exam", - "final_score": final_score, - "max_score": self.exam_max_score, - "score_rate": score_rate, - "grade": grade, - "total_steps": self.total_steps, - "step_details": {} - } - - # 添加每步详情 - for step_idx, step_record in self.step_scores.items(): - step_name = self.config_data['steps'][step_idx].get('name', f'步骤{step_idx+1}') - exam_record["step_details"][step_name] = { - "max_score": step_record['max_score'], - "current_score": step_record['current_score'], - "tool_error": step_record['tool_error'], - "operation_attempts": step_record['operation_attempts'] - } - - # 保存到文件 - filename = f"exam_result_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json" - filepath = os.path.join(".", filename) - - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(exam_record, f, ensure_ascii=False, indent=2) - - print(f"📄 考核结果已保存到: {filepath}") - - except Exception as e: - print(f"⚠️ 保存考核结果失败: {e}") - # 不影响主流程,只是记录警告 - - def stop_interaction_mode(self): - """停止交互模式""" - self.is_active = False - self.ignoreAll() - - # 恢复原有的Qt鼠标事件处理 - print("🔄 恢复原有的Qt鼠标事件处理") - if hasattr(self, 'original_mouse_press_event') and hasattr(self.world, 'qtWidget') and self.world.qtWidget: - self.world.qtWidget.mousePressEvent = self.original_mouse_press_event - self.world.qtWidget.mouseReleaseEvent = self.original_mouse_release_event - print("✅ 原有Qt鼠标事件处理已恢复") - else: - print("⚠️ 没有找到备份的Qt事件处理方法") - - if self.step_dialog: - self.step_dialog.close() - self.step_dialog = None - - self.world.taskMgr.remove("drag_model_task") - print("拆装交互模式已停止") - - def trigger_drag_by_keyboard(self): - """通过键盘触发拖拽(备用方案)""" - if not self.is_active or self.dragging_model: - return - - print("⌨️ 使用键盘备用方案触发拖拽") - step_data = self.config_data['steps'][self.current_step] - target_model_name = step_data.get('target_model') - - if not target_model_name: - print("当前步骤没有指定目标模型") - return - - target_node = self.find_model_node(target_model_name) - if not target_node: - print(f"找不到目标模型: {target_model_name}") - return - - # 对于键盘触发,我们无法获取精确的hit_point, - # 所以直接使用模型的中心点作为模拟的点击点。 - self.start_dragging(target_node, target_node.getPos()) - - def trigger_click_movement(self, model_node, step_data): - """触发点击位移动画""" - try: - # 检查操作权限(双重保险) - if not self.check_operation_permission(): - print("⚠️ 权限检查失败,无法进行点击触发位移") - return - - step_type = self.normalize_step_type(step_data) - target_model_name = step_data.get('target_model', '') - - print(f"🎬 开始点击触发位移: {model_node.getName()}") - print(f"📋 步骤类型: {step_type}") - - if step_type == 'disassemble': - # 拆卸模式:移动到目标位置然后隐藏 - self.perform_disassemble_click_movement(model_node, step_data) - elif step_type == 'assemble': - # 安装模式:显示模型然后移动到原始位置 - self.perform_assemble_click_movement(model_node, step_data) - else: - print(f"❌ 未知的步骤类型: {step_type}") - - except Exception as e: - print(f"❌ 点击触发位移失败: {e}") - import traceback - traceback.print_exc() - - def perform_disassemble_click_movement(self, model_node, step_data): - """执行拆卸点击位移""" - target_model_name = step_data.get('target_model', '') - - # 记录当前位置为原始位置(如果还没有记录) - if target_model_name not in self.model_original_positions: - current_pos = model_node.getPos() - self.model_original_positions[target_model_name] = current_pos - print(f"📍 记录模型 '{target_model_name}' 的原始位置: {current_pos}") - - # 获取目标位移位置 - target_pos_data = step_data.get('target_position') - if not target_pos_data: - print("❌ 拆卸步骤缺少target_position配置") - return - - target_pos = Vec3(*target_pos_data) - print(f"🎯 目标位置: {target_pos}") - - # 获取动画时长 - animation_duration = step_data.get('animation_duration', 2.0) - - # 创建位移动画序列 - from direct.interval.IntervalGlobal import LerpPosInterval, Sequence, Func - - move_interval = LerpPosInterval( - model_node, - animation_duration, - target_pos, - name=f"disassemble_{target_model_name}" - ) - - # 动画完成后的回调 - def on_disassemble_complete(): - print(f"✅ 拆卸动画完成,隐藏模型: {target_model_name}") - model_node.hide() # 隐藏模型 - self.model_current_states[target_model_name] = 'removed' - self.complete_current_step() # 完成当前步骤 - - # 创建动画序列 - disassemble_sequence = Sequence( - move_interval, - Func(on_disassemble_complete), - name=f"disassemble_sequence_{target_model_name}" - ) - - print(f"🎬 开始拆卸动画,持续时间: {animation_duration}秒") - disassemble_sequence.start() - - def perform_assemble_click_movement(self, model_node, step_data): - """执行安装点击位移""" - target_model_name = step_data.get('target_model', '') - - # 确保模型可见(通常在步骤开始时已经显示,这里是双重保险) - if model_node.isHidden(): - print(f"👁️ 点击触发时显示模型: {target_model_name}") - model_node.show() - else: - print(f"✅ 模型 '{target_model_name}' 已经可见,开始安装动画") - - # 获取原始位置 - if target_model_name not in self.model_original_positions: - print(f"❌ 找不到模型 '{target_model_name}' 的原始位置") - return - - original_pos = self.model_original_positions[target_model_name] - print(f"🏠 原始位置: {original_pos}") - - # 获取动画时长 - animation_duration = step_data.get('animation_duration', 2.0) - - # 创建位移动画序列 - from direct.interval.IntervalGlobal import LerpPosInterval, Sequence, Func - - move_interval = LerpPosInterval( - model_node, - animation_duration, - original_pos, - name=f"assemble_{target_model_name}" - ) - - # 动画完成后的回调 - def on_assemble_complete(): - print(f"✅ 安装动画完成,模型回到原位: {target_model_name}") - model_node.setPos(original_pos) # 确保精确位置 - self.model_current_states[target_model_name] = 'installed' - self.complete_current_step() # 完成当前步骤 - - # 创建动画序列 - assemble_sequence = Sequence( - move_interval, - Func(on_assemble_complete), - name=f"assemble_sequence_{target_model_name}" - ) - - print(f"🎬 开始安装动画,持续时间: {animation_duration}秒") - assemble_sequence.start() - - -class StepGuideDialog(QDialog): - """步骤指引对话框(UI代码保持不变)""" - - def __init__(self, interaction_manager, parent=None): - super().__init__(parent) - self.interaction_manager = interaction_manager - self.current_required_tool = "无" # 当前步骤要求的工具 - self.mode = interaction_manager.mode # 获取模式 - - self.chinese_font = None - if hasattr(interaction_manager.world,'getChineseFont'): - self.chinese_font = interaction_manager.world.getChineseFont() - - self.setupUI() - - def setupUI(self): - if self.mode == "exam": - self.setWindowTitle("拆装考核") - else: - self.setWindowTitle("拆装步骤指引") - - self.setFixedSize(450, 400) - self.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint) - layout = QVBoxLayout(self) - - # 步骤信息 - self.step_info_label = QLabel("准备开始...") - self.step_info_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #2E86C1;") - if self.chinese_font: - self.step_info_label.setFont(self.chinese_font) - layout.addWidget(self.step_info_label) - - # 步骤描述(考核模式下隐藏) - if self.mode != "exam": - self.step_desc_text = QTextEdit() - self.step_desc_text.setMaximumHeight(100) - self.step_desc_text.setReadOnly(True) - if self.chinese_font: - self.step_desc_text.setFont(self.chinese_font) - layout.addWidget(self.step_desc_text) - else: - self.step_desc_text = None - - # 工具选择组 - tool_group = QGroupBox("工具选择") - if self.chinese_font: - tool_group.setFont(self.chinese_font) - tool_layout = QVBoxLayout(tool_group) - - # 在训练模式下显示要求的工具,考核模式下不显示 - if self.mode != "exam": - self.required_tool_label = QLabel("当前步骤要求工具: 无") - self.required_tool_label.setStyleSheet("font-weight: bold; color: #E74C3C;") - if self.chinese_font: - self.required_tool_label.setFont(self.chinese_font) - tool_layout.addWidget(self.required_tool_label) - else: - self.required_tool_label = None - - current_tool_layout = QHBoxLayout() - current_tool_label = QLabel("当前选择工具:") - if self.chinese_font: - current_tool_label.setFont(self.chinese_font) - current_tool_layout.addWidget(current_tool_label) - self.current_tool_combo = QComboBox() - self.current_tool_combo.addItem("无") # 默认只有"无"选项 - self.current_tool_combo.currentTextChanged.connect(self.on_tool_changed) - if self.chinese_font: - self.current_tool_combo.setFont(self.chinese_font) - current_tool_layout.addWidget(self.current_tool_combo) - tool_layout.addLayout(current_tool_layout) - - # 在训练模式下显示工具状态,考核模式下不显示 - if self.mode != "exam": - self.tool_status_label = QLabel("✅ 工具匹配,可以进行操作") - self.tool_status_label.setStyleSheet("color: #27AE60; font-weight: bold;") - if self.chinese_font: - self.tool_status_label.setFont(self.chinese_font) - tool_layout.addWidget(self.tool_status_label) - else: - self.tool_status_label = None - - layout.addWidget(tool_group) - - # 操作提示(考核模式下隐藏) - if self.mode != "exam": - self.operation_label = QLabel("操作提示:") - self.operation_label.setStyleSheet("font-weight: bold;") - if self.chinese_font: - self.operation_label.setFont(self.chinese_font) - layout.addWidget(self.operation_label) - self.operation_text = QTextEdit() - self.operation_text.setMaximumHeight(80) - self.operation_text.setReadOnly(True) - if self.chinese_font: - self.operation_text.setFont(self.chinese_font) - layout.addWidget(self.operation_text) - else: - self.operation_label = None - self.operation_text = None - - # 按钮 - button_layout = QHBoxLayout() - self.skip_button = QPushButton("跳过当前步骤") - if self.chinese_font: - self.skip_button.setFont(self.chinese_font) - self.skip_button.clicked.connect(self.skip_current_step) - button_layout.addWidget(self.skip_button) - self.stop_button = QPushButton("停止交互") - if self.chinese_font: - self.stop_button.setFont(self.chinese_font) - self.stop_button.clicked.connect(self.stop_interaction) - button_layout.addWidget(self.stop_button) - layout.addLayout(button_layout) - - # 初始化工具状态 - self.sync_current_tool() - self.update_tool_status() - - def sync_current_tool(self): - """初始化拆装工具状态""" - # 从配置文件中加载工具列表 - self.load_tools_from_config() - # 默认选择"无" - self.current_tool_combo.setCurrentText("无") - print(f"🔧 初始化拆装工具: 无") - - def load_tools_from_config(self): - """从配置文件中加载工具列表""" - # 清空现有工具选项(保留"无") - self.current_tool_combo.clear() - self.current_tool_combo.addItem("无") - - # 从配置文件中加载工具 - if hasattr(self.interaction_manager, 'config_data') and self.interaction_manager.config_data: - tools = self.interaction_manager.config_data.get('tools', []) - for tool in tools: - tool_name = tool.get('name', '') - if tool_name and tool_name != "无": - self.current_tool_combo.addItem(tool_name) - print(f"🔧 加载工具: {tool_name}") - - if len(tools) > 0: - print(f"🔧 从配置文件加载了 {len(tools)} 个工具") - else: - print("🔧 配置文件中没有定义工具") - else: - print("⚠️ 无法访问配置数据") - - def on_tool_changed(self, tool_name): - """工具改变时的处理""" - print(f"🔧 用户选择拆装工具: {tool_name}") - - # 拆装工具不需要同步到编辑器的工具管理器 - # 这里是独立的拆装工具选择 - - # 更新工具状态显示 - self.update_tool_status() - - def update_tool_status(self): - """更新工具状态显示""" - current_tool = self.current_tool_combo.currentText() - required_tool = self.current_required_tool - - # 检查工具是否匹配 - tool_matches = self.check_tool_permission(current_tool, required_tool) - - if self.mode == "training": - # 训练模式:显示工具匹配状态 - if tool_matches: - self.tool_status_label.setText("✅ 工具匹配,可以进行操作") - self.tool_status_label.setStyleSheet("color: #27AE60; font-weight: bold;") - # 启用交互操作 - self.interaction_manager.set_operation_enabled(True) - else: - self.tool_status_label.setText(f"❌ 工具不匹配,需要选择 '{required_tool}' 工具") - self.tool_status_label.setStyleSheet("color: #E74C3C; font-weight: bold;") - # 禁用交互操作 - self.interaction_manager.set_operation_enabled(False) - else: - # 考核模式:不显示工具状态,但仍然需要正确的工具才能操作 - # 这里不再总是启用操作,而是让权限检查在实际操作时进行 - self.interaction_manager.set_operation_enabled(True) - - def check_tool_permission(self, current_tool, required_tool): - """检查工具权限""" - # 如果步骤不要求特定工具,任何工具都可以 - if required_tool == "无" or required_tool == "" or not required_tool: - return True - - # 精确匹配 - if current_tool == required_tool: - return True - - return False - - def update_step_info(self, step_data, current_step, total_steps): - step_name = step_data.get('name', f'步骤 {current_step}') - - if self.mode == "exam": - # 考核模式:显示考核信息 - step_score = step_data.get('score', 10) - self.step_info_label.setText(f"第 {current_step}/{total_steps} 步: {step_name} (分值: {step_score})") - else: - # 训练模式:正常显示 - self.step_info_label.setText(f"第 {current_step}/{total_steps} 步: {step_name}") - - # 为动态文本设置中文字体 - if self.chinese_font: - self.step_info_label.setFont(self.chinese_font) - - # 步骤描述(考核模式下不显示) - if self.step_desc_text is not None: - step_desc = step_data.get('description', '无描述') - self.step_desc_text.setPlainText(step_desc) - if self.chinese_font: - self.step_desc_text.setFont(self.chinese_font) - - # 更新工具要求 - self.current_required_tool = step_data.get('required_tool', '无') - if self.required_tool_label is not None: - self.required_tool_label.setText(f"当前步骤要求工具: {self.current_required_tool}") - if self.chinese_font: - self.required_tool_label.setFont(self.chinese_font) - - # 更新工具状态 - self.update_tool_status() - - operation_type = self.interaction_manager.normalize_operation_type(step_data) - step_type = self.interaction_manager.normalize_step_type(step_data) - target_model = step_data.get('target_model', '未指定') - - if operation_type == 'drag': - if step_type == 'assemble': - snap_tolerance = step_data.get('snap_tolerance') or step_data.get('tolerance', 5.0) - operation_hint = f"🔧 安装操作\n请用鼠标左键点击并拖拽模型 '{target_model}' 到原始位置附近。\n当距离原始位置 {snap_tolerance:.1f} 单位内时,模型会自动吸附到正确位置。" - else: - disassemble_threshold = step_data.get('disassemble_threshold', 5.0) - operation_hint = f"🔧 拆卸操作\n请用鼠标左键点击并拖拽模型 '{target_model}' 脱离原位。\n拖拽到距离原始位置 {disassemble_threshold:.1f} 单位外即可完成。" - elif operation_type == 'click': - if step_type == 'assemble': - animation_duration = step_data.get('animation_duration', 2.0) - operation_hint = f"🔧 安装操作(点击触发)\n请点击模型 '{target_model}' 触发自动安装。\n模型将自动移动到原始位置(动画时长 {animation_duration:.1f}秒)。" - else: - target_pos_data = step_data.get('target_position') - animation_duration = step_data.get('animation_duration', 2.0) - if target_pos_data: - from panda3d.core import Vec3 - target_pos = Vec3(*target_pos_data) - operation_hint = f"🔧 拆卸操作(点击触发)\n请点击模型 '{target_model}' 触发自动拆卸。\n模型将自动移动到位置 {target_pos}(动画时长 {animation_duration:.1f}秒),然后隐藏。" - else: - operation_hint = f"🔧 拆卸操作(点击触发)\n请点击模型 '{target_model}' 触发自动拆卸。\n⚠️ 警告:缺少target_position配置" - else: - # 未知操作类型的回退提示 - if step_type == 'assemble': - operation_hint = f"🔧 安装操作\n请操作模型 '{target_model}' 将其安装到原位。" - else: - operation_hint = f"🔧 拆卸操作\n请操作模型 '{target_model}' 将其拆卸。" - - # 操作提示(考核模式下不显示) - if self.operation_text is not None: - self.operation_text.setPlainText(operation_hint) - if self.chinese_font: - self.operation_text.setFont(self.chinese_font) - - def skip_current_step(self): - if self.interaction_manager.is_active: - # 停止可能正在进行的拖拽 - if self.interaction_manager.dragging_model: - self.interaction_manager.stop_dragging() - self.interaction_manager.complete_current_step() - - def stop_interaction(self): - self.interaction_manager.stop_interaction_mode() - self.close() - - def closeEvent(self, event): - reply = QMessageBox.question(self, "确认", "是否要停止拆装交互?", - QMessageBox.Yes | QMessageBox.No) - if reply == QMessageBox.Yes: - self.stop_interaction() - event.accept() - else: +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import json +import os +from direct.showbase.DirectObject import DirectObject +from direct.task import Task +from panda3d.core import CollisionTraverser, CollisionNode, CollisionHandlerQueue, CollisionRay +from panda3d.core import BitMask32, Vec3, Point3, Plane +from PyQt5.QtWidgets import QMessageBox, QFileDialog, QDialog, QVBoxLayout, QHBoxLayout +from PyQt5.QtWidgets import QLabel, QPushButton, QTextEdit, QComboBox, QGroupBox +from PyQt5.QtCore import Qt + +# 导入维修系统GUI +try: + from core.maintenance_gui import MaintenanceGUI +except ImportError as e: + print(f"⚠️ 导入维修GUI失败: {e}") + MaintenanceGUI = None + + +class AssemblyInteractionManager(DirectObject): + """拆装交互管理器(优化版)""" + + def __init__(self, world): + DirectObject.__init__(self) + self.world = world + self.config_data = None + self.current_step = 0 + self.total_steps = 0 + self.is_active = False + + # --- 模式控制 --- + self.mode = "training" # 默认训练模式,可选 "training" 或 "exam" + + # --- 拖拽相关(优化) --- + self.dragging_model = None + self.drag_offset = Vec3(0, 0, 0) + self.drag_plane = Plane(Vec3(0, 1, 0), Point3(0, 0, 0)) # 初始化一个平面 + + # --- 安装/拆卸状态管理 --- + self.model_original_positions = {} # 记录模型的原始位置 + self.model_current_states = {} # 记录模型当前状态 ('installed' 或 'removed') + + # --- UI组件 --- + self.step_dialog = None + + # --- 维修系统GUI --- + self.maintenance_gui = None + if MaintenanceGUI: + self.maintenance_gui = MaintenanceGUI(world) + + # --- 操作权限控制 --- + self.operation_enabled = True # 是否允许进行操作 + + # --- 考核相关 --- + self.exam_score = 0 # 考核总分 + self.exam_max_score = 0 # 考核满分 + self.step_scores = {} # 每步得分记录 + + # --- 碰撞检测(优化) --- + self.picker_traverser = CollisionTraverser('picker_traverser') + self.collision_handler = CollisionHandlerQueue() + self.picker_ray_node = None + + print("拆装交互管理器初始化完成") + + def init_exam_mode(self): + """初始化考核模式""" + print("📝 初始化考核模式...") + + # 重置考核数据 + self.exam_score = 0 + self.exam_max_score = 0 + self.step_scores = {} + + # 计算总分 + steps = self.config_data.get('steps', []) + for i, step_data in enumerate(steps): + step_score = step_data.get('score', 10) # 默认每步10分 + self.exam_max_score += step_score + self.step_scores[i] = { + 'max_score': step_score, + 'current_score': step_score, # 初始满分,操作错误时扣分 + 'tool_error': False, # 是否有工具错误 + 'operation_attempts': 0 # 操作尝试次数 + } + + print(f"📝 考核模式初始化完成,总分: {self.exam_max_score} 分") + + def play_step_audio(self, step_data): + """播放步骤音频""" + try: + # 考核模式下不播放音频 + if self.mode == "exam": + print("🔇 考核模式,音频播放已禁用") + return + + # 检查是否启用了自动播放音频 + settings = self.config_data.get('settings', {}) + auto_play_audio = settings.get('auto_play_audio', True) + + if not auto_play_audio: + print("🔇 音频播放已禁用") + return + + # 获取音频文件路径 + audio_file = step_data.get('audio_file', '') + + if not audio_file or audio_file.strip() == '': + print("🔇 当前步骤没有配置音频文件") + return + + print(f"🔊 准备播放步骤音频: {audio_file}") + + import os + # 检查音频文件格式 + file_ext = os.path.splitext(audio_file)[1].lower() + print(f"🔊 音频文件格式: {file_ext}") + + # 对于MP3文件,优先使用pygame播放 + if file_ext in ['.mp3', '.m4a', '.aac']: + print("🔊 检测到MP3/M4A/AAC格式,优先尝试pygame播放") + if self.play_audio_with_pygame(audio_file): + return + print("🔊 pygame不可用,尝试备用播放方法") + self.play_audio_fallback(audio_file) + return + + # 检查音频文件是否存在 + import os + if not os.path.isfile(audio_file): + # 如果不是绝对路径,尝试在项目目录下查找 + if not os.path.isabs(audio_file): + # 尝试在当前目录、Resources目录等位置查找 + possible_paths = [ + audio_file, + os.path.join("Resources", audio_file), + os.path.join("Resources", "audio", audio_file), + os.path.join("audio", audio_file), + os.path.join("sounds", audio_file) + ] + + audio_file_found = None + for path in possible_paths: + if os.path.isfile(path): + audio_file_found = path + break + + if audio_file_found: + audio_file = audio_file_found + print(f"🔊 找到音频文件: {audio_file}") + else: + print(f"❌ 找不到音频文件: {audio_file}") + print(f" 尝试的路径: {possible_paths}") + return + else: + print(f"❌ 音频文件不存在: {audio_file}") + return + + # 使用Panda3D的音频系统播放音频 + if hasattr(self.world, 'loader') and self.world.loader: + try: + # 检查音频系统是否启用 + from panda3d.core import AudioManager + print(f"🔊 音频管理器状态: {AudioManager.createAudioManager()}") + + # 加载音频文件 + print(f"🔊 尝试加载音频文件: {audio_file}") + audio_sound = self.world.loader.loadSfx(audio_file) + + if audio_sound: + print(f"✅ 音频文件加载成功: {os.path.basename(audio_file)}") + print(f"🔊 音频状态: {audio_sound.status()}") + audio_length = audio_sound.length() + print(f"🔊 音频长度: {audio_length:.2f}秒") + + # 检查音频文件是否有效 + if audio_length <= 0: + print("⚠️ 音频长度为0,可能是格式不支持或文件损坏") + print("🔊 尝试使用pygame播放") + if not self.play_audio_with_pygame(audio_file): + self.play_audio_fallback(audio_file) + return + + # 设置音量(在播放前设置) + audio_sound.setVolume(0.8) + print(f"🔊 设置音量: 80%") + + # 播放音频 + audio_sound.play() + print(f"✅ 开始播放音频: {os.path.basename(audio_file)}") + + # 等待一小段时间让播放状态更新 + import time + time.sleep(0.1) + + final_status = audio_sound.status() + print(f"🔊 播放后状态: {final_status}") + + # 检查是否真的在播放 (状态码2表示PLAYING) + if final_status == 2: # PLAYING + print("✅ 音频正在播放中") + elif final_status == 1: # READY + print("⚠️ 音频处于就绪状态但未播放,尝试pygame") + if not self.play_audio_with_pygame(audio_file): + self.play_audio_fallback(audio_file) + else: + print(f"⚠️ 音频状态异常: {final_status}") + if not self.play_audio_with_pygame(audio_file): + self.play_audio_fallback(audio_file) + else: + print(f"❌ 无法加载音频文件: {audio_file}") + print("🔊 Panda3D加载失败,尝试pygame") + if not self.play_audio_with_pygame(audio_file): + self.play_audio_fallback(audio_file) + + except Exception as e: + print(f"❌ Panda3D音频播放失败: {e}") + import traceback + traceback.print_exc() + # 尝试使用pygame播放(备用方案) + print("🔊 Panda3D播放失败,尝试pygame") + if not self.play_audio_with_pygame(audio_file): + self.play_audio_fallback(audio_file) + else: + print("⚠️ 找不到Panda3D音频加载器,尝试pygame") + if not self.play_audio_with_pygame(audio_file): + self.play_audio_fallback(audio_file) + + except Exception as e: + print(f"❌ 音频播放失败: {e}") + import traceback + traceback.print_exc() + + def play_audio_with_pygame(self, audio_file): + """使用pygame播放音频(优先方法)""" + try: + import pygame + print("🔊 使用pygame播放音频") + + # 初始化pygame mixer + if not pygame.mixer.get_init(): + pygame.mixer.pre_init(frequency=22050, size=-16, channels=2, buffer=1024) + pygame.mixer.init() + print("🔊 pygame mixer初始化完成") + + # 停止当前播放的音频(如果有) + if pygame.mixer.music.get_busy(): + pygame.mixer.music.stop() + print("🔊 停止之前的音频") + + # 加载并播放音频文件 + pygame.mixer.music.load(audio_file) + pygame.mixer.music.set_volume(0.8) + pygame.mixer.music.play() + + print(f"✅ pygame开始播放音频: {os.path.basename(audio_file)}") + + # 在后台线程中监控播放完成 + def wait_for_pygame_completion(): + import time + while pygame.mixer.music.get_busy(): + time.sleep(0.1) + print(f"🎵 pygame音频播放完成: {os.path.basename(audio_file)}") + + import threading + threading.Thread(target=wait_for_pygame_completion, daemon=True).start() + + print("🎵 音频将在后台完整播放") + return True + + except ImportError: + print("⚠️ pygame未安装") + return False + except Exception as e: + print(f"⚠️ pygame播放失败: {e}") + import traceback + traceback.print_exc() + return False + + def play_audio_fallback(self, audio_file): + """备用音频播放方法""" + import os + print(f"🔊 尝试备用音频播放方法...") + + # 方法1: 使用系统默认播放器(最简单) + try: + print("🔊 尝试使用系统默认播放器") + if os.name == 'nt': # Windows + os.startfile(audio_file) + print(f"✅ 使用系统默认播放器播放: {os.path.basename(audio_file)}") + return + elif os.name == 'posix': # Linux/Mac + os.system(f'xdg-open "{audio_file}"') + print(f"✅ 使用系统默认播放器播放: {os.path.basename(audio_file)}") + return + except Exception as e: + print(f"⚠️ 系统默认播放器失败: {e}") + + # 方法2: 尝试使用Windows Media Player + try: + if os.name == 'nt': # Windows + print("🔊 尝试使用Windows Media Player") + cmd = f'start wmplayer "{audio_file}"' + os.system(cmd) + print(f"✅ 使用Windows Media Player播放音频: {os.path.basename(audio_file)}") + return + except Exception as e: + print(f"⚠️ Windows Media Player播放失败: {e}") + + # 方法3: 使用PowerShell播放WAV文件 + try: + if os.name == 'nt': + file_ext = os.path.splitext(audio_file)[1].lower() + if file_ext in ['.wav']: + print("🔊 尝试使用PowerShell播放WAV文件") + import subprocess + cmd = f'powershell -c "(New-Object Media.SoundPlayer \\"{audio_file}\\").PlaySync()"' + subprocess.run(cmd, shell=True) + print(f"✅ 使用PowerShell播放音频: {os.path.basename(audio_file)}") + return + except Exception as e: + print(f"⚠️ PowerShell播放失败: {e}") + + # 方法4: 简单的系统调用 + try: + print("🔊 尝试使用系统默认播放器") + if os.name == 'nt': # Windows + os.startfile(audio_file) + elif os.name == 'posix': # Linux/Mac + os.system(f'xdg-open "{audio_file}"') + print(f"✅ 使用系统默认播放器播放音频: {os.path.basename(audio_file)}") + except Exception as e: + print(f"❌ 所有备用音频播放方法都失败了: {e}") + print("💡 建议检查:") + print(" 1. 音频文件是否存在且格式正确") + print(" 2. 系统音频驱动是否正常") + print(" 3. 音频文件是否损坏") + + def set_operation_enabled(self, enabled): + """设置操作是否启用""" + self.operation_enabled = enabled + print(f"🔧 操作权限: {'启用' if enabled else '禁用'}") + + def check_operation_permission(self): + """检查操作权限""" + if not self.is_active: + print("⚠️ 交互模式未激活") + return False + + # 获取当前步骤数据 + if self.current_step >= self.total_steps: + print("⚠️ 所有步骤已完成") + return False + + step_data = self.config_data['steps'][self.current_step] + required_tool = step_data.get('required_tool', '无') + + # 获取当前选择的工具(优先从维修GUI获取) + current_tool = "无" + if self.maintenance_gui: + current_tool = self.maintenance_gui.get_current_tool() + elif hasattr(self, 'step_dialog') and self.step_dialog and hasattr(self.step_dialog, 'tool_combo'): + current_tool = self.step_dialog.tool_combo.currentText() + + # 使用 getChinese 处理工具名称 + if hasattr(self.world, 'getChinese'): + required_tool = self.world.getChinese(required_tool) + current_tool = self.world.getChinese(current_tool) + + print(f"🔧 工具检查: 需要'{required_tool}',当前'{current_tool}'") + + # 检查工具是否匹配 + tool_matches = self.check_tool_match(current_tool, required_tool) + + if self.mode == "training": + # 训练模式:显示GUI警告并阻止错误操作 + if not tool_matches: + print("⚠️ 训练模式 - 工具不匹配,无法进行操作") + + # 在维修GUI中显示警告 + if self.maintenance_gui: + warning_msg = f"请先选择 '{required_tool}' 工具!" + self.maintenance_gui.show_warning(warning_msg, 2.0) + # 也在Qt对话框中显示警告(仅在非维修系统模式下) + elif hasattr(self, 'step_dialog') and self.step_dialog: + QMessageBox.warning(self.step_dialog, "工具不匹配", + f"当前步骤需要使用 '{required_tool}' 工具,请先选择正确的工具!") + return False + return True + else: + # 考核模式:不显示提示,但记录工具错误并阻止操作 + if not tool_matches: + print(f"❌ 考核模式 - 工具错误:需要'{required_tool}',实际选择'{current_tool}',操作被阻止") + + # 记录工具错误(扣分) + self.record_tool_error() + return False + return True + + def check_tool_match(self, current_tool, required_tool): + """检查工具是否匹配""" + # 如果步骤不要求特定工具,任何工具都可以 + if required_tool == "无" or required_tool == "" or not required_tool: + return True + + # 精确匹配 + if current_tool == required_tool: + return True + + return False + + def record_tool_error(self): + """记录工具错误(考核模式下扣分)""" + if self.mode != "exam": + return + + # 获取当前步骤的考核记录 + if self.current_step in self.step_scores: + step_record = self.step_scores[self.current_step] + + # 如果是第一次工具错误,扣除一定分数 + if not step_record['tool_error']: + step_record['tool_error'] = True + # 扣除该步骤50%的分数作为工具错误惩罚 + # penalty = step_record['max_score'] * 0.5 + penalty = step_record['max_score'] + step_record['current_score'] = max(0, step_record['current_score'] - penalty) + + print(f"📝 考核记录:工具错误,扣除{penalty:.0f}分,当前步骤剩余{step_record['current_score']:.0f}分") + + # 增加操作尝试次数 + step_record['operation_attempts'] += 1 + + def start_interaction_mode(self, mode="training"): + """启动交互模式""" + try: + # 设置模式 + self.mode = mode + print(f"🎯 启动拆装交互模式: {self.mode}") + + # 如果没有配置数据,尝试加载 + if not self.config_data: + if not self.load_configuration(): + return False + + if not self.config_data or not self.config_data.get('steps'): + QMessageBox.warning(None, "警告", "没有找到有效的配置步骤!\n请先在'拆装配置'中设置步骤。") + return False + + self.total_steps = len(self.config_data['steps']) + self.current_step = 0 + self.is_active = True + + # 设置维修系统GUI + if self.maintenance_gui: + tools_list = self.config_data.get('tools', ['手']) + self.maintenance_gui.setup_gui(tools_list, mode) + self.maintenance_gui.show_gui() + print(f"✅ 维修系统GUI已启动,模式: {mode}") + + # 如果是考核模式,初始化考核相关数据 + + if self.mode == "exam": + self.init_exam_mode() + # 显示考核开始提示 + if self.maintenance_gui: + exam_start_msg = f"📝 考核模式已启动\n\n总分: {self.exam_max_score} 分\n步骤数: {self.total_steps}\n\n✅ 可以选择和切换工具\n❌ 不会显示步骤描述和语音提示\n❌ 使用错误工具会扣分" + self.maintenance_gui.update_step_info(exam_start_msg) + print("📝 考核开始提示已在GUI中显示") + + # 设置碰撞检测 + self.setup_picking() + + # 拦截Qt层面的鼠标事件处理 + print("🔄 拦截Qt层面的鼠标事件处理") + print(f"🔍 检查world对象: {type(self.world).__name__}") + print(f"🔍 是否有qtWidget属性: {hasattr(self.world, 'qtWidget')}") + if hasattr(self.world, 'qtWidget'): + print(f"🔍 qtWidget值: {self.world.qtWidget}") + print(f"🔍 qtWidget类型: {type(self.world.qtWidget)}") + + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + # 备份原有的鼠标事件处理方法 + self.original_mouse_press_event = self.world.qtWidget.mousePressEvent + self.original_mouse_release_event = self.world.qtWidget.mouseReleaseEvent + + print(f"🔍 原有鼠标事件方法: {self.original_mouse_press_event}") + + # 替换为我们的处理方法 + self.world.qtWidget.mousePressEvent = self.qt_mouse_press_event + self.world.qtWidget.mouseReleaseEvent = self.qt_mouse_release_event + print("✅ Qt鼠标事件已被拦截") + else: + print("⚠️ 找不到Qt部件,使用Panda3D事件系统") + # 作为备用方案,仍然绑定Panda3D事件 + self.accept("mouse1", self.on_mouse_down) + self.accept("mouse1-up", self.on_mouse_up) + + # 绑定键盘备用触发 + self.accept("space", self.trigger_drag_by_keyboard) + self.accept("s", self.show_models_status) # S键显示模型状态 + self.accept("r", self.record_current_positions) # R键记录当前位置为原始位置 + + print("✅ 拆装交互事件绑定完成") + print("💡 提示: 按S键可查看所有模型状态,按R键重新记录原始位置") + + print("拆装交互模式已启动,核心事件已绑定。") + print("请点击目标模型进行操作。") + + # 调试:列出场景中的所有模型 + self.list_scene_models() + + # 显示步骤指引界面(维修系统使用GUI界面,跳过Qt对话框) + if not self.maintenance_gui: + self.show_step_dialog() + + # 开始第一步 + self.start_current_step() + + print(f"共 {self.total_steps} 步") + return True + + except Exception as e: + QMessageBox.critical(None, "错误", f"启动交互模式失败: {str(e)}") + import traceback + traceback.print_exc() + return False + + def load_configuration(self): + """加载拆装配置""" + try: + file_path, _ = QFileDialog.getOpenFileName( + None, "选择拆装配置文件", "", + "JSON文件 (*.json);;所有文件 (*)" + ) + if not file_path: + return False + + with open(file_path, 'r', encoding='utf-8') as f: + self.config_data = json.load(f) + + self.total_steps = len(self.config_data.get('steps', [])) + self.current_step = 0 + + # 从配置文件中恢复原始位置信息 + self.load_original_positions_from_config() + + print(f"成功加载配置: {file_path}") + return True + + except Exception as e: + QMessageBox.critical(None, "错误", f"加载配置失败: {str(e)}") + return False + + def load_original_positions_from_config(self): + """从配置文件中加载模型的原始位置信息""" + print("📍 从配置文件恢复模型原始位置...") + + # 清空现有的位置记录 + self.model_original_positions.clear() + self.model_current_states.clear() + + # 从配置文件的models部分读取原始位置(models是数组格式) + models_config = self.config_data.get('models', []) + + for model_data in models_config: + model_name = model_data.get('name') + if not model_name: + continue + + # 尝试从配置文件中获取原始位置 + original_pos_data = model_data.get('original_pos') + + if original_pos_data and len(original_pos_data) >= 3: + # 如果配置文件中有原始位置,使用配置文件中的位置 + original_pos = Vec3(original_pos_data[0], original_pos_data[1], original_pos_data[2]) + self.model_original_positions[model_name] = original_pos + self.model_current_states[model_name] = 'installed' + print(f" 📍 {model_name}: 从配置文件恢复原始位置 {original_pos}") + else: + # 如果配置文件中没有原始位置,使用当前场景中的位置作为原始位置 + target_node = self.find_model_node(model_name) + if target_node and not target_node.isEmpty(): + current_pos = target_node.getPos() + self.model_original_positions[model_name] = current_pos + self.model_current_states[model_name] = 'installed' + print(f" 📍 {model_name}: 使用当前场景位置作为原始位置 {current_pos}") + else: + print(f" ❌ {model_name}: 在场景中找不到对应模型") + + # 如果配置文件中没有models部分,从步骤中提取模型信息 + if not models_config: + print(" 配置文件中没有models部分,从步骤中提取模型信息...") + for step_data in self.config_data.get('steps', []): + target_model_name = step_data.get('target_model') + if target_model_name and target_model_name not in self.model_original_positions: + target_node = self.find_model_node(target_model_name) + if target_node and not target_node.isEmpty(): + current_pos = target_node.getPos() + self.model_original_positions[target_model_name] = current_pos + self.model_current_states[target_model_name] = 'installed' + print(f" 📍 {target_model_name}: 从场景记录原始位置 {current_pos}") + + print(f"📍 原始位置恢复完成,共记录 {len(self.model_original_positions)} 个模型") + + # 检查配置文件的步骤完整性 + self.check_step_completeness() + + def check_step_completeness(self): + """检查配置文件的步骤完整性""" + print("\n🔍 检查步骤配置完整性...") + + steps = self.config_data.get('steps', []) + if not steps: + print("⚠️ 配置文件中没有步骤") + return + + # 统计每个模型的拆卸和安装步骤 + model_operations = {} + + for i, step in enumerate(steps): + target_model = step.get('target_model') + step_type = self.normalize_step_type(step) + + if target_model: + if target_model not in model_operations: + model_operations[target_model] = {'disassemble': [], 'assemble': []} + + model_operations[target_model][step_type].append(i + 1) + + # 检查每个模型是否有配对的拆卸和安装步骤 + incomplete_models = [] + for model_name, operations in model_operations.items(): + disassemble_count = len(operations['disassemble']) + assemble_count = len(operations['assemble']) + + print(f" 📋 {model_name}: 拆卸步骤 {disassemble_count} 个, 安装步骤 {assemble_count} 个") + + if disassemble_count > 0 and assemble_count == 0: + incomplete_models.append(model_name) + print(f" ⚠️ 只有拆卸步骤,缺少安装步骤") + elif disassemble_count != assemble_count: + print(f" ⚠️ 拆卸和安装步骤数量不匹配") + + if incomplete_models: + print(f"\n⚠️ 发现 {len(incomplete_models)} 个模型缺少安装步骤:") + for model in incomplete_models: + print(f" - {model}") + print("💡 建议为每个拆卸的模型添加对应的安装步骤") + else: + print("\n✅ 所有模型都有完整的拆卸和安装步骤配置") + + def setup_picking(self): + """设置用于鼠标拾取的碰撞检测系统""" + try: + # 创建一个碰撞射线 + picker_ray = CollisionRay() + + # 创建一个用于放置射线的碰撞节点 + picker_ray_node = CollisionNode('mouse_ray') + picker_ray_node.addSolid(picker_ray) + + # 设置碰撞掩码,确保它只与可拾取的物体碰撞 + # 这里我们假设可拾取的物体有第1位的碰撞比特 + picker_ray_node.setFromCollideMask(BitMask32.bit(1)) + picker_ray_node.setIntoCollideMask(BitMask32.allOff()) + + # 将碰撞节点附加到相机上,这样射线就会跟随相机移动 + self.picker_ray_node = self.world.camera.attachNewNode(picker_ray_node) + + # 将碰撞节点添加到遍历器中,并指定一个处理器来存储碰撞结果 + self.picker_traverser.addCollider(self.picker_ray_node, self.collision_handler) + + print("射线拾取系统设置完成") + + except Exception as e: + print(f"设置碰撞检测失败: {e}") + + def show_step_dialog(self): + """显示步骤指引对话框""" + if self.step_dialog: + self.step_dialog.close() + + # 获取主窗口作为父窗口 + parent_window = None + if hasattr(self.world, 'main_window') and self.world.main_window: + parent_window = self.world.main_window + + self.step_dialog = StepGuideDialog(self, parent_window) + self.step_dialog.show() + + def start_current_step(self): + """开始当前步骤""" + if self.current_step >= self.total_steps: + self.finish_interaction() + return + + step_data = self.config_data['steps'][self.current_step] + + # 兼容中文和英文配置格式 + operation_type = self.normalize_operation_type(step_data) + step_type = self.normalize_step_type(step_data) + target_model_name = step_data.get('target_model', '未设置') + + # 调试信息:显示字段映射结果 + print(f"🔄 字段映射: '{step_data.get('type', 'N/A')}' -> '{step_type}'") + print(f"🔄 操作映射: '{step_data.get('interaction_type', 'N/A')}' -> '{operation_type}'") + + # 使用 getChinese 处理步骤名称 + step_name = step_data.get('name', '未命名') + if hasattr(self.world, 'getChinese'): + step_name = self.world.getChinese(step_name) + target_model_name = self.world.getChinese(target_model_name) + + print(f"\n=== 开始第 {self.current_step + 1} 步: {step_name} ===") + print(f"目标模型: {target_model_name}") + print(f"操作类型: {operation_type}") + print(f"步骤类型: {step_type}") + + # 显示容差信息 + if step_type == 'assemble': + snap_tolerance = step_data.get('snap_tolerance') or step_data.get('tolerance', 5.0) + print(f"吸附容差: {snap_tolerance:.1f} 单位") + else: + disassemble_threshold = step_data.get('disassemble_threshold', 5.0) + print(f"拆卸阈值: {disassemble_threshold:.1f} 单位(距离原位)") + + # 检查模型的原始位置记录 + if target_model_name: + if target_model_name in self.model_original_positions: + original_pos = self.model_original_positions[target_model_name] + print(f"📍 模型 '{target_model_name}' 的原始位置: {original_pos}") + else: + # 如果没有记录,使用当前位置作为原始位置(但会给出警告) + target_node = self.find_model_node(target_model_name) + if target_node and not target_node.isEmpty(): + current_pos = target_node.getPos() + self.model_original_positions[target_model_name] = current_pos + self.model_current_states[target_model_name] = 'installed' + print(f"⚠️ 模型 '{target_model_name}' 没有预设原始位置,使用当前位置: {current_pos}") + print(" 建议在配置文件中预设模型的original_position以确保一致性") + + # 如果是安装步骤,确保模型显示 + if step_type == 'assemble' and target_model_name: + target_node = self.find_model_node(target_model_name) + if target_node and not target_node.isEmpty(): + if target_node.isHidden(): + print(f"👁️ 安装步骤开始 - 显示隐藏的模型: {target_model_name}") + target_node.show() + else: + print(f"✅ 模型 '{target_model_name}' 已经可见") + + # 播放步骤音频(如果有配置) + self.play_step_audio(step_data) + + # 更新维修系统GUI的步骤信息 + if self.maintenance_gui: + step_name = step_data.get('name', f'步骤 {self.current_step + 1}') + step_description = step_data.get('description', '') + + # 使用 getChinese 处理文本 + if hasattr(self.world, 'getChineseFont'): + step_name = self.world.getChineseFont() + step_description = self.world.getChineseFont() + + if self.mode == "exam": + # 考核模式:只显示步骤编号和名称,不显示描述 + step_info = f"Step {self.current_step + 1}/{self.total_steps}: {step_name}" + print(f"📝 考核模式步骤信息: {step_info}") + else: + # 训练模式:显示完整信息 + step_info = f"Step: {self.current_step + 1}/{self.total_steps}: {step_name}" + if step_description: + step_info += f"\n{step_description}" + print(f"📚 训练模式步骤信息: {step_info}") + + self.maintenance_gui.update_step_info(step_info) + + # 更新Qt步骤对话框(仅在非维修系统模式下) + if self.step_dialog and not self.maintenance_gui: + self.step_dialog.update_step_info(step_data, self.current_step + 1, self.total_steps) + + # 准备交互(主要是确保目标模型可以被拾取) + self.prepare_interaction(step_data) + + def prepare_interaction(self, step_data): + """准备交互,主要是设置目标模型的碰撞掩码""" + target_model_name = step_data.get('target_model') + if not target_model_name: + print("警告: 当前步骤没有指定目标模型") + return + + target_node = self.find_model_node(target_model_name) + if not target_node: + print(f"警告: 找不到目标模型 '{target_model_name}'") + return + + # 给目标模型设置碰撞掩码,使其可以被射线拾取 + # BitMask32.bit(1) 对应 setup_picking 中设置的 from_collide_mask + target_node.setCollideMask(BitMask32.bit(1)) + print(f"已为模型 '{target_model_name}' 设置碰撞掩码,等待用户点击。") + + def find_model_node(self, model_name): + """在场景中查找指定名称的模型节点""" + # find() 是Panda3D提供的更高效的节点查找方法 + return self.world.render.find(f"**/{model_name}") + + def list_scene_models(self): + """列出场景中的所有模型(调试用)""" + print("\n📋 场景中的模型列表:") + all_nodes = self.world.render.findAllMatches("**") + model_count = 0 + for node_path in all_nodes: + node_name = node_path.getName() + # 过滤掉系统节点和碰撞节点 + if (node_name and + node_name not in ['render', 'camera', 'aspect2d', 'pixel2d', 'cam'] and + not node_name.startswith("modelCollision_") and + not node_name.startswith("collision") and + not node_name.startswith("picker")): + print(f" - {node_name}") + model_count += 1 + print(f"总共找到 {model_count} 个模型节点\n") + + def show_models_status(self): + """显示所有模型的当前状态""" + if not self.model_original_positions: + print("📊 没有记录任何模型状态") + return + + print("\n📊 模型状态信息:") + for model_name, original_pos in self.model_original_positions.items(): + current_state = self.model_current_states.get(model_name, 'unknown') + model_node = self.find_model_node(model_name) + + if model_node and not model_node.isEmpty(): + current_pos = model_node.getPos() + distance_from_original = (current_pos - original_pos).length() + + status_icon = "🔧" if current_state == 'installed' else "📦" if current_state == 'removed' else "❓" + print(f" {status_icon} {model_name}:") + print(f" 状态: {current_state}") + print(f" 原始位置: {original_pos}") + print(f" 当前位置: {current_pos}") + print(f" 距离原位: {distance_from_original:.2f}") + else: + print(f" ❌ {model_name}: 找不到模型节点") + print() + + def record_current_positions(self): + """将当前所有模型的位置记录为原始位置""" + print("\n📍 重新记录所有模型的原始位置...") + + # 从步骤中提取所有模型 + all_models = set() + for step_data in self.config_data.get('steps', []): + target_model = step_data.get('target_model') + if target_model: + all_models.add(target_model) + + # 记录每个模型的当前位置为原始位置 + for model_name in all_models: + target_node = self.find_model_node(model_name) + if target_node and not target_node.isEmpty(): + current_pos = target_node.getPos() + self.model_original_positions[model_name] = current_pos + self.model_current_states[model_name] = 'installed' + print(f" 📍 {model_name}: 记录新的原始位置 {current_pos}") + else: + print(f" ❌ {model_name}: 在场景中找不到对应模型") + + print(f"📍 重新记录完成,共记录 {len(self.model_original_positions)} 个模型") + print("💡 提示: 如需永久保存这些位置,请在配置界面中重新保存配置文件") + + def normalize_step_type(self, step_data): + """标准化步骤类型,兼容中文和英文配置""" + # 优先使用英文字段 + step_type = step_data.get('step_type') + if step_type: + return step_type + + # 兼容中文字段 + type_field = step_data.get('type', '') + if type_field == '拆卸': + return 'disassemble' + elif type_field == '安装': + return 'assemble' + else: + return 'disassemble' # 默认值 + + def normalize_operation_type(self, step_data): + """标准化操作类型,兼容中文和英文配置""" + # 优先使用英文字段 + operation_type = step_data.get('operation_type') + if operation_type: + return operation_type + + # 兼容中文字段 + interaction_type = step_data.get('interaction_type', '') + if interaction_type == '鼠标拖拽': + return 'drag' + elif interaction_type == '点击触发': + return 'click' + else: + return 'drag' # 默认值 + + def analyze_final_state(self): + """分析最终状态,检查哪些模型没有正确回到原位""" + print("\n📊 最终状态分析:") + + if not self.model_original_positions: + print("没有模型状态记录") + return + + incorrect_models = [] + for model_name, original_pos in self.model_original_positions.items(): + model_node = self.find_model_node(model_name) + if model_node and not model_node.isEmpty(): + current_pos = model_node.getPos() + distance = (current_pos - original_pos).length() + current_state = self.model_current_states.get(model_name, 'unknown') + + # 检查是否应该在原位但实际不在原位的模型 + if current_state == 'installed' and distance > 1.0: # 容差1.0单位 + incorrect_models.append({ + 'name': model_name, + 'expected_pos': original_pos, + 'actual_pos': current_pos, + 'distance': distance + }) + print(f"❌ {model_name}: 应该在原位但偏离了 {distance:.2f} 单位") + print(f" 期望位置: {original_pos}") + print(f" 实际位置: {current_pos}") + elif current_state == 'installed' and distance <= 1.0: + print(f"✅ {model_name}: 正确安装在原位 (偏差 {distance:.2f})") + elif current_state == 'removed': + print(f"📦 {model_name}: 已拆卸 (距离原位 {distance:.2f})") + + if incorrect_models: + print(f"\n⚠️ 发现 {len(incorrect_models)} 个模型没有正确回到原位") + print("💡 建议检查安装步骤的配置和吸附容差设置") + else: + print("\n✅ 所有模型都处于正确状态") + + def qt_mouse_press_event(self, event): + """Qt鼠标按下事件处理(选择性拦截版)""" + from PyQt5.QtCore import Qt + + # 只拦截左键,其他按键使用原有处理方式 + if event.button() == Qt.LeftButton: + print('🖱️ 左键被拦截处理') + print(f'🖱️ 左键点击位置: ({event.x()}, {event.y()})') + + # 检查是否点击在GUI区域 + if self.maintenance_gui and self.is_click_in_gui_area(event.x(), event.y()): + print('🎯 点击在GUI区域,直接处理工具按钮点击') + # 直接处理工具按钮点击,不依赖DirectGUI事件 + button_clicked = self.handle_gui_button_click(event.x(), event.y()) + if button_clicked: + print(f'🎯 工具按钮点击已处理: {button_clicked}') + event.accept() + return + + # 如果不是按钮点击,让原有事件处理器处理 + if hasattr(self, 'original_mouse_press_event'): + self.original_mouse_press_event(event) + else: + event.ignore() + return + + # 点击在3D区域,进行拦截处理 + print('🎯 点击在3D区域,进行拦截处理') + self.handle_qt_mouse_click(event.x(), event.y()) + event.accept() + else: + # 其他按键使用原有处理方式 + if hasattr(self, 'original_mouse_press_event'): + self.original_mouse_press_event(event) + else: + event.ignore() + + def is_click_in_gui_area(self, click_x, click_y): + """检查鼠标点击是否在GUI区域(高精度版本)""" + try: + if not self.maintenance_gui: + return False + + # 获取窗口尺寸用于坐标转换 + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + widget_width = self.world.qtWidget.width() + widget_height = self.world.qtWidget.height() + else: + # 使用默认窗口尺寸 + widget_width = 1380 + widget_height = 750 + + # 使用与maintenance_gui完全相同的坐标转换算法 + aspect_x = (click_x / widget_width) * 2.67 - 1.33 + aspect_y = 1.0 - (click_y / widget_height) * 2.0 + + # 额外的坐标验证(与Panda3D标准坐标系对比) + # Panda3D aspect2d坐标系: x范围[-1.33, 1.33], y范围[-1.0, 1.0] + normalized_x = (click_x / widget_width) * 2.0 - 1.0 # 标准化到[-1, 1] + normalized_y = 1.0 - (click_y / widget_height) * 2.0 # 标准化到[-1, 1],Y轴翻转 + aspect_ratio = widget_width / widget_height + panda_x = normalized_x * aspect_ratio # 应用宽高比 + panda_y = normalized_y + + print(f"🔍 精确GUI区域检查: Qt({click_x}, {click_y}) -> aspect2d({aspect_x:.3f}, {aspect_y:.3f})") + print(f"🔍 验证坐标转换: 标准化({normalized_x:.3f}, {normalized_y:.3f}) -> Panda3D({panda_x:.3f}, {panda_y:.3f})") + print(f"📏 窗口信息: 尺寸({widget_width}x{widget_height}), 宽高比({aspect_ratio:.3f})") + + # 快速检查工具按钮区域 + if hasattr(self.maintenance_gui, 'available_tools') and self.maintenance_gui.available_tools: + # 工具按钮区域(底部条带) + tools_area_left = -1.1 + tools_area_right = 0.5 + tools_area_bottom = -1.0 + tools_area_top = -0.5 + + if (tools_area_left <= aspect_x <= tools_area_right and + tools_area_bottom <= aspect_y <= tools_area_top): + print(f"✅ 点击在工具按钮区域内") + return True + + # 检查其他GUI区域(步骤显示、当前工具、警告等) + other_gui_areas = [] + + # 步骤显示区域(顶部) + if hasattr(self.maintenance_gui, 'step_text') and self.maintenance_gui.step_text: + other_gui_areas.append({ + 'name': '步骤显示区域', + 'left': -1.8, 'right': 1.8, + 'bottom': 0.5, 'top': 1.1 + }) + + # 当前工具显示区域(右侧) + if hasattr(self.maintenance_gui, 'current_tool_text') and self.maintenance_gui.current_tool_text: + other_gui_areas.append({ + 'name': '当前工具显示区域', + 'left': 0.2, 'right': 1.8, + 'bottom': 0.6, 'top': 1.0 + }) + + # 警告显示区域(中央) + if hasattr(self.maintenance_gui, 'warning_text') and self.maintenance_gui.warning_text: + other_gui_areas.append({ + 'name': '警告显示区域', + 'left': -1.5, 'right': 1.5, + 'bottom': 0.0, 'top': 0.6 + }) + + # 检查其他GUI区域 + for area in other_gui_areas: + if (area['left'] <= aspect_x <= area['right'] and + area['bottom'] <= aspect_y <= area['top']): + print(f"✅ 点击在GUI区域内: {area['name']}") + return True + + print("❌ 点击不在任何GUI区域内") + return False + + except Exception as e: + print(f"❌ 检查GUI区域失败: {e}") + import traceback + traceback.print_exc() + return False + + def handle_gui_button_click(self, click_x, click_y): + """直接处理GUI按钮点击""" + try: + if not self.maintenance_gui: + return None + + # 获取窗口尺寸用于坐标转换 + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + widget_width = self.world.qtWidget.width() + widget_height = self.world.qtWidget.height() + else: + widget_width = 1380 + widget_height = 750 + + # 使用与maintenance_gui完全相同的坐标转换算法 + aspect_x = (click_x / widget_width) * 2.67 - 1.33 + aspect_y = 1.0 - (click_y / widget_height) * 2.0 + + print(f"🎯 直接处理按钮点击:Qt({click_x}, {click_y}) -> aspect2d({aspect_x:.3f}, {aspect_y:.3f})") + + # 检查工具按钮点击 + if hasattr(self.maintenance_gui, 'available_tools') and self.maintenance_gui.available_tools: + button_width = 0.4 + button_height = 0.25 + button_spacing = 0.45 + start_x = -0.8 + start_y = -0.75 + click_padding = 0.05 # 点击容差 + + for i, tool_data in enumerate(self.maintenance_gui.available_tools): + # 获取工具名称 + if isinstance(tool_data, dict): + tool_name = tool_data.get('name', str(tool_data)) + else: + tool_name = str(tool_data) + + # 计算按钮位置 + button_x = start_x + i * button_spacing + button_y = start_y + + # 计算按钮边界(包含容差) + left = button_x - (button_width/2 + click_padding) + right = button_x + (button_width/2 + click_padding) + bottom = button_y - (button_height/2 + click_padding) + top = button_y + (button_height/2 + click_padding) + + print(f"🔍 检查按钮{i} '{tool_name}': 范围x[{left:.3f}, {right:.3f}], y[{bottom:.3f}, {top:.3f}]") + + # 检查点击是否在按钮范围内 + if (left <= aspect_x <= right and bottom <= aspect_y <= top): + print(f"✅ 直接匹配按钮{i} '{tool_name}'!调用工具选择") + # 直接调用维修GUI的工具选择方法 + self.maintenance_gui.on_tool_selected(tool_name) + return tool_name + + print("❌ 点击不在任何按钮范围内") + return None + + except Exception as e: + print(f"❌ 直接处理按钮点击失败: {e}") + import traceback + traceback.print_exc() + return None + + def qt_mouse_release_event(self, event): + """Qt鼠标释放事件处理(维修系统优化版)""" + from PyQt5.QtCore import Qt + + # 只拦截左键释放,其他按键使用原有处理方式 + if event.button() == Qt.LeftButton: + print('🖱️ 左键释放被拦截处理') + + # 维修系统模式:先让原有处理器处理 + if self.maintenance_gui: + if hasattr(self, 'original_mouse_release_event'): + self.original_mouse_release_event(event) + + # 处理3D交互的鼠标释放 + self.on_mouse_up() + event.accept() + else: + # 其他按键释放使用原有处理方式 + if hasattr(self, 'original_mouse_release_event'): + self.original_mouse_release_event(event) + else: + event.ignore() + + def handle_qt_mouse_click(self, x, y): + """处理Qt鼠标点击(转换为3D世界坐标)""" + try: + if not self.is_active: + print("⚠️ 交互模式未激活") + return + + # 检查操作权限 + if not self.check_operation_permission(): + return + + # 获取当前步骤的目标模型 + if self.current_step >= self.total_steps: + print("⚠️ 所有步骤已完成") + return + + step_data = self.config_data['steps'][self.current_step] + target_model_name = step_data.get('target_model') + + if not target_model_name: + print("⚠️ 当前步骤没有指定目标模型") + return + + print(f'🎯 检测点击目标: {target_model_name}') + + # 获取窗口尺寸进行坐标转换 + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + widget = self.world.qtWidget + win_width = widget.width() + win_height = widget.height() + + # 转换为Panda3D的标准化坐标 + mx = 2.0 * x / float(win_width) - 1.0 + my = 1.0 - 2.0 * y / float(win_height) + + # 执行射线检测 + self.perform_ray_cast(mx, my, target_model_name) + else: + print("❌ 找不到Qt部件,无法进行坐标转换") + + except Exception as e: + print(f"❌ Qt鼠标点击处理失败: {e}") + import traceback + traceback.print_exc() + + def perform_ray_cast(self, mx, my, target_model_name): + """执行射线检测""" + try: + # 获取鼠标射线 + from panda3d.core import Point3, Point2, CollisionTraverser, CollisionHandlerQueue + from panda3d.core import CollisionNode, CollisionRay, BitMask32 + + near_point = Point3() + far_point = Point3() + self.world.cam.node().getLens().extrude(Point2(mx, my), near_point, far_point) + + # 转换到世界坐标系 + world_near = self.world.render.getRelativePoint(self.world.camera, near_point) + world_far = self.world.render.getRelativePoint(self.world.camera, far_point) + + # 进行真正的射线检测 + picker = CollisionTraverser() + queue = CollisionHandlerQueue() + + # 创建射线碰撞检测 + pickerNode = CollisionNode('mouseRay') + pickerNP = self.world.camera.attachNewNode(pickerNode) + + # 设置射线的碰撞掩码,匹配模型的碰撞掩码(第2位) + pickerNode.setFromCollideMask(BitMask32.bit(1)) + + # 使用相机坐标系的点创建射线 + direction = far_point - near_point + direction.normalize() + pickerNode.addSolid(CollisionRay(near_point, direction)) + + picker.addCollider(pickerNP, queue) + picker.traverse(self.world.render) + + # 检查是否点击到了目标模型 + hit_target_model = False + hit_point = None + + if queue.getNumEntries() > 0: + # 遍历所有碰撞结果 + for i in range(queue.getNumEntries()): + entry = queue.getEntry(i) + hit_node_path = entry.getIntoNodePath() + hit_pos = entry.getSurfacePoint(self.world.render) + + # 检查是否击中了目标模型或其相关的碰撞体 + if self.is_target_model_hit(hit_node_path, target_model_name): + print(f"✅ 击中目标模型: {hit_node_path.getName()}") + hit_target_model = True + hit_point = hit_pos + break + + # 清理碰撞检测节点 + pickerNP.removeNode() + + if hit_target_model: + # 查找目标模型节点 + target_node = self.find_model_node(target_model_name) + if target_node and not target_node.isEmpty(): + # 根据操作类型决定行为 + step_data = self.config_data['steps'][self.current_step] + operation_type = self.normalize_operation_type(step_data) + + if operation_type == 'drag': + print(f'✅ 开始拖拽目标模型: {target_node.getName()}') + self.start_dragging(target_node, hit_point) + elif operation_type == 'click': + print(f'✅ 点击触发位移模型: {target_node.getName()}') + self.trigger_click_movement(target_node, step_data) + else: + print(f"❌ 未知的操作类型: {operation_type}") + else: + print(f"❌ 找不到目标模型节点: {target_model_name}") + else: + print(f"❌ 没有点击到目标模型 '{target_model_name}',请点击正确的模型") + + except Exception as e: + print(f"❌ 射线检测失败: {e}") + import traceback + traceback.print_exc() + + def is_target_model_hit(self, hit_node_path, target_model_name): + """检查击中的节点是否是目标模型""" + hit_name = hit_node_path.getName() + + # 直接匹配模型名称 + if hit_name == target_model_name: + return True + + # 检查是否是模型的碰撞体(通常以modelCollision_开头) + if hit_name.startswith("modelCollision_"): + collision_model_name = hit_name.replace("modelCollision_", "", 1) + if collision_model_name == target_model_name: + return True + + # 检查父节点是否是目标模型 + parent = hit_node_path.getParent() + while parent and not parent.isEmpty() and parent != self.world.render: + parent_name = parent.getName() + if parent_name == target_model_name: + return True + parent = parent.getParent() + + # 检查子节点路径 + current = hit_node_path + while current and not current.isEmpty(): + if current.getName() == target_model_name: + return True + current = current.getParent() + + return False + + def on_mouse_down(self): + """鼠标按下事件(优化版 v2:处理兄弟节点结构的碰撞体)""" + if not self.is_active or not self.world.mouseWatcherNode.hasMouse(): + return + # 获取鼠标位置并发射射线 + mouse_pos = self.world.mouseWatcherNode.getMouse() + picker_ray = self.picker_ray_node.node().getSolid(0) + picker_ray.setFromLens(self.world.camNode, mouse_pos.getX(), mouse_pos.getY()) + + # 执行遍历 + self.picker_traverser.traverse(self.world.render) + + if self.collision_handler.getNumEntries() > 0: + # 获取最近的碰撞点 + self.collision_handler.sortEntries() + entry = self.collision_handler.getEntry(0) + + hit_node_path = entry.getIntoNodePath() + hit_point = entry.getSurfacePoint(self.world.render) + + # --- 这是新的逻辑 --- + # 打印被击中的碰撞体名字,用于调试 + print(f"射线击中了碰撞节点: {hit_node_path.getName()}") + + # 根据命名约定推导模型名称 + hit_name = hit_node_path.getName() + derived_model_name = "" + + # 假设碰撞节点的命名规则是 "前缀 + 模型名" + if hit_name.startswith("modelCollision_"): + derived_model_name = hit_name.replace("modelCollision_", "", 1) + else: + derived_model_name = hit_name + + print(f"根据命名约定,推导出的模型名称为: {derived_model_name}") + + # 获取当前步骤的目标模型名称 + step_data = self.config_data['steps'][self.current_step] + target_model_name = step_data.get('target_model') + + # 检查推导出的名称是否是当前步骤需要操作的目标 + if derived_model_name == target_model_name: + print(f"✅ 击中正确!目标是 '{target_model_name}'") + + # 根据推导出的正确名称,去场景中查找可以被拖动的那个节点 + model_to_drag = self.find_model_node(derived_model_name) + + if model_to_drag and not model_to_drag.isEmpty(): + self.start_dragging(model_to_drag, hit_point) + else: + print(f"❌ 错误:虽然名称匹配,但在场景中找不到名为 '{derived_model_name}' 的可拖动节点!") + else: + print(f"❌ 击中的不是当前目标模型 (需要 '{target_model_name}', 击中了 '{derived_model_name}')") + def start_dragging(self, model_node, hit_point): + """开始拖拽模型(优化版:计算偏移量)""" + # 再次检查操作权限(双重保险) + if not self.check_operation_permission(): + print("⚠️ 权限检查失败,无法开始拖拽") + return + + self.dragging_model = model_node + + # 1. 定义一个拖拽平面。这里我们使用一个平行于XZ的平面(法线为Y轴), + # 并且该平面穿过模型的初始中心点。 + # 这意味着模型将在其初始高度上水平移动。 + # 您可以根据需要更改法线,例如 Vec3(0, 0, 1) 表示在XY平面上拖拽。 + plane_normal = Vec3(0, 1, 0) + self.drag_plane = Plane(plane_normal, model_node.getPos()) + + # 2. 计算鼠标射线与拖拽平面的交点。 + # 这个交点是鼠标在“拖拽世界”中的3D位置。 + plane_intersection_point = Point3() + if self.drag_plane.intersectsLine(plane_intersection_point, + self.world.camera.getPos(self.world.render), + hit_point): + # 3. 计算模型中心点和这个交点之间的偏移量。 + # 在整个拖拽过程中,我们将维持这个偏移量,以避免模型跳跃。 + self.drag_offset = model_node.getPos() - plane_intersection_point + print(f"开始拖拽模型: {model_node.getName()}") + print(f"初始位置: {model_node.getPos()}") + + # 启动拖拽任务 + self.world.taskMgr.add(self.drag_task, "drag_model_task") + else: + print("警告:无法计算拖拽平面的交点,拖拽取消。") + self.dragging_model = None + + def drag_task(self, task): + """拖拽任务(优化版:应用偏移量)""" + if not self.dragging_model or not self.world.mouseWatcherNode.hasMouse(): + # 如果模型被置空或鼠标不在窗口内,则停止任务 + return task.done + + # 获取最新的鼠标位置 + mouse_pos = self.world.mouseWatcherNode.getMouse() + + # 再次从相机发射射线以获取鼠标在3D空间中的指向 + near_point = Point3() + far_point = Point3() + self.world.cam.node().getLens().extrude(mouse_pos, near_point, far_point) + + # 将近点和远点转换到世界坐标系 + origin_point = self.world.render.getRelativePoint(self.world.camera, near_point) + direction_point = self.world.render.getRelativePoint(self.world.camera, far_point) + + # 计算射线与我们之前定义的拖拽平面的交点 + intersection_point = Point3() + if self.drag_plane.intersectsLine(intersection_point, origin_point, direction_point): + # 新的位置 = 当前鼠标在拖拽平面上的3D位置 + 初始偏移量 + new_pos = intersection_point + self.drag_offset + self.dragging_model.setPos(new_pos) + + # 初始化距离打印时间(如果还没有) + if not hasattr(self, '_last_distance_print_time'): + self._last_distance_print_time = 0 + + # 在拖拽过程中,实时显示距离原始位置的距离 + if hasattr(self, 'config_data') and self.current_step < len(self.config_data.get('steps', [])): + step_data = self.config_data['steps'][self.current_step] + step_type = self.normalize_step_type(step_data) + target_model_name = step_data.get('target_model', '') + + if (target_model_name in self.model_original_positions and + hasattr(self, '_last_distance_print_time')): + + # 每0.5秒显示一次距离信息 + current_time = task.time + if current_time - getattr(self, '_last_distance_print_time', 0) > 0.5: + original_pos = self.model_original_positions[target_model_name] + distance = (new_pos - original_pos).length() + + if step_type == 'assemble': + snap_tolerance = step_data.get('snap_tolerance') or step_data.get('tolerance', 5.0) + if distance <= snap_tolerance * 1.2: # 只在接近时显示 + print(f"📏 安装拖拽中 - 距离原位: {distance:.2f} (需要 <= {snap_tolerance:.2f})") + else: # 拆卸模式 + disassemble_threshold = step_data.get('disassemble_threshold', 5.0) + if distance >= disassemble_threshold * 0.8: # 接近阈值时显示 + print(f"📏 拆卸拖拽中 - 距离原位: {distance:.2f} (需要 >= {disassemble_threshold:.2f})") + + self._last_distance_print_time = current_time + + return task.cont + + def stop_dragging(self): + """停止拖拽""" + if not self.dragging_model: + return + + print(f"停止拖拽模型: {self.dragging_model.getName()}") + print(f"最终位置: {self.dragging_model.getPos()}") + + # 停止拖拽任务 + self.world.taskMgr.remove("drag_model_task") + + # 检查是否完成了当前步骤 + self.check_step_completion() + + # 清理拖拽状态 + self.dragging_model = None + + def on_mouse_up(self): + """鼠标抬起事件""" + self.stop_dragging() + + def check_step_completion(self): + """检查步骤是否完成""" + if not self.dragging_model: + return + + step_data = self.config_data['steps'][self.current_step] + step_type = self.normalize_step_type(step_data) + print(f"检查步骤完成: {step_type} ----------------------------------------") + target_model_name = step_data.get('target_model', '') + current_pos = self.dragging_model.getPos() + + if step_type == 'assemble': + # 安装模式:检查是否接近原始位置 + if target_model_name in self.model_original_positions: + original_pos = self.model_original_positions[target_model_name] + distance_to_original = (original_pos - current_pos).length() + # 吸附容差:优先使用配置的snap_tolerance,否则使用拆卸时的tolerance,最后默认5.0 + snap_tolerance = step_data.get('snap_tolerance') or step_data.get('tolerance', 5.0) + + print(f"🔧 安装模式 - 距离原始位置: {distance_to_original:.2f}") + print(f"🎯 吸附容差: {snap_tolerance:.2f}") + print(f"🔍 吸附条件: {distance_to_original:.2f} <= {snap_tolerance:.2f} = {distance_to_original <= snap_tolerance}") + + if distance_to_original <= snap_tolerance: + # 自动吸附到原始位置 + print(f"✨ 自动吸附到原始位置!") + print(f"📍 精确位置: {original_pos}") + self.dragging_model.setPos(original_pos) + self.model_current_states[target_model_name] = 'installed' + print(f"步骤 {self.current_step + 1} 完成!模型已安装到原始位置") + self.complete_current_step() + return + else: + print(f"距离原始位置还有 {distance_to_original:.2f} 单位,需要更接近才能自动吸附") + else: + print("⚠️ 没有找到模型的原始位置记录") + + else: + # 拆卸模式:检查是否远离原始位置 + if target_model_name in self.model_original_positions: + original_pos = self.model_original_positions[target_model_name] + distance_from_original = (current_pos - original_pos).length() + disassemble_threshold = step_data.get('disassemble_threshold', 5.0) # 拆卸距离阈值 + + print(f"🔧 拆卸模式 - 距离原始位置: {distance_from_original:.2f}") + print(f"🎯 拆卸阈值: {disassemble_threshold:.2f}") + print(f"🔍 拆卸条件: {distance_from_original:.2f} >= {disassemble_threshold:.2f} = {distance_from_original >= disassemble_threshold}") + + if distance_from_original >= disassemble_threshold: + print(f"步骤 {self.current_step + 1} 完成!模型已拆卸,距离原位 {distance_from_original:.2f} >= {disassemble_threshold:.2f}") + if target_model_name: + self.model_current_states[target_model_name] = 'removed' + self.complete_current_step() + return + else: + print(f"需要将模型拖拽到距离原位至少 {disassemble_threshold:.2f} 单位(当前 {distance_from_original:.2f})") + else: + # 如果没有原始位置记录,回退到原来的目标位置模式 + print("⚠️ 没有找到模型的原始位置记录,使用目标位置模式") + target_pos_data = step_data.get('target_position') + + if target_pos_data is None: + # 如果没有定义目标位置,拆卸后即完成 + print("拆卸模式 - 没有定义目标位置,视为完成。") + if target_model_name: + self.model_current_states[target_model_name] = 'removed' + self.complete_current_step() + return + + target_pos = Vec3(*target_pos_data) + distance = (target_pos - current_pos).length() + tolerance = step_data.get('tolerance', 5) + + if distance <= tolerance: + print(f"步骤 {self.current_step + 1} 完成!拆卸到目标位置 距离 {distance:.2f} < 容差 {tolerance}") + if target_model_name: + self.model_current_states[target_model_name] = 'removed' + self.complete_current_step() + else: + print(f"距离目标位置还有 {distance:.2f} 单位,未达到容差 {tolerance}") + + def complete_current_step(self): + """完成当前步骤""" + # 记录操作完成(考核模式下) + if self.mode == "exam" and self.current_step in self.step_scores: + self.step_scores[self.current_step]['operation_attempts'] += 1 + print(f"📝 考核记录:步骤 {self.current_step + 1} 完成,操作次数: {self.step_scores[self.current_step]['operation_attempts']}") + + # 在进入下一步之前,取消当前模型的碰撞,避免干扰 + step_data = self.config_data['steps'][self.current_step] + target_model_name = step_data.get('target_model') + if target_model_name: + node = self.find_model_node(target_model_name) + if node: + node.setCollideMask(BitMask32.allOff()) + + # 显示当前所有模型的状态 + self.show_models_status() + + self.current_step += 1 + + if self.current_step >= self.total_steps: + self.finish_interaction() + else: + self.start_current_step() + + def finish_interaction(self): + """完成所有交互""" + print("\n🎉 所有拆装步骤已完成!") + self.is_active = False + self.ignoreAll() + + # 清理维修系统GUI + if self.maintenance_gui: + self.maintenance_gui.cleanup_gui() + print("✅ 维修系统GUI已清理") + + # 恢复原有的Qt鼠标事件处理 + print("🔄 恢复原有的Qt鼠标事件处理") + if hasattr(self, 'original_mouse_press_event') and hasattr(self.world, 'qtWidget') and self.world.qtWidget: + self.world.qtWidget.mousePressEvent = self.original_mouse_press_event + self.world.qtWidget.mouseReleaseEvent = self.original_mouse_release_event + print("✅ 原有Qt鼠标事件处理已恢复") + else: + print("⚠️ 没有找到备份的Qt事件处理方法") + + if self.step_dialog: + self.step_dialog.close() + self.step_dialog = None + + # 根据模式显示不同的完成结果 + if self.mode == "exam": + # 考核模式:显示详细的考核结果 + self.show_exam_results() + else: + # 训练模式:显示训练完成提示 + print("\n🎉 训练模式完成!") + completion_msg = "🎉 训练完成!\n\n所有维修步骤已完成!\n现在可以尝试考核模式检验学习成果。" + + if self.maintenance_gui: + self.maintenance_gui.update_step_info(completion_msg) + print("📋 训练完成信息已在GUI中显示") + else: + QMessageBox.information(None, "训练完成", completion_msg) + + def stop_interaction(self): + """用户主动停止交互""" + print("\n🛑 用户主动停止拆装交互") + + try: + # 设置为非活动状态 + self.is_active = False + self.ignoreAll() + + # 清理维修系统GUI + if self.maintenance_gui: + self.maintenance_gui.cleanup_gui() + print("✅ 维修系统GUI已清理") + + # 恢复原有的Qt鼠标事件处理 + print("🔄 恢复原有的Qt鼠标事件处理") + if hasattr(self, 'original_mouse_press_event') and hasattr(self.world, 'qtWidget') and self.world.qtWidget: + self.world.qtWidget.mousePressEvent = self.original_mouse_press_event + self.world.qtWidget.mouseReleaseEvent = self.original_mouse_release_event + print("✅ 原有Qt鼠标事件处理已恢复") + else: + print("⚠️ 没有找到备份的Qt事件处理方法") + + # 关闭步骤对话框 + if self.step_dialog: + self.step_dialog.close() + self.step_dialog = None + + # 显示停止确认信息 + if self.mode == "exam": + stop_msg = "🛑 考核已停止\n\n您的考核进度没有被保存。" + else: + stop_msg = "🛑 训练已停止\n\n您可以随时重新开始训练。" + + if self.maintenance_gui: + # 如果GUI还在,显示停止信息 + self.maintenance_gui.update_step_info(stop_msg) + else: + QMessageBox.information(None, "操作停止", stop_msg) + + print("✅ 拆装交互已停止") + + except Exception as e: + print(f"❌ 停止拆装交互失败: {e}") + import traceback + traceback.print_exc() + + def show_exam_results(self): + """显示考核结果""" + try: + # 计算最终得分 + final_score = 0 + total_operations = 0 + total_errors = 0 + + for step_record in self.step_scores.values(): + final_score += step_record['current_score'] + total_operations += step_record['operation_attempts'] + if step_record['tool_error']: + total_errors += 1 + + # 计算得分率和准确率 + score_rate = (final_score / self.exam_max_score * 100) if self.exam_max_score > 0 else 0 + accuracy_rate = ((self.total_steps - total_errors) / self.total_steps * 100) if self.total_steps > 0 else 100 + + # 生成考核报告 + print(f"\n{'='*50}") + print(f"🎓 维修技能考核结果报告") + print(f"{'='*50}") + print(f"📊 总体成绩:") + print(f" 总得分: {final_score:.0f} / {self.exam_max_score:.0f} 分") + print(f" 得分率: {score_rate:.1f}%") + print(f" 操作准确率: {accuracy_rate:.1f}%") + print(f" 总操作次数: {total_operations}") + print(f" 错误次数: {total_errors}") + + # 评级系统 + if score_rate >= 90: + grade = "优秀" + grade_icon = "🏆" + grade_desc = "表现卓越,技能熟练" + elif score_rate >= 80: + grade = "良好" + grade_icon = "🥈" + grade_desc = "表现良好,基本掌握" + elif score_rate >= 60: + grade = "及格" + grade_icon = "✅" + grade_desc = "达到基本要求" + else: + grade = "不及格" + grade_icon = "❌" + grade_desc = "需要加强练习" + + print(f"\n🏅 评级: {grade_icon} {grade}") + print(f" 评语: {grade_desc}") + + # 详细步骤分析 + print(f"\n📋 详细步骤分析:") + print(f"{'-'*40}") + + perfect_steps = 0 + for step_idx, step_record in self.step_scores.items(): + step_data = self.config_data['steps'][step_idx] + step_name = step_data.get('name', f'步骤{step_idx+1}') + max_score = step_record['max_score'] + current_score = step_record['current_score'] + tool_error = step_record['tool_error'] + attempts = step_record['operation_attempts'] + + # 判断步骤完成质量 + if current_score >= max_score: + step_status = "🟢 完美" + perfect_steps += 1 + elif current_score >= max_score * 0.8: + step_status = "🟡 良好" + elif current_score > 0: + step_status = "🟠 一般" + else: + step_status = "🔴 失败" + + print(f"{step_name}:") + print(f" 状态: {step_status}") + print(f" 得分: {current_score:.0f}/{max_score:.0f} 分") + print(f" 工具使用: {'❌ 错误' if tool_error else '✅ 正确'}") + print(f" 操作次数: {attempts}") + print() + + # 生成改进建议 + suggestions = [] + if total_errors > 0: + suggestions.append("• 注意选择正确的工具进行操作") + if total_operations > self.total_steps * 1.5: + suggestions.append("• 减少不必要的操作,提高操作效率") + if perfect_steps < self.total_steps * 0.5: + suggestions.append("• 多练习以提高操作的精确度") + if score_rate < 80: + suggestions.append("• 建议重新学习相关理论知识") + + if suggestions: + print("💡 改进建议:") + for suggestion in suggestions: + print(f" {suggestion}") + print() + + print(f"{'='*50}") + + # 为GUI显示准备简化版本的报告 + gui_report = f"🎓 考核完成!\n\n" + gui_report += f"📊 成绩总览:\n" + gui_report += f"总得分: {final_score:.0f}/{self.exam_max_score:.0f} 分 ({score_rate:.1f}%)\n" + gui_report += f"评级: {grade_icon} {grade}\n" + gui_report += f"操作准确率: {accuracy_rate:.1f}%\n\n" + gui_report += f"完美完成步骤: {perfect_steps}/{self.total_steps}\n" + gui_report += f"总操作次数: {total_operations}\n" + gui_report += f"错误次数: {total_errors}" + + # 在维修GUI中显示详细考核结果 + if self.maintenance_gui: + # 创建详细的考核结果数据 + exam_result_data = { + 'final_score': final_score, + 'max_score': self.exam_max_score, + 'score_rate': score_rate, + 'accuracy_rate': accuracy_rate, + 'grade': grade, + 'grade_icon': grade_icon, + 'grade_desc': grade_desc, + 'total_operations': total_operations, + 'total_errors': total_errors, + 'perfect_steps': perfect_steps, + 'total_steps': self.total_steps, + 'step_details': [], + 'suggestions': suggestions + } + + # 添加每步详情 + for step_idx, step_record in self.step_scores.items(): + step_data = self.config_data['steps'][step_idx] + step_name = step_data.get('name', f'步骤{step_idx+1}') + max_score = step_record['max_score'] + current_score = step_record['current_score'] + tool_error = step_record['tool_error'] + attempts = step_record['operation_attempts'] + + # 判断步骤完成质量 + if current_score >= max_score: + step_status = "🟢 完美" + elif current_score >= max_score * 0.8: + step_status = "🟡 良好" + elif current_score > 0: + step_status = "🟠 一般" + else: + step_status = "🔴 失败" + + exam_result_data['step_details'].append({ + 'name': step_name, + 'status': step_status, + 'current_score': current_score, + 'max_score': max_score, + 'tool_error': tool_error, + 'attempts': attempts + }) + + # 使用GUI显示考核结果 + self.maintenance_gui.show_exam_results(exam_result_data) + print("📋 考核结果已在GUI中显示") + else: + # 如果没有GUI,使用简单的控制台输出 + print("⚠️ 维修GUI不可用,考核结果仅在控制台显示") + + # 保存考核结果到文件(可选) + self.save_exam_results(final_score, score_rate, grade) + + except Exception as e: + print(f"❌ 显示考核结果失败: {e}") + import traceback + traceback.print_exc() + QMessageBox.critical(None, "错误", f"显示考核结果失败: {str(e)}") + + def save_exam_results(self, final_score, score_rate, grade): + """保存考核结果到文件""" + try: + import datetime + import json + + # 生成考核记录 + exam_record = { + "timestamp": datetime.datetime.now().isoformat(), + "mode": "exam", + "final_score": final_score, + "max_score": self.exam_max_score, + "score_rate": score_rate, + "grade": grade, + "total_steps": self.total_steps, + "step_details": {} + } + + # 添加每步详情 + for step_idx, step_record in self.step_scores.items(): + step_name = self.config_data['steps'][step_idx].get('name', f'步骤{step_idx+1}') + exam_record["step_details"][step_name] = { + "max_score": step_record['max_score'], + "current_score": step_record['current_score'], + "tool_error": step_record['tool_error'], + "operation_attempts": step_record['operation_attempts'] + } + + # 保存到文件 + filename = f"exam_result_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + filepath = os.path.join(".", filename) + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(exam_record, f, ensure_ascii=False, indent=2) + + print(f"📄 考核结果已保存到: {filepath}") + + except Exception as e: + print(f"⚠️ 保存考核结果失败: {e}") + # 不影响主流程,只是记录警告 + + def stop_interaction_mode(self): + """停止交互模式""" + self.is_active = False + self.ignoreAll() + + # 恢复原有的Qt鼠标事件处理 + print("🔄 恢复原有的Qt鼠标事件处理") + if hasattr(self, 'original_mouse_press_event') and hasattr(self.world, 'qtWidget') and self.world.qtWidget: + self.world.qtWidget.mousePressEvent = self.original_mouse_press_event + self.world.qtWidget.mouseReleaseEvent = self.original_mouse_release_event + print("✅ 原有Qt鼠标事件处理已恢复") + else: + print("⚠️ 没有找到备份的Qt事件处理方法") + + if self.step_dialog: + self.step_dialog.close() + self.step_dialog = None + + self.world.taskMgr.remove("drag_model_task") + print("拆装交互模式已停止") + + def trigger_drag_by_keyboard(self): + """通过键盘触发拖拽(备用方案)""" + if not self.is_active or self.dragging_model: + return + + print("⌨️ 使用键盘备用方案触发拖拽") + step_data = self.config_data['steps'][self.current_step] + target_model_name = step_data.get('target_model') + + if not target_model_name: + print("当前步骤没有指定目标模型") + return + + target_node = self.find_model_node(target_model_name) + if not target_node: + print(f"找不到目标模型: {target_model_name}") + return + + # 对于键盘触发,我们无法获取精确的hit_point, + # 所以直接使用模型的中心点作为模拟的点击点。 + self.start_dragging(target_node, target_node.getPos()) + + def trigger_click_movement(self, model_node, step_data): + """触发点击位移动画""" + try: + # 检查操作权限(双重保险) + if not self.check_operation_permission(): + print("⚠️ 权限检查失败,无法进行点击触发位移") + return + + step_type = self.normalize_step_type(step_data) + target_model_name = step_data.get('target_model', '') + + print(f"🎬 开始点击触发位移: {model_node.getName()}") + print(f"📋 步骤类型: {step_type}") + + if step_type == 'disassemble': + # 拆卸模式:移动到目标位置然后隐藏 + self.perform_disassemble_click_movement(model_node, step_data) + elif step_type == 'assemble': + # 安装模式:显示模型然后移动到原始位置 + self.perform_assemble_click_movement(model_node, step_data) + else: + print(f"❌ 未知的步骤类型: {step_type}") + + except Exception as e: + print(f"❌ 点击触发位移失败: {e}") + import traceback + traceback.print_exc() + + def perform_disassemble_click_movement(self, model_node, step_data): + """执行拆卸点击位移""" + target_model_name = step_data.get('target_model', '') + + # 记录当前位置为原始位置(如果还没有记录) + if target_model_name not in self.model_original_positions: + current_pos = model_node.getPos() + self.model_original_positions[target_model_name] = current_pos + print(f"📍 记录模型 '{target_model_name}' 的原始位置: {current_pos}") + + # 获取目标位移位置 + target_pos_data = step_data.get('target_position') + if not target_pos_data: + print("❌ 拆卸步骤缺少target_position配置") + return + + target_pos = Vec3(*target_pos_data) + print(f"🎯 目标位置: {target_pos}") + + # 获取动画时长 + animation_duration = step_data.get('animation_duration', 2.0) + + # 创建位移动画序列 + from direct.interval.IntervalGlobal import LerpPosInterval, Sequence, Func + + move_interval = LerpPosInterval( + model_node, + animation_duration, + target_pos, + name=f"disassemble_{target_model_name}" + ) + + # 动画完成后的回调 + def on_disassemble_complete(): + print(f"✅ 拆卸动画完成,隐藏模型: {target_model_name}") + model_node.hide() # 隐藏模型 + self.model_current_states[target_model_name] = 'removed' + self.complete_current_step() # 完成当前步骤 + + # 创建动画序列 + disassemble_sequence = Sequence( + move_interval, + Func(on_disassemble_complete), + name=f"disassemble_sequence_{target_model_name}" + ) + + print(f"🎬 开始拆卸动画,持续时间: {animation_duration}秒") + disassemble_sequence.start() + + def perform_assemble_click_movement(self, model_node, step_data): + """执行安装点击位移""" + target_model_name = step_data.get('target_model', '') + + # 确保模型可见(通常在步骤开始时已经显示,这里是双重保险) + if model_node.isHidden(): + print(f"👁️ 点击触发时显示模型: {target_model_name}") + model_node.show() + else: + print(f"✅ 模型 '{target_model_name}' 已经可见,开始安装动画") + + # 获取原始位置 + if target_model_name not in self.model_original_positions: + print(f"❌ 找不到模型 '{target_model_name}' 的原始位置") + return + + original_pos = self.model_original_positions[target_model_name] + print(f"🏠 原始位置: {original_pos}") + + # 获取动画时长 + animation_duration = step_data.get('animation_duration', 2.0) + + # 创建位移动画序列 + from direct.interval.IntervalGlobal import LerpPosInterval, Sequence, Func + + move_interval = LerpPosInterval( + model_node, + animation_duration, + original_pos, + name=f"assemble_{target_model_name}" + ) + + # 动画完成后的回调 + def on_assemble_complete(): + print(f"✅ 安装动画完成,模型回到原位: {target_model_name}") + model_node.setPos(original_pos) # 确保精确位置 + self.model_current_states[target_model_name] = 'installed' + self.complete_current_step() # 完成当前步骤 + + # 创建动画序列 + assemble_sequence = Sequence( + move_interval, + Func(on_assemble_complete), + name=f"assemble_sequence_{target_model_name}" + ) + + print(f"🎬 开始安装动画,持续时间: {animation_duration}秒") + assemble_sequence.start() + + +class StepGuideDialog(QDialog): + """步骤指引对话框(UI代码保持不变)""" + + def __init__(self, interaction_manager, parent=None): + super().__init__(parent) + self.interaction_manager = interaction_manager + self.current_required_tool = "无" # 当前步骤要求的工具 + self.mode = interaction_manager.mode # 获取模式 + + self.chinese_font = None + if hasattr(interaction_manager.world,'getChineseFont'): + self.chinese_font = interaction_manager.world.getChineseFont() + + self.setupUI() + + def setupUI(self): + if self.mode == "exam": + self.setWindowTitle("拆装考核") + else: + self.setWindowTitle("拆装步骤指引") + + self.setFixedSize(450, 400) + self.setWindowFlags(Qt.Window | Qt.WindowStaysOnTopHint) + layout = QVBoxLayout(self) + + # 步骤信息 + self.step_info_label = QLabel("准备开始...") + self.step_info_label.setStyleSheet("font-size: 14px; font-weight: bold; color: #2E86C1;") + if self.chinese_font: + self.step_info_label.setFont(self.chinese_font) + layout.addWidget(self.step_info_label) + + # 步骤描述(考核模式下隐藏) + if self.mode != "exam": + self.step_desc_text = QTextEdit() + self.step_desc_text.setMaximumHeight(100) + self.step_desc_text.setReadOnly(True) + if self.chinese_font: + self.step_desc_text.setFont(self.chinese_font) + layout.addWidget(self.step_desc_text) + else: + self.step_desc_text = None + + # 工具选择组 + tool_group = QGroupBox("工具选择") + if self.chinese_font: + tool_group.setFont(self.chinese_font) + tool_layout = QVBoxLayout(tool_group) + + # 在训练模式下显示要求的工具,考核模式下不显示 + if self.mode != "exam": + self.required_tool_label = QLabel("当前步骤要求工具: 无") + self.required_tool_label.setStyleSheet("font-weight: bold; color: #E74C3C;") + if self.chinese_font: + self.required_tool_label.setFont(self.chinese_font) + tool_layout.addWidget(self.required_tool_label) + else: + self.required_tool_label = None + + current_tool_layout = QHBoxLayout() + current_tool_label = QLabel("当前选择工具:") + if self.chinese_font: + current_tool_label.setFont(self.chinese_font) + current_tool_layout.addWidget(current_tool_label) + self.current_tool_combo = QComboBox() + self.current_tool_combo.addItem("无") # 默认只有"无"选项 + self.current_tool_combo.currentTextChanged.connect(self.on_tool_changed) + if self.chinese_font: + self.current_tool_combo.setFont(self.chinese_font) + current_tool_layout.addWidget(self.current_tool_combo) + tool_layout.addLayout(current_tool_layout) + + # 在训练模式下显示工具状态,考核模式下不显示 + if self.mode != "exam": + self.tool_status_label = QLabel("✅ 工具匹配,可以进行操作") + self.tool_status_label.setStyleSheet("color: #27AE60; font-weight: bold;") + if self.chinese_font: + self.tool_status_label.setFont(self.chinese_font) + tool_layout.addWidget(self.tool_status_label) + else: + self.tool_status_label = None + + layout.addWidget(tool_group) + + # 操作提示(考核模式下隐藏) + if self.mode != "exam": + self.operation_label = QLabel("操作提示:") + self.operation_label.setStyleSheet("font-weight: bold;") + if self.chinese_font: + self.operation_label.setFont(self.chinese_font) + layout.addWidget(self.operation_label) + self.operation_text = QTextEdit() + self.operation_text.setMaximumHeight(80) + self.operation_text.setReadOnly(True) + if self.chinese_font: + self.operation_text.setFont(self.chinese_font) + layout.addWidget(self.operation_text) + else: + self.operation_label = None + self.operation_text = None + + # 按钮 + button_layout = QHBoxLayout() + self.skip_button = QPushButton("跳过当前步骤") + if self.chinese_font: + self.skip_button.setFont(self.chinese_font) + self.skip_button.clicked.connect(self.skip_current_step) + button_layout.addWidget(self.skip_button) + self.stop_button = QPushButton("停止交互") + if self.chinese_font: + self.stop_button.setFont(self.chinese_font) + self.stop_button.clicked.connect(self.stop_interaction) + button_layout.addWidget(self.stop_button) + layout.addLayout(button_layout) + + # 初始化工具状态 + self.sync_current_tool() + self.update_tool_status() + + def sync_current_tool(self): + """初始化拆装工具状态""" + # 从配置文件中加载工具列表 + self.load_tools_from_config() + # 默认选择"无" + self.current_tool_combo.setCurrentText("无") + print(f"🔧 初始化拆装工具: 无") + + def load_tools_from_config(self): + """从配置文件中加载工具列表""" + # 清空现有工具选项(保留"无") + self.current_tool_combo.clear() + self.current_tool_combo.addItem("无") + + # 从配置文件中加载工具 + if hasattr(self.interaction_manager, 'config_data') and self.interaction_manager.config_data: + tools = self.interaction_manager.config_data.get('tools', []) + for tool in tools: + tool_name = tool.get('name', '') + if tool_name and tool_name != "无": + self.current_tool_combo.addItem(tool_name) + print(f"🔧 加载工具: {tool_name}") + + if len(tools) > 0: + print(f"🔧 从配置文件加载了 {len(tools)} 个工具") + else: + print("🔧 配置文件中没有定义工具") + else: + print("⚠️ 无法访问配置数据") + + def on_tool_changed(self, tool_name): + """工具改变时的处理""" + print(f"🔧 用户选择拆装工具: {tool_name}") + + # 拆装工具不需要同步到编辑器的工具管理器 + # 这里是独立的拆装工具选择 + + # 更新工具状态显示 + self.update_tool_status() + + def update_tool_status(self): + """更新工具状态显示""" + current_tool = self.current_tool_combo.currentText() + required_tool = self.current_required_tool + + # 检查工具是否匹配 + tool_matches = self.check_tool_permission(current_tool, required_tool) + + if self.mode == "training": + # 训练模式:显示工具匹配状态 + if tool_matches: + self.tool_status_label.setText("✅ 工具匹配,可以进行操作") + self.tool_status_label.setStyleSheet("color: #27AE60; font-weight: bold;") + # 启用交互操作 + self.interaction_manager.set_operation_enabled(True) + else: + self.tool_status_label.setText(f"❌ 工具不匹配,需要选择 '{required_tool}' 工具") + self.tool_status_label.setStyleSheet("color: #E74C3C; font-weight: bold;") + # 禁用交互操作 + self.interaction_manager.set_operation_enabled(False) + else: + # 考核模式:不显示工具状态,但仍然需要正确的工具才能操作 + # 这里不再总是启用操作,而是让权限检查在实际操作时进行 + self.interaction_manager.set_operation_enabled(True) + + def check_tool_permission(self, current_tool, required_tool): + """检查工具权限""" + # 如果步骤不要求特定工具,任何工具都可以 + if required_tool == "无" or required_tool == "" or not required_tool: + return True + + # 精确匹配 + if current_tool == required_tool: + return True + + return False + + def update_step_info(self, step_data, current_step, total_steps): + step_name = step_data.get('name', f'步骤 {current_step}') + + if self.mode == "exam": + # 考核模式:显示考核信息 + step_score = step_data.get('score', 10) + self.step_info_label.setText(f"第 {current_step}/{total_steps} 步: {step_name} (分值: {step_score})") + else: + # 训练模式:正常显示 + self.step_info_label.setText(f"第 {current_step}/{total_steps} 步: {step_name}") + + # 为动态文本设置中文字体 + if self.chinese_font: + self.step_info_label.setFont(self.chinese_font) + + # 步骤描述(考核模式下不显示) + if self.step_desc_text is not None: + step_desc = step_data.get('description', '无描述') + self.step_desc_text.setPlainText(step_desc) + if self.chinese_font: + self.step_desc_text.setFont(self.chinese_font) + + # 更新工具要求 + self.current_required_tool = step_data.get('required_tool', '无') + if self.required_tool_label is not None: + self.required_tool_label.setText(f"当前步骤要求工具: {self.current_required_tool}") + if self.chinese_font: + self.required_tool_label.setFont(self.chinese_font) + + # 更新工具状态 + self.update_tool_status() + + operation_type = self.interaction_manager.normalize_operation_type(step_data) + step_type = self.interaction_manager.normalize_step_type(step_data) + target_model = step_data.get('target_model', '未指定') + + if operation_type == 'drag': + if step_type == 'assemble': + snap_tolerance = step_data.get('snap_tolerance') or step_data.get('tolerance', 5.0) + operation_hint = f"🔧 安装操作\n请用鼠标左键点击并拖拽模型 '{target_model}' 到原始位置附近。\n当距离原始位置 {snap_tolerance:.1f} 单位内时,模型会自动吸附到正确位置。" + else: + disassemble_threshold = step_data.get('disassemble_threshold', 5.0) + operation_hint = f"🔧 拆卸操作\n请用鼠标左键点击并拖拽模型 '{target_model}' 脱离原位。\n拖拽到距离原始位置 {disassemble_threshold:.1f} 单位外即可完成。" + elif operation_type == 'click': + if step_type == 'assemble': + animation_duration = step_data.get('animation_duration', 2.0) + operation_hint = f"🔧 安装操作(点击触发)\n请点击模型 '{target_model}' 触发自动安装。\n模型将自动移动到原始位置(动画时长 {animation_duration:.1f}秒)。" + else: + target_pos_data = step_data.get('target_position') + animation_duration = step_data.get('animation_duration', 2.0) + if target_pos_data: + from panda3d.core import Vec3 + target_pos = Vec3(*target_pos_data) + operation_hint = f"🔧 拆卸操作(点击触发)\n请点击模型 '{target_model}' 触发自动拆卸。\n模型将自动移动到位置 {target_pos}(动画时长 {animation_duration:.1f}秒),然后隐藏。" + else: + operation_hint = f"🔧 拆卸操作(点击触发)\n请点击模型 '{target_model}' 触发自动拆卸。\n⚠️ 警告:缺少target_position配置" + else: + # 未知操作类型的回退提示 + if step_type == 'assemble': + operation_hint = f"🔧 安装操作\n请操作模型 '{target_model}' 将其安装到原位。" + else: + operation_hint = f"🔧 拆卸操作\n请操作模型 '{target_model}' 将其拆卸。" + + # 操作提示(考核模式下不显示) + if self.operation_text is not None: + self.operation_text.setPlainText(operation_hint) + if self.chinese_font: + self.operation_text.setFont(self.chinese_font) + + def skip_current_step(self): + if self.interaction_manager.is_active: + # 停止可能正在进行的拖拽 + if self.interaction_manager.dragging_model: + self.interaction_manager.stop_dragging() + self.interaction_manager.complete_current_step() + + def stop_interaction(self): + self.interaction_manager.stop_interaction_mode() + self.close() + + def closeEvent(self, event): + reply = QMessageBox.question(self, "确认", "是否要停止拆装交互?", + QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.Yes: + self.stop_interaction() + event.accept() + else: event.ignore() \ No newline at end of file diff --git a/core/collision_manager.py b/core/collision_manager.py index 6117ea30..f335517a 100644 --- a/core/collision_manager.py +++ b/core/collision_manager.py @@ -1,1067 +1,1067 @@ -import time -from datetime import datetime -from panda3d.core import ( - CollisionTraverser, CollisionHandlerQueue, CollisionNode, - CollisionRay, CollisionSphere, BitMask32, Point3, Vec3 -) -from direct.task.TaskManagerGlobal import taskMgr - -class CollisionManager: - """统一的碰撞检测管理器""" - - def __init__(self, world): - self.world = world - self.traverser = CollisionTraverser("main_traverser") - self.queue = CollisionHandlerQueue() - - # 碰撞掩码定义 - self.MASKS = { - # === 基础碰撞掩码定义 === - # 每个掩码使用不同的位(bit)来标识,可以进行位运算组合 - - # 'TERRAIN': BitMask32.bit(0), # 地形/地面 - 通常用于地面碰撞检测 - # 'UI_ELEMENT': BitMask32.bit(1), # UI元素 - 界面组件的碰撞检测 - # 'CAMERA': BitMask32.bit(2), # 摄像机 - 相机的碰撞检测 - 'MODEL_COLLISION': BitMask32.bit(6), # 模型碰撞 - 通用模型间碰撞检测 - } - - # 碰撞体形状类型 - self.COLLISION_SHAPES = { - 'SPHERE': 'sphere', - 'BOX': 'box', - 'CAPSULE': 'capsule', - 'PLANE': 'plane', - 'POLYGON': 'polygon', - 'MESH': 'mesh' - } - - # 掩码组合定义 - self.MASK_COMBINATIONS = { - } - - # 碰撞分组管理 - self.collision_groups = {} - self.group_enabled = {} - - # 碰撞回调系统 - self.collision_callbacks = {} - self.collision_filters = {} - - # 空间分割系统(八叉树) - self.spatial_partitioning_enabled = False - self.octree = None - self.octree_max_depth = 6 - self.octree_max_objects = 10 - - # 连续碰撞检测 - self.ccd_enabled = False - self.ccd_threshold = 10.0 # 速度阈值 - - # 性能监控 - self.performance_monitor = CollisionPerformanceMonitor() - - # 模型间碰撞检测配置 - self.model_collision_enabled = False - self.collision_detection_frequency = 0.1 - self.collision_distance_threshold = 0 - self.last_collision_check = 0 - self.collision_history = [] - self.max_collision_history = 100 - - # 碰撞状态跟踪 - 避免重复打印 - self.active_collisions = set() # 当前活跃的碰撞对 - - # 模型间碰撞检测任务 - self.collision_task = None - - print("✅ 碰撞检测管理器初始化完成") - - def enableModelCollisionDetection(self, enable=True, frequency=0.1, threshold=0.5): - """启用/禁用模型间碰撞检测 - - Args: - enable: 是否启用碰撞检测 - frequency: 检测频率(秒) - threshold: 碰撞距离阈值 - """ - self.model_collision_enabled = enable - self.collision_detection_frequency = frequency - self.collision_distance_threshold = threshold - - if enable: - print(f"✅ 启用模型间碰撞检测 - 频率: {frequency}s, 阈值: {threshold}") - self._startCollisionDetectionTask() - else: - print("❌ 禁用模型间碰撞检测") - self._stopCollisionDetectionTask() - - def _startCollisionDetectionTask(self): - """启动碰撞检测任务""" - if self.collision_task: - taskMgr.remove(self.collision_task) - - def collisionDetectionTask(task): - try: - self.detectModelCollisions() - return task.again - except Exception as e: - print(f"❌ 碰撞检测任务异常: {str(e)}") - return task.again - - self.collision_task = taskMgr.doMethodLater( - self.collision_detection_frequency, - collisionDetectionTask, - "modelCollisionDetection" - ) - - def _stopCollisionDetectionTask(self): - """停止碰撞检测任务""" - if self.collision_task: - taskMgr.remove(self.collision_task) - self.collision_task = None - - def detectModelCollisions(self, specific_models=None, log_results=True): - """检测模型间碰撞""" - if not self.model_collision_enabled and specific_models is None: - return [] - - start_time = time.perf_counter() - current_time = datetime.now() - - models_to_check = specific_models if specific_models else self.world.models - if len(models_to_check) < 2: - return [] - - collision_results = [] - checked_pairs = set() - current_collision_pairs = set() - - # 遍历所有模型对 - for i, model_a in enumerate(models_to_check): - for j, model_b in enumerate(models_to_check[i+1:], i+1): - pair_key = tuple(sorted([id(model_a), id(model_b)])) - if pair_key in checked_pairs: - continue - checked_pairs.add(pair_key) - - collision_info = self._checkModelPairCollision(model_a, model_b, current_time) - if collision_info: - collision_results.append(collision_info) - current_collision_pairs.add(pair_key) - - # 只有新碰撞才打印日志 - if log_results and pair_key not in self.active_collisions: - self._logCollisionInfo(collision_info) - print(f"🆕 新碰撞检测到!") - - # 检测碰撞结束的情况 - ended_collisions = self.active_collisions - current_collision_pairs - for pair_key in ended_collisions: - print(f"✅ 碰撞结束: 模型对 {pair_key}") - - # 更新活跃碰撞状态 - self.active_collisions = current_collision_pairs - - # 记录性能和历史 - detection_time = time.perf_counter() - start_time - self.performance_monitor.recordModelCollisionDetection( - detection_time, len(models_to_check), len(collision_results)) - - if collision_results: - self.collision_history.extend(collision_results) - if len(self.collision_history) > self.max_collision_history: - self.collision_history = self.collision_history[-self.max_collision_history:] - - return collision_results - - def _checkModelPairCollision(self, model_a, model_b, timestamp): - """使用Panda3D内置碰撞系统检查两个模型是否碰撞""" - try: - # 检查模型是否有碰撞节点 - collision_node_a = self._findCollisionNode(model_a) - collision_node_b = self._findCollisionNode(model_b) - - if not collision_node_a or not collision_node_b: - return None - - # 使用Panda3D的碰撞遍历器 - temp_traverser = CollisionTraverser() - temp_queue = CollisionHandlerQueue() - - # 设置碰撞检测 - temp_traverser.addCollider(collision_node_a, temp_queue) - - # 执行碰撞检测 - temp_traverser.traverse(model_b) - - if temp_queue.getNumEntries() > 0: - # 有碰撞,获取碰撞信息 - entry = temp_queue.getEntry(0) - - center_a = model_a.getPos(self.world.render) - center_b = model_b.getPos(self.world.render) - distance = (center_b - center_a).length() - - return { - 'timestamp': timestamp, - 'model_a': { - 'name': model_a.getName(), - 'center': center_a, - 'radius': 0, # 不再需要手动计算半径 - 'node': model_a - }, - 'model_b': { - 'name': model_b.getName(), - 'center': center_b, - 'radius': 0, - 'node': model_b - }, - 'collision_point': entry.getSurfacePoint(self.world.render), - 'distance': distance, - 'penetration_depth': 0, # Panda3D会自动处理 - 'collision_normal': entry.getSurfaceNormal(self.world.render) - } - - return None - - except Exception as e: - print(f"❌ 检测模型对碰撞失败 ({model_a.getName()} vs {model_b.getName()}): {str(e)}") - return None - - def _findCollisionNode(self, model): - """查找模型的碰撞节点""" - for child in model.getChildren(): - if isinstance(child.node(), CollisionNode): - return child - return None - - def _logCollisionInfo(self, collision_info): - """输出碰撞信息日志""" - timestamp = collision_info['timestamp'].strftime('%H:%M:%S.%f')[:-3] - model_a = collision_info['model_a'] - model_b = collision_info['model_b'] - - print(f"🔥 [{timestamp}] 检测到碰撞:") - print(f" 模型A: {model_a['name']} (中心: {model_a['center']}, 半径: {model_a['radius']:.2f})") - print(f" 模型B: {model_b['name']} (中心: {model_b['center']}, 半径: {model_b['radius']:.2f})") - print(f" 碰撞点: {collision_info['collision_point']}") - print(f" 中心距离: {collision_info['distance']:.3f}") - print(f" 穿透深度: {collision_info['penetration_depth']:.3f}") - print(f" 碰撞法向: {collision_info['collision_normal']}") - - def getCollisionHistory(self, limit=None): - """获取碰撞历史记录 - - Args: - limit: 返回记录数量限制 - - Returns: - list: 碰撞历史记录 - """ - if limit: - return self.collision_history[-limit:] - return self.collision_history.copy() - - def clearCollisionHistory(self): - """清除碰撞历史记录""" - self.collision_history.clear() - print("✅ 碰撞历史记录已清除") - - def getCollisionStatistics(self): - """获取碰撞统计信息""" - if not self.collision_history: - return {"total_collisions": 0, "unique_pairs": 0, "most_frequent_pair": None} - - # 统计碰撞对 - collision_pairs = {} - for collision in self.collision_history: - pair_key = tuple(sorted([ - collision['model_a']['name'], - collision['model_b']['name'] - ])) - collision_pairs[pair_key] = collision_pairs.get(pair_key, 0) + 1 - - most_frequent_pair = max(collision_pairs.items(), key=lambda x: x[1]) - - return { - "total_collisions": len(self.collision_history), - "unique_pairs": len(collision_pairs), - "most_frequent_pair": { - "models": most_frequent_pair[0], - "count": most_frequent_pair[1] - }, - "collision_pairs": collision_pairs - } - - def createCollisionShape(self, model, shape_type='auto', **kwargs): - """为模型创建指定类型的碰撞体 - - Args: - model: 模型节点 - shape_type: 碰撞体类型 ('auto', 'sphere', 'box', 'capsule', 'plane', 'polygon') - **kwargs: 形状特定参数 - - Returns: - CollisionSolid: 创建的碰撞体 - """ - from panda3d.core import ( - CollisionSphere, CollisionBox, CollisionCapsule, - CollisionPlane, CollisionPolygon, Plane, Vec3 - ) - - # 获取考虑变换后的实际尺寸和中心 - transformed_info = self._getTransformedModelInfo(model) - if not transformed_info: - # 默认小球体 - return CollisionSphere(Point3(0, 0, 0), 1.0) - - center = transformed_info['center'] - radius = transformed_info['radius'] - actual_size = transformed_info['size'] - scale_factor = transformed_info['scale_factor'] - - # 自动选择最适合的形状 - if shape_type == 'auto': - shape_type = self._determineOptimalShape(model, transformed_info) - - if shape_type == 'sphere': - # 优化球形碰撞体 - sphere_radius = kwargs.get('radius', radius) - - # 支持位置偏移 - pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0)) - sphere_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z) - - return CollisionSphere(sphere_center, sphere_radius) - - elif shape_type == 'box': - # 优化盒型碰撞体 - 更精确的尺寸和位置控制(考虑缩放) - # 获取自定义尺寸,如果没有提供则使用变换后的实际尺寸 - width = kwargs.get('width', actual_size.x) - length = kwargs.get('length', actual_size.y) - height = kwargs.get('height', actual_size.z) - - # 支持位置偏移 - pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0)) - box_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z) - - # 计算盒子的最小和最大点(基于偏移后的中心) - half_width = width / 2 - half_length = length / 2 - half_height = height / 2 - - min_point = Point3(box_center.x - half_width, box_center.y - half_length, box_center.z - half_height) - max_point = Point3(box_center.x + half_width, box_center.y + half_length, box_center.z + half_height) - return CollisionBox(min_point, max_point) - - elif shape_type == 'capsule': - # 优化胶囊体碰撞 - 更合理的比例和位置控制(考虑缩放) - # 使用变换后的实际高度,或自定义高度 - custom_height = kwargs.get('height', actual_size.z) - - # 更合理的半径计算:基于变换后模型宽度的平均值 - default_radius = min(actual_size.x, actual_size.y) / 2.5 # 稍微小于模型的宽度 - custom_radius = kwargs.get('radius', min(default_radius, custom_height * 0.4)) - - # 支持位置偏移 - pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0)) - capsule_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z) - - # 计算胶囊体的两个端点(确保半径不会超出高度) - effective_height = max(custom_height, custom_radius * 2.1) # 确保高度至少是半径的2倍多一点 - point_a = Point3(capsule_center.x, capsule_center.y, capsule_center.z - effective_height/2 + custom_radius) - point_b = Point3(capsule_center.x, capsule_center.y, capsule_center.z + effective_height/2 - custom_radius) - return CollisionCapsule(point_a, point_b, custom_radius) - - elif shape_type == 'plane': - # 优化平面碰撞 - 支持位置偏移和更灵活的法向量 - normal = kwargs.get('normal', Vec3(0, 0, 1)) - - # 支持位置偏移 - pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0)) - plane_point = kwargs.get('point', Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z)) - - # 标准化法向量 - normal.normalize() - plane = Plane(normal, plane_point) - return CollisionPlane(plane) - - elif shape_type == 'polygon': - # === 创建多边形碰撞体 === - # CollisionPolygon特点: - # - 单个平面多边形,所有顶点必须共面 - # - 适用于:地板、墙面、简单平台等平面区域 - # - 性能较好,但只能表示平面形状 - # - # Mesh碰撞体特点: - # - 由多个三角形组成的复杂3D形状 - # - 适用于:复杂地形、建筑物、不规则物体 - # - 性能较差,但可以表示任意复杂形状 - - vertices = kwargs.get('vertices', []) - if len(vertices) >= 3: - # 将顶点转换为Point3对象 - # 要求:所有顶点必须在同一平面上 - collision_poly = CollisionPolygon(*[Point3(*v) for v in vertices]) - return collision_poly - else: - #print("⚠️ 多边形至少需要3个顶点,回退到球体") - return CollisionSphere(center, radius) - - def _determineOptimalShape(self, model, transformed_info): - """根据模型特征自动确定最适合的碰撞体形状""" - # 获取变换后的模型尺寸比例 - size = transformed_info['size'] - max_dim = max(size.x, size.y, size.z) - min_dim = min(size.x, size.y, size.z) - - # 根据模型名称和比例判断 - model_name = model.getName().lower() - - # 角色类模型使用胶囊体 - if any(keyword in model_name for keyword in ['character', 'human', 'player', 'npc']): - return 'capsule' - - # 建筑、箱子类使用包围盒 - if any(keyword in model_name for keyword in ['building', 'box', 'cube', 'wall', 'door']): - return 'box' - - # 地面、平台使用平面 - if any(keyword in model_name for keyword in ['ground', 'floor', 'platform', 'terrain']): - return 'plane' - - # 根据尺寸比例判断 - aspect_ratio = max_dim / min_dim if min_dim > 0 else 1 - - if aspect_ratio > 3: # 细长物体用胶囊体 - return 'capsule' - elif aspect_ratio < 1.5: # 接近球形用球体 - return 'sphere' - else: # 其他用包围盒 - return 'box' - - def _getTransformedModelInfo(self, model): - """获取考虑变换后的模型信息 - - Args: - model: 模型节点 - - Returns: - dict: 包含变换后的尺寸、中心、半径等信息 - """ - try: - # 获取原始包围盒 - bounds = model.getBounds() - if bounds.isEmpty(): - return None - - # 获取模型的变换矩阵 - transform = model.getTransform() - scale = model.getScale() - - # 计算缩放因子(取三轴缩放的平均值) - scale_factor = (abs(scale.x) + abs(scale.y) + abs(scale.z)) / 3.0 - - # 获取原始尺寸 - original_size = bounds.getMax() - bounds.getMin() - - # 应用缩放到尺寸 - actual_size = Vec3( - original_size.x * abs(scale.x), - original_size.y * abs(scale.y), - original_size.z * abs(scale.z) - ) - - # 获取变换后的中心点(在世界坐标系中) - original_center = bounds.getCenter() - if hasattr(model, 'getPos'): - # 模型在世界坐标系中的位置 - world_center = model.getPos(model.getParent() if model.getParent() else model) - center = Point3(world_center.x, world_center.y, world_center.z) - else: - # 如果无法获取世界位置,使用原始中心 - center = original_center - - # 计算变换后的半径(考虑缩放) - original_radius = bounds.getRadius() - transformed_radius = original_radius * scale_factor - - # 调试信息 - print(f"🔍 模型 {model.getName()} 变换信息:") - print(f" 原始尺寸: {original_size}") - print(f" 缩放因子: {scale}") - print(f" 变换后尺寸: {actual_size}") - print(f" 变换后半径: {transformed_radius:.2f}") - - return { - 'center': center, - 'radius': transformed_radius, - 'size': actual_size, - 'scale_factor': scale_factor, - 'original_size': original_size, - 'scale': scale, - 'transform': transform - } - - except Exception as e: - print(f"⚠️ 获取模型变换信息失败: {e}") - # 回退到原始包围盒 - bounds = model.getBounds() - if bounds.isEmpty(): - return None - - original_size = bounds.getMax() - bounds.getMin() - return { - 'center': bounds.getCenter(), - 'radius': bounds.getRadius(), - 'size': original_size, - 'scale_factor': 1.0, - 'original_size': original_size, - 'scale': Vec3(1, 1, 1), - 'transform': None - } - - def createMouseRay(self, screen_x, screen_y, mask_types=['SELECTABLE']): - """创建鼠标射线""" - # 组合掩码 - combined_mask = BitMask32.allOff() - for mask_type in mask_types: - if mask_type in self.MASKS: - combined_mask |= self.MASKS[mask_type] - - # 坐标转换 - near_point, far_point = self.world.event_handler.robustCoordinateConversion( - screen_x, screen_y) - - if not near_point: - return None - - # 创建射线节点 - ray_node = CollisionNode('mouse_ray') - ray_node.setFromCollideMask(combined_mask) - ray_node.setIntoCollideMask(BitMask32.allOff()) - - # 创建射线 - direction = far_point - near_point - direction.normalize() - ray = CollisionRay(near_point, direction) - ray_node.addSolid(ray) - - return ray_node, near_point, far_point - - def performRaycast(self, ray_node, scene_root=None): - """执行射线检测""" - if scene_root is None: - scene_root = self.world.render - - # 创建临时节点路径 - ray_np = self.world.cam.attachNewNode(ray_node) - - try: - # 清除之前的结果 - self.queue.clearEntries() - - # 添加碰撞器 - self.traverser.addCollider(ray_np, self.queue) - - # 执行遍历 - start_time = time.perf_counter() - self.traverser.traverse(scene_root) - detection_time = time.perf_counter() - start_time - - # 记录性能 - self.performance_monitor.recordDetection( - detection_time, self.queue.getNumEntries()) - - # 处理结果 - results = [] - if self.queue.getNumEntries() > 0: - self.queue.sortEntries() - - for i in range(self.queue.getNumEntries()): - entry = self.queue.getEntry(i) - results.append({ - 'hit_pos': entry.getSurfacePoint(scene_root), - 'hit_normal': entry.getSurfaceNormal(scene_root), - 'hit_node': entry.getIntoNodePath(), - 'distance': entry.getT() - }) - - return results - - finally: - # 清理资源 - self.traverser.removeCollider(ray_np) - ray_np.removeNode() - - def getCollisionMask(self, mask_type): - """获取碰撞掩码""" - return self.MASKS.get(mask_type, BitMask32.allOff()) - - def createCollisionGroup(self, group_name, mask_types, enabled=True): - """创建碰撞分组 - - Args: - group_name: 分组名称 - mask_types: 掩码类型列表 - enabled: 是否启用 - """ - combined_mask = BitMask32.allOff() - for mask_type in mask_types: - if mask_type in self.MASKS: - combined_mask |= self.MASKS[mask_type] - - self.collision_groups[group_name] = { - 'mask': combined_mask, - 'types': mask_types, - 'objects': [] - } - self.group_enabled[group_name] = enabled - - print(f"✅ 创建碰撞分组: {group_name}, 掩码: {mask_types}") - - def addToCollisionGroup(self, group_name, model, collision_node): - """将模型添加到碰撞分组""" - if group_name in self.collision_groups: - self.collision_groups[group_name]['objects'].append({ - 'model': model, - 'collision_node': collision_node - }) - - def enableCollisionGroup(self, group_name, enabled=True): - """启用/禁用碰撞分组""" - if group_name in self.group_enabled: - self.group_enabled[group_name] = enabled - - # 更新分组中所有对象的碰撞状态 - if group_name in self.collision_groups: - for obj in self.collision_groups[group_name]['objects']: - collision_node = obj['collision_node'] - if enabled: - collision_node.show() - else: - collision_node.hide() - - print(f"{'✅ 启用' if enabled else '❌ 禁用'}碰撞分组: {group_name}") - - def registerCollisionCallback(self, mask_a, mask_b, callback_func, filter_func=None): - """注册碰撞回调函数 - - Args: - mask_a: 第一个掩码类型 - mask_b: 第二个掩码类型 - callback_func: 碰撞回调函数 callback(collision_info) - filter_func: 过滤函数 filter(model_a, model_b) -> bool - """ - key = tuple(sorted([mask_a, mask_b])) - if key not in self.collision_callbacks: - self.collision_callbacks[key] = [] - - self.collision_callbacks[key].append(callback_func) - - if filter_func: - if key not in self.collision_filters: - self.collision_filters[key] = [] - self.collision_filters[key].append(filter_func) - - print(f"✅ 注册碰撞回调: {mask_a} <-> {mask_b}") - - def _executeCollisionCallbacks(self, collision_info): - """执行碰撞回调""" - model_a = collision_info['model_a']['node'] - model_b = collision_info['model_b']['node'] - - # 获取模型的掩码类型 - mask_a = self._getModelMaskType(model_a) - mask_b = self._getModelMaskType(model_b) - - if mask_a and mask_b: - key = tuple(sorted([mask_a, mask_b])) - - # 检查过滤条件 - if key in self.collision_filters: - for filter_func in self.collision_filters[key]: - if not filter_func(model_a, model_b): - return - - # 执行回调 - if key in self.collision_callbacks: - for callback_func in self.collision_callbacks[key]: - try: - callback_func(collision_info) - except Exception as e: - print(f"❌ 碰撞回调执行失败: {e}") - - def _getModelMaskType(self, model): - """获取模型的掩码类型""" - # 从模型标签或属性中获取掩码类型 - if model.hasTag('collision_mask_type'): - return model.getTag('collision_mask_type') - return None - - def enableSpatialPartitioning(self, enabled=True, max_depth=6, max_objects=10): - """启用空间分割优化 - - Args: - enabled: 是否启用 - max_depth: 八叉树最大深度 - max_objects: 每个节点最大对象数 - """ - self.spatial_partitioning_enabled = enabled - self.octree_max_depth = max_depth - self.octree_max_objects = max_objects - - if enabled: - self._buildOctree() - print(f"✅ 启用空间分割优化 - 深度: {max_depth}, 对象数: {max_objects}") - else: - self.octree = None - print("❌ 禁用空间分割优化") - - def _buildOctree(self): - """构建八叉树""" - if not hasattr(self.world, 'models') or not self.world.models: - return - - # 计算场景边界 - min_bound = Point3(float('inf'), float('inf'), float('inf')) - max_bound = Point3(float('-inf'), float('-inf'), float('-inf')) - - for model in self.world.models: - bounds = model.getBounds() - if not bounds.isEmpty(): - model_min = bounds.getMin() - model_max = bounds.getMax() - - min_bound.x = min(min_bound.x, model_min.x) - min_bound.y = min(min_bound.y, model_min.y) - min_bound.z = min(min_bound.z, model_min.z) - - max_bound.x = max(max_bound.x, model_max.x) - max_bound.y = max(max_bound.y, model_max.y) - max_bound.z = max(max_bound.z, model_max.z) - - # 创建八叉树根节点 - self.octree = OctreeNode(min_bound, max_bound, 0, self.octree_max_depth, self.octree_max_objects) - - # 将模型插入八叉树 - for model in self.world.models: - self.octree.insert(model) - - def detectModelCollisionsOptimized(self, specific_models=None, log_results=True): - """使用空间分割优化的碰撞检测""" - if not self.spatial_partitioning_enabled or not self.octree: - return self.detectModelCollisions(specific_models, log_results) - - start_time = time.perf_counter() - current_time = datetime.now() - - models_to_check = specific_models if specific_models else self.world.models - if len(models_to_check) < 2: - return [] - - collision_results = [] - current_collision_pairs = set() - - # 使用八叉树进行优化检测 - for model in models_to_check: - # 获取可能碰撞的模型列表 - potential_collisions = self.octree.query(model) - - for other_model in potential_collisions: - if model == other_model: - continue - - pair_key = tuple(sorted([id(model), id(other_model)])) - if pair_key in current_collision_pairs: - continue - - collision_info = self._checkModelPairCollision(model, other_model, current_time) - if collision_info: - collision_results.append(collision_info) - current_collision_pairs.add(pair_key) - - # 执行回调 - self._executeCollisionCallbacks(collision_info) - - # 只有新碰撞才打印日志 - if log_results and pair_key not in self.active_collisions: - self._logCollisionInfo(collision_info) - print(f"🆕 新碰撞检测到!") - - # 更新活跃碰撞状态 - ended_collisions = self.active_collisions - current_collision_pairs - for pair_key in ended_collisions: - print(f"✅ 碰撞结束: 模型对 {pair_key}") - - self.active_collisions = current_collision_pairs - - # 记录性能 - detection_time = time.perf_counter() - start_time - self.performance_monitor.recordModelCollisionDetection( - detection_time, len(models_to_check), len(collision_results)) - - return collision_results - - def cleanup(self): - """清理碰撞管理器资源""" - self._stopCollisionDetectionTask() - self.collision_history.clear() - self.active_collisions.clear() - print("✅ 碰撞管理器已清理") - - def setupAdvancedCollision(self, model, shape_type='auto', mask_type='SELECTABLE', - group_name=None, **shape_kwargs): - """高级碰撞设置方法 - - Args: - model: 模型节点 - shape_type: 碰撞体形状类型 - mask_type: 掩码类型 - group_name: 碰撞分组名称 - **shape_kwargs: 形状特定参数 - - Returns: - collision_node_path: 碰撞节点路径 - """ - # 创建碰撞节点 - cNode = CollisionNode(f'collision_{model.getName()}') - - # 设置掩码 - if mask_type in self.MASKS: - cNode.setIntoCollideMask(self.MASKS[mask_type]) - # 设置模型标签用于回调识别 - model.setTag('collision_mask_type', mask_type) - - # 创建碰撞体 - collision_solid = self.createCollisionShape(model, shape_type, **shape_kwargs) - cNode.addSolid(collision_solid) - - # 附加到模型 - cNodePath = model.attachNewNode(cNode) - - # 添加到分组 - if group_name: - if group_name not in self.collision_groups: - self.createCollisionGroup(group_name, [mask_type]) - self.addToCollisionGroup(group_name, model, cNodePath) - - # 根据调试设置决定是否显示 - if hasattr(self.world, 'debug_collision') and self.world.debug_collision: - cNodePath.show() - else: - cNodePath.hide() - - print(f"✅ 为 {model.getName()} 设置高级碰撞 - 形状: {shape_type}, 掩码: {mask_type}") - return cNodePath - - def example_usage(self): - """碰撞系统使用示例""" - print("\n=== 碰撞系统使用示例 ===") - - # 1. 创建碰撞分组 - self.createCollisionGroup('characters', ['CHARACTER', 'DYNAMIC_OBJECT']) - self.createCollisionGroup('environment', ['STATIC_GEOMETRY', 'TERRAIN']) - self.createCollisionGroup('pickups', ['PICKUP_ITEM', 'INTERACTIVE']) - - # 2. 注册碰撞回调 - def character_pickup_callback(collision_info): - print(f"角色拾取物品: {collision_info['model_a']['name']} -> {collision_info['model_b']['name']}") - - def character_environment_callback(collision_info): - print(f"角色碰撞环境: {collision_info['model_a']['name']} 碰到 {collision_info['model_b']['name']}") - - self.registerCollisionCallback('CHARACTER', 'PICKUP_ITEM', character_pickup_callback) - self.registerCollisionCallback('CHARACTER', 'STATIC_GEOMETRY', character_environment_callback) - - # 3. 启用空间分割优化 - self.enableSpatialPartitioning(enabled=True, max_depth=6, max_objects=8) - - # 4. 为模型设置不同类型的碰撞体 - # model1 = ... # 假设已有模型 - # self.setupAdvancedCollision(model1, 'capsule', 'CHARACTER', 'characters') - # self.setupAdvancedCollision(model2, 'box', 'PICKUP_ITEM', 'pickups') - # self.setupAdvancedCollision(model3, 'plane', 'TERRAIN', 'environment') - - print("✅ 碰撞系统示例配置完成") - -class CollisionPerformanceMonitor: - """碰撞检测性能监控器""" - - def __init__(self): - self.detection_times = [] - self.collision_counts = [] - self.model_collision_times = [] # 新增:模型间碰撞检测时间 - self.model_collision_data = [] # 新增:模型间碰撞数据 - self.max_records = 100 - - def recordDetection(self, detection_time, collision_count): - """记录射线检测性能""" - self.detection_times.append(detection_time) - self.collision_counts.append(collision_count) - - # 限制记录数量 - if len(self.detection_times) > self.max_records: - self.detection_times.pop(0) - self.collision_counts.pop(0) - - def recordModelCollisionDetection(self, detection_time, model_count, collision_count): - """记录模型间碰撞检测性能""" - self.model_collision_times.append(detection_time) - self.model_collision_data.append({ - 'model_count': model_count, - 'collision_count': collision_count, - 'timestamp': time.time() - }) - - # 限制记录数量 - if len(self.model_collision_times) > self.max_records: - self.model_collision_times.pop(0) - self.model_collision_data.pop(0) - - def getAverageTime(self): - """获取平均射线检测时间""" - if not self.detection_times: - return 0 - return sum(self.detection_times) / len(self.detection_times) - - def getAverageModelCollisionTime(self): - """获取平均模型间碰撞检测时间""" - if not self.model_collision_times: - return 0 - return sum(self.model_collision_times) / len(self.model_collision_times) - - def getPerformanceReport(self): - """获取完整性能报告""" - report = [] - - # 射线检测性能 - if self.detection_times: - avg_time = self.getAverageTime() - max_time = max(self.detection_times) - avg_collisions = sum(self.collision_counts) / len(self.collision_counts) - - report.append("=== 射线检测性能 ===") - report.append(f"平均检测时间: {avg_time*1000:.2f}ms") - report.append(f"最大检测时间: {max_time*1000:.2f}ms") - report.append(f"平均碰撞数量: {avg_collisions:.1f}") - report.append(f"检测次数: {len(self.detection_times)}") - - # 模型间碰撞检测性能 - if self.model_collision_times: - avg_model_time = self.getAverageModelCollisionTime() - max_model_time = max(self.model_collision_times) - avg_model_count = sum(d['model_count'] for d in self.model_collision_data) / len(self.model_collision_data) - total_collisions = sum(d['collision_count'] for d in self.model_collision_data) - - report.append("\n=== 模型间碰撞检测性能 ===") - report.append(f"平均检测时间: {avg_model_time*1000:.2f}ms") - report.append(f"最大检测时间: {max_model_time*1000:.2f}ms") - report.append(f"平均模型数量: {avg_model_count:.1f}") - report.append(f"总碰撞次数: {total_collisions}") - report.append(f"检测次数: {len(self.model_collision_times)}") - - return "\n".join(report) if report else "没有性能数据" - -class OctreeNode: - """八叉树节点""" - - def __init__(self, min_bound, max_bound, depth, max_depth, max_objects): - self.min_bound = min_bound - self.max_bound = max_bound - self.depth = depth - self.max_depth = max_depth - self.max_objects = max_objects - - self.objects = [] - self.children = [] - self.is_leaf = True - - def insert(self, model): - """插入模型到八叉树""" - if not self._contains(model): - return False - - if self.is_leaf: - self.objects.append(model) - - # 如果超过最大对象数且未达到最大深度,则分割 - if len(self.objects) > self.max_objects and self.depth < self.max_depth: - self._subdivide() - - # 重新分配对象到子节点 - remaining_objects = [] - for obj in self.objects: - inserted = False - for child in self.children: - if child.insert(obj): - inserted = True - break - if not inserted: - remaining_objects.append(obj) - - self.objects = remaining_objects - else: - # 尝试插入到子节点 - inserted = False - for child in self.children: - if child.insert(model): - inserted = True - break - - if not inserted: - self.objects.append(model) - - return True - - def query(self, model): - """查询可能与指定模型碰撞的模型列表""" - if not self._contains(model): - return [] - - result = list(self.objects) - - if not self.is_leaf: - for child in self.children: - result.extend(child.query(model)) - - return result - - def _contains(self, model): - """检查模型是否在此节点范围内""" - bounds = model.getBounds() - if bounds.isEmpty(): - return False - - model_min = bounds.getMin() - model_max = bounds.getMax() - - return (model_max.x >= self.min_bound.x and model_min.x <= self.max_bound.x and - model_max.y >= self.min_bound.y and model_min.y <= self.max_bound.y and - model_max.z >= self.min_bound.z and model_min.z <= self.max_bound.z) - - def _subdivide(self): - """分割节点为8个子节点""" - center = Point3( - (self.min_bound.x + self.max_bound.x) * 0.5, - (self.min_bound.y + self.max_bound.y) * 0.5, - (self.min_bound.z + self.max_bound.z) * 0.5 - ) - - # 创建8个子节点 - subdivisions = [ - (self.min_bound, center), - (Point3(center.x, self.min_bound.y, self.min_bound.z), Point3(self.max_bound.x, center.y, center.z)), - (Point3(self.min_bound.x, center.y, self.min_bound.z), Point3(center.x, self.max_bound.y, center.z)), - (Point3(center.x, center.y, self.min_bound.z), Point3(self.max_bound.x, self.max_bound.y, center.z)), - (Point3(self.min_bound.x, self.min_bound.y, center.z), Point3(center.x, center.y, self.max_bound.z)), - (Point3(center.x, self.min_bound.y, center.z), Point3(self.max_bound.x, center.y, self.max_bound.z)), - (Point3(self.min_bound.x, center.y, center.z), Point3(center.x, self.max_bound.y, self.max_bound.z)), - (center, self.max_bound) - ] - - for min_b, max_b in subdivisions: - child = OctreeNode(min_b, max_b, self.depth + 1, self.max_depth, self.max_objects) - self.children.append(child) - - self.is_leaf = False +import time +from datetime import datetime +from panda3d.core import ( + CollisionTraverser, CollisionHandlerQueue, CollisionNode, + CollisionRay, CollisionSphere, BitMask32, Point3, Vec3 +) +from direct.task.TaskManagerGlobal import taskMgr + +class CollisionManager: + """统一的碰撞检测管理器""" + + def __init__(self, world): + self.world = world + self.traverser = CollisionTraverser("main_traverser") + self.queue = CollisionHandlerQueue() + + # 碰撞掩码定义 + self.MASKS = { + # === 基础碰撞掩码定义 === + # 每个掩码使用不同的位(bit)来标识,可以进行位运算组合 + + # 'TERRAIN': BitMask32.bit(0), # 地形/地面 - 通常用于地面碰撞检测 + # 'UI_ELEMENT': BitMask32.bit(1), # UI元素 - 界面组件的碰撞检测 + # 'CAMERA': BitMask32.bit(2), # 摄像机 - 相机的碰撞检测 + 'MODEL_COLLISION': BitMask32.bit(6), # 模型碰撞 - 通用模型间碰撞检测 + } + + # 碰撞体形状类型 + self.COLLISION_SHAPES = { + 'SPHERE': 'sphere', + 'BOX': 'box', + 'CAPSULE': 'capsule', + 'PLANE': 'plane', + 'POLYGON': 'polygon', + 'MESH': 'mesh' + } + + # 掩码组合定义 + self.MASK_COMBINATIONS = { + } + + # 碰撞分组管理 + self.collision_groups = {} + self.group_enabled = {} + + # 碰撞回调系统 + self.collision_callbacks = {} + self.collision_filters = {} + + # 空间分割系统(八叉树) + self.spatial_partitioning_enabled = False + self.octree = None + self.octree_max_depth = 6 + self.octree_max_objects = 10 + + # 连续碰撞检测 + self.ccd_enabled = False + self.ccd_threshold = 10.0 # 速度阈值 + + # 性能监控 + self.performance_monitor = CollisionPerformanceMonitor() + + # 模型间碰撞检测配置 + self.model_collision_enabled = False + self.collision_detection_frequency = 0.1 + self.collision_distance_threshold = 0 + self.last_collision_check = 0 + self.collision_history = [] + self.max_collision_history = 100 + + # 碰撞状态跟踪 - 避免重复打印 + self.active_collisions = set() # 当前活跃的碰撞对 + + # 模型间碰撞检测任务 + self.collision_task = None + + print("✅ 碰撞检测管理器初始化完成") + + def enableModelCollisionDetection(self, enable=True, frequency=0.1, threshold=0.5): + """启用/禁用模型间碰撞检测 + + Args: + enable: 是否启用碰撞检测 + frequency: 检测频率(秒) + threshold: 碰撞距离阈值 + """ + self.model_collision_enabled = enable + self.collision_detection_frequency = frequency + self.collision_distance_threshold = threshold + + if enable: + print(f"✅ 启用模型间碰撞检测 - 频率: {frequency}s, 阈值: {threshold}") + self._startCollisionDetectionTask() + else: + print("❌ 禁用模型间碰撞检测") + self._stopCollisionDetectionTask() + + def _startCollisionDetectionTask(self): + """启动碰撞检测任务""" + if self.collision_task: + taskMgr.remove(self.collision_task) + + def collisionDetectionTask(task): + try: + self.detectModelCollisions() + return task.again + except Exception as e: + print(f"❌ 碰撞检测任务异常: {str(e)}") + return task.again + + self.collision_task = taskMgr.doMethodLater( + self.collision_detection_frequency, + collisionDetectionTask, + "modelCollisionDetection" + ) + + def _stopCollisionDetectionTask(self): + """停止碰撞检测任务""" + if self.collision_task: + taskMgr.remove(self.collision_task) + self.collision_task = None + + def detectModelCollisions(self, specific_models=None, log_results=True): + """检测模型间碰撞""" + if not self.model_collision_enabled and specific_models is None: + return [] + + start_time = time.perf_counter() + current_time = datetime.now() + + models_to_check = specific_models if specific_models else self.world.models + if len(models_to_check) < 2: + return [] + + collision_results = [] + checked_pairs = set() + current_collision_pairs = set() + + # 遍历所有模型对 + for i, model_a in enumerate(models_to_check): + for j, model_b in enumerate(models_to_check[i+1:], i+1): + pair_key = tuple(sorted([id(model_a), id(model_b)])) + if pair_key in checked_pairs: + continue + checked_pairs.add(pair_key) + + collision_info = self._checkModelPairCollision(model_a, model_b, current_time) + if collision_info: + collision_results.append(collision_info) + current_collision_pairs.add(pair_key) + + # 只有新碰撞才打印日志 + if log_results and pair_key not in self.active_collisions: + self._logCollisionInfo(collision_info) + print(f"🆕 新碰撞检测到!") + + # 检测碰撞结束的情况 + ended_collisions = self.active_collisions - current_collision_pairs + for pair_key in ended_collisions: + print(f"✅ 碰撞结束: 模型对 {pair_key}") + + # 更新活跃碰撞状态 + self.active_collisions = current_collision_pairs + + # 记录性能和历史 + detection_time = time.perf_counter() - start_time + self.performance_monitor.recordModelCollisionDetection( + detection_time, len(models_to_check), len(collision_results)) + + if collision_results: + self.collision_history.extend(collision_results) + if len(self.collision_history) > self.max_collision_history: + self.collision_history = self.collision_history[-self.max_collision_history:] + + return collision_results + + def _checkModelPairCollision(self, model_a, model_b, timestamp): + """使用Panda3D内置碰撞系统检查两个模型是否碰撞""" + try: + # 检查模型是否有碰撞节点 + collision_node_a = self._findCollisionNode(model_a) + collision_node_b = self._findCollisionNode(model_b) + + if not collision_node_a or not collision_node_b: + return None + + # 使用Panda3D的碰撞遍历器 + temp_traverser = CollisionTraverser() + temp_queue = CollisionHandlerQueue() + + # 设置碰撞检测 + temp_traverser.addCollider(collision_node_a, temp_queue) + + # 执行碰撞检测 + temp_traverser.traverse(model_b) + + if temp_queue.getNumEntries() > 0: + # 有碰撞,获取碰撞信息 + entry = temp_queue.getEntry(0) + + center_a = model_a.getPos(self.world.render) + center_b = model_b.getPos(self.world.render) + distance = (center_b - center_a).length() + + return { + 'timestamp': timestamp, + 'model_a': { + 'name': model_a.getName(), + 'center': center_a, + 'radius': 0, # 不再需要手动计算半径 + 'node': model_a + }, + 'model_b': { + 'name': model_b.getName(), + 'center': center_b, + 'radius': 0, + 'node': model_b + }, + 'collision_point': entry.getSurfacePoint(self.world.render), + 'distance': distance, + 'penetration_depth': 0, # Panda3D会自动处理 + 'collision_normal': entry.getSurfaceNormal(self.world.render) + } + + return None + + except Exception as e: + print(f"❌ 检测模型对碰撞失败 ({model_a.getName()} vs {model_b.getName()}): {str(e)}") + return None + + def _findCollisionNode(self, model): + """查找模型的碰撞节点""" + for child in model.getChildren(): + if isinstance(child.node(), CollisionNode): + return child + return None + + def _logCollisionInfo(self, collision_info): + """输出碰撞信息日志""" + timestamp = collision_info['timestamp'].strftime('%H:%M:%S.%f')[:-3] + model_a = collision_info['model_a'] + model_b = collision_info['model_b'] + + print(f"🔥 [{timestamp}] 检测到碰撞:") + print(f" 模型A: {model_a['name']} (中心: {model_a['center']}, 半径: {model_a['radius']:.2f})") + print(f" 模型B: {model_b['name']} (中心: {model_b['center']}, 半径: {model_b['radius']:.2f})") + print(f" 碰撞点: {collision_info['collision_point']}") + print(f" 中心距离: {collision_info['distance']:.3f}") + print(f" 穿透深度: {collision_info['penetration_depth']:.3f}") + print(f" 碰撞法向: {collision_info['collision_normal']}") + + def getCollisionHistory(self, limit=None): + """获取碰撞历史记录 + + Args: + limit: 返回记录数量限制 + + Returns: + list: 碰撞历史记录 + """ + if limit: + return self.collision_history[-limit:] + return self.collision_history.copy() + + def clearCollisionHistory(self): + """清除碰撞历史记录""" + self.collision_history.clear() + print("✅ 碰撞历史记录已清除") + + def getCollisionStatistics(self): + """获取碰撞统计信息""" + if not self.collision_history: + return {"total_collisions": 0, "unique_pairs": 0, "most_frequent_pair": None} + + # 统计碰撞对 + collision_pairs = {} + for collision in self.collision_history: + pair_key = tuple(sorted([ + collision['model_a']['name'], + collision['model_b']['name'] + ])) + collision_pairs[pair_key] = collision_pairs.get(pair_key, 0) + 1 + + most_frequent_pair = max(collision_pairs.items(), key=lambda x: x[1]) + + return { + "total_collisions": len(self.collision_history), + "unique_pairs": len(collision_pairs), + "most_frequent_pair": { + "models": most_frequent_pair[0], + "count": most_frequent_pair[1] + }, + "collision_pairs": collision_pairs + } + + def createCollisionShape(self, model, shape_type='auto', **kwargs): + """为模型创建指定类型的碰撞体 + + Args: + model: 模型节点 + shape_type: 碰撞体类型 ('auto', 'sphere', 'box', 'capsule', 'plane', 'polygon') + **kwargs: 形状特定参数 + + Returns: + CollisionSolid: 创建的碰撞体 + """ + from panda3d.core import ( + CollisionSphere, CollisionBox, CollisionCapsule, + CollisionPlane, CollisionPolygon, Plane, Vec3 + ) + + # 获取考虑变换后的实际尺寸和中心 + transformed_info = self._getTransformedModelInfo(model) + if not transformed_info: + # 默认小球体 + return CollisionSphere(Point3(0, 0, 0), 1.0) + + center = transformed_info['center'] + radius = transformed_info['radius'] + actual_size = transformed_info['size'] + scale_factor = transformed_info['scale_factor'] + + # 自动选择最适合的形状 + if shape_type == 'auto': + shape_type = self._determineOptimalShape(model, transformed_info) + + if shape_type == 'sphere': + # 优化球形碰撞体 + sphere_radius = kwargs.get('radius', radius) + + # 支持位置偏移 + pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0)) + sphere_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z) + + return CollisionSphere(sphere_center, sphere_radius) + + elif shape_type == 'box': + # 优化盒型碰撞体 - 更精确的尺寸和位置控制(考虑缩放) + # 获取自定义尺寸,如果没有提供则使用变换后的实际尺寸 + width = kwargs.get('width', actual_size.x) + length = kwargs.get('length', actual_size.y) + height = kwargs.get('height', actual_size.z) + + # 支持位置偏移 + pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0)) + box_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z) + + # 计算盒子的最小和最大点(基于偏移后的中心) + half_width = width / 2 + half_length = length / 2 + half_height = height / 2 + + min_point = Point3(box_center.x - half_width, box_center.y - half_length, box_center.z - half_height) + max_point = Point3(box_center.x + half_width, box_center.y + half_length, box_center.z + half_height) + return CollisionBox(min_point, max_point) + + elif shape_type == 'capsule': + # 优化胶囊体碰撞 - 更合理的比例和位置控制(考虑缩放) + # 使用变换后的实际高度,或自定义高度 + custom_height = kwargs.get('height', actual_size.z) + + # 更合理的半径计算:基于变换后模型宽度的平均值 + default_radius = min(actual_size.x, actual_size.y) / 2.5 # 稍微小于模型的宽度 + custom_radius = kwargs.get('radius', min(default_radius, custom_height * 0.4)) + + # 支持位置偏移 + pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0)) + capsule_center = Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z) + + # 计算胶囊体的两个端点(确保半径不会超出高度) + effective_height = max(custom_height, custom_radius * 2.1) # 确保高度至少是半径的2倍多一点 + point_a = Point3(capsule_center.x, capsule_center.y, capsule_center.z - effective_height/2 + custom_radius) + point_b = Point3(capsule_center.x, capsule_center.y, capsule_center.z + effective_height/2 - custom_radius) + return CollisionCapsule(point_a, point_b, custom_radius) + + elif shape_type == 'plane': + # 优化平面碰撞 - 支持位置偏移和更灵活的法向量 + normal = kwargs.get('normal', Vec3(0, 0, 1)) + + # 支持位置偏移 + pos_offset = kwargs.get('position_offset', Vec3(0, 0, 0)) + plane_point = kwargs.get('point', Point3(center.x + pos_offset.x, center.y + pos_offset.y, center.z + pos_offset.z)) + + # 标准化法向量 + normal.normalize() + plane = Plane(normal, plane_point) + return CollisionPlane(plane) + + elif shape_type == 'polygon': + # === 创建多边形碰撞体 === + # CollisionPolygon特点: + # - 单个平面多边形,所有顶点必须共面 + # - 适用于:地板、墙面、简单平台等平面区域 + # - 性能较好,但只能表示平面形状 + # + # Mesh碰撞体特点: + # - 由多个三角形组成的复杂3D形状 + # - 适用于:复杂地形、建筑物、不规则物体 + # - 性能较差,但可以表示任意复杂形状 + + vertices = kwargs.get('vertices', []) + if len(vertices) >= 3: + # 将顶点转换为Point3对象 + # 要求:所有顶点必须在同一平面上 + collision_poly = CollisionPolygon(*[Point3(*v) for v in vertices]) + return collision_poly + else: + #print("⚠️ 多边形至少需要3个顶点,回退到球体") + return CollisionSphere(center, radius) + + def _determineOptimalShape(self, model, transformed_info): + """根据模型特征自动确定最适合的碰撞体形状""" + # 获取变换后的模型尺寸比例 + size = transformed_info['size'] + max_dim = max(size.x, size.y, size.z) + min_dim = min(size.x, size.y, size.z) + + # 根据模型名称和比例判断 + model_name = model.getName().lower() + + # 角色类模型使用胶囊体 + if any(keyword in model_name for keyword in ['character', 'human', 'player', 'npc']): + return 'capsule' + + # 建筑、箱子类使用包围盒 + if any(keyword in model_name for keyword in ['building', 'box', 'cube', 'wall', 'door']): + return 'box' + + # 地面、平台使用平面 + if any(keyword in model_name for keyword in ['ground', 'floor', 'platform', 'terrain']): + return 'plane' + + # 根据尺寸比例判断 + aspect_ratio = max_dim / min_dim if min_dim > 0 else 1 + + if aspect_ratio > 3: # 细长物体用胶囊体 + return 'capsule' + elif aspect_ratio < 1.5: # 接近球形用球体 + return 'sphere' + else: # 其他用包围盒 + return 'box' + + def _getTransformedModelInfo(self, model): + """获取考虑变换后的模型信息 + + Args: + model: 模型节点 + + Returns: + dict: 包含变换后的尺寸、中心、半径等信息 + """ + try: + # 获取原始包围盒 + bounds = model.getBounds() + if bounds.isEmpty(): + return None + + # 获取模型的变换矩阵 + transform = model.getTransform() + scale = model.getScale() + + # 计算缩放因子(取三轴缩放的平均值) + scale_factor = (abs(scale.x) + abs(scale.y) + abs(scale.z)) / 3.0 + + # 获取原始尺寸 + original_size = bounds.getMax() - bounds.getMin() + + # 应用缩放到尺寸 + actual_size = Vec3( + original_size.x * abs(scale.x), + original_size.y * abs(scale.y), + original_size.z * abs(scale.z) + ) + + # 获取变换后的中心点(在世界坐标系中) + original_center = bounds.getCenter() + if hasattr(model, 'getPos'): + # 模型在世界坐标系中的位置 + world_center = model.getPos(model.getParent() if model.getParent() else model) + center = Point3(world_center.x, world_center.y, world_center.z) + else: + # 如果无法获取世界位置,使用原始中心 + center = original_center + + # 计算变换后的半径(考虑缩放) + original_radius = bounds.getRadius() + transformed_radius = original_radius * scale_factor + + # 调试信息 + print(f"🔍 模型 {model.getName()} 变换信息:") + print(f" 原始尺寸: {original_size}") + print(f" 缩放因子: {scale}") + print(f" 变换后尺寸: {actual_size}") + print(f" 变换后半径: {transformed_radius:.2f}") + + return { + 'center': center, + 'radius': transformed_radius, + 'size': actual_size, + 'scale_factor': scale_factor, + 'original_size': original_size, + 'scale': scale, + 'transform': transform + } + + except Exception as e: + print(f"⚠️ 获取模型变换信息失败: {e}") + # 回退到原始包围盒 + bounds = model.getBounds() + if bounds.isEmpty(): + return None + + original_size = bounds.getMax() - bounds.getMin() + return { + 'center': bounds.getCenter(), + 'radius': bounds.getRadius(), + 'size': original_size, + 'scale_factor': 1.0, + 'original_size': original_size, + 'scale': Vec3(1, 1, 1), + 'transform': None + } + + def createMouseRay(self, screen_x, screen_y, mask_types=['SELECTABLE']): + """创建鼠标射线""" + # 组合掩码 + combined_mask = BitMask32.allOff() + for mask_type in mask_types: + if mask_type in self.MASKS: + combined_mask |= self.MASKS[mask_type] + + # 坐标转换 + near_point, far_point = self.world.event_handler.robustCoordinateConversion( + screen_x, screen_y) + + if not near_point: + return None + + # 创建射线节点 + ray_node = CollisionNode('mouse_ray') + ray_node.setFromCollideMask(combined_mask) + ray_node.setIntoCollideMask(BitMask32.allOff()) + + # 创建射线 + direction = far_point - near_point + direction.normalize() + ray = CollisionRay(near_point, direction) + ray_node.addSolid(ray) + + return ray_node, near_point, far_point + + def performRaycast(self, ray_node, scene_root=None): + """执行射线检测""" + if scene_root is None: + scene_root = self.world.render + + # 创建临时节点路径 + ray_np = self.world.cam.attachNewNode(ray_node) + + try: + # 清除之前的结果 + self.queue.clearEntries() + + # 添加碰撞器 + self.traverser.addCollider(ray_np, self.queue) + + # 执行遍历 + start_time = time.perf_counter() + self.traverser.traverse(scene_root) + detection_time = time.perf_counter() - start_time + + # 记录性能 + self.performance_monitor.recordDetection( + detection_time, self.queue.getNumEntries()) + + # 处理结果 + results = [] + if self.queue.getNumEntries() > 0: + self.queue.sortEntries() + + for i in range(self.queue.getNumEntries()): + entry = self.queue.getEntry(i) + results.append({ + 'hit_pos': entry.getSurfacePoint(scene_root), + 'hit_normal': entry.getSurfaceNormal(scene_root), + 'hit_node': entry.getIntoNodePath(), + 'distance': entry.getT() + }) + + return results + + finally: + # 清理资源 + self.traverser.removeCollider(ray_np) + ray_np.removeNode() + + def getCollisionMask(self, mask_type): + """获取碰撞掩码""" + return self.MASKS.get(mask_type, BitMask32.allOff()) + + def createCollisionGroup(self, group_name, mask_types, enabled=True): + """创建碰撞分组 + + Args: + group_name: 分组名称 + mask_types: 掩码类型列表 + enabled: 是否启用 + """ + combined_mask = BitMask32.allOff() + for mask_type in mask_types: + if mask_type in self.MASKS: + combined_mask |= self.MASKS[mask_type] + + self.collision_groups[group_name] = { + 'mask': combined_mask, + 'types': mask_types, + 'objects': [] + } + self.group_enabled[group_name] = enabled + + print(f"✅ 创建碰撞分组: {group_name}, 掩码: {mask_types}") + + def addToCollisionGroup(self, group_name, model, collision_node): + """将模型添加到碰撞分组""" + if group_name in self.collision_groups: + self.collision_groups[group_name]['objects'].append({ + 'model': model, + 'collision_node': collision_node + }) + + def enableCollisionGroup(self, group_name, enabled=True): + """启用/禁用碰撞分组""" + if group_name in self.group_enabled: + self.group_enabled[group_name] = enabled + + # 更新分组中所有对象的碰撞状态 + if group_name in self.collision_groups: + for obj in self.collision_groups[group_name]['objects']: + collision_node = obj['collision_node'] + if enabled: + collision_node.show() + else: + collision_node.hide() + + print(f"{'✅ 启用' if enabled else '❌ 禁用'}碰撞分组: {group_name}") + + def registerCollisionCallback(self, mask_a, mask_b, callback_func, filter_func=None): + """注册碰撞回调函数 + + Args: + mask_a: 第一个掩码类型 + mask_b: 第二个掩码类型 + callback_func: 碰撞回调函数 callback(collision_info) + filter_func: 过滤函数 filter(model_a, model_b) -> bool + """ + key = tuple(sorted([mask_a, mask_b])) + if key not in self.collision_callbacks: + self.collision_callbacks[key] = [] + + self.collision_callbacks[key].append(callback_func) + + if filter_func: + if key not in self.collision_filters: + self.collision_filters[key] = [] + self.collision_filters[key].append(filter_func) + + print(f"✅ 注册碰撞回调: {mask_a} <-> {mask_b}") + + def _executeCollisionCallbacks(self, collision_info): + """执行碰撞回调""" + model_a = collision_info['model_a']['node'] + model_b = collision_info['model_b']['node'] + + # 获取模型的掩码类型 + mask_a = self._getModelMaskType(model_a) + mask_b = self._getModelMaskType(model_b) + + if mask_a and mask_b: + key = tuple(sorted([mask_a, mask_b])) + + # 检查过滤条件 + if key in self.collision_filters: + for filter_func in self.collision_filters[key]: + if not filter_func(model_a, model_b): + return + + # 执行回调 + if key in self.collision_callbacks: + for callback_func in self.collision_callbacks[key]: + try: + callback_func(collision_info) + except Exception as e: + print(f"❌ 碰撞回调执行失败: {e}") + + def _getModelMaskType(self, model): + """获取模型的掩码类型""" + # 从模型标签或属性中获取掩码类型 + if model.hasTag('collision_mask_type'): + return model.getTag('collision_mask_type') + return None + + def enableSpatialPartitioning(self, enabled=True, max_depth=6, max_objects=10): + """启用空间分割优化 + + Args: + enabled: 是否启用 + max_depth: 八叉树最大深度 + max_objects: 每个节点最大对象数 + """ + self.spatial_partitioning_enabled = enabled + self.octree_max_depth = max_depth + self.octree_max_objects = max_objects + + if enabled: + self._buildOctree() + print(f"✅ 启用空间分割优化 - 深度: {max_depth}, 对象数: {max_objects}") + else: + self.octree = None + print("❌ 禁用空间分割优化") + + def _buildOctree(self): + """构建八叉树""" + if not hasattr(self.world, 'models') or not self.world.models: + return + + # 计算场景边界 + min_bound = Point3(float('inf'), float('inf'), float('inf')) + max_bound = Point3(float('-inf'), float('-inf'), float('-inf')) + + for model in self.world.models: + bounds = model.getBounds() + if not bounds.isEmpty(): + model_min = bounds.getMin() + model_max = bounds.getMax() + + min_bound.x = min(min_bound.x, model_min.x) + min_bound.y = min(min_bound.y, model_min.y) + min_bound.z = min(min_bound.z, model_min.z) + + max_bound.x = max(max_bound.x, model_max.x) + max_bound.y = max(max_bound.y, model_max.y) + max_bound.z = max(max_bound.z, model_max.z) + + # 创建八叉树根节点 + self.octree = OctreeNode(min_bound, max_bound, 0, self.octree_max_depth, self.octree_max_objects) + + # 将模型插入八叉树 + for model in self.world.models: + self.octree.insert(model) + + def detectModelCollisionsOptimized(self, specific_models=None, log_results=True): + """使用空间分割优化的碰撞检测""" + if not self.spatial_partitioning_enabled or not self.octree: + return self.detectModelCollisions(specific_models, log_results) + + start_time = time.perf_counter() + current_time = datetime.now() + + models_to_check = specific_models if specific_models else self.world.models + if len(models_to_check) < 2: + return [] + + collision_results = [] + current_collision_pairs = set() + + # 使用八叉树进行优化检测 + for model in models_to_check: + # 获取可能碰撞的模型列表 + potential_collisions = self.octree.query(model) + + for other_model in potential_collisions: + if model == other_model: + continue + + pair_key = tuple(sorted([id(model), id(other_model)])) + if pair_key in current_collision_pairs: + continue + + collision_info = self._checkModelPairCollision(model, other_model, current_time) + if collision_info: + collision_results.append(collision_info) + current_collision_pairs.add(pair_key) + + # 执行回调 + self._executeCollisionCallbacks(collision_info) + + # 只有新碰撞才打印日志 + if log_results and pair_key not in self.active_collisions: + self._logCollisionInfo(collision_info) + print(f"🆕 新碰撞检测到!") + + # 更新活跃碰撞状态 + ended_collisions = self.active_collisions - current_collision_pairs + for pair_key in ended_collisions: + print(f"✅ 碰撞结束: 模型对 {pair_key}") + + self.active_collisions = current_collision_pairs + + # 记录性能 + detection_time = time.perf_counter() - start_time + self.performance_monitor.recordModelCollisionDetection( + detection_time, len(models_to_check), len(collision_results)) + + return collision_results + + def cleanup(self): + """清理碰撞管理器资源""" + self._stopCollisionDetectionTask() + self.collision_history.clear() + self.active_collisions.clear() + print("✅ 碰撞管理器已清理") + + def setupAdvancedCollision(self, model, shape_type='auto', mask_type='SELECTABLE', + group_name=None, **shape_kwargs): + """高级碰撞设置方法 + + Args: + model: 模型节点 + shape_type: 碰撞体形状类型 + mask_type: 掩码类型 + group_name: 碰撞分组名称 + **shape_kwargs: 形状特定参数 + + Returns: + collision_node_path: 碰撞节点路径 + """ + # 创建碰撞节点 + cNode = CollisionNode(f'collision_{model.getName()}') + + # 设置掩码 + if mask_type in self.MASKS: + cNode.setIntoCollideMask(self.MASKS[mask_type]) + # 设置模型标签用于回调识别 + model.setTag('collision_mask_type', mask_type) + + # 创建碰撞体 + collision_solid = self.createCollisionShape(model, shape_type, **shape_kwargs) + cNode.addSolid(collision_solid) + + # 附加到模型 + cNodePath = model.attachNewNode(cNode) + + # 添加到分组 + if group_name: + if group_name not in self.collision_groups: + self.createCollisionGroup(group_name, [mask_type]) + self.addToCollisionGroup(group_name, model, cNodePath) + + # 根据调试设置决定是否显示 + if hasattr(self.world, 'debug_collision') and self.world.debug_collision: + cNodePath.show() + else: + cNodePath.hide() + + print(f"✅ 为 {model.getName()} 设置高级碰撞 - 形状: {shape_type}, 掩码: {mask_type}") + return cNodePath + + def example_usage(self): + """碰撞系统使用示例""" + print("\n=== 碰撞系统使用示例 ===") + + # 1. 创建碰撞分组 + self.createCollisionGroup('characters', ['CHARACTER', 'DYNAMIC_OBJECT']) + self.createCollisionGroup('environment', ['STATIC_GEOMETRY', 'TERRAIN']) + self.createCollisionGroup('pickups', ['PICKUP_ITEM', 'INTERACTIVE']) + + # 2. 注册碰撞回调 + def character_pickup_callback(collision_info): + print(f"角色拾取物品: {collision_info['model_a']['name']} -> {collision_info['model_b']['name']}") + + def character_environment_callback(collision_info): + print(f"角色碰撞环境: {collision_info['model_a']['name']} 碰到 {collision_info['model_b']['name']}") + + self.registerCollisionCallback('CHARACTER', 'PICKUP_ITEM', character_pickup_callback) + self.registerCollisionCallback('CHARACTER', 'STATIC_GEOMETRY', character_environment_callback) + + # 3. 启用空间分割优化 + self.enableSpatialPartitioning(enabled=True, max_depth=6, max_objects=8) + + # 4. 为模型设置不同类型的碰撞体 + # model1 = ... # 假设已有模型 + # self.setupAdvancedCollision(model1, 'capsule', 'CHARACTER', 'characters') + # self.setupAdvancedCollision(model2, 'box', 'PICKUP_ITEM', 'pickups') + # self.setupAdvancedCollision(model3, 'plane', 'TERRAIN', 'environment') + + print("✅ 碰撞系统示例配置完成") + +class CollisionPerformanceMonitor: + """碰撞检测性能监控器""" + + def __init__(self): + self.detection_times = [] + self.collision_counts = [] + self.model_collision_times = [] # 新增:模型间碰撞检测时间 + self.model_collision_data = [] # 新增:模型间碰撞数据 + self.max_records = 100 + + def recordDetection(self, detection_time, collision_count): + """记录射线检测性能""" + self.detection_times.append(detection_time) + self.collision_counts.append(collision_count) + + # 限制记录数量 + if len(self.detection_times) > self.max_records: + self.detection_times.pop(0) + self.collision_counts.pop(0) + + def recordModelCollisionDetection(self, detection_time, model_count, collision_count): + """记录模型间碰撞检测性能""" + self.model_collision_times.append(detection_time) + self.model_collision_data.append({ + 'model_count': model_count, + 'collision_count': collision_count, + 'timestamp': time.time() + }) + + # 限制记录数量 + if len(self.model_collision_times) > self.max_records: + self.model_collision_times.pop(0) + self.model_collision_data.pop(0) + + def getAverageTime(self): + """获取平均射线检测时间""" + if not self.detection_times: + return 0 + return sum(self.detection_times) / len(self.detection_times) + + def getAverageModelCollisionTime(self): + """获取平均模型间碰撞检测时间""" + if not self.model_collision_times: + return 0 + return sum(self.model_collision_times) / len(self.model_collision_times) + + def getPerformanceReport(self): + """获取完整性能报告""" + report = [] + + # 射线检测性能 + if self.detection_times: + avg_time = self.getAverageTime() + max_time = max(self.detection_times) + avg_collisions = sum(self.collision_counts) / len(self.collision_counts) + + report.append("=== 射线检测性能 ===") + report.append(f"平均检测时间: {avg_time*1000:.2f}ms") + report.append(f"最大检测时间: {max_time*1000:.2f}ms") + report.append(f"平均碰撞数量: {avg_collisions:.1f}") + report.append(f"检测次数: {len(self.detection_times)}") + + # 模型间碰撞检测性能 + if self.model_collision_times: + avg_model_time = self.getAverageModelCollisionTime() + max_model_time = max(self.model_collision_times) + avg_model_count = sum(d['model_count'] for d in self.model_collision_data) / len(self.model_collision_data) + total_collisions = sum(d['collision_count'] for d in self.model_collision_data) + + report.append("\n=== 模型间碰撞检测性能 ===") + report.append(f"平均检测时间: {avg_model_time*1000:.2f}ms") + report.append(f"最大检测时间: {max_model_time*1000:.2f}ms") + report.append(f"平均模型数量: {avg_model_count:.1f}") + report.append(f"总碰撞次数: {total_collisions}") + report.append(f"检测次数: {len(self.model_collision_times)}") + + return "\n".join(report) if report else "没有性能数据" + +class OctreeNode: + """八叉树节点""" + + def __init__(self, min_bound, max_bound, depth, max_depth, max_objects): + self.min_bound = min_bound + self.max_bound = max_bound + self.depth = depth + self.max_depth = max_depth + self.max_objects = max_objects + + self.objects = [] + self.children = [] + self.is_leaf = True + + def insert(self, model): + """插入模型到八叉树""" + if not self._contains(model): + return False + + if self.is_leaf: + self.objects.append(model) + + # 如果超过最大对象数且未达到最大深度,则分割 + if len(self.objects) > self.max_objects and self.depth < self.max_depth: + self._subdivide() + + # 重新分配对象到子节点 + remaining_objects = [] + for obj in self.objects: + inserted = False + for child in self.children: + if child.insert(obj): + inserted = True + break + if not inserted: + remaining_objects.append(obj) + + self.objects = remaining_objects + else: + # 尝试插入到子节点 + inserted = False + for child in self.children: + if child.insert(model): + inserted = True + break + + if not inserted: + self.objects.append(model) + + return True + + def query(self, model): + """查询可能与指定模型碰撞的模型列表""" + if not self._contains(model): + return [] + + result = list(self.objects) + + if not self.is_leaf: + for child in self.children: + result.extend(child.query(model)) + + return result + + def _contains(self, model): + """检查模型是否在此节点范围内""" + bounds = model.getBounds() + if bounds.isEmpty(): + return False + + model_min = bounds.getMin() + model_max = bounds.getMax() + + return (model_max.x >= self.min_bound.x and model_min.x <= self.max_bound.x and + model_max.y >= self.min_bound.y and model_min.y <= self.max_bound.y and + model_max.z >= self.min_bound.z and model_min.z <= self.max_bound.z) + + def _subdivide(self): + """分割节点为8个子节点""" + center = Point3( + (self.min_bound.x + self.max_bound.x) * 0.5, + (self.min_bound.y + self.max_bound.y) * 0.5, + (self.min_bound.z + self.max_bound.z) * 0.5 + ) + + # 创建8个子节点 + subdivisions = [ + (self.min_bound, center), + (Point3(center.x, self.min_bound.y, self.min_bound.z), Point3(self.max_bound.x, center.y, center.z)), + (Point3(self.min_bound.x, center.y, self.min_bound.z), Point3(center.x, self.max_bound.y, center.z)), + (Point3(center.x, center.y, self.min_bound.z), Point3(self.max_bound.x, self.max_bound.y, center.z)), + (Point3(self.min_bound.x, self.min_bound.y, center.z), Point3(center.x, center.y, self.max_bound.z)), + (Point3(center.x, self.min_bound.y, center.z), Point3(self.max_bound.x, center.y, self.max_bound.z)), + (Point3(self.min_bound.x, center.y, center.z), Point3(center.x, self.max_bound.y, self.max_bound.z)), + (center, self.max_bound) + ] + + for min_b, max_b in subdivisions: + child = OctreeNode(min_b, max_b, self.depth + 1, self.max_depth, self.max_objects) + self.children.append(child) + + self.is_leaf = False diff --git a/core/event_handler.py b/core/event_handler.py index 89cb2ad9..e9f2a40d 100644 --- a/core/event_handler.py +++ b/core/event_handler.py @@ -1,584 +1,584 @@ -from panda3d.core import (Point3, Point2, CollisionTraverser, CollisionHandlerQueue, - CollisionNode, CollisionRay, GeomNode, LineSegs, RenderState, - DepthTestAttrib, ColorAttrib) - - -class EventHandler: - """事件处理器 - 处理鼠标和键盘输入事件""" - - def __init__(self, world): - """初始化事件处理器""" - self.world = world - - # 射线显示相关 - self.showRay = False # 是否显示射线(默认关闭) - self.rayNode = None # 当前显示的射线节点 - self.rayLifetime = 2.0 # 射线显示时间(秒) - - def showClickRay(self, nearPoint, farPoint, hitPos=None): - """显示鼠标点击的射线""" - if not self.showRay: - return - - try: - # 清除之前的射线 - self.clearRay() - - # 创建射线几何体 - lines = LineSegs() - lines.setThickness(3.0) - - # 设置射线颜色 - if hitPos: - # 有碰撞:射线分两段,起点到碰撞点为绿色,碰撞点到终点为红色 - lines.setColor(0, 1, 0, 1) # 绿色 - lines.moveTo(nearPoint) - lines.drawTo(hitPos) - - lines.setColor(1, 0, 0, 1) # 红色 - lines.moveTo(hitPos) - lines.drawTo(farPoint) - - # 在碰撞点添加一个小球 - lines.setColor(1, 1, 0, 1) # 黄色 - self._addHitMarker(lines, hitPos) - else: - # 无碰撞:整条射线为蓝色 - lines.setColor(0, 0, 1, 1) # 蓝色 - lines.moveTo(nearPoint) - lines.drawTo(farPoint) - - # 创建射线节点 - geomNode = lines.create() - self.rayNode = self.world.render.attachNewNode(geomNode) - self.rayNode.setName("clickRay") - - # 设置渲染状态,确保射线总是可见 - state = RenderState.make( - DepthTestAttrib.make(DepthTestAttrib.MAlways), # 总是通过深度测试 - ColorAttrib.makeFlat((1.0, 1.0, 1.0, 1.0)) - ) - self.rayNode.setState(state) - self.rayNode.setLightOff() # 不受光照影响 - - # 设置自动清除任务(先清除可能存在的旧任务) - from direct.task.TaskManagerGlobal import taskMgr - taskMgr.remove("clearRay") # 清除可能存在的旧任务 - taskMgr.doMethodLater(self.rayLifetime, self.clearRayTask, "clearRay") - - print(f"✓ 射线已显示,{self.rayLifetime}秒后自动清除") - - except Exception as e: - print(f"显示射线失败: {str(e)}") - - def _addHitMarker(self, lines, hitPos): - """在碰撞点添加标记""" - # 创建一个小十字标记 - marker_size = 0.5 - - # X方向线 - lines.moveTo(hitPos.x - marker_size, hitPos.y, hitPos.z) - lines.drawTo(hitPos.x + marker_size, hitPos.y, hitPos.z) - - # Y方向线 - lines.moveTo(hitPos.x, hitPos.y - marker_size, hitPos.z) - lines.drawTo(hitPos.x, hitPos.y + marker_size, hitPos.z) - - # Z方向线 - lines.moveTo(hitPos.x, hitPos.y, hitPos.z - marker_size) - lines.drawTo(hitPos.x, hitPos.y, hitPos.z + marker_size) - - def clearRay(self): - """清除当前显示的射线""" - if self.rayNode: - self.rayNode.removeNode() - self.rayNode = None - - # 同时清除可能存在的任务 - from direct.task.TaskManagerGlobal import taskMgr - taskMgr.remove("clearRay") - - def clearRayTask(self, task): - """清除射线的任务回调""" - self.clearRay() - return task.done - - def toggleRayDisplay(self): - """切换射线显示状态""" - self.showRay = not self.showRay - if not self.showRay: - self.clearRay() - print(f"射线显示: {'开启' if self.showRay else '关闭'}") - return self.showRay - - def mousePressEventLeft(self, evt): - """处理鼠标左键按下事件""" - print("\n=== 开始处理鼠标左键事件 ===") - #print(f"当前工具: {self.world.currentTool}") - - if not evt: - print("事件为空") - return - if self.world.currentTool == "地形编辑": - self._handleTerrainEdit(evt,"add") - return - - # 获取鼠标点击的位置 - x = evt.get('x', 0) - y = evt.get('y', 0) - #print(f"鼠标点击位置: ({x}, {y})") - - # 获取准确的窗口尺寸 - winWidth, winHeight = self.world.getWindowSize() - - # 直接使用 x, y 创建鼠标位置 - mx = 2.0 * x / float(winWidth) - 1.0 - my = 1.0 - 2.0 * y / float(winHeight) - #print(f"转换后的坐标: ({mx}, {my})") - - # 创建射线 - nearPoint = Point3() - farPoint = Point3() - self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint) - #print(f"相机坐标系射线起点: {nearPoint}") - #print(f"相机坐标系射线终点: {farPoint}") - - # 将相机坐标系的点转换到世界坐标系 - worldNearPoint = self.world.render.getRelativePoint(self.world.cam, nearPoint) - worldFarPoint = self.world.render.getRelativePoint(self.world.cam, farPoint) - #print(f"世界坐标系射线起点: {worldNearPoint}") - #print(f"世界坐标系射线终点: {worldFarPoint}") - - # 进行射线检测 - picker = CollisionTraverser() - queue = CollisionHandlerQueue() - - pickerNode = CollisionNode('mouseRay') - pickerNP = self.world.cam.attachNewNode(pickerNode) - # 设置射线的碰撞掩码,匹配模型的碰撞掩码(第2位) - from panda3d.core import BitMask32 - pickerNode.setFromCollideMask(BitMask32.bit(2)) - - # 使用相机坐标系的点创建射线(因为pickerNP是相机的子节点) - direction = farPoint - nearPoint - direction.normalize() - pickerNode.addSolid(CollisionRay(nearPoint, direction)) - - picker.addCollider(pickerNP, queue) - picker.traverse(self.world.render) - - #print(f"碰撞检测结果数量: {queue.getNumEntries()}") - - # 射线检测结果处理 - hitPos = None - hitNode = None - - if queue.getNumEntries() > 0: - # 获取最近的碰撞点 - entry = queue.getEntry(0) - hitPos = entry.getSurfacePoint(self.world.render) - hitNode = entry.getIntoNodePath() - print(f"碰撞到节点: {hitNode.getName()}") - - # 显示射线(使用世界坐标系的点) - self.showClickRay(worldNearPoint, worldFarPoint, hitPos) - - # 优先检查是否点击了坐标轴 - #print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}") - if self.world.selection.gizmo: - #print("准备检查坐标轴点击...") - try: - highlighted_axis = self.world.selection.gizmoHighlightAxis - if highlighted_axis: - print(f"✓ 检测到高亮轴: {highlighted_axis},直接开始拖拽") - # 直接使用高亮轴开始拖拽 - self.world.selection.startGizmoDrag(highlighted_axis, x, y) - pickerNP.removeNode() - return - - # 如果没有高亮轴,再尝试检测点击 - gizmoAxis = self.world.selection.checkGizmoClick(x, y) - if gizmoAxis: - print(f"✓ 检测到坐标轴点击: {gizmoAxis}") - # 开始坐标轴拖拽 - self.world.selection.startGizmoDrag(gizmoAxis, x, y) - pickerNP.removeNode() - return - else: - print("× 没有点击到坐标轴") - - # gizmoAxis = self.world.selection.checkGizmoClick(x, y) - # if gizmoAxis: - # #print(f"✓ 检测到坐标轴点击: {gizmoAxis}") - # # 开始坐标轴拖拽 - # self.world.selection.startGizmoDrag(gizmoAxis, x, y) - # pickerNP.removeNode() - # return - # else: - # print("× 没有点击到坐标轴") - except Exception as e: - print(f"❌ 坐标轴点击检测出现异常: {str(e)}") - import traceback - traceback.print_exc() - print("继续处理模型选择...") - - #print("继续处理碰撞结果...") - - if hitPos and hitNode: - #print(f"✓ 检测到碰撞,开始处理点击事件") - #print(f"GUI编辑模式: {self.world.guiEditMode}") - #print(f"当前工具: {self.world.currentTool}") - - # 处理GUI编辑模式 - if self.world.guiEditMode: - #print("处理GUI编辑模式点击") - # 检查是否点击了GUI元素 - clickedGUI = self.world.gui_manager.findClickedGUI(hitNode) - if clickedGUI: - # 选中GUI元素 - self.world.selection.updateSelection(clickedGUI) - self.world.gui_manager.selectGUIInTree(clickedGUI) - print(f"选中GUI元素: {clickedGUI.getTag('gui_text')}") - elif hasattr(self.world, 'currentGUITool') and self.world.currentGUITool: - # 在点击位置创建新GUI元素 - self.world.gui_manager.createGUIAtPosition(hitPos, self.world.currentGUITool) - pickerNP.removeNode() - return - - # 根据当前工具处理点击事件 - if self.world.currentTool == "选择": - print("✓ 使用选择工具处理点击") - try: - self._handleSelectionClick(hitNode) - print("✓ 选择处理完成") - except Exception as e: - print(f"❌ 选择处理出现异常: {str(e)}") - import traceback - traceback.print_exc() - else: - print(f"当前工具不是'选择',无法处理: {self.world.currentTool}") - else: - print("没有检测到碰撞") - - # 如果不在GUI编辑模式,清除选择 - if not self.world.guiEditMode: - self.world.selection.updateSelection(None) - - # 在GUI编辑模式下,即使没有碰撞也可以在空白区域创建GUI - if (self.world.guiEditMode and - hasattr(self.world, 'currentGUITool') and - self.world.currentGUITool): - # 使用默认的地面高度创建GUI - default_height = 0.0 - world_pos = Point3(mx * 10, 0, my * 10) # 简单的屏幕到世界坐标转换 - world_pos.setZ(default_height) - self.world.gui_manager.createGUIAtPosition(world_pos, self.world.currentGUITool) - - # 确保总是清理碰撞检测节点 - try: - pickerNP.removeNode() - print("✓ 碰撞检测节点已清理") - except Exception as e: - print(f"清理碰撞检测节点失败: {str(e)}") - - print("=== 鼠标左键事件处理结束 ===\n") - - def mousePressEventRight(self,evt): - """处理鼠标右键按下事件""" - print(f"当前工具: {self.world.currentTool}") - - # 检查是否是地形编辑模式 - if self.world.currentTool == "地形编辑": - self._handleTerrainEdit(evt, "subtract") # 降低地形 - return - - # 其他右键处理逻辑可以在这里添加 - print("鼠标右键事件处理") - - def _handleTerrainEdit(self, evt, operation): - """处理地形编辑""" - try: - x = evt.get('x', 0) - y = evt.get('y', 0) - - winWidth, winHeight = self.world.getWindowSize() - - mx = 2.0 * x / float(winWidth) - 1.0 - my = 1.0 - 2.0 * y / float(winHeight) - - nearPoint = Point3() - farPoint = Point3() - self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint) - - worldNearPoint = self.world.render.getRelativePoint(self.world.cam, nearPoint) - worldFarPoint = self.world.render.getRelativePoint(self.world.cam, farPoint) - - picker = CollisionTraverser() - queue = CollisionHandlerQueue() - - pickerNode = CollisionNode('terrain_edit_ray') - pickerNP = self.world.cam.attachNewNode(pickerNode) - - from panda3d.core import BitMask32 - # 确保我们检测所有碰撞体 - pickerNode.setFromCollideMask(BitMask32.bit(2)) - - # 使用相机坐标系的点创建射线 - direction = farPoint - nearPoint - direction.normalize() - pickerNode.addSolid(CollisionRay(nearPoint, direction)) - - picker.addCollider(pickerNP, queue) - picker.traverse(self.world.render) - print(f"地形碰撞检测结果数量: {queue.getNumEntries()}") - - # 添加调试信息,显示所有场景中的碰撞体 - print("场景中的碰撞体:") - - def show_colliders(node, level=0): - indent = " " * level - if not node.isEmpty(): - if isinstance(node.node(), CollisionNode): - print(f"{indent}CollisionNode: {node.getName()}") - for child in node.getChildren(): - show_colliders(child, level + 1) - - show_colliders(self.world.render) - - # 射线检测结果处理 - hitPos = None - hitNode = None - hitTerrainInfo = None - - if queue.getNumEntries() > 0: - queue.sortEntries() - # 遍历所有碰撞结果,找到地形节点 - for i in range(queue.getNumEntries()): - entry = queue.getEntry(i) - collided_node = entry.getIntoNodePath() - print(f"碰撞到节点: {collided_node.getName()}") - - # 检查是否是地形节点 - for terrain_info in self.world.terrain_manager.terrains: - terrain_node = terrain_info['node'] - if collided_node == terrain_node or terrain_node.isAncestorOf(collided_node): - hitPos = entry.getSurfacePoint(self.world.render) - hitNode = collided_node - hitTerrainInfo = terrain_info - print(f"找到地形节点: {terrain_node.getName()}") - break - - if hitPos: - break - - if hitPos and hitTerrainInfo: - # 修改地形高度 - x_pos, y_pos = hitPos.getX(), hitPos.getY() - - # 使用 getattr 获取地形编辑参数 - radius = getattr(self.world, 'terrain_edit_radius', 3.0) - strength = getattr(self.world, 'terrain_edit_strength', 0.3) - - print(f"准备编辑地形: 位置({x_pos:.2f}, {y_pos:.2f}), 半径{radius}, 强度{strength}, 操作{operation}") - - # 添加更多调试信息 - print(f"地形信息: {hitTerrainInfo}") - if 'heightmap' in hitTerrainInfo: - print(f"高度图路径: {hitTerrainInfo['heightmap']}") - - try: - # 检查 terrain_manager 是否有 modifyTerrainHeight 方法 - if not hasattr(self.world.terrain_manager, 'modifyTerrainHeight'): - print("✗ 错误: terrain_manager 没有 modifyTerrainHeight 方法") - self.showClickRay(worldNearPoint, worldFarPoint, hitPos) - return - - # 调用地形编辑方法 - success = self.world.terrain_manager.modifyTerrainHeight( - hitTerrainInfo, x_pos, y_pos, radius=radius, strength=strength, operation=operation) - - if success: - print(f"✓ 地形编辑成功: {operation} at ({x_pos:.2f}, {y_pos:.2f})") - # 显示射线 - self.showClickRay(worldNearPoint, worldFarPoint, hitPos) - - # 强制刷新地形显示 - if 'terrain' in hitTerrainInfo: - hitTerrainInfo['terrain'].generate() - print("✓ 地形已刷新") - else: - print("✗ 地形编辑失败") - # 即使编辑失败也显示射线 - self.showClickRay(worldNearPoint, worldFarPoint, hitPos) - except Exception as e: - print(f"✗ 地形编辑过程中出现异常: {e}") - import traceback - traceback.print_exc() - # 即使编辑失败也显示射线 - self.showClickRay(worldNearPoint, worldFarPoint, hitPos) - else: - print("没有检测到地形碰撞") - # 显示射线(无碰撞) - self.showClickRay(worldNearPoint, worldFarPoint) - - # 清理碰撞检测节点 - pickerNP.removeNode() - print("地形编辑处理完成") - - except Exception as e: - print(f"地形编辑处理出错: {e}") - import traceback - traceback.print_exc() - - def _handleSelectionClick(self, hitNode): - """处理选择工具的点击事件""" - print(f"开始处理选择点击,碰撞节点: {hitNode.getName()}") - - # 查找对应的实际模型节点 - selectedModel = None - - # 如果点击的是碰撞节点,找到它的父模型 - if isinstance(hitNode.node(), CollisionNode): - print(f"点击的是碰撞节点: {hitNode.getName()}") - # 碰撞节点的父节点应该是模型 - parent = hitNode.getParent() - if parent in self.world.models: - selectedModel = parent - print(f"找到对应的模型: {selectedModel.getName()}") - else: - print(f"碰撞节点的父节点不是模型: {parent.getName()}") - else: - # 查找可选择的节点(模型或其子节点) - current = hitNode - while current != self.world.render: - # 检查是否是模型 - if current in self.world.models: - selectedModel = current - print(f"找到模型节点: {selectedModel.getName()}") - break - - # 检查是否是模型的子节点 - for model in self.world.models: - if current.getParent() == model or current.isAncestorOf(model): - selectedModel = model - print(f"找到父模型: {selectedModel.getName()}") - break - - if selectedModel: - break - - current = current.getParent() - - if selectedModel: - #print(f"✓ 最终选中模型: {selectedModel.getName()}") - self.world.selection.handleMouseClick(selectedModel) - - # 更新选择状态并显示选择框和坐标轴 - self.world.selection.updateSelection(selectedModel) - - # 在树形控件中查找并选中对应的项 - if self.world.interface_manager.treeWidget: - #print("查找树形控件中的对应项...") - root = self.world.interface_manager.treeWidget.invisibleRootItem() - foundItem = None - - for i in range(root.childCount()): - sceneItem = root.child(i) - if sceneItem.text(0) == "场景": - #print(f"在场景节点下查找...") - foundItem = self.world.interface_manager.findTreeItem(selectedModel, sceneItem) - if foundItem: - print(f"✓ 在树形控件中找到对应项: {foundItem.text(0)}") - try: - self.world.interface_manager.treeWidget.itemClicked.disconnect() - except TypeError: - pass - - self.world.interface_manager.treeWidget.setCurrentItem(foundItem) - - self.world.interface_manager.treeWidget.itemClicked.connect( - self.world.interface_manager.onTreeItemClicked) - else: - print("× 在树形控件中没有找到对应项") - break - - if not foundItem: - print("× 没有找到场景节点或对应的树形项") - else: - print("× 树形控件不存在") - else: - print("× 没有找到可选择的模型节点") - self.world.selection.updateSelection(None) - - def mouseReleaseEventLeft(self, evt): - """处理鼠标左键释放事件""" - # 处理坐标轴拖拽结束 - if self.world.selection.isDraggingGizmo: - self.world.selection.stopGizmoDrag() - return - - def wheelForward(self, data=None): - """处理滚轮向前滚动(前进)""" - # 调用CoreWorld的父类方法 - super(type(self.world), self.world).wheelForward(data) - - # 更新属性面板 - if (self.world.interface_manager.treeWidget and - self.world.interface_manager.treeWidget.currentItem() and - self.world.interface_manager.treeWidget.currentItem().text(0) == "相机"): - self.world.property_panel.updatePropertyPanel( - self.world.interface_manager.treeWidget.currentItem()) - - def wheelBackward(self, data=None): - """处理滚轮向后滚动(后退)""" - # 调用CoreWorld的父类方法 - super(type(self.world), self.world).wheelBackward(data) - - # 更新属性面板 - if (self.world.interface_manager.treeWidget and - self.world.interface_manager.treeWidget.currentItem() and - self.world.interface_manager.treeWidget.currentItem().text(0) == "相机"): - self.world.property_panel.updatePropertyPanel( - self.world.interface_manager.treeWidget.currentItem()) - - def mousePressEventMiddle(self, evt): - """处理鼠标中键按下事件""" - # 已移除原有的Z轴拖拽功能 - pass - - def mouseReleaseEventMiddle(self, evt): - """处理鼠标中键释放事件""" - # 已移除原有的Z轴拖拽功能 - pass - - def mouseMoveEvent(self, evt): - """处理鼠标移动事件""" - if not evt: - return - - # 处理坐标轴拖拽 - if self.world.selection.isDraggingGizmo: - x = evt.get('x', 0) - y = evt.get('y', 0) - - # 获取准确的窗口尺寸 - winWidth, winHeight = self.world.getWindowSize() - - # 将屏幕坐标转换为世界坐标 - mx = 2.0 * x / float(winWidth) - 1.0 - my = 1.0 - 2.0 * y / float(winHeight) - - # 更新坐标轴拖拽 - self.world.selection.updateGizmoDrag(x, y) - return - - # 更新坐标轴高亮(鼠标悬停效果) - if self.world.selection.gizmo and not self.world.selection.isDraggingGizmo: - x = evt.get('x', 0) - y = evt.get('y', 0) - # 减少高亮调试输出,只在需要时输出 - # 已静默处理,避免控制台刷屏 - self.world.selection.updateGizmoHighlight(x, y) - - # 调用CoreWorld的父类方法处理基础的相机旋转 +from panda3d.core import (Point3, Point2, CollisionTraverser, CollisionHandlerQueue, + CollisionNode, CollisionRay, GeomNode, LineSegs, RenderState, + DepthTestAttrib, ColorAttrib) + + +class EventHandler: + """事件处理器 - 处理鼠标和键盘输入事件""" + + def __init__(self, world): + """初始化事件处理器""" + self.world = world + + # 射线显示相关 + self.showRay = False # 是否显示射线(默认关闭) + self.rayNode = None # 当前显示的射线节点 + self.rayLifetime = 2.0 # 射线显示时间(秒) + + def showClickRay(self, nearPoint, farPoint, hitPos=None): + """显示鼠标点击的射线""" + if not self.showRay: + return + + try: + # 清除之前的射线 + self.clearRay() + + # 创建射线几何体 + lines = LineSegs() + lines.setThickness(3.0) + + # 设置射线颜色 + if hitPos: + # 有碰撞:射线分两段,起点到碰撞点为绿色,碰撞点到终点为红色 + lines.setColor(0, 1, 0, 1) # 绿色 + lines.moveTo(nearPoint) + lines.drawTo(hitPos) + + lines.setColor(1, 0, 0, 1) # 红色 + lines.moveTo(hitPos) + lines.drawTo(farPoint) + + # 在碰撞点添加一个小球 + lines.setColor(1, 1, 0, 1) # 黄色 + self._addHitMarker(lines, hitPos) + else: + # 无碰撞:整条射线为蓝色 + lines.setColor(0, 0, 1, 1) # 蓝色 + lines.moveTo(nearPoint) + lines.drawTo(farPoint) + + # 创建射线节点 + geomNode = lines.create() + self.rayNode = self.world.render.attachNewNode(geomNode) + self.rayNode.setName("clickRay") + + # 设置渲染状态,确保射线总是可见 + state = RenderState.make( + DepthTestAttrib.make(DepthTestAttrib.MAlways), # 总是通过深度测试 + ColorAttrib.makeFlat((1.0, 1.0, 1.0, 1.0)) + ) + self.rayNode.setState(state) + self.rayNode.setLightOff() # 不受光照影响 + + # 设置自动清除任务(先清除可能存在的旧任务) + from direct.task.TaskManagerGlobal import taskMgr + taskMgr.remove("clearRay") # 清除可能存在的旧任务 + taskMgr.doMethodLater(self.rayLifetime, self.clearRayTask, "clearRay") + + print(f"✓ 射线已显示,{self.rayLifetime}秒后自动清除") + + except Exception as e: + print(f"显示射线失败: {str(e)}") + + def _addHitMarker(self, lines, hitPos): + """在碰撞点添加标记""" + # 创建一个小十字标记 + marker_size = 0.5 + + # X方向线 + lines.moveTo(hitPos.x - marker_size, hitPos.y, hitPos.z) + lines.drawTo(hitPos.x + marker_size, hitPos.y, hitPos.z) + + # Y方向线 + lines.moveTo(hitPos.x, hitPos.y - marker_size, hitPos.z) + lines.drawTo(hitPos.x, hitPos.y + marker_size, hitPos.z) + + # Z方向线 + lines.moveTo(hitPos.x, hitPos.y, hitPos.z - marker_size) + lines.drawTo(hitPos.x, hitPos.y, hitPos.z + marker_size) + + def clearRay(self): + """清除当前显示的射线""" + if self.rayNode: + self.rayNode.removeNode() + self.rayNode = None + + # 同时清除可能存在的任务 + from direct.task.TaskManagerGlobal import taskMgr + taskMgr.remove("clearRay") + + def clearRayTask(self, task): + """清除射线的任务回调""" + self.clearRay() + return task.done + + def toggleRayDisplay(self): + """切换射线显示状态""" + self.showRay = not self.showRay + if not self.showRay: + self.clearRay() + print(f"射线显示: {'开启' if self.showRay else '关闭'}") + return self.showRay + + def mousePressEventLeft(self, evt): + """处理鼠标左键按下事件""" + print("\n=== 开始处理鼠标左键事件 ===") + #print(f"当前工具: {self.world.currentTool}") + + if not evt: + print("事件为空") + return + if self.world.currentTool == "地形编辑": + self._handleTerrainEdit(evt,"add") + return + + # 获取鼠标点击的位置 + x = evt.get('x', 0) + y = evt.get('y', 0) + #print(f"鼠标点击位置: ({x}, {y})") + + # 获取准确的窗口尺寸 + winWidth, winHeight = self.world.getWindowSize() + + # 直接使用 x, y 创建鼠标位置 + mx = 2.0 * x / float(winWidth) - 1.0 + my = 1.0 - 2.0 * y / float(winHeight) + #print(f"转换后的坐标: ({mx}, {my})") + + # 创建射线 + nearPoint = Point3() + farPoint = Point3() + self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint) + #print(f"相机坐标系射线起点: {nearPoint}") + #print(f"相机坐标系射线终点: {farPoint}") + + # 将相机坐标系的点转换到世界坐标系 + worldNearPoint = self.world.render.getRelativePoint(self.world.cam, nearPoint) + worldFarPoint = self.world.render.getRelativePoint(self.world.cam, farPoint) + #print(f"世界坐标系射线起点: {worldNearPoint}") + #print(f"世界坐标系射线终点: {worldFarPoint}") + + # 进行射线检测 + picker = CollisionTraverser() + queue = CollisionHandlerQueue() + + pickerNode = CollisionNode('mouseRay') + pickerNP = self.world.cam.attachNewNode(pickerNode) + # 设置射线的碰撞掩码,匹配模型的碰撞掩码(第2位) + from panda3d.core import BitMask32 + pickerNode.setFromCollideMask(BitMask32.bit(2)) + + # 使用相机坐标系的点创建射线(因为pickerNP是相机的子节点) + direction = farPoint - nearPoint + direction.normalize() + pickerNode.addSolid(CollisionRay(nearPoint, direction)) + + picker.addCollider(pickerNP, queue) + picker.traverse(self.world.render) + + #print(f"碰撞检测结果数量: {queue.getNumEntries()}") + + # 射线检测结果处理 + hitPos = None + hitNode = None + + if queue.getNumEntries() > 0: + # 获取最近的碰撞点 + entry = queue.getEntry(0) + hitPos = entry.getSurfacePoint(self.world.render) + hitNode = entry.getIntoNodePath() + print(f"碰撞到节点: {hitNode.getName()}") + + # 显示射线(使用世界坐标系的点) + self.showClickRay(worldNearPoint, worldFarPoint, hitPos) + + # 优先检查是否点击了坐标轴 + #print(f"检查坐标轴点击: 坐标轴存在={bool(self.world.selection.gizmo)}") + if self.world.selection.gizmo: + #print("准备检查坐标轴点击...") + try: + highlighted_axis = self.world.selection.gizmoHighlightAxis + if highlighted_axis: + print(f"✓ 检测到高亮轴: {highlighted_axis},直接开始拖拽") + # 直接使用高亮轴开始拖拽 + self.world.selection.startGizmoDrag(highlighted_axis, x, y) + pickerNP.removeNode() + return + + # 如果没有高亮轴,再尝试检测点击 + gizmoAxis = self.world.selection.checkGizmoClick(x, y) + if gizmoAxis: + print(f"✓ 检测到坐标轴点击: {gizmoAxis}") + # 开始坐标轴拖拽 + self.world.selection.startGizmoDrag(gizmoAxis, x, y) + pickerNP.removeNode() + return + else: + print("× 没有点击到坐标轴") + + # gizmoAxis = self.world.selection.checkGizmoClick(x, y) + # if gizmoAxis: + # #print(f"✓ 检测到坐标轴点击: {gizmoAxis}") + # # 开始坐标轴拖拽 + # self.world.selection.startGizmoDrag(gizmoAxis, x, y) + # pickerNP.removeNode() + # return + # else: + # print("× 没有点击到坐标轴") + except Exception as e: + print(f"❌ 坐标轴点击检测出现异常: {str(e)}") + import traceback + traceback.print_exc() + print("继续处理模型选择...") + + #print("继续处理碰撞结果...") + + if hitPos and hitNode: + #print(f"✓ 检测到碰撞,开始处理点击事件") + #print(f"GUI编辑模式: {self.world.guiEditMode}") + #print(f"当前工具: {self.world.currentTool}") + + # 处理GUI编辑模式 + if self.world.guiEditMode: + #print("处理GUI编辑模式点击") + # 检查是否点击了GUI元素 + clickedGUI = self.world.gui_manager.findClickedGUI(hitNode) + if clickedGUI: + # 选中GUI元素 + self.world.selection.updateSelection(clickedGUI) + self.world.gui_manager.selectGUIInTree(clickedGUI) + print(f"选中GUI元素: {clickedGUI.getTag('gui_text')}") + elif hasattr(self.world, 'currentGUITool') and self.world.currentGUITool: + # 在点击位置创建新GUI元素 + self.world.gui_manager.createGUIAtPosition(hitPos, self.world.currentGUITool) + pickerNP.removeNode() + return + + # 根据当前工具处理点击事件 + if self.world.currentTool == "选择": + print("✓ 使用选择工具处理点击") + try: + self._handleSelectionClick(hitNode) + print("✓ 选择处理完成") + except Exception as e: + print(f"❌ 选择处理出现异常: {str(e)}") + import traceback + traceback.print_exc() + else: + print(f"当前工具不是'选择',无法处理: {self.world.currentTool}") + else: + print("没有检测到碰撞") + + # 如果不在GUI编辑模式,清除选择 + if not self.world.guiEditMode: + self.world.selection.updateSelection(None) + + # 在GUI编辑模式下,即使没有碰撞也可以在空白区域创建GUI + if (self.world.guiEditMode and + hasattr(self.world, 'currentGUITool') and + self.world.currentGUITool): + # 使用默认的地面高度创建GUI + default_height = 0.0 + world_pos = Point3(mx * 10, 0, my * 10) # 简单的屏幕到世界坐标转换 + world_pos.setZ(default_height) + self.world.gui_manager.createGUIAtPosition(world_pos, self.world.currentGUITool) + + # 确保总是清理碰撞检测节点 + try: + pickerNP.removeNode() + print("✓ 碰撞检测节点已清理") + except Exception as e: + print(f"清理碰撞检测节点失败: {str(e)}") + + print("=== 鼠标左键事件处理结束 ===\n") + + def mousePressEventRight(self,evt): + """处理鼠标右键按下事件""" + print(f"当前工具: {self.world.currentTool}") + + # 检查是否是地形编辑模式 + if self.world.currentTool == "地形编辑": + self._handleTerrainEdit(evt, "subtract") # 降低地形 + return + + # 其他右键处理逻辑可以在这里添加 + print("鼠标右键事件处理") + + def _handleTerrainEdit(self, evt, operation): + """处理地形编辑""" + try: + x = evt.get('x', 0) + y = evt.get('y', 0) + + winWidth, winHeight = self.world.getWindowSize() + + mx = 2.0 * x / float(winWidth) - 1.0 + my = 1.0 - 2.0 * y / float(winHeight) + + nearPoint = Point3() + farPoint = Point3() + self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint) + + worldNearPoint = self.world.render.getRelativePoint(self.world.cam, nearPoint) + worldFarPoint = self.world.render.getRelativePoint(self.world.cam, farPoint) + + picker = CollisionTraverser() + queue = CollisionHandlerQueue() + + pickerNode = CollisionNode('terrain_edit_ray') + pickerNP = self.world.cam.attachNewNode(pickerNode) + + from panda3d.core import BitMask32 + # 确保我们检测所有碰撞体 + pickerNode.setFromCollideMask(BitMask32.bit(2)) + + # 使用相机坐标系的点创建射线 + direction = farPoint - nearPoint + direction.normalize() + pickerNode.addSolid(CollisionRay(nearPoint, direction)) + + picker.addCollider(pickerNP, queue) + picker.traverse(self.world.render) + print(f"地形碰撞检测结果数量: {queue.getNumEntries()}") + + # 添加调试信息,显示所有场景中的碰撞体 + print("场景中的碰撞体:") + + def show_colliders(node, level=0): + indent = " " * level + if not node.isEmpty(): + if isinstance(node.node(), CollisionNode): + print(f"{indent}CollisionNode: {node.getName()}") + for child in node.getChildren(): + show_colliders(child, level + 1) + + show_colliders(self.world.render) + + # 射线检测结果处理 + hitPos = None + hitNode = None + hitTerrainInfo = None + + if queue.getNumEntries() > 0: + queue.sortEntries() + # 遍历所有碰撞结果,找到地形节点 + for i in range(queue.getNumEntries()): + entry = queue.getEntry(i) + collided_node = entry.getIntoNodePath() + print(f"碰撞到节点: {collided_node.getName()}") + + # 检查是否是地形节点 + for terrain_info in self.world.terrain_manager.terrains: + terrain_node = terrain_info['node'] + if collided_node == terrain_node or terrain_node.isAncestorOf(collided_node): + hitPos = entry.getSurfacePoint(self.world.render) + hitNode = collided_node + hitTerrainInfo = terrain_info + print(f"找到地形节点: {terrain_node.getName()}") + break + + if hitPos: + break + + if hitPos and hitTerrainInfo: + # 修改地形高度 + x_pos, y_pos = hitPos.getX(), hitPos.getY() + + # 使用 getattr 获取地形编辑参数 + radius = getattr(self.world, 'terrain_edit_radius', 3.0) + strength = getattr(self.world, 'terrain_edit_strength', 0.3) + + print(f"准备编辑地形: 位置({x_pos:.2f}, {y_pos:.2f}), 半径{radius}, 强度{strength}, 操作{operation}") + + # 添加更多调试信息 + print(f"地形信息: {hitTerrainInfo}") + if 'heightmap' in hitTerrainInfo: + print(f"高度图路径: {hitTerrainInfo['heightmap']}") + + try: + # 检查 terrain_manager 是否有 modifyTerrainHeight 方法 + if not hasattr(self.world.terrain_manager, 'modifyTerrainHeight'): + print("✗ 错误: terrain_manager 没有 modifyTerrainHeight 方法") + self.showClickRay(worldNearPoint, worldFarPoint, hitPos) + return + + # 调用地形编辑方法 + success = self.world.terrain_manager.modifyTerrainHeight( + hitTerrainInfo, x_pos, y_pos, radius=radius, strength=strength, operation=operation) + + if success: + print(f"✓ 地形编辑成功: {operation} at ({x_pos:.2f}, {y_pos:.2f})") + # 显示射线 + self.showClickRay(worldNearPoint, worldFarPoint, hitPos) + + # 强制刷新地形显示 + if 'terrain' in hitTerrainInfo: + hitTerrainInfo['terrain'].generate() + print("✓ 地形已刷新") + else: + print("✗ 地形编辑失败") + # 即使编辑失败也显示射线 + self.showClickRay(worldNearPoint, worldFarPoint, hitPos) + except Exception as e: + print(f"✗ 地形编辑过程中出现异常: {e}") + import traceback + traceback.print_exc() + # 即使编辑失败也显示射线 + self.showClickRay(worldNearPoint, worldFarPoint, hitPos) + else: + print("没有检测到地形碰撞") + # 显示射线(无碰撞) + self.showClickRay(worldNearPoint, worldFarPoint) + + # 清理碰撞检测节点 + pickerNP.removeNode() + print("地形编辑处理完成") + + except Exception as e: + print(f"地形编辑处理出错: {e}") + import traceback + traceback.print_exc() + + def _handleSelectionClick(self, hitNode): + """处理选择工具的点击事件""" + print(f"开始处理选择点击,碰撞节点: {hitNode.getName()}") + + # 查找对应的实际模型节点 + selectedModel = None + + # 如果点击的是碰撞节点,找到它的父模型 + if isinstance(hitNode.node(), CollisionNode): + print(f"点击的是碰撞节点: {hitNode.getName()}") + # 碰撞节点的父节点应该是模型 + parent = hitNode.getParent() + if parent in self.world.models: + selectedModel = parent + print(f"找到对应的模型: {selectedModel.getName()}") + else: + print(f"碰撞节点的父节点不是模型: {parent.getName()}") + else: + # 查找可选择的节点(模型或其子节点) + current = hitNode + while current != self.world.render: + # 检查是否是模型 + if current in self.world.models: + selectedModel = current + print(f"找到模型节点: {selectedModel.getName()}") + break + + # 检查是否是模型的子节点 + for model in self.world.models: + if current.getParent() == model or current.isAncestorOf(model): + selectedModel = model + print(f"找到父模型: {selectedModel.getName()}") + break + + if selectedModel: + break + + current = current.getParent() + + if selectedModel: + #print(f"✓ 最终选中模型: {selectedModel.getName()}") + self.world.selection.handleMouseClick(selectedModel) + + # 更新选择状态并显示选择框和坐标轴 + self.world.selection.updateSelection(selectedModel) + + # 在树形控件中查找并选中对应的项 + if self.world.interface_manager.treeWidget: + #print("查找树形控件中的对应项...") + root = self.world.interface_manager.treeWidget.invisibleRootItem() + foundItem = None + + for i in range(root.childCount()): + sceneItem = root.child(i) + if sceneItem.text(0) == "场景": + #print(f"在场景节点下查找...") + foundItem = self.world.interface_manager.findTreeItem(selectedModel, sceneItem) + if foundItem: + print(f"✓ 在树形控件中找到对应项: {foundItem.text(0)}") + try: + self.world.interface_manager.treeWidget.itemClicked.disconnect() + except TypeError: + pass + + self.world.interface_manager.treeWidget.setCurrentItem(foundItem) + + self.world.interface_manager.treeWidget.itemClicked.connect( + self.world.interface_manager.onTreeItemClicked) + else: + print("× 在树形控件中没有找到对应项") + break + + if not foundItem: + print("× 没有找到场景节点或对应的树形项") + else: + print("× 树形控件不存在") + else: + print("× 没有找到可选择的模型节点") + self.world.selection.updateSelection(None) + + def mouseReleaseEventLeft(self, evt): + """处理鼠标左键释放事件""" + # 处理坐标轴拖拽结束 + if self.world.selection.isDraggingGizmo: + self.world.selection.stopGizmoDrag() + return + + def wheelForward(self, data=None): + """处理滚轮向前滚动(前进)""" + # 调用CoreWorld的父类方法 + super(type(self.world), self.world).wheelForward(data) + + # 更新属性面板 + if (self.world.interface_manager.treeWidget and + self.world.interface_manager.treeWidget.currentItem() and + self.world.interface_manager.treeWidget.currentItem().text(0) == "相机"): + self.world.property_panel.updatePropertyPanel( + self.world.interface_manager.treeWidget.currentItem()) + + def wheelBackward(self, data=None): + """处理滚轮向后滚动(后退)""" + # 调用CoreWorld的父类方法 + super(type(self.world), self.world).wheelBackward(data) + + # 更新属性面板 + if (self.world.interface_manager.treeWidget and + self.world.interface_manager.treeWidget.currentItem() and + self.world.interface_manager.treeWidget.currentItem().text(0) == "相机"): + self.world.property_panel.updatePropertyPanel( + self.world.interface_manager.treeWidget.currentItem()) + + def mousePressEventMiddle(self, evt): + """处理鼠标中键按下事件""" + # 已移除原有的Z轴拖拽功能 + pass + + def mouseReleaseEventMiddle(self, evt): + """处理鼠标中键释放事件""" + # 已移除原有的Z轴拖拽功能 + pass + + def mouseMoveEvent(self, evt): + """处理鼠标移动事件""" + if not evt: + return + + # 处理坐标轴拖拽 + if self.world.selection.isDraggingGizmo: + x = evt.get('x', 0) + y = evt.get('y', 0) + + # 获取准确的窗口尺寸 + winWidth, winHeight = self.world.getWindowSize() + + # 将屏幕坐标转换为世界坐标 + mx = 2.0 * x / float(winWidth) - 1.0 + my = 1.0 - 2.0 * y / float(winHeight) + + # 更新坐标轴拖拽 + self.world.selection.updateGizmoDrag(x, y) + return + + # 更新坐标轴高亮(鼠标悬停效果) + if self.world.selection.gizmo and not self.world.selection.isDraggingGizmo: + x = evt.get('x', 0) + y = evt.get('y', 0) + # 减少高亮调试输出,只在需要时输出 + # 已静默处理,避免控制台刷屏 + self.world.selection.updateGizmoHighlight(x, y) + + # 调用CoreWorld的父类方法处理基础的相机旋转 super(type(self.world), self.world).mouseMoveEvent(evt) \ No newline at end of file diff --git a/core/maintenance_gui.py b/core/maintenance_gui.py index 74d0add6..1e5cb962 100644 --- a/core/maintenance_gui.py +++ b/core/maintenance_gui.py @@ -1,1094 +1,1094 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -维修系统场景GUI界面 -包括步骤显示、工具选择按钮和警告提示 -""" - -from direct.gui.DirectGui import * -from direct.gui import DirectGuiGlobals as DGG -from direct.showbase.DirectObject import DirectObject -from direct.task import Task -from direct.task.TaskManagerGlobal import taskMgr -from panda3d.core import TextNode, Vec3, Vec4 - -# 引入全局变量 -import builtins - - -class MaintenanceGUI(DirectObject): - """维修系统GUI管理器""" - - def __init__(self, world): - DirectObject.__init__(self) - self.world = world - - self.chinese_font = None - if hasattr(self.world, 'getChineseFont'): - self.chinese_font = self.world.getChineseFont() - - # 获取GUI父节点引用 - self.aspect2d = None - print(f"🔍 初始化维修GUI,world类型: {type(world).__name__}") - print(f"🔍 world是否有aspect2d: {hasattr(world, 'aspect2d')}") - - if hasattr(world, 'aspect2d'): - self.aspect2d = world.aspect2d - print("✅ 从world获取aspect2d成功") - else: - print("❌ world没有aspect2d属性") - - # GUI元素 - self.step_text = None # 步骤显示文本 - self.current_tool_text = None # 当前工具显示文本 - self.warning_text = None # 警告文本 - self.tool_buttons = [] # 工具按钮列表 - self.stop_button = None # 停止按钮 - - # 状态 - self.current_tool = None # 当前选择的工具,初始为无 - self.available_tools = [] # 可用工具列表 - self.current_step_info = "" # 当前步骤信息 - self.mode = "training" # 模式:training 或 exam - - # 警告显示任务 - self.warning_task = None - - print("✅ 维修系统GUI初始化完成") - - # 添加测试快捷键 - self.accept("t", self.test_tool_selection) # 按T键测试工具选择 - self.accept("1", lambda: self.test_specific_button(0)) # 按1键测试第一个按钮 - self.accept("2", lambda: self.test_specific_button(1)) # 按2键测试第二个按钮 - self.accept("3", lambda: self.test_specific_button(2)) # 按3键测试第三个按钮 - self.accept("p", self.print_button_positions) # 按P键打印按钮位置 - - # 添加鼠标监控任务 - self.mouse_monitor_task = None - - def setup_gui(self, tools_list, mode="training"): - """设置GUI界面""" - try: - print(f"🎨 设置维修系统GUI,工具列表: {tools_list}, 模式: {mode}") - - self.available_tools = tools_list if tools_list else ["手"] - self.mode = mode - - # 重新尝试获取aspect2d引用 - self.get_aspect2d_reference() - - # 清理现有GUI - self.cleanup_gui() - - # 创建步骤显示文本 - self.create_step_text() - - # 创建工具选择按钮(训练和考核模式都需要) - self.create_tool_buttons() - self.create_current_tool_text() - - # 创建停止按钮(训练和考核模式都需要) - # self.create_stop_button() - - # 创建警告文本(仅训练模式需要) - if mode == "training": - self.create_warning_text() - - print("✅ 维修系统GUI设置完成") - - # 启动鼠标监控任务(训练和考核模式都需要) - self.start_mouse_monitor() - - except Exception as e: - print(f"❌ 设置维修系统GUI失败: {e}") - import traceback - traceback.print_exc() - - def start_mouse_monitor(self): - """启动鼠标监控任务""" - try: - # 停止可能存在的旧任务 - if self.mouse_monitor_task: - taskMgr.remove(self.mouse_monitor_task) - - # 启动新的监控任务 - self.mouse_monitor_task = taskMgr.add(self.monitor_mouse_clicks, "mouse_monitor") - print("✅ 鼠标监控任务已启动") - print(f"✅ 任务管理器中的任务: {[task.getName() for task in taskMgr.getAllTasks()]}") - - # 测试一下任务是否能正常运行 - self.test_mouse_monitor() - - except Exception as e: - print(f"❌ 启动鼠标监控失败: {e}") - import traceback - traceback.print_exc() - - def test_mouse_monitor(self): - """测试鼠标监控功能""" - try: - print("🧪 测试鼠标监控功能...") - - # 尝试直接调用监控函数 - result = self.monitor_mouse_clicks(None) - print(f"🧪 监控函数调用结果: {result}") - - except Exception as e: - print(f"❌ 测试鼠标监控失败: {e}") - import traceback - traceback.print_exc() - - def monitor_mouse_clicks(self, task): - """监控鼠标点击并手动检测按钮""" - try: - # 添加计数器来减少日志输出 - if not hasattr(self, '_monitor_counter'): - self._monitor_counter = 0 - self._monitor_counter += 1 - - # 每100帧输出一次状态 - if self._monitor_counter % 100 == 1: - print(f"🔄 鼠标监控运行中... (帧 {self._monitor_counter})") - - # 检查鼠标状态 - mouse_watcher = None - if hasattr(self.world, 'mouseWatcherNode'): - mouse_watcher = self.world.mouseWatcherNode - if self._monitor_counter % 100 == 1: - print(f"✅ 从world获取mouseWatcherNode: {mouse_watcher}") - else: - # 使用全局base对象 - try: - import builtins - if hasattr(builtins, 'base') and hasattr(builtins.base, 'mouseWatcherNode'): - mouse_watcher = builtins.base.mouseWatcherNode - if self._monitor_counter % 100 == 1: - print(f"✅ 从base获取mouseWatcherNode: {mouse_watcher}") - except: - if self._monitor_counter % 100 == 1: - print("❌ 无法获取mouseWatcherNode") - - if mouse_watcher: - has_mouse = mouse_watcher.hasMouse() - if has_mouse: - mouse_pos = mouse_watcher.getMouse() - - # 检查左键状态 - mouse_x = mouse_pos.getX() - mouse_y = mouse_pos.getY() - - # 检查左键是否被按下 - is_button_down = mouse_watcher.isButtonDown('mouse1') - - # 如果没有记录当前状态,初始化 - if not hasattr(self, '_last_button_state'): - self._last_button_state = False - - # 检测按钮从未按下到按下的状态变化(按下瞬间) - if is_button_down and not self._last_button_state: - print(f"🖱️ 鼠标监控:检测到按钮按下,位置({mouse_x:.3f}, {mouse_y:.3f})") - - # 检查这次点击是否在按钮区域 - clicked_button = self.check_button_click(mouse_x, mouse_y) - if clicked_button is not None: - print(f"🎯 鼠标监控检测到按钮点击: {clicked_button}") - self.on_tool_selected(clicked_button) - - # 更新按钮状态 - self._last_button_state = is_button_down - else: - if self._monitor_counter % 100 == 1: - print("❌ mouseWatcher为空") - - return task.cont if task else None - - except Exception as e: - print(f"❌ 鼠标监控异常: {e}") - import traceback - traceback.print_exc() - return task.cont if task else None - - def check_button_click(self, mouse_x, mouse_y): - """检查鼠标点击是否在按钮区域""" - try: - button_width = 0.4 # 与create_tool_buttons保持一致 - button_height = 0.25 # 与create_tool_buttons保持一致 - button_spacing = 0.45 # 与create_tool_buttons保持一致 - start_x = -0.8 # 与create_tool_buttons保持一致 - start_y = -0.75 # 与create_tool_buttons保持一致 - - print(f"🔍 检查按钮点击:鼠标({mouse_x:.3f}, {mouse_y:.3f})") - - for i, tool_data in enumerate(self.available_tools): - # 获取工具名称 - if isinstance(tool_data, dict): - tool_name = tool_data.get('name', str(tool_data)) - else: - tool_name = str(tool_data) - - # 计算按钮位置 - button_x = start_x + i * button_spacing - button_y = start_y - - # 计算按钮边界 - left = button_x - button_width/2 - right = button_x + button_width/2 - bottom = button_y - button_height/2 - top = button_y + button_height/2 - - print(f" 按钮{i} '{tool_name}': 中心({button_x:.3f}, {button_y:.3f}), 范围x[{left:.3f}, {right:.3f}], y[{bottom:.3f}, {top:.3f}]") - - # 检查鼠标是否在按钮范围内 - if (left <= mouse_x <= right and bottom <= mouse_y <= top): - print(f"✅ 点击在按钮{i} '{tool_name}' 范围内") - return tool_name - - print("❌ 点击不在任何按钮范围内") - return None - - except Exception as e: - print(f"❌ 检查按钮点击异常: {e}") - return None - - def handle_mouse_click(self, qt_x, qt_y): - """直接处理鼠标点击(从拆装交互系统调用)""" - try: - print(f"🖱️ 直接处理鼠标点击:Qt坐标({qt_x}, {qt_y})") - - # 将Qt坐标转换为aspect2d坐标 - # 获取窗口尺寸 - if hasattr(self.world, 'qtWidget') and self.world.qtWidget: - widget_width = self.world.qtWidget.width() - widget_height = self.world.qtWidget.height() - else: - widget_width = 1380 - widget_height = 750 - - # 坐标转换(与之前拆装交互系统中的算法一致) - aspect_x = (qt_x / widget_width) * 2.67 - 1.33 - aspect_y = 1.0 - (qt_y / widget_height) * 2.0 - - print(f"🔄 坐标转换:Qt({qt_x}, {qt_y}) -> aspect2d({aspect_x:.3f}, {aspect_y:.3f})") - - # 检查是否点击在按钮区域 - clicked_button = self.check_button_click(aspect_x, aspect_y) - if clicked_button: - print(f"🎯 直接处理:检测到按钮点击 '{clicked_button}'") - self.on_tool_selected(clicked_button) - return True - - return False - - except Exception as e: - print(f"❌ 直接处理鼠标点击失败: {e}") - import traceback - traceback.print_exc() - return False - - def get_aspect2d_reference(self): - """获取aspect2d引用""" - if self.aspect2d: - return # 已经有引用了 - - try: - print("🔍 重新尝试获取aspect2d引用...") - - # 方法1: 直接使用world对象(推荐) - if hasattr(self.world, 'aspect2d'): - self.aspect2d = self.world.aspect2d - print("✅ 从world获取aspect2d成功") - return - - # 方法2: 使用world对象作为父节点 - # 在main.py的Panda3DWorld中,world对象本身就是ShowBase的实例 - self.aspect2d = self.world.aspect2d if hasattr(self.world, 'aspect2d') else None - - if self.aspect2d: - print("✅ 使用world.aspect2d成功") - else: - print("❌ 无法获取aspect2d引用") - - except Exception as e: - print(f"❌ 获取aspect2d引用失败: {e}") - import traceback - traceback.print_exc() - - def create_step_text(self): - """创建步骤显示文本""" - try: - if not self.aspect2d: - print("❌ aspect2d引用不可用,无法创建步骤文本") - return - - self.step_text = DirectLabel( - text="准备开始维修训练...", - text_align=TextNode.ALeft, - text_scale=0.08, - text_fg=(1, 1, 1, 1), - text_bg=(0, 0, 0, 0.7), - frameColor=(0, 0, 0, 0.5), - frameSize=(-1.8, 1.8, -0.3, 0.3), - pos=(-1.7, 0, 0.8), - parent=self.aspect2d - ) - print("✅ 步骤显示文本创建成功") - - except Exception as e: - print(f"❌ 创建步骤文本失败: {e}") - import traceback - traceback.print_exc() - - def create_tool_buttons(self): - """创建工具选择按钮""" - try: - print(f"🔧 开始创建工具按钮...") - print(f" aspect2d引用: {self.aspect2d}") - print(f" 可用工具: {self.available_tools}") - print(f" 模式: {self.mode}") - - if not self.aspect2d: - print("❌ aspect2d引用不可用,无法创建工具按钮") - print(" 尝试重新获取aspect2d引用...") - self.get_aspect2d_reference() - if not self.aspect2d: - print("❌ 重新获取aspect2d失败") - return - - print(f"🔧 使用aspect2d创建工具按钮: {self.available_tools}") - - # 按钮布局参数(优化为更容易点击) - button_width = 0.4 # 增加宽度 - button_height = 0.25 # 增加高度,让按钮更容易点击 - button_spacing = 0.45 # 增加间距 - start_x = -0.8 # 进一步向右移动 - start_y = -0.75 # 向上移动一点 - - for i, tool in enumerate(self.available_tools): - # 处理工具数据格式 - if isinstance(tool, dict): - tool_name = tool.get('name', str(tool)) - tool_description = tool.get('description', '') - else: - tool_name = str(tool) - tool_description = '' - - print(f" 正在创建按钮 {i+1}/{len(self.available_tools)}: {tool_name}") - - # 计算按钮位置(横向排列) - pos_x = start_x + i * button_spacing - pos_y = start_y - - # 创建按钮 - button = DirectButton( - text=tool_name, - text_scale=0.06, - text_fg=(1, 1, 1, 1), - frameColor=(0.3, 0.3, 0.8, 0.8), - frameSize=(-button_width/2, button_width/2, -button_height/2, button_height/2), - pos=(pos_x, 0, pos_y), - command=self.on_tool_selected, - extraArgs=[tool_name], - parent=self.aspect2d, - # 按钮交互设置 - relief=DGG.RAISED, - borderWidth=(0.01, 0.01), - text_pos=(0, -0.02), - # 强制启用鼠标交互 - enableEdit=1, - # 音效设置 - clickSound=None, - rolloverSound=None, - # 确保按钮优先级和可见性 - sortOrder=1000, # 更高的排序值 - state=DGG.NORMAL, # 确保按钮状态正常 - # 鼠标事件设置 - suppressMouse=0 - ) - - # 手动绑定额外的事件监听 - button.bind(DGG.B1PRESS, self.button_press_handler, extraArgs=[tool_name]) - button.bind(DGG.B1RELEASE, self.button_release_handler, extraArgs=[tool_name]) - - self.tool_buttons.append(button) - print(f" ✅ 成功创建工具按钮: {tool_name} at ({pos_x}, {pos_y})") - - print(f"🎯 总共创建了 {len(self.tool_buttons)} 个工具按钮") - - # 设置默认选择的工具样式 - self.update_tool_button_styles() - - except Exception as e: - print(f"❌ 创建工具按钮失败: {e}") - import traceback - traceback.print_exc() - - def create_current_tool_text(self): - """创建当前工具显示文本""" - try: - if not self.aspect2d: - print("❌ aspect2d引用不可用,无法创建当前工具文本") - return - - self.current_tool_text = DirectLabel( - text=f"Tool: {self.current_tool}", - text_align=TextNode.ALeft, - text_scale=0.07, - text_fg=(1, 1, 0, 1), - text_bg=(0, 0, 0, 0.7), - frameColor=(0, 0, 0, 0.5), - frameSize=(-0.8, 0.8, -0.2, 0.2), - pos=(1.0, 0, 0.8), - parent=self.aspect2d - ) - print("✅ 当前工具文本创建成功") - - except Exception as e: - print(f"❌ 创建当前工具文本失败: {e}") - import traceback - traceback.print_exc() - - def create_stop_button(self): - """创建停止按钮""" - try: - if not self.aspect2d: - print("❌ aspect2d引用不可用,无法创建停止按钮") - return - - # 停止按钮位置:右下角 - self.stop_button = DirectButton( - text="停止", - text_align=TextNode.ACenter, - text_scale=0.06, - text_fg=(1, 1, 1, 1), - text_bg=(0, 0, 0, 0), - frameColor=(0.8, 0.2, 0.2, 0.9), # 红色背景 - frameSize=(-0.15, 0.15, -0.08, 0.08), - pos=(1.15, 0, -0.85), # 右下角位置 - parent=self.aspect2d, - command=self.on_stop_clicked, - relief=DGG.RAISED, - borderWidth=(0.01, 0.01), - rolloverSound=None, - clickSound=None, - pressEffect=1 - ) - - print("✅ 停止按钮创建成功") - - except Exception as e: - print(f"❌ 创建停止按钮失败: {e}") - import traceback - traceback.print_exc() - - def create_warning_text(self): - """创建警告文本""" - try: - if not self.aspect2d: - print("❌ aspect2d引用不可用,无法创建警告文本") - return - - self.warning_text = DirectLabel( - text="", - text_align=TextNode.ACenter, - text_scale=0.09, - text_fg=(1, 0, 0, 1), - text_bg=(1, 1, 0, 0.8), - frameColor=(1, 0, 0, 0.8), - frameSize=(-1.5, 1.5, -0.3, 0.3), - pos=(0, 0, 0.3), - parent=self.aspect2d - ) - # 默认隐藏警告文本 - self.warning_text.hide() - print("✅ 警告文本创建成功") - - except Exception as e: - print(f"❌ 创建警告文本失败: {e}") - import traceback - traceback.print_exc() - - def button_press_handler(self, tool_name, event=None): - """按钮按下事件处理""" - print(f"🖱️ 按钮按下事件:{tool_name}") - - def button_release_handler(self, tool_name, event=None): - """按钮释放事件处理""" - print(f"🖱️ 按钮释放事件:{tool_name}") - print(f"🔄 直接调用工具选择:{tool_name}") - self.on_tool_selected(tool_name) - - def on_tool_selected(self, tool): - """工具选择回调""" - try: - print(f"🎯 按钮点击事件触发!") - print(f"🔧 选择工具: {tool}") - - # 考核模式下额外提示 - if hasattr(self, 'mode') and self.mode == "exam": - print(f"📝 考核模式工具选择: {tool}") - - print(f"📍 当前工具变更: {self.current_tool} -> {tool}") - - self.current_tool = tool - - # 更新当前工具显示 - if self.current_tool_text: - self.current_tool_text['text'] = f"Tool: {tool}" - print(f"✅ 当前工具显示已更新") - - # 更新按钮样式 - self.update_tool_button_styles() - print(f"✅ 按钮样式已更新") - - # 通知拆装交互系统工具变更 - if hasattr(self.world, 'assembly_interaction') and self.world.assembly_interaction: - if hasattr(self.world.assembly_interaction, 'step_dialog') and self.world.assembly_interaction.step_dialog: - # 更新步骤对话框中的工具选择 - step_dialog = self.world.assembly_interaction.step_dialog - if hasattr(step_dialog, 'tool_combo'): - # 同步工具选择 - tool_index = -1 - for i in range(step_dialog.tool_combo.count()): - if step_dialog.tool_combo.itemText(i) == tool: - tool_index = i - break - if tool_index >= 0: - step_dialog.tool_combo.setCurrentIndex(tool_index) - print(f"✅ Qt对话框工具选择已同步") - - print(f"✅ 工具切换完成: {tool}") - - except Exception as e: - print(f"❌ 工具选择失败: {e}") - import traceback - traceback.print_exc() - - def on_stop_clicked(self): - """停止按钮点击回调""" - try: - print("🛑 用户点击停止按钮") - - # 通知拆装交互系统停止 - if hasattr(self.world, 'assembly_interaction') and self.world.assembly_interaction: - self.world.assembly_interaction.stop_interaction() - else: - print("⚠️ 拆装交互系统不存在,无法停止") - - except Exception as e: - print(f"❌ 停止操作失败: {e}") - import traceback - traceback.print_exc() - - def update_tool_button_styles(self): - """更新工具按钮样式""" - try: - for i, button in enumerate(self.tool_buttons): - # 处理工具数据格式 - tool_data = self.available_tools[i] - if isinstance(tool_data, dict): - tool_name = tool_data.get('name', str(tool_data)) - else: - tool_name = str(tool_data) - - if tool_name == self.current_tool: - # 选中状态:亮蓝色 - button['frameColor'] = (0.2, 0.8, 1.0, 0.9) - button['text_fg'] = (0, 0, 0, 1) - else: - # 未选中状态:深蓝色 - button['frameColor'] = (0.3, 0.3, 0.8, 0.8) - button['text_fg'] = (1, 1, 1, 1) - - except Exception as e: - print(f"❌ 更新按钮样式失败: {e}") - import traceback - traceback.print_exc() - - def update_step_info(self, step_text): - """更新步骤信息""" - try: - print(f"📋 更新步骤信息: {step_text}") - - self.current_step_info = step_text - - if self.step_text: - self.step_text['text'] = step_text - - except Exception as e: - print(f"❌ 更新步骤信息失败: {e}") - - def show_warning(self, message, duration=2.0): - """显示警告信息(仅训练模式)""" - try: - # 考核模式下不显示警告 - if hasattr(self, 'mode') and self.mode == "exam": - print(f"📝 考核模式:隐藏警告 - {message}") - return - - print(f"⚠️ 显示警告: {message}") - - if not self.warning_text: - return - - # 取消之前的警告任务 - if self.warning_task: - taskMgr.remove(self.warning_task) - self.warning_task = None - - # 显示警告文本 - self.warning_text['text'] = message - self.warning_text.show() - - # 设置自动隐藏任务 - self.warning_task = taskMgr.doMethodLater( - duration, - self.hide_warning, - 'hide_warning_task' - ) - - except Exception as e: - print(f"❌ 显示警告失败: {e}") - - def hide_warning(self, task=None): - """隐藏警告信息""" - try: - if self.warning_text: - self.warning_text.hide() - - if self.warning_task: - self.warning_task = None - - return Task.done - - except Exception as e: - print(f"❌ 隐藏警告失败: {e}") - return Task.done - - def test_tool_selection(self): - """测试工具选择功能(按T键触发)""" - if self.available_tools: - # 循环选择工具进行测试 - current_index = 0 - for i, tool_data in enumerate(self.available_tools): - if isinstance(tool_data, dict): - tool_name = tool_data.get('name', str(tool_data)) - else: - tool_name = str(tool_data) - - if tool_name == self.current_tool: - current_index = i - break - - # 选择下一个工具 - next_index = (current_index + 1) % len(self.available_tools) - next_tool_data = self.available_tools[next_index] - - if isinstance(next_tool_data, dict): - next_tool_name = next_tool_data.get('name', str(next_tool_data)) - else: - next_tool_name = str(next_tool_data) - - print(f"⌨️ 键盘测试:切换到工具 '{next_tool_name}'") - self.on_tool_selected(next_tool_name) - - def test_specific_button(self, button_index): - """测试特定按钮(通过索引)""" - if not hasattr(self, 'available_tools') or not self.available_tools: - print(f"⌨️ 按钮{button_index}: 没有可用工具") - return - - if button_index >= len(self.available_tools): - print(f"⌨️ 按钮{button_index}: 索引超出范围(最大{len(self.available_tools)-1})") - return - - # 获取工具名称 - tool_data = self.available_tools[button_index] - if isinstance(tool_data, dict): - tool_name = tool_data.get('name', str(tool_data)) - else: - tool_name = str(tool_data) - - print(f"⌨️ 按钮{button_index}测试:切换到工具 '{tool_name}'") - self.on_tool_selected(tool_name) - - def print_button_positions(self): - """打印所有按钮的位置信息""" - try: - if not hasattr(self, 'available_tools') or not self.available_tools: - print("📍 没有可用工具按钮") - return - - print("📍 ===== 按钮位置信息 =====") - - button_width = 0.4 - button_height = 0.25 - button_spacing = 0.45 - start_x = -0.8 - start_y = -0.75 - - for i, tool_data in enumerate(self.available_tools): - # 获取工具名称 - if isinstance(tool_data, dict): - tool_name = tool_data.get('name', str(tool_data)) - else: - tool_name = str(tool_data) - - # 计算按钮位置 - button_x = start_x + i * button_spacing - button_y = start_y - - # 计算按钮边界 - left = button_x - button_width/2 - right = button_x + button_width/2 - bottom = button_y - button_height/2 - top = button_y + button_height/2 - - print(f"📍 按钮{i} '{tool_name}':") - print(f" 中心: ({button_x:.3f}, {button_y:.3f})") - print(f" 范围: x[{left:.3f}, {right:.3f}], y[{bottom:.3f}, {top:.3f}]") - print(f" 尺寸: {button_width} x {button_height}") - - print("📍 ========================") - - except Exception as e: - print(f"❌ 打印按钮位置失败: {e}") - import traceback - traceback.print_exc() - - def show_exam_results(self, exam_data): - """显示考核结果(GUI界面)""" - try: - print("🎓 开始显示考核结果GUI...") - - # 清理现有GUI元素 - self.cleanup_gui() - - # 创建考核结果界面 - self.create_exam_result_gui(exam_data) - - print("✅ 考核结果GUI显示完成") - - except Exception as e: - print(f"❌ 显示考核结果GUI失败: {e}") - import traceback - traceback.print_exc() - - def create_exam_result_gui(self, exam_data): - """创建考核结果GUI界面""" - try: - if not self.aspect2d: - print("❌ aspect2d引用不可用,无法创建考核结果GUI") - return - - # 主背景 - 黑色背景 - from direct.gui.DirectGui import DirectFrame - self.exam_bg = DirectFrame( - frameColor=(0, 0, 0, 0.95), # 更深的黑色背景 - frameSize=(-2.0, 2.0, -1.0, 1.0), - pos=(0, 0, 0), - parent=self.aspect2d - ) - - # 标题 - self.exam_title = DirectLabel( - text="🎓 考核结果报告", - text_align=TextNode.ACenter, - text_scale=0.12, - text_fg=(1, 1, 0, 1), # 黄色文字 - text_bg=(0, 0, 0, 0), # 透明背景 - frameColor=(0, 0, 0, 0), # 透明边框 - frameSize=(-1.5, 1.5, -0.15, 0.15), - pos=(0, 0, 0.8), - parent=self.aspect2d - ) - - # 总体成绩区域 - score_text = f"📊 总得分: {exam_data['final_score']:.0f}/{exam_data['max_score']:.0f} 分 ({exam_data['score_rate']:.1f}%)\n" - score_text += f"🏅 评级: {exam_data['grade_icon']} {exam_data['grade']} - {exam_data['grade_desc']}\n" - score_text += f"🎯 操作准确率: {exam_data['accuracy_rate']:.1f}% | 完美步骤: {exam_data['perfect_steps']}/{exam_data['total_steps']}" - - # 根据成绩选择文字颜色(在黑色背景下更突出) - if exam_data['score_rate'] >= 90: - score_fg_color = (0.2, 1.0, 0.2, 1) # 亮绿色 - elif exam_data['score_rate'] >= 80: - score_fg_color = (0.8, 1.0, 0.2, 1) # 亮黄绿色 - elif exam_data['score_rate'] >= 60: - score_fg_color = (1.0, 0.8, 0.2, 1) # 亮橙色 - else: - score_fg_color = (1.0, 0.3, 0.3, 1) # 亮红色 - - self.exam_score = DirectLabel( - text=score_text, - text_align=TextNode.ACenter, - text_scale=0.07, - text_fg=score_fg_color, # 根据成绩动态变色 - text_bg=(0, 0, 0, 0), # 透明背景 - frameColor=(0, 0, 0, 0), # 透明边框 - frameSize=(-1.9, 1.9, -0.3, 0.3), - pos=(0, 0, 0.35), - parent=self.aspect2d - ) - - # 步骤详情区域 - steps_text = "📋 步骤详情:\n" - for step in exam_data['step_details']: - steps_text += f"{step['status']} {step['name']}: {step['current_score']:.0f}/{step['max_score']:.0f}分" - if step['tool_error']: - steps_text += " ❌工具错误" - steps_text += f" ({step['attempts']}次操作)\n" - - self.exam_steps = DirectLabel( - text=steps_text, - text_align=TextNode.ALeft, - text_scale=0.06, - text_fg=(0.9, 0.9, 0.9, 1), # 浅灰色文字 - text_bg=(0, 0, 0, 0), # 透明背景 - frameColor=(0, 0, 0, 0), # 透明边框 - frameSize=(-1.8, 1.8, -0.4, 0.4), - pos=(0, 0, -0.1), - parent=self.aspect2d - ) - - # 改进建议区域(如果有建议) - if exam_data['suggestions']: - suggestions_text = "💡 改进建议:\n" - for suggestion in exam_data['suggestions']: - suggestions_text += f"{suggestion}\n" - - self.exam_suggestions = DirectLabel( - text=suggestions_text, - text_align=TextNode.ALeft, - text_scale=0.05, - text_fg=(1, 0.8, 0.2, 1), # 橙黄色文字 - text_bg=(0, 0, 0, 0), # 透明背景 - frameColor=(0, 0, 0, 0), # 透明边框 - frameSize=(-1.8, 1.8, -0.25, 0.25), - pos=(0, 0, -0.7), - parent=self.aspect2d - ) - - # 倒计时显示 - self.exam_countdown_text = DirectLabel( - text="10秒后自动关闭", - text_align=TextNode.ACenter, - text_scale=0.06, - text_fg=(0.8, 0.8, 0.8, 1), # 灰色文字 - text_bg=(0, 0, 0, 0), # 透明背景 - frameColor=(0, 0, 0, 0), # 透明边框 - frameSize=(-0.8, 0.8, -0.1, 0.1), - pos=(0, 0, -0.8), - parent=self.aspect2d - ) - - # 启动10秒倒计时任务 - self.start_countdown_timer() - - print("✅ 考核结果GUI元素创建完成") - - except Exception as e: - print(f"❌ 创建考核结果GUI失败: {e}") - import traceback - traceback.print_exc() - - def close_exam_results(self): - """关闭考核结果界面""" - try: - print("🔄 关闭考核结果界面...") - - # 清理考核结果GUI元素 - if hasattr(self, 'exam_bg') and self.exam_bg: - self.exam_bg.removeNode() - self.exam_bg = None - - if hasattr(self, 'exam_title') and self.exam_title: - self.exam_title.removeNode() - self.exam_title = None - - if hasattr(self, 'exam_score') and self.exam_score: - self.exam_score.removeNode() - self.exam_score = None - - if hasattr(self, 'exam_steps') and self.exam_steps: - self.exam_steps.removeNode() - self.exam_steps = None - - if hasattr(self, 'exam_suggestions') and self.exam_suggestions: - self.exam_suggestions.removeNode() - self.exam_suggestions = None - - if hasattr(self, 'exam_countdown_text') and self.exam_countdown_text: - self.exam_countdown_text.removeNode() - self.exam_countdown_text = None - - # 停止倒计时任务 - if hasattr(self, 'exam_countdown_task') and self.exam_countdown_task: - taskMgr.remove(self.exam_countdown_task) - self.exam_countdown_task = None - - print("✅ 考核结果界面已关闭") - - except Exception as e: - print(f"❌ 关闭考核结果界面失败: {e}") - import traceback - traceback.print_exc() - - def start_countdown_timer(self): - """启动10秒倒计时""" - try: - self.countdown_seconds = 10 - print(f"⏰ 启动考核结果倒计时:{self.countdown_seconds}秒") - - # 启动倒计时任务 - self.exam_countdown_task = taskMgr.doMethodLater( - 1.0, # 每秒执行一次 - self.update_countdown, - 'exam_countdown_task' - ) - - except Exception as e: - print(f"❌ 启动倒计时失败: {e}") - import traceback - traceback.print_exc() - - def update_countdown(self, task): - """更新倒计时显示""" - try: - self.countdown_seconds -= 1 - - if self.countdown_seconds > 0: - # 更新倒计时显示 - if hasattr(self, 'exam_countdown_text') and self.exam_countdown_text: - self.exam_countdown_text['text'] = f"{self.countdown_seconds}秒后自动关闭" - - print(f"⏰ 倒计时:{self.countdown_seconds}秒") - - # 继续倒计时 - return task.again - else: - # 倒计时结束,自动关闭 - print("⏰ 倒计时结束,自动关闭考核结果界面") - self.close_exam_results() - return task.done - - except Exception as e: - print(f"❌ 更新倒计时失败: {e}") - import traceback - traceback.print_exc() - return task.done - - - def get_current_tool(self): - """获取当前选择的工具""" - return self.current_tool - - def set_mode(self, mode): - """设置模式""" - self.mode = mode - print(f"🎯 维修GUI模式设置为: {mode}") - - def cleanup_gui(self): - """清理GUI元素""" - try: - print("🧹 清理维修系统GUI") - - # 清理步骤文本 - if self.step_text: - self.step_text.destroy() - self.step_text = None - - # 清理当前工具文本 - if self.current_tool_text: - self.current_tool_text.destroy() - self.current_tool_text = None - - # 清理警告文本 - if self.warning_text: - self.warning_text.destroy() - self.warning_text = None - - # 清理工具按钮 - for button in self.tool_buttons: - if button: - button.destroy() - self.tool_buttons.clear() - - # 清理停止按钮 - if hasattr(self, 'stop_button') and self.stop_button: - self.stop_button.destroy() - self.stop_button = None - - # 取消警告任务 - if self.warning_task: - taskMgr.remove(self.warning_task) - self.warning_task = None - - # 取消鼠标监控任务 - if self.mouse_monitor_task: - taskMgr.remove(self.mouse_monitor_task) - self.mouse_monitor_task = None - - # 清理考核结果界面 - self.close_exam_results() - - print("✅ 维修系统GUI清理完成") - - except Exception as e: - print(f"❌ 清理维修系统GUI失败: {e}") - - def show_gui(self): - """显示GUI""" - try: - print(f"🎨 显示维修系统GUI...") - print(f" 模式: {self.mode}") - print(f" 步骤文本: {self.step_text is not None}") - print(f" 当前工具文本: {self.current_tool_text is not None}") - print(f" 工具按钮数量: {len(self.tool_buttons)}") - - if self.step_text: - self.step_text.show() - print(" ✅ 步骤文本已显示") - - # 当前工具文本和工具按钮在训练和考核模式都需要显示 - if self.current_tool_text: - self.current_tool_text.show() - print(" ✅ 当前工具文本已显示") - - for i, button in enumerate(self.tool_buttons): - button.show() - print(f" ✅ 工具按钮 {i+1} 已显示") - - # 停止按钮在训练和考核模式都需要显示 - if self.stop_button: - self.stop_button.show() - print(" ✅ 停止按钮已显示") - - print("✅ 维修系统GUI显示完成") - - except Exception as e: - print(f"❌ 显示维修系统GUI失败: {e}") - import traceback - traceback.print_exc() - - def hide_gui(self): - """隐藏GUI""" - try: - if self.step_text: - self.step_text.hide() - - if self.current_tool_text: - self.current_tool_text.hide() - - if self.warning_text: - self.warning_text.hide() - - for button in self.tool_buttons: - button.hide() - - if self.stop_button: - self.stop_button.hide() - - print("✅ 维修系统GUI隐藏") - - except Exception as e: +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +维修系统场景GUI界面 +包括步骤显示、工具选择按钮和警告提示 +""" + +from direct.gui.DirectGui import * +from direct.gui import DirectGuiGlobals as DGG +from direct.showbase.DirectObject import DirectObject +from direct.task import Task +from direct.task.TaskManagerGlobal import taskMgr +from panda3d.core import TextNode, Vec3, Vec4 + +# 引入全局变量 +import builtins + + +class MaintenanceGUI(DirectObject): + """维修系统GUI管理器""" + + def __init__(self, world): + DirectObject.__init__(self) + self.world = world + + self.chinese_font = None + if hasattr(self.world, 'getChineseFont'): + self.chinese_font = self.world.getChineseFont() + + # 获取GUI父节点引用 + self.aspect2d = None + print(f"🔍 初始化维修GUI,world类型: {type(world).__name__}") + print(f"🔍 world是否有aspect2d: {hasattr(world, 'aspect2d')}") + + if hasattr(world, 'aspect2d'): + self.aspect2d = world.aspect2d + print("✅ 从world获取aspect2d成功") + else: + print("❌ world没有aspect2d属性") + + # GUI元素 + self.step_text = None # 步骤显示文本 + self.current_tool_text = None # 当前工具显示文本 + self.warning_text = None # 警告文本 + self.tool_buttons = [] # 工具按钮列表 + self.stop_button = None # 停止按钮 + + # 状态 + self.current_tool = None # 当前选择的工具,初始为无 + self.available_tools = [] # 可用工具列表 + self.current_step_info = "" # 当前步骤信息 + self.mode = "training" # 模式:training 或 exam + + # 警告显示任务 + self.warning_task = None + + print("✅ 维修系统GUI初始化完成") + + # 添加测试快捷键 + self.accept("t", self.test_tool_selection) # 按T键测试工具选择 + self.accept("1", lambda: self.test_specific_button(0)) # 按1键测试第一个按钮 + self.accept("2", lambda: self.test_specific_button(1)) # 按2键测试第二个按钮 + self.accept("3", lambda: self.test_specific_button(2)) # 按3键测试第三个按钮 + self.accept("p", self.print_button_positions) # 按P键打印按钮位置 + + # 添加鼠标监控任务 + self.mouse_monitor_task = None + + def setup_gui(self, tools_list, mode="training"): + """设置GUI界面""" + try: + print(f"🎨 设置维修系统GUI,工具列表: {tools_list}, 模式: {mode}") + + self.available_tools = tools_list if tools_list else ["手"] + self.mode = mode + + # 重新尝试获取aspect2d引用 + self.get_aspect2d_reference() + + # 清理现有GUI + self.cleanup_gui() + + # 创建步骤显示文本 + self.create_step_text() + + # 创建工具选择按钮(训练和考核模式都需要) + self.create_tool_buttons() + self.create_current_tool_text() + + # 创建停止按钮(训练和考核模式都需要) + # self.create_stop_button() + + # 创建警告文本(仅训练模式需要) + if mode == "training": + self.create_warning_text() + + print("✅ 维修系统GUI设置完成") + + # 启动鼠标监控任务(训练和考核模式都需要) + self.start_mouse_monitor() + + except Exception as e: + print(f"❌ 设置维修系统GUI失败: {e}") + import traceback + traceback.print_exc() + + def start_mouse_monitor(self): + """启动鼠标监控任务""" + try: + # 停止可能存在的旧任务 + if self.mouse_monitor_task: + taskMgr.remove(self.mouse_monitor_task) + + # 启动新的监控任务 + self.mouse_monitor_task = taskMgr.add(self.monitor_mouse_clicks, "mouse_monitor") + print("✅ 鼠标监控任务已启动") + print(f"✅ 任务管理器中的任务: {[task.getName() for task in taskMgr.getAllTasks()]}") + + # 测试一下任务是否能正常运行 + self.test_mouse_monitor() + + except Exception as e: + print(f"❌ 启动鼠标监控失败: {e}") + import traceback + traceback.print_exc() + + def test_mouse_monitor(self): + """测试鼠标监控功能""" + try: + print("🧪 测试鼠标监控功能...") + + # 尝试直接调用监控函数 + result = self.monitor_mouse_clicks(None) + print(f"🧪 监控函数调用结果: {result}") + + except Exception as e: + print(f"❌ 测试鼠标监控失败: {e}") + import traceback + traceback.print_exc() + + def monitor_mouse_clicks(self, task): + """监控鼠标点击并手动检测按钮""" + try: + # 添加计数器来减少日志输出 + if not hasattr(self, '_monitor_counter'): + self._monitor_counter = 0 + self._monitor_counter += 1 + + # 每100帧输出一次状态 + if self._monitor_counter % 100 == 1: + print(f"🔄 鼠标监控运行中... (帧 {self._monitor_counter})") + + # 检查鼠标状态 + mouse_watcher = None + if hasattr(self.world, 'mouseWatcherNode'): + mouse_watcher = self.world.mouseWatcherNode + if self._monitor_counter % 100 == 1: + print(f"✅ 从world获取mouseWatcherNode: {mouse_watcher}") + else: + # 使用全局base对象 + try: + import builtins + if hasattr(builtins, 'base') and hasattr(builtins.base, 'mouseWatcherNode'): + mouse_watcher = builtins.base.mouseWatcherNode + if self._monitor_counter % 100 == 1: + print(f"✅ 从base获取mouseWatcherNode: {mouse_watcher}") + except: + if self._monitor_counter % 100 == 1: + print("❌ 无法获取mouseWatcherNode") + + if mouse_watcher: + has_mouse = mouse_watcher.hasMouse() + if has_mouse: + mouse_pos = mouse_watcher.getMouse() + + # 检查左键状态 + mouse_x = mouse_pos.getX() + mouse_y = mouse_pos.getY() + + # 检查左键是否被按下 + is_button_down = mouse_watcher.isButtonDown('mouse1') + + # 如果没有记录当前状态,初始化 + if not hasattr(self, '_last_button_state'): + self._last_button_state = False + + # 检测按钮从未按下到按下的状态变化(按下瞬间) + if is_button_down and not self._last_button_state: + print(f"🖱️ 鼠标监控:检测到按钮按下,位置({mouse_x:.3f}, {mouse_y:.3f})") + + # 检查这次点击是否在按钮区域 + clicked_button = self.check_button_click(mouse_x, mouse_y) + if clicked_button is not None: + print(f"🎯 鼠标监控检测到按钮点击: {clicked_button}") + self.on_tool_selected(clicked_button) + + # 更新按钮状态 + self._last_button_state = is_button_down + else: + if self._monitor_counter % 100 == 1: + print("❌ mouseWatcher为空") + + return task.cont if task else None + + except Exception as e: + print(f"❌ 鼠标监控异常: {e}") + import traceback + traceback.print_exc() + return task.cont if task else None + + def check_button_click(self, mouse_x, mouse_y): + """检查鼠标点击是否在按钮区域""" + try: + button_width = 0.4 # 与create_tool_buttons保持一致 + button_height = 0.25 # 与create_tool_buttons保持一致 + button_spacing = 0.45 # 与create_tool_buttons保持一致 + start_x = -0.8 # 与create_tool_buttons保持一致 + start_y = -0.75 # 与create_tool_buttons保持一致 + + print(f"🔍 检查按钮点击:鼠标({mouse_x:.3f}, {mouse_y:.3f})") + + for i, tool_data in enumerate(self.available_tools): + # 获取工具名称 + if isinstance(tool_data, dict): + tool_name = tool_data.get('name', str(tool_data)) + else: + tool_name = str(tool_data) + + # 计算按钮位置 + button_x = start_x + i * button_spacing + button_y = start_y + + # 计算按钮边界 + left = button_x - button_width/2 + right = button_x + button_width/2 + bottom = button_y - button_height/2 + top = button_y + button_height/2 + + print(f" 按钮{i} '{tool_name}': 中心({button_x:.3f}, {button_y:.3f}), 范围x[{left:.3f}, {right:.3f}], y[{bottom:.3f}, {top:.3f}]") + + # 检查鼠标是否在按钮范围内 + if (left <= mouse_x <= right and bottom <= mouse_y <= top): + print(f"✅ 点击在按钮{i} '{tool_name}' 范围内") + return tool_name + + print("❌ 点击不在任何按钮范围内") + return None + + except Exception as e: + print(f"❌ 检查按钮点击异常: {e}") + return None + + def handle_mouse_click(self, qt_x, qt_y): + """直接处理鼠标点击(从拆装交互系统调用)""" + try: + print(f"🖱️ 直接处理鼠标点击:Qt坐标({qt_x}, {qt_y})") + + # 将Qt坐标转换为aspect2d坐标 + # 获取窗口尺寸 + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + widget_width = self.world.qtWidget.width() + widget_height = self.world.qtWidget.height() + else: + widget_width = 1380 + widget_height = 750 + + # 坐标转换(与之前拆装交互系统中的算法一致) + aspect_x = (qt_x / widget_width) * 2.67 - 1.33 + aspect_y = 1.0 - (qt_y / widget_height) * 2.0 + + print(f"🔄 坐标转换:Qt({qt_x}, {qt_y}) -> aspect2d({aspect_x:.3f}, {aspect_y:.3f})") + + # 检查是否点击在按钮区域 + clicked_button = self.check_button_click(aspect_x, aspect_y) + if clicked_button: + print(f"🎯 直接处理:检测到按钮点击 '{clicked_button}'") + self.on_tool_selected(clicked_button) + return True + + return False + + except Exception as e: + print(f"❌ 直接处理鼠标点击失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_aspect2d_reference(self): + """获取aspect2d引用""" + if self.aspect2d: + return # 已经有引用了 + + try: + print("🔍 重新尝试获取aspect2d引用...") + + # 方法1: 直接使用world对象(推荐) + if hasattr(self.world, 'aspect2d'): + self.aspect2d = self.world.aspect2d + print("✅ 从world获取aspect2d成功") + return + + # 方法2: 使用world对象作为父节点 + # 在main.py的Panda3DWorld中,world对象本身就是ShowBase的实例 + self.aspect2d = self.world.aspect2d if hasattr(self.world, 'aspect2d') else None + + if self.aspect2d: + print("✅ 使用world.aspect2d成功") + else: + print("❌ 无法获取aspect2d引用") + + except Exception as e: + print(f"❌ 获取aspect2d引用失败: {e}") + import traceback + traceback.print_exc() + + def create_step_text(self): + """创建步骤显示文本""" + try: + if not self.aspect2d: + print("❌ aspect2d引用不可用,无法创建步骤文本") + return + + self.step_text = DirectLabel( + text="准备开始维修训练...", + text_align=TextNode.ALeft, + text_scale=0.08, + text_fg=(1, 1, 1, 1), + text_bg=(0, 0, 0, 0.7), + frameColor=(0, 0, 0, 0.5), + frameSize=(-1.8, 1.8, -0.3, 0.3), + pos=(-1.7, 0, 0.8), + parent=self.aspect2d + ) + print("✅ 步骤显示文本创建成功") + + except Exception as e: + print(f"❌ 创建步骤文本失败: {e}") + import traceback + traceback.print_exc() + + def create_tool_buttons(self): + """创建工具选择按钮""" + try: + print(f"🔧 开始创建工具按钮...") + print(f" aspect2d引用: {self.aspect2d}") + print(f" 可用工具: {self.available_tools}") + print(f" 模式: {self.mode}") + + if not self.aspect2d: + print("❌ aspect2d引用不可用,无法创建工具按钮") + print(" 尝试重新获取aspect2d引用...") + self.get_aspect2d_reference() + if not self.aspect2d: + print("❌ 重新获取aspect2d失败") + return + + print(f"🔧 使用aspect2d创建工具按钮: {self.available_tools}") + + # 按钮布局参数(优化为更容易点击) + button_width = 0.4 # 增加宽度 + button_height = 0.25 # 增加高度,让按钮更容易点击 + button_spacing = 0.45 # 增加间距 + start_x = -0.8 # 进一步向右移动 + start_y = -0.75 # 向上移动一点 + + for i, tool in enumerate(self.available_tools): + # 处理工具数据格式 + if isinstance(tool, dict): + tool_name = tool.get('name', str(tool)) + tool_description = tool.get('description', '') + else: + tool_name = str(tool) + tool_description = '' + + print(f" 正在创建按钮 {i+1}/{len(self.available_tools)}: {tool_name}") + + # 计算按钮位置(横向排列) + pos_x = start_x + i * button_spacing + pos_y = start_y + + # 创建按钮 + button = DirectButton( + text=tool_name, + text_scale=0.06, + text_fg=(1, 1, 1, 1), + frameColor=(0.3, 0.3, 0.8, 0.8), + frameSize=(-button_width/2, button_width/2, -button_height/2, button_height/2), + pos=(pos_x, 0, pos_y), + command=self.on_tool_selected, + extraArgs=[tool_name], + parent=self.aspect2d, + # 按钮交互设置 + relief=DGG.RAISED, + borderWidth=(0.01, 0.01), + text_pos=(0, -0.02), + # 强制启用鼠标交互 + enableEdit=1, + # 音效设置 + clickSound=None, + rolloverSound=None, + # 确保按钮优先级和可见性 + sortOrder=1000, # 更高的排序值 + state=DGG.NORMAL, # 确保按钮状态正常 + # 鼠标事件设置 + suppressMouse=0 + ) + + # 手动绑定额外的事件监听 + button.bind(DGG.B1PRESS, self.button_press_handler, extraArgs=[tool_name]) + button.bind(DGG.B1RELEASE, self.button_release_handler, extraArgs=[tool_name]) + + self.tool_buttons.append(button) + print(f" ✅ 成功创建工具按钮: {tool_name} at ({pos_x}, {pos_y})") + + print(f"🎯 总共创建了 {len(self.tool_buttons)} 个工具按钮") + + # 设置默认选择的工具样式 + self.update_tool_button_styles() + + except Exception as e: + print(f"❌ 创建工具按钮失败: {e}") + import traceback + traceback.print_exc() + + def create_current_tool_text(self): + """创建当前工具显示文本""" + try: + if not self.aspect2d: + print("❌ aspect2d引用不可用,无法创建当前工具文本") + return + + self.current_tool_text = DirectLabel( + text=f"Tool: {self.current_tool}", + text_align=TextNode.ALeft, + text_scale=0.07, + text_fg=(1, 1, 0, 1), + text_bg=(0, 0, 0, 0.7), + frameColor=(0, 0, 0, 0.5), + frameSize=(-0.8, 0.8, -0.2, 0.2), + pos=(1.0, 0, 0.8), + parent=self.aspect2d + ) + print("✅ 当前工具文本创建成功") + + except Exception as e: + print(f"❌ 创建当前工具文本失败: {e}") + import traceback + traceback.print_exc() + + def create_stop_button(self): + """创建停止按钮""" + try: + if not self.aspect2d: + print("❌ aspect2d引用不可用,无法创建停止按钮") + return + + # 停止按钮位置:右下角 + self.stop_button = DirectButton( + text="停止", + text_align=TextNode.ACenter, + text_scale=0.06, + text_fg=(1, 1, 1, 1), + text_bg=(0, 0, 0, 0), + frameColor=(0.8, 0.2, 0.2, 0.9), # 红色背景 + frameSize=(-0.15, 0.15, -0.08, 0.08), + pos=(1.15, 0, -0.85), # 右下角位置 + parent=self.aspect2d, + command=self.on_stop_clicked, + relief=DGG.RAISED, + borderWidth=(0.01, 0.01), + rolloverSound=None, + clickSound=None, + pressEffect=1 + ) + + print("✅ 停止按钮创建成功") + + except Exception as e: + print(f"❌ 创建停止按钮失败: {e}") + import traceback + traceback.print_exc() + + def create_warning_text(self): + """创建警告文本""" + try: + if not self.aspect2d: + print("❌ aspect2d引用不可用,无法创建警告文本") + return + + self.warning_text = DirectLabel( + text="", + text_align=TextNode.ACenter, + text_scale=0.09, + text_fg=(1, 0, 0, 1), + text_bg=(1, 1, 0, 0.8), + frameColor=(1, 0, 0, 0.8), + frameSize=(-1.5, 1.5, -0.3, 0.3), + pos=(0, 0, 0.3), + parent=self.aspect2d + ) + # 默认隐藏警告文本 + self.warning_text.hide() + print("✅ 警告文本创建成功") + + except Exception as e: + print(f"❌ 创建警告文本失败: {e}") + import traceback + traceback.print_exc() + + def button_press_handler(self, tool_name, event=None): + """按钮按下事件处理""" + print(f"🖱️ 按钮按下事件:{tool_name}") + + def button_release_handler(self, tool_name, event=None): + """按钮释放事件处理""" + print(f"🖱️ 按钮释放事件:{tool_name}") + print(f"🔄 直接调用工具选择:{tool_name}") + self.on_tool_selected(tool_name) + + def on_tool_selected(self, tool): + """工具选择回调""" + try: + print(f"🎯 按钮点击事件触发!") + print(f"🔧 选择工具: {tool}") + + # 考核模式下额外提示 + if hasattr(self, 'mode') and self.mode == "exam": + print(f"📝 考核模式工具选择: {tool}") + + print(f"📍 当前工具变更: {self.current_tool} -> {tool}") + + self.current_tool = tool + + # 更新当前工具显示 + if self.current_tool_text: + self.current_tool_text['text'] = f"Tool: {tool}" + print(f"✅ 当前工具显示已更新") + + # 更新按钮样式 + self.update_tool_button_styles() + print(f"✅ 按钮样式已更新") + + # 通知拆装交互系统工具变更 + if hasattr(self.world, 'assembly_interaction') and self.world.assembly_interaction: + if hasattr(self.world.assembly_interaction, 'step_dialog') and self.world.assembly_interaction.step_dialog: + # 更新步骤对话框中的工具选择 + step_dialog = self.world.assembly_interaction.step_dialog + if hasattr(step_dialog, 'tool_combo'): + # 同步工具选择 + tool_index = -1 + for i in range(step_dialog.tool_combo.count()): + if step_dialog.tool_combo.itemText(i) == tool: + tool_index = i + break + if tool_index >= 0: + step_dialog.tool_combo.setCurrentIndex(tool_index) + print(f"✅ Qt对话框工具选择已同步") + + print(f"✅ 工具切换完成: {tool}") + + except Exception as e: + print(f"❌ 工具选择失败: {e}") + import traceback + traceback.print_exc() + + def on_stop_clicked(self): + """停止按钮点击回调""" + try: + print("🛑 用户点击停止按钮") + + # 通知拆装交互系统停止 + if hasattr(self.world, 'assembly_interaction') and self.world.assembly_interaction: + self.world.assembly_interaction.stop_interaction() + else: + print("⚠️ 拆装交互系统不存在,无法停止") + + except Exception as e: + print(f"❌ 停止操作失败: {e}") + import traceback + traceback.print_exc() + + def update_tool_button_styles(self): + """更新工具按钮样式""" + try: + for i, button in enumerate(self.tool_buttons): + # 处理工具数据格式 + tool_data = self.available_tools[i] + if isinstance(tool_data, dict): + tool_name = tool_data.get('name', str(tool_data)) + else: + tool_name = str(tool_data) + + if tool_name == self.current_tool: + # 选中状态:亮蓝色 + button['frameColor'] = (0.2, 0.8, 1.0, 0.9) + button['text_fg'] = (0, 0, 0, 1) + else: + # 未选中状态:深蓝色 + button['frameColor'] = (0.3, 0.3, 0.8, 0.8) + button['text_fg'] = (1, 1, 1, 1) + + except Exception as e: + print(f"❌ 更新按钮样式失败: {e}") + import traceback + traceback.print_exc() + + def update_step_info(self, step_text): + """更新步骤信息""" + try: + print(f"📋 更新步骤信息: {step_text}") + + self.current_step_info = step_text + + if self.step_text: + self.step_text['text'] = step_text + + except Exception as e: + print(f"❌ 更新步骤信息失败: {e}") + + def show_warning(self, message, duration=2.0): + """显示警告信息(仅训练模式)""" + try: + # 考核模式下不显示警告 + if hasattr(self, 'mode') and self.mode == "exam": + print(f"📝 考核模式:隐藏警告 - {message}") + return + + print(f"⚠️ 显示警告: {message}") + + if not self.warning_text: + return + + # 取消之前的警告任务 + if self.warning_task: + taskMgr.remove(self.warning_task) + self.warning_task = None + + # 显示警告文本 + self.warning_text['text'] = message + self.warning_text.show() + + # 设置自动隐藏任务 + self.warning_task = taskMgr.doMethodLater( + duration, + self.hide_warning, + 'hide_warning_task' + ) + + except Exception as e: + print(f"❌ 显示警告失败: {e}") + + def hide_warning(self, task=None): + """隐藏警告信息""" + try: + if self.warning_text: + self.warning_text.hide() + + if self.warning_task: + self.warning_task = None + + return Task.done + + except Exception as e: + print(f"❌ 隐藏警告失败: {e}") + return Task.done + + def test_tool_selection(self): + """测试工具选择功能(按T键触发)""" + if self.available_tools: + # 循环选择工具进行测试 + current_index = 0 + for i, tool_data in enumerate(self.available_tools): + if isinstance(tool_data, dict): + tool_name = tool_data.get('name', str(tool_data)) + else: + tool_name = str(tool_data) + + if tool_name == self.current_tool: + current_index = i + break + + # 选择下一个工具 + next_index = (current_index + 1) % len(self.available_tools) + next_tool_data = self.available_tools[next_index] + + if isinstance(next_tool_data, dict): + next_tool_name = next_tool_data.get('name', str(next_tool_data)) + else: + next_tool_name = str(next_tool_data) + + print(f"⌨️ 键盘测试:切换到工具 '{next_tool_name}'") + self.on_tool_selected(next_tool_name) + + def test_specific_button(self, button_index): + """测试特定按钮(通过索引)""" + if not hasattr(self, 'available_tools') or not self.available_tools: + print(f"⌨️ 按钮{button_index}: 没有可用工具") + return + + if button_index >= len(self.available_tools): + print(f"⌨️ 按钮{button_index}: 索引超出范围(最大{len(self.available_tools)-1})") + return + + # 获取工具名称 + tool_data = self.available_tools[button_index] + if isinstance(tool_data, dict): + tool_name = tool_data.get('name', str(tool_data)) + else: + tool_name = str(tool_data) + + print(f"⌨️ 按钮{button_index}测试:切换到工具 '{tool_name}'") + self.on_tool_selected(tool_name) + + def print_button_positions(self): + """打印所有按钮的位置信息""" + try: + if not hasattr(self, 'available_tools') or not self.available_tools: + print("📍 没有可用工具按钮") + return + + print("📍 ===== 按钮位置信息 =====") + + button_width = 0.4 + button_height = 0.25 + button_spacing = 0.45 + start_x = -0.8 + start_y = -0.75 + + for i, tool_data in enumerate(self.available_tools): + # 获取工具名称 + if isinstance(tool_data, dict): + tool_name = tool_data.get('name', str(tool_data)) + else: + tool_name = str(tool_data) + + # 计算按钮位置 + button_x = start_x + i * button_spacing + button_y = start_y + + # 计算按钮边界 + left = button_x - button_width/2 + right = button_x + button_width/2 + bottom = button_y - button_height/2 + top = button_y + button_height/2 + + print(f"📍 按钮{i} '{tool_name}':") + print(f" 中心: ({button_x:.3f}, {button_y:.3f})") + print(f" 范围: x[{left:.3f}, {right:.3f}], y[{bottom:.3f}, {top:.3f}]") + print(f" 尺寸: {button_width} x {button_height}") + + print("📍 ========================") + + except Exception as e: + print(f"❌ 打印按钮位置失败: {e}") + import traceback + traceback.print_exc() + + def show_exam_results(self, exam_data): + """显示考核结果(GUI界面)""" + try: + print("🎓 开始显示考核结果GUI...") + + # 清理现有GUI元素 + self.cleanup_gui() + + # 创建考核结果界面 + self.create_exam_result_gui(exam_data) + + print("✅ 考核结果GUI显示完成") + + except Exception as e: + print(f"❌ 显示考核结果GUI失败: {e}") + import traceback + traceback.print_exc() + + def create_exam_result_gui(self, exam_data): + """创建考核结果GUI界面""" + try: + if not self.aspect2d: + print("❌ aspect2d引用不可用,无法创建考核结果GUI") + return + + # 主背景 - 黑色背景 + from direct.gui.DirectGui import DirectFrame + self.exam_bg = DirectFrame( + frameColor=(0, 0, 0, 0.95), # 更深的黑色背景 + frameSize=(-2.0, 2.0, -1.0, 1.0), + pos=(0, 0, 0), + parent=self.aspect2d + ) + + # 标题 + self.exam_title = DirectLabel( + text="🎓 考核结果报告", + text_align=TextNode.ACenter, + text_scale=0.12, + text_fg=(1, 1, 0, 1), # 黄色文字 + text_bg=(0, 0, 0, 0), # 透明背景 + frameColor=(0, 0, 0, 0), # 透明边框 + frameSize=(-1.5, 1.5, -0.15, 0.15), + pos=(0, 0, 0.8), + parent=self.aspect2d + ) + + # 总体成绩区域 + score_text = f"📊 总得分: {exam_data['final_score']:.0f}/{exam_data['max_score']:.0f} 分 ({exam_data['score_rate']:.1f}%)\n" + score_text += f"🏅 评级: {exam_data['grade_icon']} {exam_data['grade']} - {exam_data['grade_desc']}\n" + score_text += f"🎯 操作准确率: {exam_data['accuracy_rate']:.1f}% | 完美步骤: {exam_data['perfect_steps']}/{exam_data['total_steps']}" + + # 根据成绩选择文字颜色(在黑色背景下更突出) + if exam_data['score_rate'] >= 90: + score_fg_color = (0.2, 1.0, 0.2, 1) # 亮绿色 + elif exam_data['score_rate'] >= 80: + score_fg_color = (0.8, 1.0, 0.2, 1) # 亮黄绿色 + elif exam_data['score_rate'] >= 60: + score_fg_color = (1.0, 0.8, 0.2, 1) # 亮橙色 + else: + score_fg_color = (1.0, 0.3, 0.3, 1) # 亮红色 + + self.exam_score = DirectLabel( + text=score_text, + text_align=TextNode.ACenter, + text_scale=0.07, + text_fg=score_fg_color, # 根据成绩动态变色 + text_bg=(0, 0, 0, 0), # 透明背景 + frameColor=(0, 0, 0, 0), # 透明边框 + frameSize=(-1.9, 1.9, -0.3, 0.3), + pos=(0, 0, 0.35), + parent=self.aspect2d + ) + + # 步骤详情区域 + steps_text = "📋 步骤详情:\n" + for step in exam_data['step_details']: + steps_text += f"{step['status']} {step['name']}: {step['current_score']:.0f}/{step['max_score']:.0f}分" + if step['tool_error']: + steps_text += " ❌工具错误" + steps_text += f" ({step['attempts']}次操作)\n" + + self.exam_steps = DirectLabel( + text=steps_text, + text_align=TextNode.ALeft, + text_scale=0.06, + text_fg=(0.9, 0.9, 0.9, 1), # 浅灰色文字 + text_bg=(0, 0, 0, 0), # 透明背景 + frameColor=(0, 0, 0, 0), # 透明边框 + frameSize=(-1.8, 1.8, -0.4, 0.4), + pos=(0, 0, -0.1), + parent=self.aspect2d + ) + + # 改进建议区域(如果有建议) + if exam_data['suggestions']: + suggestions_text = "💡 改进建议:\n" + for suggestion in exam_data['suggestions']: + suggestions_text += f"{suggestion}\n" + + self.exam_suggestions = DirectLabel( + text=suggestions_text, + text_align=TextNode.ALeft, + text_scale=0.05, + text_fg=(1, 0.8, 0.2, 1), # 橙黄色文字 + text_bg=(0, 0, 0, 0), # 透明背景 + frameColor=(0, 0, 0, 0), # 透明边框 + frameSize=(-1.8, 1.8, -0.25, 0.25), + pos=(0, 0, -0.7), + parent=self.aspect2d + ) + + # 倒计时显示 + self.exam_countdown_text = DirectLabel( + text="10秒后自动关闭", + text_align=TextNode.ACenter, + text_scale=0.06, + text_fg=(0.8, 0.8, 0.8, 1), # 灰色文字 + text_bg=(0, 0, 0, 0), # 透明背景 + frameColor=(0, 0, 0, 0), # 透明边框 + frameSize=(-0.8, 0.8, -0.1, 0.1), + pos=(0, 0, -0.8), + parent=self.aspect2d + ) + + # 启动10秒倒计时任务 + self.start_countdown_timer() + + print("✅ 考核结果GUI元素创建完成") + + except Exception as e: + print(f"❌ 创建考核结果GUI失败: {e}") + import traceback + traceback.print_exc() + + def close_exam_results(self): + """关闭考核结果界面""" + try: + print("🔄 关闭考核结果界面...") + + # 清理考核结果GUI元素 + if hasattr(self, 'exam_bg') and self.exam_bg: + self.exam_bg.removeNode() + self.exam_bg = None + + if hasattr(self, 'exam_title') and self.exam_title: + self.exam_title.removeNode() + self.exam_title = None + + if hasattr(self, 'exam_score') and self.exam_score: + self.exam_score.removeNode() + self.exam_score = None + + if hasattr(self, 'exam_steps') and self.exam_steps: + self.exam_steps.removeNode() + self.exam_steps = None + + if hasattr(self, 'exam_suggestions') and self.exam_suggestions: + self.exam_suggestions.removeNode() + self.exam_suggestions = None + + if hasattr(self, 'exam_countdown_text') and self.exam_countdown_text: + self.exam_countdown_text.removeNode() + self.exam_countdown_text = None + + # 停止倒计时任务 + if hasattr(self, 'exam_countdown_task') and self.exam_countdown_task: + taskMgr.remove(self.exam_countdown_task) + self.exam_countdown_task = None + + print("✅ 考核结果界面已关闭") + + except Exception as e: + print(f"❌ 关闭考核结果界面失败: {e}") + import traceback + traceback.print_exc() + + def start_countdown_timer(self): + """启动10秒倒计时""" + try: + self.countdown_seconds = 10 + print(f"⏰ 启动考核结果倒计时:{self.countdown_seconds}秒") + + # 启动倒计时任务 + self.exam_countdown_task = taskMgr.doMethodLater( + 1.0, # 每秒执行一次 + self.update_countdown, + 'exam_countdown_task' + ) + + except Exception as e: + print(f"❌ 启动倒计时失败: {e}") + import traceback + traceback.print_exc() + + def update_countdown(self, task): + """更新倒计时显示""" + try: + self.countdown_seconds -= 1 + + if self.countdown_seconds > 0: + # 更新倒计时显示 + if hasattr(self, 'exam_countdown_text') and self.exam_countdown_text: + self.exam_countdown_text['text'] = f"{self.countdown_seconds}秒后自动关闭" + + print(f"⏰ 倒计时:{self.countdown_seconds}秒") + + # 继续倒计时 + return task.again + else: + # 倒计时结束,自动关闭 + print("⏰ 倒计时结束,自动关闭考核结果界面") + self.close_exam_results() + return task.done + + except Exception as e: + print(f"❌ 更新倒计时失败: {e}") + import traceback + traceback.print_exc() + return task.done + + + def get_current_tool(self): + """获取当前选择的工具""" + return self.current_tool + + def set_mode(self, mode): + """设置模式""" + self.mode = mode + print(f"🎯 维修GUI模式设置为: {mode}") + + def cleanup_gui(self): + """清理GUI元素""" + try: + print("🧹 清理维修系统GUI") + + # 清理步骤文本 + if self.step_text: + self.step_text.destroy() + self.step_text = None + + # 清理当前工具文本 + if self.current_tool_text: + self.current_tool_text.destroy() + self.current_tool_text = None + + # 清理警告文本 + if self.warning_text: + self.warning_text.destroy() + self.warning_text = None + + # 清理工具按钮 + for button in self.tool_buttons: + if button: + button.destroy() + self.tool_buttons.clear() + + # 清理停止按钮 + if hasattr(self, 'stop_button') and self.stop_button: + self.stop_button.destroy() + self.stop_button = None + + # 取消警告任务 + if self.warning_task: + taskMgr.remove(self.warning_task) + self.warning_task = None + + # 取消鼠标监控任务 + if self.mouse_monitor_task: + taskMgr.remove(self.mouse_monitor_task) + self.mouse_monitor_task = None + + # 清理考核结果界面 + self.close_exam_results() + + print("✅ 维修系统GUI清理完成") + + except Exception as e: + print(f"❌ 清理维修系统GUI失败: {e}") + + def show_gui(self): + """显示GUI""" + try: + print(f"🎨 显示维修系统GUI...") + print(f" 模式: {self.mode}") + print(f" 步骤文本: {self.step_text is not None}") + print(f" 当前工具文本: {self.current_tool_text is not None}") + print(f" 工具按钮数量: {len(self.tool_buttons)}") + + if self.step_text: + self.step_text.show() + print(" ✅ 步骤文本已显示") + + # 当前工具文本和工具按钮在训练和考核模式都需要显示 + if self.current_tool_text: + self.current_tool_text.show() + print(" ✅ 当前工具文本已显示") + + for i, button in enumerate(self.tool_buttons): + button.show() + print(f" ✅ 工具按钮 {i+1} 已显示") + + # 停止按钮在训练和考核模式都需要显示 + if self.stop_button: + self.stop_button.show() + print(" ✅ 停止按钮已显示") + + print("✅ 维修系统GUI显示完成") + + except Exception as e: + print(f"❌ 显示维修系统GUI失败: {e}") + import traceback + traceback.print_exc() + + def hide_gui(self): + """隐藏GUI""" + try: + if self.step_text: + self.step_text.hide() + + if self.current_tool_text: + self.current_tool_text.hide() + + if self.warning_text: + self.warning_text.hide() + + for button in self.tool_buttons: + button.hide() + + if self.stop_button: + self.stop_button.hide() + + print("✅ 维修系统GUI隐藏") + + except Exception as e: print(f"❌ 隐藏维修系统GUI失败: {e}") \ No newline at end of file diff --git a/core/patrol_system.py b/core/patrol_system.py index ab951b47..25905ba7 100644 --- a/core/patrol_system.py +++ b/core/patrol_system.py @@ -1,495 +1,495 @@ -from direct.showbase.ShowBaseGlobal import globalClock -from direct.task.TaskManagerGlobal import taskMgr -from panda3d.core import Point3, Vec3 -import math - - -class PatrolSystem: - """巡检系统类""" - - def __init__(self, world): - """初始化巡检系统 - - Args: - world: 核心世界对象引用 - """ - self.world = world - - # 巡检状态 - self.is_patrolling = False - self.patrol_points = [] # 巡检点列表 [(pos, hpr, wait_time), ...] - self.current_patrol_index = 0 - self.patrol_task = None - - # 巡检参数 - self.patrol_speed = 5.0 # 巡检移动速度(单位/秒) - self.patrol_turn_speed = 90.0 # 转向速度(度/秒) - self.patrol_wait_timer = 0.0 - self.patrol_state = "moving" # "moving", "turning_to_target", "waiting", "turning_back" - - # 相机状态保存 - self.original_cam_pos = None - self.original_cam_hpr = None - - print("✓ 巡检系统初始化完成") - - def add_patrol_point(self, position, heading=None, wait_time=3.0): - if heading is None: - if self.patrol_points: - last_pos = self.patrol_points[-1][0] - direction_x = position[0] - last_pos.x - direction_y = position[1] - last_pos.y - direction_z = position[2] - last_pos.z - - import math - h=math.degrees(math.atan2(-direction_x,-direction_y)) - - distance_xy = math.sqrt(direction_x**2+direction_y**2) - p = math.degrees(math.atan2(direction_z,distance_xy)) - p = max(-89,min(89,p)) - - r=0 - - heading = (h,p,r) - - else: - # 使用当前相机朝向 - current_hpr = self.world.cam.getHpr() - heading = (current_hpr.x, current_hpr.y, current_hpr.z) - - pos = Point3(position[0], position[1], position[2]) - hpr = Vec3(heading[0], heading[1], heading[2]) - - self.patrol_points.append((pos, hpr, wait_time)) - print(f"✓ 添加巡检点 {len(self.patrol_points)}: 位置{position}, 朝向{heading}, 停留{wait_time}秒") - - # 在 PatrolSystem 类中添加以下方法 - - def add_auto_heading_patrol_point(self, position, wait_time=3.0): - """添加自动计算朝向的巡检点(朝向路径前进方向) - - Args: - position: 相机位置 (x, y, z) - wait_time: 在该点停留时间(秒) - """ - heading = None # 将自动计算朝向 - - # 复用原有的 add_patrol_point 方法 - self.add_patrol_point(position, heading, wait_time) - - def add_patrol_point_looking_at(self, position, look_at_position, wait_time=3.0): - """添加朝向指定位置的巡检点 - - Args: - position: 相机位置 (x, y, z) - look_at_position: 相机朝向的目标位置 (x, y, z) - wait_time: 在该点停留时间(秒) - """ - import math - - # 计算从当前位置到目标位置的方向向量 - direction_x = look_at_position[0] - position[0] - direction_y = look_at_position[1] - position[1] - direction_z = look_at_position[2] - position[2] - - # 计算HPR朝向 - h = math.degrees(math.atan2(-direction_x, -direction_y)) - - distance_xy = math.sqrt(direction_x ** 2 + direction_y ** 2) - p = math.degrees(math.atan2(direction_z, distance_xy)) - p = max(-89, min(89, p)) # 限制pitch角度在合理范围内 - - r = 0 # roll通常为0 - - heading = (h, p, r) - self.add_patrol_point(position, heading, wait_time) - - def clear_patrol_points(self): - """清空所有巡检点""" - self.patrol_points = [] - print("✓ 巡检点已清空") - - def set_patrol_speed(self, move_speed, turn_speed=None): - """设置巡检速度 - - Args: - move_speed: 移动速度(单位/秒) - turn_speed: 转向速度(度/秒),如果为None则保持当前值 - """ - self.patrol_speed = move_speed - if turn_speed is not None: - self.patrol_turn_speed = turn_speed - print(f"✓ 巡检速度已设置: 移动{move_speed}, 转向{turn_speed or self.patrol_turn_speed}") - - def start_patrol(self): - """开始巡检""" - if not self.patrol_points: - print("✗ 没有设置巡检点,无法开始巡检") - return False - - if self.is_patrolling: - print("⚠ 巡检已在进行中") - return True - - # 保存当前相机状态 - self.original_cam_pos = Point3(self.world.cam.getPos()) - self.original_cam_hpr = Vec3(self.world.cam.getHpr()) - - # 重置巡检状态 - self.current_patrol_index = 0 - self.patrol_state = "moving" - self.patrol_wait_timer = 0.0 - self.is_patrolling = True - - # 启动巡检任务 - if self.patrol_task: - taskMgr.remove(self.patrol_task) - self.patrol_task = taskMgr.add(self._patrol_task, "patrol_task") - - print(f"✓ 开始巡检,共{len(self.patrol_points)}个巡检点") - return True - - def stop_patrol(self): - """停止巡检""" - if not self.is_patrolling: - print("⚠ 巡检未在进行中") - return False - - # 停止巡检任务 - if self.patrol_task: - taskMgr.remove(self.patrol_task) - self.patrol_task = None - - self.is_patrolling = False - self.patrol_state = "moving" - self.patrol_wait_timer = 0.0 - - print("✓ 巡检已停止") - return True - - def pause_patrol(self): - """暂停巡检""" - if not self.is_patrolling: - print("⚠ 巡检未在进行中") - return False - - if self.patrol_task: - taskMgr.remove(self.patrol_task) - self.patrol_task = None - - print("✓ 巡检已暂停") - return True - - def resume_patrol(self): - """恢复巡检""" - if self.is_patrolling: - print("⚠ 巡检已在进行中") - return False - - if not self.patrol_points: - print("✗ 没有设置巡检点") - return False - - self.is_patrolling = True - self.patrol_task = taskMgr.add(self._patrol_task, "patrol_task") - - print("✓ 巡检已恢复") - return True - - def reset_to_original_position(self): - """重置相机到原始位置""" - if self.original_cam_pos and self.original_cam_hpr: - self.world.cam.setPos(self.original_cam_pos) - self.world.cam.setHpr(self.original_cam_hpr) - print("✓ 相机已重置到原始位置") - return True - else: - print("✗ 没有保存的原始位置") - return False - - def _patrol_task(self, task): - """巡检主任务""" - try: - if not self.is_patrolling or not self.patrol_points: - return task.done - - # 获取当前巡检点 - current_point = self.patrol_points[self.current_patrol_index] - target_pos, target_hpr, wait_time = current_point - - # 根据当前状态执行不同操作 - if self.patrol_state == "moving": - self._handle_moving_state(target_pos) - elif self.patrol_state == "turning_to_target": - self._handle_turning_to_target_state(target_hpr) - elif self.patrol_state == "waiting": - self._handle_waiting_state(wait_time) - elif self.patrol_state == "turning_back": - self._handle_turning_back_state() - - return task.cont - - except Exception as e: - print(f"巡检任务出错: {e}") - import traceback - traceback.print_exc() - return task.done - - def _handle_moving_state(self, target_pos): - """处理移动状态""" - current_pos = self.world.cam.getPos() - distance = (target_pos - current_pos).length() - - if distance < 0.1: # 到达目标点 - print(f"✓ 到达巡检点 {self.current_patrol_index + 1}") - self.patrol_state = "turning_to_target" - return - - # 计算移动方向和距离 - direction = target_pos - current_pos - direction.normalize() - - # 计算目标朝向(看向目标点) - target_hpr = self._look_at_to_hpr(direction) - current_hpr = self.world.cam.getHpr() - - # 平滑转向到目标朝向 - h_diff = self._normalize_angle(target_hpr.x - current_hpr.x) - p_diff = self._normalize_angle(target_hpr.y - current_hpr.y) - r_diff = self._normalize_angle(target_hpr.z - current_hpr.z) - - # 计算本帧应转动的角度 - dt = globalClock.getDt() - turn_amount = self.patrol_turn_speed * dt - - # 逐步转向目标角度 - new_hpr = Vec3(current_hpr) - - if abs(h_diff) > turn_amount: - new_hpr.x += turn_amount if h_diff > 0 else -turn_amount - else: - new_hpr.x = target_hpr.x - - if abs(p_diff) > turn_amount: - new_hpr.y += turn_amount if p_diff > 0 else -turn_amount - else: - new_hpr.y = target_hpr.y - - if abs(r_diff) > turn_amount: - new_hpr.z += turn_amount if r_diff > 0 else -turn_amount - else: - new_hpr.z = target_hpr.z - - self.world.cam.setHpr(new_hpr) - - # 计算本帧应移动的距离 - move_distance = self.patrol_speed * dt - - # 如果移动距离大于剩余距离,则直接移动到目标点 - if move_distance >= distance: - self.world.cam.setPos(target_pos) - else: - # 否则按方向移动 - new_pos = current_pos + direction * move_distance - self.world.cam.setPos(new_pos) - - def _handle_turning_to_target_state(self, target_hpr): - """处理转向目标状态""" - # 检查是否需要朝向下一个点 - if target_hpr == "look_next": - # 计算朝向下一个点的方向 - next_index = (self.current_patrol_index + 1) % len(self.patrol_points) - next_point_pos = self.patrol_points[next_index][0] - - current_pos = self.world.cam.getPos() - direction = next_point_pos - current_pos - direction.normalize() - - # 计算目标朝向 - target_hpr = self._look_at_to_hpr(direction) - - current_hpr = self.world.cam.getHpr() - - # 计算角度差 - h_diff = self._normalize_angle(target_hpr.x - current_hpr.x) - p_diff = self._normalize_angle(target_hpr.y - current_hpr.y) - r_diff = self._normalize_angle(target_hpr.z - current_hpr.z) - - # 检查是否已完成转向 - if abs(h_diff) < 1.0 and abs(p_diff) < 1.0 and abs(r_diff) < 1.0: - print(f"✓ 完成转向,开始停留") - self.patrol_state = "waiting" - self.patrol_wait_timer = 0.0 - return - - # 计算本帧应转动的角度 - dt = globalClock.getDt() - turn_amount = self.patrol_turn_speed * dt - - # 逐步转向目标角度 - new_hpr = Vec3(current_hpr) - - if abs(h_diff) > turn_amount: - new_hpr.x += turn_amount if h_diff > 0 else -turn_amount - else: - new_hpr.x = target_hpr.x - - if abs(p_diff) > turn_amount: - new_hpr.y += turn_amount if p_diff > 0 else -turn_amount - else: - new_hpr.y = target_hpr.y - - if abs(r_diff) > turn_amount: - new_hpr.z += turn_amount if r_diff > 0 else -turn_amount - else: - new_hpr.z = target_hpr.z - - self.world.cam.setHpr(new_hpr) - - def _handle_waiting_state(self, wait_time): - """处理等待状态""" - self.patrol_wait_timer += globalClock.getDt() - - if self.patrol_wait_timer >= wait_time: - print(f"✓ 停留结束,准备转回原朝向") - self.patrol_state = "turning_back" - - # 修改 core/patrol_system.py 中的 _handle_turning_back_state 方法 - - def _handle_turning_back_state(self): - """处理转回原朝向状态""" - # 直接完成转向状态,进入移动状态 - print(f"✓ 停留结束,开始移动到下一个点") - # 移动到下一个巡检点 - next_index = (self.current_patrol_index + 1) % len(self.patrol_points) - self.current_patrol_index = next_index - self.patrol_state = "moving" - return - - def _normalize_angle(self, angle): - """规范化角度到-180到180度之间""" - while angle > 180: - angle -= 360 - while angle < -180: - angle += 360 - return angle - - def _look_at_to_hpr(self, direction): - """将方向向量转换为HPR角度""" - # 简化的转换,实际应用中可能需要更精确的计算 - h = math.degrees(math.atan2(-direction.x, -direction.y)) - p = math.degrees(math.asin(direction.z)) - return Vec3(h, p, 0) - - def get_patrol_status(self): - """获取巡检状态信息""" - return { - "is_patrolling": self.is_patrolling, - "current_point": self.current_patrol_index, - "total_points": len(self.patrol_points), - "state": self.patrol_state, - "wait_timer": self.patrol_wait_timer - } - - def list_patrol_points(self): - """列出所有巡检点""" - if not self.patrol_points: - print("没有设置巡检点") - return - - print(f"巡检点列表 (共{len(self.patrol_points)}个):") - for i, (pos, hpr, wait_time) in enumerate(self.patrol_points): - current_marker = " >>>" if i == self.current_patrol_index and self.is_patrolling else "" - print(f" {i + 1}. 位置:({pos.x:.1f}, {pos.y:.1f}, {pos.z:.1f}) " - f"朝向:({hpr.x:.1f}, {hpr.y:.1f}, {hpr.z:.1f}) " - f"停留:{wait_time}秒{current_marker}") - - def remove_patrol_point(self, index): - """移除指定索引的巡检点""" - if 0 <= index < len(self.patrol_points): - removed_point = self.patrol_points.pop(index) - print( - f"✓ 移除巡检点 {index + 1}: 位置({removed_point[0].x:.1f}, {removed_point[0].y:.1f}, {removed_point[0].z:.1f})") - - # 调整当前索引 - if self.current_patrol_index >= len(self.patrol_points) and self.patrol_points: - self.current_patrol_index = len(self.patrol_points) - 1 - elif self.current_patrol_index >= len(self.patrol_points): - self.current_patrol_index = 0 - else: - print(f"✗ 无效的巡检点索引: {index}") - - def insert_patrol_point(self, index, position, heading=None, wait_time=3.0): - """在指定位置插入巡检点""" - if index < 0 or index > len(self.patrol_points): - print(f"✗ 无效的插入位置: {index}") - return - - if heading is None: - # 使用当前相机朝向 - current_hpr = self.world.cam.getHpr() - heading = (current_hpr.x, current_hpr.y, current_hpr.z) - - pos = Point3(position[0], position[1], position[2]) - hpr = Vec3(heading[0], heading[1], heading[2]) - - self.patrol_points.insert(index, (pos, hpr, wait_time)) - print(f"✓ 在位置 {index + 1} 插入巡检点: 位置{position}, 朝向{heading}, 停留{wait_time}秒") - - def update_patrol_point(self, index, position=None, heading=None, wait_time=None): - """更新指定巡检点的信息""" - if 0 <= index < len(self.patrol_points): - pos, hpr, wt = self.patrol_points[index] - - if position is not None: - pos = Point3(position[0], position[1], position[2]) - if heading is not None: - hpr = Vec3(heading[0], heading[1], heading[2]) - if wait_time is not None: - wt = wait_time - - self.patrol_points[index] = (pos, hpr, wt) - print(f"✓ 更新巡检点 {index + 1}") - else: - print(f"✗ 无效的巡检点索引: {index}") - - def goto_patrol_point(self, index): - """直接跳转到指定巡检点""" - if not self.patrol_points: - print("✗ 没有设置巡检点") - return False - - if 0 <= index < len(self.patrol_points): - pos, hpr, _ = self.patrol_points[index] - self.world.cam.setPos(pos) - self.world.cam.setHpr(hpr) - self.current_patrol_index = index - print(f"✓ 跳转到巡检点 {index + 1}") - return True - else: - print(f"✗ 无效的巡检点索引: {index}") - return False - - def cleanup(self): - """清理巡检系统资源""" - self.stop_patrol() - self.clear_patrol_points() - self.original_cam_pos = None - self.original_cam_hpr = None - print("✓ 巡检系统资源已清理") - - -# 使用示例和便捷函数 -def create_default_patrol_route(patrol_system): - """创建默认的巡检路线(示例)""" - # 清空现有巡检点 - patrol_system.clear_patrol_points() - - # 添加一些示例巡检点 - patrol_system.add_patrol_point((0, -20, 5), (0, -15, 0), 2.0) # 点1:前方低位置 - patrol_system.add_patrol_point((0, 0, 10), (0, -30, 0), 3.0) # 点2:中央高位置 - patrol_system.add_patrol_point((15, 10, 5), (-45, -10, 0), 2.5) # 点3:右侧位置 - patrol_system.add_patrol_point((-15, 10, 5), (45, -10, 0), 2.5) # 点4:左侧位置 - - print("✓ 默认巡检路线已创建") - +from direct.showbase.ShowBaseGlobal import globalClock +from direct.task.TaskManagerGlobal import taskMgr +from panda3d.core import Point3, Vec3 +import math + + +class PatrolSystem: + """巡检系统类""" + + def __init__(self, world): + """初始化巡检系统 + + Args: + world: 核心世界对象引用 + """ + self.world = world + + # 巡检状态 + self.is_patrolling = False + self.patrol_points = [] # 巡检点列表 [(pos, hpr, wait_time), ...] + self.current_patrol_index = 0 + self.patrol_task = None + + # 巡检参数 + self.patrol_speed = 5.0 # 巡检移动速度(单位/秒) + self.patrol_turn_speed = 90.0 # 转向速度(度/秒) + self.patrol_wait_timer = 0.0 + self.patrol_state = "moving" # "moving", "turning_to_target", "waiting", "turning_back" + + # 相机状态保存 + self.original_cam_pos = None + self.original_cam_hpr = None + + print("✓ 巡检系统初始化完成") + + def add_patrol_point(self, position, heading=None, wait_time=3.0): + if heading is None: + if self.patrol_points: + last_pos = self.patrol_points[-1][0] + direction_x = position[0] - last_pos.x + direction_y = position[1] - last_pos.y + direction_z = position[2] - last_pos.z + + import math + h=math.degrees(math.atan2(-direction_x,-direction_y)) + + distance_xy = math.sqrt(direction_x**2+direction_y**2) + p = math.degrees(math.atan2(direction_z,distance_xy)) + p = max(-89,min(89,p)) + + r=0 + + heading = (h,p,r) + + else: + # 使用当前相机朝向 + current_hpr = self.world.cam.getHpr() + heading = (current_hpr.x, current_hpr.y, current_hpr.z) + + pos = Point3(position[0], position[1], position[2]) + hpr = Vec3(heading[0], heading[1], heading[2]) + + self.patrol_points.append((pos, hpr, wait_time)) + print(f"✓ 添加巡检点 {len(self.patrol_points)}: 位置{position}, 朝向{heading}, 停留{wait_time}秒") + + # 在 PatrolSystem 类中添加以下方法 + + def add_auto_heading_patrol_point(self, position, wait_time=3.0): + """添加自动计算朝向的巡检点(朝向路径前进方向) + + Args: + position: 相机位置 (x, y, z) + wait_time: 在该点停留时间(秒) + """ + heading = None # 将自动计算朝向 + + # 复用原有的 add_patrol_point 方法 + self.add_patrol_point(position, heading, wait_time) + + def add_patrol_point_looking_at(self, position, look_at_position, wait_time=3.0): + """添加朝向指定位置的巡检点 + + Args: + position: 相机位置 (x, y, z) + look_at_position: 相机朝向的目标位置 (x, y, z) + wait_time: 在该点停留时间(秒) + """ + import math + + # 计算从当前位置到目标位置的方向向量 + direction_x = look_at_position[0] - position[0] + direction_y = look_at_position[1] - position[1] + direction_z = look_at_position[2] - position[2] + + # 计算HPR朝向 + h = math.degrees(math.atan2(-direction_x, -direction_y)) + + distance_xy = math.sqrt(direction_x ** 2 + direction_y ** 2) + p = math.degrees(math.atan2(direction_z, distance_xy)) + p = max(-89, min(89, p)) # 限制pitch角度在合理范围内 + + r = 0 # roll通常为0 + + heading = (h, p, r) + self.add_patrol_point(position, heading, wait_time) + + def clear_patrol_points(self): + """清空所有巡检点""" + self.patrol_points = [] + print("✓ 巡检点已清空") + + def set_patrol_speed(self, move_speed, turn_speed=None): + """设置巡检速度 + + Args: + move_speed: 移动速度(单位/秒) + turn_speed: 转向速度(度/秒),如果为None则保持当前值 + """ + self.patrol_speed = move_speed + if turn_speed is not None: + self.patrol_turn_speed = turn_speed + print(f"✓ 巡检速度已设置: 移动{move_speed}, 转向{turn_speed or self.patrol_turn_speed}") + + def start_patrol(self): + """开始巡检""" + if not self.patrol_points: + print("✗ 没有设置巡检点,无法开始巡检") + return False + + if self.is_patrolling: + print("⚠ 巡检已在进行中") + return True + + # 保存当前相机状态 + self.original_cam_pos = Point3(self.world.cam.getPos()) + self.original_cam_hpr = Vec3(self.world.cam.getHpr()) + + # 重置巡检状态 + self.current_patrol_index = 0 + self.patrol_state = "moving" + self.patrol_wait_timer = 0.0 + self.is_patrolling = True + + # 启动巡检任务 + if self.patrol_task: + taskMgr.remove(self.patrol_task) + self.patrol_task = taskMgr.add(self._patrol_task, "patrol_task") + + print(f"✓ 开始巡检,共{len(self.patrol_points)}个巡检点") + return True + + def stop_patrol(self): + """停止巡检""" + if not self.is_patrolling: + print("⚠ 巡检未在进行中") + return False + + # 停止巡检任务 + if self.patrol_task: + taskMgr.remove(self.patrol_task) + self.patrol_task = None + + self.is_patrolling = False + self.patrol_state = "moving" + self.patrol_wait_timer = 0.0 + + print("✓ 巡检已停止") + return True + + def pause_patrol(self): + """暂停巡检""" + if not self.is_patrolling: + print("⚠ 巡检未在进行中") + return False + + if self.patrol_task: + taskMgr.remove(self.patrol_task) + self.patrol_task = None + + print("✓ 巡检已暂停") + return True + + def resume_patrol(self): + """恢复巡检""" + if self.is_patrolling: + print("⚠ 巡检已在进行中") + return False + + if not self.patrol_points: + print("✗ 没有设置巡检点") + return False + + self.is_patrolling = True + self.patrol_task = taskMgr.add(self._patrol_task, "patrol_task") + + print("✓ 巡检已恢复") + return True + + def reset_to_original_position(self): + """重置相机到原始位置""" + if self.original_cam_pos and self.original_cam_hpr: + self.world.cam.setPos(self.original_cam_pos) + self.world.cam.setHpr(self.original_cam_hpr) + print("✓ 相机已重置到原始位置") + return True + else: + print("✗ 没有保存的原始位置") + return False + + def _patrol_task(self, task): + """巡检主任务""" + try: + if not self.is_patrolling or not self.patrol_points: + return task.done + + # 获取当前巡检点 + current_point = self.patrol_points[self.current_patrol_index] + target_pos, target_hpr, wait_time = current_point + + # 根据当前状态执行不同操作 + if self.patrol_state == "moving": + self._handle_moving_state(target_pos) + elif self.patrol_state == "turning_to_target": + self._handle_turning_to_target_state(target_hpr) + elif self.patrol_state == "waiting": + self._handle_waiting_state(wait_time) + elif self.patrol_state == "turning_back": + self._handle_turning_back_state() + + return task.cont + + except Exception as e: + print(f"巡检任务出错: {e}") + import traceback + traceback.print_exc() + return task.done + + def _handle_moving_state(self, target_pos): + """处理移动状态""" + current_pos = self.world.cam.getPos() + distance = (target_pos - current_pos).length() + + if distance < 0.1: # 到达目标点 + print(f"✓ 到达巡检点 {self.current_patrol_index + 1}") + self.patrol_state = "turning_to_target" + return + + # 计算移动方向和距离 + direction = target_pos - current_pos + direction.normalize() + + # 计算目标朝向(看向目标点) + target_hpr = self._look_at_to_hpr(direction) + current_hpr = self.world.cam.getHpr() + + # 平滑转向到目标朝向 + h_diff = self._normalize_angle(target_hpr.x - current_hpr.x) + p_diff = self._normalize_angle(target_hpr.y - current_hpr.y) + r_diff = self._normalize_angle(target_hpr.z - current_hpr.z) + + # 计算本帧应转动的角度 + dt = globalClock.getDt() + turn_amount = self.patrol_turn_speed * dt + + # 逐步转向目标角度 + new_hpr = Vec3(current_hpr) + + if abs(h_diff) > turn_amount: + new_hpr.x += turn_amount if h_diff > 0 else -turn_amount + else: + new_hpr.x = target_hpr.x + + if abs(p_diff) > turn_amount: + new_hpr.y += turn_amount if p_diff > 0 else -turn_amount + else: + new_hpr.y = target_hpr.y + + if abs(r_diff) > turn_amount: + new_hpr.z += turn_amount if r_diff > 0 else -turn_amount + else: + new_hpr.z = target_hpr.z + + self.world.cam.setHpr(new_hpr) + + # 计算本帧应移动的距离 + move_distance = self.patrol_speed * dt + + # 如果移动距离大于剩余距离,则直接移动到目标点 + if move_distance >= distance: + self.world.cam.setPos(target_pos) + else: + # 否则按方向移动 + new_pos = current_pos + direction * move_distance + self.world.cam.setPos(new_pos) + + def _handle_turning_to_target_state(self, target_hpr): + """处理转向目标状态""" + # 检查是否需要朝向下一个点 + if target_hpr == "look_next": + # 计算朝向下一个点的方向 + next_index = (self.current_patrol_index + 1) % len(self.patrol_points) + next_point_pos = self.patrol_points[next_index][0] + + current_pos = self.world.cam.getPos() + direction = next_point_pos - current_pos + direction.normalize() + + # 计算目标朝向 + target_hpr = self._look_at_to_hpr(direction) + + current_hpr = self.world.cam.getHpr() + + # 计算角度差 + h_diff = self._normalize_angle(target_hpr.x - current_hpr.x) + p_diff = self._normalize_angle(target_hpr.y - current_hpr.y) + r_diff = self._normalize_angle(target_hpr.z - current_hpr.z) + + # 检查是否已完成转向 + if abs(h_diff) < 1.0 and abs(p_diff) < 1.0 and abs(r_diff) < 1.0: + print(f"✓ 完成转向,开始停留") + self.patrol_state = "waiting" + self.patrol_wait_timer = 0.0 + return + + # 计算本帧应转动的角度 + dt = globalClock.getDt() + turn_amount = self.patrol_turn_speed * dt + + # 逐步转向目标角度 + new_hpr = Vec3(current_hpr) + + if abs(h_diff) > turn_amount: + new_hpr.x += turn_amount if h_diff > 0 else -turn_amount + else: + new_hpr.x = target_hpr.x + + if abs(p_diff) > turn_amount: + new_hpr.y += turn_amount if p_diff > 0 else -turn_amount + else: + new_hpr.y = target_hpr.y + + if abs(r_diff) > turn_amount: + new_hpr.z += turn_amount if r_diff > 0 else -turn_amount + else: + new_hpr.z = target_hpr.z + + self.world.cam.setHpr(new_hpr) + + def _handle_waiting_state(self, wait_time): + """处理等待状态""" + self.patrol_wait_timer += globalClock.getDt() + + if self.patrol_wait_timer >= wait_time: + print(f"✓ 停留结束,准备转回原朝向") + self.patrol_state = "turning_back" + + # 修改 core/patrol_system.py 中的 _handle_turning_back_state 方法 + + def _handle_turning_back_state(self): + """处理转回原朝向状态""" + # 直接完成转向状态,进入移动状态 + print(f"✓ 停留结束,开始移动到下一个点") + # 移动到下一个巡检点 + next_index = (self.current_patrol_index + 1) % len(self.patrol_points) + self.current_patrol_index = next_index + self.patrol_state = "moving" + return + + def _normalize_angle(self, angle): + """规范化角度到-180到180度之间""" + while angle > 180: + angle -= 360 + while angle < -180: + angle += 360 + return angle + + def _look_at_to_hpr(self, direction): + """将方向向量转换为HPR角度""" + # 简化的转换,实际应用中可能需要更精确的计算 + h = math.degrees(math.atan2(-direction.x, -direction.y)) + p = math.degrees(math.asin(direction.z)) + return Vec3(h, p, 0) + + def get_patrol_status(self): + """获取巡检状态信息""" + return { + "is_patrolling": self.is_patrolling, + "current_point": self.current_patrol_index, + "total_points": len(self.patrol_points), + "state": self.patrol_state, + "wait_timer": self.patrol_wait_timer + } + + def list_patrol_points(self): + """列出所有巡检点""" + if not self.patrol_points: + print("没有设置巡检点") + return + + print(f"巡检点列表 (共{len(self.patrol_points)}个):") + for i, (pos, hpr, wait_time) in enumerate(self.patrol_points): + current_marker = " >>>" if i == self.current_patrol_index and self.is_patrolling else "" + print(f" {i + 1}. 位置:({pos.x:.1f}, {pos.y:.1f}, {pos.z:.1f}) " + f"朝向:({hpr.x:.1f}, {hpr.y:.1f}, {hpr.z:.1f}) " + f"停留:{wait_time}秒{current_marker}") + + def remove_patrol_point(self, index): + """移除指定索引的巡检点""" + if 0 <= index < len(self.patrol_points): + removed_point = self.patrol_points.pop(index) + print( + f"✓ 移除巡检点 {index + 1}: 位置({removed_point[0].x:.1f}, {removed_point[0].y:.1f}, {removed_point[0].z:.1f})") + + # 调整当前索引 + if self.current_patrol_index >= len(self.patrol_points) and self.patrol_points: + self.current_patrol_index = len(self.patrol_points) - 1 + elif self.current_patrol_index >= len(self.patrol_points): + self.current_patrol_index = 0 + else: + print(f"✗ 无效的巡检点索引: {index}") + + def insert_patrol_point(self, index, position, heading=None, wait_time=3.0): + """在指定位置插入巡检点""" + if index < 0 or index > len(self.patrol_points): + print(f"✗ 无效的插入位置: {index}") + return + + if heading is None: + # 使用当前相机朝向 + current_hpr = self.world.cam.getHpr() + heading = (current_hpr.x, current_hpr.y, current_hpr.z) + + pos = Point3(position[0], position[1], position[2]) + hpr = Vec3(heading[0], heading[1], heading[2]) + + self.patrol_points.insert(index, (pos, hpr, wait_time)) + print(f"✓ 在位置 {index + 1} 插入巡检点: 位置{position}, 朝向{heading}, 停留{wait_time}秒") + + def update_patrol_point(self, index, position=None, heading=None, wait_time=None): + """更新指定巡检点的信息""" + if 0 <= index < len(self.patrol_points): + pos, hpr, wt = self.patrol_points[index] + + if position is not None: + pos = Point3(position[0], position[1], position[2]) + if heading is not None: + hpr = Vec3(heading[0], heading[1], heading[2]) + if wait_time is not None: + wt = wait_time + + self.patrol_points[index] = (pos, hpr, wt) + print(f"✓ 更新巡检点 {index + 1}") + else: + print(f"✗ 无效的巡检点索引: {index}") + + def goto_patrol_point(self, index): + """直接跳转到指定巡检点""" + if not self.patrol_points: + print("✗ 没有设置巡检点") + return False + + if 0 <= index < len(self.patrol_points): + pos, hpr, _ = self.patrol_points[index] + self.world.cam.setPos(pos) + self.world.cam.setHpr(hpr) + self.current_patrol_index = index + print(f"✓ 跳转到巡检点 {index + 1}") + return True + else: + print(f"✗ 无效的巡检点索引: {index}") + return False + + def cleanup(self): + """清理巡检系统资源""" + self.stop_patrol() + self.clear_patrol_points() + self.original_cam_pos = None + self.original_cam_hpr = None + print("✓ 巡检系统资源已清理") + + +# 使用示例和便捷函数 +def create_default_patrol_route(patrol_system): + """创建默认的巡检路线(示例)""" + # 清空现有巡检点 + patrol_system.clear_patrol_points() + + # 添加一些示例巡检点 + patrol_system.add_patrol_point((0, -20, 5), (0, -15, 0), 2.0) # 点1:前方低位置 + patrol_system.add_patrol_point((0, 0, 10), (0, -30, 0), 3.0) # 点2:中央高位置 + patrol_system.add_patrol_point((15, 10, 5), (-45, -10, 0), 2.5) # 点3:右侧位置 + patrol_system.add_patrol_point((-15, 10, 5), (45, -10, 0), 2.5) # 点4:左侧位置 + + print("✓ 默认巡检路线已创建") + diff --git a/core/script_system.py b/core/script_system.py index d9aa465c..89b1073e 100644 --- a/core/script_system.py +++ b/core/script_system.py @@ -1,858 +1,858 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -脚本系统模块 - -负责脚本的创建、管理、挂载和运行: -- 脚本管理器:统一管理所有脚本 -- 脚本组件:挂载到游戏对象的脚本实例 -- 脚本引擎:执行脚本逻辑 -- 脚本API:提供给脚本使用的API接口 -""" - -import os -import sys -import importlib -import importlib.util -import traceback -import inspect -import time -from typing import Dict, List, Any, Optional, Callable -from abc import ABC, abstractmethod -from direct.task.TaskManagerGlobal import taskMgr -from panda3d.core import PythonTask - - -class ScriptBase(ABC): - """脚本基类 - 所有用户脚本都应该继承此类""" - - def __init__(self): - self.enabled = True - self.transform = None # 挂载的对象的transform - self.gameObject = None # 挂载的游戏对象 - self.world = None # 引擎世界对象引用 - self._script_id = None - - @abstractmethod - def start(self): - """脚本开始时调用(类似Unity的Start)""" - pass - - @abstractmethod - def update(self, dt): - """每帧更新时调用(类似Unity的Update)""" - pass - - def on_destroy(self): - """脚本销毁时调用(类似Unity的OnDestroy)""" - pass - - def on_enable(self): - """脚本启用时调用""" - pass - - def on_disable(self): - """脚本禁用时调用""" - pass - - def on_collision_enter(self, other): - """碰撞开始时调用""" - pass - - def on_collision_exit(self, other): - """碰撞结束时调用""" - pass - - def log(self, message): - """日志输出""" - print(f"[{self.__class__.__name__}] {message}") - - -class ScriptComponent: - """脚本组件 - 挂载到游戏对象上的脚本实例""" - - def __init__(self, script_instance: ScriptBase, game_object, script_manager): - self.script_instance = script_instance - self.game_object = game_object - self.script_manager = script_manager - self.enabled = True - - # 保存脚本名称,便于UI显示 - self.script_name = script_instance.__class__.__name__ - - # 设置脚本实例的引用 - script_instance.gameObject = game_object - script_instance.transform = game_object # Panda3D中NodePath就是transform - script_instance.world = script_manager.world - script_instance._script_id = id(self) - - # 标记脚本已开始 - self._started = False - - def start(self): - """启动脚本""" - if not self._started and self.enabled: - try: - self.script_instance.start() - self._started = True - except Exception as e: - print(f"脚本启动失败: {e}") - traceback.print_exc() - - def update(self, dt): - """更新脚本""" - if self.enabled and self._started: - try: - self.script_instance.update(dt) - except Exception as e: - print(f"脚本更新失败: {e}") - traceback.print_exc() - - def destroy(self): - """销毁脚本""" - try: - self.script_instance.on_destroy() - except Exception as e: - print(f"脚本销毁失败: {e}") - traceback.print_exc() - - def set_enabled(self, enabled): - """设置脚本启用状态""" - if self.enabled != enabled: - self.enabled = enabled - try: - if enabled: - self.script_instance.on_enable() - else: - self.script_instance.on_disable() - except Exception as e: - print(f"设置脚本状态失败: {e}") - traceback.print_exc() - - -class ScriptEngine: - """脚本引擎 - 负责脚本的执行和生命周期管理""" - - def __init__(self, world): - self.world = world - self.script_components: List[ScriptComponent] = [] - self.update_task = None - - def start_engine(self): - """启动脚本引擎""" - if self.update_task is None: - self.update_task = taskMgr.add(self._update_scripts, "script_update") - print("✓ 脚本引擎已启动") - - def stop_engine(self): - """停止脚本引擎""" - if self.update_task: - taskMgr.remove(self.update_task) - self.update_task = None - print("✓ 脚本引擎已停止") - - def add_script_component(self, script_component: ScriptComponent): - """添加脚本组件""" - self.script_components.append(script_component) - # 如果引擎已运行,立即启动脚本 - if self.update_task: - script_component.start() - - def remove_script_component(self, script_component: ScriptComponent): - """移除脚本组件""" - if script_component in self.script_components: - script_component.destroy() - self.script_components.remove(script_component) - - def _update_scripts(self, task): - """更新所有脚本(每帧调用)""" - from direct.showbase.ShowBaseGlobal import globalClock - dt = globalClock.getDt() - - # 复制列表以避免迭代时修改 - components_copy = self.script_components.copy() - - for component in components_copy: - if component.enabled: - # 如果脚本还没开始,先调用start - if not component._started: - component.start() - # 然后调用update - component.update(dt) - - return task.cont - - -class ScriptLoader: - """脚本加载器 - 负责加载和重载脚本""" - - def __init__(self, script_manager): - self.script_manager = script_manager - self.loaded_modules = {} # 模块名 -> 模块对象 - self.script_classes = {} # 脚本名 -> 脚本类 - self.file_mtimes = {} # 文件路径 -> 修改时间 - - def load_script_from_file(self, script_path: str) -> Optional[type]: - """从文件加载脚本类""" - try: - if not os.path.exists(script_path): - print(f"脚本文件不存在: {script_path}") - return None - - # 获取脚本名称(不包含扩展名) - script_name = os.path.splitext(os.path.basename(script_path))[0] - - # 动态导入模块 - spec = importlib.util.spec_from_file_location(script_name, script_path) - if spec is None: - print(f"无法创建模块规范: {script_path}") - return None - - module = importlib.util.module_from_spec(spec) - - # 如果模块已经加载过,先卸载 - if script_name in self.loaded_modules: - self.unload_script(script_name) - - # 执行模块 - spec.loader.exec_module(module) - - # 查找继承自ScriptBase的类 - script_class = None - for name, obj in inspect.getmembers(module, inspect.isclass): - if issubclass(obj, ScriptBase) and obj != ScriptBase: - script_class = obj - break - - if script_class is None: - print(f"脚本文件中没有找到继承自ScriptBase的类: {script_path}") - return None - - # 保存模块和类信息 - self.loaded_modules[script_name] = module - self.script_classes[script_name] = script_class - self.file_mtimes[script_path] = os.path.getmtime(script_path) - - print(f"✓ 成功加载脚本: {script_name} 从 {script_path}") - return script_class - - except Exception as e: - print(f"加载脚本失败 {script_path}: {e}") - traceback.print_exc() - return None - - def unload_script(self, script_name: str): - """卸载脚本""" - if script_name in self.loaded_modules: - # 移除所有使用此脚本的组件 - components_to_remove = [] - for component in self.script_manager.engine.script_components: - if component.script_instance.__class__.__name__ == script_name: - components_to_remove.append(component) - - for component in components_to_remove: - self.script_manager.remove_script_from_object(component.game_object, script_name) - - # 从sys.modules中移除 - module = self.loaded_modules[script_name] - if module.__name__ in sys.modules: - del sys.modules[module.__name__] - - # 清理引用 - del self.loaded_modules[script_name] - if script_name in self.script_classes: - del self.script_classes[script_name] - - print(f"✓ 脚本已卸载: {script_name}") - - def reload_script(self, script_path: str) -> Optional[type]: - """重新加载脚本(热重载)""" - script_name = os.path.splitext(os.path.basename(script_path))[0] - print(f"重新加载脚本: {script_name}") - - # 先卸载旧版本 - if script_name in self.loaded_modules: - self.unload_script(script_name) - - # 重新加载 - return self.load_script_from_file(script_path) - - def check_for_changes(self): - """检查脚本文件是否有变化(用于热重载)""" - changed_scripts = [] - - for script_path, old_mtime in self.file_mtimes.items(): - if os.path.exists(script_path): - current_mtime = os.path.getmtime(script_path) - if current_mtime > old_mtime: - changed_scripts.append(script_path) - - # 重新加载变化的脚本 - for script_path in changed_scripts: - self.reload_script(script_path) - - return len(changed_scripts) > 0 - - def find_script_file(self, script_name: str) -> Optional[str]: - """根据脚本名称查找脚本文件路径""" - # 首先检查已加载的脚本 - if script_name in self.loaded_modules: - module = self.loaded_modules[script_name] - if hasattr(module, '__file__') and module.__file__: - return module.__file__ - - # 在已知的文件路径中查找 - for file_path in self.file_mtimes.keys(): - file_name = os.path.splitext(os.path.basename(file_path))[0] - if file_name == script_name: - return file_path - - # 在脚本目录中查找 - scripts_dir = self.script_manager.scripts_directory - if os.path.exists(scripts_dir): - for file_name in os.listdir(scripts_dir): - if file_name.endswith('.py'): - base_name = os.path.splitext(file_name)[0] - if base_name == script_name: - return os.path.join(scripts_dir, file_name) - - return None - - -class ScriptAPI: - """脚本API - 提供给脚本使用的API接口""" - - def __init__(self, world): - self.world = world - - # ==================== 游戏对象操作 ==================== - - def find_object_by_name(self, name: str): - """根据名称查找游戏对象""" - return self.world.render.find(name) - - def create_object(self, name: str = "GameObject"): - """创建游戏对象""" - obj = self.world.render.attachNewNode(name) - return obj - - def destroy_object(self, obj): - """销毁游戏对象""" - if obj: - obj.removeNode() - - # ==================== 变换操作 ==================== - - def get_position(self, obj): - """获取对象位置""" - return obj.getPos() if obj else None - - def set_position(self, obj, x, y, z): - """设置对象位置""" - if obj: - obj.setPos(x, y, z) - - def get_rotation(self, obj): - """获取对象旋转""" - return obj.getHpr() if obj else None - - def set_rotation(self, obj, h, p, r): - """设置对象旋转""" - if obj: - obj.setHpr(h, p, r) - - def get_scale(self, obj): - """获取对象缩放""" - return obj.getScale() if obj else None - - def set_scale(self, obj, sx, sy, sz): - """设置对象缩放""" - if obj: - obj.setScale(sx, sy, sz) - - # ==================== 输入系统 ==================== - - def is_key_pressed(self, key): - """检查按键是否被按下""" - # 这里需要集成到现有的输入系统 - return False # 暂时返回False - - # ==================== 时间系统 ==================== - - def get_time(self): - """获取游戏时间""" - return time.time() - - def get_delta_time(self): - """获取帧间隔时间""" - from direct.showbase.ShowBaseGlobal import globalClock - return globalClock.getDt() - - # ==================== 日志系统 ==================== - - def log(self, message): - """输出日志""" - print(f"[ScriptAPI] {message}") - - -class ScriptManager: - """脚本管理器 - 统一管理所有脚本功能""" - - def __init__(self, world): - """初始化脚本管理器 - - Args: - world: 主程序world对象引用 - """ - self.world = world - - # 初始化子系统 - self.engine = ScriptEngine(world) - self.loader = ScriptLoader(self) - self.api = ScriptAPI(world) - - # 脚本存储 - self.object_scripts: Dict[Any, List[ScriptComponent]] = {} # 对象 -> 脚本组件列表 - self.script_templates: Dict[str, type] = {} # 脚本名 -> 脚本类 - - # 脚本目录 - self.scripts_directory = "scripts" - self._ensure_scripts_directory() - - # 热重载监控 - self.hot_reload_enabled = True - self.hot_reload_task = None - - print("✓ 脚本管理系统初始化完成") - - def _ensure_scripts_directory(self): - """确保脚本目录存在""" - if not os.path.exists(self.scripts_directory): - os.makedirs(self.scripts_directory) - - # 创建示例脚本 - self._create_example_script() - - def _create_example_script(self): - """创建示例脚本""" - example_script = '''#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -示例脚本 - 演示如何编写脚本 -""" - -from core.script_system import ScriptBase - -class ExampleScript(ScriptBase): - """示例脚本类""" - - def __init__(self): - super().__init__() - self.counter = 0 - self.rotation_speed = 30.0 # 度/秒 - - def start(self): - """脚本开始时调用""" - self.log("示例脚本开始运行!") - self.log(f"挂载到对象: {self.gameObject.getName()}") - - def update(self, dt): - """每帧更新""" - self.counter += 1 - - # 每60帧输出一次信息 - if self.counter % 60 == 0: - self.log(f"运行了 {self.counter} 帧") - - # 让对象旋转 - if self.transform: - current_h = self.transform.getH() - new_h = current_h + self.rotation_speed * dt - self.transform.setH(new_h) - - def on_destroy(self): - """脚本销毁时调用""" - self.log("示例脚本被销毁") - - def on_enable(self): - """脚本启用时调用""" - self.log("示例脚本被启用") - - def on_disable(self): - """脚本禁用时调用""" - self.log("示例脚本被禁用") -''' - - example_path = os.path.join(self.scripts_directory, "example_script.py") - with open(example_path, 'w', encoding='utf-8') as f: - f.write(example_script) - - print(f"✓ 创建示例脚本: {example_path}") - - # ==================== 脚本管理功能 ==================== - - def start_system(self): - """启动脚本系统""" - self.engine.start_engine() - - if self.hot_reload_enabled: - self.start_hot_reload() - - print("✓ 脚本系统已启动") - - def stop_system(self): - """停止脚本系统""" - self.engine.stop_engine() - self.stop_hot_reload() - print("✓ 脚本系统已停止") - - def start_hot_reload(self): - """启动热重载监控""" - if self.hot_reload_task is None: - self.hot_reload_task = taskMgr.add(self._check_hot_reload, "script_hot_reload") - print("✓ 脚本热重载监控已启动") - - def stop_hot_reload(self): - """停止热重载监控""" - if self.hot_reload_task: - taskMgr.remove(self.hot_reload_task) - self.hot_reload_task = None - print("✓ 脚本热重载监控已停止") - - def _check_hot_reload(self, task): - """检查热重载(每秒调用一次)""" - self.loader.check_for_changes() - task.delayTime = 1.0 # 1秒后再次调用 - return task.again - - # ==================== 脚本创建和加载 ==================== - - def create_script_file(self, script_name: str, template: str = "basic") -> str: - """创建新的脚本文件""" - script_path = os.path.join(self.scripts_directory, f"{script_name}.py") - - if os.path.exists(script_path): - print(f"脚本文件已存在: {script_path}") - return script_path - - # 根据模板创建脚本 - if template == "basic": - script_content = self._get_basic_script_template(script_name) - elif template == "movement": - script_content = self._get_movement_script_template(script_name) - else: - script_content = self._get_basic_script_template(script_name) - - with open(script_path, 'w', encoding='utf-8') as f: - f.write(script_content) - - print(f"✓ 创建脚本文件: {script_path}") - return script_path - - def _get_basic_script_template(self, script_name: str) -> str: - """获取基础脚本模板""" - class_name = ''.join(word.capitalize() for word in script_name.split('_')) - - return f'''#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -{script_name} - 自定义脚本 -""" - -from core.script_system import ScriptBase - -class {class_name}(ScriptBase): - """自定义脚本类""" - - def __init__(self): - super().__init__() - # 在这里初始化您的变量 - - def start(self): - """脚本开始时调用""" - self.log("脚本开始运行!") - - def update(self, dt): - """每帧更新""" - # 在这里编写更新逻辑 - pass - - def on_destroy(self): - """脚本销毁时调用""" - self.log("脚本被销毁") -''' - - def _get_movement_script_template(self, script_name: str) -> str: - """获取移动脚本模板""" - class_name = ''.join(word.capitalize() for word in script_name.split('_')) - - return f'''#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -{script_name} - 移动脚本 -""" - -from core.script_system import ScriptBase - -class {class_name}(ScriptBase): - """移动脚本类""" - - def __init__(self): - super().__init__() - self.speed = 5.0 # 移动速度 - self.direction = [1, 0, 0] # 移动方向 - - def start(self): - """脚本开始时调用""" - self.log("移动脚本开始运行!") - - def update(self, dt): - """每帧更新""" - if self.transform: - # 计算移动偏移 - offset_x = self.direction[0] * self.speed * dt - offset_y = self.direction[1] * self.speed * dt - offset_z = self.direction[2] * self.speed * dt - - # 更新位置 - current_pos = self.transform.getPos() - new_pos = ( - current_pos.x + offset_x, - current_pos.y + offset_y, - current_pos.z + offset_z - ) - self.transform.setPos(*new_pos) - - def on_destroy(self): - """脚本销毁时调用""" - self.log("移动脚本被销毁") -''' - - def load_script_from_file(self, script_path: str) -> Optional[type]: - """从文件加载脚本""" - return self.loader.load_script_from_file(script_path) - - def load_all_scripts_from_directory(self, directory: str = None) -> List[str]: - """从目录加载所有脚本""" - if directory is None: - directory = self.scripts_directory - - if not os.path.exists(directory): - print(f"脚本目录不存在: {directory}") - return [] - - loaded_scripts = [] - for filename in os.listdir(directory): - if filename.endswith('.py') and not filename.startswith('__'): - script_path = os.path.join(directory, filename) - script_class = self.load_script_from_file(script_path) - if script_class: - script_name = os.path.splitext(filename)[0] - loaded_scripts.append(script_name) - - print(f"✓ 从目录 {directory} 加载了 {len(loaded_scripts)} 个脚本") - return loaded_scripts - - # ==================== 脚本挂载和管理 ==================== - - def add_script_to_object(self, game_object, script_name: str) -> Optional[ScriptComponent]: - """为对象添加脚本""" - # 查找脚本类 - script_class = self.loader.script_classes.get(script_name) - if script_class is None: - print(f"未找到脚本类: {script_name}") - return None - - try: - # 创建脚本实例 - script_instance = script_class() - - # 创建脚本组件 - script_component = ScriptComponent(script_instance, game_object, self) - - # 添加到对象的脚本列表 - if game_object not in self.object_scripts: - self.object_scripts[game_object] = [] - self.object_scripts[game_object].append(script_component) - - # 添加到脚本引擎 - self.engine.add_script_component(script_component) - - print(f"✓ 为对象 {game_object.getName()} 添加脚本: {script_name}") - return script_component - - except Exception as e: - print(f"添加脚本失败: {e}") - traceback.print_exc() - return None - - def remove_script_from_object(self, game_object, script_name: str) -> bool: - """从游戏对象移除脚本""" - if game_object not in self.object_scripts: - return False - - script_components = self.object_scripts[game_object] - removed = False - - for component in script_components[:]: # 复制列表以避免修改时出错 - if component.script_instance.__class__.__name__ == script_name: - # 从引擎移除 - self.engine.remove_script_component(component) - # 从对象脚本列表移除 - script_components.remove(component) - removed = True - - print(f"✓ 从对象 {game_object.getName()} 移除脚本: {script_name}") - - - if not script_components: - del self.object_scripts[game_object] - - # 更新节点上保存的脚本信息标签 - if removed: - self._update_node_script_tags_after_removal(game_object, script_name) - - return removed - - def _update_node_script_tags_after_removal(self, game_object, removed_script_name): - """在移除脚本后更新节点标签""" - try: - # 获取对象上剩余的脚本 - remaining_scripts = self.get_scripts_on_object(game_object) - - if not remaining_scripts: - # 如果没有其他脚本,清除所有脚本标签 - if game_object.hasTag("has_scripts"): - game_object.clearTag("has_scripts") - if game_object.hasTag("scripts_info"): - game_object.clearTag("scripts_info") - print(f"✓ 清除节点 {game_object.getName()} 的所有脚本标签") - else: - # 如果还有其他脚本,更新脚本信息标签 - script_info_list = [] - for script_component in remaining_scripts: - script_name = script_component.script_name - script_class = script_component.script_instance.__class__ - script_file = self.loader.find_script_file(script_name) or "" - - script_info_list.append({ - "name": script_name, - "file": script_file - }) - - import json - game_object.setTag("has_scripts", "true") - game_object.setTag("scripts_info", json.dumps(script_info_list, ensure_ascii=False)) - print(f"✓ 更新节点 {game_object.getName()} 的脚本标签信息,剩余 {len(script_info_list)} 个脚本") - - except Exception as e: - print(f"更新节点标签失败: {e}") - - def get_scripts_on_object(self, game_object) -> List[ScriptComponent]: - """获取对象上的所有脚本""" - return self.object_scripts.get(game_object, []) - - def get_script_on_object(self, game_object, script_name: str) -> Optional[ScriptComponent]: - """获取对象上的特定脚本""" - scripts = self.get_scripts_on_object(game_object) - for script in scripts: - if script.script_instance.__class__.__name__ == script_name: - return script - return None - - # ==================== 脚本信息查询 ==================== - - def get_available_scripts(self) -> List[str]: - """获取所有可用的脚本名称""" - return list(self.loader.script_classes.keys()) - - def get_script_info(self, script_name: str) -> Optional[Dict[str, Any]]: - """获取脚本信息""" - script_class = self.loader.script_classes.get(script_name) - if script_class is None: - return None - - return { - "name": script_name, - "class": script_class, - "doc": script_class.__doc__, - "file": inspect.getfile(script_class) if hasattr(script_class, '__file__') else None, - "methods": [method for method in dir(script_class) if not method.startswith('_')] - } - - def reload_script(self, script_name: str) -> bool: - """重新加载脚本""" - script_info = self.get_script_info(script_name) - if script_info and script_info["file"]: - return self.loader.reload_script(script_info["file"]) is not None - return False - - # ==================== 调试功能 ==================== - - def list_all_scripts(self): - """列出所有脚本信息""" - print("\n=== 脚本系统状态 ===") - print(f"可用脚本数量: {len(self.loader.script_classes)}") - print(f"运行中的脚本组件数量: {len(self.engine.script_components)}") - print(f"有脚本的对象数量: {len(self.object_scripts)}") - - if self.loader.script_classes: - print("\n可用脚本:") - for script_name in self.loader.script_classes: - print(f" - {script_name}") - - if self.object_scripts: - print("\n对象脚本分布:") - for obj, scripts in self.object_scripts.items(): - script_names = [s.script_instance.__class__.__name__ for s in scripts] - print(f" - {obj.getName()}: {script_names}") - - print("==================\n") - - def save_object_scripts(self,game_object,node_data:dict): - try: - if game_object in self.object_scripts: - scripts_data = [] - for script_commponent in self.object_scripts[game_object]: - script_info = { - 'script_name':script_commponent.script_name, - 'enabled':script_commponent.enabled, - 'script_class':script_commponent.script_instance.__class__.__name__ - } - scripts_data.append(script_info) - if scripts_data: - node_data['scripts'] = scripts_data - print(f"✓ 保存了 {len(scripts_data)} 个脚本到对象 {game_object.getName()}") - except Exception as e: - print(f"保存对象脚本信息失败: {e}") - traceback.print_exc() - - # def restore_object_scripts(self,game_object,node_data:dict): - # try: - # if 'scripts' in node_data: - # scripts_data = node_data['scripts'] - # restored_count = 0 - # for script_info in scripts_data: - # script_name = script_info.get('script_name') - # enabled = script_info.get('enabled',True) - # - # #检查脚本是否可用 - # if script_name in self.loader.script_classes: - #为 - -# 添加全局便捷函数,让脚本更容易使用API -def get_script_api(): - """获取脚本API实例(需要在脚本管理器初始化后使用)""" - # 这个函数将在脚本系统集成到主系统后实现 - return None - - -# 导出主要类 -__all__ = [ - 'ScriptBase', 'ScriptComponent', 'ScriptEngine', - 'ScriptLoader', 'ScriptAPI', 'ScriptManager' +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +脚本系统模块 + +负责脚本的创建、管理、挂载和运行: +- 脚本管理器:统一管理所有脚本 +- 脚本组件:挂载到游戏对象的脚本实例 +- 脚本引擎:执行脚本逻辑 +- 脚本API:提供给脚本使用的API接口 +""" + +import os +import sys +import importlib +import importlib.util +import traceback +import inspect +import time +from typing import Dict, List, Any, Optional, Callable +from abc import ABC, abstractmethod +from direct.task.TaskManagerGlobal import taskMgr +from panda3d.core import PythonTask + + +class ScriptBase(ABC): + """脚本基类 - 所有用户脚本都应该继承此类""" + + def __init__(self): + self.enabled = True + self.transform = None # 挂载的对象的transform + self.gameObject = None # 挂载的游戏对象 + self.world = None # 引擎世界对象引用 + self._script_id = None + + @abstractmethod + def start(self): + """脚本开始时调用(类似Unity的Start)""" + pass + + @abstractmethod + def update(self, dt): + """每帧更新时调用(类似Unity的Update)""" + pass + + def on_destroy(self): + """脚本销毁时调用(类似Unity的OnDestroy)""" + pass + + def on_enable(self): + """脚本启用时调用""" + pass + + def on_disable(self): + """脚本禁用时调用""" + pass + + def on_collision_enter(self, other): + """碰撞开始时调用""" + pass + + def on_collision_exit(self, other): + """碰撞结束时调用""" + pass + + def log(self, message): + """日志输出""" + print(f"[{self.__class__.__name__}] {message}") + + +class ScriptComponent: + """脚本组件 - 挂载到游戏对象上的脚本实例""" + + def __init__(self, script_instance: ScriptBase, game_object, script_manager): + self.script_instance = script_instance + self.game_object = game_object + self.script_manager = script_manager + self.enabled = True + + # 保存脚本名称,便于UI显示 + self.script_name = script_instance.__class__.__name__ + + # 设置脚本实例的引用 + script_instance.gameObject = game_object + script_instance.transform = game_object # Panda3D中NodePath就是transform + script_instance.world = script_manager.world + script_instance._script_id = id(self) + + # 标记脚本已开始 + self._started = False + + def start(self): + """启动脚本""" + if not self._started and self.enabled: + try: + self.script_instance.start() + self._started = True + except Exception as e: + print(f"脚本启动失败: {e}") + traceback.print_exc() + + def update(self, dt): + """更新脚本""" + if self.enabled and self._started: + try: + self.script_instance.update(dt) + except Exception as e: + print(f"脚本更新失败: {e}") + traceback.print_exc() + + def destroy(self): + """销毁脚本""" + try: + self.script_instance.on_destroy() + except Exception as e: + print(f"脚本销毁失败: {e}") + traceback.print_exc() + + def set_enabled(self, enabled): + """设置脚本启用状态""" + if self.enabled != enabled: + self.enabled = enabled + try: + if enabled: + self.script_instance.on_enable() + else: + self.script_instance.on_disable() + except Exception as e: + print(f"设置脚本状态失败: {e}") + traceback.print_exc() + + +class ScriptEngine: + """脚本引擎 - 负责脚本的执行和生命周期管理""" + + def __init__(self, world): + self.world = world + self.script_components: List[ScriptComponent] = [] + self.update_task = None + + def start_engine(self): + """启动脚本引擎""" + if self.update_task is None: + self.update_task = taskMgr.add(self._update_scripts, "script_update") + print("✓ 脚本引擎已启动") + + def stop_engine(self): + """停止脚本引擎""" + if self.update_task: + taskMgr.remove(self.update_task) + self.update_task = None + print("✓ 脚本引擎已停止") + + def add_script_component(self, script_component: ScriptComponent): + """添加脚本组件""" + self.script_components.append(script_component) + # 如果引擎已运行,立即启动脚本 + if self.update_task: + script_component.start() + + def remove_script_component(self, script_component: ScriptComponent): + """移除脚本组件""" + if script_component in self.script_components: + script_component.destroy() + self.script_components.remove(script_component) + + def _update_scripts(self, task): + """更新所有脚本(每帧调用)""" + from direct.showbase.ShowBaseGlobal import globalClock + dt = globalClock.getDt() + + # 复制列表以避免迭代时修改 + components_copy = self.script_components.copy() + + for component in components_copy: + if component.enabled: + # 如果脚本还没开始,先调用start + if not component._started: + component.start() + # 然后调用update + component.update(dt) + + return task.cont + + +class ScriptLoader: + """脚本加载器 - 负责加载和重载脚本""" + + def __init__(self, script_manager): + self.script_manager = script_manager + self.loaded_modules = {} # 模块名 -> 模块对象 + self.script_classes = {} # 脚本名 -> 脚本类 + self.file_mtimes = {} # 文件路径 -> 修改时间 + + def load_script_from_file(self, script_path: str) -> Optional[type]: + """从文件加载脚本类""" + try: + if not os.path.exists(script_path): + print(f"脚本文件不存在: {script_path}") + return None + + # 获取脚本名称(不包含扩展名) + script_name = os.path.splitext(os.path.basename(script_path))[0] + + # 动态导入模块 + spec = importlib.util.spec_from_file_location(script_name, script_path) + if spec is None: + print(f"无法创建模块规范: {script_path}") + return None + + module = importlib.util.module_from_spec(spec) + + # 如果模块已经加载过,先卸载 + if script_name in self.loaded_modules: + self.unload_script(script_name) + + # 执行模块 + spec.loader.exec_module(module) + + # 查找继承自ScriptBase的类 + script_class = None + for name, obj in inspect.getmembers(module, inspect.isclass): + if issubclass(obj, ScriptBase) and obj != ScriptBase: + script_class = obj + break + + if script_class is None: + print(f"脚本文件中没有找到继承自ScriptBase的类: {script_path}") + return None + + # 保存模块和类信息 + self.loaded_modules[script_name] = module + self.script_classes[script_name] = script_class + self.file_mtimes[script_path] = os.path.getmtime(script_path) + + print(f"✓ 成功加载脚本: {script_name} 从 {script_path}") + return script_class + + except Exception as e: + print(f"加载脚本失败 {script_path}: {e}") + traceback.print_exc() + return None + + def unload_script(self, script_name: str): + """卸载脚本""" + if script_name in self.loaded_modules: + # 移除所有使用此脚本的组件 + components_to_remove = [] + for component in self.script_manager.engine.script_components: + if component.script_instance.__class__.__name__ == script_name: + components_to_remove.append(component) + + for component in components_to_remove: + self.script_manager.remove_script_from_object(component.game_object, script_name) + + # 从sys.modules中移除 + module = self.loaded_modules[script_name] + if module.__name__ in sys.modules: + del sys.modules[module.__name__] + + # 清理引用 + del self.loaded_modules[script_name] + if script_name in self.script_classes: + del self.script_classes[script_name] + + print(f"✓ 脚本已卸载: {script_name}") + + def reload_script(self, script_path: str) -> Optional[type]: + """重新加载脚本(热重载)""" + script_name = os.path.splitext(os.path.basename(script_path))[0] + print(f"重新加载脚本: {script_name}") + + # 先卸载旧版本 + if script_name in self.loaded_modules: + self.unload_script(script_name) + + # 重新加载 + return self.load_script_from_file(script_path) + + def check_for_changes(self): + """检查脚本文件是否有变化(用于热重载)""" + changed_scripts = [] + + for script_path, old_mtime in self.file_mtimes.items(): + if os.path.exists(script_path): + current_mtime = os.path.getmtime(script_path) + if current_mtime > old_mtime: + changed_scripts.append(script_path) + + # 重新加载变化的脚本 + for script_path in changed_scripts: + self.reload_script(script_path) + + return len(changed_scripts) > 0 + + def find_script_file(self, script_name: str) -> Optional[str]: + """根据脚本名称查找脚本文件路径""" + # 首先检查已加载的脚本 + if script_name in self.loaded_modules: + module = self.loaded_modules[script_name] + if hasattr(module, '__file__') and module.__file__: + return module.__file__ + + # 在已知的文件路径中查找 + for file_path in self.file_mtimes.keys(): + file_name = os.path.splitext(os.path.basename(file_path))[0] + if file_name == script_name: + return file_path + + # 在脚本目录中查找 + scripts_dir = self.script_manager.scripts_directory + if os.path.exists(scripts_dir): + for file_name in os.listdir(scripts_dir): + if file_name.endswith('.py'): + base_name = os.path.splitext(file_name)[0] + if base_name == script_name: + return os.path.join(scripts_dir, file_name) + + return None + + +class ScriptAPI: + """脚本API - 提供给脚本使用的API接口""" + + def __init__(self, world): + self.world = world + + # ==================== 游戏对象操作 ==================== + + def find_object_by_name(self, name: str): + """根据名称查找游戏对象""" + return self.world.render.find(name) + + def create_object(self, name: str = "GameObject"): + """创建游戏对象""" + obj = self.world.render.attachNewNode(name) + return obj + + def destroy_object(self, obj): + """销毁游戏对象""" + if obj: + obj.removeNode() + + # ==================== 变换操作 ==================== + + def get_position(self, obj): + """获取对象位置""" + return obj.getPos() if obj else None + + def set_position(self, obj, x, y, z): + """设置对象位置""" + if obj: + obj.setPos(x, y, z) + + def get_rotation(self, obj): + """获取对象旋转""" + return obj.getHpr() if obj else None + + def set_rotation(self, obj, h, p, r): + """设置对象旋转""" + if obj: + obj.setHpr(h, p, r) + + def get_scale(self, obj): + """获取对象缩放""" + return obj.getScale() if obj else None + + def set_scale(self, obj, sx, sy, sz): + """设置对象缩放""" + if obj: + obj.setScale(sx, sy, sz) + + # ==================== 输入系统 ==================== + + def is_key_pressed(self, key): + """检查按键是否被按下""" + # 这里需要集成到现有的输入系统 + return False # 暂时返回False + + # ==================== 时间系统 ==================== + + def get_time(self): + """获取游戏时间""" + return time.time() + + def get_delta_time(self): + """获取帧间隔时间""" + from direct.showbase.ShowBaseGlobal import globalClock + return globalClock.getDt() + + # ==================== 日志系统 ==================== + + def log(self, message): + """输出日志""" + print(f"[ScriptAPI] {message}") + + +class ScriptManager: + """脚本管理器 - 统一管理所有脚本功能""" + + def __init__(self, world): + """初始化脚本管理器 + + Args: + world: 主程序world对象引用 + """ + self.world = world + + # 初始化子系统 + self.engine = ScriptEngine(world) + self.loader = ScriptLoader(self) + self.api = ScriptAPI(world) + + # 脚本存储 + self.object_scripts: Dict[Any, List[ScriptComponent]] = {} # 对象 -> 脚本组件列表 + self.script_templates: Dict[str, type] = {} # 脚本名 -> 脚本类 + + # 脚本目录 + self.scripts_directory = "scripts" + self._ensure_scripts_directory() + + # 热重载监控 + self.hot_reload_enabled = True + self.hot_reload_task = None + + print("✓ 脚本管理系统初始化完成") + + def _ensure_scripts_directory(self): + """确保脚本目录存在""" + if not os.path.exists(self.scripts_directory): + os.makedirs(self.scripts_directory) + + # 创建示例脚本 + self._create_example_script() + + def _create_example_script(self): + """创建示例脚本""" + example_script = '''#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +示例脚本 - 演示如何编写脚本 +""" + +from core.script_system import ScriptBase + +class ExampleScript(ScriptBase): + """示例脚本类""" + + def __init__(self): + super().__init__() + self.counter = 0 + self.rotation_speed = 30.0 # 度/秒 + + def start(self): + """脚本开始时调用""" + self.log("示例脚本开始运行!") + self.log(f"挂载到对象: {self.gameObject.getName()}") + + def update(self, dt): + """每帧更新""" + self.counter += 1 + + # 每60帧输出一次信息 + if self.counter % 60 == 0: + self.log(f"运行了 {self.counter} 帧") + + # 让对象旋转 + if self.transform: + current_h = self.transform.getH() + new_h = current_h + self.rotation_speed * dt + self.transform.setH(new_h) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("示例脚本被销毁") + + def on_enable(self): + """脚本启用时调用""" + self.log("示例脚本被启用") + + def on_disable(self): + """脚本禁用时调用""" + self.log("示例脚本被禁用") +''' + + example_path = os.path.join(self.scripts_directory, "example_script.py") + with open(example_path, 'w', encoding='utf-8') as f: + f.write(example_script) + + print(f"✓ 创建示例脚本: {example_path}") + + # ==================== 脚本管理功能 ==================== + + def start_system(self): + """启动脚本系统""" + self.engine.start_engine() + + if self.hot_reload_enabled: + self.start_hot_reload() + + print("✓ 脚本系统已启动") + + def stop_system(self): + """停止脚本系统""" + self.engine.stop_engine() + self.stop_hot_reload() + print("✓ 脚本系统已停止") + + def start_hot_reload(self): + """启动热重载监控""" + if self.hot_reload_task is None: + self.hot_reload_task = taskMgr.add(self._check_hot_reload, "script_hot_reload") + print("✓ 脚本热重载监控已启动") + + def stop_hot_reload(self): + """停止热重载监控""" + if self.hot_reload_task: + taskMgr.remove(self.hot_reload_task) + self.hot_reload_task = None + print("✓ 脚本热重载监控已停止") + + def _check_hot_reload(self, task): + """检查热重载(每秒调用一次)""" + self.loader.check_for_changes() + task.delayTime = 1.0 # 1秒后再次调用 + return task.again + + # ==================== 脚本创建和加载 ==================== + + def create_script_file(self, script_name: str, template: str = "basic") -> str: + """创建新的脚本文件""" + script_path = os.path.join(self.scripts_directory, f"{script_name}.py") + + if os.path.exists(script_path): + print(f"脚本文件已存在: {script_path}") + return script_path + + # 根据模板创建脚本 + if template == "basic": + script_content = self._get_basic_script_template(script_name) + elif template == "movement": + script_content = self._get_movement_script_template(script_name) + else: + script_content = self._get_basic_script_template(script_name) + + with open(script_path, 'w', encoding='utf-8') as f: + f.write(script_content) + + print(f"✓ 创建脚本文件: {script_path}") + return script_path + + def _get_basic_script_template(self, script_name: str) -> str: + """获取基础脚本模板""" + class_name = ''.join(word.capitalize() for word in script_name.split('_')) + + return f'''#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +{script_name} - 自定义脚本 +""" + +from core.script_system import ScriptBase + +class {class_name}(ScriptBase): + """自定义脚本类""" + + def __init__(self): + super().__init__() + # 在这里初始化您的变量 + + def start(self): + """脚本开始时调用""" + self.log("脚本开始运行!") + + def update(self, dt): + """每帧更新""" + # 在这里编写更新逻辑 + pass + + def on_destroy(self): + """脚本销毁时调用""" + self.log("脚本被销毁") +''' + + def _get_movement_script_template(self, script_name: str) -> str: + """获取移动脚本模板""" + class_name = ''.join(word.capitalize() for word in script_name.split('_')) + + return f'''#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +{script_name} - 移动脚本 +""" + +from core.script_system import ScriptBase + +class {class_name}(ScriptBase): + """移动脚本类""" + + def __init__(self): + super().__init__() + self.speed = 5.0 # 移动速度 + self.direction = [1, 0, 0] # 移动方向 + + def start(self): + """脚本开始时调用""" + self.log("移动脚本开始运行!") + + def update(self, dt): + """每帧更新""" + if self.transform: + # 计算移动偏移 + offset_x = self.direction[0] * self.speed * dt + offset_y = self.direction[1] * self.speed * dt + offset_z = self.direction[2] * self.speed * dt + + # 更新位置 + current_pos = self.transform.getPos() + new_pos = ( + current_pos.x + offset_x, + current_pos.y + offset_y, + current_pos.z + offset_z + ) + self.transform.setPos(*new_pos) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("移动脚本被销毁") +''' + + def load_script_from_file(self, script_path: str) -> Optional[type]: + """从文件加载脚本""" + return self.loader.load_script_from_file(script_path) + + def load_all_scripts_from_directory(self, directory: str = None) -> List[str]: + """从目录加载所有脚本""" + if directory is None: + directory = self.scripts_directory + + if not os.path.exists(directory): + print(f"脚本目录不存在: {directory}") + return [] + + loaded_scripts = [] + for filename in os.listdir(directory): + if filename.endswith('.py') and not filename.startswith('__'): + script_path = os.path.join(directory, filename) + script_class = self.load_script_from_file(script_path) + if script_class: + script_name = os.path.splitext(filename)[0] + loaded_scripts.append(script_name) + + print(f"✓ 从目录 {directory} 加载了 {len(loaded_scripts)} 个脚本") + return loaded_scripts + + # ==================== 脚本挂载和管理 ==================== + + def add_script_to_object(self, game_object, script_name: str) -> Optional[ScriptComponent]: + """为对象添加脚本""" + # 查找脚本类 + script_class = self.loader.script_classes.get(script_name) + if script_class is None: + print(f"未找到脚本类: {script_name}") + return None + + try: + # 创建脚本实例 + script_instance = script_class() + + # 创建脚本组件 + script_component = ScriptComponent(script_instance, game_object, self) + + # 添加到对象的脚本列表 + if game_object not in self.object_scripts: + self.object_scripts[game_object] = [] + self.object_scripts[game_object].append(script_component) + + # 添加到脚本引擎 + self.engine.add_script_component(script_component) + + print(f"✓ 为对象 {game_object.getName()} 添加脚本: {script_name}") + return script_component + + except Exception as e: + print(f"添加脚本失败: {e}") + traceback.print_exc() + return None + + def remove_script_from_object(self, game_object, script_name: str) -> bool: + """从游戏对象移除脚本""" + if game_object not in self.object_scripts: + return False + + script_components = self.object_scripts[game_object] + removed = False + + for component in script_components[:]: # 复制列表以避免修改时出错 + if component.script_instance.__class__.__name__ == script_name: + # 从引擎移除 + self.engine.remove_script_component(component) + # 从对象脚本列表移除 + script_components.remove(component) + removed = True + + print(f"✓ 从对象 {game_object.getName()} 移除脚本: {script_name}") + + + if not script_components: + del self.object_scripts[game_object] + + # 更新节点上保存的脚本信息标签 + if removed: + self._update_node_script_tags_after_removal(game_object, script_name) + + return removed + + def _update_node_script_tags_after_removal(self, game_object, removed_script_name): + """在移除脚本后更新节点标签""" + try: + # 获取对象上剩余的脚本 + remaining_scripts = self.get_scripts_on_object(game_object) + + if not remaining_scripts: + # 如果没有其他脚本,清除所有脚本标签 + if game_object.hasTag("has_scripts"): + game_object.clearTag("has_scripts") + if game_object.hasTag("scripts_info"): + game_object.clearTag("scripts_info") + print(f"✓ 清除节点 {game_object.getName()} 的所有脚本标签") + else: + # 如果还有其他脚本,更新脚本信息标签 + script_info_list = [] + for script_component in remaining_scripts: + script_name = script_component.script_name + script_class = script_component.script_instance.__class__ + script_file = self.loader.find_script_file(script_name) or "" + + script_info_list.append({ + "name": script_name, + "file": script_file + }) + + import json + game_object.setTag("has_scripts", "true") + game_object.setTag("scripts_info", json.dumps(script_info_list, ensure_ascii=False)) + print(f"✓ 更新节点 {game_object.getName()} 的脚本标签信息,剩余 {len(script_info_list)} 个脚本") + + except Exception as e: + print(f"更新节点标签失败: {e}") + + def get_scripts_on_object(self, game_object) -> List[ScriptComponent]: + """获取对象上的所有脚本""" + return self.object_scripts.get(game_object, []) + + def get_script_on_object(self, game_object, script_name: str) -> Optional[ScriptComponent]: + """获取对象上的特定脚本""" + scripts = self.get_scripts_on_object(game_object) + for script in scripts: + if script.script_instance.__class__.__name__ == script_name: + return script + return None + + # ==================== 脚本信息查询 ==================== + + def get_available_scripts(self) -> List[str]: + """获取所有可用的脚本名称""" + return list(self.loader.script_classes.keys()) + + def get_script_info(self, script_name: str) -> Optional[Dict[str, Any]]: + """获取脚本信息""" + script_class = self.loader.script_classes.get(script_name) + if script_class is None: + return None + + return { + "name": script_name, + "class": script_class, + "doc": script_class.__doc__, + "file": inspect.getfile(script_class) if hasattr(script_class, '__file__') else None, + "methods": [method for method in dir(script_class) if not method.startswith('_')] + } + + def reload_script(self, script_name: str) -> bool: + """重新加载脚本""" + script_info = self.get_script_info(script_name) + if script_info and script_info["file"]: + return self.loader.reload_script(script_info["file"]) is not None + return False + + # ==================== 调试功能 ==================== + + def list_all_scripts(self): + """列出所有脚本信息""" + print("\n=== 脚本系统状态 ===") + print(f"可用脚本数量: {len(self.loader.script_classes)}") + print(f"运行中的脚本组件数量: {len(self.engine.script_components)}") + print(f"有脚本的对象数量: {len(self.object_scripts)}") + + if self.loader.script_classes: + print("\n可用脚本:") + for script_name in self.loader.script_classes: + print(f" - {script_name}") + + if self.object_scripts: + print("\n对象脚本分布:") + for obj, scripts in self.object_scripts.items(): + script_names = [s.script_instance.__class__.__name__ for s in scripts] + print(f" - {obj.getName()}: {script_names}") + + print("==================\n") + + def save_object_scripts(self,game_object,node_data:dict): + try: + if game_object in self.object_scripts: + scripts_data = [] + for script_commponent in self.object_scripts[game_object]: + script_info = { + 'script_name':script_commponent.script_name, + 'enabled':script_commponent.enabled, + 'script_class':script_commponent.script_instance.__class__.__name__ + } + scripts_data.append(script_info) + if scripts_data: + node_data['scripts'] = scripts_data + print(f"✓ 保存了 {len(scripts_data)} 个脚本到对象 {game_object.getName()}") + except Exception as e: + print(f"保存对象脚本信息失败: {e}") + traceback.print_exc() + + # def restore_object_scripts(self,game_object,node_data:dict): + # try: + # if 'scripts' in node_data: + # scripts_data = node_data['scripts'] + # restored_count = 0 + # for script_info in scripts_data: + # script_name = script_info.get('script_name') + # enabled = script_info.get('enabled',True) + # + # #检查脚本是否可用 + # if script_name in self.loader.script_classes: + #为 + +# 添加全局便捷函数,让脚本更容易使用API +def get_script_api(): + """获取脚本API实例(需要在脚本管理器初始化后使用)""" + # 这个函数将在脚本系统集成到主系统后实现 + return None + + +# 导出主要类 +__all__ = [ + 'ScriptBase', 'ScriptComponent', 'ScriptEngine', + 'ScriptLoader', 'ScriptAPI', 'ScriptManager' ] \ No newline at end of file diff --git a/core/selection.py b/core/selection.py index 75fb874f..3371d6d0 100644 --- a/core/selection.py +++ b/core/selection.py @@ -1,2782 +1,2782 @@ -""" -选择和变换系统模块 - -负责物体选择和变换相关功能: -- 选择框的创建和更新 -- 坐标轴(Gizmo)系统 -- 拖拽变换逻辑 -- 射线检测和碰撞检测 -""" -from direct.showbase.ShowBaseGlobal import globalClock -from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState, - DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue, - CollisionNode, CollisionRay, GeomNode, BitMask32, Material, LColor, DepthWriteAttrib, - TransparencyAttrib, Vec4, CollisionCapsule) -from direct.task.TaskManagerGlobal import taskMgr -import math - -class SelectionSystem: - """选择和变换系统类""" - - def __init__(self, world): - """初始化选择系统 - - Args: - world: 核心世界对象引用 - """ - self.world = world - - # 选择相关状态 - self.selectedNode = None - self.selectionBox = None # 选择框 - self.selectionBoxTarget = None # 选择框跟踪的目标节点 - - # 坐标轴工具(Gizmo)相关 - self.gizmo = None # 坐标轴 - self.gizmoTarget = None # 坐标轴跟踪的目标节点 - self.gizmoXAxis = None # X轴 - self.gizmoYAxis = None # Y轴 - self.gizmoZAxis = None # Z轴 - self.gizmoRotXAxis = None - self.gizmoRotYAxis = None - self.gizmoRotZAxis = None - self.axis_length = 5.0 # 坐标轴长度(增加到5.0) - - # 拖拽相关状态 - self.isDraggingGizmo = False # 是否正在拖拽坐标轴 - self.dragGizmoAxis = None # 当前拖拽的轴("x", "y", "z") - self.gizmoStartPos = None # 拖拽开始时坐标轴的位置 - self.gizmoTargetStartPos = None # 拖拽开始时目标节点的位置 - self.dragStartMousePos = None # 拖拽开始时的鼠标位置 - - # 高亮相关 - self.gizmoHighlightAxis = None - self.gizmo_colors = { - "x": (1, 0, 0, 1.0), # 红色 - "y": (0, 1, 0, 1.0), # 绿色 - "z": (0, 0, 1, 1.00) # 蓝色 - } - self.gizmo_highlight_colors = { - "x": (1.0, 1.0, 0.0, 1.0), # 黄色高亮 - "y": (1.0, 1.0, 0.0, 1.0), # 黄色高亮 - "z": (1.0, 1.0, 0.0, 1.0) # 黄色高亮 - } - - #性能优化相关 - self._optimized_node = False - self._last_update_time = 0 - self._cached_bounds = {} - self._gizmo_update_interval = 0.1 - self._selection_box_update_interval = 0.2 - - self._current_cursor = None - self._default_cursor = None - - self._last_click_time = 0 - self._double_click_threshold = 0.3 - self._last_clicked_node = None - self._double_click_task = None - - print("✓ 选择和变换系统初始化完成") - # ==================== 光标设置 ==================== - def _setCursor(self,cursor_type): - try: - from PyQt5.QtCore import Qt - if self._current_cursor == cursor_type: - return - if hasattr(self.world,'main_window') and self.world.main_window: - main_window = self.world.main_window - else: - from PyQt5.QtWidgets import QApplication - main_window = QApplication.activeWindow() - if not main_window: - windows = QApplication.topLevelWindows() - for window in windows: - if hasattr(window,'isVisible') and window.isVisible(): - main_window = window - break - if main_window: - if cursor_type == "crosshair": - main_window.setCursor(Qt.CrossCursor) - elif cursor_type == "size_hor": - main_window.setCursor(Qt.SizeHorCursor) - elif cursor_type == "size_ver": - main_window.setCursor(Qt.SizeVerCursor) - elif cursor_type == "size_all": - main_window.setCursor(Qt.SizeAllCursor) - elif cursor_type == "pointing_hand": - main_window.setCursor(Qt.PointingHandCursor) - else: - main_window.unsetCursor() - self._current_cursor = cursor_type - #print(f"光标已设置:{cursor_type}") - self._current_cursor = cursor_type - else: - print("警告:无法获取主窗口,光标设置失败") - except Exception as e: - print(f"设置光标失败{e}") - def _resetCursor(self): - self._setCursor("default") - - # ==================== 选择框系统 ==================== - - def createSelectionBox(self, nodePath): - """为选中的节点创建选择框""" - try: - if self.selectionBox: - #print(" 移除现有选择框") - self.selectionBox.removeNode() - self.selectionBox = None - - if not nodePath: - print(" 目标节点为空,取消创建") - return - - self.selectionBox = self.world.render.attachNewNode("selectionBox") - self.selectionBoxTarget = nodePath - - taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox") - self.updateSelectionBoxGeometry() - - except Exception as e: - print(f" ✗ 创建选择框失败: {str(e)}") - import traceback - traceback.print_exc() - - def updateSelectionBoxGeometry(self): - """更新选择框的几何形状和位置""" - try: - if not self.selectionBox or not self.selectionBoxTarget: - return - - if self.selectionBoxTarget.isEmpty(): - return - - minPoint = Point3() - maxPoint = Point3() - - try: - has_bounds = self.selectionBoxTarget.calcTightBounds(minPoint, maxPoint, self.world.render) - if not has_bounds: - return - except: - return - - # 检查边界框的有效性 - if (minPoint.x > maxPoint.x or minPoint.y > maxPoint.y or minPoint.z > maxPoint.z or - abs(minPoint.x) > 1e10 or abs(minPoint.y) > 1e10 or abs(minPoint.z) > 1e10 or - abs(maxPoint.x) > 1e10 or abs(maxPoint.y) > 1e10 or abs(maxPoint.z) > 1e10): - print("警告: 检测到无效的边界框,跳过选择框更新") - return - - # 检查是否需要重新计算边界框 - if not hasattr(self, '_bounds_cache'): - self._bounds_cache = {} - - node_name = self.selectionBoxTarget.getName() - import time - current_time = time.time() - - # 如果缓存存在且未过期,则使用缓存 - if (node_name in self._bounds_cache and - current_time - self._bounds_cache[node_name]['time'] < 0.1): - minPoint, maxPoint = self._bounds_cache[node_name]['bounds'] - else: - # 计算新的边界框并缓存 - minPoint = Point3() - maxPoint = Point3() - if not self.selectionBoxTarget.calcTightBounds(minPoint, maxPoint, self.world.render): - return - - # 缓存结果 - self._bounds_cache[node_name] = { - 'bounds': (minPoint, maxPoint), - 'time': current_time - } - - # 清理旧缓存 - expired_keys = [k for k, v in self._bounds_cache.items() - if current_time - v['time'] > 1.0] - for key in expired_keys: - del self._bounds_cache[key] - - # 清除现有的几何体 - self.selectionBox.removeNode() - self.selectionBox = self.world.render.attachNewNode("selectionBox") - - # 获取目标节点在世界坐标系中的边界框(使用正确的API) - minPoint = Point3() - maxPoint = Point3() - if not self.selectionBoxTarget.calcTightBounds(minPoint, maxPoint, self.world.render): - return - - # 获取边界框的最小和最大点(世界坐标) - #print(f"世界边界框: min={minPoint}, max={maxPoint}") - - # 创建线段对象 - lines = LineSegs() - lines.setThickness(2.0) - - # 定义立方体的8个顶点 - vertices = [ - (minPoint.x, minPoint.y, minPoint.z), # 0: 前下左 - (maxPoint.x, minPoint.y, minPoint.z), # 1: 前下右 - (maxPoint.x, maxPoint.y, minPoint.z), # 2: 后下右 - (minPoint.x, maxPoint.y, minPoint.z), # 3: 后下左 - (minPoint.x, minPoint.y, maxPoint.z), # 4: 前上左 - (maxPoint.x, minPoint.y, maxPoint.z), # 5: 前上右 - (maxPoint.x, maxPoint.y, maxPoint.z), # 6: 后上右 - (minPoint.x, maxPoint.y, maxPoint.z), # 7: 后上左 - ] - - # 定义立方体的边(连接顶点的线段) - edges = [ - # 底面 - (0, 1), (1, 2), (2, 3), (3, 0), - # 顶面 - (4, 5), (5, 6), (6, 7), (7, 4), - # 垂直边 - (0, 4), (1, 5), (2, 6), (3, 7) - ] - - # 绘制所有边 - for start, end in edges: - lines.moveTo(*vertices[start]) - lines.drawTo(*vertices[end]) - - # 创建选择框几何体 - geomNode = lines.create() - self.selectionBox.attachNewNode(geomNode) - - # 设置选择框的颜色为亮橙色 - self.selectionBox.setColor(1.0, 0.5, 0.0, 1.0) - - # 设置渲染状态,确保线框总是在最前面显示 - state = RenderState.make( - DepthTestAttrib.make(DepthTestAttrib.MLess), - ColorAttrib.makeFlat((1.0, 0.5, 0.0, 1.0)) - ) - self.selectionBox.setState(state) - - # 确保选择框不被光照影响 - self.selectionBox.setLightOff() - - # 让选择框稍微大一点,避免与模型重叠 - self.selectionBox.setScale(1.01) - - except Exception as e: - print(f"更新选择框几何体失败: {str(e)}") - import traceback - traceback.print_exc() - - def updateSelectionBoxTask(self, task): - """选择框更新任务 - 平衡性能和实时性""" - try: - update_interval = 0.05 - - if not hasattr(self, '_last_selection_box_update'): - self._last_selection_box_update = 0 - - import time - current_time = time.time() - if current_time - self._last_selection_box_update < update_interval: - return task.cont - self._last_selection_box_update = current_time - - # 检查目标节点是否已被删除 - self.checkAndClearIfTargetDeleted() - - if not self.selectionBox or not self.selectionBoxTarget: - return task.done - - # 检查目标节点是否还存在 - if self.selectionBoxTarget.isEmpty(): - self.clearSelectionBox() - return task.done - - # 检查目标节点是否发生了变化(位置、旋转、缩放) - current_transform = self._getNodeTransformKey(self.selectionBoxTarget) - - if (not hasattr(self, '_last_transform_key') or - self._last_transform_key != current_transform): - # 节点发生了变化,更新选择框 - self.updateSelectionBoxGeometry() - self._last_transform_key = current_transform - - return task.cont - - except Exception as e: - print(f"选择框更新任务出错: {str(e)}") - return task.done - - def _getNodeTransformKey(self, node): - """获取节点变换的关键信息,用于快速比较""" - try: - # 获取节点的关键变换信息 - pos = node.getPos(self.world.render) - hpr = node.getHpr(self.world.render) - scale = node.getScale(self.world.render) - - # 返回一个可以比较的元组 - return (pos.x, pos.y, pos.z, hpr.x, hpr.y, hpr.z, scale.x, scale.y, scale.z) - except: - return None - - def clearSelectionBox(self): - """清除选择框""" - if self.selectionBox: - self.selectionBox.removeNode() - self.selectionBox = None - - # 停止选择框更新任务 - taskMgr.remove("updateSelectionBox") - - # 清除目标节点引用 - self.selectionBoxTarget = None - - print("清除了选择框") - - # ==================== 坐标轴(Gizmo)系统 ==================== - - def createGizmo(self, nodePath): - """为选中的节点创建坐标轴工具 - 保留箭头版本""" - try: - #print(f" 开始创建坐标轴,目标节点: {nodePath.getName()}") - - # 如果已有坐标轴,先移除 - if self.gizmo: - self.gizmo.removeNode() - self.gizmo = None - taskMgr.remove("updateGizmo") - - if not nodePath: - return - - # 创建坐标轴主节点 - self.gizmo = self.world.render.attachNewNode("gizmo") - self.gizmoTarget = nodePath - - # 添加标识标签,便于识别 - self.gizmo.setTag("is_gizmo", "1") - if hasattr(nodePath, 'getName'): - self.gizmo.setTag("gizmo_target", nodePath.getName()) - - # 为各轴添加标签 - if hasattr(self, 'gizmoXAxis') and self.gizmoXAxis: - self.gizmoXAxis.setTag("is_gizmo", "1") - self.gizmoXAxis.setTag("gizmo_axis", "x") - if hasattr(self, 'gizmoYAxis') and self.gizmoYAxis: - self.gizmoYAxis.setTag("is_gizmo", "1") - self.gizmoYAxis.setTag("gizmo_axis", "y") - if hasattr(self, 'gizmoZAxis') and self.gizmoZAxis: - self.gizmoZAxis.setTag("is_gizmo", "1") - self.gizmoZAxis.setTag("gizmo_axis", "z") - - # 设置位置和朝向 - minPoint = Point3() - maxPoint = Point3() - if nodePath.calcTightBounds(minPoint, maxPoint, self.world.render): - center = Point3((minPoint.x + maxPoint.x) * 0.5, - (minPoint.y + maxPoint.y) * 0.5, - (minPoint.z + maxPoint.z) * 0.5) - self.gizmo.setPos(center) - - parent_node = nodePath.getParent() - if parent_node and parent_node != self.world.render: - self.gizmo.setHpr(parent_node.getHpr()) - else: - self.gizmo.setHpr(0, 0, 0) - - # 只调用一次几何体创建 - self.createGizmoGeometry() - - #只调用一次颜色设置 - self.setGizmoAxisColor("x", self.gizmo_colors["x"]) - self.setGizmoAxisColor("y", self.gizmo_colors["y"]) - self.setGizmoAxisColor("z", self.gizmo_colors["z"]) - - self._updateGizmoScreenSize() - - self._setupGizmoRendering() - - self.setupGizmoCollision() - - # 现在才显示坐标轴 - self.gizmo.show() - - # 只启动一次更新任务 - taskMgr.add(self.updateGizmoTask, "updateGizmo") - - #print(f" ✓ 为节点 {nodePath.getName()} 创建了坐标轴") - - except Exception as e: - print(f"创建坐标轴失败: {str(e)}") - def createGizmoGeometry(self): - """创建坐标轴的几何体""" - from panda3d.core import Material - try: - if not self.gizmo: - return - - is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False - is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False - - import os - from panda3d.core import getModelPath, Filename - - # 获取项目根目录 - base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - # 确保core目录在模型搜索路径中 - model_path = getModelPath() - core_dir = os.path.join(base_dir, "core") - core_filename = Filename.from_os_specific(core_dir) - if not model_path.findFile(core_filename): - model_path.appendDirectory(core_filename) - print(f"✓ 添加core目录到模型搜索路径: {core_dir}") - - if is_scale_tool: - model_filename = "core/UniformScaleHandle.fbx" - elif is_rotate_tool: - model_filename = "core/RotationHandleQuarter.fbx" - arrow_filename = "core/TranslateArrowHandle.fbx" - else: - model_filename = "core/TranslateArrowHandle.fbx" - - # model_paths = [ - # "core/TranslateArrowHandle.fbx", - # "EG/core/TranslateArrowHandle.fbx", - # ] - gizmo_model = None - gizmoRot_model = None - try: - if is_rotate_tool: - gizmo_model = self.world.loader.loadModel(arrow_filename) - gizmoRot_model = self.world.loader.loadModel(model_filename) - else: - gizmo_model = self.world.loader.loadModel(model_filename) - except Exception as e: - print(f"加载模型失败: {e}") - return - - x_rHandle = None - y_rHandle = None - z_rHandle = None - - if is_rotate_tool: - self.gizmoRotXAxis = self.gizmo.attachNewNode("gizmo_rot_x_axis") - x_rHandle = gizmoRot_model.copyTo(self.gizmoRotXAxis) - x_rHandle.setName("x_handle") - self.gizmoRotYAxis = self.gizmo.attachNewNode("gizmo_rot_y_axis") - y_rHandle = gizmoRot_model.copyTo(self.gizmoRotYAxis) - y_rHandle.setName("y_handle") - self.gizmoRotZAxis = self.gizmo.attachNewNode("gizmo_rot_z_axis") - z_rHandle = gizmoRot_model.copyTo(self.gizmoRotZAxis) - z_rHandle.setName("z_handle") - - self.gizmoXAxis = self.gizmo.attachNewNode("gizmo_x_axis") - x_handle = gizmo_model.copyTo(self.gizmoXAxis) - x_handle.setName("x_handle") - - self.gizmoYAxis = self.gizmo.attachNewNode("gizmo_y_axis") - y_handle = gizmo_model.copyTo(self.gizmoYAxis) - y_handle.setName("y_handle") - - self.gizmoZAxis = self.gizmo.attachNewNode("gizmo_z_axis") - z_handle = gizmo_model.copyTo(self.gizmoZAxis) - z_handle.setName("z_handle") - - if is_scale_tool: - x_handle.setHpr(0,-90,0) - x_handle.setScale(0.6,0.03,0.03) - x_handle.setPos(2.2,0,0) - - y_handle.setHpr(90,0,0) - y_handle.setScale(0.6,0.03,0.03) - y_handle.setPos(0,2.2,0) - - z_handle.setHpr(0,0,-90) - z_handle.setScale(0.6,0.03,0.03) - z_handle.setPos(0,0,2.2) - elif is_rotate_tool: - x_rHandle.setHpr(0,0,90) - x_rHandle.setScale(0.025,0.0125,0.0125) - x_rHandle.setPos(0,0,0) - - y_rHandle.setHpr(0,0,0) - y_rHandle.setScale(0.025,0.0125,0.0125) - y_rHandle.setPos(0,0,0) - - z_rHandle.setHpr(-90,0,0) - z_rHandle.setScale(0.025,0.0125,0.0125) - z_rHandle.setPos(0,0,0) - - x_handle.setHpr(0, -90, 0) - x_handle.setScale(0.1, 0.05, 0.05) - x_handle.setPos(0, 0, 0) - - y_handle.setHpr(90, 0, 0) - y_handle.setScale(0.1, 0.05, 0.05) - y_handle.setPos(0, 0, 0) - - z_handle.setHpr(0, 0, -90) - z_handle.setScale(0.1, 0.05, 0.05) - z_handle.setPos(0, 0, 0) - - self.setGizmoRotAxisColor("x", self.gizmo_colors["x"]) - self.setGizmoRotAxisColor("y", self.gizmo_colors["y"]) - self.setGizmoRotAxisColor("z", self.gizmo_colors["z"]) - else: - x_handle.setHpr(0,-90,0) - x_handle.setScale(0.1,0.05,0.05) - x_handle.setPos(0,0,0) - - y_handle.setHpr(90,0,0) - y_handle.setScale(0.1,0.05,0.05) - y_handle.setPos(0,0,0) - - z_handle.setHpr(0,0,-90) - z_handle.setScale(0.1,0.05,0.05) - z_handle.setPos(0,0,0) - - # 设置初始颜色 - self.setGizmoAxisColor("x", self.gizmo_colors["x"]) - self.setGizmoAxisColor("y", self.gizmo_colors["y"]) - self.setGizmoAxisColor("z", self.gizmo_colors["z"]) - - #设置渲染属性,解决模型遮挡和阴影问题 - self._setupGizmoRendering() - - except Exception as e: - print(f"创建坐标轴几何体失败: {str(e)}") - - def _setupGizmoRendering(self): - try: - axis_nodes = [self.gizmoXAxis,self.gizmoYAxis,self.gizmoZAxis] - axis_Rotnodes = [self.gizmoRotXAxis, self.gizmoRotYAxis, self.gizmoRotZAxis] - - # 设置坐标轴主节点的渲染属性 - if self.gizmo: - self.gizmo.setLightOff() # 禁用光照 - self.gizmo.setShaderOff() # 禁用着色器 - self.gizmo.setFogOff() # 禁用雾效 - self.gizmo.setBin("fixed", 40) # 设置为fixed渲染层级,数值越大越优先 - self.gizmo.setDepthWrite(False) # 禁用深度写入 - self.gizmo.setDepthTest(False) # 禁用深度测试,确保始终可见 - - # 设置各轴节点的渲染属性 - for axis_node in axis_nodes: - if axis_node: - axis_node.setLightOff() - axis_node.setShaderOff() - axis_node.setFogOff() - axis_node.setBin("fixed", 40) # 与主节点相同优先级 - axis_node.setDepthWrite(False) # 禁用深度写入 - axis_node.setDepthTest(False) # 禁用深度测试 - - # 设置旋转轴节点的渲染属性 - for axis_rotnode in axis_Rotnodes: - if axis_rotnode: - axis_rotnode.setLightOff() - axis_rotnode.setShaderOff() - axis_rotnode.setFogOff() - axis_rotnode.setBin("fixed", 40) # 与主节点相同优先级 - axis_rotnode.setDepthWrite(False) # 禁用深度写入 - axis_rotnode.setDepthTest(False) # 禁用深度测试 - - # 收集所有handle节点 - arrow_nodes = [] - if self.gizmoXAxis: - x_handle = self.gizmoXAxis.find("x_handle") - if x_handle and not x_handle.isEmpty(): - arrow_nodes.append(x_handle) - if self.gizmoYAxis: - y_handle = self.gizmoYAxis.find("y_handle") - if y_handle and not y_handle.isEmpty(): - arrow_nodes.append(y_handle) - if self.gizmoZAxis: - z_handle = self.gizmoZAxis.find("z_handle") - if z_handle and not z_handle.isEmpty(): - arrow_nodes.append(z_handle) - - rot_nodes = [] - if self.gizmoRotXAxis: - x_rHandle = self.gizmoRotXAxis.find("x_handle") - if x_rHandle and not x_rHandle.isEmpty(): - rot_nodes.append(x_rHandle) - if self.gizmoRotYAxis: - y_rHandle = self.gizmoRotYAxis.find("y_handle") - if y_rHandle and not y_rHandle.isEmpty(): - rot_nodes.append(y_rHandle) - if self.gizmoRotZAxis: - z_rHandle = self.gizmoRotZAxis.find("z_handle") - if z_rHandle and not z_rHandle.isEmpty(): - rot_nodes.append(z_rHandle) - - # 设置handle节点的渲染属性 - for arrow_node in arrow_nodes: - if arrow_node: - arrow_node.setLightOff() - arrow_node.setShaderOff() - arrow_node.setFogOff() - arrow_node.setBin("fixed", 41) # 略高于主节点,确保最优先显示 - arrow_node.setDepthWrite(False) - arrow_node.setDepthTest(False) - # 启用透明度支持 - arrow_node.setTransparency(TransparencyAttrib.MAlpha) - - for rot_node in rot_nodes: - if rot_node: - rot_node.setLightOff() - rot_node.setShaderOff() - rot_node.setFogOff() - rot_node.setBin("fixed", 41) # 略高于主节点,确保最优先显示 - rot_node.setDepthWrite(False) - rot_node.setDepthTest(False) - # 启用透明度支持 - rot_node.setTransparency(TransparencyAttrib.MAlpha) - - except Exception as e: - print(f"设置坐标轴渲染属性失败: {str(e)}") - - def updateGizmoTask(self, task): - """坐标轴更新任务 - 包含固定大小功能""" - try: - # 限制更新频率 - if not hasattr(self, '_last_gizmo_update'): - self._last_gizmo_update = 0 - - import time - current_time = time.time() - if current_time - self._last_gizmo_update < 0.5: # 每0.05秒更新一次 - return task.cont - self._last_gizmo_update = current_time - - #检查目标节点是否已被删除 - self.checkAndClearIfTargetDeleted() - - if not self.gizmo or not self.gizmoTarget: - return task.done - - # 检查目标节点是否还存在 - if self.gizmoTarget.isEmpty(): - self.clearGizmo() - return task.done - - is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False - is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False - was_scale_tool = getattr(self,'_last_tool_scale_state',False) - was_rotate_tool =getattr(self,'_last_tool_rotate_state',False) - - tool_changed = (is_scale_tool!=was_scale_tool) or (is_rotate_tool != was_rotate_tool) - - if tool_changed: - self._last_tool_scale_state = is_scale_tool - self._last_tool_rotate_state = is_rotate_tool - - if self.gizmoXAxis: - self.gizmoXAxis.removeNode() - self.gizmoXAxis = None - if self.gizmoYAxis: - self.gizmoYAxis.removeNode() - self.gizmoYAxis = None - if self.gizmoZAxis: - self.gizmoZAxis.removeNode() - self.gizmoZAxis = None - if self.gizmoRotXAxis: - self.gizmoRotXAxis.removeNode() - self.gizmoRotXAxis = None - if self.gizmoRotYAxis: - self.gizmoRotYAxis.removeNode() - self.gizmoRotYAxis = None - if self.gizmoRotZAxis: - self.gizmoRotZAxis.removeNode() - self.gizmoRotZAxis = None - - self.createGizmoGeometry() - - self.setGizmoAxisColor("x",self.gizmo_colors["x"]) - self.setGizmoAxisColor("y",self.gizmo_colors["y"]) - self.setGizmoAxisColor("z",self.gizmo_colors["z"]) - - self.setupGizmoCollision() - - light_object = self.gizmoTarget.getPythonTag("rp_light_object") - if light_object: - light_pos = light_object.pos - self.gizmo.setPos(light_object.pos) - self.gizmoTarget.setPos(light_pos) - else: - # 只在必要时更新位置和朝向 - self._updateGizmoPositionAndOrientation() - - # 【新功能】:动态调整坐标轴大小,保持固定的屏幕大小 - self._updateGizmoScreenSize() - - return task.cont - - except Exception as e: - print(f"坐标轴更新任务出错: {str(e)}") - return task.done - - def _updateGizmoPositionAndOrientation(self): - """优化的Gizmo位置和朝向更新""" - # 只在必要时重新计算边界框 - if not hasattr(self, '_last_gizmo_bounds_update'): - self._last_gizmo_bounds_update = 0 - - import time - current_time = time.time() - if current_time - self._last_gizmo_bounds_update > 0.2: # 每0.2秒计算一次边界框 - minPoint = Point3() - maxPoint = Point3() - # 添加异常处理 - try: - if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render): - # 检查边界框的有效性 - if (abs(minPoint.x) < 1e10 and abs(minPoint.y) < 1e10 and abs(minPoint.z) < 1e10 and - abs(maxPoint.x) < 1e10 and abs(maxPoint.y) < 1e10 and abs(maxPoint.z) < 1e10): - # 计算中心点 - center = Point3((minPoint.x + maxPoint.x) * 0.5, - (minPoint.y + maxPoint.y) * 0.5, - (minPoint.z + maxPoint.z) * 0.5) - self.gizmo.setPos(center) - except Exception as e: - print(f"更新Gizmo位置时出错: {e}") - # if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render): - # # 计算中心点 - # center = Point3((minPoint.x + maxPoint.x) * 0.5, - # (minPoint.y + maxPoint.y) * 0.5, - # (minPoint.z + maxPoint.z) * 0.5) - # self.gizmo.setPos(center) - self._last_gizmo_bounds_update = current_time - - is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False - - # 安区地更新朝向 - - if is_scale_tool: - #self.gizmo.setHpr(self.gizmoTarget.getHpr()) - self.gizmo.setQuat(self.gizmoTarget.getQuat(self.world.render)) - else: - parent_node = self.gizmoTarget.getParent() - if parent_node and parent_node != self.world.render: - parent_hpr = parent_node.getHpr() - self.gizmo.setHpr(parent_hpr) - else: - self.gizmo.setHpr(0,0,0) - - # 更新朝向 - # parent_node = self.gizmoTarget.getParent() - # if parent_node and parent_node != self.world.render: - # parent_hpr = parent_node.getHpr() - # self.gizmo.setHpr(parent_hpr) - # else: - # self.gizmo.setHpr(0, 0, 0) - - def _updateGizmoScreenSize(self): - """动态调整坐标轴大小,保持固定的屏幕大小""" - try: - if not self.gizmo or not self.gizmoTarget: - return - - # 计算相机到坐标轴的距离 - gizmo_world_pos = self.gizmo.getPos(self.world.render) - cam_pos = self.world.cam.getPos() - distance_to_gizmo = (cam_pos - gizmo_world_pos).length() - - # 获取相机视野角度和窗口尺寸 - fov = self.world.cam.node().getLens().getFov()[0] # 水平视野角度 - fov_radians = math.radians(fov) - winWidth, winHeight = self.world.getWindowSize() - - # 计算一个像素在坐标轴距离处对应的世界坐标大小 - pixel_to_world_ratio = distance_to_gizmo * math.tan(fov_radians / 2) / (winWidth / 2) - - # 设定坐标轴在屏幕上的期望像素长度(固定值) - desired_screen_length = 120 # 像素 - - # 计算世界坐标系中的轴长度 - world_axis_length = desired_screen_length * pixel_to_world_ratio - - # 计算缩放比例(相对于基础轴长度) - scale_factor = world_axis_length / self.axis_length - - # 应用缩放到坐标轴 - self.gizmo.setScale(scale_factor) - - # 限制缩放范围,避免过大或过小 - min_scale = 0.001 - max_scale = 100.0 - final_scale = max(min_scale, min(max_scale, scale_factor)) - - if final_scale != scale_factor: - self.gizmo.setScale(final_scale) - - except Exception as e: - # 静默处理错误,避免频繁输出 - pass - - def clearGizmo(self): - """清除坐标轴""" - if self.gizmo: - self.gizmo.removeNode() - self.gizmo = None - - # 停止坐标轴更新任务 - taskMgr.remove("updateGizmo") - - # 清除坐标轴相关引用 - self.gizmoTarget = None - self.gizmoXAxis = None - self.gizmoYAxis = None - self.gizmoZAxis = None - self.isDraggingGizmo = False - self.dragGizmoAxis = None - self.dragStartMousePos = None - self.gizmoTargetStartPos = None - self.gizmoStartPos = None - self._resetCursor() - - - # def setGizmoAxisColor(self, axis, color): - # """设置坐标轴颜色 - RenderPipeline 兼容版本""" - # try: - # from panda3d.core import AntialiasAttrib,TransparencyAttrib - # - # axis_nodes = { - # "x": self.gizmoXAxis, - # "y": self.gizmoYAxis, - # "z": self.gizmoZAxis - # } - # - # if axis in axis_nodes and axis_nodes[axis]: - # axis_node = axis_nodes[axis] - # - # axis_node.setColor(color[0]*20.0,color[1]*20.0,color[2]*20.0,color[3]) - # axis_node.setColorScale(color[0]*10.0,color[1]*10.0,color[2]*10.0,color[3]) - # axis_node.setShaderOff(10000) - # axis_node.setLightOff() - # axis_node.setMaterialOff() - # axis_node.setTextureOff() - # axis_node.setFogOff() - # - # except Exception as e: - # print(f"设置坐标轴颜色失败: {str(e)}") - # # 回退到简单的颜色设置 - # try: - # if axis in axis_nodes and axis_nodes[axis]: - # axis_nodes[axis].setColor(*color) - # except: - # pass - - - def setGizmoRotAxisColor(self, axis, color): - """使用材质设置坐标轴颜色 - RenderPipeline兼容版本""" - try: - from panda3d.core import Material, Vec4,ColorWriteAttrib,DepthWriteAttrib,DepthTestAttrib,TransparencyAttrib - - # 获取对应的轴节点 - axis_nodes = { - "x": self.gizmoRotXAxis, - "y": self.gizmoRotYAxis, - "z": self.gizmoRotZAxis - } - - if axis not in axis_nodes or not axis_nodes[axis]: - return - - axis_node = axis_nodes[axis] - - handle_node = None - handle_node = axis_node.find("x_handle") if axis == "x" else handle_node - handle_node = axis_node.find("y_handle") if axis == "y" else handle_node - handle_node = axis_node.find("z_handle") if axis == "z" else handle_node - - #如果找不到特定名称的节点,尝试查找任何子节点 - if not handle_node: - children = axis_node.getChildren() - if children.getNumPath()>0: - handle_node = children[0] - - if not handle_node: - #print(f"未找到{axis}轴的处理模型") - return - - # 创建或获取材质 - mat = Material() - - # 设置材质属性 - 使用更自然的颜色,避免过亮的自发光 - adjusted_color = Vec4( - min(color[0]*20, 1.0), - min(color[1]*20, 1.0), - min(color[2]*20, 1.0), - color[3] - ) - - mat.setBaseColor(adjusted_color) - #mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光 - - # 应用材质 - handle_node.setMaterial(mat, 1) - - - # 设置透明度 - if color[3] < 1.0: - handle_node.setTransparency(TransparencyAttrib.MAlpha) - else: - handle_node.setTransparency(TransparencyAttrib.MNone) - - handle_node.setLightOff() # 禁用光照影响 - handle_node.setShaderOff() # 禁用着色器 - handle_node.setFogOff() # 禁用雾效果 - - handle_node.setBin("fixed",41) - handle_node.setDepthWrite(False) - handle_node.setDepthTest(False) - - # 保存材质引用以便后续修改 - if axis == "x": - self.xMat = mat - elif axis == "y": - self.yMat = mat - elif axis == "z": - self.zMat = mat - - axis_node.setLightOff() - axis_node.setShaderOff() - axis_node.setFogOff() - axis_node.setBin("fixed", 40) - axis_node.setDepthWrite(False) - axis_node.setDepthTest(False) - - except Exception as e: - print(f"设置坐标轴颜色失败: {str(e)}") - # 回退到简单颜色设置 - try: - axis_nodes = { - "x": self.gizmoXAxis, - "y": self.gizmoYAxis, - "z": self.gizmoZAxis - } - - if axis in axis_nodes and axis_nodes[axis]: - axis_nodes[axis].setColor(color[0], color[1], color[2], color[3]) - except: - pass - - def setGizmoAxisColor(self, axis, color): - """使用材质设置坐标轴颜色 - RenderPipeline兼容版本""" - try: - from panda3d.core import Material, Vec4,ColorWriteAttrib,DepthWriteAttrib,DepthTestAttrib,TransparencyAttrib - - # 获取对应的轴节点 - axis_nodes = { - "x": self.gizmoXAxis, - "y": self.gizmoYAxis, - "z": self.gizmoZAxis - } - - if axis not in axis_nodes or not axis_nodes[axis]: - return - - axis_node = axis_nodes[axis] - - if axis_node.isEmpty(): - return - - handle_node = None - handle_node = axis_node.find("x_handle") if axis == "x" else handle_node - handle_node = axis_node.find("y_handle") if axis == "y" else handle_node - handle_node = axis_node.find("z_handle") if axis == "z" else handle_node - - # 如果找不到特定名称的节点,尝试查找任何子节点 - if not handle_node or handle_node.isEmpty(): - children = axis_node.getChildren() - if children.getNumPaths() > 0: - handle_node = children[0] - - if not handle_node: - # print(f"未找到{axis}轴的处理模型") - return - - # 创建或获取材质 - mat = Material() - - adjusted_color = Vec4( - min(color[0], 1.0), - min(color[1], 1.0), - min(color[2], 1.0), - color[3] - ) - - mat.setBaseColor(adjusted_color) - #mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光 - - # 应用材质 - handle_node.setMaterial(mat, 1) - - # 设置透明度 - if color[3] < 1.0: - handle_node.setTransparency(TransparencyAttrib.MAlpha) - else: - handle_node.setTransparency(TransparencyAttrib.MNone) - - handle_node.setLightOff() # 禁用光照影响 - handle_node.setShaderOff() # 禁用着色器 - handle_node.setFogOff() # 禁用雾效果 - - handle_node.setBin("fixed",41) - handle_node.setDepthWrite(False) - handle_node.setDepthTest(False) - - # 保存材质引用以便后续修改 - if axis == "x": - self.xMat = mat - elif axis == "y": - self.yMat = mat - elif axis == "z": - self.zMat = mat - - axis_node.setLightOff() - axis_node.setShaderOff() - axis_node.setFogOff() - axis_node.setBin("fixed", 40) - axis_node.setDepthWrite(False) - axis_node.setDepthTest(False) - - except Exception as e: - print(f"设置坐标轴颜色失败: {str(e)}") - # 回退到简单颜色设置 - try: - axis_nodes = { - "x": self.gizmoXAxis, - "y": self.gizmoYAxis, - "z": self.gizmoZAxis - } - - if axis in axis_nodes and axis_nodes[axis]: - axis_nodes[axis].setColor(color[0], color[1], color[2], color[3]) - except: - pass - - # ==================== 鼠标交互处理 ==================== - - def _onGizmoMouseEnter(self, axis): - """鼠标进入坐标轴时的处理 - RenderPipeline 兼容版本""" - try: - # 黄色高亮,增加透明度以确保在 RenderPipeline 下可见 - highlight_color = (1.0, 1.0, 0.0, 1) - self.setGizmoAxisColor(axis, highlight_color) - - # 额外的视觉反馈 - axis_nodes = { - "x": self.gizmoXAxis, - "y": self.gizmoYAxis, - "z": self.gizmoZAxis - } - - if axis in axis_nodes and axis_nodes[axis]: - # 稍微放大以增强视觉效果 - axis_nodes[axis].setScale(1.1) - - self.gizmoHighlightAxis = axis - - except Exception as e: - print(f"鼠标进入坐标轴处理失败: {e}") - - def _onGizmoMouseLeave(self, axis): - """鼠标离开坐标轴时的处理 - RenderPipeline 兼容版本""" - try: - # 恢复原始颜色 - original_colors = { - "x": (1.0, 0.0, 0.0, 1.0), # 红色 - "y": (0.0, 1.0, 0.0, 1.0), # 绿色 - "z": (0.0, 0.0, 1.0, 1.0) # 蓝色 - } - - if axis in original_colors: - self.setGizmoAxisColor(axis, original_colors[axis]) - - # 恢复原始大小 - axis_nodes = { - "x": self.gizmoXAxis, - "y": self.gizmoYAxis, - "z": self.gizmoZAxis - } - - if axis in axis_nodes and axis_nodes[axis]: - axis_nodes[axis].setScale(1.0) - - if self.gizmoHighlightAxis == axis: - self.gizmoHighlightAxis = None - - except Exception as e: - print(f"鼠标离开坐标轴处理失败: {e}") - - def setupGizmoMouseEvents(self): - """设置坐标轴的鼠标事件""" - try: - from direct.showbase.DirectObject import DirectObject - - # 为每个轴设置鼠标事件 - axis_nodes = { - "x": self.gizmoXAxis, - "y": self.gizmoYAxis, - "z": self.gizmoZAxis - } - - for axis_name, axis_node in axis_nodes.items(): - if axis_node: - # 设置碰撞检测标签 - axis_node.setTag("gizmo_axis", axis_name) - axis_node.setTag("pickable", "1") - - - except Exception as e: - print(f"设置坐标轴鼠标事件失败: {e}") - - # ==================== 射线检测和碰撞检测 ==================== - - def checkGizmoClick(self, mouseX, mouseY): - """使用屏幕空间检测是否点击了坐标轴""" - if not self.gizmo or not self.gizmoTarget: - return None - - # 基本参数验证 - if not isinstance(mouseX, (int, float)) or not isinstance(mouseY, (int, float)): - return None - - try: - # 获取坐标轴中心的世界坐标 - gizmo_world_pos = self.gizmo.getPos(self.world.render) - - # 计算各轴端点的世界坐标 - x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0) - y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0) - z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length) - - # 使用Panda3D的内置投影方法 - def worldToScreen(world_pos): - """将世界坐标转换为屏幕坐标""" - # 将世界坐标转换为相机空间 - cam_space_pos = self.world.cam.getRelativePoint(self.world.render, world_pos) - - # 检查是否在相机前方 - if cam_space_pos.getY() <= 0: - return None - - # 使用相机的镜头进行投影 - screen_pos = Point2() - if self.world.cam.node().getLens().project(cam_space_pos, screen_pos): - # 获取准确的窗口尺寸 - win_width, win_height = self.world.getWindowSize() - - # 转换为窗口像素坐标 - win_x = (screen_pos.getX() + 1.0) * 0.5 * win_width - win_y = (1.0 - screen_pos.getY()) * 0.5 * win_height - return (win_x, win_y) - return None - - # 投影各个关键点 - center_screen = worldToScreen(gizmo_world_pos) - x_screen = worldToScreen(x_end) - y_screen = worldToScreen(y_end) - z_screen = worldToScreen(z_end) - - # 如果无法获得屏幕坐标,使用备用方法 - if not center_screen: - return self.checkGizmoClickFallback(mouseX, mouseY) - - # 计算点击阈值 - click_threshold = 15 # 增大检测范围 - - # 检测各个轴,对于端点在屏幕外的轴提供回退方案 - def getClickDetectionPoint(axis_name, original_screen_pos): - if original_screen_pos: - return original_screen_pos - # 如果端点在屏幕外,使用轴长度的一半作为检测点 - if axis_name == "x": - half_end = gizmo_world_pos + Vec3(self.axis_length * 0.5, 0, 0) - elif axis_name == "y": - half_end = gizmo_world_pos + Vec3(0, self.axis_length * 0.5, 0) - elif axis_name == "z": - half_end = gizmo_world_pos + Vec3(0, 0, self.axis_length * 0.5) - else: - return None - return worldToScreen(half_end) - - axes_data = [ - ("x", getClickDetectionPoint("x", x_screen), "X轴"), - ("y", getClickDetectionPoint("y", y_screen), "Y轴"), - ("z", getClickDetectionPoint("z", z_screen), "Z轴") - ] - - for axis_name, axis_screen, axis_label in axes_data: - if axis_screen: - # 计算鼠标到轴线的距离 - distance = self.distanceToLine( - (mouseX, mouseY), center_screen, axis_screen - ) - #print(f"{axis_label}距离: {distance:.2f}") - - if distance < click_threshold: - #print(f"✓ 点击了{axis_label}") - return axis_name - - return None - - except Exception as e: - print(f"坐标轴点击检测失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def checkGizmoClickFallback(self, mouseX, mouseY): - """备用检测方法:使用固定的屏幕区域""" - - # 获取准确的窗口尺寸 - win_width, win_height = self.world.getWindowSize() - - # 获取窗口中心作为参考点 - center_x = win_width // 2 - center_y = win_height // 2 - - # 定义相对于中心的轴区域(简化假设坐标轴在屏幕中心附近) - axis_length_pixels = 100 # 假设轴长度在屏幕上约100像素 - - # X轴:从中心向右 - x_start = (center_x, center_y) - x_end = (center_x + axis_length_pixels, center_y) - - # Y轴:从中心向上(注意Y轴方向) - y_start = (center_x, center_y) - y_end = (center_x, center_y - axis_length_pixels) - - # Z轴:从中心向右上45度 - z_start = (center_x, center_y) - z_end = (center_x + axis_length_pixels * 0.7, center_y - axis_length_pixels * 0.7) - - threshold = 25 - - # 检测各轴 - if self.distanceToLine((mouseX, mouseY), x_start, x_end) < threshold: - print("✓ 备用方法检测到X轴") - return "x" - elif self.distanceToLine((mouseX, mouseY), y_start, y_end) < threshold: - print("✓ 备用方法检测到Y轴") - return "y" - elif self.distanceToLine((mouseX, mouseY), z_start, z_end) < threshold: - print("✓ 备用方法检测到Z轴") - return "z" - - print("× 备用方法也没有检测到") - return None - - def distanceToLine(self, point, line_start, line_end): - """计算点到线段的距离""" - try: - px, py = point - x1, y1 = line_start - x2, y2 = line_end - - # 计算线段长度 - line_length = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5 - if line_length == 0: - return ((px - x1) ** 2 + (py - y1) ** 2) ** 0.5 - - # 计算点到线的距离 - t = max(0, min(1, ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / (line_length ** 2))) - projection_x = x1 + t * (x2 - x1) - projection_y = y1 + t * (y2 - y1) - - distance = ((px - projection_x) ** 2 + (py - projection_y) ** 2) ** 0.5 - return distance - - except Exception as e: - print(f"距离计算错误: {e}") - return float('inf') - - # ==================== 高亮和交互 ==================== - - def detectGizmoAxisAtMouse(self, mouseX, mouseY): - """统一的坐标轴检测方法 - 同时用于高亮和点击检测""" - if not self.gizmo or not self.gizmoTarget: - return None - - try: - # 获取坐标轴中心的世界坐标 - gizmo_world_pos = self.gizmo.getPos(self.world.render) - - #获取坐标轴的世界朝向(考虑旋转) - gizmo_world_quat = self.gizmo.getQuat(self.world.render) - - #计算各轴在世界坐标系中的实际方向向量 - x_axis_world = gizmo_world_quat.xform(Vec3(1,0,0)) - y_axis_world = gizmo_world_quat.xform(Vec3(0,1,0)) - z_axis_world = gizmo_world_quat.xform(Vec3(0,0,1)) - - x_end = gizmo_world_pos + x_axis_world * self.axis_length - y_end = gizmo_world_pos + y_axis_world * self.axis_length - z_end = gizmo_world_pos + z_axis_world * self.axis_length - - - # 计算各轴端点的世界坐标 - # x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0) - # y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0) - # z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length) - - # 将3D坐标投影到屏幕坐标 - def worldToScreen(worldPos): - try: - # 转换为相机坐标系 - camPos = self.world.cam.getRelativePoint(self.world.render, worldPos) - - # 检查点是否在相机前方 - if camPos.getY() <= 0: - return None - - # 使用相机lens进行投影 - screenPos = Point2() - lens = self.world.cam.node().getLens() - - if lens.project(camPos, screenPos): - # 获取准确的窗口尺寸 - winWidth, winHeight = self.world.getWindowSize() - - # 转换为像素坐标 - winX = (screenPos.x + 1) * 0.5 * winWidth - winY = (1 - screenPos.y) * 0.5 * winHeight - return (winX, winY) - return None - except: - return None - - # 获取各坐标轴的屏幕投影 - gizmo_screen = worldToScreen(gizmo_world_pos) - x_screen = worldToScreen(x_end) - y_screen = worldToScreen(y_end) - z_screen = worldToScreen(z_end) - - # 如果坐标轴中心不在屏幕内,返回None - if not gizmo_screen: - return None - - # 设置检测阈值 - click_threshold = 35 # 统一使用25像素的检测阈值 - - # 更准确的点到线段距离计算方法 - def distanceToLineSegment(mousePos, start, end): - import math - mx, my = mousePos - x1, y1 = start - x2, y2 = end - - # 线段向量 - dx = x2 - x1 - dy = y2 - y1 - - # 线段长度平方 - length_sq = dx * dx + dy * dy - - if length_sq == 0: - # 线段退化为点 - return math.sqrt((mx - x1) ** 2 + (my - y1) ** 2) - - # 投影参数 - t = max(0, min(1, ((mx - x1) * dx + (my - y1) * dy) / length_sq)) - - # 投影点坐标 - proj_x = x1 + t * dx - proj_y = y1 + t * dy - - # 返回点到投影点的距离 - return math.sqrt((mx - proj_x) ** 2 + (my - proj_y) ** 2) - - mouse_pos = (mouseX, mouseY) - - # 检测各个轴 - 按优先级检测(Z > X > Y) - axes_to_check = [ - ("z", z_screen), - ("x", x_screen), - ("y", y_screen) - ] - - for axis_name, axis_end in axes_to_check: - if axis_end: - distance = distanceToLineSegment(mouse_pos, gizmo_screen, axis_end) - if distance < click_threshold: - return axis_name - - # 如果没有检测到,返回None - return None - - except Exception as e: - # 静默处理错误,避免频繁输出 - return None - - def updateGizmoHighlight(self, mouseX, mouseY): - """更新坐标轴高亮状态""" - if not self.gizmo or self.isDraggingGizmo: - self._resetCursor() - return - - # 使用碰撞检测方法 - #hoveredAxis = self.detectGizmoAxisWithCollision(mouseX, mouseY) - hoveredAxis = self.detectGizmoAxisAtMouse(mouseX, mouseY) - - # 简化稳定性检测逻辑 - if not hasattr(self, '_last_detected_axis'): - self._last_detected_axis = None - - # 如果检测结果发生变化,立即更新高亮 - if hoveredAxis != self._last_detected_axis: - # 更新轴的高亮状态 - if hoveredAxis != self.gizmoHighlightAxis: - # 恢复之前高亮的轴 - if self.gizmoHighlightAxis: - self.setGizmoAxisColor(self.gizmoHighlightAxis, self.gizmo_colors[self.gizmoHighlightAxis]) - - # 高亮新的轴 - if hoveredAxis: - self.setGizmoAxisColor(hoveredAxis, self.gizmo_highlight_colors[hoveredAxis]) - self._setCursor("pointing_hand") - else: - # 如果没有悬停在任何轴上,确保所有轴都恢复原始颜色 - for axis_name in ["x", "y", "z"]: - if axis_name != self.dragGizmoAxis: # 不要改变正在拖拽的轴的颜色 - self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) - self._resetCursor() - - self.gizmoHighlightAxis = hoveredAxis - - self._last_detected_axis = hoveredAxis - elif hoveredAxis is None: - self._resetCursor() - - def _detectHoveredAxis(self, mouseX, mouseY): - """检测鼠标悬停的轴 - 提取为独立方法""" - # 将原来 updateGizmoHighlight 中的检测逻辑移到这里 - # ... 你原来的检测代码 ... - pass - - def _updateAxisHighlight(self, hoveredAxis): - """更新轴高亮状态 - 确保原子性操作""" - # 恢复之前高亮的轴 - if self.gizmoHighlightAxis: - self.setGizmoAxisColor(self.gizmoHighlightAxis, self.gizmo_colors[self.gizmoHighlightAxis]) - - # 高亮新的轴 - if hoveredAxis: - self.setGizmoAxisColor(hoveredAxis, self.gizmo_highlight_colors[hoveredAxis]) - - self.gizmoHighlightAxis = hoveredAxis - - # ==================== 拖拽变换 ==================== - - def startGizmoDrag(self, axis, mouseX, mouseY): - """开始坐标轴拖拽""" - try: - # 确保状态正确初始化 - if not self.gizmoTarget: - print("开始拖拽失败: 没有拖拽目标") - return - if not self.gizmo: - print("开始拖拽失败: 没有坐标轴") - return - - self.isDraggingGizmo = True - - # 使用当前高亮的轴,如果有的话;否则使用传入的轴 - if self.gizmoHighlightAxis: - self.dragGizmoAxis = self.gizmoHighlightAxis - elif axis and axis in self.gizmo_colors: - self.dragGizmoAxis = axis - else: - # 如果没有明确指定轴,尝试通过鼠标位置检测 - self.dragGizmoAxis = self.detectGizmoAxisAtMouse(mouseX, mouseY) - - # 如果仍然无法确定拖拽轴,则取消拖拽 - if not self.dragGizmoAxis: - print("开始拖拽失败: 无法确定拖拽轴") - self.isDraggingGizmo = False - return - - self.dragStartMousePos = (mouseX, mouseY) - - light_object = self.gizmoTarget.getPythonTag("rp_light_object") - if light_object: - self.gizmoTargetStartPos = Point3(light_object.pos) - else: - self.gizmoTargetStartPos = self.gizmoTarget.getPos() - - # 保存开始拖拽时目标节点的位置和坐标轴的位置 - #self.gizmoTargetStartPos = self.gizmoTarget.getPos() - self.gizmoStartPos = self.gizmo.getPos(self.world.render) # 坐标轴的世界位置 - - # 添加对缩放的支持:保存初始缩放值 - if self.world.tool_manager.isScaleTool(): - self.gizmoTargetStartScale = self.gizmoTarget.getScale() - elif self.world.tool_manager.isRotateTool(): - self.gizmoTargetStartHpr = self.gizmoTarget.getHpr() - - # 确保正在拖动的轴保持高亮状态 - if self.dragGizmoAxis and self.dragGizmoAxis in self.gizmo_colors: - # 先将所有轴恢复为正常颜色 - for axis_name in self.gizmo_colors.keys(): - if axis_name != self.dragGizmoAxis: - self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) - - # 然后将当前拖动的轴设置为高亮颜色 - self.setGizmoAxisColor(self.dragGizmoAxis, self.gizmo_highlight_colors[self.dragGizmoAxis]) - # elif axis and axis in self.gizmo_colors: - # for axis_name in self.gizmo_colors.keys(): - # if axis_name != axis: - # self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) - # - # self.setGizmoAxisColor(axis, self.gizmo_highlight_colors[axis]) - # self.dragGizmoAxis = axis - # - # self.gizmoHighlightAxis = self.dragGizmoAxis - # 设置拖拽光标 - if self.dragGizmoAxis == "x": - self._setCursor("size_all") # 水平调整光标 - elif self.dragGizmoAxis == "y": - self._setCursor("size_all") # 垂直调整光标 - elif self.dragGizmoAxis == "z": - self._setCursor("size_all") # 全向调整光标 - - print( - f"开始拖拽 {self.dragGizmoAxis} 轴 - 目标起始位置: {self.gizmoTargetStartPos}, 坐标轴位置: {self.gizmoStartPos}, 鼠标: ({mouseX}, {mouseY})") - - except Exception as e: - print(f"开始坐标轴拖拽失败: {str(e)}") - import traceback - traceback.print_exc() - - def updateGizmoDrag(self, mouseX, mouseY): - """更新坐标轴拖拽 - 使用正确的坐标系变换,支持旋转后的子节点拖拽""" - try: - # 添加详细的状态检查和调试信息 - if not self.isDraggingGizmo: - print("拖拽更新失败: 不在拖拽状态") - return - if not self.gizmoTarget: - print("拖拽更新失败: 没有拖拽目标") - return - if not hasattr(self, 'dragStartMousePos') or not self.dragStartMousePos: - print("拖拽更新失败: 没有拖拽起始位置") - return - if not hasattr(self, 'gizmoTargetStartPos') or not self.gizmoTargetStartPos: - print("拖拽更新失败: 没有目标起始位置") - return - if not hasattr(self, 'gizmoStartPos') or not self.gizmoStartPos: - print("拖拽更新失败: 没有坐标轴起始位置") - return - - is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False - is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False - - is_gui_element = (hasattr(self.gizmoTarget,'getTag') and - self.gizmoTarget.getTag("is_gui_element") == "1") - - - # 计算鼠标移动距离(屏幕像素) - mouseDeltaX = mouseX - self.dragStartMousePos[0] - mouseDeltaY = mouseY - self.dragStartMousePos[1] - - if is_scale_tool: - scale_factor = 1.0 + (mouseDeltaX + mouseDeltaY) * 0.01 - - scale_factor = max(0.001, scale_factor) - - start_scale = getattr(self,'gizmoTargetStartScale',Vec3(1,1,1)) - - if is_gui_element: - if self.dragGizmoAxis == "x": - new_scale = Vec3(start_scale.x * scale_factor,start_scale.y,start_scale.z) - elif self.dragGizmoAxis == "y": - new_scale = Vec3(start_scale.x, start_scale.y * scale_factor, start_scale.z) - elif self.dragGizmoAxis == "z": - new_scale = Vec3(start_scale.x, start_scale.y, start_scale.z * scale_factor) - else: - new_scale = Vec3(start_scale.x * scale_factor, - start_scale.y * scale_factor, - start_scale.z * scale_factor) - else: - # 普通3D模型的缩放处理 - if self.dragGizmoAxis == "x": - new_scale = Vec3(start_scale.x * scale_factor, start_scale.y, start_scale.z) - elif self.dragGizmoAxis == "y": - new_scale = Vec3(start_scale.x, start_scale.y * scale_factor, start_scale.z) - elif self.dragGizmoAxis == "z": - z_scale_factor = 1.0 - (mouseDeltaX + mouseDeltaY) * 0.01 - new_scale = Vec3(start_scale.x, start_scale.y, start_scale.z * z_scale_factor) - else: - new_scale = Vec3(start_scale.x * scale_factor, - start_scale.y * scale_factor, - start_scale.z * scale_factor) - - new_scale = Vec3( - max(0.001,new_scale.x), - max(0.001,new_scale.y), - max(0.001,new_scale.z) - ) - - # 应用新缩放值 - self.gizmoTarget.setScale(new_scale) - self.world.property_panel.refreshModelValues(self.gizmoTarget) - return - elif is_rotate_tool: - rotation_speed = 0.5 - rotation_amount = (mouseDeltaX + mouseDeltaY) * rotation_speed - start_hpr = getattr(self,'gizmoTargetStartHpr',self.gizmoTarget.getHpr()) - - if self.dragGizmoAxis == "x": - new_hpr = Vec3(start_hpr.x+rotation_amount,start_hpr.y,start_hpr.z) - elif self.dragGizmoAxis == "y": - new_hpr = Vec3(start_hpr.x,start_hpr.y-rotation_amount,start_hpr.z) - elif self.dragGizmoAxis == "z": - new_hpr = Vec3(start_hpr.x,start_hpr.y,start_hpr.z+rotation_amount) - else: - # 默认绕所有轴旋转 - new_hpr = Vec3(start_hpr.x + rotation_amount, - start_hpr.y + rotation_amount, - start_hpr.z + rotation_amount) - self.gizmoTarget.setHpr(new_hpr) - self.world.property_panel.refreshModelValues(self.gizmoTarget) - return - - # 使用坐标轴的实际位置而不是目标节点位置来计算屏幕投影 - gizmo_world_pos = self.gizmoStartPos - - # 【关键修复】:获取正确的轴向量,考虑父节点的旋转 - # 检查目标节点是否有父节点 - parent_node = self.gizmoTarget.getParent() - - # 计算轴向量在正确坐标系中的方向 - if self.dragGizmoAxis == "x": - # 在局部坐标系中的X轴方向 - local_axis_vector = Vec3(1, 0, 0) - elif self.dragGizmoAxis == "y": - # 在局部坐标系中的Y轴方向 - local_axis_vector = Vec3(0, 1, 0) - elif self.dragGizmoAxis == "z": - # 在局部坐标系中的Z轴方向 - local_axis_vector = Vec3(0, 0, 1) - else: - print(f"拖拽更新失败: 未知轴类型 {self.dragGizmoAxis}") - return - - world_axis_vector = local_axis_vector - - if parent_node and parent_node != self.world.render: - try: - #获取变换矩阵 - transfrom_mat = parent_node.getMat(self.world.render) - if transfrom_mat.is_identity() or self._isMatrixValid(transfrom_mat): - world_axis_vector = transfrom_mat.xformVec(local_axis_vector) - else: - print("警告: 检测到无效变换矩阵,使用默认轴向量") - except Exception as e: - print(f"变换计算出错: {e},使用默认轴向量") - else: - world_axis_vector = local_axis_vector - - # 确定轴向量的变换上下文 - # if parent_node and parent_node != self.world.render: - # transform_mat = parent_node.getMat(self.world.render) - # world_axis_vector = transform_mat.xformVec(local_axis_vector) - # else: - # world_axis_vector = local_axis_vector - - #axis_end = gizmo_world_pos + world_axis_vector - - # 投影到屏幕空间 - def worldToScreen(worldPos): - try: - camPos = self.world.cam.getRelativePoint(self.world.render, worldPos) - if camPos.getY() <= 0: - return None - - screenPos = Point2() - if self.world.cam.node().getLens().project(camPos, screenPos): - winWidth, winHeight = self.world.getWindowSize() - winX = (screenPos.x + 1) * 0.5 * winWidth - winY = (1 - screenPos.y) * 0.5 * winHeight - return (winX, winY) - return None - except Exception as e: - print(f"世界坐标转屏幕坐标失败: {e}") - return None - axis_start_screen = worldToScreen(gizmo_world_pos) - axis_end_world = gizmo_world_pos + world_axis_vector - axis_end_screen = worldToScreen(axis_end_world) - - if not axis_start_screen or not axis_end_screen: - print("拖拽更新失败: 无法获取轴线屏幕坐标") - return - - screen_axis_dir = ( - axis_end_screen[0] - axis_start_screen[0], - axis_end_screen[1] - axis_start_screen[1] - ) - - # 归一化屏幕轴方向 - import math - length = math.sqrt(screen_axis_dir[0]**2 + screen_axis_dir[1]**2) - if length > 0: - #screen_axis_dir = (screen_axis_dir[0] / length, screen_axis_dir[1] / length) - screen_axis_dir = ( - screen_axis_dir[0] / length, - screen_axis_dir[1] / length - ) - - else: - print("拖拽更新失败: 屏幕轴方向长度为0") - return - - # 将鼠标移动投影到轴方向上 - projected_distance = (mouseDeltaX * screen_axis_dir[0] + - mouseDeltaY * screen_axis_dir[1]) - - cam_pos = self.world.cam.getPos(self.world.render) - distance_to_object = (cam_pos - gizmo_world_pos).length() - - lens = self.world.cam.node().getLens() - fov = lens.getFov()[0] - winWidth,winHeight = self.world.getWindowSize() - - pixels_to_world_units = (2*distance_to_object*math.tan(math.radians(fov/2)))/winWidth - - movement_distance = projected_distance * pixels_to_world_units - - total_scale_factor = 1.0 - current_node = self.gizmoTarget.getParent() - - while current_node and current_node != self.world.render: - try: - if not current_node.isEmpty(): - node_scale = current_node.getScale() - if node_scale.x > 0 and node_scale.y >0 and node_scale.z >0 : - avg_scale = (node_scale.x + node_scale.y + node_scale.z)/3.0 - total_scale_factor *= avg_scale - #avg_scale = (node_scale.x+node_scale.y + node_scale.z) / 3.0 - #total_scale_factor *= avg_scale - current_node = current_node.getParent() - else: - break - except: - break - - - if total_scale_factor > 0: - movement_distance = movement_distance / total_scale_factor - - currentPos = self.gizmoTargetStartPos - - # 根据拖拽的轴,只修改对应的坐标分量 - if self.dragGizmoAxis == "x": - newPos = Vec3(currentPos.x + movement_distance, currentPos.y, currentPos.z) - #print(f"X轴移动:{currentPos.x} -> {newPos.x}") - elif self.dragGizmoAxis == "y": - newPos = Vec3(currentPos.x, currentPos.y + movement_distance, currentPos.z) - #print(f"Y轴移动:{currentPos.y} -> {newPos.y}") - elif self.dragGizmoAxis == "z": - newPos = Vec3(currentPos.x, currentPos.y, currentPos.z + movement_distance) - #print(f"Z轴移动:{currentPos.z} -> {newPos.z}") - else: - print(f"未知轴: {self.dragGizmoAxis}") - return - - # 应用新位置到目标节点 - light_object = self.gizmoTarget.getPythonTag("rp_light_object") - if light_object: - light_object.pos = newPos - self.gizmoTarget.setPos(newPos) - else: - self.gizmoTarget.setPos(newPos) - - # 更新坐标轴位置 - 计算新的中心位置 - minPoint = Point3() - maxPoint = Point3() - if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render): - center = Point3((minPoint.x + maxPoint.x) * 0.5, - (minPoint.y + maxPoint.y) * 0.5, - (minPoint.z + maxPoint.z) * 0.5) - self.gizmo.setPos(center) - - # 实时更新属性面板 - self.world.property_panel.refreshModelValues(self.gizmoTarget) - - # 每次拖拽都输出调试信息(但限制频率) - if not hasattr(self, '_last_drag_debug_time'): - self._last_drag_debug_time = 0 - - import time - current_time = time.time() - if current_time - self._last_drag_debug_time > 0.1: # 每0.1秒最多输出一次 - #print(f"拖拽更新成功 - 轴:{self.dragGizmoAxis}, 比例:{scale_factor:.6f}, 投影:{projected_distance:.2f}") - self._last_drag_debug_time = current_time - - except Exception as e: - print(f"更新坐标轴拖拽失败: {str(e)}") - import traceback - traceback.print_exc() - - def _isMatrixValid(self, matrix): - """检查矩阵是否有效,替代 isSingular 方法""" - try: - # 检查矩阵元素是否为有效数字 - for i in range(4): - for j in range(4): - element = matrix.getCell(i, j) - # 检查是否为 NaN 或无穷大 - if str(element) == 'nan' or str(element) == 'inf' or str(element) == '-inf': - return False - # 检查是否过大 - if abs(element) > 1e10: - return False - return True - except: - return False - - def stopGizmoDrag(self): - """停止坐标轴拖拽""" - print(f"停止坐标轴拖拽 - 轴: {self.dragGizmoAxis}") - - if hasattr(self.world,'command_manager') and self.world.command_manager and self.gizmoTarget: - current_pos = self.gizmoTarget.getPos() - - if (hasattr(self,'gizmoTargetStartPos') and self.gizmoTargetStartPos and - (abs(current_pos.x-self.gizmoTargetStartPos.x)>0.001 or - abs(current_pos.y-self.gizmoTargetStartPos.y)>0.001 or - abs(current_pos.z-self.gizmoTargetStartPos.z)>0.001)): - from core.Command_System import MoveNodeCommand - from core.Command_System import MoveLightCommand - - light_object = self.gizmoTarget.getPythonTag("rp_light_object") - if light_object: - command = MoveLightCommand(self.gizmoTarget,self.gizmoTargetStartPos,current_pos,light_object) - else: - command = MoveNodeCommand(self.gizmoTarget,self.gizmoTargetStartPos,current_pos) - self.world.command_manager.execute_command(command) - # 如果是缩放操作且缩放发生了变化,则创建缩放命令 - elif (hasattr(self, 'gizmoTargetStartScale') and hasattr(self, 'gizmoTargetStartScale') and - self.gizmoTargetStartScale): - current_scale = self.gizmoTarget.getScale() - if (abs(current_scale.x - self.gizmoTargetStartScale.x) > 0.001 or - abs(current_scale.y - self.gizmoTargetStartScale.y) > 0.001 or - abs(current_scale.z - self.gizmoTargetStartScale.z) > 0.001): - from core.Command_System import ScaleNodeCommand - command = ScaleNodeCommand(self.gizmoTarget, self.gizmoTargetStartScale, current_scale) - self.world.command_manager.execute_command(command) - # 如果是旋转操作且旋转发生了变化,则创建旋转命令 - elif (hasattr(self, 'gizmoTargetStartHpr') and hasattr(self, 'gizmoTargetStartHpr') and - self.gizmoTargetStartHpr): - current_hpr = self.gizmoTarget.getHpr() - if (abs(current_hpr.x - self.gizmoTargetStartHpr.x) > 0.001 or - abs(current_hpr.y - self.gizmoTargetStartHpr.y) > 0.001 or - abs(current_hpr.z - self.gizmoTargetStartHpr.z) > 0.001): - from core.Command_System import RotateNodeCommand - command = RotateNodeCommand(self.gizmoTarget, self.gizmoTargetStartHpr, current_hpr) - self.world.command_manager.execute_command(command) - - # 恢复所有轴的颜色 - for axis_name in ["x", "y", "z"]: - self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) - - self.isDraggingGizmo = False - self.dragGizmoAxis = None - self.dragStartMousePos = None - # 清理拖拽状态,下次拖拽开始时重新设置 - self.gizmoTargetStartPos = None - self.gizmoStartPos = None - - if hasattr(self, 'gizmoTargetStartScale'): - delattr(self, 'gizmoTargetStartScale') - if hasattr(self, 'gizmoTargetStartHpr'): - delattr(self, 'gizmoTargetStartHpr') - - # 重置高亮轴 - self.gizmoHighlightAxis = None - self._resetCursor() - # ==================== 选择管理 ==================== - - def updateSelection(self, nodePath): - try: - if self.selectedNode == nodePath: - return - #print(f"\n=== 更新选择状态 ===") - - # 如果正在删除节点,避免更新选择 - if hasattr(self, '_deleting_node') and self._deleting_node: - print("正在删除节点,跳过选择更新") - #print("=== 选择状态更新完成 ===\n") - return - - node_name = "None" - if nodePath and not nodePath.isEmpty(): - node_name = nodePath.getName() - #print(f"新选择的节点: {node_name}") - - self.selectedNode = nodePath - # 添加兼容性属性 - self.selectedObject = nodePath - - if nodePath and not nodePath.isEmpty(): - node_name = nodePath.getName() - #print(f"开始为节点 {node_name} 创建选择框和坐标轴...") - - # 创建选择框 - #print("创建选择框...") - self.createSelectionBox(nodePath) - if self.selectionBox: - box_name = "Unknown" - if self.selectionBox and not self.selectionBox.isEmpty(): - box_name = self.selectionBox.getName() - #print(f"✓ 选择框创建成功: {box_name}") - else: - print("× 选择框创建失败") - - # 创建坐标轴 - #print("创建坐标轴...") - self.createGizmo(nodePath) - if self.gizmo: - gizmo_name = "Unknown" - if self.gizmo and not self.gizmo.isEmpty(): - gizmo_name = self.gizmo.getName() - #print(f"✓ 坐标轴创建成功: {gizmo_name}") - else: - print("× 坐标轴创建失败") - - else: - print("清除选择...") - self.clearSelectionBox() - self.clearGizmo() - print("✓ 取消选择") - - #当取消选择时,同步清空树形控件的选中状态 - if (hasattr(self.world,'interface_manager')and - self.world.interface_manager and - self.world.interface_manager.treeWidget): - self.world.interface_manager.treeWidget.setCurrentItem(None) - print("✓ 树形控件选中状态已清空") - - #print("=== 选择状态更新完成 ===\n") - except Exception as e: - print(f"更新选择状态失败{str(e)}") - import traceback - traceback.print_exc() - - def _reparentTreeItem(self, item, new_parent_item): - """将树项重新父化到新的父项下""" - if not item or not new_parent_item: - return - - # 从当前父项中移除 - current_parent = item.parent() - if current_parent: - current_parent.removeChild(item) - else: - # 如果是顶级项 - index = self.indexOfTopLevelItem(item) - if index >= 0: - self.takeTopLevelItem(index) - - # 添加到新父项 - new_parent_item.addChild(item) - - # def _updateSelectionVisuals(self, nodePath): - # """更新选择的视觉效果(选择框和坐标轴)""" - # try: - # if nodePath and not nodePath.isEmpty(): - # node_name = nodePath.getName() - # print(f"开始为节点 {node_name} 创建选择框和坐标轴...") - # - # # 创建选择框 - # print("创建选择框...") - # self.createSelectionBox(nodePath) - # if self.selectionBox: - # box_name = "Unknown" - # if self.selectionBox and not self.selectionBox.isEmpty(): - # box_name = self.selectionBox.getName() - # print(f"✓ 选择框创建成功: {box_name}") - # else: - # print("× 选择框创建失败") - # - # # 创建坐标轴 - # print("创建坐标轴...") - # self.createGizmo(nodePath) - # if self.gizmo: - # gizmo_name = "Unknown" - # if self.gizmo and not self.gizmo.isEmpty(): - # gizmo_name = self.gizmo.getName() - # print(f"✓ 坐标轴创建成功: {gizmo_name}") - # else: - # print("× 坐标轴创建失败") - # - # print(f"✓ 选中了节点: {node_name}") - # else: - # print("清除选择...") - # self.clearSelectionBox() - # self.clearGizmo() - # print("✓ 取消选择") - # - # except Exception as e: - # print(f"更新选择视觉效果失败: {e}") - - def getSelectedNode(self): - """获取当前选中的节点""" - return self.selectedNode - - def hasSelection(self): - """检查是否有选中的节点""" - return self.selectedNode is not None - - def checkAndClearIfTargetDeleted(self): - if (self.gizmoTarget and self.gizmoTarget.isEmpty()): - self.clearGizmo() - - if (self.selectionBoxTarget and self.selectionBoxTarget.isEmpty()): - self.clearSelectionBox() - - def setupGizmoCollision(self): - if not self.gizmo or not self.gizmoXAxis or not self.gizmoYAxis or not self.gizmoZAxis: - return - - # 清除现有的碰撞节点 - for axis_name in ["x", "y", "z"]: - axis_node = getattr(self, f"gizmo{axis_name.upper()}Axis") - if axis_node: - # 查找并移除所有现有的碰撞节点 - collision_nodes = axis_node.findAllMatches("**/gizmo_collision_*") - for collision_node in collision_nodes: - collision_node.removeNode() - - # 为每个轴创建碰撞体 - self.createAxisCollision("x", self.gizmoXAxis) - self.createAxisCollision("y", self.gizmoYAxis) - self.createAxisCollision("z", self.gizmoZAxis) - - def createAxisCollision(self, axis_name, axis_node): - # 为单个轴创建碰撞体 - try: - handle_node = axis_node.find(f"{axis_name}_handle") - if not handle_node or handle_node.isEmpty(): - children = axis_node.getChildren() - if children.getNumPaths() > 0: - handle_node = children[0] - else: - print(f"警告: 未找到 {axis_name} 轴的 handle 节点") - return - - collision_node = CollisionNode(f"gizmo_collision_{axis_name}") - collision_node.setIntoCollideMask(BitMask32.bit(1)) # 设置为into对象 - collision_node.setFromCollideMask(BitMask32.allOff()) # 不作为from对象 - - # 调整碰撞尺寸以匹配实际的轴长度和坐标轴缩放 - scale_factor = self.gizmo.getScale().x if self.gizmo else 1.0 - axis_length = 2.0 * scale_factor - radius = 0.3 * scale_factor - - # 根据轴的类型创建合适的碰撞体 - if axis_name == "x": - capsule = CollisionCapsule( - Point3(0, 0, 0), - Point3(axis_length, 0, 0), - radius - ) - collision_node.addSolid(capsule) - elif axis_name == "y": - capsule = CollisionCapsule( - Point3(0, 0, 0), - Point3(0, axis_length, 0), - radius - ) - collision_node.addSolid(capsule) - elif axis_name == "z": - capsule = CollisionCapsule( - Point3(0, 0, 0), - Point3(0, 0, axis_length), - radius - ) - collision_node.addSolid(capsule) - - # 将碰撞节点附加到handle节点,使其与可视化几何体保持一致 - collision_np = handle_node.attachNewNode(collision_node) - - # 设置标签以便识别 - collision_np.setTag("gizmo_axis", axis_name) - collision_np.setTag("pickable", "1") - - collision_np.hide() # 隐藏碰撞体,只用于检测 - - #print(f"✓ 成功创建 {axis_name} 轴碰撞体") - - except Exception as e: - print(f"创建{axis_name}轴碰撞体失败: {e}") - import traceback - traceback.print_exc() - - def detectGizmoAxisWithCollision(self, mouseX, mouseY): - # 使用碰撞体检测鼠标是否悬停在坐标轴上 - if not self.gizmo: - return None - - try: - ray = CollisionRay() - - win_width, win_height = self.world.getWindowSize() - - mouse_x_ndc = (mouseX / win_width) * 2.0 - 1.0 - mouse_y_ndc = 1.0 - (mouseY / win_height) * 2.0 - - ray.setFromLens(self.world.cam.node(), mouse_x_ndc, mouse_y_ndc) - - traverser = CollisionTraverser("gizmo_traverser") - handler = CollisionHandlerQueue() - - # 创建射线节点 - ray_node = CollisionNode('mouseRay') - ray_node.addSolid(ray) - ray_node.setFromCollideMask(BitMask32.bit(1)) # 射线作为from对象 - ray_node.setIntoCollideMask(BitMask32.allOff()) # 射线不作为into对象 - ray_np = self.world.render.attachNewNode(ray_node) - - # 为所有轴的碰撞体设置正确的掩码并添加到遍历器 - collision_found = False - for axis_name in ["x", "y", "z"]: - axis_node = getattr(self, f"gizmo{axis_name.upper()}Axis") - if axis_node: - collision_node_path = axis_node.find("**/gizmo_collision_*") - if not collision_node_path.isEmpty(): - collision_node = collision_node_path.node() - collision_node.setFromCollideMask(BitMask32.allOff()) # 碰撞体不作为from对象 - collision_node.setIntoCollideMask(BitMask32.bit(1)) # 碰撞体作为into对象 - collision_found = True - - if not collision_found: - ray_np.removeNode() - return None - - # 执行碰撞检测 - 这里是关键修复点 - traverser.addCollider(ray_np, handler) - traverser.traverse(self.world.render) - - ray_np.removeNode() - - # 检查是否有碰撞 - if handler.getNumEntries() > 0: - handler.sortEntries() - closest_entry = handler.getEntry(0) - - # 获取碰撞的对象 - collided_object = closest_entry.getIntoNodePath() - axis_tag = collided_object.getTag("gizmo_axis") - - if axis_tag in ["x", "y", "z"]: - return axis_tag - - return None - - except Exception as e: - print(f"使用碰撞体检测坐标轴失败: {e}") - import traceback - traceback.print_exc() - return None - - def debugGizmoCollision(self): - print("===碰撞体调试信息===") - for axis_name in ["x","y","z"]: - axis_node = getattr(self,f"gizmo{axis_name.upper()}Axis") - if axis_node: - handle_node = axis_node.find(f"{axis_name}_handle") - collision_node = axis_node.find("**/gizmo_collision_*") - print(f"{axis_name.upper()}轴:") - print(f" - 轴节点: {axis_node}") - print(f" - Handle节点: {handle_node}") - print(f" - 碰撞节点: {collision_node}") - if not collision_node.isEmpty(): - print(f" - 碰撞体标签: {collision_node.getTag('gizmo_axis')}") - else: - print(f"{axis_name.upper()}轴节点不存在") - - def focusCameraOnSelectedNodeAdvanced(self): - """高级版的摄像机聚焦功能,包含平滑动画效果""" - try: - if not self.selectedNode or self.selectedNode.isEmpty(): - print("没有选中的节点,无法聚焦") - return False - - # 获取选中节点的边界框 - minPoint = Point3() - maxPoint = Point3() - - if not self.selectedNode.calcTightBounds(minPoint, maxPoint, self.world.render): - print("无法计算选中节点的边界框,使用节点为位置作为替代方案") - node_pos = self.selectedNode.getPos(self.world.render) - optimal_distance = 10.0 - current_cam_pos = self.world.cam.getPos() - view_direction = node_pos - current_cam_pos - if view_direction.length()<0.001: - view_direction = Vec3(5,-5,2) - view_direction.normalize() - target_cam_pos = node_pos - (view_direction * optimal_distance) - - temp_node =self.world.render.attachNewNode("temp_lookat_target") - temp_node.setPos(node_pos) - dummy_cam = self.world.render.attachNewNode("dummy_camera") - dummy_cam.setPos(target_cam_pos) - dummy_cam.lookAt(temp_node) - target_cam_hpr = Vec3(dummy_cam.getHpr()) - - temp_node.removeNode() - dummy_cam.removeNode() - - currrent_cam_pos = Point3(self.world.cam.getPos()) - current_cam_hpr = Vec3(self.world.cam.getHpr()) - self._startCameraFocusAnimation(current_cam_pos,target_cam_pos,current_cam_hpr,target_cam_hpr) - print(f"开始聚焦到节点(使用位置): {self.selectedNode.getName()}") - return True - - # 计算节点中心点和大小 - center = Point3( - (minPoint.x + maxPoint.x) * 0.5, - (minPoint.y + maxPoint.y) * 0.5, - (minPoint.z + maxPoint.z) * 0.5 - ) - - # 计算节点的对角线长度 - size = (maxPoint - minPoint).length() - - # 如果节点太小,使用默认大小 - if size < 0.01: - size = 1.0 - - # 获取当前摄像机位置和朝向 - current_cam_pos = Point3(self.world.cam.getPos()) - current_cam_hpr = Vec3(self.world.cam.getHpr()) - - # 计算观察方向 - view_direction = current_cam_pos - center - if view_direction.length() < 0.001: - view_direction = Vec3(5, -5, 2) - - view_direction.normalize() - - # 计算合适的观察距离 - optimal_distance = max(size * 1, 1.0) - - # 计算目标摄像机位置 - target_cam_pos = center + (view_direction * optimal_distance) - - # 计算目标朝向(不直接使用lookAt,而是计算目标HPR) - # 创建临时节点用于计算目标朝向 - temp_node = self.world.render.attachNewNode("temp_lookat_target") - temp_node.setPos(center) - - # 创建另一个临时节点用于计算朝向 - dummy_cam = self.world.render.attachNewNode("dummy_camera") - dummy_cam.setPos(target_cam_pos) - dummy_cam.lookAt(temp_node) - target_cam_hpr = Vec3(dummy_cam.getHpr()) - - # 清理临时节点 - temp_node.removeNode() - dummy_cam.removeNode() - - # 使用任务来实现平滑移动动画 - self._startCameraFocusAnimation(current_cam_pos, target_cam_pos, - current_cam_hpr, target_cam_hpr) - - print(f"开始聚焦到节点: {self.selectedNode.getName()}") - return True - - except Exception as e: - print(f"高级聚焦功能失败: {str(e)}") - import traceback - traceback.print_exc() - return False - - def _startCameraFocusAnimation(self, start_pos, end_pos, start_hpr, end_hpr): - """启动摄像机聚焦动画""" - try: - # 创建动画任务 - class CameraFocusData: - def __init__(self, start_pos, end_pos, start_hpr, end_hpr): - self.start_pos = Point3(start_pos) # 确保是Point3类型 - self.end_pos = Point3(end_pos) # 确保是Point3类型 - self.start_hpr = Vec3(start_hpr) # 确保是Vec3类型 - self.end_hpr = Vec3(end_hpr) # 确保是Vec3类型 - self.elapsed_time = 0.0 - self.duration = 0.8 # 增加动画持续时间到0.8秒,让动画更平滑 - - self._camera_focus_data = CameraFocusData(start_pos, end_pos, start_hpr, end_hpr) - - # 移除之前的任务(如果存在) - taskMgr.remove("cameraFocusTask") - - # 添加新任务 - taskMgr.add(self._cameraFocusTask, "cameraFocusTask") - - except Exception as e: - print(f"启动摄像机聚焦动画失败: {e}") - - def _normalizeAngle(self, angle): - """规范化角度到-180到180度之间""" - while angle > 180: - angle -= 360 - while angle < -180: - angle += 360 - return angle - - def _cameraFocusTask(self, task): - """摄像机聚焦动画任务""" - try: - if not hasattr(self, '_camera_focus_data'): - return task.done - - data = self._camera_focus_data - data.elapsed_time += globalClock.getDt() - - # 计算插值因子 - t = min(1.0, data.elapsed_time / data.duration) - - # 使用更平滑的插值函数 - smooth_t = t * t * (3 - 2 * t) # 平滑步进插值 - - # 手动实现lerp功能 - def lerp_point3(start, end, factor): - return Point3( - start.x + (end.x - start.x) * factor, - start.y + (end.y - start.y) * factor, - start.z + (end.z - start.z) * factor - ) - - # 角度插值需要特殊处理,确保选择最短路径 - def lerp_hpr(start, end, factor): - # 规范化角度 - start_x = self._normalizeAngle(start.x) - start_y = self._normalizeAngle(start.y) - start_z = self._normalizeAngle(start.z) - - end_x = self._normalizeAngle(end.x) - end_y = self._normalizeAngle(end.y) - end_z = self._normalizeAngle(end.z) - - # 计算最短旋转路径 - diff_x = self._normalizeAngle(end_x - start_x) - diff_y = self._normalizeAngle(end_y - start_y) - diff_z = self._normalizeAngle(end_z - start_z) - - # 插值 - result_x = start_x + diff_x * factor - result_y = start_y + diff_y * factor - result_z = start_z + diff_z * factor - - # 再次规范化 - result_x = self._normalizeAngle(result_x) - result_y = self._normalizeAngle(result_y) - result_z = self._normalizeAngle(result_z) - - return Vec3(result_x, result_y, result_z) - - # 计算当前位置和朝向 - current_pos = lerp_point3(data.start_pos, data.end_pos, smooth_t) - current_hpr = lerp_hpr(data.start_hpr, data.end_hpr, smooth_t) - - # 应用到摄像机 - self.world.cam.setPos(current_pos) - self.world.cam.setHpr(current_hpr) - - # 检查是否完成 - if t >= 1.0: - if hasattr(self, '_camera_focus_data'): - delattr(self, '_camera_focus_data') - print("摄像机聚焦动画完成") - return task.done - - return task.cont - - except Exception as e: - print(f"摄像机聚焦任务出错: {e}") - import traceback - traceback.print_exc() - return task.done - - def _smoothCameraMoveTask(self, task): - """平滑摄像机移动任务""" - try: - if not hasattr(self, '_smooth_camera_move_data'): - return task.done - - data = self._smooth_camera_move_data - data.elapsed_time += globalClock.getDt() - - # 计算插值因子 - t = min(1.0, data.elapsed_time / data.duration) - - # 使用平滑插值 - smooth_t = t * t * (3 - 2 * t) - - # 角度插值需要特殊处理 - def lerp_hpr(start, end, factor): - # 规范化角度 - start_x = self._normalizeAngle(start.x) - start_y = self._normalizeAngle(start.y) - start_z = self._normalizeAngle(start.z) - - end_x = self._normalizeAngle(end.x) - end_y = self._normalizeAngle(end.y) - end_z = self._normalizeAngle(end.z) - - # 计算最短旋转路径 - diff_x = self._normalizeAngle(end_x - start_x) - diff_y = self._normalizeAngle(end_y - start_y) - diff_z = self._normalizeAngle(end_z - start_z) - - # 插值 - result_x = start_x + diff_x * factor - result_y = start_y + diff_y * factor - result_z = start_z + diff_z * factor - - return Vec3(result_x, result_y, result_z) - - # 计算当前位置和朝向 - current_pos = Point3( - data.start_pos.x + (data.end_pos.x - data.start_pos.x) * smooth_t, - data.start_pos.y + (data.end_pos.y - data.start_pos.y) * smooth_t, - data.start_pos.z + (data.end_pos.z - data.start_pos.z) * smooth_t - ) - - current_hpr = lerp_hpr(data.start_hpr, data.end_hpr, smooth_t) - - # 应用到摄像机 - self.world.cam.setPos(current_pos) - self.world.cam.setHpr(current_hpr) - - # 检查是否完成 - if t >= 1.0: - if hasattr(self, '_smooth_camera_move_data'): - delattr(self, '_smooth_camera_move_data') - print("摄像机平滑移动完成") - return task.done - - return task.cont - - except Exception as e: - print(f"平滑摄像机移动任务出错: {e}") - import traceback - traceback.print_exc() - return task.done - - def handleMouseClick(self, nodePath, mouseX=None, mouseY=None): - """处理鼠标点击事件 - 支持坐标轴双击聚焦""" - try: - # 如果正在删除节点,忽略鼠标点击 - if hasattr(self, '_deleting_node') and self._deleting_node: - print("正在删除节点,忽略鼠标点击") - return - - import time - current_time = time.time() - - # 检查是否点击了坐标轴 - is_gizmo_click = False - target_node = nodePath - - # 判断是否点击了坐标轴 - if (nodePath and hasattr(nodePath, 'getName') and - (nodePath.getName().startswith("gizmo") or - "gizmo" in nodePath.getName().lower() or - (hasattr(nodePath, 'hasTag') and nodePath.hasTag("is_gizmo")))): - is_gizmo_click = True - # 如果有选中的模型,使用选中的模型作为聚焦目标 - if self.selectedNode and not self.selectedNode.isEmpty(): - target_node = self.selectedNode - print(f"检测到坐标轴点击,使用目标节点: {target_node.getName() if target_node else 'None'}") - - # 检查是否为双击(同一节点且在时间阈值内) - is_double_click = (self._last_clicked_node == target_node and - target_node is not None and - current_time - self._last_click_time < self._double_click_threshold) - - if is_double_click: - # 双击 detected - node_name = target_node.getName() if target_node else "None" - print(f"检测到双击节点: {node_name}") - - # 无论是点击模型还是坐标轴,都执行聚焦 - if target_node and not target_node.isEmpty(): - print(f"双击聚焦到节点: {target_node.getName()}") - if self.selectedNode != target_node: - self.updateSelection(target_node) - else: - self.focusCameraOnSelectedNodeAdvanced() - else: - print("双击事件:没有有效的目标节点") - - # 重置状态以避免三击等误触发 - self._last_click_time = 0 - self._last_clicked_node = None - else: - # 单击,更新状态 - self._last_click_time = current_time - self._last_clicked_node = target_node - - # 如果点击的是坐标轴,保持当前选择不变 - if is_gizmo_click: - print("坐标轴单击,保持当前选择") - else: - # 正常的单击选择 - self.updateSelection(nodePath) - - except Exception as e: - print(f"处理鼠标点击事件失败: {e}") - - def _onDoubleClick(self, nodePath): - """双击事件处理""" - try: - # 获取实际要聚焦的目标节点 - target_node = nodePath - - # 如果是坐标轴,确保使用关联的模型作为目标 - if (nodePath and hasattr(nodePath, 'hasTag') and - nodePath.hasTag("is_gizmo")): - if self.selectedNode and not self.selectedNode.isEmpty(): - target_node = self.selectedNode - print(f"坐标轴双击,聚焦到关联模型: {target_node.getName()}") - else: - print("坐标轴双击,但没有关联的选中模型") - return - - if target_node and not target_node.isEmpty(): - print(f"双击聚焦到节点: {target_node.getName()}") - # 更新选择(如果需要) - if self.selectedNode != target_node: - self.updateSelection(target_node) - - # 执行聚焦 - self.focusCameraOnSelectedNodeAdvanced() - else: - print("双击事件:没有有效的目标节点") - - except Exception as e: - print(f"双击事件处理失败: {e}") - - # 添加一个更精确的双击检测方法 - - def checkDoubleClick(self, nodePath): - """检查是否为双击,返回布尔值""" - try: - import time - current_time = time.time() - - # 必须是同一节点且在时间阈值内 - is_double_click = (self._last_clicked_node == nodePath and - nodePath is not None and - current_time - self._last_click_time < self._double_click_threshold) - - if is_double_click: - # 重置状态 - self._last_click_time = 0 - self._last_clicked_node = None - return True - else: - # 更新状态 - self._last_click_time = current_time - self._last_clicked_node = nodePath - return False - - except Exception as e: - print(f"双击检测失败: {e}") - return False - - # 添加一个定时重置方法,用于清除长时间未完成的双击状态 - - def _resetDoubleClickState(self): - """重置双击状态""" - self._last_click_time = 0 - self._last_clicked_node = None - - # 添加一个任务来自动重置双击状态 - - def _startDoubleClickResetTask(self): - """启动双击状态重置任务""" - if self._double_click_task: - taskMgr.remove(self._double_click_task) - - self._double_click_task = taskMgr.doMethodLater( - self._double_click_threshold * 2, # 等待2倍阈值时间 - self._resetDoubleClickStateTask, - "resetDoubleClickState" - ) - - def _resetDoubleClickStateTask(self, task): - """任务:重置双击状态""" - self._resetDoubleClickState() - self._double_click_task = None - return task.done - - # 修改 updateSelection 方法以集成双击检测 - - # def updateSelection(self, nodePath): - # """更新选择状态""" - # print(f"\n=== 更新选择状态 ===") - # - # # 如果正在删除节点,避免更新选择 - # if hasattr(self, '_deleting_node') and self._deleting_node: - # print("正在删除节点,跳过选择更新") - # print("=== 选择状态更新完成 ===\n") - # return - # - # node_name = "None" - # if nodePath and not nodePath.isEmpty(): - # node_name = nodePath.getName() - # print(f"新选择的节点: {node_name}") - # - # # 检查是否为双击 - # is_double_click = self.checkDoubleClick(nodePath) - # if is_double_click: - # print(f"检测到双击 {node_name},执行聚焦") - # # 启动聚焦(在下一帧执行,确保选择状态已更新) - # taskMgr.doMethodLater(0.01, self._delayedFocusTask, "delayedFocus") - # - # self.selectedNode = nodePath - # # 添加兼容性属性 - # self.selectedObject = nodePath - # - # if nodePath and not nodePath.isEmpty(): - # node_name = nodePath.getName() - # print(f"开始为节点 {node_name} 创建选择框和坐标轴...") - # - # # 创建选择框 - # print("创建选择框...") - # self.createSelectionBox(nodePath) - # if self.selectionBox: - # box_name = "Unknown" - # if self.selectionBox and not self.selectionBox.isEmpty(): - # box_name = self.selectionBox.getName() - # print(f"✓ 选择框创建成功: {box_name}") - # else: - # print("× 选择框创建失败") - # - # # 创建坐标轴 - # print("创建坐标轴...") - # self.createGizmo(nodePath) - # if self.gizmo: - # gizmo_name = "Unknown" - # if self.gizmo and not self.gizmo.isEmpty(): - # gizmo_name = self.gizmo.getName() - # print(f"✓ 坐标轴创建成功: {gizmo_name}") - # else: - # print("× 坐标轴创建失败") - # - # print(f"✓ 选中了节点: {node_name}") - # else: - # print("清除选择...") - # self.clearSelectionBox() - # self.clearGizmo() - # print("✓ 取消选择") - # - # print("=== 选择状态更新完成 ===\n") - - def _delayedFocusTask(self, task): - """延迟执行聚焦任务""" - try: - self.focusCameraOnSelectedNodeAdvanced() - except Exception as e: - print(f"延迟聚焦任务失败: {e}") - return task.done - - # 添加一个更智能的双击检测方法,考虑鼠标位置 - - def checkDoubleClickWithPosition(self, nodePath, mouse_x=None, mouse_y=None): - """检查是否为双击,同时考虑鼠标位置""" - try: - import time - current_time = time.time() - - # 如果没有提供鼠标位置,直接使用基本双击检测 - if mouse_x is None or mouse_y is None: - return self.checkDoubleClick(nodePath) - - # 检查节点和时间 - time_diff = current_time - self._last_click_time - is_same_node = (self._last_clicked_node == nodePath) - - # 如果是同一节点且在时间阈值内,认为是双击 - if is_same_node and time_diff < self._double_click_threshold: - # 重置状态 - self._last_click_time = 0 - self._last_clicked_node = None - return True - else: - # 更新状态 - self._last_click_time = current_time - self._last_clicked_node = nodePath - return False - - except Exception as e: - print(f"带位置的双击检测失败: {e}") - return False - - # 添加一个公共方法来设置双击阈值 - - def setDoubleClickThreshold(self, threshold_seconds): - """设置双击时间阈值""" - if threshold_seconds > 0: - self._double_click_threshold = threshold_seconds - print(f"双击阈值已设置为: {threshold_seconds} 秒") - else: - print("无效的双击阈值") - - # 添加一个方法来手动触发双击聚焦(可用于测试或其他触发方式) - - def triggerDoubleClickFocus(self, nodePath=None): - """手动触发双击聚焦""" - try: - target_node = nodePath if nodePath else self.selectedNode - if target_node and not target_node.isEmpty(): - print(f"手动触发聚焦到节点: {target_node.getName()}") - if self.selectedNode != target_node: - self.updateSelection(target_node) - self.focusCameraOnSelectedNodeAdvanced() - return True - else: - print("没有有效的目标节点进行聚焦") - return False - except Exception as e: - print(f"手动触发聚焦失败: {e}") - return False - - def cleanup(self): - """清理选择系统资源""" - # 清理双击任务 - if self._double_click_task: - taskMgr.remove(self._double_click_task) - self._double_click_task = None - - # 清理其他资源 - self.clearSelectionBox() - self.clearGizmo() - def clearSelection(self): - """清除当前选择""" - try: - self.selectedNode = None - self.selectedObject = None - self.clearSelectionBox() - self.clearGizmo() - - # 清除树形控件中的选择 - if (hasattr(self.world, 'interface_manager') and - self.world.interface_manager and - hasattr(self.world.interface_manager, 'treeWidget') and - self.world.interface_manager.treeWidget): - self.world.interface_manager.treeWidget.setCurrentItem(None) - - print("已清除选择") - except Exception as e: - print(f"清除选择失败: {e}") - +""" +选择和变换系统模块 + +负责物体选择和变换相关功能: +- 选择框的创建和更新 +- 坐标轴(Gizmo)系统 +- 拖拽变换逻辑 +- 射线检测和碰撞检测 +""" +from direct.showbase.ShowBaseGlobal import globalClock +from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState, + DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue, + CollisionNode, CollisionRay, GeomNode, BitMask32, Material, LColor, DepthWriteAttrib, + TransparencyAttrib, Vec4, CollisionCapsule) +from direct.task.TaskManagerGlobal import taskMgr +import math + +class SelectionSystem: + """选择和变换系统类""" + + def __init__(self, world): + """初始化选择系统 + + Args: + world: 核心世界对象引用 + """ + self.world = world + + # 选择相关状态 + self.selectedNode = None + self.selectionBox = None # 选择框 + self.selectionBoxTarget = None # 选择框跟踪的目标节点 + + # 坐标轴工具(Gizmo)相关 + self.gizmo = None # 坐标轴 + self.gizmoTarget = None # 坐标轴跟踪的目标节点 + self.gizmoXAxis = None # X轴 + self.gizmoYAxis = None # Y轴 + self.gizmoZAxis = None # Z轴 + self.gizmoRotXAxis = None + self.gizmoRotYAxis = None + self.gizmoRotZAxis = None + self.axis_length = 5.0 # 坐标轴长度(增加到5.0) + + # 拖拽相关状态 + self.isDraggingGizmo = False # 是否正在拖拽坐标轴 + self.dragGizmoAxis = None # 当前拖拽的轴("x", "y", "z") + self.gizmoStartPos = None # 拖拽开始时坐标轴的位置 + self.gizmoTargetStartPos = None # 拖拽开始时目标节点的位置 + self.dragStartMousePos = None # 拖拽开始时的鼠标位置 + + # 高亮相关 + self.gizmoHighlightAxis = None + self.gizmo_colors = { + "x": (1, 0, 0, 1.0), # 红色 + "y": (0, 1, 0, 1.0), # 绿色 + "z": (0, 0, 1, 1.00) # 蓝色 + } + self.gizmo_highlight_colors = { + "x": (1.0, 1.0, 0.0, 1.0), # 黄色高亮 + "y": (1.0, 1.0, 0.0, 1.0), # 黄色高亮 + "z": (1.0, 1.0, 0.0, 1.0) # 黄色高亮 + } + + #性能优化相关 + self._optimized_node = False + self._last_update_time = 0 + self._cached_bounds = {} + self._gizmo_update_interval = 0.1 + self._selection_box_update_interval = 0.2 + + self._current_cursor = None + self._default_cursor = None + + self._last_click_time = 0 + self._double_click_threshold = 0.3 + self._last_clicked_node = None + self._double_click_task = None + + print("✓ 选择和变换系统初始化完成") + # ==================== 光标设置 ==================== + def _setCursor(self,cursor_type): + try: + from PyQt5.QtCore import Qt + if self._current_cursor == cursor_type: + return + if hasattr(self.world,'main_window') and self.world.main_window: + main_window = self.world.main_window + else: + from PyQt5.QtWidgets import QApplication + main_window = QApplication.activeWindow() + if not main_window: + windows = QApplication.topLevelWindows() + for window in windows: + if hasattr(window,'isVisible') and window.isVisible(): + main_window = window + break + if main_window: + if cursor_type == "crosshair": + main_window.setCursor(Qt.CrossCursor) + elif cursor_type == "size_hor": + main_window.setCursor(Qt.SizeHorCursor) + elif cursor_type == "size_ver": + main_window.setCursor(Qt.SizeVerCursor) + elif cursor_type == "size_all": + main_window.setCursor(Qt.SizeAllCursor) + elif cursor_type == "pointing_hand": + main_window.setCursor(Qt.PointingHandCursor) + else: + main_window.unsetCursor() + self._current_cursor = cursor_type + #print(f"光标已设置:{cursor_type}") + self._current_cursor = cursor_type + else: + print("警告:无法获取主窗口,光标设置失败") + except Exception as e: + print(f"设置光标失败{e}") + def _resetCursor(self): + self._setCursor("default") + + # ==================== 选择框系统 ==================== + + def createSelectionBox(self, nodePath): + """为选中的节点创建选择框""" + try: + if self.selectionBox: + #print(" 移除现有选择框") + self.selectionBox.removeNode() + self.selectionBox = None + + if not nodePath: + print(" 目标节点为空,取消创建") + return + + self.selectionBox = self.world.render.attachNewNode("selectionBox") + self.selectionBoxTarget = nodePath + + taskMgr.add(self.updateSelectionBoxTask, "updateSelectionBox") + self.updateSelectionBoxGeometry() + + except Exception as e: + print(f" ✗ 创建选择框失败: {str(e)}") + import traceback + traceback.print_exc() + + def updateSelectionBoxGeometry(self): + """更新选择框的几何形状和位置""" + try: + if not self.selectionBox or not self.selectionBoxTarget: + return + + if self.selectionBoxTarget.isEmpty(): + return + + minPoint = Point3() + maxPoint = Point3() + + try: + has_bounds = self.selectionBoxTarget.calcTightBounds(minPoint, maxPoint, self.world.render) + if not has_bounds: + return + except: + return + + # 检查边界框的有效性 + if (minPoint.x > maxPoint.x or minPoint.y > maxPoint.y or minPoint.z > maxPoint.z or + abs(minPoint.x) > 1e10 or abs(minPoint.y) > 1e10 or abs(minPoint.z) > 1e10 or + abs(maxPoint.x) > 1e10 or abs(maxPoint.y) > 1e10 or abs(maxPoint.z) > 1e10): + print("警告: 检测到无效的边界框,跳过选择框更新") + return + + # 检查是否需要重新计算边界框 + if not hasattr(self, '_bounds_cache'): + self._bounds_cache = {} + + node_name = self.selectionBoxTarget.getName() + import time + current_time = time.time() + + # 如果缓存存在且未过期,则使用缓存 + if (node_name in self._bounds_cache and + current_time - self._bounds_cache[node_name]['time'] < 0.1): + minPoint, maxPoint = self._bounds_cache[node_name]['bounds'] + else: + # 计算新的边界框并缓存 + minPoint = Point3() + maxPoint = Point3() + if not self.selectionBoxTarget.calcTightBounds(minPoint, maxPoint, self.world.render): + return + + # 缓存结果 + self._bounds_cache[node_name] = { + 'bounds': (minPoint, maxPoint), + 'time': current_time + } + + # 清理旧缓存 + expired_keys = [k for k, v in self._bounds_cache.items() + if current_time - v['time'] > 1.0] + for key in expired_keys: + del self._bounds_cache[key] + + # 清除现有的几何体 + self.selectionBox.removeNode() + self.selectionBox = self.world.render.attachNewNode("selectionBox") + + # 获取目标节点在世界坐标系中的边界框(使用正确的API) + minPoint = Point3() + maxPoint = Point3() + if not self.selectionBoxTarget.calcTightBounds(minPoint, maxPoint, self.world.render): + return + + # 获取边界框的最小和最大点(世界坐标) + #print(f"世界边界框: min={minPoint}, max={maxPoint}") + + # 创建线段对象 + lines = LineSegs() + lines.setThickness(2.0) + + # 定义立方体的8个顶点 + vertices = [ + (minPoint.x, minPoint.y, minPoint.z), # 0: 前下左 + (maxPoint.x, minPoint.y, minPoint.z), # 1: 前下右 + (maxPoint.x, maxPoint.y, minPoint.z), # 2: 后下右 + (minPoint.x, maxPoint.y, minPoint.z), # 3: 后下左 + (minPoint.x, minPoint.y, maxPoint.z), # 4: 前上左 + (maxPoint.x, minPoint.y, maxPoint.z), # 5: 前上右 + (maxPoint.x, maxPoint.y, maxPoint.z), # 6: 后上右 + (minPoint.x, maxPoint.y, maxPoint.z), # 7: 后上左 + ] + + # 定义立方体的边(连接顶点的线段) + edges = [ + # 底面 + (0, 1), (1, 2), (2, 3), (3, 0), + # 顶面 + (4, 5), (5, 6), (6, 7), (7, 4), + # 垂直边 + (0, 4), (1, 5), (2, 6), (3, 7) + ] + + # 绘制所有边 + for start, end in edges: + lines.moveTo(*vertices[start]) + lines.drawTo(*vertices[end]) + + # 创建选择框几何体 + geomNode = lines.create() + self.selectionBox.attachNewNode(geomNode) + + # 设置选择框的颜色为亮橙色 + self.selectionBox.setColor(1.0, 0.5, 0.0, 1.0) + + # 设置渲染状态,确保线框总是在最前面显示 + state = RenderState.make( + DepthTestAttrib.make(DepthTestAttrib.MLess), + ColorAttrib.makeFlat((1.0, 0.5, 0.0, 1.0)) + ) + self.selectionBox.setState(state) + + # 确保选择框不被光照影响 + self.selectionBox.setLightOff() + + # 让选择框稍微大一点,避免与模型重叠 + self.selectionBox.setScale(1.01) + + except Exception as e: + print(f"更新选择框几何体失败: {str(e)}") + import traceback + traceback.print_exc() + + def updateSelectionBoxTask(self, task): + """选择框更新任务 - 平衡性能和实时性""" + try: + update_interval = 0.05 + + if not hasattr(self, '_last_selection_box_update'): + self._last_selection_box_update = 0 + + import time + current_time = time.time() + if current_time - self._last_selection_box_update < update_interval: + return task.cont + self._last_selection_box_update = current_time + + # 检查目标节点是否已被删除 + self.checkAndClearIfTargetDeleted() + + if not self.selectionBox or not self.selectionBoxTarget: + return task.done + + # 检查目标节点是否还存在 + if self.selectionBoxTarget.isEmpty(): + self.clearSelectionBox() + return task.done + + # 检查目标节点是否发生了变化(位置、旋转、缩放) + current_transform = self._getNodeTransformKey(self.selectionBoxTarget) + + if (not hasattr(self, '_last_transform_key') or + self._last_transform_key != current_transform): + # 节点发生了变化,更新选择框 + self.updateSelectionBoxGeometry() + self._last_transform_key = current_transform + + return task.cont + + except Exception as e: + print(f"选择框更新任务出错: {str(e)}") + return task.done + + def _getNodeTransformKey(self, node): + """获取节点变换的关键信息,用于快速比较""" + try: + # 获取节点的关键变换信息 + pos = node.getPos(self.world.render) + hpr = node.getHpr(self.world.render) + scale = node.getScale(self.world.render) + + # 返回一个可以比较的元组 + return (pos.x, pos.y, pos.z, hpr.x, hpr.y, hpr.z, scale.x, scale.y, scale.z) + except: + return None + + def clearSelectionBox(self): + """清除选择框""" + if self.selectionBox: + self.selectionBox.removeNode() + self.selectionBox = None + + # 停止选择框更新任务 + taskMgr.remove("updateSelectionBox") + + # 清除目标节点引用 + self.selectionBoxTarget = None + + print("清除了选择框") + + # ==================== 坐标轴(Gizmo)系统 ==================== + + def createGizmo(self, nodePath): + """为选中的节点创建坐标轴工具 - 保留箭头版本""" + try: + #print(f" 开始创建坐标轴,目标节点: {nodePath.getName()}") + + # 如果已有坐标轴,先移除 + if self.gizmo: + self.gizmo.removeNode() + self.gizmo = None + taskMgr.remove("updateGizmo") + + if not nodePath: + return + + # 创建坐标轴主节点 + self.gizmo = self.world.render.attachNewNode("gizmo") + self.gizmoTarget = nodePath + + # 添加标识标签,便于识别 + self.gizmo.setTag("is_gizmo", "1") + if hasattr(nodePath, 'getName'): + self.gizmo.setTag("gizmo_target", nodePath.getName()) + + # 为各轴添加标签 + if hasattr(self, 'gizmoXAxis') and self.gizmoXAxis: + self.gizmoXAxis.setTag("is_gizmo", "1") + self.gizmoXAxis.setTag("gizmo_axis", "x") + if hasattr(self, 'gizmoYAxis') and self.gizmoYAxis: + self.gizmoYAxis.setTag("is_gizmo", "1") + self.gizmoYAxis.setTag("gizmo_axis", "y") + if hasattr(self, 'gizmoZAxis') and self.gizmoZAxis: + self.gizmoZAxis.setTag("is_gizmo", "1") + self.gizmoZAxis.setTag("gizmo_axis", "z") + + # 设置位置和朝向 + minPoint = Point3() + maxPoint = Point3() + if nodePath.calcTightBounds(minPoint, maxPoint, self.world.render): + center = Point3((minPoint.x + maxPoint.x) * 0.5, + (minPoint.y + maxPoint.y) * 0.5, + (minPoint.z + maxPoint.z) * 0.5) + self.gizmo.setPos(center) + + parent_node = nodePath.getParent() + if parent_node and parent_node != self.world.render: + self.gizmo.setHpr(parent_node.getHpr()) + else: + self.gizmo.setHpr(0, 0, 0) + + # 只调用一次几何体创建 + self.createGizmoGeometry() + + #只调用一次颜色设置 + self.setGizmoAxisColor("x", self.gizmo_colors["x"]) + self.setGizmoAxisColor("y", self.gizmo_colors["y"]) + self.setGizmoAxisColor("z", self.gizmo_colors["z"]) + + self._updateGizmoScreenSize() + + self._setupGizmoRendering() + + self.setupGizmoCollision() + + # 现在才显示坐标轴 + self.gizmo.show() + + # 只启动一次更新任务 + taskMgr.add(self.updateGizmoTask, "updateGizmo") + + #print(f" ✓ 为节点 {nodePath.getName()} 创建了坐标轴") + + except Exception as e: + print(f"创建坐标轴失败: {str(e)}") + def createGizmoGeometry(self): + """创建坐标轴的几何体""" + from panda3d.core import Material + try: + if not self.gizmo: + return + + is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False + is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False + + import os + from panda3d.core import getModelPath, Filename + + # 获取项目根目录 + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + # 确保core目录在模型搜索路径中 + model_path = getModelPath() + core_dir = os.path.join(base_dir, "core") + core_filename = Filename.from_os_specific(core_dir) + if not model_path.findFile(core_filename): + model_path.appendDirectory(core_filename) + print(f"✓ 添加core目录到模型搜索路径: {core_dir}") + + if is_scale_tool: + model_filename = "core/UniformScaleHandle.fbx" + elif is_rotate_tool: + model_filename = "core/RotationHandleQuarter.fbx" + arrow_filename = "core/TranslateArrowHandle.fbx" + else: + model_filename = "core/TranslateArrowHandle.fbx" + + # model_paths = [ + # "core/TranslateArrowHandle.fbx", + # "EG/core/TranslateArrowHandle.fbx", + # ] + gizmo_model = None + gizmoRot_model = None + try: + if is_rotate_tool: + gizmo_model = self.world.loader.loadModel(arrow_filename) + gizmoRot_model = self.world.loader.loadModel(model_filename) + else: + gizmo_model = self.world.loader.loadModel(model_filename) + except Exception as e: + print(f"加载模型失败: {e}") + return + + x_rHandle = None + y_rHandle = None + z_rHandle = None + + if is_rotate_tool: + self.gizmoRotXAxis = self.gizmo.attachNewNode("gizmo_rot_x_axis") + x_rHandle = gizmoRot_model.copyTo(self.gizmoRotXAxis) + x_rHandle.setName("x_handle") + self.gizmoRotYAxis = self.gizmo.attachNewNode("gizmo_rot_y_axis") + y_rHandle = gizmoRot_model.copyTo(self.gizmoRotYAxis) + y_rHandle.setName("y_handle") + self.gizmoRotZAxis = self.gizmo.attachNewNode("gizmo_rot_z_axis") + z_rHandle = gizmoRot_model.copyTo(self.gizmoRotZAxis) + z_rHandle.setName("z_handle") + + self.gizmoXAxis = self.gizmo.attachNewNode("gizmo_x_axis") + x_handle = gizmo_model.copyTo(self.gizmoXAxis) + x_handle.setName("x_handle") + + self.gizmoYAxis = self.gizmo.attachNewNode("gizmo_y_axis") + y_handle = gizmo_model.copyTo(self.gizmoYAxis) + y_handle.setName("y_handle") + + self.gizmoZAxis = self.gizmo.attachNewNode("gizmo_z_axis") + z_handle = gizmo_model.copyTo(self.gizmoZAxis) + z_handle.setName("z_handle") + + if is_scale_tool: + x_handle.setHpr(0,-90,0) + x_handle.setScale(0.6,0.03,0.03) + x_handle.setPos(2.2,0,0) + + y_handle.setHpr(90,0,0) + y_handle.setScale(0.6,0.03,0.03) + y_handle.setPos(0,2.2,0) + + z_handle.setHpr(0,0,-90) + z_handle.setScale(0.6,0.03,0.03) + z_handle.setPos(0,0,2.2) + elif is_rotate_tool: + x_rHandle.setHpr(0,0,90) + x_rHandle.setScale(0.025,0.0125,0.0125) + x_rHandle.setPos(0,0,0) + + y_rHandle.setHpr(0,0,0) + y_rHandle.setScale(0.025,0.0125,0.0125) + y_rHandle.setPos(0,0,0) + + z_rHandle.setHpr(-90,0,0) + z_rHandle.setScale(0.025,0.0125,0.0125) + z_rHandle.setPos(0,0,0) + + x_handle.setHpr(0, -90, 0) + x_handle.setScale(0.1, 0.05, 0.05) + x_handle.setPos(0, 0, 0) + + y_handle.setHpr(90, 0, 0) + y_handle.setScale(0.1, 0.05, 0.05) + y_handle.setPos(0, 0, 0) + + z_handle.setHpr(0, 0, -90) + z_handle.setScale(0.1, 0.05, 0.05) + z_handle.setPos(0, 0, 0) + + self.setGizmoRotAxisColor("x", self.gizmo_colors["x"]) + self.setGizmoRotAxisColor("y", self.gizmo_colors["y"]) + self.setGizmoRotAxisColor("z", self.gizmo_colors["z"]) + else: + x_handle.setHpr(0,-90,0) + x_handle.setScale(0.1,0.05,0.05) + x_handle.setPos(0,0,0) + + y_handle.setHpr(90,0,0) + y_handle.setScale(0.1,0.05,0.05) + y_handle.setPos(0,0,0) + + z_handle.setHpr(0,0,-90) + z_handle.setScale(0.1,0.05,0.05) + z_handle.setPos(0,0,0) + + # 设置初始颜色 + self.setGizmoAxisColor("x", self.gizmo_colors["x"]) + self.setGizmoAxisColor("y", self.gizmo_colors["y"]) + self.setGizmoAxisColor("z", self.gizmo_colors["z"]) + + #设置渲染属性,解决模型遮挡和阴影问题 + self._setupGizmoRendering() + + except Exception as e: + print(f"创建坐标轴几何体失败: {str(e)}") + + def _setupGizmoRendering(self): + try: + axis_nodes = [self.gizmoXAxis,self.gizmoYAxis,self.gizmoZAxis] + axis_Rotnodes = [self.gizmoRotXAxis, self.gizmoRotYAxis, self.gizmoRotZAxis] + + # 设置坐标轴主节点的渲染属性 + if self.gizmo: + self.gizmo.setLightOff() # 禁用光照 + self.gizmo.setShaderOff() # 禁用着色器 + self.gizmo.setFogOff() # 禁用雾效 + self.gizmo.setBin("fixed", 40) # 设置为fixed渲染层级,数值越大越优先 + self.gizmo.setDepthWrite(False) # 禁用深度写入 + self.gizmo.setDepthTest(False) # 禁用深度测试,确保始终可见 + + # 设置各轴节点的渲染属性 + for axis_node in axis_nodes: + if axis_node: + axis_node.setLightOff() + axis_node.setShaderOff() + axis_node.setFogOff() + axis_node.setBin("fixed", 40) # 与主节点相同优先级 + axis_node.setDepthWrite(False) # 禁用深度写入 + axis_node.setDepthTest(False) # 禁用深度测试 + + # 设置旋转轴节点的渲染属性 + for axis_rotnode in axis_Rotnodes: + if axis_rotnode: + axis_rotnode.setLightOff() + axis_rotnode.setShaderOff() + axis_rotnode.setFogOff() + axis_rotnode.setBin("fixed", 40) # 与主节点相同优先级 + axis_rotnode.setDepthWrite(False) # 禁用深度写入 + axis_rotnode.setDepthTest(False) # 禁用深度测试 + + # 收集所有handle节点 + arrow_nodes = [] + if self.gizmoXAxis: + x_handle = self.gizmoXAxis.find("x_handle") + if x_handle and not x_handle.isEmpty(): + arrow_nodes.append(x_handle) + if self.gizmoYAxis: + y_handle = self.gizmoYAxis.find("y_handle") + if y_handle and not y_handle.isEmpty(): + arrow_nodes.append(y_handle) + if self.gizmoZAxis: + z_handle = self.gizmoZAxis.find("z_handle") + if z_handle and not z_handle.isEmpty(): + arrow_nodes.append(z_handle) + + rot_nodes = [] + if self.gizmoRotXAxis: + x_rHandle = self.gizmoRotXAxis.find("x_handle") + if x_rHandle and not x_rHandle.isEmpty(): + rot_nodes.append(x_rHandle) + if self.gizmoRotYAxis: + y_rHandle = self.gizmoRotYAxis.find("y_handle") + if y_rHandle and not y_rHandle.isEmpty(): + rot_nodes.append(y_rHandle) + if self.gizmoRotZAxis: + z_rHandle = self.gizmoRotZAxis.find("z_handle") + if z_rHandle and not z_rHandle.isEmpty(): + rot_nodes.append(z_rHandle) + + # 设置handle节点的渲染属性 + for arrow_node in arrow_nodes: + if arrow_node: + arrow_node.setLightOff() + arrow_node.setShaderOff() + arrow_node.setFogOff() + arrow_node.setBin("fixed", 41) # 略高于主节点,确保最优先显示 + arrow_node.setDepthWrite(False) + arrow_node.setDepthTest(False) + # 启用透明度支持 + arrow_node.setTransparency(TransparencyAttrib.MAlpha) + + for rot_node in rot_nodes: + if rot_node: + rot_node.setLightOff() + rot_node.setShaderOff() + rot_node.setFogOff() + rot_node.setBin("fixed", 41) # 略高于主节点,确保最优先显示 + rot_node.setDepthWrite(False) + rot_node.setDepthTest(False) + # 启用透明度支持 + rot_node.setTransparency(TransparencyAttrib.MAlpha) + + except Exception as e: + print(f"设置坐标轴渲染属性失败: {str(e)}") + + def updateGizmoTask(self, task): + """坐标轴更新任务 - 包含固定大小功能""" + try: + # 限制更新频率 + if not hasattr(self, '_last_gizmo_update'): + self._last_gizmo_update = 0 + + import time + current_time = time.time() + if current_time - self._last_gizmo_update < 0.5: # 每0.05秒更新一次 + return task.cont + self._last_gizmo_update = current_time + + #检查目标节点是否已被删除 + self.checkAndClearIfTargetDeleted() + + if not self.gizmo or not self.gizmoTarget: + return task.done + + # 检查目标节点是否还存在 + if self.gizmoTarget.isEmpty(): + self.clearGizmo() + return task.done + + is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False + is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False + was_scale_tool = getattr(self,'_last_tool_scale_state',False) + was_rotate_tool =getattr(self,'_last_tool_rotate_state',False) + + tool_changed = (is_scale_tool!=was_scale_tool) or (is_rotate_tool != was_rotate_tool) + + if tool_changed: + self._last_tool_scale_state = is_scale_tool + self._last_tool_rotate_state = is_rotate_tool + + if self.gizmoXAxis: + self.gizmoXAxis.removeNode() + self.gizmoXAxis = None + if self.gizmoYAxis: + self.gizmoYAxis.removeNode() + self.gizmoYAxis = None + if self.gizmoZAxis: + self.gizmoZAxis.removeNode() + self.gizmoZAxis = None + if self.gizmoRotXAxis: + self.gizmoRotXAxis.removeNode() + self.gizmoRotXAxis = None + if self.gizmoRotYAxis: + self.gizmoRotYAxis.removeNode() + self.gizmoRotYAxis = None + if self.gizmoRotZAxis: + self.gizmoRotZAxis.removeNode() + self.gizmoRotZAxis = None + + self.createGizmoGeometry() + + self.setGizmoAxisColor("x",self.gizmo_colors["x"]) + self.setGizmoAxisColor("y",self.gizmo_colors["y"]) + self.setGizmoAxisColor("z",self.gizmo_colors["z"]) + + self.setupGizmoCollision() + + light_object = self.gizmoTarget.getPythonTag("rp_light_object") + if light_object: + light_pos = light_object.pos + self.gizmo.setPos(light_object.pos) + self.gizmoTarget.setPos(light_pos) + else: + # 只在必要时更新位置和朝向 + self._updateGizmoPositionAndOrientation() + + # 【新功能】:动态调整坐标轴大小,保持固定的屏幕大小 + self._updateGizmoScreenSize() + + return task.cont + + except Exception as e: + print(f"坐标轴更新任务出错: {str(e)}") + return task.done + + def _updateGizmoPositionAndOrientation(self): + """优化的Gizmo位置和朝向更新""" + # 只在必要时重新计算边界框 + if not hasattr(self, '_last_gizmo_bounds_update'): + self._last_gizmo_bounds_update = 0 + + import time + current_time = time.time() + if current_time - self._last_gizmo_bounds_update > 0.2: # 每0.2秒计算一次边界框 + minPoint = Point3() + maxPoint = Point3() + # 添加异常处理 + try: + if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render): + # 检查边界框的有效性 + if (abs(minPoint.x) < 1e10 and abs(minPoint.y) < 1e10 and abs(minPoint.z) < 1e10 and + abs(maxPoint.x) < 1e10 and abs(maxPoint.y) < 1e10 and abs(maxPoint.z) < 1e10): + # 计算中心点 + center = Point3((minPoint.x + maxPoint.x) * 0.5, + (minPoint.y + maxPoint.y) * 0.5, + (minPoint.z + maxPoint.z) * 0.5) + self.gizmo.setPos(center) + except Exception as e: + print(f"更新Gizmo位置时出错: {e}") + # if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render): + # # 计算中心点 + # center = Point3((minPoint.x + maxPoint.x) * 0.5, + # (minPoint.y + maxPoint.y) * 0.5, + # (minPoint.z + maxPoint.z) * 0.5) + # self.gizmo.setPos(center) + self._last_gizmo_bounds_update = current_time + + is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False + + # 安区地更新朝向 + + if is_scale_tool: + #self.gizmo.setHpr(self.gizmoTarget.getHpr()) + self.gizmo.setQuat(self.gizmoTarget.getQuat(self.world.render)) + else: + parent_node = self.gizmoTarget.getParent() + if parent_node and parent_node != self.world.render: + parent_hpr = parent_node.getHpr() + self.gizmo.setHpr(parent_hpr) + else: + self.gizmo.setHpr(0,0,0) + + # 更新朝向 + # parent_node = self.gizmoTarget.getParent() + # if parent_node and parent_node != self.world.render: + # parent_hpr = parent_node.getHpr() + # self.gizmo.setHpr(parent_hpr) + # else: + # self.gizmo.setHpr(0, 0, 0) + + def _updateGizmoScreenSize(self): + """动态调整坐标轴大小,保持固定的屏幕大小""" + try: + if not self.gizmo or not self.gizmoTarget: + return + + # 计算相机到坐标轴的距离 + gizmo_world_pos = self.gizmo.getPos(self.world.render) + cam_pos = self.world.cam.getPos() + distance_to_gizmo = (cam_pos - gizmo_world_pos).length() + + # 获取相机视野角度和窗口尺寸 + fov = self.world.cam.node().getLens().getFov()[0] # 水平视野角度 + fov_radians = math.radians(fov) + winWidth, winHeight = self.world.getWindowSize() + + # 计算一个像素在坐标轴距离处对应的世界坐标大小 + pixel_to_world_ratio = distance_to_gizmo * math.tan(fov_radians / 2) / (winWidth / 2) + + # 设定坐标轴在屏幕上的期望像素长度(固定值) + desired_screen_length = 120 # 像素 + + # 计算世界坐标系中的轴长度 + world_axis_length = desired_screen_length * pixel_to_world_ratio + + # 计算缩放比例(相对于基础轴长度) + scale_factor = world_axis_length / self.axis_length + + # 应用缩放到坐标轴 + self.gizmo.setScale(scale_factor) + + # 限制缩放范围,避免过大或过小 + min_scale = 0.001 + max_scale = 100.0 + final_scale = max(min_scale, min(max_scale, scale_factor)) + + if final_scale != scale_factor: + self.gizmo.setScale(final_scale) + + except Exception as e: + # 静默处理错误,避免频繁输出 + pass + + def clearGizmo(self): + """清除坐标轴""" + if self.gizmo: + self.gizmo.removeNode() + self.gizmo = None + + # 停止坐标轴更新任务 + taskMgr.remove("updateGizmo") + + # 清除坐标轴相关引用 + self.gizmoTarget = None + self.gizmoXAxis = None + self.gizmoYAxis = None + self.gizmoZAxis = None + self.isDraggingGizmo = False + self.dragGizmoAxis = None + self.dragStartMousePos = None + self.gizmoTargetStartPos = None + self.gizmoStartPos = None + self._resetCursor() + + + # def setGizmoAxisColor(self, axis, color): + # """设置坐标轴颜色 - RenderPipeline 兼容版本""" + # try: + # from panda3d.core import AntialiasAttrib,TransparencyAttrib + # + # axis_nodes = { + # "x": self.gizmoXAxis, + # "y": self.gizmoYAxis, + # "z": self.gizmoZAxis + # } + # + # if axis in axis_nodes and axis_nodes[axis]: + # axis_node = axis_nodes[axis] + # + # axis_node.setColor(color[0]*20.0,color[1]*20.0,color[2]*20.0,color[3]) + # axis_node.setColorScale(color[0]*10.0,color[1]*10.0,color[2]*10.0,color[3]) + # axis_node.setShaderOff(10000) + # axis_node.setLightOff() + # axis_node.setMaterialOff() + # axis_node.setTextureOff() + # axis_node.setFogOff() + # + # except Exception as e: + # print(f"设置坐标轴颜色失败: {str(e)}") + # # 回退到简单的颜色设置 + # try: + # if axis in axis_nodes and axis_nodes[axis]: + # axis_nodes[axis].setColor(*color) + # except: + # pass + + + def setGizmoRotAxisColor(self, axis, color): + """使用材质设置坐标轴颜色 - RenderPipeline兼容版本""" + try: + from panda3d.core import Material, Vec4,ColorWriteAttrib,DepthWriteAttrib,DepthTestAttrib,TransparencyAttrib + + # 获取对应的轴节点 + axis_nodes = { + "x": self.gizmoRotXAxis, + "y": self.gizmoRotYAxis, + "z": self.gizmoRotZAxis + } + + if axis not in axis_nodes or not axis_nodes[axis]: + return + + axis_node = axis_nodes[axis] + + handle_node = None + handle_node = axis_node.find("x_handle") if axis == "x" else handle_node + handle_node = axis_node.find("y_handle") if axis == "y" else handle_node + handle_node = axis_node.find("z_handle") if axis == "z" else handle_node + + #如果找不到特定名称的节点,尝试查找任何子节点 + if not handle_node: + children = axis_node.getChildren() + if children.getNumPath()>0: + handle_node = children[0] + + if not handle_node: + #print(f"未找到{axis}轴的处理模型") + return + + # 创建或获取材质 + mat = Material() + + # 设置材质属性 - 使用更自然的颜色,避免过亮的自发光 + adjusted_color = Vec4( + min(color[0]*20, 1.0), + min(color[1]*20, 1.0), + min(color[2]*20, 1.0), + color[3] + ) + + mat.setBaseColor(adjusted_color) + #mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光 + + # 应用材质 + handle_node.setMaterial(mat, 1) + + + # 设置透明度 + if color[3] < 1.0: + handle_node.setTransparency(TransparencyAttrib.MAlpha) + else: + handle_node.setTransparency(TransparencyAttrib.MNone) + + handle_node.setLightOff() # 禁用光照影响 + handle_node.setShaderOff() # 禁用着色器 + handle_node.setFogOff() # 禁用雾效果 + + handle_node.setBin("fixed",41) + handle_node.setDepthWrite(False) + handle_node.setDepthTest(False) + + # 保存材质引用以便后续修改 + if axis == "x": + self.xMat = mat + elif axis == "y": + self.yMat = mat + elif axis == "z": + self.zMat = mat + + axis_node.setLightOff() + axis_node.setShaderOff() + axis_node.setFogOff() + axis_node.setBin("fixed", 40) + axis_node.setDepthWrite(False) + axis_node.setDepthTest(False) + + except Exception as e: + print(f"设置坐标轴颜色失败: {str(e)}") + # 回退到简单颜色设置 + try: + axis_nodes = { + "x": self.gizmoXAxis, + "y": self.gizmoYAxis, + "z": self.gizmoZAxis + } + + if axis in axis_nodes and axis_nodes[axis]: + axis_nodes[axis].setColor(color[0], color[1], color[2], color[3]) + except: + pass + + def setGizmoAxisColor(self, axis, color): + """使用材质设置坐标轴颜色 - RenderPipeline兼容版本""" + try: + from panda3d.core import Material, Vec4,ColorWriteAttrib,DepthWriteAttrib,DepthTestAttrib,TransparencyAttrib + + # 获取对应的轴节点 + axis_nodes = { + "x": self.gizmoXAxis, + "y": self.gizmoYAxis, + "z": self.gizmoZAxis + } + + if axis not in axis_nodes or not axis_nodes[axis]: + return + + axis_node = axis_nodes[axis] + + if axis_node.isEmpty(): + return + + handle_node = None + handle_node = axis_node.find("x_handle") if axis == "x" else handle_node + handle_node = axis_node.find("y_handle") if axis == "y" else handle_node + handle_node = axis_node.find("z_handle") if axis == "z" else handle_node + + # 如果找不到特定名称的节点,尝试查找任何子节点 + if not handle_node or handle_node.isEmpty(): + children = axis_node.getChildren() + if children.getNumPaths() > 0: + handle_node = children[0] + + if not handle_node: + # print(f"未找到{axis}轴的处理模型") + return + + # 创建或获取材质 + mat = Material() + + adjusted_color = Vec4( + min(color[0], 1.0), + min(color[1], 1.0), + min(color[2], 1.0), + color[3] + ) + + mat.setBaseColor(adjusted_color) + #mat.setEmission(Vec4(1, 1, 1, 1.0)) # 自发光 + + # 应用材质 + handle_node.setMaterial(mat, 1) + + # 设置透明度 + if color[3] < 1.0: + handle_node.setTransparency(TransparencyAttrib.MAlpha) + else: + handle_node.setTransparency(TransparencyAttrib.MNone) + + handle_node.setLightOff() # 禁用光照影响 + handle_node.setShaderOff() # 禁用着色器 + handle_node.setFogOff() # 禁用雾效果 + + handle_node.setBin("fixed",41) + handle_node.setDepthWrite(False) + handle_node.setDepthTest(False) + + # 保存材质引用以便后续修改 + if axis == "x": + self.xMat = mat + elif axis == "y": + self.yMat = mat + elif axis == "z": + self.zMat = mat + + axis_node.setLightOff() + axis_node.setShaderOff() + axis_node.setFogOff() + axis_node.setBin("fixed", 40) + axis_node.setDepthWrite(False) + axis_node.setDepthTest(False) + + except Exception as e: + print(f"设置坐标轴颜色失败: {str(e)}") + # 回退到简单颜色设置 + try: + axis_nodes = { + "x": self.gizmoXAxis, + "y": self.gizmoYAxis, + "z": self.gizmoZAxis + } + + if axis in axis_nodes and axis_nodes[axis]: + axis_nodes[axis].setColor(color[0], color[1], color[2], color[3]) + except: + pass + + # ==================== 鼠标交互处理 ==================== + + def _onGizmoMouseEnter(self, axis): + """鼠标进入坐标轴时的处理 - RenderPipeline 兼容版本""" + try: + # 黄色高亮,增加透明度以确保在 RenderPipeline 下可见 + highlight_color = (1.0, 1.0, 0.0, 1) + self.setGizmoAxisColor(axis, highlight_color) + + # 额外的视觉反馈 + axis_nodes = { + "x": self.gizmoXAxis, + "y": self.gizmoYAxis, + "z": self.gizmoZAxis + } + + if axis in axis_nodes and axis_nodes[axis]: + # 稍微放大以增强视觉效果 + axis_nodes[axis].setScale(1.1) + + self.gizmoHighlightAxis = axis + + except Exception as e: + print(f"鼠标进入坐标轴处理失败: {e}") + + def _onGizmoMouseLeave(self, axis): + """鼠标离开坐标轴时的处理 - RenderPipeline 兼容版本""" + try: + # 恢复原始颜色 + original_colors = { + "x": (1.0, 0.0, 0.0, 1.0), # 红色 + "y": (0.0, 1.0, 0.0, 1.0), # 绿色 + "z": (0.0, 0.0, 1.0, 1.0) # 蓝色 + } + + if axis in original_colors: + self.setGizmoAxisColor(axis, original_colors[axis]) + + # 恢复原始大小 + axis_nodes = { + "x": self.gizmoXAxis, + "y": self.gizmoYAxis, + "z": self.gizmoZAxis + } + + if axis in axis_nodes and axis_nodes[axis]: + axis_nodes[axis].setScale(1.0) + + if self.gizmoHighlightAxis == axis: + self.gizmoHighlightAxis = None + + except Exception as e: + print(f"鼠标离开坐标轴处理失败: {e}") + + def setupGizmoMouseEvents(self): + """设置坐标轴的鼠标事件""" + try: + from direct.showbase.DirectObject import DirectObject + + # 为每个轴设置鼠标事件 + axis_nodes = { + "x": self.gizmoXAxis, + "y": self.gizmoYAxis, + "z": self.gizmoZAxis + } + + for axis_name, axis_node in axis_nodes.items(): + if axis_node: + # 设置碰撞检测标签 + axis_node.setTag("gizmo_axis", axis_name) + axis_node.setTag("pickable", "1") + + + except Exception as e: + print(f"设置坐标轴鼠标事件失败: {e}") + + # ==================== 射线检测和碰撞检测 ==================== + + def checkGizmoClick(self, mouseX, mouseY): + """使用屏幕空间检测是否点击了坐标轴""" + if not self.gizmo or not self.gizmoTarget: + return None + + # 基本参数验证 + if not isinstance(mouseX, (int, float)) or not isinstance(mouseY, (int, float)): + return None + + try: + # 获取坐标轴中心的世界坐标 + gizmo_world_pos = self.gizmo.getPos(self.world.render) + + # 计算各轴端点的世界坐标 + x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0) + y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0) + z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length) + + # 使用Panda3D的内置投影方法 + def worldToScreen(world_pos): + """将世界坐标转换为屏幕坐标""" + # 将世界坐标转换为相机空间 + cam_space_pos = self.world.cam.getRelativePoint(self.world.render, world_pos) + + # 检查是否在相机前方 + if cam_space_pos.getY() <= 0: + return None + + # 使用相机的镜头进行投影 + screen_pos = Point2() + if self.world.cam.node().getLens().project(cam_space_pos, screen_pos): + # 获取准确的窗口尺寸 + win_width, win_height = self.world.getWindowSize() + + # 转换为窗口像素坐标 + win_x = (screen_pos.getX() + 1.0) * 0.5 * win_width + win_y = (1.0 - screen_pos.getY()) * 0.5 * win_height + return (win_x, win_y) + return None + + # 投影各个关键点 + center_screen = worldToScreen(gizmo_world_pos) + x_screen = worldToScreen(x_end) + y_screen = worldToScreen(y_end) + z_screen = worldToScreen(z_end) + + # 如果无法获得屏幕坐标,使用备用方法 + if not center_screen: + return self.checkGizmoClickFallback(mouseX, mouseY) + + # 计算点击阈值 + click_threshold = 15 # 增大检测范围 + + # 检测各个轴,对于端点在屏幕外的轴提供回退方案 + def getClickDetectionPoint(axis_name, original_screen_pos): + if original_screen_pos: + return original_screen_pos + # 如果端点在屏幕外,使用轴长度的一半作为检测点 + if axis_name == "x": + half_end = gizmo_world_pos + Vec3(self.axis_length * 0.5, 0, 0) + elif axis_name == "y": + half_end = gizmo_world_pos + Vec3(0, self.axis_length * 0.5, 0) + elif axis_name == "z": + half_end = gizmo_world_pos + Vec3(0, 0, self.axis_length * 0.5) + else: + return None + return worldToScreen(half_end) + + axes_data = [ + ("x", getClickDetectionPoint("x", x_screen), "X轴"), + ("y", getClickDetectionPoint("y", y_screen), "Y轴"), + ("z", getClickDetectionPoint("z", z_screen), "Z轴") + ] + + for axis_name, axis_screen, axis_label in axes_data: + if axis_screen: + # 计算鼠标到轴线的距离 + distance = self.distanceToLine( + (mouseX, mouseY), center_screen, axis_screen + ) + #print(f"{axis_label}距离: {distance:.2f}") + + if distance < click_threshold: + #print(f"✓ 点击了{axis_label}") + return axis_name + + return None + + except Exception as e: + print(f"坐标轴点击检测失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def checkGizmoClickFallback(self, mouseX, mouseY): + """备用检测方法:使用固定的屏幕区域""" + + # 获取准确的窗口尺寸 + win_width, win_height = self.world.getWindowSize() + + # 获取窗口中心作为参考点 + center_x = win_width // 2 + center_y = win_height // 2 + + # 定义相对于中心的轴区域(简化假设坐标轴在屏幕中心附近) + axis_length_pixels = 100 # 假设轴长度在屏幕上约100像素 + + # X轴:从中心向右 + x_start = (center_x, center_y) + x_end = (center_x + axis_length_pixels, center_y) + + # Y轴:从中心向上(注意Y轴方向) + y_start = (center_x, center_y) + y_end = (center_x, center_y - axis_length_pixels) + + # Z轴:从中心向右上45度 + z_start = (center_x, center_y) + z_end = (center_x + axis_length_pixels * 0.7, center_y - axis_length_pixels * 0.7) + + threshold = 25 + + # 检测各轴 + if self.distanceToLine((mouseX, mouseY), x_start, x_end) < threshold: + print("✓ 备用方法检测到X轴") + return "x" + elif self.distanceToLine((mouseX, mouseY), y_start, y_end) < threshold: + print("✓ 备用方法检测到Y轴") + return "y" + elif self.distanceToLine((mouseX, mouseY), z_start, z_end) < threshold: + print("✓ 备用方法检测到Z轴") + return "z" + + print("× 备用方法也没有检测到") + return None + + def distanceToLine(self, point, line_start, line_end): + """计算点到线段的距离""" + try: + px, py = point + x1, y1 = line_start + x2, y2 = line_end + + # 计算线段长度 + line_length = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5 + if line_length == 0: + return ((px - x1) ** 2 + (py - y1) ** 2) ** 0.5 + + # 计算点到线的距离 + t = max(0, min(1, ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / (line_length ** 2))) + projection_x = x1 + t * (x2 - x1) + projection_y = y1 + t * (y2 - y1) + + distance = ((px - projection_x) ** 2 + (py - projection_y) ** 2) ** 0.5 + return distance + + except Exception as e: + print(f"距离计算错误: {e}") + return float('inf') + + # ==================== 高亮和交互 ==================== + + def detectGizmoAxisAtMouse(self, mouseX, mouseY): + """统一的坐标轴检测方法 - 同时用于高亮和点击检测""" + if not self.gizmo or not self.gizmoTarget: + return None + + try: + # 获取坐标轴中心的世界坐标 + gizmo_world_pos = self.gizmo.getPos(self.world.render) + + #获取坐标轴的世界朝向(考虑旋转) + gizmo_world_quat = self.gizmo.getQuat(self.world.render) + + #计算各轴在世界坐标系中的实际方向向量 + x_axis_world = gizmo_world_quat.xform(Vec3(1,0,0)) + y_axis_world = gizmo_world_quat.xform(Vec3(0,1,0)) + z_axis_world = gizmo_world_quat.xform(Vec3(0,0,1)) + + x_end = gizmo_world_pos + x_axis_world * self.axis_length + y_end = gizmo_world_pos + y_axis_world * self.axis_length + z_end = gizmo_world_pos + z_axis_world * self.axis_length + + + # 计算各轴端点的世界坐标 + # x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0) + # y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0) + # z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length) + + # 将3D坐标投影到屏幕坐标 + def worldToScreen(worldPos): + try: + # 转换为相机坐标系 + camPos = self.world.cam.getRelativePoint(self.world.render, worldPos) + + # 检查点是否在相机前方 + if camPos.getY() <= 0: + return None + + # 使用相机lens进行投影 + screenPos = Point2() + lens = self.world.cam.node().getLens() + + if lens.project(camPos, screenPos): + # 获取准确的窗口尺寸 + winWidth, winHeight = self.world.getWindowSize() + + # 转换为像素坐标 + winX = (screenPos.x + 1) * 0.5 * winWidth + winY = (1 - screenPos.y) * 0.5 * winHeight + return (winX, winY) + return None + except: + return None + + # 获取各坐标轴的屏幕投影 + gizmo_screen = worldToScreen(gizmo_world_pos) + x_screen = worldToScreen(x_end) + y_screen = worldToScreen(y_end) + z_screen = worldToScreen(z_end) + + # 如果坐标轴中心不在屏幕内,返回None + if not gizmo_screen: + return None + + # 设置检测阈值 + click_threshold = 35 # 统一使用25像素的检测阈值 + + # 更准确的点到线段距离计算方法 + def distanceToLineSegment(mousePos, start, end): + import math + mx, my = mousePos + x1, y1 = start + x2, y2 = end + + # 线段向量 + dx = x2 - x1 + dy = y2 - y1 + + # 线段长度平方 + length_sq = dx * dx + dy * dy + + if length_sq == 0: + # 线段退化为点 + return math.sqrt((mx - x1) ** 2 + (my - y1) ** 2) + + # 投影参数 + t = max(0, min(1, ((mx - x1) * dx + (my - y1) * dy) / length_sq)) + + # 投影点坐标 + proj_x = x1 + t * dx + proj_y = y1 + t * dy + + # 返回点到投影点的距离 + return math.sqrt((mx - proj_x) ** 2 + (my - proj_y) ** 2) + + mouse_pos = (mouseX, mouseY) + + # 检测各个轴 - 按优先级检测(Z > X > Y) + axes_to_check = [ + ("z", z_screen), + ("x", x_screen), + ("y", y_screen) + ] + + for axis_name, axis_end in axes_to_check: + if axis_end: + distance = distanceToLineSegment(mouse_pos, gizmo_screen, axis_end) + if distance < click_threshold: + return axis_name + + # 如果没有检测到,返回None + return None + + except Exception as e: + # 静默处理错误,避免频繁输出 + return None + + def updateGizmoHighlight(self, mouseX, mouseY): + """更新坐标轴高亮状态""" + if not self.gizmo or self.isDraggingGizmo: + self._resetCursor() + return + + # 使用碰撞检测方法 + #hoveredAxis = self.detectGizmoAxisWithCollision(mouseX, mouseY) + hoveredAxis = self.detectGizmoAxisAtMouse(mouseX, mouseY) + + # 简化稳定性检测逻辑 + if not hasattr(self, '_last_detected_axis'): + self._last_detected_axis = None + + # 如果检测结果发生变化,立即更新高亮 + if hoveredAxis != self._last_detected_axis: + # 更新轴的高亮状态 + if hoveredAxis != self.gizmoHighlightAxis: + # 恢复之前高亮的轴 + if self.gizmoHighlightAxis: + self.setGizmoAxisColor(self.gizmoHighlightAxis, self.gizmo_colors[self.gizmoHighlightAxis]) + + # 高亮新的轴 + if hoveredAxis: + self.setGizmoAxisColor(hoveredAxis, self.gizmo_highlight_colors[hoveredAxis]) + self._setCursor("pointing_hand") + else: + # 如果没有悬停在任何轴上,确保所有轴都恢复原始颜色 + for axis_name in ["x", "y", "z"]: + if axis_name != self.dragGizmoAxis: # 不要改变正在拖拽的轴的颜色 + self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) + self._resetCursor() + + self.gizmoHighlightAxis = hoveredAxis + + self._last_detected_axis = hoveredAxis + elif hoveredAxis is None: + self._resetCursor() + + def _detectHoveredAxis(self, mouseX, mouseY): + """检测鼠标悬停的轴 - 提取为独立方法""" + # 将原来 updateGizmoHighlight 中的检测逻辑移到这里 + # ... 你原来的检测代码 ... + pass + + def _updateAxisHighlight(self, hoveredAxis): + """更新轴高亮状态 - 确保原子性操作""" + # 恢复之前高亮的轴 + if self.gizmoHighlightAxis: + self.setGizmoAxisColor(self.gizmoHighlightAxis, self.gizmo_colors[self.gizmoHighlightAxis]) + + # 高亮新的轴 + if hoveredAxis: + self.setGizmoAxisColor(hoveredAxis, self.gizmo_highlight_colors[hoveredAxis]) + + self.gizmoHighlightAxis = hoveredAxis + + # ==================== 拖拽变换 ==================== + + def startGizmoDrag(self, axis, mouseX, mouseY): + """开始坐标轴拖拽""" + try: + # 确保状态正确初始化 + if not self.gizmoTarget: + print("开始拖拽失败: 没有拖拽目标") + return + if not self.gizmo: + print("开始拖拽失败: 没有坐标轴") + return + + self.isDraggingGizmo = True + + # 使用当前高亮的轴,如果有的话;否则使用传入的轴 + if self.gizmoHighlightAxis: + self.dragGizmoAxis = self.gizmoHighlightAxis + elif axis and axis in self.gizmo_colors: + self.dragGizmoAxis = axis + else: + # 如果没有明确指定轴,尝试通过鼠标位置检测 + self.dragGizmoAxis = self.detectGizmoAxisAtMouse(mouseX, mouseY) + + # 如果仍然无法确定拖拽轴,则取消拖拽 + if not self.dragGizmoAxis: + print("开始拖拽失败: 无法确定拖拽轴") + self.isDraggingGizmo = False + return + + self.dragStartMousePos = (mouseX, mouseY) + + light_object = self.gizmoTarget.getPythonTag("rp_light_object") + if light_object: + self.gizmoTargetStartPos = Point3(light_object.pos) + else: + self.gizmoTargetStartPos = self.gizmoTarget.getPos() + + # 保存开始拖拽时目标节点的位置和坐标轴的位置 + #self.gizmoTargetStartPos = self.gizmoTarget.getPos() + self.gizmoStartPos = self.gizmo.getPos(self.world.render) # 坐标轴的世界位置 + + # 添加对缩放的支持:保存初始缩放值 + if self.world.tool_manager.isScaleTool(): + self.gizmoTargetStartScale = self.gizmoTarget.getScale() + elif self.world.tool_manager.isRotateTool(): + self.gizmoTargetStartHpr = self.gizmoTarget.getHpr() + + # 确保正在拖动的轴保持高亮状态 + if self.dragGizmoAxis and self.dragGizmoAxis in self.gizmo_colors: + # 先将所有轴恢复为正常颜色 + for axis_name in self.gizmo_colors.keys(): + if axis_name != self.dragGizmoAxis: + self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) + + # 然后将当前拖动的轴设置为高亮颜色 + self.setGizmoAxisColor(self.dragGizmoAxis, self.gizmo_highlight_colors[self.dragGizmoAxis]) + # elif axis and axis in self.gizmo_colors: + # for axis_name in self.gizmo_colors.keys(): + # if axis_name != axis: + # self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) + # + # self.setGizmoAxisColor(axis, self.gizmo_highlight_colors[axis]) + # self.dragGizmoAxis = axis + # + # self.gizmoHighlightAxis = self.dragGizmoAxis + # 设置拖拽光标 + if self.dragGizmoAxis == "x": + self._setCursor("size_all") # 水平调整光标 + elif self.dragGizmoAxis == "y": + self._setCursor("size_all") # 垂直调整光标 + elif self.dragGizmoAxis == "z": + self._setCursor("size_all") # 全向调整光标 + + print( + f"开始拖拽 {self.dragGizmoAxis} 轴 - 目标起始位置: {self.gizmoTargetStartPos}, 坐标轴位置: {self.gizmoStartPos}, 鼠标: ({mouseX}, {mouseY})") + + except Exception as e: + print(f"开始坐标轴拖拽失败: {str(e)}") + import traceback + traceback.print_exc() + + def updateGizmoDrag(self, mouseX, mouseY): + """更新坐标轴拖拽 - 使用正确的坐标系变换,支持旋转后的子节点拖拽""" + try: + # 添加详细的状态检查和调试信息 + if not self.isDraggingGizmo: + print("拖拽更新失败: 不在拖拽状态") + return + if not self.gizmoTarget: + print("拖拽更新失败: 没有拖拽目标") + return + if not hasattr(self, 'dragStartMousePos') or not self.dragStartMousePos: + print("拖拽更新失败: 没有拖拽起始位置") + return + if not hasattr(self, 'gizmoTargetStartPos') or not self.gizmoTargetStartPos: + print("拖拽更新失败: 没有目标起始位置") + return + if not hasattr(self, 'gizmoStartPos') or not self.gizmoStartPos: + print("拖拽更新失败: 没有坐标轴起始位置") + return + + is_scale_tool = self.world.tool_manager.isScaleTool() if self.world.tool_manager else False + is_rotate_tool = self.world.tool_manager.isRotateTool() if self.world.tool_manager else False + + is_gui_element = (hasattr(self.gizmoTarget,'getTag') and + self.gizmoTarget.getTag("is_gui_element") == "1") + + + # 计算鼠标移动距离(屏幕像素) + mouseDeltaX = mouseX - self.dragStartMousePos[0] + mouseDeltaY = mouseY - self.dragStartMousePos[1] + + if is_scale_tool: + scale_factor = 1.0 + (mouseDeltaX + mouseDeltaY) * 0.01 + + scale_factor = max(0.001, scale_factor) + + start_scale = getattr(self,'gizmoTargetStartScale',Vec3(1,1,1)) + + if is_gui_element: + if self.dragGizmoAxis == "x": + new_scale = Vec3(start_scale.x * scale_factor,start_scale.y,start_scale.z) + elif self.dragGizmoAxis == "y": + new_scale = Vec3(start_scale.x, start_scale.y * scale_factor, start_scale.z) + elif self.dragGizmoAxis == "z": + new_scale = Vec3(start_scale.x, start_scale.y, start_scale.z * scale_factor) + else: + new_scale = Vec3(start_scale.x * scale_factor, + start_scale.y * scale_factor, + start_scale.z * scale_factor) + else: + # 普通3D模型的缩放处理 + if self.dragGizmoAxis == "x": + new_scale = Vec3(start_scale.x * scale_factor, start_scale.y, start_scale.z) + elif self.dragGizmoAxis == "y": + new_scale = Vec3(start_scale.x, start_scale.y * scale_factor, start_scale.z) + elif self.dragGizmoAxis == "z": + z_scale_factor = 1.0 - (mouseDeltaX + mouseDeltaY) * 0.01 + new_scale = Vec3(start_scale.x, start_scale.y, start_scale.z * z_scale_factor) + else: + new_scale = Vec3(start_scale.x * scale_factor, + start_scale.y * scale_factor, + start_scale.z * scale_factor) + + new_scale = Vec3( + max(0.001,new_scale.x), + max(0.001,new_scale.y), + max(0.001,new_scale.z) + ) + + # 应用新缩放值 + self.gizmoTarget.setScale(new_scale) + self.world.property_panel.refreshModelValues(self.gizmoTarget) + return + elif is_rotate_tool: + rotation_speed = 0.5 + rotation_amount = (mouseDeltaX + mouseDeltaY) * rotation_speed + start_hpr = getattr(self,'gizmoTargetStartHpr',self.gizmoTarget.getHpr()) + + if self.dragGizmoAxis == "x": + new_hpr = Vec3(start_hpr.x+rotation_amount,start_hpr.y,start_hpr.z) + elif self.dragGizmoAxis == "y": + new_hpr = Vec3(start_hpr.x,start_hpr.y-rotation_amount,start_hpr.z) + elif self.dragGizmoAxis == "z": + new_hpr = Vec3(start_hpr.x,start_hpr.y,start_hpr.z+rotation_amount) + else: + # 默认绕所有轴旋转 + new_hpr = Vec3(start_hpr.x + rotation_amount, + start_hpr.y + rotation_amount, + start_hpr.z + rotation_amount) + self.gizmoTarget.setHpr(new_hpr) + self.world.property_panel.refreshModelValues(self.gizmoTarget) + return + + # 使用坐标轴的实际位置而不是目标节点位置来计算屏幕投影 + gizmo_world_pos = self.gizmoStartPos + + # 【关键修复】:获取正确的轴向量,考虑父节点的旋转 + # 检查目标节点是否有父节点 + parent_node = self.gizmoTarget.getParent() + + # 计算轴向量在正确坐标系中的方向 + if self.dragGizmoAxis == "x": + # 在局部坐标系中的X轴方向 + local_axis_vector = Vec3(1, 0, 0) + elif self.dragGizmoAxis == "y": + # 在局部坐标系中的Y轴方向 + local_axis_vector = Vec3(0, 1, 0) + elif self.dragGizmoAxis == "z": + # 在局部坐标系中的Z轴方向 + local_axis_vector = Vec3(0, 0, 1) + else: + print(f"拖拽更新失败: 未知轴类型 {self.dragGizmoAxis}") + return + + world_axis_vector = local_axis_vector + + if parent_node and parent_node != self.world.render: + try: + #获取变换矩阵 + transfrom_mat = parent_node.getMat(self.world.render) + if transfrom_mat.is_identity() or self._isMatrixValid(transfrom_mat): + world_axis_vector = transfrom_mat.xformVec(local_axis_vector) + else: + print("警告: 检测到无效变换矩阵,使用默认轴向量") + except Exception as e: + print(f"变换计算出错: {e},使用默认轴向量") + else: + world_axis_vector = local_axis_vector + + # 确定轴向量的变换上下文 + # if parent_node and parent_node != self.world.render: + # transform_mat = parent_node.getMat(self.world.render) + # world_axis_vector = transform_mat.xformVec(local_axis_vector) + # else: + # world_axis_vector = local_axis_vector + + #axis_end = gizmo_world_pos + world_axis_vector + + # 投影到屏幕空间 + def worldToScreen(worldPos): + try: + camPos = self.world.cam.getRelativePoint(self.world.render, worldPos) + if camPos.getY() <= 0: + return None + + screenPos = Point2() + if self.world.cam.node().getLens().project(camPos, screenPos): + winWidth, winHeight = self.world.getWindowSize() + winX = (screenPos.x + 1) * 0.5 * winWidth + winY = (1 - screenPos.y) * 0.5 * winHeight + return (winX, winY) + return None + except Exception as e: + print(f"世界坐标转屏幕坐标失败: {e}") + return None + axis_start_screen = worldToScreen(gizmo_world_pos) + axis_end_world = gizmo_world_pos + world_axis_vector + axis_end_screen = worldToScreen(axis_end_world) + + if not axis_start_screen or not axis_end_screen: + print("拖拽更新失败: 无法获取轴线屏幕坐标") + return + + screen_axis_dir = ( + axis_end_screen[0] - axis_start_screen[0], + axis_end_screen[1] - axis_start_screen[1] + ) + + # 归一化屏幕轴方向 + import math + length = math.sqrt(screen_axis_dir[0]**2 + screen_axis_dir[1]**2) + if length > 0: + #screen_axis_dir = (screen_axis_dir[0] / length, screen_axis_dir[1] / length) + screen_axis_dir = ( + screen_axis_dir[0] / length, + screen_axis_dir[1] / length + ) + + else: + print("拖拽更新失败: 屏幕轴方向长度为0") + return + + # 将鼠标移动投影到轴方向上 + projected_distance = (mouseDeltaX * screen_axis_dir[0] + + mouseDeltaY * screen_axis_dir[1]) + + cam_pos = self.world.cam.getPos(self.world.render) + distance_to_object = (cam_pos - gizmo_world_pos).length() + + lens = self.world.cam.node().getLens() + fov = lens.getFov()[0] + winWidth,winHeight = self.world.getWindowSize() + + pixels_to_world_units = (2*distance_to_object*math.tan(math.radians(fov/2)))/winWidth + + movement_distance = projected_distance * pixels_to_world_units + + total_scale_factor = 1.0 + current_node = self.gizmoTarget.getParent() + + while current_node and current_node != self.world.render: + try: + if not current_node.isEmpty(): + node_scale = current_node.getScale() + if node_scale.x > 0 and node_scale.y >0 and node_scale.z >0 : + avg_scale = (node_scale.x + node_scale.y + node_scale.z)/3.0 + total_scale_factor *= avg_scale + #avg_scale = (node_scale.x+node_scale.y + node_scale.z) / 3.0 + #total_scale_factor *= avg_scale + current_node = current_node.getParent() + else: + break + except: + break + + + if total_scale_factor > 0: + movement_distance = movement_distance / total_scale_factor + + currentPos = self.gizmoTargetStartPos + + # 根据拖拽的轴,只修改对应的坐标分量 + if self.dragGizmoAxis == "x": + newPos = Vec3(currentPos.x + movement_distance, currentPos.y, currentPos.z) + #print(f"X轴移动:{currentPos.x} -> {newPos.x}") + elif self.dragGizmoAxis == "y": + newPos = Vec3(currentPos.x, currentPos.y + movement_distance, currentPos.z) + #print(f"Y轴移动:{currentPos.y} -> {newPos.y}") + elif self.dragGizmoAxis == "z": + newPos = Vec3(currentPos.x, currentPos.y, currentPos.z + movement_distance) + #print(f"Z轴移动:{currentPos.z} -> {newPos.z}") + else: + print(f"未知轴: {self.dragGizmoAxis}") + return + + # 应用新位置到目标节点 + light_object = self.gizmoTarget.getPythonTag("rp_light_object") + if light_object: + light_object.pos = newPos + self.gizmoTarget.setPos(newPos) + else: + self.gizmoTarget.setPos(newPos) + + # 更新坐标轴位置 - 计算新的中心位置 + minPoint = Point3() + maxPoint = Point3() + if self.gizmoTarget.calcTightBounds(minPoint, maxPoint, self.world.render): + center = Point3((minPoint.x + maxPoint.x) * 0.5, + (minPoint.y + maxPoint.y) * 0.5, + (minPoint.z + maxPoint.z) * 0.5) + self.gizmo.setPos(center) + + # 实时更新属性面板 + self.world.property_panel.refreshModelValues(self.gizmoTarget) + + # 每次拖拽都输出调试信息(但限制频率) + if not hasattr(self, '_last_drag_debug_time'): + self._last_drag_debug_time = 0 + + import time + current_time = time.time() + if current_time - self._last_drag_debug_time > 0.1: # 每0.1秒最多输出一次 + #print(f"拖拽更新成功 - 轴:{self.dragGizmoAxis}, 比例:{scale_factor:.6f}, 投影:{projected_distance:.2f}") + self._last_drag_debug_time = current_time + + except Exception as e: + print(f"更新坐标轴拖拽失败: {str(e)}") + import traceback + traceback.print_exc() + + def _isMatrixValid(self, matrix): + """检查矩阵是否有效,替代 isSingular 方法""" + try: + # 检查矩阵元素是否为有效数字 + for i in range(4): + for j in range(4): + element = matrix.getCell(i, j) + # 检查是否为 NaN 或无穷大 + if str(element) == 'nan' or str(element) == 'inf' or str(element) == '-inf': + return False + # 检查是否过大 + if abs(element) > 1e10: + return False + return True + except: + return False + + def stopGizmoDrag(self): + """停止坐标轴拖拽""" + print(f"停止坐标轴拖拽 - 轴: {self.dragGizmoAxis}") + + if hasattr(self.world,'command_manager') and self.world.command_manager and self.gizmoTarget: + current_pos = self.gizmoTarget.getPos() + + if (hasattr(self,'gizmoTargetStartPos') and self.gizmoTargetStartPos and + (abs(current_pos.x-self.gizmoTargetStartPos.x)>0.001 or + abs(current_pos.y-self.gizmoTargetStartPos.y)>0.001 or + abs(current_pos.z-self.gizmoTargetStartPos.z)>0.001)): + from core.Command_System import MoveNodeCommand + from core.Command_System import MoveLightCommand + + light_object = self.gizmoTarget.getPythonTag("rp_light_object") + if light_object: + command = MoveLightCommand(self.gizmoTarget,self.gizmoTargetStartPos,current_pos,light_object) + else: + command = MoveNodeCommand(self.gizmoTarget,self.gizmoTargetStartPos,current_pos) + self.world.command_manager.execute_command(command) + # 如果是缩放操作且缩放发生了变化,则创建缩放命令 + elif (hasattr(self, 'gizmoTargetStartScale') and hasattr(self, 'gizmoTargetStartScale') and + self.gizmoTargetStartScale): + current_scale = self.gizmoTarget.getScale() + if (abs(current_scale.x - self.gizmoTargetStartScale.x) > 0.001 or + abs(current_scale.y - self.gizmoTargetStartScale.y) > 0.001 or + abs(current_scale.z - self.gizmoTargetStartScale.z) > 0.001): + from core.Command_System import ScaleNodeCommand + command = ScaleNodeCommand(self.gizmoTarget, self.gizmoTargetStartScale, current_scale) + self.world.command_manager.execute_command(command) + # 如果是旋转操作且旋转发生了变化,则创建旋转命令 + elif (hasattr(self, 'gizmoTargetStartHpr') and hasattr(self, 'gizmoTargetStartHpr') and + self.gizmoTargetStartHpr): + current_hpr = self.gizmoTarget.getHpr() + if (abs(current_hpr.x - self.gizmoTargetStartHpr.x) > 0.001 or + abs(current_hpr.y - self.gizmoTargetStartHpr.y) > 0.001 or + abs(current_hpr.z - self.gizmoTargetStartHpr.z) > 0.001): + from core.Command_System import RotateNodeCommand + command = RotateNodeCommand(self.gizmoTarget, self.gizmoTargetStartHpr, current_hpr) + self.world.command_manager.execute_command(command) + + # 恢复所有轴的颜色 + for axis_name in ["x", "y", "z"]: + self.setGizmoAxisColor(axis_name, self.gizmo_colors[axis_name]) + + self.isDraggingGizmo = False + self.dragGizmoAxis = None + self.dragStartMousePos = None + # 清理拖拽状态,下次拖拽开始时重新设置 + self.gizmoTargetStartPos = None + self.gizmoStartPos = None + + if hasattr(self, 'gizmoTargetStartScale'): + delattr(self, 'gizmoTargetStartScale') + if hasattr(self, 'gizmoTargetStartHpr'): + delattr(self, 'gizmoTargetStartHpr') + + # 重置高亮轴 + self.gizmoHighlightAxis = None + self._resetCursor() + # ==================== 选择管理 ==================== + + def updateSelection(self, nodePath): + try: + if self.selectedNode == nodePath: + return + #print(f"\n=== 更新选择状态 ===") + + # 如果正在删除节点,避免更新选择 + if hasattr(self, '_deleting_node') and self._deleting_node: + print("正在删除节点,跳过选择更新") + #print("=== 选择状态更新完成 ===\n") + return + + node_name = "None" + if nodePath and not nodePath.isEmpty(): + node_name = nodePath.getName() + #print(f"新选择的节点: {node_name}") + + self.selectedNode = nodePath + # 添加兼容性属性 + self.selectedObject = nodePath + + if nodePath and not nodePath.isEmpty(): + node_name = nodePath.getName() + #print(f"开始为节点 {node_name} 创建选择框和坐标轴...") + + # 创建选择框 + #print("创建选择框...") + self.createSelectionBox(nodePath) + if self.selectionBox: + box_name = "Unknown" + if self.selectionBox and not self.selectionBox.isEmpty(): + box_name = self.selectionBox.getName() + #print(f"✓ 选择框创建成功: {box_name}") + else: + print("× 选择框创建失败") + + # 创建坐标轴 + #print("创建坐标轴...") + self.createGizmo(nodePath) + if self.gizmo: + gizmo_name = "Unknown" + if self.gizmo and not self.gizmo.isEmpty(): + gizmo_name = self.gizmo.getName() + #print(f"✓ 坐标轴创建成功: {gizmo_name}") + else: + print("× 坐标轴创建失败") + + else: + print("清除选择...") + self.clearSelectionBox() + self.clearGizmo() + print("✓ 取消选择") + + #当取消选择时,同步清空树形控件的选中状态 + if (hasattr(self.world,'interface_manager')and + self.world.interface_manager and + self.world.interface_manager.treeWidget): + self.world.interface_manager.treeWidget.setCurrentItem(None) + print("✓ 树形控件选中状态已清空") + + #print("=== 选择状态更新完成 ===\n") + except Exception as e: + print(f"更新选择状态失败{str(e)}") + import traceback + traceback.print_exc() + + def _reparentTreeItem(self, item, new_parent_item): + """将树项重新父化到新的父项下""" + if not item or not new_parent_item: + return + + # 从当前父项中移除 + current_parent = item.parent() + if current_parent: + current_parent.removeChild(item) + else: + # 如果是顶级项 + index = self.indexOfTopLevelItem(item) + if index >= 0: + self.takeTopLevelItem(index) + + # 添加到新父项 + new_parent_item.addChild(item) + + # def _updateSelectionVisuals(self, nodePath): + # """更新选择的视觉效果(选择框和坐标轴)""" + # try: + # if nodePath and not nodePath.isEmpty(): + # node_name = nodePath.getName() + # print(f"开始为节点 {node_name} 创建选择框和坐标轴...") + # + # # 创建选择框 + # print("创建选择框...") + # self.createSelectionBox(nodePath) + # if self.selectionBox: + # box_name = "Unknown" + # if self.selectionBox and not self.selectionBox.isEmpty(): + # box_name = self.selectionBox.getName() + # print(f"✓ 选择框创建成功: {box_name}") + # else: + # print("× 选择框创建失败") + # + # # 创建坐标轴 + # print("创建坐标轴...") + # self.createGizmo(nodePath) + # if self.gizmo: + # gizmo_name = "Unknown" + # if self.gizmo and not self.gizmo.isEmpty(): + # gizmo_name = self.gizmo.getName() + # print(f"✓ 坐标轴创建成功: {gizmo_name}") + # else: + # print("× 坐标轴创建失败") + # + # print(f"✓ 选中了节点: {node_name}") + # else: + # print("清除选择...") + # self.clearSelectionBox() + # self.clearGizmo() + # print("✓ 取消选择") + # + # except Exception as e: + # print(f"更新选择视觉效果失败: {e}") + + def getSelectedNode(self): + """获取当前选中的节点""" + return self.selectedNode + + def hasSelection(self): + """检查是否有选中的节点""" + return self.selectedNode is not None + + def checkAndClearIfTargetDeleted(self): + if (self.gizmoTarget and self.gizmoTarget.isEmpty()): + self.clearGizmo() + + if (self.selectionBoxTarget and self.selectionBoxTarget.isEmpty()): + self.clearSelectionBox() + + def setupGizmoCollision(self): + if not self.gizmo or not self.gizmoXAxis or not self.gizmoYAxis or not self.gizmoZAxis: + return + + # 清除现有的碰撞节点 + for axis_name in ["x", "y", "z"]: + axis_node = getattr(self, f"gizmo{axis_name.upper()}Axis") + if axis_node: + # 查找并移除所有现有的碰撞节点 + collision_nodes = axis_node.findAllMatches("**/gizmo_collision_*") + for collision_node in collision_nodes: + collision_node.removeNode() + + # 为每个轴创建碰撞体 + self.createAxisCollision("x", self.gizmoXAxis) + self.createAxisCollision("y", self.gizmoYAxis) + self.createAxisCollision("z", self.gizmoZAxis) + + def createAxisCollision(self, axis_name, axis_node): + # 为单个轴创建碰撞体 + try: + handle_node = axis_node.find(f"{axis_name}_handle") + if not handle_node or handle_node.isEmpty(): + children = axis_node.getChildren() + if children.getNumPaths() > 0: + handle_node = children[0] + else: + print(f"警告: 未找到 {axis_name} 轴的 handle 节点") + return + + collision_node = CollisionNode(f"gizmo_collision_{axis_name}") + collision_node.setIntoCollideMask(BitMask32.bit(1)) # 设置为into对象 + collision_node.setFromCollideMask(BitMask32.allOff()) # 不作为from对象 + + # 调整碰撞尺寸以匹配实际的轴长度和坐标轴缩放 + scale_factor = self.gizmo.getScale().x if self.gizmo else 1.0 + axis_length = 2.0 * scale_factor + radius = 0.3 * scale_factor + + # 根据轴的类型创建合适的碰撞体 + if axis_name == "x": + capsule = CollisionCapsule( + Point3(0, 0, 0), + Point3(axis_length, 0, 0), + radius + ) + collision_node.addSolid(capsule) + elif axis_name == "y": + capsule = CollisionCapsule( + Point3(0, 0, 0), + Point3(0, axis_length, 0), + radius + ) + collision_node.addSolid(capsule) + elif axis_name == "z": + capsule = CollisionCapsule( + Point3(0, 0, 0), + Point3(0, 0, axis_length), + radius + ) + collision_node.addSolid(capsule) + + # 将碰撞节点附加到handle节点,使其与可视化几何体保持一致 + collision_np = handle_node.attachNewNode(collision_node) + + # 设置标签以便识别 + collision_np.setTag("gizmo_axis", axis_name) + collision_np.setTag("pickable", "1") + + collision_np.hide() # 隐藏碰撞体,只用于检测 + + #print(f"✓ 成功创建 {axis_name} 轴碰撞体") + + except Exception as e: + print(f"创建{axis_name}轴碰撞体失败: {e}") + import traceback + traceback.print_exc() + + def detectGizmoAxisWithCollision(self, mouseX, mouseY): + # 使用碰撞体检测鼠标是否悬停在坐标轴上 + if not self.gizmo: + return None + + try: + ray = CollisionRay() + + win_width, win_height = self.world.getWindowSize() + + mouse_x_ndc = (mouseX / win_width) * 2.0 - 1.0 + mouse_y_ndc = 1.0 - (mouseY / win_height) * 2.0 + + ray.setFromLens(self.world.cam.node(), mouse_x_ndc, mouse_y_ndc) + + traverser = CollisionTraverser("gizmo_traverser") + handler = CollisionHandlerQueue() + + # 创建射线节点 + ray_node = CollisionNode('mouseRay') + ray_node.addSolid(ray) + ray_node.setFromCollideMask(BitMask32.bit(1)) # 射线作为from对象 + ray_node.setIntoCollideMask(BitMask32.allOff()) # 射线不作为into对象 + ray_np = self.world.render.attachNewNode(ray_node) + + # 为所有轴的碰撞体设置正确的掩码并添加到遍历器 + collision_found = False + for axis_name in ["x", "y", "z"]: + axis_node = getattr(self, f"gizmo{axis_name.upper()}Axis") + if axis_node: + collision_node_path = axis_node.find("**/gizmo_collision_*") + if not collision_node_path.isEmpty(): + collision_node = collision_node_path.node() + collision_node.setFromCollideMask(BitMask32.allOff()) # 碰撞体不作为from对象 + collision_node.setIntoCollideMask(BitMask32.bit(1)) # 碰撞体作为into对象 + collision_found = True + + if not collision_found: + ray_np.removeNode() + return None + + # 执行碰撞检测 - 这里是关键修复点 + traverser.addCollider(ray_np, handler) + traverser.traverse(self.world.render) + + ray_np.removeNode() + + # 检查是否有碰撞 + if handler.getNumEntries() > 0: + handler.sortEntries() + closest_entry = handler.getEntry(0) + + # 获取碰撞的对象 + collided_object = closest_entry.getIntoNodePath() + axis_tag = collided_object.getTag("gizmo_axis") + + if axis_tag in ["x", "y", "z"]: + return axis_tag + + return None + + except Exception as e: + print(f"使用碰撞体检测坐标轴失败: {e}") + import traceback + traceback.print_exc() + return None + + def debugGizmoCollision(self): + print("===碰撞体调试信息===") + for axis_name in ["x","y","z"]: + axis_node = getattr(self,f"gizmo{axis_name.upper()}Axis") + if axis_node: + handle_node = axis_node.find(f"{axis_name}_handle") + collision_node = axis_node.find("**/gizmo_collision_*") + print(f"{axis_name.upper()}轴:") + print(f" - 轴节点: {axis_node}") + print(f" - Handle节点: {handle_node}") + print(f" - 碰撞节点: {collision_node}") + if not collision_node.isEmpty(): + print(f" - 碰撞体标签: {collision_node.getTag('gizmo_axis')}") + else: + print(f"{axis_name.upper()}轴节点不存在") + + def focusCameraOnSelectedNodeAdvanced(self): + """高级版的摄像机聚焦功能,包含平滑动画效果""" + try: + if not self.selectedNode or self.selectedNode.isEmpty(): + print("没有选中的节点,无法聚焦") + return False + + # 获取选中节点的边界框 + minPoint = Point3() + maxPoint = Point3() + + if not self.selectedNode.calcTightBounds(minPoint, maxPoint, self.world.render): + print("无法计算选中节点的边界框,使用节点为位置作为替代方案") + node_pos = self.selectedNode.getPos(self.world.render) + optimal_distance = 10.0 + current_cam_pos = self.world.cam.getPos() + view_direction = node_pos - current_cam_pos + if view_direction.length()<0.001: + view_direction = Vec3(5,-5,2) + view_direction.normalize() + target_cam_pos = node_pos - (view_direction * optimal_distance) + + temp_node =self.world.render.attachNewNode("temp_lookat_target") + temp_node.setPos(node_pos) + dummy_cam = self.world.render.attachNewNode("dummy_camera") + dummy_cam.setPos(target_cam_pos) + dummy_cam.lookAt(temp_node) + target_cam_hpr = Vec3(dummy_cam.getHpr()) + + temp_node.removeNode() + dummy_cam.removeNode() + + currrent_cam_pos = Point3(self.world.cam.getPos()) + current_cam_hpr = Vec3(self.world.cam.getHpr()) + self._startCameraFocusAnimation(current_cam_pos,target_cam_pos,current_cam_hpr,target_cam_hpr) + print(f"开始聚焦到节点(使用位置): {self.selectedNode.getName()}") + return True + + # 计算节点中心点和大小 + center = Point3( + (minPoint.x + maxPoint.x) * 0.5, + (minPoint.y + maxPoint.y) * 0.5, + (minPoint.z + maxPoint.z) * 0.5 + ) + + # 计算节点的对角线长度 + size = (maxPoint - minPoint).length() + + # 如果节点太小,使用默认大小 + if size < 0.01: + size = 1.0 + + # 获取当前摄像机位置和朝向 + current_cam_pos = Point3(self.world.cam.getPos()) + current_cam_hpr = Vec3(self.world.cam.getHpr()) + + # 计算观察方向 + view_direction = current_cam_pos - center + if view_direction.length() < 0.001: + view_direction = Vec3(5, -5, 2) + + view_direction.normalize() + + # 计算合适的观察距离 + optimal_distance = max(size * 1, 1.0) + + # 计算目标摄像机位置 + target_cam_pos = center + (view_direction * optimal_distance) + + # 计算目标朝向(不直接使用lookAt,而是计算目标HPR) + # 创建临时节点用于计算目标朝向 + temp_node = self.world.render.attachNewNode("temp_lookat_target") + temp_node.setPos(center) + + # 创建另一个临时节点用于计算朝向 + dummy_cam = self.world.render.attachNewNode("dummy_camera") + dummy_cam.setPos(target_cam_pos) + dummy_cam.lookAt(temp_node) + target_cam_hpr = Vec3(dummy_cam.getHpr()) + + # 清理临时节点 + temp_node.removeNode() + dummy_cam.removeNode() + + # 使用任务来实现平滑移动动画 + self._startCameraFocusAnimation(current_cam_pos, target_cam_pos, + current_cam_hpr, target_cam_hpr) + + print(f"开始聚焦到节点: {self.selectedNode.getName()}") + return True + + except Exception as e: + print(f"高级聚焦功能失败: {str(e)}") + import traceback + traceback.print_exc() + return False + + def _startCameraFocusAnimation(self, start_pos, end_pos, start_hpr, end_hpr): + """启动摄像机聚焦动画""" + try: + # 创建动画任务 + class CameraFocusData: + def __init__(self, start_pos, end_pos, start_hpr, end_hpr): + self.start_pos = Point3(start_pos) # 确保是Point3类型 + self.end_pos = Point3(end_pos) # 确保是Point3类型 + self.start_hpr = Vec3(start_hpr) # 确保是Vec3类型 + self.end_hpr = Vec3(end_hpr) # 确保是Vec3类型 + self.elapsed_time = 0.0 + self.duration = 0.8 # 增加动画持续时间到0.8秒,让动画更平滑 + + self._camera_focus_data = CameraFocusData(start_pos, end_pos, start_hpr, end_hpr) + + # 移除之前的任务(如果存在) + taskMgr.remove("cameraFocusTask") + + # 添加新任务 + taskMgr.add(self._cameraFocusTask, "cameraFocusTask") + + except Exception as e: + print(f"启动摄像机聚焦动画失败: {e}") + + def _normalizeAngle(self, angle): + """规范化角度到-180到180度之间""" + while angle > 180: + angle -= 360 + while angle < -180: + angle += 360 + return angle + + def _cameraFocusTask(self, task): + """摄像机聚焦动画任务""" + try: + if not hasattr(self, '_camera_focus_data'): + return task.done + + data = self._camera_focus_data + data.elapsed_time += globalClock.getDt() + + # 计算插值因子 + t = min(1.0, data.elapsed_time / data.duration) + + # 使用更平滑的插值函数 + smooth_t = t * t * (3 - 2 * t) # 平滑步进插值 + + # 手动实现lerp功能 + def lerp_point3(start, end, factor): + return Point3( + start.x + (end.x - start.x) * factor, + start.y + (end.y - start.y) * factor, + start.z + (end.z - start.z) * factor + ) + + # 角度插值需要特殊处理,确保选择最短路径 + def lerp_hpr(start, end, factor): + # 规范化角度 + start_x = self._normalizeAngle(start.x) + start_y = self._normalizeAngle(start.y) + start_z = self._normalizeAngle(start.z) + + end_x = self._normalizeAngle(end.x) + end_y = self._normalizeAngle(end.y) + end_z = self._normalizeAngle(end.z) + + # 计算最短旋转路径 + diff_x = self._normalizeAngle(end_x - start_x) + diff_y = self._normalizeAngle(end_y - start_y) + diff_z = self._normalizeAngle(end_z - start_z) + + # 插值 + result_x = start_x + diff_x * factor + result_y = start_y + diff_y * factor + result_z = start_z + diff_z * factor + + # 再次规范化 + result_x = self._normalizeAngle(result_x) + result_y = self._normalizeAngle(result_y) + result_z = self._normalizeAngle(result_z) + + return Vec3(result_x, result_y, result_z) + + # 计算当前位置和朝向 + current_pos = lerp_point3(data.start_pos, data.end_pos, smooth_t) + current_hpr = lerp_hpr(data.start_hpr, data.end_hpr, smooth_t) + + # 应用到摄像机 + self.world.cam.setPos(current_pos) + self.world.cam.setHpr(current_hpr) + + # 检查是否完成 + if t >= 1.0: + if hasattr(self, '_camera_focus_data'): + delattr(self, '_camera_focus_data') + print("摄像机聚焦动画完成") + return task.done + + return task.cont + + except Exception as e: + print(f"摄像机聚焦任务出错: {e}") + import traceback + traceback.print_exc() + return task.done + + def _smoothCameraMoveTask(self, task): + """平滑摄像机移动任务""" + try: + if not hasattr(self, '_smooth_camera_move_data'): + return task.done + + data = self._smooth_camera_move_data + data.elapsed_time += globalClock.getDt() + + # 计算插值因子 + t = min(1.0, data.elapsed_time / data.duration) + + # 使用平滑插值 + smooth_t = t * t * (3 - 2 * t) + + # 角度插值需要特殊处理 + def lerp_hpr(start, end, factor): + # 规范化角度 + start_x = self._normalizeAngle(start.x) + start_y = self._normalizeAngle(start.y) + start_z = self._normalizeAngle(start.z) + + end_x = self._normalizeAngle(end.x) + end_y = self._normalizeAngle(end.y) + end_z = self._normalizeAngle(end.z) + + # 计算最短旋转路径 + diff_x = self._normalizeAngle(end_x - start_x) + diff_y = self._normalizeAngle(end_y - start_y) + diff_z = self._normalizeAngle(end_z - start_z) + + # 插值 + result_x = start_x + diff_x * factor + result_y = start_y + diff_y * factor + result_z = start_z + diff_z * factor + + return Vec3(result_x, result_y, result_z) + + # 计算当前位置和朝向 + current_pos = Point3( + data.start_pos.x + (data.end_pos.x - data.start_pos.x) * smooth_t, + data.start_pos.y + (data.end_pos.y - data.start_pos.y) * smooth_t, + data.start_pos.z + (data.end_pos.z - data.start_pos.z) * smooth_t + ) + + current_hpr = lerp_hpr(data.start_hpr, data.end_hpr, smooth_t) + + # 应用到摄像机 + self.world.cam.setPos(current_pos) + self.world.cam.setHpr(current_hpr) + + # 检查是否完成 + if t >= 1.0: + if hasattr(self, '_smooth_camera_move_data'): + delattr(self, '_smooth_camera_move_data') + print("摄像机平滑移动完成") + return task.done + + return task.cont + + except Exception as e: + print(f"平滑摄像机移动任务出错: {e}") + import traceback + traceback.print_exc() + return task.done + + def handleMouseClick(self, nodePath, mouseX=None, mouseY=None): + """处理鼠标点击事件 - 支持坐标轴双击聚焦""" + try: + # 如果正在删除节点,忽略鼠标点击 + if hasattr(self, '_deleting_node') and self._deleting_node: + print("正在删除节点,忽略鼠标点击") + return + + import time + current_time = time.time() + + # 检查是否点击了坐标轴 + is_gizmo_click = False + target_node = nodePath + + # 判断是否点击了坐标轴 + if (nodePath and hasattr(nodePath, 'getName') and + (nodePath.getName().startswith("gizmo") or + "gizmo" in nodePath.getName().lower() or + (hasattr(nodePath, 'hasTag') and nodePath.hasTag("is_gizmo")))): + is_gizmo_click = True + # 如果有选中的模型,使用选中的模型作为聚焦目标 + if self.selectedNode and not self.selectedNode.isEmpty(): + target_node = self.selectedNode + print(f"检测到坐标轴点击,使用目标节点: {target_node.getName() if target_node else 'None'}") + + # 检查是否为双击(同一节点且在时间阈值内) + is_double_click = (self._last_clicked_node == target_node and + target_node is not None and + current_time - self._last_click_time < self._double_click_threshold) + + if is_double_click: + # 双击 detected + node_name = target_node.getName() if target_node else "None" + print(f"检测到双击节点: {node_name}") + + # 无论是点击模型还是坐标轴,都执行聚焦 + if target_node and not target_node.isEmpty(): + print(f"双击聚焦到节点: {target_node.getName()}") + if self.selectedNode != target_node: + self.updateSelection(target_node) + else: + self.focusCameraOnSelectedNodeAdvanced() + else: + print("双击事件:没有有效的目标节点") + + # 重置状态以避免三击等误触发 + self._last_click_time = 0 + self._last_clicked_node = None + else: + # 单击,更新状态 + self._last_click_time = current_time + self._last_clicked_node = target_node + + # 如果点击的是坐标轴,保持当前选择不变 + if is_gizmo_click: + print("坐标轴单击,保持当前选择") + else: + # 正常的单击选择 + self.updateSelection(nodePath) + + except Exception as e: + print(f"处理鼠标点击事件失败: {e}") + + def _onDoubleClick(self, nodePath): + """双击事件处理""" + try: + # 获取实际要聚焦的目标节点 + target_node = nodePath + + # 如果是坐标轴,确保使用关联的模型作为目标 + if (nodePath and hasattr(nodePath, 'hasTag') and + nodePath.hasTag("is_gizmo")): + if self.selectedNode and not self.selectedNode.isEmpty(): + target_node = self.selectedNode + print(f"坐标轴双击,聚焦到关联模型: {target_node.getName()}") + else: + print("坐标轴双击,但没有关联的选中模型") + return + + if target_node and not target_node.isEmpty(): + print(f"双击聚焦到节点: {target_node.getName()}") + # 更新选择(如果需要) + if self.selectedNode != target_node: + self.updateSelection(target_node) + + # 执行聚焦 + self.focusCameraOnSelectedNodeAdvanced() + else: + print("双击事件:没有有效的目标节点") + + except Exception as e: + print(f"双击事件处理失败: {e}") + + # 添加一个更精确的双击检测方法 + + def checkDoubleClick(self, nodePath): + """检查是否为双击,返回布尔值""" + try: + import time + current_time = time.time() + + # 必须是同一节点且在时间阈值内 + is_double_click = (self._last_clicked_node == nodePath and + nodePath is not None and + current_time - self._last_click_time < self._double_click_threshold) + + if is_double_click: + # 重置状态 + self._last_click_time = 0 + self._last_clicked_node = None + return True + else: + # 更新状态 + self._last_click_time = current_time + self._last_clicked_node = nodePath + return False + + except Exception as e: + print(f"双击检测失败: {e}") + return False + + # 添加一个定时重置方法,用于清除长时间未完成的双击状态 + + def _resetDoubleClickState(self): + """重置双击状态""" + self._last_click_time = 0 + self._last_clicked_node = None + + # 添加一个任务来自动重置双击状态 + + def _startDoubleClickResetTask(self): + """启动双击状态重置任务""" + if self._double_click_task: + taskMgr.remove(self._double_click_task) + + self._double_click_task = taskMgr.doMethodLater( + self._double_click_threshold * 2, # 等待2倍阈值时间 + self._resetDoubleClickStateTask, + "resetDoubleClickState" + ) + + def _resetDoubleClickStateTask(self, task): + """任务:重置双击状态""" + self._resetDoubleClickState() + self._double_click_task = None + return task.done + + # 修改 updateSelection 方法以集成双击检测 + + # def updateSelection(self, nodePath): + # """更新选择状态""" + # print(f"\n=== 更新选择状态 ===") + # + # # 如果正在删除节点,避免更新选择 + # if hasattr(self, '_deleting_node') and self._deleting_node: + # print("正在删除节点,跳过选择更新") + # print("=== 选择状态更新完成 ===\n") + # return + # + # node_name = "None" + # if nodePath and not nodePath.isEmpty(): + # node_name = nodePath.getName() + # print(f"新选择的节点: {node_name}") + # + # # 检查是否为双击 + # is_double_click = self.checkDoubleClick(nodePath) + # if is_double_click: + # print(f"检测到双击 {node_name},执行聚焦") + # # 启动聚焦(在下一帧执行,确保选择状态已更新) + # taskMgr.doMethodLater(0.01, self._delayedFocusTask, "delayedFocus") + # + # self.selectedNode = nodePath + # # 添加兼容性属性 + # self.selectedObject = nodePath + # + # if nodePath and not nodePath.isEmpty(): + # node_name = nodePath.getName() + # print(f"开始为节点 {node_name} 创建选择框和坐标轴...") + # + # # 创建选择框 + # print("创建选择框...") + # self.createSelectionBox(nodePath) + # if self.selectionBox: + # box_name = "Unknown" + # if self.selectionBox and not self.selectionBox.isEmpty(): + # box_name = self.selectionBox.getName() + # print(f"✓ 选择框创建成功: {box_name}") + # else: + # print("× 选择框创建失败") + # + # # 创建坐标轴 + # print("创建坐标轴...") + # self.createGizmo(nodePath) + # if self.gizmo: + # gizmo_name = "Unknown" + # if self.gizmo and not self.gizmo.isEmpty(): + # gizmo_name = self.gizmo.getName() + # print(f"✓ 坐标轴创建成功: {gizmo_name}") + # else: + # print("× 坐标轴创建失败") + # + # print(f"✓ 选中了节点: {node_name}") + # else: + # print("清除选择...") + # self.clearSelectionBox() + # self.clearGizmo() + # print("✓ 取消选择") + # + # print("=== 选择状态更新完成 ===\n") + + def _delayedFocusTask(self, task): + """延迟执行聚焦任务""" + try: + self.focusCameraOnSelectedNodeAdvanced() + except Exception as e: + print(f"延迟聚焦任务失败: {e}") + return task.done + + # 添加一个更智能的双击检测方法,考虑鼠标位置 + + def checkDoubleClickWithPosition(self, nodePath, mouse_x=None, mouse_y=None): + """检查是否为双击,同时考虑鼠标位置""" + try: + import time + current_time = time.time() + + # 如果没有提供鼠标位置,直接使用基本双击检测 + if mouse_x is None or mouse_y is None: + return self.checkDoubleClick(nodePath) + + # 检查节点和时间 + time_diff = current_time - self._last_click_time + is_same_node = (self._last_clicked_node == nodePath) + + # 如果是同一节点且在时间阈值内,认为是双击 + if is_same_node and time_diff < self._double_click_threshold: + # 重置状态 + self._last_click_time = 0 + self._last_clicked_node = None + return True + else: + # 更新状态 + self._last_click_time = current_time + self._last_clicked_node = nodePath + return False + + except Exception as e: + print(f"带位置的双击检测失败: {e}") + return False + + # 添加一个公共方法来设置双击阈值 + + def setDoubleClickThreshold(self, threshold_seconds): + """设置双击时间阈值""" + if threshold_seconds > 0: + self._double_click_threshold = threshold_seconds + print(f"双击阈值已设置为: {threshold_seconds} 秒") + else: + print("无效的双击阈值") + + # 添加一个方法来手动触发双击聚焦(可用于测试或其他触发方式) + + def triggerDoubleClickFocus(self, nodePath=None): + """手动触发双击聚焦""" + try: + target_node = nodePath if nodePath else self.selectedNode + if target_node and not target_node.isEmpty(): + print(f"手动触发聚焦到节点: {target_node.getName()}") + if self.selectedNode != target_node: + self.updateSelection(target_node) + self.focusCameraOnSelectedNodeAdvanced() + return True + else: + print("没有有效的目标节点进行聚焦") + return False + except Exception as e: + print(f"手动触发聚焦失败: {e}") + return False + + def cleanup(self): + """清理选择系统资源""" + # 清理双击任务 + if self._double_click_task: + taskMgr.remove(self._double_click_task) + self._double_click_task = None + + # 清理其他资源 + self.clearSelectionBox() + self.clearGizmo() + def clearSelection(self): + """清除当前选择""" + try: + self.selectedNode = None + self.selectedObject = None + self.clearSelectionBox() + self.clearGizmo() + + # 清除树形控件中的选择 + if (hasattr(self.world, 'interface_manager') and + self.world.interface_manager and + hasattr(self.world.interface_manager, 'treeWidget') and + self.world.interface_manager.treeWidget): + self.world.interface_manager.treeWidget.setCurrentItem(None) + + print("已清除选择") + except Exception as e: + print(f"清除选择失败: {e}") + diff --git a/core/terrain_manager.py b/core/terrain_manager.py index edbec218..a1c57dfb 100644 --- a/core/terrain_manager.py +++ b/core/terrain_manager.py @@ -1,660 +1,660 @@ -# core/terrain_manager.py -import time -import urllib - -from panda3d.core import GeoMipTerrain, PNMImage, Texture, Vec3, NodePath -from panda3d.core import Filename, Material, ColorAttrib, AmbientLight, DirectionalLight -import os - -from scene import util - - -class TerrainManager: - """地形管理类""" - - def __init__(self, world): - self.world = world - self.terrains = [] - - # core/terrain_manager.py - def _get_tree_widget(self): - """安全获取树形控件""" - try: - if (hasattr(self.world, 'interface_manager') and - hasattr(self.world.interface_manager, 'treeWidget')): - return self.world.interface_manager.treeWidget - except AttributeError: - pass - return None - - def createTerrainFromHeightMap(self, heightmap_path, scale=(1, 1, 1)): - """从高度图创建地形""" - # 检查文件是否存在 - if not os.path.exists(heightmap_path): - print(f"错误: 高度图文件 {heightmap_path} 不存在") - return None - - try: - print(f"🔆 开始创建高度图地形") - - # 获取树形控件 - tree_widget = self._get_tree_widget() - if not tree_widget: - print("❌ 无法访问树形控件") - return None - - # 获取目标父节点列表 - target_parents = tree_widget.get_target_parents_for_creation() - if not target_parents: - print("❌ 没有找到有效的父节点") - return None - - created_terrains = [] - try: - parent_item, parent_node = target_parents[0] - - # 创建GeoMipTerrain对象 - terrain_name = f"terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" - terrain = GeoMipTerrain(terrain_name) - - # 加载高度图 - height_image = PNMImage(Filename.fromOsSpecific(heightmap_path)) - - # 检查并调整图像尺寸为2的幂次方加1 - width, height = height_image.getXSize(), height_image.getYSize() - print(f"原始图像尺寸: {width}x{height}") - - # 找到最接近的有效尺寸 - valid_sizes = [17, 33, 65, 129, 257, 513, 1025, 2049] - target_size = 129 # 默认尺寸 - - # 选择最接近的尺寸 - max_dim = max(width, height) - for size in valid_sizes: - if size >= max_dim: - target_size = size - break - else: - target_size = valid_sizes[-1] # 使用最大尺寸 - - # 如果需要,调整图像尺寸 - if width != target_size or height != target_size: - print(f"调整图像尺寸从 {width}x{height} 到 {target_size}x{target_size}") - # 使用正确的图像缩放方法 - resized_image = PNMImage(target_size, target_size) - resized_image.quickFilterFrom(height_image) - height_image = resized_image - - # 使用正确的方法设置高度图 - terrain.setHeightfield(height_image) - - # 设置地形参数 - terrain.setBruteforce(True) # 使用LOD - - terrain.setBlockSize(32) - # terrain.setNearFarThreshold(50.0,200.0) - - # 生成地形 - terrain.generate() - - # 获取地形节点 - terrain_node = terrain.getRoot() - - if terrain_node.isEmpty(): - print("错误:无法生成有效的地形节点") - return None - - terrain_node.setTag("is_scene_element", "1") - terrain_node.setTag("tree_item_type", "TERRAIN_NODE") - - node_name = f"Terrain_{os.path.basename(heightmap_path)}_{len(self.terrains)}" - terrain_node.setName(node_name) - - center_offset = (target_size - 1) / 2 - terrain_node.setPos(-center_offset * scale[0], -center_offset * scale[1], -5) - - # 设置缩放 - terrain_node.setScale(scale[0], scale[1], scale[2]) - - # 将地形添加到场景中 - terrain_node.reparentTo(parent_node) - - from panda3d.core import BitMask32 - # 设置地形节点的碰撞掩码 - terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码 - # 为地形的所有子节点也设置碰撞掩码 - for child in terrain_node.getChildren(): - child.setCollideMask(BitMask32.bit(2)) - - # 添加材质 - self._applyTerrainMaterial(terrain_node) - - terrain_node.setPythonTag("selectable", True) - - # 保存地形信息(包括高度图的副本) - terrain_info = { - 'terrain': terrain, - 'node': terrain_node, - 'heightmap': heightmap_path, - 'heightfield': height_image, # 保存高度图副本 - 'scale': scale, - 'name': node_name - } - - self.terrains.append(terrain_info) - - print(f"✅ 为 {parent_item.text(0)} 创建高度图地形: {terrain_name}") - - # 在Qt树形控件中添加对应节点 - qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE") - if qt_item: - created_terrains.append((terrain_node, qt_item)) - else: - created_terrains.append((terrain_node, None)) - print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") - - except Exception as e: - print(f"❌ 为 {parent_item.text(0)} 创建高度图地形: {str(e)}") - return None - - # 处理创建结果 - if not created_terrains: - print("❌ 没有成功创建任何高度图地形") - return None - - # 选中最后创建的光源 - if created_terrains: - last_light_np, last_qt_item = created_terrains[-1] - if last_qt_item: - tree_widget.setCurrentItem(last_qt_item) - # 更新选择和属性面板 - tree_widget.update_selection_and_properties(last_light_np, last_qt_item) - - print(f"🎉 总共创建了 {len(created_terrains)} 个高度图地形") - - # 返回值处理 - if len(created_terrains) == 1: - return created_terrains[0][0] # 单个光源返回NodePath - else: - return [light_np for light_np, _ in created_terrains] # 多个光源返回列表 - - except Exception as e: - print(f"❌ 创建高度图地形过程失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def createFlatTerrain(self, size=(0.3, 0.3), resolution=129): - """创建平面地形""" - try: - print(f"🔆 开始创建平面地形") - - # 获取树形控件 - tree_widget = self._get_tree_widget() - if not tree_widget: - print("❌ 无法访问树形控件") - return None - - # 获取目标父节点列表 - target_parents = tree_widget.get_target_parents_for_creation() - if not target_parents: - print("❌ 没有找到有效的父节点") - return None - - created_terrains = [] - - try: - parent_item, parent_node = target_parents[0] - # 确保分辨率是2的幂次方加1 (如129, 257, 513等) - valid_resolutions = [17, 33, 65, 129, 257, 513, 1025] - if resolution not in valid_resolutions: - # 找到最接近的有效分辨率 - closest_res = min(valid_resolutions, key=lambda x: abs(x - resolution)) - print(f"警告: 分辨率 {resolution} 不是有效的地形分辨率,使用 {closest_res}") - resolution = closest_res - - # 创建GeoMipTerrain对象 - terrain_name = f"flat_terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" - terrain = GeoMipTerrain(terrain_name) - - # 创建一个平面高度图,尺寸必须是2^n+1 - height_image = PNMImage(resolution, resolution) - height_image.fill(0.5) # 设置为中等高度 (0.0-1.0范围) - - # 使用正确的方法设置高度图 - terrain.setHeightfield(height_image) - terrain.setBruteforce(True) # 设置LOD - - # 设置地形参数 - terrain.setBlockSize(32) # 设置块大小 - - # 生成地形 - terrain.generate() - - # 获取地形节点 - terrain_node = terrain.getRoot() - - if terrain_node.isEmpty(): - print("错误:无法生成有效的平面地形节点") - return None - - terrain_node.setTag("is_scene_element", "1") - terrain_node.setTag("tree_item_type", "TERRAIN_NODE") - - node_name = f"FlatTerrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" - terrain_node.setName(node_name) - - center_offset = (resolution - 1) / 2 - terrain_node.setPos(-center_offset * size[0], -center_offset * size[1], 0) - - # 将地形添加到场景中 - terrain_node.reparentTo(parent_node) - - # 为地形添加碰撞体 - from panda3d.core import BitMask32 - # 设置地形节点的碰撞掩码 - terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码 - # 为地形的所有子节点也设置碰撞掩码 - for child in terrain_node.getChildren(): - child.setCollideMask(BitMask32.bit(2)) - - # 添加材质 - self._applyTerrainMaterial(terrain_node) - - # 保存地形信息(包括高度图) - terrain_info = { - 'terrain': terrain, - 'node': terrain_node, - 'heightmap': None, - 'heightfield': height_image, # 保存高度图 - 'scale': (size[0], size[1], 50), - 'name': node_name - } - - self.terrains.append(terrain_info) - - print(f"✅ 为 {parent_item.text(0)} 创建平面地形: {terrain_name}") - - # 在Qt树形控件中添加对应节点 - qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE") - if qt_item: - created_terrains.append((terrain_node, qt_item)) - else: - created_terrains.append((terrain_node, None)) - print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") - - except Exception as e: - print(f"❌ 为 {parent_item.text(0)} 创建平面地形: {str(e)}") - return None - - # 处理创建结果 - if not created_terrains: - print("❌ 没有成功创建任何平面地形") - return None - - # 选中最后创建的光源 - if created_terrains: - last_light_np, last_qt_item = created_terrains[-1] - if last_qt_item: - tree_widget.setCurrentItem(last_qt_item) - # 更新选择和属性面板 - tree_widget.update_selection_and_properties(last_light_np, last_qt_item) - - print(f"🎉 总共创建了 {len(created_terrains)} 个平面地形") - - # 返回值处理 - if len(created_terrains) == 1: - return created_terrains[0][0] # 单个光源返回NodePath - else: - return [light_np for light_np, _ in created_terrains] # 多个光源返回列表 - - except Exception as e: - print(f"❌ 创建平面地形过程失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def _applyTerrainMaterial(self, terrain_node): - """为地形应用材质""" - try: - # 创建材质 - material = Material() - material.setAmbient((0.5, 0.5, 0.5, 1.0)) # 环境光反射 - material.setDiffuse((1, 1, 1, 1.0)) # 漫反射(绿色调) - material.setSpecular((0.2, 0.2, 0.2, 1.0)) # 镜面反射 - material.setShininess(5.0) # 高光强度 - - # 应用材质到地形 - terrain_node.setMaterial(material) - - # 启用材质 - terrain_node.setAttrib(ColorAttrib.makeDefault()) - - print("✓ 地形材质已应用") - except Exception as e: - print(f"应用地形材质时出错: {e}") - - def updateTerrain(self): - """更新所有地形的LOD""" - for terrain_info in self.terrains: - terrain = terrain_info['terrain'] - if hasattr(terrain, 'update'): - terrain.update() - - def setTerrainLODThreshold(self, terrain_info, threshold): - """设置地形LOD阈值""" - if terrain_info in self.terrains: - terrain = terrain_info['terrain'] - # GeoMipTerrain可能没有setNearFarThreshold方法,使用其他方式设置LOD - print("注意: GeoMipTerrain可能不支持直接设置LOD阈值") - - def getTerrainHeight(self, terrain_info, x, y): - """获取地形上指定点的高度""" - if terrain_info in self.terrains: - terrain = terrain_info['terrain'] - if hasattr(terrain, 'getElevation'): - return terrain.getElevation(x, y) - return 0 - - # core/terrain_manager.py - - def modifyTerrainHeight(self, terrain_info, x, y, radius, strength, operation="add"): - """修改地形高度""" - if terrain_info not in self.terrains: - return False - - try: - terrain = terrain_info['terrain'] - terrain_node = terrain_info['node'] # 获取地形节点 - - # 直接使用我们保存的高度图数据 - if 'heightfield' not in terrain_info: - print("地形信息中没有保存高度图数据") - return False - - heightmap = terrain_info['heightfield'] - if not heightmap: - print("无法获取地形高度图数据") - return False - - # 获取高度图尺寸 - width = heightmap.getXSize() - height = heightmap.getYSize() - - # 获取地形节点信息 - terrain_pos = terrain_node.getPos() - terrain_scale = terrain_node.getScale() - - # 正确的坐标转换方法 - # 1. 将世界坐标转换为相对于地形中心的坐标 - relative_x = x - terrain_pos.getX() - relative_y = y - terrain_pos.getY() - - # 2. 考虑地形的缩放 - scaled_x = relative_x / terrain_scale.getX() - scaled_y = relative_y / terrain_scale.getY() - - # 3. 转换为高度图坐标 - center_offset = (width - 1) / 2 - center_x = int(scaled_x + center_offset) - center_y = int(scaled_y + center_offset) - - print(f"坐标转换详情:") - print(f" 世界坐标: ({x:.2f}, {y:.2f})") - print(f" 地形位置: ({terrain_pos.getX():.2f}, {terrain_pos.getY():.2f})") - print(f" 地形缩放: ({terrain_scale.getX():.2f}, {terrain_scale.getY():.2f})") - print(f" 相对坐标: ({relative_x:.2f}, {relative_y:.2f})") - print(f" 缩放坐标: ({scaled_x:.2f}, {scaled_y:.2f})") - print(f" 中心偏移: {center_offset}") - print(f" 高度图坐标: ({center_x}, {center_y})") - print(f" 高度图尺寸: {width} x {height}") - - # 检查中心点是否在有效范围内 - if not (0 <= center_x < width and 0 <= center_y < height): - print(f"❌ 中心点坐标超出范围: ({center_x}, {center_y})") - print(f"❌ 有效范围: (0-{width - 1}, 0-{height - 1})") - return False - - # 修改半径范围内的点 - modified = False - - # 使用正确的半径计算(考虑地形缩放) - effective_radius = max(1, int(radius / max(terrain_scale.getX(), terrain_scale.getY()))) - - print(f"有效编辑半径: {effective_radius}") - - # 限制编辑半径不超过地形尺寸的1/4,避免性能问题 - effective_radius = min(effective_radius, width // 4, height // 4) - - for dx in range(-effective_radius, effective_radius + 1): - for dy in range(-effective_radius, effective_radius + 1): - # 检查距离 - distance = (dx * dx + dy * dy) ** 0.5 - if distance <= effective_radius: - # 计算影响权重 - weight = 1.0 - (distance / effective_radius) if effective_radius > 0 else 1.0 - - # 计算目标点坐标 - target_x = center_x + dx - target_y = center_y + dy - - # 检查边界 - if 0 <= target_x < width and 0 <= target_y < height: - # 获取当前高度 - current_height = heightmap.getRed(target_x, target_y) - - # 根据操作类型修改高度 - if operation == "add": - new_height = min(1.0, current_height + strength * weight * 0.01) - elif operation == "subtract": - new_height = max(0.0, current_height - strength * weight * 0.01) - elif operation == "set": - # 平坦化操作,设置为中等高度 - new_height = 0.5 - else: - new_height = current_height - - # 设置新高度 - heightmap.setRed(target_x, target_y, new_height) - heightmap.setGreen(target_x, target_y, new_height) - heightmap.setBlue(target_x, target_y, new_height) - modified = True - - # 如果有修改,则重新生成地形 - if modified: - # 重新设置高度图并生成地形 - terrain.setHeightfield(heightmap) - terrain.generate() - - # 关键修复:重新创建碰撞体,因为在生成地形时会丢失原有碰撞体 - from panda3d.core import BitMask32, CollisionNode, CollisionPlane, Plane, Point3 - # 移除旧的地形碰撞体(如果存在) - for child in terrain_node.getChildren(): - if isinstance(child.node(), CollisionNode) and child.getName().startswith("terrain_collision_"): - child.removeNode() - - # 创建新的地形碰撞体 - # 使用更简单的方法:直接给地形节点添加碰撞体 - terrain_node.setCollideMask(BitMask32.bit(2)) # 确保地形节点本身具有碰撞掩码 - - # 为地形的每个子节点也设置碰撞掩码 - for child in terrain_node.getChildren(): - child.setCollideMask(BitMask32.bit(2)) - - print(f"✓ 地形高度已修改: 位置({x}, {y}), 半径{radius}, 操作{operation}") - print(f"✓ 重新设置了地形碰撞体") - - return modified - - except Exception as e: - print(f"修改地形高度时出错: {e}") - import traceback - traceback.print_exc() - return False - - def deleteTerrain(self, terrain_info): - """删除地形""" - if terrain_info in self.terrains: - # 从场景中移除地形节点 - terrain_node = terrain_info['node'] - if terrain_node and not terrain_node.isEmpty(): - tree_widget = self._get_tree_widget() - if tree_widget: - tree_widget.delete_items(tree_widget.selectedItems()) - # terrain_node.removeNode() - # - # # 从列表中移除 - # self.terrains.remove(terrain_info) - - # # 更新场景树 - # if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): - # self.world.scene_manager.updateSceneTree() - - print(f"✓ 地形已删除: {terrain_info.get('name', 'Unknown')}") - - import gc - gc.collect() - - return True - return False - - def getTerrainByName(self, name): - """根据名称查找地形""" - for terrain_info in self.terrains: - if terrain_info.get('name') == name: - return terrain_info - return None - - def getAllTerrainNames(self): - """获取所有地形名称""" - return [terrain_info.get('name', f'Terrain_{i}') for i, terrain_info in enumerate(self.terrains)] - - def setTerrainColor(self, terrain_info, color): - """设置地形颜色""" - try: - if terrain_info in self.terrains: - terrain_node = terrain_info['node'] - if terrain_node: - # 创建新材质 - material = Material() - material.setAmbient((color[0] * 0.5, color[1] * 0.5, color[2] * 0.5, 1.0)) - material.setDiffuse((color[0], color[1], color[2], 1.0)) - material.setSpecular((0.2, 0.2, 0.2, 1.0)) - material.setShininess(5.0) - - # 应用材质 - terrain_node.setMaterial(material) - print(f"✓ 地形颜色已更新: {color}") - return True - return False - except Exception as e: - print(f"设置地形颜色时出错: {e}") - return False - - def setTerrainTexture(self, terrain_info, texture_path): - """为地形设置纹理""" - try: - if terrain_info in self.terrains: - terrain_node = terrain_info['node'] - if terrain_node and os.path.exists(texture_path): - texture_path = util.normalize_model_path(texture_path) - # 加载纹理 - texture = self.world.loader.loadTexture(texture_path) - if texture: - # 应用纹理 - terrain_node.setTexture(texture, 1) - print(f"✓ 地形纹理已应用: {texture_path}") - return True - else: - print(f"✗ 无法加载纹理: {texture_path}") - else: - print(f"✗ 纹理文件不存在: {texture_path}") - return False - except Exception as e: - print(f"应用地形纹理时出错: {e}") - return False - - def saveTerrainData(self, terrain_info, filename): - """保存地形数据到文件""" - try: - terrain_node = terrain_info['node'] - heightfield = terrain_info['heightfield'] - - # 保存高度图到文件 - if heightfield: - # 创建保存路径 - terrain_dir = os.path.join(os.path.dirname(filename), "terrains") - if not os.path.exists(terrain_dir): - os.makedirs(terrain_dir) - - # 生成唯一的地形文件名 - terrain_filename = f"terrain_{terrain_info.get('name', 'unnamed')}_{int(time.time())}.png" - terrain_path = os.path.join(terrain_dir, terrain_filename) - - # 保存高度图 - heightfield.write(Filename.fromOsSpecific(terrain_path)) - - # 保存地形信息到标签 - terrain_node.setTag("terrain_heightmap_path", terrain_path) - terrain_node.setTag("terrain_scale", str(terrain_info['scale'])) - - print(f"✓ 地形数据已保存: {terrain_path}") - return terrain_path - return None - except Exception as e: - print(f"保存地形数据时出错: {e}") - return None - - def _recreateTerrain(self, terrain_node): - """重新创建地形""" - try: - print(f"重新创建地形: {terrain_node.getName()}") - - # 获取保存的地形信息 - heightmap_path = terrain_node.getTag("terrain_heightmap_path") if terrain_node.hasTag( - "terrain_heightmap_path") else None - scale_str = terrain_node.getTag("terrain_scale") if terrain_node.hasTag("terrain_scale") else "(1, 1, 1)" - - # 解析缩放信息 - try: - scale_str = scale_str.strip("()") - scale_values = [float(x.strip()) for x in scale_str.split(",")] - scale = (scale_values[0], scale_values[1], scale_values[2]) if len(scale_values) >= 3 else (1, 1, 1) - except: - scale = (1, 1, 1) - - # 恢复位置信息 - if terrain_node.hasTag("transform_pos"): - try: - pos_str = terrain_node.getTag("transform_pos") - pos_str = pos_str.replace('LVecBase3f', '').replace('LPoint3f', '').strip('()') - pos_values = [float(x.strip()) for x in pos_str.split(',')] - if len(pos_values) >= 3: - terrain_node.setPos(pos_values[0], pos_values[1], pos_values[2]) - except Exception as e: - print(f"恢复地形位置失败: {e}") - - # 如果有高度图路径,重新创建地形 - if heightmap_path and os.path.exists(heightmap_path): - # 使用现有方法重新创建地形 - new_terrain = self.createTerrainFromHeightMap(heightmap_path, scale) - if new_terrain: - # 删除旧的地形节点 - if not terrain_node.isEmpty(): - terrain_node.removeNode() - return new_terrain - - # 如果没有高度图或创建失败,创建一个平面地形作为替代 - print("使用平面地形作为替代") - new_terrain = self.createFlatTerrain(size=(scale[0], scale[1]), resolution=129) - if new_terrain: - # 删除旧的地形节点 - if not terrain_node.isEmpty(): - terrain_node.removeNode() - return new_terrain - - return terrain_node - except Exception as e: - print(f"重新创建地形失败: {e}") - return terrain_node - +# core/terrain_manager.py +import time +import urllib + +from panda3d.core import GeoMipTerrain, PNMImage, Texture, Vec3, NodePath +from panda3d.core import Filename, Material, ColorAttrib, AmbientLight, DirectionalLight +import os + +from scene import util + + +class TerrainManager: + """地形管理类""" + + def __init__(self, world): + self.world = world + self.terrains = [] + + # core/terrain_manager.py + def _get_tree_widget(self): + """安全获取树形控件""" + try: + if (hasattr(self.world, 'interface_manager') and + hasattr(self.world.interface_manager, 'treeWidget')): + return self.world.interface_manager.treeWidget + except AttributeError: + pass + return None + + def createTerrainFromHeightMap(self, heightmap_path, scale=(1, 1, 1)): + """从高度图创建地形""" + # 检查文件是否存在 + if not os.path.exists(heightmap_path): + print(f"错误: 高度图文件 {heightmap_path} 不存在") + return None + + try: + print(f"🔆 开始创建高度图地形") + + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + # 获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None + + created_terrains = [] + try: + parent_item, parent_node = target_parents[0] + + # 创建GeoMipTerrain对象 + terrain_name = f"terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" + terrain = GeoMipTerrain(terrain_name) + + # 加载高度图 + height_image = PNMImage(Filename.fromOsSpecific(heightmap_path)) + + # 检查并调整图像尺寸为2的幂次方加1 + width, height = height_image.getXSize(), height_image.getYSize() + print(f"原始图像尺寸: {width}x{height}") + + # 找到最接近的有效尺寸 + valid_sizes = [17, 33, 65, 129, 257, 513, 1025, 2049] + target_size = 129 # 默认尺寸 + + # 选择最接近的尺寸 + max_dim = max(width, height) + for size in valid_sizes: + if size >= max_dim: + target_size = size + break + else: + target_size = valid_sizes[-1] # 使用最大尺寸 + + # 如果需要,调整图像尺寸 + if width != target_size or height != target_size: + print(f"调整图像尺寸从 {width}x{height} 到 {target_size}x{target_size}") + # 使用正确的图像缩放方法 + resized_image = PNMImage(target_size, target_size) + resized_image.quickFilterFrom(height_image) + height_image = resized_image + + # 使用正确的方法设置高度图 + terrain.setHeightfield(height_image) + + # 设置地形参数 + terrain.setBruteforce(True) # 使用LOD + + terrain.setBlockSize(32) + # terrain.setNearFarThreshold(50.0,200.0) + + # 生成地形 + terrain.generate() + + # 获取地形节点 + terrain_node = terrain.getRoot() + + if terrain_node.isEmpty(): + print("错误:无法生成有效的地形节点") + return None + + terrain_node.setTag("is_scene_element", "1") + terrain_node.setTag("tree_item_type", "TERRAIN_NODE") + + node_name = f"Terrain_{os.path.basename(heightmap_path)}_{len(self.terrains)}" + terrain_node.setName(node_name) + + center_offset = (target_size - 1) / 2 + terrain_node.setPos(-center_offset * scale[0], -center_offset * scale[1], -5) + + # 设置缩放 + terrain_node.setScale(scale[0], scale[1], scale[2]) + + # 将地形添加到场景中 + terrain_node.reparentTo(parent_node) + + from panda3d.core import BitMask32 + # 设置地形节点的碰撞掩码 + terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码 + # 为地形的所有子节点也设置碰撞掩码 + for child in terrain_node.getChildren(): + child.setCollideMask(BitMask32.bit(2)) + + # 添加材质 + self._applyTerrainMaterial(terrain_node) + + terrain_node.setPythonTag("selectable", True) + + # 保存地形信息(包括高度图的副本) + terrain_info = { + 'terrain': terrain, + 'node': terrain_node, + 'heightmap': heightmap_path, + 'heightfield': height_image, # 保存高度图副本 + 'scale': scale, + 'name': node_name + } + + self.terrains.append(terrain_info) + + print(f"✅ 为 {parent_item.text(0)} 创建高度图地形: {terrain_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE") + if qt_item: + created_terrains.append((terrain_node, qt_item)) + else: + created_terrains.append((terrain_node, None)) + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建高度图地形: {str(e)}") + return None + + # 处理创建结果 + if not created_terrains: + print("❌ 没有成功创建任何高度图地形") + return None + + # 选中最后创建的光源 + if created_terrains: + last_light_np, last_qt_item = created_terrains[-1] + if last_qt_item: + tree_widget.setCurrentItem(last_qt_item) + # 更新选择和属性面板 + tree_widget.update_selection_and_properties(last_light_np, last_qt_item) + + print(f"🎉 总共创建了 {len(created_terrains)} 个高度图地形") + + # 返回值处理 + if len(created_terrains) == 1: + return created_terrains[0][0] # 单个光源返回NodePath + else: + return [light_np for light_np, _ in created_terrains] # 多个光源返回列表 + + except Exception as e: + print(f"❌ 创建高度图地形过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def createFlatTerrain(self, size=(0.3, 0.3), resolution=129): + """创建平面地形""" + try: + print(f"🔆 开始创建平面地形") + + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + # 获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None + + created_terrains = [] + + try: + parent_item, parent_node = target_parents[0] + # 确保分辨率是2的幂次方加1 (如129, 257, 513等) + valid_resolutions = [17, 33, 65, 129, 257, 513, 1025] + if resolution not in valid_resolutions: + # 找到最接近的有效分辨率 + closest_res = min(valid_resolutions, key=lambda x: abs(x - resolution)) + print(f"警告: 分辨率 {resolution} 不是有效的地形分辨率,使用 {closest_res}") + resolution = closest_res + + # 创建GeoMipTerrain对象 + terrain_name = f"flat_terrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" + terrain = GeoMipTerrain(terrain_name) + + # 创建一个平面高度图,尺寸必须是2^n+1 + height_image = PNMImage(resolution, resolution) + height_image.fill(0.5) # 设置为中等高度 (0.0-1.0范围) + + # 使用正确的方法设置高度图 + terrain.setHeightfield(height_image) + terrain.setBruteforce(True) # 设置LOD + + # 设置地形参数 + terrain.setBlockSize(32) # 设置块大小 + + # 生成地形 + terrain.generate() + + # 获取地形节点 + terrain_node = terrain.getRoot() + + if terrain_node.isEmpty(): + print("错误:无法生成有效的平面地形节点") + return None + + terrain_node.setTag("is_scene_element", "1") + terrain_node.setTag("tree_item_type", "TERRAIN_NODE") + + node_name = f"FlatTerrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" + terrain_node.setName(node_name) + + center_offset = (resolution - 1) / 2 + terrain_node.setPos(-center_offset * size[0], -center_offset * size[1], 0) + + # 将地形添加到场景中 + terrain_node.reparentTo(parent_node) + + # 为地形添加碰撞体 + from panda3d.core import BitMask32 + # 设置地形节点的碰撞掩码 + terrain_node.setCollideMask(BitMask32.bit(2)) # 使用第2位作为地形碰撞掩码 + # 为地形的所有子节点也设置碰撞掩码 + for child in terrain_node.getChildren(): + child.setCollideMask(BitMask32.bit(2)) + + # 添加材质 + self._applyTerrainMaterial(terrain_node) + + # 保存地形信息(包括高度图) + terrain_info = { + 'terrain': terrain, + 'node': terrain_node, + 'heightmap': None, + 'heightfield': height_image, # 保存高度图 + 'scale': (size[0], size[1], 50), + 'name': node_name + } + + self.terrains.append(terrain_info) + + print(f"✅ 为 {parent_item.text(0)} 创建平面地形: {terrain_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(terrain_node, parent_item, "TERRAIN_NODE") + if qt_item: + created_terrains.append((terrain_node, qt_item)) + else: + created_terrains.append((terrain_node, None)) + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建平面地形: {str(e)}") + return None + + # 处理创建结果 + if not created_terrains: + print("❌ 没有成功创建任何平面地形") + return None + + # 选中最后创建的光源 + if created_terrains: + last_light_np, last_qt_item = created_terrains[-1] + if last_qt_item: + tree_widget.setCurrentItem(last_qt_item) + # 更新选择和属性面板 + tree_widget.update_selection_and_properties(last_light_np, last_qt_item) + + print(f"🎉 总共创建了 {len(created_terrains)} 个平面地形") + + # 返回值处理 + if len(created_terrains) == 1: + return created_terrains[0][0] # 单个光源返回NodePath + else: + return [light_np for light_np, _ in created_terrains] # 多个光源返回列表 + + except Exception as e: + print(f"❌ 创建平面地形过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def _applyTerrainMaterial(self, terrain_node): + """为地形应用材质""" + try: + # 创建材质 + material = Material() + material.setAmbient((0.5, 0.5, 0.5, 1.0)) # 环境光反射 + material.setDiffuse((1, 1, 1, 1.0)) # 漫反射(绿色调) + material.setSpecular((0.2, 0.2, 0.2, 1.0)) # 镜面反射 + material.setShininess(5.0) # 高光强度 + + # 应用材质到地形 + terrain_node.setMaterial(material) + + # 启用材质 + terrain_node.setAttrib(ColorAttrib.makeDefault()) + + print("✓ 地形材质已应用") + except Exception as e: + print(f"应用地形材质时出错: {e}") + + def updateTerrain(self): + """更新所有地形的LOD""" + for terrain_info in self.terrains: + terrain = terrain_info['terrain'] + if hasattr(terrain, 'update'): + terrain.update() + + def setTerrainLODThreshold(self, terrain_info, threshold): + """设置地形LOD阈值""" + if terrain_info in self.terrains: + terrain = terrain_info['terrain'] + # GeoMipTerrain可能没有setNearFarThreshold方法,使用其他方式设置LOD + print("注意: GeoMipTerrain可能不支持直接设置LOD阈值") + + def getTerrainHeight(self, terrain_info, x, y): + """获取地形上指定点的高度""" + if terrain_info in self.terrains: + terrain = terrain_info['terrain'] + if hasattr(terrain, 'getElevation'): + return terrain.getElevation(x, y) + return 0 + + # core/terrain_manager.py + + def modifyTerrainHeight(self, terrain_info, x, y, radius, strength, operation="add"): + """修改地形高度""" + if terrain_info not in self.terrains: + return False + + try: + terrain = terrain_info['terrain'] + terrain_node = terrain_info['node'] # 获取地形节点 + + # 直接使用我们保存的高度图数据 + if 'heightfield' not in terrain_info: + print("地形信息中没有保存高度图数据") + return False + + heightmap = terrain_info['heightfield'] + if not heightmap: + print("无法获取地形高度图数据") + return False + + # 获取高度图尺寸 + width = heightmap.getXSize() + height = heightmap.getYSize() + + # 获取地形节点信息 + terrain_pos = terrain_node.getPos() + terrain_scale = terrain_node.getScale() + + # 正确的坐标转换方法 + # 1. 将世界坐标转换为相对于地形中心的坐标 + relative_x = x - terrain_pos.getX() + relative_y = y - terrain_pos.getY() + + # 2. 考虑地形的缩放 + scaled_x = relative_x / terrain_scale.getX() + scaled_y = relative_y / terrain_scale.getY() + + # 3. 转换为高度图坐标 + center_offset = (width - 1) / 2 + center_x = int(scaled_x + center_offset) + center_y = int(scaled_y + center_offset) + + print(f"坐标转换详情:") + print(f" 世界坐标: ({x:.2f}, {y:.2f})") + print(f" 地形位置: ({terrain_pos.getX():.2f}, {terrain_pos.getY():.2f})") + print(f" 地形缩放: ({terrain_scale.getX():.2f}, {terrain_scale.getY():.2f})") + print(f" 相对坐标: ({relative_x:.2f}, {relative_y:.2f})") + print(f" 缩放坐标: ({scaled_x:.2f}, {scaled_y:.2f})") + print(f" 中心偏移: {center_offset}") + print(f" 高度图坐标: ({center_x}, {center_y})") + print(f" 高度图尺寸: {width} x {height}") + + # 检查中心点是否在有效范围内 + if not (0 <= center_x < width and 0 <= center_y < height): + print(f"❌ 中心点坐标超出范围: ({center_x}, {center_y})") + print(f"❌ 有效范围: (0-{width - 1}, 0-{height - 1})") + return False + + # 修改半径范围内的点 + modified = False + + # 使用正确的半径计算(考虑地形缩放) + effective_radius = max(1, int(radius / max(terrain_scale.getX(), terrain_scale.getY()))) + + print(f"有效编辑半径: {effective_radius}") + + # 限制编辑半径不超过地形尺寸的1/4,避免性能问题 + effective_radius = min(effective_radius, width // 4, height // 4) + + for dx in range(-effective_radius, effective_radius + 1): + for dy in range(-effective_radius, effective_radius + 1): + # 检查距离 + distance = (dx * dx + dy * dy) ** 0.5 + if distance <= effective_radius: + # 计算影响权重 + weight = 1.0 - (distance / effective_radius) if effective_radius > 0 else 1.0 + + # 计算目标点坐标 + target_x = center_x + dx + target_y = center_y + dy + + # 检查边界 + if 0 <= target_x < width and 0 <= target_y < height: + # 获取当前高度 + current_height = heightmap.getRed(target_x, target_y) + + # 根据操作类型修改高度 + if operation == "add": + new_height = min(1.0, current_height + strength * weight * 0.01) + elif operation == "subtract": + new_height = max(0.0, current_height - strength * weight * 0.01) + elif operation == "set": + # 平坦化操作,设置为中等高度 + new_height = 0.5 + else: + new_height = current_height + + # 设置新高度 + heightmap.setRed(target_x, target_y, new_height) + heightmap.setGreen(target_x, target_y, new_height) + heightmap.setBlue(target_x, target_y, new_height) + modified = True + + # 如果有修改,则重新生成地形 + if modified: + # 重新设置高度图并生成地形 + terrain.setHeightfield(heightmap) + terrain.generate() + + # 关键修复:重新创建碰撞体,因为在生成地形时会丢失原有碰撞体 + from panda3d.core import BitMask32, CollisionNode, CollisionPlane, Plane, Point3 + # 移除旧的地形碰撞体(如果存在) + for child in terrain_node.getChildren(): + if isinstance(child.node(), CollisionNode) and child.getName().startswith("terrain_collision_"): + child.removeNode() + + # 创建新的地形碰撞体 + # 使用更简单的方法:直接给地形节点添加碰撞体 + terrain_node.setCollideMask(BitMask32.bit(2)) # 确保地形节点本身具有碰撞掩码 + + # 为地形的每个子节点也设置碰撞掩码 + for child in terrain_node.getChildren(): + child.setCollideMask(BitMask32.bit(2)) + + print(f"✓ 地形高度已修改: 位置({x}, {y}), 半径{radius}, 操作{operation}") + print(f"✓ 重新设置了地形碰撞体") + + return modified + + except Exception as e: + print(f"修改地形高度时出错: {e}") + import traceback + traceback.print_exc() + return False + + def deleteTerrain(self, terrain_info): + """删除地形""" + if terrain_info in self.terrains: + # 从场景中移除地形节点 + terrain_node = terrain_info['node'] + if terrain_node and not terrain_node.isEmpty(): + tree_widget = self._get_tree_widget() + if tree_widget: + tree_widget.delete_items(tree_widget.selectedItems()) + # terrain_node.removeNode() + # + # # 从列表中移除 + # self.terrains.remove(terrain_info) + + # # 更新场景树 + # if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): + # self.world.scene_manager.updateSceneTree() + + print(f"✓ 地形已删除: {terrain_info.get('name', 'Unknown')}") + + import gc + gc.collect() + + return True + return False + + def getTerrainByName(self, name): + """根据名称查找地形""" + for terrain_info in self.terrains: + if terrain_info.get('name') == name: + return terrain_info + return None + + def getAllTerrainNames(self): + """获取所有地形名称""" + return [terrain_info.get('name', f'Terrain_{i}') for i, terrain_info in enumerate(self.terrains)] + + def setTerrainColor(self, terrain_info, color): + """设置地形颜色""" + try: + if terrain_info in self.terrains: + terrain_node = terrain_info['node'] + if terrain_node: + # 创建新材质 + material = Material() + material.setAmbient((color[0] * 0.5, color[1] * 0.5, color[2] * 0.5, 1.0)) + material.setDiffuse((color[0], color[1], color[2], 1.0)) + material.setSpecular((0.2, 0.2, 0.2, 1.0)) + material.setShininess(5.0) + + # 应用材质 + terrain_node.setMaterial(material) + print(f"✓ 地形颜色已更新: {color}") + return True + return False + except Exception as e: + print(f"设置地形颜色时出错: {e}") + return False + + def setTerrainTexture(self, terrain_info, texture_path): + """为地形设置纹理""" + try: + if terrain_info in self.terrains: + terrain_node = terrain_info['node'] + if terrain_node and os.path.exists(texture_path): + texture_path = util.normalize_model_path(texture_path) + # 加载纹理 + texture = self.world.loader.loadTexture(texture_path) + if texture: + # 应用纹理 + terrain_node.setTexture(texture, 1) + print(f"✓ 地形纹理已应用: {texture_path}") + return True + else: + print(f"✗ 无法加载纹理: {texture_path}") + else: + print(f"✗ 纹理文件不存在: {texture_path}") + return False + except Exception as e: + print(f"应用地形纹理时出错: {e}") + return False + + def saveTerrainData(self, terrain_info, filename): + """保存地形数据到文件""" + try: + terrain_node = terrain_info['node'] + heightfield = terrain_info['heightfield'] + + # 保存高度图到文件 + if heightfield: + # 创建保存路径 + terrain_dir = os.path.join(os.path.dirname(filename), "terrains") + if not os.path.exists(terrain_dir): + os.makedirs(terrain_dir) + + # 生成唯一的地形文件名 + terrain_filename = f"terrain_{terrain_info.get('name', 'unnamed')}_{int(time.time())}.png" + terrain_path = os.path.join(terrain_dir, terrain_filename) + + # 保存高度图 + heightfield.write(Filename.fromOsSpecific(terrain_path)) + + # 保存地形信息到标签 + terrain_node.setTag("terrain_heightmap_path", terrain_path) + terrain_node.setTag("terrain_scale", str(terrain_info['scale'])) + + print(f"✓ 地形数据已保存: {terrain_path}") + return terrain_path + return None + except Exception as e: + print(f"保存地形数据时出错: {e}") + return None + + def _recreateTerrain(self, terrain_node): + """重新创建地形""" + try: + print(f"重新创建地形: {terrain_node.getName()}") + + # 获取保存的地形信息 + heightmap_path = terrain_node.getTag("terrain_heightmap_path") if terrain_node.hasTag( + "terrain_heightmap_path") else None + scale_str = terrain_node.getTag("terrain_scale") if terrain_node.hasTag("terrain_scale") else "(1, 1, 1)" + + # 解析缩放信息 + try: + scale_str = scale_str.strip("()") + scale_values = [float(x.strip()) for x in scale_str.split(",")] + scale = (scale_values[0], scale_values[1], scale_values[2]) if len(scale_values) >= 3 else (1, 1, 1) + except: + scale = (1, 1, 1) + + # 恢复位置信息 + if terrain_node.hasTag("transform_pos"): + try: + pos_str = terrain_node.getTag("transform_pos") + pos_str = pos_str.replace('LVecBase3f', '').replace('LPoint3f', '').strip('()') + pos_values = [float(x.strip()) for x in pos_str.split(',')] + if len(pos_values) >= 3: + terrain_node.setPos(pos_values[0], pos_values[1], pos_values[2]) + except Exception as e: + print(f"恢复地形位置失败: {e}") + + # 如果有高度图路径,重新创建地形 + if heightmap_path and os.path.exists(heightmap_path): + # 使用现有方法重新创建地形 + new_terrain = self.createTerrainFromHeightMap(heightmap_path, scale) + if new_terrain: + # 删除旧的地形节点 + if not terrain_node.isEmpty(): + terrain_node.removeNode() + return new_terrain + + # 如果没有高度图或创建失败,创建一个平面地形作为替代 + print("使用平面地形作为替代") + new_terrain = self.createFlatTerrain(size=(scale[0], scale[1]), resolution=129) + if new_terrain: + # 删除旧的地形节点 + if not terrain_node.isEmpty(): + terrain_node.removeNode() + return new_terrain + + return terrain_node + except Exception as e: + print(f"重新创建地形失败: {e}") + return terrain_node + diff --git a/core/tool_manager.py b/core/tool_manager.py index a0906215..87ffe17c 100644 --- a/core/tool_manager.py +++ b/core/tool_manager.py @@ -1,153 +1,153 @@ -class ToolManager: - """工具管理器 - 管理编辑器工具状态""" - - def __init__(self, world): - """初始化工具管理器""" - self.world = world - self.currentTool = "选择" # 默认工具为选择工具 - print(f"当前工具: {self.currentTool}") - - def setCurrentTool(self, tool): - """设置当前工具""" - self.currentTool = tool - - print(f"\n=== 工具切换 ===") - print(f"当前工具: {tool}") - print(f"选中节点: {self.world.selection.selectedNode.getName() if self.world.selection.selectedNode else '无'}") - - # 根据工具类型启用对应的方法 - if tool == "选择": - print(f"当前工具为:{tool}") - #self._enableSelectionTool() - elif tool == "移动": - print(f"当前工具为:{tool}") - #self._enableMoveTool() - elif tool == "旋转": - print(f"当前工具为:{tool}") - #self._enableRotateTool() - elif tool == "缩放": - print(f"当前工具为:{tool}") - #self._enableScaleTool() - elif tool == "光照编辑": - print(f"当前工具为:{tool}") - self.launch_day_time_editor() - elif tool == "图形编辑": - print(f"当前工具为:{tool}") - self.launch_plugin_configurator() - - # 坐标轴现在始终跟随选中状态,不再依赖工具类型 - - def getCurrentTool(self): - """获取当前工具""" - return self.currentTool - - def isSelectionTool(self): - """检查当前是否是选择工具""" - return self.currentTool == "选择" - - def isMoveTool(self): - """检查当前是否是移动工具""" - return self.currentTool == "移动" - - def isRotateTool(self): - """检查当前是否是旋转工具""" - return self.currentTool == "旋转" - - def isScaleTool(self): - """检查当前是否是缩放工具""" - return self.currentTool == "缩放" - - def isDayTimeEditorTool(self): - """光照""" - return self.currentTool=="光照编辑" - def isPlugincongiguratorTool(self): - """检查当前是否是图形编辑工具""" - return self.currentTool == "图形编辑" - - def launch_day_time_editor(self): - - # 检查是否已经启动 - if hasattr(self, '_day_time_editor_process') and self._day_time_editor_process: - if self._day_time_editor_process.poll() is None: # 进程仍在运行 - print("Day Time Editor 已经在运行") - return True - import subprocess - import os - import sys - - try: - if not hasattr(self.world,'render_pipeline') or not self.world.render_pipeline: - print("错误:renderpipeline未初始化") - return False - - base_path = self.world.render_pipeline.mount_mgr.base_path - editor_path = os.path.join(base_path,"toolkit/day_time_editor/main.py") - - if not os.path.exists(editor_path): - print("错误文件不存在") - return False - - self._day_time_editor_process = subprocess.Popen([sys.executable, editor_path]) - print("Day Time Editor 已启动") - return True - except Exception as e: - print(f"启动 time editor失败") - - def openSunLightingTool(self): - """打开太阳光照系统工具的便捷方法""" - return self.launch_day_time_editor() - - def launch_plugin_configurator(self): - """启动插件配置器""" - import subprocess - import os - import sys - - try: - # 检查是否已经有进程在运行 - if hasattr(self, '_plugin_configurator_process') and self._plugin_configurator_process: - # 检查进程是否仍在运行 - if self._plugin_configurator_process.poll() is None: - print("⚠️ 插件配置器已经在运行中,无需重复启动") - # 尝试将窗口置于前台(在某些系统上可能有效) - try: - # 在Windows上可以尝试激活窗口 - if os.name == 'nt': - import win32gui - import win32con - def enum_windows_callback(hwnd, pid): - if win32gui.GetWindowThreadProcessId(hwnd)[1] == pid: - win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) - win32gui.SetForegroundWindow(hwnd) - return False - return True - win32gui.EnumWindows(enum_windows_callback, self._plugin_configurator_process.pid) - except ImportError: - pass # win32gui不可用,跳过窗口激活 - except Exception as e: - print(f"尝试激活窗口失败: {e}") - return True - else: - # 进程已结束,清理引用 - print("🔄 检测到之前的插件配置器进程已结束,准备启动新进程") - self._plugin_configurator_process = None - - if not self.world.render_pipeline: - print("❌ 错误:RenderPipeline未初始化") - return False - - base_path = self.world.render_pipeline.mount_mgr.base_path - editor_path = os.path.join(base_path, "toolkit/plugin_configurator/main.py") - - if not os.path.exists(editor_path): - print(f"❌ 错误:插件配置器文件不存在: {editor_path}") - return False - - print("🚀 启动插件配置器...") - self._plugin_configurator_process = subprocess.Popen([sys.executable, editor_path]) - print("✅ 插件配置器已启动") - return True - - except Exception as e: - print(f"❌ 启动插件配置器失败: {e}") - return False +class ToolManager: + """工具管理器 - 管理编辑器工具状态""" + + def __init__(self, world): + """初始化工具管理器""" + self.world = world + self.currentTool = "选择" # 默认工具为选择工具 + print(f"当前工具: {self.currentTool}") + + def setCurrentTool(self, tool): + """设置当前工具""" + self.currentTool = tool + + print(f"\n=== 工具切换 ===") + print(f"当前工具: {tool}") + print(f"选中节点: {self.world.selection.selectedNode.getName() if self.world.selection.selectedNode else '无'}") + + # 根据工具类型启用对应的方法 + if tool == "选择": + print(f"当前工具为:{tool}") + #self._enableSelectionTool() + elif tool == "移动": + print(f"当前工具为:{tool}") + #self._enableMoveTool() + elif tool == "旋转": + print(f"当前工具为:{tool}") + #self._enableRotateTool() + elif tool == "缩放": + print(f"当前工具为:{tool}") + #self._enableScaleTool() + elif tool == "光照编辑": + print(f"当前工具为:{tool}") + self.launch_day_time_editor() + elif tool == "图形编辑": + print(f"当前工具为:{tool}") + self.launch_plugin_configurator() + + # 坐标轴现在始终跟随选中状态,不再依赖工具类型 + + def getCurrentTool(self): + """获取当前工具""" + return self.currentTool + + def isSelectionTool(self): + """检查当前是否是选择工具""" + return self.currentTool == "选择" + + def isMoveTool(self): + """检查当前是否是移动工具""" + return self.currentTool == "移动" + + def isRotateTool(self): + """检查当前是否是旋转工具""" + return self.currentTool == "旋转" + + def isScaleTool(self): + """检查当前是否是缩放工具""" + return self.currentTool == "缩放" + + def isDayTimeEditorTool(self): + """光照""" + return self.currentTool=="光照编辑" + def isPlugincongiguratorTool(self): + """检查当前是否是图形编辑工具""" + return self.currentTool == "图形编辑" + + def launch_day_time_editor(self): + + # 检查是否已经启动 + if hasattr(self, '_day_time_editor_process') and self._day_time_editor_process: + if self._day_time_editor_process.poll() is None: # 进程仍在运行 + print("Day Time Editor 已经在运行") + return True + import subprocess + import os + import sys + + try: + if not hasattr(self.world,'render_pipeline') or not self.world.render_pipeline: + print("错误:renderpipeline未初始化") + return False + + base_path = self.world.render_pipeline.mount_mgr.base_path + editor_path = os.path.join(base_path,"toolkit/day_time_editor/main.py") + + if not os.path.exists(editor_path): + print("错误文件不存在") + return False + + self._day_time_editor_process = subprocess.Popen([sys.executable, editor_path]) + print("Day Time Editor 已启动") + return True + except Exception as e: + print(f"启动 time editor失败") + + def openSunLightingTool(self): + """打开太阳光照系统工具的便捷方法""" + return self.launch_day_time_editor() + + def launch_plugin_configurator(self): + """启动插件配置器""" + import subprocess + import os + import sys + + try: + # 检查是否已经有进程在运行 + if hasattr(self, '_plugin_configurator_process') and self._plugin_configurator_process: + # 检查进程是否仍在运行 + if self._plugin_configurator_process.poll() is None: + print("⚠️ 插件配置器已经在运行中,无需重复启动") + # 尝试将窗口置于前台(在某些系统上可能有效) + try: + # 在Windows上可以尝试激活窗口 + if os.name == 'nt': + import win32gui + import win32con + def enum_windows_callback(hwnd, pid): + if win32gui.GetWindowThreadProcessId(hwnd)[1] == pid: + win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) + win32gui.SetForegroundWindow(hwnd) + return False + return True + win32gui.EnumWindows(enum_windows_callback, self._plugin_configurator_process.pid) + except ImportError: + pass # win32gui不可用,跳过窗口激活 + except Exception as e: + print(f"尝试激活窗口失败: {e}") + return True + else: + # 进程已结束,清理引用 + print("🔄 检测到之前的插件配置器进程已结束,准备启动新进程") + self._plugin_configurator_process = None + + if not self.world.render_pipeline: + print("❌ 错误:RenderPipeline未初始化") + return False + + base_path = self.world.render_pipeline.mount_mgr.base_path + editor_path = os.path.join(base_path, "toolkit/plugin_configurator/main.py") + + if not os.path.exists(editor_path): + print(f"❌ 错误:插件配置器文件不存在: {editor_path}") + return False + + print("🚀 启动插件配置器...") + self._plugin_configurator_process = subprocess.Popen([sys.executable, editor_path]) + print("✅ 插件配置器已启动") + return True + + except Exception as e: + print(f"❌ 启动插件配置器失败: {e}") + return False diff --git a/core/vr/README.md b/core/vr/README.md index b7ba97c7..6bb4d762 100644 --- a/core/vr/README.md +++ b/core/vr/README.md @@ -1,163 +1,163 @@ -# VR模块重构说明 - -## 📋 重构概述 - -本次重构将原本分散在`core/`目录下的12个VR相关文件整理到`core/vr/`模块化目录结构中,提升了代码的可维护性和可扩展性。 - -## 🎯 重构目标 - -1. **符合编码规范**:每个目录不超过8个文件 -2. **职责清晰**:按功能模块组织,单一职责原则 -3. **易于维护**:清晰的分层架构,降低耦合度 -4. **向后兼容**:现有代码无需修改即可工作 - -## 📁 新目录结构 - -``` -core/vr/ -├── __init__.py # 主模块接口(重新导出VRManager) -├── rendering/ # 渲染子系统 -│ ├── __init__.py -│ └── stages.py # VR渲染stages (867行) -├── tracking/ # 跟踪子系统 -│ ├── __init__.py -│ └── controllers.py # 控制器类 (482行) -├── interaction/ # 交互子系统 -│ ├── __init__.py -│ ├── actions.py # 动作系统 (582行) -│ ├── joystick.py # 摇杆系统 (700行) -│ ├── teleport.py # 传送系统 (421行) -│ └── grab.py # 抓取交互 (431行) -├── visualization/ # 可视化子系统 -│ ├── __init__.py -│ ├── controllers.py # 控制器可视化 (780行) -│ └── effects.py # VR特效管理 (226行) -├── config/ # 配置子系统 -│ ├── __init__.py -│ ├── vr_config.py # VR配置 (273行) -│ ├── joystick_config.py # 摇杆配置 (278行) -│ └── shadow_stage.py # 阴影stage配置 (173行) -├── performance/ # 性能优化子系统(预留) -│ └── __init__.py -└── testing/ # 测试调试子系统(预留) - └── __init__.py -``` - -## 📊 文件统计 - -| 目录 | 文件数 | 状态 | -|------|--------|------| -| config | 4 | ✅ 符合规范 (<8) | -| interaction | 5 | ✅ 符合规范 (<8) | -| rendering | 2 | ✅ 符合规范 (<8) | -| tracking | 2 | ✅ 符合规范 (<8) | -| visualization | 3 | ✅ 符合规范 (<8) | -| performance | 1 | ✅ 符合规范 (<8) | -| testing | 1 | ✅ 符合规范 (<8) | - -**总计**: 所有目录都符合"不超过8个文件"的规范 ✅ - -## 🔄 文件迁移映射 - -| 原文件 | 新位置 | 行数 | -|--------|--------|------| -| `vr_stages.py` | `rendering/stages.py` | 867 | -| `vr_visualization.py` | `visualization/controllers.py` | 780 | -| `vr_effects_manager.py` | `visualization/effects.py` | 226 | -| `vr_controller.py` | `tracking/controllers.py` | 482 | -| `vr_actions.py` | `interaction/actions.py` | 582 | -| `vr_joystick.py` | `interaction/joystick.py` | 700 | -| `vr_teleport.py` | `interaction/teleport.py` | 421 | -| `vr_interaction.py` | `interaction/grab.py` | 431 | -| `vr_config.py` | `config/vr_config.py` | 273 | -| `vr_joystick_config.py` | `config/joystick_config.py` | 278 | -| `vr_shadow_stage.py` | `config/shadow_stage.py` | 173 | - -## 🔧 导入路径更新 - -### ✅ 所有代码已更新为新路径 - -所有项目代码已完全迁移到新的模块化导入路径: - -```python -# 主接口 -from core.vr import VRManager, VRRenderMode - -# 子模块(直接访问) -from core.vr.tracking.controllers import LeftController, RightController -from core.vr.interaction.actions import VRActionManager -from core.vr.interaction.joystick import VRJoystickManager -from core.vr.interaction.teleport import VRTeleportSystem -from core.vr.interaction.grab import VRInteractionManager -from core.vr.visualization.controllers import VRControllerVisualizer -from core.vr.visualization.effects import VREffectsManager -from core.vr.rendering.stages import VRPipelineController -from core.vr.config.vr_config import VRConfigManager -``` - -**已更新的文件**: -- ✅ `main.py` - VRManager导入 -- ✅ `ui/main_window.py` - VRRenderMode导入 -- ✅ `core/vr_manager.py` - 所有内部导入 -- ✅ `core/vr/config/vr_config.py` - VRRenderMode导入 -- ✅ `core/vr/tracking/controllers.py` - VRControllerVisualizer导入 -- ✅ `core/vr/visualization/controllers.py` - VRRenderMode导入 - -## ✅ 完成的工作 - -1. ✅ 创建了完整的模块化目录结构 -2. ✅ 移动了11个独立VR文件到对应子目录 -3. ✅ 更新了所有内部导入路径 -4. ✅ 创建了兼容性层(`core/vr/__init__.py`) -5. ✅ 保持了向后兼容性 - -## 🚧 待完成的工作(未来) - -### vr_manager.py重构(优先级:低) - -`core/vr_manager.py`目前仍有**4736行代码,138个方法**,严重超标。建议未来逐步拆分为: - -1. **rendering/** 子系统 - - `buffers.py` - 缓冲区和纹理管理 (~300行) - - `cameras.py` - 相机设置和更新 (~250行) - - `pipeline.py` - RenderPipeline集成 (~300行) - - `compositor.py` - 合成和OpenVR提交 (~200行) - -2. **tracking/** 子系统 - - `poses.py` - 姿态跟踪和更新 (~400行) - - `devices.py` - 设备检测管理 (~200行) - - `controller_wrapper.py` - 控制器输入包装 (~150行) - -3. **performance/** 子系统 - - `monitoring.py` - 性能监控 (~400行) - - `optimization.py` - 优化系统 (~300行) - - `diagnostics.py` - 诊断工具 (~250行) - -4. **testing/** 子系统 - - `test_mode.py` - VR测试模式 (~400行) - - `debug.py` - 调试工具 (~200行) - -5. **manager.py** - VR管理器核心 (~400行) - - 保留核心初始化和生命周期管理 - - 通过组合模式委托给各子系统 - -## 📝 注意事项 - -1. **旧文件保留**:原`core/vr_*.py`文件暂时保留,待测试无误后可删除 -2. **导入兼容性**:所有旧的导入路径仍然有效 -3. **测试验证**:建议运行VR功能测试确认重构后代码正常工作 -4. **渐进重构**:vr_manager.py的拆分可以后续逐步进行,不影响当前功能 - -## 🎉 重构成果 - -- ✅ **目录组织**:从12个文件混乱分布 → 7个清晰子系统 -- ✅ **规范遵守**:所有目录文件数 ≤ 8个 -- ✅ **职责分离**:每个子系统职责明确,易于理解和维护 -- ✅ **向后兼容**:现有代码无需修改即可工作 -- ✅ **扩展性好**:新功能可轻松添加到对应子系统 - ---- - -**重构日期**: 2025-10-11 -**重构人**: Claude Code -**版本**: v2.0.0 +# VR模块重构说明 + +## 📋 重构概述 + +本次重构将原本分散在`core/`目录下的12个VR相关文件整理到`core/vr/`模块化目录结构中,提升了代码的可维护性和可扩展性。 + +## 🎯 重构目标 + +1. **符合编码规范**:每个目录不超过8个文件 +2. **职责清晰**:按功能模块组织,单一职责原则 +3. **易于维护**:清晰的分层架构,降低耦合度 +4. **向后兼容**:现有代码无需修改即可工作 + +## 📁 新目录结构 + +``` +core/vr/ +├── __init__.py # 主模块接口(重新导出VRManager) +├── rendering/ # 渲染子系统 +│ ├── __init__.py +│ └── stages.py # VR渲染stages (867行) +├── tracking/ # 跟踪子系统 +│ ├── __init__.py +│ └── controllers.py # 控制器类 (482行) +├── interaction/ # 交互子系统 +│ ├── __init__.py +│ ├── actions.py # 动作系统 (582行) +│ ├── joystick.py # 摇杆系统 (700行) +│ ├── teleport.py # 传送系统 (421行) +│ └── grab.py # 抓取交互 (431行) +├── visualization/ # 可视化子系统 +│ ├── __init__.py +│ ├── controllers.py # 控制器可视化 (780行) +│ └── effects.py # VR特效管理 (226行) +├── config/ # 配置子系统 +│ ├── __init__.py +│ ├── vr_config.py # VR配置 (273行) +│ ├── joystick_config.py # 摇杆配置 (278行) +│ └── shadow_stage.py # 阴影stage配置 (173行) +├── performance/ # 性能优化子系统(预留) +│ └── __init__.py +└── testing/ # 测试调试子系统(预留) + └── __init__.py +``` + +## 📊 文件统计 + +| 目录 | 文件数 | 状态 | +|------|--------|------| +| config | 4 | ✅ 符合规范 (<8) | +| interaction | 5 | ✅ 符合规范 (<8) | +| rendering | 2 | ✅ 符合规范 (<8) | +| tracking | 2 | ✅ 符合规范 (<8) | +| visualization | 3 | ✅ 符合规范 (<8) | +| performance | 1 | ✅ 符合规范 (<8) | +| testing | 1 | ✅ 符合规范 (<8) | + +**总计**: 所有目录都符合"不超过8个文件"的规范 ✅ + +## 🔄 文件迁移映射 + +| 原文件 | 新位置 | 行数 | +|--------|--------|------| +| `vr_stages.py` | `rendering/stages.py` | 867 | +| `vr_visualization.py` | `visualization/controllers.py` | 780 | +| `vr_effects_manager.py` | `visualization/effects.py` | 226 | +| `vr_controller.py` | `tracking/controllers.py` | 482 | +| `vr_actions.py` | `interaction/actions.py` | 582 | +| `vr_joystick.py` | `interaction/joystick.py` | 700 | +| `vr_teleport.py` | `interaction/teleport.py` | 421 | +| `vr_interaction.py` | `interaction/grab.py` | 431 | +| `vr_config.py` | `config/vr_config.py` | 273 | +| `vr_joystick_config.py` | `config/joystick_config.py` | 278 | +| `vr_shadow_stage.py` | `config/shadow_stage.py` | 173 | + +## 🔧 导入路径更新 + +### ✅ 所有代码已更新为新路径 + +所有项目代码已完全迁移到新的模块化导入路径: + +```python +# 主接口 +from core.vr import VRManager, VRRenderMode + +# 子模块(直接访问) +from core.vr.tracking.controllers import LeftController, RightController +from core.vr.interaction.actions import VRActionManager +from core.vr.interaction.joystick import VRJoystickManager +from core.vr.interaction.teleport import VRTeleportSystem +from core.vr.interaction.grab import VRInteractionManager +from core.vr.visualization.controllers import VRControllerVisualizer +from core.vr.visualization.effects import VREffectsManager +from core.vr.rendering.stages import VRPipelineController +from core.vr.config.vr_config import VRConfigManager +``` + +**已更新的文件**: +- ✅ `main.py` - VRManager导入 +- ✅ `ui/main_window.py` - VRRenderMode导入 +- ✅ `core/vr_manager.py` - 所有内部导入 +- ✅ `core/vr/config/vr_config.py` - VRRenderMode导入 +- ✅ `core/vr/tracking/controllers.py` - VRControllerVisualizer导入 +- ✅ `core/vr/visualization/controllers.py` - VRRenderMode导入 + +## ✅ 完成的工作 + +1. ✅ 创建了完整的模块化目录结构 +2. ✅ 移动了11个独立VR文件到对应子目录 +3. ✅ 更新了所有内部导入路径 +4. ✅ 创建了兼容性层(`core/vr/__init__.py`) +5. ✅ 保持了向后兼容性 + +## 🚧 待完成的工作(未来) + +### vr_manager.py重构(优先级:低) + +`core/vr_manager.py`目前仍有**4736行代码,138个方法**,严重超标。建议未来逐步拆分为: + +1. **rendering/** 子系统 + - `buffers.py` - 缓冲区和纹理管理 (~300行) + - `cameras.py` - 相机设置和更新 (~250行) + - `pipeline.py` - RenderPipeline集成 (~300行) + - `compositor.py` - 合成和OpenVR提交 (~200行) + +2. **tracking/** 子系统 + - `poses.py` - 姿态跟踪和更新 (~400行) + - `devices.py` - 设备检测管理 (~200行) + - `controller_wrapper.py` - 控制器输入包装 (~150行) + +3. **performance/** 子系统 + - `monitoring.py` - 性能监控 (~400行) + - `optimization.py` - 优化系统 (~300行) + - `diagnostics.py` - 诊断工具 (~250行) + +4. **testing/** 子系统 + - `test_mode.py` - VR测试模式 (~400行) + - `debug.py` - 调试工具 (~200行) + +5. **manager.py** - VR管理器核心 (~400行) + - 保留核心初始化和生命周期管理 + - 通过组合模式委托给各子系统 + +## 📝 注意事项 + +1. **旧文件保留**:原`core/vr_*.py`文件暂时保留,待测试无误后可删除 +2. **导入兼容性**:所有旧的导入路径仍然有效 +3. **测试验证**:建议运行VR功能测试确认重构后代码正常工作 +4. **渐进重构**:vr_manager.py的拆分可以后续逐步进行,不影响当前功能 + +## 🎉 重构成果 + +- ✅ **目录组织**:从12个文件混乱分布 → 7个清晰子系统 +- ✅ **规范遵守**:所有目录文件数 ≤ 8个 +- ✅ **职责分离**:每个子系统职责明确,易于理解和维护 +- ✅ **向后兼容**:现有代码无需修改即可工作 +- ✅ **扩展性好**:新功能可轻松添加到对应子系统 + +--- + +**重构日期**: 2025-10-11 +**重构人**: Claude Code +**版本**: v2.0.0 diff --git a/core/vr/__init__.py b/core/vr/__init__.py index 4236700b..1851e87a 100644 --- a/core/vr/__init__.py +++ b/core/vr/__init__.py @@ -1,37 +1,37 @@ -""" -VR模块 - 虚拟现实功能 - -这个模块提供完整的VR功能支持,包括: -- VR设备初始化和管理 -- 渲染系统(双眼渲染、RenderPipeline集成) -- 跟踪系统(HMD和控制器姿态跟踪) -- 交互系统(控制器输入、摇杆、传送、抓取) -- 可视化系统(控制器模型、特效) -- 性能优化和监控 -- 测试和调试工具 -- 配置管理 - -主要接口: - VRManager: VR管理器主类 - VRRenderMode: VR渲染模式枚举 - -示例: - from core.vr import VRManager - vr_manager = VRManager(world) - vr_manager.initialize_vr() - vr_manager.enable_vr() -""" - -# 为了向后兼容,从core.vr_manager重新导出 -# vr_manager.py目前还包含VRManager的完整实现 -# 未来可以逐步将其拆分到子模块中 - -# 使用相对导入从父目录的vr_manager模块导入 -try: - from ..vr_manager import VRManager, VRRenderMode - __all__ = ['VRManager', 'VRRenderMode'] -except ImportError as e: - print(f"警告:无法从vr_manager导入: {e}") - __all__ = [] - -__version__ = '2.0.0' +""" +VR模块 - 虚拟现实功能 + +这个模块提供完整的VR功能支持,包括: +- VR设备初始化和管理 +- 渲染系统(双眼渲染、RenderPipeline集成) +- 跟踪系统(HMD和控制器姿态跟踪) +- 交互系统(控制器输入、摇杆、传送、抓取) +- 可视化系统(控制器模型、特效) +- 性能优化和监控 +- 测试和调试工具 +- 配置管理 + +主要接口: + VRManager: VR管理器主类 + VRRenderMode: VR渲染模式枚举 + +示例: + from core.vr import VRManager + vr_manager = VRManager(world) + vr_manager.initialize_vr() + vr_manager.enable_vr() +""" + +# 为了向后兼容,从core.vr_manager重新导出 +# vr_manager.py目前还包含VRManager的完整实现 +# 未来可以逐步将其拆分到子模块中 + +# 使用相对导入从父目录的vr_manager模块导入 +try: + from ..vr_manager import VRManager, VRRenderMode + __all__ = ['VRManager', 'VRRenderMode'] +except ImportError as e: + print(f"警告:无法从vr_manager导入: {e}") + __all__ = [] + +__version__ = '2.0.0' diff --git a/core/vr/config/__init__.py b/core/vr/config/__init__.py index 5cd5db14..f0e61ffa 100644 --- a/core/vr/config/__init__.py +++ b/core/vr/config/__init__.py @@ -1,10 +1,10 @@ -""" -VR配置子系统 - -负责VR配置管理: -- VR基础配置 -- 摇杆配置 -- 阴影stage配置 -""" - -__all__ = [] +""" +VR配置子系统 + +负责VR配置管理: +- VR基础配置 +- 摇杆配置 +- 阴影stage配置 +""" + +__all__ = [] diff --git a/core/vr/config/joystick_config.py b/core/vr/config/joystick_config.py index b8c24087..8ad52552 100644 --- a/core/vr/config/joystick_config.py +++ b/core/vr/config/joystick_config.py @@ -1,279 +1,279 @@ -""" -VR摇杆配置模块 - -提供摇杆交互的配置选项和预设: -- 不同的转向模式和灵敏度设置 -- 传送参数调整 -- 用户体验优化选项 -""" - -from enum import Enum - - -class TurnMode(Enum): - """转向模式枚举""" - SMOOTH = "smooth" # 平滑转向 - SNAP = "snap" # 分段转向 - - -class JoystickProfile(Enum): - """摇杆配置预设""" - COMFORTABLE = "comfortable" # 舒适模式 - 低灵敏度,平滑转向 - STANDARD = "standard" # 标准模式 - 中等灵敏度,分段转向 - GAMING = "gaming" # 游戏模式 - 高灵敏度,快速响应 - ACCESSIBLE = "accessible" # 无障碍模式 - 更大死区,更高阈值 - - -class VRJoystickConfig: - """VR摇杆配置类""" - - def __init__(self): - """初始化默认配置""" - # 基础参数 - self.deadzone = 0.15 # 摇杆死区 (0-1) - self.turn_threshold = 0.3 # 转向激活阈值 - self.teleport_threshold = 0.5 # 传送激活阈值 - - # 转向设置 - self.turn_mode = TurnMode.SNAP # 转向模式 - self.turn_sensitivity = 250.0 # 转向灵敏度(度/秒)- 增加速度 - self.snap_turn_angle = 30.0 # 分段转向角度(度) - self.snap_turn_cooldown = 0.3 # 分段转向间隔(秒) - - # 传送设置 - self.teleport_range = 20.0 # 最大传送距离 - self.teleport_arc_resolution = 50 # 抛物线精度 - self.teleport_initial_velocity = 10.0 # 传送初始速度 - self.min_teleport_distance = 1.0 # 最小传送距离 - self.player_height_offset = 1.7 # 玩家站立高度偏移(米) - - # 反馈设置 - self.haptic_feedback_enabled = True # 是否启用震动反馈 - self.teleport_start_haptic = (0.002, 0.3) # 传送开始震动 (时长, 强度) - self.teleport_success_haptic = (0.005, 0.8) # 传送成功震动 - self.teleport_fail_haptic = (0.001, 0.2) # 传送失败震动 - - # 可视化设置 - self.show_teleport_arc = True # 显示传送抛物线 - self.show_teleport_target = True # 显示传送目标标记 - self.arc_valid_color = (0.2, 0.9, 0.2, 0.8) # 有效抛物线颜色 - self.arc_invalid_color = (0.9, 0.2, 0.2, 0.8) # 无效抛物线颜色 - - def apply_profile(self, profile: JoystickProfile): - """应用预设配置 - - Args: - profile: 配置预设 - """ - if profile == JoystickProfile.COMFORTABLE: - self._apply_comfortable_profile() - elif profile == JoystickProfile.STANDARD: - self._apply_standard_profile() - elif profile == JoystickProfile.GAMING: - self._apply_gaming_profile() - elif profile == JoystickProfile.ACCESSIBLE: - self._apply_accessible_profile() - - def _apply_comfortable_profile(self): - """舒适模式 - 适合长时间使用""" - self.deadzone = 0.2 - self.turn_threshold = 0.4 - self.teleport_threshold = 0.6 - self.turn_mode = TurnMode.SMOOTH - self.turn_sensitivity = 100.0 # 适中的转向速度 - self.snap_turn_cooldown = 0.4 - print("✓ 已应用舒适模式配置") - - def _apply_standard_profile(self): - """标准模式 - 平衡的体验""" - self.deadzone = 0.15 - self.turn_threshold = 0.3 - self.teleport_threshold = 0.5 - self.turn_mode = TurnMode.SNAP - self.turn_sensitivity = 150.0 # 更快的转向速度 - self.snap_turn_angle = 30.0 - self.snap_turn_cooldown = 0.3 - print("✓ 已应用标准模式配置") - - def _apply_gaming_profile(self): - """游戏模式 - 快速响应""" - self.deadzone = 0.1 - self.turn_threshold = 0.2 - self.teleport_threshold = 0.4 - self.turn_mode = TurnMode.SMOOTH - self.turn_sensitivity = 200.0 # 高速转向 - self.snap_turn_cooldown = 0.2 - print("✓ 已应用游戏模式配置") - - def _apply_accessible_profile(self): - """无障碍模式 - 更容易控制""" - self.deadzone = 0.25 - self.turn_threshold = 0.5 - self.teleport_threshold = 0.7 - self.turn_mode = TurnMode.SNAP - self.turn_sensitivity = 80.0 # 较慢但可控的转向 - self.snap_turn_angle = 45.0 - self.snap_turn_cooldown = 0.5 - print("✓ 已应用无障碍模式配置") - - def set_turn_mode(self, mode: TurnMode): - """设置转向模式 - - Args: - mode: 转向模式 - """ - self.turn_mode = mode - print(f"✓ 转向模式设置为: {mode.value}") - - def set_turn_sensitivity(self, sensitivity: float): - """设置转向灵敏度 - - Args: - sensitivity: 转向灵敏度(度/秒) - """ - self.turn_sensitivity = max(10.0, min(180.0, sensitivity)) - print(f"✓ 转向灵敏度设置为: {self.turn_sensitivity}度/秒") - - def set_deadzone(self, deadzone: float): - """设置摇杆死区 - - Args: - deadzone: 死区大小 (0-1) - """ - self.deadzone = max(0.0, min(0.5, deadzone)) - print(f"✓ 摇杆死区设置为: {self.deadzone}") - - def set_teleport_range(self, range_meters: float): - """设置传送范围 - - Args: - range_meters: 传送范围(米) - """ - self.teleport_range = max(5.0, min(50.0, range_meters)) - print(f"✓ 传送范围设置为: {self.teleport_range}米") - - def set_player_height(self, height_meters: float): - """设置玩家站立高度偏移 - - Args: - height_meters: 站立高度偏移(米),推荐范围1.5-2.0米 - """ - self.player_height_offset = max(1.0, min(2.5, height_meters)) - print(f"✓ 玩家站立高度偏移设置为: {self.player_height_offset}米") - - def enable_haptic_feedback(self, enabled: bool): - """启用或禁用震动反馈 - - Args: - enabled: 是否启用 - """ - self.haptic_feedback_enabled = enabled - print(f"✓ 震动反馈: {'启用' if enabled else '禁用'}") - - def get_config_dict(self): - """获取配置字典 - - Returns: - dict: 配置参数字典 - """ - return { - 'deadzone': self.deadzone, - 'turn_threshold': self.turn_threshold, - 'teleport_threshold': self.teleport_threshold, - 'turn_mode': self.turn_mode.value, - 'turn_sensitivity': self.turn_sensitivity, - 'snap_turn_angle': self.snap_turn_angle, - 'snap_turn_cooldown': self.snap_turn_cooldown, - 'teleport_range': self.teleport_range, - 'haptic_feedback_enabled': self.haptic_feedback_enabled - } - - def apply_to_joystick_manager(self, joystick_manager): - """将配置应用到摇杆管理器 - - Args: - joystick_manager: VRJoystickManager实例 - """ - try: - joystick_manager.deadzone = self.deadzone - joystick_manager.turn_threshold = self.turn_threshold - joystick_manager.teleport_threshold = self.teleport_threshold - joystick_manager.turn_sensitivity = self.turn_sensitivity - joystick_manager.smooth_turning = (self.turn_mode == TurnMode.SMOOTH) - joystick_manager.snap_turn_angle = self.snap_turn_angle - joystick_manager.snap_turn_cooldown = self.snap_turn_cooldown - - # 应用传送系统配置 - if hasattr(joystick_manager, 'teleport_system') and joystick_manager.teleport_system: - teleport_sys = joystick_manager.teleport_system - teleport_sys.teleport_range = self.teleport_range - teleport_sys.arc_resolution = self.teleport_arc_resolution - teleport_sys.initial_velocity = self.teleport_initial_velocity - teleport_sys.min_teleport_distance = self.min_teleport_distance - teleport_sys.player_height_offset = self.player_height_offset - - print("✅ 配置已成功应用到摇杆管理器") - - except Exception as e: - print(f"⚠️ 应用配置失败: {e}") - - -# 预定义的配置实例 -COMFORTABLE_CONFIG = VRJoystickConfig() -COMFORTABLE_CONFIG.apply_profile(JoystickProfile.COMFORTABLE) - -STANDARD_CONFIG = VRJoystickConfig() -STANDARD_CONFIG.apply_profile(JoystickProfile.STANDARD) - -GAMING_CONFIG = VRJoystickConfig() -GAMING_CONFIG.apply_profile(JoystickProfile.GAMING) - -ACCESSIBLE_CONFIG = VRJoystickConfig() -ACCESSIBLE_CONFIG.apply_profile(JoystickProfile.ACCESSIBLE) - - -def create_custom_config(**kwargs): - """创建自定义配置 - - Args: - **kwargs: 配置参数 - - Returns: - VRJoystickConfig: 自定义配置实例 - """ - config = VRJoystickConfig() - - for key, value in kwargs.items(): - if hasattr(config, key): - setattr(config, key, value) - else: - print(f"⚠️ 未知的配置参数: {key}") - - return config - - -def print_usage_guide(): - """打印使用指南""" - print("🎮 ======= VR摇杆交互使用指南 =======") - print() - print("📋 基本操作:") - print(" • 摇杆左右移动 → 转向(旋转视角)") - print(" • 摇杆向前推 → 显示传送抛物线") - print(" • 松开摇杆 → 执行传送到落点") - print() - print("⚙️ 配置选项:") - print(" • 舒适模式 → 低灵敏度,适合长时间使用") - print(" • 标准模式 → 平衡体验,推荐大多数用户") - print(" • 游戏模式 → 高灵敏度,快速响应") - print(" • 无障碍模式 → 更大死区,容易控制") - print() - print("🎯 使用建议:") - print(" • 首次使用建议从标准模式开始") - print(" • 根据个人喜好调整转向模式(平滑/分段)") - print(" • 可以随时调整死区和灵敏度") - print(" • 传送范围可根据场景大小调整") - print() - - -if __name__ == "__main__": +""" +VR摇杆配置模块 + +提供摇杆交互的配置选项和预设: +- 不同的转向模式和灵敏度设置 +- 传送参数调整 +- 用户体验优化选项 +""" + +from enum import Enum + + +class TurnMode(Enum): + """转向模式枚举""" + SMOOTH = "smooth" # 平滑转向 + SNAP = "snap" # 分段转向 + + +class JoystickProfile(Enum): + """摇杆配置预设""" + COMFORTABLE = "comfortable" # 舒适模式 - 低灵敏度,平滑转向 + STANDARD = "standard" # 标准模式 - 中等灵敏度,分段转向 + GAMING = "gaming" # 游戏模式 - 高灵敏度,快速响应 + ACCESSIBLE = "accessible" # 无障碍模式 - 更大死区,更高阈值 + + +class VRJoystickConfig: + """VR摇杆配置类""" + + def __init__(self): + """初始化默认配置""" + # 基础参数 + self.deadzone = 0.15 # 摇杆死区 (0-1) + self.turn_threshold = 0.3 # 转向激活阈值 + self.teleport_threshold = 0.5 # 传送激活阈值 + + # 转向设置 + self.turn_mode = TurnMode.SNAP # 转向模式 + self.turn_sensitivity = 250.0 # 转向灵敏度(度/秒)- 增加速度 + self.snap_turn_angle = 30.0 # 分段转向角度(度) + self.snap_turn_cooldown = 0.3 # 分段转向间隔(秒) + + # 传送设置 + self.teleport_range = 20.0 # 最大传送距离 + self.teleport_arc_resolution = 50 # 抛物线精度 + self.teleport_initial_velocity = 10.0 # 传送初始速度 + self.min_teleport_distance = 1.0 # 最小传送距离 + self.player_height_offset = 1.7 # 玩家站立高度偏移(米) + + # 反馈设置 + self.haptic_feedback_enabled = True # 是否启用震动反馈 + self.teleport_start_haptic = (0.002, 0.3) # 传送开始震动 (时长, 强度) + self.teleport_success_haptic = (0.005, 0.8) # 传送成功震动 + self.teleport_fail_haptic = (0.001, 0.2) # 传送失败震动 + + # 可视化设置 + self.show_teleport_arc = True # 显示传送抛物线 + self.show_teleport_target = True # 显示传送目标标记 + self.arc_valid_color = (0.2, 0.9, 0.2, 0.8) # 有效抛物线颜色 + self.arc_invalid_color = (0.9, 0.2, 0.2, 0.8) # 无效抛物线颜色 + + def apply_profile(self, profile: JoystickProfile): + """应用预设配置 + + Args: + profile: 配置预设 + """ + if profile == JoystickProfile.COMFORTABLE: + self._apply_comfortable_profile() + elif profile == JoystickProfile.STANDARD: + self._apply_standard_profile() + elif profile == JoystickProfile.GAMING: + self._apply_gaming_profile() + elif profile == JoystickProfile.ACCESSIBLE: + self._apply_accessible_profile() + + def _apply_comfortable_profile(self): + """舒适模式 - 适合长时间使用""" + self.deadzone = 0.2 + self.turn_threshold = 0.4 + self.teleport_threshold = 0.6 + self.turn_mode = TurnMode.SMOOTH + self.turn_sensitivity = 100.0 # 适中的转向速度 + self.snap_turn_cooldown = 0.4 + print("✓ 已应用舒适模式配置") + + def _apply_standard_profile(self): + """标准模式 - 平衡的体验""" + self.deadzone = 0.15 + self.turn_threshold = 0.3 + self.teleport_threshold = 0.5 + self.turn_mode = TurnMode.SNAP + self.turn_sensitivity = 150.0 # 更快的转向速度 + self.snap_turn_angle = 30.0 + self.snap_turn_cooldown = 0.3 + print("✓ 已应用标准模式配置") + + def _apply_gaming_profile(self): + """游戏模式 - 快速响应""" + self.deadzone = 0.1 + self.turn_threshold = 0.2 + self.teleport_threshold = 0.4 + self.turn_mode = TurnMode.SMOOTH + self.turn_sensitivity = 200.0 # 高速转向 + self.snap_turn_cooldown = 0.2 + print("✓ 已应用游戏模式配置") + + def _apply_accessible_profile(self): + """无障碍模式 - 更容易控制""" + self.deadzone = 0.25 + self.turn_threshold = 0.5 + self.teleport_threshold = 0.7 + self.turn_mode = TurnMode.SNAP + self.turn_sensitivity = 80.0 # 较慢但可控的转向 + self.snap_turn_angle = 45.0 + self.snap_turn_cooldown = 0.5 + print("✓ 已应用无障碍模式配置") + + def set_turn_mode(self, mode: TurnMode): + """设置转向模式 + + Args: + mode: 转向模式 + """ + self.turn_mode = mode + print(f"✓ 转向模式设置为: {mode.value}") + + def set_turn_sensitivity(self, sensitivity: float): + """设置转向灵敏度 + + Args: + sensitivity: 转向灵敏度(度/秒) + """ + self.turn_sensitivity = max(10.0, min(180.0, sensitivity)) + print(f"✓ 转向灵敏度设置为: {self.turn_sensitivity}度/秒") + + def set_deadzone(self, deadzone: float): + """设置摇杆死区 + + Args: + deadzone: 死区大小 (0-1) + """ + self.deadzone = max(0.0, min(0.5, deadzone)) + print(f"✓ 摇杆死区设置为: {self.deadzone}") + + def set_teleport_range(self, range_meters: float): + """设置传送范围 + + Args: + range_meters: 传送范围(米) + """ + self.teleport_range = max(5.0, min(50.0, range_meters)) + print(f"✓ 传送范围设置为: {self.teleport_range}米") + + def set_player_height(self, height_meters: float): + """设置玩家站立高度偏移 + + Args: + height_meters: 站立高度偏移(米),推荐范围1.5-2.0米 + """ + self.player_height_offset = max(1.0, min(2.5, height_meters)) + print(f"✓ 玩家站立高度偏移设置为: {self.player_height_offset}米") + + def enable_haptic_feedback(self, enabled: bool): + """启用或禁用震动反馈 + + Args: + enabled: 是否启用 + """ + self.haptic_feedback_enabled = enabled + print(f"✓ 震动反馈: {'启用' if enabled else '禁用'}") + + def get_config_dict(self): + """获取配置字典 + + Returns: + dict: 配置参数字典 + """ + return { + 'deadzone': self.deadzone, + 'turn_threshold': self.turn_threshold, + 'teleport_threshold': self.teleport_threshold, + 'turn_mode': self.turn_mode.value, + 'turn_sensitivity': self.turn_sensitivity, + 'snap_turn_angle': self.snap_turn_angle, + 'snap_turn_cooldown': self.snap_turn_cooldown, + 'teleport_range': self.teleport_range, + 'haptic_feedback_enabled': self.haptic_feedback_enabled + } + + def apply_to_joystick_manager(self, joystick_manager): + """将配置应用到摇杆管理器 + + Args: + joystick_manager: VRJoystickManager实例 + """ + try: + joystick_manager.deadzone = self.deadzone + joystick_manager.turn_threshold = self.turn_threshold + joystick_manager.teleport_threshold = self.teleport_threshold + joystick_manager.turn_sensitivity = self.turn_sensitivity + joystick_manager.smooth_turning = (self.turn_mode == TurnMode.SMOOTH) + joystick_manager.snap_turn_angle = self.snap_turn_angle + joystick_manager.snap_turn_cooldown = self.snap_turn_cooldown + + # 应用传送系统配置 + if hasattr(joystick_manager, 'teleport_system') and joystick_manager.teleport_system: + teleport_sys = joystick_manager.teleport_system + teleport_sys.teleport_range = self.teleport_range + teleport_sys.arc_resolution = self.teleport_arc_resolution + teleport_sys.initial_velocity = self.teleport_initial_velocity + teleport_sys.min_teleport_distance = self.min_teleport_distance + teleport_sys.player_height_offset = self.player_height_offset + + print("✅ 配置已成功应用到摇杆管理器") + + except Exception as e: + print(f"⚠️ 应用配置失败: {e}") + + +# 预定义的配置实例 +COMFORTABLE_CONFIG = VRJoystickConfig() +COMFORTABLE_CONFIG.apply_profile(JoystickProfile.COMFORTABLE) + +STANDARD_CONFIG = VRJoystickConfig() +STANDARD_CONFIG.apply_profile(JoystickProfile.STANDARD) + +GAMING_CONFIG = VRJoystickConfig() +GAMING_CONFIG.apply_profile(JoystickProfile.GAMING) + +ACCESSIBLE_CONFIG = VRJoystickConfig() +ACCESSIBLE_CONFIG.apply_profile(JoystickProfile.ACCESSIBLE) + + +def create_custom_config(**kwargs): + """创建自定义配置 + + Args: + **kwargs: 配置参数 + + Returns: + VRJoystickConfig: 自定义配置实例 + """ + config = VRJoystickConfig() + + for key, value in kwargs.items(): + if hasattr(config, key): + setattr(config, key, value) + else: + print(f"⚠️ 未知的配置参数: {key}") + + return config + + +def print_usage_guide(): + """打印使用指南""" + print("🎮 ======= VR摇杆交互使用指南 =======") + print() + print("📋 基本操作:") + print(" • 摇杆左右移动 → 转向(旋转视角)") + print(" • 摇杆向前推 → 显示传送抛物线") + print(" • 松开摇杆 → 执行传送到落点") + print() + print("⚙️ 配置选项:") + print(" • 舒适模式 → 低灵敏度,适合长时间使用") + print(" • 标准模式 → 平衡体验,推荐大多数用户") + print(" • 游戏模式 → 高灵敏度,快速响应") + print(" • 无障碍模式 → 更大死区,容易控制") + print() + print("🎯 使用建议:") + print(" • 首次使用建议从标准模式开始") + print(" • 根据个人喜好调整转向模式(平滑/分段)") + print(" • 可以随时调整死区和灵敏度") + print(" • 传送范围可根据场景大小调整") + print() + + +if __name__ == "__main__": print_usage_guide() \ No newline at end of file diff --git a/core/vr/config/shadow_stage.py b/core/vr/config/shadow_stage.py index e405bd07..dee78bf7 100644 --- a/core/vr/config/shadow_stage.py +++ b/core/vr/config/shadow_stage.py @@ -1,173 +1,173 @@ -""" -VR专用阴影Stage - -为VR渲染管线提供阴影支持。采用智能复用策略: -- 默认复用主Pipeline的ShadowAtlas(从桌面相机视角) -- 可选:为VR创建独立ShadowAtlas(性能开销较大) - -设计考虑: -1. VR左右眼视差小,共享阴影合理 -2. 性能优先,避免重复渲染阴影 -3. 保留扩展性,支持未来独立阴影渲染 -""" - -from panda3d.core import SamplerState -from RenderPipelineFile.rpcore.render_target import RenderTarget - - -class VRShadowStage: - """VR阴影Stage - 为VR提供阴影数据""" - - def __init__(self, name, pipeline, use_shared_atlas=True): - """ - 初始化VR阴影Stage - - Args: - name: stage名称 - pipeline: RenderPipeline实例引用 - use_shared_atlas: 是否使用主Pipeline的ShadowAtlas(默认True) - """ - self.name = name - self.pipeline = pipeline - self.use_shared_atlas = use_shared_atlas - self.target = None - self.shadow_atlas_tex = None - self.shadow_atlas_pcf_state = None - - def create(self, width, height): - """ - 创建阴影Stage - - Args: - width: VR眼睛渲染宽度(参考,阴影atlas不一定使用此分辨率) - height: VR眼睛渲染高度 - """ - print(f"🌑 创建{self.name} 阴影Stage") - - if self.use_shared_atlas: - # 方案A: 复用主Pipeline的ShadowAtlas - return self._create_shared_shadow_atlas() - else: - # 方案B: 创建VR独立的ShadowAtlas - return self._create_independent_shadow_atlas(width, height) - - def _create_shared_shadow_atlas(self): - """复用主Pipeline的ShadowAtlas(推荐方案)""" - try: - # 从主Pipeline获取ShadowStage - shadow_stage = None - if hasattr(self.pipeline, 'stage_mgr'): - # 查找主Pipeline的ShadowStage - for stage in self.pipeline.stage_mgr.stages: - if stage.__class__.__name__ == 'ShadowStage': - shadow_stage = stage - break - - if not shadow_stage: - print(" ⚠️ 主Pipeline的ShadowStage不存在") - # 尝试从pipes获取 - pipes = self.pipeline.stage_mgr.pipes - if "ShadowAtlas" in pipes and "ShadowAtlasPCF" in pipes: - self.shadow_atlas_tex = pipes["ShadowAtlas"] - shadow_atlas_pcf_tuple = pipes["ShadowAtlasPCF"] - if isinstance(shadow_atlas_pcf_tuple, tuple) and len(shadow_atlas_pcf_tuple) >= 2: - self.shadow_atlas_pcf_state = shadow_atlas_pcf_tuple[1] - print(" ✅ 从pipes获取到ShadowAtlas") - return True - else: - print(" ❌ pipes中也没有ShadowAtlas") - return False - - # 从ShadowStage获取atlas - if hasattr(shadow_stage, 'target') and shadow_stage.target: - self.shadow_atlas_tex = shadow_stage.target.depth_tex - self.shadow_atlas_pcf_state = self._make_pcf_state() - print(" ✅ 复用主Pipeline的ShadowAtlas") - print(f" Atlas分辨率: {shadow_stage.size}x{shadow_stage.size}") - print(f" 优点: 零额外开销,与桌面视图共享阴影") - print(f" 注意: 阴影从桌面相机视角渲染") - return True - else: - print(" ❌ ShadowStage.target不存在") - return False - - except Exception as e: - print(f" ❌ 复用ShadowAtlas失败: {e}") - import traceback - traceback.print_exc() - return False - - def _create_independent_shadow_atlas(self, width, height): - """为VR创建独立的ShadowAtlas(高级方案)""" - try: - print(" 创建VR独立ShadowAtlas...") - print(" ⚠️ 此方案需要额外实现阴影渲染逻辑") - print(" ⚠️ 当前版本暂不支持,请使用共享模式") - - # TODO: 未来实现 - # 1. 创建VR专用的shadow atlas buffer - # 2. 为VR相机视角设置shadow manager - # 3. 渲染VR视角的阴影到独立atlas - # 4. 性能开销: 需要为VR单独渲染一次所有阴影 - - return False - - except Exception as e: - print(f" ❌ 创建独立ShadowAtlas失败: {e}") - return False - - def _make_pcf_state(self): - """创建PCF(Percentage Closer Filtering)采样器状态""" - state = SamplerState() - state.set_minfilter(SamplerState.FT_shadow) - state.set_magfilter(SamplerState.FT_shadow) - return state - - def get_shadow_atlas_tex(self): - """获取阴影Atlas纹理""" - return self.shadow_atlas_tex - - def get_shadow_atlas_pcf(self): - """获取带PCF的阴影Atlas(纹理+采样状态的元组)""" - if self.shadow_atlas_tex and self.shadow_atlas_pcf_state: - return (self.shadow_atlas_tex, self.shadow_atlas_pcf_state) - return None - - def bind_to_stage(self, target_stage): - """ - 将阴影数据绑定到目标stage(通常是lighting stage) - - Args: - target_stage: 需要阴影数据的目标stage(VRLightingStage等) - """ - if not target_stage or not hasattr(target_stage, 'target'): - print(f" ⚠️ 无法绑定阴影到无效的stage") - return False - - try: - # 绑定阴影atlas纹理 - if self.shadow_atlas_tex: - target_stage.target.set_shader_input("ShadowAtlas", self.shadow_atlas_tex) - - # 绑定PCF阴影atlas - pcf_tuple = self.get_shadow_atlas_pcf() - if pcf_tuple: - target_stage.target.set_shader_input("ShadowAtlasPCF", *pcf_tuple) - - print(f" ✅ 阴影数据已绑定到 {target_stage.name}") - return True - - except Exception as e: - print(f" ⚠️ 绑定阴影数据失败: {e}") - return False - - def cleanup(self): - """清理资源""" - # 如果使用共享atlas,不需要清理 - # 如果创建了独立atlas,需要在这里释放 - if self.target: - self.target.remove() - self.target = None - self.shadow_atlas_tex = None - self.shadow_atlas_pcf_state = None - print(f"✓ {self.name} 阴影Stage已清理") +""" +VR专用阴影Stage + +为VR渲染管线提供阴影支持。采用智能复用策略: +- 默认复用主Pipeline的ShadowAtlas(从桌面相机视角) +- 可选:为VR创建独立ShadowAtlas(性能开销较大) + +设计考虑: +1. VR左右眼视差小,共享阴影合理 +2. 性能优先,避免重复渲染阴影 +3. 保留扩展性,支持未来独立阴影渲染 +""" + +from panda3d.core import SamplerState +from RenderPipelineFile.rpcore.render_target import RenderTarget + + +class VRShadowStage: + """VR阴影Stage - 为VR提供阴影数据""" + + def __init__(self, name, pipeline, use_shared_atlas=True): + """ + 初始化VR阴影Stage + + Args: + name: stage名称 + pipeline: RenderPipeline实例引用 + use_shared_atlas: 是否使用主Pipeline的ShadowAtlas(默认True) + """ + self.name = name + self.pipeline = pipeline + self.use_shared_atlas = use_shared_atlas + self.target = None + self.shadow_atlas_tex = None + self.shadow_atlas_pcf_state = None + + def create(self, width, height): + """ + 创建阴影Stage + + Args: + width: VR眼睛渲染宽度(参考,阴影atlas不一定使用此分辨率) + height: VR眼睛渲染高度 + """ + print(f"🌑 创建{self.name} 阴影Stage") + + if self.use_shared_atlas: + # 方案A: 复用主Pipeline的ShadowAtlas + return self._create_shared_shadow_atlas() + else: + # 方案B: 创建VR独立的ShadowAtlas + return self._create_independent_shadow_atlas(width, height) + + def _create_shared_shadow_atlas(self): + """复用主Pipeline的ShadowAtlas(推荐方案)""" + try: + # 从主Pipeline获取ShadowStage + shadow_stage = None + if hasattr(self.pipeline, 'stage_mgr'): + # 查找主Pipeline的ShadowStage + for stage in self.pipeline.stage_mgr.stages: + if stage.__class__.__name__ == 'ShadowStage': + shadow_stage = stage + break + + if not shadow_stage: + print(" ⚠️ 主Pipeline的ShadowStage不存在") + # 尝试从pipes获取 + pipes = self.pipeline.stage_mgr.pipes + if "ShadowAtlas" in pipes and "ShadowAtlasPCF" in pipes: + self.shadow_atlas_tex = pipes["ShadowAtlas"] + shadow_atlas_pcf_tuple = pipes["ShadowAtlasPCF"] + if isinstance(shadow_atlas_pcf_tuple, tuple) and len(shadow_atlas_pcf_tuple) >= 2: + self.shadow_atlas_pcf_state = shadow_atlas_pcf_tuple[1] + print(" ✅ 从pipes获取到ShadowAtlas") + return True + else: + print(" ❌ pipes中也没有ShadowAtlas") + return False + + # 从ShadowStage获取atlas + if hasattr(shadow_stage, 'target') and shadow_stage.target: + self.shadow_atlas_tex = shadow_stage.target.depth_tex + self.shadow_atlas_pcf_state = self._make_pcf_state() + print(" ✅ 复用主Pipeline的ShadowAtlas") + print(f" Atlas分辨率: {shadow_stage.size}x{shadow_stage.size}") + print(f" 优点: 零额外开销,与桌面视图共享阴影") + print(f" 注意: 阴影从桌面相机视角渲染") + return True + else: + print(" ❌ ShadowStage.target不存在") + return False + + except Exception as e: + print(f" ❌ 复用ShadowAtlas失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_independent_shadow_atlas(self, width, height): + """为VR创建独立的ShadowAtlas(高级方案)""" + try: + print(" 创建VR独立ShadowAtlas...") + print(" ⚠️ 此方案需要额外实现阴影渲染逻辑") + print(" ⚠️ 当前版本暂不支持,请使用共享模式") + + # TODO: 未来实现 + # 1. 创建VR专用的shadow atlas buffer + # 2. 为VR相机视角设置shadow manager + # 3. 渲染VR视角的阴影到独立atlas + # 4. 性能开销: 需要为VR单独渲染一次所有阴影 + + return False + + except Exception as e: + print(f" ❌ 创建独立ShadowAtlas失败: {e}") + return False + + def _make_pcf_state(self): + """创建PCF(Percentage Closer Filtering)采样器状态""" + state = SamplerState() + state.set_minfilter(SamplerState.FT_shadow) + state.set_magfilter(SamplerState.FT_shadow) + return state + + def get_shadow_atlas_tex(self): + """获取阴影Atlas纹理""" + return self.shadow_atlas_tex + + def get_shadow_atlas_pcf(self): + """获取带PCF的阴影Atlas(纹理+采样状态的元组)""" + if self.shadow_atlas_tex and self.shadow_atlas_pcf_state: + return (self.shadow_atlas_tex, self.shadow_atlas_pcf_state) + return None + + def bind_to_stage(self, target_stage): + """ + 将阴影数据绑定到目标stage(通常是lighting stage) + + Args: + target_stage: 需要阴影数据的目标stage(VRLightingStage等) + """ + if not target_stage or not hasattr(target_stage, 'target'): + print(f" ⚠️ 无法绑定阴影到无效的stage") + return False + + try: + # 绑定阴影atlas纹理 + if self.shadow_atlas_tex: + target_stage.target.set_shader_input("ShadowAtlas", self.shadow_atlas_tex) + + # 绑定PCF阴影atlas + pcf_tuple = self.get_shadow_atlas_pcf() + if pcf_tuple: + target_stage.target.set_shader_input("ShadowAtlasPCF", *pcf_tuple) + + print(f" ✅ 阴影数据已绑定到 {target_stage.name}") + return True + + except Exception as e: + print(f" ⚠️ 绑定阴影数据失败: {e}") + return False + + def cleanup(self): + """清理资源""" + # 如果使用共享atlas,不需要清理 + # 如果创建了独立atlas,需要在这里释放 + if self.target: + self.target.remove() + self.target = None + self.shadow_atlas_tex = None + self.shadow_atlas_pcf_state = None + print(f"✓ {self.name} 阴影Stage已清理") diff --git a/core/vr/config/vr_config.py b/core/vr/config/vr_config.py index 3a2412ab..c881abcd 100644 --- a/core/vr/config/vr_config.py +++ b/core/vr/config/vr_config.py @@ -1,272 +1,272 @@ -""" -VR配置管理器模块 - -负责VR设置的保存、加载和管理 -""" - -import os -import json -from pathlib import Path - - -class VRConfigManager: - """VR配置管理器类""" - - def __init__(self, config_dir=None): - """初始化配置管理器 - - Args: - config_dir: 配置目录路径,默认为项目目录/config - """ - if config_dir is None: - # 默认使用项目根目录下的config文件夹 - project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - config_dir = os.path.join(project_root, "config") - - self.config_dir = Path(config_dir) - self.config_file = self.config_dir / "vr_settings.json" - - # 确保配置目录存在 - self.config_dir.mkdir(parents=True, exist_ok=True) - - # 默认配置 - self.default_config = { - "render_mode": "normal", # "normal" 或 "render_pipeline" - "resolution_scale": 0.75, - "pipeline_resolution_scale": 0.75, - "quality_preset": "balanced", # "performance", "balanced", "quality" - "anti_aliasing": "4x", # "无", "2x", "4x", "8x" - "refresh_rate": "90Hz", # "72Hz", "90Hz", "120Hz", "144Hz" - "async_reprojection": True, # 异步重投影开关 - "pipeline_vr_config": { - "enable_shadows": True, - "enable_ao": True, - "enable_bloom": False, - "enable_motion_blur": False, - "enable_ssr": False, - "shadow_quality": "medium", - "ao_quality": "low" - } - } - - def load_config(self): - """加载VR配置 - - Returns: - dict: VR配置字典 - """ - try: - if self.config_file.exists(): - with open(self.config_file, 'r', encoding='utf-8') as f: - config = json.load(f) - print(f"✓ VR配置已加载: {self.config_file}") - return config - else: - print(f"⚠️ 配置文件不存在,使用默认配置: {self.config_file}") - return self.default_config.copy() - - except Exception as e: - print(f"❌ 加载VR配置失败: {e}") - print(" 使用默认配置") - return self.default_config.copy() - - def save_config(self, config): - """保存VR配置 - - Args: - config: VR配置字典 - - Returns: - bool: 保存是否成功 - """ - try: - with open(self.config_file, 'w', encoding='utf-8') as f: - json.dump(config, f, indent=4, ensure_ascii=False) - print(f"✓ VR配置已保存: {self.config_file}") - return True - - except Exception as e: - print(f"❌ 保存VR配置失败: {e}") - import traceback - traceback.print_exc() - return False - - def get_render_mode(self): - """获取渲染模式配置 - - Returns: - str: 渲染模式 ("normal" 或 "render_pipeline") - """ - config = self.load_config() - return config.get("render_mode", "normal") - - def set_render_mode(self, mode): - """设置渲染模式并保存 - - Args: - mode: 渲染模式字符串 - - Returns: - bool: 设置是否成功 - """ - if mode not in ["normal", "render_pipeline"]: - print(f"❌ 无效的渲染模式: {mode}") - return False - - config = self.load_config() - config["render_mode"] = mode - return self.save_config(config) - - def get_resolution_scale(self): - """获取分辨率缩放配置 - - Returns: - float: 分辨率缩放系数 - """ - config = self.load_config() - return config.get("resolution_scale", 0.75) - - def set_resolution_scale(self, scale): - """设置分辨率缩放并保存 - - Args: - scale: 分辨率缩放系数 (0.5-1.0) - - Returns: - bool: 设置是否成功 - """ - if not 0.5 <= scale <= 1.0: - print(f"❌ 无效的分辨率缩放: {scale} (应在0.5-1.0之间)") - return False - - config = self.load_config() - config["resolution_scale"] = scale - return self.save_config(config) - - def get_quality_preset(self): - """获取质量预设 - - Returns: - str: 质量预设名称 - """ - config = self.load_config() - return config.get("quality_preset", "balanced") - - def set_quality_preset(self, preset): - """设置质量预设并保存 - - Args: - preset: 质量预设 ("performance", "balanced", "quality") - - Returns: - bool: 设置是否成功 - """ - if preset not in ["performance", "balanced", "quality"]: - print(f"❌ 无效的质量预设: {preset}") - return False - - config = self.load_config() - config["quality_preset"] = preset - return self.save_config(config) - - def get_pipeline_config(self): - """获取RenderPipeline VR配置 - - Returns: - dict: Pipeline配置字典 - """ - config = self.load_config() - return config.get("pipeline_vr_config", self.default_config["pipeline_vr_config"].copy()) - - def update_pipeline_config(self, pipeline_config): - """更新RenderPipeline VR配置 - - Args: - pipeline_config: Pipeline配置字典 - - Returns: - bool: 更新是否成功 - """ - config = self.load_config() - config["pipeline_vr_config"] = pipeline_config - return self.save_config(config) - - def reset_to_defaults(self): - """重置为默认配置 - - Returns: - bool: 重置是否成功 - """ - return self.save_config(self.default_config.copy()) - - def apply_config_to_vr_manager(self, vr_manager): - """将配置应用到VR管理器 - - Args: - vr_manager: VRManager实例 - - Returns: - bool: 应用是否成功 - """ - try: - config = self.load_config() - - # 应用渲染模式 - render_mode = config.get("render_mode", "normal") - from core.vr import VRRenderMode - if render_mode == "render_pipeline": - vr_manager.vr_render_mode = VRRenderMode.RENDER_PIPELINE - else: - vr_manager.vr_render_mode = VRRenderMode.NORMAL - - # 应用分辨率缩放 - resolution_scale = config.get("resolution_scale", 0.75) - vr_manager.resolution_scale = resolution_scale - - # 应用Pipeline分辨率缩放 - pipeline_resolution_scale = config.get("pipeline_resolution_scale", 0.75) - vr_manager.pipeline_resolution_scale = pipeline_resolution_scale - - # 应用质量预设 - quality_preset = config.get("quality_preset", "balanced") - vr_manager.current_quality_preset = quality_preset - - # 应用Pipeline配置 - pipeline_config = config.get("pipeline_vr_config", {}) - if pipeline_config: - vr_manager.pipeline_vr_config.update(pipeline_config) - - print("✓ VR配置已应用到VR管理器") - return True - - except Exception as e: - print(f"❌ 应用VR配置失败: {e}") - import traceback - traceback.print_exc() - return False - - def save_from_vr_manager(self, vr_manager): - """从VR管理器保存当前配置 - - Args: - vr_manager: VRManager实例 - - Returns: - bool: 保存是否成功 - """ - try: - config = { - "render_mode": vr_manager.vr_render_mode.value, - "resolution_scale": vr_manager.resolution_scale, - "pipeline_resolution_scale": vr_manager.pipeline_resolution_scale, - "quality_preset": vr_manager.current_quality_preset, - "pipeline_vr_config": vr_manager.pipeline_vr_config.copy() - } - - return self.save_config(config) - - except Exception as e: - print(f"❌ 从VR管理器保存配置失败: {e}") - import traceback - traceback.print_exc() - return False +""" +VR配置管理器模块 + +负责VR设置的保存、加载和管理 +""" + +import os +import json +from pathlib import Path + + +class VRConfigManager: + """VR配置管理器类""" + + def __init__(self, config_dir=None): + """初始化配置管理器 + + Args: + config_dir: 配置目录路径,默认为项目目录/config + """ + if config_dir is None: + # 默认使用项目根目录下的config文件夹 + project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + config_dir = os.path.join(project_root, "config") + + self.config_dir = Path(config_dir) + self.config_file = self.config_dir / "vr_settings.json" + + # 确保配置目录存在 + self.config_dir.mkdir(parents=True, exist_ok=True) + + # 默认配置 + self.default_config = { + "render_mode": "normal", # "normal" 或 "render_pipeline" + "resolution_scale": 0.75, + "pipeline_resolution_scale": 0.75, + "quality_preset": "balanced", # "performance", "balanced", "quality" + "anti_aliasing": "4x", # "无", "2x", "4x", "8x" + "refresh_rate": "90Hz", # "72Hz", "90Hz", "120Hz", "144Hz" + "async_reprojection": True, # 异步重投影开关 + "pipeline_vr_config": { + "enable_shadows": True, + "enable_ao": True, + "enable_bloom": False, + "enable_motion_blur": False, + "enable_ssr": False, + "shadow_quality": "medium", + "ao_quality": "low" + } + } + + def load_config(self): + """加载VR配置 + + Returns: + dict: VR配置字典 + """ + try: + if self.config_file.exists(): + with open(self.config_file, 'r', encoding='utf-8') as f: + config = json.load(f) + print(f"✓ VR配置已加载: {self.config_file}") + return config + else: + print(f"⚠️ 配置文件不存在,使用默认配置: {self.config_file}") + return self.default_config.copy() + + except Exception as e: + print(f"❌ 加载VR配置失败: {e}") + print(" 使用默认配置") + return self.default_config.copy() + + def save_config(self, config): + """保存VR配置 + + Args: + config: VR配置字典 + + Returns: + bool: 保存是否成功 + """ + try: + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump(config, f, indent=4, ensure_ascii=False) + print(f"✓ VR配置已保存: {self.config_file}") + return True + + except Exception as e: + print(f"❌ 保存VR配置失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_render_mode(self): + """获取渲染模式配置 + + Returns: + str: 渲染模式 ("normal" 或 "render_pipeline") + """ + config = self.load_config() + return config.get("render_mode", "normal") + + def set_render_mode(self, mode): + """设置渲染模式并保存 + + Args: + mode: 渲染模式字符串 + + Returns: + bool: 设置是否成功 + """ + if mode not in ["normal", "render_pipeline"]: + print(f"❌ 无效的渲染模式: {mode}") + return False + + config = self.load_config() + config["render_mode"] = mode + return self.save_config(config) + + def get_resolution_scale(self): + """获取分辨率缩放配置 + + Returns: + float: 分辨率缩放系数 + """ + config = self.load_config() + return config.get("resolution_scale", 0.75) + + def set_resolution_scale(self, scale): + """设置分辨率缩放并保存 + + Args: + scale: 分辨率缩放系数 (0.5-1.0) + + Returns: + bool: 设置是否成功 + """ + if not 0.5 <= scale <= 1.0: + print(f"❌ 无效的分辨率缩放: {scale} (应在0.5-1.0之间)") + return False + + config = self.load_config() + config["resolution_scale"] = scale + return self.save_config(config) + + def get_quality_preset(self): + """获取质量预设 + + Returns: + str: 质量预设名称 + """ + config = self.load_config() + return config.get("quality_preset", "balanced") + + def set_quality_preset(self, preset): + """设置质量预设并保存 + + Args: + preset: 质量预设 ("performance", "balanced", "quality") + + Returns: + bool: 设置是否成功 + """ + if preset not in ["performance", "balanced", "quality"]: + print(f"❌ 无效的质量预设: {preset}") + return False + + config = self.load_config() + config["quality_preset"] = preset + return self.save_config(config) + + def get_pipeline_config(self): + """获取RenderPipeline VR配置 + + Returns: + dict: Pipeline配置字典 + """ + config = self.load_config() + return config.get("pipeline_vr_config", self.default_config["pipeline_vr_config"].copy()) + + def update_pipeline_config(self, pipeline_config): + """更新RenderPipeline VR配置 + + Args: + pipeline_config: Pipeline配置字典 + + Returns: + bool: 更新是否成功 + """ + config = self.load_config() + config["pipeline_vr_config"] = pipeline_config + return self.save_config(config) + + def reset_to_defaults(self): + """重置为默认配置 + + Returns: + bool: 重置是否成功 + """ + return self.save_config(self.default_config.copy()) + + def apply_config_to_vr_manager(self, vr_manager): + """将配置应用到VR管理器 + + Args: + vr_manager: VRManager实例 + + Returns: + bool: 应用是否成功 + """ + try: + config = self.load_config() + + # 应用渲染模式 + render_mode = config.get("render_mode", "normal") + from core.vr import VRRenderMode + if render_mode == "render_pipeline": + vr_manager.vr_render_mode = VRRenderMode.RENDER_PIPELINE + else: + vr_manager.vr_render_mode = VRRenderMode.NORMAL + + # 应用分辨率缩放 + resolution_scale = config.get("resolution_scale", 0.75) + vr_manager.resolution_scale = resolution_scale + + # 应用Pipeline分辨率缩放 + pipeline_resolution_scale = config.get("pipeline_resolution_scale", 0.75) + vr_manager.pipeline_resolution_scale = pipeline_resolution_scale + + # 应用质量预设 + quality_preset = config.get("quality_preset", "balanced") + vr_manager.current_quality_preset = quality_preset + + # 应用Pipeline配置 + pipeline_config = config.get("pipeline_vr_config", {}) + if pipeline_config: + vr_manager.pipeline_vr_config.update(pipeline_config) + + print("✓ VR配置已应用到VR管理器") + return True + + except Exception as e: + print(f"❌ 应用VR配置失败: {e}") + import traceback + traceback.print_exc() + return False + + def save_from_vr_manager(self, vr_manager): + """从VR管理器保存当前配置 + + Args: + vr_manager: VRManager实例 + + Returns: + bool: 保存是否成功 + """ + try: + config = { + "render_mode": vr_manager.vr_render_mode.value, + "resolution_scale": vr_manager.resolution_scale, + "pipeline_resolution_scale": vr_manager.pipeline_resolution_scale, + "quality_preset": vr_manager.current_quality_preset, + "pipeline_vr_config": vr_manager.pipeline_vr_config.copy() + } + + return self.save_config(config) + + except Exception as e: + print(f"❌ 从VR管理器保存配置失败: {e}") + import traceback + traceback.print_exc() + return False diff --git a/core/vr/config/vr_settings.json b/core/vr/config/vr_settings.json index 556f68a2..efc5f6db 100644 --- a/core/vr/config/vr_settings.json +++ b/core/vr/config/vr_settings.json @@ -1,18 +1,18 @@ -{ - "render_mode": "render_pipeline", - "resolution_scale": 0.75, - "pipeline_resolution_scale": 0.75, - "quality_preset": "quality", - "pipeline_vr_config": { - "enable_shadows": true, - "enable_ao": true, - "enable_bloom": false, - "enable_motion_blur": false, - "enable_ssr": false, - "shadow_quality": "medium", - "ao_quality": "low" - }, - "anti_aliasing": "4x", - "refresh_rate": "90Hz", - "async_reprojection": true +{ + "render_mode": "render_pipeline", + "resolution_scale": 0.75, + "pipeline_resolution_scale": 0.75, + "quality_preset": "quality", + "pipeline_vr_config": { + "enable_shadows": true, + "enable_ao": true, + "enable_bloom": false, + "enable_motion_blur": false, + "enable_ssr": false, + "shadow_quality": "medium", + "ao_quality": "low" + }, + "anti_aliasing": "4x", + "refresh_rate": "90Hz", + "async_reprojection": true } \ No newline at end of file diff --git a/core/vr/interaction/__init__.py b/core/vr/interaction/__init__.py index 5f25e749..cdec309e 100644 --- a/core/vr/interaction/__init__.py +++ b/core/vr/interaction/__init__.py @@ -1,11 +1,11 @@ -""" -VR交互子系统 - -负责VR交互功能: -- OpenVR动作系统 -- 摇杆输入和转向/传送 -- 传送系统(抛物线轨迹) -- 对象抓取和交互 -""" - -__all__ = [] +""" +VR交互子系统 + +负责VR交互功能: +- OpenVR动作系统 +- 摇杆输入和转向/传送 +- 传送系统(抛物线轨迹) +- 对象抓取和交互 +""" + +__all__ = [] diff --git a/core/vr/interaction/actions.py b/core/vr/interaction/actions.py index 378476db..d3e405c1 100644 --- a/core/vr/interaction/actions.py +++ b/core/vr/interaction/actions.py @@ -1,583 +1,583 @@ -""" -VR动作系统模块 - -基于OpenVR Action系统,提供高级的输入处理和动作映射: -- VR动作清单管理 -- 按钮和轴输入处理 -- 触觉反馈 -- 动作集管理 -""" - -import json -import os -from pathlib import Path -from direct.showbase.DirectObject import DirectObject - -try: - import openvr - OPENVR_AVAILABLE = True -except ImportError: - OPENVR_AVAILABLE = False - - -class VRActionManager(DirectObject): - """VR动作管理器 - 处理OpenVR动作系统""" - - def __init__(self, vr_manager): - """初始化VR动作管理器 - - Args: - vr_manager: VR管理器实例 - """ - super().__init__() - - self.vr_manager = vr_manager - self.vr_input = None - self.action_set_handles = [] - self.action_handles = {} - - # 预定义的标准动作 - self.standard_actions = { - # 姿态动作 - 'pose': '/actions/default/in/Pose', - - # 按钮动作 - 'trigger': '/actions/default/in/Trigger', - 'grip': '/actions/default/in/Grip', - 'menu': '/actions/default/in/Menu', - 'system': '/actions/default/in/System', - 'trackpad_click': '/actions/default/in/TrackpadClick', - 'trackpad_touch': '/actions/default/in/TrackpadTouch', - 'a_button': '/actions/default/in/AButton', - 'b_button': '/actions/default/in/BButton', - - # 轴动作 - 'trackpad': '/actions/default/in/Trackpad', - 'joystick': '/actions/default/in/Joystick', - 'squeeze': '/actions/default/in/Squeeze', - - # 震动输出 - 'haptic': '/actions/default/out/Haptic' - } - - # 动作集 - self.default_action_set = '/actions/default' - - print("✓ VR动作管理器初始化完成") - - def initialize(self): - """初始化VR动作系统""" - if not OPENVR_AVAILABLE or not self.vr_manager.vr_system: - print("⚠️ VR系统不可用,无法初始化动作系统") - return False - - try: - print("🎮 正在初始化VR动作系统...") - - # 获取VR输入接口 - self.vr_input = openvr.VRInput() - if not self.vr_input: - print("❌ 无法获取VR输入接口") - return False - - # 创建动作清单文件 - manifest_path = self._create_action_manifest() - if not manifest_path: - print("❌ 无法创建动作清单") - return False - - # 加载动作清单 - error = self.vr_input.setActionManifestPath(manifest_path) - if error != openvr.VRInputError_None: - print(f"❌ 加载动作清单失败: {error}") - return False - - # 获取动作句柄 - self._load_action_handles() - - # 创建动作集 - self._setup_action_sets() - - print("✅ VR动作系统初始化成功") - return True - - except Exception as e: - print(f"❌ VR动作系统初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def _create_action_manifest(self): - """创建VR动作清单文件""" - try: - # 动作清单配置 - manifest_data = { - "actions": [ - # 姿态动作 - { - "name": "/actions/default/in/Pose", - "type": "pose" - }, - - # 数字动作(按钮) - { - "name": "/actions/default/in/Trigger", - "type": "boolean" - }, - { - "name": "/actions/default/in/Grip", - "type": "boolean" - }, - { - "name": "/actions/default/in/Menu", - "type": "boolean" - }, - { - "name": "/actions/default/in/System", - "type": "boolean" - }, - { - "name": "/actions/default/in/TrackpadClick", - "type": "boolean" - }, - { - "name": "/actions/default/in/TrackpadTouch", - "type": "boolean" - }, - { - "name": "/actions/default/in/AButton", - "type": "boolean" - }, - { - "name": "/actions/default/in/BButton", - "type": "boolean" - }, - - # 模拟动作(轴) - { - "name": "/actions/default/in/Trackpad", - "type": "vector2" - }, - { - "name": "/actions/default/in/Joystick", - "type": "vector2" - }, - { - "name": "/actions/default/in/Squeeze", - "type": "vector1" - }, - - # 震动输出 - { - "name": "/actions/default/out/Haptic", - "type": "vibration" - } - ], - - "action_sets": [ - { - "name": "/actions/default", - "usage": "single" - } - ], - - "default_bindings": [ - { - "controller_type": "vive_controller", - "binding_url": "bindings_vive.json" - }, - { - "controller_type": "oculus_touch", - "binding_url": "bindings_oculus.json" - }, - { - "controller_type": "knuckles", - "binding_url": "bindings_index.json" - } - ], - - "localization": [ - { - "language_tag": "zh_CN", - "/actions/default/in/Trigger": "扳机", - "/actions/default/in/Grip": "握把", - "/actions/default/in/Menu": "菜单", - "/actions/default/in/System": "系统", - "/actions/default/in/TrackpadClick": "触摸板点击", - "/actions/default/in/TrackpadTouch": "触摸板触摸", - "/actions/default/in/Pose": "手部姿态", - "/actions/default/out/Haptic": "震动反馈" - } - ] - } - - # 保存到临时目录 - manifest_dir = Path.cwd() / "vr_actions" - manifest_dir.mkdir(exist_ok=True) - - manifest_path = manifest_dir / "actions.json" - with open(manifest_path, 'w', encoding='utf-8') as f: - json.dump(manifest_data, f, indent=2, ensure_ascii=False) - - # 创建基本的绑定文件 - self._create_default_bindings(manifest_dir) - - print(f"✓ 动作清单已创建: {manifest_path}") - return str(manifest_path) - - except Exception as e: - print(f"❌ 创建动作清单失败: {e}") - return None - - def _create_default_bindings(self, manifest_dir): - """创建默认的控制器绑定文件""" - # Vive控制器绑定 - vive_bindings = { - "controller_type": "vive_controller", - "description": "Vive控制器绑定", - "name": "EG VR Editor - Vive", - "bindings": { - "/actions/default": { - "sources": [ - { - "inputs": { - "click": { - "output": "/actions/default/in/Trigger" - } - }, - "mode": "button", - "path": "/user/hand/left/input/trigger" - }, - { - "inputs": { - "click": { - "output": "/actions/default/in/Trigger" - } - }, - "mode": "button", - "path": "/user/hand/right/input/trigger" - }, - { - "inputs": { - "click": { - "output": "/actions/default/in/Grip" - } - }, - "mode": "button", - "path": "/user/hand/left/input/grip" - }, - { - "inputs": { - "click": { - "output": "/actions/default/in/Grip" - } - }, - "mode": "button", - "path": "/user/hand/right/input/grip" - }, - { - "inputs": { - "click": { - "output": "/actions/default/in/Menu" - } - }, - "mode": "button", - "path": "/user/hand/left/input/menu" - }, - { - "inputs": { - "position": { - "output": "/actions/default/in/Trackpad" - }, - "click": { - "output": "/actions/default/in/TrackpadClick" - }, - "touch": { - "output": "/actions/default/in/TrackpadTouch" - } - }, - "mode": "trackpad", - "path": "/user/hand/left/input/trackpad" - }, - { - "inputs": { - "position": { - "output": "/actions/default/in/Trackpad" - }, - "click": { - "output": "/actions/default/in/TrackpadClick" - }, - "touch": { - "output": "/actions/default/in/TrackpadTouch" - } - }, - "mode": "trackpad", - "path": "/user/hand/right/input/trackpad" - } - ], - "poses": [ - { - "output": "/actions/default/in/Pose", - "path": "/user/hand/left/pose/raw" - }, - { - "output": "/actions/default/in/Pose", - "path": "/user/hand/right/pose/raw" - } - ], - "haptics": [ - { - "output": "/actions/default/out/Haptic", - "path": "/user/hand/left/output/haptic" - }, - { - "output": "/actions/default/out/Haptic", - "path": "/user/hand/right/output/haptic" - } - ] - } - } - } - - bindings_path = manifest_dir / "bindings_vive.json" - with open(bindings_path, 'w', encoding='utf-8') as f: - json.dump(vive_bindings, f, indent=2) - - print(f"✓ Vive控制器绑定已创建: {bindings_path}") - - def _load_action_handles(self): - """加载动作句柄""" - if not self.vr_input: - return - - try: - for action_name, action_path in self.standard_actions.items(): - handle = self.vr_input.getActionHandle(action_path) - self.action_handles[action_name] = handle - print(f"✓ 加载动作: {action_name} -> {handle}") - - except Exception as e: - print(f"⚠️ 加载动作句柄失败: {e}") - - def _setup_action_sets(self): - """设置动作集""" - if not self.vr_input: - return - - try: - # 获取默认动作集句柄 - action_set_handle = self.vr_input.getActionSetHandle(self.default_action_set) - self.action_set_handles = [action_set_handle] - - print(f"✓ 动作集已设置: {self.default_action_set}") - - except Exception as e: - print(f"⚠️ 设置动作集失败: {e}") - - def update_actions(self): - """更新动作状态 - 每帧调用""" - if not self.vr_input or not self.action_set_handles: - return - - try: - # 更新动作状态 - action_sets = (openvr.VRActiveActionSet_t * len(self.action_set_handles))() - for i, action_set_handle in enumerate(self.action_set_handles): - action_sets[i].ulActionSet = action_set_handle - - self.vr_input.updateActionState(action_sets) - - except Exception as e: - # 限制错误输出频率 - if not hasattr(self, '_last_action_error_frame'): - self._last_action_error_frame = 0 - - if hasattr(self.vr_manager, 'frame_count'): - if self.vr_manager.frame_count - self._last_action_error_frame > 300: # 每5秒输出一次 - print(f"⚠️ 更新动作状态失败: {e}") - self._last_action_error_frame = self.vr_manager.frame_count - - def is_digital_action_pressed(self, action_name, device_path=None): - """检查数字动作是否被按下 - - Args: - action_name: 动作名称 - device_path: 设备路径(可选) - - Returns: - tuple: (是否按下, 设备路径) - """ - if not self.vr_input or action_name not in self.action_handles: - return False, None - - try: - action_handle = self.action_handles[action_name] - device_handle = openvr.k_ulInvalidInputValueHandle - - if device_path: - device_handle = self.vr_input.getInputSourceHandle(device_path) - - action_data = self.vr_input.getDigitalActionData(action_handle, device_handle) - - if device_path and action_data.bActive: - origin_info = self.vr_input.getOriginTrackedDeviceInfo(action_data.activeOrigin) - device_path = origin_info.devicePath - - return action_data.bActive and action_data.bState, device_path - - except Exception as e: - return False, None - - def is_digital_action_just_pressed(self, action_name, device_path=None): - """检查数字动作是否刚刚被按下(上升沿)""" - if not self.vr_input or action_name not in self.action_handles: - return False, None - - try: - action_handle = self.action_handles[action_name] - device_handle = openvr.k_ulInvalidInputValueHandle - - if device_path: - device_handle = self.vr_input.getInputSourceHandle(device_path) - - action_data = self.vr_input.getDigitalActionData(action_handle, device_handle) - - if device_path and action_data.bActive: - origin_info = self.vr_input.getOriginTrackedDeviceInfo(action_data.activeOrigin) - device_path = origin_info.devicePath - - return action_data.bActive and action_data.bChanged and action_data.bState, device_path - - except Exception as e: - return False, None - - def is_digital_action_just_released(self, action_name, device_path=None): - """检查数字动作是否刚刚被释放(下降沿)""" - if not self.vr_input or action_name not in self.action_handles: - return False, None - - try: - action_handle = self.action_handles[action_name] - device_handle = openvr.k_ulInvalidInputValueHandle - - if device_path: - device_handle = self.vr_input.getInputSourceHandle(device_path) - - action_data = self.vr_input.getDigitalActionData(action_handle, device_handle) - - if device_path and action_data.bActive: - origin_info = self.vr_input.getOriginTrackedDeviceInfo(action_data.activeOrigin) - device_path = origin_info.devicePath - - return action_data.bActive and action_data.bChanged and not action_data.bState, device_path - - except Exception as e: - return False, None - - def get_analog_action_value(self, action_name, device_path=None): - """获取模拟动作值 - - Args: - action_name: 动作名称 - device_path: 设备路径(可选) - - Returns: - tuple: (值, 设备路径) - 值为Vec2(x,y)对于vector2,float对于vector1 - """ - if not self.vr_input or action_name not in self.action_handles: - return None, None - - try: - action_handle = self.action_handles[action_name] - device_handle = openvr.k_ulInvalidInputValueHandle - - if device_path: - device_handle = self.vr_input.getInputSourceHandle(device_path) - - analog_data = self.vr_input.getAnalogActionData(action_handle, device_handle) - - if device_path and analog_data.bActive: - origin_info = self.vr_input.getOriginTrackedDeviceInfo(analog_data.activeOrigin) - device_path = origin_info.devicePath - - if analog_data.bActive: - # 根据动作类型返回适当的值 - from panda3d.core import Vec2 - if action_name in ['trackpad', 'joystick']: - return Vec2(analog_data.x, analog_data.y), device_path - else: - return analog_data.x, device_path - - return None, device_path - - except Exception as e: - return None, None - - def get_pose_action_data(self, action_name, device_path=None): - """获取姿态动作数据""" - if not self.vr_input or action_name not in self.action_handles: - return None - - try: - action_handle = self.action_handles[action_name] - device_handle = openvr.k_ulInvalidInputValueHandle - - if device_path: - device_handle = self.vr_input.getInputSourceHandle(device_path) - - pose_data = self.vr_input.getPoseActionDataForNextFrame( - action_handle, - openvr.TrackingUniverseStanding, - device_handle - ) - - return pose_data - - except Exception as e: - return None - - def trigger_haptic_pulse(self, action_name, duration=0.001, frequency=1.0, amplitude=1.0, device_path=None): - """触发震动反馈 - - Args: - action_name: 震动动作名称 - duration: 持续时间(秒) - frequency: 频率 - amplitude: 振幅 (0.0-1.0) - device_path: 设备路径(可选) - """ - if not self.vr_input or action_name not in self.action_handles: - return False - - try: - action_handle = self.action_handles[action_name] - device_handle = openvr.k_ulInvalidInputValueHandle - - if device_path: - device_handle = self.vr_input.getInputSourceHandle(device_path) - - # 触发震动 - self.vr_input.triggerHapticVibrationAction( - action_handle, - 0, # 开始时间 - duration, - frequency, - amplitude, - device_handle - ) - - return True - - except Exception as e: - print(f"⚠️ 触发震动反馈失败: {e}") - return False - - def cleanup(self): - """清理资源""" - self.ignoreAll() - - # 清理动作句柄 - self.action_handles.clear() - self.action_set_handles.clear() - +""" +VR动作系统模块 + +基于OpenVR Action系统,提供高级的输入处理和动作映射: +- VR动作清单管理 +- 按钮和轴输入处理 +- 触觉反馈 +- 动作集管理 +""" + +import json +import os +from pathlib import Path +from direct.showbase.DirectObject import DirectObject + +try: + import openvr + OPENVR_AVAILABLE = True +except ImportError: + OPENVR_AVAILABLE = False + + +class VRActionManager(DirectObject): + """VR动作管理器 - 处理OpenVR动作系统""" + + def __init__(self, vr_manager): + """初始化VR动作管理器 + + Args: + vr_manager: VR管理器实例 + """ + super().__init__() + + self.vr_manager = vr_manager + self.vr_input = None + self.action_set_handles = [] + self.action_handles = {} + + # 预定义的标准动作 + self.standard_actions = { + # 姿态动作 + 'pose': '/actions/default/in/Pose', + + # 按钮动作 + 'trigger': '/actions/default/in/Trigger', + 'grip': '/actions/default/in/Grip', + 'menu': '/actions/default/in/Menu', + 'system': '/actions/default/in/System', + 'trackpad_click': '/actions/default/in/TrackpadClick', + 'trackpad_touch': '/actions/default/in/TrackpadTouch', + 'a_button': '/actions/default/in/AButton', + 'b_button': '/actions/default/in/BButton', + + # 轴动作 + 'trackpad': '/actions/default/in/Trackpad', + 'joystick': '/actions/default/in/Joystick', + 'squeeze': '/actions/default/in/Squeeze', + + # 震动输出 + 'haptic': '/actions/default/out/Haptic' + } + + # 动作集 + self.default_action_set = '/actions/default' + + print("✓ VR动作管理器初始化完成") + + def initialize(self): + """初始化VR动作系统""" + if not OPENVR_AVAILABLE or not self.vr_manager.vr_system: + print("⚠️ VR系统不可用,无法初始化动作系统") + return False + + try: + print("🎮 正在初始化VR动作系统...") + + # 获取VR输入接口 + self.vr_input = openvr.VRInput() + if not self.vr_input: + print("❌ 无法获取VR输入接口") + return False + + # 创建动作清单文件 + manifest_path = self._create_action_manifest() + if not manifest_path: + print("❌ 无法创建动作清单") + return False + + # 加载动作清单 + error = self.vr_input.setActionManifestPath(manifest_path) + if error != openvr.VRInputError_None: + print(f"❌ 加载动作清单失败: {error}") + return False + + # 获取动作句柄 + self._load_action_handles() + + # 创建动作集 + self._setup_action_sets() + + print("✅ VR动作系统初始化成功") + return True + + except Exception as e: + print(f"❌ VR动作系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_action_manifest(self): + """创建VR动作清单文件""" + try: + # 动作清单配置 + manifest_data = { + "actions": [ + # 姿态动作 + { + "name": "/actions/default/in/Pose", + "type": "pose" + }, + + # 数字动作(按钮) + { + "name": "/actions/default/in/Trigger", + "type": "boolean" + }, + { + "name": "/actions/default/in/Grip", + "type": "boolean" + }, + { + "name": "/actions/default/in/Menu", + "type": "boolean" + }, + { + "name": "/actions/default/in/System", + "type": "boolean" + }, + { + "name": "/actions/default/in/TrackpadClick", + "type": "boolean" + }, + { + "name": "/actions/default/in/TrackpadTouch", + "type": "boolean" + }, + { + "name": "/actions/default/in/AButton", + "type": "boolean" + }, + { + "name": "/actions/default/in/BButton", + "type": "boolean" + }, + + # 模拟动作(轴) + { + "name": "/actions/default/in/Trackpad", + "type": "vector2" + }, + { + "name": "/actions/default/in/Joystick", + "type": "vector2" + }, + { + "name": "/actions/default/in/Squeeze", + "type": "vector1" + }, + + # 震动输出 + { + "name": "/actions/default/out/Haptic", + "type": "vibration" + } + ], + + "action_sets": [ + { + "name": "/actions/default", + "usage": "single" + } + ], + + "default_bindings": [ + { + "controller_type": "vive_controller", + "binding_url": "bindings_vive.json" + }, + { + "controller_type": "oculus_touch", + "binding_url": "bindings_oculus.json" + }, + { + "controller_type": "knuckles", + "binding_url": "bindings_index.json" + } + ], + + "localization": [ + { + "language_tag": "zh_CN", + "/actions/default/in/Trigger": "扳机", + "/actions/default/in/Grip": "握把", + "/actions/default/in/Menu": "菜单", + "/actions/default/in/System": "系统", + "/actions/default/in/TrackpadClick": "触摸板点击", + "/actions/default/in/TrackpadTouch": "触摸板触摸", + "/actions/default/in/Pose": "手部姿态", + "/actions/default/out/Haptic": "震动反馈" + } + ] + } + + # 保存到临时目录 + manifest_dir = Path.cwd() / "vr_actions" + manifest_dir.mkdir(exist_ok=True) + + manifest_path = manifest_dir / "actions.json" + with open(manifest_path, 'w', encoding='utf-8') as f: + json.dump(manifest_data, f, indent=2, ensure_ascii=False) + + # 创建基本的绑定文件 + self._create_default_bindings(manifest_dir) + + print(f"✓ 动作清单已创建: {manifest_path}") + return str(manifest_path) + + except Exception as e: + print(f"❌ 创建动作清单失败: {e}") + return None + + def _create_default_bindings(self, manifest_dir): + """创建默认的控制器绑定文件""" + # Vive控制器绑定 + vive_bindings = { + "controller_type": "vive_controller", + "description": "Vive控制器绑定", + "name": "EG VR Editor - Vive", + "bindings": { + "/actions/default": { + "sources": [ + { + "inputs": { + "click": { + "output": "/actions/default/in/Trigger" + } + }, + "mode": "button", + "path": "/user/hand/left/input/trigger" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Trigger" + } + }, + "mode": "button", + "path": "/user/hand/right/input/trigger" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Grip" + } + }, + "mode": "button", + "path": "/user/hand/left/input/grip" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Grip" + } + }, + "mode": "button", + "path": "/user/hand/right/input/grip" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Menu" + } + }, + "mode": "button", + "path": "/user/hand/left/input/menu" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Trackpad" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + }, + "touch": { + "output": "/actions/default/in/TrackpadTouch" + } + }, + "mode": "trackpad", + "path": "/user/hand/left/input/trackpad" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Trackpad" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + }, + "touch": { + "output": "/actions/default/in/TrackpadTouch" + } + }, + "mode": "trackpad", + "path": "/user/hand/right/input/trackpad" + } + ], + "poses": [ + { + "output": "/actions/default/in/Pose", + "path": "/user/hand/left/pose/raw" + }, + { + "output": "/actions/default/in/Pose", + "path": "/user/hand/right/pose/raw" + } + ], + "haptics": [ + { + "output": "/actions/default/out/Haptic", + "path": "/user/hand/left/output/haptic" + }, + { + "output": "/actions/default/out/Haptic", + "path": "/user/hand/right/output/haptic" + } + ] + } + } + } + + bindings_path = manifest_dir / "bindings_vive.json" + with open(bindings_path, 'w', encoding='utf-8') as f: + json.dump(vive_bindings, f, indent=2) + + print(f"✓ Vive控制器绑定已创建: {bindings_path}") + + def _load_action_handles(self): + """加载动作句柄""" + if not self.vr_input: + return + + try: + for action_name, action_path in self.standard_actions.items(): + handle = self.vr_input.getActionHandle(action_path) + self.action_handles[action_name] = handle + print(f"✓ 加载动作: {action_name} -> {handle}") + + except Exception as e: + print(f"⚠️ 加载动作句柄失败: {e}") + + def _setup_action_sets(self): + """设置动作集""" + if not self.vr_input: + return + + try: + # 获取默认动作集句柄 + action_set_handle = self.vr_input.getActionSetHandle(self.default_action_set) + self.action_set_handles = [action_set_handle] + + print(f"✓ 动作集已设置: {self.default_action_set}") + + except Exception as e: + print(f"⚠️ 设置动作集失败: {e}") + + def update_actions(self): + """更新动作状态 - 每帧调用""" + if not self.vr_input or not self.action_set_handles: + return + + try: + # 更新动作状态 + action_sets = (openvr.VRActiveActionSet_t * len(self.action_set_handles))() + for i, action_set_handle in enumerate(self.action_set_handles): + action_sets[i].ulActionSet = action_set_handle + + self.vr_input.updateActionState(action_sets) + + except Exception as e: + # 限制错误输出频率 + if not hasattr(self, '_last_action_error_frame'): + self._last_action_error_frame = 0 + + if hasattr(self.vr_manager, 'frame_count'): + if self.vr_manager.frame_count - self._last_action_error_frame > 300: # 每5秒输出一次 + print(f"⚠️ 更新动作状态失败: {e}") + self._last_action_error_frame = self.vr_manager.frame_count + + def is_digital_action_pressed(self, action_name, device_path=None): + """检查数字动作是否被按下 + + Args: + action_name: 动作名称 + device_path: 设备路径(可选) + + Returns: + tuple: (是否按下, 设备路径) + """ + if not self.vr_input or action_name not in self.action_handles: + return False, None + + try: + action_handle = self.action_handles[action_name] + device_handle = openvr.k_ulInvalidInputValueHandle + + if device_path: + device_handle = self.vr_input.getInputSourceHandle(device_path) + + action_data = self.vr_input.getDigitalActionData(action_handle, device_handle) + + if device_path and action_data.bActive: + origin_info = self.vr_input.getOriginTrackedDeviceInfo(action_data.activeOrigin) + device_path = origin_info.devicePath + + return action_data.bActive and action_data.bState, device_path + + except Exception as e: + return False, None + + def is_digital_action_just_pressed(self, action_name, device_path=None): + """检查数字动作是否刚刚被按下(上升沿)""" + if not self.vr_input or action_name not in self.action_handles: + return False, None + + try: + action_handle = self.action_handles[action_name] + device_handle = openvr.k_ulInvalidInputValueHandle + + if device_path: + device_handle = self.vr_input.getInputSourceHandle(device_path) + + action_data = self.vr_input.getDigitalActionData(action_handle, device_handle) + + if device_path and action_data.bActive: + origin_info = self.vr_input.getOriginTrackedDeviceInfo(action_data.activeOrigin) + device_path = origin_info.devicePath + + return action_data.bActive and action_data.bChanged and action_data.bState, device_path + + except Exception as e: + return False, None + + def is_digital_action_just_released(self, action_name, device_path=None): + """检查数字动作是否刚刚被释放(下降沿)""" + if not self.vr_input or action_name not in self.action_handles: + return False, None + + try: + action_handle = self.action_handles[action_name] + device_handle = openvr.k_ulInvalidInputValueHandle + + if device_path: + device_handle = self.vr_input.getInputSourceHandle(device_path) + + action_data = self.vr_input.getDigitalActionData(action_handle, device_handle) + + if device_path and action_data.bActive: + origin_info = self.vr_input.getOriginTrackedDeviceInfo(action_data.activeOrigin) + device_path = origin_info.devicePath + + return action_data.bActive and action_data.bChanged and not action_data.bState, device_path + + except Exception as e: + return False, None + + def get_analog_action_value(self, action_name, device_path=None): + """获取模拟动作值 + + Args: + action_name: 动作名称 + device_path: 设备路径(可选) + + Returns: + tuple: (值, 设备路径) - 值为Vec2(x,y)对于vector2,float对于vector1 + """ + if not self.vr_input or action_name not in self.action_handles: + return None, None + + try: + action_handle = self.action_handles[action_name] + device_handle = openvr.k_ulInvalidInputValueHandle + + if device_path: + device_handle = self.vr_input.getInputSourceHandle(device_path) + + analog_data = self.vr_input.getAnalogActionData(action_handle, device_handle) + + if device_path and analog_data.bActive: + origin_info = self.vr_input.getOriginTrackedDeviceInfo(analog_data.activeOrigin) + device_path = origin_info.devicePath + + if analog_data.bActive: + # 根据动作类型返回适当的值 + from panda3d.core import Vec2 + if action_name in ['trackpad', 'joystick']: + return Vec2(analog_data.x, analog_data.y), device_path + else: + return analog_data.x, device_path + + return None, device_path + + except Exception as e: + return None, None + + def get_pose_action_data(self, action_name, device_path=None): + """获取姿态动作数据""" + if not self.vr_input or action_name not in self.action_handles: + return None + + try: + action_handle = self.action_handles[action_name] + device_handle = openvr.k_ulInvalidInputValueHandle + + if device_path: + device_handle = self.vr_input.getInputSourceHandle(device_path) + + pose_data = self.vr_input.getPoseActionDataForNextFrame( + action_handle, + openvr.TrackingUniverseStanding, + device_handle + ) + + return pose_data + + except Exception as e: + return None + + def trigger_haptic_pulse(self, action_name, duration=0.001, frequency=1.0, amplitude=1.0, device_path=None): + """触发震动反馈 + + Args: + action_name: 震动动作名称 + duration: 持续时间(秒) + frequency: 频率 + amplitude: 振幅 (0.0-1.0) + device_path: 设备路径(可选) + """ + if not self.vr_input or action_name not in self.action_handles: + return False + + try: + action_handle = self.action_handles[action_name] + device_handle = openvr.k_ulInvalidInputValueHandle + + if device_path: + device_handle = self.vr_input.getInputSourceHandle(device_path) + + # 触发震动 + self.vr_input.triggerHapticVibrationAction( + action_handle, + 0, # 开始时间 + duration, + frequency, + amplitude, + device_handle + ) + + return True + + except Exception as e: + print(f"⚠️ 触发震动反馈失败: {e}") + return False + + def cleanup(self): + """清理资源""" + self.ignoreAll() + + # 清理动作句柄 + self.action_handles.clear() + self.action_set_handles.clear() + print("🧹 VR动作管理器已清理") \ No newline at end of file diff --git a/core/vr/interaction/grab.py b/core/vr/interaction/grab.py index e5544ecc..373ef637 100644 --- a/core/vr/interaction/grab.py +++ b/core/vr/interaction/grab.py @@ -1,432 +1,432 @@ -""" -VR交互系统模块 - -提供VR手柄与3D场景的交互功能: -- 射线投射和碰撞检测 -- 对象选择和高亮 -- 对象抓取和移动 -- UI交互 -- 距离抓取 -""" - -from panda3d.core import ( - Vec3, Vec4, Mat4, Point3, CollisionRay, CollisionTraverser, - CollisionNode, CollisionHandlerQueue, BitMask32, NodePath, - CollisionSphere, CollisionTube, RenderState, TransparencyAttrib, - ColorAttrib -) -from direct.showbase.DirectObject import DirectObject - - -class VRInteractionManager(DirectObject): - """VR交互管理器 - 处理手柄与场景的交互""" - - def __init__(self, vr_manager): - """初始化VR交互管理器 - - Args: - vr_manager: VR管理器实例 - """ - super().__init__() - - self.vr_manager = vr_manager - self.world = vr_manager.world if hasattr(vr_manager, 'world') else None - - # 碰撞检测系统 - self.collision_traverser = CollisionTraverser() - self.collision_queue = CollisionHandlerQueue() - - # 射线投射节点 - self.left_ray_node = None - self.right_ray_node = None - self.ray_collision_nodes = {} - - # 选择和抓取状态 - self.selected_objects = {} # 控制器 -> 选中对象 - self.grabbed_objects = {} # 控制器 -> 抓取对象 - self.grab_offsets = {} # 控制器 -> 抓取偏移 - - # 交互参数 - self.selection_range = 50.0 # 选择距离 - self.grab_threshold = 0.5 # 抓取扳机阈值 - self.selection_color = Vec4(0.9, 0.9, 0.2, 1.0) # 选择高亮颜色 - self.grab_color = Vec4(0.2, 0.9, 0.2, 1.0) # 抓取高亮颜色 - - # 高亮状态 - self.highlighted_objects = set() - self.original_colors = {} # 存储对象原始颜色 - - print("✓ VR交互管理器初始化完成") - - def initialize(self): - """初始化交互系统""" - try: - print("🔧 正在初始化VR交互系统...") - - # 创建射线投射节点 - self._create_ray_casters() - - # 设置碰撞检测 - self._setup_collision_detection() - - print("✅ VR交互系统初始化成功") - return True - - except Exception as e: - print(f"❌ VR交互系统初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def _create_ray_casters(self): - """创建射线投射节点""" - # 为左手控制器创建射线 - if self.vr_manager.left_controller and self.vr_manager.left_controller.anchor_node: - self.left_ray_node = self._create_controller_ray('left', self.vr_manager.left_controller.anchor_node) - - # 为右手控制器创建射线 - if self.vr_manager.right_controller and self.vr_manager.right_controller.anchor_node: - self.right_ray_node = self._create_controller_ray('right', self.vr_manager.right_controller.anchor_node) - - def _create_controller_ray(self, controller_name, anchor_node): - """为控制器创建射线投射节点""" - # 创建射线碰撞体 - ray = CollisionRay() - ray.setOrigin(0, 0, 0) # 从控制器原点开始 - ray.setDirection(0, 1, 0) # 沿Y轴正方向 - - # 创建碰撞节点 - ray_collision_node = CollisionNode(f'{controller_name}_ray') - ray_collision_node.addSolid(ray) - - # 设置碰撞掩码 - ray_collision_node.setFromCollideMask(BitMask32.bit(0)) # 射线掩码 - ray_collision_node.setIntoCollideMask(BitMask32.allOff()) # 不接受碰撞 - - # 附加到控制器锚点 - ray_node = anchor_node.attachNewNode(ray_collision_node) - self.ray_collision_nodes[controller_name] = ray_collision_node - - # 注册到碰撞遍历器 - self.collision_traverser.addCollider(ray_node, self.collision_queue) - - print(f"✓ {controller_name}手控制器射线投射已创建") - return ray_node - - def _setup_collision_detection(self): - """设置碰撞检测系统""" - if self.world: - # 使用世界的碰撞系统 - if hasattr(self.world, 'render'): - # 为所有可交互对象设置碰撞体 - self._setup_scene_collision_objects() - else: - print("⚠️ 无法访问世界对象,跳过场景碰撞设置") - - def _setup_scene_collision_objects(self): - """为场景对象设置碰撞体""" - if not self.world or not hasattr(self.world, 'render'): - return - - try: - # 遍历场景中的所有节点,为它们添加碰撞体 - for node_path in self.world.render.findAllMatches("**/+GeomNode"): - self._add_collision_to_object(node_path) - - except Exception as e: - print(f"⚠️ 设置场景碰撞对象失败: {e}") - - def _add_collision_to_object(self, node_path): - """为对象添加碰撞体""" - try: - # 获取对象的边界框 - bounds = node_path.getBounds() - if bounds.isEmpty(): - return - - # 计算边界球 - center = bounds.getCenter() - radius = bounds.getRadius() - - # 创建球形碰撞体 - collision_sphere = CollisionSphere(center, radius) - - # 创建碰撞节点 - collision_node = CollisionNode(f'{node_path.getName()}_collision') - collision_node.addSolid(collision_sphere) - - # 设置碰撞掩码 - collision_node.setIntoCollideMask(BitMask32.bit(0)) # 接受射线碰撞 - collision_node.setFromCollideMask(BitMask32.allOff()) # 不发射射线 - - # 附加碰撞节点 - collision_node_path = node_path.attachNewNode(collision_node) - - # 标记为可交互对象 - node_path.setTag('interactable', 'true') - node_path.setTag('original_name', node_path.getName()) - - except Exception as e: - print(f"⚠️ 为对象 {node_path.getName()} 添加碰撞体失败: {e}") - - def update(self): - """更新交互系统 - 每帧调用""" - if not self.vr_manager.are_controllers_connected(): - return - - # 执行碰撞检测 - self._perform_collision_detection() - - # 更新选择状态 - self._update_selections() - - # 更新抓取状态 - self._update_grabbing() - - def _perform_collision_detection(self): - """执行碰撞检测""" - if self.world and hasattr(self.world, 'render'): - self.collision_traverser.traverse(self.world.render) - - def _update_selections(self): - """更新对象选择状态""" - # 清除之前的选择高亮 - self._clear_selection_highlights() - - # 检查每个控制器的选择 - for controller in self.vr_manager.get_connected_controllers(): - if not controller: - continue - - # 获取最近的碰撞对象 - hit_object = self._get_closest_hit_object(controller.name) - - if hit_object: - # 高亮选中的对象 - self._highlight_object(hit_object, self.selection_color) - self.selected_objects[controller.name] = hit_object - - # 显示控制器射线 - controller.show_ray(True) - controller.set_ray_color([0.9, 0.9, 0.2, 0.8]) # 黄色 - else: - # 没有选中对象 - if controller.name in self.selected_objects: - del self.selected_objects[controller.name] - - # 隐藏射线(除非正在抓取) - if controller.name not in self.grabbed_objects: - controller.show_ray(False) - - def _get_closest_hit_object(self, controller_name): - """获取指定控制器射线最近的碰撞对象""" - if controller_name not in self.ray_collision_nodes: - return None - - closest_object = None - closest_distance = float('inf') - - # 检查碰撞队列中的条目 - for i in range(self.collision_queue.getNumEntries()): - entry = self.collision_queue.getEntry(i) - - # 检查是否是该控制器的射线 - from_node = entry.getFromNodePath() - if from_node.node() == self.ray_collision_nodes[controller_name]: - # 获取碰撞的对象 - hit_node_path = entry.getIntoNodePath() - - # 获取实际的几何对象(父节点) - geom_object = hit_node_path.getParent() - if geom_object and geom_object.hasTag('interactable'): - distance = entry.getSurfacePoint(geom_object).length() - - if distance < closest_distance and distance <= self.selection_range: - closest_distance = distance - closest_object = geom_object - - return closest_object - - def _update_grabbing(self): - """更新对象抓取状态""" - for controller in self.vr_manager.get_connected_controllers(): - if not controller: - continue - - controller_name = controller.name - - # 检查是否按下抓取按钮 - if controller.is_trigger_pressed(threshold=self.grab_threshold): - # 如果还没有抓取对象 - if controller_name not in self.grabbed_objects: - # 尝试抓取选中的对象 - if controller_name in self.selected_objects: - selected_obj = self.selected_objects[controller_name] - self._start_grab(controller, selected_obj) - - # 如果正在抓取,更新对象位置 - if controller_name in self.grabbed_objects: - self._update_grabbed_object(controller) - - else: - # 释放抓取 - if controller_name in self.grabbed_objects: - self._release_grab(controller) - - def _start_grab(self, controller, obj): - """开始抓取对象""" - controller_name = controller.name - - try: - # 计算抓取偏移(对象相对于控制器的位置) - controller_pos = controller.get_world_position() - object_pos = obj.getPos(self.world.render if self.world else obj.getParent()) - - offset = object_pos - controller_pos - self.grab_offsets[controller_name] = offset - - # 记录抓取状态 - self.grabbed_objects[controller_name] = obj - - # 改变对象颜色表示抓取状态 - self._highlight_object(obj, self.grab_color) - - # 触发震动反馈 - controller.trigger_haptic_feedback(0.01, 0.8) - - # 显示绿色射线表示抓取 - controller.show_ray(True) - controller.set_ray_color([0.2, 0.9, 0.2, 0.8]) - - print(f"🤏 {controller_name}手开始抓取对象: {obj.getName()}") - - except Exception as e: - print(f"⚠️ 开始抓取失败: {e}") - - def _update_grabbed_object(self, controller): - """更新被抓取对象的位置""" - controller_name = controller.name - - if controller_name not in self.grabbed_objects: - return - - try: - grabbed_obj = self.grabbed_objects[controller_name] - grab_offset = self.grab_offsets.get(controller_name, Vec3(0, 0, 0)) - - # 计算新位置 - controller_pos = controller.get_world_position() - new_pos = controller_pos + grab_offset - - # 更新对象位置 - grabbed_obj.setPos(self.world.render if self.world else grabbed_obj.getParent(), new_pos) - - # 可选:同步旋转 - if hasattr(controller, 'get_world_rotation'): - controller_rot = controller.get_world_rotation() - grabbed_obj.setHpr(self.world.render if self.world else grabbed_obj.getParent(), controller_rot) - - except Exception as e: - print(f"⚠️ 更新抓取对象失败: {e}") - - def _release_grab(self, controller): - """释放抓取的对象""" - controller_name = controller.name - - if controller_name not in self.grabbed_objects: - return - - try: - grabbed_obj = self.grabbed_objects[controller_name] - - # 恢复对象原始颜色 - self._restore_object_color(grabbed_obj) - - # 清理抓取状态 - del self.grabbed_objects[controller_name] - if controller_name in self.grab_offsets: - del self.grab_offsets[controller_name] - - # 触发震动反馈 - controller.trigger_haptic_feedback(0.005, 0.4) - - print(f"🫳 {controller_name}手释放对象: {grabbed_obj.getName()}") - - except Exception as e: - print(f"⚠️ 释放抓取失败: {e}") - - def _highlight_object(self, obj, color): - """高亮显示对象""" - if obj in self.highlighted_objects: - return - - try: - # 保存原始颜色 - if obj not in self.original_colors: - self.original_colors[obj] = obj.getColor() - - # 设置高亮颜色 - obj.setColor(color) - self.highlighted_objects.add(obj) - - except Exception as e: - print(f"⚠️ 高亮对象失败: {e}") - - def _restore_object_color(self, obj): - """恢复对象原始颜色""" - if obj not in self.highlighted_objects: - return - - try: - # 恢复原始颜色 - if obj in self.original_colors: - obj.setColor(self.original_colors[obj]) - del self.original_colors[obj] - - self.highlighted_objects.discard(obj) - - except Exception as e: - print(f"⚠️ 恢复对象颜色失败: {e}") - - def _clear_selection_highlights(self): - """清除所有选择高亮""" - for obj in list(self.highlighted_objects): - # 只清除非抓取状态的对象 - is_grabbed = any(obj == grabbed_obj for grabbed_obj in self.grabbed_objects.values()) - if not is_grabbed: - self._restore_object_color(obj) - - def get_selected_object(self, controller_name): - """获取指定控制器选中的对象""" - return self.selected_objects.get(controller_name) - - def get_grabbed_object(self, controller_name): - """获取指定控制器抓取的对象""" - return self.grabbed_objects.get(controller_name) - - def is_grabbing(self, controller_name): - """检查指定控制器是否正在抓取对象""" - return controller_name in self.grabbed_objects - - def force_release_all(self): - """强制释放所有抓取的对象""" - for controller in self.vr_manager.get_connected_controllers(): - if controller and controller.name in self.grabbed_objects: - self._release_grab(controller) - - def cleanup(self): - """清理资源""" - self.ignoreAll() - - # 释放所有抓取 - self.force_release_all() - - # 清理碰撞系统 - self.collision_traverser.clearColliders() - self.ray_collision_nodes.clear() - - # 清理高亮状态 - for obj in list(self.highlighted_objects): - self._restore_object_color(obj) - +""" +VR交互系统模块 + +提供VR手柄与3D场景的交互功能: +- 射线投射和碰撞检测 +- 对象选择和高亮 +- 对象抓取和移动 +- UI交互 +- 距离抓取 +""" + +from panda3d.core import ( + Vec3, Vec4, Mat4, Point3, CollisionRay, CollisionTraverser, + CollisionNode, CollisionHandlerQueue, BitMask32, NodePath, + CollisionSphere, CollisionTube, RenderState, TransparencyAttrib, + ColorAttrib +) +from direct.showbase.DirectObject import DirectObject + + +class VRInteractionManager(DirectObject): + """VR交互管理器 - 处理手柄与场景的交互""" + + def __init__(self, vr_manager): + """初始化VR交互管理器 + + Args: + vr_manager: VR管理器实例 + """ + super().__init__() + + self.vr_manager = vr_manager + self.world = vr_manager.world if hasattr(vr_manager, 'world') else None + + # 碰撞检测系统 + self.collision_traverser = CollisionTraverser() + self.collision_queue = CollisionHandlerQueue() + + # 射线投射节点 + self.left_ray_node = None + self.right_ray_node = None + self.ray_collision_nodes = {} + + # 选择和抓取状态 + self.selected_objects = {} # 控制器 -> 选中对象 + self.grabbed_objects = {} # 控制器 -> 抓取对象 + self.grab_offsets = {} # 控制器 -> 抓取偏移 + + # 交互参数 + self.selection_range = 50.0 # 选择距离 + self.grab_threshold = 0.5 # 抓取扳机阈值 + self.selection_color = Vec4(0.9, 0.9, 0.2, 1.0) # 选择高亮颜色 + self.grab_color = Vec4(0.2, 0.9, 0.2, 1.0) # 抓取高亮颜色 + + # 高亮状态 + self.highlighted_objects = set() + self.original_colors = {} # 存储对象原始颜色 + + print("✓ VR交互管理器初始化完成") + + def initialize(self): + """初始化交互系统""" + try: + print("🔧 正在初始化VR交互系统...") + + # 创建射线投射节点 + self._create_ray_casters() + + # 设置碰撞检测 + self._setup_collision_detection() + + print("✅ VR交互系统初始化成功") + return True + + except Exception as e: + print(f"❌ VR交互系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_ray_casters(self): + """创建射线投射节点""" + # 为左手控制器创建射线 + if self.vr_manager.left_controller and self.vr_manager.left_controller.anchor_node: + self.left_ray_node = self._create_controller_ray('left', self.vr_manager.left_controller.anchor_node) + + # 为右手控制器创建射线 + if self.vr_manager.right_controller and self.vr_manager.right_controller.anchor_node: + self.right_ray_node = self._create_controller_ray('right', self.vr_manager.right_controller.anchor_node) + + def _create_controller_ray(self, controller_name, anchor_node): + """为控制器创建射线投射节点""" + # 创建射线碰撞体 + ray = CollisionRay() + ray.setOrigin(0, 0, 0) # 从控制器原点开始 + ray.setDirection(0, 1, 0) # 沿Y轴正方向 + + # 创建碰撞节点 + ray_collision_node = CollisionNode(f'{controller_name}_ray') + ray_collision_node.addSolid(ray) + + # 设置碰撞掩码 + ray_collision_node.setFromCollideMask(BitMask32.bit(0)) # 射线掩码 + ray_collision_node.setIntoCollideMask(BitMask32.allOff()) # 不接受碰撞 + + # 附加到控制器锚点 + ray_node = anchor_node.attachNewNode(ray_collision_node) + self.ray_collision_nodes[controller_name] = ray_collision_node + + # 注册到碰撞遍历器 + self.collision_traverser.addCollider(ray_node, self.collision_queue) + + print(f"✓ {controller_name}手控制器射线投射已创建") + return ray_node + + def _setup_collision_detection(self): + """设置碰撞检测系统""" + if self.world: + # 使用世界的碰撞系统 + if hasattr(self.world, 'render'): + # 为所有可交互对象设置碰撞体 + self._setup_scene_collision_objects() + else: + print("⚠️ 无法访问世界对象,跳过场景碰撞设置") + + def _setup_scene_collision_objects(self): + """为场景对象设置碰撞体""" + if not self.world or not hasattr(self.world, 'render'): + return + + try: + # 遍历场景中的所有节点,为它们添加碰撞体 + for node_path in self.world.render.findAllMatches("**/+GeomNode"): + self._add_collision_to_object(node_path) + + except Exception as e: + print(f"⚠️ 设置场景碰撞对象失败: {e}") + + def _add_collision_to_object(self, node_path): + """为对象添加碰撞体""" + try: + # 获取对象的边界框 + bounds = node_path.getBounds() + if bounds.isEmpty(): + return + + # 计算边界球 + center = bounds.getCenter() + radius = bounds.getRadius() + + # 创建球形碰撞体 + collision_sphere = CollisionSphere(center, radius) + + # 创建碰撞节点 + collision_node = CollisionNode(f'{node_path.getName()}_collision') + collision_node.addSolid(collision_sphere) + + # 设置碰撞掩码 + collision_node.setIntoCollideMask(BitMask32.bit(0)) # 接受射线碰撞 + collision_node.setFromCollideMask(BitMask32.allOff()) # 不发射射线 + + # 附加碰撞节点 + collision_node_path = node_path.attachNewNode(collision_node) + + # 标记为可交互对象 + node_path.setTag('interactable', 'true') + node_path.setTag('original_name', node_path.getName()) + + except Exception as e: + print(f"⚠️ 为对象 {node_path.getName()} 添加碰撞体失败: {e}") + + def update(self): + """更新交互系统 - 每帧调用""" + if not self.vr_manager.are_controllers_connected(): + return + + # 执行碰撞检测 + self._perform_collision_detection() + + # 更新选择状态 + self._update_selections() + + # 更新抓取状态 + self._update_grabbing() + + def _perform_collision_detection(self): + """执行碰撞检测""" + if self.world and hasattr(self.world, 'render'): + self.collision_traverser.traverse(self.world.render) + + def _update_selections(self): + """更新对象选择状态""" + # 清除之前的选择高亮 + self._clear_selection_highlights() + + # 检查每个控制器的选择 + for controller in self.vr_manager.get_connected_controllers(): + if not controller: + continue + + # 获取最近的碰撞对象 + hit_object = self._get_closest_hit_object(controller.name) + + if hit_object: + # 高亮选中的对象 + self._highlight_object(hit_object, self.selection_color) + self.selected_objects[controller.name] = hit_object + + # 显示控制器射线 + controller.show_ray(True) + controller.set_ray_color([0.9, 0.9, 0.2, 0.8]) # 黄色 + else: + # 没有选中对象 + if controller.name in self.selected_objects: + del self.selected_objects[controller.name] + + # 隐藏射线(除非正在抓取) + if controller.name not in self.grabbed_objects: + controller.show_ray(False) + + def _get_closest_hit_object(self, controller_name): + """获取指定控制器射线最近的碰撞对象""" + if controller_name not in self.ray_collision_nodes: + return None + + closest_object = None + closest_distance = float('inf') + + # 检查碰撞队列中的条目 + for i in range(self.collision_queue.getNumEntries()): + entry = self.collision_queue.getEntry(i) + + # 检查是否是该控制器的射线 + from_node = entry.getFromNodePath() + if from_node.node() == self.ray_collision_nodes[controller_name]: + # 获取碰撞的对象 + hit_node_path = entry.getIntoNodePath() + + # 获取实际的几何对象(父节点) + geom_object = hit_node_path.getParent() + if geom_object and geom_object.hasTag('interactable'): + distance = entry.getSurfacePoint(geom_object).length() + + if distance < closest_distance and distance <= self.selection_range: + closest_distance = distance + closest_object = geom_object + + return closest_object + + def _update_grabbing(self): + """更新对象抓取状态""" + for controller in self.vr_manager.get_connected_controllers(): + if not controller: + continue + + controller_name = controller.name + + # 检查是否按下抓取按钮 + if controller.is_trigger_pressed(threshold=self.grab_threshold): + # 如果还没有抓取对象 + if controller_name not in self.grabbed_objects: + # 尝试抓取选中的对象 + if controller_name in self.selected_objects: + selected_obj = self.selected_objects[controller_name] + self._start_grab(controller, selected_obj) + + # 如果正在抓取,更新对象位置 + if controller_name in self.grabbed_objects: + self._update_grabbed_object(controller) + + else: + # 释放抓取 + if controller_name in self.grabbed_objects: + self._release_grab(controller) + + def _start_grab(self, controller, obj): + """开始抓取对象""" + controller_name = controller.name + + try: + # 计算抓取偏移(对象相对于控制器的位置) + controller_pos = controller.get_world_position() + object_pos = obj.getPos(self.world.render if self.world else obj.getParent()) + + offset = object_pos - controller_pos + self.grab_offsets[controller_name] = offset + + # 记录抓取状态 + self.grabbed_objects[controller_name] = obj + + # 改变对象颜色表示抓取状态 + self._highlight_object(obj, self.grab_color) + + # 触发震动反馈 + controller.trigger_haptic_feedback(0.01, 0.8) + + # 显示绿色射线表示抓取 + controller.show_ray(True) + controller.set_ray_color([0.2, 0.9, 0.2, 0.8]) + + print(f"🤏 {controller_name}手开始抓取对象: {obj.getName()}") + + except Exception as e: + print(f"⚠️ 开始抓取失败: {e}") + + def _update_grabbed_object(self, controller): + """更新被抓取对象的位置""" + controller_name = controller.name + + if controller_name not in self.grabbed_objects: + return + + try: + grabbed_obj = self.grabbed_objects[controller_name] + grab_offset = self.grab_offsets.get(controller_name, Vec3(0, 0, 0)) + + # 计算新位置 + controller_pos = controller.get_world_position() + new_pos = controller_pos + grab_offset + + # 更新对象位置 + grabbed_obj.setPos(self.world.render if self.world else grabbed_obj.getParent(), new_pos) + + # 可选:同步旋转 + if hasattr(controller, 'get_world_rotation'): + controller_rot = controller.get_world_rotation() + grabbed_obj.setHpr(self.world.render if self.world else grabbed_obj.getParent(), controller_rot) + + except Exception as e: + print(f"⚠️ 更新抓取对象失败: {e}") + + def _release_grab(self, controller): + """释放抓取的对象""" + controller_name = controller.name + + if controller_name not in self.grabbed_objects: + return + + try: + grabbed_obj = self.grabbed_objects[controller_name] + + # 恢复对象原始颜色 + self._restore_object_color(grabbed_obj) + + # 清理抓取状态 + del self.grabbed_objects[controller_name] + if controller_name in self.grab_offsets: + del self.grab_offsets[controller_name] + + # 触发震动反馈 + controller.trigger_haptic_feedback(0.005, 0.4) + + print(f"🫳 {controller_name}手释放对象: {grabbed_obj.getName()}") + + except Exception as e: + print(f"⚠️ 释放抓取失败: {e}") + + def _highlight_object(self, obj, color): + """高亮显示对象""" + if obj in self.highlighted_objects: + return + + try: + # 保存原始颜色 + if obj not in self.original_colors: + self.original_colors[obj] = obj.getColor() + + # 设置高亮颜色 + obj.setColor(color) + self.highlighted_objects.add(obj) + + except Exception as e: + print(f"⚠️ 高亮对象失败: {e}") + + def _restore_object_color(self, obj): + """恢复对象原始颜色""" + if obj not in self.highlighted_objects: + return + + try: + # 恢复原始颜色 + if obj in self.original_colors: + obj.setColor(self.original_colors[obj]) + del self.original_colors[obj] + + self.highlighted_objects.discard(obj) + + except Exception as e: + print(f"⚠️ 恢复对象颜色失败: {e}") + + def _clear_selection_highlights(self): + """清除所有选择高亮""" + for obj in list(self.highlighted_objects): + # 只清除非抓取状态的对象 + is_grabbed = any(obj == grabbed_obj for grabbed_obj in self.grabbed_objects.values()) + if not is_grabbed: + self._restore_object_color(obj) + + def get_selected_object(self, controller_name): + """获取指定控制器选中的对象""" + return self.selected_objects.get(controller_name) + + def get_grabbed_object(self, controller_name): + """获取指定控制器抓取的对象""" + return self.grabbed_objects.get(controller_name) + + def is_grabbing(self, controller_name): + """检查指定控制器是否正在抓取对象""" + return controller_name in self.grabbed_objects + + def force_release_all(self): + """强制释放所有抓取的对象""" + for controller in self.vr_manager.get_connected_controllers(): + if controller and controller.name in self.grabbed_objects: + self._release_grab(controller) + + def cleanup(self): + """清理资源""" + self.ignoreAll() + + # 释放所有抓取 + self.force_release_all() + + # 清理碰撞系统 + self.collision_traverser.clearColliders() + self.ray_collision_nodes.clear() + + # 清理高亮状态 + for obj in list(self.highlighted_objects): + self._restore_object_color(obj) + print("🧹 VR交互管理器已清理") \ No newline at end of file diff --git a/core/vr/interaction/joystick.py b/core/vr/interaction/joystick.py index feaa1cae..21ca2b6d 100644 --- a/core/vr/interaction/joystick.py +++ b/core/vr/interaction/joystick.py @@ -1,701 +1,701 @@ -""" -VR摇杆交互系统模块 - -提供类似SteamVR的摇杆交互功能: -- 摇杆左右转向(旋转视角) -- 摇杆向前传送预览(抛物线轨迹) -- 松开摇杆执行传送 -- 死区处理和平滑控制 -""" - -import math -from panda3d.core import Vec2, Vec3, Vec4 -from direct.showbase.DirectObject import DirectObject -try: - import openvr - OPENVR_AVAILABLE = True -except ImportError: - OPENVR_AVAILABLE = False - - -class VRJoystickManager(DirectObject): - """VR摇杆管理器 - 处理手柄摇杆的转向和传送功能""" - - def __init__(self, vr_manager): - """初始化VR摇杆管理器 - - Args: - vr_manager: VR管理器实例 - """ - super().__init__() - - self.vr_manager = vr_manager - self.teleport_system = None # 传送系统引用,稍后初始化 - - # 摇杆参数配置 - self.deadzone = 0.15 # 摇杆死区 (0-1) - self.turn_threshold = 0.3 # 转向激活阈值 - self.teleport_threshold = 0.5 # 传送激活阈值 - self.turn_sensitivity = 250.0 # 转向灵敏度(度/秒)- 增加速度 - self.smooth_turning = True # 是否平滑转向 - self.snap_turn_angle = 30.0 # 分段转向角度(度) - - # 摇杆状态跟踪 - self.left_joystick_state = JoystickState() - self.right_joystick_state = JoystickState() - - # 转向状态 - self.left_turn_cooldown = 0.0 # 分段转向冷却时间 - self.right_turn_cooldown = 0.0 - self.snap_turn_cooldown = 0.3 # 分段转向间隔(秒) - - # 传送状态 - self.active_teleport_controller = None # 正在传送的控制器 - self.teleport_preview_active = False # 传送预览是否激活 - - # 互斥状态管理 - 防止同时触发多种操作 - self.interaction_mode = 'none' # 当前交互模式: 'none', 'turning', 'teleporting' - self.left_controller_mode = 'none' # 左手控制器状态 - self.right_controller_mode = 'none' # 右手控制器状态 - self.mode_lock_timeout = 0.1 # 模式锁定超时时间(秒) - self.left_mode_timer = 0.0 # 左手模式计时器 - self.right_mode_timer = 0.0 # 右手模式计时器 - - print("✓ VR摇杆管理器初始化完成") - - def initialize(self, teleport_system): - """初始化摇杆系统 - - Args: - teleport_system: VR传送系统实例 - """ - self.teleport_system = teleport_system - print("✅ VR摇杆系统初始化成功") - - def update(self, dt): - """更新摇杆系统 - 每帧调用 - - Args: - dt: 帧间隔时间(秒) - """ - if not self.vr_manager.are_controllers_connected(): - return - - # 调试计数器 - if not hasattr(self, '_debug_frame_count'): - self._debug_frame_count = 0 - print("🎮 VR摇杆系统开始更新") - - self._debug_frame_count += 1 - - # 更新转向冷却时间 - if self.left_turn_cooldown > 0: - self.left_turn_cooldown -= dt - if self.right_turn_cooldown > 0: - self.right_turn_cooldown -= dt - - # 更新互斥状态计时器 - self._update_interaction_modes(dt) - - # 处理左手控制器摇杆 - if self.vr_manager.left_controller: - self._update_controller_joystick( - self.vr_manager.left_controller, - self.left_joystick_state, - 'left', - dt - ) - - # 处理右手控制器摇杆 - if self.vr_manager.right_controller: - self._update_controller_joystick( - self.vr_manager.right_controller, - self.right_joystick_state, - 'right', - dt - ) - - # 每5秒输出一次状态报告 - if self._debug_frame_count % 300 == 1: # 假设60fps - self._print_debug_status() - - def _update_controller_joystick(self, controller, joystick_state, hand, dt): - """更新单个控制器的摇杆状态 - - Args: - controller: VR控制器实例 - joystick_state: 摇杆状态对象 - hand: 'left' 或 'right' - dt: 帧间隔时间 - """ - # 获取摇杆输入 - joystick_input = self._get_joystick_input(controller, hand) - if joystick_input is None: - return - - # 应用死区 - filtered_input = self._apply_deadzone(joystick_input) - - # 更新摇杆状态 - joystick_state.update(filtered_input) - - # 处理转向(左右移动) - self._handle_turning(filtered_input, hand, dt) - - # 处理传送(向前移动) - self._handle_teleport(controller, filtered_input, joystick_state, hand) - - def _update_interaction_modes(self, dt): - """更新互斥交互模式状态""" - # 更新左手模式计时器 - if self.left_mode_timer > 0: - self.left_mode_timer -= dt - if self.left_mode_timer <= 0: - self.left_controller_mode = 'none' - - # 更新右手模式计时器 - if self.right_mode_timer > 0: - self.right_mode_timer -= dt - if self.right_mode_timer <= 0: - self.right_controller_mode = 'none' - - # 更新全局交互模式 - if self.left_controller_mode == 'none' and self.right_controller_mode == 'none': - if self.interaction_mode != 'none': - # 所有操作结束,恢复自由模式 - self.interaction_mode = 'none' - print("🔓 摇杆交互模式解锁,恢复自由操作") - - def _set_controller_mode(self, hand, mode): - """设置控制器交互模式 - - Args: - hand: 'left' 或 'right' - mode: 'turning', 'teleporting', 'none' - """ - if hand == 'left': - if self.left_controller_mode != mode: - old_mode = self.left_controller_mode - self.left_controller_mode = mode - self.left_mode_timer = self.mode_lock_timeout - if mode != 'none': - print(f"🔒 左手控制器: {old_mode} → {mode}模式") - else: - print(f"🔓 左手控制器解锁: {old_mode} → 自由") - else: - if self.right_controller_mode != mode: - old_mode = self.right_controller_mode - self.right_controller_mode = mode - self.right_mode_timer = self.mode_lock_timeout - if mode != 'none': - print(f"🔒 右手控制器: {old_mode} → {mode}模式") - else: - print(f"🔓 右手控制器解锁: {old_mode} → 自由") - - # 更新全局模式 - if mode != 'none' and self.interaction_mode != mode: - self.interaction_mode = mode - - def _can_use_mode(self, hand, requested_mode): - """检查是否可以使用指定的交互模式 - - Args: - hand: 'left' 或 'right' - requested_mode: 'turning' 或 'teleporting' - - Returns: - bool: 是否可以使用该模式 - """ - current_mode = self.left_controller_mode if hand == 'left' else self.right_controller_mode - - # 如果当前控制器已经是该模式,允许继续 - if current_mode == requested_mode: - return True - - # 如果当前控制器是空闲的,且全局模式兼容,允许切换 - if current_mode == 'none': - if self.interaction_mode == 'none' or self.interaction_mode == requested_mode: - return True - - # 其他情况不允许 - return False - - def _get_joystick_input(self, controller, hand): - """获取摇杆输入 - - Args: - controller: VR控制器实例 - hand: 'left' 或 'right' - - Returns: - Vec2: 摇杆位置 (x, y) 或 None - """ - # 直接从控制器读取摇杆输入(绕过动作系统) - joystick_input = Vec2(0, 0) - - # 优先读取joystick_pos - if hasattr(controller, 'joystick_pos') and controller.joystick_pos: - joystick_input = Vec2(controller.joystick_pos.x, controller.joystick_pos.y) - # 检查是否有有效输入 - if joystick_input.length() > 0.01: - # 调试输出 - 仅在有输入时显示 - if not hasattr(self, '_last_debug_time'): - self._last_debug_time = 0 - self._debug_counter = 0 - - self._debug_counter += 1 - # 每30帧输出一次调试信息 - if self._debug_counter % 30 == 1: - print(f"🎮 {hand}手摇杆输入: ({joystick_input.x:.3f}, {joystick_input.y:.3f})") - - return joystick_input - - # 备选方案:读取touchpad_pos(Quest等设备) - if hasattr(controller, 'touchpad_pos') and controller.touchpad_pos: - touchpad_input = Vec2(controller.touchpad_pos.x, controller.touchpad_pos.y) - # 检查是否有有效输入 - if touchpad_input.length() > 0.01: - # 调试输出 - 仅在有输入时显示 - if not hasattr(self, '_last_debug_time'): - self._last_debug_time = 0 - self._debug_counter = 0 - - self._debug_counter += 1 - # 每30帧输出一次调试信息 - if self._debug_counter % 30 == 1: - print(f"🎮 {hand}手触摸板输入: ({touchpad_input.x:.3f}, {touchpad_input.y:.3f})") - - return touchpad_input - - # 可选:尝试从动作系统获取(如果可用) - if (self.vr_manager.action_manager and - hasattr(self.vr_manager.action_manager, 'get_analog_action_value')): - try: - device_path = f'/user/hand/{hand}' - - # 尝试摇杆 - joystick_value, _ = self.vr_manager.action_manager.get_analog_action_value('joystick', device_path) - if joystick_value is not None: - return Vec2(joystick_value.x, joystick_value.y) - - # 尝试触摸板 - trackpad_value, _ = self.vr_manager.action_manager.get_analog_action_value('trackpad', device_path) - if trackpad_value is not None: - return Vec2(trackpad_value.x, trackpad_value.y) - except Exception: - # 静默忽略动作系统错误 - pass - - return Vec2(0, 0) - - def _apply_deadzone(self, input_vec): - """应用摇杆死区 - - Args: - input_vec: 原始摇杆输入 - - Returns: - Vec2: 应用死区后的输入 - """ - magnitude = input_vec.length() - - if magnitude < self.deadzone: - return Vec2(0, 0) - - # 重新映射到 [0, 1] 范围 - normalized_magnitude = (magnitude - self.deadzone) / (1.0 - self.deadzone) - normalized_magnitude = min(normalized_magnitude, 1.0) - - if magnitude > 0: - direction = input_vec / magnitude - return direction * normalized_magnitude - - return Vec2(0, 0) - - def _handle_turning(self, input_vec, hand, dt): - """处理摇杆转向 - - Args: - input_vec: 摇杆输入向量 - hand: 'left' 或 'right' - dt: 帧间隔时间 - """ - # 检查是否超过转向阈值 - if abs(input_vec.x) < self.turn_threshold: - # 没有转向输入,重置该控制器的转向模式 - current_mode = self.left_controller_mode if hand == 'left' else self.right_controller_mode - if current_mode == 'turning': - self._set_controller_mode(hand, 'none') - return - - # 检查是否可以使用转向模式 - if not self._can_use_mode(hand, 'turning'): - # 当前控制器被传送锁定,忽略转向输入 - current_mode = self.left_controller_mode if hand == 'left' else self.right_controller_mode - if current_mode != 'turning': - print(f"⛔ {hand}手转向被阻止 - 当前模式: {current_mode}") - return - - # 激活转向模式 - self._set_controller_mode(hand, 'turning') - - # 检查冷却时间(分段转向) - cooldown = self.left_turn_cooldown if hand == 'left' else self.right_turn_cooldown - - if self.smooth_turning: - # 平滑转向 - 反转方向:摇杆右移(+x)应该向右转(-angle) - turn_amount = -input_vec.x * self.turn_sensitivity * dt - self._apply_rotation(turn_amount) - - # 调试输出转向 - if not hasattr(self, '_turn_debug_counter'): - self._turn_debug_counter = 0 - self._turn_debug_counter += 1 - if self._turn_debug_counter % 60 == 1: # 每秒输出一次 - print(f"🔄 {hand}手转向: 输入={input_vec.x:.3f}, 角度={turn_amount:.1f}°") - else: - # 分段转向 - if cooldown <= 0: - # 反转方向:摇杆右移应该向右转 - turn_amount = -self.snap_turn_angle if input_vec.x > 0 else self.snap_turn_angle - self._apply_rotation(turn_amount) - - print(f"🔄 {hand}手分段转向: 输入={input_vec.x:.3f}, 角度={turn_amount:.1f}°") - - # 设置冷却时间 - if hand == 'left': - self.left_turn_cooldown = self.snap_turn_cooldown - else: - self.right_turn_cooldown = self.snap_turn_cooldown - - def _apply_rotation(self, angle_degrees): - """应用旋转到VR跟踪空间 - - Args: - angle_degrees: 旋转角度(度) - """ - if not self.vr_manager.tracking_space: - return - - # 绕Z轴旋转(垂直轴) - current_h = self.vr_manager.tracking_space.getH() - new_h = current_h + angle_degrees - self.vr_manager.tracking_space.setH(new_h) - - def _handle_teleport(self, controller, input_vec, joystick_state, hand): - """处理摇杆传送 - - Args: - controller: VR控制器实例 - input_vec: 摇杆输入向量 - joystick_state: 摇杆状态对象 - hand: 'left' 或 'right' - """ - # 检查Y轴向前移动是否超过阈值 - forward_input = input_vec.y - - if forward_input > self.teleport_threshold: - # 检查是否可以使用传送模式 - if not self._can_use_mode(hand, 'teleporting'): - # 当前控制器被转向锁定,忽略传送输入 - current_mode = self.left_controller_mode if hand == 'left' else self.right_controller_mode - if current_mode != 'teleporting': - print(f"⛔ {hand}手传送被阻止 - 当前模式: {current_mode}") - return - - # 激活传送模式 - self._set_controller_mode(hand, 'teleporting') - - # 开始或更新传送预览 - if not joystick_state.teleport_active: - joystick_state.teleport_active = True - self.active_teleport_controller = controller - self.teleport_preview_active = True - - # 触发震动反馈 - controller.trigger_haptic_feedback(0.002, 0.3) - - # 计算传送方向 - direction = self._calculate_teleport_direction(controller, input_vec) - - # 更新传送预览 - if self.teleport_system: - if joystick_state.teleport_just_started: - self.teleport_system.start_teleport_preview(controller, direction) - joystick_state.teleport_just_started = False - else: - self.teleport_system.update_teleport_preview(controller, direction) - - else: - # 检查是否需要执行传送 - if joystick_state.teleport_active: - # 松开摇杆,执行传送 - self._execute_teleport(controller) - joystick_state.teleport_active = False - joystick_state.teleport_just_started = True - - # 重置该控制器的传送模式 - self._set_controller_mode(hand, 'none') - else: - # 没有传送输入,检查是否需要重置传送模式 - current_mode = self.left_controller_mode if hand == 'left' else self.right_controller_mode - if current_mode == 'teleporting': - self._set_controller_mode(hand, 'none') - - def _calculate_teleport_direction(self, controller, input_vec): - """计算传送方向向量 - 基于手柄姿态 - - Args: - controller: VR控制器实例 - input_vec: 摇杆输入向量(仅用于激活,不影响方向) - - Returns: - Vec3: 世界坐标中的传送方向 - """ - # 方法1:直接使用手柄指向(推荐) - if controller and hasattr(controller, 'get_forward_direction'): - try: - # 获取手柄的前向方向 - controller_forward = controller.get_forward_direction() - - # 确保方向向量在水平面上 - horizontal_direction = Vec3(controller_forward.x, controller_forward.y, 0) - - if horizontal_direction.length() > 0: - horizontal_direction.normalize() - - # 调试输出(每30帧输出一次) - if not hasattr(self, '_direction_debug_counter'): - self._direction_debug_counter = 0 - - self._direction_debug_counter += 1 - if self._direction_debug_counter % 30 == 1: - # 获取当前tracking_space的旋转角度用于调试 - tracking_rotation = 0 - if self.vr_manager.tracking_space: - tracking_rotation = self.vr_manager.tracking_space.getH() - - print(f"🎯 综合传送方向: 视角旋转({tracking_rotation:.1f}°) + 手柄姿态({controller_forward.x:.2f},{controller_forward.y:.2f},{controller_forward.z:.2f}) → 最终方向({horizontal_direction.x:.2f},{horizontal_direction.y:.2f})") - - return horizontal_direction - - except Exception as e: - print(f"⚠️ 获取手柄方向失败: {e}") - - # 备选方案:使用玩家朝向(保留原逻辑作为备选) - print("🔄 使用备选传送方向计算...") - - # 获取玩家当前朝向的变换矩阵 - player_transform = None - - # 优先使用头显的世界变换 - if self.vr_manager.hmd_anchor and hasattr(self.vr_manager, 'world') and self.vr_manager.world: - player_transform = self.vr_manager.hmd_anchor.getMat(self.vr_manager.world.render) - # 备选:使用tracking_space的世界变换 - elif self.vr_manager.tracking_space and hasattr(self.vr_manager, 'world') and self.vr_manager.world: - player_transform = self.vr_manager.tracking_space.getMat(self.vr_manager.world.render) - - if player_transform: - # 从变换矩阵提取前向向量 - forward = Vec3(player_transform.getRow3(1)) # Y轴 = 前向 - - # 确保向量在水平面上 - forward.z = 0 - if forward.length() > 0: - forward.normalize() - return forward - - # 最终备选方案:默认前向 - print("⚠️ 无法获取任何朝向,使用默认方向") - return Vec3(0, 1, 0) - - def _execute_teleport(self, controller): - """执行传送 - - Args: - controller: VR控制器实例 - """ - if self.teleport_system and self.teleport_preview_active: - success = self.teleport_system.execute_teleport() - - if success: - # 传送成功,触发震动反馈 - controller.trigger_haptic_feedback(0.005, 0.8) - else: - # 传送失败,触发不同的震动反馈 - controller.trigger_haptic_feedback(0.001, 0.2) - - # 停止传送预览 - self.teleport_system.stop_teleport_preview() - self.teleport_preview_active = False - self.active_teleport_controller = None - - def set_turning_mode(self, smooth=True): - """设置转向模式 - - Args: - smooth: True为平滑转向,False为分段转向 - """ - self.smooth_turning = smooth - print(f"✓ 转向模式设置为: {'平滑转向' if smooth else '分段转向'}") - - def set_turn_sensitivity(self, sensitivity): - """设置转向灵敏度 - - Args: - sensitivity: 转向灵敏度(度/秒) - """ - self.turn_sensitivity = max(10.0, min(180.0, sensitivity)) - print(f"✓ 转向灵敏度设置为: {self.turn_sensitivity}度/秒") - - def apply_config(self, config): - """应用配置 - - Args: - config: VRJoystickConfig配置实例 - """ - try: - self.deadzone = config.deadzone - self.turn_threshold = config.turn_threshold - self.teleport_threshold = config.teleport_threshold - self.turn_sensitivity = config.turn_sensitivity - self.smooth_turning = (config.turn_mode.value == 'smooth') - self.snap_turn_angle = config.snap_turn_angle - self.snap_turn_cooldown = config.snap_turn_cooldown - - # 应用传送系统配置 - if self.teleport_system and hasattr(config, 'teleport_range'): - self.teleport_system.teleport_range = config.teleport_range - if hasattr(config, 'teleport_arc_resolution'): - self.teleport_system.arc_resolution = config.teleport_arc_resolution - if hasattr(config, 'teleport_initial_velocity'): - self.teleport_system.initial_velocity = config.teleport_initial_velocity - if hasattr(config, 'min_teleport_distance'): - self.teleport_system.min_teleport_distance = config.min_teleport_distance - - print("✅ 摇杆配置已成功应用") - - except Exception as e: - print(f"⚠️ 应用配置失败: {e}") - - def get_current_config(self): - """获取当前配置 - - Returns: - dict: 当前配置参数 - """ - return { - 'deadzone': self.deadzone, - 'turn_threshold': self.turn_threshold, - 'teleport_threshold': self.teleport_threshold, - 'turn_sensitivity': self.turn_sensitivity, - 'smooth_turning': self.smooth_turning, - 'snap_turn_angle': self.snap_turn_angle, - 'snap_turn_cooldown': self.snap_turn_cooldown - } - - def _print_debug_status(self): - """打印调试状态信息""" - try: - print("🔍 ======= VR摇杆调试状态 =======") - - # 控制器连接状态 - left_connected = self.vr_manager.left_controller is not None - right_connected = self.vr_manager.right_controller is not None - print(f"📱 控制器状态: 左手={left_connected}, 右手={right_connected}") - - # 检查控制器属性 - if left_connected: - left_ctrl = self.vr_manager.left_controller - has_joystick = hasattr(left_ctrl, 'joystick_pos') - has_touchpad = hasattr(left_ctrl, 'touchpad_pos') - print(f"📊 左手控制器: joystick_pos={has_joystick}, touchpad_pos={has_touchpad}") - - if has_joystick and left_ctrl.joystick_pos: - pos = left_ctrl.joystick_pos - print(f" 当前摇杆位置: ({pos.x:.3f}, {pos.y:.3f}, {pos.z:.3f})") - - if has_touchpad and left_ctrl.touchpad_pos: - pos = left_ctrl.touchpad_pos - print(f" 当前触摸板位置: ({pos.x:.3f}, {pos.y:.3f}, {pos.z:.3f})") - - if right_connected: - right_ctrl = self.vr_manager.right_controller - has_joystick = hasattr(right_ctrl, 'joystick_pos') - has_touchpad = hasattr(right_ctrl, 'touchpad_pos') - print(f"📊 右手控制器: joystick_pos={has_joystick}, touchpad_pos={has_touchpad}") - - if has_joystick and right_ctrl.joystick_pos: - pos = right_ctrl.joystick_pos - print(f" 当前摇杆位置: ({pos.x:.3f}, {pos.y:.3f}, {pos.z:.3f})") - - if has_touchpad and right_ctrl.touchpad_pos: - pos = right_ctrl.touchpad_pos - print(f" 当前触摸板位置: ({pos.x:.3f}, {pos.y:.3f}, {pos.z:.3f})") - - # 摇杆配置 - print(f"⚙️ 摇杆配置:") - print(f" 死区: {self.deadzone}") - print(f" 转向阈值: {self.turn_threshold}") - print(f" 传送阈值: {self.teleport_threshold}") - print(f" 转向模式: {'平滑' if self.smooth_turning else '分段'}") - - # 动作系统状态 - action_mgr_available = (self.vr_manager.action_manager is not None) - print(f"🎯 动作系统: {'可用' if action_mgr_available else '不可用'}") - - # 传送系统状态 - teleport_available = (self.teleport_system is not None) - print(f"🚀 传送系统: {'可用' if teleport_available else '不可用'}") - - print("🔍 ==============================") - - except Exception as e: - print(f"⚠️ 调试状态输出失败: {e}") - - def cleanup(self): - """清理摇杆系统资源""" - try: - # 停止任何活跃的传送预览 - if self.teleport_preview_active and self.teleport_system: - self.teleport_system.stop_teleport_preview() - - self.teleport_preview_active = False - self.active_teleport_controller = None - - self.ignoreAll() - print("🧹 VR摇杆系统已清理") - - except Exception as e: - print(f"⚠️ 清理摇杆系统失败: {e}") - - -class JoystickState: - """摇杆状态跟踪类""" - - def __init__(self): - self.current_input = Vec2(0, 0) # 当前摇杆输入 - self.previous_input = Vec2(0, 0) # 上一帧摇杆输入 - self.teleport_active = False # 传送是否激活 - self.teleport_just_started = True # 传送是否刚开始 - - def update(self, new_input): - """更新摇杆状态 - - Args: - new_input: 新的摇杆输入 - """ - self.previous_input = Vec2(self.current_input) - self.current_input = Vec2(new_input) - - def is_input_changed(self, threshold=0.01): - """检查输入是否发生变化 - - Args: - threshold: 变化阈值 - - Returns: - bool: 是否发生变化 - """ - diff = self.current_input - self.previous_input +""" +VR摇杆交互系统模块 + +提供类似SteamVR的摇杆交互功能: +- 摇杆左右转向(旋转视角) +- 摇杆向前传送预览(抛物线轨迹) +- 松开摇杆执行传送 +- 死区处理和平滑控制 +""" + +import math +from panda3d.core import Vec2, Vec3, Vec4 +from direct.showbase.DirectObject import DirectObject +try: + import openvr + OPENVR_AVAILABLE = True +except ImportError: + OPENVR_AVAILABLE = False + + +class VRJoystickManager(DirectObject): + """VR摇杆管理器 - 处理手柄摇杆的转向和传送功能""" + + def __init__(self, vr_manager): + """初始化VR摇杆管理器 + + Args: + vr_manager: VR管理器实例 + """ + super().__init__() + + self.vr_manager = vr_manager + self.teleport_system = None # 传送系统引用,稍后初始化 + + # 摇杆参数配置 + self.deadzone = 0.15 # 摇杆死区 (0-1) + self.turn_threshold = 0.3 # 转向激活阈值 + self.teleport_threshold = 0.5 # 传送激活阈值 + self.turn_sensitivity = 250.0 # 转向灵敏度(度/秒)- 增加速度 + self.smooth_turning = True # 是否平滑转向 + self.snap_turn_angle = 30.0 # 分段转向角度(度) + + # 摇杆状态跟踪 + self.left_joystick_state = JoystickState() + self.right_joystick_state = JoystickState() + + # 转向状态 + self.left_turn_cooldown = 0.0 # 分段转向冷却时间 + self.right_turn_cooldown = 0.0 + self.snap_turn_cooldown = 0.3 # 分段转向间隔(秒) + + # 传送状态 + self.active_teleport_controller = None # 正在传送的控制器 + self.teleport_preview_active = False # 传送预览是否激活 + + # 互斥状态管理 - 防止同时触发多种操作 + self.interaction_mode = 'none' # 当前交互模式: 'none', 'turning', 'teleporting' + self.left_controller_mode = 'none' # 左手控制器状态 + self.right_controller_mode = 'none' # 右手控制器状态 + self.mode_lock_timeout = 0.1 # 模式锁定超时时间(秒) + self.left_mode_timer = 0.0 # 左手模式计时器 + self.right_mode_timer = 0.0 # 右手模式计时器 + + print("✓ VR摇杆管理器初始化完成") + + def initialize(self, teleport_system): + """初始化摇杆系统 + + Args: + teleport_system: VR传送系统实例 + """ + self.teleport_system = teleport_system + print("✅ VR摇杆系统初始化成功") + + def update(self, dt): + """更新摇杆系统 - 每帧调用 + + Args: + dt: 帧间隔时间(秒) + """ + if not self.vr_manager.are_controllers_connected(): + return + + # 调试计数器 + if not hasattr(self, '_debug_frame_count'): + self._debug_frame_count = 0 + print("🎮 VR摇杆系统开始更新") + + self._debug_frame_count += 1 + + # 更新转向冷却时间 + if self.left_turn_cooldown > 0: + self.left_turn_cooldown -= dt + if self.right_turn_cooldown > 0: + self.right_turn_cooldown -= dt + + # 更新互斥状态计时器 + self._update_interaction_modes(dt) + + # 处理左手控制器摇杆 + if self.vr_manager.left_controller: + self._update_controller_joystick( + self.vr_manager.left_controller, + self.left_joystick_state, + 'left', + dt + ) + + # 处理右手控制器摇杆 + if self.vr_manager.right_controller: + self._update_controller_joystick( + self.vr_manager.right_controller, + self.right_joystick_state, + 'right', + dt + ) + + # 每5秒输出一次状态报告 + if self._debug_frame_count % 300 == 1: # 假设60fps + self._print_debug_status() + + def _update_controller_joystick(self, controller, joystick_state, hand, dt): + """更新单个控制器的摇杆状态 + + Args: + controller: VR控制器实例 + joystick_state: 摇杆状态对象 + hand: 'left' 或 'right' + dt: 帧间隔时间 + """ + # 获取摇杆输入 + joystick_input = self._get_joystick_input(controller, hand) + if joystick_input is None: + return + + # 应用死区 + filtered_input = self._apply_deadzone(joystick_input) + + # 更新摇杆状态 + joystick_state.update(filtered_input) + + # 处理转向(左右移动) + self._handle_turning(filtered_input, hand, dt) + + # 处理传送(向前移动) + self._handle_teleport(controller, filtered_input, joystick_state, hand) + + def _update_interaction_modes(self, dt): + """更新互斥交互模式状态""" + # 更新左手模式计时器 + if self.left_mode_timer > 0: + self.left_mode_timer -= dt + if self.left_mode_timer <= 0: + self.left_controller_mode = 'none' + + # 更新右手模式计时器 + if self.right_mode_timer > 0: + self.right_mode_timer -= dt + if self.right_mode_timer <= 0: + self.right_controller_mode = 'none' + + # 更新全局交互模式 + if self.left_controller_mode == 'none' and self.right_controller_mode == 'none': + if self.interaction_mode != 'none': + # 所有操作结束,恢复自由模式 + self.interaction_mode = 'none' + print("🔓 摇杆交互模式解锁,恢复自由操作") + + def _set_controller_mode(self, hand, mode): + """设置控制器交互模式 + + Args: + hand: 'left' 或 'right' + mode: 'turning', 'teleporting', 'none' + """ + if hand == 'left': + if self.left_controller_mode != mode: + old_mode = self.left_controller_mode + self.left_controller_mode = mode + self.left_mode_timer = self.mode_lock_timeout + if mode != 'none': + print(f"🔒 左手控制器: {old_mode} → {mode}模式") + else: + print(f"🔓 左手控制器解锁: {old_mode} → 自由") + else: + if self.right_controller_mode != mode: + old_mode = self.right_controller_mode + self.right_controller_mode = mode + self.right_mode_timer = self.mode_lock_timeout + if mode != 'none': + print(f"🔒 右手控制器: {old_mode} → {mode}模式") + else: + print(f"🔓 右手控制器解锁: {old_mode} → 自由") + + # 更新全局模式 + if mode != 'none' and self.interaction_mode != mode: + self.interaction_mode = mode + + def _can_use_mode(self, hand, requested_mode): + """检查是否可以使用指定的交互模式 + + Args: + hand: 'left' 或 'right' + requested_mode: 'turning' 或 'teleporting' + + Returns: + bool: 是否可以使用该模式 + """ + current_mode = self.left_controller_mode if hand == 'left' else self.right_controller_mode + + # 如果当前控制器已经是该模式,允许继续 + if current_mode == requested_mode: + return True + + # 如果当前控制器是空闲的,且全局模式兼容,允许切换 + if current_mode == 'none': + if self.interaction_mode == 'none' or self.interaction_mode == requested_mode: + return True + + # 其他情况不允许 + return False + + def _get_joystick_input(self, controller, hand): + """获取摇杆输入 + + Args: + controller: VR控制器实例 + hand: 'left' 或 'right' + + Returns: + Vec2: 摇杆位置 (x, y) 或 None + """ + # 直接从控制器读取摇杆输入(绕过动作系统) + joystick_input = Vec2(0, 0) + + # 优先读取joystick_pos + if hasattr(controller, 'joystick_pos') and controller.joystick_pos: + joystick_input = Vec2(controller.joystick_pos.x, controller.joystick_pos.y) + # 检查是否有有效输入 + if joystick_input.length() > 0.01: + # 调试输出 - 仅在有输入时显示 + if not hasattr(self, '_last_debug_time'): + self._last_debug_time = 0 + self._debug_counter = 0 + + self._debug_counter += 1 + # 每30帧输出一次调试信息 + if self._debug_counter % 30 == 1: + print(f"🎮 {hand}手摇杆输入: ({joystick_input.x:.3f}, {joystick_input.y:.3f})") + + return joystick_input + + # 备选方案:读取touchpad_pos(Quest等设备) + if hasattr(controller, 'touchpad_pos') and controller.touchpad_pos: + touchpad_input = Vec2(controller.touchpad_pos.x, controller.touchpad_pos.y) + # 检查是否有有效输入 + if touchpad_input.length() > 0.01: + # 调试输出 - 仅在有输入时显示 + if not hasattr(self, '_last_debug_time'): + self._last_debug_time = 0 + self._debug_counter = 0 + + self._debug_counter += 1 + # 每30帧输出一次调试信息 + if self._debug_counter % 30 == 1: + print(f"🎮 {hand}手触摸板输入: ({touchpad_input.x:.3f}, {touchpad_input.y:.3f})") + + return touchpad_input + + # 可选:尝试从动作系统获取(如果可用) + if (self.vr_manager.action_manager and + hasattr(self.vr_manager.action_manager, 'get_analog_action_value')): + try: + device_path = f'/user/hand/{hand}' + + # 尝试摇杆 + joystick_value, _ = self.vr_manager.action_manager.get_analog_action_value('joystick', device_path) + if joystick_value is not None: + return Vec2(joystick_value.x, joystick_value.y) + + # 尝试触摸板 + trackpad_value, _ = self.vr_manager.action_manager.get_analog_action_value('trackpad', device_path) + if trackpad_value is not None: + return Vec2(trackpad_value.x, trackpad_value.y) + except Exception: + # 静默忽略动作系统错误 + pass + + return Vec2(0, 0) + + def _apply_deadzone(self, input_vec): + """应用摇杆死区 + + Args: + input_vec: 原始摇杆输入 + + Returns: + Vec2: 应用死区后的输入 + """ + magnitude = input_vec.length() + + if magnitude < self.deadzone: + return Vec2(0, 0) + + # 重新映射到 [0, 1] 范围 + normalized_magnitude = (magnitude - self.deadzone) / (1.0 - self.deadzone) + normalized_magnitude = min(normalized_magnitude, 1.0) + + if magnitude > 0: + direction = input_vec / magnitude + return direction * normalized_magnitude + + return Vec2(0, 0) + + def _handle_turning(self, input_vec, hand, dt): + """处理摇杆转向 + + Args: + input_vec: 摇杆输入向量 + hand: 'left' 或 'right' + dt: 帧间隔时间 + """ + # 检查是否超过转向阈值 + if abs(input_vec.x) < self.turn_threshold: + # 没有转向输入,重置该控制器的转向模式 + current_mode = self.left_controller_mode if hand == 'left' else self.right_controller_mode + if current_mode == 'turning': + self._set_controller_mode(hand, 'none') + return + + # 检查是否可以使用转向模式 + if not self._can_use_mode(hand, 'turning'): + # 当前控制器被传送锁定,忽略转向输入 + current_mode = self.left_controller_mode if hand == 'left' else self.right_controller_mode + if current_mode != 'turning': + print(f"⛔ {hand}手转向被阻止 - 当前模式: {current_mode}") + return + + # 激活转向模式 + self._set_controller_mode(hand, 'turning') + + # 检查冷却时间(分段转向) + cooldown = self.left_turn_cooldown if hand == 'left' else self.right_turn_cooldown + + if self.smooth_turning: + # 平滑转向 - 反转方向:摇杆右移(+x)应该向右转(-angle) + turn_amount = -input_vec.x * self.turn_sensitivity * dt + self._apply_rotation(turn_amount) + + # 调试输出转向 + if not hasattr(self, '_turn_debug_counter'): + self._turn_debug_counter = 0 + self._turn_debug_counter += 1 + if self._turn_debug_counter % 60 == 1: # 每秒输出一次 + print(f"🔄 {hand}手转向: 输入={input_vec.x:.3f}, 角度={turn_amount:.1f}°") + else: + # 分段转向 + if cooldown <= 0: + # 反转方向:摇杆右移应该向右转 + turn_amount = -self.snap_turn_angle if input_vec.x > 0 else self.snap_turn_angle + self._apply_rotation(turn_amount) + + print(f"🔄 {hand}手分段转向: 输入={input_vec.x:.3f}, 角度={turn_amount:.1f}°") + + # 设置冷却时间 + if hand == 'left': + self.left_turn_cooldown = self.snap_turn_cooldown + else: + self.right_turn_cooldown = self.snap_turn_cooldown + + def _apply_rotation(self, angle_degrees): + """应用旋转到VR跟踪空间 + + Args: + angle_degrees: 旋转角度(度) + """ + if not self.vr_manager.tracking_space: + return + + # 绕Z轴旋转(垂直轴) + current_h = self.vr_manager.tracking_space.getH() + new_h = current_h + angle_degrees + self.vr_manager.tracking_space.setH(new_h) + + def _handle_teleport(self, controller, input_vec, joystick_state, hand): + """处理摇杆传送 + + Args: + controller: VR控制器实例 + input_vec: 摇杆输入向量 + joystick_state: 摇杆状态对象 + hand: 'left' 或 'right' + """ + # 检查Y轴向前移动是否超过阈值 + forward_input = input_vec.y + + if forward_input > self.teleport_threshold: + # 检查是否可以使用传送模式 + if not self._can_use_mode(hand, 'teleporting'): + # 当前控制器被转向锁定,忽略传送输入 + current_mode = self.left_controller_mode if hand == 'left' else self.right_controller_mode + if current_mode != 'teleporting': + print(f"⛔ {hand}手传送被阻止 - 当前模式: {current_mode}") + return + + # 激活传送模式 + self._set_controller_mode(hand, 'teleporting') + + # 开始或更新传送预览 + if not joystick_state.teleport_active: + joystick_state.teleport_active = True + self.active_teleport_controller = controller + self.teleport_preview_active = True + + # 触发震动反馈 + controller.trigger_haptic_feedback(0.002, 0.3) + + # 计算传送方向 + direction = self._calculate_teleport_direction(controller, input_vec) + + # 更新传送预览 + if self.teleport_system: + if joystick_state.teleport_just_started: + self.teleport_system.start_teleport_preview(controller, direction) + joystick_state.teleport_just_started = False + else: + self.teleport_system.update_teleport_preview(controller, direction) + + else: + # 检查是否需要执行传送 + if joystick_state.teleport_active: + # 松开摇杆,执行传送 + self._execute_teleport(controller) + joystick_state.teleport_active = False + joystick_state.teleport_just_started = True + + # 重置该控制器的传送模式 + self._set_controller_mode(hand, 'none') + else: + # 没有传送输入,检查是否需要重置传送模式 + current_mode = self.left_controller_mode if hand == 'left' else self.right_controller_mode + if current_mode == 'teleporting': + self._set_controller_mode(hand, 'none') + + def _calculate_teleport_direction(self, controller, input_vec): + """计算传送方向向量 - 基于手柄姿态 + + Args: + controller: VR控制器实例 + input_vec: 摇杆输入向量(仅用于激活,不影响方向) + + Returns: + Vec3: 世界坐标中的传送方向 + """ + # 方法1:直接使用手柄指向(推荐) + if controller and hasattr(controller, 'get_forward_direction'): + try: + # 获取手柄的前向方向 + controller_forward = controller.get_forward_direction() + + # 确保方向向量在水平面上 + horizontal_direction = Vec3(controller_forward.x, controller_forward.y, 0) + + if horizontal_direction.length() > 0: + horizontal_direction.normalize() + + # 调试输出(每30帧输出一次) + if not hasattr(self, '_direction_debug_counter'): + self._direction_debug_counter = 0 + + self._direction_debug_counter += 1 + if self._direction_debug_counter % 30 == 1: + # 获取当前tracking_space的旋转角度用于调试 + tracking_rotation = 0 + if self.vr_manager.tracking_space: + tracking_rotation = self.vr_manager.tracking_space.getH() + + print(f"🎯 综合传送方向: 视角旋转({tracking_rotation:.1f}°) + 手柄姿态({controller_forward.x:.2f},{controller_forward.y:.2f},{controller_forward.z:.2f}) → 最终方向({horizontal_direction.x:.2f},{horizontal_direction.y:.2f})") + + return horizontal_direction + + except Exception as e: + print(f"⚠️ 获取手柄方向失败: {e}") + + # 备选方案:使用玩家朝向(保留原逻辑作为备选) + print("🔄 使用备选传送方向计算...") + + # 获取玩家当前朝向的变换矩阵 + player_transform = None + + # 优先使用头显的世界变换 + if self.vr_manager.hmd_anchor and hasattr(self.vr_manager, 'world') and self.vr_manager.world: + player_transform = self.vr_manager.hmd_anchor.getMat(self.vr_manager.world.render) + # 备选:使用tracking_space的世界变换 + elif self.vr_manager.tracking_space and hasattr(self.vr_manager, 'world') and self.vr_manager.world: + player_transform = self.vr_manager.tracking_space.getMat(self.vr_manager.world.render) + + if player_transform: + # 从变换矩阵提取前向向量 + forward = Vec3(player_transform.getRow3(1)) # Y轴 = 前向 + + # 确保向量在水平面上 + forward.z = 0 + if forward.length() > 0: + forward.normalize() + return forward + + # 最终备选方案:默认前向 + print("⚠️ 无法获取任何朝向,使用默认方向") + return Vec3(0, 1, 0) + + def _execute_teleport(self, controller): + """执行传送 + + Args: + controller: VR控制器实例 + """ + if self.teleport_system and self.teleport_preview_active: + success = self.teleport_system.execute_teleport() + + if success: + # 传送成功,触发震动反馈 + controller.trigger_haptic_feedback(0.005, 0.8) + else: + # 传送失败,触发不同的震动反馈 + controller.trigger_haptic_feedback(0.001, 0.2) + + # 停止传送预览 + self.teleport_system.stop_teleport_preview() + self.teleport_preview_active = False + self.active_teleport_controller = None + + def set_turning_mode(self, smooth=True): + """设置转向模式 + + Args: + smooth: True为平滑转向,False为分段转向 + """ + self.smooth_turning = smooth + print(f"✓ 转向模式设置为: {'平滑转向' if smooth else '分段转向'}") + + def set_turn_sensitivity(self, sensitivity): + """设置转向灵敏度 + + Args: + sensitivity: 转向灵敏度(度/秒) + """ + self.turn_sensitivity = max(10.0, min(180.0, sensitivity)) + print(f"✓ 转向灵敏度设置为: {self.turn_sensitivity}度/秒") + + def apply_config(self, config): + """应用配置 + + Args: + config: VRJoystickConfig配置实例 + """ + try: + self.deadzone = config.deadzone + self.turn_threshold = config.turn_threshold + self.teleport_threshold = config.teleport_threshold + self.turn_sensitivity = config.turn_sensitivity + self.smooth_turning = (config.turn_mode.value == 'smooth') + self.snap_turn_angle = config.snap_turn_angle + self.snap_turn_cooldown = config.snap_turn_cooldown + + # 应用传送系统配置 + if self.teleport_system and hasattr(config, 'teleport_range'): + self.teleport_system.teleport_range = config.teleport_range + if hasattr(config, 'teleport_arc_resolution'): + self.teleport_system.arc_resolution = config.teleport_arc_resolution + if hasattr(config, 'teleport_initial_velocity'): + self.teleport_system.initial_velocity = config.teleport_initial_velocity + if hasattr(config, 'min_teleport_distance'): + self.teleport_system.min_teleport_distance = config.min_teleport_distance + + print("✅ 摇杆配置已成功应用") + + except Exception as e: + print(f"⚠️ 应用配置失败: {e}") + + def get_current_config(self): + """获取当前配置 + + Returns: + dict: 当前配置参数 + """ + return { + 'deadzone': self.deadzone, + 'turn_threshold': self.turn_threshold, + 'teleport_threshold': self.teleport_threshold, + 'turn_sensitivity': self.turn_sensitivity, + 'smooth_turning': self.smooth_turning, + 'snap_turn_angle': self.snap_turn_angle, + 'snap_turn_cooldown': self.snap_turn_cooldown + } + + def _print_debug_status(self): + """打印调试状态信息""" + try: + print("🔍 ======= VR摇杆调试状态 =======") + + # 控制器连接状态 + left_connected = self.vr_manager.left_controller is not None + right_connected = self.vr_manager.right_controller is not None + print(f"📱 控制器状态: 左手={left_connected}, 右手={right_connected}") + + # 检查控制器属性 + if left_connected: + left_ctrl = self.vr_manager.left_controller + has_joystick = hasattr(left_ctrl, 'joystick_pos') + has_touchpad = hasattr(left_ctrl, 'touchpad_pos') + print(f"📊 左手控制器: joystick_pos={has_joystick}, touchpad_pos={has_touchpad}") + + if has_joystick and left_ctrl.joystick_pos: + pos = left_ctrl.joystick_pos + print(f" 当前摇杆位置: ({pos.x:.3f}, {pos.y:.3f}, {pos.z:.3f})") + + if has_touchpad and left_ctrl.touchpad_pos: + pos = left_ctrl.touchpad_pos + print(f" 当前触摸板位置: ({pos.x:.3f}, {pos.y:.3f}, {pos.z:.3f})") + + if right_connected: + right_ctrl = self.vr_manager.right_controller + has_joystick = hasattr(right_ctrl, 'joystick_pos') + has_touchpad = hasattr(right_ctrl, 'touchpad_pos') + print(f"📊 右手控制器: joystick_pos={has_joystick}, touchpad_pos={has_touchpad}") + + if has_joystick and right_ctrl.joystick_pos: + pos = right_ctrl.joystick_pos + print(f" 当前摇杆位置: ({pos.x:.3f}, {pos.y:.3f}, {pos.z:.3f})") + + if has_touchpad and right_ctrl.touchpad_pos: + pos = right_ctrl.touchpad_pos + print(f" 当前触摸板位置: ({pos.x:.3f}, {pos.y:.3f}, {pos.z:.3f})") + + # 摇杆配置 + print(f"⚙️ 摇杆配置:") + print(f" 死区: {self.deadzone}") + print(f" 转向阈值: {self.turn_threshold}") + print(f" 传送阈值: {self.teleport_threshold}") + print(f" 转向模式: {'平滑' if self.smooth_turning else '分段'}") + + # 动作系统状态 + action_mgr_available = (self.vr_manager.action_manager is not None) + print(f"🎯 动作系统: {'可用' if action_mgr_available else '不可用'}") + + # 传送系统状态 + teleport_available = (self.teleport_system is not None) + print(f"🚀 传送系统: {'可用' if teleport_available else '不可用'}") + + print("🔍 ==============================") + + except Exception as e: + print(f"⚠️ 调试状态输出失败: {e}") + + def cleanup(self): + """清理摇杆系统资源""" + try: + # 停止任何活跃的传送预览 + if self.teleport_preview_active and self.teleport_system: + self.teleport_system.stop_teleport_preview() + + self.teleport_preview_active = False + self.active_teleport_controller = None + + self.ignoreAll() + print("🧹 VR摇杆系统已清理") + + except Exception as e: + print(f"⚠️ 清理摇杆系统失败: {e}") + + +class JoystickState: + """摇杆状态跟踪类""" + + def __init__(self): + self.current_input = Vec2(0, 0) # 当前摇杆输入 + self.previous_input = Vec2(0, 0) # 上一帧摇杆输入 + self.teleport_active = False # 传送是否激活 + self.teleport_just_started = True # 传送是否刚开始 + + def update(self, new_input): + """更新摇杆状态 + + Args: + new_input: 新的摇杆输入 + """ + self.previous_input = Vec2(self.current_input) + self.current_input = Vec2(new_input) + + def is_input_changed(self, threshold=0.01): + """检查输入是否发生变化 + + Args: + threshold: 变化阈值 + + Returns: + bool: 是否发生变化 + """ + diff = self.current_input - self.previous_input return diff.length() > threshold \ No newline at end of file diff --git a/core/vr/performance/MIGRATION_REPORT.md b/core/vr/performance/MIGRATION_REPORT.md index 01f7e860..29374df6 100644 --- a/core/vr/performance/MIGRATION_REPORT.md +++ b/core/vr/performance/MIGRATION_REPORT.md @@ -1,300 +1,300 @@ -# VR性能监控子系统迁移完成报告 - -## 📊 总体统计 - -- **文件路径**: `/home/hello/EG/core/vr/performance/monitoring.py` -- **代码行数**: 1168行 -- **迁移方法**: 35个 (包括__init__) -- **迁移属性**: 34个核心性能监控属性 -- **测试状态**: ✅ 所有测试通过 - -## 🎯 迁移目标完成度 - -✅ **性能报告方法 (3/3)** -- `_print_performance_report` - 完整的性能报告输出 -- `_print_performance_recommendations` - 性能优化建议 -- `_print_brief_performance_report` - 简短性能摘要 - -✅ **性能监控核心 (4/4)** -- `_init_performance_monitoring` - 初始化监控库 -- `_update_performance_metrics` - 更新性能指标 -- `_update_gpu_metrics` - 更新GPU指标 -- `_track_frame_time` - 记录帧时间 - -✅ **GPU计时功能 (3/3)** -- `_get_gpu_frame_timing` - 获取GPU渲染时间 -- `enable_gpu_timing_monitoring` - 启用GPU时间监控 -- `disable_gpu_timing_monitoring` - 禁用GPU时间监控 - -✅ **管线统计功能 (4/4)** -- `_start_timing` - 开始计时操作 -- `_end_timing` - 结束计时并记录 -- `_get_pipeline_stats` - 获取管线统计信息 -- `test_pipeline_monitoring` - 测试管线监控功能 - -✅ **诊断工具 (3/3)** -- `_print_render_callback_diagnostics` - 渲染回调诊断 -- `_check_rendering_optimizations` - 检查渲染优化状态 -- `_diagnose_opengl_state` - OpenGL状态诊断 - -✅ **调试控制 (5/5)** -- `enable_debug_output` - 启用调试输出 -- `disable_debug_output` - 禁用调试输出 -- `set_debug_mode` - 设置调试模式 -- `toggle_debug_output` - 切换调试输出 -- `get_debug_status` - 获取调试状态 - -✅ **配置方法 (4/4)** -- `set_performance_check_interval` - 设置性能检查间隔 -- `set_frame_time_history_size` - 设置帧时间历史大小 -- `set_performance_report_interval` - 设置报告间隔 -- `set_prediction_time` - 设置预测时间 - -✅ **查询方法 (4/4)** -- `get_performance_stats` - 获取详细性能统计 -- `get_current_performance_summary` - 获取性能摘要 -- `get_performance_monitoring_config` - 获取监控配置 -- `print_performance_monitoring_status` - 输出监控状态 - -✅ **控制方法 (4/4)** -- `enable_performance_monitoring` - 启用性能监控 -- `disable_performance_monitoring` - 禁用性能监控 -- `force_performance_report` - 强制输出报告 -- `reset_performance_counters` - 重置性能计数器 - -## 📦 核心属性列表 (34个) - -### 性能计数器 (6个) -```python -self.frame_count = 0 -self.last_fps_check = 0 -self.last_fps_time = 0 -self.vr_fps = 0 -self.submit_failures = 0 -self.pose_failures = 0 -``` - -### 性能监控配置 (10个) -```python -self.performance_monitoring = False -self.debug_output_enabled = False -self.debug_mode = 'detailed' -self.cpu_usage = 0.0 -self.memory_usage = 0.0 -self.gpu_usage = 0.0 -self.gpu_memory_usage = 0.0 -self.frame_times = [] -self.max_frame_time_history = 60 -self.last_performance_check = 0 -self.performance_check_interval = 0.5 -``` - -### 渲染管线监控 (9个) -```python -self.enable_pipeline_monitoring = True -self.performance_mode_enabled = False -self.performance_mode_trigger_frame = 600 -self.wait_poses_time = 0.0 -self.left_render_time = 0.0 -self.right_render_time = 0.0 -self.submit_time = 0.0 -self.left_render_count = 0 -self.right_render_count = 0 -self.total_frame_time = 0.0 -self.vr_sync_wait_time = 0.0 -``` - -### 时间监控历史 (5个) -```python -self.wait_poses_times = [] -self.render_times = [] -self.submit_times = [] -self.sync_wait_times = [] -self.pipeline_history_size = 30 -``` - -### GPU渲染时间监控 (9个) -```python -self.enable_gpu_timing = False -self.gpu_scene_render_ms = 0.0 -self.gpu_pre_submit_ms = 0.0 -self.gpu_post_submit_ms = 0.0 -self.gpu_total_render_ms = 0.0 -self.gpu_compositor_render_ms = 0.0 -self.gpu_client_frame_interval_ms = 0.0 -self.gpu_timing_history = [] -self.gpu_timing_history_size = 30 -self.gpu_timing_failure_count = 0 -``` - -### VR系统信息 (9个) -```python -self.current_eye_resolution = (0, 0) -self.recommended_eye_resolution = (0, 0) -self.vr_display_frequency = 0.0 -self.vr_vsync_enabled = True -self.vsync_to_photons_ms = 0.0 -self.target_frame_time_ms = 0.0 -self.vsync_window_ms = 0.0 -self.async_reprojection_enabled = False -self.motion_smoothing_enabled = False -``` - -## 🔧 关键设计决策 - -### 1. 架构模式 -- **组合模式**: VRPerformanceMonitor通过self.vr_manager引用VRManager -- **单一职责**: 只负责性能监控,不涉及其他VR功能 -- **松耦合**: 最小化对VRManager内部实现的依赖 - -### 2. 访问模式 -```python -# 监控数据 - 保存在self中 -self.frame_count -self.vr_fps -self.gpu_timing_history - -# VR管理器数据 - 通过self.vr_manager访问 -self.vr_manager.use_prediction_time -self.vr_manager.vr_compositor -self.vr_manager.world - -# 对象池状态 - 通过方法调用 -self.vr_manager.get_object_pool_status() -``` - -### 3. 依赖管理 -- 可选依赖优雅降级 (psutil, GPUtil, pynvml) -- 初始化时检测库可用性 -- 运行时根据可用性调整功能 - -## ✅ 测试验证结果 - -### 导入测试 -``` -✓ VRPerformanceMonitor导入成功 -✓ 方法数量: 20个公共方法 -``` - -### 初始化测试 -``` -✓ VRPerformanceMonitor初始化成功 -✓ 性能计数器初始化: frame_count=0, vr_fps=0 -✓ 监控配置初始化: monitoring=False, debug=False -✓ 管线监控初始化: pipeline=True, history_size=30 -✓ GPU时间监控初始化: enabled=False, history_size=30 -✓ VR系统信息初始化: resolution=(0, 0), frequency=0.0 -``` - -### 功能测试 -``` -✓ get_performance_stats(): 12个指标 -✓ get_performance_monitoring_config(): 7个配置项 -✓ get_current_performance_summary(): VR性能: 0.0fps | GPU: N/A -✓ _get_pipeline_stats(): 7个统计类别 -✓ 所有34个方法都已正确迁移 -``` - -## 📚 使用示例 - -### 基本使用 -```python -from core.vr.performance import VRPerformanceMonitor - -# 在VRManager.__init__中初始化 -self.performance_monitor = VRPerformanceMonitor(self) - -# 启用性能监控 -self.performance_monitor.enable_performance_monitoring() -self.performance_monitor.enable_debug_output() -self.performance_monitor.set_debug_mode('detailed') # 或 'brief' -``` - -### 获取性能数据 -```python -# 详细统计 -stats = self.performance_monitor.get_performance_stats() -print(f"VR FPS: {stats['vr_fps']}") -print(f"平均帧时间: {stats['frame_time_avg']}ms") - -# 简短摘要 -summary = self.performance_monitor.get_current_performance_summary() -print(summary) # "VR性能: 75.0fps | 帧时间: 13.3ms | CPU: 45% | GPU: 78%" -``` - -### 配置监控 -```python -# 设置检查间隔 -self.performance_monitor.set_performance_check_interval(0.5) # 0.5秒 - -# 设置历史记录大小 -self.performance_monitor.set_frame_time_history_size(60) # 60帧 - -# 设置报告间隔 -self.performance_monitor.set_performance_report_interval(1800) # 1800帧 -``` - -### GPU时间监控 -```python -# 启用GPU时间监控 -self.performance_monitor.enable_gpu_timing_monitoring() - -# 获取管线统计 -pipeline_stats = self.performance_monitor._get_pipeline_stats() -print(f"GPU场景渲染: {pipeline_stats['current']['gpu_scene_render']}ms") -``` - -### 诊断工具 -```python -# 测试管线监控 -self.performance_monitor.test_pipeline_monitoring() - -# 强制输出性能报告 -self.performance_monitor.force_performance_report() - -# 检查监控状态 -self.performance_monitor.print_performance_monitoring_status() -``` - -## 🎯 下一步工作 - -### 1. 集成到VRManager -- [ ] 在VRManager.__init__中创建performance_monitor实例 -- [ ] 替换所有直接访问性能属性的代码 -- [ ] 更新_update_vr方法调用performance_monitor方法 - -### 2. 清理重复代码 -- [ ] 从vr_manager.py中删除已迁移的方法 -- [ ] 从vr_manager.py中删除已迁移的属性初始化 -- [ ] 更新所有引用这些方法的代码 - -### 3. 测试验证 -- [ ] 在实际VR环境中测试性能监控 -- [ ] 验证GPU时间统计功能 -- [ ] 验证性能报告输出 -- [ ] 验证诊断工具功能 - -### 4. 文档更新 -- [ ] 更新core/vr/README.md -- [ ] 添加性能监控使用指南 -- [ ] 更新API文档 - -## 🔍 注意事项 - -1. **依赖库**: psutil、GPUtil、pynvml是可选依赖,缺失时会降级功能 -2. **性能开销**: 建议只在调试时启用详细监控,生产环境使用简短模式 -3. **历史记录**: 帧时间历史记录会占用内存,根据需要调整大小 -4. **GPU时间**: OpenVR的GPU时间统计可能在某些系统上不可用 - -## 📝 变更日志 - -**2025-10-14** -- ✅ 创建VRPerformanceMonitor类 -- ✅ 迁移35个性能监控方法 -- ✅ 迁移34个性能监控属性 -- ✅ 通过所有测试验证 -- ✅ 更新performance子系统__init__.py - ---- - -**迁移完成!所有性能监控功能已成功模块化。** +# VR性能监控子系统迁移完成报告 + +## 📊 总体统计 + +- **文件路径**: `/home/hello/EG/core/vr/performance/monitoring.py` +- **代码行数**: 1168行 +- **迁移方法**: 35个 (包括__init__) +- **迁移属性**: 34个核心性能监控属性 +- **测试状态**: ✅ 所有测试通过 + +## 🎯 迁移目标完成度 + +✅ **性能报告方法 (3/3)** +- `_print_performance_report` - 完整的性能报告输出 +- `_print_performance_recommendations` - 性能优化建议 +- `_print_brief_performance_report` - 简短性能摘要 + +✅ **性能监控核心 (4/4)** +- `_init_performance_monitoring` - 初始化监控库 +- `_update_performance_metrics` - 更新性能指标 +- `_update_gpu_metrics` - 更新GPU指标 +- `_track_frame_time` - 记录帧时间 + +✅ **GPU计时功能 (3/3)** +- `_get_gpu_frame_timing` - 获取GPU渲染时间 +- `enable_gpu_timing_monitoring` - 启用GPU时间监控 +- `disable_gpu_timing_monitoring` - 禁用GPU时间监控 + +✅ **管线统计功能 (4/4)** +- `_start_timing` - 开始计时操作 +- `_end_timing` - 结束计时并记录 +- `_get_pipeline_stats` - 获取管线统计信息 +- `test_pipeline_monitoring` - 测试管线监控功能 + +✅ **诊断工具 (3/3)** +- `_print_render_callback_diagnostics` - 渲染回调诊断 +- `_check_rendering_optimizations` - 检查渲染优化状态 +- `_diagnose_opengl_state` - OpenGL状态诊断 + +✅ **调试控制 (5/5)** +- `enable_debug_output` - 启用调试输出 +- `disable_debug_output` - 禁用调试输出 +- `set_debug_mode` - 设置调试模式 +- `toggle_debug_output` - 切换调试输出 +- `get_debug_status` - 获取调试状态 + +✅ **配置方法 (4/4)** +- `set_performance_check_interval` - 设置性能检查间隔 +- `set_frame_time_history_size` - 设置帧时间历史大小 +- `set_performance_report_interval` - 设置报告间隔 +- `set_prediction_time` - 设置预测时间 + +✅ **查询方法 (4/4)** +- `get_performance_stats` - 获取详细性能统计 +- `get_current_performance_summary` - 获取性能摘要 +- `get_performance_monitoring_config` - 获取监控配置 +- `print_performance_monitoring_status` - 输出监控状态 + +✅ **控制方法 (4/4)** +- `enable_performance_monitoring` - 启用性能监控 +- `disable_performance_monitoring` - 禁用性能监控 +- `force_performance_report` - 强制输出报告 +- `reset_performance_counters` - 重置性能计数器 + +## 📦 核心属性列表 (34个) + +### 性能计数器 (6个) +```python +self.frame_count = 0 +self.last_fps_check = 0 +self.last_fps_time = 0 +self.vr_fps = 0 +self.submit_failures = 0 +self.pose_failures = 0 +``` + +### 性能监控配置 (10个) +```python +self.performance_monitoring = False +self.debug_output_enabled = False +self.debug_mode = 'detailed' +self.cpu_usage = 0.0 +self.memory_usage = 0.0 +self.gpu_usage = 0.0 +self.gpu_memory_usage = 0.0 +self.frame_times = [] +self.max_frame_time_history = 60 +self.last_performance_check = 0 +self.performance_check_interval = 0.5 +``` + +### 渲染管线监控 (9个) +```python +self.enable_pipeline_monitoring = True +self.performance_mode_enabled = False +self.performance_mode_trigger_frame = 600 +self.wait_poses_time = 0.0 +self.left_render_time = 0.0 +self.right_render_time = 0.0 +self.submit_time = 0.0 +self.left_render_count = 0 +self.right_render_count = 0 +self.total_frame_time = 0.0 +self.vr_sync_wait_time = 0.0 +``` + +### 时间监控历史 (5个) +```python +self.wait_poses_times = [] +self.render_times = [] +self.submit_times = [] +self.sync_wait_times = [] +self.pipeline_history_size = 30 +``` + +### GPU渲染时间监控 (9个) +```python +self.enable_gpu_timing = False +self.gpu_scene_render_ms = 0.0 +self.gpu_pre_submit_ms = 0.0 +self.gpu_post_submit_ms = 0.0 +self.gpu_total_render_ms = 0.0 +self.gpu_compositor_render_ms = 0.0 +self.gpu_client_frame_interval_ms = 0.0 +self.gpu_timing_history = [] +self.gpu_timing_history_size = 30 +self.gpu_timing_failure_count = 0 +``` + +### VR系统信息 (9个) +```python +self.current_eye_resolution = (0, 0) +self.recommended_eye_resolution = (0, 0) +self.vr_display_frequency = 0.0 +self.vr_vsync_enabled = True +self.vsync_to_photons_ms = 0.0 +self.target_frame_time_ms = 0.0 +self.vsync_window_ms = 0.0 +self.async_reprojection_enabled = False +self.motion_smoothing_enabled = False +``` + +## 🔧 关键设计决策 + +### 1. 架构模式 +- **组合模式**: VRPerformanceMonitor通过self.vr_manager引用VRManager +- **单一职责**: 只负责性能监控,不涉及其他VR功能 +- **松耦合**: 最小化对VRManager内部实现的依赖 + +### 2. 访问模式 +```python +# 监控数据 - 保存在self中 +self.frame_count +self.vr_fps +self.gpu_timing_history + +# VR管理器数据 - 通过self.vr_manager访问 +self.vr_manager.use_prediction_time +self.vr_manager.vr_compositor +self.vr_manager.world + +# 对象池状态 - 通过方法调用 +self.vr_manager.get_object_pool_status() +``` + +### 3. 依赖管理 +- 可选依赖优雅降级 (psutil, GPUtil, pynvml) +- 初始化时检测库可用性 +- 运行时根据可用性调整功能 + +## ✅ 测试验证结果 + +### 导入测试 +``` +✓ VRPerformanceMonitor导入成功 +✓ 方法数量: 20个公共方法 +``` + +### 初始化测试 +``` +✓ VRPerformanceMonitor初始化成功 +✓ 性能计数器初始化: frame_count=0, vr_fps=0 +✓ 监控配置初始化: monitoring=False, debug=False +✓ 管线监控初始化: pipeline=True, history_size=30 +✓ GPU时间监控初始化: enabled=False, history_size=30 +✓ VR系统信息初始化: resolution=(0, 0), frequency=0.0 +``` + +### 功能测试 +``` +✓ get_performance_stats(): 12个指标 +✓ get_performance_monitoring_config(): 7个配置项 +✓ get_current_performance_summary(): VR性能: 0.0fps | GPU: N/A +✓ _get_pipeline_stats(): 7个统计类别 +✓ 所有34个方法都已正确迁移 +``` + +## 📚 使用示例 + +### 基本使用 +```python +from core.vr.performance import VRPerformanceMonitor + +# 在VRManager.__init__中初始化 +self.performance_monitor = VRPerformanceMonitor(self) + +# 启用性能监控 +self.performance_monitor.enable_performance_monitoring() +self.performance_monitor.enable_debug_output() +self.performance_monitor.set_debug_mode('detailed') # 或 'brief' +``` + +### 获取性能数据 +```python +# 详细统计 +stats = self.performance_monitor.get_performance_stats() +print(f"VR FPS: {stats['vr_fps']}") +print(f"平均帧时间: {stats['frame_time_avg']}ms") + +# 简短摘要 +summary = self.performance_monitor.get_current_performance_summary() +print(summary) # "VR性能: 75.0fps | 帧时间: 13.3ms | CPU: 45% | GPU: 78%" +``` + +### 配置监控 +```python +# 设置检查间隔 +self.performance_monitor.set_performance_check_interval(0.5) # 0.5秒 + +# 设置历史记录大小 +self.performance_monitor.set_frame_time_history_size(60) # 60帧 + +# 设置报告间隔 +self.performance_monitor.set_performance_report_interval(1800) # 1800帧 +``` + +### GPU时间监控 +```python +# 启用GPU时间监控 +self.performance_monitor.enable_gpu_timing_monitoring() + +# 获取管线统计 +pipeline_stats = self.performance_monitor._get_pipeline_stats() +print(f"GPU场景渲染: {pipeline_stats['current']['gpu_scene_render']}ms") +``` + +### 诊断工具 +```python +# 测试管线监控 +self.performance_monitor.test_pipeline_monitoring() + +# 强制输出性能报告 +self.performance_monitor.force_performance_report() + +# 检查监控状态 +self.performance_monitor.print_performance_monitoring_status() +``` + +## 🎯 下一步工作 + +### 1. 集成到VRManager +- [ ] 在VRManager.__init__中创建performance_monitor实例 +- [ ] 替换所有直接访问性能属性的代码 +- [ ] 更新_update_vr方法调用performance_monitor方法 + +### 2. 清理重复代码 +- [ ] 从vr_manager.py中删除已迁移的方法 +- [ ] 从vr_manager.py中删除已迁移的属性初始化 +- [ ] 更新所有引用这些方法的代码 + +### 3. 测试验证 +- [ ] 在实际VR环境中测试性能监控 +- [ ] 验证GPU时间统计功能 +- [ ] 验证性能报告输出 +- [ ] 验证诊断工具功能 + +### 4. 文档更新 +- [ ] 更新core/vr/README.md +- [ ] 添加性能监控使用指南 +- [ ] 更新API文档 + +## 🔍 注意事项 + +1. **依赖库**: psutil、GPUtil、pynvml是可选依赖,缺失时会降级功能 +2. **性能开销**: 建议只在调试时启用详细监控,生产环境使用简短模式 +3. **历史记录**: 帧时间历史记录会占用内存,根据需要调整大小 +4. **GPU时间**: OpenVR的GPU时间统计可能在某些系统上不可用 + +## 📝 变更日志 + +**2025-10-14** +- ✅ 创建VRPerformanceMonitor类 +- ✅ 迁移35个性能监控方法 +- ✅ 迁移34个性能监控属性 +- ✅ 通过所有测试验证 +- ✅ 更新performance子系统__init__.py + +--- + +**迁移完成!所有性能监控功能已成功模块化。** diff --git a/core/vr/performance/__init__.py b/core/vr/performance/__init__.py index eea166a1..26fce9be 100644 --- a/core/vr/performance/__init__.py +++ b/core/vr/performance/__init__.py @@ -1,12 +1,12 @@ -""" -VR性能优化子系统 - -负责VR性能监控和优化: -- 性能指标监控(帧时间、GPU时间) -- 优化系统(对象池、GC控制、分辨率缩放) -- 诊断和调试工具 -""" - -from .monitoring import VRPerformanceMonitor - -__all__ = ['VRPerformanceMonitor'] +""" +VR性能优化子系统 + +负责VR性能监控和优化: +- 性能指标监控(帧时间、GPU时间) +- 优化系统(对象池、GC控制、分辨率缩放) +- 诊断和调试工具 +""" + +from .monitoring import VRPerformanceMonitor + +__all__ = ['VRPerformanceMonitor'] diff --git a/core/vr/performance/monitoring.py b/core/vr/performance/monitoring.py index d51b83a3..f06802f8 100644 --- a/core/vr/performance/monitoring.py +++ b/core/vr/performance/monitoring.py @@ -1,1171 +1,1171 @@ -""" -VR性能监控子系统 -负责VR应用的性能监控、GPU计时、管线统计和性能诊断 -""" - -import sys -import gc -import time - -# 可选依赖 -try: - import psutil - PSUTIL_AVAILABLE = True -except ImportError: - PSUTIL_AVAILABLE = False - -try: - import GPUtil - GPUTIL_AVAILABLE = True -except ImportError: - GPUTIL_AVAILABLE = False - -try: - import pynvml - PYNVML_AVAILABLE = True -except ImportError: - PYNVML_AVAILABLE = False - - -class VRPerformanceMonitor: - """VR性能监控系统 - - 功能: - - 实时性能监控(CPU、内存、GPU) - - GPU渲染时间统计 - - 渲染管线分析 - - 性能诊断和优化建议 - """ - - def __init__(self, vr_manager): - """初始化性能监控系统 - - Args: - vr_manager: VRManager实例的引用 - """ - self.vr_manager = vr_manager - - # ===== 性能计数器 ===== - self.frame_count = 0 - self.last_fps_check = 0 - self.last_fps_time = 0 - self.vr_fps = 0 - self.submit_failures = 0 - self.pose_failures = 0 - - # ===== 性能监控配置 ===== - self.performance_monitoring = False # 是否启用性能监控 - self.debug_output_enabled = False # 是否启用调试输出 - self.debug_mode = 'detailed' # 'brief' 或 'detailed' - self.cpu_usage = 0.0 - self.memory_usage = 0.0 - self.gpu_usage = 0.0 - self.gpu_memory_usage = 0.0 - self.frame_times = [] # 存储最近帧时间 - self.max_frame_time_history = 60 # 保存60帧的历史 - self.last_performance_check = 0 - self.performance_check_interval = 0.5 # 每0.5秒更新一次性能数据 - - # ===== 渲染管线监控 ===== - self.enable_pipeline_monitoring = True # 是否启用管线监控 - self.performance_mode_enabled = False # 性能优化模式(禁用监控以减少对象创建) - self.performance_mode_trigger_frame = 600 # 第600帧后启用性能模式 - self.wait_poses_time = 0.0 # waitGetPoses耗时 - self.left_render_time = 0.0 # 左眼渲染耗时 - self.right_render_time = 0.0 # 右眼渲染耗时 - self.submit_time = 0.0 # 纹理提交耗时 - self.left_render_count = 0 # 左眼渲染次数计数 - self.right_render_count = 0 # 右眼渲染次数计数 - self.total_frame_time = 0.0 # 总帧时间 - self.vr_sync_wait_time = 0.0 # VR同步等待时间 - - # ===== 时间监控历史 ===== - self.wait_poses_times = [] - self.render_times = [] - self.submit_times = [] - self.sync_wait_times = [] - self.pipeline_history_size = 30 - - # ===== GPU渲染时间监控 ===== - self.enable_gpu_timing = False # 是否启用GPU时间监控(默认关闭) - self.gpu_scene_render_ms = 0.0 # GPU场景渲染时间 - self.gpu_pre_submit_ms = 0.0 # 提交前GPU时间 - self.gpu_post_submit_ms = 0.0 # 提交后GPU时间 - self.gpu_total_render_ms = 0.0 # GPU总渲染时间 - self.gpu_compositor_render_ms = 0.0 # GPU合成器渲染时间 - self.gpu_client_frame_interval_ms = 0.0 # 客户端帧间隔 - self.gpu_timing_history = [] # GPU时间历史记录 - self.gpu_timing_history_size = 30 # GPU时间历史记录大小 - self.gpu_timing_failure_count = 0 # GPU时间获取失败次数 - - # ===== VR系统信息 ===== - self.current_eye_resolution = (0, 0) - self.recommended_eye_resolution = (0, 0) - self.vr_display_frequency = 0.0 - self.vr_vsync_enabled = True - self.vsync_to_photons_ms = 0.0 # VSync到光子的延迟 - self.target_frame_time_ms = 0.0 # 目标帧时间 - self.vsync_window_ms = 0.0 # VSync时间窗口 - self.async_reprojection_enabled = False # 异步重投影状态 - self.motion_smoothing_enabled = False # 运动平滑状态 - - # ===== 性能报告间隔 ===== - self.performance_report_interval = 1800 # 默认1800帧(30秒@60fps) - - # ===== 内部状态 ===== - self._last_frame_time = None # 用于帧时间计算 - - # 初始化性能监控库 - self._init_performance_monitoring() - - # ========== 性能报告方法 ========== - - def _print_performance_report(self): - """输出VR性能报告""" - if not self.performance_monitoring or not self.debug_output_enabled: - return - - stats = self.get_performance_stats() - - # 简短模式输出 - if self.debug_mode == 'brief': - self._print_brief_performance_report(stats) - return - - print("📊 ======= VR性能监控报告 =======") - - # 帧率和帧时间信息 - print(f"🎯 渲染性能:") - print(f" VR帧率: {stats['vr_fps']:.1f} FPS") - print(f" 平均帧时间: {stats['frame_time_avg']:.2f} ms") - print(f" 最小帧时间: {stats['frame_time_min']:.2f} ms") - print(f" 最大帧时间: {stats['frame_time_max']:.2f} ms") - print(f" 95%帧时间: {stats['frame_time_95th']:.2f} ms") - - # 系统性能 - print(f"💻 系统性能:") - print(f" CPU使用率: {stats['cpu_usage']:.1f}%") - print(f" 内存使用率: {stats['memory_usage']:.1f}%") - - # GPU性能 - print(f"🎮 GPU性能:") - if self.gputil_available or self.nvidia_ml_available: - print(f" GPU使用率: {stats['gpu_usage']:.1f}%") - print(f" 显存使用率: {stats['gpu_memory_usage']:.1f}%") - else: - print(f" GPU监控: 不可用 (需要安装 GPUtil 或 pynvml)") - print(f" 安装命令: pip install GPUtil nvidia-ml-py") - - # GPU渲染时间(OpenVR Frame Timing) - if self.enable_gpu_timing: - print(f"⚡ GPU渲染时间:") - pipeline_stats = self._get_pipeline_stats() - gpu_stats = pipeline_stats.get('gpu_timing', {}) - gpu_current = pipeline_stats.get('current', {}) - - # 检查是否有可用的GPU时间数据 - has_gpu_data = any( - gpu_current.get(field, 0) > 0 - for field in ['gpu_scene_render', 'gpu_total_render', 'gpu_pre_submit', 'gpu_post_submit', 'gpu_compositor_render'] - ) - - if has_gpu_data: - # 显示GPU时间统计(最近30帧平均) - scene_render = gpu_stats.get('scene_render', {'avg': 0}) - total_render = gpu_stats.get('total_render', {'avg': 0}) - pre_submit = gpu_stats.get('pre_submit', {'avg': 0}) - post_submit = gpu_stats.get('post_submit', {'avg': 0}) - compositor = gpu_stats.get('compositor_render', {'avg': 0}) - - if scene_render['avg'] > 0: - print(f" GPU场景渲染: {scene_render['avg']:.2f}ms (min:{scene_render['min']:.1f}, max:{scene_render['max']:.1f})") - if total_render['avg'] > 0: - print(f" GPU总渲染时间: {total_render['avg']:.2f}ms (min:{total_render['min']:.1f}, max:{total_render['max']:.1f})") - if pre_submit['avg'] > 0: - print(f" GPU提交前时间: {pre_submit['avg']:.2f}ms (min:{pre_submit['min']:.1f}, max:{pre_submit['max']:.1f})") - if post_submit['avg'] > 0: - print(f" GPU提交后时间: {post_submit['avg']:.2f}ms (min:{post_submit['min']:.1f}, max:{post_submit['max']:.1f})") - if compositor['avg'] > 0: - print(f" GPU合成器时间: {compositor['avg']:.2f}ms (min:{compositor['min']:.1f}, max:{compositor['max']:.1f})") - - # 显示当前帧GPU时间 - print(f"🔍 当前帧GPU时间:") - if gpu_current.get('gpu_scene_render', 0) > 0: - print(f" 场景渲染: {gpu_current['gpu_scene_render']:.2f}ms") - if gpu_current.get('gpu_total_render', 0) > 0: - print(f" 总渲染: {gpu_current['gpu_total_render']:.2f}ms") - if gpu_current.get('gpu_compositor_render', 0) > 0: - print(f" 合成器: {gpu_current['gpu_compositor_render']:.2f}ms") - - # GPU时间瓶颈分析 - current_total = gpu_current.get('gpu_total_render', 0) - current_scene = gpu_current.get('gpu_scene_render', 0) - if current_total > 12.0: # 假设72fps目标,留出一些余量 - print(f" ⚠️ GPU总渲染时间过长: {current_total:.1f}ms") - elif current_scene > 8.0: - print(f" ⚠️ GPU场景渲染时间偏高: {current_scene:.1f}ms") - - else: - print(f" GPU渲染时间: 暂无数据") - if self.gpu_timing_failure_count > 0: - print(f" 获取失败次数: {self.gpu_timing_failure_count}") - else: - print(f" 等待OpenVR Frame Timing数据...") - else: - print(f"⚡ GPU渲染时间: 已禁用") - - # VR特定指标 - print(f"🥽 VR指标:") - print(f" 总帧数: {stats['frame_count']}") - print(f" 提交失败: {stats['submit_failures']}") - print(f" 姿态失败: {stats['pose_failures']}") - - # 计算失败率 - if stats['frame_count'] > 0: - submit_fail_rate = (stats['submit_failures'] / stats['frame_count']) * 100 - pose_fail_rate = (stats['pose_failures'] / stats['frame_count']) * 100 - print(f" 提交失败率: {submit_fail_rate:.2f}%") - print(f" 姿态失败率: {pose_fail_rate:.2f}%") - - # 渲染管线监控 - target_frame_time = 13.9 # 默认目标帧时间(72Hz) - if self.enable_pipeline_monitoring: - pipeline_stats = self._get_pipeline_stats() - print(f"⚙️ 渲染管线分析:") - print(f" 当前分辨率: {pipeline_stats['vr_info']['eye_resolution'][0]}x{pipeline_stats['vr_info']['eye_resolution'][1]}") - print(f" 推荐分辨率: {pipeline_stats['vr_info']['recommended_resolution'][0]}x{pipeline_stats['vr_info']['recommended_resolution'][1]}") - print(f" 显示频率: {pipeline_stats['vr_info']['display_frequency']:.1f} Hz") - - # VSync和时序信息 - if pipeline_stats['vr_info']['target_frame_time_ms'] > 0: - print(f"🎯 VSync时序:") - print(f" 目标帧时间: {pipeline_stats['vr_info']['target_frame_time_ms']:.2f}ms") - print(f" VSync到光子: {pipeline_stats['vr_info']['vsync_to_photons_ms']:.2f}ms") - print(f" VSync窗口: ±{pipeline_stats['vr_info']['vsync_window_ms']:.2f}ms") - print(f" 异步重投影: {'启用' if pipeline_stats['vr_info']['async_reprojection'] else '禁用'}") - print(f" 运动平滑: {'启用' if pipeline_stats['vr_info']['motion_smoothing'] else '禁用'}") - - # 分析帧时间是否在目标范围内 - current_frame_time = stats['frame_time_avg'] - target_frame_time = pipeline_stats['vr_info']['target_frame_time_ms'] - if current_frame_time > 0 and target_frame_time > 0: - frame_time_ratio = current_frame_time / target_frame_time - if frame_time_ratio > 1.1: - print(f" ⚠️ 帧时间超标: {current_frame_time:.1f}ms (目标:{target_frame_time:.1f}ms)") - elif frame_time_ratio < 0.9: - print(f" ✅ 帧时间充裕: {current_frame_time:.1f}ms (目标:{target_frame_time:.1f}ms)") - else: - print(f" ✓ 帧时间正常: {current_frame_time:.1f}ms (目标:{target_frame_time:.1f}ms)") - - # 🔧 性能优化诊断 - print(f"🔧 优化诊断:") - current = pipeline_stats.get('current', {}) if self.enable_pipeline_monitoring else {} - - # 🔍 渲染回调诊断 - 新增 - self._print_render_callback_diagnostics() - - # waitGetPoses时序分析 - wait_poses_time = current.get('wait_poses', 0) - if wait_poses_time > 10: - print(f" ⚠️ waitGetPoses时间过长: {wait_poses_time:.1f}ms") - print(f" 可能原因: 错过VSync窗口,建议调整预测时间") - elif wait_poses_time > 5: - print(f" ⚠️ waitGetPoses时间偏高: {wait_poses_time:.1f}ms") - else: - print(f" ✓ waitGetPoses时间正常: {wait_poses_time:.1f}ms") - - # CPU-GPU并行度分析 - 增强诊断 - gpu_total = current.get('gpu_total_render', 0) - cpu_render_left = self.left_render_time - cpu_render_right = self.right_render_time - cpu_render_total = cpu_render_left + cpu_render_right - - print(f" 真实渲染时间对比:") - print(f" CPU左眼: {cpu_render_left:.2f}ms") - print(f" CPU右眼: {cpu_render_right:.2f}ms") - print(f" CPU总计: {cpu_render_total:.2f}ms") - print(f" GPU总计: {gpu_total:.2f}ms") - - if cpu_render_total < 1.0: - print(f" ⚠️ CPU渲染时间异常短: {cpu_render_total:.2f}ms") - print(f" 可能原因: 渲染回调未正确执行或渲染被跳过") - elif gpu_total > 0 and cpu_render_total > 0: - ratio = gpu_total / cpu_render_total - if ratio > 10: - print(f" ⚠️ GPU严重等待CPU: 比例{ratio:.1f}:1") - print(f" 建议: 检查OpenGL命令提交时机和渲染状态") - elif ratio > 3: - print(f" ⚠️ GPU等待CPU: 比例{ratio:.1f}:1") - print(f" 建议: 检查OpenGL命令提交是否及时") - else: - print(f" ✓ CPU-GPU时间匹配: 比例{ratio:.1f}:1") - - # 预测时间诊断 - current_prediction = self.vr_manager.use_prediction_time * 1000 - print(f" 预测时间设置: {current_prediction:.1f}ms") - if current_prediction > 15: - print(f" 建议: 预测时间较高,可能增加waitGetPoses延迟") - elif current_prediction < 8: - print(f" 注意: 预测时间较低,可能影响姿态准确性") - else: - print(f" ✓ 预测时间在合理范围内") - - # 优化状态总结 - optimization_score = 0 - if wait_poses_time < 8: - optimization_score += 1 - if stats['vr_fps'] > 60: - optimization_score += 1 - if stats.get('frame_time_avg', 0) < target_frame_time * 1.1: - optimization_score += 1 - - if optimization_score >= 2: - print(f" ✅ 优化效果良好 ({optimization_score}/3)") - else: - print(f" ⚠️ 仍有优化空间 ({optimization_score}/3)") - - # 显示详细统计信息(仅当管线监控启用时) - if self.enable_pipeline_monitoring: - print(f"🕐 各阶段耗时 (最近{self.pipeline_history_size}帧平均):") - print(f" waitGetPoses: {pipeline_stats['wait_poses']['avg']:.2f}ms (min:{pipeline_stats['wait_poses']['min']:.1f}, max:{pipeline_stats['wait_poses']['max']:.1f})") - print(f" 渲染总计: {pipeline_stats['render']['avg']:.2f}ms (min:{pipeline_stats['render']['min']:.1f}, max:{pipeline_stats['render']['max']:.1f})") - print(f" 纹理提交: {pipeline_stats['submit']['avg']:.2f}ms (min:{pipeline_stats['submit']['min']:.1f}, max:{pipeline_stats['submit']['max']:.1f})") - if pipeline_stats['sync_wait']['avg'] > 0: - print(f" 同步等待: {pipeline_stats['sync_wait']['avg']:.2f}ms (min:{pipeline_stats['sync_wait']['min']:.1f}, max:{pipeline_stats['sync_wait']['max']:.1f})") - - print(f"🔍 当前帧详情:") - print(f" 左眼渲染: {pipeline_stats['current']['left_render']:.2f}ms") - print(f" 右眼渲染: {pipeline_stats['current']['right_render']:.2f}ms") - print(f" 姿态获取: {pipeline_stats['current']['wait_poses']:.2f}ms") - - # 显示Running Start模式信息 - print(f"🎯 Running Start模式:") - print(f" 模式: Valve Running Start - 帧开始时获取姿态") - print(f" 优势: VSync前3ms获取姿态,提供充足渲染时间") - print(f" 预测时间: {self.vr_manager.use_prediction_time * 1000:.1f}ms") - - # 分析最大瓶颈 - current = pipeline_stats['current'] - bottleneck_analysis = [] - if current['wait_poses'] > 5.0: - bottleneck_analysis.append(f"姿态获取耗时过长({current['wait_poses']:.1f}ms)") - if current['total_render'] > 8.0: - bottleneck_analysis.append(f"渲染耗时过长({current['total_render']:.1f}ms)") - if current['submit'] > 2.0: - bottleneck_analysis.append(f"纹理提交耗时过长({current['submit']:.1f}ms)") - - if bottleneck_analysis: - print(f"🚨 瓶颈分析:") - for analysis in bottleneck_analysis: - print(f" ⚠️ {analysis}") - - # 性能建议 - self._print_performance_recommendations(stats) - - print("===============================") - - def _print_performance_recommendations(self, stats): - """根据性能数据输出优化建议""" - print(f"💡 性能建议:") - - recommendations = [] - - # FPS相关建议 - if stats['vr_fps'] < 60: - recommendations.append(" ⚠️ VR帧率过低,可能影响体验") - - # 帧时间相关建议 - if stats['frame_time_avg'] > 16.7: # 60fps = 16.7ms - recommendations.append(" ⚠️ 平均帧时间过高,建议降低渲染质量") - - if stats['frame_time_max'] > 50: - recommendations.append(" ⚠️ 检测到严重卡顿,检查CPU/GPU负载") - - # CPU建议 - if stats['cpu_usage'] > 80: - recommendations.append(" 🔴 CPU使用率过高,可能存在CPU瓶颈") - elif stats['cpu_usage'] > 60: - recommendations.append(" 🟡 CPU使用率偏高,注意监控") - - # 内存建议 - if stats['memory_usage'] > 85: - recommendations.append(" 🔴 内存使用率过高,可能影响性能") - - # GPU建议 - if self.gputil_available or self.nvidia_ml_available: - if stats['gpu_usage'] > 95: - recommendations.append(" 🔴 GPU使用率接近满载,存在GPU瓶颈") - if stats['gpu_memory_usage'] > 90: - recommendations.append(" 🔴 显存使用率过高,可能需要降低纹理质量") - - # GPU渲染时间建议 - if self.enable_gpu_timing: - if self.gpu_total_render_ms > 12.0: - recommendations.append(" ⚠️ GPU总渲染时间过长,建议优化场景复杂度") - if self.gpu_scene_render_ms > 8.0: - recommendations.append(" ⚠️ GPU场景渲染时间偏高,考虑降低渲染质量") - if self.gpu_compositor_render_ms > 3.0: - recommendations.append(" ⚠️ GPU合成器时间过长,检查VR设置或叠加层") - if self.gpu_timing_failure_count > 100: - recommendations.append(" ⚠️ GPU时间统计频繁失败,可能需要更新OpenVR") - - # 失败率建议 - submit_fail_rate = (stats['submit_failures'] / max(stats['frame_count'], 1)) * 100 - if submit_fail_rate > 1: - recommendations.append(" ⚠️ VR帧提交失败率较高,检查VR系统状态") - - if not recommendations: - recommendations.append(" ✅ 性能表现良好,无明显问题") - - for rec in recommendations: - print(rec) - - def _print_brief_performance_report(self, stats): - """输出简短的VR性能报告""" - # 创建一行简短摘要 - summary = f"🥽 VR性能: {stats['vr_fps']:.1f}fps" - - if stats['frame_time_avg'] > 0: - summary += f" | 帧时间: {stats['frame_time_avg']:.1f}ms" - - if self.psutil_available: - summary += f" | CPU: {stats['cpu_usage']:.0f}%" - summary += f" | 内存: {stats['memory_usage']:.0f}%" - - # 显示GPU信息,如果库不可用则显示提示 - if self.gputil_available or self.nvidia_ml_available: - summary += f" | GPU: {stats['gpu_usage']:.0f}%" - else: - summary += " | GPU: N/A" - - # 添加失败率指示 - if stats['frame_count'] > 0: - submit_fail_rate = (stats['submit_failures'] / stats['frame_count']) * 100 - if submit_fail_rate > 0.1: - summary += f" | 提交失败: {submit_fail_rate:.1f}%" - - # 添加管线关键信息 - if self.enable_pipeline_monitoring: - pipeline_stats = self._get_pipeline_stats() - current = pipeline_stats['current'] - - # 显示关键瓶颈 - if current['wait_poses'] > 5.0: - summary += f" | 姿态: {current['wait_poses']:.1f}ms⚠️" - elif current['wait_poses'] > 0: - summary += f" | 姿态: {current['wait_poses']:.1f}ms" - - if current['total_render'] > 8.0: - summary += f" | 渲染: {current['total_render']:.1f}ms⚠️" - elif current['total_render'] > 0: - summary += f" | 渲染: {current['total_render']:.1f}ms" - - # 添加GPU渲染时间信息 - if self.enable_gpu_timing: - gpu_total = current.get('gpu_total_render', 0) - gpu_scene = current.get('gpu_scene_render', 0) - if gpu_total > 12.0: - summary += f" | GPU: {gpu_total:.1f}ms⚠️" - elif gpu_total > 0: - summary += f" | GPU: {gpu_total:.1f}ms" - elif gpu_scene > 0: - summary += f" | GPU场景: {gpu_scene:.1f}ms" - - # 显示目标帧时间对比 - vr_info = pipeline_stats['vr_info'] - if vr_info['target_frame_time_ms'] > 0: - target = vr_info['target_frame_time_ms'] - current_avg = stats['frame_time_avg'] - if current_avg > target * 1.1: - summary += f" | 目标:{target:.0f}ms⚠️" - else: - summary += f" | 目标:{target:.0f}ms" - - # 性能状态指示 - if stats['vr_fps'] < 72: - summary += " ⚠️" - elif stats['vr_fps'] > 85: - summary += " ✅" - - print(summary) - - # ========== 性能监控核心方法 ========== - - def _init_performance_monitoring(self): - """初始化性能监控库""" - self.psutil_available = False - self.gputil_available = False - self.nvidia_ml_available = False - - try: - import psutil - self.psutil = psutil - self.psutil_available = True - print("✓ psutil性能监控库已加载") - except ImportError: - print("⚠️ psutil库未安装,CPU和内存监控将不可用") - - try: - import GPUtil - self.gputil = GPUtil - self.gputil_available = True - print("✓ GPUtil GPU监控库已加载") - except ImportError: - print("⚠️ GPUtil库未安装,GPU监控将不可用") - - try: - import pynvml - self.pynvml = pynvml - pynvml.nvmlInit() - self.nvidia_ml_available = True - print("✓ NVIDIA-ML GPU监控库已加载") - except ImportError: - print("⚠️ pynvml库未安装,NVIDIA GPU详细监控将不可用") - except Exception as e: - print(f"⚠️ NVIDIA-ML初始化失败: {e}") - - def _update_performance_metrics(self): - """更新系统性能指标""" - if not self.performance_monitoring: - return - - current_time = time.time() - - # 限制更新频率 - if current_time - self.last_performance_check < self.performance_check_interval: - return - - self.last_performance_check = current_time - - try: - # 更新CPU和内存使用率 - if self.psutil_available: - self.cpu_usage = self.psutil.cpu_percent(interval=None) - memory = self.psutil.virtual_memory() - self.memory_usage = memory.percent - - # 更新GPU使用率 - self._update_gpu_metrics() - - except Exception as e: - if self.frame_count % 600 == 0: # 每10秒输出一次错误 - print(f"⚠️ 性能监控更新失败: {e}") - - def _update_gpu_metrics(self): - """更新GPU相关指标""" - try: - # 方法1: 使用GPUtil - if self.gputil_available: - gpus = self.gputil.getGPUs() - if gpus: - gpu = gpus[0] # 使用第一个GPU - self.gpu_usage = gpu.load * 100 - self.gpu_memory_usage = gpu.memoryUtil * 100 - - # 方法2: 使用NVIDIA-ML (更精确) - elif self.nvidia_ml_available: - try: - handle = self.pynvml.nvmlDeviceGetHandleByIndex(0) - - # GPU使用率 - utilization = self.pynvml.nvmlDeviceGetUtilizationRates(handle) - self.gpu_usage = utilization.gpu - - # GPU内存使用率 - memory_info = self.pynvml.nvmlDeviceGetMemoryInfo(handle) - self.gpu_memory_usage = (memory_info.used / memory_info.total) * 100 - - except Exception as e: - # NVIDIA-ML可能无法在某些系统上工作 - pass - - except Exception as e: - # GPU监控失败,但不影响VR功能 - pass - - def _track_frame_time(self): - """记录帧时间 - 性能优化版本""" - current_time = time.time() - - if self._last_frame_time is not None: - frame_time = (current_time - self._last_frame_time) * 1000 # 转换为毫秒 - - # 🚀 性能优化:性能模式下跳过列表操作以减少内存分配 - if not self.performance_mode_enabled: - # 添加到帧时间历史 - self.frame_times.append(frame_time) - - # 限制历史长度 - if len(self.frame_times) > self.max_frame_time_history: - self.frame_times.pop(0) - - self._last_frame_time = current_time - - # ========== GPU计时方法 ========== - - def _get_gpu_frame_timing(self, frames_ago=0): - """获取GPU渲染时间统计 - - Args: - frames_ago: 获取多少帧之前的数据,0表示当前帧 - - Returns: - dict: GPU时间数据,如果获取失败返回None - """ - if not self.enable_gpu_timing or not self.vr_manager.vr_compositor: - return None - - try: - # 调用OpenVR的getFrameTiming API - result, timing = self.vr_manager.vr_compositor.getFrameTiming(framesAgo=frames_ago) - - if not result: - self.gpu_timing_failure_count += 1 - if self.gpu_timing_failure_count % 300 == 1: # 每5秒输出一次错误 - print("⚠️ OpenVR getFrameTiming调用失败") - return None - - # 提取GPU时间数据(单位:毫秒) - gpu_data = {} - - # 检查timing对象是否有GPU时间相关的属性 - if hasattr(timing, 'm_flSceneRenderGpuMs'): - gpu_data['scene_render'] = timing.m_flSceneRenderGpuMs - self.gpu_scene_render_ms = timing.m_flSceneRenderGpuMs - - if hasattr(timing, 'm_flPreSubmitGpuMs'): - gpu_data['pre_submit'] = timing.m_flPreSubmitGpuMs - self.gpu_pre_submit_ms = timing.m_flPreSubmitGpuMs - - if hasattr(timing, 'm_flPostSubmitGpuMs'): - gpu_data['post_submit'] = timing.m_flPostSubmitGpuMs - self.gpu_post_submit_ms = timing.m_flPostSubmitGpuMs - - if hasattr(timing, 'm_flTotalRenderGpuMs'): - gpu_data['total_render'] = timing.m_flTotalRenderGpuMs - self.gpu_total_render_ms = timing.m_flTotalRenderGpuMs - - if hasattr(timing, 'm_flCompositorRenderGpuMs'): - gpu_data['compositor_render'] = timing.m_flCompositorRenderGpuMs - self.gpu_compositor_render_ms = timing.m_flCompositorRenderGpuMs - - if hasattr(timing, 'm_flClientFrameIntervalMs'): - gpu_data['frame_interval'] = timing.m_flClientFrameIntervalMs - self.gpu_client_frame_interval_ms = timing.m_flClientFrameIntervalMs - - # 将GPU时间数据添加到历史记录 - if gpu_data: - self.gpu_timing_history.append(gpu_data) - if len(self.gpu_timing_history) > self.gpu_timing_history_size: - self.gpu_timing_history.pop(0) - - # 调试信息 - 仅在第一次成功时输出 - if not hasattr(self, '_gpu_timing_success_logged'): - available_fields = list(gpu_data.keys()) - print(f"✅ GPU时间统计已启用 - 可用字段: {available_fields}") - self._gpu_timing_success_logged = True - - return gpu_data - - except AttributeError as e: - # OpenVR Python绑定可能不包含某些字段 - if self.gpu_timing_failure_count == 0: - print(f"⚠️ GPU时间统计部分功能不可用: {e}") - print(" 这可能是由于OpenVR Python绑定版本问题") - self.gpu_timing_failure_count += 1 - return None - - except Exception as e: - self.gpu_timing_failure_count += 1 - if self.gpu_timing_failure_count % 300 == 1: # 每5秒输出一次错误 - print(f"⚠️ 获取GPU时间统计失败: {e}") - return None - - def enable_gpu_timing_monitoring(self): - """启用GPU时间监控""" - self.enable_gpu_timing = True - print("✓ VR GPU时间监控已启用") - - def disable_gpu_timing_monitoring(self): - """禁用GPU时间监控""" - self.enable_gpu_timing = False - print("✓ VR GPU时间监控已禁用") - - # ========== 管线统计方法 ========== - - def _start_timing(self, operation_name): - """开始计时操作 - 性能优化版本""" - if not self.enable_pipeline_monitoring or self.performance_mode_enabled: - return None - - # 🚀 性能优化:减少字典创建,只在必要时创建 - return { - 'operation': operation_name, - 'start_time': time.perf_counter() - } - - def _end_timing(self, timing_data): - """结束计时并记录结果 - 性能优化版本""" - if not self.enable_pipeline_monitoring or self.performance_mode_enabled or not timing_data: - return 0.0 - - elapsed = (time.perf_counter() - timing_data['start_time']) * 1000 # 转换为毫秒 - - # 保存到相应的历史记录 - operation = timing_data['operation'] - if operation == 'wait_poses': - self.wait_poses_time = elapsed - self.wait_poses_times.append(elapsed) - if len(self.wait_poses_times) > self.pipeline_history_size: - self.wait_poses_times.pop(0) - elif operation == 'left_render': - self.left_render_time = elapsed - elif operation == 'right_render': - self.right_render_time = elapsed - elif operation == 'submit': - self.submit_time = elapsed - self.submit_times.append(elapsed) - if len(self.submit_times) > self.pipeline_history_size: - self.submit_times.pop(0) - elif operation == 'sync_wait': - self.vr_sync_wait_time = elapsed - self.sync_wait_times.append(elapsed) - if len(self.sync_wait_times) > self.pipeline_history_size: - self.sync_wait_times.pop(0) - - # 计算总渲染时间 - total_render = self.left_render_time + self.right_render_time - self.render_times.append(total_render) - if len(self.render_times) > self.pipeline_history_size: - self.render_times.pop(0) - - return elapsed - - def _get_pipeline_stats(self): - """获取渲染管线统计信息""" - def get_stats(times_list): - if not times_list: - return {'avg': 0.0, 'min': 0.0, 'max': 0.0} - return { - 'avg': sum(times_list) / len(times_list), - 'min': min(times_list), - 'max': max(times_list) - } - - # 计算GPU时间统计 - def get_gpu_field_stats(field_name): - """从GPU时间历史记录中提取特定字段的统计信息""" - values = [] - for gpu_data in self.gpu_timing_history: - if field_name in gpu_data: - values.append(gpu_data[field_name]) - return get_stats(values) - - return { - 'wait_poses': get_stats(self.wait_poses_times), - 'render': get_stats(self.render_times), - 'submit': get_stats(self.submit_times), - 'sync_wait': get_stats(self.sync_wait_times), - 'gpu_timing': { - 'scene_render': get_gpu_field_stats('scene_render'), - 'pre_submit': get_gpu_field_stats('pre_submit'), - 'post_submit': get_gpu_field_stats('post_submit'), - 'total_render': get_gpu_field_stats('total_render'), - 'compositor_render': get_gpu_field_stats('compositor_render'), - 'frame_interval': get_gpu_field_stats('frame_interval') - }, - 'current': { - 'wait_poses': self.wait_poses_time, - 'left_render': self.left_render_time, - 'right_render': self.right_render_time, - 'submit': self.submit_time, - 'sync_wait': self.vr_sync_wait_time, - 'total_render': self.left_render_time + self.right_render_time, - 'gpu_scene_render': self.gpu_scene_render_ms, - 'gpu_pre_submit': self.gpu_pre_submit_ms, - 'gpu_post_submit': self.gpu_post_submit_ms, - 'gpu_total_render': self.gpu_total_render_ms, - 'gpu_compositor_render': self.gpu_compositor_render_ms, - 'gpu_frame_interval': self.gpu_client_frame_interval_ms - }, - 'vr_info': { - 'eye_resolution': self.current_eye_resolution, - 'recommended_resolution': self.recommended_eye_resolution, - 'display_frequency': self.vr_display_frequency, - 'vsync_enabled': self.vr_vsync_enabled, - 'target_frame_time_ms': self.target_frame_time_ms, - 'vsync_to_photons_ms': self.vsync_to_photons_ms, - 'vsync_window_ms': self.vsync_window_ms, - 'async_reprojection': self.async_reprojection_enabled, - 'motion_smoothing': self.motion_smoothing_enabled, - 'gpu_timing_enabled': self.enable_gpu_timing, - 'gpu_timing_failures': self.gpu_timing_failure_count - } - } - - def test_pipeline_monitoring(self): - """测试管线监控功能 - 用于调试和验证""" - print("🔧 正在测试VR管线监控功能...") - - try: - # 测试基本信息获取 - print("📊 基本信息:") - print(f" 当前分辨率: {self.current_eye_resolution}") - print(f" 显示频率: {self.vr_display_frequency} Hz") - print(f" 目标帧时间: {self.target_frame_time_ms:.2f}ms") - - # Running Start模式信息 - print("🎯 Running Start模式:") - print(f" 预测时间: {self.vr_manager.use_prediction_time * 1000:.1f}ms") - print(f" 模式: Valve Running Start - 帧开始时获取姿态") - - # 测试管线统计 - if self.enable_pipeline_monitoring: - pipeline_stats = self._get_pipeline_stats() - print("⚙️ 管线统计:") - print(f" waitGetPoses历史: {len(self.wait_poses_times)}帧") - print(f" 渲染历史: {len(self.render_times)}帧") - print(f" 提交历史: {len(self.submit_times)}帧") - - # 测试性能报告 - print("📋 生成详细性能报告:") - self._print_performance_report() - - print("✅ 管线监控功能测试完成") - else: - print("⚠️ 管线监控已禁用,无法测试统计功能") - - except Exception as e: - print(f"❌ 测试管线监控功能时发生错误: {e}") - import traceback - traceback.print_exc() - - def set_prediction_time(self, prediction_time_ms): - """设置预测时间(仅用于update_task策略) - - Args: - prediction_time_ms: 预测时间,单位毫秒(通常8-16ms) - """ - prediction_time_s = prediction_time_ms / 1000.0 - if prediction_time_s < 0.005 or prediction_time_s > 0.020: - print(f"⚠️ 预测时间超出推荐范围(5-20ms): {prediction_time_ms}ms") - - old_time = self.vr_manager.use_prediction_time * 1000 - self.vr_manager.use_prediction_time = prediction_time_s - print(f"✓ VR预测时间已设置: {old_time:.1f}ms → {prediction_time_ms:.1f}ms") - - # ========== 诊断工具方法 ========== - - def _print_render_callback_diagnostics(self): - """输出渲染回调诊断信息 - 包含优化效果分析""" - try: - print(f"🔍 DrawCallback渲染诊断:") - - # 回调次数统计 - left_count = self.left_render_count - right_count = self.right_render_count - print(f" 渲染次数: 左眼={left_count}, 右眼={right_count}") - - if left_count == 0 and right_count == 0: - print(f" ❌ VR缓冲区未被渲染 - 这是严重问题!") - return - - # 渲染次数平衡性检查 - if abs(left_count - right_count) > 5: - print(f" ⚠️ 左右眼渲染次数不平衡: 差异={abs(left_count - right_count)}") - else: - print(f" ✓ 左右眼渲染次数平衡") - - # 🔧 真实渲染时间分析 - 显示优化效果 - left_render_time = self.left_render_time - right_render_time = self.right_render_time - total_render_time = left_render_time + right_render_time - - print(f" ⏱️ 精确渲染时间测量:") - print(f" 左眼cbdata.upcall(): {left_render_time:.2f}ms") - print(f" 右眼cbdata.upcall(): {right_render_time:.2f}ms") - print(f" 总计: {total_render_time:.2f}ms") - - # 渲染性能评估 - if total_render_time > 16.0: # 超过60FPS时间 - print(f" 🔴 渲染时间过长: {total_render_time:.1f}ms (目标<13.9ms@72Hz)") - print(f" 建议: 检查RenderPipeline优化是否生效") - elif total_render_time > 10.0: - print(f" 🟡 渲染时间偏高: {total_render_time:.1f}ms (可接受)") - else: - print(f" 🟢 渲染性能良好: {total_render_time:.1f}ms") - - # 🔧 渲染管线优化状态检查 - self._check_rendering_optimizations() - - # OpenGL状态诊断 - self._diagnose_opengl_state() - - except Exception as e: - print(f" 渲染回调诊断失败: {e}") - - def _check_rendering_optimizations(self): - """检查渲染优化状态""" - try: - print(f" 🔧 渲染优化状态:") - - # 检查RenderPipeline优化 - if hasattr(self.vr_manager.world, 'render_pipeline') and self.vr_manager.world.render_pipeline: - print(f" RenderPipeline: 已检测并优化") - else: - print(f" RenderPipeline: 未检测到(使用基础Panda3D)") - - # 检查GPU同步优化 - if hasattr(self.vr_manager, '_smart_sync_logged'): - last_sync_frame = getattr(self.vr_manager, '_last_gpu_sync_frame', 0) - current_frame = self.frame_count - frames_since_sync = current_frame - last_sync_frame - print(f" 智能GPU同步: 已启用 (距离上次同步: {frames_since_sync}帧)") - else: - print(f" 智能GPU同步: 未初始化") - - # 检查对象池状态 - matrix_pool_status = self.vr_manager.get_object_pool_status() - print(f" 对象池: Mat4={matrix_pool_status['matrix_pool_size']}/{matrix_pool_status['matrix_pool_capacity']}") - - # 检查垃圾回收控制 - gc_status = self.get_debug_status() - if gc_status['gc_disabled']: - print(f" GC控制: 已启用手动模式 (间隔:{gc_status['manual_gc_interval']}帧)") - else: - print(f" GC控制: 自动模式") - - except Exception as e: - print(f" 优化状态检查失败: {e}") - - def _diagnose_opengl_state(self): - """诊断OpenGL渲染状态""" - try: - # 检查VR缓冲区状态 - if hasattr(self.vr_manager, 'vr_left_eye_buffer') and self.vr_manager.vr_left_eye_buffer: - left_gsg = self.vr_manager.vr_left_eye_buffer.getGsg() - left_valid = self.vr_manager.vr_left_eye_buffer.isValid() - print(f" 左眼缓冲区: {'有效' if left_valid else '无效'}") - - if hasattr(self.vr_manager, 'vr_right_eye_buffer') and self.vr_manager.vr_right_eye_buffer: - right_gsg = self.vr_manager.vr_right_eye_buffer.getGsg() - right_valid = self.vr_manager.vr_right_eye_buffer.isValid() - print(f" 右眼缓冲区: {'有效' if right_valid else '无效'}") - - # 检查纹理准备状态 - if hasattr(self.vr_manager, 'textures_prepared'): - print(f" 纹理准备状态: {'已准备' if self.vr_manager.textures_prepared else '未准备'}") - - # 检查纹理ID缓存 - if hasattr(self.vr_manager, 'left_texture_id') and hasattr(self.vr_manager, 'right_texture_id'): - left_id = self.vr_manager.left_texture_id or 0 - right_id = self.vr_manager.right_texture_id or 0 - print(f" 纹理ID缓存: 左眼={left_id}, 右眼={right_id}") - - if left_id == 0 or right_id == 0: - print(f" ⚠️ 检测到无效的纹理ID,这可能导致提交失败") - else: - print(f" ✓ 纹理ID缓存正常") - - # 检查场景渲染状态 - if hasattr(self.vr_manager.world, 'render') and self.vr_manager.world.render: - render_children = len(self.vr_manager.world.render.getChildren()) - print(f" 场景节点数: {render_children}") - if render_children == 0: - print(f" ⚠️ 场景为空,可能导致渲染时间异常短") - - except Exception as e: - print(f" OpenGL状态诊断失败: {e}") - - # ========== 调试控制方法 ========== - - def enable_debug_output(self): - """启用调试输出""" - self.debug_output_enabled = True - print("✓ VR调试输出已启用") - - def disable_debug_output(self): - """禁用调试输出""" - self.debug_output_enabled = False - print("✓ VR调试输出已禁用") - - def set_debug_mode(self, mode): - """设置调试模式 - - Args: - mode: 'brief' 或 'detailed' - """ - if mode in ['brief', 'detailed']: - self.debug_mode = mode - print(f"✓ VR调试模式设置为: {mode}") - else: - print("⚠️ 调试模式只能是 'brief' 或 'detailed'") - - def toggle_debug_output(self): - """切换调试输出状态""" - self.debug_output_enabled = not self.debug_output_enabled - status = "启用" if self.debug_output_enabled else "禁用" - print(f"✓ VR调试输出已{status}") - return self.debug_output_enabled - - def get_debug_status(self): - """获取调试状态""" - return { - 'debug_enabled': self.debug_output_enabled, - 'debug_mode': self.debug_mode, - 'performance_monitoring': self.performance_monitoring, - 'report_interval_frames': self.performance_report_interval, - 'report_interval_seconds': self.performance_report_interval / 60, # 假设60fps - 'gc_control_enabled': self.vr_manager._gc_control_enabled, - 'gc_disabled': self.vr_manager._gc_disabled, - 'manual_gc_interval': self.vr_manager._manual_gc_interval, - } - - # ========== 配置方法 ========== - - def set_performance_check_interval(self, interval): - """设置性能检查间隔 - - Args: - interval: 检查间隔(秒),建议0.1-2.0之间 - """ - if 0.1 <= interval <= 5.0: - self.performance_check_interval = interval - print(f"✓ 性能监控间隔设置为 {interval:.1f} 秒") - else: - print("⚠️ 性能监控间隔应在0.1-5.0秒之间") - - def set_frame_time_history_size(self, size): - """设置帧时间历史记录大小 - - Args: - size: 历史记录大小(帧数),建议30-300之间 - """ - if 10 <= size <= 1000: - self.max_frame_time_history = size - # 清理超出的历史记录 - if len(self.frame_times) > size: - self.frame_times = self.frame_times[-size:] - print(f"✓ 帧时间历史记录大小设置为 {size} 帧") - else: - print("⚠️ 帧时间历史记录大小应在10-1000帧之间") - - def set_performance_report_interval(self, frames): - """设置性能报告输出间隔 - - Args: - frames: 帧数间隔,建议300-7200之间(5秒-2分钟@60fps) - """ - if 300 <= frames <= 7200: - # 修改_update_vr中的报告间隔 - print(f"✓ 性能报告间隔设置为每 {frames} 帧(约 {frames/60:.1f} 秒@60fps)") - # 这里可以添加一个实例变量来控制 - self.performance_report_interval = frames - else: - print("⚠️ 性能报告间隔应在300-7200帧之间") - - # ========== 查询方法 ========== - - def get_performance_stats(self): - """获取详细的性能统计信息""" - stats = { - 'vr_fps': self.vr_fps, - 'frame_count': self.frame_count, - 'submit_failures': self.submit_failures, - 'pose_failures': self.pose_failures, - 'cpu_usage': self.cpu_usage, - 'memory_usage': self.memory_usage, - 'gpu_usage': self.gpu_usage, - 'gpu_memory_usage': self.gpu_memory_usage, - } - - # 计算帧时间统计 - if self.frame_times: - stats['frame_time_avg'] = sum(self.frame_times) / len(self.frame_times) - stats['frame_time_min'] = min(self.frame_times) - stats['frame_time_max'] = max(self.frame_times) - stats['frame_time_95th'] = sorted(self.frame_times)[int(len(self.frame_times) * 0.95)] - else: - stats['frame_time_avg'] = 0 - stats['frame_time_min'] = 0 - stats['frame_time_max'] = 0 - stats['frame_time_95th'] = 0 - - return stats - - def get_current_performance_summary(self): - """获取当前性能摘要(简短版本)""" - stats = self.get_performance_stats() - - summary = f"VR性能: {stats['vr_fps']:.1f}fps" - - if stats['frame_time_avg'] > 0: - summary += f" | 帧时间: {stats['frame_time_avg']:.1f}ms" - - if self.psutil_available: - summary += f" | CPU: {stats['cpu_usage']:.0f}%" - summary += f" | 内存: {stats['memory_usage']:.0f}%" - - # 显示GPU信息,如果库不可用则显示提示 - if self.gputil_available or self.nvidia_ml_available: - summary += f" | GPU: {stats['gpu_usage']:.0f}%" - else: - summary += " | GPU: N/A" - - return summary - - def get_performance_monitoring_config(self): - """获取当前性能监控配置""" - return { - 'enabled': self.performance_monitoring, - 'check_interval': self.performance_check_interval, - 'frame_history_size': self.max_frame_time_history, - 'report_interval': self.performance_report_interval, - 'psutil_available': self.psutil_available, - 'gputil_available': self.gputil_available, - 'nvidia_ml_available': self.nvidia_ml_available - } - - def print_performance_monitoring_status(self): - """输出性能监控状态""" - config = self.get_performance_monitoring_config() - - print("🔧 ===== VR性能监控配置 =====") - print(f" 监控状态: {'✅ 启用' if config['enabled'] else '❌ 禁用'}") - print(f" 检查间隔: {config['check_interval']:.1f} 秒") - print(f" 帧时间历史: {config['frame_history_size']} 帧") - print(f" 报告间隔: {config['report_interval']} 帧") - - print(f" 可用库:") - print(f" psutil (CPU/内存): {'✅' if config['psutil_available'] else '❌'}") - print(f" GPUtil (GPU): {'✅' if config['gputil_available'] else '❌'}") - print(f" NVIDIA-ML (GPU): {'✅' if config['nvidia_ml_available'] else '❌'}") - print("=============================") - - # ========== 控制方法 ========== - - def enable_performance_monitoring(self): - """启用性能监控""" - self.performance_monitoring = True - print("✓ VR性能监控已启用") - - def disable_performance_monitoring(self): - """禁用性能监控""" - self.performance_monitoring = False - print("✓ VR性能监控已禁用") - - def force_performance_report(self): - """强制输出一次性能报告""" - print("🔄 手动触发性能报告...") - self._print_performance_report() - - def reset_performance_counters(self): - """重置性能计数器""" - self.frame_count = 0 - self.last_fps_check = 0 - self.last_fps_time = 0 - self.vr_fps = 0 - self.submit_failures = 0 - self.pose_failures = 0 - self.frame_times.clear() - print("✅ 性能计数器已重置") +""" +VR性能监控子系统 +负责VR应用的性能监控、GPU计时、管线统计和性能诊断 +""" + +import sys +import gc +import time + +# 可选依赖 +try: + import psutil + PSUTIL_AVAILABLE = True +except ImportError: + PSUTIL_AVAILABLE = False + +try: + import GPUtil + GPUTIL_AVAILABLE = True +except ImportError: + GPUTIL_AVAILABLE = False + +try: + import pynvml + PYNVML_AVAILABLE = True +except ImportError: + PYNVML_AVAILABLE = False + + +class VRPerformanceMonitor: + """VR性能监控系统 + + 功能: + - 实时性能监控(CPU、内存、GPU) + - GPU渲染时间统计 + - 渲染管线分析 + - 性能诊断和优化建议 + """ + + def __init__(self, vr_manager): + """初始化性能监控系统 + + Args: + vr_manager: VRManager实例的引用 + """ + self.vr_manager = vr_manager + + # ===== 性能计数器 ===== + self.frame_count = 0 + self.last_fps_check = 0 + self.last_fps_time = 0 + self.vr_fps = 0 + self.submit_failures = 0 + self.pose_failures = 0 + + # ===== 性能监控配置 ===== + self.performance_monitoring = False # 是否启用性能监控 + self.debug_output_enabled = False # 是否启用调试输出 + self.debug_mode = 'detailed' # 'brief' 或 'detailed' + self.cpu_usage = 0.0 + self.memory_usage = 0.0 + self.gpu_usage = 0.0 + self.gpu_memory_usage = 0.0 + self.frame_times = [] # 存储最近帧时间 + self.max_frame_time_history = 60 # 保存60帧的历史 + self.last_performance_check = 0 + self.performance_check_interval = 0.5 # 每0.5秒更新一次性能数据 + + # ===== 渲染管线监控 ===== + self.enable_pipeline_monitoring = True # 是否启用管线监控 + self.performance_mode_enabled = False # 性能优化模式(禁用监控以减少对象创建) + self.performance_mode_trigger_frame = 600 # 第600帧后启用性能模式 + self.wait_poses_time = 0.0 # waitGetPoses耗时 + self.left_render_time = 0.0 # 左眼渲染耗时 + self.right_render_time = 0.0 # 右眼渲染耗时 + self.submit_time = 0.0 # 纹理提交耗时 + self.left_render_count = 0 # 左眼渲染次数计数 + self.right_render_count = 0 # 右眼渲染次数计数 + self.total_frame_time = 0.0 # 总帧时间 + self.vr_sync_wait_time = 0.0 # VR同步等待时间 + + # ===== 时间监控历史 ===== + self.wait_poses_times = [] + self.render_times = [] + self.submit_times = [] + self.sync_wait_times = [] + self.pipeline_history_size = 30 + + # ===== GPU渲染时间监控 ===== + self.enable_gpu_timing = False # 是否启用GPU时间监控(默认关闭) + self.gpu_scene_render_ms = 0.0 # GPU场景渲染时间 + self.gpu_pre_submit_ms = 0.0 # 提交前GPU时间 + self.gpu_post_submit_ms = 0.0 # 提交后GPU时间 + self.gpu_total_render_ms = 0.0 # GPU总渲染时间 + self.gpu_compositor_render_ms = 0.0 # GPU合成器渲染时间 + self.gpu_client_frame_interval_ms = 0.0 # 客户端帧间隔 + self.gpu_timing_history = [] # GPU时间历史记录 + self.gpu_timing_history_size = 30 # GPU时间历史记录大小 + self.gpu_timing_failure_count = 0 # GPU时间获取失败次数 + + # ===== VR系统信息 ===== + self.current_eye_resolution = (0, 0) + self.recommended_eye_resolution = (0, 0) + self.vr_display_frequency = 0.0 + self.vr_vsync_enabled = True + self.vsync_to_photons_ms = 0.0 # VSync到光子的延迟 + self.target_frame_time_ms = 0.0 # 目标帧时间 + self.vsync_window_ms = 0.0 # VSync时间窗口 + self.async_reprojection_enabled = False # 异步重投影状态 + self.motion_smoothing_enabled = False # 运动平滑状态 + + # ===== 性能报告间隔 ===== + self.performance_report_interval = 1800 # 默认1800帧(30秒@60fps) + + # ===== 内部状态 ===== + self._last_frame_time = None # 用于帧时间计算 + + # 初始化性能监控库 + self._init_performance_monitoring() + + # ========== 性能报告方法 ========== + + def _print_performance_report(self): + """输出VR性能报告""" + if not self.performance_monitoring or not self.debug_output_enabled: + return + + stats = self.get_performance_stats() + + # 简短模式输出 + if self.debug_mode == 'brief': + self._print_brief_performance_report(stats) + return + + print("📊 ======= VR性能监控报告 =======") + + # 帧率和帧时间信息 + print(f"🎯 渲染性能:") + print(f" VR帧率: {stats['vr_fps']:.1f} FPS") + print(f" 平均帧时间: {stats['frame_time_avg']:.2f} ms") + print(f" 最小帧时间: {stats['frame_time_min']:.2f} ms") + print(f" 最大帧时间: {stats['frame_time_max']:.2f} ms") + print(f" 95%帧时间: {stats['frame_time_95th']:.2f} ms") + + # 系统性能 + print(f"💻 系统性能:") + print(f" CPU使用率: {stats['cpu_usage']:.1f}%") + print(f" 内存使用率: {stats['memory_usage']:.1f}%") + + # GPU性能 + print(f"🎮 GPU性能:") + if self.gputil_available or self.nvidia_ml_available: + print(f" GPU使用率: {stats['gpu_usage']:.1f}%") + print(f" 显存使用率: {stats['gpu_memory_usage']:.1f}%") + else: + print(f" GPU监控: 不可用 (需要安装 GPUtil 或 pynvml)") + print(f" 安装命令: pip install GPUtil nvidia-ml-py") + + # GPU渲染时间(OpenVR Frame Timing) + if self.enable_gpu_timing: + print(f"⚡ GPU渲染时间:") + pipeline_stats = self._get_pipeline_stats() + gpu_stats = pipeline_stats.get('gpu_timing', {}) + gpu_current = pipeline_stats.get('current', {}) + + # 检查是否有可用的GPU时间数据 + has_gpu_data = any( + gpu_current.get(field, 0) > 0 + for field in ['gpu_scene_render', 'gpu_total_render', 'gpu_pre_submit', 'gpu_post_submit', 'gpu_compositor_render'] + ) + + if has_gpu_data: + # 显示GPU时间统计(最近30帧平均) + scene_render = gpu_stats.get('scene_render', {'avg': 0}) + total_render = gpu_stats.get('total_render', {'avg': 0}) + pre_submit = gpu_stats.get('pre_submit', {'avg': 0}) + post_submit = gpu_stats.get('post_submit', {'avg': 0}) + compositor = gpu_stats.get('compositor_render', {'avg': 0}) + + if scene_render['avg'] > 0: + print(f" GPU场景渲染: {scene_render['avg']:.2f}ms (min:{scene_render['min']:.1f}, max:{scene_render['max']:.1f})") + if total_render['avg'] > 0: + print(f" GPU总渲染时间: {total_render['avg']:.2f}ms (min:{total_render['min']:.1f}, max:{total_render['max']:.1f})") + if pre_submit['avg'] > 0: + print(f" GPU提交前时间: {pre_submit['avg']:.2f}ms (min:{pre_submit['min']:.1f}, max:{pre_submit['max']:.1f})") + if post_submit['avg'] > 0: + print(f" GPU提交后时间: {post_submit['avg']:.2f}ms (min:{post_submit['min']:.1f}, max:{post_submit['max']:.1f})") + if compositor['avg'] > 0: + print(f" GPU合成器时间: {compositor['avg']:.2f}ms (min:{compositor['min']:.1f}, max:{compositor['max']:.1f})") + + # 显示当前帧GPU时间 + print(f"🔍 当前帧GPU时间:") + if gpu_current.get('gpu_scene_render', 0) > 0: + print(f" 场景渲染: {gpu_current['gpu_scene_render']:.2f}ms") + if gpu_current.get('gpu_total_render', 0) > 0: + print(f" 总渲染: {gpu_current['gpu_total_render']:.2f}ms") + if gpu_current.get('gpu_compositor_render', 0) > 0: + print(f" 合成器: {gpu_current['gpu_compositor_render']:.2f}ms") + + # GPU时间瓶颈分析 + current_total = gpu_current.get('gpu_total_render', 0) + current_scene = gpu_current.get('gpu_scene_render', 0) + if current_total > 12.0: # 假设72fps目标,留出一些余量 + print(f" ⚠️ GPU总渲染时间过长: {current_total:.1f}ms") + elif current_scene > 8.0: + print(f" ⚠️ GPU场景渲染时间偏高: {current_scene:.1f}ms") + + else: + print(f" GPU渲染时间: 暂无数据") + if self.gpu_timing_failure_count > 0: + print(f" 获取失败次数: {self.gpu_timing_failure_count}") + else: + print(f" 等待OpenVR Frame Timing数据...") + else: + print(f"⚡ GPU渲染时间: 已禁用") + + # VR特定指标 + print(f"🥽 VR指标:") + print(f" 总帧数: {stats['frame_count']}") + print(f" 提交失败: {stats['submit_failures']}") + print(f" 姿态失败: {stats['pose_failures']}") + + # 计算失败率 + if stats['frame_count'] > 0: + submit_fail_rate = (stats['submit_failures'] / stats['frame_count']) * 100 + pose_fail_rate = (stats['pose_failures'] / stats['frame_count']) * 100 + print(f" 提交失败率: {submit_fail_rate:.2f}%") + print(f" 姿态失败率: {pose_fail_rate:.2f}%") + + # 渲染管线监控 + target_frame_time = 13.9 # 默认目标帧时间(72Hz) + if self.enable_pipeline_monitoring: + pipeline_stats = self._get_pipeline_stats() + print(f"⚙️ 渲染管线分析:") + print(f" 当前分辨率: {pipeline_stats['vr_info']['eye_resolution'][0]}x{pipeline_stats['vr_info']['eye_resolution'][1]}") + print(f" 推荐分辨率: {pipeline_stats['vr_info']['recommended_resolution'][0]}x{pipeline_stats['vr_info']['recommended_resolution'][1]}") + print(f" 显示频率: {pipeline_stats['vr_info']['display_frequency']:.1f} Hz") + + # VSync和时序信息 + if pipeline_stats['vr_info']['target_frame_time_ms'] > 0: + print(f"🎯 VSync时序:") + print(f" 目标帧时间: {pipeline_stats['vr_info']['target_frame_time_ms']:.2f}ms") + print(f" VSync到光子: {pipeline_stats['vr_info']['vsync_to_photons_ms']:.2f}ms") + print(f" VSync窗口: ±{pipeline_stats['vr_info']['vsync_window_ms']:.2f}ms") + print(f" 异步重投影: {'启用' if pipeline_stats['vr_info']['async_reprojection'] else '禁用'}") + print(f" 运动平滑: {'启用' if pipeline_stats['vr_info']['motion_smoothing'] else '禁用'}") + + # 分析帧时间是否在目标范围内 + current_frame_time = stats['frame_time_avg'] + target_frame_time = pipeline_stats['vr_info']['target_frame_time_ms'] + if current_frame_time > 0 and target_frame_time > 0: + frame_time_ratio = current_frame_time / target_frame_time + if frame_time_ratio > 1.1: + print(f" ⚠️ 帧时间超标: {current_frame_time:.1f}ms (目标:{target_frame_time:.1f}ms)") + elif frame_time_ratio < 0.9: + print(f" ✅ 帧时间充裕: {current_frame_time:.1f}ms (目标:{target_frame_time:.1f}ms)") + else: + print(f" ✓ 帧时间正常: {current_frame_time:.1f}ms (目标:{target_frame_time:.1f}ms)") + + # 🔧 性能优化诊断 + print(f"🔧 优化诊断:") + current = pipeline_stats.get('current', {}) if self.enable_pipeline_monitoring else {} + + # 🔍 渲染回调诊断 - 新增 + self._print_render_callback_diagnostics() + + # waitGetPoses时序分析 + wait_poses_time = current.get('wait_poses', 0) + if wait_poses_time > 10: + print(f" ⚠️ waitGetPoses时间过长: {wait_poses_time:.1f}ms") + print(f" 可能原因: 错过VSync窗口,建议调整预测时间") + elif wait_poses_time > 5: + print(f" ⚠️ waitGetPoses时间偏高: {wait_poses_time:.1f}ms") + else: + print(f" ✓ waitGetPoses时间正常: {wait_poses_time:.1f}ms") + + # CPU-GPU并行度分析 - 增强诊断 + gpu_total = current.get('gpu_total_render', 0) + cpu_render_left = self.left_render_time + cpu_render_right = self.right_render_time + cpu_render_total = cpu_render_left + cpu_render_right + + print(f" 真实渲染时间对比:") + print(f" CPU左眼: {cpu_render_left:.2f}ms") + print(f" CPU右眼: {cpu_render_right:.2f}ms") + print(f" CPU总计: {cpu_render_total:.2f}ms") + print(f" GPU总计: {gpu_total:.2f}ms") + + if cpu_render_total < 1.0: + print(f" ⚠️ CPU渲染时间异常短: {cpu_render_total:.2f}ms") + print(f" 可能原因: 渲染回调未正确执行或渲染被跳过") + elif gpu_total > 0 and cpu_render_total > 0: + ratio = gpu_total / cpu_render_total + if ratio > 10: + print(f" ⚠️ GPU严重等待CPU: 比例{ratio:.1f}:1") + print(f" 建议: 检查OpenGL命令提交时机和渲染状态") + elif ratio > 3: + print(f" ⚠️ GPU等待CPU: 比例{ratio:.1f}:1") + print(f" 建议: 检查OpenGL命令提交是否及时") + else: + print(f" ✓ CPU-GPU时间匹配: 比例{ratio:.1f}:1") + + # 预测时间诊断 + current_prediction = self.vr_manager.use_prediction_time * 1000 + print(f" 预测时间设置: {current_prediction:.1f}ms") + if current_prediction > 15: + print(f" 建议: 预测时间较高,可能增加waitGetPoses延迟") + elif current_prediction < 8: + print(f" 注意: 预测时间较低,可能影响姿态准确性") + else: + print(f" ✓ 预测时间在合理范围内") + + # 优化状态总结 + optimization_score = 0 + if wait_poses_time < 8: + optimization_score += 1 + if stats['vr_fps'] > 60: + optimization_score += 1 + if stats.get('frame_time_avg', 0) < target_frame_time * 1.1: + optimization_score += 1 + + if optimization_score >= 2: + print(f" ✅ 优化效果良好 ({optimization_score}/3)") + else: + print(f" ⚠️ 仍有优化空间 ({optimization_score}/3)") + + # 显示详细统计信息(仅当管线监控启用时) + if self.enable_pipeline_monitoring: + print(f"🕐 各阶段耗时 (最近{self.pipeline_history_size}帧平均):") + print(f" waitGetPoses: {pipeline_stats['wait_poses']['avg']:.2f}ms (min:{pipeline_stats['wait_poses']['min']:.1f}, max:{pipeline_stats['wait_poses']['max']:.1f})") + print(f" 渲染总计: {pipeline_stats['render']['avg']:.2f}ms (min:{pipeline_stats['render']['min']:.1f}, max:{pipeline_stats['render']['max']:.1f})") + print(f" 纹理提交: {pipeline_stats['submit']['avg']:.2f}ms (min:{pipeline_stats['submit']['min']:.1f}, max:{pipeline_stats['submit']['max']:.1f})") + if pipeline_stats['sync_wait']['avg'] > 0: + print(f" 同步等待: {pipeline_stats['sync_wait']['avg']:.2f}ms (min:{pipeline_stats['sync_wait']['min']:.1f}, max:{pipeline_stats['sync_wait']['max']:.1f})") + + print(f"🔍 当前帧详情:") + print(f" 左眼渲染: {pipeline_stats['current']['left_render']:.2f}ms") + print(f" 右眼渲染: {pipeline_stats['current']['right_render']:.2f}ms") + print(f" 姿态获取: {pipeline_stats['current']['wait_poses']:.2f}ms") + + # 显示Running Start模式信息 + print(f"🎯 Running Start模式:") + print(f" 模式: Valve Running Start - 帧开始时获取姿态") + print(f" 优势: VSync前3ms获取姿态,提供充足渲染时间") + print(f" 预测时间: {self.vr_manager.use_prediction_time * 1000:.1f}ms") + + # 分析最大瓶颈 + current = pipeline_stats['current'] + bottleneck_analysis = [] + if current['wait_poses'] > 5.0: + bottleneck_analysis.append(f"姿态获取耗时过长({current['wait_poses']:.1f}ms)") + if current['total_render'] > 8.0: + bottleneck_analysis.append(f"渲染耗时过长({current['total_render']:.1f}ms)") + if current['submit'] > 2.0: + bottleneck_analysis.append(f"纹理提交耗时过长({current['submit']:.1f}ms)") + + if bottleneck_analysis: + print(f"🚨 瓶颈分析:") + for analysis in bottleneck_analysis: + print(f" ⚠️ {analysis}") + + # 性能建议 + self._print_performance_recommendations(stats) + + print("===============================") + + def _print_performance_recommendations(self, stats): + """根据性能数据输出优化建议""" + print(f"💡 性能建议:") + + recommendations = [] + + # FPS相关建议 + if stats['vr_fps'] < 60: + recommendations.append(" ⚠️ VR帧率过低,可能影响体验") + + # 帧时间相关建议 + if stats['frame_time_avg'] > 16.7: # 60fps = 16.7ms + recommendations.append(" ⚠️ 平均帧时间过高,建议降低渲染质量") + + if stats['frame_time_max'] > 50: + recommendations.append(" ⚠️ 检测到严重卡顿,检查CPU/GPU负载") + + # CPU建议 + if stats['cpu_usage'] > 80: + recommendations.append(" 🔴 CPU使用率过高,可能存在CPU瓶颈") + elif stats['cpu_usage'] > 60: + recommendations.append(" 🟡 CPU使用率偏高,注意监控") + + # 内存建议 + if stats['memory_usage'] > 85: + recommendations.append(" 🔴 内存使用率过高,可能影响性能") + + # GPU建议 + if self.gputil_available or self.nvidia_ml_available: + if stats['gpu_usage'] > 95: + recommendations.append(" 🔴 GPU使用率接近满载,存在GPU瓶颈") + if stats['gpu_memory_usage'] > 90: + recommendations.append(" 🔴 显存使用率过高,可能需要降低纹理质量") + + # GPU渲染时间建议 + if self.enable_gpu_timing: + if self.gpu_total_render_ms > 12.0: + recommendations.append(" ⚠️ GPU总渲染时间过长,建议优化场景复杂度") + if self.gpu_scene_render_ms > 8.0: + recommendations.append(" ⚠️ GPU场景渲染时间偏高,考虑降低渲染质量") + if self.gpu_compositor_render_ms > 3.0: + recommendations.append(" ⚠️ GPU合成器时间过长,检查VR设置或叠加层") + if self.gpu_timing_failure_count > 100: + recommendations.append(" ⚠️ GPU时间统计频繁失败,可能需要更新OpenVR") + + # 失败率建议 + submit_fail_rate = (stats['submit_failures'] / max(stats['frame_count'], 1)) * 100 + if submit_fail_rate > 1: + recommendations.append(" ⚠️ VR帧提交失败率较高,检查VR系统状态") + + if not recommendations: + recommendations.append(" ✅ 性能表现良好,无明显问题") + + for rec in recommendations: + print(rec) + + def _print_brief_performance_report(self, stats): + """输出简短的VR性能报告""" + # 创建一行简短摘要 + summary = f"🥽 VR性能: {stats['vr_fps']:.1f}fps" + + if stats['frame_time_avg'] > 0: + summary += f" | 帧时间: {stats['frame_time_avg']:.1f}ms" + + if self.psutil_available: + summary += f" | CPU: {stats['cpu_usage']:.0f}%" + summary += f" | 内存: {stats['memory_usage']:.0f}%" + + # 显示GPU信息,如果库不可用则显示提示 + if self.gputil_available or self.nvidia_ml_available: + summary += f" | GPU: {stats['gpu_usage']:.0f}%" + else: + summary += " | GPU: N/A" + + # 添加失败率指示 + if stats['frame_count'] > 0: + submit_fail_rate = (stats['submit_failures'] / stats['frame_count']) * 100 + if submit_fail_rate > 0.1: + summary += f" | 提交失败: {submit_fail_rate:.1f}%" + + # 添加管线关键信息 + if self.enable_pipeline_monitoring: + pipeline_stats = self._get_pipeline_stats() + current = pipeline_stats['current'] + + # 显示关键瓶颈 + if current['wait_poses'] > 5.0: + summary += f" | 姿态: {current['wait_poses']:.1f}ms⚠️" + elif current['wait_poses'] > 0: + summary += f" | 姿态: {current['wait_poses']:.1f}ms" + + if current['total_render'] > 8.0: + summary += f" | 渲染: {current['total_render']:.1f}ms⚠️" + elif current['total_render'] > 0: + summary += f" | 渲染: {current['total_render']:.1f}ms" + + # 添加GPU渲染时间信息 + if self.enable_gpu_timing: + gpu_total = current.get('gpu_total_render', 0) + gpu_scene = current.get('gpu_scene_render', 0) + if gpu_total > 12.0: + summary += f" | GPU: {gpu_total:.1f}ms⚠️" + elif gpu_total > 0: + summary += f" | GPU: {gpu_total:.1f}ms" + elif gpu_scene > 0: + summary += f" | GPU场景: {gpu_scene:.1f}ms" + + # 显示目标帧时间对比 + vr_info = pipeline_stats['vr_info'] + if vr_info['target_frame_time_ms'] > 0: + target = vr_info['target_frame_time_ms'] + current_avg = stats['frame_time_avg'] + if current_avg > target * 1.1: + summary += f" | 目标:{target:.0f}ms⚠️" + else: + summary += f" | 目标:{target:.0f}ms" + + # 性能状态指示 + if stats['vr_fps'] < 72: + summary += " ⚠️" + elif stats['vr_fps'] > 85: + summary += " ✅" + + print(summary) + + # ========== 性能监控核心方法 ========== + + def _init_performance_monitoring(self): + """初始化性能监控库""" + self.psutil_available = False + self.gputil_available = False + self.nvidia_ml_available = False + + try: + import psutil + self.psutil = psutil + self.psutil_available = True + print("✓ psutil性能监控库已加载") + except ImportError: + print("⚠️ psutil库未安装,CPU和内存监控将不可用") + + try: + import GPUtil + self.gputil = GPUtil + self.gputil_available = True + print("✓ GPUtil GPU监控库已加载") + except ImportError: + print("⚠️ GPUtil库未安装,GPU监控将不可用") + + try: + import pynvml + self.pynvml = pynvml + pynvml.nvmlInit() + self.nvidia_ml_available = True + print("✓ NVIDIA-ML GPU监控库已加载") + except ImportError: + print("⚠️ pynvml库未安装,NVIDIA GPU详细监控将不可用") + except Exception as e: + print(f"⚠️ NVIDIA-ML初始化失败: {e}") + + def _update_performance_metrics(self): + """更新系统性能指标""" + if not self.performance_monitoring: + return + + current_time = time.time() + + # 限制更新频率 + if current_time - self.last_performance_check < self.performance_check_interval: + return + + self.last_performance_check = current_time + + try: + # 更新CPU和内存使用率 + if self.psutil_available: + self.cpu_usage = self.psutil.cpu_percent(interval=None) + memory = self.psutil.virtual_memory() + self.memory_usage = memory.percent + + # 更新GPU使用率 + self._update_gpu_metrics() + + except Exception as e: + if self.frame_count % 600 == 0: # 每10秒输出一次错误 + print(f"⚠️ 性能监控更新失败: {e}") + + def _update_gpu_metrics(self): + """更新GPU相关指标""" + try: + # 方法1: 使用GPUtil + if self.gputil_available: + gpus = self.gputil.getGPUs() + if gpus: + gpu = gpus[0] # 使用第一个GPU + self.gpu_usage = gpu.load * 100 + self.gpu_memory_usage = gpu.memoryUtil * 100 + + # 方法2: 使用NVIDIA-ML (更精确) + elif self.nvidia_ml_available: + try: + handle = self.pynvml.nvmlDeviceGetHandleByIndex(0) + + # GPU使用率 + utilization = self.pynvml.nvmlDeviceGetUtilizationRates(handle) + self.gpu_usage = utilization.gpu + + # GPU内存使用率 + memory_info = self.pynvml.nvmlDeviceGetMemoryInfo(handle) + self.gpu_memory_usage = (memory_info.used / memory_info.total) * 100 + + except Exception as e: + # NVIDIA-ML可能无法在某些系统上工作 + pass + + except Exception as e: + # GPU监控失败,但不影响VR功能 + pass + + def _track_frame_time(self): + """记录帧时间 - 性能优化版本""" + current_time = time.time() + + if self._last_frame_time is not None: + frame_time = (current_time - self._last_frame_time) * 1000 # 转换为毫秒 + + # 🚀 性能优化:性能模式下跳过列表操作以减少内存分配 + if not self.performance_mode_enabled: + # 添加到帧时间历史 + self.frame_times.append(frame_time) + + # 限制历史长度 + if len(self.frame_times) > self.max_frame_time_history: + self.frame_times.pop(0) + + self._last_frame_time = current_time + + # ========== GPU计时方法 ========== + + def _get_gpu_frame_timing(self, frames_ago=0): + """获取GPU渲染时间统计 + + Args: + frames_ago: 获取多少帧之前的数据,0表示当前帧 + + Returns: + dict: GPU时间数据,如果获取失败返回None + """ + if not self.enable_gpu_timing or not self.vr_manager.vr_compositor: + return None + + try: + # 调用OpenVR的getFrameTiming API + result, timing = self.vr_manager.vr_compositor.getFrameTiming(framesAgo=frames_ago) + + if not result: + self.gpu_timing_failure_count += 1 + if self.gpu_timing_failure_count % 300 == 1: # 每5秒输出一次错误 + print("⚠️ OpenVR getFrameTiming调用失败") + return None + + # 提取GPU时间数据(单位:毫秒) + gpu_data = {} + + # 检查timing对象是否有GPU时间相关的属性 + if hasattr(timing, 'm_flSceneRenderGpuMs'): + gpu_data['scene_render'] = timing.m_flSceneRenderGpuMs + self.gpu_scene_render_ms = timing.m_flSceneRenderGpuMs + + if hasattr(timing, 'm_flPreSubmitGpuMs'): + gpu_data['pre_submit'] = timing.m_flPreSubmitGpuMs + self.gpu_pre_submit_ms = timing.m_flPreSubmitGpuMs + + if hasattr(timing, 'm_flPostSubmitGpuMs'): + gpu_data['post_submit'] = timing.m_flPostSubmitGpuMs + self.gpu_post_submit_ms = timing.m_flPostSubmitGpuMs + + if hasattr(timing, 'm_flTotalRenderGpuMs'): + gpu_data['total_render'] = timing.m_flTotalRenderGpuMs + self.gpu_total_render_ms = timing.m_flTotalRenderGpuMs + + if hasattr(timing, 'm_flCompositorRenderGpuMs'): + gpu_data['compositor_render'] = timing.m_flCompositorRenderGpuMs + self.gpu_compositor_render_ms = timing.m_flCompositorRenderGpuMs + + if hasattr(timing, 'm_flClientFrameIntervalMs'): + gpu_data['frame_interval'] = timing.m_flClientFrameIntervalMs + self.gpu_client_frame_interval_ms = timing.m_flClientFrameIntervalMs + + # 将GPU时间数据添加到历史记录 + if gpu_data: + self.gpu_timing_history.append(gpu_data) + if len(self.gpu_timing_history) > self.gpu_timing_history_size: + self.gpu_timing_history.pop(0) + + # 调试信息 - 仅在第一次成功时输出 + if not hasattr(self, '_gpu_timing_success_logged'): + available_fields = list(gpu_data.keys()) + print(f"✅ GPU时间统计已启用 - 可用字段: {available_fields}") + self._gpu_timing_success_logged = True + + return gpu_data + + except AttributeError as e: + # OpenVR Python绑定可能不包含某些字段 + if self.gpu_timing_failure_count == 0: + print(f"⚠️ GPU时间统计部分功能不可用: {e}") + print(" 这可能是由于OpenVR Python绑定版本问题") + self.gpu_timing_failure_count += 1 + return None + + except Exception as e: + self.gpu_timing_failure_count += 1 + if self.gpu_timing_failure_count % 300 == 1: # 每5秒输出一次错误 + print(f"⚠️ 获取GPU时间统计失败: {e}") + return None + + def enable_gpu_timing_monitoring(self): + """启用GPU时间监控""" + self.enable_gpu_timing = True + print("✓ VR GPU时间监控已启用") + + def disable_gpu_timing_monitoring(self): + """禁用GPU时间监控""" + self.enable_gpu_timing = False + print("✓ VR GPU时间监控已禁用") + + # ========== 管线统计方法 ========== + + def _start_timing(self, operation_name): + """开始计时操作 - 性能优化版本""" + if not self.enable_pipeline_monitoring or self.performance_mode_enabled: + return None + + # 🚀 性能优化:减少字典创建,只在必要时创建 + return { + 'operation': operation_name, + 'start_time': time.perf_counter() + } + + def _end_timing(self, timing_data): + """结束计时并记录结果 - 性能优化版本""" + if not self.enable_pipeline_monitoring or self.performance_mode_enabled or not timing_data: + return 0.0 + + elapsed = (time.perf_counter() - timing_data['start_time']) * 1000 # 转换为毫秒 + + # 保存到相应的历史记录 + operation = timing_data['operation'] + if operation == 'wait_poses': + self.wait_poses_time = elapsed + self.wait_poses_times.append(elapsed) + if len(self.wait_poses_times) > self.pipeline_history_size: + self.wait_poses_times.pop(0) + elif operation == 'left_render': + self.left_render_time = elapsed + elif operation == 'right_render': + self.right_render_time = elapsed + elif operation == 'submit': + self.submit_time = elapsed + self.submit_times.append(elapsed) + if len(self.submit_times) > self.pipeline_history_size: + self.submit_times.pop(0) + elif operation == 'sync_wait': + self.vr_sync_wait_time = elapsed + self.sync_wait_times.append(elapsed) + if len(self.sync_wait_times) > self.pipeline_history_size: + self.sync_wait_times.pop(0) + + # 计算总渲染时间 + total_render = self.left_render_time + self.right_render_time + self.render_times.append(total_render) + if len(self.render_times) > self.pipeline_history_size: + self.render_times.pop(0) + + return elapsed + + def _get_pipeline_stats(self): + """获取渲染管线统计信息""" + def get_stats(times_list): + if not times_list: + return {'avg': 0.0, 'min': 0.0, 'max': 0.0} + return { + 'avg': sum(times_list) / len(times_list), + 'min': min(times_list), + 'max': max(times_list) + } + + # 计算GPU时间统计 + def get_gpu_field_stats(field_name): + """从GPU时间历史记录中提取特定字段的统计信息""" + values = [] + for gpu_data in self.gpu_timing_history: + if field_name in gpu_data: + values.append(gpu_data[field_name]) + return get_stats(values) + + return { + 'wait_poses': get_stats(self.wait_poses_times), + 'render': get_stats(self.render_times), + 'submit': get_stats(self.submit_times), + 'sync_wait': get_stats(self.sync_wait_times), + 'gpu_timing': { + 'scene_render': get_gpu_field_stats('scene_render'), + 'pre_submit': get_gpu_field_stats('pre_submit'), + 'post_submit': get_gpu_field_stats('post_submit'), + 'total_render': get_gpu_field_stats('total_render'), + 'compositor_render': get_gpu_field_stats('compositor_render'), + 'frame_interval': get_gpu_field_stats('frame_interval') + }, + 'current': { + 'wait_poses': self.wait_poses_time, + 'left_render': self.left_render_time, + 'right_render': self.right_render_time, + 'submit': self.submit_time, + 'sync_wait': self.vr_sync_wait_time, + 'total_render': self.left_render_time + self.right_render_time, + 'gpu_scene_render': self.gpu_scene_render_ms, + 'gpu_pre_submit': self.gpu_pre_submit_ms, + 'gpu_post_submit': self.gpu_post_submit_ms, + 'gpu_total_render': self.gpu_total_render_ms, + 'gpu_compositor_render': self.gpu_compositor_render_ms, + 'gpu_frame_interval': self.gpu_client_frame_interval_ms + }, + 'vr_info': { + 'eye_resolution': self.current_eye_resolution, + 'recommended_resolution': self.recommended_eye_resolution, + 'display_frequency': self.vr_display_frequency, + 'vsync_enabled': self.vr_vsync_enabled, + 'target_frame_time_ms': self.target_frame_time_ms, + 'vsync_to_photons_ms': self.vsync_to_photons_ms, + 'vsync_window_ms': self.vsync_window_ms, + 'async_reprojection': self.async_reprojection_enabled, + 'motion_smoothing': self.motion_smoothing_enabled, + 'gpu_timing_enabled': self.enable_gpu_timing, + 'gpu_timing_failures': self.gpu_timing_failure_count + } + } + + def test_pipeline_monitoring(self): + """测试管线监控功能 - 用于调试和验证""" + print("🔧 正在测试VR管线监控功能...") + + try: + # 测试基本信息获取 + print("📊 基本信息:") + print(f" 当前分辨率: {self.current_eye_resolution}") + print(f" 显示频率: {self.vr_display_frequency} Hz") + print(f" 目标帧时间: {self.target_frame_time_ms:.2f}ms") + + # Running Start模式信息 + print("🎯 Running Start模式:") + print(f" 预测时间: {self.vr_manager.use_prediction_time * 1000:.1f}ms") + print(f" 模式: Valve Running Start - 帧开始时获取姿态") + + # 测试管线统计 + if self.enable_pipeline_monitoring: + pipeline_stats = self._get_pipeline_stats() + print("⚙️ 管线统计:") + print(f" waitGetPoses历史: {len(self.wait_poses_times)}帧") + print(f" 渲染历史: {len(self.render_times)}帧") + print(f" 提交历史: {len(self.submit_times)}帧") + + # 测试性能报告 + print("📋 生成详细性能报告:") + self._print_performance_report() + + print("✅ 管线监控功能测试完成") + else: + print("⚠️ 管线监控已禁用,无法测试统计功能") + + except Exception as e: + print(f"❌ 测试管线监控功能时发生错误: {e}") + import traceback + traceback.print_exc() + + def set_prediction_time(self, prediction_time_ms): + """设置预测时间(仅用于update_task策略) + + Args: + prediction_time_ms: 预测时间,单位毫秒(通常8-16ms) + """ + prediction_time_s = prediction_time_ms / 1000.0 + if prediction_time_s < 0.005 or prediction_time_s > 0.020: + print(f"⚠️ 预测时间超出推荐范围(5-20ms): {prediction_time_ms}ms") + + old_time = self.vr_manager.use_prediction_time * 1000 + self.vr_manager.use_prediction_time = prediction_time_s + print(f"✓ VR预测时间已设置: {old_time:.1f}ms → {prediction_time_ms:.1f}ms") + + # ========== 诊断工具方法 ========== + + def _print_render_callback_diagnostics(self): + """输出渲染回调诊断信息 - 包含优化效果分析""" + try: + print(f"🔍 DrawCallback渲染诊断:") + + # 回调次数统计 + left_count = self.left_render_count + right_count = self.right_render_count + print(f" 渲染次数: 左眼={left_count}, 右眼={right_count}") + + if left_count == 0 and right_count == 0: + print(f" ❌ VR缓冲区未被渲染 - 这是严重问题!") + return + + # 渲染次数平衡性检查 + if abs(left_count - right_count) > 5: + print(f" ⚠️ 左右眼渲染次数不平衡: 差异={abs(left_count - right_count)}") + else: + print(f" ✓ 左右眼渲染次数平衡") + + # 🔧 真实渲染时间分析 - 显示优化效果 + left_render_time = self.left_render_time + right_render_time = self.right_render_time + total_render_time = left_render_time + right_render_time + + print(f" ⏱️ 精确渲染时间测量:") + print(f" 左眼cbdata.upcall(): {left_render_time:.2f}ms") + print(f" 右眼cbdata.upcall(): {right_render_time:.2f}ms") + print(f" 总计: {total_render_time:.2f}ms") + + # 渲染性能评估 + if total_render_time > 16.0: # 超过60FPS时间 + print(f" 🔴 渲染时间过长: {total_render_time:.1f}ms (目标<13.9ms@72Hz)") + print(f" 建议: 检查RenderPipeline优化是否生效") + elif total_render_time > 10.0: + print(f" 🟡 渲染时间偏高: {total_render_time:.1f}ms (可接受)") + else: + print(f" 🟢 渲染性能良好: {total_render_time:.1f}ms") + + # 🔧 渲染管线优化状态检查 + self._check_rendering_optimizations() + + # OpenGL状态诊断 + self._diagnose_opengl_state() + + except Exception as e: + print(f" 渲染回调诊断失败: {e}") + + def _check_rendering_optimizations(self): + """检查渲染优化状态""" + try: + print(f" 🔧 渲染优化状态:") + + # 检查RenderPipeline优化 + if hasattr(self.vr_manager.world, 'render_pipeline') and self.vr_manager.world.render_pipeline: + print(f" RenderPipeline: 已检测并优化") + else: + print(f" RenderPipeline: 未检测到(使用基础Panda3D)") + + # 检查GPU同步优化 + if hasattr(self.vr_manager, '_smart_sync_logged'): + last_sync_frame = getattr(self.vr_manager, '_last_gpu_sync_frame', 0) + current_frame = self.frame_count + frames_since_sync = current_frame - last_sync_frame + print(f" 智能GPU同步: 已启用 (距离上次同步: {frames_since_sync}帧)") + else: + print(f" 智能GPU同步: 未初始化") + + # 检查对象池状态 + matrix_pool_status = self.vr_manager.get_object_pool_status() + print(f" 对象池: Mat4={matrix_pool_status['matrix_pool_size']}/{matrix_pool_status['matrix_pool_capacity']}") + + # 检查垃圾回收控制 + gc_status = self.get_debug_status() + if gc_status['gc_disabled']: + print(f" GC控制: 已启用手动模式 (间隔:{gc_status['manual_gc_interval']}帧)") + else: + print(f" GC控制: 自动模式") + + except Exception as e: + print(f" 优化状态检查失败: {e}") + + def _diagnose_opengl_state(self): + """诊断OpenGL渲染状态""" + try: + # 检查VR缓冲区状态 + if hasattr(self.vr_manager, 'vr_left_eye_buffer') and self.vr_manager.vr_left_eye_buffer: + left_gsg = self.vr_manager.vr_left_eye_buffer.getGsg() + left_valid = self.vr_manager.vr_left_eye_buffer.isValid() + print(f" 左眼缓冲区: {'有效' if left_valid else '无效'}") + + if hasattr(self.vr_manager, 'vr_right_eye_buffer') and self.vr_manager.vr_right_eye_buffer: + right_gsg = self.vr_manager.vr_right_eye_buffer.getGsg() + right_valid = self.vr_manager.vr_right_eye_buffer.isValid() + print(f" 右眼缓冲区: {'有效' if right_valid else '无效'}") + + # 检查纹理准备状态 + if hasattr(self.vr_manager, 'textures_prepared'): + print(f" 纹理准备状态: {'已准备' if self.vr_manager.textures_prepared else '未准备'}") + + # 检查纹理ID缓存 + if hasattr(self.vr_manager, 'left_texture_id') and hasattr(self.vr_manager, 'right_texture_id'): + left_id = self.vr_manager.left_texture_id or 0 + right_id = self.vr_manager.right_texture_id or 0 + print(f" 纹理ID缓存: 左眼={left_id}, 右眼={right_id}") + + if left_id == 0 or right_id == 0: + print(f" ⚠️ 检测到无效的纹理ID,这可能导致提交失败") + else: + print(f" ✓ 纹理ID缓存正常") + + # 检查场景渲染状态 + if hasattr(self.vr_manager.world, 'render') and self.vr_manager.world.render: + render_children = len(self.vr_manager.world.render.getChildren()) + print(f" 场景节点数: {render_children}") + if render_children == 0: + print(f" ⚠️ 场景为空,可能导致渲染时间异常短") + + except Exception as e: + print(f" OpenGL状态诊断失败: {e}") + + # ========== 调试控制方法 ========== + + def enable_debug_output(self): + """启用调试输出""" + self.debug_output_enabled = True + print("✓ VR调试输出已启用") + + def disable_debug_output(self): + """禁用调试输出""" + self.debug_output_enabled = False + print("✓ VR调试输出已禁用") + + def set_debug_mode(self, mode): + """设置调试模式 + + Args: + mode: 'brief' 或 'detailed' + """ + if mode in ['brief', 'detailed']: + self.debug_mode = mode + print(f"✓ VR调试模式设置为: {mode}") + else: + print("⚠️ 调试模式只能是 'brief' 或 'detailed'") + + def toggle_debug_output(self): + """切换调试输出状态""" + self.debug_output_enabled = not self.debug_output_enabled + status = "启用" if self.debug_output_enabled else "禁用" + print(f"✓ VR调试输出已{status}") + return self.debug_output_enabled + + def get_debug_status(self): + """获取调试状态""" + return { + 'debug_enabled': self.debug_output_enabled, + 'debug_mode': self.debug_mode, + 'performance_monitoring': self.performance_monitoring, + 'report_interval_frames': self.performance_report_interval, + 'report_interval_seconds': self.performance_report_interval / 60, # 假设60fps + 'gc_control_enabled': self.vr_manager._gc_control_enabled, + 'gc_disabled': self.vr_manager._gc_disabled, + 'manual_gc_interval': self.vr_manager._manual_gc_interval, + } + + # ========== 配置方法 ========== + + def set_performance_check_interval(self, interval): + """设置性能检查间隔 + + Args: + interval: 检查间隔(秒),建议0.1-2.0之间 + """ + if 0.1 <= interval <= 5.0: + self.performance_check_interval = interval + print(f"✓ 性能监控间隔设置为 {interval:.1f} 秒") + else: + print("⚠️ 性能监控间隔应在0.1-5.0秒之间") + + def set_frame_time_history_size(self, size): + """设置帧时间历史记录大小 + + Args: + size: 历史记录大小(帧数),建议30-300之间 + """ + if 10 <= size <= 1000: + self.max_frame_time_history = size + # 清理超出的历史记录 + if len(self.frame_times) > size: + self.frame_times = self.frame_times[-size:] + print(f"✓ 帧时间历史记录大小设置为 {size} 帧") + else: + print("⚠️ 帧时间历史记录大小应在10-1000帧之间") + + def set_performance_report_interval(self, frames): + """设置性能报告输出间隔 + + Args: + frames: 帧数间隔,建议300-7200之间(5秒-2分钟@60fps) + """ + if 300 <= frames <= 7200: + # 修改_update_vr中的报告间隔 + print(f"✓ 性能报告间隔设置为每 {frames} 帧(约 {frames/60:.1f} 秒@60fps)") + # 这里可以添加一个实例变量来控制 + self.performance_report_interval = frames + else: + print("⚠️ 性能报告间隔应在300-7200帧之间") + + # ========== 查询方法 ========== + + def get_performance_stats(self): + """获取详细的性能统计信息""" + stats = { + 'vr_fps': self.vr_fps, + 'frame_count': self.frame_count, + 'submit_failures': self.submit_failures, + 'pose_failures': self.pose_failures, + 'cpu_usage': self.cpu_usage, + 'memory_usage': self.memory_usage, + 'gpu_usage': self.gpu_usage, + 'gpu_memory_usage': self.gpu_memory_usage, + } + + # 计算帧时间统计 + if self.frame_times: + stats['frame_time_avg'] = sum(self.frame_times) / len(self.frame_times) + stats['frame_time_min'] = min(self.frame_times) + stats['frame_time_max'] = max(self.frame_times) + stats['frame_time_95th'] = sorted(self.frame_times)[int(len(self.frame_times) * 0.95)] + else: + stats['frame_time_avg'] = 0 + stats['frame_time_min'] = 0 + stats['frame_time_max'] = 0 + stats['frame_time_95th'] = 0 + + return stats + + def get_current_performance_summary(self): + """获取当前性能摘要(简短版本)""" + stats = self.get_performance_stats() + + summary = f"VR性能: {stats['vr_fps']:.1f}fps" + + if stats['frame_time_avg'] > 0: + summary += f" | 帧时间: {stats['frame_time_avg']:.1f}ms" + + if self.psutil_available: + summary += f" | CPU: {stats['cpu_usage']:.0f}%" + summary += f" | 内存: {stats['memory_usage']:.0f}%" + + # 显示GPU信息,如果库不可用则显示提示 + if self.gputil_available or self.nvidia_ml_available: + summary += f" | GPU: {stats['gpu_usage']:.0f}%" + else: + summary += " | GPU: N/A" + + return summary + + def get_performance_monitoring_config(self): + """获取当前性能监控配置""" + return { + 'enabled': self.performance_monitoring, + 'check_interval': self.performance_check_interval, + 'frame_history_size': self.max_frame_time_history, + 'report_interval': self.performance_report_interval, + 'psutil_available': self.psutil_available, + 'gputil_available': self.gputil_available, + 'nvidia_ml_available': self.nvidia_ml_available + } + + def print_performance_monitoring_status(self): + """输出性能监控状态""" + config = self.get_performance_monitoring_config() + + print("🔧 ===== VR性能监控配置 =====") + print(f" 监控状态: {'✅ 启用' if config['enabled'] else '❌ 禁用'}") + print(f" 检查间隔: {config['check_interval']:.1f} 秒") + print(f" 帧时间历史: {config['frame_history_size']} 帧") + print(f" 报告间隔: {config['report_interval']} 帧") + + print(f" 可用库:") + print(f" psutil (CPU/内存): {'✅' if config['psutil_available'] else '❌'}") + print(f" GPUtil (GPU): {'✅' if config['gputil_available'] else '❌'}") + print(f" NVIDIA-ML (GPU): {'✅' if config['nvidia_ml_available'] else '❌'}") + print("=============================") + + # ========== 控制方法 ========== + + def enable_performance_monitoring(self): + """启用性能监控""" + self.performance_monitoring = True + print("✓ VR性能监控已启用") + + def disable_performance_monitoring(self): + """禁用性能监控""" + self.performance_monitoring = False + print("✓ VR性能监控已禁用") + + def force_performance_report(self): + """强制输出一次性能报告""" + print("🔄 手动触发性能报告...") + self._print_performance_report() + + def reset_performance_counters(self): + """重置性能计数器""" + self.frame_count = 0 + self.last_fps_check = 0 + self.last_fps_time = 0 + self.vr_fps = 0 + self.submit_failures = 0 + self.pose_failures = 0 + self.frame_times.clear() + print("✅ 性能计数器已重置") diff --git a/core/vr/performance/optimization.py b/core/vr/performance/optimization.py index 42f5589a..9248f15f 100644 --- a/core/vr/performance/optimization.py +++ b/core/vr/performance/optimization.py @@ -1,346 +1,346 @@ -""" -VR对象池和优化系统 - -负责VR系统中的对象池管理、垃圾回收控制、分辨率缩放和性能模式控制。 -""" - -import gc -from panda3d.core import Mat4 - - -class VROptimization: - """VR优化系统 - 管理对象池、GC控制、分辨率缩放和性能模式""" - - def __init__(self, vr_manager): - """初始化VR优化系统 - - Args: - vr_manager: VR管理器实例引用 - """ - self.vr_manager = vr_manager - - # 对象池属性 - self._matrix_pool = [] # Mat4对象池 - self._matrix_pool_size = 8 # 池大小,足够处理多个控制器 - self._cached_matrices = {} # 设备ID到矩阵的缓存 - self._controller_poses_cache = {} # 控制器姿态缓存,避免每帧clear() - - # OpenVR Texture对象缓存 - 避免每帧创建openvr.Texture_t() - self._left_ovr_texture = None # 左眼纹理对象缓存 - self._right_ovr_texture = None # 右眼纹理对象缓存 - - # Python垃圾回收控制 - self._gc_control_enabled = True # 是否启用GC控制 - self._gc_disabled = False # GC是否被禁用 - self._manual_gc_interval = 900 # 每900帧手动触发一次GC (15秒@60fps) - 减少GC频率 - self._last_manual_gc_frame = 0 - - # VR分辨率缩放优化 - self.resolution_scale = 0.75 # 默认0.75倍分辨率,性能和质量平衡 - self.base_eye_width = 1080 # 原始推荐分辨率 - self.base_eye_height = 1200 - self.scaled_eye_width = 1080 # 实际使用的缩放后分辨率 - self.scaled_eye_height = 1200 - - # VR质量预设 - self.quality_presets = { - 'performance': 0.6, # 性能模式 - 约60%分辨率 - 'balanced': 0.75, # 平衡模式 - 约75%分辨率 - 'quality': 1.0 # 质量模式 - 100%分辨率 - } - self.current_quality_preset = 'balanced' # 默认平衡模式 - - # 性能模式控制 - self.performance_mode_enabled = False # 是否启用性能模式 - self.performance_mode_trigger_frame = 600 # 600帧后自动启用性能模式 - - def _initialize_object_pools(self): - """初始化对象池 - 修复16-19帧周期性GPU峰值""" - try: - # 预填充Mat4对象池 - for _ in range(self._matrix_pool_size): - self._matrix_pool.append(Mat4()) - - print(f"✅ Mat4对象池初始化完成 - 池大小: {self._matrix_pool_size}") - - # 🚀 预创建OpenVR Texture对象 - 消除每帧创建openvr.Texture_t()的开销 - try: - import openvr - self._left_ovr_texture = openvr.Texture_t() - self._right_ovr_texture = openvr.Texture_t() - - # 设置固定属性(这些不变) - self._left_ovr_texture.eType = openvr.TextureType_OpenGL - self._left_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma - self._right_ovr_texture.eType = openvr.TextureType_OpenGL - self._right_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma - - print("✅ OpenVR Texture对象缓存已创建 - 避免每帧创建新对象") - except Exception as texture_error: - print(f"⚠️ OpenVR Texture对象创建失败: {texture_error}") - # 不影响整体初始化,但性能可能不是最优 - - # 启用GC控制 - if self._gc_control_enabled: - # 禁用自动垃圾回收,改为手动控制 - gc.disable() - self._gc_disabled = True - print("✅ Python垃圾回收已禁用,改为手动控制") - - except Exception as e: - print(f"⚠️ 对象池初始化失败: {e}") - - def _get_pooled_matrix(self): - """从对象池获取Mat4对象""" - if self._matrix_pool: - return self._matrix_pool.pop() - else: - # 池为空时创建新对象(不应该发生,但作为备用) - return Mat4() - - def _return_pooled_matrix(self, matrix): - """将Mat4对象返回对象池""" - if len(self._matrix_pool) < self._matrix_pool_size: - # 重置矩阵到单位矩阵 - matrix.identMat() - self._matrix_pool.append(matrix) - - def _manual_gc_control(self): - """手动垃圾回收控制 - 避免VR渲染期间的GC峰值""" - if not self._gc_control_enabled or not self._gc_disabled: - return - - # 🚀 智能GC间隔:性能模式下减少GC频率 - current_interval = self._manual_gc_interval - if self.performance_mode_enabled: - current_interval = self._manual_gc_interval * 2 # 性能模式下间隔翻倍 - - # 每N帧手动触发一次垃圾回收 - if self.vr_manager.frame_count - self._last_manual_gc_frame >= current_interval: - # 在非渲染关键时刻触发GC - collected = gc.collect() - self._last_manual_gc_frame = self.vr_manager.frame_count - - # 仅在收集到对象时输出信息 - if collected > 0: - print(f"🗑️ 手动GC: 清理了 {collected} 个对象 (帧#{self.vr_manager.frame_count})") - - def enable_gc_control(self): - """启用垃圾回收控制 - 减少VR渲染期间的GC峰值""" - if not self._gc_control_enabled: - self._gc_control_enabled = True - if not self._gc_disabled: - gc.disable() - self._gc_disabled = True - print("✅ VR垃圾回收控制已启用") - else: - print("ℹ️ VR垃圾回收控制已经启用") - - def disable_gc_control(self): - """禁用垃圾回收控制 - 恢复自动垃圾回收""" - if self._gc_control_enabled: - self._gc_control_enabled = False - if self._gc_disabled: - gc.enable() - self._gc_disabled = False - print("✅ VR垃圾回收控制已禁用,恢复自动垃圾回收") - else: - print("ℹ️ VR垃圾回收控制已经禁用") - - def set_manual_gc_interval(self, frames): - """设置手动垃圾回收间隔 - - Args: - frames: 帧数间隔 (建议100-600) - """ - if 50 <= frames <= 1800: - old_interval = self._manual_gc_interval - self._manual_gc_interval = frames - print(f"✅ 手动GC间隔: {old_interval} → {frames} 帧") - else: - print("⚠️ GC间隔应在50-1800帧之间") - - def force_manual_gc(self): - """强制执行一次垃圾回收""" - collected = gc.collect() - print(f"🗑️ 强制GC: 清理了 {collected} 个对象") - return collected - - def get_object_pool_status(self): - """获取对象池状态""" - return { - 'matrix_pool_size': len(self._matrix_pool), - 'matrix_pool_capacity': self._matrix_pool_size, - 'cached_controllers': len(self.vr_manager.controller_poses), - 'cached_matrices': len(self._cached_matrices), - } - - # ====== VR分辨率缩放和质量预设系统 ====== - - def set_resolution_scale(self, scale): - """设置VR分辨率缩放系数 - - Args: - scale: 缩放系数 (0.3-1.0),0.75表示75%分辨率 - """ - if not (0.3 <= scale <= 1.0): - print(f"⚠️ 分辨率缩放系数应在0.3-1.0之间,当前: {scale}") - return False - - old_scale = self.resolution_scale - self.resolution_scale = scale - - # 如果VR已初始化,重新创建缓冲区 - if self.vr_manager.vr_initialized: - self._apply_resolution_scale() - - print(f"✓ VR分辨率缩放: {old_scale} → {scale}") - pixel_reduction = (1 - scale**2) * 100 - print(f"📊 像素减少: {pixel_reduction:.1f}%") - - return True - - def set_quality_preset(self, preset_name): - """设置VR质量预设 - - Args: - preset_name: 'performance', 'balanced', 'quality' - """ - if preset_name not in self.quality_presets: - print(f"⚠️ 未知的质量预设: {preset_name}") - print(f" 可用预设: {list(self.quality_presets.keys())}") - return False - - old_preset = self.current_quality_preset - self.current_quality_preset = preset_name - scale = self.quality_presets[preset_name] - - print(f"🎯 切换VR质量预设: {old_preset} → {preset_name}") - - return self.set_resolution_scale(scale) - - def cycle_quality_preset(self): - """循环切换质量预设""" - presets = list(self.quality_presets.keys()) - current_index = presets.index(self.current_quality_preset) - next_index = (current_index + 1) % len(presets) - next_preset = presets[next_index] - - return self.set_quality_preset(next_preset) - - def _apply_resolution_scale(self): - """应用分辨率缩放,重新创建VR缓冲区""" - try: - # 计算新的分辨率 - self.scaled_eye_width = int(self.base_eye_width * self.resolution_scale) - self.scaled_eye_height = int(self.base_eye_height * self.resolution_scale) - - # 更新当前分辨率 - self.vr_manager.eye_width = self.scaled_eye_width - self.vr_manager.eye_height = self.scaled_eye_height - if hasattr(self.vr_manager, 'current_eye_resolution'): - self.vr_manager.current_eye_resolution = (self.vr_manager.eye_width, self.vr_manager.eye_height) - - print(f"🔄 重新创建VR缓冲区...") - print(f" 新分辨率: {self.vr_manager.eye_width}x{self.vr_manager.eye_height}") - - # 清理旧的缓冲区 - self.vr_manager._cleanup_vr_buffers() - - # 🔧 关键修复:根据渲染模式选择创建方法 - success = False - if self.vr_manager.vr_render_mode.name == "RENDER_PIPELINE": - print(f" 使用RenderPipeline模式重建...") - success = self.vr_manager._create_vr_buffers_with_pipeline() - if not success: - print("⚠️ RenderPipeline模式创建失败,回退到普通渲染模式") - self.vr_manager.vr_render_mode = self.vr_manager.VRRenderMode.NORMAL - success = self.vr_manager._create_vr_buffers() - else: - print(f" 使用普通模式重建...") - success = self.vr_manager._create_vr_buffers() - - if success: - # 重新设置相机 - self.vr_manager._setup_vr_cameras() - print("✅ VR缓冲区重新创建成功") - return True - else: - print("❌ VR缓冲区重新创建失败") - return False - - except Exception as e: - print(f"❌ 应用分辨率缩放失败: {e}") - import traceback - traceback.print_exc() - return False - - def get_resolution_info(self): - """获取分辨率相关信息""" - return { - 'base_resolution': (self.base_eye_width, self.base_eye_height), - 'current_resolution': (self.vr_manager.eye_width, self.vr_manager.eye_height), - 'resolution_scale': self.resolution_scale, - 'current_preset': self.current_quality_preset, - 'available_presets': self.quality_presets, - 'pixel_reduction_percent': (1 - self.resolution_scale**2) * 100 - } - - def print_resolution_info(self): - """输出分辨率信息""" - info = self.get_resolution_info() - print("🔧 ===== VR分辨率信息 =====") - print(f" 推荐分辨率: {info['base_resolution'][0]}x{info['base_resolution'][1]}") - print(f" 当前分辨率: {info['current_resolution'][0]}x{info['current_resolution'][1]}") - print(f" 缩放系数: {info['resolution_scale']}") - print(f" 当前预设: {info['current_preset']}") - print(f" 像素减少: {info['pixel_reduction_percent']:.1f}%") - print(" 可用预设:") - for name, scale in info['available_presets'].items(): - marker = "✓" if name == info['current_preset'] else " " - print(f" {marker} {name}: {scale} ({scale*100:.0f}%)") - print("==========================") - - # ====== 性能模式控制方法 ====== - - def enable_performance_mode(self): - """手动启用性能模式 - 立即禁用详细监控以提升性能""" - if not self.performance_mode_enabled: - self.performance_mode_enabled = True - print("🎯 性能模式已手动启用 - 禁用详细监控以提升性能") - print(" 现在将减少每帧对象创建,显著提升VR性能稳定性") - else: - print("ℹ️ 性能模式已经启用") - - def disable_performance_mode(self): - """禁用性能模式 - 重新启用详细监控(用于调试)""" - if self.performance_mode_enabled: - self.performance_mode_enabled = False - print("🔍 性能模式已禁用 - 重新启用详细监控") - print(" 注意:这将增加每帧对象创建,可能影响VR性能") - else: - print("ℹ️ 性能模式已经禁用") - - def set_performance_mode_trigger_frame(self, frame_count): - """设置性能模式自动触发的帧数 - - Args: - frame_count: 触发帧数 (建议300-1200) - """ - if 100 <= frame_count <= 3600: - old_trigger = self.performance_mode_trigger_frame - self.performance_mode_trigger_frame = frame_count - print(f"✅ 性能模式触发帧数: {old_trigger} → {frame_count}") - else: - print("⚠️ 触发帧数应在100-3600之间") - - def get_performance_mode_status(self): - """获取性能模式状态""" - return { - 'performance_mode_enabled': self.performance_mode_enabled, - 'trigger_frame': self.performance_mode_trigger_frame, - 'current_frame': self.vr_manager.frame_count, - 'will_trigger_at_frame': self.performance_mode_trigger_frame if not self.performance_mode_enabled else None, - 'gc_interval_normal': self._manual_gc_interval, - 'gc_interval_performance': self._manual_gc_interval * 2, +""" +VR对象池和优化系统 + +负责VR系统中的对象池管理、垃圾回收控制、分辨率缩放和性能模式控制。 +""" + +import gc +from panda3d.core import Mat4 + + +class VROptimization: + """VR优化系统 - 管理对象池、GC控制、分辨率缩放和性能模式""" + + def __init__(self, vr_manager): + """初始化VR优化系统 + + Args: + vr_manager: VR管理器实例引用 + """ + self.vr_manager = vr_manager + + # 对象池属性 + self._matrix_pool = [] # Mat4对象池 + self._matrix_pool_size = 8 # 池大小,足够处理多个控制器 + self._cached_matrices = {} # 设备ID到矩阵的缓存 + self._controller_poses_cache = {} # 控制器姿态缓存,避免每帧clear() + + # OpenVR Texture对象缓存 - 避免每帧创建openvr.Texture_t() + self._left_ovr_texture = None # 左眼纹理对象缓存 + self._right_ovr_texture = None # 右眼纹理对象缓存 + + # Python垃圾回收控制 + self._gc_control_enabled = True # 是否启用GC控制 + self._gc_disabled = False # GC是否被禁用 + self._manual_gc_interval = 900 # 每900帧手动触发一次GC (15秒@60fps) - 减少GC频率 + self._last_manual_gc_frame = 0 + + # VR分辨率缩放优化 + self.resolution_scale = 0.75 # 默认0.75倍分辨率,性能和质量平衡 + self.base_eye_width = 1080 # 原始推荐分辨率 + self.base_eye_height = 1200 + self.scaled_eye_width = 1080 # 实际使用的缩放后分辨率 + self.scaled_eye_height = 1200 + + # VR质量预设 + self.quality_presets = { + 'performance': 0.6, # 性能模式 - 约60%分辨率 + 'balanced': 0.75, # 平衡模式 - 约75%分辨率 + 'quality': 1.0 # 质量模式 - 100%分辨率 + } + self.current_quality_preset = 'balanced' # 默认平衡模式 + + # 性能模式控制 + self.performance_mode_enabled = False # 是否启用性能模式 + self.performance_mode_trigger_frame = 600 # 600帧后自动启用性能模式 + + def _initialize_object_pools(self): + """初始化对象池 - 修复16-19帧周期性GPU峰值""" + try: + # 预填充Mat4对象池 + for _ in range(self._matrix_pool_size): + self._matrix_pool.append(Mat4()) + + print(f"✅ Mat4对象池初始化完成 - 池大小: {self._matrix_pool_size}") + + # 🚀 预创建OpenVR Texture对象 - 消除每帧创建openvr.Texture_t()的开销 + try: + import openvr + self._left_ovr_texture = openvr.Texture_t() + self._right_ovr_texture = openvr.Texture_t() + + # 设置固定属性(这些不变) + self._left_ovr_texture.eType = openvr.TextureType_OpenGL + self._left_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + self._right_ovr_texture.eType = openvr.TextureType_OpenGL + self._right_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + + print("✅ OpenVR Texture对象缓存已创建 - 避免每帧创建新对象") + except Exception as texture_error: + print(f"⚠️ OpenVR Texture对象创建失败: {texture_error}") + # 不影响整体初始化,但性能可能不是最优 + + # 启用GC控制 + if self._gc_control_enabled: + # 禁用自动垃圾回收,改为手动控制 + gc.disable() + self._gc_disabled = True + print("✅ Python垃圾回收已禁用,改为手动控制") + + except Exception as e: + print(f"⚠️ 对象池初始化失败: {e}") + + def _get_pooled_matrix(self): + """从对象池获取Mat4对象""" + if self._matrix_pool: + return self._matrix_pool.pop() + else: + # 池为空时创建新对象(不应该发生,但作为备用) + return Mat4() + + def _return_pooled_matrix(self, matrix): + """将Mat4对象返回对象池""" + if len(self._matrix_pool) < self._matrix_pool_size: + # 重置矩阵到单位矩阵 + matrix.identMat() + self._matrix_pool.append(matrix) + + def _manual_gc_control(self): + """手动垃圾回收控制 - 避免VR渲染期间的GC峰值""" + if not self._gc_control_enabled or not self._gc_disabled: + return + + # 🚀 智能GC间隔:性能模式下减少GC频率 + current_interval = self._manual_gc_interval + if self.performance_mode_enabled: + current_interval = self._manual_gc_interval * 2 # 性能模式下间隔翻倍 + + # 每N帧手动触发一次垃圾回收 + if self.vr_manager.frame_count - self._last_manual_gc_frame >= current_interval: + # 在非渲染关键时刻触发GC + collected = gc.collect() + self._last_manual_gc_frame = self.vr_manager.frame_count + + # 仅在收集到对象时输出信息 + if collected > 0: + print(f"🗑️ 手动GC: 清理了 {collected} 个对象 (帧#{self.vr_manager.frame_count})") + + def enable_gc_control(self): + """启用垃圾回收控制 - 减少VR渲染期间的GC峰值""" + if not self._gc_control_enabled: + self._gc_control_enabled = True + if not self._gc_disabled: + gc.disable() + self._gc_disabled = True + print("✅ VR垃圾回收控制已启用") + else: + print("ℹ️ VR垃圾回收控制已经启用") + + def disable_gc_control(self): + """禁用垃圾回收控制 - 恢复自动垃圾回收""" + if self._gc_control_enabled: + self._gc_control_enabled = False + if self._gc_disabled: + gc.enable() + self._gc_disabled = False + print("✅ VR垃圾回收控制已禁用,恢复自动垃圾回收") + else: + print("ℹ️ VR垃圾回收控制已经禁用") + + def set_manual_gc_interval(self, frames): + """设置手动垃圾回收间隔 + + Args: + frames: 帧数间隔 (建议100-600) + """ + if 50 <= frames <= 1800: + old_interval = self._manual_gc_interval + self._manual_gc_interval = frames + print(f"✅ 手动GC间隔: {old_interval} → {frames} 帧") + else: + print("⚠️ GC间隔应在50-1800帧之间") + + def force_manual_gc(self): + """强制执行一次垃圾回收""" + collected = gc.collect() + print(f"🗑️ 强制GC: 清理了 {collected} 个对象") + return collected + + def get_object_pool_status(self): + """获取对象池状态""" + return { + 'matrix_pool_size': len(self._matrix_pool), + 'matrix_pool_capacity': self._matrix_pool_size, + 'cached_controllers': len(self.vr_manager.controller_poses), + 'cached_matrices': len(self._cached_matrices), + } + + # ====== VR分辨率缩放和质量预设系统 ====== + + def set_resolution_scale(self, scale): + """设置VR分辨率缩放系数 + + Args: + scale: 缩放系数 (0.3-1.0),0.75表示75%分辨率 + """ + if not (0.3 <= scale <= 1.0): + print(f"⚠️ 分辨率缩放系数应在0.3-1.0之间,当前: {scale}") + return False + + old_scale = self.resolution_scale + self.resolution_scale = scale + + # 如果VR已初始化,重新创建缓冲区 + if self.vr_manager.vr_initialized: + self._apply_resolution_scale() + + print(f"✓ VR分辨率缩放: {old_scale} → {scale}") + pixel_reduction = (1 - scale**2) * 100 + print(f"📊 像素减少: {pixel_reduction:.1f}%") + + return True + + def set_quality_preset(self, preset_name): + """设置VR质量预设 + + Args: + preset_name: 'performance', 'balanced', 'quality' + """ + if preset_name not in self.quality_presets: + print(f"⚠️ 未知的质量预设: {preset_name}") + print(f" 可用预设: {list(self.quality_presets.keys())}") + return False + + old_preset = self.current_quality_preset + self.current_quality_preset = preset_name + scale = self.quality_presets[preset_name] + + print(f"🎯 切换VR质量预设: {old_preset} → {preset_name}") + + return self.set_resolution_scale(scale) + + def cycle_quality_preset(self): + """循环切换质量预设""" + presets = list(self.quality_presets.keys()) + current_index = presets.index(self.current_quality_preset) + next_index = (current_index + 1) % len(presets) + next_preset = presets[next_index] + + return self.set_quality_preset(next_preset) + + def _apply_resolution_scale(self): + """应用分辨率缩放,重新创建VR缓冲区""" + try: + # 计算新的分辨率 + self.scaled_eye_width = int(self.base_eye_width * self.resolution_scale) + self.scaled_eye_height = int(self.base_eye_height * self.resolution_scale) + + # 更新当前分辨率 + self.vr_manager.eye_width = self.scaled_eye_width + self.vr_manager.eye_height = self.scaled_eye_height + if hasattr(self.vr_manager, 'current_eye_resolution'): + self.vr_manager.current_eye_resolution = (self.vr_manager.eye_width, self.vr_manager.eye_height) + + print(f"🔄 重新创建VR缓冲区...") + print(f" 新分辨率: {self.vr_manager.eye_width}x{self.vr_manager.eye_height}") + + # 清理旧的缓冲区 + self.vr_manager._cleanup_vr_buffers() + + # 🔧 关键修复:根据渲染模式选择创建方法 + success = False + if self.vr_manager.vr_render_mode.name == "RENDER_PIPELINE": + print(f" 使用RenderPipeline模式重建...") + success = self.vr_manager._create_vr_buffers_with_pipeline() + if not success: + print("⚠️ RenderPipeline模式创建失败,回退到普通渲染模式") + self.vr_manager.vr_render_mode = self.vr_manager.VRRenderMode.NORMAL + success = self.vr_manager._create_vr_buffers() + else: + print(f" 使用普通模式重建...") + success = self.vr_manager._create_vr_buffers() + + if success: + # 重新设置相机 + self.vr_manager._setup_vr_cameras() + print("✅ VR缓冲区重新创建成功") + return True + else: + print("❌ VR缓冲区重新创建失败") + return False + + except Exception as e: + print(f"❌ 应用分辨率缩放失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_resolution_info(self): + """获取分辨率相关信息""" + return { + 'base_resolution': (self.base_eye_width, self.base_eye_height), + 'current_resolution': (self.vr_manager.eye_width, self.vr_manager.eye_height), + 'resolution_scale': self.resolution_scale, + 'current_preset': self.current_quality_preset, + 'available_presets': self.quality_presets, + 'pixel_reduction_percent': (1 - self.resolution_scale**2) * 100 + } + + def print_resolution_info(self): + """输出分辨率信息""" + info = self.get_resolution_info() + print("🔧 ===== VR分辨率信息 =====") + print(f" 推荐分辨率: {info['base_resolution'][0]}x{info['base_resolution'][1]}") + print(f" 当前分辨率: {info['current_resolution'][0]}x{info['current_resolution'][1]}") + print(f" 缩放系数: {info['resolution_scale']}") + print(f" 当前预设: {info['current_preset']}") + print(f" 像素减少: {info['pixel_reduction_percent']:.1f}%") + print(" 可用预设:") + for name, scale in info['available_presets'].items(): + marker = "✓" if name == info['current_preset'] else " " + print(f" {marker} {name}: {scale} ({scale*100:.0f}%)") + print("==========================") + + # ====== 性能模式控制方法 ====== + + def enable_performance_mode(self): + """手动启用性能模式 - 立即禁用详细监控以提升性能""" + if not self.performance_mode_enabled: + self.performance_mode_enabled = True + print("🎯 性能模式已手动启用 - 禁用详细监控以提升性能") + print(" 现在将减少每帧对象创建,显著提升VR性能稳定性") + else: + print("ℹ️ 性能模式已经启用") + + def disable_performance_mode(self): + """禁用性能模式 - 重新启用详细监控(用于调试)""" + if self.performance_mode_enabled: + self.performance_mode_enabled = False + print("🔍 性能模式已禁用 - 重新启用详细监控") + print(" 注意:这将增加每帧对象创建,可能影响VR性能") + else: + print("ℹ️ 性能模式已经禁用") + + def set_performance_mode_trigger_frame(self, frame_count): + """设置性能模式自动触发的帧数 + + Args: + frame_count: 触发帧数 (建议300-1200) + """ + if 100 <= frame_count <= 3600: + old_trigger = self.performance_mode_trigger_frame + self.performance_mode_trigger_frame = frame_count + print(f"✅ 性能模式触发帧数: {old_trigger} → {frame_count}") + else: + print("⚠️ 触发帧数应在100-3600之间") + + def get_performance_mode_status(self): + """获取性能模式状态""" + return { + 'performance_mode_enabled': self.performance_mode_enabled, + 'trigger_frame': self.performance_mode_trigger_frame, + 'current_frame': self.vr_manager.frame_count, + 'will_trigger_at_frame': self.performance_mode_trigger_frame if not self.performance_mode_enabled else None, + 'gc_interval_normal': self._manual_gc_interval, + 'gc_interval_performance': self._manual_gc_interval * 2, } \ No newline at end of file diff --git a/core/vr/rendering/__init__.py b/core/vr/rendering/__init__.py index a55d0169..447d1979 100644 --- a/core/vr/rendering/__init__.py +++ b/core/vr/rendering/__init__.py @@ -1,12 +1,12 @@ -""" -VR渲染子系统 - -负责VR的渲染相关功能: -- 缓冲区和纹理管理 -- VR相机设置和更新 -- RenderPipeline高级渲染集成 -- VR专用渲染stages -- OpenVR合成器接口 -""" - -__all__ = [] +""" +VR渲染子系统 + +负责VR的渲染相关功能: +- 缓冲区和纹理管理 +- VR相机设置和更新 +- RenderPipeline高级渲染集成 +- VR专用渲染stages +- OpenVR合成器接口 +""" + +__all__ = [] diff --git a/core/vr/rendering/stages.py b/core/vr/rendering/stages.py index 5ba80289..e7c503b0 100644 --- a/core/vr/rendering/stages.py +++ b/core/vr/rendering/stages.py @@ -1,867 +1,867 @@ -""" -VR专用渲染Stages - -为VR创建简化但完整的渲染管线,支持: -- GBuffer生成(延迟渲染基础) -- 光照计算(复用主Pipeline的LightManager) -- 最终合成(tone mapping + gamma校正) -""" - -from panda3d.core import Shader -from RenderPipelineFile.rpcore.render_target import RenderTarget -from RenderPipelineFile.rpcore.globals import Globals -from RenderPipelineFile.rpcore.loader import RPLoader -from RenderPipelineFile.rpcore.util.shader_input_blocks import SimpleInputBlock - - -def _load_rp_shader(shader_name): - """ - 加载RenderPipeline shader - - Args: - shader_name: shader文件名(如"apply_lights.frag.glsl") - - Returns: - Shader对象 - """ - default_vert = "/$$rp/shader/default_post_process.vert.glsl" - frag_path = f"/$$rp/shader/{shader_name}" - shader = RPLoader.load_shader(default_vert, frag_path) - if shader: - print(f" ✅ Shader加载成功: {shader_name}") - else: - print(f" ❌ Shader加载失败: {shader_name}") - return shader - - -class VRGBufferStage: - """VR GBuffer阶段 - 为单个VR眼睛创建GBuffer""" - - def __init__(self, name, pipeline): - """ - 初始化VR GBuffer Stage - - Args: - name: stage名称(如"VR_Left_GBuffer") - pipeline: RenderPipeline实例引用 - """ - self.name = name - self.pipeline = pipeline - self.target = None - self._camera = None - - def create(self, width, height, vr_camera): - """ - 创建GBuffer渲染目标 - - Args: - width: 渲染宽度 - height: 渲染高度 - vr_camera: VR相机NodePath - """ - print(f"🎨 创建{self.name} GBuffer: {width}x{height}") - - # 创建RenderTarget - self.target = RenderTarget(self.name) - self.target.size = width, height - - # 添加GBuffer纹理附件(与主GBufferStage相同) - self.target.add_color_attachment(bits=16, alpha=True) # Data0 - self.target.add_depth_attachment(bits=32) # Depth - self.target.add_aux_attachments(bits=16, count=2) # Data1, Data2 - - # 准备渲染(绑定相机) - self.target.prepare_render(vr_camera) - self._camera = vr_camera - - # 🌌 关键修复:确保VR相机能看到天空盒 - # RenderPipeline的天空盒在render节点下,使用unsorted bin和shader effects - if self.target._source_region: - # 确保DisplayRegion支持所有bin排序 - self.target._source_region.setSort(20) # 设置合理的sort值 - print(f" ✓ DisplayRegion sort已设置") - - # 🔑 关键:设置VR相机的initial state为render的state - # 这样VR相机才能继承RenderPipeline应用在render节点上的所有shader effects(包括天空盒shader) - if vr_camera and vr_camera.node(): - # 使用Panda3D的内置render节点(通过builtins) - try: - from panda3d.core import NodePath - import builtins - if hasattr(builtins, 'render'): - render = builtins.render - vr_camera.node().setInitialState(render.getState()) - print(f" ✓ VR相机initial state已设置为render的state") - else: - print(f" ⚠️ builtins.render不存在,跳过initial state设置") - except Exception as e: - print(f" ⚠️ 设置VR相机initial state失败: {e}") - - # 设置正常的背景色(与主窗口一致的天空色) - if self.target.internal_buffer: - buffer = self.target.internal_buffer - buffer.setClearColorActive(True) - # 使用主RenderPipeline的天空色(浅蓝色) - buffer.setClearColor((0.53, 0.81, 0.92, 1.0)) # 天空蓝 - - # 同时也设置DisplayRegion的clear - if hasattr(self.target, '_source_region') and self.target._source_region: - self.target._source_region.setClearColorActive(True) - self.target._source_region.setClearColor((0.53, 0.81, 0.92, 1.0)) - - print(f"✓ {self.name} GBuffer创建成功") - return True - - def get_gbuffer_textures(self): - """ - 获取GBuffer纹理字典 - - Returns: - dict: 包含depth, data0, data1, data2的纹理字典 - """ - if not self.target: - return None - - return { - "depth": self.target.depth_tex, - "data0": self.target.color_tex, - "data1": self.target.aux_tex[0], - "data2": self.target.aux_tex[1], - } - - def make_gbuffer_ubo(self): - """创建GBuffer UBO(与主GBufferStage相同)""" - # 注意:必须使用固定的"GBuffer"名字,shader期望这个名字 - ubo = SimpleInputBlock("GBuffer") - ubo.add_input("Depth", self.target.depth_tex) - ubo.add_input("Data0", self.target.color_tex) - ubo.add_input("Data1", self.target.aux_tex[0]) - ubo.add_input("Data2", self.target.aux_tex[1]) - return ubo - - def get_internal_buffer(self): - """获取内部GraphicsOutput""" - return self.target.internal_buffer if self.target else None - - def cleanup(self): - """清理资源""" - if self.target: - self.target.remove() - self.target = None - print(f"✓ {self.name} GBuffer已清理") - - -class VRLightingStage: - """VR光照阶段 - 使用主Pipeline的LightManager计算光照""" - - def __init__(self, name, pipeline): - """ - 初始化VR光照Stage - - Args: - name: stage名称 - pipeline: RenderPipeline实例引用 - """ - self.name = name - self.pipeline = pipeline - self.target = None - self._gbuffer_stage = None - self._vr_main_scene_data = None - - def create(self, width, height, gbuffer_stage): - """ - 创建光照渲染目标 - - Args: - width: 渲染宽度 - height: 渲染高度 - gbuffer_stage: VRGBufferStage实例(提供GBuffer纹理) - """ - print(f"💡 创建{self.name} 光照Stage: {width}x{height}") - - self._gbuffer_stage = gbuffer_stage - self._width = width - self._height = height - - # 创建光照输出目标 - self.target = RenderTarget(self.name) - self.target.size = width, height - self.target.add_color_attachment(bits=16, alpha=True) - self.target.prepare_buffer() - - # 1. 创建GBuffer UBO(使用SimpleInputBlock) - gbuffer_ubo = gbuffer_stage.make_gbuffer_ubo() - gbuffer_ubo.bind_to(self.target) - - # 2. 从主Pipeline获取光照相关inputs - self._bind_pipeline_inputs() - - # 3. 从主Pipeline获取光照相关pipes - self._bind_pipeline_pipes() - - # 4. 覆盖VR特定的MainSceneData字段 - self._set_vr_scene_data() - - # 5. 加载光照shader - self.target.shader = _load_rp_shader("apply_lights.frag.glsl") - - print(f"✓ {self.name} 光照Stage创建成功") - return True - - def _bind_pipeline_inputs(self): - """从主Pipeline获取光照相关shader inputs""" - if not self.pipeline or not self.target: - return - - stage_mgr = self.pipeline.stage_mgr - inputs = stage_mgr.inputs - - # 必需的光照inputs - required_inputs = [ - "AllLightsData", # 光源数据 - "ShadowSourceData", # 阴影源数据 - "IESDatasetTex", # IES光照纹理 - "maxLightIndex", # 最大光源索引 - ] - - for input_name in required_inputs: - if input_name in inputs: - self.target.set_shader_input(input_name, inputs[input_name]) - print(f" ✓ 绑定input: {input_name}") - else: - print(f" ⚠️ 缺失input: {input_name}") - - # 注意:不绑定MainSceneData InputBlock,因为它会被主Pipeline每帧覆盖 - # 我们会在_set_vr_scene_data()中手动设置所有需要的字段 - - def _bind_pipeline_pipes(self): - """从主Pipeline获取光照相关pipes""" - if not self.pipeline or not self.target: - return - - stage_mgr = self.pipeline.stage_mgr - pipes = stage_mgr.pipes - - # 必需的光照pipes - required_pipes = [ - "CellIndices", # 光照剔除cell索引 - "PerCellLights", # 每个cell的光源列表 - "PerCellLightsCounts", # 每个cell的光源数量 - "ShadowAtlas", # 阴影图集 - "ShadowAtlasPCF", # PCF阴影图集 - ] - - # 可选的pipes - optional_pipes = [ - "CombinedVelocity", # 运动矢量(用于调试模式) - ] - - for pipe_name in required_pipes: - if pipe_name in pipes: - pipe_value = pipes[pipe_name] - if isinstance(pipe_value, (list, tuple)): - self.target.set_shader_input(pipe_name, *pipe_value) - else: - self.target.set_shader_input(pipe_name, pipe_value) - print(f" ✓ 绑定pipe: {pipe_name}") - else: - print(f" ❌ 缺失必需pipe: {pipe_name}") - - for pipe_name in optional_pipes: - if pipe_name in pipes: - pipe_value = pipes[pipe_name] - if isinstance(pipe_value, (list, tuple)): - self.target.set_shader_input(pipe_name, *pipe_value) - else: - self.target.set_shader_input(pipe_name, pipe_value) - print(f" ✓ 绑定可选pipe: {pipe_name}") - - def _set_vr_scene_data(self): - """创建VR专用的MainSceneData UBO(从主Pipeline复制,覆盖VR特定值)""" - from panda3d.core import LVecBase2i - from RenderPipelineFile.rpcore.util.shader_input_blocks import GroupedInputBlock - import math - - # 从主Pipeline的MainSceneData InputBlock获取 - main_input_block = self.pipeline.stage_mgr.input_blocks.get("MainSceneData") - if not main_input_block: - print(" ⚠️ 主Pipeline的MainSceneData不存在") - return - - # 创建VR专用的MainSceneData GroupedInputBlock - vr_main_scene_data = GroupedInputBlock("MainSceneData") - - # 注册所有字段(与主Pipeline相同的类型) - field_definitions = [ - ("camera_pos", "vec3"), - ("view_proj_mat_no_jitter", "mat4"), - ("last_view_proj_mat_no_jitter", "mat4"), - ("last_inv_view_proj_mat_no_jitter", "mat4"), - ("view_mat_z_up", "mat4"), - ("proj_mat", "mat4"), - ("inv_proj_mat", "mat4"), - ("view_mat_billboard", "mat4"), - ("frame_delta", "float"), - ("smooth_frame_delta", "float"), - ("frame_time", "float"), - ("current_film_offset", "vec2"), - ("frame_index", "int"), - ("screen_size", "ivec2"), - ("native_screen_size", "ivec2"), - ("lc_tile_count", "ivec2"), - ("ws_frustum_directions", "mat4"), - ("vs_frustum_directions", "mat4"), - ] - - for field_name, field_type in field_definitions: - vr_main_scene_data.register_pta(field_name, field_type) - - # 复制主Pipeline的所有字段值 - for field_name, _ in field_definitions: - try: - value = main_input_block.get_input(field_name) - vr_main_scene_data.update_input(field_name, value) - except: - pass # 某些字段可能不存在 - - # 覆盖VR特定的分辨率字段 - vr_resolution = LVecBase2i(self._width, self._height) - vr_main_scene_data.update_input("screen_size", vr_resolution) - vr_main_scene_data.update_input("native_screen_size", vr_resolution) - - # 计算并设置VR的光照剔除tile数量 - tile_size_x = self.pipeline.settings["lighting.culling_grid_size_x"] - tile_size_y = self.pipeline.settings["lighting.culling_grid_size_y"] - num_tiles_x = int(math.ceil(self._width / float(tile_size_x))) - num_tiles_y = int(math.ceil(self._height / float(tile_size_y))) - vr_tile_count = LVecBase2i(num_tiles_x, num_tiles_y) - vr_main_scene_data.update_input("lc_tile_count", vr_tile_count) - - # 绑定VR专用的MainSceneData UBO - vr_main_scene_data.bind_to(self.target) - - # 保存引用以便后续更新 - self._vr_main_scene_data = vr_main_scene_data - - print(f" ✓ 创建VR专用MainSceneData UBO") - print(f" ✓ VR screen_size: {self._width}x{self._height}") - print(f" ✓ VR lc_tile_count: {num_tiles_x}x{num_tiles_y}") - - def get_shaded_texture(self): - """获取光照计算后的纹理""" - return self.target.color_tex if self.target else None - - def cleanup(self): - """清理资源""" - if self.target: - self.target.remove() - self.target = None - print(f"✓ {self.name} 光照Stage已清理") - - -class VRAmbientStage: - """VR环境光阶段 - 计算基于图像的照明(IBL)""" - - def __init__(self, name, pipeline): - """ - 初始化VR环境光Stage - - Args: - name: stage名称 - pipeline: RenderPipeline实例引用 - """ - self.name = name - self.pipeline = pipeline - self.target = None - self._lighting_stage = None - self._gbuffer_stage = None - self._vr_main_scene_data = None - - def create(self, width, height, lighting_stage, gbuffer_stage): - """ - 创建环境光渲染目标 - - Args: - width: 渲染宽度 - height: 渲染高度 - lighting_stage: VRLightingStage实例(提供直接光照结果) - gbuffer_stage: VRGBufferStage实例(提供GBuffer数据) - """ - print(f"🌍 创建{self.name} 环境光Stage: {width}x{height}") - - self._lighting_stage = lighting_stage - self._gbuffer_stage = gbuffer_stage - self._width = width - self._height = height - - # 创建环境光输出目标 - self.target = RenderTarget(self.name) - self.target.size = width, height - self.target.add_color_attachment(bits=16, alpha=True) - self.target.prepare_buffer() - - # 1. 设置直接光照输入(ShadedScene) - shaded_tex = lighting_stage.get_shaded_texture() - if shaded_tex: - self.target.set_shader_input("ShadedScene", shaded_tex) - - # 2. 绑定GBuffer UBO - gbuffer_ubo = gbuffer_stage.make_gbuffer_ubo() - gbuffer_ubo.bind_to(self.target) - - # 3. 从主Pipeline获取环境相关inputs - self._bind_environment_inputs() - - # 4. 覆盖VR特定的MainSceneData字段 - self._set_vr_scene_data() - - # 5. 加载环境光shader - self.target.shader = _load_rp_shader("ambient_stage.frag.glsl") - - print(f"✓ {self.name} 环境光Stage创建成功") - return True - - def _bind_environment_inputs(self): - """从主Pipeline获取环境相关shader inputs""" - if not self.pipeline or not self.target: - return - - stage_mgr = self.pipeline.stage_mgr - inputs = stage_mgr.inputs - pipes = stage_mgr.pipes - - # 必需的环境inputs - required_inputs = [ - "DefaultEnvmap", # 默认环境立方体贴图 - "PrefilteredBRDF", # 预计算BRDF查找表 - "PrefilteredMetalBRDF", # 金属材质BRDF - "PrefilteredCoatBRDF", # 涂层材质BRDF - ] - - for input_name in required_inputs: - if input_name in inputs: - self.target.set_shader_input(input_name, inputs[input_name]) - print(f" ✓ 绑定环境input: {input_name}") - else: - print(f" ⚠️ 缺失环境input: {input_name}") - - # 注意:不绑定MainSceneData InputBlock,因为它会被主Pipeline每帧覆盖 - # 我们会在_set_vr_scene_data()中手动设置所有需要的字段 - - # 可选的AO相关pipes(如果启用了AO插件) - optional_pipes = [ - "AmbientOcclusion", # 环境光遮蔽 - "SkyAO", # 天空AO - ] - - for pipe_name in optional_pipes: - if pipe_name in pipes: - pipe_value = pipes[pipe_name] - if isinstance(pipe_value, (list, tuple)): - self.target.set_shader_input(pipe_name, *pipe_value) - else: - self.target.set_shader_input(pipe_name, pipe_value) - print(f" ✓ 绑定可选AO pipe: {pipe_name}") - - def _set_vr_scene_data(self): - """创建VR专用的MainSceneData UBO(从主Pipeline复制,覆盖VR特定值)""" - from panda3d.core import LVecBase2i - from RenderPipelineFile.rpcore.util.shader_input_blocks import GroupedInputBlock - import math - - # 从主Pipeline的MainSceneData InputBlock获取 - main_input_block = self.pipeline.stage_mgr.input_blocks.get("MainSceneData") - if not main_input_block: - print(" ⚠️ 主Pipeline的MainSceneData不存在") - return - - # 创建VR专用的MainSceneData GroupedInputBlock - vr_main_scene_data = GroupedInputBlock("MainSceneData") - - # 注册所有字段(与主Pipeline相同的类型) - field_definitions = [ - ("camera_pos", "vec3"), - ("view_proj_mat_no_jitter", "mat4"), - ("last_view_proj_mat_no_jitter", "mat4"), - ("last_inv_view_proj_mat_no_jitter", "mat4"), - ("view_mat_z_up", "mat4"), - ("proj_mat", "mat4"), - ("inv_proj_mat", "mat4"), - ("view_mat_billboard", "mat4"), - ("frame_delta", "float"), - ("smooth_frame_delta", "float"), - ("frame_time", "float"), - ("current_film_offset", "vec2"), - ("frame_index", "int"), - ("screen_size", "ivec2"), - ("native_screen_size", "ivec2"), - ("lc_tile_count", "ivec2"), - ("ws_frustum_directions", "mat4"), - ("vs_frustum_directions", "mat4"), - ] - - for field_name, field_type in field_definitions: - vr_main_scene_data.register_pta(field_name, field_type) - - # 复制主Pipeline的所有字段值 - for field_name, _ in field_definitions: - try: - value = main_input_block.get_input(field_name) - vr_main_scene_data.update_input(field_name, value) - except: - pass # 某些字段可能不存在 - - # 覆盖VR特定的分辨率字段 - vr_resolution = LVecBase2i(self._width, self._height) - vr_main_scene_data.update_input("screen_size", vr_resolution) - vr_main_scene_data.update_input("native_screen_size", vr_resolution) - - # 计算并设置VR的光照剔除tile数量 - tile_size_x = self.pipeline.settings["lighting.culling_grid_size_x"] - tile_size_y = self.pipeline.settings["lighting.culling_grid_size_y"] - num_tiles_x = int(math.ceil(self._width / float(tile_size_x))) - num_tiles_y = int(math.ceil(self._height / float(tile_size_y))) - vr_tile_count = LVecBase2i(num_tiles_x, num_tiles_y) - vr_main_scene_data.update_input("lc_tile_count", vr_tile_count) - - # 绑定VR专用的MainSceneData UBO - vr_main_scene_data.bind_to(self.target) - - # 保存引用以便后续更新 - self._vr_main_scene_data = vr_main_scene_data - - print(f" ✓ 创建VR专用MainSceneData UBO") - print(f" ✓ VR screen_size: {self._width}x{self._height}") - print(f" ✓ VR lc_tile_count: {num_tiles_x}x{num_tiles_y}") - - def get_ambient_scene_texture(self): - """获取带环境光的场景纹理""" - return self.target.color_tex if self.target else None - - def cleanup(self): - """清理资源""" - if self.target: - self.target.remove() - self.target = None - print(f"✓ {self.name} 环境光Stage已清理") - - -class VRFinalStage: - """VR最终合成阶段 - Tone mapping + Gamma校正""" - - def __init__(self, name, pipeline): - """ - 初始化VR最终合成Stage - - Args: - name: stage名称 - pipeline: RenderPipeline实例引用 - """ - self.name = name - self.pipeline = pipeline - self.target = None - self._ambient_stage = None - self._vr_main_scene_data = None - - def create(self, width, height, ambient_stage): - """ - 创建最终合成目标 - - Args: - width: 渲染宽度 - height: 渲染高度 - ambient_stage: VRAmbientStage实例(提供带环境光的场景) - """ - print(f"🎬 创建{self.name} Final Stage: {width}x{height}") - - self._ambient_stage = ambient_stage - self._width = width - self._height = height - - # 创建最终输出目标 - self.target = RenderTarget(self.name) - self.target.size = width, height - self.target.add_color_attachment(bits=16, alpha=True) - self.target.prepare_buffer() - - # 设置带环境光的场景输入 - ambient_scene_tex = ambient_stage.get_ambient_scene_texture() - if ambient_scene_tex: - self.target.set_shader_input("ShadedScene", ambient_scene_tex) - - # 设置VR专用的MainSceneData UBO - self._set_vr_scene_data() - - # 加载最终合成shader(tone mapping + gamma校正) - self.target.shader = _load_rp_shader("final_stage.frag.glsl") - - print(f"✓ {self.name} Final Stage创建成功") - return True - - def _set_vr_scene_data(self): - """创建VR专用的MainSceneData UBO(从主Pipeline复制,覆盖VR特定值)""" - from panda3d.core import LVecBase2i - from RenderPipelineFile.rpcore.util.shader_input_blocks import GroupedInputBlock - import math - - # 从主Pipeline的MainSceneData InputBlock获取 - main_input_block = self.pipeline.stage_mgr.input_blocks.get("MainSceneData") - if not main_input_block: - print(" ⚠️ 主Pipeline的MainSceneData不存在") - return - - # 创建VR专用的MainSceneData GroupedInputBlock - vr_main_scene_data = GroupedInputBlock("MainSceneData") - - # 注册所有字段(与主Pipeline相同的类型) - field_definitions = [ - ("camera_pos", "vec3"), - ("view_proj_mat_no_jitter", "mat4"), - ("last_view_proj_mat_no_jitter", "mat4"), - ("last_inv_view_proj_mat_no_jitter", "mat4"), - ("view_mat_z_up", "mat4"), - ("proj_mat", "mat4"), - ("inv_proj_mat", "mat4"), - ("view_mat_billboard", "mat4"), - ("frame_delta", "float"), - ("smooth_frame_delta", "float"), - ("frame_time", "float"), - ("current_film_offset", "vec2"), - ("frame_index", "int"), - ("screen_size", "ivec2"), - ("native_screen_size", "ivec2"), - ("lc_tile_count", "ivec2"), - ("ws_frustum_directions", "mat4"), - ("vs_frustum_directions", "mat4"), - ] - - for field_name, field_type in field_definitions: - vr_main_scene_data.register_pta(field_name, field_type) - - # 复制主Pipeline的所有字段值 - for field_name, _ in field_definitions: - try: - value = main_input_block.get_input(field_name) - vr_main_scene_data.update_input(field_name, value) - except: - pass # 某些字段可能不存在 - - # 覆盖VR特定的分辨率字段 - vr_resolution = LVecBase2i(self._width, self._height) - vr_main_scene_data.update_input("screen_size", vr_resolution) - vr_main_scene_data.update_input("native_screen_size", vr_resolution) - - # 计算并设置VR的光照剔除tile数量 - tile_size_x = self.pipeline.settings["lighting.culling_grid_size_x"] - tile_size_y = self.pipeline.settings["lighting.culling_grid_size_y"] - num_tiles_x = int(math.ceil(self._width / float(tile_size_x))) - num_tiles_y = int(math.ceil(self._height / float(tile_size_y))) - vr_tile_count = LVecBase2i(num_tiles_x, num_tiles_y) - vr_main_scene_data.update_input("lc_tile_count", vr_tile_count) - - # 绑定VR专用的MainSceneData UBO - vr_main_scene_data.bind_to(self.target) - - # 保存引用以便后续更新 - self._vr_main_scene_data = vr_main_scene_data - - print(f" ✓ 创建VR专用MainSceneData UBO (Final)") - print(f" ✓ VR screen_size: {self._width}x{self._height}") - print(f" ✓ VR lc_tile_count: {num_tiles_x}x{num_tiles_y}") - - def get_final_texture(self): - """获取最终输出纹理(用于提交到OpenVR)""" - return self.target.color_tex if self.target else None - - def get_internal_buffer(self): - """获取内部GraphicsOutput""" - return self.target.internal_buffer if self.target else None - - def cleanup(self): - """清理资源""" - if self.target: - self.target.remove() - self.target = None - print(f"✓ {self.name} Final Stage已清理") - - -class VRPipelineController: - """VR渲染管线控制器 - 管理左右眼的完整渲染管线""" - - def __init__(self, pipeline): - """ - 初始化VR Pipeline控制器 - - Args: - pipeline: 主RenderPipeline实例 - """ - self.pipeline = pipeline - - # 左眼stages - self.left_gbuffer = None - self.left_lighting = None - self.left_ambient = None - self.left_final = None - - # 右眼stages - self.right_gbuffer = None - self.right_lighting = None - self.right_ambient = None - self.right_final = None - - def create_eye_pipeline(self, eye_name, width, height, vr_camera): - """ - 为单个眼睛创建完整的渲染管线 - - Args: - eye_name: "Left" 或 "Right" - width: 渲染宽度 - height: 渲染高度 - vr_camera: VR相机NodePath - - Returns: - tuple: (gbuffer_stage, lighting_stage, ambient_stage, final_stage) - """ - print(f"\n🚀 创建VR {eye_name}眼渲染管线") - - # 1. 创建GBuffer Stage - gbuffer = VRGBufferStage(f"VR_{eye_name}_GBuffer", self.pipeline) - if not gbuffer.create(width, height, vr_camera): - return None, None, None, None - - # 2. 创建光照Stage(直接光照) - lighting = VRLightingStage(f"VR_{eye_name}_Lighting", self.pipeline) - if not lighting.create(width, height, gbuffer): - gbuffer.cleanup() - return None, None, None, None - - # 3. 创建环境光Stage(IBL) - ambient = VRAmbientStage(f"VR_{eye_name}_Ambient", self.pipeline) - if not ambient.create(width, height, lighting, gbuffer): - lighting.cleanup() - gbuffer.cleanup() - return None, None, None, None - - # 4. 创建最终合成Stage(tone mapping + gamma) - final = VRFinalStage(f"VR_{eye_name}_Final", self.pipeline) - if not final.create(width, height, ambient): - ambient.cleanup() - lighting.cleanup() - gbuffer.cleanup() - return None, None, None, None - - print(f"✅ VR {eye_name}眼渲染管线创建成功\n") - return gbuffer, lighting, ambient, final - - def create_stereo_pipeline(self, width, height, left_camera, right_camera): - """ - 创建立体渲染管线(左右眼)- 完整版本 - - Args: - width: 每个眼睛的渲染宽度 - height: 每个眼睛的渲染高度 - left_camera: 左眼相机NodePath - right_camera: 右眼相机NodePath - - Returns: - bool: 创建是否成功 - """ - print("=" * 60) - print("🎯 开始创建VR立体渲染管线(完整版)") - print("=" * 60) - - # 创建左眼完整管线 - result = self.create_eye_pipeline("Left", width, height, left_camera) - if not all(result): - print("❌ 左眼渲染管线创建失败") - return False - self.left_gbuffer, self.left_lighting, self.left_ambient, self.left_final = result - - # 🔧 关键修复:确保左眼buffer完全初始化后再创建右眼 - # 验证左眼buffer状态 - left_buffer = self.left_final.get_internal_buffer() if self.left_final else None - if not left_buffer: - print("❌ 左眼内部buffer无效") - self.cleanup_left() - return False - - print(" ✅ 左眼管线验证通过,准备创建右眼...") - - # 创建右眼完整管线 - result = self.create_eye_pipeline("Right", width, height, right_camera) - if not all(result): - print("❌ 右眼渲染管线创建失败") - self.cleanup_left() - return False - self.right_gbuffer, self.right_lighting, self.right_ambient, self.right_final = result - - print("\n" + "=" * 60) - print("✅ VR立体渲染管线创建成功!") - print(" 管线流程: GBuffer → Lighting → Ambient → Final") - print("=" * 60) - return True - - def get_left_textures(self): - """获取左眼的所有纹理""" - if not self.left_final: - return None - - return { - "final": self.left_final.get_final_texture(), - "gbuffer": self.left_gbuffer.get_gbuffer_textures() if self.left_gbuffer else None - } - - def get_right_textures(self): - """获取右眼的所有纹理""" - if not self.right_final: - return None - - return { - "final": self.right_final.get_final_texture(), - "gbuffer": self.right_gbuffer.get_gbuffer_textures() if self.right_gbuffer else None - } - - def get_left_buffer(self): - """获取左眼Final stage的内部buffer(用于设置DrawCallback)""" - return self.left_final.get_internal_buffer() if self.left_final else None - - def get_right_buffer(self): - """获取右眼Final stage的内部buffer(用于设置DrawCallback)""" - return self.right_final.get_internal_buffer() if self.right_final else None - - def cleanup_left(self): - """清理左眼资源""" - if self.left_final: - self.left_final.cleanup() - self.left_final = None - if self.left_ambient: - self.left_ambient.cleanup() - self.left_ambient = None - if self.left_lighting: - self.left_lighting.cleanup() - self.left_lighting = None - if self.left_gbuffer: - self.left_gbuffer.cleanup() - self.left_gbuffer = None - - def cleanup_right(self): - """清理右眼资源""" - if self.right_final: - self.right_final.cleanup() - self.right_final = None - if self.right_ambient: - self.right_ambient.cleanup() - self.right_ambient = None - if self.right_lighting: - self.right_lighting.cleanup() - self.right_lighting = None - if self.right_gbuffer: - self.right_gbuffer.cleanup() - self.right_gbuffer = None - - def cleanup_all(self): - """清理所有VR渲染资源""" - print("\n🧹 清理VR渲染管线...") - self.cleanup_left() - self.cleanup_right() - print("✓ VR渲染管线已清理\n") +""" +VR专用渲染Stages + +为VR创建简化但完整的渲染管线,支持: +- GBuffer生成(延迟渲染基础) +- 光照计算(复用主Pipeline的LightManager) +- 最终合成(tone mapping + gamma校正) +""" + +from panda3d.core import Shader +from RenderPipelineFile.rpcore.render_target import RenderTarget +from RenderPipelineFile.rpcore.globals import Globals +from RenderPipelineFile.rpcore.loader import RPLoader +from RenderPipelineFile.rpcore.util.shader_input_blocks import SimpleInputBlock + + +def _load_rp_shader(shader_name): + """ + 加载RenderPipeline shader + + Args: + shader_name: shader文件名(如"apply_lights.frag.glsl") + + Returns: + Shader对象 + """ + default_vert = "/$$rp/shader/default_post_process.vert.glsl" + frag_path = f"/$$rp/shader/{shader_name}" + shader = RPLoader.load_shader(default_vert, frag_path) + if shader: + print(f" ✅ Shader加载成功: {shader_name}") + else: + print(f" ❌ Shader加载失败: {shader_name}") + return shader + + +class VRGBufferStage: + """VR GBuffer阶段 - 为单个VR眼睛创建GBuffer""" + + def __init__(self, name, pipeline): + """ + 初始化VR GBuffer Stage + + Args: + name: stage名称(如"VR_Left_GBuffer") + pipeline: RenderPipeline实例引用 + """ + self.name = name + self.pipeline = pipeline + self.target = None + self._camera = None + + def create(self, width, height, vr_camera): + """ + 创建GBuffer渲染目标 + + Args: + width: 渲染宽度 + height: 渲染高度 + vr_camera: VR相机NodePath + """ + print(f"🎨 创建{self.name} GBuffer: {width}x{height}") + + # 创建RenderTarget + self.target = RenderTarget(self.name) + self.target.size = width, height + + # 添加GBuffer纹理附件(与主GBufferStage相同) + self.target.add_color_attachment(bits=16, alpha=True) # Data0 + self.target.add_depth_attachment(bits=32) # Depth + self.target.add_aux_attachments(bits=16, count=2) # Data1, Data2 + + # 准备渲染(绑定相机) + self.target.prepare_render(vr_camera) + self._camera = vr_camera + + # 🌌 关键修复:确保VR相机能看到天空盒 + # RenderPipeline的天空盒在render节点下,使用unsorted bin和shader effects + if self.target._source_region: + # 确保DisplayRegion支持所有bin排序 + self.target._source_region.setSort(20) # 设置合理的sort值 + print(f" ✓ DisplayRegion sort已设置") + + # 🔑 关键:设置VR相机的initial state为render的state + # 这样VR相机才能继承RenderPipeline应用在render节点上的所有shader effects(包括天空盒shader) + if vr_camera and vr_camera.node(): + # 使用Panda3D的内置render节点(通过builtins) + try: + from panda3d.core import NodePath + import builtins + if hasattr(builtins, 'render'): + render = builtins.render + vr_camera.node().setInitialState(render.getState()) + print(f" ✓ VR相机initial state已设置为render的state") + else: + print(f" ⚠️ builtins.render不存在,跳过initial state设置") + except Exception as e: + print(f" ⚠️ 设置VR相机initial state失败: {e}") + + # 设置正常的背景色(与主窗口一致的天空色) + if self.target.internal_buffer: + buffer = self.target.internal_buffer + buffer.setClearColorActive(True) + # 使用主RenderPipeline的天空色(浅蓝色) + buffer.setClearColor((0.53, 0.81, 0.92, 1.0)) # 天空蓝 + + # 同时也设置DisplayRegion的clear + if hasattr(self.target, '_source_region') and self.target._source_region: + self.target._source_region.setClearColorActive(True) + self.target._source_region.setClearColor((0.53, 0.81, 0.92, 1.0)) + + print(f"✓ {self.name} GBuffer创建成功") + return True + + def get_gbuffer_textures(self): + """ + 获取GBuffer纹理字典 + + Returns: + dict: 包含depth, data0, data1, data2的纹理字典 + """ + if not self.target: + return None + + return { + "depth": self.target.depth_tex, + "data0": self.target.color_tex, + "data1": self.target.aux_tex[0], + "data2": self.target.aux_tex[1], + } + + def make_gbuffer_ubo(self): + """创建GBuffer UBO(与主GBufferStage相同)""" + # 注意:必须使用固定的"GBuffer"名字,shader期望这个名字 + ubo = SimpleInputBlock("GBuffer") + ubo.add_input("Depth", self.target.depth_tex) + ubo.add_input("Data0", self.target.color_tex) + ubo.add_input("Data1", self.target.aux_tex[0]) + ubo.add_input("Data2", self.target.aux_tex[1]) + return ubo + + def get_internal_buffer(self): + """获取内部GraphicsOutput""" + return self.target.internal_buffer if self.target else None + + def cleanup(self): + """清理资源""" + if self.target: + self.target.remove() + self.target = None + print(f"✓ {self.name} GBuffer已清理") + + +class VRLightingStage: + """VR光照阶段 - 使用主Pipeline的LightManager计算光照""" + + def __init__(self, name, pipeline): + """ + 初始化VR光照Stage + + Args: + name: stage名称 + pipeline: RenderPipeline实例引用 + """ + self.name = name + self.pipeline = pipeline + self.target = None + self._gbuffer_stage = None + self._vr_main_scene_data = None + + def create(self, width, height, gbuffer_stage): + """ + 创建光照渲染目标 + + Args: + width: 渲染宽度 + height: 渲染高度 + gbuffer_stage: VRGBufferStage实例(提供GBuffer纹理) + """ + print(f"💡 创建{self.name} 光照Stage: {width}x{height}") + + self._gbuffer_stage = gbuffer_stage + self._width = width + self._height = height + + # 创建光照输出目标 + self.target = RenderTarget(self.name) + self.target.size = width, height + self.target.add_color_attachment(bits=16, alpha=True) + self.target.prepare_buffer() + + # 1. 创建GBuffer UBO(使用SimpleInputBlock) + gbuffer_ubo = gbuffer_stage.make_gbuffer_ubo() + gbuffer_ubo.bind_to(self.target) + + # 2. 从主Pipeline获取光照相关inputs + self._bind_pipeline_inputs() + + # 3. 从主Pipeline获取光照相关pipes + self._bind_pipeline_pipes() + + # 4. 覆盖VR特定的MainSceneData字段 + self._set_vr_scene_data() + + # 5. 加载光照shader + self.target.shader = _load_rp_shader("apply_lights.frag.glsl") + + print(f"✓ {self.name} 光照Stage创建成功") + return True + + def _bind_pipeline_inputs(self): + """从主Pipeline获取光照相关shader inputs""" + if not self.pipeline or not self.target: + return + + stage_mgr = self.pipeline.stage_mgr + inputs = stage_mgr.inputs + + # 必需的光照inputs + required_inputs = [ + "AllLightsData", # 光源数据 + "ShadowSourceData", # 阴影源数据 + "IESDatasetTex", # IES光照纹理 + "maxLightIndex", # 最大光源索引 + ] + + for input_name in required_inputs: + if input_name in inputs: + self.target.set_shader_input(input_name, inputs[input_name]) + print(f" ✓ 绑定input: {input_name}") + else: + print(f" ⚠️ 缺失input: {input_name}") + + # 注意:不绑定MainSceneData InputBlock,因为它会被主Pipeline每帧覆盖 + # 我们会在_set_vr_scene_data()中手动设置所有需要的字段 + + def _bind_pipeline_pipes(self): + """从主Pipeline获取光照相关pipes""" + if not self.pipeline or not self.target: + return + + stage_mgr = self.pipeline.stage_mgr + pipes = stage_mgr.pipes + + # 必需的光照pipes + required_pipes = [ + "CellIndices", # 光照剔除cell索引 + "PerCellLights", # 每个cell的光源列表 + "PerCellLightsCounts", # 每个cell的光源数量 + "ShadowAtlas", # 阴影图集 + "ShadowAtlasPCF", # PCF阴影图集 + ] + + # 可选的pipes + optional_pipes = [ + "CombinedVelocity", # 运动矢量(用于调试模式) + ] + + for pipe_name in required_pipes: + if pipe_name in pipes: + pipe_value = pipes[pipe_name] + if isinstance(pipe_value, (list, tuple)): + self.target.set_shader_input(pipe_name, *pipe_value) + else: + self.target.set_shader_input(pipe_name, pipe_value) + print(f" ✓ 绑定pipe: {pipe_name}") + else: + print(f" ❌ 缺失必需pipe: {pipe_name}") + + for pipe_name in optional_pipes: + if pipe_name in pipes: + pipe_value = pipes[pipe_name] + if isinstance(pipe_value, (list, tuple)): + self.target.set_shader_input(pipe_name, *pipe_value) + else: + self.target.set_shader_input(pipe_name, pipe_value) + print(f" ✓ 绑定可选pipe: {pipe_name}") + + def _set_vr_scene_data(self): + """创建VR专用的MainSceneData UBO(从主Pipeline复制,覆盖VR特定值)""" + from panda3d.core import LVecBase2i + from RenderPipelineFile.rpcore.util.shader_input_blocks import GroupedInputBlock + import math + + # 从主Pipeline的MainSceneData InputBlock获取 + main_input_block = self.pipeline.stage_mgr.input_blocks.get("MainSceneData") + if not main_input_block: + print(" ⚠️ 主Pipeline的MainSceneData不存在") + return + + # 创建VR专用的MainSceneData GroupedInputBlock + vr_main_scene_data = GroupedInputBlock("MainSceneData") + + # 注册所有字段(与主Pipeline相同的类型) + field_definitions = [ + ("camera_pos", "vec3"), + ("view_proj_mat_no_jitter", "mat4"), + ("last_view_proj_mat_no_jitter", "mat4"), + ("last_inv_view_proj_mat_no_jitter", "mat4"), + ("view_mat_z_up", "mat4"), + ("proj_mat", "mat4"), + ("inv_proj_mat", "mat4"), + ("view_mat_billboard", "mat4"), + ("frame_delta", "float"), + ("smooth_frame_delta", "float"), + ("frame_time", "float"), + ("current_film_offset", "vec2"), + ("frame_index", "int"), + ("screen_size", "ivec2"), + ("native_screen_size", "ivec2"), + ("lc_tile_count", "ivec2"), + ("ws_frustum_directions", "mat4"), + ("vs_frustum_directions", "mat4"), + ] + + for field_name, field_type in field_definitions: + vr_main_scene_data.register_pta(field_name, field_type) + + # 复制主Pipeline的所有字段值 + for field_name, _ in field_definitions: + try: + value = main_input_block.get_input(field_name) + vr_main_scene_data.update_input(field_name, value) + except: + pass # 某些字段可能不存在 + + # 覆盖VR特定的分辨率字段 + vr_resolution = LVecBase2i(self._width, self._height) + vr_main_scene_data.update_input("screen_size", vr_resolution) + vr_main_scene_data.update_input("native_screen_size", vr_resolution) + + # 计算并设置VR的光照剔除tile数量 + tile_size_x = self.pipeline.settings["lighting.culling_grid_size_x"] + tile_size_y = self.pipeline.settings["lighting.culling_grid_size_y"] + num_tiles_x = int(math.ceil(self._width / float(tile_size_x))) + num_tiles_y = int(math.ceil(self._height / float(tile_size_y))) + vr_tile_count = LVecBase2i(num_tiles_x, num_tiles_y) + vr_main_scene_data.update_input("lc_tile_count", vr_tile_count) + + # 绑定VR专用的MainSceneData UBO + vr_main_scene_data.bind_to(self.target) + + # 保存引用以便后续更新 + self._vr_main_scene_data = vr_main_scene_data + + print(f" ✓ 创建VR专用MainSceneData UBO") + print(f" ✓ VR screen_size: {self._width}x{self._height}") + print(f" ✓ VR lc_tile_count: {num_tiles_x}x{num_tiles_y}") + + def get_shaded_texture(self): + """获取光照计算后的纹理""" + return self.target.color_tex if self.target else None + + def cleanup(self): + """清理资源""" + if self.target: + self.target.remove() + self.target = None + print(f"✓ {self.name} 光照Stage已清理") + + +class VRAmbientStage: + """VR环境光阶段 - 计算基于图像的照明(IBL)""" + + def __init__(self, name, pipeline): + """ + 初始化VR环境光Stage + + Args: + name: stage名称 + pipeline: RenderPipeline实例引用 + """ + self.name = name + self.pipeline = pipeline + self.target = None + self._lighting_stage = None + self._gbuffer_stage = None + self._vr_main_scene_data = None + + def create(self, width, height, lighting_stage, gbuffer_stage): + """ + 创建环境光渲染目标 + + Args: + width: 渲染宽度 + height: 渲染高度 + lighting_stage: VRLightingStage实例(提供直接光照结果) + gbuffer_stage: VRGBufferStage实例(提供GBuffer数据) + """ + print(f"🌍 创建{self.name} 环境光Stage: {width}x{height}") + + self._lighting_stage = lighting_stage + self._gbuffer_stage = gbuffer_stage + self._width = width + self._height = height + + # 创建环境光输出目标 + self.target = RenderTarget(self.name) + self.target.size = width, height + self.target.add_color_attachment(bits=16, alpha=True) + self.target.prepare_buffer() + + # 1. 设置直接光照输入(ShadedScene) + shaded_tex = lighting_stage.get_shaded_texture() + if shaded_tex: + self.target.set_shader_input("ShadedScene", shaded_tex) + + # 2. 绑定GBuffer UBO + gbuffer_ubo = gbuffer_stage.make_gbuffer_ubo() + gbuffer_ubo.bind_to(self.target) + + # 3. 从主Pipeline获取环境相关inputs + self._bind_environment_inputs() + + # 4. 覆盖VR特定的MainSceneData字段 + self._set_vr_scene_data() + + # 5. 加载环境光shader + self.target.shader = _load_rp_shader("ambient_stage.frag.glsl") + + print(f"✓ {self.name} 环境光Stage创建成功") + return True + + def _bind_environment_inputs(self): + """从主Pipeline获取环境相关shader inputs""" + if not self.pipeline or not self.target: + return + + stage_mgr = self.pipeline.stage_mgr + inputs = stage_mgr.inputs + pipes = stage_mgr.pipes + + # 必需的环境inputs + required_inputs = [ + "DefaultEnvmap", # 默认环境立方体贴图 + "PrefilteredBRDF", # 预计算BRDF查找表 + "PrefilteredMetalBRDF", # 金属材质BRDF + "PrefilteredCoatBRDF", # 涂层材质BRDF + ] + + for input_name in required_inputs: + if input_name in inputs: + self.target.set_shader_input(input_name, inputs[input_name]) + print(f" ✓ 绑定环境input: {input_name}") + else: + print(f" ⚠️ 缺失环境input: {input_name}") + + # 注意:不绑定MainSceneData InputBlock,因为它会被主Pipeline每帧覆盖 + # 我们会在_set_vr_scene_data()中手动设置所有需要的字段 + + # 可选的AO相关pipes(如果启用了AO插件) + optional_pipes = [ + "AmbientOcclusion", # 环境光遮蔽 + "SkyAO", # 天空AO + ] + + for pipe_name in optional_pipes: + if pipe_name in pipes: + pipe_value = pipes[pipe_name] + if isinstance(pipe_value, (list, tuple)): + self.target.set_shader_input(pipe_name, *pipe_value) + else: + self.target.set_shader_input(pipe_name, pipe_value) + print(f" ✓ 绑定可选AO pipe: {pipe_name}") + + def _set_vr_scene_data(self): + """创建VR专用的MainSceneData UBO(从主Pipeline复制,覆盖VR特定值)""" + from panda3d.core import LVecBase2i + from RenderPipelineFile.rpcore.util.shader_input_blocks import GroupedInputBlock + import math + + # 从主Pipeline的MainSceneData InputBlock获取 + main_input_block = self.pipeline.stage_mgr.input_blocks.get("MainSceneData") + if not main_input_block: + print(" ⚠️ 主Pipeline的MainSceneData不存在") + return + + # 创建VR专用的MainSceneData GroupedInputBlock + vr_main_scene_data = GroupedInputBlock("MainSceneData") + + # 注册所有字段(与主Pipeline相同的类型) + field_definitions = [ + ("camera_pos", "vec3"), + ("view_proj_mat_no_jitter", "mat4"), + ("last_view_proj_mat_no_jitter", "mat4"), + ("last_inv_view_proj_mat_no_jitter", "mat4"), + ("view_mat_z_up", "mat4"), + ("proj_mat", "mat4"), + ("inv_proj_mat", "mat4"), + ("view_mat_billboard", "mat4"), + ("frame_delta", "float"), + ("smooth_frame_delta", "float"), + ("frame_time", "float"), + ("current_film_offset", "vec2"), + ("frame_index", "int"), + ("screen_size", "ivec2"), + ("native_screen_size", "ivec2"), + ("lc_tile_count", "ivec2"), + ("ws_frustum_directions", "mat4"), + ("vs_frustum_directions", "mat4"), + ] + + for field_name, field_type in field_definitions: + vr_main_scene_data.register_pta(field_name, field_type) + + # 复制主Pipeline的所有字段值 + for field_name, _ in field_definitions: + try: + value = main_input_block.get_input(field_name) + vr_main_scene_data.update_input(field_name, value) + except: + pass # 某些字段可能不存在 + + # 覆盖VR特定的分辨率字段 + vr_resolution = LVecBase2i(self._width, self._height) + vr_main_scene_data.update_input("screen_size", vr_resolution) + vr_main_scene_data.update_input("native_screen_size", vr_resolution) + + # 计算并设置VR的光照剔除tile数量 + tile_size_x = self.pipeline.settings["lighting.culling_grid_size_x"] + tile_size_y = self.pipeline.settings["lighting.culling_grid_size_y"] + num_tiles_x = int(math.ceil(self._width / float(tile_size_x))) + num_tiles_y = int(math.ceil(self._height / float(tile_size_y))) + vr_tile_count = LVecBase2i(num_tiles_x, num_tiles_y) + vr_main_scene_data.update_input("lc_tile_count", vr_tile_count) + + # 绑定VR专用的MainSceneData UBO + vr_main_scene_data.bind_to(self.target) + + # 保存引用以便后续更新 + self._vr_main_scene_data = vr_main_scene_data + + print(f" ✓ 创建VR专用MainSceneData UBO") + print(f" ✓ VR screen_size: {self._width}x{self._height}") + print(f" ✓ VR lc_tile_count: {num_tiles_x}x{num_tiles_y}") + + def get_ambient_scene_texture(self): + """获取带环境光的场景纹理""" + return self.target.color_tex if self.target else None + + def cleanup(self): + """清理资源""" + if self.target: + self.target.remove() + self.target = None + print(f"✓ {self.name} 环境光Stage已清理") + + +class VRFinalStage: + """VR最终合成阶段 - Tone mapping + Gamma校正""" + + def __init__(self, name, pipeline): + """ + 初始化VR最终合成Stage + + Args: + name: stage名称 + pipeline: RenderPipeline实例引用 + """ + self.name = name + self.pipeline = pipeline + self.target = None + self._ambient_stage = None + self._vr_main_scene_data = None + + def create(self, width, height, ambient_stage): + """ + 创建最终合成目标 + + Args: + width: 渲染宽度 + height: 渲染高度 + ambient_stage: VRAmbientStage实例(提供带环境光的场景) + """ + print(f"🎬 创建{self.name} Final Stage: {width}x{height}") + + self._ambient_stage = ambient_stage + self._width = width + self._height = height + + # 创建最终输出目标 + self.target = RenderTarget(self.name) + self.target.size = width, height + self.target.add_color_attachment(bits=16, alpha=True) + self.target.prepare_buffer() + + # 设置带环境光的场景输入 + ambient_scene_tex = ambient_stage.get_ambient_scene_texture() + if ambient_scene_tex: + self.target.set_shader_input("ShadedScene", ambient_scene_tex) + + # 设置VR专用的MainSceneData UBO + self._set_vr_scene_data() + + # 加载最终合成shader(tone mapping + gamma校正) + self.target.shader = _load_rp_shader("final_stage.frag.glsl") + + print(f"✓ {self.name} Final Stage创建成功") + return True + + def _set_vr_scene_data(self): + """创建VR专用的MainSceneData UBO(从主Pipeline复制,覆盖VR特定值)""" + from panda3d.core import LVecBase2i + from RenderPipelineFile.rpcore.util.shader_input_blocks import GroupedInputBlock + import math + + # 从主Pipeline的MainSceneData InputBlock获取 + main_input_block = self.pipeline.stage_mgr.input_blocks.get("MainSceneData") + if not main_input_block: + print(" ⚠️ 主Pipeline的MainSceneData不存在") + return + + # 创建VR专用的MainSceneData GroupedInputBlock + vr_main_scene_data = GroupedInputBlock("MainSceneData") + + # 注册所有字段(与主Pipeline相同的类型) + field_definitions = [ + ("camera_pos", "vec3"), + ("view_proj_mat_no_jitter", "mat4"), + ("last_view_proj_mat_no_jitter", "mat4"), + ("last_inv_view_proj_mat_no_jitter", "mat4"), + ("view_mat_z_up", "mat4"), + ("proj_mat", "mat4"), + ("inv_proj_mat", "mat4"), + ("view_mat_billboard", "mat4"), + ("frame_delta", "float"), + ("smooth_frame_delta", "float"), + ("frame_time", "float"), + ("current_film_offset", "vec2"), + ("frame_index", "int"), + ("screen_size", "ivec2"), + ("native_screen_size", "ivec2"), + ("lc_tile_count", "ivec2"), + ("ws_frustum_directions", "mat4"), + ("vs_frustum_directions", "mat4"), + ] + + for field_name, field_type in field_definitions: + vr_main_scene_data.register_pta(field_name, field_type) + + # 复制主Pipeline的所有字段值 + for field_name, _ in field_definitions: + try: + value = main_input_block.get_input(field_name) + vr_main_scene_data.update_input(field_name, value) + except: + pass # 某些字段可能不存在 + + # 覆盖VR特定的分辨率字段 + vr_resolution = LVecBase2i(self._width, self._height) + vr_main_scene_data.update_input("screen_size", vr_resolution) + vr_main_scene_data.update_input("native_screen_size", vr_resolution) + + # 计算并设置VR的光照剔除tile数量 + tile_size_x = self.pipeline.settings["lighting.culling_grid_size_x"] + tile_size_y = self.pipeline.settings["lighting.culling_grid_size_y"] + num_tiles_x = int(math.ceil(self._width / float(tile_size_x))) + num_tiles_y = int(math.ceil(self._height / float(tile_size_y))) + vr_tile_count = LVecBase2i(num_tiles_x, num_tiles_y) + vr_main_scene_data.update_input("lc_tile_count", vr_tile_count) + + # 绑定VR专用的MainSceneData UBO + vr_main_scene_data.bind_to(self.target) + + # 保存引用以便后续更新 + self._vr_main_scene_data = vr_main_scene_data + + print(f" ✓ 创建VR专用MainSceneData UBO (Final)") + print(f" ✓ VR screen_size: {self._width}x{self._height}") + print(f" ✓ VR lc_tile_count: {num_tiles_x}x{num_tiles_y}") + + def get_final_texture(self): + """获取最终输出纹理(用于提交到OpenVR)""" + return self.target.color_tex if self.target else None + + def get_internal_buffer(self): + """获取内部GraphicsOutput""" + return self.target.internal_buffer if self.target else None + + def cleanup(self): + """清理资源""" + if self.target: + self.target.remove() + self.target = None + print(f"✓ {self.name} Final Stage已清理") + + +class VRPipelineController: + """VR渲染管线控制器 - 管理左右眼的完整渲染管线""" + + def __init__(self, pipeline): + """ + 初始化VR Pipeline控制器 + + Args: + pipeline: 主RenderPipeline实例 + """ + self.pipeline = pipeline + + # 左眼stages + self.left_gbuffer = None + self.left_lighting = None + self.left_ambient = None + self.left_final = None + + # 右眼stages + self.right_gbuffer = None + self.right_lighting = None + self.right_ambient = None + self.right_final = None + + def create_eye_pipeline(self, eye_name, width, height, vr_camera): + """ + 为单个眼睛创建完整的渲染管线 + + Args: + eye_name: "Left" 或 "Right" + width: 渲染宽度 + height: 渲染高度 + vr_camera: VR相机NodePath + + Returns: + tuple: (gbuffer_stage, lighting_stage, ambient_stage, final_stage) + """ + print(f"\n🚀 创建VR {eye_name}眼渲染管线") + + # 1. 创建GBuffer Stage + gbuffer = VRGBufferStage(f"VR_{eye_name}_GBuffer", self.pipeline) + if not gbuffer.create(width, height, vr_camera): + return None, None, None, None + + # 2. 创建光照Stage(直接光照) + lighting = VRLightingStage(f"VR_{eye_name}_Lighting", self.pipeline) + if not lighting.create(width, height, gbuffer): + gbuffer.cleanup() + return None, None, None, None + + # 3. 创建环境光Stage(IBL) + ambient = VRAmbientStage(f"VR_{eye_name}_Ambient", self.pipeline) + if not ambient.create(width, height, lighting, gbuffer): + lighting.cleanup() + gbuffer.cleanup() + return None, None, None, None + + # 4. 创建最终合成Stage(tone mapping + gamma) + final = VRFinalStage(f"VR_{eye_name}_Final", self.pipeline) + if not final.create(width, height, ambient): + ambient.cleanup() + lighting.cleanup() + gbuffer.cleanup() + return None, None, None, None + + print(f"✅ VR {eye_name}眼渲染管线创建成功\n") + return gbuffer, lighting, ambient, final + + def create_stereo_pipeline(self, width, height, left_camera, right_camera): + """ + 创建立体渲染管线(左右眼)- 完整版本 + + Args: + width: 每个眼睛的渲染宽度 + height: 每个眼睛的渲染高度 + left_camera: 左眼相机NodePath + right_camera: 右眼相机NodePath + + Returns: + bool: 创建是否成功 + """ + print("=" * 60) + print("🎯 开始创建VR立体渲染管线(完整版)") + print("=" * 60) + + # 创建左眼完整管线 + result = self.create_eye_pipeline("Left", width, height, left_camera) + if not all(result): + print("❌ 左眼渲染管线创建失败") + return False + self.left_gbuffer, self.left_lighting, self.left_ambient, self.left_final = result + + # 🔧 关键修复:确保左眼buffer完全初始化后再创建右眼 + # 验证左眼buffer状态 + left_buffer = self.left_final.get_internal_buffer() if self.left_final else None + if not left_buffer: + print("❌ 左眼内部buffer无效") + self.cleanup_left() + return False + + print(" ✅ 左眼管线验证通过,准备创建右眼...") + + # 创建右眼完整管线 + result = self.create_eye_pipeline("Right", width, height, right_camera) + if not all(result): + print("❌ 右眼渲染管线创建失败") + self.cleanup_left() + return False + self.right_gbuffer, self.right_lighting, self.right_ambient, self.right_final = result + + print("\n" + "=" * 60) + print("✅ VR立体渲染管线创建成功!") + print(" 管线流程: GBuffer → Lighting → Ambient → Final") + print("=" * 60) + return True + + def get_left_textures(self): + """获取左眼的所有纹理""" + if not self.left_final: + return None + + return { + "final": self.left_final.get_final_texture(), + "gbuffer": self.left_gbuffer.get_gbuffer_textures() if self.left_gbuffer else None + } + + def get_right_textures(self): + """获取右眼的所有纹理""" + if not self.right_final: + return None + + return { + "final": self.right_final.get_final_texture(), + "gbuffer": self.right_gbuffer.get_gbuffer_textures() if self.right_gbuffer else None + } + + def get_left_buffer(self): + """获取左眼Final stage的内部buffer(用于设置DrawCallback)""" + return self.left_final.get_internal_buffer() if self.left_final else None + + def get_right_buffer(self): + """获取右眼Final stage的内部buffer(用于设置DrawCallback)""" + return self.right_final.get_internal_buffer() if self.right_final else None + + def cleanup_left(self): + """清理左眼资源""" + if self.left_final: + self.left_final.cleanup() + self.left_final = None + if self.left_ambient: + self.left_ambient.cleanup() + self.left_ambient = None + if self.left_lighting: + self.left_lighting.cleanup() + self.left_lighting = None + if self.left_gbuffer: + self.left_gbuffer.cleanup() + self.left_gbuffer = None + + def cleanup_right(self): + """清理右眼资源""" + if self.right_final: + self.right_final.cleanup() + self.right_final = None + if self.right_ambient: + self.right_ambient.cleanup() + self.right_ambient = None + if self.right_lighting: + self.right_lighting.cleanup() + self.right_lighting = None + if self.right_gbuffer: + self.right_gbuffer.cleanup() + self.right_gbuffer = None + + def cleanup_all(self): + """清理所有VR渲染资源""" + print("\n🧹 清理VR渲染管线...") + self.cleanup_left() + self.cleanup_right() + print("✓ VR渲染管线已清理\n") diff --git a/core/vr/testing/__init__.py b/core/vr/testing/__init__.py index b5c012fb..03d0b0b4 100644 --- a/core/vr/testing/__init__.py +++ b/core/vr/testing/__init__.py @@ -1,8 +1,8 @@ -""" -VR测试调试子系统 -提供VR测试模式、HUD显示和性能测试功能 -""" - -from .test_mode import VRTestMode - -__all__ = ['VRTestMode'] +""" +VR测试调试子系统 +提供VR测试模式、HUD显示和性能测试功能 +""" + +from .test_mode import VRTestMode + +__all__ = ['VRTestMode'] diff --git a/core/vr/testing/test_mode.py b/core/vr/testing/test_mode.py index bf61169c..c12b8922 100644 --- a/core/vr/testing/test_mode.py +++ b/core/vr/testing/test_mode.py @@ -1,621 +1,621 @@ -""" -VR测试调试子系统 -负责VR测试模式、HUD显示和性能测试功能 -""" - -import time - - -class VRTestMode: - """VR测试模式系统 - - 功能: - - VR内容直接显示在屏幕上(不提交给OpenVR) - - 实时性能监控HUD - - 支持左眼/右眼/立体三种显示模式 - - 渐进式测试功能(纹理提交、姿态等待) - - 性能测试和调试工具 - """ - - def __init__(self, vr_manager): - """初始化VR测试模式系统 - - Args: - vr_manager: VRManager实例的引用 - """ - self.vr_manager = vr_manager - - # ===== 测试模式状态 ===== - self.vr_test_mode = False # 是否启用VR测试模式 - self.test_display_mode = 'stereo' # 'left', 'right', 'stereo' - self.test_mode_initialized = False # 测试模式是否已初始化 - - # ===== 显示组件 ===== - self.test_display_quad = None # 测试显示的四边形 - self.test_right_quad = None # 右眼显示的四边形(立体模式) - self.stereo_display_created = False # 立体显示是否已创建 - - # ===== HUD组件 ===== - self.test_performance_hud = None # 性能HUD - self.test_performance_text = None # 性能文本节点 - self.hud_update_counter = 0 # HUD更新计数器 - self.hud_update_interval = 30 # HUD更新间隔(帧数), 30帧约0.5秒@60fps - - # ===== 测试模式调试选项 ===== - self.test_mode_submit_texture = False # 是否在测试模式提交纹理到OpenVR - self.test_mode_wait_poses = False # 是否在测试模式调用waitGetPoses - - # ===== 纹理缓存 ===== - self._left_ovr_texture = None # 缓存的左眼OpenVR Texture对象 - self._right_ovr_texture = None # 缓存的右眼OpenVR Texture对象 - - # ========== 测试模式控制方法 ========== - - def enable_vr_test_mode(self, display_mode='stereo'): - """启用VR测试模式 - 将VR渲染直接显示在屏幕上 - - Args: - display_mode: 显示模式 - - 'stereo': 左右眼并排显示 - - 'left': 只显示左眼 - - 'right': 只显示右眼 - """ - if not self.vr_manager.is_vr_available(): - print("❌ VR系统不可用,无法启动测试模式") - return False - - try: - print(f"🧪 启动VR测试模式 - 显示模式: {display_mode}") - - # 初始化VR系统(如果还没初始化) - if not self.vr_manager.vr_initialized: - if not self.vr_manager.initialize_vr(): - print("❌ VR初始化失败") - return False - - # 🔧 关键修复:确保测试模式的纹理资源已初始化 - # 这解决了测试模式纹理提交失败导致的36FPS问题 - print("🔧 检查VR测试模式纹理资源...") - if not self._ensure_test_mode_textures(): - print("❌ VR测试模式纹理资源初始化失败") - return False - - # 设置测试模式 - self.vr_test_mode = True - self.test_display_mode = display_mode - - # 启用VR渲染但不提交给OpenVR - if not self.vr_manager.vr_enabled: - # 启用VR渲染流程(但会在回调中跳过提交) - self.vr_manager.vr_enabled = True - self.vr_manager._disable_main_cam() - - # 设置高帧率用于测试 - if hasattr(self.vr_manager.world, 'qtWidget') and self.vr_manager.world.qtWidget: - if hasattr(self.vr_manager.world.qtWidget, 'synchronizer'): - self.vr_manager.world.qtWidget.synchronizer.setInterval(int(1000/144)) - print("✓ 测试模式:Qt Timer设置为144Hz") - - # 初始化测试显示系统 - if not self._initialize_test_display(): - print("❌ 测试显示系统初始化失败") - return False - - # 创建性能HUD - if not self._initialize_test_performance_hud(): - print("❌ 性能HUD初始化失败") - return False - - # 恢复主相机以查看测试内容 - self.vr_manager._enable_main_cam() - - # 重置HUD更新计数器 - self.hud_update_counter = 0 - - print("✅ VR测试模式已启用") - print(" - VR内容将显示在屏幕上") - print(" - 不会向OpenVR提交纹理") - print(" - 可以准确测量纯渲染性能") - print(f" - 当前显示模式: {display_mode}") - - return True - - except Exception as e: - print(f"❌ 启动VR测试模式失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable_vr_test_mode(self): - """禁用VR测试模式""" - try: - print("🧪 禁用VR测试模式...") - - # 清理测试显示 - self._cleanup_test_display() - - # 清理性能HUD - self._cleanup_test_performance_hud() - - # 关闭测试模式 - self.vr_test_mode = False - self.test_mode_initialized = False - - # 重置HUD更新计数器 - self.hud_update_counter = 0 - - # 恢复正常VR模式或禁用VR - if self.vr_manager.vr_enabled: - print(" 选择: [1] 恢复正常VR模式 [2] 完全禁用VR") - # 这里可以让用户选择,现在默认禁用VR - self.vr_manager.disable_vr() - - print("✅ VR测试模式已禁用") - return True - - except Exception as e: - print(f"❌ 禁用VR测试模式失败: {e}") - return False - - def switch_test_display_mode(self, display_mode): - """切换测试显示模式 - - Args: - display_mode: 'stereo', 'left', 'right' - """ - if not self.vr_test_mode: - print("⚠️ 请先启用VR测试模式") - return False - - if display_mode not in ['stereo', 'left', 'right']: - print(f"⚠️ 无效的显示模式: {display_mode}") - return False - - print(f"🧪 切换显示模式: {self.test_display_mode} → {display_mode}") - - # 如果切换模式,需要清理旧的显示并重置标志位 - if self.test_display_mode != display_mode: - self._cleanup_test_display() - - self.test_display_mode = display_mode - - # 更新显示 - self._update_test_display() - return True - - # ========== 纹理管理方法 ========== - - def _ensure_test_mode_textures(self): - """确保VR测试模式的纹理资源已正确初始化 - - 这解决了测试模式启用纹理提交时的36FPS问题: - - VR测试模式可能跳过了VR渲染缓冲区的初始化 - - submit_texture()需要有效的texture ID和OpenVR Texture对象 - - 如果未初始化会导致提交失败,造成帧率减半 - """ - try: - print(" 检查VR渲染缓冲区...") - - # 检查VR渲染缓冲区是否存在 - buffers_exist = ( - hasattr(self.vr_manager, 'vr_left_eye_buffer') and self.vr_manager.vr_left_eye_buffer and - hasattr(self.vr_manager, 'vr_right_eye_buffer') and self.vr_manager.vr_right_eye_buffer - ) - - if not buffers_exist: - print(" ⚠️ VR渲染缓冲区不存在,正在创建...") - # 🐛 BUG修复: 原代码调用了不存在的 _setup_vr_render_buffers() - # 应该调用 _create_vr_buffers() 或 _create_vr_buffers_with_pipeline() - if hasattr(self.vr_manager.world, 'render_pipeline') and self.vr_manager.world.render_pipeline: - success = self.vr_manager._create_vr_buffers_with_pipeline() - else: - success = self.vr_manager._create_vr_buffers() - - if not success: - print(" ❌ VR渲染缓冲区创建失败") - return False - print(" ✅ VR渲染缓冲区创建成功") - else: - print(" ✅ VR渲染缓冲区已存在") - - # 检查纹理ID是否已缓存 - print(" 检查纹理ID缓存...") - texture_ids_cached = ( - hasattr(self.vr_manager, 'left_texture_id') and self.vr_manager.left_texture_id and self.vr_manager.left_texture_id > 0 and - hasattr(self.vr_manager, 'right_texture_id') and self.vr_manager.right_texture_id and self.vr_manager.right_texture_id > 0 - ) - - if not texture_ids_cached: - print(" ⚠️ 纹理ID未缓存,正在准备纹理...") - if not self.vr_manager._prepare_and_cache_textures(): - print(" ❌ 纹理准备和缓存失败") - return False - print(f" ✅ 纹理ID缓存成功 - 左眼:{self.vr_manager.left_texture_id}, 右眼:{self.vr_manager.right_texture_id}") - else: - print(f" ✅ 纹理ID已缓存 - 左眼:{self.vr_manager.left_texture_id}, 右眼:{self.vr_manager.right_texture_id}") - - # 检查OpenVR Texture对象是否已创建 - print(" 检查OpenVR Texture对象...") - ovr_textures_exist = ( - self._left_ovr_texture and self._right_ovr_texture - ) - - if not ovr_textures_exist: - print(" ⚠️ OpenVR Texture对象未创建,正在创建...") - self._create_cached_ovr_textures() - print(" ✅ OpenVR Texture对象创建成功") - else: - print(" ✅ OpenVR Texture对象已存在") - - print(" ✅ VR测试模式纹理资源检查完成,可安全启用纹理提交") - return True - - except Exception as e: - print(f" ❌ VR测试模式纹理资源检查失败: {e}") - import traceback - traceback.print_exc() - return False - - def _create_cached_ovr_textures(self): - """创建缓存的OpenVR Texture对象 - 避免每帧创建新对象""" - try: - import openvr - self._left_ovr_texture = openvr.Texture_t() - self._right_ovr_texture = openvr.Texture_t() - - # 设置固定属性(这些不变) - self._left_ovr_texture.eType = openvr.TextureType_OpenGL - self._left_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma - self._right_ovr_texture.eType = openvr.TextureType_OpenGL - self._right_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma - - print("✅ OpenVR Texture对象缓存已创建") - except Exception as e: - print(f"⚠️ OpenVR Texture对象创建失败: {e}") - # 不抛出异常,使用备用方案 - - # 注意: _batch_submit_textures() 方法已移至VRManager - # 因为它是VR渲染的核心功能,不仅用于测试模式 - - # ========== 显示系统方法 ========== - - def _initialize_test_display(self): - """初始化测试显示系统""" - try: - print("🔧 正在初始化测试显示系统...") - - # 导入必要的Panda3D组件 - from panda3d.core import CardMaker, PandaNode - - # 创建显示四边形 - cm = CardMaker("vr_test_display") - - if self.test_display_mode == 'stereo': - # 并排显示:左眼在左半屏,右眼在右半屏 - cm.setFrame(-1, 1, -0.5, 0.5) # 全屏 - else: - # 单眼显示:占据整个屏幕 - cm.setFrame(-1, 1, -1, 1) - - # 创建节点 - self.test_display_quad = self.vr_manager.world.render2d.attachNewNode(cm.generate()) - - # 设置初始纹理 - self._update_test_display() - - # 设置渲染状态 - self.test_display_quad.setTransparency(0) # 不透明 - self.test_display_quad.setBin("background", 0) # 背景层 - - self.test_mode_initialized = True - print("✅ 测试显示系统初始化完成") - return True - - except Exception as e: - print(f"❌ 测试显示系统初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def _update_test_display(self): - """更新测试显示内容""" - try: - if not self.test_mode_initialized: - return - - # 根据显示模式设置纹理 - if self.test_display_mode == 'left': - if not self.test_display_quad: - return - if self.vr_manager.vr_left_texture: - self.test_display_quad.setTexture(self.vr_manager.vr_left_texture) - elif self.test_display_mode == 'right': - if not self.test_display_quad: - return - if self.vr_manager.vr_right_texture: - self.test_display_quad.setTexture(self.vr_manager.vr_right_texture) - elif self.test_display_mode == 'stereo': - # 立体显示:只在第一次创建,后续只更新纹理 - if not self.stereo_display_created: - self._create_stereo_display() - else: - # 只更新纹理,不重新创建 - if self.test_display_quad and self.vr_manager.vr_left_texture: - self.test_display_quad.setTexture(self.vr_manager.vr_left_texture) - if self.test_right_quad and self.vr_manager.vr_right_texture: - self.test_right_quad.setTexture(self.vr_manager.vr_right_texture) - - except Exception as e: - print(f"⚠️ 更新测试显示失败: {e}") - - def _create_stereo_display(self): - """创建左右眼并排显示""" - try: - # 如果已经创建,直接返回 - if self.stereo_display_created and self.test_display_quad and self.test_right_quad: - return - - # 移除旧的显示 - if self.test_display_quad: - self.test_display_quad.removeNode() - if self.test_right_quad: - self.test_right_quad.removeNode() - - from panda3d.core import CardMaker - - # 创建左眼显示 - left_cm = CardMaker("vr_test_left") - left_cm.setFrame(-1, 0, -1, 1) # 左半屏 - left_quad = self.vr_manager.world.render2d.attachNewNode(left_cm.generate()) - if self.vr_manager.vr_left_texture: - left_quad.setTexture(self.vr_manager.vr_left_texture) - - # 创建右眼显示 - right_cm = CardMaker("vr_test_right") - right_cm.setFrame(0, 1, -1, 1) # 右半屏 - right_quad = self.vr_manager.world.render2d.attachNewNode(right_cm.generate()) - if self.vr_manager.vr_right_texture: - right_quad.setTexture(self.vr_manager.vr_right_texture) - - # 保存引用 - self.test_display_quad = left_quad # 保存左眼引用 - self.test_right_quad = right_quad # 额外保存右眼引用 - self.stereo_display_created = True # 标记已创建 - - print("✓ 立体显示已创建") - - except Exception as e: - print(f"⚠️ 创建立体显示失败: {e}") - self.stereo_display_created = False - - def _cleanup_test_display(self): - """清理测试显示""" - try: - if self.test_display_quad: - self.test_display_quad.removeNode() - self.test_display_quad = None - - if hasattr(self, 'test_right_quad') and self.test_right_quad: - self.test_right_quad.removeNode() - self.test_right_quad = None - - # 重置立体显示标志位 - self.stereo_display_created = False - - print("✓ 测试显示已清理") - - except Exception as e: - print(f"⚠️ 清理测试显示失败: {e}") - - # ========== HUD系统方法 ========== - - def _initialize_test_performance_hud(self): - """初始化性能HUD""" - try: - print("🔧 正在初始化性能HUD...") - - from direct.gui.OnscreenText import OnscreenText - from panda3d.core import TextNode - - # 创建性能文本显示 - self.test_performance_text = OnscreenText( - text="初始化中...", - pos=(-0.95, 0.9), # 左上角,调整到屏幕内 - scale=0.05, - fg=(1, 1, 0, 1), # 黄色 - align=TextNode.ALeft, - shadow=(0, 0, 0, 0.5), # 黑色阴影 - parent=self.vr_manager.world.render2d - ) - - print("✅ 性能HUD初始化完成") - return True - - except Exception as e: - print(f"❌ 性能HUD初始化失败: {e}") - return False - - def _update_test_performance_hud(self): - """更新性能HUD显示(限制更新频率避免重影)""" - try: - if not self.test_performance_text or not self.vr_test_mode: - return - - # 增加计数器 - self.hud_update_counter += 1 - - # 只在指定间隔更新文本,避免重影 - if self.hud_update_counter < self.hud_update_interval: - return - - # 重置计数器 - self.hud_update_counter = 0 - - # 收集性能数据 - perf_mon = self.vr_manager.performance_monitor - left_render_time = perf_mon.left_render_time if perf_mon else 0 - right_render_time = perf_mon.right_render_time if perf_mon else 0 - total_render_time = left_render_time + right_render_time - - # 计算FPS - current_fps = perf_mon.vr_fps if perf_mon else 0 - - # 获取系统性能 - cpu_usage = perf_mon.cpu_usage if perf_mon else 0 - memory_usage = perf_mon.memory_usage if perf_mon else 0 - gpu_usage = perf_mon.gpu_usage if perf_mon else 0 - - # 构建显示文本 - hud_text = f"""VR TEST MODE - {self.test_display_mode.upper()} - -Render Performance: - FPS: {current_fps:.1f} - Left Eye: {left_render_time:.2f}ms - Right Eye: {right_render_time:.2f}ms - Total: {total_render_time:.2f}ms - -System Performance: - CPU: {cpu_usage:.1f}% - Memory: {memory_usage:.1f}% - GPU: {gpu_usage:.1f}% - -Render Count: - Left Eye: {perf_mon.left_render_count if perf_mon else 0} - Right Eye: {perf_mon.right_render_count if perf_mon else 0} - -Target: <13.9ms@72Hz -Status: {'GOOD' if total_render_time < 10 else 'HIGH' if total_render_time < 16 else 'SLOW'} - -Hotkeys: -F1=Left F2=Right F3=SideBySide -ESC=Exit Test Mode""" - - # 更新文本 - self.test_performance_text.setText(hud_text) - - except Exception as e: - print(f"⚠️ 更新性能HUD失败: {e}") - - def _cleanup_test_performance_hud(self): - """清理性能HUD""" - try: - if self.test_performance_text: - self.test_performance_text.destroy() - self.test_performance_text = None - - print("✓ 性能HUD已清理") - - except Exception as e: - print(f"⚠️ 清理性能HUD失败: {e}") - - # ========== 状态查询方法 ========== - - def get_test_mode_status(self): - """获取测试模式状态""" - if not self.vr_test_mode: - return None - - perf_mon = self.vr_manager.performance_monitor - left_render_time = perf_mon.left_render_time if perf_mon else 0 - right_render_time = perf_mon.right_render_time if perf_mon else 0 - total_render_time = left_render_time + right_render_time - - return { - 'test_mode_enabled': self.vr_test_mode, - 'display_mode': self.test_display_mode, - 'vr_fps': perf_mon.vr_fps if perf_mon else 0, - 'left_render_time': left_render_time, - 'right_render_time': right_render_time, - 'total_render_time': total_render_time, - 'left_render_count': perf_mon.left_render_count if perf_mon else 0, - 'right_render_count': perf_mon.right_render_count if perf_mon else 0, - 'performance_rating': ('excellent' if total_render_time < 10 else - 'good' if total_render_time < 16 else 'poor') - } - - def get_test_mode_features(self): - """获取当前测试模式功能设置""" - return { - 'submit_texture': self.test_mode_submit_texture, - 'wait_poses': self.test_mode_wait_poses, - 'test_mode_active': self.vr_test_mode - } - - def set_test_mode_features(self, submit_texture=None, wait_poses=None): - """设置测试模式功能开关 - 用于渐进式调试 - - Args: - submit_texture: 是否在测试模式启用纹理提交(True/False/None=保持当前) - wait_poses: 是否在测试模式启用waitGetPoses(True/False/None=保持当前) - """ - if submit_texture is not None: - self.test_mode_submit_texture = submit_texture - if wait_poses is not None: - self.test_mode_wait_poses = wait_poses - - print(f"🔧 VR测试模式功能设置:") - print(f" 纹理提交: {'启用' if self.test_mode_submit_texture else '禁用'}") - print(f" 姿态等待: {'启用' if self.test_mode_wait_poses else '禁用'}") - - # 如果当前在测试模式,输出预期效果 - if self.vr_test_mode: - print(f" 当前测试模式已启用,设置将立即生效") - else: - print(f" 设置已保存,将在下次启用测试模式时生效") - - # ========== 性能测试方法 ========== - - def run_vr_performance_test(self, duration_seconds=30, display_mode='stereo'): - """运行VR性能测试 - 简单的测试入口方法 - - Args: - duration_seconds: 测试持续时间(秒) - display_mode: 显示模式 ('stereo', 'left', 'right') - - Returns: - dict: 测试结果统计 - """ - print("🧪 ======= VR性能测试开始 =======") - print(f" 测试模式: {display_mode}") - print(f" 测试时长: {duration_seconds}秒") - print(" 按ESC键提前退出测试") - print(" F1=左眼 F2=右眼 F3=并排显示") - print("=================================") - - # 启动测试模式 - if not self.enable_vr_test_mode(display_mode): - print("❌ VR测试模式启动失败") - return None - - # 记录测试开始状态 - test_start_time = time.time() - perf_mon = self.vr_manager.performance_monitor - start_frame_count = perf_mon.frame_count if perf_mon else 0 - - print("✅ VR测试模式已启动") - print(" - VR内容直接显示在屏幕上") - print(" - 无OpenVR纹理提交开销") - print(" - 实时性能监控已启用") - print(f" - 当前显示: {display_mode.upper()}模式") - print("\n开始性能测试...") - - # 等待测试完成(用户手动停止或超时) - print(f"\n📊 测试将运行 {duration_seconds} 秒") - print(" 实时性能数据显示在屏幕左上角") - print(" 观察帧率和渲染时间变化") - - # 这里可以添加自动停止逻辑,但现在让用户手动控制 - print(f"\n⏰ 请观察 {duration_seconds} 秒后手动调用 disable_vr_test_mode() 停止测试") - print(" 或者随时调用 get_test_mode_status() 查看当前状态") - - return { - 'status': 'running', - 'start_time': test_start_time, - 'start_frame': start_frame_count, - 'display_mode': display_mode, - 'instructions': { - 'stop_test': 'vr_manager.test_mode.disable_vr_test_mode()', - 'check_status': 'vr_manager.test_mode.get_test_mode_status()', - 'switch_mode': 'vr_manager.test_mode.switch_test_display_mode("left/right/stereo")' - } - } +""" +VR测试调试子系统 +负责VR测试模式、HUD显示和性能测试功能 +""" + +import time + + +class VRTestMode: + """VR测试模式系统 + + 功能: + - VR内容直接显示在屏幕上(不提交给OpenVR) + - 实时性能监控HUD + - 支持左眼/右眼/立体三种显示模式 + - 渐进式测试功能(纹理提交、姿态等待) + - 性能测试和调试工具 + """ + + def __init__(self, vr_manager): + """初始化VR测试模式系统 + + Args: + vr_manager: VRManager实例的引用 + """ + self.vr_manager = vr_manager + + # ===== 测试模式状态 ===== + self.vr_test_mode = False # 是否启用VR测试模式 + self.test_display_mode = 'stereo' # 'left', 'right', 'stereo' + self.test_mode_initialized = False # 测试模式是否已初始化 + + # ===== 显示组件 ===== + self.test_display_quad = None # 测试显示的四边形 + self.test_right_quad = None # 右眼显示的四边形(立体模式) + self.stereo_display_created = False # 立体显示是否已创建 + + # ===== HUD组件 ===== + self.test_performance_hud = None # 性能HUD + self.test_performance_text = None # 性能文本节点 + self.hud_update_counter = 0 # HUD更新计数器 + self.hud_update_interval = 30 # HUD更新间隔(帧数), 30帧约0.5秒@60fps + + # ===== 测试模式调试选项 ===== + self.test_mode_submit_texture = False # 是否在测试模式提交纹理到OpenVR + self.test_mode_wait_poses = False # 是否在测试模式调用waitGetPoses + + # ===== 纹理缓存 ===== + self._left_ovr_texture = None # 缓存的左眼OpenVR Texture对象 + self._right_ovr_texture = None # 缓存的右眼OpenVR Texture对象 + + # ========== 测试模式控制方法 ========== + + def enable_vr_test_mode(self, display_mode='stereo'): + """启用VR测试模式 - 将VR渲染直接显示在屏幕上 + + Args: + display_mode: 显示模式 + - 'stereo': 左右眼并排显示 + - 'left': 只显示左眼 + - 'right': 只显示右眼 + """ + if not self.vr_manager.is_vr_available(): + print("❌ VR系统不可用,无法启动测试模式") + return False + + try: + print(f"🧪 启动VR测试模式 - 显示模式: {display_mode}") + + # 初始化VR系统(如果还没初始化) + if not self.vr_manager.vr_initialized: + if not self.vr_manager.initialize_vr(): + print("❌ VR初始化失败") + return False + + # 🔧 关键修复:确保测试模式的纹理资源已初始化 + # 这解决了测试模式纹理提交失败导致的36FPS问题 + print("🔧 检查VR测试模式纹理资源...") + if not self._ensure_test_mode_textures(): + print("❌ VR测试模式纹理资源初始化失败") + return False + + # 设置测试模式 + self.vr_test_mode = True + self.test_display_mode = display_mode + + # 启用VR渲染但不提交给OpenVR + if not self.vr_manager.vr_enabled: + # 启用VR渲染流程(但会在回调中跳过提交) + self.vr_manager.vr_enabled = True + self.vr_manager._disable_main_cam() + + # 设置高帧率用于测试 + if hasattr(self.vr_manager.world, 'qtWidget') and self.vr_manager.world.qtWidget: + if hasattr(self.vr_manager.world.qtWidget, 'synchronizer'): + self.vr_manager.world.qtWidget.synchronizer.setInterval(int(1000/144)) + print("✓ 测试模式:Qt Timer设置为144Hz") + + # 初始化测试显示系统 + if not self._initialize_test_display(): + print("❌ 测试显示系统初始化失败") + return False + + # 创建性能HUD + if not self._initialize_test_performance_hud(): + print("❌ 性能HUD初始化失败") + return False + + # 恢复主相机以查看测试内容 + self.vr_manager._enable_main_cam() + + # 重置HUD更新计数器 + self.hud_update_counter = 0 + + print("✅ VR测试模式已启用") + print(" - VR内容将显示在屏幕上") + print(" - 不会向OpenVR提交纹理") + print(" - 可以准确测量纯渲染性能") + print(f" - 当前显示模式: {display_mode}") + + return True + + except Exception as e: + print(f"❌ 启动VR测试模式失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable_vr_test_mode(self): + """禁用VR测试模式""" + try: + print("🧪 禁用VR测试模式...") + + # 清理测试显示 + self._cleanup_test_display() + + # 清理性能HUD + self._cleanup_test_performance_hud() + + # 关闭测试模式 + self.vr_test_mode = False + self.test_mode_initialized = False + + # 重置HUD更新计数器 + self.hud_update_counter = 0 + + # 恢复正常VR模式或禁用VR + if self.vr_manager.vr_enabled: + print(" 选择: [1] 恢复正常VR模式 [2] 完全禁用VR") + # 这里可以让用户选择,现在默认禁用VR + self.vr_manager.disable_vr() + + print("✅ VR测试模式已禁用") + return True + + except Exception as e: + print(f"❌ 禁用VR测试模式失败: {e}") + return False + + def switch_test_display_mode(self, display_mode): + """切换测试显示模式 + + Args: + display_mode: 'stereo', 'left', 'right' + """ + if not self.vr_test_mode: + print("⚠️ 请先启用VR测试模式") + return False + + if display_mode not in ['stereo', 'left', 'right']: + print(f"⚠️ 无效的显示模式: {display_mode}") + return False + + print(f"🧪 切换显示模式: {self.test_display_mode} → {display_mode}") + + # 如果切换模式,需要清理旧的显示并重置标志位 + if self.test_display_mode != display_mode: + self._cleanup_test_display() + + self.test_display_mode = display_mode + + # 更新显示 + self._update_test_display() + return True + + # ========== 纹理管理方法 ========== + + def _ensure_test_mode_textures(self): + """确保VR测试模式的纹理资源已正确初始化 + + 这解决了测试模式启用纹理提交时的36FPS问题: + - VR测试模式可能跳过了VR渲染缓冲区的初始化 + - submit_texture()需要有效的texture ID和OpenVR Texture对象 + - 如果未初始化会导致提交失败,造成帧率减半 + """ + try: + print(" 检查VR渲染缓冲区...") + + # 检查VR渲染缓冲区是否存在 + buffers_exist = ( + hasattr(self.vr_manager, 'vr_left_eye_buffer') and self.vr_manager.vr_left_eye_buffer and + hasattr(self.vr_manager, 'vr_right_eye_buffer') and self.vr_manager.vr_right_eye_buffer + ) + + if not buffers_exist: + print(" ⚠️ VR渲染缓冲区不存在,正在创建...") + # 🐛 BUG修复: 原代码调用了不存在的 _setup_vr_render_buffers() + # 应该调用 _create_vr_buffers() 或 _create_vr_buffers_with_pipeline() + if hasattr(self.vr_manager.world, 'render_pipeline') and self.vr_manager.world.render_pipeline: + success = self.vr_manager._create_vr_buffers_with_pipeline() + else: + success = self.vr_manager._create_vr_buffers() + + if not success: + print(" ❌ VR渲染缓冲区创建失败") + return False + print(" ✅ VR渲染缓冲区创建成功") + else: + print(" ✅ VR渲染缓冲区已存在") + + # 检查纹理ID是否已缓存 + print(" 检查纹理ID缓存...") + texture_ids_cached = ( + hasattr(self.vr_manager, 'left_texture_id') and self.vr_manager.left_texture_id and self.vr_manager.left_texture_id > 0 and + hasattr(self.vr_manager, 'right_texture_id') and self.vr_manager.right_texture_id and self.vr_manager.right_texture_id > 0 + ) + + if not texture_ids_cached: + print(" ⚠️ 纹理ID未缓存,正在准备纹理...") + if not self.vr_manager._prepare_and_cache_textures(): + print(" ❌ 纹理准备和缓存失败") + return False + print(f" ✅ 纹理ID缓存成功 - 左眼:{self.vr_manager.left_texture_id}, 右眼:{self.vr_manager.right_texture_id}") + else: + print(f" ✅ 纹理ID已缓存 - 左眼:{self.vr_manager.left_texture_id}, 右眼:{self.vr_manager.right_texture_id}") + + # 检查OpenVR Texture对象是否已创建 + print(" 检查OpenVR Texture对象...") + ovr_textures_exist = ( + self._left_ovr_texture and self._right_ovr_texture + ) + + if not ovr_textures_exist: + print(" ⚠️ OpenVR Texture对象未创建,正在创建...") + self._create_cached_ovr_textures() + print(" ✅ OpenVR Texture对象创建成功") + else: + print(" ✅ OpenVR Texture对象已存在") + + print(" ✅ VR测试模式纹理资源检查完成,可安全启用纹理提交") + return True + + except Exception as e: + print(f" ❌ VR测试模式纹理资源检查失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_cached_ovr_textures(self): + """创建缓存的OpenVR Texture对象 - 避免每帧创建新对象""" + try: + import openvr + self._left_ovr_texture = openvr.Texture_t() + self._right_ovr_texture = openvr.Texture_t() + + # 设置固定属性(这些不变) + self._left_ovr_texture.eType = openvr.TextureType_OpenGL + self._left_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + self._right_ovr_texture.eType = openvr.TextureType_OpenGL + self._right_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + + print("✅ OpenVR Texture对象缓存已创建") + except Exception as e: + print(f"⚠️ OpenVR Texture对象创建失败: {e}") + # 不抛出异常,使用备用方案 + + # 注意: _batch_submit_textures() 方法已移至VRManager + # 因为它是VR渲染的核心功能,不仅用于测试模式 + + # ========== 显示系统方法 ========== + + def _initialize_test_display(self): + """初始化测试显示系统""" + try: + print("🔧 正在初始化测试显示系统...") + + # 导入必要的Panda3D组件 + from panda3d.core import CardMaker, PandaNode + + # 创建显示四边形 + cm = CardMaker("vr_test_display") + + if self.test_display_mode == 'stereo': + # 并排显示:左眼在左半屏,右眼在右半屏 + cm.setFrame(-1, 1, -0.5, 0.5) # 全屏 + else: + # 单眼显示:占据整个屏幕 + cm.setFrame(-1, 1, -1, 1) + + # 创建节点 + self.test_display_quad = self.vr_manager.world.render2d.attachNewNode(cm.generate()) + + # 设置初始纹理 + self._update_test_display() + + # 设置渲染状态 + self.test_display_quad.setTransparency(0) # 不透明 + self.test_display_quad.setBin("background", 0) # 背景层 + + self.test_mode_initialized = True + print("✅ 测试显示系统初始化完成") + return True + + except Exception as e: + print(f"❌ 测试显示系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def _update_test_display(self): + """更新测试显示内容""" + try: + if not self.test_mode_initialized: + return + + # 根据显示模式设置纹理 + if self.test_display_mode == 'left': + if not self.test_display_quad: + return + if self.vr_manager.vr_left_texture: + self.test_display_quad.setTexture(self.vr_manager.vr_left_texture) + elif self.test_display_mode == 'right': + if not self.test_display_quad: + return + if self.vr_manager.vr_right_texture: + self.test_display_quad.setTexture(self.vr_manager.vr_right_texture) + elif self.test_display_mode == 'stereo': + # 立体显示:只在第一次创建,后续只更新纹理 + if not self.stereo_display_created: + self._create_stereo_display() + else: + # 只更新纹理,不重新创建 + if self.test_display_quad and self.vr_manager.vr_left_texture: + self.test_display_quad.setTexture(self.vr_manager.vr_left_texture) + if self.test_right_quad and self.vr_manager.vr_right_texture: + self.test_right_quad.setTexture(self.vr_manager.vr_right_texture) + + except Exception as e: + print(f"⚠️ 更新测试显示失败: {e}") + + def _create_stereo_display(self): + """创建左右眼并排显示""" + try: + # 如果已经创建,直接返回 + if self.stereo_display_created and self.test_display_quad and self.test_right_quad: + return + + # 移除旧的显示 + if self.test_display_quad: + self.test_display_quad.removeNode() + if self.test_right_quad: + self.test_right_quad.removeNode() + + from panda3d.core import CardMaker + + # 创建左眼显示 + left_cm = CardMaker("vr_test_left") + left_cm.setFrame(-1, 0, -1, 1) # 左半屏 + left_quad = self.vr_manager.world.render2d.attachNewNode(left_cm.generate()) + if self.vr_manager.vr_left_texture: + left_quad.setTexture(self.vr_manager.vr_left_texture) + + # 创建右眼显示 + right_cm = CardMaker("vr_test_right") + right_cm.setFrame(0, 1, -1, 1) # 右半屏 + right_quad = self.vr_manager.world.render2d.attachNewNode(right_cm.generate()) + if self.vr_manager.vr_right_texture: + right_quad.setTexture(self.vr_manager.vr_right_texture) + + # 保存引用 + self.test_display_quad = left_quad # 保存左眼引用 + self.test_right_quad = right_quad # 额外保存右眼引用 + self.stereo_display_created = True # 标记已创建 + + print("✓ 立体显示已创建") + + except Exception as e: + print(f"⚠️ 创建立体显示失败: {e}") + self.stereo_display_created = False + + def _cleanup_test_display(self): + """清理测试显示""" + try: + if self.test_display_quad: + self.test_display_quad.removeNode() + self.test_display_quad = None + + if hasattr(self, 'test_right_quad') and self.test_right_quad: + self.test_right_quad.removeNode() + self.test_right_quad = None + + # 重置立体显示标志位 + self.stereo_display_created = False + + print("✓ 测试显示已清理") + + except Exception as e: + print(f"⚠️ 清理测试显示失败: {e}") + + # ========== HUD系统方法 ========== + + def _initialize_test_performance_hud(self): + """初始化性能HUD""" + try: + print("🔧 正在初始化性能HUD...") + + from direct.gui.OnscreenText import OnscreenText + from panda3d.core import TextNode + + # 创建性能文本显示 + self.test_performance_text = OnscreenText( + text="初始化中...", + pos=(-0.95, 0.9), # 左上角,调整到屏幕内 + scale=0.05, + fg=(1, 1, 0, 1), # 黄色 + align=TextNode.ALeft, + shadow=(0, 0, 0, 0.5), # 黑色阴影 + parent=self.vr_manager.world.render2d + ) + + print("✅ 性能HUD初始化完成") + return True + + except Exception as e: + print(f"❌ 性能HUD初始化失败: {e}") + return False + + def _update_test_performance_hud(self): + """更新性能HUD显示(限制更新频率避免重影)""" + try: + if not self.test_performance_text or not self.vr_test_mode: + return + + # 增加计数器 + self.hud_update_counter += 1 + + # 只在指定间隔更新文本,避免重影 + if self.hud_update_counter < self.hud_update_interval: + return + + # 重置计数器 + self.hud_update_counter = 0 + + # 收集性能数据 + perf_mon = self.vr_manager.performance_monitor + left_render_time = perf_mon.left_render_time if perf_mon else 0 + right_render_time = perf_mon.right_render_time if perf_mon else 0 + total_render_time = left_render_time + right_render_time + + # 计算FPS + current_fps = perf_mon.vr_fps if perf_mon else 0 + + # 获取系统性能 + cpu_usage = perf_mon.cpu_usage if perf_mon else 0 + memory_usage = perf_mon.memory_usage if perf_mon else 0 + gpu_usage = perf_mon.gpu_usage if perf_mon else 0 + + # 构建显示文本 + hud_text = f"""VR TEST MODE - {self.test_display_mode.upper()} + +Render Performance: + FPS: {current_fps:.1f} + Left Eye: {left_render_time:.2f}ms + Right Eye: {right_render_time:.2f}ms + Total: {total_render_time:.2f}ms + +System Performance: + CPU: {cpu_usage:.1f}% + Memory: {memory_usage:.1f}% + GPU: {gpu_usage:.1f}% + +Render Count: + Left Eye: {perf_mon.left_render_count if perf_mon else 0} + Right Eye: {perf_mon.right_render_count if perf_mon else 0} + +Target: <13.9ms@72Hz +Status: {'GOOD' if total_render_time < 10 else 'HIGH' if total_render_time < 16 else 'SLOW'} + +Hotkeys: +F1=Left F2=Right F3=SideBySide +ESC=Exit Test Mode""" + + # 更新文本 + self.test_performance_text.setText(hud_text) + + except Exception as e: + print(f"⚠️ 更新性能HUD失败: {e}") + + def _cleanup_test_performance_hud(self): + """清理性能HUD""" + try: + if self.test_performance_text: + self.test_performance_text.destroy() + self.test_performance_text = None + + print("✓ 性能HUD已清理") + + except Exception as e: + print(f"⚠️ 清理性能HUD失败: {e}") + + # ========== 状态查询方法 ========== + + def get_test_mode_status(self): + """获取测试模式状态""" + if not self.vr_test_mode: + return None + + perf_mon = self.vr_manager.performance_monitor + left_render_time = perf_mon.left_render_time if perf_mon else 0 + right_render_time = perf_mon.right_render_time if perf_mon else 0 + total_render_time = left_render_time + right_render_time + + return { + 'test_mode_enabled': self.vr_test_mode, + 'display_mode': self.test_display_mode, + 'vr_fps': perf_mon.vr_fps if perf_mon else 0, + 'left_render_time': left_render_time, + 'right_render_time': right_render_time, + 'total_render_time': total_render_time, + 'left_render_count': perf_mon.left_render_count if perf_mon else 0, + 'right_render_count': perf_mon.right_render_count if perf_mon else 0, + 'performance_rating': ('excellent' if total_render_time < 10 else + 'good' if total_render_time < 16 else 'poor') + } + + def get_test_mode_features(self): + """获取当前测试模式功能设置""" + return { + 'submit_texture': self.test_mode_submit_texture, + 'wait_poses': self.test_mode_wait_poses, + 'test_mode_active': self.vr_test_mode + } + + def set_test_mode_features(self, submit_texture=None, wait_poses=None): + """设置测试模式功能开关 - 用于渐进式调试 + + Args: + submit_texture: 是否在测试模式启用纹理提交(True/False/None=保持当前) + wait_poses: 是否在测试模式启用waitGetPoses(True/False/None=保持当前) + """ + if submit_texture is not None: + self.test_mode_submit_texture = submit_texture + if wait_poses is not None: + self.test_mode_wait_poses = wait_poses + + print(f"🔧 VR测试模式功能设置:") + print(f" 纹理提交: {'启用' if self.test_mode_submit_texture else '禁用'}") + print(f" 姿态等待: {'启用' if self.test_mode_wait_poses else '禁用'}") + + # 如果当前在测试模式,输出预期效果 + if self.vr_test_mode: + print(f" 当前测试模式已启用,设置将立即生效") + else: + print(f" 设置已保存,将在下次启用测试模式时生效") + + # ========== 性能测试方法 ========== + + def run_vr_performance_test(self, duration_seconds=30, display_mode='stereo'): + """运行VR性能测试 - 简单的测试入口方法 + + Args: + duration_seconds: 测试持续时间(秒) + display_mode: 显示模式 ('stereo', 'left', 'right') + + Returns: + dict: 测试结果统计 + """ + print("🧪 ======= VR性能测试开始 =======") + print(f" 测试模式: {display_mode}") + print(f" 测试时长: {duration_seconds}秒") + print(" 按ESC键提前退出测试") + print(" F1=左眼 F2=右眼 F3=并排显示") + print("=================================") + + # 启动测试模式 + if not self.enable_vr_test_mode(display_mode): + print("❌ VR测试模式启动失败") + return None + + # 记录测试开始状态 + test_start_time = time.time() + perf_mon = self.vr_manager.performance_monitor + start_frame_count = perf_mon.frame_count if perf_mon else 0 + + print("✅ VR测试模式已启动") + print(" - VR内容直接显示在屏幕上") + print(" - 无OpenVR纹理提交开销") + print(" - 实时性能监控已启用") + print(f" - 当前显示: {display_mode.upper()}模式") + print("\n开始性能测试...") + + # 等待测试完成(用户手动停止或超时) + print(f"\n📊 测试将运行 {duration_seconds} 秒") + print(" 实时性能数据显示在屏幕左上角") + print(" 观察帧率和渲染时间变化") + + # 这里可以添加自动停止逻辑,但现在让用户手动控制 + print(f"\n⏰ 请观察 {duration_seconds} 秒后手动调用 disable_vr_test_mode() 停止测试") + print(" 或者随时调用 get_test_mode_status() 查看当前状态") + + return { + 'status': 'running', + 'start_time': test_start_time, + 'start_frame': start_frame_count, + 'display_mode': display_mode, + 'instructions': { + 'stop_test': 'vr_manager.test_mode.disable_vr_test_mode()', + 'check_status': 'vr_manager.test_mode.get_test_mode_status()', + 'switch_mode': 'vr_manager.test_mode.switch_test_display_mode("left/right/stereo")' + } + } diff --git a/core/vr/tracking/__init__.py b/core/vr/tracking/__init__.py index 8c70c1be..233e33cb 100644 --- a/core/vr/tracking/__init__.py +++ b/core/vr/tracking/__init__.py @@ -1,11 +1,11 @@ -""" -VR跟踪子系统 - -负责VR设备的跟踪和姿态管理: -- HMD和控制器姿态跟踪 -- OpenVR姿态数据处理 -- 设备检测和管理 -- 控制器类和输入包装 -""" - -__all__ = [] +""" +VR跟踪子系统 + +负责VR设备的跟踪和姿态管理: +- HMD和控制器姿态跟踪 +- OpenVR姿态数据处理 +- 设备检测和管理 +- 控制器类和输入包装 +""" + +__all__ = [] diff --git a/core/vr/tracking/controllers.py b/core/vr/tracking/controllers.py index 31dcc849..17250788 100644 --- a/core/vr/tracking/controllers.py +++ b/core/vr/tracking/controllers.py @@ -1,483 +1,483 @@ -""" -VR手柄管理模块 - -基于panda3d-openvr参考实现,提供完整的VR手柄追踪和交互功能: -- 手柄位置和姿态追踪 -- 按钮和触摸板输入处理 -- 手柄可视化和射线显示 -- 震动反馈支持 -""" - -from panda3d.core import ( - NodePath, PandaNode, Vec3, Mat4, LVector3, LMatrix4, - GeomNode, LineSegs, CardMaker, Texture, RenderState, - TransparencyAttrib, ColorAttrib, Vec4 -) -from direct.actor.Actor import Actor -from direct.showbase.DirectObject import DirectObject - -try: - import openvr - OPENVR_AVAILABLE = True -except ImportError: - OPENVR_AVAILABLE = False - -# 导入可视化器 -from core.vr.visualization.controllers import VRControllerVisualizer - - -class VRController(DirectObject): - """VR手柄基类 - 管理单个手柄的追踪和交互""" - - def __init__(self, vr_manager, name, hand_path, device_index=None): - """初始化VR手柄 - - Args: - vr_manager: VR管理器实例 - name: 手柄名称 ('left' 或 'right') - hand_path: OpenVR手部路径 ('/user/hand/left' 或 '/user/hand/right') - device_index: OpenVR设备索引(可选) - """ - super().__init__() - - self.vr_manager = vr_manager - self.name = name - self.hand_path = hand_path - self.device_index = device_index - - # 手柄状态 - self.is_connected = False - self.is_pose_valid = False - self.pose = Mat4.identMat() - self.velocity = Vec3(0, 0, 0) - self.angular_velocity = Vec3(0, 0, 0) - - # 按钮状态 - self.button_states = {} - self.previous_button_states = {} - self.trigger_value = 0.0 - self.grip_value = 0.0 - self.touchpad_pos = Vec3(0, 0, 0) - self.touchpad_touched = False - - # 摇杆状态 - 用于传送和转向交互 - self.joystick_pos = Vec3(0, 0, 0) # 摇杆位置 (x, y, 0) - self.joystick_touched = False # 摇杆是否被触摸 - self.joystick_pressed = False # 摇杆是否被按下 - self.previous_joystick_pos = Vec3(0, 0, 0) # 上一帧摇杆位置 - - # 3D节点和可视化 - self.anchor_node = None - self.visualizer = None - self.ray_length = 10.0 - - # 初始化 - self._create_anchor() - self._create_visualizer() - - print(f"✓ {name}手柄控制器初始化完成") - - def _create_anchor(self): - """创建手柄锚点节点 - 直接挂在render下,手动管理世界坐标""" - if hasattr(self.vr_manager, 'world') and self.vr_manager.world: - self.anchor_node = self.vr_manager.world.render.attachNewNode(f'{self.name}-controller') - self.anchor_node.hide() # 初始隐藏,直到获得有效姿态 - - def _create_visualizer(self): - """创建手柄可视化器""" - if self.anchor_node and hasattr(self.vr_manager, 'world'): - self.visualizer = VRControllerVisualizer(self, self.vr_manager.world.render) - elif self.anchor_node: - # 如果没有世界对象,使用基础渲染节点 - from panda3d.core import NodePath - render = NodePath('render') - self.visualizer = VRControllerVisualizer(self, render) - - def set_device_index(self, device_index): - """设置OpenVR设备索引""" - self.device_index = device_index - self.is_connected = True - print(f"📱 {self.name}手柄连接 (设备索引: {device_index})") - - def update_pose(self, pose_data): - """更新手柄姿态 - - Args: - pose_data: OpenVR TrackedDevicePose_t数据 - """ - if not pose_data.bPoseIsValid: - self.is_pose_valid = False - if self.anchor_node: - self.anchor_node.hide() - return - - self.is_pose_valid = True - - # 转换OpenVR矩阵到Panda3D - if hasattr(self.vr_manager, 'convert_mat') and hasattr(self.vr_manager, 'coord_mat_inv') and hasattr(self.vr_manager, 'coord_mat'): - modelview = self.vr_manager.convert_mat(pose_data.mDeviceToAbsoluteTracking) - self.pose = self.vr_manager.coord_mat_inv * modelview * self.vr_manager.coord_mat - else: - # 直接使用矩阵数据 - m = pose_data.mDeviceToAbsoluteTracking.m - self.pose = LMatrix4( - m[0][0], m[1][0], m[2][0], m[3][0], - m[0][1], m[1][1], m[2][1], m[3][1], - m[0][2], m[1][2], m[2][2], m[3][2], - m[0][3], m[1][3], m[2][3], m[3][3] - ) - - # 更新锚点变换 - 简化方案:直接计算世界坐标 - if self.anchor_node: - # 从OpenVR姿态矩阵提取位置和旋转 - openvr_pos = Vec3( - self.pose.getRow3(3)[0], - self.pose.getRow3(3)[1], - self.pose.getRow3(3)[2] - ) - - # 获取tracking_space的世界位置(传送偏移) - tracking_offset = Vec3(0, 0, 0) - if self.vr_manager.tracking_space: - tracking_offset = self.vr_manager.tracking_space.getPos(self.vr_manager.world.render) - - # 计算手柄的世界位置 = tracking_space偏移 + OpenVR相对位置 - world_pos = tracking_offset + openvr_pos - - # 设置世界位置 - self.anchor_node.setPos(self.vr_manager.world.render, world_pos) - - # 提取并设置旋转(使用四元数避免矩阵问题) - from panda3d.core import LQuaternion - quat = LQuaternion() - quat.setFromMatrix(self.pose) - self.anchor_node.setQuat(self.vr_manager.world.render, quat) - - self.anchor_node.show() - - # 更新可视化 - if self.visualizer: - self.visualizer.update() - - # 更新速度信息 - vel = pose_data.vVelocity - self.velocity = Vec3(vel[0], vel[1], vel[2]) - - ang_vel = pose_data.vAngularVelocity - self.angular_velocity = Vec3(ang_vel[0], ang_vel[1], ang_vel[2]) - - def update_input_state(self, vr_system): - """更新输入状态 - - Args: - vr_system: OpenVR系统实例 - """ - if not self.is_connected or not OPENVR_AVAILABLE or not vr_system: - return - - # 保存上一帧的按钮状态和摇杆位置 - self.previous_button_states = self.button_states.copy() - self.previous_joystick_pos = Vec3(self.joystick_pos) - - # 获取控制器状态 - try: - result, state = vr_system.getControllerState(self.device_index) - if result: - # 更新按钮状态 - 使用正确的OpenVR属性名 - for i in range(openvr.k_EButton_Max): - button_mask = 1 << i - # OpenVR Python绑定中使用ulButtonPressed而不是rButtonPressed - self.button_states[i] = (state.ulButtonPressed & button_mask) != 0 - - # 更新轴状态(扳机、握把、触摸板、摇杆) - # 兼容不同版本的OpenVR Python绑定 - axis_data = None - if hasattr(state, 'rAxis'): - axis_data = state.rAxis # 旧版本使用rAxis - elif hasattr(state, 'vAxis'): - axis_data = state.vAxis # 新版本使用vAxis - - if axis_data is not None and len(axis_data) > 0: - # 调试输出 - 显示所有轴数据(仅当有变化时) - self._debug_axis_data(axis_data) - - # 扳机轴通常在axis[1].x - if len(axis_data) > 1: - self.trigger_value = axis_data[1].x - - # 触摸板/摇杆轴通常在axis[0] - if len(axis_data) > 0: - self.touchpad_pos = Vec3(axis_data[0].x, axis_data[0].y, 0) - # 摇杆和触摸板通常使用同一个轴,但可以区分设备类型 - self.joystick_pos = Vec3(axis_data[0].x, axis_data[0].y, 0) - - # 额外检查其他轴(某些控制器可能将摇杆分配到不同轴) - if len(axis_data) > 2: - # 有些控制器可能在axis[2]有摇杆数据 - axis2_magnitude = abs(axis_data[2].x) + abs(axis_data[2].y) - if axis2_magnitude > 0.1: # 如果有显著输入,使用这个轴作为摇杆 - self.joystick_pos = Vec3(axis_data[2].x, axis_data[2].y, 0) - - # 检查axis[3]和axis[4](Quest控制器可能使用这些轴) - for axis_idx in range(3, min(len(axis_data), 5)): - axis_magnitude = abs(axis_data[axis_idx].x) + abs(axis_data[axis_idx].y) - if axis_magnitude > 0.1: # 如果有显著输入 - # 覆盖之前的摇杆数据,使用最有活动的轴 - self.joystick_pos = Vec3(axis_data[axis_idx].x, axis_data[axis_idx].y, 0) - # 调试输出 - if not hasattr(self, '_last_axis_notify'): - self._last_axis_notify = {} - if self._last_axis_notify.get(axis_idx, 0) == 0: - print(f"🎮 {self.name}手检测到axis[{axis_idx}]活动: ({axis_data[axis_idx].x:.3f}, {axis_data[axis_idx].y:.3f})") - self._last_axis_notify[axis_idx] = 60 # 60帧后再次提醒 - else: - self._last_axis_notify[axis_idx] -= 1 - - # 触摸板和摇杆触摸状态 - self.touchpad_touched = (state.ulButtonTouched & (1 << openvr.k_EButton_SteamVR_Touchpad)) != 0 - - # 摇杆触摸状态(检查多个可能的按钮) - joystick_touch_mask = 0 - if hasattr(openvr, 'k_EButton_Joystick'): - joystick_touch_mask |= (1 << openvr.k_EButton_Joystick) - if hasattr(openvr, 'k_EButton_Thumbstick'): - joystick_touch_mask |= (1 << openvr.k_EButton_Thumbstick) - - self.joystick_touched = (state.ulButtonTouched & joystick_touch_mask) != 0 or self.touchpad_touched - - # 摇杆按下状态 - joystick_press_mask = 0 - if hasattr(openvr, 'k_EButton_Joystick'): - joystick_press_mask |= (1 << openvr.k_EButton_Joystick) - if hasattr(openvr, 'k_EButton_Thumbstick'): - joystick_press_mask |= (1 << openvr.k_EButton_Thumbstick) - - self.joystick_pressed = (state.ulButtonPressed & joystick_press_mask) != 0 - - except Exception as e: - # 减少错误输出频率 - if not hasattr(self, '_last_input_error_frame'): - self._last_input_error_frame = 0 - - # 获取当前帧数(通过VR管理器) - current_frame = getattr(self.vr_manager, 'frame_count', 0) - - # 每5秒最多输出一次错误(300帧@60fps) - if current_frame - self._last_input_error_frame > 300: - print(f"⚠️ 更新{self.name}手柄输入状态失败: {e}") - self._last_input_error_frame = current_frame - - def is_button_pressed(self, button_id): - """检查按钮是否被按下""" - return self.button_states.get(button_id, False) - - def is_button_just_pressed(self, button_id): - """检查按钮是否刚刚被按下(上升沿)""" - current = self.button_states.get(button_id, False) - previous = self.previous_button_states.get(button_id, False) - return current and not previous - - def is_button_just_released(self, button_id): - """检查按钮是否刚刚被释放(下降沿)""" - current = self.button_states.get(button_id, False) - previous = self.previous_button_states.get(button_id, False) - return not current and previous - - def is_trigger_pressed(self, threshold=0.1): - """检查扳机是否被按下""" - return self.trigger_value > threshold - - def is_grip_pressed(self, threshold=0.1): - """检查握把是否被按下""" - return self.grip_value > threshold - - def show_ray(self, show=True): - """显示或隐藏交互射线""" - if self.visualizer: - if show: - self.visualizer.show_ray() - else: - self.visualizer.hide_ray() - - def set_ray_color(self, color): - """设置射线颜色""" - if self.visualizer and len(color) >= 3: - from panda3d.core import Vec4 - color_vec = Vec4(color[0], color[1], color[2], color[3] if len(color) > 3 else 1.0) - self.visualizer.set_ray_color(color_vec) - - def trigger_haptic_feedback(self, duration=0.001, strength=1.0): - """触发震动反馈 - - Args: - duration: 震动持续时间(秒) - strength: 震动强度 (0.0-1.0) - """ - if not self.is_connected or not OPENVR_AVAILABLE: - return - - try: - if hasattr(self.vr_manager, 'vr_system') and self.vr_manager.vr_system: - # OpenVR的震动API - duration_microseconds = int(duration * 1000000) - self.vr_manager.vr_system.triggerHapticPulse( - self.device_index, - 0, # axis ID (通常为0) - int(strength * 3999) # 强度 (0-3999) - ) - except Exception as e: - print(f"⚠️ {self.name}手柄震动反馈失败: {e}") - - def get_world_position(self): - """获取手柄在世界坐标系中的位置""" - if self.anchor_node: - return self.anchor_node.getPos(self.vr_manager.world.render) - return Vec3(0, 0, 0) - - def get_world_rotation(self): - """获取手柄在世界坐标系中的旋转""" - if self.anchor_node: - return self.anchor_node.getHpr(self.vr_manager.world.render) - return Vec3(0, 0, 0) - - def get_forward_direction(self): - """获取手柄指向的方向向量(包含视角转向)""" - if self.anchor_node: - # 获取相对于世界坐标系的方向,包含tracking_space的旋转 - if hasattr(self.vr_manager, 'world') and self.vr_manager.world: - # 使用世界变换,包含所有父节点的旋转 - world_transform = self.anchor_node.getMat(self.vr_manager.world.render) - forward = Vec3(world_transform.getRow3(1)) # Y轴 = 前方 - else: - # 备选:使用局部变换 - forward = Vec3(self.anchor_node.getMat().getRow3(1)) - - if forward.length() > 0: - return forward.normalized() - return Vec3(0, 1, 0) - - def is_joystick_touched(self): - """检查摇杆是否被触摸""" - return self.joystick_touched - - def is_joystick_pressed(self): - """检查摇杆是否被按下""" - return self.joystick_pressed - - def get_joystick_position(self): - """获取摇杆位置 - - Returns: - Vec3: 摇杆位置 (x, y, 0),范围 [-1, 1] - """ - return Vec3(self.joystick_pos) - - def get_joystick_delta(self): - """获取摇杆位置变化 - - Returns: - Vec3: 摇杆位置变化向量 - """ - return self.joystick_pos - self.previous_joystick_pos - - def is_joystick_moved(self, threshold=0.01): - """检查摇杆是否移动 - - Args: - threshold: 移动阈值 - - Returns: - bool: 是否移动 - """ - delta = self.get_joystick_delta() - return delta.length() > threshold - - def _debug_axis_data(self, axis_data): - """调试输出轴数据""" - try: - # 只在有活动时输出调试信息 - has_activity = False - active_axes = [] - - for i, axis in enumerate(axis_data): - magnitude = abs(axis.x) + abs(axis.y) - if magnitude > 0.01: # 检测到活动 - has_activity = True - active_axes.append(f"axis[{i}]: ({axis.x:.3f}, {axis.y:.3f})") - - if has_activity: - # 初始化调试计数器 - if not hasattr(self, '_debug_axis_counter'): - self._debug_axis_counter = 0 - - self._debug_axis_counter += 1 - - # 每30帧输出一次详细信息 - if self._debug_axis_counter % 30 == 1: - print(f"🔍 {self.name}手轴数据调试:") - print(f" 总轴数: {len(axis_data)}") - print(f" 活跃轴: {', '.join(active_axes)}") - - # 显示所有轴的当前值(不管是否活跃) - all_axes = [] - for i, axis in enumerate(axis_data): - all_axes.append(f"[{i}]:({axis.x:.3f},{axis.y:.3f})") - print(f" 所有轴: {' '.join(all_axes)}") - - except Exception as e: - print(f"⚠️ 轴数据调试失败: {e}") - - def recreate_visualizer(self): - """重新创建visualizer - 用于渲染模式切换后刷新 - - 当VR渲染模式在运行时改变时调用此方法,以确保visualizer - 使用正确的渲染设置(普通模式 vs RenderPipeline模式) - """ - # 清理旧visualizer - if self.visualizer: - try: - self.visualizer.cleanup() - self.visualizer = None - print(f"🧹 {self.name}手柄visualizer已清理") - except Exception as e: - print(f"⚠️ 清理{self.name}手柄visualizer失败: {e}") - - # 重新创建visualizer - if self.anchor_node: - self._create_visualizer() - if self.visualizer: - print(f"✨ {self.name}手柄visualizer已重建(使用当前渲染模式)") - return True - else: - print(f"❌ {self.name}手柄visualizer重建失败") - return False - else: - print(f"⚠️ {self.name}手柄anchor_node不存在,无法重建visualizer") - return False - - def cleanup(self): - """清理资源""" - self.ignoreAll() - - if self.visualizer: - self.visualizer.cleanup() - - if self.anchor_node: - self.anchor_node.removeNode() - - self.is_connected = False - print(f"🧹 {self.name}手柄控制器已清理") - - -class LeftController(VRController): - """左手控制器""" - - def __init__(self, vr_manager): - super().__init__(vr_manager, 'left', '/user/hand/left') - - -class RightController(VRController): - """右手控制器""" - - def __init__(self, vr_manager): +""" +VR手柄管理模块 + +基于panda3d-openvr参考实现,提供完整的VR手柄追踪和交互功能: +- 手柄位置和姿态追踪 +- 按钮和触摸板输入处理 +- 手柄可视化和射线显示 +- 震动反馈支持 +""" + +from panda3d.core import ( + NodePath, PandaNode, Vec3, Mat4, LVector3, LMatrix4, + GeomNode, LineSegs, CardMaker, Texture, RenderState, + TransparencyAttrib, ColorAttrib, Vec4 +) +from direct.actor.Actor import Actor +from direct.showbase.DirectObject import DirectObject + +try: + import openvr + OPENVR_AVAILABLE = True +except ImportError: + OPENVR_AVAILABLE = False + +# 导入可视化器 +from core.vr.visualization.controllers import VRControllerVisualizer + + +class VRController(DirectObject): + """VR手柄基类 - 管理单个手柄的追踪和交互""" + + def __init__(self, vr_manager, name, hand_path, device_index=None): + """初始化VR手柄 + + Args: + vr_manager: VR管理器实例 + name: 手柄名称 ('left' 或 'right') + hand_path: OpenVR手部路径 ('/user/hand/left' 或 '/user/hand/right') + device_index: OpenVR设备索引(可选) + """ + super().__init__() + + self.vr_manager = vr_manager + self.name = name + self.hand_path = hand_path + self.device_index = device_index + + # 手柄状态 + self.is_connected = False + self.is_pose_valid = False + self.pose = Mat4.identMat() + self.velocity = Vec3(0, 0, 0) + self.angular_velocity = Vec3(0, 0, 0) + + # 按钮状态 + self.button_states = {} + self.previous_button_states = {} + self.trigger_value = 0.0 + self.grip_value = 0.0 + self.touchpad_pos = Vec3(0, 0, 0) + self.touchpad_touched = False + + # 摇杆状态 - 用于传送和转向交互 + self.joystick_pos = Vec3(0, 0, 0) # 摇杆位置 (x, y, 0) + self.joystick_touched = False # 摇杆是否被触摸 + self.joystick_pressed = False # 摇杆是否被按下 + self.previous_joystick_pos = Vec3(0, 0, 0) # 上一帧摇杆位置 + + # 3D节点和可视化 + self.anchor_node = None + self.visualizer = None + self.ray_length = 10.0 + + # 初始化 + self._create_anchor() + self._create_visualizer() + + print(f"✓ {name}手柄控制器初始化完成") + + def _create_anchor(self): + """创建手柄锚点节点 - 直接挂在render下,手动管理世界坐标""" + if hasattr(self.vr_manager, 'world') and self.vr_manager.world: + self.anchor_node = self.vr_manager.world.render.attachNewNode(f'{self.name}-controller') + self.anchor_node.hide() # 初始隐藏,直到获得有效姿态 + + def _create_visualizer(self): + """创建手柄可视化器""" + if self.anchor_node and hasattr(self.vr_manager, 'world'): + self.visualizer = VRControllerVisualizer(self, self.vr_manager.world.render) + elif self.anchor_node: + # 如果没有世界对象,使用基础渲染节点 + from panda3d.core import NodePath + render = NodePath('render') + self.visualizer = VRControllerVisualizer(self, render) + + def set_device_index(self, device_index): + """设置OpenVR设备索引""" + self.device_index = device_index + self.is_connected = True + print(f"📱 {self.name}手柄连接 (设备索引: {device_index})") + + def update_pose(self, pose_data): + """更新手柄姿态 + + Args: + pose_data: OpenVR TrackedDevicePose_t数据 + """ + if not pose_data.bPoseIsValid: + self.is_pose_valid = False + if self.anchor_node: + self.anchor_node.hide() + return + + self.is_pose_valid = True + + # 转换OpenVR矩阵到Panda3D + if hasattr(self.vr_manager, 'convert_mat') and hasattr(self.vr_manager, 'coord_mat_inv') and hasattr(self.vr_manager, 'coord_mat'): + modelview = self.vr_manager.convert_mat(pose_data.mDeviceToAbsoluteTracking) + self.pose = self.vr_manager.coord_mat_inv * modelview * self.vr_manager.coord_mat + else: + # 直接使用矩阵数据 + m = pose_data.mDeviceToAbsoluteTracking.m + self.pose = LMatrix4( + m[0][0], m[1][0], m[2][0], m[3][0], + m[0][1], m[1][1], m[2][1], m[3][1], + m[0][2], m[1][2], m[2][2], m[3][2], + m[0][3], m[1][3], m[2][3], m[3][3] + ) + + # 更新锚点变换 - 简化方案:直接计算世界坐标 + if self.anchor_node: + # 从OpenVR姿态矩阵提取位置和旋转 + openvr_pos = Vec3( + self.pose.getRow3(3)[0], + self.pose.getRow3(3)[1], + self.pose.getRow3(3)[2] + ) + + # 获取tracking_space的世界位置(传送偏移) + tracking_offset = Vec3(0, 0, 0) + if self.vr_manager.tracking_space: + tracking_offset = self.vr_manager.tracking_space.getPos(self.vr_manager.world.render) + + # 计算手柄的世界位置 = tracking_space偏移 + OpenVR相对位置 + world_pos = tracking_offset + openvr_pos + + # 设置世界位置 + self.anchor_node.setPos(self.vr_manager.world.render, world_pos) + + # 提取并设置旋转(使用四元数避免矩阵问题) + from panda3d.core import LQuaternion + quat = LQuaternion() + quat.setFromMatrix(self.pose) + self.anchor_node.setQuat(self.vr_manager.world.render, quat) + + self.anchor_node.show() + + # 更新可视化 + if self.visualizer: + self.visualizer.update() + + # 更新速度信息 + vel = pose_data.vVelocity + self.velocity = Vec3(vel[0], vel[1], vel[2]) + + ang_vel = pose_data.vAngularVelocity + self.angular_velocity = Vec3(ang_vel[0], ang_vel[1], ang_vel[2]) + + def update_input_state(self, vr_system): + """更新输入状态 + + Args: + vr_system: OpenVR系统实例 + """ + if not self.is_connected or not OPENVR_AVAILABLE or not vr_system: + return + + # 保存上一帧的按钮状态和摇杆位置 + self.previous_button_states = self.button_states.copy() + self.previous_joystick_pos = Vec3(self.joystick_pos) + + # 获取控制器状态 + try: + result, state = vr_system.getControllerState(self.device_index) + if result: + # 更新按钮状态 - 使用正确的OpenVR属性名 + for i in range(openvr.k_EButton_Max): + button_mask = 1 << i + # OpenVR Python绑定中使用ulButtonPressed而不是rButtonPressed + self.button_states[i] = (state.ulButtonPressed & button_mask) != 0 + + # 更新轴状态(扳机、握把、触摸板、摇杆) + # 兼容不同版本的OpenVR Python绑定 + axis_data = None + if hasattr(state, 'rAxis'): + axis_data = state.rAxis # 旧版本使用rAxis + elif hasattr(state, 'vAxis'): + axis_data = state.vAxis # 新版本使用vAxis + + if axis_data is not None and len(axis_data) > 0: + # 调试输出 - 显示所有轴数据(仅当有变化时) + self._debug_axis_data(axis_data) + + # 扳机轴通常在axis[1].x + if len(axis_data) > 1: + self.trigger_value = axis_data[1].x + + # 触摸板/摇杆轴通常在axis[0] + if len(axis_data) > 0: + self.touchpad_pos = Vec3(axis_data[0].x, axis_data[0].y, 0) + # 摇杆和触摸板通常使用同一个轴,但可以区分设备类型 + self.joystick_pos = Vec3(axis_data[0].x, axis_data[0].y, 0) + + # 额外检查其他轴(某些控制器可能将摇杆分配到不同轴) + if len(axis_data) > 2: + # 有些控制器可能在axis[2]有摇杆数据 + axis2_magnitude = abs(axis_data[2].x) + abs(axis_data[2].y) + if axis2_magnitude > 0.1: # 如果有显著输入,使用这个轴作为摇杆 + self.joystick_pos = Vec3(axis_data[2].x, axis_data[2].y, 0) + + # 检查axis[3]和axis[4](Quest控制器可能使用这些轴) + for axis_idx in range(3, min(len(axis_data), 5)): + axis_magnitude = abs(axis_data[axis_idx].x) + abs(axis_data[axis_idx].y) + if axis_magnitude > 0.1: # 如果有显著输入 + # 覆盖之前的摇杆数据,使用最有活动的轴 + self.joystick_pos = Vec3(axis_data[axis_idx].x, axis_data[axis_idx].y, 0) + # 调试输出 + if not hasattr(self, '_last_axis_notify'): + self._last_axis_notify = {} + if self._last_axis_notify.get(axis_idx, 0) == 0: + print(f"🎮 {self.name}手检测到axis[{axis_idx}]活动: ({axis_data[axis_idx].x:.3f}, {axis_data[axis_idx].y:.3f})") + self._last_axis_notify[axis_idx] = 60 # 60帧后再次提醒 + else: + self._last_axis_notify[axis_idx] -= 1 + + # 触摸板和摇杆触摸状态 + self.touchpad_touched = (state.ulButtonTouched & (1 << openvr.k_EButton_SteamVR_Touchpad)) != 0 + + # 摇杆触摸状态(检查多个可能的按钮) + joystick_touch_mask = 0 + if hasattr(openvr, 'k_EButton_Joystick'): + joystick_touch_mask |= (1 << openvr.k_EButton_Joystick) + if hasattr(openvr, 'k_EButton_Thumbstick'): + joystick_touch_mask |= (1 << openvr.k_EButton_Thumbstick) + + self.joystick_touched = (state.ulButtonTouched & joystick_touch_mask) != 0 or self.touchpad_touched + + # 摇杆按下状态 + joystick_press_mask = 0 + if hasattr(openvr, 'k_EButton_Joystick'): + joystick_press_mask |= (1 << openvr.k_EButton_Joystick) + if hasattr(openvr, 'k_EButton_Thumbstick'): + joystick_press_mask |= (1 << openvr.k_EButton_Thumbstick) + + self.joystick_pressed = (state.ulButtonPressed & joystick_press_mask) != 0 + + except Exception as e: + # 减少错误输出频率 + if not hasattr(self, '_last_input_error_frame'): + self._last_input_error_frame = 0 + + # 获取当前帧数(通过VR管理器) + current_frame = getattr(self.vr_manager, 'frame_count', 0) + + # 每5秒最多输出一次错误(300帧@60fps) + if current_frame - self._last_input_error_frame > 300: + print(f"⚠️ 更新{self.name}手柄输入状态失败: {e}") + self._last_input_error_frame = current_frame + + def is_button_pressed(self, button_id): + """检查按钮是否被按下""" + return self.button_states.get(button_id, False) + + def is_button_just_pressed(self, button_id): + """检查按钮是否刚刚被按下(上升沿)""" + current = self.button_states.get(button_id, False) + previous = self.previous_button_states.get(button_id, False) + return current and not previous + + def is_button_just_released(self, button_id): + """检查按钮是否刚刚被释放(下降沿)""" + current = self.button_states.get(button_id, False) + previous = self.previous_button_states.get(button_id, False) + return not current and previous + + def is_trigger_pressed(self, threshold=0.1): + """检查扳机是否被按下""" + return self.trigger_value > threshold + + def is_grip_pressed(self, threshold=0.1): + """检查握把是否被按下""" + return self.grip_value > threshold + + def show_ray(self, show=True): + """显示或隐藏交互射线""" + if self.visualizer: + if show: + self.visualizer.show_ray() + else: + self.visualizer.hide_ray() + + def set_ray_color(self, color): + """设置射线颜色""" + if self.visualizer and len(color) >= 3: + from panda3d.core import Vec4 + color_vec = Vec4(color[0], color[1], color[2], color[3] if len(color) > 3 else 1.0) + self.visualizer.set_ray_color(color_vec) + + def trigger_haptic_feedback(self, duration=0.001, strength=1.0): + """触发震动反馈 + + Args: + duration: 震动持续时间(秒) + strength: 震动强度 (0.0-1.0) + """ + if not self.is_connected or not OPENVR_AVAILABLE: + return + + try: + if hasattr(self.vr_manager, 'vr_system') and self.vr_manager.vr_system: + # OpenVR的震动API + duration_microseconds = int(duration * 1000000) + self.vr_manager.vr_system.triggerHapticPulse( + self.device_index, + 0, # axis ID (通常为0) + int(strength * 3999) # 强度 (0-3999) + ) + except Exception as e: + print(f"⚠️ {self.name}手柄震动反馈失败: {e}") + + def get_world_position(self): + """获取手柄在世界坐标系中的位置""" + if self.anchor_node: + return self.anchor_node.getPos(self.vr_manager.world.render) + return Vec3(0, 0, 0) + + def get_world_rotation(self): + """获取手柄在世界坐标系中的旋转""" + if self.anchor_node: + return self.anchor_node.getHpr(self.vr_manager.world.render) + return Vec3(0, 0, 0) + + def get_forward_direction(self): + """获取手柄指向的方向向量(包含视角转向)""" + if self.anchor_node: + # 获取相对于世界坐标系的方向,包含tracking_space的旋转 + if hasattr(self.vr_manager, 'world') and self.vr_manager.world: + # 使用世界变换,包含所有父节点的旋转 + world_transform = self.anchor_node.getMat(self.vr_manager.world.render) + forward = Vec3(world_transform.getRow3(1)) # Y轴 = 前方 + else: + # 备选:使用局部变换 + forward = Vec3(self.anchor_node.getMat().getRow3(1)) + + if forward.length() > 0: + return forward.normalized() + return Vec3(0, 1, 0) + + def is_joystick_touched(self): + """检查摇杆是否被触摸""" + return self.joystick_touched + + def is_joystick_pressed(self): + """检查摇杆是否被按下""" + return self.joystick_pressed + + def get_joystick_position(self): + """获取摇杆位置 + + Returns: + Vec3: 摇杆位置 (x, y, 0),范围 [-1, 1] + """ + return Vec3(self.joystick_pos) + + def get_joystick_delta(self): + """获取摇杆位置变化 + + Returns: + Vec3: 摇杆位置变化向量 + """ + return self.joystick_pos - self.previous_joystick_pos + + def is_joystick_moved(self, threshold=0.01): + """检查摇杆是否移动 + + Args: + threshold: 移动阈值 + + Returns: + bool: 是否移动 + """ + delta = self.get_joystick_delta() + return delta.length() > threshold + + def _debug_axis_data(self, axis_data): + """调试输出轴数据""" + try: + # 只在有活动时输出调试信息 + has_activity = False + active_axes = [] + + for i, axis in enumerate(axis_data): + magnitude = abs(axis.x) + abs(axis.y) + if magnitude > 0.01: # 检测到活动 + has_activity = True + active_axes.append(f"axis[{i}]: ({axis.x:.3f}, {axis.y:.3f})") + + if has_activity: + # 初始化调试计数器 + if not hasattr(self, '_debug_axis_counter'): + self._debug_axis_counter = 0 + + self._debug_axis_counter += 1 + + # 每30帧输出一次详细信息 + if self._debug_axis_counter % 30 == 1: + print(f"🔍 {self.name}手轴数据调试:") + print(f" 总轴数: {len(axis_data)}") + print(f" 活跃轴: {', '.join(active_axes)}") + + # 显示所有轴的当前值(不管是否活跃) + all_axes = [] + for i, axis in enumerate(axis_data): + all_axes.append(f"[{i}]:({axis.x:.3f},{axis.y:.3f})") + print(f" 所有轴: {' '.join(all_axes)}") + + except Exception as e: + print(f"⚠️ 轴数据调试失败: {e}") + + def recreate_visualizer(self): + """重新创建visualizer - 用于渲染模式切换后刷新 + + 当VR渲染模式在运行时改变时调用此方法,以确保visualizer + 使用正确的渲染设置(普通模式 vs RenderPipeline模式) + """ + # 清理旧visualizer + if self.visualizer: + try: + self.visualizer.cleanup() + self.visualizer = None + print(f"🧹 {self.name}手柄visualizer已清理") + except Exception as e: + print(f"⚠️ 清理{self.name}手柄visualizer失败: {e}") + + # 重新创建visualizer + if self.anchor_node: + self._create_visualizer() + if self.visualizer: + print(f"✨ {self.name}手柄visualizer已重建(使用当前渲染模式)") + return True + else: + print(f"❌ {self.name}手柄visualizer重建失败") + return False + else: + print(f"⚠️ {self.name}手柄anchor_node不存在,无法重建visualizer") + return False + + def cleanup(self): + """清理资源""" + self.ignoreAll() + + if self.visualizer: + self.visualizer.cleanup() + + if self.anchor_node: + self.anchor_node.removeNode() + + self.is_connected = False + print(f"🧹 {self.name}手柄控制器已清理") + + +class LeftController(VRController): + """左手控制器""" + + def __init__(self, vr_manager): + super().__init__(vr_manager, 'left', '/user/hand/left') + + +class RightController(VRController): + """右手控制器""" + + def __init__(self, vr_manager): super().__init__(vr_manager, 'right', '/user/hand/right') \ No newline at end of file diff --git a/core/vr/visualization/__init__.py b/core/vr/visualization/__init__.py index 554c24b7..acbae2c9 100644 --- a/core/vr/visualization/__init__.py +++ b/core/vr/visualization/__init__.py @@ -1,10 +1,10 @@ -""" -VR可视化子系统 - -负责VR元素的可视化: -- 控制器3D模型渲染 -- 交互射线和指示器 -- VR特效管理 -""" - -__all__ = [] +""" +VR可视化子系统 + +负责VR元素的可视化: +- 控制器3D模型渲染 +- 交互射线和指示器 +- VR特效管理 +""" + +__all__ = [] diff --git a/core/vr/visualization/controllers.py b/core/vr/visualization/controllers.py index a976f068..c9080aee 100644 --- a/core/vr/visualization/controllers.py +++ b/core/vr/visualization/controllers.py @@ -1,781 +1,781 @@ -""" -VR可视化模块 - -提供VR手柄和交互元素的高级可视化功能: -- 手柄3D模型渲染 -- 交互射线显示 -- 按钮状态可视化 -- 触摸板和扳机反馈 -""" - -from panda3d.core import ( - NodePath, GeomNode, LineSegs, CardMaker, Geom, GeomVertexData, - GeomVertexFormat, GeomVertexWriter, GeomTriangles, GeomPoints, - Vec3, Vec4, Mat4, TransparencyAttrib, RenderState, ColorAttrib, - InternalName, loadPrcFileData -) -from panda3d.core import Texture, Material, TextureStage - -# 启用Assimp支持OBJ文件加载 -loadPrcFileData("", "load-file-type p3assimp") - - -class VRControllerVisualizer: - """VR手柄可视化器""" - - def __init__(self, controller, render_node): - """初始化手柄可视化器 - - Args: - controller: VRController实例 - render_node: 渲染节点 - """ - self.controller = controller - self.render = render_node - - # 可视化节点 - self.visual_node = None - self.model_node = None - self.ray_node = None - self.button_indicator_node = None - - # 射线参数 - self.ray_length = 10.0 - self.ray_color = Vec4(0.9, 0.9, 0.2, 0.8) - self.ray_hit_color = Vec4(0.2, 0.9, 0.2, 0.8) - - # 按钮指示器参数 - self.button_colors = { - 'normal': Vec4(0.3, 0.3, 0.8, 1.0), - 'left': Vec4(0.2, 0.6, 0.9, 1.0), - 'right': Vec4(0.9, 0.3, 0.3, 1.0), - 'trigger': Vec4(0.9, 0.6, 0.2, 1.0), - 'grip': Vec4(0.6, 0.9, 0.3, 1.0), - 'menu': Vec4(0.9, 0.2, 0.9, 1.0), - 'trackpad': Vec4(0.2, 0.9, 0.9, 1.0) - } - - self._create_visual_components() - - def _create_visual_components(self): - """创建可视化组件""" - if not self.controller.anchor_node: - return - - # 创建主可视化节点 - self.visual_node = self.controller.anchor_node.attachNewNode(f'{self.controller.name}_visual') - - # 创建手柄模型 - self._create_controller_model() - - # 创建交互射线 - self._create_interaction_ray() - - # 暂时注释按钮指示器功能,避免额外几何体造成悬空零件 - # self._create_button_indicators() - - def _create_controller_model(self): - """创建手柄3D模型""" - if not self.visual_node: - return - - # 创建模型节点 - self.model_node = self.visual_node.attachNewNode(f'{self.controller.name}_model') - - # 尝试加载SteamVR官方模型 - steamvr_model = self._load_steamvr_model() - - if steamvr_model: - # 使用SteamVR官方模型 - steamvr_model.reparentTo(self.model_node) - - # 应用SteamVR配置中的正确旋转值,绕Y轴(俯仰轴)旋转90度 - # body组件的rotate_xyz: [5.037,0.0,0.0],再加上绕Y轴旋转90度 - # 右手正确,左手需要反向 - if self.controller.name == 'left': - # 左手控制器:绕Y轴俯仰+90度(修正反向) - steamvr_model.setHpr(0, 5.037 + 90, 0) - else: - # 右手控制器:绕Y轴俯仰+90度(保持不变) - steamvr_model.setHpr(0, 5.037 + 90, 0) - - # 设置合适的缩放值 - steamvr_model.setScale(1.0) - - # 打印实际应用的旋转值 - if self.controller.name == 'left': - print(f"🔧 {self.controller.name}手柄:缩放: 1.0,旋转: (0, {5.037 + 90}, 0) [Y轴俯仰+90度]") - else: - print(f"🔧 {self.controller.name}手柄:缩放: 1.0,旋转: (0, {5.037 + 90}, 0) [Y轴俯仰+90度]") - - # 修复纯黑色问题:重新设置材质属性 - self._fix_model_material(steamvr_model) - - # 暂时注释身份标记功能,避免额外几何体造成悬空零件 - # self._apply_controller_identity_marker(steamvr_model) - - # 根据渲染模式设置渲染属性 - self._apply_render_mode_settings(steamvr_model) - - print(f"✅ {self.controller.name}手柄已加载SteamVR官方模型(缩放: 1.0,实体渲染模式)") - else: - # 降级到改进的程序化模型 - self._create_fallback_model() - print(f"⚠️ {self.controller.name}手柄使用程序化模型(未找到SteamVR模型)") - - def _load_steamvr_model(self): - """加载SteamVR官方手柄模型""" - import os - from panda3d.core import Filename, Texture, TextureStage - - # SteamVR模型基础路径 - steamvr_base_paths = [ - "/home/hello/.local/share/Steam/steamapps/common/SteamVR/resources/rendermodels/vr_controller_vive_1_5", - "/home/hello/.steam/steam/steamapps/common/SteamVR/resources/rendermodels/vr_controller_vive_1_5", - "~/.local/share/Steam/steamapps/common/SteamVR/resources/rendermodels/vr_controller_vive_1_5" - ] - - for base_path in steamvr_base_paths: - expanded_base_path = os.path.expanduser(base_path) - if os.path.exists(expanded_base_path): - print(f"🔍 找到SteamVR模型目录: {expanded_base_path}") - - # 不再添加目录到搜索路径,避免自动加载多余组件 - # from panda3d.core import getModelPath - # getModelPath().appendDirectory(expanded_base_path) - - # 尝试加载不同的模型文件,按优先级排序 - model_files = [ - ("body.obj", "手柄主体"), # 最重要的部分 - ("vr_controller_vive_1_5.obj", "完整手柄模型"), # 组合模型 - ] - - for model_file, description in model_files: - model_path = os.path.join(expanded_base_path, model_file) - if os.path.exists(model_path): - try: - print(f"🎮 尝试加载{description}: {model_file}") - - # 加载主模型 - model = loader.loadModel(Filename.fromOsSpecific(model_path)) - if model: - # 先应用纹理,再修复材质(保持纹理效果) - self._apply_steamvr_textures(model, expanded_base_path) - print(f"✅ 成功加载{description}") - return model - else: - print(f"⚠️ 模型文件存在但加载失败: {model_file}") - - except Exception as e: - print(f"❌ 加载{description}失败: {e}") - continue - - # 不再尝试组合加载多个部件,避免悬空零件问题 - print("⚠️ 单个模型文件加载失败,跳过组合加载以避免悬空零件") - break - - print("❌ 未找到任何SteamVR模型目录") - return None - - def _fix_model_material(self, model): - """修复模型材质,使用RenderPipeline兼容的新Material API""" - from panda3d.core import Material, Vec4 - - # 检查模型是否有纹理 - has_texture = model.hasTexture() - - # 创建新的材质,使用RenderPipeline兼容的API - material = Material() - - if has_texture: - # 有纹理时,设置白色基础颜色让纹理完全显示 - material.setBaseColor(Vec4(1.0, 1.0, 1.0, 1.0)) # 白色让纹理完全显示 - material.setRoughness(0.4) # 中等粗糙度 - material.setMetallic(0.3) # 轻度金属感 - material.setRefractiveIndex(1.5) # 标准折射率 - print(f"🎨 {self.controller.name}手柄:已设置材质(纹理+PBR)") - else: - # 无纹理时,设置手柄颜色 - material.setBaseColor(Vec4(0.7, 0.7, 0.8, 1.0)) # 略偏蓝的灰色 - material.setRoughness(0.5) # 中等粗糙度 - material.setMetallic(0.2) # 轻度金属感 - material.setRefractiveIndex(1.5) # 标准折射率 - print(f"🎨 {self.controller.name}手柄:已设置材质(颜色+PBR)") - - # 应用材质到模型,使用优先级1确保覆盖默认材质 - model.setMaterial(material, 1) - - # 确保模型能正确渲染(双面渲染是NodePath的方法) - model.setTwoSided(False) - - print(f"🔧 {self.controller.name}手柄:已使用RenderPipeline兼容的Material API") - - def _apply_controller_identity_marker(self, model): - """为控制器添加身份标记,区分左右手""" - from panda3d.core import RenderModeAttrib - - # 创建一个小的标识几何体 - marker_geom = self._create_box_geometry(0.005, 0.005, 0.02) - marker_node = model.attachNewNode(marker_geom) - - # 根据左右手设置不同位置和颜色(现在都是Y轴+90度俯仰) - if self.controller.name == 'left': - # 左手控制器:左侧标记 - marker_node.setPos(-0.03, 0.05, 0.02) - marker_node.setColor(0.2, 0.4, 1.0, 1.0) # 蓝色 - print(f"🔵 {self.controller.name}手柄已添加蓝色身份标记") - else: - # 右手控制器:右侧标记 - marker_node.setPos(0.03, 0.05, 0.02) - marker_node.setColor(1.0, 0.2, 0.2, 1.0) # 红色 - print(f"🔴 {self.controller.name}手柄已添加红色身份标记") - - # 让标记发光以便更容易看到 - marker_node.setLightOff() # 不受光照影响,保持明亮 - - # 添加轻微的色彩调整(非常微弱,不影响主要纹理) - if self.controller.name == 'left': - model.setColorScale(0.98, 0.98, 1.02, 1.0) # 极轻微的蓝色调 - else: - model.setColorScale(1.02, 0.98, 0.98, 1.0) # 极轻微的红色调 - - - def _apply_steamvr_textures(self, model, base_path): - """为SteamVR模型应用纹理""" - import os - from panda3d.core import Texture, TextureStage - - # SteamVR纹理文件 - texture_files = { - 'diffuse': 'onepointfive_texture.png', - 'specular': 'onepointfive_spec.png' - } - - textures_applied = 0 - - for texture_type, texture_file in texture_files.items(): - texture_path = os.path.join(base_path, texture_file) - if os.path.exists(texture_path): - try: - texture = loader.loadTexture(texture_path) - if texture: - # 确保纹理能正确加载 - texture.setWrapU(Texture.WMClamp) - texture.setWrapV(Texture.WMClamp) - texture.setMinfilter(Texture.FTLinearMipmapLinear) - texture.setMagfilter(Texture.FTLinear) - - if texture_type == 'diffuse': - # 应用主要漫反射纹理 - model.setTexture(texture) - print(f"✅ 应用了主纹理: {texture_file}") - textures_applied += 1 - elif texture_type == 'specular': - # 应用高光纹理 - ts = TextureStage('specular') - ts.setMode(TextureStage.MModulate) - model.setTexture(ts, texture) - print(f"✅ 应用了高光纹理: {texture_file}") - textures_applied += 1 - except Exception as e: - print(f"⚠️ 应用纹理失败 {texture_file}: {e}") - - if textures_applied == 0: - print(f"⚠️ {self.controller.name}手柄未能加载任何纹理,将使用材质颜色") - else: - print(f"🎨 {self.controller.name}手柄成功应用了 {textures_applied} 个纹理") - - def _load_combined_steamvr_model(self, base_path): - """尝试组合加载多个SteamVR模型部件""" - import os - from panda3d.core import NodePath - - # 重要的模型部件 - important_components = [ - "body.obj", - "trigger.obj", - "trackpad.obj", - "l_grip.obj" if self.controller.name == 'left' else "r_grip.obj" - ] - - combined_model = NodePath("combined_controller") - has_components = False - - for component in important_components: - component_path = os.path.join(base_path, component) - if os.path.exists(component_path): - try: - part = loader.loadModel(component_path) - if part: - part.reparentTo(combined_model) - has_components = True - print(f"✅ 加载了部件: {component}") - except Exception as e: - print(f"⚠️ 加载部件失败 {component}: {e}") - - if has_components: - self._apply_steamvr_textures(combined_model, base_path) - return combined_model - - return None - - def _create_fallback_model(self): - """创建改进的程序化手柄模型作为后备方案""" - # 主体(长条形状) - main_body = self._create_box_geometry(0.025, 0.15, 0.04) - main_node = self.model_node.attachNewNode(main_body) - - # 根据左右手设置不同颜色 - if self.controller.name == 'left': - color = self.button_colors['left'] - else: - color = self.button_colors['right'] - - main_node.setColor(color) - - # 启用光照响应 - from panda3d.core import RenderState, MaterialAttrib, Material - material = Material() - material.setShininess(32) - material.setAmbient((0.2, 0.2, 0.2, 1)) - material.setDiffuse(color) - material.setSpecular((0.5, 0.5, 0.5, 1)) - main_node.setMaterial(material) - - # 扳机区域(小突起) - trigger = self._create_box_geometry(0.015, 0.03, 0.02) - trigger_node = self.model_node.attachNewNode(trigger) - trigger_node.setPos(0, -0.08, 0.03) - trigger_node.setColor(self.button_colors['trigger']) - trigger_node.setMaterial(material) - - # 握把区域 - grip = self._create_box_geometry(0.02, 0.06, 0.03) - grip_node = self.model_node.attachNewNode(grip) - grip_node.setPos(0, 0.05, -0.03) - grip_node.setColor(self.button_colors['grip']) - grip_node.setMaterial(material) - - # 触摸板区域(圆盘) - trackpad = self._create_disc_geometry(0.015, 0.005) - trackpad_node = self.model_node.attachNewNode(trackpad) - trackpad_node.setPos(0, -0.02, 0.04) - trackpad_node.setColor(self.button_colors['trackpad']) - trackpad_node.setMaterial(material) - - # 应用渲染模式设置 - self._apply_render_mode_settings(self.model_node) - - def _create_box_geometry(self, width, length, height): - """创建立方体几何体""" - # 创建顶点格式 - format = GeomVertexFormat.getV3n3() - vdata = GeomVertexData('box', format, Geom.UHStatic) - - vertex = GeomVertexWriter(vdata, 'vertex') - normal = GeomVertexWriter(vdata, 'normal') - - # 定义立方体的8个顶点 - vertices = [ - Vec3(-width/2, -length/2, -height/2), - Vec3( width/2, -length/2, -height/2), - Vec3( width/2, length/2, -height/2), - Vec3(-width/2, length/2, -height/2), - Vec3(-width/2, -length/2, height/2), - Vec3( width/2, -length/2, height/2), - Vec3( width/2, length/2, height/2), - Vec3(-width/2, length/2, height/2) - ] - - # 立方体的6个面,每个面4个顶点 - faces = [ - # 底面 (z = -height/2) - [0, 1, 2, 3, Vec3(0, 0, -1)], - # 顶面 (z = height/2) - [7, 6, 5, 4, Vec3(0, 0, 1)], - # 前面 (y = -length/2) - [4, 5, 1, 0, Vec3(0, -1, 0)], - # 后面 (y = length/2) - [3, 2, 6, 7, Vec3(0, 1, 0)], - # 左面 (x = -width/2) - [0, 3, 7, 4, Vec3(-1, 0, 0)], - # 右面 (x = width/2) - [5, 6, 2, 1, Vec3(1, 0, 0)] - ] - - # 添加顶点和法线 - for face in faces: - for i in range(4): - vertex.addData3(vertices[face[i]]) - normal.addData3(face[4]) # 法线向量 - - # 创建几何体 - geom = Geom(vdata) - - # 为每个面创建三角形 - for face_idx in range(6): - base_idx = face_idx * 4 - prim = GeomTriangles(Geom.UHStatic) - # 第一个三角形 - prim.addVertices(base_idx, base_idx + 1, base_idx + 2) - # 第二个三角形 - prim.addVertices(base_idx, base_idx + 2, base_idx + 3) - geom.addPrimitive(prim) - - # 创建几何体节点 - geom_node = GeomNode('box') - geom_node.addGeom(geom) - - return geom_node - - def _create_disc_geometry(self, radius, thickness): - """创建圆盘几何体(用于触摸板)""" - format = GeomVertexFormat.getV3n3() - vdata = GeomVertexData('disc', format, Geom.UHStatic) - - vertex = GeomVertexWriter(vdata, 'vertex') - normal = GeomVertexWriter(vdata, 'normal') - - # 创建圆盘顶点 - segments = 16 - import math - - # 中心点 - vertex.addData3(0, 0, thickness/2) - normal.addData3(0, 0, 1) - - # 圆周点 - for i in range(segments): - angle = 2 * math.pi * i / segments - x = radius * math.cos(angle) - y = radius * math.sin(angle) - - vertex.addData3(x, y, thickness/2) - normal.addData3(0, 0, 1) - - # 创建几何体 - geom = Geom(vdata) - prim = GeomTriangles(Geom.UHStatic) - - # 创建扇形三角形 - for i in range(segments): - next_i = (i + 1) % segments - prim.addVertices(0, i + 1, next_i + 1) - - geom.addPrimitive(prim) - - # 创建几何体节点 - geom_node = GeomNode('disc') - geom_node.addGeom(geom) - - return geom_node - - def _create_interaction_ray(self): - """创建交互射线""" - if not self.visual_node: - return - - # 创建射线几何 - line_segs = LineSegs() - line_segs.setThickness(3) - line_segs.setColor(self.ray_color) - - # 射线主体 - line_segs.moveTo(0, 0, 0) - line_segs.drawTo(0, self.ray_length, 0) - - # 射线端点(小球) - end_point = self._create_sphere_geometry(0.02) - - # 创建射线节点 - geom_node = line_segs.create() - self.ray_node = self.visual_node.attachNewNode(geom_node) - - # 添加端点球 - end_node = self.ray_node.attachNewNode(end_point) - end_node.setPos(0, self.ray_length, 0) - end_node.setColor(self.ray_color) - - # 设置透明度 - self.ray_node.setTransparency(TransparencyAttrib.MAlpha) - - # 使用RenderPipeline兼容的Material API设置射线材质 - from panda3d.core import Material, Vec4 - ray_material = Material() - ray_material.setBaseColor(Vec4(self.ray_color.x, self.ray_color.y, self.ray_color.z, self.ray_color.w)) - ray_material.setRoughness(0.1) # 光滑射线 - ray_material.setMetallic(0.0) # 非金属 - ray_material.setRefractiveIndex(1.5) # 标准折射率 - - # 为射线应用材质 - self.ray_node.setMaterial(ray_material, 1) - self.ray_node.setTwoSided(False) - - # 为端点球设置相同的材质 - end_material = Material() - end_material.setBaseColor(Vec4(self.ray_color.x, self.ray_color.y, self.ray_color.z, self.ray_color.w)) - end_material.setRoughness(0.2) # 略粗糙 - end_material.setMetallic(0.0) # 非金属 - end_material.setRefractiveIndex(1.5) # 标准折射率 - - # 为端点球应用材质 - end_node.setMaterial(end_material, 1) - end_node.setTwoSided(False) - - # 默认隐藏射线 - self.ray_node.hide() - - print(f"✓ {self.controller.name}手柄交互射线已创建(含PBR支持)") - - def _create_sphere_geometry(self, radius): - """创建球体几何体""" - # 简单的立方体作为球体替代 - return self._create_box_geometry(radius, radius, radius) - - def _create_button_indicators(self): - """创建按钮状态指示器""" - if not self.visual_node: - return - - # 创建按钮指示器容器 - self.button_indicator_node = self.visual_node.attachNewNode(f'{self.controller.name}_indicators') - - # 扳机指示器 - trigger_indicator = self._create_box_geometry(0.005, 0.01, 0.005) - self.trigger_indicator = self.button_indicator_node.attachNewNode(trigger_indicator) - self.trigger_indicator.setPos(0.02, -0.08, 0.03) - self.trigger_indicator.setColor(0.2, 0.2, 0.2, 1.0) - - # 握把指示器 - grip_indicator = self._create_box_geometry(0.005, 0.02, 0.005) - self.grip_indicator = self.button_indicator_node.attachNewNode(grip_indicator) - self.grip_indicator.setPos(-0.02, 0.05, -0.03) - self.grip_indicator.setColor(0.2, 0.2, 0.2, 1.0) - - # 触摸板指示器 - trackpad_indicator = self._create_disc_geometry(0.003, 0.002) - self.trackpad_indicator = self.button_indicator_node.attachNewNode(trackpad_indicator) - self.trackpad_indicator.setPos(0, -0.02, 0.045) - self.trackpad_indicator.setColor(0.2, 0.2, 0.2, 1.0) - - # 为所有按钮指示器设置RenderPipeline兼容的材质 - from panda3d.core import Material, Vec4 - indicator_material = Material() - indicator_material.setBaseColor(Vec4(0.2, 0.2, 0.2, 1.0)) # 深灰色 - indicator_material.setRoughness(0.8) # 比较粗糙的表面 - indicator_material.setMetallic(0.1) # 轻微金属感 - indicator_material.setRefractiveIndex(1.5) # 标准折射率 - - # 为所有指示器应用材质 - for indicator in [self.trigger_indicator, self.grip_indicator, self.trackpad_indicator]: - indicator.setMaterial(indicator_material, 1) - indicator.setTwoSided(False) - - print(f"✓ {self.controller.name}手柄按钮指示器已创建") - - def update(self): - """更新可视化状态""" - if not self.controller.is_connected: - self.hide() - return - - self.show() - - # 更新按钮指示器状态 - self._update_button_indicators() - - # 更新射线显示状态 - self._update_ray_display() - - def _update_button_indicators(self): - """更新按钮指示器状态""" - if not hasattr(self, 'trigger_indicator'): - return - - # 扳机指示器 - if self.controller.is_trigger_pressed(): - self.trigger_indicator.setColor(self.button_colors['trigger']) - # 根据扳机值调整位置 - trigger_offset = self.controller.trigger_value * 0.01 - self.trigger_indicator.setPos(0.02, -0.08 + trigger_offset, 0.03) - else: - self.trigger_indicator.setColor(0.2, 0.2, 0.2, 1.0) - self.trigger_indicator.setPos(0.02, -0.08, 0.03) - - # 握把指示器 - if self.controller.is_grip_pressed(): - self.grip_indicator.setColor(self.button_colors['grip']) - else: - self.grip_indicator.setColor(0.2, 0.2, 0.2, 1.0) - - # 触摸板指示器 - if self.controller.touchpad_touched: - self.trackpad_indicator.setColor(self.button_colors['trackpad']) - # 根据触摸位置调整指示器位置 - if hasattr(self.controller, 'touchpad_pos'): - offset_x = self.controller.touchpad_pos.x * 0.01 - offset_y = self.controller.touchpad_pos.y * 0.01 - self.trackpad_indicator.setPos(offset_x, -0.02 + offset_y, 0.045) - else: - self.trackpad_indicator.setColor(0.2, 0.2, 0.2, 1.0) - self.trackpad_indicator.setPos(0, -0.02, 0.045) - - def _update_ray_display(self): - """更新射线显示""" - if not self.ray_node: - return - - # 根据交互状态显示/隐藏射线 - # 这里可以添加更复杂的逻辑,比如只在指向对象时显示 - show_ray = (self.controller.is_trigger_pressed(threshold=0.1) or - self.controller.touchpad_touched) - - if show_ray: - self.show_ray() - else: - self.hide_ray() - - def show(self): - """显示手柄可视化""" - if self.visual_node: - self.visual_node.show() - - def hide(self): - """隐藏手柄可视化""" - if self.visual_node: - self.visual_node.hide() - - def show_ray(self): - """显示交互射线""" - if self.ray_node: - self.ray_node.show() - - def hide_ray(self): - """隐藏交互射线""" - if self.ray_node: - self.ray_node.hide() - - def set_ray_color(self, color): - """设置射线颜色""" - if self.ray_node: - self.ray_node.setColor(color) - - # 更新射线材质的baseColor - from panda3d.core import Material, Vec4 - if isinstance(color, (list, tuple)) and len(color) >= 3: - alpha = color[3] if len(color) > 3 else 0.8 - ray_color = Vec4(color[0], color[1], color[2], alpha) - elif isinstance(color, Vec4): - ray_color = color - else: - ray_color = Vec4(0.9, 0.9, 0.2, 0.8) # 默认黄色 - - # 更新射线材质 - ray_material = Material() - ray_material.setBaseColor(ray_color) - ray_material.setRoughness(0.1) - ray_material.setMetallic(0.0) - ray_material.setRefractiveIndex(1.5) - self.ray_node.setMaterial(ray_material, 1) - - def set_ray_length(self, length): - """设置射线长度""" - self.ray_length = length - # 重新创建射线(简单的实现) - if self.ray_node: - self.ray_node.removeNode() - self._create_interaction_ray() - - def _apply_render_mode_settings(self, model_node): - """根据当前渲染模式应用渲染设置 - - Args: - model_node: 手柄模型节点 - """ - if not model_node: - return - - # 检测是否启用RenderPipeline模式 - is_render_pipeline = False - try: - # 通过VR管理器获取渲染模式 - vr_manager = self.controller.vr_manager - if hasattr(vr_manager, 'vr_render_mode'): - from core.vr import VRRenderMode - is_render_pipeline = (vr_manager.vr_render_mode == VRRenderMode.RENDER_PIPELINE and - vr_manager.render_pipeline_enabled) - except Exception as e: - print(f"⚠️ 检测渲染模式失败: {e}") - - if is_render_pipeline: - # RenderPipeline模式:使用正常深度测试,添加着色器标签 - print(f"🎨 {self.controller.name}手柄:应用RenderPipeline渲染模式") - - # 设置着色器标签,使模型通过RenderPipeline的GBuffer渲染 - model_node.setTag("RenderPipeline", "1") - - # 使用正常的深度测试和深度写入 - model_node.setDepthTest(True) - model_node.setDepthWrite(True) - - # 设置合适的渲染bin(transparent bin用于透明度支持) - # 使用默认的opaque bin确保正常渲染 - model_node.clearBin() - - # 递归设置所有子节点 - for child in model_node.findAllMatches("**"): - child.setTag("RenderPipeline", "1") - child.setDepthTest(True) - child.setDepthWrite(True) - child.clearBin() - - print(f"✅ {self.controller.name}手柄已配置RenderPipeline渲染") - else: - # 普通模式:使用always-on-top设置 - print(f"🎨 {self.controller.name}手柄:应用普通渲染模式(always-on-top)") - self._set_always_on_top(model_node) - - def _set_always_on_top(self, model_node): - """设置手柄模型始终显示在上层,不被其他物体遮挡(仅普通渲染模式)""" - if not model_node: - return - - from panda3d.core import RenderState - - # 设置为固定渲染bin,优先级设为较高值(1000) - # fixed bin中的对象按sort值从小到大渲染,越大越后渲染(越在上层) - model_node.setBin("fixed", 1000) - - # 禁用深度测试和深度写入,确保始终可见 - model_node.setDepthTest(False) - model_node.setDepthWrite(False) - - # 递归设置所有子节点的渲染属性 - for child in model_node.findAllMatches("**"): - child.setBin("fixed", 1000) - child.setDepthTest(False) - child.setDepthWrite(False) - - print(f"🔝 {self.controller.name}手柄已设置为始终显示在上层") - - def cleanup(self): - """清理资源""" - # 清理射线节点 - if hasattr(self, 'ray_node') and self.ray_node: - self.ray_node.removeNode() - self.ray_node = None - print(f"🧹 {self.controller.name}手柄射线已清理") - - # 清理模型节点 - if hasattr(self, 'model_node') and self.model_node: - self.model_node.removeNode() - self.model_node = None - print(f"🧹 {self.controller.name}手柄模型已清理") - - # 清理主可视化节点 - if self.visual_node: - self.visual_node.removeNode() - self.visual_node = None - print(f"🧹 {self.controller.name}手柄主节点已清理") - +""" +VR可视化模块 + +提供VR手柄和交互元素的高级可视化功能: +- 手柄3D模型渲染 +- 交互射线显示 +- 按钮状态可视化 +- 触摸板和扳机反馈 +""" + +from panda3d.core import ( + NodePath, GeomNode, LineSegs, CardMaker, Geom, GeomVertexData, + GeomVertexFormat, GeomVertexWriter, GeomTriangles, GeomPoints, + Vec3, Vec4, Mat4, TransparencyAttrib, RenderState, ColorAttrib, + InternalName, loadPrcFileData +) +from panda3d.core import Texture, Material, TextureStage + +# 启用Assimp支持OBJ文件加载 +loadPrcFileData("", "load-file-type p3assimp") + + +class VRControllerVisualizer: + """VR手柄可视化器""" + + def __init__(self, controller, render_node): + """初始化手柄可视化器 + + Args: + controller: VRController实例 + render_node: 渲染节点 + """ + self.controller = controller + self.render = render_node + + # 可视化节点 + self.visual_node = None + self.model_node = None + self.ray_node = None + self.button_indicator_node = None + + # 射线参数 + self.ray_length = 10.0 + self.ray_color = Vec4(0.9, 0.9, 0.2, 0.8) + self.ray_hit_color = Vec4(0.2, 0.9, 0.2, 0.8) + + # 按钮指示器参数 + self.button_colors = { + 'normal': Vec4(0.3, 0.3, 0.8, 1.0), + 'left': Vec4(0.2, 0.6, 0.9, 1.0), + 'right': Vec4(0.9, 0.3, 0.3, 1.0), + 'trigger': Vec4(0.9, 0.6, 0.2, 1.0), + 'grip': Vec4(0.6, 0.9, 0.3, 1.0), + 'menu': Vec4(0.9, 0.2, 0.9, 1.0), + 'trackpad': Vec4(0.2, 0.9, 0.9, 1.0) + } + + self._create_visual_components() + + def _create_visual_components(self): + """创建可视化组件""" + if not self.controller.anchor_node: + return + + # 创建主可视化节点 + self.visual_node = self.controller.anchor_node.attachNewNode(f'{self.controller.name}_visual') + + # 创建手柄模型 + self._create_controller_model() + + # 创建交互射线 + self._create_interaction_ray() + + # 暂时注释按钮指示器功能,避免额外几何体造成悬空零件 + # self._create_button_indicators() + + def _create_controller_model(self): + """创建手柄3D模型""" + if not self.visual_node: + return + + # 创建模型节点 + self.model_node = self.visual_node.attachNewNode(f'{self.controller.name}_model') + + # 尝试加载SteamVR官方模型 + steamvr_model = self._load_steamvr_model() + + if steamvr_model: + # 使用SteamVR官方模型 + steamvr_model.reparentTo(self.model_node) + + # 应用SteamVR配置中的正确旋转值,绕Y轴(俯仰轴)旋转90度 + # body组件的rotate_xyz: [5.037,0.0,0.0],再加上绕Y轴旋转90度 + # 右手正确,左手需要反向 + if self.controller.name == 'left': + # 左手控制器:绕Y轴俯仰+90度(修正反向) + steamvr_model.setHpr(0, 5.037 + 90, 0) + else: + # 右手控制器:绕Y轴俯仰+90度(保持不变) + steamvr_model.setHpr(0, 5.037 + 90, 0) + + # 设置合适的缩放值 + steamvr_model.setScale(1.0) + + # 打印实际应用的旋转值 + if self.controller.name == 'left': + print(f"🔧 {self.controller.name}手柄:缩放: 1.0,旋转: (0, {5.037 + 90}, 0) [Y轴俯仰+90度]") + else: + print(f"🔧 {self.controller.name}手柄:缩放: 1.0,旋转: (0, {5.037 + 90}, 0) [Y轴俯仰+90度]") + + # 修复纯黑色问题:重新设置材质属性 + self._fix_model_material(steamvr_model) + + # 暂时注释身份标记功能,避免额外几何体造成悬空零件 + # self._apply_controller_identity_marker(steamvr_model) + + # 根据渲染模式设置渲染属性 + self._apply_render_mode_settings(steamvr_model) + + print(f"✅ {self.controller.name}手柄已加载SteamVR官方模型(缩放: 1.0,实体渲染模式)") + else: + # 降级到改进的程序化模型 + self._create_fallback_model() + print(f"⚠️ {self.controller.name}手柄使用程序化模型(未找到SteamVR模型)") + + def _load_steamvr_model(self): + """加载SteamVR官方手柄模型""" + import os + from panda3d.core import Filename, Texture, TextureStage + + # SteamVR模型基础路径 + steamvr_base_paths = [ + "/home/hello/.local/share/Steam/steamapps/common/SteamVR/resources/rendermodels/vr_controller_vive_1_5", + "/home/hello/.steam/steam/steamapps/common/SteamVR/resources/rendermodels/vr_controller_vive_1_5", + "~/.local/share/Steam/steamapps/common/SteamVR/resources/rendermodels/vr_controller_vive_1_5" + ] + + for base_path in steamvr_base_paths: + expanded_base_path = os.path.expanduser(base_path) + if os.path.exists(expanded_base_path): + print(f"🔍 找到SteamVR模型目录: {expanded_base_path}") + + # 不再添加目录到搜索路径,避免自动加载多余组件 + # from panda3d.core import getModelPath + # getModelPath().appendDirectory(expanded_base_path) + + # 尝试加载不同的模型文件,按优先级排序 + model_files = [ + ("body.obj", "手柄主体"), # 最重要的部分 + ("vr_controller_vive_1_5.obj", "完整手柄模型"), # 组合模型 + ] + + for model_file, description in model_files: + model_path = os.path.join(expanded_base_path, model_file) + if os.path.exists(model_path): + try: + print(f"🎮 尝试加载{description}: {model_file}") + + # 加载主模型 + model = loader.loadModel(Filename.fromOsSpecific(model_path)) + if model: + # 先应用纹理,再修复材质(保持纹理效果) + self._apply_steamvr_textures(model, expanded_base_path) + print(f"✅ 成功加载{description}") + return model + else: + print(f"⚠️ 模型文件存在但加载失败: {model_file}") + + except Exception as e: + print(f"❌ 加载{description}失败: {e}") + continue + + # 不再尝试组合加载多个部件,避免悬空零件问题 + print("⚠️ 单个模型文件加载失败,跳过组合加载以避免悬空零件") + break + + print("❌ 未找到任何SteamVR模型目录") + return None + + def _fix_model_material(self, model): + """修复模型材质,使用RenderPipeline兼容的新Material API""" + from panda3d.core import Material, Vec4 + + # 检查模型是否有纹理 + has_texture = model.hasTexture() + + # 创建新的材质,使用RenderPipeline兼容的API + material = Material() + + if has_texture: + # 有纹理时,设置白色基础颜色让纹理完全显示 + material.setBaseColor(Vec4(1.0, 1.0, 1.0, 1.0)) # 白色让纹理完全显示 + material.setRoughness(0.4) # 中等粗糙度 + material.setMetallic(0.3) # 轻度金属感 + material.setRefractiveIndex(1.5) # 标准折射率 + print(f"🎨 {self.controller.name}手柄:已设置材质(纹理+PBR)") + else: + # 无纹理时,设置手柄颜色 + material.setBaseColor(Vec4(0.7, 0.7, 0.8, 1.0)) # 略偏蓝的灰色 + material.setRoughness(0.5) # 中等粗糙度 + material.setMetallic(0.2) # 轻度金属感 + material.setRefractiveIndex(1.5) # 标准折射率 + print(f"🎨 {self.controller.name}手柄:已设置材质(颜色+PBR)") + + # 应用材质到模型,使用优先级1确保覆盖默认材质 + model.setMaterial(material, 1) + + # 确保模型能正确渲染(双面渲染是NodePath的方法) + model.setTwoSided(False) + + print(f"🔧 {self.controller.name}手柄:已使用RenderPipeline兼容的Material API") + + def _apply_controller_identity_marker(self, model): + """为控制器添加身份标记,区分左右手""" + from panda3d.core import RenderModeAttrib + + # 创建一个小的标识几何体 + marker_geom = self._create_box_geometry(0.005, 0.005, 0.02) + marker_node = model.attachNewNode(marker_geom) + + # 根据左右手设置不同位置和颜色(现在都是Y轴+90度俯仰) + if self.controller.name == 'left': + # 左手控制器:左侧标记 + marker_node.setPos(-0.03, 0.05, 0.02) + marker_node.setColor(0.2, 0.4, 1.0, 1.0) # 蓝色 + print(f"🔵 {self.controller.name}手柄已添加蓝色身份标记") + else: + # 右手控制器:右侧标记 + marker_node.setPos(0.03, 0.05, 0.02) + marker_node.setColor(1.0, 0.2, 0.2, 1.0) # 红色 + print(f"🔴 {self.controller.name}手柄已添加红色身份标记") + + # 让标记发光以便更容易看到 + marker_node.setLightOff() # 不受光照影响,保持明亮 + + # 添加轻微的色彩调整(非常微弱,不影响主要纹理) + if self.controller.name == 'left': + model.setColorScale(0.98, 0.98, 1.02, 1.0) # 极轻微的蓝色调 + else: + model.setColorScale(1.02, 0.98, 0.98, 1.0) # 极轻微的红色调 + + + def _apply_steamvr_textures(self, model, base_path): + """为SteamVR模型应用纹理""" + import os + from panda3d.core import Texture, TextureStage + + # SteamVR纹理文件 + texture_files = { + 'diffuse': 'onepointfive_texture.png', + 'specular': 'onepointfive_spec.png' + } + + textures_applied = 0 + + for texture_type, texture_file in texture_files.items(): + texture_path = os.path.join(base_path, texture_file) + if os.path.exists(texture_path): + try: + texture = loader.loadTexture(texture_path) + if texture: + # 确保纹理能正确加载 + texture.setWrapU(Texture.WMClamp) + texture.setWrapV(Texture.WMClamp) + texture.setMinfilter(Texture.FTLinearMipmapLinear) + texture.setMagfilter(Texture.FTLinear) + + if texture_type == 'diffuse': + # 应用主要漫反射纹理 + model.setTexture(texture) + print(f"✅ 应用了主纹理: {texture_file}") + textures_applied += 1 + elif texture_type == 'specular': + # 应用高光纹理 + ts = TextureStage('specular') + ts.setMode(TextureStage.MModulate) + model.setTexture(ts, texture) + print(f"✅ 应用了高光纹理: {texture_file}") + textures_applied += 1 + except Exception as e: + print(f"⚠️ 应用纹理失败 {texture_file}: {e}") + + if textures_applied == 0: + print(f"⚠️ {self.controller.name}手柄未能加载任何纹理,将使用材质颜色") + else: + print(f"🎨 {self.controller.name}手柄成功应用了 {textures_applied} 个纹理") + + def _load_combined_steamvr_model(self, base_path): + """尝试组合加载多个SteamVR模型部件""" + import os + from panda3d.core import NodePath + + # 重要的模型部件 + important_components = [ + "body.obj", + "trigger.obj", + "trackpad.obj", + "l_grip.obj" if self.controller.name == 'left' else "r_grip.obj" + ] + + combined_model = NodePath("combined_controller") + has_components = False + + for component in important_components: + component_path = os.path.join(base_path, component) + if os.path.exists(component_path): + try: + part = loader.loadModel(component_path) + if part: + part.reparentTo(combined_model) + has_components = True + print(f"✅ 加载了部件: {component}") + except Exception as e: + print(f"⚠️ 加载部件失败 {component}: {e}") + + if has_components: + self._apply_steamvr_textures(combined_model, base_path) + return combined_model + + return None + + def _create_fallback_model(self): + """创建改进的程序化手柄模型作为后备方案""" + # 主体(长条形状) + main_body = self._create_box_geometry(0.025, 0.15, 0.04) + main_node = self.model_node.attachNewNode(main_body) + + # 根据左右手设置不同颜色 + if self.controller.name == 'left': + color = self.button_colors['left'] + else: + color = self.button_colors['right'] + + main_node.setColor(color) + + # 启用光照响应 + from panda3d.core import RenderState, MaterialAttrib, Material + material = Material() + material.setShininess(32) + material.setAmbient((0.2, 0.2, 0.2, 1)) + material.setDiffuse(color) + material.setSpecular((0.5, 0.5, 0.5, 1)) + main_node.setMaterial(material) + + # 扳机区域(小突起) + trigger = self._create_box_geometry(0.015, 0.03, 0.02) + trigger_node = self.model_node.attachNewNode(trigger) + trigger_node.setPos(0, -0.08, 0.03) + trigger_node.setColor(self.button_colors['trigger']) + trigger_node.setMaterial(material) + + # 握把区域 + grip = self._create_box_geometry(0.02, 0.06, 0.03) + grip_node = self.model_node.attachNewNode(grip) + grip_node.setPos(0, 0.05, -0.03) + grip_node.setColor(self.button_colors['grip']) + grip_node.setMaterial(material) + + # 触摸板区域(圆盘) + trackpad = self._create_disc_geometry(0.015, 0.005) + trackpad_node = self.model_node.attachNewNode(trackpad) + trackpad_node.setPos(0, -0.02, 0.04) + trackpad_node.setColor(self.button_colors['trackpad']) + trackpad_node.setMaterial(material) + + # 应用渲染模式设置 + self._apply_render_mode_settings(self.model_node) + + def _create_box_geometry(self, width, length, height): + """创建立方体几何体""" + # 创建顶点格式 + format = GeomVertexFormat.getV3n3() + vdata = GeomVertexData('box', format, Geom.UHStatic) + + vertex = GeomVertexWriter(vdata, 'vertex') + normal = GeomVertexWriter(vdata, 'normal') + + # 定义立方体的8个顶点 + vertices = [ + Vec3(-width/2, -length/2, -height/2), + Vec3( width/2, -length/2, -height/2), + Vec3( width/2, length/2, -height/2), + Vec3(-width/2, length/2, -height/2), + Vec3(-width/2, -length/2, height/2), + Vec3( width/2, -length/2, height/2), + Vec3( width/2, length/2, height/2), + Vec3(-width/2, length/2, height/2) + ] + + # 立方体的6个面,每个面4个顶点 + faces = [ + # 底面 (z = -height/2) + [0, 1, 2, 3, Vec3(0, 0, -1)], + # 顶面 (z = height/2) + [7, 6, 5, 4, Vec3(0, 0, 1)], + # 前面 (y = -length/2) + [4, 5, 1, 0, Vec3(0, -1, 0)], + # 后面 (y = length/2) + [3, 2, 6, 7, Vec3(0, 1, 0)], + # 左面 (x = -width/2) + [0, 3, 7, 4, Vec3(-1, 0, 0)], + # 右面 (x = width/2) + [5, 6, 2, 1, Vec3(1, 0, 0)] + ] + + # 添加顶点和法线 + for face in faces: + for i in range(4): + vertex.addData3(vertices[face[i]]) + normal.addData3(face[4]) # 法线向量 + + # 创建几何体 + geom = Geom(vdata) + + # 为每个面创建三角形 + for face_idx in range(6): + base_idx = face_idx * 4 + prim = GeomTriangles(Geom.UHStatic) + # 第一个三角形 + prim.addVertices(base_idx, base_idx + 1, base_idx + 2) + # 第二个三角形 + prim.addVertices(base_idx, base_idx + 2, base_idx + 3) + geom.addPrimitive(prim) + + # 创建几何体节点 + geom_node = GeomNode('box') + geom_node.addGeom(geom) + + return geom_node + + def _create_disc_geometry(self, radius, thickness): + """创建圆盘几何体(用于触摸板)""" + format = GeomVertexFormat.getV3n3() + vdata = GeomVertexData('disc', format, Geom.UHStatic) + + vertex = GeomVertexWriter(vdata, 'vertex') + normal = GeomVertexWriter(vdata, 'normal') + + # 创建圆盘顶点 + segments = 16 + import math + + # 中心点 + vertex.addData3(0, 0, thickness/2) + normal.addData3(0, 0, 1) + + # 圆周点 + for i in range(segments): + angle = 2 * math.pi * i / segments + x = radius * math.cos(angle) + y = radius * math.sin(angle) + + vertex.addData3(x, y, thickness/2) + normal.addData3(0, 0, 1) + + # 创建几何体 + geom = Geom(vdata) + prim = GeomTriangles(Geom.UHStatic) + + # 创建扇形三角形 + for i in range(segments): + next_i = (i + 1) % segments + prim.addVertices(0, i + 1, next_i + 1) + + geom.addPrimitive(prim) + + # 创建几何体节点 + geom_node = GeomNode('disc') + geom_node.addGeom(geom) + + return geom_node + + def _create_interaction_ray(self): + """创建交互射线""" + if not self.visual_node: + return + + # 创建射线几何 + line_segs = LineSegs() + line_segs.setThickness(3) + line_segs.setColor(self.ray_color) + + # 射线主体 + line_segs.moveTo(0, 0, 0) + line_segs.drawTo(0, self.ray_length, 0) + + # 射线端点(小球) + end_point = self._create_sphere_geometry(0.02) + + # 创建射线节点 + geom_node = line_segs.create() + self.ray_node = self.visual_node.attachNewNode(geom_node) + + # 添加端点球 + end_node = self.ray_node.attachNewNode(end_point) + end_node.setPos(0, self.ray_length, 0) + end_node.setColor(self.ray_color) + + # 设置透明度 + self.ray_node.setTransparency(TransparencyAttrib.MAlpha) + + # 使用RenderPipeline兼容的Material API设置射线材质 + from panda3d.core import Material, Vec4 + ray_material = Material() + ray_material.setBaseColor(Vec4(self.ray_color.x, self.ray_color.y, self.ray_color.z, self.ray_color.w)) + ray_material.setRoughness(0.1) # 光滑射线 + ray_material.setMetallic(0.0) # 非金属 + ray_material.setRefractiveIndex(1.5) # 标准折射率 + + # 为射线应用材质 + self.ray_node.setMaterial(ray_material, 1) + self.ray_node.setTwoSided(False) + + # 为端点球设置相同的材质 + end_material = Material() + end_material.setBaseColor(Vec4(self.ray_color.x, self.ray_color.y, self.ray_color.z, self.ray_color.w)) + end_material.setRoughness(0.2) # 略粗糙 + end_material.setMetallic(0.0) # 非金属 + end_material.setRefractiveIndex(1.5) # 标准折射率 + + # 为端点球应用材质 + end_node.setMaterial(end_material, 1) + end_node.setTwoSided(False) + + # 默认隐藏射线 + self.ray_node.hide() + + print(f"✓ {self.controller.name}手柄交互射线已创建(含PBR支持)") + + def _create_sphere_geometry(self, radius): + """创建球体几何体""" + # 简单的立方体作为球体替代 + return self._create_box_geometry(radius, radius, radius) + + def _create_button_indicators(self): + """创建按钮状态指示器""" + if not self.visual_node: + return + + # 创建按钮指示器容器 + self.button_indicator_node = self.visual_node.attachNewNode(f'{self.controller.name}_indicators') + + # 扳机指示器 + trigger_indicator = self._create_box_geometry(0.005, 0.01, 0.005) + self.trigger_indicator = self.button_indicator_node.attachNewNode(trigger_indicator) + self.trigger_indicator.setPos(0.02, -0.08, 0.03) + self.trigger_indicator.setColor(0.2, 0.2, 0.2, 1.0) + + # 握把指示器 + grip_indicator = self._create_box_geometry(0.005, 0.02, 0.005) + self.grip_indicator = self.button_indicator_node.attachNewNode(grip_indicator) + self.grip_indicator.setPos(-0.02, 0.05, -0.03) + self.grip_indicator.setColor(0.2, 0.2, 0.2, 1.0) + + # 触摸板指示器 + trackpad_indicator = self._create_disc_geometry(0.003, 0.002) + self.trackpad_indicator = self.button_indicator_node.attachNewNode(trackpad_indicator) + self.trackpad_indicator.setPos(0, -0.02, 0.045) + self.trackpad_indicator.setColor(0.2, 0.2, 0.2, 1.0) + + # 为所有按钮指示器设置RenderPipeline兼容的材质 + from panda3d.core import Material, Vec4 + indicator_material = Material() + indicator_material.setBaseColor(Vec4(0.2, 0.2, 0.2, 1.0)) # 深灰色 + indicator_material.setRoughness(0.8) # 比较粗糙的表面 + indicator_material.setMetallic(0.1) # 轻微金属感 + indicator_material.setRefractiveIndex(1.5) # 标准折射率 + + # 为所有指示器应用材质 + for indicator in [self.trigger_indicator, self.grip_indicator, self.trackpad_indicator]: + indicator.setMaterial(indicator_material, 1) + indicator.setTwoSided(False) + + print(f"✓ {self.controller.name}手柄按钮指示器已创建") + + def update(self): + """更新可视化状态""" + if not self.controller.is_connected: + self.hide() + return + + self.show() + + # 更新按钮指示器状态 + self._update_button_indicators() + + # 更新射线显示状态 + self._update_ray_display() + + def _update_button_indicators(self): + """更新按钮指示器状态""" + if not hasattr(self, 'trigger_indicator'): + return + + # 扳机指示器 + if self.controller.is_trigger_pressed(): + self.trigger_indicator.setColor(self.button_colors['trigger']) + # 根据扳机值调整位置 + trigger_offset = self.controller.trigger_value * 0.01 + self.trigger_indicator.setPos(0.02, -0.08 + trigger_offset, 0.03) + else: + self.trigger_indicator.setColor(0.2, 0.2, 0.2, 1.0) + self.trigger_indicator.setPos(0.02, -0.08, 0.03) + + # 握把指示器 + if self.controller.is_grip_pressed(): + self.grip_indicator.setColor(self.button_colors['grip']) + else: + self.grip_indicator.setColor(0.2, 0.2, 0.2, 1.0) + + # 触摸板指示器 + if self.controller.touchpad_touched: + self.trackpad_indicator.setColor(self.button_colors['trackpad']) + # 根据触摸位置调整指示器位置 + if hasattr(self.controller, 'touchpad_pos'): + offset_x = self.controller.touchpad_pos.x * 0.01 + offset_y = self.controller.touchpad_pos.y * 0.01 + self.trackpad_indicator.setPos(offset_x, -0.02 + offset_y, 0.045) + else: + self.trackpad_indicator.setColor(0.2, 0.2, 0.2, 1.0) + self.trackpad_indicator.setPos(0, -0.02, 0.045) + + def _update_ray_display(self): + """更新射线显示""" + if not self.ray_node: + return + + # 根据交互状态显示/隐藏射线 + # 这里可以添加更复杂的逻辑,比如只在指向对象时显示 + show_ray = (self.controller.is_trigger_pressed(threshold=0.1) or + self.controller.touchpad_touched) + + if show_ray: + self.show_ray() + else: + self.hide_ray() + + def show(self): + """显示手柄可视化""" + if self.visual_node: + self.visual_node.show() + + def hide(self): + """隐藏手柄可视化""" + if self.visual_node: + self.visual_node.hide() + + def show_ray(self): + """显示交互射线""" + if self.ray_node: + self.ray_node.show() + + def hide_ray(self): + """隐藏交互射线""" + if self.ray_node: + self.ray_node.hide() + + def set_ray_color(self, color): + """设置射线颜色""" + if self.ray_node: + self.ray_node.setColor(color) + + # 更新射线材质的baseColor + from panda3d.core import Material, Vec4 + if isinstance(color, (list, tuple)) and len(color) >= 3: + alpha = color[3] if len(color) > 3 else 0.8 + ray_color = Vec4(color[0], color[1], color[2], alpha) + elif isinstance(color, Vec4): + ray_color = color + else: + ray_color = Vec4(0.9, 0.9, 0.2, 0.8) # 默认黄色 + + # 更新射线材质 + ray_material = Material() + ray_material.setBaseColor(ray_color) + ray_material.setRoughness(0.1) + ray_material.setMetallic(0.0) + ray_material.setRefractiveIndex(1.5) + self.ray_node.setMaterial(ray_material, 1) + + def set_ray_length(self, length): + """设置射线长度""" + self.ray_length = length + # 重新创建射线(简单的实现) + if self.ray_node: + self.ray_node.removeNode() + self._create_interaction_ray() + + def _apply_render_mode_settings(self, model_node): + """根据当前渲染模式应用渲染设置 + + Args: + model_node: 手柄模型节点 + """ + if not model_node: + return + + # 检测是否启用RenderPipeline模式 + is_render_pipeline = False + try: + # 通过VR管理器获取渲染模式 + vr_manager = self.controller.vr_manager + if hasattr(vr_manager, 'vr_render_mode'): + from core.vr import VRRenderMode + is_render_pipeline = (vr_manager.vr_render_mode == VRRenderMode.RENDER_PIPELINE and + vr_manager.render_pipeline_enabled) + except Exception as e: + print(f"⚠️ 检测渲染模式失败: {e}") + + if is_render_pipeline: + # RenderPipeline模式:使用正常深度测试,添加着色器标签 + print(f"🎨 {self.controller.name}手柄:应用RenderPipeline渲染模式") + + # 设置着色器标签,使模型通过RenderPipeline的GBuffer渲染 + model_node.setTag("RenderPipeline", "1") + + # 使用正常的深度测试和深度写入 + model_node.setDepthTest(True) + model_node.setDepthWrite(True) + + # 设置合适的渲染bin(transparent bin用于透明度支持) + # 使用默认的opaque bin确保正常渲染 + model_node.clearBin() + + # 递归设置所有子节点 + for child in model_node.findAllMatches("**"): + child.setTag("RenderPipeline", "1") + child.setDepthTest(True) + child.setDepthWrite(True) + child.clearBin() + + print(f"✅ {self.controller.name}手柄已配置RenderPipeline渲染") + else: + # 普通模式:使用always-on-top设置 + print(f"🎨 {self.controller.name}手柄:应用普通渲染模式(always-on-top)") + self._set_always_on_top(model_node) + + def _set_always_on_top(self, model_node): + """设置手柄模型始终显示在上层,不被其他物体遮挡(仅普通渲染模式)""" + if not model_node: + return + + from panda3d.core import RenderState + + # 设置为固定渲染bin,优先级设为较高值(1000) + # fixed bin中的对象按sort值从小到大渲染,越大越后渲染(越在上层) + model_node.setBin("fixed", 1000) + + # 禁用深度测试和深度写入,确保始终可见 + model_node.setDepthTest(False) + model_node.setDepthWrite(False) + + # 递归设置所有子节点的渲染属性 + for child in model_node.findAllMatches("**"): + child.setBin("fixed", 1000) + child.setDepthTest(False) + child.setDepthWrite(False) + + print(f"🔝 {self.controller.name}手柄已设置为始终显示在上层") + + def cleanup(self): + """清理资源""" + # 清理射线节点 + if hasattr(self, 'ray_node') and self.ray_node: + self.ray_node.removeNode() + self.ray_node = None + print(f"🧹 {self.controller.name}手柄射线已清理") + + # 清理模型节点 + if hasattr(self, 'model_node') and self.model_node: + self.model_node.removeNode() + self.model_node = None + print(f"🧹 {self.controller.name}手柄模型已清理") + + # 清理主可视化节点 + if self.visual_node: + self.visual_node.removeNode() + self.visual_node = None + print(f"🧹 {self.controller.name}手柄主节点已清理") + print(f"✅ {self.controller.name}手柄可视化已彻底清理") \ No newline at end of file diff --git a/core/vr/visualization/effects.py b/core/vr/visualization/effects.py index a968491c..39e74aad 100644 --- a/core/vr/visualization/effects.py +++ b/core/vr/visualization/effects.py @@ -1,226 +1,226 @@ -""" -VR Effects Manager - 为VR场景中的模型自动应用RenderPipeline Effects - -主要功能: -1. 扫描VR场景中的所有模型节点 -2. 自动应用RenderPipeline的默认effect配置 -3. 设置正确的shader tags以确保模型能正确渲染到GBuffer -4. 支持批量应用和单个模型应用 -""" - -from panda3d.core import NodePath, GeomNode - - -class VREffectsManager: - """VR场景的RenderPipeline Effects管理器""" - - def __init__(self, pipeline): - """ - 初始化VR Effects Manager - - Args: - pipeline: RenderPipeline实例 - """ - self.pipeline = pipeline - self.applied_models = set() # 记录已应用effects的模型 - - # 默认的effect配置 - self.default_effect_config = { - "normal_mapping": True, # 法线贴图 - "render_gbuffer": True, # 渲染到GBuffer - "alpha_testing": False, # Alpha测试 - "parallax_mapping": False, # 视差贴图(性能考虑,VR中默认关闭) - "render_shadow": True, # 投射阴影 - "render_envmap": True, # 环境映射 - } - - def apply_effects_to_scene(self, scene_root): - """ - 为场景根节点下的所有模型应用effects - - Args: - scene_root: 场景根节点(通常是render或其子节点) - - Returns: - int: 应用effects的模型数量 - """ - if not self.pipeline: - print("⚠️ RenderPipeline未初始化,无法应用effects") - return 0 - - print("🎨 开始为VR场景应用RenderPipeline Effects...") - count = 0 - - # 递归遍历所有子节点 - for node in scene_root.findAllMatches("**/+GeomNode"): - if self._should_apply_effect(node): - if self.apply_effect_to_model(node): - count += 1 - - print(f"✅ 为 {count} 个模型应用了RenderPipeline Effects") - return count - - def apply_effect_to_model(self, model_node, effect_config=None): - """ - 为单个模型节点应用RenderPipeline effect - - Args: - model_node: 模型的NodePath - effect_config: 自定义effect配置(可选,默认使用default_effect_config) - - Returns: - bool: 是否成功应用 - """ - if not isinstance(model_node, NodePath): - print(f"⚠️ 无效的模型节点: {model_node}") - return False - - # 使用提供的配置或默认配置 - config = effect_config or self.default_effect_config - - try: - # 应用RenderPipeline effect - self.pipeline.set_effect( - model_node, - "effects/default.yaml", - config, - sort=50 # 默认排序值 - ) - - # 设置RenderPipeline shader tag - # 这个tag告诉RenderPipeline这个模型需要通过GBuffer渲染 - model_node.setTag("RenderPipeline", "1") - - # 递归设置所有子节点的tag - for child in model_node.findAllMatches("**/+GeomNode"): - child.setTag("RenderPipeline", "1") - - # 记录已应用的模型 - model_id = id(model_node.node()) - self.applied_models.add(model_id) - - return True - - except Exception as e: - print(f"❌ 应用effect失败: {model_node.getName()} - {e}") - return False - - def apply_effect_to_model_simple(self, model_node): - """ - 为模型应用简化版effect(只设置tags,不调用set_effect) - - 适用于某些特殊情况,例如: - - 模型已经有自定义shader - - 只需要确保模型能渲染到GBuffer - - 避免覆盖现有的shader配置 - - Args: - model_node: 模型的NodePath - - Returns: - bool: 是否成功 - """ - try: - model_node.setTag("RenderPipeline", "1") - - # 递归设置所有子节点 - for child in model_node.findAllMatches("**/+GeomNode"): - child.setTag("RenderPipeline", "1") - - return True - - except Exception as e: - print(f"❌ 设置tags失败: {model_node.getName()} - {e}") - return False - - def _should_apply_effect(self, node): - """ - 判断是否应该为节点应用effect - - Args: - node: 待检查的节点 - - Returns: - bool: 是否应该应用effect - """ - # 跳过已应用的模型 - model_id = id(node.node()) - if model_id in self.applied_models: - return False - - # 跳过没有几何数据的节点 - if not node.node().getNumGeoms() > 0: - return False - - # 跳过特殊标记的节点(例如:UI元素、调试几何体等) - # 检查节点是否有"skip_pipeline"标签 - if node.hasTag("skip_pipeline"): - return False - - # 跳过隐藏的节点 - if node.isHidden(): - return False - - return True - - def apply_effects_to_new_models(self, model_list): - """ - 为新添加的模型列表批量应用effects - - Args: - model_list: 模型NodePath列表 - - Returns: - int: 成功应用effects的模型数量 - """ - count = 0 - for model in model_list: - if self.apply_effect_to_model(model): - count += 1 - - print(f"✅ 为 {count}/{len(model_list)} 个新模型应用了effects") - return count - - def update_effect_config(self, new_config): - """ - 更新默认effect配置 - - Args: - new_config: 新的配置字典(会合并到现有配置) - """ - self.default_effect_config.update(new_config) - print(f"✓ 更新effect配置: {new_config}") - - def get_applied_models_count(self): - """获取已应用effects的模型数量""" - return len(self.applied_models) - - def clear_applied_models_cache(self): - """清空已应用模型的缓存(用于场景重置等情况)""" - self.applied_models.clear() - print("✓ 已清空effects应用缓存") - - -def setup_vr_model_effects(world, vr_root=None): - """ - 便捷函数:为VR场景设置RenderPipeline Effects - - Args: - world: CoreWorld实例(需要有render_pipeline属性) - vr_root: VR场景根节点(可选,默认使用world.render) - - Returns: - VREffectsManager实例,或None(如果失败) - """ - if not hasattr(world, 'render_pipeline') or not world.render_pipeline: - print("⚠️ RenderPipeline未初始化") - return None - - # 创建Effects Manager - effects_mgr = VREffectsManager(world.render_pipeline) - - # 应用effects到场景 - scene_root = vr_root or world.render - effects_mgr.apply_effects_to_scene(scene_root) - - return effects_mgr +""" +VR Effects Manager - 为VR场景中的模型自动应用RenderPipeline Effects + +主要功能: +1. 扫描VR场景中的所有模型节点 +2. 自动应用RenderPipeline的默认effect配置 +3. 设置正确的shader tags以确保模型能正确渲染到GBuffer +4. 支持批量应用和单个模型应用 +""" + +from panda3d.core import NodePath, GeomNode + + +class VREffectsManager: + """VR场景的RenderPipeline Effects管理器""" + + def __init__(self, pipeline): + """ + 初始化VR Effects Manager + + Args: + pipeline: RenderPipeline实例 + """ + self.pipeline = pipeline + self.applied_models = set() # 记录已应用effects的模型 + + # 默认的effect配置 + self.default_effect_config = { + "normal_mapping": True, # 法线贴图 + "render_gbuffer": True, # 渲染到GBuffer + "alpha_testing": False, # Alpha测试 + "parallax_mapping": False, # 视差贴图(性能考虑,VR中默认关闭) + "render_shadow": True, # 投射阴影 + "render_envmap": True, # 环境映射 + } + + def apply_effects_to_scene(self, scene_root): + """ + 为场景根节点下的所有模型应用effects + + Args: + scene_root: 场景根节点(通常是render或其子节点) + + Returns: + int: 应用effects的模型数量 + """ + if not self.pipeline: + print("⚠️ RenderPipeline未初始化,无法应用effects") + return 0 + + print("🎨 开始为VR场景应用RenderPipeline Effects...") + count = 0 + + # 递归遍历所有子节点 + for node in scene_root.findAllMatches("**/+GeomNode"): + if self._should_apply_effect(node): + if self.apply_effect_to_model(node): + count += 1 + + print(f"✅ 为 {count} 个模型应用了RenderPipeline Effects") + return count + + def apply_effect_to_model(self, model_node, effect_config=None): + """ + 为单个模型节点应用RenderPipeline effect + + Args: + model_node: 模型的NodePath + effect_config: 自定义effect配置(可选,默认使用default_effect_config) + + Returns: + bool: 是否成功应用 + """ + if not isinstance(model_node, NodePath): + print(f"⚠️ 无效的模型节点: {model_node}") + return False + + # 使用提供的配置或默认配置 + config = effect_config or self.default_effect_config + + try: + # 应用RenderPipeline effect + self.pipeline.set_effect( + model_node, + "effects/default.yaml", + config, + sort=50 # 默认排序值 + ) + + # 设置RenderPipeline shader tag + # 这个tag告诉RenderPipeline这个模型需要通过GBuffer渲染 + model_node.setTag("RenderPipeline", "1") + + # 递归设置所有子节点的tag + for child in model_node.findAllMatches("**/+GeomNode"): + child.setTag("RenderPipeline", "1") + + # 记录已应用的模型 + model_id = id(model_node.node()) + self.applied_models.add(model_id) + + return True + + except Exception as e: + print(f"❌ 应用effect失败: {model_node.getName()} - {e}") + return False + + def apply_effect_to_model_simple(self, model_node): + """ + 为模型应用简化版effect(只设置tags,不调用set_effect) + + 适用于某些特殊情况,例如: + - 模型已经有自定义shader + - 只需要确保模型能渲染到GBuffer + - 避免覆盖现有的shader配置 + + Args: + model_node: 模型的NodePath + + Returns: + bool: 是否成功 + """ + try: + model_node.setTag("RenderPipeline", "1") + + # 递归设置所有子节点 + for child in model_node.findAllMatches("**/+GeomNode"): + child.setTag("RenderPipeline", "1") + + return True + + except Exception as e: + print(f"❌ 设置tags失败: {model_node.getName()} - {e}") + return False + + def _should_apply_effect(self, node): + """ + 判断是否应该为节点应用effect + + Args: + node: 待检查的节点 + + Returns: + bool: 是否应该应用effect + """ + # 跳过已应用的模型 + model_id = id(node.node()) + if model_id in self.applied_models: + return False + + # 跳过没有几何数据的节点 + if not node.node().getNumGeoms() > 0: + return False + + # 跳过特殊标记的节点(例如:UI元素、调试几何体等) + # 检查节点是否有"skip_pipeline"标签 + if node.hasTag("skip_pipeline"): + return False + + # 跳过隐藏的节点 + if node.isHidden(): + return False + + return True + + def apply_effects_to_new_models(self, model_list): + """ + 为新添加的模型列表批量应用effects + + Args: + model_list: 模型NodePath列表 + + Returns: + int: 成功应用effects的模型数量 + """ + count = 0 + for model in model_list: + if self.apply_effect_to_model(model): + count += 1 + + print(f"✅ 为 {count}/{len(model_list)} 个新模型应用了effects") + return count + + def update_effect_config(self, new_config): + """ + 更新默认effect配置 + + Args: + new_config: 新的配置字典(会合并到现有配置) + """ + self.default_effect_config.update(new_config) + print(f"✓ 更新effect配置: {new_config}") + + def get_applied_models_count(self): + """获取已应用effects的模型数量""" + return len(self.applied_models) + + def clear_applied_models_cache(self): + """清空已应用模型的缓存(用于场景重置等情况)""" + self.applied_models.clear() + print("✓ 已清空effects应用缓存") + + +def setup_vr_model_effects(world, vr_root=None): + """ + 便捷函数:为VR场景设置RenderPipeline Effects + + Args: + world: CoreWorld实例(需要有render_pipeline属性) + vr_root: VR场景根节点(可选,默认使用world.render) + + Returns: + VREffectsManager实例,或None(如果失败) + """ + if not hasattr(world, 'render_pipeline') or not world.render_pipeline: + print("⚠️ RenderPipeline未初始化") + return None + + # 创建Effects Manager + effects_mgr = VREffectsManager(world.render_pipeline) + + # 应用effects到场景 + scene_root = vr_root or world.render + effects_mgr.apply_effects_to_scene(scene_root) + + return effects_mgr diff --git a/core/vr_manager.py b/core/vr_manager.py index daa2b300..b82186a5 100644 --- a/core/vr_manager.py +++ b/core/vr_manager.py @@ -1,3554 +1,3554 @@ -""" -VR管理器模块 - -负责VR功能的初始化、渲染和交互: -- OpenVR/SteamVR集成 -- VR头显跟踪和渲染 -- VR控制器交互 -- VR模式切换 -""" - -import sys -import gc -import numpy as np -from panda3d.core import ( - WindowProperties, GraphicsPipe, FrameBufferProperties, - GraphicsOutput, Texture, Camera, PerspectiveLens, MatrixLens, - Mat4, Vec3, TransformState, RenderState, CardMaker, - BitMask32, PandaNode, NodePath, LMatrix4, LVector3, LVector4, - CS_yup_right, CS_default, PythonCallbackObject -) -from direct.task import Task -from direct.showbase.DirectObject import DirectObject - -try: - import openvr - OPENVR_AVAILABLE = True -except ImportError: - OPENVR_AVAILABLE = False - print("警告: OpenVR未安装,VR功能将不可用") - -# 导入手柄控制器、动作系统、交互系统、摇杆系统和传送系统 -from core.vr.tracking.controllers import LeftController, RightController -from core.vr.interaction.actions import VRActionManager -from core.vr.interaction.grab import VRInteractionManager -from core.vr.interaction.joystick import VRJoystickManager -from core.vr.interaction.teleport import VRTeleportSystem -from core.vr.performance.monitoring import VRPerformanceMonitor -from core.vr.performance.optimization import VROptimization -from core.vr.testing import VRTestMode -from enum import Enum - - -class VRRenderMode(Enum): - """VR渲染模式枚举""" - NORMAL = "normal" # 普通渲染模式 - RENDER_PIPELINE = "render_pipeline" # RenderPipeline高级渲染模式 - - -class VRManager(DirectObject): - """VR管理器类 - 处理所有VR相关功能""" - - def __init__(self, world): - """初始化VR管理器 - - Args: - world: 主世界对象引用 - """ - super().__init__() - - self.world = world - self.vr_system = None - self.vr_enabled = False - self.vr_initialized = False - - # VR渲染相关 - self.vr_left_eye_buffer = None - self.vr_right_eye_buffer = None - self.vr_left_camera = None - self.vr_right_camera = None - self.vr_compositor = None - - # VR纹理和ID缓存 - 修复重复准备问题 - self.vr_left_texture = None - self.vr_right_texture = None - self.left_texture_id = None # 缓存左眼纹理的OpenGL ID - self.right_texture_id = None # 缓存右眼纹理的OpenGL ID - self.textures_prepared = False # 标记纹理是否已准备 - - # VR跟踪数据 - self.hmd_pose = Mat4.identMat() - self.controller_poses = {} - self.tracked_device_poses = [] - self.poses = None # OpenVR渲染姿态数组 - self.game_poses = None # OpenVR游戏逻辑姿态数组 - - # 🚀 立即初始化对象池和GC控制(在其他组件之前) - # 对象池和优化系统 - 模块化重构 - try: - self.optimization = VROptimization(self) - self.optimization._initialize_object_pools() - print("✓ VR对象池和优化系统初始化完成") - except Exception as e: - print(f"⚠️ VR对象池和优化系统初始化失败: {e}") - self.optimization = None - - # VR渲染参数 - self.eye_width = 1080 - self.eye_height = 1200 - self.near_clip = 0.1 - self.far_clip = 1000.0 - - # VR分辨率缩放优化 - 现在由优化模块管理 - self.resolution_scale = 0.75 # 默认0.75倍分辨率,性能和质量平衡 - self.base_eye_width = 1080 # 原始推荐分辨率 - self.base_eye_height = 1200 - self.scaled_eye_width = 1080 # 实际使用的缩放后分辨率 - self.scaled_eye_height = 1200 - - # VR质量预设 - self.quality_presets = { - 'performance': 0.6, # 性能模式 - 约60%分辨率 - 'balanced': 0.75, # 平衡模式 - 约75%分辨率 - 'quality': 1.0 # 质量模式 - 100%分辨率 - } - self.current_quality_preset = 'balanced' # 默认平衡模式 - - # 🎨 VR渲染模式配置 - RenderPipeline集成 - self.vr_render_mode = VRRenderMode.NORMAL # 默认使用普通渲染模式 - self.render_pipeline_enabled = False # RenderPipeline是否已启用 - self.vr_pipeline_left_target = None # 左眼RenderPipeline渲染目标 - self.vr_pipeline_right_target = None # 右眼RenderPipeline渲染目标 - self.pipeline_resolution_scale = 0.75 # RenderPipeline模式下的分辨率缩放(性能优化) - self.vr_pipeline_controller = None # VR Pipeline控制器(管理完整的VR渲染管线) - - # RenderPipeline VR优化配置 - self.pipeline_vr_config = { - 'enable_shadows': True, # 启用阴影 - 'enable_ao': True, # 启用环境光遮蔽 - 'enable_bloom': False, # 禁用泛光(VR性能考虑) - 'enable_motion_blur': False, # 禁用运动模糊(VR中会引起不适) - 'enable_ssr': False, # 禁用屏幕空间反射(性能密集) - 'shadow_quality': 'medium', # 阴影质量:low/medium/high - 'ao_quality': 'low', # AO质量:low/medium/high - } - - # VR任务 - self.vr_task = None - - # VR锚点层级系统 - self.tracking_space = None - self.hmd_anchor = None - self.left_eye_anchor = None - self.right_eye_anchor = None - - # 坐标系转换矩阵 - 使用Panda3D内置方法 - self.coord_mat = LMatrix4.convert_mat(CS_yup_right, CS_default) - self.coord_mat_inv = LMatrix4.convert_mat(CS_default, CS_yup_right) - - # OpenVR 帧ID跟踪(防止重复提交) - self.openvr_frame_id = 0 - self.left_eye_last_render_frame = -1 - self.right_eye_last_render_frame = -1 - - # waitGetPoses调用策略配置 - 使用高性能的update_task策略 - self.use_prediction_time = 0.011 # 11ms的预测时间 - OpenVR标准值,平衡准确性和延迟 - self.poses_updated_in_task = True # 始终在更新任务中获取姿态(Running Start模式) - - # 🎨 初始化VR配置管理器 - try: - from core.vr.config.vr_config import VRConfigManager - self.config_manager = VRConfigManager() - # 加载配置并应用 - self.config_manager.apply_config_to_vr_manager(self) - print("✓ VR配置管理器初始化完成并已加载配置") - except Exception as e: - print(f"⚠️ VR配置管理器初始化失败: {e}") - self.config_manager = None - - # VR提交策略 - 基于参考实现 - self.submit_together = True # 是否在right_cb中同时提交左右眼 - - - # Running Start标记 - Valve最佳实践 - self._waitgetposes_called_this_frame = False - - # 姿态缓存 - 修复时序不匹配 - self._cached_render_poses = None # 用于渲染的缓存姿态 - self._first_frame = True # 首帧标记 - - # ATW控制选项 - 备选方案 - self.disable_async_reprojection = False # 是否禁用异步重投影 - - # VR手柄控制器 - self.left_controller = None - self.right_controller = None - self.controllers = {} # 设备索引到控制器的映射 - self.tracked_device_anchors = {} # 跟踪设备锚点 - - # VR动作系统 - 可选择禁用(用于API兼容性问题) - self.disable_action_system = True # 禁用动作系统,使用直接控制器输入 - - if not self.disable_action_system: - try: - self.action_manager = VRActionManager(self) - print("✓ VR动作管理器初始化完成") - except Exception as e: - print(f"⚠️ VR动作管理器初始化失败: {e}") - self.action_manager = None - else: - self.action_manager = None - print("🚫 VR动作系统已禁用,使用直接控制器输入") - - # VR交互系统 - 添加异常保护 - try: - self.interaction_manager = VRInteractionManager(self) - print("✓ VR交互管理器初始化完成") - except Exception as e: - print(f"⚠️ VR交互管理器初始化失败: {e}") - self.interaction_manager = None - - # VR传送系统 - 添加异常保护 - try: - self.teleport_system = VRTeleportSystem(self) - print("✓ VR传送系统初始化完成") - except Exception as e: - print(f"⚠️ VR传送系统初始化失败: {e}") - self.teleport_system = None - - # VR摇杆系统 - 添加异常保护 - try: - self.joystick_manager = VRJoystickManager(self) - print("✓ VR摇杆管理器初始化完成") - except Exception as e: - print(f"⚠️ VR摇杆管理器初始化失败: {e}") - self.joystick_manager = None - - # 🎯 性能监控系统 - 模块化重构 - try: - self.performance_monitor = VRPerformanceMonitor(self) - print("✓ VR性能监控系统初始化完成") - except Exception as e: - print(f"⚠️ VR性能监控系统初始化失败: {e}") - self.performance_monitor = None - - # 🧪 测试调试系统 - 模块化重构 - try: - self.test_mode = VRTestMode(self) - print("✓ VR测试模式系统初始化完成") - except Exception as e: - print(f"⚠️ VR测试模式系统初始化失败: {e}") - self.test_mode = None - - print("✓ VR管理器初始化完成") - - # ====== 对象池和优化系统委托方法 ====== - - def enable_gc_control(self): - """启用垃圾回收控制 - 减少VR渲染期间的GC峰值""" - if self.optimization: - return self.optimization.enable_gc_control() - return False - - def disable_gc_control(self): - """禁用垃圾回收控制 - 恢复自动垃圾回收""" - if self.optimization: - return self.optimization.disable_gc_control() - return False - - def set_manual_gc_interval(self, frames): - """设置手动垃圾回收间隔 - - Args: - frames: 帧数间隔 (建议100-600) - """ - if self.optimization: - return self.optimization.set_manual_gc_interval(frames) - return False - - def force_manual_gc(self): - """强制执行一次垃圾回收""" - if self.optimization: - return self.optimization.force_manual_gc() - return 0 - - def get_object_pool_status(self): - """获取对象池状态""" - if self.optimization: - return self.optimization.get_object_pool_status() - return {} - - def set_resolution_scale(self, scale): - """设置VR分辨率缩放系数 - - Args: - scale: 缩放系数 (0.3-1.0),0.75表示75%分辨率 - """ - if self.optimization: - return self.optimization.set_resolution_scale(scale) - return False - - def set_quality_preset(self, preset_name): - """设置VR质量预设 - - Args: - preset_name: 'performance', 'balanced', 'quality' - """ - if self.optimization: - return self.optimization.set_quality_preset(preset_name) - return False - - def cycle_quality_preset(self): - """循环切换质量预设""" - if self.optimization: - return self.optimization.cycle_quality_preset() - return False - - def get_resolution_info(self): - """获取分辨率相关信息""" - if self.optimization: - return self.optimization.get_resolution_info() - return {} - - def print_resolution_info(self): - """输出分辨率信息""" - if self.optimization: - return self.optimization.print_resolution_info() - - def enable_performance_mode(self): - """手动启用性能模式 - 立即禁用详细监控以提升性能""" - if self.optimization: - return self.optimization.enable_performance_mode() - - def disable_performance_mode(self): - """禁用性能模式 - 重新启用详细监控(用于调试)""" - if self.optimization: - return self.optimization.disable_performance_mode() - - def set_performance_mode_trigger_frame(self, frame_count): - """设置性能模式自动触发的帧数 - - Args: - frame_count: 触发帧数 (建议300-1200) - """ - if self.optimization: - return self.optimization.set_performance_mode_trigger_frame(frame_count) - - def get_performance_mode_status(self): - """获取性能模式状态""" - if self.optimization: - return self.optimization.get_performance_mode_status() - return {} - - - - - - def convert_mat(self, mat): - """ - 将OpenVR矩阵转换为Panda3D矩阵 - 基于参考实现 - """ - if len(mat.m) == 4: - result = LMatrix4( - mat.m[0][0], mat.m[1][0], mat.m[2][0], mat.m[3][0], - mat.m[0][1], mat.m[1][1], mat.m[2][1], mat.m[3][1], - mat.m[0][2], mat.m[1][2], mat.m[2][2], mat.m[3][2], - mat.m[0][3], mat.m[1][3], mat.m[2][3], mat.m[3][3]) - elif len(mat.m) == 3: - result = LMatrix4( - mat.m[0][0], mat.m[1][0], mat.m[2][0], 0.0, - mat.m[0][1], mat.m[1][1], mat.m[2][1], 0.0, - mat.m[0][2], mat.m[1][2], mat.m[2][2], 0.0, - mat.m[0][3], mat.m[1][3], mat.m[2][3], 1.0) - return result - - def is_vr_available(self): - """检查VR系统是否可用""" - if not OPENVR_AVAILABLE: - return False - - try: - # 检查SteamVR是否运行 - return openvr.isRuntimeInstalled() and openvr.isHmdPresent() - except Exception as e: - print(f"VR检查失败: {e}") - return False - - def initialize_vr(self): - """初始化VR系统""" - if not OPENVR_AVAILABLE: - print("❌ OpenVR不可用,无法初始化VR") - return False - - if self.vr_initialized: - print("VR系统已经初始化") - return True - - try: - print("🔄 正在初始化VR系统...") - - # 🚀 确保对象池已正确初始化(备用检查) - if self.optimization is None: - print("⚠️ 优化系统未初始化,正在重新初始化...") - from core.vr.performance.optimization import VROptimization - self.optimization = VROptimization(self) - self.optimization._initialize_object_pools() - - # 初始化OpenVR - 使用Scene应用类型确保正确的焦点管理 - self.vr_system = openvr.init(openvr.VRApplication_Scene) - if not self.vr_system: - print("❌ 无法初始化OpenVR系统") - return False - - # 获取compositor - self.vr_compositor = openvr.VRCompositor() - if not self.vr_compositor: - print("❌ 无法获取VR Compositor") - return False - - # 获取推荐的渲染目标尺寸 - base_width, base_height = self.vr_system.getRecommendedRenderTargetSize() - self.base_eye_width = base_width - self.base_eye_height = base_height - - # 应用分辨率缩放 - self.scaled_eye_width = int(base_width * self.resolution_scale) - self.scaled_eye_height = int(base_height * self.resolution_scale) - - # 使用缩放后的分辨率作为实际渲染分辨率 - self.eye_width = self.scaled_eye_width - self.eye_height = self.scaled_eye_height - - self.current_eye_resolution = (self.eye_width, self.eye_height) - self.recommended_eye_resolution = (base_width, base_height) - - print(f"✓ VR基础分辨率: {base_width}x{base_height}") - print(f"✓ VR缩放系数: {self.resolution_scale}") - print(f"✓ VR实际分辨率: {self.eye_width}x{self.eye_height}") - print(f"📊 分辨率优化: {(1 - self.resolution_scale**2) * 100:.1f}% 像素减少") - - # 获取VR系统信息 - try: - # 获取显示频率 - self.vr_display_frequency = self.vr_system.getFloatTrackedDeviceProperty( - openvr.k_unTrackedDeviceIndex_Hmd, - openvr.Prop_DisplayFrequency_Float - ) - print(f"✓ VR显示频率: {self.vr_display_frequency} Hz") - - # 获取IPD(瞳距) - ipd = self.vr_system.getFloatTrackedDeviceProperty( - openvr.k_unTrackedDeviceIndex_Hmd, - openvr.Prop_UserIpdMeters_Float - ) - print(f"✓ IPD(瞳距): {ipd * 1000:.1f}mm") - - # 获取设备制造商和型号 - manufacturer = self.vr_system.getStringTrackedDeviceProperty( - openvr.k_unTrackedDeviceIndex_Hmd, - openvr.Prop_ManufacturerName_String - ) - model = self.vr_system.getStringTrackedDeviceProperty( - openvr.k_unTrackedDeviceIndex_Hmd, - openvr.Prop_ModelNumber_String - ) - print(f"✓ VR设备: {manufacturer} {model}") - - # 获取更多系统配置信息 - try: - # 获取刷新率相关信息 - seconds_since_last_vsync = self.vr_system.getFloatTrackedDeviceProperty( - openvr.k_unTrackedDeviceIndex_Hmd, - openvr.Prop_SecondsFromVsyncToPhotons_Float - ) - self.vsync_to_photons_ms = seconds_since_last_vsync * 1000 - print(f"✓ VSync到光子延迟: {self.vsync_to_photons_ms:.2f}ms") - - # 获取显示延迟信息 - display_refresh_rate = self.vr_display_frequency - if display_refresh_rate > 0: - self.target_frame_time_ms = 1000.0 / display_refresh_rate - print(f"✓ 目标帧时间: {self.target_frame_time_ms:.2f}ms") - - # 计算VSync时间窗口 - self.vsync_window_ms = self.target_frame_time_ms * 0.1 # 10%的窗口 - print(f"✓ VSync时间窗口: ±{self.vsync_window_ms:.2f}ms") - - except Exception as vsync_error: - print(f"⚠️ 获取VSync信息失败: {vsync_error}") - - # 检查是否启用了异步重投影 - try: - if hasattr(openvr, 'VRSettings'): - settings = openvr.VRSettings() - if settings: - self.async_reprojection_enabled = settings.getBool("steamvr", "enableAsyncReprojection") - print(f"✓ 异步重投影: {'启用' if self.async_reprojection_enabled else '禁用'}") - - self.motion_smoothing_enabled = settings.getBool("steamvr", "motionSmoothing") - print(f"✓ 运动平滑: {'启用' if self.motion_smoothing_enabled else '禁用'}") - - except Exception as settings_error: - print(f"⚠️ 获取VR设置失败: {settings_error}") - - except Exception as e: - print(f"⚠️ 获取VR系统信息失败: {e}") - - # 创建OpenVR姿态数组 - 分离渲染和游戏姿态 - poses_t = openvr.TrackedDevicePose_t * openvr.k_unMaxTrackedDeviceCount - self.poses = poses_t() # 渲染姿态(预测的) - self.game_poses = poses_t() # 游戏逻辑姿态(当前的) - print("✓ VR渲染和游戏姿态数组已创建") - - # 🔧 关键修复:统一初始化流程 - # 如果目标模式是RenderPipeline,先用普通模式初始化,然后切换 - # 这样两个场景都走相同的、已验证的代码路径 - target_render_mode = self.vr_render_mode - use_deferred_pipeline_switch = False - - if target_render_mode == VRRenderMode.RENDER_PIPELINE: - print("🎨 目标VR渲染模式: RenderPipeline") - print(" 策略:先用普通模式初始化,然后切换到RenderPipeline") - self.vr_render_mode = VRRenderMode.NORMAL - use_deferred_pipeline_switch = True - else: - print(f"🎨 VR渲染模式: {self.vr_render_mode.value}") - - # 创建VR渲染缓冲区 - 始终用普通模式初始化 - if not self._create_vr_buffers(): - print("❌ 创建VR渲染缓冲区失败") - return False - - # 设置VR相机 - if not self._setup_vr_cameras(): - print("❌ 设置VR相机失败") - return False - - # 优化VR渲染管线 - self._optimize_vr_rendering() - - # 初始化手柄控制器 - self._initialize_controllers() - - # 初始化动作系统 - 仅在启用时初始化 - if not self.disable_action_system and self.action_manager: - try: - if not self.action_manager.initialize(): - print("⚠️ VR动作系统初始化失败,但VR系统将继续运行") - except Exception as e: - print(f"⚠️ VR动作系统初始化异常: {e}") - else: - if self.disable_action_system: - print("🚫 VR动作系统已禁用,跳过初始化") - else: - print("⚠️ VR动作管理器未创建,跳过动作系统初始化") - - # 初始化交互系统 - 检查是否存在 - if self.interaction_manager: - try: - if not self.interaction_manager.initialize(): - print("⚠️ VR交互系统初始化失败,但VR系统将继续运行") - except Exception as e: - print(f"⚠️ VR交互系统初始化异常: {e}") - else: - print("⚠️ VR交互管理器未创建,跳过交互系统初始化") - - # 初始化VR传送系统 - if self.teleport_system: - try: - if not self.teleport_system.initialize(): - print("⚠️ VR传送系统初始化失败,但VR系统将继续运行") - except Exception as e: - print(f"⚠️ VR传送系统初始化异常: {e}") - else: - print("⚠️ VR传送系统未创建,跳过传送系统初始化") - - # 初始化VR摇杆系统 - if self.joystick_manager: - try: - # 传入传送系统引用给摇杆管理器 - self.joystick_manager.initialize(self.teleport_system) - print("✓ VR摇杆系统初始化成功") - except Exception as e: - print(f"⚠️ VR摇杆系统初始化异常: {e}") - else: - print("⚠️ VR摇杆管理器未创建,跳过摇杆系统初始化") - - # 可选:禁用异步重投影(备选方案) - if self.disable_async_reprojection: - self._disable_async_reprojection() - - # 启动VR更新任务 - self._start_vr_task() - - self.vr_initialized = True - print("✅ VR系统初始化成功(普通模式)") - - # 🔧 关键修复:如果目标是RenderPipeline模式,现在切换过去 - # 这样场景2(先设RP再进VR)会走场景1(先VR再设RP)相同的代码路径 - if use_deferred_pipeline_switch: - print("\n🔄 现在切换到目标渲染模式: RenderPipeline") - print(" 这将触发buffer重建和visualizer刷新...") - if self.set_vr_render_mode(target_render_mode): - print("✅ 已成功切换到RenderPipeline模式") - else: - print("⚠️ 切换到RenderPipeline失败,保持普通模式") - - return True - - except Exception as e: - print(f"❌ VR初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def _create_vr_buffers(self): - """创建VR渲染缓冲区 - 使用分辨率缩放优化""" - try: - print(f"🔧 创建VR缓冲区:") - print(f" 推荐分辨率: {self.base_eye_width}x{self.base_eye_height}") - print(f" 缩放系数: {self.resolution_scale}") - print(f" 实际分辨率: {self.eye_width}x{self.eye_height}") - print(f" 像素减少: {(1 - self.resolution_scale**2) * 100:.1f}%") - - # 创建左眼纹理和缓冲区 - self.vr_left_texture = self._create_vr_texture("VR Left Eye Texture") - self.vr_left_eye_buffer = self._create_vr_buffer( - "VR Left Eye", - self.vr_left_texture, - self.eye_width, - self.eye_height - ) - - if not self.vr_left_eye_buffer: - print("❌ 创建左眼缓冲区失败") - return False - - # 设置左眼缓冲区属性 - self.vr_left_eye_buffer.setSort(-100) - self.vr_left_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) # 深蓝色背景便于调试 - self.vr_left_eye_buffer.setActive(True) - - # 创建右眼纹理和缓冲区 - self.vr_right_texture = self._create_vr_texture("VR Right Eye Texture") - self.vr_right_eye_buffer = self._create_vr_buffer( - "VR Right Eye", - self.vr_right_texture, - self.eye_width, - self.eye_height - ) - - if not self.vr_right_eye_buffer: - print("❌ 创建右眼缓冲区失败") - return False - - # 设置右眼缓冲区属性 - self.vr_right_eye_buffer.setSort(-99) - self.vr_right_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) # 深蓝色背景便于调试 - self.vr_right_eye_buffer.setActive(True) - - # 🚀 关键优化:立即准备纹理并缓存OpenGL ID,避免后续重复准备 - print("🔧 准备纹理并缓存OpenGL ID...") - if self._prepare_and_cache_textures(): - print("✅ VR渲染缓冲区和纹理缓存创建成功") - return True - else: - print("❌ 纹理准备失败") - return False - - except Exception as e: - print(f"❌ 创建VR缓冲区失败: {e}") - import traceback - traceback.print_exc() - return False - - def _create_vr_texture(self, name): - """创建VR纹理对象 - 基于参考实现""" - texture = Texture(name) - texture.setWrapU(Texture.WMClamp) - texture.setWrapV(Texture.WMClamp) - texture.setMinfilter(Texture.FTLinear) - texture.setMagfilter(Texture.FTLinear) - return texture - - def _prepare_and_cache_textures(self): - """准备纹理并缓存OpenGL ID - 解决重复准备问题""" - try: - # 🔧 验证纹理对象存在 - if not self.vr_left_texture or not self.vr_right_texture: - print("❌ VR纹理对象不存在") - print(f" 左眼纹理: {self.vr_left_texture}") - print(f" 右眼纹理: {self.vr_right_texture}") - return False - - # 获取graphics state guardian和prepared objects - gsg = self.world.win.getGsg() - if not gsg: - print("❌ 无法获取GraphicsStateGuardian") - return False - - prepared_objects = gsg.getPreparedObjects() - if not prepared_objects: - print("❌ 无法获取PreparedObjects") - return False - - # 准备左眼纹理并缓存ID - print(f" 准备左眼纹理: {self.vr_left_texture.getXSize()}x{self.vr_left_texture.getYSize()}") - texture_context = self.vr_left_texture.prepareNow(0, prepared_objects, gsg) - if texture_context and hasattr(texture_context, 'getNativeId'): - self.left_texture_id = texture_context.getNativeId() - if self.left_texture_id > 0: - print(f" ✅ 左眼纹理准备完成 ({self.vr_render_mode.value}): ID={self.left_texture_id}") - else: - print(" ❌ 左眼纹理ID无效") - return False - else: - print(" ❌ 左眼纹理准备失败") - return False - - # 准备右眼纹理并缓存ID - print(f" 准备右眼纹理: {self.vr_right_texture.getXSize()}x{self.vr_right_texture.getYSize()}") - texture_context = self.vr_right_texture.prepareNow(0, prepared_objects, gsg) - if texture_context and hasattr(texture_context, 'getNativeId'): - self.right_texture_id = texture_context.getNativeId() - if self.right_texture_id > 0: - print(f" ✅ 右眼纹理准备完成 ({self.vr_render_mode.value}): ID={self.right_texture_id}") - else: - print(" ❌ 右眼纹理ID无效") - return False - else: - print(" ❌ 右眼纹理准备失败") - return False - - # 标记纹理已准备 - self.textures_prepared = True - print(" ✅ 纹理准备完成,ID已缓存,后续帧将直接使用缓存ID") - return True - - except Exception as e: - print(f"❌ 纹理准备和缓存失败: {e}") - import traceback - traceback.print_exc() - return False - - def _create_vr_buffer(self, name, texture, width, height): - """创建VR渲染缓冲区 - 基于参考实现 + 性能诊断""" - # 设置帧缓冲属性 - fbprops = FrameBufferProperties() - fbprops.setRgbaBits(1, 1, 1, 1) - # 可以在这里添加多重采样抗锯齿 - # fbprops.setMultisamples(4) - - # 创建缓冲区 - buffer = self.world.win.makeTextureBuffer(name, width, height, to_ram=False, fbp=fbprops) - - if buffer: - # 🔍 性能诊断:检查缓冲区类型和属性 - self._diagnose_buffer_performance(buffer, name, width, height) - - # 清除默认渲染纹理 - buffer.clearRenderTextures() - # 🚀 使用RTMBindOrCopy模式 - 已经是最优选择 - # RTMBindOrCopy会尝试直接绑定到纹理,只有硬件不支持时才回退到复制 - # 这已经是Panda3D中最高效的渲染到纹理模式 - buffer.addRenderTexture(texture, GraphicsOutput.RTMBindOrCopy, GraphicsOutput.RTPColor) - - else: - print(f"⚠️ VR缓冲区创建失败: {name} ({width}x{height})") - - return buffer - - def _diagnose_buffer_performance(self, buffer, name, width, height): - """诊断VR缓冲区性能特性""" - try: - # 检查缓冲区类型 - buffer_type = type(buffer).__name__ - - # 检查是否为parasite buffer(性能较差的类型) - is_parasite = 'Parasite' in buffer_type - - # 获取缓冲区信息 - is_valid = buffer.isValid() - is_hardware = hasattr(buffer, 'getGsg') and buffer.getGsg() is not None - - # 输出诊断信息(仅第一次创建时) - if not hasattr(self, '_buffer_diagnosis_shown'): - print(f"🔍 VR缓冲区诊断 [{name}]:") - print(f" 类型: {buffer_type}") - print(f" 分辨率: {width}x{height} ({width*height/1000000:.1f}M像素)") - print(f" 有效性: {'✓' if is_valid else '✗'}") - print(f" 硬件支持: {'✓' if is_hardware else '✗'}") - - if is_parasite: - print(f" ⚠️ 检测到Parasite Buffer - 性能可能受限") - print(f" 建议: 检查显卡是否支持真正的离屏渲染") - else: - print(f" ✓ 使用硬件FBO - 性能良好") - - # 显示帧缓冲区属性 - if hasattr(buffer, 'getFbProperties'): - fbp = buffer.getFbProperties() - if fbp: - print(f" 颜色位数: R{fbp.getRedBits()}G{fbp.getGreenBits()}B{fbp.getBlueBits()}A{fbp.getAlphaBits()}") - if hasattr(fbp, 'getMultisamples') and fbp.getMultisamples() > 0: - print(f" MSAA: {fbp.getMultisamples()}x") - - self._buffer_diagnosis_shown = True - - except Exception as e: - print(f"缓冲区诊断失败: {e}") - - def _create_vr_buffers_with_pipeline(self): - """创建带RenderPipeline的VR渲染缓冲区 - 高级渲染模式""" - try: - print(f"🎨 创建RenderPipeline VR缓冲区:") - print(f" 推荐分辨率: {self.base_eye_width}x{self.base_eye_height}") - print(f" Pipeline缩放: {self.pipeline_resolution_scale}") - - # 检查RenderPipeline是否可用 - if not hasattr(self.world, 'render_pipeline') or not self.world.render_pipeline: - print("❌ RenderPipeline未初始化,无法使用高级渲染模式") - return False - - pipeline = self.world.render_pipeline - - # 计算RenderPipeline模式下的分辨率 - pipeline_width = int(self.base_eye_width * self.pipeline_resolution_scale) - pipeline_height = int(self.base_eye_height * self.pipeline_resolution_scale) - - print(f" 实际分辨率: {pipeline_width}x{pipeline_height}") - print(f" 像素减少: {(1 - self.pipeline_resolution_scale**2) * 100:.1f}%") - - # 导入RenderPipeline相关模块 - try: - from RenderPipelineFile.rpcore.render_target import RenderTarget - except ImportError as e: - print(f"❌ 无法导入RenderPipeline模块: {e}") - return False - - # 导入VR stages模块 - try: - from core.vr.rendering.stages import VRPipelineController - except ImportError as e: - print(f"❌ 无法导入VR stages模块: {e}") - return False - - # 创建VR Pipeline控制器 - print(" 初始化VR Pipeline控制器...") - self.vr_pipeline_controller = VRPipelineController(pipeline) - - # 保存分辨率信息(将在_setup_vr_cameras中使用) - self.pipeline_vr_width = pipeline_width - self.pipeline_vr_height = pipeline_height - - # 应用RenderPipeline效果配置 - self._apply_pipeline_vr_effects() - - self.render_pipeline_enabled = True - print("✅ VR Pipeline控制器已初始化(将在相机设置时创建完整管线)") - - # 检查天空盒状态 - self._check_skybox_status() - - return True - - except Exception as e: - print(f"❌ 创建RenderPipeline VR缓冲区失败: {e}") - import traceback - traceback.print_exc() - return False - - def _apply_pipeline_vr_effects(self): - """为VR场景应用RenderPipeline效果配置""" - try: - print("🎨 应用RenderPipeline VR优化配置...") - - # 根据配置调整RenderPipeline设置 - config = self.pipeline_vr_config - - # 注意:实际的插件启用/禁用需要在pipeline.yaml中配置 - # 这里只是记录当前的VR优化意图 - print(f" 阴影: {'启用' if config['enable_shadows'] else '禁用'}") - print(f" 环境光遮蔽: {'启用' if config['enable_ao'] else '禁用'}") - print(f" 泛光效果: {'启用' if config['enable_bloom'] else '禁用'} (VR推荐禁用)") - print(f" 运动模糊: {'启用' if config['enable_motion_blur'] else '禁用'} (VR推荐禁用)") - print(f" 屏幕空间反射: {'启用' if config['enable_ssr'] else '禁用'} (VR推荐禁用)") - - print("✅ RenderPipeline VR效果配置完成") - - except Exception as e: - print(f"⚠️ 应用RenderPipeline VR效果失败: {e}") - - def _check_skybox_status(self): - """检查并报告天空盒状态""" - try: - print("\n🌌 检查天空盒状态...") - - # 方法1:在render节点下搜索所有名字包含skybox的节点 - skybox_nodes = self.world.render.findAllMatches("**/skybox*") - if skybox_nodes and skybox_nodes.getNumPaths() > 0: - print(f" ✓ 找到 {skybox_nodes.getNumPaths()} 个天空盒节点(使用名称搜索)") - for i in range(skybox_nodes.getNumPaths()): - skybox = skybox_nodes.getPath(i) - print(f" 天空盒 #{i+1}:") - print(f" 名称: {skybox.getName()}") - print(f" 位置: {skybox.getPos()}") - print(f" 缩放: {skybox.getScale()}") - print(f" 父节点: {skybox.getParent().getName()}") - - # 检查是否有shader - if skybox.hasShader(): - print(f" ✓ 已应用shader效果") - else: - print(f" ⚠️ 未应用shader效果") - - # 检查bin设置 - if skybox.hasBin(): - print(f" Bin: {skybox.getBinName()}") - else: - print(" ⚠️ 未找到天空盒(名称搜索)") - - # 方法2:搜索所有GeomNode(几何节点),天空盒通常是一个大的几何体 - print(" 搜索所有几何节点...") - all_geoms = self.world.render.findAllMatches("**/+GeomNode") - print(f" 找到 {all_geoms.getNumPaths()} 个几何节点") - - # 查找大型几何体(缩放值很大,可能是天空盒) - skybox_candidates = [] - for i in range(all_geoms.getNumPaths()): - geom_node = all_geoms.getPath(i) - scale = geom_node.getScale() - # 天空盒通常缩放很大(如40000) - if scale[0] > 1000 or scale[1] > 1000 or scale[2] > 1000: - print(f" ✓ 找到大型几何体(可能是天空盒):") - print(f" 名称: {geom_node.getName()}") - print(f" 缩放: {scale}") - print(f" 位置: {geom_node.getPos()}") - if geom_node.hasShader(): - print(f" ✓ 已应用shader") - skybox_candidates.append(geom_node) - - # 方法3:检查render节点的所有直接子节点(显示全部) - print(" 检查render的所有直接子节点...") - render_children = self.world.render.getChildren() - print(f" render有 {render_children.getNumPaths()} 个直接子节点:") - for i in range(render_children.getNumPaths()): - child = render_children.getPath(i) - scale = child.getScale() - # 标记大型节点(可能是天空盒) - if scale[0] > 1000 or scale[1] > 1000 or scale[2] > 1000: - print(f" - {child.getName()} ⭐ (大型节点,缩放: {scale})") - else: - print(f" - {child.getName()}") - - # 报告结果但不自动创建 - if len(skybox_candidates) == 0: - print(" ⚠️ 未找到明显的天空盒(缩放>1000的几何体)") - print(" 💡 RenderPipeline的天空盒可能在上述列表中,但缩放值不同") - print(" 💡 或者可能RenderPipeline未在VR模式下自动创建天空盒") - else: - print(f" ✓ 找到 {len(skybox_candidates)} 个可能的天空盒") - - print("🌌 天空盒状态检查完成\n") - - except Exception as e: - print(f"⚠️ 天空盒状态检查失败: {e}") - import traceback - traceback.print_exc() - - def _create_vr_skybox(self): - """为VR创建天空盒""" - try: - print(" 🎨 创建VR天空盒...") - - # 检查RenderPipeline是否可用 - if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline: - pipeline = self.world.render_pipeline - - # 使用RenderPipeline的load_default_skybox方法 - print(" 使用RenderPipeline方法创建天空盒...") - skybox = pipeline.common_resources.load_default_skybox() - - if skybox: - # 设置天空盒属性 - skybox.set_scale(40000) # 大型缩放 - skybox.reparent_to(self.world.render) - skybox.set_bin("unsorted", 10000) # 最后渲染 - - # 应用天空盒shader效果 - pipeline.set_effect(skybox, "effects/skybox.yaml", { - "render_shadow": False, - "render_envmap": False, - "render_voxelize": False, - "alpha_testing": False, - "normal_mapping": False, - "parallax_mapping": False - }, 1000) - - print(f" ✅ VR天空盒已创建") - print(f" 名称: {skybox.getName()}") - print(f" 缩放: {skybox.getScale()}") - print(f" 位置: {skybox.getPos()}") - return skybox - else: - print(" ❌ 无法加载天空盒模型") - return None - else: - print(" ❌ RenderPipeline未初始化,无法创建天空盒") - return None - - except Exception as e: - print(f" ❌ 创建VR天空盒失败: {e}") - import traceback - traceback.print_exc() - return None - - def _setup_vr_cameras(self): - """设置VR相机 - 使用锚点层级系统""" - try: - # 创建VR追踪空间锚点层级 - self.tracking_space = self.world.render.attachNewNode('tracking-space') - self.hmd_anchor = self.tracking_space.attachNewNode('hmd-anchor') - self.left_eye_anchor = self.hmd_anchor.attachNewNode('left-eye') - self.right_eye_anchor = self.hmd_anchor.attachNewNode('right-eye') - - # 获取投影矩阵 - projection_left = self.coord_mat_inv * self.convert_mat( - self.vr_system.getProjectionMatrix(openvr.Eye_Left, self.near_clip, self.far_clip)) - projection_right = self.coord_mat_inv * self.convert_mat( - self.vr_system.getProjectionMatrix(openvr.Eye_Right, self.near_clip, self.far_clip)) - - # 创建左眼相机节点 - left_cam_node = Camera('left-cam') - left_lens = MatrixLens() - left_lens.setUserMat(projection_left) - left_cam_node.setLens(left_lens) - - # 创建右眼相机节点 - right_cam_node = Camera('right-cam') - right_lens = MatrixLens() - right_lens.setUserMat(projection_right) - right_cam_node.setLens(right_lens) - - # 附加相机到眼睛锚点 - self.vr_left_camera = self.left_eye_anchor.attachNewNode(left_cam_node) - self.vr_right_camera = self.right_eye_anchor.attachNewNode(right_cam_node) - - # 设置显示区域 - 区分RenderPipeline和普通模式 - if self.vr_render_mode == VRRenderMode.RENDER_PIPELINE and self.vr_pipeline_controller: - # RenderPipeline模式:使用VRPipelineController创建完整管线 - print(" 使用VR Pipeline创建立体渲染管线...") - - # 创建立体渲染管线(GBuffer + Lighting + Final stages) - success = self.vr_pipeline_controller.create_stereo_pipeline( - self.pipeline_vr_width, - self.pipeline_vr_height, - self.vr_left_camera, - self.vr_right_camera - ) - - if not success: - print("❌ VR Pipeline创建失败") - return False - - # 获取GBuffer的内部buffer(用于DisplayRegion) - self.vr_left_eye_buffer = self.vr_pipeline_controller.get_left_buffer() - self.vr_right_eye_buffer = self.vr_pipeline_controller.get_right_buffer() - - # 🔧 关键修复:验证buffer有效性 - if not self.vr_left_eye_buffer or not self.vr_right_eye_buffer: - print("❌ VR Pipeline buffer创建失败") - print(f" 左眼buffer: {self.vr_left_eye_buffer}") - print(f" 右眼buffer: {self.vr_right_eye_buffer}") - return False - - # 获取最终输出纹理(用于提交到OpenVR) - left_textures = self.vr_pipeline_controller.get_left_textures() - right_textures = self.vr_pipeline_controller.get_right_textures() - - if left_textures and right_textures: - self.vr_left_texture = left_textures["final"] - self.vr_right_texture = right_textures["final"] - else: - print("❌ 无法获取VR Pipeline输出纹理") - return False - - # 设置缓冲区排序和清除颜色 - if self.vr_left_eye_buffer: - self.vr_left_eye_buffer.setSort(-100) - self.vr_left_eye_buffer.setClearColorActive(True) - self.vr_left_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) - - if self.vr_right_eye_buffer: - self.vr_right_eye_buffer.setSort(-99) - self.vr_right_eye_buffer.setClearColorActive(True) - self.vr_right_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) - - # 🔧 关键修复:为RenderPipeline的GBuffer DisplayRegion设置DrawCallback - # 这确保纹理在渲染完成后被提交到OpenVR - if self.vr_left_eye_buffer and self.vr_right_eye_buffer: - print(" 设置RenderPipeline DisplayRegion回调...") - try: - # 🔧 验证DisplayRegion数量 - left_dr_count = self.vr_left_eye_buffer.getNumDisplayRegions() - right_dr_count = self.vr_right_eye_buffer.getNumDisplayRegions() - - print(f" 左眼buffer DisplayRegion数量: {left_dr_count}") - print(f" 右眼buffer DisplayRegion数量: {right_dr_count}") - - if left_dr_count == 0 or right_dr_count == 0: - print(" ❌ DisplayRegion未创建,无法设置回调") - return False - - # 获取RenderTarget创建的DisplayRegion - left_dr = self.vr_left_eye_buffer.get_display_region(0) - right_dr = self.vr_right_eye_buffer.get_display_region(0) - - # 验证DisplayRegion有效性 - if not left_dr or not right_dr: - print(" ❌ DisplayRegion无效") - return False - - # 设置渲染回调(与普通模式一致) - left_dr.setDrawCallback(PythonCallbackObject(self.simple_left_cb)) - right_dr.setDrawCallback(PythonCallbackObject(self.simple_right_cb)) - - # 确保DisplayRegion处于活动状态 - left_dr.setActive(True) - right_dr.setActive(True) - - print(" ✅ RenderPipeline DisplayRegion回调已设置") - except Exception as e: - print(f" ⚠️ 设置DisplayRegion回调失败: {e}") - import traceback - traceback.print_exc() - return False - - # 🔧 关键修复:强制同步GraphicsEngine,确保所有buffer完全初始化 - print(" 同步GraphicsEngine...") - self.world.graphicsEngine.renderFrame() - - # 准备纹理并缓存OpenGL ID - if not self._prepare_and_cache_textures(): - print("❌ RenderPipeline纹理准备失败") - return False - - print("✅ VR Pipeline立体渲染管线已准备完成") - else: - # 普通模式:创建新的DisplayRegion - print(" 使用普通渲染区域...") - left_dr = self.vr_left_eye_buffer.makeDisplayRegion() - left_dr.setCamera(self.vr_left_camera) - left_dr.setActive(True) - left_dr.setDrawCallback(PythonCallbackObject(self.simple_left_cb)) - - right_dr = self.vr_right_eye_buffer.makeDisplayRegion() - right_dr.setCamera(self.vr_right_camera) - right_dr.setActive(True) - right_dr.setDrawCallback(PythonCallbackObject(self.simple_right_cb)) - - print("✓ VR相机锚点层级系统设置完成") - return True - - except Exception as e: - print(f"❌ 设置VR相机失败: {e}") - import traceback - traceback.print_exc() - return False - - def _get_eye_offset(self, eye): - """获取眼睛相对于头显的偏移""" - try: - if not self.vr_system: - # 使用标准IPD(瞳距)估算值 - ipd = 0.064 # 64mm,平均IPD - if eye == openvr.Eye_Left: - return Vec3(-ipd/2, 0, 0) - else: - return Vec3(ipd/2, 0, 0) - - # 从OpenVR获取眼睛到头显的变换矩阵 - eye_transform = self.vr_system.getEyeToHeadTransform(eye) - - # 提取位移信息 - x = eye_transform[0][3] - y = eye_transform[1][3] - z = eye_transform[2][3] - - return Vec3(x, y, z) - - except Exception as e: - print(f"❌ 获取眼睛偏移失败: {e}") - # 返回默认值 - ipd = 0.064 - if eye == openvr.Eye_Left: - return Vec3(-ipd/2, 0, 0) - else: - return Vec3(ipd/2, 0, 0) - - def _optimize_vr_rendering(self): - """优化VR模式下的渲染管线 - 减少不必要的后处理开销""" - try: - print("🔧 正在优化VR渲染管线...") - - # 检查是否有RenderPipeline - if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline: - print(" 检测到RenderPipeline,正在优化...") - - # 对VR缓冲区应用轻量级渲染设置 - if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: - self._apply_lightweight_rendering(self.vr_left_eye_buffer, "左眼") - - if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: - self._apply_lightweight_rendering(self.vr_right_eye_buffer, "右眼") - - print("✅ VR渲染管线优化完成") - else: - print(" 未检测到RenderPipeline,使用默认Panda3D渲染") - - # 禁用VR缓冲区的不必要功能 - self._disable_vr_buffer_extras() - - except Exception as e: - print(f"⚠️ VR渲染优化失败: {e}") - import traceback - traceback.print_exc() - - def _apply_lightweight_rendering(self, buffer, eye_name): - """为VR缓冲区应用轻量级渲染设置""" - try: - # 禁用多重采样抗锯齿(MSAA)以提升性能 - if hasattr(buffer, 'setMultisample'): - buffer.setMultisample(0) - print(f" {eye_name}: 禁用MSAA") - - # 设置更简单的清除颜色操作 - buffer.setClearColorActive(True) - buffer.setClearColor((0.1, 0.2, 0.4, 1.0)) # 深蓝色,便于调试 - - # 禁用深度写入到某些不需要的缓冲区 - # (保留主要深度测试,但减少不必要的写入) - - print(f" {eye_name}: 应用轻量级渲染设置") - - except Exception as e: - print(f"⚠️ {eye_name}缓冲区优化失败: {e}") - - def _disable_vr_buffer_extras(self): - """禁用VR缓冲区的额外功能以提升性能""" - try: - # 禁用VR缓冲区的统计收集 - if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: - if hasattr(self.vr_left_eye_buffer, 'setOneShot'): - self.vr_left_eye_buffer.setOneShot(False) - - if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: - if hasattr(self.vr_right_eye_buffer, 'setOneShot'): - self.vr_right_eye_buffer.setOneShot(False) - - print(" VR缓冲区额外功能已优化") - - except Exception as e: - print(f"⚠️ VR缓冲区额外功能优化失败: {e}") - - def _start_vr_task(self): - """启动VR更新任务""" - if self.vr_task: - self.world.taskMgr.remove(self.vr_task) - - # 设置高优先级(sort=-1000),确保在渲染之前执行,参考 panda3d-openvr - self.vr_task = self.world.taskMgr.add(self._update_vr, "update_vr", sort=-1000) - print("✓ VR更新任务已启动(高优先级,sort=-1000)") - - def _update_vr(self, task): - """VR更新任务 - 每帧调用(参考 panda3d-openvr 架构)""" - if not self.vr_enabled or not self.vr_system: - return task.cont - - try: - # 📌 第一件事:waitGetPoses 阻塞等待 VSync(参考 panda3d-openvr) - # 这会阻塞整个任务,确保每个 VSync 周期只执行一次 - should_call_waitgetposes = ( - not self.vr_test_mode or # 普通VR模式总是需要 - self.test_mode_wait_poses # 测试模式:仅当启用姿态等待时 - ) - - if should_call_waitgetposes: - self._wait_get_poses_immediate() - # 立即更新手柄,使用最新姿态(避免过时数据) - self.update_tracked_devices() - - # 性能监控 - self.frame_count += 1 - - # 🚀 自动启用性能模式 - 减少计时对象创建 - if (self.optimization and - not self.optimization.performance_mode_enabled and - self.frame_count >= self.optimization.performance_mode_trigger_frame): - self.optimization.performance_mode_enabled = True - if self.frame_count <= self.optimization.performance_mode_trigger_frame + 5: # 只输出一次 - print(f"🎯 性能模式已启用 (帧#{self.frame_count}) - 禁用详细监控以提升性能") - - # 记录帧时间 - self._track_frame_time() - - # 计算VR FPS - import time - current_time = time.time() - if self.last_fps_time == 0: - self.last_fps_time = current_time - elif current_time - self.last_fps_time >= 1.0: # 每秒更新一次FPS - self.vr_fps = (self.frame_count - self.last_fps_check) / (current_time - self.last_fps_time) - self.last_fps_check = self.frame_count - self.last_fps_time = current_time - - # 📌 使用刚获取的姿态更新相机(而不是旧姿态) - self._update_camera_poses() - - # 输出策略信息(仅第一次) - if not hasattr(self, '_new_architecture_logged'): - print("✅ 新架构已启用 - waitGetPoses 在任务开始阻塞,参考 panda3d-openvr") - print(" 这确保每个 VSync 周期只渲染一次,解决 AlreadySubmitted 错误") - self._new_architecture_logged = True - - # 更新VR动作状态 - 仅在启用时更新 - if not self.disable_action_system and self.action_manager: - self.action_manager.update_actions() - - # 更新VR交互系统 - if self.interaction_manager: - self.interaction_manager.update() - - # 更新VR摇杆系统 - if self.joystick_manager: - # 计算帧间隔时间 - import time - if not hasattr(self, '_last_frame_time'): - self._last_frame_time = time.time() - dt = 0.016 # 默认60fps - else: - current_time = time.time() - dt = current_time - self._last_frame_time - self._last_frame_time = current_time - # 限制dt范围,避免异常情况 - dt = max(0.001, min(0.1, dt)) - - self.joystick_manager.update(dt) - - # 更新系统性能指标(减少频率) - if self.frame_count % 30 == 1: # 每30帧更新一次,减少开销 - self._update_performance_metrics() - - # 更新GPU渲染时间统计(减少频率) - if self.enable_gpu_timing and self.frame_count % 60 == 1: - self._get_gpu_frame_timing() - - # 🚀 手动垃圾回收控制 - 避免16-19帧周期性峰值 - if self.optimization: - self.optimization._manual_gc_control() - - # 定期输出性能报告 - 默认10秒间隔 - report_interval = getattr(self, 'performance_report_interval', 600) - if self.frame_count % report_interval == 1: - self._print_performance_report() - - except Exception as e: - print(f"VR更新错误: {e}") - import traceback - traceback.print_exc() - - return task.cont - - def _sync_gpu_if_needed(self): - """可选的GPU同步 - 仅在需要时使用""" - try: - # 同步等待GPU完成(如果需要) - gsg = self.world.win.getGsg() - if gsg: - gsg.getEngine().syncFrame() - except Exception as e: - print(f"❌ GPU同步异常: {e}") - - def simple_left_cb(self, cbdata): - """简化的左眼渲染回调 - 精确控制渲染和提交""" - try: - # 🔍 精确测量渲染时间 - import time - render_start = time.perf_counter() - - # 触发实际渲染 - cbdata.upcall() - - # 计算真实渲染时间 - self.left_render_time = (time.perf_counter() - render_start) * 1000 # 转换为毫秒 - - # 记录渲染次数 - self.left_render_count += 1 - - # 📌 OpenVR 帧边界检查:防止同一 OpenVR 帧内重复渲染 - if self.left_eye_last_render_frame == self.openvr_frame_id: - return # 已在当前 OpenVR 帧渲染过,跳过 - - # 🔧 OpenVR最佳实践:左眼只渲染,不立即提交 - # 基于官方hellovr示例:两眼都渲染完后再批量提交 - self.left_eye_last_render_frame = self.openvr_frame_id - - # 渐进式VR功能测试:单独启用纹理提交时保留原逻辑 - should_submit = self.vr_test_mode and self.test_mode_submit_texture and not self.test_mode_wait_poses - - if should_submit: - # 测试模式:单独启用纹理提交时的兼容模式 - if self.vr_compositor and self.vr_left_texture: - self.submit_texture(openvr.Eye_Left, self.vr_left_texture) - - if self.vr_test_mode: - # 测试模式:始终触发屏幕显示更新 - if self.test_mode: - self.test_mode._update_test_display() - - except Exception as e: - print(f"左眼渲染回调错误: {e}") - - def simple_right_cb(self, cbdata): - """简化的右眼渲染回调 - 精确控制渲染和提交""" - try: - # 🔍 精确测量渲染时间 - import time - render_start = time.perf_counter() - - # 触发实际渲染 - cbdata.upcall() - - # 计算真实渲染时间 - self.right_render_time = (time.perf_counter() - render_start) * 1000 # 转换为毫秒 - - # 记录渲染次数 - self.right_render_count += 1 - - # 📌 OpenVR 帧边界检查:防止同一 OpenVR 帧内重复渲染 - if self.right_eye_last_render_frame == self.openvr_frame_id: - return # 已在当前 OpenVR 帧渲染过,跳过 - - # 🔧 OpenVR最佳实践:右眼渲染完成后批量提交两眼纹理 - # 基于官方hellovr示例:避免分散提交导致的VSync阻塞 - self.right_eye_last_render_frame = self.openvr_frame_id - - # 🔧 渐进式VR功能测试:根据调试标志决定启用哪些功能 - should_batch_submit = not self.vr_test_mode or self.test_mode_submit_texture - should_wait_poses = not self.vr_test_mode or self.test_mode_wait_poses - - # 特殊处理:测试模式单独启用功能时的兼容模式 - if self.vr_test_mode: - if self.test_mode_submit_texture and not self.test_mode_wait_poses: - # 单独测试纹理提交:使用原来的分散提交方式 - if self.vr_compositor and self.vr_right_texture: - self.submit_texture(openvr.Eye_Right, self.vr_right_texture) - - # 🚀 测试模式也需要PostPresentHandoff避免36FPS - try: - if hasattr(self.vr_compositor, 'postPresentHandoff'): - self.vr_compositor.postPresentHandoff() - elif hasattr(self.vr_compositor, 'PostPresentHandoff'): - self.vr_compositor.PostPresentHandoff() - except Exception as e: - pass # 测试模式静默忽略错误 - should_batch_submit = False - elif self.test_mode_wait_poses and not self.test_mode_submit_texture: - # 单独测试姿态等待:不提交纹理 - should_batch_submit = False - - if should_batch_submit: - # 🚀 OpenVR最佳实践:批量提交两眼纹理 - # 这是解决36FPS问题的关键修复 - self._batch_submit_textures() - - # 🔧 关键修复:移除Submit后立即WaitGetPoses的错误实现 - # 根据OpenVR官方文档:"Calling WaitGetPoses immediately after Submit is conspicuously wrong" - # WaitGetPoses应该在下一帧开始时通过update_vr_task调用,不是Submit后立即调用 - # - # if should_wait_poses: - # # 错误的实现:Submit后立即获取姿态导致36FPS - # self._wait_get_poses_immediate() # ← 这是36FPS的根本原因! - # - # 正确的实现:让update_vr_task在下一帧开始时调用WaitGetPoses - - if self.vr_test_mode: - # 测试模式:始终更新性能HUD - if self.test_mode: - self.test_mode._update_test_performance_hud() - - except Exception as e: - print(f"右眼渲染回调错误: {e}") - - def _wait_get_poses(self): - """调用VRCompositor的waitGetPoses来获取焦点和姿态数据""" - try: - if not self.vr_compositor or not self.poses: - return - - # 调用waitGetPoses获取焦点和姿态数据 - # 这个调用可能会阻塞直到下一个VR同步点 - result = self.vr_compositor.waitGetPoses(self.poses, None) - - # 检查姿态数据的有效性 - valid_poses = 0 - - # 更新HMD姿态(设备0通常是头显) - if len(self.poses) > 0 and self.poses[0].bPoseIsValid: - valid_poses += 1 - else: - # 如果HMD姿态无效,不要频繁输出错误信息 - if not hasattr(self, '_hmd_invalid_warning_shown'): - print("⚠️ HMD姿态数据无效") - self._hmd_invalid_warning_shown = True - - # 更新控制器姿态 - self.controller_poses.clear() - for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): - if self.poses[device_id].bPoseIsValid: - device_class = self.vr_system.getTrackedDeviceClass(device_id) - if device_class == openvr.TrackedDeviceClass_Controller: - controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking - self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) - valid_poses += 1 - - # 性能监控 - 偶尔输出姿态状态 - if self.frame_count % 600 == 1: # 每10秒输出一次@60fps - print(f"📊 VR姿态状态 - 有效姿态数: {valid_poses}, 总帧数: {self.frame_count}") - - except Exception as e: - # 限制错误输出频率 - if not hasattr(self, '_last_error_frame'): - self._last_error_frame = 0 - - if self.frame_count - self._last_error_frame > 300: # 每5秒最多输出一次错误 - print(f"waitGetPoses失败: {e}") - self._last_error_frame = self.frame_count - - # 记录姿态失败次数 - self.pose_failures += 1 - - def _wait_get_poses_immediate(self): - """立即获取VR姿态 - 修复ATW闪烁的关键方法(双姿态版本)""" - # 开始计时waitGetPoses操作 - timing = self._start_timing('wait_poses') - - try: - if not self.vr_compositor or not self.poses or not self.game_poses: - self._end_timing(timing) - return - - # 关键修复:传递渲染姿态和游戏姿态数组 - # 渲染姿态用于绘制,游戏姿态用于逻辑,避免ATW过度补偿 - result = self.vr_compositor.waitGetPoses(self.poses, self.game_poses) - - # 📌 waitGetPoses 成功后立即递增 OpenVR 帧ID(新帧开始) - self.openvr_frame_id += 1 - - # 结束计时 - wait_time = self._end_timing(timing) - - # 检查姿态数据的有效性 - valid_poses = 0 - - # 更新HMD姿态(设备0通常是头显) - if len(self.poses) > 0 and self.poses[0].bPoseIsValid: - valid_poses += 1 - else: - # 如果HMD姿态无效,不要频繁输出错误信息 - if not hasattr(self, '_hmd_invalid_warning_shown'): - print("⚠️ HMD姿态数据无效(立即模式)") - self._hmd_invalid_warning_shown = True - - # 🚀 优化控制器姿态更新:使用缓存,避免每帧clear()和重新创建对象 - for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): - if self.poses[device_id].bPoseIsValid: - device_class = self.vr_system.getTrackedDeviceClass(device_id) - if device_class == openvr.TrackedDeviceClass_Controller: - controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking - - # 检查是否已有此设备的缓存矩阵 - if device_id not in self.controller_poses: - # 第一次检测到该控制器,创建新的缓存矩阵 - self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) - else: - # 复用现有矩阵,只更新数值,避免创建新对象 - cached_mat = self.controller_poses[device_id] - self._update_matrix_from_openvr(cached_mat, controller_matrix) - - valid_poses += 1 - else: - # 设备姿态无效,从字典中移除(如果存在) - if device_id in self.controller_poses: - # 将矩阵返回对象池 - if self.optimization: - self.optimization._return_pooled_matrix(self.controller_poses[device_id]) - del self.controller_poses[device_id] - - # 🔧 关键修复:立即更新手柄和跟踪设备 - # Running Start模式下必须在WaitGetPoses后立即更新,避免手柄消失 - self.update_tracked_devices() - - # 调试信息 - 仅在第一次成功时输出 - if not hasattr(self, '_dual_pose_mode_logged') and valid_poses > 0: - print(f"✅ 双姿态立即获取模式启用 - 有效姿态数: {valid_poses}") - print(" 渲染姿态用于绘制,游戏姿态用于逻辑,防止ATW过度补偿") - print(" 手柄和跟踪设备在WaitGetPoses后立即更新") - self._dual_pose_mode_logged = True - - except Exception as e: - # 限制错误输出频率 - if not hasattr(self, '_last_immediate_error_frame'): - self._last_immediate_error_frame = 0 - - if self.frame_count - self._last_immediate_error_frame > 300: # 每5秒最多输出一次错误 - print(f"立即姿态获取失败: {e}") - self._last_immediate_error_frame = self.frame_count - - self.pose_failures += 1 - - def _wait_get_poses_with_prediction(self): - """使用预测时间获取VR姿态 - 优化性能的新策略""" - try: - if not self.vr_compositor or not self.poses or not self.game_poses: - return - - # 使用预测时间获取姿态 - # 预测时间通常为11-16ms,对应下一个VR帧的时间 - result = self.vr_compositor.waitGetPoses(self.poses, self.game_poses) - - # 检查姿态数据的有效性 - valid_poses = 0 - - # 更新HMD姿态(设备0通常是头显) - if len(self.poses) > 0 and self.poses[0].bPoseIsValid: - valid_poses += 1 - else: - # 如果HMD姿态无效,不要频繁输出错误信息 - if not hasattr(self, '_hmd_invalid_warning_task'): - print("⚠️ HMD姿态数据无效(更新任务模式)") - self._hmd_invalid_warning_task = True - - # 更新控制器姿态 - self.controller_poses.clear() - for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): - if self.poses[device_id].bPoseIsValid: - device_class = self.vr_system.getTrackedDeviceClass(device_id) - if device_class == openvr.TrackedDeviceClass_Controller: - controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking - self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) - valid_poses += 1 - - # 调试信息 - 仅在第一次成功时输出 - if not hasattr(self, '_update_task_mode_logged') and valid_poses > 0: - print(f"✅ 更新任务姿态获取模式启用 - 有效姿态数: {valid_poses}") - print(f" 预测时间: {self.use_prediction_time*1000:.1f}ms") - self._update_task_mode_logged = True - - except Exception as e: - # 限制错误输出频率 - if not hasattr(self, '_last_task_error_frame'): - self._last_task_error_frame = 0 - - if self.frame_count - self._last_task_error_frame > 300: # 每5秒最多输出一次错误 - print(f"更新任务姿态获取失败: {e}") - self._last_task_error_frame = self.frame_count - - # 记录姿态失败次数 - self.pose_failures += 1 - - def _cache_poses_for_next_frame(self): - """缓存当前姿态供下一帧渲染使用 - 修复时序不匹配""" - try: - if not self.poses or len(self.poses) == 0: - return - - # 如果是第一帧,直接使用当前姿态 - if self._first_frame: - self._cached_render_poses = self.poses - self._first_frame = False - print("✓ 首帧姿态缓存已设置") - return - - # 复制当前渲染姿态到缓存 - # 下一帧将使用这些姿态进行渲染 - poses_t = openvr.TrackedDevicePose_t * openvr.k_unMaxTrackedDeviceCount - cached_poses = poses_t() - - # 复制姿态数据 - for i in range(len(self.poses)): - cached_poses[i] = self.poses[i] - - self._cached_render_poses = cached_poses - - except Exception as e: - print(f"⚠️ 姿态缓存失败: {e}") - - - def _reset_waitgetposes_flag(self, task): - """重置WaitGetPoses标记 - 确保下一帧可以调用WaitGetPoses""" - self._waitgetposes_called_this_frame = False - return task.done - - def _update_tracking_data(self): - """更新VR追踪数据""" - try: - # 获取设备姿态 - poses = self.vr_system.getDeviceToAbsoluteTrackingPose( - openvr.TrackingUniverseStanding, 0.0, openvr.k_unMaxTrackedDeviceCount - ) - - # 更新HMD姿态(设备0通常是头显) - if poses[0].bPoseIsValid: - hmd_matrix = poses[0].mDeviceToAbsoluteTracking - self.hmd_pose = self._convert_openvr_matrix_to_panda(hmd_matrix) - - # 更新控制器姿态 - for device_id in range(1, openvr.k_unMaxTrackedDeviceCount): - if poses[device_id].bPoseIsValid: - device_class = self.vr_system.getTrackedDeviceClass(device_id) - if device_class == openvr.TrackedDeviceClass_Controller: - controller_matrix = poses[device_id].mDeviceToAbsoluteTracking - self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) - - except Exception as e: - print(f"更新追踪数据失败: {e}") - - def _convert_openvr_matrix_to_panda(self, ovr_matrix): - """将OpenVR矩阵转换为Panda3D矩阵 - 使用对象池优化 - - 坐标系转换: - OpenVR: X右, Y上, -Z前(右手坐标系) - Panda3D: X右, Y前, Z上(右手坐标系) - - 转换规则: - OpenVR X → Panda3D X - OpenVR Y → Panda3D Z - OpenVR -Z → Panda3D Y - """ - # 🚀 使用对象池获取预分配的Mat4对象,避免每帧创建新对象 - if self.optimization: - mat = self.optimization._get_pooled_matrix() - else: - mat = Mat4() - - # 修正的坐标转换矩阵 - # OpenVR: X右, Y上, -Z前 → Panda3D: X右, Y前, Z上 - # 转换规则: (ovr_x, ovr_y, ovr_z) → (panda_x, panda_y, panda_z) - # (ovr_x, ovr_y, ovr_z) → (ovr_x, -ovr_z, ovr_y) - - # X轴行:Panda3D的X轴对应OpenVR的X轴 - mat.setCell(0, 0, ovr_matrix[0][0]) # X_x → X_x - mat.setCell(0, 1, ovr_matrix[0][1]) # X_y → X_y - mat.setCell(0, 2, ovr_matrix[0][2]) # X_z → X_z - mat.setCell(0, 3, ovr_matrix[0][3]) # 位移X分量 - - # Y轴行:Panda3D的Y轴对应OpenVR的-Z轴 - mat.setCell(1, 0, -ovr_matrix[2][0]) # -Z_x → Y_x - mat.setCell(1, 1, -ovr_matrix[2][1]) # -Z_y → Y_y - mat.setCell(1, 2, -ovr_matrix[2][2]) # -Z_z → Y_z - mat.setCell(1, 3, -ovr_matrix[2][3]) # 位移Y分量(-Z位移) - - # Z轴行:Panda3D的Z轴对应OpenVR的Y轴 - mat.setCell(2, 0, ovr_matrix[1][0]) # Y_x → Z_x - mat.setCell(2, 1, ovr_matrix[1][1]) # Y_y → Z_y - mat.setCell(2, 2, ovr_matrix[1][2]) # Y_z → Z_z - mat.setCell(2, 3, ovr_matrix[1][3]) # 位移Z分量(Y位移) - - # 齐次坐标 - mat.setCell(3, 0, 0) - mat.setCell(3, 1, 0) - mat.setCell(3, 2, 0) - mat.setCell(3, 3, 1) - - # 🚀 优化调试信息 - 避免创建Vec3对象,减少GC压力 - if not hasattr(self, '_coord_debug_counter'): - self._coord_debug_counter = 0 - self._coord_debug_counter += 1 - - if self._coord_debug_counter % 600 == 1: # 每10秒输出一次@60fps - print(f"🔄 坐标系转换调试 (第{self._coord_debug_counter}帧)") - - # 直接输出数值,避免创建Vec3对象 - ovr_x, ovr_y, ovr_z = ovr_matrix[0][3], ovr_matrix[1][3], ovr_matrix[2][3] - print(f" OpenVR原始位置: ({ovr_x:.3f}, {ovr_y:.3f}, {ovr_z:.3f})") - - # 直接从矩阵读取数值,避免创建Vec3对象 - panda_x = mat.getCell(0, 3) - panda_y = mat.getCell(1, 3) - panda_z = mat.getCell(2, 3) - print(f" Panda3D转换位置: ({panda_x:.3f}, {panda_y:.3f}, {panda_z:.3f})") - - # 手动验证转换:OpenVR (x,y,z) → Panda3D (x,-z,y) - expected_x = ovr_x - expected_y = -ovr_z - expected_z = ovr_y - print(f" 预期转换结果: ({expected_x:.3f}, {expected_y:.3f}, {expected_z:.3f})") - - # 计算误差,避免创建Vec3对象 - diff_x = panda_x - expected_x - diff_y = panda_y - expected_y - diff_z = panda_z - expected_z - diff_magnitude = (diff_x*diff_x + diff_y*diff_y + diff_z*diff_z)**0.5 - - if diff_magnitude < 0.001: - print(f" ✅ 坐标转换正确 (误差: {diff_magnitude:.6f})") - else: - print(f" ⚠️ 坐标转换可能有误 (误差: {diff_magnitude:.6f})") - print(f" 差异: ({diff_x:.6f}, {diff_y:.6f}, {diff_z:.6f})") - - return mat - - def update_hmd(self, pose): - """ - 更新HMD锚点 - 基于参考实现 - """ - try: - # 将OpenVR姿态转换为Panda3D矩阵 - modelview = self.convert_mat(pose.mDeviceToAbsoluteTracking) - - # 应用坐标系转换并设置HMD锚点 - self.hmd_anchor.setMat(self.coord_mat_inv * modelview * self.coord_mat) - - # 获取眼睛到头部的变换 - view_left = self.convert_mat(self.vr_system.getEyeToHeadTransform(openvr.Eye_Left)) - view_right = self.convert_mat(self.vr_system.getEyeToHeadTransform(openvr.Eye_Right)) - - # 设置眼睛锚点 - self.left_eye_anchor.setMat(self.coord_mat_inv * view_left * self.coord_mat) - self.right_eye_anchor.setMat(self.coord_mat_inv * view_right * self.coord_mat) - - except Exception as e: - print(f"更新HMD姿态失败: {e}") - - def _update_camera_poses(self): - """更新相机姿态 - 使用锚点系统简化处理""" - try: - # 使用锚点系统后,相机位置自动跟随锚点 - # 只需要获取HMD姿态并更新锚点即可 - - # 从poses数组中获取HMD姿态 - if hasattr(self, 'poses') and len(self.poses) > 0: - hmd_pose = self.poses[openvr.k_unTrackedDeviceIndex_Hmd] - if hmd_pose.bPoseIsValid: - self.update_hmd(hmd_pose) - else: - print("⚠️ HMD姿态数据无效") - - except Exception as e: - print(f"更新相机姿态失败: {e}") - import traceback - traceback.print_exc() - - def _update_camera_poses_with_cache(self): - """使用缓存姿态更新相机 - 符合OpenVR时序假设""" - try: - # 使用缓存的姿态,符合OpenVR的时序假设 - # OpenVR假设你用上一次WaitGetPoses的姿态渲染当前提交的帧 - - if not self._cached_render_poses: - # 如果没有缓存姿态,回退到当前姿态(首帧情况) - print("⚠️ 没有缓存姿态,使用当前姿态") - return self._update_camera_poses() - - # 从缓存姿态数组中获取HMD姿态 - if len(self._cached_render_poses) > 0: - hmd_pose = self._cached_render_poses[openvr.k_unTrackedDeviceIndex_Hmd] - if hmd_pose.bPoseIsValid: - self.update_hmd(hmd_pose) - - # 调试信息 - 验证缓存姿态使用 - if not hasattr(self, '_cached_pose_logged'): - print("✅ 使用缓存姿态更新相机 - 符合OpenVR时序假设") - self._cached_pose_logged = True - else: - print("⚠️ 缓存的HMD姿态数据无效") - - except Exception as e: - print(f"使用缓存姿态更新相机失败: {e}") - # 回退到正常更新方式 - self._update_camera_poses() - import traceback - traceback.print_exc() - - - def enable_vr(self): - """启用VR模式""" - if not self.is_vr_available(): - print("❌ VR系统不可用") - return False - - if not self.vr_initialized: - if not self.initialize_vr(): - return False - - self.vr_enabled = True - - # 禁用主相机避免干扰VR渲染 - self._disable_main_cam() - - # VR性能优化:使用Running Start模式(Valve最佳实践) - print("🚀 VR性能优化:Running Start模式已启用") - print(" 优势:在帧开始时获取姿态,提供VSync前3ms的渲染时间") - print(" 注意:Submit后立即调用WaitGetPoses是错误实现") - self.set_prediction_time(11.0) # 11ms预测时间 - OpenVR标准值,平衡准确性和延迟 - - # 🚀 动态调整Qt Timer频率以支持VR - if hasattr(self.world, 'qtWidget') and self.world.qtWidget: - if hasattr(self.world.qtWidget, 'synchronizer'): - # 设置为144Hz,让OpenVR控制实际渲染节奏 - self.world.qtWidget.synchronizer.setInterval(int(1000/144)) - print("✓ Qt Timer调整为144Hz,让OpenVR控制VR渲染节奏") - - # 🔧 关键修复:检测并重建缺失的手柄visualizer - # 当渲染模式切换时,visualizer可能被清理但控制器对象仍存在 - if hasattr(self, 'left_controller') and self.left_controller: - if not self.left_controller.visualizer and self.left_controller.anchor_node: - print("🔧 检测到左手柄visualizer缺失,正在重建...") - self.left_controller._create_visualizer() - if self.left_controller.visualizer: - print("✅ 左手柄visualizer已重建") - - if hasattr(self, 'right_controller') and self.right_controller: - if not self.right_controller.visualizer and self.right_controller.anchor_node: - print("🔧 检测到右手柄visualizer缺失,正在重建...") - self.right_controller._create_visualizer() - if self.right_controller.visualizer: - print("✅ 右手柄visualizer已重建") - - print("✅ VR模式已启用") - return True - - def disable_vr(self): - """禁用VR模式""" - self.vr_enabled = False - - # 清理手柄可视化(但保留控制器对象以便重新启用) - if hasattr(self, 'left_controller') and self.left_controller: - if hasattr(self.left_controller, 'visualizer') and self.left_controller.visualizer: - try: - self.left_controller.visualizer.cleanup() - self.left_controller.visualizer = None - print("✓ 左手控制器可视化已清理") - except Exception as e: - print(f"⚠️ 清理左手控制器可视化失败: {e}") - - if hasattr(self, 'right_controller') and self.right_controller: - if hasattr(self.right_controller, 'visualizer') and self.right_controller.visualizer: - try: - self.right_controller.visualizer.cleanup() - self.right_controller.visualizer = None - print("✓ 右手控制器可视化已清理") - except Exception as e: - print(f"⚠️ 清理右手控制器可视化失败: {e}") - - # 隐藏手柄锚点节点 - if hasattr(self, 'left_controller') and self.left_controller and self.left_controller.anchor_node: - self.left_controller.anchor_node.hide() - - if hasattr(self, 'right_controller') and self.right_controller and self.right_controller.anchor_node: - self.right_controller.anchor_node.hide() - - # 恢复主相机 - self._enable_main_cam() - - # 恢复Qt Timer到60FPS - if hasattr(self.world, 'qtWidget') and self.world.qtWidget: - if hasattr(self.world.qtWidget, 'synchronizer'): - self.world.qtWidget.synchronizer.setInterval(int(1000/60)) - print("✓ Qt Timer恢复为60Hz") - - print("✅ VR模式已禁用,手柄模型已隐藏") - - def set_vr_render_mode(self, mode): - """切换VR渲染模式 - - Args: - mode: VRRenderMode枚举值或字符串 ('normal' 或 'render_pipeline') - - Returns: - bool: 切换是否成功 - """ - try: - # 转换输入为枚举类型 - if isinstance(mode, str): - mode_str = mode.lower() - if mode_str == "normal": - new_mode = VRRenderMode.NORMAL - elif mode_str in ["render_pipeline", "renderpipeline", "pipeline"]: - new_mode = VRRenderMode.RENDER_PIPELINE - else: - print(f"❌ 无效的渲染模式: {mode}") - print(" 支持的模式: 'normal' 或 'render_pipeline'") - return False - elif isinstance(mode, VRRenderMode): - new_mode = mode - else: - print(f"❌ 无效的模式类型: {type(mode)}") - return False - - # 检查是否与当前模式相同 - if new_mode == self.vr_render_mode: - print(f"✓ VR渲染模式已经是 {new_mode.value},无需切换") - return True - - print(f"🔄 正在切换VR渲染模式: {self.vr_render_mode.value} → {new_mode.value}") - - # 检查VR是否已初始化 - if not self.vr_initialized: - print("⚠️ VR未初始化,仅更新渲染模式配置") - self.vr_render_mode = new_mode - print(f"✓ VR渲染模式已更新为 {new_mode.value}(下次启动VR时生效)") - return True - - # 保存当前VR启用状态 - was_enabled = self.vr_enabled - - # 如果VR已启用,先禁用 - if was_enabled: - print(" 暂时禁用VR...") - self.disable_vr() - - # 更新渲染模式 - old_mode = self.vr_render_mode - self.vr_render_mode = new_mode - - # 清理现有缓冲区 - print(" 清理现有渲染缓冲区...") - self._cleanup_vr_buffers() - - # 根据新模式重建缓冲区 - print(f" 创建新的渲染缓冲区({new_mode.value})...") - success = False - - if new_mode == VRRenderMode.RENDER_PIPELINE: - success = self._create_vr_buffers_with_pipeline() - if not success: - print("⚠️ RenderPipeline模式创建失败,回退到普通渲染模式") - self.vr_render_mode = VRRenderMode.NORMAL - success = self._create_vr_buffers() - else: - success = self._create_vr_buffers() - - if not success: - print("❌ 缓冲区创建失败,尝试恢复原模式") - self.vr_render_mode = old_mode - if old_mode == VRRenderMode.RENDER_PIPELINE: - self._create_vr_buffers_with_pipeline() - else: - self._create_vr_buffers() - return False - - # 重新设置相机 - print(" 重新设置VR相机...") - if not self._setup_vr_cameras(): - print("❌ 相机设置失败") - return False - - # 如果之前VR是启用的,重新启用 - if was_enabled: - print(" 重新启用VR...") - self.enable_vr() - - # 🔧 关键修复:重建所有手柄的visualizer以适配新渲染模式 - print(" 刷新手柄visualizer以适配新渲染模式...") - if hasattr(self, 'left_controller') and self.left_controller: - self.left_controller.recreate_visualizer() - - if hasattr(self, 'right_controller') and self.right_controller: - self.right_controller.recreate_visualizer() - - print(f"✅ VR渲染模式已切换为 {self.vr_render_mode.value}") - - # 保存配置 - if self.config_manager: - self.config_manager.save_from_vr_manager(self) - - return True - - except Exception as e: - print(f"❌ 切换VR渲染模式失败: {e}") - import traceback - traceback.print_exc() - return False - - def get_vr_render_mode(self): - """获取当前VR渲染模式 - - Returns: - VRRenderMode: 当前渲染模式 - """ - return self.vr_render_mode - - def _cleanup_vr_buffers(self): - """清理VR渲染缓冲区""" - try: - # 清理VR Pipeline Controller(如果使用RenderPipeline模式) - if self.vr_pipeline_controller: - print(" 清理VR Pipeline...") - self.vr_pipeline_controller.cleanup_all() - self.vr_pipeline_controller = None - - # 清理左眼缓冲区 - if self.vr_left_eye_buffer: - # 如果是RenderPipeline模式,buffer已被VRPipelineController清理 - # 如果是普通模式,需要手动清理 - if self.vr_render_mode == VRRenderMode.NORMAL: - self.world.graphicsEngine.removeWindow(self.vr_left_eye_buffer) - self.vr_left_eye_buffer = None - - # 清理右眼缓冲区 - if self.vr_right_eye_buffer: - if self.vr_render_mode == VRRenderMode.NORMAL: - self.world.graphicsEngine.removeWindow(self.vr_right_eye_buffer) - self.vr_right_eye_buffer = None - - # 清理纹理 - self.vr_left_texture = None - self.vr_right_texture = None - self.left_texture_id = None - self.right_texture_id = None - self.textures_prepared = False - - # 清理RenderPipeline渲染目标(旧版,保留兼容性) - if self.vr_pipeline_left_target: - self.vr_pipeline_left_target = None - - if self.vr_pipeline_right_target: - self.vr_pipeline_right_target = None - - self.render_pipeline_enabled = False - - print("✓ VR渲染缓冲区已清理") - - except Exception as e: - print(f"⚠️ 清理VR缓冲区时出错: {e}") - - def cleanup(self): - """清理VR资源""" - try: - print("🔄 正在清理VR资源...") - - # 停止VR任务 - if self.vr_task: - self.world.taskMgr.remove(self.vr_task) - self.vr_task = None - - # 🚀 恢复Python垃圾回收并清理对象池 - if self.optimization and self.optimization._gc_disabled: - # 最后一次手动垃圾回收 - collected = gc.collect() - print(f"🗑️ 最终GC清理: {collected} 个对象") - - # 恢复自动垃圾回收 - gc.enable() - self.optimization._gc_disabled = False - print("✅ Python垃圾回收已恢复为自动模式") - - # 清理对象池 - if self.optimization and hasattr(self.optimization, '_matrix_pool'): - pool_size = len(self.optimization._matrix_pool) - self.optimization._matrix_pool.clear() - print(f"🧹 Mat4对象池已清理: {pool_size} 个对象") - - # 清理缓存 - if self.optimization and hasattr(self.optimization, '_cached_matrices'): - self.optimization._cached_matrices.clear() - if self.optimization and hasattr(self.optimization, '_controller_poses_cache'): - self.optimization._controller_poses_cache.clear() - - # 清理OpenVR Texture对象缓存 - # 清理OpenVR Texture对象缓存 - if self.optimization: - self.optimization._left_ovr_texture = None - self.optimization._right_ovr_texture = None - print("🧹 OpenVR Texture对象缓存已清理") - - # 清理渲染缓冲区 - if self.vr_left_eye_buffer: - self.vr_left_eye_buffer.removeAllDisplayRegions() - self.world.graphicsEngine.removeWindow(self.vr_left_eye_buffer) - self.vr_left_eye_buffer = None - - if self.vr_right_eye_buffer: - self.vr_right_eye_buffer.removeAllDisplayRegions() - self.world.graphicsEngine.removeWindow(self.vr_right_eye_buffer) - self.vr_right_eye_buffer = None - - # 清理相机 - if self.vr_left_camera: - self.vr_left_camera.removeNode() - self.vr_left_camera = None - - if self.vr_right_camera: - self.vr_right_camera.removeNode() - self.vr_right_camera = None - - # 清理控制器 - if hasattr(self, 'left_controller') and self.left_controller: - try: - self.left_controller.cleanup() - self.left_controller = None - print("✓ 左手控制器已清理") - except Exception as e: - print(f"⚠️ 清理左手控制器失败: {e}") - - if hasattr(self, 'right_controller') and self.right_controller: - try: - self.right_controller.cleanup() - self.right_controller = None - print("✓ 右手控制器已清理") - except Exception as e: - print(f"⚠️ 清理右手控制器失败: {e}") - - # 清理控制器字典 - if hasattr(self, 'controllers'): - self.controllers.clear() - print("✓ 控制器字典已清理") - - # 清理VR子系统 - if hasattr(self, 'joystick_manager') and self.joystick_manager: - try: - self.joystick_manager.cleanup() - self.joystick_manager = None - print("✓ VR摇杆系统已清理") - except Exception as e: - print(f"⚠️ 清理VR摇杆系统失败: {e}") - - if hasattr(self, 'teleport_system') and self.teleport_system: - try: - self.teleport_system.cleanup() - self.teleport_system = None - print("✓ VR传送系统已清理") - except Exception as e: - print(f"⚠️ 清理VR传送系统失败: {e}") - - if hasattr(self, 'interaction_manager') and self.interaction_manager: - try: - self.interaction_manager.cleanup() - self.interaction_manager = None - print("✓ VR交互系统已清理") - except Exception as e: - print(f"⚠️ 清理VR交互系统失败: {e}") - - if hasattr(self, 'performance_monitor') and self.performance_monitor: - try: - # 性能监控系统不需要特殊清理,只需要置空引用 - self.performance_monitor = None - print("✓ VR性能监控系统已清理") - except Exception as e: - print(f"⚠️ 清理VR性能监控系统失败: {e}") - - if hasattr(self, 'action_manager') and self.action_manager: - try: - self.action_manager.cleanup() - self.action_manager = None - print("✓ VR动作系统已清理") - except Exception as e: - print(f"⚠️ 清理VR动作系统失败: {e}") - - # 关闭OpenVR - if self.vr_system and OPENVR_AVAILABLE: - try: - openvr.shutdown() - except: - pass - self.vr_system = None - - self.vr_enabled = False - self.vr_initialized = False - - print("✅ VR资源清理完成") - - except Exception as e: - print(f"⚠️ VR清理过程中出错: {e}") - - def get_vr_status(self): - """获取VR状态信息""" - return { - 'available': self.is_vr_available(), - 'initialized': self.vr_initialized, - 'enabled': self.vr_enabled, - 'eye_resolution': (self.eye_width, self.eye_height), - 'device_count': len(self.controller_poses) + (1 if self.vr_enabled else 0), - 'vr_fps': self.vr_fps, - 'frame_count': self.frame_count, - 'submit_failures': self.submit_failures, - 'pose_failures': self.pose_failures - } - - def submit_texture(self, eye, texture): - """优化的VR纹理提交 - 使用缓存的纹理ID,避免重复prepareNow""" - try: - if not self.vr_compositor: - print("❌ VR compositor不可用") - self.submit_failures += 1 - return - - # 🚀 关键修复:防止同一帧重复提交 - current_frame = getattr(self, 'frame_count', 0) - if not hasattr(self, '_last_submit_frame'): - self._last_submit_frame = {} - - # 检查当前帧是否已经提交过此眼睛的纹理 - if eye in self._last_submit_frame and self._last_submit_frame[eye] == current_frame: - # 静默跳过,避免spam输出 - return - - # 记录当前帧提交 - self._last_submit_frame[eye] = current_frame - - # 🚀 关键优化:直接使用缓存的纹理ID,避免重复prepareNow - if eye == openvr.Eye_Left: - handle = self.left_texture_id - eye_name = "左眼" - elif eye == openvr.Eye_Right: - handle = self.right_texture_id - eye_name = "右眼" - else: - print(f"❌ 未知的眼睛类型: {eye}") - self.submit_failures += 1 - return - - # 检查缓存的纹理ID是否有效 - if not handle or handle <= 0: - print(f"❌ {eye_name}纹理ID缓存无效: {handle}") - print(" 这可能表示纹理准备失败,需要检查_prepare_and_cache_textures()") - self.submit_failures += 1 - return - - # ❌ 移除gsg.flush()调用 - 基于OpenVR官方实践 - # gsg.flush()等同于OpenGL的glFlush(),导致强制CPU-GPU同步 - # 每帧调用2次(左右眼)是GPU周期性峰值的主要原因 - # 参考:OpenVR社区经验表明同步调用是性能杀手 - # - # gsg = self.world.win.getGsg() - # if gsg and hasattr(gsg, 'flush'): - # try: - # gsg.flush() - # except Exception as flush_error: - # if not hasattr(self, '_gsg_flush_error_logged'): - # print(f"⚠️ GSG刷新失败: {flush_error}") - # self._gsg_flush_error_logged = True - - # 🚀 关键优化:使用缓存的OpenVR Texture对象,避免每帧创建 - if eye == openvr.Eye_Left: - ovr_texture = self.optimization._left_ovr_texture if self.optimization else None - else: - ovr_texture = self.optimization._right_ovr_texture if self.optimization else None - - # 检查缓存对象是否存在(向后兼容) - if ovr_texture is None: - # 备用方案:如果缓存对象不存在,创建新的(性能较差) - ovr_texture = openvr.Texture_t() - ovr_texture.eType = openvr.TextureType_OpenGL - ovr_texture.eColorSpace = openvr.ColorSpace_Gamma - if not hasattr(self, '_texture_fallback_warned'): - print("⚠️ 使用Texture对象备用方案(性能次优)") - self._texture_fallback_warned = True - - # 只更新handle,其他属性已预设置 - ovr_texture.handle = handle - - # 提交到VR系统 - error = self.vr_compositor.submit(eye, ovr_texture) - - # 检查错误 - if error and error != openvr.VRCompositorError_None: - print(f"⚠️ VR{eye_name}纹理提交错误: {error}") - self.submit_failures += 1 - else: - # 只在第一次成功时输出 - if not hasattr(self, '_optimized_submit_success_logged'): - print(f"✅ 优化版VR纹理提交成功! 使用缓存ID,避免重复prepareNow") - print(f" {eye_name}纹理ID: {handle}") - self._optimized_submit_success_logged = True - - # 🔧 智能GPU同步策略 - 减少不必要的flush调用 - # 只有在检测到性能问题或每N帧时才强制flush - if eye == openvr.Eye_Right: - self._smart_gpu_sync() - - except Exception as e: - print(f"❌ VR纹理提交异常: {e}") - import traceback - traceback.print_exc() - self.submit_failures += 1 - - def _smart_gpu_sync(self): - """智能GPU同步策略 - 只在必要时进行同步""" - try: - # 初始化同步控制变量 - if not hasattr(self, '_last_gpu_sync_frame'): - self._last_gpu_sync_frame = 0 - self._gpu_sync_interval = 60 # 每60帧强制同步一次(1秒@60FPS) - self._performance_based_sync = True - - current_frame = getattr(self, 'frame_count', 0) - - # 策略1: 性能自适应同步 - if self._performance_based_sync: - # 如果渲染时间过长,增加同步频率 - total_render_time = getattr(self, 'left_render_time', 0) + getattr(self, 'right_render_time', 0) - - if total_render_time > 15.0: # 如果总渲染时间超过15ms - # 高负载时更频繁同步 - sync_interval = 30 # 每30帧同步 - elif total_render_time > 10.0: - sync_interval = 45 # 中等负载 - else: - sync_interval = self._gpu_sync_interval # 正常负载 - - should_sync = (current_frame - self._last_gpu_sync_frame) >= sync_interval - else: - # 策略2: 固定间隔同步 - should_sync = (current_frame - self._last_gpu_sync_frame) >= self._gpu_sync_interval - - # 执行同步 - if should_sync: - gsg = self.world.win.getGsg() - if gsg and hasattr(gsg, 'flush'): - try: - gsg.flush() - self._last_gpu_sync_frame = current_frame - - # 只在首次或Debug时输出 - if not hasattr(self, '_smart_sync_logged') or self.debug_output_enabled: - if not hasattr(self, '_smart_sync_logged'): - print(f"🔧 智能GPU同步已启用 - 间隔:{sync_interval}帧") - self._smart_sync_logged = True - elif self.debug_output_enabled and current_frame % 600 == 1: - print(f"🔧 智能同步触发 (帧#{current_frame}, 间隔:{sync_interval})") - - except Exception as flush_error: - if not hasattr(self, '_smart_sync_error_logged'): - print(f"⚠️ 智能GPU同步失败: {flush_error}") - self._smart_sync_error_logged = True - - except Exception as e: - print(f"⚠️ 智能GPU同步策略异常: {e}") - - def _disable_main_cam(self): - """禁用主相机 - 基于参考实现""" - try: - # 保存原始相机状态 - if not hasattr(self, '_original_camera_parent'): - self._original_camera_parent = self.world.camera.getParent() - - # 创建空节点并将主相机重新附加到它 - self._empty_world = NodePath("empty_world") - self.world.camera.reparentTo(self._empty_world) - - print("✓ 主相机已禁用") - except Exception as e: - print(f"⚠️ 禁用主相机失败: {e}") - - def _enable_main_cam(self): - """恢复主相机 - 基于参考实现""" - try: - # 恢复原始相机状态 - if hasattr(self, '_original_camera_parent') and self._original_camera_parent: - self.world.camera.reparentTo(self._original_camera_parent) - else: - # 如果没有保存的父节点,重新附加到render - self.world.camera.reparentTo(self.world.render) - - # 清理空世界节点 - if hasattr(self, '_empty_world'): - self._empty_world.removeNode() - delattr(self, '_empty_world') - - print("✓ 主相机已恢复") - except Exception as e: - print(f"⚠️ 恢复主相机失败: {e}") - - def _initialize_controllers(self): - """初始化VR手柄控制器""" - try: - print("🎮 正在初始化VR手柄控制器...") - - # 创建左右手柄控制器实例 - self.left_controller = LeftController(self) - self.right_controller = RightController(self) - - # 检测现有连接的控制器 - self._detect_controllers() - - print("✓ VR手柄控制器初始化完成") - - except Exception as e: - print(f"⚠️ VR手柄初始化失败: {e}") - import traceback - traceback.print_exc() - - def _detect_controllers(self): - """检测并连接VR控制器""" - if not self.vr_system: - return - - try: - for device_index in range(openvr.k_unMaxTrackedDeviceCount): - # 检查设备是否已连接 - if not self.vr_system.isTrackedDeviceConnected(device_index): - continue - - # 获取设备类型 - device_class = self.vr_system.getTrackedDeviceClass(device_index) - - if device_class == openvr.TrackedDeviceClass_Controller: - # 获取控制器角色 - role = self.vr_system.getControllerRoleForTrackedDeviceIndex(device_index) - - if role == openvr.TrackedControllerRole_LeftHand and self.left_controller: - self.left_controller.set_device_index(device_index) - self.controllers[device_index] = self.left_controller - # 为设备创建锚点 - self._create_tracked_device_anchor(device_index, 'left_controller') - - elif role == openvr.TrackedControllerRole_RightHand and self.right_controller: - self.right_controller.set_device_index(device_index) - self.controllers[device_index] = self.right_controller - # 为设备创建锚点 - self._create_tracked_device_anchor(device_index, 'right_controller') - - print(f"🎮 检测到 {len(self.controllers)} 个控制器") - - except Exception as e: - print(f"⚠️ 控制器检测失败: {e}") - - def _create_tracked_device_anchor(self, device_index, name): - """为跟踪设备创建锚点节点""" - if not self.tracking_space: - print(f"⚠️ 无法为设备 {device_index} 创建锚点 - tracking_space未初始化") - return - - try: - # 获取设备模型名称 - if self.vr_system: - model_name = self.vr_system.getStringTrackedDeviceProperty( - device_index, - openvr.Prop_RenderModelName_String - ) - anchor_name = f"{device_index}:{model_name}:{name}" - else: - anchor_name = f"{device_index}:{name}" - - # 创建锚点节点 - device_anchor = self.tracking_space.attachNewNode(anchor_name) - self.tracked_device_anchors[device_index] = device_anchor - - print(f"✓ 为设备 {device_index} 创建锚点: {anchor_name}") - - except Exception as e: - print(f"⚠️ 创建设备锚点失败: {e}") - - def update_tracked_devices(self): - """更新所有跟踪设备的姿态 - 基于参考实现""" - if not self.poses or not self.vr_system: - return - - try: - # 更新每个已连接的控制器 - for device_index, controller in self.controllers.items(): - if device_index < len(self.poses): - pose_data = self.poses[device_index] - - # 更新控制器姿态 - controller.update_pose(pose_data) - - # 更新控制器输入状态 - controller.update_input_state(self.vr_system) - - # 更新其他跟踪设备的锚点 - for device_index in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): - if device_index in self.tracked_device_anchors: - pose_data = self.poses[device_index] - if pose_data.bPoseIsValid: - # 转换姿态矩阵 - modelview = self.convert_mat(pose_data.mDeviceToAbsoluteTracking) - final_matrix = self.coord_mat_inv * modelview * self.coord_mat - - # 更新锚点变换 - anchor = self.tracked_device_anchors[device_index] - anchor.setMat(final_matrix) - anchor.show() - else: - # 姿态无效,隐藏锚点 - self.tracked_device_anchors[device_index].hide() - - except Exception as e: - if self.frame_count % 300 == 0: # 每5秒输出一次错误 - print(f"⚠️ 更新跟踪设备失败: {e}") - - def get_controller_by_role(self, role): - """根据角色获取控制器 - - Args: - role: 'left' 或 'right' - - Returns: - VRController实例或None - """ - if role == 'left': - return self.left_controller - elif role == 'right': - return self.right_controller - return None - - def are_controllers_connected(self): - """检查是否有控制器连接""" - return len(self.controllers) > 0 - - def get_connected_controllers(self): - """获取所有连接的控制器列表""" - return list(self.controllers.values()) - - def trigger_controller_haptic(self, role, duration=0.001, strength=1.0): - """触发控制器震动反馈 - - Args: - role: 'left', 'right' 或 'both' - duration: 震动持续时间(秒) - strength: 震动强度 (0.0-1.0) - """ - if role in ['left', 'both'] and self.left_controller: - self.left_controller.trigger_haptic_feedback(duration, strength) - - if role in ['right', 'both'] and self.right_controller: - self.right_controller.trigger_haptic_feedback(duration, strength) - - # VR动作系统便捷方法 - def is_trigger_pressed(self, hand='any'): - """检查扳机是否被按下 - - Args: - hand: 'left', 'right', 'any' - """ - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - pressed, _ = self.action_manager.is_digital_action_pressed('trigger', device_path) - return pressed - - def is_trigger_just_pressed(self, hand='any'): - """检查扳机是否刚刚被按下""" - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - pressed, _ = self.action_manager.is_digital_action_just_pressed('trigger', device_path) - return pressed - - def is_grip_pressed(self, hand='any'): - """检查握把是否被按下""" - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - pressed, _ = self.action_manager.is_digital_action_pressed('grip', device_path) - return pressed - - def is_grip_just_pressed(self, hand='any'): - """检查握把是否刚刚被按下""" - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - pressed, _ = self.action_manager.is_digital_action_just_pressed('grip', device_path) - return pressed - - def is_menu_pressed(self, hand='any'): - """检查菜单按钮是否被按下""" - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - pressed, _ = self.action_manager.is_digital_action_pressed('menu', device_path) - return pressed - - def is_trackpad_touched(self, hand='any'): - """检查触摸板是否被触摸""" - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - touched, _ = self.action_manager.is_digital_action_pressed('trackpad_touch', device_path) - return touched - - def get_trackpad_position(self, hand='any'): - """获取触摸板位置 - - Returns: - Vec2或None: 触摸板位置 (-1到1的范围) - """ - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - value, _ = self.action_manager.get_analog_action_value('trackpad', device_path) - return value - - # VR交互系统便捷方法 - def get_selected_object(self, hand='any'): - """获取指定手选中的对象 - - Args: - hand: 'left', 'right', 'any' - - Returns: - 选中的对象节点或None - """ - if hand == 'any': - # 返回任意手选中的对象 - for controller in self.get_connected_controllers(): - selected = self.interaction_manager.get_selected_object(controller.name) - if selected: - return selected - return None - else: - return self.interaction_manager.get_selected_object(hand) - - def get_grabbed_object(self, hand='any'): - """获取指定手抓取的对象 - - Args: - hand: 'left', 'right', 'any' - - Returns: - 抓取的对象节点或None - """ - if hand == 'any': - # 返回任意手抓取的对象 - for controller in self.get_connected_controllers(): - grabbed = self.interaction_manager.get_grabbed_object(controller.name) - if grabbed: - return grabbed - return None - else: - return self.interaction_manager.get_grabbed_object(hand) - - def is_grabbing_object(self, hand='any'): - """检查是否正在抓取对象 - - Args: - hand: 'left', 'right', 'any' - - Returns: - bool: 是否正在抓取 - """ - if hand == 'any': - # 检查任意手是否正在抓取 - for controller in self.get_connected_controllers(): - if self.interaction_manager.is_grabbing(controller.name): - return True - return False - else: - return self.interaction_manager.is_grabbing(hand) - - def force_release_all_grabs(self): - """强制释放所有抓取的对象""" - self.interaction_manager.force_release_all() - - def add_interactable_object(self, object_node): - """将对象标记为可交互 - - Args: - object_node: 要标记的对象节点 - """ - self.interaction_manager._add_collision_to_object(object_node) - - def _disable_async_reprojection(self): - """禁用异步重投影 - 备选修复方案""" - try: - # 尝试通过OpenVR设置禁用异步重投影 - if hasattr(openvr, 'VRSettings'): - settings = openvr.VRSettings() - if settings: - # 禁用异步重投影 - error = settings.setBool("steamvr", "enableAsyncReprojection", False) - if error == openvr.VRSettingsError_None: - print("✅ 异步重投影已禁用") - else: - print(f"⚠️ 禁用异步重投影失败: 设置错误 {error}") - else: - print("⚠️ 无法获取VR设置接口") - else: - print("⚠️ OpenVR设置接口不可用") - - except Exception as e: - print(f"⚠️ 禁用异步重投影失败: {e}") - - def enable_async_reprojection_disable(self): - """启用异步重投影禁用选项 - 用户可调用的方法""" - self.disable_async_reprojection = True - print("📝 异步重投影禁用选项已启用,将在下次VR初始化时生效") - - def disable_async_reprojection_disable(self): - """禁用异步重投影禁用选项 - 恢复默认行为""" - self.disable_async_reprojection = False - print("📝 异步重投影禁用选项已关闭,将使用默认ATW行为") - - - - # ====== VR分辨率缩放和质量预设系统 ====== - - - - - - - # ====== 性能模式控制方法 ====== - - - - # ======================================================================== - # 性能监控属性代理 - 属性级别的API向后兼容 - # ======================================================================== - - @property - def left_render_count(self): - """左眼渲染计数器 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.left_render_count - return 0 - - @left_render_count.setter - def left_render_count(self, value): - """左眼渲染计数器设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.left_render_count = value - - @property - def right_render_count(self): - """右眼渲染计数器 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.right_render_count - return 0 - - @right_render_count.setter - def right_render_count(self, value): - """右眼渲染计数器设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.right_render_count = value - - @property - def left_render_time(self): - """左眼渲染时间 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.left_render_time - return 0.0 - - @left_render_time.setter - def left_render_time(self, value): - """左眼渲染时间设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.left_render_time = value - - @property - def right_render_time(self): - """右眼渲染时间 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.right_render_time - return 0.0 - - @right_render_time.setter - def right_render_time(self, value): - """右眼渲染时间设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.right_render_time = value - - # 帧计数和FPS相关属性 - @property - def frame_count(self): - """帧计数器 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.frame_count - return 0 - - @frame_count.setter - def frame_count(self, value): - """帧计数器设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.frame_count = value - - @property - def vr_fps(self): - """VR帧率 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.vr_fps - return 0.0 - - @vr_fps.setter - def vr_fps(self, value): - """VR帧率设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.vr_fps = value - - @property - def last_fps_check(self): - """上次FPS检查 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.last_fps_check - return 0 - - @last_fps_check.setter - def last_fps_check(self, value): - """上次FPS检查设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.last_fps_check = value - - @property - def last_fps_time(self): - """上次FPS时间 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.last_fps_time - return 0.0 - - @last_fps_time.setter - def last_fps_time(self, value): - """上次FPS时间设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.last_fps_time = value - - # 失败计数器 - @property - def submit_failures(self): - """提交失败计数 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.submit_failures - return 0 - - @submit_failures.setter - def submit_failures(self, value): - """提交失败计数设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.submit_failures = value - - @property - def pose_failures(self): - """姿态失败计数 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.pose_failures - return 0 - - @pose_failures.setter - def pose_failures(self, value): - """姿态失败计数设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.pose_failures = value - - # 性能监控开关 - @property - def performance_monitoring(self): - """性能监控开关 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.performance_monitoring - return False - - @performance_monitoring.setter - def performance_monitoring(self, value): - """性能监控开关设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.performance_monitoring = value - - @property - def debug_output_enabled(self): - """调试输出开关 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.debug_output_enabled - return False - - @debug_output_enabled.setter - def debug_output_enabled(self, value): - """调试输出开关设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.debug_output_enabled = value - - - - # 时间监控属性 - @property - def wait_poses_time(self): - """姿态等待时间 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.wait_poses_time - return 0.0 - - @wait_poses_time.setter - def wait_poses_time(self, value): - """姿态等待时间设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.wait_poses_time = value - - @property - def submit_time(self): - """纹理提交时间 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.submit_time - return 0.0 - - @submit_time.setter - def submit_time(self, value): - """纹理提交时间设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.submit_time = value - - # 监控开关 - @property - def enable_gpu_timing(self): - """GPU计时开关 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.enable_gpu_timing - return False - - @enable_gpu_timing.setter - def enable_gpu_timing(self, value): - """GPU计时开关设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.enable_gpu_timing = value - - @property - def enable_pipeline_monitoring(self): - """管线监控开关 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.enable_pipeline_monitoring - return True - - @enable_pipeline_monitoring.setter - def enable_pipeline_monitoring(self, value): - """管线监控开关设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.enable_pipeline_monitoring = value - - @property - def performance_mode_enabled(self): - """性能模式开关 - 代理到优化模块""" - if self.optimization: - return self.optimization.performance_mode_enabled - return False - - @performance_mode_enabled.setter - def performance_mode_enabled(self, value): - """性能模式开关设置 - 代理到优化模块""" - if self.optimization: - self.optimization.performance_mode_enabled = value - - @property - def performance_mode_trigger_frame(self): - """性能模式触发帧 - 代理到优化模块""" - if self.optimization: - return self.optimization.performance_mode_trigger_frame - return 600 - - @performance_mode_trigger_frame.setter - def performance_mode_trigger_frame(self, value): - """性能模式触发帧设置 - 代理到优化模块""" - if self.optimization: - self.optimization.performance_mode_trigger_frame = value - - # GC控制属性代理 - 修复'gc_control_enabled'属性访问问题 - @property - def _gc_control_enabled(self): - """GC控制启用状态 - 代理到优化模块""" - if self.optimization: - return self.optimization._gc_control_enabled - return True - - @_gc_control_enabled.setter - def _gc_control_enabled(self, value): - """GC控制启用状态设置 - 代理到优化模块""" - if self.optimization: - self.optimization._gc_control_enabled = value - - @property - def _gc_disabled(self): - """GC禁用状态 - 代理到优化模块""" - if self.optimization: - return self.optimization._gc_disabled - return False - - @_gc_disabled.setter - def _gc_disabled(self, value): - """GC禁用状态设置 - 代理到优化模块""" - if self.optimization: - self.optimization._gc_disabled = value - - @property - def _manual_gc_interval(self): - """手动GC间隔 - 代理到优化模块""" - if self.optimization: - return self.optimization._manual_gc_interval - return 900 - - @_manual_gc_interval.setter - def _manual_gc_interval(self, value): - """手动GC间隔设置 - 代理到优化模块""" - if self.optimization: - self.optimization._manual_gc_interval = value - - # 分辨率缩放属性代理 - @property - def resolution_scale(self): - """分辨率缩放系数 - 代理到优化模块""" - if self.optimization: - return self.optimization.resolution_scale - return 0.75 - - @resolution_scale.setter - def resolution_scale(self, value): - """分辨率缩放系数设置 - 代理到优化模块""" - if self.optimization: - self.optimization.resolution_scale = value - - @property - def base_eye_width(self): - """基础眼宽 - 代理到优化模块""" - if self.optimization: - return self.optimization.base_eye_width - return 1080 - - @base_eye_width.setter - def base_eye_width(self, value): - """基础眼宽设置 - 代理到优化模块""" - if self.optimization: - self.optimization.base_eye_width = value - - @property - def base_eye_height(self): - """基础眼高 - 代理到优化模块""" - if self.optimization: - return self.optimization.base_eye_height - return 1200 - - @base_eye_height.setter - def base_eye_height(self, value): - """基础眼高设置 - 代理到优化模块""" - if self.optimization: - self.optimization.base_eye_height = value - - @property - def scaled_eye_width(self): - """缩放后眼宽 - 代理到优化模块""" - if self.optimization: - return self.optimization.scaled_eye_width - return 1080 - - @scaled_eye_width.setter - def scaled_eye_width(self, value): - """缩放后眼宽设置 - 代理到优化模块""" - if self.optimization: - self.optimization.scaled_eye_width = value - - @property - def scaled_eye_height(self): - """缩放后眼高 - 代理到优化模块""" - if self.optimization: - return self.optimization.scaled_eye_height - return 1200 - - @scaled_eye_height.setter - def scaled_eye_height(self, value): - """缩放后眼高设置 - 代理到优化模块""" - if self.optimization: - self.optimization.scaled_eye_height = value - - @property - def current_quality_preset(self): - """当前质量预设 - 代理到优化模块""" - if self.optimization: - return self.optimization.current_quality_preset - return "balanced" - - @current_quality_preset.setter - def current_quality_preset(self, value): - """当前质量预设设置 - 代理到优化模块""" - if self.optimization: - self.optimization.current_quality_preset = value - - @property - def quality_presets(self): - """质量预设 - 代理到优化模块""" - if self.optimization: - return self.optimization.quality_presets - return { - "performance": 0.6, - "balanced": 0.75, - "quality": 1.0 - } - - @quality_presets.setter - def quality_presets(self, value): - """质量预设设置 - 代理到优化模块""" - if self.optimization: - self.optimization.quality_presets = value - - # ======================================================================== - # 性能监控委托方法 - API向后兼容层 - # ======================================================================== - - # 性能报告方法 - def _print_performance_report(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._print_performance_report() - - def _print_performance_recommendations(self, stats): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._print_performance_recommendations(stats) - - def _print_brief_performance_report(self, stats): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._print_brief_performance_report(stats) - - # 性能监控核心方法 - def _update_performance_metrics(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._update_performance_metrics() - - def _update_gpu_metrics(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._update_gpu_metrics() - - def _track_frame_time(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._track_frame_time() - - # GPU计时方法 - def _get_gpu_frame_timing(self, frames_ago=0): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._get_gpu_frame_timing(frames_ago) - return None - - def enable_gpu_timing_monitoring(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.enable_gpu_timing_monitoring() - - def disable_gpu_timing_monitoring(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.disable_gpu_timing_monitoring() - - # 管线统计方法 - def _start_timing(self, operation_name): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._start_timing(operation_name) - return None - - def _end_timing(self, timing_data): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._end_timing(timing_data) - return 0.0 - - def _get_pipeline_stats(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._get_pipeline_stats() - return {} - - def test_pipeline_monitoring(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.test_pipeline_monitoring() - - # 诊断方法 - def _print_render_callback_diagnostics(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._print_render_callback_diagnostics() - - def _check_rendering_optimizations(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._check_rendering_optimizations() - - def _diagnose_opengl_state(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._diagnose_opengl_state() - - # 调试控制方法 - def enable_debug_output(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.enable_debug_output() - - def disable_debug_output(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.disable_debug_output() - - def set_debug_mode(self, mode): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.set_debug_mode(mode) - - def toggle_debug_output(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.toggle_debug_output() - return False - - def get_debug_status(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.get_debug_status() - return {} - - # 配置方法 - def set_performance_check_interval(self, interval): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.set_performance_check_interval(interval) - - def set_frame_time_history_size(self, size): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.set_frame_time_history_size(size) - - def set_performance_report_interval(self, frames): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.set_performance_report_interval(frames) - - def set_prediction_time(self, prediction_time_ms): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.set_prediction_time(prediction_time_ms) - - # 查询方法 - def get_performance_stats(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.get_performance_stats() - return {} - - def get_current_performance_summary(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.get_current_performance_summary() - return "性能监控未初始化" - - def get_performance_monitoring_config(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.get_performance_monitoring_config() - return {} - - def print_performance_monitoring_status(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.print_performance_monitoring_status() - - # 控制方法 - def enable_performance_monitoring(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.enable_performance_monitoring() - - def disable_performance_monitoring(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.disable_performance_monitoring() - - def force_performance_report(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.force_performance_report() - - def reset_performance_counters(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.reset_performance_counters() - - # ======================================================================== - # 纹理提交方法 - VR渲染核心功能 - # ======================================================================== - - def _batch_submit_textures(self): - """批量提交两眼纹理 - OpenVR最佳实践 - """ - try: - if not self.vr_compositor: - return False - - # 检查纹理是否准备好 - if not (self.vr_left_texture and self.vr_right_texture): - return False - - import openvr - - # 🚀 关键:快速连续提交两眼,最小化阻塞时间 - # 这符合OpenVR官方示例的做法 - success_left = False - success_right = False - - # 提交左眼纹理 - try: - self.submit_texture(openvr.Eye_Left, self.vr_left_texture) - success_left = True - except Exception as e: - print(f"❌ 批量提交左眼失败: {e}") - - # 立即提交右眼纹理(不等待) - try: - self.submit_texture(openvr.Eye_Right, self.vr_right_texture) - success_right = True - except Exception as e: - print(f"❌ 批量提交右眼失败: {e}") - - # 🚀 关键修复:调用PostPresentHandoff解除compositor阻塞 - # 这是解决36FPS问题的核心 - 确保compositor不会等待VSync - if success_left and success_right: - try: - # PostPresentHandoff告诉compositor我们已完成帧处理 - # 防止compositor等待下一个VSync周期 - if hasattr(self.vr_compositor, 'postPresentHandoff'): - self.vr_compositor.postPresentHandoff() - elif hasattr(self.vr_compositor, 'PostPresentHandoff'): - self.vr_compositor.PostPresentHandoff() - else: - # 备用方案:如果没有PostPresentHandoff,记录警告 - if not hasattr(self, '_post_present_warning_logged'): - print("⚠️ PostPresentHandoff方法未找到,可能影响时序") - self._post_present_warning_logged = True - except Exception as handoff_error: - if not hasattr(self, '_handoff_error_logged'): - print(f"⚠️ PostPresentHandoff调用失败: {handoff_error}") - self._handoff_error_logged = True - - # 记录批量提交状态(仅首次成功时) - if not hasattr(self, '_batch_submit_success_logged'): - print("✅ OpenVR批量提交模式+PostPresentHandoff已启用") - print(" 这应该解决36FPS → 72FPS的问题") - self._batch_submit_success_logged = True - return True - else: - return False - - except Exception as e: - print(f"❌ 批量提交纹理失败: {e}") - return False - - def enable_vr_test_mode(self, display_mode='stereo'): - """委托到测试模式系统""" - if self.test_mode: - return self.test_mode.enable_vr_test_mode(display_mode) - return False - - def disable_vr_test_mode(self): - """委托到测试模式系统""" - if self.test_mode: - return self.test_mode.disable_vr_test_mode() - return False - - def switch_test_display_mode(self, display_mode): - """委托到测试模式系统""" - if self.test_mode: - return self.test_mode.switch_test_display_mode(display_mode) - return False - - def get_test_mode_status(self): - """委托到测试模式系统""" - if self.test_mode: - return self.test_mode.get_test_mode_status() - return None - - def get_test_mode_features(self): - """委托到测试模式系统""" - if self.test_mode: - return self.test_mode.get_test_mode_features() - return {} - - def set_test_mode_features(self, submit_texture=None, wait_poses=None): - """委托到测试模式系统""" - if self.test_mode: - return self.test_mode.set_test_mode_features(submit_texture, wait_poses) - - def run_vr_performance_test(self, duration_seconds=30, display_mode='stereo'): - """委托到测试模式系统""" - if self.test_mode: - return self.test_mode.run_vr_performance_test(duration_seconds, display_mode) - return None - - @property - def vr_test_mode(self): - """测试模式启用状态 - 代理到测试模式系统""" - if self.test_mode: - return self.test_mode.vr_test_mode - return False - - @vr_test_mode.setter - def vr_test_mode(self, value): - """测试模式启用状态设置 - 代理到测试模式系统""" - if self.test_mode: - self.test_mode.vr_test_mode = value - - @property - def test_display_mode(self): - """测试显示模式 - 代理到测试模式系统""" - if self.test_mode: - return self.test_mode.test_display_mode - return 'stereo' - - @test_display_mode.setter - def test_display_mode(self, value): - """测试显示模式设置 - 代理到测试模式系统""" - if self.test_mode: - self.test_mode.test_display_mode = value - - @property - def test_mode_submit_texture(self): - """测试模式纹理提交开关 - 代理到测试模式系统""" - if self.test_mode: - return self.test_mode.test_mode_submit_texture - return False - - @test_mode_submit_texture.setter - def test_mode_submit_texture(self, value): - """测试模式纹理提交开关设置 - 代理到测试模式系统""" - if self.test_mode: - self.test_mode.test_mode_submit_texture = value - - @property - def test_mode_wait_poses(self): - """测试模式姿态等待开关 - 代理到测试模式系统""" - if self.test_mode: - return self.test_mode.test_mode_wait_poses - return False - - @test_mode_wait_poses.setter - def test_mode_wait_poses(self, value): - """测试模式姿态等待开关设置 - 代理到测试模式系统""" - if self.test_mode: - self.test_mode.test_mode_wait_poses = value - - @property - def test_mode_initialized(self): - """测试模式初始化状态 - 代理到测试模式系统""" - if self.test_mode: - return self.test_mode.test_mode_initialized - return False +""" +VR管理器模块 + +负责VR功能的初始化、渲染和交互: +- OpenVR/SteamVR集成 +- VR头显跟踪和渲染 +- VR控制器交互 +- VR模式切换 +""" + +import sys +import gc +import numpy as np +from panda3d.core import ( + WindowProperties, GraphicsPipe, FrameBufferProperties, + GraphicsOutput, Texture, Camera, PerspectiveLens, MatrixLens, + Mat4, Vec3, TransformState, RenderState, CardMaker, + BitMask32, PandaNode, NodePath, LMatrix4, LVector3, LVector4, + CS_yup_right, CS_default, PythonCallbackObject +) +from direct.task import Task +from direct.showbase.DirectObject import DirectObject + +try: + import openvr + OPENVR_AVAILABLE = True +except ImportError: + OPENVR_AVAILABLE = False + print("警告: OpenVR未安装,VR功能将不可用") + +# 导入手柄控制器、动作系统、交互系统、摇杆系统和传送系统 +from core.vr.tracking.controllers import LeftController, RightController +from core.vr.interaction.actions import VRActionManager +from core.vr.interaction.grab import VRInteractionManager +from core.vr.interaction.joystick import VRJoystickManager +from core.vr.interaction.teleport import VRTeleportSystem +from core.vr.performance.monitoring import VRPerformanceMonitor +from core.vr.performance.optimization import VROptimization +from core.vr.testing import VRTestMode +from enum import Enum + + +class VRRenderMode(Enum): + """VR渲染模式枚举""" + NORMAL = "normal" # 普通渲染模式 + RENDER_PIPELINE = "render_pipeline" # RenderPipeline高级渲染模式 + + +class VRManager(DirectObject): + """VR管理器类 - 处理所有VR相关功能""" + + def __init__(self, world): + """初始化VR管理器 + + Args: + world: 主世界对象引用 + """ + super().__init__() + + self.world = world + self.vr_system = None + self.vr_enabled = False + self.vr_initialized = False + + # VR渲染相关 + self.vr_left_eye_buffer = None + self.vr_right_eye_buffer = None + self.vr_left_camera = None + self.vr_right_camera = None + self.vr_compositor = None + + # VR纹理和ID缓存 - 修复重复准备问题 + self.vr_left_texture = None + self.vr_right_texture = None + self.left_texture_id = None # 缓存左眼纹理的OpenGL ID + self.right_texture_id = None # 缓存右眼纹理的OpenGL ID + self.textures_prepared = False # 标记纹理是否已准备 + + # VR跟踪数据 + self.hmd_pose = Mat4.identMat() + self.controller_poses = {} + self.tracked_device_poses = [] + self.poses = None # OpenVR渲染姿态数组 + self.game_poses = None # OpenVR游戏逻辑姿态数组 + + # 🚀 立即初始化对象池和GC控制(在其他组件之前) + # 对象池和优化系统 - 模块化重构 + try: + self.optimization = VROptimization(self) + self.optimization._initialize_object_pools() + print("✓ VR对象池和优化系统初始化完成") + except Exception as e: + print(f"⚠️ VR对象池和优化系统初始化失败: {e}") + self.optimization = None + + # VR渲染参数 + self.eye_width = 1080 + self.eye_height = 1200 + self.near_clip = 0.1 + self.far_clip = 1000.0 + + # VR分辨率缩放优化 - 现在由优化模块管理 + self.resolution_scale = 0.75 # 默认0.75倍分辨率,性能和质量平衡 + self.base_eye_width = 1080 # 原始推荐分辨率 + self.base_eye_height = 1200 + self.scaled_eye_width = 1080 # 实际使用的缩放后分辨率 + self.scaled_eye_height = 1200 + + # VR质量预设 + self.quality_presets = { + 'performance': 0.6, # 性能模式 - 约60%分辨率 + 'balanced': 0.75, # 平衡模式 - 约75%分辨率 + 'quality': 1.0 # 质量模式 - 100%分辨率 + } + self.current_quality_preset = 'balanced' # 默认平衡模式 + + # 🎨 VR渲染模式配置 - RenderPipeline集成 + self.vr_render_mode = VRRenderMode.NORMAL # 默认使用普通渲染模式 + self.render_pipeline_enabled = False # RenderPipeline是否已启用 + self.vr_pipeline_left_target = None # 左眼RenderPipeline渲染目标 + self.vr_pipeline_right_target = None # 右眼RenderPipeline渲染目标 + self.pipeline_resolution_scale = 0.75 # RenderPipeline模式下的分辨率缩放(性能优化) + self.vr_pipeline_controller = None # VR Pipeline控制器(管理完整的VR渲染管线) + + # RenderPipeline VR优化配置 + self.pipeline_vr_config = { + 'enable_shadows': True, # 启用阴影 + 'enable_ao': True, # 启用环境光遮蔽 + 'enable_bloom': False, # 禁用泛光(VR性能考虑) + 'enable_motion_blur': False, # 禁用运动模糊(VR中会引起不适) + 'enable_ssr': False, # 禁用屏幕空间反射(性能密集) + 'shadow_quality': 'medium', # 阴影质量:low/medium/high + 'ao_quality': 'low', # AO质量:low/medium/high + } + + # VR任务 + self.vr_task = None + + # VR锚点层级系统 + self.tracking_space = None + self.hmd_anchor = None + self.left_eye_anchor = None + self.right_eye_anchor = None + + # 坐标系转换矩阵 - 使用Panda3D内置方法 + self.coord_mat = LMatrix4.convert_mat(CS_yup_right, CS_default) + self.coord_mat_inv = LMatrix4.convert_mat(CS_default, CS_yup_right) + + # OpenVR 帧ID跟踪(防止重复提交) + self.openvr_frame_id = 0 + self.left_eye_last_render_frame = -1 + self.right_eye_last_render_frame = -1 + + # waitGetPoses调用策略配置 - 使用高性能的update_task策略 + self.use_prediction_time = 0.011 # 11ms的预测时间 - OpenVR标准值,平衡准确性和延迟 + self.poses_updated_in_task = True # 始终在更新任务中获取姿态(Running Start模式) + + # 🎨 初始化VR配置管理器 + try: + from core.vr.config.vr_config import VRConfigManager + self.config_manager = VRConfigManager() + # 加载配置并应用 + self.config_manager.apply_config_to_vr_manager(self) + print("✓ VR配置管理器初始化完成并已加载配置") + except Exception as e: + print(f"⚠️ VR配置管理器初始化失败: {e}") + self.config_manager = None + + # VR提交策略 - 基于参考实现 + self.submit_together = True # 是否在right_cb中同时提交左右眼 + + + # Running Start标记 - Valve最佳实践 + self._waitgetposes_called_this_frame = False + + # 姿态缓存 - 修复时序不匹配 + self._cached_render_poses = None # 用于渲染的缓存姿态 + self._first_frame = True # 首帧标记 + + # ATW控制选项 - 备选方案 + self.disable_async_reprojection = False # 是否禁用异步重投影 + + # VR手柄控制器 + self.left_controller = None + self.right_controller = None + self.controllers = {} # 设备索引到控制器的映射 + self.tracked_device_anchors = {} # 跟踪设备锚点 + + # VR动作系统 - 可选择禁用(用于API兼容性问题) + self.disable_action_system = True # 禁用动作系统,使用直接控制器输入 + + if not self.disable_action_system: + try: + self.action_manager = VRActionManager(self) + print("✓ VR动作管理器初始化完成") + except Exception as e: + print(f"⚠️ VR动作管理器初始化失败: {e}") + self.action_manager = None + else: + self.action_manager = None + print("🚫 VR动作系统已禁用,使用直接控制器输入") + + # VR交互系统 - 添加异常保护 + try: + self.interaction_manager = VRInteractionManager(self) + print("✓ VR交互管理器初始化完成") + except Exception as e: + print(f"⚠️ VR交互管理器初始化失败: {e}") + self.interaction_manager = None + + # VR传送系统 - 添加异常保护 + try: + self.teleport_system = VRTeleportSystem(self) + print("✓ VR传送系统初始化完成") + except Exception as e: + print(f"⚠️ VR传送系统初始化失败: {e}") + self.teleport_system = None + + # VR摇杆系统 - 添加异常保护 + try: + self.joystick_manager = VRJoystickManager(self) + print("✓ VR摇杆管理器初始化完成") + except Exception as e: + print(f"⚠️ VR摇杆管理器初始化失败: {e}") + self.joystick_manager = None + + # 🎯 性能监控系统 - 模块化重构 + try: + self.performance_monitor = VRPerformanceMonitor(self) + print("✓ VR性能监控系统初始化完成") + except Exception as e: + print(f"⚠️ VR性能监控系统初始化失败: {e}") + self.performance_monitor = None + + # 🧪 测试调试系统 - 模块化重构 + try: + self.test_mode = VRTestMode(self) + print("✓ VR测试模式系统初始化完成") + except Exception as e: + print(f"⚠️ VR测试模式系统初始化失败: {e}") + self.test_mode = None + + print("✓ VR管理器初始化完成") + + # ====== 对象池和优化系统委托方法 ====== + + def enable_gc_control(self): + """启用垃圾回收控制 - 减少VR渲染期间的GC峰值""" + if self.optimization: + return self.optimization.enable_gc_control() + return False + + def disable_gc_control(self): + """禁用垃圾回收控制 - 恢复自动垃圾回收""" + if self.optimization: + return self.optimization.disable_gc_control() + return False + + def set_manual_gc_interval(self, frames): + """设置手动垃圾回收间隔 + + Args: + frames: 帧数间隔 (建议100-600) + """ + if self.optimization: + return self.optimization.set_manual_gc_interval(frames) + return False + + def force_manual_gc(self): + """强制执行一次垃圾回收""" + if self.optimization: + return self.optimization.force_manual_gc() + return 0 + + def get_object_pool_status(self): + """获取对象池状态""" + if self.optimization: + return self.optimization.get_object_pool_status() + return {} + + def set_resolution_scale(self, scale): + """设置VR分辨率缩放系数 + + Args: + scale: 缩放系数 (0.3-1.0),0.75表示75%分辨率 + """ + if self.optimization: + return self.optimization.set_resolution_scale(scale) + return False + + def set_quality_preset(self, preset_name): + """设置VR质量预设 + + Args: + preset_name: 'performance', 'balanced', 'quality' + """ + if self.optimization: + return self.optimization.set_quality_preset(preset_name) + return False + + def cycle_quality_preset(self): + """循环切换质量预设""" + if self.optimization: + return self.optimization.cycle_quality_preset() + return False + + def get_resolution_info(self): + """获取分辨率相关信息""" + if self.optimization: + return self.optimization.get_resolution_info() + return {} + + def print_resolution_info(self): + """输出分辨率信息""" + if self.optimization: + return self.optimization.print_resolution_info() + + def enable_performance_mode(self): + """手动启用性能模式 - 立即禁用详细监控以提升性能""" + if self.optimization: + return self.optimization.enable_performance_mode() + + def disable_performance_mode(self): + """禁用性能模式 - 重新启用详细监控(用于调试)""" + if self.optimization: + return self.optimization.disable_performance_mode() + + def set_performance_mode_trigger_frame(self, frame_count): + """设置性能模式自动触发的帧数 + + Args: + frame_count: 触发帧数 (建议300-1200) + """ + if self.optimization: + return self.optimization.set_performance_mode_trigger_frame(frame_count) + + def get_performance_mode_status(self): + """获取性能模式状态""" + if self.optimization: + return self.optimization.get_performance_mode_status() + return {} + + + + + + def convert_mat(self, mat): + """ + 将OpenVR矩阵转换为Panda3D矩阵 - 基于参考实现 + """ + if len(mat.m) == 4: + result = LMatrix4( + mat.m[0][0], mat.m[1][0], mat.m[2][0], mat.m[3][0], + mat.m[0][1], mat.m[1][1], mat.m[2][1], mat.m[3][1], + mat.m[0][2], mat.m[1][2], mat.m[2][2], mat.m[3][2], + mat.m[0][3], mat.m[1][3], mat.m[2][3], mat.m[3][3]) + elif len(mat.m) == 3: + result = LMatrix4( + mat.m[0][0], mat.m[1][0], mat.m[2][0], 0.0, + mat.m[0][1], mat.m[1][1], mat.m[2][1], 0.0, + mat.m[0][2], mat.m[1][2], mat.m[2][2], 0.0, + mat.m[0][3], mat.m[1][3], mat.m[2][3], 1.0) + return result + + def is_vr_available(self): + """检查VR系统是否可用""" + if not OPENVR_AVAILABLE: + return False + + try: + # 检查SteamVR是否运行 + return openvr.isRuntimeInstalled() and openvr.isHmdPresent() + except Exception as e: + print(f"VR检查失败: {e}") + return False + + def initialize_vr(self): + """初始化VR系统""" + if not OPENVR_AVAILABLE: + print("❌ OpenVR不可用,无法初始化VR") + return False + + if self.vr_initialized: + print("VR系统已经初始化") + return True + + try: + print("🔄 正在初始化VR系统...") + + # 🚀 确保对象池已正确初始化(备用检查) + if self.optimization is None: + print("⚠️ 优化系统未初始化,正在重新初始化...") + from core.vr.performance.optimization import VROptimization + self.optimization = VROptimization(self) + self.optimization._initialize_object_pools() + + # 初始化OpenVR - 使用Scene应用类型确保正确的焦点管理 + self.vr_system = openvr.init(openvr.VRApplication_Scene) + if not self.vr_system: + print("❌ 无法初始化OpenVR系统") + return False + + # 获取compositor + self.vr_compositor = openvr.VRCompositor() + if not self.vr_compositor: + print("❌ 无法获取VR Compositor") + return False + + # 获取推荐的渲染目标尺寸 + base_width, base_height = self.vr_system.getRecommendedRenderTargetSize() + self.base_eye_width = base_width + self.base_eye_height = base_height + + # 应用分辨率缩放 + self.scaled_eye_width = int(base_width * self.resolution_scale) + self.scaled_eye_height = int(base_height * self.resolution_scale) + + # 使用缩放后的分辨率作为实际渲染分辨率 + self.eye_width = self.scaled_eye_width + self.eye_height = self.scaled_eye_height + + self.current_eye_resolution = (self.eye_width, self.eye_height) + self.recommended_eye_resolution = (base_width, base_height) + + print(f"✓ VR基础分辨率: {base_width}x{base_height}") + print(f"✓ VR缩放系数: {self.resolution_scale}") + print(f"✓ VR实际分辨率: {self.eye_width}x{self.eye_height}") + print(f"📊 分辨率优化: {(1 - self.resolution_scale**2) * 100:.1f}% 像素减少") + + # 获取VR系统信息 + try: + # 获取显示频率 + self.vr_display_frequency = self.vr_system.getFloatTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_DisplayFrequency_Float + ) + print(f"✓ VR显示频率: {self.vr_display_frequency} Hz") + + # 获取IPD(瞳距) + ipd = self.vr_system.getFloatTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_UserIpdMeters_Float + ) + print(f"✓ IPD(瞳距): {ipd * 1000:.1f}mm") + + # 获取设备制造商和型号 + manufacturer = self.vr_system.getStringTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_ManufacturerName_String + ) + model = self.vr_system.getStringTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_ModelNumber_String + ) + print(f"✓ VR设备: {manufacturer} {model}") + + # 获取更多系统配置信息 + try: + # 获取刷新率相关信息 + seconds_since_last_vsync = self.vr_system.getFloatTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_SecondsFromVsyncToPhotons_Float + ) + self.vsync_to_photons_ms = seconds_since_last_vsync * 1000 + print(f"✓ VSync到光子延迟: {self.vsync_to_photons_ms:.2f}ms") + + # 获取显示延迟信息 + display_refresh_rate = self.vr_display_frequency + if display_refresh_rate > 0: + self.target_frame_time_ms = 1000.0 / display_refresh_rate + print(f"✓ 目标帧时间: {self.target_frame_time_ms:.2f}ms") + + # 计算VSync时间窗口 + self.vsync_window_ms = self.target_frame_time_ms * 0.1 # 10%的窗口 + print(f"✓ VSync时间窗口: ±{self.vsync_window_ms:.2f}ms") + + except Exception as vsync_error: + print(f"⚠️ 获取VSync信息失败: {vsync_error}") + + # 检查是否启用了异步重投影 + try: + if hasattr(openvr, 'VRSettings'): + settings = openvr.VRSettings() + if settings: + self.async_reprojection_enabled = settings.getBool("steamvr", "enableAsyncReprojection") + print(f"✓ 异步重投影: {'启用' if self.async_reprojection_enabled else '禁用'}") + + self.motion_smoothing_enabled = settings.getBool("steamvr", "motionSmoothing") + print(f"✓ 运动平滑: {'启用' if self.motion_smoothing_enabled else '禁用'}") + + except Exception as settings_error: + print(f"⚠️ 获取VR设置失败: {settings_error}") + + except Exception as e: + print(f"⚠️ 获取VR系统信息失败: {e}") + + # 创建OpenVR姿态数组 - 分离渲染和游戏姿态 + poses_t = openvr.TrackedDevicePose_t * openvr.k_unMaxTrackedDeviceCount + self.poses = poses_t() # 渲染姿态(预测的) + self.game_poses = poses_t() # 游戏逻辑姿态(当前的) + print("✓ VR渲染和游戏姿态数组已创建") + + # 🔧 关键修复:统一初始化流程 + # 如果目标模式是RenderPipeline,先用普通模式初始化,然后切换 + # 这样两个场景都走相同的、已验证的代码路径 + target_render_mode = self.vr_render_mode + use_deferred_pipeline_switch = False + + if target_render_mode == VRRenderMode.RENDER_PIPELINE: + print("🎨 目标VR渲染模式: RenderPipeline") + print(" 策略:先用普通模式初始化,然后切换到RenderPipeline") + self.vr_render_mode = VRRenderMode.NORMAL + use_deferred_pipeline_switch = True + else: + print(f"🎨 VR渲染模式: {self.vr_render_mode.value}") + + # 创建VR渲染缓冲区 - 始终用普通模式初始化 + if not self._create_vr_buffers(): + print("❌ 创建VR渲染缓冲区失败") + return False + + # 设置VR相机 + if not self._setup_vr_cameras(): + print("❌ 设置VR相机失败") + return False + + # 优化VR渲染管线 + self._optimize_vr_rendering() + + # 初始化手柄控制器 + self._initialize_controllers() + + # 初始化动作系统 - 仅在启用时初始化 + if not self.disable_action_system and self.action_manager: + try: + if not self.action_manager.initialize(): + print("⚠️ VR动作系统初始化失败,但VR系统将继续运行") + except Exception as e: + print(f"⚠️ VR动作系统初始化异常: {e}") + else: + if self.disable_action_system: + print("🚫 VR动作系统已禁用,跳过初始化") + else: + print("⚠️ VR动作管理器未创建,跳过动作系统初始化") + + # 初始化交互系统 - 检查是否存在 + if self.interaction_manager: + try: + if not self.interaction_manager.initialize(): + print("⚠️ VR交互系统初始化失败,但VR系统将继续运行") + except Exception as e: + print(f"⚠️ VR交互系统初始化异常: {e}") + else: + print("⚠️ VR交互管理器未创建,跳过交互系统初始化") + + # 初始化VR传送系统 + if self.teleport_system: + try: + if not self.teleport_system.initialize(): + print("⚠️ VR传送系统初始化失败,但VR系统将继续运行") + except Exception as e: + print(f"⚠️ VR传送系统初始化异常: {e}") + else: + print("⚠️ VR传送系统未创建,跳过传送系统初始化") + + # 初始化VR摇杆系统 + if self.joystick_manager: + try: + # 传入传送系统引用给摇杆管理器 + self.joystick_manager.initialize(self.teleport_system) + print("✓ VR摇杆系统初始化成功") + except Exception as e: + print(f"⚠️ VR摇杆系统初始化异常: {e}") + else: + print("⚠️ VR摇杆管理器未创建,跳过摇杆系统初始化") + + # 可选:禁用异步重投影(备选方案) + if self.disable_async_reprojection: + self._disable_async_reprojection() + + # 启动VR更新任务 + self._start_vr_task() + + self.vr_initialized = True + print("✅ VR系统初始化成功(普通模式)") + + # 🔧 关键修复:如果目标是RenderPipeline模式,现在切换过去 + # 这样场景2(先设RP再进VR)会走场景1(先VR再设RP)相同的代码路径 + if use_deferred_pipeline_switch: + print("\n🔄 现在切换到目标渲染模式: RenderPipeline") + print(" 这将触发buffer重建和visualizer刷新...") + if self.set_vr_render_mode(target_render_mode): + print("✅ 已成功切换到RenderPipeline模式") + else: + print("⚠️ 切换到RenderPipeline失败,保持普通模式") + + return True + + except Exception as e: + print(f"❌ VR初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_vr_buffers(self): + """创建VR渲染缓冲区 - 使用分辨率缩放优化""" + try: + print(f"🔧 创建VR缓冲区:") + print(f" 推荐分辨率: {self.base_eye_width}x{self.base_eye_height}") + print(f" 缩放系数: {self.resolution_scale}") + print(f" 实际分辨率: {self.eye_width}x{self.eye_height}") + print(f" 像素减少: {(1 - self.resolution_scale**2) * 100:.1f}%") + + # 创建左眼纹理和缓冲区 + self.vr_left_texture = self._create_vr_texture("VR Left Eye Texture") + self.vr_left_eye_buffer = self._create_vr_buffer( + "VR Left Eye", + self.vr_left_texture, + self.eye_width, + self.eye_height + ) + + if not self.vr_left_eye_buffer: + print("❌ 创建左眼缓冲区失败") + return False + + # 设置左眼缓冲区属性 + self.vr_left_eye_buffer.setSort(-100) + self.vr_left_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) # 深蓝色背景便于调试 + self.vr_left_eye_buffer.setActive(True) + + # 创建右眼纹理和缓冲区 + self.vr_right_texture = self._create_vr_texture("VR Right Eye Texture") + self.vr_right_eye_buffer = self._create_vr_buffer( + "VR Right Eye", + self.vr_right_texture, + self.eye_width, + self.eye_height + ) + + if not self.vr_right_eye_buffer: + print("❌ 创建右眼缓冲区失败") + return False + + # 设置右眼缓冲区属性 + self.vr_right_eye_buffer.setSort(-99) + self.vr_right_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) # 深蓝色背景便于调试 + self.vr_right_eye_buffer.setActive(True) + + # 🚀 关键优化:立即准备纹理并缓存OpenGL ID,避免后续重复准备 + print("🔧 准备纹理并缓存OpenGL ID...") + if self._prepare_and_cache_textures(): + print("✅ VR渲染缓冲区和纹理缓存创建成功") + return True + else: + print("❌ 纹理准备失败") + return False + + except Exception as e: + print(f"❌ 创建VR缓冲区失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_vr_texture(self, name): + """创建VR纹理对象 - 基于参考实现""" + texture = Texture(name) + texture.setWrapU(Texture.WMClamp) + texture.setWrapV(Texture.WMClamp) + texture.setMinfilter(Texture.FTLinear) + texture.setMagfilter(Texture.FTLinear) + return texture + + def _prepare_and_cache_textures(self): + """准备纹理并缓存OpenGL ID - 解决重复准备问题""" + try: + # 🔧 验证纹理对象存在 + if not self.vr_left_texture or not self.vr_right_texture: + print("❌ VR纹理对象不存在") + print(f" 左眼纹理: {self.vr_left_texture}") + print(f" 右眼纹理: {self.vr_right_texture}") + return False + + # 获取graphics state guardian和prepared objects + gsg = self.world.win.getGsg() + if not gsg: + print("❌ 无法获取GraphicsStateGuardian") + return False + + prepared_objects = gsg.getPreparedObjects() + if not prepared_objects: + print("❌ 无法获取PreparedObjects") + return False + + # 准备左眼纹理并缓存ID + print(f" 准备左眼纹理: {self.vr_left_texture.getXSize()}x{self.vr_left_texture.getYSize()}") + texture_context = self.vr_left_texture.prepareNow(0, prepared_objects, gsg) + if texture_context and hasattr(texture_context, 'getNativeId'): + self.left_texture_id = texture_context.getNativeId() + if self.left_texture_id > 0: + print(f" ✅ 左眼纹理准备完成 ({self.vr_render_mode.value}): ID={self.left_texture_id}") + else: + print(" ❌ 左眼纹理ID无效") + return False + else: + print(" ❌ 左眼纹理准备失败") + return False + + # 准备右眼纹理并缓存ID + print(f" 准备右眼纹理: {self.vr_right_texture.getXSize()}x{self.vr_right_texture.getYSize()}") + texture_context = self.vr_right_texture.prepareNow(0, prepared_objects, gsg) + if texture_context and hasattr(texture_context, 'getNativeId'): + self.right_texture_id = texture_context.getNativeId() + if self.right_texture_id > 0: + print(f" ✅ 右眼纹理准备完成 ({self.vr_render_mode.value}): ID={self.right_texture_id}") + else: + print(" ❌ 右眼纹理ID无效") + return False + else: + print(" ❌ 右眼纹理准备失败") + return False + + # 标记纹理已准备 + self.textures_prepared = True + print(" ✅ 纹理准备完成,ID已缓存,后续帧将直接使用缓存ID") + return True + + except Exception as e: + print(f"❌ 纹理准备和缓存失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_vr_buffer(self, name, texture, width, height): + """创建VR渲染缓冲区 - 基于参考实现 + 性能诊断""" + # 设置帧缓冲属性 + fbprops = FrameBufferProperties() + fbprops.setRgbaBits(1, 1, 1, 1) + # 可以在这里添加多重采样抗锯齿 + # fbprops.setMultisamples(4) + + # 创建缓冲区 + buffer = self.world.win.makeTextureBuffer(name, width, height, to_ram=False, fbp=fbprops) + + if buffer: + # 🔍 性能诊断:检查缓冲区类型和属性 + self._diagnose_buffer_performance(buffer, name, width, height) + + # 清除默认渲染纹理 + buffer.clearRenderTextures() + # 🚀 使用RTMBindOrCopy模式 - 已经是最优选择 + # RTMBindOrCopy会尝试直接绑定到纹理,只有硬件不支持时才回退到复制 + # 这已经是Panda3D中最高效的渲染到纹理模式 + buffer.addRenderTexture(texture, GraphicsOutput.RTMBindOrCopy, GraphicsOutput.RTPColor) + + else: + print(f"⚠️ VR缓冲区创建失败: {name} ({width}x{height})") + + return buffer + + def _diagnose_buffer_performance(self, buffer, name, width, height): + """诊断VR缓冲区性能特性""" + try: + # 检查缓冲区类型 + buffer_type = type(buffer).__name__ + + # 检查是否为parasite buffer(性能较差的类型) + is_parasite = 'Parasite' in buffer_type + + # 获取缓冲区信息 + is_valid = buffer.isValid() + is_hardware = hasattr(buffer, 'getGsg') and buffer.getGsg() is not None + + # 输出诊断信息(仅第一次创建时) + if not hasattr(self, '_buffer_diagnosis_shown'): + print(f"🔍 VR缓冲区诊断 [{name}]:") + print(f" 类型: {buffer_type}") + print(f" 分辨率: {width}x{height} ({width*height/1000000:.1f}M像素)") + print(f" 有效性: {'✓' if is_valid else '✗'}") + print(f" 硬件支持: {'✓' if is_hardware else '✗'}") + + if is_parasite: + print(f" ⚠️ 检测到Parasite Buffer - 性能可能受限") + print(f" 建议: 检查显卡是否支持真正的离屏渲染") + else: + print(f" ✓ 使用硬件FBO - 性能良好") + + # 显示帧缓冲区属性 + if hasattr(buffer, 'getFbProperties'): + fbp = buffer.getFbProperties() + if fbp: + print(f" 颜色位数: R{fbp.getRedBits()}G{fbp.getGreenBits()}B{fbp.getBlueBits()}A{fbp.getAlphaBits()}") + if hasattr(fbp, 'getMultisamples') and fbp.getMultisamples() > 0: + print(f" MSAA: {fbp.getMultisamples()}x") + + self._buffer_diagnosis_shown = True + + except Exception as e: + print(f"缓冲区诊断失败: {e}") + + def _create_vr_buffers_with_pipeline(self): + """创建带RenderPipeline的VR渲染缓冲区 - 高级渲染模式""" + try: + print(f"🎨 创建RenderPipeline VR缓冲区:") + print(f" 推荐分辨率: {self.base_eye_width}x{self.base_eye_height}") + print(f" Pipeline缩放: {self.pipeline_resolution_scale}") + + # 检查RenderPipeline是否可用 + if not hasattr(self.world, 'render_pipeline') or not self.world.render_pipeline: + print("❌ RenderPipeline未初始化,无法使用高级渲染模式") + return False + + pipeline = self.world.render_pipeline + + # 计算RenderPipeline模式下的分辨率 + pipeline_width = int(self.base_eye_width * self.pipeline_resolution_scale) + pipeline_height = int(self.base_eye_height * self.pipeline_resolution_scale) + + print(f" 实际分辨率: {pipeline_width}x{pipeline_height}") + print(f" 像素减少: {(1 - self.pipeline_resolution_scale**2) * 100:.1f}%") + + # 导入RenderPipeline相关模块 + try: + from RenderPipelineFile.rpcore.render_target import RenderTarget + except ImportError as e: + print(f"❌ 无法导入RenderPipeline模块: {e}") + return False + + # 导入VR stages模块 + try: + from core.vr.rendering.stages import VRPipelineController + except ImportError as e: + print(f"❌ 无法导入VR stages模块: {e}") + return False + + # 创建VR Pipeline控制器 + print(" 初始化VR Pipeline控制器...") + self.vr_pipeline_controller = VRPipelineController(pipeline) + + # 保存分辨率信息(将在_setup_vr_cameras中使用) + self.pipeline_vr_width = pipeline_width + self.pipeline_vr_height = pipeline_height + + # 应用RenderPipeline效果配置 + self._apply_pipeline_vr_effects() + + self.render_pipeline_enabled = True + print("✅ VR Pipeline控制器已初始化(将在相机设置时创建完整管线)") + + # 检查天空盒状态 + self._check_skybox_status() + + return True + + except Exception as e: + print(f"❌ 创建RenderPipeline VR缓冲区失败: {e}") + import traceback + traceback.print_exc() + return False + + def _apply_pipeline_vr_effects(self): + """为VR场景应用RenderPipeline效果配置""" + try: + print("🎨 应用RenderPipeline VR优化配置...") + + # 根据配置调整RenderPipeline设置 + config = self.pipeline_vr_config + + # 注意:实际的插件启用/禁用需要在pipeline.yaml中配置 + # 这里只是记录当前的VR优化意图 + print(f" 阴影: {'启用' if config['enable_shadows'] else '禁用'}") + print(f" 环境光遮蔽: {'启用' if config['enable_ao'] else '禁用'}") + print(f" 泛光效果: {'启用' if config['enable_bloom'] else '禁用'} (VR推荐禁用)") + print(f" 运动模糊: {'启用' if config['enable_motion_blur'] else '禁用'} (VR推荐禁用)") + print(f" 屏幕空间反射: {'启用' if config['enable_ssr'] else '禁用'} (VR推荐禁用)") + + print("✅ RenderPipeline VR效果配置完成") + + except Exception as e: + print(f"⚠️ 应用RenderPipeline VR效果失败: {e}") + + def _check_skybox_status(self): + """检查并报告天空盒状态""" + try: + print("\n🌌 检查天空盒状态...") + + # 方法1:在render节点下搜索所有名字包含skybox的节点 + skybox_nodes = self.world.render.findAllMatches("**/skybox*") + if skybox_nodes and skybox_nodes.getNumPaths() > 0: + print(f" ✓ 找到 {skybox_nodes.getNumPaths()} 个天空盒节点(使用名称搜索)") + for i in range(skybox_nodes.getNumPaths()): + skybox = skybox_nodes.getPath(i) + print(f" 天空盒 #{i+1}:") + print(f" 名称: {skybox.getName()}") + print(f" 位置: {skybox.getPos()}") + print(f" 缩放: {skybox.getScale()}") + print(f" 父节点: {skybox.getParent().getName()}") + + # 检查是否有shader + if skybox.hasShader(): + print(f" ✓ 已应用shader效果") + else: + print(f" ⚠️ 未应用shader效果") + + # 检查bin设置 + if skybox.hasBin(): + print(f" Bin: {skybox.getBinName()}") + else: + print(" ⚠️ 未找到天空盒(名称搜索)") + + # 方法2:搜索所有GeomNode(几何节点),天空盒通常是一个大的几何体 + print(" 搜索所有几何节点...") + all_geoms = self.world.render.findAllMatches("**/+GeomNode") + print(f" 找到 {all_geoms.getNumPaths()} 个几何节点") + + # 查找大型几何体(缩放值很大,可能是天空盒) + skybox_candidates = [] + for i in range(all_geoms.getNumPaths()): + geom_node = all_geoms.getPath(i) + scale = geom_node.getScale() + # 天空盒通常缩放很大(如40000) + if scale[0] > 1000 or scale[1] > 1000 or scale[2] > 1000: + print(f" ✓ 找到大型几何体(可能是天空盒):") + print(f" 名称: {geom_node.getName()}") + print(f" 缩放: {scale}") + print(f" 位置: {geom_node.getPos()}") + if geom_node.hasShader(): + print(f" ✓ 已应用shader") + skybox_candidates.append(geom_node) + + # 方法3:检查render节点的所有直接子节点(显示全部) + print(" 检查render的所有直接子节点...") + render_children = self.world.render.getChildren() + print(f" render有 {render_children.getNumPaths()} 个直接子节点:") + for i in range(render_children.getNumPaths()): + child = render_children.getPath(i) + scale = child.getScale() + # 标记大型节点(可能是天空盒) + if scale[0] > 1000 or scale[1] > 1000 or scale[2] > 1000: + print(f" - {child.getName()} ⭐ (大型节点,缩放: {scale})") + else: + print(f" - {child.getName()}") + + # 报告结果但不自动创建 + if len(skybox_candidates) == 0: + print(" ⚠️ 未找到明显的天空盒(缩放>1000的几何体)") + print(" 💡 RenderPipeline的天空盒可能在上述列表中,但缩放值不同") + print(" 💡 或者可能RenderPipeline未在VR模式下自动创建天空盒") + else: + print(f" ✓ 找到 {len(skybox_candidates)} 个可能的天空盒") + + print("🌌 天空盒状态检查完成\n") + + except Exception as e: + print(f"⚠️ 天空盒状态检查失败: {e}") + import traceback + traceback.print_exc() + + def _create_vr_skybox(self): + """为VR创建天空盒""" + try: + print(" 🎨 创建VR天空盒...") + + # 检查RenderPipeline是否可用 + if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline: + pipeline = self.world.render_pipeline + + # 使用RenderPipeline的load_default_skybox方法 + print(" 使用RenderPipeline方法创建天空盒...") + skybox = pipeline.common_resources.load_default_skybox() + + if skybox: + # 设置天空盒属性 + skybox.set_scale(40000) # 大型缩放 + skybox.reparent_to(self.world.render) + skybox.set_bin("unsorted", 10000) # 最后渲染 + + # 应用天空盒shader效果 + pipeline.set_effect(skybox, "effects/skybox.yaml", { + "render_shadow": False, + "render_envmap": False, + "render_voxelize": False, + "alpha_testing": False, + "normal_mapping": False, + "parallax_mapping": False + }, 1000) + + print(f" ✅ VR天空盒已创建") + print(f" 名称: {skybox.getName()}") + print(f" 缩放: {skybox.getScale()}") + print(f" 位置: {skybox.getPos()}") + return skybox + else: + print(" ❌ 无法加载天空盒模型") + return None + else: + print(" ❌ RenderPipeline未初始化,无法创建天空盒") + return None + + except Exception as e: + print(f" ❌ 创建VR天空盒失败: {e}") + import traceback + traceback.print_exc() + return None + + def _setup_vr_cameras(self): + """设置VR相机 - 使用锚点层级系统""" + try: + # 创建VR追踪空间锚点层级 + self.tracking_space = self.world.render.attachNewNode('tracking-space') + self.hmd_anchor = self.tracking_space.attachNewNode('hmd-anchor') + self.left_eye_anchor = self.hmd_anchor.attachNewNode('left-eye') + self.right_eye_anchor = self.hmd_anchor.attachNewNode('right-eye') + + # 获取投影矩阵 + projection_left = self.coord_mat_inv * self.convert_mat( + self.vr_system.getProjectionMatrix(openvr.Eye_Left, self.near_clip, self.far_clip)) + projection_right = self.coord_mat_inv * self.convert_mat( + self.vr_system.getProjectionMatrix(openvr.Eye_Right, self.near_clip, self.far_clip)) + + # 创建左眼相机节点 + left_cam_node = Camera('left-cam') + left_lens = MatrixLens() + left_lens.setUserMat(projection_left) + left_cam_node.setLens(left_lens) + + # 创建右眼相机节点 + right_cam_node = Camera('right-cam') + right_lens = MatrixLens() + right_lens.setUserMat(projection_right) + right_cam_node.setLens(right_lens) + + # 附加相机到眼睛锚点 + self.vr_left_camera = self.left_eye_anchor.attachNewNode(left_cam_node) + self.vr_right_camera = self.right_eye_anchor.attachNewNode(right_cam_node) + + # 设置显示区域 - 区分RenderPipeline和普通模式 + if self.vr_render_mode == VRRenderMode.RENDER_PIPELINE and self.vr_pipeline_controller: + # RenderPipeline模式:使用VRPipelineController创建完整管线 + print(" 使用VR Pipeline创建立体渲染管线...") + + # 创建立体渲染管线(GBuffer + Lighting + Final stages) + success = self.vr_pipeline_controller.create_stereo_pipeline( + self.pipeline_vr_width, + self.pipeline_vr_height, + self.vr_left_camera, + self.vr_right_camera + ) + + if not success: + print("❌ VR Pipeline创建失败") + return False + + # 获取GBuffer的内部buffer(用于DisplayRegion) + self.vr_left_eye_buffer = self.vr_pipeline_controller.get_left_buffer() + self.vr_right_eye_buffer = self.vr_pipeline_controller.get_right_buffer() + + # 🔧 关键修复:验证buffer有效性 + if not self.vr_left_eye_buffer or not self.vr_right_eye_buffer: + print("❌ VR Pipeline buffer创建失败") + print(f" 左眼buffer: {self.vr_left_eye_buffer}") + print(f" 右眼buffer: {self.vr_right_eye_buffer}") + return False + + # 获取最终输出纹理(用于提交到OpenVR) + left_textures = self.vr_pipeline_controller.get_left_textures() + right_textures = self.vr_pipeline_controller.get_right_textures() + + if left_textures and right_textures: + self.vr_left_texture = left_textures["final"] + self.vr_right_texture = right_textures["final"] + else: + print("❌ 无法获取VR Pipeline输出纹理") + return False + + # 设置缓冲区排序和清除颜色 + if self.vr_left_eye_buffer: + self.vr_left_eye_buffer.setSort(-100) + self.vr_left_eye_buffer.setClearColorActive(True) + self.vr_left_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) + + if self.vr_right_eye_buffer: + self.vr_right_eye_buffer.setSort(-99) + self.vr_right_eye_buffer.setClearColorActive(True) + self.vr_right_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) + + # 🔧 关键修复:为RenderPipeline的GBuffer DisplayRegion设置DrawCallback + # 这确保纹理在渲染完成后被提交到OpenVR + if self.vr_left_eye_buffer and self.vr_right_eye_buffer: + print(" 设置RenderPipeline DisplayRegion回调...") + try: + # 🔧 验证DisplayRegion数量 + left_dr_count = self.vr_left_eye_buffer.getNumDisplayRegions() + right_dr_count = self.vr_right_eye_buffer.getNumDisplayRegions() + + print(f" 左眼buffer DisplayRegion数量: {left_dr_count}") + print(f" 右眼buffer DisplayRegion数量: {right_dr_count}") + + if left_dr_count == 0 or right_dr_count == 0: + print(" ❌ DisplayRegion未创建,无法设置回调") + return False + + # 获取RenderTarget创建的DisplayRegion + left_dr = self.vr_left_eye_buffer.get_display_region(0) + right_dr = self.vr_right_eye_buffer.get_display_region(0) + + # 验证DisplayRegion有效性 + if not left_dr or not right_dr: + print(" ❌ DisplayRegion无效") + return False + + # 设置渲染回调(与普通模式一致) + left_dr.setDrawCallback(PythonCallbackObject(self.simple_left_cb)) + right_dr.setDrawCallback(PythonCallbackObject(self.simple_right_cb)) + + # 确保DisplayRegion处于活动状态 + left_dr.setActive(True) + right_dr.setActive(True) + + print(" ✅ RenderPipeline DisplayRegion回调已设置") + except Exception as e: + print(f" ⚠️ 设置DisplayRegion回调失败: {e}") + import traceback + traceback.print_exc() + return False + + # 🔧 关键修复:强制同步GraphicsEngine,确保所有buffer完全初始化 + print(" 同步GraphicsEngine...") + self.world.graphicsEngine.renderFrame() + + # 准备纹理并缓存OpenGL ID + if not self._prepare_and_cache_textures(): + print("❌ RenderPipeline纹理准备失败") + return False + + print("✅ VR Pipeline立体渲染管线已准备完成") + else: + # 普通模式:创建新的DisplayRegion + print(" 使用普通渲染区域...") + left_dr = self.vr_left_eye_buffer.makeDisplayRegion() + left_dr.setCamera(self.vr_left_camera) + left_dr.setActive(True) + left_dr.setDrawCallback(PythonCallbackObject(self.simple_left_cb)) + + right_dr = self.vr_right_eye_buffer.makeDisplayRegion() + right_dr.setCamera(self.vr_right_camera) + right_dr.setActive(True) + right_dr.setDrawCallback(PythonCallbackObject(self.simple_right_cb)) + + print("✓ VR相机锚点层级系统设置完成") + return True + + except Exception as e: + print(f"❌ 设置VR相机失败: {e}") + import traceback + traceback.print_exc() + return False + + def _get_eye_offset(self, eye): + """获取眼睛相对于头显的偏移""" + try: + if not self.vr_system: + # 使用标准IPD(瞳距)估算值 + ipd = 0.064 # 64mm,平均IPD + if eye == openvr.Eye_Left: + return Vec3(-ipd/2, 0, 0) + else: + return Vec3(ipd/2, 0, 0) + + # 从OpenVR获取眼睛到头显的变换矩阵 + eye_transform = self.vr_system.getEyeToHeadTransform(eye) + + # 提取位移信息 + x = eye_transform[0][3] + y = eye_transform[1][3] + z = eye_transform[2][3] + + return Vec3(x, y, z) + + except Exception as e: + print(f"❌ 获取眼睛偏移失败: {e}") + # 返回默认值 + ipd = 0.064 + if eye == openvr.Eye_Left: + return Vec3(-ipd/2, 0, 0) + else: + return Vec3(ipd/2, 0, 0) + + def _optimize_vr_rendering(self): + """优化VR模式下的渲染管线 - 减少不必要的后处理开销""" + try: + print("🔧 正在优化VR渲染管线...") + + # 检查是否有RenderPipeline + if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline: + print(" 检测到RenderPipeline,正在优化...") + + # 对VR缓冲区应用轻量级渲染设置 + if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: + self._apply_lightweight_rendering(self.vr_left_eye_buffer, "左眼") + + if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: + self._apply_lightweight_rendering(self.vr_right_eye_buffer, "右眼") + + print("✅ VR渲染管线优化完成") + else: + print(" 未检测到RenderPipeline,使用默认Panda3D渲染") + + # 禁用VR缓冲区的不必要功能 + self._disable_vr_buffer_extras() + + except Exception as e: + print(f"⚠️ VR渲染优化失败: {e}") + import traceback + traceback.print_exc() + + def _apply_lightweight_rendering(self, buffer, eye_name): + """为VR缓冲区应用轻量级渲染设置""" + try: + # 禁用多重采样抗锯齿(MSAA)以提升性能 + if hasattr(buffer, 'setMultisample'): + buffer.setMultisample(0) + print(f" {eye_name}: 禁用MSAA") + + # 设置更简单的清除颜色操作 + buffer.setClearColorActive(True) + buffer.setClearColor((0.1, 0.2, 0.4, 1.0)) # 深蓝色,便于调试 + + # 禁用深度写入到某些不需要的缓冲区 + # (保留主要深度测试,但减少不必要的写入) + + print(f" {eye_name}: 应用轻量级渲染设置") + + except Exception as e: + print(f"⚠️ {eye_name}缓冲区优化失败: {e}") + + def _disable_vr_buffer_extras(self): + """禁用VR缓冲区的额外功能以提升性能""" + try: + # 禁用VR缓冲区的统计收集 + if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: + if hasattr(self.vr_left_eye_buffer, 'setOneShot'): + self.vr_left_eye_buffer.setOneShot(False) + + if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: + if hasattr(self.vr_right_eye_buffer, 'setOneShot'): + self.vr_right_eye_buffer.setOneShot(False) + + print(" VR缓冲区额外功能已优化") + + except Exception as e: + print(f"⚠️ VR缓冲区额外功能优化失败: {e}") + + def _start_vr_task(self): + """启动VR更新任务""" + if self.vr_task: + self.world.taskMgr.remove(self.vr_task) + + # 设置高优先级(sort=-1000),确保在渲染之前执行,参考 panda3d-openvr + self.vr_task = self.world.taskMgr.add(self._update_vr, "update_vr", sort=-1000) + print("✓ VR更新任务已启动(高优先级,sort=-1000)") + + def _update_vr(self, task): + """VR更新任务 - 每帧调用(参考 panda3d-openvr 架构)""" + if not self.vr_enabled or not self.vr_system: + return task.cont + + try: + # 📌 第一件事:waitGetPoses 阻塞等待 VSync(参考 panda3d-openvr) + # 这会阻塞整个任务,确保每个 VSync 周期只执行一次 + should_call_waitgetposes = ( + not self.vr_test_mode or # 普通VR模式总是需要 + self.test_mode_wait_poses # 测试模式:仅当启用姿态等待时 + ) + + if should_call_waitgetposes: + self._wait_get_poses_immediate() + # 立即更新手柄,使用最新姿态(避免过时数据) + self.update_tracked_devices() + + # 性能监控 + self.frame_count += 1 + + # 🚀 自动启用性能模式 - 减少计时对象创建 + if (self.optimization and + not self.optimization.performance_mode_enabled and + self.frame_count >= self.optimization.performance_mode_trigger_frame): + self.optimization.performance_mode_enabled = True + if self.frame_count <= self.optimization.performance_mode_trigger_frame + 5: # 只输出一次 + print(f"🎯 性能模式已启用 (帧#{self.frame_count}) - 禁用详细监控以提升性能") + + # 记录帧时间 + self._track_frame_time() + + # 计算VR FPS + import time + current_time = time.time() + if self.last_fps_time == 0: + self.last_fps_time = current_time + elif current_time - self.last_fps_time >= 1.0: # 每秒更新一次FPS + self.vr_fps = (self.frame_count - self.last_fps_check) / (current_time - self.last_fps_time) + self.last_fps_check = self.frame_count + self.last_fps_time = current_time + + # 📌 使用刚获取的姿态更新相机(而不是旧姿态) + self._update_camera_poses() + + # 输出策略信息(仅第一次) + if not hasattr(self, '_new_architecture_logged'): + print("✅ 新架构已启用 - waitGetPoses 在任务开始阻塞,参考 panda3d-openvr") + print(" 这确保每个 VSync 周期只渲染一次,解决 AlreadySubmitted 错误") + self._new_architecture_logged = True + + # 更新VR动作状态 - 仅在启用时更新 + if not self.disable_action_system and self.action_manager: + self.action_manager.update_actions() + + # 更新VR交互系统 + if self.interaction_manager: + self.interaction_manager.update() + + # 更新VR摇杆系统 + if self.joystick_manager: + # 计算帧间隔时间 + import time + if not hasattr(self, '_last_frame_time'): + self._last_frame_time = time.time() + dt = 0.016 # 默认60fps + else: + current_time = time.time() + dt = current_time - self._last_frame_time + self._last_frame_time = current_time + # 限制dt范围,避免异常情况 + dt = max(0.001, min(0.1, dt)) + + self.joystick_manager.update(dt) + + # 更新系统性能指标(减少频率) + if self.frame_count % 30 == 1: # 每30帧更新一次,减少开销 + self._update_performance_metrics() + + # 更新GPU渲染时间统计(减少频率) + if self.enable_gpu_timing and self.frame_count % 60 == 1: + self._get_gpu_frame_timing() + + # 🚀 手动垃圾回收控制 - 避免16-19帧周期性峰值 + if self.optimization: + self.optimization._manual_gc_control() + + # 定期输出性能报告 - 默认10秒间隔 + report_interval = getattr(self, 'performance_report_interval', 600) + if self.frame_count % report_interval == 1: + self._print_performance_report() + + except Exception as e: + print(f"VR更新错误: {e}") + import traceback + traceback.print_exc() + + return task.cont + + def _sync_gpu_if_needed(self): + """可选的GPU同步 - 仅在需要时使用""" + try: + # 同步等待GPU完成(如果需要) + gsg = self.world.win.getGsg() + if gsg: + gsg.getEngine().syncFrame() + except Exception as e: + print(f"❌ GPU同步异常: {e}") + + def simple_left_cb(self, cbdata): + """简化的左眼渲染回调 - 精确控制渲染和提交""" + try: + # 🔍 精确测量渲染时间 + import time + render_start = time.perf_counter() + + # 触发实际渲染 + cbdata.upcall() + + # 计算真实渲染时间 + self.left_render_time = (time.perf_counter() - render_start) * 1000 # 转换为毫秒 + + # 记录渲染次数 + self.left_render_count += 1 + + # 📌 OpenVR 帧边界检查:防止同一 OpenVR 帧内重复渲染 + if self.left_eye_last_render_frame == self.openvr_frame_id: + return # 已在当前 OpenVR 帧渲染过,跳过 + + # 🔧 OpenVR最佳实践:左眼只渲染,不立即提交 + # 基于官方hellovr示例:两眼都渲染完后再批量提交 + self.left_eye_last_render_frame = self.openvr_frame_id + + # 渐进式VR功能测试:单独启用纹理提交时保留原逻辑 + should_submit = self.vr_test_mode and self.test_mode_submit_texture and not self.test_mode_wait_poses + + if should_submit: + # 测试模式:单独启用纹理提交时的兼容模式 + if self.vr_compositor and self.vr_left_texture: + self.submit_texture(openvr.Eye_Left, self.vr_left_texture) + + if self.vr_test_mode: + # 测试模式:始终触发屏幕显示更新 + if self.test_mode: + self.test_mode._update_test_display() + + except Exception as e: + print(f"左眼渲染回调错误: {e}") + + def simple_right_cb(self, cbdata): + """简化的右眼渲染回调 - 精确控制渲染和提交""" + try: + # 🔍 精确测量渲染时间 + import time + render_start = time.perf_counter() + + # 触发实际渲染 + cbdata.upcall() + + # 计算真实渲染时间 + self.right_render_time = (time.perf_counter() - render_start) * 1000 # 转换为毫秒 + + # 记录渲染次数 + self.right_render_count += 1 + + # 📌 OpenVR 帧边界检查:防止同一 OpenVR 帧内重复渲染 + if self.right_eye_last_render_frame == self.openvr_frame_id: + return # 已在当前 OpenVR 帧渲染过,跳过 + + # 🔧 OpenVR最佳实践:右眼渲染完成后批量提交两眼纹理 + # 基于官方hellovr示例:避免分散提交导致的VSync阻塞 + self.right_eye_last_render_frame = self.openvr_frame_id + + # 🔧 渐进式VR功能测试:根据调试标志决定启用哪些功能 + should_batch_submit = not self.vr_test_mode or self.test_mode_submit_texture + should_wait_poses = not self.vr_test_mode or self.test_mode_wait_poses + + # 特殊处理:测试模式单独启用功能时的兼容模式 + if self.vr_test_mode: + if self.test_mode_submit_texture and not self.test_mode_wait_poses: + # 单独测试纹理提交:使用原来的分散提交方式 + if self.vr_compositor and self.vr_right_texture: + self.submit_texture(openvr.Eye_Right, self.vr_right_texture) + + # 🚀 测试模式也需要PostPresentHandoff避免36FPS + try: + if hasattr(self.vr_compositor, 'postPresentHandoff'): + self.vr_compositor.postPresentHandoff() + elif hasattr(self.vr_compositor, 'PostPresentHandoff'): + self.vr_compositor.PostPresentHandoff() + except Exception as e: + pass # 测试模式静默忽略错误 + should_batch_submit = False + elif self.test_mode_wait_poses and not self.test_mode_submit_texture: + # 单独测试姿态等待:不提交纹理 + should_batch_submit = False + + if should_batch_submit: + # 🚀 OpenVR最佳实践:批量提交两眼纹理 + # 这是解决36FPS问题的关键修复 + self._batch_submit_textures() + + # 🔧 关键修复:移除Submit后立即WaitGetPoses的错误实现 + # 根据OpenVR官方文档:"Calling WaitGetPoses immediately after Submit is conspicuously wrong" + # WaitGetPoses应该在下一帧开始时通过update_vr_task调用,不是Submit后立即调用 + # + # if should_wait_poses: + # # 错误的实现:Submit后立即获取姿态导致36FPS + # self._wait_get_poses_immediate() # ← 这是36FPS的根本原因! + # + # 正确的实现:让update_vr_task在下一帧开始时调用WaitGetPoses + + if self.vr_test_mode: + # 测试模式:始终更新性能HUD + if self.test_mode: + self.test_mode._update_test_performance_hud() + + except Exception as e: + print(f"右眼渲染回调错误: {e}") + + def _wait_get_poses(self): + """调用VRCompositor的waitGetPoses来获取焦点和姿态数据""" + try: + if not self.vr_compositor or not self.poses: + return + + # 调用waitGetPoses获取焦点和姿态数据 + # 这个调用可能会阻塞直到下一个VR同步点 + result = self.vr_compositor.waitGetPoses(self.poses, None) + + # 检查姿态数据的有效性 + valid_poses = 0 + + # 更新HMD姿态(设备0通常是头显) + if len(self.poses) > 0 and self.poses[0].bPoseIsValid: + valid_poses += 1 + else: + # 如果HMD姿态无效,不要频繁输出错误信息 + if not hasattr(self, '_hmd_invalid_warning_shown'): + print("⚠️ HMD姿态数据无效") + self._hmd_invalid_warning_shown = True + + # 更新控制器姿态 + self.controller_poses.clear() + for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): + if self.poses[device_id].bPoseIsValid: + device_class = self.vr_system.getTrackedDeviceClass(device_id) + if device_class == openvr.TrackedDeviceClass_Controller: + controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking + self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) + valid_poses += 1 + + # 性能监控 - 偶尔输出姿态状态 + if self.frame_count % 600 == 1: # 每10秒输出一次@60fps + print(f"📊 VR姿态状态 - 有效姿态数: {valid_poses}, 总帧数: {self.frame_count}") + + except Exception as e: + # 限制错误输出频率 + if not hasattr(self, '_last_error_frame'): + self._last_error_frame = 0 + + if self.frame_count - self._last_error_frame > 300: # 每5秒最多输出一次错误 + print(f"waitGetPoses失败: {e}") + self._last_error_frame = self.frame_count + + # 记录姿态失败次数 + self.pose_failures += 1 + + def _wait_get_poses_immediate(self): + """立即获取VR姿态 - 修复ATW闪烁的关键方法(双姿态版本)""" + # 开始计时waitGetPoses操作 + timing = self._start_timing('wait_poses') + + try: + if not self.vr_compositor or not self.poses or not self.game_poses: + self._end_timing(timing) + return + + # 关键修复:传递渲染姿态和游戏姿态数组 + # 渲染姿态用于绘制,游戏姿态用于逻辑,避免ATW过度补偿 + result = self.vr_compositor.waitGetPoses(self.poses, self.game_poses) + + # 📌 waitGetPoses 成功后立即递增 OpenVR 帧ID(新帧开始) + self.openvr_frame_id += 1 + + # 结束计时 + wait_time = self._end_timing(timing) + + # 检查姿态数据的有效性 + valid_poses = 0 + + # 更新HMD姿态(设备0通常是头显) + if len(self.poses) > 0 and self.poses[0].bPoseIsValid: + valid_poses += 1 + else: + # 如果HMD姿态无效,不要频繁输出错误信息 + if not hasattr(self, '_hmd_invalid_warning_shown'): + print("⚠️ HMD姿态数据无效(立即模式)") + self._hmd_invalid_warning_shown = True + + # 🚀 优化控制器姿态更新:使用缓存,避免每帧clear()和重新创建对象 + for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): + if self.poses[device_id].bPoseIsValid: + device_class = self.vr_system.getTrackedDeviceClass(device_id) + if device_class == openvr.TrackedDeviceClass_Controller: + controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking + + # 检查是否已有此设备的缓存矩阵 + if device_id not in self.controller_poses: + # 第一次检测到该控制器,创建新的缓存矩阵 + self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) + else: + # 复用现有矩阵,只更新数值,避免创建新对象 + cached_mat = self.controller_poses[device_id] + self._update_matrix_from_openvr(cached_mat, controller_matrix) + + valid_poses += 1 + else: + # 设备姿态无效,从字典中移除(如果存在) + if device_id in self.controller_poses: + # 将矩阵返回对象池 + if self.optimization: + self.optimization._return_pooled_matrix(self.controller_poses[device_id]) + del self.controller_poses[device_id] + + # 🔧 关键修复:立即更新手柄和跟踪设备 + # Running Start模式下必须在WaitGetPoses后立即更新,避免手柄消失 + self.update_tracked_devices() + + # 调试信息 - 仅在第一次成功时输出 + if not hasattr(self, '_dual_pose_mode_logged') and valid_poses > 0: + print(f"✅ 双姿态立即获取模式启用 - 有效姿态数: {valid_poses}") + print(" 渲染姿态用于绘制,游戏姿态用于逻辑,防止ATW过度补偿") + print(" 手柄和跟踪设备在WaitGetPoses后立即更新") + self._dual_pose_mode_logged = True + + except Exception as e: + # 限制错误输出频率 + if not hasattr(self, '_last_immediate_error_frame'): + self._last_immediate_error_frame = 0 + + if self.frame_count - self._last_immediate_error_frame > 300: # 每5秒最多输出一次错误 + print(f"立即姿态获取失败: {e}") + self._last_immediate_error_frame = self.frame_count + + self.pose_failures += 1 + + def _wait_get_poses_with_prediction(self): + """使用预测时间获取VR姿态 - 优化性能的新策略""" + try: + if not self.vr_compositor or not self.poses or not self.game_poses: + return + + # 使用预测时间获取姿态 + # 预测时间通常为11-16ms,对应下一个VR帧的时间 + result = self.vr_compositor.waitGetPoses(self.poses, self.game_poses) + + # 检查姿态数据的有效性 + valid_poses = 0 + + # 更新HMD姿态(设备0通常是头显) + if len(self.poses) > 0 and self.poses[0].bPoseIsValid: + valid_poses += 1 + else: + # 如果HMD姿态无效,不要频繁输出错误信息 + if not hasattr(self, '_hmd_invalid_warning_task'): + print("⚠️ HMD姿态数据无效(更新任务模式)") + self._hmd_invalid_warning_task = True + + # 更新控制器姿态 + self.controller_poses.clear() + for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): + if self.poses[device_id].bPoseIsValid: + device_class = self.vr_system.getTrackedDeviceClass(device_id) + if device_class == openvr.TrackedDeviceClass_Controller: + controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking + self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) + valid_poses += 1 + + # 调试信息 - 仅在第一次成功时输出 + if not hasattr(self, '_update_task_mode_logged') and valid_poses > 0: + print(f"✅ 更新任务姿态获取模式启用 - 有效姿态数: {valid_poses}") + print(f" 预测时间: {self.use_prediction_time*1000:.1f}ms") + self._update_task_mode_logged = True + + except Exception as e: + # 限制错误输出频率 + if not hasattr(self, '_last_task_error_frame'): + self._last_task_error_frame = 0 + + if self.frame_count - self._last_task_error_frame > 300: # 每5秒最多输出一次错误 + print(f"更新任务姿态获取失败: {e}") + self._last_task_error_frame = self.frame_count + + # 记录姿态失败次数 + self.pose_failures += 1 + + def _cache_poses_for_next_frame(self): + """缓存当前姿态供下一帧渲染使用 - 修复时序不匹配""" + try: + if not self.poses or len(self.poses) == 0: + return + + # 如果是第一帧,直接使用当前姿态 + if self._first_frame: + self._cached_render_poses = self.poses + self._first_frame = False + print("✓ 首帧姿态缓存已设置") + return + + # 复制当前渲染姿态到缓存 + # 下一帧将使用这些姿态进行渲染 + poses_t = openvr.TrackedDevicePose_t * openvr.k_unMaxTrackedDeviceCount + cached_poses = poses_t() + + # 复制姿态数据 + for i in range(len(self.poses)): + cached_poses[i] = self.poses[i] + + self._cached_render_poses = cached_poses + + except Exception as e: + print(f"⚠️ 姿态缓存失败: {e}") + + + def _reset_waitgetposes_flag(self, task): + """重置WaitGetPoses标记 - 确保下一帧可以调用WaitGetPoses""" + self._waitgetposes_called_this_frame = False + return task.done + + def _update_tracking_data(self): + """更新VR追踪数据""" + try: + # 获取设备姿态 + poses = self.vr_system.getDeviceToAbsoluteTrackingPose( + openvr.TrackingUniverseStanding, 0.0, openvr.k_unMaxTrackedDeviceCount + ) + + # 更新HMD姿态(设备0通常是头显) + if poses[0].bPoseIsValid: + hmd_matrix = poses[0].mDeviceToAbsoluteTracking + self.hmd_pose = self._convert_openvr_matrix_to_panda(hmd_matrix) + + # 更新控制器姿态 + for device_id in range(1, openvr.k_unMaxTrackedDeviceCount): + if poses[device_id].bPoseIsValid: + device_class = self.vr_system.getTrackedDeviceClass(device_id) + if device_class == openvr.TrackedDeviceClass_Controller: + controller_matrix = poses[device_id].mDeviceToAbsoluteTracking + self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) + + except Exception as e: + print(f"更新追踪数据失败: {e}") + + def _convert_openvr_matrix_to_panda(self, ovr_matrix): + """将OpenVR矩阵转换为Panda3D矩阵 - 使用对象池优化 + + 坐标系转换: + OpenVR: X右, Y上, -Z前(右手坐标系) + Panda3D: X右, Y前, Z上(右手坐标系) + + 转换规则: + OpenVR X → Panda3D X + OpenVR Y → Panda3D Z + OpenVR -Z → Panda3D Y + """ + # 🚀 使用对象池获取预分配的Mat4对象,避免每帧创建新对象 + if self.optimization: + mat = self.optimization._get_pooled_matrix() + else: + mat = Mat4() + + # 修正的坐标转换矩阵 + # OpenVR: X右, Y上, -Z前 → Panda3D: X右, Y前, Z上 + # 转换规则: (ovr_x, ovr_y, ovr_z) → (panda_x, panda_y, panda_z) + # (ovr_x, ovr_y, ovr_z) → (ovr_x, -ovr_z, ovr_y) + + # X轴行:Panda3D的X轴对应OpenVR的X轴 + mat.setCell(0, 0, ovr_matrix[0][0]) # X_x → X_x + mat.setCell(0, 1, ovr_matrix[0][1]) # X_y → X_y + mat.setCell(0, 2, ovr_matrix[0][2]) # X_z → X_z + mat.setCell(0, 3, ovr_matrix[0][3]) # 位移X分量 + + # Y轴行:Panda3D的Y轴对应OpenVR的-Z轴 + mat.setCell(1, 0, -ovr_matrix[2][0]) # -Z_x → Y_x + mat.setCell(1, 1, -ovr_matrix[2][1]) # -Z_y → Y_y + mat.setCell(1, 2, -ovr_matrix[2][2]) # -Z_z → Y_z + mat.setCell(1, 3, -ovr_matrix[2][3]) # 位移Y分量(-Z位移) + + # Z轴行:Panda3D的Z轴对应OpenVR的Y轴 + mat.setCell(2, 0, ovr_matrix[1][0]) # Y_x → Z_x + mat.setCell(2, 1, ovr_matrix[1][1]) # Y_y → Z_y + mat.setCell(2, 2, ovr_matrix[1][2]) # Y_z → Z_z + mat.setCell(2, 3, ovr_matrix[1][3]) # 位移Z分量(Y位移) + + # 齐次坐标 + mat.setCell(3, 0, 0) + mat.setCell(3, 1, 0) + mat.setCell(3, 2, 0) + mat.setCell(3, 3, 1) + + # 🚀 优化调试信息 - 避免创建Vec3对象,减少GC压力 + if not hasattr(self, '_coord_debug_counter'): + self._coord_debug_counter = 0 + self._coord_debug_counter += 1 + + if self._coord_debug_counter % 600 == 1: # 每10秒输出一次@60fps + print(f"🔄 坐标系转换调试 (第{self._coord_debug_counter}帧)") + + # 直接输出数值,避免创建Vec3对象 + ovr_x, ovr_y, ovr_z = ovr_matrix[0][3], ovr_matrix[1][3], ovr_matrix[2][3] + print(f" OpenVR原始位置: ({ovr_x:.3f}, {ovr_y:.3f}, {ovr_z:.3f})") + + # 直接从矩阵读取数值,避免创建Vec3对象 + panda_x = mat.getCell(0, 3) + panda_y = mat.getCell(1, 3) + panda_z = mat.getCell(2, 3) + print(f" Panda3D转换位置: ({panda_x:.3f}, {panda_y:.3f}, {panda_z:.3f})") + + # 手动验证转换:OpenVR (x,y,z) → Panda3D (x,-z,y) + expected_x = ovr_x + expected_y = -ovr_z + expected_z = ovr_y + print(f" 预期转换结果: ({expected_x:.3f}, {expected_y:.3f}, {expected_z:.3f})") + + # 计算误差,避免创建Vec3对象 + diff_x = panda_x - expected_x + diff_y = panda_y - expected_y + diff_z = panda_z - expected_z + diff_magnitude = (diff_x*diff_x + diff_y*diff_y + diff_z*diff_z)**0.5 + + if diff_magnitude < 0.001: + print(f" ✅ 坐标转换正确 (误差: {diff_magnitude:.6f})") + else: + print(f" ⚠️ 坐标转换可能有误 (误差: {diff_magnitude:.6f})") + print(f" 差异: ({diff_x:.6f}, {diff_y:.6f}, {diff_z:.6f})") + + return mat + + def update_hmd(self, pose): + """ + 更新HMD锚点 - 基于参考实现 + """ + try: + # 将OpenVR姿态转换为Panda3D矩阵 + modelview = self.convert_mat(pose.mDeviceToAbsoluteTracking) + + # 应用坐标系转换并设置HMD锚点 + self.hmd_anchor.setMat(self.coord_mat_inv * modelview * self.coord_mat) + + # 获取眼睛到头部的变换 + view_left = self.convert_mat(self.vr_system.getEyeToHeadTransform(openvr.Eye_Left)) + view_right = self.convert_mat(self.vr_system.getEyeToHeadTransform(openvr.Eye_Right)) + + # 设置眼睛锚点 + self.left_eye_anchor.setMat(self.coord_mat_inv * view_left * self.coord_mat) + self.right_eye_anchor.setMat(self.coord_mat_inv * view_right * self.coord_mat) + + except Exception as e: + print(f"更新HMD姿态失败: {e}") + + def _update_camera_poses(self): + """更新相机姿态 - 使用锚点系统简化处理""" + try: + # 使用锚点系统后,相机位置自动跟随锚点 + # 只需要获取HMD姿态并更新锚点即可 + + # 从poses数组中获取HMD姿态 + if hasattr(self, 'poses') and len(self.poses) > 0: + hmd_pose = self.poses[openvr.k_unTrackedDeviceIndex_Hmd] + if hmd_pose.bPoseIsValid: + self.update_hmd(hmd_pose) + else: + print("⚠️ HMD姿态数据无效") + + except Exception as e: + print(f"更新相机姿态失败: {e}") + import traceback + traceback.print_exc() + + def _update_camera_poses_with_cache(self): + """使用缓存姿态更新相机 - 符合OpenVR时序假设""" + try: + # 使用缓存的姿态,符合OpenVR的时序假设 + # OpenVR假设你用上一次WaitGetPoses的姿态渲染当前提交的帧 + + if not self._cached_render_poses: + # 如果没有缓存姿态,回退到当前姿态(首帧情况) + print("⚠️ 没有缓存姿态,使用当前姿态") + return self._update_camera_poses() + + # 从缓存姿态数组中获取HMD姿态 + if len(self._cached_render_poses) > 0: + hmd_pose = self._cached_render_poses[openvr.k_unTrackedDeviceIndex_Hmd] + if hmd_pose.bPoseIsValid: + self.update_hmd(hmd_pose) + + # 调试信息 - 验证缓存姿态使用 + if not hasattr(self, '_cached_pose_logged'): + print("✅ 使用缓存姿态更新相机 - 符合OpenVR时序假设") + self._cached_pose_logged = True + else: + print("⚠️ 缓存的HMD姿态数据无效") + + except Exception as e: + print(f"使用缓存姿态更新相机失败: {e}") + # 回退到正常更新方式 + self._update_camera_poses() + import traceback + traceback.print_exc() + + + def enable_vr(self): + """启用VR模式""" + if not self.is_vr_available(): + print("❌ VR系统不可用") + return False + + if not self.vr_initialized: + if not self.initialize_vr(): + return False + + self.vr_enabled = True + + # 禁用主相机避免干扰VR渲染 + self._disable_main_cam() + + # VR性能优化:使用Running Start模式(Valve最佳实践) + print("🚀 VR性能优化:Running Start模式已启用") + print(" 优势:在帧开始时获取姿态,提供VSync前3ms的渲染时间") + print(" 注意:Submit后立即调用WaitGetPoses是错误实现") + self.set_prediction_time(11.0) # 11ms预测时间 - OpenVR标准值,平衡准确性和延迟 + + # 🚀 动态调整Qt Timer频率以支持VR + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + if hasattr(self.world.qtWidget, 'synchronizer'): + # 设置为144Hz,让OpenVR控制实际渲染节奏 + self.world.qtWidget.synchronizer.setInterval(int(1000/144)) + print("✓ Qt Timer调整为144Hz,让OpenVR控制VR渲染节奏") + + # 🔧 关键修复:检测并重建缺失的手柄visualizer + # 当渲染模式切换时,visualizer可能被清理但控制器对象仍存在 + if hasattr(self, 'left_controller') and self.left_controller: + if not self.left_controller.visualizer and self.left_controller.anchor_node: + print("🔧 检测到左手柄visualizer缺失,正在重建...") + self.left_controller._create_visualizer() + if self.left_controller.visualizer: + print("✅ 左手柄visualizer已重建") + + if hasattr(self, 'right_controller') and self.right_controller: + if not self.right_controller.visualizer and self.right_controller.anchor_node: + print("🔧 检测到右手柄visualizer缺失,正在重建...") + self.right_controller._create_visualizer() + if self.right_controller.visualizer: + print("✅ 右手柄visualizer已重建") + + print("✅ VR模式已启用") + return True + + def disable_vr(self): + """禁用VR模式""" + self.vr_enabled = False + + # 清理手柄可视化(但保留控制器对象以便重新启用) + if hasattr(self, 'left_controller') and self.left_controller: + if hasattr(self.left_controller, 'visualizer') and self.left_controller.visualizer: + try: + self.left_controller.visualizer.cleanup() + self.left_controller.visualizer = None + print("✓ 左手控制器可视化已清理") + except Exception as e: + print(f"⚠️ 清理左手控制器可视化失败: {e}") + + if hasattr(self, 'right_controller') and self.right_controller: + if hasattr(self.right_controller, 'visualizer') and self.right_controller.visualizer: + try: + self.right_controller.visualizer.cleanup() + self.right_controller.visualizer = None + print("✓ 右手控制器可视化已清理") + except Exception as e: + print(f"⚠️ 清理右手控制器可视化失败: {e}") + + # 隐藏手柄锚点节点 + if hasattr(self, 'left_controller') and self.left_controller and self.left_controller.anchor_node: + self.left_controller.anchor_node.hide() + + if hasattr(self, 'right_controller') and self.right_controller and self.right_controller.anchor_node: + self.right_controller.anchor_node.hide() + + # 恢复主相机 + self._enable_main_cam() + + # 恢复Qt Timer到60FPS + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + if hasattr(self.world.qtWidget, 'synchronizer'): + self.world.qtWidget.synchronizer.setInterval(int(1000/60)) + print("✓ Qt Timer恢复为60Hz") + + print("✅ VR模式已禁用,手柄模型已隐藏") + + def set_vr_render_mode(self, mode): + """切换VR渲染模式 + + Args: + mode: VRRenderMode枚举值或字符串 ('normal' 或 'render_pipeline') + + Returns: + bool: 切换是否成功 + """ + try: + # 转换输入为枚举类型 + if isinstance(mode, str): + mode_str = mode.lower() + if mode_str == "normal": + new_mode = VRRenderMode.NORMAL + elif mode_str in ["render_pipeline", "renderpipeline", "pipeline"]: + new_mode = VRRenderMode.RENDER_PIPELINE + else: + print(f"❌ 无效的渲染模式: {mode}") + print(" 支持的模式: 'normal' 或 'render_pipeline'") + return False + elif isinstance(mode, VRRenderMode): + new_mode = mode + else: + print(f"❌ 无效的模式类型: {type(mode)}") + return False + + # 检查是否与当前模式相同 + if new_mode == self.vr_render_mode: + print(f"✓ VR渲染模式已经是 {new_mode.value},无需切换") + return True + + print(f"🔄 正在切换VR渲染模式: {self.vr_render_mode.value} → {new_mode.value}") + + # 检查VR是否已初始化 + if not self.vr_initialized: + print("⚠️ VR未初始化,仅更新渲染模式配置") + self.vr_render_mode = new_mode + print(f"✓ VR渲染模式已更新为 {new_mode.value}(下次启动VR时生效)") + return True + + # 保存当前VR启用状态 + was_enabled = self.vr_enabled + + # 如果VR已启用,先禁用 + if was_enabled: + print(" 暂时禁用VR...") + self.disable_vr() + + # 更新渲染模式 + old_mode = self.vr_render_mode + self.vr_render_mode = new_mode + + # 清理现有缓冲区 + print(" 清理现有渲染缓冲区...") + self._cleanup_vr_buffers() + + # 根据新模式重建缓冲区 + print(f" 创建新的渲染缓冲区({new_mode.value})...") + success = False + + if new_mode == VRRenderMode.RENDER_PIPELINE: + success = self._create_vr_buffers_with_pipeline() + if not success: + print("⚠️ RenderPipeline模式创建失败,回退到普通渲染模式") + self.vr_render_mode = VRRenderMode.NORMAL + success = self._create_vr_buffers() + else: + success = self._create_vr_buffers() + + if not success: + print("❌ 缓冲区创建失败,尝试恢复原模式") + self.vr_render_mode = old_mode + if old_mode == VRRenderMode.RENDER_PIPELINE: + self._create_vr_buffers_with_pipeline() + else: + self._create_vr_buffers() + return False + + # 重新设置相机 + print(" 重新设置VR相机...") + if not self._setup_vr_cameras(): + print("❌ 相机设置失败") + return False + + # 如果之前VR是启用的,重新启用 + if was_enabled: + print(" 重新启用VR...") + self.enable_vr() + + # 🔧 关键修复:重建所有手柄的visualizer以适配新渲染模式 + print(" 刷新手柄visualizer以适配新渲染模式...") + if hasattr(self, 'left_controller') and self.left_controller: + self.left_controller.recreate_visualizer() + + if hasattr(self, 'right_controller') and self.right_controller: + self.right_controller.recreate_visualizer() + + print(f"✅ VR渲染模式已切换为 {self.vr_render_mode.value}") + + # 保存配置 + if self.config_manager: + self.config_manager.save_from_vr_manager(self) + + return True + + except Exception as e: + print(f"❌ 切换VR渲染模式失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_vr_render_mode(self): + """获取当前VR渲染模式 + + Returns: + VRRenderMode: 当前渲染模式 + """ + return self.vr_render_mode + + def _cleanup_vr_buffers(self): + """清理VR渲染缓冲区""" + try: + # 清理VR Pipeline Controller(如果使用RenderPipeline模式) + if self.vr_pipeline_controller: + print(" 清理VR Pipeline...") + self.vr_pipeline_controller.cleanup_all() + self.vr_pipeline_controller = None + + # 清理左眼缓冲区 + if self.vr_left_eye_buffer: + # 如果是RenderPipeline模式,buffer已被VRPipelineController清理 + # 如果是普通模式,需要手动清理 + if self.vr_render_mode == VRRenderMode.NORMAL: + self.world.graphicsEngine.removeWindow(self.vr_left_eye_buffer) + self.vr_left_eye_buffer = None + + # 清理右眼缓冲区 + if self.vr_right_eye_buffer: + if self.vr_render_mode == VRRenderMode.NORMAL: + self.world.graphicsEngine.removeWindow(self.vr_right_eye_buffer) + self.vr_right_eye_buffer = None + + # 清理纹理 + self.vr_left_texture = None + self.vr_right_texture = None + self.left_texture_id = None + self.right_texture_id = None + self.textures_prepared = False + + # 清理RenderPipeline渲染目标(旧版,保留兼容性) + if self.vr_pipeline_left_target: + self.vr_pipeline_left_target = None + + if self.vr_pipeline_right_target: + self.vr_pipeline_right_target = None + + self.render_pipeline_enabled = False + + print("✓ VR渲染缓冲区已清理") + + except Exception as e: + print(f"⚠️ 清理VR缓冲区时出错: {e}") + + def cleanup(self): + """清理VR资源""" + try: + print("🔄 正在清理VR资源...") + + # 停止VR任务 + if self.vr_task: + self.world.taskMgr.remove(self.vr_task) + self.vr_task = None + + # 🚀 恢复Python垃圾回收并清理对象池 + if self.optimization and self.optimization._gc_disabled: + # 最后一次手动垃圾回收 + collected = gc.collect() + print(f"🗑️ 最终GC清理: {collected} 个对象") + + # 恢复自动垃圾回收 + gc.enable() + self.optimization._gc_disabled = False + print("✅ Python垃圾回收已恢复为自动模式") + + # 清理对象池 + if self.optimization and hasattr(self.optimization, '_matrix_pool'): + pool_size = len(self.optimization._matrix_pool) + self.optimization._matrix_pool.clear() + print(f"🧹 Mat4对象池已清理: {pool_size} 个对象") + + # 清理缓存 + if self.optimization and hasattr(self.optimization, '_cached_matrices'): + self.optimization._cached_matrices.clear() + if self.optimization and hasattr(self.optimization, '_controller_poses_cache'): + self.optimization._controller_poses_cache.clear() + + # 清理OpenVR Texture对象缓存 + # 清理OpenVR Texture对象缓存 + if self.optimization: + self.optimization._left_ovr_texture = None + self.optimization._right_ovr_texture = None + print("🧹 OpenVR Texture对象缓存已清理") + + # 清理渲染缓冲区 + if self.vr_left_eye_buffer: + self.vr_left_eye_buffer.removeAllDisplayRegions() + self.world.graphicsEngine.removeWindow(self.vr_left_eye_buffer) + self.vr_left_eye_buffer = None + + if self.vr_right_eye_buffer: + self.vr_right_eye_buffer.removeAllDisplayRegions() + self.world.graphicsEngine.removeWindow(self.vr_right_eye_buffer) + self.vr_right_eye_buffer = None + + # 清理相机 + if self.vr_left_camera: + self.vr_left_camera.removeNode() + self.vr_left_camera = None + + if self.vr_right_camera: + self.vr_right_camera.removeNode() + self.vr_right_camera = None + + # 清理控制器 + if hasattr(self, 'left_controller') and self.left_controller: + try: + self.left_controller.cleanup() + self.left_controller = None + print("✓ 左手控制器已清理") + except Exception as e: + print(f"⚠️ 清理左手控制器失败: {e}") + + if hasattr(self, 'right_controller') and self.right_controller: + try: + self.right_controller.cleanup() + self.right_controller = None + print("✓ 右手控制器已清理") + except Exception as e: + print(f"⚠️ 清理右手控制器失败: {e}") + + # 清理控制器字典 + if hasattr(self, 'controllers'): + self.controllers.clear() + print("✓ 控制器字典已清理") + + # 清理VR子系统 + if hasattr(self, 'joystick_manager') and self.joystick_manager: + try: + self.joystick_manager.cleanup() + self.joystick_manager = None + print("✓ VR摇杆系统已清理") + except Exception as e: + print(f"⚠️ 清理VR摇杆系统失败: {e}") + + if hasattr(self, 'teleport_system') and self.teleport_system: + try: + self.teleport_system.cleanup() + self.teleport_system = None + print("✓ VR传送系统已清理") + except Exception as e: + print(f"⚠️ 清理VR传送系统失败: {e}") + + if hasattr(self, 'interaction_manager') and self.interaction_manager: + try: + self.interaction_manager.cleanup() + self.interaction_manager = None + print("✓ VR交互系统已清理") + except Exception as e: + print(f"⚠️ 清理VR交互系统失败: {e}") + + if hasattr(self, 'performance_monitor') and self.performance_monitor: + try: + # 性能监控系统不需要特殊清理,只需要置空引用 + self.performance_monitor = None + print("✓ VR性能监控系统已清理") + except Exception as e: + print(f"⚠️ 清理VR性能监控系统失败: {e}") + + if hasattr(self, 'action_manager') and self.action_manager: + try: + self.action_manager.cleanup() + self.action_manager = None + print("✓ VR动作系统已清理") + except Exception as e: + print(f"⚠️ 清理VR动作系统失败: {e}") + + # 关闭OpenVR + if self.vr_system and OPENVR_AVAILABLE: + try: + openvr.shutdown() + except: + pass + self.vr_system = None + + self.vr_enabled = False + self.vr_initialized = False + + print("✅ VR资源清理完成") + + except Exception as e: + print(f"⚠️ VR清理过程中出错: {e}") + + def get_vr_status(self): + """获取VR状态信息""" + return { + 'available': self.is_vr_available(), + 'initialized': self.vr_initialized, + 'enabled': self.vr_enabled, + 'eye_resolution': (self.eye_width, self.eye_height), + 'device_count': len(self.controller_poses) + (1 if self.vr_enabled else 0), + 'vr_fps': self.vr_fps, + 'frame_count': self.frame_count, + 'submit_failures': self.submit_failures, + 'pose_failures': self.pose_failures + } + + def submit_texture(self, eye, texture): + """优化的VR纹理提交 - 使用缓存的纹理ID,避免重复prepareNow""" + try: + if not self.vr_compositor: + print("❌ VR compositor不可用") + self.submit_failures += 1 + return + + # 🚀 关键修复:防止同一帧重复提交 + current_frame = getattr(self, 'frame_count', 0) + if not hasattr(self, '_last_submit_frame'): + self._last_submit_frame = {} + + # 检查当前帧是否已经提交过此眼睛的纹理 + if eye in self._last_submit_frame and self._last_submit_frame[eye] == current_frame: + # 静默跳过,避免spam输出 + return + + # 记录当前帧提交 + self._last_submit_frame[eye] = current_frame + + # 🚀 关键优化:直接使用缓存的纹理ID,避免重复prepareNow + if eye == openvr.Eye_Left: + handle = self.left_texture_id + eye_name = "左眼" + elif eye == openvr.Eye_Right: + handle = self.right_texture_id + eye_name = "右眼" + else: + print(f"❌ 未知的眼睛类型: {eye}") + self.submit_failures += 1 + return + + # 检查缓存的纹理ID是否有效 + if not handle or handle <= 0: + print(f"❌ {eye_name}纹理ID缓存无效: {handle}") + print(" 这可能表示纹理准备失败,需要检查_prepare_and_cache_textures()") + self.submit_failures += 1 + return + + # ❌ 移除gsg.flush()调用 - 基于OpenVR官方实践 + # gsg.flush()等同于OpenGL的glFlush(),导致强制CPU-GPU同步 + # 每帧调用2次(左右眼)是GPU周期性峰值的主要原因 + # 参考:OpenVR社区经验表明同步调用是性能杀手 + # + # gsg = self.world.win.getGsg() + # if gsg and hasattr(gsg, 'flush'): + # try: + # gsg.flush() + # except Exception as flush_error: + # if not hasattr(self, '_gsg_flush_error_logged'): + # print(f"⚠️ GSG刷新失败: {flush_error}") + # self._gsg_flush_error_logged = True + + # 🚀 关键优化:使用缓存的OpenVR Texture对象,避免每帧创建 + if eye == openvr.Eye_Left: + ovr_texture = self.optimization._left_ovr_texture if self.optimization else None + else: + ovr_texture = self.optimization._right_ovr_texture if self.optimization else None + + # 检查缓存对象是否存在(向后兼容) + if ovr_texture is None: + # 备用方案:如果缓存对象不存在,创建新的(性能较差) + ovr_texture = openvr.Texture_t() + ovr_texture.eType = openvr.TextureType_OpenGL + ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + if not hasattr(self, '_texture_fallback_warned'): + print("⚠️ 使用Texture对象备用方案(性能次优)") + self._texture_fallback_warned = True + + # 只更新handle,其他属性已预设置 + ovr_texture.handle = handle + + # 提交到VR系统 + error = self.vr_compositor.submit(eye, ovr_texture) + + # 检查错误 + if error and error != openvr.VRCompositorError_None: + print(f"⚠️ VR{eye_name}纹理提交错误: {error}") + self.submit_failures += 1 + else: + # 只在第一次成功时输出 + if not hasattr(self, '_optimized_submit_success_logged'): + print(f"✅ 优化版VR纹理提交成功! 使用缓存ID,避免重复prepareNow") + print(f" {eye_name}纹理ID: {handle}") + self._optimized_submit_success_logged = True + + # 🔧 智能GPU同步策略 - 减少不必要的flush调用 + # 只有在检测到性能问题或每N帧时才强制flush + if eye == openvr.Eye_Right: + self._smart_gpu_sync() + + except Exception as e: + print(f"❌ VR纹理提交异常: {e}") + import traceback + traceback.print_exc() + self.submit_failures += 1 + + def _smart_gpu_sync(self): + """智能GPU同步策略 - 只在必要时进行同步""" + try: + # 初始化同步控制变量 + if not hasattr(self, '_last_gpu_sync_frame'): + self._last_gpu_sync_frame = 0 + self._gpu_sync_interval = 60 # 每60帧强制同步一次(1秒@60FPS) + self._performance_based_sync = True + + current_frame = getattr(self, 'frame_count', 0) + + # 策略1: 性能自适应同步 + if self._performance_based_sync: + # 如果渲染时间过长,增加同步频率 + total_render_time = getattr(self, 'left_render_time', 0) + getattr(self, 'right_render_time', 0) + + if total_render_time > 15.0: # 如果总渲染时间超过15ms + # 高负载时更频繁同步 + sync_interval = 30 # 每30帧同步 + elif total_render_time > 10.0: + sync_interval = 45 # 中等负载 + else: + sync_interval = self._gpu_sync_interval # 正常负载 + + should_sync = (current_frame - self._last_gpu_sync_frame) >= sync_interval + else: + # 策略2: 固定间隔同步 + should_sync = (current_frame - self._last_gpu_sync_frame) >= self._gpu_sync_interval + + # 执行同步 + if should_sync: + gsg = self.world.win.getGsg() + if gsg and hasattr(gsg, 'flush'): + try: + gsg.flush() + self._last_gpu_sync_frame = current_frame + + # 只在首次或Debug时输出 + if not hasattr(self, '_smart_sync_logged') or self.debug_output_enabled: + if not hasattr(self, '_smart_sync_logged'): + print(f"🔧 智能GPU同步已启用 - 间隔:{sync_interval}帧") + self._smart_sync_logged = True + elif self.debug_output_enabled and current_frame % 600 == 1: + print(f"🔧 智能同步触发 (帧#{current_frame}, 间隔:{sync_interval})") + + except Exception as flush_error: + if not hasattr(self, '_smart_sync_error_logged'): + print(f"⚠️ 智能GPU同步失败: {flush_error}") + self._smart_sync_error_logged = True + + except Exception as e: + print(f"⚠️ 智能GPU同步策略异常: {e}") + + def _disable_main_cam(self): + """禁用主相机 - 基于参考实现""" + try: + # 保存原始相机状态 + if not hasattr(self, '_original_camera_parent'): + self._original_camera_parent = self.world.camera.getParent() + + # 创建空节点并将主相机重新附加到它 + self._empty_world = NodePath("empty_world") + self.world.camera.reparentTo(self._empty_world) + + print("✓ 主相机已禁用") + except Exception as e: + print(f"⚠️ 禁用主相机失败: {e}") + + def _enable_main_cam(self): + """恢复主相机 - 基于参考实现""" + try: + # 恢复原始相机状态 + if hasattr(self, '_original_camera_parent') and self._original_camera_parent: + self.world.camera.reparentTo(self._original_camera_parent) + else: + # 如果没有保存的父节点,重新附加到render + self.world.camera.reparentTo(self.world.render) + + # 清理空世界节点 + if hasattr(self, '_empty_world'): + self._empty_world.removeNode() + delattr(self, '_empty_world') + + print("✓ 主相机已恢复") + except Exception as e: + print(f"⚠️ 恢复主相机失败: {e}") + + def _initialize_controllers(self): + """初始化VR手柄控制器""" + try: + print("🎮 正在初始化VR手柄控制器...") + + # 创建左右手柄控制器实例 + self.left_controller = LeftController(self) + self.right_controller = RightController(self) + + # 检测现有连接的控制器 + self._detect_controllers() + + print("✓ VR手柄控制器初始化完成") + + except Exception as e: + print(f"⚠️ VR手柄初始化失败: {e}") + import traceback + traceback.print_exc() + + def _detect_controllers(self): + """检测并连接VR控制器""" + if not self.vr_system: + return + + try: + for device_index in range(openvr.k_unMaxTrackedDeviceCount): + # 检查设备是否已连接 + if not self.vr_system.isTrackedDeviceConnected(device_index): + continue + + # 获取设备类型 + device_class = self.vr_system.getTrackedDeviceClass(device_index) + + if device_class == openvr.TrackedDeviceClass_Controller: + # 获取控制器角色 + role = self.vr_system.getControllerRoleForTrackedDeviceIndex(device_index) + + if role == openvr.TrackedControllerRole_LeftHand and self.left_controller: + self.left_controller.set_device_index(device_index) + self.controllers[device_index] = self.left_controller + # 为设备创建锚点 + self._create_tracked_device_anchor(device_index, 'left_controller') + + elif role == openvr.TrackedControllerRole_RightHand and self.right_controller: + self.right_controller.set_device_index(device_index) + self.controllers[device_index] = self.right_controller + # 为设备创建锚点 + self._create_tracked_device_anchor(device_index, 'right_controller') + + print(f"🎮 检测到 {len(self.controllers)} 个控制器") + + except Exception as e: + print(f"⚠️ 控制器检测失败: {e}") + + def _create_tracked_device_anchor(self, device_index, name): + """为跟踪设备创建锚点节点""" + if not self.tracking_space: + print(f"⚠️ 无法为设备 {device_index} 创建锚点 - tracking_space未初始化") + return + + try: + # 获取设备模型名称 + if self.vr_system: + model_name = self.vr_system.getStringTrackedDeviceProperty( + device_index, + openvr.Prop_RenderModelName_String + ) + anchor_name = f"{device_index}:{model_name}:{name}" + else: + anchor_name = f"{device_index}:{name}" + + # 创建锚点节点 + device_anchor = self.tracking_space.attachNewNode(anchor_name) + self.tracked_device_anchors[device_index] = device_anchor + + print(f"✓ 为设备 {device_index} 创建锚点: {anchor_name}") + + except Exception as e: + print(f"⚠️ 创建设备锚点失败: {e}") + + def update_tracked_devices(self): + """更新所有跟踪设备的姿态 - 基于参考实现""" + if not self.poses or not self.vr_system: + return + + try: + # 更新每个已连接的控制器 + for device_index, controller in self.controllers.items(): + if device_index < len(self.poses): + pose_data = self.poses[device_index] + + # 更新控制器姿态 + controller.update_pose(pose_data) + + # 更新控制器输入状态 + controller.update_input_state(self.vr_system) + + # 更新其他跟踪设备的锚点 + for device_index in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): + if device_index in self.tracked_device_anchors: + pose_data = self.poses[device_index] + if pose_data.bPoseIsValid: + # 转换姿态矩阵 + modelview = self.convert_mat(pose_data.mDeviceToAbsoluteTracking) + final_matrix = self.coord_mat_inv * modelview * self.coord_mat + + # 更新锚点变换 + anchor = self.tracked_device_anchors[device_index] + anchor.setMat(final_matrix) + anchor.show() + else: + # 姿态无效,隐藏锚点 + self.tracked_device_anchors[device_index].hide() + + except Exception as e: + if self.frame_count % 300 == 0: # 每5秒输出一次错误 + print(f"⚠️ 更新跟踪设备失败: {e}") + + def get_controller_by_role(self, role): + """根据角色获取控制器 + + Args: + role: 'left' 或 'right' + + Returns: + VRController实例或None + """ + if role == 'left': + return self.left_controller + elif role == 'right': + return self.right_controller + return None + + def are_controllers_connected(self): + """检查是否有控制器连接""" + return len(self.controllers) > 0 + + def get_connected_controllers(self): + """获取所有连接的控制器列表""" + return list(self.controllers.values()) + + def trigger_controller_haptic(self, role, duration=0.001, strength=1.0): + """触发控制器震动反馈 + + Args: + role: 'left', 'right' 或 'both' + duration: 震动持续时间(秒) + strength: 震动强度 (0.0-1.0) + """ + if role in ['left', 'both'] and self.left_controller: + self.left_controller.trigger_haptic_feedback(duration, strength) + + if role in ['right', 'both'] and self.right_controller: + self.right_controller.trigger_haptic_feedback(duration, strength) + + # VR动作系统便捷方法 + def is_trigger_pressed(self, hand='any'): + """检查扳机是否被按下 + + Args: + hand: 'left', 'right', 'any' + """ + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_pressed('trigger', device_path) + return pressed + + def is_trigger_just_pressed(self, hand='any'): + """检查扳机是否刚刚被按下""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_just_pressed('trigger', device_path) + return pressed + + def is_grip_pressed(self, hand='any'): + """检查握把是否被按下""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_pressed('grip', device_path) + return pressed + + def is_grip_just_pressed(self, hand='any'): + """检查握把是否刚刚被按下""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_just_pressed('grip', device_path) + return pressed + + def is_menu_pressed(self, hand='any'): + """检查菜单按钮是否被按下""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_pressed('menu', device_path) + return pressed + + def is_trackpad_touched(self, hand='any'): + """检查触摸板是否被触摸""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + touched, _ = self.action_manager.is_digital_action_pressed('trackpad_touch', device_path) + return touched + + def get_trackpad_position(self, hand='any'): + """获取触摸板位置 + + Returns: + Vec2或None: 触摸板位置 (-1到1的范围) + """ + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + value, _ = self.action_manager.get_analog_action_value('trackpad', device_path) + return value + + # VR交互系统便捷方法 + def get_selected_object(self, hand='any'): + """获取指定手选中的对象 + + Args: + hand: 'left', 'right', 'any' + + Returns: + 选中的对象节点或None + """ + if hand == 'any': + # 返回任意手选中的对象 + for controller in self.get_connected_controllers(): + selected = self.interaction_manager.get_selected_object(controller.name) + if selected: + return selected + return None + else: + return self.interaction_manager.get_selected_object(hand) + + def get_grabbed_object(self, hand='any'): + """获取指定手抓取的对象 + + Args: + hand: 'left', 'right', 'any' + + Returns: + 抓取的对象节点或None + """ + if hand == 'any': + # 返回任意手抓取的对象 + for controller in self.get_connected_controllers(): + grabbed = self.interaction_manager.get_grabbed_object(controller.name) + if grabbed: + return grabbed + return None + else: + return self.interaction_manager.get_grabbed_object(hand) + + def is_grabbing_object(self, hand='any'): + """检查是否正在抓取对象 + + Args: + hand: 'left', 'right', 'any' + + Returns: + bool: 是否正在抓取 + """ + if hand == 'any': + # 检查任意手是否正在抓取 + for controller in self.get_connected_controllers(): + if self.interaction_manager.is_grabbing(controller.name): + return True + return False + else: + return self.interaction_manager.is_grabbing(hand) + + def force_release_all_grabs(self): + """强制释放所有抓取的对象""" + self.interaction_manager.force_release_all() + + def add_interactable_object(self, object_node): + """将对象标记为可交互 + + Args: + object_node: 要标记的对象节点 + """ + self.interaction_manager._add_collision_to_object(object_node) + + def _disable_async_reprojection(self): + """禁用异步重投影 - 备选修复方案""" + try: + # 尝试通过OpenVR设置禁用异步重投影 + if hasattr(openvr, 'VRSettings'): + settings = openvr.VRSettings() + if settings: + # 禁用异步重投影 + error = settings.setBool("steamvr", "enableAsyncReprojection", False) + if error == openvr.VRSettingsError_None: + print("✅ 异步重投影已禁用") + else: + print(f"⚠️ 禁用异步重投影失败: 设置错误 {error}") + else: + print("⚠️ 无法获取VR设置接口") + else: + print("⚠️ OpenVR设置接口不可用") + + except Exception as e: + print(f"⚠️ 禁用异步重投影失败: {e}") + + def enable_async_reprojection_disable(self): + """启用异步重投影禁用选项 - 用户可调用的方法""" + self.disable_async_reprojection = True + print("📝 异步重投影禁用选项已启用,将在下次VR初始化时生效") + + def disable_async_reprojection_disable(self): + """禁用异步重投影禁用选项 - 恢复默认行为""" + self.disable_async_reprojection = False + print("📝 异步重投影禁用选项已关闭,将使用默认ATW行为") + + + + # ====== VR分辨率缩放和质量预设系统 ====== + + + + + + + # ====== 性能模式控制方法 ====== + + + + # ======================================================================== + # 性能监控属性代理 - 属性级别的API向后兼容 + # ======================================================================== + + @property + def left_render_count(self): + """左眼渲染计数器 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.left_render_count + return 0 + + @left_render_count.setter + def left_render_count(self, value): + """左眼渲染计数器设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.left_render_count = value + + @property + def right_render_count(self): + """右眼渲染计数器 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.right_render_count + return 0 + + @right_render_count.setter + def right_render_count(self, value): + """右眼渲染计数器设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.right_render_count = value + + @property + def left_render_time(self): + """左眼渲染时间 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.left_render_time + return 0.0 + + @left_render_time.setter + def left_render_time(self, value): + """左眼渲染时间设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.left_render_time = value + + @property + def right_render_time(self): + """右眼渲染时间 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.right_render_time + return 0.0 + + @right_render_time.setter + def right_render_time(self, value): + """右眼渲染时间设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.right_render_time = value + + # 帧计数和FPS相关属性 + @property + def frame_count(self): + """帧计数器 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.frame_count + return 0 + + @frame_count.setter + def frame_count(self, value): + """帧计数器设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.frame_count = value + + @property + def vr_fps(self): + """VR帧率 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.vr_fps + return 0.0 + + @vr_fps.setter + def vr_fps(self, value): + """VR帧率设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.vr_fps = value + + @property + def last_fps_check(self): + """上次FPS检查 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.last_fps_check + return 0 + + @last_fps_check.setter + def last_fps_check(self, value): + """上次FPS检查设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.last_fps_check = value + + @property + def last_fps_time(self): + """上次FPS时间 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.last_fps_time + return 0.0 + + @last_fps_time.setter + def last_fps_time(self, value): + """上次FPS时间设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.last_fps_time = value + + # 失败计数器 + @property + def submit_failures(self): + """提交失败计数 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.submit_failures + return 0 + + @submit_failures.setter + def submit_failures(self, value): + """提交失败计数设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.submit_failures = value + + @property + def pose_failures(self): + """姿态失败计数 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.pose_failures + return 0 + + @pose_failures.setter + def pose_failures(self, value): + """姿态失败计数设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.pose_failures = value + + # 性能监控开关 + @property + def performance_monitoring(self): + """性能监控开关 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.performance_monitoring + return False + + @performance_monitoring.setter + def performance_monitoring(self, value): + """性能监控开关设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.performance_monitoring = value + + @property + def debug_output_enabled(self): + """调试输出开关 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.debug_output_enabled + return False + + @debug_output_enabled.setter + def debug_output_enabled(self, value): + """调试输出开关设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.debug_output_enabled = value + + + + # 时间监控属性 + @property + def wait_poses_time(self): + """姿态等待时间 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.wait_poses_time + return 0.0 + + @wait_poses_time.setter + def wait_poses_time(self, value): + """姿态等待时间设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.wait_poses_time = value + + @property + def submit_time(self): + """纹理提交时间 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.submit_time + return 0.0 + + @submit_time.setter + def submit_time(self, value): + """纹理提交时间设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.submit_time = value + + # 监控开关 + @property + def enable_gpu_timing(self): + """GPU计时开关 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.enable_gpu_timing + return False + + @enable_gpu_timing.setter + def enable_gpu_timing(self, value): + """GPU计时开关设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.enable_gpu_timing = value + + @property + def enable_pipeline_monitoring(self): + """管线监控开关 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.enable_pipeline_monitoring + return True + + @enable_pipeline_monitoring.setter + def enable_pipeline_monitoring(self, value): + """管线监控开关设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.enable_pipeline_monitoring = value + + @property + def performance_mode_enabled(self): + """性能模式开关 - 代理到优化模块""" + if self.optimization: + return self.optimization.performance_mode_enabled + return False + + @performance_mode_enabled.setter + def performance_mode_enabled(self, value): + """性能模式开关设置 - 代理到优化模块""" + if self.optimization: + self.optimization.performance_mode_enabled = value + + @property + def performance_mode_trigger_frame(self): + """性能模式触发帧 - 代理到优化模块""" + if self.optimization: + return self.optimization.performance_mode_trigger_frame + return 600 + + @performance_mode_trigger_frame.setter + def performance_mode_trigger_frame(self, value): + """性能模式触发帧设置 - 代理到优化模块""" + if self.optimization: + self.optimization.performance_mode_trigger_frame = value + + # GC控制属性代理 - 修复'gc_control_enabled'属性访问问题 + @property + def _gc_control_enabled(self): + """GC控制启用状态 - 代理到优化模块""" + if self.optimization: + return self.optimization._gc_control_enabled + return True + + @_gc_control_enabled.setter + def _gc_control_enabled(self, value): + """GC控制启用状态设置 - 代理到优化模块""" + if self.optimization: + self.optimization._gc_control_enabled = value + + @property + def _gc_disabled(self): + """GC禁用状态 - 代理到优化模块""" + if self.optimization: + return self.optimization._gc_disabled + return False + + @_gc_disabled.setter + def _gc_disabled(self, value): + """GC禁用状态设置 - 代理到优化模块""" + if self.optimization: + self.optimization._gc_disabled = value + + @property + def _manual_gc_interval(self): + """手动GC间隔 - 代理到优化模块""" + if self.optimization: + return self.optimization._manual_gc_interval + return 900 + + @_manual_gc_interval.setter + def _manual_gc_interval(self, value): + """手动GC间隔设置 - 代理到优化模块""" + if self.optimization: + self.optimization._manual_gc_interval = value + + # 分辨率缩放属性代理 + @property + def resolution_scale(self): + """分辨率缩放系数 - 代理到优化模块""" + if self.optimization: + return self.optimization.resolution_scale + return 0.75 + + @resolution_scale.setter + def resolution_scale(self, value): + """分辨率缩放系数设置 - 代理到优化模块""" + if self.optimization: + self.optimization.resolution_scale = value + + @property + def base_eye_width(self): + """基础眼宽 - 代理到优化模块""" + if self.optimization: + return self.optimization.base_eye_width + return 1080 + + @base_eye_width.setter + def base_eye_width(self, value): + """基础眼宽设置 - 代理到优化模块""" + if self.optimization: + self.optimization.base_eye_width = value + + @property + def base_eye_height(self): + """基础眼高 - 代理到优化模块""" + if self.optimization: + return self.optimization.base_eye_height + return 1200 + + @base_eye_height.setter + def base_eye_height(self, value): + """基础眼高设置 - 代理到优化模块""" + if self.optimization: + self.optimization.base_eye_height = value + + @property + def scaled_eye_width(self): + """缩放后眼宽 - 代理到优化模块""" + if self.optimization: + return self.optimization.scaled_eye_width + return 1080 + + @scaled_eye_width.setter + def scaled_eye_width(self, value): + """缩放后眼宽设置 - 代理到优化模块""" + if self.optimization: + self.optimization.scaled_eye_width = value + + @property + def scaled_eye_height(self): + """缩放后眼高 - 代理到优化模块""" + if self.optimization: + return self.optimization.scaled_eye_height + return 1200 + + @scaled_eye_height.setter + def scaled_eye_height(self, value): + """缩放后眼高设置 - 代理到优化模块""" + if self.optimization: + self.optimization.scaled_eye_height = value + + @property + def current_quality_preset(self): + """当前质量预设 - 代理到优化模块""" + if self.optimization: + return self.optimization.current_quality_preset + return "balanced" + + @current_quality_preset.setter + def current_quality_preset(self, value): + """当前质量预设设置 - 代理到优化模块""" + if self.optimization: + self.optimization.current_quality_preset = value + + @property + def quality_presets(self): + """质量预设 - 代理到优化模块""" + if self.optimization: + return self.optimization.quality_presets + return { + "performance": 0.6, + "balanced": 0.75, + "quality": 1.0 + } + + @quality_presets.setter + def quality_presets(self, value): + """质量预设设置 - 代理到优化模块""" + if self.optimization: + self.optimization.quality_presets = value + + # ======================================================================== + # 性能监控委托方法 - API向后兼容层 + # ======================================================================== + + # 性能报告方法 + def _print_performance_report(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._print_performance_report() + + def _print_performance_recommendations(self, stats): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._print_performance_recommendations(stats) + + def _print_brief_performance_report(self, stats): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._print_brief_performance_report(stats) + + # 性能监控核心方法 + def _update_performance_metrics(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._update_performance_metrics() + + def _update_gpu_metrics(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._update_gpu_metrics() + + def _track_frame_time(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._track_frame_time() + + # GPU计时方法 + def _get_gpu_frame_timing(self, frames_ago=0): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._get_gpu_frame_timing(frames_ago) + return None + + def enable_gpu_timing_monitoring(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.enable_gpu_timing_monitoring() + + def disable_gpu_timing_monitoring(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.disable_gpu_timing_monitoring() + + # 管线统计方法 + def _start_timing(self, operation_name): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._start_timing(operation_name) + return None + + def _end_timing(self, timing_data): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._end_timing(timing_data) + return 0.0 + + def _get_pipeline_stats(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._get_pipeline_stats() + return {} + + def test_pipeline_monitoring(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.test_pipeline_monitoring() + + # 诊断方法 + def _print_render_callback_diagnostics(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._print_render_callback_diagnostics() + + def _check_rendering_optimizations(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._check_rendering_optimizations() + + def _diagnose_opengl_state(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._diagnose_opengl_state() + + # 调试控制方法 + def enable_debug_output(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.enable_debug_output() + + def disable_debug_output(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.disable_debug_output() + + def set_debug_mode(self, mode): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.set_debug_mode(mode) + + def toggle_debug_output(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.toggle_debug_output() + return False + + def get_debug_status(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.get_debug_status() + return {} + + # 配置方法 + def set_performance_check_interval(self, interval): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.set_performance_check_interval(interval) + + def set_frame_time_history_size(self, size): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.set_frame_time_history_size(size) + + def set_performance_report_interval(self, frames): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.set_performance_report_interval(frames) + + def set_prediction_time(self, prediction_time_ms): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.set_prediction_time(prediction_time_ms) + + # 查询方法 + def get_performance_stats(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.get_performance_stats() + return {} + + def get_current_performance_summary(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.get_current_performance_summary() + return "性能监控未初始化" + + def get_performance_monitoring_config(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.get_performance_monitoring_config() + return {} + + def print_performance_monitoring_status(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.print_performance_monitoring_status() + + # 控制方法 + def enable_performance_monitoring(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.enable_performance_monitoring() + + def disable_performance_monitoring(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.disable_performance_monitoring() + + def force_performance_report(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.force_performance_report() + + def reset_performance_counters(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.reset_performance_counters() + + # ======================================================================== + # 纹理提交方法 - VR渲染核心功能 + # ======================================================================== + + def _batch_submit_textures(self): + """批量提交两眼纹理 - OpenVR最佳实践 + """ + try: + if not self.vr_compositor: + return False + + # 检查纹理是否准备好 + if not (self.vr_left_texture and self.vr_right_texture): + return False + + import openvr + + # 🚀 关键:快速连续提交两眼,最小化阻塞时间 + # 这符合OpenVR官方示例的做法 + success_left = False + success_right = False + + # 提交左眼纹理 + try: + self.submit_texture(openvr.Eye_Left, self.vr_left_texture) + success_left = True + except Exception as e: + print(f"❌ 批量提交左眼失败: {e}") + + # 立即提交右眼纹理(不等待) + try: + self.submit_texture(openvr.Eye_Right, self.vr_right_texture) + success_right = True + except Exception as e: + print(f"❌ 批量提交右眼失败: {e}") + + # 🚀 关键修复:调用PostPresentHandoff解除compositor阻塞 + # 这是解决36FPS问题的核心 - 确保compositor不会等待VSync + if success_left and success_right: + try: + # PostPresentHandoff告诉compositor我们已完成帧处理 + # 防止compositor等待下一个VSync周期 + if hasattr(self.vr_compositor, 'postPresentHandoff'): + self.vr_compositor.postPresentHandoff() + elif hasattr(self.vr_compositor, 'PostPresentHandoff'): + self.vr_compositor.PostPresentHandoff() + else: + # 备用方案:如果没有PostPresentHandoff,记录警告 + if not hasattr(self, '_post_present_warning_logged'): + print("⚠️ PostPresentHandoff方法未找到,可能影响时序") + self._post_present_warning_logged = True + except Exception as handoff_error: + if not hasattr(self, '_handoff_error_logged'): + print(f"⚠️ PostPresentHandoff调用失败: {handoff_error}") + self._handoff_error_logged = True + + # 记录批量提交状态(仅首次成功时) + if not hasattr(self, '_batch_submit_success_logged'): + print("✅ OpenVR批量提交模式+PostPresentHandoff已启用") + print(" 这应该解决36FPS → 72FPS的问题") + self._batch_submit_success_logged = True + return True + else: + return False + + except Exception as e: + print(f"❌ 批量提交纹理失败: {e}") + return False + + def enable_vr_test_mode(self, display_mode='stereo'): + """委托到测试模式系统""" + if self.test_mode: + return self.test_mode.enable_vr_test_mode(display_mode) + return False + + def disable_vr_test_mode(self): + """委托到测试模式系统""" + if self.test_mode: + return self.test_mode.disable_vr_test_mode() + return False + + def switch_test_display_mode(self, display_mode): + """委托到测试模式系统""" + if self.test_mode: + return self.test_mode.switch_test_display_mode(display_mode) + return False + + def get_test_mode_status(self): + """委托到测试模式系统""" + if self.test_mode: + return self.test_mode.get_test_mode_status() + return None + + def get_test_mode_features(self): + """委托到测试模式系统""" + if self.test_mode: + return self.test_mode.get_test_mode_features() + return {} + + def set_test_mode_features(self, submit_texture=None, wait_poses=None): + """委托到测试模式系统""" + if self.test_mode: + return self.test_mode.set_test_mode_features(submit_texture, wait_poses) + + def run_vr_performance_test(self, duration_seconds=30, display_mode='stereo'): + """委托到测试模式系统""" + if self.test_mode: + return self.test_mode.run_vr_performance_test(duration_seconds, display_mode) + return None + + @property + def vr_test_mode(self): + """测试模式启用状态 - 代理到测试模式系统""" + if self.test_mode: + return self.test_mode.vr_test_mode + return False + + @vr_test_mode.setter + def vr_test_mode(self, value): + """测试模式启用状态设置 - 代理到测试模式系统""" + if self.test_mode: + self.test_mode.vr_test_mode = value + + @property + def test_display_mode(self): + """测试显示模式 - 代理到测试模式系统""" + if self.test_mode: + return self.test_mode.test_display_mode + return 'stereo' + + @test_display_mode.setter + def test_display_mode(self, value): + """测试显示模式设置 - 代理到测试模式系统""" + if self.test_mode: + self.test_mode.test_display_mode = value + + @property + def test_mode_submit_texture(self): + """测试模式纹理提交开关 - 代理到测试模式系统""" + if self.test_mode: + return self.test_mode.test_mode_submit_texture + return False + + @test_mode_submit_texture.setter + def test_mode_submit_texture(self, value): + """测试模式纹理提交开关设置 - 代理到测试模式系统""" + if self.test_mode: + self.test_mode.test_mode_submit_texture = value + + @property + def test_mode_wait_poses(self): + """测试模式姿态等待开关 - 代理到测试模式系统""" + if self.test_mode: + return self.test_mode.test_mode_wait_poses + return False + + @test_mode_wait_poses.setter + def test_mode_wait_poses(self, value): + """测试模式姿态等待开关设置 - 代理到测试模式系统""" + if self.test_mode: + self.test_mode.test_mode_wait_poses = value + + @property + def test_mode_initialized(self): + """测试模式初始化状态 - 代理到测试模式系统""" + if self.test_mode: + return self.test_mode.test_mode_initialized + return False diff --git a/core/vr_manager.py.backup.stage1 b/core/vr_manager.py.backup.stage1 index e01afa79..a559e0fd 100644 --- a/core/vr_manager.py.backup.stage1 +++ b/core/vr_manager.py.backup.stage1 @@ -1,4737 +1,4737 @@ -""" -VR管理器模块 - -负责VR功能的初始化、渲染和交互: -- OpenVR/SteamVR集成 -- VR头显跟踪和渲染 -- VR控制器交互 -- VR模式切换 -""" - -import sys -import gc -import numpy as np -from panda3d.core import ( - WindowProperties, GraphicsPipe, FrameBufferProperties, - GraphicsOutput, Texture, Camera, PerspectiveLens, MatrixLens, - Mat4, Vec3, TransformState, RenderState, CardMaker, - BitMask32, PandaNode, NodePath, LMatrix4, LVector3, LVector4, - CS_yup_right, CS_default, PythonCallbackObject -) -from direct.task import Task -from direct.showbase.DirectObject import DirectObject - -try: - import openvr - OPENVR_AVAILABLE = True -except ImportError: - OPENVR_AVAILABLE = False - print("警告: OpenVR未安装,VR功能将不可用") - -# 导入手柄控制器、动作系统、交互系统、摇杆系统和传送系统 -from core.vr.tracking.controllers import LeftController, RightController -from core.vr.interaction.actions import VRActionManager -from core.vr.interaction.grab import VRInteractionManager -from core.vr.interaction.joystick import VRJoystickManager -from core.vr.interaction.teleport import VRTeleportSystem -from enum import Enum - - -class VRRenderMode(Enum): - """VR渲染模式枚举""" - NORMAL = "normal" # 普通渲染模式 - RENDER_PIPELINE = "render_pipeline" # RenderPipeline高级渲染模式 - - -class VRManager(DirectObject): - """VR管理器类 - 处理所有VR相关功能""" - - def __init__(self, world): - """初始化VR管理器 - - Args: - world: 主世界对象引用 - """ - super().__init__() - - self.world = world - self.vr_system = None - self.vr_enabled = False - self.vr_initialized = False - - # VR渲染相关 - self.vr_left_eye_buffer = None - self.vr_right_eye_buffer = None - self.vr_left_camera = None - self.vr_right_camera = None - self.vr_compositor = None - - # VR纹理和ID缓存 - 修复重复准备问题 - self.vr_left_texture = None - self.vr_right_texture = None - self.left_texture_id = None # 缓存左眼纹理的OpenGL ID - self.right_texture_id = None # 缓存右眼纹理的OpenGL ID - self.textures_prepared = False # 标记纹理是否已准备 - - # VR跟踪数据 - self.hmd_pose = Mat4.identMat() - self.controller_poses = {} - self.tracked_device_poses = [] - self.poses = None # OpenVR渲染姿态数组 - self.game_poses = None # OpenVR游戏逻辑姿态数组 - - # 🚀 对象池和缓存系统 - 修复16-19帧周期性GPU峰值 - self._matrix_pool = [] # Mat4对象池 - self._matrix_pool_size = 8 # 池大小,足够处理多个控制器 - self._cached_matrices = {} # 设备ID到矩阵的缓存 - self._controller_poses_cache = {} # 控制器姿态缓存,避免每帧clear() - - # 🚀 OpenVR Texture对象缓存 - 避免每帧创建openvr.Texture_t() - self._left_ovr_texture = None # 左眼纹理对象缓存 - self._right_ovr_texture = None # 右眼纹理对象缓存 - - # Python垃圾回收控制 - self._gc_control_enabled = True # 是否启用GC控制 - self._gc_disabled = False # GC是否被禁用 - self._manual_gc_interval = 900 # 每900帧手动触发一次GC (15秒@60fps) - 减少GC频率 - self._last_manual_gc_frame = 0 - - # 🚀 立即初始化对象池和GC控制(在其他组件之前) - self._initialize_object_pools() - - # VR渲染参数 - self.eye_width = 1080 - self.eye_height = 1200 - self.near_clip = 0.1 - self.far_clip = 1000.0 - - # VR分辨率缩放优化 - self.resolution_scale = 0.75 # 默认0.75倍分辨率,性能和质量平衡 - self.base_eye_width = 1080 # 原始推荐分辨率 - self.base_eye_height = 1200 - self.scaled_eye_width = 1080 # 实际使用的缩放后分辨率 - self.scaled_eye_height = 1200 - - # VR质量预设 - self.quality_presets = { - 'performance': 0.6, # 性能模式 - 约60%分辨率 - 'balanced': 0.75, # 平衡模式 - 约75%分辨率 - 'quality': 1.0 # 质量模式 - 100%分辨率 - } - self.current_quality_preset = 'balanced' # 默认平衡模式 - - # 🎨 VR渲染模式配置 - RenderPipeline集成 - self.vr_render_mode = VRRenderMode.NORMAL # 默认使用普通渲染模式 - self.render_pipeline_enabled = False # RenderPipeline是否已启用 - self.vr_pipeline_left_target = None # 左眼RenderPipeline渲染目标 - self.vr_pipeline_right_target = None # 右眼RenderPipeline渲染目标 - self.pipeline_resolution_scale = 0.75 # RenderPipeline模式下的分辨率缩放(性能优化) - self.vr_pipeline_controller = None # VR Pipeline控制器(管理完整的VR渲染管线) - - # RenderPipeline VR优化配置 - self.pipeline_vr_config = { - 'enable_shadows': True, # 启用阴影 - 'enable_ao': True, # 启用环境光遮蔽 - 'enable_bloom': False, # 禁用泛光(VR性能考虑) - 'enable_motion_blur': False, # 禁用运动模糊(VR中会引起不适) - 'enable_ssr': False, # 禁用屏幕空间反射(性能密集) - 'shadow_quality': 'medium', # 阴影质量:low/medium/high - 'ao_quality': 'low', # AO质量:low/medium/high - } - - # VR任务 - self.vr_task = None - - # VR锚点层级系统 - self.tracking_space = None - self.hmd_anchor = None - self.left_eye_anchor = None - self.right_eye_anchor = None - - # 坐标系转换矩阵 - 使用Panda3D内置方法 - self.coord_mat = LMatrix4.convert_mat(CS_yup_right, CS_default) - self.coord_mat_inv = LMatrix4.convert_mat(CS_default, CS_yup_right) - - # 性能监控 - self.frame_count = 0 - self.last_fps_check = 0 - self.last_fps_time = 0 - self.vr_fps = 0 - self.submit_failures = 0 - self.pose_failures = 0 - - # OpenVR 帧ID跟踪(防止重复提交) - self.openvr_frame_id = 0 - self.left_eye_last_render_frame = -1 - self.right_eye_last_render_frame = -1 - - # 高级性能监控(默认关闭,手动开启) - self.performance_monitoring = False # 是否启用性能监控 - self.debug_output_enabled = False # 是否启用调试输出 - self.debug_mode = 'detailed' # 'brief' 或 'detailed' - self.cpu_usage = 0.0 - self.memory_usage = 0.0 - self.gpu_usage = 0.0 - self.gpu_memory_usage = 0.0 - self.frame_times = [] # 存储最近帧时间 - self.max_frame_time_history = 60 # 保存60帧的历史 - self.last_performance_check = 0 - self.performance_check_interval = 0.5 # 每0.5秒更新一次性能数据 - - # 渲染管线详细监控 - self.enable_pipeline_monitoring = True # 是否启用管线监控 - self.performance_mode_enabled = False # 性能优化模式(禁用监控以减少对象创建) - self.performance_mode_trigger_frame = 600 # 第600帧后启用性能模式 - self.wait_poses_time = 0.0 # waitGetPoses耗时 - self.left_render_time = 0.0 # 左眼渲染耗时 - self.right_render_time = 0.0 # 右眼渲染耗时 - self.submit_time = 0.0 # 纹理提交耗时 - self.left_render_count = 0 # 左眼渲染次数计数 - self.right_render_count = 0 # 右眼渲染次数计数 - self.total_frame_time = 0.0 # 总帧时间 - self.vr_sync_wait_time = 0.0 # VR同步等待时间 - - # 时间监控历史(保存最近30帧) - self.wait_poses_times = [] - self.render_times = [] - self.submit_times = [] - self.sync_wait_times = [] - self.pipeline_history_size = 30 - - # GPU渲染时间监控(OpenVR Frame Timing) - self.enable_gpu_timing = False # 是否启用GPU时间监控(默认关闭) - self.gpu_scene_render_ms = 0.0 # GPU场景渲染时间 - self.gpu_pre_submit_ms = 0.0 # 提交前GPU时间 - self.gpu_post_submit_ms = 0.0 # 提交后GPU时间 - self.gpu_total_render_ms = 0.0 # GPU总渲染时间 - self.gpu_compositor_render_ms = 0.0 # GPU合成器渲染时间 - self.gpu_client_frame_interval_ms = 0.0 # 客户端帧间隔 - self.gpu_timing_history = [] # GPU时间历史记录 - self.gpu_timing_history_size = 30 # GPU时间历史记录大小 - self.gpu_timing_failure_count = 0 # GPU时间获取失败次数 - - # VR系统信息 - self.current_eye_resolution = (self.eye_width, self.eye_height) - self.recommended_eye_resolution = (0, 0) - self.vr_display_frequency = 0.0 - self.vr_vsync_enabled = True - self.vsync_to_photons_ms = 0.0 # VSync到光子的延迟 - self.target_frame_time_ms = 0.0 # 目标帧时间 - self.vsync_window_ms = 0.0 # VSync时间窗口 - self.async_reprojection_enabled = False # 异步重投影状态 - self.motion_smoothing_enabled = False # 运动平滑状态 - - # 🧪 VR测试模式 - self.vr_test_mode = False # 是否启用VR测试模式 - self.test_display_mode = 'stereo' # 'left', 'right', 'stereo' - self.test_display_quad = None # 测试显示的四边形 - self.test_right_quad = None # 右眼显示的四边形(立体模式) - self.stereo_display_created = False # 立体显示是否已创建 - self.test_performance_hud = None # 性能HUD - self.test_performance_text = None # 性能文本节点 - self.test_mode_initialized = False # 测试模式是否已初始化 - self.hud_update_counter = 0 # HUD更新计数器 - self.hud_update_interval = 30 # HUD更新间隔(帧数),30帧约0.5秒@60fps - - # 🔧 VR测试模式调试选项 - 逐步启用普通VR功能 - self.test_mode_submit_texture = False # 是否在测试模式提交纹理到OpenVR - self.test_mode_wait_poses = False # 是否在测试模式调用waitGetPoses - - # waitGetPoses调用策略配置 - 使用高性能的update_task策略 - self.use_prediction_time = 0.011 # 11ms的预测时间 - OpenVR标准值,平衡准确性和延迟 - self.poses_updated_in_task = True # 始终在更新任务中获取姿态(Running Start模式) - - # 🎨 初始化VR配置管理器 - try: - from core.vr.config.vr_config import VRConfigManager - self.config_manager = VRConfigManager() - # 加载配置并应用 - self.config_manager.apply_config_to_vr_manager(self) - print("✓ VR配置管理器初始化完成并已加载配置") - except Exception as e: - print(f"⚠️ VR配置管理器初始化失败: {e}") - self.config_manager = None - - # 尝试导入性能监控库 - self._init_performance_monitoring() - - # VR提交策略 - 基于参考实现 - self.submit_together = True # 是否在right_cb中同时提交左右眼 - - - # Running Start标记 - Valve最佳实践 - self._waitgetposes_called_this_frame = False - - # 姿态缓存 - 修复时序不匹配 - self._cached_render_poses = None # 用于渲染的缓存姿态 - self._first_frame = True # 首帧标记 - - # ATW控制选项 - 备选方案 - self.disable_async_reprojection = False # 是否禁用异步重投影 - - # VR手柄控制器 - self.left_controller = None - self.right_controller = None - self.controllers = {} # 设备索引到控制器的映射 - self.tracked_device_anchors = {} # 跟踪设备锚点 - - # VR动作系统 - 可选择禁用(用于API兼容性问题) - self.disable_action_system = True # 禁用动作系统,使用直接控制器输入 - - if not self.disable_action_system: - try: - self.action_manager = VRActionManager(self) - print("✓ VR动作管理器初始化完成") - except Exception as e: - print(f"⚠️ VR动作管理器初始化失败: {e}") - self.action_manager = None - else: - self.action_manager = None - print("🚫 VR动作系统已禁用,使用直接控制器输入") - - # VR交互系统 - 添加异常保护 - try: - self.interaction_manager = VRInteractionManager(self) - print("✓ VR交互管理器初始化完成") - except Exception as e: - print(f"⚠️ VR交互管理器初始化失败: {e}") - self.interaction_manager = None - - # VR传送系统 - 添加异常保护 - try: - self.teleport_system = VRTeleportSystem(self) - print("✓ VR传送系统初始化完成") - except Exception as e: - print(f"⚠️ VR传送系统初始化失败: {e}") - self.teleport_system = None - - # VR摇杆系统 - 添加异常保护 - try: - self.joystick_manager = VRJoystickManager(self) - print("✓ VR摇杆管理器初始化完成") - except Exception as e: - print(f"⚠️ VR摇杆管理器初始化失败: {e}") - self.joystick_manager = None - - print("✓ VR管理器初始化完成") - - def _initialize_object_pools(self): - """初始化对象池 - 修复16-19帧周期性GPU峰值""" - try: - # 预填充Mat4对象池 - for _ in range(self._matrix_pool_size): - self._matrix_pool.append(Mat4()) - - print(f"✅ Mat4对象池初始化完成 - 池大小: {self._matrix_pool_size}") - - # 🚀 预创建OpenVR Texture对象 - 消除每帧创建openvr.Texture_t()的开销 - try: - import openvr - self._left_ovr_texture = openvr.Texture_t() - self._right_ovr_texture = openvr.Texture_t() - - # 设置固定属性(这些不变) - self._left_ovr_texture.eType = openvr.TextureType_OpenGL - self._left_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma - self._right_ovr_texture.eType = openvr.TextureType_OpenGL - self._right_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma - - print("✅ OpenVR Texture对象缓存已创建 - 避免每帧创建新对象") - except Exception as texture_error: - print(f"⚠️ OpenVR Texture对象创建失败: {texture_error}") - # 不影响整体初始化,但性能可能不是最优 - - # 启用GC控制 - if self._gc_control_enabled: - # 禁用自动垃圾回收,改为手动控制 - gc.disable() - self._gc_disabled = True - print("✅ Python垃圾回收已禁用,改为手动控制") - - except Exception as e: - print(f"⚠️ 对象池初始化失败: {e}") - - def _get_pooled_matrix(self): - """从对象池获取Mat4对象""" - if self._matrix_pool: - return self._matrix_pool.pop() - else: - # 池为空时创建新对象(不应该发生,但作为备用) - return Mat4() - - def _return_pooled_matrix(self, matrix): - """将Mat4对象返回对象池""" - if len(self._matrix_pool) < self._matrix_pool_size: - # 重置矩阵到单位矩阵 - matrix.identMat() - self._matrix_pool.append(matrix) - - def _manual_gc_control(self): - """手动垃圾回收控制 - 避免VR渲染期间的GC峰值""" - if not self._gc_control_enabled or not self._gc_disabled: - return - - # 🚀 智能GC间隔:性能模式下减少GC频率 - current_interval = self._manual_gc_interval - if self.performance_mode_enabled: - current_interval = self._manual_gc_interval * 2 # 性能模式下间隔翻倍 - - # 每N帧手动触发一次垃圾回收 - if self.frame_count - self._last_manual_gc_frame >= current_interval: - # 在非渲染关键时刻触发GC - collected = gc.collect() - self._last_manual_gc_frame = self.frame_count - - # 仅在收集到对象时输出信息 - if collected > 0: - print(f"🗑️ 手动GC: 清理了 {collected} 个对象 (帧#{self.frame_count})") - - def _update_matrix_from_openvr(self, panda_mat, ovr_matrix): - """直接更新现有Mat4对象的数值,避免创建新对象""" - # 复用_convert_openvr_matrix_to_panda中的转换逻辑 - # X轴行:Panda3D的X轴对应OpenVR的X轴 - panda_mat.setCell(0, 0, ovr_matrix[0][0]) # X_x → X_x - panda_mat.setCell(0, 1, ovr_matrix[0][1]) # X_y → X_y - panda_mat.setCell(0, 2, ovr_matrix[0][2]) # X_z → X_z - panda_mat.setCell(0, 3, ovr_matrix[0][3]) # 位移X分量 - - # Y轴行:Panda3D的Y轴对应OpenVR的-Z轴 - panda_mat.setCell(1, 0, -ovr_matrix[2][0]) # -Z_x → Y_x - panda_mat.setCell(1, 1, -ovr_matrix[2][1]) # -Z_y → Y_y - panda_mat.setCell(1, 2, -ovr_matrix[2][2]) # -Z_z → Y_z - panda_mat.setCell(1, 3, -ovr_matrix[2][3]) # 位移Y分量(-Z位移) - - # Z轴行:Panda3D的Z轴对应OpenVR的Y轴 - panda_mat.setCell(2, 0, ovr_matrix[1][0]) # Y_x → Z_x - panda_mat.setCell(2, 1, ovr_matrix[1][1]) # Y_y → Z_y - panda_mat.setCell(2, 2, ovr_matrix[1][2]) # Y_z → Z_z - panda_mat.setCell(2, 3, ovr_matrix[1][3]) # 位移Z分量(Y位移) - - # 齐次坐标 - panda_mat.setCell(3, 0, 0) - panda_mat.setCell(3, 1, 0) - panda_mat.setCell(3, 2, 0) - panda_mat.setCell(3, 3, 1) - - def convert_mat(self, mat): - """ - 将OpenVR矩阵转换为Panda3D矩阵 - 基于参考实现 - """ - if len(mat.m) == 4: - result = LMatrix4( - mat.m[0][0], mat.m[1][0], mat.m[2][0], mat.m[3][0], - mat.m[0][1], mat.m[1][1], mat.m[2][1], mat.m[3][1], - mat.m[0][2], mat.m[1][2], mat.m[2][2], mat.m[3][2], - mat.m[0][3], mat.m[1][3], mat.m[2][3], mat.m[3][3]) - elif len(mat.m) == 3: - result = LMatrix4( - mat.m[0][0], mat.m[1][0], mat.m[2][0], 0.0, - mat.m[0][1], mat.m[1][1], mat.m[2][1], 0.0, - mat.m[0][2], mat.m[1][2], mat.m[2][2], 0.0, - mat.m[0][3], mat.m[1][3], mat.m[2][3], 1.0) - return result - - def is_vr_available(self): - """检查VR系统是否可用""" - if not OPENVR_AVAILABLE: - return False - - try: - # 检查SteamVR是否运行 - return openvr.isRuntimeInstalled() and openvr.isHmdPresent() - except Exception as e: - print(f"VR检查失败: {e}") - return False - - def initialize_vr(self): - """初始化VR系统""" - if not OPENVR_AVAILABLE: - print("❌ OpenVR不可用,无法初始化VR") - return False - - if self.vr_initialized: - print("VR系统已经初始化") - return True - - try: - print("🔄 正在初始化VR系统...") - - # 🚀 确保对象池已正确初始化(备用检查) - if not hasattr(self, '_matrix_pool') or len(self._matrix_pool) == 0: - print("⚠️ 对象池未初始化,正在重新初始化...") - self._initialize_object_pools() - - # 初始化OpenVR - 使用Scene应用类型确保正确的焦点管理 - self.vr_system = openvr.init(openvr.VRApplication_Scene) - if not self.vr_system: - print("❌ 无法初始化OpenVR系统") - return False - - # 获取compositor - self.vr_compositor = openvr.VRCompositor() - if not self.vr_compositor: - print("❌ 无法获取VR Compositor") - return False - - # 获取推荐的渲染目标尺寸 - base_width, base_height = self.vr_system.getRecommendedRenderTargetSize() - self.base_eye_width = base_width - self.base_eye_height = base_height - - # 应用分辨率缩放 - self.scaled_eye_width = int(base_width * self.resolution_scale) - self.scaled_eye_height = int(base_height * self.resolution_scale) - - # 使用缩放后的分辨率作为实际渲染分辨率 - self.eye_width = self.scaled_eye_width - self.eye_height = self.scaled_eye_height - - self.current_eye_resolution = (self.eye_width, self.eye_height) - self.recommended_eye_resolution = (base_width, base_height) - - print(f"✓ VR基础分辨率: {base_width}x{base_height}") - print(f"✓ VR缩放系数: {self.resolution_scale}") - print(f"✓ VR实际分辨率: {self.eye_width}x{self.eye_height}") - print(f"📊 分辨率优化: {(1 - self.resolution_scale**2) * 100:.1f}% 像素减少") - - # 获取VR系统信息 - try: - # 获取显示频率 - self.vr_display_frequency = self.vr_system.getFloatTrackedDeviceProperty( - openvr.k_unTrackedDeviceIndex_Hmd, - openvr.Prop_DisplayFrequency_Float - ) - print(f"✓ VR显示频率: {self.vr_display_frequency} Hz") - - # 获取IPD(瞳距) - ipd = self.vr_system.getFloatTrackedDeviceProperty( - openvr.k_unTrackedDeviceIndex_Hmd, - openvr.Prop_UserIpdMeters_Float - ) - print(f"✓ IPD(瞳距): {ipd * 1000:.1f}mm") - - # 获取设备制造商和型号 - manufacturer = self.vr_system.getStringTrackedDeviceProperty( - openvr.k_unTrackedDeviceIndex_Hmd, - openvr.Prop_ManufacturerName_String - ) - model = self.vr_system.getStringTrackedDeviceProperty( - openvr.k_unTrackedDeviceIndex_Hmd, - openvr.Prop_ModelNumber_String - ) - print(f"✓ VR设备: {manufacturer} {model}") - - # 获取更多系统配置信息 - try: - # 获取刷新率相关信息 - seconds_since_last_vsync = self.vr_system.getFloatTrackedDeviceProperty( - openvr.k_unTrackedDeviceIndex_Hmd, - openvr.Prop_SecondsFromVsyncToPhotons_Float - ) - self.vsync_to_photons_ms = seconds_since_last_vsync * 1000 - print(f"✓ VSync到光子延迟: {self.vsync_to_photons_ms:.2f}ms") - - # 获取显示延迟信息 - display_refresh_rate = self.vr_display_frequency - if display_refresh_rate > 0: - self.target_frame_time_ms = 1000.0 / display_refresh_rate - print(f"✓ 目标帧时间: {self.target_frame_time_ms:.2f}ms") - - # 计算VSync时间窗口 - self.vsync_window_ms = self.target_frame_time_ms * 0.1 # 10%的窗口 - print(f"✓ VSync时间窗口: ±{self.vsync_window_ms:.2f}ms") - - except Exception as vsync_error: - print(f"⚠️ 获取VSync信息失败: {vsync_error}") - - # 检查是否启用了异步重投影 - try: - if hasattr(openvr, 'VRSettings'): - settings = openvr.VRSettings() - if settings: - self.async_reprojection_enabled = settings.getBool("steamvr", "enableAsyncReprojection") - print(f"✓ 异步重投影: {'启用' if self.async_reprojection_enabled else '禁用'}") - - self.motion_smoothing_enabled = settings.getBool("steamvr", "motionSmoothing") - print(f"✓ 运动平滑: {'启用' if self.motion_smoothing_enabled else '禁用'}") - - except Exception as settings_error: - print(f"⚠️ 获取VR设置失败: {settings_error}") - - except Exception as e: - print(f"⚠️ 获取VR系统信息失败: {e}") - - # 创建OpenVR姿态数组 - 分离渲染和游戏姿态 - poses_t = openvr.TrackedDevicePose_t * openvr.k_unMaxTrackedDeviceCount - self.poses = poses_t() # 渲染姿态(预测的) - self.game_poses = poses_t() # 游戏逻辑姿态(当前的) - print("✓ VR渲染和游戏姿态数组已创建") - - # 🔧 关键修复:统一初始化流程 - # 如果目标模式是RenderPipeline,先用普通模式初始化,然后切换 - # 这样两个场景都走相同的、已验证的代码路径 - target_render_mode = self.vr_render_mode - use_deferred_pipeline_switch = False - - if target_render_mode == VRRenderMode.RENDER_PIPELINE: - print("🎨 目标VR渲染模式: RenderPipeline") - print(" 策略:先用普通模式初始化,然后切换到RenderPipeline") - self.vr_render_mode = VRRenderMode.NORMAL - use_deferred_pipeline_switch = True - else: - print(f"🎨 VR渲染模式: {self.vr_render_mode.value}") - - # 创建VR渲染缓冲区 - 始终用普通模式初始化 - if not self._create_vr_buffers(): - print("❌ 创建VR渲染缓冲区失败") - return False - - # 设置VR相机 - if not self._setup_vr_cameras(): - print("❌ 设置VR相机失败") - return False - - # 优化VR渲染管线 - self._optimize_vr_rendering() - - # 初始化手柄控制器 - self._initialize_controllers() - - # 初始化动作系统 - 仅在启用时初始化 - if not self.disable_action_system and self.action_manager: - try: - if not self.action_manager.initialize(): - print("⚠️ VR动作系统初始化失败,但VR系统将继续运行") - except Exception as e: - print(f"⚠️ VR动作系统初始化异常: {e}") - else: - if self.disable_action_system: - print("🚫 VR动作系统已禁用,跳过初始化") - else: - print("⚠️ VR动作管理器未创建,跳过动作系统初始化") - - # 初始化交互系统 - 检查是否存在 - if self.interaction_manager: - try: - if not self.interaction_manager.initialize(): - print("⚠️ VR交互系统初始化失败,但VR系统将继续运行") - except Exception as e: - print(f"⚠️ VR交互系统初始化异常: {e}") - else: - print("⚠️ VR交互管理器未创建,跳过交互系统初始化") - - # 初始化VR传送系统 - if self.teleport_system: - try: - if not self.teleport_system.initialize(): - print("⚠️ VR传送系统初始化失败,但VR系统将继续运行") - except Exception as e: - print(f"⚠️ VR传送系统初始化异常: {e}") - else: - print("⚠️ VR传送系统未创建,跳过传送系统初始化") - - # 初始化VR摇杆系统 - if self.joystick_manager: - try: - # 传入传送系统引用给摇杆管理器 - self.joystick_manager.initialize(self.teleport_system) - print("✓ VR摇杆系统初始化成功") - except Exception as e: - print(f"⚠️ VR摇杆系统初始化异常: {e}") - else: - print("⚠️ VR摇杆管理器未创建,跳过摇杆系统初始化") - - # 可选:禁用异步重投影(备选方案) - if self.disable_async_reprojection: - self._disable_async_reprojection() - - # 启动VR更新任务 - self._start_vr_task() - - self.vr_initialized = True - print("✅ VR系统初始化成功(普通模式)") - - # 🔧 关键修复:如果目标是RenderPipeline模式,现在切换过去 - # 这样场景2(先设RP再进VR)会走场景1(先VR再设RP)相同的代码路径 - if use_deferred_pipeline_switch: - print("\n🔄 现在切换到目标渲染模式: RenderPipeline") - print(" 这将触发buffer重建和visualizer刷新...") - if self.set_vr_render_mode(target_render_mode): - print("✅ 已成功切换到RenderPipeline模式") - else: - print("⚠️ 切换到RenderPipeline失败,保持普通模式") - - return True - - except Exception as e: - print(f"❌ VR初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def _create_vr_buffers(self): - """创建VR渲染缓冲区 - 使用分辨率缩放优化""" - try: - print(f"🔧 创建VR缓冲区:") - print(f" 推荐分辨率: {self.base_eye_width}x{self.base_eye_height}") - print(f" 缩放系数: {self.resolution_scale}") - print(f" 实际分辨率: {self.eye_width}x{self.eye_height}") - print(f" 像素减少: {(1 - self.resolution_scale**2) * 100:.1f}%") - - # 创建左眼纹理和缓冲区 - self.vr_left_texture = self._create_vr_texture("VR Left Eye Texture") - self.vr_left_eye_buffer = self._create_vr_buffer( - "VR Left Eye", - self.vr_left_texture, - self.eye_width, - self.eye_height - ) - - if not self.vr_left_eye_buffer: - print("❌ 创建左眼缓冲区失败") - return False - - # 设置左眼缓冲区属性 - self.vr_left_eye_buffer.setSort(-100) - self.vr_left_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) # 深蓝色背景便于调试 - self.vr_left_eye_buffer.setActive(True) - - # 创建右眼纹理和缓冲区 - self.vr_right_texture = self._create_vr_texture("VR Right Eye Texture") - self.vr_right_eye_buffer = self._create_vr_buffer( - "VR Right Eye", - self.vr_right_texture, - self.eye_width, - self.eye_height - ) - - if not self.vr_right_eye_buffer: - print("❌ 创建右眼缓冲区失败") - return False - - # 设置右眼缓冲区属性 - self.vr_right_eye_buffer.setSort(-99) - self.vr_right_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) # 深蓝色背景便于调试 - self.vr_right_eye_buffer.setActive(True) - - # 🚀 关键优化:立即准备纹理并缓存OpenGL ID,避免后续重复准备 - print("🔧 准备纹理并缓存OpenGL ID...") - if self._prepare_and_cache_textures(): - print("✅ VR渲染缓冲区和纹理缓存创建成功") - return True - else: - print("❌ 纹理准备失败") - return False - - except Exception as e: - print(f"❌ 创建VR缓冲区失败: {e}") - import traceback - traceback.print_exc() - return False - - def _create_vr_texture(self, name): - """创建VR纹理对象 - 基于参考实现""" - texture = Texture(name) - texture.setWrapU(Texture.WMClamp) - texture.setWrapV(Texture.WMClamp) - texture.setMinfilter(Texture.FTLinear) - texture.setMagfilter(Texture.FTLinear) - return texture - - def _prepare_and_cache_textures(self): - """准备纹理并缓存OpenGL ID - 解决重复准备问题""" - try: - # 🔧 验证纹理对象存在 - if not self.vr_left_texture or not self.vr_right_texture: - print("❌ VR纹理对象不存在") - print(f" 左眼纹理: {self.vr_left_texture}") - print(f" 右眼纹理: {self.vr_right_texture}") - return False - - # 获取graphics state guardian和prepared objects - gsg = self.world.win.getGsg() - if not gsg: - print("❌ 无法获取GraphicsStateGuardian") - return False - - prepared_objects = gsg.getPreparedObjects() - if not prepared_objects: - print("❌ 无法获取PreparedObjects") - return False - - # 准备左眼纹理并缓存ID - print(f" 准备左眼纹理: {self.vr_left_texture.getXSize()}x{self.vr_left_texture.getYSize()}") - texture_context = self.vr_left_texture.prepareNow(0, prepared_objects, gsg) - if texture_context and hasattr(texture_context, 'getNativeId'): - self.left_texture_id = texture_context.getNativeId() - if self.left_texture_id > 0: - print(f" ✅ 左眼纹理准备完成 ({self.vr_render_mode.value}): ID={self.left_texture_id}") - else: - print(" ❌ 左眼纹理ID无效") - return False - else: - print(" ❌ 左眼纹理准备失败") - return False - - # 准备右眼纹理并缓存ID - print(f" 准备右眼纹理: {self.vr_right_texture.getXSize()}x{self.vr_right_texture.getYSize()}") - texture_context = self.vr_right_texture.prepareNow(0, prepared_objects, gsg) - if texture_context and hasattr(texture_context, 'getNativeId'): - self.right_texture_id = texture_context.getNativeId() - if self.right_texture_id > 0: - print(f" ✅ 右眼纹理准备完成 ({self.vr_render_mode.value}): ID={self.right_texture_id}") - else: - print(" ❌ 右眼纹理ID无效") - return False - else: - print(" ❌ 右眼纹理准备失败") - return False - - # 标记纹理已准备 - self.textures_prepared = True - print(" ✅ 纹理准备完成,ID已缓存,后续帧将直接使用缓存ID") - return True - - except Exception as e: - print(f"❌ 纹理准备和缓存失败: {e}") - import traceback - traceback.print_exc() - return False - - def _create_vr_buffer(self, name, texture, width, height): - """创建VR渲染缓冲区 - 基于参考实现 + 性能诊断""" - # 设置帧缓冲属性 - fbprops = FrameBufferProperties() - fbprops.setRgbaBits(1, 1, 1, 1) - # 可以在这里添加多重采样抗锯齿 - # fbprops.setMultisamples(4) - - # 创建缓冲区 - buffer = self.world.win.makeTextureBuffer(name, width, height, to_ram=False, fbp=fbprops) - - if buffer: - # 🔍 性能诊断:检查缓冲区类型和属性 - self._diagnose_buffer_performance(buffer, name, width, height) - - # 清除默认渲染纹理 - buffer.clearRenderTextures() - # 🚀 使用RTMBindOrCopy模式 - 已经是最优选择 - # RTMBindOrCopy会尝试直接绑定到纹理,只有硬件不支持时才回退到复制 - # 这已经是Panda3D中最高效的渲染到纹理模式 - buffer.addRenderTexture(texture, GraphicsOutput.RTMBindOrCopy, GraphicsOutput.RTPColor) - - else: - print(f"⚠️ VR缓冲区创建失败: {name} ({width}x{height})") - - return buffer - - def _diagnose_buffer_performance(self, buffer, name, width, height): - """诊断VR缓冲区性能特性""" - try: - # 检查缓冲区类型 - buffer_type = type(buffer).__name__ - - # 检查是否为parasite buffer(性能较差的类型) - is_parasite = 'Parasite' in buffer_type - - # 获取缓冲区信息 - is_valid = buffer.isValid() - is_hardware = hasattr(buffer, 'getGsg') and buffer.getGsg() is not None - - # 输出诊断信息(仅第一次创建时) - if not hasattr(self, '_buffer_diagnosis_shown'): - print(f"🔍 VR缓冲区诊断 [{name}]:") - print(f" 类型: {buffer_type}") - print(f" 分辨率: {width}x{height} ({width*height/1000000:.1f}M像素)") - print(f" 有效性: {'✓' if is_valid else '✗'}") - print(f" 硬件支持: {'✓' if is_hardware else '✗'}") - - if is_parasite: - print(f" ⚠️ 检测到Parasite Buffer - 性能可能受限") - print(f" 建议: 检查显卡是否支持真正的离屏渲染") - else: - print(f" ✓ 使用硬件FBO - 性能良好") - - # 显示帧缓冲区属性 - if hasattr(buffer, 'getFbProperties'): - fbp = buffer.getFbProperties() - if fbp: - print(f" 颜色位数: R{fbp.getRedBits()}G{fbp.getGreenBits()}B{fbp.getBlueBits()}A{fbp.getAlphaBits()}") - if hasattr(fbp, 'getMultisamples') and fbp.getMultisamples() > 0: - print(f" MSAA: {fbp.getMultisamples()}x") - - self._buffer_diagnosis_shown = True - - except Exception as e: - print(f"缓冲区诊断失败: {e}") - - def _create_vr_buffers_with_pipeline(self): - """创建带RenderPipeline的VR渲染缓冲区 - 高级渲染模式""" - try: - print(f"🎨 创建RenderPipeline VR缓冲区:") - print(f" 推荐分辨率: {self.base_eye_width}x{self.base_eye_height}") - print(f" Pipeline缩放: {self.pipeline_resolution_scale}") - - # 检查RenderPipeline是否可用 - if not hasattr(self.world, 'render_pipeline') or not self.world.render_pipeline: - print("❌ RenderPipeline未初始化,无法使用高级渲染模式") - return False - - pipeline = self.world.render_pipeline - - # 计算RenderPipeline模式下的分辨率 - pipeline_width = int(self.base_eye_width * self.pipeline_resolution_scale) - pipeline_height = int(self.base_eye_height * self.pipeline_resolution_scale) - - print(f" 实际分辨率: {pipeline_width}x{pipeline_height}") - print(f" 像素减少: {(1 - self.pipeline_resolution_scale**2) * 100:.1f}%") - - # 导入RenderPipeline相关模块 - try: - from RenderPipelineFile.rpcore.render_target import RenderTarget - except ImportError as e: - print(f"❌ 无法导入RenderPipeline模块: {e}") - return False - - # 导入VR stages模块 - try: - from core.vr.rendering.stages import VRPipelineController - except ImportError as e: - print(f"❌ 无法导入VR stages模块: {e}") - return False - - # 创建VR Pipeline控制器 - print(" 初始化VR Pipeline控制器...") - self.vr_pipeline_controller = VRPipelineController(pipeline) - - # 保存分辨率信息(将在_setup_vr_cameras中使用) - self.pipeline_vr_width = pipeline_width - self.pipeline_vr_height = pipeline_height - - # 应用RenderPipeline效果配置 - self._apply_pipeline_vr_effects() - - self.render_pipeline_enabled = True - print("✅ VR Pipeline控制器已初始化(将在相机设置时创建完整管线)") - - # 检查天空盒状态 - self._check_skybox_status() - - return True - - except Exception as e: - print(f"❌ 创建RenderPipeline VR缓冲区失败: {e}") - import traceback - traceback.print_exc() - return False - - def _apply_pipeline_vr_effects(self): - """为VR场景应用RenderPipeline效果配置""" - try: - print("🎨 应用RenderPipeline VR优化配置...") - - # 根据配置调整RenderPipeline设置 - config = self.pipeline_vr_config - - # 注意:实际的插件启用/禁用需要在pipeline.yaml中配置 - # 这里只是记录当前的VR优化意图 - print(f" 阴影: {'启用' if config['enable_shadows'] else '禁用'}") - print(f" 环境光遮蔽: {'启用' if config['enable_ao'] else '禁用'}") - print(f" 泛光效果: {'启用' if config['enable_bloom'] else '禁用'} (VR推荐禁用)") - print(f" 运动模糊: {'启用' if config['enable_motion_blur'] else '禁用'} (VR推荐禁用)") - print(f" 屏幕空间反射: {'启用' if config['enable_ssr'] else '禁用'} (VR推荐禁用)") - - print("✅ RenderPipeline VR效果配置完成") - - except Exception as e: - print(f"⚠️ 应用RenderPipeline VR效果失败: {e}") - - def _check_skybox_status(self): - """检查并报告天空盒状态""" - try: - print("\n🌌 检查天空盒状态...") - - # 方法1:在render节点下搜索所有名字包含skybox的节点 - skybox_nodes = self.world.render.findAllMatches("**/skybox*") - if skybox_nodes and skybox_nodes.getNumPaths() > 0: - print(f" ✓ 找到 {skybox_nodes.getNumPaths()} 个天空盒节点(使用名称搜索)") - for i in range(skybox_nodes.getNumPaths()): - skybox = skybox_nodes.getPath(i) - print(f" 天空盒 #{i+1}:") - print(f" 名称: {skybox.getName()}") - print(f" 位置: {skybox.getPos()}") - print(f" 缩放: {skybox.getScale()}") - print(f" 父节点: {skybox.getParent().getName()}") - - # 检查是否有shader - if skybox.hasShader(): - print(f" ✓ 已应用shader效果") - else: - print(f" ⚠️ 未应用shader效果") - - # 检查bin设置 - if skybox.hasBin(): - print(f" Bin: {skybox.getBinName()}") - else: - print(" ⚠️ 未找到天空盒(名称搜索)") - - # 方法2:搜索所有GeomNode(几何节点),天空盒通常是一个大的几何体 - print(" 搜索所有几何节点...") - all_geoms = self.world.render.findAllMatches("**/+GeomNode") - print(f" 找到 {all_geoms.getNumPaths()} 个几何节点") - - # 查找大型几何体(缩放值很大,可能是天空盒) - skybox_candidates = [] - for i in range(all_geoms.getNumPaths()): - geom_node = all_geoms.getPath(i) - scale = geom_node.getScale() - # 天空盒通常缩放很大(如40000) - if scale[0] > 1000 or scale[1] > 1000 or scale[2] > 1000: - print(f" ✓ 找到大型几何体(可能是天空盒):") - print(f" 名称: {geom_node.getName()}") - print(f" 缩放: {scale}") - print(f" 位置: {geom_node.getPos()}") - if geom_node.hasShader(): - print(f" ✓ 已应用shader") - skybox_candidates.append(geom_node) - - # 方法3:检查render节点的所有直接子节点(显示全部) - print(" 检查render的所有直接子节点...") - render_children = self.world.render.getChildren() - print(f" render有 {render_children.getNumPaths()} 个直接子节点:") - for i in range(render_children.getNumPaths()): - child = render_children.getPath(i) - scale = child.getScale() - # 标记大型节点(可能是天空盒) - if scale[0] > 1000 or scale[1] > 1000 or scale[2] > 1000: - print(f" - {child.getName()} ⭐ (大型节点,缩放: {scale})") - else: - print(f" - {child.getName()}") - - # 报告结果但不自动创建 - if len(skybox_candidates) == 0: - print(" ⚠️ 未找到明显的天空盒(缩放>1000的几何体)") - print(" 💡 RenderPipeline的天空盒可能在上述列表中,但缩放值不同") - print(" 💡 或者可能RenderPipeline未在VR模式下自动创建天空盒") - else: - print(f" ✓ 找到 {len(skybox_candidates)} 个可能的天空盒") - - print("🌌 天空盒状态检查完成\n") - - except Exception as e: - print(f"⚠️ 天空盒状态检查失败: {e}") - import traceback - traceback.print_exc() - - def _create_vr_skybox(self): - """为VR创建天空盒""" - try: - print(" 🎨 创建VR天空盒...") - - # 检查RenderPipeline是否可用 - if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline: - pipeline = self.world.render_pipeline - - # 使用RenderPipeline的load_default_skybox方法 - print(" 使用RenderPipeline方法创建天空盒...") - skybox = pipeline.common_resources.load_default_skybox() - - if skybox: - # 设置天空盒属性 - skybox.set_scale(40000) # 大型缩放 - skybox.reparent_to(self.world.render) - skybox.set_bin("unsorted", 10000) # 最后渲染 - - # 应用天空盒shader效果 - pipeline.set_effect(skybox, "effects/skybox.yaml", { - "render_shadow": False, - "render_envmap": False, - "render_voxelize": False, - "alpha_testing": False, - "normal_mapping": False, - "parallax_mapping": False - }, 1000) - - print(f" ✅ VR天空盒已创建") - print(f" 名称: {skybox.getName()}") - print(f" 缩放: {skybox.getScale()}") - print(f" 位置: {skybox.getPos()}") - return skybox - else: - print(" ❌ 无法加载天空盒模型") - return None - else: - print(" ❌ RenderPipeline未初始化,无法创建天空盒") - return None - - except Exception as e: - print(f" ❌ 创建VR天空盒失败: {e}") - import traceback - traceback.print_exc() - return None - - def _setup_vr_cameras(self): - """设置VR相机 - 使用锚点层级系统""" - try: - # 创建VR追踪空间锚点层级 - self.tracking_space = self.world.render.attachNewNode('tracking-space') - self.hmd_anchor = self.tracking_space.attachNewNode('hmd-anchor') - self.left_eye_anchor = self.hmd_anchor.attachNewNode('left-eye') - self.right_eye_anchor = self.hmd_anchor.attachNewNode('right-eye') - - # 获取投影矩阵 - projection_left = self.coord_mat_inv * self.convert_mat( - self.vr_system.getProjectionMatrix(openvr.Eye_Left, self.near_clip, self.far_clip)) - projection_right = self.coord_mat_inv * self.convert_mat( - self.vr_system.getProjectionMatrix(openvr.Eye_Right, self.near_clip, self.far_clip)) - - # 创建左眼相机节点 - left_cam_node = Camera('left-cam') - left_lens = MatrixLens() - left_lens.setUserMat(projection_left) - left_cam_node.setLens(left_lens) - - # 创建右眼相机节点 - right_cam_node = Camera('right-cam') - right_lens = MatrixLens() - right_lens.setUserMat(projection_right) - right_cam_node.setLens(right_lens) - - # 附加相机到眼睛锚点 - self.vr_left_camera = self.left_eye_anchor.attachNewNode(left_cam_node) - self.vr_right_camera = self.right_eye_anchor.attachNewNode(right_cam_node) - - # 设置显示区域 - 区分RenderPipeline和普通模式 - if self.vr_render_mode == VRRenderMode.RENDER_PIPELINE and self.vr_pipeline_controller: - # RenderPipeline模式:使用VRPipelineController创建完整管线 - print(" 使用VR Pipeline创建立体渲染管线...") - - # 创建立体渲染管线(GBuffer + Lighting + Final stages) - success = self.vr_pipeline_controller.create_stereo_pipeline( - self.pipeline_vr_width, - self.pipeline_vr_height, - self.vr_left_camera, - self.vr_right_camera - ) - - if not success: - print("❌ VR Pipeline创建失败") - return False - - # 获取GBuffer的内部buffer(用于DisplayRegion) - self.vr_left_eye_buffer = self.vr_pipeline_controller.get_left_buffer() - self.vr_right_eye_buffer = self.vr_pipeline_controller.get_right_buffer() - - # 🔧 关键修复:验证buffer有效性 - if not self.vr_left_eye_buffer or not self.vr_right_eye_buffer: - print("❌ VR Pipeline buffer创建失败") - print(f" 左眼buffer: {self.vr_left_eye_buffer}") - print(f" 右眼buffer: {self.vr_right_eye_buffer}") - return False - - # 获取最终输出纹理(用于提交到OpenVR) - left_textures = self.vr_pipeline_controller.get_left_textures() - right_textures = self.vr_pipeline_controller.get_right_textures() - - if left_textures and right_textures: - self.vr_left_texture = left_textures["final"] - self.vr_right_texture = right_textures["final"] - else: - print("❌ 无法获取VR Pipeline输出纹理") - return False - - # 设置缓冲区排序和清除颜色 - if self.vr_left_eye_buffer: - self.vr_left_eye_buffer.setSort(-100) - self.vr_left_eye_buffer.setClearColorActive(True) - self.vr_left_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) - - if self.vr_right_eye_buffer: - self.vr_right_eye_buffer.setSort(-99) - self.vr_right_eye_buffer.setClearColorActive(True) - self.vr_right_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) - - # 🔧 关键修复:为RenderPipeline的GBuffer DisplayRegion设置DrawCallback - # 这确保纹理在渲染完成后被提交到OpenVR - if self.vr_left_eye_buffer and self.vr_right_eye_buffer: - print(" 设置RenderPipeline DisplayRegion回调...") - try: - # 🔧 验证DisplayRegion数量 - left_dr_count = self.vr_left_eye_buffer.getNumDisplayRegions() - right_dr_count = self.vr_right_eye_buffer.getNumDisplayRegions() - - print(f" 左眼buffer DisplayRegion数量: {left_dr_count}") - print(f" 右眼buffer DisplayRegion数量: {right_dr_count}") - - if left_dr_count == 0 or right_dr_count == 0: - print(" ❌ DisplayRegion未创建,无法设置回调") - return False - - # 获取RenderTarget创建的DisplayRegion - left_dr = self.vr_left_eye_buffer.get_display_region(0) - right_dr = self.vr_right_eye_buffer.get_display_region(0) - - # 验证DisplayRegion有效性 - if not left_dr or not right_dr: - print(" ❌ DisplayRegion无效") - return False - - # 设置渲染回调(与普通模式一致) - left_dr.setDrawCallback(PythonCallbackObject(self.simple_left_cb)) - right_dr.setDrawCallback(PythonCallbackObject(self.simple_right_cb)) - - # 确保DisplayRegion处于活动状态 - left_dr.setActive(True) - right_dr.setActive(True) - - print(" ✅ RenderPipeline DisplayRegion回调已设置") - except Exception as e: - print(f" ⚠️ 设置DisplayRegion回调失败: {e}") - import traceback - traceback.print_exc() - return False - - # 🔧 关键修复:强制同步GraphicsEngine,确保所有buffer完全初始化 - print(" 同步GraphicsEngine...") - self.world.graphicsEngine.renderFrame() - - # 准备纹理并缓存OpenGL ID - if not self._prepare_and_cache_textures(): - print("❌ RenderPipeline纹理准备失败") - return False - - print("✅ VR Pipeline立体渲染管线已准备完成") - else: - # 普通模式:创建新的DisplayRegion - print(" 使用普通渲染区域...") - left_dr = self.vr_left_eye_buffer.makeDisplayRegion() - left_dr.setCamera(self.vr_left_camera) - left_dr.setActive(True) - left_dr.setDrawCallback(PythonCallbackObject(self.simple_left_cb)) - - right_dr = self.vr_right_eye_buffer.makeDisplayRegion() - right_dr.setCamera(self.vr_right_camera) - right_dr.setActive(True) - right_dr.setDrawCallback(PythonCallbackObject(self.simple_right_cb)) - - print("✓ VR相机锚点层级系统设置完成") - return True - - except Exception as e: - print(f"❌ 设置VR相机失败: {e}") - import traceback - traceback.print_exc() - return False - - def _get_eye_offset(self, eye): - """获取眼睛相对于头显的偏移""" - try: - if not self.vr_system: - # 使用标准IPD(瞳距)估算值 - ipd = 0.064 # 64mm,平均IPD - if eye == openvr.Eye_Left: - return Vec3(-ipd/2, 0, 0) - else: - return Vec3(ipd/2, 0, 0) - - # 从OpenVR获取眼睛到头显的变换矩阵 - eye_transform = self.vr_system.getEyeToHeadTransform(eye) - - # 提取位移信息 - x = eye_transform[0][3] - y = eye_transform[1][3] - z = eye_transform[2][3] - - return Vec3(x, y, z) - - except Exception as e: - print(f"❌ 获取眼睛偏移失败: {e}") - # 返回默认值 - ipd = 0.064 - if eye == openvr.Eye_Left: - return Vec3(-ipd/2, 0, 0) - else: - return Vec3(ipd/2, 0, 0) - - def _optimize_vr_rendering(self): - """优化VR模式下的渲染管线 - 减少不必要的后处理开销""" - try: - print("🔧 正在优化VR渲染管线...") - - # 检查是否有RenderPipeline - if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline: - print(" 检测到RenderPipeline,正在优化...") - - # 对VR缓冲区应用轻量级渲染设置 - if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: - self._apply_lightweight_rendering(self.vr_left_eye_buffer, "左眼") - - if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: - self._apply_lightweight_rendering(self.vr_right_eye_buffer, "右眼") - - print("✅ VR渲染管线优化完成") - else: - print(" 未检测到RenderPipeline,使用默认Panda3D渲染") - - # 禁用VR缓冲区的不必要功能 - self._disable_vr_buffer_extras() - - except Exception as e: - print(f"⚠️ VR渲染优化失败: {e}") - import traceback - traceback.print_exc() - - def _apply_lightweight_rendering(self, buffer, eye_name): - """为VR缓冲区应用轻量级渲染设置""" - try: - # 禁用多重采样抗锯齿(MSAA)以提升性能 - if hasattr(buffer, 'setMultisample'): - buffer.setMultisample(0) - print(f" {eye_name}: 禁用MSAA") - - # 设置更简单的清除颜色操作 - buffer.setClearColorActive(True) - buffer.setClearColor((0.1, 0.2, 0.4, 1.0)) # 深蓝色,便于调试 - - # 禁用深度写入到某些不需要的缓冲区 - # (保留主要深度测试,但减少不必要的写入) - - print(f" {eye_name}: 应用轻量级渲染设置") - - except Exception as e: - print(f"⚠️ {eye_name}缓冲区优化失败: {e}") - - def _disable_vr_buffer_extras(self): - """禁用VR缓冲区的额外功能以提升性能""" - try: - # 禁用VR缓冲区的统计收集 - if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: - if hasattr(self.vr_left_eye_buffer, 'setOneShot'): - self.vr_left_eye_buffer.setOneShot(False) - - if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: - if hasattr(self.vr_right_eye_buffer, 'setOneShot'): - self.vr_right_eye_buffer.setOneShot(False) - - print(" VR缓冲区额外功能已优化") - - except Exception as e: - print(f"⚠️ VR缓冲区额外功能优化失败: {e}") - - def _start_vr_task(self): - """启动VR更新任务""" - if self.vr_task: - self.world.taskMgr.remove(self.vr_task) - - # 设置高优先级(sort=-1000),确保在渲染之前执行,参考 panda3d-openvr - self.vr_task = self.world.taskMgr.add(self._update_vr, "update_vr", sort=-1000) - print("✓ VR更新任务已启动(高优先级,sort=-1000)") - - def _update_vr(self, task): - """VR更新任务 - 每帧调用(参考 panda3d-openvr 架构)""" - if not self.vr_enabled or not self.vr_system: - return task.cont - - try: - # 📌 第一件事:waitGetPoses 阻塞等待 VSync(参考 panda3d-openvr) - # 这会阻塞整个任务,确保每个 VSync 周期只执行一次 - should_call_waitgetposes = ( - not self.vr_test_mode or # 普通VR模式总是需要 - self.test_mode_wait_poses # 测试模式:仅当启用姿态等待时 - ) - - if should_call_waitgetposes: - self._wait_get_poses_immediate() - # 立即更新手柄,使用最新姿态(避免过时数据) - self.update_tracked_devices() - - # 性能监控 - self.frame_count += 1 - - # 🚀 自动启用性能模式 - 减少计时对象创建 - if not self.performance_mode_enabled and self.frame_count >= self.performance_mode_trigger_frame: - self.performance_mode_enabled = True - if self.frame_count <= self.performance_mode_trigger_frame + 5: # 只输出一次 - print(f"🎯 性能模式已启用 (帧#{self.frame_count}) - 禁用详细监控以提升性能") - - # 记录帧时间 - self._track_frame_time() - - # 计算VR FPS - import time - current_time = time.time() - if self.last_fps_time == 0: - self.last_fps_time = current_time - elif current_time - self.last_fps_time >= 1.0: # 每秒更新一次FPS - self.vr_fps = (self.frame_count - self.last_fps_check) / (current_time - self.last_fps_time) - self.last_fps_check = self.frame_count - self.last_fps_time = current_time - - # 📌 使用刚获取的姿态更新相机(而不是旧姿态) - self._update_camera_poses() - - # 输出策略信息(仅第一次) - if not hasattr(self, '_new_architecture_logged'): - print("✅ 新架构已启用 - waitGetPoses 在任务开始阻塞,参考 panda3d-openvr") - print(" 这确保每个 VSync 周期只渲染一次,解决 AlreadySubmitted 错误") - self._new_architecture_logged = True - - # 更新VR动作状态 - 仅在启用时更新 - if not self.disable_action_system and self.action_manager: - self.action_manager.update_actions() - - # 更新VR交互系统 - if self.interaction_manager: - self.interaction_manager.update() - - # 更新VR摇杆系统 - if self.joystick_manager: - # 计算帧间隔时间 - import time - if not hasattr(self, '_last_frame_time'): - self._last_frame_time = time.time() - dt = 0.016 # 默认60fps - else: - current_time = time.time() - dt = current_time - self._last_frame_time - self._last_frame_time = current_time - # 限制dt范围,避免异常情况 - dt = max(0.001, min(0.1, dt)) - - self.joystick_manager.update(dt) - - # 更新系统性能指标(减少频率) - if self.frame_count % 30 == 1: # 每30帧更新一次,减少开销 - self._update_performance_metrics() - - # 更新GPU渲染时间统计(减少频率) - if self.enable_gpu_timing and self.frame_count % 60 == 1: - self._get_gpu_frame_timing() - - # 🚀 手动垃圾回收控制 - 避免16-19帧周期性峰值 - self._manual_gc_control() - - # 定期输出性能报告 - 默认10秒间隔 - report_interval = getattr(self, 'performance_report_interval', 600) - if self.frame_count % report_interval == 1: - self._print_performance_report() - - except Exception as e: - print(f"VR更新错误: {e}") - import traceback - traceback.print_exc() - - return task.cont - - def _sync_gpu_if_needed(self): - """可选的GPU同步 - 仅在需要时使用""" - try: - # 同步等待GPU完成(如果需要) - gsg = self.world.win.getGsg() - if gsg: - gsg.getEngine().syncFrame() - except Exception as e: - print(f"❌ GPU同步异常: {e}") - - def simple_left_cb(self, cbdata): - """简化的左眼渲染回调 - 精确控制渲染和提交""" - try: - # 🔍 精确测量渲染时间 - import time - render_start = time.perf_counter() - - # 触发实际渲染 - cbdata.upcall() - - # 计算真实渲染时间 - self.left_render_time = (time.perf_counter() - render_start) * 1000 # 转换为毫秒 - - # 记录渲染次数 - self.left_render_count += 1 - - # 📌 OpenVR 帧边界检查:防止同一 OpenVR 帧内重复渲染 - if self.left_eye_last_render_frame == self.openvr_frame_id: - return # 已在当前 OpenVR 帧渲染过,跳过 - - # 🔧 OpenVR最佳实践:左眼只渲染,不立即提交 - # 基于官方hellovr示例:两眼都渲染完后再批量提交 - self.left_eye_last_render_frame = self.openvr_frame_id - - # 渐进式VR功能测试:单独启用纹理提交时保留原逻辑 - should_submit = self.vr_test_mode and self.test_mode_submit_texture and not self.test_mode_wait_poses - - if should_submit: - # 测试模式:单独启用纹理提交时的兼容模式 - if self.vr_compositor and self.vr_left_texture: - self.submit_texture(openvr.Eye_Left, self.vr_left_texture) - - if self.vr_test_mode: - # 测试模式:始终触发屏幕显示更新 - self._update_test_display() - - except Exception as e: - print(f"左眼渲染回调错误: {e}") - - def simple_right_cb(self, cbdata): - """简化的右眼渲染回调 - 精确控制渲染和提交""" - try: - # 🔍 精确测量渲染时间 - import time - render_start = time.perf_counter() - - # 触发实际渲染 - cbdata.upcall() - - # 计算真实渲染时间 - self.right_render_time = (time.perf_counter() - render_start) * 1000 # 转换为毫秒 - - # 记录渲染次数 - self.right_render_count += 1 - - # 📌 OpenVR 帧边界检查:防止同一 OpenVR 帧内重复渲染 - if self.right_eye_last_render_frame == self.openvr_frame_id: - return # 已在当前 OpenVR 帧渲染过,跳过 - - # 🔧 OpenVR最佳实践:右眼渲染完成后批量提交两眼纹理 - # 基于官方hellovr示例:避免分散提交导致的VSync阻塞 - self.right_eye_last_render_frame = self.openvr_frame_id - - # 🔧 渐进式VR功能测试:根据调试标志决定启用哪些功能 - should_batch_submit = not self.vr_test_mode or self.test_mode_submit_texture - should_wait_poses = not self.vr_test_mode or self.test_mode_wait_poses - - # 特殊处理:测试模式单独启用功能时的兼容模式 - if self.vr_test_mode: - if self.test_mode_submit_texture and not self.test_mode_wait_poses: - # 单独测试纹理提交:使用原来的分散提交方式 - if self.vr_compositor and self.vr_right_texture: - self.submit_texture(openvr.Eye_Right, self.vr_right_texture) - - # 🚀 测试模式也需要PostPresentHandoff避免36FPS - try: - if hasattr(self.vr_compositor, 'postPresentHandoff'): - self.vr_compositor.postPresentHandoff() - elif hasattr(self.vr_compositor, 'PostPresentHandoff'): - self.vr_compositor.PostPresentHandoff() - except Exception as e: - pass # 测试模式静默忽略错误 - should_batch_submit = False - elif self.test_mode_wait_poses and not self.test_mode_submit_texture: - # 单独测试姿态等待:不提交纹理 - should_batch_submit = False - - if should_batch_submit: - # 🚀 OpenVR最佳实践:批量提交两眼纹理 - # 这是解决36FPS问题的关键修复 - self._batch_submit_textures() - - # 🔧 关键修复:移除Submit后立即WaitGetPoses的错误实现 - # 根据OpenVR官方文档:"Calling WaitGetPoses immediately after Submit is conspicuously wrong" - # WaitGetPoses应该在下一帧开始时通过update_vr_task调用,不是Submit后立即调用 - # - # if should_wait_poses: - # # 错误的实现:Submit后立即获取姿态导致36FPS - # self._wait_get_poses_immediate() # ← 这是36FPS的根本原因! - # - # 正确的实现:让update_vr_task在下一帧开始时调用WaitGetPoses - - if self.vr_test_mode: - # 测试模式:始终更新性能HUD - self._update_test_performance_hud() - - except Exception as e: - print(f"右眼渲染回调错误: {e}") - - def _wait_get_poses(self): - """调用VRCompositor的waitGetPoses来获取焦点和姿态数据""" - try: - if not self.vr_compositor or not self.poses: - return - - # 调用waitGetPoses获取焦点和姿态数据 - # 这个调用可能会阻塞直到下一个VR同步点 - result = self.vr_compositor.waitGetPoses(self.poses, None) - - # 检查姿态数据的有效性 - valid_poses = 0 - - # 更新HMD姿态(设备0通常是头显) - if len(self.poses) > 0 and self.poses[0].bPoseIsValid: - valid_poses += 1 - else: - # 如果HMD姿态无效,不要频繁输出错误信息 - if not hasattr(self, '_hmd_invalid_warning_shown'): - print("⚠️ HMD姿态数据无效") - self._hmd_invalid_warning_shown = True - - # 更新控制器姿态 - self.controller_poses.clear() - for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): - if self.poses[device_id].bPoseIsValid: - device_class = self.vr_system.getTrackedDeviceClass(device_id) - if device_class == openvr.TrackedDeviceClass_Controller: - controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking - self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) - valid_poses += 1 - - # 性能监控 - 偶尔输出姿态状态 - if self.frame_count % 600 == 1: # 每10秒输出一次@60fps - print(f"📊 VR姿态状态 - 有效姿态数: {valid_poses}, 总帧数: {self.frame_count}") - - except Exception as e: - # 限制错误输出频率 - if not hasattr(self, '_last_error_frame'): - self._last_error_frame = 0 - - if self.frame_count - self._last_error_frame > 300: # 每5秒最多输出一次错误 - print(f"waitGetPoses失败: {e}") - self._last_error_frame = self.frame_count - - # 记录姿态失败次数 - self.pose_failures += 1 - - def _wait_get_poses_immediate(self): - """立即获取VR姿态 - 修复ATW闪烁的关键方法(双姿态版本)""" - # 开始计时waitGetPoses操作 - timing = self._start_timing('wait_poses') - - try: - if not self.vr_compositor or not self.poses or not self.game_poses: - self._end_timing(timing) - return - - # 关键修复:传递渲染姿态和游戏姿态数组 - # 渲染姿态用于绘制,游戏姿态用于逻辑,避免ATW过度补偿 - result = self.vr_compositor.waitGetPoses(self.poses, self.game_poses) - - # 📌 waitGetPoses 成功后立即递增 OpenVR 帧ID(新帧开始) - self.openvr_frame_id += 1 - - # 结束计时 - wait_time = self._end_timing(timing) - - # 检查姿态数据的有效性 - valid_poses = 0 - - # 更新HMD姿态(设备0通常是头显) - if len(self.poses) > 0 and self.poses[0].bPoseIsValid: - valid_poses += 1 - else: - # 如果HMD姿态无效,不要频繁输出错误信息 - if not hasattr(self, '_hmd_invalid_warning_shown'): - print("⚠️ HMD姿态数据无效(立即模式)") - self._hmd_invalid_warning_shown = True - - # 🚀 优化控制器姿态更新:使用缓存,避免每帧clear()和重新创建对象 - for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): - if self.poses[device_id].bPoseIsValid: - device_class = self.vr_system.getTrackedDeviceClass(device_id) - if device_class == openvr.TrackedDeviceClass_Controller: - controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking - - # 检查是否已有此设备的缓存矩阵 - if device_id not in self.controller_poses: - # 第一次检测到该控制器,创建新的缓存矩阵 - self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) - else: - # 复用现有矩阵,只更新数值,避免创建新对象 - cached_mat = self.controller_poses[device_id] - self._update_matrix_from_openvr(cached_mat, controller_matrix) - - valid_poses += 1 - else: - # 设备姿态无效,从字典中移除(如果存在) - if device_id in self.controller_poses: - # 将矩阵返回对象池 - self._return_pooled_matrix(self.controller_poses[device_id]) - del self.controller_poses[device_id] - - # 🔧 关键修复:立即更新手柄和跟踪设备 - # Running Start模式下必须在WaitGetPoses后立即更新,避免手柄消失 - self.update_tracked_devices() - - # 调试信息 - 仅在第一次成功时输出 - if not hasattr(self, '_dual_pose_mode_logged') and valid_poses > 0: - print(f"✅ 双姿态立即获取模式启用 - 有效姿态数: {valid_poses}") - print(" 渲染姿态用于绘制,游戏姿态用于逻辑,防止ATW过度补偿") - print(" 手柄和跟踪设备在WaitGetPoses后立即更新") - self._dual_pose_mode_logged = True - - except Exception as e: - # 限制错误输出频率 - if not hasattr(self, '_last_immediate_error_frame'): - self._last_immediate_error_frame = 0 - - if self.frame_count - self._last_immediate_error_frame > 300: # 每5秒最多输出一次错误 - print(f"立即姿态获取失败: {e}") - self._last_immediate_error_frame = self.frame_count - - self.pose_failures += 1 - - def _wait_get_poses_with_prediction(self): - """使用预测时间获取VR姿态 - 优化性能的新策略""" - try: - if not self.vr_compositor or not self.poses or not self.game_poses: - return - - # 使用预测时间获取姿态 - # 预测时间通常为11-16ms,对应下一个VR帧的时间 - result = self.vr_compositor.waitGetPoses(self.poses, self.game_poses) - - # 检查姿态数据的有效性 - valid_poses = 0 - - # 更新HMD姿态(设备0通常是头显) - if len(self.poses) > 0 and self.poses[0].bPoseIsValid: - valid_poses += 1 - else: - # 如果HMD姿态无效,不要频繁输出错误信息 - if not hasattr(self, '_hmd_invalid_warning_task'): - print("⚠️ HMD姿态数据无效(更新任务模式)") - self._hmd_invalid_warning_task = True - - # 更新控制器姿态 - self.controller_poses.clear() - for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): - if self.poses[device_id].bPoseIsValid: - device_class = self.vr_system.getTrackedDeviceClass(device_id) - if device_class == openvr.TrackedDeviceClass_Controller: - controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking - self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) - valid_poses += 1 - - # 调试信息 - 仅在第一次成功时输出 - if not hasattr(self, '_update_task_mode_logged') and valid_poses > 0: - print(f"✅ 更新任务姿态获取模式启用 - 有效姿态数: {valid_poses}") - print(f" 预测时间: {self.use_prediction_time*1000:.1f}ms") - self._update_task_mode_logged = True - - except Exception as e: - # 限制错误输出频率 - if not hasattr(self, '_last_task_error_frame'): - self._last_task_error_frame = 0 - - if self.frame_count - self._last_task_error_frame > 300: # 每5秒最多输出一次错误 - print(f"更新任务姿态获取失败: {e}") - self._last_task_error_frame = self.frame_count - - # 记录姿态失败次数 - self.pose_failures += 1 - - def _cache_poses_for_next_frame(self): - """缓存当前姿态供下一帧渲染使用 - 修复时序不匹配""" - try: - if not self.poses or len(self.poses) == 0: - return - - # 如果是第一帧,直接使用当前姿态 - if self._first_frame: - self._cached_render_poses = self.poses - self._first_frame = False - print("✓ 首帧姿态缓存已设置") - return - - # 复制当前渲染姿态到缓存 - # 下一帧将使用这些姿态进行渲染 - poses_t = openvr.TrackedDevicePose_t * openvr.k_unMaxTrackedDeviceCount - cached_poses = poses_t() - - # 复制姿态数据 - for i in range(len(self.poses)): - cached_poses[i] = self.poses[i] - - self._cached_render_poses = cached_poses - - except Exception as e: - print(f"⚠️ 姿态缓存失败: {e}") - - - def _reset_waitgetposes_flag(self, task): - """重置WaitGetPoses标记 - 确保下一帧可以调用WaitGetPoses""" - self._waitgetposes_called_this_frame = False - return task.done - - def _update_tracking_data(self): - """更新VR追踪数据""" - try: - # 获取设备姿态 - poses = self.vr_system.getDeviceToAbsoluteTrackingPose( - openvr.TrackingUniverseStanding, 0.0, openvr.k_unMaxTrackedDeviceCount - ) - - # 更新HMD姿态(设备0通常是头显) - if poses[0].bPoseIsValid: - hmd_matrix = poses[0].mDeviceToAbsoluteTracking - self.hmd_pose = self._convert_openvr_matrix_to_panda(hmd_matrix) - - # 更新控制器姿态 - for device_id in range(1, openvr.k_unMaxTrackedDeviceCount): - if poses[device_id].bPoseIsValid: - device_class = self.vr_system.getTrackedDeviceClass(device_id) - if device_class == openvr.TrackedDeviceClass_Controller: - controller_matrix = poses[device_id].mDeviceToAbsoluteTracking - self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) - - except Exception as e: - print(f"更新追踪数据失败: {e}") - - def _convert_openvr_matrix_to_panda(self, ovr_matrix): - """将OpenVR矩阵转换为Panda3D矩阵 - 使用对象池优化 - - 坐标系转换: - OpenVR: X右, Y上, -Z前(右手坐标系) - Panda3D: X右, Y前, Z上(右手坐标系) - - 转换规则: - OpenVR X → Panda3D X - OpenVR Y → Panda3D Z - OpenVR -Z → Panda3D Y - """ - # 🚀 使用对象池获取预分配的Mat4对象,避免每帧创建新对象 - mat = self._get_pooled_matrix() - - # 修正的坐标转换矩阵 - # OpenVR: X右, Y上, -Z前 → Panda3D: X右, Y前, Z上 - # 转换规则: (ovr_x, ovr_y, ovr_z) → (panda_x, panda_y, panda_z) - # (ovr_x, ovr_y, ovr_z) → (ovr_x, -ovr_z, ovr_y) - - # X轴行:Panda3D的X轴对应OpenVR的X轴 - mat.setCell(0, 0, ovr_matrix[0][0]) # X_x → X_x - mat.setCell(0, 1, ovr_matrix[0][1]) # X_y → X_y - mat.setCell(0, 2, ovr_matrix[0][2]) # X_z → X_z - mat.setCell(0, 3, ovr_matrix[0][3]) # 位移X分量 - - # Y轴行:Panda3D的Y轴对应OpenVR的-Z轴 - mat.setCell(1, 0, -ovr_matrix[2][0]) # -Z_x → Y_x - mat.setCell(1, 1, -ovr_matrix[2][1]) # -Z_y → Y_y - mat.setCell(1, 2, -ovr_matrix[2][2]) # -Z_z → Y_z - mat.setCell(1, 3, -ovr_matrix[2][3]) # 位移Y分量(-Z位移) - - # Z轴行:Panda3D的Z轴对应OpenVR的Y轴 - mat.setCell(2, 0, ovr_matrix[1][0]) # Y_x → Z_x - mat.setCell(2, 1, ovr_matrix[1][1]) # Y_y → Z_y - mat.setCell(2, 2, ovr_matrix[1][2]) # Y_z → Z_z - mat.setCell(2, 3, ovr_matrix[1][3]) # 位移Z分量(Y位移) - - # 齐次坐标 - mat.setCell(3, 0, 0) - mat.setCell(3, 1, 0) - mat.setCell(3, 2, 0) - mat.setCell(3, 3, 1) - - # 🚀 优化调试信息 - 避免创建Vec3对象,减少GC压力 - if not hasattr(self, '_coord_debug_counter'): - self._coord_debug_counter = 0 - self._coord_debug_counter += 1 - - if self._coord_debug_counter % 600 == 1: # 每10秒输出一次@60fps - print(f"🔄 坐标系转换调试 (第{self._coord_debug_counter}帧)") - - # 直接输出数值,避免创建Vec3对象 - ovr_x, ovr_y, ovr_z = ovr_matrix[0][3], ovr_matrix[1][3], ovr_matrix[2][3] - print(f" OpenVR原始位置: ({ovr_x:.3f}, {ovr_y:.3f}, {ovr_z:.3f})") - - # 直接从矩阵读取数值,避免创建Vec3对象 - panda_x = mat.getCell(0, 3) - panda_y = mat.getCell(1, 3) - panda_z = mat.getCell(2, 3) - print(f" Panda3D转换位置: ({panda_x:.3f}, {panda_y:.3f}, {panda_z:.3f})") - - # 手动验证转换:OpenVR (x,y,z) → Panda3D (x,-z,y) - expected_x = ovr_x - expected_y = -ovr_z - expected_z = ovr_y - print(f" 预期转换结果: ({expected_x:.3f}, {expected_y:.3f}, {expected_z:.3f})") - - # 计算误差,避免创建Vec3对象 - diff_x = panda_x - expected_x - diff_y = panda_y - expected_y - diff_z = panda_z - expected_z - diff_magnitude = (diff_x*diff_x + diff_y*diff_y + diff_z*diff_z)**0.5 - - if diff_magnitude < 0.001: - print(f" ✅ 坐标转换正确 (误差: {diff_magnitude:.6f})") - else: - print(f" ⚠️ 坐标转换可能有误 (误差: {diff_magnitude:.6f})") - print(f" 差异: ({diff_x:.6f}, {diff_y:.6f}, {diff_z:.6f})") - - return mat - - def update_hmd(self, pose): - """ - 更新HMD锚点 - 基于参考实现 - """ - try: - # 将OpenVR姿态转换为Panda3D矩阵 - modelview = self.convert_mat(pose.mDeviceToAbsoluteTracking) - - # 应用坐标系转换并设置HMD锚点 - self.hmd_anchor.setMat(self.coord_mat_inv * modelview * self.coord_mat) - - # 获取眼睛到头部的变换 - view_left = self.convert_mat(self.vr_system.getEyeToHeadTransform(openvr.Eye_Left)) - view_right = self.convert_mat(self.vr_system.getEyeToHeadTransform(openvr.Eye_Right)) - - # 设置眼睛锚点 - self.left_eye_anchor.setMat(self.coord_mat_inv * view_left * self.coord_mat) - self.right_eye_anchor.setMat(self.coord_mat_inv * view_right * self.coord_mat) - - except Exception as e: - print(f"更新HMD姿态失败: {e}") - - def _update_camera_poses(self): - """更新相机姿态 - 使用锚点系统简化处理""" - try: - # 使用锚点系统后,相机位置自动跟随锚点 - # 只需要获取HMD姿态并更新锚点即可 - - # 从poses数组中获取HMD姿态 - if hasattr(self, 'poses') and len(self.poses) > 0: - hmd_pose = self.poses[openvr.k_unTrackedDeviceIndex_Hmd] - if hmd_pose.bPoseIsValid: - self.update_hmd(hmd_pose) - else: - print("⚠️ HMD姿态数据无效") - - except Exception as e: - print(f"更新相机姿态失败: {e}") - import traceback - traceback.print_exc() - - def _update_camera_poses_with_cache(self): - """使用缓存姿态更新相机 - 符合OpenVR时序假设""" - try: - # 使用缓存的姿态,符合OpenVR的时序假设 - # OpenVR假设你用上一次WaitGetPoses的姿态渲染当前提交的帧 - - if not self._cached_render_poses: - # 如果没有缓存姿态,回退到当前姿态(首帧情况) - print("⚠️ 没有缓存姿态,使用当前姿态") - return self._update_camera_poses() - - # 从缓存姿态数组中获取HMD姿态 - if len(self._cached_render_poses) > 0: - hmd_pose = self._cached_render_poses[openvr.k_unTrackedDeviceIndex_Hmd] - if hmd_pose.bPoseIsValid: - self.update_hmd(hmd_pose) - - # 调试信息 - 验证缓存姿态使用 - if not hasattr(self, '_cached_pose_logged'): - print("✅ 使用缓存姿态更新相机 - 符合OpenVR时序假设") - self._cached_pose_logged = True - else: - print("⚠️ 缓存的HMD姿态数据无效") - - except Exception as e: - print(f"使用缓存姿态更新相机失败: {e}") - # 回退到正常更新方式 - self._update_camera_poses() - import traceback - traceback.print_exc() - - - def enable_vr(self): - """启用VR模式""" - if not self.is_vr_available(): - print("❌ VR系统不可用") - return False - - if not self.vr_initialized: - if not self.initialize_vr(): - return False - - self.vr_enabled = True - - # 禁用主相机避免干扰VR渲染 - self._disable_main_cam() - - # VR性能优化:使用Running Start模式(Valve最佳实践) - print("🚀 VR性能优化:Running Start模式已启用") - print(" 优势:在帧开始时获取姿态,提供VSync前3ms的渲染时间") - print(" 注意:Submit后立即调用WaitGetPoses是错误实现") - self.set_prediction_time(11.0) # 11ms预测时间 - OpenVR标准值,平衡准确性和延迟 - - # 🚀 动态调整Qt Timer频率以支持VR - if hasattr(self.world, 'qtWidget') and self.world.qtWidget: - if hasattr(self.world.qtWidget, 'synchronizer'): - # 设置为144Hz,让OpenVR控制实际渲染节奏 - self.world.qtWidget.synchronizer.setInterval(int(1000/144)) - print("✓ Qt Timer调整为144Hz,让OpenVR控制VR渲染节奏") - - # 🔧 关键修复:检测并重建缺失的手柄visualizer - # 当渲染模式切换时,visualizer可能被清理但控制器对象仍存在 - if hasattr(self, 'left_controller') and self.left_controller: - if not self.left_controller.visualizer and self.left_controller.anchor_node: - print("🔧 检测到左手柄visualizer缺失,正在重建...") - self.left_controller._create_visualizer() - if self.left_controller.visualizer: - print("✅ 左手柄visualizer已重建") - - if hasattr(self, 'right_controller') and self.right_controller: - if not self.right_controller.visualizer and self.right_controller.anchor_node: - print("🔧 检测到右手柄visualizer缺失,正在重建...") - self.right_controller._create_visualizer() - if self.right_controller.visualizer: - print("✅ 右手柄visualizer已重建") - - print("✅ VR模式已启用") - return True - - def disable_vr(self): - """禁用VR模式""" - self.vr_enabled = False - - # 清理手柄可视化(但保留控制器对象以便重新启用) - if hasattr(self, 'left_controller') and self.left_controller: - if hasattr(self.left_controller, 'visualizer') and self.left_controller.visualizer: - try: - self.left_controller.visualizer.cleanup() - self.left_controller.visualizer = None - print("✓ 左手控制器可视化已清理") - except Exception as e: - print(f"⚠️ 清理左手控制器可视化失败: {e}") - - if hasattr(self, 'right_controller') and self.right_controller: - if hasattr(self.right_controller, 'visualizer') and self.right_controller.visualizer: - try: - self.right_controller.visualizer.cleanup() - self.right_controller.visualizer = None - print("✓ 右手控制器可视化已清理") - except Exception as e: - print(f"⚠️ 清理右手控制器可视化失败: {e}") - - # 隐藏手柄锚点节点 - if hasattr(self, 'left_controller') and self.left_controller and self.left_controller.anchor_node: - self.left_controller.anchor_node.hide() - - if hasattr(self, 'right_controller') and self.right_controller and self.right_controller.anchor_node: - self.right_controller.anchor_node.hide() - - # 恢复主相机 - self._enable_main_cam() - - # 恢复Qt Timer到60FPS - if hasattr(self.world, 'qtWidget') and self.world.qtWidget: - if hasattr(self.world.qtWidget, 'synchronizer'): - self.world.qtWidget.synchronizer.setInterval(int(1000/60)) - print("✓ Qt Timer恢复为60Hz") - - print("✅ VR模式已禁用,手柄模型已隐藏") - - def set_vr_render_mode(self, mode): - """切换VR渲染模式 - - Args: - mode: VRRenderMode枚举值或字符串 ('normal' 或 'render_pipeline') - - Returns: - bool: 切换是否成功 - """ - try: - # 转换输入为枚举类型 - if isinstance(mode, str): - mode_str = mode.lower() - if mode_str == "normal": - new_mode = VRRenderMode.NORMAL - elif mode_str in ["render_pipeline", "renderpipeline", "pipeline"]: - new_mode = VRRenderMode.RENDER_PIPELINE - else: - print(f"❌ 无效的渲染模式: {mode}") - print(" 支持的模式: 'normal' 或 'render_pipeline'") - return False - elif isinstance(mode, VRRenderMode): - new_mode = mode - else: - print(f"❌ 无效的模式类型: {type(mode)}") - return False - - # 检查是否与当前模式相同 - if new_mode == self.vr_render_mode: - print(f"✓ VR渲染模式已经是 {new_mode.value},无需切换") - return True - - print(f"🔄 正在切换VR渲染模式: {self.vr_render_mode.value} → {new_mode.value}") - - # 检查VR是否已初始化 - if not self.vr_initialized: - print("⚠️ VR未初始化,仅更新渲染模式配置") - self.vr_render_mode = new_mode - print(f"✓ VR渲染模式已更新为 {new_mode.value}(下次启动VR时生效)") - return True - - # 保存当前VR启用状态 - was_enabled = self.vr_enabled - - # 如果VR已启用,先禁用 - if was_enabled: - print(" 暂时禁用VR...") - self.disable_vr() - - # 更新渲染模式 - old_mode = self.vr_render_mode - self.vr_render_mode = new_mode - - # 清理现有缓冲区 - print(" 清理现有渲染缓冲区...") - self._cleanup_vr_buffers() - - # 根据新模式重建缓冲区 - print(f" 创建新的渲染缓冲区({new_mode.value})...") - success = False - - if new_mode == VRRenderMode.RENDER_PIPELINE: - success = self._create_vr_buffers_with_pipeline() - if not success: - print("⚠️ RenderPipeline模式创建失败,回退到普通渲染模式") - self.vr_render_mode = VRRenderMode.NORMAL - success = self._create_vr_buffers() - else: - success = self._create_vr_buffers() - - if not success: - print("❌ 缓冲区创建失败,尝试恢复原模式") - self.vr_render_mode = old_mode - if old_mode == VRRenderMode.RENDER_PIPELINE: - self._create_vr_buffers_with_pipeline() - else: - self._create_vr_buffers() - return False - - # 重新设置相机 - print(" 重新设置VR相机...") - if not self._setup_vr_cameras(): - print("❌ 相机设置失败") - return False - - # 如果之前VR是启用的,重新启用 - if was_enabled: - print(" 重新启用VR...") - self.enable_vr() - - # 🔧 关键修复:重建所有手柄的visualizer以适配新渲染模式 - print(" 刷新手柄visualizer以适配新渲染模式...") - if hasattr(self, 'left_controller') and self.left_controller: - self.left_controller.recreate_visualizer() - - if hasattr(self, 'right_controller') and self.right_controller: - self.right_controller.recreate_visualizer() - - print(f"✅ VR渲染模式已切换为 {self.vr_render_mode.value}") - - # 保存配置 - if self.config_manager: - self.config_manager.save_from_vr_manager(self) - - return True - - except Exception as e: - print(f"❌ 切换VR渲染模式失败: {e}") - import traceback - traceback.print_exc() - return False - - def get_vr_render_mode(self): - """获取当前VR渲染模式 - - Returns: - VRRenderMode: 当前渲染模式 - """ - return self.vr_render_mode - - def _cleanup_vr_buffers(self): - """清理VR渲染缓冲区""" - try: - # 清理VR Pipeline Controller(如果使用RenderPipeline模式) - if self.vr_pipeline_controller: - print(" 清理VR Pipeline...") - self.vr_pipeline_controller.cleanup_all() - self.vr_pipeline_controller = None - - # 清理左眼缓冲区 - if self.vr_left_eye_buffer: - # 如果是RenderPipeline模式,buffer已被VRPipelineController清理 - # 如果是普通模式,需要手动清理 - if self.vr_render_mode == VRRenderMode.NORMAL: - self.world.graphicsEngine.removeWindow(self.vr_left_eye_buffer) - self.vr_left_eye_buffer = None - - # 清理右眼缓冲区 - if self.vr_right_eye_buffer: - if self.vr_render_mode == VRRenderMode.NORMAL: - self.world.graphicsEngine.removeWindow(self.vr_right_eye_buffer) - self.vr_right_eye_buffer = None - - # 清理纹理 - self.vr_left_texture = None - self.vr_right_texture = None - self.left_texture_id = None - self.right_texture_id = None - self.textures_prepared = False - - # 清理RenderPipeline渲染目标(旧版,保留兼容性) - if self.vr_pipeline_left_target: - self.vr_pipeline_left_target = None - - if self.vr_pipeline_right_target: - self.vr_pipeline_right_target = None - - self.render_pipeline_enabled = False - - print("✓ VR渲染缓冲区已清理") - - except Exception as e: - print(f"⚠️ 清理VR缓冲区时出错: {e}") - - def cleanup(self): - """清理VR资源""" - try: - print("🔄 正在清理VR资源...") - - # 停止VR任务 - if self.vr_task: - self.world.taskMgr.remove(self.vr_task) - self.vr_task = None - - # 🚀 恢复Python垃圾回收并清理对象池 - if self._gc_disabled: - # 最后一次手动垃圾回收 - collected = gc.collect() - print(f"🗑️ 最终GC清理: {collected} 个对象") - - # 恢复自动垃圾回收 - gc.enable() - self._gc_disabled = False - print("✅ Python垃圾回收已恢复为自动模式") - - # 清理对象池 - if hasattr(self, '_matrix_pool'): - pool_size = len(self._matrix_pool) - self._matrix_pool.clear() - print(f"🧹 Mat4对象池已清理: {pool_size} 个对象") - - # 清理缓存 - if hasattr(self, '_cached_matrices'): - self._cached_matrices.clear() - if hasattr(self, '_controller_poses_cache'): - self._controller_poses_cache.clear() - - # 清理OpenVR Texture对象缓存 - if hasattr(self, '_left_ovr_texture'): - self._left_ovr_texture = None - if hasattr(self, '_right_ovr_texture'): - self._right_ovr_texture = None - print("🧹 OpenVR Texture对象缓存已清理") - - # 清理渲染缓冲区 - if self.vr_left_eye_buffer: - self.vr_left_eye_buffer.removeAllDisplayRegions() - self.world.graphicsEngine.removeWindow(self.vr_left_eye_buffer) - self.vr_left_eye_buffer = None - - if self.vr_right_eye_buffer: - self.vr_right_eye_buffer.removeAllDisplayRegions() - self.world.graphicsEngine.removeWindow(self.vr_right_eye_buffer) - self.vr_right_eye_buffer = None - - # 清理相机 - if self.vr_left_camera: - self.vr_left_camera.removeNode() - self.vr_left_camera = None - - if self.vr_right_camera: - self.vr_right_camera.removeNode() - self.vr_right_camera = None - - # 清理控制器 - if hasattr(self, 'left_controller') and self.left_controller: - try: - self.left_controller.cleanup() - self.left_controller = None - print("✓ 左手控制器已清理") - except Exception as e: - print(f"⚠️ 清理左手控制器失败: {e}") - - if hasattr(self, 'right_controller') and self.right_controller: - try: - self.right_controller.cleanup() - self.right_controller = None - print("✓ 右手控制器已清理") - except Exception as e: - print(f"⚠️ 清理右手控制器失败: {e}") - - # 清理控制器字典 - if hasattr(self, 'controllers'): - self.controllers.clear() - print("✓ 控制器字典已清理") - - # 清理VR子系统 - if hasattr(self, 'joystick_manager') and self.joystick_manager: - try: - self.joystick_manager.cleanup() - self.joystick_manager = None - print("✓ VR摇杆系统已清理") - except Exception as e: - print(f"⚠️ 清理VR摇杆系统失败: {e}") - - if hasattr(self, 'teleport_system') and self.teleport_system: - try: - self.teleport_system.cleanup() - self.teleport_system = None - print("✓ VR传送系统已清理") - except Exception as e: - print(f"⚠️ 清理VR传送系统失败: {e}") - - if hasattr(self, 'interaction_manager') and self.interaction_manager: - try: - self.interaction_manager.cleanup() - self.interaction_manager = None - print("✓ VR交互系统已清理") - except Exception as e: - print(f"⚠️ 清理VR交互系统失败: {e}") - - if hasattr(self, 'action_manager') and self.action_manager: - try: - self.action_manager.cleanup() - self.action_manager = None - print("✓ VR动作系统已清理") - except Exception as e: - print(f"⚠️ 清理VR动作系统失败: {e}") - - # 关闭OpenVR - if self.vr_system and OPENVR_AVAILABLE: - try: - openvr.shutdown() - except: - pass - self.vr_system = None - - self.vr_enabled = False - self.vr_initialized = False - - print("✅ VR资源清理完成") - - except Exception as e: - print(f"⚠️ VR清理过程中出错: {e}") - - def get_vr_status(self): - """获取VR状态信息""" - return { - 'available': self.is_vr_available(), - 'initialized': self.vr_initialized, - 'enabled': self.vr_enabled, - 'eye_resolution': (self.eye_width, self.eye_height), - 'device_count': len(self.controller_poses) + (1 if self.vr_enabled else 0), - 'vr_fps': self.vr_fps, - 'frame_count': self.frame_count, - 'submit_failures': self.submit_failures, - 'pose_failures': self.pose_failures - } - - def _print_performance_report(self): - """输出VR性能报告""" - if not self.performance_monitoring or not self.debug_output_enabled: - return - - stats = self.get_performance_stats() - - # 简短模式输出 - if self.debug_mode == 'brief': - self._print_brief_performance_report(stats) - return - - print("📊 ======= VR性能监控报告 =======") - - # 帧率和帧时间信息 - print(f"🎯 渲染性能:") - print(f" VR帧率: {stats['vr_fps']:.1f} FPS") - print(f" 平均帧时间: {stats['frame_time_avg']:.2f} ms") - print(f" 最小帧时间: {stats['frame_time_min']:.2f} ms") - print(f" 最大帧时间: {stats['frame_time_max']:.2f} ms") - print(f" 95%帧时间: {stats['frame_time_95th']:.2f} ms") - - # 系统性能 - print(f"💻 系统性能:") - print(f" CPU使用率: {stats['cpu_usage']:.1f}%") - print(f" 内存使用率: {stats['memory_usage']:.1f}%") - - # GPU性能 - print(f"🎮 GPU性能:") - if self.gputil_available or self.nvidia_ml_available: - print(f" GPU使用率: {stats['gpu_usage']:.1f}%") - print(f" 显存使用率: {stats['gpu_memory_usage']:.1f}%") - else: - print(f" GPU监控: 不可用 (需要安装 GPUtil 或 pynvml)") - print(f" 安装命令: pip install GPUtil nvidia-ml-py") - - # GPU渲染时间(OpenVR Frame Timing) - if self.enable_gpu_timing: - print(f"⚡ GPU渲染时间:") - pipeline_stats = self._get_pipeline_stats() - gpu_stats = pipeline_stats.get('gpu_timing', {}) - gpu_current = pipeline_stats.get('current', {}) - - # 检查是否有可用的GPU时间数据 - has_gpu_data = any( - gpu_current.get(field, 0) > 0 - for field in ['gpu_scene_render', 'gpu_total_render', 'gpu_pre_submit', 'gpu_post_submit', 'gpu_compositor_render'] - ) - - if has_gpu_data: - # 显示GPU时间统计(最近30帧平均) - scene_render = gpu_stats.get('scene_render', {'avg': 0}) - total_render = gpu_stats.get('total_render', {'avg': 0}) - pre_submit = gpu_stats.get('pre_submit', {'avg': 0}) - post_submit = gpu_stats.get('post_submit', {'avg': 0}) - compositor = gpu_stats.get('compositor_render', {'avg': 0}) - - if scene_render['avg'] > 0: - print(f" GPU场景渲染: {scene_render['avg']:.2f}ms (min:{scene_render['min']:.1f}, max:{scene_render['max']:.1f})") - if total_render['avg'] > 0: - print(f" GPU总渲染时间: {total_render['avg']:.2f}ms (min:{total_render['min']:.1f}, max:{total_render['max']:.1f})") - if pre_submit['avg'] > 0: - print(f" GPU提交前时间: {pre_submit['avg']:.2f}ms (min:{pre_submit['min']:.1f}, max:{pre_submit['max']:.1f})") - if post_submit['avg'] > 0: - print(f" GPU提交后时间: {post_submit['avg']:.2f}ms (min:{post_submit['min']:.1f}, max:{post_submit['max']:.1f})") - if compositor['avg'] > 0: - print(f" GPU合成器时间: {compositor['avg']:.2f}ms (min:{compositor['min']:.1f}, max:{compositor['max']:.1f})") - - # 显示当前帧GPU时间 - print(f"🔍 当前帧GPU时间:") - if gpu_current.get('gpu_scene_render', 0) > 0: - print(f" 场景渲染: {gpu_current['gpu_scene_render']:.2f}ms") - if gpu_current.get('gpu_total_render', 0) > 0: - print(f" 总渲染: {gpu_current['gpu_total_render']:.2f}ms") - if gpu_current.get('gpu_compositor_render', 0) > 0: - print(f" 合成器: {gpu_current['gpu_compositor_render']:.2f}ms") - - # GPU时间瓶颈分析 - current_total = gpu_current.get('gpu_total_render', 0) - current_scene = gpu_current.get('gpu_scene_render', 0) - if current_total > 12.0: # 假设72fps目标,留出一些余量 - print(f" ⚠️ GPU总渲染时间过长: {current_total:.1f}ms") - elif current_scene > 8.0: - print(f" ⚠️ GPU场景渲染时间偏高: {current_scene:.1f}ms") - - else: - print(f" GPU渲染时间: 暂无数据") - if self.gpu_timing_failure_count > 0: - print(f" 获取失败次数: {self.gpu_timing_failure_count}") - else: - print(f" 等待OpenVR Frame Timing数据...") - else: - print(f"⚡ GPU渲染时间: 已禁用") - - # VR特定指标 - print(f"🥽 VR指标:") - print(f" 总帧数: {stats['frame_count']}") - print(f" 提交失败: {stats['submit_failures']}") - print(f" 姿态失败: {stats['pose_failures']}") - - # 计算失败率 - if stats['frame_count'] > 0: - submit_fail_rate = (stats['submit_failures'] / stats['frame_count']) * 100 - pose_fail_rate = (stats['pose_failures'] / stats['frame_count']) * 100 - print(f" 提交失败率: {submit_fail_rate:.2f}%") - print(f" 姿态失败率: {pose_fail_rate:.2f}%") - - # 渲染管线监控 - if self.enable_pipeline_monitoring: - pipeline_stats = self._get_pipeline_stats() - print(f"⚙️ 渲染管线分析:") - print(f" 当前分辨率: {pipeline_stats['vr_info']['eye_resolution'][0]}x{pipeline_stats['vr_info']['eye_resolution'][1]}") - print(f" 推荐分辨率: {pipeline_stats['vr_info']['recommended_resolution'][0]}x{pipeline_stats['vr_info']['recommended_resolution'][1]}") - print(f" 显示频率: {pipeline_stats['vr_info']['display_frequency']:.1f} Hz") - - # VSync和时序信息 - if pipeline_stats['vr_info']['target_frame_time_ms'] > 0: - print(f"🎯 VSync时序:") - print(f" 目标帧时间: {pipeline_stats['vr_info']['target_frame_time_ms']:.2f}ms") - print(f" VSync到光子: {pipeline_stats['vr_info']['vsync_to_photons_ms']:.2f}ms") - print(f" VSync窗口: ±{pipeline_stats['vr_info']['vsync_window_ms']:.2f}ms") - print(f" 异步重投影: {'启用' if pipeline_stats['vr_info']['async_reprojection'] else '禁用'}") - print(f" 运动平滑: {'启用' if pipeline_stats['vr_info']['motion_smoothing'] else '禁用'}") - - # 分析帧时间是否在目标范围内 - current_frame_time = stats['frame_time_avg'] - target_frame_time = pipeline_stats['vr_info']['target_frame_time_ms'] - if current_frame_time > 0 and target_frame_time > 0: - frame_time_ratio = current_frame_time / target_frame_time - if frame_time_ratio > 1.1: - print(f" ⚠️ 帧时间超标: {current_frame_time:.1f}ms (目标:{target_frame_time:.1f}ms)") - elif frame_time_ratio < 0.9: - print(f" ✅ 帧时间充裕: {current_frame_time:.1f}ms (目标:{target_frame_time:.1f}ms)") - else: - print(f" ✓ 帧时间正常: {current_frame_time:.1f}ms (目标:{target_frame_time:.1f}ms)") - - # 🔧 性能优化诊断 - print(f"🔧 优化诊断:") - current = pipeline_stats.get('current', {}) if self.enable_pipeline_monitoring else {} - - # 🔍 渲染回调诊断 - 新增 - self._print_render_callback_diagnostics() - - # waitGetPoses时序分析 - wait_poses_time = current.get('wait_poses', 0) - if wait_poses_time > 10: - print(f" ⚠️ waitGetPoses时间过长: {wait_poses_time:.1f}ms") - print(f" 可能原因: 错过VSync窗口,建议调整预测时间") - elif wait_poses_time > 5: - print(f" ⚠️ waitGetPoses时间偏高: {wait_poses_time:.1f}ms") - else: - print(f" ✓ waitGetPoses时间正常: {wait_poses_time:.1f}ms") - - # CPU-GPU并行度分析 - 增强诊断 - gpu_total = current.get('gpu_total_render', 0) - cpu_render_left = getattr(self, 'left_render_time', 0) - cpu_render_right = getattr(self, 'right_render_time', 0) - cpu_render_total = cpu_render_left + cpu_render_right - - print(f" 真实渲染时间对比:") - print(f" CPU左眼: {cpu_render_left:.2f}ms") - print(f" CPU右眼: {cpu_render_right:.2f}ms") - print(f" CPU总计: {cpu_render_total:.2f}ms") - print(f" GPU总计: {gpu_total:.2f}ms") - - if cpu_render_total < 1.0: - print(f" ⚠️ CPU渲染时间异常短: {cpu_render_total:.2f}ms") - print(f" 可能原因: 渲染回调未正确执行或渲染被跳过") - elif gpu_total > 0 and cpu_render_total > 0: - ratio = gpu_total / cpu_render_total - if ratio > 10: - print(f" ⚠️ GPU严重等待CPU: 比例{ratio:.1f}:1") - print(f" 建议: 检查OpenGL命令提交时机和渲染状态") - elif ratio > 3: - print(f" ⚠️ GPU等待CPU: 比例{ratio:.1f}:1") - print(f" 建议: 检查OpenGL命令提交是否及时") - else: - print(f" ✓ CPU-GPU时间匹配: 比例{ratio:.1f}:1") - - # 预测时间诊断 - current_prediction = self.use_prediction_time * 1000 - print(f" 预测时间设置: {current_prediction:.1f}ms") - if current_prediction > 15: - print(f" 建议: 预测时间较高,可能增加waitGetPoses延迟") - elif current_prediction < 8: - print(f" 注意: 预测时间较低,可能影响姿态准确性") - else: - print(f" ✓ 预测时间在合理范围内") - - # 优化状态总结 - optimization_score = 0 - if wait_poses_time < 8: - optimization_score += 1 - if stats['vr_fps'] > 60: - optimization_score += 1 - if stats.get('frame_time_avg', 0) < target_frame_time * 1.1: - optimization_score += 1 - - if optimization_score >= 2: - print(f" ✅ 优化效果良好 ({optimization_score}/3)") - else: - print(f" ⚠️ 仍有优化空间 ({optimization_score}/3)") - - print(f"🕐 各阶段耗时 (最近{self.pipeline_history_size}帧平均):") - print(f" waitGetPoses: {pipeline_stats['wait_poses']['avg']:.2f}ms (min:{pipeline_stats['wait_poses']['min']:.1f}, max:{pipeline_stats['wait_poses']['max']:.1f})") - print(f" 渲染总计: {pipeline_stats['render']['avg']:.2f}ms (min:{pipeline_stats['render']['min']:.1f}, max:{pipeline_stats['render']['max']:.1f})") - print(f" 纹理提交: {pipeline_stats['submit']['avg']:.2f}ms (min:{pipeline_stats['submit']['min']:.1f}, max:{pipeline_stats['submit']['max']:.1f})") - if pipeline_stats['sync_wait']['avg'] > 0: - print(f" 同步等待: {pipeline_stats['sync_wait']['avg']:.2f}ms (min:{pipeline_stats['sync_wait']['min']:.1f}, max:{pipeline_stats['sync_wait']['max']:.1f})") - - print(f"🔍 当前帧详情:") - print(f" 左眼渲染: {pipeline_stats['current']['left_render']:.2f}ms") - print(f" 右眼渲染: {pipeline_stats['current']['right_render']:.2f}ms") - print(f" 姿态获取: {pipeline_stats['current']['wait_poses']:.2f}ms") - - # 显示Running Start模式信息 - print(f"🎯 Running Start模式:") - print(f" 模式: Valve Running Start - 帧开始时获取姿态") - print(f" 优势: VSync前3ms获取姿态,提供充足渲染时间") - print(f" 预测时间: {self.use_prediction_time * 1000:.1f}ms") - - # 分析最大瓶颈 - current = pipeline_stats['current'] - bottleneck_analysis = [] - if current['wait_poses'] > 5.0: - bottleneck_analysis.append(f"姿态获取耗时过长({current['wait_poses']:.1f}ms)") - if current['total_render'] > 8.0: - bottleneck_analysis.append(f"渲染耗时过长({current['total_render']:.1f}ms)") - if current['submit'] > 2.0: - bottleneck_analysis.append(f"纹理提交耗时过长({current['submit']:.1f}ms)") - - if bottleneck_analysis: - print(f"🚨 瓶颈分析:") - for analysis in bottleneck_analysis: - print(f" ⚠️ {analysis}") - - # 性能建议 - self._print_performance_recommendations(stats) - - print("===============================") - - def _print_performance_recommendations(self, stats): - """根据性能数据输出优化建议""" - print(f"💡 性能建议:") - - recommendations = [] - - # FPS相关建议 - if stats['vr_fps'] < 60: - recommendations.append(" ⚠️ VR帧率过低,可能影响体验") - - # 帧时间相关建议 - if stats['frame_time_avg'] > 16.7: # 60fps = 16.7ms - recommendations.append(" ⚠️ 平均帧时间过高,建议降低渲染质量") - - if stats['frame_time_max'] > 50: - recommendations.append(" ⚠️ 检测到严重卡顿,检查CPU/GPU负载") - - # CPU建议 - if stats['cpu_usage'] > 80: - recommendations.append(" 🔴 CPU使用率过高,可能存在CPU瓶颈") - elif stats['cpu_usage'] > 60: - recommendations.append(" 🟡 CPU使用率偏高,注意监控") - - # 内存建议 - if stats['memory_usage'] > 85: - recommendations.append(" 🔴 内存使用率过高,可能影响性能") - - # GPU建议 - if self.gputil_available or self.nvidia_ml_available: - if stats['gpu_usage'] > 95: - recommendations.append(" 🔴 GPU使用率接近满载,存在GPU瓶颈") - if stats['gpu_memory_usage'] > 90: - recommendations.append(" 🔴 显存使用率过高,可能需要降低纹理质量") - - # GPU渲染时间建议 - if self.enable_gpu_timing: - if self.gpu_total_render_ms > 12.0: - recommendations.append(" ⚠️ GPU总渲染时间过长,建议优化场景复杂度") - if self.gpu_scene_render_ms > 8.0: - recommendations.append(" ⚠️ GPU场景渲染时间偏高,考虑降低渲染质量") - if self.gpu_compositor_render_ms > 3.0: - recommendations.append(" ⚠️ GPU合成器时间过长,检查VR设置或叠加层") - if self.gpu_timing_failure_count > 100: - recommendations.append(" ⚠️ GPU时间统计频繁失败,可能需要更新OpenVR") - - # 失败率建议 - submit_fail_rate = (stats['submit_failures'] / max(stats['frame_count'], 1)) * 100 - if submit_fail_rate > 1: - recommendations.append(" ⚠️ VR帧提交失败率较高,检查VR系统状态") - - if not recommendations: - recommendations.append(" ✅ 性能表现良好,无明显问题") - - for rec in recommendations: - print(rec) - - def _print_brief_performance_report(self, stats): - """输出简短的VR性能报告""" - # 创建一行简短摘要 - summary = f"🥽 VR性能: {stats['vr_fps']:.1f}fps" - - if stats['frame_time_avg'] > 0: - summary += f" | 帧时间: {stats['frame_time_avg']:.1f}ms" - - if self.psutil_available: - summary += f" | CPU: {stats['cpu_usage']:.0f}%" - summary += f" | 内存: {stats['memory_usage']:.0f}%" - - # 显示GPU信息,如果库不可用则显示提示 - if self.gputil_available or self.nvidia_ml_available: - summary += f" | GPU: {stats['gpu_usage']:.0f}%" - else: - summary += " | GPU: N/A" - - # 添加失败率指示 - if stats['frame_count'] > 0: - submit_fail_rate = (stats['submit_failures'] / stats['frame_count']) * 100 - if submit_fail_rate > 0.1: - summary += f" | 提交失败: {submit_fail_rate:.1f}%" - - # 添加管线关键信息 - if self.enable_pipeline_monitoring: - pipeline_stats = self._get_pipeline_stats() - current = pipeline_stats['current'] - - # 显示关键瓶颈 - if current['wait_poses'] > 5.0: - summary += f" | 姿态: {current['wait_poses']:.1f}ms⚠️" - elif current['wait_poses'] > 0: - summary += f" | 姿态: {current['wait_poses']:.1f}ms" - - if current['total_render'] > 8.0: - summary += f" | 渲染: {current['total_render']:.1f}ms⚠️" - elif current['total_render'] > 0: - summary += f" | 渲染: {current['total_render']:.1f}ms" - - # 添加GPU渲染时间信息 - if self.enable_gpu_timing: - gpu_total = current.get('gpu_total_render', 0) - gpu_scene = current.get('gpu_scene_render', 0) - if gpu_total > 12.0: - summary += f" | GPU: {gpu_total:.1f}ms⚠️" - elif gpu_total > 0: - summary += f" | GPU: {gpu_total:.1f}ms" - elif gpu_scene > 0: - summary += f" | GPU场景: {gpu_scene:.1f}ms" - - # 显示目标帧时间对比 - vr_info = pipeline_stats['vr_info'] - if vr_info['target_frame_time_ms'] > 0: - target = vr_info['target_frame_time_ms'] - current_avg = stats['frame_time_avg'] - if current_avg > target * 1.1: - summary += f" | 目标:{target:.0f}ms⚠️" - else: - summary += f" | 目标:{target:.0f}ms" - - # 性能状态指示 - if stats['vr_fps'] < 72: - summary += " ⚠️" - elif stats['vr_fps'] > 85: - summary += " ✅" - - print(summary) - - # 注意:原来的left_cb和right_cb函数已被删除 - # 它们的功能已集成到_handle_vr_rendering_and_submit方法中 - - # 注意:_safe_submit_texture方法已删除 - # VR纹理提交现在完全由Panda3D的renderFrame()自动处理 - - def submit_texture(self, eye, texture): - """优化的VR纹理提交 - 使用缓存的纹理ID,避免重复prepareNow""" - try: - if not self.vr_compositor: - print("❌ VR compositor不可用") - self.submit_failures += 1 - return - - # 🚀 关键修复:防止同一帧重复提交 - current_frame = getattr(self, 'frame_count', 0) - if not hasattr(self, '_last_submit_frame'): - self._last_submit_frame = {} - - # 检查当前帧是否已经提交过此眼睛的纹理 - if eye in self._last_submit_frame and self._last_submit_frame[eye] == current_frame: - # 静默跳过,避免spam输出 - return - - # 记录当前帧提交 - self._last_submit_frame[eye] = current_frame - - # 🚀 关键优化:直接使用缓存的纹理ID,避免重复prepareNow - if eye == openvr.Eye_Left: - handle = self.left_texture_id - eye_name = "左眼" - elif eye == openvr.Eye_Right: - handle = self.right_texture_id - eye_name = "右眼" - else: - print(f"❌ 未知的眼睛类型: {eye}") - self.submit_failures += 1 - return - - # 检查缓存的纹理ID是否有效 - if not handle or handle <= 0: - print(f"❌ {eye_name}纹理ID缓存无效: {handle}") - print(" 这可能表示纹理准备失败,需要检查_prepare_and_cache_textures()") - self.submit_failures += 1 - return - - # ❌ 移除gsg.flush()调用 - 基于OpenVR官方实践 - # gsg.flush()等同于OpenGL的glFlush(),导致强制CPU-GPU同步 - # 每帧调用2次(左右眼)是GPU周期性峰值的主要原因 - # 参考:OpenVR社区经验表明同步调用是性能杀手 - # - # gsg = self.world.win.getGsg() - # if gsg and hasattr(gsg, 'flush'): - # try: - # gsg.flush() - # except Exception as flush_error: - # if not hasattr(self, '_gsg_flush_error_logged'): - # print(f"⚠️ GSG刷新失败: {flush_error}") - # self._gsg_flush_error_logged = True - - # 🚀 关键优化:使用缓存的OpenVR Texture对象,避免每帧创建 - if eye == openvr.Eye_Left: - ovr_texture = self._left_ovr_texture - else: - ovr_texture = self._right_ovr_texture - - # 检查缓存对象是否存在(向后兼容) - if ovr_texture is None: - # 备用方案:如果缓存对象不存在,创建新的(性能较差) - ovr_texture = openvr.Texture_t() - ovr_texture.eType = openvr.TextureType_OpenGL - ovr_texture.eColorSpace = openvr.ColorSpace_Gamma - if not hasattr(self, '_texture_fallback_warned'): - print("⚠️ 使用Texture对象备用方案(性能次优)") - self._texture_fallback_warned = True - - # 只更新handle,其他属性已预设置 - ovr_texture.handle = handle - - # 提交到VR系统 - error = self.vr_compositor.submit(eye, ovr_texture) - - # 检查错误 - if error and error != openvr.VRCompositorError_None: - print(f"⚠️ VR{eye_name}纹理提交错误: {error}") - self.submit_failures += 1 - else: - # 只在第一次成功时输出 - if not hasattr(self, '_optimized_submit_success_logged'): - print(f"✅ 优化版VR纹理提交成功! 使用缓存ID,避免重复prepareNow") - print(f" {eye_name}纹理ID: {handle}") - self._optimized_submit_success_logged = True - - # 🔧 智能GPU同步策略 - 减少不必要的flush调用 - # 只有在检测到性能问题或每N帧时才强制flush - if eye == openvr.Eye_Right: - self._smart_gpu_sync() - - except Exception as e: - print(f"❌ VR纹理提交异常: {e}") - import traceback - traceback.print_exc() - self.submit_failures += 1 - - def _smart_gpu_sync(self): - """智能GPU同步策略 - 只在必要时进行同步""" - try: - # 初始化同步控制变量 - if not hasattr(self, '_last_gpu_sync_frame'): - self._last_gpu_sync_frame = 0 - self._gpu_sync_interval = 60 # 每60帧强制同步一次(1秒@60FPS) - self._performance_based_sync = True - - current_frame = getattr(self, 'frame_count', 0) - - # 策略1: 性能自适应同步 - if self._performance_based_sync: - # 如果渲染时间过长,增加同步频率 - total_render_time = getattr(self, 'left_render_time', 0) + getattr(self, 'right_render_time', 0) - - if total_render_time > 15.0: # 如果总渲染时间超过15ms - # 高负载时更频繁同步 - sync_interval = 30 # 每30帧同步 - elif total_render_time > 10.0: - sync_interval = 45 # 中等负载 - else: - sync_interval = self._gpu_sync_interval # 正常负载 - - should_sync = (current_frame - self._last_gpu_sync_frame) >= sync_interval - else: - # 策略2: 固定间隔同步 - should_sync = (current_frame - self._last_gpu_sync_frame) >= self._gpu_sync_interval - - # 执行同步 - if should_sync: - gsg = self.world.win.getGsg() - if gsg and hasattr(gsg, 'flush'): - try: - gsg.flush() - self._last_gpu_sync_frame = current_frame - - # 只在首次或Debug时输出 - if not hasattr(self, '_smart_sync_logged') or self.debug_output_enabled: - if not hasattr(self, '_smart_sync_logged'): - print(f"🔧 智能GPU同步已启用 - 间隔:{sync_interval}帧") - self._smart_sync_logged = True - elif self.debug_output_enabled and current_frame % 600 == 1: - print(f"🔧 智能同步触发 (帧#{current_frame}, 间隔:{sync_interval})") - - except Exception as flush_error: - if not hasattr(self, '_smart_sync_error_logged'): - print(f"⚠️ 智能GPU同步失败: {flush_error}") - self._smart_sync_error_logged = True - - except Exception as e: - print(f"⚠️ 智能GPU同步策略异常: {e}") - - def _disable_main_cam(self): - """禁用主相机 - 基于参考实现""" - try: - # 保存原始相机状态 - if not hasattr(self, '_original_camera_parent'): - self._original_camera_parent = self.world.camera.getParent() - - # 创建空节点并将主相机重新附加到它 - self._empty_world = NodePath("empty_world") - self.world.camera.reparentTo(self._empty_world) - - print("✓ 主相机已禁用") - except Exception as e: - print(f"⚠️ 禁用主相机失败: {e}") - - def _enable_main_cam(self): - """恢复主相机 - 基于参考实现""" - try: - # 恢复原始相机状态 - if hasattr(self, '_original_camera_parent') and self._original_camera_parent: - self.world.camera.reparentTo(self._original_camera_parent) - else: - # 如果没有保存的父节点,重新附加到render - self.world.camera.reparentTo(self.world.render) - - # 清理空世界节点 - if hasattr(self, '_empty_world'): - self._empty_world.removeNode() - delattr(self, '_empty_world') - - print("✓ 主相机已恢复") - except Exception as e: - print(f"⚠️ 恢复主相机失败: {e}") - - def _initialize_controllers(self): - """初始化VR手柄控制器""" - try: - print("🎮 正在初始化VR手柄控制器...") - - # 创建左右手柄控制器实例 - self.left_controller = LeftController(self) - self.right_controller = RightController(self) - - # 检测现有连接的控制器 - self._detect_controllers() - - print("✓ VR手柄控制器初始化完成") - - except Exception as e: - print(f"⚠️ VR手柄初始化失败: {e}") - import traceback - traceback.print_exc() - - def _detect_controllers(self): - """检测并连接VR控制器""" - if not self.vr_system: - return - - try: - for device_index in range(openvr.k_unMaxTrackedDeviceCount): - # 检查设备是否已连接 - if not self.vr_system.isTrackedDeviceConnected(device_index): - continue - - # 获取设备类型 - device_class = self.vr_system.getTrackedDeviceClass(device_index) - - if device_class == openvr.TrackedDeviceClass_Controller: - # 获取控制器角色 - role = self.vr_system.getControllerRoleForTrackedDeviceIndex(device_index) - - if role == openvr.TrackedControllerRole_LeftHand and self.left_controller: - self.left_controller.set_device_index(device_index) - self.controllers[device_index] = self.left_controller - # 为设备创建锚点 - self._create_tracked_device_anchor(device_index, 'left_controller') - - elif role == openvr.TrackedControllerRole_RightHand and self.right_controller: - self.right_controller.set_device_index(device_index) - self.controllers[device_index] = self.right_controller - # 为设备创建锚点 - self._create_tracked_device_anchor(device_index, 'right_controller') - - print(f"🎮 检测到 {len(self.controllers)} 个控制器") - - except Exception as e: - print(f"⚠️ 控制器检测失败: {e}") - - def _create_tracked_device_anchor(self, device_index, name): - """为跟踪设备创建锚点节点""" - if not self.tracking_space: - print(f"⚠️ 无法为设备 {device_index} 创建锚点 - tracking_space未初始化") - return - - try: - # 获取设备模型名称 - if self.vr_system: - model_name = self.vr_system.getStringTrackedDeviceProperty( - device_index, - openvr.Prop_RenderModelName_String - ) - anchor_name = f"{device_index}:{model_name}:{name}" - else: - anchor_name = f"{device_index}:{name}" - - # 创建锚点节点 - device_anchor = self.tracking_space.attachNewNode(anchor_name) - self.tracked_device_anchors[device_index] = device_anchor - - print(f"✓ 为设备 {device_index} 创建锚点: {anchor_name}") - - except Exception as e: - print(f"⚠️ 创建设备锚点失败: {e}") - - def update_tracked_devices(self): - """更新所有跟踪设备的姿态 - 基于参考实现""" - if not self.poses or not self.vr_system: - return - - try: - # 更新每个已连接的控制器 - for device_index, controller in self.controllers.items(): - if device_index < len(self.poses): - pose_data = self.poses[device_index] - - # 更新控制器姿态 - controller.update_pose(pose_data) - - # 更新控制器输入状态 - controller.update_input_state(self.vr_system) - - # 更新其他跟踪设备的锚点 - for device_index in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): - if device_index in self.tracked_device_anchors: - pose_data = self.poses[device_index] - if pose_data.bPoseIsValid: - # 转换姿态矩阵 - modelview = self.convert_mat(pose_data.mDeviceToAbsoluteTracking) - final_matrix = self.coord_mat_inv * modelview * self.coord_mat - - # 更新锚点变换 - anchor = self.tracked_device_anchors[device_index] - anchor.setMat(final_matrix) - anchor.show() - else: - # 姿态无效,隐藏锚点 - self.tracked_device_anchors[device_index].hide() - - except Exception as e: - if self.frame_count % 300 == 0: # 每5秒输出一次错误 - print(f"⚠️ 更新跟踪设备失败: {e}") - - def get_controller_by_role(self, role): - """根据角色获取控制器 - - Args: - role: 'left' 或 'right' - - Returns: - VRController实例或None - """ - if role == 'left': - return self.left_controller - elif role == 'right': - return self.right_controller - return None - - def are_controllers_connected(self): - """检查是否有控制器连接""" - return len(self.controllers) > 0 - - def get_connected_controllers(self): - """获取所有连接的控制器列表""" - return list(self.controllers.values()) - - def trigger_controller_haptic(self, role, duration=0.001, strength=1.0): - """触发控制器震动反馈 - - Args: - role: 'left', 'right' 或 'both' - duration: 震动持续时间(秒) - strength: 震动强度 (0.0-1.0) - """ - if role in ['left', 'both'] and self.left_controller: - self.left_controller.trigger_haptic_feedback(duration, strength) - - if role in ['right', 'both'] and self.right_controller: - self.right_controller.trigger_haptic_feedback(duration, strength) - - # VR动作系统便捷方法 - def is_trigger_pressed(self, hand='any'): - """检查扳机是否被按下 - - Args: - hand: 'left', 'right', 'any' - """ - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - pressed, _ = self.action_manager.is_digital_action_pressed('trigger', device_path) - return pressed - - def is_trigger_just_pressed(self, hand='any'): - """检查扳机是否刚刚被按下""" - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - pressed, _ = self.action_manager.is_digital_action_just_pressed('trigger', device_path) - return pressed - - def is_grip_pressed(self, hand='any'): - """检查握把是否被按下""" - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - pressed, _ = self.action_manager.is_digital_action_pressed('grip', device_path) - return pressed - - def is_grip_just_pressed(self, hand='any'): - """检查握把是否刚刚被按下""" - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - pressed, _ = self.action_manager.is_digital_action_just_pressed('grip', device_path) - return pressed - - def is_menu_pressed(self, hand='any'): - """检查菜单按钮是否被按下""" - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - pressed, _ = self.action_manager.is_digital_action_pressed('menu', device_path) - return pressed - - def is_trackpad_touched(self, hand='any'): - """检查触摸板是否被触摸""" - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - touched, _ = self.action_manager.is_digital_action_pressed('trackpad_touch', device_path) - return touched - - def get_trackpad_position(self, hand='any'): - """获取触摸板位置 - - Returns: - Vec2或None: 触摸板位置 (-1到1的范围) - """ - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - value, _ = self.action_manager.get_analog_action_value('trackpad', device_path) - return value - - # VR交互系统便捷方法 - def get_selected_object(self, hand='any'): - """获取指定手选中的对象 - - Args: - hand: 'left', 'right', 'any' - - Returns: - 选中的对象节点或None - """ - if hand == 'any': - # 返回任意手选中的对象 - for controller in self.get_connected_controllers(): - selected = self.interaction_manager.get_selected_object(controller.name) - if selected: - return selected - return None - else: - return self.interaction_manager.get_selected_object(hand) - - def get_grabbed_object(self, hand='any'): - """获取指定手抓取的对象 - - Args: - hand: 'left', 'right', 'any' - - Returns: - 抓取的对象节点或None - """ - if hand == 'any': - # 返回任意手抓取的对象 - for controller in self.get_connected_controllers(): - grabbed = self.interaction_manager.get_grabbed_object(controller.name) - if grabbed: - return grabbed - return None - else: - return self.interaction_manager.get_grabbed_object(hand) - - def is_grabbing_object(self, hand='any'): - """检查是否正在抓取对象 - - Args: - hand: 'left', 'right', 'any' - - Returns: - bool: 是否正在抓取 - """ - if hand == 'any': - # 检查任意手是否正在抓取 - for controller in self.get_connected_controllers(): - if self.interaction_manager.is_grabbing(controller.name): - return True - return False - else: - return self.interaction_manager.is_grabbing(hand) - - def force_release_all_grabs(self): - """强制释放所有抓取的对象""" - self.interaction_manager.force_release_all() - - def add_interactable_object(self, object_node): - """将对象标记为可交互 - - Args: - object_node: 要标记的对象节点 - """ - self.interaction_manager._add_collision_to_object(object_node) - - def _disable_async_reprojection(self): - """禁用异步重投影 - 备选修复方案""" - try: - # 尝试通过OpenVR设置禁用异步重投影 - if hasattr(openvr, 'VRSettings'): - settings = openvr.VRSettings() - if settings: - # 禁用异步重投影 - error = settings.setBool("steamvr", "enableAsyncReprojection", False) - if error == openvr.VRSettingsError_None: - print("✅ 异步重投影已禁用") - else: - print(f"⚠️ 禁用异步重投影失败: 设置错误 {error}") - else: - print("⚠️ 无法获取VR设置接口") - else: - print("⚠️ OpenVR设置接口不可用") - - except Exception as e: - print(f"⚠️ 禁用异步重投影失败: {e}") - - def enable_async_reprojection_disable(self): - """启用异步重投影禁用选项 - 用户可调用的方法""" - self.disable_async_reprojection = True - print("📝 异步重投影禁用选项已启用,将在下次VR初始化时生效") - - def disable_async_reprojection_disable(self): - """禁用异步重投影禁用选项 - 恢复默认行为""" - self.disable_async_reprojection = False - print("📝 异步重投影禁用选项已关闭,将使用默认ATW行为") - - def _start_timing(self, operation_name): - """开始计时操作 - 性能优化版本""" - if not self.enable_pipeline_monitoring or self.performance_mode_enabled: - return None - - import time - # 🚀 性能优化:减少字典创建,只在必要时创建 - return { - 'operation': operation_name, - 'start_time': time.perf_counter() - } - - def _end_timing(self, timing_data): - """结束计时并记录结果 - 性能优化版本""" - if not self.enable_pipeline_monitoring or self.performance_mode_enabled or not timing_data: - return 0.0 - - import time - elapsed = (time.perf_counter() - timing_data['start_time']) * 1000 # 转换为毫秒 - - # 保存到相应的历史记录 - operation = timing_data['operation'] - if operation == 'wait_poses': - self.wait_poses_time = elapsed - self.wait_poses_times.append(elapsed) - if len(self.wait_poses_times) > self.pipeline_history_size: - self.wait_poses_times.pop(0) - elif operation == 'left_render': - self.left_render_time = elapsed - elif operation == 'right_render': - self.right_render_time = elapsed - elif operation == 'submit': - self.submit_time = elapsed - self.submit_times.append(elapsed) - if len(self.submit_times) > self.pipeline_history_size: - self.submit_times.pop(0) - elif operation == 'sync_wait': - self.vr_sync_wait_time = elapsed - self.sync_wait_times.append(elapsed) - if len(self.sync_wait_times) > self.pipeline_history_size: - self.sync_wait_times.pop(0) - - # 计算总渲染时间 - total_render = self.left_render_time + self.right_render_time - self.render_times.append(total_render) - if len(self.render_times) > self.pipeline_history_size: - self.render_times.pop(0) - - return elapsed - - def _get_pipeline_stats(self): - """获取渲染管线统计信息""" - def get_stats(times_list): - if not times_list: - return {'avg': 0.0, 'min': 0.0, 'max': 0.0} - return { - 'avg': sum(times_list) / len(times_list), - 'min': min(times_list), - 'max': max(times_list) - } - - # 计算GPU时间统计 - def get_gpu_field_stats(field_name): - """从GPU时间历史记录中提取特定字段的统计信息""" - values = [] - for gpu_data in self.gpu_timing_history: - if field_name in gpu_data: - values.append(gpu_data[field_name]) - return get_stats(values) - - return { - 'wait_poses': get_stats(self.wait_poses_times), - 'render': get_stats(self.render_times), - 'submit': get_stats(self.submit_times), - 'sync_wait': get_stats(self.sync_wait_times), - 'gpu_timing': { - 'scene_render': get_gpu_field_stats('scene_render'), - 'pre_submit': get_gpu_field_stats('pre_submit'), - 'post_submit': get_gpu_field_stats('post_submit'), - 'total_render': get_gpu_field_stats('total_render'), - 'compositor_render': get_gpu_field_stats('compositor_render'), - 'frame_interval': get_gpu_field_stats('frame_interval') - }, - 'current': { - 'wait_poses': self.wait_poses_time, - 'left_render': self.left_render_time, - 'right_render': self.right_render_time, - 'submit': self.submit_time, - 'sync_wait': self.vr_sync_wait_time, - 'total_render': self.left_render_time + self.right_render_time, - 'gpu_scene_render': self.gpu_scene_render_ms, - 'gpu_pre_submit': self.gpu_pre_submit_ms, - 'gpu_post_submit': self.gpu_post_submit_ms, - 'gpu_total_render': self.gpu_total_render_ms, - 'gpu_compositor_render': self.gpu_compositor_render_ms, - 'gpu_frame_interval': self.gpu_client_frame_interval_ms - }, - 'vr_info': { - 'eye_resolution': self.current_eye_resolution, - 'recommended_resolution': self.recommended_eye_resolution, - 'display_frequency': self.vr_display_frequency, - 'vsync_enabled': self.vr_vsync_enabled, - 'target_frame_time_ms': self.target_frame_time_ms, - 'vsync_to_photons_ms': self.vsync_to_photons_ms, - 'vsync_window_ms': self.vsync_window_ms, - 'async_reprojection': self.async_reprojection_enabled, - 'motion_smoothing': self.motion_smoothing_enabled, - 'gpu_timing_enabled': self.enable_gpu_timing, - 'gpu_timing_failures': self.gpu_timing_failure_count - } - } - - - def set_prediction_time(self, prediction_time_ms): - """设置预测时间(仅用于update_task策略) - - Args: - prediction_time_ms: 预测时间,单位毫秒(通常8-16ms) - """ - prediction_time_s = prediction_time_ms / 1000.0 - if prediction_time_s < 0.005 or prediction_time_s > 0.020: - print(f"⚠️ 预测时间超出推荐范围(5-20ms): {prediction_time_ms}ms") - - old_time = self.use_prediction_time * 1000 - self.use_prediction_time = prediction_time_s - print(f"✓ VR预测时间已设置: {old_time:.1f}ms → {prediction_time_ms:.1f}ms") - - - def test_pipeline_monitoring(self): - """测试管线监控功能 - 用于调试和验证""" - print("🔧 正在测试VR管线监控功能...") - - try: - # 测试基本信息获取 - print("📊 基本信息:") - print(f" 当前分辨率: {self.current_eye_resolution}") - print(f" 显示频率: {self.vr_display_frequency} Hz") - print(f" 目标帧时间: {self.target_frame_time_ms:.2f}ms") - - # Running Start模式信息 - print("🎯 Running Start模式:") - print(f" 预测时间: {self.use_prediction_time * 1000:.1f}ms") - print(f" 模式: Valve Running Start - 帧开始时获取姿态") - - # 测试管线统计 - if self.enable_pipeline_monitoring: - pipeline_stats = self._get_pipeline_stats() - print("⚙️ 管线统计:") - print(f" waitGetPoses历史: {len(self.wait_poses_times)}帧") - print(f" 渲染历史: {len(self.render_times)}帧") - print(f" 提交历史: {len(self.submit_times)}帧") - - # 测试性能报告 - print("📋 生成详细性能报告:") - self._print_performance_report() - - print("✅ 管线监控功能测试完成") - else: - print("⚠️ 管线监控已禁用,无法测试统计功能") - - except Exception as e: - print(f"❌ 测试管线监控功能时发生错误: {e}") - import traceback - traceback.print_exc() - - def _init_performance_monitoring(self): - """初始化性能监控库""" - self.psutil_available = False - self.gputil_available = False - self.nvidia_ml_available = False - - try: - import psutil - self.psutil = psutil - self.psutil_available = True - print("✓ psutil性能监控库已加载") - except ImportError: - print("⚠️ psutil库未安装,CPU和内存监控将不可用") - - try: - import GPUtil - self.gputil = GPUtil - self.gputil_available = True - print("✓ GPUtil GPU监控库已加载") - except ImportError: - print("⚠️ GPUtil库未安装,GPU监控将不可用") - - try: - import pynvml - self.pynvml = pynvml - pynvml.nvmlInit() - self.nvidia_ml_available = True - print("✓ NVIDIA-ML GPU监控库已加载") - except ImportError: - print("⚠️ pynvml库未安装,NVIDIA GPU详细监控将不可用") - except Exception as e: - print(f"⚠️ NVIDIA-ML初始化失败: {e}") - - def _get_gpu_frame_timing(self, frames_ago=0): - """获取GPU渲染时间统计 - - Args: - frames_ago: 获取多少帧之前的数据,0表示当前帧 - - Returns: - dict: GPU时间数据,如果获取失败返回None - """ - if not self.enable_gpu_timing or not self.vr_compositor: - return None - - try: - # 调用OpenVR的getFrameTiming API - result, timing = self.vr_compositor.getFrameTiming(framesAgo=frames_ago) - - if not result: - self.gpu_timing_failure_count += 1 - if self.gpu_timing_failure_count % 300 == 1: # 每5秒输出一次错误 - print("⚠️ OpenVR getFrameTiming调用失败") - return None - - # 提取GPU时间数据(单位:毫秒) - gpu_data = {} - - # 检查timing对象是否有GPU时间相关的属性 - if hasattr(timing, 'm_flSceneRenderGpuMs'): - gpu_data['scene_render'] = timing.m_flSceneRenderGpuMs - self.gpu_scene_render_ms = timing.m_flSceneRenderGpuMs - - if hasattr(timing, 'm_flPreSubmitGpuMs'): - gpu_data['pre_submit'] = timing.m_flPreSubmitGpuMs - self.gpu_pre_submit_ms = timing.m_flPreSubmitGpuMs - - if hasattr(timing, 'm_flPostSubmitGpuMs'): - gpu_data['post_submit'] = timing.m_flPostSubmitGpuMs - self.gpu_post_submit_ms = timing.m_flPostSubmitGpuMs - - if hasattr(timing, 'm_flTotalRenderGpuMs'): - gpu_data['total_render'] = timing.m_flTotalRenderGpuMs - self.gpu_total_render_ms = timing.m_flTotalRenderGpuMs - - if hasattr(timing, 'm_flCompositorRenderGpuMs'): - gpu_data['compositor_render'] = timing.m_flCompositorRenderGpuMs - self.gpu_compositor_render_ms = timing.m_flCompositorRenderGpuMs - - if hasattr(timing, 'm_flClientFrameIntervalMs'): - gpu_data['frame_interval'] = timing.m_flClientFrameIntervalMs - self.gpu_client_frame_interval_ms = timing.m_flClientFrameIntervalMs - - # 将GPU时间数据添加到历史记录 - if gpu_data: - self.gpu_timing_history.append(gpu_data) - if len(self.gpu_timing_history) > self.gpu_timing_history_size: - self.gpu_timing_history.pop(0) - - # 调试信息 - 仅在第一次成功时输出 - if not hasattr(self, '_gpu_timing_success_logged'): - available_fields = list(gpu_data.keys()) - print(f"✅ GPU时间统计已启用 - 可用字段: {available_fields}") - self._gpu_timing_success_logged = True - - return gpu_data - - except AttributeError as e: - # OpenVR Python绑定可能不包含某些字段 - if self.gpu_timing_failure_count == 0: - print(f"⚠️ GPU时间统计部分功能不可用: {e}") - print(" 这可能是由于OpenVR Python绑定版本问题") - self.gpu_timing_failure_count += 1 - return None - - except Exception as e: - self.gpu_timing_failure_count += 1 - if self.gpu_timing_failure_count % 300 == 1: # 每5秒输出一次错误 - print(f"⚠️ 获取GPU时间统计失败: {e}") - return None - - def _update_performance_metrics(self): - """更新系统性能指标""" - if not self.performance_monitoring: - return - - import time - current_time = time.time() - - # 限制更新频率 - if current_time - self.last_performance_check < self.performance_check_interval: - return - - self.last_performance_check = current_time - - try: - # 更新CPU和内存使用率 - if self.psutil_available: - self.cpu_usage = self.psutil.cpu_percent(interval=None) - memory = self.psutil.virtual_memory() - self.memory_usage = memory.percent - - # 更新GPU使用率 - self._update_gpu_metrics() - - except Exception as e: - if self.frame_count % 600 == 0: # 每10秒输出一次错误 - print(f"⚠️ 性能监控更新失败: {e}") - - def _update_gpu_metrics(self): - """更新GPU相关指标""" - try: - # 方法1: 使用GPUtil - if self.gputil_available: - gpus = self.gputil.getGPUs() - if gpus: - gpu = gpus[0] # 使用第一个GPU - self.gpu_usage = gpu.load * 100 - self.gpu_memory_usage = gpu.memoryUtil * 100 - - # 方法2: 使用NVIDIA-ML (更精确) - elif self.nvidia_ml_available: - try: - handle = self.pynvml.nvmlDeviceGetHandleByIndex(0) - - # GPU使用率 - utilization = self.pynvml.nvmlDeviceGetUtilizationRates(handle) - self.gpu_usage = utilization.gpu - - # GPU内存使用率 - memory_info = self.pynvml.nvmlDeviceGetMemoryInfo(handle) - self.gpu_memory_usage = (memory_info.used / memory_info.total) * 100 - - except Exception as e: - # NVIDIA-ML可能无法在某些系统上工作 - pass - - except Exception as e: - # GPU监控失败,但不影响VR功能 - pass - - def _track_frame_time(self): - """记录帧时间 - 性能优化版本""" - import time - current_time = time.time() - - if hasattr(self, '_last_frame_time'): - frame_time = (current_time - self._last_frame_time) * 1000 # 转换为毫秒 - - # 🚀 性能优化:性能模式下跳过列表操作以减少内存分配 - if not self.performance_mode_enabled: - # 添加到帧时间历史 - self.frame_times.append(frame_time) - - # 限制历史长度 - if len(self.frame_times) > self.max_frame_time_history: - self.frame_times.pop(0) - - self._last_frame_time = current_time - - def get_performance_stats(self): - """获取详细的性能统计信息""" - stats = { - 'vr_fps': self.vr_fps, - 'frame_count': self.frame_count, - 'submit_failures': self.submit_failures, - 'pose_failures': self.pose_failures, - 'cpu_usage': self.cpu_usage, - 'memory_usage': self.memory_usage, - 'gpu_usage': self.gpu_usage, - 'gpu_memory_usage': self.gpu_memory_usage, - } - - # 计算帧时间统计 - if self.frame_times: - stats['frame_time_avg'] = sum(self.frame_times) / len(self.frame_times) - stats['frame_time_min'] = min(self.frame_times) - stats['frame_time_max'] = max(self.frame_times) - stats['frame_time_95th'] = sorted(self.frame_times)[int(len(self.frame_times) * 0.95)] - else: - stats['frame_time_avg'] = 0 - stats['frame_time_min'] = 0 - stats['frame_time_max'] = 0 - stats['frame_time_95th'] = 0 - - return stats - - def enable_performance_monitoring(self): - """启用性能监控""" - self.performance_monitoring = True - print("✓ VR性能监控已启用") - - def disable_performance_monitoring(self): - """禁用性能监控""" - self.performance_monitoring = False - print("✓ VR性能监控已禁用") - - def enable_gpu_timing_monitoring(self): - """启用GPU时间监控""" - self.enable_gpu_timing = True - print("✓ VR GPU时间监控已启用") - - def disable_gpu_timing_monitoring(self): - """禁用GPU时间监控""" - self.enable_gpu_timing = False - print("✓ VR GPU时间监控已禁用") - - def set_performance_check_interval(self, interval): - """设置性能检查间隔 - - Args: - interval: 检查间隔(秒),建议0.1-2.0之间 - """ - if 0.1 <= interval <= 5.0: - self.performance_check_interval = interval - print(f"✓ 性能监控间隔设置为 {interval:.1f} 秒") - else: - print("⚠️ 性能监控间隔应在0.1-5.0秒之间") - - def set_frame_time_history_size(self, size): - """设置帧时间历史记录大小 - - Args: - size: 历史记录大小(帧数),建议30-300之间 - """ - if 10 <= size <= 1000: - self.max_frame_time_history = size - # 清理超出的历史记录 - if len(self.frame_times) > size: - self.frame_times = self.frame_times[-size:] - print(f"✓ 帧时间历史记录大小设置为 {size} 帧") - else: - print("⚠️ 帧时间历史记录大小应在10-1000帧之间") - - def set_performance_report_interval(self, frames): - """设置性能报告输出间隔 - - Args: - frames: 帧数间隔,建议300-7200之间(5秒-2分钟@60fps) - """ - if 300 <= frames <= 7200: - # 修改_update_vr中的报告间隔 - print(f"✓ 性能报告间隔设置为每 {frames} 帧(约 {frames/60:.1f} 秒@60fps)") - # 这里可以添加一个实例变量来控制 - self.performance_report_interval = frames - else: - print("⚠️ 性能报告间隔应在300-7200帧之间") - - def get_performance_monitoring_config(self): - """获取当前性能监控配置""" - return { - 'enabled': self.performance_monitoring, - 'check_interval': self.performance_check_interval, - 'frame_history_size': self.max_frame_time_history, - 'report_interval': getattr(self, 'performance_report_interval', 1800), - 'psutil_available': self.psutil_available, - 'gputil_available': self.gputil_available, - 'nvidia_ml_available': self.nvidia_ml_available - } - - def print_performance_monitoring_status(self): - """输出性能监控状态""" - config = self.get_performance_monitoring_config() - - print("🔧 ===== VR性能监控配置 =====") - print(f" 监控状态: {'✅ 启用' if config['enabled'] else '❌ 禁用'}") - print(f" 检查间隔: {config['check_interval']:.1f} 秒") - print(f" 帧时间历史: {config['frame_history_size']} 帧") - print(f" 报告间隔: {config['report_interval']} 帧") - - print(f" 可用库:") - print(f" psutil (CPU/内存): {'✅' if config['psutil_available'] else '❌'}") - print(f" GPUtil (GPU): {'✅' if config['gputil_available'] else '❌'}") - print(f" NVIDIA-ML (GPU): {'✅' if config['nvidia_ml_available'] else '❌'}") - print("=============================") - - def force_performance_report(self): - """强制输出一次性能报告""" - print("🔄 手动触发性能报告...") - self._print_performance_report() - - def reset_performance_counters(self): - """重置性能计数器""" - self.frame_count = 0 - self.last_fps_check = 0 - self.last_fps_time = 0 - self.vr_fps = 0 - self.submit_failures = 0 - self.pose_failures = 0 - self.frame_times.clear() - print("✅ 性能计数器已重置") - - def get_current_performance_summary(self): - """获取当前性能摘要(简短版本)""" - stats = self.get_performance_stats() - - summary = f"VR性能: {stats['vr_fps']:.1f}fps" - - if stats['frame_time_avg'] > 0: - summary += f" | 帧时间: {stats['frame_time_avg']:.1f}ms" - - if self.psutil_available: - summary += f" | CPU: {stats['cpu_usage']:.0f}%" - summary += f" | 内存: {stats['memory_usage']:.0f}%" - - # 显示GPU信息,如果库不可用则显示提示 - if self.gputil_available or self.nvidia_ml_available: - summary += f" | GPU: {stats['gpu_usage']:.0f}%" - else: - summary += " | GPU: N/A" - - return summary - - def _print_render_callback_diagnostics(self): - """输出渲染回调诊断信息 - 包含优化效果分析""" - try: - print(f"🔍 DrawCallback渲染诊断:") - - # 回调次数统计 - left_count = self.left_render_count - right_count = self.right_render_count - print(f" 渲染次数: 左眼={left_count}, 右眼={right_count}") - - if left_count == 0 and right_count == 0: - print(f" ❌ VR缓冲区未被渲染 - 这是严重问题!") - return - - # 渲染次数平衡性检查 - if abs(left_count - right_count) > 5: - print(f" ⚠️ 左右眼渲染次数不平衡: 差异={abs(left_count - right_count)}") - else: - print(f" ✓ 左右眼渲染次数平衡") - - # 🔧 真实渲染时间分析 - 显示优化效果 - left_render_time = getattr(self, 'left_render_time', 0) - right_render_time = getattr(self, 'right_render_time', 0) - total_render_time = left_render_time + right_render_time - - print(f" ⏱️ 精确渲染时间测量:") - print(f" 左眼cbdata.upcall(): {left_render_time:.2f}ms") - print(f" 右眼cbdata.upcall(): {right_render_time:.2f}ms") - print(f" 总计: {total_render_time:.2f}ms") - - # 渲染性能评估 - if total_render_time > 16.0: # 超过60FPS时间 - print(f" 🔴 渲染时间过长: {total_render_time:.1f}ms (目标<13.9ms@72Hz)") - print(f" 建议: 检查RenderPipeline优化是否生效") - elif total_render_time > 10.0: - print(f" 🟡 渲染时间偏高: {total_render_time:.1f}ms (可接受)") - else: - print(f" 🟢 渲染性能良好: {total_render_time:.1f}ms") - - # 🔧 渲染管线优化状态检查 - self._check_rendering_optimizations() - - # OpenGL状态诊断 - self._diagnose_opengl_state() - - except Exception as e: - print(f" 渲染回调诊断失败: {e}") - - def _check_rendering_optimizations(self): - """检查渲染优化状态""" - try: - print(f" 🔧 渲染优化状态:") - - # 检查RenderPipeline优化 - if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline: - print(f" RenderPipeline: 已检测并优化") - else: - print(f" RenderPipeline: 未检测到(使用基础Panda3D)") - - # 检查GPU同步优化 - if hasattr(self, '_smart_sync_logged'): - last_sync_frame = getattr(self, '_last_gpu_sync_frame', 0) - current_frame = getattr(self, 'frame_count', 0) - frames_since_sync = current_frame - last_sync_frame - print(f" 智能GPU同步: 已启用 (距离上次同步: {frames_since_sync}帧)") - else: - print(f" 智能GPU同步: 未初始化") - - # 检查对象池状态 - matrix_pool_status = self.get_object_pool_status() - print(f" 对象池: Mat4={matrix_pool_status['matrix_pool_size']}/{matrix_pool_status['matrix_pool_capacity']}") - - # 检查垃圾回收控制 - gc_status = self.get_debug_status() - if gc_status['gc_disabled']: - print(f" GC控制: 已启用手动模式 (间隔:{gc_status['manual_gc_interval']}帧)") - else: - print(f" GC控制: 自动模式") - - except Exception as e: - print(f" 优化状态检查失败: {e}") - - def _diagnose_opengl_state(self): - """诊断OpenGL渲染状态""" - try: - # 检查VR缓冲区状态 - if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: - left_gsg = self.vr_left_eye_buffer.getGsg() - left_valid = self.vr_left_eye_buffer.isValid() - print(f" 左眼缓冲区: {'有效' if left_valid else '无效'}") - - if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: - right_gsg = self.vr_right_eye_buffer.getGsg() - right_valid = self.vr_right_eye_buffer.isValid() - print(f" 右眼缓冲区: {'有效' if right_valid else '无效'}") - - # 检查纹理准备状态 - if hasattr(self, 'textures_prepared'): - print(f" 纹理准备状态: {'已准备' if self.textures_prepared else '未准备'}") - - # 检查纹理ID缓存 - if hasattr(self, 'left_texture_id') and hasattr(self, 'right_texture_id'): - left_id = self.left_texture_id or 0 - right_id = self.right_texture_id or 0 - print(f" 纹理ID缓存: 左眼={left_id}, 右眼={right_id}") - - if left_id == 0 or right_id == 0: - print(f" ⚠️ 检测到无效的纹理ID,这可能导致提交失败") - else: - print(f" ✓ 纹理ID缓存正常") - - # 检查场景渲染状态 - if hasattr(self.world, 'render') and self.world.render: - render_children = len(self.world.render.getChildren()) - print(f" 场景节点数: {render_children}") - if render_children == 0: - print(f" ⚠️ 场景为空,可能导致渲染时间异常短") - - except Exception as e: - print(f" OpenGL状态诊断失败: {e}") - - def enable_debug_output(self): - """启用调试输出""" - self.debug_output_enabled = True - print("✓ VR调试输出已启用") - - def disable_debug_output(self): - """禁用调试输出""" - self.debug_output_enabled = False - print("✓ VR调试输出已禁用") - - def set_debug_mode(self, mode): - """设置调试模式 - - Args: - mode: 'brief' 或 'detailed' - """ - if mode in ['brief', 'detailed']: - self.debug_mode = mode - print(f"✓ VR调试模式设置为: {mode}") - else: - print("⚠️ 调试模式只能是 'brief' 或 'detailed'") - - def toggle_debug_output(self): - """切换调试输出状态""" - self.debug_output_enabled = not self.debug_output_enabled - status = "启用" if self.debug_output_enabled else "禁用" - print(f"✓ VR调试输出已{status}") - return self.debug_output_enabled - - def get_debug_status(self): - """获取调试状态""" - return { - 'debug_enabled': self.debug_output_enabled, - 'debug_mode': self.debug_mode, - 'performance_monitoring': self.performance_monitoring, - 'report_interval_frames': getattr(self, 'performance_report_interval', 600), - 'report_interval_seconds': getattr(self, 'performance_report_interval', 600) / 60, # 假设60fps - 'gc_control_enabled': self._gc_control_enabled, - 'gc_disabled': self._gc_disabled, - 'manual_gc_interval': self._manual_gc_interval, - } - - # ====== Python垃圾回收控制方法 ====== - - def enable_gc_control(self): - """启用垃圾回收控制 - 减少VR渲染期间的GC峰值""" - if not self._gc_control_enabled: - self._gc_control_enabled = True - if not self._gc_disabled: - gc.disable() - self._gc_disabled = True - print("✅ VR垃圾回收控制已启用") - else: - print("ℹ️ VR垃圾回收控制已经启用") - - def disable_gc_control(self): - """禁用垃圾回收控制 - 恢复自动垃圾回收""" - if self._gc_control_enabled: - self._gc_control_enabled = False - if self._gc_disabled: - gc.enable() - self._gc_disabled = False - print("✅ VR垃圾回收控制已禁用,恢复自动垃圾回收") - else: - print("ℹ️ VR垃圾回收控制已经禁用") - - def set_manual_gc_interval(self, frames): - """设置手动垃圾回收间隔 - - Args: - frames: 帧数间隔 (建议100-600) - """ - if 50 <= frames <= 1800: - old_interval = self._manual_gc_interval - self._manual_gc_interval = frames - print(f"✅ 手动GC间隔: {old_interval} → {frames} 帧") - else: - print("⚠️ GC间隔应在50-1800帧之间") - - def force_manual_gc(self): - """强制执行一次垃圾回收""" - collected = gc.collect() - print(f"🗑️ 强制GC: 清理了 {collected} 个对象") - return collected - - def get_object_pool_status(self): - """获取对象池状态""" - return { - 'matrix_pool_size': len(self._matrix_pool) if hasattr(self, '_matrix_pool') else 0, - 'matrix_pool_capacity': getattr(self, '_matrix_pool_size', 0), - 'cached_controllers': len(self.controller_poses), - 'cached_matrices': len(getattr(self, '_cached_matrices', {})), - } - - # ====== VR分辨率缩放和质量预设系统 ====== - - def set_resolution_scale(self, scale): - """设置VR分辨率缩放系数 - - Args: - scale: 缩放系数 (0.3-1.0),0.75表示75%分辨率 - """ - if not (0.3 <= scale <= 1.0): - print(f"⚠️ 分辨率缩放系数应在0.3-1.0之间,当前: {scale}") - return False - - old_scale = self.resolution_scale - self.resolution_scale = scale - - # 如果VR已初始化,重新创建缓冲区 - if self.vr_initialized: - self._apply_resolution_scale() - - print(f"✓ VR分辨率缩放: {old_scale} → {scale}") - pixel_reduction = (1 - scale**2) * 100 - print(f"📊 像素减少: {pixel_reduction:.1f}%") - - return True - - def set_quality_preset(self, preset_name): - """设置VR质量预设 - - Args: - preset_name: 'performance', 'balanced', 'quality' - """ - if preset_name not in self.quality_presets: - print(f"⚠️ 未知的质量预设: {preset_name}") - print(f" 可用预设: {list(self.quality_presets.keys())}") - return False - - old_preset = self.current_quality_preset - self.current_quality_preset = preset_name - scale = self.quality_presets[preset_name] - - print(f"🎯 切换VR质量预设: {old_preset} → {preset_name}") - - return self.set_resolution_scale(scale) - - def cycle_quality_preset(self): - """循环切换质量预设""" - presets = list(self.quality_presets.keys()) - current_index = presets.index(self.current_quality_preset) - next_index = (current_index + 1) % len(presets) - next_preset = presets[next_index] - - return self.set_quality_preset(next_preset) - - def _apply_resolution_scale(self): - """应用分辨率缩放,重新创建VR缓冲区""" - try: - # 计算新的分辨率 - self.scaled_eye_width = int(self.base_eye_width * self.resolution_scale) - self.scaled_eye_height = int(self.base_eye_height * self.resolution_scale) - - # 更新当前分辨率 - self.eye_width = self.scaled_eye_width - self.eye_height = self.scaled_eye_height - self.current_eye_resolution = (self.eye_width, self.eye_height) - - print(f"🔄 重新创建VR缓冲区...") - print(f" 新分辨率: {self.eye_width}x{self.eye_height}") - - # 清理旧的缓冲区 - self._cleanup_vr_buffers() - - # 🔧 关键修复:根据渲染模式选择创建方法 - success = False - if self.vr_render_mode == VRRenderMode.RENDER_PIPELINE: - print(f" 使用RenderPipeline模式重建...") - success = self._create_vr_buffers_with_pipeline() - if not success: - print("⚠️ RenderPipeline模式创建失败,回退到普通渲染模式") - self.vr_render_mode = VRRenderMode.NORMAL - success = self._create_vr_buffers() - else: - print(f" 使用普通模式重建...") - success = self._create_vr_buffers() - - if success: - # 重新设置相机 - self._setup_vr_cameras() - print("✅ VR缓冲区重新创建成功") - return True - else: - print("❌ VR缓冲区重新创建失败") - return False - - except Exception as e: - print(f"❌ 应用分辨率缩放失败: {e}") - import traceback - traceback.print_exc() - return False - - - def get_resolution_info(self): - """获取分辨率相关信息""" - return { - 'base_resolution': (self.base_eye_width, self.base_eye_height), - 'current_resolution': (self.eye_width, self.eye_height), - 'resolution_scale': self.resolution_scale, - 'current_preset': self.current_quality_preset, - 'available_presets': self.quality_presets, - 'pixel_reduction_percent': (1 - self.resolution_scale**2) * 100 - } - - def print_resolution_info(self): - """输出分辨率信息""" - info = self.get_resolution_info() - print("🔧 ===== VR分辨率信息 =====") - print(f" 推荐分辨率: {info['base_resolution'][0]}x{info['base_resolution'][1]}") - print(f" 当前分辨率: {info['current_resolution'][0]}x{info['current_resolution'][1]}") - print(f" 缩放系数: {info['resolution_scale']}") - print(f" 当前预设: {info['current_preset']}") - print(f" 像素减少: {info['pixel_reduction_percent']:.1f}%") - print(" 可用预设:") - for name, scale in info['available_presets'].items(): - marker = "✓" if name == info['current_preset'] else " " - print(f" {marker} {name}: {scale} ({scale*100:.0f}%)") - print("==========================") - - # ====== 性能模式控制方法 ====== - - def enable_performance_mode(self): - """手动启用性能模式 - 立即禁用详细监控以提升性能""" - if not self.performance_mode_enabled: - self.performance_mode_enabled = True - print("🎯 性能模式已手动启用 - 禁用详细监控以提升性能") - print(" 现在将减少每帧对象创建,显著提升VR性能稳定性") - else: - print("ℹ️ 性能模式已经启用") - - def disable_performance_mode(self): - """禁用性能模式 - 重新启用详细监控(用于调试)""" - if self.performance_mode_enabled: - self.performance_mode_enabled = False - print("🔍 性能模式已禁用 - 重新启用详细监控") - print(" 注意:这将增加每帧对象创建,可能影响VR性能") - else: - print("ℹ️ 性能模式已经禁用") - - def set_performance_mode_trigger_frame(self, frame_count): - """设置性能模式自动触发的帧数 - - Args: - frame_count: 触发帧数 (建议300-1200) - """ - if 100 <= frame_count <= 3600: - old_trigger = self.performance_mode_trigger_frame - self.performance_mode_trigger_frame = frame_count - print(f"✅ 性能模式触发帧数: {old_trigger} → {frame_count}") - else: - print("⚠️ 触发帧数应在100-3600之间") - - def get_performance_mode_status(self): - """获取性能模式状态""" - return { - 'performance_mode_enabled': self.performance_mode_enabled, - 'trigger_frame': self.performance_mode_trigger_frame, - 'current_frame': self.frame_count, - 'will_trigger_at_frame': self.performance_mode_trigger_frame if not self.performance_mode_enabled else None, - 'gc_interval_normal': self._manual_gc_interval, - 'gc_interval_performance': self._manual_gc_interval * 2, - } - - # ====== VR测试模式 ====== - - def enable_vr_test_mode(self, display_mode='stereo'): - """启用VR测试模式 - 将VR渲染直接显示在屏幕上 - - Args: - display_mode: 显示模式 - - 'stereo': 左右眼并排显示 - - 'left': 只显示左眼 - - 'right': 只显示右眼 - """ - if not self.is_vr_available(): - print("❌ VR系统不可用,无法启动测试模式") - return False - - try: - print(f"🧪 启动VR测试模式 - 显示模式: {display_mode}") - - # 初始化VR系统(如果还没初始化) - if not self.vr_initialized: - if not self.initialize_vr(): - print("❌ VR初始化失败") - return False - - # 🔧 关键修复:确保测试模式的纹理资源已初始化 - # 这解决了测试模式纹理提交失败导致的36FPS问题 - print("🔧 检查VR测试模式纹理资源...") - if not self._ensure_test_mode_textures(): - print("❌ VR测试模式纹理资源初始化失败") - return False - - # 设置测试模式 - self.vr_test_mode = True - self.test_display_mode = display_mode - - # 启用VR渲染但不提交给OpenVR - if not self.vr_enabled: - # 启用VR渲染流程(但会在回调中跳过提交) - self.vr_enabled = True - self._disable_main_cam() - - # 设置高帧率用于测试 - if hasattr(self.world, 'qtWidget') and self.world.qtWidget: - if hasattr(self.world.qtWidget, 'synchronizer'): - self.world.qtWidget.synchronizer.setInterval(int(1000/144)) - print("✓ 测试模式:Qt Timer设置为144Hz") - - # 初始化测试显示系统 - if not self._initialize_test_display(): - print("❌ 测试显示系统初始化失败") - return False - - # 创建性能HUD - if not self._initialize_test_performance_hud(): - print("❌ 性能HUD初始化失败") - return False - - # 恢复主相机以查看测试内容 - self._enable_main_cam() - - # 重置HUD更新计数器 - self.hud_update_counter = 0 - - print("✅ VR测试模式已启用") - print(" - VR内容将显示在屏幕上") - print(" - 不会向OpenVR提交纹理") - print(" - 可以准确测量纯渲染性能") - print(f" - 当前显示模式: {display_mode}") - - return True - - except Exception as e: - print(f"❌ 启动VR测试模式失败: {e}") - import traceback - traceback.print_exc() - return False - - def _ensure_test_mode_textures(self): - """确保VR测试模式的纹理资源已正确初始化 - - 这解决了测试模式启用纹理提交时的36FPS问题: - - VR测试模式可能跳过了VR渲染缓冲区的初始化 - - submit_texture()需要有效的texture ID和OpenVR Texture对象 - - 如果未初始化会导致提交失败,造成帧率减半 - """ - try: - print(" 检查VR渲染缓冲区...") - - # 检查VR渲染缓冲区是否存在 - buffers_exist = ( - hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer and - hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer - ) - - if not buffers_exist: - print(" ⚠️ VR渲染缓冲区不存在,正在创建...") - if not self._setup_vr_render_buffers(): - print(" ❌ VR渲染缓冲区创建失败") - return False - print(" ✅ VR渲染缓冲区创建成功") - else: - print(" ✅ VR渲染缓冲区已存在") - - # 检查纹理ID是否已缓存 - print(" 检查纹理ID缓存...") - texture_ids_cached = ( - hasattr(self, 'left_texture_id') and self.left_texture_id and self.left_texture_id > 0 and - hasattr(self, 'right_texture_id') and self.right_texture_id and self.right_texture_id > 0 - ) - - if not texture_ids_cached: - print(" ⚠️ 纹理ID未缓存,正在准备纹理...") - if not self._prepare_and_cache_textures(): - print(" ❌ 纹理准备和缓存失败") - return False - print(f" ✅ 纹理ID缓存成功 - 左眼:{self.left_texture_id}, 右眼:{self.right_texture_id}") - else: - print(f" ✅ 纹理ID已缓存 - 左眼:{self.left_texture_id}, 右眼:{self.right_texture_id}") - - # 检查OpenVR Texture对象是否已创建 - print(" 检查OpenVR Texture对象...") - ovr_textures_exist = ( - hasattr(self, '_left_ovr_texture') and self._left_ovr_texture and - hasattr(self, '_right_ovr_texture') and self._right_ovr_texture - ) - - if not ovr_textures_exist: - print(" ⚠️ OpenVR Texture对象未创建,正在创建...") - self._create_cached_ovr_textures() - print(" ✅ OpenVR Texture对象创建成功") - else: - print(" ✅ OpenVR Texture对象已存在") - - print(" ✅ VR测试模式纹理资源检查完成,可安全启用纹理提交") - return True - - except Exception as e: - print(f" ❌ VR测试模式纹理资源检查失败: {e}") - import traceback - traceback.print_exc() - return False - - def _create_cached_ovr_textures(self): - """创建缓存的OpenVR Texture对象 - 避免每帧创建新对象""" - try: - import openvr - self._left_ovr_texture = openvr.Texture_t() - self._right_ovr_texture = openvr.Texture_t() - - # 设置固定属性(这些不变) - self._left_ovr_texture.eType = openvr.TextureType_OpenGL - self._left_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma - self._right_ovr_texture.eType = openvr.TextureType_OpenGL - self._right_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma - - print("✅ OpenVR Texture对象缓存已创建") - except Exception as e: - print(f"⚠️ OpenVR Texture对象创建失败: {e}") - # 不抛出异常,使用备用方案 - - def _batch_submit_textures(self): - """批量提交两眼纹理 - OpenVR最佳实践 - - 基于官方hellovr示例的实现: - - 两眼都渲染完成后,快速连续提交 - - 减少submit阻塞时间,避免错过VSync窗口 - - 这是解决36FPS问题的关键 - """ - try: - if not self.vr_compositor: - return False - - # 检查纹理是否准备好 - if not (self.vr_left_texture and self.vr_right_texture): - return False - - # 🚀 关键:快速连续提交两眼,最小化阻塞时间 - # 这符合OpenVR官方示例的做法 - success_left = False - success_right = False - - # 提交左眼纹理 - try: - self.submit_texture(openvr.Eye_Left, self.vr_left_texture) - success_left = True - except Exception as e: - print(f"❌ 批量提交左眼失败: {e}") - - # 立即提交右眼纹理(不等待) - try: - self.submit_texture(openvr.Eye_Right, self.vr_right_texture) - success_right = True - except Exception as e: - print(f"❌ 批量提交右眼失败: {e}") - - # 🚀 关键修复:调用PostPresentHandoff解除compositor阻塞 - # 这是解决36FPS问题的核心 - 确保compositor不会等待VSync - if success_left and success_right: - try: - # PostPresentHandoff告诉compositor我们已完成帧处理 - # 防止compositor等待下一个VSync周期 - if hasattr(self.vr_compositor, 'postPresentHandoff'): - self.vr_compositor.postPresentHandoff() - elif hasattr(self.vr_compositor, 'PostPresentHandoff'): - self.vr_compositor.PostPresentHandoff() - else: - # 备用方案:如果没有PostPresentHandoff,记录警告 - if not hasattr(self, '_post_present_warning_logged'): - print("⚠️ PostPresentHandoff方法未找到,可能影响时序") - self._post_present_warning_logged = True - except Exception as handoff_error: - if not hasattr(self, '_handoff_error_logged'): - print(f"⚠️ PostPresentHandoff调用失败: {handoff_error}") - self._handoff_error_logged = True - - # 记录批量提交状态(仅首次成功时) - if not hasattr(self, '_batch_submit_success_logged'): - print("✅ OpenVR批量提交模式+PostPresentHandoff已启用") - print(" 这应该解决36FPS → 72FPS的问题") - self._batch_submit_success_logged = True - return True - else: - return False - - except Exception as e: - print(f"❌ 批量提交纹理失败: {e}") - return False - - def disable_vr_test_mode(self): - """禁用VR测试模式""" - try: - print("🧪 禁用VR测试模式...") - - # 清理测试显示 - self._cleanup_test_display() - - # 清理性能HUD - self._cleanup_test_performance_hud() - - # 关闭测试模式 - self.vr_test_mode = False - self.test_mode_initialized = False - - # 重置HUD更新计数器 - self.hud_update_counter = 0 - - # 恢复正常VR模式或禁用VR - if self.vr_enabled: - print(" 选择: [1] 恢复正常VR模式 [2] 完全禁用VR") - # 这里可以让用户选择,现在默认禁用VR - self.disable_vr() - - print("✅ VR测试模式已禁用") - return True - - except Exception as e: - print(f"❌ 禁用VR测试模式失败: {e}") - return False - - def switch_test_display_mode(self, display_mode): - """切换测试显示模式 - - Args: - display_mode: 'stereo', 'left', 'right' - """ - if not self.vr_test_mode: - print("⚠️ 请先启用VR测试模式") - return False - - if display_mode not in ['stereo', 'left', 'right']: - print(f"⚠️ 无效的显示模式: {display_mode}") - return False - - print(f"🧪 切换显示模式: {self.test_display_mode} → {display_mode}") - - # 如果切换模式,需要清理旧的显示并重置标志位 - if self.test_display_mode != display_mode: - self._cleanup_test_display() - - self.test_display_mode = display_mode - - # 更新显示 - self._update_test_display() - return True - - def _initialize_test_display(self): - """初始化测试显示系统""" - try: - print("🔧 正在初始化测试显示系统...") - - # 导入必要的Panda3D组件 - from panda3d.core import CardMaker, PandaNode - - # 创建显示四边形 - cm = CardMaker("vr_test_display") - - if self.test_display_mode == 'stereo': - # 并排显示:左眼在左半屏,右眼在右半屏 - cm.setFrame(-1, 1, -0.5, 0.5) # 全屏 - else: - # 单眼显示:占据整个屏幕 - cm.setFrame(-1, 1, -1, 1) - - # 创建节点 - self.test_display_quad = self.world.render2d.attachNewNode(cm.generate()) - - # 设置初始纹理 - self._update_test_display() - - # 设置渲染状态 - self.test_display_quad.setTransparency(0) # 不透明 - self.test_display_quad.setBin("background", 0) # 背景层 - - self.test_mode_initialized = True - print("✅ 测试显示系统初始化完成") - return True - - except Exception as e: - print(f"❌ 测试显示系统初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def _update_test_display(self): - """更新测试显示内容""" - try: - if not self.test_mode_initialized: - return - - # 根据显示模式设置纹理 - if self.test_display_mode == 'left': - if not self.test_display_quad: - return - if self.vr_left_texture: - self.test_display_quad.setTexture(self.vr_left_texture) - elif self.test_display_mode == 'right': - if not self.test_display_quad: - return - if self.vr_right_texture: - self.test_display_quad.setTexture(self.vr_right_texture) - elif self.test_display_mode == 'stereo': - # 立体显示:只在第一次创建,后续只更新纹理 - if not self.stereo_display_created: - self._create_stereo_display() - else: - # 只更新纹理,不重新创建 - if self.test_display_quad and self.vr_left_texture: - self.test_display_quad.setTexture(self.vr_left_texture) - if self.test_right_quad and self.vr_right_texture: - self.test_right_quad.setTexture(self.vr_right_texture) - - except Exception as e: - print(f"⚠️ 更新测试显示失败: {e}") - - def _create_stereo_display(self): - """创建左右眼并排显示""" - try: - # 如果已经创建,直接返回 - if self.stereo_display_created and self.test_display_quad and self.test_right_quad: - return - - # 移除旧的显示 - if self.test_display_quad: - self.test_display_quad.removeNode() - if self.test_right_quad: - self.test_right_quad.removeNode() - - from panda3d.core import CardMaker - - # 创建左眼显示 - left_cm = CardMaker("vr_test_left") - left_cm.setFrame(-1, 0, -1, 1) # 左半屏 - left_quad = self.world.render2d.attachNewNode(left_cm.generate()) - if self.vr_left_texture: - left_quad.setTexture(self.vr_left_texture) - - # 创建右眼显示 - right_cm = CardMaker("vr_test_right") - right_cm.setFrame(0, 1, -1, 1) # 右半屏 - right_quad = self.world.render2d.attachNewNode(right_cm.generate()) - if self.vr_right_texture: - right_quad.setTexture(self.vr_right_texture) - - # 保存引用 - self.test_display_quad = left_quad # 保存左眼引用 - self.test_right_quad = right_quad # 额外保存右眼引用 - self.stereo_display_created = True # 标记已创建 - - print("✓ 立体显示已创建") - - except Exception as e: - print(f"⚠️ 创建立体显示失败: {e}") - self.stereo_display_created = False - - def _cleanup_test_display(self): - """清理测试显示""" - try: - if self.test_display_quad: - self.test_display_quad.removeNode() - self.test_display_quad = None - - if hasattr(self, 'test_right_quad') and self.test_right_quad: - self.test_right_quad.removeNode() - self.test_right_quad = None - - # 重置立体显示标志位 - self.stereo_display_created = False - - print("✓ 测试显示已清理") - - except Exception as e: - print(f"⚠️ 清理测试显示失败: {e}") - - def _initialize_test_performance_hud(self): - """初始化性能HUD""" - try: - print("🔧 正在初始化性能HUD...") - - from direct.gui.OnscreenText import OnscreenText - from panda3d.core import TextNode - - # 创建性能文本显示 - self.test_performance_text = OnscreenText( - text="初始化中...", - pos=(-0.95, 0.9), # 左上角,调整到屏幕内 - scale=0.05, - fg=(1, 1, 0, 1), # 黄色 - align=TextNode.ALeft, - shadow=(0, 0, 0, 0.5), # 黑色阴影 - parent=self.world.render2d - ) - - print("✅ 性能HUD初始化完成") - return True - - except Exception as e: - print(f"❌ 性能HUD初始化失败: {e}") - return False - - def _update_test_performance_hud(self): - """更新性能HUD显示(限制更新频率避免重影)""" - try: - if not self.test_performance_text or not self.vr_test_mode: - return - - # 增加计数器 - self.hud_update_counter += 1 - - # 只在指定间隔更新文本,避免重影 - if self.hud_update_counter < self.hud_update_interval: - return - - # 重置计数器 - self.hud_update_counter = 0 - - # 收集性能数据 - left_render_time = getattr(self, 'left_render_time', 0) - right_render_time = getattr(self, 'right_render_time', 0) - total_render_time = left_render_time + right_render_time - - # 计算FPS - current_fps = self.vr_fps if hasattr(self, 'vr_fps') else 0 - - # 获取系统性能 - cpu_usage = getattr(self, 'cpu_usage', 0) - memory_usage = getattr(self, 'memory_usage', 0) - gpu_usage = getattr(self, 'gpu_usage', 0) - - # 构建显示文本 - hud_text = f"""VR TEST MODE - {self.test_display_mode.upper()} - -Render Performance: - FPS: {current_fps:.1f} - Left Eye: {left_render_time:.2f}ms - Right Eye: {right_render_time:.2f}ms - Total: {total_render_time:.2f}ms - -System Performance: - CPU: {cpu_usage:.1f}% - Memory: {memory_usage:.1f}% - GPU: {gpu_usage:.1f}% - -Render Count: - Left Eye: {getattr(self, 'left_render_count', 0)} - Right Eye: {getattr(self, 'right_render_count', 0)} - -Target: <13.9ms@72Hz -Status: {'GOOD' if total_render_time < 10 else 'HIGH' if total_render_time < 16 else 'SLOW'} - -Hotkeys: -F1=Left F2=Right F3=SideBySide -ESC=Exit Test Mode""" - - # 更新文本 - self.test_performance_text.setText(hud_text) - - except Exception as e: - print(f"⚠️ 更新性能HUD失败: {e}") - - def _cleanup_test_performance_hud(self): - """清理性能HUD""" - try: - if self.test_performance_text: - self.test_performance_text.destroy() - self.test_performance_text = None - - print("✓ 性能HUD已清理") - - except Exception as e: - print(f"⚠️ 清理性能HUD失败: {e}") - - def get_test_mode_status(self): - """获取测试模式状态""" - if not self.vr_test_mode: - return None - - left_render_time = getattr(self, 'left_render_time', 0) - right_render_time = getattr(self, 'right_render_time', 0) - total_render_time = left_render_time + right_render_time - - return { - 'test_mode_enabled': self.vr_test_mode, - 'display_mode': self.test_display_mode, - 'vr_fps': getattr(self, 'vr_fps', 0), - 'left_render_time': left_render_time, - 'right_render_time': right_render_time, - 'total_render_time': total_render_time, - 'left_render_count': getattr(self, 'left_render_count', 0), - 'right_render_count': getattr(self, 'right_render_count', 0), - 'performance_rating': ('excellent' if total_render_time < 10 else - 'good' if total_render_time < 16 else 'poor') - } - - def set_test_mode_features(self, submit_texture=None, wait_poses=None): - """设置测试模式功能开关 - 用于渐进式调试 - - Args: - submit_texture: 是否在测试模式启用纹理提交(True/False/None=保持当前) - wait_poses: 是否在测试模式启用waitGetPoses(True/False/None=保持当前) - """ - if submit_texture is not None: - self.test_mode_submit_texture = submit_texture - if wait_poses is not None: - self.test_mode_wait_poses = wait_poses - - print(f"🔧 VR测试模式功能设置:") - print(f" 纹理提交: {'启用' if self.test_mode_submit_texture else '禁用'}") - print(f" 姿态等待: {'启用' if self.test_mode_wait_poses else '禁用'}") - - # 如果当前在测试模式,输出预期效果 - if self.vr_test_mode: - print(f" 当前测试模式已启用,设置将立即生效") - else: - print(f" 设置已保存,将在下次启用测试模式时生效") - - def get_test_mode_features(self): - """获取当前测试模式功能设置""" - return { - 'submit_texture': self.test_mode_submit_texture, - 'wait_poses': self.test_mode_wait_poses, - 'test_mode_active': self.vr_test_mode - } - - def run_vr_performance_test(self, duration_seconds=30, display_mode='stereo'): - """运行VR性能测试 - 简单的测试入口方法 - - Args: - duration_seconds: 测试持续时间(秒) - display_mode: 显示模式 ('stereo', 'left', 'right') - - Returns: - dict: 测试结果统计 - """ - print("🧪 ======= VR性能测试开始 =======") - print(f" 测试模式: {display_mode}") - print(f" 测试时长: {duration_seconds}秒") - print(" 按ESC键提前退出测试") - print(" F1=左眼 F2=右眼 F3=并排显示") - print("=================================") - - # 启动测试模式 - if not self.enable_vr_test_mode(display_mode): - print("❌ VR测试模式启动失败") - return None - - # 记录测试开始状态 - import time - test_start_time = time.time() - start_frame_count = getattr(self, 'frame_count', 0) - - print("✅ VR测试模式已启动") - print(" - VR内容直接显示在屏幕上") - print(" - 无OpenVR纹理提交开销") - print(" - 实时性能监控已启用") - print(f" - 当前显示: {display_mode.upper()}模式") - print("\n开始性能测试...") - - # 等待测试完成(用户手动停止或超时) - print(f"\n📊 测试将运行 {duration_seconds} 秒") - print(" 实时性能数据显示在屏幕左上角") - print(" 观察帧率和渲染时间变化") - - # 这里可以添加自动停止逻辑,但现在让用户手动控制 - print(f"\n⏰ 请观察 {duration_seconds} 秒后手动调用 disable_vr_test_mode() 停止测试") - print(" 或者随时调用 get_test_mode_status() 查看当前状态") - - return { - 'status': 'running', - 'start_time': test_start_time, - 'start_frame': start_frame_count, - 'display_mode': display_mode, - 'instructions': { - 'stop_test': 'vr_manager.disable_vr_test_mode()', - 'check_status': 'vr_manager.get_test_mode_status()', - 'switch_mode': 'vr_manager.switch_test_display_mode("left/right/stereo")' - } +""" +VR管理器模块 + +负责VR功能的初始化、渲染和交互: +- OpenVR/SteamVR集成 +- VR头显跟踪和渲染 +- VR控制器交互 +- VR模式切换 +""" + +import sys +import gc +import numpy as np +from panda3d.core import ( + WindowProperties, GraphicsPipe, FrameBufferProperties, + GraphicsOutput, Texture, Camera, PerspectiveLens, MatrixLens, + Mat4, Vec3, TransformState, RenderState, CardMaker, + BitMask32, PandaNode, NodePath, LMatrix4, LVector3, LVector4, + CS_yup_right, CS_default, PythonCallbackObject +) +from direct.task import Task +from direct.showbase.DirectObject import DirectObject + +try: + import openvr + OPENVR_AVAILABLE = True +except ImportError: + OPENVR_AVAILABLE = False + print("警告: OpenVR未安装,VR功能将不可用") + +# 导入手柄控制器、动作系统、交互系统、摇杆系统和传送系统 +from core.vr.tracking.controllers import LeftController, RightController +from core.vr.interaction.actions import VRActionManager +from core.vr.interaction.grab import VRInteractionManager +from core.vr.interaction.joystick import VRJoystickManager +from core.vr.interaction.teleport import VRTeleportSystem +from enum import Enum + + +class VRRenderMode(Enum): + """VR渲染模式枚举""" + NORMAL = "normal" # 普通渲染模式 + RENDER_PIPELINE = "render_pipeline" # RenderPipeline高级渲染模式 + + +class VRManager(DirectObject): + """VR管理器类 - 处理所有VR相关功能""" + + def __init__(self, world): + """初始化VR管理器 + + Args: + world: 主世界对象引用 + """ + super().__init__() + + self.world = world + self.vr_system = None + self.vr_enabled = False + self.vr_initialized = False + + # VR渲染相关 + self.vr_left_eye_buffer = None + self.vr_right_eye_buffer = None + self.vr_left_camera = None + self.vr_right_camera = None + self.vr_compositor = None + + # VR纹理和ID缓存 - 修复重复准备问题 + self.vr_left_texture = None + self.vr_right_texture = None + self.left_texture_id = None # 缓存左眼纹理的OpenGL ID + self.right_texture_id = None # 缓存右眼纹理的OpenGL ID + self.textures_prepared = False # 标记纹理是否已准备 + + # VR跟踪数据 + self.hmd_pose = Mat4.identMat() + self.controller_poses = {} + self.tracked_device_poses = [] + self.poses = None # OpenVR渲染姿态数组 + self.game_poses = None # OpenVR游戏逻辑姿态数组 + + # 🚀 对象池和缓存系统 - 修复16-19帧周期性GPU峰值 + self._matrix_pool = [] # Mat4对象池 + self._matrix_pool_size = 8 # 池大小,足够处理多个控制器 + self._cached_matrices = {} # 设备ID到矩阵的缓存 + self._controller_poses_cache = {} # 控制器姿态缓存,避免每帧clear() + + # 🚀 OpenVR Texture对象缓存 - 避免每帧创建openvr.Texture_t() + self._left_ovr_texture = None # 左眼纹理对象缓存 + self._right_ovr_texture = None # 右眼纹理对象缓存 + + # Python垃圾回收控制 + self._gc_control_enabled = True # 是否启用GC控制 + self._gc_disabled = False # GC是否被禁用 + self._manual_gc_interval = 900 # 每900帧手动触发一次GC (15秒@60fps) - 减少GC频率 + self._last_manual_gc_frame = 0 + + # 🚀 立即初始化对象池和GC控制(在其他组件之前) + self._initialize_object_pools() + + # VR渲染参数 + self.eye_width = 1080 + self.eye_height = 1200 + self.near_clip = 0.1 + self.far_clip = 1000.0 + + # VR分辨率缩放优化 + self.resolution_scale = 0.75 # 默认0.75倍分辨率,性能和质量平衡 + self.base_eye_width = 1080 # 原始推荐分辨率 + self.base_eye_height = 1200 + self.scaled_eye_width = 1080 # 实际使用的缩放后分辨率 + self.scaled_eye_height = 1200 + + # VR质量预设 + self.quality_presets = { + 'performance': 0.6, # 性能模式 - 约60%分辨率 + 'balanced': 0.75, # 平衡模式 - 约75%分辨率 + 'quality': 1.0 # 质量模式 - 100%分辨率 + } + self.current_quality_preset = 'balanced' # 默认平衡模式 + + # 🎨 VR渲染模式配置 - RenderPipeline集成 + self.vr_render_mode = VRRenderMode.NORMAL # 默认使用普通渲染模式 + self.render_pipeline_enabled = False # RenderPipeline是否已启用 + self.vr_pipeline_left_target = None # 左眼RenderPipeline渲染目标 + self.vr_pipeline_right_target = None # 右眼RenderPipeline渲染目标 + self.pipeline_resolution_scale = 0.75 # RenderPipeline模式下的分辨率缩放(性能优化) + self.vr_pipeline_controller = None # VR Pipeline控制器(管理完整的VR渲染管线) + + # RenderPipeline VR优化配置 + self.pipeline_vr_config = { + 'enable_shadows': True, # 启用阴影 + 'enable_ao': True, # 启用环境光遮蔽 + 'enable_bloom': False, # 禁用泛光(VR性能考虑) + 'enable_motion_blur': False, # 禁用运动模糊(VR中会引起不适) + 'enable_ssr': False, # 禁用屏幕空间反射(性能密集) + 'shadow_quality': 'medium', # 阴影质量:low/medium/high + 'ao_quality': 'low', # AO质量:low/medium/high + } + + # VR任务 + self.vr_task = None + + # VR锚点层级系统 + self.tracking_space = None + self.hmd_anchor = None + self.left_eye_anchor = None + self.right_eye_anchor = None + + # 坐标系转换矩阵 - 使用Panda3D内置方法 + self.coord_mat = LMatrix4.convert_mat(CS_yup_right, CS_default) + self.coord_mat_inv = LMatrix4.convert_mat(CS_default, CS_yup_right) + + # 性能监控 + self.frame_count = 0 + self.last_fps_check = 0 + self.last_fps_time = 0 + self.vr_fps = 0 + self.submit_failures = 0 + self.pose_failures = 0 + + # OpenVR 帧ID跟踪(防止重复提交) + self.openvr_frame_id = 0 + self.left_eye_last_render_frame = -1 + self.right_eye_last_render_frame = -1 + + # 高级性能监控(默认关闭,手动开启) + self.performance_monitoring = False # 是否启用性能监控 + self.debug_output_enabled = False # 是否启用调试输出 + self.debug_mode = 'detailed' # 'brief' 或 'detailed' + self.cpu_usage = 0.0 + self.memory_usage = 0.0 + self.gpu_usage = 0.0 + self.gpu_memory_usage = 0.0 + self.frame_times = [] # 存储最近帧时间 + self.max_frame_time_history = 60 # 保存60帧的历史 + self.last_performance_check = 0 + self.performance_check_interval = 0.5 # 每0.5秒更新一次性能数据 + + # 渲染管线详细监控 + self.enable_pipeline_monitoring = True # 是否启用管线监控 + self.performance_mode_enabled = False # 性能优化模式(禁用监控以减少对象创建) + self.performance_mode_trigger_frame = 600 # 第600帧后启用性能模式 + self.wait_poses_time = 0.0 # waitGetPoses耗时 + self.left_render_time = 0.0 # 左眼渲染耗时 + self.right_render_time = 0.0 # 右眼渲染耗时 + self.submit_time = 0.0 # 纹理提交耗时 + self.left_render_count = 0 # 左眼渲染次数计数 + self.right_render_count = 0 # 右眼渲染次数计数 + self.total_frame_time = 0.0 # 总帧时间 + self.vr_sync_wait_time = 0.0 # VR同步等待时间 + + # 时间监控历史(保存最近30帧) + self.wait_poses_times = [] + self.render_times = [] + self.submit_times = [] + self.sync_wait_times = [] + self.pipeline_history_size = 30 + + # GPU渲染时间监控(OpenVR Frame Timing) + self.enable_gpu_timing = False # 是否启用GPU时间监控(默认关闭) + self.gpu_scene_render_ms = 0.0 # GPU场景渲染时间 + self.gpu_pre_submit_ms = 0.0 # 提交前GPU时间 + self.gpu_post_submit_ms = 0.0 # 提交后GPU时间 + self.gpu_total_render_ms = 0.0 # GPU总渲染时间 + self.gpu_compositor_render_ms = 0.0 # GPU合成器渲染时间 + self.gpu_client_frame_interval_ms = 0.0 # 客户端帧间隔 + self.gpu_timing_history = [] # GPU时间历史记录 + self.gpu_timing_history_size = 30 # GPU时间历史记录大小 + self.gpu_timing_failure_count = 0 # GPU时间获取失败次数 + + # VR系统信息 + self.current_eye_resolution = (self.eye_width, self.eye_height) + self.recommended_eye_resolution = (0, 0) + self.vr_display_frequency = 0.0 + self.vr_vsync_enabled = True + self.vsync_to_photons_ms = 0.0 # VSync到光子的延迟 + self.target_frame_time_ms = 0.0 # 目标帧时间 + self.vsync_window_ms = 0.0 # VSync时间窗口 + self.async_reprojection_enabled = False # 异步重投影状态 + self.motion_smoothing_enabled = False # 运动平滑状态 + + # 🧪 VR测试模式 + self.vr_test_mode = False # 是否启用VR测试模式 + self.test_display_mode = 'stereo' # 'left', 'right', 'stereo' + self.test_display_quad = None # 测试显示的四边形 + self.test_right_quad = None # 右眼显示的四边形(立体模式) + self.stereo_display_created = False # 立体显示是否已创建 + self.test_performance_hud = None # 性能HUD + self.test_performance_text = None # 性能文本节点 + self.test_mode_initialized = False # 测试模式是否已初始化 + self.hud_update_counter = 0 # HUD更新计数器 + self.hud_update_interval = 30 # HUD更新间隔(帧数),30帧约0.5秒@60fps + + # 🔧 VR测试模式调试选项 - 逐步启用普通VR功能 + self.test_mode_submit_texture = False # 是否在测试模式提交纹理到OpenVR + self.test_mode_wait_poses = False # 是否在测试模式调用waitGetPoses + + # waitGetPoses调用策略配置 - 使用高性能的update_task策略 + self.use_prediction_time = 0.011 # 11ms的预测时间 - OpenVR标准值,平衡准确性和延迟 + self.poses_updated_in_task = True # 始终在更新任务中获取姿态(Running Start模式) + + # 🎨 初始化VR配置管理器 + try: + from core.vr.config.vr_config import VRConfigManager + self.config_manager = VRConfigManager() + # 加载配置并应用 + self.config_manager.apply_config_to_vr_manager(self) + print("✓ VR配置管理器初始化完成并已加载配置") + except Exception as e: + print(f"⚠️ VR配置管理器初始化失败: {e}") + self.config_manager = None + + # 尝试导入性能监控库 + self._init_performance_monitoring() + + # VR提交策略 - 基于参考实现 + self.submit_together = True # 是否在right_cb中同时提交左右眼 + + + # Running Start标记 - Valve最佳实践 + self._waitgetposes_called_this_frame = False + + # 姿态缓存 - 修复时序不匹配 + self._cached_render_poses = None # 用于渲染的缓存姿态 + self._first_frame = True # 首帧标记 + + # ATW控制选项 - 备选方案 + self.disable_async_reprojection = False # 是否禁用异步重投影 + + # VR手柄控制器 + self.left_controller = None + self.right_controller = None + self.controllers = {} # 设备索引到控制器的映射 + self.tracked_device_anchors = {} # 跟踪设备锚点 + + # VR动作系统 - 可选择禁用(用于API兼容性问题) + self.disable_action_system = True # 禁用动作系统,使用直接控制器输入 + + if not self.disable_action_system: + try: + self.action_manager = VRActionManager(self) + print("✓ VR动作管理器初始化完成") + except Exception as e: + print(f"⚠️ VR动作管理器初始化失败: {e}") + self.action_manager = None + else: + self.action_manager = None + print("🚫 VR动作系统已禁用,使用直接控制器输入") + + # VR交互系统 - 添加异常保护 + try: + self.interaction_manager = VRInteractionManager(self) + print("✓ VR交互管理器初始化完成") + except Exception as e: + print(f"⚠️ VR交互管理器初始化失败: {e}") + self.interaction_manager = None + + # VR传送系统 - 添加异常保护 + try: + self.teleport_system = VRTeleportSystem(self) + print("✓ VR传送系统初始化完成") + except Exception as e: + print(f"⚠️ VR传送系统初始化失败: {e}") + self.teleport_system = None + + # VR摇杆系统 - 添加异常保护 + try: + self.joystick_manager = VRJoystickManager(self) + print("✓ VR摇杆管理器初始化完成") + except Exception as e: + print(f"⚠️ VR摇杆管理器初始化失败: {e}") + self.joystick_manager = None + + print("✓ VR管理器初始化完成") + + def _initialize_object_pools(self): + """初始化对象池 - 修复16-19帧周期性GPU峰值""" + try: + # 预填充Mat4对象池 + for _ in range(self._matrix_pool_size): + self._matrix_pool.append(Mat4()) + + print(f"✅ Mat4对象池初始化完成 - 池大小: {self._matrix_pool_size}") + + # 🚀 预创建OpenVR Texture对象 - 消除每帧创建openvr.Texture_t()的开销 + try: + import openvr + self._left_ovr_texture = openvr.Texture_t() + self._right_ovr_texture = openvr.Texture_t() + + # 设置固定属性(这些不变) + self._left_ovr_texture.eType = openvr.TextureType_OpenGL + self._left_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + self._right_ovr_texture.eType = openvr.TextureType_OpenGL + self._right_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + + print("✅ OpenVR Texture对象缓存已创建 - 避免每帧创建新对象") + except Exception as texture_error: + print(f"⚠️ OpenVR Texture对象创建失败: {texture_error}") + # 不影响整体初始化,但性能可能不是最优 + + # 启用GC控制 + if self._gc_control_enabled: + # 禁用自动垃圾回收,改为手动控制 + gc.disable() + self._gc_disabled = True + print("✅ Python垃圾回收已禁用,改为手动控制") + + except Exception as e: + print(f"⚠️ 对象池初始化失败: {e}") + + def _get_pooled_matrix(self): + """从对象池获取Mat4对象""" + if self._matrix_pool: + return self._matrix_pool.pop() + else: + # 池为空时创建新对象(不应该发生,但作为备用) + return Mat4() + + def _return_pooled_matrix(self, matrix): + """将Mat4对象返回对象池""" + if len(self._matrix_pool) < self._matrix_pool_size: + # 重置矩阵到单位矩阵 + matrix.identMat() + self._matrix_pool.append(matrix) + + def _manual_gc_control(self): + """手动垃圾回收控制 - 避免VR渲染期间的GC峰值""" + if not self._gc_control_enabled or not self._gc_disabled: + return + + # 🚀 智能GC间隔:性能模式下减少GC频率 + current_interval = self._manual_gc_interval + if self.performance_mode_enabled: + current_interval = self._manual_gc_interval * 2 # 性能模式下间隔翻倍 + + # 每N帧手动触发一次垃圾回收 + if self.frame_count - self._last_manual_gc_frame >= current_interval: + # 在非渲染关键时刻触发GC + collected = gc.collect() + self._last_manual_gc_frame = self.frame_count + + # 仅在收集到对象时输出信息 + if collected > 0: + print(f"🗑️ 手动GC: 清理了 {collected} 个对象 (帧#{self.frame_count})") + + def _update_matrix_from_openvr(self, panda_mat, ovr_matrix): + """直接更新现有Mat4对象的数值,避免创建新对象""" + # 复用_convert_openvr_matrix_to_panda中的转换逻辑 + # X轴行:Panda3D的X轴对应OpenVR的X轴 + panda_mat.setCell(0, 0, ovr_matrix[0][0]) # X_x → X_x + panda_mat.setCell(0, 1, ovr_matrix[0][1]) # X_y → X_y + panda_mat.setCell(0, 2, ovr_matrix[0][2]) # X_z → X_z + panda_mat.setCell(0, 3, ovr_matrix[0][3]) # 位移X分量 + + # Y轴行:Panda3D的Y轴对应OpenVR的-Z轴 + panda_mat.setCell(1, 0, -ovr_matrix[2][0]) # -Z_x → Y_x + panda_mat.setCell(1, 1, -ovr_matrix[2][1]) # -Z_y → Y_y + panda_mat.setCell(1, 2, -ovr_matrix[2][2]) # -Z_z → Y_z + panda_mat.setCell(1, 3, -ovr_matrix[2][3]) # 位移Y分量(-Z位移) + + # Z轴行:Panda3D的Z轴对应OpenVR的Y轴 + panda_mat.setCell(2, 0, ovr_matrix[1][0]) # Y_x → Z_x + panda_mat.setCell(2, 1, ovr_matrix[1][1]) # Y_y → Z_y + panda_mat.setCell(2, 2, ovr_matrix[1][2]) # Y_z → Z_z + panda_mat.setCell(2, 3, ovr_matrix[1][3]) # 位移Z分量(Y位移) + + # 齐次坐标 + panda_mat.setCell(3, 0, 0) + panda_mat.setCell(3, 1, 0) + panda_mat.setCell(3, 2, 0) + panda_mat.setCell(3, 3, 1) + + def convert_mat(self, mat): + """ + 将OpenVR矩阵转换为Panda3D矩阵 - 基于参考实现 + """ + if len(mat.m) == 4: + result = LMatrix4( + mat.m[0][0], mat.m[1][0], mat.m[2][0], mat.m[3][0], + mat.m[0][1], mat.m[1][1], mat.m[2][1], mat.m[3][1], + mat.m[0][2], mat.m[1][2], mat.m[2][2], mat.m[3][2], + mat.m[0][3], mat.m[1][3], mat.m[2][3], mat.m[3][3]) + elif len(mat.m) == 3: + result = LMatrix4( + mat.m[0][0], mat.m[1][0], mat.m[2][0], 0.0, + mat.m[0][1], mat.m[1][1], mat.m[2][1], 0.0, + mat.m[0][2], mat.m[1][2], mat.m[2][2], 0.0, + mat.m[0][3], mat.m[1][3], mat.m[2][3], 1.0) + return result + + def is_vr_available(self): + """检查VR系统是否可用""" + if not OPENVR_AVAILABLE: + return False + + try: + # 检查SteamVR是否运行 + return openvr.isRuntimeInstalled() and openvr.isHmdPresent() + except Exception as e: + print(f"VR检查失败: {e}") + return False + + def initialize_vr(self): + """初始化VR系统""" + if not OPENVR_AVAILABLE: + print("❌ OpenVR不可用,无法初始化VR") + return False + + if self.vr_initialized: + print("VR系统已经初始化") + return True + + try: + print("🔄 正在初始化VR系统...") + + # 🚀 确保对象池已正确初始化(备用检查) + if not hasattr(self, '_matrix_pool') or len(self._matrix_pool) == 0: + print("⚠️ 对象池未初始化,正在重新初始化...") + self._initialize_object_pools() + + # 初始化OpenVR - 使用Scene应用类型确保正确的焦点管理 + self.vr_system = openvr.init(openvr.VRApplication_Scene) + if not self.vr_system: + print("❌ 无法初始化OpenVR系统") + return False + + # 获取compositor + self.vr_compositor = openvr.VRCompositor() + if not self.vr_compositor: + print("❌ 无法获取VR Compositor") + return False + + # 获取推荐的渲染目标尺寸 + base_width, base_height = self.vr_system.getRecommendedRenderTargetSize() + self.base_eye_width = base_width + self.base_eye_height = base_height + + # 应用分辨率缩放 + self.scaled_eye_width = int(base_width * self.resolution_scale) + self.scaled_eye_height = int(base_height * self.resolution_scale) + + # 使用缩放后的分辨率作为实际渲染分辨率 + self.eye_width = self.scaled_eye_width + self.eye_height = self.scaled_eye_height + + self.current_eye_resolution = (self.eye_width, self.eye_height) + self.recommended_eye_resolution = (base_width, base_height) + + print(f"✓ VR基础分辨率: {base_width}x{base_height}") + print(f"✓ VR缩放系数: {self.resolution_scale}") + print(f"✓ VR实际分辨率: {self.eye_width}x{self.eye_height}") + print(f"📊 分辨率优化: {(1 - self.resolution_scale**2) * 100:.1f}% 像素减少") + + # 获取VR系统信息 + try: + # 获取显示频率 + self.vr_display_frequency = self.vr_system.getFloatTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_DisplayFrequency_Float + ) + print(f"✓ VR显示频率: {self.vr_display_frequency} Hz") + + # 获取IPD(瞳距) + ipd = self.vr_system.getFloatTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_UserIpdMeters_Float + ) + print(f"✓ IPD(瞳距): {ipd * 1000:.1f}mm") + + # 获取设备制造商和型号 + manufacturer = self.vr_system.getStringTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_ManufacturerName_String + ) + model = self.vr_system.getStringTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_ModelNumber_String + ) + print(f"✓ VR设备: {manufacturer} {model}") + + # 获取更多系统配置信息 + try: + # 获取刷新率相关信息 + seconds_since_last_vsync = self.vr_system.getFloatTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_SecondsFromVsyncToPhotons_Float + ) + self.vsync_to_photons_ms = seconds_since_last_vsync * 1000 + print(f"✓ VSync到光子延迟: {self.vsync_to_photons_ms:.2f}ms") + + # 获取显示延迟信息 + display_refresh_rate = self.vr_display_frequency + if display_refresh_rate > 0: + self.target_frame_time_ms = 1000.0 / display_refresh_rate + print(f"✓ 目标帧时间: {self.target_frame_time_ms:.2f}ms") + + # 计算VSync时间窗口 + self.vsync_window_ms = self.target_frame_time_ms * 0.1 # 10%的窗口 + print(f"✓ VSync时间窗口: ±{self.vsync_window_ms:.2f}ms") + + except Exception as vsync_error: + print(f"⚠️ 获取VSync信息失败: {vsync_error}") + + # 检查是否启用了异步重投影 + try: + if hasattr(openvr, 'VRSettings'): + settings = openvr.VRSettings() + if settings: + self.async_reprojection_enabled = settings.getBool("steamvr", "enableAsyncReprojection") + print(f"✓ 异步重投影: {'启用' if self.async_reprojection_enabled else '禁用'}") + + self.motion_smoothing_enabled = settings.getBool("steamvr", "motionSmoothing") + print(f"✓ 运动平滑: {'启用' if self.motion_smoothing_enabled else '禁用'}") + + except Exception as settings_error: + print(f"⚠️ 获取VR设置失败: {settings_error}") + + except Exception as e: + print(f"⚠️ 获取VR系统信息失败: {e}") + + # 创建OpenVR姿态数组 - 分离渲染和游戏姿态 + poses_t = openvr.TrackedDevicePose_t * openvr.k_unMaxTrackedDeviceCount + self.poses = poses_t() # 渲染姿态(预测的) + self.game_poses = poses_t() # 游戏逻辑姿态(当前的) + print("✓ VR渲染和游戏姿态数组已创建") + + # 🔧 关键修复:统一初始化流程 + # 如果目标模式是RenderPipeline,先用普通模式初始化,然后切换 + # 这样两个场景都走相同的、已验证的代码路径 + target_render_mode = self.vr_render_mode + use_deferred_pipeline_switch = False + + if target_render_mode == VRRenderMode.RENDER_PIPELINE: + print("🎨 目标VR渲染模式: RenderPipeline") + print(" 策略:先用普通模式初始化,然后切换到RenderPipeline") + self.vr_render_mode = VRRenderMode.NORMAL + use_deferred_pipeline_switch = True + else: + print(f"🎨 VR渲染模式: {self.vr_render_mode.value}") + + # 创建VR渲染缓冲区 - 始终用普通模式初始化 + if not self._create_vr_buffers(): + print("❌ 创建VR渲染缓冲区失败") + return False + + # 设置VR相机 + if not self._setup_vr_cameras(): + print("❌ 设置VR相机失败") + return False + + # 优化VR渲染管线 + self._optimize_vr_rendering() + + # 初始化手柄控制器 + self._initialize_controllers() + + # 初始化动作系统 - 仅在启用时初始化 + if not self.disable_action_system and self.action_manager: + try: + if not self.action_manager.initialize(): + print("⚠️ VR动作系统初始化失败,但VR系统将继续运行") + except Exception as e: + print(f"⚠️ VR动作系统初始化异常: {e}") + else: + if self.disable_action_system: + print("🚫 VR动作系统已禁用,跳过初始化") + else: + print("⚠️ VR动作管理器未创建,跳过动作系统初始化") + + # 初始化交互系统 - 检查是否存在 + if self.interaction_manager: + try: + if not self.interaction_manager.initialize(): + print("⚠️ VR交互系统初始化失败,但VR系统将继续运行") + except Exception as e: + print(f"⚠️ VR交互系统初始化异常: {e}") + else: + print("⚠️ VR交互管理器未创建,跳过交互系统初始化") + + # 初始化VR传送系统 + if self.teleport_system: + try: + if not self.teleport_system.initialize(): + print("⚠️ VR传送系统初始化失败,但VR系统将继续运行") + except Exception as e: + print(f"⚠️ VR传送系统初始化异常: {e}") + else: + print("⚠️ VR传送系统未创建,跳过传送系统初始化") + + # 初始化VR摇杆系统 + if self.joystick_manager: + try: + # 传入传送系统引用给摇杆管理器 + self.joystick_manager.initialize(self.teleport_system) + print("✓ VR摇杆系统初始化成功") + except Exception as e: + print(f"⚠️ VR摇杆系统初始化异常: {e}") + else: + print("⚠️ VR摇杆管理器未创建,跳过摇杆系统初始化") + + # 可选:禁用异步重投影(备选方案) + if self.disable_async_reprojection: + self._disable_async_reprojection() + + # 启动VR更新任务 + self._start_vr_task() + + self.vr_initialized = True + print("✅ VR系统初始化成功(普通模式)") + + # 🔧 关键修复:如果目标是RenderPipeline模式,现在切换过去 + # 这样场景2(先设RP再进VR)会走场景1(先VR再设RP)相同的代码路径 + if use_deferred_pipeline_switch: + print("\n🔄 现在切换到目标渲染模式: RenderPipeline") + print(" 这将触发buffer重建和visualizer刷新...") + if self.set_vr_render_mode(target_render_mode): + print("✅ 已成功切换到RenderPipeline模式") + else: + print("⚠️ 切换到RenderPipeline失败,保持普通模式") + + return True + + except Exception as e: + print(f"❌ VR初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_vr_buffers(self): + """创建VR渲染缓冲区 - 使用分辨率缩放优化""" + try: + print(f"🔧 创建VR缓冲区:") + print(f" 推荐分辨率: {self.base_eye_width}x{self.base_eye_height}") + print(f" 缩放系数: {self.resolution_scale}") + print(f" 实际分辨率: {self.eye_width}x{self.eye_height}") + print(f" 像素减少: {(1 - self.resolution_scale**2) * 100:.1f}%") + + # 创建左眼纹理和缓冲区 + self.vr_left_texture = self._create_vr_texture("VR Left Eye Texture") + self.vr_left_eye_buffer = self._create_vr_buffer( + "VR Left Eye", + self.vr_left_texture, + self.eye_width, + self.eye_height + ) + + if not self.vr_left_eye_buffer: + print("❌ 创建左眼缓冲区失败") + return False + + # 设置左眼缓冲区属性 + self.vr_left_eye_buffer.setSort(-100) + self.vr_left_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) # 深蓝色背景便于调试 + self.vr_left_eye_buffer.setActive(True) + + # 创建右眼纹理和缓冲区 + self.vr_right_texture = self._create_vr_texture("VR Right Eye Texture") + self.vr_right_eye_buffer = self._create_vr_buffer( + "VR Right Eye", + self.vr_right_texture, + self.eye_width, + self.eye_height + ) + + if not self.vr_right_eye_buffer: + print("❌ 创建右眼缓冲区失败") + return False + + # 设置右眼缓冲区属性 + self.vr_right_eye_buffer.setSort(-99) + self.vr_right_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) # 深蓝色背景便于调试 + self.vr_right_eye_buffer.setActive(True) + + # 🚀 关键优化:立即准备纹理并缓存OpenGL ID,避免后续重复准备 + print("🔧 准备纹理并缓存OpenGL ID...") + if self._prepare_and_cache_textures(): + print("✅ VR渲染缓冲区和纹理缓存创建成功") + return True + else: + print("❌ 纹理准备失败") + return False + + except Exception as e: + print(f"❌ 创建VR缓冲区失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_vr_texture(self, name): + """创建VR纹理对象 - 基于参考实现""" + texture = Texture(name) + texture.setWrapU(Texture.WMClamp) + texture.setWrapV(Texture.WMClamp) + texture.setMinfilter(Texture.FTLinear) + texture.setMagfilter(Texture.FTLinear) + return texture + + def _prepare_and_cache_textures(self): + """准备纹理并缓存OpenGL ID - 解决重复准备问题""" + try: + # 🔧 验证纹理对象存在 + if not self.vr_left_texture or not self.vr_right_texture: + print("❌ VR纹理对象不存在") + print(f" 左眼纹理: {self.vr_left_texture}") + print(f" 右眼纹理: {self.vr_right_texture}") + return False + + # 获取graphics state guardian和prepared objects + gsg = self.world.win.getGsg() + if not gsg: + print("❌ 无法获取GraphicsStateGuardian") + return False + + prepared_objects = gsg.getPreparedObjects() + if not prepared_objects: + print("❌ 无法获取PreparedObjects") + return False + + # 准备左眼纹理并缓存ID + print(f" 准备左眼纹理: {self.vr_left_texture.getXSize()}x{self.vr_left_texture.getYSize()}") + texture_context = self.vr_left_texture.prepareNow(0, prepared_objects, gsg) + if texture_context and hasattr(texture_context, 'getNativeId'): + self.left_texture_id = texture_context.getNativeId() + if self.left_texture_id > 0: + print(f" ✅ 左眼纹理准备完成 ({self.vr_render_mode.value}): ID={self.left_texture_id}") + else: + print(" ❌ 左眼纹理ID无效") + return False + else: + print(" ❌ 左眼纹理准备失败") + return False + + # 准备右眼纹理并缓存ID + print(f" 准备右眼纹理: {self.vr_right_texture.getXSize()}x{self.vr_right_texture.getYSize()}") + texture_context = self.vr_right_texture.prepareNow(0, prepared_objects, gsg) + if texture_context and hasattr(texture_context, 'getNativeId'): + self.right_texture_id = texture_context.getNativeId() + if self.right_texture_id > 0: + print(f" ✅ 右眼纹理准备完成 ({self.vr_render_mode.value}): ID={self.right_texture_id}") + else: + print(" ❌ 右眼纹理ID无效") + return False + else: + print(" ❌ 右眼纹理准备失败") + return False + + # 标记纹理已准备 + self.textures_prepared = True + print(" ✅ 纹理准备完成,ID已缓存,后续帧将直接使用缓存ID") + return True + + except Exception as e: + print(f"❌ 纹理准备和缓存失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_vr_buffer(self, name, texture, width, height): + """创建VR渲染缓冲区 - 基于参考实现 + 性能诊断""" + # 设置帧缓冲属性 + fbprops = FrameBufferProperties() + fbprops.setRgbaBits(1, 1, 1, 1) + # 可以在这里添加多重采样抗锯齿 + # fbprops.setMultisamples(4) + + # 创建缓冲区 + buffer = self.world.win.makeTextureBuffer(name, width, height, to_ram=False, fbp=fbprops) + + if buffer: + # 🔍 性能诊断:检查缓冲区类型和属性 + self._diagnose_buffer_performance(buffer, name, width, height) + + # 清除默认渲染纹理 + buffer.clearRenderTextures() + # 🚀 使用RTMBindOrCopy模式 - 已经是最优选择 + # RTMBindOrCopy会尝试直接绑定到纹理,只有硬件不支持时才回退到复制 + # 这已经是Panda3D中最高效的渲染到纹理模式 + buffer.addRenderTexture(texture, GraphicsOutput.RTMBindOrCopy, GraphicsOutput.RTPColor) + + else: + print(f"⚠️ VR缓冲区创建失败: {name} ({width}x{height})") + + return buffer + + def _diagnose_buffer_performance(self, buffer, name, width, height): + """诊断VR缓冲区性能特性""" + try: + # 检查缓冲区类型 + buffer_type = type(buffer).__name__ + + # 检查是否为parasite buffer(性能较差的类型) + is_parasite = 'Parasite' in buffer_type + + # 获取缓冲区信息 + is_valid = buffer.isValid() + is_hardware = hasattr(buffer, 'getGsg') and buffer.getGsg() is not None + + # 输出诊断信息(仅第一次创建时) + if not hasattr(self, '_buffer_diagnosis_shown'): + print(f"🔍 VR缓冲区诊断 [{name}]:") + print(f" 类型: {buffer_type}") + print(f" 分辨率: {width}x{height} ({width*height/1000000:.1f}M像素)") + print(f" 有效性: {'✓' if is_valid else '✗'}") + print(f" 硬件支持: {'✓' if is_hardware else '✗'}") + + if is_parasite: + print(f" ⚠️ 检测到Parasite Buffer - 性能可能受限") + print(f" 建议: 检查显卡是否支持真正的离屏渲染") + else: + print(f" ✓ 使用硬件FBO - 性能良好") + + # 显示帧缓冲区属性 + if hasattr(buffer, 'getFbProperties'): + fbp = buffer.getFbProperties() + if fbp: + print(f" 颜色位数: R{fbp.getRedBits()}G{fbp.getGreenBits()}B{fbp.getBlueBits()}A{fbp.getAlphaBits()}") + if hasattr(fbp, 'getMultisamples') and fbp.getMultisamples() > 0: + print(f" MSAA: {fbp.getMultisamples()}x") + + self._buffer_diagnosis_shown = True + + except Exception as e: + print(f"缓冲区诊断失败: {e}") + + def _create_vr_buffers_with_pipeline(self): + """创建带RenderPipeline的VR渲染缓冲区 - 高级渲染模式""" + try: + print(f"🎨 创建RenderPipeline VR缓冲区:") + print(f" 推荐分辨率: {self.base_eye_width}x{self.base_eye_height}") + print(f" Pipeline缩放: {self.pipeline_resolution_scale}") + + # 检查RenderPipeline是否可用 + if not hasattr(self.world, 'render_pipeline') or not self.world.render_pipeline: + print("❌ RenderPipeline未初始化,无法使用高级渲染模式") + return False + + pipeline = self.world.render_pipeline + + # 计算RenderPipeline模式下的分辨率 + pipeline_width = int(self.base_eye_width * self.pipeline_resolution_scale) + pipeline_height = int(self.base_eye_height * self.pipeline_resolution_scale) + + print(f" 实际分辨率: {pipeline_width}x{pipeline_height}") + print(f" 像素减少: {(1 - self.pipeline_resolution_scale**2) * 100:.1f}%") + + # 导入RenderPipeline相关模块 + try: + from RenderPipelineFile.rpcore.render_target import RenderTarget + except ImportError as e: + print(f"❌ 无法导入RenderPipeline模块: {e}") + return False + + # 导入VR stages模块 + try: + from core.vr.rendering.stages import VRPipelineController + except ImportError as e: + print(f"❌ 无法导入VR stages模块: {e}") + return False + + # 创建VR Pipeline控制器 + print(" 初始化VR Pipeline控制器...") + self.vr_pipeline_controller = VRPipelineController(pipeline) + + # 保存分辨率信息(将在_setup_vr_cameras中使用) + self.pipeline_vr_width = pipeline_width + self.pipeline_vr_height = pipeline_height + + # 应用RenderPipeline效果配置 + self._apply_pipeline_vr_effects() + + self.render_pipeline_enabled = True + print("✅ VR Pipeline控制器已初始化(将在相机设置时创建完整管线)") + + # 检查天空盒状态 + self._check_skybox_status() + + return True + + except Exception as e: + print(f"❌ 创建RenderPipeline VR缓冲区失败: {e}") + import traceback + traceback.print_exc() + return False + + def _apply_pipeline_vr_effects(self): + """为VR场景应用RenderPipeline效果配置""" + try: + print("🎨 应用RenderPipeline VR优化配置...") + + # 根据配置调整RenderPipeline设置 + config = self.pipeline_vr_config + + # 注意:实际的插件启用/禁用需要在pipeline.yaml中配置 + # 这里只是记录当前的VR优化意图 + print(f" 阴影: {'启用' if config['enable_shadows'] else '禁用'}") + print(f" 环境光遮蔽: {'启用' if config['enable_ao'] else '禁用'}") + print(f" 泛光效果: {'启用' if config['enable_bloom'] else '禁用'} (VR推荐禁用)") + print(f" 运动模糊: {'启用' if config['enable_motion_blur'] else '禁用'} (VR推荐禁用)") + print(f" 屏幕空间反射: {'启用' if config['enable_ssr'] else '禁用'} (VR推荐禁用)") + + print("✅ RenderPipeline VR效果配置完成") + + except Exception as e: + print(f"⚠️ 应用RenderPipeline VR效果失败: {e}") + + def _check_skybox_status(self): + """检查并报告天空盒状态""" + try: + print("\n🌌 检查天空盒状态...") + + # 方法1:在render节点下搜索所有名字包含skybox的节点 + skybox_nodes = self.world.render.findAllMatches("**/skybox*") + if skybox_nodes and skybox_nodes.getNumPaths() > 0: + print(f" ✓ 找到 {skybox_nodes.getNumPaths()} 个天空盒节点(使用名称搜索)") + for i in range(skybox_nodes.getNumPaths()): + skybox = skybox_nodes.getPath(i) + print(f" 天空盒 #{i+1}:") + print(f" 名称: {skybox.getName()}") + print(f" 位置: {skybox.getPos()}") + print(f" 缩放: {skybox.getScale()}") + print(f" 父节点: {skybox.getParent().getName()}") + + # 检查是否有shader + if skybox.hasShader(): + print(f" ✓ 已应用shader效果") + else: + print(f" ⚠️ 未应用shader效果") + + # 检查bin设置 + if skybox.hasBin(): + print(f" Bin: {skybox.getBinName()}") + else: + print(" ⚠️ 未找到天空盒(名称搜索)") + + # 方法2:搜索所有GeomNode(几何节点),天空盒通常是一个大的几何体 + print(" 搜索所有几何节点...") + all_geoms = self.world.render.findAllMatches("**/+GeomNode") + print(f" 找到 {all_geoms.getNumPaths()} 个几何节点") + + # 查找大型几何体(缩放值很大,可能是天空盒) + skybox_candidates = [] + for i in range(all_geoms.getNumPaths()): + geom_node = all_geoms.getPath(i) + scale = geom_node.getScale() + # 天空盒通常缩放很大(如40000) + if scale[0] > 1000 or scale[1] > 1000 or scale[2] > 1000: + print(f" ✓ 找到大型几何体(可能是天空盒):") + print(f" 名称: {geom_node.getName()}") + print(f" 缩放: {scale}") + print(f" 位置: {geom_node.getPos()}") + if geom_node.hasShader(): + print(f" ✓ 已应用shader") + skybox_candidates.append(geom_node) + + # 方法3:检查render节点的所有直接子节点(显示全部) + print(" 检查render的所有直接子节点...") + render_children = self.world.render.getChildren() + print(f" render有 {render_children.getNumPaths()} 个直接子节点:") + for i in range(render_children.getNumPaths()): + child = render_children.getPath(i) + scale = child.getScale() + # 标记大型节点(可能是天空盒) + if scale[0] > 1000 or scale[1] > 1000 or scale[2] > 1000: + print(f" - {child.getName()} ⭐ (大型节点,缩放: {scale})") + else: + print(f" - {child.getName()}") + + # 报告结果但不自动创建 + if len(skybox_candidates) == 0: + print(" ⚠️ 未找到明显的天空盒(缩放>1000的几何体)") + print(" 💡 RenderPipeline的天空盒可能在上述列表中,但缩放值不同") + print(" 💡 或者可能RenderPipeline未在VR模式下自动创建天空盒") + else: + print(f" ✓ 找到 {len(skybox_candidates)} 个可能的天空盒") + + print("🌌 天空盒状态检查完成\n") + + except Exception as e: + print(f"⚠️ 天空盒状态检查失败: {e}") + import traceback + traceback.print_exc() + + def _create_vr_skybox(self): + """为VR创建天空盒""" + try: + print(" 🎨 创建VR天空盒...") + + # 检查RenderPipeline是否可用 + if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline: + pipeline = self.world.render_pipeline + + # 使用RenderPipeline的load_default_skybox方法 + print(" 使用RenderPipeline方法创建天空盒...") + skybox = pipeline.common_resources.load_default_skybox() + + if skybox: + # 设置天空盒属性 + skybox.set_scale(40000) # 大型缩放 + skybox.reparent_to(self.world.render) + skybox.set_bin("unsorted", 10000) # 最后渲染 + + # 应用天空盒shader效果 + pipeline.set_effect(skybox, "effects/skybox.yaml", { + "render_shadow": False, + "render_envmap": False, + "render_voxelize": False, + "alpha_testing": False, + "normal_mapping": False, + "parallax_mapping": False + }, 1000) + + print(f" ✅ VR天空盒已创建") + print(f" 名称: {skybox.getName()}") + print(f" 缩放: {skybox.getScale()}") + print(f" 位置: {skybox.getPos()}") + return skybox + else: + print(" ❌ 无法加载天空盒模型") + return None + else: + print(" ❌ RenderPipeline未初始化,无法创建天空盒") + return None + + except Exception as e: + print(f" ❌ 创建VR天空盒失败: {e}") + import traceback + traceback.print_exc() + return None + + def _setup_vr_cameras(self): + """设置VR相机 - 使用锚点层级系统""" + try: + # 创建VR追踪空间锚点层级 + self.tracking_space = self.world.render.attachNewNode('tracking-space') + self.hmd_anchor = self.tracking_space.attachNewNode('hmd-anchor') + self.left_eye_anchor = self.hmd_anchor.attachNewNode('left-eye') + self.right_eye_anchor = self.hmd_anchor.attachNewNode('right-eye') + + # 获取投影矩阵 + projection_left = self.coord_mat_inv * self.convert_mat( + self.vr_system.getProjectionMatrix(openvr.Eye_Left, self.near_clip, self.far_clip)) + projection_right = self.coord_mat_inv * self.convert_mat( + self.vr_system.getProjectionMatrix(openvr.Eye_Right, self.near_clip, self.far_clip)) + + # 创建左眼相机节点 + left_cam_node = Camera('left-cam') + left_lens = MatrixLens() + left_lens.setUserMat(projection_left) + left_cam_node.setLens(left_lens) + + # 创建右眼相机节点 + right_cam_node = Camera('right-cam') + right_lens = MatrixLens() + right_lens.setUserMat(projection_right) + right_cam_node.setLens(right_lens) + + # 附加相机到眼睛锚点 + self.vr_left_camera = self.left_eye_anchor.attachNewNode(left_cam_node) + self.vr_right_camera = self.right_eye_anchor.attachNewNode(right_cam_node) + + # 设置显示区域 - 区分RenderPipeline和普通模式 + if self.vr_render_mode == VRRenderMode.RENDER_PIPELINE and self.vr_pipeline_controller: + # RenderPipeline模式:使用VRPipelineController创建完整管线 + print(" 使用VR Pipeline创建立体渲染管线...") + + # 创建立体渲染管线(GBuffer + Lighting + Final stages) + success = self.vr_pipeline_controller.create_stereo_pipeline( + self.pipeline_vr_width, + self.pipeline_vr_height, + self.vr_left_camera, + self.vr_right_camera + ) + + if not success: + print("❌ VR Pipeline创建失败") + return False + + # 获取GBuffer的内部buffer(用于DisplayRegion) + self.vr_left_eye_buffer = self.vr_pipeline_controller.get_left_buffer() + self.vr_right_eye_buffer = self.vr_pipeline_controller.get_right_buffer() + + # 🔧 关键修复:验证buffer有效性 + if not self.vr_left_eye_buffer or not self.vr_right_eye_buffer: + print("❌ VR Pipeline buffer创建失败") + print(f" 左眼buffer: {self.vr_left_eye_buffer}") + print(f" 右眼buffer: {self.vr_right_eye_buffer}") + return False + + # 获取最终输出纹理(用于提交到OpenVR) + left_textures = self.vr_pipeline_controller.get_left_textures() + right_textures = self.vr_pipeline_controller.get_right_textures() + + if left_textures and right_textures: + self.vr_left_texture = left_textures["final"] + self.vr_right_texture = right_textures["final"] + else: + print("❌ 无法获取VR Pipeline输出纹理") + return False + + # 设置缓冲区排序和清除颜色 + if self.vr_left_eye_buffer: + self.vr_left_eye_buffer.setSort(-100) + self.vr_left_eye_buffer.setClearColorActive(True) + self.vr_left_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) + + if self.vr_right_eye_buffer: + self.vr_right_eye_buffer.setSort(-99) + self.vr_right_eye_buffer.setClearColorActive(True) + self.vr_right_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) + + # 🔧 关键修复:为RenderPipeline的GBuffer DisplayRegion设置DrawCallback + # 这确保纹理在渲染完成后被提交到OpenVR + if self.vr_left_eye_buffer and self.vr_right_eye_buffer: + print(" 设置RenderPipeline DisplayRegion回调...") + try: + # 🔧 验证DisplayRegion数量 + left_dr_count = self.vr_left_eye_buffer.getNumDisplayRegions() + right_dr_count = self.vr_right_eye_buffer.getNumDisplayRegions() + + print(f" 左眼buffer DisplayRegion数量: {left_dr_count}") + print(f" 右眼buffer DisplayRegion数量: {right_dr_count}") + + if left_dr_count == 0 or right_dr_count == 0: + print(" ❌ DisplayRegion未创建,无法设置回调") + return False + + # 获取RenderTarget创建的DisplayRegion + left_dr = self.vr_left_eye_buffer.get_display_region(0) + right_dr = self.vr_right_eye_buffer.get_display_region(0) + + # 验证DisplayRegion有效性 + if not left_dr or not right_dr: + print(" ❌ DisplayRegion无效") + return False + + # 设置渲染回调(与普通模式一致) + left_dr.setDrawCallback(PythonCallbackObject(self.simple_left_cb)) + right_dr.setDrawCallback(PythonCallbackObject(self.simple_right_cb)) + + # 确保DisplayRegion处于活动状态 + left_dr.setActive(True) + right_dr.setActive(True) + + print(" ✅ RenderPipeline DisplayRegion回调已设置") + except Exception as e: + print(f" ⚠️ 设置DisplayRegion回调失败: {e}") + import traceback + traceback.print_exc() + return False + + # 🔧 关键修复:强制同步GraphicsEngine,确保所有buffer完全初始化 + print(" 同步GraphicsEngine...") + self.world.graphicsEngine.renderFrame() + + # 准备纹理并缓存OpenGL ID + if not self._prepare_and_cache_textures(): + print("❌ RenderPipeline纹理准备失败") + return False + + print("✅ VR Pipeline立体渲染管线已准备完成") + else: + # 普通模式:创建新的DisplayRegion + print(" 使用普通渲染区域...") + left_dr = self.vr_left_eye_buffer.makeDisplayRegion() + left_dr.setCamera(self.vr_left_camera) + left_dr.setActive(True) + left_dr.setDrawCallback(PythonCallbackObject(self.simple_left_cb)) + + right_dr = self.vr_right_eye_buffer.makeDisplayRegion() + right_dr.setCamera(self.vr_right_camera) + right_dr.setActive(True) + right_dr.setDrawCallback(PythonCallbackObject(self.simple_right_cb)) + + print("✓ VR相机锚点层级系统设置完成") + return True + + except Exception as e: + print(f"❌ 设置VR相机失败: {e}") + import traceback + traceback.print_exc() + return False + + def _get_eye_offset(self, eye): + """获取眼睛相对于头显的偏移""" + try: + if not self.vr_system: + # 使用标准IPD(瞳距)估算值 + ipd = 0.064 # 64mm,平均IPD + if eye == openvr.Eye_Left: + return Vec3(-ipd/2, 0, 0) + else: + return Vec3(ipd/2, 0, 0) + + # 从OpenVR获取眼睛到头显的变换矩阵 + eye_transform = self.vr_system.getEyeToHeadTransform(eye) + + # 提取位移信息 + x = eye_transform[0][3] + y = eye_transform[1][3] + z = eye_transform[2][3] + + return Vec3(x, y, z) + + except Exception as e: + print(f"❌ 获取眼睛偏移失败: {e}") + # 返回默认值 + ipd = 0.064 + if eye == openvr.Eye_Left: + return Vec3(-ipd/2, 0, 0) + else: + return Vec3(ipd/2, 0, 0) + + def _optimize_vr_rendering(self): + """优化VR模式下的渲染管线 - 减少不必要的后处理开销""" + try: + print("🔧 正在优化VR渲染管线...") + + # 检查是否有RenderPipeline + if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline: + print(" 检测到RenderPipeline,正在优化...") + + # 对VR缓冲区应用轻量级渲染设置 + if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: + self._apply_lightweight_rendering(self.vr_left_eye_buffer, "左眼") + + if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: + self._apply_lightweight_rendering(self.vr_right_eye_buffer, "右眼") + + print("✅ VR渲染管线优化完成") + else: + print(" 未检测到RenderPipeline,使用默认Panda3D渲染") + + # 禁用VR缓冲区的不必要功能 + self._disable_vr_buffer_extras() + + except Exception as e: + print(f"⚠️ VR渲染优化失败: {e}") + import traceback + traceback.print_exc() + + def _apply_lightweight_rendering(self, buffer, eye_name): + """为VR缓冲区应用轻量级渲染设置""" + try: + # 禁用多重采样抗锯齿(MSAA)以提升性能 + if hasattr(buffer, 'setMultisample'): + buffer.setMultisample(0) + print(f" {eye_name}: 禁用MSAA") + + # 设置更简单的清除颜色操作 + buffer.setClearColorActive(True) + buffer.setClearColor((0.1, 0.2, 0.4, 1.0)) # 深蓝色,便于调试 + + # 禁用深度写入到某些不需要的缓冲区 + # (保留主要深度测试,但减少不必要的写入) + + print(f" {eye_name}: 应用轻量级渲染设置") + + except Exception as e: + print(f"⚠️ {eye_name}缓冲区优化失败: {e}") + + def _disable_vr_buffer_extras(self): + """禁用VR缓冲区的额外功能以提升性能""" + try: + # 禁用VR缓冲区的统计收集 + if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: + if hasattr(self.vr_left_eye_buffer, 'setOneShot'): + self.vr_left_eye_buffer.setOneShot(False) + + if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: + if hasattr(self.vr_right_eye_buffer, 'setOneShot'): + self.vr_right_eye_buffer.setOneShot(False) + + print(" VR缓冲区额外功能已优化") + + except Exception as e: + print(f"⚠️ VR缓冲区额外功能优化失败: {e}") + + def _start_vr_task(self): + """启动VR更新任务""" + if self.vr_task: + self.world.taskMgr.remove(self.vr_task) + + # 设置高优先级(sort=-1000),确保在渲染之前执行,参考 panda3d-openvr + self.vr_task = self.world.taskMgr.add(self._update_vr, "update_vr", sort=-1000) + print("✓ VR更新任务已启动(高优先级,sort=-1000)") + + def _update_vr(self, task): + """VR更新任务 - 每帧调用(参考 panda3d-openvr 架构)""" + if not self.vr_enabled or not self.vr_system: + return task.cont + + try: + # 📌 第一件事:waitGetPoses 阻塞等待 VSync(参考 panda3d-openvr) + # 这会阻塞整个任务,确保每个 VSync 周期只执行一次 + should_call_waitgetposes = ( + not self.vr_test_mode or # 普通VR模式总是需要 + self.test_mode_wait_poses # 测试模式:仅当启用姿态等待时 + ) + + if should_call_waitgetposes: + self._wait_get_poses_immediate() + # 立即更新手柄,使用最新姿态(避免过时数据) + self.update_tracked_devices() + + # 性能监控 + self.frame_count += 1 + + # 🚀 自动启用性能模式 - 减少计时对象创建 + if not self.performance_mode_enabled and self.frame_count >= self.performance_mode_trigger_frame: + self.performance_mode_enabled = True + if self.frame_count <= self.performance_mode_trigger_frame + 5: # 只输出一次 + print(f"🎯 性能模式已启用 (帧#{self.frame_count}) - 禁用详细监控以提升性能") + + # 记录帧时间 + self._track_frame_time() + + # 计算VR FPS + import time + current_time = time.time() + if self.last_fps_time == 0: + self.last_fps_time = current_time + elif current_time - self.last_fps_time >= 1.0: # 每秒更新一次FPS + self.vr_fps = (self.frame_count - self.last_fps_check) / (current_time - self.last_fps_time) + self.last_fps_check = self.frame_count + self.last_fps_time = current_time + + # 📌 使用刚获取的姿态更新相机(而不是旧姿态) + self._update_camera_poses() + + # 输出策略信息(仅第一次) + if not hasattr(self, '_new_architecture_logged'): + print("✅ 新架构已启用 - waitGetPoses 在任务开始阻塞,参考 panda3d-openvr") + print(" 这确保每个 VSync 周期只渲染一次,解决 AlreadySubmitted 错误") + self._new_architecture_logged = True + + # 更新VR动作状态 - 仅在启用时更新 + if not self.disable_action_system and self.action_manager: + self.action_manager.update_actions() + + # 更新VR交互系统 + if self.interaction_manager: + self.interaction_manager.update() + + # 更新VR摇杆系统 + if self.joystick_manager: + # 计算帧间隔时间 + import time + if not hasattr(self, '_last_frame_time'): + self._last_frame_time = time.time() + dt = 0.016 # 默认60fps + else: + current_time = time.time() + dt = current_time - self._last_frame_time + self._last_frame_time = current_time + # 限制dt范围,避免异常情况 + dt = max(0.001, min(0.1, dt)) + + self.joystick_manager.update(dt) + + # 更新系统性能指标(减少频率) + if self.frame_count % 30 == 1: # 每30帧更新一次,减少开销 + self._update_performance_metrics() + + # 更新GPU渲染时间统计(减少频率) + if self.enable_gpu_timing and self.frame_count % 60 == 1: + self._get_gpu_frame_timing() + + # 🚀 手动垃圾回收控制 - 避免16-19帧周期性峰值 + self._manual_gc_control() + + # 定期输出性能报告 - 默认10秒间隔 + report_interval = getattr(self, 'performance_report_interval', 600) + if self.frame_count % report_interval == 1: + self._print_performance_report() + + except Exception as e: + print(f"VR更新错误: {e}") + import traceback + traceback.print_exc() + + return task.cont + + def _sync_gpu_if_needed(self): + """可选的GPU同步 - 仅在需要时使用""" + try: + # 同步等待GPU完成(如果需要) + gsg = self.world.win.getGsg() + if gsg: + gsg.getEngine().syncFrame() + except Exception as e: + print(f"❌ GPU同步异常: {e}") + + def simple_left_cb(self, cbdata): + """简化的左眼渲染回调 - 精确控制渲染和提交""" + try: + # 🔍 精确测量渲染时间 + import time + render_start = time.perf_counter() + + # 触发实际渲染 + cbdata.upcall() + + # 计算真实渲染时间 + self.left_render_time = (time.perf_counter() - render_start) * 1000 # 转换为毫秒 + + # 记录渲染次数 + self.left_render_count += 1 + + # 📌 OpenVR 帧边界检查:防止同一 OpenVR 帧内重复渲染 + if self.left_eye_last_render_frame == self.openvr_frame_id: + return # 已在当前 OpenVR 帧渲染过,跳过 + + # 🔧 OpenVR最佳实践:左眼只渲染,不立即提交 + # 基于官方hellovr示例:两眼都渲染完后再批量提交 + self.left_eye_last_render_frame = self.openvr_frame_id + + # 渐进式VR功能测试:单独启用纹理提交时保留原逻辑 + should_submit = self.vr_test_mode and self.test_mode_submit_texture and not self.test_mode_wait_poses + + if should_submit: + # 测试模式:单独启用纹理提交时的兼容模式 + if self.vr_compositor and self.vr_left_texture: + self.submit_texture(openvr.Eye_Left, self.vr_left_texture) + + if self.vr_test_mode: + # 测试模式:始终触发屏幕显示更新 + self._update_test_display() + + except Exception as e: + print(f"左眼渲染回调错误: {e}") + + def simple_right_cb(self, cbdata): + """简化的右眼渲染回调 - 精确控制渲染和提交""" + try: + # 🔍 精确测量渲染时间 + import time + render_start = time.perf_counter() + + # 触发实际渲染 + cbdata.upcall() + + # 计算真实渲染时间 + self.right_render_time = (time.perf_counter() - render_start) * 1000 # 转换为毫秒 + + # 记录渲染次数 + self.right_render_count += 1 + + # 📌 OpenVR 帧边界检查:防止同一 OpenVR 帧内重复渲染 + if self.right_eye_last_render_frame == self.openvr_frame_id: + return # 已在当前 OpenVR 帧渲染过,跳过 + + # 🔧 OpenVR最佳实践:右眼渲染完成后批量提交两眼纹理 + # 基于官方hellovr示例:避免分散提交导致的VSync阻塞 + self.right_eye_last_render_frame = self.openvr_frame_id + + # 🔧 渐进式VR功能测试:根据调试标志决定启用哪些功能 + should_batch_submit = not self.vr_test_mode or self.test_mode_submit_texture + should_wait_poses = not self.vr_test_mode or self.test_mode_wait_poses + + # 特殊处理:测试模式单独启用功能时的兼容模式 + if self.vr_test_mode: + if self.test_mode_submit_texture and not self.test_mode_wait_poses: + # 单独测试纹理提交:使用原来的分散提交方式 + if self.vr_compositor and self.vr_right_texture: + self.submit_texture(openvr.Eye_Right, self.vr_right_texture) + + # 🚀 测试模式也需要PostPresentHandoff避免36FPS + try: + if hasattr(self.vr_compositor, 'postPresentHandoff'): + self.vr_compositor.postPresentHandoff() + elif hasattr(self.vr_compositor, 'PostPresentHandoff'): + self.vr_compositor.PostPresentHandoff() + except Exception as e: + pass # 测试模式静默忽略错误 + should_batch_submit = False + elif self.test_mode_wait_poses and not self.test_mode_submit_texture: + # 单独测试姿态等待:不提交纹理 + should_batch_submit = False + + if should_batch_submit: + # 🚀 OpenVR最佳实践:批量提交两眼纹理 + # 这是解决36FPS问题的关键修复 + self._batch_submit_textures() + + # 🔧 关键修复:移除Submit后立即WaitGetPoses的错误实现 + # 根据OpenVR官方文档:"Calling WaitGetPoses immediately after Submit is conspicuously wrong" + # WaitGetPoses应该在下一帧开始时通过update_vr_task调用,不是Submit后立即调用 + # + # if should_wait_poses: + # # 错误的实现:Submit后立即获取姿态导致36FPS + # self._wait_get_poses_immediate() # ← 这是36FPS的根本原因! + # + # 正确的实现:让update_vr_task在下一帧开始时调用WaitGetPoses + + if self.vr_test_mode: + # 测试模式:始终更新性能HUD + self._update_test_performance_hud() + + except Exception as e: + print(f"右眼渲染回调错误: {e}") + + def _wait_get_poses(self): + """调用VRCompositor的waitGetPoses来获取焦点和姿态数据""" + try: + if not self.vr_compositor or not self.poses: + return + + # 调用waitGetPoses获取焦点和姿态数据 + # 这个调用可能会阻塞直到下一个VR同步点 + result = self.vr_compositor.waitGetPoses(self.poses, None) + + # 检查姿态数据的有效性 + valid_poses = 0 + + # 更新HMD姿态(设备0通常是头显) + if len(self.poses) > 0 and self.poses[0].bPoseIsValid: + valid_poses += 1 + else: + # 如果HMD姿态无效,不要频繁输出错误信息 + if not hasattr(self, '_hmd_invalid_warning_shown'): + print("⚠️ HMD姿态数据无效") + self._hmd_invalid_warning_shown = True + + # 更新控制器姿态 + self.controller_poses.clear() + for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): + if self.poses[device_id].bPoseIsValid: + device_class = self.vr_system.getTrackedDeviceClass(device_id) + if device_class == openvr.TrackedDeviceClass_Controller: + controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking + self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) + valid_poses += 1 + + # 性能监控 - 偶尔输出姿态状态 + if self.frame_count % 600 == 1: # 每10秒输出一次@60fps + print(f"📊 VR姿态状态 - 有效姿态数: {valid_poses}, 总帧数: {self.frame_count}") + + except Exception as e: + # 限制错误输出频率 + if not hasattr(self, '_last_error_frame'): + self._last_error_frame = 0 + + if self.frame_count - self._last_error_frame > 300: # 每5秒最多输出一次错误 + print(f"waitGetPoses失败: {e}") + self._last_error_frame = self.frame_count + + # 记录姿态失败次数 + self.pose_failures += 1 + + def _wait_get_poses_immediate(self): + """立即获取VR姿态 - 修复ATW闪烁的关键方法(双姿态版本)""" + # 开始计时waitGetPoses操作 + timing = self._start_timing('wait_poses') + + try: + if not self.vr_compositor or not self.poses or not self.game_poses: + self._end_timing(timing) + return + + # 关键修复:传递渲染姿态和游戏姿态数组 + # 渲染姿态用于绘制,游戏姿态用于逻辑,避免ATW过度补偿 + result = self.vr_compositor.waitGetPoses(self.poses, self.game_poses) + + # 📌 waitGetPoses 成功后立即递增 OpenVR 帧ID(新帧开始) + self.openvr_frame_id += 1 + + # 结束计时 + wait_time = self._end_timing(timing) + + # 检查姿态数据的有效性 + valid_poses = 0 + + # 更新HMD姿态(设备0通常是头显) + if len(self.poses) > 0 and self.poses[0].bPoseIsValid: + valid_poses += 1 + else: + # 如果HMD姿态无效,不要频繁输出错误信息 + if not hasattr(self, '_hmd_invalid_warning_shown'): + print("⚠️ HMD姿态数据无效(立即模式)") + self._hmd_invalid_warning_shown = True + + # 🚀 优化控制器姿态更新:使用缓存,避免每帧clear()和重新创建对象 + for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): + if self.poses[device_id].bPoseIsValid: + device_class = self.vr_system.getTrackedDeviceClass(device_id) + if device_class == openvr.TrackedDeviceClass_Controller: + controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking + + # 检查是否已有此设备的缓存矩阵 + if device_id not in self.controller_poses: + # 第一次检测到该控制器,创建新的缓存矩阵 + self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) + else: + # 复用现有矩阵,只更新数值,避免创建新对象 + cached_mat = self.controller_poses[device_id] + self._update_matrix_from_openvr(cached_mat, controller_matrix) + + valid_poses += 1 + else: + # 设备姿态无效,从字典中移除(如果存在) + if device_id in self.controller_poses: + # 将矩阵返回对象池 + self._return_pooled_matrix(self.controller_poses[device_id]) + del self.controller_poses[device_id] + + # 🔧 关键修复:立即更新手柄和跟踪设备 + # Running Start模式下必须在WaitGetPoses后立即更新,避免手柄消失 + self.update_tracked_devices() + + # 调试信息 - 仅在第一次成功时输出 + if not hasattr(self, '_dual_pose_mode_logged') and valid_poses > 0: + print(f"✅ 双姿态立即获取模式启用 - 有效姿态数: {valid_poses}") + print(" 渲染姿态用于绘制,游戏姿态用于逻辑,防止ATW过度补偿") + print(" 手柄和跟踪设备在WaitGetPoses后立即更新") + self._dual_pose_mode_logged = True + + except Exception as e: + # 限制错误输出频率 + if not hasattr(self, '_last_immediate_error_frame'): + self._last_immediate_error_frame = 0 + + if self.frame_count - self._last_immediate_error_frame > 300: # 每5秒最多输出一次错误 + print(f"立即姿态获取失败: {e}") + self._last_immediate_error_frame = self.frame_count + + self.pose_failures += 1 + + def _wait_get_poses_with_prediction(self): + """使用预测时间获取VR姿态 - 优化性能的新策略""" + try: + if not self.vr_compositor or not self.poses or not self.game_poses: + return + + # 使用预测时间获取姿态 + # 预测时间通常为11-16ms,对应下一个VR帧的时间 + result = self.vr_compositor.waitGetPoses(self.poses, self.game_poses) + + # 检查姿态数据的有效性 + valid_poses = 0 + + # 更新HMD姿态(设备0通常是头显) + if len(self.poses) > 0 and self.poses[0].bPoseIsValid: + valid_poses += 1 + else: + # 如果HMD姿态无效,不要频繁输出错误信息 + if not hasattr(self, '_hmd_invalid_warning_task'): + print("⚠️ HMD姿态数据无效(更新任务模式)") + self._hmd_invalid_warning_task = True + + # 更新控制器姿态 + self.controller_poses.clear() + for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): + if self.poses[device_id].bPoseIsValid: + device_class = self.vr_system.getTrackedDeviceClass(device_id) + if device_class == openvr.TrackedDeviceClass_Controller: + controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking + self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) + valid_poses += 1 + + # 调试信息 - 仅在第一次成功时输出 + if not hasattr(self, '_update_task_mode_logged') and valid_poses > 0: + print(f"✅ 更新任务姿态获取模式启用 - 有效姿态数: {valid_poses}") + print(f" 预测时间: {self.use_prediction_time*1000:.1f}ms") + self._update_task_mode_logged = True + + except Exception as e: + # 限制错误输出频率 + if not hasattr(self, '_last_task_error_frame'): + self._last_task_error_frame = 0 + + if self.frame_count - self._last_task_error_frame > 300: # 每5秒最多输出一次错误 + print(f"更新任务姿态获取失败: {e}") + self._last_task_error_frame = self.frame_count + + # 记录姿态失败次数 + self.pose_failures += 1 + + def _cache_poses_for_next_frame(self): + """缓存当前姿态供下一帧渲染使用 - 修复时序不匹配""" + try: + if not self.poses or len(self.poses) == 0: + return + + # 如果是第一帧,直接使用当前姿态 + if self._first_frame: + self._cached_render_poses = self.poses + self._first_frame = False + print("✓ 首帧姿态缓存已设置") + return + + # 复制当前渲染姿态到缓存 + # 下一帧将使用这些姿态进行渲染 + poses_t = openvr.TrackedDevicePose_t * openvr.k_unMaxTrackedDeviceCount + cached_poses = poses_t() + + # 复制姿态数据 + for i in range(len(self.poses)): + cached_poses[i] = self.poses[i] + + self._cached_render_poses = cached_poses + + except Exception as e: + print(f"⚠️ 姿态缓存失败: {e}") + + + def _reset_waitgetposes_flag(self, task): + """重置WaitGetPoses标记 - 确保下一帧可以调用WaitGetPoses""" + self._waitgetposes_called_this_frame = False + return task.done + + def _update_tracking_data(self): + """更新VR追踪数据""" + try: + # 获取设备姿态 + poses = self.vr_system.getDeviceToAbsoluteTrackingPose( + openvr.TrackingUniverseStanding, 0.0, openvr.k_unMaxTrackedDeviceCount + ) + + # 更新HMD姿态(设备0通常是头显) + if poses[0].bPoseIsValid: + hmd_matrix = poses[0].mDeviceToAbsoluteTracking + self.hmd_pose = self._convert_openvr_matrix_to_panda(hmd_matrix) + + # 更新控制器姿态 + for device_id in range(1, openvr.k_unMaxTrackedDeviceCount): + if poses[device_id].bPoseIsValid: + device_class = self.vr_system.getTrackedDeviceClass(device_id) + if device_class == openvr.TrackedDeviceClass_Controller: + controller_matrix = poses[device_id].mDeviceToAbsoluteTracking + self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) + + except Exception as e: + print(f"更新追踪数据失败: {e}") + + def _convert_openvr_matrix_to_panda(self, ovr_matrix): + """将OpenVR矩阵转换为Panda3D矩阵 - 使用对象池优化 + + 坐标系转换: + OpenVR: X右, Y上, -Z前(右手坐标系) + Panda3D: X右, Y前, Z上(右手坐标系) + + 转换规则: + OpenVR X → Panda3D X + OpenVR Y → Panda3D Z + OpenVR -Z → Panda3D Y + """ + # 🚀 使用对象池获取预分配的Mat4对象,避免每帧创建新对象 + mat = self._get_pooled_matrix() + + # 修正的坐标转换矩阵 + # OpenVR: X右, Y上, -Z前 → Panda3D: X右, Y前, Z上 + # 转换规则: (ovr_x, ovr_y, ovr_z) → (panda_x, panda_y, panda_z) + # (ovr_x, ovr_y, ovr_z) → (ovr_x, -ovr_z, ovr_y) + + # X轴行:Panda3D的X轴对应OpenVR的X轴 + mat.setCell(0, 0, ovr_matrix[0][0]) # X_x → X_x + mat.setCell(0, 1, ovr_matrix[0][1]) # X_y → X_y + mat.setCell(0, 2, ovr_matrix[0][2]) # X_z → X_z + mat.setCell(0, 3, ovr_matrix[0][3]) # 位移X分量 + + # Y轴行:Panda3D的Y轴对应OpenVR的-Z轴 + mat.setCell(1, 0, -ovr_matrix[2][0]) # -Z_x → Y_x + mat.setCell(1, 1, -ovr_matrix[2][1]) # -Z_y → Y_y + mat.setCell(1, 2, -ovr_matrix[2][2]) # -Z_z → Y_z + mat.setCell(1, 3, -ovr_matrix[2][3]) # 位移Y分量(-Z位移) + + # Z轴行:Panda3D的Z轴对应OpenVR的Y轴 + mat.setCell(2, 0, ovr_matrix[1][0]) # Y_x → Z_x + mat.setCell(2, 1, ovr_matrix[1][1]) # Y_y → Z_y + mat.setCell(2, 2, ovr_matrix[1][2]) # Y_z → Z_z + mat.setCell(2, 3, ovr_matrix[1][3]) # 位移Z分量(Y位移) + + # 齐次坐标 + mat.setCell(3, 0, 0) + mat.setCell(3, 1, 0) + mat.setCell(3, 2, 0) + mat.setCell(3, 3, 1) + + # 🚀 优化调试信息 - 避免创建Vec3对象,减少GC压力 + if not hasattr(self, '_coord_debug_counter'): + self._coord_debug_counter = 0 + self._coord_debug_counter += 1 + + if self._coord_debug_counter % 600 == 1: # 每10秒输出一次@60fps + print(f"🔄 坐标系转换调试 (第{self._coord_debug_counter}帧)") + + # 直接输出数值,避免创建Vec3对象 + ovr_x, ovr_y, ovr_z = ovr_matrix[0][3], ovr_matrix[1][3], ovr_matrix[2][3] + print(f" OpenVR原始位置: ({ovr_x:.3f}, {ovr_y:.3f}, {ovr_z:.3f})") + + # 直接从矩阵读取数值,避免创建Vec3对象 + panda_x = mat.getCell(0, 3) + panda_y = mat.getCell(1, 3) + panda_z = mat.getCell(2, 3) + print(f" Panda3D转换位置: ({panda_x:.3f}, {panda_y:.3f}, {panda_z:.3f})") + + # 手动验证转换:OpenVR (x,y,z) → Panda3D (x,-z,y) + expected_x = ovr_x + expected_y = -ovr_z + expected_z = ovr_y + print(f" 预期转换结果: ({expected_x:.3f}, {expected_y:.3f}, {expected_z:.3f})") + + # 计算误差,避免创建Vec3对象 + diff_x = panda_x - expected_x + diff_y = panda_y - expected_y + diff_z = panda_z - expected_z + diff_magnitude = (diff_x*diff_x + diff_y*diff_y + diff_z*diff_z)**0.5 + + if diff_magnitude < 0.001: + print(f" ✅ 坐标转换正确 (误差: {diff_magnitude:.6f})") + else: + print(f" ⚠️ 坐标转换可能有误 (误差: {diff_magnitude:.6f})") + print(f" 差异: ({diff_x:.6f}, {diff_y:.6f}, {diff_z:.6f})") + + return mat + + def update_hmd(self, pose): + """ + 更新HMD锚点 - 基于参考实现 + """ + try: + # 将OpenVR姿态转换为Panda3D矩阵 + modelview = self.convert_mat(pose.mDeviceToAbsoluteTracking) + + # 应用坐标系转换并设置HMD锚点 + self.hmd_anchor.setMat(self.coord_mat_inv * modelview * self.coord_mat) + + # 获取眼睛到头部的变换 + view_left = self.convert_mat(self.vr_system.getEyeToHeadTransform(openvr.Eye_Left)) + view_right = self.convert_mat(self.vr_system.getEyeToHeadTransform(openvr.Eye_Right)) + + # 设置眼睛锚点 + self.left_eye_anchor.setMat(self.coord_mat_inv * view_left * self.coord_mat) + self.right_eye_anchor.setMat(self.coord_mat_inv * view_right * self.coord_mat) + + except Exception as e: + print(f"更新HMD姿态失败: {e}") + + def _update_camera_poses(self): + """更新相机姿态 - 使用锚点系统简化处理""" + try: + # 使用锚点系统后,相机位置自动跟随锚点 + # 只需要获取HMD姿态并更新锚点即可 + + # 从poses数组中获取HMD姿态 + if hasattr(self, 'poses') and len(self.poses) > 0: + hmd_pose = self.poses[openvr.k_unTrackedDeviceIndex_Hmd] + if hmd_pose.bPoseIsValid: + self.update_hmd(hmd_pose) + else: + print("⚠️ HMD姿态数据无效") + + except Exception as e: + print(f"更新相机姿态失败: {e}") + import traceback + traceback.print_exc() + + def _update_camera_poses_with_cache(self): + """使用缓存姿态更新相机 - 符合OpenVR时序假设""" + try: + # 使用缓存的姿态,符合OpenVR的时序假设 + # OpenVR假设你用上一次WaitGetPoses的姿态渲染当前提交的帧 + + if not self._cached_render_poses: + # 如果没有缓存姿态,回退到当前姿态(首帧情况) + print("⚠️ 没有缓存姿态,使用当前姿态") + return self._update_camera_poses() + + # 从缓存姿态数组中获取HMD姿态 + if len(self._cached_render_poses) > 0: + hmd_pose = self._cached_render_poses[openvr.k_unTrackedDeviceIndex_Hmd] + if hmd_pose.bPoseIsValid: + self.update_hmd(hmd_pose) + + # 调试信息 - 验证缓存姿态使用 + if not hasattr(self, '_cached_pose_logged'): + print("✅ 使用缓存姿态更新相机 - 符合OpenVR时序假设") + self._cached_pose_logged = True + else: + print("⚠️ 缓存的HMD姿态数据无效") + + except Exception as e: + print(f"使用缓存姿态更新相机失败: {e}") + # 回退到正常更新方式 + self._update_camera_poses() + import traceback + traceback.print_exc() + + + def enable_vr(self): + """启用VR模式""" + if not self.is_vr_available(): + print("❌ VR系统不可用") + return False + + if not self.vr_initialized: + if not self.initialize_vr(): + return False + + self.vr_enabled = True + + # 禁用主相机避免干扰VR渲染 + self._disable_main_cam() + + # VR性能优化:使用Running Start模式(Valve最佳实践) + print("🚀 VR性能优化:Running Start模式已启用") + print(" 优势:在帧开始时获取姿态,提供VSync前3ms的渲染时间") + print(" 注意:Submit后立即调用WaitGetPoses是错误实现") + self.set_prediction_time(11.0) # 11ms预测时间 - OpenVR标准值,平衡准确性和延迟 + + # 🚀 动态调整Qt Timer频率以支持VR + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + if hasattr(self.world.qtWidget, 'synchronizer'): + # 设置为144Hz,让OpenVR控制实际渲染节奏 + self.world.qtWidget.synchronizer.setInterval(int(1000/144)) + print("✓ Qt Timer调整为144Hz,让OpenVR控制VR渲染节奏") + + # 🔧 关键修复:检测并重建缺失的手柄visualizer + # 当渲染模式切换时,visualizer可能被清理但控制器对象仍存在 + if hasattr(self, 'left_controller') and self.left_controller: + if not self.left_controller.visualizer and self.left_controller.anchor_node: + print("🔧 检测到左手柄visualizer缺失,正在重建...") + self.left_controller._create_visualizer() + if self.left_controller.visualizer: + print("✅ 左手柄visualizer已重建") + + if hasattr(self, 'right_controller') and self.right_controller: + if not self.right_controller.visualizer and self.right_controller.anchor_node: + print("🔧 检测到右手柄visualizer缺失,正在重建...") + self.right_controller._create_visualizer() + if self.right_controller.visualizer: + print("✅ 右手柄visualizer已重建") + + print("✅ VR模式已启用") + return True + + def disable_vr(self): + """禁用VR模式""" + self.vr_enabled = False + + # 清理手柄可视化(但保留控制器对象以便重新启用) + if hasattr(self, 'left_controller') and self.left_controller: + if hasattr(self.left_controller, 'visualizer') and self.left_controller.visualizer: + try: + self.left_controller.visualizer.cleanup() + self.left_controller.visualizer = None + print("✓ 左手控制器可视化已清理") + except Exception as e: + print(f"⚠️ 清理左手控制器可视化失败: {e}") + + if hasattr(self, 'right_controller') and self.right_controller: + if hasattr(self.right_controller, 'visualizer') and self.right_controller.visualizer: + try: + self.right_controller.visualizer.cleanup() + self.right_controller.visualizer = None + print("✓ 右手控制器可视化已清理") + except Exception as e: + print(f"⚠️ 清理右手控制器可视化失败: {e}") + + # 隐藏手柄锚点节点 + if hasattr(self, 'left_controller') and self.left_controller and self.left_controller.anchor_node: + self.left_controller.anchor_node.hide() + + if hasattr(self, 'right_controller') and self.right_controller and self.right_controller.anchor_node: + self.right_controller.anchor_node.hide() + + # 恢复主相机 + self._enable_main_cam() + + # 恢复Qt Timer到60FPS + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + if hasattr(self.world.qtWidget, 'synchronizer'): + self.world.qtWidget.synchronizer.setInterval(int(1000/60)) + print("✓ Qt Timer恢复为60Hz") + + print("✅ VR模式已禁用,手柄模型已隐藏") + + def set_vr_render_mode(self, mode): + """切换VR渲染模式 + + Args: + mode: VRRenderMode枚举值或字符串 ('normal' 或 'render_pipeline') + + Returns: + bool: 切换是否成功 + """ + try: + # 转换输入为枚举类型 + if isinstance(mode, str): + mode_str = mode.lower() + if mode_str == "normal": + new_mode = VRRenderMode.NORMAL + elif mode_str in ["render_pipeline", "renderpipeline", "pipeline"]: + new_mode = VRRenderMode.RENDER_PIPELINE + else: + print(f"❌ 无效的渲染模式: {mode}") + print(" 支持的模式: 'normal' 或 'render_pipeline'") + return False + elif isinstance(mode, VRRenderMode): + new_mode = mode + else: + print(f"❌ 无效的模式类型: {type(mode)}") + return False + + # 检查是否与当前模式相同 + if new_mode == self.vr_render_mode: + print(f"✓ VR渲染模式已经是 {new_mode.value},无需切换") + return True + + print(f"🔄 正在切换VR渲染模式: {self.vr_render_mode.value} → {new_mode.value}") + + # 检查VR是否已初始化 + if not self.vr_initialized: + print("⚠️ VR未初始化,仅更新渲染模式配置") + self.vr_render_mode = new_mode + print(f"✓ VR渲染模式已更新为 {new_mode.value}(下次启动VR时生效)") + return True + + # 保存当前VR启用状态 + was_enabled = self.vr_enabled + + # 如果VR已启用,先禁用 + if was_enabled: + print(" 暂时禁用VR...") + self.disable_vr() + + # 更新渲染模式 + old_mode = self.vr_render_mode + self.vr_render_mode = new_mode + + # 清理现有缓冲区 + print(" 清理现有渲染缓冲区...") + self._cleanup_vr_buffers() + + # 根据新模式重建缓冲区 + print(f" 创建新的渲染缓冲区({new_mode.value})...") + success = False + + if new_mode == VRRenderMode.RENDER_PIPELINE: + success = self._create_vr_buffers_with_pipeline() + if not success: + print("⚠️ RenderPipeline模式创建失败,回退到普通渲染模式") + self.vr_render_mode = VRRenderMode.NORMAL + success = self._create_vr_buffers() + else: + success = self._create_vr_buffers() + + if not success: + print("❌ 缓冲区创建失败,尝试恢复原模式") + self.vr_render_mode = old_mode + if old_mode == VRRenderMode.RENDER_PIPELINE: + self._create_vr_buffers_with_pipeline() + else: + self._create_vr_buffers() + return False + + # 重新设置相机 + print(" 重新设置VR相机...") + if not self._setup_vr_cameras(): + print("❌ 相机设置失败") + return False + + # 如果之前VR是启用的,重新启用 + if was_enabled: + print(" 重新启用VR...") + self.enable_vr() + + # 🔧 关键修复:重建所有手柄的visualizer以适配新渲染模式 + print(" 刷新手柄visualizer以适配新渲染模式...") + if hasattr(self, 'left_controller') and self.left_controller: + self.left_controller.recreate_visualizer() + + if hasattr(self, 'right_controller') and self.right_controller: + self.right_controller.recreate_visualizer() + + print(f"✅ VR渲染模式已切换为 {self.vr_render_mode.value}") + + # 保存配置 + if self.config_manager: + self.config_manager.save_from_vr_manager(self) + + return True + + except Exception as e: + print(f"❌ 切换VR渲染模式失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_vr_render_mode(self): + """获取当前VR渲染模式 + + Returns: + VRRenderMode: 当前渲染模式 + """ + return self.vr_render_mode + + def _cleanup_vr_buffers(self): + """清理VR渲染缓冲区""" + try: + # 清理VR Pipeline Controller(如果使用RenderPipeline模式) + if self.vr_pipeline_controller: + print(" 清理VR Pipeline...") + self.vr_pipeline_controller.cleanup_all() + self.vr_pipeline_controller = None + + # 清理左眼缓冲区 + if self.vr_left_eye_buffer: + # 如果是RenderPipeline模式,buffer已被VRPipelineController清理 + # 如果是普通模式,需要手动清理 + if self.vr_render_mode == VRRenderMode.NORMAL: + self.world.graphicsEngine.removeWindow(self.vr_left_eye_buffer) + self.vr_left_eye_buffer = None + + # 清理右眼缓冲区 + if self.vr_right_eye_buffer: + if self.vr_render_mode == VRRenderMode.NORMAL: + self.world.graphicsEngine.removeWindow(self.vr_right_eye_buffer) + self.vr_right_eye_buffer = None + + # 清理纹理 + self.vr_left_texture = None + self.vr_right_texture = None + self.left_texture_id = None + self.right_texture_id = None + self.textures_prepared = False + + # 清理RenderPipeline渲染目标(旧版,保留兼容性) + if self.vr_pipeline_left_target: + self.vr_pipeline_left_target = None + + if self.vr_pipeline_right_target: + self.vr_pipeline_right_target = None + + self.render_pipeline_enabled = False + + print("✓ VR渲染缓冲区已清理") + + except Exception as e: + print(f"⚠️ 清理VR缓冲区时出错: {e}") + + def cleanup(self): + """清理VR资源""" + try: + print("🔄 正在清理VR资源...") + + # 停止VR任务 + if self.vr_task: + self.world.taskMgr.remove(self.vr_task) + self.vr_task = None + + # 🚀 恢复Python垃圾回收并清理对象池 + if self._gc_disabled: + # 最后一次手动垃圾回收 + collected = gc.collect() + print(f"🗑️ 最终GC清理: {collected} 个对象") + + # 恢复自动垃圾回收 + gc.enable() + self._gc_disabled = False + print("✅ Python垃圾回收已恢复为自动模式") + + # 清理对象池 + if hasattr(self, '_matrix_pool'): + pool_size = len(self._matrix_pool) + self._matrix_pool.clear() + print(f"🧹 Mat4对象池已清理: {pool_size} 个对象") + + # 清理缓存 + if hasattr(self, '_cached_matrices'): + self._cached_matrices.clear() + if hasattr(self, '_controller_poses_cache'): + self._controller_poses_cache.clear() + + # 清理OpenVR Texture对象缓存 + if hasattr(self, '_left_ovr_texture'): + self._left_ovr_texture = None + if hasattr(self, '_right_ovr_texture'): + self._right_ovr_texture = None + print("🧹 OpenVR Texture对象缓存已清理") + + # 清理渲染缓冲区 + if self.vr_left_eye_buffer: + self.vr_left_eye_buffer.removeAllDisplayRegions() + self.world.graphicsEngine.removeWindow(self.vr_left_eye_buffer) + self.vr_left_eye_buffer = None + + if self.vr_right_eye_buffer: + self.vr_right_eye_buffer.removeAllDisplayRegions() + self.world.graphicsEngine.removeWindow(self.vr_right_eye_buffer) + self.vr_right_eye_buffer = None + + # 清理相机 + if self.vr_left_camera: + self.vr_left_camera.removeNode() + self.vr_left_camera = None + + if self.vr_right_camera: + self.vr_right_camera.removeNode() + self.vr_right_camera = None + + # 清理控制器 + if hasattr(self, 'left_controller') and self.left_controller: + try: + self.left_controller.cleanup() + self.left_controller = None + print("✓ 左手控制器已清理") + except Exception as e: + print(f"⚠️ 清理左手控制器失败: {e}") + + if hasattr(self, 'right_controller') and self.right_controller: + try: + self.right_controller.cleanup() + self.right_controller = None + print("✓ 右手控制器已清理") + except Exception as e: + print(f"⚠️ 清理右手控制器失败: {e}") + + # 清理控制器字典 + if hasattr(self, 'controllers'): + self.controllers.clear() + print("✓ 控制器字典已清理") + + # 清理VR子系统 + if hasattr(self, 'joystick_manager') and self.joystick_manager: + try: + self.joystick_manager.cleanup() + self.joystick_manager = None + print("✓ VR摇杆系统已清理") + except Exception as e: + print(f"⚠️ 清理VR摇杆系统失败: {e}") + + if hasattr(self, 'teleport_system') and self.teleport_system: + try: + self.teleport_system.cleanup() + self.teleport_system = None + print("✓ VR传送系统已清理") + except Exception as e: + print(f"⚠️ 清理VR传送系统失败: {e}") + + if hasattr(self, 'interaction_manager') and self.interaction_manager: + try: + self.interaction_manager.cleanup() + self.interaction_manager = None + print("✓ VR交互系统已清理") + except Exception as e: + print(f"⚠️ 清理VR交互系统失败: {e}") + + if hasattr(self, 'action_manager') and self.action_manager: + try: + self.action_manager.cleanup() + self.action_manager = None + print("✓ VR动作系统已清理") + except Exception as e: + print(f"⚠️ 清理VR动作系统失败: {e}") + + # 关闭OpenVR + if self.vr_system and OPENVR_AVAILABLE: + try: + openvr.shutdown() + except: + pass + self.vr_system = None + + self.vr_enabled = False + self.vr_initialized = False + + print("✅ VR资源清理完成") + + except Exception as e: + print(f"⚠️ VR清理过程中出错: {e}") + + def get_vr_status(self): + """获取VR状态信息""" + return { + 'available': self.is_vr_available(), + 'initialized': self.vr_initialized, + 'enabled': self.vr_enabled, + 'eye_resolution': (self.eye_width, self.eye_height), + 'device_count': len(self.controller_poses) + (1 if self.vr_enabled else 0), + 'vr_fps': self.vr_fps, + 'frame_count': self.frame_count, + 'submit_failures': self.submit_failures, + 'pose_failures': self.pose_failures + } + + def _print_performance_report(self): + """输出VR性能报告""" + if not self.performance_monitoring or not self.debug_output_enabled: + return + + stats = self.get_performance_stats() + + # 简短模式输出 + if self.debug_mode == 'brief': + self._print_brief_performance_report(stats) + return + + print("📊 ======= VR性能监控报告 =======") + + # 帧率和帧时间信息 + print(f"🎯 渲染性能:") + print(f" VR帧率: {stats['vr_fps']:.1f} FPS") + print(f" 平均帧时间: {stats['frame_time_avg']:.2f} ms") + print(f" 最小帧时间: {stats['frame_time_min']:.2f} ms") + print(f" 最大帧时间: {stats['frame_time_max']:.2f} ms") + print(f" 95%帧时间: {stats['frame_time_95th']:.2f} ms") + + # 系统性能 + print(f"💻 系统性能:") + print(f" CPU使用率: {stats['cpu_usage']:.1f}%") + print(f" 内存使用率: {stats['memory_usage']:.1f}%") + + # GPU性能 + print(f"🎮 GPU性能:") + if self.gputil_available or self.nvidia_ml_available: + print(f" GPU使用率: {stats['gpu_usage']:.1f}%") + print(f" 显存使用率: {stats['gpu_memory_usage']:.1f}%") + else: + print(f" GPU监控: 不可用 (需要安装 GPUtil 或 pynvml)") + print(f" 安装命令: pip install GPUtil nvidia-ml-py") + + # GPU渲染时间(OpenVR Frame Timing) + if self.enable_gpu_timing: + print(f"⚡ GPU渲染时间:") + pipeline_stats = self._get_pipeline_stats() + gpu_stats = pipeline_stats.get('gpu_timing', {}) + gpu_current = pipeline_stats.get('current', {}) + + # 检查是否有可用的GPU时间数据 + has_gpu_data = any( + gpu_current.get(field, 0) > 0 + for field in ['gpu_scene_render', 'gpu_total_render', 'gpu_pre_submit', 'gpu_post_submit', 'gpu_compositor_render'] + ) + + if has_gpu_data: + # 显示GPU时间统计(最近30帧平均) + scene_render = gpu_stats.get('scene_render', {'avg': 0}) + total_render = gpu_stats.get('total_render', {'avg': 0}) + pre_submit = gpu_stats.get('pre_submit', {'avg': 0}) + post_submit = gpu_stats.get('post_submit', {'avg': 0}) + compositor = gpu_stats.get('compositor_render', {'avg': 0}) + + if scene_render['avg'] > 0: + print(f" GPU场景渲染: {scene_render['avg']:.2f}ms (min:{scene_render['min']:.1f}, max:{scene_render['max']:.1f})") + if total_render['avg'] > 0: + print(f" GPU总渲染时间: {total_render['avg']:.2f}ms (min:{total_render['min']:.1f}, max:{total_render['max']:.1f})") + if pre_submit['avg'] > 0: + print(f" GPU提交前时间: {pre_submit['avg']:.2f}ms (min:{pre_submit['min']:.1f}, max:{pre_submit['max']:.1f})") + if post_submit['avg'] > 0: + print(f" GPU提交后时间: {post_submit['avg']:.2f}ms (min:{post_submit['min']:.1f}, max:{post_submit['max']:.1f})") + if compositor['avg'] > 0: + print(f" GPU合成器时间: {compositor['avg']:.2f}ms (min:{compositor['min']:.1f}, max:{compositor['max']:.1f})") + + # 显示当前帧GPU时间 + print(f"🔍 当前帧GPU时间:") + if gpu_current.get('gpu_scene_render', 0) > 0: + print(f" 场景渲染: {gpu_current['gpu_scene_render']:.2f}ms") + if gpu_current.get('gpu_total_render', 0) > 0: + print(f" 总渲染: {gpu_current['gpu_total_render']:.2f}ms") + if gpu_current.get('gpu_compositor_render', 0) > 0: + print(f" 合成器: {gpu_current['gpu_compositor_render']:.2f}ms") + + # GPU时间瓶颈分析 + current_total = gpu_current.get('gpu_total_render', 0) + current_scene = gpu_current.get('gpu_scene_render', 0) + if current_total > 12.0: # 假设72fps目标,留出一些余量 + print(f" ⚠️ GPU总渲染时间过长: {current_total:.1f}ms") + elif current_scene > 8.0: + print(f" ⚠️ GPU场景渲染时间偏高: {current_scene:.1f}ms") + + else: + print(f" GPU渲染时间: 暂无数据") + if self.gpu_timing_failure_count > 0: + print(f" 获取失败次数: {self.gpu_timing_failure_count}") + else: + print(f" 等待OpenVR Frame Timing数据...") + else: + print(f"⚡ GPU渲染时间: 已禁用") + + # VR特定指标 + print(f"🥽 VR指标:") + print(f" 总帧数: {stats['frame_count']}") + print(f" 提交失败: {stats['submit_failures']}") + print(f" 姿态失败: {stats['pose_failures']}") + + # 计算失败率 + if stats['frame_count'] > 0: + submit_fail_rate = (stats['submit_failures'] / stats['frame_count']) * 100 + pose_fail_rate = (stats['pose_failures'] / stats['frame_count']) * 100 + print(f" 提交失败率: {submit_fail_rate:.2f}%") + print(f" 姿态失败率: {pose_fail_rate:.2f}%") + + # 渲染管线监控 + if self.enable_pipeline_monitoring: + pipeline_stats = self._get_pipeline_stats() + print(f"⚙️ 渲染管线分析:") + print(f" 当前分辨率: {pipeline_stats['vr_info']['eye_resolution'][0]}x{pipeline_stats['vr_info']['eye_resolution'][1]}") + print(f" 推荐分辨率: {pipeline_stats['vr_info']['recommended_resolution'][0]}x{pipeline_stats['vr_info']['recommended_resolution'][1]}") + print(f" 显示频率: {pipeline_stats['vr_info']['display_frequency']:.1f} Hz") + + # VSync和时序信息 + if pipeline_stats['vr_info']['target_frame_time_ms'] > 0: + print(f"🎯 VSync时序:") + print(f" 目标帧时间: {pipeline_stats['vr_info']['target_frame_time_ms']:.2f}ms") + print(f" VSync到光子: {pipeline_stats['vr_info']['vsync_to_photons_ms']:.2f}ms") + print(f" VSync窗口: ±{pipeline_stats['vr_info']['vsync_window_ms']:.2f}ms") + print(f" 异步重投影: {'启用' if pipeline_stats['vr_info']['async_reprojection'] else '禁用'}") + print(f" 运动平滑: {'启用' if pipeline_stats['vr_info']['motion_smoothing'] else '禁用'}") + + # 分析帧时间是否在目标范围内 + current_frame_time = stats['frame_time_avg'] + target_frame_time = pipeline_stats['vr_info']['target_frame_time_ms'] + if current_frame_time > 0 and target_frame_time > 0: + frame_time_ratio = current_frame_time / target_frame_time + if frame_time_ratio > 1.1: + print(f" ⚠️ 帧时间超标: {current_frame_time:.1f}ms (目标:{target_frame_time:.1f}ms)") + elif frame_time_ratio < 0.9: + print(f" ✅ 帧时间充裕: {current_frame_time:.1f}ms (目标:{target_frame_time:.1f}ms)") + else: + print(f" ✓ 帧时间正常: {current_frame_time:.1f}ms (目标:{target_frame_time:.1f}ms)") + + # 🔧 性能优化诊断 + print(f"🔧 优化诊断:") + current = pipeline_stats.get('current', {}) if self.enable_pipeline_monitoring else {} + + # 🔍 渲染回调诊断 - 新增 + self._print_render_callback_diagnostics() + + # waitGetPoses时序分析 + wait_poses_time = current.get('wait_poses', 0) + if wait_poses_time > 10: + print(f" ⚠️ waitGetPoses时间过长: {wait_poses_time:.1f}ms") + print(f" 可能原因: 错过VSync窗口,建议调整预测时间") + elif wait_poses_time > 5: + print(f" ⚠️ waitGetPoses时间偏高: {wait_poses_time:.1f}ms") + else: + print(f" ✓ waitGetPoses时间正常: {wait_poses_time:.1f}ms") + + # CPU-GPU并行度分析 - 增强诊断 + gpu_total = current.get('gpu_total_render', 0) + cpu_render_left = getattr(self, 'left_render_time', 0) + cpu_render_right = getattr(self, 'right_render_time', 0) + cpu_render_total = cpu_render_left + cpu_render_right + + print(f" 真实渲染时间对比:") + print(f" CPU左眼: {cpu_render_left:.2f}ms") + print(f" CPU右眼: {cpu_render_right:.2f}ms") + print(f" CPU总计: {cpu_render_total:.2f}ms") + print(f" GPU总计: {gpu_total:.2f}ms") + + if cpu_render_total < 1.0: + print(f" ⚠️ CPU渲染时间异常短: {cpu_render_total:.2f}ms") + print(f" 可能原因: 渲染回调未正确执行或渲染被跳过") + elif gpu_total > 0 and cpu_render_total > 0: + ratio = gpu_total / cpu_render_total + if ratio > 10: + print(f" ⚠️ GPU严重等待CPU: 比例{ratio:.1f}:1") + print(f" 建议: 检查OpenGL命令提交时机和渲染状态") + elif ratio > 3: + print(f" ⚠️ GPU等待CPU: 比例{ratio:.1f}:1") + print(f" 建议: 检查OpenGL命令提交是否及时") + else: + print(f" ✓ CPU-GPU时间匹配: 比例{ratio:.1f}:1") + + # 预测时间诊断 + current_prediction = self.use_prediction_time * 1000 + print(f" 预测时间设置: {current_prediction:.1f}ms") + if current_prediction > 15: + print(f" 建议: 预测时间较高,可能增加waitGetPoses延迟") + elif current_prediction < 8: + print(f" 注意: 预测时间较低,可能影响姿态准确性") + else: + print(f" ✓ 预测时间在合理范围内") + + # 优化状态总结 + optimization_score = 0 + if wait_poses_time < 8: + optimization_score += 1 + if stats['vr_fps'] > 60: + optimization_score += 1 + if stats.get('frame_time_avg', 0) < target_frame_time * 1.1: + optimization_score += 1 + + if optimization_score >= 2: + print(f" ✅ 优化效果良好 ({optimization_score}/3)") + else: + print(f" ⚠️ 仍有优化空间 ({optimization_score}/3)") + + print(f"🕐 各阶段耗时 (最近{self.pipeline_history_size}帧平均):") + print(f" waitGetPoses: {pipeline_stats['wait_poses']['avg']:.2f}ms (min:{pipeline_stats['wait_poses']['min']:.1f}, max:{pipeline_stats['wait_poses']['max']:.1f})") + print(f" 渲染总计: {pipeline_stats['render']['avg']:.2f}ms (min:{pipeline_stats['render']['min']:.1f}, max:{pipeline_stats['render']['max']:.1f})") + print(f" 纹理提交: {pipeline_stats['submit']['avg']:.2f}ms (min:{pipeline_stats['submit']['min']:.1f}, max:{pipeline_stats['submit']['max']:.1f})") + if pipeline_stats['sync_wait']['avg'] > 0: + print(f" 同步等待: {pipeline_stats['sync_wait']['avg']:.2f}ms (min:{pipeline_stats['sync_wait']['min']:.1f}, max:{pipeline_stats['sync_wait']['max']:.1f})") + + print(f"🔍 当前帧详情:") + print(f" 左眼渲染: {pipeline_stats['current']['left_render']:.2f}ms") + print(f" 右眼渲染: {pipeline_stats['current']['right_render']:.2f}ms") + print(f" 姿态获取: {pipeline_stats['current']['wait_poses']:.2f}ms") + + # 显示Running Start模式信息 + print(f"🎯 Running Start模式:") + print(f" 模式: Valve Running Start - 帧开始时获取姿态") + print(f" 优势: VSync前3ms获取姿态,提供充足渲染时间") + print(f" 预测时间: {self.use_prediction_time * 1000:.1f}ms") + + # 分析最大瓶颈 + current = pipeline_stats['current'] + bottleneck_analysis = [] + if current['wait_poses'] > 5.0: + bottleneck_analysis.append(f"姿态获取耗时过长({current['wait_poses']:.1f}ms)") + if current['total_render'] > 8.0: + bottleneck_analysis.append(f"渲染耗时过长({current['total_render']:.1f}ms)") + if current['submit'] > 2.0: + bottleneck_analysis.append(f"纹理提交耗时过长({current['submit']:.1f}ms)") + + if bottleneck_analysis: + print(f"🚨 瓶颈分析:") + for analysis in bottleneck_analysis: + print(f" ⚠️ {analysis}") + + # 性能建议 + self._print_performance_recommendations(stats) + + print("===============================") + + def _print_performance_recommendations(self, stats): + """根据性能数据输出优化建议""" + print(f"💡 性能建议:") + + recommendations = [] + + # FPS相关建议 + if stats['vr_fps'] < 60: + recommendations.append(" ⚠️ VR帧率过低,可能影响体验") + + # 帧时间相关建议 + if stats['frame_time_avg'] > 16.7: # 60fps = 16.7ms + recommendations.append(" ⚠️ 平均帧时间过高,建议降低渲染质量") + + if stats['frame_time_max'] > 50: + recommendations.append(" ⚠️ 检测到严重卡顿,检查CPU/GPU负载") + + # CPU建议 + if stats['cpu_usage'] > 80: + recommendations.append(" 🔴 CPU使用率过高,可能存在CPU瓶颈") + elif stats['cpu_usage'] > 60: + recommendations.append(" 🟡 CPU使用率偏高,注意监控") + + # 内存建议 + if stats['memory_usage'] > 85: + recommendations.append(" 🔴 内存使用率过高,可能影响性能") + + # GPU建议 + if self.gputil_available or self.nvidia_ml_available: + if stats['gpu_usage'] > 95: + recommendations.append(" 🔴 GPU使用率接近满载,存在GPU瓶颈") + if stats['gpu_memory_usage'] > 90: + recommendations.append(" 🔴 显存使用率过高,可能需要降低纹理质量") + + # GPU渲染时间建议 + if self.enable_gpu_timing: + if self.gpu_total_render_ms > 12.0: + recommendations.append(" ⚠️ GPU总渲染时间过长,建议优化场景复杂度") + if self.gpu_scene_render_ms > 8.0: + recommendations.append(" ⚠️ GPU场景渲染时间偏高,考虑降低渲染质量") + if self.gpu_compositor_render_ms > 3.0: + recommendations.append(" ⚠️ GPU合成器时间过长,检查VR设置或叠加层") + if self.gpu_timing_failure_count > 100: + recommendations.append(" ⚠️ GPU时间统计频繁失败,可能需要更新OpenVR") + + # 失败率建议 + submit_fail_rate = (stats['submit_failures'] / max(stats['frame_count'], 1)) * 100 + if submit_fail_rate > 1: + recommendations.append(" ⚠️ VR帧提交失败率较高,检查VR系统状态") + + if not recommendations: + recommendations.append(" ✅ 性能表现良好,无明显问题") + + for rec in recommendations: + print(rec) + + def _print_brief_performance_report(self, stats): + """输出简短的VR性能报告""" + # 创建一行简短摘要 + summary = f"🥽 VR性能: {stats['vr_fps']:.1f}fps" + + if stats['frame_time_avg'] > 0: + summary += f" | 帧时间: {stats['frame_time_avg']:.1f}ms" + + if self.psutil_available: + summary += f" | CPU: {stats['cpu_usage']:.0f}%" + summary += f" | 内存: {stats['memory_usage']:.0f}%" + + # 显示GPU信息,如果库不可用则显示提示 + if self.gputil_available or self.nvidia_ml_available: + summary += f" | GPU: {stats['gpu_usage']:.0f}%" + else: + summary += " | GPU: N/A" + + # 添加失败率指示 + if stats['frame_count'] > 0: + submit_fail_rate = (stats['submit_failures'] / stats['frame_count']) * 100 + if submit_fail_rate > 0.1: + summary += f" | 提交失败: {submit_fail_rate:.1f}%" + + # 添加管线关键信息 + if self.enable_pipeline_monitoring: + pipeline_stats = self._get_pipeline_stats() + current = pipeline_stats['current'] + + # 显示关键瓶颈 + if current['wait_poses'] > 5.0: + summary += f" | 姿态: {current['wait_poses']:.1f}ms⚠️" + elif current['wait_poses'] > 0: + summary += f" | 姿态: {current['wait_poses']:.1f}ms" + + if current['total_render'] > 8.0: + summary += f" | 渲染: {current['total_render']:.1f}ms⚠️" + elif current['total_render'] > 0: + summary += f" | 渲染: {current['total_render']:.1f}ms" + + # 添加GPU渲染时间信息 + if self.enable_gpu_timing: + gpu_total = current.get('gpu_total_render', 0) + gpu_scene = current.get('gpu_scene_render', 0) + if gpu_total > 12.0: + summary += f" | GPU: {gpu_total:.1f}ms⚠️" + elif gpu_total > 0: + summary += f" | GPU: {gpu_total:.1f}ms" + elif gpu_scene > 0: + summary += f" | GPU场景: {gpu_scene:.1f}ms" + + # 显示目标帧时间对比 + vr_info = pipeline_stats['vr_info'] + if vr_info['target_frame_time_ms'] > 0: + target = vr_info['target_frame_time_ms'] + current_avg = stats['frame_time_avg'] + if current_avg > target * 1.1: + summary += f" | 目标:{target:.0f}ms⚠️" + else: + summary += f" | 目标:{target:.0f}ms" + + # 性能状态指示 + if stats['vr_fps'] < 72: + summary += " ⚠️" + elif stats['vr_fps'] > 85: + summary += " ✅" + + print(summary) + + # 注意:原来的left_cb和right_cb函数已被删除 + # 它们的功能已集成到_handle_vr_rendering_and_submit方法中 + + # 注意:_safe_submit_texture方法已删除 + # VR纹理提交现在完全由Panda3D的renderFrame()自动处理 + + def submit_texture(self, eye, texture): + """优化的VR纹理提交 - 使用缓存的纹理ID,避免重复prepareNow""" + try: + if not self.vr_compositor: + print("❌ VR compositor不可用") + self.submit_failures += 1 + return + + # 🚀 关键修复:防止同一帧重复提交 + current_frame = getattr(self, 'frame_count', 0) + if not hasattr(self, '_last_submit_frame'): + self._last_submit_frame = {} + + # 检查当前帧是否已经提交过此眼睛的纹理 + if eye in self._last_submit_frame and self._last_submit_frame[eye] == current_frame: + # 静默跳过,避免spam输出 + return + + # 记录当前帧提交 + self._last_submit_frame[eye] = current_frame + + # 🚀 关键优化:直接使用缓存的纹理ID,避免重复prepareNow + if eye == openvr.Eye_Left: + handle = self.left_texture_id + eye_name = "左眼" + elif eye == openvr.Eye_Right: + handle = self.right_texture_id + eye_name = "右眼" + else: + print(f"❌ 未知的眼睛类型: {eye}") + self.submit_failures += 1 + return + + # 检查缓存的纹理ID是否有效 + if not handle or handle <= 0: + print(f"❌ {eye_name}纹理ID缓存无效: {handle}") + print(" 这可能表示纹理准备失败,需要检查_prepare_and_cache_textures()") + self.submit_failures += 1 + return + + # ❌ 移除gsg.flush()调用 - 基于OpenVR官方实践 + # gsg.flush()等同于OpenGL的glFlush(),导致强制CPU-GPU同步 + # 每帧调用2次(左右眼)是GPU周期性峰值的主要原因 + # 参考:OpenVR社区经验表明同步调用是性能杀手 + # + # gsg = self.world.win.getGsg() + # if gsg and hasattr(gsg, 'flush'): + # try: + # gsg.flush() + # except Exception as flush_error: + # if not hasattr(self, '_gsg_flush_error_logged'): + # print(f"⚠️ GSG刷新失败: {flush_error}") + # self._gsg_flush_error_logged = True + + # 🚀 关键优化:使用缓存的OpenVR Texture对象,避免每帧创建 + if eye == openvr.Eye_Left: + ovr_texture = self._left_ovr_texture + else: + ovr_texture = self._right_ovr_texture + + # 检查缓存对象是否存在(向后兼容) + if ovr_texture is None: + # 备用方案:如果缓存对象不存在,创建新的(性能较差) + ovr_texture = openvr.Texture_t() + ovr_texture.eType = openvr.TextureType_OpenGL + ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + if not hasattr(self, '_texture_fallback_warned'): + print("⚠️ 使用Texture对象备用方案(性能次优)") + self._texture_fallback_warned = True + + # 只更新handle,其他属性已预设置 + ovr_texture.handle = handle + + # 提交到VR系统 + error = self.vr_compositor.submit(eye, ovr_texture) + + # 检查错误 + if error and error != openvr.VRCompositorError_None: + print(f"⚠️ VR{eye_name}纹理提交错误: {error}") + self.submit_failures += 1 + else: + # 只在第一次成功时输出 + if not hasattr(self, '_optimized_submit_success_logged'): + print(f"✅ 优化版VR纹理提交成功! 使用缓存ID,避免重复prepareNow") + print(f" {eye_name}纹理ID: {handle}") + self._optimized_submit_success_logged = True + + # 🔧 智能GPU同步策略 - 减少不必要的flush调用 + # 只有在检测到性能问题或每N帧时才强制flush + if eye == openvr.Eye_Right: + self._smart_gpu_sync() + + except Exception as e: + print(f"❌ VR纹理提交异常: {e}") + import traceback + traceback.print_exc() + self.submit_failures += 1 + + def _smart_gpu_sync(self): + """智能GPU同步策略 - 只在必要时进行同步""" + try: + # 初始化同步控制变量 + if not hasattr(self, '_last_gpu_sync_frame'): + self._last_gpu_sync_frame = 0 + self._gpu_sync_interval = 60 # 每60帧强制同步一次(1秒@60FPS) + self._performance_based_sync = True + + current_frame = getattr(self, 'frame_count', 0) + + # 策略1: 性能自适应同步 + if self._performance_based_sync: + # 如果渲染时间过长,增加同步频率 + total_render_time = getattr(self, 'left_render_time', 0) + getattr(self, 'right_render_time', 0) + + if total_render_time > 15.0: # 如果总渲染时间超过15ms + # 高负载时更频繁同步 + sync_interval = 30 # 每30帧同步 + elif total_render_time > 10.0: + sync_interval = 45 # 中等负载 + else: + sync_interval = self._gpu_sync_interval # 正常负载 + + should_sync = (current_frame - self._last_gpu_sync_frame) >= sync_interval + else: + # 策略2: 固定间隔同步 + should_sync = (current_frame - self._last_gpu_sync_frame) >= self._gpu_sync_interval + + # 执行同步 + if should_sync: + gsg = self.world.win.getGsg() + if gsg and hasattr(gsg, 'flush'): + try: + gsg.flush() + self._last_gpu_sync_frame = current_frame + + # 只在首次或Debug时输出 + if not hasattr(self, '_smart_sync_logged') or self.debug_output_enabled: + if not hasattr(self, '_smart_sync_logged'): + print(f"🔧 智能GPU同步已启用 - 间隔:{sync_interval}帧") + self._smart_sync_logged = True + elif self.debug_output_enabled and current_frame % 600 == 1: + print(f"🔧 智能同步触发 (帧#{current_frame}, 间隔:{sync_interval})") + + except Exception as flush_error: + if not hasattr(self, '_smart_sync_error_logged'): + print(f"⚠️ 智能GPU同步失败: {flush_error}") + self._smart_sync_error_logged = True + + except Exception as e: + print(f"⚠️ 智能GPU同步策略异常: {e}") + + def _disable_main_cam(self): + """禁用主相机 - 基于参考实现""" + try: + # 保存原始相机状态 + if not hasattr(self, '_original_camera_parent'): + self._original_camera_parent = self.world.camera.getParent() + + # 创建空节点并将主相机重新附加到它 + self._empty_world = NodePath("empty_world") + self.world.camera.reparentTo(self._empty_world) + + print("✓ 主相机已禁用") + except Exception as e: + print(f"⚠️ 禁用主相机失败: {e}") + + def _enable_main_cam(self): + """恢复主相机 - 基于参考实现""" + try: + # 恢复原始相机状态 + if hasattr(self, '_original_camera_parent') and self._original_camera_parent: + self.world.camera.reparentTo(self._original_camera_parent) + else: + # 如果没有保存的父节点,重新附加到render + self.world.camera.reparentTo(self.world.render) + + # 清理空世界节点 + if hasattr(self, '_empty_world'): + self._empty_world.removeNode() + delattr(self, '_empty_world') + + print("✓ 主相机已恢复") + except Exception as e: + print(f"⚠️ 恢复主相机失败: {e}") + + def _initialize_controllers(self): + """初始化VR手柄控制器""" + try: + print("🎮 正在初始化VR手柄控制器...") + + # 创建左右手柄控制器实例 + self.left_controller = LeftController(self) + self.right_controller = RightController(self) + + # 检测现有连接的控制器 + self._detect_controllers() + + print("✓ VR手柄控制器初始化完成") + + except Exception as e: + print(f"⚠️ VR手柄初始化失败: {e}") + import traceback + traceback.print_exc() + + def _detect_controllers(self): + """检测并连接VR控制器""" + if not self.vr_system: + return + + try: + for device_index in range(openvr.k_unMaxTrackedDeviceCount): + # 检查设备是否已连接 + if not self.vr_system.isTrackedDeviceConnected(device_index): + continue + + # 获取设备类型 + device_class = self.vr_system.getTrackedDeviceClass(device_index) + + if device_class == openvr.TrackedDeviceClass_Controller: + # 获取控制器角色 + role = self.vr_system.getControllerRoleForTrackedDeviceIndex(device_index) + + if role == openvr.TrackedControllerRole_LeftHand and self.left_controller: + self.left_controller.set_device_index(device_index) + self.controllers[device_index] = self.left_controller + # 为设备创建锚点 + self._create_tracked_device_anchor(device_index, 'left_controller') + + elif role == openvr.TrackedControllerRole_RightHand and self.right_controller: + self.right_controller.set_device_index(device_index) + self.controllers[device_index] = self.right_controller + # 为设备创建锚点 + self._create_tracked_device_anchor(device_index, 'right_controller') + + print(f"🎮 检测到 {len(self.controllers)} 个控制器") + + except Exception as e: + print(f"⚠️ 控制器检测失败: {e}") + + def _create_tracked_device_anchor(self, device_index, name): + """为跟踪设备创建锚点节点""" + if not self.tracking_space: + print(f"⚠️ 无法为设备 {device_index} 创建锚点 - tracking_space未初始化") + return + + try: + # 获取设备模型名称 + if self.vr_system: + model_name = self.vr_system.getStringTrackedDeviceProperty( + device_index, + openvr.Prop_RenderModelName_String + ) + anchor_name = f"{device_index}:{model_name}:{name}" + else: + anchor_name = f"{device_index}:{name}" + + # 创建锚点节点 + device_anchor = self.tracking_space.attachNewNode(anchor_name) + self.tracked_device_anchors[device_index] = device_anchor + + print(f"✓ 为设备 {device_index} 创建锚点: {anchor_name}") + + except Exception as e: + print(f"⚠️ 创建设备锚点失败: {e}") + + def update_tracked_devices(self): + """更新所有跟踪设备的姿态 - 基于参考实现""" + if not self.poses or not self.vr_system: + return + + try: + # 更新每个已连接的控制器 + for device_index, controller in self.controllers.items(): + if device_index < len(self.poses): + pose_data = self.poses[device_index] + + # 更新控制器姿态 + controller.update_pose(pose_data) + + # 更新控制器输入状态 + controller.update_input_state(self.vr_system) + + # 更新其他跟踪设备的锚点 + for device_index in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): + if device_index in self.tracked_device_anchors: + pose_data = self.poses[device_index] + if pose_data.bPoseIsValid: + # 转换姿态矩阵 + modelview = self.convert_mat(pose_data.mDeviceToAbsoluteTracking) + final_matrix = self.coord_mat_inv * modelview * self.coord_mat + + # 更新锚点变换 + anchor = self.tracked_device_anchors[device_index] + anchor.setMat(final_matrix) + anchor.show() + else: + # 姿态无效,隐藏锚点 + self.tracked_device_anchors[device_index].hide() + + except Exception as e: + if self.frame_count % 300 == 0: # 每5秒输出一次错误 + print(f"⚠️ 更新跟踪设备失败: {e}") + + def get_controller_by_role(self, role): + """根据角色获取控制器 + + Args: + role: 'left' 或 'right' + + Returns: + VRController实例或None + """ + if role == 'left': + return self.left_controller + elif role == 'right': + return self.right_controller + return None + + def are_controllers_connected(self): + """检查是否有控制器连接""" + return len(self.controllers) > 0 + + def get_connected_controllers(self): + """获取所有连接的控制器列表""" + return list(self.controllers.values()) + + def trigger_controller_haptic(self, role, duration=0.001, strength=1.0): + """触发控制器震动反馈 + + Args: + role: 'left', 'right' 或 'both' + duration: 震动持续时间(秒) + strength: 震动强度 (0.0-1.0) + """ + if role in ['left', 'both'] and self.left_controller: + self.left_controller.trigger_haptic_feedback(duration, strength) + + if role in ['right', 'both'] and self.right_controller: + self.right_controller.trigger_haptic_feedback(duration, strength) + + # VR动作系统便捷方法 + def is_trigger_pressed(self, hand='any'): + """检查扳机是否被按下 + + Args: + hand: 'left', 'right', 'any' + """ + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_pressed('trigger', device_path) + return pressed + + def is_trigger_just_pressed(self, hand='any'): + """检查扳机是否刚刚被按下""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_just_pressed('trigger', device_path) + return pressed + + def is_grip_pressed(self, hand='any'): + """检查握把是否被按下""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_pressed('grip', device_path) + return pressed + + def is_grip_just_pressed(self, hand='any'): + """检查握把是否刚刚被按下""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_just_pressed('grip', device_path) + return pressed + + def is_menu_pressed(self, hand='any'): + """检查菜单按钮是否被按下""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_pressed('menu', device_path) + return pressed + + def is_trackpad_touched(self, hand='any'): + """检查触摸板是否被触摸""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + touched, _ = self.action_manager.is_digital_action_pressed('trackpad_touch', device_path) + return touched + + def get_trackpad_position(self, hand='any'): + """获取触摸板位置 + + Returns: + Vec2或None: 触摸板位置 (-1到1的范围) + """ + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + value, _ = self.action_manager.get_analog_action_value('trackpad', device_path) + return value + + # VR交互系统便捷方法 + def get_selected_object(self, hand='any'): + """获取指定手选中的对象 + + Args: + hand: 'left', 'right', 'any' + + Returns: + 选中的对象节点或None + """ + if hand == 'any': + # 返回任意手选中的对象 + for controller in self.get_connected_controllers(): + selected = self.interaction_manager.get_selected_object(controller.name) + if selected: + return selected + return None + else: + return self.interaction_manager.get_selected_object(hand) + + def get_grabbed_object(self, hand='any'): + """获取指定手抓取的对象 + + Args: + hand: 'left', 'right', 'any' + + Returns: + 抓取的对象节点或None + """ + if hand == 'any': + # 返回任意手抓取的对象 + for controller in self.get_connected_controllers(): + grabbed = self.interaction_manager.get_grabbed_object(controller.name) + if grabbed: + return grabbed + return None + else: + return self.interaction_manager.get_grabbed_object(hand) + + def is_grabbing_object(self, hand='any'): + """检查是否正在抓取对象 + + Args: + hand: 'left', 'right', 'any' + + Returns: + bool: 是否正在抓取 + """ + if hand == 'any': + # 检查任意手是否正在抓取 + for controller in self.get_connected_controllers(): + if self.interaction_manager.is_grabbing(controller.name): + return True + return False + else: + return self.interaction_manager.is_grabbing(hand) + + def force_release_all_grabs(self): + """强制释放所有抓取的对象""" + self.interaction_manager.force_release_all() + + def add_interactable_object(self, object_node): + """将对象标记为可交互 + + Args: + object_node: 要标记的对象节点 + """ + self.interaction_manager._add_collision_to_object(object_node) + + def _disable_async_reprojection(self): + """禁用异步重投影 - 备选修复方案""" + try: + # 尝试通过OpenVR设置禁用异步重投影 + if hasattr(openvr, 'VRSettings'): + settings = openvr.VRSettings() + if settings: + # 禁用异步重投影 + error = settings.setBool("steamvr", "enableAsyncReprojection", False) + if error == openvr.VRSettingsError_None: + print("✅ 异步重投影已禁用") + else: + print(f"⚠️ 禁用异步重投影失败: 设置错误 {error}") + else: + print("⚠️ 无法获取VR设置接口") + else: + print("⚠️ OpenVR设置接口不可用") + + except Exception as e: + print(f"⚠️ 禁用异步重投影失败: {e}") + + def enable_async_reprojection_disable(self): + """启用异步重投影禁用选项 - 用户可调用的方法""" + self.disable_async_reprojection = True + print("📝 异步重投影禁用选项已启用,将在下次VR初始化时生效") + + def disable_async_reprojection_disable(self): + """禁用异步重投影禁用选项 - 恢复默认行为""" + self.disable_async_reprojection = False + print("📝 异步重投影禁用选项已关闭,将使用默认ATW行为") + + def _start_timing(self, operation_name): + """开始计时操作 - 性能优化版本""" + if not self.enable_pipeline_monitoring or self.performance_mode_enabled: + return None + + import time + # 🚀 性能优化:减少字典创建,只在必要时创建 + return { + 'operation': operation_name, + 'start_time': time.perf_counter() + } + + def _end_timing(self, timing_data): + """结束计时并记录结果 - 性能优化版本""" + if not self.enable_pipeline_monitoring or self.performance_mode_enabled or not timing_data: + return 0.0 + + import time + elapsed = (time.perf_counter() - timing_data['start_time']) * 1000 # 转换为毫秒 + + # 保存到相应的历史记录 + operation = timing_data['operation'] + if operation == 'wait_poses': + self.wait_poses_time = elapsed + self.wait_poses_times.append(elapsed) + if len(self.wait_poses_times) > self.pipeline_history_size: + self.wait_poses_times.pop(0) + elif operation == 'left_render': + self.left_render_time = elapsed + elif operation == 'right_render': + self.right_render_time = elapsed + elif operation == 'submit': + self.submit_time = elapsed + self.submit_times.append(elapsed) + if len(self.submit_times) > self.pipeline_history_size: + self.submit_times.pop(0) + elif operation == 'sync_wait': + self.vr_sync_wait_time = elapsed + self.sync_wait_times.append(elapsed) + if len(self.sync_wait_times) > self.pipeline_history_size: + self.sync_wait_times.pop(0) + + # 计算总渲染时间 + total_render = self.left_render_time + self.right_render_time + self.render_times.append(total_render) + if len(self.render_times) > self.pipeline_history_size: + self.render_times.pop(0) + + return elapsed + + def _get_pipeline_stats(self): + """获取渲染管线统计信息""" + def get_stats(times_list): + if not times_list: + return {'avg': 0.0, 'min': 0.0, 'max': 0.0} + return { + 'avg': sum(times_list) / len(times_list), + 'min': min(times_list), + 'max': max(times_list) + } + + # 计算GPU时间统计 + def get_gpu_field_stats(field_name): + """从GPU时间历史记录中提取特定字段的统计信息""" + values = [] + for gpu_data in self.gpu_timing_history: + if field_name in gpu_data: + values.append(gpu_data[field_name]) + return get_stats(values) + + return { + 'wait_poses': get_stats(self.wait_poses_times), + 'render': get_stats(self.render_times), + 'submit': get_stats(self.submit_times), + 'sync_wait': get_stats(self.sync_wait_times), + 'gpu_timing': { + 'scene_render': get_gpu_field_stats('scene_render'), + 'pre_submit': get_gpu_field_stats('pre_submit'), + 'post_submit': get_gpu_field_stats('post_submit'), + 'total_render': get_gpu_field_stats('total_render'), + 'compositor_render': get_gpu_field_stats('compositor_render'), + 'frame_interval': get_gpu_field_stats('frame_interval') + }, + 'current': { + 'wait_poses': self.wait_poses_time, + 'left_render': self.left_render_time, + 'right_render': self.right_render_time, + 'submit': self.submit_time, + 'sync_wait': self.vr_sync_wait_time, + 'total_render': self.left_render_time + self.right_render_time, + 'gpu_scene_render': self.gpu_scene_render_ms, + 'gpu_pre_submit': self.gpu_pre_submit_ms, + 'gpu_post_submit': self.gpu_post_submit_ms, + 'gpu_total_render': self.gpu_total_render_ms, + 'gpu_compositor_render': self.gpu_compositor_render_ms, + 'gpu_frame_interval': self.gpu_client_frame_interval_ms + }, + 'vr_info': { + 'eye_resolution': self.current_eye_resolution, + 'recommended_resolution': self.recommended_eye_resolution, + 'display_frequency': self.vr_display_frequency, + 'vsync_enabled': self.vr_vsync_enabled, + 'target_frame_time_ms': self.target_frame_time_ms, + 'vsync_to_photons_ms': self.vsync_to_photons_ms, + 'vsync_window_ms': self.vsync_window_ms, + 'async_reprojection': self.async_reprojection_enabled, + 'motion_smoothing': self.motion_smoothing_enabled, + 'gpu_timing_enabled': self.enable_gpu_timing, + 'gpu_timing_failures': self.gpu_timing_failure_count + } + } + + + def set_prediction_time(self, prediction_time_ms): + """设置预测时间(仅用于update_task策略) + + Args: + prediction_time_ms: 预测时间,单位毫秒(通常8-16ms) + """ + prediction_time_s = prediction_time_ms / 1000.0 + if prediction_time_s < 0.005 or prediction_time_s > 0.020: + print(f"⚠️ 预测时间超出推荐范围(5-20ms): {prediction_time_ms}ms") + + old_time = self.use_prediction_time * 1000 + self.use_prediction_time = prediction_time_s + print(f"✓ VR预测时间已设置: {old_time:.1f}ms → {prediction_time_ms:.1f}ms") + + + def test_pipeline_monitoring(self): + """测试管线监控功能 - 用于调试和验证""" + print("🔧 正在测试VR管线监控功能...") + + try: + # 测试基本信息获取 + print("📊 基本信息:") + print(f" 当前分辨率: {self.current_eye_resolution}") + print(f" 显示频率: {self.vr_display_frequency} Hz") + print(f" 目标帧时间: {self.target_frame_time_ms:.2f}ms") + + # Running Start模式信息 + print("🎯 Running Start模式:") + print(f" 预测时间: {self.use_prediction_time * 1000:.1f}ms") + print(f" 模式: Valve Running Start - 帧开始时获取姿态") + + # 测试管线统计 + if self.enable_pipeline_monitoring: + pipeline_stats = self._get_pipeline_stats() + print("⚙️ 管线统计:") + print(f" waitGetPoses历史: {len(self.wait_poses_times)}帧") + print(f" 渲染历史: {len(self.render_times)}帧") + print(f" 提交历史: {len(self.submit_times)}帧") + + # 测试性能报告 + print("📋 生成详细性能报告:") + self._print_performance_report() + + print("✅ 管线监控功能测试完成") + else: + print("⚠️ 管线监控已禁用,无法测试统计功能") + + except Exception as e: + print(f"❌ 测试管线监控功能时发生错误: {e}") + import traceback + traceback.print_exc() + + def _init_performance_monitoring(self): + """初始化性能监控库""" + self.psutil_available = False + self.gputil_available = False + self.nvidia_ml_available = False + + try: + import psutil + self.psutil = psutil + self.psutil_available = True + print("✓ psutil性能监控库已加载") + except ImportError: + print("⚠️ psutil库未安装,CPU和内存监控将不可用") + + try: + import GPUtil + self.gputil = GPUtil + self.gputil_available = True + print("✓ GPUtil GPU监控库已加载") + except ImportError: + print("⚠️ GPUtil库未安装,GPU监控将不可用") + + try: + import pynvml + self.pynvml = pynvml + pynvml.nvmlInit() + self.nvidia_ml_available = True + print("✓ NVIDIA-ML GPU监控库已加载") + except ImportError: + print("⚠️ pynvml库未安装,NVIDIA GPU详细监控将不可用") + except Exception as e: + print(f"⚠️ NVIDIA-ML初始化失败: {e}") + + def _get_gpu_frame_timing(self, frames_ago=0): + """获取GPU渲染时间统计 + + Args: + frames_ago: 获取多少帧之前的数据,0表示当前帧 + + Returns: + dict: GPU时间数据,如果获取失败返回None + """ + if not self.enable_gpu_timing or not self.vr_compositor: + return None + + try: + # 调用OpenVR的getFrameTiming API + result, timing = self.vr_compositor.getFrameTiming(framesAgo=frames_ago) + + if not result: + self.gpu_timing_failure_count += 1 + if self.gpu_timing_failure_count % 300 == 1: # 每5秒输出一次错误 + print("⚠️ OpenVR getFrameTiming调用失败") + return None + + # 提取GPU时间数据(单位:毫秒) + gpu_data = {} + + # 检查timing对象是否有GPU时间相关的属性 + if hasattr(timing, 'm_flSceneRenderGpuMs'): + gpu_data['scene_render'] = timing.m_flSceneRenderGpuMs + self.gpu_scene_render_ms = timing.m_flSceneRenderGpuMs + + if hasattr(timing, 'm_flPreSubmitGpuMs'): + gpu_data['pre_submit'] = timing.m_flPreSubmitGpuMs + self.gpu_pre_submit_ms = timing.m_flPreSubmitGpuMs + + if hasattr(timing, 'm_flPostSubmitGpuMs'): + gpu_data['post_submit'] = timing.m_flPostSubmitGpuMs + self.gpu_post_submit_ms = timing.m_flPostSubmitGpuMs + + if hasattr(timing, 'm_flTotalRenderGpuMs'): + gpu_data['total_render'] = timing.m_flTotalRenderGpuMs + self.gpu_total_render_ms = timing.m_flTotalRenderGpuMs + + if hasattr(timing, 'm_flCompositorRenderGpuMs'): + gpu_data['compositor_render'] = timing.m_flCompositorRenderGpuMs + self.gpu_compositor_render_ms = timing.m_flCompositorRenderGpuMs + + if hasattr(timing, 'm_flClientFrameIntervalMs'): + gpu_data['frame_interval'] = timing.m_flClientFrameIntervalMs + self.gpu_client_frame_interval_ms = timing.m_flClientFrameIntervalMs + + # 将GPU时间数据添加到历史记录 + if gpu_data: + self.gpu_timing_history.append(gpu_data) + if len(self.gpu_timing_history) > self.gpu_timing_history_size: + self.gpu_timing_history.pop(0) + + # 调试信息 - 仅在第一次成功时输出 + if not hasattr(self, '_gpu_timing_success_logged'): + available_fields = list(gpu_data.keys()) + print(f"✅ GPU时间统计已启用 - 可用字段: {available_fields}") + self._gpu_timing_success_logged = True + + return gpu_data + + except AttributeError as e: + # OpenVR Python绑定可能不包含某些字段 + if self.gpu_timing_failure_count == 0: + print(f"⚠️ GPU时间统计部分功能不可用: {e}") + print(" 这可能是由于OpenVR Python绑定版本问题") + self.gpu_timing_failure_count += 1 + return None + + except Exception as e: + self.gpu_timing_failure_count += 1 + if self.gpu_timing_failure_count % 300 == 1: # 每5秒输出一次错误 + print(f"⚠️ 获取GPU时间统计失败: {e}") + return None + + def _update_performance_metrics(self): + """更新系统性能指标""" + if not self.performance_monitoring: + return + + import time + current_time = time.time() + + # 限制更新频率 + if current_time - self.last_performance_check < self.performance_check_interval: + return + + self.last_performance_check = current_time + + try: + # 更新CPU和内存使用率 + if self.psutil_available: + self.cpu_usage = self.psutil.cpu_percent(interval=None) + memory = self.psutil.virtual_memory() + self.memory_usage = memory.percent + + # 更新GPU使用率 + self._update_gpu_metrics() + + except Exception as e: + if self.frame_count % 600 == 0: # 每10秒输出一次错误 + print(f"⚠️ 性能监控更新失败: {e}") + + def _update_gpu_metrics(self): + """更新GPU相关指标""" + try: + # 方法1: 使用GPUtil + if self.gputil_available: + gpus = self.gputil.getGPUs() + if gpus: + gpu = gpus[0] # 使用第一个GPU + self.gpu_usage = gpu.load * 100 + self.gpu_memory_usage = gpu.memoryUtil * 100 + + # 方法2: 使用NVIDIA-ML (更精确) + elif self.nvidia_ml_available: + try: + handle = self.pynvml.nvmlDeviceGetHandleByIndex(0) + + # GPU使用率 + utilization = self.pynvml.nvmlDeviceGetUtilizationRates(handle) + self.gpu_usage = utilization.gpu + + # GPU内存使用率 + memory_info = self.pynvml.nvmlDeviceGetMemoryInfo(handle) + self.gpu_memory_usage = (memory_info.used / memory_info.total) * 100 + + except Exception as e: + # NVIDIA-ML可能无法在某些系统上工作 + pass + + except Exception as e: + # GPU监控失败,但不影响VR功能 + pass + + def _track_frame_time(self): + """记录帧时间 - 性能优化版本""" + import time + current_time = time.time() + + if hasattr(self, '_last_frame_time'): + frame_time = (current_time - self._last_frame_time) * 1000 # 转换为毫秒 + + # 🚀 性能优化:性能模式下跳过列表操作以减少内存分配 + if not self.performance_mode_enabled: + # 添加到帧时间历史 + self.frame_times.append(frame_time) + + # 限制历史长度 + if len(self.frame_times) > self.max_frame_time_history: + self.frame_times.pop(0) + + self._last_frame_time = current_time + + def get_performance_stats(self): + """获取详细的性能统计信息""" + stats = { + 'vr_fps': self.vr_fps, + 'frame_count': self.frame_count, + 'submit_failures': self.submit_failures, + 'pose_failures': self.pose_failures, + 'cpu_usage': self.cpu_usage, + 'memory_usage': self.memory_usage, + 'gpu_usage': self.gpu_usage, + 'gpu_memory_usage': self.gpu_memory_usage, + } + + # 计算帧时间统计 + if self.frame_times: + stats['frame_time_avg'] = sum(self.frame_times) / len(self.frame_times) + stats['frame_time_min'] = min(self.frame_times) + stats['frame_time_max'] = max(self.frame_times) + stats['frame_time_95th'] = sorted(self.frame_times)[int(len(self.frame_times) * 0.95)] + else: + stats['frame_time_avg'] = 0 + stats['frame_time_min'] = 0 + stats['frame_time_max'] = 0 + stats['frame_time_95th'] = 0 + + return stats + + def enable_performance_monitoring(self): + """启用性能监控""" + self.performance_monitoring = True + print("✓ VR性能监控已启用") + + def disable_performance_monitoring(self): + """禁用性能监控""" + self.performance_monitoring = False + print("✓ VR性能监控已禁用") + + def enable_gpu_timing_monitoring(self): + """启用GPU时间监控""" + self.enable_gpu_timing = True + print("✓ VR GPU时间监控已启用") + + def disable_gpu_timing_monitoring(self): + """禁用GPU时间监控""" + self.enable_gpu_timing = False + print("✓ VR GPU时间监控已禁用") + + def set_performance_check_interval(self, interval): + """设置性能检查间隔 + + Args: + interval: 检查间隔(秒),建议0.1-2.0之间 + """ + if 0.1 <= interval <= 5.0: + self.performance_check_interval = interval + print(f"✓ 性能监控间隔设置为 {interval:.1f} 秒") + else: + print("⚠️ 性能监控间隔应在0.1-5.0秒之间") + + def set_frame_time_history_size(self, size): + """设置帧时间历史记录大小 + + Args: + size: 历史记录大小(帧数),建议30-300之间 + """ + if 10 <= size <= 1000: + self.max_frame_time_history = size + # 清理超出的历史记录 + if len(self.frame_times) > size: + self.frame_times = self.frame_times[-size:] + print(f"✓ 帧时间历史记录大小设置为 {size} 帧") + else: + print("⚠️ 帧时间历史记录大小应在10-1000帧之间") + + def set_performance_report_interval(self, frames): + """设置性能报告输出间隔 + + Args: + frames: 帧数间隔,建议300-7200之间(5秒-2分钟@60fps) + """ + if 300 <= frames <= 7200: + # 修改_update_vr中的报告间隔 + print(f"✓ 性能报告间隔设置为每 {frames} 帧(约 {frames/60:.1f} 秒@60fps)") + # 这里可以添加一个实例变量来控制 + self.performance_report_interval = frames + else: + print("⚠️ 性能报告间隔应在300-7200帧之间") + + def get_performance_monitoring_config(self): + """获取当前性能监控配置""" + return { + 'enabled': self.performance_monitoring, + 'check_interval': self.performance_check_interval, + 'frame_history_size': self.max_frame_time_history, + 'report_interval': getattr(self, 'performance_report_interval', 1800), + 'psutil_available': self.psutil_available, + 'gputil_available': self.gputil_available, + 'nvidia_ml_available': self.nvidia_ml_available + } + + def print_performance_monitoring_status(self): + """输出性能监控状态""" + config = self.get_performance_monitoring_config() + + print("🔧 ===== VR性能监控配置 =====") + print(f" 监控状态: {'✅ 启用' if config['enabled'] else '❌ 禁用'}") + print(f" 检查间隔: {config['check_interval']:.1f} 秒") + print(f" 帧时间历史: {config['frame_history_size']} 帧") + print(f" 报告间隔: {config['report_interval']} 帧") + + print(f" 可用库:") + print(f" psutil (CPU/内存): {'✅' if config['psutil_available'] else '❌'}") + print(f" GPUtil (GPU): {'✅' if config['gputil_available'] else '❌'}") + print(f" NVIDIA-ML (GPU): {'✅' if config['nvidia_ml_available'] else '❌'}") + print("=============================") + + def force_performance_report(self): + """强制输出一次性能报告""" + print("🔄 手动触发性能报告...") + self._print_performance_report() + + def reset_performance_counters(self): + """重置性能计数器""" + self.frame_count = 0 + self.last_fps_check = 0 + self.last_fps_time = 0 + self.vr_fps = 0 + self.submit_failures = 0 + self.pose_failures = 0 + self.frame_times.clear() + print("✅ 性能计数器已重置") + + def get_current_performance_summary(self): + """获取当前性能摘要(简短版本)""" + stats = self.get_performance_stats() + + summary = f"VR性能: {stats['vr_fps']:.1f}fps" + + if stats['frame_time_avg'] > 0: + summary += f" | 帧时间: {stats['frame_time_avg']:.1f}ms" + + if self.psutil_available: + summary += f" | CPU: {stats['cpu_usage']:.0f}%" + summary += f" | 内存: {stats['memory_usage']:.0f}%" + + # 显示GPU信息,如果库不可用则显示提示 + if self.gputil_available or self.nvidia_ml_available: + summary += f" | GPU: {stats['gpu_usage']:.0f}%" + else: + summary += " | GPU: N/A" + + return summary + + def _print_render_callback_diagnostics(self): + """输出渲染回调诊断信息 - 包含优化效果分析""" + try: + print(f"🔍 DrawCallback渲染诊断:") + + # 回调次数统计 + left_count = self.left_render_count + right_count = self.right_render_count + print(f" 渲染次数: 左眼={left_count}, 右眼={right_count}") + + if left_count == 0 and right_count == 0: + print(f" ❌ VR缓冲区未被渲染 - 这是严重问题!") + return + + # 渲染次数平衡性检查 + if abs(left_count - right_count) > 5: + print(f" ⚠️ 左右眼渲染次数不平衡: 差异={abs(left_count - right_count)}") + else: + print(f" ✓ 左右眼渲染次数平衡") + + # 🔧 真实渲染时间分析 - 显示优化效果 + left_render_time = getattr(self, 'left_render_time', 0) + right_render_time = getattr(self, 'right_render_time', 0) + total_render_time = left_render_time + right_render_time + + print(f" ⏱️ 精确渲染时间测量:") + print(f" 左眼cbdata.upcall(): {left_render_time:.2f}ms") + print(f" 右眼cbdata.upcall(): {right_render_time:.2f}ms") + print(f" 总计: {total_render_time:.2f}ms") + + # 渲染性能评估 + if total_render_time > 16.0: # 超过60FPS时间 + print(f" 🔴 渲染时间过长: {total_render_time:.1f}ms (目标<13.9ms@72Hz)") + print(f" 建议: 检查RenderPipeline优化是否生效") + elif total_render_time > 10.0: + print(f" 🟡 渲染时间偏高: {total_render_time:.1f}ms (可接受)") + else: + print(f" 🟢 渲染性能良好: {total_render_time:.1f}ms") + + # 🔧 渲染管线优化状态检查 + self._check_rendering_optimizations() + + # OpenGL状态诊断 + self._diagnose_opengl_state() + + except Exception as e: + print(f" 渲染回调诊断失败: {e}") + + def _check_rendering_optimizations(self): + """检查渲染优化状态""" + try: + print(f" 🔧 渲染优化状态:") + + # 检查RenderPipeline优化 + if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline: + print(f" RenderPipeline: 已检测并优化") + else: + print(f" RenderPipeline: 未检测到(使用基础Panda3D)") + + # 检查GPU同步优化 + if hasattr(self, '_smart_sync_logged'): + last_sync_frame = getattr(self, '_last_gpu_sync_frame', 0) + current_frame = getattr(self, 'frame_count', 0) + frames_since_sync = current_frame - last_sync_frame + print(f" 智能GPU同步: 已启用 (距离上次同步: {frames_since_sync}帧)") + else: + print(f" 智能GPU同步: 未初始化") + + # 检查对象池状态 + matrix_pool_status = self.get_object_pool_status() + print(f" 对象池: Mat4={matrix_pool_status['matrix_pool_size']}/{matrix_pool_status['matrix_pool_capacity']}") + + # 检查垃圾回收控制 + gc_status = self.get_debug_status() + if gc_status['gc_disabled']: + print(f" GC控制: 已启用手动模式 (间隔:{gc_status['manual_gc_interval']}帧)") + else: + print(f" GC控制: 自动模式") + + except Exception as e: + print(f" 优化状态检查失败: {e}") + + def _diagnose_opengl_state(self): + """诊断OpenGL渲染状态""" + try: + # 检查VR缓冲区状态 + if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: + left_gsg = self.vr_left_eye_buffer.getGsg() + left_valid = self.vr_left_eye_buffer.isValid() + print(f" 左眼缓冲区: {'有效' if left_valid else '无效'}") + + if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: + right_gsg = self.vr_right_eye_buffer.getGsg() + right_valid = self.vr_right_eye_buffer.isValid() + print(f" 右眼缓冲区: {'有效' if right_valid else '无效'}") + + # 检查纹理准备状态 + if hasattr(self, 'textures_prepared'): + print(f" 纹理准备状态: {'已准备' if self.textures_prepared else '未准备'}") + + # 检查纹理ID缓存 + if hasattr(self, 'left_texture_id') and hasattr(self, 'right_texture_id'): + left_id = self.left_texture_id or 0 + right_id = self.right_texture_id or 0 + print(f" 纹理ID缓存: 左眼={left_id}, 右眼={right_id}") + + if left_id == 0 or right_id == 0: + print(f" ⚠️ 检测到无效的纹理ID,这可能导致提交失败") + else: + print(f" ✓ 纹理ID缓存正常") + + # 检查场景渲染状态 + if hasattr(self.world, 'render') and self.world.render: + render_children = len(self.world.render.getChildren()) + print(f" 场景节点数: {render_children}") + if render_children == 0: + print(f" ⚠️ 场景为空,可能导致渲染时间异常短") + + except Exception as e: + print(f" OpenGL状态诊断失败: {e}") + + def enable_debug_output(self): + """启用调试输出""" + self.debug_output_enabled = True + print("✓ VR调试输出已启用") + + def disable_debug_output(self): + """禁用调试输出""" + self.debug_output_enabled = False + print("✓ VR调试输出已禁用") + + def set_debug_mode(self, mode): + """设置调试模式 + + Args: + mode: 'brief' 或 'detailed' + """ + if mode in ['brief', 'detailed']: + self.debug_mode = mode + print(f"✓ VR调试模式设置为: {mode}") + else: + print("⚠️ 调试模式只能是 'brief' 或 'detailed'") + + def toggle_debug_output(self): + """切换调试输出状态""" + self.debug_output_enabled = not self.debug_output_enabled + status = "启用" if self.debug_output_enabled else "禁用" + print(f"✓ VR调试输出已{status}") + return self.debug_output_enabled + + def get_debug_status(self): + """获取调试状态""" + return { + 'debug_enabled': self.debug_output_enabled, + 'debug_mode': self.debug_mode, + 'performance_monitoring': self.performance_monitoring, + 'report_interval_frames': getattr(self, 'performance_report_interval', 600), + 'report_interval_seconds': getattr(self, 'performance_report_interval', 600) / 60, # 假设60fps + 'gc_control_enabled': self._gc_control_enabled, + 'gc_disabled': self._gc_disabled, + 'manual_gc_interval': self._manual_gc_interval, + } + + # ====== Python垃圾回收控制方法 ====== + + def enable_gc_control(self): + """启用垃圾回收控制 - 减少VR渲染期间的GC峰值""" + if not self._gc_control_enabled: + self._gc_control_enabled = True + if not self._gc_disabled: + gc.disable() + self._gc_disabled = True + print("✅ VR垃圾回收控制已启用") + else: + print("ℹ️ VR垃圾回收控制已经启用") + + def disable_gc_control(self): + """禁用垃圾回收控制 - 恢复自动垃圾回收""" + if self._gc_control_enabled: + self._gc_control_enabled = False + if self._gc_disabled: + gc.enable() + self._gc_disabled = False + print("✅ VR垃圾回收控制已禁用,恢复自动垃圾回收") + else: + print("ℹ️ VR垃圾回收控制已经禁用") + + def set_manual_gc_interval(self, frames): + """设置手动垃圾回收间隔 + + Args: + frames: 帧数间隔 (建议100-600) + """ + if 50 <= frames <= 1800: + old_interval = self._manual_gc_interval + self._manual_gc_interval = frames + print(f"✅ 手动GC间隔: {old_interval} → {frames} 帧") + else: + print("⚠️ GC间隔应在50-1800帧之间") + + def force_manual_gc(self): + """强制执行一次垃圾回收""" + collected = gc.collect() + print(f"🗑️ 强制GC: 清理了 {collected} 个对象") + return collected + + def get_object_pool_status(self): + """获取对象池状态""" + return { + 'matrix_pool_size': len(self._matrix_pool) if hasattr(self, '_matrix_pool') else 0, + 'matrix_pool_capacity': getattr(self, '_matrix_pool_size', 0), + 'cached_controllers': len(self.controller_poses), + 'cached_matrices': len(getattr(self, '_cached_matrices', {})), + } + + # ====== VR分辨率缩放和质量预设系统 ====== + + def set_resolution_scale(self, scale): + """设置VR分辨率缩放系数 + + Args: + scale: 缩放系数 (0.3-1.0),0.75表示75%分辨率 + """ + if not (0.3 <= scale <= 1.0): + print(f"⚠️ 分辨率缩放系数应在0.3-1.0之间,当前: {scale}") + return False + + old_scale = self.resolution_scale + self.resolution_scale = scale + + # 如果VR已初始化,重新创建缓冲区 + if self.vr_initialized: + self._apply_resolution_scale() + + print(f"✓ VR分辨率缩放: {old_scale} → {scale}") + pixel_reduction = (1 - scale**2) * 100 + print(f"📊 像素减少: {pixel_reduction:.1f}%") + + return True + + def set_quality_preset(self, preset_name): + """设置VR质量预设 + + Args: + preset_name: 'performance', 'balanced', 'quality' + """ + if preset_name not in self.quality_presets: + print(f"⚠️ 未知的质量预设: {preset_name}") + print(f" 可用预设: {list(self.quality_presets.keys())}") + return False + + old_preset = self.current_quality_preset + self.current_quality_preset = preset_name + scale = self.quality_presets[preset_name] + + print(f"🎯 切换VR质量预设: {old_preset} → {preset_name}") + + return self.set_resolution_scale(scale) + + def cycle_quality_preset(self): + """循环切换质量预设""" + presets = list(self.quality_presets.keys()) + current_index = presets.index(self.current_quality_preset) + next_index = (current_index + 1) % len(presets) + next_preset = presets[next_index] + + return self.set_quality_preset(next_preset) + + def _apply_resolution_scale(self): + """应用分辨率缩放,重新创建VR缓冲区""" + try: + # 计算新的分辨率 + self.scaled_eye_width = int(self.base_eye_width * self.resolution_scale) + self.scaled_eye_height = int(self.base_eye_height * self.resolution_scale) + + # 更新当前分辨率 + self.eye_width = self.scaled_eye_width + self.eye_height = self.scaled_eye_height + self.current_eye_resolution = (self.eye_width, self.eye_height) + + print(f"🔄 重新创建VR缓冲区...") + print(f" 新分辨率: {self.eye_width}x{self.eye_height}") + + # 清理旧的缓冲区 + self._cleanup_vr_buffers() + + # 🔧 关键修复:根据渲染模式选择创建方法 + success = False + if self.vr_render_mode == VRRenderMode.RENDER_PIPELINE: + print(f" 使用RenderPipeline模式重建...") + success = self._create_vr_buffers_with_pipeline() + if not success: + print("⚠️ RenderPipeline模式创建失败,回退到普通渲染模式") + self.vr_render_mode = VRRenderMode.NORMAL + success = self._create_vr_buffers() + else: + print(f" 使用普通模式重建...") + success = self._create_vr_buffers() + + if success: + # 重新设置相机 + self._setup_vr_cameras() + print("✅ VR缓冲区重新创建成功") + return True + else: + print("❌ VR缓冲区重新创建失败") + return False + + except Exception as e: + print(f"❌ 应用分辨率缩放失败: {e}") + import traceback + traceback.print_exc() + return False + + + def get_resolution_info(self): + """获取分辨率相关信息""" + return { + 'base_resolution': (self.base_eye_width, self.base_eye_height), + 'current_resolution': (self.eye_width, self.eye_height), + 'resolution_scale': self.resolution_scale, + 'current_preset': self.current_quality_preset, + 'available_presets': self.quality_presets, + 'pixel_reduction_percent': (1 - self.resolution_scale**2) * 100 + } + + def print_resolution_info(self): + """输出分辨率信息""" + info = self.get_resolution_info() + print("🔧 ===== VR分辨率信息 =====") + print(f" 推荐分辨率: {info['base_resolution'][0]}x{info['base_resolution'][1]}") + print(f" 当前分辨率: {info['current_resolution'][0]}x{info['current_resolution'][1]}") + print(f" 缩放系数: {info['resolution_scale']}") + print(f" 当前预设: {info['current_preset']}") + print(f" 像素减少: {info['pixel_reduction_percent']:.1f}%") + print(" 可用预设:") + for name, scale in info['available_presets'].items(): + marker = "✓" if name == info['current_preset'] else " " + print(f" {marker} {name}: {scale} ({scale*100:.0f}%)") + print("==========================") + + # ====== 性能模式控制方法 ====== + + def enable_performance_mode(self): + """手动启用性能模式 - 立即禁用详细监控以提升性能""" + if not self.performance_mode_enabled: + self.performance_mode_enabled = True + print("🎯 性能模式已手动启用 - 禁用详细监控以提升性能") + print(" 现在将减少每帧对象创建,显著提升VR性能稳定性") + else: + print("ℹ️ 性能模式已经启用") + + def disable_performance_mode(self): + """禁用性能模式 - 重新启用详细监控(用于调试)""" + if self.performance_mode_enabled: + self.performance_mode_enabled = False + print("🔍 性能模式已禁用 - 重新启用详细监控") + print(" 注意:这将增加每帧对象创建,可能影响VR性能") + else: + print("ℹ️ 性能模式已经禁用") + + def set_performance_mode_trigger_frame(self, frame_count): + """设置性能模式自动触发的帧数 + + Args: + frame_count: 触发帧数 (建议300-1200) + """ + if 100 <= frame_count <= 3600: + old_trigger = self.performance_mode_trigger_frame + self.performance_mode_trigger_frame = frame_count + print(f"✅ 性能模式触发帧数: {old_trigger} → {frame_count}") + else: + print("⚠️ 触发帧数应在100-3600之间") + + def get_performance_mode_status(self): + """获取性能模式状态""" + return { + 'performance_mode_enabled': self.performance_mode_enabled, + 'trigger_frame': self.performance_mode_trigger_frame, + 'current_frame': self.frame_count, + 'will_trigger_at_frame': self.performance_mode_trigger_frame if not self.performance_mode_enabled else None, + 'gc_interval_normal': self._manual_gc_interval, + 'gc_interval_performance': self._manual_gc_interval * 2, + } + + # ====== VR测试模式 ====== + + def enable_vr_test_mode(self, display_mode='stereo'): + """启用VR测试模式 - 将VR渲染直接显示在屏幕上 + + Args: + display_mode: 显示模式 + - 'stereo': 左右眼并排显示 + - 'left': 只显示左眼 + - 'right': 只显示右眼 + """ + if not self.is_vr_available(): + print("❌ VR系统不可用,无法启动测试模式") + return False + + try: + print(f"🧪 启动VR测试模式 - 显示模式: {display_mode}") + + # 初始化VR系统(如果还没初始化) + if not self.vr_initialized: + if not self.initialize_vr(): + print("❌ VR初始化失败") + return False + + # 🔧 关键修复:确保测试模式的纹理资源已初始化 + # 这解决了测试模式纹理提交失败导致的36FPS问题 + print("🔧 检查VR测试模式纹理资源...") + if not self._ensure_test_mode_textures(): + print("❌ VR测试模式纹理资源初始化失败") + return False + + # 设置测试模式 + self.vr_test_mode = True + self.test_display_mode = display_mode + + # 启用VR渲染但不提交给OpenVR + if not self.vr_enabled: + # 启用VR渲染流程(但会在回调中跳过提交) + self.vr_enabled = True + self._disable_main_cam() + + # 设置高帧率用于测试 + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + if hasattr(self.world.qtWidget, 'synchronizer'): + self.world.qtWidget.synchronizer.setInterval(int(1000/144)) + print("✓ 测试模式:Qt Timer设置为144Hz") + + # 初始化测试显示系统 + if not self._initialize_test_display(): + print("❌ 测试显示系统初始化失败") + return False + + # 创建性能HUD + if not self._initialize_test_performance_hud(): + print("❌ 性能HUD初始化失败") + return False + + # 恢复主相机以查看测试内容 + self._enable_main_cam() + + # 重置HUD更新计数器 + self.hud_update_counter = 0 + + print("✅ VR测试模式已启用") + print(" - VR内容将显示在屏幕上") + print(" - 不会向OpenVR提交纹理") + print(" - 可以准确测量纯渲染性能") + print(f" - 当前显示模式: {display_mode}") + + return True + + except Exception as e: + print(f"❌ 启动VR测试模式失败: {e}") + import traceback + traceback.print_exc() + return False + + def _ensure_test_mode_textures(self): + """确保VR测试模式的纹理资源已正确初始化 + + 这解决了测试模式启用纹理提交时的36FPS问题: + - VR测试模式可能跳过了VR渲染缓冲区的初始化 + - submit_texture()需要有效的texture ID和OpenVR Texture对象 + - 如果未初始化会导致提交失败,造成帧率减半 + """ + try: + print(" 检查VR渲染缓冲区...") + + # 检查VR渲染缓冲区是否存在 + buffers_exist = ( + hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer and + hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer + ) + + if not buffers_exist: + print(" ⚠️ VR渲染缓冲区不存在,正在创建...") + if not self._setup_vr_render_buffers(): + print(" ❌ VR渲染缓冲区创建失败") + return False + print(" ✅ VR渲染缓冲区创建成功") + else: + print(" ✅ VR渲染缓冲区已存在") + + # 检查纹理ID是否已缓存 + print(" 检查纹理ID缓存...") + texture_ids_cached = ( + hasattr(self, 'left_texture_id') and self.left_texture_id and self.left_texture_id > 0 and + hasattr(self, 'right_texture_id') and self.right_texture_id and self.right_texture_id > 0 + ) + + if not texture_ids_cached: + print(" ⚠️ 纹理ID未缓存,正在准备纹理...") + if not self._prepare_and_cache_textures(): + print(" ❌ 纹理准备和缓存失败") + return False + print(f" ✅ 纹理ID缓存成功 - 左眼:{self.left_texture_id}, 右眼:{self.right_texture_id}") + else: + print(f" ✅ 纹理ID已缓存 - 左眼:{self.left_texture_id}, 右眼:{self.right_texture_id}") + + # 检查OpenVR Texture对象是否已创建 + print(" 检查OpenVR Texture对象...") + ovr_textures_exist = ( + hasattr(self, '_left_ovr_texture') and self._left_ovr_texture and + hasattr(self, '_right_ovr_texture') and self._right_ovr_texture + ) + + if not ovr_textures_exist: + print(" ⚠️ OpenVR Texture对象未创建,正在创建...") + self._create_cached_ovr_textures() + print(" ✅ OpenVR Texture对象创建成功") + else: + print(" ✅ OpenVR Texture对象已存在") + + print(" ✅ VR测试模式纹理资源检查完成,可安全启用纹理提交") + return True + + except Exception as e: + print(f" ❌ VR测试模式纹理资源检查失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_cached_ovr_textures(self): + """创建缓存的OpenVR Texture对象 - 避免每帧创建新对象""" + try: + import openvr + self._left_ovr_texture = openvr.Texture_t() + self._right_ovr_texture = openvr.Texture_t() + + # 设置固定属性(这些不变) + self._left_ovr_texture.eType = openvr.TextureType_OpenGL + self._left_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + self._right_ovr_texture.eType = openvr.TextureType_OpenGL + self._right_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + + print("✅ OpenVR Texture对象缓存已创建") + except Exception as e: + print(f"⚠️ OpenVR Texture对象创建失败: {e}") + # 不抛出异常,使用备用方案 + + def _batch_submit_textures(self): + """批量提交两眼纹理 - OpenVR最佳实践 + + 基于官方hellovr示例的实现: + - 两眼都渲染完成后,快速连续提交 + - 减少submit阻塞时间,避免错过VSync窗口 + - 这是解决36FPS问题的关键 + """ + try: + if not self.vr_compositor: + return False + + # 检查纹理是否准备好 + if not (self.vr_left_texture and self.vr_right_texture): + return False + + # 🚀 关键:快速连续提交两眼,最小化阻塞时间 + # 这符合OpenVR官方示例的做法 + success_left = False + success_right = False + + # 提交左眼纹理 + try: + self.submit_texture(openvr.Eye_Left, self.vr_left_texture) + success_left = True + except Exception as e: + print(f"❌ 批量提交左眼失败: {e}") + + # 立即提交右眼纹理(不等待) + try: + self.submit_texture(openvr.Eye_Right, self.vr_right_texture) + success_right = True + except Exception as e: + print(f"❌ 批量提交右眼失败: {e}") + + # 🚀 关键修复:调用PostPresentHandoff解除compositor阻塞 + # 这是解决36FPS问题的核心 - 确保compositor不会等待VSync + if success_left and success_right: + try: + # PostPresentHandoff告诉compositor我们已完成帧处理 + # 防止compositor等待下一个VSync周期 + if hasattr(self.vr_compositor, 'postPresentHandoff'): + self.vr_compositor.postPresentHandoff() + elif hasattr(self.vr_compositor, 'PostPresentHandoff'): + self.vr_compositor.PostPresentHandoff() + else: + # 备用方案:如果没有PostPresentHandoff,记录警告 + if not hasattr(self, '_post_present_warning_logged'): + print("⚠️ PostPresentHandoff方法未找到,可能影响时序") + self._post_present_warning_logged = True + except Exception as handoff_error: + if not hasattr(self, '_handoff_error_logged'): + print(f"⚠️ PostPresentHandoff调用失败: {handoff_error}") + self._handoff_error_logged = True + + # 记录批量提交状态(仅首次成功时) + if not hasattr(self, '_batch_submit_success_logged'): + print("✅ OpenVR批量提交模式+PostPresentHandoff已启用") + print(" 这应该解决36FPS → 72FPS的问题") + self._batch_submit_success_logged = True + return True + else: + return False + + except Exception as e: + print(f"❌ 批量提交纹理失败: {e}") + return False + + def disable_vr_test_mode(self): + """禁用VR测试模式""" + try: + print("🧪 禁用VR测试模式...") + + # 清理测试显示 + self._cleanup_test_display() + + # 清理性能HUD + self._cleanup_test_performance_hud() + + # 关闭测试模式 + self.vr_test_mode = False + self.test_mode_initialized = False + + # 重置HUD更新计数器 + self.hud_update_counter = 0 + + # 恢复正常VR模式或禁用VR + if self.vr_enabled: + print(" 选择: [1] 恢复正常VR模式 [2] 完全禁用VR") + # 这里可以让用户选择,现在默认禁用VR + self.disable_vr() + + print("✅ VR测试模式已禁用") + return True + + except Exception as e: + print(f"❌ 禁用VR测试模式失败: {e}") + return False + + def switch_test_display_mode(self, display_mode): + """切换测试显示模式 + + Args: + display_mode: 'stereo', 'left', 'right' + """ + if not self.vr_test_mode: + print("⚠️ 请先启用VR测试模式") + return False + + if display_mode not in ['stereo', 'left', 'right']: + print(f"⚠️ 无效的显示模式: {display_mode}") + return False + + print(f"🧪 切换显示模式: {self.test_display_mode} → {display_mode}") + + # 如果切换模式,需要清理旧的显示并重置标志位 + if self.test_display_mode != display_mode: + self._cleanup_test_display() + + self.test_display_mode = display_mode + + # 更新显示 + self._update_test_display() + return True + + def _initialize_test_display(self): + """初始化测试显示系统""" + try: + print("🔧 正在初始化测试显示系统...") + + # 导入必要的Panda3D组件 + from panda3d.core import CardMaker, PandaNode + + # 创建显示四边形 + cm = CardMaker("vr_test_display") + + if self.test_display_mode == 'stereo': + # 并排显示:左眼在左半屏,右眼在右半屏 + cm.setFrame(-1, 1, -0.5, 0.5) # 全屏 + else: + # 单眼显示:占据整个屏幕 + cm.setFrame(-1, 1, -1, 1) + + # 创建节点 + self.test_display_quad = self.world.render2d.attachNewNode(cm.generate()) + + # 设置初始纹理 + self._update_test_display() + + # 设置渲染状态 + self.test_display_quad.setTransparency(0) # 不透明 + self.test_display_quad.setBin("background", 0) # 背景层 + + self.test_mode_initialized = True + print("✅ 测试显示系统初始化完成") + return True + + except Exception as e: + print(f"❌ 测试显示系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def _update_test_display(self): + """更新测试显示内容""" + try: + if not self.test_mode_initialized: + return + + # 根据显示模式设置纹理 + if self.test_display_mode == 'left': + if not self.test_display_quad: + return + if self.vr_left_texture: + self.test_display_quad.setTexture(self.vr_left_texture) + elif self.test_display_mode == 'right': + if not self.test_display_quad: + return + if self.vr_right_texture: + self.test_display_quad.setTexture(self.vr_right_texture) + elif self.test_display_mode == 'stereo': + # 立体显示:只在第一次创建,后续只更新纹理 + if not self.stereo_display_created: + self._create_stereo_display() + else: + # 只更新纹理,不重新创建 + if self.test_display_quad and self.vr_left_texture: + self.test_display_quad.setTexture(self.vr_left_texture) + if self.test_right_quad and self.vr_right_texture: + self.test_right_quad.setTexture(self.vr_right_texture) + + except Exception as e: + print(f"⚠️ 更新测试显示失败: {e}") + + def _create_stereo_display(self): + """创建左右眼并排显示""" + try: + # 如果已经创建,直接返回 + if self.stereo_display_created and self.test_display_quad and self.test_right_quad: + return + + # 移除旧的显示 + if self.test_display_quad: + self.test_display_quad.removeNode() + if self.test_right_quad: + self.test_right_quad.removeNode() + + from panda3d.core import CardMaker + + # 创建左眼显示 + left_cm = CardMaker("vr_test_left") + left_cm.setFrame(-1, 0, -1, 1) # 左半屏 + left_quad = self.world.render2d.attachNewNode(left_cm.generate()) + if self.vr_left_texture: + left_quad.setTexture(self.vr_left_texture) + + # 创建右眼显示 + right_cm = CardMaker("vr_test_right") + right_cm.setFrame(0, 1, -1, 1) # 右半屏 + right_quad = self.world.render2d.attachNewNode(right_cm.generate()) + if self.vr_right_texture: + right_quad.setTexture(self.vr_right_texture) + + # 保存引用 + self.test_display_quad = left_quad # 保存左眼引用 + self.test_right_quad = right_quad # 额外保存右眼引用 + self.stereo_display_created = True # 标记已创建 + + print("✓ 立体显示已创建") + + except Exception as e: + print(f"⚠️ 创建立体显示失败: {e}") + self.stereo_display_created = False + + def _cleanup_test_display(self): + """清理测试显示""" + try: + if self.test_display_quad: + self.test_display_quad.removeNode() + self.test_display_quad = None + + if hasattr(self, 'test_right_quad') and self.test_right_quad: + self.test_right_quad.removeNode() + self.test_right_quad = None + + # 重置立体显示标志位 + self.stereo_display_created = False + + print("✓ 测试显示已清理") + + except Exception as e: + print(f"⚠️ 清理测试显示失败: {e}") + + def _initialize_test_performance_hud(self): + """初始化性能HUD""" + try: + print("🔧 正在初始化性能HUD...") + + from direct.gui.OnscreenText import OnscreenText + from panda3d.core import TextNode + + # 创建性能文本显示 + self.test_performance_text = OnscreenText( + text="初始化中...", + pos=(-0.95, 0.9), # 左上角,调整到屏幕内 + scale=0.05, + fg=(1, 1, 0, 1), # 黄色 + align=TextNode.ALeft, + shadow=(0, 0, 0, 0.5), # 黑色阴影 + parent=self.world.render2d + ) + + print("✅ 性能HUD初始化完成") + return True + + except Exception as e: + print(f"❌ 性能HUD初始化失败: {e}") + return False + + def _update_test_performance_hud(self): + """更新性能HUD显示(限制更新频率避免重影)""" + try: + if not self.test_performance_text or not self.vr_test_mode: + return + + # 增加计数器 + self.hud_update_counter += 1 + + # 只在指定间隔更新文本,避免重影 + if self.hud_update_counter < self.hud_update_interval: + return + + # 重置计数器 + self.hud_update_counter = 0 + + # 收集性能数据 + left_render_time = getattr(self, 'left_render_time', 0) + right_render_time = getattr(self, 'right_render_time', 0) + total_render_time = left_render_time + right_render_time + + # 计算FPS + current_fps = self.vr_fps if hasattr(self, 'vr_fps') else 0 + + # 获取系统性能 + cpu_usage = getattr(self, 'cpu_usage', 0) + memory_usage = getattr(self, 'memory_usage', 0) + gpu_usage = getattr(self, 'gpu_usage', 0) + + # 构建显示文本 + hud_text = f"""VR TEST MODE - {self.test_display_mode.upper()} + +Render Performance: + FPS: {current_fps:.1f} + Left Eye: {left_render_time:.2f}ms + Right Eye: {right_render_time:.2f}ms + Total: {total_render_time:.2f}ms + +System Performance: + CPU: {cpu_usage:.1f}% + Memory: {memory_usage:.1f}% + GPU: {gpu_usage:.1f}% + +Render Count: + Left Eye: {getattr(self, 'left_render_count', 0)} + Right Eye: {getattr(self, 'right_render_count', 0)} + +Target: <13.9ms@72Hz +Status: {'GOOD' if total_render_time < 10 else 'HIGH' if total_render_time < 16 else 'SLOW'} + +Hotkeys: +F1=Left F2=Right F3=SideBySide +ESC=Exit Test Mode""" + + # 更新文本 + self.test_performance_text.setText(hud_text) + + except Exception as e: + print(f"⚠️ 更新性能HUD失败: {e}") + + def _cleanup_test_performance_hud(self): + """清理性能HUD""" + try: + if self.test_performance_text: + self.test_performance_text.destroy() + self.test_performance_text = None + + print("✓ 性能HUD已清理") + + except Exception as e: + print(f"⚠️ 清理性能HUD失败: {e}") + + def get_test_mode_status(self): + """获取测试模式状态""" + if not self.vr_test_mode: + return None + + left_render_time = getattr(self, 'left_render_time', 0) + right_render_time = getattr(self, 'right_render_time', 0) + total_render_time = left_render_time + right_render_time + + return { + 'test_mode_enabled': self.vr_test_mode, + 'display_mode': self.test_display_mode, + 'vr_fps': getattr(self, 'vr_fps', 0), + 'left_render_time': left_render_time, + 'right_render_time': right_render_time, + 'total_render_time': total_render_time, + 'left_render_count': getattr(self, 'left_render_count', 0), + 'right_render_count': getattr(self, 'right_render_count', 0), + 'performance_rating': ('excellent' if total_render_time < 10 else + 'good' if total_render_time < 16 else 'poor') + } + + def set_test_mode_features(self, submit_texture=None, wait_poses=None): + """设置测试模式功能开关 - 用于渐进式调试 + + Args: + submit_texture: 是否在测试模式启用纹理提交(True/False/None=保持当前) + wait_poses: 是否在测试模式启用waitGetPoses(True/False/None=保持当前) + """ + if submit_texture is not None: + self.test_mode_submit_texture = submit_texture + if wait_poses is not None: + self.test_mode_wait_poses = wait_poses + + print(f"🔧 VR测试模式功能设置:") + print(f" 纹理提交: {'启用' if self.test_mode_submit_texture else '禁用'}") + print(f" 姿态等待: {'启用' if self.test_mode_wait_poses else '禁用'}") + + # 如果当前在测试模式,输出预期效果 + if self.vr_test_mode: + print(f" 当前测试模式已启用,设置将立即生效") + else: + print(f" 设置已保存,将在下次启用测试模式时生效") + + def get_test_mode_features(self): + """获取当前测试模式功能设置""" + return { + 'submit_texture': self.test_mode_submit_texture, + 'wait_poses': self.test_mode_wait_poses, + 'test_mode_active': self.vr_test_mode + } + + def run_vr_performance_test(self, duration_seconds=30, display_mode='stereo'): + """运行VR性能测试 - 简单的测试入口方法 + + Args: + duration_seconds: 测试持续时间(秒) + display_mode: 显示模式 ('stereo', 'left', 'right') + + Returns: + dict: 测试结果统计 + """ + print("🧪 ======= VR性能测试开始 =======") + print(f" 测试模式: {display_mode}") + print(f" 测试时长: {duration_seconds}秒") + print(" 按ESC键提前退出测试") + print(" F1=左眼 F2=右眼 F3=并排显示") + print("=================================") + + # 启动测试模式 + if not self.enable_vr_test_mode(display_mode): + print("❌ VR测试模式启动失败") + return None + + # 记录测试开始状态 + import time + test_start_time = time.time() + start_frame_count = getattr(self, 'frame_count', 0) + + print("✅ VR测试模式已启动") + print(" - VR内容直接显示在屏幕上") + print(" - 无OpenVR纹理提交开销") + print(" - 实时性能监控已启用") + print(f" - 当前显示: {display_mode.upper()}模式") + print("\n开始性能测试...") + + # 等待测试完成(用户手动停止或超时) + print(f"\n📊 测试将运行 {duration_seconds} 秒") + print(" 实时性能数据显示在屏幕左上角") + print(" 观察帧率和渲染时间变化") + + # 这里可以添加自动停止逻辑,但现在让用户手动控制 + print(f"\n⏰ 请观察 {duration_seconds} 秒后手动调用 disable_vr_test_mode() 停止测试") + print(" 或者随时调用 get_test_mode_status() 查看当前状态") + + return { + 'status': 'running', + 'start_time': test_start_time, + 'start_frame': start_frame_count, + 'display_mode': display_mode, + 'instructions': { + 'stop_test': 'vr_manager.disable_vr_test_mode()', + 'check_status': 'vr_manager.get_test_mode_status()', + 'switch_mode': 'vr_manager.switch_test_display_mode("left/right/stereo")' + } } \ No newline at end of file diff --git a/core/vr_manager.py.backup.stage2 b/core/vr_manager.py.backup.stage2 index 628dc076..c9d5b67f 100644 --- a/core/vr_manager.py.backup.stage2 +++ b/core/vr_manager.py.backup.stage2 @@ -1,4072 +1,4072 @@ -""" -VR管理器模块 - -负责VR功能的初始化、渲染和交互: -- OpenVR/SteamVR集成 -- VR头显跟踪和渲染 -- VR控制器交互 -- VR模式切换 -""" - -import sys -import gc -import numpy as np -from panda3d.core import ( - WindowProperties, GraphicsPipe, FrameBufferProperties, - GraphicsOutput, Texture, Camera, PerspectiveLens, MatrixLens, - Mat4, Vec3, TransformState, RenderState, CardMaker, - BitMask32, PandaNode, NodePath, LMatrix4, LVector3, LVector4, - CS_yup_right, CS_default, PythonCallbackObject -) -from direct.task import Task -from direct.showbase.DirectObject import DirectObject - -try: - import openvr - OPENVR_AVAILABLE = True -except ImportError: - OPENVR_AVAILABLE = False - print("警告: OpenVR未安装,VR功能将不可用") - -# 导入手柄控制器、动作系统、交互系统、摇杆系统和传送系统 -from core.vr.tracking.controllers import LeftController, RightController -from core.vr.interaction.actions import VRActionManager -from core.vr.interaction.grab import VRInteractionManager -from core.vr.interaction.joystick import VRJoystickManager -from core.vr.interaction.teleport import VRTeleportSystem -from core.vr.performance.monitoring import VRPerformanceMonitor -from enum import Enum - - -class VRRenderMode(Enum): - """VR渲染模式枚举""" - NORMAL = "normal" # 普通渲染模式 - RENDER_PIPELINE = "render_pipeline" # RenderPipeline高级渲染模式 - - -class VRManager(DirectObject): - """VR管理器类 - 处理所有VR相关功能""" - - def __init__(self, world): - """初始化VR管理器 - - Args: - world: 主世界对象引用 - """ - super().__init__() - - self.world = world - self.vr_system = None - self.vr_enabled = False - self.vr_initialized = False - - # VR渲染相关 - self.vr_left_eye_buffer = None - self.vr_right_eye_buffer = None - self.vr_left_camera = None - self.vr_right_camera = None - self.vr_compositor = None - - # VR纹理和ID缓存 - 修复重复准备问题 - self.vr_left_texture = None - self.vr_right_texture = None - self.left_texture_id = None # 缓存左眼纹理的OpenGL ID - self.right_texture_id = None # 缓存右眼纹理的OpenGL ID - self.textures_prepared = False # 标记纹理是否已准备 - - # VR跟踪数据 - self.hmd_pose = Mat4.identMat() - self.controller_poses = {} - self.tracked_device_poses = [] - self.poses = None # OpenVR渲染姿态数组 - self.game_poses = None # OpenVR游戏逻辑姿态数组 - - # 🚀 对象池和缓存系统 - 修复16-19帧周期性GPU峰值 - self._matrix_pool = [] # Mat4对象池 - self._matrix_pool_size = 8 # 池大小,足够处理多个控制器 - self._cached_matrices = {} # 设备ID到矩阵的缓存 - self._controller_poses_cache = {} # 控制器姿态缓存,避免每帧clear() - - # 🚀 OpenVR Texture对象缓存 - 避免每帧创建openvr.Texture_t() - self._left_ovr_texture = None # 左眼纹理对象缓存 - self._right_ovr_texture = None # 右眼纹理对象缓存 - - # Python垃圾回收控制 - self._gc_control_enabled = True # 是否启用GC控制 - self._gc_disabled = False # GC是否被禁用 - self._manual_gc_interval = 900 # 每900帧手动触发一次GC (15秒@60fps) - 减少GC频率 - self._last_manual_gc_frame = 0 - - # 🚀 立即初始化对象池和GC控制(在其他组件之前) - self._initialize_object_pools() - - # VR渲染参数 - self.eye_width = 1080 - self.eye_height = 1200 - self.near_clip = 0.1 - self.far_clip = 1000.0 - - # VR分辨率缩放优化 - self.resolution_scale = 0.75 # 默认0.75倍分辨率,性能和质量平衡 - self.base_eye_width = 1080 # 原始推荐分辨率 - self.base_eye_height = 1200 - self.scaled_eye_width = 1080 # 实际使用的缩放后分辨率 - self.scaled_eye_height = 1200 - - # VR质量预设 - self.quality_presets = { - 'performance': 0.6, # 性能模式 - 约60%分辨率 - 'balanced': 0.75, # 平衡模式 - 约75%分辨率 - 'quality': 1.0 # 质量模式 - 100%分辨率 - } - self.current_quality_preset = 'balanced' # 默认平衡模式 - - # 🎨 VR渲染模式配置 - RenderPipeline集成 - self.vr_render_mode = VRRenderMode.NORMAL # 默认使用普通渲染模式 - self.render_pipeline_enabled = False # RenderPipeline是否已启用 - self.vr_pipeline_left_target = None # 左眼RenderPipeline渲染目标 - self.vr_pipeline_right_target = None # 右眼RenderPipeline渲染目标 - self.pipeline_resolution_scale = 0.75 # RenderPipeline模式下的分辨率缩放(性能优化) - self.vr_pipeline_controller = None # VR Pipeline控制器(管理完整的VR渲染管线) - - # RenderPipeline VR优化配置 - self.pipeline_vr_config = { - 'enable_shadows': True, # 启用阴影 - 'enable_ao': True, # 启用环境光遮蔽 - 'enable_bloom': False, # 禁用泛光(VR性能考虑) - 'enable_motion_blur': False, # 禁用运动模糊(VR中会引起不适) - 'enable_ssr': False, # 禁用屏幕空间反射(性能密集) - 'shadow_quality': 'medium', # 阴影质量:low/medium/high - 'ao_quality': 'low', # AO质量:low/medium/high - } - - # VR任务 - self.vr_task = None - - # VR锚点层级系统 - self.tracking_space = None - self.hmd_anchor = None - self.left_eye_anchor = None - self.right_eye_anchor = None - - # 坐标系转换矩阵 - 使用Panda3D内置方法 - self.coord_mat = LMatrix4.convert_mat(CS_yup_right, CS_default) - self.coord_mat_inv = LMatrix4.convert_mat(CS_default, CS_yup_right) - - # OpenVR 帧ID跟踪(防止重复提交) - self.openvr_frame_id = 0 - self.left_eye_last_render_frame = -1 - self.right_eye_last_render_frame = -1 - - # 🧪 VR测试模式 - self.vr_test_mode = False # 是否启用VR测试模式 - self.test_display_mode = 'stereo' # 'left', 'right', 'stereo' - self.test_display_quad = None # 测试显示的四边形 - self.test_right_quad = None # 右眼显示的四边形(立体模式) - self.stereo_display_created = False # 立体显示是否已创建 - self.test_performance_hud = None # 性能HUD - self.test_performance_text = None # 性能文本节点 - self.test_mode_initialized = False # 测试模式是否已初始化 - self.hud_update_counter = 0 # HUD更新计数器 - self.hud_update_interval = 30 # HUD更新间隔(帧数),30帧约0.5秒@60fps - - # 🔧 VR测试模式调试选项 - 逐步启用普通VR功能 - self.test_mode_submit_texture = False # 是否在测试模式提交纹理到OpenVR - self.test_mode_wait_poses = False # 是否在测试模式调用waitGetPoses - - # waitGetPoses调用策略配置 - 使用高性能的update_task策略 - self.use_prediction_time = 0.011 # 11ms的预测时间 - OpenVR标准值,平衡准确性和延迟 - self.poses_updated_in_task = True # 始终在更新任务中获取姿态(Running Start模式) - - # 🎨 初始化VR配置管理器 - try: - from core.vr.config.vr_config import VRConfigManager - self.config_manager = VRConfigManager() - # 加载配置并应用 - self.config_manager.apply_config_to_vr_manager(self) - print("✓ VR配置管理器初始化完成并已加载配置") - except Exception as e: - print(f"⚠️ VR配置管理器初始化失败: {e}") - self.config_manager = None - - # VR提交策略 - 基于参考实现 - self.submit_together = True # 是否在right_cb中同时提交左右眼 - - - # Running Start标记 - Valve最佳实践 - self._waitgetposes_called_this_frame = False - - # 姿态缓存 - 修复时序不匹配 - self._cached_render_poses = None # 用于渲染的缓存姿态 - self._first_frame = True # 首帧标记 - - # ATW控制选项 - 备选方案 - self.disable_async_reprojection = False # 是否禁用异步重投影 - - # VR手柄控制器 - self.left_controller = None - self.right_controller = None - self.controllers = {} # 设备索引到控制器的映射 - self.tracked_device_anchors = {} # 跟踪设备锚点 - - # VR动作系统 - 可选择禁用(用于API兼容性问题) - self.disable_action_system = True # 禁用动作系统,使用直接控制器输入 - - if not self.disable_action_system: - try: - self.action_manager = VRActionManager(self) - print("✓ VR动作管理器初始化完成") - except Exception as e: - print(f"⚠️ VR动作管理器初始化失败: {e}") - self.action_manager = None - else: - self.action_manager = None - print("🚫 VR动作系统已禁用,使用直接控制器输入") - - # VR交互系统 - 添加异常保护 - try: - self.interaction_manager = VRInteractionManager(self) - print("✓ VR交互管理器初始化完成") - except Exception as e: - print(f"⚠️ VR交互管理器初始化失败: {e}") - self.interaction_manager = None - - # VR传送系统 - 添加异常保护 - try: - self.teleport_system = VRTeleportSystem(self) - print("✓ VR传送系统初始化完成") - except Exception as e: - print(f"⚠️ VR传送系统初始化失败: {e}") - self.teleport_system = None - - # VR摇杆系统 - 添加异常保护 - try: - self.joystick_manager = VRJoystickManager(self) - print("✓ VR摇杆管理器初始化完成") - except Exception as e: - print(f"⚠️ VR摇杆管理器初始化失败: {e}") - self.joystick_manager = None - - # 🎯 性能监控系统 - 模块化重构 - try: - self.performance_monitor = VRPerformanceMonitor(self) - print("✓ VR性能监控系统初始化完成") - except Exception as e: - print(f"⚠️ VR性能监控系统初始化失败: {e}") - self.performance_monitor = None - - print("✓ VR管理器初始化完成") - - def _initialize_object_pools(self): - """初始化对象池 - 修复16-19帧周期性GPU峰值""" - try: - # 预填充Mat4对象池 - for _ in range(self._matrix_pool_size): - self._matrix_pool.append(Mat4()) - - print(f"✅ Mat4对象池初始化完成 - 池大小: {self._matrix_pool_size}") - - # 🚀 预创建OpenVR Texture对象 - 消除每帧创建openvr.Texture_t()的开销 - try: - import openvr - self._left_ovr_texture = openvr.Texture_t() - self._right_ovr_texture = openvr.Texture_t() - - # 设置固定属性(这些不变) - self._left_ovr_texture.eType = openvr.TextureType_OpenGL - self._left_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma - self._right_ovr_texture.eType = openvr.TextureType_OpenGL - self._right_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma - - print("✅ OpenVR Texture对象缓存已创建 - 避免每帧创建新对象") - except Exception as texture_error: - print(f"⚠️ OpenVR Texture对象创建失败: {texture_error}") - # 不影响整体初始化,但性能可能不是最优 - - # 启用GC控制 - if self._gc_control_enabled: - # 禁用自动垃圾回收,改为手动控制 - gc.disable() - self._gc_disabled = True - print("✅ Python垃圾回收已禁用,改为手动控制") - - except Exception as e: - print(f"⚠️ 对象池初始化失败: {e}") - - def _get_pooled_matrix(self): - """从对象池获取Mat4对象""" - if self._matrix_pool: - return self._matrix_pool.pop() - else: - # 池为空时创建新对象(不应该发生,但作为备用) - return Mat4() - - def _return_pooled_matrix(self, matrix): - """将Mat4对象返回对象池""" - if len(self._matrix_pool) < self._matrix_pool_size: - # 重置矩阵到单位矩阵 - matrix.identMat() - self._matrix_pool.append(matrix) - - def _manual_gc_control(self): - """手动垃圾回收控制 - 避免VR渲染期间的GC峰值""" - if not self._gc_control_enabled or not self._gc_disabled: - return - - # 🚀 智能GC间隔:性能模式下减少GC频率 - current_interval = self._manual_gc_interval - if self.performance_mode_enabled: - current_interval = self._manual_gc_interval * 2 # 性能模式下间隔翻倍 - - # 每N帧手动触发一次垃圾回收 - if self.frame_count - self._last_manual_gc_frame >= current_interval: - # 在非渲染关键时刻触发GC - collected = gc.collect() - self._last_manual_gc_frame = self.frame_count - - # 仅在收集到对象时输出信息 - if collected > 0: - print(f"🗑️ 手动GC: 清理了 {collected} 个对象 (帧#{self.frame_count})") - - def _update_matrix_from_openvr(self, panda_mat, ovr_matrix): - """直接更新现有Mat4对象的数值,避免创建新对象""" - # 复用_convert_openvr_matrix_to_panda中的转换逻辑 - # X轴行:Panda3D的X轴对应OpenVR的X轴 - panda_mat.setCell(0, 0, ovr_matrix[0][0]) # X_x → X_x - panda_mat.setCell(0, 1, ovr_matrix[0][1]) # X_y → X_y - panda_mat.setCell(0, 2, ovr_matrix[0][2]) # X_z → X_z - panda_mat.setCell(0, 3, ovr_matrix[0][3]) # 位移X分量 - - # Y轴行:Panda3D的Y轴对应OpenVR的-Z轴 - panda_mat.setCell(1, 0, -ovr_matrix[2][0]) # -Z_x → Y_x - panda_mat.setCell(1, 1, -ovr_matrix[2][1]) # -Z_y → Y_y - panda_mat.setCell(1, 2, -ovr_matrix[2][2]) # -Z_z → Y_z - panda_mat.setCell(1, 3, -ovr_matrix[2][3]) # 位移Y分量(-Z位移) - - # Z轴行:Panda3D的Z轴对应OpenVR的Y轴 - panda_mat.setCell(2, 0, ovr_matrix[1][0]) # Y_x → Z_x - panda_mat.setCell(2, 1, ovr_matrix[1][1]) # Y_y → Z_y - panda_mat.setCell(2, 2, ovr_matrix[1][2]) # Y_z → Z_z - panda_mat.setCell(2, 3, ovr_matrix[1][3]) # 位移Z分量(Y位移) - - # 齐次坐标 - panda_mat.setCell(3, 0, 0) - panda_mat.setCell(3, 1, 0) - panda_mat.setCell(3, 2, 0) - panda_mat.setCell(3, 3, 1) - - def convert_mat(self, mat): - """ - 将OpenVR矩阵转换为Panda3D矩阵 - 基于参考实现 - """ - if len(mat.m) == 4: - result = LMatrix4( - mat.m[0][0], mat.m[1][0], mat.m[2][0], mat.m[3][0], - mat.m[0][1], mat.m[1][1], mat.m[2][1], mat.m[3][1], - mat.m[0][2], mat.m[1][2], mat.m[2][2], mat.m[3][2], - mat.m[0][3], mat.m[1][3], mat.m[2][3], mat.m[3][3]) - elif len(mat.m) == 3: - result = LMatrix4( - mat.m[0][0], mat.m[1][0], mat.m[2][0], 0.0, - mat.m[0][1], mat.m[1][1], mat.m[2][1], 0.0, - mat.m[0][2], mat.m[1][2], mat.m[2][2], 0.0, - mat.m[0][3], mat.m[1][3], mat.m[2][3], 1.0) - return result - - def is_vr_available(self): - """检查VR系统是否可用""" - if not OPENVR_AVAILABLE: - return False - - try: - # 检查SteamVR是否运行 - return openvr.isRuntimeInstalled() and openvr.isHmdPresent() - except Exception as e: - print(f"VR检查失败: {e}") - return False - - def initialize_vr(self): - """初始化VR系统""" - if not OPENVR_AVAILABLE: - print("❌ OpenVR不可用,无法初始化VR") - return False - - if self.vr_initialized: - print("VR系统已经初始化") - return True - - try: - print("🔄 正在初始化VR系统...") - - # 🚀 确保对象池已正确初始化(备用检查) - if not hasattr(self, '_matrix_pool') or len(self._matrix_pool) == 0: - print("⚠️ 对象池未初始化,正在重新初始化...") - self._initialize_object_pools() - - # 初始化OpenVR - 使用Scene应用类型确保正确的焦点管理 - self.vr_system = openvr.init(openvr.VRApplication_Scene) - if not self.vr_system: - print("❌ 无法初始化OpenVR系统") - return False - - # 获取compositor - self.vr_compositor = openvr.VRCompositor() - if not self.vr_compositor: - print("❌ 无法获取VR Compositor") - return False - - # 获取推荐的渲染目标尺寸 - base_width, base_height = self.vr_system.getRecommendedRenderTargetSize() - self.base_eye_width = base_width - self.base_eye_height = base_height - - # 应用分辨率缩放 - self.scaled_eye_width = int(base_width * self.resolution_scale) - self.scaled_eye_height = int(base_height * self.resolution_scale) - - # 使用缩放后的分辨率作为实际渲染分辨率 - self.eye_width = self.scaled_eye_width - self.eye_height = self.scaled_eye_height - - self.current_eye_resolution = (self.eye_width, self.eye_height) - self.recommended_eye_resolution = (base_width, base_height) - - print(f"✓ VR基础分辨率: {base_width}x{base_height}") - print(f"✓ VR缩放系数: {self.resolution_scale}") - print(f"✓ VR实际分辨率: {self.eye_width}x{self.eye_height}") - print(f"📊 分辨率优化: {(1 - self.resolution_scale**2) * 100:.1f}% 像素减少") - - # 获取VR系统信息 - try: - # 获取显示频率 - self.vr_display_frequency = self.vr_system.getFloatTrackedDeviceProperty( - openvr.k_unTrackedDeviceIndex_Hmd, - openvr.Prop_DisplayFrequency_Float - ) - print(f"✓ VR显示频率: {self.vr_display_frequency} Hz") - - # 获取IPD(瞳距) - ipd = self.vr_system.getFloatTrackedDeviceProperty( - openvr.k_unTrackedDeviceIndex_Hmd, - openvr.Prop_UserIpdMeters_Float - ) - print(f"✓ IPD(瞳距): {ipd * 1000:.1f}mm") - - # 获取设备制造商和型号 - manufacturer = self.vr_system.getStringTrackedDeviceProperty( - openvr.k_unTrackedDeviceIndex_Hmd, - openvr.Prop_ManufacturerName_String - ) - model = self.vr_system.getStringTrackedDeviceProperty( - openvr.k_unTrackedDeviceIndex_Hmd, - openvr.Prop_ModelNumber_String - ) - print(f"✓ VR设备: {manufacturer} {model}") - - # 获取更多系统配置信息 - try: - # 获取刷新率相关信息 - seconds_since_last_vsync = self.vr_system.getFloatTrackedDeviceProperty( - openvr.k_unTrackedDeviceIndex_Hmd, - openvr.Prop_SecondsFromVsyncToPhotons_Float - ) - self.vsync_to_photons_ms = seconds_since_last_vsync * 1000 - print(f"✓ VSync到光子延迟: {self.vsync_to_photons_ms:.2f}ms") - - # 获取显示延迟信息 - display_refresh_rate = self.vr_display_frequency - if display_refresh_rate > 0: - self.target_frame_time_ms = 1000.0 / display_refresh_rate - print(f"✓ 目标帧时间: {self.target_frame_time_ms:.2f}ms") - - # 计算VSync时间窗口 - self.vsync_window_ms = self.target_frame_time_ms * 0.1 # 10%的窗口 - print(f"✓ VSync时间窗口: ±{self.vsync_window_ms:.2f}ms") - - except Exception as vsync_error: - print(f"⚠️ 获取VSync信息失败: {vsync_error}") - - # 检查是否启用了异步重投影 - try: - if hasattr(openvr, 'VRSettings'): - settings = openvr.VRSettings() - if settings: - self.async_reprojection_enabled = settings.getBool("steamvr", "enableAsyncReprojection") - print(f"✓ 异步重投影: {'启用' if self.async_reprojection_enabled else '禁用'}") - - self.motion_smoothing_enabled = settings.getBool("steamvr", "motionSmoothing") - print(f"✓ 运动平滑: {'启用' if self.motion_smoothing_enabled else '禁用'}") - - except Exception as settings_error: - print(f"⚠️ 获取VR设置失败: {settings_error}") - - except Exception as e: - print(f"⚠️ 获取VR系统信息失败: {e}") - - # 创建OpenVR姿态数组 - 分离渲染和游戏姿态 - poses_t = openvr.TrackedDevicePose_t * openvr.k_unMaxTrackedDeviceCount - self.poses = poses_t() # 渲染姿态(预测的) - self.game_poses = poses_t() # 游戏逻辑姿态(当前的) - print("✓ VR渲染和游戏姿态数组已创建") - - # 🔧 关键修复:统一初始化流程 - # 如果目标模式是RenderPipeline,先用普通模式初始化,然后切换 - # 这样两个场景都走相同的、已验证的代码路径 - target_render_mode = self.vr_render_mode - use_deferred_pipeline_switch = False - - if target_render_mode == VRRenderMode.RENDER_PIPELINE: - print("🎨 目标VR渲染模式: RenderPipeline") - print(" 策略:先用普通模式初始化,然后切换到RenderPipeline") - self.vr_render_mode = VRRenderMode.NORMAL - use_deferred_pipeline_switch = True - else: - print(f"🎨 VR渲染模式: {self.vr_render_mode.value}") - - # 创建VR渲染缓冲区 - 始终用普通模式初始化 - if not self._create_vr_buffers(): - print("❌ 创建VR渲染缓冲区失败") - return False - - # 设置VR相机 - if not self._setup_vr_cameras(): - print("❌ 设置VR相机失败") - return False - - # 优化VR渲染管线 - self._optimize_vr_rendering() - - # 初始化手柄控制器 - self._initialize_controllers() - - # 初始化动作系统 - 仅在启用时初始化 - if not self.disable_action_system and self.action_manager: - try: - if not self.action_manager.initialize(): - print("⚠️ VR动作系统初始化失败,但VR系统将继续运行") - except Exception as e: - print(f"⚠️ VR动作系统初始化异常: {e}") - else: - if self.disable_action_system: - print("🚫 VR动作系统已禁用,跳过初始化") - else: - print("⚠️ VR动作管理器未创建,跳过动作系统初始化") - - # 初始化交互系统 - 检查是否存在 - if self.interaction_manager: - try: - if not self.interaction_manager.initialize(): - print("⚠️ VR交互系统初始化失败,但VR系统将继续运行") - except Exception as e: - print(f"⚠️ VR交互系统初始化异常: {e}") - else: - print("⚠️ VR交互管理器未创建,跳过交互系统初始化") - - # 初始化VR传送系统 - if self.teleport_system: - try: - if not self.teleport_system.initialize(): - print("⚠️ VR传送系统初始化失败,但VR系统将继续运行") - except Exception as e: - print(f"⚠️ VR传送系统初始化异常: {e}") - else: - print("⚠️ VR传送系统未创建,跳过传送系统初始化") - - # 初始化VR摇杆系统 - if self.joystick_manager: - try: - # 传入传送系统引用给摇杆管理器 - self.joystick_manager.initialize(self.teleport_system) - print("✓ VR摇杆系统初始化成功") - except Exception as e: - print(f"⚠️ VR摇杆系统初始化异常: {e}") - else: - print("⚠️ VR摇杆管理器未创建,跳过摇杆系统初始化") - - # 可选:禁用异步重投影(备选方案) - if self.disable_async_reprojection: - self._disable_async_reprojection() - - # 启动VR更新任务 - self._start_vr_task() - - self.vr_initialized = True - print("✅ VR系统初始化成功(普通模式)") - - # 🔧 关键修复:如果目标是RenderPipeline模式,现在切换过去 - # 这样场景2(先设RP再进VR)会走场景1(先VR再设RP)相同的代码路径 - if use_deferred_pipeline_switch: - print("\n🔄 现在切换到目标渲染模式: RenderPipeline") - print(" 这将触发buffer重建和visualizer刷新...") - if self.set_vr_render_mode(target_render_mode): - print("✅ 已成功切换到RenderPipeline模式") - else: - print("⚠️ 切换到RenderPipeline失败,保持普通模式") - - return True - - except Exception as e: - print(f"❌ VR初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def _create_vr_buffers(self): - """创建VR渲染缓冲区 - 使用分辨率缩放优化""" - try: - print(f"🔧 创建VR缓冲区:") - print(f" 推荐分辨率: {self.base_eye_width}x{self.base_eye_height}") - print(f" 缩放系数: {self.resolution_scale}") - print(f" 实际分辨率: {self.eye_width}x{self.eye_height}") - print(f" 像素减少: {(1 - self.resolution_scale**2) * 100:.1f}%") - - # 创建左眼纹理和缓冲区 - self.vr_left_texture = self._create_vr_texture("VR Left Eye Texture") - self.vr_left_eye_buffer = self._create_vr_buffer( - "VR Left Eye", - self.vr_left_texture, - self.eye_width, - self.eye_height - ) - - if not self.vr_left_eye_buffer: - print("❌ 创建左眼缓冲区失败") - return False - - # 设置左眼缓冲区属性 - self.vr_left_eye_buffer.setSort(-100) - self.vr_left_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) # 深蓝色背景便于调试 - self.vr_left_eye_buffer.setActive(True) - - # 创建右眼纹理和缓冲区 - self.vr_right_texture = self._create_vr_texture("VR Right Eye Texture") - self.vr_right_eye_buffer = self._create_vr_buffer( - "VR Right Eye", - self.vr_right_texture, - self.eye_width, - self.eye_height - ) - - if not self.vr_right_eye_buffer: - print("❌ 创建右眼缓冲区失败") - return False - - # 设置右眼缓冲区属性 - self.vr_right_eye_buffer.setSort(-99) - self.vr_right_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) # 深蓝色背景便于调试 - self.vr_right_eye_buffer.setActive(True) - - # 🚀 关键优化:立即准备纹理并缓存OpenGL ID,避免后续重复准备 - print("🔧 准备纹理并缓存OpenGL ID...") - if self._prepare_and_cache_textures(): - print("✅ VR渲染缓冲区和纹理缓存创建成功") - return True - else: - print("❌ 纹理准备失败") - return False - - except Exception as e: - print(f"❌ 创建VR缓冲区失败: {e}") - import traceback - traceback.print_exc() - return False - - def _create_vr_texture(self, name): - """创建VR纹理对象 - 基于参考实现""" - texture = Texture(name) - texture.setWrapU(Texture.WMClamp) - texture.setWrapV(Texture.WMClamp) - texture.setMinfilter(Texture.FTLinear) - texture.setMagfilter(Texture.FTLinear) - return texture - - def _prepare_and_cache_textures(self): - """准备纹理并缓存OpenGL ID - 解决重复准备问题""" - try: - # 🔧 验证纹理对象存在 - if not self.vr_left_texture or not self.vr_right_texture: - print("❌ VR纹理对象不存在") - print(f" 左眼纹理: {self.vr_left_texture}") - print(f" 右眼纹理: {self.vr_right_texture}") - return False - - # 获取graphics state guardian和prepared objects - gsg = self.world.win.getGsg() - if not gsg: - print("❌ 无法获取GraphicsStateGuardian") - return False - - prepared_objects = gsg.getPreparedObjects() - if not prepared_objects: - print("❌ 无法获取PreparedObjects") - return False - - # 准备左眼纹理并缓存ID - print(f" 准备左眼纹理: {self.vr_left_texture.getXSize()}x{self.vr_left_texture.getYSize()}") - texture_context = self.vr_left_texture.prepareNow(0, prepared_objects, gsg) - if texture_context and hasattr(texture_context, 'getNativeId'): - self.left_texture_id = texture_context.getNativeId() - if self.left_texture_id > 0: - print(f" ✅ 左眼纹理准备完成 ({self.vr_render_mode.value}): ID={self.left_texture_id}") - else: - print(" ❌ 左眼纹理ID无效") - return False - else: - print(" ❌ 左眼纹理准备失败") - return False - - # 准备右眼纹理并缓存ID - print(f" 准备右眼纹理: {self.vr_right_texture.getXSize()}x{self.vr_right_texture.getYSize()}") - texture_context = self.vr_right_texture.prepareNow(0, prepared_objects, gsg) - if texture_context and hasattr(texture_context, 'getNativeId'): - self.right_texture_id = texture_context.getNativeId() - if self.right_texture_id > 0: - print(f" ✅ 右眼纹理准备完成 ({self.vr_render_mode.value}): ID={self.right_texture_id}") - else: - print(" ❌ 右眼纹理ID无效") - return False - else: - print(" ❌ 右眼纹理准备失败") - return False - - # 标记纹理已准备 - self.textures_prepared = True - print(" ✅ 纹理准备完成,ID已缓存,后续帧将直接使用缓存ID") - return True - - except Exception as e: - print(f"❌ 纹理准备和缓存失败: {e}") - import traceback - traceback.print_exc() - return False - - def _create_vr_buffer(self, name, texture, width, height): - """创建VR渲染缓冲区 - 基于参考实现 + 性能诊断""" - # 设置帧缓冲属性 - fbprops = FrameBufferProperties() - fbprops.setRgbaBits(1, 1, 1, 1) - # 可以在这里添加多重采样抗锯齿 - # fbprops.setMultisamples(4) - - # 创建缓冲区 - buffer = self.world.win.makeTextureBuffer(name, width, height, to_ram=False, fbp=fbprops) - - if buffer: - # 🔍 性能诊断:检查缓冲区类型和属性 - self._diagnose_buffer_performance(buffer, name, width, height) - - # 清除默认渲染纹理 - buffer.clearRenderTextures() - # 🚀 使用RTMBindOrCopy模式 - 已经是最优选择 - # RTMBindOrCopy会尝试直接绑定到纹理,只有硬件不支持时才回退到复制 - # 这已经是Panda3D中最高效的渲染到纹理模式 - buffer.addRenderTexture(texture, GraphicsOutput.RTMBindOrCopy, GraphicsOutput.RTPColor) - - else: - print(f"⚠️ VR缓冲区创建失败: {name} ({width}x{height})") - - return buffer - - def _diagnose_buffer_performance(self, buffer, name, width, height): - """诊断VR缓冲区性能特性""" - try: - # 检查缓冲区类型 - buffer_type = type(buffer).__name__ - - # 检查是否为parasite buffer(性能较差的类型) - is_parasite = 'Parasite' in buffer_type - - # 获取缓冲区信息 - is_valid = buffer.isValid() - is_hardware = hasattr(buffer, 'getGsg') and buffer.getGsg() is not None - - # 输出诊断信息(仅第一次创建时) - if not hasattr(self, '_buffer_diagnosis_shown'): - print(f"🔍 VR缓冲区诊断 [{name}]:") - print(f" 类型: {buffer_type}") - print(f" 分辨率: {width}x{height} ({width*height/1000000:.1f}M像素)") - print(f" 有效性: {'✓' if is_valid else '✗'}") - print(f" 硬件支持: {'✓' if is_hardware else '✗'}") - - if is_parasite: - print(f" ⚠️ 检测到Parasite Buffer - 性能可能受限") - print(f" 建议: 检查显卡是否支持真正的离屏渲染") - else: - print(f" ✓ 使用硬件FBO - 性能良好") - - # 显示帧缓冲区属性 - if hasattr(buffer, 'getFbProperties'): - fbp = buffer.getFbProperties() - if fbp: - print(f" 颜色位数: R{fbp.getRedBits()}G{fbp.getGreenBits()}B{fbp.getBlueBits()}A{fbp.getAlphaBits()}") - if hasattr(fbp, 'getMultisamples') and fbp.getMultisamples() > 0: - print(f" MSAA: {fbp.getMultisamples()}x") - - self._buffer_diagnosis_shown = True - - except Exception as e: - print(f"缓冲区诊断失败: {e}") - - def _create_vr_buffers_with_pipeline(self): - """创建带RenderPipeline的VR渲染缓冲区 - 高级渲染模式""" - try: - print(f"🎨 创建RenderPipeline VR缓冲区:") - print(f" 推荐分辨率: {self.base_eye_width}x{self.base_eye_height}") - print(f" Pipeline缩放: {self.pipeline_resolution_scale}") - - # 检查RenderPipeline是否可用 - if not hasattr(self.world, 'render_pipeline') or not self.world.render_pipeline: - print("❌ RenderPipeline未初始化,无法使用高级渲染模式") - return False - - pipeline = self.world.render_pipeline - - # 计算RenderPipeline模式下的分辨率 - pipeline_width = int(self.base_eye_width * self.pipeline_resolution_scale) - pipeline_height = int(self.base_eye_height * self.pipeline_resolution_scale) - - print(f" 实际分辨率: {pipeline_width}x{pipeline_height}") - print(f" 像素减少: {(1 - self.pipeline_resolution_scale**2) * 100:.1f}%") - - # 导入RenderPipeline相关模块 - try: - from RenderPipelineFile.rpcore.render_target import RenderTarget - except ImportError as e: - print(f"❌ 无法导入RenderPipeline模块: {e}") - return False - - # 导入VR stages模块 - try: - from core.vr.rendering.stages import VRPipelineController - except ImportError as e: - print(f"❌ 无法导入VR stages模块: {e}") - return False - - # 创建VR Pipeline控制器 - print(" 初始化VR Pipeline控制器...") - self.vr_pipeline_controller = VRPipelineController(pipeline) - - # 保存分辨率信息(将在_setup_vr_cameras中使用) - self.pipeline_vr_width = pipeline_width - self.pipeline_vr_height = pipeline_height - - # 应用RenderPipeline效果配置 - self._apply_pipeline_vr_effects() - - self.render_pipeline_enabled = True - print("✅ VR Pipeline控制器已初始化(将在相机设置时创建完整管线)") - - # 检查天空盒状态 - self._check_skybox_status() - - return True - - except Exception as e: - print(f"❌ 创建RenderPipeline VR缓冲区失败: {e}") - import traceback - traceback.print_exc() - return False - - def _apply_pipeline_vr_effects(self): - """为VR场景应用RenderPipeline效果配置""" - try: - print("🎨 应用RenderPipeline VR优化配置...") - - # 根据配置调整RenderPipeline设置 - config = self.pipeline_vr_config - - # 注意:实际的插件启用/禁用需要在pipeline.yaml中配置 - # 这里只是记录当前的VR优化意图 - print(f" 阴影: {'启用' if config['enable_shadows'] else '禁用'}") - print(f" 环境光遮蔽: {'启用' if config['enable_ao'] else '禁用'}") - print(f" 泛光效果: {'启用' if config['enable_bloom'] else '禁用'} (VR推荐禁用)") - print(f" 运动模糊: {'启用' if config['enable_motion_blur'] else '禁用'} (VR推荐禁用)") - print(f" 屏幕空间反射: {'启用' if config['enable_ssr'] else '禁用'} (VR推荐禁用)") - - print("✅ RenderPipeline VR效果配置完成") - - except Exception as e: - print(f"⚠️ 应用RenderPipeline VR效果失败: {e}") - - def _check_skybox_status(self): - """检查并报告天空盒状态""" - try: - print("\n🌌 检查天空盒状态...") - - # 方法1:在render节点下搜索所有名字包含skybox的节点 - skybox_nodes = self.world.render.findAllMatches("**/skybox*") - if skybox_nodes and skybox_nodes.getNumPaths() > 0: - print(f" ✓ 找到 {skybox_nodes.getNumPaths()} 个天空盒节点(使用名称搜索)") - for i in range(skybox_nodes.getNumPaths()): - skybox = skybox_nodes.getPath(i) - print(f" 天空盒 #{i+1}:") - print(f" 名称: {skybox.getName()}") - print(f" 位置: {skybox.getPos()}") - print(f" 缩放: {skybox.getScale()}") - print(f" 父节点: {skybox.getParent().getName()}") - - # 检查是否有shader - if skybox.hasShader(): - print(f" ✓ 已应用shader效果") - else: - print(f" ⚠️ 未应用shader效果") - - # 检查bin设置 - if skybox.hasBin(): - print(f" Bin: {skybox.getBinName()}") - else: - print(" ⚠️ 未找到天空盒(名称搜索)") - - # 方法2:搜索所有GeomNode(几何节点),天空盒通常是一个大的几何体 - print(" 搜索所有几何节点...") - all_geoms = self.world.render.findAllMatches("**/+GeomNode") - print(f" 找到 {all_geoms.getNumPaths()} 个几何节点") - - # 查找大型几何体(缩放值很大,可能是天空盒) - skybox_candidates = [] - for i in range(all_geoms.getNumPaths()): - geom_node = all_geoms.getPath(i) - scale = geom_node.getScale() - # 天空盒通常缩放很大(如40000) - if scale[0] > 1000 or scale[1] > 1000 or scale[2] > 1000: - print(f" ✓ 找到大型几何体(可能是天空盒):") - print(f" 名称: {geom_node.getName()}") - print(f" 缩放: {scale}") - print(f" 位置: {geom_node.getPos()}") - if geom_node.hasShader(): - print(f" ✓ 已应用shader") - skybox_candidates.append(geom_node) - - # 方法3:检查render节点的所有直接子节点(显示全部) - print(" 检查render的所有直接子节点...") - render_children = self.world.render.getChildren() - print(f" render有 {render_children.getNumPaths()} 个直接子节点:") - for i in range(render_children.getNumPaths()): - child = render_children.getPath(i) - scale = child.getScale() - # 标记大型节点(可能是天空盒) - if scale[0] > 1000 or scale[1] > 1000 or scale[2] > 1000: - print(f" - {child.getName()} ⭐ (大型节点,缩放: {scale})") - else: - print(f" - {child.getName()}") - - # 报告结果但不自动创建 - if len(skybox_candidates) == 0: - print(" ⚠️ 未找到明显的天空盒(缩放>1000的几何体)") - print(" 💡 RenderPipeline的天空盒可能在上述列表中,但缩放值不同") - print(" 💡 或者可能RenderPipeline未在VR模式下自动创建天空盒") - else: - print(f" ✓ 找到 {len(skybox_candidates)} 个可能的天空盒") - - print("🌌 天空盒状态检查完成\n") - - except Exception as e: - print(f"⚠️ 天空盒状态检查失败: {e}") - import traceback - traceback.print_exc() - - def _create_vr_skybox(self): - """为VR创建天空盒""" - try: - print(" 🎨 创建VR天空盒...") - - # 检查RenderPipeline是否可用 - if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline: - pipeline = self.world.render_pipeline - - # 使用RenderPipeline的load_default_skybox方法 - print(" 使用RenderPipeline方法创建天空盒...") - skybox = pipeline.common_resources.load_default_skybox() - - if skybox: - # 设置天空盒属性 - skybox.set_scale(40000) # 大型缩放 - skybox.reparent_to(self.world.render) - skybox.set_bin("unsorted", 10000) # 最后渲染 - - # 应用天空盒shader效果 - pipeline.set_effect(skybox, "effects/skybox.yaml", { - "render_shadow": False, - "render_envmap": False, - "render_voxelize": False, - "alpha_testing": False, - "normal_mapping": False, - "parallax_mapping": False - }, 1000) - - print(f" ✅ VR天空盒已创建") - print(f" 名称: {skybox.getName()}") - print(f" 缩放: {skybox.getScale()}") - print(f" 位置: {skybox.getPos()}") - return skybox - else: - print(" ❌ 无法加载天空盒模型") - return None - else: - print(" ❌ RenderPipeline未初始化,无法创建天空盒") - return None - - except Exception as e: - print(f" ❌ 创建VR天空盒失败: {e}") - import traceback - traceback.print_exc() - return None - - def _setup_vr_cameras(self): - """设置VR相机 - 使用锚点层级系统""" - try: - # 创建VR追踪空间锚点层级 - self.tracking_space = self.world.render.attachNewNode('tracking-space') - self.hmd_anchor = self.tracking_space.attachNewNode('hmd-anchor') - self.left_eye_anchor = self.hmd_anchor.attachNewNode('left-eye') - self.right_eye_anchor = self.hmd_anchor.attachNewNode('right-eye') - - # 获取投影矩阵 - projection_left = self.coord_mat_inv * self.convert_mat( - self.vr_system.getProjectionMatrix(openvr.Eye_Left, self.near_clip, self.far_clip)) - projection_right = self.coord_mat_inv * self.convert_mat( - self.vr_system.getProjectionMatrix(openvr.Eye_Right, self.near_clip, self.far_clip)) - - # 创建左眼相机节点 - left_cam_node = Camera('left-cam') - left_lens = MatrixLens() - left_lens.setUserMat(projection_left) - left_cam_node.setLens(left_lens) - - # 创建右眼相机节点 - right_cam_node = Camera('right-cam') - right_lens = MatrixLens() - right_lens.setUserMat(projection_right) - right_cam_node.setLens(right_lens) - - # 附加相机到眼睛锚点 - self.vr_left_camera = self.left_eye_anchor.attachNewNode(left_cam_node) - self.vr_right_camera = self.right_eye_anchor.attachNewNode(right_cam_node) - - # 设置显示区域 - 区分RenderPipeline和普通模式 - if self.vr_render_mode == VRRenderMode.RENDER_PIPELINE and self.vr_pipeline_controller: - # RenderPipeline模式:使用VRPipelineController创建完整管线 - print(" 使用VR Pipeline创建立体渲染管线...") - - # 创建立体渲染管线(GBuffer + Lighting + Final stages) - success = self.vr_pipeline_controller.create_stereo_pipeline( - self.pipeline_vr_width, - self.pipeline_vr_height, - self.vr_left_camera, - self.vr_right_camera - ) - - if not success: - print("❌ VR Pipeline创建失败") - return False - - # 获取GBuffer的内部buffer(用于DisplayRegion) - self.vr_left_eye_buffer = self.vr_pipeline_controller.get_left_buffer() - self.vr_right_eye_buffer = self.vr_pipeline_controller.get_right_buffer() - - # 🔧 关键修复:验证buffer有效性 - if not self.vr_left_eye_buffer or not self.vr_right_eye_buffer: - print("❌ VR Pipeline buffer创建失败") - print(f" 左眼buffer: {self.vr_left_eye_buffer}") - print(f" 右眼buffer: {self.vr_right_eye_buffer}") - return False - - # 获取最终输出纹理(用于提交到OpenVR) - left_textures = self.vr_pipeline_controller.get_left_textures() - right_textures = self.vr_pipeline_controller.get_right_textures() - - if left_textures and right_textures: - self.vr_left_texture = left_textures["final"] - self.vr_right_texture = right_textures["final"] - else: - print("❌ 无法获取VR Pipeline输出纹理") - return False - - # 设置缓冲区排序和清除颜色 - if self.vr_left_eye_buffer: - self.vr_left_eye_buffer.setSort(-100) - self.vr_left_eye_buffer.setClearColorActive(True) - self.vr_left_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) - - if self.vr_right_eye_buffer: - self.vr_right_eye_buffer.setSort(-99) - self.vr_right_eye_buffer.setClearColorActive(True) - self.vr_right_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) - - # 🔧 关键修复:为RenderPipeline的GBuffer DisplayRegion设置DrawCallback - # 这确保纹理在渲染完成后被提交到OpenVR - if self.vr_left_eye_buffer and self.vr_right_eye_buffer: - print(" 设置RenderPipeline DisplayRegion回调...") - try: - # 🔧 验证DisplayRegion数量 - left_dr_count = self.vr_left_eye_buffer.getNumDisplayRegions() - right_dr_count = self.vr_right_eye_buffer.getNumDisplayRegions() - - print(f" 左眼buffer DisplayRegion数量: {left_dr_count}") - print(f" 右眼buffer DisplayRegion数量: {right_dr_count}") - - if left_dr_count == 0 or right_dr_count == 0: - print(" ❌ DisplayRegion未创建,无法设置回调") - return False - - # 获取RenderTarget创建的DisplayRegion - left_dr = self.vr_left_eye_buffer.get_display_region(0) - right_dr = self.vr_right_eye_buffer.get_display_region(0) - - # 验证DisplayRegion有效性 - if not left_dr or not right_dr: - print(" ❌ DisplayRegion无效") - return False - - # 设置渲染回调(与普通模式一致) - left_dr.setDrawCallback(PythonCallbackObject(self.simple_left_cb)) - right_dr.setDrawCallback(PythonCallbackObject(self.simple_right_cb)) - - # 确保DisplayRegion处于活动状态 - left_dr.setActive(True) - right_dr.setActive(True) - - print(" ✅ RenderPipeline DisplayRegion回调已设置") - except Exception as e: - print(f" ⚠️ 设置DisplayRegion回调失败: {e}") - import traceback - traceback.print_exc() - return False - - # 🔧 关键修复:强制同步GraphicsEngine,确保所有buffer完全初始化 - print(" 同步GraphicsEngine...") - self.world.graphicsEngine.renderFrame() - - # 准备纹理并缓存OpenGL ID - if not self._prepare_and_cache_textures(): - print("❌ RenderPipeline纹理准备失败") - return False - - print("✅ VR Pipeline立体渲染管线已准备完成") - else: - # 普通模式:创建新的DisplayRegion - print(" 使用普通渲染区域...") - left_dr = self.vr_left_eye_buffer.makeDisplayRegion() - left_dr.setCamera(self.vr_left_camera) - left_dr.setActive(True) - left_dr.setDrawCallback(PythonCallbackObject(self.simple_left_cb)) - - right_dr = self.vr_right_eye_buffer.makeDisplayRegion() - right_dr.setCamera(self.vr_right_camera) - right_dr.setActive(True) - right_dr.setDrawCallback(PythonCallbackObject(self.simple_right_cb)) - - print("✓ VR相机锚点层级系统设置完成") - return True - - except Exception as e: - print(f"❌ 设置VR相机失败: {e}") - import traceback - traceback.print_exc() - return False - - def _get_eye_offset(self, eye): - """获取眼睛相对于头显的偏移""" - try: - if not self.vr_system: - # 使用标准IPD(瞳距)估算值 - ipd = 0.064 # 64mm,平均IPD - if eye == openvr.Eye_Left: - return Vec3(-ipd/2, 0, 0) - else: - return Vec3(ipd/2, 0, 0) - - # 从OpenVR获取眼睛到头显的变换矩阵 - eye_transform = self.vr_system.getEyeToHeadTransform(eye) - - # 提取位移信息 - x = eye_transform[0][3] - y = eye_transform[1][3] - z = eye_transform[2][3] - - return Vec3(x, y, z) - - except Exception as e: - print(f"❌ 获取眼睛偏移失败: {e}") - # 返回默认值 - ipd = 0.064 - if eye == openvr.Eye_Left: - return Vec3(-ipd/2, 0, 0) - else: - return Vec3(ipd/2, 0, 0) - - def _optimize_vr_rendering(self): - """优化VR模式下的渲染管线 - 减少不必要的后处理开销""" - try: - print("🔧 正在优化VR渲染管线...") - - # 检查是否有RenderPipeline - if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline: - print(" 检测到RenderPipeline,正在优化...") - - # 对VR缓冲区应用轻量级渲染设置 - if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: - self._apply_lightweight_rendering(self.vr_left_eye_buffer, "左眼") - - if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: - self._apply_lightweight_rendering(self.vr_right_eye_buffer, "右眼") - - print("✅ VR渲染管线优化完成") - else: - print(" 未检测到RenderPipeline,使用默认Panda3D渲染") - - # 禁用VR缓冲区的不必要功能 - self._disable_vr_buffer_extras() - - except Exception as e: - print(f"⚠️ VR渲染优化失败: {e}") - import traceback - traceback.print_exc() - - def _apply_lightweight_rendering(self, buffer, eye_name): - """为VR缓冲区应用轻量级渲染设置""" - try: - # 禁用多重采样抗锯齿(MSAA)以提升性能 - if hasattr(buffer, 'setMultisample'): - buffer.setMultisample(0) - print(f" {eye_name}: 禁用MSAA") - - # 设置更简单的清除颜色操作 - buffer.setClearColorActive(True) - buffer.setClearColor((0.1, 0.2, 0.4, 1.0)) # 深蓝色,便于调试 - - # 禁用深度写入到某些不需要的缓冲区 - # (保留主要深度测试,但减少不必要的写入) - - print(f" {eye_name}: 应用轻量级渲染设置") - - except Exception as e: - print(f"⚠️ {eye_name}缓冲区优化失败: {e}") - - def _disable_vr_buffer_extras(self): - """禁用VR缓冲区的额外功能以提升性能""" - try: - # 禁用VR缓冲区的统计收集 - if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: - if hasattr(self.vr_left_eye_buffer, 'setOneShot'): - self.vr_left_eye_buffer.setOneShot(False) - - if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: - if hasattr(self.vr_right_eye_buffer, 'setOneShot'): - self.vr_right_eye_buffer.setOneShot(False) - - print(" VR缓冲区额外功能已优化") - - except Exception as e: - print(f"⚠️ VR缓冲区额外功能优化失败: {e}") - - def _start_vr_task(self): - """启动VR更新任务""" - if self.vr_task: - self.world.taskMgr.remove(self.vr_task) - - # 设置高优先级(sort=-1000),确保在渲染之前执行,参考 panda3d-openvr - self.vr_task = self.world.taskMgr.add(self._update_vr, "update_vr", sort=-1000) - print("✓ VR更新任务已启动(高优先级,sort=-1000)") - - def _update_vr(self, task): - """VR更新任务 - 每帧调用(参考 panda3d-openvr 架构)""" - if not self.vr_enabled or not self.vr_system: - return task.cont - - try: - # 📌 第一件事:waitGetPoses 阻塞等待 VSync(参考 panda3d-openvr) - # 这会阻塞整个任务,确保每个 VSync 周期只执行一次 - should_call_waitgetposes = ( - not self.vr_test_mode or # 普通VR模式总是需要 - self.test_mode_wait_poses # 测试模式:仅当启用姿态等待时 - ) - - if should_call_waitgetposes: - self._wait_get_poses_immediate() - # 立即更新手柄,使用最新姿态(避免过时数据) - self.update_tracked_devices() - - # 性能监控 - self.frame_count += 1 - - # 🚀 自动启用性能模式 - 减少计时对象创建 - if not self.performance_mode_enabled and self.frame_count >= self.performance_mode_trigger_frame: - self.performance_mode_enabled = True - if self.frame_count <= self.performance_mode_trigger_frame + 5: # 只输出一次 - print(f"🎯 性能模式已启用 (帧#{self.frame_count}) - 禁用详细监控以提升性能") - - # 记录帧时间 - self._track_frame_time() - - # 计算VR FPS - import time - current_time = time.time() - if self.last_fps_time == 0: - self.last_fps_time = current_time - elif current_time - self.last_fps_time >= 1.0: # 每秒更新一次FPS - self.vr_fps = (self.frame_count - self.last_fps_check) / (current_time - self.last_fps_time) - self.last_fps_check = self.frame_count - self.last_fps_time = current_time - - # 📌 使用刚获取的姿态更新相机(而不是旧姿态) - self._update_camera_poses() - - # 输出策略信息(仅第一次) - if not hasattr(self, '_new_architecture_logged'): - print("✅ 新架构已启用 - waitGetPoses 在任务开始阻塞,参考 panda3d-openvr") - print(" 这确保每个 VSync 周期只渲染一次,解决 AlreadySubmitted 错误") - self._new_architecture_logged = True - - # 更新VR动作状态 - 仅在启用时更新 - if not self.disable_action_system and self.action_manager: - self.action_manager.update_actions() - - # 更新VR交互系统 - if self.interaction_manager: - self.interaction_manager.update() - - # 更新VR摇杆系统 - if self.joystick_manager: - # 计算帧间隔时间 - import time - if not hasattr(self, '_last_frame_time'): - self._last_frame_time = time.time() - dt = 0.016 # 默认60fps - else: - current_time = time.time() - dt = current_time - self._last_frame_time - self._last_frame_time = current_time - # 限制dt范围,避免异常情况 - dt = max(0.001, min(0.1, dt)) - - self.joystick_manager.update(dt) - - # 更新系统性能指标(减少频率) - if self.frame_count % 30 == 1: # 每30帧更新一次,减少开销 - self._update_performance_metrics() - - # 更新GPU渲染时间统计(减少频率) - if self.enable_gpu_timing and self.frame_count % 60 == 1: - self._get_gpu_frame_timing() - - # 🚀 手动垃圾回收控制 - 避免16-19帧周期性峰值 - self._manual_gc_control() - - # 定期输出性能报告 - 默认10秒间隔 - report_interval = getattr(self, 'performance_report_interval', 600) - if self.frame_count % report_interval == 1: - self._print_performance_report() - - except Exception as e: - print(f"VR更新错误: {e}") - import traceback - traceback.print_exc() - - return task.cont - - def _sync_gpu_if_needed(self): - """可选的GPU同步 - 仅在需要时使用""" - try: - # 同步等待GPU完成(如果需要) - gsg = self.world.win.getGsg() - if gsg: - gsg.getEngine().syncFrame() - except Exception as e: - print(f"❌ GPU同步异常: {e}") - - def simple_left_cb(self, cbdata): - """简化的左眼渲染回调 - 精确控制渲染和提交""" - try: - # 🔍 精确测量渲染时间 - import time - render_start = time.perf_counter() - - # 触发实际渲染 - cbdata.upcall() - - # 计算真实渲染时间 - self.left_render_time = (time.perf_counter() - render_start) * 1000 # 转换为毫秒 - - # 记录渲染次数 - self.left_render_count += 1 - - # 📌 OpenVR 帧边界检查:防止同一 OpenVR 帧内重复渲染 - if self.left_eye_last_render_frame == self.openvr_frame_id: - return # 已在当前 OpenVR 帧渲染过,跳过 - - # 🔧 OpenVR最佳实践:左眼只渲染,不立即提交 - # 基于官方hellovr示例:两眼都渲染完后再批量提交 - self.left_eye_last_render_frame = self.openvr_frame_id - - # 渐进式VR功能测试:单独启用纹理提交时保留原逻辑 - should_submit = self.vr_test_mode and self.test_mode_submit_texture and not self.test_mode_wait_poses - - if should_submit: - # 测试模式:单独启用纹理提交时的兼容模式 - if self.vr_compositor and self.vr_left_texture: - self.submit_texture(openvr.Eye_Left, self.vr_left_texture) - - if self.vr_test_mode: - # 测试模式:始终触发屏幕显示更新 - self._update_test_display() - - except Exception as e: - print(f"左眼渲染回调错误: {e}") - - def simple_right_cb(self, cbdata): - """简化的右眼渲染回调 - 精确控制渲染和提交""" - try: - # 🔍 精确测量渲染时间 - import time - render_start = time.perf_counter() - - # 触发实际渲染 - cbdata.upcall() - - # 计算真实渲染时间 - self.right_render_time = (time.perf_counter() - render_start) * 1000 # 转换为毫秒 - - # 记录渲染次数 - self.right_render_count += 1 - - # 📌 OpenVR 帧边界检查:防止同一 OpenVR 帧内重复渲染 - if self.right_eye_last_render_frame == self.openvr_frame_id: - return # 已在当前 OpenVR 帧渲染过,跳过 - - # 🔧 OpenVR最佳实践:右眼渲染完成后批量提交两眼纹理 - # 基于官方hellovr示例:避免分散提交导致的VSync阻塞 - self.right_eye_last_render_frame = self.openvr_frame_id - - # 🔧 渐进式VR功能测试:根据调试标志决定启用哪些功能 - should_batch_submit = not self.vr_test_mode or self.test_mode_submit_texture - should_wait_poses = not self.vr_test_mode or self.test_mode_wait_poses - - # 特殊处理:测试模式单独启用功能时的兼容模式 - if self.vr_test_mode: - if self.test_mode_submit_texture and not self.test_mode_wait_poses: - # 单独测试纹理提交:使用原来的分散提交方式 - if self.vr_compositor and self.vr_right_texture: - self.submit_texture(openvr.Eye_Right, self.vr_right_texture) - - # 🚀 测试模式也需要PostPresentHandoff避免36FPS - try: - if hasattr(self.vr_compositor, 'postPresentHandoff'): - self.vr_compositor.postPresentHandoff() - elif hasattr(self.vr_compositor, 'PostPresentHandoff'): - self.vr_compositor.PostPresentHandoff() - except Exception as e: - pass # 测试模式静默忽略错误 - should_batch_submit = False - elif self.test_mode_wait_poses and not self.test_mode_submit_texture: - # 单独测试姿态等待:不提交纹理 - should_batch_submit = False - - if should_batch_submit: - # 🚀 OpenVR最佳实践:批量提交两眼纹理 - # 这是解决36FPS问题的关键修复 - self._batch_submit_textures() - - # 🔧 关键修复:移除Submit后立即WaitGetPoses的错误实现 - # 根据OpenVR官方文档:"Calling WaitGetPoses immediately after Submit is conspicuously wrong" - # WaitGetPoses应该在下一帧开始时通过update_vr_task调用,不是Submit后立即调用 - # - # if should_wait_poses: - # # 错误的实现:Submit后立即获取姿态导致36FPS - # self._wait_get_poses_immediate() # ← 这是36FPS的根本原因! - # - # 正确的实现:让update_vr_task在下一帧开始时调用WaitGetPoses - - if self.vr_test_mode: - # 测试模式:始终更新性能HUD - self._update_test_performance_hud() - - except Exception as e: - print(f"右眼渲染回调错误: {e}") - - def _wait_get_poses(self): - """调用VRCompositor的waitGetPoses来获取焦点和姿态数据""" - try: - if not self.vr_compositor or not self.poses: - return - - # 调用waitGetPoses获取焦点和姿态数据 - # 这个调用可能会阻塞直到下一个VR同步点 - result = self.vr_compositor.waitGetPoses(self.poses, None) - - # 检查姿态数据的有效性 - valid_poses = 0 - - # 更新HMD姿态(设备0通常是头显) - if len(self.poses) > 0 and self.poses[0].bPoseIsValid: - valid_poses += 1 - else: - # 如果HMD姿态无效,不要频繁输出错误信息 - if not hasattr(self, '_hmd_invalid_warning_shown'): - print("⚠️ HMD姿态数据无效") - self._hmd_invalid_warning_shown = True - - # 更新控制器姿态 - self.controller_poses.clear() - for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): - if self.poses[device_id].bPoseIsValid: - device_class = self.vr_system.getTrackedDeviceClass(device_id) - if device_class == openvr.TrackedDeviceClass_Controller: - controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking - self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) - valid_poses += 1 - - # 性能监控 - 偶尔输出姿态状态 - if self.frame_count % 600 == 1: # 每10秒输出一次@60fps - print(f"📊 VR姿态状态 - 有效姿态数: {valid_poses}, 总帧数: {self.frame_count}") - - except Exception as e: - # 限制错误输出频率 - if not hasattr(self, '_last_error_frame'): - self._last_error_frame = 0 - - if self.frame_count - self._last_error_frame > 300: # 每5秒最多输出一次错误 - print(f"waitGetPoses失败: {e}") - self._last_error_frame = self.frame_count - - # 记录姿态失败次数 - self.pose_failures += 1 - - def _wait_get_poses_immediate(self): - """立即获取VR姿态 - 修复ATW闪烁的关键方法(双姿态版本)""" - # 开始计时waitGetPoses操作 - timing = self._start_timing('wait_poses') - - try: - if not self.vr_compositor or not self.poses or not self.game_poses: - self._end_timing(timing) - return - - # 关键修复:传递渲染姿态和游戏姿态数组 - # 渲染姿态用于绘制,游戏姿态用于逻辑,避免ATW过度补偿 - result = self.vr_compositor.waitGetPoses(self.poses, self.game_poses) - - # 📌 waitGetPoses 成功后立即递增 OpenVR 帧ID(新帧开始) - self.openvr_frame_id += 1 - - # 结束计时 - wait_time = self._end_timing(timing) - - # 检查姿态数据的有效性 - valid_poses = 0 - - # 更新HMD姿态(设备0通常是头显) - if len(self.poses) > 0 and self.poses[0].bPoseIsValid: - valid_poses += 1 - else: - # 如果HMD姿态无效,不要频繁输出错误信息 - if not hasattr(self, '_hmd_invalid_warning_shown'): - print("⚠️ HMD姿态数据无效(立即模式)") - self._hmd_invalid_warning_shown = True - - # 🚀 优化控制器姿态更新:使用缓存,避免每帧clear()和重新创建对象 - for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): - if self.poses[device_id].bPoseIsValid: - device_class = self.vr_system.getTrackedDeviceClass(device_id) - if device_class == openvr.TrackedDeviceClass_Controller: - controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking - - # 检查是否已有此设备的缓存矩阵 - if device_id not in self.controller_poses: - # 第一次检测到该控制器,创建新的缓存矩阵 - self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) - else: - # 复用现有矩阵,只更新数值,避免创建新对象 - cached_mat = self.controller_poses[device_id] - self._update_matrix_from_openvr(cached_mat, controller_matrix) - - valid_poses += 1 - else: - # 设备姿态无效,从字典中移除(如果存在) - if device_id in self.controller_poses: - # 将矩阵返回对象池 - self._return_pooled_matrix(self.controller_poses[device_id]) - del self.controller_poses[device_id] - - # 🔧 关键修复:立即更新手柄和跟踪设备 - # Running Start模式下必须在WaitGetPoses后立即更新,避免手柄消失 - self.update_tracked_devices() - - # 调试信息 - 仅在第一次成功时输出 - if not hasattr(self, '_dual_pose_mode_logged') and valid_poses > 0: - print(f"✅ 双姿态立即获取模式启用 - 有效姿态数: {valid_poses}") - print(" 渲染姿态用于绘制,游戏姿态用于逻辑,防止ATW过度补偿") - print(" 手柄和跟踪设备在WaitGetPoses后立即更新") - self._dual_pose_mode_logged = True - - except Exception as e: - # 限制错误输出频率 - if not hasattr(self, '_last_immediate_error_frame'): - self._last_immediate_error_frame = 0 - - if self.frame_count - self._last_immediate_error_frame > 300: # 每5秒最多输出一次错误 - print(f"立即姿态获取失败: {e}") - self._last_immediate_error_frame = self.frame_count - - self.pose_failures += 1 - - def _wait_get_poses_with_prediction(self): - """使用预测时间获取VR姿态 - 优化性能的新策略""" - try: - if not self.vr_compositor or not self.poses or not self.game_poses: - return - - # 使用预测时间获取姿态 - # 预测时间通常为11-16ms,对应下一个VR帧的时间 - result = self.vr_compositor.waitGetPoses(self.poses, self.game_poses) - - # 检查姿态数据的有效性 - valid_poses = 0 - - # 更新HMD姿态(设备0通常是头显) - if len(self.poses) > 0 and self.poses[0].bPoseIsValid: - valid_poses += 1 - else: - # 如果HMD姿态无效,不要频繁输出错误信息 - if not hasattr(self, '_hmd_invalid_warning_task'): - print("⚠️ HMD姿态数据无效(更新任务模式)") - self._hmd_invalid_warning_task = True - - # 更新控制器姿态 - self.controller_poses.clear() - for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): - if self.poses[device_id].bPoseIsValid: - device_class = self.vr_system.getTrackedDeviceClass(device_id) - if device_class == openvr.TrackedDeviceClass_Controller: - controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking - self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) - valid_poses += 1 - - # 调试信息 - 仅在第一次成功时输出 - if not hasattr(self, '_update_task_mode_logged') and valid_poses > 0: - print(f"✅ 更新任务姿态获取模式启用 - 有效姿态数: {valid_poses}") - print(f" 预测时间: {self.use_prediction_time*1000:.1f}ms") - self._update_task_mode_logged = True - - except Exception as e: - # 限制错误输出频率 - if not hasattr(self, '_last_task_error_frame'): - self._last_task_error_frame = 0 - - if self.frame_count - self._last_task_error_frame > 300: # 每5秒最多输出一次错误 - print(f"更新任务姿态获取失败: {e}") - self._last_task_error_frame = self.frame_count - - # 记录姿态失败次数 - self.pose_failures += 1 - - def _cache_poses_for_next_frame(self): - """缓存当前姿态供下一帧渲染使用 - 修复时序不匹配""" - try: - if not self.poses or len(self.poses) == 0: - return - - # 如果是第一帧,直接使用当前姿态 - if self._first_frame: - self._cached_render_poses = self.poses - self._first_frame = False - print("✓ 首帧姿态缓存已设置") - return - - # 复制当前渲染姿态到缓存 - # 下一帧将使用这些姿态进行渲染 - poses_t = openvr.TrackedDevicePose_t * openvr.k_unMaxTrackedDeviceCount - cached_poses = poses_t() - - # 复制姿态数据 - for i in range(len(self.poses)): - cached_poses[i] = self.poses[i] - - self._cached_render_poses = cached_poses - - except Exception as e: - print(f"⚠️ 姿态缓存失败: {e}") - - - def _reset_waitgetposes_flag(self, task): - """重置WaitGetPoses标记 - 确保下一帧可以调用WaitGetPoses""" - self._waitgetposes_called_this_frame = False - return task.done - - def _update_tracking_data(self): - """更新VR追踪数据""" - try: - # 获取设备姿态 - poses = self.vr_system.getDeviceToAbsoluteTrackingPose( - openvr.TrackingUniverseStanding, 0.0, openvr.k_unMaxTrackedDeviceCount - ) - - # 更新HMD姿态(设备0通常是头显) - if poses[0].bPoseIsValid: - hmd_matrix = poses[0].mDeviceToAbsoluteTracking - self.hmd_pose = self._convert_openvr_matrix_to_panda(hmd_matrix) - - # 更新控制器姿态 - for device_id in range(1, openvr.k_unMaxTrackedDeviceCount): - if poses[device_id].bPoseIsValid: - device_class = self.vr_system.getTrackedDeviceClass(device_id) - if device_class == openvr.TrackedDeviceClass_Controller: - controller_matrix = poses[device_id].mDeviceToAbsoluteTracking - self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) - - except Exception as e: - print(f"更新追踪数据失败: {e}") - - def _convert_openvr_matrix_to_panda(self, ovr_matrix): - """将OpenVR矩阵转换为Panda3D矩阵 - 使用对象池优化 - - 坐标系转换: - OpenVR: X右, Y上, -Z前(右手坐标系) - Panda3D: X右, Y前, Z上(右手坐标系) - - 转换规则: - OpenVR X → Panda3D X - OpenVR Y → Panda3D Z - OpenVR -Z → Panda3D Y - """ - # 🚀 使用对象池获取预分配的Mat4对象,避免每帧创建新对象 - mat = self._get_pooled_matrix() - - # 修正的坐标转换矩阵 - # OpenVR: X右, Y上, -Z前 → Panda3D: X右, Y前, Z上 - # 转换规则: (ovr_x, ovr_y, ovr_z) → (panda_x, panda_y, panda_z) - # (ovr_x, ovr_y, ovr_z) → (ovr_x, -ovr_z, ovr_y) - - # X轴行:Panda3D的X轴对应OpenVR的X轴 - mat.setCell(0, 0, ovr_matrix[0][0]) # X_x → X_x - mat.setCell(0, 1, ovr_matrix[0][1]) # X_y → X_y - mat.setCell(0, 2, ovr_matrix[0][2]) # X_z → X_z - mat.setCell(0, 3, ovr_matrix[0][3]) # 位移X分量 - - # Y轴行:Panda3D的Y轴对应OpenVR的-Z轴 - mat.setCell(1, 0, -ovr_matrix[2][0]) # -Z_x → Y_x - mat.setCell(1, 1, -ovr_matrix[2][1]) # -Z_y → Y_y - mat.setCell(1, 2, -ovr_matrix[2][2]) # -Z_z → Y_z - mat.setCell(1, 3, -ovr_matrix[2][3]) # 位移Y分量(-Z位移) - - # Z轴行:Panda3D的Z轴对应OpenVR的Y轴 - mat.setCell(2, 0, ovr_matrix[1][0]) # Y_x → Z_x - mat.setCell(2, 1, ovr_matrix[1][1]) # Y_y → Z_y - mat.setCell(2, 2, ovr_matrix[1][2]) # Y_z → Z_z - mat.setCell(2, 3, ovr_matrix[1][3]) # 位移Z分量(Y位移) - - # 齐次坐标 - mat.setCell(3, 0, 0) - mat.setCell(3, 1, 0) - mat.setCell(3, 2, 0) - mat.setCell(3, 3, 1) - - # 🚀 优化调试信息 - 避免创建Vec3对象,减少GC压力 - if not hasattr(self, '_coord_debug_counter'): - self._coord_debug_counter = 0 - self._coord_debug_counter += 1 - - if self._coord_debug_counter % 600 == 1: # 每10秒输出一次@60fps - print(f"🔄 坐标系转换调试 (第{self._coord_debug_counter}帧)") - - # 直接输出数值,避免创建Vec3对象 - ovr_x, ovr_y, ovr_z = ovr_matrix[0][3], ovr_matrix[1][3], ovr_matrix[2][3] - print(f" OpenVR原始位置: ({ovr_x:.3f}, {ovr_y:.3f}, {ovr_z:.3f})") - - # 直接从矩阵读取数值,避免创建Vec3对象 - panda_x = mat.getCell(0, 3) - panda_y = mat.getCell(1, 3) - panda_z = mat.getCell(2, 3) - print(f" Panda3D转换位置: ({panda_x:.3f}, {panda_y:.3f}, {panda_z:.3f})") - - # 手动验证转换:OpenVR (x,y,z) → Panda3D (x,-z,y) - expected_x = ovr_x - expected_y = -ovr_z - expected_z = ovr_y - print(f" 预期转换结果: ({expected_x:.3f}, {expected_y:.3f}, {expected_z:.3f})") - - # 计算误差,避免创建Vec3对象 - diff_x = panda_x - expected_x - diff_y = panda_y - expected_y - diff_z = panda_z - expected_z - diff_magnitude = (diff_x*diff_x + diff_y*diff_y + diff_z*diff_z)**0.5 - - if diff_magnitude < 0.001: - print(f" ✅ 坐标转换正确 (误差: {diff_magnitude:.6f})") - else: - print(f" ⚠️ 坐标转换可能有误 (误差: {diff_magnitude:.6f})") - print(f" 差异: ({diff_x:.6f}, {diff_y:.6f}, {diff_z:.6f})") - - return mat - - def update_hmd(self, pose): - """ - 更新HMD锚点 - 基于参考实现 - """ - try: - # 将OpenVR姿态转换为Panda3D矩阵 - modelview = self.convert_mat(pose.mDeviceToAbsoluteTracking) - - # 应用坐标系转换并设置HMD锚点 - self.hmd_anchor.setMat(self.coord_mat_inv * modelview * self.coord_mat) - - # 获取眼睛到头部的变换 - view_left = self.convert_mat(self.vr_system.getEyeToHeadTransform(openvr.Eye_Left)) - view_right = self.convert_mat(self.vr_system.getEyeToHeadTransform(openvr.Eye_Right)) - - # 设置眼睛锚点 - self.left_eye_anchor.setMat(self.coord_mat_inv * view_left * self.coord_mat) - self.right_eye_anchor.setMat(self.coord_mat_inv * view_right * self.coord_mat) - - except Exception as e: - print(f"更新HMD姿态失败: {e}") - - def _update_camera_poses(self): - """更新相机姿态 - 使用锚点系统简化处理""" - try: - # 使用锚点系统后,相机位置自动跟随锚点 - # 只需要获取HMD姿态并更新锚点即可 - - # 从poses数组中获取HMD姿态 - if hasattr(self, 'poses') and len(self.poses) > 0: - hmd_pose = self.poses[openvr.k_unTrackedDeviceIndex_Hmd] - if hmd_pose.bPoseIsValid: - self.update_hmd(hmd_pose) - else: - print("⚠️ HMD姿态数据无效") - - except Exception as e: - print(f"更新相机姿态失败: {e}") - import traceback - traceback.print_exc() - - def _update_camera_poses_with_cache(self): - """使用缓存姿态更新相机 - 符合OpenVR时序假设""" - try: - # 使用缓存的姿态,符合OpenVR的时序假设 - # OpenVR假设你用上一次WaitGetPoses的姿态渲染当前提交的帧 - - if not self._cached_render_poses: - # 如果没有缓存姿态,回退到当前姿态(首帧情况) - print("⚠️ 没有缓存姿态,使用当前姿态") - return self._update_camera_poses() - - # 从缓存姿态数组中获取HMD姿态 - if len(self._cached_render_poses) > 0: - hmd_pose = self._cached_render_poses[openvr.k_unTrackedDeviceIndex_Hmd] - if hmd_pose.bPoseIsValid: - self.update_hmd(hmd_pose) - - # 调试信息 - 验证缓存姿态使用 - if not hasattr(self, '_cached_pose_logged'): - print("✅ 使用缓存姿态更新相机 - 符合OpenVR时序假设") - self._cached_pose_logged = True - else: - print("⚠️ 缓存的HMD姿态数据无效") - - except Exception as e: - print(f"使用缓存姿态更新相机失败: {e}") - # 回退到正常更新方式 - self._update_camera_poses() - import traceback - traceback.print_exc() - - - def enable_vr(self): - """启用VR模式""" - if not self.is_vr_available(): - print("❌ VR系统不可用") - return False - - if not self.vr_initialized: - if not self.initialize_vr(): - return False - - self.vr_enabled = True - - # 禁用主相机避免干扰VR渲染 - self._disable_main_cam() - - # VR性能优化:使用Running Start模式(Valve最佳实践) - print("🚀 VR性能优化:Running Start模式已启用") - print(" 优势:在帧开始时获取姿态,提供VSync前3ms的渲染时间") - print(" 注意:Submit后立即调用WaitGetPoses是错误实现") - self.set_prediction_time(11.0) # 11ms预测时间 - OpenVR标准值,平衡准确性和延迟 - - # 🚀 动态调整Qt Timer频率以支持VR - if hasattr(self.world, 'qtWidget') and self.world.qtWidget: - if hasattr(self.world.qtWidget, 'synchronizer'): - # 设置为144Hz,让OpenVR控制实际渲染节奏 - self.world.qtWidget.synchronizer.setInterval(int(1000/144)) - print("✓ Qt Timer调整为144Hz,让OpenVR控制VR渲染节奏") - - # 🔧 关键修复:检测并重建缺失的手柄visualizer - # 当渲染模式切换时,visualizer可能被清理但控制器对象仍存在 - if hasattr(self, 'left_controller') and self.left_controller: - if not self.left_controller.visualizer and self.left_controller.anchor_node: - print("🔧 检测到左手柄visualizer缺失,正在重建...") - self.left_controller._create_visualizer() - if self.left_controller.visualizer: - print("✅ 左手柄visualizer已重建") - - if hasattr(self, 'right_controller') and self.right_controller: - if not self.right_controller.visualizer and self.right_controller.anchor_node: - print("🔧 检测到右手柄visualizer缺失,正在重建...") - self.right_controller._create_visualizer() - if self.right_controller.visualizer: - print("✅ 右手柄visualizer已重建") - - print("✅ VR模式已启用") - return True - - def disable_vr(self): - """禁用VR模式""" - self.vr_enabled = False - - # 清理手柄可视化(但保留控制器对象以便重新启用) - if hasattr(self, 'left_controller') and self.left_controller: - if hasattr(self.left_controller, 'visualizer') and self.left_controller.visualizer: - try: - self.left_controller.visualizer.cleanup() - self.left_controller.visualizer = None - print("✓ 左手控制器可视化已清理") - except Exception as e: - print(f"⚠️ 清理左手控制器可视化失败: {e}") - - if hasattr(self, 'right_controller') and self.right_controller: - if hasattr(self.right_controller, 'visualizer') and self.right_controller.visualizer: - try: - self.right_controller.visualizer.cleanup() - self.right_controller.visualizer = None - print("✓ 右手控制器可视化已清理") - except Exception as e: - print(f"⚠️ 清理右手控制器可视化失败: {e}") - - # 隐藏手柄锚点节点 - if hasattr(self, 'left_controller') and self.left_controller and self.left_controller.anchor_node: - self.left_controller.anchor_node.hide() - - if hasattr(self, 'right_controller') and self.right_controller and self.right_controller.anchor_node: - self.right_controller.anchor_node.hide() - - # 恢复主相机 - self._enable_main_cam() - - # 恢复Qt Timer到60FPS - if hasattr(self.world, 'qtWidget') and self.world.qtWidget: - if hasattr(self.world.qtWidget, 'synchronizer'): - self.world.qtWidget.synchronizer.setInterval(int(1000/60)) - print("✓ Qt Timer恢复为60Hz") - - print("✅ VR模式已禁用,手柄模型已隐藏") - - def set_vr_render_mode(self, mode): - """切换VR渲染模式 - - Args: - mode: VRRenderMode枚举值或字符串 ('normal' 或 'render_pipeline') - - Returns: - bool: 切换是否成功 - """ - try: - # 转换输入为枚举类型 - if isinstance(mode, str): - mode_str = mode.lower() - if mode_str == "normal": - new_mode = VRRenderMode.NORMAL - elif mode_str in ["render_pipeline", "renderpipeline", "pipeline"]: - new_mode = VRRenderMode.RENDER_PIPELINE - else: - print(f"❌ 无效的渲染模式: {mode}") - print(" 支持的模式: 'normal' 或 'render_pipeline'") - return False - elif isinstance(mode, VRRenderMode): - new_mode = mode - else: - print(f"❌ 无效的模式类型: {type(mode)}") - return False - - # 检查是否与当前模式相同 - if new_mode == self.vr_render_mode: - print(f"✓ VR渲染模式已经是 {new_mode.value},无需切换") - return True - - print(f"🔄 正在切换VR渲染模式: {self.vr_render_mode.value} → {new_mode.value}") - - # 检查VR是否已初始化 - if not self.vr_initialized: - print("⚠️ VR未初始化,仅更新渲染模式配置") - self.vr_render_mode = new_mode - print(f"✓ VR渲染模式已更新为 {new_mode.value}(下次启动VR时生效)") - return True - - # 保存当前VR启用状态 - was_enabled = self.vr_enabled - - # 如果VR已启用,先禁用 - if was_enabled: - print(" 暂时禁用VR...") - self.disable_vr() - - # 更新渲染模式 - old_mode = self.vr_render_mode - self.vr_render_mode = new_mode - - # 清理现有缓冲区 - print(" 清理现有渲染缓冲区...") - self._cleanup_vr_buffers() - - # 根据新模式重建缓冲区 - print(f" 创建新的渲染缓冲区({new_mode.value})...") - success = False - - if new_mode == VRRenderMode.RENDER_PIPELINE: - success = self._create_vr_buffers_with_pipeline() - if not success: - print("⚠️ RenderPipeline模式创建失败,回退到普通渲染模式") - self.vr_render_mode = VRRenderMode.NORMAL - success = self._create_vr_buffers() - else: - success = self._create_vr_buffers() - - if not success: - print("❌ 缓冲区创建失败,尝试恢复原模式") - self.vr_render_mode = old_mode - if old_mode == VRRenderMode.RENDER_PIPELINE: - self._create_vr_buffers_with_pipeline() - else: - self._create_vr_buffers() - return False - - # 重新设置相机 - print(" 重新设置VR相机...") - if not self._setup_vr_cameras(): - print("❌ 相机设置失败") - return False - - # 如果之前VR是启用的,重新启用 - if was_enabled: - print(" 重新启用VR...") - self.enable_vr() - - # 🔧 关键修复:重建所有手柄的visualizer以适配新渲染模式 - print(" 刷新手柄visualizer以适配新渲染模式...") - if hasattr(self, 'left_controller') and self.left_controller: - self.left_controller.recreate_visualizer() - - if hasattr(self, 'right_controller') and self.right_controller: - self.right_controller.recreate_visualizer() - - print(f"✅ VR渲染模式已切换为 {self.vr_render_mode.value}") - - # 保存配置 - if self.config_manager: - self.config_manager.save_from_vr_manager(self) - - return True - - except Exception as e: - print(f"❌ 切换VR渲染模式失败: {e}") - import traceback - traceback.print_exc() - return False - - def get_vr_render_mode(self): - """获取当前VR渲染模式 - - Returns: - VRRenderMode: 当前渲染模式 - """ - return self.vr_render_mode - - def _cleanup_vr_buffers(self): - """清理VR渲染缓冲区""" - try: - # 清理VR Pipeline Controller(如果使用RenderPipeline模式) - if self.vr_pipeline_controller: - print(" 清理VR Pipeline...") - self.vr_pipeline_controller.cleanup_all() - self.vr_pipeline_controller = None - - # 清理左眼缓冲区 - if self.vr_left_eye_buffer: - # 如果是RenderPipeline模式,buffer已被VRPipelineController清理 - # 如果是普通模式,需要手动清理 - if self.vr_render_mode == VRRenderMode.NORMAL: - self.world.graphicsEngine.removeWindow(self.vr_left_eye_buffer) - self.vr_left_eye_buffer = None - - # 清理右眼缓冲区 - if self.vr_right_eye_buffer: - if self.vr_render_mode == VRRenderMode.NORMAL: - self.world.graphicsEngine.removeWindow(self.vr_right_eye_buffer) - self.vr_right_eye_buffer = None - - # 清理纹理 - self.vr_left_texture = None - self.vr_right_texture = None - self.left_texture_id = None - self.right_texture_id = None - self.textures_prepared = False - - # 清理RenderPipeline渲染目标(旧版,保留兼容性) - if self.vr_pipeline_left_target: - self.vr_pipeline_left_target = None - - if self.vr_pipeline_right_target: - self.vr_pipeline_right_target = None - - self.render_pipeline_enabled = False - - print("✓ VR渲染缓冲区已清理") - - except Exception as e: - print(f"⚠️ 清理VR缓冲区时出错: {e}") - - def cleanup(self): - """清理VR资源""" - try: - print("🔄 正在清理VR资源...") - - # 停止VR任务 - if self.vr_task: - self.world.taskMgr.remove(self.vr_task) - self.vr_task = None - - # 🚀 恢复Python垃圾回收并清理对象池 - if self._gc_disabled: - # 最后一次手动垃圾回收 - collected = gc.collect() - print(f"🗑️ 最终GC清理: {collected} 个对象") - - # 恢复自动垃圾回收 - gc.enable() - self._gc_disabled = False - print("✅ Python垃圾回收已恢复为自动模式") - - # 清理对象池 - if hasattr(self, '_matrix_pool'): - pool_size = len(self._matrix_pool) - self._matrix_pool.clear() - print(f"🧹 Mat4对象池已清理: {pool_size} 个对象") - - # 清理缓存 - if hasattr(self, '_cached_matrices'): - self._cached_matrices.clear() - if hasattr(self, '_controller_poses_cache'): - self._controller_poses_cache.clear() - - # 清理OpenVR Texture对象缓存 - if hasattr(self, '_left_ovr_texture'): - self._left_ovr_texture = None - if hasattr(self, '_right_ovr_texture'): - self._right_ovr_texture = None - print("🧹 OpenVR Texture对象缓存已清理") - - # 清理渲染缓冲区 - if self.vr_left_eye_buffer: - self.vr_left_eye_buffer.removeAllDisplayRegions() - self.world.graphicsEngine.removeWindow(self.vr_left_eye_buffer) - self.vr_left_eye_buffer = None - - if self.vr_right_eye_buffer: - self.vr_right_eye_buffer.removeAllDisplayRegions() - self.world.graphicsEngine.removeWindow(self.vr_right_eye_buffer) - self.vr_right_eye_buffer = None - - # 清理相机 - if self.vr_left_camera: - self.vr_left_camera.removeNode() - self.vr_left_camera = None - - if self.vr_right_camera: - self.vr_right_camera.removeNode() - self.vr_right_camera = None - - # 清理控制器 - if hasattr(self, 'left_controller') and self.left_controller: - try: - self.left_controller.cleanup() - self.left_controller = None - print("✓ 左手控制器已清理") - except Exception as e: - print(f"⚠️ 清理左手控制器失败: {e}") - - if hasattr(self, 'right_controller') and self.right_controller: - try: - self.right_controller.cleanup() - self.right_controller = None - print("✓ 右手控制器已清理") - except Exception as e: - print(f"⚠️ 清理右手控制器失败: {e}") - - # 清理控制器字典 - if hasattr(self, 'controllers'): - self.controllers.clear() - print("✓ 控制器字典已清理") - - # 清理VR子系统 - if hasattr(self, 'joystick_manager') and self.joystick_manager: - try: - self.joystick_manager.cleanup() - self.joystick_manager = None - print("✓ VR摇杆系统已清理") - except Exception as e: - print(f"⚠️ 清理VR摇杆系统失败: {e}") - - if hasattr(self, 'teleport_system') and self.teleport_system: - try: - self.teleport_system.cleanup() - self.teleport_system = None - print("✓ VR传送系统已清理") - except Exception as e: - print(f"⚠️ 清理VR传送系统失败: {e}") - - if hasattr(self, 'interaction_manager') and self.interaction_manager: - try: - self.interaction_manager.cleanup() - self.interaction_manager = None - print("✓ VR交互系统已清理") - except Exception as e: - print(f"⚠️ 清理VR交互系统失败: {e}") - - if hasattr(self, 'performance_monitor') and self.performance_monitor: - try: - # 性能监控系统不需要特殊清理,只需要置空引用 - self.performance_monitor = None - print("✓ VR性能监控系统已清理") - except Exception as e: - print(f"⚠️ 清理VR性能监控系统失败: {e}") - - if hasattr(self, 'action_manager') and self.action_manager: - try: - self.action_manager.cleanup() - self.action_manager = None - print("✓ VR动作系统已清理") - except Exception as e: - print(f"⚠️ 清理VR动作系统失败: {e}") - - # 关闭OpenVR - if self.vr_system and OPENVR_AVAILABLE: - try: - openvr.shutdown() - except: - pass - self.vr_system = None - - self.vr_enabled = False - self.vr_initialized = False - - print("✅ VR资源清理完成") - - except Exception as e: - print(f"⚠️ VR清理过程中出错: {e}") - - def get_vr_status(self): - """获取VR状态信息""" - return { - 'available': self.is_vr_available(), - 'initialized': self.vr_initialized, - 'enabled': self.vr_enabled, - 'eye_resolution': (self.eye_width, self.eye_height), - 'device_count': len(self.controller_poses) + (1 if self.vr_enabled else 0), - 'vr_fps': self.vr_fps, - 'frame_count': self.frame_count, - 'submit_failures': self.submit_failures, - 'pose_failures': self.pose_failures - } - - def submit_texture(self, eye, texture): - """优化的VR纹理提交 - 使用缓存的纹理ID,避免重复prepareNow""" - try: - if not self.vr_compositor: - print("❌ VR compositor不可用") - self.submit_failures += 1 - return - - # 🚀 关键修复:防止同一帧重复提交 - current_frame = getattr(self, 'frame_count', 0) - if not hasattr(self, '_last_submit_frame'): - self._last_submit_frame = {} - - # 检查当前帧是否已经提交过此眼睛的纹理 - if eye in self._last_submit_frame and self._last_submit_frame[eye] == current_frame: - # 静默跳过,避免spam输出 - return - - # 记录当前帧提交 - self._last_submit_frame[eye] = current_frame - - # 🚀 关键优化:直接使用缓存的纹理ID,避免重复prepareNow - if eye == openvr.Eye_Left: - handle = self.left_texture_id - eye_name = "左眼" - elif eye == openvr.Eye_Right: - handle = self.right_texture_id - eye_name = "右眼" - else: - print(f"❌ 未知的眼睛类型: {eye}") - self.submit_failures += 1 - return - - # 检查缓存的纹理ID是否有效 - if not handle or handle <= 0: - print(f"❌ {eye_name}纹理ID缓存无效: {handle}") - print(" 这可能表示纹理准备失败,需要检查_prepare_and_cache_textures()") - self.submit_failures += 1 - return - - # ❌ 移除gsg.flush()调用 - 基于OpenVR官方实践 - # gsg.flush()等同于OpenGL的glFlush(),导致强制CPU-GPU同步 - # 每帧调用2次(左右眼)是GPU周期性峰值的主要原因 - # 参考:OpenVR社区经验表明同步调用是性能杀手 - # - # gsg = self.world.win.getGsg() - # if gsg and hasattr(gsg, 'flush'): - # try: - # gsg.flush() - # except Exception as flush_error: - # if not hasattr(self, '_gsg_flush_error_logged'): - # print(f"⚠️ GSG刷新失败: {flush_error}") - # self._gsg_flush_error_logged = True - - # 🚀 关键优化:使用缓存的OpenVR Texture对象,避免每帧创建 - if eye == openvr.Eye_Left: - ovr_texture = self._left_ovr_texture - else: - ovr_texture = self._right_ovr_texture - - # 检查缓存对象是否存在(向后兼容) - if ovr_texture is None: - # 备用方案:如果缓存对象不存在,创建新的(性能较差) - ovr_texture = openvr.Texture_t() - ovr_texture.eType = openvr.TextureType_OpenGL - ovr_texture.eColorSpace = openvr.ColorSpace_Gamma - if not hasattr(self, '_texture_fallback_warned'): - print("⚠️ 使用Texture对象备用方案(性能次优)") - self._texture_fallback_warned = True - - # 只更新handle,其他属性已预设置 - ovr_texture.handle = handle - - # 提交到VR系统 - error = self.vr_compositor.submit(eye, ovr_texture) - - # 检查错误 - if error and error != openvr.VRCompositorError_None: - print(f"⚠️ VR{eye_name}纹理提交错误: {error}") - self.submit_failures += 1 - else: - # 只在第一次成功时输出 - if not hasattr(self, '_optimized_submit_success_logged'): - print(f"✅ 优化版VR纹理提交成功! 使用缓存ID,避免重复prepareNow") - print(f" {eye_name}纹理ID: {handle}") - self._optimized_submit_success_logged = True - - # 🔧 智能GPU同步策略 - 减少不必要的flush调用 - # 只有在检测到性能问题或每N帧时才强制flush - if eye == openvr.Eye_Right: - self._smart_gpu_sync() - - except Exception as e: - print(f"❌ VR纹理提交异常: {e}") - import traceback - traceback.print_exc() - self.submit_failures += 1 - - def _smart_gpu_sync(self): - """智能GPU同步策略 - 只在必要时进行同步""" - try: - # 初始化同步控制变量 - if not hasattr(self, '_last_gpu_sync_frame'): - self._last_gpu_sync_frame = 0 - self._gpu_sync_interval = 60 # 每60帧强制同步一次(1秒@60FPS) - self._performance_based_sync = True - - current_frame = getattr(self, 'frame_count', 0) - - # 策略1: 性能自适应同步 - if self._performance_based_sync: - # 如果渲染时间过长,增加同步频率 - total_render_time = getattr(self, 'left_render_time', 0) + getattr(self, 'right_render_time', 0) - - if total_render_time > 15.0: # 如果总渲染时间超过15ms - # 高负载时更频繁同步 - sync_interval = 30 # 每30帧同步 - elif total_render_time > 10.0: - sync_interval = 45 # 中等负载 - else: - sync_interval = self._gpu_sync_interval # 正常负载 - - should_sync = (current_frame - self._last_gpu_sync_frame) >= sync_interval - else: - # 策略2: 固定间隔同步 - should_sync = (current_frame - self._last_gpu_sync_frame) >= self._gpu_sync_interval - - # 执行同步 - if should_sync: - gsg = self.world.win.getGsg() - if gsg and hasattr(gsg, 'flush'): - try: - gsg.flush() - self._last_gpu_sync_frame = current_frame - - # 只在首次或Debug时输出 - if not hasattr(self, '_smart_sync_logged') or self.debug_output_enabled: - if not hasattr(self, '_smart_sync_logged'): - print(f"🔧 智能GPU同步已启用 - 间隔:{sync_interval}帧") - self._smart_sync_logged = True - elif self.debug_output_enabled and current_frame % 600 == 1: - print(f"🔧 智能同步触发 (帧#{current_frame}, 间隔:{sync_interval})") - - except Exception as flush_error: - if not hasattr(self, '_smart_sync_error_logged'): - print(f"⚠️ 智能GPU同步失败: {flush_error}") - self._smart_sync_error_logged = True - - except Exception as e: - print(f"⚠️ 智能GPU同步策略异常: {e}") - - def _disable_main_cam(self): - """禁用主相机 - 基于参考实现""" - try: - # 保存原始相机状态 - if not hasattr(self, '_original_camera_parent'): - self._original_camera_parent = self.world.camera.getParent() - - # 创建空节点并将主相机重新附加到它 - self._empty_world = NodePath("empty_world") - self.world.camera.reparentTo(self._empty_world) - - print("✓ 主相机已禁用") - except Exception as e: - print(f"⚠️ 禁用主相机失败: {e}") - - def _enable_main_cam(self): - """恢复主相机 - 基于参考实现""" - try: - # 恢复原始相机状态 - if hasattr(self, '_original_camera_parent') and self._original_camera_parent: - self.world.camera.reparentTo(self._original_camera_parent) - else: - # 如果没有保存的父节点,重新附加到render - self.world.camera.reparentTo(self.world.render) - - # 清理空世界节点 - if hasattr(self, '_empty_world'): - self._empty_world.removeNode() - delattr(self, '_empty_world') - - print("✓ 主相机已恢复") - except Exception as e: - print(f"⚠️ 恢复主相机失败: {e}") - - def _initialize_controllers(self): - """初始化VR手柄控制器""" - try: - print("🎮 正在初始化VR手柄控制器...") - - # 创建左右手柄控制器实例 - self.left_controller = LeftController(self) - self.right_controller = RightController(self) - - # 检测现有连接的控制器 - self._detect_controllers() - - print("✓ VR手柄控制器初始化完成") - - except Exception as e: - print(f"⚠️ VR手柄初始化失败: {e}") - import traceback - traceback.print_exc() - - def _detect_controllers(self): - """检测并连接VR控制器""" - if not self.vr_system: - return - - try: - for device_index in range(openvr.k_unMaxTrackedDeviceCount): - # 检查设备是否已连接 - if not self.vr_system.isTrackedDeviceConnected(device_index): - continue - - # 获取设备类型 - device_class = self.vr_system.getTrackedDeviceClass(device_index) - - if device_class == openvr.TrackedDeviceClass_Controller: - # 获取控制器角色 - role = self.vr_system.getControllerRoleForTrackedDeviceIndex(device_index) - - if role == openvr.TrackedControllerRole_LeftHand and self.left_controller: - self.left_controller.set_device_index(device_index) - self.controllers[device_index] = self.left_controller - # 为设备创建锚点 - self._create_tracked_device_anchor(device_index, 'left_controller') - - elif role == openvr.TrackedControllerRole_RightHand and self.right_controller: - self.right_controller.set_device_index(device_index) - self.controllers[device_index] = self.right_controller - # 为设备创建锚点 - self._create_tracked_device_anchor(device_index, 'right_controller') - - print(f"🎮 检测到 {len(self.controllers)} 个控制器") - - except Exception as e: - print(f"⚠️ 控制器检测失败: {e}") - - def _create_tracked_device_anchor(self, device_index, name): - """为跟踪设备创建锚点节点""" - if not self.tracking_space: - print(f"⚠️ 无法为设备 {device_index} 创建锚点 - tracking_space未初始化") - return - - try: - # 获取设备模型名称 - if self.vr_system: - model_name = self.vr_system.getStringTrackedDeviceProperty( - device_index, - openvr.Prop_RenderModelName_String - ) - anchor_name = f"{device_index}:{model_name}:{name}" - else: - anchor_name = f"{device_index}:{name}" - - # 创建锚点节点 - device_anchor = self.tracking_space.attachNewNode(anchor_name) - self.tracked_device_anchors[device_index] = device_anchor - - print(f"✓ 为设备 {device_index} 创建锚点: {anchor_name}") - - except Exception as e: - print(f"⚠️ 创建设备锚点失败: {e}") - - def update_tracked_devices(self): - """更新所有跟踪设备的姿态 - 基于参考实现""" - if not self.poses or not self.vr_system: - return - - try: - # 更新每个已连接的控制器 - for device_index, controller in self.controllers.items(): - if device_index < len(self.poses): - pose_data = self.poses[device_index] - - # 更新控制器姿态 - controller.update_pose(pose_data) - - # 更新控制器输入状态 - controller.update_input_state(self.vr_system) - - # 更新其他跟踪设备的锚点 - for device_index in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): - if device_index in self.tracked_device_anchors: - pose_data = self.poses[device_index] - if pose_data.bPoseIsValid: - # 转换姿态矩阵 - modelview = self.convert_mat(pose_data.mDeviceToAbsoluteTracking) - final_matrix = self.coord_mat_inv * modelview * self.coord_mat - - # 更新锚点变换 - anchor = self.tracked_device_anchors[device_index] - anchor.setMat(final_matrix) - anchor.show() - else: - # 姿态无效,隐藏锚点 - self.tracked_device_anchors[device_index].hide() - - except Exception as e: - if self.frame_count % 300 == 0: # 每5秒输出一次错误 - print(f"⚠️ 更新跟踪设备失败: {e}") - - def get_controller_by_role(self, role): - """根据角色获取控制器 - - Args: - role: 'left' 或 'right' - - Returns: - VRController实例或None - """ - if role == 'left': - return self.left_controller - elif role == 'right': - return self.right_controller - return None - - def are_controllers_connected(self): - """检查是否有控制器连接""" - return len(self.controllers) > 0 - - def get_connected_controllers(self): - """获取所有连接的控制器列表""" - return list(self.controllers.values()) - - def trigger_controller_haptic(self, role, duration=0.001, strength=1.0): - """触发控制器震动反馈 - - Args: - role: 'left', 'right' 或 'both' - duration: 震动持续时间(秒) - strength: 震动强度 (0.0-1.0) - """ - if role in ['left', 'both'] and self.left_controller: - self.left_controller.trigger_haptic_feedback(duration, strength) - - if role in ['right', 'both'] and self.right_controller: - self.right_controller.trigger_haptic_feedback(duration, strength) - - # VR动作系统便捷方法 - def is_trigger_pressed(self, hand='any'): - """检查扳机是否被按下 - - Args: - hand: 'left', 'right', 'any' - """ - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - pressed, _ = self.action_manager.is_digital_action_pressed('trigger', device_path) - return pressed - - def is_trigger_just_pressed(self, hand='any'): - """检查扳机是否刚刚被按下""" - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - pressed, _ = self.action_manager.is_digital_action_just_pressed('trigger', device_path) - return pressed - - def is_grip_pressed(self, hand='any'): - """检查握把是否被按下""" - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - pressed, _ = self.action_manager.is_digital_action_pressed('grip', device_path) - return pressed - - def is_grip_just_pressed(self, hand='any'): - """检查握把是否刚刚被按下""" - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - pressed, _ = self.action_manager.is_digital_action_just_pressed('grip', device_path) - return pressed - - def is_menu_pressed(self, hand='any'): - """检查菜单按钮是否被按下""" - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - pressed, _ = self.action_manager.is_digital_action_pressed('menu', device_path) - return pressed - - def is_trackpad_touched(self, hand='any'): - """检查触摸板是否被触摸""" - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - touched, _ = self.action_manager.is_digital_action_pressed('trackpad_touch', device_path) - return touched - - def get_trackpad_position(self, hand='any'): - """获取触摸板位置 - - Returns: - Vec2或None: 触摸板位置 (-1到1的范围) - """ - device_path = None - if hand == 'left': - device_path = '/user/hand/left' - elif hand == 'right': - device_path = '/user/hand/right' - - value, _ = self.action_manager.get_analog_action_value('trackpad', device_path) - return value - - # VR交互系统便捷方法 - def get_selected_object(self, hand='any'): - """获取指定手选中的对象 - - Args: - hand: 'left', 'right', 'any' - - Returns: - 选中的对象节点或None - """ - if hand == 'any': - # 返回任意手选中的对象 - for controller in self.get_connected_controllers(): - selected = self.interaction_manager.get_selected_object(controller.name) - if selected: - return selected - return None - else: - return self.interaction_manager.get_selected_object(hand) - - def get_grabbed_object(self, hand='any'): - """获取指定手抓取的对象 - - Args: - hand: 'left', 'right', 'any' - - Returns: - 抓取的对象节点或None - """ - if hand == 'any': - # 返回任意手抓取的对象 - for controller in self.get_connected_controllers(): - grabbed = self.interaction_manager.get_grabbed_object(controller.name) - if grabbed: - return grabbed - return None - else: - return self.interaction_manager.get_grabbed_object(hand) - - def is_grabbing_object(self, hand='any'): - """检查是否正在抓取对象 - - Args: - hand: 'left', 'right', 'any' - - Returns: - bool: 是否正在抓取 - """ - if hand == 'any': - # 检查任意手是否正在抓取 - for controller in self.get_connected_controllers(): - if self.interaction_manager.is_grabbing(controller.name): - return True - return False - else: - return self.interaction_manager.is_grabbing(hand) - - def force_release_all_grabs(self): - """强制释放所有抓取的对象""" - self.interaction_manager.force_release_all() - - def add_interactable_object(self, object_node): - """将对象标记为可交互 - - Args: - object_node: 要标记的对象节点 - """ - self.interaction_manager._add_collision_to_object(object_node) - - def _disable_async_reprojection(self): - """禁用异步重投影 - 备选修复方案""" - try: - # 尝试通过OpenVR设置禁用异步重投影 - if hasattr(openvr, 'VRSettings'): - settings = openvr.VRSettings() - if settings: - # 禁用异步重投影 - error = settings.setBool("steamvr", "enableAsyncReprojection", False) - if error == openvr.VRSettingsError_None: - print("✅ 异步重投影已禁用") - else: - print(f"⚠️ 禁用异步重投影失败: 设置错误 {error}") - else: - print("⚠️ 无法获取VR设置接口") - else: - print("⚠️ OpenVR设置接口不可用") - - except Exception as e: - print(f"⚠️ 禁用异步重投影失败: {e}") - - def enable_async_reprojection_disable(self): - """启用异步重投影禁用选项 - 用户可调用的方法""" - self.disable_async_reprojection = True - print("📝 异步重投影禁用选项已启用,将在下次VR初始化时生效") - - def disable_async_reprojection_disable(self): - """禁用异步重投影禁用选项 - 恢复默认行为""" - self.disable_async_reprojection = False - print("📝 异步重投影禁用选项已关闭,将使用默认ATW行为") - - def enable_gc_control(self): - """启用垃圾回收控制 - 减少VR渲染期间的GC峰值""" - if not self._gc_control_enabled: - self._gc_control_enabled = True - if not self._gc_disabled: - gc.disable() - self._gc_disabled = True - print("✅ VR垃圾回收控制已启用") - else: - print("ℹ️ VR垃圾回收控制已经启用") - - def disable_gc_control(self): - """禁用垃圾回收控制 - 恢复自动垃圾回收""" - if self._gc_control_enabled: - self._gc_control_enabled = False - if self._gc_disabled: - gc.enable() - self._gc_disabled = False - print("✅ VR垃圾回收控制已禁用,恢复自动垃圾回收") - else: - print("ℹ️ VR垃圾回收控制已经禁用") - - def set_manual_gc_interval(self, frames): - """设置手动垃圾回收间隔 - - Args: - frames: 帧数间隔 (建议100-600) - """ - if 50 <= frames <= 1800: - old_interval = self._manual_gc_interval - self._manual_gc_interval = frames - print(f"✅ 手动GC间隔: {old_interval} → {frames} 帧") - else: - print("⚠️ GC间隔应在50-1800帧之间") - - def force_manual_gc(self): - """强制执行一次垃圾回收""" - collected = gc.collect() - print(f"🗑️ 强制GC: 清理了 {collected} 个对象") - return collected - - def get_object_pool_status(self): - """获取对象池状态""" - return { - 'matrix_pool_size': len(self._matrix_pool) if hasattr(self, '_matrix_pool') else 0, - 'matrix_pool_capacity': getattr(self, '_matrix_pool_size', 0), - 'cached_controllers': len(self.controller_poses), - 'cached_matrices': len(getattr(self, '_cached_matrices', {})), - } - - # ====== VR分辨率缩放和质量预设系统 ====== - - def set_resolution_scale(self, scale): - """设置VR分辨率缩放系数 - - Args: - scale: 缩放系数 (0.3-1.0),0.75表示75%分辨率 - """ - if not (0.3 <= scale <= 1.0): - print(f"⚠️ 分辨率缩放系数应在0.3-1.0之间,当前: {scale}") - return False - - old_scale = self.resolution_scale - self.resolution_scale = scale - - # 如果VR已初始化,重新创建缓冲区 - if self.vr_initialized: - self._apply_resolution_scale() - - print(f"✓ VR分辨率缩放: {old_scale} → {scale}") - pixel_reduction = (1 - scale**2) * 100 - print(f"📊 像素减少: {pixel_reduction:.1f}%") - - return True - - def set_quality_preset(self, preset_name): - """设置VR质量预设 - - Args: - preset_name: 'performance', 'balanced', 'quality' - """ - if preset_name not in self.quality_presets: - print(f"⚠️ 未知的质量预设: {preset_name}") - print(f" 可用预设: {list(self.quality_presets.keys())}") - return False - - old_preset = self.current_quality_preset - self.current_quality_preset = preset_name - scale = self.quality_presets[preset_name] - - print(f"🎯 切换VR质量预设: {old_preset} → {preset_name}") - - return self.set_resolution_scale(scale) - - def cycle_quality_preset(self): - """循环切换质量预设""" - presets = list(self.quality_presets.keys()) - current_index = presets.index(self.current_quality_preset) - next_index = (current_index + 1) % len(presets) - next_preset = presets[next_index] - - return self.set_quality_preset(next_preset) - - def _apply_resolution_scale(self): - """应用分辨率缩放,重新创建VR缓冲区""" - try: - # 计算新的分辨率 - self.scaled_eye_width = int(self.base_eye_width * self.resolution_scale) - self.scaled_eye_height = int(self.base_eye_height * self.resolution_scale) - - # 更新当前分辨率 - self.eye_width = self.scaled_eye_width - self.eye_height = self.scaled_eye_height - self.current_eye_resolution = (self.eye_width, self.eye_height) - - print(f"🔄 重新创建VR缓冲区...") - print(f" 新分辨率: {self.eye_width}x{self.eye_height}") - - # 清理旧的缓冲区 - self._cleanup_vr_buffers() - - # 🔧 关键修复:根据渲染模式选择创建方法 - success = False - if self.vr_render_mode == VRRenderMode.RENDER_PIPELINE: - print(f" 使用RenderPipeline模式重建...") - success = self._create_vr_buffers_with_pipeline() - if not success: - print("⚠️ RenderPipeline模式创建失败,回退到普通渲染模式") - self.vr_render_mode = VRRenderMode.NORMAL - success = self._create_vr_buffers() - else: - print(f" 使用普通模式重建...") - success = self._create_vr_buffers() - - if success: - # 重新设置相机 - self._setup_vr_cameras() - print("✅ VR缓冲区重新创建成功") - return True - else: - print("❌ VR缓冲区重新创建失败") - return False - - except Exception as e: - print(f"❌ 应用分辨率缩放失败: {e}") - import traceback - traceback.print_exc() - return False - - - def get_resolution_info(self): - """获取分辨率相关信息""" - return { - 'base_resolution': (self.base_eye_width, self.base_eye_height), - 'current_resolution': (self.eye_width, self.eye_height), - 'resolution_scale': self.resolution_scale, - 'current_preset': self.current_quality_preset, - 'available_presets': self.quality_presets, - 'pixel_reduction_percent': (1 - self.resolution_scale**2) * 100 - } - - def print_resolution_info(self): - """输出分辨率信息""" - info = self.get_resolution_info() - print("🔧 ===== VR分辨率信息 =====") - print(f" 推荐分辨率: {info['base_resolution'][0]}x{info['base_resolution'][1]}") - print(f" 当前分辨率: {info['current_resolution'][0]}x{info['current_resolution'][1]}") - print(f" 缩放系数: {info['resolution_scale']}") - print(f" 当前预设: {info['current_preset']}") - print(f" 像素减少: {info['pixel_reduction_percent']:.1f}%") - print(" 可用预设:") - for name, scale in info['available_presets'].items(): - marker = "✓" if name == info['current_preset'] else " " - print(f" {marker} {name}: {scale} ({scale*100:.0f}%)") - print("==========================") - - # ====== 性能模式控制方法 ====== - - def enable_performance_mode(self): - """手动启用性能模式 - 立即禁用详细监控以提升性能""" - if not self.performance_mode_enabled: - self.performance_mode_enabled = True - print("🎯 性能模式已手动启用 - 禁用详细监控以提升性能") - print(" 现在将减少每帧对象创建,显著提升VR性能稳定性") - else: - print("ℹ️ 性能模式已经启用") - - def disable_performance_mode(self): - """禁用性能模式 - 重新启用详细监控(用于调试)""" - if self.performance_mode_enabled: - self.performance_mode_enabled = False - print("🔍 性能模式已禁用 - 重新启用详细监控") - print(" 注意:这将增加每帧对象创建,可能影响VR性能") - else: - print("ℹ️ 性能模式已经禁用") - - def set_performance_mode_trigger_frame(self, frame_count): - """设置性能模式自动触发的帧数 - - Args: - frame_count: 触发帧数 (建议300-1200) - """ - if 100 <= frame_count <= 3600: - old_trigger = self.performance_mode_trigger_frame - self.performance_mode_trigger_frame = frame_count - print(f"✅ 性能模式触发帧数: {old_trigger} → {frame_count}") - else: - print("⚠️ 触发帧数应在100-3600之间") - - def get_performance_mode_status(self): - """获取性能模式状态""" - return { - 'performance_mode_enabled': self.performance_mode_enabled, - 'trigger_frame': self.performance_mode_trigger_frame, - 'current_frame': self.frame_count, - 'will_trigger_at_frame': self.performance_mode_trigger_frame if not self.performance_mode_enabled else None, - 'gc_interval_normal': self._manual_gc_interval, - 'gc_interval_performance': self._manual_gc_interval * 2, - } - - # ====== VR测试模式 ====== - - def enable_vr_test_mode(self, display_mode='stereo'): - """启用VR测试模式 - 将VR渲染直接显示在屏幕上 - - Args: - display_mode: 显示模式 - - 'stereo': 左右眼并排显示 - - 'left': 只显示左眼 - - 'right': 只显示右眼 - """ - if not self.is_vr_available(): - print("❌ VR系统不可用,无法启动测试模式") - return False - - try: - print(f"🧪 启动VR测试模式 - 显示模式: {display_mode}") - - # 初始化VR系统(如果还没初始化) - if not self.vr_initialized: - if not self.initialize_vr(): - print("❌ VR初始化失败") - return False - - # 🔧 关键修复:确保测试模式的纹理资源已初始化 - # 这解决了测试模式纹理提交失败导致的36FPS问题 - print("🔧 检查VR测试模式纹理资源...") - if not self._ensure_test_mode_textures(): - print("❌ VR测试模式纹理资源初始化失败") - return False - - # 设置测试模式 - self.vr_test_mode = True - self.test_display_mode = display_mode - - # 启用VR渲染但不提交给OpenVR - if not self.vr_enabled: - # 启用VR渲染流程(但会在回调中跳过提交) - self.vr_enabled = True - self._disable_main_cam() - - # 设置高帧率用于测试 - if hasattr(self.world, 'qtWidget') and self.world.qtWidget: - if hasattr(self.world.qtWidget, 'synchronizer'): - self.world.qtWidget.synchronizer.setInterval(int(1000/144)) - print("✓ 测试模式:Qt Timer设置为144Hz") - - # 初始化测试显示系统 - if not self._initialize_test_display(): - print("❌ 测试显示系统初始化失败") - return False - - # 创建性能HUD - if not self._initialize_test_performance_hud(): - print("❌ 性能HUD初始化失败") - return False - - # 恢复主相机以查看测试内容 - self._enable_main_cam() - - # 重置HUD更新计数器 - self.hud_update_counter = 0 - - print("✅ VR测试模式已启用") - print(" - VR内容将显示在屏幕上") - print(" - 不会向OpenVR提交纹理") - print(" - 可以准确测量纯渲染性能") - print(f" - 当前显示模式: {display_mode}") - - return True - - except Exception as e: - print(f"❌ 启动VR测试模式失败: {e}") - import traceback - traceback.print_exc() - return False - - def _ensure_test_mode_textures(self): - """确保VR测试模式的纹理资源已正确初始化 - - 这解决了测试模式启用纹理提交时的36FPS问题: - - VR测试模式可能跳过了VR渲染缓冲区的初始化 - - submit_texture()需要有效的texture ID和OpenVR Texture对象 - - 如果未初始化会导致提交失败,造成帧率减半 - """ - try: - print(" 检查VR渲染缓冲区...") - - # 检查VR渲染缓冲区是否存在 - buffers_exist = ( - hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer and - hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer - ) - - if not buffers_exist: - print(" ⚠️ VR渲染缓冲区不存在,正在创建...") - if not self._setup_vr_render_buffers(): - print(" ❌ VR渲染缓冲区创建失败") - return False - print(" ✅ VR渲染缓冲区创建成功") - else: - print(" ✅ VR渲染缓冲区已存在") - - # 检查纹理ID是否已缓存 - print(" 检查纹理ID缓存...") - texture_ids_cached = ( - hasattr(self, 'left_texture_id') and self.left_texture_id and self.left_texture_id > 0 and - hasattr(self, 'right_texture_id') and self.right_texture_id and self.right_texture_id > 0 - ) - - if not texture_ids_cached: - print(" ⚠️ 纹理ID未缓存,正在准备纹理...") - if not self._prepare_and_cache_textures(): - print(" ❌ 纹理准备和缓存失败") - return False - print(f" ✅ 纹理ID缓存成功 - 左眼:{self.left_texture_id}, 右眼:{self.right_texture_id}") - else: - print(f" ✅ 纹理ID已缓存 - 左眼:{self.left_texture_id}, 右眼:{self.right_texture_id}") - - # 检查OpenVR Texture对象是否已创建 - print(" 检查OpenVR Texture对象...") - ovr_textures_exist = ( - hasattr(self, '_left_ovr_texture') and self._left_ovr_texture and - hasattr(self, '_right_ovr_texture') and self._right_ovr_texture - ) - - if not ovr_textures_exist: - print(" ⚠️ OpenVR Texture对象未创建,正在创建...") - self._create_cached_ovr_textures() - print(" ✅ OpenVR Texture对象创建成功") - else: - print(" ✅ OpenVR Texture对象已存在") - - print(" ✅ VR测试模式纹理资源检查完成,可安全启用纹理提交") - return True - - except Exception as e: - print(f" ❌ VR测试模式纹理资源检查失败: {e}") - import traceback - traceback.print_exc() - return False - - def _create_cached_ovr_textures(self): - """创建缓存的OpenVR Texture对象 - 避免每帧创建新对象""" - try: - import openvr - self._left_ovr_texture = openvr.Texture_t() - self._right_ovr_texture = openvr.Texture_t() - - # 设置固定属性(这些不变) - self._left_ovr_texture.eType = openvr.TextureType_OpenGL - self._left_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma - self._right_ovr_texture.eType = openvr.TextureType_OpenGL - self._right_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma - - print("✅ OpenVR Texture对象缓存已创建") - except Exception as e: - print(f"⚠️ OpenVR Texture对象创建失败: {e}") - # 不抛出异常,使用备用方案 - - def _batch_submit_textures(self): - """批量提交两眼纹理 - OpenVR最佳实践 - - 基于官方hellovr示例的实现: - - 两眼都渲染完成后,快速连续提交 - - 减少submit阻塞时间,避免错过VSync窗口 - - 这是解决36FPS问题的关键 - """ - try: - if not self.vr_compositor: - return False - - # 检查纹理是否准备好 - if not (self.vr_left_texture and self.vr_right_texture): - return False - - # 🚀 关键:快速连续提交两眼,最小化阻塞时间 - # 这符合OpenVR官方示例的做法 - success_left = False - success_right = False - - # 提交左眼纹理 - try: - self.submit_texture(openvr.Eye_Left, self.vr_left_texture) - success_left = True - except Exception as e: - print(f"❌ 批量提交左眼失败: {e}") - - # 立即提交右眼纹理(不等待) - try: - self.submit_texture(openvr.Eye_Right, self.vr_right_texture) - success_right = True - except Exception as e: - print(f"❌ 批量提交右眼失败: {e}") - - # 🚀 关键修复:调用PostPresentHandoff解除compositor阻塞 - # 这是解决36FPS问题的核心 - 确保compositor不会等待VSync - if success_left and success_right: - try: - # PostPresentHandoff告诉compositor我们已完成帧处理 - # 防止compositor等待下一个VSync周期 - if hasattr(self.vr_compositor, 'postPresentHandoff'): - self.vr_compositor.postPresentHandoff() - elif hasattr(self.vr_compositor, 'PostPresentHandoff'): - self.vr_compositor.PostPresentHandoff() - else: - # 备用方案:如果没有PostPresentHandoff,记录警告 - if not hasattr(self, '_post_present_warning_logged'): - print("⚠️ PostPresentHandoff方法未找到,可能影响时序") - self._post_present_warning_logged = True - except Exception as handoff_error: - if not hasattr(self, '_handoff_error_logged'): - print(f"⚠️ PostPresentHandoff调用失败: {handoff_error}") - self._handoff_error_logged = True - - # 记录批量提交状态(仅首次成功时) - if not hasattr(self, '_batch_submit_success_logged'): - print("✅ OpenVR批量提交模式+PostPresentHandoff已启用") - print(" 这应该解决36FPS → 72FPS的问题") - self._batch_submit_success_logged = True - return True - else: - return False - - except Exception as e: - print(f"❌ 批量提交纹理失败: {e}") - return False - - def disable_vr_test_mode(self): - """禁用VR测试模式""" - try: - print("🧪 禁用VR测试模式...") - - # 清理测试显示 - self._cleanup_test_display() - - # 清理性能HUD - self._cleanup_test_performance_hud() - - # 关闭测试模式 - self.vr_test_mode = False - self.test_mode_initialized = False - - # 重置HUD更新计数器 - self.hud_update_counter = 0 - - # 恢复正常VR模式或禁用VR - if self.vr_enabled: - print(" 选择: [1] 恢复正常VR模式 [2] 完全禁用VR") - # 这里可以让用户选择,现在默认禁用VR - self.disable_vr() - - print("✅ VR测试模式已禁用") - return True - - except Exception as e: - print(f"❌ 禁用VR测试模式失败: {e}") - return False - - def switch_test_display_mode(self, display_mode): - """切换测试显示模式 - - Args: - display_mode: 'stereo', 'left', 'right' - """ - if not self.vr_test_mode: - print("⚠️ 请先启用VR测试模式") - return False - - if display_mode not in ['stereo', 'left', 'right']: - print(f"⚠️ 无效的显示模式: {display_mode}") - return False - - print(f"🧪 切换显示模式: {self.test_display_mode} → {display_mode}") - - # 如果切换模式,需要清理旧的显示并重置标志位 - if self.test_display_mode != display_mode: - self._cleanup_test_display() - - self.test_display_mode = display_mode - - # 更新显示 - self._update_test_display() - return True - - def _initialize_test_display(self): - """初始化测试显示系统""" - try: - print("🔧 正在初始化测试显示系统...") - - # 导入必要的Panda3D组件 - from panda3d.core import CardMaker, PandaNode - - # 创建显示四边形 - cm = CardMaker("vr_test_display") - - if self.test_display_mode == 'stereo': - # 并排显示:左眼在左半屏,右眼在右半屏 - cm.setFrame(-1, 1, -0.5, 0.5) # 全屏 - else: - # 单眼显示:占据整个屏幕 - cm.setFrame(-1, 1, -1, 1) - - # 创建节点 - self.test_display_quad = self.world.render2d.attachNewNode(cm.generate()) - - # 设置初始纹理 - self._update_test_display() - - # 设置渲染状态 - self.test_display_quad.setTransparency(0) # 不透明 - self.test_display_quad.setBin("background", 0) # 背景层 - - self.test_mode_initialized = True - print("✅ 测试显示系统初始化完成") - return True - - except Exception as e: - print(f"❌ 测试显示系统初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def _update_test_display(self): - """更新测试显示内容""" - try: - if not self.test_mode_initialized: - return - - # 根据显示模式设置纹理 - if self.test_display_mode == 'left': - if not self.test_display_quad: - return - if self.vr_left_texture: - self.test_display_quad.setTexture(self.vr_left_texture) - elif self.test_display_mode == 'right': - if not self.test_display_quad: - return - if self.vr_right_texture: - self.test_display_quad.setTexture(self.vr_right_texture) - elif self.test_display_mode == 'stereo': - # 立体显示:只在第一次创建,后续只更新纹理 - if not self.stereo_display_created: - self._create_stereo_display() - else: - # 只更新纹理,不重新创建 - if self.test_display_quad and self.vr_left_texture: - self.test_display_quad.setTexture(self.vr_left_texture) - if self.test_right_quad and self.vr_right_texture: - self.test_right_quad.setTexture(self.vr_right_texture) - - except Exception as e: - print(f"⚠️ 更新测试显示失败: {e}") - - def _create_stereo_display(self): - """创建左右眼并排显示""" - try: - # 如果已经创建,直接返回 - if self.stereo_display_created and self.test_display_quad and self.test_right_quad: - return - - # 移除旧的显示 - if self.test_display_quad: - self.test_display_quad.removeNode() - if self.test_right_quad: - self.test_right_quad.removeNode() - - from panda3d.core import CardMaker - - # 创建左眼显示 - left_cm = CardMaker("vr_test_left") - left_cm.setFrame(-1, 0, -1, 1) # 左半屏 - left_quad = self.world.render2d.attachNewNode(left_cm.generate()) - if self.vr_left_texture: - left_quad.setTexture(self.vr_left_texture) - - # 创建右眼显示 - right_cm = CardMaker("vr_test_right") - right_cm.setFrame(0, 1, -1, 1) # 右半屏 - right_quad = self.world.render2d.attachNewNode(right_cm.generate()) - if self.vr_right_texture: - right_quad.setTexture(self.vr_right_texture) - - # 保存引用 - self.test_display_quad = left_quad # 保存左眼引用 - self.test_right_quad = right_quad # 额外保存右眼引用 - self.stereo_display_created = True # 标记已创建 - - print("✓ 立体显示已创建") - - except Exception as e: - print(f"⚠️ 创建立体显示失败: {e}") - self.stereo_display_created = False - - def _cleanup_test_display(self): - """清理测试显示""" - try: - if self.test_display_quad: - self.test_display_quad.removeNode() - self.test_display_quad = None - - if hasattr(self, 'test_right_quad') and self.test_right_quad: - self.test_right_quad.removeNode() - self.test_right_quad = None - - # 重置立体显示标志位 - self.stereo_display_created = False - - print("✓ 测试显示已清理") - - except Exception as e: - print(f"⚠️ 清理测试显示失败: {e}") - - def _initialize_test_performance_hud(self): - """初始化性能HUD""" - try: - print("🔧 正在初始化性能HUD...") - - from direct.gui.OnscreenText import OnscreenText - from panda3d.core import TextNode - - # 创建性能文本显示 - self.test_performance_text = OnscreenText( - text="初始化中...", - pos=(-0.95, 0.9), # 左上角,调整到屏幕内 - scale=0.05, - fg=(1, 1, 0, 1), # 黄色 - align=TextNode.ALeft, - shadow=(0, 0, 0, 0.5), # 黑色阴影 - parent=self.world.render2d - ) - - print("✅ 性能HUD初始化完成") - return True - - except Exception as e: - print(f"❌ 性能HUD初始化失败: {e}") - return False - - def _update_test_performance_hud(self): - """更新性能HUD显示(限制更新频率避免重影)""" - try: - if not self.test_performance_text or not self.vr_test_mode: - return - - # 增加计数器 - self.hud_update_counter += 1 - - # 只在指定间隔更新文本,避免重影 - if self.hud_update_counter < self.hud_update_interval: - return - - # 重置计数器 - self.hud_update_counter = 0 - - # 收集性能数据 - left_render_time = getattr(self, 'left_render_time', 0) - right_render_time = getattr(self, 'right_render_time', 0) - total_render_time = left_render_time + right_render_time - - # 计算FPS - current_fps = self.vr_fps if hasattr(self, 'vr_fps') else 0 - - # 获取系统性能 - cpu_usage = getattr(self, 'cpu_usage', 0) - memory_usage = getattr(self, 'memory_usage', 0) - gpu_usage = getattr(self, 'gpu_usage', 0) - - # 构建显示文本 - hud_text = f"""VR TEST MODE - {self.test_display_mode.upper()} - -Render Performance: - FPS: {current_fps:.1f} - Left Eye: {left_render_time:.2f}ms - Right Eye: {right_render_time:.2f}ms - Total: {total_render_time:.2f}ms - -System Performance: - CPU: {cpu_usage:.1f}% - Memory: {memory_usage:.1f}% - GPU: {gpu_usage:.1f}% - -Render Count: - Left Eye: {getattr(self, 'left_render_count', 0)} - Right Eye: {getattr(self, 'right_render_count', 0)} - -Target: <13.9ms@72Hz -Status: {'GOOD' if total_render_time < 10 else 'HIGH' if total_render_time < 16 else 'SLOW'} - -Hotkeys: -F1=Left F2=Right F3=SideBySide -ESC=Exit Test Mode""" - - # 更新文本 - self.test_performance_text.setText(hud_text) - - except Exception as e: - print(f"⚠️ 更新性能HUD失败: {e}") - - def _cleanup_test_performance_hud(self): - """清理性能HUD""" - try: - if self.test_performance_text: - self.test_performance_text.destroy() - self.test_performance_text = None - - print("✓ 性能HUD已清理") - - except Exception as e: - print(f"⚠️ 清理性能HUD失败: {e}") - - def get_test_mode_status(self): - """获取测试模式状态""" - if not self.vr_test_mode: - return None - - left_render_time = getattr(self, 'left_render_time', 0) - right_render_time = getattr(self, 'right_render_time', 0) - total_render_time = left_render_time + right_render_time - - return { - 'test_mode_enabled': self.vr_test_mode, - 'display_mode': self.test_display_mode, - 'vr_fps': getattr(self, 'vr_fps', 0), - 'left_render_time': left_render_time, - 'right_render_time': right_render_time, - 'total_render_time': total_render_time, - 'left_render_count': getattr(self, 'left_render_count', 0), - 'right_render_count': getattr(self, 'right_render_count', 0), - 'performance_rating': ('excellent' if total_render_time < 10 else - 'good' if total_render_time < 16 else 'poor') - } - - def set_test_mode_features(self, submit_texture=None, wait_poses=None): - """设置测试模式功能开关 - 用于渐进式调试 - - Args: - submit_texture: 是否在测试模式启用纹理提交(True/False/None=保持当前) - wait_poses: 是否在测试模式启用waitGetPoses(True/False/None=保持当前) - """ - if submit_texture is not None: - self.test_mode_submit_texture = submit_texture - if wait_poses is not None: - self.test_mode_wait_poses = wait_poses - - print(f"🔧 VR测试模式功能设置:") - print(f" 纹理提交: {'启用' if self.test_mode_submit_texture else '禁用'}") - print(f" 姿态等待: {'启用' if self.test_mode_wait_poses else '禁用'}") - - # 如果当前在测试模式,输出预期效果 - if self.vr_test_mode: - print(f" 当前测试模式已启用,设置将立即生效") - else: - print(f" 设置已保存,将在下次启用测试模式时生效") - - def get_test_mode_features(self): - """获取当前测试模式功能设置""" - return { - 'submit_texture': self.test_mode_submit_texture, - 'wait_poses': self.test_mode_wait_poses, - 'test_mode_active': self.vr_test_mode - } - - def run_vr_performance_test(self, duration_seconds=30, display_mode='stereo'): - """运行VR性能测试 - 简单的测试入口方法 - - Args: - duration_seconds: 测试持续时间(秒) - display_mode: 显示模式 ('stereo', 'left', 'right') - - Returns: - dict: 测试结果统计 - """ - print("🧪 ======= VR性能测试开始 =======") - print(f" 测试模式: {display_mode}") - print(f" 测试时长: {duration_seconds}秒") - print(" 按ESC键提前退出测试") - print(" F1=左眼 F2=右眼 F3=并排显示") - print("=================================") - - # 启动测试模式 - if not self.enable_vr_test_mode(display_mode): - print("❌ VR测试模式启动失败") - return None - - # 记录测试开始状态 - import time - test_start_time = time.time() - start_frame_count = getattr(self, 'frame_count', 0) - - print("✅ VR测试模式已启动") - print(" - VR内容直接显示在屏幕上") - print(" - 无OpenVR纹理提交开销") - print(" - 实时性能监控已启用") - print(f" - 当前显示: {display_mode.upper()}模式") - print("\n开始性能测试...") - - # 等待测试完成(用户手动停止或超时) - print(f"\n📊 测试将运行 {duration_seconds} 秒") - print(" 实时性能数据显示在屏幕左上角") - print(" 观察帧率和渲染时间变化") - - # 这里可以添加自动停止逻辑,但现在让用户手动控制 - print(f"\n⏰ 请观察 {duration_seconds} 秒后手动调用 disable_vr_test_mode() 停止测试") - print(" 或者随时调用 get_test_mode_status() 查看当前状态") - - return { - 'status': 'running', - 'start_time': test_start_time, - 'start_frame': start_frame_count, - 'display_mode': display_mode, - 'instructions': { - 'stop_test': 'vr_manager.disable_vr_test_mode()', - 'check_status': 'vr_manager.get_test_mode_status()', - 'switch_mode': 'vr_manager.switch_test_display_mode("left/right/stereo")' - } - } - - # ======================================================================== - # 性能监控属性代理 - 属性级别的API向后兼容 - # ======================================================================== - - @property - def left_render_count(self): - """左眼渲染计数器 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.left_render_count - return 0 - - @left_render_count.setter - def left_render_count(self, value): - """左眼渲染计数器设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.left_render_count = value - - @property - def right_render_count(self): - """右眼渲染计数器 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.right_render_count - return 0 - - @right_render_count.setter - def right_render_count(self, value): - """右眼渲染计数器设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.right_render_count = value - - @property - def left_render_time(self): - """左眼渲染时间 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.left_render_time - return 0.0 - - @left_render_time.setter - def left_render_time(self, value): - """左眼渲染时间设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.left_render_time = value - - @property - def right_render_time(self): - """右眼渲染时间 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.right_render_time - return 0.0 - - @right_render_time.setter - def right_render_time(self, value): - """右眼渲染时间设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.right_render_time = value - - # 帧计数和FPS相关属性 - @property - def frame_count(self): - """帧计数器 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.frame_count - return 0 - - @frame_count.setter - def frame_count(self, value): - """帧计数器设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.frame_count = value - - @property - def vr_fps(self): - """VR帧率 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.vr_fps - return 0.0 - - @vr_fps.setter - def vr_fps(self, value): - """VR帧率设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.vr_fps = value - - @property - def last_fps_check(self): - """上次FPS检查 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.last_fps_check - return 0 - - @last_fps_check.setter - def last_fps_check(self, value): - """上次FPS检查设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.last_fps_check = value - - @property - def last_fps_time(self): - """上次FPS时间 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.last_fps_time - return 0.0 - - @last_fps_time.setter - def last_fps_time(self, value): - """上次FPS时间设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.last_fps_time = value - - # 失败计数器 - @property - def submit_failures(self): - """提交失败计数 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.submit_failures - return 0 - - @submit_failures.setter - def submit_failures(self, value): - """提交失败计数设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.submit_failures = value - - @property - def pose_failures(self): - """姿态失败计数 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.pose_failures - return 0 - - @pose_failures.setter - def pose_failures(self, value): - """姿态失败计数设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.pose_failures = value - - # 性能监控开关 - @property - def performance_monitoring(self): - """性能监控开关 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.performance_monitoring - return False - - @performance_monitoring.setter - def performance_monitoring(self, value): - """性能监控开关设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.performance_monitoring = value - - @property - def debug_output_enabled(self): - """调试输出开关 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.debug_output_enabled - return False - - @debug_output_enabled.setter - def debug_output_enabled(self, value): - """调试输出开关设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.debug_output_enabled = value - - @property - def performance_mode_enabled(self): - """性能模式开关 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.performance_mode_enabled - return False - - @performance_mode_enabled.setter - def performance_mode_enabled(self, value): - """性能模式开关设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.performance_mode_enabled = value - - @property - def performance_mode_trigger_frame(self): - """性能模式触发帧 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.performance_mode_trigger_frame - return 600 - - @performance_mode_trigger_frame.setter - def performance_mode_trigger_frame(self, value): - """性能模式触发帧设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.performance_mode_trigger_frame = value - - # 时间监控属性 - @property - def wait_poses_time(self): - """姿态等待时间 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.wait_poses_time - return 0.0 - - @wait_poses_time.setter - def wait_poses_time(self, value): - """姿态等待时间设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.wait_poses_time = value - - @property - def submit_time(self): - """纹理提交时间 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.submit_time - return 0.0 - - @submit_time.setter - def submit_time(self, value): - """纹理提交时间设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.submit_time = value - - # 监控开关 - @property - def enable_gpu_timing(self): - """GPU计时开关 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.enable_gpu_timing - return False - - @enable_gpu_timing.setter - def enable_gpu_timing(self, value): - """GPU计时开关设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.enable_gpu_timing = value - - @property - def enable_pipeline_monitoring(self): - """管线监控开关 - 代理到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.enable_pipeline_monitoring - return True - - @enable_pipeline_monitoring.setter - def enable_pipeline_monitoring(self, value): - """管线监控开关设置 - 代理到性能监控系统""" - if self.performance_monitor: - self.performance_monitor.enable_pipeline_monitoring = value - - # ======================================================================== - # 性能监控委托方法 - API向后兼容层 - # ======================================================================== - - # 性能报告方法 - def _print_performance_report(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._print_performance_report() - - def _print_performance_recommendations(self, stats): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._print_performance_recommendations(stats) - - def _print_brief_performance_report(self, stats): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._print_brief_performance_report(stats) - - # 性能监控核心方法 - def _update_performance_metrics(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._update_performance_metrics() - - def _update_gpu_metrics(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._update_gpu_metrics() - - def _track_frame_time(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._track_frame_time() - - # GPU计时方法 - def _get_gpu_frame_timing(self, frames_ago=0): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._get_gpu_frame_timing(frames_ago) - return None - - def enable_gpu_timing_monitoring(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.enable_gpu_timing_monitoring() - - def disable_gpu_timing_monitoring(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.disable_gpu_timing_monitoring() - - # 管线统计方法 - def _start_timing(self, operation_name): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._start_timing(operation_name) - return None - - def _end_timing(self, timing_data): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._end_timing(timing_data) - return 0.0 - - def _get_pipeline_stats(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._get_pipeline_stats() - return {} - - def test_pipeline_monitoring(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.test_pipeline_monitoring() - - # 诊断方法 - def _print_render_callback_diagnostics(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._print_render_callback_diagnostics() - - def _check_rendering_optimizations(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._check_rendering_optimizations() - - def _diagnose_opengl_state(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor._diagnose_opengl_state() - - # 调试控制方法 - def enable_debug_output(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.enable_debug_output() - - def disable_debug_output(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.disable_debug_output() - - def set_debug_mode(self, mode): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.set_debug_mode(mode) - - def toggle_debug_output(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.toggle_debug_output() - return False - - def get_debug_status(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.get_debug_status() - return {} - - # 配置方法 - def set_performance_check_interval(self, interval): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.set_performance_check_interval(interval) - - def set_frame_time_history_size(self, size): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.set_frame_time_history_size(size) - - def set_performance_report_interval(self, frames): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.set_performance_report_interval(frames) - - def set_prediction_time(self, prediction_time_ms): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.set_prediction_time(prediction_time_ms) - - # 查询方法 - def get_performance_stats(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.get_performance_stats() - return {} - - def get_current_performance_summary(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.get_current_performance_summary() - return "性能监控未初始化" - - def get_performance_monitoring_config(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.get_performance_monitoring_config() - return {} - - def print_performance_monitoring_status(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.print_performance_monitoring_status() - - # 控制方法 - def enable_performance_monitoring(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.enable_performance_monitoring() - - def disable_performance_monitoring(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.disable_performance_monitoring() - - def force_performance_report(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.force_performance_report() - - def reset_performance_counters(self): - """委托到性能监控系统""" - if self.performance_monitor: - return self.performance_monitor.reset_performance_counters() +""" +VR管理器模块 + +负责VR功能的初始化、渲染和交互: +- OpenVR/SteamVR集成 +- VR头显跟踪和渲染 +- VR控制器交互 +- VR模式切换 +""" + +import sys +import gc +import numpy as np +from panda3d.core import ( + WindowProperties, GraphicsPipe, FrameBufferProperties, + GraphicsOutput, Texture, Camera, PerspectiveLens, MatrixLens, + Mat4, Vec3, TransformState, RenderState, CardMaker, + BitMask32, PandaNode, NodePath, LMatrix4, LVector3, LVector4, + CS_yup_right, CS_default, PythonCallbackObject +) +from direct.task import Task +from direct.showbase.DirectObject import DirectObject + +try: + import openvr + OPENVR_AVAILABLE = True +except ImportError: + OPENVR_AVAILABLE = False + print("警告: OpenVR未安装,VR功能将不可用") + +# 导入手柄控制器、动作系统、交互系统、摇杆系统和传送系统 +from core.vr.tracking.controllers import LeftController, RightController +from core.vr.interaction.actions import VRActionManager +from core.vr.interaction.grab import VRInteractionManager +from core.vr.interaction.joystick import VRJoystickManager +from core.vr.interaction.teleport import VRTeleportSystem +from core.vr.performance.monitoring import VRPerformanceMonitor +from enum import Enum + + +class VRRenderMode(Enum): + """VR渲染模式枚举""" + NORMAL = "normal" # 普通渲染模式 + RENDER_PIPELINE = "render_pipeline" # RenderPipeline高级渲染模式 + + +class VRManager(DirectObject): + """VR管理器类 - 处理所有VR相关功能""" + + def __init__(self, world): + """初始化VR管理器 + + Args: + world: 主世界对象引用 + """ + super().__init__() + + self.world = world + self.vr_system = None + self.vr_enabled = False + self.vr_initialized = False + + # VR渲染相关 + self.vr_left_eye_buffer = None + self.vr_right_eye_buffer = None + self.vr_left_camera = None + self.vr_right_camera = None + self.vr_compositor = None + + # VR纹理和ID缓存 - 修复重复准备问题 + self.vr_left_texture = None + self.vr_right_texture = None + self.left_texture_id = None # 缓存左眼纹理的OpenGL ID + self.right_texture_id = None # 缓存右眼纹理的OpenGL ID + self.textures_prepared = False # 标记纹理是否已准备 + + # VR跟踪数据 + self.hmd_pose = Mat4.identMat() + self.controller_poses = {} + self.tracked_device_poses = [] + self.poses = None # OpenVR渲染姿态数组 + self.game_poses = None # OpenVR游戏逻辑姿态数组 + + # 🚀 对象池和缓存系统 - 修复16-19帧周期性GPU峰值 + self._matrix_pool = [] # Mat4对象池 + self._matrix_pool_size = 8 # 池大小,足够处理多个控制器 + self._cached_matrices = {} # 设备ID到矩阵的缓存 + self._controller_poses_cache = {} # 控制器姿态缓存,避免每帧clear() + + # 🚀 OpenVR Texture对象缓存 - 避免每帧创建openvr.Texture_t() + self._left_ovr_texture = None # 左眼纹理对象缓存 + self._right_ovr_texture = None # 右眼纹理对象缓存 + + # Python垃圾回收控制 + self._gc_control_enabled = True # 是否启用GC控制 + self._gc_disabled = False # GC是否被禁用 + self._manual_gc_interval = 900 # 每900帧手动触发一次GC (15秒@60fps) - 减少GC频率 + self._last_manual_gc_frame = 0 + + # 🚀 立即初始化对象池和GC控制(在其他组件之前) + self._initialize_object_pools() + + # VR渲染参数 + self.eye_width = 1080 + self.eye_height = 1200 + self.near_clip = 0.1 + self.far_clip = 1000.0 + + # VR分辨率缩放优化 + self.resolution_scale = 0.75 # 默认0.75倍分辨率,性能和质量平衡 + self.base_eye_width = 1080 # 原始推荐分辨率 + self.base_eye_height = 1200 + self.scaled_eye_width = 1080 # 实际使用的缩放后分辨率 + self.scaled_eye_height = 1200 + + # VR质量预设 + self.quality_presets = { + 'performance': 0.6, # 性能模式 - 约60%分辨率 + 'balanced': 0.75, # 平衡模式 - 约75%分辨率 + 'quality': 1.0 # 质量模式 - 100%分辨率 + } + self.current_quality_preset = 'balanced' # 默认平衡模式 + + # 🎨 VR渲染模式配置 - RenderPipeline集成 + self.vr_render_mode = VRRenderMode.NORMAL # 默认使用普通渲染模式 + self.render_pipeline_enabled = False # RenderPipeline是否已启用 + self.vr_pipeline_left_target = None # 左眼RenderPipeline渲染目标 + self.vr_pipeline_right_target = None # 右眼RenderPipeline渲染目标 + self.pipeline_resolution_scale = 0.75 # RenderPipeline模式下的分辨率缩放(性能优化) + self.vr_pipeline_controller = None # VR Pipeline控制器(管理完整的VR渲染管线) + + # RenderPipeline VR优化配置 + self.pipeline_vr_config = { + 'enable_shadows': True, # 启用阴影 + 'enable_ao': True, # 启用环境光遮蔽 + 'enable_bloom': False, # 禁用泛光(VR性能考虑) + 'enable_motion_blur': False, # 禁用运动模糊(VR中会引起不适) + 'enable_ssr': False, # 禁用屏幕空间反射(性能密集) + 'shadow_quality': 'medium', # 阴影质量:low/medium/high + 'ao_quality': 'low', # AO质量:low/medium/high + } + + # VR任务 + self.vr_task = None + + # VR锚点层级系统 + self.tracking_space = None + self.hmd_anchor = None + self.left_eye_anchor = None + self.right_eye_anchor = None + + # 坐标系转换矩阵 - 使用Panda3D内置方法 + self.coord_mat = LMatrix4.convert_mat(CS_yup_right, CS_default) + self.coord_mat_inv = LMatrix4.convert_mat(CS_default, CS_yup_right) + + # OpenVR 帧ID跟踪(防止重复提交) + self.openvr_frame_id = 0 + self.left_eye_last_render_frame = -1 + self.right_eye_last_render_frame = -1 + + # 🧪 VR测试模式 + self.vr_test_mode = False # 是否启用VR测试模式 + self.test_display_mode = 'stereo' # 'left', 'right', 'stereo' + self.test_display_quad = None # 测试显示的四边形 + self.test_right_quad = None # 右眼显示的四边形(立体模式) + self.stereo_display_created = False # 立体显示是否已创建 + self.test_performance_hud = None # 性能HUD + self.test_performance_text = None # 性能文本节点 + self.test_mode_initialized = False # 测试模式是否已初始化 + self.hud_update_counter = 0 # HUD更新计数器 + self.hud_update_interval = 30 # HUD更新间隔(帧数),30帧约0.5秒@60fps + + # 🔧 VR测试模式调试选项 - 逐步启用普通VR功能 + self.test_mode_submit_texture = False # 是否在测试模式提交纹理到OpenVR + self.test_mode_wait_poses = False # 是否在测试模式调用waitGetPoses + + # waitGetPoses调用策略配置 - 使用高性能的update_task策略 + self.use_prediction_time = 0.011 # 11ms的预测时间 - OpenVR标准值,平衡准确性和延迟 + self.poses_updated_in_task = True # 始终在更新任务中获取姿态(Running Start模式) + + # 🎨 初始化VR配置管理器 + try: + from core.vr.config.vr_config import VRConfigManager + self.config_manager = VRConfigManager() + # 加载配置并应用 + self.config_manager.apply_config_to_vr_manager(self) + print("✓ VR配置管理器初始化完成并已加载配置") + except Exception as e: + print(f"⚠️ VR配置管理器初始化失败: {e}") + self.config_manager = None + + # VR提交策略 - 基于参考实现 + self.submit_together = True # 是否在right_cb中同时提交左右眼 + + + # Running Start标记 - Valve最佳实践 + self._waitgetposes_called_this_frame = False + + # 姿态缓存 - 修复时序不匹配 + self._cached_render_poses = None # 用于渲染的缓存姿态 + self._first_frame = True # 首帧标记 + + # ATW控制选项 - 备选方案 + self.disable_async_reprojection = False # 是否禁用异步重投影 + + # VR手柄控制器 + self.left_controller = None + self.right_controller = None + self.controllers = {} # 设备索引到控制器的映射 + self.tracked_device_anchors = {} # 跟踪设备锚点 + + # VR动作系统 - 可选择禁用(用于API兼容性问题) + self.disable_action_system = True # 禁用动作系统,使用直接控制器输入 + + if not self.disable_action_system: + try: + self.action_manager = VRActionManager(self) + print("✓ VR动作管理器初始化完成") + except Exception as e: + print(f"⚠️ VR动作管理器初始化失败: {e}") + self.action_manager = None + else: + self.action_manager = None + print("🚫 VR动作系统已禁用,使用直接控制器输入") + + # VR交互系统 - 添加异常保护 + try: + self.interaction_manager = VRInteractionManager(self) + print("✓ VR交互管理器初始化完成") + except Exception as e: + print(f"⚠️ VR交互管理器初始化失败: {e}") + self.interaction_manager = None + + # VR传送系统 - 添加异常保护 + try: + self.teleport_system = VRTeleportSystem(self) + print("✓ VR传送系统初始化完成") + except Exception as e: + print(f"⚠️ VR传送系统初始化失败: {e}") + self.teleport_system = None + + # VR摇杆系统 - 添加异常保护 + try: + self.joystick_manager = VRJoystickManager(self) + print("✓ VR摇杆管理器初始化完成") + except Exception as e: + print(f"⚠️ VR摇杆管理器初始化失败: {e}") + self.joystick_manager = None + + # 🎯 性能监控系统 - 模块化重构 + try: + self.performance_monitor = VRPerformanceMonitor(self) + print("✓ VR性能监控系统初始化完成") + except Exception as e: + print(f"⚠️ VR性能监控系统初始化失败: {e}") + self.performance_monitor = None + + print("✓ VR管理器初始化完成") + + def _initialize_object_pools(self): + """初始化对象池 - 修复16-19帧周期性GPU峰值""" + try: + # 预填充Mat4对象池 + for _ in range(self._matrix_pool_size): + self._matrix_pool.append(Mat4()) + + print(f"✅ Mat4对象池初始化完成 - 池大小: {self._matrix_pool_size}") + + # 🚀 预创建OpenVR Texture对象 - 消除每帧创建openvr.Texture_t()的开销 + try: + import openvr + self._left_ovr_texture = openvr.Texture_t() + self._right_ovr_texture = openvr.Texture_t() + + # 设置固定属性(这些不变) + self._left_ovr_texture.eType = openvr.TextureType_OpenGL + self._left_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + self._right_ovr_texture.eType = openvr.TextureType_OpenGL + self._right_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + + print("✅ OpenVR Texture对象缓存已创建 - 避免每帧创建新对象") + except Exception as texture_error: + print(f"⚠️ OpenVR Texture对象创建失败: {texture_error}") + # 不影响整体初始化,但性能可能不是最优 + + # 启用GC控制 + if self._gc_control_enabled: + # 禁用自动垃圾回收,改为手动控制 + gc.disable() + self._gc_disabled = True + print("✅ Python垃圾回收已禁用,改为手动控制") + + except Exception as e: + print(f"⚠️ 对象池初始化失败: {e}") + + def _get_pooled_matrix(self): + """从对象池获取Mat4对象""" + if self._matrix_pool: + return self._matrix_pool.pop() + else: + # 池为空时创建新对象(不应该发生,但作为备用) + return Mat4() + + def _return_pooled_matrix(self, matrix): + """将Mat4对象返回对象池""" + if len(self._matrix_pool) < self._matrix_pool_size: + # 重置矩阵到单位矩阵 + matrix.identMat() + self._matrix_pool.append(matrix) + + def _manual_gc_control(self): + """手动垃圾回收控制 - 避免VR渲染期间的GC峰值""" + if not self._gc_control_enabled or not self._gc_disabled: + return + + # 🚀 智能GC间隔:性能模式下减少GC频率 + current_interval = self._manual_gc_interval + if self.performance_mode_enabled: + current_interval = self._manual_gc_interval * 2 # 性能模式下间隔翻倍 + + # 每N帧手动触发一次垃圾回收 + if self.frame_count - self._last_manual_gc_frame >= current_interval: + # 在非渲染关键时刻触发GC + collected = gc.collect() + self._last_manual_gc_frame = self.frame_count + + # 仅在收集到对象时输出信息 + if collected > 0: + print(f"🗑️ 手动GC: 清理了 {collected} 个对象 (帧#{self.frame_count})") + + def _update_matrix_from_openvr(self, panda_mat, ovr_matrix): + """直接更新现有Mat4对象的数值,避免创建新对象""" + # 复用_convert_openvr_matrix_to_panda中的转换逻辑 + # X轴行:Panda3D的X轴对应OpenVR的X轴 + panda_mat.setCell(0, 0, ovr_matrix[0][0]) # X_x → X_x + panda_mat.setCell(0, 1, ovr_matrix[0][1]) # X_y → X_y + panda_mat.setCell(0, 2, ovr_matrix[0][2]) # X_z → X_z + panda_mat.setCell(0, 3, ovr_matrix[0][3]) # 位移X分量 + + # Y轴行:Panda3D的Y轴对应OpenVR的-Z轴 + panda_mat.setCell(1, 0, -ovr_matrix[2][0]) # -Z_x → Y_x + panda_mat.setCell(1, 1, -ovr_matrix[2][1]) # -Z_y → Y_y + panda_mat.setCell(1, 2, -ovr_matrix[2][2]) # -Z_z → Y_z + panda_mat.setCell(1, 3, -ovr_matrix[2][3]) # 位移Y分量(-Z位移) + + # Z轴行:Panda3D的Z轴对应OpenVR的Y轴 + panda_mat.setCell(2, 0, ovr_matrix[1][0]) # Y_x → Z_x + panda_mat.setCell(2, 1, ovr_matrix[1][1]) # Y_y → Z_y + panda_mat.setCell(2, 2, ovr_matrix[1][2]) # Y_z → Z_z + panda_mat.setCell(2, 3, ovr_matrix[1][3]) # 位移Z分量(Y位移) + + # 齐次坐标 + panda_mat.setCell(3, 0, 0) + panda_mat.setCell(3, 1, 0) + panda_mat.setCell(3, 2, 0) + panda_mat.setCell(3, 3, 1) + + def convert_mat(self, mat): + """ + 将OpenVR矩阵转换为Panda3D矩阵 - 基于参考实现 + """ + if len(mat.m) == 4: + result = LMatrix4( + mat.m[0][0], mat.m[1][0], mat.m[2][0], mat.m[3][0], + mat.m[0][1], mat.m[1][1], mat.m[2][1], mat.m[3][1], + mat.m[0][2], mat.m[1][2], mat.m[2][2], mat.m[3][2], + mat.m[0][3], mat.m[1][3], mat.m[2][3], mat.m[3][3]) + elif len(mat.m) == 3: + result = LMatrix4( + mat.m[0][0], mat.m[1][0], mat.m[2][0], 0.0, + mat.m[0][1], mat.m[1][1], mat.m[2][1], 0.0, + mat.m[0][2], mat.m[1][2], mat.m[2][2], 0.0, + mat.m[0][3], mat.m[1][3], mat.m[2][3], 1.0) + return result + + def is_vr_available(self): + """检查VR系统是否可用""" + if not OPENVR_AVAILABLE: + return False + + try: + # 检查SteamVR是否运行 + return openvr.isRuntimeInstalled() and openvr.isHmdPresent() + except Exception as e: + print(f"VR检查失败: {e}") + return False + + def initialize_vr(self): + """初始化VR系统""" + if not OPENVR_AVAILABLE: + print("❌ OpenVR不可用,无法初始化VR") + return False + + if self.vr_initialized: + print("VR系统已经初始化") + return True + + try: + print("🔄 正在初始化VR系统...") + + # 🚀 确保对象池已正确初始化(备用检查) + if not hasattr(self, '_matrix_pool') or len(self._matrix_pool) == 0: + print("⚠️ 对象池未初始化,正在重新初始化...") + self._initialize_object_pools() + + # 初始化OpenVR - 使用Scene应用类型确保正确的焦点管理 + self.vr_system = openvr.init(openvr.VRApplication_Scene) + if not self.vr_system: + print("❌ 无法初始化OpenVR系统") + return False + + # 获取compositor + self.vr_compositor = openvr.VRCompositor() + if not self.vr_compositor: + print("❌ 无法获取VR Compositor") + return False + + # 获取推荐的渲染目标尺寸 + base_width, base_height = self.vr_system.getRecommendedRenderTargetSize() + self.base_eye_width = base_width + self.base_eye_height = base_height + + # 应用分辨率缩放 + self.scaled_eye_width = int(base_width * self.resolution_scale) + self.scaled_eye_height = int(base_height * self.resolution_scale) + + # 使用缩放后的分辨率作为实际渲染分辨率 + self.eye_width = self.scaled_eye_width + self.eye_height = self.scaled_eye_height + + self.current_eye_resolution = (self.eye_width, self.eye_height) + self.recommended_eye_resolution = (base_width, base_height) + + print(f"✓ VR基础分辨率: {base_width}x{base_height}") + print(f"✓ VR缩放系数: {self.resolution_scale}") + print(f"✓ VR实际分辨率: {self.eye_width}x{self.eye_height}") + print(f"📊 分辨率优化: {(1 - self.resolution_scale**2) * 100:.1f}% 像素减少") + + # 获取VR系统信息 + try: + # 获取显示频率 + self.vr_display_frequency = self.vr_system.getFloatTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_DisplayFrequency_Float + ) + print(f"✓ VR显示频率: {self.vr_display_frequency} Hz") + + # 获取IPD(瞳距) + ipd = self.vr_system.getFloatTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_UserIpdMeters_Float + ) + print(f"✓ IPD(瞳距): {ipd * 1000:.1f}mm") + + # 获取设备制造商和型号 + manufacturer = self.vr_system.getStringTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_ManufacturerName_String + ) + model = self.vr_system.getStringTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_ModelNumber_String + ) + print(f"✓ VR设备: {manufacturer} {model}") + + # 获取更多系统配置信息 + try: + # 获取刷新率相关信息 + seconds_since_last_vsync = self.vr_system.getFloatTrackedDeviceProperty( + openvr.k_unTrackedDeviceIndex_Hmd, + openvr.Prop_SecondsFromVsyncToPhotons_Float + ) + self.vsync_to_photons_ms = seconds_since_last_vsync * 1000 + print(f"✓ VSync到光子延迟: {self.vsync_to_photons_ms:.2f}ms") + + # 获取显示延迟信息 + display_refresh_rate = self.vr_display_frequency + if display_refresh_rate > 0: + self.target_frame_time_ms = 1000.0 / display_refresh_rate + print(f"✓ 目标帧时间: {self.target_frame_time_ms:.2f}ms") + + # 计算VSync时间窗口 + self.vsync_window_ms = self.target_frame_time_ms * 0.1 # 10%的窗口 + print(f"✓ VSync时间窗口: ±{self.vsync_window_ms:.2f}ms") + + except Exception as vsync_error: + print(f"⚠️ 获取VSync信息失败: {vsync_error}") + + # 检查是否启用了异步重投影 + try: + if hasattr(openvr, 'VRSettings'): + settings = openvr.VRSettings() + if settings: + self.async_reprojection_enabled = settings.getBool("steamvr", "enableAsyncReprojection") + print(f"✓ 异步重投影: {'启用' if self.async_reprojection_enabled else '禁用'}") + + self.motion_smoothing_enabled = settings.getBool("steamvr", "motionSmoothing") + print(f"✓ 运动平滑: {'启用' if self.motion_smoothing_enabled else '禁用'}") + + except Exception as settings_error: + print(f"⚠️ 获取VR设置失败: {settings_error}") + + except Exception as e: + print(f"⚠️ 获取VR系统信息失败: {e}") + + # 创建OpenVR姿态数组 - 分离渲染和游戏姿态 + poses_t = openvr.TrackedDevicePose_t * openvr.k_unMaxTrackedDeviceCount + self.poses = poses_t() # 渲染姿态(预测的) + self.game_poses = poses_t() # 游戏逻辑姿态(当前的) + print("✓ VR渲染和游戏姿态数组已创建") + + # 🔧 关键修复:统一初始化流程 + # 如果目标模式是RenderPipeline,先用普通模式初始化,然后切换 + # 这样两个场景都走相同的、已验证的代码路径 + target_render_mode = self.vr_render_mode + use_deferred_pipeline_switch = False + + if target_render_mode == VRRenderMode.RENDER_PIPELINE: + print("🎨 目标VR渲染模式: RenderPipeline") + print(" 策略:先用普通模式初始化,然后切换到RenderPipeline") + self.vr_render_mode = VRRenderMode.NORMAL + use_deferred_pipeline_switch = True + else: + print(f"🎨 VR渲染模式: {self.vr_render_mode.value}") + + # 创建VR渲染缓冲区 - 始终用普通模式初始化 + if not self._create_vr_buffers(): + print("❌ 创建VR渲染缓冲区失败") + return False + + # 设置VR相机 + if not self._setup_vr_cameras(): + print("❌ 设置VR相机失败") + return False + + # 优化VR渲染管线 + self._optimize_vr_rendering() + + # 初始化手柄控制器 + self._initialize_controllers() + + # 初始化动作系统 - 仅在启用时初始化 + if not self.disable_action_system and self.action_manager: + try: + if not self.action_manager.initialize(): + print("⚠️ VR动作系统初始化失败,但VR系统将继续运行") + except Exception as e: + print(f"⚠️ VR动作系统初始化异常: {e}") + else: + if self.disable_action_system: + print("🚫 VR动作系统已禁用,跳过初始化") + else: + print("⚠️ VR动作管理器未创建,跳过动作系统初始化") + + # 初始化交互系统 - 检查是否存在 + if self.interaction_manager: + try: + if not self.interaction_manager.initialize(): + print("⚠️ VR交互系统初始化失败,但VR系统将继续运行") + except Exception as e: + print(f"⚠️ VR交互系统初始化异常: {e}") + else: + print("⚠️ VR交互管理器未创建,跳过交互系统初始化") + + # 初始化VR传送系统 + if self.teleport_system: + try: + if not self.teleport_system.initialize(): + print("⚠️ VR传送系统初始化失败,但VR系统将继续运行") + except Exception as e: + print(f"⚠️ VR传送系统初始化异常: {e}") + else: + print("⚠️ VR传送系统未创建,跳过传送系统初始化") + + # 初始化VR摇杆系统 + if self.joystick_manager: + try: + # 传入传送系统引用给摇杆管理器 + self.joystick_manager.initialize(self.teleport_system) + print("✓ VR摇杆系统初始化成功") + except Exception as e: + print(f"⚠️ VR摇杆系统初始化异常: {e}") + else: + print("⚠️ VR摇杆管理器未创建,跳过摇杆系统初始化") + + # 可选:禁用异步重投影(备选方案) + if self.disable_async_reprojection: + self._disable_async_reprojection() + + # 启动VR更新任务 + self._start_vr_task() + + self.vr_initialized = True + print("✅ VR系统初始化成功(普通模式)") + + # 🔧 关键修复:如果目标是RenderPipeline模式,现在切换过去 + # 这样场景2(先设RP再进VR)会走场景1(先VR再设RP)相同的代码路径 + if use_deferred_pipeline_switch: + print("\n🔄 现在切换到目标渲染模式: RenderPipeline") + print(" 这将触发buffer重建和visualizer刷新...") + if self.set_vr_render_mode(target_render_mode): + print("✅ 已成功切换到RenderPipeline模式") + else: + print("⚠️ 切换到RenderPipeline失败,保持普通模式") + + return True + + except Exception as e: + print(f"❌ VR初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_vr_buffers(self): + """创建VR渲染缓冲区 - 使用分辨率缩放优化""" + try: + print(f"🔧 创建VR缓冲区:") + print(f" 推荐分辨率: {self.base_eye_width}x{self.base_eye_height}") + print(f" 缩放系数: {self.resolution_scale}") + print(f" 实际分辨率: {self.eye_width}x{self.eye_height}") + print(f" 像素减少: {(1 - self.resolution_scale**2) * 100:.1f}%") + + # 创建左眼纹理和缓冲区 + self.vr_left_texture = self._create_vr_texture("VR Left Eye Texture") + self.vr_left_eye_buffer = self._create_vr_buffer( + "VR Left Eye", + self.vr_left_texture, + self.eye_width, + self.eye_height + ) + + if not self.vr_left_eye_buffer: + print("❌ 创建左眼缓冲区失败") + return False + + # 设置左眼缓冲区属性 + self.vr_left_eye_buffer.setSort(-100) + self.vr_left_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) # 深蓝色背景便于调试 + self.vr_left_eye_buffer.setActive(True) + + # 创建右眼纹理和缓冲区 + self.vr_right_texture = self._create_vr_texture("VR Right Eye Texture") + self.vr_right_eye_buffer = self._create_vr_buffer( + "VR Right Eye", + self.vr_right_texture, + self.eye_width, + self.eye_height + ) + + if not self.vr_right_eye_buffer: + print("❌ 创建右眼缓冲区失败") + return False + + # 设置右眼缓冲区属性 + self.vr_right_eye_buffer.setSort(-99) + self.vr_right_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) # 深蓝色背景便于调试 + self.vr_right_eye_buffer.setActive(True) + + # 🚀 关键优化:立即准备纹理并缓存OpenGL ID,避免后续重复准备 + print("🔧 准备纹理并缓存OpenGL ID...") + if self._prepare_and_cache_textures(): + print("✅ VR渲染缓冲区和纹理缓存创建成功") + return True + else: + print("❌ 纹理准备失败") + return False + + except Exception as e: + print(f"❌ 创建VR缓冲区失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_vr_texture(self, name): + """创建VR纹理对象 - 基于参考实现""" + texture = Texture(name) + texture.setWrapU(Texture.WMClamp) + texture.setWrapV(Texture.WMClamp) + texture.setMinfilter(Texture.FTLinear) + texture.setMagfilter(Texture.FTLinear) + return texture + + def _prepare_and_cache_textures(self): + """准备纹理并缓存OpenGL ID - 解决重复准备问题""" + try: + # 🔧 验证纹理对象存在 + if not self.vr_left_texture or not self.vr_right_texture: + print("❌ VR纹理对象不存在") + print(f" 左眼纹理: {self.vr_left_texture}") + print(f" 右眼纹理: {self.vr_right_texture}") + return False + + # 获取graphics state guardian和prepared objects + gsg = self.world.win.getGsg() + if not gsg: + print("❌ 无法获取GraphicsStateGuardian") + return False + + prepared_objects = gsg.getPreparedObjects() + if not prepared_objects: + print("❌ 无法获取PreparedObjects") + return False + + # 准备左眼纹理并缓存ID + print(f" 准备左眼纹理: {self.vr_left_texture.getXSize()}x{self.vr_left_texture.getYSize()}") + texture_context = self.vr_left_texture.prepareNow(0, prepared_objects, gsg) + if texture_context and hasattr(texture_context, 'getNativeId'): + self.left_texture_id = texture_context.getNativeId() + if self.left_texture_id > 0: + print(f" ✅ 左眼纹理准备完成 ({self.vr_render_mode.value}): ID={self.left_texture_id}") + else: + print(" ❌ 左眼纹理ID无效") + return False + else: + print(" ❌ 左眼纹理准备失败") + return False + + # 准备右眼纹理并缓存ID + print(f" 准备右眼纹理: {self.vr_right_texture.getXSize()}x{self.vr_right_texture.getYSize()}") + texture_context = self.vr_right_texture.prepareNow(0, prepared_objects, gsg) + if texture_context and hasattr(texture_context, 'getNativeId'): + self.right_texture_id = texture_context.getNativeId() + if self.right_texture_id > 0: + print(f" ✅ 右眼纹理准备完成 ({self.vr_render_mode.value}): ID={self.right_texture_id}") + else: + print(" ❌ 右眼纹理ID无效") + return False + else: + print(" ❌ 右眼纹理准备失败") + return False + + # 标记纹理已准备 + self.textures_prepared = True + print(" ✅ 纹理准备完成,ID已缓存,后续帧将直接使用缓存ID") + return True + + except Exception as e: + print(f"❌ 纹理准备和缓存失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_vr_buffer(self, name, texture, width, height): + """创建VR渲染缓冲区 - 基于参考实现 + 性能诊断""" + # 设置帧缓冲属性 + fbprops = FrameBufferProperties() + fbprops.setRgbaBits(1, 1, 1, 1) + # 可以在这里添加多重采样抗锯齿 + # fbprops.setMultisamples(4) + + # 创建缓冲区 + buffer = self.world.win.makeTextureBuffer(name, width, height, to_ram=False, fbp=fbprops) + + if buffer: + # 🔍 性能诊断:检查缓冲区类型和属性 + self._diagnose_buffer_performance(buffer, name, width, height) + + # 清除默认渲染纹理 + buffer.clearRenderTextures() + # 🚀 使用RTMBindOrCopy模式 - 已经是最优选择 + # RTMBindOrCopy会尝试直接绑定到纹理,只有硬件不支持时才回退到复制 + # 这已经是Panda3D中最高效的渲染到纹理模式 + buffer.addRenderTexture(texture, GraphicsOutput.RTMBindOrCopy, GraphicsOutput.RTPColor) + + else: + print(f"⚠️ VR缓冲区创建失败: {name} ({width}x{height})") + + return buffer + + def _diagnose_buffer_performance(self, buffer, name, width, height): + """诊断VR缓冲区性能特性""" + try: + # 检查缓冲区类型 + buffer_type = type(buffer).__name__ + + # 检查是否为parasite buffer(性能较差的类型) + is_parasite = 'Parasite' in buffer_type + + # 获取缓冲区信息 + is_valid = buffer.isValid() + is_hardware = hasattr(buffer, 'getGsg') and buffer.getGsg() is not None + + # 输出诊断信息(仅第一次创建时) + if not hasattr(self, '_buffer_diagnosis_shown'): + print(f"🔍 VR缓冲区诊断 [{name}]:") + print(f" 类型: {buffer_type}") + print(f" 分辨率: {width}x{height} ({width*height/1000000:.1f}M像素)") + print(f" 有效性: {'✓' if is_valid else '✗'}") + print(f" 硬件支持: {'✓' if is_hardware else '✗'}") + + if is_parasite: + print(f" ⚠️ 检测到Parasite Buffer - 性能可能受限") + print(f" 建议: 检查显卡是否支持真正的离屏渲染") + else: + print(f" ✓ 使用硬件FBO - 性能良好") + + # 显示帧缓冲区属性 + if hasattr(buffer, 'getFbProperties'): + fbp = buffer.getFbProperties() + if fbp: + print(f" 颜色位数: R{fbp.getRedBits()}G{fbp.getGreenBits()}B{fbp.getBlueBits()}A{fbp.getAlphaBits()}") + if hasattr(fbp, 'getMultisamples') and fbp.getMultisamples() > 0: + print(f" MSAA: {fbp.getMultisamples()}x") + + self._buffer_diagnosis_shown = True + + except Exception as e: + print(f"缓冲区诊断失败: {e}") + + def _create_vr_buffers_with_pipeline(self): + """创建带RenderPipeline的VR渲染缓冲区 - 高级渲染模式""" + try: + print(f"🎨 创建RenderPipeline VR缓冲区:") + print(f" 推荐分辨率: {self.base_eye_width}x{self.base_eye_height}") + print(f" Pipeline缩放: {self.pipeline_resolution_scale}") + + # 检查RenderPipeline是否可用 + if not hasattr(self.world, 'render_pipeline') or not self.world.render_pipeline: + print("❌ RenderPipeline未初始化,无法使用高级渲染模式") + return False + + pipeline = self.world.render_pipeline + + # 计算RenderPipeline模式下的分辨率 + pipeline_width = int(self.base_eye_width * self.pipeline_resolution_scale) + pipeline_height = int(self.base_eye_height * self.pipeline_resolution_scale) + + print(f" 实际分辨率: {pipeline_width}x{pipeline_height}") + print(f" 像素减少: {(1 - self.pipeline_resolution_scale**2) * 100:.1f}%") + + # 导入RenderPipeline相关模块 + try: + from RenderPipelineFile.rpcore.render_target import RenderTarget + except ImportError as e: + print(f"❌ 无法导入RenderPipeline模块: {e}") + return False + + # 导入VR stages模块 + try: + from core.vr.rendering.stages import VRPipelineController + except ImportError as e: + print(f"❌ 无法导入VR stages模块: {e}") + return False + + # 创建VR Pipeline控制器 + print(" 初始化VR Pipeline控制器...") + self.vr_pipeline_controller = VRPipelineController(pipeline) + + # 保存分辨率信息(将在_setup_vr_cameras中使用) + self.pipeline_vr_width = pipeline_width + self.pipeline_vr_height = pipeline_height + + # 应用RenderPipeline效果配置 + self._apply_pipeline_vr_effects() + + self.render_pipeline_enabled = True + print("✅ VR Pipeline控制器已初始化(将在相机设置时创建完整管线)") + + # 检查天空盒状态 + self._check_skybox_status() + + return True + + except Exception as e: + print(f"❌ 创建RenderPipeline VR缓冲区失败: {e}") + import traceback + traceback.print_exc() + return False + + def _apply_pipeline_vr_effects(self): + """为VR场景应用RenderPipeline效果配置""" + try: + print("🎨 应用RenderPipeline VR优化配置...") + + # 根据配置调整RenderPipeline设置 + config = self.pipeline_vr_config + + # 注意:实际的插件启用/禁用需要在pipeline.yaml中配置 + # 这里只是记录当前的VR优化意图 + print(f" 阴影: {'启用' if config['enable_shadows'] else '禁用'}") + print(f" 环境光遮蔽: {'启用' if config['enable_ao'] else '禁用'}") + print(f" 泛光效果: {'启用' if config['enable_bloom'] else '禁用'} (VR推荐禁用)") + print(f" 运动模糊: {'启用' if config['enable_motion_blur'] else '禁用'} (VR推荐禁用)") + print(f" 屏幕空间反射: {'启用' if config['enable_ssr'] else '禁用'} (VR推荐禁用)") + + print("✅ RenderPipeline VR效果配置完成") + + except Exception as e: + print(f"⚠️ 应用RenderPipeline VR效果失败: {e}") + + def _check_skybox_status(self): + """检查并报告天空盒状态""" + try: + print("\n🌌 检查天空盒状态...") + + # 方法1:在render节点下搜索所有名字包含skybox的节点 + skybox_nodes = self.world.render.findAllMatches("**/skybox*") + if skybox_nodes and skybox_nodes.getNumPaths() > 0: + print(f" ✓ 找到 {skybox_nodes.getNumPaths()} 个天空盒节点(使用名称搜索)") + for i in range(skybox_nodes.getNumPaths()): + skybox = skybox_nodes.getPath(i) + print(f" 天空盒 #{i+1}:") + print(f" 名称: {skybox.getName()}") + print(f" 位置: {skybox.getPos()}") + print(f" 缩放: {skybox.getScale()}") + print(f" 父节点: {skybox.getParent().getName()}") + + # 检查是否有shader + if skybox.hasShader(): + print(f" ✓ 已应用shader效果") + else: + print(f" ⚠️ 未应用shader效果") + + # 检查bin设置 + if skybox.hasBin(): + print(f" Bin: {skybox.getBinName()}") + else: + print(" ⚠️ 未找到天空盒(名称搜索)") + + # 方法2:搜索所有GeomNode(几何节点),天空盒通常是一个大的几何体 + print(" 搜索所有几何节点...") + all_geoms = self.world.render.findAllMatches("**/+GeomNode") + print(f" 找到 {all_geoms.getNumPaths()} 个几何节点") + + # 查找大型几何体(缩放值很大,可能是天空盒) + skybox_candidates = [] + for i in range(all_geoms.getNumPaths()): + geom_node = all_geoms.getPath(i) + scale = geom_node.getScale() + # 天空盒通常缩放很大(如40000) + if scale[0] > 1000 or scale[1] > 1000 or scale[2] > 1000: + print(f" ✓ 找到大型几何体(可能是天空盒):") + print(f" 名称: {geom_node.getName()}") + print(f" 缩放: {scale}") + print(f" 位置: {geom_node.getPos()}") + if geom_node.hasShader(): + print(f" ✓ 已应用shader") + skybox_candidates.append(geom_node) + + # 方法3:检查render节点的所有直接子节点(显示全部) + print(" 检查render的所有直接子节点...") + render_children = self.world.render.getChildren() + print(f" render有 {render_children.getNumPaths()} 个直接子节点:") + for i in range(render_children.getNumPaths()): + child = render_children.getPath(i) + scale = child.getScale() + # 标记大型节点(可能是天空盒) + if scale[0] > 1000 or scale[1] > 1000 or scale[2] > 1000: + print(f" - {child.getName()} ⭐ (大型节点,缩放: {scale})") + else: + print(f" - {child.getName()}") + + # 报告结果但不自动创建 + if len(skybox_candidates) == 0: + print(" ⚠️ 未找到明显的天空盒(缩放>1000的几何体)") + print(" 💡 RenderPipeline的天空盒可能在上述列表中,但缩放值不同") + print(" 💡 或者可能RenderPipeline未在VR模式下自动创建天空盒") + else: + print(f" ✓ 找到 {len(skybox_candidates)} 个可能的天空盒") + + print("🌌 天空盒状态检查完成\n") + + except Exception as e: + print(f"⚠️ 天空盒状态检查失败: {e}") + import traceback + traceback.print_exc() + + def _create_vr_skybox(self): + """为VR创建天空盒""" + try: + print(" 🎨 创建VR天空盒...") + + # 检查RenderPipeline是否可用 + if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline: + pipeline = self.world.render_pipeline + + # 使用RenderPipeline的load_default_skybox方法 + print(" 使用RenderPipeline方法创建天空盒...") + skybox = pipeline.common_resources.load_default_skybox() + + if skybox: + # 设置天空盒属性 + skybox.set_scale(40000) # 大型缩放 + skybox.reparent_to(self.world.render) + skybox.set_bin("unsorted", 10000) # 最后渲染 + + # 应用天空盒shader效果 + pipeline.set_effect(skybox, "effects/skybox.yaml", { + "render_shadow": False, + "render_envmap": False, + "render_voxelize": False, + "alpha_testing": False, + "normal_mapping": False, + "parallax_mapping": False + }, 1000) + + print(f" ✅ VR天空盒已创建") + print(f" 名称: {skybox.getName()}") + print(f" 缩放: {skybox.getScale()}") + print(f" 位置: {skybox.getPos()}") + return skybox + else: + print(" ❌ 无法加载天空盒模型") + return None + else: + print(" ❌ RenderPipeline未初始化,无法创建天空盒") + return None + + except Exception as e: + print(f" ❌ 创建VR天空盒失败: {e}") + import traceback + traceback.print_exc() + return None + + def _setup_vr_cameras(self): + """设置VR相机 - 使用锚点层级系统""" + try: + # 创建VR追踪空间锚点层级 + self.tracking_space = self.world.render.attachNewNode('tracking-space') + self.hmd_anchor = self.tracking_space.attachNewNode('hmd-anchor') + self.left_eye_anchor = self.hmd_anchor.attachNewNode('left-eye') + self.right_eye_anchor = self.hmd_anchor.attachNewNode('right-eye') + + # 获取投影矩阵 + projection_left = self.coord_mat_inv * self.convert_mat( + self.vr_system.getProjectionMatrix(openvr.Eye_Left, self.near_clip, self.far_clip)) + projection_right = self.coord_mat_inv * self.convert_mat( + self.vr_system.getProjectionMatrix(openvr.Eye_Right, self.near_clip, self.far_clip)) + + # 创建左眼相机节点 + left_cam_node = Camera('left-cam') + left_lens = MatrixLens() + left_lens.setUserMat(projection_left) + left_cam_node.setLens(left_lens) + + # 创建右眼相机节点 + right_cam_node = Camera('right-cam') + right_lens = MatrixLens() + right_lens.setUserMat(projection_right) + right_cam_node.setLens(right_lens) + + # 附加相机到眼睛锚点 + self.vr_left_camera = self.left_eye_anchor.attachNewNode(left_cam_node) + self.vr_right_camera = self.right_eye_anchor.attachNewNode(right_cam_node) + + # 设置显示区域 - 区分RenderPipeline和普通模式 + if self.vr_render_mode == VRRenderMode.RENDER_PIPELINE and self.vr_pipeline_controller: + # RenderPipeline模式:使用VRPipelineController创建完整管线 + print(" 使用VR Pipeline创建立体渲染管线...") + + # 创建立体渲染管线(GBuffer + Lighting + Final stages) + success = self.vr_pipeline_controller.create_stereo_pipeline( + self.pipeline_vr_width, + self.pipeline_vr_height, + self.vr_left_camera, + self.vr_right_camera + ) + + if not success: + print("❌ VR Pipeline创建失败") + return False + + # 获取GBuffer的内部buffer(用于DisplayRegion) + self.vr_left_eye_buffer = self.vr_pipeline_controller.get_left_buffer() + self.vr_right_eye_buffer = self.vr_pipeline_controller.get_right_buffer() + + # 🔧 关键修复:验证buffer有效性 + if not self.vr_left_eye_buffer or not self.vr_right_eye_buffer: + print("❌ VR Pipeline buffer创建失败") + print(f" 左眼buffer: {self.vr_left_eye_buffer}") + print(f" 右眼buffer: {self.vr_right_eye_buffer}") + return False + + # 获取最终输出纹理(用于提交到OpenVR) + left_textures = self.vr_pipeline_controller.get_left_textures() + right_textures = self.vr_pipeline_controller.get_right_textures() + + if left_textures and right_textures: + self.vr_left_texture = left_textures["final"] + self.vr_right_texture = right_textures["final"] + else: + print("❌ 无法获取VR Pipeline输出纹理") + return False + + # 设置缓冲区排序和清除颜色 + if self.vr_left_eye_buffer: + self.vr_left_eye_buffer.setSort(-100) + self.vr_left_eye_buffer.setClearColorActive(True) + self.vr_left_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) + + if self.vr_right_eye_buffer: + self.vr_right_eye_buffer.setSort(-99) + self.vr_right_eye_buffer.setClearColorActive(True) + self.vr_right_eye_buffer.setClearColor((0.1, 0.2, 0.4, 1)) + + # 🔧 关键修复:为RenderPipeline的GBuffer DisplayRegion设置DrawCallback + # 这确保纹理在渲染完成后被提交到OpenVR + if self.vr_left_eye_buffer and self.vr_right_eye_buffer: + print(" 设置RenderPipeline DisplayRegion回调...") + try: + # 🔧 验证DisplayRegion数量 + left_dr_count = self.vr_left_eye_buffer.getNumDisplayRegions() + right_dr_count = self.vr_right_eye_buffer.getNumDisplayRegions() + + print(f" 左眼buffer DisplayRegion数量: {left_dr_count}") + print(f" 右眼buffer DisplayRegion数量: {right_dr_count}") + + if left_dr_count == 0 or right_dr_count == 0: + print(" ❌ DisplayRegion未创建,无法设置回调") + return False + + # 获取RenderTarget创建的DisplayRegion + left_dr = self.vr_left_eye_buffer.get_display_region(0) + right_dr = self.vr_right_eye_buffer.get_display_region(0) + + # 验证DisplayRegion有效性 + if not left_dr or not right_dr: + print(" ❌ DisplayRegion无效") + return False + + # 设置渲染回调(与普通模式一致) + left_dr.setDrawCallback(PythonCallbackObject(self.simple_left_cb)) + right_dr.setDrawCallback(PythonCallbackObject(self.simple_right_cb)) + + # 确保DisplayRegion处于活动状态 + left_dr.setActive(True) + right_dr.setActive(True) + + print(" ✅ RenderPipeline DisplayRegion回调已设置") + except Exception as e: + print(f" ⚠️ 设置DisplayRegion回调失败: {e}") + import traceback + traceback.print_exc() + return False + + # 🔧 关键修复:强制同步GraphicsEngine,确保所有buffer完全初始化 + print(" 同步GraphicsEngine...") + self.world.graphicsEngine.renderFrame() + + # 准备纹理并缓存OpenGL ID + if not self._prepare_and_cache_textures(): + print("❌ RenderPipeline纹理准备失败") + return False + + print("✅ VR Pipeline立体渲染管线已准备完成") + else: + # 普通模式:创建新的DisplayRegion + print(" 使用普通渲染区域...") + left_dr = self.vr_left_eye_buffer.makeDisplayRegion() + left_dr.setCamera(self.vr_left_camera) + left_dr.setActive(True) + left_dr.setDrawCallback(PythonCallbackObject(self.simple_left_cb)) + + right_dr = self.vr_right_eye_buffer.makeDisplayRegion() + right_dr.setCamera(self.vr_right_camera) + right_dr.setActive(True) + right_dr.setDrawCallback(PythonCallbackObject(self.simple_right_cb)) + + print("✓ VR相机锚点层级系统设置完成") + return True + + except Exception as e: + print(f"❌ 设置VR相机失败: {e}") + import traceback + traceback.print_exc() + return False + + def _get_eye_offset(self, eye): + """获取眼睛相对于头显的偏移""" + try: + if not self.vr_system: + # 使用标准IPD(瞳距)估算值 + ipd = 0.064 # 64mm,平均IPD + if eye == openvr.Eye_Left: + return Vec3(-ipd/2, 0, 0) + else: + return Vec3(ipd/2, 0, 0) + + # 从OpenVR获取眼睛到头显的变换矩阵 + eye_transform = self.vr_system.getEyeToHeadTransform(eye) + + # 提取位移信息 + x = eye_transform[0][3] + y = eye_transform[1][3] + z = eye_transform[2][3] + + return Vec3(x, y, z) + + except Exception as e: + print(f"❌ 获取眼睛偏移失败: {e}") + # 返回默认值 + ipd = 0.064 + if eye == openvr.Eye_Left: + return Vec3(-ipd/2, 0, 0) + else: + return Vec3(ipd/2, 0, 0) + + def _optimize_vr_rendering(self): + """优化VR模式下的渲染管线 - 减少不必要的后处理开销""" + try: + print("🔧 正在优化VR渲染管线...") + + # 检查是否有RenderPipeline + if hasattr(self.world, 'render_pipeline') and self.world.render_pipeline: + print(" 检测到RenderPipeline,正在优化...") + + # 对VR缓冲区应用轻量级渲染设置 + if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: + self._apply_lightweight_rendering(self.vr_left_eye_buffer, "左眼") + + if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: + self._apply_lightweight_rendering(self.vr_right_eye_buffer, "右眼") + + print("✅ VR渲染管线优化完成") + else: + print(" 未检测到RenderPipeline,使用默认Panda3D渲染") + + # 禁用VR缓冲区的不必要功能 + self._disable_vr_buffer_extras() + + except Exception as e: + print(f"⚠️ VR渲染优化失败: {e}") + import traceback + traceback.print_exc() + + def _apply_lightweight_rendering(self, buffer, eye_name): + """为VR缓冲区应用轻量级渲染设置""" + try: + # 禁用多重采样抗锯齿(MSAA)以提升性能 + if hasattr(buffer, 'setMultisample'): + buffer.setMultisample(0) + print(f" {eye_name}: 禁用MSAA") + + # 设置更简单的清除颜色操作 + buffer.setClearColorActive(True) + buffer.setClearColor((0.1, 0.2, 0.4, 1.0)) # 深蓝色,便于调试 + + # 禁用深度写入到某些不需要的缓冲区 + # (保留主要深度测试,但减少不必要的写入) + + print(f" {eye_name}: 应用轻量级渲染设置") + + except Exception as e: + print(f"⚠️ {eye_name}缓冲区优化失败: {e}") + + def _disable_vr_buffer_extras(self): + """禁用VR缓冲区的额外功能以提升性能""" + try: + # 禁用VR缓冲区的统计收集 + if hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer: + if hasattr(self.vr_left_eye_buffer, 'setOneShot'): + self.vr_left_eye_buffer.setOneShot(False) + + if hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer: + if hasattr(self.vr_right_eye_buffer, 'setOneShot'): + self.vr_right_eye_buffer.setOneShot(False) + + print(" VR缓冲区额外功能已优化") + + except Exception as e: + print(f"⚠️ VR缓冲区额外功能优化失败: {e}") + + def _start_vr_task(self): + """启动VR更新任务""" + if self.vr_task: + self.world.taskMgr.remove(self.vr_task) + + # 设置高优先级(sort=-1000),确保在渲染之前执行,参考 panda3d-openvr + self.vr_task = self.world.taskMgr.add(self._update_vr, "update_vr", sort=-1000) + print("✓ VR更新任务已启动(高优先级,sort=-1000)") + + def _update_vr(self, task): + """VR更新任务 - 每帧调用(参考 panda3d-openvr 架构)""" + if not self.vr_enabled or not self.vr_system: + return task.cont + + try: + # 📌 第一件事:waitGetPoses 阻塞等待 VSync(参考 panda3d-openvr) + # 这会阻塞整个任务,确保每个 VSync 周期只执行一次 + should_call_waitgetposes = ( + not self.vr_test_mode or # 普通VR模式总是需要 + self.test_mode_wait_poses # 测试模式:仅当启用姿态等待时 + ) + + if should_call_waitgetposes: + self._wait_get_poses_immediate() + # 立即更新手柄,使用最新姿态(避免过时数据) + self.update_tracked_devices() + + # 性能监控 + self.frame_count += 1 + + # 🚀 自动启用性能模式 - 减少计时对象创建 + if not self.performance_mode_enabled and self.frame_count >= self.performance_mode_trigger_frame: + self.performance_mode_enabled = True + if self.frame_count <= self.performance_mode_trigger_frame + 5: # 只输出一次 + print(f"🎯 性能模式已启用 (帧#{self.frame_count}) - 禁用详细监控以提升性能") + + # 记录帧时间 + self._track_frame_time() + + # 计算VR FPS + import time + current_time = time.time() + if self.last_fps_time == 0: + self.last_fps_time = current_time + elif current_time - self.last_fps_time >= 1.0: # 每秒更新一次FPS + self.vr_fps = (self.frame_count - self.last_fps_check) / (current_time - self.last_fps_time) + self.last_fps_check = self.frame_count + self.last_fps_time = current_time + + # 📌 使用刚获取的姿态更新相机(而不是旧姿态) + self._update_camera_poses() + + # 输出策略信息(仅第一次) + if not hasattr(self, '_new_architecture_logged'): + print("✅ 新架构已启用 - waitGetPoses 在任务开始阻塞,参考 panda3d-openvr") + print(" 这确保每个 VSync 周期只渲染一次,解决 AlreadySubmitted 错误") + self._new_architecture_logged = True + + # 更新VR动作状态 - 仅在启用时更新 + if not self.disable_action_system and self.action_manager: + self.action_manager.update_actions() + + # 更新VR交互系统 + if self.interaction_manager: + self.interaction_manager.update() + + # 更新VR摇杆系统 + if self.joystick_manager: + # 计算帧间隔时间 + import time + if not hasattr(self, '_last_frame_time'): + self._last_frame_time = time.time() + dt = 0.016 # 默认60fps + else: + current_time = time.time() + dt = current_time - self._last_frame_time + self._last_frame_time = current_time + # 限制dt范围,避免异常情况 + dt = max(0.001, min(0.1, dt)) + + self.joystick_manager.update(dt) + + # 更新系统性能指标(减少频率) + if self.frame_count % 30 == 1: # 每30帧更新一次,减少开销 + self._update_performance_metrics() + + # 更新GPU渲染时间统计(减少频率) + if self.enable_gpu_timing and self.frame_count % 60 == 1: + self._get_gpu_frame_timing() + + # 🚀 手动垃圾回收控制 - 避免16-19帧周期性峰值 + self._manual_gc_control() + + # 定期输出性能报告 - 默认10秒间隔 + report_interval = getattr(self, 'performance_report_interval', 600) + if self.frame_count % report_interval == 1: + self._print_performance_report() + + except Exception as e: + print(f"VR更新错误: {e}") + import traceback + traceback.print_exc() + + return task.cont + + def _sync_gpu_if_needed(self): + """可选的GPU同步 - 仅在需要时使用""" + try: + # 同步等待GPU完成(如果需要) + gsg = self.world.win.getGsg() + if gsg: + gsg.getEngine().syncFrame() + except Exception as e: + print(f"❌ GPU同步异常: {e}") + + def simple_left_cb(self, cbdata): + """简化的左眼渲染回调 - 精确控制渲染和提交""" + try: + # 🔍 精确测量渲染时间 + import time + render_start = time.perf_counter() + + # 触发实际渲染 + cbdata.upcall() + + # 计算真实渲染时间 + self.left_render_time = (time.perf_counter() - render_start) * 1000 # 转换为毫秒 + + # 记录渲染次数 + self.left_render_count += 1 + + # 📌 OpenVR 帧边界检查:防止同一 OpenVR 帧内重复渲染 + if self.left_eye_last_render_frame == self.openvr_frame_id: + return # 已在当前 OpenVR 帧渲染过,跳过 + + # 🔧 OpenVR最佳实践:左眼只渲染,不立即提交 + # 基于官方hellovr示例:两眼都渲染完后再批量提交 + self.left_eye_last_render_frame = self.openvr_frame_id + + # 渐进式VR功能测试:单独启用纹理提交时保留原逻辑 + should_submit = self.vr_test_mode and self.test_mode_submit_texture and not self.test_mode_wait_poses + + if should_submit: + # 测试模式:单独启用纹理提交时的兼容模式 + if self.vr_compositor and self.vr_left_texture: + self.submit_texture(openvr.Eye_Left, self.vr_left_texture) + + if self.vr_test_mode: + # 测试模式:始终触发屏幕显示更新 + self._update_test_display() + + except Exception as e: + print(f"左眼渲染回调错误: {e}") + + def simple_right_cb(self, cbdata): + """简化的右眼渲染回调 - 精确控制渲染和提交""" + try: + # 🔍 精确测量渲染时间 + import time + render_start = time.perf_counter() + + # 触发实际渲染 + cbdata.upcall() + + # 计算真实渲染时间 + self.right_render_time = (time.perf_counter() - render_start) * 1000 # 转换为毫秒 + + # 记录渲染次数 + self.right_render_count += 1 + + # 📌 OpenVR 帧边界检查:防止同一 OpenVR 帧内重复渲染 + if self.right_eye_last_render_frame == self.openvr_frame_id: + return # 已在当前 OpenVR 帧渲染过,跳过 + + # 🔧 OpenVR最佳实践:右眼渲染完成后批量提交两眼纹理 + # 基于官方hellovr示例:避免分散提交导致的VSync阻塞 + self.right_eye_last_render_frame = self.openvr_frame_id + + # 🔧 渐进式VR功能测试:根据调试标志决定启用哪些功能 + should_batch_submit = not self.vr_test_mode or self.test_mode_submit_texture + should_wait_poses = not self.vr_test_mode or self.test_mode_wait_poses + + # 特殊处理:测试模式单独启用功能时的兼容模式 + if self.vr_test_mode: + if self.test_mode_submit_texture and not self.test_mode_wait_poses: + # 单独测试纹理提交:使用原来的分散提交方式 + if self.vr_compositor and self.vr_right_texture: + self.submit_texture(openvr.Eye_Right, self.vr_right_texture) + + # 🚀 测试模式也需要PostPresentHandoff避免36FPS + try: + if hasattr(self.vr_compositor, 'postPresentHandoff'): + self.vr_compositor.postPresentHandoff() + elif hasattr(self.vr_compositor, 'PostPresentHandoff'): + self.vr_compositor.PostPresentHandoff() + except Exception as e: + pass # 测试模式静默忽略错误 + should_batch_submit = False + elif self.test_mode_wait_poses and not self.test_mode_submit_texture: + # 单独测试姿态等待:不提交纹理 + should_batch_submit = False + + if should_batch_submit: + # 🚀 OpenVR最佳实践:批量提交两眼纹理 + # 这是解决36FPS问题的关键修复 + self._batch_submit_textures() + + # 🔧 关键修复:移除Submit后立即WaitGetPoses的错误实现 + # 根据OpenVR官方文档:"Calling WaitGetPoses immediately after Submit is conspicuously wrong" + # WaitGetPoses应该在下一帧开始时通过update_vr_task调用,不是Submit后立即调用 + # + # if should_wait_poses: + # # 错误的实现:Submit后立即获取姿态导致36FPS + # self._wait_get_poses_immediate() # ← 这是36FPS的根本原因! + # + # 正确的实现:让update_vr_task在下一帧开始时调用WaitGetPoses + + if self.vr_test_mode: + # 测试模式:始终更新性能HUD + self._update_test_performance_hud() + + except Exception as e: + print(f"右眼渲染回调错误: {e}") + + def _wait_get_poses(self): + """调用VRCompositor的waitGetPoses来获取焦点和姿态数据""" + try: + if not self.vr_compositor or not self.poses: + return + + # 调用waitGetPoses获取焦点和姿态数据 + # 这个调用可能会阻塞直到下一个VR同步点 + result = self.vr_compositor.waitGetPoses(self.poses, None) + + # 检查姿态数据的有效性 + valid_poses = 0 + + # 更新HMD姿态(设备0通常是头显) + if len(self.poses) > 0 and self.poses[0].bPoseIsValid: + valid_poses += 1 + else: + # 如果HMD姿态无效,不要频繁输出错误信息 + if not hasattr(self, '_hmd_invalid_warning_shown'): + print("⚠️ HMD姿态数据无效") + self._hmd_invalid_warning_shown = True + + # 更新控制器姿态 + self.controller_poses.clear() + for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): + if self.poses[device_id].bPoseIsValid: + device_class = self.vr_system.getTrackedDeviceClass(device_id) + if device_class == openvr.TrackedDeviceClass_Controller: + controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking + self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) + valid_poses += 1 + + # 性能监控 - 偶尔输出姿态状态 + if self.frame_count % 600 == 1: # 每10秒输出一次@60fps + print(f"📊 VR姿态状态 - 有效姿态数: {valid_poses}, 总帧数: {self.frame_count}") + + except Exception as e: + # 限制错误输出频率 + if not hasattr(self, '_last_error_frame'): + self._last_error_frame = 0 + + if self.frame_count - self._last_error_frame > 300: # 每5秒最多输出一次错误 + print(f"waitGetPoses失败: {e}") + self._last_error_frame = self.frame_count + + # 记录姿态失败次数 + self.pose_failures += 1 + + def _wait_get_poses_immediate(self): + """立即获取VR姿态 - 修复ATW闪烁的关键方法(双姿态版本)""" + # 开始计时waitGetPoses操作 + timing = self._start_timing('wait_poses') + + try: + if not self.vr_compositor or not self.poses or not self.game_poses: + self._end_timing(timing) + return + + # 关键修复:传递渲染姿态和游戏姿态数组 + # 渲染姿态用于绘制,游戏姿态用于逻辑,避免ATW过度补偿 + result = self.vr_compositor.waitGetPoses(self.poses, self.game_poses) + + # 📌 waitGetPoses 成功后立即递增 OpenVR 帧ID(新帧开始) + self.openvr_frame_id += 1 + + # 结束计时 + wait_time = self._end_timing(timing) + + # 检查姿态数据的有效性 + valid_poses = 0 + + # 更新HMD姿态(设备0通常是头显) + if len(self.poses) > 0 and self.poses[0].bPoseIsValid: + valid_poses += 1 + else: + # 如果HMD姿态无效,不要频繁输出错误信息 + if not hasattr(self, '_hmd_invalid_warning_shown'): + print("⚠️ HMD姿态数据无效(立即模式)") + self._hmd_invalid_warning_shown = True + + # 🚀 优化控制器姿态更新:使用缓存,避免每帧clear()和重新创建对象 + for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): + if self.poses[device_id].bPoseIsValid: + device_class = self.vr_system.getTrackedDeviceClass(device_id) + if device_class == openvr.TrackedDeviceClass_Controller: + controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking + + # 检查是否已有此设备的缓存矩阵 + if device_id not in self.controller_poses: + # 第一次检测到该控制器,创建新的缓存矩阵 + self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) + else: + # 复用现有矩阵,只更新数值,避免创建新对象 + cached_mat = self.controller_poses[device_id] + self._update_matrix_from_openvr(cached_mat, controller_matrix) + + valid_poses += 1 + else: + # 设备姿态无效,从字典中移除(如果存在) + if device_id in self.controller_poses: + # 将矩阵返回对象池 + self._return_pooled_matrix(self.controller_poses[device_id]) + del self.controller_poses[device_id] + + # 🔧 关键修复:立即更新手柄和跟踪设备 + # Running Start模式下必须在WaitGetPoses后立即更新,避免手柄消失 + self.update_tracked_devices() + + # 调试信息 - 仅在第一次成功时输出 + if not hasattr(self, '_dual_pose_mode_logged') and valid_poses > 0: + print(f"✅ 双姿态立即获取模式启用 - 有效姿态数: {valid_poses}") + print(" 渲染姿态用于绘制,游戏姿态用于逻辑,防止ATW过度补偿") + print(" 手柄和跟踪设备在WaitGetPoses后立即更新") + self._dual_pose_mode_logged = True + + except Exception as e: + # 限制错误输出频率 + if not hasattr(self, '_last_immediate_error_frame'): + self._last_immediate_error_frame = 0 + + if self.frame_count - self._last_immediate_error_frame > 300: # 每5秒最多输出一次错误 + print(f"立即姿态获取失败: {e}") + self._last_immediate_error_frame = self.frame_count + + self.pose_failures += 1 + + def _wait_get_poses_with_prediction(self): + """使用预测时间获取VR姿态 - 优化性能的新策略""" + try: + if not self.vr_compositor or not self.poses or not self.game_poses: + return + + # 使用预测时间获取姿态 + # 预测时间通常为11-16ms,对应下一个VR帧的时间 + result = self.vr_compositor.waitGetPoses(self.poses, self.game_poses) + + # 检查姿态数据的有效性 + valid_poses = 0 + + # 更新HMD姿态(设备0通常是头显) + if len(self.poses) > 0 and self.poses[0].bPoseIsValid: + valid_poses += 1 + else: + # 如果HMD姿态无效,不要频繁输出错误信息 + if not hasattr(self, '_hmd_invalid_warning_task'): + print("⚠️ HMD姿态数据无效(更新任务模式)") + self._hmd_invalid_warning_task = True + + # 更新控制器姿态 + self.controller_poses.clear() + for device_id in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): + if self.poses[device_id].bPoseIsValid: + device_class = self.vr_system.getTrackedDeviceClass(device_id) + if device_class == openvr.TrackedDeviceClass_Controller: + controller_matrix = self.poses[device_id].mDeviceToAbsoluteTracking + self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) + valid_poses += 1 + + # 调试信息 - 仅在第一次成功时输出 + if not hasattr(self, '_update_task_mode_logged') and valid_poses > 0: + print(f"✅ 更新任务姿态获取模式启用 - 有效姿态数: {valid_poses}") + print(f" 预测时间: {self.use_prediction_time*1000:.1f}ms") + self._update_task_mode_logged = True + + except Exception as e: + # 限制错误输出频率 + if not hasattr(self, '_last_task_error_frame'): + self._last_task_error_frame = 0 + + if self.frame_count - self._last_task_error_frame > 300: # 每5秒最多输出一次错误 + print(f"更新任务姿态获取失败: {e}") + self._last_task_error_frame = self.frame_count + + # 记录姿态失败次数 + self.pose_failures += 1 + + def _cache_poses_for_next_frame(self): + """缓存当前姿态供下一帧渲染使用 - 修复时序不匹配""" + try: + if not self.poses or len(self.poses) == 0: + return + + # 如果是第一帧,直接使用当前姿态 + if self._first_frame: + self._cached_render_poses = self.poses + self._first_frame = False + print("✓ 首帧姿态缓存已设置") + return + + # 复制当前渲染姿态到缓存 + # 下一帧将使用这些姿态进行渲染 + poses_t = openvr.TrackedDevicePose_t * openvr.k_unMaxTrackedDeviceCount + cached_poses = poses_t() + + # 复制姿态数据 + for i in range(len(self.poses)): + cached_poses[i] = self.poses[i] + + self._cached_render_poses = cached_poses + + except Exception as e: + print(f"⚠️ 姿态缓存失败: {e}") + + + def _reset_waitgetposes_flag(self, task): + """重置WaitGetPoses标记 - 确保下一帧可以调用WaitGetPoses""" + self._waitgetposes_called_this_frame = False + return task.done + + def _update_tracking_data(self): + """更新VR追踪数据""" + try: + # 获取设备姿态 + poses = self.vr_system.getDeviceToAbsoluteTrackingPose( + openvr.TrackingUniverseStanding, 0.0, openvr.k_unMaxTrackedDeviceCount + ) + + # 更新HMD姿态(设备0通常是头显) + if poses[0].bPoseIsValid: + hmd_matrix = poses[0].mDeviceToAbsoluteTracking + self.hmd_pose = self._convert_openvr_matrix_to_panda(hmd_matrix) + + # 更新控制器姿态 + for device_id in range(1, openvr.k_unMaxTrackedDeviceCount): + if poses[device_id].bPoseIsValid: + device_class = self.vr_system.getTrackedDeviceClass(device_id) + if device_class == openvr.TrackedDeviceClass_Controller: + controller_matrix = poses[device_id].mDeviceToAbsoluteTracking + self.controller_poses[device_id] = self._convert_openvr_matrix_to_panda(controller_matrix) + + except Exception as e: + print(f"更新追踪数据失败: {e}") + + def _convert_openvr_matrix_to_panda(self, ovr_matrix): + """将OpenVR矩阵转换为Panda3D矩阵 - 使用对象池优化 + + 坐标系转换: + OpenVR: X右, Y上, -Z前(右手坐标系) + Panda3D: X右, Y前, Z上(右手坐标系) + + 转换规则: + OpenVR X → Panda3D X + OpenVR Y → Panda3D Z + OpenVR -Z → Panda3D Y + """ + # 🚀 使用对象池获取预分配的Mat4对象,避免每帧创建新对象 + mat = self._get_pooled_matrix() + + # 修正的坐标转换矩阵 + # OpenVR: X右, Y上, -Z前 → Panda3D: X右, Y前, Z上 + # 转换规则: (ovr_x, ovr_y, ovr_z) → (panda_x, panda_y, panda_z) + # (ovr_x, ovr_y, ovr_z) → (ovr_x, -ovr_z, ovr_y) + + # X轴行:Panda3D的X轴对应OpenVR的X轴 + mat.setCell(0, 0, ovr_matrix[0][0]) # X_x → X_x + mat.setCell(0, 1, ovr_matrix[0][1]) # X_y → X_y + mat.setCell(0, 2, ovr_matrix[0][2]) # X_z → X_z + mat.setCell(0, 3, ovr_matrix[0][3]) # 位移X分量 + + # Y轴行:Panda3D的Y轴对应OpenVR的-Z轴 + mat.setCell(1, 0, -ovr_matrix[2][0]) # -Z_x → Y_x + mat.setCell(1, 1, -ovr_matrix[2][1]) # -Z_y → Y_y + mat.setCell(1, 2, -ovr_matrix[2][2]) # -Z_z → Y_z + mat.setCell(1, 3, -ovr_matrix[2][3]) # 位移Y分量(-Z位移) + + # Z轴行:Panda3D的Z轴对应OpenVR的Y轴 + mat.setCell(2, 0, ovr_matrix[1][0]) # Y_x → Z_x + mat.setCell(2, 1, ovr_matrix[1][1]) # Y_y → Z_y + mat.setCell(2, 2, ovr_matrix[1][2]) # Y_z → Z_z + mat.setCell(2, 3, ovr_matrix[1][3]) # 位移Z分量(Y位移) + + # 齐次坐标 + mat.setCell(3, 0, 0) + mat.setCell(3, 1, 0) + mat.setCell(3, 2, 0) + mat.setCell(3, 3, 1) + + # 🚀 优化调试信息 - 避免创建Vec3对象,减少GC压力 + if not hasattr(self, '_coord_debug_counter'): + self._coord_debug_counter = 0 + self._coord_debug_counter += 1 + + if self._coord_debug_counter % 600 == 1: # 每10秒输出一次@60fps + print(f"🔄 坐标系转换调试 (第{self._coord_debug_counter}帧)") + + # 直接输出数值,避免创建Vec3对象 + ovr_x, ovr_y, ovr_z = ovr_matrix[0][3], ovr_matrix[1][3], ovr_matrix[2][3] + print(f" OpenVR原始位置: ({ovr_x:.3f}, {ovr_y:.3f}, {ovr_z:.3f})") + + # 直接从矩阵读取数值,避免创建Vec3对象 + panda_x = mat.getCell(0, 3) + panda_y = mat.getCell(1, 3) + panda_z = mat.getCell(2, 3) + print(f" Panda3D转换位置: ({panda_x:.3f}, {panda_y:.3f}, {panda_z:.3f})") + + # 手动验证转换:OpenVR (x,y,z) → Panda3D (x,-z,y) + expected_x = ovr_x + expected_y = -ovr_z + expected_z = ovr_y + print(f" 预期转换结果: ({expected_x:.3f}, {expected_y:.3f}, {expected_z:.3f})") + + # 计算误差,避免创建Vec3对象 + diff_x = panda_x - expected_x + diff_y = panda_y - expected_y + diff_z = panda_z - expected_z + diff_magnitude = (diff_x*diff_x + diff_y*diff_y + diff_z*diff_z)**0.5 + + if diff_magnitude < 0.001: + print(f" ✅ 坐标转换正确 (误差: {diff_magnitude:.6f})") + else: + print(f" ⚠️ 坐标转换可能有误 (误差: {diff_magnitude:.6f})") + print(f" 差异: ({diff_x:.6f}, {diff_y:.6f}, {diff_z:.6f})") + + return mat + + def update_hmd(self, pose): + """ + 更新HMD锚点 - 基于参考实现 + """ + try: + # 将OpenVR姿态转换为Panda3D矩阵 + modelview = self.convert_mat(pose.mDeviceToAbsoluteTracking) + + # 应用坐标系转换并设置HMD锚点 + self.hmd_anchor.setMat(self.coord_mat_inv * modelview * self.coord_mat) + + # 获取眼睛到头部的变换 + view_left = self.convert_mat(self.vr_system.getEyeToHeadTransform(openvr.Eye_Left)) + view_right = self.convert_mat(self.vr_system.getEyeToHeadTransform(openvr.Eye_Right)) + + # 设置眼睛锚点 + self.left_eye_anchor.setMat(self.coord_mat_inv * view_left * self.coord_mat) + self.right_eye_anchor.setMat(self.coord_mat_inv * view_right * self.coord_mat) + + except Exception as e: + print(f"更新HMD姿态失败: {e}") + + def _update_camera_poses(self): + """更新相机姿态 - 使用锚点系统简化处理""" + try: + # 使用锚点系统后,相机位置自动跟随锚点 + # 只需要获取HMD姿态并更新锚点即可 + + # 从poses数组中获取HMD姿态 + if hasattr(self, 'poses') and len(self.poses) > 0: + hmd_pose = self.poses[openvr.k_unTrackedDeviceIndex_Hmd] + if hmd_pose.bPoseIsValid: + self.update_hmd(hmd_pose) + else: + print("⚠️ HMD姿态数据无效") + + except Exception as e: + print(f"更新相机姿态失败: {e}") + import traceback + traceback.print_exc() + + def _update_camera_poses_with_cache(self): + """使用缓存姿态更新相机 - 符合OpenVR时序假设""" + try: + # 使用缓存的姿态,符合OpenVR的时序假设 + # OpenVR假设你用上一次WaitGetPoses的姿态渲染当前提交的帧 + + if not self._cached_render_poses: + # 如果没有缓存姿态,回退到当前姿态(首帧情况) + print("⚠️ 没有缓存姿态,使用当前姿态") + return self._update_camera_poses() + + # 从缓存姿态数组中获取HMD姿态 + if len(self._cached_render_poses) > 0: + hmd_pose = self._cached_render_poses[openvr.k_unTrackedDeviceIndex_Hmd] + if hmd_pose.bPoseIsValid: + self.update_hmd(hmd_pose) + + # 调试信息 - 验证缓存姿态使用 + if not hasattr(self, '_cached_pose_logged'): + print("✅ 使用缓存姿态更新相机 - 符合OpenVR时序假设") + self._cached_pose_logged = True + else: + print("⚠️ 缓存的HMD姿态数据无效") + + except Exception as e: + print(f"使用缓存姿态更新相机失败: {e}") + # 回退到正常更新方式 + self._update_camera_poses() + import traceback + traceback.print_exc() + + + def enable_vr(self): + """启用VR模式""" + if not self.is_vr_available(): + print("❌ VR系统不可用") + return False + + if not self.vr_initialized: + if not self.initialize_vr(): + return False + + self.vr_enabled = True + + # 禁用主相机避免干扰VR渲染 + self._disable_main_cam() + + # VR性能优化:使用Running Start模式(Valve最佳实践) + print("🚀 VR性能优化:Running Start模式已启用") + print(" 优势:在帧开始时获取姿态,提供VSync前3ms的渲染时间") + print(" 注意:Submit后立即调用WaitGetPoses是错误实现") + self.set_prediction_time(11.0) # 11ms预测时间 - OpenVR标准值,平衡准确性和延迟 + + # 🚀 动态调整Qt Timer频率以支持VR + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + if hasattr(self.world.qtWidget, 'synchronizer'): + # 设置为144Hz,让OpenVR控制实际渲染节奏 + self.world.qtWidget.synchronizer.setInterval(int(1000/144)) + print("✓ Qt Timer调整为144Hz,让OpenVR控制VR渲染节奏") + + # 🔧 关键修复:检测并重建缺失的手柄visualizer + # 当渲染模式切换时,visualizer可能被清理但控制器对象仍存在 + if hasattr(self, 'left_controller') and self.left_controller: + if not self.left_controller.visualizer and self.left_controller.anchor_node: + print("🔧 检测到左手柄visualizer缺失,正在重建...") + self.left_controller._create_visualizer() + if self.left_controller.visualizer: + print("✅ 左手柄visualizer已重建") + + if hasattr(self, 'right_controller') and self.right_controller: + if not self.right_controller.visualizer and self.right_controller.anchor_node: + print("🔧 检测到右手柄visualizer缺失,正在重建...") + self.right_controller._create_visualizer() + if self.right_controller.visualizer: + print("✅ 右手柄visualizer已重建") + + print("✅ VR模式已启用") + return True + + def disable_vr(self): + """禁用VR模式""" + self.vr_enabled = False + + # 清理手柄可视化(但保留控制器对象以便重新启用) + if hasattr(self, 'left_controller') and self.left_controller: + if hasattr(self.left_controller, 'visualizer') and self.left_controller.visualizer: + try: + self.left_controller.visualizer.cleanup() + self.left_controller.visualizer = None + print("✓ 左手控制器可视化已清理") + except Exception as e: + print(f"⚠️ 清理左手控制器可视化失败: {e}") + + if hasattr(self, 'right_controller') and self.right_controller: + if hasattr(self.right_controller, 'visualizer') and self.right_controller.visualizer: + try: + self.right_controller.visualizer.cleanup() + self.right_controller.visualizer = None + print("✓ 右手控制器可视化已清理") + except Exception as e: + print(f"⚠️ 清理右手控制器可视化失败: {e}") + + # 隐藏手柄锚点节点 + if hasattr(self, 'left_controller') and self.left_controller and self.left_controller.anchor_node: + self.left_controller.anchor_node.hide() + + if hasattr(self, 'right_controller') and self.right_controller and self.right_controller.anchor_node: + self.right_controller.anchor_node.hide() + + # 恢复主相机 + self._enable_main_cam() + + # 恢复Qt Timer到60FPS + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + if hasattr(self.world.qtWidget, 'synchronizer'): + self.world.qtWidget.synchronizer.setInterval(int(1000/60)) + print("✓ Qt Timer恢复为60Hz") + + print("✅ VR模式已禁用,手柄模型已隐藏") + + def set_vr_render_mode(self, mode): + """切换VR渲染模式 + + Args: + mode: VRRenderMode枚举值或字符串 ('normal' 或 'render_pipeline') + + Returns: + bool: 切换是否成功 + """ + try: + # 转换输入为枚举类型 + if isinstance(mode, str): + mode_str = mode.lower() + if mode_str == "normal": + new_mode = VRRenderMode.NORMAL + elif mode_str in ["render_pipeline", "renderpipeline", "pipeline"]: + new_mode = VRRenderMode.RENDER_PIPELINE + else: + print(f"❌ 无效的渲染模式: {mode}") + print(" 支持的模式: 'normal' 或 'render_pipeline'") + return False + elif isinstance(mode, VRRenderMode): + new_mode = mode + else: + print(f"❌ 无效的模式类型: {type(mode)}") + return False + + # 检查是否与当前模式相同 + if new_mode == self.vr_render_mode: + print(f"✓ VR渲染模式已经是 {new_mode.value},无需切换") + return True + + print(f"🔄 正在切换VR渲染模式: {self.vr_render_mode.value} → {new_mode.value}") + + # 检查VR是否已初始化 + if not self.vr_initialized: + print("⚠️ VR未初始化,仅更新渲染模式配置") + self.vr_render_mode = new_mode + print(f"✓ VR渲染模式已更新为 {new_mode.value}(下次启动VR时生效)") + return True + + # 保存当前VR启用状态 + was_enabled = self.vr_enabled + + # 如果VR已启用,先禁用 + if was_enabled: + print(" 暂时禁用VR...") + self.disable_vr() + + # 更新渲染模式 + old_mode = self.vr_render_mode + self.vr_render_mode = new_mode + + # 清理现有缓冲区 + print(" 清理现有渲染缓冲区...") + self._cleanup_vr_buffers() + + # 根据新模式重建缓冲区 + print(f" 创建新的渲染缓冲区({new_mode.value})...") + success = False + + if new_mode == VRRenderMode.RENDER_PIPELINE: + success = self._create_vr_buffers_with_pipeline() + if not success: + print("⚠️ RenderPipeline模式创建失败,回退到普通渲染模式") + self.vr_render_mode = VRRenderMode.NORMAL + success = self._create_vr_buffers() + else: + success = self._create_vr_buffers() + + if not success: + print("❌ 缓冲区创建失败,尝试恢复原模式") + self.vr_render_mode = old_mode + if old_mode == VRRenderMode.RENDER_PIPELINE: + self._create_vr_buffers_with_pipeline() + else: + self._create_vr_buffers() + return False + + # 重新设置相机 + print(" 重新设置VR相机...") + if not self._setup_vr_cameras(): + print("❌ 相机设置失败") + return False + + # 如果之前VR是启用的,重新启用 + if was_enabled: + print(" 重新启用VR...") + self.enable_vr() + + # 🔧 关键修复:重建所有手柄的visualizer以适配新渲染模式 + print(" 刷新手柄visualizer以适配新渲染模式...") + if hasattr(self, 'left_controller') and self.left_controller: + self.left_controller.recreate_visualizer() + + if hasattr(self, 'right_controller') and self.right_controller: + self.right_controller.recreate_visualizer() + + print(f"✅ VR渲染模式已切换为 {self.vr_render_mode.value}") + + # 保存配置 + if self.config_manager: + self.config_manager.save_from_vr_manager(self) + + return True + + except Exception as e: + print(f"❌ 切换VR渲染模式失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_vr_render_mode(self): + """获取当前VR渲染模式 + + Returns: + VRRenderMode: 当前渲染模式 + """ + return self.vr_render_mode + + def _cleanup_vr_buffers(self): + """清理VR渲染缓冲区""" + try: + # 清理VR Pipeline Controller(如果使用RenderPipeline模式) + if self.vr_pipeline_controller: + print(" 清理VR Pipeline...") + self.vr_pipeline_controller.cleanup_all() + self.vr_pipeline_controller = None + + # 清理左眼缓冲区 + if self.vr_left_eye_buffer: + # 如果是RenderPipeline模式,buffer已被VRPipelineController清理 + # 如果是普通模式,需要手动清理 + if self.vr_render_mode == VRRenderMode.NORMAL: + self.world.graphicsEngine.removeWindow(self.vr_left_eye_buffer) + self.vr_left_eye_buffer = None + + # 清理右眼缓冲区 + if self.vr_right_eye_buffer: + if self.vr_render_mode == VRRenderMode.NORMAL: + self.world.graphicsEngine.removeWindow(self.vr_right_eye_buffer) + self.vr_right_eye_buffer = None + + # 清理纹理 + self.vr_left_texture = None + self.vr_right_texture = None + self.left_texture_id = None + self.right_texture_id = None + self.textures_prepared = False + + # 清理RenderPipeline渲染目标(旧版,保留兼容性) + if self.vr_pipeline_left_target: + self.vr_pipeline_left_target = None + + if self.vr_pipeline_right_target: + self.vr_pipeline_right_target = None + + self.render_pipeline_enabled = False + + print("✓ VR渲染缓冲区已清理") + + except Exception as e: + print(f"⚠️ 清理VR缓冲区时出错: {e}") + + def cleanup(self): + """清理VR资源""" + try: + print("🔄 正在清理VR资源...") + + # 停止VR任务 + if self.vr_task: + self.world.taskMgr.remove(self.vr_task) + self.vr_task = None + + # 🚀 恢复Python垃圾回收并清理对象池 + if self._gc_disabled: + # 最后一次手动垃圾回收 + collected = gc.collect() + print(f"🗑️ 最终GC清理: {collected} 个对象") + + # 恢复自动垃圾回收 + gc.enable() + self._gc_disabled = False + print("✅ Python垃圾回收已恢复为自动模式") + + # 清理对象池 + if hasattr(self, '_matrix_pool'): + pool_size = len(self._matrix_pool) + self._matrix_pool.clear() + print(f"🧹 Mat4对象池已清理: {pool_size} 个对象") + + # 清理缓存 + if hasattr(self, '_cached_matrices'): + self._cached_matrices.clear() + if hasattr(self, '_controller_poses_cache'): + self._controller_poses_cache.clear() + + # 清理OpenVR Texture对象缓存 + if hasattr(self, '_left_ovr_texture'): + self._left_ovr_texture = None + if hasattr(self, '_right_ovr_texture'): + self._right_ovr_texture = None + print("🧹 OpenVR Texture对象缓存已清理") + + # 清理渲染缓冲区 + if self.vr_left_eye_buffer: + self.vr_left_eye_buffer.removeAllDisplayRegions() + self.world.graphicsEngine.removeWindow(self.vr_left_eye_buffer) + self.vr_left_eye_buffer = None + + if self.vr_right_eye_buffer: + self.vr_right_eye_buffer.removeAllDisplayRegions() + self.world.graphicsEngine.removeWindow(self.vr_right_eye_buffer) + self.vr_right_eye_buffer = None + + # 清理相机 + if self.vr_left_camera: + self.vr_left_camera.removeNode() + self.vr_left_camera = None + + if self.vr_right_camera: + self.vr_right_camera.removeNode() + self.vr_right_camera = None + + # 清理控制器 + if hasattr(self, 'left_controller') and self.left_controller: + try: + self.left_controller.cleanup() + self.left_controller = None + print("✓ 左手控制器已清理") + except Exception as e: + print(f"⚠️ 清理左手控制器失败: {e}") + + if hasattr(self, 'right_controller') and self.right_controller: + try: + self.right_controller.cleanup() + self.right_controller = None + print("✓ 右手控制器已清理") + except Exception as e: + print(f"⚠️ 清理右手控制器失败: {e}") + + # 清理控制器字典 + if hasattr(self, 'controllers'): + self.controllers.clear() + print("✓ 控制器字典已清理") + + # 清理VR子系统 + if hasattr(self, 'joystick_manager') and self.joystick_manager: + try: + self.joystick_manager.cleanup() + self.joystick_manager = None + print("✓ VR摇杆系统已清理") + except Exception as e: + print(f"⚠️ 清理VR摇杆系统失败: {e}") + + if hasattr(self, 'teleport_system') and self.teleport_system: + try: + self.teleport_system.cleanup() + self.teleport_system = None + print("✓ VR传送系统已清理") + except Exception as e: + print(f"⚠️ 清理VR传送系统失败: {e}") + + if hasattr(self, 'interaction_manager') and self.interaction_manager: + try: + self.interaction_manager.cleanup() + self.interaction_manager = None + print("✓ VR交互系统已清理") + except Exception as e: + print(f"⚠️ 清理VR交互系统失败: {e}") + + if hasattr(self, 'performance_monitor') and self.performance_monitor: + try: + # 性能监控系统不需要特殊清理,只需要置空引用 + self.performance_monitor = None + print("✓ VR性能监控系统已清理") + except Exception as e: + print(f"⚠️ 清理VR性能监控系统失败: {e}") + + if hasattr(self, 'action_manager') and self.action_manager: + try: + self.action_manager.cleanup() + self.action_manager = None + print("✓ VR动作系统已清理") + except Exception as e: + print(f"⚠️ 清理VR动作系统失败: {e}") + + # 关闭OpenVR + if self.vr_system and OPENVR_AVAILABLE: + try: + openvr.shutdown() + except: + pass + self.vr_system = None + + self.vr_enabled = False + self.vr_initialized = False + + print("✅ VR资源清理完成") + + except Exception as e: + print(f"⚠️ VR清理过程中出错: {e}") + + def get_vr_status(self): + """获取VR状态信息""" + return { + 'available': self.is_vr_available(), + 'initialized': self.vr_initialized, + 'enabled': self.vr_enabled, + 'eye_resolution': (self.eye_width, self.eye_height), + 'device_count': len(self.controller_poses) + (1 if self.vr_enabled else 0), + 'vr_fps': self.vr_fps, + 'frame_count': self.frame_count, + 'submit_failures': self.submit_failures, + 'pose_failures': self.pose_failures + } + + def submit_texture(self, eye, texture): + """优化的VR纹理提交 - 使用缓存的纹理ID,避免重复prepareNow""" + try: + if not self.vr_compositor: + print("❌ VR compositor不可用") + self.submit_failures += 1 + return + + # 🚀 关键修复:防止同一帧重复提交 + current_frame = getattr(self, 'frame_count', 0) + if not hasattr(self, '_last_submit_frame'): + self._last_submit_frame = {} + + # 检查当前帧是否已经提交过此眼睛的纹理 + if eye in self._last_submit_frame and self._last_submit_frame[eye] == current_frame: + # 静默跳过,避免spam输出 + return + + # 记录当前帧提交 + self._last_submit_frame[eye] = current_frame + + # 🚀 关键优化:直接使用缓存的纹理ID,避免重复prepareNow + if eye == openvr.Eye_Left: + handle = self.left_texture_id + eye_name = "左眼" + elif eye == openvr.Eye_Right: + handle = self.right_texture_id + eye_name = "右眼" + else: + print(f"❌ 未知的眼睛类型: {eye}") + self.submit_failures += 1 + return + + # 检查缓存的纹理ID是否有效 + if not handle or handle <= 0: + print(f"❌ {eye_name}纹理ID缓存无效: {handle}") + print(" 这可能表示纹理准备失败,需要检查_prepare_and_cache_textures()") + self.submit_failures += 1 + return + + # ❌ 移除gsg.flush()调用 - 基于OpenVR官方实践 + # gsg.flush()等同于OpenGL的glFlush(),导致强制CPU-GPU同步 + # 每帧调用2次(左右眼)是GPU周期性峰值的主要原因 + # 参考:OpenVR社区经验表明同步调用是性能杀手 + # + # gsg = self.world.win.getGsg() + # if gsg and hasattr(gsg, 'flush'): + # try: + # gsg.flush() + # except Exception as flush_error: + # if not hasattr(self, '_gsg_flush_error_logged'): + # print(f"⚠️ GSG刷新失败: {flush_error}") + # self._gsg_flush_error_logged = True + + # 🚀 关键优化:使用缓存的OpenVR Texture对象,避免每帧创建 + if eye == openvr.Eye_Left: + ovr_texture = self._left_ovr_texture + else: + ovr_texture = self._right_ovr_texture + + # 检查缓存对象是否存在(向后兼容) + if ovr_texture is None: + # 备用方案:如果缓存对象不存在,创建新的(性能较差) + ovr_texture = openvr.Texture_t() + ovr_texture.eType = openvr.TextureType_OpenGL + ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + if not hasattr(self, '_texture_fallback_warned'): + print("⚠️ 使用Texture对象备用方案(性能次优)") + self._texture_fallback_warned = True + + # 只更新handle,其他属性已预设置 + ovr_texture.handle = handle + + # 提交到VR系统 + error = self.vr_compositor.submit(eye, ovr_texture) + + # 检查错误 + if error and error != openvr.VRCompositorError_None: + print(f"⚠️ VR{eye_name}纹理提交错误: {error}") + self.submit_failures += 1 + else: + # 只在第一次成功时输出 + if not hasattr(self, '_optimized_submit_success_logged'): + print(f"✅ 优化版VR纹理提交成功! 使用缓存ID,避免重复prepareNow") + print(f" {eye_name}纹理ID: {handle}") + self._optimized_submit_success_logged = True + + # 🔧 智能GPU同步策略 - 减少不必要的flush调用 + # 只有在检测到性能问题或每N帧时才强制flush + if eye == openvr.Eye_Right: + self._smart_gpu_sync() + + except Exception as e: + print(f"❌ VR纹理提交异常: {e}") + import traceback + traceback.print_exc() + self.submit_failures += 1 + + def _smart_gpu_sync(self): + """智能GPU同步策略 - 只在必要时进行同步""" + try: + # 初始化同步控制变量 + if not hasattr(self, '_last_gpu_sync_frame'): + self._last_gpu_sync_frame = 0 + self._gpu_sync_interval = 60 # 每60帧强制同步一次(1秒@60FPS) + self._performance_based_sync = True + + current_frame = getattr(self, 'frame_count', 0) + + # 策略1: 性能自适应同步 + if self._performance_based_sync: + # 如果渲染时间过长,增加同步频率 + total_render_time = getattr(self, 'left_render_time', 0) + getattr(self, 'right_render_time', 0) + + if total_render_time > 15.0: # 如果总渲染时间超过15ms + # 高负载时更频繁同步 + sync_interval = 30 # 每30帧同步 + elif total_render_time > 10.0: + sync_interval = 45 # 中等负载 + else: + sync_interval = self._gpu_sync_interval # 正常负载 + + should_sync = (current_frame - self._last_gpu_sync_frame) >= sync_interval + else: + # 策略2: 固定间隔同步 + should_sync = (current_frame - self._last_gpu_sync_frame) >= self._gpu_sync_interval + + # 执行同步 + if should_sync: + gsg = self.world.win.getGsg() + if gsg and hasattr(gsg, 'flush'): + try: + gsg.flush() + self._last_gpu_sync_frame = current_frame + + # 只在首次或Debug时输出 + if not hasattr(self, '_smart_sync_logged') or self.debug_output_enabled: + if not hasattr(self, '_smart_sync_logged'): + print(f"🔧 智能GPU同步已启用 - 间隔:{sync_interval}帧") + self._smart_sync_logged = True + elif self.debug_output_enabled and current_frame % 600 == 1: + print(f"🔧 智能同步触发 (帧#{current_frame}, 间隔:{sync_interval})") + + except Exception as flush_error: + if not hasattr(self, '_smart_sync_error_logged'): + print(f"⚠️ 智能GPU同步失败: {flush_error}") + self._smart_sync_error_logged = True + + except Exception as e: + print(f"⚠️ 智能GPU同步策略异常: {e}") + + def _disable_main_cam(self): + """禁用主相机 - 基于参考实现""" + try: + # 保存原始相机状态 + if not hasattr(self, '_original_camera_parent'): + self._original_camera_parent = self.world.camera.getParent() + + # 创建空节点并将主相机重新附加到它 + self._empty_world = NodePath("empty_world") + self.world.camera.reparentTo(self._empty_world) + + print("✓ 主相机已禁用") + except Exception as e: + print(f"⚠️ 禁用主相机失败: {e}") + + def _enable_main_cam(self): + """恢复主相机 - 基于参考实现""" + try: + # 恢复原始相机状态 + if hasattr(self, '_original_camera_parent') and self._original_camera_parent: + self.world.camera.reparentTo(self._original_camera_parent) + else: + # 如果没有保存的父节点,重新附加到render + self.world.camera.reparentTo(self.world.render) + + # 清理空世界节点 + if hasattr(self, '_empty_world'): + self._empty_world.removeNode() + delattr(self, '_empty_world') + + print("✓ 主相机已恢复") + except Exception as e: + print(f"⚠️ 恢复主相机失败: {e}") + + def _initialize_controllers(self): + """初始化VR手柄控制器""" + try: + print("🎮 正在初始化VR手柄控制器...") + + # 创建左右手柄控制器实例 + self.left_controller = LeftController(self) + self.right_controller = RightController(self) + + # 检测现有连接的控制器 + self._detect_controllers() + + print("✓ VR手柄控制器初始化完成") + + except Exception as e: + print(f"⚠️ VR手柄初始化失败: {e}") + import traceback + traceback.print_exc() + + def _detect_controllers(self): + """检测并连接VR控制器""" + if not self.vr_system: + return + + try: + for device_index in range(openvr.k_unMaxTrackedDeviceCount): + # 检查设备是否已连接 + if not self.vr_system.isTrackedDeviceConnected(device_index): + continue + + # 获取设备类型 + device_class = self.vr_system.getTrackedDeviceClass(device_index) + + if device_class == openvr.TrackedDeviceClass_Controller: + # 获取控制器角色 + role = self.vr_system.getControllerRoleForTrackedDeviceIndex(device_index) + + if role == openvr.TrackedControllerRole_LeftHand and self.left_controller: + self.left_controller.set_device_index(device_index) + self.controllers[device_index] = self.left_controller + # 为设备创建锚点 + self._create_tracked_device_anchor(device_index, 'left_controller') + + elif role == openvr.TrackedControllerRole_RightHand and self.right_controller: + self.right_controller.set_device_index(device_index) + self.controllers[device_index] = self.right_controller + # 为设备创建锚点 + self._create_tracked_device_anchor(device_index, 'right_controller') + + print(f"🎮 检测到 {len(self.controllers)} 个控制器") + + except Exception as e: + print(f"⚠️ 控制器检测失败: {e}") + + def _create_tracked_device_anchor(self, device_index, name): + """为跟踪设备创建锚点节点""" + if not self.tracking_space: + print(f"⚠️ 无法为设备 {device_index} 创建锚点 - tracking_space未初始化") + return + + try: + # 获取设备模型名称 + if self.vr_system: + model_name = self.vr_system.getStringTrackedDeviceProperty( + device_index, + openvr.Prop_RenderModelName_String + ) + anchor_name = f"{device_index}:{model_name}:{name}" + else: + anchor_name = f"{device_index}:{name}" + + # 创建锚点节点 + device_anchor = self.tracking_space.attachNewNode(anchor_name) + self.tracked_device_anchors[device_index] = device_anchor + + print(f"✓ 为设备 {device_index} 创建锚点: {anchor_name}") + + except Exception as e: + print(f"⚠️ 创建设备锚点失败: {e}") + + def update_tracked_devices(self): + """更新所有跟踪设备的姿态 - 基于参考实现""" + if not self.poses or not self.vr_system: + return + + try: + # 更新每个已连接的控制器 + for device_index, controller in self.controllers.items(): + if device_index < len(self.poses): + pose_data = self.poses[device_index] + + # 更新控制器姿态 + controller.update_pose(pose_data) + + # 更新控制器输入状态 + controller.update_input_state(self.vr_system) + + # 更新其他跟踪设备的锚点 + for device_index in range(1, min(len(self.poses), openvr.k_unMaxTrackedDeviceCount)): + if device_index in self.tracked_device_anchors: + pose_data = self.poses[device_index] + if pose_data.bPoseIsValid: + # 转换姿态矩阵 + modelview = self.convert_mat(pose_data.mDeviceToAbsoluteTracking) + final_matrix = self.coord_mat_inv * modelview * self.coord_mat + + # 更新锚点变换 + anchor = self.tracked_device_anchors[device_index] + anchor.setMat(final_matrix) + anchor.show() + else: + # 姿态无效,隐藏锚点 + self.tracked_device_anchors[device_index].hide() + + except Exception as e: + if self.frame_count % 300 == 0: # 每5秒输出一次错误 + print(f"⚠️ 更新跟踪设备失败: {e}") + + def get_controller_by_role(self, role): + """根据角色获取控制器 + + Args: + role: 'left' 或 'right' + + Returns: + VRController实例或None + """ + if role == 'left': + return self.left_controller + elif role == 'right': + return self.right_controller + return None + + def are_controllers_connected(self): + """检查是否有控制器连接""" + return len(self.controllers) > 0 + + def get_connected_controllers(self): + """获取所有连接的控制器列表""" + return list(self.controllers.values()) + + def trigger_controller_haptic(self, role, duration=0.001, strength=1.0): + """触发控制器震动反馈 + + Args: + role: 'left', 'right' 或 'both' + duration: 震动持续时间(秒) + strength: 震动强度 (0.0-1.0) + """ + if role in ['left', 'both'] and self.left_controller: + self.left_controller.trigger_haptic_feedback(duration, strength) + + if role in ['right', 'both'] and self.right_controller: + self.right_controller.trigger_haptic_feedback(duration, strength) + + # VR动作系统便捷方法 + def is_trigger_pressed(self, hand='any'): + """检查扳机是否被按下 + + Args: + hand: 'left', 'right', 'any' + """ + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_pressed('trigger', device_path) + return pressed + + def is_trigger_just_pressed(self, hand='any'): + """检查扳机是否刚刚被按下""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_just_pressed('trigger', device_path) + return pressed + + def is_grip_pressed(self, hand='any'): + """检查握把是否被按下""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_pressed('grip', device_path) + return pressed + + def is_grip_just_pressed(self, hand='any'): + """检查握把是否刚刚被按下""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_just_pressed('grip', device_path) + return pressed + + def is_menu_pressed(self, hand='any'): + """检查菜单按钮是否被按下""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + pressed, _ = self.action_manager.is_digital_action_pressed('menu', device_path) + return pressed + + def is_trackpad_touched(self, hand='any'): + """检查触摸板是否被触摸""" + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + touched, _ = self.action_manager.is_digital_action_pressed('trackpad_touch', device_path) + return touched + + def get_trackpad_position(self, hand='any'): + """获取触摸板位置 + + Returns: + Vec2或None: 触摸板位置 (-1到1的范围) + """ + device_path = None + if hand == 'left': + device_path = '/user/hand/left' + elif hand == 'right': + device_path = '/user/hand/right' + + value, _ = self.action_manager.get_analog_action_value('trackpad', device_path) + return value + + # VR交互系统便捷方法 + def get_selected_object(self, hand='any'): + """获取指定手选中的对象 + + Args: + hand: 'left', 'right', 'any' + + Returns: + 选中的对象节点或None + """ + if hand == 'any': + # 返回任意手选中的对象 + for controller in self.get_connected_controllers(): + selected = self.interaction_manager.get_selected_object(controller.name) + if selected: + return selected + return None + else: + return self.interaction_manager.get_selected_object(hand) + + def get_grabbed_object(self, hand='any'): + """获取指定手抓取的对象 + + Args: + hand: 'left', 'right', 'any' + + Returns: + 抓取的对象节点或None + """ + if hand == 'any': + # 返回任意手抓取的对象 + for controller in self.get_connected_controllers(): + grabbed = self.interaction_manager.get_grabbed_object(controller.name) + if grabbed: + return grabbed + return None + else: + return self.interaction_manager.get_grabbed_object(hand) + + def is_grabbing_object(self, hand='any'): + """检查是否正在抓取对象 + + Args: + hand: 'left', 'right', 'any' + + Returns: + bool: 是否正在抓取 + """ + if hand == 'any': + # 检查任意手是否正在抓取 + for controller in self.get_connected_controllers(): + if self.interaction_manager.is_grabbing(controller.name): + return True + return False + else: + return self.interaction_manager.is_grabbing(hand) + + def force_release_all_grabs(self): + """强制释放所有抓取的对象""" + self.interaction_manager.force_release_all() + + def add_interactable_object(self, object_node): + """将对象标记为可交互 + + Args: + object_node: 要标记的对象节点 + """ + self.interaction_manager._add_collision_to_object(object_node) + + def _disable_async_reprojection(self): + """禁用异步重投影 - 备选修复方案""" + try: + # 尝试通过OpenVR设置禁用异步重投影 + if hasattr(openvr, 'VRSettings'): + settings = openvr.VRSettings() + if settings: + # 禁用异步重投影 + error = settings.setBool("steamvr", "enableAsyncReprojection", False) + if error == openvr.VRSettingsError_None: + print("✅ 异步重投影已禁用") + else: + print(f"⚠️ 禁用异步重投影失败: 设置错误 {error}") + else: + print("⚠️ 无法获取VR设置接口") + else: + print("⚠️ OpenVR设置接口不可用") + + except Exception as e: + print(f"⚠️ 禁用异步重投影失败: {e}") + + def enable_async_reprojection_disable(self): + """启用异步重投影禁用选项 - 用户可调用的方法""" + self.disable_async_reprojection = True + print("📝 异步重投影禁用选项已启用,将在下次VR初始化时生效") + + def disable_async_reprojection_disable(self): + """禁用异步重投影禁用选项 - 恢复默认行为""" + self.disable_async_reprojection = False + print("📝 异步重投影禁用选项已关闭,将使用默认ATW行为") + + def enable_gc_control(self): + """启用垃圾回收控制 - 减少VR渲染期间的GC峰值""" + if not self._gc_control_enabled: + self._gc_control_enabled = True + if not self._gc_disabled: + gc.disable() + self._gc_disabled = True + print("✅ VR垃圾回收控制已启用") + else: + print("ℹ️ VR垃圾回收控制已经启用") + + def disable_gc_control(self): + """禁用垃圾回收控制 - 恢复自动垃圾回收""" + if self._gc_control_enabled: + self._gc_control_enabled = False + if self._gc_disabled: + gc.enable() + self._gc_disabled = False + print("✅ VR垃圾回收控制已禁用,恢复自动垃圾回收") + else: + print("ℹ️ VR垃圾回收控制已经禁用") + + def set_manual_gc_interval(self, frames): + """设置手动垃圾回收间隔 + + Args: + frames: 帧数间隔 (建议100-600) + """ + if 50 <= frames <= 1800: + old_interval = self._manual_gc_interval + self._manual_gc_interval = frames + print(f"✅ 手动GC间隔: {old_interval} → {frames} 帧") + else: + print("⚠️ GC间隔应在50-1800帧之间") + + def force_manual_gc(self): + """强制执行一次垃圾回收""" + collected = gc.collect() + print(f"🗑️ 强制GC: 清理了 {collected} 个对象") + return collected + + def get_object_pool_status(self): + """获取对象池状态""" + return { + 'matrix_pool_size': len(self._matrix_pool) if hasattr(self, '_matrix_pool') else 0, + 'matrix_pool_capacity': getattr(self, '_matrix_pool_size', 0), + 'cached_controllers': len(self.controller_poses), + 'cached_matrices': len(getattr(self, '_cached_matrices', {})), + } + + # ====== VR分辨率缩放和质量预设系统 ====== + + def set_resolution_scale(self, scale): + """设置VR分辨率缩放系数 + + Args: + scale: 缩放系数 (0.3-1.0),0.75表示75%分辨率 + """ + if not (0.3 <= scale <= 1.0): + print(f"⚠️ 分辨率缩放系数应在0.3-1.0之间,当前: {scale}") + return False + + old_scale = self.resolution_scale + self.resolution_scale = scale + + # 如果VR已初始化,重新创建缓冲区 + if self.vr_initialized: + self._apply_resolution_scale() + + print(f"✓ VR分辨率缩放: {old_scale} → {scale}") + pixel_reduction = (1 - scale**2) * 100 + print(f"📊 像素减少: {pixel_reduction:.1f}%") + + return True + + def set_quality_preset(self, preset_name): + """设置VR质量预设 + + Args: + preset_name: 'performance', 'balanced', 'quality' + """ + if preset_name not in self.quality_presets: + print(f"⚠️ 未知的质量预设: {preset_name}") + print(f" 可用预设: {list(self.quality_presets.keys())}") + return False + + old_preset = self.current_quality_preset + self.current_quality_preset = preset_name + scale = self.quality_presets[preset_name] + + print(f"🎯 切换VR质量预设: {old_preset} → {preset_name}") + + return self.set_resolution_scale(scale) + + def cycle_quality_preset(self): + """循环切换质量预设""" + presets = list(self.quality_presets.keys()) + current_index = presets.index(self.current_quality_preset) + next_index = (current_index + 1) % len(presets) + next_preset = presets[next_index] + + return self.set_quality_preset(next_preset) + + def _apply_resolution_scale(self): + """应用分辨率缩放,重新创建VR缓冲区""" + try: + # 计算新的分辨率 + self.scaled_eye_width = int(self.base_eye_width * self.resolution_scale) + self.scaled_eye_height = int(self.base_eye_height * self.resolution_scale) + + # 更新当前分辨率 + self.eye_width = self.scaled_eye_width + self.eye_height = self.scaled_eye_height + self.current_eye_resolution = (self.eye_width, self.eye_height) + + print(f"🔄 重新创建VR缓冲区...") + print(f" 新分辨率: {self.eye_width}x{self.eye_height}") + + # 清理旧的缓冲区 + self._cleanup_vr_buffers() + + # 🔧 关键修复:根据渲染模式选择创建方法 + success = False + if self.vr_render_mode == VRRenderMode.RENDER_PIPELINE: + print(f" 使用RenderPipeline模式重建...") + success = self._create_vr_buffers_with_pipeline() + if not success: + print("⚠️ RenderPipeline模式创建失败,回退到普通渲染模式") + self.vr_render_mode = VRRenderMode.NORMAL + success = self._create_vr_buffers() + else: + print(f" 使用普通模式重建...") + success = self._create_vr_buffers() + + if success: + # 重新设置相机 + self._setup_vr_cameras() + print("✅ VR缓冲区重新创建成功") + return True + else: + print("❌ VR缓冲区重新创建失败") + return False + + except Exception as e: + print(f"❌ 应用分辨率缩放失败: {e}") + import traceback + traceback.print_exc() + return False + + + def get_resolution_info(self): + """获取分辨率相关信息""" + return { + 'base_resolution': (self.base_eye_width, self.base_eye_height), + 'current_resolution': (self.eye_width, self.eye_height), + 'resolution_scale': self.resolution_scale, + 'current_preset': self.current_quality_preset, + 'available_presets': self.quality_presets, + 'pixel_reduction_percent': (1 - self.resolution_scale**2) * 100 + } + + def print_resolution_info(self): + """输出分辨率信息""" + info = self.get_resolution_info() + print("🔧 ===== VR分辨率信息 =====") + print(f" 推荐分辨率: {info['base_resolution'][0]}x{info['base_resolution'][1]}") + print(f" 当前分辨率: {info['current_resolution'][0]}x{info['current_resolution'][1]}") + print(f" 缩放系数: {info['resolution_scale']}") + print(f" 当前预设: {info['current_preset']}") + print(f" 像素减少: {info['pixel_reduction_percent']:.1f}%") + print(" 可用预设:") + for name, scale in info['available_presets'].items(): + marker = "✓" if name == info['current_preset'] else " " + print(f" {marker} {name}: {scale} ({scale*100:.0f}%)") + print("==========================") + + # ====== 性能模式控制方法 ====== + + def enable_performance_mode(self): + """手动启用性能模式 - 立即禁用详细监控以提升性能""" + if not self.performance_mode_enabled: + self.performance_mode_enabled = True + print("🎯 性能模式已手动启用 - 禁用详细监控以提升性能") + print(" 现在将减少每帧对象创建,显著提升VR性能稳定性") + else: + print("ℹ️ 性能模式已经启用") + + def disable_performance_mode(self): + """禁用性能模式 - 重新启用详细监控(用于调试)""" + if self.performance_mode_enabled: + self.performance_mode_enabled = False + print("🔍 性能模式已禁用 - 重新启用详细监控") + print(" 注意:这将增加每帧对象创建,可能影响VR性能") + else: + print("ℹ️ 性能模式已经禁用") + + def set_performance_mode_trigger_frame(self, frame_count): + """设置性能模式自动触发的帧数 + + Args: + frame_count: 触发帧数 (建议300-1200) + """ + if 100 <= frame_count <= 3600: + old_trigger = self.performance_mode_trigger_frame + self.performance_mode_trigger_frame = frame_count + print(f"✅ 性能模式触发帧数: {old_trigger} → {frame_count}") + else: + print("⚠️ 触发帧数应在100-3600之间") + + def get_performance_mode_status(self): + """获取性能模式状态""" + return { + 'performance_mode_enabled': self.performance_mode_enabled, + 'trigger_frame': self.performance_mode_trigger_frame, + 'current_frame': self.frame_count, + 'will_trigger_at_frame': self.performance_mode_trigger_frame if not self.performance_mode_enabled else None, + 'gc_interval_normal': self._manual_gc_interval, + 'gc_interval_performance': self._manual_gc_interval * 2, + } + + # ====== VR测试模式 ====== + + def enable_vr_test_mode(self, display_mode='stereo'): + """启用VR测试模式 - 将VR渲染直接显示在屏幕上 + + Args: + display_mode: 显示模式 + - 'stereo': 左右眼并排显示 + - 'left': 只显示左眼 + - 'right': 只显示右眼 + """ + if not self.is_vr_available(): + print("❌ VR系统不可用,无法启动测试模式") + return False + + try: + print(f"🧪 启动VR测试模式 - 显示模式: {display_mode}") + + # 初始化VR系统(如果还没初始化) + if not self.vr_initialized: + if not self.initialize_vr(): + print("❌ VR初始化失败") + return False + + # 🔧 关键修复:确保测试模式的纹理资源已初始化 + # 这解决了测试模式纹理提交失败导致的36FPS问题 + print("🔧 检查VR测试模式纹理资源...") + if not self._ensure_test_mode_textures(): + print("❌ VR测试模式纹理资源初始化失败") + return False + + # 设置测试模式 + self.vr_test_mode = True + self.test_display_mode = display_mode + + # 启用VR渲染但不提交给OpenVR + if not self.vr_enabled: + # 启用VR渲染流程(但会在回调中跳过提交) + self.vr_enabled = True + self._disable_main_cam() + + # 设置高帧率用于测试 + if hasattr(self.world, 'qtWidget') and self.world.qtWidget: + if hasattr(self.world.qtWidget, 'synchronizer'): + self.world.qtWidget.synchronizer.setInterval(int(1000/144)) + print("✓ 测试模式:Qt Timer设置为144Hz") + + # 初始化测试显示系统 + if not self._initialize_test_display(): + print("❌ 测试显示系统初始化失败") + return False + + # 创建性能HUD + if not self._initialize_test_performance_hud(): + print("❌ 性能HUD初始化失败") + return False + + # 恢复主相机以查看测试内容 + self._enable_main_cam() + + # 重置HUD更新计数器 + self.hud_update_counter = 0 + + print("✅ VR测试模式已启用") + print(" - VR内容将显示在屏幕上") + print(" - 不会向OpenVR提交纹理") + print(" - 可以准确测量纯渲染性能") + print(f" - 当前显示模式: {display_mode}") + + return True + + except Exception as e: + print(f"❌ 启动VR测试模式失败: {e}") + import traceback + traceback.print_exc() + return False + + def _ensure_test_mode_textures(self): + """确保VR测试模式的纹理资源已正确初始化 + + 这解决了测试模式启用纹理提交时的36FPS问题: + - VR测试模式可能跳过了VR渲染缓冲区的初始化 + - submit_texture()需要有效的texture ID和OpenVR Texture对象 + - 如果未初始化会导致提交失败,造成帧率减半 + """ + try: + print(" 检查VR渲染缓冲区...") + + # 检查VR渲染缓冲区是否存在 + buffers_exist = ( + hasattr(self, 'vr_left_eye_buffer') and self.vr_left_eye_buffer and + hasattr(self, 'vr_right_eye_buffer') and self.vr_right_eye_buffer + ) + + if not buffers_exist: + print(" ⚠️ VR渲染缓冲区不存在,正在创建...") + if not self._setup_vr_render_buffers(): + print(" ❌ VR渲染缓冲区创建失败") + return False + print(" ✅ VR渲染缓冲区创建成功") + else: + print(" ✅ VR渲染缓冲区已存在") + + # 检查纹理ID是否已缓存 + print(" 检查纹理ID缓存...") + texture_ids_cached = ( + hasattr(self, 'left_texture_id') and self.left_texture_id and self.left_texture_id > 0 and + hasattr(self, 'right_texture_id') and self.right_texture_id and self.right_texture_id > 0 + ) + + if not texture_ids_cached: + print(" ⚠️ 纹理ID未缓存,正在准备纹理...") + if not self._prepare_and_cache_textures(): + print(" ❌ 纹理准备和缓存失败") + return False + print(f" ✅ 纹理ID缓存成功 - 左眼:{self.left_texture_id}, 右眼:{self.right_texture_id}") + else: + print(f" ✅ 纹理ID已缓存 - 左眼:{self.left_texture_id}, 右眼:{self.right_texture_id}") + + # 检查OpenVR Texture对象是否已创建 + print(" 检查OpenVR Texture对象...") + ovr_textures_exist = ( + hasattr(self, '_left_ovr_texture') and self._left_ovr_texture and + hasattr(self, '_right_ovr_texture') and self._right_ovr_texture + ) + + if not ovr_textures_exist: + print(" ⚠️ OpenVR Texture对象未创建,正在创建...") + self._create_cached_ovr_textures() + print(" ✅ OpenVR Texture对象创建成功") + else: + print(" ✅ OpenVR Texture对象已存在") + + print(" ✅ VR测试模式纹理资源检查完成,可安全启用纹理提交") + return True + + except Exception as e: + print(f" ❌ VR测试模式纹理资源检查失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_cached_ovr_textures(self): + """创建缓存的OpenVR Texture对象 - 避免每帧创建新对象""" + try: + import openvr + self._left_ovr_texture = openvr.Texture_t() + self._right_ovr_texture = openvr.Texture_t() + + # 设置固定属性(这些不变) + self._left_ovr_texture.eType = openvr.TextureType_OpenGL + self._left_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + self._right_ovr_texture.eType = openvr.TextureType_OpenGL + self._right_ovr_texture.eColorSpace = openvr.ColorSpace_Gamma + + print("✅ OpenVR Texture对象缓存已创建") + except Exception as e: + print(f"⚠️ OpenVR Texture对象创建失败: {e}") + # 不抛出异常,使用备用方案 + + def _batch_submit_textures(self): + """批量提交两眼纹理 - OpenVR最佳实践 + + 基于官方hellovr示例的实现: + - 两眼都渲染完成后,快速连续提交 + - 减少submit阻塞时间,避免错过VSync窗口 + - 这是解决36FPS问题的关键 + """ + try: + if not self.vr_compositor: + return False + + # 检查纹理是否准备好 + if not (self.vr_left_texture and self.vr_right_texture): + return False + + # 🚀 关键:快速连续提交两眼,最小化阻塞时间 + # 这符合OpenVR官方示例的做法 + success_left = False + success_right = False + + # 提交左眼纹理 + try: + self.submit_texture(openvr.Eye_Left, self.vr_left_texture) + success_left = True + except Exception as e: + print(f"❌ 批量提交左眼失败: {e}") + + # 立即提交右眼纹理(不等待) + try: + self.submit_texture(openvr.Eye_Right, self.vr_right_texture) + success_right = True + except Exception as e: + print(f"❌ 批量提交右眼失败: {e}") + + # 🚀 关键修复:调用PostPresentHandoff解除compositor阻塞 + # 这是解决36FPS问题的核心 - 确保compositor不会等待VSync + if success_left and success_right: + try: + # PostPresentHandoff告诉compositor我们已完成帧处理 + # 防止compositor等待下一个VSync周期 + if hasattr(self.vr_compositor, 'postPresentHandoff'): + self.vr_compositor.postPresentHandoff() + elif hasattr(self.vr_compositor, 'PostPresentHandoff'): + self.vr_compositor.PostPresentHandoff() + else: + # 备用方案:如果没有PostPresentHandoff,记录警告 + if not hasattr(self, '_post_present_warning_logged'): + print("⚠️ PostPresentHandoff方法未找到,可能影响时序") + self._post_present_warning_logged = True + except Exception as handoff_error: + if not hasattr(self, '_handoff_error_logged'): + print(f"⚠️ PostPresentHandoff调用失败: {handoff_error}") + self._handoff_error_logged = True + + # 记录批量提交状态(仅首次成功时) + if not hasattr(self, '_batch_submit_success_logged'): + print("✅ OpenVR批量提交模式+PostPresentHandoff已启用") + print(" 这应该解决36FPS → 72FPS的问题") + self._batch_submit_success_logged = True + return True + else: + return False + + except Exception as e: + print(f"❌ 批量提交纹理失败: {e}") + return False + + def disable_vr_test_mode(self): + """禁用VR测试模式""" + try: + print("🧪 禁用VR测试模式...") + + # 清理测试显示 + self._cleanup_test_display() + + # 清理性能HUD + self._cleanup_test_performance_hud() + + # 关闭测试模式 + self.vr_test_mode = False + self.test_mode_initialized = False + + # 重置HUD更新计数器 + self.hud_update_counter = 0 + + # 恢复正常VR模式或禁用VR + if self.vr_enabled: + print(" 选择: [1] 恢复正常VR模式 [2] 完全禁用VR") + # 这里可以让用户选择,现在默认禁用VR + self.disable_vr() + + print("✅ VR测试模式已禁用") + return True + + except Exception as e: + print(f"❌ 禁用VR测试模式失败: {e}") + return False + + def switch_test_display_mode(self, display_mode): + """切换测试显示模式 + + Args: + display_mode: 'stereo', 'left', 'right' + """ + if not self.vr_test_mode: + print("⚠️ 请先启用VR测试模式") + return False + + if display_mode not in ['stereo', 'left', 'right']: + print(f"⚠️ 无效的显示模式: {display_mode}") + return False + + print(f"🧪 切换显示模式: {self.test_display_mode} → {display_mode}") + + # 如果切换模式,需要清理旧的显示并重置标志位 + if self.test_display_mode != display_mode: + self._cleanup_test_display() + + self.test_display_mode = display_mode + + # 更新显示 + self._update_test_display() + return True + + def _initialize_test_display(self): + """初始化测试显示系统""" + try: + print("🔧 正在初始化测试显示系统...") + + # 导入必要的Panda3D组件 + from panda3d.core import CardMaker, PandaNode + + # 创建显示四边形 + cm = CardMaker("vr_test_display") + + if self.test_display_mode == 'stereo': + # 并排显示:左眼在左半屏,右眼在右半屏 + cm.setFrame(-1, 1, -0.5, 0.5) # 全屏 + else: + # 单眼显示:占据整个屏幕 + cm.setFrame(-1, 1, -1, 1) + + # 创建节点 + self.test_display_quad = self.world.render2d.attachNewNode(cm.generate()) + + # 设置初始纹理 + self._update_test_display() + + # 设置渲染状态 + self.test_display_quad.setTransparency(0) # 不透明 + self.test_display_quad.setBin("background", 0) # 背景层 + + self.test_mode_initialized = True + print("✅ 测试显示系统初始化完成") + return True + + except Exception as e: + print(f"❌ 测试显示系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def _update_test_display(self): + """更新测试显示内容""" + try: + if not self.test_mode_initialized: + return + + # 根据显示模式设置纹理 + if self.test_display_mode == 'left': + if not self.test_display_quad: + return + if self.vr_left_texture: + self.test_display_quad.setTexture(self.vr_left_texture) + elif self.test_display_mode == 'right': + if not self.test_display_quad: + return + if self.vr_right_texture: + self.test_display_quad.setTexture(self.vr_right_texture) + elif self.test_display_mode == 'stereo': + # 立体显示:只在第一次创建,后续只更新纹理 + if not self.stereo_display_created: + self._create_stereo_display() + else: + # 只更新纹理,不重新创建 + if self.test_display_quad and self.vr_left_texture: + self.test_display_quad.setTexture(self.vr_left_texture) + if self.test_right_quad and self.vr_right_texture: + self.test_right_quad.setTexture(self.vr_right_texture) + + except Exception as e: + print(f"⚠️ 更新测试显示失败: {e}") + + def _create_stereo_display(self): + """创建左右眼并排显示""" + try: + # 如果已经创建,直接返回 + if self.stereo_display_created and self.test_display_quad and self.test_right_quad: + return + + # 移除旧的显示 + if self.test_display_quad: + self.test_display_quad.removeNode() + if self.test_right_quad: + self.test_right_quad.removeNode() + + from panda3d.core import CardMaker + + # 创建左眼显示 + left_cm = CardMaker("vr_test_left") + left_cm.setFrame(-1, 0, -1, 1) # 左半屏 + left_quad = self.world.render2d.attachNewNode(left_cm.generate()) + if self.vr_left_texture: + left_quad.setTexture(self.vr_left_texture) + + # 创建右眼显示 + right_cm = CardMaker("vr_test_right") + right_cm.setFrame(0, 1, -1, 1) # 右半屏 + right_quad = self.world.render2d.attachNewNode(right_cm.generate()) + if self.vr_right_texture: + right_quad.setTexture(self.vr_right_texture) + + # 保存引用 + self.test_display_quad = left_quad # 保存左眼引用 + self.test_right_quad = right_quad # 额外保存右眼引用 + self.stereo_display_created = True # 标记已创建 + + print("✓ 立体显示已创建") + + except Exception as e: + print(f"⚠️ 创建立体显示失败: {e}") + self.stereo_display_created = False + + def _cleanup_test_display(self): + """清理测试显示""" + try: + if self.test_display_quad: + self.test_display_quad.removeNode() + self.test_display_quad = None + + if hasattr(self, 'test_right_quad') and self.test_right_quad: + self.test_right_quad.removeNode() + self.test_right_quad = None + + # 重置立体显示标志位 + self.stereo_display_created = False + + print("✓ 测试显示已清理") + + except Exception as e: + print(f"⚠️ 清理测试显示失败: {e}") + + def _initialize_test_performance_hud(self): + """初始化性能HUD""" + try: + print("🔧 正在初始化性能HUD...") + + from direct.gui.OnscreenText import OnscreenText + from panda3d.core import TextNode + + # 创建性能文本显示 + self.test_performance_text = OnscreenText( + text="初始化中...", + pos=(-0.95, 0.9), # 左上角,调整到屏幕内 + scale=0.05, + fg=(1, 1, 0, 1), # 黄色 + align=TextNode.ALeft, + shadow=(0, 0, 0, 0.5), # 黑色阴影 + parent=self.world.render2d + ) + + print("✅ 性能HUD初始化完成") + return True + + except Exception as e: + print(f"❌ 性能HUD初始化失败: {e}") + return False + + def _update_test_performance_hud(self): + """更新性能HUD显示(限制更新频率避免重影)""" + try: + if not self.test_performance_text or not self.vr_test_mode: + return + + # 增加计数器 + self.hud_update_counter += 1 + + # 只在指定间隔更新文本,避免重影 + if self.hud_update_counter < self.hud_update_interval: + return + + # 重置计数器 + self.hud_update_counter = 0 + + # 收集性能数据 + left_render_time = getattr(self, 'left_render_time', 0) + right_render_time = getattr(self, 'right_render_time', 0) + total_render_time = left_render_time + right_render_time + + # 计算FPS + current_fps = self.vr_fps if hasattr(self, 'vr_fps') else 0 + + # 获取系统性能 + cpu_usage = getattr(self, 'cpu_usage', 0) + memory_usage = getattr(self, 'memory_usage', 0) + gpu_usage = getattr(self, 'gpu_usage', 0) + + # 构建显示文本 + hud_text = f"""VR TEST MODE - {self.test_display_mode.upper()} + +Render Performance: + FPS: {current_fps:.1f} + Left Eye: {left_render_time:.2f}ms + Right Eye: {right_render_time:.2f}ms + Total: {total_render_time:.2f}ms + +System Performance: + CPU: {cpu_usage:.1f}% + Memory: {memory_usage:.1f}% + GPU: {gpu_usage:.1f}% + +Render Count: + Left Eye: {getattr(self, 'left_render_count', 0)} + Right Eye: {getattr(self, 'right_render_count', 0)} + +Target: <13.9ms@72Hz +Status: {'GOOD' if total_render_time < 10 else 'HIGH' if total_render_time < 16 else 'SLOW'} + +Hotkeys: +F1=Left F2=Right F3=SideBySide +ESC=Exit Test Mode""" + + # 更新文本 + self.test_performance_text.setText(hud_text) + + except Exception as e: + print(f"⚠️ 更新性能HUD失败: {e}") + + def _cleanup_test_performance_hud(self): + """清理性能HUD""" + try: + if self.test_performance_text: + self.test_performance_text.destroy() + self.test_performance_text = None + + print("✓ 性能HUD已清理") + + except Exception as e: + print(f"⚠️ 清理性能HUD失败: {e}") + + def get_test_mode_status(self): + """获取测试模式状态""" + if not self.vr_test_mode: + return None + + left_render_time = getattr(self, 'left_render_time', 0) + right_render_time = getattr(self, 'right_render_time', 0) + total_render_time = left_render_time + right_render_time + + return { + 'test_mode_enabled': self.vr_test_mode, + 'display_mode': self.test_display_mode, + 'vr_fps': getattr(self, 'vr_fps', 0), + 'left_render_time': left_render_time, + 'right_render_time': right_render_time, + 'total_render_time': total_render_time, + 'left_render_count': getattr(self, 'left_render_count', 0), + 'right_render_count': getattr(self, 'right_render_count', 0), + 'performance_rating': ('excellent' if total_render_time < 10 else + 'good' if total_render_time < 16 else 'poor') + } + + def set_test_mode_features(self, submit_texture=None, wait_poses=None): + """设置测试模式功能开关 - 用于渐进式调试 + + Args: + submit_texture: 是否在测试模式启用纹理提交(True/False/None=保持当前) + wait_poses: 是否在测试模式启用waitGetPoses(True/False/None=保持当前) + """ + if submit_texture is not None: + self.test_mode_submit_texture = submit_texture + if wait_poses is not None: + self.test_mode_wait_poses = wait_poses + + print(f"🔧 VR测试模式功能设置:") + print(f" 纹理提交: {'启用' if self.test_mode_submit_texture else '禁用'}") + print(f" 姿态等待: {'启用' if self.test_mode_wait_poses else '禁用'}") + + # 如果当前在测试模式,输出预期效果 + if self.vr_test_mode: + print(f" 当前测试模式已启用,设置将立即生效") + else: + print(f" 设置已保存,将在下次启用测试模式时生效") + + def get_test_mode_features(self): + """获取当前测试模式功能设置""" + return { + 'submit_texture': self.test_mode_submit_texture, + 'wait_poses': self.test_mode_wait_poses, + 'test_mode_active': self.vr_test_mode + } + + def run_vr_performance_test(self, duration_seconds=30, display_mode='stereo'): + """运行VR性能测试 - 简单的测试入口方法 + + Args: + duration_seconds: 测试持续时间(秒) + display_mode: 显示模式 ('stereo', 'left', 'right') + + Returns: + dict: 测试结果统计 + """ + print("🧪 ======= VR性能测试开始 =======") + print(f" 测试模式: {display_mode}") + print(f" 测试时长: {duration_seconds}秒") + print(" 按ESC键提前退出测试") + print(" F1=左眼 F2=右眼 F3=并排显示") + print("=================================") + + # 启动测试模式 + if not self.enable_vr_test_mode(display_mode): + print("❌ VR测试模式启动失败") + return None + + # 记录测试开始状态 + import time + test_start_time = time.time() + start_frame_count = getattr(self, 'frame_count', 0) + + print("✅ VR测试模式已启动") + print(" - VR内容直接显示在屏幕上") + print(" - 无OpenVR纹理提交开销") + print(" - 实时性能监控已启用") + print(f" - 当前显示: {display_mode.upper()}模式") + print("\n开始性能测试...") + + # 等待测试完成(用户手动停止或超时) + print(f"\n📊 测试将运行 {duration_seconds} 秒") + print(" 实时性能数据显示在屏幕左上角") + print(" 观察帧率和渲染时间变化") + + # 这里可以添加自动停止逻辑,但现在让用户手动控制 + print(f"\n⏰ 请观察 {duration_seconds} 秒后手动调用 disable_vr_test_mode() 停止测试") + print(" 或者随时调用 get_test_mode_status() 查看当前状态") + + return { + 'status': 'running', + 'start_time': test_start_time, + 'start_frame': start_frame_count, + 'display_mode': display_mode, + 'instructions': { + 'stop_test': 'vr_manager.disable_vr_test_mode()', + 'check_status': 'vr_manager.get_test_mode_status()', + 'switch_mode': 'vr_manager.switch_test_display_mode("left/right/stereo")' + } + } + + # ======================================================================== + # 性能监控属性代理 - 属性级别的API向后兼容 + # ======================================================================== + + @property + def left_render_count(self): + """左眼渲染计数器 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.left_render_count + return 0 + + @left_render_count.setter + def left_render_count(self, value): + """左眼渲染计数器设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.left_render_count = value + + @property + def right_render_count(self): + """右眼渲染计数器 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.right_render_count + return 0 + + @right_render_count.setter + def right_render_count(self, value): + """右眼渲染计数器设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.right_render_count = value + + @property + def left_render_time(self): + """左眼渲染时间 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.left_render_time + return 0.0 + + @left_render_time.setter + def left_render_time(self, value): + """左眼渲染时间设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.left_render_time = value + + @property + def right_render_time(self): + """右眼渲染时间 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.right_render_time + return 0.0 + + @right_render_time.setter + def right_render_time(self, value): + """右眼渲染时间设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.right_render_time = value + + # 帧计数和FPS相关属性 + @property + def frame_count(self): + """帧计数器 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.frame_count + return 0 + + @frame_count.setter + def frame_count(self, value): + """帧计数器设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.frame_count = value + + @property + def vr_fps(self): + """VR帧率 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.vr_fps + return 0.0 + + @vr_fps.setter + def vr_fps(self, value): + """VR帧率设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.vr_fps = value + + @property + def last_fps_check(self): + """上次FPS检查 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.last_fps_check + return 0 + + @last_fps_check.setter + def last_fps_check(self, value): + """上次FPS检查设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.last_fps_check = value + + @property + def last_fps_time(self): + """上次FPS时间 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.last_fps_time + return 0.0 + + @last_fps_time.setter + def last_fps_time(self, value): + """上次FPS时间设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.last_fps_time = value + + # 失败计数器 + @property + def submit_failures(self): + """提交失败计数 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.submit_failures + return 0 + + @submit_failures.setter + def submit_failures(self, value): + """提交失败计数设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.submit_failures = value + + @property + def pose_failures(self): + """姿态失败计数 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.pose_failures + return 0 + + @pose_failures.setter + def pose_failures(self, value): + """姿态失败计数设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.pose_failures = value + + # 性能监控开关 + @property + def performance_monitoring(self): + """性能监控开关 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.performance_monitoring + return False + + @performance_monitoring.setter + def performance_monitoring(self, value): + """性能监控开关设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.performance_monitoring = value + + @property + def debug_output_enabled(self): + """调试输出开关 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.debug_output_enabled + return False + + @debug_output_enabled.setter + def debug_output_enabled(self, value): + """调试输出开关设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.debug_output_enabled = value + + @property + def performance_mode_enabled(self): + """性能模式开关 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.performance_mode_enabled + return False + + @performance_mode_enabled.setter + def performance_mode_enabled(self, value): + """性能模式开关设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.performance_mode_enabled = value + + @property + def performance_mode_trigger_frame(self): + """性能模式触发帧 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.performance_mode_trigger_frame + return 600 + + @performance_mode_trigger_frame.setter + def performance_mode_trigger_frame(self, value): + """性能模式触发帧设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.performance_mode_trigger_frame = value + + # 时间监控属性 + @property + def wait_poses_time(self): + """姿态等待时间 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.wait_poses_time + return 0.0 + + @wait_poses_time.setter + def wait_poses_time(self, value): + """姿态等待时间设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.wait_poses_time = value + + @property + def submit_time(self): + """纹理提交时间 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.submit_time + return 0.0 + + @submit_time.setter + def submit_time(self, value): + """纹理提交时间设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.submit_time = value + + # 监控开关 + @property + def enable_gpu_timing(self): + """GPU计时开关 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.enable_gpu_timing + return False + + @enable_gpu_timing.setter + def enable_gpu_timing(self, value): + """GPU计时开关设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.enable_gpu_timing = value + + @property + def enable_pipeline_monitoring(self): + """管线监控开关 - 代理到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.enable_pipeline_monitoring + return True + + @enable_pipeline_monitoring.setter + def enable_pipeline_monitoring(self, value): + """管线监控开关设置 - 代理到性能监控系统""" + if self.performance_monitor: + self.performance_monitor.enable_pipeline_monitoring = value + + # ======================================================================== + # 性能监控委托方法 - API向后兼容层 + # ======================================================================== + + # 性能报告方法 + def _print_performance_report(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._print_performance_report() + + def _print_performance_recommendations(self, stats): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._print_performance_recommendations(stats) + + def _print_brief_performance_report(self, stats): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._print_brief_performance_report(stats) + + # 性能监控核心方法 + def _update_performance_metrics(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._update_performance_metrics() + + def _update_gpu_metrics(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._update_gpu_metrics() + + def _track_frame_time(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._track_frame_time() + + # GPU计时方法 + def _get_gpu_frame_timing(self, frames_ago=0): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._get_gpu_frame_timing(frames_ago) + return None + + def enable_gpu_timing_monitoring(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.enable_gpu_timing_monitoring() + + def disable_gpu_timing_monitoring(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.disable_gpu_timing_monitoring() + + # 管线统计方法 + def _start_timing(self, operation_name): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._start_timing(operation_name) + return None + + def _end_timing(self, timing_data): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._end_timing(timing_data) + return 0.0 + + def _get_pipeline_stats(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._get_pipeline_stats() + return {} + + def test_pipeline_monitoring(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.test_pipeline_monitoring() + + # 诊断方法 + def _print_render_callback_diagnostics(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._print_render_callback_diagnostics() + + def _check_rendering_optimizations(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._check_rendering_optimizations() + + def _diagnose_opengl_state(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor._diagnose_opengl_state() + + # 调试控制方法 + def enable_debug_output(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.enable_debug_output() + + def disable_debug_output(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.disable_debug_output() + + def set_debug_mode(self, mode): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.set_debug_mode(mode) + + def toggle_debug_output(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.toggle_debug_output() + return False + + def get_debug_status(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.get_debug_status() + return {} + + # 配置方法 + def set_performance_check_interval(self, interval): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.set_performance_check_interval(interval) + + def set_frame_time_history_size(self, size): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.set_frame_time_history_size(size) + + def set_performance_report_interval(self, frames): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.set_performance_report_interval(frames) + + def set_prediction_time(self, prediction_time_ms): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.set_prediction_time(prediction_time_ms) + + # 查询方法 + def get_performance_stats(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.get_performance_stats() + return {} + + def get_current_performance_summary(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.get_current_performance_summary() + return "性能监控未初始化" + + def get_performance_monitoring_config(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.get_performance_monitoring_config() + return {} + + def print_performance_monitoring_status(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.print_performance_monitoring_status() + + # 控制方法 + def enable_performance_monitoring(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.enable_performance_monitoring() + + def disable_performance_monitoring(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.disable_performance_monitoring() + + def force_performance_report(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.force_performance_report() + + def reset_performance_counters(self): + """委托到性能监控系统""" + if self.performance_monitor: + return self.performance_monitor.reset_performance_counters() diff --git a/core/world.py b/core/world.py index 549617c8..c9d37090 100644 --- a/core/world.py +++ b/core/world.py @@ -1,1192 +1,1192 @@ - -import math -import warnings - -from direct.actor.Actor import Actor - -warnings.filterwarnings("ignore", category=DeprecationWarning) - -from QMeta3D.Meta3DWorld import Meta3DWorld -from panda3d.core import (CardMaker, Vec4, Vec3, AmbientLight, DirectionalLight, - Point3, WindowProperties,Material,LColor) -from direct.showbase.ShowBaseGlobal import globalClock -from scene.scene_manager import SceneManager - -# 尝试导入插件管理器(如果存在) -try: - from plugins.plugin_manager import PluginManager - PLUGIN_SUPPORT = True -except ImportError: - PLUGIN_SUPPORT = False - print("⚠ 插件管理器未找到,插件功能将不可用") - -class CoreWorld(Meta3DWorld): - """核心世界功能类 - 负责基础的3D世界设置和核心功能""" - - def __init__(self): - super().__init__() - - # 初始化基础属性 - self.qtWidget = None # Qt部件引用(用于获取准确的渲染区域尺寸) - - # 设置相机控制参数 - #self.cameraSpeed = 200.0 # 移动速度 - self.cameraSpeed=20.0 - #self.cameraRotateSpeed = 40.0 # 旋转速度 - self.cameraRotateSpeed = 10.0 - - # 鼠标控制相关变量 - self.lastMouseX = 0 - self.lastMouseY = 0 - self.mouseRightPressed = False - - # 初始化插件管理器(如果支持) - if PLUGIN_SUPPORT: - self.plugin_manager = PluginManager(self) - else: - self.plugin_manager = None - - # 初始化世界 - self._setupResourcePaths() - self._setupCamera() - self._setupLighting() - self._setupGround() - self._loadFont() - - def _setupResourcePaths(self): - """设置Panda3D资源搜索路径,确保能正确找到Resources文件夹中的模型和贴图""" - try: - import os - from panda3d.core import getModelPath, DSearchPath, Filename - - # 获取项目根目录 - project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - resources_dir = os.path.join(project_root, "Resources") - - # 确保Resources目录存在 - if not os.path.exists(resources_dir): - os.makedirs(resources_dir, exist_ok=True) - print(f"✓ 创建Resources目录: {resources_dir}") - - # 添加Resources目录到Panda3D模型搜索路径 - model_path = getModelPath() - resources_filename = Filename.from_os_specific(resources_dir) - - # 检查路径是否已存在,避免重复添加 - if not model_path.findFile(resources_filename): - model_path.appendDirectory(resources_filename) - print(f"✓ 添加Resources到模型搜索路径: {resources_dir}") - - # 添加core目录到搜索路径 - core_dir = os.path.join(project_root, "core") - core_filename = Filename.from_os_specific(core_dir) - if not model_path.findFile(core_filename): - model_path.appendDirectory(core_filename) - print(f"✓ 添加core目录到模型搜索路径: {core_dir}") - - # 添加RenderPipeline目录到搜索路径 - rp_dir = os.path.join(project_root, "RenderPipelineFile") - rp_filename = Filename.from_os_specific(rp_dir) - if not model_path.findFile(rp_filename): - model_path.appendDirectory(rp_filename) - print(f"✓ 添加RenderPipeline目录到模型搜索路径: {rp_dir}") - - # 添加RenderPipeline data目录到搜索路径 - rp_data_dir = os.path.join(rp_dir, "data") - rp_data_filename = Filename.from_os_specific(rp_data_dir) - if not model_path.findFile(rp_data_filename): - model_path.appendDirectory(rp_data_filename) - print(f"✓ 添加RenderPipeline data目录到模型搜索路径: {rp_data_dir}") - - # 同时添加各个子目录到搜索路径 - subdirs = ['models', 'textures', 'animations', 'icons', 'materials'] - for subdir in subdirs: - subdir_path = os.path.join(resources_dir, subdir) - if os.path.exists(subdir_path): - subdir_filename = Filename.from_os_specific(subdir_path) - if not model_path.findFile(subdir_filename): - model_path.appendDirectory(subdir_filename) - print(f"✓ 添加子目录到搜索路径: {subdir}") - else: - # 创建不存在的子目录 - os.makedirs(subdir_path, exist_ok=True) - subdir_filename = Filename.from_os_specific(subdir_path) - model_path.appendDirectory(subdir_filename) - print(f"✓ 创建并添加子目录: {subdir}") - - # 设置纹理搜索路径 - from panda3d.core import getTexturePath - texture_path = getTexturePath() - if not texture_path.findFile(resources_filename): - texture_path.appendDirectory(resources_filename) - - for subdir in ['textures', 'materials', 'icons']: - subdir_path = os.path.join(resources_dir, subdir) - if os.path.exists(subdir_path): - subdir_filename = Filename.from_os_specific(subdir_path) - if not texture_path.findFile(subdir_filename): - texture_path.appendDirectory(subdir_filename) - - print(f"✓ 资源路径设置完成") - print(f" 项目根目录: {project_root}") - print(f" Resources目录: {resources_dir}") - - except Exception as e: - print(f"⚠️ 设置资源路径失败: {e}") - - def diagnose_fbx_loading(self, fbx_path): - """诊断FBX加载状态""" - print("=== FBX加载诊断 ===") - - # 检查文件存在 - import os - if not os.path.exists(fbx_path): - print("❌ FBX文件不存在") - return None - - try: - # 尝试加载 - actor = Actor(fbx_path) - - # 检查动画 - anims = actor.getAnimNames() - print(f"✓ 找到 {len(anims)} 个动画: {anims}") - - # 检查PartBundle - bundles = actor.getPartBundles() - print(f"✓ 找到 {len(bundles)} 个PartBundle") - - # 测试动画控制器 - if anims: - control = actor.getAnimControl(anims[0]) - if control: - print(f"✓ 动画控制器创建成功: {anims[0]}") - return actor - else: - print(f"❌ 动画控制器创建失败: {anims[0]}") - - return actor - - except Exception as e: - - print(f"❌ FBX加载异常: {e}") - - return None - - - def diagnose_actor_animation(self,model_path, anim_path, anim_name="walk"): - print("=== 开始诊断 Actor 动画问题 ===") - import os - from direct.actor.Actor import Actor - from panda3d.core import Loader - from panda3d.core import Filename - - # 检查文件存在性 - print(f"1. 文件检查:") - print(f" 模型文件存在: {os.path.exists(model_path)}") - print(f" 动画文件存在: {os.path.exists(anim_path)}") - - if not os.path.exists(model_path): - print("❌ 模型文件不存在,请检查路径") - return None - if not os.path.exists(anim_path): - print("❌ 动画文件不存在,请检查路径") - return None - # 2. 检查模型结构 - print(f"2. 模型结构检查:") - try: - # 先用普通loader加载检查结构 - temp_model = self.loader.loadModel(model_path) - if temp_model is None: - print("❌ 无法加载模型文件") - return None - - # 检查Character节点 - bundleNP = temp_model.find("**/+Character") - if bundleNP.isEmpty(): - print("❌ 模型不包含Character节点 - 这是动画模型必需的") - print(" 建议: 确保模型文件是正确的角色模型,包含骨骼结构") - return None - else: - print("✓ 模型包含Character节点") - - temp_model.removeNode() # 清理临时模型 - except Exception as e: - print(f"❌ 模型加载失败: {e}") - return None - # 3. 检查动画文件结构 - print(f"3. 动画文件检查:") - try: - temp_anim = self.loader.loadModel(anim_path) - if temp_anim is None: - print("❌ 无法加载动画文件") - return None - - # 检查AnimBundleNode - animBundleNP = temp_anim.find('**/+AnimBundleNode') - if animBundleNP.isEmpty(): - print("❌ 动画文件不包含AnimBundleNode") - print(" 建议: 确保动画文件是正确导出的动画数据") - return None - else: - print("✓ 动画文件包含AnimBundleNode") - - temp_anim.removeNode() # 清理临时动画 - except Exception as e: - print(f"❌ 动画文件加载失败: {e}") - # 4. 尝试创建Actor - print(f"4. Actor创建测试:") - - # 方法1: 构造函数方式 - try: - print(" 尝试方法1: 构造函数加载") - actor = Actor(model_path, {anim_name: anim_path}) - - print(f" 动画列表: {actor.getAnimNames()}") - - # 强制同步绑定 - print(" 尝试强制绑定动画...") - actor.bindAnim(anim_name, allowAsyncBind=False) - - control = actor.getAnimControl(anim_name) - if control is not None: - print("✓ 方法1成功 - 动画控制器创建成功") - return actor - else: - print("❌ 方法1失败 - 动画控制器为None") - - except Exception as e: - print(f"❌ 方法1异常: {e}") - - # 方法2: 分步加载 - try: - print(" 尝试方法2: 分步加载") - actor = Actor() - actor.loadModel(model_path) - actor.loadAnims({anim_name: anim_path}) - - # 强制绑定 - actor.bindAnim(anim_name, allowAsyncBind=False) - control = actor.getAnimControl(anim_name) - - if control is not None: - print("✓ 方法2成功 - 动画控制器创建成功") - return actor - else: - print("❌ 方法2失败 - 动画控制器为None") - - except Exception as e: - print(f"❌ 方法2异常: {e}") - # 5. 深度诊断 - print(f"5. 深度诊断:") - try: - actor = Actor() - actor.loadModel(model_path) - - # 检查PartBundle - bundles = actor.getPartBundles() - print(f" PartBundle数量: {len(bundles)}") - - if len(bundles) == 0: - print("❌ 没有找到PartBundle - 模型可能不是正确的角色模型") - return None - - # 检查动画绑定过程 - print(" 尝试手动绑定动画...") - actor.loadAnims({anim_name: anim_path}) - - # 获取详细的绑定信息 - actor_info = actor.getActorInfo() - print(f" Actor信息: {actor_info}") - - return None - - except Exception as e: - print(f"❌ 深度诊断异常: {e}") - return None - - print("❌ 所有方法都失败了") - return None - - def _setYCModel(self): - model = self.loader.loadModel("/home/tiger/文档/Tzjyc_GLTF/tzjyc.gltf") - model.reparentTo(self.render) - model.setScale(0.25) - model.setPos(-8, 42, 0) - model.setHpr(0, 90, 0) - - - def _setupCamera(self): - - """设置相机位置和朝向""" - self.cam.setPos(0, -50, 20) - self.cam.lookAt(0, 0, 0) - self.camLens.setFov(80) - self.cam.setTag("is_scene_element", "1") - self.cam.setTag("tree_item_type", "CAMERA_NODE") - print("✓ 相机设置完成") - - def _setupLighting(self): - """设置基础光照系统""" - # 环境光 - alight = AmbientLight('alight') - alight.setColor((0.2, 0.2, 0.2, 1)) - alnp = self.render.attachNewNode(alight) - self.render.setLight(alnp) - - # 定向光(模拟太阳光) - dlight = DirectionalLight('dlight') - dlight.setColor((0.8, 0.8, 0.8, 1)) - dlnp = self.render.attachNewNode(dlight) - dlnp.setHpr(45, -45, 0) # 设置光照方向 - self.render.setLight(dlnp) - - # 保存光源引用 - self.ambient_light = alnp - self.directional_light = dlnp - - print("✓ 光照系统设置完成") - - def _setupGround(self): - """创建地板""" - cm = CardMaker('ground') - cm.setFrame(-50, 50, -50, 50) - - # 创建地板节点 - self.ground = self.render.attachNewNode(cm.generate()) - self.ground.setP(-90) - self.ground.setZ(-1.0) - self.ground.setColor(0.8, 0.8, 0.8, 1) - self.ground.setTag("is_scene_element", "1") - self.ground.setTag("tree_item_type", "SCENE_NODE") - - # 创建支持贴图的材质 - mat = Material() - mat.setName("GroundMaterial") - color = LColor(1, 1, 1, 0.8) - mat.set_base_color(color) - mat.set_roughness(1) # 设置合适的初始粗糙度 - mat.set_metallic(0.5) # 设置较低的初始金属性 - self.ground.set_material(mat) - - # #创建第二个相同的地面,位置稍有偏移 - # self.ground2 = self.render.attachNewNode(cm.generate()) - # self.ground2.setH(-90) - # self.ground2.setZ(-1.0) - # self.ground2.setX(50) # 在X轴方向偏移 - # self.ground2.setZ(49) # 在X轴方向偏移 - # self.ground2.setColor(0.8, 0.8, 0.8, 1) - # self.ground2.set_material(mat) - # self.ground2.setTag("is_scene_element", "1") - # self.ground2.setTag("tree_item_type", "SCENE_NODE") - # - # # 创建第三个相同的地面,位置在另一个方向 - # self.ground3 = self.render.attachNewNode(cm.generate()) - # self.ground3.setH(90) - # self.ground3.setZ(-1.0) - # self.ground3.setX(-50) # 在X轴负方向偏移 - # self.ground3.setZ(49) # 在X轴负方向偏移 - # self.ground3.setColor(0.8, 0.8, 0.8, 1) - # self.ground3.set_material(mat) - # self.ground3.setTag("is_scene_element", "1") - # self.ground3.setTag("tree_item_type", "SCENE_NODE") - # - # self.ground4 = self.render.attachNewNode(cm.generate()) - # # self.ground3.setR(90) - # self.ground4.setZ(-1.0) - # self.ground4.setY(50) # 在X轴负方向偏移 - # self.ground4.setZ(49) # 在X轴负方向偏移 - # self.ground4.setColor(0.8, 0.8, 0.8, 1) - # self.ground4.set_material(mat) - # self.ground4.setTag("is_scene_element", "1") - # self.ground4.setTag("tree_item_type", "SCENE_NODE") - # - # self.ground5 = self.render.attachNewNode(cm.generate()) - # self.ground5.setP(180) - # self.ground5.setZ(-1) - # self.ground5.setY(-50) # 在X轴负方向偏移 - # self.ground5.setZ(49) # 在X轴负方向偏移 - # self.ground5.setColor(0.8, 0.8, 0.8, 1) - # self.ground5.set_material(mat) - # self.ground5.setTag("is_scene_element", "1") - # self.ground5.setTag("tree_item_type", "SCENE_NODE") - # - # self.ground6 = self.render.attachNewNode(cm.generate()) - # self.ground6.setP(90) - # self.ground6.setZ(-1) - # self.ground6.setZ(99) # 在X轴负方向偏移 - # self.ground6.setColor(0.8, 0.8, 0.8, 1) - # self.ground6.set_material(mat) - # self.ground6.setTag("is_scene_element", "1") - # self.ground6.setTag("tree_item_type", "SCENE_NODE") - - # 应用默认PBR效果,确保支持贴图 - try: - if hasattr(self, 'render_pipeline') and self.render_pipeline: - self.render_pipeline.set_effect( - self.ground, - "effects/default.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": False, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 50 - ) - # # 为其他两个地面也应用相同的效果 - # self.render_pipeline.set_effect( - # self.ground2, - # "effects/default.yaml", - # { - # "normal_mapping": True, - # "render_gbuffer": True, - # "alpha_testing": False, - # "parallax_mapping": False, - # "render_shadow": True, - # "render_envmap": True - # }, - # 50 - # ) - # self.render_pipeline.set_effect( - # self.ground3, - # "effects/default.yaml", - # { - # "normal_mapping": True, - # "render_gbuffer": True, - # "alpha_testing": False, - # "parallax_mapping": False, - # "render_shadow": True, - # "render_envmap": True - # }, - # 50 - # ) - # self.render_pipeline.set_effect( - # self.ground4, - # "effects/default.yaml", - # { - # "normal_mapping": True, - # "render_gbuffer": True, - # "alpha_testing": False, - # "parallax_mapping": False, - # "render_shadow": True, - # "render_envmap": True - # }, - # 50 - # ) - # self.render_pipeline.set_effect( - # self.ground5, - # "effects/default.yaml", - # { - # "normal_mapping": True, - # "render_gbuffer": True, - # "alpha_testing": False, - # "parallax_mapping": False, - # "render_shadow": True, - # "render_envmap": True - # }, - # 50 - # ) - # self.render_pipeline.set_effect( - # self.ground6, - # "effects/default.yaml", - # { - # "normal_mapping": True, - # "render_gbuffer": True, - # "alpha_testing": False, - # "parallax_mapping": False, - # "render_shadow": True, - # "render_envmap": True - # }, - # 50 - # ) - # print("✓ 地板PBR效果已应用") - else: - print("⚠️ RenderPipeline未初始化,地板将使用基础渲染") - except Exception as e: - print(f"⚠️ 地板PBR效果应用失败: {e}") - - print("✓ 地板创建完成(支持材质贴图)") - - - - # def _loadFont(self): - # """加载中文字体""" - # try: - # self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') - # if not self.chinese_font: - # print("警告: 无法加载中文字体,将使用默认字体") - # else: - # print("✓ 中文字体加载成功") - # except: - # print("警告: 无法加载中文字体,将使用默认字体") - # self.chinese_font = None - - def _loadFont(self): - """加载中文字体 - 跨平台,Panda3D 友好路径""" - try: - import os - import platform - from pathlib import Path - from panda3d.core import Filename - - self.chinese_font = None # 初始化 - - # --- 平台与项目根 --- - system = platform.system().lower() - project_root = Path(__file__).resolve().parent.parent # 上两级 - - # --- 候选字体路径(按平台) --- - if system == "windows": - win_dir = os.environ.get("WINDIR") or r"C:\Windows" - win_fonts_dir = Path(win_dir) / "Fonts" - font_candidates = [ - project_root / "RenderPipelineFile" / "data" / "font" / "msyh.ttc", - win_fonts_dir / "msyh.ttc", - win_fonts_dir / "msyh.ttf", - win_fonts_dir / "simhei.ttf", - win_fonts_dir / "simsun.ttc", - ] - elif system == "darwin": # macOS - font_candidates = [ - Path("/System/Library/Fonts/PingFang.ttc"), - Path("/System/Library/Fonts/STHeiti.ttc"), - Path("/Library/Fonts/Songti.ttc"), - ] - else: # Linux / 其他 - font_candidates = [ - Path("/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"), - Path("/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc"), - Path("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"), - ] - - # --- 逐个尝试加载 --- - for os_path in font_candidates: - os_path_str = str(os_path) - if not os.path.exists(os_path_str): - # 文件确实不存在就跳过 - continue - - # 1) 先把 OS 路径转换成 Panda 内部路径(/d/... 或 /usr/...) - panda_fn = Filename.fromOsSpecific(os_path_str) - panda_fn.makeTrueCase() # Windows 上修正大小写(更稳) - panda_internal = panda_fn.getFullpath() # 取“内部样式”的字符串 - - # 2) 打印便于对照 Panda 的日志 - try: - print(f"[FontLoader] 尝试加载: {panda_internal}") - except Exception: - pass - - try: - # 3) 传“字符串”给 loader.loadFont(关键修复点) - font = self.loader.loadFont(panda_internal) - if font: - self.chinese_font = font - print("✓ 中文字体加载成功:", os_path_str) - break - else: - print("[FontLoader] 返回空字体对象,继续尝试下一个候选...") - except TypeError as te: - # 某些旧版绑定只接受 str,不接受 Filename;此处已传 str,若仍报错再兜底一次 - try: - font = self.loader.loadFont(str(panda_fn)) # 退一步:str(Filename) - if font: - self.chinese_font = font - print("✓ 中文字体加载成功(备用API):", os_path_str) - break - else: - print("[FontLoader] 备用API仍返回空字体对象,继续…") - except Exception as te2: - print(f"[FontLoader] 加载失败(TypeError),继续下一个: {os_path},原因: {te2}") - except Exception as e: - print(f"[FontLoader] 加载失败,继续下一个: {os_path},原因: {e}") - - # --- 兜底 --- - if not self.chinese_font: - print("警告: 无法加载中文字体,将使用默认字体(可能导致中文显示不全)") - self.chinese_font = None - - except Exception as e: - print(f"警告: 加载中文字体时发生错误: {e}") - self.chinese_font = None - - def setQtWidget(self, widget): - """设置Qt部件引用""" - self.qtWidget = widget - print(f"✓ 设置Qt部件引用: {widget}") - - def getWindowSize(self): - """获取准确的窗口尺寸""" - if self.qtWidget: - # 优先使用Qt部件的实际尺寸 - width, height = self.qtWidget.getActualSize() - if width > 0 and height > 0: - return width, height - - # 备用方案:使用Panda3D窗口尺寸 - if hasattr(self, 'win') and self.win: - width = self.win.getXSize() - height = self.win.getYSize() - print(f"从Panda3D窗口获取尺寸!!!!!!!!!!!!!!!!!!!!!: {width} x {height}") - return width, height - - # 最后的默认值 - print("使用默认窗口尺寸: 800 x 600") - return 800, 600 - - # ==================== 相机控制功能 ==================== - - def wheelForward(self, data=None): - """处理滚轮向前滚动(前进)""" - # 获取相机的前向向量 - forward = self.cam.getMat().getRow3(1) - # 计算移动距离 - distance = self.cameraSpeed * globalClock.getDt() - # 更新相机位置 - currentPos = self.cam.getPos() - newPos = currentPos + forward * distance - self.cam.setPos(newPos) - - def wheelBackward(self, data=None): - """处理滚轮向后滚动(后退)""" - # 获取相机的前向向量 - forward = self.cam.getMat().getRow3(1) - # 计算移动距离 - distance = self.cameraSpeed * globalClock.getDt() - # 更新相机位置 - currentPos = self.cam.getPos() - newPos = currentPos - forward * distance - self.cam.setPos(newPos) - - def moveCamera(self, x, y, z): - """移动相机位置(垂直移动)""" - # 获取相机的上向量 - upVector = self.cam.getMat().getRow3(2) - # 计算移动距离 - distance = self.cameraSpeed * globalClock.getDt() - # 更新相机位置 - currentPos = self.cam.getPos() - newPos = currentPos + upVector * z * distance - self.cam.setPos(newPos) - - # ==================== 鼠标事件处理 ==================== - - def mousePressEventRight(self, evt): - """处理鼠标右键按下事件""" - #print("右键按下") - self.mouseRightPressed = True - self.lastMouseX = evt['x'] - self.lastMouseY = evt['y'] - # - # # 通过 Qt 窗口隐藏光标并捕获鼠标 - # try: - # if hasattr(self, 'qtWidget') and self.qtWidget: - # from PyQt5.QtCore import Qt - # self.qtWidget.setCursor(Qt.BlankCursor) - # # 捕获鼠标,使其无法离开窗口 - # self.qtWidget.grabMouse() - # except Exception as e: - # print(f"通过 Qt 隐藏光标时出错: {e}") - - def mouseReleaseEventRight(self, evt): - """处理鼠标右键释放事件""" - #print("右键释放") - self.mouseRightPressed = False - - # # 恢复 Qt 窗口光标并释放鼠标捕获 - # try: - # if hasattr(self, 'qtWidget') and self.qtWidget: - # from PyQt5.QtCore import Qt - # self.qtWidget.unsetCursor() # 恢复默认光标 - # # 释放鼠标捕获 - # self.qtWidget.releaseMouse() - # except Exception as e: - # print(f"恢复 Qt 光标时出错: {e}") - - def mouseMoveEvent(self, evt): - """处理鼠标移动事件 - 只处理相机旋转""" - if not evt: - return - - if self.mouseRightPressed: - # 计算鼠标移动距离 - dx = evt.get('x', 0) - self.lastMouseX - dy = evt.get('y', 0) - self.lastMouseY - - # 计算旋转角度 - rotateSpeed = self.cameraRotateSpeed * globalClock.getDt() - - # 更新相机朝向 - currentH = self.cam.getH() - currentP = self.cam.getP() - - # 限制俯仰角度在-90到90度之间 - newP = max(-90, min(90, currentP - dy * rotateSpeed)) - - self.cam.setH(currentH - dx * rotateSpeed) - self.cam.setP(newP) - - # 更新鼠标位置 - self.lastMouseX = evt.get('x', 0) - self.lastMouseY = evt.get('y', 0) - - # ==================== 其他基础功能 ==================== - - def getChineseFont(self): - """获取中文字体""" - return self.chinese_font - - def getGroundNode(self): - """获取地板节点""" - return self.ground - - def getAmbientLight(self): - """获取环境光""" - return self.ambient_light - - def getDirectionalLight(self): - """获取定向光""" - return self.directional_light - - def start_day_night_cycle(self, duration_seconds=10.0): - """让天空盒在 duration_seconds 秒内从 0:00 过渡到 24:00""" - self._cycle_start_time = self.taskMgr.globalClock.get_real_time() - self._cycle_duration = duration_seconds - self.taskMgr.add(self._day_night_cycle_task, "day_night_cycle_task") - - def _day_night_cycle_task(self, task): - elapsed = self.taskMgr.globalClock.get_real_time() - self._cycle_start_time - t = (elapsed % self._cycle_duration) / self._cycle_duration # 始终在 0~1 循环 - self.render_pipeline.daytime_mgr.time = 24.0 * t - return task.cont - - - def set_daytime(self, time_str): - """设置时间""" - try: - if hasattr(self, 'render_pipeline') and self.render_pipeline: - self.render_pipeline.daytime_mgr.time = time_str - print(f"当前时间设置为: {time_str}") - else: - print(f"⚠️ RenderPipeline 不可用,无法设置时间: {time_str}") - except Exception as e: - print(f"设置时间失败: {e}") - - def get_render_pipeline(self): - """获取 RenderPipeline 实例""" - return getattr(self, 'render_pipeline', None) - - def _setupSkybox(self): - # 加载天空盒模型 - self.skybox = self.loader.loadModel("data/builtin_models/skybox/skybox.bam") - self.skybox.reparentTo(self.camera) # 绑定到相机 - self.skybox.setScale(500) - self.skybox.setBin('background', 0) - self.skybox.setDepthWrite(False) - self.skybox.setLightOff() - self.skybox.setCompass() # 始终朝向固定 - - print("✓ 静态天空盒加载完成") - - def createDirectionalLight(self): - from RenderPipelineFile.rpcore import light_manager - from panda3d.core import DirectionalLight, Vec3 - dlight = DirectionalLight("1") - - dlight_np = self.render.attachNewNode(dlight) - #light_manager.add_light(dlight) - - dlight_np.setHpr(45,-45,0) - - self.render.setLight(dlight_np) - - dlight.setColor((1,1,1,1)) - dlight.setShadowCaster(True,2048,2048) - - print("平行光创建完成") - - # dlight.direction = Vec3(0, 0, -1) # 光照方向 - # dlight.fov = self.lamp_fov # 光源角度(类似手电筒) - # dlight.set_color_from_temperature(1 * 1000.0) # 色温(K) - # dlight.energy = self.half_energy # 光照强度 - # dlight.radius = self.lamp_radius # 影响范围 - # dlight.casts_shadows = True # 是否投射阴影 - # dlight.shadow_map_resolution = 256 # 阴影分辨率 - # dlight.setPos(0,0,10) - - def check_material_editor_connection(self): - """检查材质编辑器连接状态""" - try: - # 确保 RenderPipeline 已完全初始化 - if not hasattr(self, 'render_pipeline') or not self.render_pipeline: - print("RenderPipeline 未初始化") - return False - - # 检查网络监听器是否存在 - if not hasattr(self.render_pipeline, '_listener'): - print("NetworkCommunication 监听器未初始化") - return False - - from RenderPipelineFile.rpcore.util.network_communication import NetworkCommunication - import tempfile - import os - import time - - # 使用唯一的测试文件名 - import uuid - test_filename = f"test_materials_{uuid.uuid4().hex[:8]}.data" - temp_path = os.path.join(tempfile.gettempdir(), test_filename) - - print(f"测试材质编辑器连接,文件路径: {temp_path}") - - # 确保测试文件不存在 - if os.path.exists(temp_path): - os.remove(temp_path) - - # 发送导出命令 - NetworkCommunication.send_async( - NetworkCommunication.MATERIAL_PORT, - f"dump_materials {temp_path}" - ) - - # 大幅增加等待时间,因为网络命令处理可能有延迟 - for i in range(60): # 等待最多30秒 - time.sleep(0.5) - if os.path.exists(temp_path): - # 等待文件写入完成 - time.sleep(1.0) - try: - with open(temp_path, 'r') as f: - content = f.read().strip() - print(f"材质编辑器连接测试成功,文件内容: {len(content)} 字符") - os.remove(temp_path) - return True - except: - print("文件读取失败,继续等待...") - continue - - if i % 20 == 0 and i > 0: # 每10秒打印一次状态 - print(f"等待材质文件创建... ({i // 2}s)") - - print("材质编辑器连接测试超时") - return False - - except Exception as e: - print(f"材质编辑器连接测试出错: {e}") - return False - - - def create_material_editor_widget(self): - """创建材质编辑器组件""" - try: - # 确保 RenderPipeline 已完全初始化 - if not hasattr(self, 'render_pipeline') or not self.render_pipeline: - print("RenderPipeline 未初始化") - return None - - # 检查网络连接 - if not self.check_material_editor_connection(): - print("无法连接到材质编辑器网络服务") - return None - - # 创建材质编辑器组件 - from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel - - material_widget = QWidget() - layout = QVBoxLayout() - - # 添加基本的材质编辑控件 - layout.addWidget(QLabel("材质编辑器")) - # 这里可以添加更多的材质编辑控件 - - material_widget.setLayout(layout) - return material_widget - - except Exception as e: - print(f"创建材质编辑器失败: {e}") - return None - - def _delayed_material_test(self, task): - """延迟执行的材质编辑器连接测试""" - print("开始延迟材质编辑器连接测试...") - success = self.check_material_editor_connection() - if success: - print("✓ 材质编辑器连接正常") - else: - print("✗ 材质编辑器连接失败") - return task.done - - def launch_day_time_editor(self): - - # 检查是否已经启动 - if hasattr(self, '_day_time_editor_process') and self._day_time_editor_process: - if self._day_time_editor_process.poll() is None: # 进程仍在运行 - print("Day Time Editor 已经在运行") - return True - import subprocess - import os - import sys - - try: - if not hasattr(self.world,'render_pipeline') or not self.world.render_pipeline: - print("错误:renderpipeline未初始化") - return False - - base_path = self.world.render_pipeline.mount_mgr.base_path - editor_path = os.path.join(base_path,"toolkit/day_time_editor/main.py") - - if not os.path.exists(editor_path): - print("错误文件不存在") - return False - - self._day_time_editor_process = subprocess.Popen([sys.executable, editor_path]) - print("Day Time Editor 已启动") - return True - except Exception as e: - print(f"启动 time editor失败") - - def setup_material_editor_network(self): - """设置材质编辑器网络通信""" - from RenderPipelineFile.rpcore.util.network_communication import NetworkCommunication - - def handle_material_requests(message): - """处理材质编辑器的网络请求""" - try: - print(f"收到材质编辑器请求: {message}") - parts = message.strip().split() - if not parts: - return - - command = parts[0] - - if command == "dump_materials": - # 处理导出材质列表请求 - path = parts[1] if len(parts) > 1 else "" - if path: - self.export_materials_to_file(path) - elif command.startswith("update_material"): - # 处理更新材质请求 - data = message[len("update_material "):].strip() - self.update_material_from_editor(data.split()) - - except Exception as e: - print(f"处理材质编辑器请求失败: {e}") - - # 注册网络消息处理器 - try: - NetworkCommunication.listen_threaded( - NetworkCommunication.MATERIAL_PORT, - handle_material_requests - ) - print("✓ 材质编辑器网络通信已设置") - except Exception as e: - print(f"设置材质编辑器网络通信失败: {e}") - - def export_materials_to_file(self, path): - """导出材质列表到文件""" - try: - print(f"导出材质列表到: {path}") - materials = [] - - # 收集所有材质 - def collect_materials(node): - if node.hasMaterial(): - material = node.getMaterial() - if material: - materials.append(material) - - for child in node.getChildren(): - collect_materials(child) - - collect_materials(self.render) - - # 写入文件 - with open(path, "w") as f: - for i, material in enumerate(materials): - name = f"{i}-{material.getName() or 'unnamed'}" - - # 获取材质属性,使用默认值如果不存在 - base_color = material.getBaseColor() if hasattr(material, 'getBaseColor') else Vec4(0.6, 0.6, 0.6, 1.0) - roughness = material.getRoughness() if hasattr(material, 'getRoughness') else 0.5 - metallic = material.getMetallic() if hasattr(material, 'getMetallic') else 0.0 - - # 写入材质数据 - f.write(("{} " * 11).format( - name, - base_color.x, - base_color.y, - base_color.z, - roughness, - 0.5, # refractive_index - metallic, - 0, # shading_model - 1.0, # normal_strength - 0.0, # param1 - 0.0 # param2 - ) + "\n") - - print(f"✓ 成功导出 {len(materials)} 个材质到 {path}") - - except Exception as e: - print(f"导出材质列表失败: {e}") - - def update_material_from_editor(self, data): - """从编辑器更新材质""" - try: - if len(data) < 11: - print(f"材质数据不完整: {data}") - return - - name_parts = data[0].split("-") - if len(name_parts) < 2: - print(f"材质名称格式错误: {data[0]}") - return - - index = int(name_parts[0]) - name = "-".join(name_parts[1:]) - - # 收集所有材质 - materials = [] - def collect_materials(node): - if node.hasMaterial(): - material = node.getMaterial() - if material: - materials.append(material) - - for child in node.getChildren(): - collect_materials(child) - - collect_materials(self.render) - - # 查找匹配的材质 - if index < len(materials): - material = materials[index] - - # 更新材质属性 - material.setBaseColor(Vec4(float(data[1]), float(data[2]), float(data[3]), 1.0)) - material.setRoughness(float(data[4])) - material.setMetallic(float(data[6])) - - print(f"✓ 更新材质 {name}: 颜色=({data[1]}, {data[2]}, {data[3]}), 粗糙度={data[4]}, 金属度={data[6]}") - else: - print(f"未找到索引为 {index} 的材质") - - except Exception as e: - print(f"更新材质失败: {e}") - - def launch_material_editor(self): - """启动材质编辑器""" - import subprocess - import os - import sys - - try: - if not self.render_pipeline: - print("错误:renderpipeline未初始化") - return False - - base_path = self.render_pipeline.mount_mgr.base_path - editor_path = os.path.join(base_path,"toolkit/material_editor/main.py") - - if not os.path.exists(editor_path): - print("错误文件不存在") - return False - - self._material_editor_process = subprocess.Popen([sys.executable, editor_path]) - print("Material Editor 已启动") - return True - except Exception as e: - print(f"启动 Material editor失败") - - def create_sample_materials(self): - """创建一些示例材质供编辑器使用""" - from panda3d.core import Material, Vec4, CardMaker - - # 创建几个测试几何体,每个都有不同的材质 - sample_materials = [ - {"name": "MetalMaterial", "color": Vec4(0.7, 0.7, 0.8, 1.0), "metallic": True, "roughness": 0.2}, - {"name": "PlasticMaterial", "color": Vec4(0.8, 0.2, 0.2, 1.0), "metallic": False, "roughness": 0.8}, - {"name": "GlassMaterial", "color": Vec4(0.9, 0.9, 1.0, 0.3), "metallic": False, "roughness": 0.1}, - {"name": "WoodMaterial", "color": Vec4(0.6, 0.4, 0.2, 1.0), "metallic": False, "roughness": 0.7}, - {"name": "TestPlaneMaterial", "color": Vec4(0.8, 0.8, 0.8, 1.0), "metallic": False, "roughness": 1.0} - ] - - for i, mat_data in enumerate(sample_materials): - # 创建几何体 - if mat_data["name"] == "TestPlaneMaterial": - # 创建一个大的测试平面,类似Blender中的平面 - cm = CardMaker('test_plane') - cm.setFrame(-5, 5, -5, 5) # 更大的平面 - geom_node = self.render.attachNewNode(cm.generate()) - geom_node.setPos(0, 15, 0) # 放在前面显眼位置 - geom_node.setP(-90) # 水平放置 - print("✓ 创建大型测试平面,适合测试粗糙度贴图") - else: - cm = CardMaker(f'sample_geom_{i}') - cm.setFrame(-1, 1, -1, 1) - geom_node = self.render.attachNewNode(cm.generate()) - geom_node.setPos(i * 3 - 6, 10, 1) - geom_node.setP(-90) # 水平放置 - - # 创建材质并确保名称正确设置 - material = Material() - material.setName(mat_data["name"]) # 明确设置材质名称 - material.setBaseColor(mat_data["color"]) - material.setRoughness(mat_data["roughness"]) - material.setMetallic(1.0 if mat_data["metallic"] else 0.0) - - # 应用材质 - geom_node.setMaterial(material) - - print(f"✓ 创建示例材质: {mat_data['name']}") - - # 延迟一下确保材质完全创建 - import time - time.sleep(0.1) - - def load_test_models_with_materials(self): - """加载一些测试模型以提供更多材质供编辑""" - try: - # 你可以在这里加载你的模型文件 - # test_model = self.loader.loadModel("models/your_model.gltf") - # if test_model: - # test_model.reparentTo(self.render) - # test_model.setPos(5, 0, 0) - # test_model.setScale(1.0) - # print(f"✓ 测试模型已加载") - - # 创建示例材质 - self.create_sample_materials() - - except Exception as e: - print(f"加载测试模型失败: {e}") - # 创建示例材质作为备选 - self.create_sample_materials() - - def launch_plugin_configurator(self): - """启动材质编辑器""" - import subprocess - import os - import sys - - try: - if not self.render_pipeline: - print("错误:renderpipeline未初始化") - return False - - base_path = self.render_pipeline.mount_mgr.base_path - editor_path = os.path.join(base_path, "toolkit/plugin_configurator/main.py") - - if not os.path.exists(editor_path): - print("错误文件不存在") - return False - - self._material_editor_process = subprocess.Popen([sys.executable, editor_path]) - print("plugin_configurator 已启动") - return True - except Exception as e: - print(f"启动plugin_configurator失败") - + +import math +import warnings + +from direct.actor.Actor import Actor + +warnings.filterwarnings("ignore", category=DeprecationWarning) + +from QMeta3D.Meta3DWorld import Meta3DWorld +from panda3d.core import (CardMaker, Vec4, Vec3, AmbientLight, DirectionalLight, + Point3, WindowProperties,Material,LColor) +from direct.showbase.ShowBaseGlobal import globalClock +from scene.scene_manager import SceneManager + +# 尝试导入插件管理器(如果存在) +try: + from plugins.plugin_manager import PluginManager + PLUGIN_SUPPORT = True +except ImportError: + PLUGIN_SUPPORT = False + print("⚠ 插件管理器未找到,插件功能将不可用") + +class CoreWorld(Meta3DWorld): + """核心世界功能类 - 负责基础的3D世界设置和核心功能""" + + def __init__(self): + super().__init__() + + # 初始化基础属性 + self.qtWidget = None # Qt部件引用(用于获取准确的渲染区域尺寸) + + # 设置相机控制参数 + #self.cameraSpeed = 200.0 # 移动速度 + self.cameraSpeed=20.0 + #self.cameraRotateSpeed = 40.0 # 旋转速度 + self.cameraRotateSpeed = 10.0 + + # 鼠标控制相关变量 + self.lastMouseX = 0 + self.lastMouseY = 0 + self.mouseRightPressed = False + + # 初始化插件管理器(如果支持) + if PLUGIN_SUPPORT: + self.plugin_manager = PluginManager(self) + else: + self.plugin_manager = None + + # 初始化世界 + self._setupResourcePaths() + self._setupCamera() + self._setupLighting() + self._setupGround() + self._loadFont() + + def _setupResourcePaths(self): + """设置Panda3D资源搜索路径,确保能正确找到Resources文件夹中的模型和贴图""" + try: + import os + from panda3d.core import getModelPath, DSearchPath, Filename + + # 获取项目根目录 + project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + resources_dir = os.path.join(project_root, "Resources") + + # 确保Resources目录存在 + if not os.path.exists(resources_dir): + os.makedirs(resources_dir, exist_ok=True) + print(f"✓ 创建Resources目录: {resources_dir}") + + # 添加Resources目录到Panda3D模型搜索路径 + model_path = getModelPath() + resources_filename = Filename.from_os_specific(resources_dir) + + # 检查路径是否已存在,避免重复添加 + if not model_path.findFile(resources_filename): + model_path.appendDirectory(resources_filename) + print(f"✓ 添加Resources到模型搜索路径: {resources_dir}") + + # 添加core目录到搜索路径 + core_dir = os.path.join(project_root, "core") + core_filename = Filename.from_os_specific(core_dir) + if not model_path.findFile(core_filename): + model_path.appendDirectory(core_filename) + print(f"✓ 添加core目录到模型搜索路径: {core_dir}") + + # 添加RenderPipeline目录到搜索路径 + rp_dir = os.path.join(project_root, "RenderPipelineFile") + rp_filename = Filename.from_os_specific(rp_dir) + if not model_path.findFile(rp_filename): + model_path.appendDirectory(rp_filename) + print(f"✓ 添加RenderPipeline目录到模型搜索路径: {rp_dir}") + + # 添加RenderPipeline data目录到搜索路径 + rp_data_dir = os.path.join(rp_dir, "data") + rp_data_filename = Filename.from_os_specific(rp_data_dir) + if not model_path.findFile(rp_data_filename): + model_path.appendDirectory(rp_data_filename) + print(f"✓ 添加RenderPipeline data目录到模型搜索路径: {rp_data_dir}") + + # 同时添加各个子目录到搜索路径 + subdirs = ['models', 'textures', 'animations', 'icons', 'materials'] + for subdir in subdirs: + subdir_path = os.path.join(resources_dir, subdir) + if os.path.exists(subdir_path): + subdir_filename = Filename.from_os_specific(subdir_path) + if not model_path.findFile(subdir_filename): + model_path.appendDirectory(subdir_filename) + print(f"✓ 添加子目录到搜索路径: {subdir}") + else: + # 创建不存在的子目录 + os.makedirs(subdir_path, exist_ok=True) + subdir_filename = Filename.from_os_specific(subdir_path) + model_path.appendDirectory(subdir_filename) + print(f"✓ 创建并添加子目录: {subdir}") + + # 设置纹理搜索路径 + from panda3d.core import getTexturePath + texture_path = getTexturePath() + if not texture_path.findFile(resources_filename): + texture_path.appendDirectory(resources_filename) + + for subdir in ['textures', 'materials', 'icons']: + subdir_path = os.path.join(resources_dir, subdir) + if os.path.exists(subdir_path): + subdir_filename = Filename.from_os_specific(subdir_path) + if not texture_path.findFile(subdir_filename): + texture_path.appendDirectory(subdir_filename) + + print(f"✓ 资源路径设置完成") + print(f" 项目根目录: {project_root}") + print(f" Resources目录: {resources_dir}") + + except Exception as e: + print(f"⚠️ 设置资源路径失败: {e}") + + def diagnose_fbx_loading(self, fbx_path): + """诊断FBX加载状态""" + print("=== FBX加载诊断 ===") + + # 检查文件存在 + import os + if not os.path.exists(fbx_path): + print("❌ FBX文件不存在") + return None + + try: + # 尝试加载 + actor = Actor(fbx_path) + + # 检查动画 + anims = actor.getAnimNames() + print(f"✓ 找到 {len(anims)} 个动画: {anims}") + + # 检查PartBundle + bundles = actor.getPartBundles() + print(f"✓ 找到 {len(bundles)} 个PartBundle") + + # 测试动画控制器 + if anims: + control = actor.getAnimControl(anims[0]) + if control: + print(f"✓ 动画控制器创建成功: {anims[0]}") + return actor + else: + print(f"❌ 动画控制器创建失败: {anims[0]}") + + return actor + + except Exception as e: + + print(f"❌ FBX加载异常: {e}") + + return None + + + def diagnose_actor_animation(self,model_path, anim_path, anim_name="walk"): + print("=== 开始诊断 Actor 动画问题 ===") + import os + from direct.actor.Actor import Actor + from panda3d.core import Loader + from panda3d.core import Filename + + # 检查文件存在性 + print(f"1. 文件检查:") + print(f" 模型文件存在: {os.path.exists(model_path)}") + print(f" 动画文件存在: {os.path.exists(anim_path)}") + + if not os.path.exists(model_path): + print("❌ 模型文件不存在,请检查路径") + return None + if not os.path.exists(anim_path): + print("❌ 动画文件不存在,请检查路径") + return None + # 2. 检查模型结构 + print(f"2. 模型结构检查:") + try: + # 先用普通loader加载检查结构 + temp_model = self.loader.loadModel(model_path) + if temp_model is None: + print("❌ 无法加载模型文件") + return None + + # 检查Character节点 + bundleNP = temp_model.find("**/+Character") + if bundleNP.isEmpty(): + print("❌ 模型不包含Character节点 - 这是动画模型必需的") + print(" 建议: 确保模型文件是正确的角色模型,包含骨骼结构") + return None + else: + print("✓ 模型包含Character节点") + + temp_model.removeNode() # 清理临时模型 + except Exception as e: + print(f"❌ 模型加载失败: {e}") + return None + # 3. 检查动画文件结构 + print(f"3. 动画文件检查:") + try: + temp_anim = self.loader.loadModel(anim_path) + if temp_anim is None: + print("❌ 无法加载动画文件") + return None + + # 检查AnimBundleNode + animBundleNP = temp_anim.find('**/+AnimBundleNode') + if animBundleNP.isEmpty(): + print("❌ 动画文件不包含AnimBundleNode") + print(" 建议: 确保动画文件是正确导出的动画数据") + return None + else: + print("✓ 动画文件包含AnimBundleNode") + + temp_anim.removeNode() # 清理临时动画 + except Exception as e: + print(f"❌ 动画文件加载失败: {e}") + # 4. 尝试创建Actor + print(f"4. Actor创建测试:") + + # 方法1: 构造函数方式 + try: + print(" 尝试方法1: 构造函数加载") + actor = Actor(model_path, {anim_name: anim_path}) + + print(f" 动画列表: {actor.getAnimNames()}") + + # 强制同步绑定 + print(" 尝试强制绑定动画...") + actor.bindAnim(anim_name, allowAsyncBind=False) + + control = actor.getAnimControl(anim_name) + if control is not None: + print("✓ 方法1成功 - 动画控制器创建成功") + return actor + else: + print("❌ 方法1失败 - 动画控制器为None") + + except Exception as e: + print(f"❌ 方法1异常: {e}") + + # 方法2: 分步加载 + try: + print(" 尝试方法2: 分步加载") + actor = Actor() + actor.loadModel(model_path) + actor.loadAnims({anim_name: anim_path}) + + # 强制绑定 + actor.bindAnim(anim_name, allowAsyncBind=False) + control = actor.getAnimControl(anim_name) + + if control is not None: + print("✓ 方法2成功 - 动画控制器创建成功") + return actor + else: + print("❌ 方法2失败 - 动画控制器为None") + + except Exception as e: + print(f"❌ 方法2异常: {e}") + # 5. 深度诊断 + print(f"5. 深度诊断:") + try: + actor = Actor() + actor.loadModel(model_path) + + # 检查PartBundle + bundles = actor.getPartBundles() + print(f" PartBundle数量: {len(bundles)}") + + if len(bundles) == 0: + print("❌ 没有找到PartBundle - 模型可能不是正确的角色模型") + return None + + # 检查动画绑定过程 + print(" 尝试手动绑定动画...") + actor.loadAnims({anim_name: anim_path}) + + # 获取详细的绑定信息 + actor_info = actor.getActorInfo() + print(f" Actor信息: {actor_info}") + + return None + + except Exception as e: + print(f"❌ 深度诊断异常: {e}") + return None + + print("❌ 所有方法都失败了") + return None + + def _setYCModel(self): + model = self.loader.loadModel("/home/tiger/文档/Tzjyc_GLTF/tzjyc.gltf") + model.reparentTo(self.render) + model.setScale(0.25) + model.setPos(-8, 42, 0) + model.setHpr(0, 90, 0) + + + def _setupCamera(self): + + """设置相机位置和朝向""" + self.cam.setPos(0, -50, 20) + self.cam.lookAt(0, 0, 0) + self.camLens.setFov(80) + self.cam.setTag("is_scene_element", "1") + self.cam.setTag("tree_item_type", "CAMERA_NODE") + print("✓ 相机设置完成") + + def _setupLighting(self): + """设置基础光照系统""" + # 环境光 + alight = AmbientLight('alight') + alight.setColor((0.2, 0.2, 0.2, 1)) + alnp = self.render.attachNewNode(alight) + self.render.setLight(alnp) + + # 定向光(模拟太阳光) + dlight = DirectionalLight('dlight') + dlight.setColor((0.8, 0.8, 0.8, 1)) + dlnp = self.render.attachNewNode(dlight) + dlnp.setHpr(45, -45, 0) # 设置光照方向 + self.render.setLight(dlnp) + + # 保存光源引用 + self.ambient_light = alnp + self.directional_light = dlnp + + print("✓ 光照系统设置完成") + + def _setupGround(self): + """创建地板""" + cm = CardMaker('ground') + cm.setFrame(-50, 50, -50, 50) + + # 创建地板节点 + self.ground = self.render.attachNewNode(cm.generate()) + self.ground.setP(-90) + self.ground.setZ(-1.0) + self.ground.setColor(0.8, 0.8, 0.8, 1) + self.ground.setTag("is_scene_element", "1") + self.ground.setTag("tree_item_type", "SCENE_NODE") + + # 创建支持贴图的材质 + mat = Material() + mat.setName("GroundMaterial") + color = LColor(1, 1, 1, 0.8) + mat.set_base_color(color) + mat.set_roughness(1) # 设置合适的初始粗糙度 + mat.set_metallic(0.5) # 设置较低的初始金属性 + self.ground.set_material(mat) + + # #创建第二个相同的地面,位置稍有偏移 + # self.ground2 = self.render.attachNewNode(cm.generate()) + # self.ground2.setH(-90) + # self.ground2.setZ(-1.0) + # self.ground2.setX(50) # 在X轴方向偏移 + # self.ground2.setZ(49) # 在X轴方向偏移 + # self.ground2.setColor(0.8, 0.8, 0.8, 1) + # self.ground2.set_material(mat) + # self.ground2.setTag("is_scene_element", "1") + # self.ground2.setTag("tree_item_type", "SCENE_NODE") + # + # # 创建第三个相同的地面,位置在另一个方向 + # self.ground3 = self.render.attachNewNode(cm.generate()) + # self.ground3.setH(90) + # self.ground3.setZ(-1.0) + # self.ground3.setX(-50) # 在X轴负方向偏移 + # self.ground3.setZ(49) # 在X轴负方向偏移 + # self.ground3.setColor(0.8, 0.8, 0.8, 1) + # self.ground3.set_material(mat) + # self.ground3.setTag("is_scene_element", "1") + # self.ground3.setTag("tree_item_type", "SCENE_NODE") + # + # self.ground4 = self.render.attachNewNode(cm.generate()) + # # self.ground3.setR(90) + # self.ground4.setZ(-1.0) + # self.ground4.setY(50) # 在X轴负方向偏移 + # self.ground4.setZ(49) # 在X轴负方向偏移 + # self.ground4.setColor(0.8, 0.8, 0.8, 1) + # self.ground4.set_material(mat) + # self.ground4.setTag("is_scene_element", "1") + # self.ground4.setTag("tree_item_type", "SCENE_NODE") + # + # self.ground5 = self.render.attachNewNode(cm.generate()) + # self.ground5.setP(180) + # self.ground5.setZ(-1) + # self.ground5.setY(-50) # 在X轴负方向偏移 + # self.ground5.setZ(49) # 在X轴负方向偏移 + # self.ground5.setColor(0.8, 0.8, 0.8, 1) + # self.ground5.set_material(mat) + # self.ground5.setTag("is_scene_element", "1") + # self.ground5.setTag("tree_item_type", "SCENE_NODE") + # + # self.ground6 = self.render.attachNewNode(cm.generate()) + # self.ground6.setP(90) + # self.ground6.setZ(-1) + # self.ground6.setZ(99) # 在X轴负方向偏移 + # self.ground6.setColor(0.8, 0.8, 0.8, 1) + # self.ground6.set_material(mat) + # self.ground6.setTag("is_scene_element", "1") + # self.ground6.setTag("tree_item_type", "SCENE_NODE") + + # 应用默认PBR效果,确保支持贴图 + try: + if hasattr(self, 'render_pipeline') and self.render_pipeline: + self.render_pipeline.set_effect( + self.ground, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 50 + ) + # # 为其他两个地面也应用相同的效果 + # self.render_pipeline.set_effect( + # self.ground2, + # "effects/default.yaml", + # { + # "normal_mapping": True, + # "render_gbuffer": True, + # "alpha_testing": False, + # "parallax_mapping": False, + # "render_shadow": True, + # "render_envmap": True + # }, + # 50 + # ) + # self.render_pipeline.set_effect( + # self.ground3, + # "effects/default.yaml", + # { + # "normal_mapping": True, + # "render_gbuffer": True, + # "alpha_testing": False, + # "parallax_mapping": False, + # "render_shadow": True, + # "render_envmap": True + # }, + # 50 + # ) + # self.render_pipeline.set_effect( + # self.ground4, + # "effects/default.yaml", + # { + # "normal_mapping": True, + # "render_gbuffer": True, + # "alpha_testing": False, + # "parallax_mapping": False, + # "render_shadow": True, + # "render_envmap": True + # }, + # 50 + # ) + # self.render_pipeline.set_effect( + # self.ground5, + # "effects/default.yaml", + # { + # "normal_mapping": True, + # "render_gbuffer": True, + # "alpha_testing": False, + # "parallax_mapping": False, + # "render_shadow": True, + # "render_envmap": True + # }, + # 50 + # ) + # self.render_pipeline.set_effect( + # self.ground6, + # "effects/default.yaml", + # { + # "normal_mapping": True, + # "render_gbuffer": True, + # "alpha_testing": False, + # "parallax_mapping": False, + # "render_shadow": True, + # "render_envmap": True + # }, + # 50 + # ) + # print("✓ 地板PBR效果已应用") + else: + print("⚠️ RenderPipeline未初始化,地板将使用基础渲染") + except Exception as e: + print(f"⚠️ 地板PBR效果应用失败: {e}") + + print("✓ 地板创建完成(支持材质贴图)") + + + + # def _loadFont(self): + # """加载中文字体""" + # try: + # self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') + # if not self.chinese_font: + # print("警告: 无法加载中文字体,将使用默认字体") + # else: + # print("✓ 中文字体加载成功") + # except: + # print("警告: 无法加载中文字体,将使用默认字体") + # self.chinese_font = None + + def _loadFont(self): + """加载中文字体 - 跨平台,Panda3D 友好路径""" + try: + import os + import platform + from pathlib import Path + from panda3d.core import Filename + + self.chinese_font = None # 初始化 + + # --- 平台与项目根 --- + system = platform.system().lower() + project_root = Path(__file__).resolve().parent.parent # 上两级 + + # --- 候选字体路径(按平台) --- + if system == "windows": + win_dir = os.environ.get("WINDIR") or r"C:\Windows" + win_fonts_dir = Path(win_dir) / "Fonts" + font_candidates = [ + project_root / "RenderPipelineFile" / "data" / "font" / "msyh.ttc", + win_fonts_dir / "msyh.ttc", + win_fonts_dir / "msyh.ttf", + win_fonts_dir / "simhei.ttf", + win_fonts_dir / "simsun.ttc", + ] + elif system == "darwin": # macOS + font_candidates = [ + Path("/System/Library/Fonts/PingFang.ttc"), + Path("/System/Library/Fonts/STHeiti.ttc"), + Path("/Library/Fonts/Songti.ttc"), + ] + else: # Linux / 其他 + font_candidates = [ + Path("/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"), + Path("/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc"), + Path("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"), + ] + + # --- 逐个尝试加载 --- + for os_path in font_candidates: + os_path_str = str(os_path) + if not os.path.exists(os_path_str): + # 文件确实不存在就跳过 + continue + + # 1) 先把 OS 路径转换成 Panda 内部路径(/d/... 或 /usr/...) + panda_fn = Filename.fromOsSpecific(os_path_str) + panda_fn.makeTrueCase() # Windows 上修正大小写(更稳) + panda_internal = panda_fn.getFullpath() # 取“内部样式”的字符串 + + # 2) 打印便于对照 Panda 的日志 + try: + print(f"[FontLoader] 尝试加载: {panda_internal}") + except Exception: + pass + + try: + # 3) 传“字符串”给 loader.loadFont(关键修复点) + font = self.loader.loadFont(panda_internal) + if font: + self.chinese_font = font + print("✓ 中文字体加载成功:", os_path_str) + break + else: + print("[FontLoader] 返回空字体对象,继续尝试下一个候选...") + except TypeError as te: + # 某些旧版绑定只接受 str,不接受 Filename;此处已传 str,若仍报错再兜底一次 + try: + font = self.loader.loadFont(str(panda_fn)) # 退一步:str(Filename) + if font: + self.chinese_font = font + print("✓ 中文字体加载成功(备用API):", os_path_str) + break + else: + print("[FontLoader] 备用API仍返回空字体对象,继续…") + except Exception as te2: + print(f"[FontLoader] 加载失败(TypeError),继续下一个: {os_path},原因: {te2}") + except Exception as e: + print(f"[FontLoader] 加载失败,继续下一个: {os_path},原因: {e}") + + # --- 兜底 --- + if not self.chinese_font: + print("警告: 无法加载中文字体,将使用默认字体(可能导致中文显示不全)") + self.chinese_font = None + + except Exception as e: + print(f"警告: 加载中文字体时发生错误: {e}") + self.chinese_font = None + + def setQtWidget(self, widget): + """设置Qt部件引用""" + self.qtWidget = widget + print(f"✓ 设置Qt部件引用: {widget}") + + def getWindowSize(self): + """获取准确的窗口尺寸""" + if self.qtWidget: + # 优先使用Qt部件的实际尺寸 + width, height = self.qtWidget.getActualSize() + if width > 0 and height > 0: + return width, height + + # 备用方案:使用Panda3D窗口尺寸 + if hasattr(self, 'win') and self.win: + width = self.win.getXSize() + height = self.win.getYSize() + print(f"从Panda3D窗口获取尺寸!!!!!!!!!!!!!!!!!!!!!: {width} x {height}") + return width, height + + # 最后的默认值 + print("使用默认窗口尺寸: 800 x 600") + return 800, 600 + + # ==================== 相机控制功能 ==================== + + def wheelForward(self, data=None): + """处理滚轮向前滚动(前进)""" + # 获取相机的前向向量 + forward = self.cam.getMat().getRow3(1) + # 计算移动距离 + distance = self.cameraSpeed * globalClock.getDt() + # 更新相机位置 + currentPos = self.cam.getPos() + newPos = currentPos + forward * distance + self.cam.setPos(newPos) + + def wheelBackward(self, data=None): + """处理滚轮向后滚动(后退)""" + # 获取相机的前向向量 + forward = self.cam.getMat().getRow3(1) + # 计算移动距离 + distance = self.cameraSpeed * globalClock.getDt() + # 更新相机位置 + currentPos = self.cam.getPos() + newPos = currentPos - forward * distance + self.cam.setPos(newPos) + + def moveCamera(self, x, y, z): + """移动相机位置(垂直移动)""" + # 获取相机的上向量 + upVector = self.cam.getMat().getRow3(2) + # 计算移动距离 + distance = self.cameraSpeed * globalClock.getDt() + # 更新相机位置 + currentPos = self.cam.getPos() + newPos = currentPos + upVector * z * distance + self.cam.setPos(newPos) + + # ==================== 鼠标事件处理 ==================== + + def mousePressEventRight(self, evt): + """处理鼠标右键按下事件""" + #print("右键按下") + self.mouseRightPressed = True + self.lastMouseX = evt['x'] + self.lastMouseY = evt['y'] + # + # # 通过 Qt 窗口隐藏光标并捕获鼠标 + # try: + # if hasattr(self, 'qtWidget') and self.qtWidget: + # from PyQt5.QtCore import Qt + # self.qtWidget.setCursor(Qt.BlankCursor) + # # 捕获鼠标,使其无法离开窗口 + # self.qtWidget.grabMouse() + # except Exception as e: + # print(f"通过 Qt 隐藏光标时出错: {e}") + + def mouseReleaseEventRight(self, evt): + """处理鼠标右键释放事件""" + #print("右键释放") + self.mouseRightPressed = False + + # # 恢复 Qt 窗口光标并释放鼠标捕获 + # try: + # if hasattr(self, 'qtWidget') and self.qtWidget: + # from PyQt5.QtCore import Qt + # self.qtWidget.unsetCursor() # 恢复默认光标 + # # 释放鼠标捕获 + # self.qtWidget.releaseMouse() + # except Exception as e: + # print(f"恢复 Qt 光标时出错: {e}") + + def mouseMoveEvent(self, evt): + """处理鼠标移动事件 - 只处理相机旋转""" + if not evt: + return + + if self.mouseRightPressed: + # 计算鼠标移动距离 + dx = evt.get('x', 0) - self.lastMouseX + dy = evt.get('y', 0) - self.lastMouseY + + # 计算旋转角度 + rotateSpeed = self.cameraRotateSpeed * globalClock.getDt() + + # 更新相机朝向 + currentH = self.cam.getH() + currentP = self.cam.getP() + + # 限制俯仰角度在-90到90度之间 + newP = max(-90, min(90, currentP - dy * rotateSpeed)) + + self.cam.setH(currentH - dx * rotateSpeed) + self.cam.setP(newP) + + # 更新鼠标位置 + self.lastMouseX = evt.get('x', 0) + self.lastMouseY = evt.get('y', 0) + + # ==================== 其他基础功能 ==================== + + def getChineseFont(self): + """获取中文字体""" + return self.chinese_font + + def getGroundNode(self): + """获取地板节点""" + return self.ground + + def getAmbientLight(self): + """获取环境光""" + return self.ambient_light + + def getDirectionalLight(self): + """获取定向光""" + return self.directional_light + + def start_day_night_cycle(self, duration_seconds=10.0): + """让天空盒在 duration_seconds 秒内从 0:00 过渡到 24:00""" + self._cycle_start_time = self.taskMgr.globalClock.get_real_time() + self._cycle_duration = duration_seconds + self.taskMgr.add(self._day_night_cycle_task, "day_night_cycle_task") + + def _day_night_cycle_task(self, task): + elapsed = self.taskMgr.globalClock.get_real_time() - self._cycle_start_time + t = (elapsed % self._cycle_duration) / self._cycle_duration # 始终在 0~1 循环 + self.render_pipeline.daytime_mgr.time = 24.0 * t + return task.cont + + + def set_daytime(self, time_str): + """设置时间""" + try: + if hasattr(self, 'render_pipeline') and self.render_pipeline: + self.render_pipeline.daytime_mgr.time = time_str + print(f"当前时间设置为: {time_str}") + else: + print(f"⚠️ RenderPipeline 不可用,无法设置时间: {time_str}") + except Exception as e: + print(f"设置时间失败: {e}") + + def get_render_pipeline(self): + """获取 RenderPipeline 实例""" + return getattr(self, 'render_pipeline', None) + + def _setupSkybox(self): + # 加载天空盒模型 + self.skybox = self.loader.loadModel("data/builtin_models/skybox/skybox.bam") + self.skybox.reparentTo(self.camera) # 绑定到相机 + self.skybox.setScale(500) + self.skybox.setBin('background', 0) + self.skybox.setDepthWrite(False) + self.skybox.setLightOff() + self.skybox.setCompass() # 始终朝向固定 + + print("✓ 静态天空盒加载完成") + + def createDirectionalLight(self): + from RenderPipelineFile.rpcore import light_manager + from panda3d.core import DirectionalLight, Vec3 + dlight = DirectionalLight("1") + + dlight_np = self.render.attachNewNode(dlight) + #light_manager.add_light(dlight) + + dlight_np.setHpr(45,-45,0) + + self.render.setLight(dlight_np) + + dlight.setColor((1,1,1,1)) + dlight.setShadowCaster(True,2048,2048) + + print("平行光创建完成") + + # dlight.direction = Vec3(0, 0, -1) # 光照方向 + # dlight.fov = self.lamp_fov # 光源角度(类似手电筒) + # dlight.set_color_from_temperature(1 * 1000.0) # 色温(K) + # dlight.energy = self.half_energy # 光照强度 + # dlight.radius = self.lamp_radius # 影响范围 + # dlight.casts_shadows = True # 是否投射阴影 + # dlight.shadow_map_resolution = 256 # 阴影分辨率 + # dlight.setPos(0,0,10) + + def check_material_editor_connection(self): + """检查材质编辑器连接状态""" + try: + # 确保 RenderPipeline 已完全初始化 + if not hasattr(self, 'render_pipeline') or not self.render_pipeline: + print("RenderPipeline 未初始化") + return False + + # 检查网络监听器是否存在 + if not hasattr(self.render_pipeline, '_listener'): + print("NetworkCommunication 监听器未初始化") + return False + + from RenderPipelineFile.rpcore.util.network_communication import NetworkCommunication + import tempfile + import os + import time + + # 使用唯一的测试文件名 + import uuid + test_filename = f"test_materials_{uuid.uuid4().hex[:8]}.data" + temp_path = os.path.join(tempfile.gettempdir(), test_filename) + + print(f"测试材质编辑器连接,文件路径: {temp_path}") + + # 确保测试文件不存在 + if os.path.exists(temp_path): + os.remove(temp_path) + + # 发送导出命令 + NetworkCommunication.send_async( + NetworkCommunication.MATERIAL_PORT, + f"dump_materials {temp_path}" + ) + + # 大幅增加等待时间,因为网络命令处理可能有延迟 + for i in range(60): # 等待最多30秒 + time.sleep(0.5) + if os.path.exists(temp_path): + # 等待文件写入完成 + time.sleep(1.0) + try: + with open(temp_path, 'r') as f: + content = f.read().strip() + print(f"材质编辑器连接测试成功,文件内容: {len(content)} 字符") + os.remove(temp_path) + return True + except: + print("文件读取失败,继续等待...") + continue + + if i % 20 == 0 and i > 0: # 每10秒打印一次状态 + print(f"等待材质文件创建... ({i // 2}s)") + + print("材质编辑器连接测试超时") + return False + + except Exception as e: + print(f"材质编辑器连接测试出错: {e}") + return False + + + def create_material_editor_widget(self): + """创建材质编辑器组件""" + try: + # 确保 RenderPipeline 已完全初始化 + if not hasattr(self, 'render_pipeline') or not self.render_pipeline: + print("RenderPipeline 未初始化") + return None + + # 检查网络连接 + if not self.check_material_editor_connection(): + print("无法连接到材质编辑器网络服务") + return None + + # 创建材质编辑器组件 + from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel + + material_widget = QWidget() + layout = QVBoxLayout() + + # 添加基本的材质编辑控件 + layout.addWidget(QLabel("材质编辑器")) + # 这里可以添加更多的材质编辑控件 + + material_widget.setLayout(layout) + return material_widget + + except Exception as e: + print(f"创建材质编辑器失败: {e}") + return None + + def _delayed_material_test(self, task): + """延迟执行的材质编辑器连接测试""" + print("开始延迟材质编辑器连接测试...") + success = self.check_material_editor_connection() + if success: + print("✓ 材质编辑器连接正常") + else: + print("✗ 材质编辑器连接失败") + return task.done + + def launch_day_time_editor(self): + + # 检查是否已经启动 + if hasattr(self, '_day_time_editor_process') and self._day_time_editor_process: + if self._day_time_editor_process.poll() is None: # 进程仍在运行 + print("Day Time Editor 已经在运行") + return True + import subprocess + import os + import sys + + try: + if not hasattr(self.world,'render_pipeline') or not self.world.render_pipeline: + print("错误:renderpipeline未初始化") + return False + + base_path = self.world.render_pipeline.mount_mgr.base_path + editor_path = os.path.join(base_path,"toolkit/day_time_editor/main.py") + + if not os.path.exists(editor_path): + print("错误文件不存在") + return False + + self._day_time_editor_process = subprocess.Popen([sys.executable, editor_path]) + print("Day Time Editor 已启动") + return True + except Exception as e: + print(f"启动 time editor失败") + + def setup_material_editor_network(self): + """设置材质编辑器网络通信""" + from RenderPipelineFile.rpcore.util.network_communication import NetworkCommunication + + def handle_material_requests(message): + """处理材质编辑器的网络请求""" + try: + print(f"收到材质编辑器请求: {message}") + parts = message.strip().split() + if not parts: + return + + command = parts[0] + + if command == "dump_materials": + # 处理导出材质列表请求 + path = parts[1] if len(parts) > 1 else "" + if path: + self.export_materials_to_file(path) + elif command.startswith("update_material"): + # 处理更新材质请求 + data = message[len("update_material "):].strip() + self.update_material_from_editor(data.split()) + + except Exception as e: + print(f"处理材质编辑器请求失败: {e}") + + # 注册网络消息处理器 + try: + NetworkCommunication.listen_threaded( + NetworkCommunication.MATERIAL_PORT, + handle_material_requests + ) + print("✓ 材质编辑器网络通信已设置") + except Exception as e: + print(f"设置材质编辑器网络通信失败: {e}") + + def export_materials_to_file(self, path): + """导出材质列表到文件""" + try: + print(f"导出材质列表到: {path}") + materials = [] + + # 收集所有材质 + def collect_materials(node): + if node.hasMaterial(): + material = node.getMaterial() + if material: + materials.append(material) + + for child in node.getChildren(): + collect_materials(child) + + collect_materials(self.render) + + # 写入文件 + with open(path, "w") as f: + for i, material in enumerate(materials): + name = f"{i}-{material.getName() or 'unnamed'}" + + # 获取材质属性,使用默认值如果不存在 + base_color = material.getBaseColor() if hasattr(material, 'getBaseColor') else Vec4(0.6, 0.6, 0.6, 1.0) + roughness = material.getRoughness() if hasattr(material, 'getRoughness') else 0.5 + metallic = material.getMetallic() if hasattr(material, 'getMetallic') else 0.0 + + # 写入材质数据 + f.write(("{} " * 11).format( + name, + base_color.x, + base_color.y, + base_color.z, + roughness, + 0.5, # refractive_index + metallic, + 0, # shading_model + 1.0, # normal_strength + 0.0, # param1 + 0.0 # param2 + ) + "\n") + + print(f"✓ 成功导出 {len(materials)} 个材质到 {path}") + + except Exception as e: + print(f"导出材质列表失败: {e}") + + def update_material_from_editor(self, data): + """从编辑器更新材质""" + try: + if len(data) < 11: + print(f"材质数据不完整: {data}") + return + + name_parts = data[0].split("-") + if len(name_parts) < 2: + print(f"材质名称格式错误: {data[0]}") + return + + index = int(name_parts[0]) + name = "-".join(name_parts[1:]) + + # 收集所有材质 + materials = [] + def collect_materials(node): + if node.hasMaterial(): + material = node.getMaterial() + if material: + materials.append(material) + + for child in node.getChildren(): + collect_materials(child) + + collect_materials(self.render) + + # 查找匹配的材质 + if index < len(materials): + material = materials[index] + + # 更新材质属性 + material.setBaseColor(Vec4(float(data[1]), float(data[2]), float(data[3]), 1.0)) + material.setRoughness(float(data[4])) + material.setMetallic(float(data[6])) + + print(f"✓ 更新材质 {name}: 颜色=({data[1]}, {data[2]}, {data[3]}), 粗糙度={data[4]}, 金属度={data[6]}") + else: + print(f"未找到索引为 {index} 的材质") + + except Exception as e: + print(f"更新材质失败: {e}") + + def launch_material_editor(self): + """启动材质编辑器""" + import subprocess + import os + import sys + + try: + if not self.render_pipeline: + print("错误:renderpipeline未初始化") + return False + + base_path = self.render_pipeline.mount_mgr.base_path + editor_path = os.path.join(base_path,"toolkit/material_editor/main.py") + + if not os.path.exists(editor_path): + print("错误文件不存在") + return False + + self._material_editor_process = subprocess.Popen([sys.executable, editor_path]) + print("Material Editor 已启动") + return True + except Exception as e: + print(f"启动 Material editor失败") + + def create_sample_materials(self): + """创建一些示例材质供编辑器使用""" + from panda3d.core import Material, Vec4, CardMaker + + # 创建几个测试几何体,每个都有不同的材质 + sample_materials = [ + {"name": "MetalMaterial", "color": Vec4(0.7, 0.7, 0.8, 1.0), "metallic": True, "roughness": 0.2}, + {"name": "PlasticMaterial", "color": Vec4(0.8, 0.2, 0.2, 1.0), "metallic": False, "roughness": 0.8}, + {"name": "GlassMaterial", "color": Vec4(0.9, 0.9, 1.0, 0.3), "metallic": False, "roughness": 0.1}, + {"name": "WoodMaterial", "color": Vec4(0.6, 0.4, 0.2, 1.0), "metallic": False, "roughness": 0.7}, + {"name": "TestPlaneMaterial", "color": Vec4(0.8, 0.8, 0.8, 1.0), "metallic": False, "roughness": 1.0} + ] + + for i, mat_data in enumerate(sample_materials): + # 创建几何体 + if mat_data["name"] == "TestPlaneMaterial": + # 创建一个大的测试平面,类似Blender中的平面 + cm = CardMaker('test_plane') + cm.setFrame(-5, 5, -5, 5) # 更大的平面 + geom_node = self.render.attachNewNode(cm.generate()) + geom_node.setPos(0, 15, 0) # 放在前面显眼位置 + geom_node.setP(-90) # 水平放置 + print("✓ 创建大型测试平面,适合测试粗糙度贴图") + else: + cm = CardMaker(f'sample_geom_{i}') + cm.setFrame(-1, 1, -1, 1) + geom_node = self.render.attachNewNode(cm.generate()) + geom_node.setPos(i * 3 - 6, 10, 1) + geom_node.setP(-90) # 水平放置 + + # 创建材质并确保名称正确设置 + material = Material() + material.setName(mat_data["name"]) # 明确设置材质名称 + material.setBaseColor(mat_data["color"]) + material.setRoughness(mat_data["roughness"]) + material.setMetallic(1.0 if mat_data["metallic"] else 0.0) + + # 应用材质 + geom_node.setMaterial(material) + + print(f"✓ 创建示例材质: {mat_data['name']}") + + # 延迟一下确保材质完全创建 + import time + time.sleep(0.1) + + def load_test_models_with_materials(self): + """加载一些测试模型以提供更多材质供编辑""" + try: + # 你可以在这里加载你的模型文件 + # test_model = self.loader.loadModel("models/your_model.gltf") + # if test_model: + # test_model.reparentTo(self.render) + # test_model.setPos(5, 0, 0) + # test_model.setScale(1.0) + # print(f"✓ 测试模型已加载") + + # 创建示例材质 + self.create_sample_materials() + + except Exception as e: + print(f"加载测试模型失败: {e}") + # 创建示例材质作为备选 + self.create_sample_materials() + + def launch_plugin_configurator(self): + """启动材质编辑器""" + import subprocess + import os + import sys + + try: + if not self.render_pipeline: + print("错误:renderpipeline未初始化") + return False + + base_path = self.render_pipeline.mount_mgr.base_path + editor_path = os.path.join(base_path, "toolkit/plugin_configurator/main.py") + + if not os.path.exists(editor_path): + print("错误文件不存在") + return False + + self._material_editor_process = subprocess.Popen([sys.executable, editor_path]) + print("plugin_configurator 已启动") + return True + except Exception as e: + print(f"启动plugin_configurator失败") + diff --git a/demo/COMPLETE_FIXES_SUMMARY.md b/demo/COMPLETE_FIXES_SUMMARY.md index 81812e5c..153f7979 100644 --- a/demo/COMPLETE_FIXES_SUMMARY.md +++ b/demo/COMPLETE_FIXES_SUMMARY.md @@ -1,370 +1,370 @@ -# PANDA3D GUI 中文显示问题完整修复总结 - -## 🎯 修复概览 - -本次修复解决了PANDA3D GUI系统中中文字符显示问题,涉及5个测试文件的全面更新。 - -## 📋 修复清单 - -### ✅ 已修复的文件 - -| 文件名 | 修复状态 | 主要修复内容 | -|--------|----------|--------------| -| `chinese_gui_test.py` | ✅ 完成 | 创建专门的中文字体测试程序 | -| `gui_test.py` | ✅ 完成 | 修复DirectRadioButton回调函数+所有组件中文支持 | -| `simple_gui_test.py` | ✅ 完成 | 添加中文字体加载和按钮支持 | -| `basic_gui_demo.py` | ✅ 完成 | 基础组件中文字体支持 | -| `gui_3d_demo.py` | ✅ 完成 | 2D/3D文本组件中文字体支持 | - -## 🔧 具体修复内容 - -### 1. 中文字体加载系统 - -在每个文件中添加了统一的字体加载函数: - -```python -def loadChineseFont(self): - """尝试加载中文字体""" - import os - font_paths = [ - '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', - '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', - '/usr/share/fonts/truetype/arphic/ukai.ttc', - '/usr/share/fonts/truetype/arphic/uming.ttc', - ] - - for font_path in font_paths: - if os.path.exists(font_path): - try: - font = self.loader.loadFont(font_path) - if font: - return font - except: - continue - return None -``` - -### 2. GUI组件字体设置 - -#### OnscreenText 组件 -```python -# ✅ 正确的方法 -text = OnscreenText( - text="中文文本", - font=self.chinese_font if self.chinese_font else None -) -``` - -#### DirectButton 组件 -```python -# ✅ 正确的方法(注意使用text_font参数) -button = DirectButton( - text="按钮文本", - text_font=self.chinese_font if self.chinese_font else None -) -``` - -#### DirectLabel 组件 -```python -# ✅ 正确的方法 -label = DirectLabel( - text="标签文本", - text_font=self.chinese_font if self.chinese_font else None -) -``` - -#### DirectCheckButton 组件 -```python -# ✅ 正确的方法 -checkbox = DirectCheckButton( - text="复选框文本", - text_font=self.chinese_font if self.chinese_font else None -) -``` - -#### DirectRadioButton 组件 -```python -# ✅ 正确的方法 -radio = DirectRadioButton( - text="单选按钮文本", - text_font=self.chinese_font if self.chinese_font else None -) -``` - -#### DirectOptionMenu 组件 -```python -# ✅ 正确的方法 -menu = DirectOptionMenu( - text="选项菜单", - text_font=self.chinese_font if self.chinese_font else None -) -``` - -### 3. 3D空间文本组件 - -#### TextNode(3D文本) -```python -# ✅ 正确的方法 -text3d = TextNode('text-name') -text3d.setText("3D中文文本") -if self.chinese_font: - text3d.setFont(self.chinese_font) -``` - -### 4. 特殊修复 - -#### DirectRadioButton 回调函数修复 - -**问题:** `gui_test.py`中DirectRadioButton的回调函数参数不匹配 - -**修复前:** -```python -def onRadioButtonSelect(self, status): # ❌ 缺少参数 -``` - -**修复后:** -```python -# 添加extraArgs参数 -radio = DirectRadioButton( - command=self.onRadioButtonSelect, - extraArgs=[i], # 传递索引参数 -) - -# 修改回调函数 -def onRadioButtonSelect(self, index, status): # ✅ 正确的参数 -``` - -## 🎨 字体组件对应表 - -| GUI组件类型 | 字体参数名 | 示例 | -|-------------|------------|------| -| OnscreenText | `font` | `OnscreenText(font=chinese_font)` | -| DirectButton | `text_font` | `DirectButton(text_font=chinese_font)` | -| DirectLabel | `text_font` | `DirectLabel(text_font=chinese_font)` | -| DirectCheckButton | `text_font` | `DirectCheckButton(text_font=chinese_font)` | -| DirectRadioButton | `text_font` | `DirectRadioButton(text_font=chinese_font)` | -| DirectOptionMenu | `text_font` | `DirectOptionMenu(text_font=chinese_font)` | -| TextNode | `setFont()` | `textnode.setFont(chinese_font)` | - -## 🚀 测试验证 - -所有修复的文件都已通过测试: - -```bash -# 测试基础GUI -python3 basic_gui_demo.py - -# 测试简单GUI -python3 simple_gui_test.py - -# 测试完整GUI功能 -python3 gui_test.py - -# 测试中文字体专门功能 -python3 chinese_gui_test.py - -# 测试3D空间GUI -python3 gui_3d_demo.py -``` - -## 📚 使用建议 - -1. **字体安装:** 确保系统已安装中文字体 - ```bash - sudo apt-get install fonts-wqy-microhei fonts-wqy-zenhei - ``` - -2. **字体检测:** 运行程序时查看控制台输出,确认字体加载状态 - -3. **回退机制:** 如果中文字体加载失败,程序会自动使用默认字体 - -4. **组件选择:** 根据需要选择合适的示例程序开始学习 - -## ✨ 修复成果 - -- ✅ 5个测试文件全部支持中文显示 -- ✅ 涵盖所有主要DirectGUI组件 -- ✅ 支持2D和3D空间文本 -- ✅ 提供字体加载状态反馈 -- ✅ 包含完整的使用示例和说明 - -现在PANDA3D GUI系统可以完美显示中文字符了!🎉 - -# 完整的GUI修复总结报告 - -## 问题诊断 - -### 用户报告的问题 -在 `test.py` 程序中,3D文本和虚拟屏幕能正常显示,但其他的GUI组件(按钮、标签、输入框)看不见。 - -### 根本原因分析 -通过深入分析发现,问题的根本原因是**坐标系统不匹配**: - -1. **3D GUI组件**(3D文本、虚拟屏幕)使用**世界坐标系统** - - 坐标范围:任意实数范围 - - 挂载到:`render` 节点下 - - 位置:世界3D空间中的绝对位置 - -2. **2D GUI组件**(按钮、标签、输入框)使用**屏幕坐标系统** - - 坐标范围:-1 到 1 的标准化坐标 - - 挂载到:`render2d/aspect2d` 节点下 - - 位置:屏幕空间中的相对位置 - -### 具体问题 -当传入 `(5, 0, 0)` 这样的坐标时: -- 3D组件正常显示(世界坐标系可以接受任意值) -- 2D组件超出屏幕范围而不可见(屏幕坐标系只接受-1到1的值) - -## 解决方案实施 - -### 1. 修改GUI创建方法 -在 `test.py` 中修改了三个2D GUI创建方法,添加坐标转换: - -```python -def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1): - # 将3D逻辑坐标转换为2D屏幕坐标 - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - # ... 其余代码保持不变 -``` - -**转换公式**:屏幕坐标 = 逻辑坐标 × 0.1 - -### 2. 优化属性面板 -修改了 `updateGUIPropertyPanel()` 方法,为2D GUI组件提供专门的编辑界面: - -```python -def updateGUIPropertyPanel(self, gui_element): - gui_type = gui_element.getTag("gui_type") - - if gui_type in ["button", "label", "entry"]: - # 2D GUI使用逻辑坐标编辑(用户友好) - logical_x = pos.getX() / 0.1 # 反向转换 - logical_z = pos.getZ() / 0.1 - # 显示实际屏幕坐标(只读) - else: - # 3D GUI继续使用世界坐标编辑 -``` - -### 3. 添加专用编辑方法 -新增了 `editGUI2DPosition()` 方法处理2D GUI位置编辑: - -```python -def editGUI2DPosition(self, gui_element, axis, value): - if axis == "x": - new_screen_x = value * 0.1 # 逻辑坐标转屏幕坐标 - gui_element.setPos(new_screen_x, current_pos.getY(), current_pos.getZ()) -``` - -## 测试验证 - -### 创建的测试脚本 - -1. **test_2d_gui_fix.py** - ShowBase环境基础测试 - - ✅ 运行成功,所有GUI组件可见可交互 - - 验证了修复逻辑的正确性 - -2. **test_gui_complete.py** - Qt集成环境完整测试 - - ✅ GUI元素创建成功 - - ⚠️ 在Qt环境中可能存在渲染显示问题 - -3. **test_simple_gui.py** - 简化验证测试 - - ✅ 验证了修复逻辑正确 - -4. **test_qt_fix.py** - Qt环境专门测试 - - ✅ GUI元素正确创建并添加到scene graph - - ⚠️ Qt环境显示可能有问题 - -5. **test_qt_debug.py** - Qt深度诊断 - - 🔍 发现Qt环境创建的是`GraphicsBuffer`而不是`GraphicsWindow` - - 🔍 这是导致渲染问题的根本原因 - -6. **test_qt_showbase.py** - ShowBase独立验证 - - ✅ 在独立窗口中完全正常显示 - -## 测试结果总结 - -### ShowBase环境 ✅ -- **状态**:完全成功 -- **表现**:所有2D GUI组件正常显示和交互 -- **验证**:修复方案完全有效 - -### Qt集成环境 ⚠️ -- **状态**:GUI元素创建成功,但渲染可能有问题 -- **原因**:Qt环境中Panda3D创建的是Buffer而不是Window -- **影响**:可能需要额外的Qt-Panda3D集成调试 - -## 技术细节 - -### 坐标转换系统 -- **逻辑坐标范围**:-50 到 50(用户友好) -- **屏幕坐标范围**:-1 到 1(Panda3D标准) -- **转换比例**:0.1(可调整) - -### 用户界面改进 -- 2D GUI组件编辑使用逻辑坐标(直观) -- 显示实际屏幕坐标(调试用) -- 3D GUI组件继续使用世界坐标 -- 添加了坐标类型提示 - -### 场景图结构 -``` -render2d/ -├── aspect2d/ -│ ├── DirectButton (2D GUI按钮) -│ ├── DirectLabel (2D GUI标签) -│ └── DirectEntry (2D GUI输入框) -└── camera2d - -render/ -├── 3d-text-* (3D文本) -├── virtual-screen-* (虚拟屏幕) -└── camera -``` - -## 使用建议 - -### 1. 在主程序中使用 -修复已应用到主程序 `test.py`,所有2D GUI组件现在应该正常显示。 - -### 2. 创建GUI时的坐标建议 -```python -# 2D GUI组件 - 使用逻辑坐标(-50到50范围) -world.createGUIButton((0, 0, 0), "中心按钮") # 屏幕中心 -world.createGUIButton((-20, 0, 0), "左侧按钮") # 屏幕左侧 -world.createGUIButton((20, 0, 0), "右侧按钮") # 屏幕右侧 - -# 3D GUI组件 - 使用世界坐标 -world.createGUI3DText((0, 5, 2), "3D文本") # 世界空间位置 -world.createGUIVirtualScreen((3, 8, 0), "屏幕") # 世界空间位置 -``` - -### 3. 属性面板编辑 -- 2D GUI:编辑逻辑坐标(-50到50),系统自动转换 -- 3D GUI:直接编辑世界坐标 -- 实际屏幕坐标以只读方式显示 - -## 后续建议 - -### 1. Qt集成优化 -如果需要在Qt环境中获得完美的渲染效果,可能需要: -- 深入研究Qt-Panda3D集成机制 -- 考虑使用不同的Qt集成方案 -- 或使用独立的Panda3D窗口 - -### 2. 功能扩展 -- 可以考虑添加更多坐标系统支持 -- 增加GUI组件的更多属性编辑 -- 提供坐标转换的可视化工具 - -### 3. 测试完善 -- 添加更多边界条件测试 -- 测试更复杂的GUI布局 -- 验证不同分辨率下的表现 - -## 结论 - -**主要问题已解决**:2D GUI组件的坐标转换修复完成,在ShowBase环境中测试完全成功。 - -**Qt环境状态**:GUI元素创建正确,但可能需要额外的渲染调试。 - +# PANDA3D GUI 中文显示问题完整修复总结 + +## 🎯 修复概览 + +本次修复解决了PANDA3D GUI系统中中文字符显示问题,涉及5个测试文件的全面更新。 + +## 📋 修复清单 + +### ✅ 已修复的文件 + +| 文件名 | 修复状态 | 主要修复内容 | +|--------|----------|--------------| +| `chinese_gui_test.py` | ✅ 完成 | 创建专门的中文字体测试程序 | +| `gui_test.py` | ✅ 完成 | 修复DirectRadioButton回调函数+所有组件中文支持 | +| `simple_gui_test.py` | ✅ 完成 | 添加中文字体加载和按钮支持 | +| `basic_gui_demo.py` | ✅ 完成 | 基础组件中文字体支持 | +| `gui_3d_demo.py` | ✅ 完成 | 2D/3D文本组件中文字体支持 | + +## 🔧 具体修复内容 + +### 1. 中文字体加载系统 + +在每个文件中添加了统一的字体加载函数: + +```python +def loadChineseFont(self): + """尝试加载中文字体""" + import os + font_paths = [ + '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', + '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', + '/usr/share/fonts/truetype/arphic/ukai.ttc', + '/usr/share/fonts/truetype/arphic/uming.ttc', + ] + + for font_path in font_paths: + if os.path.exists(font_path): + try: + font = self.loader.loadFont(font_path) + if font: + return font + except: + continue + return None +``` + +### 2. GUI组件字体设置 + +#### OnscreenText 组件 +```python +# ✅ 正确的方法 +text = OnscreenText( + text="中文文本", + font=self.chinese_font if self.chinese_font else None +) +``` + +#### DirectButton 组件 +```python +# ✅ 正确的方法(注意使用text_font参数) +button = DirectButton( + text="按钮文本", + text_font=self.chinese_font if self.chinese_font else None +) +``` + +#### DirectLabel 组件 +```python +# ✅ 正确的方法 +label = DirectLabel( + text="标签文本", + text_font=self.chinese_font if self.chinese_font else None +) +``` + +#### DirectCheckButton 组件 +```python +# ✅ 正确的方法 +checkbox = DirectCheckButton( + text="复选框文本", + text_font=self.chinese_font if self.chinese_font else None +) +``` + +#### DirectRadioButton 组件 +```python +# ✅ 正确的方法 +radio = DirectRadioButton( + text="单选按钮文本", + text_font=self.chinese_font if self.chinese_font else None +) +``` + +#### DirectOptionMenu 组件 +```python +# ✅ 正确的方法 +menu = DirectOptionMenu( + text="选项菜单", + text_font=self.chinese_font if self.chinese_font else None +) +``` + +### 3. 3D空间文本组件 + +#### TextNode(3D文本) +```python +# ✅ 正确的方法 +text3d = TextNode('text-name') +text3d.setText("3D中文文本") +if self.chinese_font: + text3d.setFont(self.chinese_font) +``` + +### 4. 特殊修复 + +#### DirectRadioButton 回调函数修复 + +**问题:** `gui_test.py`中DirectRadioButton的回调函数参数不匹配 + +**修复前:** +```python +def onRadioButtonSelect(self, status): # ❌ 缺少参数 +``` + +**修复后:** +```python +# 添加extraArgs参数 +radio = DirectRadioButton( + command=self.onRadioButtonSelect, + extraArgs=[i], # 传递索引参数 +) + +# 修改回调函数 +def onRadioButtonSelect(self, index, status): # ✅ 正确的参数 +``` + +## 🎨 字体组件对应表 + +| GUI组件类型 | 字体参数名 | 示例 | +|-------------|------------|------| +| OnscreenText | `font` | `OnscreenText(font=chinese_font)` | +| DirectButton | `text_font` | `DirectButton(text_font=chinese_font)` | +| DirectLabel | `text_font` | `DirectLabel(text_font=chinese_font)` | +| DirectCheckButton | `text_font` | `DirectCheckButton(text_font=chinese_font)` | +| DirectRadioButton | `text_font` | `DirectRadioButton(text_font=chinese_font)` | +| DirectOptionMenu | `text_font` | `DirectOptionMenu(text_font=chinese_font)` | +| TextNode | `setFont()` | `textnode.setFont(chinese_font)` | + +## 🚀 测试验证 + +所有修复的文件都已通过测试: + +```bash +# 测试基础GUI +python3 basic_gui_demo.py + +# 测试简单GUI +python3 simple_gui_test.py + +# 测试完整GUI功能 +python3 gui_test.py + +# 测试中文字体专门功能 +python3 chinese_gui_test.py + +# 测试3D空间GUI +python3 gui_3d_demo.py +``` + +## 📚 使用建议 + +1. **字体安装:** 确保系统已安装中文字体 + ```bash + sudo apt-get install fonts-wqy-microhei fonts-wqy-zenhei + ``` + +2. **字体检测:** 运行程序时查看控制台输出,确认字体加载状态 + +3. **回退机制:** 如果中文字体加载失败,程序会自动使用默认字体 + +4. **组件选择:** 根据需要选择合适的示例程序开始学习 + +## ✨ 修复成果 + +- ✅ 5个测试文件全部支持中文显示 +- ✅ 涵盖所有主要DirectGUI组件 +- ✅ 支持2D和3D空间文本 +- ✅ 提供字体加载状态反馈 +- ✅ 包含完整的使用示例和说明 + +现在PANDA3D GUI系统可以完美显示中文字符了!🎉 + +# 完整的GUI修复总结报告 + +## 问题诊断 + +### 用户报告的问题 +在 `test.py` 程序中,3D文本和虚拟屏幕能正常显示,但其他的GUI组件(按钮、标签、输入框)看不见。 + +### 根本原因分析 +通过深入分析发现,问题的根本原因是**坐标系统不匹配**: + +1. **3D GUI组件**(3D文本、虚拟屏幕)使用**世界坐标系统** + - 坐标范围:任意实数范围 + - 挂载到:`render` 节点下 + - 位置:世界3D空间中的绝对位置 + +2. **2D GUI组件**(按钮、标签、输入框)使用**屏幕坐标系统** + - 坐标范围:-1 到 1 的标准化坐标 + - 挂载到:`render2d/aspect2d` 节点下 + - 位置:屏幕空间中的相对位置 + +### 具体问题 +当传入 `(5, 0, 0)` 这样的坐标时: +- 3D组件正常显示(世界坐标系可以接受任意值) +- 2D组件超出屏幕范围而不可见(屏幕坐标系只接受-1到1的值) + +## 解决方案实施 + +### 1. 修改GUI创建方法 +在 `test.py` 中修改了三个2D GUI创建方法,添加坐标转换: + +```python +def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1): + # 将3D逻辑坐标转换为2D屏幕坐标 + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + # ... 其余代码保持不变 +``` + +**转换公式**:屏幕坐标 = 逻辑坐标 × 0.1 + +### 2. 优化属性面板 +修改了 `updateGUIPropertyPanel()` 方法,为2D GUI组件提供专门的编辑界面: + +```python +def updateGUIPropertyPanel(self, gui_element): + gui_type = gui_element.getTag("gui_type") + + if gui_type in ["button", "label", "entry"]: + # 2D GUI使用逻辑坐标编辑(用户友好) + logical_x = pos.getX() / 0.1 # 反向转换 + logical_z = pos.getZ() / 0.1 + # 显示实际屏幕坐标(只读) + else: + # 3D GUI继续使用世界坐标编辑 +``` + +### 3. 添加专用编辑方法 +新增了 `editGUI2DPosition()` 方法处理2D GUI位置编辑: + +```python +def editGUI2DPosition(self, gui_element, axis, value): + if axis == "x": + new_screen_x = value * 0.1 # 逻辑坐标转屏幕坐标 + gui_element.setPos(new_screen_x, current_pos.getY(), current_pos.getZ()) +``` + +## 测试验证 + +### 创建的测试脚本 + +1. **test_2d_gui_fix.py** - ShowBase环境基础测试 + - ✅ 运行成功,所有GUI组件可见可交互 + - 验证了修复逻辑的正确性 + +2. **test_gui_complete.py** - Qt集成环境完整测试 + - ✅ GUI元素创建成功 + - ⚠️ 在Qt环境中可能存在渲染显示问题 + +3. **test_simple_gui.py** - 简化验证测试 + - ✅ 验证了修复逻辑正确 + +4. **test_qt_fix.py** - Qt环境专门测试 + - ✅ GUI元素正确创建并添加到scene graph + - ⚠️ Qt环境显示可能有问题 + +5. **test_qt_debug.py** - Qt深度诊断 + - 🔍 发现Qt环境创建的是`GraphicsBuffer`而不是`GraphicsWindow` + - 🔍 这是导致渲染问题的根本原因 + +6. **test_qt_showbase.py** - ShowBase独立验证 + - ✅ 在独立窗口中完全正常显示 + +## 测试结果总结 + +### ShowBase环境 ✅ +- **状态**:完全成功 +- **表现**:所有2D GUI组件正常显示和交互 +- **验证**:修复方案完全有效 + +### Qt集成环境 ⚠️ +- **状态**:GUI元素创建成功,但渲染可能有问题 +- **原因**:Qt环境中Panda3D创建的是Buffer而不是Window +- **影响**:可能需要额外的Qt-Panda3D集成调试 + +## 技术细节 + +### 坐标转换系统 +- **逻辑坐标范围**:-50 到 50(用户友好) +- **屏幕坐标范围**:-1 到 1(Panda3D标准) +- **转换比例**:0.1(可调整) + +### 用户界面改进 +- 2D GUI组件编辑使用逻辑坐标(直观) +- 显示实际屏幕坐标(调试用) +- 3D GUI组件继续使用世界坐标 +- 添加了坐标类型提示 + +### 场景图结构 +``` +render2d/ +├── aspect2d/ +│ ├── DirectButton (2D GUI按钮) +│ ├── DirectLabel (2D GUI标签) +│ └── DirectEntry (2D GUI输入框) +└── camera2d + +render/ +├── 3d-text-* (3D文本) +├── virtual-screen-* (虚拟屏幕) +└── camera +``` + +## 使用建议 + +### 1. 在主程序中使用 +修复已应用到主程序 `test.py`,所有2D GUI组件现在应该正常显示。 + +### 2. 创建GUI时的坐标建议 +```python +# 2D GUI组件 - 使用逻辑坐标(-50到50范围) +world.createGUIButton((0, 0, 0), "中心按钮") # 屏幕中心 +world.createGUIButton((-20, 0, 0), "左侧按钮") # 屏幕左侧 +world.createGUIButton((20, 0, 0), "右侧按钮") # 屏幕右侧 + +# 3D GUI组件 - 使用世界坐标 +world.createGUI3DText((0, 5, 2), "3D文本") # 世界空间位置 +world.createGUIVirtualScreen((3, 8, 0), "屏幕") # 世界空间位置 +``` + +### 3. 属性面板编辑 +- 2D GUI:编辑逻辑坐标(-50到50),系统自动转换 +- 3D GUI:直接编辑世界坐标 +- 实际屏幕坐标以只读方式显示 + +## 后续建议 + +### 1. Qt集成优化 +如果需要在Qt环境中获得完美的渲染效果,可能需要: +- 深入研究Qt-Panda3D集成机制 +- 考虑使用不同的Qt集成方案 +- 或使用独立的Panda3D窗口 + +### 2. 功能扩展 +- 可以考虑添加更多坐标系统支持 +- 增加GUI组件的更多属性编辑 +- 提供坐标转换的可视化工具 + +### 3. 测试完善 +- 添加更多边界条件测试 +- 测试更复杂的GUI布局 +- 验证不同分辨率下的表现 + +## 结论 + +**主要问题已解决**:2D GUI组件的坐标转换修复完成,在ShowBase环境中测试完全成功。 + +**Qt环境状态**:GUI元素创建正确,但可能需要额外的渲染调试。 + **修复效果**:用户现在应该能够在主程序中看到所有GUI组件,包括按钮、标签和输入框。 \ No newline at end of file diff --git a/demo/FBX缩放层级修复说明.md b/demo/FBX缩放层级修复说明.md index fa5b3895..297793e1 100644 --- a/demo/FBX缩放层级修复说明.md +++ b/demo/FBX缩放层级修复说明.md @@ -1,211 +1,211 @@ -# FBX模型缩放层级修复说明 - -## 🔍 问题描述 - -用户反馈FBX模型导入时会出现缩放层级混乱的问题: -- **根节点缩放**: 0.01(强制应用的单位转换) -- **子节点缩放**: 100(FBX内部的原始缩放) -- **视觉效果**: 正常显示,但层级结构复杂难处理 - -## ⚡ 问题原因分析 - -### 原始导入逻辑 -```python -# 旧代码 - 强制FBX单位转换 -if filepath.lower().endswith('.fbx'): - scale_factor = 0.01 # 厘米到米 - model.setScale(scale_factor) # 根节点 = 0.01 -``` - -### 层级结构混乱 -``` -FBX模型结构: -├─ 根节点 (setScale 0.01) ← 强制设置 -│ ├─ 子节点A (原始缩放 100) ← FBX内部缩放 -│ ├─ 子节点B (原始缩放 100) ← FBX内部缩放 -│ └─ 子节点C (原始缩放 50) ← FBX内部缩放 - -结果: 0.01 × 100 = 1.0 (正常显示,但层级复杂) -``` - -## 🛠 解决方案 - -### 1. **新的导入逻辑** -```python -def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True): - """新的导入方法 - 智能缩放处理""" - - # 可选: 单位转换 - if apply_unit_conversion and filepath.lower().endswith('.fbx'): - self._applyUnitConversion(model, 0.01) - - # 智能: 缩放标准化(推荐开启) - if normalize_scales and filepath.lower().endswith('.fbx'): - self._normalizeModelScales(model) - - # 只调整位置,不强制缩放 - self._adjustModelToGround(model) -``` - -### 2. **智能缩放标准化** -新增核心功能,自动检测和处理FBX子节点的大缩放值: -- 🔍 **自动检测**: 扫描所有子节点,识别大缩放值(>10) -- 📊 **统计分析**: 找到最常见的大缩放值(如100) -- ⚙️ **智能标准化**: 计算合适的标准化因子(如1/100 = 0.01) -- 🎯 **精确应用**: 只处理有问题的大缩放节点 - -### 2. **保持原有结构的优势** -- ✅ **简化层级**: 避免0.01 × 100的复杂计算 -- ✅ **保持一致**: 所有文件格式统一处理 -- ✅ **用户选择**: 可选择是否应用单位转换 -- ✅ **易于处理**: 缩放操作更直观 - -## 📊 修复对比 - -### **修复前** -```python -# 强制FBX单位转换 -FBX根节点.setScale(0.01) - -层级结构: -根节点(0.01) -> 子节点A(100) -> 孙子节点(1) -实际显示: 0.01 × 100 × 1 = 1.0 ✅视觉正常 -处理复杂度: ❌复杂,需要考虑多层缩放 -``` - -### **修复后** -```python -# 智能缩放标准化(推荐) -model.importModel(filepath, normalize_scales=True) - -层级结构: -根节点(1.0) -> 子节点A(1.0) -> 孙子节点(1.0) -实际显示: 1.0 × 1.0 × 1.0 = 1.0 ✅完美 -处理复杂度: ✅简单,统一缩放层级 - -# 或者保持原始结构 -model.importModel(filepath, normalize_scales=False) - -层级结构: -根节点(1.0) -> 子节点A(100) -> 孙子节点(1) -实际显示: 1.0 × 100 × 1 = 100 ⚠️可能很大 -处理复杂度: ✅简单,但需要手动调整 -``` - -## 🎛 使用方式 - -### **方式1: 智能导入(推荐)** -```python -# 默认开启缩放标准化,自动处理子节点大缩放值 -model = world.importModel("model.fbx") # normalize_scales=True -``` - -### **方式2: 完全保持原始结构** -```python -# 关闭所有自动处理,保持FBX原始结构 -model = world.importModel("model.fbx", normalize_scales=False) -``` - -### **方式3: 传统单位转换** -```python -# 应用厘米到米的转换 + 缩放标准化 -model = world.importModel("model.fbx", apply_unit_conversion=True, normalize_scales=True) -``` - -### **方式4: 交互式测试** -```python -# 使用测试脚本 -python demo/fbx_import_test.py -# 按U键切换单位转换模式 -# 按N键切换缩放标准化模式 -``` - -## 🧪 测试验证 - -### **测试场景** -1. **大型建筑模型**: 通常FBX使用厘米,模型会很大 -2. **角色模型**: 通常已经合适的比例 -3. **道具模型**: 混合使用情况 - -### **验证方法** -```bash -# 启动测试程序 -python demo/fbx_import_test.py - -# 测试步骤: -1. 导入FBX模型(默认保持原有缩放) -2. 按I键查看缩放信息 -3. 按U键切换单位转换模式 -4. 重新导入同一模型对比 -5. 观察层级结构差异 -``` - -## 🎯 智能位置调整 - -### **地面对齐算法** -```python -def _adjustModelToGround(self, model): - """智能调整到地面,不改变缩放""" - bounds = model.getBounds() - min_point = bounds.getMin() - - # 计算地面偏移(不涉及缩放) - ground_offset = -min_point.getZ() - model.setPos(0, 0, ground_offset) -``` - -### **优势** -- 🎯 **精确对齐**: 无论模型大小都能准确放在地面 -- 🔄 **缩放无关**: 位置调整独立于缩放操作 -- 🛡 **错误处理**: 边界获取失败时使用默认位置 - -## 📋 最佳实践建议 - -### **推荐工作流程** -1. **首次导入**: 使用默认设置(不转换单位) -2. **检查大小**: 如果模型过大,考虑单位转换 -3. **手动调整**: 根据需要手动设置合适的缩放 -4. **保存设置**: 记录适合项目的导入参数 - -### **针对不同模型类型** -| 模型类型 | 推荐设置 | 说明 | -|---------|---------|------| -| 建筑/场景 | 单位转换=True | 通常用厘米,需要转换 | -| 角色/动物 | 单位转换=False | 通常已合适比例 | -| 道具/物品 | 按情况选择 | 根据实际大小决定 | -| 机械设备 | 单位转换=True | CAD导出常用厘米 | - -### **调试技巧** -```python -# 检查模型层级结构 -def print_model_hierarchy(model, depth=0): - indent = " " * depth - print(f"{indent}{model.getName()}: scale={model.getScale()}") - for i in range(model.getNumChildren()): - print_model_hierarchy(model.getChild(i), depth+1) -``` - -## 🚀 总结 - -这次修复彻底解决了FBX导入时的缩放层级问题: - -### 🎯 **核心突破** -- ✅ **智能标准化**: 自动检测并修复子节点大缩放值(如100 → 1) -- ✅ **统一层级**: 实现全模型1:1缩放,避免复杂的多层计算 -- ✅ **保持结构**: 维护FBX内部的相对比例关系 -- ✅ **零干扰**: 只处理有问题的节点,不影响正常节点 - -### 🔧 **技术优势** -- 🔍 **智能检测**: 自动扫描识别大缩放值 -- 📊 **统计分析**: 基于最常见缩放值计算标准化因子 -- ⚙️ **精确处理**: 只标准化明显异常的缩放(>10) -- 🎛️ **用户控制**: 提供完整的开关选项 - -### 📈 **效果对比** -| 处理方式 | 根节点缩放 | 子节点缩放 | 最终效果 | 推荐度 | -|---------|-----------|-----------|---------|--------| -| 旧方案 | 0.01 | 100 | 复杂层级 | ❌ | -| 关闭处理 | 1.0 | 100 | 模型过大 | ⚠️ | -| **智能标准化** | **1.0** | **1.0** | **完美统一** | ✅ | - +# FBX模型缩放层级修复说明 + +## 🔍 问题描述 + +用户反馈FBX模型导入时会出现缩放层级混乱的问题: +- **根节点缩放**: 0.01(强制应用的单位转换) +- **子节点缩放**: 100(FBX内部的原始缩放) +- **视觉效果**: 正常显示,但层级结构复杂难处理 + +## ⚡ 问题原因分析 + +### 原始导入逻辑 +```python +# 旧代码 - 强制FBX单位转换 +if filepath.lower().endswith('.fbx'): + scale_factor = 0.01 # 厘米到米 + model.setScale(scale_factor) # 根节点 = 0.01 +``` + +### 层级结构混乱 +``` +FBX模型结构: +├─ 根节点 (setScale 0.01) ← 强制设置 +│ ├─ 子节点A (原始缩放 100) ← FBX内部缩放 +│ ├─ 子节点B (原始缩放 100) ← FBX内部缩放 +│ └─ 子节点C (原始缩放 50) ← FBX内部缩放 + +结果: 0.01 × 100 = 1.0 (正常显示,但层级复杂) +``` + +## 🛠 解决方案 + +### 1. **新的导入逻辑** +```python +def importModel(self, filepath, apply_unit_conversion=False, normalize_scales=True): + """新的导入方法 - 智能缩放处理""" + + # 可选: 单位转换 + if apply_unit_conversion and filepath.lower().endswith('.fbx'): + self._applyUnitConversion(model, 0.01) + + # 智能: 缩放标准化(推荐开启) + if normalize_scales and filepath.lower().endswith('.fbx'): + self._normalizeModelScales(model) + + # 只调整位置,不强制缩放 + self._adjustModelToGround(model) +``` + +### 2. **智能缩放标准化** +新增核心功能,自动检测和处理FBX子节点的大缩放值: +- 🔍 **自动检测**: 扫描所有子节点,识别大缩放值(>10) +- 📊 **统计分析**: 找到最常见的大缩放值(如100) +- ⚙️ **智能标准化**: 计算合适的标准化因子(如1/100 = 0.01) +- 🎯 **精确应用**: 只处理有问题的大缩放节点 + +### 2. **保持原有结构的优势** +- ✅ **简化层级**: 避免0.01 × 100的复杂计算 +- ✅ **保持一致**: 所有文件格式统一处理 +- ✅ **用户选择**: 可选择是否应用单位转换 +- ✅ **易于处理**: 缩放操作更直观 + +## 📊 修复对比 + +### **修复前** +```python +# 强制FBX单位转换 +FBX根节点.setScale(0.01) + +层级结构: +根节点(0.01) -> 子节点A(100) -> 孙子节点(1) +实际显示: 0.01 × 100 × 1 = 1.0 ✅视觉正常 +处理复杂度: ❌复杂,需要考虑多层缩放 +``` + +### **修复后** +```python +# 智能缩放标准化(推荐) +model.importModel(filepath, normalize_scales=True) + +层级结构: +根节点(1.0) -> 子节点A(1.0) -> 孙子节点(1.0) +实际显示: 1.0 × 1.0 × 1.0 = 1.0 ✅完美 +处理复杂度: ✅简单,统一缩放层级 + +# 或者保持原始结构 +model.importModel(filepath, normalize_scales=False) + +层级结构: +根节点(1.0) -> 子节点A(100) -> 孙子节点(1) +实际显示: 1.0 × 100 × 1 = 100 ⚠️可能很大 +处理复杂度: ✅简单,但需要手动调整 +``` + +## 🎛 使用方式 + +### **方式1: 智能导入(推荐)** +```python +# 默认开启缩放标准化,自动处理子节点大缩放值 +model = world.importModel("model.fbx") # normalize_scales=True +``` + +### **方式2: 完全保持原始结构** +```python +# 关闭所有自动处理,保持FBX原始结构 +model = world.importModel("model.fbx", normalize_scales=False) +``` + +### **方式3: 传统单位转换** +```python +# 应用厘米到米的转换 + 缩放标准化 +model = world.importModel("model.fbx", apply_unit_conversion=True, normalize_scales=True) +``` + +### **方式4: 交互式测试** +```python +# 使用测试脚本 +python demo/fbx_import_test.py +# 按U键切换单位转换模式 +# 按N键切换缩放标准化模式 +``` + +## 🧪 测试验证 + +### **测试场景** +1. **大型建筑模型**: 通常FBX使用厘米,模型会很大 +2. **角色模型**: 通常已经合适的比例 +3. **道具模型**: 混合使用情况 + +### **验证方法** +```bash +# 启动测试程序 +python demo/fbx_import_test.py + +# 测试步骤: +1. 导入FBX模型(默认保持原有缩放) +2. 按I键查看缩放信息 +3. 按U键切换单位转换模式 +4. 重新导入同一模型对比 +5. 观察层级结构差异 +``` + +## 🎯 智能位置调整 + +### **地面对齐算法** +```python +def _adjustModelToGround(self, model): + """智能调整到地面,不改变缩放""" + bounds = model.getBounds() + min_point = bounds.getMin() + + # 计算地面偏移(不涉及缩放) + ground_offset = -min_point.getZ() + model.setPos(0, 0, ground_offset) +``` + +### **优势** +- 🎯 **精确对齐**: 无论模型大小都能准确放在地面 +- 🔄 **缩放无关**: 位置调整独立于缩放操作 +- 🛡 **错误处理**: 边界获取失败时使用默认位置 + +## 📋 最佳实践建议 + +### **推荐工作流程** +1. **首次导入**: 使用默认设置(不转换单位) +2. **检查大小**: 如果模型过大,考虑单位转换 +3. **手动调整**: 根据需要手动设置合适的缩放 +4. **保存设置**: 记录适合项目的导入参数 + +### **针对不同模型类型** +| 模型类型 | 推荐设置 | 说明 | +|---------|---------|------| +| 建筑/场景 | 单位转换=True | 通常用厘米,需要转换 | +| 角色/动物 | 单位转换=False | 通常已合适比例 | +| 道具/物品 | 按情况选择 | 根据实际大小决定 | +| 机械设备 | 单位转换=True | CAD导出常用厘米 | + +### **调试技巧** +```python +# 检查模型层级结构 +def print_model_hierarchy(model, depth=0): + indent = " " * depth + print(f"{indent}{model.getName()}: scale={model.getScale()}") + for i in range(model.getNumChildren()): + print_model_hierarchy(model.getChild(i), depth+1) +``` + +## 🚀 总结 + +这次修复彻底解决了FBX导入时的缩放层级问题: + +### 🎯 **核心突破** +- ✅ **智能标准化**: 自动检测并修复子节点大缩放值(如100 → 1) +- ✅ **统一层级**: 实现全模型1:1缩放,避免复杂的多层计算 +- ✅ **保持结构**: 维护FBX内部的相对比例关系 +- ✅ **零干扰**: 只处理有问题的节点,不影响正常节点 + +### 🔧 **技术优势** +- 🔍 **智能检测**: 自动扫描识别大缩放值 +- 📊 **统计分析**: 基于最常见缩放值计算标准化因子 +- ⚙️ **精确处理**: 只标准化明显异常的缩放(>10) +- 🎛️ **用户控制**: 提供完整的开关选项 + +### 📈 **效果对比** +| 处理方式 | 根节点缩放 | 子节点缩放 | 最终效果 | 推荐度 | +|---------|-----------|-----------|---------|--------| +| 旧方案 | 0.01 | 100 | 复杂层级 | ❌ | +| 关闭处理 | 1.0 | 100 | 模型过大 | ⚠️ | +| **智能标准化** | **1.0** | **1.0** | **完美统一** | ✅ | + 现在用户可以简单地导入FBX模型,系统会自动处理所有缩放问题,实现真正的"一键导入,完美显示"! \ No newline at end of file diff --git a/demo/GUI_FIXES_SUMMARY.md b/demo/GUI_FIXES_SUMMARY.md index 99835f8e..3b3a64a9 100644 --- a/demo/GUI_FIXES_SUMMARY.md +++ b/demo/GUI_FIXES_SUMMARY.md @@ -1,150 +1,150 @@ -# GUI 显示问题修复总结 - -## 问题描述 - -在 `test.py` 主程序中,3D文本和虚拟屏幕能够正常显示,但是2D GUI组件(按钮、标签、输入框)看不见。 - -## 问题原因分析 - -### 1. 坐标系统不匹配 -- **3D GUI组件**(3D文本、虚拟屏幕)使用**世界坐标系统**,坐标范围可以是任意值 -- **2D GUI组件**(按钮、标签、输入框)使用**屏幕坐标系统**,坐标范围通常是 -1 到 1 - -### 2. 具体问题 -原代码中,所有GUI组件都使用相同的位置参数 `pos=(0, 0, 0)`: -```python -# 原来的错误代码 -button = DirectButton( - text=text, - pos=pos, # 直接使用3D坐标,错误! - scale=size, - # ... -) -``` - -当传入像 `(5, 0, 0)` 这样的3D坐标时: -- 3D组件正常显示在世界坐标 (5, 0, 0) -- 2D组件会被显示在屏幕坐标 (5, 0, 0),这超出了屏幕范围 [-1, 1],因此不可见 - -## 解决方案 - -### 1. 坐标转换 -为2D GUI组件添加坐标转换逻辑: - -```python -def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1): - from direct.gui.DirectGui import DirectButton - - # 将3D坐标转换为2D屏幕坐标 - # DirectGUI使用屏幕坐标系,范围是-1到1 - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) # 缩放到合适的屏幕范围 - - button = DirectButton( - text=text, - pos=gui_pos, # 使用转换后的屏幕坐标 - scale=size, - # ... - ) -``` - -### 2. 转换规则 -- **输入坐标**: 3D逻辑坐标,范围 -50 到 50(用户友好) -- **输出坐标**: 2D屏幕坐标,范围 -1 到 1(DirectGUI要求) -- **转换公式**: `屏幕坐标 = 逻辑坐标 × 0.1` - -### 3. 属性面板优化 -为了更好地编辑2D GUI组件,属性面板中添加了特殊处理: - -```python -def updateGUIPropertyPanel(self, gui_element): - gui_type = gui_element.getTag("gui_type") - - if gui_type in ["button", "label", "entry"]: - # 2D GUI组件使用逻辑坐标编辑 - logical_x = pos.getX() / 0.1 # 反向转换为逻辑坐标 - logical_z = pos.getZ() / 0.1 - - # 提供逻辑坐标编辑界面 - xPos.setValue(logical_x) - xPos.valueChanged.connect(lambda v: self.editGUI2DPosition(gui_element, "x", v)) - - # 同时显示实际屏幕坐标(只读) - actualXLabel = QLabel(f"{pos.getX():.3f}") - else: - # 3D GUI组件使用世界坐标编辑 - # 正常的3D坐标编辑逻辑 -``` - -## 修复效果验证 - -### 1. 测试脚本 -创建了两个测试脚本验证修复效果: -- `test_2d_gui_fix.py`: 简单的2D GUI显示测试 -- `test_gui_complete.py`: 完整的GUI功能测试 - -### 2. 测试结果 -修复后的测试显示: -- ✅ 所有位置的2D按钮都可见且可点击 -- ✅ 所有位置的2D标签都正常显示 -- ✅ 所有位置的2D输入框都可见且可输入 -- ✅ 3D文本和虚拟屏幕继续正常工作 -- ✅ 坐标转换计算正确 - -### 3. 控制台输出示例 -``` -创建了 中心 位置的GUI元素: - - 按钮位置: (0.0, 0, 0.0) - - 标签位置: (0.0, 0, -0.2) - - 输入框位置: (0.0, 0, -0.4) -创建了 左侧 位置的GUI元素: - - 按钮位置: (-0.5, 0, 0.0) - - 标签位置: (-0.5, 0, -0.2) - - 输入框位置: (-0.5, 0, -0.4) -``` - -## 关键改进点 - -### 1. 用户体验 -- 用户仍然可以使用直观的"逻辑坐标"(如 -5, 0, 5)来指定GUI位置 -- 系统自动处理坐标转换,对用户透明 - -### 2. 一致性 -- 3D GUI组件继续使用世界坐标系统 -- 2D GUI组件使用合适的屏幕坐标系统 -- 属性面板根据组件类型提供相应的编辑界面 - -### 3. 可维护性 -- 坐标转换逻辑集中在创建方法中 -- 添加了专门的2D GUI位置编辑方法 `editGUI2DPosition()` -- 保持了代码结构的清晰性 - -## 使用建议 - -### 1. 创建2D GUI组件 -```python -# 使用逻辑坐标创建(推荐) -button = world.createGUIButton((-5, 0, 0), "左侧按钮") # 显示在屏幕左侧 -button = world.createGUIButton((5, 0, 0), "右侧按钮") # 显示在屏幕右侧 -button = world.createGUIButton((0, 0, 5), "上方按钮") # 显示在屏幕上方 -``` - -### 2. 创建3D GUI组件 -```python -# 使用世界坐标创建(不变) -text3d = world.createGUI3DText((0, 5, 2), "3D文本") -screen = world.createGUIVirtualScreen((3, 8, 0), (2, 1), "虚拟屏幕") -``` - -### 3. 坐标范围建议 -- **2D GUI逻辑坐标**: -10 到 10(对应屏幕范围 -1 到 1) -- **3D GUI世界坐标**: 任意合理范围 - -## 总结 - -这次修复解决了2D GUI组件不可见的核心问题,通过正确的坐标系统转换,确保了: -1. 所有GUI组件都能正常显示 -2. 用户界面保持直观和一致 -3. 编辑功能完全可用 -4. 代码结构保持清晰 - +# GUI 显示问题修复总结 + +## 问题描述 + +在 `test.py` 主程序中,3D文本和虚拟屏幕能够正常显示,但是2D GUI组件(按钮、标签、输入框)看不见。 + +## 问题原因分析 + +### 1. 坐标系统不匹配 +- **3D GUI组件**(3D文本、虚拟屏幕)使用**世界坐标系统**,坐标范围可以是任意值 +- **2D GUI组件**(按钮、标签、输入框)使用**屏幕坐标系统**,坐标范围通常是 -1 到 1 + +### 2. 具体问题 +原代码中,所有GUI组件都使用相同的位置参数 `pos=(0, 0, 0)`: +```python +# 原来的错误代码 +button = DirectButton( + text=text, + pos=pos, # 直接使用3D坐标,错误! + scale=size, + # ... +) +``` + +当传入像 `(5, 0, 0)` 这样的3D坐标时: +- 3D组件正常显示在世界坐标 (5, 0, 0) +- 2D组件会被显示在屏幕坐标 (5, 0, 0),这超出了屏幕范围 [-1, 1],因此不可见 + +## 解决方案 + +### 1. 坐标转换 +为2D GUI组件添加坐标转换逻辑: + +```python +def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1): + from direct.gui.DirectGui import DirectButton + + # 将3D坐标转换为2D屏幕坐标 + # DirectGUI使用屏幕坐标系,范围是-1到1 + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) # 缩放到合适的屏幕范围 + + button = DirectButton( + text=text, + pos=gui_pos, # 使用转换后的屏幕坐标 + scale=size, + # ... + ) +``` + +### 2. 转换规则 +- **输入坐标**: 3D逻辑坐标,范围 -50 到 50(用户友好) +- **输出坐标**: 2D屏幕坐标,范围 -1 到 1(DirectGUI要求) +- **转换公式**: `屏幕坐标 = 逻辑坐标 × 0.1` + +### 3. 属性面板优化 +为了更好地编辑2D GUI组件,属性面板中添加了特殊处理: + +```python +def updateGUIPropertyPanel(self, gui_element): + gui_type = gui_element.getTag("gui_type") + + if gui_type in ["button", "label", "entry"]: + # 2D GUI组件使用逻辑坐标编辑 + logical_x = pos.getX() / 0.1 # 反向转换为逻辑坐标 + logical_z = pos.getZ() / 0.1 + + # 提供逻辑坐标编辑界面 + xPos.setValue(logical_x) + xPos.valueChanged.connect(lambda v: self.editGUI2DPosition(gui_element, "x", v)) + + # 同时显示实际屏幕坐标(只读) + actualXLabel = QLabel(f"{pos.getX():.3f}") + else: + # 3D GUI组件使用世界坐标编辑 + # 正常的3D坐标编辑逻辑 +``` + +## 修复效果验证 + +### 1. 测试脚本 +创建了两个测试脚本验证修复效果: +- `test_2d_gui_fix.py`: 简单的2D GUI显示测试 +- `test_gui_complete.py`: 完整的GUI功能测试 + +### 2. 测试结果 +修复后的测试显示: +- ✅ 所有位置的2D按钮都可见且可点击 +- ✅ 所有位置的2D标签都正常显示 +- ✅ 所有位置的2D输入框都可见且可输入 +- ✅ 3D文本和虚拟屏幕继续正常工作 +- ✅ 坐标转换计算正确 + +### 3. 控制台输出示例 +``` +创建了 中心 位置的GUI元素: + - 按钮位置: (0.0, 0, 0.0) + - 标签位置: (0.0, 0, -0.2) + - 输入框位置: (0.0, 0, -0.4) +创建了 左侧 位置的GUI元素: + - 按钮位置: (-0.5, 0, 0.0) + - 标签位置: (-0.5, 0, -0.2) + - 输入框位置: (-0.5, 0, -0.4) +``` + +## 关键改进点 + +### 1. 用户体验 +- 用户仍然可以使用直观的"逻辑坐标"(如 -5, 0, 5)来指定GUI位置 +- 系统自动处理坐标转换,对用户透明 + +### 2. 一致性 +- 3D GUI组件继续使用世界坐标系统 +- 2D GUI组件使用合适的屏幕坐标系统 +- 属性面板根据组件类型提供相应的编辑界面 + +### 3. 可维护性 +- 坐标转换逻辑集中在创建方法中 +- 添加了专门的2D GUI位置编辑方法 `editGUI2DPosition()` +- 保持了代码结构的清晰性 + +## 使用建议 + +### 1. 创建2D GUI组件 +```python +# 使用逻辑坐标创建(推荐) +button = world.createGUIButton((-5, 0, 0), "左侧按钮") # 显示在屏幕左侧 +button = world.createGUIButton((5, 0, 0), "右侧按钮") # 显示在屏幕右侧 +button = world.createGUIButton((0, 0, 5), "上方按钮") # 显示在屏幕上方 +``` + +### 2. 创建3D GUI组件 +```python +# 使用世界坐标创建(不变) +text3d = world.createGUI3DText((0, 5, 2), "3D文本") +screen = world.createGUIVirtualScreen((3, 8, 0), (2, 1), "虚拟屏幕") +``` + +### 3. 坐标范围建议 +- **2D GUI逻辑坐标**: -10 到 10(对应屏幕范围 -1 到 1) +- **3D GUI世界坐标**: 任意合理范围 + +## 总结 + +这次修复解决了2D GUI组件不可见的核心问题,通过正确的坐标系统转换,确保了: +1. 所有GUI组件都能正常显示 +2. 用户界面保持直观和一致 +3. 编辑功能完全可用 +4. 代码结构保持清晰 + 修复验证显示所有功能都正常工作,用户现在可以在主程序中成功创建和使用所有类型的GUI元素。 \ No newline at end of file diff --git a/demo/GUI_IMPLEMENTATION_GUIDE.md b/demo/GUI_IMPLEMENTATION_GUIDE.md index c26358a2..76589c19 100644 --- a/demo/GUI_IMPLEMENTATION_GUIDE.md +++ b/demo/GUI_IMPLEMENTATION_GUIDE.md @@ -1,283 +1,283 @@ -# 🎯 GUI功能实现完整指南 - -## 📋 功能概述 - -我已经为你的`test.py`程序添加了完整的GUI管理功能,包括: - -### ✨ 核心功能 -- **GUI元素创建** - 支持多种2D和3D GUI组件 -- **GUI元素编辑** - 实时属性修改和可视化编辑 -- **GUI元素删除** - 安全的元素移除机制 -- **场景树显示** - GUI元素在层级窗口中的组织 -- **属性面板** - 专门的GUI属性编辑界面 - ---- - -## 🎮 支持的GUI类型 - -### 📱 2D GUI组件 -1. **DirectButton (按钮)** - - 可点击的交互按钮 - - 支持自定义文本和颜色 - - 带有点击事件响应 - -2. **DirectLabel (标签)** - - 静态文本显示 - - 透明背景 - - 可自定义字体和颜色 - -3. **DirectEntry (输入框)** - - 文本输入组件 - - 支持占位符文本 - - 带有提交事件 - -### 🌐 3D空间GUI组件 -1. **3D TextNode (3D文本)** - - 3D空间中的文本节点 - - Billboard效果(始终面向相机) - - 支持中文字体 - -2. **Virtual Screen (虚拟屏幕)** - - 3D空间中的平面屏幕 - - 可显示文本内容 - - 半透明效果 - ---- - -## 🛠️ 使用方法 - -### 创建GUI元素 - -#### 方法1: 使用菜单 -``` -GUI菜单 → 创建按钮/标签/输入框/3D文本/虚拟屏幕 -``` - -#### 方法2: 使用工具栏 -``` -工具栏 → 创建按钮/创建标签/3D文本 -``` - -#### 方法3: 程序化创建 -```python -# 创建按钮 -button = world.createGUIButton(pos=(0, 0, 0.5), text="我的按钮") - -# 创建标签 -label = world.createGUILabel(pos=(0, 0, 0.3), text="标签文本") - -# 创建3D文本 -text3d = world.createGUI3DText(pos=(0, 5, 2), text="3D文本") -``` - -### 编辑GUI元素 - -#### 属性面板编辑 -1. 在层级窗口选中GUI元素 -2. 在属性面板中修改: - - 文本内容 - - 位置坐标 - - 缩放大小 - - 颜色属性 - -#### 右键菜单编辑 -1. 右键点击场景树中的GUI元素 -2. 选择"编辑"打开编辑对话框 -3. 批量修改属性 - -#### 程序化编辑 -```python -# 修改文本 -world.editGUIElement(gui_element, "text", "新文本") - -# 修改位置 -world.editGUIElement(gui_element, "position", [1, 2, 3]) - -# 修改缩放 -world.editGUIElement(gui_element, "scale", 1.5) -``` - -### 删除GUI元素 - -#### 方法1: 右键删除 -``` -场景树中右键GUI元素 → 删除GUI元素 -``` - -#### 方法2: 程序化删除 -```python -world.deleteGUIElement(gui_element) -``` - ---- - -## 🎛️ 界面布局 - -### 场景树组织 -``` -场景 -├── 相机 -├── 模型 -│ └── [所有3D模型] -├── GUI元素 -│ ├── button: 按钮文本 -│ ├── label: 标签文本 -│ ├── 3d_text: 3D文本 -│ └── virtual_screen: 屏幕文本 -└── 地板 -``` - -### 属性面板 -- **通用属性**: 名称、GUI类型 -- **文本属性**: 可编辑的文本内容 -- **位置属性**: X/Y/Z坐标控制 -- **缩放属性**: 统一缩放控制 -- **颜色属性**: 2D GUI背景色选择 -- **旋转属性**: 3D GUI的H/P/R旋转 - ---- - -## 🔧 高级功能 - -### GUI元素复制 -``` -右键GUI元素 → 复制 -``` -自动在原位置偏移处创建副本。 - -### 批量编辑 -通过编辑对话框可以一次性修改多个属性。 - -### 事件响应 -```python -def onGUIButtonClick(self, button_id): - """自定义按钮点击处理""" - print(f"按钮 {button_id} 被点击") - # 添加你的自定义逻辑 - -def onGUIEntrySubmit(self, text, entry_id): - """自定义输入框提交处理""" - print(f"输入内容: {text}") - # 添加你的自定义逻辑 -``` - ---- - -## 💡 使用技巧 - -### 1. 中文字体支持 -程序会自动检测并加载系统中文字体: -- 微米黑体 (wqy-microhei) -- 文泉驿正黑 (wqy-zenhei) -- 文鼎字体系列 - -### 2. 3D GUI最佳实践 -- 3D文本适合显示场景标注 -- 虚拟屏幕适合信息面板 -- 合理使用Billboard效果 - -### 3. 性能优化 -- 避免创建过多GUI元素 -- 适时删除不需要的元素 -- 使用合适的缩放比例 - -### 4. 交互设计 -- 按钮提供即时反馈 -- 输入框有明确的提示 -- 颜色搭配要易于识别 - ---- - -## 🚀 扩展开发 - -### 添加新GUI类型 -在`MyWorld`类中添加新的创建方法: - -```python -def createGUISlider(self, pos=(0, 0, 0), range=(0, 100)): - """创建滑块组件""" - from direct.gui.DirectGui import DirectSlider - - slider = DirectSlider( - range=range, - pos=pos, - scale=0.5, - # ... 其他属性 - ) - - slider.setTag("gui_type", "slider") - slider.setTag("gui_id", f"slider_{len(self.gui_elements)}") - - self.gui_elements.append(slider) - self.updateSceneTree() - return slider -``` - -### 自定义事件处理 -修改事件处理方法来实现特定功能: - -```python -def onGUIButtonClick(self, button_id): - """自定义按钮行为""" - if button_id == "button_0": - # 第一个按钮的特殊行为 - self.performSpecialAction() - elif button_id.startswith("menu_"): - # 菜单按钮的处理 - self.handleMenuAction(button_id) -``` - ---- - -## 🎨 界面定制 - -### 主题配置 -```python -# 在创建GUI时设置主题色 -button = DirectButton( - frameColor=(0.2, 0.6, 0.8, 1), # 蓝色主题 - text_fg=(1, 1, 1, 1), # 白色文字 -) -``` - -### 布局管理 -建议将相关GUI元素进行分组管理: - -```python -# 创建UI组 -ui_group = { - 'main_menu': [], - 'game_hud': [], - 'settings': [] -} - -# 按组管理GUI元素 -ui_group['main_menu'].append(start_button) -ui_group['main_menu'].append(exit_button) -``` - ---- - -## 📚 API参考 - -### 核心方法 -- `createGUIButton(pos, text, size)` - 创建按钮 -- `createGUILabel(pos, text, size)` - 创建标签 -- `createGUIEntry(pos, placeholder, size)` - 创建输入框 -- `createGUI3DText(pos, text, size)` - 创建3D文本 -- `createGUIVirtualScreen(pos, size, text)` - 创建虚拟屏幕 -- `editGUIElement(element, property, value)` - 编辑元素 -- `deleteGUIElement(element)` - 删除元素 -- `duplicateGUIElement(element)` - 复制元素 - -### 属性类型 -- `"text"` - 文本内容 -- `"position"` - 位置坐标 [x, y, z] -- `"scale"` - 缩放大小 -- `"color"` - 颜色值 [r, g, b, a] - ---- - -现在你的`test.py`程序具备了完整的GUI创建、编辑、删除功能!🎉 - +# 🎯 GUI功能实现完整指南 + +## 📋 功能概述 + +我已经为你的`test.py`程序添加了完整的GUI管理功能,包括: + +### ✨ 核心功能 +- **GUI元素创建** - 支持多种2D和3D GUI组件 +- **GUI元素编辑** - 实时属性修改和可视化编辑 +- **GUI元素删除** - 安全的元素移除机制 +- **场景树显示** - GUI元素在层级窗口中的组织 +- **属性面板** - 专门的GUI属性编辑界面 + +--- + +## 🎮 支持的GUI类型 + +### 📱 2D GUI组件 +1. **DirectButton (按钮)** + - 可点击的交互按钮 + - 支持自定义文本和颜色 + - 带有点击事件响应 + +2. **DirectLabel (标签)** + - 静态文本显示 + - 透明背景 + - 可自定义字体和颜色 + +3. **DirectEntry (输入框)** + - 文本输入组件 + - 支持占位符文本 + - 带有提交事件 + +### 🌐 3D空间GUI组件 +1. **3D TextNode (3D文本)** + - 3D空间中的文本节点 + - Billboard效果(始终面向相机) + - 支持中文字体 + +2. **Virtual Screen (虚拟屏幕)** + - 3D空间中的平面屏幕 + - 可显示文本内容 + - 半透明效果 + +--- + +## 🛠️ 使用方法 + +### 创建GUI元素 + +#### 方法1: 使用菜单 +``` +GUI菜单 → 创建按钮/标签/输入框/3D文本/虚拟屏幕 +``` + +#### 方法2: 使用工具栏 +``` +工具栏 → 创建按钮/创建标签/3D文本 +``` + +#### 方法3: 程序化创建 +```python +# 创建按钮 +button = world.createGUIButton(pos=(0, 0, 0.5), text="我的按钮") + +# 创建标签 +label = world.createGUILabel(pos=(0, 0, 0.3), text="标签文本") + +# 创建3D文本 +text3d = world.createGUI3DText(pos=(0, 5, 2), text="3D文本") +``` + +### 编辑GUI元素 + +#### 属性面板编辑 +1. 在层级窗口选中GUI元素 +2. 在属性面板中修改: + - 文本内容 + - 位置坐标 + - 缩放大小 + - 颜色属性 + +#### 右键菜单编辑 +1. 右键点击场景树中的GUI元素 +2. 选择"编辑"打开编辑对话框 +3. 批量修改属性 + +#### 程序化编辑 +```python +# 修改文本 +world.editGUIElement(gui_element, "text", "新文本") + +# 修改位置 +world.editGUIElement(gui_element, "position", [1, 2, 3]) + +# 修改缩放 +world.editGUIElement(gui_element, "scale", 1.5) +``` + +### 删除GUI元素 + +#### 方法1: 右键删除 +``` +场景树中右键GUI元素 → 删除GUI元素 +``` + +#### 方法2: 程序化删除 +```python +world.deleteGUIElement(gui_element) +``` + +--- + +## 🎛️ 界面布局 + +### 场景树组织 +``` +场景 +├── 相机 +├── 模型 +│ └── [所有3D模型] +├── GUI元素 +│ ├── button: 按钮文本 +│ ├── label: 标签文本 +│ ├── 3d_text: 3D文本 +│ └── virtual_screen: 屏幕文本 +└── 地板 +``` + +### 属性面板 +- **通用属性**: 名称、GUI类型 +- **文本属性**: 可编辑的文本内容 +- **位置属性**: X/Y/Z坐标控制 +- **缩放属性**: 统一缩放控制 +- **颜色属性**: 2D GUI背景色选择 +- **旋转属性**: 3D GUI的H/P/R旋转 + +--- + +## 🔧 高级功能 + +### GUI元素复制 +``` +右键GUI元素 → 复制 +``` +自动在原位置偏移处创建副本。 + +### 批量编辑 +通过编辑对话框可以一次性修改多个属性。 + +### 事件响应 +```python +def onGUIButtonClick(self, button_id): + """自定义按钮点击处理""" + print(f"按钮 {button_id} 被点击") + # 添加你的自定义逻辑 + +def onGUIEntrySubmit(self, text, entry_id): + """自定义输入框提交处理""" + print(f"输入内容: {text}") + # 添加你的自定义逻辑 +``` + +--- + +## 💡 使用技巧 + +### 1. 中文字体支持 +程序会自动检测并加载系统中文字体: +- 微米黑体 (wqy-microhei) +- 文泉驿正黑 (wqy-zenhei) +- 文鼎字体系列 + +### 2. 3D GUI最佳实践 +- 3D文本适合显示场景标注 +- 虚拟屏幕适合信息面板 +- 合理使用Billboard效果 + +### 3. 性能优化 +- 避免创建过多GUI元素 +- 适时删除不需要的元素 +- 使用合适的缩放比例 + +### 4. 交互设计 +- 按钮提供即时反馈 +- 输入框有明确的提示 +- 颜色搭配要易于识别 + +--- + +## 🚀 扩展开发 + +### 添加新GUI类型 +在`MyWorld`类中添加新的创建方法: + +```python +def createGUISlider(self, pos=(0, 0, 0), range=(0, 100)): + """创建滑块组件""" + from direct.gui.DirectGui import DirectSlider + + slider = DirectSlider( + range=range, + pos=pos, + scale=0.5, + # ... 其他属性 + ) + + slider.setTag("gui_type", "slider") + slider.setTag("gui_id", f"slider_{len(self.gui_elements)}") + + self.gui_elements.append(slider) + self.updateSceneTree() + return slider +``` + +### 自定义事件处理 +修改事件处理方法来实现特定功能: + +```python +def onGUIButtonClick(self, button_id): + """自定义按钮行为""" + if button_id == "button_0": + # 第一个按钮的特殊行为 + self.performSpecialAction() + elif button_id.startswith("menu_"): + # 菜单按钮的处理 + self.handleMenuAction(button_id) +``` + +--- + +## 🎨 界面定制 + +### 主题配置 +```python +# 在创建GUI时设置主题色 +button = DirectButton( + frameColor=(0.2, 0.6, 0.8, 1), # 蓝色主题 + text_fg=(1, 1, 1, 1), # 白色文字 +) +``` + +### 布局管理 +建议将相关GUI元素进行分组管理: + +```python +# 创建UI组 +ui_group = { + 'main_menu': [], + 'game_hud': [], + 'settings': [] +} + +# 按组管理GUI元素 +ui_group['main_menu'].append(start_button) +ui_group['main_menu'].append(exit_button) +``` + +--- + +## 📚 API参考 + +### 核心方法 +- `createGUIButton(pos, text, size)` - 创建按钮 +- `createGUILabel(pos, text, size)` - 创建标签 +- `createGUIEntry(pos, placeholder, size)` - 创建输入框 +- `createGUI3DText(pos, text, size)` - 创建3D文本 +- `createGUIVirtualScreen(pos, size, text)` - 创建虚拟屏幕 +- `editGUIElement(element, property, value)` - 编辑元素 +- `deleteGUIElement(element)` - 删除元素 +- `duplicateGUIElement(element)` - 复制元素 + +### 属性类型 +- `"text"` - 文本内容 +- `"position"` - 位置坐标 [x, y, z] +- `"scale"` - 缩放大小 +- `"color"` - 颜色值 [r, g, b, a] + +--- + +现在你的`test.py`程序具备了完整的GUI创建、编辑、删除功能!🎉 + 试试通过菜单或工具栏创建一些GUI元素,然后在属性面板中编辑它们的属性。GUI元素会自动出现在场景树的"GUI元素"分组中,方便管理。 \ No newline at end of file diff --git a/demo/GUI_README.md b/demo/GUI_README.md index 40ccaecf..06f4df3d 100644 --- a/demo/GUI_README.md +++ b/demo/GUI_README.md @@ -1,262 +1,262 @@ -# PANDA3D GUI功能测试文件 - -本项目包含了多个PANDA3D GUI功能的测试示例,帮助您了解和学习PANDA3D的GUI系统。 - -## 文件说明 - -### 1. `basic_gui_demo.py` - 基础GUI演示 🌟 -**推荐从这个开始学习** - -这是最简单的GUI示例,包含: -- 基本按钮(DirectButton) -- 文本标签(DirectLabel) -- 屏幕文本(OnscreenText) -- 简单的事件处理 -- 按钮颜色变化效果 - -**运行方式:** -```bash -python3 basic_gui_demo.py -``` - -**功能:** -- 点击"点击我!"按钮可以看到计数增加 -- 按钮颜色会循环变化 -- 状态标签会显示当前点击次数 -- 红色"退出"按钮可以关闭程序 - ---- - -### 2. `simple_gui_test.py` - 完整GUI组件测试 ⭐⭐ -**中级学习示例** - -包含更多GUI组件: -- 多种按钮样式 -- 文本输入框(DirectEntry) -- 滑块控件(DirectSlider) -- 复选框(DirectCheckButton) -- 选项菜单(DirectOptionMenu) -- 实时状态更新 - -**运行方式:** -```bash -python3 simple_gui_test.py -``` - -**功能:** -- 测试按钮:点击增加计数 -- 重置按钮:重置所有控件状态 -- 文本输入:在输入框中输入文本并按回车 -- 滑块:拖动查看数值实时变化 -- 复选框:切换特殊模式 -- 选项菜单:选择不同选项 -- 状态栏显示当前操作 - ---- - -### 3. `gui_test.py` - 高级GUI功能展示 ⭐⭐⭐ -**高级功能演示** - -最复杂的GUI示例,包含: -- 所有DirectGUI组件 -- 单选按钮组(DirectRadioButton) -- 滚动框架(DirectScrolledFrame) -- 密码输入框 -- 复杂的事件处理系统 -- 自定义字体加载 - -**运行方式:** -```bash -python3 gui_test.py -``` - -**注意:** 如果字体加载失败,程序可能无法正常运行。 - ---- - -### 4. `gui_3d_demo.py` - 3D空间GUI演示 🚀 -**3D GUI概念展示** - -展示如何在3D空间中使用GUI: -- 2D界面控制3D场景 -- 3D空间中的文本元素 -- 虚拟3D屏幕 -- 3D对象与GUI交互 -- 动画和场景控制 - -**运行方式:** -```bash -python3 gui_3d_demo.py -``` - -**功能:** -- 旋转场景:让3D对象旋转 -- 变换颜色:改变场景颜色方案 -- 重置:恢复初始状态 -- 3D文本:在3D空间中显示文字 -- 虚拟屏幕:3D空间中的信息显示板 - ---- - -### 5. `chinese_gui_test.py` - 中文字体支持测试 🇨🇳 -**中文字体显示解决方案** - -专门解决中文字体在PANDA3D中的显示问题: -- 自动检测和加载中文字体 -- 支持OnscreenText中文显示 -- 支持DirectButton中文显示 -- 字体加载状态检测 -- 多种中文字体路径支持 - -**运行方式:** -```bash -python3 chinese_gui_test.py -``` - -**功能:** -- 测试中文字体在各种GUI组件中的显示 -- 显示字体加载状态和建议 -- 演示正确的中文字体使用方法 - -### 📊 中文字体修复状态 - -| 文件 | 修复状态 | 支持组件 | -|------|----------|----------| -| `chinese_gui_test.py` | ✅ 完成 | OnscreenText, DirectButton | -| `gui_test.py` | ✅ 完成 | 所有DirectGUI组件 | -| `simple_gui_test.py` | ✅ 完成 | 基础GUI组件 | -| `basic_gui_demo.py` | ✅ 完成 | 基础组件 | -| `gui_3d_demo.py` | ✅ 完成 | 2D/3D文本组件 | - ---- - -## PANDA3D GUI系统概述 - -### DirectGUI 组件 - -PANDA3D的DirectGUI系统提供了以下主要组件: - -1. **DirectButton** - 按钮 - - 可自定义颜色、大小、文本 - - 支持点击事件处理 - -2. **DirectLabel** - 标签 - - 显示静态文本 - - 可设置背景和文字颜色 - -3. **DirectEntry** - 文本输入框 - - 单行或多行文本输入 - - 支持密码模式(obscured) - -4. **DirectSlider** - 滑块 - - 水平或垂直方向 - - 数值范围可自定义 - -5. **DirectCheckButton** - 复选框 - - 开/关状态切换 - - 可绑定状态变化事件 - -6. **DirectRadioButton** - 单选按钮 - - 单选按钮组 - - 共享变量控制选择状态 - -7. **DirectOptionMenu** - 选项菜单 - - 下拉选择列表 - - 支持动态选项更新 - -8. **DirectScrolledFrame** - 滚动框架 - - 可滚动的内容区域 - - 支持垂直和水平滚动 - -### 3D空间GUI - -PANDA3D还支持在3D空间中创建GUI元素: - -1. **TextNode** - 3D文本节点 - - 可放置在3D空间任意位置 - - 支持billboard效果(总是面向相机) - -2. **虚拟屏幕** - 使用CardMaker创建 - - 3D空间中的平面显示区域 - - 可在上面显示文本或图像 - -3. **混合GUI** - 2D控制3D场景 - - 2D界面控制3D对象 - - 实时交互和反馈 - -### 中文字体支持 - -PANDA3D的中文字体支持需要特殊处理: - -1. **OnscreenText** - 屏幕文本 - ```python - text = OnscreenText( - text="中文文本", - font=chinese_font # 设置中文字体 - ) - ``` - -2. **DirectButton** - 按钮文本 - ```python - button = DirectButton( - text="按钮文本", - text_font=chinese_font # 注意使用text_font参数 - ) - ``` - -3. **字体加载** - 自动检测中文字体 - ```python - font_paths = [ - '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', - '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc' - ] - ``` - ---- - -## 学习建议 - -1. **从基础开始**:先运行 `basic_gui_demo.py` -2. **逐步深入**:然后尝试 `simple_gui_test.py` -3. **探索高级功能**:学习 `gui_test.py` 中的复杂组件 -4. **创新应用**:最后体验 `gui_3d_demo.py` 的3D GUI概念 - -## 常见问题 - -### Q: 程序无法启动? -A: 确保已正确安装PANDA3D:`pip install panda3d` - -### Q: 字体加载失败? -A: 使用 `basic_gui_demo.py` 或 `simple_gui_test.py`,它们不依赖外部字体 - -### Q: 中文显示为方块? -A: 需要安装中文字体: -```bash -# 运行字体安装脚本 -bash install_chinese_fonts.sh - -# 或手动安装 -sudo apt-get install fonts-wqy-microhei fonts-wqy-zenhei -``` - -### Q: 按钮中的中文无法显示? -A: DirectButton需要使用 `text_font` 参数来设置字体,参考 `chinese_gui_test.py` 的解决方案 - -### Q: 如何自定义GUI样式? -A: 查看代码中的 `frameColor`、`text_fg` 等参数设置 - -### Q: 如何添加更多交互? -A: 参考现有的事件处理函数,如 `onButtonClick()` 等 - ---- - -## 扩展学习 - -这些示例为您提供了PANDA3D GUI系统的基础。您可以: - -1. 修改现有代码,尝试不同的参数设置 -2. 组合不同的GUI组件创建自己的界面 -3. 将GUI集成到您的3D应用程序中 -4. 探索PANDA3D的其他高级GUI功能 - +# PANDA3D GUI功能测试文件 + +本项目包含了多个PANDA3D GUI功能的测试示例,帮助您了解和学习PANDA3D的GUI系统。 + +## 文件说明 + +### 1. `basic_gui_demo.py` - 基础GUI演示 🌟 +**推荐从这个开始学习** + +这是最简单的GUI示例,包含: +- 基本按钮(DirectButton) +- 文本标签(DirectLabel) +- 屏幕文本(OnscreenText) +- 简单的事件处理 +- 按钮颜色变化效果 + +**运行方式:** +```bash +python3 basic_gui_demo.py +``` + +**功能:** +- 点击"点击我!"按钮可以看到计数增加 +- 按钮颜色会循环变化 +- 状态标签会显示当前点击次数 +- 红色"退出"按钮可以关闭程序 + +--- + +### 2. `simple_gui_test.py` - 完整GUI组件测试 ⭐⭐ +**中级学习示例** + +包含更多GUI组件: +- 多种按钮样式 +- 文本输入框(DirectEntry) +- 滑块控件(DirectSlider) +- 复选框(DirectCheckButton) +- 选项菜单(DirectOptionMenu) +- 实时状态更新 + +**运行方式:** +```bash +python3 simple_gui_test.py +``` + +**功能:** +- 测试按钮:点击增加计数 +- 重置按钮:重置所有控件状态 +- 文本输入:在输入框中输入文本并按回车 +- 滑块:拖动查看数值实时变化 +- 复选框:切换特殊模式 +- 选项菜单:选择不同选项 +- 状态栏显示当前操作 + +--- + +### 3. `gui_test.py` - 高级GUI功能展示 ⭐⭐⭐ +**高级功能演示** + +最复杂的GUI示例,包含: +- 所有DirectGUI组件 +- 单选按钮组(DirectRadioButton) +- 滚动框架(DirectScrolledFrame) +- 密码输入框 +- 复杂的事件处理系统 +- 自定义字体加载 + +**运行方式:** +```bash +python3 gui_test.py +``` + +**注意:** 如果字体加载失败,程序可能无法正常运行。 + +--- + +### 4. `gui_3d_demo.py` - 3D空间GUI演示 🚀 +**3D GUI概念展示** + +展示如何在3D空间中使用GUI: +- 2D界面控制3D场景 +- 3D空间中的文本元素 +- 虚拟3D屏幕 +- 3D对象与GUI交互 +- 动画和场景控制 + +**运行方式:** +```bash +python3 gui_3d_demo.py +``` + +**功能:** +- 旋转场景:让3D对象旋转 +- 变换颜色:改变场景颜色方案 +- 重置:恢复初始状态 +- 3D文本:在3D空间中显示文字 +- 虚拟屏幕:3D空间中的信息显示板 + +--- + +### 5. `chinese_gui_test.py` - 中文字体支持测试 🇨🇳 +**中文字体显示解决方案** + +专门解决中文字体在PANDA3D中的显示问题: +- 自动检测和加载中文字体 +- 支持OnscreenText中文显示 +- 支持DirectButton中文显示 +- 字体加载状态检测 +- 多种中文字体路径支持 + +**运行方式:** +```bash +python3 chinese_gui_test.py +``` + +**功能:** +- 测试中文字体在各种GUI组件中的显示 +- 显示字体加载状态和建议 +- 演示正确的中文字体使用方法 + +### 📊 中文字体修复状态 + +| 文件 | 修复状态 | 支持组件 | +|------|----------|----------| +| `chinese_gui_test.py` | ✅ 完成 | OnscreenText, DirectButton | +| `gui_test.py` | ✅ 完成 | 所有DirectGUI组件 | +| `simple_gui_test.py` | ✅ 完成 | 基础GUI组件 | +| `basic_gui_demo.py` | ✅ 完成 | 基础组件 | +| `gui_3d_demo.py` | ✅ 完成 | 2D/3D文本组件 | + +--- + +## PANDA3D GUI系统概述 + +### DirectGUI 组件 + +PANDA3D的DirectGUI系统提供了以下主要组件: + +1. **DirectButton** - 按钮 + - 可自定义颜色、大小、文本 + - 支持点击事件处理 + +2. **DirectLabel** - 标签 + - 显示静态文本 + - 可设置背景和文字颜色 + +3. **DirectEntry** - 文本输入框 + - 单行或多行文本输入 + - 支持密码模式(obscured) + +4. **DirectSlider** - 滑块 + - 水平或垂直方向 + - 数值范围可自定义 + +5. **DirectCheckButton** - 复选框 + - 开/关状态切换 + - 可绑定状态变化事件 + +6. **DirectRadioButton** - 单选按钮 + - 单选按钮组 + - 共享变量控制选择状态 + +7. **DirectOptionMenu** - 选项菜单 + - 下拉选择列表 + - 支持动态选项更新 + +8. **DirectScrolledFrame** - 滚动框架 + - 可滚动的内容区域 + - 支持垂直和水平滚动 + +### 3D空间GUI + +PANDA3D还支持在3D空间中创建GUI元素: + +1. **TextNode** - 3D文本节点 + - 可放置在3D空间任意位置 + - 支持billboard效果(总是面向相机) + +2. **虚拟屏幕** - 使用CardMaker创建 + - 3D空间中的平面显示区域 + - 可在上面显示文本或图像 + +3. **混合GUI** - 2D控制3D场景 + - 2D界面控制3D对象 + - 实时交互和反馈 + +### 中文字体支持 + +PANDA3D的中文字体支持需要特殊处理: + +1. **OnscreenText** - 屏幕文本 + ```python + text = OnscreenText( + text="中文文本", + font=chinese_font # 设置中文字体 + ) + ``` + +2. **DirectButton** - 按钮文本 + ```python + button = DirectButton( + text="按钮文本", + text_font=chinese_font # 注意使用text_font参数 + ) + ``` + +3. **字体加载** - 自动检测中文字体 + ```python + font_paths = [ + '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', + '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc' + ] + ``` + +--- + +## 学习建议 + +1. **从基础开始**:先运行 `basic_gui_demo.py` +2. **逐步深入**:然后尝试 `simple_gui_test.py` +3. **探索高级功能**:学习 `gui_test.py` 中的复杂组件 +4. **创新应用**:最后体验 `gui_3d_demo.py` 的3D GUI概念 + +## 常见问题 + +### Q: 程序无法启动? +A: 确保已正确安装PANDA3D:`pip install panda3d` + +### Q: 字体加载失败? +A: 使用 `basic_gui_demo.py` 或 `simple_gui_test.py`,它们不依赖外部字体 + +### Q: 中文显示为方块? +A: 需要安装中文字体: +```bash +# 运行字体安装脚本 +bash install_chinese_fonts.sh + +# 或手动安装 +sudo apt-get install fonts-wqy-microhei fonts-wqy-zenhei +``` + +### Q: 按钮中的中文无法显示? +A: DirectButton需要使用 `text_font` 参数来设置字体,参考 `chinese_gui_test.py` 的解决方案 + +### Q: 如何自定义GUI样式? +A: 查看代码中的 `frameColor`、`text_fg` 等参数设置 + +### Q: 如何添加更多交互? +A: 参考现有的事件处理函数,如 `onButtonClick()` 等 + +--- + +## 扩展学习 + +这些示例为您提供了PANDA3D GUI系统的基础。您可以: + +1. 修改现有代码,尝试不同的参数设置 +2. 组合不同的GUI组件创建自己的界面 +3. 将GUI集成到您的3D应用程序中 +4. 探索PANDA3D的其他高级GUI功能 + 祝您学习愉快!🎮 \ No newline at end of file diff --git a/demo/GUI_TESTING_README.md b/demo/GUI_TESTING_README.md index 8cc5661d..44caa17d 100644 --- a/demo/GUI_TESTING_README.md +++ b/demo/GUI_TESTING_README.md @@ -1,162 +1,162 @@ -# 🧪 GUI功能测试和故障排除指南 - -## 📋 问题修复说明 - -已经修复了GUI文本编辑功能中的`TextNode`导入问题和逻辑错误。现在GUI文本编辑应该可以正常工作了。 - -## 🔧 主要修复内容 - -### 1. 导入问题修复 -- 在`editGUIElement`方法中添加了`TextNode`导入 -- 解决了"name 'TextNode' is not defined"错误 - -### 2. 编辑逻辑优化 -- **3D文本**: 直接修改自身的TextNode,因为3D文本本身就是TextNode -- **虚拟屏幕**: 查找TextNode子节点并修改其内容 -- **2D GUI**: 使用标准的DirectGUI属性修改方式 - -### 3. 调试信息增强 -- 添加了详细的调试输出 -- 显示节点类型和子节点信息 -- 便于排查问题 - ---- - -## 🎯 如何测试GUI功能 - -### 方法1: 使用主程序测试 -1. 运行`python3 test.py` -2. 通过菜单或工具栏创建GUI元素: - - `GUI菜单 → 创建按钮/标签/3D文本/虚拟屏幕` - - 或点击工具栏中的GUI创建按钮 -3. 在场景树中选择GUI元素 -4. 在属性面板中编辑文本内容 -5. 检查GUI元素是否实时更新 - -### 方法2: 使用测试脚本 -```bash -python3 test_gui_edit.py -``` - -测试脚本提供以下功能: -- 按`Space`键:循环测试所有GUI元素的文本编辑 -- 按`1`键:单独测试按钮文本编辑 -- 按`2`键:单独测试标签文本编辑 -- 按`3`键:单独测试3D文本编辑 - ---- - -## 🔍 故障排除 - -### 常见问题和解决方案 - -#### 1. "TextNode未定义"错误 -**症状**: 控制台显示"name 'TextNode' is not defined" -**解决**: 已修复,确保使用最新版本的代码 - -#### 2. 3D文本无法编辑 -**症状**: 3D文本创建成功但编辑时文本不变化 -**排查步骤**: -1. 检查控制台输出,查看是否显示"成功更新3D文本" -2. 确认3D文本的节点类型是TextNode -3. 查看调试信息中的节点类型报告 - -#### 3. 虚拟屏幕文本无法编辑 -**症状**: 虚拟屏幕的文本内容不更新 -**排查步骤**: -1. 查看控制台输出中的子节点数量 -2. 检查是否找到TextNode类型的子节点 -3. 确认虚拟屏幕创建时正确添加了文本子节点 - -#### 4. 属性面板编辑无响应 -**症状**: 在属性面板中输入文本但GUI不更新 -**排查步骤**: -1. 检查场景树中是否正确选中了GUI元素 -2. 确认GUI元素的类型标签设置正确 -3. 查看控制台是否有错误信息 - ---- - -## 🧪 测试用例 - -### 测试用例1: 2D GUI文本编辑 -```python -# 创建按钮 -button = world.createGUIButton(pos=(0, 0, 0.5), text="原始按钮") - -# 编辑文本 -success = world.editGUIElement(button, "text", "修改后的按钮") - -# 期望结果: 按钮显示"修改后的按钮" -``` - -### 测试用例2: 3D文本编辑 -```python -# 创建3D文本 -text3d = world.createGUI3DText(pos=(0, 5, 2), text="原始3D文本") - -# 编辑文本 -success = world.editGUIElement(text3d, "text", "修改后的3D文本") - -# 期望结果: 3D文本显示"修改后的3D文本" -``` - -### 测试用例3: 虚拟屏幕文本编辑 -```python -# 创建虚拟屏幕 -screen = world.createGUIVirtualScreen(pos=(3, 5, 0), text="原始屏幕文本") - -# 编辑文本 -success = world.editGUIElement(screen, "text", "修改后的屏幕文本") - -# 期望结果: 虚拟屏幕显示"修改后的屏幕文本" -``` - ---- - -## 📊 调试输出示例 - -### 正常工作的输出 -``` -开始编辑GUI元素: 类型=button, 属性=text, 值=新按钮文本 -成功更新2D GUI文本: 新按钮文本 -编辑GUI元素 button: text = 新按钮文本 -``` - -### 3D文本编辑输出 -``` -开始编辑GUI元素: 类型=3d_text, 属性=text, 值=新3D文本 -成功更新3D文本: 新3D文本 -编辑GUI元素 3d_text: text = 新3D文本 -``` - -### 虚拟屏幕编辑输出 -``` -开始编辑GUI元素: 类型=virtual_screen, 属性=text, 值=新屏幕文本 -虚拟屏幕有 1 个子节点 -子节点 0: screen-text-0, 类型: -成功更新虚拟屏幕文本: 新屏幕文本 -编辑GUI元素 virtual_screen: text = 新屏幕文本 -``` - ---- - -## 🚀 进一步测试建议 - -1. **批量测试**: 创建多个GUI元素并逐一测试编辑功能 -2. **交互测试**: 测试右键菜单的编辑功能 -3. **属性面板测试**: 测试实时属性编辑 -4. **复制功能测试**: 测试GUI元素的复制功能 -5. **删除功能测试**: 确保删除功能正常工作 - ---- - -## 💡 开发提示 - -如果需要添加新的GUI类型,请确保: -1. 在创建时正确设置`gui_type`标签 -2. 在`editGUIElement`中添加对应的编辑逻辑 -3. 在`updateGUIPropertyPanel`中添加属性面板支持 -4. 编写相应的测试用例 - +# 🧪 GUI功能测试和故障排除指南 + +## 📋 问题修复说明 + +已经修复了GUI文本编辑功能中的`TextNode`导入问题和逻辑错误。现在GUI文本编辑应该可以正常工作了。 + +## 🔧 主要修复内容 + +### 1. 导入问题修复 +- 在`editGUIElement`方法中添加了`TextNode`导入 +- 解决了"name 'TextNode' is not defined"错误 + +### 2. 编辑逻辑优化 +- **3D文本**: 直接修改自身的TextNode,因为3D文本本身就是TextNode +- **虚拟屏幕**: 查找TextNode子节点并修改其内容 +- **2D GUI**: 使用标准的DirectGUI属性修改方式 + +### 3. 调试信息增强 +- 添加了详细的调试输出 +- 显示节点类型和子节点信息 +- 便于排查问题 + +--- + +## 🎯 如何测试GUI功能 + +### 方法1: 使用主程序测试 +1. 运行`python3 test.py` +2. 通过菜单或工具栏创建GUI元素: + - `GUI菜单 → 创建按钮/标签/3D文本/虚拟屏幕` + - 或点击工具栏中的GUI创建按钮 +3. 在场景树中选择GUI元素 +4. 在属性面板中编辑文本内容 +5. 检查GUI元素是否实时更新 + +### 方法2: 使用测试脚本 +```bash +python3 test_gui_edit.py +``` + +测试脚本提供以下功能: +- 按`Space`键:循环测试所有GUI元素的文本编辑 +- 按`1`键:单独测试按钮文本编辑 +- 按`2`键:单独测试标签文本编辑 +- 按`3`键:单独测试3D文本编辑 + +--- + +## 🔍 故障排除 + +### 常见问题和解决方案 + +#### 1. "TextNode未定义"错误 +**症状**: 控制台显示"name 'TextNode' is not defined" +**解决**: 已修复,确保使用最新版本的代码 + +#### 2. 3D文本无法编辑 +**症状**: 3D文本创建成功但编辑时文本不变化 +**排查步骤**: +1. 检查控制台输出,查看是否显示"成功更新3D文本" +2. 确认3D文本的节点类型是TextNode +3. 查看调试信息中的节点类型报告 + +#### 3. 虚拟屏幕文本无法编辑 +**症状**: 虚拟屏幕的文本内容不更新 +**排查步骤**: +1. 查看控制台输出中的子节点数量 +2. 检查是否找到TextNode类型的子节点 +3. 确认虚拟屏幕创建时正确添加了文本子节点 + +#### 4. 属性面板编辑无响应 +**症状**: 在属性面板中输入文本但GUI不更新 +**排查步骤**: +1. 检查场景树中是否正确选中了GUI元素 +2. 确认GUI元素的类型标签设置正确 +3. 查看控制台是否有错误信息 + +--- + +## 🧪 测试用例 + +### 测试用例1: 2D GUI文本编辑 +```python +# 创建按钮 +button = world.createGUIButton(pos=(0, 0, 0.5), text="原始按钮") + +# 编辑文本 +success = world.editGUIElement(button, "text", "修改后的按钮") + +# 期望结果: 按钮显示"修改后的按钮" +``` + +### 测试用例2: 3D文本编辑 +```python +# 创建3D文本 +text3d = world.createGUI3DText(pos=(0, 5, 2), text="原始3D文本") + +# 编辑文本 +success = world.editGUIElement(text3d, "text", "修改后的3D文本") + +# 期望结果: 3D文本显示"修改后的3D文本" +``` + +### 测试用例3: 虚拟屏幕文本编辑 +```python +# 创建虚拟屏幕 +screen = world.createGUIVirtualScreen(pos=(3, 5, 0), text="原始屏幕文本") + +# 编辑文本 +success = world.editGUIElement(screen, "text", "修改后的屏幕文本") + +# 期望结果: 虚拟屏幕显示"修改后的屏幕文本" +``` + +--- + +## 📊 调试输出示例 + +### 正常工作的输出 +``` +开始编辑GUI元素: 类型=button, 属性=text, 值=新按钮文本 +成功更新2D GUI文本: 新按钮文本 +编辑GUI元素 button: text = 新按钮文本 +``` + +### 3D文本编辑输出 +``` +开始编辑GUI元素: 类型=3d_text, 属性=text, 值=新3D文本 +成功更新3D文本: 新3D文本 +编辑GUI元素 3d_text: text = 新3D文本 +``` + +### 虚拟屏幕编辑输出 +``` +开始编辑GUI元素: 类型=virtual_screen, 属性=text, 值=新屏幕文本 +虚拟屏幕有 1 个子节点 +子节点 0: screen-text-0, 类型: +成功更新虚拟屏幕文本: 新屏幕文本 +编辑GUI元素 virtual_screen: text = 新屏幕文本 +``` + +--- + +## 🚀 进一步测试建议 + +1. **批量测试**: 创建多个GUI元素并逐一测试编辑功能 +2. **交互测试**: 测试右键菜单的编辑功能 +3. **属性面板测试**: 测试实时属性编辑 +4. **复制功能测试**: 测试GUI元素的复制功能 +5. **删除功能测试**: 确保删除功能正常工作 + +--- + +## 💡 开发提示 + +如果需要添加新的GUI类型,请确保: +1. 在创建时正确设置`gui_type`标签 +2. 在`editGUIElement`中添加对应的编辑逻辑 +3. 在`updateGUIPropertyPanel`中添加属性面板支持 +4. 编写相应的测试用例 + 现在GUI文本编辑功能应该可以正常工作了!🎉 \ No newline at end of file diff --git a/demo/GUI_编辑模式使用指南.md b/demo/GUI_编辑模式使用指南.md index 30e9f23c..0f1c65a8 100644 --- a/demo/GUI_编辑模式使用指南.md +++ b/demo/GUI_编辑模式使用指南.md @@ -1,231 +1,231 @@ -# GUI编辑模式使用指南 - -## 概述 - -主程序现在集成了完整的GUI编辑模式,基于 `test_qt_showbase.py` 中验证有效的方法实现。这个功能允许你在3D场景中轻松创建、编辑和管理各种GUI元素。 - -## 功能特点 - -### ✨ 核心功能 -- **可视化GUI编辑**: 右侧浮动面板提供直观的工具选择 -- **多种GUI类型**: 支持2D和3D GUI元素 -- **实时预览**: 即时查看创建的GUI效果 -- **属性编辑**: 通过属性面板编辑GUI元素属性 -- **场景集成**: GUI元素与3D场景完美融合 - -### 🎯 支持的GUI类型 - -#### 2D GUI元素 -1. **按钮 (Button)**: 可点击的2D按钮 -2. **标签 (Label)**: 显示文本信息的标签 -3. **输入框 (Entry)**: 用户可输入文本的输入框 - -#### 3D GUI元素 -1. **3D文本**: 在3D空间中显示的文本,支持广告牌效果 -2. **虚拟屏幕**: 3D空间中的虚拟显示屏 - -## 使用方法 - -### 🚀 启动GUI编辑模式 - -#### 方法1: 通过菜单 -1. 启动主程序 `python3 test.py` -2. 点击菜单栏 `GUI` -> `进入GUI编辑模式` -3. 右侧将出现GUI编辑面板 - -#### 方法2: 运行测试程序 -```bash -cd /home/hello/eg -python3 test_gui_edit_mode.py -``` - -### 🎨 创建GUI元素 - -#### 步骤1: 选择工具 -在右侧GUI编辑面板中: -- 点击 `按钮` - 创建2D按钮 -- 点击 `标签` - 创建2D标签 -- 点击 `输入框` - 创建2D输入框 -- 点击 `3D文本` - 创建3D文本 -- 点击 `虚拟屏幕` - 创建3D虚拟屏幕 - -#### 步骤2: 放置元素 -选择工具后: -1. 在3D场景中点击鼠标左键 -2. GUI元素将在点击位置创建 -3. 新创建的元素会自动选中并显示在层级树中 - -### 📝 编辑GUI元素 - -#### 选择元素 -- **方法1**: 在GUI编辑模式下直接点击GUI元素 -- **方法2**: 在左侧层级树中点击GUI元素 - -#### 编辑属性 -选中GUI元素后,右侧属性面板会显示可编辑的属性: - -**通用属性**: -- 名称 -- 位置 (X, Y, Z) -- 缩放 - -**特定属性**: -- **文本类元素**: 可编辑显示文本 -- **2D元素**: 屏幕坐标和实际坐标 -- **3D元素**: 世界坐标和旋转 - -### 🔧 管理GUI元素 - -#### 删除元素 -1. 选中要删除的GUI元素 -2. 点击GUI编辑面板中的 `删除` 按钮 -3. 或在层级树中右键选择 `删除GUI元素` - -#### 复制元素 -1. 选中要复制的GUI元素 -2. 点击GUI编辑面板中的 `复制` 按钮 -3. 或在层级树中右键选择 `复制` - -#### 在层级树中查看 -- 所有GUI元素显示在 `场景` -> `GUI元素` 节点下 -- 显示格式: `类型: 文本内容` -- 支持右键菜单操作 - -### 🎮 交互控制 - -#### 鼠标操作 -- **左键点击**: 选择GUI元素或创建新元素 -- **右键拖拽**: 移动选中的GUI元素 (仅3D元素) -- **中键拖拽**: Z轴移动 (仅3D元素) - -#### 键盘快捷键 -- **Delete**: 删除选中的GUI元素 -- **ESC**: 退出GUI编辑模式 (测试程序) - -### 💾 保存和加载 - -#### 保存项目 -GUI元素会随项目一起保存: -1. `文件` -> `保存` -2. GUI元素信息保存在BAM场景文件中 - -#### 加载项目 -加载项目时GUI元素会自动恢复: -1. `文件` -> `打开` -2. 选择包含GUI的项目文件夹 - -## 坐标系统说明 - -### 2D GUI坐标 -- **屏幕坐标系**: 范围 -1 到 1 -- **逻辑坐标**: 用户友好的坐标,自动转换为屏幕坐标 -- **转换公式**: `屏幕坐标 = 逻辑坐标 × 0.1` - -### 3D GUI坐标 -- **世界坐标系**: 与3D场景使用相同的坐标系 -- **支持完整的3D变换**: 位置、旋转、缩放 - -## 技术实现细节 - -### 基于验证有效的方法 -- 使用 `test_qt_showbase.py` 中验证的GUI创建逻辑 -- 确保在Qt集成环境中的稳定性 -- 统一的错误处理和调试输出 - -### 关键组件 -```python -# GUI编辑模式状态 -self.guiEditMode = False -self.guiEditPanel = None -self.currentGUITool = None - -# GUI创建方法 (基于test_qt_showbase.py) -def createGUIButton(pos, text, size) -def createGUILabel(pos, text, size) -def createGUIEntry(pos, placeholder, size) -def createGUI3DText(pos, text, size) -def createGUIVirtualScreen(pos, size, text) -``` - -### 事件处理 -- 鼠标事件优先处理GUI编辑模式 -- 碰撞检测用于GUI元素选择 -- 树形控件与场景实时同步 - -## 常见问题解答 - -### Q: GUI元素创建后看不到? -**A**: 检查以下几点: -1. 确保已进入GUI编辑模式 -2. 检查创建位置是否在视野范围内 -3. 查看控制台输出确认创建成功 - -### Q: 如何调整GUI元素大小? -**A**: -1. 选中GUI元素 -2. 在属性面板中修改 `缩放` 属性 -3. 或通过代码设置: `element.setScale(new_scale)` - -### Q: 2D GUI元素位置不准确? -**A**: -- 2D GUI使用屏幕坐标系 -- 在属性面板中查看 `实际屏幕坐标` -- 使用 `逻辑坐标` 进行编辑更容易 - -### Q: 中文字体显示问题? -**A**: -- 程序会自动尝试加载系统中文字体 -- 如果中文显示异常,检查系统是否安装了中文字体包 -- 支持的字体路径: `/usr/share/fonts/truetype/wqy/` - -### Q: 如何扩展新的GUI类型? -**A**: -1. 在 `MyWorld` 类中添加新的创建方法 -2. 在 `createGUIEditPanel()` 中添加对应按钮 -3. 在 `createGUIAtPosition()` 中添加处理逻辑 - -## 示例代码 - -### 程序化创建GUI元素 -```python -# 创建按钮 -button = world.createGUIButton((0, 0, 5), "我的按钮", 0.1) - -# 创建3D文本 -text3d = world.createGUI3DText((0, 5, 2), "3D文本", 0.5) - -# 创建虚拟屏幕 -screen = world.createGUIVirtualScreen((3, 8, 0), (2, 1), "信息显示") -``` - -### 编辑GUI属性 -```python -# 修改文本 -world.editGUIElement(gui_element, "text", "新文本") - -# 修改位置 -world.editGUIElement(gui_element, "position", [x, y, z]) - -# 修改缩放 -world.editGUIElement(gui_element, "scale", 1.5) -``` - -## 未来扩展 - -### 计划功能 -- [ ] 更多GUI组件类型 (滑块、复选框等) -- [ ] GUI动画系统 -- [ ] 主题和样式编辑器 -- [ ] GUI模板系统 -- [ ] 导出为独立GUI文件 - -### 贡献指南 -如需添加新功能或修复问题: -1. 参考现有的GUI创建方法 -2. 保持与 `test_qt_showbase.py` 验证逻辑的一致性 -3. 添加适当的错误处理和调试输出 -4. 更新文档和使用示例 - ---- - +# GUI编辑模式使用指南 + +## 概述 + +主程序现在集成了完整的GUI编辑模式,基于 `test_qt_showbase.py` 中验证有效的方法实现。这个功能允许你在3D场景中轻松创建、编辑和管理各种GUI元素。 + +## 功能特点 + +### ✨ 核心功能 +- **可视化GUI编辑**: 右侧浮动面板提供直观的工具选择 +- **多种GUI类型**: 支持2D和3D GUI元素 +- **实时预览**: 即时查看创建的GUI效果 +- **属性编辑**: 通过属性面板编辑GUI元素属性 +- **场景集成**: GUI元素与3D场景完美融合 + +### 🎯 支持的GUI类型 + +#### 2D GUI元素 +1. **按钮 (Button)**: 可点击的2D按钮 +2. **标签 (Label)**: 显示文本信息的标签 +3. **输入框 (Entry)**: 用户可输入文本的输入框 + +#### 3D GUI元素 +1. **3D文本**: 在3D空间中显示的文本,支持广告牌效果 +2. **虚拟屏幕**: 3D空间中的虚拟显示屏 + +## 使用方法 + +### 🚀 启动GUI编辑模式 + +#### 方法1: 通过菜单 +1. 启动主程序 `python3 test.py` +2. 点击菜单栏 `GUI` -> `进入GUI编辑模式` +3. 右侧将出现GUI编辑面板 + +#### 方法2: 运行测试程序 +```bash +cd /home/hello/eg +python3 test_gui_edit_mode.py +``` + +### 🎨 创建GUI元素 + +#### 步骤1: 选择工具 +在右侧GUI编辑面板中: +- 点击 `按钮` - 创建2D按钮 +- 点击 `标签` - 创建2D标签 +- 点击 `输入框` - 创建2D输入框 +- 点击 `3D文本` - 创建3D文本 +- 点击 `虚拟屏幕` - 创建3D虚拟屏幕 + +#### 步骤2: 放置元素 +选择工具后: +1. 在3D场景中点击鼠标左键 +2. GUI元素将在点击位置创建 +3. 新创建的元素会自动选中并显示在层级树中 + +### 📝 编辑GUI元素 + +#### 选择元素 +- **方法1**: 在GUI编辑模式下直接点击GUI元素 +- **方法2**: 在左侧层级树中点击GUI元素 + +#### 编辑属性 +选中GUI元素后,右侧属性面板会显示可编辑的属性: + +**通用属性**: +- 名称 +- 位置 (X, Y, Z) +- 缩放 + +**特定属性**: +- **文本类元素**: 可编辑显示文本 +- **2D元素**: 屏幕坐标和实际坐标 +- **3D元素**: 世界坐标和旋转 + +### 🔧 管理GUI元素 + +#### 删除元素 +1. 选中要删除的GUI元素 +2. 点击GUI编辑面板中的 `删除` 按钮 +3. 或在层级树中右键选择 `删除GUI元素` + +#### 复制元素 +1. 选中要复制的GUI元素 +2. 点击GUI编辑面板中的 `复制` 按钮 +3. 或在层级树中右键选择 `复制` + +#### 在层级树中查看 +- 所有GUI元素显示在 `场景` -> `GUI元素` 节点下 +- 显示格式: `类型: 文本内容` +- 支持右键菜单操作 + +### 🎮 交互控制 + +#### 鼠标操作 +- **左键点击**: 选择GUI元素或创建新元素 +- **右键拖拽**: 移动选中的GUI元素 (仅3D元素) +- **中键拖拽**: Z轴移动 (仅3D元素) + +#### 键盘快捷键 +- **Delete**: 删除选中的GUI元素 +- **ESC**: 退出GUI编辑模式 (测试程序) + +### 💾 保存和加载 + +#### 保存项目 +GUI元素会随项目一起保存: +1. `文件` -> `保存` +2. GUI元素信息保存在BAM场景文件中 + +#### 加载项目 +加载项目时GUI元素会自动恢复: +1. `文件` -> `打开` +2. 选择包含GUI的项目文件夹 + +## 坐标系统说明 + +### 2D GUI坐标 +- **屏幕坐标系**: 范围 -1 到 1 +- **逻辑坐标**: 用户友好的坐标,自动转换为屏幕坐标 +- **转换公式**: `屏幕坐标 = 逻辑坐标 × 0.1` + +### 3D GUI坐标 +- **世界坐标系**: 与3D场景使用相同的坐标系 +- **支持完整的3D变换**: 位置、旋转、缩放 + +## 技术实现细节 + +### 基于验证有效的方法 +- 使用 `test_qt_showbase.py` 中验证的GUI创建逻辑 +- 确保在Qt集成环境中的稳定性 +- 统一的错误处理和调试输出 + +### 关键组件 +```python +# GUI编辑模式状态 +self.guiEditMode = False +self.guiEditPanel = None +self.currentGUITool = None + +# GUI创建方法 (基于test_qt_showbase.py) +def createGUIButton(pos, text, size) +def createGUILabel(pos, text, size) +def createGUIEntry(pos, placeholder, size) +def createGUI3DText(pos, text, size) +def createGUIVirtualScreen(pos, size, text) +``` + +### 事件处理 +- 鼠标事件优先处理GUI编辑模式 +- 碰撞检测用于GUI元素选择 +- 树形控件与场景实时同步 + +## 常见问题解答 + +### Q: GUI元素创建后看不到? +**A**: 检查以下几点: +1. 确保已进入GUI编辑模式 +2. 检查创建位置是否在视野范围内 +3. 查看控制台输出确认创建成功 + +### Q: 如何调整GUI元素大小? +**A**: +1. 选中GUI元素 +2. 在属性面板中修改 `缩放` 属性 +3. 或通过代码设置: `element.setScale(new_scale)` + +### Q: 2D GUI元素位置不准确? +**A**: +- 2D GUI使用屏幕坐标系 +- 在属性面板中查看 `实际屏幕坐标` +- 使用 `逻辑坐标` 进行编辑更容易 + +### Q: 中文字体显示问题? +**A**: +- 程序会自动尝试加载系统中文字体 +- 如果中文显示异常,检查系统是否安装了中文字体包 +- 支持的字体路径: `/usr/share/fonts/truetype/wqy/` + +### Q: 如何扩展新的GUI类型? +**A**: +1. 在 `MyWorld` 类中添加新的创建方法 +2. 在 `createGUIEditPanel()` 中添加对应按钮 +3. 在 `createGUIAtPosition()` 中添加处理逻辑 + +## 示例代码 + +### 程序化创建GUI元素 +```python +# 创建按钮 +button = world.createGUIButton((0, 0, 5), "我的按钮", 0.1) + +# 创建3D文本 +text3d = world.createGUI3DText((0, 5, 2), "3D文本", 0.5) + +# 创建虚拟屏幕 +screen = world.createGUIVirtualScreen((3, 8, 0), (2, 1), "信息显示") +``` + +### 编辑GUI属性 +```python +# 修改文本 +world.editGUIElement(gui_element, "text", "新文本") + +# 修改位置 +world.editGUIElement(gui_element, "position", [x, y, z]) + +# 修改缩放 +world.editGUIElement(gui_element, "scale", 1.5) +``` + +## 未来扩展 + +### 计划功能 +- [ ] 更多GUI组件类型 (滑块、复选框等) +- [ ] GUI动画系统 +- [ ] 主题和样式编辑器 +- [ ] GUI模板系统 +- [ ] 导出为独立GUI文件 + +### 贡献指南 +如需添加新功能或修复问题: +1. 参考现有的GUI创建方法 +2. 保持与 `test_qt_showbase.py` 验证逻辑的一致性 +3. 添加适当的错误处理和调试输出 +4. 更新文档和使用示例 + +--- + *最后更新: 2024年* \ No newline at end of file diff --git a/demo/GUI预览窗口使用说明.md b/demo/GUI预览窗口使用说明.md index 38de672a..e5b48b21 100644 --- a/demo/GUI预览窗口使用说明.md +++ b/demo/GUI预览窗口使用说明.md @@ -1,169 +1,169 @@ -# GUI预览窗口使用说明 - -## 概述 - -GUI预览窗口是一个独立的Panda3D窗口,专门用于正确显示2D GUI元素,解决Qt集成中DirectGUI显示的问题。 - -## 主要特性 - -### ✅ 解决的问题 -- **2D GUI显示问题**: 在Qt集成的主窗口中,DirectGUI元素可能无法正确显示 -- **独立渲染环境**: 提供纯Panda3D环境,确保GUI元素正确渲染 -- **实时同步**: 自动与主程序中的GUI元素保持同步 - -### 🚀 核心功能 -- **独立窗口**: 完全独立的Panda3D窗口,不受Qt影响 -- **自动同步**: 每0.5秒自动检测主程序GUI元素变化 -- **支持所有GUI类型**: 按钮、标签、输入框、滑块、3D文本、虚拟屏幕 -- **交互预览**: 在预览窗口中可以真实地与GUI元素交互 - -## 使用方式 - -### 1. 在主程序中使用 - -```python -from gui_preview_window import GUIPreviewWindow - -# 创建预览窗口 -preview = GUIPreviewWindow() - -# 连接到主程序world对象 -preview.set_main_world(your_world_object) - -# 预览窗口会自动同步显示GUI元素 -``` - -### 2. 通过GUI编辑模式 - -1. 在主程序中点击菜单:"GUI" → "进入GUI编辑模式" -2. 预览窗口会自动弹出 -3. 使用主程序的工具栏创建GUI元素 -4. 在预览窗口中实时查看效果 - -### 3. 工具栏快速创建 - -主程序提供了便捷的工具栏按钮: -- **创建按钮**: 在场景中创建可点击按钮 -- **创建标签**: 创建文本标签 -- **3D文本**: 创建3D空间文本 - -## 支持的GUI元素类型 - -### 2D GUI元素 (DirectGUI) -| 类型 | 说明 | 在预览窗口中的表现 | -|------|------|-------------------| -| 按钮 | DirectButton | 完全可交互,支持点击 | -| 标签 | DirectLabel | 正确显示文本内容 | -| 输入框 | DirectEntry | 支持文本输入和提交 | -| 滑块 | DirectSlider | 支持值调整交互 | - -### 3D GUI元素 -| 类型 | 说明 | 在预览窗口中的表现 | -|------|------|-------------------| -| 3D文本 | TextNode | 3D空间中的文本,支持广告牌效果 | -| 虚拟屏幕 | CardMaker + TextNode | 带背景的虚拟显示屏 | - -## 技术实现 - -### 自动同步机制 -```python -def sync_with_main(self, task): - """每0.5秒检测GUI元素变化""" - if self.main_world and hasattr(self.main_world, 'gui_elements'): - main_count = len(self.main_world.gui_elements) - preview_count = len(self.gui_elements) - - if main_count != preview_count: - self.refresh_all_gui() # 自动刷新 - - return task.again -``` - -### 窗口配置 -```python -loadPrcFileData("", """ - win-size 800 600 - window-title GUI预览窗口 - 2D GUI专用显示 - framebuffer-multisample 1 - multisamples 2 - audio-library-name null - notify-level-audio error -""") -``` - -## 操作说明 - -### 鼠标控制 -- **滚轮**: 缩放视图 -- **右键拖拽**: 旋转视角 (未来版本) - -### 状态显示 -预览窗口底部会显示当前状态: -- "等待GUI元素..." - 尚未同步到任何GUI -- "已同步 N 个GUI元素" - 正在显示N个GUI元素 - -## 优势对比 - -### 与嵌入式方案对比 -| 特性 | 独立预览窗口 | Qt嵌入式 | -|------|-------------|-----------| -| 2D GUI显示 | ✅ 完美支持 | ❌ 显示问题 | -| 性能 | ✅ 原生性能 | ⚠️ 集成开销 | -| 交互性 | ✅ 完全交互 | ⚠️ 部分限制 | -| 开发复杂度 | ✅ 简单 | ❌ 复杂 | - -### 与独立编辑器对比 -| 特性 | 预览窗口 | 独立编辑器 | -|------|----------|------------| -| 操作流程 | ✅ 简化 | ❌ 复杂 | -| 学习成本 | ✅ 低 | ❌ 高 | -| 实时性 | ✅ 自动同步 | ⚠️ 手动刷新 | -| 代码维护 | ✅ 简单 | ❌ 复杂 | - -## 故障排除 - -### 常见问题 - -**Q: 预览窗口没有显示GUI元素** -A: 检查是否正确连接到主程序world对象,确保调用了`set_main_world()` - -**Q: GUI元素显示位置不正确** -A: 2D GUI使用屏幕坐标系,3D GUI使用世界坐标系,检查创建时的坐标参数 - -**Q: 预览窗口无法交互** -A: 确保预览窗口有焦点,尝试点击窗口使其获得焦点 - -### 调试信息 -预览窗口会在控制台输出详细的调试信息: -``` -✓ GUI预览窗口已创建 - 专用于显示2D GUI元素 -预览窗口已连接到主程序 -检测到GUI元素变化: 主程序1个, 预览0个 -预览窗口创建元素: button - 测试按钮 -``` - -## 未来扩展 - -### 计划功能 -- [ ] 预览窗口中直接编辑GUI属性 -- [ ] 鼠标右键拖拽旋转视角 -- [ ] GUI元素高亮和选择 -- [ ] 布局网格显示 -- [ ] 多视角预览(正交、透视) - -### API扩展 -```python -# 未来可能的API -preview.set_view_mode("orthographic") # 设置正交视图 -preview.enable_grid(True) # 显示网格 -preview.highlight_element(gui_element) # 高亮元素 -``` - -## 总结 - -GUI预览窗口提供了一个简洁高效的解决方案,让开发者能够: -1. **在主程序中直接编辑** - 不需要额外的编辑器窗口 -2. **在独立窗口中预览** - 确保GUI元素正确显示 -3. **实时同步更新** - 无需手动刷新,自动保持同步 - +# GUI预览窗口使用说明 + +## 概述 + +GUI预览窗口是一个独立的Panda3D窗口,专门用于正确显示2D GUI元素,解决Qt集成中DirectGUI显示的问题。 + +## 主要特性 + +### ✅ 解决的问题 +- **2D GUI显示问题**: 在Qt集成的主窗口中,DirectGUI元素可能无法正确显示 +- **独立渲染环境**: 提供纯Panda3D环境,确保GUI元素正确渲染 +- **实时同步**: 自动与主程序中的GUI元素保持同步 + +### 🚀 核心功能 +- **独立窗口**: 完全独立的Panda3D窗口,不受Qt影响 +- **自动同步**: 每0.5秒自动检测主程序GUI元素变化 +- **支持所有GUI类型**: 按钮、标签、输入框、滑块、3D文本、虚拟屏幕 +- **交互预览**: 在预览窗口中可以真实地与GUI元素交互 + +## 使用方式 + +### 1. 在主程序中使用 + +```python +from gui_preview_window import GUIPreviewWindow + +# 创建预览窗口 +preview = GUIPreviewWindow() + +# 连接到主程序world对象 +preview.set_main_world(your_world_object) + +# 预览窗口会自动同步显示GUI元素 +``` + +### 2. 通过GUI编辑模式 + +1. 在主程序中点击菜单:"GUI" → "进入GUI编辑模式" +2. 预览窗口会自动弹出 +3. 使用主程序的工具栏创建GUI元素 +4. 在预览窗口中实时查看效果 + +### 3. 工具栏快速创建 + +主程序提供了便捷的工具栏按钮: +- **创建按钮**: 在场景中创建可点击按钮 +- **创建标签**: 创建文本标签 +- **3D文本**: 创建3D空间文本 + +## 支持的GUI元素类型 + +### 2D GUI元素 (DirectGUI) +| 类型 | 说明 | 在预览窗口中的表现 | +|------|------|-------------------| +| 按钮 | DirectButton | 完全可交互,支持点击 | +| 标签 | DirectLabel | 正确显示文本内容 | +| 输入框 | DirectEntry | 支持文本输入和提交 | +| 滑块 | DirectSlider | 支持值调整交互 | + +### 3D GUI元素 +| 类型 | 说明 | 在预览窗口中的表现 | +|------|------|-------------------| +| 3D文本 | TextNode | 3D空间中的文本,支持广告牌效果 | +| 虚拟屏幕 | CardMaker + TextNode | 带背景的虚拟显示屏 | + +## 技术实现 + +### 自动同步机制 +```python +def sync_with_main(self, task): + """每0.5秒检测GUI元素变化""" + if self.main_world and hasattr(self.main_world, 'gui_elements'): + main_count = len(self.main_world.gui_elements) + preview_count = len(self.gui_elements) + + if main_count != preview_count: + self.refresh_all_gui() # 自动刷新 + + return task.again +``` + +### 窗口配置 +```python +loadPrcFileData("", """ + win-size 800 600 + window-title GUI预览窗口 - 2D GUI专用显示 + framebuffer-multisample 1 + multisamples 2 + audio-library-name null + notify-level-audio error +""") +``` + +## 操作说明 + +### 鼠标控制 +- **滚轮**: 缩放视图 +- **右键拖拽**: 旋转视角 (未来版本) + +### 状态显示 +预览窗口底部会显示当前状态: +- "等待GUI元素..." - 尚未同步到任何GUI +- "已同步 N 个GUI元素" - 正在显示N个GUI元素 + +## 优势对比 + +### 与嵌入式方案对比 +| 特性 | 独立预览窗口 | Qt嵌入式 | +|------|-------------|-----------| +| 2D GUI显示 | ✅ 完美支持 | ❌ 显示问题 | +| 性能 | ✅ 原生性能 | ⚠️ 集成开销 | +| 交互性 | ✅ 完全交互 | ⚠️ 部分限制 | +| 开发复杂度 | ✅ 简单 | ❌ 复杂 | + +### 与独立编辑器对比 +| 特性 | 预览窗口 | 独立编辑器 | +|------|----------|------------| +| 操作流程 | ✅ 简化 | ❌ 复杂 | +| 学习成本 | ✅ 低 | ❌ 高 | +| 实时性 | ✅ 自动同步 | ⚠️ 手动刷新 | +| 代码维护 | ✅ 简单 | ❌ 复杂 | + +## 故障排除 + +### 常见问题 + +**Q: 预览窗口没有显示GUI元素** +A: 检查是否正确连接到主程序world对象,确保调用了`set_main_world()` + +**Q: GUI元素显示位置不正确** +A: 2D GUI使用屏幕坐标系,3D GUI使用世界坐标系,检查创建时的坐标参数 + +**Q: 预览窗口无法交互** +A: 确保预览窗口有焦点,尝试点击窗口使其获得焦点 + +### 调试信息 +预览窗口会在控制台输出详细的调试信息: +``` +✓ GUI预览窗口已创建 - 专用于显示2D GUI元素 +预览窗口已连接到主程序 +检测到GUI元素变化: 主程序1个, 预览0个 +预览窗口创建元素: button - 测试按钮 +``` + +## 未来扩展 + +### 计划功能 +- [ ] 预览窗口中直接编辑GUI属性 +- [ ] 鼠标右键拖拽旋转视角 +- [ ] GUI元素高亮和选择 +- [ ] 布局网格显示 +- [ ] 多视角预览(正交、透视) + +### API扩展 +```python +# 未来可能的API +preview.set_view_mode("orthographic") # 设置正交视图 +preview.enable_grid(True) # 显示网格 +preview.highlight_element(gui_element) # 高亮元素 +``` + +## 总结 + +GUI预览窗口提供了一个简洁高效的解决方案,让开发者能够: +1. **在主程序中直接编辑** - 不需要额外的编辑器窗口 +2. **在独立窗口中预览** - 确保GUI元素正确显示 +3. **实时同步更新** - 无需手动刷新,自动保持同步 + 这种设计大大简化了GUI开发流程,同时解决了Qt集成中的显示问题。 \ No newline at end of file diff --git a/demo/PICKING_SYSTEM_README.md b/demo/PICKING_SYSTEM_README.md index f852e4f6..91de69b1 100644 --- a/demo/PICKING_SYSTEM_README.md +++ b/demo/PICKING_SYSTEM_README.md @@ -1,116 +1,116 @@ -# 屏幕投影拾取系统使用说明 - -## 🎯 **概述** - -为了解决您遇到的射线检测问题(射线偏向屏幕中心),我们实现了一个新的**屏幕投影拾取系统**,它直接基于2D屏幕坐标进行对象选择,比传统的3D射线检测更准确、更直观。 - -## 🚀 **新系统优势** - -### ✅ **相比射线检测的优点** -1. **准确性高**:完全对应用户的视觉感受,点击哪里就选中哪里 -2. **性能优秀**:不需要复杂的3D数学计算 -3. **稳定可靠**:不会出现射线偏向屏幕中心的问题 -4. **支持任意形状**:通过包围盒自动计算选择范围 - -### ❌ **传统射线检测的问题** -- 射线不是从相机发出,而是从近平面发出 -- 在Qt嵌入环境中容易出现坐标系转换问题 -- 微小的坐标偏移会导致射线方向计算错误 - -## 🔧 **技术实现** - -### **核心原理** -1. **3D到2D投影**:将对象的世界坐标投影到屏幕坐标 -2. **距离计算**:计算鼠标点击位置与对象屏幕位置的2D距离 -3. **智能选择**:基于距离和优先级选择最合适的对象 - -### **关键特性** -- **自动半径计算**:根据对象包围盒自动计算屏幕选择半径 -- **优先级系统**:支持不同类型对象的选择优先级 -- **容差调节**:可调节选择容差,使选择更容易或更精确 -- **自动更新**:对象移动后自动更新位置信息 - -## 🎮 **使用方法** - -### **在应用中的集成** -```python -# 1. 系统已自动初始化 -# 屏幕投影拾取器在 MyWorld.__init__() 中自动创建 - -# 2. 模型自动注册 -# 导入的模型会自动注册到拾取器 - -# 3. 手动注册对象(可选) -self.screen_picker.registerObject(node_path, "对象名称", priority=1) - -# 4. 更新对象位置(对象移动后) -self.screen_picker.updateObjectPositions() -``` - -### **键盘快捷键** -- **P键**:切换拾取方法(屏幕投影 ↔ 射线检测) -- **T键**:创建测试立方体(用于测试拾取功能) -- **R键**:切换射线调试显示 - -## 📋 **测试步骤** - -1. **启动应用**:运行 `python3 test.py` - -2. **创建测试对象**:按 **T键** 创建测试立方体 - -3. **测试选择**: - - 点击立方体附近区域 - - 观察选择效果 - -4. **比较方法**:按 **P键** 切换拾取方法,比较效果差异 - -5. **导入模型**:拖拽3D模型文件到应用中测试 - -## 🔍 **调试信息** - -系统会在控制台输出详细的调试信息: - -``` -=== 开始屏幕投影拾取 === -鼠标位置: (372, 265) -选中对象: TestCube_1 -``` - -## 🛠 **技术细节** - -### **坐标系转换** -```python -# Qt坐标系 → 世界坐标系 → 屏幕坐标系 -pixel_x = (screen_point.getX() + 1.0) * win_width * 0.5 -pixel_y = (1.0 - screen_point.getY()) * win_height * 0.5 -``` - -### **半径计算** -- **自动模式**:基于对象包围盒的最大尺寸 -- **自定义模式**:可为特定对象设置自定义选择半径 -- **距离补偿**:远距离对象使用基于透视投影的估算 - -### **选择算法** -```python -# 计算选择分数 -distance_score = 1.0 - (distance / effective_radius) -priority_score = priority * 10.0 -total_score = distance_score + priority_score -``` - -## 🎯 **使用建议** - -1. **优先使用屏幕投影拾取**:对于大多数交互场景更准确 -2. **射线检测作为备选**:某些特殊情况下仍然有用 -3. **调整容差**:根据需要调整 `tolerance_multiplier` 参数 -4. **设置优先级**:为不同类型的对象设置合适的选择优先级 - -## 🔄 **后向兼容** - -- 原有的射线检测系统仍然保留 -- 现在屏幕投影拾取为主要方法,射线检测为备选 -- 所有现有的选择、拖拽、GUI编辑功能都正常工作 - -## 📝 **总结** - +# 屏幕投影拾取系统使用说明 + +## 🎯 **概述** + +为了解决您遇到的射线检测问题(射线偏向屏幕中心),我们实现了一个新的**屏幕投影拾取系统**,它直接基于2D屏幕坐标进行对象选择,比传统的3D射线检测更准确、更直观。 + +## 🚀 **新系统优势** + +### ✅ **相比射线检测的优点** +1. **准确性高**:完全对应用户的视觉感受,点击哪里就选中哪里 +2. **性能优秀**:不需要复杂的3D数学计算 +3. **稳定可靠**:不会出现射线偏向屏幕中心的问题 +4. **支持任意形状**:通过包围盒自动计算选择范围 + +### ❌ **传统射线检测的问题** +- 射线不是从相机发出,而是从近平面发出 +- 在Qt嵌入环境中容易出现坐标系转换问题 +- 微小的坐标偏移会导致射线方向计算错误 + +## 🔧 **技术实现** + +### **核心原理** +1. **3D到2D投影**:将对象的世界坐标投影到屏幕坐标 +2. **距离计算**:计算鼠标点击位置与对象屏幕位置的2D距离 +3. **智能选择**:基于距离和优先级选择最合适的对象 + +### **关键特性** +- **自动半径计算**:根据对象包围盒自动计算屏幕选择半径 +- **优先级系统**:支持不同类型对象的选择优先级 +- **容差调节**:可调节选择容差,使选择更容易或更精确 +- **自动更新**:对象移动后自动更新位置信息 + +## 🎮 **使用方法** + +### **在应用中的集成** +```python +# 1. 系统已自动初始化 +# 屏幕投影拾取器在 MyWorld.__init__() 中自动创建 + +# 2. 模型自动注册 +# 导入的模型会自动注册到拾取器 + +# 3. 手动注册对象(可选) +self.screen_picker.registerObject(node_path, "对象名称", priority=1) + +# 4. 更新对象位置(对象移动后) +self.screen_picker.updateObjectPositions() +``` + +### **键盘快捷键** +- **P键**:切换拾取方法(屏幕投影 ↔ 射线检测) +- **T键**:创建测试立方体(用于测试拾取功能) +- **R键**:切换射线调试显示 + +## 📋 **测试步骤** + +1. **启动应用**:运行 `python3 test.py` + +2. **创建测试对象**:按 **T键** 创建测试立方体 + +3. **测试选择**: + - 点击立方体附近区域 + - 观察选择效果 + +4. **比较方法**:按 **P键** 切换拾取方法,比较效果差异 + +5. **导入模型**:拖拽3D模型文件到应用中测试 + +## 🔍 **调试信息** + +系统会在控制台输出详细的调试信息: + +``` +=== 开始屏幕投影拾取 === +鼠标位置: (372, 265) +选中对象: TestCube_1 +``` + +## 🛠 **技术细节** + +### **坐标系转换** +```python +# Qt坐标系 → 世界坐标系 → 屏幕坐标系 +pixel_x = (screen_point.getX() + 1.0) * win_width * 0.5 +pixel_y = (1.0 - screen_point.getY()) * win_height * 0.5 +``` + +### **半径计算** +- **自动模式**:基于对象包围盒的最大尺寸 +- **自定义模式**:可为特定对象设置自定义选择半径 +- **距离补偿**:远距离对象使用基于透视投影的估算 + +### **选择算法** +```python +# 计算选择分数 +distance_score = 1.0 - (distance / effective_radius) +priority_score = priority * 10.0 +total_score = distance_score + priority_score +``` + +## 🎯 **使用建议** + +1. **优先使用屏幕投影拾取**:对于大多数交互场景更准确 +2. **射线检测作为备选**:某些特殊情况下仍然有用 +3. **调整容差**:根据需要调整 `tolerance_multiplier` 参数 +4. **设置优先级**:为不同类型的对象设置合适的选择优先级 + +## 🔄 **后向兼容** + +- 原有的射线检测系统仍然保留 +- 现在屏幕投影拾取为主要方法,射线检测为备选 +- 所有现有的选择、拖拽、GUI编辑功能都正常工作 + +## 📝 **总结** + 新的屏幕投影拾取系统解决了您遇到的射线偏向屏幕中心的问题,提供了更准确、更直观的对象选择体验。系统已完全集成到您的应用中,并保持了向后兼容性。 \ No newline at end of file diff --git a/demo/SCRIPT_SYSTEM_IMPLEMENTATION.md b/demo/SCRIPT_SYSTEM_IMPLEMENTATION.md index 3f5f1f26..bf7d4bf9 100644 --- a/demo/SCRIPT_SYSTEM_IMPLEMENTATION.md +++ b/demo/SCRIPT_SYSTEM_IMPLEMENTATION.md @@ -1,313 +1,313 @@ -# 脚本系统完整实现方案 - -## 📋 项目概述 - -为基于Panda3D的3D引擎项目实现了完整的脚本系统,提供类似Unity MonoBehaviour的脚本管理功能。 - -## 🏗️ 系统架构 - -### 整体设计 - -``` -MyWorld (主类) -├── ScriptManager (脚本管理器) - ├── ScriptEngine (脚本引擎) - ├── ScriptLoader (脚本加载器) - ├── ScriptAPI (脚本API) - └── ScriptComponent[] (脚本组件列表) -``` - -### 核心组件 - -| 组件 | 文件 | 职责 | -|------|------|------| -| **ScriptManager** | `core/script_system.py` | 统一管理所有脚本功能 | -| **ScriptEngine** | `core/script_system.py` | 脚本执行引擎和更新循环 | -| **ScriptLoader** | `core/script_system.py` | 动态加载、卸载、热重载脚本 | -| **ScriptComponent** | `core/script_system.py` | 挂载到游戏对象的脚本实例 | -| **ScriptBase** | `core/script_system.py` | 所有用户脚本的基类 | -| **ScriptAPI** | `core/script_system.py` | 提供给脚本的引擎API | - -## 🔧 实现的核心功能 - -### 1. 脚本生命周期管理 - -```python -class ScriptBase(ABC): - def start(self): # 脚本开始时调用 - def update(self, dt): # 每帧更新调用 - def on_destroy(self): # 销毁时调用 - def on_enable(self): # 启用时调用 - def on_disable(self): # 禁用时调用 -``` - -### 2. 动态脚本加载 - -- ✅ 从文件动态加载Python脚本 -- ✅ 自动查找继承自ScriptBase的类 -- ✅ 模块依赖管理和错误处理 -- ✅ 支持脚本卸载和重新加载 - -### 3. 热重载系统 - -- ✅ 监控脚本文件变化 -- ✅ 自动重新加载修改的脚本 -- ✅ 保持运行时状态的连续性 -- ✅ 错误隔离,不影响其他脚本 - -### 4. 脚本组件系统 - -- ✅ 脚本实例与游戏对象绑定 -- ✅ 脚本启用/禁用控制 -- ✅ 多脚本挂载支持 -- ✅ 脚本间通信机制 - -### 5. 脚本模板系统 - -```python -# 基础脚本模板 -world.createScript("my_script", "basic") - -# 移动脚本模板 -world.createScript("move_script", "movement") -``` - -### 6. 调试和监控 - -- ✅ 脚本状态查看 -- ✅ 错误捕获和报告 -- ✅ 性能监控 -- ✅ 日志系统集成 - -## 📁 文件结构 - -### 新增文件 - -``` -core/ -├── script_system.py # 脚本系统核心实现 (新增) -└── __init__.py # 更新,添加脚本系统导入 - -demo/ -├── script_system_demo.py # 完整演示 (新增) -├── quick_script_test.py # 快速测试 (新增) -├── SCRIPT_SYSTEM_GUIDE.md # 使用指南 (新增) -└── SCRIPT_SYSTEM_IMPLEMENTATION.md # 实现文档 (新增) - -scripts/ # 脚本文件目录 (自动创建) -├── example_script.py # 示例脚本 (自动创建) -└── *.py # 用户脚本文件 -``` - -### 修改文件 - -``` -main.py # 集成脚本系统到主类 -``` - -## 🔌 系统集成 - -### 主类集成 - -在`main.py`的`MyWorld`类中添加了脚本系统集成: - -```python -class MyWorld(CoreWorld): - def __init__(self): - # ... 其他初始化 - self.script_manager = ScriptManager(self) - self.script_manager.start_system() - - # 添加脚本系统代理方法 - def startScriptSystem(self): ... - def createScript(self, name, template): ... - def addScript(self, obj, script_name): ... - # ... 更多方法 -``` - -### 自动初始化 - -- ✅ 脚本系统在MyWorld初始化时自动启动 -- ✅ 自动创建scripts目录和示例脚本 -- ✅ 自动启用热重载功能 -- ✅ 集成到主系统的更新循环 - -## 🎮 使用接口 - -### 基本API - -```python -# 脚本系统控制 -world.startScriptSystem() -world.stopScriptSystem() -world.enableHotReload(True/False) - -# 脚本创建和加载 -world.createScript("script_name", "template") -world.loadScript("path/to/script.py") -world.loadAllScripts() - -# 脚本挂载和管理 -world.addScript(game_object, "ScriptClass") -world.removeScript(game_object, "ScriptClass") -world.getScripts(game_object) - -# 脚本信息查询 -world.getAvailableScripts() -world.getScriptInfo("ScriptClass") -world.listAllScripts() -``` - -### 脚本编写API - -```python -from core.script_system import ScriptBase - -class MyScript(ScriptBase): - def start(self): - # 访问游戏对象 - obj_name = self.gameObject.getName() - - # 访问Transform - pos = self.transform.getPos() - - # 访问世界对象 - render = self.world.render - - # 日志输出 - self.log("脚本开始运行") -``` - -## ⚡ 关键特性 - -### 1. 高性能设计 - -- **高效更新循环**:使用Panda3D的Task系统 -- **错误隔离**:单个脚本错误不影响其他脚本 -- **按需执行**:支持脚本启用/禁用控制 -- **内存管理**:正确的脚本生命周期管理 - -### 2. 开发友好 - -- **热重载**:修改脚本立即生效,无需重启 -- **模板系统**:快速创建常用类型的脚本 -- **丰富调试**:详细的状态信息和错误报告 -- **API文档**:完整的使用指南和示例 - -### 3. 灵活扩展 - -- **插件化架构**:各组件独立,易于扩展 -- **自定义基类**:支持创建专门的脚本基类 -- **API扩展**:可以轻松添加新的脚本API -- **事件系统**:支持脚本间通信 - -### 4. 生产就绪 - -- **错误处理**:完善的异常捕获和处理 -- **状态管理**:正确的脚本状态跟踪 -- **资源清理**:自动的资源管理和清理 -- **性能监控**:内置的性能监控功能 - -## 🔍 技术亮点 - -### 1. 动态模块加载 - -```python -def load_script_from_file(self, script_path: str): - spec = importlib.util.spec_from_file_location(script_name, script_path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - # 查找ScriptBase子类... -``` - -### 2. 热重载实现 - -```python -def check_for_changes(self): - for script_path, old_mtime in self.file_mtimes.items(): - current_mtime = os.path.getmtime(script_path) - if current_mtime > old_mtime: - self.reload_script(script_path) -``` - -### 3. 组件化脚本管理 - -```python -class ScriptComponent: - def __init__(self, script_instance, game_object, script_manager): - script_instance.gameObject = game_object - script_instance.transform = game_object - script_instance.world = script_manager.world -``` - -### 4. 任务调度集成 - -```python -def start_engine(self): - self.update_task = taskMgr.add(self._update_scripts, "script_update") - -def _update_scripts(self, task): - dt = globalClock.getDt() - for component in self.script_components: - component.update(dt) - return task.cont -``` - -## 🧪 测试验证 - -### 测试文件 - -- **`quick_script_test.py`**:快速功能验证 -- **`script_system_demo.py`**:完整功能演示 - -### 测试覆盖 - -✅ 脚本系统初始化 -✅ 脚本文件创建和加载 -✅ 脚本挂载到游戏对象 -✅ 脚本生命周期执行 -✅ 热重载功能 -✅ 错误处理 -✅ 性能监控 - -## 📈 性能特征 - -- **启动时间**:< 100ms (包括示例脚本创建) -- **更新开销**:每个脚本 < 0.1ms per frame -- **内存占用**:基础系统 < 5MB -- **热重载延迟**:< 500ms (文件变化到重载完成) - -## 🔮 扩展方向 - -### 短期扩展 - -1. **可视化脚本编辑器**:集成到主界面 -2. **脚本调试器**:断点、变量查看 -3. **更多脚本模板**:AI、物理、动画等 -4. **脚本依赖管理**:自动处理脚本间依赖 - -### 长期规划 - -1. **可视化脚本**:节点式脚本编辑 -2. **脚本编译**:提高运行时性能 -3. **分布式脚本**:网络游戏支持 -4. **脚本市场**:脚本分享和下载 - -## 📚 相关文档 - -- **[SCRIPT_SYSTEM_GUIDE.md](SCRIPT_SYSTEM_GUIDE.md)**:详细使用指南 -- **[script_system_demo.py](script_system_demo.py)**:完整功能演示 -- **[quick_script_test.py](quick_script_test.py)**:快速测试脚本 - -## 🎯 总结 - -成功实现了一个功能完整、性能优秀的脚本系统,具备以下优势: - -✅ **完整性**:涵盖脚本创建、加载、挂载、管理的完整流程 -✅ **易用性**:简单直观的API,类似Unity的使用体验 -✅ **开发效率**:热重载支持,脚本模板,丰富调试信息 -✅ **性能优秀**:高效的更新循环,错误隔离,资源管理 -✅ **扩展性强**:模块化设计,易于自定义和扩展 -✅ **生产就绪**:完善的错误处理,状态管理,监控功能 - +# 脚本系统完整实现方案 + +## 📋 项目概述 + +为基于Panda3D的3D引擎项目实现了完整的脚本系统,提供类似Unity MonoBehaviour的脚本管理功能。 + +## 🏗️ 系统架构 + +### 整体设计 + +``` +MyWorld (主类) +├── ScriptManager (脚本管理器) + ├── ScriptEngine (脚本引擎) + ├── ScriptLoader (脚本加载器) + ├── ScriptAPI (脚本API) + └── ScriptComponent[] (脚本组件列表) +``` + +### 核心组件 + +| 组件 | 文件 | 职责 | +|------|------|------| +| **ScriptManager** | `core/script_system.py` | 统一管理所有脚本功能 | +| **ScriptEngine** | `core/script_system.py` | 脚本执行引擎和更新循环 | +| **ScriptLoader** | `core/script_system.py` | 动态加载、卸载、热重载脚本 | +| **ScriptComponent** | `core/script_system.py` | 挂载到游戏对象的脚本实例 | +| **ScriptBase** | `core/script_system.py` | 所有用户脚本的基类 | +| **ScriptAPI** | `core/script_system.py` | 提供给脚本的引擎API | + +## 🔧 实现的核心功能 + +### 1. 脚本生命周期管理 + +```python +class ScriptBase(ABC): + def start(self): # 脚本开始时调用 + def update(self, dt): # 每帧更新调用 + def on_destroy(self): # 销毁时调用 + def on_enable(self): # 启用时调用 + def on_disable(self): # 禁用时调用 +``` + +### 2. 动态脚本加载 + +- ✅ 从文件动态加载Python脚本 +- ✅ 自动查找继承自ScriptBase的类 +- ✅ 模块依赖管理和错误处理 +- ✅ 支持脚本卸载和重新加载 + +### 3. 热重载系统 + +- ✅ 监控脚本文件变化 +- ✅ 自动重新加载修改的脚本 +- ✅ 保持运行时状态的连续性 +- ✅ 错误隔离,不影响其他脚本 + +### 4. 脚本组件系统 + +- ✅ 脚本实例与游戏对象绑定 +- ✅ 脚本启用/禁用控制 +- ✅ 多脚本挂载支持 +- ✅ 脚本间通信机制 + +### 5. 脚本模板系统 + +```python +# 基础脚本模板 +world.createScript("my_script", "basic") + +# 移动脚本模板 +world.createScript("move_script", "movement") +``` + +### 6. 调试和监控 + +- ✅ 脚本状态查看 +- ✅ 错误捕获和报告 +- ✅ 性能监控 +- ✅ 日志系统集成 + +## 📁 文件结构 + +### 新增文件 + +``` +core/ +├── script_system.py # 脚本系统核心实现 (新增) +└── __init__.py # 更新,添加脚本系统导入 + +demo/ +├── script_system_demo.py # 完整演示 (新增) +├── quick_script_test.py # 快速测试 (新增) +├── SCRIPT_SYSTEM_GUIDE.md # 使用指南 (新增) +└── SCRIPT_SYSTEM_IMPLEMENTATION.md # 实现文档 (新增) + +scripts/ # 脚本文件目录 (自动创建) +├── example_script.py # 示例脚本 (自动创建) +└── *.py # 用户脚本文件 +``` + +### 修改文件 + +``` +main.py # 集成脚本系统到主类 +``` + +## 🔌 系统集成 + +### 主类集成 + +在`main.py`的`MyWorld`类中添加了脚本系统集成: + +```python +class MyWorld(CoreWorld): + def __init__(self): + # ... 其他初始化 + self.script_manager = ScriptManager(self) + self.script_manager.start_system() + + # 添加脚本系统代理方法 + def startScriptSystem(self): ... + def createScript(self, name, template): ... + def addScript(self, obj, script_name): ... + # ... 更多方法 +``` + +### 自动初始化 + +- ✅ 脚本系统在MyWorld初始化时自动启动 +- ✅ 自动创建scripts目录和示例脚本 +- ✅ 自动启用热重载功能 +- ✅ 集成到主系统的更新循环 + +## 🎮 使用接口 + +### 基本API + +```python +# 脚本系统控制 +world.startScriptSystem() +world.stopScriptSystem() +world.enableHotReload(True/False) + +# 脚本创建和加载 +world.createScript("script_name", "template") +world.loadScript("path/to/script.py") +world.loadAllScripts() + +# 脚本挂载和管理 +world.addScript(game_object, "ScriptClass") +world.removeScript(game_object, "ScriptClass") +world.getScripts(game_object) + +# 脚本信息查询 +world.getAvailableScripts() +world.getScriptInfo("ScriptClass") +world.listAllScripts() +``` + +### 脚本编写API + +```python +from core.script_system import ScriptBase + +class MyScript(ScriptBase): + def start(self): + # 访问游戏对象 + obj_name = self.gameObject.getName() + + # 访问Transform + pos = self.transform.getPos() + + # 访问世界对象 + render = self.world.render + + # 日志输出 + self.log("脚本开始运行") +``` + +## ⚡ 关键特性 + +### 1. 高性能设计 + +- **高效更新循环**:使用Panda3D的Task系统 +- **错误隔离**:单个脚本错误不影响其他脚本 +- **按需执行**:支持脚本启用/禁用控制 +- **内存管理**:正确的脚本生命周期管理 + +### 2. 开发友好 + +- **热重载**:修改脚本立即生效,无需重启 +- **模板系统**:快速创建常用类型的脚本 +- **丰富调试**:详细的状态信息和错误报告 +- **API文档**:完整的使用指南和示例 + +### 3. 灵活扩展 + +- **插件化架构**:各组件独立,易于扩展 +- **自定义基类**:支持创建专门的脚本基类 +- **API扩展**:可以轻松添加新的脚本API +- **事件系统**:支持脚本间通信 + +### 4. 生产就绪 + +- **错误处理**:完善的异常捕获和处理 +- **状态管理**:正确的脚本状态跟踪 +- **资源清理**:自动的资源管理和清理 +- **性能监控**:内置的性能监控功能 + +## 🔍 技术亮点 + +### 1. 动态模块加载 + +```python +def load_script_from_file(self, script_path: str): + spec = importlib.util.spec_from_file_location(script_name, script_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + # 查找ScriptBase子类... +``` + +### 2. 热重载实现 + +```python +def check_for_changes(self): + for script_path, old_mtime in self.file_mtimes.items(): + current_mtime = os.path.getmtime(script_path) + if current_mtime > old_mtime: + self.reload_script(script_path) +``` + +### 3. 组件化脚本管理 + +```python +class ScriptComponent: + def __init__(self, script_instance, game_object, script_manager): + script_instance.gameObject = game_object + script_instance.transform = game_object + script_instance.world = script_manager.world +``` + +### 4. 任务调度集成 + +```python +def start_engine(self): + self.update_task = taskMgr.add(self._update_scripts, "script_update") + +def _update_scripts(self, task): + dt = globalClock.getDt() + for component in self.script_components: + component.update(dt) + return task.cont +``` + +## 🧪 测试验证 + +### 测试文件 + +- **`quick_script_test.py`**:快速功能验证 +- **`script_system_demo.py`**:完整功能演示 + +### 测试覆盖 + +✅ 脚本系统初始化 +✅ 脚本文件创建和加载 +✅ 脚本挂载到游戏对象 +✅ 脚本生命周期执行 +✅ 热重载功能 +✅ 错误处理 +✅ 性能监控 + +## 📈 性能特征 + +- **启动时间**:< 100ms (包括示例脚本创建) +- **更新开销**:每个脚本 < 0.1ms per frame +- **内存占用**:基础系统 < 5MB +- **热重载延迟**:< 500ms (文件变化到重载完成) + +## 🔮 扩展方向 + +### 短期扩展 + +1. **可视化脚本编辑器**:集成到主界面 +2. **脚本调试器**:断点、变量查看 +3. **更多脚本模板**:AI、物理、动画等 +4. **脚本依赖管理**:自动处理脚本间依赖 + +### 长期规划 + +1. **可视化脚本**:节点式脚本编辑 +2. **脚本编译**:提高运行时性能 +3. **分布式脚本**:网络游戏支持 +4. **脚本市场**:脚本分享和下载 + +## 📚 相关文档 + +- **[SCRIPT_SYSTEM_GUIDE.md](SCRIPT_SYSTEM_GUIDE.md)**:详细使用指南 +- **[script_system_demo.py](script_system_demo.py)**:完整功能演示 +- **[quick_script_test.py](quick_script_test.py)**:快速测试脚本 + +## 🎯 总结 + +成功实现了一个功能完整、性能优秀的脚本系统,具备以下优势: + +✅ **完整性**:涵盖脚本创建、加载、挂载、管理的完整流程 +✅ **易用性**:简单直观的API,类似Unity的使用体验 +✅ **开发效率**:热重载支持,脚本模板,丰富调试信息 +✅ **性能优秀**:高效的更新循环,错误隔离,资源管理 +✅ **扩展性强**:模块化设计,易于自定义和扩展 +✅ **生产就绪**:完善的错误处理,状态管理,监控功能 + 该脚本系统为3D引擎项目提供了强大的游戏逻辑编写能力,显著提升了开发效率和代码组织能力。 \ No newline at end of file diff --git a/demo/VIDEO_INTEGRATION_GUIDE.md b/demo/VIDEO_INTEGRATION_GUIDE.md index 786ecb57..971ad902 100644 --- a/demo/VIDEO_INTEGRATION_GUIDE.md +++ b/demo/VIDEO_INTEGRATION_GUIDE.md @@ -1,249 +1,249 @@ -# Panda3D 视频播放功能集成指南 - -## 概述 - -Panda3D 完全支持内嵌视频播放功能,通过 `MovieTexture` 类可以实现: -- ✅ **视频文件播放** - 支持 MP4, AVI, MOV, MKV, WebM 等格式 -- ✅ **3D场景中的视频纹理** - 将视频作为材质应用到3D模型 -- ✅ **视频控制** - 播放、暂停、停止、调速、循环等 -- ✅ **程序化动画** - 创建动态纹理效果 -- ✅ **全景视频** - 360度球形视频播放 -- ✅ **广告牌视频** - 始终面向相机的视频面板 - -## 核心组件 - -### 1. MovieTexture 类 -```python -from panda3d.core import MovieTexture - -# 创建视频纹理 -movie_texture = MovieTexture("video_name") -success = movie_texture.read("path/to/video.mp4") - -if success: - # 播放控制 - movie_texture.play() # 播放 - movie_texture.stop() # 停止 - movie_texture.setTime(0) # 设置播放位置 - movie_texture.setPlayRate(1.5) # 设置播放速度 - movie_texture.setLoop(True) # 设置循环播放 - - # 获取信息 - length = movie_texture.getVideoLength() - current_time = movie_texture.getTime() - is_playing = movie_texture.isPlaying() -``` - -### 2. 视频管理器 (VideoManager) -```python -from video_integration import VideoManager - -# 在 MyWorld 类中初始化 -self.video_manager = VideoManager(self) - -# 创建视频屏幕 -screen = self.video_manager.create_video_screen( - pos=(0, 0, 2), - size=(4, 3), - video_path="video.mp4", - name="main_screen" -) - -# 创建广告牌 -billboard = self.video_manager.create_video_billboard( - pos=(2, 0, 1), - size=(2, 1.5), - name="info_billboard" -) - -# 创建全景视频 -spherical = self.video_manager.create_spherical_video( - pos=(0, 0, 0), - radius=10, - video_path="360_video.mp4", - name="vr_environment" -) -``` - -## 错误修复说明 - -### 1. 任务管理器参数传递错误 -**问题**: `TypeError: missing 1 required positional argument: 'task'` - -**原因**: 使用 `extraArgs` 时参数顺序错误 - -**修复方案**: -```python -# ❌ 错误的方式 -self.taskMgr.add(self.update_texture, "task_name", extraArgs=[texture, data]) - -def update_texture(self, texture, data, task): # task 参数位置错误 - pass - -# ✅ 正确的方式 -self.taskMgr.add(lambda task: self.update_texture(task, texture, data), "task_name") - -def update_texture(self, task, texture, data): # task 参数在前 - return task.cont -``` - -### 2. 模型依赖问题 -**问题**: `Couldn't load file models/sphere.egg` - -**解决方案**: 程序化生成几何体 -```python -def create_sphere_geometry(self, radius=1): - """程序化创建球体几何""" - sphere_root = self.render.attachNewNode("sphere") - - faces = [ - (Vec3(0, radius, 0), Vec3(0, 0, 0)), # 前面 - (Vec3(0, -radius, 0), Vec3(0, 180, 0)), # 后面 - (Vec3(radius, 0, 0), Vec3(0, 90, 0)), # 右面 - (Vec3(-radius, 0, 0), Vec3(0, -90, 0)), # 左面 - (Vec3(0, 0, radius), Vec3(-90, 0, 0)), # 上面 - (Vec3(0, 0, -radius), Vec3(90, 0, 0)), # 下面 - ] - - cm = CardMaker("sphere_face") - cm.setFrame(-radius, radius, -radius, radius) - - for pos, hpr in faces: - face = sphere_root.attachNewNode(cm.generate()) - face.setPos(pos) - face.setHpr(hpr) - - return sphere_root -``` - -## 快速开始 - -### 1. 基础视频播放 -```python -from direct.showbase.ShowBase import ShowBase -from panda3d.core import MovieTexture, CardMaker - -class VideoApp(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 创建视频纹理 - movie_texture = MovieTexture("video") - success = movie_texture.read("your_video.mp4") - - if success: - # 创建显示面板 - cm = CardMaker("video_card") - cm.setFrame(-2, 2, -1.5, 1.5) - video_card = self.render.attachNewNode(cm.generate()) - video_card.setTexture(movie_texture) - - # 播放视频 - movie_texture.play() - - # 设置相机 - self.cam.setPos(0, -5, 0) - -app = VideoApp() -app.run() -``` - -### 2. 集成到现有编辑器 -```python -# 在 MyWorld 类的 __init__ 方法中添加 -from video_integration import VideoManager -self.video_manager = VideoManager(self) - -# 在文件拖放处理中添加视频支持 -def dropEvent(self, event): - if event.mimeData().hasUrls(): - for url in event.mimeData().urls(): - filepath = url.toLocalFile() - - # 检查视频文件 - if self.world.video_manager.is_video_file(filepath): - self.world.video_manager.create_video_screen( - pos=(0, 0, 2), - video_path=filepath, - name=f"video_{len(self.world.video_manager.video_objects)}" - ) - # 检查模型文件 - elif filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx')): - self.world.importModel(filepath) -``` - -### 3. 视频控制示例 -```python -# 播放控制 -video_manager.play_video("video_name") -video_manager.pause_video("video_name") -video_manager.stop_video("video_name") - -# 速度控制 -video_manager.set_video_speed("video_name", 1.5) # 1.5倍速 - -# 获取信息 -info = video_manager.get_video_info("video_name") -print(f"视频长度: {info['length']}秒") -print(f"当前时间: {info['current_time']}秒") -print(f"播放状态: {info['is_playing']}") - -# 列出所有视频 -videos = video_manager.list_videos() -for video in videos: - print(f"视频: {video['name']}, 类型: {video['type']}, 播放中: {video['is_playing']}") -``` - -## 支持的视频格式 - -- **MP4** - 推荐格式,兼容性最好 -- **AVI** - 传统格式,支持良好 -- **MOV** - QuickTime 格式 -- **MKV** - 开源容器格式 -- **WebM** - 网络优化格式 - -## 性能优化建议 - -1. **视频分辨率**: 建议使用 1080p 或更低分辨率 -2. **编码格式**: 优先使用 H.264 编码 -3. **文件大小**: 控制在合理范围内避免内存问题 -4. **多视频播放**: 同时播放多个视频时注意性能监控 -5. **纹理尺寸**: 程序化纹理建议使用 2 的幂次方尺寸 - -## 常见问题解决 - -### Q: 视频不播放怎么办? -A: 检查文件路径是否正确,文件格式是否支持,系统是否安装了相应的解码器。 - -### Q: 动画纹理不更新? -A: 确认任务管理器正常运行,检查 `task.cont` 返回值。 - -### Q: 全景视频显示异常? -A: 检查球体几何体的法线方向,确保使用 `setTwoSided(True)`。 - -### Q: 内存占用过高? -A: 减小纹理分辨率,限制同时播放的视频数量,及时移除不需要的视频对象。 - -## 测试文件 - -项目包含以下测试文件: -- `video_player_example.py` - 完整的视频播放器演示 -- `video_integration.py` - 视频管理器模块 -- `test_video_fix.py` - 功能测试验证 - -运行测试: -```bash -python test_video_fix.py -python video_player_example.py -``` - -## 总结 - -Panda3D 的视频播放功能非常强大,支持多种使用场景。通过本指南提供的 VideoManager 类,可以轻松集成到现有项目中,实现丰富的多媒体体验。 - -主要优势: -- 🔥 **易于集成** - 几行代码即可添加视频功能 -- 🎯 **功能丰富** - 支持多种视频显示模式 -- 🚀 **性能优秀** - 基于 Panda3D 的高效渲染 -- 🛠️ **高度可定制** - 支持程序化动画和特效 +# Panda3D 视频播放功能集成指南 + +## 概述 + +Panda3D 完全支持内嵌视频播放功能,通过 `MovieTexture` 类可以实现: +- ✅ **视频文件播放** - 支持 MP4, AVI, MOV, MKV, WebM 等格式 +- ✅ **3D场景中的视频纹理** - 将视频作为材质应用到3D模型 +- ✅ **视频控制** - 播放、暂停、停止、调速、循环等 +- ✅ **程序化动画** - 创建动态纹理效果 +- ✅ **全景视频** - 360度球形视频播放 +- ✅ **广告牌视频** - 始终面向相机的视频面板 + +## 核心组件 + +### 1. MovieTexture 类 +```python +from panda3d.core import MovieTexture + +# 创建视频纹理 +movie_texture = MovieTexture("video_name") +success = movie_texture.read("path/to/video.mp4") + +if success: + # 播放控制 + movie_texture.play() # 播放 + movie_texture.stop() # 停止 + movie_texture.setTime(0) # 设置播放位置 + movie_texture.setPlayRate(1.5) # 设置播放速度 + movie_texture.setLoop(True) # 设置循环播放 + + # 获取信息 + length = movie_texture.getVideoLength() + current_time = movie_texture.getTime() + is_playing = movie_texture.isPlaying() +``` + +### 2. 视频管理器 (VideoManager) +```python +from video_integration import VideoManager + +# 在 MyWorld 类中初始化 +self.video_manager = VideoManager(self) + +# 创建视频屏幕 +screen = self.video_manager.create_video_screen( + pos=(0, 0, 2), + size=(4, 3), + video_path="video.mp4", + name="main_screen" +) + +# 创建广告牌 +billboard = self.video_manager.create_video_billboard( + pos=(2, 0, 1), + size=(2, 1.5), + name="info_billboard" +) + +# 创建全景视频 +spherical = self.video_manager.create_spherical_video( + pos=(0, 0, 0), + radius=10, + video_path="360_video.mp4", + name="vr_environment" +) +``` + +## 错误修复说明 + +### 1. 任务管理器参数传递错误 +**问题**: `TypeError: missing 1 required positional argument: 'task'` + +**原因**: 使用 `extraArgs` 时参数顺序错误 + +**修复方案**: +```python +# ❌ 错误的方式 +self.taskMgr.add(self.update_texture, "task_name", extraArgs=[texture, data]) + +def update_texture(self, texture, data, task): # task 参数位置错误 + pass + +# ✅ 正确的方式 +self.taskMgr.add(lambda task: self.update_texture(task, texture, data), "task_name") + +def update_texture(self, task, texture, data): # task 参数在前 + return task.cont +``` + +### 2. 模型依赖问题 +**问题**: `Couldn't load file models/sphere.egg` + +**解决方案**: 程序化生成几何体 +```python +def create_sphere_geometry(self, radius=1): + """程序化创建球体几何""" + sphere_root = self.render.attachNewNode("sphere") + + faces = [ + (Vec3(0, radius, 0), Vec3(0, 0, 0)), # 前面 + (Vec3(0, -radius, 0), Vec3(0, 180, 0)), # 后面 + (Vec3(radius, 0, 0), Vec3(0, 90, 0)), # 右面 + (Vec3(-radius, 0, 0), Vec3(0, -90, 0)), # 左面 + (Vec3(0, 0, radius), Vec3(-90, 0, 0)), # 上面 + (Vec3(0, 0, -radius), Vec3(90, 0, 0)), # 下面 + ] + + cm = CardMaker("sphere_face") + cm.setFrame(-radius, radius, -radius, radius) + + for pos, hpr in faces: + face = sphere_root.attachNewNode(cm.generate()) + face.setPos(pos) + face.setHpr(hpr) + + return sphere_root +``` + +## 快速开始 + +### 1. 基础视频播放 +```python +from direct.showbase.ShowBase import ShowBase +from panda3d.core import MovieTexture, CardMaker + +class VideoApp(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 创建视频纹理 + movie_texture = MovieTexture("video") + success = movie_texture.read("your_video.mp4") + + if success: + # 创建显示面板 + cm = CardMaker("video_card") + cm.setFrame(-2, 2, -1.5, 1.5) + video_card = self.render.attachNewNode(cm.generate()) + video_card.setTexture(movie_texture) + + # 播放视频 + movie_texture.play() + + # 设置相机 + self.cam.setPos(0, -5, 0) + +app = VideoApp() +app.run() +``` + +### 2. 集成到现有编辑器 +```python +# 在 MyWorld 类的 __init__ 方法中添加 +from video_integration import VideoManager +self.video_manager = VideoManager(self) + +# 在文件拖放处理中添加视频支持 +def dropEvent(self, event): + if event.mimeData().hasUrls(): + for url in event.mimeData().urls(): + filepath = url.toLocalFile() + + # 检查视频文件 + if self.world.video_manager.is_video_file(filepath): + self.world.video_manager.create_video_screen( + pos=(0, 0, 2), + video_path=filepath, + name=f"video_{len(self.world.video_manager.video_objects)}" + ) + # 检查模型文件 + elif filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx')): + self.world.importModel(filepath) +``` + +### 3. 视频控制示例 +```python +# 播放控制 +video_manager.play_video("video_name") +video_manager.pause_video("video_name") +video_manager.stop_video("video_name") + +# 速度控制 +video_manager.set_video_speed("video_name", 1.5) # 1.5倍速 + +# 获取信息 +info = video_manager.get_video_info("video_name") +print(f"视频长度: {info['length']}秒") +print(f"当前时间: {info['current_time']}秒") +print(f"播放状态: {info['is_playing']}") + +# 列出所有视频 +videos = video_manager.list_videos() +for video in videos: + print(f"视频: {video['name']}, 类型: {video['type']}, 播放中: {video['is_playing']}") +``` + +## 支持的视频格式 + +- **MP4** - 推荐格式,兼容性最好 +- **AVI** - 传统格式,支持良好 +- **MOV** - QuickTime 格式 +- **MKV** - 开源容器格式 +- **WebM** - 网络优化格式 + +## 性能优化建议 + +1. **视频分辨率**: 建议使用 1080p 或更低分辨率 +2. **编码格式**: 优先使用 H.264 编码 +3. **文件大小**: 控制在合理范围内避免内存问题 +4. **多视频播放**: 同时播放多个视频时注意性能监控 +5. **纹理尺寸**: 程序化纹理建议使用 2 的幂次方尺寸 + +## 常见问题解决 + +### Q: 视频不播放怎么办? +A: 检查文件路径是否正确,文件格式是否支持,系统是否安装了相应的解码器。 + +### Q: 动画纹理不更新? +A: 确认任务管理器正常运行,检查 `task.cont` 返回值。 + +### Q: 全景视频显示异常? +A: 检查球体几何体的法线方向,确保使用 `setTwoSided(True)`。 + +### Q: 内存占用过高? +A: 减小纹理分辨率,限制同时播放的视频数量,及时移除不需要的视频对象。 + +## 测试文件 + +项目包含以下测试文件: +- `video_player_example.py` - 完整的视频播放器演示 +- `video_integration.py` - 视频管理器模块 +- `test_video_fix.py` - 功能测试验证 + +运行测试: +```bash +python test_video_fix.py +python video_player_example.py +``` + +## 总结 + +Panda3D 的视频播放功能非常强大,支持多种使用场景。通过本指南提供的 VideoManager 类,可以轻松集成到现有项目中,实现丰富的多媒体体验。 + +主要优势: +- 🔥 **易于集成** - 几行代码即可添加视频功能 +- 🎯 **功能丰富** - 支持多种视频显示模式 +- 🚀 **性能优秀** - 基于 Panda3D 的高效渲染 +- 🛠️ **高度可定制** - 支持程序化动画和特效 - 💡 **实时控制** - 完整的播放控制接口 \ No newline at end of file diff --git a/demo/basic_gui_demo.py b/demo/basic_gui_demo.py index 29654bc8..e5d68f5b 100644 --- a/demo/basic_gui_demo.py +++ b/demo/basic_gui_demo.py @@ -1,112 +1,112 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -PANDA3D 基础GUI演示 -最简单的DirectGUI使用示例 -""" - -from direct.showbase.ShowBase import ShowBase -from direct.gui.DirectGui import DirectButton, DirectLabel -from direct.gui.OnscreenText import OnscreenText -from panda3d.core import TextNode - -class BasicGUIDemo(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 设置背景颜色 - self.setBackgroundColor(0.2, 0.4, 0.6) - - # 尝试加载中文字体 - self.chinese_font = self.loadChineseFont() - - # 创建标题 - self.title = OnscreenText( - text="PANDA3D DirectGUI 基础演示", - pos=(0, 0.8), - scale=0.1, - fg=(1, 1, 1, 1), - align=TextNode.ACenter, - font=self.chinese_font if self.chinese_font else None - ) - - # 创建一个简单按钮 - self.button = DirectButton( - text="点击我!", - pos=(0, 0, 0.2), - scale=0.1, - command=self.buttonClicked, - frameColor=(0.2, 0.8, 0.2, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - # 创建标签显示状态 - self.statusLabel = DirectLabel( - text="状态: 等待点击", - pos=(0, 0, -0.2), - scale=0.08, - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 0, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - # 创建退出按钮 - self.exitButton = DirectButton( - text="退出", - pos=(0, 0, -0.5), - scale=0.08, - command=self.exitDemo, - frameColor=(0.8, 0.2, 0.2, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - # 点击计数器 - self.clickCount = 0 - - print("PANDA3D 基础GUI演示启动成功!") - print("请点击窗口中的按钮来测试GUI功能") - - def buttonClicked(self): - """按钮点击处理函数""" - self.clickCount += 1 - self.statusLabel['text'] = f"状态: 按钮被点击了 {self.clickCount} 次" - print(f"按钮被点击! 总共点击了 {self.clickCount} 次") - - # 改变按钮颜色作为反馈 - if self.clickCount % 3 == 0: - self.button['frameColor'] = (0.8, 0.2, 0.8, 1) # 紫色 - elif self.clickCount % 3 == 1: - self.button['frameColor'] = (0.2, 0.8, 0.2, 1) # 绿色 - else: - self.button['frameColor'] = (0.2, 0.2, 0.8, 1) # 蓝色 - - def exitDemo(self): - """退出演示""" - print("用户选择退出") - self.userExit() - - def loadChineseFont(self): - """尝试加载中文字体""" - import os - font_paths = [ - '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', - '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', - '/usr/share/fonts/truetype/arphic/ukai.ttc', - '/usr/share/fonts/truetype/arphic/uming.ttc', - ] - - for font_path in font_paths: - if os.path.exists(font_path): - try: - font = self.loader.loadFont(font_path) - if font: - return font - except: - continue - return None - -if __name__ == "__main__": - print("正在启动PANDA3D基础GUI演示...") - demo = BasicGUIDemo() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +PANDA3D 基础GUI演示 +最简单的DirectGUI使用示例 +""" + +from direct.showbase.ShowBase import ShowBase +from direct.gui.DirectGui import DirectButton, DirectLabel +from direct.gui.OnscreenText import OnscreenText +from panda3d.core import TextNode + +class BasicGUIDemo(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 设置背景颜色 + self.setBackgroundColor(0.2, 0.4, 0.6) + + # 尝试加载中文字体 + self.chinese_font = self.loadChineseFont() + + # 创建标题 + self.title = OnscreenText( + text="PANDA3D DirectGUI 基础演示", + pos=(0, 0.8), + scale=0.1, + fg=(1, 1, 1, 1), + align=TextNode.ACenter, + font=self.chinese_font if self.chinese_font else None + ) + + # 创建一个简单按钮 + self.button = DirectButton( + text="点击我!", + pos=(0, 0, 0.2), + scale=0.1, + command=self.buttonClicked, + frameColor=(0.2, 0.8, 0.2, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + # 创建标签显示状态 + self.statusLabel = DirectLabel( + text="状态: 等待点击", + pos=(0, 0, -0.2), + scale=0.08, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 0, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + # 创建退出按钮 + self.exitButton = DirectButton( + text="退出", + pos=(0, 0, -0.5), + scale=0.08, + command=self.exitDemo, + frameColor=(0.8, 0.2, 0.2, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + # 点击计数器 + self.clickCount = 0 + + print("PANDA3D 基础GUI演示启动成功!") + print("请点击窗口中的按钮来测试GUI功能") + + def buttonClicked(self): + """按钮点击处理函数""" + self.clickCount += 1 + self.statusLabel['text'] = f"状态: 按钮被点击了 {self.clickCount} 次" + print(f"按钮被点击! 总共点击了 {self.clickCount} 次") + + # 改变按钮颜色作为反馈 + if self.clickCount % 3 == 0: + self.button['frameColor'] = (0.8, 0.2, 0.8, 1) # 紫色 + elif self.clickCount % 3 == 1: + self.button['frameColor'] = (0.2, 0.8, 0.2, 1) # 绿色 + else: + self.button['frameColor'] = (0.2, 0.2, 0.8, 1) # 蓝色 + + def exitDemo(self): + """退出演示""" + print("用户选择退出") + self.userExit() + + def loadChineseFont(self): + """尝试加载中文字体""" + import os + font_paths = [ + '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', + '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', + '/usr/share/fonts/truetype/arphic/ukai.ttc', + '/usr/share/fonts/truetype/arphic/uming.ttc', + ] + + for font_path in font_paths: + if os.path.exists(font_path): + try: + font = self.loader.loadFont(font_path) + if font: + return font + except: + continue + return None + +if __name__ == "__main__": + print("正在启动PANDA3D基础GUI演示...") + demo = BasicGUIDemo() demo.run() \ No newline at end of file diff --git a/demo/basic_ray_test.py b/demo/basic_ray_test.py index 6f79b5dd..ad56e71f 100644 --- a/demo/basic_ray_test.py +++ b/demo/basic_ray_test.py @@ -1,175 +1,175 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import sys -from direct.showbase.ShowBase import ShowBase -from panda3d.core import ( - Point3, Vec3, CollisionTraverser, CollisionHandlerQueue, - CollisionNode, CollisionRay, CollisionSphere, BitMask32 -) - -class BasicRayTest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - print("=== 基础射线测试 ===") - - # 简单的相机设置 - self.cam.setPos(0, -20, 5) - self.cam.lookAt(0, 0, 0) - - print(f"相机位置: {self.cam.getPos()}") - - # 创建简单的碰撞球体在原点 - self.createSimpleSphere() - - # 创建简单的射线检测 - self.createSimpleRay() - - # 测试简单的射线 - self.testSimpleRay() - - def createSimpleSphere(self): - """在原点创建简单的碰撞球体""" - print(f"\n创建测试球体...") - - # 创建碰撞节点 - collisionNode = CollisionNode('testSphere') - - # 创建球体,中心在原点,半径5 - sphere = CollisionSphere(Point3(0, 0, 0), 5.0) - collisionNode.addSolid(sphere) - - # 设置掩码 - collisionNode.setIntoCollideMask(BitMask32.bit(0)) - collisionNode.setFromCollideMask(BitMask32.allOff()) - - # 附加到render - self.sphereNP = self.render.attachNewNode(collisionNode) - - # 显示碰撞体 - self.sphereNP.show() - - print(f"球体创建在原点: (0, 0, 0)") - print(f"球体半径: 5.0") - print(f"球体 IntoMask: {collisionNode.getIntoCollideMask()}") - - def createSimpleRay(self): - """创建简单的射线检测""" - print(f"\n创建射线检测...") - - # 创建遍历器和队列 - self.traverser = CollisionTraverser() - self.queue = CollisionHandlerQueue() - - # 创建射线节点 - rayNode = CollisionNode('testRay') - self.ray = CollisionRay() - rayNode.addSolid(self.ray) - - # 设置射线掩码 - rayNode.setFromCollideMask(BitMask32.bit(0)) - rayNode.setIntoCollideMask(BitMask32.allOff()) - - # 创建射线节点路径 - self.rayNP = self.render.attachNewNode(rayNode) - - # 注册到遍历器 - self.traverser.addCollider(self.rayNP, self.queue) - - print(f"射线 FromMask: {rayNode.getFromCollideMask()}") - print(f"遍历器中的碰撞器数量: {self.traverser.getNumColliders()}") - - def testSimpleRay(self): - """测试简单的射线""" - print(f"\n=== 射线测试 ===") - - # 测试1: 从相机直接射向原点 - camera_pos = self.cam.getPos() - target_pos = Point3(0, 0, 0) - - direction = target_pos - camera_pos - direction.normalize() - - print(f"测试1: 从相机射向原点") - print(f" 起点: {camera_pos}") - print(f" 方向: {direction}") - - # 设置射线 - self.ray.setOrigin(camera_pos) - self.ray.setDirection(direction) - - # 执行检测 - self.queue.clearEntries() - self.traverser.traverse(self.render) - - entries = self.queue.getNumEntries() - print(f" 检测到 {entries} 个碰撞") - - if entries > 0: - self.queue.sortEntries() - for i in range(entries): - entry = self.queue.getEntry(i) - hitPos = entry.getSurfacePoint(self.render) - hitNode = entry.getIntoNodePath() - distance = (hitPos - camera_pos).length() - print(f" 碰撞 {i}: {hitNode.getName()}, 距离: {distance:.2f}") - - # 测试2: 从固定位置射向球体 - print(f"\n测试2: 从固定位置射向球体") - fixed_origin = Point3(-10, 0, 0) - fixed_direction = Vec3(1, 0, 0) # 直接向右 - - print(f" 起点: {fixed_origin}") - print(f" 方向: {fixed_direction}") - - self.ray.setOrigin(fixed_origin) - self.ray.setDirection(fixed_direction) - - self.queue.clearEntries() - self.traverser.traverse(self.render) - - entries = self.queue.getNumEntries() - print(f" 检测到 {entries} 个碰撞") - - if entries > 0: - self.queue.sortEntries() - for i in range(entries): - entry = self.queue.getEntry(i) - hitPos = entry.getSurfacePoint(self.render) - hitNode = entry.getIntoNodePath() - distance = (hitPos - fixed_origin).length() - print(f" 碰撞 {i}: {hitNode.getName()}, 距离: {distance:.2f}") - - # 测试3: 检查射线是否正确设置 - print(f"\n测试3: 检查射线设置") - print(f" 射线起点: {self.ray.getOrigin()}") - print(f" 射线方向: {self.ray.getDirection()}") - - # 手动计算射线是否应该击中球体 - origin = self.ray.getOrigin() - direction = self.ray.getDirection() - sphere_center = Point3(0, 0, 0) - sphere_radius = 5.0 - - # 计算从射线起点到球心的向量 - to_sphere = sphere_center - origin - - # 计算射线上最接近球心的点 - t = to_sphere.dot(direction) - closest_point = origin + direction * t - - # 计算最近距离 - distance_to_center = (closest_point - sphere_center).length() - - print(f" 到球心的最近距离: {distance_to_center:.2f}") - print(f" 球体半径: {sphere_radius:.2f}") - - if distance_to_center <= sphere_radius: - print(f" 射线应该击中球体!") - else: - print(f" 射线会错过球体") - -if __name__ == "__main__": - app = BasicRayTest() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +from direct.showbase.ShowBase import ShowBase +from panda3d.core import ( + Point3, Vec3, CollisionTraverser, CollisionHandlerQueue, + CollisionNode, CollisionRay, CollisionSphere, BitMask32 +) + +class BasicRayTest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + print("=== 基础射线测试 ===") + + # 简单的相机设置 + self.cam.setPos(0, -20, 5) + self.cam.lookAt(0, 0, 0) + + print(f"相机位置: {self.cam.getPos()}") + + # 创建简单的碰撞球体在原点 + self.createSimpleSphere() + + # 创建简单的射线检测 + self.createSimpleRay() + + # 测试简单的射线 + self.testSimpleRay() + + def createSimpleSphere(self): + """在原点创建简单的碰撞球体""" + print(f"\n创建测试球体...") + + # 创建碰撞节点 + collisionNode = CollisionNode('testSphere') + + # 创建球体,中心在原点,半径5 + sphere = CollisionSphere(Point3(0, 0, 0), 5.0) + collisionNode.addSolid(sphere) + + # 设置掩码 + collisionNode.setIntoCollideMask(BitMask32.bit(0)) + collisionNode.setFromCollideMask(BitMask32.allOff()) + + # 附加到render + self.sphereNP = self.render.attachNewNode(collisionNode) + + # 显示碰撞体 + self.sphereNP.show() + + print(f"球体创建在原点: (0, 0, 0)") + print(f"球体半径: 5.0") + print(f"球体 IntoMask: {collisionNode.getIntoCollideMask()}") + + def createSimpleRay(self): + """创建简单的射线检测""" + print(f"\n创建射线检测...") + + # 创建遍历器和队列 + self.traverser = CollisionTraverser() + self.queue = CollisionHandlerQueue() + + # 创建射线节点 + rayNode = CollisionNode('testRay') + self.ray = CollisionRay() + rayNode.addSolid(self.ray) + + # 设置射线掩码 + rayNode.setFromCollideMask(BitMask32.bit(0)) + rayNode.setIntoCollideMask(BitMask32.allOff()) + + # 创建射线节点路径 + self.rayNP = self.render.attachNewNode(rayNode) + + # 注册到遍历器 + self.traverser.addCollider(self.rayNP, self.queue) + + print(f"射线 FromMask: {rayNode.getFromCollideMask()}") + print(f"遍历器中的碰撞器数量: {self.traverser.getNumColliders()}") + + def testSimpleRay(self): + """测试简单的射线""" + print(f"\n=== 射线测试 ===") + + # 测试1: 从相机直接射向原点 + camera_pos = self.cam.getPos() + target_pos = Point3(0, 0, 0) + + direction = target_pos - camera_pos + direction.normalize() + + print(f"测试1: 从相机射向原点") + print(f" 起点: {camera_pos}") + print(f" 方向: {direction}") + + # 设置射线 + self.ray.setOrigin(camera_pos) + self.ray.setDirection(direction) + + # 执行检测 + self.queue.clearEntries() + self.traverser.traverse(self.render) + + entries = self.queue.getNumEntries() + print(f" 检测到 {entries} 个碰撞") + + if entries > 0: + self.queue.sortEntries() + for i in range(entries): + entry = self.queue.getEntry(i) + hitPos = entry.getSurfacePoint(self.render) + hitNode = entry.getIntoNodePath() + distance = (hitPos - camera_pos).length() + print(f" 碰撞 {i}: {hitNode.getName()}, 距离: {distance:.2f}") + + # 测试2: 从固定位置射向球体 + print(f"\n测试2: 从固定位置射向球体") + fixed_origin = Point3(-10, 0, 0) + fixed_direction = Vec3(1, 0, 0) # 直接向右 + + print(f" 起点: {fixed_origin}") + print(f" 方向: {fixed_direction}") + + self.ray.setOrigin(fixed_origin) + self.ray.setDirection(fixed_direction) + + self.queue.clearEntries() + self.traverser.traverse(self.render) + + entries = self.queue.getNumEntries() + print(f" 检测到 {entries} 个碰撞") + + if entries > 0: + self.queue.sortEntries() + for i in range(entries): + entry = self.queue.getEntry(i) + hitPos = entry.getSurfacePoint(self.render) + hitNode = entry.getIntoNodePath() + distance = (hitPos - fixed_origin).length() + print(f" 碰撞 {i}: {hitNode.getName()}, 距离: {distance:.2f}") + + # 测试3: 检查射线是否正确设置 + print(f"\n测试3: 检查射线设置") + print(f" 射线起点: {self.ray.getOrigin()}") + print(f" 射线方向: {self.ray.getDirection()}") + + # 手动计算射线是否应该击中球体 + origin = self.ray.getOrigin() + direction = self.ray.getDirection() + sphere_center = Point3(0, 0, 0) + sphere_radius = 5.0 + + # 计算从射线起点到球心的向量 + to_sphere = sphere_center - origin + + # 计算射线上最接近球心的点 + t = to_sphere.dot(direction) + closest_point = origin + direction * t + + # 计算最近距离 + distance_to_center = (closest_point - sphere_center).length() + + print(f" 到球心的最近距离: {distance_to_center:.2f}") + print(f" 球体半径: {sphere_radius:.2f}") + + if distance_to_center <= sphere_radius: + print(f" 射线应该击中球体!") + else: + print(f" 射线会错过球体") + +if __name__ == "__main__": + app = BasicRayTest() print("\n基础测试完成!") \ No newline at end of file diff --git a/demo/center_ray_test.py b/demo/center_ray_test.py index 99fb3cf7..c0b9de05 100644 --- a/demo/center_ray_test.py +++ b/demo/center_ray_test.py @@ -1,169 +1,169 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import sys -from direct.showbase.ShowBase import ShowBase -from panda3d.core import ( - Point3, Point2, Vec3, CollisionTraverser, CollisionHandlerQueue, - CollisionNode, CollisionRay, CollisionSphere, BitMask32, CardMaker -) - -class CenterRayTest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 设置相机位置 - 和你的程序一样 - self.cam.setPos(-3.87655, -188.38084, 82.602684) - self.cam.lookAt(0, 0, 0) - - print(f"相机位置: {self.cam.getPos()}") - print(f"相机朝向: {self.cam.getHpr()}") - - # 创建测试立方体 - self.createTestCube() - - # 设置射线检测 - self.setupRayPicking() - - # 测试从屏幕中心射出的射线 - self.testCenterRay() - - # 测试朝向立方体的直接射线 - self.testDirectRay() - - def createTestCube(self): - """创建测试立方体""" - cm = CardMaker("TestCube") - cm.setFrame(-1, 1, -1, 1) - self.cube = self.render.attachNewNode(cm.generate()) - - self.cube.setPos(0, 10, 3) - self.cube.setScale(3, 3, 3) - self.cube.setColor(1, 0, 0, 1) - self.cube.setTwoSided(True) - self.cube.setHpr(45, 15, 0) - - print(f"立方体世界位置: {self.cube.getPos(self.render)}") - - # 添加碰撞检测 - cNode = CollisionNode('TestCubeCollision') - cSphere = CollisionSphere(Point3(0, 0, 0), 4.0) - cNode.addSolid(cSphere) - cNode.setIntoCollideMask(BitMask32.bit(0)) - cNode.setFromCollideMask(BitMask32.allOff()) - - self.cNodePath = self.cube.attachNewNode(cNode) - print(f"碰撞球体半径: 4.0, 实际半径: {4.0 * 3} = 12.0") - - def setupRayPicking(self): - """设置射线检测""" - self.picker = CollisionTraverser() - self.pickerQueue = CollisionHandlerQueue() - - self.pickerNode = CollisionNode('mouseRay') - self.pickerRay = CollisionRay() - self.pickerNode.addSolid(self.pickerRay) - - mask = BitMask32.bit(0) - self.pickerNode.setFromCollideMask(mask) - self.pickerNode.setIntoCollideMask(BitMask32.allOff()) - - self.pickerNP = self.cam.attachNewNode(self.pickerNode) - self.picker.addCollider(self.pickerNP, self.pickerQueue) - - def testCenterRay(self): - """测试从屏幕中心射出的射线""" - print(f"\n=== 屏幕中心射线测试 ===") - - # 屏幕中心的归一化坐标应该是 (0, 0) - normalized_x = 0.0 - normalized_y = 0.0 - - print(f"屏幕中心归一化坐标: ({normalized_x}, {normalized_y})") - - camera_pos = self.cam.getPos(self.render) - lens = self.cam.node().getLens() - - nearPoint = Point3() - farPoint = Point3() - - if lens.extrude(Point2(normalized_x, normalized_y), nearPoint, farPoint): - nearWorldPoint = self.cam.getRelativePoint(self.render, nearPoint) - farWorldPoint = self.cam.getRelativePoint(self.render, farPoint) - - rayDirWorld = farWorldPoint - nearWorldPoint - rayDirWorld.normalize() - - print(f"射线起点: {camera_pos}") - print(f"射线方向: {rayDirWorld}") - - # 设置射线 - self.pickerRay.setOrigin(camera_pos) - self.pickerRay.setDirection(rayDirWorld) - - # 执行碰撞检测 - self.pickerQueue.clearEntries() - self.picker.traverse(self.render) - - numEntries = self.pickerQueue.getNumEntries() - print(f"检测到 {numEntries} 个碰撞") - - if numEntries > 0: - self.pickerQueue.sortEntries() - for i in range(numEntries): - entry = self.pickerQueue.getEntry(i) - hitPos = entry.getSurfacePoint(self.render) - hitNode = entry.getIntoNodePath() - distance = (hitPos - camera_pos).length() - print(f" 碰撞 {i}: {hitNode.getName()}, 距离: {distance:.2f}, 位置: {hitPos}") - else: - # 分析射线到立方体的距离 - cube_world_pos = self.cube.getPos(self.render) - to_cube = cube_world_pos - camera_pos - cross_product = to_cube.cross(rayDirWorld) - distance_to_ray = cross_product.length() - - print(f" 射线到立方体中心的距离: {distance_to_ray:.2f}") - print(f" 需要小于: 12.0") - print(f" 射线错过了立方体") - - def testDirectRay(self): - """测试直接朝向立方体的射线""" - print(f"\n=== 直接射线测试 ===") - - camera_pos = self.cam.getPos(self.render) - cube_pos = self.cube.getPos(self.render) - - # 计算从相机直接指向立方体的射线 - direct_direction = cube_pos - camera_pos - direct_direction.normalize() - - print(f"相机位置: {camera_pos}") - print(f"立方体位置: {cube_pos}") - print(f"直接方向: {direct_direction}") - - # 设置射线 - self.pickerRay.setOrigin(camera_pos) - self.pickerRay.setDirection(direct_direction) - - # 执行碰撞检测 - self.pickerQueue.clearEntries() - self.picker.traverse(self.render) - - numEntries = self.pickerQueue.getNumEntries() - print(f"检测到 {numEntries} 个碰撞") - - if numEntries > 0: - self.pickerQueue.sortEntries() - for i in range(numEntries): - entry = self.pickerQueue.getEntry(i) - hitPos = entry.getSurfacePoint(self.render) - hitNode = entry.getIntoNodePath() - distance = (hitPos - camera_pos).length() - print(f" 碰撞 {i}: {hitNode.getName()}, 距离: {distance:.2f}, 位置: {hitPos}") - else: - print(" 直接射线也没击中!可能是碰撞设置问题。") - -if __name__ == "__main__": - app = CenterRayTest() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +from direct.showbase.ShowBase import ShowBase +from panda3d.core import ( + Point3, Point2, Vec3, CollisionTraverser, CollisionHandlerQueue, + CollisionNode, CollisionRay, CollisionSphere, BitMask32, CardMaker +) + +class CenterRayTest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 设置相机位置 - 和你的程序一样 + self.cam.setPos(-3.87655, -188.38084, 82.602684) + self.cam.lookAt(0, 0, 0) + + print(f"相机位置: {self.cam.getPos()}") + print(f"相机朝向: {self.cam.getHpr()}") + + # 创建测试立方体 + self.createTestCube() + + # 设置射线检测 + self.setupRayPicking() + + # 测试从屏幕中心射出的射线 + self.testCenterRay() + + # 测试朝向立方体的直接射线 + self.testDirectRay() + + def createTestCube(self): + """创建测试立方体""" + cm = CardMaker("TestCube") + cm.setFrame(-1, 1, -1, 1) + self.cube = self.render.attachNewNode(cm.generate()) + + self.cube.setPos(0, 10, 3) + self.cube.setScale(3, 3, 3) + self.cube.setColor(1, 0, 0, 1) + self.cube.setTwoSided(True) + self.cube.setHpr(45, 15, 0) + + print(f"立方体世界位置: {self.cube.getPos(self.render)}") + + # 添加碰撞检测 + cNode = CollisionNode('TestCubeCollision') + cSphere = CollisionSphere(Point3(0, 0, 0), 4.0) + cNode.addSolid(cSphere) + cNode.setIntoCollideMask(BitMask32.bit(0)) + cNode.setFromCollideMask(BitMask32.allOff()) + + self.cNodePath = self.cube.attachNewNode(cNode) + print(f"碰撞球体半径: 4.0, 实际半径: {4.0 * 3} = 12.0") + + def setupRayPicking(self): + """设置射线检测""" + self.picker = CollisionTraverser() + self.pickerQueue = CollisionHandlerQueue() + + self.pickerNode = CollisionNode('mouseRay') + self.pickerRay = CollisionRay() + self.pickerNode.addSolid(self.pickerRay) + + mask = BitMask32.bit(0) + self.pickerNode.setFromCollideMask(mask) + self.pickerNode.setIntoCollideMask(BitMask32.allOff()) + + self.pickerNP = self.cam.attachNewNode(self.pickerNode) + self.picker.addCollider(self.pickerNP, self.pickerQueue) + + def testCenterRay(self): + """测试从屏幕中心射出的射线""" + print(f"\n=== 屏幕中心射线测试 ===") + + # 屏幕中心的归一化坐标应该是 (0, 0) + normalized_x = 0.0 + normalized_y = 0.0 + + print(f"屏幕中心归一化坐标: ({normalized_x}, {normalized_y})") + + camera_pos = self.cam.getPos(self.render) + lens = self.cam.node().getLens() + + nearPoint = Point3() + farPoint = Point3() + + if lens.extrude(Point2(normalized_x, normalized_y), nearPoint, farPoint): + nearWorldPoint = self.cam.getRelativePoint(self.render, nearPoint) + farWorldPoint = self.cam.getRelativePoint(self.render, farPoint) + + rayDirWorld = farWorldPoint - nearWorldPoint + rayDirWorld.normalize() + + print(f"射线起点: {camera_pos}") + print(f"射线方向: {rayDirWorld}") + + # 设置射线 + self.pickerRay.setOrigin(camera_pos) + self.pickerRay.setDirection(rayDirWorld) + + # 执行碰撞检测 + self.pickerQueue.clearEntries() + self.picker.traverse(self.render) + + numEntries = self.pickerQueue.getNumEntries() + print(f"检测到 {numEntries} 个碰撞") + + if numEntries > 0: + self.pickerQueue.sortEntries() + for i in range(numEntries): + entry = self.pickerQueue.getEntry(i) + hitPos = entry.getSurfacePoint(self.render) + hitNode = entry.getIntoNodePath() + distance = (hitPos - camera_pos).length() + print(f" 碰撞 {i}: {hitNode.getName()}, 距离: {distance:.2f}, 位置: {hitPos}") + else: + # 分析射线到立方体的距离 + cube_world_pos = self.cube.getPos(self.render) + to_cube = cube_world_pos - camera_pos + cross_product = to_cube.cross(rayDirWorld) + distance_to_ray = cross_product.length() + + print(f" 射线到立方体中心的距离: {distance_to_ray:.2f}") + print(f" 需要小于: 12.0") + print(f" 射线错过了立方体") + + def testDirectRay(self): + """测试直接朝向立方体的射线""" + print(f"\n=== 直接射线测试 ===") + + camera_pos = self.cam.getPos(self.render) + cube_pos = self.cube.getPos(self.render) + + # 计算从相机直接指向立方体的射线 + direct_direction = cube_pos - camera_pos + direct_direction.normalize() + + print(f"相机位置: {camera_pos}") + print(f"立方体位置: {cube_pos}") + print(f"直接方向: {direct_direction}") + + # 设置射线 + self.pickerRay.setOrigin(camera_pos) + self.pickerRay.setDirection(direct_direction) + + # 执行碰撞检测 + self.pickerQueue.clearEntries() + self.picker.traverse(self.render) + + numEntries = self.pickerQueue.getNumEntries() + print(f"检测到 {numEntries} 个碰撞") + + if numEntries > 0: + self.pickerQueue.sortEntries() + for i in range(numEntries): + entry = self.pickerQueue.getEntry(i) + hitPos = entry.getSurfacePoint(self.render) + hitNode = entry.getIntoNodePath() + distance = (hitPos - camera_pos).length() + print(f" 碰撞 {i}: {hitNode.getName()}, 距离: {distance:.2f}, 位置: {hitPos}") + else: + print(" 直接射线也没击中!可能是碰撞设置问题。") + +if __name__ == "__main__": + app = CenterRayTest() print("\n测试完成!") \ No newline at end of file diff --git a/demo/chinese_gui_test.py b/demo/chinese_gui_test.py index 6d40f91e..d88590e4 100644 --- a/demo/chinese_gui_test.py +++ b/demo/chinese_gui_test.py @@ -1,261 +1,261 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -PANDA3D 中文GUI支持测试 -解决中文字体显示问题的示例 -""" - -from direct.showbase.ShowBase import ShowBase -from direct.gui.DirectGui import * -from direct.gui.OnscreenText import OnscreenText -from panda3d.core import * -import os - -class ChineseGUITest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 设置窗口属性 - wp = WindowProperties() - wp.setTitle("PANDA3D Chinese GUI Test") - self.win.requestProperties(wp) - self.setBackgroundColor(0.1, 0.2, 0.4) - - # 尝试加载中文字体 - self.chinese_font = self.loadChineseFont() - - # 创建GUI组件 - self.createGUI() - - print("="*50) - print("PANDA3D 中文GUI测试程序启动") - print(f"中文字体加载状态: {'成功' if self.chinese_font else '失败,使用默认字体'}") - print("="*50) - - def loadChineseFont(self): - """尝试加载中文字体""" - # 常见的中文字体路径列表 - font_paths = [ - # Ubuntu/Debian系统的中文字体 - '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', - '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', - '/usr/share/fonts/truetype/arphic/ukai.ttc', - '/usr/share/fonts/truetype/arphic/uming.ttc', - '/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf', - '/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc', - '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf', - # 其他可能的字体路径 - '/System/Library/Fonts/PingFang.ttc', # macOS - 'C:/Windows/Fonts/msyh.ttc', # Windows - 'C:/Windows/Fonts/simhei.ttf', # Windows - ] - - print("正在尝试加载中文字体...") - - for font_path in font_paths: - if os.path.exists(font_path): - try: - print(f"尝试加载字体: {font_path}") - font = self.loader.loadFont(font_path) - if font: - print(f"成功加载字体: {font_path}") - return font - else: - print(f"字体文件存在但加载失败: {font_path}") - except Exception as e: - print(f"加载字体时出错: {font_path}, 错误: {e}") - - print("未找到可用的中文字体,将使用默认字体") - print("如果中文显示为方块,请安装中文字体:") - print("sudo apt-get install fonts-wqy-microhei fonts-wqy-zenhei") - return None - - def createText(self, text, pos, scale=0.06, fg=(1, 1, 1, 1)): - """创建文本,优先使用中文字体""" - if self.chinese_font: - return OnscreenText( - text=text, - pos=pos, - scale=scale, - fg=fg, - align=TextNode.ALeft, - font=self.chinese_font - ) - else: - return OnscreenText( - text=text, - pos=pos, - scale=scale, - fg=fg, - align=TextNode.ALeft - ) - - def createButton(self, text, pos, scale=0.06, command=None, frameColor=(0.5, 0.5, 0.5, 1)): - """创建支持中文字体的按钮""" - return DirectButton( - text=text, - pos=pos, - scale=scale, - command=command, - frameColor=frameColor, - text_fg=(1, 1, 1, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - def createGUI(self): - """创建GUI组件""" - # 标题 - self.title = self.createText( - "PANDA3D 中文GUI测试", - (0, 0.85), - 0.08, - (1, 1, 0, 1) - ) - self.title.setAlign(TextNode.ACenter) - - # 说明文本 - self.info = self.createText( - "这个程序测试中文字体在PANDA3D中的显示效果", - (-0.95, 0.7), - 0.05, - (0.8, 0.8, 0.8, 1) - ) - - # 创建测试按钮 - 支持中文字体 - self.testButton = self.createButton( - text="Test Button (测试按钮)", - pos=(-0.6, 0, 0.5), - scale=0.08, - command=self.onTestClick, - frameColor=(0.2, 0.7, 0.2, 1) - ) - - # 创建标签显示区域 - self.statusLabel = self.createText( - "状态: 程序就绪", - (-0.95, 0.3), - 0.06, - (0, 1, 0, 1) - ) - - # 创建功能测试区域 - self.createTestArea() - - # 创建字体信息显示 - self.createFontInfo() - - # 退出按钮 - 支持中文字体 - self.exitButton = self.createButton( - text="Exit (退出)", - pos=(0.7, 0, -0.7), - scale=0.06, - command=self.exitProgram, - frameColor=(0.8, 0.2, 0.2, 1) - ) - - def createTestArea(self): - """创建测试区域""" - # 中文测试文本 - chinese_texts = [ - "简体中文测试:你好世界!", - "常用汉字:一二三四五六七八九十", - "标点符号:,。!?;:""''()", - "数字混合:2023年12月25日", - "英中混合:Hello 世界 World 中国" - ] - - self.test_texts = [] - for i, text in enumerate(chinese_texts): - text_obj = self.createText( - text, - (-0.95, 0.1 - i*0.08), - 0.05, - (1, 1, 1, 1) - ) - self.test_texts.append(text_obj) - - # 计数器 - self.clickCount = 0 - self.counterText = self.createText( - f"点击次数: {self.clickCount}", - (-0.95, -0.5), - 0.06, - (1, 0.5, 0, 1) - ) - - def createFontInfo(self): - """创建字体信息显示""" - font_status = "已加载中文字体" if self.chinese_font else "使用默认字体" - - self.fontInfo = self.createText( - f"字体状态: {font_status}", - (0.2, 0.7), - 0.05, - (0.8, 0.8, 0.2, 1) - ) - - # 显示字体路径信息 - if self.chinese_font: - # 尝试获取字体信息 - try: - font_name = self.chinese_font.getName() - self.fontPath = self.createText( - f"字体名称: {font_name}", - (0.2, 0.6), - 0.04, - (0.6, 0.6, 0.6, 1) - ) - except: - self.fontPath = self.createText( - "字体信息: 无法获取", - (0.2, 0.6), - 0.04, - (0.6, 0.6, 0.6, 1) - ) - else: - self.fontPath = self.createText( - "建议安装: fonts-wqy-microhei", - (0.2, 0.6), - 0.04, - (1, 0.5, 0.5, 1) - ) - - def onTestClick(self): - """测试按钮点击事件""" - self.clickCount += 1 - - # 更新状态和计数器 - self.statusLabel.setText(f"状态: 按钮被点击了 {self.clickCount} 次") - self.counterText.setText(f"点击次数: {self.clickCount}") - - # 循环变换测试文本 - test_messages = [ - "第一次点击:欢迎使用PANDA3D!", - "第二次点击:中文显示测试正常", - "第三次点击:功能运行良好", - "继续点击:一切正常运行中..." - ] - - if self.clickCount <= len(test_messages): - message = test_messages[self.clickCount - 1] - else: - message = f"第{self.clickCount}次点击:持续测试中..." - - # 更新第一行测试文本 - if self.test_texts: - self.test_texts[0].setText(message) - - print(f"按钮点击: {self.clickCount} 次, 消息: {message}") - - def exitProgram(self): - """退出程序""" - print("用户选择退出程序") - print("再见!Goodbye!") - self.userExit() - -if __name__ == "__main__": - print("启动PANDA3D中文GUI测试程序...") - print("如果看到方块字符,说明需要安装中文字体") - app = ChineseGUITest() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +PANDA3D 中文GUI支持测试 +解决中文字体显示问题的示例 +""" + +from direct.showbase.ShowBase import ShowBase +from direct.gui.DirectGui import * +from direct.gui.OnscreenText import OnscreenText +from panda3d.core import * +import os + +class ChineseGUITest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 设置窗口属性 + wp = WindowProperties() + wp.setTitle("PANDA3D Chinese GUI Test") + self.win.requestProperties(wp) + self.setBackgroundColor(0.1, 0.2, 0.4) + + # 尝试加载中文字体 + self.chinese_font = self.loadChineseFont() + + # 创建GUI组件 + self.createGUI() + + print("="*50) + print("PANDA3D 中文GUI测试程序启动") + print(f"中文字体加载状态: {'成功' if self.chinese_font else '失败,使用默认字体'}") + print("="*50) + + def loadChineseFont(self): + """尝试加载中文字体""" + # 常见的中文字体路径列表 + font_paths = [ + # Ubuntu/Debian系统的中文字体 + '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', + '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', + '/usr/share/fonts/truetype/arphic/ukai.ttc', + '/usr/share/fonts/truetype/arphic/uming.ttc', + '/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf', + '/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc', + '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf', + # 其他可能的字体路径 + '/System/Library/Fonts/PingFang.ttc', # macOS + 'C:/Windows/Fonts/msyh.ttc', # Windows + 'C:/Windows/Fonts/simhei.ttf', # Windows + ] + + print("正在尝试加载中文字体...") + + for font_path in font_paths: + if os.path.exists(font_path): + try: + print(f"尝试加载字体: {font_path}") + font = self.loader.loadFont(font_path) + if font: + print(f"成功加载字体: {font_path}") + return font + else: + print(f"字体文件存在但加载失败: {font_path}") + except Exception as e: + print(f"加载字体时出错: {font_path}, 错误: {e}") + + print("未找到可用的中文字体,将使用默认字体") + print("如果中文显示为方块,请安装中文字体:") + print("sudo apt-get install fonts-wqy-microhei fonts-wqy-zenhei") + return None + + def createText(self, text, pos, scale=0.06, fg=(1, 1, 1, 1)): + """创建文本,优先使用中文字体""" + if self.chinese_font: + return OnscreenText( + text=text, + pos=pos, + scale=scale, + fg=fg, + align=TextNode.ALeft, + font=self.chinese_font + ) + else: + return OnscreenText( + text=text, + pos=pos, + scale=scale, + fg=fg, + align=TextNode.ALeft + ) + + def createButton(self, text, pos, scale=0.06, command=None, frameColor=(0.5, 0.5, 0.5, 1)): + """创建支持中文字体的按钮""" + return DirectButton( + text=text, + pos=pos, + scale=scale, + command=command, + frameColor=frameColor, + text_fg=(1, 1, 1, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + def createGUI(self): + """创建GUI组件""" + # 标题 + self.title = self.createText( + "PANDA3D 中文GUI测试", + (0, 0.85), + 0.08, + (1, 1, 0, 1) + ) + self.title.setAlign(TextNode.ACenter) + + # 说明文本 + self.info = self.createText( + "这个程序测试中文字体在PANDA3D中的显示效果", + (-0.95, 0.7), + 0.05, + (0.8, 0.8, 0.8, 1) + ) + + # 创建测试按钮 - 支持中文字体 + self.testButton = self.createButton( + text="Test Button (测试按钮)", + pos=(-0.6, 0, 0.5), + scale=0.08, + command=self.onTestClick, + frameColor=(0.2, 0.7, 0.2, 1) + ) + + # 创建标签显示区域 + self.statusLabel = self.createText( + "状态: 程序就绪", + (-0.95, 0.3), + 0.06, + (0, 1, 0, 1) + ) + + # 创建功能测试区域 + self.createTestArea() + + # 创建字体信息显示 + self.createFontInfo() + + # 退出按钮 - 支持中文字体 + self.exitButton = self.createButton( + text="Exit (退出)", + pos=(0.7, 0, -0.7), + scale=0.06, + command=self.exitProgram, + frameColor=(0.8, 0.2, 0.2, 1) + ) + + def createTestArea(self): + """创建测试区域""" + # 中文测试文本 + chinese_texts = [ + "简体中文测试:你好世界!", + "常用汉字:一二三四五六七八九十", + "标点符号:,。!?;:""''()", + "数字混合:2023年12月25日", + "英中混合:Hello 世界 World 中国" + ] + + self.test_texts = [] + for i, text in enumerate(chinese_texts): + text_obj = self.createText( + text, + (-0.95, 0.1 - i*0.08), + 0.05, + (1, 1, 1, 1) + ) + self.test_texts.append(text_obj) + + # 计数器 + self.clickCount = 0 + self.counterText = self.createText( + f"点击次数: {self.clickCount}", + (-0.95, -0.5), + 0.06, + (1, 0.5, 0, 1) + ) + + def createFontInfo(self): + """创建字体信息显示""" + font_status = "已加载中文字体" if self.chinese_font else "使用默认字体" + + self.fontInfo = self.createText( + f"字体状态: {font_status}", + (0.2, 0.7), + 0.05, + (0.8, 0.8, 0.2, 1) + ) + + # 显示字体路径信息 + if self.chinese_font: + # 尝试获取字体信息 + try: + font_name = self.chinese_font.getName() + self.fontPath = self.createText( + f"字体名称: {font_name}", + (0.2, 0.6), + 0.04, + (0.6, 0.6, 0.6, 1) + ) + except: + self.fontPath = self.createText( + "字体信息: 无法获取", + (0.2, 0.6), + 0.04, + (0.6, 0.6, 0.6, 1) + ) + else: + self.fontPath = self.createText( + "建议安装: fonts-wqy-microhei", + (0.2, 0.6), + 0.04, + (1, 0.5, 0.5, 1) + ) + + def onTestClick(self): + """测试按钮点击事件""" + self.clickCount += 1 + + # 更新状态和计数器 + self.statusLabel.setText(f"状态: 按钮被点击了 {self.clickCount} 次") + self.counterText.setText(f"点击次数: {self.clickCount}") + + # 循环变换测试文本 + test_messages = [ + "第一次点击:欢迎使用PANDA3D!", + "第二次点击:中文显示测试正常", + "第三次点击:功能运行良好", + "继续点击:一切正常运行中..." + ] + + if self.clickCount <= len(test_messages): + message = test_messages[self.clickCount - 1] + else: + message = f"第{self.clickCount}次点击:持续测试中..." + + # 更新第一行测试文本 + if self.test_texts: + self.test_texts[0].setText(message) + + print(f"按钮点击: {self.clickCount} 次, 消息: {message}") + + def exitProgram(self): + """退出程序""" + print("用户选择退出程序") + print("再见!Goodbye!") + self.userExit() + +if __name__ == "__main__": + print("启动PANDA3D中文GUI测试程序...") + print("如果看到方块字符,说明需要安装中文字体") + app = ChineseGUITest() app.run() \ No newline at end of file diff --git a/demo/collision_debug.py b/demo/collision_debug.py index 4266707f..91654257 100644 --- a/demo/collision_debug.py +++ b/demo/collision_debug.py @@ -1,189 +1,189 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import sys -from direct.showbase.ShowBase import ShowBase -from panda3d.core import ( - Point3, Point2, Vec3, CollisionTraverser, CollisionHandlerQueue, - CollisionNode, CollisionRay, CollisionSphere, BitMask32, CardMaker -) - -class CollisionDebugTest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 设置相机位置 - self.cam.setPos(-3.87655, -188.38084, 82.602684) - self.cam.lookAt(0, 0, 0) - - print(f"相机位置: {self.cam.getPos()}") - - # 创建测试立方体 - self.createTestCube() - - # 设置射线检测 - self.setupRayPicking() - - # 调试碰撞系统 - self.debugCollisionSystem() - - # 测试不同类型的碰撞体 - self.testDifferentCollisionTypes() - - def createTestCube(self): - """创建测试立方体""" - cm = CardMaker("TestCube") - cm.setFrame(-1, 1, -1, 1) - self.cube = self.render.attachNewNode(cm.generate()) - - self.cube.setPos(0, 10, 3) - self.cube.setScale(3, 3, 3) - self.cube.setColor(1, 0, 0, 1) - self.cube.setTwoSided(True) - - print(f"立方体世界位置: {self.cube.getPos(self.render)}") - - # 添加碰撞检测 - cNode = CollisionNode('TestCubeCollision') - cSphere = CollisionSphere(Point3(0, 0, 0), 4.0) - cNode.addSolid(cSphere) - cNode.setIntoCollideMask(BitMask32.bit(0)) - cNode.setFromCollideMask(BitMask32.allOff()) - - self.cNodePath = self.cube.attachNewNode(cNode) - - # 强制显示碰撞体 - self.cNodePath.show() - print(f"碰撞体已强制显示") - - def setupRayPicking(self): - """设置射线检测""" - self.picker = CollisionTraverser() - self.pickerQueue = CollisionHandlerQueue() - - self.pickerNode = CollisionNode('mouseRay') - self.pickerRay = CollisionRay() - self.pickerNode.addSolid(self.pickerRay) - - # 设置掩码 - fromMask = BitMask32.bit(0) - self.pickerNode.setFromCollideMask(fromMask) - self.pickerNode.setIntoCollideMask(BitMask32.allOff()) - - self.pickerNP = self.cam.attachNewNode(self.pickerNode) - self.picker.addCollider(self.pickerNP, self.pickerQueue) - - print(f"射线检测器 From 掩码: {fromMask}") - print(f"立方体 Into 掩码: {self.cNodePath.node().getIntoCollideMask()}") - - def debugCollisionSystem(self): - """调试碰撞系统""" - print(f"\n=== 碰撞系统调试 ===") - - # 检查碰撞遍历器 - print(f"碰撞遍历器中的碰撞器数量: {self.picker.getNumColliders()}") - - for i in range(self.picker.getNumColliders()): - collider = self.picker.getCollider(i) - print(f" 碰撞器 {i}: {collider}") - - # 检查render下的所有碰撞节点 - print(f"\nrender下的所有节点:") - self.printNodeHierarchy(self.render, 0) - - def printNodeHierarchy(self, node, depth): - """打印节点层次结构""" - indent = " " * depth - print(f"{indent}{node.getName()}: {type(node.node()).__name__}") - - if isinstance(node.node(), CollisionNode): - cnode = node.node() - print(f"{indent} CollisionNode:") - print(f"{indent} IntoMask: {cnode.getIntoCollideMask()}") - print(f"{indent} FromMask: {cnode.getFromCollideMask()}") - print(f"{indent} Solids: {cnode.getNumSolids()}") - for i in range(cnode.getNumSolids()): - solid = cnode.getSolid(i) - print(f"{indent} Solid {i}: {type(solid).__name__}") - - # 递归处理子节点,但限制深度 - if depth < 3: - for child in node.getChildren(): - self.printNodeHierarchy(child, depth + 1) - - def testDifferentCollisionTypes(self): - """测试不同类型的碰撞体""" - print(f"\n=== 测试不同碰撞体类型 ===") - - camera_pos = self.cam.getPos(self.render) - cube_pos = self.cube.getPos(self.render) - - # 计算直接射线 - direct_direction = cube_pos - camera_pos - direct_direction.normalize() - - print(f"使用直接射线: 起点={camera_pos}, 方向={direct_direction}") - - # 测试1: 当前设置 - print(f"\n1. 测试当前碰撞球体设置:") - self.testRayHit(camera_pos, direct_direction) - - # 测试2: 更大的碰撞球体 - print(f"\n2. 测试更大的碰撞球体:") - # 移除当前碰撞体 - self.cNodePath.removeNode() - - # 创建更大的碰撞球体 - cNode2 = CollisionNode('LargeSphere') - cSphere2 = CollisionSphere(Point3(0, 0, 0), 20.0) # 更大的半径 - cNode2.addSolid(cSphere2) - cNode2.setIntoCollideMask(BitMask32.bit(0)) - cNode2.setFromCollideMask(BitMask32.allOff()) - - self.cNodePath2 = self.cube.attachNewNode(cNode2) - self.cNodePath2.show() - - self.testRayHit(camera_pos, direct_direction) - - # 测试3: 在render根下创建独立碰撞体 - print(f"\n3. 测试独立碰撞体:") - cNode3 = CollisionNode('IndependentSphere') - cSphere3 = CollisionSphere(cube_pos, 15.0) # 直接使用世界坐标 - cNode3.addSolid(cSphere3) - cNode3.setIntoCollideMask(BitMask32.bit(0)) - cNode3.setFromCollideMask(BitMask32.allOff()) - - self.cNodePath3 = self.render.attachNewNode(cNode3) - self.cNodePath3.show() - - self.testRayHit(camera_pos, direct_direction) - - def testRayHit(self, origin, direction): - """测试射线碰撞""" - # 设置射线 - self.pickerRay.setOrigin(origin) - self.pickerRay.setDirection(direction) - - # 清除之前的结果 - self.pickerQueue.clearEntries() - - # 执行碰撞检测 - self.picker.traverse(self.render) - - numEntries = self.pickerQueue.getNumEntries() - print(f" 检测到 {numEntries} 个碰撞") - - if numEntries > 0: - self.pickerQueue.sortEntries() - for i in range(numEntries): - entry = self.pickerQueue.getEntry(i) - hitPos = entry.getSurfacePoint(self.render) - hitNode = entry.getIntoNodePath() - distance = (hitPos - origin).length() - print(f" 碰撞 {i}: {hitNode.getName()}, 距离: {distance:.2f}, 位置: {hitPos}") - else: - print(f" 没有检测到碰撞") - -if __name__ == "__main__": - app = CollisionDebugTest() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +from direct.showbase.ShowBase import ShowBase +from panda3d.core import ( + Point3, Point2, Vec3, CollisionTraverser, CollisionHandlerQueue, + CollisionNode, CollisionRay, CollisionSphere, BitMask32, CardMaker +) + +class CollisionDebugTest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 设置相机位置 + self.cam.setPos(-3.87655, -188.38084, 82.602684) + self.cam.lookAt(0, 0, 0) + + print(f"相机位置: {self.cam.getPos()}") + + # 创建测试立方体 + self.createTestCube() + + # 设置射线检测 + self.setupRayPicking() + + # 调试碰撞系统 + self.debugCollisionSystem() + + # 测试不同类型的碰撞体 + self.testDifferentCollisionTypes() + + def createTestCube(self): + """创建测试立方体""" + cm = CardMaker("TestCube") + cm.setFrame(-1, 1, -1, 1) + self.cube = self.render.attachNewNode(cm.generate()) + + self.cube.setPos(0, 10, 3) + self.cube.setScale(3, 3, 3) + self.cube.setColor(1, 0, 0, 1) + self.cube.setTwoSided(True) + + print(f"立方体世界位置: {self.cube.getPos(self.render)}") + + # 添加碰撞检测 + cNode = CollisionNode('TestCubeCollision') + cSphere = CollisionSphere(Point3(0, 0, 0), 4.0) + cNode.addSolid(cSphere) + cNode.setIntoCollideMask(BitMask32.bit(0)) + cNode.setFromCollideMask(BitMask32.allOff()) + + self.cNodePath = self.cube.attachNewNode(cNode) + + # 强制显示碰撞体 + self.cNodePath.show() + print(f"碰撞体已强制显示") + + def setupRayPicking(self): + """设置射线检测""" + self.picker = CollisionTraverser() + self.pickerQueue = CollisionHandlerQueue() + + self.pickerNode = CollisionNode('mouseRay') + self.pickerRay = CollisionRay() + self.pickerNode.addSolid(self.pickerRay) + + # 设置掩码 + fromMask = BitMask32.bit(0) + self.pickerNode.setFromCollideMask(fromMask) + self.pickerNode.setIntoCollideMask(BitMask32.allOff()) + + self.pickerNP = self.cam.attachNewNode(self.pickerNode) + self.picker.addCollider(self.pickerNP, self.pickerQueue) + + print(f"射线检测器 From 掩码: {fromMask}") + print(f"立方体 Into 掩码: {self.cNodePath.node().getIntoCollideMask()}") + + def debugCollisionSystem(self): + """调试碰撞系统""" + print(f"\n=== 碰撞系统调试 ===") + + # 检查碰撞遍历器 + print(f"碰撞遍历器中的碰撞器数量: {self.picker.getNumColliders()}") + + for i in range(self.picker.getNumColliders()): + collider = self.picker.getCollider(i) + print(f" 碰撞器 {i}: {collider}") + + # 检查render下的所有碰撞节点 + print(f"\nrender下的所有节点:") + self.printNodeHierarchy(self.render, 0) + + def printNodeHierarchy(self, node, depth): + """打印节点层次结构""" + indent = " " * depth + print(f"{indent}{node.getName()}: {type(node.node()).__name__}") + + if isinstance(node.node(), CollisionNode): + cnode = node.node() + print(f"{indent} CollisionNode:") + print(f"{indent} IntoMask: {cnode.getIntoCollideMask()}") + print(f"{indent} FromMask: {cnode.getFromCollideMask()}") + print(f"{indent} Solids: {cnode.getNumSolids()}") + for i in range(cnode.getNumSolids()): + solid = cnode.getSolid(i) + print(f"{indent} Solid {i}: {type(solid).__name__}") + + # 递归处理子节点,但限制深度 + if depth < 3: + for child in node.getChildren(): + self.printNodeHierarchy(child, depth + 1) + + def testDifferentCollisionTypes(self): + """测试不同类型的碰撞体""" + print(f"\n=== 测试不同碰撞体类型 ===") + + camera_pos = self.cam.getPos(self.render) + cube_pos = self.cube.getPos(self.render) + + # 计算直接射线 + direct_direction = cube_pos - camera_pos + direct_direction.normalize() + + print(f"使用直接射线: 起点={camera_pos}, 方向={direct_direction}") + + # 测试1: 当前设置 + print(f"\n1. 测试当前碰撞球体设置:") + self.testRayHit(camera_pos, direct_direction) + + # 测试2: 更大的碰撞球体 + print(f"\n2. 测试更大的碰撞球体:") + # 移除当前碰撞体 + self.cNodePath.removeNode() + + # 创建更大的碰撞球体 + cNode2 = CollisionNode('LargeSphere') + cSphere2 = CollisionSphere(Point3(0, 0, 0), 20.0) # 更大的半径 + cNode2.addSolid(cSphere2) + cNode2.setIntoCollideMask(BitMask32.bit(0)) + cNode2.setFromCollideMask(BitMask32.allOff()) + + self.cNodePath2 = self.cube.attachNewNode(cNode2) + self.cNodePath2.show() + + self.testRayHit(camera_pos, direct_direction) + + # 测试3: 在render根下创建独立碰撞体 + print(f"\n3. 测试独立碰撞体:") + cNode3 = CollisionNode('IndependentSphere') + cSphere3 = CollisionSphere(cube_pos, 15.0) # 直接使用世界坐标 + cNode3.addSolid(cSphere3) + cNode3.setIntoCollideMask(BitMask32.bit(0)) + cNode3.setFromCollideMask(BitMask32.allOff()) + + self.cNodePath3 = self.render.attachNewNode(cNode3) + self.cNodePath3.show() + + self.testRayHit(camera_pos, direct_direction) + + def testRayHit(self, origin, direction): + """测试射线碰撞""" + # 设置射线 + self.pickerRay.setOrigin(origin) + self.pickerRay.setDirection(direction) + + # 清除之前的结果 + self.pickerQueue.clearEntries() + + # 执行碰撞检测 + self.picker.traverse(self.render) + + numEntries = self.pickerQueue.getNumEntries() + print(f" 检测到 {numEntries} 个碰撞") + + if numEntries > 0: + self.pickerQueue.sortEntries() + for i in range(numEntries): + entry = self.pickerQueue.getEntry(i) + hitPos = entry.getSurfacePoint(self.render) + hitNode = entry.getIntoNodePath() + distance = (hitPos - origin).length() + print(f" 碰撞 {i}: {hitNode.getName()}, 距离: {distance:.2f}, 位置: {hitPos}") + else: + print(f" 没有检测到碰撞") + +if __name__ == "__main__": + app = CollisionDebugTest() print("\n调试完成!") \ No newline at end of file diff --git a/demo/color_picking_test.py b/demo/color_picking_test.py index 477ff19a..31e74e3c 100644 --- a/demo/color_picking_test.py +++ b/demo/color_picking_test.py @@ -1,240 +1,240 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import sys -from direct.showbase.ShowBase import ShowBase -from panda3d.core import ( - Point3, Vec3, Vec4, CardMaker, Texture, GraphicsOutput, - FrameBufferProperties, WindowProperties, RenderState, - ColorWriteAttrib, DepthTestAttrib, BitMask32, PNMImage, - Camera, DisplayRegion, GraphicsStateGuardian, RenderState -) - -class ColorPickingTest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 相机设置 - self.cam.setPos(-3.87655, -188.38084, 82.602684) - self.cam.lookAt(0, 0, 0) - - print("=== 颜色编码点击检测测试 ===") - - # 创建测试对象 - self.createTestObjects() - - # 设置颜色拾取缓冲区 - self.setupColorPickingBuffer() - - # 测试颜色拾取 - self.testColorPicking() - - def createTestObjects(self): - """创建测试对象""" - self.objects = [] - self.id_to_object = {} # ID到对象的映射 - - # 创建几个测试立方体 - positions = [ - (0, 10, 3, "中心立方体"), - (-5, 10, 3, "左侧立方体"), - (5, 10, 3, "右侧立方体"), - (0, 10, 8, "上方立方体"), - (0, 15, 3, "远处立方体") - ] - - display_colors = [ - (1, 0, 0, 1), # 红色 - (0, 1, 0, 1), # 绿色 - (0, 0, 1, 1), # 蓝色 - (1, 1, 0, 1), # 黄色 - (1, 0, 1, 1), # 紫色 - ] - - for i, ((x, y, z, name), display_color) in enumerate(zip(positions, display_colors)): - obj_id = i + 1 # 从1开始编号 - - # 创建立方体 - cm = CardMaker(f"cube_{i}") - cm.setFrame(-1, 1, -1, 1) - cube = self.render.attachNewNode(cm.generate()) - cube.setPos(x, y, z) - cube.setScale(2, 2, 2) - cube.setColor(*display_color) - cube.setBillboardAxis() - cube.setName(name) - - # 为颜色拾取创建唯一颜色 - # 将ID编码为RGB颜色 - pick_color = self.encodeIdToColor(obj_id) - - # 存储对象信息 - obj_info = { - 'node': cube, - 'name': name, - 'world_pos': Point3(x, y, z), - 'id': obj_id, - 'pick_color': pick_color, - 'display_color': display_color - } - self.objects.append(obj_info) - self.id_to_object[obj_id] = obj_info - - print(f"创建 {name} 在位置 ({x}, {y}, {z}), ID: {obj_id}, 拾取颜色: {pick_color}") - - def encodeIdToColor(self, obj_id): - """将对象ID编码为RGB颜色""" - # 将ID分解为RGB分量(24位) - r = (obj_id & 0xFF0000) >> 16 - g = (obj_id & 0x00FF00) >> 8 - b = (obj_id & 0x0000FF) - - # 转换为0-1范围的浮点数 - return Vec4(r / 255.0, g / 255.0, b / 255.0, 1.0) - - def decodeColorToId(self, color): - """将RGB颜色解码为对象ID""" - if color is None: - return 0 - - # 转换为0-255范围的整数 - r = int(color[0] * 255 + 0.5) - g = int(color[1] * 255 + 0.5) - b = int(color[2] * 255 + 0.5) - - # 组合为ID - obj_id = (r << 16) | (g << 8) | b - return obj_id - - def setupColorPickingBuffer(self): - """设置颜色拾取渲染缓冲区""" - # 创建离屏渲染缓冲区 - fbp = FrameBufferProperties() - fbp.setRgbColor(True) - fbp.setColorBits(8, 8, 8, 0) # RGB,不需要Alpha - fbp.setDepthBits(24) - - wp = WindowProperties() - wp.setSize(self.win.getXSize(), self.win.getYSize()) - - # 创建缓冲区 - self.pick_buffer = self.graphicsEngine.makeOutput( - self.pipe, "pick_buffer", -1, fbp, wp, - GraphicsOutput.M_offscreen, self.win.getGsg(), self.win - ) - - if self.pick_buffer is None: - print("无法创建颜色拾取缓冲区") - return - - # 创建拾取相机 - self.pick_cam = self.makeCamera(self.pick_buffer) - self.pick_cam.node().setLens(self.cam.node().getLens()) - self.pick_cam.setPos(self.cam.getPos()) - self.pick_cam.setHpr(self.cam.getHpr()) - - # 创建拾取场景(带有颜色编码的对象) - self.pick_scene = self.render.attachNewNode("pick_scene") - - # 为每个对象创建拾取版本 - for obj_info in self.objects: - # 复制几何体 - pick_node = obj_info['node'].copyTo(self.pick_scene) - pick_node.setColor(obj_info['pick_color']) - pick_node.setRenderModeWireframe(False) - pick_node.setLightOff() # 关闭光照以确保颜色准确 - - # 设置拾取相机只渲染拾取场景 - self.pick_cam.node().setCameraMask(BitMask32.allOn()) - - # 创建颜色纹理 - self.pick_tex = Texture() - self.pick_tex.setFormat(Texture.FRgb8) - self.pick_buffer.addRenderTexture(self.pick_tex, GraphicsOutput.RTMCopyRam) - - print("颜色拾取缓冲区设置完成") - - def getColorAtPixel(self, mouseX, mouseY): - """获取指定像素的颜色值""" - if self.pick_buffer is None: - return None - - # 获取窗口大小 - winWidth = self.pick_buffer.getXSize() - winHeight = self.pick_buffer.getYSize() - - # 确保坐标在窗口范围内 - if mouseX < 0 or mouseX >= winWidth or mouseY < 0 or mouseY >= winHeight: - return None - - # 强制渲染拾取缓冲区 - self.pick_buffer.setActive(True) - self.graphicsEngine.renderFrame() - - # 读取颜色纹理 - if self.pick_tex.hasRamImage(): - # 获取颜色图像 - pnm = PNMImage() - self.pick_tex.store(pnm) - - # Y坐标需要翻转(OpenGL坐标系) - flippedY = winHeight - 1 - mouseY - - if flippedY < pnm.getYSize() and mouseX < pnm.getXSize(): - # 获取颜色值 - r = pnm.getRed(mouseX, flippedY) - g = pnm.getGreen(mouseX, flippedY) - b = pnm.getBlue(mouseX, flippedY) - return (r, g, b) - - return None - - def pickObjectByColor(self, mouseX, mouseY): - """使用颜色编码选择对象""" - print(f"\n=== 颜色拾取: 鼠标位置 ({mouseX}, {mouseY}) ===") - - # 获取点击位置的颜色 - color = self.getColorAtPixel(mouseX, mouseY) - if color is None: - print("无法读取颜色值") - return None - - print(f"拾取颜色: RGB({color[0]:.3f}, {color[1]:.3f}, {color[2]:.3f})") - - # 解码颜色为对象ID - obj_id = self.decodeColorToId(color) - print(f"解码的对象ID: {obj_id}") - - # 查找对应的对象 - if obj_id in self.id_to_object: - obj_info = self.id_to_object[obj_id] - print(f"选中对象: {obj_info['name']}") - return obj_info - else: - print("没有找到对应的对象") - return None - - def testColorPicking(self): - """测试颜色拾取""" - print(f"\n=== 颜色拾取测试 ===") - - # 等待几帧以确保场景渲染完成 - for i in range(5): - self.graphicsEngine.renderFrame() - - # 模拟不同的点击位置 - test_clicks = [ - (619, 362, "屏幕中心"), - (400, 362, "屏幕偏左"), - (800, 362, "屏幕偏右"), - (619, 200, "屏幕上方"), - (619, 500, "屏幕下方"), - ] - - for mouseX, mouseY, description in test_clicks: - print(f"\n--- 测试 {description}: ({mouseX}, {mouseY}) ---") - selected = self.pickObjectByColor(mouseX, mouseY) - -if __name__ == "__main__": - app = ColorPickingTest() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +from direct.showbase.ShowBase import ShowBase +from panda3d.core import ( + Point3, Vec3, Vec4, CardMaker, Texture, GraphicsOutput, + FrameBufferProperties, WindowProperties, RenderState, + ColorWriteAttrib, DepthTestAttrib, BitMask32, PNMImage, + Camera, DisplayRegion, GraphicsStateGuardian, RenderState +) + +class ColorPickingTest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 相机设置 + self.cam.setPos(-3.87655, -188.38084, 82.602684) + self.cam.lookAt(0, 0, 0) + + print("=== 颜色编码点击检测测试 ===") + + # 创建测试对象 + self.createTestObjects() + + # 设置颜色拾取缓冲区 + self.setupColorPickingBuffer() + + # 测试颜色拾取 + self.testColorPicking() + + def createTestObjects(self): + """创建测试对象""" + self.objects = [] + self.id_to_object = {} # ID到对象的映射 + + # 创建几个测试立方体 + positions = [ + (0, 10, 3, "中心立方体"), + (-5, 10, 3, "左侧立方体"), + (5, 10, 3, "右侧立方体"), + (0, 10, 8, "上方立方体"), + (0, 15, 3, "远处立方体") + ] + + display_colors = [ + (1, 0, 0, 1), # 红色 + (0, 1, 0, 1), # 绿色 + (0, 0, 1, 1), # 蓝色 + (1, 1, 0, 1), # 黄色 + (1, 0, 1, 1), # 紫色 + ] + + for i, ((x, y, z, name), display_color) in enumerate(zip(positions, display_colors)): + obj_id = i + 1 # 从1开始编号 + + # 创建立方体 + cm = CardMaker(f"cube_{i}") + cm.setFrame(-1, 1, -1, 1) + cube = self.render.attachNewNode(cm.generate()) + cube.setPos(x, y, z) + cube.setScale(2, 2, 2) + cube.setColor(*display_color) + cube.setBillboardAxis() + cube.setName(name) + + # 为颜色拾取创建唯一颜色 + # 将ID编码为RGB颜色 + pick_color = self.encodeIdToColor(obj_id) + + # 存储对象信息 + obj_info = { + 'node': cube, + 'name': name, + 'world_pos': Point3(x, y, z), + 'id': obj_id, + 'pick_color': pick_color, + 'display_color': display_color + } + self.objects.append(obj_info) + self.id_to_object[obj_id] = obj_info + + print(f"创建 {name} 在位置 ({x}, {y}, {z}), ID: {obj_id}, 拾取颜色: {pick_color}") + + def encodeIdToColor(self, obj_id): + """将对象ID编码为RGB颜色""" + # 将ID分解为RGB分量(24位) + r = (obj_id & 0xFF0000) >> 16 + g = (obj_id & 0x00FF00) >> 8 + b = (obj_id & 0x0000FF) + + # 转换为0-1范围的浮点数 + return Vec4(r / 255.0, g / 255.0, b / 255.0, 1.0) + + def decodeColorToId(self, color): + """将RGB颜色解码为对象ID""" + if color is None: + return 0 + + # 转换为0-255范围的整数 + r = int(color[0] * 255 + 0.5) + g = int(color[1] * 255 + 0.5) + b = int(color[2] * 255 + 0.5) + + # 组合为ID + obj_id = (r << 16) | (g << 8) | b + return obj_id + + def setupColorPickingBuffer(self): + """设置颜色拾取渲染缓冲区""" + # 创建离屏渲染缓冲区 + fbp = FrameBufferProperties() + fbp.setRgbColor(True) + fbp.setColorBits(8, 8, 8, 0) # RGB,不需要Alpha + fbp.setDepthBits(24) + + wp = WindowProperties() + wp.setSize(self.win.getXSize(), self.win.getYSize()) + + # 创建缓冲区 + self.pick_buffer = self.graphicsEngine.makeOutput( + self.pipe, "pick_buffer", -1, fbp, wp, + GraphicsOutput.M_offscreen, self.win.getGsg(), self.win + ) + + if self.pick_buffer is None: + print("无法创建颜色拾取缓冲区") + return + + # 创建拾取相机 + self.pick_cam = self.makeCamera(self.pick_buffer) + self.pick_cam.node().setLens(self.cam.node().getLens()) + self.pick_cam.setPos(self.cam.getPos()) + self.pick_cam.setHpr(self.cam.getHpr()) + + # 创建拾取场景(带有颜色编码的对象) + self.pick_scene = self.render.attachNewNode("pick_scene") + + # 为每个对象创建拾取版本 + for obj_info in self.objects: + # 复制几何体 + pick_node = obj_info['node'].copyTo(self.pick_scene) + pick_node.setColor(obj_info['pick_color']) + pick_node.setRenderModeWireframe(False) + pick_node.setLightOff() # 关闭光照以确保颜色准确 + + # 设置拾取相机只渲染拾取场景 + self.pick_cam.node().setCameraMask(BitMask32.allOn()) + + # 创建颜色纹理 + self.pick_tex = Texture() + self.pick_tex.setFormat(Texture.FRgb8) + self.pick_buffer.addRenderTexture(self.pick_tex, GraphicsOutput.RTMCopyRam) + + print("颜色拾取缓冲区设置完成") + + def getColorAtPixel(self, mouseX, mouseY): + """获取指定像素的颜色值""" + if self.pick_buffer is None: + return None + + # 获取窗口大小 + winWidth = self.pick_buffer.getXSize() + winHeight = self.pick_buffer.getYSize() + + # 确保坐标在窗口范围内 + if mouseX < 0 or mouseX >= winWidth or mouseY < 0 or mouseY >= winHeight: + return None + + # 强制渲染拾取缓冲区 + self.pick_buffer.setActive(True) + self.graphicsEngine.renderFrame() + + # 读取颜色纹理 + if self.pick_tex.hasRamImage(): + # 获取颜色图像 + pnm = PNMImage() + self.pick_tex.store(pnm) + + # Y坐标需要翻转(OpenGL坐标系) + flippedY = winHeight - 1 - mouseY + + if flippedY < pnm.getYSize() and mouseX < pnm.getXSize(): + # 获取颜色值 + r = pnm.getRed(mouseX, flippedY) + g = pnm.getGreen(mouseX, flippedY) + b = pnm.getBlue(mouseX, flippedY) + return (r, g, b) + + return None + + def pickObjectByColor(self, mouseX, mouseY): + """使用颜色编码选择对象""" + print(f"\n=== 颜色拾取: 鼠标位置 ({mouseX}, {mouseY}) ===") + + # 获取点击位置的颜色 + color = self.getColorAtPixel(mouseX, mouseY) + if color is None: + print("无法读取颜色值") + return None + + print(f"拾取颜色: RGB({color[0]:.3f}, {color[1]:.3f}, {color[2]:.3f})") + + # 解码颜色为对象ID + obj_id = self.decodeColorToId(color) + print(f"解码的对象ID: {obj_id}") + + # 查找对应的对象 + if obj_id in self.id_to_object: + obj_info = self.id_to_object[obj_id] + print(f"选中对象: {obj_info['name']}") + return obj_info + else: + print("没有找到对应的对象") + return None + + def testColorPicking(self): + """测试颜色拾取""" + print(f"\n=== 颜色拾取测试 ===") + + # 等待几帧以确保场景渲染完成 + for i in range(5): + self.graphicsEngine.renderFrame() + + # 模拟不同的点击位置 + test_clicks = [ + (619, 362, "屏幕中心"), + (400, 362, "屏幕偏左"), + (800, 362, "屏幕偏右"), + (619, 200, "屏幕上方"), + (619, 500, "屏幕下方"), + ] + + for mouseX, mouseY, description in test_clicks: + print(f"\n--- 测试 {description}: ({mouseX}, {mouseY}) ---") + selected = self.pickObjectByColor(mouseX, mouseY) + +if __name__ == "__main__": + app = ColorPickingTest() print("颜色拾取测试完成") \ No newline at end of file diff --git a/demo/color_test.py b/demo/color_test.py index d086323b..6f8b187f 100644 --- a/demo/color_test.py +++ b/demo/color_test.py @@ -1,87 +1,87 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -简单颜色测试程序 -""" - -from direct.showbase.ShowBase import ShowBase -from panda3d.core import LineSegs, Vec3, Vec4 -from direct.task import Task - -class ColorTest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 设置背景色 - self.setBackgroundColor(0.2, 0.2, 0.3) - - # 设置相机位置 - self.cam.setPos(0, -10, 0) - self.cam.lookAt(0, 0, 0) - - # 创建测试线段 - self.createTestLines() - - # 颜色变化任务 - self.taskMgr.add(self.colorChangeTask, "colorChange") - - self.time = 0 - - def createTestLines(self): - """创建测试线段""" - # 创建X轴线段 - x_lines = LineSegs() - x_lines.setThickness(5.0) - x_lines.moveTo(-3, 0, 0) - x_lines.drawTo(3, 0, 0) - - self.x_axis = self.render.attachNewNode(x_lines.create()) - self.x_axis.setColor(1, 0, 0, 1) # 红色 - self.x_axis.setLightOff() - - # 创建Y轴线段 - y_lines = LineSegs() - y_lines.setThickness(5.0) - y_lines.moveTo(0, -3, 0) - y_lines.drawTo(0, 3, 0) - - self.y_axis = self.render.attachNewNode(y_lines.create()) - self.y_axis.setColor(0, 1, 0, 1) # 绿色 - self.y_axis.setLightOff() - - # 创建Z轴线段 - z_lines = LineSegs() - z_lines.setThickness(5.0) - z_lines.moveTo(0, 0, -3) - z_lines.drawTo(0, 0, 3) - - self.z_axis = self.render.attachNewNode(z_lines.create()) - self.z_axis.setColor(0, 0, 1, 1) # 蓝色 - self.z_axis.setLightOff() - - print("测试轴创建完成") - print("X轴颜色:", self.x_axis.getColor()) - print("Y轴颜色:", self.y_axis.getColor()) - print("Z轴颜色:", self.z_axis.getColor()) - - def colorChangeTask(self, task): - """颜色变化任务""" - self.time += globalClock.getDt() - - # 每2秒变换一次颜色 - if int(self.time) % 2 == 0: - # 原色 - self.x_axis.setColor(1, 0, 0, 1) - self.y_axis.setColor(0, 1, 0, 1) - self.z_axis.setColor(0, 0, 1, 1) - else: - # 白色 - self.x_axis.setColor(1, 1, 1, 1) - self.y_axis.setColor(1, 1, 1, 1) - self.z_axis.setColor(1, 1, 1, 1) - - return task.cont - -if __name__ == "__main__": - app = ColorTest() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +简单颜色测试程序 +""" + +from direct.showbase.ShowBase import ShowBase +from panda3d.core import LineSegs, Vec3, Vec4 +from direct.task import Task + +class ColorTest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 设置背景色 + self.setBackgroundColor(0.2, 0.2, 0.3) + + # 设置相机位置 + self.cam.setPos(0, -10, 0) + self.cam.lookAt(0, 0, 0) + + # 创建测试线段 + self.createTestLines() + + # 颜色变化任务 + self.taskMgr.add(self.colorChangeTask, "colorChange") + + self.time = 0 + + def createTestLines(self): + """创建测试线段""" + # 创建X轴线段 + x_lines = LineSegs() + x_lines.setThickness(5.0) + x_lines.moveTo(-3, 0, 0) + x_lines.drawTo(3, 0, 0) + + self.x_axis = self.render.attachNewNode(x_lines.create()) + self.x_axis.setColor(1, 0, 0, 1) # 红色 + self.x_axis.setLightOff() + + # 创建Y轴线段 + y_lines = LineSegs() + y_lines.setThickness(5.0) + y_lines.moveTo(0, -3, 0) + y_lines.drawTo(0, 3, 0) + + self.y_axis = self.render.attachNewNode(y_lines.create()) + self.y_axis.setColor(0, 1, 0, 1) # 绿色 + self.y_axis.setLightOff() + + # 创建Z轴线段 + z_lines = LineSegs() + z_lines.setThickness(5.0) + z_lines.moveTo(0, 0, -3) + z_lines.drawTo(0, 0, 3) + + self.z_axis = self.render.attachNewNode(z_lines.create()) + self.z_axis.setColor(0, 0, 1, 1) # 蓝色 + self.z_axis.setLightOff() + + print("测试轴创建完成") + print("X轴颜色:", self.x_axis.getColor()) + print("Y轴颜色:", self.y_axis.getColor()) + print("Z轴颜色:", self.z_axis.getColor()) + + def colorChangeTask(self, task): + """颜色变化任务""" + self.time += globalClock.getDt() + + # 每2秒变换一次颜色 + if int(self.time) % 2 == 0: + # 原色 + self.x_axis.setColor(1, 0, 0, 1) + self.y_axis.setColor(0, 1, 0, 1) + self.z_axis.setColor(0, 0, 1, 1) + else: + # 白色 + self.x_axis.setColor(1, 1, 1, 1) + self.y_axis.setColor(1, 1, 1, 1) + self.z_axis.setColor(1, 1, 1, 1) + + return task.cont + +if __name__ == "__main__": + app = ColorTest() app.run() \ No newline at end of file diff --git a/demo/coordinate_test.py b/demo/coordinate_test.py index 80c8da45..e3f34a27 100644 --- a/demo/coordinate_test.py +++ b/demo/coordinate_test.py @@ -1,160 +1,160 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -def test_coordinate_conversion(): - """测试坐标转换""" - - print("=== 坐标转换测试 ===\n") - - # 假设窗口大小 - window_width = 800 - window_height = 600 - - print(f"窗口大小: {window_width} x {window_height}") - print("\n测试不同位置的坐标转换:") - - test_points = [ - # (屏幕坐标, 描述) - ((0, 0), "左上角"), - ((window_width/2, window_height/2), "中心点"), - ((window_width, window_height), "右下角"), - ((window_width/4, window_height/4), "左上象限"), - ((3*window_width/4, 3*window_height/4), "右下象限"), - ((419, 96), "用户点击的位置"), # 来自之前的日志 - ] - - for (screen_x, screen_y), description in test_points: - # 方法1:标准转换(我们当前使用的) - normalized_x1 = (2.0 * screen_x / window_width) - 1.0 - normalized_y1 = 1.0 - (2.0 * screen_y / window_height) - - # 方法2:简单转换 - normalized_x2 = (screen_x / window_width) * 2 - 1 - normalized_y2 = 1 - (screen_y / window_height) * 2 - - # 方法3:不反转Y轴 - normalized_x3 = (2.0 * screen_x / window_width) - 1.0 - normalized_y3 = (2.0 * screen_y / window_height) - 1.0 - - print(f"\n{description}: 屏幕坐标({screen_x:.0f}, {screen_y:.0f})") - print(f" 方法1 (当前): ({normalized_x1:.3f}, {normalized_y1:.3f})") - print(f" 方法2 (等价): ({normalized_x2:.3f}, {normalized_y2:.3f})") - print(f" 方法3 (不反转Y): ({normalized_x3:.3f}, {normalized_y3:.3f})") - - # 验证是否在有效范围内 - in_range1 = -1 <= normalized_x1 <= 1 and -1 <= normalized_y1 <= 1 - in_range3 = -1 <= normalized_x3 <= 1 and -1 <= normalized_y3 <= 1 - - print(f" 方法1 在范围内: {in_range1}") - print(f" 方法3 在范围内: {in_range3}") - -def test_qt_coordinate_precision(): - """测试Qt坐标精度问题""" - print("\n=== Qt坐标精度测试 ===") - - # 模拟真实的Qt widget大小(来自日志) - qt_width = 1238.0 - qt_height = 725.0 - - # 模拟真实的点击位置(来自日志) - click_x = 615 - click_y = 237 - - print(f"Qt Widget大小: {qt_width} x {qt_height}") - print(f"点击位置: ({click_x}, {click_y})") - - # 当前的转换方法 - normalized_x = (2.0 * click_x / qt_width) - 1.0 - normalized_y = 1.0 - (2.0 * click_y / qt_height) - - print(f"归一化坐标: ({normalized_x:.6f}, {normalized_y:.6f})") - - # 验证屏幕中心 - center_x = qt_width / 2 - center_y = qt_height / 2 - - center_norm_x = (2.0 * center_x / qt_width) - 1.0 - center_norm_y = 1.0 - (2.0 * center_y / qt_height) - - print(f"屏幕中心: ({center_x}, {center_y}) -> ({center_norm_x:.6f}, {center_norm_y:.6f})") - - # 分析点击位置相对于中心的偏移 - offset_from_center_x = click_x - center_x - offset_from_center_y = click_y - center_y - - print(f"相对于中心的偏移: ({offset_from_center_x:.1f}, {offset_from_center_y:.1f})") - - # 计算归一化偏移 - norm_offset_x = offset_from_center_x / (qt_width / 2) - norm_offset_y = -offset_from_center_y / (qt_height / 2) # Y轴反转 - - print(f"归一化偏移: ({norm_offset_x:.6f}, {norm_offset_y:.6f})") - - # 预期的归一化坐标(从中心偏移) - expected_norm_x = norm_offset_x - expected_norm_y = norm_offset_y - - print(f"预期归一化坐标: ({expected_norm_x:.6f}, {expected_norm_y:.6f})") - - # 比较 - diff_x = abs(normalized_x - expected_norm_x) - diff_y = abs(normalized_y - expected_norm_y) - - print(f"坐标差异: X={diff_x:.6f}, Y={diff_y:.6f}") - -def test_panda3d_coordinate_systems(): - """测试Panda3D的坐标系统""" - print("\n=== Panda3D坐标系统 ===") - print("屏幕坐标系统:") - print(" Qt: 左上角(0,0), 右下角(w,h), Y轴向下") - print(" Panda3D: 左下角(-1,-1), 右上角(1,1), Y轴向上") - print() - print("3D世界坐标系统:") - print(" X轴: 向右为正") - print(" Y轴: 向前为正(远离相机)") - print(" Z轴: 向上为正") - print() - print("相机坐标系统:") - print(" X轴: 向右为正") - print(" Y轴: 向前为正(相机朝向)") - print(" Z轴: 向上为正") - -def test_coordinate_alignment_issue(): - """测试坐标对齐问题""" - print("\n=== 坐标对齐问题分析 ===") - - qt_width = 1238.0 - qt_height = 725.0 - - print("测试不同位置的归一化坐标:") - - test_positions = [ - ("左边缘", 50, qt_height/2), - ("右边缘", qt_width-50, qt_height/2), - ("上边缘", qt_width/2, 50), - ("下边缘", qt_width/2, qt_height-50), - ("左上角", 100, 100), - ("右下角", qt_width-100, qt_height-100), - ("中心", qt_width/2, qt_height/2), - ("用户点击", 615, 237) - ] - - for name, x, y in test_positions: - # 当前方法 - norm_x = (2.0 * x / qt_width) - 1.0 - norm_y = 1.0 - (2.0 * y / qt_height) - - # 期望:点击边缘应该接近±1,点击中心应该接近0 - # 左边缘应该接近-1,右边缘应该接近+1 - # 上边缘应该接近+1,下边缘应该接近-1 - - expected_x = (x - qt_width/2) / (qt_width/2) - expected_y = (qt_height/2 - y) / (qt_height/2) - - print(f"{name:8s}: 实际({norm_x:6.3f}, {norm_y:6.3f}) 期望({expected_x:6.3f}, {expected_y:6.3f})") - -if __name__ == "__main__": - test_coordinate_conversion() - test_qt_coordinate_precision() - test_panda3d_coordinate_systems() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +def test_coordinate_conversion(): + """测试坐标转换""" + + print("=== 坐标转换测试 ===\n") + + # 假设窗口大小 + window_width = 800 + window_height = 600 + + print(f"窗口大小: {window_width} x {window_height}") + print("\n测试不同位置的坐标转换:") + + test_points = [ + # (屏幕坐标, 描述) + ((0, 0), "左上角"), + ((window_width/2, window_height/2), "中心点"), + ((window_width, window_height), "右下角"), + ((window_width/4, window_height/4), "左上象限"), + ((3*window_width/4, 3*window_height/4), "右下象限"), + ((419, 96), "用户点击的位置"), # 来自之前的日志 + ] + + for (screen_x, screen_y), description in test_points: + # 方法1:标准转换(我们当前使用的) + normalized_x1 = (2.0 * screen_x / window_width) - 1.0 + normalized_y1 = 1.0 - (2.0 * screen_y / window_height) + + # 方法2:简单转换 + normalized_x2 = (screen_x / window_width) * 2 - 1 + normalized_y2 = 1 - (screen_y / window_height) * 2 + + # 方法3:不反转Y轴 + normalized_x3 = (2.0 * screen_x / window_width) - 1.0 + normalized_y3 = (2.0 * screen_y / window_height) - 1.0 + + print(f"\n{description}: 屏幕坐标({screen_x:.0f}, {screen_y:.0f})") + print(f" 方法1 (当前): ({normalized_x1:.3f}, {normalized_y1:.3f})") + print(f" 方法2 (等价): ({normalized_x2:.3f}, {normalized_y2:.3f})") + print(f" 方法3 (不反转Y): ({normalized_x3:.3f}, {normalized_y3:.3f})") + + # 验证是否在有效范围内 + in_range1 = -1 <= normalized_x1 <= 1 and -1 <= normalized_y1 <= 1 + in_range3 = -1 <= normalized_x3 <= 1 and -1 <= normalized_y3 <= 1 + + print(f" 方法1 在范围内: {in_range1}") + print(f" 方法3 在范围内: {in_range3}") + +def test_qt_coordinate_precision(): + """测试Qt坐标精度问题""" + print("\n=== Qt坐标精度测试 ===") + + # 模拟真实的Qt widget大小(来自日志) + qt_width = 1238.0 + qt_height = 725.0 + + # 模拟真实的点击位置(来自日志) + click_x = 615 + click_y = 237 + + print(f"Qt Widget大小: {qt_width} x {qt_height}") + print(f"点击位置: ({click_x}, {click_y})") + + # 当前的转换方法 + normalized_x = (2.0 * click_x / qt_width) - 1.0 + normalized_y = 1.0 - (2.0 * click_y / qt_height) + + print(f"归一化坐标: ({normalized_x:.6f}, {normalized_y:.6f})") + + # 验证屏幕中心 + center_x = qt_width / 2 + center_y = qt_height / 2 + + center_norm_x = (2.0 * center_x / qt_width) - 1.0 + center_norm_y = 1.0 - (2.0 * center_y / qt_height) + + print(f"屏幕中心: ({center_x}, {center_y}) -> ({center_norm_x:.6f}, {center_norm_y:.6f})") + + # 分析点击位置相对于中心的偏移 + offset_from_center_x = click_x - center_x + offset_from_center_y = click_y - center_y + + print(f"相对于中心的偏移: ({offset_from_center_x:.1f}, {offset_from_center_y:.1f})") + + # 计算归一化偏移 + norm_offset_x = offset_from_center_x / (qt_width / 2) + norm_offset_y = -offset_from_center_y / (qt_height / 2) # Y轴反转 + + print(f"归一化偏移: ({norm_offset_x:.6f}, {norm_offset_y:.6f})") + + # 预期的归一化坐标(从中心偏移) + expected_norm_x = norm_offset_x + expected_norm_y = norm_offset_y + + print(f"预期归一化坐标: ({expected_norm_x:.6f}, {expected_norm_y:.6f})") + + # 比较 + diff_x = abs(normalized_x - expected_norm_x) + diff_y = abs(normalized_y - expected_norm_y) + + print(f"坐标差异: X={diff_x:.6f}, Y={diff_y:.6f}") + +def test_panda3d_coordinate_systems(): + """测试Panda3D的坐标系统""" + print("\n=== Panda3D坐标系统 ===") + print("屏幕坐标系统:") + print(" Qt: 左上角(0,0), 右下角(w,h), Y轴向下") + print(" Panda3D: 左下角(-1,-1), 右上角(1,1), Y轴向上") + print() + print("3D世界坐标系统:") + print(" X轴: 向右为正") + print(" Y轴: 向前为正(远离相机)") + print(" Z轴: 向上为正") + print() + print("相机坐标系统:") + print(" X轴: 向右为正") + print(" Y轴: 向前为正(相机朝向)") + print(" Z轴: 向上为正") + +def test_coordinate_alignment_issue(): + """测试坐标对齐问题""" + print("\n=== 坐标对齐问题分析 ===") + + qt_width = 1238.0 + qt_height = 725.0 + + print("测试不同位置的归一化坐标:") + + test_positions = [ + ("左边缘", 50, qt_height/2), + ("右边缘", qt_width-50, qt_height/2), + ("上边缘", qt_width/2, 50), + ("下边缘", qt_width/2, qt_height-50), + ("左上角", 100, 100), + ("右下角", qt_width-100, qt_height-100), + ("中心", qt_width/2, qt_height/2), + ("用户点击", 615, 237) + ] + + for name, x, y in test_positions: + # 当前方法 + norm_x = (2.0 * x / qt_width) - 1.0 + norm_y = 1.0 - (2.0 * y / qt_height) + + # 期望:点击边缘应该接近±1,点击中心应该接近0 + # 左边缘应该接近-1,右边缘应该接近+1 + # 上边缘应该接近+1,下边缘应该接近-1 + + expected_x = (x - qt_width/2) / (qt_width/2) + expected_y = (qt_height/2 - y) / (qt_height/2) + + print(f"{name:8s}: 实际({norm_x:6.3f}, {norm_y:6.3f}) 期望({expected_x:6.3f}, {expected_y:6.3f})") + +if __name__ == "__main__": + test_coordinate_conversion() + test_qt_coordinate_precision() + test_panda3d_coordinate_systems() test_coordinate_alignment_issue() \ No newline at end of file diff --git a/demo/depth_picking_test.py b/demo/depth_picking_test.py index 157069b8..f30503fc 100644 --- a/demo/depth_picking_test.py +++ b/demo/depth_picking_test.py @@ -1,197 +1,197 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import sys -from direct.showbase.ShowBase import ShowBase -from panda3d.core import ( - Point3, Vec3, CardMaker, Texture, GraphicsOutput, - FrameBufferProperties, WindowProperties, RenderState, - ColorWriteAttrib, DepthTestAttrib, ColorBlendAttrib, - BitMask32, PNMImage, Filename -) - -class DepthPickingTest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 相机设置 - self.cam.setPos(-3.87655, -188.38084, 82.602684) - self.cam.lookAt(0, 0, 0) - - print("=== 深度缓冲区点击检测测试 ===") - - # 创建测试对象 - self.createTestObjects() - - # 设置深度缓冲区读取 - self.setupDepthBuffer() - - # 测试深度检测 - self.testDepthPicking() - - def createTestObjects(self): - """创建测试对象""" - self.objects = [] - - # 创建几个测试立方体 - positions = [ - (0, 10, 3, "中心立方体"), - (-5, 10, 3, "左侧立方体"), - (5, 10, 3, "右侧立方体"), - (0, 10, 8, "上方立方体"), - (0, 15, 3, "远处立方体") - ] - - colors = [ - (1, 0, 0, 1), # 红色 - (0, 1, 0, 1), # 绿色 - (0, 0, 1, 1), # 蓝色 - (1, 1, 0, 1), # 黄色 - (1, 0, 1, 1), # 紫色 - ] - - for i, ((x, y, z, name), color) in enumerate(zip(positions, colors)): - # 创建立方体 - cm = CardMaker(f"cube_{i}") - cm.setFrame(-1, 1, -1, 1) - cube = self.render.attachNewNode(cm.generate()) - cube.setPos(x, y, z) - cube.setScale(2, 2, 2) - cube.setColor(*color) - cube.setBillboardAxis() - cube.setName(name) - - # 存储对象信息 - obj_info = { - 'node': cube, - 'name': name, - 'world_pos': Point3(x, y, z), - 'id': i + 1 # 用于颜色编码 - } - self.objects.append(obj_info) - - print(f"创建 {name} 在位置 ({x}, {y}, {z})") - - def setupDepthBuffer(self): - """设置深度缓冲区读取""" - # 获取主窗口的深度纹理 - self.depth_tex = Texture() - self.depth_tex.setFormat(Texture.FDepthComponent) - self.depth_tex.setComponentType(Texture.TFloat) - - # 将深度纹理绑定到主窗口 - self.win.addRenderTexture(self.depth_tex, GraphicsOutput.RTPDepth) - - print("深度缓冲区设置完成") - - def getDepthAtPixel(self, mouseX, mouseY): - """获取指定像素的深度值""" - # 获取窗口大小 - winWidth = self.win.getXSize() - winHeight = self.win.getYSize() - - # 确保坐标在窗口范围内 - if mouseX < 0 or mouseX >= winWidth or mouseY < 0 or mouseY >= winHeight: - return None - - # 强制渲染以更新深度缓冲区 - self.graphicsEngine.renderFrame() - - # 读取深度纹理 - if self.depth_tex.hasRamImage(): - # 获取深度图像 - pnm = PNMImage() - self.depth_tex.store(pnm) - - # Y坐标需要翻转(OpenGL坐标系) - flippedY = winHeight - 1 - mouseY - - if flippedY < pnm.getYSize() and mouseX < pnm.getXSize(): - # 获取深度值(0.0 = 近平面,1.0 = 远平面) - depth = pnm.getGray(mouseX, flippedY) - return depth - - return None - - def depthToWorldDistance(self, depth): - """将深度值转换为世界距离""" - if depth is None: - return None - - # 获取相机镜头参数 - lens = self.cam.node().getLens() - near = lens.getNear() - far = lens.getFar() - - # 将归一化深度值转换为线性距离 - # 这是透视投影的逆变换 - z_ndc = 2.0 * depth - 1.0 # 转换到 [-1, 1] 范围 - z_eye = 2.0 * near * far / (far + near - z_ndc * (far - near)) - - return abs(z_eye) - - def pickObjectByDepth(self, mouseX, mouseY): - """使用深度检测选择对象""" - print(f"\n=== 深度检测: 鼠标位置 ({mouseX}, {mouseY}) ===") - - # 获取点击位置的深度 - depth = self.getDepthAtPixel(mouseX, mouseY) - if depth is None: - print("无法读取深度值") - return None - - # 转换为世界距离 - worldDistance = self.depthToWorldDistance(depth) - print(f"深度值: {depth:.6f}, 世界距离: {worldDistance:.2f}") - - # 查找距离相机最近且在该深度附近的对象 - closest_obj = None - min_distance_diff = float('inf') - - cam_pos = self.cam.getPos() - - for obj_info in self.objects: - # 计算对象到相机的距离 - obj_distance = (obj_info['world_pos'] - cam_pos).length() - distance_diff = abs(obj_distance - worldDistance) - - print(f"{obj_info['name']}: 距离相机 {obj_distance:.2f}, 差值 {distance_diff:.2f}") - - # 使用一定的容差(因为对象有体积) - tolerance = 3.0 # 容差范围 - if distance_diff <= tolerance and distance_diff < min_distance_diff: - min_distance_diff = distance_diff - closest_obj = obj_info - print(f" -> 可能被选中 (差值: {distance_diff:.2f})") - - if closest_obj: - print(f"\n选中对象: {closest_obj['name']}") - return closest_obj - else: - print(f"\n没有找到匹配的对象") - return None - - def testDepthPicking(self): - """测试深度点击检测""" - print(f"\n=== 深度点击检测测试 ===") - - # 等待几帧以确保场景渲染完成 - for i in range(5): - self.graphicsEngine.renderFrame() - - # 模拟不同的点击位置 - test_clicks = [ - (619, 362, "屏幕中心"), - (400, 362, "屏幕偏左"), - (800, 362, "屏幕偏右"), - (619, 200, "屏幕上方"), - (619, 500, "屏幕下方"), - ] - - for mouseX, mouseY, description in test_clicks: - print(f"\n--- 测试 {description}: ({mouseX}, {mouseY}) ---") - selected = self.pickObjectByDepth(mouseX, mouseY) - -if __name__ == "__main__": - app = DepthPickingTest() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +from direct.showbase.ShowBase import ShowBase +from panda3d.core import ( + Point3, Vec3, CardMaker, Texture, GraphicsOutput, + FrameBufferProperties, WindowProperties, RenderState, + ColorWriteAttrib, DepthTestAttrib, ColorBlendAttrib, + BitMask32, PNMImage, Filename +) + +class DepthPickingTest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 相机设置 + self.cam.setPos(-3.87655, -188.38084, 82.602684) + self.cam.lookAt(0, 0, 0) + + print("=== 深度缓冲区点击检测测试 ===") + + # 创建测试对象 + self.createTestObjects() + + # 设置深度缓冲区读取 + self.setupDepthBuffer() + + # 测试深度检测 + self.testDepthPicking() + + def createTestObjects(self): + """创建测试对象""" + self.objects = [] + + # 创建几个测试立方体 + positions = [ + (0, 10, 3, "中心立方体"), + (-5, 10, 3, "左侧立方体"), + (5, 10, 3, "右侧立方体"), + (0, 10, 8, "上方立方体"), + (0, 15, 3, "远处立方体") + ] + + colors = [ + (1, 0, 0, 1), # 红色 + (0, 1, 0, 1), # 绿色 + (0, 0, 1, 1), # 蓝色 + (1, 1, 0, 1), # 黄色 + (1, 0, 1, 1), # 紫色 + ] + + for i, ((x, y, z, name), color) in enumerate(zip(positions, colors)): + # 创建立方体 + cm = CardMaker(f"cube_{i}") + cm.setFrame(-1, 1, -1, 1) + cube = self.render.attachNewNode(cm.generate()) + cube.setPos(x, y, z) + cube.setScale(2, 2, 2) + cube.setColor(*color) + cube.setBillboardAxis() + cube.setName(name) + + # 存储对象信息 + obj_info = { + 'node': cube, + 'name': name, + 'world_pos': Point3(x, y, z), + 'id': i + 1 # 用于颜色编码 + } + self.objects.append(obj_info) + + print(f"创建 {name} 在位置 ({x}, {y}, {z})") + + def setupDepthBuffer(self): + """设置深度缓冲区读取""" + # 获取主窗口的深度纹理 + self.depth_tex = Texture() + self.depth_tex.setFormat(Texture.FDepthComponent) + self.depth_tex.setComponentType(Texture.TFloat) + + # 将深度纹理绑定到主窗口 + self.win.addRenderTexture(self.depth_tex, GraphicsOutput.RTPDepth) + + print("深度缓冲区设置完成") + + def getDepthAtPixel(self, mouseX, mouseY): + """获取指定像素的深度值""" + # 获取窗口大小 + winWidth = self.win.getXSize() + winHeight = self.win.getYSize() + + # 确保坐标在窗口范围内 + if mouseX < 0 or mouseX >= winWidth or mouseY < 0 or mouseY >= winHeight: + return None + + # 强制渲染以更新深度缓冲区 + self.graphicsEngine.renderFrame() + + # 读取深度纹理 + if self.depth_tex.hasRamImage(): + # 获取深度图像 + pnm = PNMImage() + self.depth_tex.store(pnm) + + # Y坐标需要翻转(OpenGL坐标系) + flippedY = winHeight - 1 - mouseY + + if flippedY < pnm.getYSize() and mouseX < pnm.getXSize(): + # 获取深度值(0.0 = 近平面,1.0 = 远平面) + depth = pnm.getGray(mouseX, flippedY) + return depth + + return None + + def depthToWorldDistance(self, depth): + """将深度值转换为世界距离""" + if depth is None: + return None + + # 获取相机镜头参数 + lens = self.cam.node().getLens() + near = lens.getNear() + far = lens.getFar() + + # 将归一化深度值转换为线性距离 + # 这是透视投影的逆变换 + z_ndc = 2.0 * depth - 1.0 # 转换到 [-1, 1] 范围 + z_eye = 2.0 * near * far / (far + near - z_ndc * (far - near)) + + return abs(z_eye) + + def pickObjectByDepth(self, mouseX, mouseY): + """使用深度检测选择对象""" + print(f"\n=== 深度检测: 鼠标位置 ({mouseX}, {mouseY}) ===") + + # 获取点击位置的深度 + depth = self.getDepthAtPixel(mouseX, mouseY) + if depth is None: + print("无法读取深度值") + return None + + # 转换为世界距离 + worldDistance = self.depthToWorldDistance(depth) + print(f"深度值: {depth:.6f}, 世界距离: {worldDistance:.2f}") + + # 查找距离相机最近且在该深度附近的对象 + closest_obj = None + min_distance_diff = float('inf') + + cam_pos = self.cam.getPos() + + for obj_info in self.objects: + # 计算对象到相机的距离 + obj_distance = (obj_info['world_pos'] - cam_pos).length() + distance_diff = abs(obj_distance - worldDistance) + + print(f"{obj_info['name']}: 距离相机 {obj_distance:.2f}, 差值 {distance_diff:.2f}") + + # 使用一定的容差(因为对象有体积) + tolerance = 3.0 # 容差范围 + if distance_diff <= tolerance and distance_diff < min_distance_diff: + min_distance_diff = distance_diff + closest_obj = obj_info + print(f" -> 可能被选中 (差值: {distance_diff:.2f})") + + if closest_obj: + print(f"\n选中对象: {closest_obj['name']}") + return closest_obj + else: + print(f"\n没有找到匹配的对象") + return None + + def testDepthPicking(self): + """测试深度点击检测""" + print(f"\n=== 深度点击检测测试 ===") + + # 等待几帧以确保场景渲染完成 + for i in range(5): + self.graphicsEngine.renderFrame() + + # 模拟不同的点击位置 + test_clicks = [ + (619, 362, "屏幕中心"), + (400, 362, "屏幕偏左"), + (800, 362, "屏幕偏右"), + (619, 200, "屏幕上方"), + (619, 500, "屏幕下方"), + ] + + for mouseX, mouseY, description in test_clicks: + print(f"\n--- 测试 {description}: ({mouseX}, {mouseY}) ---") + selected = self.pickObjectByDepth(mouseX, mouseY) + +if __name__ == "__main__": + app = DepthPickingTest() print("深度检测测试完成") \ No newline at end of file diff --git a/demo/fbx_import_test.py b/demo/fbx_import_test.py index ca821984..39df926e 100644 --- a/demo/fbx_import_test.py +++ b/demo/fbx_import_test.py @@ -1,362 +1,362 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -FBX模型导入测试 - 演示新的缩放处理选项 - -修复内容: -1. 默认保持模型原有缩放结构 -2. 提供可选的单位转换功能 -3. 智能缩放标准化(处理子节点大缩放值) -4. 避免缩放层级混乱问题 - -使用说明: -1. 运行脚本启动3D编辑器 -2. 通过文件菜单或拖拽导入FBX模型 -3. 观察模型的缩放层级结构 -4. 按U键切换单位转换模式 -5. 按N键切换缩放标准化模式 -""" - -import sys -import os - -# 添加主目录到Python路径 -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - -from main import MyWorld -from ui.main_window import setup_main_window -from PyQt5.QtCore import Qt - - -def setup_fbx_import_demo(): - """设置FBX导入演示""" - - # 创建世界对象 - world = MyWorld() - - # 使用新的UI模块创建主窗口 - app, main_window = setup_main_window(world) - - # 设置窗口标题 - main_window.setWindowTitle("FBX导入测试 - 缩放层级修复") - - # 设置焦点策略 - main_window.setFocusPolicy(Qt.StrongFocus) - main_window.setFocus() - - # 导入选项 - unit_conversion_enabled = False - scale_normalization_enabled = True # 默认开启缩放标准化 - - def keyPressEvent(event): - nonlocal unit_conversion_enabled, scale_normalization_enabled - key = event.key() - - if key == Qt.Key_U: # U键切换单位转换模式 - unit_conversion_enabled = not unit_conversion_enabled - status = "开启" if unit_conversion_enabled else "关闭" - print(f"\n=== 单位转换模式已{status} ===") - print("下次导入FBX文件时将使用此设置") - event.accept() - return - elif key == Qt.Key_N: # N键切换缩放标准化模式 - scale_normalization_enabled = not scale_normalization_enabled - status = "开启" if scale_normalization_enabled else "关闭" - print(f"\n=== 缩放标准化模式已{status} ===") - print("下次导入FBX文件时将使用此设置") - event.accept() - return - elif key == Qt.Key_I: # I键显示导入信息 - print_import_info(world) - event.accept() - return - elif key == Qt.Key_R: # R键切换射线显示 - state = world.toggleRayDisplay() - status = "开启" if state else "关闭" - print(f"\n=== 射线显示已{status} ===") - event.accept() - return - elif key == Qt.Key_S: # S键显示当前设置 - print_current_settings(unit_conversion_enabled, scale_normalization_enabled) - event.accept() - return - - # 调用原始键盘事件处理 - if hasattr(main_window, '_original_keyPressEvent'): - main_window._original_keyPressEvent(event) - else: - event.ignore() - - # 覆盖importModel方法以使用当前的导入设置 - original_import = world.scene_manager.importModel - - def enhanced_import(filepath): - """增强的导入方法,使用当前导入设置""" - print(f"\n" + "="*60) - print(f"导入模型: {os.path.basename(filepath)}") - print(f"单位转换: {'开启' if unit_conversion_enabled else '关闭'}") - print(f"缩放标准化: {'开启' if scale_normalization_enabled else '关闭'}") - print("="*60) - - result = original_import( - filepath, - apply_unit_conversion=unit_conversion_enabled, - normalize_scales=scale_normalization_enabled - ) - - if result: - print("\n导入后的模型结构:") - print_model_structure(result, max_depth=3, world=world) # 限制显示深度 - - return result - - world.scene_manager.importModel = enhanced_import - - # 保存原始键盘事件处理器 - if hasattr(main_window, 'keyPressEvent'): - main_window._original_keyPressEvent = main_window.keyPressEvent - main_window.keyPressEvent = keyPressEvent - - # 添加自定义菜单 - add_custom_menus(main_window, world, unit_conversion_enabled, scale_normalization_enabled) - - # 输出使用说明 - print_usage_instructions() - - return app, main_window, world - - -def add_custom_menus(main_window, world, unit_conversion_enabled, scale_normalization_enabled): - """添加自定义菜单选项""" - # 添加FBX测试菜单 - fbx_menu = main_window.menuBar().addMenu('FBX测试') - - # 切换单位转换 - toggle_unit_action = fbx_menu.addAction('切换单位转换 (U)') - toggle_unit_action.triggered.connect(lambda: toggle_unit_conversion()) - - # 切换缩放标准化 - toggle_scale_action = fbx_menu.addAction('切换缩放标准化 (N)') - toggle_scale_action.triggered.connect(lambda: toggle_scale_normalization()) - - fbx_menu.addSeparator() - - # 显示当前设置 - settings_action = fbx_menu.addAction('显示当前设置 (S)') - settings_action.triggered.connect(lambda: print_current_settings(unit_conversion_enabled, scale_normalization_enabled)) - - # 显示导入信息 - info_action = fbx_menu.addAction('显示导入信息 (I)') - info_action.triggered.connect(lambda: print_import_info(world)) - - # 射线显示切换 - ray_action = fbx_menu.addAction('切换射线显示 (R)') - ray_action.triggered.connect(lambda: world.toggleRayDisplay()) - - fbx_menu.addSeparator() - - # 手动标准化当前模型 - normalize_action = fbx_menu.addAction('手动标准化当前模型缩放') - normalize_action.triggered.connect(lambda: manual_normalize_current_models(world)) - - # 重置所有模型缩放 - reset_action = fbx_menu.addAction('重置所有模型缩放为1.0') - reset_action.triggered.connect(lambda: reset_all_model_scales(world)) - - def toggle_unit_conversion(): - nonlocal unit_conversion_enabled - unit_conversion_enabled = not unit_conversion_enabled - status = "开启" if unit_conversion_enabled else "关闭" - print(f"\n=== 单位转换模式已{status} ===") - - def toggle_scale_normalization(): - nonlocal scale_normalization_enabled - scale_normalization_enabled = not scale_normalization_enabled - status = "开启" if scale_normalization_enabled else "关闭" - print(f"\n=== 缩放标准化模式已{status} ===") - - -def print_model_structure(model, depth=0, max_depth=5, world=None): - """打印模型的层级结构、缩放和位置信息""" - if depth > max_depth: - return - - indent = " " * depth - scale = model.getScale() - local_pos = model.getPos() - - # 如果有world引用,显示世界位置 - if world: - world_pos = model.getPos(world.render) - pos_info = f"本地{local_pos} / 世界{world_pos}" - else: - pos_info = f"{local_pos}" - - # 计算最大缩放分量 - max_scale = max(abs(scale.x), abs(scale.y), abs(scale.z)) - scale_status = "🔴大" if max_scale > 10 else "🟢正常" - - print(f"{indent}📦 {model.getName()}") - print(f"{indent} 缩放: {scale} {scale_status}") - print(f"{indent} 位置: {pos_info}") - print(f"{indent} 类型: {model.node().__class__.__name__}") - - # 递归打印重要子节点(限制数量避免输出过多) - child_count = model.getNumChildren() - max_children_to_show = 3 if depth == 0 else 2 - - for i in range(min(child_count, max_children_to_show)): - child = model.getChild(i) - print_model_structure(child, depth + 1, max_depth, world) - - if child_count > max_children_to_show: - print(f"{indent} ... 还有 {child_count - max_children_to_show} 个子节点") - - -def print_import_info(world): - """打印当前导入的模型信息""" - print("\n" + "="*60) - print("🔍 当前场景中的模型信息") - print("="*60) - - if not world.models: - print("❌ 场景中没有模型") - return - - for i, model in enumerate(world.models): - print(f"\n📦 模型 {i+1}: {model.getName()}") - print(f" 文件: {model.getTag('file') if model.hasTag('file') else '未知'}") - print(f" 单位转换: {'✅是' if model.hasTag('unit_conversion_applied') else '❌否'}") - print(f" 缩放标准化: {'✅是' if model.hasTag('scale_normalization_applied') else '❌否'}") - print(f" 根节点位置: {model.getPos()}") - print(f" 根节点缩放: {model.getScale()}") - print(f" 子节点数量: {model.getNumChildren()}") - - # 检查子节点的缩放情况 - large_scale_children = 0 - if model.getNumChildren() > 0: - print(" 📋 子节点缩放概况:") - for j in range(min(5, model.getNumChildren())): # 只检查前5个子节点 - child = model.getChild(j) - child_scale = child.getScale() - max_scale = max(abs(child_scale.x), abs(child_scale.y), abs(child_scale.z)) - - if max_scale > 10: - large_scale_children += 1 - status = "🔴大缩放" - else: - status = "🟢正常" - - print(f" {child.getName()}: {child_scale} {status}") - - if model.getNumChildren() > 5: - print(f" ... 还有 {model.getNumChildren() - 5} 个子节点") - - if large_scale_children > 0: - print(f" ⚠️ 发现 {large_scale_children} 个子节点有大缩放值") - - print("="*60) - - -def print_current_settings(unit_conversion_enabled, scale_normalization_enabled): - """显示当前导入设置""" - print("\n" + "="*40) - print("⚙️ 当前FBX导入设置") - print("="*40) - print(f"单位转换 (U键): {'🟢开启' if unit_conversion_enabled else '🔴关闭'}") - print(f"缩放标准化 (N键): {'🟢开启' if scale_normalization_enabled else '🔴关闭'}") - print("\n说明:") - print("• 单位转换: 将FBX的厘米单位转换为米") - print("• 缩放标准化: 自动处理子节点的大缩放值(如100)") - print("="*40) - - -def manual_normalize_current_models(world): - """手动标准化当前所有模型的缩放""" - print("\n🔧 手动标准化所有模型缩放...") - - if not world.models: - print("❌ 没有模型需要处理") - return - - for model in world.models: - print(f"\n处理模型: {model.getName()}") - world.scene_manager._normalizeModelScales(model) - - print("✅ 手动标准化完成") - - -def reset_all_model_scales(world): - """重置所有模型的缩放为1.0(调试用)""" - print("\n🔄 重置所有模型缩放为1.0...") - - count = 0 - for model in world.models: - # 递归重置所有节点 - reset_node_scale_recursive(model) - count += 1 - - print(f"✅ 已重置 {count} 个模型的所有节点缩放") - - -def reset_node_scale_recursive(node, depth=0): - """递归重置节点缩放""" - indent = " " * depth - node.setScale(1.0, 1.0, 1.0) - print(f"{indent}重置 {node.getName()}") - - for i in range(node.getNumChildren()): - child = node.getChild(i) - reset_node_scale_recursive(child, depth + 1) - - -def print_usage_instructions(): - """打印使用说明""" - print("\n" + "="*60) - print("🚀 FBX导入测试启动完成!") - print("="*60) - print("🎯 主要改进:") - print("✅ 智能缩放标准化 - 自动处理子节点大缩放值") - print("✅ 保持模型原有缩放结构") - print("✅ 避免根节点0.01 + 子节点100的复杂层级") - print("✅ 缩放时保持世界位置不变 - 修复位置偏移问题") - print("✅ 提供灵活的导入选项") - print("") - print("⌨️ 键盘快捷键:") - print("• U键 - 切换单位转换模式") - print("• N键 - 切换缩放标准化模式") - print("• S键 - 显示当前设置") - print("• I键 - 显示模型信息") - print("• R键 - 切换射线显示") - print("") - print("📁 导入方式:") - print("• 拖拽FBX文件到3D场景") - print("• 使用菜单 [文件] -> [导入模型]") - print("• 使用菜单 [FBX测试] 查看更多选项") - print("") - print("🎛️ 缩放处理模式:") - print("• 关闭单位转换 + 开启缩放标准化(推荐)") - print(" → 保持FBX结构,但标准化大缩放值") - print("• 开启单位转换 + 关闭缩放标准化") - print(" → 传统方式,应用0.01根缩放") - print("• 两者都关闭") - print(" → 完全保持原始FBX结构") - print("") - print("📊 当前设置:") - print("• 单位转换: 🔴关闭") - print("• 缩放标准化: 🟢开启 (推荐)") - print("="*60) - - -if __name__ == "__main__": - try: - app, main_window, world = setup_fbx_import_demo() - - # 启动应用程序 - sys.exit(app.exec_()) - - except Exception as e: - print(f"❌ 启动失败: {str(e)}") - import traceback +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +FBX模型导入测试 - 演示新的缩放处理选项 + +修复内容: +1. 默认保持模型原有缩放结构 +2. 提供可选的单位转换功能 +3. 智能缩放标准化(处理子节点大缩放值) +4. 避免缩放层级混乱问题 + +使用说明: +1. 运行脚本启动3D编辑器 +2. 通过文件菜单或拖拽导入FBX模型 +3. 观察模型的缩放层级结构 +4. 按U键切换单位转换模式 +5. 按N键切换缩放标准化模式 +""" + +import sys +import os + +# 添加主目录到Python路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from main import MyWorld +from ui.main_window import setup_main_window +from PyQt5.QtCore import Qt + + +def setup_fbx_import_demo(): + """设置FBX导入演示""" + + # 创建世界对象 + world = MyWorld() + + # 使用新的UI模块创建主窗口 + app, main_window = setup_main_window(world) + + # 设置窗口标题 + main_window.setWindowTitle("FBX导入测试 - 缩放层级修复") + + # 设置焦点策略 + main_window.setFocusPolicy(Qt.StrongFocus) + main_window.setFocus() + + # 导入选项 + unit_conversion_enabled = False + scale_normalization_enabled = True # 默认开启缩放标准化 + + def keyPressEvent(event): + nonlocal unit_conversion_enabled, scale_normalization_enabled + key = event.key() + + if key == Qt.Key_U: # U键切换单位转换模式 + unit_conversion_enabled = not unit_conversion_enabled + status = "开启" if unit_conversion_enabled else "关闭" + print(f"\n=== 单位转换模式已{status} ===") + print("下次导入FBX文件时将使用此设置") + event.accept() + return + elif key == Qt.Key_N: # N键切换缩放标准化模式 + scale_normalization_enabled = not scale_normalization_enabled + status = "开启" if scale_normalization_enabled else "关闭" + print(f"\n=== 缩放标准化模式已{status} ===") + print("下次导入FBX文件时将使用此设置") + event.accept() + return + elif key == Qt.Key_I: # I键显示导入信息 + print_import_info(world) + event.accept() + return + elif key == Qt.Key_R: # R键切换射线显示 + state = world.toggleRayDisplay() + status = "开启" if state else "关闭" + print(f"\n=== 射线显示已{status} ===") + event.accept() + return + elif key == Qt.Key_S: # S键显示当前设置 + print_current_settings(unit_conversion_enabled, scale_normalization_enabled) + event.accept() + return + + # 调用原始键盘事件处理 + if hasattr(main_window, '_original_keyPressEvent'): + main_window._original_keyPressEvent(event) + else: + event.ignore() + + # 覆盖importModel方法以使用当前的导入设置 + original_import = world.scene_manager.importModel + + def enhanced_import(filepath): + """增强的导入方法,使用当前导入设置""" + print(f"\n" + "="*60) + print(f"导入模型: {os.path.basename(filepath)}") + print(f"单位转换: {'开启' if unit_conversion_enabled else '关闭'}") + print(f"缩放标准化: {'开启' if scale_normalization_enabled else '关闭'}") + print("="*60) + + result = original_import( + filepath, + apply_unit_conversion=unit_conversion_enabled, + normalize_scales=scale_normalization_enabled + ) + + if result: + print("\n导入后的模型结构:") + print_model_structure(result, max_depth=3, world=world) # 限制显示深度 + + return result + + world.scene_manager.importModel = enhanced_import + + # 保存原始键盘事件处理器 + if hasattr(main_window, 'keyPressEvent'): + main_window._original_keyPressEvent = main_window.keyPressEvent + main_window.keyPressEvent = keyPressEvent + + # 添加自定义菜单 + add_custom_menus(main_window, world, unit_conversion_enabled, scale_normalization_enabled) + + # 输出使用说明 + print_usage_instructions() + + return app, main_window, world + + +def add_custom_menus(main_window, world, unit_conversion_enabled, scale_normalization_enabled): + """添加自定义菜单选项""" + # 添加FBX测试菜单 + fbx_menu = main_window.menuBar().addMenu('FBX测试') + + # 切换单位转换 + toggle_unit_action = fbx_menu.addAction('切换单位转换 (U)') + toggle_unit_action.triggered.connect(lambda: toggle_unit_conversion()) + + # 切换缩放标准化 + toggle_scale_action = fbx_menu.addAction('切换缩放标准化 (N)') + toggle_scale_action.triggered.connect(lambda: toggle_scale_normalization()) + + fbx_menu.addSeparator() + + # 显示当前设置 + settings_action = fbx_menu.addAction('显示当前设置 (S)') + settings_action.triggered.connect(lambda: print_current_settings(unit_conversion_enabled, scale_normalization_enabled)) + + # 显示导入信息 + info_action = fbx_menu.addAction('显示导入信息 (I)') + info_action.triggered.connect(lambda: print_import_info(world)) + + # 射线显示切换 + ray_action = fbx_menu.addAction('切换射线显示 (R)') + ray_action.triggered.connect(lambda: world.toggleRayDisplay()) + + fbx_menu.addSeparator() + + # 手动标准化当前模型 + normalize_action = fbx_menu.addAction('手动标准化当前模型缩放') + normalize_action.triggered.connect(lambda: manual_normalize_current_models(world)) + + # 重置所有模型缩放 + reset_action = fbx_menu.addAction('重置所有模型缩放为1.0') + reset_action.triggered.connect(lambda: reset_all_model_scales(world)) + + def toggle_unit_conversion(): + nonlocal unit_conversion_enabled + unit_conversion_enabled = not unit_conversion_enabled + status = "开启" if unit_conversion_enabled else "关闭" + print(f"\n=== 单位转换模式已{status} ===") + + def toggle_scale_normalization(): + nonlocal scale_normalization_enabled + scale_normalization_enabled = not scale_normalization_enabled + status = "开启" if scale_normalization_enabled else "关闭" + print(f"\n=== 缩放标准化模式已{status} ===") + + +def print_model_structure(model, depth=0, max_depth=5, world=None): + """打印模型的层级结构、缩放和位置信息""" + if depth > max_depth: + return + + indent = " " * depth + scale = model.getScale() + local_pos = model.getPos() + + # 如果有world引用,显示世界位置 + if world: + world_pos = model.getPos(world.render) + pos_info = f"本地{local_pos} / 世界{world_pos}" + else: + pos_info = f"{local_pos}" + + # 计算最大缩放分量 + max_scale = max(abs(scale.x), abs(scale.y), abs(scale.z)) + scale_status = "🔴大" if max_scale > 10 else "🟢正常" + + print(f"{indent}📦 {model.getName()}") + print(f"{indent} 缩放: {scale} {scale_status}") + print(f"{indent} 位置: {pos_info}") + print(f"{indent} 类型: {model.node().__class__.__name__}") + + # 递归打印重要子节点(限制数量避免输出过多) + child_count = model.getNumChildren() + max_children_to_show = 3 if depth == 0 else 2 + + for i in range(min(child_count, max_children_to_show)): + child = model.getChild(i) + print_model_structure(child, depth + 1, max_depth, world) + + if child_count > max_children_to_show: + print(f"{indent} ... 还有 {child_count - max_children_to_show} 个子节点") + + +def print_import_info(world): + """打印当前导入的模型信息""" + print("\n" + "="*60) + print("🔍 当前场景中的模型信息") + print("="*60) + + if not world.models: + print("❌ 场景中没有模型") + return + + for i, model in enumerate(world.models): + print(f"\n📦 模型 {i+1}: {model.getName()}") + print(f" 文件: {model.getTag('file') if model.hasTag('file') else '未知'}") + print(f" 单位转换: {'✅是' if model.hasTag('unit_conversion_applied') else '❌否'}") + print(f" 缩放标准化: {'✅是' if model.hasTag('scale_normalization_applied') else '❌否'}") + print(f" 根节点位置: {model.getPos()}") + print(f" 根节点缩放: {model.getScale()}") + print(f" 子节点数量: {model.getNumChildren()}") + + # 检查子节点的缩放情况 + large_scale_children = 0 + if model.getNumChildren() > 0: + print(" 📋 子节点缩放概况:") + for j in range(min(5, model.getNumChildren())): # 只检查前5个子节点 + child = model.getChild(j) + child_scale = child.getScale() + max_scale = max(abs(child_scale.x), abs(child_scale.y), abs(child_scale.z)) + + if max_scale > 10: + large_scale_children += 1 + status = "🔴大缩放" + else: + status = "🟢正常" + + print(f" {child.getName()}: {child_scale} {status}") + + if model.getNumChildren() > 5: + print(f" ... 还有 {model.getNumChildren() - 5} 个子节点") + + if large_scale_children > 0: + print(f" ⚠️ 发现 {large_scale_children} 个子节点有大缩放值") + + print("="*60) + + +def print_current_settings(unit_conversion_enabled, scale_normalization_enabled): + """显示当前导入设置""" + print("\n" + "="*40) + print("⚙️ 当前FBX导入设置") + print("="*40) + print(f"单位转换 (U键): {'🟢开启' if unit_conversion_enabled else '🔴关闭'}") + print(f"缩放标准化 (N键): {'🟢开启' if scale_normalization_enabled else '🔴关闭'}") + print("\n说明:") + print("• 单位转换: 将FBX的厘米单位转换为米") + print("• 缩放标准化: 自动处理子节点的大缩放值(如100)") + print("="*40) + + +def manual_normalize_current_models(world): + """手动标准化当前所有模型的缩放""" + print("\n🔧 手动标准化所有模型缩放...") + + if not world.models: + print("❌ 没有模型需要处理") + return + + for model in world.models: + print(f"\n处理模型: {model.getName()}") + world.scene_manager._normalizeModelScales(model) + + print("✅ 手动标准化完成") + + +def reset_all_model_scales(world): + """重置所有模型的缩放为1.0(调试用)""" + print("\n🔄 重置所有模型缩放为1.0...") + + count = 0 + for model in world.models: + # 递归重置所有节点 + reset_node_scale_recursive(model) + count += 1 + + print(f"✅ 已重置 {count} 个模型的所有节点缩放") + + +def reset_node_scale_recursive(node, depth=0): + """递归重置节点缩放""" + indent = " " * depth + node.setScale(1.0, 1.0, 1.0) + print(f"{indent}重置 {node.getName()}") + + for i in range(node.getNumChildren()): + child = node.getChild(i) + reset_node_scale_recursive(child, depth + 1) + + +def print_usage_instructions(): + """打印使用说明""" + print("\n" + "="*60) + print("🚀 FBX导入测试启动完成!") + print("="*60) + print("🎯 主要改进:") + print("✅ 智能缩放标准化 - 自动处理子节点大缩放值") + print("✅ 保持模型原有缩放结构") + print("✅ 避免根节点0.01 + 子节点100的复杂层级") + print("✅ 缩放时保持世界位置不变 - 修复位置偏移问题") + print("✅ 提供灵活的导入选项") + print("") + print("⌨️ 键盘快捷键:") + print("• U键 - 切换单位转换模式") + print("• N键 - 切换缩放标准化模式") + print("• S键 - 显示当前设置") + print("• I键 - 显示模型信息") + print("• R键 - 切换射线显示") + print("") + print("📁 导入方式:") + print("• 拖拽FBX文件到3D场景") + print("• 使用菜单 [文件] -> [导入模型]") + print("• 使用菜单 [FBX测试] 查看更多选项") + print("") + print("🎛️ 缩放处理模式:") + print("• 关闭单位转换 + 开启缩放标准化(推荐)") + print(" → 保持FBX结构,但标准化大缩放值") + print("• 开启单位转换 + 关闭缩放标准化") + print(" → 传统方式,应用0.01根缩放") + print("• 两者都关闭") + print(" → 完全保持原始FBX结构") + print("") + print("📊 当前设置:") + print("• 单位转换: 🔴关闭") + print("• 缩放标准化: 🟢开启 (推荐)") + print("="*60) + + +if __name__ == "__main__": + try: + app, main_window, world = setup_fbx_import_demo() + + # 启动应用程序 + sys.exit(app.exec_()) + + except Exception as e: + print(f"❌ 启动失败: {str(e)}") + import traceback traceback.print_exc() \ No newline at end of file diff --git a/demo/gui_3d_demo.py b/demo/gui_3d_demo.py index 2dc0b2d6..f05de72c 100644 --- a/demo/gui_3d_demo.py +++ b/demo/gui_3d_demo.py @@ -1,317 +1,317 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -PANDA3D 3D空间GUI演示 -展示如何在3D场景中使用GUI元素 -""" - -from direct.showbase.ShowBase import ShowBase -from direct.gui.DirectGui import * -from direct.gui.OnscreenText import OnscreenText -from direct.gui.OnscreenImage import OnscreenImage -from panda3d.core import * -from direct.task import Task - -class GUI3DDemo(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 设置背景色 - self.setBackgroundColor(0.1, 0.1, 0.3) - - # 设置相机位置 - self.cam.setPos(0, -10, 0) - self.cam.lookAt(0, 0, 0) - - # 尝试加载中文字体 - self.chinese_font = self.loadChineseFont() - - # 创建标题(2D界面) - self.title = OnscreenText( - text="PANDA3D 3D空间GUI演示", - pos=(0, 0.9), - scale=0.08, - fg=(1, 1, 1, 1), - align=TextNode.ACenter, - font=self.chinese_font if self.chinese_font else None - ) - - # 创建基本的2D GUI - self.create2DGUI() - - # 创建3D场景中的GUI元素 - self.create3DGUI() - - # 加载一个简单的3D模型作为背景 - self.createScene() - - print("3D GUI演示启动成功!") - print(f"中文字体加载状态: {'成功' if self.chinese_font else '失败,使用默认字体'}") - print("你可以看到2D界面元素和3D空间中的GUI") - - def loadChineseFont(self): - """尝试加载中文字体""" - import os - font_paths = [ - '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', - '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', - '/usr/share/fonts/truetype/arphic/ukai.ttc', - '/usr/share/fonts/truetype/arphic/uming.ttc', - ] - - for font_path in font_paths: - if os.path.exists(font_path): - try: - font = self.loader.loadFont(font_path) - if font: - return font - except: - continue - return None - - def create2DGUI(self): - """创建传统的2D GUI元素""" - # 控制面板标题 - self.controlTitle = OnscreenText( - text="控制面板", - pos=(-1.2, 0.8), - scale=0.06, - fg=(1, 1, 0, 1), - align=TextNode.ALeft, - font=self.chinese_font if self.chinese_font else None - ) - - # 旋转控制按钮 - self.rotateButton = DirectButton( - text="旋转场景", - pos=(-1.2, 0, 0.6), - scale=0.05, - command=self.toggleRotation, - frameColor=(0.2, 0.6, 0.8, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - # 颜色变化按钮 - self.colorButton = DirectButton( - text="变换颜色", - pos=(-1.2, 0, 0.4), - scale=0.05, - command=self.changeColors, - frameColor=(0.8, 0.2, 0.6, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - # 重置按钮 - self.resetButton = DirectButton( - text="重置", - pos=(-1.2, 0, 0.2), - scale=0.05, - command=self.resetScene, - frameColor=(0.6, 0.8, 0.2, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - # 状态显示 - self.statusText = OnscreenText( - text="状态: 就绪", - pos=(-1.2, -0.8), - scale=0.04, - fg=(0, 1, 0, 1), - align=TextNode.ALeft, - font=self.chinese_font if self.chinese_font else None - ) - - # 退出按钮 - self.exitButton = DirectButton( - text="退出", - pos=(1.2, 0, -0.8), - scale=0.05, - command=self.exitDemo, - frameColor=(0.8, 0.2, 0.2, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - def create3DGUI(self): - """创建3D空间中的GUI元素""" - # 创建一个在3D空间中的按钮 - # 这实际上是一个2D GUI元素,但我们可以将其父级设置为3D节点 - - # 创建3D空间中的文本 - self.text3D = TextNode('3d-text') - self.text3D.setText("3D空间中的文本") - self.text3D.setAlign(TextNode.ACenter) - if self.chinese_font: - self.text3D.setFont(self.chinese_font) - self.textNodePath = self.render.attachNewNode(self.text3D) - self.textNodePath.setPos(0, 5, 2) - self.textNodePath.setScale(0.5) - self.textNodePath.setColor(1, 1, 0, 1) - self.textNodePath.setBillboardAxis() # 让文本总是面向相机 - - # 创建另一个3D文本,显示说明 - self.info3D = TextNode('info-text') - self.info3D.setText("这是3D空间中的GUI元素\n它们可以与3D场景交互") - self.info3D.setAlign(TextNode.ACenter) - if self.chinese_font: - self.info3D.setFont(self.chinese_font) - self.infoNodePath = self.render.attachNewNode(self.info3D) - self.infoNodePath.setPos(0, 3, -1) - self.infoNodePath.setScale(0.3) - self.infoNodePath.setColor(0, 1, 1, 1) - self.infoNodePath.setBillboardAxis() - - # 创建一个3D平面作为"虚拟屏幕" - self.createVirtualScreen() - - def createVirtualScreen(self): - """创建一个3D空间中的虚拟屏幕""" - # 创建一个卡片(平面) - cm = CardMaker("virtual-screen") - cm.setFrame(-2, 2, -1, 1) - self.virtualScreen = self.render.attachNewNode(cm.generate()) - self.virtualScreen.setPos(3, 5, 0) - self.virtualScreen.setColor(0.2, 0.2, 0.2, 0.8) - self.virtualScreen.setTransparency(TransparencyAttrib.MAlpha) - - # 在虚拟屏幕上添加文本 - screenText = TextNode('screen-text') - screenText.setText("虚拟3D屏幕\n\n这里可以显示\n任何信息") - screenText.setAlign(TextNode.ACenter) - if self.chinese_font: - screenText.setFont(self.chinese_font) - screenTextNP = self.virtualScreen.attachNewNode(screenText) - screenTextNP.setPos(0, 0.01, 0) # 稍微偏移,避免z-fighting - screenTextNP.setScale(0.3) - screenTextNP.setColor(0, 1, 0, 1) - - def createScene(self): - """创建3D场景""" - # 创建一个简单的立方体 - from panda3d.core import CardMaker - - # 创建地面 - cm = CardMaker("ground") - cm.setFrame(-10, 10, -10, 10) - ground = self.render.attachNewNode(cm.generate()) - ground.setP(-90) # 旋转成水平面 - ground.setColor(0.5, 0.5, 0.5, 1) - - # 创建一些立方体 - self.cubes = [] - for i in range(3): - for j in range(3): - # 使用内置的立方体几何 - cube = self.loader.loadModel("models/environment") - if not cube: - # 如果找不到模型,创建一个简单的几何体 - cube = self.render.attachNewNode("cube") - - cube.reparentTo(self.render) - cube.setPos(-2 + i*2, 2 + j*2, 0.5) - cube.setScale(0.5) - cube.setColor( - 0.2 + i*0.3, - 0.2 + j*0.3, - 0.5, - 1 - ) - self.cubes.append(cube) - - # 添加光照 - dlight = DirectionalLight('dlight') - dlight.setColor((1, 1, 1, 1)) - dlnp = self.render.attachNewNode(dlight) - dlnp.setHpr(45, -45, 0) - self.render.setLight(dlnp) - - alight = AmbientLight('alight') - alight.setColor((0.3, 0.3, 0.3, 1)) - alnp = self.render.attachNewNode(alight) - self.render.setLight(alnp) - - # 初始化旋转状态 - self.rotating = False - self.colorIndex = 0 - - def toggleRotation(self): - """切换场景旋转""" - if self.rotating: - self.taskMgr.remove("rotate-scene") - self.rotating = False - self.rotateButton['text'] = "旋转场景" - self.statusText.setText("状态: 停止旋转") - else: - self.taskMgr.add(self.rotateSceneTask, "rotate-scene") - self.rotating = True - self.rotateButton['text'] = "停止旋转" - self.statusText.setText("状态: 正在旋转") - - print(f"旋转状态: {'开启' if self.rotating else '关闭'}") - - def rotateSceneTask(self, task): - """旋转场景的任务""" - for cube in self.cubes: - cube.setH(cube.getH() + 50 * globalClock.getDt()) - - # 旋转3D文本 - self.textNodePath.setH(self.textNodePath.getH() + 30 * globalClock.getDt()) - self.virtualScreen.setH(self.virtualScreen.getH() + 20 * globalClock.getDt()) - - return task.cont - - def changeColors(self): - """改变场景颜色""" - self.colorIndex = (self.colorIndex + 1) % 3 - - colors = [ - [(1, 0.5, 0.5), (0.5, 1, 0.5), (0.5, 0.5, 1)], # 红绿蓝 - [(1, 1, 0.5), (1, 0.5, 1), (0.5, 1, 1)], # 黄洋红青 - [(0.8, 0.2, 0.8), (0.2, 0.8, 0.8), (0.8, 0.8, 0.2)] # 紫青黄 - ] - - colorSet = colors[self.colorIndex] - - for i, cube in enumerate(self.cubes): - color = colorSet[i % 3] - cube.setColor(*color, 1) - - # 改变3D文本颜色 - textColors = [(1, 1, 0), (0, 1, 1), (1, 0, 1)] - self.textNodePath.setColor(*textColors[self.colorIndex], 1) - - self.statusText.setText(f"状态: 颜色方案 {self.colorIndex + 1}") - print(f"切换到颜色方案 {self.colorIndex + 1}") - - def resetScene(self): - """重置场景""" - # 停止旋转 - if self.rotating: - self.toggleRotation() - - # 重置立方体位置和颜色 - for i, cube in enumerate(self.cubes): - row = i // 3 - col = i % 3 - cube.setPos(-2 + col*2, 2 + row*2, 0.5) - cube.setHpr(0, 0, 0) - cube.setColor(0.2 + col*0.3, 0.2 + row*0.3, 0.5, 1) - - # 重置3D文本 - self.textNodePath.setHpr(0, 0, 0) - self.textNodePath.setColor(1, 1, 0, 1) - self.virtualScreen.setHpr(0, 0, 0) - - self.colorIndex = 0 - self.statusText.setText("状态: 场景已重置") - print("场景已重置") - - def exitDemo(self): - """退出演示""" - print("退出3D GUI演示") - self.userExit() - -if __name__ == "__main__": - print("启动PANDA3D 3D GUI演示...") - demo = GUI3DDemo() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +PANDA3D 3D空间GUI演示 +展示如何在3D场景中使用GUI元素 +""" + +from direct.showbase.ShowBase import ShowBase +from direct.gui.DirectGui import * +from direct.gui.OnscreenText import OnscreenText +from direct.gui.OnscreenImage import OnscreenImage +from panda3d.core import * +from direct.task import Task + +class GUI3DDemo(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 设置背景色 + self.setBackgroundColor(0.1, 0.1, 0.3) + + # 设置相机位置 + self.cam.setPos(0, -10, 0) + self.cam.lookAt(0, 0, 0) + + # 尝试加载中文字体 + self.chinese_font = self.loadChineseFont() + + # 创建标题(2D界面) + self.title = OnscreenText( + text="PANDA3D 3D空间GUI演示", + pos=(0, 0.9), + scale=0.08, + fg=(1, 1, 1, 1), + align=TextNode.ACenter, + font=self.chinese_font if self.chinese_font else None + ) + + # 创建基本的2D GUI + self.create2DGUI() + + # 创建3D场景中的GUI元素 + self.create3DGUI() + + # 加载一个简单的3D模型作为背景 + self.createScene() + + print("3D GUI演示启动成功!") + print(f"中文字体加载状态: {'成功' if self.chinese_font else '失败,使用默认字体'}") + print("你可以看到2D界面元素和3D空间中的GUI") + + def loadChineseFont(self): + """尝试加载中文字体""" + import os + font_paths = [ + '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', + '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', + '/usr/share/fonts/truetype/arphic/ukai.ttc', + '/usr/share/fonts/truetype/arphic/uming.ttc', + ] + + for font_path in font_paths: + if os.path.exists(font_path): + try: + font = self.loader.loadFont(font_path) + if font: + return font + except: + continue + return None + + def create2DGUI(self): + """创建传统的2D GUI元素""" + # 控制面板标题 + self.controlTitle = OnscreenText( + text="控制面板", + pos=(-1.2, 0.8), + scale=0.06, + fg=(1, 1, 0, 1), + align=TextNode.ALeft, + font=self.chinese_font if self.chinese_font else None + ) + + # 旋转控制按钮 + self.rotateButton = DirectButton( + text="旋转场景", + pos=(-1.2, 0, 0.6), + scale=0.05, + command=self.toggleRotation, + frameColor=(0.2, 0.6, 0.8, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + # 颜色变化按钮 + self.colorButton = DirectButton( + text="变换颜色", + pos=(-1.2, 0, 0.4), + scale=0.05, + command=self.changeColors, + frameColor=(0.8, 0.2, 0.6, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + # 重置按钮 + self.resetButton = DirectButton( + text="重置", + pos=(-1.2, 0, 0.2), + scale=0.05, + command=self.resetScene, + frameColor=(0.6, 0.8, 0.2, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + # 状态显示 + self.statusText = OnscreenText( + text="状态: 就绪", + pos=(-1.2, -0.8), + scale=0.04, + fg=(0, 1, 0, 1), + align=TextNode.ALeft, + font=self.chinese_font if self.chinese_font else None + ) + + # 退出按钮 + self.exitButton = DirectButton( + text="退出", + pos=(1.2, 0, -0.8), + scale=0.05, + command=self.exitDemo, + frameColor=(0.8, 0.2, 0.2, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + def create3DGUI(self): + """创建3D空间中的GUI元素""" + # 创建一个在3D空间中的按钮 + # 这实际上是一个2D GUI元素,但我们可以将其父级设置为3D节点 + + # 创建3D空间中的文本 + self.text3D = TextNode('3d-text') + self.text3D.setText("3D空间中的文本") + self.text3D.setAlign(TextNode.ACenter) + if self.chinese_font: + self.text3D.setFont(self.chinese_font) + self.textNodePath = self.render.attachNewNode(self.text3D) + self.textNodePath.setPos(0, 5, 2) + self.textNodePath.setScale(0.5) + self.textNodePath.setColor(1, 1, 0, 1) + self.textNodePath.setBillboardAxis() # 让文本总是面向相机 + + # 创建另一个3D文本,显示说明 + self.info3D = TextNode('info-text') + self.info3D.setText("这是3D空间中的GUI元素\n它们可以与3D场景交互") + self.info3D.setAlign(TextNode.ACenter) + if self.chinese_font: + self.info3D.setFont(self.chinese_font) + self.infoNodePath = self.render.attachNewNode(self.info3D) + self.infoNodePath.setPos(0, 3, -1) + self.infoNodePath.setScale(0.3) + self.infoNodePath.setColor(0, 1, 1, 1) + self.infoNodePath.setBillboardAxis() + + # 创建一个3D平面作为"虚拟屏幕" + self.createVirtualScreen() + + def createVirtualScreen(self): + """创建一个3D空间中的虚拟屏幕""" + # 创建一个卡片(平面) + cm = CardMaker("virtual-screen") + cm.setFrame(-2, 2, -1, 1) + self.virtualScreen = self.render.attachNewNode(cm.generate()) + self.virtualScreen.setPos(3, 5, 0) + self.virtualScreen.setColor(0.2, 0.2, 0.2, 0.8) + self.virtualScreen.setTransparency(TransparencyAttrib.MAlpha) + + # 在虚拟屏幕上添加文本 + screenText = TextNode('screen-text') + screenText.setText("虚拟3D屏幕\n\n这里可以显示\n任何信息") + screenText.setAlign(TextNode.ACenter) + if self.chinese_font: + screenText.setFont(self.chinese_font) + screenTextNP = self.virtualScreen.attachNewNode(screenText) + screenTextNP.setPos(0, 0.01, 0) # 稍微偏移,避免z-fighting + screenTextNP.setScale(0.3) + screenTextNP.setColor(0, 1, 0, 1) + + def createScene(self): + """创建3D场景""" + # 创建一个简单的立方体 + from panda3d.core import CardMaker + + # 创建地面 + cm = CardMaker("ground") + cm.setFrame(-10, 10, -10, 10) + ground = self.render.attachNewNode(cm.generate()) + ground.setP(-90) # 旋转成水平面 + ground.setColor(0.5, 0.5, 0.5, 1) + + # 创建一些立方体 + self.cubes = [] + for i in range(3): + for j in range(3): + # 使用内置的立方体几何 + cube = self.loader.loadModel("models/environment") + if not cube: + # 如果找不到模型,创建一个简单的几何体 + cube = self.render.attachNewNode("cube") + + cube.reparentTo(self.render) + cube.setPos(-2 + i*2, 2 + j*2, 0.5) + cube.setScale(0.5) + cube.setColor( + 0.2 + i*0.3, + 0.2 + j*0.3, + 0.5, + 1 + ) + self.cubes.append(cube) + + # 添加光照 + dlight = DirectionalLight('dlight') + dlight.setColor((1, 1, 1, 1)) + dlnp = self.render.attachNewNode(dlight) + dlnp.setHpr(45, -45, 0) + self.render.setLight(dlnp) + + alight = AmbientLight('alight') + alight.setColor((0.3, 0.3, 0.3, 1)) + alnp = self.render.attachNewNode(alight) + self.render.setLight(alnp) + + # 初始化旋转状态 + self.rotating = False + self.colorIndex = 0 + + def toggleRotation(self): + """切换场景旋转""" + if self.rotating: + self.taskMgr.remove("rotate-scene") + self.rotating = False + self.rotateButton['text'] = "旋转场景" + self.statusText.setText("状态: 停止旋转") + else: + self.taskMgr.add(self.rotateSceneTask, "rotate-scene") + self.rotating = True + self.rotateButton['text'] = "停止旋转" + self.statusText.setText("状态: 正在旋转") + + print(f"旋转状态: {'开启' if self.rotating else '关闭'}") + + def rotateSceneTask(self, task): + """旋转场景的任务""" + for cube in self.cubes: + cube.setH(cube.getH() + 50 * globalClock.getDt()) + + # 旋转3D文本 + self.textNodePath.setH(self.textNodePath.getH() + 30 * globalClock.getDt()) + self.virtualScreen.setH(self.virtualScreen.getH() + 20 * globalClock.getDt()) + + return task.cont + + def changeColors(self): + """改变场景颜色""" + self.colorIndex = (self.colorIndex + 1) % 3 + + colors = [ + [(1, 0.5, 0.5), (0.5, 1, 0.5), (0.5, 0.5, 1)], # 红绿蓝 + [(1, 1, 0.5), (1, 0.5, 1), (0.5, 1, 1)], # 黄洋红青 + [(0.8, 0.2, 0.8), (0.2, 0.8, 0.8), (0.8, 0.8, 0.2)] # 紫青黄 + ] + + colorSet = colors[self.colorIndex] + + for i, cube in enumerate(self.cubes): + color = colorSet[i % 3] + cube.setColor(*color, 1) + + # 改变3D文本颜色 + textColors = [(1, 1, 0), (0, 1, 1), (1, 0, 1)] + self.textNodePath.setColor(*textColors[self.colorIndex], 1) + + self.statusText.setText(f"状态: 颜色方案 {self.colorIndex + 1}") + print(f"切换到颜色方案 {self.colorIndex + 1}") + + def resetScene(self): + """重置场景""" + # 停止旋转 + if self.rotating: + self.toggleRotation() + + # 重置立方体位置和颜色 + for i, cube in enumerate(self.cubes): + row = i // 3 + col = i % 3 + cube.setPos(-2 + col*2, 2 + row*2, 0.5) + cube.setHpr(0, 0, 0) + cube.setColor(0.2 + col*0.3, 0.2 + row*0.3, 0.5, 1) + + # 重置3D文本 + self.textNodePath.setHpr(0, 0, 0) + self.textNodePath.setColor(1, 1, 0, 1) + self.virtualScreen.setHpr(0, 0, 0) + + self.colorIndex = 0 + self.statusText.setText("状态: 场景已重置") + print("场景已重置") + + def exitDemo(self): + """退出演示""" + print("退出3D GUI演示") + self.userExit() + +if __name__ == "__main__": + print("启动PANDA3D 3D GUI演示...") + demo = GUI3DDemo() demo.run() \ No newline at end of file diff --git a/demo/gui_editor_window.py b/demo/gui_editor_window.py index 769d67c8..22eeb52c 100644 --- a/demo/gui_editor_window.py +++ b/demo/gui_editor_window.py @@ -1,1008 +1,1008 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -独立的GUI编辑器窗口 -当主程序进入GUI编辑模式时,弹出此窗口进行GUI编辑操作 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -import os -from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton, - QLabel, QGroupBox, QGridLayout, QLineEdit, - QDoubleSpinBox, QSpinBox, QTextEdit, QComboBox, - QCheckBox, QSlider, QColorDialog, QFrame, - QSplitter, QTreeWidget, QTreeWidgetItem, - QFormLayout, QScrollArea, QMessageBox) -from PyQt5.QtCore import Qt, pyqtSignal, QTimer -from PyQt5.QtGui import QFont, QColor, QPalette - -# 导入Panda3D相关模块 -from direct.showbase.ShowBase import ShowBase -from direct.gui.DirectGui import * -from panda3d.core import * - -class GUIPreviewWindow: - """独立的Panda3D GUI预览窗口""" - def __init__(self): - self.preview_base = None - self.chinese_font = None - self.gui_elements = [] - self.welcome_text = None - self.tip_text = None - self.setupPanda3D() - - def setupPanda3D(self): - """设置独立的Panda3D预览窗口""" - try: - # 配置Panda3D设置 - 创建独立窗口 - loadPrcFileData("", """ - win-size 600 400 - window-title GUI Preview - 独立预览窗口 - framebuffer-multisample 1 - multisamples 2 - audio-library-name null - notify-level-audio error - show-frame-rate-meter 0 - want-pstats 0 - """) - - # 创建ShowBase实例 - self.preview_base = ShowBase() - - # 设置背景色 - self.preview_base.setBackgroundColor(0.2, 0.2, 0.3) - - # 调整相机位置(主要用于查看2D GUI) - self.preview_base.cam.setPos(0, -10, 0) - self.preview_base.cam.lookAt(0, 0, 0) - - # 尝试加载中文字体 - try: - self.chinese_font = self.preview_base.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') - if not self.chinese_font: - print("预览窗口: 无法加载中文字体,将使用默认字体") - except: - print("预览窗口: 无法加载中文字体,将使用默认字体") - self.chinese_font = None - - # 创建说明文本 - self.createInfoText() - - print("✓ 独立GUI预览窗口已创建") - print("这是一个独立的Panda3D窗口,可以正确显示2D GUI元素") - - except Exception as e: - print(f"创建独立预览窗口失败: {str(e)}") - - def createInfoText(self): - """创建说明文本""" - # 创建欢迎信息 - self.welcome_text = OnscreenText( - text="GUI预览窗口\n\n这是独立的Panda3D窗口\n在GUI编辑器中创建的元素\n会在这里正确显示\n\n特别是2D GUI元素", - pos=(0, 0.2), - scale=0.08, - fg=(1, 1, 1, 1), - align=TextNode.ACenter, - font=self.chinese_font if self.chinese_font else None - ) - - # 创建提示信息 - self.tip_text = OnscreenText( - text="提示: 这个窗口独立于Qt,可以正确渲染DirectGUI元素", - pos=(0, -0.7), - scale=0.05, - fg=(0.8, 0.8, 0.8, 1), - align=TextNode.ACenter, - font=self.chinese_font if self.chinese_font else None - ) - - # 创建操作说明 - self.control_text = OnscreenText( - text="操作说明:\n• 鼠标滚轮: 缩放视图\n• 右键拖拽: 旋转相机", - pos=(-0.9, -0.3), - scale=0.04, - fg=(0.7, 0.7, 1, 1), - align=TextNode.ALeft, - font=self.chinese_font if self.chinese_font else None - ) - - # 绑定鼠标控制 - self.preview_base.accept("wheel_up", self.zoomIn) - self.preview_base.accept("wheel_down", self.zoomOut) - - def zoomIn(self): - """放大视图""" - pos = self.preview_base.cam.getPos() - forward = self.preview_base.cam.getQuat().getForward() - self.preview_base.cam.setPos(pos + forward * 2) - - def zoomOut(self): - """缩小视图""" - pos = self.preview_base.cam.getPos() - forward = self.preview_base.cam.getQuat().getForward() - self.preview_base.cam.setPos(pos - forward * 2) - - def clearGUIElements(self): - """清空所有GUI元素""" - # 移除所有已创建的GUI元素 - for gui_element in self.gui_elements: - try: - if hasattr(gui_element, 'removeNode'): - gui_element.removeNode() - elif hasattr(gui_element, 'destroy'): - gui_element.destroy() - except: - pass - - self.gui_elements.clear() - - # 恢复说明文本 - if not self.welcome_text: - self.createInfoText() - - def updatePreview(self, gui_elements_info): - """更新预览显示""" - try: - # 清除说明文本(如果存在GUI元素) - if gui_elements_info and self.welcome_text: - self.welcome_text.removeNode() - self.tip_text.removeNode() - self.control_text.removeNode() - self.welcome_text = None - self.tip_text = None - self.control_text = None - elif not gui_elements_info and not self.welcome_text: - # 如果没有GUI元素了,重新创建说明文本 - self.createInfoText() - - except Exception as e: - print(f"更新预览失败: {str(e)}") - - def createElementInPreview(self, gui_type, params): - """在预览窗口中创建GUI元素""" - try: - if not self.preview_base: - return None - - element = None - - if gui_type == "button": - # 创建按钮 - 使用适合预览的坐标 - gui_pos = (params['pos'][0] * 0.08, 0, params['pos'][2] * 0.08) - element = DirectButton( - text=params['text'], - pos=gui_pos, - scale=params['scale'], - frameColor=(0.2, 0.6, 0.8, 1), - text_font=self.chinese_font if self.chinese_font else None, - command=lambda: print(f"预览按钮点击: {params['text']}") - ) - print(f"预览窗口创建按钮: {params['text']} at {gui_pos}") - - elif gui_type == "label": - # 创建标签 - gui_pos = (params['pos'][0] * 0.08, 0, params['pos'][2] * 0.08) - element = DirectLabel( - text=params['text'], - pos=gui_pos, - scale=params['scale'], - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 1, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - print(f"预览窗口创建标签: {params['text']} at {gui_pos}") - - elif gui_type == "entry": - # 创建输入框 - gui_pos = (params['pos'][0] * 0.08, 0, params['pos'][2] * 0.08) - element = DirectEntry( - text="", - pos=gui_pos, - scale=params['scale'], - initialText=params['text'], - numLines=1, - width=12, - focus=0, - command=lambda text: print(f"预览输入框输入: {text}") - ) - print(f"预览窗口创建输入框: {params['text']} at {gui_pos}") - - elif gui_type == "slider": - # 创建滑块 - gui_pos = (params['pos'][0] * 0.08, 0, params['pos'][2] * 0.08) - element = DirectSlider( - pos=gui_pos, - scale=params['scale'], - range=(0, 100), - value=50, - frameColor=(0.6, 0.6, 0.6, 1), - thumbColor=(0.2, 0.8, 0.2, 1), - command=lambda: print(f"预览滑块值变化") - ) - print(f"预览窗口创建滑块: {params['text']} at {gui_pos}") - - elif gui_type == "3d_text": - # 创建3D文本 - textNode = TextNode(f'preview-3d-text-{len(self.gui_elements)}') - textNode.setText(params['text']) - textNode.setAlign(TextNode.ACenter) - if self.chinese_font: - textNode.setFont(self.chinese_font) - - element = self.preview_base.render.attachNewNode(textNode) - element.setPos(*params['pos']) - element.setScale(params['scale']) - element.setColor(1, 1, 0, 1) - element.setBillboardAxis() - print(f"预览窗口创建3D文本: {params['text']} at {params['pos']}") - - elif gui_type == "virtual_screen": - # 创建虚拟屏幕 - cm = CardMaker(f"preview-virtual-screen-{len(self.gui_elements)}") - size = params.get('size', (2, 1)) - cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2) - element = self.preview_base.render.attachNewNode(cm.generate()) - element.setPos(*params['pos']) - element.setColor(0.2, 0.2, 0.2, 0.8) - element.setTransparency(TransparencyAttrib.MAlpha) - - # 在虚拟屏幕上添加文本 - screenText = TextNode(f'preview-screen-text-{len(self.gui_elements)}') - screenText.setText(params['text']) - screenText.setAlign(TextNode.ACenter) - if self.chinese_font: - screenText.setFont(self.chinese_font) - screenTextNP = element.attachNewNode(screenText) - screenTextNP.setPos(0, 0.01, 0) - screenTextNP.setScale(0.3) - screenTextNP.setColor(0, 1, 0, 1) - print(f"预览窗口创建虚拟屏幕: {params['text']} at {params['pos']}") - - if element: - self.gui_elements.append(element) - return element - - except Exception as e: - print(f"在预览窗口中创建GUI元素失败: {str(e)}") - - return None - - def refreshAllElements(self, elements_info): - """刷新所有元素""" - # 清空现有元素 - self.clearGUIElements() - - # 重新创建所有元素 - for element_info in elements_info: - self.createElementInPreview(element_info['type'], element_info['params']) - - # 更新预览状态 - self.updatePreview(elements_info) - - def destroy(self): - """销毁预览窗口""" - try: - if self.preview_base: - self.clearGUIElements() - self.preview_base.destroy() - self.preview_base = None - print("预览窗口已销毁") - except Exception as e: - print(f"销毁预览窗口失败: {str(e)}") - -class GUIEditorWindow(QWidget): - """独立的GUI编辑器窗口""" - - # 信号定义 - gui_created = pyqtSignal(str, dict) # GUI类型, 参数字典 - gui_modified = pyqtSignal(object, str, object) # GUI对象, 属性名, 新值 - gui_deleted = pyqtSignal(object) # GUI对象 - editor_closed = pyqtSignal() # 编辑器关闭 - - def __init__(self, parent=None, world=None): - super().__init__(parent) - self.world = world # 主程序的world对象 - self.gui_elements = [] # 当前编辑的GUI元素列表 - self.selected_element = None # 当前选中的GUI元素 - self.preview_window = None # 独立的预览窗口 - - self.setupUI() - self.setupConnections() - - # 设置窗口属性 - self.setWindowTitle("GUI编辑器 - 独立预览") - self.setMinimumSize(800, 600) - self.resize(1000, 700) - - # 应用深色主题 - self.applyDarkTheme() - - # 创建独立预览窗口 - self.createPreviewWindow() - - # 从主程序同步现有的GUI元素 - self.syncGUIElements() - - print("GUI编辑器窗口已创建") - - def createPreviewWindow(self): - """创建独立的预览窗口""" - try: - self.preview_window = GUIPreviewWindow() - print("独立预览窗口已创建") - except Exception as e: - print(f"创建预览窗口失败: {str(e)}") - - def setupUI(self): - """设置用户界面""" - main_layout = QHBoxLayout(self) - - # 创建分割器 - splitter = QSplitter(Qt.Horizontal) - main_layout.addWidget(splitter) - - # 左侧面板 - 工具和属性 - left_panel = self.createLeftPanel() - splitter.addWidget(left_panel) - - # 右侧面板 - 元素列表和预览说明 - right_panel = self.createRightPanel() - splitter.addWidget(right_panel) - - # 设置分割比例 - splitter.setSizes([400, 600]) - - def createLeftPanel(self): - """创建左侧工具面板""" - panel = QWidget() - layout = QVBoxLayout(panel) - - # GUI创建工具组 - tools_group = self.createToolsGroup() - layout.addWidget(tools_group) - - # 属性编辑组 - properties_group = self.createPropertiesGroup() - layout.addWidget(properties_group) - - # 操作按钮组 - actions_group = self.createActionsGroup() - layout.addWidget(actions_group) - - layout.addStretch() - return panel - - def createToolsGroup(self): - """创建GUI工具组""" - group = QGroupBox("GUI创建工具") - layout = QGridLayout(group) - - # 2D GUI工具 - label_2d = QLabel("2D GUI元素") - label_2d.setStyleSheet("font-weight: bold; color: #4CAF50;") - layout.addWidget(label_2d, 0, 0, 1, 2) - - # 按钮工具 - btn_button = QPushButton("创建按钮") - btn_button.clicked.connect(lambda: self.createGUIElement("button")) - btn_button.setStyleSheet("QPushButton { background-color: #2196F3; color: white; padding: 8px; }") - layout.addWidget(btn_button, 1, 0) - - # 标签工具 - btn_label = QPushButton("创建标签") - btn_label.clicked.connect(lambda: self.createGUIElement("label")) - btn_label.setStyleSheet("QPushButton { background-color: #4CAF50; color: white; padding: 8px; }") - layout.addWidget(btn_label, 1, 1) - - # 输入框工具 - btn_entry = QPushButton("创建输入框") - btn_entry.clicked.connect(lambda: self.createGUIElement("entry")) - btn_entry.setStyleSheet("QPushButton { background-color: #FF9800; color: white; padding: 8px; }") - layout.addWidget(btn_entry, 2, 0) - - # 滑块工具 - btn_slider = QPushButton("创建滑块") - btn_slider.clicked.connect(lambda: self.createGUIElement("slider")) - btn_slider.setStyleSheet("QPushButton { background-color: #9C27B0; color: white; padding: 8px; }") - layout.addWidget(btn_slider, 2, 1) - - # 3D GUI工具 - label_3d = QLabel("3D GUI元素") - label_3d.setStyleSheet("font-weight: bold; color: #FF5722;") - layout.addWidget(label_3d, 3, 0, 1, 2) - - # 3D文本工具 - btn_3dtext = QPushButton("创建3D文本") - btn_3dtext.clicked.connect(lambda: self.createGUIElement("3d_text")) - btn_3dtext.setStyleSheet("QPushButton { background-color: #E91E63; color: white; padding: 8px; }") - layout.addWidget(btn_3dtext, 4, 0) - - # 虚拟屏幕工具 - btn_screen = QPushButton("创建虚拟屏幕") - btn_screen.clicked.connect(lambda: self.createGUIElement("virtual_screen")) - btn_screen.setStyleSheet("QPushButton { background-color: #607D8B; color: white; padding: 8px; }") - layout.addWidget(btn_screen, 4, 1) - - return group - - def createPropertiesGroup(self): - """创建属性编辑组""" - group = QGroupBox("属性编辑") - layout = QVBoxLayout(group) - - # 创建滚动区域 - scroll = QScrollArea() - scroll.setWidgetResizable(True) - scroll.setMinimumHeight(200) - - # 属性容器 - self.properties_widget = QWidget() - self.properties_layout = QFormLayout(self.properties_widget) - scroll.setWidget(self.properties_widget) - - layout.addWidget(scroll) - - # 初始提示 - self.showNoSelectionMessage() - - return group - - def createActionsGroup(self): - """创建操作按钮组""" - group = QGroupBox("操作") - layout = QGridLayout(group) - - # 删除按钮 - btn_delete = QPushButton("删除元素") - btn_delete.clicked.connect(self.deleteSelectedElement) - btn_delete.setStyleSheet("QPushButton { background-color: #F44336; color: white; padding: 8px; }") - layout.addWidget(btn_delete, 0, 0) - - # 复制按钮 - btn_copy = QPushButton("复制元素") - btn_copy.clicked.connect(self.copySelectedElement) - btn_copy.setStyleSheet("QPushButton { background-color: #2196F3; color: white; padding: 8px; }") - layout.addWidget(btn_copy, 0, 1) - - # 清空所有按钮 - btn_clear = QPushButton("清空所有") - btn_clear.clicked.connect(self.clearAllElements) - btn_clear.setStyleSheet("QPushButton { background-color: #795548; color: white; padding: 8px; }") - layout.addWidget(btn_clear, 1, 0) - - # 关闭编辑器按钮 - btn_close = QPushButton("关闭编辑器") - btn_close.clicked.connect(self.close) - btn_close.setStyleSheet("QPushButton { background-color: #9E9E9E; color: white; padding: 8px; }") - layout.addWidget(btn_close, 1, 1) - - return group - - def createRightPanel(self): - """创建右侧面板""" - panel = QWidget() - layout = QVBoxLayout(panel) - - # GUI元素列表 - elements_group = QGroupBox("GUI元素列表") - elements_layout = QVBoxLayout(elements_group) - - self.elements_tree = QTreeWidget() - self.elements_tree.setHeaderLabel("GUI元素") - self.elements_tree.itemClicked.connect(self.onElementSelected) - elements_layout.addWidget(self.elements_tree) - - layout.addWidget(elements_group) - - # 预览说明区域 - preview_group = QGroupBox("预览窗口说明") - preview_layout = QVBoxLayout(preview_group) - - # 说明文本 - info_label = QLabel(""" -独立预览窗口

-GUI元素会在独立的Panda3D窗口中显示,这样可以:
-• 正确渲染2D GUI元素(DirectGUI)
-• 避免Qt集成导致的显示问题
-• 提供更好的预览效果

-操作方式:
-• 在左侧创建GUI元素
-• 在预览窗口中查看效果
-• 选择元素后在属性面板中编辑 - """) - info_label.setWordWrap(True) - info_label.setStyleSheet("color: #CCCCCC; padding: 10px; background-color: #3c3c3c; border-radius: 5px;") - preview_layout.addWidget(info_label) - - # 预览窗口控制按钮 - btn_layout = QHBoxLayout() - - btn_show_preview = QPushButton("显示预览窗口") - btn_show_preview.clicked.connect(self.showPreviewWindow) - btn_show_preview.setStyleSheet("QPushButton { background-color: #4CAF50; color: white; padding: 8px; }") - btn_layout.addWidget(btn_show_preview) - - btn_refresh_preview = QPushButton("刷新预览") - btn_refresh_preview.clicked.connect(self.refreshPreview) - btn_refresh_preview.setStyleSheet("QPushButton { background-color: #2196F3; color: white; padding: 8px; }") - btn_layout.addWidget(btn_refresh_preview) - - preview_layout.addLayout(btn_layout) - - layout.addWidget(preview_group) - - return panel - - def showPreviewWindow(self): - """显示预览窗口""" - if not self.preview_window: - self.createPreviewWindow() - - # 确保预览窗口可见 - if self.preview_window and self.preview_window.preview_base: - # 预览窗口应该已经是独立的,这里只是确保它在前台 - try: - # 如果有窗口属性,将其置于前台 - win = self.preview_window.preview_base.win - if win: - print("预览窗口已显示") - except: - pass - - def refreshPreview(self): - """刷新预览""" - if self.preview_window: - self.preview_window.refreshAllElements(self.gui_elements) - print("预览已刷新") - - def setupConnections(self): - """设置信号连接""" - pass - - def syncGUIElements(self): - """从主程序同步现有的GUI元素""" - if not self.world: - return - - print("正在同步现有GUI元素...") - - # 清空列表 - self.gui_elements.clear() - self.elements_tree.clear() - - # 获取主程序中的GUI元素 - for gui_element in self.world.gui_elements: - try: - gui_type = gui_element.getTag("gui_type") if hasattr(gui_element, 'getTag') else "unknown" - gui_text = gui_element.getTag("gui_text") if hasattr(gui_element, 'getTag') else "GUI元素" - - # 构建参数字典 - params = { - 'text': gui_text, - 'pos': (0, 0, 0), - 'scale': 0.1 - } - - if hasattr(gui_element, 'getPos'): - pos = gui_element.getPos() - params['pos'] = (pos.getX(), pos.getY(), pos.getZ()) - - if hasattr(gui_element, 'getScale'): - scale = gui_element.getScale() - params['scale'] = scale.getX() - - self.addElementToList(gui_element, gui_type, params) - - except Exception as e: - print(f"同步GUI元素失败: {str(e)}") - - print(f"同步完成,共 {len(self.gui_elements)} 个GUI元素") - - # 刷新预览 - self.refreshPreview() - - def createGUIElement(self, gui_type): - """创建GUI元素""" - print(f"创建GUI元素: {gui_type}") - - # 收集参数 - params = self.collectGUIParameters(gui_type) - - # 在主程序中创建 - if self.world: - element = self.createElementInWorld(gui_type, params) - if element: - self.addElementToList(element, gui_type, params) - - # 在预览窗口中创建 - if self.preview_window: - self.preview_window.createElementInPreview(gui_type, params) - - # 更新预览 - self.updatePreview() - - def collectGUIParameters(self, gui_type): - """收集GUI创建参数""" - # 计算一个合适的位置,避免重叠 - element_count = len(self.gui_elements) - x_offset = (element_count % 5) * 3 - z_offset = (element_count // 5) * 2 - - # 默认参数 - params = { - 'pos': (x_offset, 0, z_offset), - 'scale': 0.1, - 'text': f'{gui_type}_{element_count}' - } - - # 根据类型调整参数 - if gui_type == "button": - params['text'] = f"按钮_{element_count}" - params['scale'] = 0.08 - elif gui_type == "label": - params['text'] = f"标签_{element_count}" - params['scale'] = 0.06 - elif gui_type == "entry": - params['text'] = f"输入框_{element_count}" - params['scale'] = 0.05 - elif gui_type == "slider": - params['text'] = f"滑块_{element_count}" - params['scale'] = 0.3 - params['range'] = (0, 100) - params['value'] = 50 - elif gui_type == "3d_text": - params['text'] = f"3D文本_{element_count}" - params['pos'] = (x_offset, 5, z_offset) - params['scale'] = 0.5 - elif gui_type == "virtual_screen": - params['text'] = f"屏幕_{element_count}" - params['pos'] = (x_offset, 8, z_offset) - params['size'] = (2, 1) - - return params - - def createElementInWorld(self, gui_type, params): - """在主程序世界中创建GUI元素""" - try: - if not self.world: - return None - - if gui_type == "button": - return self.world.createGUIButton(params['pos'], params['text'], params['scale']) - elif gui_type == "label": - return self.world.createGUILabel(params['pos'], params['text'], params['scale']) - elif gui_type == "entry": - return self.world.createGUIEntry(params['pos'], params['text'], params['scale']) - elif gui_type == "slider": - return self.world.createGUISlider(params['pos'], params['text'], params['scale']) - elif gui_type == "3d_text": - return self.world.createGUI3DText(params['pos'], params['text'], params['scale']) - elif gui_type == "virtual_screen": - return self.world.createGUIVirtualScreen(params['pos'], params.get('size', (2, 1)), params['text']) - - except Exception as e: - print(f"创建GUI元素失败: {str(e)}") - QMessageBox.warning(self, "错误", f"创建GUI元素失败: {str(e)}") - - return None - - def addElementToList(self, element, gui_type, params): - """添加元素到列表""" - # 添加到内部列表 - element_info = { - 'element': element, - 'type': gui_type, - 'params': params - } - self.gui_elements.append(element_info) - - # 添加到树形控件 - item = QTreeWidgetItem(self.elements_tree) - item.setText(0, f"{gui_type}: {params['text']}") - item.setData(0, Qt.UserRole, element_info) - - # 自动选中新创建的元素 - self.elements_tree.setCurrentItem(item) - self.onElementSelected(item, 0) - - def onElementSelected(self, item, column): - """元素被选中""" - element_info = item.data(0, Qt.UserRole) - if element_info: - self.selected_element = element_info - self.updatePropertiesPanel(element_info) - - def updatePropertiesPanel(self, element_info): - """更新属性面板""" - # 清空现有属性 - self.clearPropertiesPanel() - - element = element_info['element'] - gui_type = element_info['type'] - params = element_info['params'] - - # 通用属性 - self.addPropertyEditor("类型", gui_type, read_only=True) - - # 文本属性 - if 'text' in params: - text_edit = QLineEdit(params['text']) - text_edit.textChanged.connect(lambda text: self.updateElementProperty('text', text)) - self.properties_layout.addRow("文本:", text_edit) - - # 位置属性 - if hasattr(element, 'getPos'): - pos = element.getPos() - - x_spin = QDoubleSpinBox() - x_spin.setRange(-1000, 1000) - x_spin.setValue(pos.getX()) - x_spin.valueChanged.connect(lambda v: self.updateElementPosition('x', v)) - self.properties_layout.addRow("位置 X:", x_spin) - - y_spin = QDoubleSpinBox() - y_spin.setRange(-1000, 1000) - y_spin.setValue(pos.getY()) - y_spin.valueChanged.connect(lambda v: self.updateElementPosition('y', v)) - self.properties_layout.addRow("位置 Y:", y_spin) - - z_spin = QDoubleSpinBox() - z_spin.setRange(-1000, 1000) - z_spin.setValue(pos.getZ()) - z_spin.valueChanged.connect(lambda v: self.updateElementPosition('z', v)) - self.properties_layout.addRow("位置 Z:", z_spin) - - # 缩放属性 - if hasattr(element, 'getScale'): - scale = element.getScale() - scale_spin = QDoubleSpinBox() - scale_spin.setRange(0.01, 10.0) - scale_spin.setSingleStep(0.1) - scale_spin.setValue(scale.getX()) - scale_spin.valueChanged.connect(lambda v: self.updateElementScale(v)) - self.properties_layout.addRow("缩放:", scale_spin) - - def addPropertyEditor(self, label, value, read_only=False): - """添加属性编辑器""" - if read_only: - editor = QLabel(str(value)) - editor.setStyleSheet("color: gray;") - else: - editor = QLineEdit(str(value)) - - self.properties_layout.addRow(f"{label}:", editor) - - def clearPropertiesPanel(self): - """清空属性面板""" - while self.properties_layout.count(): - child = self.properties_layout.takeAt(0) - if child.widget(): - child.widget().deleteLater() - - def showNoSelectionMessage(self): - """显示未选中提示""" - self.clearPropertiesPanel() - label = QLabel("请选择一个GUI元素进行编辑") - label.setAlignment(Qt.AlignCenter) - label.setStyleSheet("color: gray; font-style: italic; padding: 20px;") - self.properties_layout.addRow(label) - - def updateElementProperty(self, property_name, value): - """更新元素属性""" - if not self.selected_element: - return - - element = self.selected_element['element'] - - try: - if self.world: - success = self.world.editGUIElement(element, property_name, value) - if success: - self.selected_element['params'][property_name] = value - # 更新树形控件显示 - current_item = self.elements_tree.currentItem() - if current_item and property_name == 'text': - gui_type = self.selected_element['type'] - current_item.setText(0, f"{gui_type}: {value}") - - # 刷新预览 - self.refreshPreview() - except Exception as e: - print(f"更新属性失败: {str(e)}") - - def updateElementPosition(self, axis, value): - """更新元素位置""" - if not self.selected_element: - return - - element = self.selected_element['element'] - current_pos = element.getPos() - - if axis == 'x': - new_pos = [value, current_pos.getY(), current_pos.getZ()] - elif axis == 'y': - new_pos = [current_pos.getX(), value, current_pos.getZ()] - elif axis == 'z': - new_pos = [current_pos.getX(), current_pos.getY(), value] - - self.updateElementProperty('position', new_pos) - - def updateElementScale(self, value): - """更新元素缩放""" - self.updateElementProperty('scale', value) - - def updatePreview(self): - """更新预览显示""" - try: - if self.preview_window: - self.preview_window.updatePreview(self.gui_elements) - except Exception as e: - print(f"更新预览失败: {str(e)}") - - def deleteSelectedElement(self): - """删除选中的元素""" - if not self.selected_element: - QMessageBox.information(self, "提示", "请先选择要删除的元素") - return - - # 确认删除 - reply = QMessageBox.question(self, "确认删除", - "确定要删除选中的GUI元素吗?", - QMessageBox.Yes | QMessageBox.No) - - if reply == QMessageBox.Yes: - element = self.selected_element['element'] - - # 从主程序中删除 - if self.world: - success = self.world.deleteGUIElement(element) - if success: - # 从列表中移除 - self.gui_elements.remove(self.selected_element) - - # 从树形控件中移除 - current_item = self.elements_tree.currentItem() - if current_item: - self.elements_tree.takeTopLevelItem( - self.elements_tree.indexOfTopLevelItem(current_item) - ) - - self.selected_element = None - self.showNoSelectionMessage() - - # 刷新预览 - self.refreshPreview() - - def copySelectedElement(self): - """复制选中的元素""" - if not self.selected_element: - QMessageBox.information(self, "提示", "请先选择要复制的元素") - return - - element = self.selected_element['element'] - gui_type = self.selected_element['type'] - params = self.selected_element['params'].copy() - - # 修改复制元素的参数 - params['text'] = params['text'] + "_副本" - if 'pos' in params: - pos = list(params['pos']) - pos[0] += 1 # X轴偏移 - params['pos'] = tuple(pos) - - # 创建新元素 - new_element = self.createElementInWorld(gui_type, params) - if new_element: - self.addElementToList(new_element, gui_type, params) - # 在预览窗口中创建 - if self.preview_window: - self.preview_window.createElementInPreview(gui_type, params) - # 更新预览 - self.updatePreview() - - def clearAllElements(self): - """清空所有元素""" - if not self.gui_elements: - QMessageBox.information(self, "提示", "没有可清空的元素") - return - - # 确认清空 - reply = QMessageBox.question(self, "确认清空", - f"确定要删除所有 {len(self.gui_elements)} 个GUI元素吗?", - QMessageBox.Yes | QMessageBox.No) - - if reply == QMessageBox.Yes: - # 删除所有元素 - for element_info in self.gui_elements.copy(): - element = element_info['element'] - if self.world: - self.world.deleteGUIElement(element) - - # 清空列表 - self.gui_elements.clear() - self.elements_tree.clear() - self.selected_element = None - self.showNoSelectionMessage() - - # 刷新预览窗口 - self.refreshPreview() - - def applyDarkTheme(self): - """应用深色主题""" - self.setStyleSheet(""" - QWidget { - background-color: #2b2b2b; - color: #ffffff; - } - QGroupBox { - font-weight: bold; - border: 2px solid #555555; - border-radius: 8px; - margin-top: 1ex; - padding-top: 8px; - } - QGroupBox::title { - subcontrol-origin: margin; - left: 10px; - padding: 0 5px 0 5px; - } - QPushButton { - border: 1px solid #555555; - border-radius: 4px; - padding: 6px; - min-width: 80px; - } - QPushButton:hover { - border: 1px solid #777777; - } - QPushButton:pressed { - background-color: #404040; - } - QLineEdit, QDoubleSpinBox, QSpinBox, QComboBox { - border: 1px solid #555555; - border-radius: 4px; - padding: 4px; - background-color: #3c3c3c; - } - QTreeWidget { - border: 1px solid #555555; - background-color: #3c3c3c; - alternate-background-color: #404040; - } - QTextEdit { - border: 1px solid #555555; - background-color: #3c3c3c; - } - """) - - def closeEvent(self, event): - """窗口关闭事件""" - try: - # 销毁预览窗口 - if self.preview_window: - self.preview_window.destroy() - self.preview_window = None - except Exception as e: - print(f"关闭预览窗口失败: {str(e)}") - - self.editor_closed.emit() - super().closeEvent(event) - -def main(): - """独立测试函数""" - from PyQt5.QtWidgets import QApplication - - app = QApplication(sys.argv) - - # 创建测试窗口 - editor = GUIEditorWindow() - editor.show() - - sys.exit(app.exec_()) - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +独立的GUI编辑器窗口 +当主程序进入GUI编辑模式时,弹出此窗口进行GUI编辑操作 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +import os +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton, + QLabel, QGroupBox, QGridLayout, QLineEdit, + QDoubleSpinBox, QSpinBox, QTextEdit, QComboBox, + QCheckBox, QSlider, QColorDialog, QFrame, + QSplitter, QTreeWidget, QTreeWidgetItem, + QFormLayout, QScrollArea, QMessageBox) +from PyQt5.QtCore import Qt, pyqtSignal, QTimer +from PyQt5.QtGui import QFont, QColor, QPalette + +# 导入Panda3D相关模块 +from direct.showbase.ShowBase import ShowBase +from direct.gui.DirectGui import * +from panda3d.core import * + +class GUIPreviewWindow: + """独立的Panda3D GUI预览窗口""" + def __init__(self): + self.preview_base = None + self.chinese_font = None + self.gui_elements = [] + self.welcome_text = None + self.tip_text = None + self.setupPanda3D() + + def setupPanda3D(self): + """设置独立的Panda3D预览窗口""" + try: + # 配置Panda3D设置 - 创建独立窗口 + loadPrcFileData("", """ + win-size 600 400 + window-title GUI Preview - 独立预览窗口 + framebuffer-multisample 1 + multisamples 2 + audio-library-name null + notify-level-audio error + show-frame-rate-meter 0 + want-pstats 0 + """) + + # 创建ShowBase实例 + self.preview_base = ShowBase() + + # 设置背景色 + self.preview_base.setBackgroundColor(0.2, 0.2, 0.3) + + # 调整相机位置(主要用于查看2D GUI) + self.preview_base.cam.setPos(0, -10, 0) + self.preview_base.cam.lookAt(0, 0, 0) + + # 尝试加载中文字体 + try: + self.chinese_font = self.preview_base.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') + if not self.chinese_font: + print("预览窗口: 无法加载中文字体,将使用默认字体") + except: + print("预览窗口: 无法加载中文字体,将使用默认字体") + self.chinese_font = None + + # 创建说明文本 + self.createInfoText() + + print("✓ 独立GUI预览窗口已创建") + print("这是一个独立的Panda3D窗口,可以正确显示2D GUI元素") + + except Exception as e: + print(f"创建独立预览窗口失败: {str(e)}") + + def createInfoText(self): + """创建说明文本""" + # 创建欢迎信息 + self.welcome_text = OnscreenText( + text="GUI预览窗口\n\n这是独立的Panda3D窗口\n在GUI编辑器中创建的元素\n会在这里正确显示\n\n特别是2D GUI元素", + pos=(0, 0.2), + scale=0.08, + fg=(1, 1, 1, 1), + align=TextNode.ACenter, + font=self.chinese_font if self.chinese_font else None + ) + + # 创建提示信息 + self.tip_text = OnscreenText( + text="提示: 这个窗口独立于Qt,可以正确渲染DirectGUI元素", + pos=(0, -0.7), + scale=0.05, + fg=(0.8, 0.8, 0.8, 1), + align=TextNode.ACenter, + font=self.chinese_font if self.chinese_font else None + ) + + # 创建操作说明 + self.control_text = OnscreenText( + text="操作说明:\n• 鼠标滚轮: 缩放视图\n• 右键拖拽: 旋转相机", + pos=(-0.9, -0.3), + scale=0.04, + fg=(0.7, 0.7, 1, 1), + align=TextNode.ALeft, + font=self.chinese_font if self.chinese_font else None + ) + + # 绑定鼠标控制 + self.preview_base.accept("wheel_up", self.zoomIn) + self.preview_base.accept("wheel_down", self.zoomOut) + + def zoomIn(self): + """放大视图""" + pos = self.preview_base.cam.getPos() + forward = self.preview_base.cam.getQuat().getForward() + self.preview_base.cam.setPos(pos + forward * 2) + + def zoomOut(self): + """缩小视图""" + pos = self.preview_base.cam.getPos() + forward = self.preview_base.cam.getQuat().getForward() + self.preview_base.cam.setPos(pos - forward * 2) + + def clearGUIElements(self): + """清空所有GUI元素""" + # 移除所有已创建的GUI元素 + for gui_element in self.gui_elements: + try: + if hasattr(gui_element, 'removeNode'): + gui_element.removeNode() + elif hasattr(gui_element, 'destroy'): + gui_element.destroy() + except: + pass + + self.gui_elements.clear() + + # 恢复说明文本 + if not self.welcome_text: + self.createInfoText() + + def updatePreview(self, gui_elements_info): + """更新预览显示""" + try: + # 清除说明文本(如果存在GUI元素) + if gui_elements_info and self.welcome_text: + self.welcome_text.removeNode() + self.tip_text.removeNode() + self.control_text.removeNode() + self.welcome_text = None + self.tip_text = None + self.control_text = None + elif not gui_elements_info and not self.welcome_text: + # 如果没有GUI元素了,重新创建说明文本 + self.createInfoText() + + except Exception as e: + print(f"更新预览失败: {str(e)}") + + def createElementInPreview(self, gui_type, params): + """在预览窗口中创建GUI元素""" + try: + if not self.preview_base: + return None + + element = None + + if gui_type == "button": + # 创建按钮 - 使用适合预览的坐标 + gui_pos = (params['pos'][0] * 0.08, 0, params['pos'][2] * 0.08) + element = DirectButton( + text=params['text'], + pos=gui_pos, + scale=params['scale'], + frameColor=(0.2, 0.6, 0.8, 1), + text_font=self.chinese_font if self.chinese_font else None, + command=lambda: print(f"预览按钮点击: {params['text']}") + ) + print(f"预览窗口创建按钮: {params['text']} at {gui_pos}") + + elif gui_type == "label": + # 创建标签 + gui_pos = (params['pos'][0] * 0.08, 0, params['pos'][2] * 0.08) + element = DirectLabel( + text=params['text'], + pos=gui_pos, + scale=params['scale'], + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + print(f"预览窗口创建标签: {params['text']} at {gui_pos}") + + elif gui_type == "entry": + # 创建输入框 + gui_pos = (params['pos'][0] * 0.08, 0, params['pos'][2] * 0.08) + element = DirectEntry( + text="", + pos=gui_pos, + scale=params['scale'], + initialText=params['text'], + numLines=1, + width=12, + focus=0, + command=lambda text: print(f"预览输入框输入: {text}") + ) + print(f"预览窗口创建输入框: {params['text']} at {gui_pos}") + + elif gui_type == "slider": + # 创建滑块 + gui_pos = (params['pos'][0] * 0.08, 0, params['pos'][2] * 0.08) + element = DirectSlider( + pos=gui_pos, + scale=params['scale'], + range=(0, 100), + value=50, + frameColor=(0.6, 0.6, 0.6, 1), + thumbColor=(0.2, 0.8, 0.2, 1), + command=lambda: print(f"预览滑块值变化") + ) + print(f"预览窗口创建滑块: {params['text']} at {gui_pos}") + + elif gui_type == "3d_text": + # 创建3D文本 + textNode = TextNode(f'preview-3d-text-{len(self.gui_elements)}') + textNode.setText(params['text']) + textNode.setAlign(TextNode.ACenter) + if self.chinese_font: + textNode.setFont(self.chinese_font) + + element = self.preview_base.render.attachNewNode(textNode) + element.setPos(*params['pos']) + element.setScale(params['scale']) + element.setColor(1, 1, 0, 1) + element.setBillboardAxis() + print(f"预览窗口创建3D文本: {params['text']} at {params['pos']}") + + elif gui_type == "virtual_screen": + # 创建虚拟屏幕 + cm = CardMaker(f"preview-virtual-screen-{len(self.gui_elements)}") + size = params.get('size', (2, 1)) + cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2) + element = self.preview_base.render.attachNewNode(cm.generate()) + element.setPos(*params['pos']) + element.setColor(0.2, 0.2, 0.2, 0.8) + element.setTransparency(TransparencyAttrib.MAlpha) + + # 在虚拟屏幕上添加文本 + screenText = TextNode(f'preview-screen-text-{len(self.gui_elements)}') + screenText.setText(params['text']) + screenText.setAlign(TextNode.ACenter) + if self.chinese_font: + screenText.setFont(self.chinese_font) + screenTextNP = element.attachNewNode(screenText) + screenTextNP.setPos(0, 0.01, 0) + screenTextNP.setScale(0.3) + screenTextNP.setColor(0, 1, 0, 1) + print(f"预览窗口创建虚拟屏幕: {params['text']} at {params['pos']}") + + if element: + self.gui_elements.append(element) + return element + + except Exception as e: + print(f"在预览窗口中创建GUI元素失败: {str(e)}") + + return None + + def refreshAllElements(self, elements_info): + """刷新所有元素""" + # 清空现有元素 + self.clearGUIElements() + + # 重新创建所有元素 + for element_info in elements_info: + self.createElementInPreview(element_info['type'], element_info['params']) + + # 更新预览状态 + self.updatePreview(elements_info) + + def destroy(self): + """销毁预览窗口""" + try: + if self.preview_base: + self.clearGUIElements() + self.preview_base.destroy() + self.preview_base = None + print("预览窗口已销毁") + except Exception as e: + print(f"销毁预览窗口失败: {str(e)}") + +class GUIEditorWindow(QWidget): + """独立的GUI编辑器窗口""" + + # 信号定义 + gui_created = pyqtSignal(str, dict) # GUI类型, 参数字典 + gui_modified = pyqtSignal(object, str, object) # GUI对象, 属性名, 新值 + gui_deleted = pyqtSignal(object) # GUI对象 + editor_closed = pyqtSignal() # 编辑器关闭 + + def __init__(self, parent=None, world=None): + super().__init__(parent) + self.world = world # 主程序的world对象 + self.gui_elements = [] # 当前编辑的GUI元素列表 + self.selected_element = None # 当前选中的GUI元素 + self.preview_window = None # 独立的预览窗口 + + self.setupUI() + self.setupConnections() + + # 设置窗口属性 + self.setWindowTitle("GUI编辑器 - 独立预览") + self.setMinimumSize(800, 600) + self.resize(1000, 700) + + # 应用深色主题 + self.applyDarkTheme() + + # 创建独立预览窗口 + self.createPreviewWindow() + + # 从主程序同步现有的GUI元素 + self.syncGUIElements() + + print("GUI编辑器窗口已创建") + + def createPreviewWindow(self): + """创建独立的预览窗口""" + try: + self.preview_window = GUIPreviewWindow() + print("独立预览窗口已创建") + except Exception as e: + print(f"创建预览窗口失败: {str(e)}") + + def setupUI(self): + """设置用户界面""" + main_layout = QHBoxLayout(self) + + # 创建分割器 + splitter = QSplitter(Qt.Horizontal) + main_layout.addWidget(splitter) + + # 左侧面板 - 工具和属性 + left_panel = self.createLeftPanel() + splitter.addWidget(left_panel) + + # 右侧面板 - 元素列表和预览说明 + right_panel = self.createRightPanel() + splitter.addWidget(right_panel) + + # 设置分割比例 + splitter.setSizes([400, 600]) + + def createLeftPanel(self): + """创建左侧工具面板""" + panel = QWidget() + layout = QVBoxLayout(panel) + + # GUI创建工具组 + tools_group = self.createToolsGroup() + layout.addWidget(tools_group) + + # 属性编辑组 + properties_group = self.createPropertiesGroup() + layout.addWidget(properties_group) + + # 操作按钮组 + actions_group = self.createActionsGroup() + layout.addWidget(actions_group) + + layout.addStretch() + return panel + + def createToolsGroup(self): + """创建GUI工具组""" + group = QGroupBox("GUI创建工具") + layout = QGridLayout(group) + + # 2D GUI工具 + label_2d = QLabel("2D GUI元素") + label_2d.setStyleSheet("font-weight: bold; color: #4CAF50;") + layout.addWidget(label_2d, 0, 0, 1, 2) + + # 按钮工具 + btn_button = QPushButton("创建按钮") + btn_button.clicked.connect(lambda: self.createGUIElement("button")) + btn_button.setStyleSheet("QPushButton { background-color: #2196F3; color: white; padding: 8px; }") + layout.addWidget(btn_button, 1, 0) + + # 标签工具 + btn_label = QPushButton("创建标签") + btn_label.clicked.connect(lambda: self.createGUIElement("label")) + btn_label.setStyleSheet("QPushButton { background-color: #4CAF50; color: white; padding: 8px; }") + layout.addWidget(btn_label, 1, 1) + + # 输入框工具 + btn_entry = QPushButton("创建输入框") + btn_entry.clicked.connect(lambda: self.createGUIElement("entry")) + btn_entry.setStyleSheet("QPushButton { background-color: #FF9800; color: white; padding: 8px; }") + layout.addWidget(btn_entry, 2, 0) + + # 滑块工具 + btn_slider = QPushButton("创建滑块") + btn_slider.clicked.connect(lambda: self.createGUIElement("slider")) + btn_slider.setStyleSheet("QPushButton { background-color: #9C27B0; color: white; padding: 8px; }") + layout.addWidget(btn_slider, 2, 1) + + # 3D GUI工具 + label_3d = QLabel("3D GUI元素") + label_3d.setStyleSheet("font-weight: bold; color: #FF5722;") + layout.addWidget(label_3d, 3, 0, 1, 2) + + # 3D文本工具 + btn_3dtext = QPushButton("创建3D文本") + btn_3dtext.clicked.connect(lambda: self.createGUIElement("3d_text")) + btn_3dtext.setStyleSheet("QPushButton { background-color: #E91E63; color: white; padding: 8px; }") + layout.addWidget(btn_3dtext, 4, 0) + + # 虚拟屏幕工具 + btn_screen = QPushButton("创建虚拟屏幕") + btn_screen.clicked.connect(lambda: self.createGUIElement("virtual_screen")) + btn_screen.setStyleSheet("QPushButton { background-color: #607D8B; color: white; padding: 8px; }") + layout.addWidget(btn_screen, 4, 1) + + return group + + def createPropertiesGroup(self): + """创建属性编辑组""" + group = QGroupBox("属性编辑") + layout = QVBoxLayout(group) + + # 创建滚动区域 + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setMinimumHeight(200) + + # 属性容器 + self.properties_widget = QWidget() + self.properties_layout = QFormLayout(self.properties_widget) + scroll.setWidget(self.properties_widget) + + layout.addWidget(scroll) + + # 初始提示 + self.showNoSelectionMessage() + + return group + + def createActionsGroup(self): + """创建操作按钮组""" + group = QGroupBox("操作") + layout = QGridLayout(group) + + # 删除按钮 + btn_delete = QPushButton("删除元素") + btn_delete.clicked.connect(self.deleteSelectedElement) + btn_delete.setStyleSheet("QPushButton { background-color: #F44336; color: white; padding: 8px; }") + layout.addWidget(btn_delete, 0, 0) + + # 复制按钮 + btn_copy = QPushButton("复制元素") + btn_copy.clicked.connect(self.copySelectedElement) + btn_copy.setStyleSheet("QPushButton { background-color: #2196F3; color: white; padding: 8px; }") + layout.addWidget(btn_copy, 0, 1) + + # 清空所有按钮 + btn_clear = QPushButton("清空所有") + btn_clear.clicked.connect(self.clearAllElements) + btn_clear.setStyleSheet("QPushButton { background-color: #795548; color: white; padding: 8px; }") + layout.addWidget(btn_clear, 1, 0) + + # 关闭编辑器按钮 + btn_close = QPushButton("关闭编辑器") + btn_close.clicked.connect(self.close) + btn_close.setStyleSheet("QPushButton { background-color: #9E9E9E; color: white; padding: 8px; }") + layout.addWidget(btn_close, 1, 1) + + return group + + def createRightPanel(self): + """创建右侧面板""" + panel = QWidget() + layout = QVBoxLayout(panel) + + # GUI元素列表 + elements_group = QGroupBox("GUI元素列表") + elements_layout = QVBoxLayout(elements_group) + + self.elements_tree = QTreeWidget() + self.elements_tree.setHeaderLabel("GUI元素") + self.elements_tree.itemClicked.connect(self.onElementSelected) + elements_layout.addWidget(self.elements_tree) + + layout.addWidget(elements_group) + + # 预览说明区域 + preview_group = QGroupBox("预览窗口说明") + preview_layout = QVBoxLayout(preview_group) + + # 说明文本 + info_label = QLabel(""" +独立预览窗口

+GUI元素会在独立的Panda3D窗口中显示,这样可以:
+• 正确渲染2D GUI元素(DirectGUI)
+• 避免Qt集成导致的显示问题
+• 提供更好的预览效果

+操作方式:
+• 在左侧创建GUI元素
+• 在预览窗口中查看效果
+• 选择元素后在属性面板中编辑 + """) + info_label.setWordWrap(True) + info_label.setStyleSheet("color: #CCCCCC; padding: 10px; background-color: #3c3c3c; border-radius: 5px;") + preview_layout.addWidget(info_label) + + # 预览窗口控制按钮 + btn_layout = QHBoxLayout() + + btn_show_preview = QPushButton("显示预览窗口") + btn_show_preview.clicked.connect(self.showPreviewWindow) + btn_show_preview.setStyleSheet("QPushButton { background-color: #4CAF50; color: white; padding: 8px; }") + btn_layout.addWidget(btn_show_preview) + + btn_refresh_preview = QPushButton("刷新预览") + btn_refresh_preview.clicked.connect(self.refreshPreview) + btn_refresh_preview.setStyleSheet("QPushButton { background-color: #2196F3; color: white; padding: 8px; }") + btn_layout.addWidget(btn_refresh_preview) + + preview_layout.addLayout(btn_layout) + + layout.addWidget(preview_group) + + return panel + + def showPreviewWindow(self): + """显示预览窗口""" + if not self.preview_window: + self.createPreviewWindow() + + # 确保预览窗口可见 + if self.preview_window and self.preview_window.preview_base: + # 预览窗口应该已经是独立的,这里只是确保它在前台 + try: + # 如果有窗口属性,将其置于前台 + win = self.preview_window.preview_base.win + if win: + print("预览窗口已显示") + except: + pass + + def refreshPreview(self): + """刷新预览""" + if self.preview_window: + self.preview_window.refreshAllElements(self.gui_elements) + print("预览已刷新") + + def setupConnections(self): + """设置信号连接""" + pass + + def syncGUIElements(self): + """从主程序同步现有的GUI元素""" + if not self.world: + return + + print("正在同步现有GUI元素...") + + # 清空列表 + self.gui_elements.clear() + self.elements_tree.clear() + + # 获取主程序中的GUI元素 + for gui_element in self.world.gui_elements: + try: + gui_type = gui_element.getTag("gui_type") if hasattr(gui_element, 'getTag') else "unknown" + gui_text = gui_element.getTag("gui_text") if hasattr(gui_element, 'getTag') else "GUI元素" + + # 构建参数字典 + params = { + 'text': gui_text, + 'pos': (0, 0, 0), + 'scale': 0.1 + } + + if hasattr(gui_element, 'getPos'): + pos = gui_element.getPos() + params['pos'] = (pos.getX(), pos.getY(), pos.getZ()) + + if hasattr(gui_element, 'getScale'): + scale = gui_element.getScale() + params['scale'] = scale.getX() + + self.addElementToList(gui_element, gui_type, params) + + except Exception as e: + print(f"同步GUI元素失败: {str(e)}") + + print(f"同步完成,共 {len(self.gui_elements)} 个GUI元素") + + # 刷新预览 + self.refreshPreview() + + def createGUIElement(self, gui_type): + """创建GUI元素""" + print(f"创建GUI元素: {gui_type}") + + # 收集参数 + params = self.collectGUIParameters(gui_type) + + # 在主程序中创建 + if self.world: + element = self.createElementInWorld(gui_type, params) + if element: + self.addElementToList(element, gui_type, params) + + # 在预览窗口中创建 + if self.preview_window: + self.preview_window.createElementInPreview(gui_type, params) + + # 更新预览 + self.updatePreview() + + def collectGUIParameters(self, gui_type): + """收集GUI创建参数""" + # 计算一个合适的位置,避免重叠 + element_count = len(self.gui_elements) + x_offset = (element_count % 5) * 3 + z_offset = (element_count // 5) * 2 + + # 默认参数 + params = { + 'pos': (x_offset, 0, z_offset), + 'scale': 0.1, + 'text': f'{gui_type}_{element_count}' + } + + # 根据类型调整参数 + if gui_type == "button": + params['text'] = f"按钮_{element_count}" + params['scale'] = 0.08 + elif gui_type == "label": + params['text'] = f"标签_{element_count}" + params['scale'] = 0.06 + elif gui_type == "entry": + params['text'] = f"输入框_{element_count}" + params['scale'] = 0.05 + elif gui_type == "slider": + params['text'] = f"滑块_{element_count}" + params['scale'] = 0.3 + params['range'] = (0, 100) + params['value'] = 50 + elif gui_type == "3d_text": + params['text'] = f"3D文本_{element_count}" + params['pos'] = (x_offset, 5, z_offset) + params['scale'] = 0.5 + elif gui_type == "virtual_screen": + params['text'] = f"屏幕_{element_count}" + params['pos'] = (x_offset, 8, z_offset) + params['size'] = (2, 1) + + return params + + def createElementInWorld(self, gui_type, params): + """在主程序世界中创建GUI元素""" + try: + if not self.world: + return None + + if gui_type == "button": + return self.world.createGUIButton(params['pos'], params['text'], params['scale']) + elif gui_type == "label": + return self.world.createGUILabel(params['pos'], params['text'], params['scale']) + elif gui_type == "entry": + return self.world.createGUIEntry(params['pos'], params['text'], params['scale']) + elif gui_type == "slider": + return self.world.createGUISlider(params['pos'], params['text'], params['scale']) + elif gui_type == "3d_text": + return self.world.createGUI3DText(params['pos'], params['text'], params['scale']) + elif gui_type == "virtual_screen": + return self.world.createGUIVirtualScreen(params['pos'], params.get('size', (2, 1)), params['text']) + + except Exception as e: + print(f"创建GUI元素失败: {str(e)}") + QMessageBox.warning(self, "错误", f"创建GUI元素失败: {str(e)}") + + return None + + def addElementToList(self, element, gui_type, params): + """添加元素到列表""" + # 添加到内部列表 + element_info = { + 'element': element, + 'type': gui_type, + 'params': params + } + self.gui_elements.append(element_info) + + # 添加到树形控件 + item = QTreeWidgetItem(self.elements_tree) + item.setText(0, f"{gui_type}: {params['text']}") + item.setData(0, Qt.UserRole, element_info) + + # 自动选中新创建的元素 + self.elements_tree.setCurrentItem(item) + self.onElementSelected(item, 0) + + def onElementSelected(self, item, column): + """元素被选中""" + element_info = item.data(0, Qt.UserRole) + if element_info: + self.selected_element = element_info + self.updatePropertiesPanel(element_info) + + def updatePropertiesPanel(self, element_info): + """更新属性面板""" + # 清空现有属性 + self.clearPropertiesPanel() + + element = element_info['element'] + gui_type = element_info['type'] + params = element_info['params'] + + # 通用属性 + self.addPropertyEditor("类型", gui_type, read_only=True) + + # 文本属性 + if 'text' in params: + text_edit = QLineEdit(params['text']) + text_edit.textChanged.connect(lambda text: self.updateElementProperty('text', text)) + self.properties_layout.addRow("文本:", text_edit) + + # 位置属性 + if hasattr(element, 'getPos'): + pos = element.getPos() + + x_spin = QDoubleSpinBox() + x_spin.setRange(-1000, 1000) + x_spin.setValue(pos.getX()) + x_spin.valueChanged.connect(lambda v: self.updateElementPosition('x', v)) + self.properties_layout.addRow("位置 X:", x_spin) + + y_spin = QDoubleSpinBox() + y_spin.setRange(-1000, 1000) + y_spin.setValue(pos.getY()) + y_spin.valueChanged.connect(lambda v: self.updateElementPosition('y', v)) + self.properties_layout.addRow("位置 Y:", y_spin) + + z_spin = QDoubleSpinBox() + z_spin.setRange(-1000, 1000) + z_spin.setValue(pos.getZ()) + z_spin.valueChanged.connect(lambda v: self.updateElementPosition('z', v)) + self.properties_layout.addRow("位置 Z:", z_spin) + + # 缩放属性 + if hasattr(element, 'getScale'): + scale = element.getScale() + scale_spin = QDoubleSpinBox() + scale_spin.setRange(0.01, 10.0) + scale_spin.setSingleStep(0.1) + scale_spin.setValue(scale.getX()) + scale_spin.valueChanged.connect(lambda v: self.updateElementScale(v)) + self.properties_layout.addRow("缩放:", scale_spin) + + def addPropertyEditor(self, label, value, read_only=False): + """添加属性编辑器""" + if read_only: + editor = QLabel(str(value)) + editor.setStyleSheet("color: gray;") + else: + editor = QLineEdit(str(value)) + + self.properties_layout.addRow(f"{label}:", editor) + + def clearPropertiesPanel(self): + """清空属性面板""" + while self.properties_layout.count(): + child = self.properties_layout.takeAt(0) + if child.widget(): + child.widget().deleteLater() + + def showNoSelectionMessage(self): + """显示未选中提示""" + self.clearPropertiesPanel() + label = QLabel("请选择一个GUI元素进行编辑") + label.setAlignment(Qt.AlignCenter) + label.setStyleSheet("color: gray; font-style: italic; padding: 20px;") + self.properties_layout.addRow(label) + + def updateElementProperty(self, property_name, value): + """更新元素属性""" + if not self.selected_element: + return + + element = self.selected_element['element'] + + try: + if self.world: + success = self.world.editGUIElement(element, property_name, value) + if success: + self.selected_element['params'][property_name] = value + # 更新树形控件显示 + current_item = self.elements_tree.currentItem() + if current_item and property_name == 'text': + gui_type = self.selected_element['type'] + current_item.setText(0, f"{gui_type}: {value}") + + # 刷新预览 + self.refreshPreview() + except Exception as e: + print(f"更新属性失败: {str(e)}") + + def updateElementPosition(self, axis, value): + """更新元素位置""" + if not self.selected_element: + return + + element = self.selected_element['element'] + current_pos = element.getPos() + + if axis == 'x': + new_pos = [value, current_pos.getY(), current_pos.getZ()] + elif axis == 'y': + new_pos = [current_pos.getX(), value, current_pos.getZ()] + elif axis == 'z': + new_pos = [current_pos.getX(), current_pos.getY(), value] + + self.updateElementProperty('position', new_pos) + + def updateElementScale(self, value): + """更新元素缩放""" + self.updateElementProperty('scale', value) + + def updatePreview(self): + """更新预览显示""" + try: + if self.preview_window: + self.preview_window.updatePreview(self.gui_elements) + except Exception as e: + print(f"更新预览失败: {str(e)}") + + def deleteSelectedElement(self): + """删除选中的元素""" + if not self.selected_element: + QMessageBox.information(self, "提示", "请先选择要删除的元素") + return + + # 确认删除 + reply = QMessageBox.question(self, "确认删除", + "确定要删除选中的GUI元素吗?", + QMessageBox.Yes | QMessageBox.No) + + if reply == QMessageBox.Yes: + element = self.selected_element['element'] + + # 从主程序中删除 + if self.world: + success = self.world.deleteGUIElement(element) + if success: + # 从列表中移除 + self.gui_elements.remove(self.selected_element) + + # 从树形控件中移除 + current_item = self.elements_tree.currentItem() + if current_item: + self.elements_tree.takeTopLevelItem( + self.elements_tree.indexOfTopLevelItem(current_item) + ) + + self.selected_element = None + self.showNoSelectionMessage() + + # 刷新预览 + self.refreshPreview() + + def copySelectedElement(self): + """复制选中的元素""" + if not self.selected_element: + QMessageBox.information(self, "提示", "请先选择要复制的元素") + return + + element = self.selected_element['element'] + gui_type = self.selected_element['type'] + params = self.selected_element['params'].copy() + + # 修改复制元素的参数 + params['text'] = params['text'] + "_副本" + if 'pos' in params: + pos = list(params['pos']) + pos[0] += 1 # X轴偏移 + params['pos'] = tuple(pos) + + # 创建新元素 + new_element = self.createElementInWorld(gui_type, params) + if new_element: + self.addElementToList(new_element, gui_type, params) + # 在预览窗口中创建 + if self.preview_window: + self.preview_window.createElementInPreview(gui_type, params) + # 更新预览 + self.updatePreview() + + def clearAllElements(self): + """清空所有元素""" + if not self.gui_elements: + QMessageBox.information(self, "提示", "没有可清空的元素") + return + + # 确认清空 + reply = QMessageBox.question(self, "确认清空", + f"确定要删除所有 {len(self.gui_elements)} 个GUI元素吗?", + QMessageBox.Yes | QMessageBox.No) + + if reply == QMessageBox.Yes: + # 删除所有元素 + for element_info in self.gui_elements.copy(): + element = element_info['element'] + if self.world: + self.world.deleteGUIElement(element) + + # 清空列表 + self.gui_elements.clear() + self.elements_tree.clear() + self.selected_element = None + self.showNoSelectionMessage() + + # 刷新预览窗口 + self.refreshPreview() + + def applyDarkTheme(self): + """应用深色主题""" + self.setStyleSheet(""" + QWidget { + background-color: #2b2b2b; + color: #ffffff; + } + QGroupBox { + font-weight: bold; + border: 2px solid #555555; + border-radius: 8px; + margin-top: 1ex; + padding-top: 8px; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 10px; + padding: 0 5px 0 5px; + } + QPushButton { + border: 1px solid #555555; + border-radius: 4px; + padding: 6px; + min-width: 80px; + } + QPushButton:hover { + border: 1px solid #777777; + } + QPushButton:pressed { + background-color: #404040; + } + QLineEdit, QDoubleSpinBox, QSpinBox, QComboBox { + border: 1px solid #555555; + border-radius: 4px; + padding: 4px; + background-color: #3c3c3c; + } + QTreeWidget { + border: 1px solid #555555; + background-color: #3c3c3c; + alternate-background-color: #404040; + } + QTextEdit { + border: 1px solid #555555; + background-color: #3c3c3c; + } + """) + + def closeEvent(self, event): + """窗口关闭事件""" + try: + # 销毁预览窗口 + if self.preview_window: + self.preview_window.destroy() + self.preview_window = None + except Exception as e: + print(f"关闭预览窗口失败: {str(e)}") + + self.editor_closed.emit() + super().closeEvent(event) + +def main(): + """独立测试函数""" + from PyQt5.QtWidgets import QApplication + + app = QApplication(sys.argv) + + # 创建测试窗口 + editor = GUIEditorWindow() + editor.show() + + sys.exit(app.exec_()) + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/demo/gui_test.py b/demo/gui_test.py index 9522bac9..57d5efc5 100644 --- a/demo/gui_test.py +++ b/demo/gui_test.py @@ -1,358 +1,358 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -PANDA3D GUI功能测试示例 -展示DirectGUI系统的各种组件使用方法 -""" - -import sys -from direct.showbase.ShowBase import ShowBase -from direct.gui.DirectGui import * -from direct.gui.OnscreenText import OnscreenText -from direct.gui.OnscreenImage import OnscreenImage -from panda3d.core import * - -class GUITestApp(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 设置窗口标题 - wp = WindowProperties() - wp.setTitle("PANDA3D GUI功能测试") - self.win.requestProperties(wp) - - # 设置背景颜色 - self.setBackgroundColor(0.2, 0.3, 0.5) - - # 尝试加载中文字体 - self.chinese_font = self.loadChineseFont() - - # 创建标题文本 - self.title = OnscreenText( - text="PANDA3D DirectGUI 组件示例", - pos=(0, 0.9), - scale=0.08, - fg=(1, 1, 1, 1), - align=TextNode.ACenter, - font=self.chinese_font if self.chinese_font else None - ) - - # 创建各种GUI组件 - self.createButtons() - self.createLabels() - self.createEntries() - self.createSliders() - self.createCheckboxes() - self.createRadioButtons() - self.createScrolledFrame() - self.createOptionMenu() - - # 状态显示文本 - self.statusText = OnscreenText( - text="状态: 准备就绪", - pos=(-1.2, -0.9), - scale=0.05, - fg=(0, 1, 0, 1), - align=TextNode.ALeft - ) - - print("GUI测试应用启动成功!") - print(f"中文字体加载状态: {'成功' if self.chinese_font else '失败,使用默认字体'}") - print("尝试点击各种GUI组件来测试功能") - - def loadChineseFont(self): - """尝试加载中文字体""" - import os - font_paths = [ - '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', - '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', - '/usr/share/fonts/truetype/arphic/ukai.ttc', - '/usr/share/fonts/truetype/arphic/uming.ttc', - ] - - for font_path in font_paths: - if os.path.exists(font_path): - try: - font = self.loader.loadFont(font_path) - if font: - return font - except: - continue - return None - - def createButtons(self): - """创建按钮组件""" - # 普通按钮 - self.button1 = DirectButton( - text="普通按钮", - pos=(-0.8, 0, 0.6), - scale=0.06, - command=self.onButton1Click, - rolloverSound=None, - clickSound=None, - text_font=self.chinese_font if self.chinese_font else None - ) - - # 带图像的按钮 - self.button2 = DirectButton( - text="彩色按钮", - pos=(-0.4, 0, 0.6), - scale=0.06, - command=self.onButton2Click, - frameColor=(0.2, 0.8, 0.2, 1), - rolloverSound=None, - clickSound=None, - text_font=self.chinese_font if self.chinese_font else None - ) - - # 退出按钮 - self.exitButton = DirectButton( - text="退出程序", - pos=(0.8, 0, 0.6), - scale=0.06, - command=self.exitApp, - frameColor=(0.8, 0.2, 0.2, 1), - rolloverSound=None, - clickSound=None, - text_font=self.chinese_font if self.chinese_font else None - ) - - def createLabels(self): - """创建标签组件""" - self.label1 = DirectLabel( - text="这是一个标签", - pos=(-0.8, 0, 0.4), - scale=0.05, - frameColor=(0, 0, 0, 0), # 透明背景 - text_fg=(1, 1, 0, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - self.label2 = DirectLabel( - text="带背景的标签", - pos=(-0.4, 0, 0.4), - scale=0.05, - frameColor=(0.3, 0.3, 0.7, 0.8), - text_fg=(1, 1, 1, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - def createEntries(self): - """创建文本输入框""" - # 普通输入框 - self.entry1 = DirectEntry( - text="", - pos=(-0.8, 0, 0.2), - scale=0.05, - command=self.onEntry1Submit, - initialText="在这里输入文本...", - numLines=1, - focus=0, - width=12 - ) - - # 密码输入框 - self.entry2 = DirectEntry( - text="", - pos=(-0.4, 0, 0.2), - scale=0.05, - command=self.onEntry2Submit, - initialText="密码输入框", - numLines=1, - focus=0, - width=12, - obscured=1 # 密码模式 - ) - - def createSliders(self): - """创建滑块组件""" - # 水平滑块 - self.slider1 = DirectSlider( - range=(0, 100), - value=50, - pos=(0.2, 0, 0.4), - scale=0.4, - command=self.onSlider1Change, - orientation=DGG.HORIZONTAL - ) - - # 垂直滑块 - self.slider2 = DirectSlider( - range=(0, 100), - value=25, - pos=(0.6, 0, 0.2), - scale=0.3, - command=self.onSlider2Change, - orientation=DGG.VERTICAL - ) - - # 滑块值显示 - self.sliderValueText = OnscreenText( - text="滑块值: H=50, V=25", - pos=(0.4, 0.1), - scale=0.04, - fg=(1, 1, 1, 1), - font=self.chinese_font if self.chinese_font else None - ) - - def createCheckboxes(self): - """创建复选框""" - self.checkbox1 = DirectCheckButton( - text="选项 1", - pos=(-0.8, 0, 0), - scale=0.05, - command=self.onCheckbox1Toggle, - rolloverSound=None, - clickSound=None, - text_font=self.chinese_font if self.chinese_font else None - ) - - self.checkbox2 = DirectCheckButton( - text="选项 2", - pos=(-0.4, 0, 0), - scale=0.05, - command=self.onCheckbox2Toggle, - rolloverSound=None, - clickSound=None, - text_font=self.chinese_font if self.chinese_font else None - ) - - def createRadioButtons(self): - """创建单选按钮组""" - self.radioButtons = [] - self.radioVar = [0] # 用列表来存储选中的索引 - - for i in range(3): - radio = DirectRadioButton( - text=f"单选 {i+1}", - pos=(-0.8 + i*0.2, 0, -0.2), - scale=0.05, - variable=self.radioVar, - value=[i], - command=self.onRadioButtonSelect, - extraArgs=[i], # 添加额外参数 - rolloverSound=None, - clickSound=None, - text_font=self.chinese_font if self.chinese_font else None - ) - self.radioButtons.append(radio) - - def createScrolledFrame(self): - """创建滚动框架""" - # 创建滚动框架 - self.scrollFrame = DirectScrolledFrame( - frameSize=(-0.5, 0.5, -0.3, 0.3), - canvasSize=(-0.5, 0.5, -1, 1), - pos=(0.2, 0, -0.2), - frameColor=(0.2, 0.2, 0.2, 0.8), - scrollBarWidth=0.04, - verticalScroll_relief=DGG.SUNKEN, - horizontalScroll_relief=DGG.SUNKEN - ) - - # 在滚动框架中添加内容 - for i in range(10): - label = DirectLabel( - text=f"滚动项目 {i+1}", - pos=(0, 0, 0.8 - i*0.15), - scale=0.04, - parent=self.scrollFrame.getCanvas(), - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 1, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - def createOptionMenu(self): - """创建选项菜单""" - items = ["选项A", "选项B", "选项C", "选项D"] - self.optionMenu = DirectOptionMenu( - text="选择选项", - pos=(0.6, 0, 0), - scale=0.05, - items=items, - initialitem=0, - command=self.onOptionMenuSelect, - rolloverSound=None, - clickSound=None, - text_font=self.chinese_font if self.chinese_font else None - ) - - # 事件处理函数 - def onButton1Click(self): - """按钮1点击事件""" - self.updateStatus("普通按钮被点击!") - print("普通按钮被点击") - - def onButton2Click(self): - """按钮2点击事件""" - self.updateStatus("彩色按钮被点击!") - print("彩色按钮被点击") - - def onEntry1Submit(self, text): - """文本输入框提交事件""" - self.updateStatus(f"输入的文本: {text}") - print(f"输入的文本: {text}") - - def onEntry2Submit(self, text): - """密码输入框提交事件""" - self.updateStatus(f"输入的密码长度: {len(text)}") - print(f"输入的密码: {text}") - - def onSlider1Change(self): - """水平滑块变化事件""" - value = self.slider1['value'] - self.updateSliderValues() - print(f"水平滑块值: {value}") - - def onSlider2Change(self): - """垂直滑块变化事件""" - value = self.slider2['value'] - self.updateSliderValues() - print(f"垂直滑块值: {value}") - - def updateSliderValues(self): - """更新滑块值显示""" - h_val = int(self.slider1['value']) - v_val = int(self.slider2['value']) - self.sliderValueText.setText(f"滑块值: H={h_val}, V={v_val}") - - def onCheckbox1Toggle(self, status): - """复选框1切换事件""" - state = "选中" if status else "未选中" - self.updateStatus(f"复选框1: {state}") - print(f"复选框1: {state}") - - def onCheckbox2Toggle(self, status): - """复选框2切换事件""" - state = "选中" if status else "未选中" - self.updateStatus(f"复选框2: {state}") - print(f"复选框2: {state}") - - def onRadioButtonSelect(self, index, status): - """单选按钮选择事件""" - if status: - self.updateStatus(f"选中单选按钮: {index + 1}") - print(f"选中单选按钮: {index + 1}") - - def onOptionMenuSelect(self, arg): - """选项菜单选择事件""" - self.updateStatus(f"选择了: {arg}") - print(f"选择了选项: {arg}") - - def updateStatus(self, message): - """更新状态显示""" - self.statusText.setText(f"状态: {message}") - - def exitApp(self): - """退出应用程序""" - print("正在退出程序...") - self.userExit() - -if __name__ == "__main__": - try: - app = GUITestApp() - app.run() - except Exception as e: - print(f"程序运行出错: {e}") +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +PANDA3D GUI功能测试示例 +展示DirectGUI系统的各种组件使用方法 +""" + +import sys +from direct.showbase.ShowBase import ShowBase +from direct.gui.DirectGui import * +from direct.gui.OnscreenText import OnscreenText +from direct.gui.OnscreenImage import OnscreenImage +from panda3d.core import * + +class GUITestApp(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 设置窗口标题 + wp = WindowProperties() + wp.setTitle("PANDA3D GUI功能测试") + self.win.requestProperties(wp) + + # 设置背景颜色 + self.setBackgroundColor(0.2, 0.3, 0.5) + + # 尝试加载中文字体 + self.chinese_font = self.loadChineseFont() + + # 创建标题文本 + self.title = OnscreenText( + text="PANDA3D DirectGUI 组件示例", + pos=(0, 0.9), + scale=0.08, + fg=(1, 1, 1, 1), + align=TextNode.ACenter, + font=self.chinese_font if self.chinese_font else None + ) + + # 创建各种GUI组件 + self.createButtons() + self.createLabels() + self.createEntries() + self.createSliders() + self.createCheckboxes() + self.createRadioButtons() + self.createScrolledFrame() + self.createOptionMenu() + + # 状态显示文本 + self.statusText = OnscreenText( + text="状态: 准备就绪", + pos=(-1.2, -0.9), + scale=0.05, + fg=(0, 1, 0, 1), + align=TextNode.ALeft + ) + + print("GUI测试应用启动成功!") + print(f"中文字体加载状态: {'成功' if self.chinese_font else '失败,使用默认字体'}") + print("尝试点击各种GUI组件来测试功能") + + def loadChineseFont(self): + """尝试加载中文字体""" + import os + font_paths = [ + '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', + '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', + '/usr/share/fonts/truetype/arphic/ukai.ttc', + '/usr/share/fonts/truetype/arphic/uming.ttc', + ] + + for font_path in font_paths: + if os.path.exists(font_path): + try: + font = self.loader.loadFont(font_path) + if font: + return font + except: + continue + return None + + def createButtons(self): + """创建按钮组件""" + # 普通按钮 + self.button1 = DirectButton( + text="普通按钮", + pos=(-0.8, 0, 0.6), + scale=0.06, + command=self.onButton1Click, + rolloverSound=None, + clickSound=None, + text_font=self.chinese_font if self.chinese_font else None + ) + + # 带图像的按钮 + self.button2 = DirectButton( + text="彩色按钮", + pos=(-0.4, 0, 0.6), + scale=0.06, + command=self.onButton2Click, + frameColor=(0.2, 0.8, 0.2, 1), + rolloverSound=None, + clickSound=None, + text_font=self.chinese_font if self.chinese_font else None + ) + + # 退出按钮 + self.exitButton = DirectButton( + text="退出程序", + pos=(0.8, 0, 0.6), + scale=0.06, + command=self.exitApp, + frameColor=(0.8, 0.2, 0.2, 1), + rolloverSound=None, + clickSound=None, + text_font=self.chinese_font if self.chinese_font else None + ) + + def createLabels(self): + """创建标签组件""" + self.label1 = DirectLabel( + text="这是一个标签", + pos=(-0.8, 0, 0.4), + scale=0.05, + frameColor=(0, 0, 0, 0), # 透明背景 + text_fg=(1, 1, 0, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + self.label2 = DirectLabel( + text="带背景的标签", + pos=(-0.4, 0, 0.4), + scale=0.05, + frameColor=(0.3, 0.3, 0.7, 0.8), + text_fg=(1, 1, 1, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + def createEntries(self): + """创建文本输入框""" + # 普通输入框 + self.entry1 = DirectEntry( + text="", + pos=(-0.8, 0, 0.2), + scale=0.05, + command=self.onEntry1Submit, + initialText="在这里输入文本...", + numLines=1, + focus=0, + width=12 + ) + + # 密码输入框 + self.entry2 = DirectEntry( + text="", + pos=(-0.4, 0, 0.2), + scale=0.05, + command=self.onEntry2Submit, + initialText="密码输入框", + numLines=1, + focus=0, + width=12, + obscured=1 # 密码模式 + ) + + def createSliders(self): + """创建滑块组件""" + # 水平滑块 + self.slider1 = DirectSlider( + range=(0, 100), + value=50, + pos=(0.2, 0, 0.4), + scale=0.4, + command=self.onSlider1Change, + orientation=DGG.HORIZONTAL + ) + + # 垂直滑块 + self.slider2 = DirectSlider( + range=(0, 100), + value=25, + pos=(0.6, 0, 0.2), + scale=0.3, + command=self.onSlider2Change, + orientation=DGG.VERTICAL + ) + + # 滑块值显示 + self.sliderValueText = OnscreenText( + text="滑块值: H=50, V=25", + pos=(0.4, 0.1), + scale=0.04, + fg=(1, 1, 1, 1), + font=self.chinese_font if self.chinese_font else None + ) + + def createCheckboxes(self): + """创建复选框""" + self.checkbox1 = DirectCheckButton( + text="选项 1", + pos=(-0.8, 0, 0), + scale=0.05, + command=self.onCheckbox1Toggle, + rolloverSound=None, + clickSound=None, + text_font=self.chinese_font if self.chinese_font else None + ) + + self.checkbox2 = DirectCheckButton( + text="选项 2", + pos=(-0.4, 0, 0), + scale=0.05, + command=self.onCheckbox2Toggle, + rolloverSound=None, + clickSound=None, + text_font=self.chinese_font if self.chinese_font else None + ) + + def createRadioButtons(self): + """创建单选按钮组""" + self.radioButtons = [] + self.radioVar = [0] # 用列表来存储选中的索引 + + for i in range(3): + radio = DirectRadioButton( + text=f"单选 {i+1}", + pos=(-0.8 + i*0.2, 0, -0.2), + scale=0.05, + variable=self.radioVar, + value=[i], + command=self.onRadioButtonSelect, + extraArgs=[i], # 添加额外参数 + rolloverSound=None, + clickSound=None, + text_font=self.chinese_font if self.chinese_font else None + ) + self.radioButtons.append(radio) + + def createScrolledFrame(self): + """创建滚动框架""" + # 创建滚动框架 + self.scrollFrame = DirectScrolledFrame( + frameSize=(-0.5, 0.5, -0.3, 0.3), + canvasSize=(-0.5, 0.5, -1, 1), + pos=(0.2, 0, -0.2), + frameColor=(0.2, 0.2, 0.2, 0.8), + scrollBarWidth=0.04, + verticalScroll_relief=DGG.SUNKEN, + horizontalScroll_relief=DGG.SUNKEN + ) + + # 在滚动框架中添加内容 + for i in range(10): + label = DirectLabel( + text=f"滚动项目 {i+1}", + pos=(0, 0, 0.8 - i*0.15), + scale=0.04, + parent=self.scrollFrame.getCanvas(), + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + def createOptionMenu(self): + """创建选项菜单""" + items = ["选项A", "选项B", "选项C", "选项D"] + self.optionMenu = DirectOptionMenu( + text="选择选项", + pos=(0.6, 0, 0), + scale=0.05, + items=items, + initialitem=0, + command=self.onOptionMenuSelect, + rolloverSound=None, + clickSound=None, + text_font=self.chinese_font if self.chinese_font else None + ) + + # 事件处理函数 + def onButton1Click(self): + """按钮1点击事件""" + self.updateStatus("普通按钮被点击!") + print("普通按钮被点击") + + def onButton2Click(self): + """按钮2点击事件""" + self.updateStatus("彩色按钮被点击!") + print("彩色按钮被点击") + + def onEntry1Submit(self, text): + """文本输入框提交事件""" + self.updateStatus(f"输入的文本: {text}") + print(f"输入的文本: {text}") + + def onEntry2Submit(self, text): + """密码输入框提交事件""" + self.updateStatus(f"输入的密码长度: {len(text)}") + print(f"输入的密码: {text}") + + def onSlider1Change(self): + """水平滑块变化事件""" + value = self.slider1['value'] + self.updateSliderValues() + print(f"水平滑块值: {value}") + + def onSlider2Change(self): + """垂直滑块变化事件""" + value = self.slider2['value'] + self.updateSliderValues() + print(f"垂直滑块值: {value}") + + def updateSliderValues(self): + """更新滑块值显示""" + h_val = int(self.slider1['value']) + v_val = int(self.slider2['value']) + self.sliderValueText.setText(f"滑块值: H={h_val}, V={v_val}") + + def onCheckbox1Toggle(self, status): + """复选框1切换事件""" + state = "选中" if status else "未选中" + self.updateStatus(f"复选框1: {state}") + print(f"复选框1: {state}") + + def onCheckbox2Toggle(self, status): + """复选框2切换事件""" + state = "选中" if status else "未选中" + self.updateStatus(f"复选框2: {state}") + print(f"复选框2: {state}") + + def onRadioButtonSelect(self, index, status): + """单选按钮选择事件""" + if status: + self.updateStatus(f"选中单选按钮: {index + 1}") + print(f"选中单选按钮: {index + 1}") + + def onOptionMenuSelect(self, arg): + """选项菜单选择事件""" + self.updateStatus(f"选择了: {arg}") + print(f"选择了选项: {arg}") + + def updateStatus(self, message): + """更新状态显示""" + self.statusText.setText(f"状态: {message}") + + def exitApp(self): + """退出应用程序""" + print("正在退出程序...") + self.userExit() + +if __name__ == "__main__": + try: + app = GUITestApp() + app.run() + except Exception as e: + print(f"程序运行出错: {e}") sys.exit(1) \ No newline at end of file diff --git a/demo/improved_picking_test.py b/demo/improved_picking_test.py index b47c986f..4fb76f94 100644 --- a/demo/improved_picking_test.py +++ b/demo/improved_picking_test.py @@ -1,351 +1,351 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import sys -from direct.showbase.ShowBase import ShowBase -from panda3d.core import ( - Point3, Point2, Vec3, Vec4, CardMaker, CollisionTraverser, - CollisionHandlerQueue, CollisionNode, CollisionRay, CollisionSphere, - BitMask32, PerspectiveLens -) - -class ImprovedPickingTest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 相机设置 - self.cam.setPos(-3.87655, -188.38084, 82.602684) - self.cam.lookAt(0, 0, 0) - - print("=== 改进的点击检测测试 ===") - - # 创建测试对象 - self.createTestObjects() - - # 测试各种点击检测方法 - self.testAllPickingMethods() - - def createTestObjects(self): - """创建测试对象""" - self.objects = [] - - # 创建几个测试立方体 - positions = [ - (0, 10, 3, "中心立方体"), - (-5, 10, 3, "左侧立方体"), - (5, 10, 3, "右侧立方体"), - (0, 10, 8, "上方立方体"), - (0, 15, 3, "远处立方体") - ] - - colors = [ - (1, 0, 0, 1), # 红色 - (0, 1, 0, 1), # 绿色 - (0, 0, 1, 1), # 蓝色 - (1, 1, 0, 1), # 黄色 - (1, 0, 1, 1), # 紫色 - ] - - for i, ((x, y, z, name), color) in enumerate(zip(positions, colors)): - # 创建立方体 - cm = CardMaker(f"cube_{i}") - cm.setFrame(-1, 1, -1, 1) - cube = self.render.attachNewNode(cm.generate()) - cube.setPos(x, y, z) - cube.setScale(2, 2, 2) - cube.setColor(*color) - cube.setBillboardAxis() - cube.setName(name) - - # 为每个对象添加球形碰撞体(用于精确的碰撞检测) - collider = cube.attachNewNode(CollisionNode(f"cube_collider_{i}")) - collider.node().addSolid(CollisionSphere(0, 0, 0, 2.0)) - collider.node().setFromCollideMask(BitMask32.bit(0)) - - # 存储对象信息 - obj_info = { - 'node': cube, - 'name': name, - 'world_pos': Point3(x, y, z), - 'radius': 2.0, - 'collider': collider - } - self.objects.append(obj_info) - - print(f"创建 {name} 在位置 ({x}, {y}, {z})") - - # 方法1:改进的射线检测(从相机发出) - def pickByRayFromCamera(self, mouseX, mouseY): - """使用从相机发出的射线进行检测""" - print(f"\n=== 方法1: 从相机发出的射线检测 ===") - - # 获取窗口尺寸 - winWidth = self.win.getXSize() - winHeight = self.win.getYSize() - - # 转换为归一化坐标 - normalizedX = (2.0 * mouseX / winWidth) - 1.0 - normalizedY = 1.0 - (2.0 * mouseY / winHeight) - - print(f"鼠标位置: ({mouseX}, {mouseY})") - print(f"归一化坐标: ({normalizedX:.6f}, {normalizedY:.6f})") - - # 获取相机镜头 - lens = self.cam.node().getLens() - - # 计算射线在相机坐标系中的方向 - # 使用镜头的逆投影来获取正确的射线方向 - near_point = Point3() - far_point = Point3() - - if lens.extrude(Point2(normalizedX, normalizedY), near_point, far_point): - # 转换到世界坐标系 - near_world = self.cam.getRelativePoint(self.render, near_point) - far_world = self.cam.getRelativePoint(self.render, far_point) - - # 计算射线方向 - ray_origin = near_world - ray_direction = (far_world - near_world).normalized() - - print(f"射线起点 (相机近平面): {near_world}") - print(f"射线方向: {ray_direction}") - - # 手动检测与对象的交集 - return self.intersectRayWithObjects(ray_origin, ray_direction) - else: - print("无法生成射线") - return None - - def intersectRayWithObjects(self, ray_origin, ray_direction): - """手动计算射线与对象的交集""" - closest_obj = None - closest_distance = float('inf') - - for obj_info in self.objects: - # 计算射线与球体的交点 - center = obj_info['world_pos'] - radius = obj_info['radius'] - - # 射线到球心的向量 - oc = ray_origin - center - - # 二次方程系数 - a = ray_direction.dot(ray_direction) - b = 2.0 * oc.dot(ray_direction) - c = oc.dot(oc) - radius * radius - - # 判别式 - discriminant = b * b - 4 * a * c - - if discriminant >= 0: - # 有交点,计算距离 - sqrt_discriminant = discriminant ** 0.5 - t1 = (-b - sqrt_discriminant) / (2 * a) - t2 = (-b + sqrt_discriminant) / (2 * a) - - # 取最近的正值交点 - t = t1 if t1 > 0 else t2 - if t > 0: - distance = t - if distance < closest_distance: - closest_distance = distance - closest_obj = obj_info - print(f"{obj_info['name']}: 交点距离 {distance:.2f}") - else: - print(f"{obj_info['name']}: 在射线后方") - else: - print(f"{obj_info['name']}: 无交点") - - if closest_obj: - print(f"选中: {closest_obj['name']}") - else: - print("没有选中任何对象") - - return closest_obj - - # 方法2:基于屏幕投影的检测 - def pickByProjection(self, mouseX, mouseY): - """使用屏幕投影进行检测""" - print(f"\n=== 方法2: 屏幕投影检测 ===") - - closest_obj = None - closest_distance = float('inf') - - for obj_info in self.objects: - # 将3D位置投影到屏幕 - screenPos = self.worldToScreen(obj_info['world_pos']) - if screenPos: - screenX, screenY = screenPos - - # 计算屏幕半径 - screenRadius = self.getObjectScreenRadius(obj_info) - - # 计算鼠标到对象中心的距离 - dx = mouseX - screenX - dy = mouseY - screenY - distance = (dx * dx + dy * dy) ** 0.5 - - print(f"{obj_info['name']}: 屏幕位置({screenX:.1f}, {screenY:.1f}), 半径{screenRadius:.1f}, 距离{distance:.1f}") - - # 检查是否在点击范围内 - if distance <= screenRadius and distance < closest_distance: - closest_distance = distance - closest_obj = obj_info - else: - print(f"{obj_info['name']}: 不在屏幕内") - - if closest_obj: - print(f"选中: {closest_obj['name']}") - else: - print("没有选中任何对象") - - return closest_obj - - def worldToScreen(self, worldPos): - """将世界坐标转换为屏幕坐标""" - lens = self.cam.node().getLens() - camPos = self.cam.getRelativePoint(self.render, worldPos) - - screenPoint = Point2() - if lens.project(camPos, screenPoint): - winWidth = self.win.getXSize() - winHeight = self.win.getYSize() - - pixelX = (screenPoint.getX() + 1.0) * winWidth * 0.5 - pixelY = (1.0 - screenPoint.getY()) * winHeight * 0.5 - - return pixelX, pixelY - return None - - def getObjectScreenRadius(self, obj_info): - """计算对象在屏幕上的半径""" - worldPos = obj_info['world_pos'] - worldRadius = obj_info['radius'] - - centerScreen = self.worldToScreen(worldPos) - if not centerScreen: - return 0 - - edgePos = worldPos + Vec3(worldRadius, 0, 0) - edgeScreen = self.worldToScreen(edgePos) - - if edgeScreen: - return abs(edgeScreen[0] - centerScreen[0]) - return 0 - - # 方法3:混合检测(投影预选 + 射线精确) - def pickByHybrid(self, mouseX, mouseY): - """使用混合方法进行检测""" - print(f"\n=== 方法3: 混合检测(投影预选 + 射线精确)===") - - # 第一步:使用投影快速预选 - candidates = [] - - for obj_info in self.objects: - screenPos = self.worldToScreen(obj_info['world_pos']) - if screenPos: - screenX, screenY = screenPos - screenRadius = self.getObjectScreenRadius(obj_info) - - dx = mouseX - screenX - dy = mouseY - screenY - distance = (dx * dx + dy * dy) ** 0.5 - - # 使用更大的容差进行预选 - tolerance = screenRadius * 1.5 - if distance <= tolerance: - candidates.append((obj_info, distance)) - print(f"预选候选: {obj_info['name']}, 屏幕距离: {distance:.1f}") - - if not candidates: - print("投影预选: 没有候选对象") - return None - - # 第二步:对候选对象使用精确射线检测 - print("对候选对象进行精确射线检测:") - winWidth = self.win.getXSize() - winHeight = self.win.getYSize() - - normalizedX = (2.0 * mouseX / winWidth) - 1.0 - normalizedY = 1.0 - (2.0 * mouseY / winHeight) - - lens = self.cam.node().getLens() - near_point = Point3() - far_point = Point3() - - if lens.extrude(Point2(normalizedX, normalizedY), near_point, far_point): - near_world = self.cam.getRelativePoint(self.render, near_point) - far_world = self.cam.getRelativePoint(self.render, far_point) - - ray_origin = near_world - ray_direction = (far_world - near_world).normalized() - - # 只检测候选对象 - closest_obj = None - closest_distance = float('inf') - - for obj_info, _ in candidates: - center = obj_info['world_pos'] - radius = obj_info['radius'] - - oc = ray_origin - center - a = ray_direction.dot(ray_direction) - b = 2.0 * oc.dot(ray_direction) - c = oc.dot(oc) - radius * radius - - discriminant = b * b - 4 * a * c - - if discriminant >= 0: - sqrt_discriminant = discriminant ** 0.5 - t1 = (-b - sqrt_discriminant) / (2 * a) - t2 = (-b + sqrt_discriminant) / (2 * a) - - t = t1 if t1 > 0 else t2 - if t > 0 and t < closest_distance: - closest_distance = t - closest_obj = obj_info - print(f" {obj_info['name']}: 精确射线距离 {t:.2f}") - - if closest_obj: - print(f"最终选中: {closest_obj['name']}") - else: - print("精确检测: 没有有效交点") - - return closest_obj - - return None - - def testAllPickingMethods(self): - """测试所有点击检测方法""" - print(f"\n=== 测试所有点击检测方法 ===") - - # 测试点击位置(模拟点击左侧立方体附近) - test_clicks = [ - (372, 265, "左侧立方体附近"), - (399, 265, "中心立方体附近"), - (425, 265, "右侧立方体附近"), - ] - - for mouseX, mouseY, description in test_clicks: - print(f"\n{'='*60}") - print(f"测试位置: {description} ({mouseX}, {mouseY})") - print(f"{'='*60}") - - # 方法1:从相机发出的射线 - result1 = self.pickByRayFromCamera(mouseX, mouseY) - - # 方法2:屏幕投影 - result2 = self.pickByProjection(mouseX, mouseY) - - # 方法3:混合方法 - result3 = self.pickByHybrid(mouseX, mouseY) - - # 比较结果 - print(f"\n--- 结果比较 ---") - print(f"射线检测: {result1['name'] if result1 else '无'}") - print(f"投影检测: {result2['name'] if result2 else '无'}") - print(f"混合检测: {result3['name'] if result3 else '无'}") - -if __name__ == "__main__": - app = ImprovedPickingTest() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +from direct.showbase.ShowBase import ShowBase +from panda3d.core import ( + Point3, Point2, Vec3, Vec4, CardMaker, CollisionTraverser, + CollisionHandlerQueue, CollisionNode, CollisionRay, CollisionSphere, + BitMask32, PerspectiveLens +) + +class ImprovedPickingTest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 相机设置 + self.cam.setPos(-3.87655, -188.38084, 82.602684) + self.cam.lookAt(0, 0, 0) + + print("=== 改进的点击检测测试 ===") + + # 创建测试对象 + self.createTestObjects() + + # 测试各种点击检测方法 + self.testAllPickingMethods() + + def createTestObjects(self): + """创建测试对象""" + self.objects = [] + + # 创建几个测试立方体 + positions = [ + (0, 10, 3, "中心立方体"), + (-5, 10, 3, "左侧立方体"), + (5, 10, 3, "右侧立方体"), + (0, 10, 8, "上方立方体"), + (0, 15, 3, "远处立方体") + ] + + colors = [ + (1, 0, 0, 1), # 红色 + (0, 1, 0, 1), # 绿色 + (0, 0, 1, 1), # 蓝色 + (1, 1, 0, 1), # 黄色 + (1, 0, 1, 1), # 紫色 + ] + + for i, ((x, y, z, name), color) in enumerate(zip(positions, colors)): + # 创建立方体 + cm = CardMaker(f"cube_{i}") + cm.setFrame(-1, 1, -1, 1) + cube = self.render.attachNewNode(cm.generate()) + cube.setPos(x, y, z) + cube.setScale(2, 2, 2) + cube.setColor(*color) + cube.setBillboardAxis() + cube.setName(name) + + # 为每个对象添加球形碰撞体(用于精确的碰撞检测) + collider = cube.attachNewNode(CollisionNode(f"cube_collider_{i}")) + collider.node().addSolid(CollisionSphere(0, 0, 0, 2.0)) + collider.node().setFromCollideMask(BitMask32.bit(0)) + + # 存储对象信息 + obj_info = { + 'node': cube, + 'name': name, + 'world_pos': Point3(x, y, z), + 'radius': 2.0, + 'collider': collider + } + self.objects.append(obj_info) + + print(f"创建 {name} 在位置 ({x}, {y}, {z})") + + # 方法1:改进的射线检测(从相机发出) + def pickByRayFromCamera(self, mouseX, mouseY): + """使用从相机发出的射线进行检测""" + print(f"\n=== 方法1: 从相机发出的射线检测 ===") + + # 获取窗口尺寸 + winWidth = self.win.getXSize() + winHeight = self.win.getYSize() + + # 转换为归一化坐标 + normalizedX = (2.0 * mouseX / winWidth) - 1.0 + normalizedY = 1.0 - (2.0 * mouseY / winHeight) + + print(f"鼠标位置: ({mouseX}, {mouseY})") + print(f"归一化坐标: ({normalizedX:.6f}, {normalizedY:.6f})") + + # 获取相机镜头 + lens = self.cam.node().getLens() + + # 计算射线在相机坐标系中的方向 + # 使用镜头的逆投影来获取正确的射线方向 + near_point = Point3() + far_point = Point3() + + if lens.extrude(Point2(normalizedX, normalizedY), near_point, far_point): + # 转换到世界坐标系 + near_world = self.cam.getRelativePoint(self.render, near_point) + far_world = self.cam.getRelativePoint(self.render, far_point) + + # 计算射线方向 + ray_origin = near_world + ray_direction = (far_world - near_world).normalized() + + print(f"射线起点 (相机近平面): {near_world}") + print(f"射线方向: {ray_direction}") + + # 手动检测与对象的交集 + return self.intersectRayWithObjects(ray_origin, ray_direction) + else: + print("无法生成射线") + return None + + def intersectRayWithObjects(self, ray_origin, ray_direction): + """手动计算射线与对象的交集""" + closest_obj = None + closest_distance = float('inf') + + for obj_info in self.objects: + # 计算射线与球体的交点 + center = obj_info['world_pos'] + radius = obj_info['radius'] + + # 射线到球心的向量 + oc = ray_origin - center + + # 二次方程系数 + a = ray_direction.dot(ray_direction) + b = 2.0 * oc.dot(ray_direction) + c = oc.dot(oc) - radius * radius + + # 判别式 + discriminant = b * b - 4 * a * c + + if discriminant >= 0: + # 有交点,计算距离 + sqrt_discriminant = discriminant ** 0.5 + t1 = (-b - sqrt_discriminant) / (2 * a) + t2 = (-b + sqrt_discriminant) / (2 * a) + + # 取最近的正值交点 + t = t1 if t1 > 0 else t2 + if t > 0: + distance = t + if distance < closest_distance: + closest_distance = distance + closest_obj = obj_info + print(f"{obj_info['name']}: 交点距离 {distance:.2f}") + else: + print(f"{obj_info['name']}: 在射线后方") + else: + print(f"{obj_info['name']}: 无交点") + + if closest_obj: + print(f"选中: {closest_obj['name']}") + else: + print("没有选中任何对象") + + return closest_obj + + # 方法2:基于屏幕投影的检测 + def pickByProjection(self, mouseX, mouseY): + """使用屏幕投影进行检测""" + print(f"\n=== 方法2: 屏幕投影检测 ===") + + closest_obj = None + closest_distance = float('inf') + + for obj_info in self.objects: + # 将3D位置投影到屏幕 + screenPos = self.worldToScreen(obj_info['world_pos']) + if screenPos: + screenX, screenY = screenPos + + # 计算屏幕半径 + screenRadius = self.getObjectScreenRadius(obj_info) + + # 计算鼠标到对象中心的距离 + dx = mouseX - screenX + dy = mouseY - screenY + distance = (dx * dx + dy * dy) ** 0.5 + + print(f"{obj_info['name']}: 屏幕位置({screenX:.1f}, {screenY:.1f}), 半径{screenRadius:.1f}, 距离{distance:.1f}") + + # 检查是否在点击范围内 + if distance <= screenRadius and distance < closest_distance: + closest_distance = distance + closest_obj = obj_info + else: + print(f"{obj_info['name']}: 不在屏幕内") + + if closest_obj: + print(f"选中: {closest_obj['name']}") + else: + print("没有选中任何对象") + + return closest_obj + + def worldToScreen(self, worldPos): + """将世界坐标转换为屏幕坐标""" + lens = self.cam.node().getLens() + camPos = self.cam.getRelativePoint(self.render, worldPos) + + screenPoint = Point2() + if lens.project(camPos, screenPoint): + winWidth = self.win.getXSize() + winHeight = self.win.getYSize() + + pixelX = (screenPoint.getX() + 1.0) * winWidth * 0.5 + pixelY = (1.0 - screenPoint.getY()) * winHeight * 0.5 + + return pixelX, pixelY + return None + + def getObjectScreenRadius(self, obj_info): + """计算对象在屏幕上的半径""" + worldPos = obj_info['world_pos'] + worldRadius = obj_info['radius'] + + centerScreen = self.worldToScreen(worldPos) + if not centerScreen: + return 0 + + edgePos = worldPos + Vec3(worldRadius, 0, 0) + edgeScreen = self.worldToScreen(edgePos) + + if edgeScreen: + return abs(edgeScreen[0] - centerScreen[0]) + return 0 + + # 方法3:混合检测(投影预选 + 射线精确) + def pickByHybrid(self, mouseX, mouseY): + """使用混合方法进行检测""" + print(f"\n=== 方法3: 混合检测(投影预选 + 射线精确)===") + + # 第一步:使用投影快速预选 + candidates = [] + + for obj_info in self.objects: + screenPos = self.worldToScreen(obj_info['world_pos']) + if screenPos: + screenX, screenY = screenPos + screenRadius = self.getObjectScreenRadius(obj_info) + + dx = mouseX - screenX + dy = mouseY - screenY + distance = (dx * dx + dy * dy) ** 0.5 + + # 使用更大的容差进行预选 + tolerance = screenRadius * 1.5 + if distance <= tolerance: + candidates.append((obj_info, distance)) + print(f"预选候选: {obj_info['name']}, 屏幕距离: {distance:.1f}") + + if not candidates: + print("投影预选: 没有候选对象") + return None + + # 第二步:对候选对象使用精确射线检测 + print("对候选对象进行精确射线检测:") + winWidth = self.win.getXSize() + winHeight = self.win.getYSize() + + normalizedX = (2.0 * mouseX / winWidth) - 1.0 + normalizedY = 1.0 - (2.0 * mouseY / winHeight) + + lens = self.cam.node().getLens() + near_point = Point3() + far_point = Point3() + + if lens.extrude(Point2(normalizedX, normalizedY), near_point, far_point): + near_world = self.cam.getRelativePoint(self.render, near_point) + far_world = self.cam.getRelativePoint(self.render, far_point) + + ray_origin = near_world + ray_direction = (far_world - near_world).normalized() + + # 只检测候选对象 + closest_obj = None + closest_distance = float('inf') + + for obj_info, _ in candidates: + center = obj_info['world_pos'] + radius = obj_info['radius'] + + oc = ray_origin - center + a = ray_direction.dot(ray_direction) + b = 2.0 * oc.dot(ray_direction) + c = oc.dot(oc) - radius * radius + + discriminant = b * b - 4 * a * c + + if discriminant >= 0: + sqrt_discriminant = discriminant ** 0.5 + t1 = (-b - sqrt_discriminant) / (2 * a) + t2 = (-b + sqrt_discriminant) / (2 * a) + + t = t1 if t1 > 0 else t2 + if t > 0 and t < closest_distance: + closest_distance = t + closest_obj = obj_info + print(f" {obj_info['name']}: 精确射线距离 {t:.2f}") + + if closest_obj: + print(f"最终选中: {closest_obj['name']}") + else: + print("精确检测: 没有有效交点") + + return closest_obj + + return None + + def testAllPickingMethods(self): + """测试所有点击检测方法""" + print(f"\n=== 测试所有点击检测方法 ===") + + # 测试点击位置(模拟点击左侧立方体附近) + test_clicks = [ + (372, 265, "左侧立方体附近"), + (399, 265, "中心立方体附近"), + (425, 265, "右侧立方体附近"), + ] + + for mouseX, mouseY, description in test_clicks: + print(f"\n{'='*60}") + print(f"测试位置: {description} ({mouseX}, {mouseY})") + print(f"{'='*60}") + + # 方法1:从相机发出的射线 + result1 = self.pickByRayFromCamera(mouseX, mouseY) + + # 方法2:屏幕投影 + result2 = self.pickByProjection(mouseX, mouseY) + + # 方法3:混合方法 + result3 = self.pickByHybrid(mouseX, mouseY) + + # 比较结果 + print(f"\n--- 结果比较 ---") + print(f"射线检测: {result1['name'] if result1 else '无'}") + print(f"投影检测: {result2['name'] if result2 else '无'}") + print(f"混合检测: {result3['name'] if result3 else '无'}") + +if __name__ == "__main__": + app = ImprovedPickingTest() print("改进的点击检测测试完成") \ No newline at end of file diff --git a/demo/production_picking_system.py b/demo/production_picking_system.py index 787a9ab4..90904967 100644 --- a/demo/production_picking_system.py +++ b/demo/production_picking_system.py @@ -1,283 +1,283 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -生产级屏幕投影点击检测系统 -适用于Qt嵌入的Panda3D环境 -""" - -from panda3d.core import Point3, Point2, Vec3 -from typing import List, Optional, Tuple, Dict, Any -import math - -class ScreenProjectionPicker: - def __init__(self, camera, lens, window): - """ - 初始化屏幕投影拾取器 - - Args: - camera: Panda3D相机节点 - lens: 相机镜头 - window: 渲染窗口 - """ - self.camera = camera - self.lens = lens - self.window = window - self.objects = [] # 可选中的对象列表 - - def registerObject(self, node_path, name: str, priority: int = 0, - custom_radius: Optional[float] = None): - """ - 注册可选中的对象 - - Args: - node_path: Panda3D节点路径 - name: 对象名称 - priority: 选择优先级(数值越大优先级越高) - custom_radius: 自定义选择半径(世界坐标) - """ - obj_info = { - 'node': node_path, - 'name': name, - 'priority': priority, - 'custom_radius': custom_radius, - 'world_pos': node_path.getPos(), - 'bounds': node_path.getBounds() - } - self.objects.append(obj_info) - - def unregisterObject(self, node_path): - """取消注册对象""" - self.objects = [obj for obj in self.objects if obj['node'] != node_path] - - def updateObjectPositions(self): - """更新所有对象的世界位置(在对象移动后调用)""" - for obj_info in self.objects: - obj_info['world_pos'] = obj_info['node'].getPos() - obj_info['bounds'] = obj_info['node'].getBounds() - - def worldToScreen(self, world_pos: Point3) -> Optional[Tuple[float, float]]: - """ - 将世界坐标转换为屏幕像素坐标 - - Args: - world_pos: 世界坐标位置 - - Returns: - (pixel_x, pixel_y) 或 None(如果不在屏幕内) - """ - # 转换到相机坐标系 - cam_pos = self.camera.getRelativePoint(self.camera.getParent(), world_pos) - - # 投影到屏幕坐标 - screen_point = Point2() - if self.lens.project(cam_pos, screen_point): - # 转换为像素坐标 - win_width = self.window.getXSize() - win_height = self.window.getYSize() - - pixel_x = (screen_point.getX() + 1.0) * win_width * 0.5 - pixel_y = (1.0 - screen_point.getY()) * win_height * 0.5 - - return pixel_x, pixel_y - - return None - - def getObjectScreenRadius(self, obj_info: Dict) -> float: - """ - 计算对象在屏幕上的选择半径 - - Args: - obj_info: 对象信息字典 - - Returns: - 屏幕像素半径 - """ - world_pos = obj_info['world_pos'] - - # 如果有自定义半径,使用自定义半径 - if obj_info['custom_radius'] is not None: - world_radius = obj_info['custom_radius'] - else: - # 否则根据包围盒计算半径 - bounds = obj_info['bounds'] - if bounds.isEmpty(): - world_radius = 1.0 # 默认半径 - else: - # 使用包围盒的最大尺寸作为半径 - min_point = bounds.getMin() - max_point = bounds.getMax() - size = max_point - min_point - world_radius = max(size.getX(), size.getY(), size.getZ()) * 0.5 - - # 计算屏幕半径 - center_screen = self.worldToScreen(world_pos) - if not center_screen: - return 0 - - # 使用对象右侧边缘点计算屏幕半径 - edge_pos = world_pos + Vec3(world_radius, 0, 0) - edge_screen = self.worldToScreen(edge_pos) - - if edge_screen: - return abs(edge_screen[0] - center_screen[0]) - - # 如果边缘点不可见,使用距离相机的距离估算 - cam_distance = (world_pos - self.camera.getPos()).length() - if cam_distance > 0: - # 基于透视投影的简单估算 - angular_size = world_radius / cam_distance - fov = self.lens.getHfov() # 水平视场角 - win_width = self.window.getXSize() - screen_radius = angular_size * (win_width * 0.5) / math.tan(math.radians(fov * 0.5)) - return max(10.0, screen_radius) # 最小10像素 - - return 20.0 # 默认半径 - - def pickObject(self, mouse_x: int, mouse_y: int, - tolerance_multiplier: float = 1.0) -> Optional[Dict]: - """ - 在指定鼠标位置选择对象 - - Args: - mouse_x: 鼠标X坐标(像素) - mouse_y: 鼠标Y坐标(像素) - tolerance_multiplier: 容差倍数(增大可使选择更容易) - - Returns: - 选中的对象信息字典,或None - """ - candidates = [] - - for obj_info in self.objects: - # 计算对象在屏幕上的位置 - screen_pos = self.worldToScreen(obj_info['world_pos']) - if not screen_pos: - continue # 对象不在屏幕内 - - screen_x, screen_y = screen_pos - - # 计算屏幕半径 - screen_radius = self.getObjectScreenRadius(obj_info) - - # 应用容差倍数 - effective_radius = screen_radius * tolerance_multiplier - - # 计算鼠标到对象中心的距离 - dx = mouse_x - screen_x - dy = mouse_y - screen_y - distance = math.sqrt(dx * dx + dy * dy) - - # 检查是否在选择范围内 - if distance <= effective_radius: - # 计算选择分数(距离越近、优先级越高分数越高) - distance_score = 1.0 - (distance / effective_radius) - priority_score = obj_info['priority'] * 10.0 - total_score = distance_score + priority_score - - candidates.append((obj_info, distance, total_score)) - - if not candidates: - return None - - # 按分数排序,选择最高分的对象 - candidates.sort(key=lambda x: x[2], reverse=True) - return candidates[0][0] - - def pickMultipleObjects(self, mouse_x: int, mouse_y: int, - tolerance_multiplier: float = 1.0) -> List[Dict]: - """ - 在指定位置选择所有可能的对象(用于上下文菜单等) - - Args: - mouse_x: 鼠标X坐标 - mouse_y: 鼠标Y坐标 - tolerance_multiplier: 容差倍数 - - Returns: - 按优先级排序的对象列表 - """ - candidates = [] - - for obj_info in self.objects: - screen_pos = self.worldToScreen(obj_info['world_pos']) - if not screen_pos: - continue - - screen_x, screen_y = screen_pos - screen_radius = self.getObjectScreenRadius(obj_info) - effective_radius = screen_radius * tolerance_multiplier - - dx = mouse_x - screen_x - dy = mouse_y - screen_y - distance = math.sqrt(dx * dx + dy * dy) - - if distance <= effective_radius: - distance_score = 1.0 - (distance / effective_radius) - priority_score = obj_info['priority'] * 10.0 - total_score = distance_score + priority_score - - candidates.append((obj_info, distance, total_score)) - - # 按分数排序 - candidates.sort(key=lambda x: x[2], reverse=True) - return [candidate[0] for candidate in candidates] - - def getDebugInfo(self, mouse_x: int, mouse_y: int) -> str: - """获取调试信息""" - debug_lines = [f"鼠标位置: ({mouse_x}, {mouse_y})"] - debug_lines.append(f"注册对象数量: {len(self.objects)}") - debug_lines.append("") - - for obj_info in self.objects: - screen_pos = self.worldToScreen(obj_info['world_pos']) - if screen_pos: - screen_x, screen_y = screen_pos - screen_radius = self.getObjectScreenRadius(obj_info) - dx = mouse_x - screen_x - dy = mouse_y - screen_y - distance = math.sqrt(dx * dx + dy * dy) - - debug_lines.append(f"{obj_info['name']}:") - debug_lines.append(f" 世界位置: {obj_info['world_pos']}") - debug_lines.append(f" 屏幕位置: ({screen_x:.1f}, {screen_y:.1f})") - debug_lines.append(f" 屏幕半径: {screen_radius:.1f}") - debug_lines.append(f" 鼠标距离: {distance:.1f}") - debug_lines.append(f" 可选中: {'是' if distance <= screen_radius else '否'}") - debug_lines.append("") - else: - debug_lines.append(f"{obj_info['name']}: 不在屏幕内") - debug_lines.append("") - - return "\n".join(debug_lines) - -# 使用示例 -def example_usage(): - """使用示例""" - print(""" -# 在您的应用中使用屏幕投影拾取器: - -# 1. 初始化拾取器 -picker = ScreenProjectionPicker(self.cam, self.cam.node().getLens(), self.win) - -# 2. 注册可选中的对象 -picker.registerObject(cube_node, "立方体", priority=1) -picker.registerObject(sphere_node, "球体", priority=2, custom_radius=3.0) - -# 3. 在鼠标点击事件中使用 -def on_mouse_click(mouse_x, mouse_y): - selected_obj = picker.pickObject(mouse_x, mouse_y, tolerance_multiplier=1.2) - if selected_obj: - print(f"选中了: {selected_obj['name']}") - else: - print("没有选中任何对象") - -# 4. 对象移动后更新位置 -picker.updateObjectPositions() - -# 5. 获取调试信息 -debug_info = picker.getDebugInfo(mouse_x, mouse_y) -print(debug_info) - """) - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +生产级屏幕投影点击检测系统 +适用于Qt嵌入的Panda3D环境 +""" + +from panda3d.core import Point3, Point2, Vec3 +from typing import List, Optional, Tuple, Dict, Any +import math + +class ScreenProjectionPicker: + def __init__(self, camera, lens, window): + """ + 初始化屏幕投影拾取器 + + Args: + camera: Panda3D相机节点 + lens: 相机镜头 + window: 渲染窗口 + """ + self.camera = camera + self.lens = lens + self.window = window + self.objects = [] # 可选中的对象列表 + + def registerObject(self, node_path, name: str, priority: int = 0, + custom_radius: Optional[float] = None): + """ + 注册可选中的对象 + + Args: + node_path: Panda3D节点路径 + name: 对象名称 + priority: 选择优先级(数值越大优先级越高) + custom_radius: 自定义选择半径(世界坐标) + """ + obj_info = { + 'node': node_path, + 'name': name, + 'priority': priority, + 'custom_radius': custom_radius, + 'world_pos': node_path.getPos(), + 'bounds': node_path.getBounds() + } + self.objects.append(obj_info) + + def unregisterObject(self, node_path): + """取消注册对象""" + self.objects = [obj for obj in self.objects if obj['node'] != node_path] + + def updateObjectPositions(self): + """更新所有对象的世界位置(在对象移动后调用)""" + for obj_info in self.objects: + obj_info['world_pos'] = obj_info['node'].getPos() + obj_info['bounds'] = obj_info['node'].getBounds() + + def worldToScreen(self, world_pos: Point3) -> Optional[Tuple[float, float]]: + """ + 将世界坐标转换为屏幕像素坐标 + + Args: + world_pos: 世界坐标位置 + + Returns: + (pixel_x, pixel_y) 或 None(如果不在屏幕内) + """ + # 转换到相机坐标系 + cam_pos = self.camera.getRelativePoint(self.camera.getParent(), world_pos) + + # 投影到屏幕坐标 + screen_point = Point2() + if self.lens.project(cam_pos, screen_point): + # 转换为像素坐标 + win_width = self.window.getXSize() + win_height = self.window.getYSize() + + pixel_x = (screen_point.getX() + 1.0) * win_width * 0.5 + pixel_y = (1.0 - screen_point.getY()) * win_height * 0.5 + + return pixel_x, pixel_y + + return None + + def getObjectScreenRadius(self, obj_info: Dict) -> float: + """ + 计算对象在屏幕上的选择半径 + + Args: + obj_info: 对象信息字典 + + Returns: + 屏幕像素半径 + """ + world_pos = obj_info['world_pos'] + + # 如果有自定义半径,使用自定义半径 + if obj_info['custom_radius'] is not None: + world_radius = obj_info['custom_radius'] + else: + # 否则根据包围盒计算半径 + bounds = obj_info['bounds'] + if bounds.isEmpty(): + world_radius = 1.0 # 默认半径 + else: + # 使用包围盒的最大尺寸作为半径 + min_point = bounds.getMin() + max_point = bounds.getMax() + size = max_point - min_point + world_radius = max(size.getX(), size.getY(), size.getZ()) * 0.5 + + # 计算屏幕半径 + center_screen = self.worldToScreen(world_pos) + if not center_screen: + return 0 + + # 使用对象右侧边缘点计算屏幕半径 + edge_pos = world_pos + Vec3(world_radius, 0, 0) + edge_screen = self.worldToScreen(edge_pos) + + if edge_screen: + return abs(edge_screen[0] - center_screen[0]) + + # 如果边缘点不可见,使用距离相机的距离估算 + cam_distance = (world_pos - self.camera.getPos()).length() + if cam_distance > 0: + # 基于透视投影的简单估算 + angular_size = world_radius / cam_distance + fov = self.lens.getHfov() # 水平视场角 + win_width = self.window.getXSize() + screen_radius = angular_size * (win_width * 0.5) / math.tan(math.radians(fov * 0.5)) + return max(10.0, screen_radius) # 最小10像素 + + return 20.0 # 默认半径 + + def pickObject(self, mouse_x: int, mouse_y: int, + tolerance_multiplier: float = 1.0) -> Optional[Dict]: + """ + 在指定鼠标位置选择对象 + + Args: + mouse_x: 鼠标X坐标(像素) + mouse_y: 鼠标Y坐标(像素) + tolerance_multiplier: 容差倍数(增大可使选择更容易) + + Returns: + 选中的对象信息字典,或None + """ + candidates = [] + + for obj_info in self.objects: + # 计算对象在屏幕上的位置 + screen_pos = self.worldToScreen(obj_info['world_pos']) + if not screen_pos: + continue # 对象不在屏幕内 + + screen_x, screen_y = screen_pos + + # 计算屏幕半径 + screen_radius = self.getObjectScreenRadius(obj_info) + + # 应用容差倍数 + effective_radius = screen_radius * tolerance_multiplier + + # 计算鼠标到对象中心的距离 + dx = mouse_x - screen_x + dy = mouse_y - screen_y + distance = math.sqrt(dx * dx + dy * dy) + + # 检查是否在选择范围内 + if distance <= effective_radius: + # 计算选择分数(距离越近、优先级越高分数越高) + distance_score = 1.0 - (distance / effective_radius) + priority_score = obj_info['priority'] * 10.0 + total_score = distance_score + priority_score + + candidates.append((obj_info, distance, total_score)) + + if not candidates: + return None + + # 按分数排序,选择最高分的对象 + candidates.sort(key=lambda x: x[2], reverse=True) + return candidates[0][0] + + def pickMultipleObjects(self, mouse_x: int, mouse_y: int, + tolerance_multiplier: float = 1.0) -> List[Dict]: + """ + 在指定位置选择所有可能的对象(用于上下文菜单等) + + Args: + mouse_x: 鼠标X坐标 + mouse_y: 鼠标Y坐标 + tolerance_multiplier: 容差倍数 + + Returns: + 按优先级排序的对象列表 + """ + candidates = [] + + for obj_info in self.objects: + screen_pos = self.worldToScreen(obj_info['world_pos']) + if not screen_pos: + continue + + screen_x, screen_y = screen_pos + screen_radius = self.getObjectScreenRadius(obj_info) + effective_radius = screen_radius * tolerance_multiplier + + dx = mouse_x - screen_x + dy = mouse_y - screen_y + distance = math.sqrt(dx * dx + dy * dy) + + if distance <= effective_radius: + distance_score = 1.0 - (distance / effective_radius) + priority_score = obj_info['priority'] * 10.0 + total_score = distance_score + priority_score + + candidates.append((obj_info, distance, total_score)) + + # 按分数排序 + candidates.sort(key=lambda x: x[2], reverse=True) + return [candidate[0] for candidate in candidates] + + def getDebugInfo(self, mouse_x: int, mouse_y: int) -> str: + """获取调试信息""" + debug_lines = [f"鼠标位置: ({mouse_x}, {mouse_y})"] + debug_lines.append(f"注册对象数量: {len(self.objects)}") + debug_lines.append("") + + for obj_info in self.objects: + screen_pos = self.worldToScreen(obj_info['world_pos']) + if screen_pos: + screen_x, screen_y = screen_pos + screen_radius = self.getObjectScreenRadius(obj_info) + dx = mouse_x - screen_x + dy = mouse_y - screen_y + distance = math.sqrt(dx * dx + dy * dy) + + debug_lines.append(f"{obj_info['name']}:") + debug_lines.append(f" 世界位置: {obj_info['world_pos']}") + debug_lines.append(f" 屏幕位置: ({screen_x:.1f}, {screen_y:.1f})") + debug_lines.append(f" 屏幕半径: {screen_radius:.1f}") + debug_lines.append(f" 鼠标距离: {distance:.1f}") + debug_lines.append(f" 可选中: {'是' if distance <= screen_radius else '否'}") + debug_lines.append("") + else: + debug_lines.append(f"{obj_info['name']}: 不在屏幕内") + debug_lines.append("") + + return "\n".join(debug_lines) + +# 使用示例 +def example_usage(): + """使用示例""" + print(""" +# 在您的应用中使用屏幕投影拾取器: + +# 1. 初始化拾取器 +picker = ScreenProjectionPicker(self.cam, self.cam.node().getLens(), self.win) + +# 2. 注册可选中的对象 +picker.registerObject(cube_node, "立方体", priority=1) +picker.registerObject(sphere_node, "球体", priority=2, custom_radius=3.0) + +# 3. 在鼠标点击事件中使用 +def on_mouse_click(mouse_x, mouse_y): + selected_obj = picker.pickObject(mouse_x, mouse_y, tolerance_multiplier=1.2) + if selected_obj: + print(f"选中了: {selected_obj['name']}") + else: + print("没有选中任何对象") + +# 4. 对象移动后更新位置 +picker.updateObjectPositions() + +# 5. 获取调试信息 +debug_info = picker.getDebugInfo(mouse_x, mouse_y) +print(debug_info) + """) + +if __name__ == "__main__": example_usage() \ No newline at end of file diff --git a/demo/projection_picking_test.py b/demo/projection_picking_test.py index effa5930..f3b94638 100644 --- a/demo/projection_picking_test.py +++ b/demo/projection_picking_test.py @@ -1,172 +1,172 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import sys -from direct.showbase.ShowBase import ShowBase -from panda3d.core import ( - Point3, Point2, Vec3, CardMaker, CollisionNode, CollisionSphere, - BitMask32, PerspectiveLens -) - -class ProjectionPickingTest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 相机设置 - self.cam.setPos(-3.87655, -188.38084, 82.602684) - self.cam.lookAt(0, 0, 0) - - print("=== 屏幕投影点击检测测试 ===") - - # 创建测试对象 - self.createTestObjects() - - # 测试投影检测 - self.testProjectionPicking() - - def createTestObjects(self): - """创建测试对象""" - self.objects = [] - - # 创建几个测试立方体 - positions = [ - (0, 10, 3, "中心立方体"), - (-5, 10, 3, "左侧立方体"), - (5, 10, 3, "右侧立方体"), - (0, 10, 8, "上方立方体"), - (0, 15, 3, "远处立方体") - ] - - colors = [ - (1, 0, 0, 1), # 红色 - (0, 1, 0, 1), # 绿色 - (0, 0, 1, 1), # 蓝色 - (1, 1, 0, 1), # 黄色 - (1, 0, 1, 1), # 紫色 - ] - - for i, ((x, y, z, name), color) in enumerate(zip(positions, colors)): - # 创建立方体 - cm = CardMaker(f"cube_{i}") - cm.setFrame(-1, 1, -1, 1) - cube = self.render.attachNewNode(cm.generate()) - cube.setPos(x, y, z) - cube.setScale(2, 2, 2) - cube.setColor(*color) - cube.setBillboardAxis() - cube.setName(name) - - # 存储对象信息 - obj_info = { - 'node': cube, - 'name': name, - 'world_pos': Point3(x, y, z), - 'radius': 2.0 # 立方体的半径 - } - self.objects.append(obj_info) - - print(f"创建 {name} 在位置 ({x}, {y}, {z})") - - def worldToScreen(self, worldPos): - """将世界坐标转换为屏幕坐标""" - # 获取相机的投影矩阵和模型视图矩阵 - lens = self.cam.node().getLens() - - # 将世界坐标转换到相机坐标系 - camPos = self.cam.getRelativePoint(self.render, worldPos) - - # 使用镜头投影到屏幕坐标 - screenPoint = Point2() - if lens.project(camPos, screenPoint): - # 转换为窗口像素坐标 - winWidth = self.win.getXSize() - winHeight = self.win.getYSize() - - # 归一化坐标转换为像素坐标 - pixelX = (screenPoint.getX() + 1.0) * winWidth * 0.5 - pixelY = (1.0 - screenPoint.getY()) * winHeight * 0.5 - - return pixelX, pixelY, True - else: - return 0, 0, False - - def getObjectScreenRadius(self, obj_info): - """计算对象在屏幕上的半径""" - worldPos = obj_info['world_pos'] - worldRadius = obj_info['radius'] - - # 计算对象中心在屏幕上的位置 - centerX, centerY, visible = self.worldToScreen(worldPos) - if not visible: - return 0 - - # 计算对象边缘点在屏幕上的位置 - # 使用对象右侧边缘点 - edgePoint = worldPos + Vec3(worldRadius, 0, 0) - edgeX, edgeY, edge_visible = self.worldToScreen(edgePoint) - - if edge_visible: - # 计算屏幕半径 - screenRadius = abs(edgeX - centerX) - return screenRadius - return 0 - - def pickObjectByProjection(self, mouseX, mouseY): - """使用投影方法选择对象""" - print(f"\n=== 投影检测: 鼠标位置 ({mouseX}, {mouseY}) ===") - - closest_obj = None - closest_distance = float('inf') - - for obj_info in self.objects: - # 计算对象在屏幕上的位置 - screenX, screenY, visible = self.worldToScreen(obj_info['world_pos']) - - if not visible: - print(f"{obj_info['name']}: 不可见") - continue - - # 计算屏幕半径 - screenRadius = self.getObjectScreenRadius(obj_info) - - # 计算鼠标到对象中心的距离 - dx = mouseX - screenX - dy = mouseY - screenY - distance = (dx * dx + dy * dy) ** 0.5 - - print(f"{obj_info['name']}: 屏幕位置({screenX:.1f}, {screenY:.1f}), 半径{screenRadius:.1f}, 距离{distance:.1f}") - - # 检查是否在点击范围内 - if distance <= screenRadius: - if distance < closest_distance: - closest_distance = distance - closest_obj = obj_info - print(f" -> 在点击范围内!") - - if closest_obj: - print(f"\n选中对象: {closest_obj['name']}") - return closest_obj - else: - print(f"\n没有选中任何对象") - return None - - def testProjectionPicking(self): - """测试投影点击检测""" - print(f"\n=== 投影点击检测测试 ===") - - # 模拟不同的点击位置 - test_clicks = [ - (619, 362, "屏幕中心"), - (400, 362, "屏幕偏左"), - (800, 362, "屏幕偏右"), - (619, 200, "屏幕上方"), - (619, 500, "屏幕下方"), - ] - - for mouseX, mouseY, description in test_clicks: - print(f"\n--- 测试 {description}: ({mouseX}, {mouseY}) ---") - selected = self.pickObjectByProjection(mouseX, mouseY) - -if __name__ == "__main__": - app = ProjectionPickingTest() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +from direct.showbase.ShowBase import ShowBase +from panda3d.core import ( + Point3, Point2, Vec3, CardMaker, CollisionNode, CollisionSphere, + BitMask32, PerspectiveLens +) + +class ProjectionPickingTest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 相机设置 + self.cam.setPos(-3.87655, -188.38084, 82.602684) + self.cam.lookAt(0, 0, 0) + + print("=== 屏幕投影点击检测测试 ===") + + # 创建测试对象 + self.createTestObjects() + + # 测试投影检测 + self.testProjectionPicking() + + def createTestObjects(self): + """创建测试对象""" + self.objects = [] + + # 创建几个测试立方体 + positions = [ + (0, 10, 3, "中心立方体"), + (-5, 10, 3, "左侧立方体"), + (5, 10, 3, "右侧立方体"), + (0, 10, 8, "上方立方体"), + (0, 15, 3, "远处立方体") + ] + + colors = [ + (1, 0, 0, 1), # 红色 + (0, 1, 0, 1), # 绿色 + (0, 0, 1, 1), # 蓝色 + (1, 1, 0, 1), # 黄色 + (1, 0, 1, 1), # 紫色 + ] + + for i, ((x, y, z, name), color) in enumerate(zip(positions, colors)): + # 创建立方体 + cm = CardMaker(f"cube_{i}") + cm.setFrame(-1, 1, -1, 1) + cube = self.render.attachNewNode(cm.generate()) + cube.setPos(x, y, z) + cube.setScale(2, 2, 2) + cube.setColor(*color) + cube.setBillboardAxis() + cube.setName(name) + + # 存储对象信息 + obj_info = { + 'node': cube, + 'name': name, + 'world_pos': Point3(x, y, z), + 'radius': 2.0 # 立方体的半径 + } + self.objects.append(obj_info) + + print(f"创建 {name} 在位置 ({x}, {y}, {z})") + + def worldToScreen(self, worldPos): + """将世界坐标转换为屏幕坐标""" + # 获取相机的投影矩阵和模型视图矩阵 + lens = self.cam.node().getLens() + + # 将世界坐标转换到相机坐标系 + camPos = self.cam.getRelativePoint(self.render, worldPos) + + # 使用镜头投影到屏幕坐标 + screenPoint = Point2() + if lens.project(camPos, screenPoint): + # 转换为窗口像素坐标 + winWidth = self.win.getXSize() + winHeight = self.win.getYSize() + + # 归一化坐标转换为像素坐标 + pixelX = (screenPoint.getX() + 1.0) * winWidth * 0.5 + pixelY = (1.0 - screenPoint.getY()) * winHeight * 0.5 + + return pixelX, pixelY, True + else: + return 0, 0, False + + def getObjectScreenRadius(self, obj_info): + """计算对象在屏幕上的半径""" + worldPos = obj_info['world_pos'] + worldRadius = obj_info['radius'] + + # 计算对象中心在屏幕上的位置 + centerX, centerY, visible = self.worldToScreen(worldPos) + if not visible: + return 0 + + # 计算对象边缘点在屏幕上的位置 + # 使用对象右侧边缘点 + edgePoint = worldPos + Vec3(worldRadius, 0, 0) + edgeX, edgeY, edge_visible = self.worldToScreen(edgePoint) + + if edge_visible: + # 计算屏幕半径 + screenRadius = abs(edgeX - centerX) + return screenRadius + return 0 + + def pickObjectByProjection(self, mouseX, mouseY): + """使用投影方法选择对象""" + print(f"\n=== 投影检测: 鼠标位置 ({mouseX}, {mouseY}) ===") + + closest_obj = None + closest_distance = float('inf') + + for obj_info in self.objects: + # 计算对象在屏幕上的位置 + screenX, screenY, visible = self.worldToScreen(obj_info['world_pos']) + + if not visible: + print(f"{obj_info['name']}: 不可见") + continue + + # 计算屏幕半径 + screenRadius = self.getObjectScreenRadius(obj_info) + + # 计算鼠标到对象中心的距离 + dx = mouseX - screenX + dy = mouseY - screenY + distance = (dx * dx + dy * dy) ** 0.5 + + print(f"{obj_info['name']}: 屏幕位置({screenX:.1f}, {screenY:.1f}), 半径{screenRadius:.1f}, 距离{distance:.1f}") + + # 检查是否在点击范围内 + if distance <= screenRadius: + if distance < closest_distance: + closest_distance = distance + closest_obj = obj_info + print(f" -> 在点击范围内!") + + if closest_obj: + print(f"\n选中对象: {closest_obj['name']}") + return closest_obj + else: + print(f"\n没有选中任何对象") + return None + + def testProjectionPicking(self): + """测试投影点击检测""" + print(f"\n=== 投影点击检测测试 ===") + + # 模拟不同的点击位置 + test_clicks = [ + (619, 362, "屏幕中心"), + (400, 362, "屏幕偏左"), + (800, 362, "屏幕偏右"), + (619, 200, "屏幕上方"), + (619, 500, "屏幕下方"), + ] + + for mouseX, mouseY, description in test_clicks: + print(f"\n--- 测试 {description}: ({mouseX}, {mouseY}) ---") + selected = self.pickObjectByProjection(mouseX, mouseY) + +if __name__ == "__main__": + app = ProjectionPickingTest() print("投影检测测试完成") \ No newline at end of file diff --git a/demo/quick_gui_test.py b/demo/quick_gui_test.py index b320b5f8..dc0db4e0 100644 --- a/demo/quick_gui_test.py +++ b/demo/quick_gui_test.py @@ -1,139 +1,139 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -快速GUI测试 -验证主程序中的GUI修复是否有效 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -from direct.showbase.ShowBase import ShowBase -from direct.gui.DirectGui import * -from direct.gui.OnscreenText import OnscreenText -from panda3d.core import * - -# 配置Panda3D -loadPrcFileData("", """ - win-size 800 600 - window-title Quick GUI Test -""") - -class QuickGUITest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - print("=== 快速GUI测试 ===") - - # 设置背景色 - self.setBackgroundColor(0.2, 0.2, 0.3) - - # 调整相机位置 - self.cam.setPos(0, -10, 5) - self.cam.lookAt(0, 0, 0) - - self.gui_elements = [] - - # 尝试加载中文字体 - try: - self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') - except: - self.chinese_font = None - - # 创建标题 - self.title = OnscreenText( - text="快速GUI测试 - 验证修复效果", - pos=(0, 0.9), - scale=0.08, - fg=(1, 1, 1, 1), - align=TextNode.ACenter, - font=self.chinese_font if self.chinese_font else None - ) - - # 立即创建GUI元素测试 - self.createTestGUI() - - def createTestGUI(self): - """创建测试GUI元素""" - print("\n创建测试GUI元素...") - - # 使用和主程序相同的方法创建GUI - # 测试不同的坐标值 - test_coords = [ - (0, 0, 0), # 中心 - (-10, 0, 0), # 左侧 - (10, 0, 0), # 右侧 - (0, 0, 10), # 上方 - (0, 0, -10), # 下方 - ] - - print("使用新的0.05缩放比例...") - - # 创建按钮测试 - for i, pos in enumerate(test_coords): - gui_pos = (pos[0] * 0.05, 0, pos[2] * 0.05) - - button = DirectButton( - text=f"按钮{i+1}", - pos=gui_pos, - scale=0.08, - frameColor=(0.2, 0.6, 0.8, 1), - text_font=self.chinese_font if self.chinese_font else None, - rolloverSound=None, - clickSound=None - ) - - self.gui_elements.append(button) - print(f"按钮{i+1}: 逻辑坐标{pos} -> 屏幕坐标{gui_pos}") - - # 创建标签测试 - label_coords = [ - (0, 0, -15), # 下方 - (-15, 0, -5), # 左下 - (15, 0, -5), # 右下 - ] - - for i, pos in enumerate(label_coords): - gui_pos = (pos[0] * 0.05, 0, pos[2] * 0.05) - - label = DirectLabel( - text=f"标签{i+1}", - pos=gui_pos, - scale=0.06, - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 1, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - self.gui_elements.append(label) - print(f"标签{i+1}: 逻辑坐标{pos} -> 屏幕坐标{gui_pos}") - - # 创建3D元素作为对比 - textNode = TextNode('3d-text') - textNode.setText("3D文本对比") - textNode.setAlign(TextNode.ACenter) - if self.chinese_font: - textNode.setFont(self.chinese_font) - - text3D = self.render.attachNewNode(textNode) - text3D.setPos(0, 5, 2) - text3D.setScale(0.5) - text3D.setColor(1, 1, 0, 1) - text3D.setBillboardAxis() - - print(f"\n创建完成!GUI元素总数: {len(self.gui_elements)}") - print("如果你能看到按钮和标签,说明修复成功!") - print("如果只能看到黄色的3D文本,说明2D GUI仍有问题。") - -def main(): - """主函数""" - print("启动快速GUI测试...") - - # 创建并运行应用 - app = QuickGUITest() - app.run() - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +快速GUI测试 +验证主程序中的GUI修复是否有效 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +from direct.showbase.ShowBase import ShowBase +from direct.gui.DirectGui import * +from direct.gui.OnscreenText import OnscreenText +from panda3d.core import * + +# 配置Panda3D +loadPrcFileData("", """ + win-size 800 600 + window-title Quick GUI Test +""") + +class QuickGUITest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + print("=== 快速GUI测试 ===") + + # 设置背景色 + self.setBackgroundColor(0.2, 0.2, 0.3) + + # 调整相机位置 + self.cam.setPos(0, -10, 5) + self.cam.lookAt(0, 0, 0) + + self.gui_elements = [] + + # 尝试加载中文字体 + try: + self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') + except: + self.chinese_font = None + + # 创建标题 + self.title = OnscreenText( + text="快速GUI测试 - 验证修复效果", + pos=(0, 0.9), + scale=0.08, + fg=(1, 1, 1, 1), + align=TextNode.ACenter, + font=self.chinese_font if self.chinese_font else None + ) + + # 立即创建GUI元素测试 + self.createTestGUI() + + def createTestGUI(self): + """创建测试GUI元素""" + print("\n创建测试GUI元素...") + + # 使用和主程序相同的方法创建GUI + # 测试不同的坐标值 + test_coords = [ + (0, 0, 0), # 中心 + (-10, 0, 0), # 左侧 + (10, 0, 0), # 右侧 + (0, 0, 10), # 上方 + (0, 0, -10), # 下方 + ] + + print("使用新的0.05缩放比例...") + + # 创建按钮测试 + for i, pos in enumerate(test_coords): + gui_pos = (pos[0] * 0.05, 0, pos[2] * 0.05) + + button = DirectButton( + text=f"按钮{i+1}", + pos=gui_pos, + scale=0.08, + frameColor=(0.2, 0.6, 0.8, 1), + text_font=self.chinese_font if self.chinese_font else None, + rolloverSound=None, + clickSound=None + ) + + self.gui_elements.append(button) + print(f"按钮{i+1}: 逻辑坐标{pos} -> 屏幕坐标{gui_pos}") + + # 创建标签测试 + label_coords = [ + (0, 0, -15), # 下方 + (-15, 0, -5), # 左下 + (15, 0, -5), # 右下 + ] + + for i, pos in enumerate(label_coords): + gui_pos = (pos[0] * 0.05, 0, pos[2] * 0.05) + + label = DirectLabel( + text=f"标签{i+1}", + pos=gui_pos, + scale=0.06, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + self.gui_elements.append(label) + print(f"标签{i+1}: 逻辑坐标{pos} -> 屏幕坐标{gui_pos}") + + # 创建3D元素作为对比 + textNode = TextNode('3d-text') + textNode.setText("3D文本对比") + textNode.setAlign(TextNode.ACenter) + if self.chinese_font: + textNode.setFont(self.chinese_font) + + text3D = self.render.attachNewNode(textNode) + text3D.setPos(0, 5, 2) + text3D.setScale(0.5) + text3D.setColor(1, 1, 0, 1) + text3D.setBillboardAxis() + + print(f"\n创建完成!GUI元素总数: {len(self.gui_elements)}") + print("如果你能看到按钮和标签,说明修复成功!") + print("如果只能看到黄色的3D文本,说明2D GUI仍有问题。") + +def main(): + """主函数""" + print("启动快速GUI测试...") + + # 创建并运行应用 + app = QuickGUITest() + app.run() + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/demo/quick_scale_test.py b/demo/quick_scale_test.py index 5ff8f8a9..c7f64b5f 100644 --- a/demo/quick_scale_test.py +++ b/demo/quick_scale_test.py @@ -1,204 +1,204 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -快速缩放测试脚本 - 验证FBX缩放标准化功能 - -测试用例: -1. 模拟带有大缩放值的FBX结构 -2. 验证智能标准化功能 -3. 对比修复前后的效果 -""" - -import sys -import os - -# 添加主目录到Python路径 -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - -from panda3d.core import Vec3 -from main import MyWorld - - -def test_scale_normalization(): - """测试缩放标准化功能""" - print("🧪 FBX缩放标准化功能测试") - print("="*50) - - # 创建测试世界 - world = MyWorld() - - # 创建模拟的FBX模型结构 - test_model = create_mock_fbx_model(world) - - print("\n📋 测试前的模型结构:") - print_model_hierarchy(test_model) - - # 测试缩放标准化 - print("\n🔧 执行缩放标准化...") - world.scene_manager._normalizeModelScales(test_model) - - print("\n📋 测试后的模型结构:") - print_model_hierarchy(test_model) - - # 验证结果 - verify_normalization_results(test_model) - - print("\n✅ 测试完成!") - - -def create_mock_fbx_model(world): - """创建模拟的FBX模型结构""" - print("🏗️ 创建模拟FBX模型结构...") - - # 创建根节点 - root = world.render.attachNewNode("MockFBXModel") - root.setScale(1.0, 1.0, 1.0) # 根节点正常缩放 - - # 创建子节点(模拟FBX的大缩放值) - child1 = root.attachNewNode("Mesh001") - child1.setScale(100.0, 100.0, 100.0) # 典型的FBX大缩放 - - child2 = root.attachNewNode("Mesh002") - child2.setScale(100.0, 100.0, 100.0) # 另一个大缩放 - - child3 = root.attachNewNode("Bone001") - child3.setScale(1.0, 1.0, 1.0) # 正常缩放的骨骼 - - # 创建孙子节点 - grandchild1 = child1.attachNewNode("SubMesh001") - grandchild1.setScale(1.0, 1.0, 1.0) # 正常缩放 - - grandchild2 = child2.attachNewNode("SubMesh002") - grandchild2.setScale(0.5, 0.5, 0.5) # 小缩放 - - # 创建一个异常的大缩放孙子节点 - grandchild3 = child3.attachNewNode("BigScale") - grandchild3.setScale(50.0, 50.0, 50.0) # 另一种大缩放 - - print(f" ✓ 创建了包含 {count_all_nodes(root)} 个节点的模型") - return root - - -def count_all_nodes(node): - """递归计算节点总数""" - count = 1 - for i in range(node.getNumChildren()): - count += count_all_nodes(node.getChild(i)) - return count - - -def print_model_hierarchy(model, depth=0): - """打印模型层级结构""" - indent = " " * depth - scale = model.getScale() - max_scale = max(abs(scale.x), abs(scale.y), abs(scale.z)) - - # 标记缩放状态 - if max_scale > 10: - status = "🔴大缩放" - elif max_scale < 1: - status = "🟡小缩放" - else: - status = "🟢正常" - - print(f"{indent}📦 {model.getName()}: {scale} {status}") - - # 递归打印子节点 - for i in range(model.getNumChildren()): - child = model.getChild(i) - print_model_hierarchy(child, depth + 1) - - -def verify_normalization_results(model): - """验证标准化结果""" - print("\n🔍 验证标准化结果:") - - # 收集所有节点的缩放信息 - all_scales = [] - collect_all_scales(model, all_scales) - - # 统计分析 - large_scales = [s for s in all_scales if max(abs(s.x), abs(s.y), abs(s.z)) > 10] - normal_scales = [s for s in all_scales if 0.1 <= max(abs(s.x), abs(s.y), abs(s.z)) <= 10] - small_scales = [s for s in all_scales if max(abs(s.x), abs(s.y), abs(s.z)) < 0.1] - - print(f" 总节点数: {len(all_scales)}") - print(f" 大缩放节点: {len(large_scales)} 个") - print(f" 正常缩放节点: {len(normal_scales)} 个") - print(f" 小缩放节点: {len(small_scales)} 个") - - # 验证结果 - if len(large_scales) == 0: - print(" ✅ 成功:没有大缩放节点残留") - else: - print(f" ❌ 失败:仍有 {len(large_scales)} 个大缩放节点") - - if len(normal_scales) >= len(all_scales) * 0.7: # 至少70%是正常缩放 - print(" ✅ 成功:大部分节点缩放正常化") - else: - print(" ⚠️ 警告:正常缩放节点比例较低") - - -def collect_all_scales(node, scales_list): - """递归收集所有节点的缩放""" - scales_list.append(node.getScale()) - - for i in range(node.getNumChildren()): - child = node.getChild(i) - collect_all_scales(child, scales_list) - - -def test_scale_detection(): - """测试缩放检测算法""" - print("\n🔬 测试缩放检测算法:") - - world = MyWorld() - scene_manager = world.scene_manager - - # 创建测试数据 - test_scales = [ - {'scale': Vec3(100, 100, 100), 'name': 'Mesh001'}, - {'scale': Vec3(100, 100, 100), 'name': 'Mesh002'}, - {'scale': Vec3(100, 100, 100), 'name': 'Mesh003'}, - {'scale': Vec3(1, 1, 1), 'name': 'Bone001'}, - {'scale': Vec3(50, 50, 50), 'name': 'Special'}, - {'scale': Vec3(1, 1, 1), 'name': 'Normal001'}, - ] - - # 模拟检测过程 - large_scales = [info for info in test_scales if max(abs(info['scale'].x), abs(info['scale'].y), abs(info['scale'].z)) > 10] - - print(f" 发现 {len(large_scales)} 个大缩放节点:") - for info in large_scales: - max_scale = max(abs(info['scale'].x), abs(info['scale'].y), abs(info['scale'].z)) - print(f" {info['name']}: {max_scale}") - - # 测试常见缩放值检测 - common_scale = scene_manager._findCommonLargeScale(large_scales) - print(f" 检测到常见大缩放值: {common_scale}") - - if common_scale: - normalize_factor = 1.0 / common_scale - print(f" 计算的标准化因子: {normalize_factor}") - print(f" 标准化后的示例: {100.0 * normalize_factor}") - - -if __name__ == "__main__": - try: - print("🚀 启动FBX缩放标准化测试") - print("\n" + "="*60) - - # 运行主要测试 - test_scale_normalization() - - # 运行算法测试 - test_scale_detection() - - print("\n" + "="*60) - print("🎉 所有测试完成!") - - except Exception as e: - print(f"❌ 测试失败: {str(e)}") - import traceback +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +快速缩放测试脚本 - 验证FBX缩放标准化功能 + +测试用例: +1. 模拟带有大缩放值的FBX结构 +2. 验证智能标准化功能 +3. 对比修复前后的效果 +""" + +import sys +import os + +# 添加主目录到Python路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from panda3d.core import Vec3 +from main import MyWorld + + +def test_scale_normalization(): + """测试缩放标准化功能""" + print("🧪 FBX缩放标准化功能测试") + print("="*50) + + # 创建测试世界 + world = MyWorld() + + # 创建模拟的FBX模型结构 + test_model = create_mock_fbx_model(world) + + print("\n📋 测试前的模型结构:") + print_model_hierarchy(test_model) + + # 测试缩放标准化 + print("\n🔧 执行缩放标准化...") + world.scene_manager._normalizeModelScales(test_model) + + print("\n📋 测试后的模型结构:") + print_model_hierarchy(test_model) + + # 验证结果 + verify_normalization_results(test_model) + + print("\n✅ 测试完成!") + + +def create_mock_fbx_model(world): + """创建模拟的FBX模型结构""" + print("🏗️ 创建模拟FBX模型结构...") + + # 创建根节点 + root = world.render.attachNewNode("MockFBXModel") + root.setScale(1.0, 1.0, 1.0) # 根节点正常缩放 + + # 创建子节点(模拟FBX的大缩放值) + child1 = root.attachNewNode("Mesh001") + child1.setScale(100.0, 100.0, 100.0) # 典型的FBX大缩放 + + child2 = root.attachNewNode("Mesh002") + child2.setScale(100.0, 100.0, 100.0) # 另一个大缩放 + + child3 = root.attachNewNode("Bone001") + child3.setScale(1.0, 1.0, 1.0) # 正常缩放的骨骼 + + # 创建孙子节点 + grandchild1 = child1.attachNewNode("SubMesh001") + grandchild1.setScale(1.0, 1.0, 1.0) # 正常缩放 + + grandchild2 = child2.attachNewNode("SubMesh002") + grandchild2.setScale(0.5, 0.5, 0.5) # 小缩放 + + # 创建一个异常的大缩放孙子节点 + grandchild3 = child3.attachNewNode("BigScale") + grandchild3.setScale(50.0, 50.0, 50.0) # 另一种大缩放 + + print(f" ✓ 创建了包含 {count_all_nodes(root)} 个节点的模型") + return root + + +def count_all_nodes(node): + """递归计算节点总数""" + count = 1 + for i in range(node.getNumChildren()): + count += count_all_nodes(node.getChild(i)) + return count + + +def print_model_hierarchy(model, depth=0): + """打印模型层级结构""" + indent = " " * depth + scale = model.getScale() + max_scale = max(abs(scale.x), abs(scale.y), abs(scale.z)) + + # 标记缩放状态 + if max_scale > 10: + status = "🔴大缩放" + elif max_scale < 1: + status = "🟡小缩放" + else: + status = "🟢正常" + + print(f"{indent}📦 {model.getName()}: {scale} {status}") + + # 递归打印子节点 + for i in range(model.getNumChildren()): + child = model.getChild(i) + print_model_hierarchy(child, depth + 1) + + +def verify_normalization_results(model): + """验证标准化结果""" + print("\n🔍 验证标准化结果:") + + # 收集所有节点的缩放信息 + all_scales = [] + collect_all_scales(model, all_scales) + + # 统计分析 + large_scales = [s for s in all_scales if max(abs(s.x), abs(s.y), abs(s.z)) > 10] + normal_scales = [s for s in all_scales if 0.1 <= max(abs(s.x), abs(s.y), abs(s.z)) <= 10] + small_scales = [s for s in all_scales if max(abs(s.x), abs(s.y), abs(s.z)) < 0.1] + + print(f" 总节点数: {len(all_scales)}") + print(f" 大缩放节点: {len(large_scales)} 个") + print(f" 正常缩放节点: {len(normal_scales)} 个") + print(f" 小缩放节点: {len(small_scales)} 个") + + # 验证结果 + if len(large_scales) == 0: + print(" ✅ 成功:没有大缩放节点残留") + else: + print(f" ❌ 失败:仍有 {len(large_scales)} 个大缩放节点") + + if len(normal_scales) >= len(all_scales) * 0.7: # 至少70%是正常缩放 + print(" ✅ 成功:大部分节点缩放正常化") + else: + print(" ⚠️ 警告:正常缩放节点比例较低") + + +def collect_all_scales(node, scales_list): + """递归收集所有节点的缩放""" + scales_list.append(node.getScale()) + + for i in range(node.getNumChildren()): + child = node.getChild(i) + collect_all_scales(child, scales_list) + + +def test_scale_detection(): + """测试缩放检测算法""" + print("\n🔬 测试缩放检测算法:") + + world = MyWorld() + scene_manager = world.scene_manager + + # 创建测试数据 + test_scales = [ + {'scale': Vec3(100, 100, 100), 'name': 'Mesh001'}, + {'scale': Vec3(100, 100, 100), 'name': 'Mesh002'}, + {'scale': Vec3(100, 100, 100), 'name': 'Mesh003'}, + {'scale': Vec3(1, 1, 1), 'name': 'Bone001'}, + {'scale': Vec3(50, 50, 50), 'name': 'Special'}, + {'scale': Vec3(1, 1, 1), 'name': 'Normal001'}, + ] + + # 模拟检测过程 + large_scales = [info for info in test_scales if max(abs(info['scale'].x), abs(info['scale'].y), abs(info['scale'].z)) > 10] + + print(f" 发现 {len(large_scales)} 个大缩放节点:") + for info in large_scales: + max_scale = max(abs(info['scale'].x), abs(info['scale'].y), abs(info['scale'].z)) + print(f" {info['name']}: {max_scale}") + + # 测试常见缩放值检测 + common_scale = scene_manager._findCommonLargeScale(large_scales) + print(f" 检测到常见大缩放值: {common_scale}") + + if common_scale: + normalize_factor = 1.0 / common_scale + print(f" 计算的标准化因子: {normalize_factor}") + print(f" 标准化后的示例: {100.0 * normalize_factor}") + + +if __name__ == "__main__": + try: + print("🚀 启动FBX缩放标准化测试") + print("\n" + "="*60) + + # 运行主要测试 + test_scale_normalization() + + # 运行算法测试 + test_scale_detection() + + print("\n" + "="*60) + print("🎉 所有测试完成!") + + except Exception as e: + print(f"❌ 测试失败: {str(e)}") + import traceback traceback.print_exc() \ No newline at end of file diff --git a/demo/quick_script_test.py b/demo/quick_script_test.py index a0d1b48d..4d3d4736 100644 --- a/demo/quick_script_test.py +++ b/demo/quick_script_test.py @@ -1,170 +1,170 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -脚本系统快速测试 -验证脚本系统的基本功能是否正常工作 -""" - -import sys -import os -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -def quick_test(): - """快速测试脚本系统""" - print("=== 脚本系统快速测试 ===\n") - - try: - # 1. 导入主程序 - print("1. 导入主程序...") - from main import MyWorld - print("✓ 主程序导入成功") - - # 2. 创建世界实例 - print("\n2. 创建世界实例...") - world = MyWorld() - print("✓ 世界实例创建成功") - - # 3. 检查脚本管理器 - print("\n3. 检查脚本管理器...") - if hasattr(world, 'script_manager'): - print("✓ 脚本管理器存在") - print(f" - 脚本引擎: {world.script_manager.engine}") - print(f" - 脚本加载器: {world.script_manager.loader}") - print(f" - 脚本API: {world.script_manager.api}") - else: - print("✗ 脚本管理器不存在") - return False - - # 4. 测试脚本目录创建 - print("\n4. 测试脚本目录...") - scripts_dir = world.script_manager.scripts_directory - if os.path.exists(scripts_dir): - print(f"✓ 脚本目录存在: {scripts_dir}") - - # 列出目录中的文件 - files = os.listdir(scripts_dir) - print(f" - 目录中的文件: {files}") - else: - print(f"✗ 脚本目录不存在: {scripts_dir}") - - # 5. 测试脚本加载 - print("\n5. 测试脚本加载...") - world.loadAllScripts() - available_scripts = world.getAvailableScripts() - print(f"✓ 可用脚本: {available_scripts}") - - # 6. 测试对象创建和脚本挂载 - print("\n6. 测试脚本挂载...") - test_object = world.render.attachNewNode("TestObject") - print(f"✓ 创建测试对象: {test_object.getName()}") - - if "ExampleScript" in available_scripts: - script_comp = world.addScript(test_object, "ExampleScript") - if script_comp: - print("✓ 脚本挂载成功") - print(f" - 脚本类型: {script_comp.script_instance.__class__.__name__}") - print(f" - 脚本启用: {script_comp.enabled}") - else: - print("✗ 脚本挂载失败") - else: - print("! 没有ExampleScript可用于测试") - - # 7. 测试脚本系统状态 - print("\n7. 脚本系统状态...") - engine = world.script_manager.engine - print(f" - 脚本引擎运行: {engine.update_task is not None}") - print(f" - 脚本组件数量: {len(engine.script_components)}") - print(f" - 有脚本的对象数量: {len(world.script_manager.object_scripts)}") - - # 8. 测试脚本创建 - print("\n8. 测试脚本创建...") - new_script_path = world.createScript("test_quick_script", "basic") - print(f"✓ 创建新脚本: {new_script_path}") - - if os.path.exists(new_script_path): - print("✓ 脚本文件创建成功") - else: - print("✗ 脚本文件创建失败") - - print("\n=== 快速测试完成 ===") - print("✓ 脚本系统基本功能正常") - return True - - except Exception as e: - print(f"\n✗ 测试过程中出现错误: {e}") - import traceback - traceback.print_exc() - return False - - -def test_script_execution(): - """测试脚本执行""" - print("\n=== 脚本执行测试 ===") - - try: - from main import MyWorld - - world = MyWorld() - world.loadAllScripts() - - # 创建测试对象 - test_obj = world.render.attachNewNode("ExecutionTest") - test_obj.setPos(0, 10, 0) - - # 添加脚本 - available_scripts = world.getAvailableScripts() - if available_scripts: - script_name = available_scripts[0] - script_comp = world.addScript(test_obj, script_name) - - if script_comp: - print(f"✓ 添加脚本: {script_name}") - - # 手动触发脚本生命周期 - print("\n模拟脚本执行...") - - # Start - if not script_comp._started: - script_comp.start() - print("✓ 调用脚本start()方法") - - # Update几次 - for i in range(3): - script_comp.update(0.016) # 约60FPS - print(f"✓ 调用脚本update()方法 ({i+1}/3)") - - # 禁用/启用测试 - script_comp.set_enabled(False) - print("✓ 禁用脚本") - - script_comp.set_enabled(True) - print("✓ 重新启用脚本") - - # 销毁 - script_comp.destroy() - print("✓ 销毁脚本") - - print("\n✓ 脚本执行测试完成") - else: - print("✗ 脚本添加失败") - else: - print("! 没有可用脚本进行测试") - - except Exception as e: - print(f"✗ 脚本执行测试失败: {e}") - import traceback - traceback.print_exc() - - -if __name__ == "__main__": - # 运行快速测试 - success = quick_test() - - if success: - # 如果基本测试通过,运行执行测试 - test_script_execution() - else: - print("\n基本测试失败,跳过执行测试") - +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +脚本系统快速测试 +验证脚本系统的基本功能是否正常工作 +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +def quick_test(): + """快速测试脚本系统""" + print("=== 脚本系统快速测试 ===\n") + + try: + # 1. 导入主程序 + print("1. 导入主程序...") + from main import MyWorld + print("✓ 主程序导入成功") + + # 2. 创建世界实例 + print("\n2. 创建世界实例...") + world = MyWorld() + print("✓ 世界实例创建成功") + + # 3. 检查脚本管理器 + print("\n3. 检查脚本管理器...") + if hasattr(world, 'script_manager'): + print("✓ 脚本管理器存在") + print(f" - 脚本引擎: {world.script_manager.engine}") + print(f" - 脚本加载器: {world.script_manager.loader}") + print(f" - 脚本API: {world.script_manager.api}") + else: + print("✗ 脚本管理器不存在") + return False + + # 4. 测试脚本目录创建 + print("\n4. 测试脚本目录...") + scripts_dir = world.script_manager.scripts_directory + if os.path.exists(scripts_dir): + print(f"✓ 脚本目录存在: {scripts_dir}") + + # 列出目录中的文件 + files = os.listdir(scripts_dir) + print(f" - 目录中的文件: {files}") + else: + print(f"✗ 脚本目录不存在: {scripts_dir}") + + # 5. 测试脚本加载 + print("\n5. 测试脚本加载...") + world.loadAllScripts() + available_scripts = world.getAvailableScripts() + print(f"✓ 可用脚本: {available_scripts}") + + # 6. 测试对象创建和脚本挂载 + print("\n6. 测试脚本挂载...") + test_object = world.render.attachNewNode("TestObject") + print(f"✓ 创建测试对象: {test_object.getName()}") + + if "ExampleScript" in available_scripts: + script_comp = world.addScript(test_object, "ExampleScript") + if script_comp: + print("✓ 脚本挂载成功") + print(f" - 脚本类型: {script_comp.script_instance.__class__.__name__}") + print(f" - 脚本启用: {script_comp.enabled}") + else: + print("✗ 脚本挂载失败") + else: + print("! 没有ExampleScript可用于测试") + + # 7. 测试脚本系统状态 + print("\n7. 脚本系统状态...") + engine = world.script_manager.engine + print(f" - 脚本引擎运行: {engine.update_task is not None}") + print(f" - 脚本组件数量: {len(engine.script_components)}") + print(f" - 有脚本的对象数量: {len(world.script_manager.object_scripts)}") + + # 8. 测试脚本创建 + print("\n8. 测试脚本创建...") + new_script_path = world.createScript("test_quick_script", "basic") + print(f"✓ 创建新脚本: {new_script_path}") + + if os.path.exists(new_script_path): + print("✓ 脚本文件创建成功") + else: + print("✗ 脚本文件创建失败") + + print("\n=== 快速测试完成 ===") + print("✓ 脚本系统基本功能正常") + return True + + except Exception as e: + print(f"\n✗ 测试过程中出现错误: {e}") + import traceback + traceback.print_exc() + return False + + +def test_script_execution(): + """测试脚本执行""" + print("\n=== 脚本执行测试 ===") + + try: + from main import MyWorld + + world = MyWorld() + world.loadAllScripts() + + # 创建测试对象 + test_obj = world.render.attachNewNode("ExecutionTest") + test_obj.setPos(0, 10, 0) + + # 添加脚本 + available_scripts = world.getAvailableScripts() + if available_scripts: + script_name = available_scripts[0] + script_comp = world.addScript(test_obj, script_name) + + if script_comp: + print(f"✓ 添加脚本: {script_name}") + + # 手动触发脚本生命周期 + print("\n模拟脚本执行...") + + # Start + if not script_comp._started: + script_comp.start() + print("✓ 调用脚本start()方法") + + # Update几次 + for i in range(3): + script_comp.update(0.016) # 约60FPS + print(f"✓ 调用脚本update()方法 ({i+1}/3)") + + # 禁用/启用测试 + script_comp.set_enabled(False) + print("✓ 禁用脚本") + + script_comp.set_enabled(True) + print("✓ 重新启用脚本") + + # 销毁 + script_comp.destroy() + print("✓ 销毁脚本") + + print("\n✓ 脚本执行测试完成") + else: + print("✗ 脚本添加失败") + else: + print("! 没有可用脚本进行测试") + + except Exception as e: + print(f"✗ 脚本执行测试失败: {e}") + import traceback + traceback.print_exc() + + +if __name__ == "__main__": + # 运行快速测试 + success = quick_test() + + if success: + # 如果基本测试通过,运行执行测试 + test_script_execution() + else: + print("\n基本测试失败,跳过执行测试") + print("\n测试完成!") \ No newline at end of file diff --git a/demo/quick_selection_test.py b/demo/quick_selection_test.py index 9f3c56e6..2a67ef0e 100644 --- a/demo/quick_selection_test.py +++ b/demo/quick_selection_test.py @@ -1,94 +1,94 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -快速选择功能测试 - -测试修复后的选择功能是否能正确工作 -""" - -import sys -import os -sys.path.append('..') - -from direct.showbase.ShowBase import ShowBase -from panda3d.core import CardMaker, Vec3, Point3, Material, ModelRoot -from PyQt5.QtWidgets import QApplication - -def main(): - """运行完整的主程序进行测试""" - print("启动选择功能修复测试...") - - # 直接运行主程序 - from main import MyWorld - from ui.main_window import setup_main_window - - world = MyWorld() - app, main_window = setup_main_window(world) - - # 创建一个简单的测试立方体 - print("创建测试立方体...") - cm = CardMaker('test_cube') - cm.setFrame(-2, 2, -2, 2) - - model_root = world.render.attachNewNode(ModelRoot("TestCube")) - - # 创建6个面 - for i, (x, y, z, rx, ry, rz) in enumerate([ - (0, 0, 2, 0, 0, 0), # 前面 - (0, 0, -2, 0, 180, 0), # 后面 - (2, 0, 0, 0, 90, 0), # 右面 - (-2, 0, 0, 0, -90, 0), # 左面 - (0, 2, 0, 90, 0, 0), # 顶面 - (0, -2, 0, -90, 0, 0), # 底面 - ]): - face = model_root.attachNewNode(cm.generate()) - face.setPos(x, y, z) - face.setHpr(rx, ry, rz) - - # 设置位置和颜色 - model_root.setPos(0, 10, 3) - model_root.setColor(0.8, 0.3, 0.3, 1.0) - - # 创建材质 - material = Material() - material.setDiffuse((0.8, 0.3, 0.3, 1.0)) - material.setAmbient((0.2, 0.1, 0.1, 1.0)) - material.setSpecular((0.5, 0.5, 0.5, 1.0)) - material.setShininess(32.0) - model_root.setMaterial(material) - - # 设置标签 - model_root.setTag("file", "TestCube") - model_root.setTag("is_model_root", "1") - - # 添加到场景管理器 - world.scene_manager.models.append(model_root) - - # 设置碰撞检测 - world.scene_manager.setupCollision(model_root) - - # 更新场景树 - world.scene_manager.updateSceneTree() - - print("✓ 测试立方体创建完成") - print("\n=== 测试说明 ===") - print("1. 点击红色立方体测试选择功能") - print("2. 观察控制台输出,确认选择过程") - print("3. 检查是否显示选择框和坐标轴") - print("4. 检查左侧树形控件是否高亮") - print("5. 尝试拖拽坐标轴移动物体") - print("================") - - # 启用射线显示用于调试 - world.setRayDisplay(True) - print("射线显示已启用") - - # 显示窗口 - main_window.show() - - # 运行应用 - sys.exit(app.exec_()) - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +快速选择功能测试 + +测试修复后的选择功能是否能正确工作 +""" + +import sys +import os +sys.path.append('..') + +from direct.showbase.ShowBase import ShowBase +from panda3d.core import CardMaker, Vec3, Point3, Material, ModelRoot +from PyQt5.QtWidgets import QApplication + +def main(): + """运行完整的主程序进行测试""" + print("启动选择功能修复测试...") + + # 直接运行主程序 + from main import MyWorld + from ui.main_window import setup_main_window + + world = MyWorld() + app, main_window = setup_main_window(world) + + # 创建一个简单的测试立方体 + print("创建测试立方体...") + cm = CardMaker('test_cube') + cm.setFrame(-2, 2, -2, 2) + + model_root = world.render.attachNewNode(ModelRoot("TestCube")) + + # 创建6个面 + for i, (x, y, z, rx, ry, rz) in enumerate([ + (0, 0, 2, 0, 0, 0), # 前面 + (0, 0, -2, 0, 180, 0), # 后面 + (2, 0, 0, 0, 90, 0), # 右面 + (-2, 0, 0, 0, -90, 0), # 左面 + (0, 2, 0, 90, 0, 0), # 顶面 + (0, -2, 0, -90, 0, 0), # 底面 + ]): + face = model_root.attachNewNode(cm.generate()) + face.setPos(x, y, z) + face.setHpr(rx, ry, rz) + + # 设置位置和颜色 + model_root.setPos(0, 10, 3) + model_root.setColor(0.8, 0.3, 0.3, 1.0) + + # 创建材质 + material = Material() + material.setDiffuse((0.8, 0.3, 0.3, 1.0)) + material.setAmbient((0.2, 0.1, 0.1, 1.0)) + material.setSpecular((0.5, 0.5, 0.5, 1.0)) + material.setShininess(32.0) + model_root.setMaterial(material) + + # 设置标签 + model_root.setTag("file", "TestCube") + model_root.setTag("is_model_root", "1") + + # 添加到场景管理器 + world.scene_manager.models.append(model_root) + + # 设置碰撞检测 + world.scene_manager.setupCollision(model_root) + + # 更新场景树 + world.scene_manager.updateSceneTree() + + print("✓ 测试立方体创建完成") + print("\n=== 测试说明 ===") + print("1. 点击红色立方体测试选择功能") + print("2. 观察控制台输出,确认选择过程") + print("3. 检查是否显示选择框和坐标轴") + print("4. 检查左侧树形控件是否高亮") + print("5. 尝试拖拽坐标轴移动物体") + print("================") + + # 启用射线显示用于调试 + world.setRayDisplay(True) + print("射线显示已启用") + + # 显示窗口 + main_window.show() + + # 运行应用 + sys.exit(app.exec_()) + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/demo/ray_display_test.py b/demo/ray_display_test.py index f354e084..e12b690b 100644 --- a/demo/ray_display_test.py +++ b/demo/ray_display_test.py @@ -1,111 +1,111 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -射线显示测试 - 测试鼠标点击射线的可视化功能 - -使用说明: -1. 运行脚本启动3D编辑器 -2. 点击鼠标左键,观察射线显示 -3. 按R键切换射线显示开关 -4. 射线颜色说明: - - 蓝色:没有碰撞的射线 - - 绿色段:从相机到碰撞点 - - 红色段:从碰撞点到远处 - - 黄色十字:碰撞点标记 -""" - -import sys -import os - -# 添加主目录到Python路径 -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - -from main import MyWorld -from ui.main_window import setup_main_window -from PyQt5.QtCore import Qt - - -def setup_ray_display_demo(): - """设置射线显示演示""" - - # 创建世界对象 - world = MyWorld() - - # 使用新的UI模块创建主窗口 - app, main_window = setup_main_window(world) - - # 设置窗口标题 - main_window.setWindowTitle("射线显示测试 - 点击鼠标查看射线,按R键切换显示") - - # 设置焦点策略,确保主窗口能接收键盘事件 - main_window.setFocusPolicy(Qt.StrongFocus) - main_window.setFocus() - - # 添加键盘事件处理 - def keyPressEvent(event): - print(f"检测到按键: {event.key()}, Qt.Key_R = {Qt.Key_R}") - key = event.key() - if key == Qt.Key_R: # R键 - state = world.toggleRayDisplay() - status = "开启" if state else "关闭" - print(f"\n=== 射线显示已{status} ===") - print(f"当前射线显示状态: {world.getRayDisplay()}") - event.accept() # 标记事件已处理 - return - - # 调用原始的键盘事件处理(如果存在) - if hasattr(main_window, '_original_keyPressEvent'): - main_window._original_keyPressEvent(event) - else: - event.ignore() - - # 保存原始的键盘事件处理器(如果存在) - if hasattr(main_window, 'keyPressEvent'): - main_window._original_keyPressEvent = main_window.keyPressEvent - main_window.keyPressEvent = keyPressEvent - - # 添加射线显示菜单项(备用方案) - rayMenu = main_window.menuBar().addMenu('射线调试') - toggleRayAction = rayMenu.addAction('切换射线显示 (R)') - toggleRayAction.triggered.connect(lambda: toggle_and_print_ray_status()) - - def toggle_and_print_ray_status(): - state = world.toggleRayDisplay() - status = "开启" if state else "关闭" - print(f"\n=== 射线显示已{status} ===") - print(f"当前射线显示状态: {world.getRayDisplay()}") - - # 输出使用说明 - print("\n" + "="*60) - print("射线显示测试启动完成!") - print("="*60) - print("使用说明:") - print("1. 默认不显示射线") - print("2. 切换射线显示的两种方式:") - print(" - 按R键切换") - print(" - 或点击菜单 [射线调试] -> [切换射线显示]") - print("3. 点击鼠标左键查看射线(需先开启显示)") - print("4. 射线颜色说明:") - print(" - 蓝色:没有碰撞的射线") - print(" - 绿色段:从相机到碰撞点") - print(" - 红色段:从碰撞点延伸") - print(" - 黄色十字:碰撞点标记") - print("5. 射线会在2秒后自动消失") - print(f"6. 当前射线显示状态: {'开启' if world.getRayDisplay() else '关闭'}") - print("="*60) - - return app, main_window, world - - -if __name__ == "__main__": - try: - app, main_window, world = setup_ray_display_demo() - - # 启动应用程序 - sys.exit(app.exec_()) - - except Exception as e: - print(f"启动失败: {str(e)}") - import traceback +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +射线显示测试 - 测试鼠标点击射线的可视化功能 + +使用说明: +1. 运行脚本启动3D编辑器 +2. 点击鼠标左键,观察射线显示 +3. 按R键切换射线显示开关 +4. 射线颜色说明: + - 蓝色:没有碰撞的射线 + - 绿色段:从相机到碰撞点 + - 红色段:从碰撞点到远处 + - 黄色十字:碰撞点标记 +""" + +import sys +import os + +# 添加主目录到Python路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from main import MyWorld +from ui.main_window import setup_main_window +from PyQt5.QtCore import Qt + + +def setup_ray_display_demo(): + """设置射线显示演示""" + + # 创建世界对象 + world = MyWorld() + + # 使用新的UI模块创建主窗口 + app, main_window = setup_main_window(world) + + # 设置窗口标题 + main_window.setWindowTitle("射线显示测试 - 点击鼠标查看射线,按R键切换显示") + + # 设置焦点策略,确保主窗口能接收键盘事件 + main_window.setFocusPolicy(Qt.StrongFocus) + main_window.setFocus() + + # 添加键盘事件处理 + def keyPressEvent(event): + print(f"检测到按键: {event.key()}, Qt.Key_R = {Qt.Key_R}") + key = event.key() + if key == Qt.Key_R: # R键 + state = world.toggleRayDisplay() + status = "开启" if state else "关闭" + print(f"\n=== 射线显示已{status} ===") + print(f"当前射线显示状态: {world.getRayDisplay()}") + event.accept() # 标记事件已处理 + return + + # 调用原始的键盘事件处理(如果存在) + if hasattr(main_window, '_original_keyPressEvent'): + main_window._original_keyPressEvent(event) + else: + event.ignore() + + # 保存原始的键盘事件处理器(如果存在) + if hasattr(main_window, 'keyPressEvent'): + main_window._original_keyPressEvent = main_window.keyPressEvent + main_window.keyPressEvent = keyPressEvent + + # 添加射线显示菜单项(备用方案) + rayMenu = main_window.menuBar().addMenu('射线调试') + toggleRayAction = rayMenu.addAction('切换射线显示 (R)') + toggleRayAction.triggered.connect(lambda: toggle_and_print_ray_status()) + + def toggle_and_print_ray_status(): + state = world.toggleRayDisplay() + status = "开启" if state else "关闭" + print(f"\n=== 射线显示已{status} ===") + print(f"当前射线显示状态: {world.getRayDisplay()}") + + # 输出使用说明 + print("\n" + "="*60) + print("射线显示测试启动完成!") + print("="*60) + print("使用说明:") + print("1. 默认不显示射线") + print("2. 切换射线显示的两种方式:") + print(" - 按R键切换") + print(" - 或点击菜单 [射线调试] -> [切换射线显示]") + print("3. 点击鼠标左键查看射线(需先开启显示)") + print("4. 射线颜色说明:") + print(" - 蓝色:没有碰撞的射线") + print(" - 绿色段:从相机到碰撞点") + print(" - 红色段:从碰撞点延伸") + print(" - 黄色十字:碰撞点标记") + print("5. 射线会在2秒后自动消失") + print(f"6. 当前射线显示状态: {'开启' if world.getRayDisplay() else '关闭'}") + print("="*60) + + return app, main_window, world + + +if __name__ == "__main__": + try: + app, main_window, world = setup_ray_display_demo() + + # 启动应用程序 + sys.exit(app.exec_()) + + except Exception as e: + print(f"启动失败: {str(e)}") + import traceback traceback.print_exc() \ No newline at end of file diff --git a/demo/ray_offset_debug.py b/demo/ray_offset_debug.py index 3972dcc7..b0cd183b 100644 --- a/demo/ray_offset_debug.py +++ b/demo/ray_offset_debug.py @@ -1,116 +1,116 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import sys -from direct.showbase.ShowBase import ShowBase -from panda3d.core import ( - Point3, Point2, Vec3, CollisionTraverser, CollisionHandlerQueue, - CollisionNode, CollisionRay, CollisionSphere, BitMask32 -) - -class RayOffsetDebug(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 使用和主程序相同的相机设置 - self.cam.setPos(-3.87655, -188.38084, 82.602684) - self.cam.lookAt(0, 0, 0) - - print("=== 射线偏移调试 ===") - print(f"相机位置: {self.cam.getPos()}") - print(f"相机朝向: {self.cam.getHpr()}") - - # 显示镜头参数 - lens = self.cam.node().getLens() - print(f"镜头FOV: {lens.getFov()}") - print(f"宽高比: {lens.getAspectRatio()}") - - # 设置射线检测 - self.setupRayPicking() - - # 测试不同位置的射线 - self.testRayDirections() - - print("调试完成") - - def setupRayPicking(self): - """设置射线检测""" - self.pickerNode = CollisionNode('mouseRay') - self.pickerRay = CollisionRay() - self.pickerNode.addSolid(self.pickerRay) - - mask = BitMask32.bit(0) - self.pickerNode.setFromCollideMask(mask) - self.pickerNode.setIntoCollideMask(BitMask32.allOff()) - - self.pickerNP = self.render.attachNewNode(self.pickerNode) - - def testRayDirections(self): - """测试不同位置的射线方向""" - print("\n=== 射线方向测试 ===") - - # 模拟不同的屏幕点击位置 - test_cases = [ - (0.0, 0.0, "屏幕中心"), - (-0.5, 0.0, "屏幕左侧"), - (0.5, 0.0, "屏幕右侧"), - (0.0, 0.5, "屏幕上方"), - (0.0, -0.5, "屏幕下方"), - (-0.006, 0.346, "用户点击位置"), - ] - - for norm_x, norm_y, description in test_cases: - print(f"\n--- {description}: ({norm_x:.3f}, {norm_y:.3f}) ---") - - # 使用setFromLens方法 - success = self.pickerRay.setFromLens(self.cam.node(), norm_x, norm_y) - if success: - rayOrigin = self.pickerRay.getOrigin() - rayDirection = self.pickerRay.getDirection() - - print(f"射线起点: {rayOrigin}") - print(f"射线方向: {rayDirection}") - - # 分析相机坐标系中的分量 - camForward = self.cam.getMat().getRow3(1) - camRight = self.cam.getMat().getRow3(0) - camUp = self.cam.getMat().getRow3(2) - - forwardComp = rayDirection.dot(camForward) - rightComp = rayDirection.dot(camRight) - upComp = rayDirection.dot(camUp) - - print(f"相机坐标系分量:") - print(f" 前向: {forwardComp:.6f}") - print(f" 右向: {rightComp:.6f} (正=右, 负=左)") - print(f" 上向: {upComp:.6f} (正=上, 负=下)") - - # 计算射线与(0,10,3)点的交点(地面上的一个点) - targetPoint = Point3(0, 10, 3) - rayToTarget = targetPoint - rayOrigin - - # 检查射线是否指向这个方向 - if rayDirection.dot(rayToTarget) > 0: - # 计算交点 - distance = rayToTarget.length() - angle = rayDirection.dot(rayToTarget.normalized()) - print(f"与参考点(0,10,3)的角度: {angle:.6f}") - - # 预测射线在y=10平面上的交点 - if abs(rayDirection.getY()) > 0.001: - t = (10 - rayOrigin.getY()) / rayDirection.getY() - if t > 0: - intersectionPoint = rayOrigin + rayDirection * t - print(f"在Y=10平面上的交点: {intersectionPoint}") - else: - print("射线不指向Y=10平面") - else: - print("射线平行于Y=10平面") - else: - print("射线背离参考点") - else: - print("setFromLens 失败") - -if __name__ == "__main__": - app = RayOffsetDebug() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +from direct.showbase.ShowBase import ShowBase +from panda3d.core import ( + Point3, Point2, Vec3, CollisionTraverser, CollisionHandlerQueue, + CollisionNode, CollisionRay, CollisionSphere, BitMask32 +) + +class RayOffsetDebug(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 使用和主程序相同的相机设置 + self.cam.setPos(-3.87655, -188.38084, 82.602684) + self.cam.lookAt(0, 0, 0) + + print("=== 射线偏移调试 ===") + print(f"相机位置: {self.cam.getPos()}") + print(f"相机朝向: {self.cam.getHpr()}") + + # 显示镜头参数 + lens = self.cam.node().getLens() + print(f"镜头FOV: {lens.getFov()}") + print(f"宽高比: {lens.getAspectRatio()}") + + # 设置射线检测 + self.setupRayPicking() + + # 测试不同位置的射线 + self.testRayDirections() + + print("调试完成") + + def setupRayPicking(self): + """设置射线检测""" + self.pickerNode = CollisionNode('mouseRay') + self.pickerRay = CollisionRay() + self.pickerNode.addSolid(self.pickerRay) + + mask = BitMask32.bit(0) + self.pickerNode.setFromCollideMask(mask) + self.pickerNode.setIntoCollideMask(BitMask32.allOff()) + + self.pickerNP = self.render.attachNewNode(self.pickerNode) + + def testRayDirections(self): + """测试不同位置的射线方向""" + print("\n=== 射线方向测试 ===") + + # 模拟不同的屏幕点击位置 + test_cases = [ + (0.0, 0.0, "屏幕中心"), + (-0.5, 0.0, "屏幕左侧"), + (0.5, 0.0, "屏幕右侧"), + (0.0, 0.5, "屏幕上方"), + (0.0, -0.5, "屏幕下方"), + (-0.006, 0.346, "用户点击位置"), + ] + + for norm_x, norm_y, description in test_cases: + print(f"\n--- {description}: ({norm_x:.3f}, {norm_y:.3f}) ---") + + # 使用setFromLens方法 + success = self.pickerRay.setFromLens(self.cam.node(), norm_x, norm_y) + if success: + rayOrigin = self.pickerRay.getOrigin() + rayDirection = self.pickerRay.getDirection() + + print(f"射线起点: {rayOrigin}") + print(f"射线方向: {rayDirection}") + + # 分析相机坐标系中的分量 + camForward = self.cam.getMat().getRow3(1) + camRight = self.cam.getMat().getRow3(0) + camUp = self.cam.getMat().getRow3(2) + + forwardComp = rayDirection.dot(camForward) + rightComp = rayDirection.dot(camRight) + upComp = rayDirection.dot(camUp) + + print(f"相机坐标系分量:") + print(f" 前向: {forwardComp:.6f}") + print(f" 右向: {rightComp:.6f} (正=右, 负=左)") + print(f" 上向: {upComp:.6f} (正=上, 负=下)") + + # 计算射线与(0,10,3)点的交点(地面上的一个点) + targetPoint = Point3(0, 10, 3) + rayToTarget = targetPoint - rayOrigin + + # 检查射线是否指向这个方向 + if rayDirection.dot(rayToTarget) > 0: + # 计算交点 + distance = rayToTarget.length() + angle = rayDirection.dot(rayToTarget.normalized()) + print(f"与参考点(0,10,3)的角度: {angle:.6f}") + + # 预测射线在y=10平面上的交点 + if abs(rayDirection.getY()) > 0.001: + t = (10 - rayOrigin.getY()) / rayDirection.getY() + if t > 0: + intersectionPoint = rayOrigin + rayDirection * t + print(f"在Y=10平面上的交点: {intersectionPoint}") + else: + print("射线不指向Y=10平面") + else: + print("射线平行于Y=10平面") + else: + print("射线背离参考点") + else: + print("setFromLens 失败") + +if __name__ == "__main__": + app = RayOffsetDebug() print("测试完成") \ No newline at end of file diff --git a/demo/ray_test.py b/demo/ray_test.py index dc9edd72..897150df 100644 --- a/demo/ray_test.py +++ b/demo/ray_test.py @@ -1,177 +1,177 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import sys -from direct.showbase.ShowBase import ShowBase -from panda3d.core import ( - Point3, Point2, Vec3, CollisionTraverser, CollisionHandlerQueue, - CollisionNode, CollisionRay, CollisionSphere, BitMask32, CardMaker -) - -class RayTest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 设置相机位置 - 和你的程序一样 - self.cam.setPos(-3.87655, -188.38084, 82.602684) - self.cam.lookAt(0, 0, 0) - - print(f"相机位置: {self.cam.getPos()}") - print(f"相机朝向: {self.cam.getHpr()}") - - # 创建测试立方体 - 位置和你的程序一样 - self.createTestCube() - - # 设置射线检测 - self.setupRayPicking() - - # 测试不同的射线方向 - self.testRaycast() - - def createTestCube(self): - """创建测试立方体""" - # 创建简单的平面作为立方体 - cm = CardMaker("TestCube") - cm.setFrame(-1, 1, -1, 1) - self.cube = self.render.attachNewNode(cm.generate()) - - # 设置位置和大小 - 和你的程序完全一样 - self.cube.setPos(0, 10, 3) - self.cube.setScale(3, 3, 3) - self.cube.setColor(1, 0, 0, 1) - self.cube.setTwoSided(True) - self.cube.setHpr(45, 15, 0) - - print(f"立方体世界位置: {self.cube.getPos(self.render)}") - print(f"立方体缩放: {self.cube.getScale()}") - - # 添加碰撞检测 - cNode = CollisionNode('TestCubeCollision') - cSphere = CollisionSphere(Point3(0, 0, 0), 4.0) - cNode.addSolid(cSphere) - cNode.setIntoCollideMask(BitMask32.bit(0)) - cNode.setFromCollideMask(BitMask32.allOff()) - - self.cNodePath = self.cube.attachNewNode(cNode) - print(f"碰撞球体半径: 4.0") - - # 显示碰撞体 - self.cNodePath.show() - - def setupRayPicking(self): - """设置射线检测""" - self.picker = CollisionTraverser() - self.pickerQueue = CollisionHandlerQueue() - - self.pickerNode = CollisionNode('mouseRay') - self.pickerRay = CollisionRay() - self.pickerNode.addSolid(self.pickerRay) - - # 设置掩码 - mask = BitMask32.bit(0) - self.pickerNode.setFromCollideMask(mask) - self.pickerNode.setIntoCollideMask(BitMask32.allOff()) - - self.pickerNP = self.cam.attachNewNode(self.pickerNode) - self.picker.addCollider(self.pickerNP, self.pickerQueue) - - print(f"射线检测器掩码: {mask}") - - def testRaycast(self): - """测试射线投射""" - print(f"\n=== 射线投射测试 ===") - - # 测试用例:模拟你的点击位置 - window_width = 800 # 假设窗口大小 - window_height = 600 - mouse_x = 419 - mouse_y = 96 - - # 计算归一化坐标 - normalized_x = (2.0 * mouse_x / window_width) - 1.0 - normalized_y = 1.0 - (2.0 * mouse_y / window_height) - - print(f"鼠标位置: ({mouse_x}, {mouse_y})") - print(f"归一化坐标: ({normalized_x:.3f}, {normalized_y:.3f})") - - # 获取相机参数 - camera_pos = self.cam.getPos(self.render) - lens = self.cam.node().getLens() - - # 计算射线 - nearPoint = Point3() - farPoint = Point3() - - if lens.extrude(Point2(normalized_x, normalized_y), nearPoint, farPoint): - print(f"lens.extrude() 成功") - print(f"近点(相机坐标): {nearPoint}") - print(f"远点(相机坐标): {farPoint}") - - # 转换到世界坐标 - nearWorldPoint = self.cam.getRelativePoint(self.render, nearPoint) - farWorldPoint = self.cam.getRelativePoint(self.render, farPoint) - - print(f"近点(世界坐标): {nearWorldPoint}") - print(f"远点(世界坐标): {farWorldPoint}") - - # 计算射线方向 - rayDirWorld = farWorldPoint - nearWorldPoint - rayDirWorld.normalize() - - print(f"射线起点: {camera_pos}") - print(f"射线方向: {rayDirWorld}") - - # 设置射线 - self.pickerRay.setOrigin(camera_pos) - self.pickerRay.setDirection(rayDirWorld) - - # 执行碰撞检测 - self.pickerQueue.clearEntries() - self.picker.traverse(self.render) - - numEntries = self.pickerQueue.getNumEntries() - print(f"\n检测到 {numEntries} 个碰撞") - - if numEntries > 0: - self.pickerQueue.sortEntries() - for i in range(numEntries): - entry = self.pickerQueue.getEntry(i) - hitPos = entry.getSurfacePoint(self.render) - hitNode = entry.getIntoNodePath() - distance = (hitPos - camera_pos).length() - print(f" 碰撞 {i}: {hitNode.getName()}, 距离: {distance:.2f}, 位置: {hitPos}") - else: - print(" 没有检测到碰撞") - - # 分析为什么没有碰撞 - print(f"\n=== 碰撞分析 ===") - cube_world_pos = self.cube.getPos(self.render) - cube_scale = self.cube.getScale() - - print(f"立方体中心: {cube_world_pos}") - print(f"立方体缩放: {cube_scale}") - print(f"碰撞球体实际半径: {4.0 * cube_scale.getX()}") - - # 计算射线到立方体中心的最近距离 - # 射线: P = origin + t * direction - # 立方体中心: C - # 最近距离: |cross(C - origin, direction)| / |direction| - - to_cube = cube_world_pos - camera_pos - cross_product = to_cube.cross(rayDirWorld) - distance_to_ray = cross_product.length() - - print(f"射线到立方体中心的距离: {distance_to_ray:.2f}") - print(f"需要小于碰撞半径: {4.0 * cube_scale.getX():.2f}") - - if distance_to_ray < 4.0 * cube_scale.getX(): - print("射线应该能击中立方体!可能是其他问题。") - else: - print("射线错过了立方体。") - else: - print("lens.extrude() 失败") - -if __name__ == "__main__": - app = RayTest() - # 不运行主循环,只测试 +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import sys +from direct.showbase.ShowBase import ShowBase +from panda3d.core import ( + Point3, Point2, Vec3, CollisionTraverser, CollisionHandlerQueue, + CollisionNode, CollisionRay, CollisionSphere, BitMask32, CardMaker +) + +class RayTest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 设置相机位置 - 和你的程序一样 + self.cam.setPos(-3.87655, -188.38084, 82.602684) + self.cam.lookAt(0, 0, 0) + + print(f"相机位置: {self.cam.getPos()}") + print(f"相机朝向: {self.cam.getHpr()}") + + # 创建测试立方体 - 位置和你的程序一样 + self.createTestCube() + + # 设置射线检测 + self.setupRayPicking() + + # 测试不同的射线方向 + self.testRaycast() + + def createTestCube(self): + """创建测试立方体""" + # 创建简单的平面作为立方体 + cm = CardMaker("TestCube") + cm.setFrame(-1, 1, -1, 1) + self.cube = self.render.attachNewNode(cm.generate()) + + # 设置位置和大小 - 和你的程序完全一样 + self.cube.setPos(0, 10, 3) + self.cube.setScale(3, 3, 3) + self.cube.setColor(1, 0, 0, 1) + self.cube.setTwoSided(True) + self.cube.setHpr(45, 15, 0) + + print(f"立方体世界位置: {self.cube.getPos(self.render)}") + print(f"立方体缩放: {self.cube.getScale()}") + + # 添加碰撞检测 + cNode = CollisionNode('TestCubeCollision') + cSphere = CollisionSphere(Point3(0, 0, 0), 4.0) + cNode.addSolid(cSphere) + cNode.setIntoCollideMask(BitMask32.bit(0)) + cNode.setFromCollideMask(BitMask32.allOff()) + + self.cNodePath = self.cube.attachNewNode(cNode) + print(f"碰撞球体半径: 4.0") + + # 显示碰撞体 + self.cNodePath.show() + + def setupRayPicking(self): + """设置射线检测""" + self.picker = CollisionTraverser() + self.pickerQueue = CollisionHandlerQueue() + + self.pickerNode = CollisionNode('mouseRay') + self.pickerRay = CollisionRay() + self.pickerNode.addSolid(self.pickerRay) + + # 设置掩码 + mask = BitMask32.bit(0) + self.pickerNode.setFromCollideMask(mask) + self.pickerNode.setIntoCollideMask(BitMask32.allOff()) + + self.pickerNP = self.cam.attachNewNode(self.pickerNode) + self.picker.addCollider(self.pickerNP, self.pickerQueue) + + print(f"射线检测器掩码: {mask}") + + def testRaycast(self): + """测试射线投射""" + print(f"\n=== 射线投射测试 ===") + + # 测试用例:模拟你的点击位置 + window_width = 800 # 假设窗口大小 + window_height = 600 + mouse_x = 419 + mouse_y = 96 + + # 计算归一化坐标 + normalized_x = (2.0 * mouse_x / window_width) - 1.0 + normalized_y = 1.0 - (2.0 * mouse_y / window_height) + + print(f"鼠标位置: ({mouse_x}, {mouse_y})") + print(f"归一化坐标: ({normalized_x:.3f}, {normalized_y:.3f})") + + # 获取相机参数 + camera_pos = self.cam.getPos(self.render) + lens = self.cam.node().getLens() + + # 计算射线 + nearPoint = Point3() + farPoint = Point3() + + if lens.extrude(Point2(normalized_x, normalized_y), nearPoint, farPoint): + print(f"lens.extrude() 成功") + print(f"近点(相机坐标): {nearPoint}") + print(f"远点(相机坐标): {farPoint}") + + # 转换到世界坐标 + nearWorldPoint = self.cam.getRelativePoint(self.render, nearPoint) + farWorldPoint = self.cam.getRelativePoint(self.render, farPoint) + + print(f"近点(世界坐标): {nearWorldPoint}") + print(f"远点(世界坐标): {farWorldPoint}") + + # 计算射线方向 + rayDirWorld = farWorldPoint - nearWorldPoint + rayDirWorld.normalize() + + print(f"射线起点: {camera_pos}") + print(f"射线方向: {rayDirWorld}") + + # 设置射线 + self.pickerRay.setOrigin(camera_pos) + self.pickerRay.setDirection(rayDirWorld) + + # 执行碰撞检测 + self.pickerQueue.clearEntries() + self.picker.traverse(self.render) + + numEntries = self.pickerQueue.getNumEntries() + print(f"\n检测到 {numEntries} 个碰撞") + + if numEntries > 0: + self.pickerQueue.sortEntries() + for i in range(numEntries): + entry = self.pickerQueue.getEntry(i) + hitPos = entry.getSurfacePoint(self.render) + hitNode = entry.getIntoNodePath() + distance = (hitPos - camera_pos).length() + print(f" 碰撞 {i}: {hitNode.getName()}, 距离: {distance:.2f}, 位置: {hitPos}") + else: + print(" 没有检测到碰撞") + + # 分析为什么没有碰撞 + print(f"\n=== 碰撞分析 ===") + cube_world_pos = self.cube.getPos(self.render) + cube_scale = self.cube.getScale() + + print(f"立方体中心: {cube_world_pos}") + print(f"立方体缩放: {cube_scale}") + print(f"碰撞球体实际半径: {4.0 * cube_scale.getX()}") + + # 计算射线到立方体中心的最近距离 + # 射线: P = origin + t * direction + # 立方体中心: C + # 最近距离: |cross(C - origin, direction)| / |direction| + + to_cube = cube_world_pos - camera_pos + cross_product = to_cube.cross(rayDirWorld) + distance_to_ray = cross_product.length() + + print(f"射线到立方体中心的距离: {distance_to_ray:.2f}") + print(f"需要小于碰撞半径: {4.0 * cube_scale.getX():.2f}") + + if distance_to_ray < 4.0 * cube_scale.getX(): + print("射线应该能击中立方体!可能是其他问题。") + else: + print("射线错过了立方体。") + else: + print("lens.extrude() 失败") + +if __name__ == "__main__": + app = RayTest() + # 不运行主循环,只测试 print("\n测试完成!") \ No newline at end of file diff --git a/demo/scale_position_test.py b/demo/scale_position_test.py index 96b615a7..84905a84 100644 --- a/demo/scale_position_test.py +++ b/demo/scale_position_test.py @@ -1,261 +1,261 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -缩放位置测试 - 验证缩放标准化时位置是否正确保持 - -测试内容: -1. 创建模拟FBX层级结构 -2. 验证缩放标准化前后的世界位置 -3. 对比修复前后的效果 - -控制说明: -- I键:显示当前模型信息 -- N键:切换缩放标准化 -- R键:重置模型 -- T键:运行位置测试 -- Q键:退出 -""" - -import sys -import os -sys.path.append('..') # 添加父目录到路径 - -from direct.showbase.ShowBase import ShowBase -from panda3d.core import (CardMaker, Vec3, Point3, Material, - ModelRoot, PandaNode, LineSegs, ColorAttrib, RenderState, - DepthTestAttrib) -from PyQt5.QtWidgets import QApplication - -def create_test_fbx_structure(world): - """创建模拟FBX结构用于测试""" - print("\n=== 创建模拟FBX结构 ===") - - # 创建根节点 - root = world.render.attachNewNode(ModelRoot("test_fbx_model")) - root.setScale(1.0) # 根节点缩放为1 - root.setPos(10, 5, 2) # 给根节点一个偏移位置 - - # 创建子节点1 - 模拟有大缩放值的子节点 - child1_node = PandaNode("child1") - child1 = root.attachNewNode(child1_node) - child1.setScale(100.0) # 大缩放值 - child1.setPos(5, 0, 0) # 相对于父节点的位置 - - # 创建子节点2 - 另一个有大缩放值的子节点 - child2_node = PandaNode("child2") - child2 = root.attachNewNode(child2_node) - child2.setScale(100.0) # 大缩放值 - child2.setPos(-3, 8, 1) # 相对于父节点的位置 - - # 创建孙子节点 - 嵌套结构 - grandchild_node = PandaNode("grandchild") - grandchild = child1.attachNewNode(grandchild_node) - grandchild.setScale(100.0) # 大缩放值 - grandchild.setPos(2, 2, 1) # 相对于父节点的位置 - - # 创建可视化几何体 - def create_marker(node, color): - """为节点创建可视化标记""" - cm = CardMaker(f'marker_{node.getName()}') - cm.setFrame(-0.5, 0.5, -0.5, 0.5) - marker = node.attachNewNode(cm.generate()) - marker.setColor(*color) - marker.setBillboardAxis() # 始终面向相机 - return marker - - # 为各节点创建可视化标记 - create_marker(root, (1, 0, 0, 1)) # 红色 - 根节点 - create_marker(child1, (0, 1, 0, 1)) # 绿色 - 子节点1 - create_marker(child2, (0, 0, 1, 1)) # 蓝色 - 子节点2 - create_marker(grandchild, (1, 1, 0, 1)) # 黄色 - 孙子节点 - - # 添加到模型列表 - world.scene_manager.models.append(root) - - print("✓ 模拟FBX结构创建完成") - return root - -def print_position_info(node, label, depth=0): - """打印节点的位置和缩放信息""" - indent = " " * depth - local_pos = node.getPos() - world_pos = node.getPos(node.getTopParent()) - scale = node.getScale() - - print(f"{indent}{label}:") - print(f"{indent} 本地位置: {local_pos}") - print(f"{indent} 世界位置: {world_pos}") - print(f"{indent} 缩放: {scale}") - -def run_position_test(world, model): - """运行位置测试""" - print("\n=== 位置测试开始 ===") - - # 收集所有节点 - nodes = [] - def collect_nodes(node): - nodes.append(node) - for i in range(node.getNumChildren()): - collect_nodes(node.getChild(i)) - - collect_nodes(model) - - # 记录标准化前的位置 - print("\n--- 标准化前的位置信息 ---") - before_positions = {} - for i, node in enumerate(nodes): - label = f"节点{i+1}({node.getName()})" - print_position_info(node, label) - before_positions[node.getName()] = { - 'local': node.getPos(), - 'world': node.getPos(world.render), - 'scale': node.getScale() - } - - # 应用缩放标准化 - print("\n--- 应用缩放标准化 ---") - world.scene_manager._normalizeModelScales(model) - - # 记录标准化后的位置 - print("\n--- 标准化后的位置信息 ---") - after_positions = {} - for i, node in enumerate(nodes): - label = f"节点{i+1}({node.getName()})" - print_position_info(node, label) - after_positions[node.getName()] = { - 'local': node.getPos(), - 'world': node.getPos(world.render), - 'scale': node.getScale() - } - - # 分析位置和缩放变化 - print("\n--- 位置和缩放变化分析 ---") - for name in before_positions: - before = before_positions[name] - after = after_positions[name] - - scale_change = after['scale'] - before['scale'] - local_pos_change = after['local'] - before['local'] - world_pos_change = after['world'] - before['world'] - - local_pos_distance = local_pos_change.length() - world_pos_distance = world_pos_change.length() - - print(f"\n{name}:") - print(f" 缩放变化: {before['scale']} -> {after['scale']}") - print(f" 本地位置变化: {before['local']} -> {after['local']}") - print(f" 本地位置变化距离: {local_pos_distance:.6f}") - print(f" 世界位置变化距离: {world_pos_distance:.6f}") - - # 检查是否按比例缩放 - if before['scale'].x > 10: # 如果原来有大缩放 - expected_scale_factor = 0.01 # 期望的缩放因子 - actual_scale_factor = after['scale'].x / before['scale'].x if before['scale'].x != 0 else 0 - expected_pos = before['local'] * expected_scale_factor - pos_error = (after['local'] - expected_pos).length() - - print(f" 期望缩放因子: {expected_scale_factor}") - print(f" 实际缩放因子: {actual_scale_factor:.6f}") - print(f" 位置缩放误差: {pos_error:.6f}") - - if abs(actual_scale_factor - expected_scale_factor) < 0.001 and pos_error < 0.01: - print(f" ✓ 缩放和位置标准化正确") - else: - print(f" ⚠ 缩放或位置标准化可能有问题") - else: - print(f" ℹ 未标准化(缩放值正常)") - - print("\n=== 位置测试完成 ===") - -class ScalePositionTest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 导入我们的模块 - from main import MyWorld - - # 创建世界实例 - self.world = MyWorld() - - # 设置相机 - self.cam.setPos(0, -30, 10) - self.cam.lookAt(0, 0, 0) - - # 创建测试模型 - self.test_model = create_test_fbx_structure(self.world) - - # 设置键盘事件 - self.setupKeyEvents() - - print("\n=== 缩放位置测试程序启动 ===") - print("控制说明:") - print("I键:显示当前模型信息") - print("N键:应用缩放标准化") - print("R键:重置模型") - print("T键:运行完整位置测试") - print("Q键:退出程序") - print("================================") - - def setupKeyEvents(self): - """设置键盘事件""" - self.accept('i', self.showModelInfo) - self.accept('n', self.applyNormalization) - self.accept('r', self.resetModel) - self.accept('t', self.runPositionTest) - self.accept('q', self.quit) - self.accept('escape', self.quit) - - def showModelInfo(self): - """显示模型信息""" - print("\n=== 当前模型信息 ===") - if self.test_model: - def show_node_info(node, depth=0): - indent = " " * depth - print(f"{indent}节点: {node.getName()}") - print(f"{indent} 本地位置: {node.getPos()}") - print(f"{indent} 世界位置: {node.getPos(self.world.render)}") - print(f"{indent} 缩放: {node.getScale()}") - - for i in range(node.getNumChildren()): - child = node.getChild(i) - show_node_info(child, depth + 1) - - show_node_info(self.test_model) - print("==================") - - def applyNormalization(self): - """应用缩放标准化""" - print("\n=== 应用缩放标准化 ===") - if self.test_model: - self.world.scene_manager._normalizeModelScales(self.test_model) - print("✓ 缩放标准化完成") - else: - print("× 没有找到测试模型") - - def resetModel(self): - """重置模型""" - print("\n=== 重置模型 ===") - if self.test_model: - self.test_model.removeNode() - - # 重新创建 - self.test_model = create_test_fbx_structure(self.world) - print("✓ 模型重置完成") - - def runPositionTest(self): - """运行完整位置测试""" - if self.test_model: - run_position_test(self.world, self.test_model) - else: - print("× 没有找到测试模型") - - def quit(self): - """退出程序""" - print("\n退出缩放位置测试程序") - sys.exit() - -if __name__ == "__main__": - app = QApplication(sys.argv) - test = ScalePositionTest() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +缩放位置测试 - 验证缩放标准化时位置是否正确保持 + +测试内容: +1. 创建模拟FBX层级结构 +2. 验证缩放标准化前后的世界位置 +3. 对比修复前后的效果 + +控制说明: +- I键:显示当前模型信息 +- N键:切换缩放标准化 +- R键:重置模型 +- T键:运行位置测试 +- Q键:退出 +""" + +import sys +import os +sys.path.append('..') # 添加父目录到路径 + +from direct.showbase.ShowBase import ShowBase +from panda3d.core import (CardMaker, Vec3, Point3, Material, + ModelRoot, PandaNode, LineSegs, ColorAttrib, RenderState, + DepthTestAttrib) +from PyQt5.QtWidgets import QApplication + +def create_test_fbx_structure(world): + """创建模拟FBX结构用于测试""" + print("\n=== 创建模拟FBX结构 ===") + + # 创建根节点 + root = world.render.attachNewNode(ModelRoot("test_fbx_model")) + root.setScale(1.0) # 根节点缩放为1 + root.setPos(10, 5, 2) # 给根节点一个偏移位置 + + # 创建子节点1 - 模拟有大缩放值的子节点 + child1_node = PandaNode("child1") + child1 = root.attachNewNode(child1_node) + child1.setScale(100.0) # 大缩放值 + child1.setPos(5, 0, 0) # 相对于父节点的位置 + + # 创建子节点2 - 另一个有大缩放值的子节点 + child2_node = PandaNode("child2") + child2 = root.attachNewNode(child2_node) + child2.setScale(100.0) # 大缩放值 + child2.setPos(-3, 8, 1) # 相对于父节点的位置 + + # 创建孙子节点 - 嵌套结构 + grandchild_node = PandaNode("grandchild") + grandchild = child1.attachNewNode(grandchild_node) + grandchild.setScale(100.0) # 大缩放值 + grandchild.setPos(2, 2, 1) # 相对于父节点的位置 + + # 创建可视化几何体 + def create_marker(node, color): + """为节点创建可视化标记""" + cm = CardMaker(f'marker_{node.getName()}') + cm.setFrame(-0.5, 0.5, -0.5, 0.5) + marker = node.attachNewNode(cm.generate()) + marker.setColor(*color) + marker.setBillboardAxis() # 始终面向相机 + return marker + + # 为各节点创建可视化标记 + create_marker(root, (1, 0, 0, 1)) # 红色 - 根节点 + create_marker(child1, (0, 1, 0, 1)) # 绿色 - 子节点1 + create_marker(child2, (0, 0, 1, 1)) # 蓝色 - 子节点2 + create_marker(grandchild, (1, 1, 0, 1)) # 黄色 - 孙子节点 + + # 添加到模型列表 + world.scene_manager.models.append(root) + + print("✓ 模拟FBX结构创建完成") + return root + +def print_position_info(node, label, depth=0): + """打印节点的位置和缩放信息""" + indent = " " * depth + local_pos = node.getPos() + world_pos = node.getPos(node.getTopParent()) + scale = node.getScale() + + print(f"{indent}{label}:") + print(f"{indent} 本地位置: {local_pos}") + print(f"{indent} 世界位置: {world_pos}") + print(f"{indent} 缩放: {scale}") + +def run_position_test(world, model): + """运行位置测试""" + print("\n=== 位置测试开始 ===") + + # 收集所有节点 + nodes = [] + def collect_nodes(node): + nodes.append(node) + for i in range(node.getNumChildren()): + collect_nodes(node.getChild(i)) + + collect_nodes(model) + + # 记录标准化前的位置 + print("\n--- 标准化前的位置信息 ---") + before_positions = {} + for i, node in enumerate(nodes): + label = f"节点{i+1}({node.getName()})" + print_position_info(node, label) + before_positions[node.getName()] = { + 'local': node.getPos(), + 'world': node.getPos(world.render), + 'scale': node.getScale() + } + + # 应用缩放标准化 + print("\n--- 应用缩放标准化 ---") + world.scene_manager._normalizeModelScales(model) + + # 记录标准化后的位置 + print("\n--- 标准化后的位置信息 ---") + after_positions = {} + for i, node in enumerate(nodes): + label = f"节点{i+1}({node.getName()})" + print_position_info(node, label) + after_positions[node.getName()] = { + 'local': node.getPos(), + 'world': node.getPos(world.render), + 'scale': node.getScale() + } + + # 分析位置和缩放变化 + print("\n--- 位置和缩放变化分析 ---") + for name in before_positions: + before = before_positions[name] + after = after_positions[name] + + scale_change = after['scale'] - before['scale'] + local_pos_change = after['local'] - before['local'] + world_pos_change = after['world'] - before['world'] + + local_pos_distance = local_pos_change.length() + world_pos_distance = world_pos_change.length() + + print(f"\n{name}:") + print(f" 缩放变化: {before['scale']} -> {after['scale']}") + print(f" 本地位置变化: {before['local']} -> {after['local']}") + print(f" 本地位置变化距离: {local_pos_distance:.6f}") + print(f" 世界位置变化距离: {world_pos_distance:.6f}") + + # 检查是否按比例缩放 + if before['scale'].x > 10: # 如果原来有大缩放 + expected_scale_factor = 0.01 # 期望的缩放因子 + actual_scale_factor = after['scale'].x / before['scale'].x if before['scale'].x != 0 else 0 + expected_pos = before['local'] * expected_scale_factor + pos_error = (after['local'] - expected_pos).length() + + print(f" 期望缩放因子: {expected_scale_factor}") + print(f" 实际缩放因子: {actual_scale_factor:.6f}") + print(f" 位置缩放误差: {pos_error:.6f}") + + if abs(actual_scale_factor - expected_scale_factor) < 0.001 and pos_error < 0.01: + print(f" ✓ 缩放和位置标准化正确") + else: + print(f" ⚠ 缩放或位置标准化可能有问题") + else: + print(f" ℹ 未标准化(缩放值正常)") + + print("\n=== 位置测试完成 ===") + +class ScalePositionTest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 导入我们的模块 + from main import MyWorld + + # 创建世界实例 + self.world = MyWorld() + + # 设置相机 + self.cam.setPos(0, -30, 10) + self.cam.lookAt(0, 0, 0) + + # 创建测试模型 + self.test_model = create_test_fbx_structure(self.world) + + # 设置键盘事件 + self.setupKeyEvents() + + print("\n=== 缩放位置测试程序启动 ===") + print("控制说明:") + print("I键:显示当前模型信息") + print("N键:应用缩放标准化") + print("R键:重置模型") + print("T键:运行完整位置测试") + print("Q键:退出程序") + print("================================") + + def setupKeyEvents(self): + """设置键盘事件""" + self.accept('i', self.showModelInfo) + self.accept('n', self.applyNormalization) + self.accept('r', self.resetModel) + self.accept('t', self.runPositionTest) + self.accept('q', self.quit) + self.accept('escape', self.quit) + + def showModelInfo(self): + """显示模型信息""" + print("\n=== 当前模型信息 ===") + if self.test_model: + def show_node_info(node, depth=0): + indent = " " * depth + print(f"{indent}节点: {node.getName()}") + print(f"{indent} 本地位置: {node.getPos()}") + print(f"{indent} 世界位置: {node.getPos(self.world.render)}") + print(f"{indent} 缩放: {node.getScale()}") + + for i in range(node.getNumChildren()): + child = node.getChild(i) + show_node_info(child, depth + 1) + + show_node_info(self.test_model) + print("==================") + + def applyNormalization(self): + """应用缩放标准化""" + print("\n=== 应用缩放标准化 ===") + if self.test_model: + self.world.scene_manager._normalizeModelScales(self.test_model) + print("✓ 缩放标准化完成") + else: + print("× 没有找到测试模型") + + def resetModel(self): + """重置模型""" + print("\n=== 重置模型 ===") + if self.test_model: + self.test_model.removeNode() + + # 重新创建 + self.test_model = create_test_fbx_structure(self.world) + print("✓ 模型重置完成") + + def runPositionTest(self): + """运行完整位置测试""" + if self.test_model: + run_position_test(self.world, self.test_model) + else: + print("× 没有找到测试模型") + + def quit(self): + """退出程序""" + print("\n退出缩放位置测试程序") + sys.exit() + +if __name__ == "__main__": + app = QApplication(sys.argv) + test = ScalePositionTest() test.run() \ No newline at end of file diff --git a/demo/script_gui_test.py b/demo/script_gui_test.py index 627c2ba2..5b43ddb7 100644 --- a/demo/script_gui_test.py +++ b/demo/script_gui_test.py @@ -1,113 +1,113 @@ -#!/usr/bin/env python3 -""" -脚本管理界面测试 - -这个脚本测试新添加的脚本管理界面功能: -1. 脚本菜单 -2. 脚本管理面板 -3. 脚本挂载和卸载 -4. 属性面板中的脚本信息显示 -""" - -import sys -import os - -# 确保能导入项目模块 -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - -from main import MyWorld -from ui.main_window import setup_main_window -from panda3d.core import * - -def test_script_management(): - """测试脚本管理功能""" - print("=== 脚本管理界面测试 ===") - - # 创建世界对象 - world = MyWorld() - - # 设置主窗口 - app, main_window = setup_main_window(world) - - # 创建一些测试对象 - print("\n1. 创建测试对象...") - - # 创建一个立方体 - cube = world.loader.loadModel("models/environment") - if cube: - cube.reparentTo(world.render) - cube.setPos(0, 10, 0) - cube.setScale(0.5) - cube.setName("测试立方体") - world.models.append(cube) - print("✓ 创建立方体") - - # 更新场景树 - world.updateSceneTree() - - print("\n2. 脚本系统状态:") - print(f"✓ 脚本系统已启动: {world.script_manager.engine.update_task is not None}") - print(f"✓ 热重载已启用: {world.script_manager.hot_reload_enabled}") - print(f"✓ 可用脚本数量: {len(world.getAvailableScripts())}") - - print("\n3. 测试脚本创建...") - # 创建一些测试脚本 - test_scripts = [ - ("TestRotator", "basic"), - ("TestMover", "movement"), - ("TestScaler", "basic") - ] - - for script_name, template in test_scripts: - try: - success = world.createScript(script_name, template) - if success: - print(f"✓ 创建脚本: {script_name}") - else: - print(f"✗ 创建脚本失败: {script_name}") - except Exception as e: - print(f"✗ 创建脚本出错 {script_name}: {e}") - - print("\n4. 加载所有脚本...") - try: - scripts_loaded = world.loadAllScripts() - print(f"✓ 成功加载 {len(scripts_loaded)} 个脚本") - for script_name in scripts_loaded: - print(f" - {script_name}") - except Exception as e: - print(f"✗ 加载脚本失败: {e}") - - print("\n5. 测试脚本挂载...") - if cube and world.getAvailableScripts(): - script_name = world.getAvailableScripts()[0] - try: - success = world.addScript(cube, script_name) - if success: - print(f"✓ 成功挂载脚本 {script_name} 到立方体") - - # 获取挂载的脚本 - scripts = world.getScripts(cube) - print(f"✓ 立方体上的脚本数量: {len(scripts)}") - else: - print(f"✗ 挂载脚本失败") - except Exception as e: - print(f"✗ 挂载脚本出错: {e}") - - print("\n6. 界面使用说明:") - print("─" * 50) - print("• 使用菜单栏 -> 脚本 来访问脚本功能") - print("• 在右侧停靠窗口中查看'脚本管理'标签页") - print("• 选择场景中的对象查看其脚本属性") - print("• 使用脚本面板挂载/卸载脚本") - print("• 双击脚本名称可以(将来)打开外部编辑器") - print("• 热重载功能会自动检测脚本文件变化") - print("─" * 50) - - print("\n✓ 脚本管理界面测试完成!") - print("现在可以通过GUI界面管理脚本了。") - - # 启动应用 - return app.exec_() - -if __name__ == "__main__": +#!/usr/bin/env python3 +""" +脚本管理界面测试 + +这个脚本测试新添加的脚本管理界面功能: +1. 脚本菜单 +2. 脚本管理面板 +3. 脚本挂载和卸载 +4. 属性面板中的脚本信息显示 +""" + +import sys +import os + +# 确保能导入项目模块 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from main import MyWorld +from ui.main_window import setup_main_window +from panda3d.core import * + +def test_script_management(): + """测试脚本管理功能""" + print("=== 脚本管理界面测试 ===") + + # 创建世界对象 + world = MyWorld() + + # 设置主窗口 + app, main_window = setup_main_window(world) + + # 创建一些测试对象 + print("\n1. 创建测试对象...") + + # 创建一个立方体 + cube = world.loader.loadModel("models/environment") + if cube: + cube.reparentTo(world.render) + cube.setPos(0, 10, 0) + cube.setScale(0.5) + cube.setName("测试立方体") + world.models.append(cube) + print("✓ 创建立方体") + + # 更新场景树 + world.updateSceneTree() + + print("\n2. 脚本系统状态:") + print(f"✓ 脚本系统已启动: {world.script_manager.engine.update_task is not None}") + print(f"✓ 热重载已启用: {world.script_manager.hot_reload_enabled}") + print(f"✓ 可用脚本数量: {len(world.getAvailableScripts())}") + + print("\n3. 测试脚本创建...") + # 创建一些测试脚本 + test_scripts = [ + ("TestRotator", "basic"), + ("TestMover", "movement"), + ("TestScaler", "basic") + ] + + for script_name, template in test_scripts: + try: + success = world.createScript(script_name, template) + if success: + print(f"✓ 创建脚本: {script_name}") + else: + print(f"✗ 创建脚本失败: {script_name}") + except Exception as e: + print(f"✗ 创建脚本出错 {script_name}: {e}") + + print("\n4. 加载所有脚本...") + try: + scripts_loaded = world.loadAllScripts() + print(f"✓ 成功加载 {len(scripts_loaded)} 个脚本") + for script_name in scripts_loaded: + print(f" - {script_name}") + except Exception as e: + print(f"✗ 加载脚本失败: {e}") + + print("\n5. 测试脚本挂载...") + if cube and world.getAvailableScripts(): + script_name = world.getAvailableScripts()[0] + try: + success = world.addScript(cube, script_name) + if success: + print(f"✓ 成功挂载脚本 {script_name} 到立方体") + + # 获取挂载的脚本 + scripts = world.getScripts(cube) + print(f"✓ 立方体上的脚本数量: {len(scripts)}") + else: + print(f"✗ 挂载脚本失败") + except Exception as e: + print(f"✗ 挂载脚本出错: {e}") + + print("\n6. 界面使用说明:") + print("─" * 50) + print("• 使用菜单栏 -> 脚本 来访问脚本功能") + print("• 在右侧停靠窗口中查看'脚本管理'标签页") + print("• 选择场景中的对象查看其脚本属性") + print("• 使用脚本面板挂载/卸载脚本") + print("• 双击脚本名称可以(将来)打开外部编辑器") + print("• 热重载功能会自动检测脚本文件变化") + print("─" * 50) + + print("\n✓ 脚本管理界面测试完成!") + print("现在可以通过GUI界面管理脚本了。") + + # 启动应用 + return app.exec_() + +if __name__ == "__main__": sys.exit(test_script_management()) \ No newline at end of file diff --git a/demo/script_system_demo.py b/demo/script_system_demo.py index 5659a628..2d18d18c 100644 --- a/demo/script_system_demo.py +++ b/demo/script_system_demo.py @@ -1,285 +1,285 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -脚本系统演示 -展示如何使用脚本系统创建、加载、挂载和管理脚本 -""" - -import sys -import os -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from main import MyWorld - -def test_script_system(): - """测试脚本系统的所有功能""" - print("=== 脚本系统演示开始 ===\n") - - # 创建世界实例 - world = MyWorld() - - # 1. 启动脚本系统 - print("1. 启动脚本系统") - world.startScriptSystem() - print() - - # 2. 创建测试脚本 - print("2. 创建测试脚本") - script_path1 = world.createScript("test_rotation", "basic") - script_path2 = world.createScript("test_movement", "movement") - print(f"创建的脚本文件:") - print(f" - {script_path1}") - print(f" - {script_path2}") - print() - - # 3. 加载脚本 - print("3. 加载脚本") - world.loadAllScripts() - - # 显示可用脚本 - available_scripts = world.getAvailableScripts() - print(f"可用脚本: {available_scripts}") - print() - - # 4. 创建测试对象 - print("4. 创建测试对象") - test_object1 = world.render.attachNewNode("TestObject1") - test_object1.setPos(0, 10, 0) - - test_object2 = world.render.attachNewNode("TestObject2") - test_object2.setPos(5, 10, 0) - print(f"创建测试对象: {test_object1.getName()}, {test_object2.getName()}") - print() - - # 5. 为对象添加脚本 - print("5. 为对象添加脚本") - if "ExampleScript" in available_scripts: - script_comp1 = world.addScript(test_object1, "ExampleScript") - print(f"为 {test_object1.getName()} 添加 ExampleScript") - - if "TestRotation" in available_scripts: - script_comp2 = world.addScript(test_object2, "TestRotation") - print(f"为 {test_object2.getName()} 添加 TestRotation") - print() - - # 6. 查看脚本信息 - print("6. 查看脚本信息") - for script_name in available_scripts: - script_info = world.getScriptInfo(script_name) - if script_info: - print(f"脚本: {script_name}") - print(f" 文档: {script_info.get('doc', '无')}") - print(f" 文件: {script_info.get('file', '无')}") - print(f" 方法: {script_info.get('methods', [])}") - print() - - # 7. 检查对象上的脚本 - print("7. 检查对象上的脚本") - scripts_on_obj1 = world.getScripts(test_object1) - scripts_on_obj2 = world.getScripts(test_object2) - - print(f"{test_object1.getName()} 上的脚本数量: {len(scripts_on_obj1)}") - for script_comp in scripts_on_obj1: - script_name = script_comp.script_instance.__class__.__name__ - print(f" - {script_name} (启用: {script_comp.enabled})") - - print(f"{test_object2.getName()} 上的脚本数量: {len(scripts_on_obj2)}") - for script_comp in scripts_on_obj2: - script_name = script_comp.script_instance.__class__.__name__ - print(f" - {script_name} (启用: {script_comp.enabled})") - print() - - # 8. 列出所有脚本状态 - print("8. 脚本系统整体状态") - world.listAllScripts() - - # 9. 模拟运行一段时间(让脚本执行) - print("9. 模拟脚本运行(5秒)") - print("观察控制台输出...") - - import time - start_time = time.time() - - # 手动调用几次更新来模拟游戏循环 - for i in range(10): - # 模拟脚本更新 - dt = 0.5 # 假设每次0.5秒 - for script_comp in world.script_manager.engine.script_components: - if not script_comp._started: - script_comp.start() - script_comp.update(dt) - time.sleep(0.5) - print(f" 更新 {i+1}/10 完成") - - print() - - # 10. 脚本控制演示 - print("10. 脚本控制演示") - if scripts_on_obj1: - script_comp = scripts_on_obj1[0] - print(f"禁用脚本: {script_comp.script_instance.__class__.__name__}") - script_comp.set_enabled(False) - - time.sleep(1) - - print(f"重新启用脚本: {script_comp.script_instance.__class__.__name__}") - script_comp.set_enabled(True) - print() - - # 11. 移除脚本 - print("11. 移除脚本") - if scripts_on_obj1: - script_name = scripts_on_obj1[0].script_instance.__class__.__name__ - success = world.removeScript(test_object1, script_name) - print(f"从 {test_object1.getName()} 移除 {script_name}: {'成功' if success else '失败'}") - print() - - # 12. 热重载演示 - print("12. 热重载演示") - print("热重载功能已启用,修改scripts目录中的.py文件会自动重新加载") - print("当前热重载状态:", "启用" if world.script_manager.hot_reload_enabled else "禁用") - print() - - # 13. 清理 - print("13. 清理和停止") - world.stopScriptSystem() - - print("=== 脚本系统演示完成 ===") - - -def create_custom_script_example(): - """创建自定义脚本示例""" - print("\n=== 创建自定义脚本示例 ===") - - # 确保scripts目录存在 - scripts_dir = "scripts" - if not os.path.exists(scripts_dir): - os.makedirs(scripts_dir) - - # 创建一个更复杂的示例脚本 - custom_script_content = '''#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Custom Script - 自定义复杂脚本示例 -展示脚本的高级功能 -""" - -from core.script_system import ScriptBase -import math - -class CustomScript(ScriptBase): - """自定义脚本类 - 展示高级功能""" - - def __init__(self): - super().__init__() - self.time = 0.0 - self.amplitude = 2.0 # 振幅 - self.frequency = 1.0 # 频率 - self.original_pos = None - self.rotation_speed = 45.0 # 旋转速度(度/秒) - - def start(self): - """脚本开始时调用""" - self.log("CustomScript 开始运行!") - if self.transform: - self.original_pos = self.transform.getPos() - self.log(f"记录原始位置: {self.original_pos}") - - def update(self, dt): - """每帧更新 - 实现复杂的运动模式""" - if not self.transform or not self.original_pos: - return - - self.time += dt - - # 正弦波运动 + 旋转 - offset_y = math.sin(self.time * self.frequency) * self.amplitude - offset_z = math.cos(self.time * self.frequency * 0.5) * self.amplitude * 0.5 - - # 更新位置 - new_pos = ( - self.original_pos.x, - self.original_pos.y + offset_y, - self.original_pos.z + offset_z - ) - self.transform.setPos(*new_pos) - - # 旋转 - current_h = self.transform.getH() - new_h = current_h + self.rotation_speed * dt - self.transform.setH(new_h) - - # 每5秒输出一次状态 - if int(self.time) % 5 == 0 and int(self.time * 10) % 10 == 0: - self.log(f"运行时间: {self.time:.1f}s, 位置: {new_pos}") - - def on_destroy(self): - """脚本销毁时调用""" - self.log("CustomScript 被销毁") - - # 恢复原始位置 - if self.transform and self.original_pos: - self.transform.setPos(self.original_pos) - self.log("已恢复原始位置") - - def on_enable(self): - """脚本启用时调用""" - self.log("CustomScript 被启用") - - def on_disable(self): - """脚本禁用时调用""" - self.log("CustomScript 被禁用") -''' - - script_path = os.path.join(scripts_dir, "custom_script.py") - with open(script_path, 'w', encoding='utf-8') as f: - f.write(custom_script_content) - - print(f"✓ 创建自定义脚本: {script_path}") - return script_path - - -def usage_examples(): - """使用示例""" - print("\n=== 脚本系统使用示例 ===") - - print(""" -# 基本使用流程: - -1. 启动脚本系统 - world.startScriptSystem() - -2. 创建脚本文件 - script_path = world.createScript("my_script", "basic") - -3. 加载脚本 - world.loadAllScripts() - -4. 为对象添加脚本 - my_object = world.render.attachNewNode("MyObject") - world.addScript(my_object, "MyScript") - -5. 管理脚本 - scripts = world.getScripts(my_object) - world.removeScript(my_object, "MyScript") - -# 高级功能: - -- 热重载:修改脚本文件会自动重新加载 -- 脚本调试:通过控制台查看脚本状态 -- 脚本模板:支持多种脚本模板 -- 生命周期管理:完整的start/update/destroy循环 -""") - - -if __name__ == "__main__": - # 创建自定义脚本示例 - create_custom_script_example() - - # 运行演示 - test_script_system() - - # 显示使用示例 +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +脚本系统演示 +展示如何使用脚本系统创建、加载、挂载和管理脚本 +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from main import MyWorld + +def test_script_system(): + """测试脚本系统的所有功能""" + print("=== 脚本系统演示开始 ===\n") + + # 创建世界实例 + world = MyWorld() + + # 1. 启动脚本系统 + print("1. 启动脚本系统") + world.startScriptSystem() + print() + + # 2. 创建测试脚本 + print("2. 创建测试脚本") + script_path1 = world.createScript("test_rotation", "basic") + script_path2 = world.createScript("test_movement", "movement") + print(f"创建的脚本文件:") + print(f" - {script_path1}") + print(f" - {script_path2}") + print() + + # 3. 加载脚本 + print("3. 加载脚本") + world.loadAllScripts() + + # 显示可用脚本 + available_scripts = world.getAvailableScripts() + print(f"可用脚本: {available_scripts}") + print() + + # 4. 创建测试对象 + print("4. 创建测试对象") + test_object1 = world.render.attachNewNode("TestObject1") + test_object1.setPos(0, 10, 0) + + test_object2 = world.render.attachNewNode("TestObject2") + test_object2.setPos(5, 10, 0) + print(f"创建测试对象: {test_object1.getName()}, {test_object2.getName()}") + print() + + # 5. 为对象添加脚本 + print("5. 为对象添加脚本") + if "ExampleScript" in available_scripts: + script_comp1 = world.addScript(test_object1, "ExampleScript") + print(f"为 {test_object1.getName()} 添加 ExampleScript") + + if "TestRotation" in available_scripts: + script_comp2 = world.addScript(test_object2, "TestRotation") + print(f"为 {test_object2.getName()} 添加 TestRotation") + print() + + # 6. 查看脚本信息 + print("6. 查看脚本信息") + for script_name in available_scripts: + script_info = world.getScriptInfo(script_name) + if script_info: + print(f"脚本: {script_name}") + print(f" 文档: {script_info.get('doc', '无')}") + print(f" 文件: {script_info.get('file', '无')}") + print(f" 方法: {script_info.get('methods', [])}") + print() + + # 7. 检查对象上的脚本 + print("7. 检查对象上的脚本") + scripts_on_obj1 = world.getScripts(test_object1) + scripts_on_obj2 = world.getScripts(test_object2) + + print(f"{test_object1.getName()} 上的脚本数量: {len(scripts_on_obj1)}") + for script_comp in scripts_on_obj1: + script_name = script_comp.script_instance.__class__.__name__ + print(f" - {script_name} (启用: {script_comp.enabled})") + + print(f"{test_object2.getName()} 上的脚本数量: {len(scripts_on_obj2)}") + for script_comp in scripts_on_obj2: + script_name = script_comp.script_instance.__class__.__name__ + print(f" - {script_name} (启用: {script_comp.enabled})") + print() + + # 8. 列出所有脚本状态 + print("8. 脚本系统整体状态") + world.listAllScripts() + + # 9. 模拟运行一段时间(让脚本执行) + print("9. 模拟脚本运行(5秒)") + print("观察控制台输出...") + + import time + start_time = time.time() + + # 手动调用几次更新来模拟游戏循环 + for i in range(10): + # 模拟脚本更新 + dt = 0.5 # 假设每次0.5秒 + for script_comp in world.script_manager.engine.script_components: + if not script_comp._started: + script_comp.start() + script_comp.update(dt) + time.sleep(0.5) + print(f" 更新 {i+1}/10 完成") + + print() + + # 10. 脚本控制演示 + print("10. 脚本控制演示") + if scripts_on_obj1: + script_comp = scripts_on_obj1[0] + print(f"禁用脚本: {script_comp.script_instance.__class__.__name__}") + script_comp.set_enabled(False) + + time.sleep(1) + + print(f"重新启用脚本: {script_comp.script_instance.__class__.__name__}") + script_comp.set_enabled(True) + print() + + # 11. 移除脚本 + print("11. 移除脚本") + if scripts_on_obj1: + script_name = scripts_on_obj1[0].script_instance.__class__.__name__ + success = world.removeScript(test_object1, script_name) + print(f"从 {test_object1.getName()} 移除 {script_name}: {'成功' if success else '失败'}") + print() + + # 12. 热重载演示 + print("12. 热重载演示") + print("热重载功能已启用,修改scripts目录中的.py文件会自动重新加载") + print("当前热重载状态:", "启用" if world.script_manager.hot_reload_enabled else "禁用") + print() + + # 13. 清理 + print("13. 清理和停止") + world.stopScriptSystem() + + print("=== 脚本系统演示完成 ===") + + +def create_custom_script_example(): + """创建自定义脚本示例""" + print("\n=== 创建自定义脚本示例 ===") + + # 确保scripts目录存在 + scripts_dir = "scripts" + if not os.path.exists(scripts_dir): + os.makedirs(scripts_dir) + + # 创建一个更复杂的示例脚本 + custom_script_content = '''#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Custom Script - 自定义复杂脚本示例 +展示脚本的高级功能 +""" + +from core.script_system import ScriptBase +import math + +class CustomScript(ScriptBase): + """自定义脚本类 - 展示高级功能""" + + def __init__(self): + super().__init__() + self.time = 0.0 + self.amplitude = 2.0 # 振幅 + self.frequency = 1.0 # 频率 + self.original_pos = None + self.rotation_speed = 45.0 # 旋转速度(度/秒) + + def start(self): + """脚本开始时调用""" + self.log("CustomScript 开始运行!") + if self.transform: + self.original_pos = self.transform.getPos() + self.log(f"记录原始位置: {self.original_pos}") + + def update(self, dt): + """每帧更新 - 实现复杂的运动模式""" + if not self.transform or not self.original_pos: + return + + self.time += dt + + # 正弦波运动 + 旋转 + offset_y = math.sin(self.time * self.frequency) * self.amplitude + offset_z = math.cos(self.time * self.frequency * 0.5) * self.amplitude * 0.5 + + # 更新位置 + new_pos = ( + self.original_pos.x, + self.original_pos.y + offset_y, + self.original_pos.z + offset_z + ) + self.transform.setPos(*new_pos) + + # 旋转 + current_h = self.transform.getH() + new_h = current_h + self.rotation_speed * dt + self.transform.setH(new_h) + + # 每5秒输出一次状态 + if int(self.time) % 5 == 0 and int(self.time * 10) % 10 == 0: + self.log(f"运行时间: {self.time:.1f}s, 位置: {new_pos}") + + def on_destroy(self): + """脚本销毁时调用""" + self.log("CustomScript 被销毁") + + # 恢复原始位置 + if self.transform and self.original_pos: + self.transform.setPos(self.original_pos) + self.log("已恢复原始位置") + + def on_enable(self): + """脚本启用时调用""" + self.log("CustomScript 被启用") + + def on_disable(self): + """脚本禁用时调用""" + self.log("CustomScript 被禁用") +''' + + script_path = os.path.join(scripts_dir, "custom_script.py") + with open(script_path, 'w', encoding='utf-8') as f: + f.write(custom_script_content) + + print(f"✓ 创建自定义脚本: {script_path}") + return script_path + + +def usage_examples(): + """使用示例""" + print("\n=== 脚本系统使用示例 ===") + + print(""" +# 基本使用流程: + +1. 启动脚本系统 + world.startScriptSystem() + +2. 创建脚本文件 + script_path = world.createScript("my_script", "basic") + +3. 加载脚本 + world.loadAllScripts() + +4. 为对象添加脚本 + my_object = world.render.attachNewNode("MyObject") + world.addScript(my_object, "MyScript") + +5. 管理脚本 + scripts = world.getScripts(my_object) + world.removeScript(my_object, "MyScript") + +# 高级功能: + +- 热重载:修改脚本文件会自动重新加载 +- 脚本调试:通过控制台查看脚本状态 +- 脚本模板:支持多种脚本模板 +- 生命周期管理:完整的start/update/destroy循环 +""") + + +if __name__ == "__main__": + # 创建自定义脚本示例 + create_custom_script_example() + + # 运行演示 + test_script_system() + + # 显示使用示例 usage_examples() \ No newline at end of file diff --git a/demo/selection_test.py b/demo/selection_test.py index c4e2f27c..1ab29321 100644 --- a/demo/selection_test.py +++ b/demo/selection_test.py @@ -1,249 +1,249 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -选择功能测试 - 验证模型选择功能是否正常工作 - -测试内容: -1. 验证模型碰撞检测设置 -2. 测试射线检测和选择功能 -3. 验证选择框和坐标轴显示 - -控制说明: -- 鼠标左键:点击选择模型 -- I键:显示当前选择信息 -- C键:显示碰撞检测信息 -- S键:切换碰撞体显示(调试用) -- R键:切换射线显示 -- Q键:退出 -""" - -import sys -import os -sys.path.append('..') # 添加父目录到路径 - -from direct.showbase.ShowBase import ShowBase -from panda3d.core import (CardMaker, Vec3, Point3, Material, - ModelRoot, PandaNode, CollisionNode, CollisionSphere, - BitMask32) -from PyQt5.QtWidgets import QApplication - -def create_test_models(world): - """创建测试模型""" - print("\n=== 创建测试模型 ===") - - # 创建几个简单的测试模型 - test_models = [] - - positions = [ - (0, 0, 1), # 中心 - (5, 0, 1), # 右侧 - (-5, 0, 1), # 左侧 - (0, 5, 1), # 后方 - ] - - colors = [ - (1, 0, 0, 1), # 红色 - (0, 1, 0, 1), # 绿色 - (0, 0, 1, 1), # 蓝色 - (1, 1, 0, 1), # 黄色 - ] - - for i, (pos, color) in enumerate(zip(positions, colors)): - # 创建立方体 - cm = CardMaker(f'test_cube_{i}') - cm.setFrame(-1, 1, -1, 1) - - # 创建模型根节点 - model_root = world.render.attachNewNode(ModelRoot(f"TestCube_{i}")) - - # 创建6个面(简单立方体) - faces = [ - # 前面和后面 - (0, 0, 1, 0, 0, 1), # 前面 Z+ - (0, 0, -1, 0, 0, -1), # 后面 Z- - # 左面和右面 - (-1, 0, 0, 1, 0, 0), # 左面 X- - (1, 0, 0, -1, 0, 0), # 右面 X+ - # 顶面和底面 - (0, 1, 0, 0, -1, 0), # 顶面 Y+ - (0, -1, 0, 0, 1, 0), # 底面 Y- - ] - - for j, (x, y, z, nx, ny, nz) in enumerate(faces): - face = model_root.attachNewNode(cm.generate()) - face.setPos(x, y, z) - if nx != 0: # X面 - face.setH(90 if nx > 0 else -90) - elif ny != 0: # Y面 - face.setP(90 if ny > 0 else -90) - # Z面保持默认朝向 - - # 设置位置和颜色 - model_root.setPos(*pos) - model_root.setColor(*color) - - # 创建材质 - material = Material() - material.setDiffuse(color) - material.setAmbient((0.2, 0.2, 0.2, 1.0)) - material.setSpecular((0.5, 0.5, 0.5, 1.0)) - material.setShininess(32.0) - model_root.setMaterial(material) - - # 设置标签 - model_root.setTag("file", f"TestCube_{i}") - model_root.setTag("is_model_root", "1") - - # 添加到场景管理器 - world.scene_manager.models.append(model_root) - test_models.append(model_root) - - print(f"✓ 创建测试立方体 {i}: 位置{pos}, 颜色{color[:3]}") - - # 为所有模型设置碰撞检测 - print("\n=== 设置碰撞检测 ===") - for model in test_models: - world.scene_manager.setupCollision(model) - print(f"✓ 为 {model.getName()} 设置碰撞检测") - - # 更新场景树 - world.scene_manager.updateSceneTree() - - print("✓ 测试模型创建完成") - return test_models - -def show_collision_info(world): - """显示碰撞检测信息""" - print("\n=== 碰撞检测信息 ===") - - for i, model in enumerate(world.scene_manager.models): - print(f"\n模型 {i+1}: {model.getName()}") - print(f"位置: {model.getPos()}") - print(f"边界: {model.getBounds()}") - - # 查找碰撞节点 - collision_nodes = [] - for child in model.getChildren(): - if isinstance(child.node(), CollisionNode): - collision_nodes.append(child) - - if collision_nodes: - print(f"碰撞节点数量: {len(collision_nodes)}") - for j, cnode in enumerate(collision_nodes): - cn = cnode.node() - print(f" 碰撞节点 {j+1}: {cn.getName()}") - print(f" 碰撞掩码: {cn.getIntoCollideMask()}") - print(f" 碰撞体数量: {cn.getNumSolids()}") - for k in range(cn.getNumSolids()): - solid = cn.getSolid(k) - print(f" 碰撞体 {k+1}: {solid.__class__.__name__}") - else: - print("❌ 没有碰撞节点") - - print("==================") - -def show_selection_info(world): - """显示当前选择信息""" - print("\n=== 当前选择信息 ===") - - selected = world.selection.getSelectedNode() - if selected: - print(f"选中节点: {selected.getName()}") - print(f"位置: {selected.getPos()}") - print(f"缩放: {selected.getScale()}") - print(f"有选择框: {bool(world.selection.selectionBox)}") - print(f"有坐标轴: {bool(world.selection.gizmo)}") - else: - print("当前没有选中任何节点") - - print(f"当前工具: {world.currentTool}") - print("=================") - -def toggle_collision_display(world): - """切换碰撞体显示""" - print("\n=== 切换碰撞体显示 ===") - - count = 0 - for model in world.scene_manager.models: - for child in model.getChildren(): - if isinstance(child.node(), CollisionNode): - if child.isHidden(): - child.show() - count += 1 - else: - child.hide() - - if count > 0: - print(f"显示了 {count} 个碰撞体") - else: - print("隐藏了所有碰撞体") - -class SelectionTest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 导入我们的模块 - from main import MyWorld - - # 创建世界实例 - self.world = MyWorld() - - # 设置相机 - self.cam.setPos(0, -20, 10) - self.cam.lookAt(0, 0, 0) - - # 创建测试模型 - self.test_models = create_test_models(self.world) - - # 设置键盘事件 - self.setupKeyEvents() - - print("\n=== 选择功能测试程序启动 ===") - print("控制说明:") - print("鼠标左键:点击选择模型") - print("I键:显示当前选择信息") - print("C键:显示碰撞检测信息") - print("S键:切换碰撞体显示(调试用)") - print("R键:切换射线显示") - print("Q键:退出程序") - print("================================") - - # 显示初始信息 - show_collision_info(self.world) - - def setupKeyEvents(self): - """设置键盘事件""" - self.accept('i', self.showSelectionInfo) - self.accept('c', self.showCollisionInfo) - self.accept('s', self.toggleCollisionDisplay) - self.accept('r', self.toggleRayDisplay) - self.accept('q', self.quit) - self.accept('escape', self.quit) - - def showSelectionInfo(self): - """显示选择信息""" - show_selection_info(self.world) - - def showCollisionInfo(self): - """显示碰撞信息""" - show_collision_info(self.world) - - def toggleCollisionDisplay(self): - """切换碰撞体显示""" - toggle_collision_display(self.world) - - def toggleRayDisplay(self): - """切换射线显示""" - self.world.toggleRayDisplay() - print(f"射线显示: {'开启' if self.world.getRayDisplay() else '关闭'}") - - def quit(self): - """退出程序""" - print("\n退出选择功能测试程序") - sys.exit() - -if __name__ == "__main__": - app = QApplication(sys.argv) - test = SelectionTest() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +选择功能测试 - 验证模型选择功能是否正常工作 + +测试内容: +1. 验证模型碰撞检测设置 +2. 测试射线检测和选择功能 +3. 验证选择框和坐标轴显示 + +控制说明: +- 鼠标左键:点击选择模型 +- I键:显示当前选择信息 +- C键:显示碰撞检测信息 +- S键:切换碰撞体显示(调试用) +- R键:切换射线显示 +- Q键:退出 +""" + +import sys +import os +sys.path.append('..') # 添加父目录到路径 + +from direct.showbase.ShowBase import ShowBase +from panda3d.core import (CardMaker, Vec3, Point3, Material, + ModelRoot, PandaNode, CollisionNode, CollisionSphere, + BitMask32) +from PyQt5.QtWidgets import QApplication + +def create_test_models(world): + """创建测试模型""" + print("\n=== 创建测试模型 ===") + + # 创建几个简单的测试模型 + test_models = [] + + positions = [ + (0, 0, 1), # 中心 + (5, 0, 1), # 右侧 + (-5, 0, 1), # 左侧 + (0, 5, 1), # 后方 + ] + + colors = [ + (1, 0, 0, 1), # 红色 + (0, 1, 0, 1), # 绿色 + (0, 0, 1, 1), # 蓝色 + (1, 1, 0, 1), # 黄色 + ] + + for i, (pos, color) in enumerate(zip(positions, colors)): + # 创建立方体 + cm = CardMaker(f'test_cube_{i}') + cm.setFrame(-1, 1, -1, 1) + + # 创建模型根节点 + model_root = world.render.attachNewNode(ModelRoot(f"TestCube_{i}")) + + # 创建6个面(简单立方体) + faces = [ + # 前面和后面 + (0, 0, 1, 0, 0, 1), # 前面 Z+ + (0, 0, -1, 0, 0, -1), # 后面 Z- + # 左面和右面 + (-1, 0, 0, 1, 0, 0), # 左面 X- + (1, 0, 0, -1, 0, 0), # 右面 X+ + # 顶面和底面 + (0, 1, 0, 0, -1, 0), # 顶面 Y+ + (0, -1, 0, 0, 1, 0), # 底面 Y- + ] + + for j, (x, y, z, nx, ny, nz) in enumerate(faces): + face = model_root.attachNewNode(cm.generate()) + face.setPos(x, y, z) + if nx != 0: # X面 + face.setH(90 if nx > 0 else -90) + elif ny != 0: # Y面 + face.setP(90 if ny > 0 else -90) + # Z面保持默认朝向 + + # 设置位置和颜色 + model_root.setPos(*pos) + model_root.setColor(*color) + + # 创建材质 + material = Material() + material.setDiffuse(color) + material.setAmbient((0.2, 0.2, 0.2, 1.0)) + material.setSpecular((0.5, 0.5, 0.5, 1.0)) + material.setShininess(32.0) + model_root.setMaterial(material) + + # 设置标签 + model_root.setTag("file", f"TestCube_{i}") + model_root.setTag("is_model_root", "1") + + # 添加到场景管理器 + world.scene_manager.models.append(model_root) + test_models.append(model_root) + + print(f"✓ 创建测试立方体 {i}: 位置{pos}, 颜色{color[:3]}") + + # 为所有模型设置碰撞检测 + print("\n=== 设置碰撞检测 ===") + for model in test_models: + world.scene_manager.setupCollision(model) + print(f"✓ 为 {model.getName()} 设置碰撞检测") + + # 更新场景树 + world.scene_manager.updateSceneTree() + + print("✓ 测试模型创建完成") + return test_models + +def show_collision_info(world): + """显示碰撞检测信息""" + print("\n=== 碰撞检测信息 ===") + + for i, model in enumerate(world.scene_manager.models): + print(f"\n模型 {i+1}: {model.getName()}") + print(f"位置: {model.getPos()}") + print(f"边界: {model.getBounds()}") + + # 查找碰撞节点 + collision_nodes = [] + for child in model.getChildren(): + if isinstance(child.node(), CollisionNode): + collision_nodes.append(child) + + if collision_nodes: + print(f"碰撞节点数量: {len(collision_nodes)}") + for j, cnode in enumerate(collision_nodes): + cn = cnode.node() + print(f" 碰撞节点 {j+1}: {cn.getName()}") + print(f" 碰撞掩码: {cn.getIntoCollideMask()}") + print(f" 碰撞体数量: {cn.getNumSolids()}") + for k in range(cn.getNumSolids()): + solid = cn.getSolid(k) + print(f" 碰撞体 {k+1}: {solid.__class__.__name__}") + else: + print("❌ 没有碰撞节点") + + print("==================") + +def show_selection_info(world): + """显示当前选择信息""" + print("\n=== 当前选择信息 ===") + + selected = world.selection.getSelectedNode() + if selected: + print(f"选中节点: {selected.getName()}") + print(f"位置: {selected.getPos()}") + print(f"缩放: {selected.getScale()}") + print(f"有选择框: {bool(world.selection.selectionBox)}") + print(f"有坐标轴: {bool(world.selection.gizmo)}") + else: + print("当前没有选中任何节点") + + print(f"当前工具: {world.currentTool}") + print("=================") + +def toggle_collision_display(world): + """切换碰撞体显示""" + print("\n=== 切换碰撞体显示 ===") + + count = 0 + for model in world.scene_manager.models: + for child in model.getChildren(): + if isinstance(child.node(), CollisionNode): + if child.isHidden(): + child.show() + count += 1 + else: + child.hide() + + if count > 0: + print(f"显示了 {count} 个碰撞体") + else: + print("隐藏了所有碰撞体") + +class SelectionTest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 导入我们的模块 + from main import MyWorld + + # 创建世界实例 + self.world = MyWorld() + + # 设置相机 + self.cam.setPos(0, -20, 10) + self.cam.lookAt(0, 0, 0) + + # 创建测试模型 + self.test_models = create_test_models(self.world) + + # 设置键盘事件 + self.setupKeyEvents() + + print("\n=== 选择功能测试程序启动 ===") + print("控制说明:") + print("鼠标左键:点击选择模型") + print("I键:显示当前选择信息") + print("C键:显示碰撞检测信息") + print("S键:切换碰撞体显示(调试用)") + print("R键:切换射线显示") + print("Q键:退出程序") + print("================================") + + # 显示初始信息 + show_collision_info(self.world) + + def setupKeyEvents(self): + """设置键盘事件""" + self.accept('i', self.showSelectionInfo) + self.accept('c', self.showCollisionInfo) + self.accept('s', self.toggleCollisionDisplay) + self.accept('r', self.toggleRayDisplay) + self.accept('q', self.quit) + self.accept('escape', self.quit) + + def showSelectionInfo(self): + """显示选择信息""" + show_selection_info(self.world) + + def showCollisionInfo(self): + """显示碰撞信息""" + show_collision_info(self.world) + + def toggleCollisionDisplay(self): + """切换碰撞体显示""" + toggle_collision_display(self.world) + + def toggleRayDisplay(self): + """切换射线显示""" + self.world.toggleRayDisplay() + print(f"射线显示: {'开启' if self.world.getRayDisplay() else '关闭'}") + + def quit(self): + """退出程序""" + print("\n退出选择功能测试程序") + sys.exit() + +if __name__ == "__main__": + app = QApplication(sys.argv) + test = SelectionTest() test.run() \ No newline at end of file diff --git a/demo/simple_gui_test.py b/demo/simple_gui_test.py index ffeba61d..fa6b365e 100644 --- a/demo/simple_gui_test.py +++ b/demo/simple_gui_test.py @@ -1,299 +1,299 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -""" -PANDA3D 简化版GUI功能测试 -演示DirectGUI基本组件的使用 -""" - -from direct.showbase.ShowBase import ShowBase -from direct.gui.DirectGui import * -from direct.gui.OnscreenText import OnscreenText -from panda3d.core import * - -class SimpleGUITest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 设置窗口属性和背景 - wp = WindowProperties() - wp.setTitle("PANDA3D 简化GUI测试") - self.win.requestProperties(wp) - self.setBackgroundColor(0.1, 0.2, 0.4) - - # 标题文本 - self.title = OnscreenText( - text="PANDA3D DirectGUI 基础测试", - pos=(0, 0.85), - scale=0.08, - fg=(1, 1, 1, 1), - align=TextNode.ACenter - ) - - # 创建GUI组件 - self.setupGUI() - - # 尝试加载中文字体 - self.chinese_font = self.loadChineseFont() - - # 变量用于存储状态 - self.clickCount = 0 - - print("="*50) - print("PANDA3D GUI测试程序启动") - print(f"中文字体加载状态: {'成功' if self.chinese_font else '失败,使用默认字体'}") - print("尝试点击不同的GUI组件来测试功能") - print("="*50) - - def setupGUI(self): - """设置所有GUI组件""" - - # 1. 按钮测试 - self.createButtons() - - # 2. 文本输入框测试 - self.createTextEntry() - - # 3. 滑块测试 - self.createSlider() - - # 4. 复选框测试 - self.createCheckbox() - - # 5. 选项菜单测试 - self.createOptionMenu() - - # 6. 状态显示 - self.createStatusDisplay() - - def createButtons(self): - """创建按钮组件""" - # 主要测试按钮 - self.testButton = DirectButton( - text="点击测试", - pos=(-0.6, 0, 0.5), - scale=0.08, - command=self.onTestButtonClick, - frameColor=(0.2, 0.7, 0.2, 1), - text_fg=(1, 1, 1, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - # 重置按钮 - self.resetButton = DirectButton( - text="重置", - pos=(0, 0, 0.5), - scale=0.08, - command=self.onResetClick, - frameColor=(0.7, 0.7, 0.2, 1), - text_fg=(1, 1, 1, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - # 退出按钮 - self.exitButton = DirectButton( - text="退出", - pos=(0.6, 0, 0.5), - scale=0.08, - command=self.exitProgram, - frameColor=(0.7, 0.2, 0.2, 1), - text_fg=(1, 1, 1, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - def createTextEntry(self): - """创建文本输入框""" - # 输入框标签 - self.entryLabel = DirectLabel( - text="文本输入:", - pos=(-0.8, 0, 0.2), - scale=0.06, - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 1, 1) - ) - - # 文本输入框 - self.textEntry = DirectEntry( - text="", - pos=(-0.3, 0, 0.2), - scale=0.06, - command=self.onTextSubmit, - initialText="在此输入...", - numLines=1, - width=15, - focus=0 - ) - - def createSlider(self): - """创建滑块""" - # 滑块标签 - self.sliderLabel = DirectLabel( - text="滑块:", - pos=(-0.8, 0, 0), - scale=0.06, - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 1, 1) - ) - - # 滑块控件 - self.slider = DirectSlider( - range=(0, 100), - value=50, - pos=(-0.2, 0, 0), - scale=0.5, - command=self.onSliderChange, - orientation=DGG.HORIZONTAL - ) - - # 滑块值显示 - self.sliderValueLabel = DirectLabel( - text="值: 50", - pos=(0.4, 0, 0), - scale=0.06, - frameColor=(0, 0, 0, 0), - text_fg=(0, 1, 1, 1) - ) - - def createCheckbox(self): - """创建复选框""" - self.checkbox = DirectCheckButton( - text="启用特殊模式", - pos=(-0.6, 0, -0.2), - scale=0.06, - command=self.onCheckboxToggle - ) - - def createOptionMenu(self): - """创建选项菜单""" - # 选项菜单标签 - self.menuLabel = DirectLabel( - text="选择:", - pos=(-0.8, 0, -0.4), - scale=0.06, - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 1, 1) - ) - - # 选项列表 - options = ["选项1", "选项2", "选项3", "高级选项"] - self.optionMenu = DirectOptionMenu( - text="请选择", - pos=(-0.3, 0, -0.4), - scale=0.06, - items=options, - initialitem=0, - command=self.onOptionSelect - ) - - def createStatusDisplay(self): - """创建状态显示区域""" - # 状态标题 - self.statusTitle = DirectLabel( - text="状态信息:", - pos=(-0.95, 0, -0.7), - scale=0.05, - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 0, 1), - text_align=TextNode.ALeft - ) - - # 状态显示文本 - self.statusText = OnscreenText( - text="程序就绪", - pos=(-0.95, -0.8), - scale=0.05, - fg=(0, 1, 0, 1), - align=TextNode.ALeft - ) - - # 计数器显示 - self.counterText = OnscreenText( - text="点击次数: 0", - pos=(0.5, -0.8), - scale=0.05, - fg=(1, 0.5, 0, 1), - align=TextNode.ALeft - ) - - # 事件处理函数 - def onTestButtonClick(self): - """测试按钮点击事件""" - self.clickCount += 1 - self.updateStatus(f"按钮被点击! 第{self.clickCount}次") - self.counterText.setText(f"点击次数: {self.clickCount}") - print(f"测试按钮被点击,当前点击次数: {self.clickCount}") - - def onResetClick(self): - """重置按钮点击事件""" - self.clickCount = 0 - self.slider['value'] = 50 - self.sliderValueLabel['text'] = "值: 50" - self.textEntry.enterText("") - self.textEntry.set("在此输入...") - self.checkbox['indicatorValue'] = 0 - self.optionMenu.set(0) - self.updateStatus("所有设置已重置") - self.counterText.setText("点击次数: 0") - print("界面已重置") - - def loadChineseFont(self): - """尝试加载中文字体""" - import os - font_paths = [ - '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', - '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', - '/usr/share/fonts/truetype/arphic/ukai.ttc', - '/usr/share/fonts/truetype/arphic/uming.ttc', - ] - - for font_path in font_paths: - if os.path.exists(font_path): - try: - font = self.loader.loadFont(font_path) - if font: - return font - except: - continue - return None - - def onTextSubmit(self, text): - """文本提交事件""" - if text.strip(): - self.updateStatus(f"输入文本: {text}") - print(f"用户输入: {text}") - else: - self.updateStatus("输入为空") - - def onSliderChange(self): - """滑块变化事件""" - value = int(self.slider['value']) - self.sliderValueLabel['text'] = f"值: {value}" - self.updateStatus(f"滑块值: {value}") - print(f"滑块值变更为: {value}") - - def onCheckboxToggle(self, status): - """复选框切换事件""" - state = "开启" if status else "关闭" - self.updateStatus(f"特殊模式: {state}") - print(f"复选框状态: {state}") - - def onOptionSelect(self, selectedOption): - """选项菜单选择事件""" - self.updateStatus(f"选择: {selectedOption}") - print(f"选项变更为: {selectedOption}") - - def updateStatus(self, message): - """更新状态显示""" - self.statusText.setText(message) - - def exitProgram(self): - """退出程序""" - print("用户点击退出按钮") - print("程序即将关闭...") - self.userExit() - -# 程序入口 -if __name__ == "__main__": - print("启动PANDA3D GUI测试程序...") - app = SimpleGUITest() +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +PANDA3D 简化版GUI功能测试 +演示DirectGUI基本组件的使用 +""" + +from direct.showbase.ShowBase import ShowBase +from direct.gui.DirectGui import * +from direct.gui.OnscreenText import OnscreenText +from panda3d.core import * + +class SimpleGUITest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 设置窗口属性和背景 + wp = WindowProperties() + wp.setTitle("PANDA3D 简化GUI测试") + self.win.requestProperties(wp) + self.setBackgroundColor(0.1, 0.2, 0.4) + + # 标题文本 + self.title = OnscreenText( + text="PANDA3D DirectGUI 基础测试", + pos=(0, 0.85), + scale=0.08, + fg=(1, 1, 1, 1), + align=TextNode.ACenter + ) + + # 创建GUI组件 + self.setupGUI() + + # 尝试加载中文字体 + self.chinese_font = self.loadChineseFont() + + # 变量用于存储状态 + self.clickCount = 0 + + print("="*50) + print("PANDA3D GUI测试程序启动") + print(f"中文字体加载状态: {'成功' if self.chinese_font else '失败,使用默认字体'}") + print("尝试点击不同的GUI组件来测试功能") + print("="*50) + + def setupGUI(self): + """设置所有GUI组件""" + + # 1. 按钮测试 + self.createButtons() + + # 2. 文本输入框测试 + self.createTextEntry() + + # 3. 滑块测试 + self.createSlider() + + # 4. 复选框测试 + self.createCheckbox() + + # 5. 选项菜单测试 + self.createOptionMenu() + + # 6. 状态显示 + self.createStatusDisplay() + + def createButtons(self): + """创建按钮组件""" + # 主要测试按钮 + self.testButton = DirectButton( + text="点击测试", + pos=(-0.6, 0, 0.5), + scale=0.08, + command=self.onTestButtonClick, + frameColor=(0.2, 0.7, 0.2, 1), + text_fg=(1, 1, 1, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + # 重置按钮 + self.resetButton = DirectButton( + text="重置", + pos=(0, 0, 0.5), + scale=0.08, + command=self.onResetClick, + frameColor=(0.7, 0.7, 0.2, 1), + text_fg=(1, 1, 1, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + # 退出按钮 + self.exitButton = DirectButton( + text="退出", + pos=(0.6, 0, 0.5), + scale=0.08, + command=self.exitProgram, + frameColor=(0.7, 0.2, 0.2, 1), + text_fg=(1, 1, 1, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + def createTextEntry(self): + """创建文本输入框""" + # 输入框标签 + self.entryLabel = DirectLabel( + text="文本输入:", + pos=(-0.8, 0, 0.2), + scale=0.06, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1) + ) + + # 文本输入框 + self.textEntry = DirectEntry( + text="", + pos=(-0.3, 0, 0.2), + scale=0.06, + command=self.onTextSubmit, + initialText="在此输入...", + numLines=1, + width=15, + focus=0 + ) + + def createSlider(self): + """创建滑块""" + # 滑块标签 + self.sliderLabel = DirectLabel( + text="滑块:", + pos=(-0.8, 0, 0), + scale=0.06, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1) + ) + + # 滑块控件 + self.slider = DirectSlider( + range=(0, 100), + value=50, + pos=(-0.2, 0, 0), + scale=0.5, + command=self.onSliderChange, + orientation=DGG.HORIZONTAL + ) + + # 滑块值显示 + self.sliderValueLabel = DirectLabel( + text="值: 50", + pos=(0.4, 0, 0), + scale=0.06, + frameColor=(0, 0, 0, 0), + text_fg=(0, 1, 1, 1) + ) + + def createCheckbox(self): + """创建复选框""" + self.checkbox = DirectCheckButton( + text="启用特殊模式", + pos=(-0.6, 0, -0.2), + scale=0.06, + command=self.onCheckboxToggle + ) + + def createOptionMenu(self): + """创建选项菜单""" + # 选项菜单标签 + self.menuLabel = DirectLabel( + text="选择:", + pos=(-0.8, 0, -0.4), + scale=0.06, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1) + ) + + # 选项列表 + options = ["选项1", "选项2", "选项3", "高级选项"] + self.optionMenu = DirectOptionMenu( + text="请选择", + pos=(-0.3, 0, -0.4), + scale=0.06, + items=options, + initialitem=0, + command=self.onOptionSelect + ) + + def createStatusDisplay(self): + """创建状态显示区域""" + # 状态标题 + self.statusTitle = DirectLabel( + text="状态信息:", + pos=(-0.95, 0, -0.7), + scale=0.05, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 0, 1), + text_align=TextNode.ALeft + ) + + # 状态显示文本 + self.statusText = OnscreenText( + text="程序就绪", + pos=(-0.95, -0.8), + scale=0.05, + fg=(0, 1, 0, 1), + align=TextNode.ALeft + ) + + # 计数器显示 + self.counterText = OnscreenText( + text="点击次数: 0", + pos=(0.5, -0.8), + scale=0.05, + fg=(1, 0.5, 0, 1), + align=TextNode.ALeft + ) + + # 事件处理函数 + def onTestButtonClick(self): + """测试按钮点击事件""" + self.clickCount += 1 + self.updateStatus(f"按钮被点击! 第{self.clickCount}次") + self.counterText.setText(f"点击次数: {self.clickCount}") + print(f"测试按钮被点击,当前点击次数: {self.clickCount}") + + def onResetClick(self): + """重置按钮点击事件""" + self.clickCount = 0 + self.slider['value'] = 50 + self.sliderValueLabel['text'] = "值: 50" + self.textEntry.enterText("") + self.textEntry.set("在此输入...") + self.checkbox['indicatorValue'] = 0 + self.optionMenu.set(0) + self.updateStatus("所有设置已重置") + self.counterText.setText("点击次数: 0") + print("界面已重置") + + def loadChineseFont(self): + """尝试加载中文字体""" + import os + font_paths = [ + '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', + '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', + '/usr/share/fonts/truetype/arphic/ukai.ttc', + '/usr/share/fonts/truetype/arphic/uming.ttc', + ] + + for font_path in font_paths: + if os.path.exists(font_path): + try: + font = self.loader.loadFont(font_path) + if font: + return font + except: + continue + return None + + def onTextSubmit(self, text): + """文本提交事件""" + if text.strip(): + self.updateStatus(f"输入文本: {text}") + print(f"用户输入: {text}") + else: + self.updateStatus("输入为空") + + def onSliderChange(self): + """滑块变化事件""" + value = int(self.slider['value']) + self.sliderValueLabel['text'] = f"值: {value}" + self.updateStatus(f"滑块值: {value}") + print(f"滑块值变更为: {value}") + + def onCheckboxToggle(self, status): + """复选框切换事件""" + state = "开启" if status else "关闭" + self.updateStatus(f"特殊模式: {state}") + print(f"复选框状态: {state}") + + def onOptionSelect(self, selectedOption): + """选项菜单选择事件""" + self.updateStatus(f"选择: {selectedOption}") + print(f"选项变更为: {selectedOption}") + + def updateStatus(self, message): + """更新状态显示""" + self.statusText.setText(message) + + def exitProgram(self): + """退出程序""" + print("用户点击退出按钮") + print("程序即将关闭...") + self.userExit() + +# 程序入口 +if __name__ == "__main__": + print("启动PANDA3D GUI测试程序...") + app = SimpleGUITest() app.run() \ No newline at end of file diff --git a/demo/standalone_gizmo_test.py b/demo/standalone_gizmo_test.py index e6802950..f7259a17 100644 --- a/demo/standalone_gizmo_test.py +++ b/demo/standalone_gizmo_test.py @@ -1,581 +1,581 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -独立的坐标轴点击测试demo -专门用于测试和调试坐标轴的点击检测和拖拽约束功能 -""" - -import sys -import math -from direct.showbase.ShowBase import ShowBase -from panda3d.core import ( - Vec3, Vec4, Point3, Point2, LineSegs, - AmbientLight, DirectionalLight, TextNode, - CollisionTraverser, CollisionHandlerQueue, - CollisionNode, CollisionRay, GeomNode, - CardMaker, Material -) -from direct.task import Task -from direct.gui.DirectGui import DirectFrame, DirectLabel - -class GizmoTest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 关闭默认的鼠标控制 - self.disableMouse() - - print("坐标轴测试程序启动") - - # 设置背景色 - self.setBackgroundColor(0.2, 0.2, 0.3) - - # 设置相机位置 - self.cam.setPos(10, -20, 10) - self.cam.lookAt(0, 0, 0) - - # 添加光照 - self.setupLighting() - - # 创建参考立方体 - self.createReferenceCube() - - # 创建坐标轴 - self.createGizmo() - - # 设置点击检测 - self.setupClickDetection() - - # 创建信息显示 - self.createInfoDisplay() - - # 拖拽状态变量 - self.isDragging = False - self.dragAxis = None - self.dragStartMousePos = None - self.dragStartCubePos = None - - # 高亮状态 - self.highlightAxis = None - - # 设置事件处理 - self.accept("mouse1", self.onMouseClick) - self.accept("mouse1-up", self.onMouseRelease) - self.taskMgr.add(self.mouseTask, "mouseTask") - - # 添加颜色测试任务 - self.taskMgr.add(self.colorTestTask, "colorTest") - self.test_time = 0 - - print("=== Gizmo Click Test Demo Started ===") - print("Instructions:") - print("- ONLY click on red/green/blue gizmo axes to drag") - print("- Each axis constrains movement to that direction only") - print("- Click elsewhere should do nothing") - print("- Real-time display of mouse and projection info") - print("- Color test will cycle through colors every 3 seconds") - - def setupLighting(self): - """设置光照""" - # 环境光 - alight = AmbientLight('alight') - alight.setColor((0.3, 0.3, 0.3, 1)) - alnp = self.render.attachNewNode(alight) - self.render.setLight(alnp) - - # 定向光 - dlight = DirectionalLight('dlight') - dlight.setColor((0.8, 0.8, 0.8, 1)) - dlnp = self.render.attachNewNode(dlight) - dlnp.setHpr(45, -45, 0) - self.render.setLight(dlnp) - - def createReferenceCube(self): - """创建参考立方体""" - # 创建立方体 - cm = CardMaker('cube') - cm.setFrame(-1, 1, -1, 1) - - # 创建6个面 - faces = [ - ('front', (0, 1, 0), (0, 0, 0), (1, 0, 0, 1)), # 红色前面 - - ('back', (0, -1, 0), (0, 0, 180), (0, 1, 0, 1)), # 绿色后面 - ('left', (-1, 0, 0), (0, 0, 90), (0, 0, 1, 1)), # 蓝色左面 - ('right', (1, 0, 0), (0, 0, -90), (1, 1, 0, 1)), # 黄色右面 - ('top', (0, 0, 1), (-90, 0, 0), (1, 0, 1, 1)), # 紫色顶面 - ('bottom', (0, 0, -1), (90, 0, 0), (0, 1, 1, 1)) # 青色底面 - ] - - self.cube = self.render.attachNewNode("cube") - - for name, pos, hpr, color in faces: - face = self.cube.attachNewNode(cm.generate()) - face.setPos(*pos) - face.setHpr(*hpr) - face.setColor(*color) - face.setName(name) - - print(f"✓ 参考立方体创建完成,位置: {self.cube.getPos()}") - - def createGizmo(self): - """创建坐标轴""" - self.gizmo = self.render.attachNewNode("gizmo") - self.gizmo.setPos(0, 0, 3) # 放在立方体上方 - - self.axis_length = 3.0 - self.click_threshold = 25 # 点击检测阈值(像素) - - # 创建X轴(不在LineSegs级别设置颜色) - x_lines = LineSegs("x_axis") - x_lines.setThickness(6.0) - # 不在LineSegs级别设置颜色! - x_lines.moveTo(0, 0, 0) - x_lines.drawTo(self.axis_length, 0, 0) - # 箭头 - x_lines.moveTo(self.axis_length - 0.3, -0.1, 0) - x_lines.drawTo(self.axis_length, 0, 0) - x_lines.drawTo(self.axis_length - 0.3, 0.1, 0) - x_geom = x_lines.create() - self.gizmoXAxis = self.gizmo.attachNewNode(x_geom) - self.gizmoXAxis.setLightOff() - # 使用RenderState强制设置颜色 - from panda3d.core import RenderState, ColorAttrib - red_state = RenderState.make(ColorAttrib.makeFlat((1, 0, 0, 1))) - self.gizmoXAxis.setState(red_state) - self.gizmoXAxis.setColor(1, 0, 0, 1) - - # 创建Y轴(不在LineSegs级别设置颜色) - y_lines = LineSegs("y_axis") - y_lines.setThickness(6.0) - # 不在LineSegs级别设置颜色! - y_lines.moveTo(0, 0, 0) - y_lines.drawTo(0, self.axis_length, 0) - # 箭头 - y_lines.moveTo(-0.1, self.axis_length - 0.3, 0) - y_lines.drawTo(0, self.axis_length, 0) - y_lines.drawTo(0.1, self.axis_length - 0.3, 0) - y_geom = y_lines.create() - self.gizmoYAxis = self.gizmo.attachNewNode(y_geom) - self.gizmoYAxis.setLightOff() - # 使用RenderState强制设置颜色 - green_state = RenderState.make(ColorAttrib.makeFlat((0, 1, 0, 1))) - self.gizmoYAxis.setState(green_state) - self.gizmoYAxis.setColor(0, 1, 0, 1) - - # 创建Z轴(不在LineSegs级别设置颜色) - z_lines = LineSegs("z_axis") - z_lines.setThickness(6.0) - # 不在LineSegs级别设置颜色! - z_lines.moveTo(0, 0, 0) - z_lines.drawTo(0, 0, self.axis_length) - # 箭头 - z_lines.moveTo(-0.1, 0, self.axis_length - 0.3) - z_lines.drawTo(0, 0, self.axis_length) - z_lines.drawTo(0.1, 0, self.axis_length - 0.3) - z_geom = z_lines.create() - self.gizmoZAxis = self.gizmo.attachNewNode(z_geom) - self.gizmoZAxis.setLightOff() - # 使用RenderState强制设置颜色 - blue_state = RenderState.make(ColorAttrib.makeFlat((0, 0, 1, 1))) - self.gizmoZAxis.setState(blue_state) - self.gizmoZAxis.setColor(0, 0, 1, 1) - - # 轴颜色字典 - self.axis_colors = { - "x": (1, 0, 0, 1), # 红色 - "y": (0, 1, 0, 1), # 绿色 - "z": (0, 0, 1, 1) # 蓝色 - } - - print(f"✓ 坐标轴创建完成,长度={self.axis_length}") - print(f"X轴节点颜色: {self.gizmoXAxis.getColor()}") - print(f"Y轴节点颜色: {self.gizmoYAxis.getColor()}") - print(f"Z轴节点颜色: {self.gizmoZAxis.getColor()}") - - def setupClickDetection(self): - """设置点击检测""" - self.picker = CollisionTraverser() - self.pq = CollisionHandlerQueue() - - self.pickerNode = CollisionNode('mouseRay') - self.pickerNP = self.camera.attachNewNode(self.pickerNode) - self.pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask()) - self.picker.addCollider(self.pickerNP, self.pq) - - def createInfoDisplay(self): - """创建信息显示面板""" - self.infoPanel = DirectFrame( - frameColor=(0, 0, 0, 0.7), - frameSize=(-0.4, 0.4, -0.3, 0.3), - pos=(-0.6, 0, 0.8) - ) - - self.infoText = DirectLabel( - parent=self.infoPanel, - text="Mouse Info\nWaiting...", - text_scale=0.05, - text_fg=(1, 1, 1, 1), - frameColor=(0, 0, 0, 0), - pos=(0, 0, 0) - ) - - def worldToScreen(self, world_pos): - """将世界坐标转换为屏幕坐标""" - try: - # 转换为相机坐标系 - cam_pos = self.cam.getRelativePoint(self.render, world_pos) - - # 检查是否在相机前方 - if cam_pos.getY() <= 0: - return None - - # 使用相机lens进行投影 - screen_pos = Point2() - if self.cam.node().getLens().project(cam_pos, screen_pos): - # 转换为像素坐标 - win_x = (screen_pos.getX() + 1.0) * 0.5 * self.win.getXSize() - win_y = (1.0 - screen_pos.getY()) * 0.5 * self.win.getYSize() - return (win_x, win_y) - return None - except: - return None - - def distanceToLine(self, point, line_start, line_end): - """计算点到线段的距离""" - try: - px, py = point - x1, y1 = line_start - x2, y2 = line_end - - # 计算线段长度 - line_length_sq = (x2 - x1) ** 2 + (y2 - y1) ** 2 - if line_length_sq < 0.001: - return math.sqrt((px - x1) ** 2 + (py - y1) ** 2) - - # 计算投影参数 - t = max(0, min(1, ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / line_length_sq)) - - # 计算投影点 - projection_x = x1 + t * (x2 - x1) - projection_y = y1 + t * (y2 - y1) - - # 返回距离 - distance = math.sqrt((px - projection_x) ** 2 + (py - projection_y) ** 2) - return distance - except: - return float('inf') - - def checkGizmoClick(self, mouse_x, mouse_y): - """检查是否点击了坐标轴""" - if not self.gizmo: - return None - - # 获取坐标轴中心的世界坐标 - gizmo_world_pos = self.gizmo.getPos(self.render) - - # 计算各轴端点的世界坐标 - x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0) - y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0) - z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length) - - # 投影到屏幕坐标 - center_screen = self.worldToScreen(gizmo_world_pos) - x_screen = self.worldToScreen(x_end) - y_screen = self.worldToScreen(y_end) - z_screen = self.worldToScreen(z_end) - - if not center_screen: - return None - - # 检测各个轴 - axes_data = [ - ("x", x_screen, "X轴"), - ("y", y_screen, "Y轴"), - ("z", z_screen, "Z轴") - ] - - closest_axis = None - closest_distance = float('inf') - - for axis_name, axis_screen, axis_label in axes_data: - if axis_screen: - distance = self.distanceToLine( - (mouse_x, mouse_y), center_screen, axis_screen - ) - - if distance < self.click_threshold and distance < closest_distance: - closest_axis = axis_name - closest_distance = distance - - return closest_axis - - def setAxisColor(self, axis, color): - """设置坐标轴颜色 - 使用RenderState强制覆盖""" - from panda3d.core import RenderState, ColorAttrib - - # 创建强制颜色状态 - color_state = RenderState.make(ColorAttrib.makeFlat(color)) - - if axis == "x" and self.gizmoXAxis: - self.gizmoXAxis.setState(color_state) - self.gizmoXAxis.setColor(*color) - self.gizmoXAxis.setColorScale(*color) - print(f"✓ 强制设置X轴颜色: {color}") - elif axis == "y" and self.gizmoYAxis: - self.gizmoYAxis.setState(color_state) - self.gizmoYAxis.setColor(*color) - self.gizmoYAxis.setColorScale(*color) - print(f"✓ 强制设置Y轴颜色: {color}") - elif axis == "z" and self.gizmoZAxis: - self.gizmoZAxis.setState(color_state) - self.gizmoZAxis.setColor(*color) - self.gizmoZAxis.setColorScale(*color) - print(f"✓ 强制设置Z轴颜色: {color}") - - def mouseTask(self, task): - """鼠标任务 - 处理高亮和拖拽""" - if not self.mouseWatcherNode.hasMouse(): - return task.cont - - # 获取鼠标屏幕坐标 - mouse_x = (self.mouseWatcherNode.getMouseX() + 1.0) * 0.5 * self.win.getXSize() - mouse_y = (1.0 - self.mouseWatcherNode.getMouseY()) * 0.5 * self.win.getYSize() - - # 检查是否在颜色测试周期内 - cycle_time = int(self.test_time) % 15 - is_color_testing = (cycle_time >= 3 and cycle_time < 12) # 在黄、白、橙色测试期间 - - # 更新信息显示 - info_text = f"Mouse Info:\n" - info_text += f"Normalized: ({self.mouseWatcherNode.getMouseX():.3f}, {self.mouseWatcherNode.getMouseY():.3f})\n" - info_text += f"Pixels: ({mouse_x:.1f}, {mouse_y:.1f})\n" - info_text += f"Window Size: {self.win.getXSize()} x {self.win.getYSize()}\n" - - if is_color_testing: - info_text += f"Color Test Active (cycle: {cycle_time})\n" - - # 只在不拖拽且不在颜色测试时检测高亮 - if not self.isDragging and not is_color_testing: - # 简化的高亮检测 - hovered_axis = None - - if self.gizmo: - # 获取坐标轴中心的世界坐标 - gizmo_world_pos = self.gizmo.getPos(self.render) - - # 计算各轴端点的世界坐标 - x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0) - y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0) - z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length) - - # 投影到屏幕坐标 - center_screen = self.worldToScreen(gizmo_world_pos) - x_screen = self.worldToScreen(x_end) - y_screen = self.worldToScreen(y_end) - z_screen = self.worldToScreen(z_end) - - if center_screen: - # 检测距离最近的轴 - min_distance = float('inf') - - for axis_name, axis_screen in [("x", x_screen), ("y", y_screen), ("z", z_screen)]: - if axis_screen: - distance = self.distanceToLine((mouse_x, mouse_y), center_screen, axis_screen) - if distance < self.click_threshold and distance < min_distance: - min_distance = distance - hovered_axis = axis_name - - # 处理高亮状态变化 - if hovered_axis != self.highlightAxis: - # 恢复之前高亮的轴 - if self.highlightAxis: - self.setAxisColor(self.highlightAxis, self.axis_colors[self.highlightAxis]) - print(f"恢复 {self.highlightAxis} 轴原色") - - # 高亮新的轴 - if hovered_axis: - self.setAxisColor(hovered_axis, (1, 1, 0, 1)) # 纯黄色高亮 - print(f"高亮 {hovered_axis} 轴为黄色") - - self.highlightAxis = hovered_axis - - # 更新信息显示 - if hovered_axis: - info_text += f"Hover: {hovered_axis.upper()} axis (YELLOW)" - else: - info_text += "Hover: None" - elif is_color_testing: - info_text += "Hover: Disabled (Color Test)" - else: - info_text += f"Dragging: {self.dragAxis.upper()} axis" - - # 处理拖拽 - if self.isDragging: - self.handleDrag(mouse_x, mouse_y) - - self.infoText.setText(info_text) - return task.cont - - def onMouseClick(self): - """鼠标点击事件 - 只有点击坐标轴才开始拖拽""" - if not self.mouseWatcherNode.hasMouse(): - return - - # 检查是否在颜色测试期间 - cycle_time = int(self.test_time) % 15 - is_color_testing = (cycle_time >= 3 and cycle_time < 12) - - if is_color_testing: - print("点击被禁用 - 颜色测试进行中") - return - - # 获取鼠标坐标 - mouse_x = (self.mouseWatcherNode.getMouseX() + 1.0) * 0.5 * self.win.getXSize() - mouse_y = (1.0 - self.mouseWatcherNode.getMouseY()) * 0.5 * self.win.getYSize() - - print(f"\n=== Mouse Click ===") - print(f"Pixel Coords: ({mouse_x:.1f}, {mouse_y:.1f})") - - # 检测坐标轴点击 - clicked_axis = self.checkGizmoClick(mouse_x, mouse_y) - - if clicked_axis: - print(f"✓ Clicked {clicked_axis.upper()} Axis - Starting drag") - # 只有真正点击到轴才开始拖拽 - self.isDragging = True - self.dragAxis = clicked_axis - self.dragStartMousePos = (mouse_x, mouse_y) - self.dragStartCubePos = self.cube.getPos() - - # 显示拖拽颜色 - 使用更明显的橙色 - self.setAxisColor(clicked_axis, (1, 0.5, 0, 1)) # 更亮的橙色表示拖拽 - - print(f" 开始位置: {self.dragStartCubePos}") - else: - print("✗ No axis clicked - No action") - # 确保没有点击到轴时不开始拖拽 - self.isDragging = False - self.dragAxis = None - - def onMouseRelease(self): - """鼠标释放事件""" - if self.isDragging and self.dragAxis: - print(f"Stop dragging {self.dragAxis.upper()} axis") - - # 恢复轴颜色 - self.setAxisColor(self.dragAxis, self.axis_colors[self.dragAxis]) - - # 清理所有拖拽状态 - self.isDragging = False - self.dragAxis = None - self.dragStartMousePos = None - self.dragStartCubePos = None - - def handleDrag(self, mouse_x, mouse_y): - """处理拖拽 - 真正的轴约束拖拽""" - if not self.dragAxis or not self.dragStartMousePos or not self.dragStartCubePos: - return - - # 计算从拖拽开始到现在的总鼠标移动距离 - total_dx = mouse_x - self.dragStartMousePos[0] - total_dy = mouse_y - self.dragStartMousePos[1] - - # 根据拖拽的轴,确定世界空间中的轴方向向量 - if self.dragAxis == "x": - world_axis_dir = Vec3(1, 0, 0) # X轴方向 - elif self.dragAxis == "y": - world_axis_dir = Vec3(0, 1, 0) # Y轴方向 - elif self.dragAxis == "z": - world_axis_dir = Vec3(0, 0, 1) # Z轴方向 - else: - return - - # 将世界轴方向投影到屏幕空间 - axis_start_world = self.dragStartCubePos - axis_end_world = self.dragStartCubePos + world_axis_dir - - axis_start_screen = self.worldToScreen(axis_start_world) - axis_end_screen = self.worldToScreen(axis_end_world) - - if not axis_start_screen or not axis_end_screen: - return - - # 计算轴在屏幕空间的方向向量 - screen_axis_dir = ( - axis_end_screen[0] - axis_start_screen[0], - axis_end_screen[1] - axis_start_screen[1] - ) - - # 归一化屏幕轴方向 - screen_axis_length = math.sqrt(screen_axis_dir[0]**2 + screen_axis_dir[1]**2) - if screen_axis_length > 0.001: - screen_axis_dir = ( - screen_axis_dir[0] / screen_axis_length, - screen_axis_dir[1] / screen_axis_length - ) - else: - return - - # 将总鼠标移动向量投影到屏幕轴方向上 - projected_distance = ( - total_dx * screen_axis_dir[0] + - total_dy * screen_axis_dir[1] - ) - - # 将投影距离转换为世界空间移动量 - move_scale = 0.02 # 移动灵敏度 - world_movement = world_axis_dir * projected_distance * move_scale - - # 从拖拽开始位置计算新位置(确保只沿选定轴移动) - new_cube_pos = self.dragStartCubePos + world_movement - self.cube.setPos(new_cube_pos) - - # 坐标轴跟随立方体移动 - self.gizmo.setPos(new_cube_pos + Vec3(0, 0, 3)) - - def colorTestTask(self, task): - """颜色测试任务 - 每3秒循环测试坐标轴颜色""" - from panda3d.core import RenderState, ColorAttrib - - self.test_time += globalClock.getDt() - - # 每3秒切换一次测试 - cycle_time = int(self.test_time) % 15 # 15秒一个完整周期 - - if cycle_time < 3: - # 正常颜色 - if hasattr(self, 'gizmoXAxis'): - self.setAxisColor("x", (1, 0, 0, 1)) # 红色 - self.setAxisColor("y", (0, 1, 0, 1)) # 绿色 - self.setAxisColor("z", (0, 0, 1, 1)) # 蓝色 - elif cycle_time < 6: - # 全部黄色 - if hasattr(self, 'gizmoXAxis'): - self.setAxisColor("x", (1, 1, 0, 1)) - self.setAxisColor("y", (1, 1, 0, 1)) - self.setAxisColor("z", (1, 1, 0, 1)) - elif cycle_time < 9: - # 全部白色 - if hasattr(self, 'gizmoXAxis'): - self.setAxisColor("x", (1, 1, 1, 1)) - self.setAxisColor("y", (1, 1, 1, 1)) - self.setAxisColor("z", (1, 1, 1, 1)) - elif cycle_time < 12: - # 全部橙色 - if hasattr(self, 'gizmoXAxis'): - self.setAxisColor("x", (1, 0.5, 0, 1)) - self.setAxisColor("y", (1, 0.5, 0, 1)) - self.setAxisColor("z", (1, 0.5, 0, 1)) - else: - # 恢复正常颜色 - if hasattr(self, 'gizmoXAxis'): - self.setAxisColor("x", (1, 0, 0, 1)) # 红色 - self.setAxisColor("y", (0, 1, 0, 1)) # 绿色 - self.setAxisColor("z", (0, 0, 1, 1)) # 蓝色 - - return task.cont - -if __name__ == "__main__": - app = GizmoTest() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +独立的坐标轴点击测试demo +专门用于测试和调试坐标轴的点击检测和拖拽约束功能 +""" + +import sys +import math +from direct.showbase.ShowBase import ShowBase +from panda3d.core import ( + Vec3, Vec4, Point3, Point2, LineSegs, + AmbientLight, DirectionalLight, TextNode, + CollisionTraverser, CollisionHandlerQueue, + CollisionNode, CollisionRay, GeomNode, + CardMaker, Material +) +from direct.task import Task +from direct.gui.DirectGui import DirectFrame, DirectLabel + +class GizmoTest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 关闭默认的鼠标控制 + self.disableMouse() + + print("坐标轴测试程序启动") + + # 设置背景色 + self.setBackgroundColor(0.2, 0.2, 0.3) + + # 设置相机位置 + self.cam.setPos(10, -20, 10) + self.cam.lookAt(0, 0, 0) + + # 添加光照 + self.setupLighting() + + # 创建参考立方体 + self.createReferenceCube() + + # 创建坐标轴 + self.createGizmo() + + # 设置点击检测 + self.setupClickDetection() + + # 创建信息显示 + self.createInfoDisplay() + + # 拖拽状态变量 + self.isDragging = False + self.dragAxis = None + self.dragStartMousePos = None + self.dragStartCubePos = None + + # 高亮状态 + self.highlightAxis = None + + # 设置事件处理 + self.accept("mouse1", self.onMouseClick) + self.accept("mouse1-up", self.onMouseRelease) + self.taskMgr.add(self.mouseTask, "mouseTask") + + # 添加颜色测试任务 + self.taskMgr.add(self.colorTestTask, "colorTest") + self.test_time = 0 + + print("=== Gizmo Click Test Demo Started ===") + print("Instructions:") + print("- ONLY click on red/green/blue gizmo axes to drag") + print("- Each axis constrains movement to that direction only") + print("- Click elsewhere should do nothing") + print("- Real-time display of mouse and projection info") + print("- Color test will cycle through colors every 3 seconds") + + def setupLighting(self): + """设置光照""" + # 环境光 + alight = AmbientLight('alight') + alight.setColor((0.3, 0.3, 0.3, 1)) + alnp = self.render.attachNewNode(alight) + self.render.setLight(alnp) + + # 定向光 + dlight = DirectionalLight('dlight') + dlight.setColor((0.8, 0.8, 0.8, 1)) + dlnp = self.render.attachNewNode(dlight) + dlnp.setHpr(45, -45, 0) + self.render.setLight(dlnp) + + def createReferenceCube(self): + """创建参考立方体""" + # 创建立方体 + cm = CardMaker('cube') + cm.setFrame(-1, 1, -1, 1) + + # 创建6个面 + faces = [ + ('front', (0, 1, 0), (0, 0, 0), (1, 0, 0, 1)), # 红色前面 + + ('back', (0, -1, 0), (0, 0, 180), (0, 1, 0, 1)), # 绿色后面 + ('left', (-1, 0, 0), (0, 0, 90), (0, 0, 1, 1)), # 蓝色左面 + ('right', (1, 0, 0), (0, 0, -90), (1, 1, 0, 1)), # 黄色右面 + ('top', (0, 0, 1), (-90, 0, 0), (1, 0, 1, 1)), # 紫色顶面 + ('bottom', (0, 0, -1), (90, 0, 0), (0, 1, 1, 1)) # 青色底面 + ] + + self.cube = self.render.attachNewNode("cube") + + for name, pos, hpr, color in faces: + face = self.cube.attachNewNode(cm.generate()) + face.setPos(*pos) + face.setHpr(*hpr) + face.setColor(*color) + face.setName(name) + + print(f"✓ 参考立方体创建完成,位置: {self.cube.getPos()}") + + def createGizmo(self): + """创建坐标轴""" + self.gizmo = self.render.attachNewNode("gizmo") + self.gizmo.setPos(0, 0, 3) # 放在立方体上方 + + self.axis_length = 3.0 + self.click_threshold = 25 # 点击检测阈值(像素) + + # 创建X轴(不在LineSegs级别设置颜色) + x_lines = LineSegs("x_axis") + x_lines.setThickness(6.0) + # 不在LineSegs级别设置颜色! + x_lines.moveTo(0, 0, 0) + x_lines.drawTo(self.axis_length, 0, 0) + # 箭头 + x_lines.moveTo(self.axis_length - 0.3, -0.1, 0) + x_lines.drawTo(self.axis_length, 0, 0) + x_lines.drawTo(self.axis_length - 0.3, 0.1, 0) + x_geom = x_lines.create() + self.gizmoXAxis = self.gizmo.attachNewNode(x_geom) + self.gizmoXAxis.setLightOff() + # 使用RenderState强制设置颜色 + from panda3d.core import RenderState, ColorAttrib + red_state = RenderState.make(ColorAttrib.makeFlat((1, 0, 0, 1))) + self.gizmoXAxis.setState(red_state) + self.gizmoXAxis.setColor(1, 0, 0, 1) + + # 创建Y轴(不在LineSegs级别设置颜色) + y_lines = LineSegs("y_axis") + y_lines.setThickness(6.0) + # 不在LineSegs级别设置颜色! + y_lines.moveTo(0, 0, 0) + y_lines.drawTo(0, self.axis_length, 0) + # 箭头 + y_lines.moveTo(-0.1, self.axis_length - 0.3, 0) + y_lines.drawTo(0, self.axis_length, 0) + y_lines.drawTo(0.1, self.axis_length - 0.3, 0) + y_geom = y_lines.create() + self.gizmoYAxis = self.gizmo.attachNewNode(y_geom) + self.gizmoYAxis.setLightOff() + # 使用RenderState强制设置颜色 + green_state = RenderState.make(ColorAttrib.makeFlat((0, 1, 0, 1))) + self.gizmoYAxis.setState(green_state) + self.gizmoYAxis.setColor(0, 1, 0, 1) + + # 创建Z轴(不在LineSegs级别设置颜色) + z_lines = LineSegs("z_axis") + z_lines.setThickness(6.0) + # 不在LineSegs级别设置颜色! + z_lines.moveTo(0, 0, 0) + z_lines.drawTo(0, 0, self.axis_length) + # 箭头 + z_lines.moveTo(-0.1, 0, self.axis_length - 0.3) + z_lines.drawTo(0, 0, self.axis_length) + z_lines.drawTo(0.1, 0, self.axis_length - 0.3) + z_geom = z_lines.create() + self.gizmoZAxis = self.gizmo.attachNewNode(z_geom) + self.gizmoZAxis.setLightOff() + # 使用RenderState强制设置颜色 + blue_state = RenderState.make(ColorAttrib.makeFlat((0, 0, 1, 1))) + self.gizmoZAxis.setState(blue_state) + self.gizmoZAxis.setColor(0, 0, 1, 1) + + # 轴颜色字典 + self.axis_colors = { + "x": (1, 0, 0, 1), # 红色 + "y": (0, 1, 0, 1), # 绿色 + "z": (0, 0, 1, 1) # 蓝色 + } + + print(f"✓ 坐标轴创建完成,长度={self.axis_length}") + print(f"X轴节点颜色: {self.gizmoXAxis.getColor()}") + print(f"Y轴节点颜色: {self.gizmoYAxis.getColor()}") + print(f"Z轴节点颜色: {self.gizmoZAxis.getColor()}") + + def setupClickDetection(self): + """设置点击检测""" + self.picker = CollisionTraverser() + self.pq = CollisionHandlerQueue() + + self.pickerNode = CollisionNode('mouseRay') + self.pickerNP = self.camera.attachNewNode(self.pickerNode) + self.pickerNode.setFromCollideMask(GeomNode.getDefaultCollideMask()) + self.picker.addCollider(self.pickerNP, self.pq) + + def createInfoDisplay(self): + """创建信息显示面板""" + self.infoPanel = DirectFrame( + frameColor=(0, 0, 0, 0.7), + frameSize=(-0.4, 0.4, -0.3, 0.3), + pos=(-0.6, 0, 0.8) + ) + + self.infoText = DirectLabel( + parent=self.infoPanel, + text="Mouse Info\nWaiting...", + text_scale=0.05, + text_fg=(1, 1, 1, 1), + frameColor=(0, 0, 0, 0), + pos=(0, 0, 0) + ) + + def worldToScreen(self, world_pos): + """将世界坐标转换为屏幕坐标""" + try: + # 转换为相机坐标系 + cam_pos = self.cam.getRelativePoint(self.render, world_pos) + + # 检查是否在相机前方 + if cam_pos.getY() <= 0: + return None + + # 使用相机lens进行投影 + screen_pos = Point2() + if self.cam.node().getLens().project(cam_pos, screen_pos): + # 转换为像素坐标 + win_x = (screen_pos.getX() + 1.0) * 0.5 * self.win.getXSize() + win_y = (1.0 - screen_pos.getY()) * 0.5 * self.win.getYSize() + return (win_x, win_y) + return None + except: + return None + + def distanceToLine(self, point, line_start, line_end): + """计算点到线段的距离""" + try: + px, py = point + x1, y1 = line_start + x2, y2 = line_end + + # 计算线段长度 + line_length_sq = (x2 - x1) ** 2 + (y2 - y1) ** 2 + if line_length_sq < 0.001: + return math.sqrt((px - x1) ** 2 + (py - y1) ** 2) + + # 计算投影参数 + t = max(0, min(1, ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / line_length_sq)) + + # 计算投影点 + projection_x = x1 + t * (x2 - x1) + projection_y = y1 + t * (y2 - y1) + + # 返回距离 + distance = math.sqrt((px - projection_x) ** 2 + (py - projection_y) ** 2) + return distance + except: + return float('inf') + + def checkGizmoClick(self, mouse_x, mouse_y): + """检查是否点击了坐标轴""" + if not self.gizmo: + return None + + # 获取坐标轴中心的世界坐标 + gizmo_world_pos = self.gizmo.getPos(self.render) + + # 计算各轴端点的世界坐标 + x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0) + y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0) + z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length) + + # 投影到屏幕坐标 + center_screen = self.worldToScreen(gizmo_world_pos) + x_screen = self.worldToScreen(x_end) + y_screen = self.worldToScreen(y_end) + z_screen = self.worldToScreen(z_end) + + if not center_screen: + return None + + # 检测各个轴 + axes_data = [ + ("x", x_screen, "X轴"), + ("y", y_screen, "Y轴"), + ("z", z_screen, "Z轴") + ] + + closest_axis = None + closest_distance = float('inf') + + for axis_name, axis_screen, axis_label in axes_data: + if axis_screen: + distance = self.distanceToLine( + (mouse_x, mouse_y), center_screen, axis_screen + ) + + if distance < self.click_threshold and distance < closest_distance: + closest_axis = axis_name + closest_distance = distance + + return closest_axis + + def setAxisColor(self, axis, color): + """设置坐标轴颜色 - 使用RenderState强制覆盖""" + from panda3d.core import RenderState, ColorAttrib + + # 创建强制颜色状态 + color_state = RenderState.make(ColorAttrib.makeFlat(color)) + + if axis == "x" and self.gizmoXAxis: + self.gizmoXAxis.setState(color_state) + self.gizmoXAxis.setColor(*color) + self.gizmoXAxis.setColorScale(*color) + print(f"✓ 强制设置X轴颜色: {color}") + elif axis == "y" and self.gizmoYAxis: + self.gizmoYAxis.setState(color_state) + self.gizmoYAxis.setColor(*color) + self.gizmoYAxis.setColorScale(*color) + print(f"✓ 强制设置Y轴颜色: {color}") + elif axis == "z" and self.gizmoZAxis: + self.gizmoZAxis.setState(color_state) + self.gizmoZAxis.setColor(*color) + self.gizmoZAxis.setColorScale(*color) + print(f"✓ 强制设置Z轴颜色: {color}") + + def mouseTask(self, task): + """鼠标任务 - 处理高亮和拖拽""" + if not self.mouseWatcherNode.hasMouse(): + return task.cont + + # 获取鼠标屏幕坐标 + mouse_x = (self.mouseWatcherNode.getMouseX() + 1.0) * 0.5 * self.win.getXSize() + mouse_y = (1.0 - self.mouseWatcherNode.getMouseY()) * 0.5 * self.win.getYSize() + + # 检查是否在颜色测试周期内 + cycle_time = int(self.test_time) % 15 + is_color_testing = (cycle_time >= 3 and cycle_time < 12) # 在黄、白、橙色测试期间 + + # 更新信息显示 + info_text = f"Mouse Info:\n" + info_text += f"Normalized: ({self.mouseWatcherNode.getMouseX():.3f}, {self.mouseWatcherNode.getMouseY():.3f})\n" + info_text += f"Pixels: ({mouse_x:.1f}, {mouse_y:.1f})\n" + info_text += f"Window Size: {self.win.getXSize()} x {self.win.getYSize()}\n" + + if is_color_testing: + info_text += f"Color Test Active (cycle: {cycle_time})\n" + + # 只在不拖拽且不在颜色测试时检测高亮 + if not self.isDragging and not is_color_testing: + # 简化的高亮检测 + hovered_axis = None + + if self.gizmo: + # 获取坐标轴中心的世界坐标 + gizmo_world_pos = self.gizmo.getPos(self.render) + + # 计算各轴端点的世界坐标 + x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0) + y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0) + z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length) + + # 投影到屏幕坐标 + center_screen = self.worldToScreen(gizmo_world_pos) + x_screen = self.worldToScreen(x_end) + y_screen = self.worldToScreen(y_end) + z_screen = self.worldToScreen(z_end) + + if center_screen: + # 检测距离最近的轴 + min_distance = float('inf') + + for axis_name, axis_screen in [("x", x_screen), ("y", y_screen), ("z", z_screen)]: + if axis_screen: + distance = self.distanceToLine((mouse_x, mouse_y), center_screen, axis_screen) + if distance < self.click_threshold and distance < min_distance: + min_distance = distance + hovered_axis = axis_name + + # 处理高亮状态变化 + if hovered_axis != self.highlightAxis: + # 恢复之前高亮的轴 + if self.highlightAxis: + self.setAxisColor(self.highlightAxis, self.axis_colors[self.highlightAxis]) + print(f"恢复 {self.highlightAxis} 轴原色") + + # 高亮新的轴 + if hovered_axis: + self.setAxisColor(hovered_axis, (1, 1, 0, 1)) # 纯黄色高亮 + print(f"高亮 {hovered_axis} 轴为黄色") + + self.highlightAxis = hovered_axis + + # 更新信息显示 + if hovered_axis: + info_text += f"Hover: {hovered_axis.upper()} axis (YELLOW)" + else: + info_text += "Hover: None" + elif is_color_testing: + info_text += "Hover: Disabled (Color Test)" + else: + info_text += f"Dragging: {self.dragAxis.upper()} axis" + + # 处理拖拽 + if self.isDragging: + self.handleDrag(mouse_x, mouse_y) + + self.infoText.setText(info_text) + return task.cont + + def onMouseClick(self): + """鼠标点击事件 - 只有点击坐标轴才开始拖拽""" + if not self.mouseWatcherNode.hasMouse(): + return + + # 检查是否在颜色测试期间 + cycle_time = int(self.test_time) % 15 + is_color_testing = (cycle_time >= 3 and cycle_time < 12) + + if is_color_testing: + print("点击被禁用 - 颜色测试进行中") + return + + # 获取鼠标坐标 + mouse_x = (self.mouseWatcherNode.getMouseX() + 1.0) * 0.5 * self.win.getXSize() + mouse_y = (1.0 - self.mouseWatcherNode.getMouseY()) * 0.5 * self.win.getYSize() + + print(f"\n=== Mouse Click ===") + print(f"Pixel Coords: ({mouse_x:.1f}, {mouse_y:.1f})") + + # 检测坐标轴点击 + clicked_axis = self.checkGizmoClick(mouse_x, mouse_y) + + if clicked_axis: + print(f"✓ Clicked {clicked_axis.upper()} Axis - Starting drag") + # 只有真正点击到轴才开始拖拽 + self.isDragging = True + self.dragAxis = clicked_axis + self.dragStartMousePos = (mouse_x, mouse_y) + self.dragStartCubePos = self.cube.getPos() + + # 显示拖拽颜色 - 使用更明显的橙色 + self.setAxisColor(clicked_axis, (1, 0.5, 0, 1)) # 更亮的橙色表示拖拽 + + print(f" 开始位置: {self.dragStartCubePos}") + else: + print("✗ No axis clicked - No action") + # 确保没有点击到轴时不开始拖拽 + self.isDragging = False + self.dragAxis = None + + def onMouseRelease(self): + """鼠标释放事件""" + if self.isDragging and self.dragAxis: + print(f"Stop dragging {self.dragAxis.upper()} axis") + + # 恢复轴颜色 + self.setAxisColor(self.dragAxis, self.axis_colors[self.dragAxis]) + + # 清理所有拖拽状态 + self.isDragging = False + self.dragAxis = None + self.dragStartMousePos = None + self.dragStartCubePos = None + + def handleDrag(self, mouse_x, mouse_y): + """处理拖拽 - 真正的轴约束拖拽""" + if not self.dragAxis or not self.dragStartMousePos or not self.dragStartCubePos: + return + + # 计算从拖拽开始到现在的总鼠标移动距离 + total_dx = mouse_x - self.dragStartMousePos[0] + total_dy = mouse_y - self.dragStartMousePos[1] + + # 根据拖拽的轴,确定世界空间中的轴方向向量 + if self.dragAxis == "x": + world_axis_dir = Vec3(1, 0, 0) # X轴方向 + elif self.dragAxis == "y": + world_axis_dir = Vec3(0, 1, 0) # Y轴方向 + elif self.dragAxis == "z": + world_axis_dir = Vec3(0, 0, 1) # Z轴方向 + else: + return + + # 将世界轴方向投影到屏幕空间 + axis_start_world = self.dragStartCubePos + axis_end_world = self.dragStartCubePos + world_axis_dir + + axis_start_screen = self.worldToScreen(axis_start_world) + axis_end_screen = self.worldToScreen(axis_end_world) + + if not axis_start_screen or not axis_end_screen: + return + + # 计算轴在屏幕空间的方向向量 + screen_axis_dir = ( + axis_end_screen[0] - axis_start_screen[0], + axis_end_screen[1] - axis_start_screen[1] + ) + + # 归一化屏幕轴方向 + screen_axis_length = math.sqrt(screen_axis_dir[0]**2 + screen_axis_dir[1]**2) + if screen_axis_length > 0.001: + screen_axis_dir = ( + screen_axis_dir[0] / screen_axis_length, + screen_axis_dir[1] / screen_axis_length + ) + else: + return + + # 将总鼠标移动向量投影到屏幕轴方向上 + projected_distance = ( + total_dx * screen_axis_dir[0] + + total_dy * screen_axis_dir[1] + ) + + # 将投影距离转换为世界空间移动量 + move_scale = 0.02 # 移动灵敏度 + world_movement = world_axis_dir * projected_distance * move_scale + + # 从拖拽开始位置计算新位置(确保只沿选定轴移动) + new_cube_pos = self.dragStartCubePos + world_movement + self.cube.setPos(new_cube_pos) + + # 坐标轴跟随立方体移动 + self.gizmo.setPos(new_cube_pos + Vec3(0, 0, 3)) + + def colorTestTask(self, task): + """颜色测试任务 - 每3秒循环测试坐标轴颜色""" + from panda3d.core import RenderState, ColorAttrib + + self.test_time += globalClock.getDt() + + # 每3秒切换一次测试 + cycle_time = int(self.test_time) % 15 # 15秒一个完整周期 + + if cycle_time < 3: + # 正常颜色 + if hasattr(self, 'gizmoXAxis'): + self.setAxisColor("x", (1, 0, 0, 1)) # 红色 + self.setAxisColor("y", (0, 1, 0, 1)) # 绿色 + self.setAxisColor("z", (0, 0, 1, 1)) # 蓝色 + elif cycle_time < 6: + # 全部黄色 + if hasattr(self, 'gizmoXAxis'): + self.setAxisColor("x", (1, 1, 0, 1)) + self.setAxisColor("y", (1, 1, 0, 1)) + self.setAxisColor("z", (1, 1, 0, 1)) + elif cycle_time < 9: + # 全部白色 + if hasattr(self, 'gizmoXAxis'): + self.setAxisColor("x", (1, 1, 1, 1)) + self.setAxisColor("y", (1, 1, 1, 1)) + self.setAxisColor("z", (1, 1, 1, 1)) + elif cycle_time < 12: + # 全部橙色 + if hasattr(self, 'gizmoXAxis'): + self.setAxisColor("x", (1, 0.5, 0, 1)) + self.setAxisColor("y", (1, 0.5, 0, 1)) + self.setAxisColor("z", (1, 0.5, 0, 1)) + else: + # 恢复正常颜色 + if hasattr(self, 'gizmoXAxis'): + self.setAxisColor("x", (1, 0, 0, 1)) # 红色 + self.setAxisColor("y", (0, 1, 0, 1)) # 绿色 + self.setAxisColor("z", (0, 0, 1, 1)) # 蓝色 + + return task.cont + +if __name__ == "__main__": + app = GizmoTest() app.run() \ No newline at end of file diff --git a/demo/test_2d_gui_fix.py b/demo/test_2d_gui_fix.py index e36498dc..c5f17012 100644 --- a/demo/test_2d_gui_fix.py +++ b/demo/test_2d_gui_fix.py @@ -1,165 +1,165 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -测试2D GUI组件显示修复 -验证按钮、标签、输入框是否能正常显示 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -from direct.showbase.ShowBase import ShowBase -from direct.gui.DirectGui import * -from panda3d.core import * -import os - -class GUITestApp(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 设置背景色 - self.setBackgroundColor(0.2, 0.2, 0.3) - - # 尝试加载中文字体 - self.chinese_font = self.loadChineseFont() - - # 创建测试GUI元素 - self.createTestGUI() - - print("GUI测试应用启动成功!") - print(f"中文字体加载状态: {'成功' if self.chinese_font else '失败,使用默认字体'}") - - def loadChineseFont(self): - """尝试加载中文字体""" - font_paths = [ - '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', - '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', - '/usr/share/fonts/truetype/arphic/ukai.ttc', - '/usr/share/fonts/truetype/arphic/uming.ttc', - ] - - for font_path in font_paths: - if os.path.exists(font_path): - try: - font = self.loader.loadFont(font_path) - if font: - return font - except: - continue - return None - - def createTestGUI(self): - """创建测试GUI元素""" - - # 标题 - self.title = OnscreenText( - text="2D GUI 显示测试", - pos=(0, 0.9), - scale=0.08, - fg=(1, 1, 1, 1), - align=TextNode.ACenter, - font=self.chinese_font if self.chinese_font else None - ) - - # 测试不同位置的GUI元素 - positions = [ - ("中心", (0, 0, 0)), - ("左侧", (-5, 0, 0)), - ("右侧", (5, 0, 0)), - ("上方", (0, 0, 5)), - ("下方", (0, 0, -5)) - ] - - self.gui_elements = [] - y_offset = 0.6 - - for i, (name, pos) in enumerate(positions): - # 计算屏幕位置(模拟修复后的坐标转换) - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - - # 创建按钮 - button = DirectButton( - text=f"{name}按钮", - pos=gui_pos, - scale=0.08, - command=self.onButtonClick, - extraArgs=[name], - frameColor=(0.2, 0.6, 0.8, 1), - text_font=self.chinese_font if self.chinese_font else None, - rolloverSound=None, - clickSound=None - ) - - # 偏移位置创建标签 - label_pos = (gui_pos[0], 0, gui_pos[2] - 0.2) - label = DirectLabel( - text=f"{name}标签", - pos=label_pos, - scale=0.06, - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 0, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - # 偏移位置创建输入框 - entry_pos = (gui_pos[0], 0, gui_pos[2] - 0.4) - entry = DirectEntry( - text="", - pos=entry_pos, - scale=0.05, - command=self.onEntrySubmit, - extraArgs=[name], - initialText=f"{name}输入框", - numLines=1, - width=12, - focus=0 - ) - - self.gui_elements.extend([button, label, entry]) - - print(f"创建了 {name} 位置的GUI元素:") - print(f" - 按钮位置: {gui_pos}") - print(f" - 标签位置: {label_pos}") - print(f" - 输入框位置: {entry_pos}") - - # 创建退出按钮 - self.exitButton = DirectButton( - text="退出测试", - pos=(0, 0, -0.8), - scale=0.06, - command=self.exitTest, - frameColor=(0.8, 0.2, 0.2, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - # 状态信息 - self.statusText = OnscreenText( - text="如果你能看到各个位置的按钮、标签和输入框,说明修复成功!", - pos=(0, -0.9), - scale=0.04, - fg=(0, 1, 0, 1), - align=TextNode.ACenter, - font=self.chinese_font if self.chinese_font else None - ) - - def onButtonClick(self, name): - """按钮点击事件""" - print(f"点击了 {name} 按钮") - self.statusText.setText(f"最后点击: {name} 按钮") - - def onEntrySubmit(self, text, name): - """输入框提交事件""" - print(f"{name} 输入框内容: {text}") - self.statusText.setText(f"{name} 输入框: {text}") - - def exitTest(self): - """退出测试""" - print("退出GUI测试") - self.userExit() - -if __name__ == "__main__": - print("启动2D GUI显示测试...") - print("如果修复成功,你应该能看到不同位置的按钮、标签和输入框") - app = GUITestApp() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +测试2D GUI组件显示修复 +验证按钮、标签、输入框是否能正常显示 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +from direct.showbase.ShowBase import ShowBase +from direct.gui.DirectGui import * +from panda3d.core import * +import os + +class GUITestApp(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 设置背景色 + self.setBackgroundColor(0.2, 0.2, 0.3) + + # 尝试加载中文字体 + self.chinese_font = self.loadChineseFont() + + # 创建测试GUI元素 + self.createTestGUI() + + print("GUI测试应用启动成功!") + print(f"中文字体加载状态: {'成功' if self.chinese_font else '失败,使用默认字体'}") + + def loadChineseFont(self): + """尝试加载中文字体""" + font_paths = [ + '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', + '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc', + '/usr/share/fonts/truetype/arphic/ukai.ttc', + '/usr/share/fonts/truetype/arphic/uming.ttc', + ] + + for font_path in font_paths: + if os.path.exists(font_path): + try: + font = self.loader.loadFont(font_path) + if font: + return font + except: + continue + return None + + def createTestGUI(self): + """创建测试GUI元素""" + + # 标题 + self.title = OnscreenText( + text="2D GUI 显示测试", + pos=(0, 0.9), + scale=0.08, + fg=(1, 1, 1, 1), + align=TextNode.ACenter, + font=self.chinese_font if self.chinese_font else None + ) + + # 测试不同位置的GUI元素 + positions = [ + ("中心", (0, 0, 0)), + ("左侧", (-5, 0, 0)), + ("右侧", (5, 0, 0)), + ("上方", (0, 0, 5)), + ("下方", (0, 0, -5)) + ] + + self.gui_elements = [] + y_offset = 0.6 + + for i, (name, pos) in enumerate(positions): + # 计算屏幕位置(模拟修复后的坐标转换) + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + + # 创建按钮 + button = DirectButton( + text=f"{name}按钮", + pos=gui_pos, + scale=0.08, + command=self.onButtonClick, + extraArgs=[name], + frameColor=(0.2, 0.6, 0.8, 1), + text_font=self.chinese_font if self.chinese_font else None, + rolloverSound=None, + clickSound=None + ) + + # 偏移位置创建标签 + label_pos = (gui_pos[0], 0, gui_pos[2] - 0.2) + label = DirectLabel( + text=f"{name}标签", + pos=label_pos, + scale=0.06, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 0, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + # 偏移位置创建输入框 + entry_pos = (gui_pos[0], 0, gui_pos[2] - 0.4) + entry = DirectEntry( + text="", + pos=entry_pos, + scale=0.05, + command=self.onEntrySubmit, + extraArgs=[name], + initialText=f"{name}输入框", + numLines=1, + width=12, + focus=0 + ) + + self.gui_elements.extend([button, label, entry]) + + print(f"创建了 {name} 位置的GUI元素:") + print(f" - 按钮位置: {gui_pos}") + print(f" - 标签位置: {label_pos}") + print(f" - 输入框位置: {entry_pos}") + + # 创建退出按钮 + self.exitButton = DirectButton( + text="退出测试", + pos=(0, 0, -0.8), + scale=0.06, + command=self.exitTest, + frameColor=(0.8, 0.2, 0.2, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + # 状态信息 + self.statusText = OnscreenText( + text="如果你能看到各个位置的按钮、标签和输入框,说明修复成功!", + pos=(0, -0.9), + scale=0.04, + fg=(0, 1, 0, 1), + align=TextNode.ACenter, + font=self.chinese_font if self.chinese_font else None + ) + + def onButtonClick(self, name): + """按钮点击事件""" + print(f"点击了 {name} 按钮") + self.statusText.setText(f"最后点击: {name} 按钮") + + def onEntrySubmit(self, text, name): + """输入框提交事件""" + print(f"{name} 输入框内容: {text}") + self.statusText.setText(f"{name} 输入框: {text}") + + def exitTest(self): + """退出测试""" + print("退出GUI测试") + self.userExit() + +if __name__ == "__main__": + print("启动2D GUI显示测试...") + print("如果修复成功,你应该能看到不同位置的按钮、标签和输入框") + app = GUITestApp() app.run() \ No newline at end of file diff --git a/demo/test_center_gizmo.py b/demo/test_center_gizmo.py index bd052bca..5a9cc2c8 100644 --- a/demo/test_center_gizmo.py +++ b/demo/test_center_gizmo.py @@ -1,135 +1,135 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -坐标轴中心位置测试脚本 - -测试坐标轴是否正确显示在实体中心,并且不被实体遮挡 -""" - -import sys -import os -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from main import MyWorld -from PyQt5.QtWidgets import QApplication - -def test_center_gizmo(): - """测试坐标轴中心位置显示""" - - print("=== 坐标轴中心位置测试 ===") - - # 创建应用程序 - app = QApplication.instance() - if app is None: - app = QApplication(sys.argv) - - # 创建世界 - world = MyWorld() - - print("\n1. 测试导入模型...") - - # 查找FBX测试文件 - fbx_files = [] - current_dir = os.path.dirname(os.path.abspath(__file__)) - for filename in os.listdir(current_dir): - if filename.lower().endswith('.fbx'): - fbx_files.append(os.path.join(current_dir, filename)) - - if not fbx_files: - print("× 没有找到FBX测试文件") - return - - # 导入第一个找到的FBX文件 - test_file = fbx_files[0] - print(f"导入测试文件: {test_file}") - - model = world.scene_manager.importModel(test_file) - if not model: - print("× 模型导入失败") - return - - print("✓ 模型导入成功") - - print("\n2. 测试选择模型...") - - # 模拟选择模型 - world.selection.updateSelection(model) - - if world.selection.selectedNode: - print("✓ 模型选择成功") - - # 检查坐标轴是否创建 - if world.selection.gizmo: - print("✓ 坐标轴创建成功") - - # 获取模型边界和坐标轴位置 - bounds = model.getBounds() - if bounds and not bounds.isEmpty(): - center = bounds.getCenter() - gizmo_pos = world.selection.gizmo.getPos() - - print(f"\n3. 位置验证:") - print(f" 模型边界中心: {center}") - print(f" 坐标轴位置: {gizmo_pos}") - - # 验证坐标轴是否在中心位置(允许小的浮点误差) - pos_diff = abs(gizmo_pos.x - center.x) + abs(gizmo_pos.y - center.y) + abs(gizmo_pos.z - center.z) - if pos_diff < 0.1: # 允许0.1的误差 - print("✓ 坐标轴位置正确设置在实体中心") - else: - print(f"× 坐标轴位置不在中心,偏差: {pos_diff}") - - # 检查渲染设置 - print(f"\n4. 渲染设置验证:") - print(f" 坐标轴渲染bin: {world.selection.gizmo.getBin()}") - print(f" 坐标轴深度测试: {world.selection.gizmo.getDepthTest()}") - print(f" 坐标轴深度写入: {world.selection.gizmo.getDepthWrite()}") - - if world.selection.gizmoXAxis: - print(f" X轴渲染bin: {world.selection.gizmoXAxis.getBin()}") - print(f" X轴深度测试: {world.selection.gizmoXAxis.getDepthTest()}") - - if world.selection.gizmoYAxis: - print(f" Y轴渲染bin: {world.selection.gizmoYAxis.getBin()}") - print(f" Y轴深度测试: {world.selection.gizmoYAxis.getDepthTest()}") - - if world.selection.gizmoZAxis: - print(f" Z轴渲染bin: {world.selection.gizmoZAxis.getBin()}") - print(f" Z轴深度测试: {world.selection.gizmoZAxis.getDepthTest()}") - - print("✓ 渲染设置已应用") - else: - print("× 坐标轴创建失败") - else: - print("× 模型选择失败") - - print("\n5. 测试说明:") - print(" - 坐标轴现在应该显示在实体的几何中心") - print(" - 即使部分坐标轴在实体内部,也应该完全可见") - print(" - 坐标轴具有最高的渲染优先级,不会被任何实体遮挡") - print(" - 三个轴有独立的渲染优先级:X(201), Y(202), Z(203)") - - print("\n=== 测试完成 ===") - - # 启动交互模式让用户查看结果 - print("\n按任意键查看3D场景...") - input() - - # 显示3D窗口 - try: - from ui.main_window import setup_main_window - app, main_window = setup_main_window(world) - main_window.show() - - print("✓ 3D窗口已打开,请验证:") - print(" 1. 坐标轴是否显示在实体中心") - print(" 2. 坐标轴是否完全可见(不被实体遮挡)") - print(" 3. 可以正常点击和拖拽坐标轴") - - app.exec_() - except Exception as e: - print(f"显示3D窗口时出错: {e}") - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +坐标轴中心位置测试脚本 + +测试坐标轴是否正确显示在实体中心,并且不被实体遮挡 +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from main import MyWorld +from PyQt5.QtWidgets import QApplication + +def test_center_gizmo(): + """测试坐标轴中心位置显示""" + + print("=== 坐标轴中心位置测试 ===") + + # 创建应用程序 + app = QApplication.instance() + if app is None: + app = QApplication(sys.argv) + + # 创建世界 + world = MyWorld() + + print("\n1. 测试导入模型...") + + # 查找FBX测试文件 + fbx_files = [] + current_dir = os.path.dirname(os.path.abspath(__file__)) + for filename in os.listdir(current_dir): + if filename.lower().endswith('.fbx'): + fbx_files.append(os.path.join(current_dir, filename)) + + if not fbx_files: + print("× 没有找到FBX测试文件") + return + + # 导入第一个找到的FBX文件 + test_file = fbx_files[0] + print(f"导入测试文件: {test_file}") + + model = world.scene_manager.importModel(test_file) + if not model: + print("× 模型导入失败") + return + + print("✓ 模型导入成功") + + print("\n2. 测试选择模型...") + + # 模拟选择模型 + world.selection.updateSelection(model) + + if world.selection.selectedNode: + print("✓ 模型选择成功") + + # 检查坐标轴是否创建 + if world.selection.gizmo: + print("✓ 坐标轴创建成功") + + # 获取模型边界和坐标轴位置 + bounds = model.getBounds() + if bounds and not bounds.isEmpty(): + center = bounds.getCenter() + gizmo_pos = world.selection.gizmo.getPos() + + print(f"\n3. 位置验证:") + print(f" 模型边界中心: {center}") + print(f" 坐标轴位置: {gizmo_pos}") + + # 验证坐标轴是否在中心位置(允许小的浮点误差) + pos_diff = abs(gizmo_pos.x - center.x) + abs(gizmo_pos.y - center.y) + abs(gizmo_pos.z - center.z) + if pos_diff < 0.1: # 允许0.1的误差 + print("✓ 坐标轴位置正确设置在实体中心") + else: + print(f"× 坐标轴位置不在中心,偏差: {pos_diff}") + + # 检查渲染设置 + print(f"\n4. 渲染设置验证:") + print(f" 坐标轴渲染bin: {world.selection.gizmo.getBin()}") + print(f" 坐标轴深度测试: {world.selection.gizmo.getDepthTest()}") + print(f" 坐标轴深度写入: {world.selection.gizmo.getDepthWrite()}") + + if world.selection.gizmoXAxis: + print(f" X轴渲染bin: {world.selection.gizmoXAxis.getBin()}") + print(f" X轴深度测试: {world.selection.gizmoXAxis.getDepthTest()}") + + if world.selection.gizmoYAxis: + print(f" Y轴渲染bin: {world.selection.gizmoYAxis.getBin()}") + print(f" Y轴深度测试: {world.selection.gizmoYAxis.getDepthTest()}") + + if world.selection.gizmoZAxis: + print(f" Z轴渲染bin: {world.selection.gizmoZAxis.getBin()}") + print(f" Z轴深度测试: {world.selection.gizmoZAxis.getDepthTest()}") + + print("✓ 渲染设置已应用") + else: + print("× 坐标轴创建失败") + else: + print("× 模型选择失败") + + print("\n5. 测试说明:") + print(" - 坐标轴现在应该显示在实体的几何中心") + print(" - 即使部分坐标轴在实体内部,也应该完全可见") + print(" - 坐标轴具有最高的渲染优先级,不会被任何实体遮挡") + print(" - 三个轴有独立的渲染优先级:X(201), Y(202), Z(203)") + + print("\n=== 测试完成 ===") + + # 启动交互模式让用户查看结果 + print("\n按任意键查看3D场景...") + input() + + # 显示3D窗口 + try: + from ui.main_window import setup_main_window + app, main_window = setup_main_window(world) + main_window.show() + + print("✓ 3D窗口已打开,请验证:") + print(" 1. 坐标轴是否显示在实体中心") + print(" 2. 坐标轴是否完全可见(不被实体遮挡)") + print(" 3. 可以正常点击和拖拽坐标轴") + + app.exec_() + except Exception as e: + print(f"显示3D窗口时出错: {e}") + +if __name__ == "__main__": test_center_gizmo() \ No newline at end of file diff --git a/demo/test_gizmo_drag.py b/demo/test_gizmo_drag.py index 9f3bb338..cc52d0c4 100644 --- a/demo/test_gizmo_drag.py +++ b/demo/test_gizmo_drag.py @@ -1,622 +1,622 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -测试坐标轴拖拽功能 -验证物体移动是否正常工作 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel, QPushButton, QHBoxLayout -from PyQt5.QtCore import Qt -from QMeta3D.QMeta3DWidget import QMeta3DWidget -from QMeta3D.Meta3DWorld import Meta3DWorld -from panda3d.core import * -from direct.task import Task - -class GizmoDragTestWorld(Meta3DWorld): - def __init__(self): - super().__init__() - - print("=== 坐标轴拖拽测试 ===") - self.setBackgroundColor(0.2, 0.2, 0.3) - - # 调整相机位置 - self.cam.setPos(10, -20, 10) - self.cam.lookAt(0, 0, 0) - - # Qt部件引用 - self.qtWidget = None - - # 添加基础光照 - alight = AmbientLight('alight') - alight.setColor((0.3, 0.3, 0.3, 1)) - alnp = self.render.attachNewNode(alight) - self.render.setLight(alnp) - - dlight = DirectionalLight('dlight') - dlight.setColor((0.8, 0.8, 0.8, 1)) - dlnp = self.render.attachNewNode(dlight) - dlnp.setHpr(45, -45, 0) - self.render.setLight(dlnp) - - # 创建测试立方体 - self.createTestCube() - - # 坐标轴相关变量(复制自主程序) - self.gizmo = None - self.gizmoTarget = None - self.gizmoXAxis = None - self.gizmoYAxis = None - self.gizmoZAxis = None - self.axis_length = 3.0 - - self.isDraggingGizmo = False - self.dragGizmoAxis = None - self.dragStartMousePos = None - self.gizmoTargetStartPos = None - self.gizmoHighlightAxis = None - - self.gizmo_colors = { - "x": (1, 0, 0, 1), # 红色 - "y": (0, 1, 0, 1), # 绿色 - "z": (0, 0, 1, 1) # 蓝色 - } - - self.gizmo_highlight_colors = { - "x": (1, 1, 0, 1), # 黄色高亮 - "y": (1, 1, 0, 1), - "z": (1, 1, 0, 1) - } - - # 当前工具 - self.currentTool = "移动" - - # 自动选中立方体并创建坐标轴 - self.selectedNode = self.testCube - self.createGizmo(self.testCube) - - print("测试说明:") - print("1. 立方体已选中,坐标轴已显示") - print("2. 鼠标悬停在坐标轴上应该变为黄色") - print("3. 点击并拖拽坐标轴应该移动立方体") - print("4. 观看控制台输出以了解拖拽过程") - - def setQtWidget(self, widget): - """设置Qt部件引用""" - self.qtWidget = widget - print(f"✓ 设置Qt部件引用") - - def getWindowSize(self): - """获取准确的窗口尺寸""" - if self.qtWidget: - width, height = self.qtWidget.getActualSize() - if width > 0 and height > 0: - return width, height - - if hasattr(self, 'win') and self.win: - width = self.win.getXSize() - height = self.win.getYSize() - return width, height - - return 800, 600 - - def createTestCube(self): - """创建测试立方体""" - from panda3d.core import CardMaker - - # 创建一个简单的立方体 - cm = CardMaker('cube') - cm.setFrame(-1, 1, -1, 1) - - # 创建6个面 - self.testCube = self.render.attachNewNode("testCube") - - # 前面(红色) - front = self.testCube.attachNewNode(cm.generate()) - front.setPos(0, 1, 0) - front.setColor(1, 0.5, 0.5, 1) - - # 后面(绿色) - back = self.testCube.attachNewNode(cm.generate()) - back.setPos(0, -1, 0) - back.setHpr(0, 0, 180) - back.setColor(0.5, 1, 0.5, 1) - - # 左面(蓝色) - left = self.testCube.attachNewNode(cm.generate()) - left.setPos(-1, 0, 0) - left.setHpr(0, 0, 90) - left.setColor(0.5, 0.5, 1, 1) - - # 右面(黄色) - right = self.testCube.attachNewNode(cm.generate()) - right.setPos(1, 0, 0) - right.setHpr(0, 0, -90) - right.setColor(1, 1, 0.5, 1) - - # 顶面(紫色) - top = self.testCube.attachNewNode(cm.generate()) - top.setPos(0, 0, 1) - top.setHpr(-90, 0, 0) - top.setColor(1, 0.5, 1, 1) - - # 底面(青色) - bottom = self.testCube.attachNewNode(cm.generate()) - bottom.setPos(0, 0, -1) - bottom.setHpr(90, 0, 0) - bottom.setColor(0.5, 1, 1, 1) - - print(f"✓ 创建测试立方体,位置: {self.testCube.getPos()}") - return self.testCube - - # 从主程序复制的坐标轴相关方法 - def createGizmo(self, nodePath): - """为选中的节点创建坐标轴工具""" - try: - if self.gizmo: - self.gizmo.removeNode() - self.gizmo = None - - if not nodePath: - return - - self.gizmo = self.render.attachNewNode("gizmo") - self.gizmoTarget = nodePath - - # 将坐标轴放在实体上方 - bounds = nodePath.getBounds() - if bounds and not bounds.isEmpty(): - center = bounds.getCenter() - maxPoint = bounds.getMax() - gizmo_pos = Point3(center.x, center.y, maxPoint.z + 2.0) - self.gizmo.setPos(gizmo_pos) - - self.createGizmoGeometry() - print(f"✓ 为节点 {nodePath.getName()} 创建了坐标轴") - - except Exception as e: - print(f"创建坐标轴失败: {str(e)}") - - def createGizmoGeometry(self): - """创建坐标轴的几何体""" - try: - if not self.gizmo: - return - - from panda3d.core import LineSegs - - # 创建X轴(红色) - x_lines = LineSegs() - x_lines.setThickness(4.0) - x_lines.setColor(1, 0, 0, 1) - x_lines.moveTo(0, 0, 0) - x_lines.drawTo(self.axis_length, 0, 0) - # 箭头 - x_lines.moveTo(self.axis_length - 0.5, -0.2, 0) - x_lines.drawTo(self.axis_length, 0, 0) - x_lines.drawTo(self.axis_length - 0.5, 0.2, 0) - - self.gizmoXAxis = self.gizmo.attachNewNode(x_lines.create()) - self.gizmoXAxis.setName("gizmo_x_axis") - - # 创建Y轴(绿色) - y_lines = LineSegs() - y_lines.setThickness(4.0) - y_lines.setColor(0, 1, 0, 1) - y_lines.moveTo(0, 0, 0) - y_lines.drawTo(0, self.axis_length, 0) - # 箭头 - y_lines.moveTo(-0.2, self.axis_length - 0.5, 0) - y_lines.drawTo(0, self.axis_length, 0) - y_lines.drawTo(0.2, self.axis_length - 0.5, 0) - - self.gizmoYAxis = self.gizmo.attachNewNode(y_lines.create()) - self.gizmoYAxis.setName("gizmo_y_axis") - - # 创建Z轴(蓝色) - z_lines = LineSegs() - z_lines.setThickness(4.0) - z_lines.setColor(0, 0, 1, 1) - z_lines.moveTo(0, 0, 0) - z_lines.drawTo(0, 0, self.axis_length) - # 箭头 - z_lines.moveTo(-0.2, 0, self.axis_length - 0.5) - z_lines.drawTo(0, 0, self.axis_length) - z_lines.drawTo(0.2, 0, self.axis_length - 0.5) - - self.gizmoZAxis = self.gizmo.attachNewNode(z_lines.create()) - self.gizmoZAxis.setName("gizmo_z_axis") - - # 确保坐标轴不被光照影响 - self.gizmoXAxis.setLightOff() - self.gizmoYAxis.setLightOff() - self.gizmoZAxis.setLightOff() - - except Exception as e: - print(f"创建坐标轴几何体失败: {str(e)}") - - # 主程序的鼠标事件处理方法(简化版) - def mousePressEventLeft(self, evt): - """处理鼠标左键按下事件""" - print(f"\n=== 鼠标左键点击 ===") - - if not evt: - return - - x = evt.get('x', 0) - y = evt.get('y', 0) - print(f"鼠标位置: ({x}, {y})") - - # 检查是否点击了坐标轴 - if self.currentTool == "移动" and self.gizmo: - gizmoAxis = self.checkGizmoClick(x, y) - if gizmoAxis: - print(f"✓ 检测到坐标轴点击: {gizmoAxis}") - self.startGizmoDrag(gizmoAxis, x, y) - return - else: - print("× 没有点击到坐标轴") - - def mouseReleaseEventLeft(self, evt): - """处理鼠标左键释放事件""" - if self.isDraggingGizmo: - self.stopGizmoDrag() - - def mouseMoveEvent(self, evt): - """处理鼠标移动事件""" - if not evt: - return - - # 处理坐标轴拖拽 - if self.isDraggingGizmo: - x = evt.get('x', 0) - y = evt.get('y', 0) - self.updateGizmoDrag(x, y) - return - - # 更新坐标轴高亮 - if self.currentTool == "移动" and self.gizmo and not self.isDraggingGizmo: - x = evt.get('x', 0) - y = evt.get('y', 0) - self.updateGizmoHighlight(x, y) - - # 从主程序复制的方法(这里需要包含所有坐标轴相关的方法) - # [为了简洁,这里只展示核心方法,实际应该包含所有方法] - - def checkGizmoClick(self, mouseX, mouseY): - """简化的坐标轴点击检测""" - if not self.gizmo or not self.gizmoTarget: - return None - - try: - print(f"检查坐标轴点击: 鼠标({mouseX}, {mouseY})") - - # 获取坐标轴中心的世界坐标 - gizmo_world_pos = self.gizmo.getPos(self.render) - print(f"坐标轴世界位置: {gizmo_world_pos}") - - # 计算各轴端点的世界坐标 - x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0) - y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0) - z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length) - - def worldToScreen(world_pos): - cam_pos = self.cam.getRelativePoint(self.render, world_pos) - if cam_pos.getY() <= 0: - return None - - screen_pos = Point2() - if self.cam.node().getLens().project(cam_pos, screen_pos): - win_width, win_height = self.getWindowSize() - win_x = (screen_pos.getX() + 1.0) * 0.5 * win_width - win_y = (1.0 - screen_pos.getY()) * 0.5 * win_height - return (win_x, win_y) - return None - - center_screen = worldToScreen(gizmo_world_pos) - x_screen = worldToScreen(x_end) - y_screen = worldToScreen(y_end) - z_screen = worldToScreen(z_end) - - if not center_screen: - return None - - print(f"中心屏幕坐标: {center_screen}") - print(f"X轴端点屏幕坐标: {x_screen}") - - click_threshold = 30 - - # 简化的距离检测 - def distance_to_line(point, line_start, line_end): - import math - px, py = point - x1, y1 = line_start - x2, y2 = line_end - - line_length_sq = (x2 - x1) ** 2 + (y2 - y1) ** 2 - if line_length_sq < 0.001: - return math.sqrt((px - x1) ** 2 + (py - y1) ** 2) - - t = max(0, min(1, ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / line_length_sq)) - projection_x = x1 + t * (x2 - x1) - projection_y = y1 + t * (y2 - y1) - - distance = math.sqrt((px - projection_x) ** 2 + (py - projection_y) ** 2) - return distance - - # 检测各个轴 - axes_data = [ - ("x", x_screen, "X轴"), - ("y", y_screen, "Y轴"), - ("z", z_screen, "Z轴") - ] - - for axis_name, axis_screen, axis_label in axes_data: - if axis_screen: - distance = distance_to_line( - (mouseX, mouseY), center_screen, axis_screen - ) - #print(f"{axis_label}距离: {distance:.2f}") - - if distance < click_threshold: - print(f"✓ 点击了{axis_label}") - return axis_name - - print("× 没有点击任何轴") - return None - - except Exception as e: - print(f"坐标轴点击检测失败: {str(e)}") - return None - - def startGizmoDrag(self, axis, mouseX, mouseY): - """开始坐标轴拖拽""" - try: - self.isDraggingGizmo = True - self.dragGizmoAxis = axis - self.dragStartMousePos = (mouseX, mouseY) - - # 保存开始拖拽时目标节点的位置 - if self.gizmoTarget: - self.gizmoTargetStartPos = self.gizmoTarget.getPos() - - print(f"✓ 开始拖拽 {axis} 轴,开始位置: {self.gizmoTargetStartPos}") - - except Exception as e: - print(f"开始坐标轴拖拽失败: {str(e)}") - - def updateGizmoDrag(self, mouseX, mouseY): - """更新坐标轴拖拽""" - try: - if not self.isDraggingGizmo or not self.gizmoTarget or not hasattr(self, 'dragStartMousePos'): - return - - # 计算鼠标移动距离(屏幕像素) - mouseDeltaX = mouseX - self.dragStartMousePos[0] - mouseDeltaY = mouseY - self.dragStartMousePos[1] - - # 获取坐标轴在屏幕空间的方向向量 - gizmo_world_pos = self.gizmoTargetStartPos - - if self.dragGizmoAxis == "x": - axis_end = gizmo_world_pos + Vec3(1, 0, 0) - elif self.dragGizmoAxis == "y": - axis_end = gizmo_world_pos + Vec3(0, 1, 0) - elif self.dragGizmoAxis == "z": - axis_end = gizmo_world_pos + Vec3(0, 0, 1) - else: - return - - # 投影到屏幕空间 - def worldToScreen(worldPos): - # 先转换为相机坐标系 - camPos = self.cam.getRelativePoint(self.render, worldPos) - - # 检查是否在相机前方 - if camPos.getY() <= 0: - return None - - screenPos = Point2() - if self.cam.node().getLens().project(camPos, screenPos): - # 获取准确的窗口尺寸 - winWidth, winHeight = self.getWindowSize() - - winX = (screenPos.x + 1) * 0.5 * winWidth - winY = (1 - screenPos.y) * 0.5 * winHeight - return (winX, winY) - return None - - gizmo_screen = worldToScreen(gizmo_world_pos) - axis_screen = worldToScreen(axis_end) - - if not gizmo_screen or not axis_screen: - return - - # 计算轴在屏幕空间的方向向量 - screen_axis_dir = ( - axis_screen[0] - gizmo_screen[0], - axis_screen[1] - gizmo_screen[1] - ) - - # 归一化屏幕轴方向 - import math - length = math.sqrt(screen_axis_dir[0]**2 + screen_axis_dir[1]**2) - if length > 0: - screen_axis_dir = (screen_axis_dir[0] / length, screen_axis_dir[1] / length) - else: - return - - # 将鼠标移动投影到轴方向上 - projected_distance = (mouseDeltaX * screen_axis_dir[0] + - mouseDeltaY * screen_axis_dir[1]) - - # 转换投影距离为世界坐标移动距离 - scale_factor = 0.01 # 可以调整这个值来改变拖拽灵敏度 - - if self.dragGizmoAxis == "x": - movement = Vec3(projected_distance * scale_factor, 0, 0) - elif self.dragGizmoAxis == "y": - movement = Vec3(0, projected_distance * scale_factor, 0) - elif self.dragGizmoAxis == "z": - movement = Vec3(0, 0, projected_distance * scale_factor) - - # 应用移动到目标节点 - newPos = self.gizmoTargetStartPos + movement - self.gizmoTarget.setPos(newPos) - - # 添加调试输出(只在前几次输出,避免刷屏) - if not hasattr(self, '_drag_debug_count'): - self._drag_debug_count = 0 - if self._drag_debug_count < 3: - print(f"拖拽调试 - 轴: {self.dragGizmoAxis}") - print(f" 鼠标移动: ({mouseDeltaX:.1f}, {mouseDeltaY:.1f})") - print(f" 投影距离: {projected_distance:.3f}") - print(f" 世界移动: {movement}") - print(f" 开始位置: {self.gizmoTargetStartPos}") - print(f" 新位置: {newPos}") - print(f" 实际设置位置: {self.gizmoTarget.getPos()}") - self._drag_debug_count += 1 - - except Exception as e: - print(f"更新坐标轴拖拽失败: {str(e)}") - - def stopGizmoDrag(self): - """停止坐标轴拖拽""" - self.isDraggingGizmo = False - self.dragGizmoAxis = None - self.dragStartMousePos = None - self.gizmoTargetStartPos = None - - # 重置调试计数器 - if hasattr(self, '_drag_debug_count'): - self._drag_debug_count = 0 - - print("✓ 停止坐标轴拖拽") - - def updateGizmoHighlight(self, mouseX, mouseY): - """更新坐标轴高亮状态(简化版)""" - if not self.gizmo or self.isDraggingGizmo: - return - - # 检测鼠标悬停的轴 - hoveredAxis = self.checkGizmoClick(mouseX, mouseY) - - # 如果高亮状态发生变化 - if hoveredAxis != self.gizmoHighlightAxis: - # 恢复之前高亮的轴 - if self.gizmoHighlightAxis: - self.setGizmoAxisColor(self.gizmoHighlightAxis, self.gizmo_colors[self.gizmoHighlightAxis]) - - # 高亮新的轴 - if hoveredAxis: - self.setGizmoAxisColor(hoveredAxis, self.gizmo_highlight_colors[hoveredAxis]) - - self.gizmoHighlightAxis = hoveredAxis - - def setGizmoAxisColor(self, axis, color): - """设置坐标轴颜色""" - try: - if axis == "x" and self.gizmoXAxis: - self.gizmoXAxis.setColor(*color) - elif axis == "y" and self.gizmoYAxis: - self.gizmoYAxis.setColor(*color) - elif axis == "z" and self.gizmoZAxis: - self.gizmoZAxis.setColor(*color) - except Exception as e: - print(f"设置坐标轴颜色失败: {str(e)}") - - -class CustomGizmoDragTestWidget(QMeta3DWidget): - """支持坐标轴拖拽测试的部件""" - def __init__(self, world, parent=None): - super().__init__(world, parent) - self.world = world - - if hasattr(world, 'setQtWidget'): - world.setQtWidget(self) - - def getActualSize(self): - return (self.width(), self.height()) - - def mousePressEvent(self, event): - """处理Qt鼠标按下事件""" - if event.button() == Qt.LeftButton: - self.world.mousePressEventLeft({ - 'x': event.x(), - 'y': event.y() - }) - event.accept() - - def mouseReleaseEvent(self, event): - """处理Qt鼠标释放事件""" - if event.button() == Qt.LeftButton: - self.world.mouseReleaseEventLeft({ - 'x': event.x(), - 'y': event.y() - }) - event.accept() - - def mouseMoveEvent(self, event): - """处理Qt鼠标移动事件""" - self.world.mouseMoveEvent({ - 'x': event.x(), - 'y': event.y() - }) - event.accept() - - -def main(): - print("启动坐标轴拖拽测试...") - - app = QApplication(sys.argv) - - # 创建主窗口 - mainWindow = QMainWindow() - mainWindow.setWindowTitle("坐标轴拖拽测试") - mainWindow.setGeometry(100, 100, 900, 700) - - # 创建布局 - centralWidget = QWidget() - layout = QVBoxLayout(centralWidget) - - # 添加说明 - infoLayout = QHBoxLayout() - label = QLabel("坐标轴拖拽测试 - 立方体已选中,尝试拖拽红色/绿色/蓝色坐标轴") - label.setAlignment(Qt.AlignCenter) - label.setStyleSheet("background-color: lightcyan; padding: 10px; font-size: 12px;") - infoLayout.addWidget(label) - - resetBtn = QPushButton("重置立方体位置") - infoLayout.addWidget(resetBtn) - - layout.addLayout(infoLayout) - - # 创建世界 - world = GizmoDragTestWorld() - - # 连接重置按钮 - def resetCube(): - world.testCube.setPos(0, 0, 0) - print("立方体位置已重置") - resetBtn.clicked.connect(resetCube) - - # 创建测试部件 - pandaWidget = CustomGizmoDragTestWidget(world) - layout.addWidget(pandaWidget) - - mainWindow.setCentralWidget(centralWidget) - mainWindow.show() - - print("\n使用说明:") - print("1. 立方体已选中,红/绿/蓝坐标轴应该可见") - print("2. 鼠标悬停在坐标轴上应该变为黄色") - print("3. 点击并拖拽坐标轴,立方体应该沿对应轴移动") - print("4. 观察控制台输出了解拖拽过程") - - return app.exec_() - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +测试坐标轴拖拽功能 +验证物体移动是否正常工作 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel, QPushButton, QHBoxLayout +from PyQt5.QtCore import Qt +from QMeta3D.QMeta3DWidget import QMeta3DWidget +from QMeta3D.Meta3DWorld import Meta3DWorld +from panda3d.core import * +from direct.task import Task + +class GizmoDragTestWorld(Meta3DWorld): + def __init__(self): + super().__init__() + + print("=== 坐标轴拖拽测试 ===") + self.setBackgroundColor(0.2, 0.2, 0.3) + + # 调整相机位置 + self.cam.setPos(10, -20, 10) + self.cam.lookAt(0, 0, 0) + + # Qt部件引用 + self.qtWidget = None + + # 添加基础光照 + alight = AmbientLight('alight') + alight.setColor((0.3, 0.3, 0.3, 1)) + alnp = self.render.attachNewNode(alight) + self.render.setLight(alnp) + + dlight = DirectionalLight('dlight') + dlight.setColor((0.8, 0.8, 0.8, 1)) + dlnp = self.render.attachNewNode(dlight) + dlnp.setHpr(45, -45, 0) + self.render.setLight(dlnp) + + # 创建测试立方体 + self.createTestCube() + + # 坐标轴相关变量(复制自主程序) + self.gizmo = None + self.gizmoTarget = None + self.gizmoXAxis = None + self.gizmoYAxis = None + self.gizmoZAxis = None + self.axis_length = 3.0 + + self.isDraggingGizmo = False + self.dragGizmoAxis = None + self.dragStartMousePos = None + self.gizmoTargetStartPos = None + self.gizmoHighlightAxis = None + + self.gizmo_colors = { + "x": (1, 0, 0, 1), # 红色 + "y": (0, 1, 0, 1), # 绿色 + "z": (0, 0, 1, 1) # 蓝色 + } + + self.gizmo_highlight_colors = { + "x": (1, 1, 0, 1), # 黄色高亮 + "y": (1, 1, 0, 1), + "z": (1, 1, 0, 1) + } + + # 当前工具 + self.currentTool = "移动" + + # 自动选中立方体并创建坐标轴 + self.selectedNode = self.testCube + self.createGizmo(self.testCube) + + print("测试说明:") + print("1. 立方体已选中,坐标轴已显示") + print("2. 鼠标悬停在坐标轴上应该变为黄色") + print("3. 点击并拖拽坐标轴应该移动立方体") + print("4. 观看控制台输出以了解拖拽过程") + + def setQtWidget(self, widget): + """设置Qt部件引用""" + self.qtWidget = widget + print(f"✓ 设置Qt部件引用") + + def getWindowSize(self): + """获取准确的窗口尺寸""" + if self.qtWidget: + width, height = self.qtWidget.getActualSize() + if width > 0 and height > 0: + return width, height + + if hasattr(self, 'win') and self.win: + width = self.win.getXSize() + height = self.win.getYSize() + return width, height + + return 800, 600 + + def createTestCube(self): + """创建测试立方体""" + from panda3d.core import CardMaker + + # 创建一个简单的立方体 + cm = CardMaker('cube') + cm.setFrame(-1, 1, -1, 1) + + # 创建6个面 + self.testCube = self.render.attachNewNode("testCube") + + # 前面(红色) + front = self.testCube.attachNewNode(cm.generate()) + front.setPos(0, 1, 0) + front.setColor(1, 0.5, 0.5, 1) + + # 后面(绿色) + back = self.testCube.attachNewNode(cm.generate()) + back.setPos(0, -1, 0) + back.setHpr(0, 0, 180) + back.setColor(0.5, 1, 0.5, 1) + + # 左面(蓝色) + left = self.testCube.attachNewNode(cm.generate()) + left.setPos(-1, 0, 0) + left.setHpr(0, 0, 90) + left.setColor(0.5, 0.5, 1, 1) + + # 右面(黄色) + right = self.testCube.attachNewNode(cm.generate()) + right.setPos(1, 0, 0) + right.setHpr(0, 0, -90) + right.setColor(1, 1, 0.5, 1) + + # 顶面(紫色) + top = self.testCube.attachNewNode(cm.generate()) + top.setPos(0, 0, 1) + top.setHpr(-90, 0, 0) + top.setColor(1, 0.5, 1, 1) + + # 底面(青色) + bottom = self.testCube.attachNewNode(cm.generate()) + bottom.setPos(0, 0, -1) + bottom.setHpr(90, 0, 0) + bottom.setColor(0.5, 1, 1, 1) + + print(f"✓ 创建测试立方体,位置: {self.testCube.getPos()}") + return self.testCube + + # 从主程序复制的坐标轴相关方法 + def createGizmo(self, nodePath): + """为选中的节点创建坐标轴工具""" + try: + if self.gizmo: + self.gizmo.removeNode() + self.gizmo = None + + if not nodePath: + return + + self.gizmo = self.render.attachNewNode("gizmo") + self.gizmoTarget = nodePath + + # 将坐标轴放在实体上方 + bounds = nodePath.getBounds() + if bounds and not bounds.isEmpty(): + center = bounds.getCenter() + maxPoint = bounds.getMax() + gizmo_pos = Point3(center.x, center.y, maxPoint.z + 2.0) + self.gizmo.setPos(gizmo_pos) + + self.createGizmoGeometry() + print(f"✓ 为节点 {nodePath.getName()} 创建了坐标轴") + + except Exception as e: + print(f"创建坐标轴失败: {str(e)}") + + def createGizmoGeometry(self): + """创建坐标轴的几何体""" + try: + if not self.gizmo: + return + + from panda3d.core import LineSegs + + # 创建X轴(红色) + x_lines = LineSegs() + x_lines.setThickness(4.0) + x_lines.setColor(1, 0, 0, 1) + x_lines.moveTo(0, 0, 0) + x_lines.drawTo(self.axis_length, 0, 0) + # 箭头 + x_lines.moveTo(self.axis_length - 0.5, -0.2, 0) + x_lines.drawTo(self.axis_length, 0, 0) + x_lines.drawTo(self.axis_length - 0.5, 0.2, 0) + + self.gizmoXAxis = self.gizmo.attachNewNode(x_lines.create()) + self.gizmoXAxis.setName("gizmo_x_axis") + + # 创建Y轴(绿色) + y_lines = LineSegs() + y_lines.setThickness(4.0) + y_lines.setColor(0, 1, 0, 1) + y_lines.moveTo(0, 0, 0) + y_lines.drawTo(0, self.axis_length, 0) + # 箭头 + y_lines.moveTo(-0.2, self.axis_length - 0.5, 0) + y_lines.drawTo(0, self.axis_length, 0) + y_lines.drawTo(0.2, self.axis_length - 0.5, 0) + + self.gizmoYAxis = self.gizmo.attachNewNode(y_lines.create()) + self.gizmoYAxis.setName("gizmo_y_axis") + + # 创建Z轴(蓝色) + z_lines = LineSegs() + z_lines.setThickness(4.0) + z_lines.setColor(0, 0, 1, 1) + z_lines.moveTo(0, 0, 0) + z_lines.drawTo(0, 0, self.axis_length) + # 箭头 + z_lines.moveTo(-0.2, 0, self.axis_length - 0.5) + z_lines.drawTo(0, 0, self.axis_length) + z_lines.drawTo(0.2, 0, self.axis_length - 0.5) + + self.gizmoZAxis = self.gizmo.attachNewNode(z_lines.create()) + self.gizmoZAxis.setName("gizmo_z_axis") + + # 确保坐标轴不被光照影响 + self.gizmoXAxis.setLightOff() + self.gizmoYAxis.setLightOff() + self.gizmoZAxis.setLightOff() + + except Exception as e: + print(f"创建坐标轴几何体失败: {str(e)}") + + # 主程序的鼠标事件处理方法(简化版) + def mousePressEventLeft(self, evt): + """处理鼠标左键按下事件""" + print(f"\n=== 鼠标左键点击 ===") + + if not evt: + return + + x = evt.get('x', 0) + y = evt.get('y', 0) + print(f"鼠标位置: ({x}, {y})") + + # 检查是否点击了坐标轴 + if self.currentTool == "移动" and self.gizmo: + gizmoAxis = self.checkGizmoClick(x, y) + if gizmoAxis: + print(f"✓ 检测到坐标轴点击: {gizmoAxis}") + self.startGizmoDrag(gizmoAxis, x, y) + return + else: + print("× 没有点击到坐标轴") + + def mouseReleaseEventLeft(self, evt): + """处理鼠标左键释放事件""" + if self.isDraggingGizmo: + self.stopGizmoDrag() + + def mouseMoveEvent(self, evt): + """处理鼠标移动事件""" + if not evt: + return + + # 处理坐标轴拖拽 + if self.isDraggingGizmo: + x = evt.get('x', 0) + y = evt.get('y', 0) + self.updateGizmoDrag(x, y) + return + + # 更新坐标轴高亮 + if self.currentTool == "移动" and self.gizmo and not self.isDraggingGizmo: + x = evt.get('x', 0) + y = evt.get('y', 0) + self.updateGizmoHighlight(x, y) + + # 从主程序复制的方法(这里需要包含所有坐标轴相关的方法) + # [为了简洁,这里只展示核心方法,实际应该包含所有方法] + + def checkGizmoClick(self, mouseX, mouseY): + """简化的坐标轴点击检测""" + if not self.gizmo or not self.gizmoTarget: + return None + + try: + print(f"检查坐标轴点击: 鼠标({mouseX}, {mouseY})") + + # 获取坐标轴中心的世界坐标 + gizmo_world_pos = self.gizmo.getPos(self.render) + print(f"坐标轴世界位置: {gizmo_world_pos}") + + # 计算各轴端点的世界坐标 + x_end = gizmo_world_pos + Vec3(self.axis_length, 0, 0) + y_end = gizmo_world_pos + Vec3(0, self.axis_length, 0) + z_end = gizmo_world_pos + Vec3(0, 0, self.axis_length) + + def worldToScreen(world_pos): + cam_pos = self.cam.getRelativePoint(self.render, world_pos) + if cam_pos.getY() <= 0: + return None + + screen_pos = Point2() + if self.cam.node().getLens().project(cam_pos, screen_pos): + win_width, win_height = self.getWindowSize() + win_x = (screen_pos.getX() + 1.0) * 0.5 * win_width + win_y = (1.0 - screen_pos.getY()) * 0.5 * win_height + return (win_x, win_y) + return None + + center_screen = worldToScreen(gizmo_world_pos) + x_screen = worldToScreen(x_end) + y_screen = worldToScreen(y_end) + z_screen = worldToScreen(z_end) + + if not center_screen: + return None + + print(f"中心屏幕坐标: {center_screen}") + print(f"X轴端点屏幕坐标: {x_screen}") + + click_threshold = 30 + + # 简化的距离检测 + def distance_to_line(point, line_start, line_end): + import math + px, py = point + x1, y1 = line_start + x2, y2 = line_end + + line_length_sq = (x2 - x1) ** 2 + (y2 - y1) ** 2 + if line_length_sq < 0.001: + return math.sqrt((px - x1) ** 2 + (py - y1) ** 2) + + t = max(0, min(1, ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / line_length_sq)) + projection_x = x1 + t * (x2 - x1) + projection_y = y1 + t * (y2 - y1) + + distance = math.sqrt((px - projection_x) ** 2 + (py - projection_y) ** 2) + return distance + + # 检测各个轴 + axes_data = [ + ("x", x_screen, "X轴"), + ("y", y_screen, "Y轴"), + ("z", z_screen, "Z轴") + ] + + for axis_name, axis_screen, axis_label in axes_data: + if axis_screen: + distance = distance_to_line( + (mouseX, mouseY), center_screen, axis_screen + ) + #print(f"{axis_label}距离: {distance:.2f}") + + if distance < click_threshold: + print(f"✓ 点击了{axis_label}") + return axis_name + + print("× 没有点击任何轴") + return None + + except Exception as e: + print(f"坐标轴点击检测失败: {str(e)}") + return None + + def startGizmoDrag(self, axis, mouseX, mouseY): + """开始坐标轴拖拽""" + try: + self.isDraggingGizmo = True + self.dragGizmoAxis = axis + self.dragStartMousePos = (mouseX, mouseY) + + # 保存开始拖拽时目标节点的位置 + if self.gizmoTarget: + self.gizmoTargetStartPos = self.gizmoTarget.getPos() + + print(f"✓ 开始拖拽 {axis} 轴,开始位置: {self.gizmoTargetStartPos}") + + except Exception as e: + print(f"开始坐标轴拖拽失败: {str(e)}") + + def updateGizmoDrag(self, mouseX, mouseY): + """更新坐标轴拖拽""" + try: + if not self.isDraggingGizmo or not self.gizmoTarget or not hasattr(self, 'dragStartMousePos'): + return + + # 计算鼠标移动距离(屏幕像素) + mouseDeltaX = mouseX - self.dragStartMousePos[0] + mouseDeltaY = mouseY - self.dragStartMousePos[1] + + # 获取坐标轴在屏幕空间的方向向量 + gizmo_world_pos = self.gizmoTargetStartPos + + if self.dragGizmoAxis == "x": + axis_end = gizmo_world_pos + Vec3(1, 0, 0) + elif self.dragGizmoAxis == "y": + axis_end = gizmo_world_pos + Vec3(0, 1, 0) + elif self.dragGizmoAxis == "z": + axis_end = gizmo_world_pos + Vec3(0, 0, 1) + else: + return + + # 投影到屏幕空间 + def worldToScreen(worldPos): + # 先转换为相机坐标系 + camPos = self.cam.getRelativePoint(self.render, worldPos) + + # 检查是否在相机前方 + if camPos.getY() <= 0: + return None + + screenPos = Point2() + if self.cam.node().getLens().project(camPos, screenPos): + # 获取准确的窗口尺寸 + winWidth, winHeight = self.getWindowSize() + + winX = (screenPos.x + 1) * 0.5 * winWidth + winY = (1 - screenPos.y) * 0.5 * winHeight + return (winX, winY) + return None + + gizmo_screen = worldToScreen(gizmo_world_pos) + axis_screen = worldToScreen(axis_end) + + if not gizmo_screen or not axis_screen: + return + + # 计算轴在屏幕空间的方向向量 + screen_axis_dir = ( + axis_screen[0] - gizmo_screen[0], + axis_screen[1] - gizmo_screen[1] + ) + + # 归一化屏幕轴方向 + import math + length = math.sqrt(screen_axis_dir[0]**2 + screen_axis_dir[1]**2) + if length > 0: + screen_axis_dir = (screen_axis_dir[0] / length, screen_axis_dir[1] / length) + else: + return + + # 将鼠标移动投影到轴方向上 + projected_distance = (mouseDeltaX * screen_axis_dir[0] + + mouseDeltaY * screen_axis_dir[1]) + + # 转换投影距离为世界坐标移动距离 + scale_factor = 0.01 # 可以调整这个值来改变拖拽灵敏度 + + if self.dragGizmoAxis == "x": + movement = Vec3(projected_distance * scale_factor, 0, 0) + elif self.dragGizmoAxis == "y": + movement = Vec3(0, projected_distance * scale_factor, 0) + elif self.dragGizmoAxis == "z": + movement = Vec3(0, 0, projected_distance * scale_factor) + + # 应用移动到目标节点 + newPos = self.gizmoTargetStartPos + movement + self.gizmoTarget.setPos(newPos) + + # 添加调试输出(只在前几次输出,避免刷屏) + if not hasattr(self, '_drag_debug_count'): + self._drag_debug_count = 0 + if self._drag_debug_count < 3: + print(f"拖拽调试 - 轴: {self.dragGizmoAxis}") + print(f" 鼠标移动: ({mouseDeltaX:.1f}, {mouseDeltaY:.1f})") + print(f" 投影距离: {projected_distance:.3f}") + print(f" 世界移动: {movement}") + print(f" 开始位置: {self.gizmoTargetStartPos}") + print(f" 新位置: {newPos}") + print(f" 实际设置位置: {self.gizmoTarget.getPos()}") + self._drag_debug_count += 1 + + except Exception as e: + print(f"更新坐标轴拖拽失败: {str(e)}") + + def stopGizmoDrag(self): + """停止坐标轴拖拽""" + self.isDraggingGizmo = False + self.dragGizmoAxis = None + self.dragStartMousePos = None + self.gizmoTargetStartPos = None + + # 重置调试计数器 + if hasattr(self, '_drag_debug_count'): + self._drag_debug_count = 0 + + print("✓ 停止坐标轴拖拽") + + def updateGizmoHighlight(self, mouseX, mouseY): + """更新坐标轴高亮状态(简化版)""" + if not self.gizmo or self.isDraggingGizmo: + return + + # 检测鼠标悬停的轴 + hoveredAxis = self.checkGizmoClick(mouseX, mouseY) + + # 如果高亮状态发生变化 + if hoveredAxis != self.gizmoHighlightAxis: + # 恢复之前高亮的轴 + if self.gizmoHighlightAxis: + self.setGizmoAxisColor(self.gizmoHighlightAxis, self.gizmo_colors[self.gizmoHighlightAxis]) + + # 高亮新的轴 + if hoveredAxis: + self.setGizmoAxisColor(hoveredAxis, self.gizmo_highlight_colors[hoveredAxis]) + + self.gizmoHighlightAxis = hoveredAxis + + def setGizmoAxisColor(self, axis, color): + """设置坐标轴颜色""" + try: + if axis == "x" and self.gizmoXAxis: + self.gizmoXAxis.setColor(*color) + elif axis == "y" and self.gizmoYAxis: + self.gizmoYAxis.setColor(*color) + elif axis == "z" and self.gizmoZAxis: + self.gizmoZAxis.setColor(*color) + except Exception as e: + print(f"设置坐标轴颜色失败: {str(e)}") + + +class CustomGizmoDragTestWidget(QMeta3DWidget): + """支持坐标轴拖拽测试的部件""" + def __init__(self, world, parent=None): + super().__init__(world, parent) + self.world = world + + if hasattr(world, 'setQtWidget'): + world.setQtWidget(self) + + def getActualSize(self): + return (self.width(), self.height()) + + def mousePressEvent(self, event): + """处理Qt鼠标按下事件""" + if event.button() == Qt.LeftButton: + self.world.mousePressEventLeft({ + 'x': event.x(), + 'y': event.y() + }) + event.accept() + + def mouseReleaseEvent(self, event): + """处理Qt鼠标释放事件""" + if event.button() == Qt.LeftButton: + self.world.mouseReleaseEventLeft({ + 'x': event.x(), + 'y': event.y() + }) + event.accept() + + def mouseMoveEvent(self, event): + """处理Qt鼠标移动事件""" + self.world.mouseMoveEvent({ + 'x': event.x(), + 'y': event.y() + }) + event.accept() + + +def main(): + print("启动坐标轴拖拽测试...") + + app = QApplication(sys.argv) + + # 创建主窗口 + mainWindow = QMainWindow() + mainWindow.setWindowTitle("坐标轴拖拽测试") + mainWindow.setGeometry(100, 100, 900, 700) + + # 创建布局 + centralWidget = QWidget() + layout = QVBoxLayout(centralWidget) + + # 添加说明 + infoLayout = QHBoxLayout() + label = QLabel("坐标轴拖拽测试 - 立方体已选中,尝试拖拽红色/绿色/蓝色坐标轴") + label.setAlignment(Qt.AlignCenter) + label.setStyleSheet("background-color: lightcyan; padding: 10px; font-size: 12px;") + infoLayout.addWidget(label) + + resetBtn = QPushButton("重置立方体位置") + infoLayout.addWidget(resetBtn) + + layout.addLayout(infoLayout) + + # 创建世界 + world = GizmoDragTestWorld() + + # 连接重置按钮 + def resetCube(): + world.testCube.setPos(0, 0, 0) + print("立方体位置已重置") + resetBtn.clicked.connect(resetCube) + + # 创建测试部件 + pandaWidget = CustomGizmoDragTestWidget(world) + layout.addWidget(pandaWidget) + + mainWindow.setCentralWidget(centralWidget) + mainWindow.show() + + print("\n使用说明:") + print("1. 立方体已选中,红/绿/蓝坐标轴应该可见") + print("2. 鼠标悬停在坐标轴上应该变为黄色") + print("3. 点击并拖拽坐标轴,立方体应该沿对应轴移动") + print("4. 观察控制台输出了解拖拽过程") + + return app.exec_() + +if __name__ == "__main__": sys.exit(main()) \ No newline at end of file diff --git a/demo/test_graphics_buffer_gui.py b/demo/test_graphics_buffer_gui.py index ea8841d8..a3924d89 100644 --- a/demo/test_graphics_buffer_gui.py +++ b/demo/test_graphics_buffer_gui.py @@ -1,210 +1,210 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -GraphicsBuffer DirectGUI专门测试 -验证DirectGUI在GraphicsBuffer中的行为 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -from direct.showbase.ShowBase import ShowBase -from direct.gui.DirectGui import * -from direct.gui.OnscreenText import OnscreenText -from panda3d.core import * - -class GraphicsBufferGUITest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - print("=== GraphicsBuffer DirectGUI测试 ===") - - # 设置背景色 - self.setBackgroundColor(0.2, 0.2, 0.3) - - # 创建一个GraphicsBuffer来模拟Qt环境 - self.createGraphicsBuffer() - - # 在主窗口中创建GUI (作为对照) - self.createMainWindowGUI() - - # 在GraphicsBuffer中创建GUI (测试目标) - self.createBufferGUI() - - print("\n测试说明:") - print("这个测试创建了GraphicsBuffer来模拟Qt环境") - print("比较主窗口和GraphicsBuffer中DirectGUI的行为差异") - - def createGraphicsBuffer(self): - """创建GraphicsBuffer""" - print("\n--- 创建GraphicsBuffer ---") - - # 创建缓冲区 - fbp = FrameBufferProperties() - wp = WindowProperties.size(800, 600) - wp.setUndecorated(True) - - self.buffer = self.graphicsEngine.makeOutput( - self.pipe, - "test-buffer", - -100, # sort - fbp, - wp, - GraphicsPipe.BFRequireWindow # flags - ) - - if not self.buffer: - print("❌ 创建GraphicsBuffer失败") - return - - print(f"✓ GraphicsBuffer创建成功: {self.buffer}") - print(f" 类型: {type(self.buffer)}") - print(f" 大小: {self.buffer.getXSize()} x {self.buffer.getYSize()}") - - # 为缓冲区创建显示区域 - self.bufferDR = self.buffer.makeDisplayRegion() - print(f"✓ 显示区域创建: {self.bufferDR}") - - # 创建专用的2D相机给GraphicsBuffer - self.bufferCamera2d = self.makeCamera2d(self.buffer) - print(f"✓ 2D相机创建: {self.bufferCamera2d}") - - # 设置相机 - self.bufferDR.setCamera(self.bufferCamera2d) - - # 获取render2d节点 - 这是关键 - self.bufferRender2d = self.bufferCamera2d.getParent() - print(f"✓ 缓冲区render2d: {self.bufferRender2d}") - - # 获取aspect2d节点 - self.bufferAspect2d = self.bufferRender2d.find("**/aspect2d") - if self.bufferAspect2d.isEmpty(): - print("⚠️ 缓冲区中没有找到aspect2d,手动创建") - self.bufferAspect2d = self.bufferRender2d.attachNewNode("aspect2d") - else: - print(f"✓ 缓冲区aspect2d: {self.bufferAspect2d}") - - def createMainWindowGUI(self): - """在主窗口中创建GUI(对照组)""" - print("\n--- 主窗口GUI(对照组)---") - - self.mainTitle = OnscreenText( - text="主窗口GUI(正常)", - pos=(-0.5, 0.8), - scale=0.08, - fg=(1, 1, 1, 1), - align=TextNode.ACenter - ) - - self.mainButton = DirectButton( - text="主窗口按钮", - pos=(-0.5, 0, 0.5), - scale=0.1, - frameColor=(0, 1, 0, 1), - command=self.onMainButtonClick - ) - - print(f"✓ 主窗口标题: {self.mainTitle}") - print(f"✓ 主窗口按钮: {self.mainButton}") - print(f" 按钮父节点: {self.mainButton.getParent()}") - - def createBufferGUI(self): - """在GraphicsBuffer中创建GUI(测试组)""" - print("\n--- GraphicsBuffer GUI(测试组)---") - - if not hasattr(self, 'bufferAspect2d'): - print("❌ 缓冲区aspect2d不可用") - return - - # 方法1: 直接在缓冲区的aspect2d下创建 - try: - self.bufferTitle = OnscreenText( - text="缓冲区GUI(测试)", - pos=(0.5, 0.8), - scale=0.08, - fg=(1, 1, 0, 1), - align=TextNode.ACenter, - parent=self.bufferAspect2d # 指定父节点 - ) - print(f"✓ 缓冲区标题: {self.bufferTitle}") - print(f" 标题父节点: {self.bufferTitle.getParent()}") - except Exception as e: - print(f"❌ 缓冲区标题创建失败: {str(e)}") - - # 方法2: 创建DirectGUI组件并重新父化 - try: - self.bufferButton = DirectButton( - text="缓冲区按钮", - pos=(0.5, 0, 0.5), - scale=0.1, - frameColor=(1, 0, 0, 1), - command=self.onBufferButtonClick - ) - - # 重新父化到缓冲区的aspect2d - self.bufferButton.reparentTo(self.bufferAspect2d) - - print(f"✓ 缓冲区按钮: {self.bufferButton}") - print(f" 按钮父节点: {self.bufferButton.getParent()}") - - except Exception as e: - print(f"❌ 缓冲区按钮创建失败: {str(e)}") - - # 方法3: 直接在bufferRender2d下创建TextNode - try: - textNode = TextNode('buffer-direct-text') - textNode.setText("直接创建的文本") - textNode.setAlign(TextNode.ACenter) - self.bufferDirectText = self.bufferRender2d.attachNewNode(textNode) - self.bufferDirectText.setPos(0.5, 0, 0) - self.bufferDirectText.setScale(0.08) - self.bufferDirectText.setColor(0, 1, 1, 1) - - print(f"✓ 缓冲区直接文本: {self.bufferDirectText}") - - except Exception as e: - print(f"❌ 缓冲区直接文本创建失败: {str(e)}") - - # 检查缓冲区状态 - self.checkBufferState() - - def checkBufferState(self): - """检查缓冲区状态""" - print("\n--- 缓冲区状态检查 ---") - - if hasattr(self, 'buffer'): - print(f"缓冲区有效: {self.buffer.isValid()}") - print(f"缓冲区激活: {self.buffer.isActive()}") - print(f"显示区域数量: {self.buffer.getNumDisplayRegions()}") - - for i in range(self.buffer.getNumDisplayRegions()): - dr = self.buffer.getDisplayRegion(i) - print(f" 显示区域 {i}: {dr}") - print(f" 相机: {dr.getCamera()}") - print(f" 激活: {dr.isActive()}") - - if hasattr(self, 'bufferAspect2d'): - print(f"缓冲区aspect2d子节点数: {self.bufferAspect2d.getNumChildren()}") - for i in range(self.bufferAspect2d.getNumChildren()): - child = self.bufferAspect2d.getChild(i) - print(f" 子节点 {i}: {child}") - - def onMainButtonClick(self): - """主窗口按钮点击""" - print("🟢 主窗口按钮被点击!") - - def onBufferButtonClick(self): - """缓冲区按钮点击""" - print("🔴 缓冲区按钮被点击!") - -def main(): - """主函数""" - print("启动GraphicsBuffer DirectGUI专门测试...") - - app = GraphicsBufferGUITest() - app.run() - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +GraphicsBuffer DirectGUI专门测试 +验证DirectGUI在GraphicsBuffer中的行为 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +from direct.showbase.ShowBase import ShowBase +from direct.gui.DirectGui import * +from direct.gui.OnscreenText import OnscreenText +from panda3d.core import * + +class GraphicsBufferGUITest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + print("=== GraphicsBuffer DirectGUI测试 ===") + + # 设置背景色 + self.setBackgroundColor(0.2, 0.2, 0.3) + + # 创建一个GraphicsBuffer来模拟Qt环境 + self.createGraphicsBuffer() + + # 在主窗口中创建GUI (作为对照) + self.createMainWindowGUI() + + # 在GraphicsBuffer中创建GUI (测试目标) + self.createBufferGUI() + + print("\n测试说明:") + print("这个测试创建了GraphicsBuffer来模拟Qt环境") + print("比较主窗口和GraphicsBuffer中DirectGUI的行为差异") + + def createGraphicsBuffer(self): + """创建GraphicsBuffer""" + print("\n--- 创建GraphicsBuffer ---") + + # 创建缓冲区 + fbp = FrameBufferProperties() + wp = WindowProperties.size(800, 600) + wp.setUndecorated(True) + + self.buffer = self.graphicsEngine.makeOutput( + self.pipe, + "test-buffer", + -100, # sort + fbp, + wp, + GraphicsPipe.BFRequireWindow # flags + ) + + if not self.buffer: + print("❌ 创建GraphicsBuffer失败") + return + + print(f"✓ GraphicsBuffer创建成功: {self.buffer}") + print(f" 类型: {type(self.buffer)}") + print(f" 大小: {self.buffer.getXSize()} x {self.buffer.getYSize()}") + + # 为缓冲区创建显示区域 + self.bufferDR = self.buffer.makeDisplayRegion() + print(f"✓ 显示区域创建: {self.bufferDR}") + + # 创建专用的2D相机给GraphicsBuffer + self.bufferCamera2d = self.makeCamera2d(self.buffer) + print(f"✓ 2D相机创建: {self.bufferCamera2d}") + + # 设置相机 + self.bufferDR.setCamera(self.bufferCamera2d) + + # 获取render2d节点 - 这是关键 + self.bufferRender2d = self.bufferCamera2d.getParent() + print(f"✓ 缓冲区render2d: {self.bufferRender2d}") + + # 获取aspect2d节点 + self.bufferAspect2d = self.bufferRender2d.find("**/aspect2d") + if self.bufferAspect2d.isEmpty(): + print("⚠️ 缓冲区中没有找到aspect2d,手动创建") + self.bufferAspect2d = self.bufferRender2d.attachNewNode("aspect2d") + else: + print(f"✓ 缓冲区aspect2d: {self.bufferAspect2d}") + + def createMainWindowGUI(self): + """在主窗口中创建GUI(对照组)""" + print("\n--- 主窗口GUI(对照组)---") + + self.mainTitle = OnscreenText( + text="主窗口GUI(正常)", + pos=(-0.5, 0.8), + scale=0.08, + fg=(1, 1, 1, 1), + align=TextNode.ACenter + ) + + self.mainButton = DirectButton( + text="主窗口按钮", + pos=(-0.5, 0, 0.5), + scale=0.1, + frameColor=(0, 1, 0, 1), + command=self.onMainButtonClick + ) + + print(f"✓ 主窗口标题: {self.mainTitle}") + print(f"✓ 主窗口按钮: {self.mainButton}") + print(f" 按钮父节点: {self.mainButton.getParent()}") + + def createBufferGUI(self): + """在GraphicsBuffer中创建GUI(测试组)""" + print("\n--- GraphicsBuffer GUI(测试组)---") + + if not hasattr(self, 'bufferAspect2d'): + print("❌ 缓冲区aspect2d不可用") + return + + # 方法1: 直接在缓冲区的aspect2d下创建 + try: + self.bufferTitle = OnscreenText( + text="缓冲区GUI(测试)", + pos=(0.5, 0.8), + scale=0.08, + fg=(1, 1, 0, 1), + align=TextNode.ACenter, + parent=self.bufferAspect2d # 指定父节点 + ) + print(f"✓ 缓冲区标题: {self.bufferTitle}") + print(f" 标题父节点: {self.bufferTitle.getParent()}") + except Exception as e: + print(f"❌ 缓冲区标题创建失败: {str(e)}") + + # 方法2: 创建DirectGUI组件并重新父化 + try: + self.bufferButton = DirectButton( + text="缓冲区按钮", + pos=(0.5, 0, 0.5), + scale=0.1, + frameColor=(1, 0, 0, 1), + command=self.onBufferButtonClick + ) + + # 重新父化到缓冲区的aspect2d + self.bufferButton.reparentTo(self.bufferAspect2d) + + print(f"✓ 缓冲区按钮: {self.bufferButton}") + print(f" 按钮父节点: {self.bufferButton.getParent()}") + + except Exception as e: + print(f"❌ 缓冲区按钮创建失败: {str(e)}") + + # 方法3: 直接在bufferRender2d下创建TextNode + try: + textNode = TextNode('buffer-direct-text') + textNode.setText("直接创建的文本") + textNode.setAlign(TextNode.ACenter) + self.bufferDirectText = self.bufferRender2d.attachNewNode(textNode) + self.bufferDirectText.setPos(0.5, 0, 0) + self.bufferDirectText.setScale(0.08) + self.bufferDirectText.setColor(0, 1, 1, 1) + + print(f"✓ 缓冲区直接文本: {self.bufferDirectText}") + + except Exception as e: + print(f"❌ 缓冲区直接文本创建失败: {str(e)}") + + # 检查缓冲区状态 + self.checkBufferState() + + def checkBufferState(self): + """检查缓冲区状态""" + print("\n--- 缓冲区状态检查 ---") + + if hasattr(self, 'buffer'): + print(f"缓冲区有效: {self.buffer.isValid()}") + print(f"缓冲区激活: {self.buffer.isActive()}") + print(f"显示区域数量: {self.buffer.getNumDisplayRegions()}") + + for i in range(self.buffer.getNumDisplayRegions()): + dr = self.buffer.getDisplayRegion(i) + print(f" 显示区域 {i}: {dr}") + print(f" 相机: {dr.getCamera()}") + print(f" 激活: {dr.isActive()}") + + if hasattr(self, 'bufferAspect2d'): + print(f"缓冲区aspect2d子节点数: {self.bufferAspect2d.getNumChildren()}") + for i in range(self.bufferAspect2d.getNumChildren()): + child = self.bufferAspect2d.getChild(i) + print(f" 子节点 {i}: {child}") + + def onMainButtonClick(self): + """主窗口按钮点击""" + print("🟢 主窗口按钮被点击!") + + def onBufferButtonClick(self): + """缓冲区按钮点击""" + print("🔴 缓冲区按钮被点击!") + +def main(): + """主函数""" + print("启动GraphicsBuffer DirectGUI专门测试...") + + app = GraphicsBufferGUITest() + app.run() + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/demo/test_gui_complete.py b/demo/test_gui_complete.py index 57052c80..cbc9c9d8 100644 --- a/demo/test_gui_complete.py +++ b/demo/test_gui_complete.py @@ -1,311 +1,311 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -完整的GUI功能测试 -测试主程序中的所有GUI创建和编辑功能 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -import builtins -from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction, - QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem, - QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QTreeView, QInputDialog, QFileDialog, QMessageBox, QDialog, QGroupBox, QHBoxLayout, QPushButton, QDialogButtonBox) -from PyQt5.QtCore import Qt, QDir, QUrl -from PyQt5.QtGui import QDrag, QPainter, QPixmap -from QMeta3D.QMeta3DWidget import QMeta3DWidget -from QMeta3D.Meta3DWorld import Meta3DWorld -from direct.gui.DirectGui import * -from direct.gui.OnscreenText import OnscreenText -from panda3d.core import * -import os - -# 简化的世界类,专门用于GUI测试 -class GUITestWorld(Meta3DWorld): - def __init__(self): - super().__init__() - - # 设置相机位置 - self.cam.setPos(0, -10, 5) - self.cam.lookAt(0, 0, 0) - - # 设置背景色 - self.setBackgroundColor(0.2, 0.2, 0.3) - - self.gui_elements = [] - - # 尝试加载中文字体 - try: - self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') - if not self.chinese_font: - print("警告: 无法加载中文字体,将使用默认字体") - except: - print("警告: 无法加载中文字体,将使用默认字体") - self.chinese_font = None - - # 延迟创建GUI元素,确保所有系统都已初始化 - self.taskMgr.doMethodLater(0.1, self.createTestGUIElements, "create-gui") - - def createTestGUIElements(self, task=None): - """创建测试用的GUI元素""" - print("\n=== 开始创建测试GUI元素 ===") - - # 测试2D GUI组件 - print("创建2D GUI组件...") - - # 按钮测试 - 不同位置 - self.button1 = self.createGUIButton((0, 0, 0), "中心按钮", 0.08) - self.button2 = self.createGUIButton((-5, 0, 0), "左侧按钮", 0.08) - self.button3 = self.createGUIButton((5, 0, 0), "右侧按钮", 0.08) - self.button4 = self.createGUIButton((0, 0, 5), "上方按钮", 0.08) - self.button5 = self.createGUIButton((0, 0, -5), "下方按钮", 0.08) - - # 标签测试 - self.label1 = self.createGUILabel((0, 0, -8), "测试标签", 0.06) - self.label2 = self.createGUILabel((-8, 0, 0), "左标签", 0.06) - self.label3 = self.createGUILabel((8, 0, 0), "右标签", 0.06) - - # 输入框测试 - self.entry1 = self.createGUIEntry((0, 0, -12), "中心输入框", 0.05) - self.entry2 = self.createGUIEntry((-8, 0, -8), "左输入框", 0.05) - - # 测试3D GUI组件 - print("创建3D GUI组件...") - - # 3D文本测试 - self.text3d1 = self.createGUI3DText((0, 5, 2), "3D测试文本", 0.5) - self.text3d2 = self.createGUI3DText((-3, 3, 1), "左3D文本", 0.3) - self.text3d3 = self.createGUI3DText((3, 3, 1), "右3D文本", 0.3) - - # 虚拟屏幕测试 - self.screen1 = self.createGUIVirtualScreen((0, 8, 0), (3, 2), "主虚拟屏幕") - self.screen2 = self.createGUIVirtualScreen((-5, 5, 2), (2, 1), "左屏幕") - self.screen3 = self.createGUIVirtualScreen((5, 5, 2), (2, 1), "右屏幕") - - print(f"=== GUI元素创建完成,总计: {len(self.gui_elements)} 个 ===\n") - - # 验证每个元素的显示状态 - self.verifyGUIElements() - - # 添加标题 - self.title = OnscreenText( - text="Qt集成GUI测试", - pos=(0, 0.9), - scale=0.08, - fg=(1, 1, 1, 1), - align=TextNode.ACenter, - font=self.chinese_font if self.chinese_font else None - ) - - return task.done if task else None - - def verifyGUIElements(self): - """验证GUI元素的显示状态""" - print("=== 验证GUI元素显示状态 ===") - - for i, element in enumerate(self.gui_elements): - gui_type = element.getTag("gui_type") if hasattr(element, 'getTag') else "unknown" - gui_text = element.getTag("gui_text") if hasattr(element, 'getTag') else "unnamed" - - if hasattr(element, 'getPos'): - pos = element.getPos() - print(f"{i+1}. {gui_type}: '{gui_text}' 位置: {pos}") - else: - print(f"{i+1}. {gui_type}: '{gui_text}' (无位置信息)") - - print("=== 验证完成 ===\n") - - # 复制主程序中的GUI创建方法 - def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1): - """创建2D GUI按钮""" - from direct.gui.DirectGui import DirectButton - - # 将3D坐标转换为2D屏幕坐标 - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - - try: - button = DirectButton( - text=text, - pos=gui_pos, - scale=size, - command=self.onGUIButtonClick, - extraArgs=[f"button_{len(self.gui_elements)}"], - frameColor=(0.2, 0.6, 0.8, 1), - text_font=self.chinese_font if self.chinese_font else None, - rolloverSound=None, - clickSound=None - ) - - button.setTag("gui_type", "button") - button.setTag("gui_id", f"button_{len(self.gui_elements)}") - button.setTag("gui_text", text) - - self.gui_elements.append(button) - print(f"✓ 创建GUI按钮成功: {text} 在位置 {gui_pos}") - print(f" 按钮节点: {button}") - print(f" 按钮父节点: {button.getParent()}") - - # 验证按钮是否真的可见 - if hasattr(button, 'isHidden'): - print(f" 按钮隐藏状态: {button.isHidden()}") - - return button - - except Exception as e: - print(f"✗ 创建GUI按钮失败: {text} - 错误: {str(e)}") - return None - - def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08): - """创建2D GUI标签""" - from direct.gui.DirectGui import DirectLabel - - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - - label = DirectLabel( - text=text, - pos=gui_pos, - scale=size, - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 1, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - label.setTag("gui_type", "label") - label.setTag("gui_id", f"label_{len(self.gui_elements)}") - label.setTag("gui_text", text) - - self.gui_elements.append(label) - print(f"创建GUI标签: {text} 在位置 {gui_pos}") - return label - - def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08): - """创建2D GUI文本输入框""" - from direct.gui.DirectGui import DirectEntry - - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - - entry = DirectEntry( - text="", - pos=gui_pos, - scale=size, - command=self.onGUIEntrySubmit, - extraArgs=[f"entry_{len(self.gui_elements)}"], - initialText=placeholder, - numLines=1, - width=12, - focus=0 - ) - - entry.setTag("gui_type", "entry") - entry.setTag("gui_id", f"entry_{len(self.gui_elements)}") - entry.setTag("gui_placeholder", placeholder) - - self.gui_elements.append(entry) - print(f"创建GUI输入框: {placeholder} 在位置 {gui_pos}") - return entry - - def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): - """创建3D空间文本""" - from panda3d.core import TextNode - - textNode = TextNode(f'3d-text-{len(self.gui_elements)}') - textNode.setText(text) - textNode.setAlign(TextNode.ACenter) - if self.chinese_font: - textNode.setFont(self.chinese_font) - - textNodePath = self.render.attachNewNode(textNode) - textNodePath.setPos(*pos) - textNodePath.setScale(size) - textNodePath.setColor(1, 1, 0, 1) - textNodePath.setBillboardAxis() - - textNodePath.setTag("gui_type", "3d_text") - textNodePath.setTag("gui_id", f"3d_text_{len(self.gui_elements)}") - textNodePath.setTag("gui_text", text) - textNodePath.setTag("is_gui_element", "1") - - self.gui_elements.append(textNodePath) - print(f"创建3D文本: {text} 在位置 {pos}") - return textNodePath - - def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"): - """创建3D虚拟屏幕""" - from panda3d.core import CardMaker, TransparencyAttrib, TextNode - - cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}") - cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2) - virtualScreen = self.render.attachNewNode(cm.generate()) - virtualScreen.setPos(*pos) - virtualScreen.setColor(0.2, 0.2, 0.2, 0.8) - virtualScreen.setTransparency(TransparencyAttrib.MAlpha) - - screenText = TextNode(f'screen-text-{len(self.gui_elements)}') - screenText.setText(text) - screenText.setAlign(TextNode.ACenter) - if self.chinese_font: - screenText.setFont(self.chinese_font) - screenTextNP = virtualScreen.attachNewNode(screenText) - screenTextNP.setPos(0, 0.01, 0) - screenTextNP.setScale(0.3) - screenTextNP.setColor(0, 1, 0, 1) - - virtualScreen.setTag("gui_type", "virtual_screen") - virtualScreen.setTag("gui_id", f"virtual_screen_{len(self.gui_elements)}") - virtualScreen.setTag("gui_text", text) - virtualScreen.setTag("is_gui_element", "1") - - self.gui_elements.append(virtualScreen) - print(f"创建虚拟屏幕: {text} 在位置 {pos}") - return virtualScreen - - def onGUIButtonClick(self, button_id): - """GUI按钮点击事件处理""" - print(f"✓ GUI按钮点击测试成功: {button_id}") - - def onGUIEntrySubmit(self, text, entry_id): - """GUI输入框提交事件处理""" - print(f"✓ GUI输入框提交测试成功: {entry_id} = {text}") - -class GUITestWidget(QMeta3DWidget): - def __init__(self, world, parent=None): - super().__init__(world, parent) - self.world = world - -def testMainGUIFeatures(): - """测试主程序的GUI功能""" - print("=== 开始测试主程序GUI功能 ===") - - app = QApplication(sys.argv) - - # 创建测试世界 - world = GUITestWorld() - - # 创建主窗口 - mainWindow = QMainWindow() - mainWindow.setWindowTitle("GUI功能完整测试") - mainWindow.setGeometry(100, 100, 1200, 800) - - # 创建Panda3D部件 - pandaWidget = GUITestWidget(world) - mainWindow.setCentralWidget(pandaWidget) - - # 显示窗口 - mainWindow.show() - - print("测试窗口已打开,你应该能看到:") - print("- 5个不同位置的2D按钮(可点击)") - print("- 3个不同位置的2D标签") - print("- 2个不同位置的2D输入框(可输入)") - print("- 3个不同位置的3D文本(面向相机)") - print("- 3个不同位置的3D虚拟屏幕") - print("如果所有元素都可见,说明修复成功!") - - # 运行应用 - return app.exec_() - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +完整的GUI功能测试 +测试主程序中的所有GUI创建和编辑功能 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +import builtins +from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction, + QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem, + QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QTreeView, QInputDialog, QFileDialog, QMessageBox, QDialog, QGroupBox, QHBoxLayout, QPushButton, QDialogButtonBox) +from PyQt5.QtCore import Qt, QDir, QUrl +from PyQt5.QtGui import QDrag, QPainter, QPixmap +from QMeta3D.QMeta3DWidget import QMeta3DWidget +from QMeta3D.Meta3DWorld import Meta3DWorld +from direct.gui.DirectGui import * +from direct.gui.OnscreenText import OnscreenText +from panda3d.core import * +import os + +# 简化的世界类,专门用于GUI测试 +class GUITestWorld(Meta3DWorld): + def __init__(self): + super().__init__() + + # 设置相机位置 + self.cam.setPos(0, -10, 5) + self.cam.lookAt(0, 0, 0) + + # 设置背景色 + self.setBackgroundColor(0.2, 0.2, 0.3) + + self.gui_elements = [] + + # 尝试加载中文字体 + try: + self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') + if not self.chinese_font: + print("警告: 无法加载中文字体,将使用默认字体") + except: + print("警告: 无法加载中文字体,将使用默认字体") + self.chinese_font = None + + # 延迟创建GUI元素,确保所有系统都已初始化 + self.taskMgr.doMethodLater(0.1, self.createTestGUIElements, "create-gui") + + def createTestGUIElements(self, task=None): + """创建测试用的GUI元素""" + print("\n=== 开始创建测试GUI元素 ===") + + # 测试2D GUI组件 + print("创建2D GUI组件...") + + # 按钮测试 - 不同位置 + self.button1 = self.createGUIButton((0, 0, 0), "中心按钮", 0.08) + self.button2 = self.createGUIButton((-5, 0, 0), "左侧按钮", 0.08) + self.button3 = self.createGUIButton((5, 0, 0), "右侧按钮", 0.08) + self.button4 = self.createGUIButton((0, 0, 5), "上方按钮", 0.08) + self.button5 = self.createGUIButton((0, 0, -5), "下方按钮", 0.08) + + # 标签测试 + self.label1 = self.createGUILabel((0, 0, -8), "测试标签", 0.06) + self.label2 = self.createGUILabel((-8, 0, 0), "左标签", 0.06) + self.label3 = self.createGUILabel((8, 0, 0), "右标签", 0.06) + + # 输入框测试 + self.entry1 = self.createGUIEntry((0, 0, -12), "中心输入框", 0.05) + self.entry2 = self.createGUIEntry((-8, 0, -8), "左输入框", 0.05) + + # 测试3D GUI组件 + print("创建3D GUI组件...") + + # 3D文本测试 + self.text3d1 = self.createGUI3DText((0, 5, 2), "3D测试文本", 0.5) + self.text3d2 = self.createGUI3DText((-3, 3, 1), "左3D文本", 0.3) + self.text3d3 = self.createGUI3DText((3, 3, 1), "右3D文本", 0.3) + + # 虚拟屏幕测试 + self.screen1 = self.createGUIVirtualScreen((0, 8, 0), (3, 2), "主虚拟屏幕") + self.screen2 = self.createGUIVirtualScreen((-5, 5, 2), (2, 1), "左屏幕") + self.screen3 = self.createGUIVirtualScreen((5, 5, 2), (2, 1), "右屏幕") + + print(f"=== GUI元素创建完成,总计: {len(self.gui_elements)} 个 ===\n") + + # 验证每个元素的显示状态 + self.verifyGUIElements() + + # 添加标题 + self.title = OnscreenText( + text="Qt集成GUI测试", + pos=(0, 0.9), + scale=0.08, + fg=(1, 1, 1, 1), + align=TextNode.ACenter, + font=self.chinese_font if self.chinese_font else None + ) + + return task.done if task else None + + def verifyGUIElements(self): + """验证GUI元素的显示状态""" + print("=== 验证GUI元素显示状态 ===") + + for i, element in enumerate(self.gui_elements): + gui_type = element.getTag("gui_type") if hasattr(element, 'getTag') else "unknown" + gui_text = element.getTag("gui_text") if hasattr(element, 'getTag') else "unnamed" + + if hasattr(element, 'getPos'): + pos = element.getPos() + print(f"{i+1}. {gui_type}: '{gui_text}' 位置: {pos}") + else: + print(f"{i+1}. {gui_type}: '{gui_text}' (无位置信息)") + + print("=== 验证完成 ===\n") + + # 复制主程序中的GUI创建方法 + def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1): + """创建2D GUI按钮""" + from direct.gui.DirectGui import DirectButton + + # 将3D坐标转换为2D屏幕坐标 + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + + try: + button = DirectButton( + text=text, + pos=gui_pos, + scale=size, + command=self.onGUIButtonClick, + extraArgs=[f"button_{len(self.gui_elements)}"], + frameColor=(0.2, 0.6, 0.8, 1), + text_font=self.chinese_font if self.chinese_font else None, + rolloverSound=None, + clickSound=None + ) + + button.setTag("gui_type", "button") + button.setTag("gui_id", f"button_{len(self.gui_elements)}") + button.setTag("gui_text", text) + + self.gui_elements.append(button) + print(f"✓ 创建GUI按钮成功: {text} 在位置 {gui_pos}") + print(f" 按钮节点: {button}") + print(f" 按钮父节点: {button.getParent()}") + + # 验证按钮是否真的可见 + if hasattr(button, 'isHidden'): + print(f" 按钮隐藏状态: {button.isHidden()}") + + return button + + except Exception as e: + print(f"✗ 创建GUI按钮失败: {text} - 错误: {str(e)}") + return None + + def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08): + """创建2D GUI标签""" + from direct.gui.DirectGui import DirectLabel + + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + + label = DirectLabel( + text=text, + pos=gui_pos, + scale=size, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + label.setTag("gui_type", "label") + label.setTag("gui_id", f"label_{len(self.gui_elements)}") + label.setTag("gui_text", text) + + self.gui_elements.append(label) + print(f"创建GUI标签: {text} 在位置 {gui_pos}") + return label + + def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08): + """创建2D GUI文本输入框""" + from direct.gui.DirectGui import DirectEntry + + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + + entry = DirectEntry( + text="", + pos=gui_pos, + scale=size, + command=self.onGUIEntrySubmit, + extraArgs=[f"entry_{len(self.gui_elements)}"], + initialText=placeholder, + numLines=1, + width=12, + focus=0 + ) + + entry.setTag("gui_type", "entry") + entry.setTag("gui_id", f"entry_{len(self.gui_elements)}") + entry.setTag("gui_placeholder", placeholder) + + self.gui_elements.append(entry) + print(f"创建GUI输入框: {placeholder} 在位置 {gui_pos}") + return entry + + def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): + """创建3D空间文本""" + from panda3d.core import TextNode + + textNode = TextNode(f'3d-text-{len(self.gui_elements)}') + textNode.setText(text) + textNode.setAlign(TextNode.ACenter) + if self.chinese_font: + textNode.setFont(self.chinese_font) + + textNodePath = self.render.attachNewNode(textNode) + textNodePath.setPos(*pos) + textNodePath.setScale(size) + textNodePath.setColor(1, 1, 0, 1) + textNodePath.setBillboardAxis() + + textNodePath.setTag("gui_type", "3d_text") + textNodePath.setTag("gui_id", f"3d_text_{len(self.gui_elements)}") + textNodePath.setTag("gui_text", text) + textNodePath.setTag("is_gui_element", "1") + + self.gui_elements.append(textNodePath) + print(f"创建3D文本: {text} 在位置 {pos}") + return textNodePath + + def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"): + """创建3D虚拟屏幕""" + from panda3d.core import CardMaker, TransparencyAttrib, TextNode + + cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}") + cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2) + virtualScreen = self.render.attachNewNode(cm.generate()) + virtualScreen.setPos(*pos) + virtualScreen.setColor(0.2, 0.2, 0.2, 0.8) + virtualScreen.setTransparency(TransparencyAttrib.MAlpha) + + screenText = TextNode(f'screen-text-{len(self.gui_elements)}') + screenText.setText(text) + screenText.setAlign(TextNode.ACenter) + if self.chinese_font: + screenText.setFont(self.chinese_font) + screenTextNP = virtualScreen.attachNewNode(screenText) + screenTextNP.setPos(0, 0.01, 0) + screenTextNP.setScale(0.3) + screenTextNP.setColor(0, 1, 0, 1) + + virtualScreen.setTag("gui_type", "virtual_screen") + virtualScreen.setTag("gui_id", f"virtual_screen_{len(self.gui_elements)}") + virtualScreen.setTag("gui_text", text) + virtualScreen.setTag("is_gui_element", "1") + + self.gui_elements.append(virtualScreen) + print(f"创建虚拟屏幕: {text} 在位置 {pos}") + return virtualScreen + + def onGUIButtonClick(self, button_id): + """GUI按钮点击事件处理""" + print(f"✓ GUI按钮点击测试成功: {button_id}") + + def onGUIEntrySubmit(self, text, entry_id): + """GUI输入框提交事件处理""" + print(f"✓ GUI输入框提交测试成功: {entry_id} = {text}") + +class GUITestWidget(QMeta3DWidget): + def __init__(self, world, parent=None): + super().__init__(world, parent) + self.world = world + +def testMainGUIFeatures(): + """测试主程序的GUI功能""" + print("=== 开始测试主程序GUI功能 ===") + + app = QApplication(sys.argv) + + # 创建测试世界 + world = GUITestWorld() + + # 创建主窗口 + mainWindow = QMainWindow() + mainWindow.setWindowTitle("GUI功能完整测试") + mainWindow.setGeometry(100, 100, 1200, 800) + + # 创建Panda3D部件 + pandaWidget = GUITestWidget(world) + mainWindow.setCentralWidget(pandaWidget) + + # 显示窗口 + mainWindow.show() + + print("测试窗口已打开,你应该能看到:") + print("- 5个不同位置的2D按钮(可点击)") + print("- 3个不同位置的2D标签") + print("- 2个不同位置的2D输入框(可输入)") + print("- 3个不同位置的3D文本(面向相机)") + print("- 3个不同位置的3D虚拟屏幕") + print("如果所有元素都可见,说明修复成功!") + + # 运行应用 + return app.exec_() + +if __name__ == "__main__": exit_code = testMainGUIFeatures() \ No newline at end of file diff --git a/demo/test_gui_edit.py b/demo/test_gui_edit.py index 420ca9ac..ab132527 100644 --- a/demo/test_gui_edit.py +++ b/demo/test_gui_edit.py @@ -1,158 +1,158 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -GUI文本编辑功能测试脚本 -""" - -import sys -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -from direct.showbase.ShowBase import ShowBase -from direct.gui.DirectGui import DirectButton, DirectLabel -from panda3d.core import TextNode, WindowProperties - -class GUIEditTest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 设置窗口 - wp = WindowProperties() - wp.setTitle("GUI文本编辑测试") - self.win.requestProperties(wp) - self.setBackgroundColor(0.2, 0.3, 0.5) - - # 创建GUI元素列表 - self.gui_elements = [] - - # 创建测试按钮 - self.button = DirectButton( - text="测试按钮", - pos=(0, 0, 0.3), - scale=0.1, - command=self.testButtonEdit, - frameColor=(0.2, 0.6, 0.8, 1) - ) - self.button.setTag("gui_type", "button") - self.button.setTag("gui_text", "测试按钮") - self.gui_elements.append(self.button) - - # 创建测试标签 - self.label = DirectLabel( - text="测试标签", - pos=(0, 0, 0), - scale=0.08, - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 1, 1) - ) - self.label.setTag("gui_type", "label") - self.label.setTag("gui_text", "测试标签") - self.gui_elements.append(self.label) - - # 创建3D文本 - textNode = TextNode('3d-test-text') - textNode.setText("3D测试文本") - textNode.setAlign(TextNode.ACenter) - self.text3d = self.render.attachNewNode(textNode) - self.text3d.setPos(0, 5, 0) - self.text3d.setScale(0.5) - self.text3d.setColor(1, 1, 0, 1) - self.text3d.setBillboardAxis() - self.text3d.setTag("gui_type", "3d_text") - self.text3d.setTag("gui_text", "3D测试文本") - self.gui_elements.append(self.text3d) - - # 测试计数器 - self.test_count = 0 - - print("GUI编辑测试启动") - print("按任意键开始测试文本编辑功能") - - # 绑定键盘事件 - self.accept("space", self.testGUIEdit) - self.accept("1", self.testButtonTextEdit) - self.accept("2", self.testLabelTextEdit) - self.accept("3", self.test3DTextEdit) - - # 显示说明 - self.instructions = DirectLabel( - text="按键说明:\nSpace - 循环测试所有GUI\n1 - 测试按钮\n2 - 测试标签\n3 - 测试3D文本", - pos=(-0.9, 0, -0.5), - scale=0.05, - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 1, 1), - text_align=TextNode.ALeft - ) - - def editGUIElement(self, gui_element, property_name, value): - """编辑GUI元素属性""" - try: - from panda3d.core import TextNode - - gui_type = gui_element.getTag("gui_type") if hasattr(gui_element, 'getTag') else "unknown" - print(f"编辑 {gui_type}: {property_name} = {value}") - - if property_name == "text": - if gui_type in ["button", "label"]: - gui_element['text'] = value - print(f"成功更新2D GUI文本: {value}") - elif gui_type == "3d_text": - if isinstance(gui_element.node(), TextNode): - gui_element.node().setText(value) - print(f"成功更新3D文本: {value}") - else: - print(f"警告: {gui_type}不是TextNode类型") - - gui_element.setTag("gui_text", value) - return True - - except Exception as e: - print(f"编辑GUI元素失败: {str(e)}") - import traceback - traceback.print_exc() - return False - - def testGUIEdit(self): - """循环测试所有GUI元素""" - self.test_count += 1 - - for i, gui_element in enumerate(self.gui_elements): - gui_type = gui_element.getTag("gui_type") - new_text = f"{gui_type}_测试_{self.test_count}" - print(f"\n测试编辑GUI元素 {i}: {gui_type}") - success = self.editGUIElement(gui_element, "text", new_text) - print(f"结果: {'成功' if success else '失败'}") - - print(f"\n第 {self.test_count} 轮测试完成") - - def testButtonTextEdit(self): - """测试按钮文本编辑""" - new_text = f"按钮_修改_{self.test_count}" - self.test_count += 1 - success = self.editGUIElement(self.button, "text", new_text) - print(f"按钮文本编辑: {'成功' if success else '失败'}") - - def testLabelTextEdit(self): - """测试标签文本编辑""" - new_text = f"标签_修改_{self.test_count}" - self.test_count += 1 - success = self.editGUIElement(self.label, "text", new_text) - print(f"标签文本编辑: {'成功' if success else '失败'}") - - def test3DTextEdit(self): - """测试3D文本编辑""" - new_text = f"3D文本_修改_{self.test_count}" - self.test_count += 1 - success = self.editGUIElement(self.text3d, "text", new_text) - print(f"3D文本编辑: {'成功' if success else '失败'}") - - def testButtonEdit(self): - """按钮点击测试""" - print("测试按钮被点击!") - self.testGUIEdit() - -if __name__ == "__main__": - print("启动GUI文本编辑测试...") - test = GUIEditTest() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +GUI文本编辑功能测试脚本 +""" + +import sys +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +from direct.showbase.ShowBase import ShowBase +from direct.gui.DirectGui import DirectButton, DirectLabel +from panda3d.core import TextNode, WindowProperties + +class GUIEditTest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 设置窗口 + wp = WindowProperties() + wp.setTitle("GUI文本编辑测试") + self.win.requestProperties(wp) + self.setBackgroundColor(0.2, 0.3, 0.5) + + # 创建GUI元素列表 + self.gui_elements = [] + + # 创建测试按钮 + self.button = DirectButton( + text="测试按钮", + pos=(0, 0, 0.3), + scale=0.1, + command=self.testButtonEdit, + frameColor=(0.2, 0.6, 0.8, 1) + ) + self.button.setTag("gui_type", "button") + self.button.setTag("gui_text", "测试按钮") + self.gui_elements.append(self.button) + + # 创建测试标签 + self.label = DirectLabel( + text="测试标签", + pos=(0, 0, 0), + scale=0.08, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1) + ) + self.label.setTag("gui_type", "label") + self.label.setTag("gui_text", "测试标签") + self.gui_elements.append(self.label) + + # 创建3D文本 + textNode = TextNode('3d-test-text') + textNode.setText("3D测试文本") + textNode.setAlign(TextNode.ACenter) + self.text3d = self.render.attachNewNode(textNode) + self.text3d.setPos(0, 5, 0) + self.text3d.setScale(0.5) + self.text3d.setColor(1, 1, 0, 1) + self.text3d.setBillboardAxis() + self.text3d.setTag("gui_type", "3d_text") + self.text3d.setTag("gui_text", "3D测试文本") + self.gui_elements.append(self.text3d) + + # 测试计数器 + self.test_count = 0 + + print("GUI编辑测试启动") + print("按任意键开始测试文本编辑功能") + + # 绑定键盘事件 + self.accept("space", self.testGUIEdit) + self.accept("1", self.testButtonTextEdit) + self.accept("2", self.testLabelTextEdit) + self.accept("3", self.test3DTextEdit) + + # 显示说明 + self.instructions = DirectLabel( + text="按键说明:\nSpace - 循环测试所有GUI\n1 - 测试按钮\n2 - 测试标签\n3 - 测试3D文本", + pos=(-0.9, 0, -0.5), + scale=0.05, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1), + text_align=TextNode.ALeft + ) + + def editGUIElement(self, gui_element, property_name, value): + """编辑GUI元素属性""" + try: + from panda3d.core import TextNode + + gui_type = gui_element.getTag("gui_type") if hasattr(gui_element, 'getTag') else "unknown" + print(f"编辑 {gui_type}: {property_name} = {value}") + + if property_name == "text": + if gui_type in ["button", "label"]: + gui_element['text'] = value + print(f"成功更新2D GUI文本: {value}") + elif gui_type == "3d_text": + if isinstance(gui_element.node(), TextNode): + gui_element.node().setText(value) + print(f"成功更新3D文本: {value}") + else: + print(f"警告: {gui_type}不是TextNode类型") + + gui_element.setTag("gui_text", value) + return True + + except Exception as e: + print(f"编辑GUI元素失败: {str(e)}") + import traceback + traceback.print_exc() + return False + + def testGUIEdit(self): + """循环测试所有GUI元素""" + self.test_count += 1 + + for i, gui_element in enumerate(self.gui_elements): + gui_type = gui_element.getTag("gui_type") + new_text = f"{gui_type}_测试_{self.test_count}" + print(f"\n测试编辑GUI元素 {i}: {gui_type}") + success = self.editGUIElement(gui_element, "text", new_text) + print(f"结果: {'成功' if success else '失败'}") + + print(f"\n第 {self.test_count} 轮测试完成") + + def testButtonTextEdit(self): + """测试按钮文本编辑""" + new_text = f"按钮_修改_{self.test_count}" + self.test_count += 1 + success = self.editGUIElement(self.button, "text", new_text) + print(f"按钮文本编辑: {'成功' if success else '失败'}") + + def testLabelTextEdit(self): + """测试标签文本编辑""" + new_text = f"标签_修改_{self.test_count}" + self.test_count += 1 + success = self.editGUIElement(self.label, "text", new_text) + print(f"标签文本编辑: {'成功' if success else '失败'}") + + def test3DTextEdit(self): + """测试3D文本编辑""" + new_text = f"3D文本_修改_{self.test_count}" + self.test_count += 1 + success = self.editGUIElement(self.text3d, "text", new_text) + print(f"3D文本编辑: {'成功' if success else '失败'}") + + def testButtonEdit(self): + """按钮点击测试""" + print("测试按钮被点击!") + self.testGUIEdit() + +if __name__ == "__main__": + print("启动GUI文本编辑测试...") + test = GUIEditTest() test.run() \ No newline at end of file diff --git a/demo/test_gui_edit_mode.py b/demo/test_gui_edit_mode.py index 9c0ee305..f2e43c96 100644 --- a/demo/test_gui_edit_mode.py +++ b/demo/test_gui_edit_mode.py @@ -1,361 +1,361 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -GUI编辑模式功能测试 -验证主程序中的GUI编辑模式是否正常工作 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -from direct.showbase.ShowBase import ShowBase -from direct.gui.DirectGui import * -from panda3d.core import * - -# 配置Panda3D -loadPrcFileData("", """ - win-size 800 600 - window-title GUI编辑模式测试 - framebuffer-multisample 1 - multisamples 2 -""") - -class GUIEditModeTest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - print("=== GUI编辑模式功能测试 ===") - - # 设置背景色 - self.setBackgroundColor(0.2, 0.2, 0.3) - - # 初始化GUI相关变量 - self.gui_elements = [] - self.guiEditMode = False - self.guiEditPanel = None - self.currentGUITool = None - self.selectedGUI = None - - # 尝试加载中文字体 - try: - self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') - if not self.chinese_font: - print("警告: 无法加载中文字体,将使用默认字体") - except: - print("警告: 无法加载中文字体,将使用默认字体") - self.chinese_font = None - - # 创建测试说明 - self.createInstructions() - - # 自动进入GUI编辑模式进行测试 - self.enterGUIEditMode() - - print("\n=== 测试说明 ===") - print("1. 应该能看到右侧的GUI编辑面板") - print("2. 点击面板中的按钮选择不同工具") - print("3. 点击场景中的位置应该能创建相应的GUI元素") - print("4. 按ESC键退出") - - # 绑定退出键 - self.accept("escape", self.exitTest) - - def createInstructions(self): - """创建说明文本""" - self.title = OnscreenText( - text="GUI编辑模式功能测试", - pos=(0, 0.9), - scale=0.08, - fg=(1, 1, 1, 1), - align=TextNode.ACenter, - font=self.chinese_font if self.chinese_font else None - ) - - self.instruction = OnscreenText( - text="右侧应该显示GUI编辑面板\n点击面板中的工具,然后点击场景创建GUI元素", - pos=(0, -0.8), - scale=0.05, - fg=(1, 1, 0, 1), - align=TextNode.ACenter, - font=self.chinese_font if self.chinese_font else None - ) - - def enterGUIEditMode(self): - """进入GUI编辑模式""" - self.guiEditMode = True - print("\n=== 进入GUI编辑模式 ===") - - # 创建GUI编辑面板 - self.createGUIEditPanel() - - print("GUI编辑模式已激活") - - def createGUIEditPanel(self): - """创建GUI编辑面板""" - # 创建主面板 - self.guiEditPanel = DirectFrame( - pos=(0.85, 0, 0), - frameSize=(-0.15, 0.15, -0.9, 0.9), - frameColor=(0.1, 0.1, 0.1, 0.8), - text="GUI编辑器", - text_pos=(0, 0.85), - text_scale=0.05, - text_fg=(1, 1, 1, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - # 创建工具按钮 - y_pos = 0.7 - spacing = 0.12 - - # 2D GUI工具标签 - label_2d = DirectLabel( - parent=self.guiEditPanel, - text="2D GUI", - pos=(0, 0, y_pos), - scale=0.04, - text_fg=(1, 1, 0, 1), - frameColor=(0, 0, 0, 0), - text_font=self.chinese_font if self.chinese_font else None - ) - y_pos -= 0.08 - - # 按钮工具 - btn_button = DirectButton( - parent=self.guiEditPanel, - text="按钮", - pos=(0, 0, y_pos), - scale=0.04, - command=self.setGUICreateTool, - extraArgs=["button"], - frameColor=(0.2, 0.6, 0.8, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - y_pos -= spacing - - # 标签工具 - btn_label = DirectButton( - parent=self.guiEditPanel, - text="标签", - pos=(0, 0, y_pos), - scale=0.04, - command=self.setGUICreateTool, - extraArgs=["label"], - frameColor=(0.6, 0.8, 0.2, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - y_pos -= spacing - - # 输入框工具 - btn_entry = DirectButton( - parent=self.guiEditPanel, - text="输入框", - pos=(0, 0, y_pos), - scale=0.04, - command=self.setGUICreateTool, - extraArgs=["entry"], - frameColor=(0.8, 0.6, 0.2, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - y_pos -= spacing - - # 3D GUI工具标签 - label_3d = DirectLabel( - parent=self.guiEditPanel, - text="3D GUI", - pos=(0, 0, y_pos), - scale=0.04, - text_fg=(1, 1, 0, 1), - frameColor=(0, 0, 0, 0), - text_font=self.chinese_font if self.chinese_font else None - ) - y_pos -= 0.08 - - # 3D文本工具 - btn_3dtext = DirectButton( - parent=self.guiEditPanel, - text="3D文本", - pos=(0, 0, y_pos), - scale=0.04, - command=self.setGUICreateTool, - extraArgs=["3d_text"], - frameColor=(0.8, 0.2, 0.6, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - y_pos -= spacing - - # 虚拟屏幕工具 - btn_screen = DirectButton( - parent=self.guiEditPanel, - text="虚拟屏幕", - pos=(0, 0, y_pos), - scale=0.04, - command=self.setGUICreateTool, - extraArgs=["virtual_screen"], - frameColor=(0.6, 0.2, 0.8, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - y_pos -= spacing - - # 退出按钮 - btn_exit = DirectButton( - parent=self.guiEditPanel, - text="退出测试", - pos=(0, 0, -0.8), - scale=0.04, - command=self.exitTest, - frameColor=(0.8, 0.2, 0.2, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - print("✓ GUI编辑面板创建成功") - - def setGUICreateTool(self, tool_type): - """设置GUI创建工具""" - self.currentGUITool = tool_type - print(f"✓ 选择GUI创建工具: {tool_type}") - - # 绑定鼠标点击事件 - self.accept("mouse1", self.handleMouseClick) - - def handleMouseClick(self): - """处理鼠标点击""" - if not self.guiEditMode or not self.currentGUITool: - return - - # 获取鼠标位置 - if not base.mouseWatcherNode.hasMouse(): - return - - mouse_x = base.mouseWatcherNode.getMouseX() - mouse_y = base.mouseWatcherNode.getMouseY() - - print(f"鼠标点击位置: ({mouse_x:.2f}, {mouse_y:.2f})") - - # 创建GUI元素 - if self.currentGUITool == "button": - self.createGUIButton((mouse_x * 10, 0, mouse_y * 10), f"按钮{len(self.gui_elements)}") - elif self.currentGUITool == "label": - self.createGUILabel((mouse_x * 10, 0, mouse_y * 10), f"标签{len(self.gui_elements)}") - elif self.currentGUITool == "entry": - self.createGUIEntry((mouse_x * 10, 0, mouse_y * 10), f"输入框{len(self.gui_elements)}") - elif self.currentGUITool == "3d_text": - self.createGUI3DText((mouse_x * 5, 0, mouse_y * 5), f"3D文本{len(self.gui_elements)}") - elif self.currentGUITool == "virtual_screen": - self.createGUIVirtualScreen((mouse_x * 5, 5, mouse_y * 5), text=f"屏幕{len(self.gui_elements)}") - - def createGUIButton(self, pos=(0, 0, 0), text="按钮"): - """创建2D GUI按钮""" - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - - button = DirectButton( - text=text, - pos=gui_pos, - scale=0.08, - frameColor=(0.2, 0.6, 0.8, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - self.gui_elements.append(button) - print(f"✓ 创建GUI按钮: {text} 在位置 {gui_pos}") - return button - - def createGUILabel(self, pos=(0, 0, 0), text="标签"): - """创建2D GUI标签""" - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - - label = DirectLabel( - text=text, - pos=gui_pos, - scale=0.06, - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 1, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - self.gui_elements.append(label) - print(f"✓ 创建GUI标签: {text} 在位置 {gui_pos}") - return label - - def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入框"): - """创建2D GUI输入框""" - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - - entry = DirectEntry( - text="", - pos=gui_pos, - scale=0.05, - initialText=placeholder, - numLines=1, - width=12, - focus=0 - ) - - self.gui_elements.append(entry) - print(f"✓ 创建GUI输入框: {placeholder} 在位置 {gui_pos}") - return entry - - def createGUI3DText(self, pos=(0, 0, 0), text="3D文本"): - """创建3D文本""" - textNode = TextNode(f'3d-text-{len(self.gui_elements)}') - textNode.setText(text) - textNode.setAlign(TextNode.ACenter) - if self.chinese_font: - textNode.setFont(self.chinese_font) - - textNodePath = self.render.attachNewNode(textNode) - textNodePath.setPos(*pos) - textNodePath.setScale(0.5) - textNodePath.setColor(1, 1, 0, 1) - textNodePath.setBillboardAxis() - - self.gui_elements.append(textNodePath) - print(f"✓ 创建3D文本: {text} 在位置 {pos}") - return textNodePath - - def createGUIVirtualScreen(self, pos=(0, 0, 0), text="虚拟屏幕"): - """创建3D虚拟屏幕""" - from panda3d.core import CardMaker, TransparencyAttrib - - cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}") - cm.setFrame(-1, 1, -0.5, 0.5) - virtualScreen = self.render.attachNewNode(cm.generate()) - virtualScreen.setPos(*pos) - virtualScreen.setColor(0.2, 0.2, 0.2, 0.8) - virtualScreen.setTransparency(TransparencyAttrib.MAlpha) - - # 添加文本 - screenText = TextNode(f'screen-text-{len(self.gui_elements)}') - screenText.setText(text) - screenText.setAlign(TextNode.ACenter) - if self.chinese_font: - screenText.setFont(self.chinese_font) - screenTextNP = virtualScreen.attachNewNode(screenText) - screenTextNP.setPos(0, 0.01, 0) - screenTextNP.setScale(0.3) - screenTextNP.setColor(0, 1, 0, 1) - - self.gui_elements.append(virtualScreen) - print(f"✓ 创建虚拟屏幕: {text} 在位置 {pos}") - return virtualScreen - - def exitTest(self): - """退出测试""" - print(f"\n=== 测试完成 ===") - print(f"总共创建了 {len(self.gui_elements)} 个GUI元素") - print("如果能看到GUI编辑面板并成功创建元素,说明功能正常!") - self.userExit() - -def main(): - """主函数""" - print("启动GUI编辑模式功能测试...") - print("这将测试主程序中新增的GUI编辑模式功能") - - # 创建并运行测试 - app = GUIEditModeTest() - app.run() - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +GUI编辑模式功能测试 +验证主程序中的GUI编辑模式是否正常工作 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +from direct.showbase.ShowBase import ShowBase +from direct.gui.DirectGui import * +from panda3d.core import * + +# 配置Panda3D +loadPrcFileData("", """ + win-size 800 600 + window-title GUI编辑模式测试 + framebuffer-multisample 1 + multisamples 2 +""") + +class GUIEditModeTest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + print("=== GUI编辑模式功能测试 ===") + + # 设置背景色 + self.setBackgroundColor(0.2, 0.2, 0.3) + + # 初始化GUI相关变量 + self.gui_elements = [] + self.guiEditMode = False + self.guiEditPanel = None + self.currentGUITool = None + self.selectedGUI = None + + # 尝试加载中文字体 + try: + self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') + if not self.chinese_font: + print("警告: 无法加载中文字体,将使用默认字体") + except: + print("警告: 无法加载中文字体,将使用默认字体") + self.chinese_font = None + + # 创建测试说明 + self.createInstructions() + + # 自动进入GUI编辑模式进行测试 + self.enterGUIEditMode() + + print("\n=== 测试说明 ===") + print("1. 应该能看到右侧的GUI编辑面板") + print("2. 点击面板中的按钮选择不同工具") + print("3. 点击场景中的位置应该能创建相应的GUI元素") + print("4. 按ESC键退出") + + # 绑定退出键 + self.accept("escape", self.exitTest) + + def createInstructions(self): + """创建说明文本""" + self.title = OnscreenText( + text="GUI编辑模式功能测试", + pos=(0, 0.9), + scale=0.08, + fg=(1, 1, 1, 1), + align=TextNode.ACenter, + font=self.chinese_font if self.chinese_font else None + ) + + self.instruction = OnscreenText( + text="右侧应该显示GUI编辑面板\n点击面板中的工具,然后点击场景创建GUI元素", + pos=(0, -0.8), + scale=0.05, + fg=(1, 1, 0, 1), + align=TextNode.ACenter, + font=self.chinese_font if self.chinese_font else None + ) + + def enterGUIEditMode(self): + """进入GUI编辑模式""" + self.guiEditMode = True + print("\n=== 进入GUI编辑模式 ===") + + # 创建GUI编辑面板 + self.createGUIEditPanel() + + print("GUI编辑模式已激活") + + def createGUIEditPanel(self): + """创建GUI编辑面板""" + # 创建主面板 + self.guiEditPanel = DirectFrame( + pos=(0.85, 0, 0), + frameSize=(-0.15, 0.15, -0.9, 0.9), + frameColor=(0.1, 0.1, 0.1, 0.8), + text="GUI编辑器", + text_pos=(0, 0.85), + text_scale=0.05, + text_fg=(1, 1, 1, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + # 创建工具按钮 + y_pos = 0.7 + spacing = 0.12 + + # 2D GUI工具标签 + label_2d = DirectLabel( + parent=self.guiEditPanel, + text="2D GUI", + pos=(0, 0, y_pos), + scale=0.04, + text_fg=(1, 1, 0, 1), + frameColor=(0, 0, 0, 0), + text_font=self.chinese_font if self.chinese_font else None + ) + y_pos -= 0.08 + + # 按钮工具 + btn_button = DirectButton( + parent=self.guiEditPanel, + text="按钮", + pos=(0, 0, y_pos), + scale=0.04, + command=self.setGUICreateTool, + extraArgs=["button"], + frameColor=(0.2, 0.6, 0.8, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + y_pos -= spacing + + # 标签工具 + btn_label = DirectButton( + parent=self.guiEditPanel, + text="标签", + pos=(0, 0, y_pos), + scale=0.04, + command=self.setGUICreateTool, + extraArgs=["label"], + frameColor=(0.6, 0.8, 0.2, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + y_pos -= spacing + + # 输入框工具 + btn_entry = DirectButton( + parent=self.guiEditPanel, + text="输入框", + pos=(0, 0, y_pos), + scale=0.04, + command=self.setGUICreateTool, + extraArgs=["entry"], + frameColor=(0.8, 0.6, 0.2, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + y_pos -= spacing + + # 3D GUI工具标签 + label_3d = DirectLabel( + parent=self.guiEditPanel, + text="3D GUI", + pos=(0, 0, y_pos), + scale=0.04, + text_fg=(1, 1, 0, 1), + frameColor=(0, 0, 0, 0), + text_font=self.chinese_font if self.chinese_font else None + ) + y_pos -= 0.08 + + # 3D文本工具 + btn_3dtext = DirectButton( + parent=self.guiEditPanel, + text="3D文本", + pos=(0, 0, y_pos), + scale=0.04, + command=self.setGUICreateTool, + extraArgs=["3d_text"], + frameColor=(0.8, 0.2, 0.6, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + y_pos -= spacing + + # 虚拟屏幕工具 + btn_screen = DirectButton( + parent=self.guiEditPanel, + text="虚拟屏幕", + pos=(0, 0, y_pos), + scale=0.04, + command=self.setGUICreateTool, + extraArgs=["virtual_screen"], + frameColor=(0.6, 0.2, 0.8, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + y_pos -= spacing + + # 退出按钮 + btn_exit = DirectButton( + parent=self.guiEditPanel, + text="退出测试", + pos=(0, 0, -0.8), + scale=0.04, + command=self.exitTest, + frameColor=(0.8, 0.2, 0.2, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + print("✓ GUI编辑面板创建成功") + + def setGUICreateTool(self, tool_type): + """设置GUI创建工具""" + self.currentGUITool = tool_type + print(f"✓ 选择GUI创建工具: {tool_type}") + + # 绑定鼠标点击事件 + self.accept("mouse1", self.handleMouseClick) + + def handleMouseClick(self): + """处理鼠标点击""" + if not self.guiEditMode or not self.currentGUITool: + return + + # 获取鼠标位置 + if not base.mouseWatcherNode.hasMouse(): + return + + mouse_x = base.mouseWatcherNode.getMouseX() + mouse_y = base.mouseWatcherNode.getMouseY() + + print(f"鼠标点击位置: ({mouse_x:.2f}, {mouse_y:.2f})") + + # 创建GUI元素 + if self.currentGUITool == "button": + self.createGUIButton((mouse_x * 10, 0, mouse_y * 10), f"按钮{len(self.gui_elements)}") + elif self.currentGUITool == "label": + self.createGUILabel((mouse_x * 10, 0, mouse_y * 10), f"标签{len(self.gui_elements)}") + elif self.currentGUITool == "entry": + self.createGUIEntry((mouse_x * 10, 0, mouse_y * 10), f"输入框{len(self.gui_elements)}") + elif self.currentGUITool == "3d_text": + self.createGUI3DText((mouse_x * 5, 0, mouse_y * 5), f"3D文本{len(self.gui_elements)}") + elif self.currentGUITool == "virtual_screen": + self.createGUIVirtualScreen((mouse_x * 5, 5, mouse_y * 5), text=f"屏幕{len(self.gui_elements)}") + + def createGUIButton(self, pos=(0, 0, 0), text="按钮"): + """创建2D GUI按钮""" + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + + button = DirectButton( + text=text, + pos=gui_pos, + scale=0.08, + frameColor=(0.2, 0.6, 0.8, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + self.gui_elements.append(button) + print(f"✓ 创建GUI按钮: {text} 在位置 {gui_pos}") + return button + + def createGUILabel(self, pos=(0, 0, 0), text="标签"): + """创建2D GUI标签""" + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + + label = DirectLabel( + text=text, + pos=gui_pos, + scale=0.06, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + self.gui_elements.append(label) + print(f"✓ 创建GUI标签: {text} 在位置 {gui_pos}") + return label + + def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入框"): + """创建2D GUI输入框""" + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + + entry = DirectEntry( + text="", + pos=gui_pos, + scale=0.05, + initialText=placeholder, + numLines=1, + width=12, + focus=0 + ) + + self.gui_elements.append(entry) + print(f"✓ 创建GUI输入框: {placeholder} 在位置 {gui_pos}") + return entry + + def createGUI3DText(self, pos=(0, 0, 0), text="3D文本"): + """创建3D文本""" + textNode = TextNode(f'3d-text-{len(self.gui_elements)}') + textNode.setText(text) + textNode.setAlign(TextNode.ACenter) + if self.chinese_font: + textNode.setFont(self.chinese_font) + + textNodePath = self.render.attachNewNode(textNode) + textNodePath.setPos(*pos) + textNodePath.setScale(0.5) + textNodePath.setColor(1, 1, 0, 1) + textNodePath.setBillboardAxis() + + self.gui_elements.append(textNodePath) + print(f"✓ 创建3D文本: {text} 在位置 {pos}") + return textNodePath + + def createGUIVirtualScreen(self, pos=(0, 0, 0), text="虚拟屏幕"): + """创建3D虚拟屏幕""" + from panda3d.core import CardMaker, TransparencyAttrib + + cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}") + cm.setFrame(-1, 1, -0.5, 0.5) + virtualScreen = self.render.attachNewNode(cm.generate()) + virtualScreen.setPos(*pos) + virtualScreen.setColor(0.2, 0.2, 0.2, 0.8) + virtualScreen.setTransparency(TransparencyAttrib.MAlpha) + + # 添加文本 + screenText = TextNode(f'screen-text-{len(self.gui_elements)}') + screenText.setText(text) + screenText.setAlign(TextNode.ACenter) + if self.chinese_font: + screenText.setFont(self.chinese_font) + screenTextNP = virtualScreen.attachNewNode(screenText) + screenTextNP.setPos(0, 0.01, 0) + screenTextNP.setScale(0.3) + screenTextNP.setColor(0, 1, 0, 1) + + self.gui_elements.append(virtualScreen) + print(f"✓ 创建虚拟屏幕: {text} 在位置 {pos}") + return virtualScreen + + def exitTest(self): + """退出测试""" + print(f"\n=== 测试完成 ===") + print(f"总共创建了 {len(self.gui_elements)} 个GUI元素") + print("如果能看到GUI编辑面板并成功创建元素,说明功能正常!") + self.userExit() + +def main(): + """主函数""" + print("启动GUI编辑模式功能测试...") + print("这将测试主程序中新增的GUI编辑模式功能") + + # 创建并运行测试 + app = GUIEditModeTest() + app.run() + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/demo/test_gui_editor_standalone.py b/demo/test_gui_editor_standalone.py index 0e673075..5282f214 100644 --- a/demo/test_gui_editor_standalone.py +++ b/demo/test_gui_editor_standalone.py @@ -1,237 +1,237 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -独立GUI编辑器测试脚本 -测试GUI编辑器窗口的功能 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget -from PyQt5.QtCore import Qt - -# 模拟world对象 -class MockWorld: - def __init__(self): - self.gui_elements = [] # 存储GUI元素 - - def createGUIButton(self, pos, text, scale): - """模拟创建GUI按钮""" - print(f"模拟创建按钮: {text} at {pos} scale {scale}") - button = MockGUIElement("button", text, pos, scale) - self.gui_elements.append(button) - return button - - def createGUILabel(self, pos, text, scale): - """模拟创建GUI标签""" - print(f"模拟创建标签: {text} at {pos} scale {scale}") - label = MockGUIElement("label", text, pos, scale) - self.gui_elements.append(label) - return label - - def createGUIEntry(self, pos, text, scale): - """模拟创建GUI输入框""" - print(f"模拟创建输入框: {text} at {pos} scale {scale}") - entry = MockGUIElement("entry", text, pos, scale) - self.gui_elements.append(entry) - return entry - - def createGUISlider(self, pos, text, scale): - """模拟创建GUI滑块""" - print(f"模拟创建滑块: {text} at {pos} scale {scale}") - slider = MockGUIElement("slider", text, pos, scale) - self.gui_elements.append(slider) - return slider - - def createGUI3DText(self, pos, text, scale): - """模拟创建3D文本""" - print(f"模拟创建3D文本: {text} at {pos} scale {scale}") - text3d = MockGUIElement("3d_text", text, pos, scale) - self.gui_elements.append(text3d) - return text3d - - def createGUIVirtualScreen(self, pos, size, text): - """模拟创建虚拟屏幕""" - print(f"模拟创建虚拟屏幕: {text} at {pos} size {size}") - screen = MockGUIElement("virtual_screen", text, pos, 1.0, size=size) - self.gui_elements.append(screen) - return screen - - def deleteGUIElement(self, element): - """模拟删除GUI元素""" - if element in self.gui_elements: - self.gui_elements.remove(element) - print(f"模拟删除GUI元素: {element.text}") - return True - return False - - def editGUIElement(self, element, property_name, value): - """模拟编辑GUI元素""" - if property_name == "text": - element.text = value - print(f"模拟编辑GUI元素文本: {element.text} -> {value}") - elif property_name == "position": - element.pos = value - print(f"模拟编辑GUI元素位置: {element.pos} -> {value}") - elif property_name == "scale": - element.scale = value - print(f"模拟编辑GUI元素缩放: {element.scale} -> {value}") - return True - -class MockGUIElement: - """模拟GUI元素""" - def __init__(self, gui_type, text, pos, scale, size=None): - self.gui_type = gui_type - self.text = text - self.pos = pos - self.scale = scale - self.size = size or (1, 1) - - def getTag(self, tag_name): - """模拟获取标签""" - if tag_name == "gui_type": - return self.gui_type - elif tag_name == "gui_text": - return self.text - return "" - - def setTag(self, tag_name, value): - """模拟设置标签""" - if tag_name == "gui_text": - self.text = value - - def getPos(self): - """模拟获取位置""" - return MockVector3(self.pos) - - def setPos(self, *args): - """模拟设置位置""" - if len(args) == 1: - self.pos = (args[0].getX(), args[0].getY(), args[0].getZ()) - else: - self.pos = args[:3] - - def getScale(self): - """模拟获取缩放""" - return MockVector3((self.scale, self.scale, self.scale)) - - def setScale(self, *args): - """模拟设置缩放""" - if len(args) == 1: - if hasattr(args[0], 'getX'): - self.scale = args[0].getX() - else: - self.scale = args[0] - else: - self.scale = args[0] - -class MockVector3: - """模拟三维向量""" - def __init__(self, values): - if isinstance(values, (list, tuple)): - self.x, self.y, self.z = values[:3] - else: - self.x = self.y = self.z = values - - def getX(self): - return self.x - - def getY(self): - return self.y - - def getZ(self): - return self.z - -class TestMainWindow(QMainWindow): - """测试主窗口""" - def __init__(self): - super().__init__() - self.world = MockWorld() - self.gui_editor_window = None - - self.setupUI() - - def setupUI(self): - """设置UI""" - self.setWindowTitle("GUI编辑器测试") - self.setGeometry(100, 100, 400, 200) - - central_widget = QWidget() - self.setCentralWidget(central_widget) - - layout = QVBoxLayout(central_widget) - - # 打开GUI编辑器按钮 - open_button = QPushButton("打开GUI编辑器") - open_button.clicked.connect(self.openGUIEditor) - layout.addWidget(open_button) - - # 添加一些测试GUI元素按钮 - add_button = QPushButton("添加测试GUI元素") - add_button.clicked.connect(self.addTestGUIElements) - layout.addWidget(add_button) - - # 显示状态 - self.status_label = QPushButton("状态: 准备就绪") - self.status_label.setEnabled(False) - layout.addWidget(self.status_label) - - def openGUIEditor(self): - """打开GUI编辑器""" - try: - from gui_editor_window import GUIEditorWindow - - if self.gui_editor_window is None: - self.gui_editor_window = GUIEditorWindow(world=self.world) - self.gui_editor_window.editor_closed.connect(self.onGUIEditorClosed) - self.gui_editor_window.show() - self.status_label.setText("状态: GUI编辑器已打开") - print("GUI编辑器窗口已打开") - else: - self.gui_editor_window.raise_() - self.gui_editor_window.activateWindow() - - except ImportError as e: - print(f"无法导入GUI编辑器: {e}") - self.status_label.setText("状态: 导入GUI编辑器失败") - except Exception as e: - print(f"打开GUI编辑器失败: {e}") - self.status_label.setText(f"状态: 打开失败 - {e}") - - def onGUIEditorClosed(self): - """GUI编辑器关闭回调""" - self.gui_editor_window = None - self.status_label.setText("状态: GUI编辑器已关闭") - print("GUI编辑器窗口已关闭") - - def addTestGUIElements(self): - """添加测试GUI元素""" - # 添加一些测试元素 - self.world.createGUIButton((0, 0, 0), "测试按钮", 0.08) - self.world.createGUILabel((3, 0, 0), "测试标签", 0.06) - self.world.createGUI3DText((0, 5, 0), "测试3D文本", 0.5) - - # 如果GUI编辑器窗口打开,同步元素 - if self.gui_editor_window: - self.gui_editor_window.syncGUIElements() - - self.status_label.setText(f"状态: 已添加测试元素,总计 {len(self.world.gui_elements)} 个") - -def main(): - """主函数""" - app = QApplication(sys.argv) - - # 创建测试窗口 - main_window = TestMainWindow() - main_window.show() - - print("GUI编辑器测试启动") - print("点击'打开GUI编辑器'按钮来测试独立的GUI编辑器窗口") - - sys.exit(app.exec_()) - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +独立GUI编辑器测试脚本 +测试GUI编辑器窗口的功能 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QVBoxLayout, QWidget +from PyQt5.QtCore import Qt + +# 模拟world对象 +class MockWorld: + def __init__(self): + self.gui_elements = [] # 存储GUI元素 + + def createGUIButton(self, pos, text, scale): + """模拟创建GUI按钮""" + print(f"模拟创建按钮: {text} at {pos} scale {scale}") + button = MockGUIElement("button", text, pos, scale) + self.gui_elements.append(button) + return button + + def createGUILabel(self, pos, text, scale): + """模拟创建GUI标签""" + print(f"模拟创建标签: {text} at {pos} scale {scale}") + label = MockGUIElement("label", text, pos, scale) + self.gui_elements.append(label) + return label + + def createGUIEntry(self, pos, text, scale): + """模拟创建GUI输入框""" + print(f"模拟创建输入框: {text} at {pos} scale {scale}") + entry = MockGUIElement("entry", text, pos, scale) + self.gui_elements.append(entry) + return entry + + def createGUISlider(self, pos, text, scale): + """模拟创建GUI滑块""" + print(f"模拟创建滑块: {text} at {pos} scale {scale}") + slider = MockGUIElement("slider", text, pos, scale) + self.gui_elements.append(slider) + return slider + + def createGUI3DText(self, pos, text, scale): + """模拟创建3D文本""" + print(f"模拟创建3D文本: {text} at {pos} scale {scale}") + text3d = MockGUIElement("3d_text", text, pos, scale) + self.gui_elements.append(text3d) + return text3d + + def createGUIVirtualScreen(self, pos, size, text): + """模拟创建虚拟屏幕""" + print(f"模拟创建虚拟屏幕: {text} at {pos} size {size}") + screen = MockGUIElement("virtual_screen", text, pos, 1.0, size=size) + self.gui_elements.append(screen) + return screen + + def deleteGUIElement(self, element): + """模拟删除GUI元素""" + if element in self.gui_elements: + self.gui_elements.remove(element) + print(f"模拟删除GUI元素: {element.text}") + return True + return False + + def editGUIElement(self, element, property_name, value): + """模拟编辑GUI元素""" + if property_name == "text": + element.text = value + print(f"模拟编辑GUI元素文本: {element.text} -> {value}") + elif property_name == "position": + element.pos = value + print(f"模拟编辑GUI元素位置: {element.pos} -> {value}") + elif property_name == "scale": + element.scale = value + print(f"模拟编辑GUI元素缩放: {element.scale} -> {value}") + return True + +class MockGUIElement: + """模拟GUI元素""" + def __init__(self, gui_type, text, pos, scale, size=None): + self.gui_type = gui_type + self.text = text + self.pos = pos + self.scale = scale + self.size = size or (1, 1) + + def getTag(self, tag_name): + """模拟获取标签""" + if tag_name == "gui_type": + return self.gui_type + elif tag_name == "gui_text": + return self.text + return "" + + def setTag(self, tag_name, value): + """模拟设置标签""" + if tag_name == "gui_text": + self.text = value + + def getPos(self): + """模拟获取位置""" + return MockVector3(self.pos) + + def setPos(self, *args): + """模拟设置位置""" + if len(args) == 1: + self.pos = (args[0].getX(), args[0].getY(), args[0].getZ()) + else: + self.pos = args[:3] + + def getScale(self): + """模拟获取缩放""" + return MockVector3((self.scale, self.scale, self.scale)) + + def setScale(self, *args): + """模拟设置缩放""" + if len(args) == 1: + if hasattr(args[0], 'getX'): + self.scale = args[0].getX() + else: + self.scale = args[0] + else: + self.scale = args[0] + +class MockVector3: + """模拟三维向量""" + def __init__(self, values): + if isinstance(values, (list, tuple)): + self.x, self.y, self.z = values[:3] + else: + self.x = self.y = self.z = values + + def getX(self): + return self.x + + def getY(self): + return self.y + + def getZ(self): + return self.z + +class TestMainWindow(QMainWindow): + """测试主窗口""" + def __init__(self): + super().__init__() + self.world = MockWorld() + self.gui_editor_window = None + + self.setupUI() + + def setupUI(self): + """设置UI""" + self.setWindowTitle("GUI编辑器测试") + self.setGeometry(100, 100, 400, 200) + + central_widget = QWidget() + self.setCentralWidget(central_widget) + + layout = QVBoxLayout(central_widget) + + # 打开GUI编辑器按钮 + open_button = QPushButton("打开GUI编辑器") + open_button.clicked.connect(self.openGUIEditor) + layout.addWidget(open_button) + + # 添加一些测试GUI元素按钮 + add_button = QPushButton("添加测试GUI元素") + add_button.clicked.connect(self.addTestGUIElements) + layout.addWidget(add_button) + + # 显示状态 + self.status_label = QPushButton("状态: 准备就绪") + self.status_label.setEnabled(False) + layout.addWidget(self.status_label) + + def openGUIEditor(self): + """打开GUI编辑器""" + try: + from gui_editor_window import GUIEditorWindow + + if self.gui_editor_window is None: + self.gui_editor_window = GUIEditorWindow(world=self.world) + self.gui_editor_window.editor_closed.connect(self.onGUIEditorClosed) + self.gui_editor_window.show() + self.status_label.setText("状态: GUI编辑器已打开") + print("GUI编辑器窗口已打开") + else: + self.gui_editor_window.raise_() + self.gui_editor_window.activateWindow() + + except ImportError as e: + print(f"无法导入GUI编辑器: {e}") + self.status_label.setText("状态: 导入GUI编辑器失败") + except Exception as e: + print(f"打开GUI编辑器失败: {e}") + self.status_label.setText(f"状态: 打开失败 - {e}") + + def onGUIEditorClosed(self): + """GUI编辑器关闭回调""" + self.gui_editor_window = None + self.status_label.setText("状态: GUI编辑器已关闭") + print("GUI编辑器窗口已关闭") + + def addTestGUIElements(self): + """添加测试GUI元素""" + # 添加一些测试元素 + self.world.createGUIButton((0, 0, 0), "测试按钮", 0.08) + self.world.createGUILabel((3, 0, 0), "测试标签", 0.06) + self.world.createGUI3DText((0, 5, 0), "测试3D文本", 0.5) + + # 如果GUI编辑器窗口打开,同步元素 + if self.gui_editor_window: + self.gui_editor_window.syncGUIElements() + + self.status_label.setText(f"状态: 已添加测试元素,总计 {len(self.world.gui_elements)} 个") + +def main(): + """主函数""" + app = QApplication(sys.argv) + + # 创建测试窗口 + main_window = TestMainWindow() + main_window.show() + + print("GUI编辑器测试启动") + print("点击'打开GUI编辑器'按钮来测试独立的GUI编辑器窗口") + + sys.exit(app.exec_()) + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/demo/test_gui_new_approach.py b/demo/test_gui_new_approach.py index afa892a3..8ea22a67 100644 --- a/demo/test_gui_new_approach.py +++ b/demo/test_gui_new_approach.py @@ -1,285 +1,285 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -新方法GUI测试 -测试按照gui_3d_demo.py的成功方法修改后的GUI功能 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -from direct.showbase.ShowBase import ShowBase -from direct.gui.DirectGui import * -from direct.gui.OnscreenText import OnscreenText -from panda3d.core import * - -# 配置Panda3D -loadPrcFileData("", """ - win-size 1024 768 - window-title New Approach GUI Test - framebuffer-multisample 1 - multisamples 2 -""") - -class NewApproachGUITest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - print("=== 新方法GUI测试 ===") - - # 设置背景色 - self.setBackgroundColor(0.2, 0.2, 0.3) - - # 调整相机位置 - self.cam.setPos(0, -10, 5) - self.cam.lookAt(0, 0, 0) - - # 初始化GUI元素列表 - self.gui_elements = [] - - # 尝试加载中文字体 - try: - self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') - if not self.chinese_font: - print("警告: 无法加载中文字体,将使用默认字体") - except: - print("警告: 无法加载中文字体,将使用默认字体") - self.chinese_font = None - - # 创建测试内容 - self.createTestContent() - - # 创建GUI元素 - self.createGUIElements() - - print("\n测试说明:") - print("这是使用新方法的GUI测试") - print("参考gui_3d_demo.py的成功实现") - print("如果GUI元素正常显示,说明修复是成功的!") - - def createTestContent(self): - """创建测试内容""" - print("\n--- 创建3D测试内容 ---") - - # 创建地面 - cm = CardMaker("ground") - cm.setFrame(-10, 10, -10, 10) - ground = self.render.attachNewNode(cm.generate()) - ground.setP(-90) - ground.setColor(0.5, 0.5, 0.5, 1) - - # 创建一个立方体 - cube = self.loader.loadModel("models/environment") - if not cube: - # 如果找不到内置模型,创建一个简单几何体 - cm = CardMaker("test-cube") - cm.setFrame(-1, 1, -1, 1) - cube = self.render.attachNewNode(cm.generate()) - - cube.reparentTo(self.render) - cube.setPos(0, 5, 1) - cube.setColor(0.8, 0.2, 0.2, 1) - - # 添加光照 - alight = AmbientLight('alight') - alight.setColor((0.3, 0.3, 0.3, 1)) - alnp = self.render.attachNewNode(alight) - self.render.setLight(alnp) - - dlight = DirectionalLight('dlight') - dlight.setColor((0.8, 0.8, 0.8, 1)) - dlnp = self.render.attachNewNode(dlight) - dlnp.setHpr(45, -45, 0) - self.render.setLight(dlnp) - - print("3D内容创建完成") - - def createGUIElements(self): - """创建GUI元素""" - print("\n--- 使用新方法创建GUI元素 ---") - - # 创建标题 - self.title = OnscreenText( - text="新方法GUI功能测试", - pos=(0, 0.9), - scale=0.08, - fg=(1, 1, 1, 1), - align=TextNode.ACenter, - font=self.chinese_font if self.chinese_font else None - ) - - # 使用新的GUI创建方法 - print("开始创建各种GUI元素...") - - # 按钮测试 - 使用新的逻辑坐标 - self.button1 = self.createGUIButton((0, 0, 0), "中心按钮", 0.08) - self.button2 = self.createGUIButton((-10, 0, 0), "左按钮", 0.08) - self.button3 = self.createGUIButton((10, 0, 0), "右按钮", 0.08) - self.button4 = self.createGUIButton((0, 0, 8), "上按钮", 0.08) - self.button5 = self.createGUIButton((0, 0, -8), "下按钮", 0.08) - - # 标签测试 - self.label1 = self.createGUILabel((0, 0, -12), "中心标签", 0.06) - self.label2 = self.createGUILabel((-15, 0, -5), "左标签", 0.06) - self.label3 = self.createGUILabel((15, 0, -5), "右标签", 0.06) - - # 输入框测试 - self.entry1 = self.createGUIEntry((0, 0, -18), "中心输入框", 0.05) - self.entry2 = self.createGUIEntry((-15, 0, -12), "左输入框", 0.05) - self.entry3 = self.createGUIEntry((15, 0, -12), "右输入框", 0.05) - - # 3D文本测试 - self.text3d1 = self.createGUI3DText((0, 5, 3), "主3D文本", 0.5) - self.text3d2 = self.createGUI3DText((-3, 3, 1), "左3D文本", 0.3) - self.text3d3 = self.createGUI3DText((3, 3, 1), "右3D文本", 0.3) - - # 虚拟屏幕测试 - self.screen1 = self.createGUIVirtualScreen((0, 8, 0), (3, 2), "主虚拟屏幕") - self.screen2 = self.createGUIVirtualScreen((-5, 5, 2), (2, 1), "左屏幕") - self.screen3 = self.createGUIVirtualScreen((5, 5, 2), (2, 1), "右屏幕") - - print(f"\nGUI创建完成!总计: {len(self.gui_elements)} 个元素") - print("检查窗口中是否能看到所有GUI元素...") - - # 使用修改后的GUI创建方法 - def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1): - """创建2D GUI按钮 - 新方法""" - # 直接使用合理的屏幕坐标,参考gui_3d_demo.py的成功实现 - if pos == (0, 0, 0): # 默认中心位置 - gui_pos = (0, 0, 0) - else: - # 简单映射:将输入坐标范围映射到合理的屏幕坐标 - scale_factor = 0.075 # 调整这个值来控制GUI分布范围 - gui_pos = (pos[0] * scale_factor, 0, pos[2] * scale_factor) - - button = DirectButton( - text=text, - pos=gui_pos, - scale=size, - command=self.onGUIButtonClick, - extraArgs=[f"button_{len(self.gui_elements)}"], - frameColor=(0.2, 0.6, 0.8, 1), - text_font=self.chinese_font if self.chinese_font else None, - rolloverSound=None, - clickSound=None - ) - - self.gui_elements.append(button) - print(f"✓ 创建GUI按钮: {text}") - print(f" 逻辑位置: {pos}") - print(f" 屏幕位置: {gui_pos}") - return button - - def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08): - """创建2D GUI标签 - 新方法""" - # 使用与按钮相同的坐标映射逻辑 - if pos == (0, 0, 0): - gui_pos = (0, 0, -0.3) # 标签默认稍微偏下 - else: - scale_factor = 0.075 - gui_pos = (pos[0] * scale_factor, 0, pos[2] * scale_factor) - - label = DirectLabel( - text=text, - pos=gui_pos, - scale=size, - frameColor=(0, 0, 0, 0), # 透明背景 - text_fg=(1, 1, 1, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - self.gui_elements.append(label) - print(f"✓ 创建GUI标签: {text}") - print(f" 逻辑位置: {pos}") - print(f" 屏幕位置: {gui_pos}") - return label - - def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08): - """创建2D GUI文本输入框 - 新方法""" - # 使用与按钮相同的坐标映射逻辑 - if pos == (0, 0, 0): - gui_pos = (0, 0, -0.6) # 输入框默认更靠下 - else: - scale_factor = 0.075 - gui_pos = (pos[0] * scale_factor, 0, pos[2] * scale_factor) - - entry = DirectEntry( - text="", - pos=gui_pos, - scale=size, - command=self.onGUIEntrySubmit, - extraArgs=[f"entry_{len(self.gui_elements)}"], - initialText=placeholder, - numLines=1, - width=12, - focus=0 - ) - - self.gui_elements.append(entry) - print(f"✓ 创建GUI输入框: {placeholder}") - print(f" 逻辑位置: {pos}") - print(f" 屏幕位置: {gui_pos}") - return entry - - def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): - """创建3D空间文本""" - textNode = TextNode(f'3d-text-{len(self.gui_elements)}') - textNode.setText(text) - textNode.setAlign(TextNode.ACenter) - if self.chinese_font: - textNode.setFont(self.chinese_font) - - textNodePath = self.render.attachNewNode(textNode) - textNodePath.setPos(*pos) - textNodePath.setScale(size) - textNodePath.setColor(1, 1, 0, 1) - textNodePath.setBillboardAxis() - - self.gui_elements.append(textNodePath) - print(f"✓ 创建3D文本: {text} (世界位置: {pos})") - return textNodePath - - def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"): - """创建3D虚拟屏幕""" - cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}") - cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2) - virtualScreen = self.render.attachNewNode(cm.generate()) - virtualScreen.setPos(*pos) - virtualScreen.setColor(0.2, 0.2, 0.2, 0.8) - virtualScreen.setTransparency(TransparencyAttrib.MAlpha) - - screenText = TextNode(f'screen-text-{len(self.gui_elements)}') - screenText.setText(text) - screenText.setAlign(TextNode.ACenter) - if self.chinese_font: - screenText.setFont(self.chinese_font) - screenTextNP = virtualScreen.attachNewNode(screenText) - screenTextNP.setPos(0, 0.01, 0) - screenTextNP.setScale(0.3) - screenTextNP.setColor(0, 1, 0, 1) - - self.gui_elements.append(virtualScreen) - print(f"✓ 创建虚拟屏幕: {text} (世界位置: {pos})") - return virtualScreen - - def onGUIButtonClick(self, button_id): - """GUI按钮点击事件处理""" - print(f"🎯 GUI按钮被点击: {button_id}") - - def onGUIEntrySubmit(self, text, entry_id): - """GUI输入框提交事件处理""" - print(f"📝 GUI输入框提交: {entry_id} = '{text}'") - -def main(): - """主函数""" - print("启动新方法GUI功能测试...") - print("这将测试使用gui_3d_demo.py成功方法修改后的GUI创建") - - # 创建并运行应用 - app = NewApproachGUITest() - app.run() - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +新方法GUI测试 +测试按照gui_3d_demo.py的成功方法修改后的GUI功能 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +from direct.showbase.ShowBase import ShowBase +from direct.gui.DirectGui import * +from direct.gui.OnscreenText import OnscreenText +from panda3d.core import * + +# 配置Panda3D +loadPrcFileData("", """ + win-size 1024 768 + window-title New Approach GUI Test + framebuffer-multisample 1 + multisamples 2 +""") + +class NewApproachGUITest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + print("=== 新方法GUI测试 ===") + + # 设置背景色 + self.setBackgroundColor(0.2, 0.2, 0.3) + + # 调整相机位置 + self.cam.setPos(0, -10, 5) + self.cam.lookAt(0, 0, 0) + + # 初始化GUI元素列表 + self.gui_elements = [] + + # 尝试加载中文字体 + try: + self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') + if not self.chinese_font: + print("警告: 无法加载中文字体,将使用默认字体") + except: + print("警告: 无法加载中文字体,将使用默认字体") + self.chinese_font = None + + # 创建测试内容 + self.createTestContent() + + # 创建GUI元素 + self.createGUIElements() + + print("\n测试说明:") + print("这是使用新方法的GUI测试") + print("参考gui_3d_demo.py的成功实现") + print("如果GUI元素正常显示,说明修复是成功的!") + + def createTestContent(self): + """创建测试内容""" + print("\n--- 创建3D测试内容 ---") + + # 创建地面 + cm = CardMaker("ground") + cm.setFrame(-10, 10, -10, 10) + ground = self.render.attachNewNode(cm.generate()) + ground.setP(-90) + ground.setColor(0.5, 0.5, 0.5, 1) + + # 创建一个立方体 + cube = self.loader.loadModel("models/environment") + if not cube: + # 如果找不到内置模型,创建一个简单几何体 + cm = CardMaker("test-cube") + cm.setFrame(-1, 1, -1, 1) + cube = self.render.attachNewNode(cm.generate()) + + cube.reparentTo(self.render) + cube.setPos(0, 5, 1) + cube.setColor(0.8, 0.2, 0.2, 1) + + # 添加光照 + alight = AmbientLight('alight') + alight.setColor((0.3, 0.3, 0.3, 1)) + alnp = self.render.attachNewNode(alight) + self.render.setLight(alnp) + + dlight = DirectionalLight('dlight') + dlight.setColor((0.8, 0.8, 0.8, 1)) + dlnp = self.render.attachNewNode(dlight) + dlnp.setHpr(45, -45, 0) + self.render.setLight(dlnp) + + print("3D内容创建完成") + + def createGUIElements(self): + """创建GUI元素""" + print("\n--- 使用新方法创建GUI元素 ---") + + # 创建标题 + self.title = OnscreenText( + text="新方法GUI功能测试", + pos=(0, 0.9), + scale=0.08, + fg=(1, 1, 1, 1), + align=TextNode.ACenter, + font=self.chinese_font if self.chinese_font else None + ) + + # 使用新的GUI创建方法 + print("开始创建各种GUI元素...") + + # 按钮测试 - 使用新的逻辑坐标 + self.button1 = self.createGUIButton((0, 0, 0), "中心按钮", 0.08) + self.button2 = self.createGUIButton((-10, 0, 0), "左按钮", 0.08) + self.button3 = self.createGUIButton((10, 0, 0), "右按钮", 0.08) + self.button4 = self.createGUIButton((0, 0, 8), "上按钮", 0.08) + self.button5 = self.createGUIButton((0, 0, -8), "下按钮", 0.08) + + # 标签测试 + self.label1 = self.createGUILabel((0, 0, -12), "中心标签", 0.06) + self.label2 = self.createGUILabel((-15, 0, -5), "左标签", 0.06) + self.label3 = self.createGUILabel((15, 0, -5), "右标签", 0.06) + + # 输入框测试 + self.entry1 = self.createGUIEntry((0, 0, -18), "中心输入框", 0.05) + self.entry2 = self.createGUIEntry((-15, 0, -12), "左输入框", 0.05) + self.entry3 = self.createGUIEntry((15, 0, -12), "右输入框", 0.05) + + # 3D文本测试 + self.text3d1 = self.createGUI3DText((0, 5, 3), "主3D文本", 0.5) + self.text3d2 = self.createGUI3DText((-3, 3, 1), "左3D文本", 0.3) + self.text3d3 = self.createGUI3DText((3, 3, 1), "右3D文本", 0.3) + + # 虚拟屏幕测试 + self.screen1 = self.createGUIVirtualScreen((0, 8, 0), (3, 2), "主虚拟屏幕") + self.screen2 = self.createGUIVirtualScreen((-5, 5, 2), (2, 1), "左屏幕") + self.screen3 = self.createGUIVirtualScreen((5, 5, 2), (2, 1), "右屏幕") + + print(f"\nGUI创建完成!总计: {len(self.gui_elements)} 个元素") + print("检查窗口中是否能看到所有GUI元素...") + + # 使用修改后的GUI创建方法 + def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1): + """创建2D GUI按钮 - 新方法""" + # 直接使用合理的屏幕坐标,参考gui_3d_demo.py的成功实现 + if pos == (0, 0, 0): # 默认中心位置 + gui_pos = (0, 0, 0) + else: + # 简单映射:将输入坐标范围映射到合理的屏幕坐标 + scale_factor = 0.075 # 调整这个值来控制GUI分布范围 + gui_pos = (pos[0] * scale_factor, 0, pos[2] * scale_factor) + + button = DirectButton( + text=text, + pos=gui_pos, + scale=size, + command=self.onGUIButtonClick, + extraArgs=[f"button_{len(self.gui_elements)}"], + frameColor=(0.2, 0.6, 0.8, 1), + text_font=self.chinese_font if self.chinese_font else None, + rolloverSound=None, + clickSound=None + ) + + self.gui_elements.append(button) + print(f"✓ 创建GUI按钮: {text}") + print(f" 逻辑位置: {pos}") + print(f" 屏幕位置: {gui_pos}") + return button + + def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08): + """创建2D GUI标签 - 新方法""" + # 使用与按钮相同的坐标映射逻辑 + if pos == (0, 0, 0): + gui_pos = (0, 0, -0.3) # 标签默认稍微偏下 + else: + scale_factor = 0.075 + gui_pos = (pos[0] * scale_factor, 0, pos[2] * scale_factor) + + label = DirectLabel( + text=text, + pos=gui_pos, + scale=size, + frameColor=(0, 0, 0, 0), # 透明背景 + text_fg=(1, 1, 1, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + self.gui_elements.append(label) + print(f"✓ 创建GUI标签: {text}") + print(f" 逻辑位置: {pos}") + print(f" 屏幕位置: {gui_pos}") + return label + + def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08): + """创建2D GUI文本输入框 - 新方法""" + # 使用与按钮相同的坐标映射逻辑 + if pos == (0, 0, 0): + gui_pos = (0, 0, -0.6) # 输入框默认更靠下 + else: + scale_factor = 0.075 + gui_pos = (pos[0] * scale_factor, 0, pos[2] * scale_factor) + + entry = DirectEntry( + text="", + pos=gui_pos, + scale=size, + command=self.onGUIEntrySubmit, + extraArgs=[f"entry_{len(self.gui_elements)}"], + initialText=placeholder, + numLines=1, + width=12, + focus=0 + ) + + self.gui_elements.append(entry) + print(f"✓ 创建GUI输入框: {placeholder}") + print(f" 逻辑位置: {pos}") + print(f" 屏幕位置: {gui_pos}") + return entry + + def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): + """创建3D空间文本""" + textNode = TextNode(f'3d-text-{len(self.gui_elements)}') + textNode.setText(text) + textNode.setAlign(TextNode.ACenter) + if self.chinese_font: + textNode.setFont(self.chinese_font) + + textNodePath = self.render.attachNewNode(textNode) + textNodePath.setPos(*pos) + textNodePath.setScale(size) + textNodePath.setColor(1, 1, 0, 1) + textNodePath.setBillboardAxis() + + self.gui_elements.append(textNodePath) + print(f"✓ 创建3D文本: {text} (世界位置: {pos})") + return textNodePath + + def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"): + """创建3D虚拟屏幕""" + cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}") + cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2) + virtualScreen = self.render.attachNewNode(cm.generate()) + virtualScreen.setPos(*pos) + virtualScreen.setColor(0.2, 0.2, 0.2, 0.8) + virtualScreen.setTransparency(TransparencyAttrib.MAlpha) + + screenText = TextNode(f'screen-text-{len(self.gui_elements)}') + screenText.setText(text) + screenText.setAlign(TextNode.ACenter) + if self.chinese_font: + screenText.setFont(self.chinese_font) + screenTextNP = virtualScreen.attachNewNode(screenText) + screenTextNP.setPos(0, 0.01, 0) + screenTextNP.setScale(0.3) + screenTextNP.setColor(0, 1, 0, 1) + + self.gui_elements.append(virtualScreen) + print(f"✓ 创建虚拟屏幕: {text} (世界位置: {pos})") + return virtualScreen + + def onGUIButtonClick(self, button_id): + """GUI按钮点击事件处理""" + print(f"🎯 GUI按钮被点击: {button_id}") + + def onGUIEntrySubmit(self, text, entry_id): + """GUI输入框提交事件处理""" + print(f"📝 GUI输入框提交: {entry_id} = '{text}'") + +def main(): + """主函数""" + print("启动新方法GUI功能测试...") + print("这将测试使用gui_3d_demo.py成功方法修改后的GUI创建") + + # 创建并运行应用 + app = NewApproachGUITest() + app.run() + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/demo/test_gui_preview_integration.py b/demo/test_gui_preview_integration.py index d465994d..da124561 100644 --- a/demo/test_gui_preview_integration.py +++ b/demo/test_gui_preview_integration.py @@ -1,71 +1,71 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -测试GUI预览窗口与主程序的集成 -验证新的简化GUI编辑模式 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -import time -from test import MyWorld -from gui_preview_window import GUIPreviewWindow - -def test_gui_preview_integration(): - """测试GUI预览窗口集成""" - - print("开始测试GUI预览窗口集成...") - - # 创建模拟的主程序world - world = MyWorld() - - # 创建GUI预览窗口 - preview = GUIPreviewWindow() - preview.set_main_world(world) - - print("✓ 预览窗口已创建并连接到主程序") - - # 等待一下让窗口显示 - time.sleep(2) - - # 在主程序中创建一些GUI元素 - print("\n在主程序中创建GUI元素...") - - # 创建按钮 - button = world.createGUIButton((0, 0, 3), "测试按钮", 0.08) - print("✓ 已创建按钮") - - time.sleep(1) - - # 创建标签 - label = world.createGUILabel((0, 0, 1), "测试标签", 0.06) - print("✓ 已创建标签") - - time.sleep(1) - - # 创建输入框 - entry = world.createGUIEntry((0, 0, -1), "输入框", 0.05) - print("✓ 已创建输入框") - - time.sleep(1) - - # 创建3D文本 - text3d = world.createGUI3DText((5, 5, 0), "3D文本", 0.5) - print("✓ 已创建3D文本") - - print(f"\n主程序GUI元素总数: {len(world.gui_elements)}") - print("预览窗口应该会自动同步显示这些元素") - - print("\n测试完成!请查看预览窗口中的GUI元素") - print("预览窗口会自动与主程序同步") - - # 运行预览窗口 - if preview.preview_base: - print("\n启动预览窗口事件循环...") - preview.preview_base.run() - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +测试GUI预览窗口与主程序的集成 +验证新的简化GUI编辑模式 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +import time +from test import MyWorld +from gui_preview_window import GUIPreviewWindow + +def test_gui_preview_integration(): + """测试GUI预览窗口集成""" + + print("开始测试GUI预览窗口集成...") + + # 创建模拟的主程序world + world = MyWorld() + + # 创建GUI预览窗口 + preview = GUIPreviewWindow() + preview.set_main_world(world) + + print("✓ 预览窗口已创建并连接到主程序") + + # 等待一下让窗口显示 + time.sleep(2) + + # 在主程序中创建一些GUI元素 + print("\n在主程序中创建GUI元素...") + + # 创建按钮 + button = world.createGUIButton((0, 0, 3), "测试按钮", 0.08) + print("✓ 已创建按钮") + + time.sleep(1) + + # 创建标签 + label = world.createGUILabel((0, 0, 1), "测试标签", 0.06) + print("✓ 已创建标签") + + time.sleep(1) + + # 创建输入框 + entry = world.createGUIEntry((0, 0, -1), "输入框", 0.05) + print("✓ 已创建输入框") + + time.sleep(1) + + # 创建3D文本 + text3d = world.createGUI3DText((5, 5, 0), "3D文本", 0.5) + print("✓ 已创建3D文本") + + print(f"\n主程序GUI元素总数: {len(world.gui_elements)}") + print("预览窗口应该会自动同步显示这些元素") + + print("\n测试完成!请查看预览窗口中的GUI元素") + print("预览窗口会自动与主程序同步") + + # 运行预览窗口 + if preview.preview_base: + print("\n启动预览窗口事件循环...") + preview.preview_base.run() + +if __name__ == "__main__": test_gui_preview_integration() \ No newline at end of file diff --git a/demo/test_main_gui.py b/demo/test_main_gui.py index 4877e428..8017889d 100644 --- a/demo/test_main_gui.py +++ b/demo/test_main_gui.py @@ -1,130 +1,130 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -主程序GUI功能直接测试 -使用工具栏按钮创建GUI元素并验证显示 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -import os -import time -from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox -from PyQt5.QtCore import QTimer - -# 导入主程序 -from test import MyWorld, CustomPanda3DWidget - -def testMainProgramGUI(): - """测试主程序的GUI功能""" - print("=== 测试主程序GUI功能 ===") - - app = QApplication(sys.argv) - - # 创建主程序的世界对象 - world = MyWorld() - - # 创建窗口 - mainWindow = QMainWindow() - mainWindow.setWindowTitle("主程序GUI测试") - mainWindow.setGeometry(100, 100, 800, 600) - - # 创建Panda3D部件 - pandaWidget = CustomPanda3DWidget(world) - mainWindow.setCentralWidget(pandaWidget) - - # 显示窗口 - mainWindow.show() - - # 使用定时器延迟创建GUI元素 - def createTestGUIElements(): - print("\n开始创建GUI元素...") - - try: - # 使用不同的测试坐标 - test_positions = [ - (0, 0, 0), # 中心 - (-5, 0, 0), # 左侧 - (5, 0, 0), # 右侧 - (0, 0, 5), # 上方 - (0, 0, -5), # 下方 - ] - - # 创建按钮 - print("创建按钮...") - for i, pos in enumerate(test_positions): - button = world.createGUIButton(pos, f"按钮{i+1}", 0.08) - print(f" 按钮{i+1}: 位置{pos} -> 屏幕位置{button.getPos() if button else 'None'}") - - # 创建标签 - print("创建标签...") - label_positions = [ - (0, 0, -8), # 中心下方 - (-8, 0, 0), # 左侧 - (8, 0, 0), # 右侧 - ] - for i, pos in enumerate(label_positions): - label = world.createGUILabel(pos, f"标签{i+1}", 0.06) - print(f" 标签{i+1}: 位置{pos} -> 屏幕位置{label.getPos() if label else 'None'}") - - # 创建输入框 - print("创建输入框...") - entry_positions = [ - (0, 0, -12), # 中心下方 - (-10, 0, -8), # 左下 - ] - for i, pos in enumerate(entry_positions): - entry = world.createGUIEntry(pos, f"输入框{i+1}", 0.05) - print(f" 输入框{i+1}: 位置{pos} -> 屏幕位置{entry.getPos() if entry else 'None'}") - - # 创建3D元素作为对比 - print("创建3D元素...") - world.createGUI3DText((0, 5, 2), "3D测试文本", 0.5) - world.createGUIVirtualScreen((3, 8, 0), (2, 1), "测试屏幕") - - print(f"\n总共创建了 {len(world.gui_elements)} 个GUI元素") - - # 检查aspect2d的子节点 - print(f"\naspect2d子节点数量: {world.aspect2d.getNumChildren()}") - for i in range(world.aspect2d.getNumChildren()): - child = world.aspect2d.getChild(i) - print(f" 子节点{i}: {child.getName()}") - - # 显示消息框告知结果 - msg = f"""GUI元素创建完成! - -创建的元素: -- 5个按钮 -- 3个标签 -- 2个输入框 -- 1个3D文本 -- 1个虚拟屏幕 - -总计: {len(world.gui_elements)} 个 - -如果你看不到2D GUI元素(按钮、标签、输入框), -但能看到3D元素(黄色文本、虚拟屏幕), -说明坐标转换可能还有问题。 - -请观察窗口中的GUI元素。""" - - QMessageBox.information(mainWindow, "GUI创建完成", msg) - - except Exception as e: - error_msg = f"创建GUI元素时出错: {str(e)}" - print(error_msg) - QMessageBox.critical(mainWindow, "错误", error_msg) - - # 2秒后创建GUI元素 - QTimer.singleShot(2000, createTestGUIElements) - - print("窗口已显示,2秒后将创建GUI元素...") - print("请观察是否能看到所有GUI组件") - - return app.exec_() - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +主程序GUI功能直接测试 +使用工具栏按钮创建GUI元素并验证显示 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +import os +import time +from PyQt5.QtWidgets import QApplication, QMainWindow, QMessageBox +from PyQt5.QtCore import QTimer + +# 导入主程序 +from test import MyWorld, CustomPanda3DWidget + +def testMainProgramGUI(): + """测试主程序的GUI功能""" + print("=== 测试主程序GUI功能 ===") + + app = QApplication(sys.argv) + + # 创建主程序的世界对象 + world = MyWorld() + + # 创建窗口 + mainWindow = QMainWindow() + mainWindow.setWindowTitle("主程序GUI测试") + mainWindow.setGeometry(100, 100, 800, 600) + + # 创建Panda3D部件 + pandaWidget = CustomPanda3DWidget(world) + mainWindow.setCentralWidget(pandaWidget) + + # 显示窗口 + mainWindow.show() + + # 使用定时器延迟创建GUI元素 + def createTestGUIElements(): + print("\n开始创建GUI元素...") + + try: + # 使用不同的测试坐标 + test_positions = [ + (0, 0, 0), # 中心 + (-5, 0, 0), # 左侧 + (5, 0, 0), # 右侧 + (0, 0, 5), # 上方 + (0, 0, -5), # 下方 + ] + + # 创建按钮 + print("创建按钮...") + for i, pos in enumerate(test_positions): + button = world.createGUIButton(pos, f"按钮{i+1}", 0.08) + print(f" 按钮{i+1}: 位置{pos} -> 屏幕位置{button.getPos() if button else 'None'}") + + # 创建标签 + print("创建标签...") + label_positions = [ + (0, 0, -8), # 中心下方 + (-8, 0, 0), # 左侧 + (8, 0, 0), # 右侧 + ] + for i, pos in enumerate(label_positions): + label = world.createGUILabel(pos, f"标签{i+1}", 0.06) + print(f" 标签{i+1}: 位置{pos} -> 屏幕位置{label.getPos() if label else 'None'}") + + # 创建输入框 + print("创建输入框...") + entry_positions = [ + (0, 0, -12), # 中心下方 + (-10, 0, -8), # 左下 + ] + for i, pos in enumerate(entry_positions): + entry = world.createGUIEntry(pos, f"输入框{i+1}", 0.05) + print(f" 输入框{i+1}: 位置{pos} -> 屏幕位置{entry.getPos() if entry else 'None'}") + + # 创建3D元素作为对比 + print("创建3D元素...") + world.createGUI3DText((0, 5, 2), "3D测试文本", 0.5) + world.createGUIVirtualScreen((3, 8, 0), (2, 1), "测试屏幕") + + print(f"\n总共创建了 {len(world.gui_elements)} 个GUI元素") + + # 检查aspect2d的子节点 + print(f"\naspect2d子节点数量: {world.aspect2d.getNumChildren()}") + for i in range(world.aspect2d.getNumChildren()): + child = world.aspect2d.getChild(i) + print(f" 子节点{i}: {child.getName()}") + + # 显示消息框告知结果 + msg = f"""GUI元素创建完成! + +创建的元素: +- 5个按钮 +- 3个标签 +- 2个输入框 +- 1个3D文本 +- 1个虚拟屏幕 + +总计: {len(world.gui_elements)} 个 + +如果你看不到2D GUI元素(按钮、标签、输入框), +但能看到3D元素(黄色文本、虚拟屏幕), +说明坐标转换可能还有问题。 + +请观察窗口中的GUI元素。""" + + QMessageBox.information(mainWindow, "GUI创建完成", msg) + + except Exception as e: + error_msg = f"创建GUI元素时出错: {str(e)}" + print(error_msg) + QMessageBox.critical(mainWindow, "错误", error_msg) + + # 2秒后创建GUI元素 + QTimer.singleShot(2000, createTestGUIElements) + + print("窗口已显示,2秒后将创建GUI元素...") + print("请观察是否能看到所有GUI组件") + + return app.exec_() + +if __name__ == "__main__": testMainProgramGUI() \ No newline at end of file diff --git a/demo/test_packaging.py b/demo/test_packaging.py index 4fb30d46..1bc4d4bf 100644 --- a/demo/test_packaging.py +++ b/demo/test_packaging.py @@ -1,220 +1,220 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -打包功能测试脚本 - -测试项目管理器的打包功能是否按照Panda3D官方标准正常工作 -""" - -import sys -import os -import tempfile -import shutil - -# 添加项目路径 -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from project.project_manager import ProjectManager -from main import MyWorld - -def test_packaging(): - """测试打包功能""" - - print("=== Panda3D 标准打包功能测试 ===\n") - - # 创建临时项目用于测试 - temp_dir = tempfile.mkdtemp(prefix="panda3d_package_test_") - print(f"创建临时测试项目: {temp_dir}") - - try: - # 设置测试项目结构 - test_project_path = os.path.join(temp_dir, "TestProject") - scenes_path = os.path.join(test_project_path, "scenes") - os.makedirs(scenes_path) - - # 创建项目管理器(不需要完整的world对象) - project_manager = ProjectManager(None) - project_manager.current_project_path = test_project_path - - # 创建一个简单的测试场景文件 - scene_file = os.path.join(scenes_path, "scene.bam") - - # 创建一个空的BAM文件用于测试 - print("创建测试场景文件...") - try: - # 创建一个最小的场景文件 - with open(scene_file, 'wb') as f: - # 写入一个最小的Panda3D BAM文件头(只是为了测试) - f.write(b'BAM\x00') # 最简单的BAM文件标识 - print("✓ 测试场景文件创建成功") - except Exception as e: - print(f"✗ 测试场景文件创建失败: {e}") - return False - - # 测试打包文件创建 - build_dir = os.path.join(test_project_path, "build") - print(f"\n创建打包文件到: {build_dir}") - - project_manager._createStandardBuildFiles(build_dir, test_project_path, scene_file) - - # 检查生成的文件 - main_py = os.path.join(build_dir, "main.py") - setup_py = os.path.join(build_dir, "setup.py") - scene_bam = os.path.join(build_dir, "scene.bam") - - success = True - - if os.path.exists(main_py): - print("✓ main.py 创建成功") - # 检查文件内容 - with open(main_py, 'r', encoding='utf-8') as f: - content = f.read() - if "TestProject" in content: - print(" - 项目名称正确替换") - else: - print(" ✗ 项目名称替换失败") - success = False - else: - print("✗ main.py 创建失败") - success = False - - if os.path.exists(setup_py): - print("✓ setup.py 创建成功") - # 检查配置内容 - with open(setup_py, 'r', encoding='utf-8') as f: - content = f.read() - if "build_apps" in content and "gui_apps" in content: - print(" - 包含标准打包配置") - else: - print(" ✗ 缺少标准打包配置") - success = False - else: - print("✗ setup.py 创建失败") - success = False - - if os.path.exists(scene_bam): - print("✓ scene.bam 复制成功") - else: - print("✗ scene.bam 复制失败") - success = False - - # 显示生成的文件内容概要 - print(f"\n=== 生成的文件概要 ===") - for filename in os.listdir(build_dir): - filepath = os.path.join(build_dir, filename) - size = os.path.getsize(filepath) - print(f" {filename}: {size} bytes") - - # 检查setup.py的关键配置 - print(f"\n=== setup.py 配置检查 ===") - if os.path.exists(setup_py): - with open(setup_py, 'r', encoding='utf-8') as f: - content = f.read() - - checks = [ - ("APP_NAME", "应用程序名称"), - ("build_apps", "构建应用配置"), - ("gui_apps", "GUI应用配置"), - ("include_patterns", "文件包含模式"), - ("plugins", "Panda3D插件"), - ("platforms", "目标平台"), - ] - - for check, desc in checks: - if check in content: - print(f" ✓ {desc} 配置正确") - else: - print(f" ✗ {desc} 配置缺失") - success = False - - print(f"\n=== 测试结果 ===") - if success: - print("✓ 所有打包文件创建成功!") - print("✓ 配置符合Panda3D官方标准") - print("\n可以手动运行以下命令进行实际打包:") - print(f" cd {build_dir}") - print(f" python setup.py bdist_apps") - return True - else: - print("✗ 打包文件创建存在问题") - return False - - except Exception as e: - print(f"测试过程中出现错误: {str(e)}") - import traceback - traceback.print_exc() - return False - - finally: - # 清理临时文件 - try: - shutil.rmtree(temp_dir) - print(f"\n清理临时文件: {temp_dir}") - except Exception as e: - print(f"清理临时文件失败: {str(e)}") - -def test_setup_validation(): - """验证setup.py文件的语法正确性""" - - print("\n=== setup.py 语法验证 ===") - - # 创建临时目录 - temp_dir = tempfile.mkdtemp(prefix="setup_validation_") - - try: - # 创建项目管理器实例(不需要完整的world对象) - project_manager = ProjectManager(None) - - # 生成setup.py文件 - project_manager._createStandardSetupFile(temp_dir, "ValidationTest") - - setup_file = os.path.join(temp_dir, "setup.py") - - if not os.path.exists(setup_file): - print("✗ setup.py 文件未生成") - return False - - # 检查Python语法 - print("检查Python语法...") - try: - with open(setup_file, 'r', encoding='utf-8') as f: - code = f.read() - - # 编译代码检查语法 - compile(code, setup_file, 'exec') - print("✓ Python语法正确") - - except SyntaxError as e: - print(f"✗ Python语法错误: {e}") - return False - - print("✓ setup.py 验证通过") - return True - - except Exception as e: - print(f"验证过程出错: {str(e)}") - return False - - finally: - try: - shutil.rmtree(temp_dir) - except: - pass - -if __name__ == "__main__": - print("Panda3D 标准打包功能测试\n") - - # 运行测试 - test1_result = test_packaging() - test2_result = test_setup_validation() - - print(f"\n=== 最终测试结果 ===") - print(f"打包文件创建测试: {'通过' if test1_result else '失败'}") - print(f"setup.py语法验证: {'通过' if test2_result else '失败'}") - - if test1_result and test2_result: - print("\n🎉 所有测试通过!新的打包功能工作正常。") - print("📦 现在可以使用标准的Panda3D打包流程了。") - else: +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +打包功能测试脚本 + +测试项目管理器的打包功能是否按照Panda3D官方标准正常工作 +""" + +import sys +import os +import tempfile +import shutil + +# 添加项目路径 +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from project.project_manager import ProjectManager +from main import MyWorld + +def test_packaging(): + """测试打包功能""" + + print("=== Panda3D 标准打包功能测试 ===\n") + + # 创建临时项目用于测试 + temp_dir = tempfile.mkdtemp(prefix="panda3d_package_test_") + print(f"创建临时测试项目: {temp_dir}") + + try: + # 设置测试项目结构 + test_project_path = os.path.join(temp_dir, "TestProject") + scenes_path = os.path.join(test_project_path, "scenes") + os.makedirs(scenes_path) + + # 创建项目管理器(不需要完整的world对象) + project_manager = ProjectManager(None) + project_manager.current_project_path = test_project_path + + # 创建一个简单的测试场景文件 + scene_file = os.path.join(scenes_path, "scene.bam") + + # 创建一个空的BAM文件用于测试 + print("创建测试场景文件...") + try: + # 创建一个最小的场景文件 + with open(scene_file, 'wb') as f: + # 写入一个最小的Panda3D BAM文件头(只是为了测试) + f.write(b'BAM\x00') # 最简单的BAM文件标识 + print("✓ 测试场景文件创建成功") + except Exception as e: + print(f"✗ 测试场景文件创建失败: {e}") + return False + + # 测试打包文件创建 + build_dir = os.path.join(test_project_path, "build") + print(f"\n创建打包文件到: {build_dir}") + + project_manager._createStandardBuildFiles(build_dir, test_project_path, scene_file) + + # 检查生成的文件 + main_py = os.path.join(build_dir, "main.py") + setup_py = os.path.join(build_dir, "setup.py") + scene_bam = os.path.join(build_dir, "scene.bam") + + success = True + + if os.path.exists(main_py): + print("✓ main.py 创建成功") + # 检查文件内容 + with open(main_py, 'r', encoding='utf-8') as f: + content = f.read() + if "TestProject" in content: + print(" - 项目名称正确替换") + else: + print(" ✗ 项目名称替换失败") + success = False + else: + print("✗ main.py 创建失败") + success = False + + if os.path.exists(setup_py): + print("✓ setup.py 创建成功") + # 检查配置内容 + with open(setup_py, 'r', encoding='utf-8') as f: + content = f.read() + if "build_apps" in content and "gui_apps" in content: + print(" - 包含标准打包配置") + else: + print(" ✗ 缺少标准打包配置") + success = False + else: + print("✗ setup.py 创建失败") + success = False + + if os.path.exists(scene_bam): + print("✓ scene.bam 复制成功") + else: + print("✗ scene.bam 复制失败") + success = False + + # 显示生成的文件内容概要 + print(f"\n=== 生成的文件概要 ===") + for filename in os.listdir(build_dir): + filepath = os.path.join(build_dir, filename) + size = os.path.getsize(filepath) + print(f" {filename}: {size} bytes") + + # 检查setup.py的关键配置 + print(f"\n=== setup.py 配置检查 ===") + if os.path.exists(setup_py): + with open(setup_py, 'r', encoding='utf-8') as f: + content = f.read() + + checks = [ + ("APP_NAME", "应用程序名称"), + ("build_apps", "构建应用配置"), + ("gui_apps", "GUI应用配置"), + ("include_patterns", "文件包含模式"), + ("plugins", "Panda3D插件"), + ("platforms", "目标平台"), + ] + + for check, desc in checks: + if check in content: + print(f" ✓ {desc} 配置正确") + else: + print(f" ✗ {desc} 配置缺失") + success = False + + print(f"\n=== 测试结果 ===") + if success: + print("✓ 所有打包文件创建成功!") + print("✓ 配置符合Panda3D官方标准") + print("\n可以手动运行以下命令进行实际打包:") + print(f" cd {build_dir}") + print(f" python setup.py bdist_apps") + return True + else: + print("✗ 打包文件创建存在问题") + return False + + except Exception as e: + print(f"测试过程中出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + + finally: + # 清理临时文件 + try: + shutil.rmtree(temp_dir) + print(f"\n清理临时文件: {temp_dir}") + except Exception as e: + print(f"清理临时文件失败: {str(e)}") + +def test_setup_validation(): + """验证setup.py文件的语法正确性""" + + print("\n=== setup.py 语法验证 ===") + + # 创建临时目录 + temp_dir = tempfile.mkdtemp(prefix="setup_validation_") + + try: + # 创建项目管理器实例(不需要完整的world对象) + project_manager = ProjectManager(None) + + # 生成setup.py文件 + project_manager._createStandardSetupFile(temp_dir, "ValidationTest") + + setup_file = os.path.join(temp_dir, "setup.py") + + if not os.path.exists(setup_file): + print("✗ setup.py 文件未生成") + return False + + # 检查Python语法 + print("检查Python语法...") + try: + with open(setup_file, 'r', encoding='utf-8') as f: + code = f.read() + + # 编译代码检查语法 + compile(code, setup_file, 'exec') + print("✓ Python语法正确") + + except SyntaxError as e: + print(f"✗ Python语法错误: {e}") + return False + + print("✓ setup.py 验证通过") + return True + + except Exception as e: + print(f"验证过程出错: {str(e)}") + return False + + finally: + try: + shutil.rmtree(temp_dir) + except: + pass + +if __name__ == "__main__": + print("Panda3D 标准打包功能测试\n") + + # 运行测试 + test1_result = test_packaging() + test2_result = test_setup_validation() + + print(f"\n=== 最终测试结果 ===") + print(f"打包文件创建测试: {'通过' if test1_result else '失败'}") + print(f"setup.py语法验证: {'通过' if test2_result else '失败'}") + + if test1_result and test2_result: + print("\n🎉 所有测试通过!新的打包功能工作正常。") + print("📦 现在可以使用标准的Panda3D打包流程了。") + else: print("\n❌ 部分测试失败,需要检查配置。") \ No newline at end of file diff --git a/demo/test_qt_debug.py b/demo/test_qt_debug.py index db72d695..6e858aa5 100644 --- a/demo/test_qt_debug.py +++ b/demo/test_qt_debug.py @@ -1,237 +1,237 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Qt环境深度调试 -诊断Qt集成环境中的渲染问题 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel -from PyQt5.QtCore import Qt -from QMeta3D.QMeta3DWidget import QMeta3DWidget -from QMeta3D.Meta3DWorld import Meta3DWorld -from direct.gui.DirectGui import * -from direct.gui.OnscreenText import OnscreenText -from panda3d.core import * -import os - -class DebugGUIWorld(Meta3DWorld): - def __init__(self): - super().__init__() - - print("=== 开始Qt环境调试 ===") - - # 设置背景色 - self.setBackgroundColor(0.2, 0.2, 0.3) - print("背景色设置完成") - - # 检查基本渲染状态 - print(f"render节点: {self.render}") - print(f"render2d节点: {self.render2d}") - print(f"主相机: {self.cam}") - print(f"2D相机: {self.camera2d}") - - # 检查图形状态 - print(f"图形引擎: {self.graphicsEngine}") - if hasattr(self, 'win'): - print(f"主窗口: {self.win}") - print(f"窗口大小: {self.win.getXSize()} x {self.win.getYSize()}") - - # 创建一个简单的3D几何体测试 - self.create3DTest() - - # 延迟创建2D GUI - self.taskMgr.doMethodLater(1.0, self.create2DTest, "create-2d-test") - - def create3DTest(self): - """创建3D测试内容""" - print("\n--- 创建3D测试内容 ---") - - # 创建一个简单的立方体 - from panda3d.core import CardMaker - cm = CardMaker("test-cube") - cm.setFrame(-1, 1, -1, 1) - - self.testCube = self.render.attachNewNode(cm.generate()) - self.testCube.setPos(0, 5, 0) - self.testCube.setColor(1, 0, 0, 1) # 红色 - - print(f"3D立方体创建: {self.testCube}") - print(f"立方体位置: {self.testCube.getPos()}") - - # 创建3D文本 - textNode = TextNode('3d-debug-text') - textNode.setText("3D Test") - textNode.setAlign(TextNode.ACenter) - - self.text3D = self.render.attachNewNode(textNode) - self.text3D.setPos(0, 3, 2) - self.text3D.setScale(0.5) - self.text3D.setColor(1, 1, 0, 1) # 黄色 - self.text3D.setBillboardAxis() - - print(f"3D文本创建: {self.text3D}") - - # 添加基础光照 - alight = AmbientLight('debug-alight') - alight.setColor((0.3, 0.3, 0.3, 1)) - alnp = self.render.attachNewNode(alight) - self.render.setLight(alnp) - - dlight = DirectionalLight('debug-dlight') - dlight.setColor((0.8, 0.8, 0.8, 1)) - dlnp = self.render.attachNewNode(dlight) - dlnp.setHpr(45, -45, 0) - self.render.setLight(dlnp) - - print("光照设置完成") - - def create2DTest(self, task=None): - """创建2D测试内容""" - print("\n--- 创建2D测试内容 ---") - - # 检查2D渲染状态 - print(f"render2d: {self.render2d}") - print(f"aspect2d: {self.aspect2d}") - print(f"camera2d: {self.camera2d}") - print(f"camera2d位置: {self.camera2d.getPos()}") - print(f"camera2d朝向: {self.camera2d.getHpr()}") - - # 创建屏幕文本 - try: - self.screenText = OnscreenText( - text="2D Screen Test", - pos=(0, 0.8), - scale=0.1, - fg=(1, 1, 1, 1), - align=TextNode.ACenter - ) - print(f"屏幕文本创建成功: {self.screenText}") - print(f"屏幕文本父节点: {self.screenText.getParent()}") - except Exception as e: - print(f"屏幕文本创建失败: {str(e)}") - - # 创建DirectGUI按钮 - try: - self.testButton = DirectButton( - text="Test Button", - pos=(0, 0, 0), - scale=0.15, - frameColor=(1, 0, 0, 1), # 红色背景 - text_fg=(1, 1, 1, 1), # 白色文字 - command=self.onButtonClick - ) - print(f"DirectGUI按钮创建成功: {self.testButton}") - print(f"按钮父节点: {self.testButton.getParent()}") - print(f"按钮位置: {self.testButton.getPos()}") - print(f"按钮隐藏状态: {self.testButton.isHidden()}") - except Exception as e: - print(f"DirectGUI按钮创建失败: {str(e)}") - - # 创建DirectGUI标签 - try: - self.testLabel = DirectLabel( - text="Test Label", - pos=(0, 0, -0.3), - scale=0.1, - frameColor=(0, 1, 0, 1), # 绿色背景 - text_fg=(0, 0, 0, 1) # 黑色文字 - ) - print(f"DirectGUI标签创建成功: {self.testLabel}") - except Exception as e: - print(f"DirectGUI标签创建失败: {str(e)}") - - # 检查aspect2d的所有子节点 - print(f"\naspect2d子节点总数: {self.aspect2d.getNumChildren()}") - for i in range(self.aspect2d.getNumChildren()): - child = self.aspect2d.getChild(i) - print(f" 子节点 {i}: {child.getName()} - {child}") - - # 检查渲染状态 - self.checkRenderState() - - return task.done if task else None - - def checkRenderState(self): - """检查渲染状态""" - print("\n--- 检查渲染状态 ---") - - # 检查显示区域 - if hasattr(self, 'win') and self.win: - print(f"窗口有效: {self.win.isValid()}") - print(f"窗口活跃: {self.win.isActive()}") - print(f"窗口大小: {self.win.getXSize()} x {self.win.getYSize()}") - - # 检查显示区域 - for i in range(self.win.getNumDisplayRegions()): - dr = self.win.getDisplayRegion(i) - print(f"显示区域 {i}: {dr}") - print(f" 相机: {dr.getCamera()}") - print(f" 激活状态: {dr.isActive()}") - print(f" 维度: {dr.getDimensions()}") - - # 强制渲染一帧 - try: - if hasattr(self, 'graphicsEngine'): - self.graphicsEngine.renderFrame() - print("强制渲染帧完成") - except Exception as e: - print(f"强制渲染失败: {str(e)}") - - def onButtonClick(self): - """按钮点击事件""" - print("✓ 按钮被点击了!") - -class DebugWidget(QMeta3DWidget): - def __init__(self, world, parent=None): - super().__init__(world, parent) - self.world = world - print(f"Qt部件初始化完成: {self}") - -def main(): - """主函数""" - print("启动Qt环境深度调试...") - - app = QApplication(sys.argv) - - # 创建主窗口 - mainWindow = QMainWindow() - mainWindow.setWindowTitle("Qt环境深度调试") - mainWindow.setGeometry(100, 100, 800, 600) - - # 创建布局和容器 - centralWidget = QWidget() - layout = QVBoxLayout(centralWidget) - - # 添加状态标签 - statusLabel = QLabel("Qt环境调试 - 查看控制台输出") - statusLabel.setAlignment(Qt.AlignCenter) - statusLabel.setStyleSheet("background-color: lightblue; padding: 10px;") - layout.addWidget(statusLabel) - - # 创建调试世界 - world = DebugGUIWorld() - - # 创建Panda3D部件 - pandaWidget = DebugWidget(world) - layout.addWidget(pandaWidget) - - mainWindow.setCentralWidget(centralWidget) - - # 显示窗口 - mainWindow.show() - - print("调试窗口已显示") - print("如果看到红色立方体和黄色3D文本,说明3D渲染正常") - print("如果看到红色按钮和绿色标签,说明2D GUI渲染正常") - print("如果窗口是黑色的,说明存在渲染问题") - - # 运行应用 - return app.exec_() - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Qt环境深度调试 +诊断Qt集成环境中的渲染问题 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel +from PyQt5.QtCore import Qt +from QMeta3D.QMeta3DWidget import QMeta3DWidget +from QMeta3D.Meta3DWorld import Meta3DWorld +from direct.gui.DirectGui import * +from direct.gui.OnscreenText import OnscreenText +from panda3d.core import * +import os + +class DebugGUIWorld(Meta3DWorld): + def __init__(self): + super().__init__() + + print("=== 开始Qt环境调试 ===") + + # 设置背景色 + self.setBackgroundColor(0.2, 0.2, 0.3) + print("背景色设置完成") + + # 检查基本渲染状态 + print(f"render节点: {self.render}") + print(f"render2d节点: {self.render2d}") + print(f"主相机: {self.cam}") + print(f"2D相机: {self.camera2d}") + + # 检查图形状态 + print(f"图形引擎: {self.graphicsEngine}") + if hasattr(self, 'win'): + print(f"主窗口: {self.win}") + print(f"窗口大小: {self.win.getXSize()} x {self.win.getYSize()}") + + # 创建一个简单的3D几何体测试 + self.create3DTest() + + # 延迟创建2D GUI + self.taskMgr.doMethodLater(1.0, self.create2DTest, "create-2d-test") + + def create3DTest(self): + """创建3D测试内容""" + print("\n--- 创建3D测试内容 ---") + + # 创建一个简单的立方体 + from panda3d.core import CardMaker + cm = CardMaker("test-cube") + cm.setFrame(-1, 1, -1, 1) + + self.testCube = self.render.attachNewNode(cm.generate()) + self.testCube.setPos(0, 5, 0) + self.testCube.setColor(1, 0, 0, 1) # 红色 + + print(f"3D立方体创建: {self.testCube}") + print(f"立方体位置: {self.testCube.getPos()}") + + # 创建3D文本 + textNode = TextNode('3d-debug-text') + textNode.setText("3D Test") + textNode.setAlign(TextNode.ACenter) + + self.text3D = self.render.attachNewNode(textNode) + self.text3D.setPos(0, 3, 2) + self.text3D.setScale(0.5) + self.text3D.setColor(1, 1, 0, 1) # 黄色 + self.text3D.setBillboardAxis() + + print(f"3D文本创建: {self.text3D}") + + # 添加基础光照 + alight = AmbientLight('debug-alight') + alight.setColor((0.3, 0.3, 0.3, 1)) + alnp = self.render.attachNewNode(alight) + self.render.setLight(alnp) + + dlight = DirectionalLight('debug-dlight') + dlight.setColor((0.8, 0.8, 0.8, 1)) + dlnp = self.render.attachNewNode(dlight) + dlnp.setHpr(45, -45, 0) + self.render.setLight(dlnp) + + print("光照设置完成") + + def create2DTest(self, task=None): + """创建2D测试内容""" + print("\n--- 创建2D测试内容 ---") + + # 检查2D渲染状态 + print(f"render2d: {self.render2d}") + print(f"aspect2d: {self.aspect2d}") + print(f"camera2d: {self.camera2d}") + print(f"camera2d位置: {self.camera2d.getPos()}") + print(f"camera2d朝向: {self.camera2d.getHpr()}") + + # 创建屏幕文本 + try: + self.screenText = OnscreenText( + text="2D Screen Test", + pos=(0, 0.8), + scale=0.1, + fg=(1, 1, 1, 1), + align=TextNode.ACenter + ) + print(f"屏幕文本创建成功: {self.screenText}") + print(f"屏幕文本父节点: {self.screenText.getParent()}") + except Exception as e: + print(f"屏幕文本创建失败: {str(e)}") + + # 创建DirectGUI按钮 + try: + self.testButton = DirectButton( + text="Test Button", + pos=(0, 0, 0), + scale=0.15, + frameColor=(1, 0, 0, 1), # 红色背景 + text_fg=(1, 1, 1, 1), # 白色文字 + command=self.onButtonClick + ) + print(f"DirectGUI按钮创建成功: {self.testButton}") + print(f"按钮父节点: {self.testButton.getParent()}") + print(f"按钮位置: {self.testButton.getPos()}") + print(f"按钮隐藏状态: {self.testButton.isHidden()}") + except Exception as e: + print(f"DirectGUI按钮创建失败: {str(e)}") + + # 创建DirectGUI标签 + try: + self.testLabel = DirectLabel( + text="Test Label", + pos=(0, 0, -0.3), + scale=0.1, + frameColor=(0, 1, 0, 1), # 绿色背景 + text_fg=(0, 0, 0, 1) # 黑色文字 + ) + print(f"DirectGUI标签创建成功: {self.testLabel}") + except Exception as e: + print(f"DirectGUI标签创建失败: {str(e)}") + + # 检查aspect2d的所有子节点 + print(f"\naspect2d子节点总数: {self.aspect2d.getNumChildren()}") + for i in range(self.aspect2d.getNumChildren()): + child = self.aspect2d.getChild(i) + print(f" 子节点 {i}: {child.getName()} - {child}") + + # 检查渲染状态 + self.checkRenderState() + + return task.done if task else None + + def checkRenderState(self): + """检查渲染状态""" + print("\n--- 检查渲染状态 ---") + + # 检查显示区域 + if hasattr(self, 'win') and self.win: + print(f"窗口有效: {self.win.isValid()}") + print(f"窗口活跃: {self.win.isActive()}") + print(f"窗口大小: {self.win.getXSize()} x {self.win.getYSize()}") + + # 检查显示区域 + for i in range(self.win.getNumDisplayRegions()): + dr = self.win.getDisplayRegion(i) + print(f"显示区域 {i}: {dr}") + print(f" 相机: {dr.getCamera()}") + print(f" 激活状态: {dr.isActive()}") + print(f" 维度: {dr.getDimensions()}") + + # 强制渲染一帧 + try: + if hasattr(self, 'graphicsEngine'): + self.graphicsEngine.renderFrame() + print("强制渲染帧完成") + except Exception as e: + print(f"强制渲染失败: {str(e)}") + + def onButtonClick(self): + """按钮点击事件""" + print("✓ 按钮被点击了!") + +class DebugWidget(QMeta3DWidget): + def __init__(self, world, parent=None): + super().__init__(world, parent) + self.world = world + print(f"Qt部件初始化完成: {self}") + +def main(): + """主函数""" + print("启动Qt环境深度调试...") + + app = QApplication(sys.argv) + + # 创建主窗口 + mainWindow = QMainWindow() + mainWindow.setWindowTitle("Qt环境深度调试") + mainWindow.setGeometry(100, 100, 800, 600) + + # 创建布局和容器 + centralWidget = QWidget() + layout = QVBoxLayout(centralWidget) + + # 添加状态标签 + statusLabel = QLabel("Qt环境调试 - 查看控制台输出") + statusLabel.setAlignment(Qt.AlignCenter) + statusLabel.setStyleSheet("background-color: lightblue; padding: 10px;") + layout.addWidget(statusLabel) + + # 创建调试世界 + world = DebugGUIWorld() + + # 创建Panda3D部件 + pandaWidget = DebugWidget(world) + layout.addWidget(pandaWidget) + + mainWindow.setCentralWidget(centralWidget) + + # 显示窗口 + mainWindow.show() + + print("调试窗口已显示") + print("如果看到红色立方体和黄色3D文本,说明3D渲染正常") + print("如果看到红色按钮和绿色标签,说明2D GUI渲染正常") + print("如果窗口是黑色的,说明存在渲染问题") + + # 运行应用 + return app.exec_() + +if __name__ == "__main__": exit_code = main() \ No newline at end of file diff --git a/demo/test_qt_fix.py b/demo/test_qt_fix.py index 7d99fd78..2cd02505 100644 --- a/demo/test_qt_fix.py +++ b/demo/test_qt_fix.py @@ -1,145 +1,145 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Qt环境下的2D GUI测试 -专门测试在Qt集成环境中的GUI显示问题 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -from PyQt5.QtWidgets import QApplication, QMainWindow -from PyQt5.QtCore import Qt -from QMeta3D.QMeta3DWidget import QMeta3DWidget -from QMeta3D.Meta3DWorld import Meta3DWorld -from direct.gui.DirectGui import * -from direct.gui.OnscreenText import OnscreenText -from panda3d.core import * -import os - -class QtGUITestWorld(Meta3DWorld): - def __init__(self): - super().__init__() - - # 设置背景色 - self.setBackgroundColor(0.2, 0.2, 0.3) - - # 尝试加载中文字体 - try: - self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') - except: - self.chinese_font = None - - # 延迟创建GUI,确保渲染系统就绪 - self.taskMgr.doMethodLater(0.5, self.createTestGUI, "create-gui") - - def createTestGUI(self, task=None): - """创建测试GUI""" - print("\n=== Qt环境GUI测试 ===") - - # 检查render2d状态 - print(f"render2d: {self.render2d}") - print(f"aspect2d: {self.aspect2d}") - print(f"pixel2d: {self.pixel2d}") - - # 创建标题 - self.title = OnscreenText( - text="Qt环境GUI测试", - pos=(0, 0.9), - scale=0.08, - fg=(1, 1, 1, 1), - align=TextNode.ACenter, - font=self.chinese_font if self.chinese_font else None - ) - print(f"标题创建成功: {self.title}") - - # 创建简单的按钮 - self.testButton = DirectButton( - text="测试按钮", - pos=(0, 0, 0), - scale=0.1, - command=self.onButtonClick, - frameColor=(1, 0, 0, 1), # 使用红色,更容易看见 - text_font=self.chinese_font if self.chinese_font else None, - rolloverSound=None, - clickSound=None - ) - print(f"按钮创建成功: {self.testButton}") - print(f"按钮父节点: {self.testButton.getParent()}") - print(f"按钮位置: {self.testButton.getPos()}") - print(f"按钮隐藏状态: {self.testButton.isHidden()}") - - # 创建标签 - self.testLabel = DirectLabel( - text="测试标签", - pos=(0, 0, -0.3), - scale=0.08, - frameColor=(0, 1, 0, 1), # 使用绿色背景 - text_fg=(0, 0, 0, 1), # 黑色文字 - text_font=self.chinese_font if self.chinese_font else None - ) - print(f"标签创建成功: {self.testLabel}") - - # 检查render2d的子节点 - print(f"\nrender2d子节点数量: {self.render2d.getNumChildren()}") - for i in range(self.render2d.getNumChildren()): - child = self.render2d.getChild(i) - print(f" 子节点 {i}: {child}") - - print(f"\naspect2d子节点数量: {self.aspect2d.getNumChildren()}") - for i in range(self.aspect2d.getNumChildren()): - child = self.aspect2d.getChild(i) - print(f" 子节点 {i}: {child}") - - # 检查相机设置 - print(f"\nrender2d相机: {self.camera2d}") - print(f"render2d相机位置: {self.camera2d.getPos()}") - print(f"render2d相机HPR: {self.camera2d.getHpr()}") - - # 尝试手动刷新2D渲染 - if hasattr(self, 'graphicsEngine'): - print("\n尝试刷新图形引擎...") - self.graphicsEngine.renderFrame() - - return task.done if task else None - - def onButtonClick(self): - """按钮点击事件""" - print("✓ 按钮被点击了!") - -class QtGUITestWidget(QMeta3DWidget): - def __init__(self, world, parent=None): - super().__init__(world, parent) - self.world = world - -def main(): - """主函数""" - print("启动Qt环境GUI测试...") - - app = QApplication(sys.argv) - - # 创建测试世界 - world = QtGUITestWorld() - - # 创建主窗口 - mainWindow = QMainWindow() - mainWindow.setWindowTitle("Qt环境GUI测试") - mainWindow.setGeometry(100, 100, 800, 600) - - # 创建Panda3D部件 - pandaWidget = QtGUITestWidget(world) - mainWindow.setCentralWidget(pandaWidget) - - # 显示窗口 - mainWindow.show() - - print("窗口已显示,检查控制台输出...") - print("如果看到红色按钮和绿色标签,说明Qt环境GUI工作正常") - - # 运行应用 - return app.exec_() - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Qt环境下的2D GUI测试 +专门测试在Qt集成环境中的GUI显示问题 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +from PyQt5.QtWidgets import QApplication, QMainWindow +from PyQt5.QtCore import Qt +from QMeta3D.QMeta3DWidget import QMeta3DWidget +from QMeta3D.Meta3DWorld import Meta3DWorld +from direct.gui.DirectGui import * +from direct.gui.OnscreenText import OnscreenText +from panda3d.core import * +import os + +class QtGUITestWorld(Meta3DWorld): + def __init__(self): + super().__init__() + + # 设置背景色 + self.setBackgroundColor(0.2, 0.2, 0.3) + + # 尝试加载中文字体 + try: + self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') + except: + self.chinese_font = None + + # 延迟创建GUI,确保渲染系统就绪 + self.taskMgr.doMethodLater(0.5, self.createTestGUI, "create-gui") + + def createTestGUI(self, task=None): + """创建测试GUI""" + print("\n=== Qt环境GUI测试 ===") + + # 检查render2d状态 + print(f"render2d: {self.render2d}") + print(f"aspect2d: {self.aspect2d}") + print(f"pixel2d: {self.pixel2d}") + + # 创建标题 + self.title = OnscreenText( + text="Qt环境GUI测试", + pos=(0, 0.9), + scale=0.08, + fg=(1, 1, 1, 1), + align=TextNode.ACenter, + font=self.chinese_font if self.chinese_font else None + ) + print(f"标题创建成功: {self.title}") + + # 创建简单的按钮 + self.testButton = DirectButton( + text="测试按钮", + pos=(0, 0, 0), + scale=0.1, + command=self.onButtonClick, + frameColor=(1, 0, 0, 1), # 使用红色,更容易看见 + text_font=self.chinese_font if self.chinese_font else None, + rolloverSound=None, + clickSound=None + ) + print(f"按钮创建成功: {self.testButton}") + print(f"按钮父节点: {self.testButton.getParent()}") + print(f"按钮位置: {self.testButton.getPos()}") + print(f"按钮隐藏状态: {self.testButton.isHidden()}") + + # 创建标签 + self.testLabel = DirectLabel( + text="测试标签", + pos=(0, 0, -0.3), + scale=0.08, + frameColor=(0, 1, 0, 1), # 使用绿色背景 + text_fg=(0, 0, 0, 1), # 黑色文字 + text_font=self.chinese_font if self.chinese_font else None + ) + print(f"标签创建成功: {self.testLabel}") + + # 检查render2d的子节点 + print(f"\nrender2d子节点数量: {self.render2d.getNumChildren()}") + for i in range(self.render2d.getNumChildren()): + child = self.render2d.getChild(i) + print(f" 子节点 {i}: {child}") + + print(f"\naspect2d子节点数量: {self.aspect2d.getNumChildren()}") + for i in range(self.aspect2d.getNumChildren()): + child = self.aspect2d.getChild(i) + print(f" 子节点 {i}: {child}") + + # 检查相机设置 + print(f"\nrender2d相机: {self.camera2d}") + print(f"render2d相机位置: {self.camera2d.getPos()}") + print(f"render2d相机HPR: {self.camera2d.getHpr()}") + + # 尝试手动刷新2D渲染 + if hasattr(self, 'graphicsEngine'): + print("\n尝试刷新图形引擎...") + self.graphicsEngine.renderFrame() + + return task.done if task else None + + def onButtonClick(self): + """按钮点击事件""" + print("✓ 按钮被点击了!") + +class QtGUITestWidget(QMeta3DWidget): + def __init__(self, world, parent=None): + super().__init__(world, parent) + self.world = world + +def main(): + """主函数""" + print("启动Qt环境GUI测试...") + + app = QApplication(sys.argv) + + # 创建测试世界 + world = QtGUITestWorld() + + # 创建主窗口 + mainWindow = QMainWindow() + mainWindow.setWindowTitle("Qt环境GUI测试") + mainWindow.setGeometry(100, 100, 800, 600) + + # 创建Panda3D部件 + pandaWidget = QtGUITestWidget(world) + mainWindow.setCentralWidget(pandaWidget) + + # 显示窗口 + mainWindow.show() + + print("窗口已显示,检查控制台输出...") + print("如果看到红色按钮和绿色标签,说明Qt环境GUI工作正常") + + # 运行应用 + return app.exec_() + +if __name__ == "__main__": exit_code = main() \ No newline at end of file diff --git a/demo/test_qt_fix_v2.py b/demo/test_qt_fix_v2.py index 6f1beacc..ab00b206 100644 --- a/demo/test_qt_fix_v2.py +++ b/demo/test_qt_fix_v2.py @@ -1,338 +1,338 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Qt环境渲染修复版本 -解决Qt集成环境中的渲染问题 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel -from PyQt5.QtCore import Qt -from PyQt5.QtOpenGL import QOpenGLWidget -from direct.showbase.ShowBase import ShowBase -from direct.gui.DirectGui import * -from direct.gui.OnscreenText import OnscreenText -from panda3d.core import * -import os - -# 强制配置Panda3D使用OpenGL -loadPrcFileData("", """ - win-size 800 600 - window-title Qt Fixed Test - framebuffer-multisample 1 - multisamples 2 - prefer-parasite-buffer false -""") - -class QtFixedWorld(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - print("=== Qt修复版本世界初始化 ===") - - # 设置背景色 - self.setBackgroundColor(0.2, 0.2, 0.3) - print("背景色设置完成") - - # 调整相机位置 - self.cam.setPos(0, -10, 5) - self.cam.lookAt(0, 0, 0) - - # 检查基本状态 - print(f"窗口类型: {type(self.win)}") - print(f"图形引擎: {self.graphicsEngine}") - print(f"窗口大小: {self.win.getXSize()} x {self.win.getYSize()}") - - # 初始化GUI元素列表 - self.gui_elements = [] - - # 尝试加载中文字体 - try: - self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') - if not self.chinese_font: - print("警告: 无法加载中文字体,将使用默认字体") - except: - print("警告: 无法加载中文字体,将使用默认字体") - self.chinese_font = None - - # 创建测试内容 - self.createTestContent() - - # 延迟创建GUI元素 - self.taskMgr.doMethodLater(0.5, self.createGUIElements, "create-gui") - - def createTestContent(self): - """创建测试内容""" - print("\n--- 创建测试内容 ---") - - # 创建3D几何体 - from panda3d.core import CardMaker - cm = CardMaker("test-plane") - cm.setFrame(-2, 2, -2, 2) - - self.testPlane = self.render.attachNewNode(cm.generate()) - self.testPlane.setPos(0, 5, 0) - self.testPlane.setColor(0.8, 0.2, 0.2, 1) # 红色 - - # 创建3D文本 - textNode = TextNode('3d-text') - textNode.setText("3D Test Content") - textNode.setAlign(TextNode.ACenter) - if self.chinese_font: - textNode.setFont(self.chinese_font) - - self.text3D = self.render.attachNewNode(textNode) - self.text3D.setPos(0, 3, 2) - self.text3D.setScale(0.5) - self.text3D.setColor(1, 1, 0, 1) - self.text3D.setBillboardAxis() - - # 添加光照 - alight = AmbientLight('alight') - alight.setColor((0.3, 0.3, 0.3, 1)) - alnp = self.render.attachNewNode(alight) - self.render.setLight(alnp) - - dlight = DirectionalLight('dlight') - dlight.setColor((0.8, 0.8, 0.8, 1)) - dlnp = self.render.attachNewNode(dlight) - dlnp.setHpr(45, -45, 0) - self.render.setLight(dlnp) - - print("3D内容创建完成") - - def createGUIElements(self, task=None): - """创建GUI元素""" - print("\n--- 创建GUI元素 ---") - - # 创建屏幕标题 - self.title = OnscreenText( - text="Qt环境修复测试", - pos=(0, 0.9), - scale=0.08, - fg=(1, 1, 1, 1), - align=TextNode.ACenter, - font=self.chinese_font if self.chinese_font else None - ) - print("屏幕标题创建完成") - - # 测试主程序中的GUI创建方法 - try: - # 按钮测试 - self.button1 = self.createGUIButton((0, 0, 0), "中心按钮", 0.08) - self.button2 = self.createGUIButton((-10, 0, 0), "左按钮", 0.08) - self.button3 = self.createGUIButton((10, 0, 0), "右按钮", 0.08) - - # 标签测试 - self.label1 = self.createGUILabel((0, 0, -10), "测试标签", 0.06) - self.label2 = self.createGUILabel((-10, 0, -5), "左标签", 0.06) - - # 输入框测试 - self.entry1 = self.createGUIEntry((10, 0, -5), "输入框", 0.05) - - # 3D文本测试 - self.text3d1 = self.createGUI3DText((0, 5, 3), "3D文本测试", 0.5) - self.text3d2 = self.createGUI3DText((-3, 3, 1), "左3D文本", 0.3) - - # 虚拟屏幕测试 - self.screen1 = self.createGUIVirtualScreen((3, 8, 0), (2, 1), "虚拟屏幕") - - print(f"GUI创建完成,总计: {len(self.gui_elements)} 个元素") - - except Exception as e: - print(f"GUI创建失败: {str(e)}") - - return task.done if task else None - - # 复制主程序中的GUI创建方法 - def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1): - """创建2D GUI按钮""" - # 将3D坐标转换为2D屏幕坐标 - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - - button = DirectButton( - text=text, - pos=gui_pos, - scale=size, - command=self.onGUIButtonClick, - extraArgs=[f"button_{len(self.gui_elements)}"], - frameColor=(0.2, 0.6, 0.8, 1), - text_font=self.chinese_font if self.chinese_font else None, - rolloverSound=None, - clickSound=None - ) - - button.setTag("gui_type", "button") - button.setTag("gui_id", f"button_{len(self.gui_elements)}") - button.setTag("gui_text", text) - - self.gui_elements.append(button) - print(f"创建GUI按钮: {text} 在屏幕位置 {gui_pos}") - return button - - def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08): - """创建2D GUI标签""" - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - - label = DirectLabel( - text=text, - pos=gui_pos, - scale=size, - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 1, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - label.setTag("gui_type", "label") - label.setTag("gui_id", f"label_{len(self.gui_elements)}") - label.setTag("gui_text", text) - - self.gui_elements.append(label) - print(f"创建GUI标签: {text} 在屏幕位置 {gui_pos}") - return label - - def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08): - """创建2D GUI文本输入框""" - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - - entry = DirectEntry( - text="", - pos=gui_pos, - scale=size, - command=self.onGUIEntrySubmit, - extraArgs=[f"entry_{len(self.gui_elements)}"], - initialText=placeholder, - numLines=1, - width=12, - focus=0 - ) - - entry.setTag("gui_type", "entry") - entry.setTag("gui_id", f"entry_{len(self.gui_elements)}") - entry.setTag("gui_placeholder", placeholder) - - self.gui_elements.append(entry) - print(f"创建GUI输入框: {placeholder} 在屏幕位置 {gui_pos}") - return entry - - def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): - """创建3D空间文本""" - textNode = TextNode(f'3d-text-{len(self.gui_elements)}') - textNode.setText(text) - textNode.setAlign(TextNode.ACenter) - if self.chinese_font: - textNode.setFont(self.chinese_font) - - textNodePath = self.render.attachNewNode(textNode) - textNodePath.setPos(*pos) - textNodePath.setScale(size) - textNodePath.setColor(1, 1, 0, 1) - textNodePath.setBillboardAxis() - - textNodePath.setTag("gui_type", "3d_text") - textNodePath.setTag("gui_id", f"3d_text_{len(self.gui_elements)}") - textNodePath.setTag("gui_text", text) - - self.gui_elements.append(textNodePath) - print(f"创建3D文本: {text} 在世界位置 {pos}") - return textNodePath - - def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"): - """创建3D虚拟屏幕""" - cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}") - cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2) - virtualScreen = self.render.attachNewNode(cm.generate()) - virtualScreen.setPos(*pos) - virtualScreen.setColor(0.2, 0.2, 0.2, 0.8) - virtualScreen.setTransparency(TransparencyAttrib.MAlpha) - - screenText = TextNode(f'screen-text-{len(self.gui_elements)}') - screenText.setText(text) - screenText.setAlign(TextNode.ACenter) - if self.chinese_font: - screenText.setFont(self.chinese_font) - screenTextNP = virtualScreen.attachNewNode(screenText) - screenTextNP.setPos(0, 0.01, 0) - screenTextNP.setScale(0.3) - screenTextNP.setColor(0, 1, 0, 1) - - virtualScreen.setTag("gui_type", "virtual_screen") - virtualScreen.setTag("gui_id", f"virtual_screen_{len(self.gui_elements)}") - virtualScreen.setTag("gui_text", text) - - self.gui_elements.append(virtualScreen) - print(f"创建虚拟屏幕: {text} 在世界位置 {pos}") - return virtualScreen - - def onGUIButtonClick(self, button_id): - """GUI按钮点击事件处理""" - print(f"✓ GUI按钮被点击: {button_id}") - - def onGUIEntrySubmit(self, text, entry_id): - """GUI输入框提交事件处理""" - print(f"✓ GUI输入框提交: {entry_id} = {text}") - -class QtFixedWidget(QOpenGLWidget): - """使用QOpenGLWidget的Panda3D部件""" - def __init__(self, world, parent=None): - super().__init__(parent) - self.world = world - self.setMinimumSize(800, 600) - print("Qt OpenGL部件创建完成") - - def paintGL(self): - """OpenGL绘制函数""" - if self.world and hasattr(self.world, 'graphicsEngine'): - self.world.graphicsEngine.renderFrame() - -def main(): - """主函数""" - print("启动Qt环境修复测试...") - - app = QApplication(sys.argv) - - # 创建主窗口 - mainWindow = QMainWindow() - mainWindow.setWindowTitle("Qt环境修复测试") - mainWindow.setGeometry(100, 100, 800, 600) - - # 创建布局和容器 - centralWidget = QWidget() - layout = QVBoxLayout(centralWidget) - - # 添加状态标签 - statusLabel = QLabel("Qt环境修复测试 - 应该能看到GUI元素") - statusLabel.setAlignment(Qt.AlignCenter) - statusLabel.setStyleSheet("background-color: lightgreen; padding: 10px; color: black;") - layout.addWidget(statusLabel) - - # 创建修复版本的世界 - world = QtFixedWorld() - - # 创建OpenGL部件 - openglWidget = QtFixedWidget(world) - layout.addWidget(openglWidget) - - mainWindow.setCentralWidget(centralWidget) - - # 显示窗口 - mainWindow.show() - - print("修复版本窗口已显示") - print("应该能看到:") - print("- 红色的3D平面") - print("- 黄色的3D文本") - print("- 多个2D GUI按钮(蓝色)") - print("- 2D GUI标签(白色文字)") - print("- 2D GUI输入框") - print("- 3D虚拟屏幕") - - # 运行应用 - return app.exec_() - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Qt环境渲染修复版本 +解决Qt集成环境中的渲染问题 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel +from PyQt5.QtCore import Qt +from PyQt5.QtOpenGL import QOpenGLWidget +from direct.showbase.ShowBase import ShowBase +from direct.gui.DirectGui import * +from direct.gui.OnscreenText import OnscreenText +from panda3d.core import * +import os + +# 强制配置Panda3D使用OpenGL +loadPrcFileData("", """ + win-size 800 600 + window-title Qt Fixed Test + framebuffer-multisample 1 + multisamples 2 + prefer-parasite-buffer false +""") + +class QtFixedWorld(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + print("=== Qt修复版本世界初始化 ===") + + # 设置背景色 + self.setBackgroundColor(0.2, 0.2, 0.3) + print("背景色设置完成") + + # 调整相机位置 + self.cam.setPos(0, -10, 5) + self.cam.lookAt(0, 0, 0) + + # 检查基本状态 + print(f"窗口类型: {type(self.win)}") + print(f"图形引擎: {self.graphicsEngine}") + print(f"窗口大小: {self.win.getXSize()} x {self.win.getYSize()}") + + # 初始化GUI元素列表 + self.gui_elements = [] + + # 尝试加载中文字体 + try: + self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') + if not self.chinese_font: + print("警告: 无法加载中文字体,将使用默认字体") + except: + print("警告: 无法加载中文字体,将使用默认字体") + self.chinese_font = None + + # 创建测试内容 + self.createTestContent() + + # 延迟创建GUI元素 + self.taskMgr.doMethodLater(0.5, self.createGUIElements, "create-gui") + + def createTestContent(self): + """创建测试内容""" + print("\n--- 创建测试内容 ---") + + # 创建3D几何体 + from panda3d.core import CardMaker + cm = CardMaker("test-plane") + cm.setFrame(-2, 2, -2, 2) + + self.testPlane = self.render.attachNewNode(cm.generate()) + self.testPlane.setPos(0, 5, 0) + self.testPlane.setColor(0.8, 0.2, 0.2, 1) # 红色 + + # 创建3D文本 + textNode = TextNode('3d-text') + textNode.setText("3D Test Content") + textNode.setAlign(TextNode.ACenter) + if self.chinese_font: + textNode.setFont(self.chinese_font) + + self.text3D = self.render.attachNewNode(textNode) + self.text3D.setPos(0, 3, 2) + self.text3D.setScale(0.5) + self.text3D.setColor(1, 1, 0, 1) + self.text3D.setBillboardAxis() + + # 添加光照 + alight = AmbientLight('alight') + alight.setColor((0.3, 0.3, 0.3, 1)) + alnp = self.render.attachNewNode(alight) + self.render.setLight(alnp) + + dlight = DirectionalLight('dlight') + dlight.setColor((0.8, 0.8, 0.8, 1)) + dlnp = self.render.attachNewNode(dlight) + dlnp.setHpr(45, -45, 0) + self.render.setLight(dlnp) + + print("3D内容创建完成") + + def createGUIElements(self, task=None): + """创建GUI元素""" + print("\n--- 创建GUI元素 ---") + + # 创建屏幕标题 + self.title = OnscreenText( + text="Qt环境修复测试", + pos=(0, 0.9), + scale=0.08, + fg=(1, 1, 1, 1), + align=TextNode.ACenter, + font=self.chinese_font if self.chinese_font else None + ) + print("屏幕标题创建完成") + + # 测试主程序中的GUI创建方法 + try: + # 按钮测试 + self.button1 = self.createGUIButton((0, 0, 0), "中心按钮", 0.08) + self.button2 = self.createGUIButton((-10, 0, 0), "左按钮", 0.08) + self.button3 = self.createGUIButton((10, 0, 0), "右按钮", 0.08) + + # 标签测试 + self.label1 = self.createGUILabel((0, 0, -10), "测试标签", 0.06) + self.label2 = self.createGUILabel((-10, 0, -5), "左标签", 0.06) + + # 输入框测试 + self.entry1 = self.createGUIEntry((10, 0, -5), "输入框", 0.05) + + # 3D文本测试 + self.text3d1 = self.createGUI3DText((0, 5, 3), "3D文本测试", 0.5) + self.text3d2 = self.createGUI3DText((-3, 3, 1), "左3D文本", 0.3) + + # 虚拟屏幕测试 + self.screen1 = self.createGUIVirtualScreen((3, 8, 0), (2, 1), "虚拟屏幕") + + print(f"GUI创建完成,总计: {len(self.gui_elements)} 个元素") + + except Exception as e: + print(f"GUI创建失败: {str(e)}") + + return task.done if task else None + + # 复制主程序中的GUI创建方法 + def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1): + """创建2D GUI按钮""" + # 将3D坐标转换为2D屏幕坐标 + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + + button = DirectButton( + text=text, + pos=gui_pos, + scale=size, + command=self.onGUIButtonClick, + extraArgs=[f"button_{len(self.gui_elements)}"], + frameColor=(0.2, 0.6, 0.8, 1), + text_font=self.chinese_font if self.chinese_font else None, + rolloverSound=None, + clickSound=None + ) + + button.setTag("gui_type", "button") + button.setTag("gui_id", f"button_{len(self.gui_elements)}") + button.setTag("gui_text", text) + + self.gui_elements.append(button) + print(f"创建GUI按钮: {text} 在屏幕位置 {gui_pos}") + return button + + def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08): + """创建2D GUI标签""" + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + + label = DirectLabel( + text=text, + pos=gui_pos, + scale=size, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + label.setTag("gui_type", "label") + label.setTag("gui_id", f"label_{len(self.gui_elements)}") + label.setTag("gui_text", text) + + self.gui_elements.append(label) + print(f"创建GUI标签: {text} 在屏幕位置 {gui_pos}") + return label + + def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08): + """创建2D GUI文本输入框""" + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + + entry = DirectEntry( + text="", + pos=gui_pos, + scale=size, + command=self.onGUIEntrySubmit, + extraArgs=[f"entry_{len(self.gui_elements)}"], + initialText=placeholder, + numLines=1, + width=12, + focus=0 + ) + + entry.setTag("gui_type", "entry") + entry.setTag("gui_id", f"entry_{len(self.gui_elements)}") + entry.setTag("gui_placeholder", placeholder) + + self.gui_elements.append(entry) + print(f"创建GUI输入框: {placeholder} 在屏幕位置 {gui_pos}") + return entry + + def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): + """创建3D空间文本""" + textNode = TextNode(f'3d-text-{len(self.gui_elements)}') + textNode.setText(text) + textNode.setAlign(TextNode.ACenter) + if self.chinese_font: + textNode.setFont(self.chinese_font) + + textNodePath = self.render.attachNewNode(textNode) + textNodePath.setPos(*pos) + textNodePath.setScale(size) + textNodePath.setColor(1, 1, 0, 1) + textNodePath.setBillboardAxis() + + textNodePath.setTag("gui_type", "3d_text") + textNodePath.setTag("gui_id", f"3d_text_{len(self.gui_elements)}") + textNodePath.setTag("gui_text", text) + + self.gui_elements.append(textNodePath) + print(f"创建3D文本: {text} 在世界位置 {pos}") + return textNodePath + + def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"): + """创建3D虚拟屏幕""" + cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}") + cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2) + virtualScreen = self.render.attachNewNode(cm.generate()) + virtualScreen.setPos(*pos) + virtualScreen.setColor(0.2, 0.2, 0.2, 0.8) + virtualScreen.setTransparency(TransparencyAttrib.MAlpha) + + screenText = TextNode(f'screen-text-{len(self.gui_elements)}') + screenText.setText(text) + screenText.setAlign(TextNode.ACenter) + if self.chinese_font: + screenText.setFont(self.chinese_font) + screenTextNP = virtualScreen.attachNewNode(screenText) + screenTextNP.setPos(0, 0.01, 0) + screenTextNP.setScale(0.3) + screenTextNP.setColor(0, 1, 0, 1) + + virtualScreen.setTag("gui_type", "virtual_screen") + virtualScreen.setTag("gui_id", f"virtual_screen_{len(self.gui_elements)}") + virtualScreen.setTag("gui_text", text) + + self.gui_elements.append(virtualScreen) + print(f"创建虚拟屏幕: {text} 在世界位置 {pos}") + return virtualScreen + + def onGUIButtonClick(self, button_id): + """GUI按钮点击事件处理""" + print(f"✓ GUI按钮被点击: {button_id}") + + def onGUIEntrySubmit(self, text, entry_id): + """GUI输入框提交事件处理""" + print(f"✓ GUI输入框提交: {entry_id} = {text}") + +class QtFixedWidget(QOpenGLWidget): + """使用QOpenGLWidget的Panda3D部件""" + def __init__(self, world, parent=None): + super().__init__(parent) + self.world = world + self.setMinimumSize(800, 600) + print("Qt OpenGL部件创建完成") + + def paintGL(self): + """OpenGL绘制函数""" + if self.world and hasattr(self.world, 'graphicsEngine'): + self.world.graphicsEngine.renderFrame() + +def main(): + """主函数""" + print("启动Qt环境修复测试...") + + app = QApplication(sys.argv) + + # 创建主窗口 + mainWindow = QMainWindow() + mainWindow.setWindowTitle("Qt环境修复测试") + mainWindow.setGeometry(100, 100, 800, 600) + + # 创建布局和容器 + centralWidget = QWidget() + layout = QVBoxLayout(centralWidget) + + # 添加状态标签 + statusLabel = QLabel("Qt环境修复测试 - 应该能看到GUI元素") + statusLabel.setAlignment(Qt.AlignCenter) + statusLabel.setStyleSheet("background-color: lightgreen; padding: 10px; color: black;") + layout.addWidget(statusLabel) + + # 创建修复版本的世界 + world = QtFixedWorld() + + # 创建OpenGL部件 + openglWidget = QtFixedWidget(world) + layout.addWidget(openglWidget) + + mainWindow.setCentralWidget(centralWidget) + + # 显示窗口 + mainWindow.show() + + print("修复版本窗口已显示") + print("应该能看到:") + print("- 红色的3D平面") + print("- 黄色的3D文本") + print("- 多个2D GUI按钮(蓝色)") + print("- 2D GUI标签(白色文字)") + print("- 2D GUI输入框") + print("- 3D虚拟屏幕") + + # 运行应用 + return app.exec_() + +if __name__ == "__main__": exit_code = main() \ No newline at end of file diff --git a/demo/test_qt_gui_fix.py b/demo/test_qt_gui_fix.py index 8038cd2f..0971cade 100644 --- a/demo/test_qt_gui_fix.py +++ b/demo/test_qt_gui_fix.py @@ -1,259 +1,259 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Qt环境GUI事件修复测试 -解决Qt集成环境中DirectGUI事件处理问题 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QMouseEvent -from QMeta3D.QMeta3DWidget import QMeta3DWidget -from QMeta3D.Meta3DWorld import Meta3DWorld -from direct.gui.DirectGui import * -from direct.gui.OnscreenText import OnscreenText -from panda3d.core import * - -class QtGUIFixWorld(Meta3DWorld): - def __init__(self): - super().__init__() - - print("=== Qt GUI事件修复测试 ===") - self.setBackgroundColor(0.2, 0.2, 0.3) - - # 检查环境 - print(f"窗口类型: {type(self.win)}") - print(f"图形引擎: {self.graphicsEngine}") - - # 延迟创建GUI - self.taskMgr.doMethodLater(0.5, self.createTestGUI, "create-gui") - - # 保存GUI元素引用用于事件处理 - self.gui_elements = [] - - def createTestGUI(self, task=None): - """创建测试GUI""" - print("\n--- 创建Qt环境GUI ---") - - # 方法1: 标准DirectGUI创建 - try: - self.title = OnscreenText( - text="Qt环境GUI测试", - pos=(0, 0.8), - scale=0.08, - fg=(1, 1, 1, 1), - align=TextNode.ACenter - ) - print(f"✓ 标题创建成功: {self.title}") - except Exception as e: - print(f"❌ 标题创建失败: {str(e)}") - - # 测试按钮1 - 标准创建 - try: - self.button1 = DirectButton( - text="标准按钮", - pos=(0, 0, 0.4), - scale=0.1, - frameColor=(0, 1, 0, 1), - command=self.onButton1Click - ) - self.gui_elements.append(('button1', self.button1)) - print(f"✓ 标准按钮创建成功: {self.button1}") - except Exception as e: - print(f"❌ 标准按钮创建失败: {str(e)}") - - # 测试按钮2 - 手动事件处理 - try: - self.button2 = DirectButton( - text="修复按钮", - pos=(0, 0, 0.1), - scale=0.1, - frameColor=(1, 0, 0, 1), - command=self.onButton2Click - ) - - # 手动设置事件处理 - self.setupManualEvents(self.button2) - self.gui_elements.append(('button2', self.button2)) - print(f"✓ 修复按钮创建成功: {self.button2}") - except Exception as e: - print(f"❌ 修复按钮创建失败: {str(e)}") - - # 测试按钮3 - 自定义点击检测 - try: - self.button3 = DirectButton( - text="自检按钮", - pos=(0, 0, -0.2), - scale=0.1, - frameColor=(0, 0, 1, 1), - # 不设置command,使用自定义检测 - ) - self.gui_elements.append(('button3', self.button3)) - print(f"✓ 自检按钮创建成功: {self.button3}") - - # 启动自定义点击检测 - self.taskMgr.add(self.checkButtonClicks, "check-clicks") - - except Exception as e: - print(f"❌ 自检按钮创建失败: {str(e)}") - - return task.done if task else None - - def setupManualEvents(self, button): - """为按钮设置手动事件处理""" - try: - # 获取按钮的PGItem - if hasattr(button, 'guiItem'): - pgItem = button.guiItem - # 强制启用事件 - pgItem.setActive(True) - print(f" 手动启用按钮事件: {pgItem}") - except Exception as e: - print(f" 手动事件设置失败: {str(e)}") - - def checkButtonClicks(self, task): - """自定义按钮点击检测""" - # 检查鼠标状态 - if hasattr(self, 'mouseWatcherNode') and self.mouseWatcherNode.hasMouse(): - # 获取鼠标位置 - mouseX = self.mouseWatcherNode.getMouseX() - mouseY = self.mouseWatcherNode.getMouseY() - - # 检查是否在button3范围内 - if hasattr(self, 'button3'): - buttonPos = self.button3.getPos() - buttonScale = self.button3.getScale() - - # 简单的边界检测 - if (abs(mouseX - buttonPos.getX()) < buttonScale.getX() * 0.5 and - abs(mouseY - buttonPos.getZ()) < buttonScale.getZ() * 0.5): - - # 检查是否有鼠标点击 - if self.mouseWatcherNode.isButtonDown('button1'): - self.onButton3Click() - # 防止重复触发 - return task.pause(0.5) - - return task.cont - - def onButton1Click(self): - """标准按钮点击""" - print("🟢 标准按钮被点击!") - - def onButton2Click(self): - """修复按钮点击""" - print("🔴 修复按钮被点击!") - - def onButton3Click(self): - """自检按钮点击""" - print("🔵 自检按钮被点击!") - - def handleQtMouseEvent(self, event): - """处理Qt鼠标事件""" - try: - # 将Qt事件转换为Panda3D坐标 - if hasattr(self, 'win'): - winX = event.x() - winY = event.y() - winWidth = self.win.getXSize() - winHeight = self.win.getYSize() - - # 转换为归一化坐标 (-1 到 1) - normalizedX = (winX / winWidth) * 2.0 - 1.0 - normalizedY = 1.0 - (winY / winHeight) * 2.0 - - print(f"Qt鼠标事件: ({winX}, {winY}) -> 归一化: ({normalizedX:.3f}, {normalizedY:.3f})") - - # 手动检查GUI元素 - for name, element in self.gui_elements: - if hasattr(element, 'getPos'): - elemPos = element.getPos() - elemScale = element.getScale() - - # 简单边界检测 - if (abs(normalizedX - elemPos.getX()) < elemScale.getX() * 0.5 and - abs(normalizedY - elemPos.getZ()) < elemScale.getZ() * 0.5): - print(f" 点击在 {name} 范围内!") - - # 手动触发回调 - if name == 'button1': - self.onButton1Click() - elif name == 'button2': - self.onButton2Click() - elif name == 'button3': - self.onButton3Click() - - return True - - except Exception as e: - print(f"Qt事件处理失败: {str(e)}") - return False - -class CustomQMeta3DWidget(QMeta3DWidget): - """自定义QPanda3DWidget,支持事件转发""" - def __init__(self, world, parent=None): - super().__init__(world, parent) - self.world = world - print("自定义QPanda3D部件初始化") - - def mousePressEvent(self, event): - """重写鼠标按下事件""" - print(f"\n=== Qt鼠标按下事件 ===") - print(f"位置: ({event.x()}, {event.y()})") - print(f"按钮: {event.button()}") - - # 尝试让世界处理事件 - if hasattr(self.world, 'handleQtMouseEvent'): - handled = self.world.handleQtMouseEvent(event) - if handled: - print("事件已被世界处理") - return - - # 调用父类方法 - super().mousePressEvent(event) - -def main(): - print("启动Qt环境GUI事件修复测试...") - - app = QApplication(sys.argv) - - # 创建主窗口 - mainWindow = QMainWindow() - mainWindow.setWindowTitle("Qt GUI事件修复测试") - mainWindow.setGeometry(100, 100, 800, 600) - - # 创建布局 - centralWidget = QWidget() - layout = QVBoxLayout(centralWidget) - - # 添加说明 - label = QLabel("Qt环境DirectGUI事件修复测试") - label.setAlignment(Qt.AlignCenter) - label.setStyleSheet("background-color: lightgreen; padding: 10px;") - layout.addWidget(label) - - # 创建世界 - world = QtGUIFixWorld() - - # 创建自定义Panda3D部件 - pandaWidget = CustomQMeta3DWidget(world) - layout.addWidget(pandaWidget) - - mainWindow.setCentralWidget(centralWidget) - mainWindow.show() - - print("\n测试说明:") - print("绿色按钮: 标准DirectGUI事件处理") - print("红色按钮: 手动事件修复") - print("蓝色按钮: 自定义点击检测") - print("查看控制台输出了解事件处理情况") - - return app.exec_() - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Qt环境GUI事件修复测试 +解决Qt集成环境中DirectGUI事件处理问题 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QMouseEvent +from QMeta3D.QMeta3DWidget import QMeta3DWidget +from QMeta3D.Meta3DWorld import Meta3DWorld +from direct.gui.DirectGui import * +from direct.gui.OnscreenText import OnscreenText +from panda3d.core import * + +class QtGUIFixWorld(Meta3DWorld): + def __init__(self): + super().__init__() + + print("=== Qt GUI事件修复测试 ===") + self.setBackgroundColor(0.2, 0.2, 0.3) + + # 检查环境 + print(f"窗口类型: {type(self.win)}") + print(f"图形引擎: {self.graphicsEngine}") + + # 延迟创建GUI + self.taskMgr.doMethodLater(0.5, self.createTestGUI, "create-gui") + + # 保存GUI元素引用用于事件处理 + self.gui_elements = [] + + def createTestGUI(self, task=None): + """创建测试GUI""" + print("\n--- 创建Qt环境GUI ---") + + # 方法1: 标准DirectGUI创建 + try: + self.title = OnscreenText( + text="Qt环境GUI测试", + pos=(0, 0.8), + scale=0.08, + fg=(1, 1, 1, 1), + align=TextNode.ACenter + ) + print(f"✓ 标题创建成功: {self.title}") + except Exception as e: + print(f"❌ 标题创建失败: {str(e)}") + + # 测试按钮1 - 标准创建 + try: + self.button1 = DirectButton( + text="标准按钮", + pos=(0, 0, 0.4), + scale=0.1, + frameColor=(0, 1, 0, 1), + command=self.onButton1Click + ) + self.gui_elements.append(('button1', self.button1)) + print(f"✓ 标准按钮创建成功: {self.button1}") + except Exception as e: + print(f"❌ 标准按钮创建失败: {str(e)}") + + # 测试按钮2 - 手动事件处理 + try: + self.button2 = DirectButton( + text="修复按钮", + pos=(0, 0, 0.1), + scale=0.1, + frameColor=(1, 0, 0, 1), + command=self.onButton2Click + ) + + # 手动设置事件处理 + self.setupManualEvents(self.button2) + self.gui_elements.append(('button2', self.button2)) + print(f"✓ 修复按钮创建成功: {self.button2}") + except Exception as e: + print(f"❌ 修复按钮创建失败: {str(e)}") + + # 测试按钮3 - 自定义点击检测 + try: + self.button3 = DirectButton( + text="自检按钮", + pos=(0, 0, -0.2), + scale=0.1, + frameColor=(0, 0, 1, 1), + # 不设置command,使用自定义检测 + ) + self.gui_elements.append(('button3', self.button3)) + print(f"✓ 自检按钮创建成功: {self.button3}") + + # 启动自定义点击检测 + self.taskMgr.add(self.checkButtonClicks, "check-clicks") + + except Exception as e: + print(f"❌ 自检按钮创建失败: {str(e)}") + + return task.done if task else None + + def setupManualEvents(self, button): + """为按钮设置手动事件处理""" + try: + # 获取按钮的PGItem + if hasattr(button, 'guiItem'): + pgItem = button.guiItem + # 强制启用事件 + pgItem.setActive(True) + print(f" 手动启用按钮事件: {pgItem}") + except Exception as e: + print(f" 手动事件设置失败: {str(e)}") + + def checkButtonClicks(self, task): + """自定义按钮点击检测""" + # 检查鼠标状态 + if hasattr(self, 'mouseWatcherNode') and self.mouseWatcherNode.hasMouse(): + # 获取鼠标位置 + mouseX = self.mouseWatcherNode.getMouseX() + mouseY = self.mouseWatcherNode.getMouseY() + + # 检查是否在button3范围内 + if hasattr(self, 'button3'): + buttonPos = self.button3.getPos() + buttonScale = self.button3.getScale() + + # 简单的边界检测 + if (abs(mouseX - buttonPos.getX()) < buttonScale.getX() * 0.5 and + abs(mouseY - buttonPos.getZ()) < buttonScale.getZ() * 0.5): + + # 检查是否有鼠标点击 + if self.mouseWatcherNode.isButtonDown('button1'): + self.onButton3Click() + # 防止重复触发 + return task.pause(0.5) + + return task.cont + + def onButton1Click(self): + """标准按钮点击""" + print("🟢 标准按钮被点击!") + + def onButton2Click(self): + """修复按钮点击""" + print("🔴 修复按钮被点击!") + + def onButton3Click(self): + """自检按钮点击""" + print("🔵 自检按钮被点击!") + + def handleQtMouseEvent(self, event): + """处理Qt鼠标事件""" + try: + # 将Qt事件转换为Panda3D坐标 + if hasattr(self, 'win'): + winX = event.x() + winY = event.y() + winWidth = self.win.getXSize() + winHeight = self.win.getYSize() + + # 转换为归一化坐标 (-1 到 1) + normalizedX = (winX / winWidth) * 2.0 - 1.0 + normalizedY = 1.0 - (winY / winHeight) * 2.0 + + print(f"Qt鼠标事件: ({winX}, {winY}) -> 归一化: ({normalizedX:.3f}, {normalizedY:.3f})") + + # 手动检查GUI元素 + for name, element in self.gui_elements: + if hasattr(element, 'getPos'): + elemPos = element.getPos() + elemScale = element.getScale() + + # 简单边界检测 + if (abs(normalizedX - elemPos.getX()) < elemScale.getX() * 0.5 and + abs(normalizedY - elemPos.getZ()) < elemScale.getZ() * 0.5): + print(f" 点击在 {name} 范围内!") + + # 手动触发回调 + if name == 'button1': + self.onButton1Click() + elif name == 'button2': + self.onButton2Click() + elif name == 'button3': + self.onButton3Click() + + return True + + except Exception as e: + print(f"Qt事件处理失败: {str(e)}") + return False + +class CustomQMeta3DWidget(QMeta3DWidget): + """自定义QPanda3DWidget,支持事件转发""" + def __init__(self, world, parent=None): + super().__init__(world, parent) + self.world = world + print("自定义QPanda3D部件初始化") + + def mousePressEvent(self, event): + """重写鼠标按下事件""" + print(f"\n=== Qt鼠标按下事件 ===") + print(f"位置: ({event.x()}, {event.y()})") + print(f"按钮: {event.button()}") + + # 尝试让世界处理事件 + if hasattr(self.world, 'handleQtMouseEvent'): + handled = self.world.handleQtMouseEvent(event) + if handled: + print("事件已被世界处理") + return + + # 调用父类方法 + super().mousePressEvent(event) + +def main(): + print("启动Qt环境GUI事件修复测试...") + + app = QApplication(sys.argv) + + # 创建主窗口 + mainWindow = QMainWindow() + mainWindow.setWindowTitle("Qt GUI事件修复测试") + mainWindow.setGeometry(100, 100, 800, 600) + + # 创建布局 + centralWidget = QWidget() + layout = QVBoxLayout(centralWidget) + + # 添加说明 + label = QLabel("Qt环境DirectGUI事件修复测试") + label.setAlignment(Qt.AlignCenter) + label.setStyleSheet("background-color: lightgreen; padding: 10px;") + layout.addWidget(label) + + # 创建世界 + world = QtGUIFixWorld() + + # 创建自定义Panda3D部件 + pandaWidget = CustomQMeta3DWidget(world) + layout.addWidget(pandaWidget) + + mainWindow.setCentralWidget(centralWidget) + mainWindow.show() + + print("\n测试说明:") + print("绿色按钮: 标准DirectGUI事件处理") + print("红色按钮: 手动事件修复") + print("蓝色按钮: 自定义点击检测") + print("查看控制台输出了解事件处理情况") + + return app.exec_() + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/demo/test_qt_only.py b/demo/test_qt_only.py index 4e521b9a..aefcf021 100644 --- a/demo/test_qt_only.py +++ b/demo/test_qt_only.py @@ -1,155 +1,155 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Qt环境专用GUI测试 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel -from PyQt5.QtCore import Qt -from QMeta3D.QMeta3DWidget import QMeta3DWidget -from QMeta3D.Meta3DWorld import Meta3DWorld -from direct.gui.DirectGui import * -from direct.gui.OnscreenText import OnscreenText -from panda3d.core import * - -class QtOnlyWorld(Meta3DWorld): - def __init__(self): - super().__init__() - - print("=== 专用Qt环境测试 ===") - self.setBackgroundColor(0.2, 0.2, 0.3) - - # 检查环境差异 - print(f"基类: {self.__class__.__bases__}") - print(f"render2d: {self.render2d}") - print(f"aspect2d: {self.aspect2d}") - print(f"窗口类型: {type(self.win) if hasattr(self, 'win') else 'None'}") - - # 检查更多属性 - if hasattr(self, 'win'): - print(f"窗口: {self.win}") - print(f"窗口大小: {self.win.getXSize()} x {self.win.getYSize()}") - - print(f"图形引擎: {self.graphicsEngine}") - print(f"显示区域数量: {self.win.getNumDisplayRegions() if hasattr(self, 'win') else 'None'}") - - # 延迟创建GUI - self.taskMgr.doMethodLater(0.5, self.createTestGUI, "create-gui") - - def createTestGUI(self, task=None): - """创建测试GUI""" - print("\n--- Qt环境创建GUI ---") - - # 检查2D渲染系统状态 - print(f"render2d可用: {self.render2d is not None}") - print(f"aspect2d可用: {self.aspect2d is not None}") - print(f"camera2d可用: {hasattr(self, 'camera2d') and self.camera2d is not None}") - - if hasattr(self, 'camera2d'): - print(f"camera2d位置: {self.camera2d.getPos()}") - print(f"camera2d朝向: {self.camera2d.getHpr()}") - - # 创建标题文本 - 这通常能工作 - try: - self.title = OnscreenText( - text="Qt Only Test", - pos=(0, 0.8), - scale=0.08, - fg=(1, 1, 1, 1), - align=TextNode.ACenter - ) - print(f"✓ 标题创建成功: {self.title}") - print(f" 标题父节点: {self.title.getParent()}") - except Exception as e: - print(f"✗ 标题创建失败: {str(e)}") - - # 创建DirectButton - 问题可能在这里 - try: - self.testButton = DirectButton( - text="Test Button", - pos=(0, 0, 0), - scale=0.1, - command=self.onButtonClick, - frameColor=(1, 0, 0, 1), - text_font=None, - rolloverSound=None, - clickSound=None - ) - print(f"✓ 按钮创建成功: {self.testButton}") - print(f" 按钮父节点: {self.testButton.getParent()}") - print(f" 按钮位置: {self.testButton.getPos()}") - print(f" 按钮隐藏状态: {self.testButton.isHidden()}") - - # 检查按钮的PGButton节点 - pg_button = self.testButton.node() - print(f" PGButton节点: {pg_button}") - print(f" PGButton类型: {type(pg_button)}") - - except Exception as e: - print(f"✗ 按钮创建失败: {str(e)}") - import traceback - traceback.print_exc() - - # 检查aspect2d的子节点 - print(f"\naspect2d子节点数量: {self.aspect2d.getNumChildren()}") - for i in range(self.aspect2d.getNumChildren()): - child = self.aspect2d.getChild(i) - print(f" 子节点 {i}: {child}") - - # 尝试手动强制渲染 - try: - if hasattr(self, 'graphicsEngine'): - print("\n尝试强制渲染...") - self.graphicsEngine.renderFrame() - print("强制渲染完成") - except Exception as e: - print(f"强制渲染失败: {str(e)}") - - return task.done if task else None - - def onButtonClick(self): - print("🎯 Qt环境按钮被点击了!") - -def main(): - print("启动Qt环境专用测试...") - - app = QApplication(sys.argv) - - # 创建主窗口 - mainWindow = QMainWindow() - mainWindow.setWindowTitle("Qt专用GUI测试") - mainWindow.setGeometry(100, 100, 800, 600) - - # 创建布局 - centralWidget = QWidget() - layout = QVBoxLayout(centralWidget) - - # 添加说明 - label = QLabel("Qt环境DirectGUI专用测试 - 检查控制台输出") - label.setAlignment(Qt.AlignCenter) - label.setStyleSheet("background-color: lightcoral; padding: 10px;") - layout.addWidget(label) - - # 创建世界 - world = QtOnlyWorld() - - # 创建Panda3D部件 - pandaWidget = QMeta3DWidget(world) - layout.addWidget(pandaWidget) - - mainWindow.setCentralWidget(centralWidget) - mainWindow.show() - - print("\nQt环境窗口已显示") - print("如果能看到红色按钮,说明Qt环境下的DirectGUI是正常的") - print("如果看不到按钮但有标题,说明DirectButton在Qt环境中有问题") - - return app.exec_() - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Qt环境专用GUI测试 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel +from PyQt5.QtCore import Qt +from QMeta3D.QMeta3DWidget import QMeta3DWidget +from QMeta3D.Meta3DWorld import Meta3DWorld +from direct.gui.DirectGui import * +from direct.gui.OnscreenText import OnscreenText +from panda3d.core import * + +class QtOnlyWorld(Meta3DWorld): + def __init__(self): + super().__init__() + + print("=== 专用Qt环境测试 ===") + self.setBackgroundColor(0.2, 0.2, 0.3) + + # 检查环境差异 + print(f"基类: {self.__class__.__bases__}") + print(f"render2d: {self.render2d}") + print(f"aspect2d: {self.aspect2d}") + print(f"窗口类型: {type(self.win) if hasattr(self, 'win') else 'None'}") + + # 检查更多属性 + if hasattr(self, 'win'): + print(f"窗口: {self.win}") + print(f"窗口大小: {self.win.getXSize()} x {self.win.getYSize()}") + + print(f"图形引擎: {self.graphicsEngine}") + print(f"显示区域数量: {self.win.getNumDisplayRegions() if hasattr(self, 'win') else 'None'}") + + # 延迟创建GUI + self.taskMgr.doMethodLater(0.5, self.createTestGUI, "create-gui") + + def createTestGUI(self, task=None): + """创建测试GUI""" + print("\n--- Qt环境创建GUI ---") + + # 检查2D渲染系统状态 + print(f"render2d可用: {self.render2d is not None}") + print(f"aspect2d可用: {self.aspect2d is not None}") + print(f"camera2d可用: {hasattr(self, 'camera2d') and self.camera2d is not None}") + + if hasattr(self, 'camera2d'): + print(f"camera2d位置: {self.camera2d.getPos()}") + print(f"camera2d朝向: {self.camera2d.getHpr()}") + + # 创建标题文本 - 这通常能工作 + try: + self.title = OnscreenText( + text="Qt Only Test", + pos=(0, 0.8), + scale=0.08, + fg=(1, 1, 1, 1), + align=TextNode.ACenter + ) + print(f"✓ 标题创建成功: {self.title}") + print(f" 标题父节点: {self.title.getParent()}") + except Exception as e: + print(f"✗ 标题创建失败: {str(e)}") + + # 创建DirectButton - 问题可能在这里 + try: + self.testButton = DirectButton( + text="Test Button", + pos=(0, 0, 0), + scale=0.1, + command=self.onButtonClick, + frameColor=(1, 0, 0, 1), + text_font=None, + rolloverSound=None, + clickSound=None + ) + print(f"✓ 按钮创建成功: {self.testButton}") + print(f" 按钮父节点: {self.testButton.getParent()}") + print(f" 按钮位置: {self.testButton.getPos()}") + print(f" 按钮隐藏状态: {self.testButton.isHidden()}") + + # 检查按钮的PGButton节点 + pg_button = self.testButton.node() + print(f" PGButton节点: {pg_button}") + print(f" PGButton类型: {type(pg_button)}") + + except Exception as e: + print(f"✗ 按钮创建失败: {str(e)}") + import traceback + traceback.print_exc() + + # 检查aspect2d的子节点 + print(f"\naspect2d子节点数量: {self.aspect2d.getNumChildren()}") + for i in range(self.aspect2d.getNumChildren()): + child = self.aspect2d.getChild(i) + print(f" 子节点 {i}: {child}") + + # 尝试手动强制渲染 + try: + if hasattr(self, 'graphicsEngine'): + print("\n尝试强制渲染...") + self.graphicsEngine.renderFrame() + print("强制渲染完成") + except Exception as e: + print(f"强制渲染失败: {str(e)}") + + return task.done if task else None + + def onButtonClick(self): + print("🎯 Qt环境按钮被点击了!") + +def main(): + print("启动Qt环境专用测试...") + + app = QApplication(sys.argv) + + # 创建主窗口 + mainWindow = QMainWindow() + mainWindow.setWindowTitle("Qt专用GUI测试") + mainWindow.setGeometry(100, 100, 800, 600) + + # 创建布局 + centralWidget = QWidget() + layout = QVBoxLayout(centralWidget) + + # 添加说明 + label = QLabel("Qt环境DirectGUI专用测试 - 检查控制台输出") + label.setAlignment(Qt.AlignCenter) + label.setStyleSheet("background-color: lightcoral; padding: 10px;") + layout.addWidget(label) + + # 创建世界 + world = QtOnlyWorld() + + # 创建Panda3D部件 + pandaWidget = QMeta3DWidget(world) + layout.addWidget(pandaWidget) + + mainWindow.setCentralWidget(centralWidget) + mainWindow.show() + + print("\nQt环境窗口已显示") + print("如果能看到红色按钮,说明Qt环境下的DirectGUI是正常的") + print("如果看不到按钮但有标题,说明DirectButton在Qt环境中有问题") + + return app.exec_() + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/demo/test_qt_showbase.py b/demo/test_qt_showbase.py index 49c73f76..67fb89f4 100644 --- a/demo/test_qt_showbase.py +++ b/demo/test_qt_showbase.py @@ -1,284 +1,284 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Qt环境ShowBase测试 -使用ShowBase在独立窗口中验证GUI功能 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -from direct.showbase.ShowBase import ShowBase -from direct.gui.DirectGui import * -from direct.gui.OnscreenText import OnscreenText -from panda3d.core import * - -# 配置Panda3D -loadPrcFileData("", """ - win-size 800 600 - window-title ShowBase GUI Test - framebuffer-multisample 1 - multisamples 2 -""") - -class ShowBaseGUITest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - print("=== ShowBase GUI测试 ===") - - # 设置背景色 - self.setBackgroundColor(0.2, 0.2, 0.3) - - # 调整相机位置 - self.cam.setPos(0, -10, 5) - self.cam.lookAt(0, 0, 0) - - # 初始化GUI元素列表 - self.gui_elements = [] - - # 尝试加载中文字体 - try: - self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') - if not self.chinese_font: - print("警告: 无法加载中文字体,将使用默认字体") - except: - print("警告: 无法加载中文字体,将使用默认字体") - self.chinese_font = None - - # 创建测试内容 - self.createTestContent() - - # 立即创建GUI元素 - self.createGUIElements() - - print("\n测试说明:") - print("这是使用ShowBase的独立窗口测试") - print("如果GUI元素正常显示,说明主程序的GUI创建逻辑是正确的") - print("问题可能在于Qt集成部分") - - def createTestContent(self): - """创建测试内容""" - print("\n--- 创建3D测试内容 ---") - - # 创建地面 - cm = CardMaker("ground") - cm.setFrame(-10, 10, -10, 10) - ground = self.render.attachNewNode(cm.generate()) - ground.setP(-90) - ground.setColor(0.5, 0.5, 0.5, 1) - - # 创建一个立方体 - cube = self.loader.loadModel("models/environment") - if not cube: - # 如果找不到内置模型,创建一个简单几何体 - cm = CardMaker("test-cube") - cm.setFrame(-1, 1, -1, 1) - cube = self.render.attachNewNode(cm.generate()) - - cube.reparentTo(self.render) - cube.setPos(0, 5, 1) - cube.setColor(0.8, 0.2, 0.2, 1) - - # 添加光照 - alight = AmbientLight('alight') - alight.setColor((0.3, 0.3, 0.3, 1)) - alnp = self.render.attachNewNode(alight) - self.render.setLight(alnp) - - dlight = DirectionalLight('dlight') - dlight.setColor((0.8, 0.8, 0.8, 1)) - dlnp = self.render.attachNewNode(dlight) - dlnp.setHpr(45, -45, 0) - self.render.setLight(dlnp) - - print("3D内容创建完成") - - def createGUIElements(self): - """创建GUI元素""" - print("\n--- 创建GUI元素 ---") - - # 创建标题 - self.title = OnscreenText( - text="ShowBase GUI功能测试", - pos=(0, 0.9), - scale=0.08, - fg=(1, 1, 1, 1), - align=TextNode.ACenter, - font=self.chinese_font if self.chinese_font else None - ) - - # 测试主程序中的GUI创建方法 - print("开始创建各种GUI元素...") - - # 按钮测试 - 使用不同的逻辑坐标 - self.button1 = self.createGUIButton((0, 0, 0), "中心按钮", 0.08) - self.button2 = self.createGUIButton((-15, 0, 0), "左按钮", 0.08) - self.button3 = self.createGUIButton((15, 0, 0), "右按钮", 0.08) - self.button4 = self.createGUIButton((0, 0, 15), "上按钮", 0.08) - self.button5 = self.createGUIButton((0, 0, -15), "下按钮", 0.08) - - # 标签测试 - self.label1 = self.createGUILabel((0, 0, -25), "中心标签", 0.06) - self.label2 = self.createGUILabel((-20, 0, -10), "左标签", 0.06) - self.label3 = self.createGUILabel((20, 0, -10), "右标签", 0.06) - - # 输入框测试 - self.entry1 = self.createGUIEntry((0, 0, -35), "中心输入框", 0.05) - self.entry2 = self.createGUIEntry((-20, 0, -25), "左输入框", 0.05) - self.entry3 = self.createGUIEntry((20, 0, -25), "右输入框", 0.05) - - # 3D文本测试 - self.text3d1 = self.createGUI3DText((0, 5, 3), "主3D文本", 0.5) - self.text3d2 = self.createGUI3DText((-3, 3, 1), "左3D文本", 0.3) - self.text3d3 = self.createGUI3DText((3, 3, 1), "右3D文本", 0.3) - - # 虚拟屏幕测试 - self.screen1 = self.createGUIVirtualScreen((0, 8, 0), (3, 2), "主虚拟屏幕") - self.screen2 = self.createGUIVirtualScreen((-5, 5, 2), (2, 1), "左屏幕") - self.screen3 = self.createGUIVirtualScreen((5, 5, 2), (2, 1), "右屏幕") - - print(f"\nGUI创建完成!总计: {len(self.gui_elements)} 个元素") - print("如果你能看到所有GUI元素,说明修复是成功的!") - - # 复制主程序中的GUI创建方法 - def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1): - """创建2D GUI按钮""" - # 将3D坐标转换为2D屏幕坐标 - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - - button = DirectButton( - text=text, - pos=gui_pos, - scale=size, - command=self.onGUIButtonClick, - extraArgs=[f"button_{len(self.gui_elements)}"], - frameColor=(0.2, 0.6, 0.8, 1), - text_font=self.chinese_font if self.chinese_font else None, - rolloverSound=None, - clickSound=None - ) - - button.setTag("gui_type", "button") - button.setTag("gui_id", f"button_{len(self.gui_elements)}") - button.setTag("gui_text", text) - - self.gui_elements.append(button) - print(f"✓ 创建GUI按钮: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})") - return button - - def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08): - """创建2D GUI标签""" - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - - label = DirectLabel( - text=text, - pos=gui_pos, - scale=size, - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 1, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - label.setTag("gui_type", "label") - label.setTag("gui_id", f"label_{len(self.gui_elements)}") - label.setTag("gui_text", text) - - self.gui_elements.append(label) - print(f"✓ 创建GUI标签: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})") - return label - - def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08): - """创建2D GUI文本输入框""" - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - - entry = DirectEntry( - text="", - pos=gui_pos, - scale=size, - command=self.onGUIEntrySubmit, - extraArgs=[f"entry_{len(self.gui_elements)}"], - initialText=placeholder, - numLines=1, - width=12, - focus=0 - ) - - entry.setTag("gui_type", "entry") - entry.setTag("gui_id", f"entry_{len(self.gui_elements)}") - entry.setTag("gui_placeholder", placeholder) - - self.gui_elements.append(entry) - print(f"✓ 创建GUI输入框: {placeholder} (逻辑位置: {pos}, 屏幕位置: {gui_pos})") - return entry - - def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): - """创建3D空间文本""" - textNode = TextNode(f'3d-text-{len(self.gui_elements)}') - textNode.setText(text) - textNode.setAlign(TextNode.ACenter) - if self.chinese_font: - textNode.setFont(self.chinese_font) - - textNodePath = self.render.attachNewNode(textNode) - textNodePath.setPos(*pos) - textNodePath.setScale(size) - textNodePath.setColor(1, 1, 0, 1) - textNodePath.setBillboardAxis() - - textNodePath.setTag("gui_type", "3d_text") - textNodePath.setTag("gui_id", f"3d_text_{len(self.gui_elements)}") - textNodePath.setTag("gui_text", text) - - self.gui_elements.append(textNodePath) - print(f"✓ 创建3D文本: {text} (世界位置: {pos})") - return textNodePath - - def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"): - """创建3D虚拟屏幕""" - cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}") - cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2) - virtualScreen = self.render.attachNewNode(cm.generate()) - virtualScreen.setPos(*pos) - virtualScreen.setColor(0.2, 0.2, 0.2, 0.8) - virtualScreen.setTransparency(TransparencyAttrib.MAlpha) - - screenText = TextNode(f'screen-text-{len(self.gui_elements)}') - screenText.setText(text) - screenText.setAlign(TextNode.ACenter) - if self.chinese_font: - screenText.setFont(self.chinese_font) - screenTextNP = virtualScreen.attachNewNode(screenText) - screenTextNP.setPos(0, 0.01, 0) - screenTextNP.setScale(0.3) - screenTextNP.setColor(0, 1, 0, 1) - - virtualScreen.setTag("gui_type", "virtual_screen") - virtualScreen.setTag("gui_id", f"virtual_screen_{len(self.gui_elements)}") - virtualScreen.setTag("gui_text", text) - - self.gui_elements.append(virtualScreen) - print(f"✓ 创建虚拟屏幕: {text} (世界位置: {pos})") - return virtualScreen - - def onGUIButtonClick(self, button_id): - """GUI按钮点击事件处理""" - print(f"🎯 GUI按钮被点击: {button_id}") - - def onGUIEntrySubmit(self, text, entry_id): - """GUI输入框提交事件处理""" - print(f"📝 GUI输入框提交: {entry_id} = '{text}'") - -def main(): - """主函数""" - print("启动ShowBase GUI功能测试...") - print("这将在独立的Panda3D窗口中测试GUI功能") - - # 创建并运行应用 - app = ShowBaseGUITest() - app.run() - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Qt环境ShowBase测试 +使用ShowBase在独立窗口中验证GUI功能 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +from direct.showbase.ShowBase import ShowBase +from direct.gui.DirectGui import * +from direct.gui.OnscreenText import OnscreenText +from panda3d.core import * + +# 配置Panda3D +loadPrcFileData("", """ + win-size 800 600 + window-title ShowBase GUI Test + framebuffer-multisample 1 + multisamples 2 +""") + +class ShowBaseGUITest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + print("=== ShowBase GUI测试 ===") + + # 设置背景色 + self.setBackgroundColor(0.2, 0.2, 0.3) + + # 调整相机位置 + self.cam.setPos(0, -10, 5) + self.cam.lookAt(0, 0, 0) + + # 初始化GUI元素列表 + self.gui_elements = [] + + # 尝试加载中文字体 + try: + self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') + if not self.chinese_font: + print("警告: 无法加载中文字体,将使用默认字体") + except: + print("警告: 无法加载中文字体,将使用默认字体") + self.chinese_font = None + + # 创建测试内容 + self.createTestContent() + + # 立即创建GUI元素 + self.createGUIElements() + + print("\n测试说明:") + print("这是使用ShowBase的独立窗口测试") + print("如果GUI元素正常显示,说明主程序的GUI创建逻辑是正确的") + print("问题可能在于Qt集成部分") + + def createTestContent(self): + """创建测试内容""" + print("\n--- 创建3D测试内容 ---") + + # 创建地面 + cm = CardMaker("ground") + cm.setFrame(-10, 10, -10, 10) + ground = self.render.attachNewNode(cm.generate()) + ground.setP(-90) + ground.setColor(0.5, 0.5, 0.5, 1) + + # 创建一个立方体 + cube = self.loader.loadModel("models/environment") + if not cube: + # 如果找不到内置模型,创建一个简单几何体 + cm = CardMaker("test-cube") + cm.setFrame(-1, 1, -1, 1) + cube = self.render.attachNewNode(cm.generate()) + + cube.reparentTo(self.render) + cube.setPos(0, 5, 1) + cube.setColor(0.8, 0.2, 0.2, 1) + + # 添加光照 + alight = AmbientLight('alight') + alight.setColor((0.3, 0.3, 0.3, 1)) + alnp = self.render.attachNewNode(alight) + self.render.setLight(alnp) + + dlight = DirectionalLight('dlight') + dlight.setColor((0.8, 0.8, 0.8, 1)) + dlnp = self.render.attachNewNode(dlight) + dlnp.setHpr(45, -45, 0) + self.render.setLight(dlnp) + + print("3D内容创建完成") + + def createGUIElements(self): + """创建GUI元素""" + print("\n--- 创建GUI元素 ---") + + # 创建标题 + self.title = OnscreenText( + text="ShowBase GUI功能测试", + pos=(0, 0.9), + scale=0.08, + fg=(1, 1, 1, 1), + align=TextNode.ACenter, + font=self.chinese_font if self.chinese_font else None + ) + + # 测试主程序中的GUI创建方法 + print("开始创建各种GUI元素...") + + # 按钮测试 - 使用不同的逻辑坐标 + self.button1 = self.createGUIButton((0, 0, 0), "中心按钮", 0.08) + self.button2 = self.createGUIButton((-15, 0, 0), "左按钮", 0.08) + self.button3 = self.createGUIButton((15, 0, 0), "右按钮", 0.08) + self.button4 = self.createGUIButton((0, 0, 15), "上按钮", 0.08) + self.button5 = self.createGUIButton((0, 0, -15), "下按钮", 0.08) + + # 标签测试 + self.label1 = self.createGUILabel((0, 0, -25), "中心标签", 0.06) + self.label2 = self.createGUILabel((-20, 0, -10), "左标签", 0.06) + self.label3 = self.createGUILabel((20, 0, -10), "右标签", 0.06) + + # 输入框测试 + self.entry1 = self.createGUIEntry((0, 0, -35), "中心输入框", 0.05) + self.entry2 = self.createGUIEntry((-20, 0, -25), "左输入框", 0.05) + self.entry3 = self.createGUIEntry((20, 0, -25), "右输入框", 0.05) + + # 3D文本测试 + self.text3d1 = self.createGUI3DText((0, 5, 3), "主3D文本", 0.5) + self.text3d2 = self.createGUI3DText((-3, 3, 1), "左3D文本", 0.3) + self.text3d3 = self.createGUI3DText((3, 3, 1), "右3D文本", 0.3) + + # 虚拟屏幕测试 + self.screen1 = self.createGUIVirtualScreen((0, 8, 0), (3, 2), "主虚拟屏幕") + self.screen2 = self.createGUIVirtualScreen((-5, 5, 2), (2, 1), "左屏幕") + self.screen3 = self.createGUIVirtualScreen((5, 5, 2), (2, 1), "右屏幕") + + print(f"\nGUI创建完成!总计: {len(self.gui_elements)} 个元素") + print("如果你能看到所有GUI元素,说明修复是成功的!") + + # 复制主程序中的GUI创建方法 + def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1): + """创建2D GUI按钮""" + # 将3D坐标转换为2D屏幕坐标 + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + + button = DirectButton( + text=text, + pos=gui_pos, + scale=size, + command=self.onGUIButtonClick, + extraArgs=[f"button_{len(self.gui_elements)}"], + frameColor=(0.2, 0.6, 0.8, 1), + text_font=self.chinese_font if self.chinese_font else None, + rolloverSound=None, + clickSound=None + ) + + button.setTag("gui_type", "button") + button.setTag("gui_id", f"button_{len(self.gui_elements)}") + button.setTag("gui_text", text) + + self.gui_elements.append(button) + print(f"✓ 创建GUI按钮: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})") + return button + + def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08): + """创建2D GUI标签""" + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + + label = DirectLabel( + text=text, + pos=gui_pos, + scale=size, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + label.setTag("gui_type", "label") + label.setTag("gui_id", f"label_{len(self.gui_elements)}") + label.setTag("gui_text", text) + + self.gui_elements.append(label) + print(f"✓ 创建GUI标签: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})") + return label + + def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08): + """创建2D GUI文本输入框""" + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + + entry = DirectEntry( + text="", + pos=gui_pos, + scale=size, + command=self.onGUIEntrySubmit, + extraArgs=[f"entry_{len(self.gui_elements)}"], + initialText=placeholder, + numLines=1, + width=12, + focus=0 + ) + + entry.setTag("gui_type", "entry") + entry.setTag("gui_id", f"entry_{len(self.gui_elements)}") + entry.setTag("gui_placeholder", placeholder) + + self.gui_elements.append(entry) + print(f"✓ 创建GUI输入框: {placeholder} (逻辑位置: {pos}, 屏幕位置: {gui_pos})") + return entry + + def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): + """创建3D空间文本""" + textNode = TextNode(f'3d-text-{len(self.gui_elements)}') + textNode.setText(text) + textNode.setAlign(TextNode.ACenter) + if self.chinese_font: + textNode.setFont(self.chinese_font) + + textNodePath = self.render.attachNewNode(textNode) + textNodePath.setPos(*pos) + textNodePath.setScale(size) + textNodePath.setColor(1, 1, 0, 1) + textNodePath.setBillboardAxis() + + textNodePath.setTag("gui_type", "3d_text") + textNodePath.setTag("gui_id", f"3d_text_{len(self.gui_elements)}") + textNodePath.setTag("gui_text", text) + + self.gui_elements.append(textNodePath) + print(f"✓ 创建3D文本: {text} (世界位置: {pos})") + return textNodePath + + def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"): + """创建3D虚拟屏幕""" + cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}") + cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2) + virtualScreen = self.render.attachNewNode(cm.generate()) + virtualScreen.setPos(*pos) + virtualScreen.setColor(0.2, 0.2, 0.2, 0.8) + virtualScreen.setTransparency(TransparencyAttrib.MAlpha) + + screenText = TextNode(f'screen-text-{len(self.gui_elements)}') + screenText.setText(text) + screenText.setAlign(TextNode.ACenter) + if self.chinese_font: + screenText.setFont(self.chinese_font) + screenTextNP = virtualScreen.attachNewNode(screenText) + screenTextNP.setPos(0, 0.01, 0) + screenTextNP.setScale(0.3) + screenTextNP.setColor(0, 1, 0, 1) + + virtualScreen.setTag("gui_type", "virtual_screen") + virtualScreen.setTag("gui_id", f"virtual_screen_{len(self.gui_elements)}") + virtualScreen.setTag("gui_text", text) + + self.gui_elements.append(virtualScreen) + print(f"✓ 创建虚拟屏幕: {text} (世界位置: {pos})") + return virtualScreen + + def onGUIButtonClick(self, button_id): + """GUI按钮点击事件处理""" + print(f"🎯 GUI按钮被点击: {button_id}") + + def onGUIEntrySubmit(self, text, entry_id): + """GUI输入框提交事件处理""" + print(f"📝 GUI输入框提交: {entry_id} = '{text}'") + +def main(): + """主函数""" + print("启动ShowBase GUI功能测试...") + print("这将在独立的Panda3D窗口中测试GUI功能") + + # 创建并运行应用 + app = ShowBaseGUITest() + app.run() + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/demo/test_qt_vs_showbase.py b/demo/test_qt_vs_showbase.py index 2c9340d4..b2159631 100644 --- a/demo/test_qt_vs_showbase.py +++ b/demo/test_qt_vs_showbase.py @@ -1,224 +1,224 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -Qt环境 vs ShowBase环境 GUI对比测试 -验证两种环境中DirectGUI的差异 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel, QPushButton -from PyQt5.QtCore import Qt -from QMeta3D.QMeta3DWidget import QMeta3DWidget -from QMeta3D.Meta3DWorld import Meta3DWorld -from direct.showbase.ShowBase import ShowBase -from direct.gui.DirectGui import * -from direct.gui.OnscreenText import OnscreenText -from panda3d.core import * - -class QtTestWorld(Meta3DWorld): - """Qt集成环境测试""" - def __init__(self): - super().__init__() - - print("=== Qt环境GUI测试 ===") - self.setBackgroundColor(0.2, 0.2, 0.3) - - # 检查环境 - print(f"基类: {self.__class__.__bases__}") - print(f"render2d: {self.render2d}") - print(f"aspect2d: {self.aspect2d}") - print(f"窗口类型: {type(self.win) if hasattr(self, 'win') else 'None'}") - - # 延迟创建GUI - self.taskMgr.doMethodLater(1.0, self.createTestGUI, "create-gui") - - def createTestGUI(self, task=None): - """创建测试GUI""" - print("\n--- Qt环境创建GUI ---") - - # 完全模仿gui_3d_demo.py的创建方式 - try: - self.testButton = DirectButton( - text="Qt测试按钮", - pos=(0, 0, 0), - scale=0.1, - command=self.onButtonClick, - frameColor=(1, 0, 0, 1), # 红色,更显眼 - text_font=None - ) - print(f"✓ Qt环境按钮创建成功: {self.testButton}") - print(f" 父节点: {self.testButton.getParent()}") - print(f" 位置: {self.testButton.getPos()}") - print(f" 隐藏状态: {self.testButton.isHidden()}") - print(f" 渲染状态: {self.testButton.node().isOnstage()}") - - except Exception as e: - print(f"✗ Qt环境按钮创建失败: {str(e)}") - - # 创建标题文本 - try: - self.title = OnscreenText( - text="Qt Environment Test", - pos=(0, 0.8), - scale=0.08, - fg=(1, 1, 1, 1), - align=TextNode.ACenter - ) - print(f"✓ Qt环境标题创建成功: {self.title}") - except Exception as e: - print(f"✗ Qt环境标题创建失败: {str(e)}") - - # 检查render2d和aspect2d的状态 - print(f"\nQt环境渲染状态:") - print(f" render2d子节点数: {self.render2d.getNumChildren()}") - print(f" aspect2d子节点数: {self.aspect2d.getNumChildren()}") - - return task.done if task else None - - def onButtonClick(self): - print("🎯 Qt环境按钮被点击了!") - -class ShowBaseTestWorld(ShowBase): - """ShowBase环境测试""" - def __init__(self): - ShowBase.__init__(self) - - print("\n=== ShowBase环境GUI测试 ===") - self.setBackgroundColor(0.2, 0.2, 0.3) - - # 检查环境 - print(f"基类: {self.__class__.__bases__}") - print(f"render2d: {self.render2d}") - print(f"aspect2d: {self.aspect2d}") - print(f"窗口类型: {type(self.win)}") - - # 立即创建GUI - self.createTestGUI() - - def createTestGUI(self): - """创建测试GUI""" - print("\n--- ShowBase环境创建GUI ---") - - # 完全相同的GUI创建方式 - try: - self.testButton = DirectButton( - text="ShowBase测试按钮", - pos=(0, 0, 0), - scale=0.1, - command=self.onButtonClick, - frameColor=(0, 1, 0, 1), # 绿色,区分 - text_font=None - ) - print(f"✓ ShowBase环境按钮创建成功: {self.testButton}") - print(f" 父节点: {self.testButton.getParent()}") - print(f" 位置: {self.testButton.getPos()}") - print(f" 隐藏状态: {self.testButton.isHidden()}") - print(f" 渲染状态: {self.testButton.node().isOnstage()}") - - except Exception as e: - print(f"✗ ShowBase环境按钮创建失败: {str(e)}") - - # 创建标题文本 - try: - self.title = OnscreenText( - text="ShowBase Environment Test", - pos=(0, 0.8), - scale=0.08, - fg=(1, 1, 1, 1), - align=TextNode.ACenter - ) - print(f"✓ ShowBase环境标题创建成功: {self.title}") - except Exception as e: - print(f"✗ ShowBase环境标题创建失败: {str(e)}") - - # 检查render2d和aspect2d的状态 - print(f"\nShowBase环境渲染状态:") - print(f" render2d子节点数: {self.render2d.getNumChildren()}") - print(f" aspect2d子节点数: {self.aspect2d.getNumChildren()}") - - def onButtonClick(self): - print("🎯 ShowBase环境按钮被点击了!") - -def testQtEnvironment(): - """测试Qt环境""" - print("启动Qt环境测试...") - - app = QApplication(sys.argv) - - # 创建主窗口 - mainWindow = QMainWindow() - mainWindow.setWindowTitle("Qt环境测试") - mainWindow.setGeometry(100, 100, 600, 400) - - # 创建布局 - centralWidget = QWidget() - layout = QVBoxLayout(centralWidget) - - # 添加说明 - label = QLabel("Qt环境DirectGUI测试") - label.setAlignment(Qt.AlignCenter) - label.setStyleSheet("background-color: lightblue; padding: 10px;") - layout.addWidget(label) - - # 创建世界 - world = QtTestWorld() - - # 创建Panda3D部件 - pandaWidget = QMeta3DWidget(world) - layout.addWidget(pandaWidget) - - mainWindow.setCentralWidget(centralWidget) - mainWindow.show() - - print("Qt环境窗口已显示") - return app.exec_() - -def testShowBaseEnvironment(): - """测试ShowBase环境""" - print("\n启动ShowBase环境测试...") - - # 配置Panda3D窗口 - loadPrcFileData("", """ - win-size 600 400 - window-title ShowBase Environment Test - """) - - # 创建ShowBase应用 - app = ShowBaseTestWorld() - app.run() - -def main(): - """主函数""" - print("DirectGUI环境对比测试") - print("="*50) - - choice = input("选择测试环境:\n1. Qt环境\n2. ShowBase环境\n3. 两个都测试\n请输入(1/2/3): ").strip() - - if choice == "1": - testQtEnvironment() - elif choice == "2": - testShowBaseEnvironment() - elif choice == "3": - print("先测试ShowBase环境...") - import threading - import time - - # 先启动ShowBase环境 - showbase_thread = threading.Thread(target=testShowBaseEnvironment) - showbase_thread.daemon = True - showbase_thread.start() - - # 等待一会 - time.sleep(2) - - print("\n然后测试Qt环境...") - testQtEnvironment() - else: - print("无效选择") - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Qt环境 vs ShowBase环境 GUI对比测试 +验证两种环境中DirectGUI的差异 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel, QPushButton +from PyQt5.QtCore import Qt +from QMeta3D.QMeta3DWidget import QMeta3DWidget +from QMeta3D.Meta3DWorld import Meta3DWorld +from direct.showbase.ShowBase import ShowBase +from direct.gui.DirectGui import * +from direct.gui.OnscreenText import OnscreenText +from panda3d.core import * + +class QtTestWorld(Meta3DWorld): + """Qt集成环境测试""" + def __init__(self): + super().__init__() + + print("=== Qt环境GUI测试 ===") + self.setBackgroundColor(0.2, 0.2, 0.3) + + # 检查环境 + print(f"基类: {self.__class__.__bases__}") + print(f"render2d: {self.render2d}") + print(f"aspect2d: {self.aspect2d}") + print(f"窗口类型: {type(self.win) if hasattr(self, 'win') else 'None'}") + + # 延迟创建GUI + self.taskMgr.doMethodLater(1.0, self.createTestGUI, "create-gui") + + def createTestGUI(self, task=None): + """创建测试GUI""" + print("\n--- Qt环境创建GUI ---") + + # 完全模仿gui_3d_demo.py的创建方式 + try: + self.testButton = DirectButton( + text="Qt测试按钮", + pos=(0, 0, 0), + scale=0.1, + command=self.onButtonClick, + frameColor=(1, 0, 0, 1), # 红色,更显眼 + text_font=None + ) + print(f"✓ Qt环境按钮创建成功: {self.testButton}") + print(f" 父节点: {self.testButton.getParent()}") + print(f" 位置: {self.testButton.getPos()}") + print(f" 隐藏状态: {self.testButton.isHidden()}") + print(f" 渲染状态: {self.testButton.node().isOnstage()}") + + except Exception as e: + print(f"✗ Qt环境按钮创建失败: {str(e)}") + + # 创建标题文本 + try: + self.title = OnscreenText( + text="Qt Environment Test", + pos=(0, 0.8), + scale=0.08, + fg=(1, 1, 1, 1), + align=TextNode.ACenter + ) + print(f"✓ Qt环境标题创建成功: {self.title}") + except Exception as e: + print(f"✗ Qt环境标题创建失败: {str(e)}") + + # 检查render2d和aspect2d的状态 + print(f"\nQt环境渲染状态:") + print(f" render2d子节点数: {self.render2d.getNumChildren()}") + print(f" aspect2d子节点数: {self.aspect2d.getNumChildren()}") + + return task.done if task else None + + def onButtonClick(self): + print("🎯 Qt环境按钮被点击了!") + +class ShowBaseTestWorld(ShowBase): + """ShowBase环境测试""" + def __init__(self): + ShowBase.__init__(self) + + print("\n=== ShowBase环境GUI测试 ===") + self.setBackgroundColor(0.2, 0.2, 0.3) + + # 检查环境 + print(f"基类: {self.__class__.__bases__}") + print(f"render2d: {self.render2d}") + print(f"aspect2d: {self.aspect2d}") + print(f"窗口类型: {type(self.win)}") + + # 立即创建GUI + self.createTestGUI() + + def createTestGUI(self): + """创建测试GUI""" + print("\n--- ShowBase环境创建GUI ---") + + # 完全相同的GUI创建方式 + try: + self.testButton = DirectButton( + text="ShowBase测试按钮", + pos=(0, 0, 0), + scale=0.1, + command=self.onButtonClick, + frameColor=(0, 1, 0, 1), # 绿色,区分 + text_font=None + ) + print(f"✓ ShowBase环境按钮创建成功: {self.testButton}") + print(f" 父节点: {self.testButton.getParent()}") + print(f" 位置: {self.testButton.getPos()}") + print(f" 隐藏状态: {self.testButton.isHidden()}") + print(f" 渲染状态: {self.testButton.node().isOnstage()}") + + except Exception as e: + print(f"✗ ShowBase环境按钮创建失败: {str(e)}") + + # 创建标题文本 + try: + self.title = OnscreenText( + text="ShowBase Environment Test", + pos=(0, 0.8), + scale=0.08, + fg=(1, 1, 1, 1), + align=TextNode.ACenter + ) + print(f"✓ ShowBase环境标题创建成功: {self.title}") + except Exception as e: + print(f"✗ ShowBase环境标题创建失败: {str(e)}") + + # 检查render2d和aspect2d的状态 + print(f"\nShowBase环境渲染状态:") + print(f" render2d子节点数: {self.render2d.getNumChildren()}") + print(f" aspect2d子节点数: {self.aspect2d.getNumChildren()}") + + def onButtonClick(self): + print("🎯 ShowBase环境按钮被点击了!") + +def testQtEnvironment(): + """测试Qt环境""" + print("启动Qt环境测试...") + + app = QApplication(sys.argv) + + # 创建主窗口 + mainWindow = QMainWindow() + mainWindow.setWindowTitle("Qt环境测试") + mainWindow.setGeometry(100, 100, 600, 400) + + # 创建布局 + centralWidget = QWidget() + layout = QVBoxLayout(centralWidget) + + # 添加说明 + label = QLabel("Qt环境DirectGUI测试") + label.setAlignment(Qt.AlignCenter) + label.setStyleSheet("background-color: lightblue; padding: 10px;") + layout.addWidget(label) + + # 创建世界 + world = QtTestWorld() + + # 创建Panda3D部件 + pandaWidget = QMeta3DWidget(world) + layout.addWidget(pandaWidget) + + mainWindow.setCentralWidget(centralWidget) + mainWindow.show() + + print("Qt环境窗口已显示") + return app.exec_() + +def testShowBaseEnvironment(): + """测试ShowBase环境""" + print("\n启动ShowBase环境测试...") + + # 配置Panda3D窗口 + loadPrcFileData("", """ + win-size 600 400 + window-title ShowBase Environment Test + """) + + # 创建ShowBase应用 + app = ShowBaseTestWorld() + app.run() + +def main(): + """主函数""" + print("DirectGUI环境对比测试") + print("="*50) + + choice = input("选择测试环境:\n1. Qt环境\n2. ShowBase环境\n3. 两个都测试\n请输入(1/2/3): ").strip() + + if choice == "1": + testQtEnvironment() + elif choice == "2": + testShowBaseEnvironment() + elif choice == "3": + print("先测试ShowBase环境...") + import threading + import time + + # 先启动ShowBase环境 + showbase_thread = threading.Thread(target=testShowBaseEnvironment) + showbase_thread.daemon = True + showbase_thread.start() + + # 等待一会 + time.sleep(2) + + print("\n然后测试Qt环境...") + testQtEnvironment() + else: + print("无效选择") + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/demo/test_rotation_drag.py b/demo/test_rotation_drag.py index d1991505..d1b49926 100644 --- a/demo/test_rotation_drag.py +++ b/demo/test_rotation_drag.py @@ -1,158 +1,158 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -测试脚本:验证旋转后模型的子节点拖拽方向是否正确 -""" - -import sys -import os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - -from main import MyWorld -from panda3d.core import Vec3, Point3, CardMaker -from direct.task.TaskManagerGlobal import taskMgr -import time -import math - -def test_rotation_drag(): - """测试旋转后模型的子节点拖拽方向""" - print("=== 测试旋转后模型的子节点拖拽方向 ===") - - # 创建世界 - world = MyWorld() - - # 创建父模型(立方体) - cm_parent = CardMaker("parent_cube") - cm_parent.setFrame(-2, 2, -2, 2) - parent_model = world.render.attachNewNode(cm_parent.generate()) - parent_model.setName("parent_model") - parent_model.setPos(0, 0, 1) # 在地面上方 - parent_model.setColor(0.5, 0.5, 1, 1) # 蓝色 - - # 创建子节点(小立方体) - cm_child = CardMaker("child_cube") - cm_child.setFrame(-0.5, 0.5, -0.5, 0.5) - child_model = parent_model.attachNewNode(cm_child.generate()) - child_model.setName("child_model") - child_model.setPos(3, 0, 0) # 相对于父节点的位置(父节点右侧) - child_model.setColor(1, 0, 0, 1) # 红色 - - # 将父模型添加到模型列表 - world.models.append(parent_model) - - # 设置碰撞检测 - world.scene_manager.setupCollision(parent_model) - world.scene_manager.setupCollision(child_model) - - print(f"父模型位置: {parent_model.getPos()}") - print(f"父模型旋转: {parent_model.getHpr()}") - print(f"子模型位置: {child_model.getPos()}") - print(f"子模型世界位置: {child_model.getPos(world.render)}") - - # 测试1:选择父模型,验证其拖拽方向 - print("\n=== 测试1:选择父模型 ===") - world.selection.updateSelection(parent_model) - - def test_parent_drag(task): - print("父模型拖拽测试开始...") - - # 检查坐标轴 - if world.selection.gizmo: - gizmo_pos = world.selection.gizmo.getPos() - gizmo_hpr = world.selection.gizmo.getHpr() - print(f"父模型坐标轴位置: {gizmo_pos}") - print(f"父模型坐标轴朝向: {gizmo_hpr}") - print("✓ 父模型坐标轴应该使用世界坐标系(H=0, P=0, R=0)") - - # 等待3秒后旋转父模型 - taskMgr.doMethodLater(3.0, rotate_parent, "rotate_parent") - return task.done - - def rotate_parent(task): - print("\n=== 旋转父模型45度 ===") - parent_model.setHpr(45, 0, 0) # 绕Z轴旋转45度 - print(f"父模型新旋转: {parent_model.getHpr()}") - print(f"子模型新世界位置: {child_model.getPos(world.render)}") - - # 选择子节点进行测试 - taskMgr.doMethodLater(1.0, test_child_drag, "test_child_drag") - return task.done - - def test_child_drag(task): - print("\n=== 测试2:选择子模型 ===") - world.selection.updateSelection(child_model) - - # 检查子节点的坐标轴 - if world.selection.gizmo: - gizmo_pos = world.selection.gizmo.getPos() - gizmo_hpr = world.selection.gizmo.getHpr() - parent_hpr = parent_model.getHpr() - print(f"子模型坐标轴位置: {gizmo_pos}") - print(f"子模型坐标轴朝向: {gizmo_hpr}") - print(f"父模型朝向: {parent_hpr}") - - # 验证坐标轴朝向 - if abs(gizmo_hpr.x - parent_hpr.x) < 0.1: - print("✓ 子模型坐标轴朝向正确跟随父模型") - else: - print("✗ 子模型坐标轴朝向未跟随父模型") - - # 模拟拖拽测试 - taskMgr.doMethodLater(2.0, simulate_drag_test, "simulate_drag_test") - return task.done - - def simulate_drag_test(task): - print("\n=== 模拟拖拽测试 ===") - print("请手动测试以下操作:") - print("1. 点击子模型的红色X轴并拖拽") - print(" - 应该沿着父模型的局部X轴方向移动(已旋转45度)") - print(" - 而不是沿着世界X轴方向移动") - print("2. 点击子模型的绿色Y轴并拖拽") - print(" - 应该沿着父模型的局部Y轴方向移动(已旋转45度)") - print("3. 点击子模型的蓝色Z轴并拖拽") - print(" - 应该沿着Z轴方向移动(Z轴未旋转)") - - # 添加更多旋转测试 - taskMgr.doMethodLater(5.0, test_more_rotations, "test_more_rotations") - return task.done - - def test_more_rotations(task): - print("\n=== 测试更复杂的旋转 ===") - - # 旋转父模型到不同角度 - parent_model.setHpr(30, 45, 15) # 复杂旋转 - print(f"父模型复杂旋转: {parent_model.getHpr()}") - - # 强制更新坐标轴 - world.selection.updateSelection(child_model) - - if world.selection.gizmo: - gizmo_hpr = world.selection.gizmo.getHpr() - print(f"子模型坐标轴新朝向: {gizmo_hpr}") - print("坐标轴朝向应该与父模型一致") - - # 添加视觉验证指南 - taskMgr.doMethodLater(2.0, visual_guide, "visual_guide") - return task.done - - def visual_guide(task): - print("\n=== 视觉验证指南 ===") - print("观察要点:") - print("1. 子模型的坐标轴应该与父模型保持相同的旋转角度") - print("2. 拖拽子模型时,移动方向应该遵循坐标轴的视觉方向") - print("3. 红色X轴拖拽 → 沿红色轴方向移动") - print("4. 绿色Y轴拖拽 → 沿绿色轴方向移动") - print("5. 蓝色Z轴拖拽 → 沿蓝色轴方向移动") - print("\n如果拖拽方向与坐标轴视觉方向一致,则修复成功!") - - return task.done - - # 启动测试 - taskMgr.doMethodLater(1.0, test_parent_drag, "test_parent_drag") - - # 运行引擎 - world.run() - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +测试脚本:验证旋转后模型的子节点拖拽方向是否正确 +""" + +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from main import MyWorld +from panda3d.core import Vec3, Point3, CardMaker +from direct.task.TaskManagerGlobal import taskMgr +import time +import math + +def test_rotation_drag(): + """测试旋转后模型的子节点拖拽方向""" + print("=== 测试旋转后模型的子节点拖拽方向 ===") + + # 创建世界 + world = MyWorld() + + # 创建父模型(立方体) + cm_parent = CardMaker("parent_cube") + cm_parent.setFrame(-2, 2, -2, 2) + parent_model = world.render.attachNewNode(cm_parent.generate()) + parent_model.setName("parent_model") + parent_model.setPos(0, 0, 1) # 在地面上方 + parent_model.setColor(0.5, 0.5, 1, 1) # 蓝色 + + # 创建子节点(小立方体) + cm_child = CardMaker("child_cube") + cm_child.setFrame(-0.5, 0.5, -0.5, 0.5) + child_model = parent_model.attachNewNode(cm_child.generate()) + child_model.setName("child_model") + child_model.setPos(3, 0, 0) # 相对于父节点的位置(父节点右侧) + child_model.setColor(1, 0, 0, 1) # 红色 + + # 将父模型添加到模型列表 + world.models.append(parent_model) + + # 设置碰撞检测 + world.scene_manager.setupCollision(parent_model) + world.scene_manager.setupCollision(child_model) + + print(f"父模型位置: {parent_model.getPos()}") + print(f"父模型旋转: {parent_model.getHpr()}") + print(f"子模型位置: {child_model.getPos()}") + print(f"子模型世界位置: {child_model.getPos(world.render)}") + + # 测试1:选择父模型,验证其拖拽方向 + print("\n=== 测试1:选择父模型 ===") + world.selection.updateSelection(parent_model) + + def test_parent_drag(task): + print("父模型拖拽测试开始...") + + # 检查坐标轴 + if world.selection.gizmo: + gizmo_pos = world.selection.gizmo.getPos() + gizmo_hpr = world.selection.gizmo.getHpr() + print(f"父模型坐标轴位置: {gizmo_pos}") + print(f"父模型坐标轴朝向: {gizmo_hpr}") + print("✓ 父模型坐标轴应该使用世界坐标系(H=0, P=0, R=0)") + + # 等待3秒后旋转父模型 + taskMgr.doMethodLater(3.0, rotate_parent, "rotate_parent") + return task.done + + def rotate_parent(task): + print("\n=== 旋转父模型45度 ===") + parent_model.setHpr(45, 0, 0) # 绕Z轴旋转45度 + print(f"父模型新旋转: {parent_model.getHpr()}") + print(f"子模型新世界位置: {child_model.getPos(world.render)}") + + # 选择子节点进行测试 + taskMgr.doMethodLater(1.0, test_child_drag, "test_child_drag") + return task.done + + def test_child_drag(task): + print("\n=== 测试2:选择子模型 ===") + world.selection.updateSelection(child_model) + + # 检查子节点的坐标轴 + if world.selection.gizmo: + gizmo_pos = world.selection.gizmo.getPos() + gizmo_hpr = world.selection.gizmo.getHpr() + parent_hpr = parent_model.getHpr() + print(f"子模型坐标轴位置: {gizmo_pos}") + print(f"子模型坐标轴朝向: {gizmo_hpr}") + print(f"父模型朝向: {parent_hpr}") + + # 验证坐标轴朝向 + if abs(gizmo_hpr.x - parent_hpr.x) < 0.1: + print("✓ 子模型坐标轴朝向正确跟随父模型") + else: + print("✗ 子模型坐标轴朝向未跟随父模型") + + # 模拟拖拽测试 + taskMgr.doMethodLater(2.0, simulate_drag_test, "simulate_drag_test") + return task.done + + def simulate_drag_test(task): + print("\n=== 模拟拖拽测试 ===") + print("请手动测试以下操作:") + print("1. 点击子模型的红色X轴并拖拽") + print(" - 应该沿着父模型的局部X轴方向移动(已旋转45度)") + print(" - 而不是沿着世界X轴方向移动") + print("2. 点击子模型的绿色Y轴并拖拽") + print(" - 应该沿着父模型的局部Y轴方向移动(已旋转45度)") + print("3. 点击子模型的蓝色Z轴并拖拽") + print(" - 应该沿着Z轴方向移动(Z轴未旋转)") + + # 添加更多旋转测试 + taskMgr.doMethodLater(5.0, test_more_rotations, "test_more_rotations") + return task.done + + def test_more_rotations(task): + print("\n=== 测试更复杂的旋转 ===") + + # 旋转父模型到不同角度 + parent_model.setHpr(30, 45, 15) # 复杂旋转 + print(f"父模型复杂旋转: {parent_model.getHpr()}") + + # 强制更新坐标轴 + world.selection.updateSelection(child_model) + + if world.selection.gizmo: + gizmo_hpr = world.selection.gizmo.getHpr() + print(f"子模型坐标轴新朝向: {gizmo_hpr}") + print("坐标轴朝向应该与父模型一致") + + # 添加视觉验证指南 + taskMgr.doMethodLater(2.0, visual_guide, "visual_guide") + return task.done + + def visual_guide(task): + print("\n=== 视觉验证指南 ===") + print("观察要点:") + print("1. 子模型的坐标轴应该与父模型保持相同的旋转角度") + print("2. 拖拽子模型时,移动方向应该遵循坐标轴的视觉方向") + print("3. 红色X轴拖拽 → 沿红色轴方向移动") + print("4. 绿色Y轴拖拽 → 沿绿色轴方向移动") + print("5. 蓝色Z轴拖拽 → 沿蓝色轴方向移动") + print("\n如果拖拽方向与坐标轴视觉方向一致,则修复成功!") + + return task.done + + # 启动测试 + taskMgr.doMethodLater(1.0, test_parent_drag, "test_parent_drag") + + # 运行引擎 + world.run() + +if __name__ == "__main__": test_rotation_drag() \ No newline at end of file diff --git a/demo/test_selection_bounds.py b/demo/test_selection_bounds.py index ee49c5f0..f7787e82 100644 --- a/demo/test_selection_bounds.py +++ b/demo/test_selection_bounds.py @@ -1,359 +1,359 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -选择框边界测试脚本 - -测试保存和加载场景后选择框是否正常显示 -""" - -import sys -import os -import tempfile -import shutil - -# 添加项目路径 -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from main import MyWorld -from PyQt5.QtWidgets import QApplication - -def test_selection_bounds(): - """测试选择框边界问题""" - - print("=== 选择框边界测试 ===\n") - - # 创建应用程序 - app = QApplication.instance() - if app is None: - app = QApplication(sys.argv) - - # 创建世界 - world = MyWorld() - - return test_selection_bounds_with_world(world) - -def test_selection_bounds_with_world(world): - """使用提供的world实例测试选择框边界问题""" - - print("=== 选择框边界测试 ===\n") - - # 清理之前的测试数据 - world.models.clear() - for child in world.render.getChildren(): - if child.getName() in ["test_cube", "fbx_model", "selectionBox", "gizmo"]: - child.removeNode() - - print("1. 创建测试模型...") - - # 创建一个简单的测试立方体 - from panda3d.core import CardMaker - cm = CardMaker("test_cube") - cm.setFrame(-1, 1, -1, 1) - - # 创建立方体的6个面(使用ModelRoot确保保存/加载正常) - from panda3d.core import ModelRoot - model_root = ModelRoot("test_cube") - cube = world.render.attachNewNode(model_root) - - # 前面 - front = cube.attachNewNode(cm.generate()) - front.setP(-90) - front.setZ(1) - front.setColor(1, 0, 0, 1) - - # 后面 - back = cube.attachNewNode(cm.generate()) - back.setP(-90) - back.setZ(-1) - back.setColor(0, 1, 0, 1) - - # 设置测试缩放(模拟FBX导入后的状态) - cube.setScale(0.5, 0.5, 0.5) - cube.setPos(2, 0, 1) - - # 设置碰撞检测 - world.scene_manager.setupCollision(cube) - - # 添加到模型列表 - world.models.append(cube) - cube.setTag("file", "test_cube.fbx") - cube.setTag("is_model_root", "1") - - print("✓ 测试模型创建完成") - print(f" 位置: {cube.getPos()}") - print(f" 缩放: {cube.getScale()}") - print(f" 边界: {cube.getBounds().getMin()} 到 {cube.getBounds().getMax()}") - - print("\n2. 测试选择功能...") - - # 选择模型 - world.selection.updateSelection(cube) - - # 获取原始选择框信息 - original_bounds = cube.getBounds() - original_pos = cube.getPos() - original_scale = cube.getScale() - - print(f" 原始边界: {original_bounds.getMin()} 到 {original_bounds.getMax()}") - print(f" 原始位置: {original_pos}") - print(f" 原始缩放: {original_scale}") - - print("\n3. 保存场景...") - - # 创建临时文件 - temp_dir = tempfile.mkdtemp(prefix="bounds_test_") - scene_file = os.path.join(temp_dir, "test_scene.bam") - - try: - # 保存场景 - success = world.scene_manager.saveScene(scene_file) - if success: - print("✓ 场景保存成功") - else: - print("✗ 场景保存失败") - return False - - print("\n4. 重新加载场景...") - - # 加载场景 - success = world.scene_manager.loadScene(scene_file) - if success: - print("✓ 场景加载成功") - else: - print("✗ 场景加载失败") - return False - - print("\n5. 检查加载后的模型状态...") - - if not world.models: - print("✗ 加载后没有找到模型") - return False - - loaded_model = world.models[0] - loaded_bounds = loaded_model.getBounds() - loaded_pos = loaded_model.getPos() - loaded_scale = loaded_model.getScale() - - print(f" 加载后边界: {loaded_bounds.getMin()} 到 {loaded_bounds.getMax()}") - print(f" 加载后位置: {loaded_pos}") - print(f" 加载后缩放: {loaded_scale}") - - print("\n6. 比较结果...") - - # 检查位置是否一致 - pos_diff = (loaded_pos - original_pos).length() - scale_diff = (loaded_scale - original_scale).length() - - print(f" 位置差异: {pos_diff}") - print(f" 缩放差异: {scale_diff}") - - # 检查边界大小 - original_size = (original_bounds.getMax() - original_bounds.getMin()).length() - loaded_size = (loaded_bounds.getMax() - loaded_bounds.getMin()).length() - size_diff = abs(loaded_size - original_size) - - print(f" 原始边界大小: {original_size:.3f}") - print(f" 加载后边界大小: {loaded_size:.3f}") - print(f" 边界大小差异: {size_diff:.3f}") - - print("\n7. 测试选择框...") - - # 选择加载后的模型 - world.selection.updateSelection(loaded_model) - - # 检查选择框是否创建成功 - if world.selection.selectionBox: - print("✓ 选择框创建成功") - else: - print("✗ 选择框创建失败") - return False - - print("\n=== 测试结果 ===") - - success = True - - if pos_diff < 0.01: - print("✓ 位置信息正确恢复") - else: - print("✗ 位置信息恢复有误") - success = False - - if scale_diff < 0.01: - print("✓ 缩放信息正确恢复") - else: - print("✗ 缩放信息恢复有误") - success = False - - if size_diff < 0.1: - print("✓ 边界大小正常") - else: - print("✗ 边界大小异常") - success = False - - if success: - print("\n🎉 选择框边界测试通过!") - print("保存和加载后选择框显示正常。") - else: - print("\n❌ 选择框边界测试失败。") - print("需要进一步检查变换信息的保存和恢复。") - - return success - - except Exception as e: - print(f"测试过程中出现错误: {str(e)}") - import traceback - traceback.print_exc() - return False - - finally: - # 清理临时文件 - try: - shutil.rmtree(temp_dir) - print(f"\n清理临时文件: {temp_dir}") - except Exception as e: - print(f"清理临时文件失败: {str(e)}") - -def test_fbx_simulation(world=None): - """模拟FBX模型的缩放标准化情况""" - - print("\n=== FBX缩放标准化测试 ===\n") - - # 重用现有的world实例,避免ShowBase冲突 - if world is None: - # 创建应用程序 - app = QApplication.instance() - if app is None: - app = QApplication(sys.argv) - - # 创建世界 - world = MyWorld() - else: - # 清理之前的测试数据 - world.models.clear() - for child in world.render.getChildren(): - if child.getName() in ["test_cube", "fbx_model", "selectionBox", "gizmo"]: - child.removeNode() - - print("1. 创建模拟FBX模型(大缩放值)...") - - # 创建一个模拟FBX导入的模型结构 - from panda3d.core import CardMaker, ModelRoot - - # 创建根节点(使用ModelRoot确保保存/加载正常) - model_root_node = ModelRoot("fbx_model") - model_root = world.render.attachNewNode(model_root_node) - - # 创建子节点,模拟FBX的大缩放值 - child_node = model_root.attachNewNode("mesh_node") - child_node.setScale(100, 100, 100) # 模拟FBX的厘米到米转换问题 - - # 创建几何体 - cm = CardMaker("geometry") - cm.setFrame(-0.01, 0.01, -0.01, 0.01) # 很小的几何体,配合大缩放 - geom = child_node.attachNewNode(cm.generate()) - geom.setColor(0, 0, 1, 1) - - # 应用标准化(模拟导入时的处理) - print("2. 应用缩放标准化...") - normalize_factor = 0.01 # 1/100 - child_node.setScale(child_node.getScale() * normalize_factor) - child_node.setPos(child_node.getPos() * normalize_factor) - - print(f" 标准化后缩放: {child_node.getScale()}") - print(f" 标准化后位置: {child_node.getPos()}") - - # 设置模型根的标准变换 - model_root.setPos(0, 5, 0) - model_root.setScale(2, 2, 2) - - # 设置碰撞检测 - world.scene_manager.setupCollision(model_root) - - # 添加到模型列表 - world.models.append(model_root) - model_root.setTag("file", "test_fbx.fbx") - model_root.setTag("is_model_root", "1") - model_root.setTag("scale_normalization_applied", "true") - - print(f" 模型根位置: {model_root.getPos()}") - print(f" 模型根缩放: {model_root.getScale()}") - print(f" 模型边界: {model_root.getBounds().getMin()} 到 {model_root.getBounds().getMax()}") - - # 继续使用与前面测试相同的流程 - print("\n3. 保存和加载测试...") - - # 创建临时文件 - temp_dir = tempfile.mkdtemp(prefix="fbx_bounds_test_") - scene_file = os.path.join(temp_dir, "fbx_test_scene.bam") - - try: - # 保存场景 - world.scene_manager.saveScene(scene_file) - - # 记录原始状态 - original_bounds = model_root.getBounds() - original_pos = model_root.getPos() - original_scale = model_root.getScale() - - # 重新加载 - world.scene_manager.loadScene(scene_file) - - # 检查结果 - if world.models: - loaded_model = world.models[0] - loaded_bounds = loaded_model.getBounds() - loaded_pos = loaded_model.getPos() - loaded_scale = loaded_model.getScale() - - print(f" 原始边界大小: {(original_bounds.getMax() - original_bounds.getMin()).length():.3f}") - print(f" 加载后边界大小: {(loaded_bounds.getMax() - loaded_bounds.getMin()).length():.3f}") - - pos_diff = (loaded_pos - original_pos).length() - scale_diff = (loaded_scale - original_scale).length() - - print(f" 位置差异: {pos_diff:.6f}") - print(f" 缩放差异: {scale_diff:.6f}") - - if pos_diff < 0.01 and scale_diff < 0.01: - print("✓ FBX模拟测试通过") - return True - else: - print("✗ FBX模拟测试失败") - return False - else: - print("✗ 加载后没有找到模型") - return False - - finally: - # 清理临时文件 - try: - shutil.rmtree(temp_dir) - except: - pass - -if __name__ == "__main__": - print("选择框边界问题测试\n") - - # 创建应用程序 - app = QApplication.instance() - if app is None: - app = QApplication(sys.argv) - - # 创建世界(只创建一次) - world = MyWorld() - - # 运行基础测试 - test1_result = test_selection_bounds_with_world(world) - - # 运行FBX模拟测试 - test2_result = test_fbx_simulation(world) - - print(f"\n=== 最终测试结果 ===") - print(f"基础边界测试: {'通过' if test1_result else '失败'}") - print(f"FBX模拟测试: {'通过' if test2_result else '失败'}") - - if test1_result and test2_result: - print("\n🎉 所有测试通过!选择框边界问题已修复。") - else: +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +选择框边界测试脚本 + +测试保存和加载场景后选择框是否正常显示 +""" + +import sys +import os +import tempfile +import shutil + +# 添加项目路径 +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from main import MyWorld +from PyQt5.QtWidgets import QApplication + +def test_selection_bounds(): + """测试选择框边界问题""" + + print("=== 选择框边界测试 ===\n") + + # 创建应用程序 + app = QApplication.instance() + if app is None: + app = QApplication(sys.argv) + + # 创建世界 + world = MyWorld() + + return test_selection_bounds_with_world(world) + +def test_selection_bounds_with_world(world): + """使用提供的world实例测试选择框边界问题""" + + print("=== 选择框边界测试 ===\n") + + # 清理之前的测试数据 + world.models.clear() + for child in world.render.getChildren(): + if child.getName() in ["test_cube", "fbx_model", "selectionBox", "gizmo"]: + child.removeNode() + + print("1. 创建测试模型...") + + # 创建一个简单的测试立方体 + from panda3d.core import CardMaker + cm = CardMaker("test_cube") + cm.setFrame(-1, 1, -1, 1) + + # 创建立方体的6个面(使用ModelRoot确保保存/加载正常) + from panda3d.core import ModelRoot + model_root = ModelRoot("test_cube") + cube = world.render.attachNewNode(model_root) + + # 前面 + front = cube.attachNewNode(cm.generate()) + front.setP(-90) + front.setZ(1) + front.setColor(1, 0, 0, 1) + + # 后面 + back = cube.attachNewNode(cm.generate()) + back.setP(-90) + back.setZ(-1) + back.setColor(0, 1, 0, 1) + + # 设置测试缩放(模拟FBX导入后的状态) + cube.setScale(0.5, 0.5, 0.5) + cube.setPos(2, 0, 1) + + # 设置碰撞检测 + world.scene_manager.setupCollision(cube) + + # 添加到模型列表 + world.models.append(cube) + cube.setTag("file", "test_cube.fbx") + cube.setTag("is_model_root", "1") + + print("✓ 测试模型创建完成") + print(f" 位置: {cube.getPos()}") + print(f" 缩放: {cube.getScale()}") + print(f" 边界: {cube.getBounds().getMin()} 到 {cube.getBounds().getMax()}") + + print("\n2. 测试选择功能...") + + # 选择模型 + world.selection.updateSelection(cube) + + # 获取原始选择框信息 + original_bounds = cube.getBounds() + original_pos = cube.getPos() + original_scale = cube.getScale() + + print(f" 原始边界: {original_bounds.getMin()} 到 {original_bounds.getMax()}") + print(f" 原始位置: {original_pos}") + print(f" 原始缩放: {original_scale}") + + print("\n3. 保存场景...") + + # 创建临时文件 + temp_dir = tempfile.mkdtemp(prefix="bounds_test_") + scene_file = os.path.join(temp_dir, "test_scene.bam") + + try: + # 保存场景 + success = world.scene_manager.saveScene(scene_file) + if success: + print("✓ 场景保存成功") + else: + print("✗ 场景保存失败") + return False + + print("\n4. 重新加载场景...") + + # 加载场景 + success = world.scene_manager.loadScene(scene_file) + if success: + print("✓ 场景加载成功") + else: + print("✗ 场景加载失败") + return False + + print("\n5. 检查加载后的模型状态...") + + if not world.models: + print("✗ 加载后没有找到模型") + return False + + loaded_model = world.models[0] + loaded_bounds = loaded_model.getBounds() + loaded_pos = loaded_model.getPos() + loaded_scale = loaded_model.getScale() + + print(f" 加载后边界: {loaded_bounds.getMin()} 到 {loaded_bounds.getMax()}") + print(f" 加载后位置: {loaded_pos}") + print(f" 加载后缩放: {loaded_scale}") + + print("\n6. 比较结果...") + + # 检查位置是否一致 + pos_diff = (loaded_pos - original_pos).length() + scale_diff = (loaded_scale - original_scale).length() + + print(f" 位置差异: {pos_diff}") + print(f" 缩放差异: {scale_diff}") + + # 检查边界大小 + original_size = (original_bounds.getMax() - original_bounds.getMin()).length() + loaded_size = (loaded_bounds.getMax() - loaded_bounds.getMin()).length() + size_diff = abs(loaded_size - original_size) + + print(f" 原始边界大小: {original_size:.3f}") + print(f" 加载后边界大小: {loaded_size:.3f}") + print(f" 边界大小差异: {size_diff:.3f}") + + print("\n7. 测试选择框...") + + # 选择加载后的模型 + world.selection.updateSelection(loaded_model) + + # 检查选择框是否创建成功 + if world.selection.selectionBox: + print("✓ 选择框创建成功") + else: + print("✗ 选择框创建失败") + return False + + print("\n=== 测试结果 ===") + + success = True + + if pos_diff < 0.01: + print("✓ 位置信息正确恢复") + else: + print("✗ 位置信息恢复有误") + success = False + + if scale_diff < 0.01: + print("✓ 缩放信息正确恢复") + else: + print("✗ 缩放信息恢复有误") + success = False + + if size_diff < 0.1: + print("✓ 边界大小正常") + else: + print("✗ 边界大小异常") + success = False + + if success: + print("\n🎉 选择框边界测试通过!") + print("保存和加载后选择框显示正常。") + else: + print("\n❌ 选择框边界测试失败。") + print("需要进一步检查变换信息的保存和恢复。") + + return success + + except Exception as e: + print(f"测试过程中出现错误: {str(e)}") + import traceback + traceback.print_exc() + return False + + finally: + # 清理临时文件 + try: + shutil.rmtree(temp_dir) + print(f"\n清理临时文件: {temp_dir}") + except Exception as e: + print(f"清理临时文件失败: {str(e)}") + +def test_fbx_simulation(world=None): + """模拟FBX模型的缩放标准化情况""" + + print("\n=== FBX缩放标准化测试 ===\n") + + # 重用现有的world实例,避免ShowBase冲突 + if world is None: + # 创建应用程序 + app = QApplication.instance() + if app is None: + app = QApplication(sys.argv) + + # 创建世界 + world = MyWorld() + else: + # 清理之前的测试数据 + world.models.clear() + for child in world.render.getChildren(): + if child.getName() in ["test_cube", "fbx_model", "selectionBox", "gizmo"]: + child.removeNode() + + print("1. 创建模拟FBX模型(大缩放值)...") + + # 创建一个模拟FBX导入的模型结构 + from panda3d.core import CardMaker, ModelRoot + + # 创建根节点(使用ModelRoot确保保存/加载正常) + model_root_node = ModelRoot("fbx_model") + model_root = world.render.attachNewNode(model_root_node) + + # 创建子节点,模拟FBX的大缩放值 + child_node = model_root.attachNewNode("mesh_node") + child_node.setScale(100, 100, 100) # 模拟FBX的厘米到米转换问题 + + # 创建几何体 + cm = CardMaker("geometry") + cm.setFrame(-0.01, 0.01, -0.01, 0.01) # 很小的几何体,配合大缩放 + geom = child_node.attachNewNode(cm.generate()) + geom.setColor(0, 0, 1, 1) + + # 应用标准化(模拟导入时的处理) + print("2. 应用缩放标准化...") + normalize_factor = 0.01 # 1/100 + child_node.setScale(child_node.getScale() * normalize_factor) + child_node.setPos(child_node.getPos() * normalize_factor) + + print(f" 标准化后缩放: {child_node.getScale()}") + print(f" 标准化后位置: {child_node.getPos()}") + + # 设置模型根的标准变换 + model_root.setPos(0, 5, 0) + model_root.setScale(2, 2, 2) + + # 设置碰撞检测 + world.scene_manager.setupCollision(model_root) + + # 添加到模型列表 + world.models.append(model_root) + model_root.setTag("file", "test_fbx.fbx") + model_root.setTag("is_model_root", "1") + model_root.setTag("scale_normalization_applied", "true") + + print(f" 模型根位置: {model_root.getPos()}") + print(f" 模型根缩放: {model_root.getScale()}") + print(f" 模型边界: {model_root.getBounds().getMin()} 到 {model_root.getBounds().getMax()}") + + # 继续使用与前面测试相同的流程 + print("\n3. 保存和加载测试...") + + # 创建临时文件 + temp_dir = tempfile.mkdtemp(prefix="fbx_bounds_test_") + scene_file = os.path.join(temp_dir, "fbx_test_scene.bam") + + try: + # 保存场景 + world.scene_manager.saveScene(scene_file) + + # 记录原始状态 + original_bounds = model_root.getBounds() + original_pos = model_root.getPos() + original_scale = model_root.getScale() + + # 重新加载 + world.scene_manager.loadScene(scene_file) + + # 检查结果 + if world.models: + loaded_model = world.models[0] + loaded_bounds = loaded_model.getBounds() + loaded_pos = loaded_model.getPos() + loaded_scale = loaded_model.getScale() + + print(f" 原始边界大小: {(original_bounds.getMax() - original_bounds.getMin()).length():.3f}") + print(f" 加载后边界大小: {(loaded_bounds.getMax() - loaded_bounds.getMin()).length():.3f}") + + pos_diff = (loaded_pos - original_pos).length() + scale_diff = (loaded_scale - original_scale).length() + + print(f" 位置差异: {pos_diff:.6f}") + print(f" 缩放差异: {scale_diff:.6f}") + + if pos_diff < 0.01 and scale_diff < 0.01: + print("✓ FBX模拟测试通过") + return True + else: + print("✗ FBX模拟测试失败") + return False + else: + print("✗ 加载后没有找到模型") + return False + + finally: + # 清理临时文件 + try: + shutil.rmtree(temp_dir) + except: + pass + +if __name__ == "__main__": + print("选择框边界问题测试\n") + + # 创建应用程序 + app = QApplication.instance() + if app is None: + app = QApplication(sys.argv) + + # 创建世界(只创建一次) + world = MyWorld() + + # 运行基础测试 + test1_result = test_selection_bounds_with_world(world) + + # 运行FBX模拟测试 + test2_result = test_fbx_simulation(world) + + print(f"\n=== 最终测试结果 ===") + print(f"基础边界测试: {'通过' if test1_result else '失败'}") + print(f"FBX模拟测试: {'通过' if test2_result else '失败'}") + + if test1_result and test2_result: + print("\n🎉 所有测试通过!选择框边界问题已修复。") + else: print("\n❌ 部分测试失败,需要进一步调试。") \ No newline at end of file diff --git a/demo/test_selection_follow.py b/demo/test_selection_follow.py index 46808521..3c2bb37f 100644 --- a/demo/test_selection_follow.py +++ b/demo/test_selection_follow.py @@ -1,184 +1,184 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -测试脚本:验证子节点的选择框和坐标轴跟随父模型移动 -""" - -import sys -import os -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) - -from main import MyWorld -from panda3d.core import Vec3, Point3, CardMaker, GeomNode -from direct.task.TaskManagerGlobal import taskMgr -import time - -def getWorldCenter(nodePath, render): - """获取节点在世界坐标系中的边界框中心""" - minPoint = Point3() - maxPoint = Point3() - if nodePath.calcTightBounds(minPoint, maxPoint, render): - return Point3((minPoint.x + maxPoint.x) * 0.5, - (minPoint.y + maxPoint.y) * 0.5, - (minPoint.z + maxPoint.z) * 0.5) - return Point3(0, 0, 0) - -def test_selection_follow(): - """测试子节点选择框和坐标轴跟随父模型移动""" - print("=== 测试子节点选择框和坐标轴跟随 ===") - - # 创建世界 - world = MyWorld() - - # 创建主父节点 - parent = world.render.attachNewNode("parent_model") - - # 创建子节点1 - 一个简单的几何体 - cm1 = CardMaker("child1_card") - cm1.setFrame(-1, 1, -1, 1) - child1 = parent.attachNewNode(cm1.generate()) - child1.setName("child1") - child1.setPos(2, 0, 0) # 相对于父节点的位置 - child1.setColor(1, 0, 0, 1) # 红色 - - # 创建子节点2 - 另一个几何体 - cm2 = CardMaker("child2_card") - cm2.setFrame(-0.5, 0.5, -0.5, 0.5) - child2 = parent.attachNewNode(cm2.generate()) - child2.setName("child2") - child2.setPos(-2, 0, 1) # 相对于父节点的位置 - child2.setColor(0, 1, 0, 1) # 绿色 - - # 设置父节点的初始位置 - parent.setPos(0, 0, 0) - - # 将父节点添加到模型列表(这样它可以被选择) - world.models.append(parent) - world.models.append(child1) - world.models.append(child2) - - print(f"创建了父节点: {parent.getName()}") - print(f"创建了子节点1: {child1.getName()}, 位置: {child1.getPos()}") - print(f"创建了子节点2: {child2.getName()}, 位置: {child2.getPos()}") - - # 选择子节点1 - print("\n--- 选择子节点1 ---") - world.selection.updateSelection(child1) - - print(f"子节点1的相对边界框: {child1.getBounds()}") - - # 获取移动后的世界边界框 - minPoint = Point3() - maxPoint = Point3() - if child1.calcTightBounds(minPoint, maxPoint, world.render): - print(f"子节点1的世界边界框: min={minPoint}, max={maxPoint}") - else: - print("子节点1无法计算世界边界框") - - # 移动父节点 - print("\n--- 移动父节点到新位置 ---") - new_parent_pos = Vec3(5, 3, 2) - parent.setPos(new_parent_pos) - - print(f"父节点新位置: {parent.getPos()}") - print(f"子节点1的相对边界框: {child1.getBounds()}") - - # 获取移动后的世界边界框 - minPoint = Point3() - maxPoint = Point3() - if child1.calcTightBounds(minPoint, maxPoint, world.render): - print(f"子节点1的世界边界框: min={minPoint}, max={maxPoint}") - else: - print("子节点1无法计算世界边界框") - - # 等待一帧,让更新任务运行 - def check_after_move(task): - print("\n--- 检查移动后的状态 ---") - - # 检查选择框是否跟随 - if world.selection.selectionBox: - # 获取选择框目标的世界边界框 - minPoint = Point3() - maxPoint = Point3() - if world.selection.selectionBoxTarget.calcTightBounds(minPoint, maxPoint, world.render): - print(f"选择框目标的世界边界框: min={minPoint}, max={maxPoint}") - print(f"选择框是否存在: {world.selection.selectionBox is not None}") - - # 检查坐标轴是否跟随 - if world.selection.gizmo: - gizmo_pos = world.selection.gizmo.getPos() - print(f"坐标轴位置: {gizmo_pos}") - - # 验证坐标轴是否在正确的位置 - expected_center = getWorldCenter(child1, world.render) - print(f"期望的坐标轴位置: {expected_center}") - - # 计算位置差异 - diff = (gizmo_pos - expected_center).length() - print(f"坐标轴位置差异: {diff}") - - if diff < 0.1: # 允许小的浮点误差 - print("✓ 坐标轴正确跟随了父模型移动") - else: - print("✗ 坐标轴没有正确跟随父模型移动") - - # 再次移动父节点测试 - print("\n--- 再次移动父节点 ---") - another_pos = Vec3(-3, -2, 1) - parent.setPos(another_pos) - print(f"父节点再次移动到: {another_pos}") - - # 等待另一帧 - def final_check(task): - print("\n--- 最终检查 ---") - if world.selection.gizmo: - final_gizmo_pos = world.selection.gizmo.getPos() - final_expected_center = getWorldCenter(child1, world.render) - final_diff = (final_gizmo_pos - final_expected_center).length() - - print(f"最终坐标轴位置: {final_gizmo_pos}") - print(f"最终期望位置: {final_expected_center}") - print(f"最终位置差异: {final_diff}") - - if final_diff < 0.1: - print("✓ 测试通过:坐标轴正确跟随父模型移动") - else: - print("✗ 测试失败:坐标轴没有正确跟随") - - # 测试选择其他子节点 - print("\n--- 测试选择子节点2 ---") - world.selection.updateSelection(child2) - - def check_child2(task): - if world.selection.gizmo: - child2_gizmo_pos = world.selection.gizmo.getPos() - child2_expected_center = getWorldCenter(child2, world.render) - child2_diff = (child2_gizmo_pos - child2_expected_center).length() - - print(f"子节点2坐标轴位置: {child2_gizmo_pos}") - print(f"子节点2期望位置: {child2_expected_center}") - print(f"子节点2位置差异: {child2_diff}") - - if child2_diff < 0.1: - print("✓ 子节点2坐标轴位置正确") - else: - print("✗ 子节点2坐标轴位置错误") - - print("\n=== 测试完成 ===") - return task.done - - taskMgr.doMethodLater(0.1, check_child2, "check_child2") - return task.done - - taskMgr.doMethodLater(0.1, final_check, "final_check") - return task.done - - taskMgr.doMethodLater(0.1, check_after_move, "check_after_move") - - # 运行测试 - print("\n开始运行测试...") - world.run() - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +测试脚本:验证子节点的选择框和坐标轴跟随父模型移动 +""" + +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from main import MyWorld +from panda3d.core import Vec3, Point3, CardMaker, GeomNode +from direct.task.TaskManagerGlobal import taskMgr +import time + +def getWorldCenter(nodePath, render): + """获取节点在世界坐标系中的边界框中心""" + minPoint = Point3() + maxPoint = Point3() + if nodePath.calcTightBounds(minPoint, maxPoint, render): + return Point3((minPoint.x + maxPoint.x) * 0.5, + (minPoint.y + maxPoint.y) * 0.5, + (minPoint.z + maxPoint.z) * 0.5) + return Point3(0, 0, 0) + +def test_selection_follow(): + """测试子节点选择框和坐标轴跟随父模型移动""" + print("=== 测试子节点选择框和坐标轴跟随 ===") + + # 创建世界 + world = MyWorld() + + # 创建主父节点 + parent = world.render.attachNewNode("parent_model") + + # 创建子节点1 - 一个简单的几何体 + cm1 = CardMaker("child1_card") + cm1.setFrame(-1, 1, -1, 1) + child1 = parent.attachNewNode(cm1.generate()) + child1.setName("child1") + child1.setPos(2, 0, 0) # 相对于父节点的位置 + child1.setColor(1, 0, 0, 1) # 红色 + + # 创建子节点2 - 另一个几何体 + cm2 = CardMaker("child2_card") + cm2.setFrame(-0.5, 0.5, -0.5, 0.5) + child2 = parent.attachNewNode(cm2.generate()) + child2.setName("child2") + child2.setPos(-2, 0, 1) # 相对于父节点的位置 + child2.setColor(0, 1, 0, 1) # 绿色 + + # 设置父节点的初始位置 + parent.setPos(0, 0, 0) + + # 将父节点添加到模型列表(这样它可以被选择) + world.models.append(parent) + world.models.append(child1) + world.models.append(child2) + + print(f"创建了父节点: {parent.getName()}") + print(f"创建了子节点1: {child1.getName()}, 位置: {child1.getPos()}") + print(f"创建了子节点2: {child2.getName()}, 位置: {child2.getPos()}") + + # 选择子节点1 + print("\n--- 选择子节点1 ---") + world.selection.updateSelection(child1) + + print(f"子节点1的相对边界框: {child1.getBounds()}") + + # 获取移动后的世界边界框 + minPoint = Point3() + maxPoint = Point3() + if child1.calcTightBounds(minPoint, maxPoint, world.render): + print(f"子节点1的世界边界框: min={minPoint}, max={maxPoint}") + else: + print("子节点1无法计算世界边界框") + + # 移动父节点 + print("\n--- 移动父节点到新位置 ---") + new_parent_pos = Vec3(5, 3, 2) + parent.setPos(new_parent_pos) + + print(f"父节点新位置: {parent.getPos()}") + print(f"子节点1的相对边界框: {child1.getBounds()}") + + # 获取移动后的世界边界框 + minPoint = Point3() + maxPoint = Point3() + if child1.calcTightBounds(minPoint, maxPoint, world.render): + print(f"子节点1的世界边界框: min={minPoint}, max={maxPoint}") + else: + print("子节点1无法计算世界边界框") + + # 等待一帧,让更新任务运行 + def check_after_move(task): + print("\n--- 检查移动后的状态 ---") + + # 检查选择框是否跟随 + if world.selection.selectionBox: + # 获取选择框目标的世界边界框 + minPoint = Point3() + maxPoint = Point3() + if world.selection.selectionBoxTarget.calcTightBounds(minPoint, maxPoint, world.render): + print(f"选择框目标的世界边界框: min={minPoint}, max={maxPoint}") + print(f"选择框是否存在: {world.selection.selectionBox is not None}") + + # 检查坐标轴是否跟随 + if world.selection.gizmo: + gizmo_pos = world.selection.gizmo.getPos() + print(f"坐标轴位置: {gizmo_pos}") + + # 验证坐标轴是否在正确的位置 + expected_center = getWorldCenter(child1, world.render) + print(f"期望的坐标轴位置: {expected_center}") + + # 计算位置差异 + diff = (gizmo_pos - expected_center).length() + print(f"坐标轴位置差异: {diff}") + + if diff < 0.1: # 允许小的浮点误差 + print("✓ 坐标轴正确跟随了父模型移动") + else: + print("✗ 坐标轴没有正确跟随父模型移动") + + # 再次移动父节点测试 + print("\n--- 再次移动父节点 ---") + another_pos = Vec3(-3, -2, 1) + parent.setPos(another_pos) + print(f"父节点再次移动到: {another_pos}") + + # 等待另一帧 + def final_check(task): + print("\n--- 最终检查 ---") + if world.selection.gizmo: + final_gizmo_pos = world.selection.gizmo.getPos() + final_expected_center = getWorldCenter(child1, world.render) + final_diff = (final_gizmo_pos - final_expected_center).length() + + print(f"最终坐标轴位置: {final_gizmo_pos}") + print(f"最终期望位置: {final_expected_center}") + print(f"最终位置差异: {final_diff}") + + if final_diff < 0.1: + print("✓ 测试通过:坐标轴正确跟随父模型移动") + else: + print("✗ 测试失败:坐标轴没有正确跟随") + + # 测试选择其他子节点 + print("\n--- 测试选择子节点2 ---") + world.selection.updateSelection(child2) + + def check_child2(task): + if world.selection.gizmo: + child2_gizmo_pos = world.selection.gizmo.getPos() + child2_expected_center = getWorldCenter(child2, world.render) + child2_diff = (child2_gizmo_pos - child2_expected_center).length() + + print(f"子节点2坐标轴位置: {child2_gizmo_pos}") + print(f"子节点2期望位置: {child2_expected_center}") + print(f"子节点2位置差异: {child2_diff}") + + if child2_diff < 0.1: + print("✓ 子节点2坐标轴位置正确") + else: + print("✗ 子节点2坐标轴位置错误") + + print("\n=== 测试完成 ===") + return task.done + + taskMgr.doMethodLater(0.1, check_child2, "check_child2") + return task.done + + taskMgr.doMethodLater(0.1, final_check, "final_check") + return task.done + + taskMgr.doMethodLater(0.1, check_after_move, "check_after_move") + + # 运行测试 + print("\n开始运行测试...") + world.run() + +if __name__ == "__main__": test_selection_follow() \ No newline at end of file diff --git a/demo/test_simple_gui.py b/demo/test_simple_gui.py index d49c5feb..8b884b14 100644 --- a/demo/test_simple_gui.py +++ b/demo/test_simple_gui.py @@ -1,173 +1,173 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -简单的2D GUI测试 -基于ShowBase直接测试2D GUI组件显示 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -from direct.showbase.ShowBase import ShowBase -from direct.gui.DirectGui import * -from panda3d.core import * -import os - -class SimpleGUITest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 设置背景色 - self.setBackgroundColor(0.2, 0.2, 0.3) - - # 尝试加载中文字体 - self.chinese_font = self.loadChineseFont() - - # 创建测试GUI元素 - self.createTestGUI() - - print("简单GUI测试启动成功!") - print(f"中文字体加载状态: {'成功' if self.chinese_font else '失败,使用默认字体'}") - - def loadChineseFont(self): - """尝试加载中文字体""" - font_paths = [ - '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', - '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc' - ] - - for font_path in font_paths: - if os.path.exists(font_path): - try: - font = self.loader.loadFont(font_path) - if font: - return font - except: - continue - return None - - def createTestGUI(self): - """创建测试GUI元素""" - print("\n=== 创建2D GUI测试元素 ===") - - # 标题 - self.title = OnscreenText( - text="2D GUI 修复测试", - pos=(0, 0.9), - scale=0.08, - fg=(1, 1, 1, 1), - align=TextNode.ACenter, - font=self.chinese_font if self.chinese_font else None - ) - - # 测试修复后的坐标转换 - test_positions = [ - ("中心", (0, 0, 0)), - ("左侧", (-5, 0, 0)), - ("右侧", (5, 0, 0)), - ("上方", (0, 0, 5)), - ("下方", (0, 0, -5)) - ] - - self.buttons = [] - self.labels = [] - - for i, (name, logical_pos) in enumerate(test_positions): - # 应用修复后的坐标转换 - screen_pos = (logical_pos[0] * 0.1, 0, logical_pos[2] * 0.1) - - print(f"创建 {name} GUI元素:") - print(f" 逻辑坐标: {logical_pos}") - print(f" 屏幕坐标: {screen_pos}") - - # 创建按钮 - button = DirectButton( - text=f"{name}按钮", - pos=screen_pos, - scale=0.08, - command=self.onButtonClick, - extraArgs=[name], - frameColor=(0.2, 0.6, 0.8, 1), - text_font=self.chinese_font if self.chinese_font else None, - rolloverSound=None, - clickSound=None - ) - self.buttons.append(button) - - # 创建标签(偏移位置) - label_pos = (screen_pos[0], 0, screen_pos[2] - 0.15) - label = DirectLabel( - text=f"{name}标签", - pos=label_pos, - scale=0.06, - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 0, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - self.labels.append(label) - - print(f" 按钮已创建在: {screen_pos}") - print(f" 标签已创建在: {label_pos}") - - # 创建输入框 - self.entry = DirectEntry( - text="", - pos=(0, 0, -0.7), - scale=0.06, - command=self.onEntrySubmit, - initialText="测试输入框", - numLines=1, - width=15, - focus=0 - ) - - print(f"输入框已创建在: (0, 0, -0.7)") - - # 创建退出按钮 - self.exitButton = DirectButton( - text="退出测试", - pos=(0, 0, -0.85), - scale=0.06, - command=self.exitTest, - frameColor=(0.8, 0.2, 0.2, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - # 状态信息 - self.statusText = OnscreenText( - text="如果你能看到5个按钮和5个标签,说明2D GUI修复成功!", - pos=(0, -0.95), - scale=0.04, - fg=(0, 1, 0, 1), - align=TextNode.ACenter, - font=self.chinese_font if self.chinese_font else None - ) - - print("=== 所有GUI元素创建完成 ===") - print("如果修复成功,你应该看到:") - print("- 5个不同位置的按钮(中心、左侧、右侧、上方、下方)") - print("- 5个对应的标签") - print("- 1个输入框") - print("- 1个退出按钮") - - def onButtonClick(self, name): - """按钮点击事件""" - print(f"✓ 按钮点击测试成功: {name}") - self.statusText.setText(f"点击了 {name} 按钮 - 测试成功!") - - def onEntrySubmit(self, text): - """输入框提交事件""" - print(f"✓ 输入框测试成功: {text}") - self.statusText.setText(f"输入框内容: {text}") - - def exitTest(self): - """退出测试""" - print("退出简单GUI测试") - self.userExit() - -if __name__ == "__main__": - print("启动简单2D GUI测试...") - print("这个测试使用ShowBase基类,应该能正确显示2D GUI组件") - app = SimpleGUITest() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +简单的2D GUI测试 +基于ShowBase直接测试2D GUI组件显示 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +from direct.showbase.ShowBase import ShowBase +from direct.gui.DirectGui import * +from panda3d.core import * +import os + +class SimpleGUITest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 设置背景色 + self.setBackgroundColor(0.2, 0.2, 0.3) + + # 尝试加载中文字体 + self.chinese_font = self.loadChineseFont() + + # 创建测试GUI元素 + self.createTestGUI() + + print("简单GUI测试启动成功!") + print(f"中文字体加载状态: {'成功' if self.chinese_font else '失败,使用默认字体'}") + + def loadChineseFont(self): + """尝试加载中文字体""" + font_paths = [ + '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc', + '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc' + ] + + for font_path in font_paths: + if os.path.exists(font_path): + try: + font = self.loader.loadFont(font_path) + if font: + return font + except: + continue + return None + + def createTestGUI(self): + """创建测试GUI元素""" + print("\n=== 创建2D GUI测试元素 ===") + + # 标题 + self.title = OnscreenText( + text="2D GUI 修复测试", + pos=(0, 0.9), + scale=0.08, + fg=(1, 1, 1, 1), + align=TextNode.ACenter, + font=self.chinese_font if self.chinese_font else None + ) + + # 测试修复后的坐标转换 + test_positions = [ + ("中心", (0, 0, 0)), + ("左侧", (-5, 0, 0)), + ("右侧", (5, 0, 0)), + ("上方", (0, 0, 5)), + ("下方", (0, 0, -5)) + ] + + self.buttons = [] + self.labels = [] + + for i, (name, logical_pos) in enumerate(test_positions): + # 应用修复后的坐标转换 + screen_pos = (logical_pos[0] * 0.1, 0, logical_pos[2] * 0.1) + + print(f"创建 {name} GUI元素:") + print(f" 逻辑坐标: {logical_pos}") + print(f" 屏幕坐标: {screen_pos}") + + # 创建按钮 + button = DirectButton( + text=f"{name}按钮", + pos=screen_pos, + scale=0.08, + command=self.onButtonClick, + extraArgs=[name], + frameColor=(0.2, 0.6, 0.8, 1), + text_font=self.chinese_font if self.chinese_font else None, + rolloverSound=None, + clickSound=None + ) + self.buttons.append(button) + + # 创建标签(偏移位置) + label_pos = (screen_pos[0], 0, screen_pos[2] - 0.15) + label = DirectLabel( + text=f"{name}标签", + pos=label_pos, + scale=0.06, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 0, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + self.labels.append(label) + + print(f" 按钮已创建在: {screen_pos}") + print(f" 标签已创建在: {label_pos}") + + # 创建输入框 + self.entry = DirectEntry( + text="", + pos=(0, 0, -0.7), + scale=0.06, + command=self.onEntrySubmit, + initialText="测试输入框", + numLines=1, + width=15, + focus=0 + ) + + print(f"输入框已创建在: (0, 0, -0.7)") + + # 创建退出按钮 + self.exitButton = DirectButton( + text="退出测试", + pos=(0, 0, -0.85), + scale=0.06, + command=self.exitTest, + frameColor=(0.8, 0.2, 0.2, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + # 状态信息 + self.statusText = OnscreenText( + text="如果你能看到5个按钮和5个标签,说明2D GUI修复成功!", + pos=(0, -0.95), + scale=0.04, + fg=(0, 1, 0, 1), + align=TextNode.ACenter, + font=self.chinese_font if self.chinese_font else None + ) + + print("=== 所有GUI元素创建完成 ===") + print("如果修复成功,你应该看到:") + print("- 5个不同位置的按钮(中心、左侧、右侧、上方、下方)") + print("- 5个对应的标签") + print("- 1个输入框") + print("- 1个退出按钮") + + def onButtonClick(self, name): + """按钮点击事件""" + print(f"✓ 按钮点击测试成功: {name}") + self.statusText.setText(f"点击了 {name} 按钮 - 测试成功!") + + def onEntrySubmit(self, text): + """输入框提交事件""" + print(f"✓ 输入框测试成功: {text}") + self.statusText.setText(f"输入框内容: {text}") + + def exitTest(self): + """退出测试""" + print("退出简单GUI测试") + self.userExit() + +if __name__ == "__main__": + print("启动简单2D GUI测试...") + print("这个测试使用ShowBase基类,应该能正确显示2D GUI组件") + app = SimpleGUITest() app.run() \ No newline at end of file diff --git a/demo/test_simplified_gizmo.py b/demo/test_simplified_gizmo.py index 9246ada7..3c04001b 100644 --- a/demo/test_simplified_gizmo.py +++ b/demo/test_simplified_gizmo.py @@ -1,61 +1,61 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -测试简化后的坐标轴系统 -验证选中物体自动显示坐标轴功能 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel, QPushButton, QHBoxLayout -from PyQt5.QtCore import Qt -from QMeta3D.QMeta3DWidget import QMeta3DWidget -sys.path.append('..') -from main import MyWorld, CustomMeta3DWidget - -def main(): - print("启动简化坐标轴测试...") - - app = QApplication(sys.argv) - - # 创建主窗口 - mainWindow = QMainWindow() - mainWindow.setWindowTitle("简化坐标轴测试 - 选中物体自动显示坐标轴") - mainWindow.setGeometry(100, 100, 1000, 800) - - # 创建布局 - centralWidget = QWidget() - layout = QVBoxLayout(centralWidget) - - # 添加说明 - infoLayout = QHBoxLayout() - label = QLabel("简化坐标轴测试 - 点击任何物体都会自动显示坐标轴,无需移动工具") - label.setAlignment(Qt.AlignCenter) - label.setStyleSheet("background-color: lightgreen; padding: 10px; font-size: 12px;") - infoLayout.addWidget(label) - - layout.addLayout(infoLayout) - - # 创建世界 - world = MyWorld() - - # 创建部件 - pandaWidget = CustomMeta3DWidget(world) - layout.addWidget(pandaWidget) - - mainWindow.setCentralWidget(centralWidget) - mainWindow.show() - - print("\n测试说明:") - print("1. 程序启动后会显示一个空的3D场景") - print("2. 可以导入模型或创建基本几何体") - print("3. 点击任何物体都会自动显示坐标轴") - print("4. 不再需要选择移动工具") - print("5. 直接拖拽坐标轴即可移动物体") - - return app.exec_() - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +测试简化后的坐标轴系统 +验证选中物体自动显示坐标轴功能 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel, QPushButton, QHBoxLayout +from PyQt5.QtCore import Qt +from QMeta3D.QMeta3DWidget import QMeta3DWidget +sys.path.append('..') +from main import MyWorld, CustomMeta3DWidget + +def main(): + print("启动简化坐标轴测试...") + + app = QApplication(sys.argv) + + # 创建主窗口 + mainWindow = QMainWindow() + mainWindow.setWindowTitle("简化坐标轴测试 - 选中物体自动显示坐标轴") + mainWindow.setGeometry(100, 100, 1000, 800) + + # 创建布局 + centralWidget = QWidget() + layout = QVBoxLayout(centralWidget) + + # 添加说明 + infoLayout = QHBoxLayout() + label = QLabel("简化坐标轴测试 - 点击任何物体都会自动显示坐标轴,无需移动工具") + label.setAlignment(Qt.AlignCenter) + label.setStyleSheet("background-color: lightgreen; padding: 10px; font-size: 12px;") + infoLayout.addWidget(label) + + layout.addLayout(infoLayout) + + # 创建世界 + world = MyWorld() + + # 创建部件 + pandaWidget = CustomMeta3DWidget(world) + layout.addWidget(pandaWidget) + + mainWindow.setCentralWidget(centralWidget) + mainWindow.show() + + print("\n测试说明:") + print("1. 程序启动后会显示一个空的3D场景") + print("2. 可以导入模型或创建基本几何体") + print("3. 点击任何物体都会自动显示坐标轴") + print("4. 不再需要选择移动工具") + print("5. 直接拖拽坐标轴即可移动物体") + + return app.exec_() + +if __name__ == "__main__": sys.exit(main()) \ No newline at end of file diff --git a/demo/test_size_fix.py b/demo/test_size_fix.py index 6904e5fd..f5822de4 100644 --- a/demo/test_size_fix.py +++ b/demo/test_size_fix.py @@ -1,158 +1,158 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -测试Qt环境下窗口尺寸获取修复 -验证从Qt部件获取准确尺寸的功能 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel -from PyQt5.QtCore import Qt -from QMeta3D.QMeta3DWidget import QMeta3DWidget -from QMeta3D.Meta3DWorld import Meta3DWorld -from panda3d.core import * -from direct.task import Task - -class SizeTestWorld(Meta3DWorld): - def __init__(self): - super().__init__() - - print("=== Qt窗口尺寸测试 ===") - self.setBackgroundColor(0.2, 0.2, 0.3) - - # Qt部件引用 - self.qtWidget = None - - # 启动定时检查任务 - self.taskMgr.doMethodLater(2.0, self.checkSizes, "check-sizes") - - def setQtWidget(self, widget): - """设置Qt部件引用""" - self.qtWidget = widget - print(f"✓ 设置Qt部件引用: {widget}") - print(f" Qt部件类型: {type(widget)}") - - def getWindowSize(self): - """获取准确的窗口尺寸""" - if self.qtWidget: - # 优先使用Qt部件的实际尺寸 - width, height = self.qtWidget.getActualSize() - if width > 0 and height > 0: - print(f"✓ 从Qt部件获取窗口尺寸: {width} x {height}") - return width, height - - # 备用方案:使用Panda3D窗口尺寸 - if hasattr(self, 'win') and self.win: - width = self.win.getXSize() - height = self.win.getYSize() - print(f"⚠ 从Panda3D窗口获取尺寸: {width} x {height}") - return width, height - - # 最后的默认值 - print("⚠ 使用默认窗口尺寸: 800 x 600") - return 800, 600 - - def checkSizes(self, task): - """检查各种尺寸获取方法的结果""" - print("\n=== 窗口尺寸对比测试 ===") - - # 方法1:从Qt部件获取 - if self.qtWidget: - qt_width, qt_height = self.qtWidget.getActualSize() - print(f"Qt部件尺寸: {qt_width} x {qt_height}") - else: - print("Qt部件尺寸: 未设置") - - # 方法2:从Panda3D窗口获取 - if hasattr(self, 'win') and self.win: - panda_width = self.win.getXSize() - panda_height = self.win.getYSize() - print(f"Panda3D窗口尺寸: {panda_width} x {panda_height}") - else: - print("Panda3D窗口尺寸: 无效") - - # 方法3:使用新的getWindowSize方法 - final_width, final_height = self.getWindowSize() - print(f"最终使用尺寸: {final_width} x {final_height}") - - # 检查是否有差异 - if self.qtWidget and hasattr(self, 'win') and self.win: - qt_width, qt_height = self.qtWidget.getActualSize() - panda_width = self.win.getXSize() - panda_height = self.win.getYSize() - - if qt_width != panda_width or qt_height != panda_height: - print(f"⚠ 发现尺寸差异!Qt: {qt_width}x{qt_height}, Panda3D: {panda_width}x{panda_height}") - print("这就是之前坐标轴点击检测失败的原因!") - else: - print("✓ 尺寸一致,没有问题") - - # 每5秒检查一次 - return task.again - -class CustomSizeTestWidget(QMeta3DWidget): - """支持尺寸获取的测试部件""" - def __init__(self, world, parent=None): - super().__init__(world, parent) - self.world = world - - # 让world引用这个widget - if hasattr(world, 'setQtWidget'): - world.setQtWidget(self) - - def getActualSize(self): - """获取Qt部件的实际渲染尺寸""" - return (self.width(), self.height()) - - def resizeEvent(self, event): - """处理窗口大小改变事件""" - super().resizeEvent(event) - print(f"\n窗口大小改变: {self.width()} x {self.height()}") - - # 触发新的尺寸检查 - if hasattr(self.world, 'checkSizes'): - self.world.taskMgr.doMethodLater(0.1, self.world.checkSizes, "check-sizes-after-resize") - -def main(): - print("启动Qt窗口尺寸测试...") - - app = QApplication(sys.argv) - - # 创建主窗口 - mainWindow = QMainWindow() - mainWindow.setWindowTitle("Qt窗口尺寸测试 - 坐标轴修复验证") - mainWindow.setGeometry(100, 100, 900, 700) # 设置一个明确的大小 - - # 创建布局 - centralWidget = QWidget() - layout = QVBoxLayout(centralWidget) - - # 添加说明 - label = QLabel("Qt窗口尺寸测试 - 查看控制台输出") - label.setAlignment(Qt.AlignCenter) - label.setStyleSheet("background-color: lightgreen; padding: 10px; font-size: 14px;") - layout.addWidget(label) - - # 创建世界 - world = SizeTestWorld() - - # 创建测试部件 - pandaWidget = CustomSizeTestWidget(world) - layout.addWidget(pandaWidget) - - mainWindow.setCentralWidget(centralWidget) - mainWindow.show() - - print("\n测试说明:") - print("1. 程序会每5秒检查一次窗口尺寸") - print("2. 尝试调整窗口大小,观察尺寸变化") - print("3. 如果Qt尺寸和Panda3D尺寸不同,说明修复有效") - print("4. 如果尺寸一致,说明你的环境没有这个问题") - - return app.exec_() - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +测试Qt环境下窗口尺寸获取修复 +验证从Qt部件获取准确尺寸的功能 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel +from PyQt5.QtCore import Qt +from QMeta3D.QMeta3DWidget import QMeta3DWidget +from QMeta3D.Meta3DWorld import Meta3DWorld +from panda3d.core import * +from direct.task import Task + +class SizeTestWorld(Meta3DWorld): + def __init__(self): + super().__init__() + + print("=== Qt窗口尺寸测试 ===") + self.setBackgroundColor(0.2, 0.2, 0.3) + + # Qt部件引用 + self.qtWidget = None + + # 启动定时检查任务 + self.taskMgr.doMethodLater(2.0, self.checkSizes, "check-sizes") + + def setQtWidget(self, widget): + """设置Qt部件引用""" + self.qtWidget = widget + print(f"✓ 设置Qt部件引用: {widget}") + print(f" Qt部件类型: {type(widget)}") + + def getWindowSize(self): + """获取准确的窗口尺寸""" + if self.qtWidget: + # 优先使用Qt部件的实际尺寸 + width, height = self.qtWidget.getActualSize() + if width > 0 and height > 0: + print(f"✓ 从Qt部件获取窗口尺寸: {width} x {height}") + return width, height + + # 备用方案:使用Panda3D窗口尺寸 + if hasattr(self, 'win') and self.win: + width = self.win.getXSize() + height = self.win.getYSize() + print(f"⚠ 从Panda3D窗口获取尺寸: {width} x {height}") + return width, height + + # 最后的默认值 + print("⚠ 使用默认窗口尺寸: 800 x 600") + return 800, 600 + + def checkSizes(self, task): + """检查各种尺寸获取方法的结果""" + print("\n=== 窗口尺寸对比测试 ===") + + # 方法1:从Qt部件获取 + if self.qtWidget: + qt_width, qt_height = self.qtWidget.getActualSize() + print(f"Qt部件尺寸: {qt_width} x {qt_height}") + else: + print("Qt部件尺寸: 未设置") + + # 方法2:从Panda3D窗口获取 + if hasattr(self, 'win') and self.win: + panda_width = self.win.getXSize() + panda_height = self.win.getYSize() + print(f"Panda3D窗口尺寸: {panda_width} x {panda_height}") + else: + print("Panda3D窗口尺寸: 无效") + + # 方法3:使用新的getWindowSize方法 + final_width, final_height = self.getWindowSize() + print(f"最终使用尺寸: {final_width} x {final_height}") + + # 检查是否有差异 + if self.qtWidget and hasattr(self, 'win') and self.win: + qt_width, qt_height = self.qtWidget.getActualSize() + panda_width = self.win.getXSize() + panda_height = self.win.getYSize() + + if qt_width != panda_width or qt_height != panda_height: + print(f"⚠ 发现尺寸差异!Qt: {qt_width}x{qt_height}, Panda3D: {panda_width}x{panda_height}") + print("这就是之前坐标轴点击检测失败的原因!") + else: + print("✓ 尺寸一致,没有问题") + + # 每5秒检查一次 + return task.again + +class CustomSizeTestWidget(QMeta3DWidget): + """支持尺寸获取的测试部件""" + def __init__(self, world, parent=None): + super().__init__(world, parent) + self.world = world + + # 让world引用这个widget + if hasattr(world, 'setQtWidget'): + world.setQtWidget(self) + + def getActualSize(self): + """获取Qt部件的实际渲染尺寸""" + return (self.width(), self.height()) + + def resizeEvent(self, event): + """处理窗口大小改变事件""" + super().resizeEvent(event) + print(f"\n窗口大小改变: {self.width()} x {self.height()}") + + # 触发新的尺寸检查 + if hasattr(self.world, 'checkSizes'): + self.world.taskMgr.doMethodLater(0.1, self.world.checkSizes, "check-sizes-after-resize") + +def main(): + print("启动Qt窗口尺寸测试...") + + app = QApplication(sys.argv) + + # 创建主窗口 + mainWindow = QMainWindow() + mainWindow.setWindowTitle("Qt窗口尺寸测试 - 坐标轴修复验证") + mainWindow.setGeometry(100, 100, 900, 700) # 设置一个明确的大小 + + # 创建布局 + centralWidget = QWidget() + layout = QVBoxLayout(centralWidget) + + # 添加说明 + label = QLabel("Qt窗口尺寸测试 - 查看控制台输出") + label.setAlignment(Qt.AlignCenter) + label.setStyleSheet("background-color: lightgreen; padding: 10px; font-size: 14px;") + layout.addWidget(label) + + # 创建世界 + world = SizeTestWorld() + + # 创建测试部件 + pandaWidget = CustomSizeTestWidget(world) + layout.addWidget(pandaWidget) + + mainWindow.setCentralWidget(centralWidget) + mainWindow.show() + + print("\n测试说明:") + print("1. 程序会每5秒检查一次窗口尺寸") + print("2. 尝试调整窗口大小,观察尺寸变化") + print("3. 如果Qt尺寸和Panda3D尺寸不同,说明修复有效") + print("4. 如果尺寸一致,说明你的环境没有这个问题") + + return app.exec_() + +if __name__ == "__main__": sys.exit(main()) \ No newline at end of file diff --git a/demo/test_toolbar_gui.py b/demo/test_toolbar_gui.py index c6059a03..2721686f 100644 --- a/demo/test_toolbar_gui.py +++ b/demo/test_toolbar_gui.py @@ -1,91 +1,91 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -工具栏GUI测试 -模拟用户点击工具栏按钮创建GUI元素 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -from PyQt5.QtWidgets import QApplication, QMainWindow -from PyQt5.QtCore import QTimer - -# 导入主程序 -from test import MyWorld, CustomPanda3DWidget - -def testToolbarGUI(): - """测试工具栏GUI创建功能""" - print("=== 工具栏GUI创建测试 ===") - - app = QApplication(sys.argv) - - # 创建主程序的世界对象 - world = MyWorld() - - # 创建窗口 - mainWindow = QMainWindow() - mainWindow.setWindowTitle("工具栏GUI测试") - mainWindow.setGeometry(100, 100, 800, 600) - - # 创建Panda3D部件 - pandaWidget = CustomPanda3DWidget(world) - mainWindow.setCentralWidget(pandaWidget) - - # 显示窗口 - mainWindow.show() - - # 模拟工具栏按钮点击 - def createGUIElements(): - print("\n模拟工具栏按钮点击...") - - # 创建一些GUI元素,使用合理的坐标 - print("创建GUI按钮...") - button1 = world.createGUIButton((0, 0, 0), "中心按钮", 0.08) - print(f"按钮1屏幕位置: {button1.getPos()}") - - button2 = world.createGUIButton((-10, 0, 0), "左按钮", 0.08) - print(f"按钮2屏幕位置: {button2.getPos()}") - - button3 = world.createGUIButton((10, 0, 0), "右按钮", 0.08) - print(f"按钮3屏幕位置: {button3.getPos()}") - - print("创建GUI标签...") - label1 = world.createGUILabel((0, 0, -10), "测试标签", 0.06) - print(f"标签1屏幕位置: {label1.getPos()}") - - print("创建GUI输入框...") - entry1 = world.createGUIEntry((0, 0, -15), "输入测试", 0.05) - print(f"输入框1屏幕位置: {entry1.getPos()}") - - print("创建3D文本...") - text3d = world.createGUI3DText((0, 5, 2), "3D测试", 0.5) - print(f"3D文本位置: {text3d.getPos()}") - - print(f"\n总共创建了 {len(world.gui_elements)} 个GUI元素") - - # 检查所有坐标是否在有效范围内 - print("\n检查2D GUI坐标范围:") - for i, element in enumerate(world.gui_elements): - if hasattr(element, 'getTag'): - gui_type = element.getTag("gui_type") - if gui_type in ["button", "label", "entry"]: - pos = element.getPos() - x, z = pos.getX(), pos.getZ() - in_range = (-1 <= x <= 1) and (-1 <= z <= 1) - print(f" {gui_type}: ({x:.3f}, {z:.3f}) - {'✓' if in_range else '✗ 超出范围'}") - - print("\n如果你能看到蓝色按钮、白色标签和输入框,说明修复成功!") - print("如果只能看到黄色3D文本,说明2D GUI坐标可能仍有问题。") - - # 1秒后创建GUI元素 - QTimer.singleShot(1000, createGUIElements) - - print("窗口已显示,即将测试GUI创建...") - - return app.exec_() - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +工具栏GUI测试 +模拟用户点击工具栏按钮创建GUI元素 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +from PyQt5.QtWidgets import QApplication, QMainWindow +from PyQt5.QtCore import QTimer + +# 导入主程序 +from test import MyWorld, CustomPanda3DWidget + +def testToolbarGUI(): + """测试工具栏GUI创建功能""" + print("=== 工具栏GUI创建测试 ===") + + app = QApplication(sys.argv) + + # 创建主程序的世界对象 + world = MyWorld() + + # 创建窗口 + mainWindow = QMainWindow() + mainWindow.setWindowTitle("工具栏GUI测试") + mainWindow.setGeometry(100, 100, 800, 600) + + # 创建Panda3D部件 + pandaWidget = CustomPanda3DWidget(world) + mainWindow.setCentralWidget(pandaWidget) + + # 显示窗口 + mainWindow.show() + + # 模拟工具栏按钮点击 + def createGUIElements(): + print("\n模拟工具栏按钮点击...") + + # 创建一些GUI元素,使用合理的坐标 + print("创建GUI按钮...") + button1 = world.createGUIButton((0, 0, 0), "中心按钮", 0.08) + print(f"按钮1屏幕位置: {button1.getPos()}") + + button2 = world.createGUIButton((-10, 0, 0), "左按钮", 0.08) + print(f"按钮2屏幕位置: {button2.getPos()}") + + button3 = world.createGUIButton((10, 0, 0), "右按钮", 0.08) + print(f"按钮3屏幕位置: {button3.getPos()}") + + print("创建GUI标签...") + label1 = world.createGUILabel((0, 0, -10), "测试标签", 0.06) + print(f"标签1屏幕位置: {label1.getPos()}") + + print("创建GUI输入框...") + entry1 = world.createGUIEntry((0, 0, -15), "输入测试", 0.05) + print(f"输入框1屏幕位置: {entry1.getPos()}") + + print("创建3D文本...") + text3d = world.createGUI3DText((0, 5, 2), "3D测试", 0.5) + print(f"3D文本位置: {text3d.getPos()}") + + print(f"\n总共创建了 {len(world.gui_elements)} 个GUI元素") + + # 检查所有坐标是否在有效范围内 + print("\n检查2D GUI坐标范围:") + for i, element in enumerate(world.gui_elements): + if hasattr(element, 'getTag'): + gui_type = element.getTag("gui_type") + if gui_type in ["button", "label", "entry"]: + pos = element.getPos() + x, z = pos.getX(), pos.getZ() + in_range = (-1 <= x <= 1) and (-1 <= z <= 1) + print(f" {gui_type}: ({x:.3f}, {z:.3f}) - {'✓' if in_range else '✗ 超出范围'}") + + print("\n如果你能看到蓝色按钮、白色标签和输入框,说明修复成功!") + print("如果只能看到黄色3D文本,说明2D GUI坐标可能仍有问题。") + + # 1秒后创建GUI元素 + QTimer.singleShot(1000, createGUIElements) + + print("窗口已显示,即将测试GUI创建...") + + return app.exec_() + +if __name__ == "__main__": testToolbarGUI() \ No newline at end of file diff --git a/demo/test_video_fix.py b/demo/test_video_fix.py index b448dff9..144f3918 100644 --- a/demo/test_video_fix.py +++ b/demo/test_video_fix.py @@ -1,102 +1,102 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -视频播放功能修复测试 -""" - -from direct.showbase.ShowBase import ShowBase -from panda3d.core import loadPrcFileData -import sys - -# 配置窗口 -loadPrcFileData("", """ - win-size 800 600 - window-title 视频功能测试 -""") - -class VideoTest(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 初始化属性以兼容 VideoManager - self.models = [] # 模型列表 - - print("🚀 开始视频功能测试...") - - # 测试导入 - try: - from video_integration import VideoManager - print("✅ 视频管理器导入成功") - except Exception as e: - print(f"❌ 视频管理器导入失败: {e}") - return - - # 创建视频管理器 - try: - self.video_manager = VideoManager(self) - print("✅ 视频管理器创建成功") - except Exception as e: - print(f"❌ 视频管理器创建失败: {e}") - return - - # 测试创建动画屏幕 - try: - screen = self.video_manager.create_video_screen( - pos=(0, 0, 2), - name="test_screen" - ) - if screen: - print("✅ 动画屏幕创建成功") - else: - print("❌ 动画屏幕创建失败") - except Exception as e: - print(f"❌ 动画屏幕创建错误: {e}") - - # 测试创建广告牌 - try: - billboard = self.video_manager.create_video_billboard( - pos=(3, 0, 1), - name="test_billboard" - ) - if billboard: - print("✅ 视频广告牌创建成功") - else: - print("❌ 视频广告牌创建失败") - except Exception as e: - print(f"❌ 视频广告牌创建错误: {e}") - - # 测试创建全景视频 - try: - spherical = self.video_manager.create_spherical_video( - pos=(-3, 0, 0), - radius=2, - name="test_spherical" - ) - if spherical: - print("✅ 全景视频创建成功") - else: - print("❌ 全景视频创建失败") - except Exception as e: - print(f"❌ 全景视频创建错误: {e}") - - # 等待几秒钟查看动画效果 - print("\n🎬 测试完成!您应该看到:") - print(" - 左侧: 波纹动画屏幕") - print(" - 中间: 面向相机的广告牌") - print(" - 右侧: 全景动画球体") - print("\n按 ESC 键退出") - - # 设置退出键 - self.accept("escape", sys.exit) - - # 设置相机 - self.cam.setPos(0, -8, 2) - self.cam.lookAt(0, 0, 1) - - def updateSceneTree(self): - """空的场景树更新方法(用于兼容 VideoManager)""" - pass - -if __name__ == "__main__": - app = VideoTest() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +视频播放功能修复测试 +""" + +from direct.showbase.ShowBase import ShowBase +from panda3d.core import loadPrcFileData +import sys + +# 配置窗口 +loadPrcFileData("", """ + win-size 800 600 + window-title 视频功能测试 +""") + +class VideoTest(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 初始化属性以兼容 VideoManager + self.models = [] # 模型列表 + + print("🚀 开始视频功能测试...") + + # 测试导入 + try: + from video_integration import VideoManager + print("✅ 视频管理器导入成功") + except Exception as e: + print(f"❌ 视频管理器导入失败: {e}") + return + + # 创建视频管理器 + try: + self.video_manager = VideoManager(self) + print("✅ 视频管理器创建成功") + except Exception as e: + print(f"❌ 视频管理器创建失败: {e}") + return + + # 测试创建动画屏幕 + try: + screen = self.video_manager.create_video_screen( + pos=(0, 0, 2), + name="test_screen" + ) + if screen: + print("✅ 动画屏幕创建成功") + else: + print("❌ 动画屏幕创建失败") + except Exception as e: + print(f"❌ 动画屏幕创建错误: {e}") + + # 测试创建广告牌 + try: + billboard = self.video_manager.create_video_billboard( + pos=(3, 0, 1), + name="test_billboard" + ) + if billboard: + print("✅ 视频广告牌创建成功") + else: + print("❌ 视频广告牌创建失败") + except Exception as e: + print(f"❌ 视频广告牌创建错误: {e}") + + # 测试创建全景视频 + try: + spherical = self.video_manager.create_spherical_video( + pos=(-3, 0, 0), + radius=2, + name="test_spherical" + ) + if spherical: + print("✅ 全景视频创建成功") + else: + print("❌ 全景视频创建失败") + except Exception as e: + print(f"❌ 全景视频创建错误: {e}") + + # 等待几秒钟查看动画效果 + print("\n🎬 测试完成!您应该看到:") + print(" - 左侧: 波纹动画屏幕") + print(" - 中间: 面向相机的广告牌") + print(" - 右侧: 全景动画球体") + print("\n按 ESC 键退出") + + # 设置退出键 + self.accept("escape", sys.exit) + + # 设置相机 + self.cam.setPos(0, -8, 2) + self.cam.lookAt(0, 0, 1) + + def updateSceneTree(self): + """空的场景树更新方法(用于兼容 VideoManager)""" + pass + +if __name__ == "__main__": + app = VideoTest() app.run() \ No newline at end of file diff --git a/demo/video_integration.py b/demo/video_integration.py index 1b3fe0ef..433cb696 100644 --- a/demo/video_integration.py +++ b/demo/video_integration.py @@ -1,454 +1,454 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -视频播放集成模块 -用于在现有的Panda3D编辑器中添加视频播放功能 -""" - -from panda3d.core import ( - MovieTexture, CardMaker, Texture, PNMImage, - TextureStage, NodePath, Vec3 -) -import os -import math - -class VideoManager: - """视频管理器 - 集成到现有的MyWorld类中""" - - def __init__(self, world_instance): - self.world = world_instance - self.video_objects = [] # 存储所有视频对象 - self.supported_formats = ['.mp4', '.avi', '.mov', '.mkv', '.webm'] - - def create_video_screen(self, pos=(0, 0, 0), size=(4, 3), video_path=None, name="video_screen"): - """创建视频屏幕""" - try: - # 创建屏幕几何体 - cm = CardMaker(f"video_screen_{len(self.video_objects)}") - cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2) - - video_screen = self.world.render.attachNewNode(cm.generate()) - video_screen.setPos(*pos) - video_screen.setName(name) - - # 创建视频纹理 - if video_path and os.path.exists(video_path): - # Convert Windows path to Panda3D compatible path format - from panda3d.core import Filename - panda_path = Filename.fromOsSpecific(video_path) - converted_path = str(panda_path) - - movie_texture = MovieTexture(name) - success = movie_texture.read(converted_path) - - if success: - video_screen.setTexture(movie_texture) - - # 添加视频对象到管理器 - video_obj = { - 'node': video_screen, - 'texture': movie_texture, - 'path': converted_path, - 'name': name, - 'type': 'movie', - 'is_playing': False - } - - # 播放视频 - movie_texture.play() - video_obj['is_playing'] = True - - self.video_objects.append(video_obj) - - # 添加到模型列表和场景树 - self.world.models.append(video_screen) - self.world.updateSceneTree() - - print(f"✓ 视频屏幕创建成功: {name}") - return video_screen - else: - print(f"✗ 无法加载视频: {converted_path}") - # 创建占位符纹理 - self._create_placeholder_texture(video_screen, name) - else: - # 创建程序化动画纹理作为演示 - self._create_animated_texture(video_screen, name) - - return video_screen - - except Exception as e: - print(f"创建视频屏幕失败: {str(e)}") - return None - - def create_video_billboard(self, pos=(0, 0, 0), size=(2, 1.5), video_path=None, name="video_billboard"): - """创建面向相机的视频广告牌""" - try: - video_screen = self.create_video_screen(pos, size, video_path, name) - if video_screen: - # 设置为广告牌模式(总是面向相机) - video_screen.setBillboardAxis() - print(f"✓ 视频广告牌创建成功: {name}") - return video_screen - except Exception as e: - print(f"创建视频广告牌失败: {str(e)}") - return None - - def create_spherical_video(self, pos=(0, 0, 0), radius=10, video_path=None, name="spherical_video"): - """创建球形全景视频""" - try: - # 创建球体几何(使用多个面片模拟) - sphere = self._create_sphere_geometry(radius) - sphere.reparentTo(self.world.render) - sphere.setPos(*pos) - sphere.setName(name) - - # 反转法线以便从内部观看 - sphere.setTwoSided(True) - sphere.setScale(-1, 1, 1) # 反转X轴以正确显示内部 - - # 创建视频纹理 - if video_path and os.path.exists(video_path): - # Convert Windows path to Panda3D compatible path format - from panda3d.core import Filename - panda_path = Filename.fromOsSpecific(video_path) - converted_path = str(panda_path) - - movie_texture = MovieTexture(name) - success = movie_texture.read(converted_path) - - if success: - sphere.setTexture(movie_texture) - movie_texture.play() - - video_obj = { - 'node': sphere, - 'texture': movie_texture, - 'path': converted_path, - 'name': name, - 'type': 'spherical', - 'is_playing': True - } - - self.video_objects.append(video_obj) - else: - # 创建程序化全景纹理 - self._create_panoramic_texture(sphere, name) - - # 添加到模型列表 - self.world.models.append(sphere) - self.world.updateSceneTree() - - print(f"✓ 球形全景视频创建成功: {name}") - return sphere - - except Exception as e: - print(f"创建球形全景视频失败: {str(e)}") - return None - - def _create_sphere_geometry(self, radius): - """创建球体几何""" - # 简化版本:创建立方体作为球体替代 - cm = CardMaker("sphere_face") - cm.setFrame(-radius, radius, -radius, radius) - - sphere_root = self.world.render.attachNewNode("sphere") - - # 创建6个面(立方体的6个面) - faces = [ - (Vec3(0, radius, 0), Vec3(0, 0, 0)), # 前面 - (Vec3(0, -radius, 0), Vec3(0, 180, 0)), # 后面 - (Vec3(radius, 0, 0), Vec3(0, 90, 0)), # 右面 - (Vec3(-radius, 0, 0), Vec3(0, -90, 0)), # 左面 - (Vec3(0, 0, radius), Vec3(-90, 0, 0)), # 上面 - (Vec3(0, 0, -radius), Vec3(90, 0, 0)), # 下面 - ] - - for i, (pos, hpr) in enumerate(faces): - face = sphere_root.attachNewNode(cm.generate()) - face.setPos(pos) - face.setHpr(hpr) - - return sphere_root - - def _create_placeholder_texture(self, node, name): - """创建占位符纹理""" - # 创建简单的渐变纹理 - texture = Texture() - texture.setup2dTexture(256, 256, Texture.TUnsignedByte, Texture.FRgba) - - img = PNMImage(256, 256, 4) - for y in range(256): - for x in range(256): - # 创建棋盘格图案 - if (x // 32 + y // 32) % 2: - img.setXel(x, y, 0.8, 0.8, 0.8) - else: - img.setXel(x, y, 0.3, 0.3, 0.3) - img.setAlpha(x, y, 1.0) - - texture.load(img) - node.setTexture(texture) - - # 添加到视频对象列表 - video_obj = { - 'node': node, - 'texture': texture, - 'path': None, - 'name': name, - 'type': 'placeholder', - 'is_playing': False - } - self.video_objects.append(video_obj) - - def _create_animated_texture(self, node, name): - """创建动画纹理""" - texture = Texture() - texture.setup2dTexture(256, 256, Texture.TUnsignedByte, Texture.FRgba) - - node.setTexture(texture) - - # 启动动画更新任务 - 修复参数传递顺序 - task_name = f"animate_texture_{name}" - self.world.taskMgr.add(lambda task: self._update_animated_texture(task, texture, name), task_name) - - # 添加到视频对象列表 - video_obj = { - 'node': node, - 'texture': texture, - 'path': None, - 'name': name, - 'type': 'animated', - 'is_playing': True, - 'task_name': task_name - } - self.video_objects.append(video_obj) - - def _create_panoramic_texture(self, node, name): - """创建全景动画纹理""" - texture = Texture() - texture.setup2dTexture(512, 256, Texture.TUnsignedByte, Texture.FRgba) - - node.setTexture(texture) - - # 启动全景动画更新任务 - 修复参数传递顺序 - task_name = f"animate_panoramic_{name}" - self.world.taskMgr.add(lambda task: self._update_panoramic_texture(task, texture, name), task_name) - - # 添加到视频对象列表 - video_obj = { - 'node': node, - 'texture': texture, - 'path': None, - 'name': name, - 'type': 'panoramic', - 'is_playing': True, - 'task_name': task_name - } - self.video_objects.append(video_obj) - - def _update_animated_texture(self, task, texture, name): - """更新动画纹理""" - try: - time = task.time - img = PNMImage(256, 256, 4) - - for y in range(256): - for x in range(256): - # 创建波纹效果 - dist_x = (x - 128) / 128.0 - dist_y = (y - 128) / 128.0 - distance = math.sqrt(dist_x**2 + dist_y**2) - - wave = math.sin(distance * 8 - time * 3) - r = (wave + 1) * 0.5 - g = (math.sin(dist_x * 4 + time * 2) + 1) * 0.5 - b = (math.sin(dist_y * 4 + time * 2.5) + 1) * 0.5 - - img.setXel(x, y, r, g, b) - img.setAlpha(x, y, 1.0) - - texture.load(img) - return task.cont - - except Exception as e: - print(f"更新动画纹理失败: {str(e)}") - return task.done - - def _update_panoramic_texture(self, task, texture, name): - """更新全景动画纹理""" - try: - time = task.time - img = PNMImage(512, 256, 4) - - for y in range(256): - for x in range(512): - # 创建全景旋转效果 - u = x / 512.0 # 经度 (0-1) - v = y / 256.0 # 纬度 (0-1) - - angle = u * 2 * math.pi + time * 0.5 - height = (v - 0.5) * math.pi - - r = (math.sin(angle * 3) + 1) * 0.5 - g = (math.cos(angle * 2 + height * 4) + 1) * 0.5 - b = (math.sin(height * 6 + time) + 1) * 0.5 - - img.setXel(x, y, r, g, b) - img.setAlpha(x, y, 1.0) - - texture.load(img) - return task.cont - - except Exception as e: - print(f"更新全景纹理失败: {str(e)}") - return task.done - - def play_video(self, video_name): - """播放指定视频""" - for video_obj in self.video_objects: - if video_obj['name'] == video_name: - if video_obj['type'] == 'movie' and hasattr(video_obj['texture'], 'play'): - video_obj['texture'].play() - video_obj['is_playing'] = True - print(f"播放视频: {video_name}") - elif video_obj['type'] in ['animated', 'panoramic']: - # 重新启动动画任务 - if 'task_name' in video_obj: - task_name = video_obj['task_name'] - if video_obj['type'] == 'animated': - self.world.taskMgr.add( - lambda task: self._update_animated_texture(task, video_obj['texture'], video_name), - task_name - ) - else: - self.world.taskMgr.add( - lambda task: self._update_panoramic_texture(task, video_obj['texture'], video_name), - task_name - ) - video_obj['is_playing'] = True - print(f"恢复动画: {video_name}") - break - - def pause_video(self, video_name): - """暂停指定视频""" - for video_obj in self.video_objects: - if video_obj['name'] == video_name: - if video_obj['type'] == 'movie' and hasattr(video_obj['texture'], 'stop'): - video_obj['texture'].stop() - video_obj['is_playing'] = False - print(f"暂停视频: {video_name}") - elif video_obj['type'] in ['animated', 'panoramic']: - # 停止动画任务 - if 'task_name' in video_obj: - self.world.taskMgr.remove(video_obj['task_name']) - video_obj['is_playing'] = False - print(f"暂停动画: {video_name}") - break - - def stop_video(self, video_name): - """停止指定视频""" - for video_obj in self.video_objects: - if video_obj['name'] == video_name: - if video_obj['type'] == 'movie': - if hasattr(video_obj['texture'], 'stop'): - video_obj['texture'].stop() - if hasattr(video_obj['texture'], 'setTime'): - video_obj['texture'].setTime(0) - video_obj['is_playing'] = False - print(f"停止视频: {video_name}") - elif video_obj['type'] in ['animated', 'panoramic']: - # 停止动画任务 - if 'task_name' in video_obj: - self.world.taskMgr.remove(video_obj['task_name']) - video_obj['is_playing'] = False - print(f"停止动画: {video_name}") - break - - def set_video_speed(self, video_name, speed): - """设置视频播放速度""" - for video_obj in self.video_objects: - if video_obj['name'] == video_name: - if video_obj['type'] == 'movie' and hasattr(video_obj['texture'], 'setPlayRate'): - video_obj['texture'].setPlayRate(speed) - print(f"设置视频 {video_name} 播放速度: {speed}x") - break - - def remove_video(self, video_name): - """移除指定视频""" - for i, video_obj in enumerate(self.video_objects): - if video_obj['name'] == video_name: - # 停止播放 - self.stop_video(video_name) - - # 从场景中移除节点 - if video_obj['node'] in self.world.models: - self.world.models.remove(video_obj['node']) - video_obj['node'].removeNode() - - # 从列表中移除 - self.video_objects.pop(i) - - # 更新场景树 - self.world.updateSceneTree() - - print(f"移除视频: {video_name}") - break - - def get_video_info(self, video_name): - """获取视频信息""" - for video_obj in self.video_objects: - if video_obj['name'] == video_name: - info = { - 'name': video_obj['name'], - 'type': video_obj['type'], - 'is_playing': video_obj['is_playing'], - 'path': video_obj.get('path', 'N/A') - } - - if video_obj['type'] == 'movie' and hasattr(video_obj['texture'], 'getVideoLength'): - info['length'] = video_obj['texture'].getVideoLength() - info['current_time'] = video_obj['texture'].getTime() - info['play_rate'] = video_obj['texture'].getPlayRate() - - return info - return None - - def list_videos(self): - """列出所有视频""" - videos = [] - for video_obj in self.video_objects: - videos.append({ - 'name': video_obj['name'], - 'type': video_obj['type'], - 'is_playing': video_obj['is_playing'] - }) - return videos - - def is_video_file(self, filepath): - """检查文件是否是支持的视频格式""" - if not filepath: - return False - - ext = os.path.splitext(filepath)[1].lower() - return ext in self.supported_formats - -# 使用示例(集成到现有的 MyWorld 类中): -""" -# 在 MyWorld 类的 __init__ 方法中添加: -self.video_manager = VideoManager(self) - -# 在文件拖放处理中添加视频支持: -def dropEvent(self, event): - if event.mimeData().hasUrls(): - for url in event.mimeData().urls(): - filepath = url.toLocalFile() - if self.world.video_manager.is_video_file(filepath): - # 创建视频屏幕 - self.world.video_manager.create_video_screen( - pos=(0, 0, 2), - video_path=filepath, - name=f"video_{len(self.world.video_manager.video_objects)}" - ) - elif filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx')): - self.world.importModel(filepath) +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +视频播放集成模块 +用于在现有的Panda3D编辑器中添加视频播放功能 +""" + +from panda3d.core import ( + MovieTexture, CardMaker, Texture, PNMImage, + TextureStage, NodePath, Vec3 +) +import os +import math + +class VideoManager: + """视频管理器 - 集成到现有的MyWorld类中""" + + def __init__(self, world_instance): + self.world = world_instance + self.video_objects = [] # 存储所有视频对象 + self.supported_formats = ['.mp4', '.avi', '.mov', '.mkv', '.webm'] + + def create_video_screen(self, pos=(0, 0, 0), size=(4, 3), video_path=None, name="video_screen"): + """创建视频屏幕""" + try: + # 创建屏幕几何体 + cm = CardMaker(f"video_screen_{len(self.video_objects)}") + cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2) + + video_screen = self.world.render.attachNewNode(cm.generate()) + video_screen.setPos(*pos) + video_screen.setName(name) + + # 创建视频纹理 + if video_path and os.path.exists(video_path): + # Convert Windows path to Panda3D compatible path format + from panda3d.core import Filename + panda_path = Filename.fromOsSpecific(video_path) + converted_path = str(panda_path) + + movie_texture = MovieTexture(name) + success = movie_texture.read(converted_path) + + if success: + video_screen.setTexture(movie_texture) + + # 添加视频对象到管理器 + video_obj = { + 'node': video_screen, + 'texture': movie_texture, + 'path': converted_path, + 'name': name, + 'type': 'movie', + 'is_playing': False + } + + # 播放视频 + movie_texture.play() + video_obj['is_playing'] = True + + self.video_objects.append(video_obj) + + # 添加到模型列表和场景树 + self.world.models.append(video_screen) + self.world.updateSceneTree() + + print(f"✓ 视频屏幕创建成功: {name}") + return video_screen + else: + print(f"✗ 无法加载视频: {converted_path}") + # 创建占位符纹理 + self._create_placeholder_texture(video_screen, name) + else: + # 创建程序化动画纹理作为演示 + self._create_animated_texture(video_screen, name) + + return video_screen + + except Exception as e: + print(f"创建视频屏幕失败: {str(e)}") + return None + + def create_video_billboard(self, pos=(0, 0, 0), size=(2, 1.5), video_path=None, name="video_billboard"): + """创建面向相机的视频广告牌""" + try: + video_screen = self.create_video_screen(pos, size, video_path, name) + if video_screen: + # 设置为广告牌模式(总是面向相机) + video_screen.setBillboardAxis() + print(f"✓ 视频广告牌创建成功: {name}") + return video_screen + except Exception as e: + print(f"创建视频广告牌失败: {str(e)}") + return None + + def create_spherical_video(self, pos=(0, 0, 0), radius=10, video_path=None, name="spherical_video"): + """创建球形全景视频""" + try: + # 创建球体几何(使用多个面片模拟) + sphere = self._create_sphere_geometry(radius) + sphere.reparentTo(self.world.render) + sphere.setPos(*pos) + sphere.setName(name) + + # 反转法线以便从内部观看 + sphere.setTwoSided(True) + sphere.setScale(-1, 1, 1) # 反转X轴以正确显示内部 + + # 创建视频纹理 + if video_path and os.path.exists(video_path): + # Convert Windows path to Panda3D compatible path format + from panda3d.core import Filename + panda_path = Filename.fromOsSpecific(video_path) + converted_path = str(panda_path) + + movie_texture = MovieTexture(name) + success = movie_texture.read(converted_path) + + if success: + sphere.setTexture(movie_texture) + movie_texture.play() + + video_obj = { + 'node': sphere, + 'texture': movie_texture, + 'path': converted_path, + 'name': name, + 'type': 'spherical', + 'is_playing': True + } + + self.video_objects.append(video_obj) + else: + # 创建程序化全景纹理 + self._create_panoramic_texture(sphere, name) + + # 添加到模型列表 + self.world.models.append(sphere) + self.world.updateSceneTree() + + print(f"✓ 球形全景视频创建成功: {name}") + return sphere + + except Exception as e: + print(f"创建球形全景视频失败: {str(e)}") + return None + + def _create_sphere_geometry(self, radius): + """创建球体几何""" + # 简化版本:创建立方体作为球体替代 + cm = CardMaker("sphere_face") + cm.setFrame(-radius, radius, -radius, radius) + + sphere_root = self.world.render.attachNewNode("sphere") + + # 创建6个面(立方体的6个面) + faces = [ + (Vec3(0, radius, 0), Vec3(0, 0, 0)), # 前面 + (Vec3(0, -radius, 0), Vec3(0, 180, 0)), # 后面 + (Vec3(radius, 0, 0), Vec3(0, 90, 0)), # 右面 + (Vec3(-radius, 0, 0), Vec3(0, -90, 0)), # 左面 + (Vec3(0, 0, radius), Vec3(-90, 0, 0)), # 上面 + (Vec3(0, 0, -radius), Vec3(90, 0, 0)), # 下面 + ] + + for i, (pos, hpr) in enumerate(faces): + face = sphere_root.attachNewNode(cm.generate()) + face.setPos(pos) + face.setHpr(hpr) + + return sphere_root + + def _create_placeholder_texture(self, node, name): + """创建占位符纹理""" + # 创建简单的渐变纹理 + texture = Texture() + texture.setup2dTexture(256, 256, Texture.TUnsignedByte, Texture.FRgba) + + img = PNMImage(256, 256, 4) + for y in range(256): + for x in range(256): + # 创建棋盘格图案 + if (x // 32 + y // 32) % 2: + img.setXel(x, y, 0.8, 0.8, 0.8) + else: + img.setXel(x, y, 0.3, 0.3, 0.3) + img.setAlpha(x, y, 1.0) + + texture.load(img) + node.setTexture(texture) + + # 添加到视频对象列表 + video_obj = { + 'node': node, + 'texture': texture, + 'path': None, + 'name': name, + 'type': 'placeholder', + 'is_playing': False + } + self.video_objects.append(video_obj) + + def _create_animated_texture(self, node, name): + """创建动画纹理""" + texture = Texture() + texture.setup2dTexture(256, 256, Texture.TUnsignedByte, Texture.FRgba) + + node.setTexture(texture) + + # 启动动画更新任务 - 修复参数传递顺序 + task_name = f"animate_texture_{name}" + self.world.taskMgr.add(lambda task: self._update_animated_texture(task, texture, name), task_name) + + # 添加到视频对象列表 + video_obj = { + 'node': node, + 'texture': texture, + 'path': None, + 'name': name, + 'type': 'animated', + 'is_playing': True, + 'task_name': task_name + } + self.video_objects.append(video_obj) + + def _create_panoramic_texture(self, node, name): + """创建全景动画纹理""" + texture = Texture() + texture.setup2dTexture(512, 256, Texture.TUnsignedByte, Texture.FRgba) + + node.setTexture(texture) + + # 启动全景动画更新任务 - 修复参数传递顺序 + task_name = f"animate_panoramic_{name}" + self.world.taskMgr.add(lambda task: self._update_panoramic_texture(task, texture, name), task_name) + + # 添加到视频对象列表 + video_obj = { + 'node': node, + 'texture': texture, + 'path': None, + 'name': name, + 'type': 'panoramic', + 'is_playing': True, + 'task_name': task_name + } + self.video_objects.append(video_obj) + + def _update_animated_texture(self, task, texture, name): + """更新动画纹理""" + try: + time = task.time + img = PNMImage(256, 256, 4) + + for y in range(256): + for x in range(256): + # 创建波纹效果 + dist_x = (x - 128) / 128.0 + dist_y = (y - 128) / 128.0 + distance = math.sqrt(dist_x**2 + dist_y**2) + + wave = math.sin(distance * 8 - time * 3) + r = (wave + 1) * 0.5 + g = (math.sin(dist_x * 4 + time * 2) + 1) * 0.5 + b = (math.sin(dist_y * 4 + time * 2.5) + 1) * 0.5 + + img.setXel(x, y, r, g, b) + img.setAlpha(x, y, 1.0) + + texture.load(img) + return task.cont + + except Exception as e: + print(f"更新动画纹理失败: {str(e)}") + return task.done + + def _update_panoramic_texture(self, task, texture, name): + """更新全景动画纹理""" + try: + time = task.time + img = PNMImage(512, 256, 4) + + for y in range(256): + for x in range(512): + # 创建全景旋转效果 + u = x / 512.0 # 经度 (0-1) + v = y / 256.0 # 纬度 (0-1) + + angle = u * 2 * math.pi + time * 0.5 + height = (v - 0.5) * math.pi + + r = (math.sin(angle * 3) + 1) * 0.5 + g = (math.cos(angle * 2 + height * 4) + 1) * 0.5 + b = (math.sin(height * 6 + time) + 1) * 0.5 + + img.setXel(x, y, r, g, b) + img.setAlpha(x, y, 1.0) + + texture.load(img) + return task.cont + + except Exception as e: + print(f"更新全景纹理失败: {str(e)}") + return task.done + + def play_video(self, video_name): + """播放指定视频""" + for video_obj in self.video_objects: + if video_obj['name'] == video_name: + if video_obj['type'] == 'movie' and hasattr(video_obj['texture'], 'play'): + video_obj['texture'].play() + video_obj['is_playing'] = True + print(f"播放视频: {video_name}") + elif video_obj['type'] in ['animated', 'panoramic']: + # 重新启动动画任务 + if 'task_name' in video_obj: + task_name = video_obj['task_name'] + if video_obj['type'] == 'animated': + self.world.taskMgr.add( + lambda task: self._update_animated_texture(task, video_obj['texture'], video_name), + task_name + ) + else: + self.world.taskMgr.add( + lambda task: self._update_panoramic_texture(task, video_obj['texture'], video_name), + task_name + ) + video_obj['is_playing'] = True + print(f"恢复动画: {video_name}") + break + + def pause_video(self, video_name): + """暂停指定视频""" + for video_obj in self.video_objects: + if video_obj['name'] == video_name: + if video_obj['type'] == 'movie' and hasattr(video_obj['texture'], 'stop'): + video_obj['texture'].stop() + video_obj['is_playing'] = False + print(f"暂停视频: {video_name}") + elif video_obj['type'] in ['animated', 'panoramic']: + # 停止动画任务 + if 'task_name' in video_obj: + self.world.taskMgr.remove(video_obj['task_name']) + video_obj['is_playing'] = False + print(f"暂停动画: {video_name}") + break + + def stop_video(self, video_name): + """停止指定视频""" + for video_obj in self.video_objects: + if video_obj['name'] == video_name: + if video_obj['type'] == 'movie': + if hasattr(video_obj['texture'], 'stop'): + video_obj['texture'].stop() + if hasattr(video_obj['texture'], 'setTime'): + video_obj['texture'].setTime(0) + video_obj['is_playing'] = False + print(f"停止视频: {video_name}") + elif video_obj['type'] in ['animated', 'panoramic']: + # 停止动画任务 + if 'task_name' in video_obj: + self.world.taskMgr.remove(video_obj['task_name']) + video_obj['is_playing'] = False + print(f"停止动画: {video_name}") + break + + def set_video_speed(self, video_name, speed): + """设置视频播放速度""" + for video_obj in self.video_objects: + if video_obj['name'] == video_name: + if video_obj['type'] == 'movie' and hasattr(video_obj['texture'], 'setPlayRate'): + video_obj['texture'].setPlayRate(speed) + print(f"设置视频 {video_name} 播放速度: {speed}x") + break + + def remove_video(self, video_name): + """移除指定视频""" + for i, video_obj in enumerate(self.video_objects): + if video_obj['name'] == video_name: + # 停止播放 + self.stop_video(video_name) + + # 从场景中移除节点 + if video_obj['node'] in self.world.models: + self.world.models.remove(video_obj['node']) + video_obj['node'].removeNode() + + # 从列表中移除 + self.video_objects.pop(i) + + # 更新场景树 + self.world.updateSceneTree() + + print(f"移除视频: {video_name}") + break + + def get_video_info(self, video_name): + """获取视频信息""" + for video_obj in self.video_objects: + if video_obj['name'] == video_name: + info = { + 'name': video_obj['name'], + 'type': video_obj['type'], + 'is_playing': video_obj['is_playing'], + 'path': video_obj.get('path', 'N/A') + } + + if video_obj['type'] == 'movie' and hasattr(video_obj['texture'], 'getVideoLength'): + info['length'] = video_obj['texture'].getVideoLength() + info['current_time'] = video_obj['texture'].getTime() + info['play_rate'] = video_obj['texture'].getPlayRate() + + return info + return None + + def list_videos(self): + """列出所有视频""" + videos = [] + for video_obj in self.video_objects: + videos.append({ + 'name': video_obj['name'], + 'type': video_obj['type'], + 'is_playing': video_obj['is_playing'] + }) + return videos + + def is_video_file(self, filepath): + """检查文件是否是支持的视频格式""" + if not filepath: + return False + + ext = os.path.splitext(filepath)[1].lower() + return ext in self.supported_formats + +# 使用示例(集成到现有的 MyWorld 类中): +""" +# 在 MyWorld 类的 __init__ 方法中添加: +self.video_manager = VideoManager(self) + +# 在文件拖放处理中添加视频支持: +def dropEvent(self, event): + if event.mimeData().hasUrls(): + for url in event.mimeData().urls(): + filepath = url.toLocalFile() + if self.world.video_manager.is_video_file(filepath): + # 创建视频屏幕 + self.world.video_manager.create_video_screen( + pos=(0, 0, 2), + video_path=filepath, + name=f"video_{len(self.world.video_manager.video_objects)}" + ) + elif filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx')): + self.world.importModel(filepath) """ \ No newline at end of file diff --git a/demo/video_player_example.py b/demo/video_player_example.py index b5444b7d..8fb8046c 100644 --- a/demo/video_player_example.py +++ b/demo/video_player_example.py @@ -1,395 +1,395 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Panda3D 视频播放示例 -演示如何在Panda3D中播放内嵌视频 -""" - -from direct.showbase.ShowBase import ShowBase -from panda3d.core import ( - MovieTexture, CardMaker, TextureStage, NodePath, - WindowProperties, loadPrcFileData, Vec3, Point3, - AmbientLight, DirectionalLight, GeomNode -) -import os - -# 配置Panda3D -loadPrcFileData("", """ - win-size 1280 720 - window-title Panda3D 视频播放器 - show-frame-rate-meter true -""") - -class VideoPlayerDemo(ShowBase): - def __init__(self): - ShowBase.__init__(self) - - # 设置背景色 - self.setBackgroundColor(0.1, 0.1, 0.1) - - # 设置相机 - self.cam.setPos(0, -10, 0) - self.cam.lookAt(0, 0, 0) - - # 添加光照 - self.setup_lighting() - - # 创建视频播放器 - self.setup_video_players() - - # 设置控制 - self.setup_controls() - - print("视频播放器已启动!") - print("控制键:") - print(" SPACE - 播放/暂停") - print(" R - 重新播放") - print(" + - 加速播放") - print(" - - 减速播放") - print(" ESC - 退出") - - def setup_lighting(self): - """设置基础光照""" - # 环境光 - alight = AmbientLight('ambient') - alight.setColor((0.3, 0.3, 0.3, 1)) - alnp = self.render.attachNewNode(alight) - self.render.setLight(alnp) - - # 定向光 - dlight = DirectionalLight('directional') - dlight.setColor((0.8, 0.8, 0.8, 1)) - dlnp = self.render.attachNewNode(dlight) - dlnp.setHpr(45, -45, 0) - self.render.setLight(dlnp) - - def setup_video_players(self): - """设置视频播放器""" - self.videos = [] - - # 方法1: 平面视频播放器 - self.create_flat_video_player() - - # 方法2: 3D模型上的视频纹理 - self.create_3d_video_screen() - - # 方法3: 球形全景视频 - self.create_spherical_video() - - def create_flat_video_player(self): - """创建平面视频播放器""" - try: - # 创建视频纹理 (这里使用示例路径,需要替换为实际视频文件) - video_path = "video/sample.mp4" # 支持 .mp4, .avi, .mov 等格式 - - # 如果文件不存在,创建一个简单的色彩动画作为演示 - if not os.path.exists(video_path): - self.create_color_animation_demo() - return - - # 加载视频 - movie_texture = MovieTexture() - success = movie_texture.read(video_path) - - if success: - # 创建卡片几何体 - cm = CardMaker("video_card") - cm.setFrame(-2, 2, -1.5, 1.5) - - video_card = self.render.attachNewNode(cm.generate()) - video_card.setTexture(movie_texture) - video_card.setPos(-4, 0, 0) - - # 播放视频 - movie_texture.play() - - self.videos.append({ - 'texture': movie_texture, - 'node': video_card, - 'name': 'flat_video' - }) - - print(f"✓ 平面视频播放器创建成功: {video_path}") - else: - print(f"✗ 无法加载视频: {video_path}") - - except Exception as e: - print(f"创建平面视频播放器失败: {str(e)}") - - def create_3d_video_screen(self): - """在3D模型上创建视频纹理""" - try: - # 创建一个简单的立方体作为"电视屏幕" - from panda3d.core import CardMaker - - # 创建屏幕边框 - frame_cm = CardMaker("screen_frame") - frame_cm.setFrame(-2.2, 2.2, -1.7, 1.7) - screen_frame = self.render.attachNewNode(frame_cm.generate()) - screen_frame.setColor(0.1, 0.1, 0.1, 1) - screen_frame.setPos(0, 0, 0) - - # 创建屏幕 - screen_cm = CardMaker("video_screen") - screen_cm.setFrame(-2, 2, -1.5, 1.5) - video_screen = screen_frame.attachNewNode(screen_cm.generate()) - - # 创建程序化视频内容 (颜色渐变动画) - movie_texture = self.create_procedural_video(panoramic=False) - video_screen.setTexture(movie_texture) - - self.videos.append({ - 'texture': movie_texture, - 'node': video_screen, - 'name': '3d_screen' - }) - - print("✓ 3D视频屏幕创建成功") - - except Exception as e: - print(f"创建3D视频屏幕失败: {str(e)}") - - def create_spherical_video(self): - """创建球形全景视频""" - try: - # 创建球体几何(程序化生成) - sphere = self.create_sphere_geometry(radius=2) - sphere.reparentTo(self.render) - sphere.setPos(4, 0, 0) - sphere.setScale(2) - - # 反转法线以便从内部观看 - sphere.setTwoSided(True) - - # 创建全景视频纹理 - panoramic_texture = self.create_procedural_video(panoramic=True) - sphere.setTexture(panoramic_texture) - - self.videos.append({ - 'texture': panoramic_texture, - 'node': sphere, - 'name': 'spherical_video' - }) - - print("✓ 球形全景视频创建成功") - - except Exception as e: - print(f"创建球形全景视频失败: {str(e)}") - - def create_sphere_geometry(self, radius=1): - """程序化创建球体几何""" - # 创建简化的球体(立方体的6个面) - sphere_root = self.render.attachNewNode("sphere") - - faces = [ - # (位置, 旋转) - (Vec3(0, radius, 0), Vec3(0, 0, 0)), # 前面 - (Vec3(0, -radius, 0), Vec3(0, 180, 0)), # 后面 - (Vec3(radius, 0, 0), Vec3(0, 90, 0)), # 右面 - (Vec3(-radius, 0, 0), Vec3(0, -90, 0)), # 左面 - (Vec3(0, 0, radius), Vec3(-90, 0, 0)), # 上面 - (Vec3(0, 0, -radius), Vec3(90, 0, 0)), # 下面 - ] - - cm = CardMaker("sphere_face") - cm.setFrame(-radius, radius, -radius, radius) - - for i, (pos, hpr) in enumerate(faces): - face = sphere_root.attachNewNode(cm.generate()) - face.setPos(pos) - face.setHpr(hpr) - - return sphere_root - - def create_procedural_video(self, panoramic=False): - """创建程序化视频内容作为演示""" - from panda3d.core import PNMImage, Texture - - # 创建动态纹理 - texture = Texture() - if panoramic: - texture.setup2dTexture(512, 256, Texture.TUnsignedByte, Texture.FRgba) - else: - texture.setup2dTexture(256, 256, Texture.TUnsignedByte, Texture.FRgba) - - # 启动更新任务 - 修复参数传递使用lambda - task_name = f"update_video_{'panoramic' if panoramic else 'normal'}" - self.taskMgr.add(lambda task: self.update_procedural_video(task, texture, panoramic), task_name) - - return texture - - def update_procedural_video(self, task, texture, panoramic): - """更新程序化视频内容""" - try: - from panda3d.core import PNMImage - import math - - # 获取当前时间 - time = task.time - - # 根据纹理类型创建不同尺寸的图像 - if panoramic: - img = PNMImage(512, 256, 4) # 全景纹理 - width, height = 512, 256 - else: - img = PNMImage(256, 256, 4) # 普通纹理 - width, height = 256, 256 - - for y in range(height): - for x in range(width): - # 创建动态色彩效果 - if panoramic: - # 全景模式:创建旋转的渐变 - u = x / float(width) # 经度 (0-1) - v = y / float(height) # 纬度 (0-1) - - angle = u * 2 * math.pi + time * 0.5 - height_factor = (v - 0.5) * math.pi - - r = (math.sin(angle * 3) + 1) * 0.5 - g = (math.cos(angle * 2 + height_factor * 4) + 1) * 0.5 - b = (math.sin(height_factor * 6 + time) + 1) * 0.5 - else: - # 普通模式:创建波纹效果 - center_x = width // 2 - center_y = height // 2 - dist_x = (x - center_x) / float(center_x) - dist_y = (y - center_y) / float(center_y) - distance = math.sqrt(dist_x**2 + dist_y**2) - - wave = math.sin(distance * 10 - time * 5) - r = (wave + 1) * 0.5 - g = (math.sin(dist_x * 5 + time * 2) + 1) * 0.5 - b = (math.sin(dist_y * 5 + time * 3) + 1) * 0.5 - - img.setXel(x, y, r, g, b) - img.setAlpha(x, y, 1.0) - - # 更新纹理 - texture.load(img) - - return task.cont - - except Exception as e: - print(f"更新程序化视频失败: {str(e)}") - return task.done - - def create_color_animation_demo(self): - """创建颜色动画演示(当没有视频文件时)""" - try: - # 创建卡片 - cm = CardMaker("color_demo") - cm.setFrame(-2, 2, -1.5, 1.5) - - demo_card = self.render.attachNewNode(cm.generate()) - demo_card.setPos(-4, 0, 0) - - # 创建动态颜色纹理 - demo_texture = self.create_procedural_video(panoramic=False) - demo_card.setTexture(demo_texture) - - self.videos.append({ - 'texture': demo_texture, - 'node': demo_card, - 'name': 'color_demo' - }) - - print("✓ 颜色动画演示创建成功") - - except Exception as e: - print(f"创建颜色动画演示失败: {str(e)}") - - def setup_controls(self): - """设置控制键""" - self.accept("space", self.toggle_play_pause) - self.accept("r", self.restart_videos) - self.accept("plus", self.speed_up) - self.accept("minus", self.slow_down) - self.accept("escape", self.quit_app) - - # 鼠标控制相机 - self.accept("mouse1", self.start_camera_rotation) - self.accept("mouse1-up", self.stop_camera_rotation) - - self.camera_rotating = False - self.last_mouse_x = 0 - self.last_mouse_y = 0 - - # 启动相机更新任务 - self.taskMgr.add(self.update_camera_rotation, "camera_rotation") - - def toggle_play_pause(self): - """切换播放/暂停""" - for video in self.videos: - if hasattr(video['texture'], 'isPlaying'): - if video['texture'].isPlaying(): - video['texture'].stop() - print(f"暂停视频: {video['name']}") - else: - video['texture'].play() - print(f"播放视频: {video['name']}") - - def restart_videos(self): - """重新播放所有视频""" - for video in self.videos: - if hasattr(video['texture'], 'play'): - video['texture'].setTime(0) - video['texture'].play() - print(f"重新播放视频: {video['name']}") - - def speed_up(self): - """加速播放""" - for video in self.videos: - if hasattr(video['texture'], 'getPlayRate'): - current_rate = video['texture'].getPlayRate() - new_rate = min(current_rate * 1.5, 4.0) - video['texture'].setPlayRate(new_rate) - print(f"加速播放 {video['name']}: {new_rate:.2f}x") - - def slow_down(self): - """减速播放""" - for video in self.videos: - if hasattr(video['texture'], 'getPlayRate'): - current_rate = video['texture'].getPlayRate() - new_rate = max(current_rate / 1.5, 0.25) - video['texture'].setPlayRate(new_rate) - print(f"减速播放 {video['name']}: {new_rate:.2f}x") - - def start_camera_rotation(self): - """开始相机旋转""" - if self.mouseWatcherNode.hasMouse(): - self.camera_rotating = True - self.last_mouse_x = self.mouseWatcherNode.getMouseX() - self.last_mouse_y = self.mouseWatcherNode.getMouseY() - - def stop_camera_rotation(self): - """停止相机旋转""" - self.camera_rotating = False - - def update_camera_rotation(self, task): - """更新相机旋转""" - if self.camera_rotating and self.mouseWatcherNode.hasMouse(): - mouse_x = self.mouseWatcherNode.getMouseX() - mouse_y = self.mouseWatcherNode.getMouseY() - - delta_x = mouse_x - self.last_mouse_x - delta_y = mouse_y - self.last_mouse_y - - # 更新相机方向 - self.cam.setH(self.cam.getH() - delta_x * 50) - new_p = self.cam.getP() + delta_y * 50 - self.cam.setP(max(min(new_p, 89), -89)) - - self.last_mouse_x = mouse_x - self.last_mouse_y = mouse_y - - return task.cont - - def quit_app(self): - """退出应用""" - print("退出视频播放器...") - self.userExit() - -if __name__ == "__main__": - app = VideoPlayerDemo() +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Panda3D 视频播放示例 +演示如何在Panda3D中播放内嵌视频 +""" + +from direct.showbase.ShowBase import ShowBase +from panda3d.core import ( + MovieTexture, CardMaker, TextureStage, NodePath, + WindowProperties, loadPrcFileData, Vec3, Point3, + AmbientLight, DirectionalLight, GeomNode +) +import os + +# 配置Panda3D +loadPrcFileData("", """ + win-size 1280 720 + window-title Panda3D 视频播放器 + show-frame-rate-meter true +""") + +class VideoPlayerDemo(ShowBase): + def __init__(self): + ShowBase.__init__(self) + + # 设置背景色 + self.setBackgroundColor(0.1, 0.1, 0.1) + + # 设置相机 + self.cam.setPos(0, -10, 0) + self.cam.lookAt(0, 0, 0) + + # 添加光照 + self.setup_lighting() + + # 创建视频播放器 + self.setup_video_players() + + # 设置控制 + self.setup_controls() + + print("视频播放器已启动!") + print("控制键:") + print(" SPACE - 播放/暂停") + print(" R - 重新播放") + print(" + - 加速播放") + print(" - - 减速播放") + print(" ESC - 退出") + + def setup_lighting(self): + """设置基础光照""" + # 环境光 + alight = AmbientLight('ambient') + alight.setColor((0.3, 0.3, 0.3, 1)) + alnp = self.render.attachNewNode(alight) + self.render.setLight(alnp) + + # 定向光 + dlight = DirectionalLight('directional') + dlight.setColor((0.8, 0.8, 0.8, 1)) + dlnp = self.render.attachNewNode(dlight) + dlnp.setHpr(45, -45, 0) + self.render.setLight(dlnp) + + def setup_video_players(self): + """设置视频播放器""" + self.videos = [] + + # 方法1: 平面视频播放器 + self.create_flat_video_player() + + # 方法2: 3D模型上的视频纹理 + self.create_3d_video_screen() + + # 方法3: 球形全景视频 + self.create_spherical_video() + + def create_flat_video_player(self): + """创建平面视频播放器""" + try: + # 创建视频纹理 (这里使用示例路径,需要替换为实际视频文件) + video_path = "video/sample.mp4" # 支持 .mp4, .avi, .mov 等格式 + + # 如果文件不存在,创建一个简单的色彩动画作为演示 + if not os.path.exists(video_path): + self.create_color_animation_demo() + return + + # 加载视频 + movie_texture = MovieTexture() + success = movie_texture.read(video_path) + + if success: + # 创建卡片几何体 + cm = CardMaker("video_card") + cm.setFrame(-2, 2, -1.5, 1.5) + + video_card = self.render.attachNewNode(cm.generate()) + video_card.setTexture(movie_texture) + video_card.setPos(-4, 0, 0) + + # 播放视频 + movie_texture.play() + + self.videos.append({ + 'texture': movie_texture, + 'node': video_card, + 'name': 'flat_video' + }) + + print(f"✓ 平面视频播放器创建成功: {video_path}") + else: + print(f"✗ 无法加载视频: {video_path}") + + except Exception as e: + print(f"创建平面视频播放器失败: {str(e)}") + + def create_3d_video_screen(self): + """在3D模型上创建视频纹理""" + try: + # 创建一个简单的立方体作为"电视屏幕" + from panda3d.core import CardMaker + + # 创建屏幕边框 + frame_cm = CardMaker("screen_frame") + frame_cm.setFrame(-2.2, 2.2, -1.7, 1.7) + screen_frame = self.render.attachNewNode(frame_cm.generate()) + screen_frame.setColor(0.1, 0.1, 0.1, 1) + screen_frame.setPos(0, 0, 0) + + # 创建屏幕 + screen_cm = CardMaker("video_screen") + screen_cm.setFrame(-2, 2, -1.5, 1.5) + video_screen = screen_frame.attachNewNode(screen_cm.generate()) + + # 创建程序化视频内容 (颜色渐变动画) + movie_texture = self.create_procedural_video(panoramic=False) + video_screen.setTexture(movie_texture) + + self.videos.append({ + 'texture': movie_texture, + 'node': video_screen, + 'name': '3d_screen' + }) + + print("✓ 3D视频屏幕创建成功") + + except Exception as e: + print(f"创建3D视频屏幕失败: {str(e)}") + + def create_spherical_video(self): + """创建球形全景视频""" + try: + # 创建球体几何(程序化生成) + sphere = self.create_sphere_geometry(radius=2) + sphere.reparentTo(self.render) + sphere.setPos(4, 0, 0) + sphere.setScale(2) + + # 反转法线以便从内部观看 + sphere.setTwoSided(True) + + # 创建全景视频纹理 + panoramic_texture = self.create_procedural_video(panoramic=True) + sphere.setTexture(panoramic_texture) + + self.videos.append({ + 'texture': panoramic_texture, + 'node': sphere, + 'name': 'spherical_video' + }) + + print("✓ 球形全景视频创建成功") + + except Exception as e: + print(f"创建球形全景视频失败: {str(e)}") + + def create_sphere_geometry(self, radius=1): + """程序化创建球体几何""" + # 创建简化的球体(立方体的6个面) + sphere_root = self.render.attachNewNode("sphere") + + faces = [ + # (位置, 旋转) + (Vec3(0, radius, 0), Vec3(0, 0, 0)), # 前面 + (Vec3(0, -radius, 0), Vec3(0, 180, 0)), # 后面 + (Vec3(radius, 0, 0), Vec3(0, 90, 0)), # 右面 + (Vec3(-radius, 0, 0), Vec3(0, -90, 0)), # 左面 + (Vec3(0, 0, radius), Vec3(-90, 0, 0)), # 上面 + (Vec3(0, 0, -radius), Vec3(90, 0, 0)), # 下面 + ] + + cm = CardMaker("sphere_face") + cm.setFrame(-radius, radius, -radius, radius) + + for i, (pos, hpr) in enumerate(faces): + face = sphere_root.attachNewNode(cm.generate()) + face.setPos(pos) + face.setHpr(hpr) + + return sphere_root + + def create_procedural_video(self, panoramic=False): + """创建程序化视频内容作为演示""" + from panda3d.core import PNMImage, Texture + + # 创建动态纹理 + texture = Texture() + if panoramic: + texture.setup2dTexture(512, 256, Texture.TUnsignedByte, Texture.FRgba) + else: + texture.setup2dTexture(256, 256, Texture.TUnsignedByte, Texture.FRgba) + + # 启动更新任务 - 修复参数传递使用lambda + task_name = f"update_video_{'panoramic' if panoramic else 'normal'}" + self.taskMgr.add(lambda task: self.update_procedural_video(task, texture, panoramic), task_name) + + return texture + + def update_procedural_video(self, task, texture, panoramic): + """更新程序化视频内容""" + try: + from panda3d.core import PNMImage + import math + + # 获取当前时间 + time = task.time + + # 根据纹理类型创建不同尺寸的图像 + if panoramic: + img = PNMImage(512, 256, 4) # 全景纹理 + width, height = 512, 256 + else: + img = PNMImage(256, 256, 4) # 普通纹理 + width, height = 256, 256 + + for y in range(height): + for x in range(width): + # 创建动态色彩效果 + if panoramic: + # 全景模式:创建旋转的渐变 + u = x / float(width) # 经度 (0-1) + v = y / float(height) # 纬度 (0-1) + + angle = u * 2 * math.pi + time * 0.5 + height_factor = (v - 0.5) * math.pi + + r = (math.sin(angle * 3) + 1) * 0.5 + g = (math.cos(angle * 2 + height_factor * 4) + 1) * 0.5 + b = (math.sin(height_factor * 6 + time) + 1) * 0.5 + else: + # 普通模式:创建波纹效果 + center_x = width // 2 + center_y = height // 2 + dist_x = (x - center_x) / float(center_x) + dist_y = (y - center_y) / float(center_y) + distance = math.sqrt(dist_x**2 + dist_y**2) + + wave = math.sin(distance * 10 - time * 5) + r = (wave + 1) * 0.5 + g = (math.sin(dist_x * 5 + time * 2) + 1) * 0.5 + b = (math.sin(dist_y * 5 + time * 3) + 1) * 0.5 + + img.setXel(x, y, r, g, b) + img.setAlpha(x, y, 1.0) + + # 更新纹理 + texture.load(img) + + return task.cont + + except Exception as e: + print(f"更新程序化视频失败: {str(e)}") + return task.done + + def create_color_animation_demo(self): + """创建颜色动画演示(当没有视频文件时)""" + try: + # 创建卡片 + cm = CardMaker("color_demo") + cm.setFrame(-2, 2, -1.5, 1.5) + + demo_card = self.render.attachNewNode(cm.generate()) + demo_card.setPos(-4, 0, 0) + + # 创建动态颜色纹理 + demo_texture = self.create_procedural_video(panoramic=False) + demo_card.setTexture(demo_texture) + + self.videos.append({ + 'texture': demo_texture, + 'node': demo_card, + 'name': 'color_demo' + }) + + print("✓ 颜色动画演示创建成功") + + except Exception as e: + print(f"创建颜色动画演示失败: {str(e)}") + + def setup_controls(self): + """设置控制键""" + self.accept("space", self.toggle_play_pause) + self.accept("r", self.restart_videos) + self.accept("plus", self.speed_up) + self.accept("minus", self.slow_down) + self.accept("escape", self.quit_app) + + # 鼠标控制相机 + self.accept("mouse1", self.start_camera_rotation) + self.accept("mouse1-up", self.stop_camera_rotation) + + self.camera_rotating = False + self.last_mouse_x = 0 + self.last_mouse_y = 0 + + # 启动相机更新任务 + self.taskMgr.add(self.update_camera_rotation, "camera_rotation") + + def toggle_play_pause(self): + """切换播放/暂停""" + for video in self.videos: + if hasattr(video['texture'], 'isPlaying'): + if video['texture'].isPlaying(): + video['texture'].stop() + print(f"暂停视频: {video['name']}") + else: + video['texture'].play() + print(f"播放视频: {video['name']}") + + def restart_videos(self): + """重新播放所有视频""" + for video in self.videos: + if hasattr(video['texture'], 'play'): + video['texture'].setTime(0) + video['texture'].play() + print(f"重新播放视频: {video['name']}") + + def speed_up(self): + """加速播放""" + for video in self.videos: + if hasattr(video['texture'], 'getPlayRate'): + current_rate = video['texture'].getPlayRate() + new_rate = min(current_rate * 1.5, 4.0) + video['texture'].setPlayRate(new_rate) + print(f"加速播放 {video['name']}: {new_rate:.2f}x") + + def slow_down(self): + """减速播放""" + for video in self.videos: + if hasattr(video['texture'], 'getPlayRate'): + current_rate = video['texture'].getPlayRate() + new_rate = max(current_rate / 1.5, 0.25) + video['texture'].setPlayRate(new_rate) + print(f"减速播放 {video['name']}: {new_rate:.2f}x") + + def start_camera_rotation(self): + """开始相机旋转""" + if self.mouseWatcherNode.hasMouse(): + self.camera_rotating = True + self.last_mouse_x = self.mouseWatcherNode.getMouseX() + self.last_mouse_y = self.mouseWatcherNode.getMouseY() + + def stop_camera_rotation(self): + """停止相机旋转""" + self.camera_rotating = False + + def update_camera_rotation(self, task): + """更新相机旋转""" + if self.camera_rotating and self.mouseWatcherNode.hasMouse(): + mouse_x = self.mouseWatcherNode.getMouseX() + mouse_y = self.mouseWatcherNode.getMouseY() + + delta_x = mouse_x - self.last_mouse_x + delta_y = mouse_y - self.last_mouse_y + + # 更新相机方向 + self.cam.setH(self.cam.getH() - delta_x * 50) + new_p = self.cam.getP() + delta_y * 50 + self.cam.setP(max(min(new_p, 89), -89)) + + self.last_mouse_x = mouse_x + self.last_mouse_y = mouse_y + + return task.cont + + def quit_app(self): + """退出应用""" + print("退出视频播放器...") + self.userExit() + +if __name__ == "__main__": + app = VideoPlayerDemo() app.run() \ No newline at end of file diff --git a/demo/坐标轴修复说明.md b/demo/坐标轴修复说明.md index 4baf2a21..aecbd801 100644 --- a/demo/坐标轴修复说明.md +++ b/demo/坐标轴修复说明.md @@ -1,149 +1,149 @@ -# 坐标轴系统修复说明 - -## 问题描述 - -用户报告主程序中的坐标轴系统无响应: -- 鼠标悬停在坐标轴上无高亮效果 -- 点击坐标轴无法开始拖拽 -- 独立的demo程序(`standalone_gizmo_test.py`)工作正常 - -## 根本原因 - -经过分析发现,问题出在**Qt集成环境下的窗口尺寸获取不准确**: - -1. **独立demo**:直接继承自`ShowBase`,使用`self.win.getXSize()`和`self.win.getYSize()`获取窗口尺寸是准确的 -2. **主程序**:使用Qt集成的`Panda3DWorld`基类,`self.win.getXSize()`返回的尺寸与实际Qt渲染区域尺寸不匹配 -3. **坐标轴点击检测**:依赖屏幕空间投影计算,窗口尺寸错误导致投影坐标计算错误 - -## 修复方案 - -### 1. 添加Qt部件引用机制 - -在`MyWorld`类中添加: -```python -# Qt部件引用(用于获取准确的渲染区域尺寸) -self.qtWidget = None - -def setQtWidget(self, widget): - """设置Qt部件引用""" - self.qtWidget = widget - -def getWindowSize(self): - """获取准确的窗口尺寸""" - if self.qtWidget: - # 优先使用Qt部件的实际尺寸 - width, height = self.qtWidget.getActualSize() - if width > 0 and height > 0: - return width, height - - # 备用方案:使用Panda3D窗口尺寸 - if hasattr(self, 'win') and self.win: - width = self.win.getXSize() - height = self.win.getYSize() - return width, height - - # 最后的默认值 - return 800, 600 -``` - -### 2. 修改CustomPanda3DWidget - -添加方法以获取Qt部件实际尺寸: -```python -def getActualSize(self): - """获取Qt部件的实际渲染尺寸""" - return (self.width(), self.height()) -``` - -在构造函数中建立引用: -```python -# 让world引用这个widget,以便获取准确的尺寸 -if hasattr(world, 'setQtWidget'): - world.setQtWidget(self) -``` - -### 3. 修改所有屏幕坐标计算 - -将所有使用`self.win.getXSize()`和`self.win.getYSize()`的地方改为使用新的`getWindowSize()`方法: - -#### 修改前: -```python -win_x = (screen_pos.getX() + 1.0) * 0.5 * self.win.getXSize() -win_y = (1.0 - screen_pos.getY()) * 0.5 * self.win.getYSize() -``` - -#### 修改后: -```python -# 获取准确的窗口尺寸 -win_width, win_height = self.getWindowSize() - -win_x = (screen_pos.getX() + 1.0) * 0.5 * win_width -win_y = (1.0 - screen_pos.getY()) * 0.5 * win_height -``` - -### 4. 修改的关键方法 - -1. `checkGizmoClick()` - 坐标轴点击检测的主方法 -2. `updateGizmoHighlight()` - 坐标轴悬停高亮 -3. `updateGizmoDrag()` - 坐标轴拖拽更新 -4. `mousePressEventLeft()` - 鼠标点击事件处理 -5. `mouseMoveEvent()` - 鼠标移动事件处理 -6. `checkGizmoClickFallback()` - 备用点击检测方法 - -## 技术原理 - -### 屏幕空间投影算法 - -坐标轴点击检测使用以下步骤: - -1. **世界坐标 → 相机坐标**: - ```python - cam_space_pos = self.cam.getRelativePoint(self.render, world_pos) - ``` - -2. **相机坐标 → 标准化屏幕坐标**: - ```python - screen_pos = Point2() - self.cam.node().getLens().project(cam_space_pos, screen_pos) - ``` - -3. **标准化坐标 → 像素坐标**: - ```python - win_x = (screen_pos.getX() + 1.0) * 0.5 * win_width - win_y = (1.0 - screen_pos.getY()) * 0.5 * win_height - ``` - -4. **点到线段距离计算**: - ```python - distance = self.distanceToLine((mouse_x, mouse_y), center_screen, axis_screen) - ``` - -### 窗口尺寸差异的影响 - -如果窗口尺寸不准确: -- 步骤3中的像素坐标计算错误 -- 坐标轴在屏幕上的投影位置计算错误 -- 鼠标点击位置与计算出的轴位置不匹配 -- 导致点击检测失败 - -## 测试验证 - -创建了测试脚本`demo/test_size_fix.py`来验证修复效果: -- 对比Qt部件尺寸和Panda3D窗口尺寸 -- 实时监控尺寸变化 -- 验证`getWindowSize()`方法的正确性 - -## 预期效果 - -修复后的坐标轴系统应该: -1. ✅ 鼠标悬停时正确高亮对应的轴(红色X轴、绿色Y轴、蓝色Z轴变为黄色) -2. ✅ 点击坐标轴能够正确开始拖拽 -3. ✅ 拖拽时只沿选定轴方向移动物体 -4. ✅ 松开鼠标时正确结束拖拽 - -## 兼容性 - -修复方案完全向后兼容: -- 如果没有Qt环境,自动回退到使用Panda3D窗口尺寸 -- 如果Qt部件引用未设置,使用备用方案 +# 坐标轴系统修复说明 + +## 问题描述 + +用户报告主程序中的坐标轴系统无响应: +- 鼠标悬停在坐标轴上无高亮效果 +- 点击坐标轴无法开始拖拽 +- 独立的demo程序(`standalone_gizmo_test.py`)工作正常 + +## 根本原因 + +经过分析发现,问题出在**Qt集成环境下的窗口尺寸获取不准确**: + +1. **独立demo**:直接继承自`ShowBase`,使用`self.win.getXSize()`和`self.win.getYSize()`获取窗口尺寸是准确的 +2. **主程序**:使用Qt集成的`Panda3DWorld`基类,`self.win.getXSize()`返回的尺寸与实际Qt渲染区域尺寸不匹配 +3. **坐标轴点击检测**:依赖屏幕空间投影计算,窗口尺寸错误导致投影坐标计算错误 + +## 修复方案 + +### 1. 添加Qt部件引用机制 + +在`MyWorld`类中添加: +```python +# Qt部件引用(用于获取准确的渲染区域尺寸) +self.qtWidget = None + +def setQtWidget(self, widget): + """设置Qt部件引用""" + self.qtWidget = widget + +def getWindowSize(self): + """获取准确的窗口尺寸""" + if self.qtWidget: + # 优先使用Qt部件的实际尺寸 + width, height = self.qtWidget.getActualSize() + if width > 0 and height > 0: + return width, height + + # 备用方案:使用Panda3D窗口尺寸 + if hasattr(self, 'win') and self.win: + width = self.win.getXSize() + height = self.win.getYSize() + return width, height + + # 最后的默认值 + return 800, 600 +``` + +### 2. 修改CustomPanda3DWidget + +添加方法以获取Qt部件实际尺寸: +```python +def getActualSize(self): + """获取Qt部件的实际渲染尺寸""" + return (self.width(), self.height()) +``` + +在构造函数中建立引用: +```python +# 让world引用这个widget,以便获取准确的尺寸 +if hasattr(world, 'setQtWidget'): + world.setQtWidget(self) +``` + +### 3. 修改所有屏幕坐标计算 + +将所有使用`self.win.getXSize()`和`self.win.getYSize()`的地方改为使用新的`getWindowSize()`方法: + +#### 修改前: +```python +win_x = (screen_pos.getX() + 1.0) * 0.5 * self.win.getXSize() +win_y = (1.0 - screen_pos.getY()) * 0.5 * self.win.getYSize() +``` + +#### 修改后: +```python +# 获取准确的窗口尺寸 +win_width, win_height = self.getWindowSize() + +win_x = (screen_pos.getX() + 1.0) * 0.5 * win_width +win_y = (1.0 - screen_pos.getY()) * 0.5 * win_height +``` + +### 4. 修改的关键方法 + +1. `checkGizmoClick()` - 坐标轴点击检测的主方法 +2. `updateGizmoHighlight()` - 坐标轴悬停高亮 +3. `updateGizmoDrag()` - 坐标轴拖拽更新 +4. `mousePressEventLeft()` - 鼠标点击事件处理 +5. `mouseMoveEvent()` - 鼠标移动事件处理 +6. `checkGizmoClickFallback()` - 备用点击检测方法 + +## 技术原理 + +### 屏幕空间投影算法 + +坐标轴点击检测使用以下步骤: + +1. **世界坐标 → 相机坐标**: + ```python + cam_space_pos = self.cam.getRelativePoint(self.render, world_pos) + ``` + +2. **相机坐标 → 标准化屏幕坐标**: + ```python + screen_pos = Point2() + self.cam.node().getLens().project(cam_space_pos, screen_pos) + ``` + +3. **标准化坐标 → 像素坐标**: + ```python + win_x = (screen_pos.getX() + 1.0) * 0.5 * win_width + win_y = (1.0 - screen_pos.getY()) * 0.5 * win_height + ``` + +4. **点到线段距离计算**: + ```python + distance = self.distanceToLine((mouse_x, mouse_y), center_screen, axis_screen) + ``` + +### 窗口尺寸差异的影响 + +如果窗口尺寸不准确: +- 步骤3中的像素坐标计算错误 +- 坐标轴在屏幕上的投影位置计算错误 +- 鼠标点击位置与计算出的轴位置不匹配 +- 导致点击检测失败 + +## 测试验证 + +创建了测试脚本`demo/test_size_fix.py`来验证修复效果: +- 对比Qt部件尺寸和Panda3D窗口尺寸 +- 实时监控尺寸变化 +- 验证`getWindowSize()`方法的正确性 + +## 预期效果 + +修复后的坐标轴系统应该: +1. ✅ 鼠标悬停时正确高亮对应的轴(红色X轴、绿色Y轴、蓝色Z轴变为黄色) +2. ✅ 点击坐标轴能够正确开始拖拽 +3. ✅ 拖拽时只沿选定轴方向移动物体 +4. ✅ 松开鼠标时正确结束拖拽 + +## 兼容性 + +修复方案完全向后兼容: +- 如果没有Qt环境,自动回退到使用Panda3D窗口尺寸 +- 如果Qt部件引用未设置,使用备用方案 - 不影响独立的ShowBase应用程序 \ No newline at end of file diff --git a/demo/射线坐标系统修复说明.md b/demo/射线坐标系统修复说明.md index 32a92dd0..8aa8f0a9 100644 --- a/demo/射线坐标系统修复说明.md +++ b/demo/射线坐标系统修复说明.md @@ -1,126 +1,126 @@ -# 射线显示坐标系统修复说明 - -## 🔍 问题描述 - -用户发现射线显示时是从场景中心点射出,而不是从相机位置射出。这违反了鼠标点击射线的基本原理。 - -## ⚡ 问题原因 - -### 1. **坐标系混淆** -```python -# 原始错误代码 -nearPoint = Point3() -farPoint = Point3() -self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint) - -# 直接使用相机坐标系的点显示射线(错误!) -self.showClickRay(nearPoint, farPoint, hitPos) -``` - -### 2. **坐标系不匹配** -- `lens.extrude()` 返回的是**相机坐标系**中的点 -- 射线显示节点挂在 `render` 下,使用**世界坐标系** -- 直接使用相机坐标系的点会导致射线从错误位置显示 - -## 🛠 修复方案 - -### 1. **正确的坐标变换** -```python -# 获取相机坐标系中的射线点 -nearPoint = Point3() -farPoint = Point3() -self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint) - -# 转换到世界坐标系用于显示 -worldNearPoint = self.world.render.getRelativePoint(self.world.cam, nearPoint) -worldFarPoint = self.world.render.getRelativePoint(self.world.cam, farPoint) - -# 使用世界坐标系的点显示射线 -self.showClickRay(worldNearPoint, worldFarPoint, hitPos) -``` - -### 2. **碰撞检测保持不变** -```python -# 碰撞检测仍使用相机坐标系(正确!) -pickerNode = CollisionNode('mouseRay') -pickerNP = self.world.cam.attachNewNode(pickerNode) # 相机子节点 -direction = farPoint - nearPoint -direction.normalize() -pickerNode.addSolid(CollisionRay(nearPoint, direction)) # 相机坐标系 -``` - -## 📐 坐标系统详解 - -### **相机坐标系 vs 世界坐标系** - -| 坐标系 | 用途 | 特点 | -|--------|------|------| -| 相机坐标系 | 碰撞检测 | 以相机为原点,Z轴向前 | -| 世界坐标系 | 射线显示 | 以场景为原点,固定坐标 | - -### **为什么需要不同坐标系?** - -1. **碰撞检测**: - - 碰撞节点是相机的子节点 - - 使用相机坐标系可以跟随相机移动 - - 射线方向相对于相机计算 - -2. **射线显示**: - - 射线节点是render的子节点 - - 需要在世界坐标系中显示固定位置 - - 从相机真实位置到点击点 - -## 🎯 修复效果 - -### **修复前** -``` -射线起点: (相机坐标系原点) = 场景中心附近 -射线方向: 正确,但起点错误 -显示效果: 射线从场景中心发射 ❌ -``` - -### **修复后** -``` -射线起点: (相机世界位置) = 真实相机位置 -射线方向: 正确,指向鼠标点击方向 -显示效果: 射线从相机位置发射 ✅ -``` - -## 🧪 验证方法 - -1. **启动射线测试**: - ```bash - python demo/ray_display_test.py - ``` - -2. **按R键开启射线显示** - -3. **移动相机到不同位置** - -4. **点击鼠标观察射线**: - - 射线应该从当前相机位置开始 - - 射线应该指向鼠标点击的方向 - - 相机移动后射线起点应该跟随变化 - -## 📋 技术要点 - -### **关键API使用** -```python -# 获取相机坐标系中的射线 -lens.extrude(screen_point, near_point, far_point) - -# 坐标系转换 -world_point = render.getRelativePoint(camera, camera_point) - -# 反向转换 -camera_point = camera.getRelativePoint(render, world_point) -``` - -### **调试输出** -现在会显示两套坐标: -``` -相机坐标系射线起点: (0, 1, 0) -世界坐标系射线起点: (-15.2, -42.3, 18.7) -``` - +# 射线显示坐标系统修复说明 + +## 🔍 问题描述 + +用户发现射线显示时是从场景中心点射出,而不是从相机位置射出。这违反了鼠标点击射线的基本原理。 + +## ⚡ 问题原因 + +### 1. **坐标系混淆** +```python +# 原始错误代码 +nearPoint = Point3() +farPoint = Point3() +self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint) + +# 直接使用相机坐标系的点显示射线(错误!) +self.showClickRay(nearPoint, farPoint, hitPos) +``` + +### 2. **坐标系不匹配** +- `lens.extrude()` 返回的是**相机坐标系**中的点 +- 射线显示节点挂在 `render` 下,使用**世界坐标系** +- 直接使用相机坐标系的点会导致射线从错误位置显示 + +## 🛠 修复方案 + +### 1. **正确的坐标变换** +```python +# 获取相机坐标系中的射线点 +nearPoint = Point3() +farPoint = Point3() +self.world.cam.node().getLens().extrude(Point2(mx, my), nearPoint, farPoint) + +# 转换到世界坐标系用于显示 +worldNearPoint = self.world.render.getRelativePoint(self.world.cam, nearPoint) +worldFarPoint = self.world.render.getRelativePoint(self.world.cam, farPoint) + +# 使用世界坐标系的点显示射线 +self.showClickRay(worldNearPoint, worldFarPoint, hitPos) +``` + +### 2. **碰撞检测保持不变** +```python +# 碰撞检测仍使用相机坐标系(正确!) +pickerNode = CollisionNode('mouseRay') +pickerNP = self.world.cam.attachNewNode(pickerNode) # 相机子节点 +direction = farPoint - nearPoint +direction.normalize() +pickerNode.addSolid(CollisionRay(nearPoint, direction)) # 相机坐标系 +``` + +## 📐 坐标系统详解 + +### **相机坐标系 vs 世界坐标系** + +| 坐标系 | 用途 | 特点 | +|--------|------|------| +| 相机坐标系 | 碰撞检测 | 以相机为原点,Z轴向前 | +| 世界坐标系 | 射线显示 | 以场景为原点,固定坐标 | + +### **为什么需要不同坐标系?** + +1. **碰撞检测**: + - 碰撞节点是相机的子节点 + - 使用相机坐标系可以跟随相机移动 + - 射线方向相对于相机计算 + +2. **射线显示**: + - 射线节点是render的子节点 + - 需要在世界坐标系中显示固定位置 + - 从相机真实位置到点击点 + +## 🎯 修复效果 + +### **修复前** +``` +射线起点: (相机坐标系原点) = 场景中心附近 +射线方向: 正确,但起点错误 +显示效果: 射线从场景中心发射 ❌ +``` + +### **修复后** +``` +射线起点: (相机世界位置) = 真实相机位置 +射线方向: 正确,指向鼠标点击方向 +显示效果: 射线从相机位置发射 ✅ +``` + +## 🧪 验证方法 + +1. **启动射线测试**: + ```bash + python demo/ray_display_test.py + ``` + +2. **按R键开启射线显示** + +3. **移动相机到不同位置** + +4. **点击鼠标观察射线**: + - 射线应该从当前相机位置开始 + - 射线应该指向鼠标点击的方向 + - 相机移动后射线起点应该跟随变化 + +## 📋 技术要点 + +### **关键API使用** +```python +# 获取相机坐标系中的射线 +lens.extrude(screen_point, near_point, far_point) + +# 坐标系转换 +world_point = render.getRelativePoint(camera, camera_point) + +# 反向转换 +camera_point = camera.getRelativePoint(render, world_point) +``` + +### **调试输出** +现在会显示两套坐标: +``` +相机坐标系射线起点: (0, 1, 0) +世界坐标系射线起点: (-15.2, -42.3, 18.7) +``` + 这样您就可以清楚地看到射线现在是从真实的相机位置发射,而不是从场景中心发射了!🎯 \ No newline at end of file diff --git a/demo/缩放位置修复说明.md b/demo/缩放位置修复说明.md index b90316cc..7b7a6f96 100644 --- a/demo/缩放位置修复说明.md +++ b/demo/缩放位置修复说明.md @@ -1,150 +1,150 @@ -# FBX模型缩放标准化位置修复说明 - -## 问题描述 - -在FBX模型导入时,经常遇到子节点有大缩放值(如100)的问题。我们的缩放标准化功能可以将这些大缩放值调整为1,但最初的实现存在一个重要问题:**只调整了缩放,没有相应调整位置,导致子节点之间的距离变得过大**。 - -## 问题分析 - -### 原始FBX结构(示例) -``` -根节点 (缩放: 1.0, 位置: 0,0,0) -├── 子节点A (缩放: 100, 位置: 0,0,0) -├── 子节点B (缩放: 100, 位置: 60,5,320) -└── 子节点C (缩放: 100, 位置: -16,-3,-347) -``` - -### 问题:仅缩放标准化 -``` -根节点 (缩放: 1.0, 位置: 0,0,0) -├── 子节点A (缩放: 1.0, 位置: 0,0,0) ✓ 正常 -├── 子节点B (缩放: 1.0, 位置: 60,5,320) ✗ 距离过大! -└── 子节点C (缩放: 1.0, 位置: -16,-3,-347) ✗ 距离过大! -``` - -**问题根源**:原来在100倍缩放下,(60,5,320)这样的位置是合理的,因为视觉上的有效距离会被缩放影响。但当缩放变成1时,这个位置就显得过大了。 - -## 解决方案 - -### 核心思路 -当我们将子节点的缩放按比例缩小时,也要将它们的位置按相同比例缩小,以保持视觉上的相对关系。 - -### 修复后的标准化 -``` -根节点 (缩放: 1.0, 位置: 0,0,0) -├── 子节点A (缩放: 1.0, 位置: 0,0,0) ✓ 正常 -├── 子节点B (缩放: 1.0, 位置: 0.6,0.05,3.2) ✓ 距离合理 -└── 子节点C (缩放: 1.0, 位置: -0.16,-0.03,-3.47) ✓ 距离合理 -``` - -## 技术实现 - -### 修复前的代码 -```python -def _applyScaleNormalization(self, node, normalize_factor, depth=0): - # 只调整缩放 - if max_scale_component > 10: - new_scale = current_scale * normalize_factor - node.setScale(new_scale) - # 位置保持不变 - 这是问题所在! -``` - -### 修复后的代码 -```python -def _applyScaleNormalization(self, node, normalize_factor, depth=0): - # 同时调整缩放和位置 - if max_scale_component > 10: - # 应用新的缩放 - new_scale = current_scale * normalize_factor - node.setScale(new_scale) - - # 同时调整位置:保持视觉相对位置一致 - new_pos = current_pos * normalize_factor - node.setPos(new_pos) -``` - -## 效果对比 - -### 修复前 -- ✅ 缩放值正确(100 → 1.0) -- ❌ 子节点距离过大(几百个单位) -- ❌ 视觉布局异常 - -### 修复后 -- ✅ 缩放值正确(100 → 1.0) -- ✅ 子节点距离合理(几个单位) -- ✅ 视觉布局保持原有比例关系 - -## 测试验证 - -使用测试脚本验证修复效果: - -### 1. 位置测试脚本 -```bash -cd demo -python scale_position_test.py -``` - -按T键运行完整的位置测试,检查: -- 缩放因子是否正确(100 → 1.0) -- 位置缩放是否正确(位置 × 0.01) -- 相对位置关系是否保持 - -### 2. FBX导入测试 -```bash -cd demo -python fbx_import_test.py -``` - -导入实际的FBX文件,使用I键查看模型信息,确认: -- 子节点缩放标准化为1.0 -- 子节点位置为合理数值 -- 整体视觉效果正常 - -## 算法详解 - -### 标准化因子计算 -1. **收集缩放信息**:递归扫描所有节点的缩放值 -2. **识别大缩放**:找出缩放值 > 10 的节点 -3. **统计最常见值**:使用Counter找到最频繁的大缩放值(如100) -4. **计算标准化因子**:normalize_factor = 1.0 / common_large_scale - -### 同步调整规则 -```python -# 对于每个需要标准化的节点 -if max_scale_component > 10: - new_scale = current_scale * normalize_factor # 缩放调整 - new_pos = current_pos * normalize_factor # 位置同步调整 -``` - -## 配置选项 - -在`importModel`方法中提供灵活配置: - -```python -# 推荐配置(默认) -world.importModel("model.fbx", - apply_unit_conversion=False, # 不使用传统单位转换 - normalize_scales=True) # 使用智能缩放标准化 - -# 传统配置 -world.importModel("model.fbx", - apply_unit_conversion=True, # 使用传统0.01根缩放 - normalize_scales=False) # 不使用智能标准化 - -# 完全保持原始 -world.importModel("model.fbx", - apply_unit_conversion=False, # 不转换 - normalize_scales=False) # 不标准化 -``` - -## 总结 - -这个修复解决了FBX模型缩放标准化时的关键问题: - -1. **保持视觉一致性**:子节点之间的相对位置关系不变 -2. **简化层级结构**:避免复杂的缩放层级组合 -3. **提高易用性**:导入后的模型结构直观易懂 -4. **兼容性好**:不影响现有功能,提供可选配置 - +# FBX模型缩放标准化位置修复说明 + +## 问题描述 + +在FBX模型导入时,经常遇到子节点有大缩放值(如100)的问题。我们的缩放标准化功能可以将这些大缩放值调整为1,但最初的实现存在一个重要问题:**只调整了缩放,没有相应调整位置,导致子节点之间的距离变得过大**。 + +## 问题分析 + +### 原始FBX结构(示例) +``` +根节点 (缩放: 1.0, 位置: 0,0,0) +├── 子节点A (缩放: 100, 位置: 0,0,0) +├── 子节点B (缩放: 100, 位置: 60,5,320) +└── 子节点C (缩放: 100, 位置: -16,-3,-347) +``` + +### 问题:仅缩放标准化 +``` +根节点 (缩放: 1.0, 位置: 0,0,0) +├── 子节点A (缩放: 1.0, 位置: 0,0,0) ✓ 正常 +├── 子节点B (缩放: 1.0, 位置: 60,5,320) ✗ 距离过大! +└── 子节点C (缩放: 1.0, 位置: -16,-3,-347) ✗ 距离过大! +``` + +**问题根源**:原来在100倍缩放下,(60,5,320)这样的位置是合理的,因为视觉上的有效距离会被缩放影响。但当缩放变成1时,这个位置就显得过大了。 + +## 解决方案 + +### 核心思路 +当我们将子节点的缩放按比例缩小时,也要将它们的位置按相同比例缩小,以保持视觉上的相对关系。 + +### 修复后的标准化 +``` +根节点 (缩放: 1.0, 位置: 0,0,0) +├── 子节点A (缩放: 1.0, 位置: 0,0,0) ✓ 正常 +├── 子节点B (缩放: 1.0, 位置: 0.6,0.05,3.2) ✓ 距离合理 +└── 子节点C (缩放: 1.0, 位置: -0.16,-0.03,-3.47) ✓ 距离合理 +``` + +## 技术实现 + +### 修复前的代码 +```python +def _applyScaleNormalization(self, node, normalize_factor, depth=0): + # 只调整缩放 + if max_scale_component > 10: + new_scale = current_scale * normalize_factor + node.setScale(new_scale) + # 位置保持不变 - 这是问题所在! +``` + +### 修复后的代码 +```python +def _applyScaleNormalization(self, node, normalize_factor, depth=0): + # 同时调整缩放和位置 + if max_scale_component > 10: + # 应用新的缩放 + new_scale = current_scale * normalize_factor + node.setScale(new_scale) + + # 同时调整位置:保持视觉相对位置一致 + new_pos = current_pos * normalize_factor + node.setPos(new_pos) +``` + +## 效果对比 + +### 修复前 +- ✅ 缩放值正确(100 → 1.0) +- ❌ 子节点距离过大(几百个单位) +- ❌ 视觉布局异常 + +### 修复后 +- ✅ 缩放值正确(100 → 1.0) +- ✅ 子节点距离合理(几个单位) +- ✅ 视觉布局保持原有比例关系 + +## 测试验证 + +使用测试脚本验证修复效果: + +### 1. 位置测试脚本 +```bash +cd demo +python scale_position_test.py +``` + +按T键运行完整的位置测试,检查: +- 缩放因子是否正确(100 → 1.0) +- 位置缩放是否正确(位置 × 0.01) +- 相对位置关系是否保持 + +### 2. FBX导入测试 +```bash +cd demo +python fbx_import_test.py +``` + +导入实际的FBX文件,使用I键查看模型信息,确认: +- 子节点缩放标准化为1.0 +- 子节点位置为合理数值 +- 整体视觉效果正常 + +## 算法详解 + +### 标准化因子计算 +1. **收集缩放信息**:递归扫描所有节点的缩放值 +2. **识别大缩放**:找出缩放值 > 10 的节点 +3. **统计最常见值**:使用Counter找到最频繁的大缩放值(如100) +4. **计算标准化因子**:normalize_factor = 1.0 / common_large_scale + +### 同步调整规则 +```python +# 对于每个需要标准化的节点 +if max_scale_component > 10: + new_scale = current_scale * normalize_factor # 缩放调整 + new_pos = current_pos * normalize_factor # 位置同步调整 +``` + +## 配置选项 + +在`importModel`方法中提供灵活配置: + +```python +# 推荐配置(默认) +world.importModel("model.fbx", + apply_unit_conversion=False, # 不使用传统单位转换 + normalize_scales=True) # 使用智能缩放标准化 + +# 传统配置 +world.importModel("model.fbx", + apply_unit_conversion=True, # 使用传统0.01根缩放 + normalize_scales=False) # 不使用智能标准化 + +# 完全保持原始 +world.importModel("model.fbx", + apply_unit_conversion=False, # 不转换 + normalize_scales=False) # 不标准化 +``` + +## 总结 + +这个修复解决了FBX模型缩放标准化时的关键问题: + +1. **保持视觉一致性**:子节点之间的相对位置关系不变 +2. **简化层级结构**:避免复杂的缩放层级组合 +3. **提高易用性**:导入后的模型结构直观易懂 +4. **兼容性好**:不影响现有功能,提供可选配置 + 这确保了"一键导入,完美显示"的用户体验,解决了FBX模型导入中的核心痛点。 \ No newline at end of file diff --git a/demo/脚本管理界面使用指南.md b/demo/脚本管理界面使用指南.md index fa680a14..c15aaa0f 100644 --- a/demo/脚本管理界面使用指南.md +++ b/demo/脚本管理界面使用指南.md @@ -1,205 +1,205 @@ -# 脚本管理界面使用指南 - -## 概述 - -脚本管理界面已集成到主程序中,提供了完整的脚本创建、管理、挂载和使用功能。编辑功能可以通过外部编辑器进行。 - -## 功能特性 - -### 1. 脚本系统管理 -- ✅ 脚本创建和模板选择 -- ✅ 脚本加载和重载 -- ✅ 热重载功能(自动检测文件变化) -- ✅ 脚本挂载到游戏对象 -- ✅ 脚本启用/禁用控制 - -### 2. 界面组件 -- ✅ 脚本菜单(菜单栏) -- ✅ 脚本管理面板(右侧停靠窗口) -- ✅ 属性面板脚本信息显示 -- ✅ 实时状态更新 - -## 使用方法 - -### 访问脚本功能 - -#### 方法1:通过菜单栏 -1. 点击菜单栏 → **脚本** -2. 选择相应的功能: - - **创建脚本...** - 快速创建新脚本 - - **加载脚本文件...** - 从文件加载脚本 - - **重载所有脚本** - 重新加载所有脚本 - - **启用热重载** - 切换热重载功能 - - **脚本管理器** - 打开脚本管理面板 - -#### 方法2:通过脚本管理面板 -1. 查看右侧停靠窗口 -2. 点击 **脚本管理** 标签页 -3. 使用面板中的各种控件 - -### 脚本管理面板详解 - -#### 脚本系统状态组 -- **脚本系统状态**:显示系统是否正在运行 -- **热重载状态**:显示热重载是否启用 - -#### 创建脚本组 -1. **脚本名称**:输入新脚本的名称 -2. **模板选择**:选择脚本模板 - - `basic`:基础脚本模板 - - `movement`:移动相关脚本模板 - - `animation`:动画相关脚本模板 -3. **创建脚本**:点击按钮创建脚本文件 - -#### 可用脚本组 -- **脚本列表**:显示所有可用的脚本 -- **双击脚本名称**:显示提示(将来可打开外部编辑器) -- **加载脚本**:重新加载选中的脚本 -- **重载全部**:重新加载所有脚本 - -#### 脚本挂载组 -- **选中对象显示**:显示当前选中的游戏对象 -- **脚本选择**:从下拉列表选择要挂载的脚本 -- **挂载按钮**:将脚本挂载到选中对象 -- **已挂载脚本列表**:显示对象上的所有脚本及其状态 - - ✓ 表示脚本已启用 - - ✗ 表示脚本已禁用 -- **卸载选中脚本**:从对象移除选中的脚本 - -### 属性面板脚本信息 - -当选择一个游戏对象时,属性面板会显示: - -#### 脚本信息区域 -- **已挂载脚本**:列出对象上的所有脚本 -- **脚本名称**:显示每个脚本的名称和状态 -- **启用/禁用按钮**:控制每个脚本的运行状态 - - 绿色按钮:脚本已启用,点击禁用 - - 红色按钮:脚本已禁用,点击启用 - -## 使用流程示例 - -### 创建和使用脚本的完整流程 - -1. **创建脚本** - ``` - 菜单栏 → 脚本 → 创建脚本... - 输入名称:MyRotator - 点击确定 - ``` - -2. **编辑脚本**(外部编辑器) - ``` - 打开 scripts/MyRotator.py - 编辑脚本内容 - 保存文件(热重载会自动检测) - ``` - -3. **挂载脚本到对象** - ``` - a. 在场景中选择一个对象 - b. 右侧脚本管理面板 → 脚本挂载组 - c. 选择 MyRotator 脚本 - d. 点击"挂载" - ``` - -4. **管理脚本状态** - ``` - 在属性面板中: - - 查看脚本信息 - - 启用/禁用脚本 - - 监控脚本运行状态 - ``` - -### 热重载功能使用 - -1. **启用热重载** - ``` - 菜单栏 → 脚本 → 启用热重载 ✓ - ``` - -2. **编辑脚本文件** - ``` - 使用任何文本编辑器修改 scripts/ 目录下的脚本 - 保存文件 - ``` - -3. **自动重载** - ``` - 系统会自动检测文件变化并重新加载脚本 - 已挂载的脚本会自动更新 - ``` - -## 快捷操作 - -### 键盘快捷键 -- 目前没有设置特定的快捷键,主要通过鼠标操作 - -### 常用操作流程 -1. **快速创建脚本**:菜单 → 脚本 → 创建脚本... -2. **批量重载**:菜单 → 脚本 → 重载所有脚本 -3. **脚本管理**:右侧脚本管理面板 -4. **状态切换**:属性面板中的启用/禁用按钮 - -## 注意事项 - -### 脚本编辑 -- 脚本编辑需要使用外部编辑器(如VS Code、PyCharm等) -- 脚本文件保存在 `scripts/` 目录下 -- 热重载功能会监控文件变化并自动更新 - -### 对象选择 -- 脚本挂载需要先选择游戏对象 -- 在场景树(左侧面板)中点击对象进行选择 -- 选中对象后脚本挂载功能才会启用 - -### 错误处理 -- 脚本加载错误会显示在控制台 -- 创建重复名称的脚本会提示错误 -- 挂载/卸载操作会有成功/失败提示 - -## 技术特性 - -### 性能优化 -- 使用 Panda3D 任务系统进行脚本更新 -- 热重载使用文件监控,避免频繁检查 -- 脚本组件化设计,便于管理 - -### 扩展性 -- 支持多种脚本模板 -- 可扩展的脚本 API -- 模块化的脚本系统架构 - -## 故障排除 - -### 常见问题 - -1. **脚本创建失败** - - 检查脚本名称是否合法 - - 确保 scripts/ 目录存在 - - 检查文件权限 - -2. **热重载不工作** - - 确认热重载已启用 - - 检查文件监控服务是否正常 - - 重启应用程序 - -3. **脚本挂载失败** - - 确认已选择游戏对象 - - 检查脚本是否已加载 - - 查看控制台错误信息 - -4. **脚本无法运行** - - 检查脚本语法错误 - - 确认脚本已启用 - - 查看脚本系统状态 - -### 调试建议 -- 使用控制台输出查看详细错误信息 -- 检查脚本文件的语法和逻辑 -- 确认脚本系统正常运行 -- 重载脚本或重启应用来解决问题 - -## 总结 - +# 脚本管理界面使用指南 + +## 概述 + +脚本管理界面已集成到主程序中,提供了完整的脚本创建、管理、挂载和使用功能。编辑功能可以通过外部编辑器进行。 + +## 功能特性 + +### 1. 脚本系统管理 +- ✅ 脚本创建和模板选择 +- ✅ 脚本加载和重载 +- ✅ 热重载功能(自动检测文件变化) +- ✅ 脚本挂载到游戏对象 +- ✅ 脚本启用/禁用控制 + +### 2. 界面组件 +- ✅ 脚本菜单(菜单栏) +- ✅ 脚本管理面板(右侧停靠窗口) +- ✅ 属性面板脚本信息显示 +- ✅ 实时状态更新 + +## 使用方法 + +### 访问脚本功能 + +#### 方法1:通过菜单栏 +1. 点击菜单栏 → **脚本** +2. 选择相应的功能: + - **创建脚本...** - 快速创建新脚本 + - **加载脚本文件...** - 从文件加载脚本 + - **重载所有脚本** - 重新加载所有脚本 + - **启用热重载** - 切换热重载功能 + - **脚本管理器** - 打开脚本管理面板 + +#### 方法2:通过脚本管理面板 +1. 查看右侧停靠窗口 +2. 点击 **脚本管理** 标签页 +3. 使用面板中的各种控件 + +### 脚本管理面板详解 + +#### 脚本系统状态组 +- **脚本系统状态**:显示系统是否正在运行 +- **热重载状态**:显示热重载是否启用 + +#### 创建脚本组 +1. **脚本名称**:输入新脚本的名称 +2. **模板选择**:选择脚本模板 + - `basic`:基础脚本模板 + - `movement`:移动相关脚本模板 + - `animation`:动画相关脚本模板 +3. **创建脚本**:点击按钮创建脚本文件 + +#### 可用脚本组 +- **脚本列表**:显示所有可用的脚本 +- **双击脚本名称**:显示提示(将来可打开外部编辑器) +- **加载脚本**:重新加载选中的脚本 +- **重载全部**:重新加载所有脚本 + +#### 脚本挂载组 +- **选中对象显示**:显示当前选中的游戏对象 +- **脚本选择**:从下拉列表选择要挂载的脚本 +- **挂载按钮**:将脚本挂载到选中对象 +- **已挂载脚本列表**:显示对象上的所有脚本及其状态 + - ✓ 表示脚本已启用 + - ✗ 表示脚本已禁用 +- **卸载选中脚本**:从对象移除选中的脚本 + +### 属性面板脚本信息 + +当选择一个游戏对象时,属性面板会显示: + +#### 脚本信息区域 +- **已挂载脚本**:列出对象上的所有脚本 +- **脚本名称**:显示每个脚本的名称和状态 +- **启用/禁用按钮**:控制每个脚本的运行状态 + - 绿色按钮:脚本已启用,点击禁用 + - 红色按钮:脚本已禁用,点击启用 + +## 使用流程示例 + +### 创建和使用脚本的完整流程 + +1. **创建脚本** + ``` + 菜单栏 → 脚本 → 创建脚本... + 输入名称:MyRotator + 点击确定 + ``` + +2. **编辑脚本**(外部编辑器) + ``` + 打开 scripts/MyRotator.py + 编辑脚本内容 + 保存文件(热重载会自动检测) + ``` + +3. **挂载脚本到对象** + ``` + a. 在场景中选择一个对象 + b. 右侧脚本管理面板 → 脚本挂载组 + c. 选择 MyRotator 脚本 + d. 点击"挂载" + ``` + +4. **管理脚本状态** + ``` + 在属性面板中: + - 查看脚本信息 + - 启用/禁用脚本 + - 监控脚本运行状态 + ``` + +### 热重载功能使用 + +1. **启用热重载** + ``` + 菜单栏 → 脚本 → 启用热重载 ✓ + ``` + +2. **编辑脚本文件** + ``` + 使用任何文本编辑器修改 scripts/ 目录下的脚本 + 保存文件 + ``` + +3. **自动重载** + ``` + 系统会自动检测文件变化并重新加载脚本 + 已挂载的脚本会自动更新 + ``` + +## 快捷操作 + +### 键盘快捷键 +- 目前没有设置特定的快捷键,主要通过鼠标操作 + +### 常用操作流程 +1. **快速创建脚本**:菜单 → 脚本 → 创建脚本... +2. **批量重载**:菜单 → 脚本 → 重载所有脚本 +3. **脚本管理**:右侧脚本管理面板 +4. **状态切换**:属性面板中的启用/禁用按钮 + +## 注意事项 + +### 脚本编辑 +- 脚本编辑需要使用外部编辑器(如VS Code、PyCharm等) +- 脚本文件保存在 `scripts/` 目录下 +- 热重载功能会监控文件变化并自动更新 + +### 对象选择 +- 脚本挂载需要先选择游戏对象 +- 在场景树(左侧面板)中点击对象进行选择 +- 选中对象后脚本挂载功能才会启用 + +### 错误处理 +- 脚本加载错误会显示在控制台 +- 创建重复名称的脚本会提示错误 +- 挂载/卸载操作会有成功/失败提示 + +## 技术特性 + +### 性能优化 +- 使用 Panda3D 任务系统进行脚本更新 +- 热重载使用文件监控,避免频繁检查 +- 脚本组件化设计,便于管理 + +### 扩展性 +- 支持多种脚本模板 +- 可扩展的脚本 API +- 模块化的脚本系统架构 + +## 故障排除 + +### 常见问题 + +1. **脚本创建失败** + - 检查脚本名称是否合法 + - 确保 scripts/ 目录存在 + - 检查文件权限 + +2. **热重载不工作** + - 确认热重载已启用 + - 检查文件监控服务是否正常 + - 重启应用程序 + +3. **脚本挂载失败** + - 确认已选择游戏对象 + - 检查脚本是否已加载 + - 查看控制台错误信息 + +4. **脚本无法运行** + - 检查脚本语法错误 + - 确认脚本已启用 + - 查看脚本系统状态 + +### 调试建议 +- 使用控制台输出查看详细错误信息 +- 检查脚本文件的语法和逻辑 +- 确认脚本系统正常运行 +- 重载脚本或重启应用来解决问题 + +## 总结 + 脚本管理界面提供了完整的脚本生命周期管理功能,从创建、编辑、加载到挂载和运行,都可以通过直观的图形界面操作。配合热重载功能,可以实现高效的脚本开发工作流。 \ No newline at end of file diff --git a/demo/脚本管理界面实现总结.md b/demo/脚本管理界面实现总结.md index 86dffdfd..e257c9ae 100644 --- a/demo/脚本管理界面实现总结.md +++ b/demo/脚本管理界面实现总结.md @@ -1,205 +1,205 @@ -# 脚本管理界面实现总结 - -## 📋 项目概述 - -已成功为主程序实现了完整的脚本管理界面,提供了脚本创建、管理、挂载和使用功能。用户可以通过图形界面管理脚本,而脚本编辑可以通过外部编辑器进行。 - -## ✅ 已实现功能 - -### 1. 脚本系统核心功能 -- **✅ 脚本引擎**:集成到Panda3D任务系统,提供稳定的脚本执行环境 -- **✅ 脚本加载器**:支持动态加载Python脚本文件,具有错误处理机制 -- **✅ 热重载系统**:自动监控文件变化,实时重新加载修改的脚本 -- **✅ 脚本API**:为脚本提供游戏引擎功能接口 -- **✅ 组件化管理**:脚本以组件形式挂载到游戏对象 - -### 2. 用户界面组件 - -#### 📋 脚本菜单(菜单栏) -- **创建脚本...** - 快速创建新脚本对话框 -- **加载脚本文件...** - 从文件系统加载脚本 -- **重载所有脚本** - 批量重新加载所有脚本 -- **启用热重载** - 切换热重载功能开关 -- **脚本管理器** - 打开脚本管理面板 - -#### 🛠️ 脚本管理面板(右侧停靠窗口) -**脚本系统状态组:** -- 实时显示脚本系统运行状态 -- 显示热重载启用/禁用状态 - -**创建脚本组:** -- 脚本名称输入框 -- 模板选择下拉菜单(basic、movement、animation) -- 创建脚本按钮 - -**可用脚本组:** -- 脚本列表显示(支持双击操作) -- 加载脚本按钮 -- 重载全部按钮 - -**脚本挂载组:** -- 当前选中对象显示 -- 脚本选择下拉菜单 -- 挂载/卸载按钮 -- 已挂载脚本列表(显示启用状态) - -#### 📊 属性面板集成 -- 显示对象上的所有挂载脚本 -- 脚本名称和状态显示 -- 启用/禁用按钮控制 -- 实时状态更新 - -### 3. 脚本模板系统 -- **Basic模板**:基础脚本结构,包含start()和update()方法 -- **Movement模板**:移动相关脚本,包含位置变换功能 -- **Animation模板**:动画相关脚本模板 - -### 4. 交互功能 -- **实时状态更新**:定时器每秒更新界面状态 -- **对象选择集成**:与现有选择系统完美集成 -- **错误处理**:完整的错误提示和异常处理 -- **用户反馈**:操作成功/失败的消息提示 - -## 🎯 功能验证 - -### 测试结果 -通过 `script_gui_test.py` 测试验证了以下功能: - -✅ **脚本系统初始化**: -- 脚本系统启动正常 -- 热重载功能启用 -- 可用脚本数量统计正确 - -✅ **脚本创建功能**: -- 成功创建TestRotator、TestMover、TestScaler脚本 -- 脚本文件正确生成到scripts/目录 - -✅ **脚本加载功能**: -- 成功加载5个脚本文件 -- 动态模块导入正常工作 - -✅ **脚本挂载功能**: -- 成功将脚本挂载到游戏对象 -- 脚本组件正确创建和管理 - -✅ **界面交互功能**: -- 对象选择功能正常 -- 属性面板更新正常 -- 坐标轴拖拽功能正常 - -## 📁 文件结构 - -### 核心文件 -``` -├── core/ -│ ├── script_system.py # 脚本系统核心实现 -│ └── __init__.py # 导出脚本系统类 -├── ui/ -│ ├── main_window.py # 主窗口,包含脚本管理界面 -│ └── property_panel.py # 属性面板,包含脚本信息显示 -├── main.py # 主程序,集成脚本系统 -└── scripts/ # 脚本文件存储目录 - ├── example_script.py # 示例脚本 - └── ... # 用户创建的脚本 -``` - -### 文档和测试 -``` -├── demo/ -│ ├── script_gui_test.py # 界面功能测试 -│ ├── 脚本管理界面使用指南.md # 详细使用说明 -│ └── 脚本管理界面实现总结.md # 本文档 -``` - -## 🔧 技术实现 - -### 架构设计 -- **模块化设计**:脚本系统分为引擎、加载器、API等独立模块 -- **事件驱动**:使用Qt信号槽机制处理界面事件 -- **组件化**:脚本以组件形式挂载,便于管理 -- **热重载**:文件监控系统实现开发时的实时更新 - -### 性能优化 -- **任务系统集成**:使用Panda3D原生任务系统,性能稳定 -- **定时更新**:界面状态每秒更新一次,避免过度刷新 -- **错误隔离**:脚本错误不会影响主程序运行 -- **内存管理**:正确的模块加载和卸载机制 - -### 兼容性 -- **现有系统集成**:与选择系统、属性面板等现有功能完美集成 -- **代理模式**:main.py中使用代理方法,保持接口简洁 -- **向后兼容**:不影响现有功能的正常使用 - -## 🎨 用户体验 - -### 界面设计 -- **标签式布局**:脚本管理面板作为独立标签页 -- **分组组织**:功能按逻辑分组,界面清晰 -- **状态指示**:颜色编码显示脚本状态 -- **实时反馈**:操作结果立即显示 - -### 操作流程 -1. **创建脚本**:菜单或面板中输入名称和选择模板 -2. **编辑脚本**:使用外部编辑器修改脚本文件 -3. **加载脚本**:自动检测或手动重载脚本 -4. **挂载脚本**:选择对象后从下拉菜单选择脚本 -5. **管理脚本**:在属性面板中启用/禁用脚本 - -## 📚 使用说明 - -### 快速开始 -1. 启动主程序 -2. 通过菜单栏 → 脚本 → 创建脚本 创建新脚本 -3. 使用外部编辑器编辑脚本文件 -4. 在场景中选择游戏对象 -5. 在脚本管理面板中挂载脚本 -6. 在属性面板中管理脚本状态 - -### 高级功能 -- **热重载**:启用后自动检测文件变化 -- **批量操作**:一次性重载所有脚本 -- **状态管理**:单独控制每个脚本的启用状态 -- **模板系统**:使用预定义模板快速创建脚本 - -## 🚀 特色亮点 - -### 1. 开发效率 -- **热重载**:修改脚本后立即生效,无需重启 -- **模板系统**:快速创建标准化脚本结构 -- **图形界面**:直观的脚本管理,无需命令行操作 - -### 2. 系统集成 -- **无缝集成**:与现有选择、属性系统完美配合 -- **统一界面**:所有功能在同一界面中管理 -- **实时反馈**:操作结果立即在界面中体现 - -### 3. 稳定性 -- **错误隔离**:脚本错误不影响主程序 -- **状态管理**:完整的启用/禁用控制 -- **内存安全**:正确的模块加载和卸载 - -## 📈 未来扩展 - -### 计划功能 -- **脚本调试**:集成调试工具和断点功能 -- **性能监控**:脚本执行时间和资源使用统计 -- **版本控制**:脚本文件的版本管理 -- **外部编辑器集成**:一键打开常用编辑器 - -### 优化方向 -- **UI改进**:更多的快捷操作和键盘快捷键 -- **性能优化**:大量脚本时的加载性能 -- **错误报告**:更详细的错误信息和解决建议 -- **文档生成**:自动生成脚本API文档 - -## 📊 总结 - -脚本管理界面的实现完全满足了用户需求: - -- ✅ **完整功能**:覆盖了脚本创建、管理、挂载、使用的全流程 -- ✅ **易用性**:图形界面操作简单直观 -- ✅ **开发效率**:热重载和模板系统提高开发效率 -- ✅ **系统集成**:与现有功能完美融合 -- ✅ **稳定性**:经过测试验证,功能稳定可靠 - +# 脚本管理界面实现总结 + +## 📋 项目概述 + +已成功为主程序实现了完整的脚本管理界面,提供了脚本创建、管理、挂载和使用功能。用户可以通过图形界面管理脚本,而脚本编辑可以通过外部编辑器进行。 + +## ✅ 已实现功能 + +### 1. 脚本系统核心功能 +- **✅ 脚本引擎**:集成到Panda3D任务系统,提供稳定的脚本执行环境 +- **✅ 脚本加载器**:支持动态加载Python脚本文件,具有错误处理机制 +- **✅ 热重载系统**:自动监控文件变化,实时重新加载修改的脚本 +- **✅ 脚本API**:为脚本提供游戏引擎功能接口 +- **✅ 组件化管理**:脚本以组件形式挂载到游戏对象 + +### 2. 用户界面组件 + +#### 📋 脚本菜单(菜单栏) +- **创建脚本...** - 快速创建新脚本对话框 +- **加载脚本文件...** - 从文件系统加载脚本 +- **重载所有脚本** - 批量重新加载所有脚本 +- **启用热重载** - 切换热重载功能开关 +- **脚本管理器** - 打开脚本管理面板 + +#### 🛠️ 脚本管理面板(右侧停靠窗口) +**脚本系统状态组:** +- 实时显示脚本系统运行状态 +- 显示热重载启用/禁用状态 + +**创建脚本组:** +- 脚本名称输入框 +- 模板选择下拉菜单(basic、movement、animation) +- 创建脚本按钮 + +**可用脚本组:** +- 脚本列表显示(支持双击操作) +- 加载脚本按钮 +- 重载全部按钮 + +**脚本挂载组:** +- 当前选中对象显示 +- 脚本选择下拉菜单 +- 挂载/卸载按钮 +- 已挂载脚本列表(显示启用状态) + +#### 📊 属性面板集成 +- 显示对象上的所有挂载脚本 +- 脚本名称和状态显示 +- 启用/禁用按钮控制 +- 实时状态更新 + +### 3. 脚本模板系统 +- **Basic模板**:基础脚本结构,包含start()和update()方法 +- **Movement模板**:移动相关脚本,包含位置变换功能 +- **Animation模板**:动画相关脚本模板 + +### 4. 交互功能 +- **实时状态更新**:定时器每秒更新界面状态 +- **对象选择集成**:与现有选择系统完美集成 +- **错误处理**:完整的错误提示和异常处理 +- **用户反馈**:操作成功/失败的消息提示 + +## 🎯 功能验证 + +### 测试结果 +通过 `script_gui_test.py` 测试验证了以下功能: + +✅ **脚本系统初始化**: +- 脚本系统启动正常 +- 热重载功能启用 +- 可用脚本数量统计正确 + +✅ **脚本创建功能**: +- 成功创建TestRotator、TestMover、TestScaler脚本 +- 脚本文件正确生成到scripts/目录 + +✅ **脚本加载功能**: +- 成功加载5个脚本文件 +- 动态模块导入正常工作 + +✅ **脚本挂载功能**: +- 成功将脚本挂载到游戏对象 +- 脚本组件正确创建和管理 + +✅ **界面交互功能**: +- 对象选择功能正常 +- 属性面板更新正常 +- 坐标轴拖拽功能正常 + +## 📁 文件结构 + +### 核心文件 +``` +├── core/ +│ ├── script_system.py # 脚本系统核心实现 +│ └── __init__.py # 导出脚本系统类 +├── ui/ +│ ├── main_window.py # 主窗口,包含脚本管理界面 +│ └── property_panel.py # 属性面板,包含脚本信息显示 +├── main.py # 主程序,集成脚本系统 +└── scripts/ # 脚本文件存储目录 + ├── example_script.py # 示例脚本 + └── ... # 用户创建的脚本 +``` + +### 文档和测试 +``` +├── demo/ +│ ├── script_gui_test.py # 界面功能测试 +│ ├── 脚本管理界面使用指南.md # 详细使用说明 +│ └── 脚本管理界面实现总结.md # 本文档 +``` + +## 🔧 技术实现 + +### 架构设计 +- **模块化设计**:脚本系统分为引擎、加载器、API等独立模块 +- **事件驱动**:使用Qt信号槽机制处理界面事件 +- **组件化**:脚本以组件形式挂载,便于管理 +- **热重载**:文件监控系统实现开发时的实时更新 + +### 性能优化 +- **任务系统集成**:使用Panda3D原生任务系统,性能稳定 +- **定时更新**:界面状态每秒更新一次,避免过度刷新 +- **错误隔离**:脚本错误不会影响主程序运行 +- **内存管理**:正确的模块加载和卸载机制 + +### 兼容性 +- **现有系统集成**:与选择系统、属性面板等现有功能完美集成 +- **代理模式**:main.py中使用代理方法,保持接口简洁 +- **向后兼容**:不影响现有功能的正常使用 + +## 🎨 用户体验 + +### 界面设计 +- **标签式布局**:脚本管理面板作为独立标签页 +- **分组组织**:功能按逻辑分组,界面清晰 +- **状态指示**:颜色编码显示脚本状态 +- **实时反馈**:操作结果立即显示 + +### 操作流程 +1. **创建脚本**:菜单或面板中输入名称和选择模板 +2. **编辑脚本**:使用外部编辑器修改脚本文件 +3. **加载脚本**:自动检测或手动重载脚本 +4. **挂载脚本**:选择对象后从下拉菜单选择脚本 +5. **管理脚本**:在属性面板中启用/禁用脚本 + +## 📚 使用说明 + +### 快速开始 +1. 启动主程序 +2. 通过菜单栏 → 脚本 → 创建脚本 创建新脚本 +3. 使用外部编辑器编辑脚本文件 +4. 在场景中选择游戏对象 +5. 在脚本管理面板中挂载脚本 +6. 在属性面板中管理脚本状态 + +### 高级功能 +- **热重载**:启用后自动检测文件变化 +- **批量操作**:一次性重载所有脚本 +- **状态管理**:单独控制每个脚本的启用状态 +- **模板系统**:使用预定义模板快速创建脚本 + +## 🚀 特色亮点 + +### 1. 开发效率 +- **热重载**:修改脚本后立即生效,无需重启 +- **模板系统**:快速创建标准化脚本结构 +- **图形界面**:直观的脚本管理,无需命令行操作 + +### 2. 系统集成 +- **无缝集成**:与现有选择、属性系统完美配合 +- **统一界面**:所有功能在同一界面中管理 +- **实时反馈**:操作结果立即在界面中体现 + +### 3. 稳定性 +- **错误隔离**:脚本错误不影响主程序 +- **状态管理**:完整的启用/禁用控制 +- **内存安全**:正确的模块加载和卸载 + +## 📈 未来扩展 + +### 计划功能 +- **脚本调试**:集成调试工具和断点功能 +- **性能监控**:脚本执行时间和资源使用统计 +- **版本控制**:脚本文件的版本管理 +- **外部编辑器集成**:一键打开常用编辑器 + +### 优化方向 +- **UI改进**:更多的快捷操作和键盘快捷键 +- **性能优化**:大量脚本时的加载性能 +- **错误报告**:更详细的错误信息和解决建议 +- **文档生成**:自动生成脚本API文档 + +## 📊 总结 + +脚本管理界面的实现完全满足了用户需求: + +- ✅ **完整功能**:覆盖了脚本创建、管理、挂载、使用的全流程 +- ✅ **易用性**:图形界面操作简单直观 +- ✅ **开发效率**:热重载和模板系统提高开发效率 +- ✅ **系统集成**:与现有功能完美融合 +- ✅ **稳定性**:经过测试验证,功能稳定可靠 + 用户现在可以通过主程序界面完成所有脚本管理工作,编辑工作可以使用任何外部编辑器进行,形成了高效的脚本开发工作流。 \ No newline at end of file diff --git a/demo/选择功能修复说明.md b/demo/选择功能修复说明.md index d2ee2ab4..a78f9094 100644 --- a/demo/选择功能修复说明.md +++ b/demo/选择功能修复说明.md @@ -1,169 +1,169 @@ -# 选择功能修复说明 - -## 问题描述 - -用户反馈点击模型后没有反应,应该要能够选中模型并显示选择框和坐标轴。 - -## 问题分析 - -通过分析代码发现了几个关键问题: - -### 1. 缺少碰撞检测设置 -**问题**:模型导入时没有设置碰撞检测,导致射线检测无法检测到模型。 -- `scene_manager.py` 中的 `importModel` 方法没有调用 `setupCollision` -- 所有模型加载方法都缺少碰撞设置 - -### 2. 默认工具未设置 -**问题**:`tool_manager.py` 中默认工具为 `None`,而选择功能需要当前工具为"选择"。 -- 事件处理器中的条件 `if self.world.currentTool == "选择"` 永远不成立 - -### 3. 碰撞掩码不匹配 -**问题**:射线检测和模型碰撞体使用不同的碰撞掩码。 -- 模型碰撞体使用 `BitMask32.bit(2)` -- 射线检测使用 `GeomNode.getDefaultCollideMask()` - -## 修复方案 - -### 1. 为所有模型添加碰撞检测 - -**修复位置**:`scene/scene_manager.py` - -在以下方法中添加碰撞检测设置: -- `importModel()` - 主要模型导入方法 -- `loadScene()` - 场景加载时为每个模型设置碰撞 -- `processLoadedModel()` - 异步加载回调 -- `loadAnimatedModel()` - 动画模型加载 - -```python -# 设置碰撞检测(重要!用于选择功能) -print("\n=== 设置碰撞检测 ===") -self.setupCollision(model) -``` - -### 2. 设置默认工具为选择 - -**修复位置**:`core/tool_manager.py` - -```python -def __init__(self, world): - """初始化工具管理器""" - self.world = world - self.currentTool = "选择" # 默认工具为选择工具 -``` - -### 3. 统一碰撞掩码设置 - -**修复位置**:`core/event_handler.py` - -```python -# 设置射线的碰撞掩码,匹配模型的碰撞掩码(第2位) -from panda3d.core import BitMask32 -pickerNode.setFromCollideMask(BitMask32.bit(2)) -``` - -## 碰撞检测系统工作原理 - -### 模型碰撞体设置 -每个模型都会创建一个包围球体作为碰撞体: -```python -def setupCollision(self, model): - # 创建碰撞节点 - cNode = CollisionNode(f'modelCollision_{model.getName()}') - cNode.setIntoCollideMask(BitMask32.bit(2)) # 使用第2位 - - # 获取模型边界并创建碰撞球体 - bounds = model.getBounds() - center = bounds.getCenter() - radius = bounds.getRadius() - cSphere = CollisionSphere(center, radius) - cNode.addSolid(cSphere) - - # 附加到模型 - cNodePath = model.attachNewNode(cNode) -``` - -### 射线检测机制 -鼠标点击时创建射线进行碰撞检测: -```python -# 创建射线检测 -picker = CollisionTraverser() -queue = CollisionHandlerQueue() - -pickerNode = CollisionNode('mouseRay') -pickerNP = self.world.cam.attachNewNode(pickerNode) -pickerNode.setFromCollideMask(BitMask32.bit(2)) # 匹配模型掩码 - -# 创建射线几何体 -direction = farPoint - nearPoint -direction.normalize() -pickerNode.addSolid(CollisionRay(nearPoint, direction)) - -# 执行碰撞检测 -picker.addCollider(pickerNP, queue) -picker.traverse(self.world.render) -``` - -## 选择系统工作流程 - -1. **鼠标点击** → `mousePressEventLeft()` -2. **坐标转换** → 屏幕坐标转世界坐标 -3. **创建射线** → 相机位置到点击位置 -4. **碰撞检测** → 射线与模型碰撞体检测 -5. **选择处理** → `_handleSelectionClick()` -6. **更新UI** → 选择框、坐标轴、属性面板 - -## 测试验证 - -创建了专用测试脚本 `demo/selection_test.py`: - -### 测试功能 -- 创建多个测试立方体模型 -- 验证碰撞检测设置 -- 测试点击选择功能 -- 显示选择框和坐标轴 - -### 测试控制 -- **鼠标左键**:点击选择模型 -- **I键**:显示当前选择信息 -- **C键**:显示碰撞检测信息 -- **S键**:切换碰撞体显示(调试) -- **R键**:切换射线显示 -- **Q键**:退出 - -### 运行测试 -```bash -cd demo -python selection_test.py -``` - -## 修复效果 - -**修复前**: -- 点击模型无反应 -- 无法选中任何物体 -- 选择框和坐标轴不显示 - -**修复后**: -- ✅ 点击模型正确选中 -- ✅ 显示橙色选择框 -- ✅ 显示彩色坐标轴(红X、绿Y、蓝Z) -- ✅ 树形控件同步选择 -- ✅ 属性面板更新显示 -- ✅ 坐标轴支持拖拽变换 - -## 相关功能 - -选择功能修复后,以下功能也恢复正常: -- **坐标轴拖拽**:点击坐标轴可拖拽移动物体 -- **属性编辑**:选中后可在属性面板编辑位置、旋转、缩放 -- **场景树同步**:点击模型会在场景树中高亮对应项 -- **射线调试**:按R键可显示点击射线用于调试 - -## 技术要点 - -1. **碰撞掩码一致性**:射线检测和模型碰撞体必须使用相同的掩码位 -2. **碰撞体覆盖**:包围球体确保模型的所有部分都可点击 -3. **工具状态管理**:默认工具状态影响事件处理逻辑 -4. **模块化设计**:碰撞设置在所有模型加载路径中都要调用 - +# 选择功能修复说明 + +## 问题描述 + +用户反馈点击模型后没有反应,应该要能够选中模型并显示选择框和坐标轴。 + +## 问题分析 + +通过分析代码发现了几个关键问题: + +### 1. 缺少碰撞检测设置 +**问题**:模型导入时没有设置碰撞检测,导致射线检测无法检测到模型。 +- `scene_manager.py` 中的 `importModel` 方法没有调用 `setupCollision` +- 所有模型加载方法都缺少碰撞设置 + +### 2. 默认工具未设置 +**问题**:`tool_manager.py` 中默认工具为 `None`,而选择功能需要当前工具为"选择"。 +- 事件处理器中的条件 `if self.world.currentTool == "选择"` 永远不成立 + +### 3. 碰撞掩码不匹配 +**问题**:射线检测和模型碰撞体使用不同的碰撞掩码。 +- 模型碰撞体使用 `BitMask32.bit(2)` +- 射线检测使用 `GeomNode.getDefaultCollideMask()` + +## 修复方案 + +### 1. 为所有模型添加碰撞检测 + +**修复位置**:`scene/scene_manager.py` + +在以下方法中添加碰撞检测设置: +- `importModel()` - 主要模型导入方法 +- `loadScene()` - 场景加载时为每个模型设置碰撞 +- `processLoadedModel()` - 异步加载回调 +- `loadAnimatedModel()` - 动画模型加载 + +```python +# 设置碰撞检测(重要!用于选择功能) +print("\n=== 设置碰撞检测 ===") +self.setupCollision(model) +``` + +### 2. 设置默认工具为选择 + +**修复位置**:`core/tool_manager.py` + +```python +def __init__(self, world): + """初始化工具管理器""" + self.world = world + self.currentTool = "选择" # 默认工具为选择工具 +``` + +### 3. 统一碰撞掩码设置 + +**修复位置**:`core/event_handler.py` + +```python +# 设置射线的碰撞掩码,匹配模型的碰撞掩码(第2位) +from panda3d.core import BitMask32 +pickerNode.setFromCollideMask(BitMask32.bit(2)) +``` + +## 碰撞检测系统工作原理 + +### 模型碰撞体设置 +每个模型都会创建一个包围球体作为碰撞体: +```python +def setupCollision(self, model): + # 创建碰撞节点 + cNode = CollisionNode(f'modelCollision_{model.getName()}') + cNode.setIntoCollideMask(BitMask32.bit(2)) # 使用第2位 + + # 获取模型边界并创建碰撞球体 + bounds = model.getBounds() + center = bounds.getCenter() + radius = bounds.getRadius() + cSphere = CollisionSphere(center, radius) + cNode.addSolid(cSphere) + + # 附加到模型 + cNodePath = model.attachNewNode(cNode) +``` + +### 射线检测机制 +鼠标点击时创建射线进行碰撞检测: +```python +# 创建射线检测 +picker = CollisionTraverser() +queue = CollisionHandlerQueue() + +pickerNode = CollisionNode('mouseRay') +pickerNP = self.world.cam.attachNewNode(pickerNode) +pickerNode.setFromCollideMask(BitMask32.bit(2)) # 匹配模型掩码 + +# 创建射线几何体 +direction = farPoint - nearPoint +direction.normalize() +pickerNode.addSolid(CollisionRay(nearPoint, direction)) + +# 执行碰撞检测 +picker.addCollider(pickerNP, queue) +picker.traverse(self.world.render) +``` + +## 选择系统工作流程 + +1. **鼠标点击** → `mousePressEventLeft()` +2. **坐标转换** → 屏幕坐标转世界坐标 +3. **创建射线** → 相机位置到点击位置 +4. **碰撞检测** → 射线与模型碰撞体检测 +5. **选择处理** → `_handleSelectionClick()` +6. **更新UI** → 选择框、坐标轴、属性面板 + +## 测试验证 + +创建了专用测试脚本 `demo/selection_test.py`: + +### 测试功能 +- 创建多个测试立方体模型 +- 验证碰撞检测设置 +- 测试点击选择功能 +- 显示选择框和坐标轴 + +### 测试控制 +- **鼠标左键**:点击选择模型 +- **I键**:显示当前选择信息 +- **C键**:显示碰撞检测信息 +- **S键**:切换碰撞体显示(调试) +- **R键**:切换射线显示 +- **Q键**:退出 + +### 运行测试 +```bash +cd demo +python selection_test.py +``` + +## 修复效果 + +**修复前**: +- 点击模型无反应 +- 无法选中任何物体 +- 选择框和坐标轴不显示 + +**修复后**: +- ✅ 点击模型正确选中 +- ✅ 显示橙色选择框 +- ✅ 显示彩色坐标轴(红X、绿Y、蓝Z) +- ✅ 树形控件同步选择 +- ✅ 属性面板更新显示 +- ✅ 坐标轴支持拖拽变换 + +## 相关功能 + +选择功能修复后,以下功能也恢复正常: +- **坐标轴拖拽**:点击坐标轴可拖拽移动物体 +- **属性编辑**:选中后可在属性面板编辑位置、旋转、缩放 +- **场景树同步**:点击模型会在场景树中高亮对应项 +- **射线调试**:按R键可显示点击射线用于调试 + +## 技术要点 + +1. **碰撞掩码一致性**:射线检测和模型碰撞体必须使用相同的掩码位 +2. **碰撞体覆盖**:包围球体确保模型的所有部分都可点击 +3. **工具状态管理**:默认工具状态影响事件处理逻辑 +4. **模块化设计**:碰撞设置在所有模型加载路径中都要调用 + 这次修复确保了3D编辑器的核心交互功能正常工作,为后续的编辑操作打下了坚实基础。 \ No newline at end of file diff --git a/gui/__init__.py b/gui/__init__.py index da194f54..df95b6c4 100644 --- a/gui/__init__.py +++ b/gui/__init__.py @@ -1,10 +1,10 @@ -""" -GUI包 - -提供GUI元素管理相关的功能: -- GUIManager: GUI元素管理器类 -""" - -from .gui_manager import GUIManager - +""" +GUI包 + +提供GUI元素管理相关的功能: +- GUIManager: GUI元素管理器类 +""" + +from .gui_manager import GUIManager + __all__ = ['GUIManager'] \ No newline at end of file diff --git a/gui/gui_manager.py b/gui/gui_manager.py index 55644875..111cd8e7 100644 --- a/gui/gui_manager.py +++ b/gui/gui_manager.py @@ -1,3994 +1,3994 @@ -""" -GUI元素管理模块 - -负责2D/3D GUI元素的管理: -- GUI元素的创建(按钮、标签、输入框等) -- GUI编辑模式 -- GUI属性编辑 -- GUI元素的复制和删除 -""" - -from direct.gui.DirectGui import * -from panda3d.core import * -from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QFormLayout, QLineEdit, - QDoubleSpinBox, QPushButton, QDialogButtonBox, - QColorDialog, QLabel, QWidget, QGroupBox, QHBoxLayout, QGridLayout, QSpinBox) -from PyQt5.QtGui import QColor -from PyQt5.QtCore import Qt -# 尝试导入 QtWebEngineWidgets,如果失败则设置为 None -try: - from PyQt5.QtWebEngineWidgets import QWebEngineView - WEB_ENGINE_AVAILABLE = True -except ImportError: - QWebEngineView = None - WEB_ENGINE_AVAILABLE = False - print("⚠️ QtWebEngineWidgets 不可用,Cesium 集成功能将被禁用") - - # def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0): - # from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib - # - # # 参数类型检查和转换 - # if isinstance(size, (list, tuple)): - # if len(size) >= 2: - # x_size, y_size = float(size[0]), float(size[1]) - # else: - # x_size = y_size = float(size[0]) if size else 1.0 - # else: - # x_size = y_size = float(size) - # - # # 创建卡片 - # cm = CardMaker('gui_3d_image') - # cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2) - # - # # 创建3D图像节点 - # image_node = self.world.render.attachNewNode(cm.generate()) - # image_node.setPos(*pos) - # - # # 为3D图像创建独立的材质 - # material = Material(f"image-material-{len(self.gui_elements)}") - # material.setBaseColor(LColor(1, 1, 1, 1)) - # material.setDiffuse(LColor(1, 1, 1, 1)) - # material.setAmbient(LColor(0.5, 0.5, 0.5, 1)) - # material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0)) - # material.setShininess(10.0) - # material.setEmission(LColor(0, 0, 0, 1)) # 无自发光 - # image_node.setMaterial(material, 1) - # - # image_node.setTransparency(TransparencyAttrib.MAlpha) - # - # # 如果提供了图像路径,则加载纹理 - # if image_path: - # self.update3DImageTexture(image_node, image_path) - # - # # 应用PBR效果(如果可用) - # try: - # if hasattr(self, 'render_pipeline') and self.render_pipeline: - # self.render_pipeline.set_effect( - # image_node, - # "effects/default.yaml", - # { - # "normal_mapping": True, - # "render_gbuffer": True, - # "alpha_testing": False, - # "parallax_mapping": False, - # "render_shadow": False, - # "render_envmap": True, - # "disable_children_effects": True - # }, - # 50 - # ) - # print("✓ GUI 3D图像PBR效果已应用") - # except Exception as e: - # print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}") - # - # # 为GUI元素添加标识(效仿3D文本方法) - # image_node.setTag("gui_type", "3d_image") - # image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}") - # image_node.setTag("is_scene_element", "1") - # image_node.setTag("tree_item_type", "GUI_3DIMAGE") - # if image_path: - # image_node.setTag("gui_image_path", image_path) - # image_node.setTag("is_gui_element", "1") - # - # self.gui_elements.append(image_node) - # - # # 更新场景树 - # if hasattr(self.world, 'updateSceneTree'): - # self.world.updateSceneTree() - # - # print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})") - # return image_node - -class GUIManager: - """GUI元素管理系统类""" - - def __init__(self, world): - """初始化GUI管理系统 - - Args: - world: 核心世界对象引用 - """ - self.world = world - - # GUI元素列表 - self.gui_elements = [] - - #光源列表 - self.light_elements = [] - - # GUI编辑模式状态 - self.guiEditMode = False - self.guiEditPanel = None - self.guiPreviewWindow = None - self.currentGUITool = None - - print("✓ GUI管理系统初始化完成") - - # ==================== GUI元素创建方法 ==================== - - def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=(0.1,0.1,0.1)): - """创建2D GUI按钮 - 支持多选创建和GUI父子关系,优化版本""" - try: - from direct.gui.DirectGui import DirectButton - from PyQt5.QtCore import Qt - - print(f"🔘 开始创建GUI按钮,位置: {pos}, 文本: {text}, 尺寸: {size}") - - # 获取树形控件 - tree_widget = self._get_tree_widget() - if not tree_widget: - print("❌ 无法访问树形控件") - return None - - # 使用CustomTreeWidget的方法获取目标父节点列表 - target_parents = tree_widget.get_target_parents_for_gui_creation() - if not target_parents: - print("❌ 没有找到有效的父节点") - return None - - created_buttons = [] - - # 为每个有效的父节点创建GUI按钮 - for parent_item, parent_node in target_parents: - try: - # 生成唯一名称 - button_name = f"GUIButton_{len(self.gui_elements)}" - - # 使用CustomTreeWidget的方法判断父节点类型并设置相应的挂载方式 - if tree_widget.is_gui_element(parent_node): - # 父节点是GUI元素 - 作为子GUI挂载 - gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node) - parent_gui_node = parent_node # 直接挂载到GUI元素 - parent_scale=parent_node.getScale() - relative_scale = ( - size/parent_scale[0] if parent_scale[0]!=0 else size, - size/parent_scale[1] if parent_scale[1]!=0 else size, - size/parent_scale[2] if parent_scale[2]!=0 else size - ) - else: - # 父节点是普通3D节点 - 使用屏幕坐标 - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - parent_gui_node = None # 使用默认的aspect2d - relative_scale = size - print(f"📎 挂载到3D父节点: {parent_item.text(0)}") - - button = DirectButton( - text=text, - pos=gui_pos, - scale=relative_scale, - command=self.onGUIButtonClick, - extraArgs=[f"button_{len(self.gui_elements)}"], - frameColor=(0.2, 0.6, 0.8, 1), - text_font=self.world.getChineseFont() if self.world.getChineseFont() else None, - rolloverSound=None, - clickSound=None, - parent=parent_gui_node - ) - - if not hasattr(button,'_tags'): - button._tags = {} - - # button._tags["gui_type"] = "button" - # button._tags["gui_id"] = f"button_{len(self.gui_elements)}" - # button._tags["gui_text"] = text - # button._tags["is_gui_element"] = "1" - # button._tags["is_scene_element"] = "1" - # button._tags["saved_gui_type"] = "button" - # button._tags["gui_element_type"] = "button" - # button._tags["created_by_user"] = "1" - # button._tags["name"] = button_name - # button.setName(button_name) - - # 设置节点标签 - button.setTag("gui_type", "button") - button.setTag("gui_id", f"button_{len(self.gui_elements)}") - button.setTag("gui_text", text) - button.setTag("is_gui_element", "1") - button.setTag("is_scene_element", "1") # 确保这个标签被设置 - button.setTag("tree_item_type", "GUI_BUTTON") - button.setTag("saved_gui_type", "button") # 添加这个标签以确保兼容性 - button.setTag("gui_element_type", "button") - button.setTag("created_by_user", "1") - button.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") - button.setTag("name", button_name) - button.setName(button_name) - - # 如果有GUI父节点,建立引用关系 - if parent_gui_node: - parent_id = parent_gui_node.getTag("gui_id") if hasattr(parent_gui_node, 'getTag') else "" - button.setTag("gui_parent_id", parent_id) - - # 添加到GUI元素列表 - self.gui_elements.append(button) - button.reparentTo(self.world.aspect2d) - - print(f"✅ 为 {parent_item.text(0)} 创建GUI按钮成功: {button_name}") - - # 使用CustomTreeWidget的方法在Qt树形控件中添加对应节点 - qt_item = tree_widget.add_node_to_tree_widget(button, parent_item, "GUI_BUTTON") - if qt_item: - created_buttons.append((button, qt_item)) - else: - created_buttons.append((button, None)) - print("⚠️ Qt树节点添加失败,但GUI对象已创建") - - except Exception as e: - print(f"❌ 为 {parent_item.text(0)} 创建GUI按钮失败: {str(e)}") - continue - - # 处理创建结果 - if not created_buttons: - print("❌ 没有成功创建任何GUI按钮") - return None - - # 选中最后创建的按钮并更新场景树 - # if created_buttons: - # last_button, last_qt_item = created_buttons[-1] - # if last_qt_item: - # tree_widget.setCurrentItem(last_qt_item) - # tree_widget.update_selection_and_properties(last_button, last_qt_item) - - print(f"🎉 总共创建了 {len(created_buttons)} 个GUI按钮") - - # 返回值处理 - if len(created_buttons) == 1: - return created_buttons[0][0] - else: - return [button for button, _ in created_buttons] - - except Exception as e: - print(f"❌ 创建GUI按钮过程失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08): - """创建2D GUI标签 - 支持多选创建和GUI父子关系,优化版本""" - try: - from direct.gui.DirectGui import DirectLabel - from PyQt5.QtCore import Qt - - print(f"🏷️ 开始创建GUI标签,位置: {pos}, 文本: {text}, 尺寸: {size}") - - # 获取树形控件 - tree_widget = self._get_tree_widget() - if not tree_widget: - print("❌ 无法访问树形控件") - return None - - # 使用CustomTreeWidget的方法获取目标父节点列表 - target_parents = tree_widget.get_target_parents_for_gui_creation() - if not target_parents: - print("❌ 没有找到有效的父节点") - return None - - created_labels = [] - - # 为每个有效的父节点创建GUI标签 - for parent_item, parent_node in target_parents: - try: - # 生成唯一名称 - label_name = f"GUILabel_{len(self.gui_elements)}" - - # 使用CustomTreeWidget的方法判断父节点类型并设置相应的挂载方式 - if tree_widget.is_gui_element(parent_node): - # 父节点是GUI元素 - 作为子GUI挂载 - gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node) - parent_gui_node = parent_node - - parent_scale = parent_node.getScale() - relative_scale = ( - size/parent_scale[0] if parent_scale[0]!= 0 else size, - size/parent_scale[1] if parent_scale[1]!= 0 else size, - size/parent_scale[2] if parent_scale[2]!= 0 else size - ) - else: - # 父节点是普通3D节点 - 使用屏幕坐标 - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - parent_gui_node = None - relative_scale = size - label = DirectLabel( - text=text, - pos=gui_pos, - scale=relative_scale, - frameColor=(0, 0, 0, 0), # 透明背景 - text_fg=(1, 1, 1, 1), - text_font=self.world.getChineseFont() if self.world.getChineseFont() else None, - parent=parent_gui_node # 设置GUI父节点 - ) - - # 设置节点标签 - label.setTag("gui_type", "label") - label.setTag("gui_id", f"label_{len(self.gui_elements)}") - label.setTag("gui_text", text) - label.setTag("is_gui_element", "1") - label.setTag("tree_item_type", "GUI_LABEL") - label.setTag("is_scene_element", "1") - label.setTag("created_by_user", "1") - label.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") - label.setTag("name",label_name) - label.setName(label_name) - - # 如果有GUI父节点,建立引用关系 - if parent_gui_node: - parent_id = parent_gui_node.getTag("gui_id") if hasattr(parent_gui_node, 'getTag') else "" - label.setTag("gui_parent_id", parent_id) - - # 添加到GUI元素列表 - self.gui_elements.append(label) - - print(f"✅ 为 {parent_item.text(0)} 创建GUI标签成功: {label_name}") - - # 使用CustomTreeWidget的方法在Qt树形控件中添加对应节点 - qt_item = tree_widget.add_node_to_tree_widget(label, parent_item, "GUI_LABEL") - if qt_item: - created_labels.append((label, qt_item)) - else: - created_labels.append((label, None)) - print("⚠️ Qt树节点添加失败,但GUI对象已创建") - - except Exception as e: - print(f"❌ 为 {parent_item.text(0)} 创建GUI标签失败: {str(e)}") - continue - - # 处理创建结果 - if not created_labels: - print("❌ 没有成功创建任何GUI标签") - return None - - # 选中最后创建的标签并更新场景树 - # if created_labels: - # last_label, last_qt_item = created_labels[-1] - # if last_qt_item: - # tree_widget.setCurrentItem(last_qt_item) - # tree_widget.update_selection_and_properties(last_label, last_qt_item) - - print(f"🎉 总共创建了 {len(created_labels)} 个GUI标签") - - # 返回值处理 - if len(created_labels) == 1: - return created_labels[0][0] - else: - return [label for label, _ in created_labels] - - except Exception as e: - print(f"❌ 创建GUI标签过程失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08): - """创建2D GUI文本输入框 - 支持多选创建和GUI父子关系,优化版本""" - try: - from direct.gui.DirectGui import DirectEntry - from PyQt5.QtCore import Qt - - print(f"📝 开始创建GUI输入框,位置: {pos}, 占位符: {placeholder}, 尺寸: {size}") - - # 获取树形控件 - tree_widget = self._get_tree_widget() - if not tree_widget: - print("❌ 无法访问树形控件") - return None - - # 使用CustomTreeWidget的方法获取目标父节点列表 - target_parents = tree_widget.get_target_parents_for_gui_creation() - if not target_parents: - print("❌ 没有找到有效的父节点") - return None - - created_entries = [] - - # 为每个有效的父节点创建GUI输入框 - for parent_item, parent_node in target_parents: - try: - # 生成唯一名称 - entry_name = f"GUIEntry_{len(self.gui_elements)}" - - # 使用CustomTreeWidget的方法判断父节点类型并设置相应的挂载方式 - if tree_widget.is_gui_element(parent_node): - # 父节点是GUI元素 - 作为子GUI挂载 - gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node) - parent_gui_node = parent_node - print(f"📎 挂载到GUI父节点: {parent_node.getName()}") - else: - # 父节点是普通3D节点 - 使用屏幕坐标 - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - parent_gui_node = None - print(f"📎 挂载到3D父节点: {parent_item.text(0)}") - - font = None - if hasattr(self.world,'getChineseFont'): - font = self.world.getChineseFont() - - entry = DirectEntry( - text="", - pos=(pos[0],pos[1],pos[2]), - scale=size, - command=self.onGUIEntrySubmit, - extraArgs=[f"entry_{len(self.gui_elements)}"], - initialText=placeholder, - numLines=1, - width=12, - focus=0, - frameColor = (0,0,0,0), - text_fg=(1,1,1,1), - text_align=TextNode.ACenter, - text_wordwrap=None, - rolloverSound=None, - clickSound=None, - parent=parent_gui_node, # 设置GUI父节点 - text_font = font, - frameSize=(-0.1,0.1,-0.05,0.05) - ) - - # 设置节点标签 - entry.setTag("gui_type", "entry") - entry.setTag("gui_id", f"entry_{len(self.gui_elements)}") - entry.setTag("gui_placeholder", placeholder) - entry.setTag("is_gui_element", "1") - entry.setTag("tree_item_type", "GUI_ENTRY") - entry.setTag("is_scene_element", "1") - entry.setTag("created_by_user", "1") - entry.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") - entry.setTag("name",entry_name) - entry.setName(entry_name) - - # 如果有GUI父节点,建立引用关系 - if parent_gui_node: - parent_id = parent_gui_node.getTag("gui_id") if hasattr(parent_gui_node, 'getTag') else "" - entry.setTag("gui_parent_id", parent_id) - - # 添加到GUI元素列表 - self.gui_elements.append(entry) - - print(f"✅ 为 {parent_item.text(0)} 创建GUI输入框成功: {entry_name}") - - # 使用CustomTreeWidget的方法在Qt树形控件中添加对应节点 - qt_item = tree_widget.add_node_to_tree_widget(entry, parent_item, "GUI_ENTRY") - if qt_item: - created_entries.append((entry, qt_item)) - else: - created_entries.append((entry, None)) - print("⚠️ Qt树节点添加失败,但GUI对象已创建") - - except Exception as e: - print(f"❌ 为 {parent_item.text(0)} 创建GUI输入框失败: {str(e)}") - continue - - # 处理创建结果 - if not created_entries: - print("❌ 没有成功创建任何GUI输入框") - return None - - # 选中最后创建的输入框并更新场景树 - # if created_entries: - # last_entry, last_qt_item = created_entries[-1] - # if last_qt_item: - # tree_widget.setCurrentItem(last_qt_item) - # tree_widget.update_selection_and_properties(last_entry, last_qt_item) - - print(f"🎉 总共创建了 {len(created_entries)} 个GUI输入框") - - # 返回值处理 - if len(created_entries) == 1: - return created_entries[0][0] - else: - return [entry for entry, _ in created_entries] - - except Exception as e: - print(f"❌ 创建GUI输入框过程失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=(1,1,1)): - """创建2D GUI图片""" - try: - from direct.gui.DirectGui import DirectButton - from PyQt5.QtCore import Qt - - print(f"🔘 开始创建GUI按钮,位置: {pos}, 图片路径: {image_path}, 尺寸: {size}") - - # 获取树形控件 - tree_widget = self._get_tree_widget() - if not tree_widget: - print("❌ 无法访问树形控件") - return None - - # 使用CustomTreeWidget的方法获取目标父节点列表 - target_parents = tree_widget.get_target_parents_for_gui_creation() - if not target_parents: - print("❌ 没有找到有效的父节点") - return None - - created_2dimage = [] - - # 为每个有效的父节点创建GUI按钮 - for parent_item, parent_node in target_parents: - try: - # 生成唯一名称 - image_name = f"GUIImage_{len(self.gui_elements)}" - - # 使用CustomTreeWidget的方法判断父节点类型并设置相应的挂载方式 - if tree_widget.is_gui_element(parent_node): - # 父节点是GUI元素 - 作为子GUI挂载 - gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node) - parent_gui_node = parent_node # 直接挂载到GUI元素 - print(f"📎 挂载到GUI父节点: {parent_node.getName()}") - else: - # 父节点是普通3D节点 - 使用屏幕坐标 - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - parent_gui_node = None # 使用默认的aspect2d - print(f"📎 挂载到3D父节点: {parent_item.text(0)}") - - if isinstance(size, (list, tuple)) and len(size) >= 2: - # 分别处理宽度和高度的缩放 - width_scale = size[0] * 0.25 - height_scale = size[2] * 0.25 - else: - # 如果只提供了一个缩放值,则使用相同值 - width_scale = size * 0.1 if isinstance(size, (int, float)) else 0.2 - height_scale = width_scale - - cm = CardMaker("gui-2d-image") - cm.setFrame(-width_scale, width_scale, -height_scale, height_scale) - - # image_node = self.world.aspect2d.attachNewNode(cm.generate()) - if parent_gui_node: - image_node = parent_gui_node.attachNewNode(cm.generate()) - else: - image_node = self.world.aspect2d.attachNewNode(cm.generate()) - image_node.setPos(gui_pos) - image_node.setBin('fixed', 0) - image_node.setDepthWrite(False) - image_node.setDepthTest(False) - image_node.setColor(1, 1, 1, 1) - - # 设置透明度支持 - image_node.setTransparency(TransparencyAttrib.MAlpha) - - # 如果提供了图像路径,则加载纹理 - if image_path: - try: - image_node.setTag("image_path", image_path) - texture = self.world.loader.loadTexture(image_path) - if texture: - image_node.setTexture(texture, 1) - texture.setWrapU(Texture.WM_clamp) - texture.setWrapV(Texture.WM_clamp) - texture.setMinfilter(Texture.FT_linear) - texture.setMagfilter(Texture.FT_linear) - image_node.setColor(1, 1, 1, 1) - else: - print(f"⚠️ 无法加载2D图片纹理: {image_path}") - except Exception as e: - print(f"❌ 加载2D图片纹理失败: {e}") - - # 设置节点标签 - image_node.setTag("gui_type", "2d_image") - image_node.setTag("gui_id", f"2d_image_{len(self.gui_elements)}") - image_node.setTag("gui_text", f"2D图片_{len(self.gui_elements)}") - image_node.setTag("is_gui_element", "1") - image_node.setTag("is_scene_element", "1") - image_node.setTag("tree_item_type", "GUI_IMAGE") - image_node.setTag("created_by_user", "1") - image_node.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") - image_node.setTag("name",image_name) - image_node.setName(image_name) - - # 如果有GUI父节点,建立引用关系 - if parent_gui_node: - parent_id = parent_gui_node.getTag("gui_id") if hasattr(parent_gui_node, 'getTag') else "" - image_node.setTag("gui_parent_id", parent_id) - - # 添加到GUI元素列表 - self.gui_elements.append(image_node) - - print(f"✅ 为 {parent_item.text(0)} 创建GUI按钮成功: {image_name}") - - # 使用CustomTreeWidget的方法在Qt树形控件中添加对应节点 - qt_item = tree_widget.add_node_to_tree_widget(image_node, parent_item, "GUI_IMAGE") - if qt_item: - created_2dimage.append((image_node, qt_item)) - else: - created_2dimage.append((image_node, None)) - print("⚠️ Qt树节点添加失败,但GUI对象已创建") - - except Exception as e: - print(f"❌ 为 {parent_item.text(0)} 创建GUI按钮失败: {str(e)}") - continue - - # 处理创建结果 - if not created_2dimage: - print("❌ 没有成功创建任何GUI按钮") - return None - - # 选中最后创建的按钮并更新场景树 - # if created_2dimage: - # last_button, last_qt_item = created_2dimage[-1] - # if last_qt_item: - # tree_widget.setCurrentItem(last_qt_item) - # tree_widget.update_selection_and_properties(last_button, last_qt_item) - - print(f"🎉 总共创建了 {len(created_2dimage)} 个GUI按钮") - - # 返回值处理 - if len(created_2dimage) == 1: - return created_2dimage[0][0] - else: - return [button for button, _ in created_2dimage] - - except Exception as e: - print(f"❌ 创建GUI按钮过程失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def constrain2DPosition(self,gui_element,new_x=None,new_z=None): - """限制2dGUI元素位置在屏幕范围内""" - try: - from panda3d.core import Vec3 - - bounds = gui_element.getTightBounds() - element_width=0 - element_height=0 - - if bounds: - min_point,max_point = bounds - element_width = (max_point.getX() - min_point.getX())/2 - element_height = (max_point.getZ()-min_point.getZ())/2 - - #获取当前缩放 - scale = gui_element.getScale() - if hasattr(scale,'getX'): - scale_x = scale.getX() - scale_z = scale.getZ() if hasattr(scale,'getZ') else scale_x - else: - scale_x = scale_z = scale if isinstance(scale,(int,float)) else 1.0 - - actual_width = element_width * scale_x - actual_height = element_height * scale_z - - screen_width = 1.9 - screen_height = 0.9 - - min_x = -screen_width + actual_width - max_x = screen_width - actual_width - min_z = -screen_height + actual_height - max_z = screen_height - actual_height - - #获取当前位置 - current_pos = gui_element.getPos() - x = new_x if new_x is not None else current_pos.getX() - z = new_z if new_z is not None else current_pos.getZ() - - #应用边界限制 - x = max(min_x,min(max_x,x)) - z = max(min_z,min(max_z,z)) - - return Vec3(x,current_pos.getY(),z) - except Exception as e: - print(f"约束2D位置时出错: {e}") - # 出错时返回原始值 - current_pos = gui_element.getPos() - x = new_x if new_x is not None else current_pos.getX() - z = new_z if new_z is not None else current_pos.getZ() - return Vec3(x, current_pos.getY(), z) - - def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): - """创建3D空间文本 - 支持多选创建,优化版本""" - try: - from panda3d.core import TextNode - from PyQt5.QtCore import Qt - - print(f"📄 开始创建3D文本,位置: {pos}, 文本: {text}, 尺寸: {size}") - - # 获取树形控件 - tree_widget = self._get_tree_widget() - if not tree_widget: - print("❌ 无法访问树形控件") - return None - - # 获取目标父节点列表 - target_parents = tree_widget.get_target_parents_for_creation() - if not target_parents: - print("❌ 没有找到有效的父节点") - return None - - created_texts = [] - - # 为每个有效的父节点创建3D文本 - for parent_item, parent_node in target_parents: - try: - # 生成唯一名称 - text_name = f"GUI3DText_{len(self.gui_elements)}" - - textNode = TextNode(f'3d-text-{len(self.gui_elements)}') - textNode.setText(text) - textNode.setAlign(TextNode.ACenter) - if self.world.getChineseFont(): - textNode.setFont(self.world.getChineseFont()) - - # 挂载到选中的父节点 - textNodePath = parent_node.attachNewNode(textNode) - textNodePath.setPos(*pos) - textNodePath.setScale(size) - textNodePath.setColor(1, 1, 0, 1) - #textNodePath.setBillboardAxis() # 让文本总是面向相机 - textNodePath.setName(text_name) - - # 设置节点标签 - textNodePath.setTag("gui_type", "3d_text") - textNodePath.setTag("gui_id", f"3d_text_{len(self.gui_elements)}") - textNodePath.setTag("gui_text", text) - textNodePath.setTag("is_gui_element", "1") - textNodePath.setTag("is_scene_element", "1") - textNodePath.setTag("tree_item_type", "GUI_3DTEXT") - textNodePath.setTag("created_by_user", "1") - textNodePath.setTag("name", text_name) - - # 添加到GUI元素列表 - self.gui_elements.append(textNodePath) - - print(f"✅ 为 {parent_item.text(0)} 创建3D文本成功: {text_name}") - - # 在Qt树形控件中添加对应节点 - qt_item = tree_widget.add_node_to_tree_widget(textNodePath, parent_item, "GUI_3DTEXT") - if qt_item: - created_texts.append((textNodePath, qt_item)) - else: - created_texts.append((textNodePath, None)) - print("⚠️ Qt树节点添加失败,但GUI对象已创建") - - except Exception as e: - print(f"❌ 为 {parent_item.text(0)} 创建3D文本失败: {str(e)}") - continue - - # 处理创建结果 - if not created_texts: - print("❌ 没有成功创建任何3D文本") - return None - - # 选中最后创建的文本并更新场景树 - # if created_texts: - # last_text, last_qt_item = created_texts[-1] - # if last_qt_item: - # tree_widget.setCurrentItem(last_qt_item) - # tree_widget.update_selection_and_properties(last_text, last_qt_item) - - print(f"🎉 总共创建了 {len(created_texts)} 个3D文本") - - # 返回值处理 - if len(created_texts) == 1: - return created_texts[0][0] - else: - return [text_np for text_np, _ in created_texts] - - except Exception as e: - print(f"❌ 创建3D文本过程失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=(1.0,1.0,1.0)): - """创建3D空间图片""" - try: - - from panda3d.core import TextNode - from PyQt5.QtCore import Qt - - print(f"📄 开始创建3D文本,位置: {pos}, 3D图片位置: {image_path}, 尺寸: {size}") - - # 获取树形控件 - tree_widget = self._get_tree_widget() - if not tree_widget: - print("❌ 无法访问树形控件") - return None - - # 获取目标父节点列表 - target_parents = tree_widget.get_target_parents_for_creation() - if not target_parents: - print("❌ 没有找到有效的父节点") - return None - - created_3dimage = [] - - # 为每个有效的父节点创建3D文本 - for parent_item, parent_node in target_parents: - try: - # 生成唯一名称 - image_name = f"GUI3DImage_{len(self.gui_elements)}" - - # 参数类型检查和转换 - if isinstance(size, (list, tuple)): - if len(size) >= 2: - x_size, y_size = float(size[0]), float(size[2]) - else: - x_size = y_size = float(size[0]) if size else 1.0 - else: - x_size = y_size = float(size) - - # 创建卡片 - cm = CardMaker('gui_3d_image') - cm.setFrame(-x_size, x_size, -y_size, y_size) - - # 创建3D图像节点 - # image_node = self.world.render.attachNewNode(cm.generate()) - image_node = parent_node.attachNewNode(cm.generate()) - image_node.setPos(*pos) - - # 为3D图像创建独立的材质 - material = Material(f"image-material-{len(self.gui_elements)}") - material.setBaseColor(LColor(1, 1, 1, 1)) - material.setDiffuse(LColor(1, 1, 1, 1)) - material.setAmbient(LColor(0.5, 0.5, 0.5, 1)) - material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0)) - material.setShininess(10.0) - material.setEmission(LColor(0, 0, 0, 1)) # 无自发光 - image_node.setMaterial(material, 1) - - image_node.setTransparency(TransparencyAttrib.MAlpha) - - # 如果提供了图像路径,则加载纹理 - if image_path: - self.update3DImageTexture(image_node, image_path) - - # 应用PBR效果(如果可用) - try: - if hasattr(self, 'render_pipeline') and self.render_pipeline: - self.render_pipeline.set_effect( - image_node, - "effects/default.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": False, - "parallax_mapping": False, - "render_shadow": False, - "render_envmap": True, - "disable_children_effects": True - }, - 50 - ) - print("✓ GUI 3D图像PBR效果已应用") - except Exception as e: - print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}") - image_node.setName(image_name) - - # 设置节点标签 - image_node.setTag("gui_type", "3d_image") - image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}") - if image_path: - image_node.setTag("image_path", image_path) - image_node.setTag("is_gui_element", "1") - image_node.setTag("is_scene_element", "1") - image_node.setTag("tree_item_type", "GUI_3DIMAGE") - image_node.setTag("created_by_user", "1") - image_node.setTag("name",image_name) - - # 添加到GUI元素列表 - self.gui_elements.append(image_node) - - print(f"✅ 为 {parent_item.text(0)} 创建3D文本成功: {image_name}") - - # 在Qt树形控件中添加对应节点 - qt_item = tree_widget.add_node_to_tree_widget(image_node, parent_item, "GUI_3DIMAGE") - if qt_item: - created_3dimage.append((image_node, qt_item)) - else: - created_3dimage.append((image_node, None)) - print("⚠️ Qt树节点添加失败,但GUI对象已创建") - - except Exception as e: - print(f"❌ 为 {parent_item.text(0)} 创建3D文本失败: {str(e)}") - continue - - # 处理创建结果 - if not created_3dimage: - print("❌ 没有成功创建任何3D文本") - return None - - # 选中最后创建的文本并更新场景树 - # if created_3dimage: - # last_image, last_qt_item = created_3dimage[-1] - # if last_qt_item: - # tree_widget.setCurrentItem(last_qt_item) - # tree_widget.update_selection_and_properties(last_image, last_qt_item) - - print(f"🎉 总共创建了 {len(created_3dimage)} 个3D文本") - - # 返回值处理 - if len(created_3dimage) == 1: - return created_3dimage[0][0] - else: - return [text_np for text_np, _ in created_3dimage] - - except Exception as e: - print(f"❌ 创建3D文本过程失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def createVideoScreen(self, pos=(0, 0, 0), size=1, video_path=None): - """创建3D视频播放屏幕 - 添加占位符纹理支持""" - try: - from panda3d.core import CardMaker, TransparencyAttrib, Texture, TextureStage - import os - - # 确保 pos 是有效的三维坐标元组 - if not isinstance(pos, (tuple, list)) or len(pos) != 3: - print(f"⚠️ 位置参数无效,使用默认值 (0, 0, 0),原始值: {pos}") - pos = (0, 0, 0) - else: - # 确保所有坐标都是数值类型 - try: - pos = (float(pos[0]), float(pos[1]), float(pos[2])) - except (ValueError, TypeError): - print(f"⚠️ 位置参数包含非数值,使用默认值 (0, 0, 0),原始值: {pos}") - pos = (0, 0, 0) - - # 确保 size 是有效数值 - try: - size = float(size) - except (ValueError, TypeError): - print(f"⚠️ 尺寸参数无效,使用默认值 0.2,原始值: {size}") - size = 0.2*5 - - print(f"📺 开始创建视频屏幕,位置: {pos}, 尺寸: {size}, 视频路径: {video_path}") - - # 获取树形控件 - tree_widget = self._get_tree_widget() - if not tree_widget: - print("❌ 无法访问树形控件") - return None - - # 获取目标父节点列表 - target_parents = tree_widget.get_target_parents_for_creation() - if not target_parents: - print("❌ 没有找到有效的父节点") - return None - - created_videoscreens = [] - - # 为每个有效的父节点创建视频屏幕 - for parent_item, parent_node in target_parents: - try: - # 生成唯一名称 - screen_name = f"VideoScreen_{len(self.gui_elements)}" - - # 使用CardMaker创建视频屏幕框架 - cm = CardMaker('video-screen') - cm.setFrame(-size, size, -size, size) - - # 创建挂载节点 - 挂载到选中的父节点 - video_screen = parent_node.attachNewNode(cm.generate()) - video_screen.setPos(*pos) - video_screen.setName(screen_name) - video_screen.setBin('fixed', 10) - - # 设置透明度支持 - video_screen.setTransparency(TransparencyAttrib.MAlpha) - - # 设置初始颜色为白色,确保纹理能正确显示 - video_screen.setColor(1, 1, 1, 1) - - # 确保视频屏幕有正确的材质 - self._ensureVideoScreenMaterial(video_screen) - - # 设置节点标签 - video_screen.setTag("gui_type", "video_screen") - video_screen.setTag("gui_id", f"video_screen_{len(self.gui_elements)}") - video_screen.setTag("gui_text", f"视频屏幕_{len(self.gui_elements)}") - video_screen.setTag("is_gui_element", "1") - video_screen.setTag("is_scene_element", "1") - video_screen.setTag("tree_item_type", "GUI_VIDEO_SCREEN") - video_screen.setTag("created_by_user", "1") - video_screen.setTag("name",screen_name) - - # 设置视频路径标签 - if video_path and os.path.exists(video_path): - video_screen.setTag("video_path", video_path) - else: - video_screen.setTag("video_path", "") - - # 关键修改:预先创建一个占位符纹理,为后续视频播放做准备 - placeholder_texture = Texture(f"placeholder_video_texture_3d_{len(self.gui_elements)}") - placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) - placeholder_data = b'\x19\x19\x4c' # 深蓝色占位符颜色 (25, 25, 76) - placeholder_texture.setRamImage(placeholder_data) - - # 创建纹理阶段并应用占位符纹理到视频屏幕 - texture_stage = TextureStage("video_placeholder") - texture_stage.setSort(0) - texture_stage.setMode(TextureStage.MModulate) - video_screen.setTexture(texture_stage, placeholder_texture) - - # 保存占位符纹理引用 - video_screen.setPythonTag("placeholder_texture", placeholder_texture) - - print(f"🔧 为3D视频屏幕创建了占位符纹理环境: {screen_name}") - - # 如果提供了视频路径,则加载视频纹理 - movie_texture = None - - if video_path: - if video_path.startswith(("http://","https://")): - try: - if hasattr(self.world,'property_panel'): - success = self.world.property_panel._loadVideoFromURLWithOpenCV_3D(video_screen,video_path) - if success: - print(f"✅ 视频流URL加载成功: {video_path}") - else: - print(f"⚠️ 视频流URL加载失败: {video_path}") - else: - print("⚠️ property_manager不可用,无法加载视频流") - except Exception as e: - print(f"❌ 加载视频流URL失败: {e}") - import traceback - traceback.print_exc() - elif os.path.exists(video_path): - # 对于本地视频文件,使用原有方法 - try: - print(f"🔍 尝试加载2D视频纹理: {video_path}") - # 加载视频纹理 - movie_texture = self._loadMovieTexture(video_path) - if movie_texture: - # 应用纹理到视频屏幕(替换占位符) - video_screen["frameTexture"] = movie_texture - print(f"✅ 2D视频纹理加载成功: {video_path}") - - # 保存视频纹理引用以便后续控制 - video_screen.setPythonTag("movie_texture", movie_texture) - - # 尝试自动播放视频(如果支持) - try: - if hasattr(movie_texture, 'play'): - movie_texture.play() - print("▶️ 2D视频已开始播放") - except Exception as play_error: - print(f"⚠️ 2D视频自动播放失败: {play_error}") - else: - print(f"⚠️ 无法加载2D视频纹理: {video_path}") - except Exception as e: - print(f"❌ 加载2D视频纹理失败: {e}") - import traceback - traceback.print_exc() - else: - print(f"⚠️ 2D视频文件不存在: {video_path}") - - # if video_path and os.path.exists(video_path): - # try: - # print(f"🔍 尝试加载视频纹理: {video_path}") - # # 加载视频纹理 - # movie_texture = self._loadMovieTexture(video_path) - # if movie_texture: - # # 创建纹理阶段 - # texture_stage = TextureStage("video") - # texture_stage.setSort(0) - # texture_stage.setMode(TextureStage.MModulate) - # video_screen.setTexture(texture_stage, movie_texture) - # - # print(f"✅ 视频纹理加载成功: {video_path}") - # - # # 保存视频纹理引用以便后续控制 - # video_screen.setPythonTag("movie_texture", movie_texture) - # - # # 尝试自动播放视频(如果支持) - # try: - # if hasattr(movie_texture, 'play'): - # movie_texture.play() - # print("▶️ 视频已开始播放") - # except Exception as play_error: - # print(f"⚠️ 视频自动播放失败: {play_error}") - # else: - # print(f"⚠️ 无法加载视频纹理: {video_path}") - # # 使用默认颜色作为占位符 - # video_screen.setColor(0.1, 0.1, 0.3, 0.8) - # except Exception as e: - # print(f"❌ 加载视频纹理失败: {e}") - # import traceback - # traceback.print_exc() - # # 使用默认颜色作为占位符 - # video_screen.setColor(0.1, 0.1, 0.3, 0.8) - # else: - # # 没有视频文件时显示默认颜色 - # video_screen.setColor(0.1, 0.1, 0.3, 0.8) - # if video_path: - # print(f"⚠️ 视频文件不存在: {video_path}") - # else: - # print("ℹ️ 未提供视频文件,显示默认占位符") - # - # # 保存视频纹理引用以便后续控制 - # if movie_texture: - # video_screen.setPythonTag("movie_texture", movie_texture) - - # 添加到GUI元素列表 - self.gui_elements.append(video_screen) - - print(f"✅ 为 {parent_item.text(0)} 创建视频屏幕成功: {screen_name}") - - # 在Qt树形控件中添加对应节点 - qt_item = tree_widget.add_node_to_tree_widget(video_screen, parent_item, "GUI_VIDEO_SCREEN") - if qt_item: - created_videoscreens.append((video_screen, qt_item)) - else: - created_videoscreens.append((video_screen, None)) - print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") - - except Exception as e: - print(f"❌ 为 {parent_item.text(0)} 创建视频屏幕失败: {str(e)}") - import traceback - traceback.print_exc() - continue - - # 处理创建结果 - if not created_videoscreens: - print("❌ 没有成功创建任何视频屏幕") - return None - - # 选中最后创建的视频屏幕 - # if created_videoscreens: - # last_screen_np, last_qt_item = created_videoscreens[-1] - # if last_qt_item: - # tree_widget.setCurrentItem(last_qt_item) - # # 更新选择和属性面板 - # tree_widget.update_selection_and_properties(last_screen_np, last_qt_item) - - print(f"🎉 总共创建了 {len(created_videoscreens)} 个视频屏幕") - - # 返回值处理 - if len(created_videoscreens) == 1: - return created_videoscreens[0][0] # 单个屏幕返回NodePath - else: - return [screen_np for screen_np, _ in created_videoscreens] # 多个屏幕返回列表 - - except Exception as e: - print(f"❌ 创建视频屏幕过程失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def _ensureVideoScreenMaterial(self, video_screen): - """确保视频屏幕有正确的材质设置""" - try: - from panda3d.core import Material, LColor - - # 如果还没有材质,则创建一个 - if not video_screen.hasMaterial(): - material = Material(f"video-material-{video_screen.getName()}") - material.setBaseColor(LColor(1, 1, 1, 1)) - material.setDiffuse(LColor(1, 1, 1, 1)) - material.setAmbient(LColor(1, 1, 1, 1)) # 确保环境光为白色 - material.setEmission(LColor(0, 0, 0, 1)) - material.setSpecular(LColor(0, 0, 0, 1)) - material.setShininess(0) - video_screen.setMaterial(material, 1) - print(f"✅ 为视频屏幕创建了新材质: {video_screen.getName()}") - else: - # 更新现有材质确保正确设置 - material = video_screen.getMaterial() - material.setBaseColor(LColor(1, 1, 1, 1)) - material.setAmbient(LColor(1, 1, 1, 1)) # 确保环境光为白色 - video_screen.setMaterial(material, 1) - print(f"✅ 更新了视频屏幕材质: {video_screen.getName()}") - - except Exception as e: - print(f"⚠️ 设置视频屏幕材质时出错: {e}") - - def _debugVideoScreenTextures(self, video_screen): - """调试视频屏幕的纹理状态""" - try: - print(f"调试视频屏幕 {video_screen.getName()}:") - - # 检查PythonTag - movie_texture = video_screen.getPythonTag("movie_texture") - if movie_texture: - print(f" - PythonTag movie_texture: {type(movie_texture)}") - if hasattr(movie_texture, 'is_playable'): - print(f" - is_playable: {movie_texture.is_playable()}") - else: - print(" - PythonTag movie_texture: None") - - # 检查所有纹理阶段 - texture_stages = video_screen.findAllTextureStages() - print(f" - 纹理阶段数: {texture_stages.getNumStages()}") - for i in range(texture_stages.getNumStages()): - stage = texture_stages.getStage(i) - texture = video_screen.getTexture(stage) - print(f" - 阶段 {i}: {stage.getName()}, 纹理: {texture.getName() if texture else 'None'}") - - except Exception as e: - print(f"调试视频屏幕纹理时出错: {e}") - - def playVideo(self, video_screen): - """播放视频 - 改进版本,支持从暂停处继续播放""" - try: - # 获取视频纹理 - movie_texture = self._getMovieTextureFromScreen(video_screen) - - if movie_texture: - # 检查是否有播放方法 - if hasattr(movie_texture, 'play'): - try: - self.loadVideoFile(video_screen, video_screen.getTag("video_path")) - movie_texture.play() - print(f"▶️ 继续播放视频: {video_screen.getName()}") - return True - except Exception as play_error: - print(f"⚠️ 播放视频时出错: {play_error}") - return False - else: - print(f"⚠️ 纹理对象没有播放方法: {video_screen.getName()}") - return False - else: - self.loadVideoFile(video_screen,video_screen.getTag("video_path")) - except Exception as e: - print(f"❌ 播放视频失败: {e}") - import traceback - traceback.print_exc() - return False - - def _getMovieTextureFromScreen(self, video_screen): - """从视频屏幕获取视频纹理""" - try: - # 方法1: 从PythonTag获取 - movie_texture = video_screen.getPythonTag("movie_texture") - if movie_texture: - return movie_texture - - # 方法2: 从纹理阶段获取 - from panda3d.core import TextureStage - texture_stage = video_screen.findTextureStage("video") - if texture_stage: - movie_texture = video_screen.getTexture(texture_stage) - if movie_texture: - return movie_texture - - # 方法3: 获取第一个纹理 - if video_screen.hasTexture(): - movie_texture = video_screen.getTexture() - if movie_texture: - return movie_texture - - return None - except Exception as e: - print(f"获取视频纹理时出错: {e}") - return None - - def pauseVideo(self, video_screen): - """暂停视频""" - try: - movie_texture = self._getMovieTextureFromScreen(video_screen) - - if movie_texture: - # 检查是否有暂停方法 - if hasattr(movie_texture, 'stop'): # MovieTexture使用stop来暂停 - try: - movie_texture.stop() - print(f"⏸️ 视频已暂停: {video_screen.getName()}") - return True - except Exception as stop_error: - print(f"⚠️ 暂停视频时出错: {stop_error}") - return False - elif hasattr(movie_texture, 'set play rate'): # 某些版本支持设置播放速率 - try: - movie_texture.setPlayRate(0.0) - print(f"⏸️ 视频已暂停(播放速率设为0): {video_screen.getName()}") - return True - except Exception as rate_error: - print(f"⚠️ 设置播放速率时出错: {rate_error}") - return False - else: - print(f"⚠️ 纹理对象没有暂停方法: {video_screen.getName()}") - return False - else: - print(f"❌ 视频屏幕没有关联的视频纹理: {video_screen.getName()}") - return False - except Exception as e: - print(f"❌ 暂停视频失败: {e}") - import traceback - traceback.print_exc() - return False - - def stopVideo(self, video_screen): - """停止视频(回到开头)""" - try: - movie_texture = self._getMovieTextureFromScreen(video_screen) - - if movie_texture: - # 停止并重置到开头 - if hasattr(movie_texture, 'stop'): - try: - movie_texture.stop() - # 如果有重置方法,调用它 - if hasattr(movie_texture, 'setTime'): - movie_texture.setTime(0.0) - print(f"⏹️ 视频已停止: {video_screen.getName()}") - return True - except Exception as stop_error: - print(f"⚠️ 停止视频时出错: {stop_error}") - return False - else: - print(f"⚠️ 纹理对象没有停止方法: {video_screen.getName()}") - return False - else: - print(f"❌ 视频屏幕没有关联的视频纹理: {video_screen.getName()}") - return False - except Exception as e: - print(f"❌ 停止视频失败: {e}") - import traceback - traceback.print_exc() - return False - def setVideoTime(self, video_screen, time_seconds): - """设置视频播放时间""" - try: - movie_texture = video_screen.getPythonTag("movie_texture") - - # 备用获取方法 - if not movie_texture: - from panda3d.core import TextureStage - texture_stage = video_screen.findTextureStage("video") - if texture_stage: - movie_texture = video_screen.getTexture(texture_stage) - - if not movie_texture and video_screen.hasTexture(): - movie_texture = video_screen.getTexture() - - if movie_texture and hasattr(movie_texture, 'set_time'): - movie_texture.set_time(time_seconds) - print(f"🕒 设置视频时间 {time_seconds}s: {video_screen.getName()}") - return True - else: - print(f"❌ 视频屏幕没有关联的视频纹理: {video_screen.getName()}") - return False - except Exception as e: - print(f"❌ 设置视频时间失败: {e}") - return False - - def loadVideoFile(self, video_screen, video_path): - """为视频屏幕加载新的视频文件""" - try: - import os - - if not os.path.exists(video_path): - print(f"❌ 视频文件不存在: {video_path}") - return False - - # Convert Windows path to Panda3D compatible path format - from panda3d.core import Filename - panda_path = Filename.fromOsSpecific(video_path) - converted_path = str(panda_path) - - # 加载新的视频纹理 - movie_texture = self._loadMovieTexture(video_path) # Pass original path for existence check - if movie_texture: - # 清除现有的纹理 - video_screen.clearTexture() - - # 设置视频纹理属性 - movie_texture.setWrapU(Texture.WM_clamp) - movie_texture.setWrapV(Texture.WM_clamp) - movie_texture.setMinfilter(Texture.FT_linear) - movie_texture.setMagfilter(Texture.FT_linear) - - # 如果视频纹理支持循环播放,设置循环 - if hasattr(movie_texture, 'set_loop'): - movie_texture.set_loop(True) - - # 如果视频纹理支持播放速率控制,设置正常速率 - if hasattr(movie_texture, 'set_play_rate'): - movie_texture.set_play_rate(1.0) - - # 重要:为视频纹理创建专用的纹理阶段 - texture_stage = TextureStage("video") - texture_stage.setSort(0) # 使用第一个纹理槽 - texture_stage.setMode(TextureStage.MModulate) - - # 应用纹理到视频屏幕 - video_screen.setTexture(texture_stage, movie_texture) - - # 保存新的视频纹理引用到PythonTag - video_screen.setPythonTag("movie_texture", movie_texture) - video_screen.setTag("video_path", converted_path) # Store converted path - - # 确保视频屏幕有正确的材质 - self._ensureVideoScreenMaterial(video_screen) - - print(f"✅ 成功加载新视频: {converted_path}") - return True - else: - print(f"❌ 无法加载视频文件: {converted_path}") - return False - - except Exception as e: - print(f"❌ 加载视频文件失败: {e}") - import traceback - traceback.print_exc() - return False - - def _loadMovieTexture(self, video_path): - """加载视频纹理的兼容方法""" - try: - from panda3d.core import Texture, MovieTexture, Filename - import os - - # 检查文件是否存在 - if not os.path.exists(video_path): - print(f"❌ 视频文件不存在: {video_path}") - return None - - # Convert Windows path to Panda3D compatible path format - panda_path = Filename.fromOsSpecific(video_path) - converted_path = str(panda_path) - - print(f"🔍 尝试加载视频文件: {converted_path}") - - # 方法1: 尝试使用 MovieTexture(专门用于视频) - try: - movie_texture = MovieTexture(converted_path) - if movie_texture.read(converted_path): - print("✅ 使用 MovieTexture 成功加载视频") - self._configureVideoTexture(movie_texture) - return movie_texture - else: - print("⚠️ MovieTexture.read() 返回失败") - except Exception as e: - print(f"⚠️ MovieTexture 方法失败: {e}") - - # 方法2: 尝试使用 loader.loadTexture - try: - movie_texture = self.world.loader.loadTexture(converted_path) - if movie_texture and hasattr(movie_texture, 'is_playable') and movie_texture.is_playable(): - print("✅ 使用 loader.loadTexture 成功加载视频纹理") - self._configureVideoTexture(movie_texture) - return movie_texture - else: - print("⚠️ loader.loadTexture 加载的不是可播放的视频纹理") - except Exception as e: - print(f"⚠️ loader.loadTexture 方法失败: {e}") - - # 方法3: 尝试使用 Texture.read(作为最后备选) - try: - texture = Texture() - if texture.read(converted_path): - print("✅ 使用 Texture.read 成功加载(可能作为静态纹理)") - self._configureVideoTexture(texture) - return texture - except Exception as e: - print(f"⚠️ Texture.read 方法失败: {e}") - - print("❌ 所有视频纹理加载方法都失败") - return None - - except Exception as e: - print(f"❌ 加载视频纹理时发生未知错误: {e}") - import traceback - traceback.print_exc() - return None - - def _configureVideoTexture(self, texture): - """配置视频纹理属性""" - try: - from panda3d.core import Texture - - # 设置纹理属性 - texture.setWrapU(Texture.WM_clamp) - texture.setWrapV(Texture.WM_clamp) - texture.setMinfilter(Texture.FT_linear) - texture.setMagfilter(Texture.FT_linear) - - # 如果是可播放的视频纹理,设置播放属性 - if hasattr(texture, 'set_loop') and hasattr(texture, 'set_play_rate'): - texture.set_loop(True) - texture.set_play_rate(1.0) - - print(f"✅ 视频纹理配置完成: {texture.getName()}") - - except Exception as e: - print(f"⚠️ 配置视频纹理时出错: {e}") - - def createGUI2DVideoScreen(self, pos=(0, 0), size=0.2, video_path=None): - """创建2D视频播放屏幕 - 使用2D坐标""" - try: - from direct.gui.DirectGui import DirectFrame - from panda3d.core import TransparencyAttrib, Texture, TextureStage - from PyQt5.QtCore import Qt - import os - - # 确保 pos 是有效的二维坐标元组 - if pos is None or pos is False or not isinstance(pos, (tuple, list)) or len(pos) != 2: - print(f"⚠️ 位置参数无效,使用默认值 (0, 0),原始值: {pos}") - pos = (0, 0) - else: - # 确保所有坐标都是数值类型 - try: - pos = (float(pos[0]), float(pos[1])) - except (ValueError, TypeError, IndexError) as e: - print(f"⚠️ 位置参数包含非数值或索引错误,使用默认值 (0, 0),原始值: {pos}, 错误: {e}") - pos = (0, 0) - - # 确保 size 是有效数值 - try: - size = float(size) - except (ValueError, TypeError) as e: - print(f"⚠️ 尺寸参数无效,使用默认值 0.2,原始值: {size}, 错误: {e}") - size = 0.2 - - # 获取树形控件 - tree_widget = self._get_tree_widget() - if not tree_widget: - print("❌ 无法访问树形控件") - return None - - # 获取目标父节点列表 - target_parents = tree_widget.get_target_parents_for_gui_creation() - if not target_parents: - print("❌ 没有找到有效的父节点") - return None - - created_videoscreens = [] - - # 为每个有效的父节点创建2D视频屏幕 - for parent_item, parent_node in target_parents: - try: - # 生成唯一名称 - screen_name = f"GUI2DVideoScreen_{len(self.gui_elements)}" - - # 使用DirectFrame创建2D视频屏幕 - video_screen = DirectFrame( - frameSize=(-size, size, -size, size), - frameColor=(1, 1, 1, 1), # 默认背景色 - pos=(pos[0] * 0.1, 0, pos[1] * 0.1), # 转换为屏幕坐标 - parent=parent_node if tree_widget.is_gui_element(parent_node) else self.world.aspect2d, - suppressMouse=True, - ) - - video_screen.setName(screen_name) - - # 设置透明度支持 - video_screen.setTransparency(TransparencyAttrib.MAlpha) - - #设置2D视频屏幕特有的标签 - video_screen.setTag("gui_type", "2d_video_screen") - video_screen.setTag("gui_id", f"2d_video_screen_{len(self.gui_elements)}") - video_screen.setTag("gui_text", f"2D视频屏幕_{len(self.gui_elements)}") - video_screen.setTag("is_gui_element", "1") - video_screen.setTag("is_scene_element", "1") - video_screen.setTag("tree_item_type", "GUI_2D_VIDEO_SCREEN") - video_screen.setTag("created_by_user", "1") - video_screen.setTag("name",screen_name) - video_screen.setTag("video_path",video_path or "") - - # 关键修改:预先创建一个占位符纹理,为后续视频播放做准备 - placeholder_texture = Texture(f"placeholder_video_texture_{len(self.gui_elements)}") - placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) - placeholder_data = b'\x19\x19\x4c' # 深蓝色占位符颜色 (25, 25, 76) - placeholder_texture.setRamImage(placeholder_data) - - # 应用占位符纹理到视频屏幕 - video_screen["frameTexture"] = placeholder_texture - - # 保存占位符纹理引用 - video_screen.setPythonTag("placeholder_texture", placeholder_texture) - - print(f"🔧 为2D视频屏幕创建了占位符纹理环境: {screen_name}") - - # 如果提供了视频路径,则加载视频纹理 - movie_texture = None - - if video_path: - if video_path.startswith(("http://","https://")): - try: - if hasattr(self.world,'property_panel'): - success = self.world.property_panel._loadVideoFromURLWithOpenCV(video_screen,video_path) - if success: - print(f"✅ 视频流URL加载成功: {video_path}") - else: - print(f"⚠️ 视频流URL加载失败: {video_path}") - else: - print("⚠️ property_manager不可用,无法加载视频流") - except Exception as e: - print(f"❌ 加载视频流URL失败: {e}") - import traceback - traceback.print_exc() - elif os.path.exists(video_path): - try: - print(f"🔍 尝试加载2D视频纹理: {video_path}") - # 加载视频纹理 - movie_texture = self._loadMovieTexture(video_path) - if movie_texture: - # 应用纹理到视频屏幕(替换占位符) - video_screen["frameTexture"] = movie_texture - print(f"✅ 2D视频纹理加载成功: {video_path}") - - # 保存视频纹理引用以便后续控制 - video_screen.setPythonTag("movie_texture", movie_texture) - - # 尝试自动播放视频(如果支持) - try: - if hasattr(movie_texture, 'play'): - movie_texture.play() - print("▶️ 2D视频已开始播放") - except Exception as play_error: - print(f"⚠️ 2D视频自动播放失败: {play_error}") - else: - print(f"⚠️ 无法加载2D视频纹理: {video_path}") - except Exception as e: - print(f"❌ 加载2D视频纹理失败: {e}") - import traceback - traceback.print_exc() - else: - print(f"⚠️ 2D视频文件不存在: {video_path}") - - # if video_path and os.path.exists(video_path): - # try: - # print(f"🔍 尝试加载2D视频纹理: {video_path}") - # # 加载视频纹理 - # movie_texture = self._loadMovieTexture(video_path) - # if movie_texture: - # # 应用纹理到视频屏幕(替换占位符) - # video_screen["frameTexture"] = movie_texture - # print(f"✅ 2D视频纹理加载成功: {video_path}") - # - # # 保存视频纹理引用以便后续控制 - # video_screen.setPythonTag("movie_texture", movie_texture) - # - # # 尝试自动播放视频(如果支持) - # try: - # if hasattr(movie_texture, 'play'): - # movie_texture.play() - # print("▶️ 2D视频已开始播放") - # except Exception as play_error: - # print(f"⚠️ 2D视频自动播放失败: {play_error}") - # else: - # print(f"⚠️ 无法加载2D视频纹理: {video_path}") - # except Exception as e: - # print(f"❌ 加载2D视频纹理失败: {e}") - # import traceback - # traceback.print_exc() - # else: - # if not video_path: - # print(f"⚠️ 2D视频文件不存在: {video_path}") - - # 添加到GUI元素列表 - self.gui_elements.append(video_screen) - - print(f"✅ 为 {parent_item.text(0)} 创建2D视频屏幕成功: {screen_name}") - - # 在Qt树形控件中添加对应节点 - qt_item = tree_widget.add_node_to_tree_widget(video_screen, parent_item, "GUI_2D_VIDEO_SCREEN") - if qt_item: - created_videoscreens.append((video_screen, qt_item)) - else: - created_videoscreens.append((video_screen, None)) - print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") - - except Exception as e: - print(f"❌ 为 {parent_item.text(0)} 创建2D视频屏幕失败: {str(e)}") - import traceback - traceback.print_exc() - continue - - # 处理创建结果 - if not created_videoscreens: - print("❌ 没有成功创建任何2D视频屏幕") - return None - - # 选中最后创建的视频屏幕 - # if created_videoscreens: - # last_screen_np, last_qt_item = created_videoscreens[-1] - # if last_qt_item: - # tree_widget.setCurrentItem(last_qt_item) - # # 更新选择和属性面板 - # tree_widget.update_selection_and_properties(last_screen_np, last_qt_item) - - print(f"🎉 总共创建了 {len(created_videoscreens)} 个2D视频屏幕") - - # 返回值处理 - if len(created_videoscreens) == 1: - return created_videoscreens[0][0] # 单个屏幕返回NodePath - else: - return [screen_np for screen_np, _ in created_videoscreens] # 多个屏幕返回列表 - - except Exception as e: - print(f"❌ 创建2D视频屏幕过程失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def load2DVideoFile(self, video_screen, video_path): - """为2D视频屏幕加载新的视频文件""" - try: - import os - - video_screen.setTag("video_path",video_path) - path = video_screen.getTag("video_path") - print({video_screen.getTag("gui_type")}) - print(f"🔧 更新2D视频屏幕标签 - video_path: {path}") - - if not video_path or not os.path.exists(video_path): - print(f"❌ 2D视频文件不存在: {video_path}") - return False - - # 加载新的视频纹理 - movie_texture = self._loadMovieTexture(video_path) - if movie_texture: - # 应用纹理到2D视频屏幕 - video_screen["frameTexture"] = movie_texture - - # 保存视频纹理引用 - video_screen.setPythonTag("movie_texture", movie_texture) - - print(f"✅ 成功加载新2D视频: {video_path}") - return True - else: - print(f"❌ 无法加载2D视频文件: {video_path}") - return False - - except Exception as e: - print(f"❌ 加载2D视频文件失败: {e}") - import traceback - traceback.print_exc() - return False - - def play2DVideo(self, video_screen): - """播放2D视频""" - try: - if video_screen.hasPythonTag("video_capture"): - print("视频已在播放中") - return True - - # 获取视频路径并重新开始播放 - video_path = video_screen.getTag("video_path") - if video_path: - return self.world.property_manager._loadVideoFromURL(video_screen, video_path) - else: - print("❌ 没有找到视频源") - return False - - except Exception as e: - print(f"❌ 播放视频失败: {e}") - return False - - def pause2DVideo(self, video_screen): - """暂停2D视频""" - try: - # 在OpenCV中没有直接的暂停功能,我们通过停止线程来模拟暂停 - if video_screen.hasPythonTag("video_capture"): - cap = video_screen.getPythonTag("video_capture") - if cap: - cap.release() - video_screen.clearPythonTag("video_capture") - print("⏸️ 视频已暂停") - return True - return False - except Exception as e: - print(f"❌ 暂停视频失败: {e}") - return False - - def stop2DVideo(self, video_screen): - """停止2D视频""" - try: - # 停止视频捕获 - if video_screen.hasPythonTag("video_capture"): - cap = video_screen.getPythonTag("video_capture") - if cap: - cap.release() - video_screen.clearPythonTag("video_capture") - - # 清理纹理 - if video_screen.hasPythonTag("video_texture"): - video_screen.clearPythonTag("video_texture") - - # 清理视频路径标签 - video_screen.clearTag("video_path") - - print("⏹️ 视频已停止") - return True - except Exception as e: - print(f"❌ 停止视频失败: {e}") - return False - - def createSphericalVideo(self, pos=(0, 0, 0), radius=5.0, video_path=None): - """创建球形视频(360度视频)- 支持无初始视频文件""" - try: - from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter - from panda3d.core import Geom, GeomTriangles, GeomNode - from panda3d.core import TextureStage, Texture - import math - import os - - # 确保 pos 是有效的三维坐标元组 - if not isinstance(pos, (tuple, list)) or len(pos) != 3: - print(f"⚠️ 位置参数无效,使用默认值 (0, 0, 0),原始值: {pos}") - pos = (0, 0, 0) - else: - # 确保所有坐标都是数值类型 - try: - pos = (float(pos[0]), float(pos[1]), float(pos[2])) - except (ValueError, TypeError): - print(f"⚠️ 位置参数包含非数值,使用默认值 (0, 0, 0),原始值: {pos}") - pos = (0, 0, 0) - - # 确保 radius 是有效数值 - try: - radius = float(radius) - except (ValueError, TypeError): - print(f"⚠️ 半径参数无效,使用默认值 5.0,原始值: {radius}") - radius = 5.0 - - print(f"🌍 开始创建球形视频,位置: {pos}, 半径: {radius}, 视频路径: {video_path}") - - # 不再强制检查视频文件是否存在,允许创建空的球形视频 - if video_path and not os.path.exists(video_path): - print(f"⚠️ 视频文件不存在,将创建空的球形视频: {video_path}") - - # 获取树形控件 - tree_widget = self._get_tree_widget() - if not tree_widget: - print("❌ 无法访问树形控件") - return None - - # 获取目标父节点列表 - target_parents = tree_widget.get_target_parents_for_creation() - if not target_parents: - print("❌ 没有找到有效的父节点") - return None - - created_spherical_videos = [] - - # 为每个有效的父节点创建球形视频 - for parent_item, parent_node in target_parents: - try: - # 生成唯一名称 - sphere_name = f"SphericalVideo_{len(self.gui_elements)}" - - # 创建球形几何体 - sphere_geom = self._createSphereGeometry(radius, segments=32) - - # 创建几何节点 - sphere_node = GeomNode('sphere_video') - sphere_node.addGeom(sphere_geom) - - # 创建节点路径并挂载 - sphere_np = parent_node.attachNewNode(sphere_node) - sphere_np.setPos(*pos) # 现在 pos 已经是有效的元组 - sphere_np.setName(sphere_name) - - # 翻转法线,使视频在球体内部显示 - sphere_np.setTwoSided(True) - sphere_np.setBin('background', 0) # 确保在背景层 - sphere_np.setDepthWrite(False) # 不写入深度缓冲 - - # 设置初始颜色为占位符颜色 - sphere_np.setColor(0.1, 0.1, 0.3, 0.8) # 深蓝色占位符 - - # 如果提供了视频路径且文件存在,则加载视频纹理 - movie_texture = None - if video_path and os.path.exists(video_path): - try: - # 加载视频纹理 - movie_texture = self._loadMovieTexture(video_path) - if movie_texture: - # 设置视频纹理属性 - movie_texture.setWrapU(Texture.WM_clamp) - movie_texture.setWrapV(Texture.WM_clamp) - movie_texture.setMinfilter(Texture.FT_linear) - movie_texture.setMagfilter(Texture.FT_linear) - - # 为视频纹理创建专用的纹理阶段 - texture_stage = TextureStage("spherical_video") - texture_stage.setMode(TextureStage.MModulate) - - # 应用纹理到球体 - sphere_np.setTexture(texture_stage, movie_texture) - - # 设置为白色以正确显示视频 - sphere_np.setColor(1, 1, 1, 1) - - print(f"✅ 视频纹理已应用到球形视频: {video_path}") - - # 尝试自动播放 - try: - if hasattr(movie_texture, 'play'): - movie_texture.play() - print("▶️ 球形视频已开始播放") - except Exception as play_error: - print(f"⚠️ 球形视频自动播放失败: {play_error}") - else: - print(f"⚠️ 无法加载视频纹理: {video_path}") - # 保持占位符颜色 - except Exception as e: - print(f"❌ 加载视频纹理失败: {e}") - import traceback - traceback.print_exc() - # 保持占位符颜色 - else: - if video_path: - print(f"⚠️ 视频文件不存在: {video_path}") - else: - print("ℹ️ 未提供视频文件,创建空的球形视频") - - # 设置标签以便识别和管理 - sphere_np.setTag("gui_type", "spherical_video") - sphere_np.setTag("is_gui_element", "1") - sphere_np.setTag("tree_item_type", "GUI_SPHERICAL_VIDEO") - sphere_np.setTag("video_path", video_path or "") - sphere_np.setTag("original_radius", str(radius)) - - # 保存视频纹理引用 - if movie_texture: - sphere_np.setPythonTag("movie_texture", movie_texture) - - # 添加到GUI元素列表 - self.gui_elements.append(sphere_np) - - print(f"✅ 为 {parent_item.text(0)} 创建球形视频成功: {sphere_name}") - - # 在Qt树形控件中添加对应节点 - qt_item = tree_widget.add_node_to_tree_widget(sphere_np, parent_item, "GUI_SPHERICAL_VIDEO") - if qt_item: - created_spherical_videos.append((sphere_np, qt_item)) - else: - created_spherical_videos.append((sphere_np, None)) - print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") - - except Exception as e: - print(f"❌ 为 {parent_item.text(0)} 创建球形视频失败: {str(e)}") - import traceback - traceback.print_exc() - continue - - # 处理创建结果 - if not created_spherical_videos: - print("❌ 没有成功创建任何球形视频") - return None - - # 选中最后创建的球形视频 - # if created_spherical_videos: - # last_sphere_np, last_qt_item = created_spherical_videos[-1] - # if last_qt_item: - # tree_widget.setCurrentItem(last_qt_item) - # # 更新选择和属性面板 - # tree_widget.update_selection_and_properties(last_sphere_np, last_qt_item) - - print(f"🎉 总共创建了 {len(created_spherical_videos)} 个球形视频") - - # 返回值处理 - if len(created_spherical_videos) == 1: - return created_spherical_videos[0][0] # 单个球形视频返回NodePath - else: - return [sphere_np for sphere_np, _ in created_spherical_videos] # 多个球形视频返回列表 - - except Exception as e: - print(f"❌ 创建球形视频过程失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def _createSphereGeometry(self, radius=5.0, segments=32): - """创建球形几何体""" - try: - from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter - from panda3d.core import Geom, GeomTriangles - import math - - # 创建顶点数据 - format = GeomVertexFormat.getV3n3t2() - vdata = GeomVertexData('sphere', format, Geom.UHStatic) - - vertex = GeomVertexWriter(vdata, 'vertex') - normal = GeomVertexWriter(vdata, 'normal') - texcoord = GeomVertexWriter(vdata, 'texcoord') - - # 生成球体顶点 - vertices = [] - for i in range(segments + 1): - phi = math.pi * i / segments # 从0到π - for j in range(segments * 2 + 1): - theta = 2 * math.pi * j / (segments * 2) # 从0到2π - - x = radius * math.sin(phi) * math.cos(theta) - y = radius * math.cos(phi) - z = radius * math.sin(phi) * math.sin(theta) - - # 纹理坐标 - u = j / (segments * 2) - v = i / segments - - # 修复:将坐标作为一个元组添加到列表中 - vertices.append((x, y, z, u, v)) - - # 添加顶点数据 - for vert in vertices: - vertex.addData3f(vert[0], vert[1], vert[2]) - # 法线(标准化顶点位置) - length = math.sqrt(vert[0] ** 2 + vert[1] ** 2 + vert[2] ** 2) - if length > 0: - normal.addData3f(vert[0] / length, vert[1] / length, vert[2] / length) - else: - normal.addData3f(0, 1, 0) - texcoord.addData2f(vert[3], vert[4]) - - # 创建几何体 - geom = Geom(vdata) - prim = GeomTriangles(Geom.UHStatic) - - # 生成三角形面 - for i in range(segments): - for j in range(segments * 2): - # 计算顶点索引 - v1 = i * (segments * 2 + 1) + j - v2 = (i + 1) * (segments * 2 + 1) + j - v3 = (i + 1) * (segments * 2 + 1) + (j + 1) - v4 = i * (segments * 2 + 1) + (j + 1) - - # 添加两个三角形 - prim.addVertices(v1, v2, v3) - prim.addVertices(v1, v3, v4) - - prim.closePrimitive() - geom.addPrimitive(prim) - - return geom - - except Exception as e: - print(f"❌ 创建球形几何体失败: {e}") - import traceback - traceback.print_exc() - raise - - def playSphericalVideo(self,spherical_video_node): - try: - if not spherical_video_node: - return False - texture = spherical_video_node.getTexture() - if texture and hasattr(texture,'play'): - texture.play() - return True - else: - return False - except Exception as e: - return False - - def pauseSphericalVideo(self,spherical_video_node): - try: - if not spherical_video_node: - return False - texture = spherical_video_node.getTexture() - if texture and hasattr(texture,'stop'): - texture.stop() - return True - else: - return False - except Exception as e: - return False - - def setSphericalVideoTime(self,spherical_video_node,time_seconds): - try: - if not spherical_video_node: - return False - texture = spherical_video_node.getTexture() - if texture and hasattr(texture,'set_time'): - texture.setTime(time_seconds) - return True - else: - return False - except Exception as e: - return False - - def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"): - """创建3D虚拟屏幕 - 支持多选创建,优化版本""" - try: - from panda3d.core import CardMaker, TransparencyAttrib, TextNode - from PyQt5.QtCore import Qt - - print(f"🖥️ 开始创建虚拟屏幕,位置: {pos}, 尺寸: {size}, 文本: {text}") - - # 获取树形控件 - tree_widget = self._get_tree_widget() - if not tree_widget: - print("❌ 无法访问树形控件") - return None - - # 获取目标父节点列表 - target_parents = tree_widget.get_target_parents_for_creation() - if not target_parents: - print("❌ 没有找到有效的父节点") - return None - - created_screens = [] - - # 为每个有效的父节点创建虚拟屏幕 - for parent_item, parent_node in target_parents: - try: - # 生成唯一名称 - screen_name = f"VirtualScreen_{len(self.gui_elements)}" - - # 创建虚拟屏幕几何体 - cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}") - cm.setFrame(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2) - - # 创建挂载节点 - 挂载到选中的父节点 - virtual_screen = parent_node.attachNewNode(cm.generate()) - virtual_screen.setPos(*pos) - virtual_screen.setName(screen_name) - virtual_screen.setColor(0.2, 0.2, 0.2, 0.8) - virtual_screen.setTransparency(TransparencyAttrib.MAlpha) - - # 在虚拟屏幕上添加文本 - screen_text_node = self._create_screen_text(virtual_screen, text, len(self.gui_elements)) - - # 设置节点标签 - virtual_screen.setTag("gui_type", "virtual_screen") - virtual_screen.setTag("gui_id", f"virtual_screen_{len(self.gui_elements)}") - virtual_screen.setTag("gui_text", text) - virtual_screen.setTag("is_gui_element", "1") - virtual_screen.setTag("is_scene_element", "1") - virtual_screen.setTag("tree_item_type", "GUI_VirtualScreen") - virtual_screen.setTag("created_by_user", "1") - - # 添加到GUI元素列表 - self.gui_elements.append(virtual_screen) - - print(f"✅ 为 {parent_item.text(0)} 创建虚拟屏幕成功: {screen_name}") - - # 在Qt树形控件中添加对应节点 - qt_item = tree_widget.add_node_to_tree_widget(virtual_screen, parent_item, "GUI_VirtualScreen") - if qt_item: - created_screens.append((virtual_screen, qt_item)) - else: - created_screens.append((virtual_screen, None)) - print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") - - except Exception as e: - print(f"❌ 为 {parent_item.text(0)} 创建虚拟屏幕失败: {str(e)}") - continue - - # 处理创建结果 - if not created_screens: - print("❌ 没有成功创建任何虚拟屏幕") - return None - - # 选中最后创建的虚拟屏幕 - # if created_screens: - # last_screen_np, last_qt_item = created_screens[-1] - # if last_qt_item: - # tree_widget.setCurrentItem(last_qt_item) - # # 更新选择和属性面板 - # tree_widget.update_selection_and_properties(last_screen_np, last_qt_item) - - print(f"🎉 总共创建了 {len(created_screens)} 个虚拟屏幕") - - # 返回值处理 - if len(created_screens) == 1: - return created_screens[0][0] # 单个屏幕返回NodePath - else: - return [screen_np for screen_np, _ in created_screens] # 多个屏幕返回列表 - - except Exception as e: - print(f"❌ 创建虚拟屏幕过程失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def _create_screen_text(self, virtual_screen, text, screen_index): - """为虚拟屏幕创建文本节点""" - try: - from panda3d.core import TextNode - - screen_text = TextNode(f'screen-text-{screen_index}') - screen_text.setText(text) - screen_text.setAlign(TextNode.ACenter) - - # 设置中文字体 - if hasattr(self.world, 'getChineseFont') and self.world.getChineseFont(): - screen_text.setFont(self.world.getChineseFont()) - - # 创建文本节点路径并设置属性 - screen_text_np = virtual_screen.attachNewNode(screen_text) - screen_text_np.setPos(0, 0.01, 0) # 稍微向前偏移避免Z-fighting - screen_text_np.setScale(0.3) - screen_text_np.setColor(0, 1, 0, 1) # 绿色文本 - - print(f"✅ 虚拟屏幕文本创建成功: {text}") - return screen_text_np - - except Exception as e: - print(f"❌ 创建虚拟屏幕文本失败: {str(e)}") - return None - - def _get_tree_widget(self): - """安全获取树形控件""" - try: - if (hasattr(self.world, 'interface_manager') and - hasattr(self.world.interface_manager, 'treeWidget')): - return self.world.interface_manager.treeWidget - except AttributeError: - pass - return None - - # 暂无滑块功能 - def createGUISlider(self, pos=(0, 0, 0), text="滑块", scale=0.3): - pass - """创建2D GUI滑块""" - from direct.gui.DirectGui import DirectSlider - - gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) - - slider = DirectSlider( - pos=gui_pos, - scale=scale, - range=(0, 100), - value=50, - frameColor=(0.6, 0.6, 0.6, 1), - thumbColor=(0.2, 0.8, 0.2, 1) - ) - - slider.setTag("gui_type", "slider") - slider.setTag("gui_id", f"slider_{len(self.gui_elements)}") - slider.setTag("gui_text", text) - slider.setTag("is_gui_element", "1") - - self.gui_elements.append(slider) - # 安全地调用updateSceneTree - if hasattr(self.world, 'updateSceneTree'): - pass # CH - self.world.updateSceneTree() - - print(f"✓ 创建GUI滑块: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})") - return slider - - # ==================== GUI元素管理方法 ==================== - - def deleteGUIElement(self, gui_element): - """删除GUI元素""" - try: - if gui_element in self.gui_elements: - # # 移除GUI元素 - # if hasattr(gui_element, 'removeNode'): - # gui_element.removeNode() - # elif hasattr(gui_element, 'destroy'): - # gui_element.destroy() - # - # # 从列表中移除 - # self.gui_elements.remove(gui_element) - - # 更新场景树 - # 安全地调用updateSceneTree - tree_widget = self._get_tree_widget() - if tree_widget: - tree_widget.delete_items(tree_widget.selectedItems()) - # if hasattr(self.world, 'updateSceneTree'): - # self.world.updateSceneTree() - - print(f"删除GUI元素: {gui_element}") - return True - except Exception as e: - print(f"删除GUI元素失败: {str(e)}") - return False - - # 在 gui_manager.py 中确保 editGUIElement 方法正确处理文本颜色 - def editGUIElement(self, gui_element, property_name, value): - """编辑GUI元素属性""" - try: - from panda3d.core import TextNode - - gui_type = gui_element.getTag("gui_type") if hasattr(gui_element, 'getTag') else "unknown" - print(f"开始编辑GUI元素: 类型={gui_type}, 属性={property_name}, 值={value}") - - if property_name == "text": - original_frame_size = None - if hasattr(gui_element,'getFrameSize'): - try: - original_frame_size = gui_element.getFrameSize() - except: - pass - if gui_type in ["button", "label"]: - gui_element['text'] = value - print(f"成功更新2D GUI文本: {value}") - # if gui_type == "button": - # self._resizeButtonToText(gui_element,value,original_frame_size) - elif gui_type == "entry": - gui_element.set(value) - print(f"成功更新输入框文本: {value}") - elif gui_type == "3d_text": - # 对于3D文本,直接修改自身的TextNode - if isinstance(gui_element.node(), TextNode): - gui_element.node().setText(value) - print(f"成功更新3D文本: {value}") - else: - print(f"警告: {gui_type}节点类型为{type(gui_element.node())},不是TextNode类型") - - elif gui_type == "virtual_screen": - # 对于虚拟屏幕,需要找到TextNode子节点 - print(f"虚拟屏幕有 {gui_element.getNumChildren()} 个子节点") - text_found = False - for i, child in enumerate(gui_element.getChildren()): - print(f"子节点 {i}: {child.getName()}, 类型: {type(child.node())}") - if isinstance(child.node(), TextNode): - child.node().setText(value) - text_found = True - print(f"成功更新虚拟屏幕文本: {value}") - break - - if not text_found: - print(f"警告: 在{gui_type}中未找到TextNode子节点") - - gui_element.setTag("gui_text", value) - - elif property_name == "color": # 添加颜色处理 - if isinstance(value, (list, tuple)) and len(value) >= 3: - # 对于2D GUI元素(button和label),使用frameColor属性 - if gui_type in ["button", "label"]: - # 设置背景颜色 - gui_element['frameColor'] = (value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0) - print(f"成功更新2D GUI背景颜色: {gui_type} -> {value}") - - # 对于3D元素,使用材质颜色 - elif gui_type in ["3d_text", "3d_image", "virtual_screen"]: - # 更新材质颜色 - if not gui_element.hasMaterial(): - material = Material(f"text-material-{gui_element.getName()}") - material.setBaseColor(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) - material.setDiffuse(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) - gui_element.setMaterial(material, 1) - else: - material = gui_element.getMaterial() - material.setBaseColor(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) - material.setDiffuse(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) - gui_element.setMaterial(material, 1) - - # 更新 TextNode 的文本颜色 - if isinstance(gui_element.node(), TextNode): - gui_element.node().setTextColor( - Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) - print(f"成功更新3D GUI颜色: {gui_type} -> {value}") - - else: - print(f"警告: 未知的GUI类型 {gui_type},无法设置颜色") - - elif property_name == "position": - if isinstance(value, (list, tuple)) and len(value) >= 3: - gui_element.setPos(*value[:3]) - - elif property_name == "scale": - if isinstance(value, (int, float)): - gui_element.setScale(value) - elif isinstance(value, (list, tuple)) and len(value) >= 3: - gui_element.setScale(*value[:3]) - - print(f"编辑GUI元素 {gui_type}: {property_name} = {value}") - return True - - except Exception as e: - print(f"编辑GUI元素失败: {str(e)}") - import traceback - traceback.print_exc() - return False - - def _resizeButtonToText(self, button, text, original_frame_size=None): - """ - 根据文本内容调整按钮大小 - - Args: - button: DirectButton 对象 - text: 新的文本内容 - original_frame_size: 原始frameSize,用于计算合适的padding - """ - try: - # 获取按钮当前的文本缩放 - text_scale = 0.03 # 默认文本缩放 - padding = 0.2 # 默认边距 - - # 尝试从按钮获取实际的文本缩放值 - if hasattr(button, 'getTextScale'): - text_scale = button.getTextScale() - elif hasattr(button, 'textScale'): - text_scale = button.textScale - - # 根据原始frameSize计算合适的padding - if original_frame_size and len(original_frame_size) >= 4: - # 基于原始按钮宽度计算合适的padding - padding = max((original_frame_size[1] - original_frame_size[0]) * 0.15, 0.1) - - # 更精确的文本尺寸估算 - # 考虑中文字符和英文字符的不同宽度 - chinese_chars = len([c for c in text if ord(c) > 127]) - english_chars = len(text) - chinese_chars - - # 中文字符通常比英文字符宽 - char_width = text_scale * 0.6 - text_width = (chinese_chars * char_width * 1.5) + (english_chars * char_width) - text_height = text_scale * 1.2 # 文本高度 - - # 计算新的frameSize,确保有足够的边距 - half_width = max(text_width * 0.5 + padding, 0.3) # 最小宽度0.3 - half_height = max(text_height * 0.5 + padding * 0.5, 0.15) # 最小高度0.15 - - # 正确设置frameSize - 使用字典方式设置 - new_frame = (-half_width, half_width, -half_height, half_height) - button['frameSize'] = new_frame # 使用字典方式设置 - - print(f"按钮大小已调整: {new_frame} (文本: {text})") - - except Exception as e: - print(f"调整按钮大小失败: {e}") - # 如果自动调整失败,保持原有大小或设置一个合理的默认大小 - try: - if original_frame_size: - # 保持原有大小 - button['frameSize'] = original_frame_size - else: - # 设置一个合理的默认大小 - button['frameSize'] = (-0.5, 0.5, -0.15, 0.15) - except: - pass - - def duplicateGUIElement(self, gui_element): - """复制GUI元素""" - try: - gui_type = gui_element.getTag("gui_type") - gui_text = gui_element.getTag("gui_text") - - # 获取当前位置并偏移 - pos = gui_element.getPos() - new_pos = (pos.getX() + 0.2, pos.getY(), pos.getZ() + 0.2) - - # 根据类型创建新的GUI元素 - if gui_type == "button": - self.createGUIButton(new_pos, gui_text + "_副本") - elif gui_type == "label": - self.createGUILabel(new_pos, gui_text + "_副本") - elif gui_type == "entry": - self.createGUIEntry(new_pos, gui_text + "_副本") - elif gui_type == "3d_text": - self.createGUI3DText(new_pos, gui_text + "_副本") - elif gui_type == "3d_image": - image_path = gui_element.getTag("image_path") - self.createGUI3DImage(new_pos,image_path,size=(2,2)) - elif gui_type == "2d_image": - image_path = gui_element.getTag("image_path") - self.createGUI2DImage(new_pos, image_path, size=0.2) - elif gui_type == "virtual_screen": - self.createGUIVirtualScreen(new_pos, text=gui_text + "_副本") - - print(f"复制GUI元素: {gui_type} - {gui_text}") - - except Exception as e: - print(f"复制GUI元素失败: {str(e)}") - - def editGUIElementDialog(self, gui_element): - """显示GUI元素编辑对话框""" - dialog = QDialog() - dialog.setWindowTitle("编辑GUI元素") - dialog.setMinimumWidth(300) - - layout = QVBoxLayout(dialog) - form = QFormLayout() - - gui_type = gui_element.getTag("gui_type") - gui_text = gui_element.getTag("gui_text") - - # 文本编辑 - if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]: - textEdit = QLineEdit(gui_text or "") - form.addRow("文本:", textEdit) - - # 位置编辑 - if hasattr(gui_element, 'getPos'): - pos = gui_element.getPos() - xEdit = QDoubleSpinBox() - xEdit.setRange(-1000, 1000) - xEdit.setValue(pos.getX()) - form.addRow("位置 X:", xEdit) - - yEdit = QDoubleSpinBox() - yEdit.setRange(-1000, 1000) - yEdit.setValue(pos.getY()) - form.addRow("位置 Y:", yEdit) - - zEdit = QDoubleSpinBox() - zEdit.setRange(-1000, 1000) - zEdit.setValue(pos.getZ()) - form.addRow("位置 Z:", zEdit) - - # 缩放编辑 - if hasattr(gui_element, 'getScale'): - scale = gui_element.getScale() - scaleEdit = QDoubleSpinBox() - scaleEdit.setRange(0.01, 100) - scaleEdit.setSingleStep(0.1) - scaleEdit.setValue(scale.getX()) - form.addRow("缩放:", scaleEdit) - - layout.addLayout(form) - - # 按钮 - buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) - buttonBox.accepted.connect(dialog.accept) - buttonBox.rejected.connect(dialog.reject) - layout.addWidget(buttonBox) - - # 执行对话框 - if dialog.exec_() == QDialog.Accepted: - try: - # 应用更改 - if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]: - self.editGUIElement(gui_element, "text", textEdit.text()) - - if hasattr(gui_element, 'getPos'): - self.editGUIElement(gui_element, "position", [xEdit.value(), yEdit.value(), zEdit.value()]) - - if hasattr(gui_element, 'getScale'): - self.editGUIElement(gui_element, "scale", scaleEdit.value()) - - # 更新属性面板 - if self.world.treeWidget: - currentItem = self.world.treeWidget.currentItem() - if currentItem: - self.world.updatePropertyPanel(currentItem) - - print("GUI元素编辑完成") - - except Exception as e: - print(f"应用GUI编辑失败: {str(e)}") - - # ==================== GUI事件处理方法 ==================== - - def onGUIButtonClick(self, button_id): - """GUI按钮点击事件处理""" - print(f"GUI按钮被点击: {button_id}") - - def onGUIEntrySubmit(self, text, entry_id): - """GUI输入框提交事件处理""" - print(f"GUI输入框提交: {entry_id} = {text}") - - # ==================== GUI编辑模式方法 ==================== - - def toggleGUIEditMode(self): - """切换GUI编辑模式""" - self.guiEditMode = not self.guiEditMode - - if self.guiEditMode: - self.enterGUIEditMode() - else: - self.exitGUIEditMode() - - def enterGUIEditMode(self): - """进入GUI编辑模式""" - print("\n=== 进入GUI编辑模式 ===") - - # 打开GUI预览窗口 - self.openGUIPreviewWindow() - - # 创建GUI编辑面板 - self.createGUIEditPanel() - - # 改变当前工具为GUI选择工具 - self.world.currentTool = "GUI编辑" - - print("GUI编辑模式已激活") - print("- 使用右侧工具栏创建GUI元素") - print("- 在独立预览窗口中查看效果") - print("- 左键点击现有GUI元素选择和编辑") - - def exitGUIEditMode(self): - """退出GUI编辑模式""" - print("\n=== 退出GUI编辑模式 ===") - - # 关闭GUI预览窗口 - self.closeGUIPreviewWindow() - - # 移除GUI编辑面板 - if self.guiEditPanel: - self.guiEditPanel.destroy() - self.guiEditPanel = None - - # 恢复普通工具 - self.world.currentTool = "选择" - - print("GUI编辑模式已关闭") - - def createGUIEditPanel(self): - """创建GUI编辑面板""" - from direct.gui.DirectGui import DirectFrame, DirectButton, DirectLabel - - # 创建主面板 - self.guiEditPanel = DirectFrame( - pos=(0.85, 0, 0), - frameSize=(-0.15, 0.15, -0.9, 0.9), - frameColor=(0.1, 0.1, 0.1, 0.8), - text="GUI编辑器", - text_pos=(0, 0.85), - text_scale=0.05, - text_fg=(1, 1, 1, 1), - text_font=self.world.getChineseFont() if self.world.getChineseFont() else None - ) - - # 创建工具按钮 - y_pos = 0.7 - spacing = 0.12 - - # 2D GUI工具 - label_2d = DirectLabel( - parent=self.guiEditPanel, - text="2D GUI", - pos=(0, 0, y_pos), - scale=0.04, - text_fg=(1, 1, 0, 1), - frameColor=(0, 0, 0, 0), - text_font=self.world.getChineseFont() if self.world.getChineseFont() else None - ) - y_pos -= 0.08 - - # 按钮工具 - btn_button = DirectButton( - parent=self.guiEditPanel, - text="按钮", - pos=(0, 0, y_pos), - scale=0.04, - command=self.setGUICreateTool, - extraArgs=["button"], - frameColor=(0.2, 0.6, 0.8, 1), - text_font=self.world.getChineseFont() if self.world.getChineseFont() else None - ) - y_pos -= spacing - - # 标签工具 - btn_label = DirectButton( - parent=self.guiEditPanel, - text="标签", - pos=(0, 0, y_pos), - scale=0.04, - command=self.setGUICreateTool, - extraArgs=["label"], - frameColor=(0.6, 0.8, 0.2, 1), - text_font=self.world.getChineseFont() if self.world.getChineseFont() else None - ) - y_pos -= spacing - - # 输入框工具 - btn_entry = DirectButton( - parent=self.guiEditPanel, - text="输入框", - pos=(0, 0, y_pos), - scale=0.04, - command=self.setGUICreateTool, - extraArgs=["entry"], - frameColor=(0.8, 0.6, 0.2, 1), - text_font=self.world.getChineseFont() if self.world.getChineseFont() else None - ) - y_pos -= spacing - - # 3D GUI工具 - label_3d = DirectLabel( - parent=self.guiEditPanel, - text="3D GUI", - pos=(0, 0, y_pos), - scale=0.04, - text_fg=(1, 1, 0, 1), - frameColor=(0, 0, 0, 0), - text_font=self.world.getChineseFont() if self.world.getChineseFont() else None - ) - y_pos -= 0.08 - - # 3D文本工具 - btn_3dtext = DirectButton( - parent=self.guiEditPanel, - text="3D文本", - pos=(0, 0, y_pos), - scale=0.04, - command=self.setGUICreateTool, - extraArgs=["3d_text"], - frameColor=(0.8, 0.2, 0.6, 1), - text_font=self.world.getChineseFont() if self.world.getChineseFont() else None - ) - y_pos -= spacing - - #3D图片工具 - btn_image = DirectButton( - parent = self.guiEditPanel, - text="3D图片", - pos=(0,0,y_pos), - scale=0.04, - command=self.setGUICreateTool, - extraArgs=["3d_image"], - frameColor=(0.2,0.8,0.8,1), - text_font=self.world.getChineseFont() if self.world.getChineseFont() else None - ) - y_pos -= spacing - - # 2D图片工具 - btn_2d_image = DirectButton( - parent=self.guiEditPanel, - text="2D图片", - pos=(0, 0, y_pos), - scale=0.04, - command=self.setGUICreateTool, - extraArgs=["2d_image"], - frameColor=(0.8, 0.6, 0.2, 1), - text_font=self.world.getChineseFont() if self.world.getChineseFont() else None - ) - y_pos -= spacing - - # 虚拟屏幕工具 - btn_screen = DirectButton( - parent=self.guiEditPanel, - text="虚拟屏幕", - pos=(0, 0, y_pos), - scale=0.04, - command=self.setGUICreateTool, - extraArgs=["virtual_screen"], - frameColor=(0.6, 0.2, 0.8, 1), - text_font=self.world.getChineseFont() if self.world.getChineseFont() else None - ) - y_pos -= spacing - - #Cesium 集成工具 (仅在Webengine 可用时显示) - if WEB_ENGINE_AVAILABLE: - label_cesium = DirectLabel( - parent=self.guiEditPanel, - text="Cesium 集成", - pos=(0, 0, y_pos), - scale=0.04, - text_fg=(1, 1, 0, 1), - frameColor=(0, 0, 0, 0), - text_font=self.world.getChineseFont() if self.world.getChineseFont() else None - ) - y_pos -= 0.08 - - # 切换 Cesium 视图按钮 - btn_toggle_cesium = DirectButton( - parent=self.guiEditPanel, - text="切换地图视图", - pos=(0, 0, y_pos), - scale=0.04, - command=self.toggleCesiumView, - frameColor=(0.2, 0.8, 0.6, 1), - text_font=self.world.getChineseFont() if self.world.getChineseFont() else None - ) - y_pos -= spacing - - # 刷新 Cesium 视图按钮 - btn_refresh_cesium = DirectButton( - parent=self.guiEditPanel, - text="刷新地图", - pos=(0, 0, y_pos), - scale=0.04, - command=self.refreshCesiumView, - frameColor=(0.6, 0.8, 0.2, 1), - text_font=self.world.getChineseFont() if self.world.getChineseFont() else None - ) - y_pos -= spacing - - # 分隔线 - y_pos -= 0.1 - - # 操作按钮 - label_ops = DirectLabel( - parent=self.guiEditPanel, - text="操作", - pos=(0, 0, y_pos), - scale=0.04, - text_fg=(1, 1, 0, 1), - frameColor=(0, 0, 0, 0), - text_font=self.world.getChineseFont() if self.world.getChineseFont() else None - ) - y_pos -= 0.08 - - # 删除工具 - btn_delete = DirectButton( - parent=self.guiEditPanel, - text="删除", - pos=(0, 0, y_pos), - scale=0.04, - command=self.deleteSelectedGUI, - frameColor=(0.8, 0.2, 0.2, 1), - text_font=self.world.getChineseFont() if self.world.getChineseFont() else None - ) - y_pos -= spacing - - # 复制工具 - btn_copy = DirectButton( - parent=self.guiEditPanel, - text="复制", - pos=(0, 0, y_pos), - scale=0.04, - command=self.copySelectedGUI, - frameColor=(0.2, 0.8, 0.2, 1), - text_font=self.world.getChineseFont() if self.world.getChineseFont() else None - ) - y_pos -= spacing - - # 退出GUI编辑模式 - btn_exit = DirectButton( - parent=self.guiEditPanel, - text="退出", - pos=(0, 0, -0.8), - scale=0.04, - command=self.toggleGUIEditMode, - frameColor=(0.5, 0.5, 0.5, 1), - text_font=self.world.getChineseFont() if self.world.getChineseFont() else None - ) - - # 存储当前的GUI创建工具 - self.currentGUITool = None - - def openGUIPreviewWindow(self): - """打开独立的GUI预览窗口""" - try: - from gui_preview_window import GUIPreviewWindow - - self.guiPreviewWindow = GUIPreviewWindow() - self.guiPreviewWindow.set_main_world(self.world) - - print("✓ GUI预览窗口已打开") - print("这个独立窗口会实时显示您创建的GUI元素") - - except ImportError: - print("错误: 无法导入GUI预览窗口模块") - except Exception as e: - print(f"打开GUI预览窗口失败: {str(e)}") - - def closeGUIPreviewWindow(self): - """关闭GUI预览窗口""" - if self.guiPreviewWindow: - self.guiPreviewWindow.destroy() - self.guiPreviewWindow = None - print("GUI预览窗口已关闭") - - # ==================== GUI工具和选择方法 ==================== - - def setGUICreateTool(self, tool_type): - """设置GUI创建工具""" - self.currentGUITool = tool_type - print(f"选择GUI创建工具: {tool_type}") - - def deleteSelectedGUI(self): - """删除选中的GUI元素""" - if self.world.selection.selectedNode and hasattr(self.world.selection.selectedNode, 'getTag'): - gui_type = self.world.selection.selectedNode.getTag("gui_type") - if gui_type: - success = self.deleteGUIElement(self.world.selection.selectedNode) - if success: - self.world.selection.updateSelection(None) - print("GUI元素已删除") - else: - print("删除GUI元素失败") - else: - print("选中的不是GUI元素") - else: - print("没有选中的GUI元素") - - def copySelectedGUI(self): - """复制选中的GUI元素""" - if self.world.selection.selectedNode and hasattr(self.world.selection.selectedNode, 'getTag'): - gui_type = self.world.selection.selectedNode.getTag("gui_type") - if gui_type: - self.duplicateGUIElement(self.world.selection.selectedNode) - print("GUI元素已复制") - else: - print("选中的不是GUI元素") - else: - print("没有选中的GUI元素") - - def handleGUIEditClick(self, hitPos): - """处理GUI编辑模式下的点击""" - if not self.guiEditMode: - return False - - if self.currentGUITool: - # 创建新的GUI元素 - self.createGUIAtPosition(hitPos, self.currentGUITool) - return True - - return False - - def createGUIAtPosition(self, world_pos, gui_type): - """在指定位置创建GUI元素""" - print(f"在位置 {world_pos} 创建 {gui_type}") - - # 根据GUI类型选择合适的坐标转换 - if gui_type in ["button", "label", "entry"]: - # 2D GUI - 将世界坐标转换为屏幕逻辑坐标 - screen_x = world_pos.getX() * 2 # 缩放因子 - screen_z = world_pos.getZ() * 2 - pos = (screen_x, 0, screen_z) - else: - # 3D GUI - 直接使用世界坐标 - pos = (world_pos.getX(), world_pos.getY(), world_pos.getZ()) - - # 创建不同类型的GUI元素 - if gui_type == "button": - element = self.createGUIButton(pos, f"按钮_{len(self.gui_elements)}") - elif gui_type == "label": - element = self.createGUILabel(pos, f"标签_{len(self.gui_elements)}") - elif gui_type == "entry": - element = self.createGUIEntry(pos, f"输入框_{len(self.gui_elements)}") - elif gui_type == "3d_text": - element = self.createGUI3DText(pos, f"3D文本_{len(self.gui_elements)}") - elif gui_type == "3d_image": - element = self.createGUI3DImage(pos) - elif gui_type == "2d_image": - element = self.createGUI2DImage(pos) - elif gui_type == "virtual_screen": - element = self.createGUIVirtualScreen(pos, text=f"屏幕_{len(self.gui_elements)}") - else: - print(f"未知的GUI类型: {gui_type}") - return - - # 自动选中新创建的元素 - self.world.selection.updateSelection(element) - self.selectGUIInTree(element) - print(f"创建并选中了新的{gui_type}元素") - - def findClickedGUI(self, hitNode): - """查找被点击的GUI元素""" - # 检查点击的节点是否是GUI元素 - current = hitNode - while current != self.world.render: - if hasattr(current, 'getTag') and current.getTag("gui_type"): - return current - current = current.getParent() - return None - - def selectGUIInTree(self, gui_element): - """在树形控件中选中GUI元素""" - if not self.world.treeWidget or not gui_element: - return - - def findGUIItem(item): - """递归查找GUI元素对应的树形项""" - if item.data(0, Qt.UserRole) == gui_element: - return item - - for i in range(item.childCount()): - child = item.child(i) - result = findGUIItem(child) - if result: - return result - return None - - # 从根开始查找 - root = self.world.treeWidget.invisibleRootItem() - for i in range(root.childCount()): - sceneItem = root.child(i) - if sceneItem.text(0) == "场景": - for j in range(sceneItem.childCount()): - childItem = sceneItem.child(j) - if childItem.text(0) == "GUI元素": - foundItem = findGUIItem(childItem) - if foundItem: - self.world.treeWidget.setCurrentItem(foundItem) - self.world.updatePropertyPanel(foundItem) - return - - def updateGUISelection(self, gui_element): - """更新GUI元素选择状态""" - self.world.selection.updateSelection(gui_element) - if gui_element and hasattr(gui_element, 'getTag'): - gui_type = gui_element.getTag("gui_type") - gui_text = gui_element.getTag("gui_text") - print(f"选中GUI元素: {gui_type} - {gui_text}") - - # 在树形控件中选中 - self.selectGUIInTree(gui_element) - - # ==================== GUI属性面板方法 ==================== - - def updateGUIPropertyPanel(self, gui_element, layout): - """更新GUI元素属性面板""" - gui_type = gui_element.getTag("gui_type") - gui_text = gui_element.getTag("gui_text") - - # GUI类型显示 - typeLabel = QLabel("GUI类型:") - typeValue = QLabel(gui_type) - typeValue.setStyleSheet("color: #00AAFF; font-weight: bold;") - layout.addRow(typeLabel, typeValue) - - # 文本属性(如果适用) - if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]: - textLabel = QLabel("文本:") - textEdit = QLineEdit(gui_text or "") - - # 创建一个更新函数来处理文本变化 - def updateText(text): - success = self.editGUIElement(gui_element, "text", text) - if success: - # 更新场景树显示的名称 - # 安全地调用updateSceneTree - if hasattr(self.world, 'updateSceneTree'): - self.world.updateSceneTree() - - textEdit.textChanged.connect(updateText) - layout.addRow(textLabel, textEdit) - - # 位置属性 - if hasattr(gui_element, 'getPos'): - # 根据GUI类型设置组名 - if gui_type in ["button", "label", "entry", "2d_image"]: - transform_group = QGroupBox("变换 Rect Transform") - else: - transform_group = QGroupBox("变换 Transform") - - transform_layout = QGridLayout() - - pos = gui_element.getPos() - - # 根据GUI类型决定位置编辑方式 - if gui_type in ["button", "label", "entry","2d_image"]: - # 2D GUI组件使用屏幕坐标 - logical_x = pos.getX() / 0.1 # 反向转换为逻辑坐标 - logical_z = pos.getZ() / 0.1 - - transform_layout.addWidget(QLabel("屏幕位置"), 0, 0) - - x_label = QLabel("X") - z_label = QLabel("z") - x_label.setAlignment(Qt.Aligncenter) - z_label.setAlignment(Qt.AlignCenter) - - transform_layout.addWidget(x_label, 0, 1) - transform_layout.addWidget(z_label, 0, 2) - - xPos = QDoubleSpinBox() - xPos.setRange(-1000, 1000) - xPos.setValue(logical_x) - xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "x", v)) - transform_layout.addWidget(xPos, 1, 1) - - zPos = QDoubleSpinBox() - zPos.setRange(-1000, 1000) - zPos.setValue(logical_z) - zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "z", v)) - transform_layout.addWidget(zPos, 1, 2) - - # 显示实际屏幕坐标(只读) - transform_layout.addWidget(QLabel("实际坐标"), 2, 0) - - actualXLabel = QLabel(f"{pos.getX():.3f}") - actualXLabel.setStyleSheet("color: gray; font-size: 10px;") - actualZLabel = QLabel(f"{pos.getZ():.3f}") - actualZLabel.setStyleSheet("color: gray; font-size: 10px;") - - transform_layout.addWidget(actualXLabel, 3, 1) - transform_layout.addWidget(actualZLabel, 3, 2) - # 添加宽度和高度控件(对于2D图像) - if gui_type == "2d_image": - - # 添加排序控制组 - sort_group = QGroupBox("渲染顺序") - sort_layout = QGridLayout() - - # 获取当前的sort值,如果没有设置则默认为0 - current_sort = int(gui_element.getTag("sort") or "0") - - sort_layout.addWidget(QLabel("层级:"), 0, 0) - - sort_spin = QSpinBox() - sort_spin.setRange(-1000, 1000) # 设置合理的范围 - sort_spin.setValue(current_sort) - - # 创建更新排序的函数 - def updateSort(value): - try: - # 设置标签 - gui_element.setTag("sort", str(value)) - # 应用sort到节点 - 使用fixed bin和指定的值 - gui_element.setBin("fixed", value) - print(f"✓ 更新2D图像渲染顺序: {value}") - except Exception as e: - print(f"✗ 更新2D图像渲染顺序失败: {e}") - - sort_spin.valueChanged.connect(updateSort) - sort_layout.addWidget(sort_spin, 0, 1) - - # 添加说明标签 - sort_help = QLabel("(数值越大越靠前)") - sort_help.setStyleSheet("font-size: 10px; color: gray;") - sort_layout.addWidget(sort_help, 1, 0, 1, 2) - - sort_group.setLayout(sort_layout) - layout.addWidget(sort_group) - - scale = gui_element.getScale() - width = scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, - (tuple, list)) else scale - height = scale.getZ() if hasattr(scale, 'getZ') else scale[1] if isinstance(scale, - (tuple, list)) and len( - scale) > 1 else scale - - # 宽度控件 - transform_layout.addWidget(QLabel("宽度"), 4, 0) - widthSpinBox = QDoubleSpinBox() - widthSpinBox.setRange(0.01, 100) - widthSpinBox.setSingleStep(0.1) - widthSpinBox.setValue(width) - widthSpinBox.valueChanged.connect( - lambda v: self.world.gui_manager.editGUIScale(gui_element, "x", v)) - transform_layout.addWidget(widthSpinBox, 4, 1) - - # 高度控件 - transform_layout.addWidget(QLabel("高度"), 4, 2) - heightSpinBox = QDoubleSpinBox() - heightSpinBox.setRange(0.01, 100) - heightSpinBox.setSingleStep(0.1) - heightSpinBox.setValue(height) - heightSpinBox.valueChanged.connect( - lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v)) - transform_layout.addWidget(heightSpinBox, 4, 3) - - else: - # 3D GUI组件使用世界坐标 - transform_layout.addWidget(QLabel("位置"), 0, 0) - - # X, Y, Z 标签居中 - x_label = QLabel("X") - y_label = QLabel("Y") - z_label = QLabel("Z") - x_label.setAlignment(Qt.AlignCenter) - y_label.setAlignment(Qt.AlignCenter) - z_label.setAlignment(Qt.AlignCenter) - - transform_layout.addWidget(x_label, 0, 1) - transform_layout.addWidget(y_label, 0, 2) - transform_layout.addWidget(z_label, 0, 3) - - # 位置数值输入框 - xPos = QDoubleSpinBox() - xPos.setRange(-100, 100) - xPos.setValue(pos.getX()) - xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "x", v)) - transform_layout.addWidget(xPos, 1, 1) - - yPos = QDoubleSpinBox() - yPos.setRange(-100, 100) - yPos.setValue(pos.getY()) - yPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "y", v)) - transform_layout.addWidget(yPos, 1, 2) - - zPos = QDoubleSpinBox() - zPos.setRange(-100, 100) - zPos.setValue(pos.getZ()) - zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "z", v)) - transform_layout.addWidget(zPos, 1, 3) - - # 缩放属性 - scale = gui_element.getScale() - transform_layout.addWidget(QLabel("缩放"), 2, 0) - - # X, Y, Z 缩放标签居中 - sx_label = QLabel("X") - sy_label = QLabel("Y") - sz_label = QLabel("Z") - sx_label.setAlignment(Qt.AlignCenter) - sy_label.setAlignment(Qt.AlignCenter) - sz_label.setAlignment(Qt.AlignCenter) - - transform_layout.addWidget(sx_label, 2, 1) - transform_layout.addWidget(sy_label, 2, 2) - transform_layout.addWidget(sz_label, 2, 3) - - # 缩放数值输入框 - scale_x = QDoubleSpinBox() - scale_x.setRange(0.01, 100) - scale_x.setSingleStep(0.1) - scale_x.setValue( - scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, (tuple, list)) else scale) - scale_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "x", v)) - transform_layout.addWidget(scale_x, 3, 1) - - scale_y = QDoubleSpinBox() - scale_y.setRange(0.01, 100) - scale_y.setSingleStep(0.1) - scale_y.setValue( - scale.getY() if hasattr(scale, 'getY') else scale[1] if isinstance(scale, (tuple, list)) and len( - scale) > 1 else scale) - scale_y.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "y", v)) - transform_layout.addWidget(scale_y, 3, 2) - - scale_z = QDoubleSpinBox() - scale_z.setRange(0.01, 100) - scale_z.setSingleStep(0.1) - scale_z.setValue( - scale.getZ() if hasattr(scale, 'getZ') else scale[2] if isinstance(scale, (tuple, list)) and len( - scale) > 2 else scale) - scale_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v)) - transform_layout.addWidget(scale_z, 3, 3) - transform_group.setLayout(transform_layout) - self._propertyLayout.addWidget(transform_group) - - # 缩放属性 - if hasattr(gui_element, 'getScale'): - scale = gui_element.getScale() - - scaleSpinBox = QDoubleSpinBox() - scaleSpinBox.setRange(0.01, 100) - scaleSpinBox.setSingleStep(0.1) - scaleSpinBox.setValue(scale.getX()) - scaleSpinBox.valueChanged.connect(lambda v: self.editGUIElement(gui_element, "scale", v)) - layout.addRow("缩放:", scaleSpinBox) - - # 颜色属性(针对2D GUI) - if gui_type in ["button", "label"]: - colorButton = QPushButton("选择颜色") - colorButton.clicked.connect(lambda: self.selectGUIColor(gui_element)) - layout.addRow("背景颜色:", colorButton) - - # 3D特有属性 - if gui_type in ["3d_text", "virtual_screen"]: - # 旋转属性 - if hasattr(gui_element, 'getHpr'): - hpr = gui_element.getHpr() - - hRot = QDoubleSpinBox() - hRot.setRange(-180, 180) - hRot.setValue(hpr.getX()) - hRot.valueChanged.connect(lambda v: gui_element.setH(v)) - layout.addRow("旋转 H:", hRot) - - pRot = QDoubleSpinBox() - pRot.setRange(-180, 180) - pRot.setValue(hpr.getY()) - pRot.valueChanged.connect(lambda v: gui_element.setP(v)) - layout.addRow("旋转 P:", pRot) - - rRot = QDoubleSpinBox() - rRot.setRange(-180, 180) - rRot.setValue(hpr.getZ()) - rRot.valueChanged.connect(lambda v: gui_element.setR(v)) - layout.addRow("旋转 R:", rRot) - - def selectGUIColor(self, gui_element): - """选择GUI元素颜色""" - color = QColorDialog.getColor(QColor(128, 128, 128), None, "选择颜色") - if color.isValid(): - r, g, b = color.red() / 255.0, color.green() / 255.0, color.blue() / 255.0 - self.editGUIElement(gui_element, "color", [r, g, b, 1.0]) - - def update3DImageTexture(self, model_nodepath, image_path): - from panda3d.core import Texture, TextureStage - - try: - # 加载新纹理 - new_texture = self.world.loader.loadTexture(image_path) - if new_texture: - # 确保纹理过滤质量 - new_texture.setMagfilter(Texture.FT_linear) - new_texture.setMinfilter(Texture.FT_linear_mipmap_linear) - - model_nodepath.clearTexture() - - # 为3D图像创建独立的纹理阶段 - image_stage = TextureStage("3d_image_texture") - image_stage.setSort(0) # 使用第一个纹理槽 - image_stage.setMode(TextureStage.MModulate) # 使用调制模式 - - # 应用纹理到模型,使用独立的纹理阶段 - model_nodepath.setTexture(image_stage, new_texture) - - # 更新标签 - model_nodepath.setTag("gui_image_path", image_path) - - # 确保材质设置正确 - if not model_nodepath.hasMaterial(): - from panda3d.core import Material, LColor - mat = Material() - mat.setName(f"image-material-{id(model_nodepath)}") - mat.setBaseColor(LColor(1, 1, 1, 1)) - mat.setDiffuse(LColor(1, 1, 1, 1)) - mat.setAmbient(LColor(0.5, 0.5, 0.5, 1)) - mat.setSpecular(LColor(0.1, 0.1, 0.1, 1.0)) - mat.setShininess(10.0) - model_nodepath.setMaterial(mat, 1) - - # 保护子节点的纹理(特别是3D文本) - self._preserveChildNodeTextures(model_nodepath) - - print(f"✅ 3D图像纹理已更新为: {image_path}") - return True - else: - print(f"❌ 无法加载纹理: {image_path}") - return False - except Exception as e: - print(f"❌ 更新纹理时出错: {e}") - return False - - def _preserveChildNodeTextures(self, parent_node): - """保护子节点的纹理不被父节点纹理影响""" - try: - # 遍历所有直接子节点 - for i in range(parent_node.getNumChildren()): - child = parent_node.getChild(i) - - # 检查子节点是否为3D文本或其他需要特殊处理的节点 - if self._is3DTextElement(child): - # 为3D文本创建独立的纹理阶段 - self._restore3DTextTexture(child) - elif self._isOtherSpecialElement(child): - # 为其他特殊元素恢复纹理 - self._restoreSpecialElementTexture(child) - - except Exception as e: - print(f"保护子节点纹理时出错: {e}") - - def _is3DTextElement(self, node): - """检查节点是否为3D文本元素""" - try: - return (hasattr(node, 'getTag') and - node.getTag("gui_type") == "3d_text") - except: - return False - - def _isOtherSpecialElement(self, node): - """检查节点是否为其他需要特殊处理的元素""" - try: - return (hasattr(node, 'getTag') and - node.getTag("gui_type") in ["3d_image", "button", "label"]) - except: - return False - - def _restore3DTextTexture(self, text_node): - """恢复3D文本的纹理设置""" - try: - from panda3d.core import TextureStage - - # 如果3D文本已经有字体纹理,确保它使用正确的纹理阶段 - if hasattr(text_node, 'getTexture') and text_node.getTexture(): - # 创建专门用于文本的纹理阶段 - text_stage = TextureStage("text_texture") - text_stage.setSort(10) # 使用较高的纹理槽索引 - text_stage.setMode(TextureStage.MModulate) - - # 重新应用文本纹理 - current_texture = text_node.getTexture() - text_node.setTexture(text_stage, current_texture) - - print(f"已恢复3D文本纹理: {text_node.getName()}") - - except Exception as e: - print(f"恢复3D文本纹理失败: {e}") - - def update2DImageTexture(self, gui_element, image_path): - """更新2D图片纹理""" - try: - # 加载新纹理 - new_texture = self.world.loader.loadTexture(image_path) - if new_texture: - new_texture.setMagfilter(Texture.FT_linear) - new_texture.setMinfilter(Texture.FT_linear_mipmap_linear) - - new_texture.setFormat(Texture.F_rgba) - new_texture.setWrapU(Texture.WM_clamp) - new_texture.setWrapV(Texture.WM_clamp) - - # 应用纹理到模型 - gui_element.setTexture(new_texture, 1) - - # 更新标签 - gui_element.setTag("texture_path", image_path) - gui_element.setTag("image_path", image_path) - - print(f"✅ 2D图像纹理已更新为: {image_path}") - return True - else: - print(f"❌ 无法加载2D图片纹理: {image_path}") - return False - except Exception as e: - print(f"❌ 更新2D图片纹理时出错: {e}") - return False - - # 在gui_manager.py或其他相关文件中添加以下方法 - - def editGUI2DPosition(self, gui_element, axis, value): - """编辑2D GUI元素位置并应用边界约束""" - try: - gui_type = gui_element.getTag("gui_type") - - if gui_type in ["button", "label", "entry", "2d_image","2d_video_screen"]: - # 2D元素使用屏幕坐标,需要转换 - current_pos = gui_element.getPos() - - if axis == "x": - # 转换逻辑坐标到屏幕坐标 - screen_x = value * 0.1 - new_pos = (screen_x,current_pos[1],current_pos[2]) - elif axis == "z": - screen_z = value * 0.1 - new_pos = (current_pos[0],current_pos[1],screen_z) - else: - return False - gui_element.setPos(*new_pos) - return True - else: - print(f"✗ 不支持的GUI类型进行2D位置编辑: {gui_type}") - return False - except Exception as e: - print(f"✗ 更新2D GUI元素位置失败: {e}") - import traceback - traceback.print_exc() - return False - - def editGUI3DPosition(self, gui_element, axis, value): - """编辑3D GUI元素位置""" - try: - gui_type = gui_element.getTag("gui_type") - - if gui_type in ["3d_text", "3d_image", "video_screen","info_panel","info_panel_3d"]: - current_pos = gui_element.getPos() - - if axis == "x": - new_pos = (value, current_pos.getY(), current_pos.getZ()) - elif axis == "y": - new_pos = (current_pos.getX(), value, current_pos.getZ()) - elif axis == "z": - new_pos = (current_pos.getX(), current_pos.getY(), value) - else: - return False - - gui_element.setPos(*new_pos) - #print(f"✓ 更新3D GUI元素位置: {axis}={value}") - return True - else: - print(f"✗ 不支持的GUI类型进行3D位置编辑: {gui_type}") - return False - except Exception as e: - print(f"✗ 更新3D GUI元素位置失败: {e}") - import traceback - traceback.print_exc() - return False - - def editGUIScale(self, gui_element, axis, value): - """编辑GUI元素缩放""" - try: - gui_type = gui_element.getTag("gui_type") - current_scale = gui_element.getScale() - - # 确保缩放值不为0 - if value == 0: - value = 0.01 - - if gui_type in ["3d_text", "3d_image","video_screen","virtual_screen","info_panel","info_panel_3d"]: - # 3D元素处理 - if axis == "x": - new_scale = (value, current_scale.getY(), current_scale.getZ()) - elif axis == "y": - new_scale = (current_scale.getX(), value, current_scale.getZ()) - elif axis == "z": - new_scale = (current_scale.getX(), current_scale.getY(), value) - else: - return False - - gui_element.setScale(*new_scale) - elif gui_type == "2d_image": - # 2D图像特殊处理 - 分别控制宽度和高度 - if axis == "x": - # X轴控制宽度 - gui_element.setScale(value, current_scale.getZ() if hasattr(current_scale, 'getZ') - else current_scale[1] if isinstance(current_scale, (list, tuple)) - else current_scale) - elif axis == "z": - # Z轴控制高度 - gui_element.setScale(current_scale.getX() if hasattr(current_scale, 'getX') - else current_scale[0] if isinstance(current_scale, (list, tuple)) - else current_scale, value) - else: - # 其他情况使用统一缩放 - gui_element.setScale(value) - gui_element.setTransparency(TransparencyAttrib.MAlpha) - else: - # 其他2D元素处理 - if axis in ["x", "z"]: # 对于2D图像,x和z分别代表宽度和高度 - # 保持原有缩放比例,仅调整指定轴 - if axis == "x": - gui_element.setScale(value, - current_scale.getZ() if hasattr(current_scale, 'getZ') - else current_scale[1] if isinstance(current_scale, (list, tuple)) - else current_scale) - elif axis == "z": - gui_element.setScale( - current_scale.getX() if hasattr(current_scale, 'getX') - else current_scale[0] if isinstance(current_scale, (list, tuple)) - else current_scale, value) - else: - # 对于其他2D元素,使用统一缩放 - gui_element.setScale(value) - - return True - except Exception as e: - print(f"✗ 更新GUI元素缩放失败: {e}") - import traceback - traceback.print_exc() - return False - - def createCesiumView(self, main_window=None): - """创建 Cesium 视图窗口(离线版本)""" - if not WEB_ENGINE_AVAILABLE: - print("❌ 无法创建Cesium视图: Web引擎不可用") - return None - - try: - from PyQt5.QtWebEngineWidgets import QWebEngineView - from PyQt5.QtWidgets import QDockWidget - from PyQt5.QtCore import QUrl - import os - - # 尝试获取主窗口引用 - if main_window is None: - print("🔍 尝试获取主窗口引用...") - - # 检查各种可能的主窗口引用 - if hasattr(self.world, 'interface_manager'): - print(f" - interface_manager 存在: {self.world.interface_manager}") - if hasattr(self.world.interface_manager, 'main_window'): - main_window = self.world.interface_manager.main_window - print(f" - interface_manager.main_window: {main_window}") - - if main_window is None and hasattr(self.world, 'main_window'): - main_window = self.world.main_window - print(f" - world.main_window: {main_window}") - - # 如果仍然没有主窗口,尝试从树形控件获取 - if main_window is None and self.world.treeWidget: - try: - main_window = self.world.treeWidget.window() - print(f" - 从 treeWidget 获取窗口: {main_window}") - except: - pass - - if main_window is None: - print("✗ 无法获取主窗口引用") - return None - else: - print(f"✅ 使用传入的主窗口引用: {main_window}") - - # 检查主窗口是否有效 - if not hasattr(main_window, 'addDockWidget'): - print(f"✗ 主窗口引用无效,缺少 addDockWidget 方法") - return None - - # 检查是否已经存在 Cesium 视图 - for element in self.gui_elements: - if hasattr(element, 'objectName') and element.objectName() == "CesiumView": - print("⚠ Cesium 视图已经存在") - # 将其前置显示 - element.show() - element.raise_() - return element - - # 创建停靠窗口 - print(f"🔧 创建 Cesium 停靠窗口,父窗口: {main_window}") - cesium_dock = QDockWidget("Cesium 地图视图(离线)", main_window) - cesium_dock.setObjectName("CesiumView") - - # 创建 Web 视图 - self.cesium_view = QWebEngineView() - - # 使用本地 HTML 文件(离线模式) - local_html_path = os.path.abspath("./cesium_offline.html") - if os.path.exists(local_html_path): - print(f"🌐 加载离线 Cesium: file://{local_html_path}") - self.cesium_view.load(QUrl(f"file://{local_html_path}")) - else: - print("⚠️ 离线文件不存在,使用在线版本") - self.cesium_view.load(QUrl("http://localhost:8080/Apps/HelloWorld.html")) - - # 设置内容 - cesium_dock.setWidget(self.cesium_view) - - # 添加到主窗口 - print("📍 将 Cesium 视图添加到主窗口") - main_window.addDockWidget(Qt.RightDockWidgetArea, cesium_dock) - - # 添加到GUI元素列表以便管理 - self.gui_elements.append(cesium_dock) - - print("✓ Cesium 离线视图已创建并集成到项目中") - return cesium_dock - - except Exception as e: - print(f"✗ 创建 Cesium 视图失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def toggleCesiumView(self): - """切换 Cesium 视图显示状态""" - if not WEB_ENGINE_AVAILABLE: - print("✗ QtWebEngineWidgets 不可用,无法切换 Cesium 视图") - return None - - try: - # 查找现有的 Cesium 视图 - cesium_dock = None - cesium_index = -1 - for i, element in enumerate(self.gui_elements): - if hasattr(element, 'objectName') and element.objectName() == "CesiumView": - cesium_dock = element - cesium_index = i - break - - # 如果存在则移除,否则创建 - if cesium_dock: - # 获取主窗口引用以正确移除停靠窗口 - main_window = None - if (hasattr(self.world, 'interface_manager') and - hasattr(self.world.interface_manager, 'main_window') and - self.world.interface_manager.main_window): - main_window = self.world.interface_manager.main_window - elif hasattr(self.world, 'main_window') and self.world.main_window: - main_window = self.world.main_window - - if main_window and hasattr(main_window, 'removeDockWidget'): - main_window.removeDockWidget(cesium_dock) - - # 从列表中移除 - if cesium_index >= 0: - self.gui_elements.pop(cesium_index) - - print("✓ Cesium 视图已隐藏") - return None - else: - return self.createCesiumView() - - except Exception as e: - print(f"✗ 切换 Cesium 视图失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def refreshCesiumView(self): - """刷新 Cesium 视图""" - if not WEB_ENGINE_AVAILABLE: - print("✗ QtWebEngineWidgets 不可用,无法刷新 Cesium 视图") - return False - - try: - for element in self.gui_elements: - if hasattr(element, 'objectName') and element.objectName() == "CesiumView": - web_view = element.widget() - if isinstance(web_view, QWebEngineView): - web_view.reload() - print("✓ Cesium 视图已刷新") - return True - print("⚠ 未找到 Cesium 视图") - return False - except Exception as e: - print(f"✗ 刷新 Cesium 视图失败: {str(e)}") - return False - - def updateCesiumURL(self, url): - """更新 Cesium 视图的 URL""" - if not WEB_ENGINE_AVAILABLE: - print("✗ QtWebEngineWidgets 不可用,无法更新 Cesium URL") - return False - - try: - for element in self.gui_elements: - if hasattr(element, 'objectName') and element.objectName() == "CesiumView": - web_view = element.widget() - if isinstance(web_view, QWebEngineView): - from PyQt5.QtCore import QUrl - web_view.load(QUrl(url)) - print(f"✓ Cesium URL 已更新为: {url}") - return True - print("⚠ 未找到 Cesium 视图") - return False - except Exception as e: - print(f"✗ 更新 Cesium URL 失败: {str(e)}") - return False - - # 在 GUIManager 类中添加以下方法 - - def addModelToCesium(self, model_id, model_url, longitude, latitude, height=0, scale=1.0): - """向 Cesium 添加模型""" - if not WEB_ENGINE_AVAILABLE: - print("✗ QtWebEngineWidgets 不可用,无法操作 Cesium") - return False - - try: - # 查找 Cesium 视图 - cesium_view = None - for element in self.gui_elements: - if (hasattr(element, 'objectName') and - element.objectName() == "CesiumView" and - hasattr(element, 'widget')): - cesium_view = element.widget() - break - - if not cesium_view: - print("✗ 未找到 Cesium 视图") - return False - - # 转义特殊字符以防止 JavaScript 语法错误 - escaped_model_id = str(model_id).replace("'", "\\'") - escaped_model_url = str(model_url).replace("'", "\\'").replace("\\", "/") - - # 构造 JavaScript 调用 - js_code = f""" - (function() {{ - if (window.CesiumAPI && typeof window.CesiumAPI.addModel === 'function') {{ - try {{ - var result = window.CesiumAPI.addModel( - '{escaped_model_id}', - '{escaped_model_url}', - {{ - longitude: {longitude}, - latitude: {latitude}, - height: {height} - }}, - {scale} - ); - console.log('Cesium 添加模型结果:', result); - return result || {{success: true, message: 'Model added'}}; - }} catch (error) {{ - console.error('JavaScript 错误:', error); - return {{success: false, message: 'JavaScript error: ' + error.message}}; - }} - }} else {{ - console.error('CesiumAPI.addModel 不可用'); - return {{success: false, message: 'CesiumAPI.addModel not available'}}; - }} - }})(); - """ - - # 定义回调函数处理结果 - def handle_result(result): - try: - if isinstance(result, dict): - if result.get('success', False): - print(f"✓ 成功在 Cesium 中添加模型: {model_id}") - else: - print(f"✗ 在 Cesium 中添加模型失败: {result.get('message', 'Unknown error')}") - else: - print(f"✓ 已发送添加模型请求: {model_id}") - except Exception as callback_error: - print(f"✗ 处理回调结果时出错: {callback_error}") - - # 执行 JavaScript 并获取结果 - cesium_view.page().runJavaScript(js_code, handle_result) - return True - - except Exception as e: - print(f"✗ 添加模型到 Cesium 失败: {e}") - import traceback - traceback.print_exc() - return False - - # 添加新的方法来集成 Panda3D 场景中的 Cesium Tiles - def addCesiumTilesetToScene(self, tileset_name, tileset_url, position=(0, 0, 0)): - """在 Panda3D 场景中添加 Cesium 3D Tiles""" - try: - # 使用场景管理器加载 tileset - tileset_node = self.world.scene_manager.load_cesium_tileset(tileset_url, position) - - if tileset_node: - # 添加到 GUI 元素列表以便管理 - self.gui_elements.append({ - 'type': 'cesium_tileset', - 'name': tileset_name, - 'node': tileset_node, - 'url': tileset_url - }) - - print(f"✓ 在场景中添加 Cesium tileset: {tileset_name}") - return tileset_node - else: - print(f"✗ 在场景中添加 Cesium tileset 失败: {tileset_name}") - return None - - except Exception as e: - print(f"✗ 在场景中添加 Cesium tileset 出错: {e}") - return None - - def removeModelFromCesium(self, model_id): - """从 Cesium 移除模型""" - if not WEB_ENGINE_AVAILABLE: - print("✗ QtWebEngineWidgets 不可用") - return False - - try: - # 查找 Cesium 视图 - cesium_view = None - for element in self.gui_elements: - if (hasattr(element, 'objectName') and - element.objectName() == "CesiumView" and - hasattr(element, 'widget')): - cesium_view = element.widget() - break - - if not cesium_view: - print("✗ 未找到 Cesium 视图") - return False - - # 构造 JavaScript 调用 - js_code = f""" - if (window.CesiumAPI && typeof window.CesiumAPI.removeModel === 'function') {{ - var result = window.CesiumAPI.removeModel('{model_id}'); - result; - }} else {{ - {{success: false, message: 'CesiumAPI.removeModel not available'}}; - }} - """ - - # 定义回调函数处理结果 - def handle_result(result): - if result and isinstance(result, dict): - if result.get('success', False): - print(f"✓ 成功从 Cesium 中移除模型: {model_id}") - else: - print(f"✗ 从 Cesium 中移除模型失败: {result.get('message', 'Unknown error')}") - else: - print(f"✓ 已发送移除模型请求: {model_id} (无法获取详细结果)") - - # 执行 JavaScript 并获取结果 - cesium_view.page().runJavaScript(js_code, handle_result) - return True - - except Exception as e: - print(f"✗ 从 Cesium 移除模型失败: {e}") - return False - - def updateCesiumModelPosition(self, model_id, longitude, latitude, height=0): - """更新 Cesium 中模型的位置""" - if not WEB_ENGINE_AVAILABLE: - print("✗ QtWebEngineWidgets 不可用") - return False - - try: - # 查找 Cesium 视图 - cesium_view = None - for element in self.gui_elements: - if (hasattr(element, 'objectName') and - element.objectName() == "CesiumView" and - hasattr(element, 'widget')): - cesium_view = element.widget() - break - - if not cesium_view: - print("✗ 未找到 Cesium 视图") - return False - - # 使用更安全的 JavaScript 字符串构造方式 - escaped_model_id = model_id.replace("'", "\\'") - - # 构造 JavaScript 调用 - js_code = f""" - (function() {{ - if (window.CesiumAPI && typeof window.CesiumAPI.updateModelPosition === 'function') {{ - try {{ - var result = window.CesiumAPI.updateModelPosition( - '{escaped_model_id}', - {{ - longitude: {longitude}, - latitude: {latitude}, - height: {height} - }} - ); - return result || {{success: true, message: 'Position updated'}}; - }} catch (error) {{ - return {{success: false, message: 'JavaScript error: ' + error.message}}; - }} - }} else {{ - return {{success: false, message: 'CesiumAPI.updateModelPosition not available'}}; - }} - }})(); - """ - - # 定义回调函数处理结果 - def handle_result(result): - try: - if isinstance(result, dict): - if result.get('success', False): - print(f"✓ 成功更新 Cesium 中模型位置: {model_id}") - else: - print(f"✗ 更新 Cesium 中模型位置失败: {result.get('message', 'Unknown error')}") - else: - print(f"✓ 已发送更新模型位置请求: {model_id}") - except Exception as callback_error: - print(f"✗ 处理回调结果时出错: {callback_error}") - - # 执行 JavaScript 并获取结果 - cesium_view.page().runJavaScript(js_code, handle_result) - return True - - except Exception as e: - print(f"✗ 更新 Cesium 中模型位置失败: {e}") - return False - - def getAllCesiumModels(self): - """获取 Cesium 中所有模型的列表""" - if not WEB_ENGINE_AVAILABLE: - print("✗ QtWebEngineWidgets 不可用") - return None - - try: - # 查找 Cesium 视图 - cesium_view = None - for element in self.gui_elements: - if (hasattr(element, 'objectName') and - element.objectName() == "CesiumView" and - hasattr(element, 'widget')): - cesium_view = element.widget() - break - - if not cesium_view: - print("✗ 未找到 Cesium 视图") - return None - - # 构造 JavaScript 调用 - js_code = """ - if (window.CesiumAPI && typeof window.CesiumAPI.getAllModels === 'function') { - var result = window.CesiumAPI.getAllModels(); - JSON.stringify(result); - } else { - JSON.stringify({success: false, message: 'CesiumAPI.getAllModels not available'}); - } - """ - - # 定义回调函数处理结果 - def handle_result(result): - try: - if isinstance(result, str): - import json - result = json.loads(result) - - if result and result.get('success', False): - models = result.get('models', []) - print(f"✓ Cesium 中的模型列表: {models}") - return models - else: - print(f"✗ 获取 Cesium 模型列表失败: {result.get('message', 'Unknown error')}") - return [] - except Exception as e: - print(f"✗ 解析 Cesium 模型列表结果失败: {e}") - return [] - - # 执行 JavaScript 并获取结果 - cesium_view.page().runJavaScript(js_code) - return None # 异步操作,实际结果通过回调处理 - - except Exception as e: - print(f"✗ 获取 Cesium 模型列表失败: {e}") - return None - - # 添加一个便捷方法来加载本地模型文件 - def addLocalModelToCesium(self, model_id, local_model_path, longitude, latitude, height=0, scale=1.0): - """向 Cesium 添加本地模型文件""" - try: - # 将本地路径转换为相对路径或 URL - import os - if os.path.exists(local_model_path): - # 如果 Cesium 服务器可以访问该路径,可以直接使用 - # 否则需要将模型文件放在 Cesium 的静态资源目录中 - model_url = local_model_path.replace('\\', '/') # 确保使用正斜杠 - return self.addModelToCesium(model_id, model_url, longitude, latitude, height, scale) - else: - print(f"✗ 模型文件不存在: {local_model_path}") - return False - except Exception as e: - print(f"✗ 添加本地模型失败: {e}") - return False +""" +GUI元素管理模块 + +负责2D/3D GUI元素的管理: +- GUI元素的创建(按钮、标签、输入框等) +- GUI编辑模式 +- GUI属性编辑 +- GUI元素的复制和删除 +""" + +from direct.gui.DirectGui import * +from panda3d.core import * +from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QFormLayout, QLineEdit, + QDoubleSpinBox, QPushButton, QDialogButtonBox, + QColorDialog, QLabel, QWidget, QGroupBox, QHBoxLayout, QGridLayout, QSpinBox) +from PyQt5.QtGui import QColor +from PyQt5.QtCore import Qt +# 尝试导入 QtWebEngineWidgets,如果失败则设置为 None +try: + from PyQt5.QtWebEngineWidgets import QWebEngineView + WEB_ENGINE_AVAILABLE = True +except ImportError: + QWebEngineView = None + WEB_ENGINE_AVAILABLE = False + print("⚠️ QtWebEngineWidgets 不可用,Cesium 集成功能将被禁用") + + # def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=1.0): + # from panda3d.core import CardMaker, Material, LColor,TransparencyAttrib + # + # # 参数类型检查和转换 + # if isinstance(size, (list, tuple)): + # if len(size) >= 2: + # x_size, y_size = float(size[0]), float(size[1]) + # else: + # x_size = y_size = float(size[0]) if size else 1.0 + # else: + # x_size = y_size = float(size) + # + # # 创建卡片 + # cm = CardMaker('gui_3d_image') + # cm.setFrame(-x_size/2, x_size/2, -y_size/2, y_size/2) + # + # # 创建3D图像节点 + # image_node = self.world.render.attachNewNode(cm.generate()) + # image_node.setPos(*pos) + # + # # 为3D图像创建独立的材质 + # material = Material(f"image-material-{len(self.gui_elements)}") + # material.setBaseColor(LColor(1, 1, 1, 1)) + # material.setDiffuse(LColor(1, 1, 1, 1)) + # material.setAmbient(LColor(0.5, 0.5, 0.5, 1)) + # material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0)) + # material.setShininess(10.0) + # material.setEmission(LColor(0, 0, 0, 1)) # 无自发光 + # image_node.setMaterial(material, 1) + # + # image_node.setTransparency(TransparencyAttrib.MAlpha) + # + # # 如果提供了图像路径,则加载纹理 + # if image_path: + # self.update3DImageTexture(image_node, image_path) + # + # # 应用PBR效果(如果可用) + # try: + # if hasattr(self, 'render_pipeline') and self.render_pipeline: + # self.render_pipeline.set_effect( + # image_node, + # "effects/default.yaml", + # { + # "normal_mapping": True, + # "render_gbuffer": True, + # "alpha_testing": False, + # "parallax_mapping": False, + # "render_shadow": False, + # "render_envmap": True, + # "disable_children_effects": True + # }, + # 50 + # ) + # print("✓ GUI 3D图像PBR效果已应用") + # except Exception as e: + # print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}") + # + # # 为GUI元素添加标识(效仿3D文本方法) + # image_node.setTag("gui_type", "3d_image") + # image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}") + # image_node.setTag("is_scene_element", "1") + # image_node.setTag("tree_item_type", "GUI_3DIMAGE") + # if image_path: + # image_node.setTag("gui_image_path", image_path) + # image_node.setTag("is_gui_element", "1") + # + # self.gui_elements.append(image_node) + # + # # 更新场景树 + # if hasattr(self.world, 'updateSceneTree'): + # self.world.updateSceneTree() + # + # print(f"✓ 3D图像创建完成: {image_path or '无纹理'} (世界位置: {pos})") + # return image_node + +class GUIManager: + """GUI元素管理系统类""" + + def __init__(self, world): + """初始化GUI管理系统 + + Args: + world: 核心世界对象引用 + """ + self.world = world + + # GUI元素列表 + self.gui_elements = [] + + #光源列表 + self.light_elements = [] + + # GUI编辑模式状态 + self.guiEditMode = False + self.guiEditPanel = None + self.guiPreviewWindow = None + self.currentGUITool = None + + print("✓ GUI管理系统初始化完成") + + # ==================== GUI元素创建方法 ==================== + + def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=(0.1,0.1,0.1)): + """创建2D GUI按钮 - 支持多选创建和GUI父子关系,优化版本""" + try: + from direct.gui.DirectGui import DirectButton + from PyQt5.QtCore import Qt + + print(f"🔘 开始创建GUI按钮,位置: {pos}, 文本: {text}, 尺寸: {size}") + + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + # 使用CustomTreeWidget的方法获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_gui_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None + + created_buttons = [] + + # 为每个有效的父节点创建GUI按钮 + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + button_name = f"GUIButton_{len(self.gui_elements)}" + + # 使用CustomTreeWidget的方法判断父节点类型并设置相应的挂载方式 + if tree_widget.is_gui_element(parent_node): + # 父节点是GUI元素 - 作为子GUI挂载 + gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node) + parent_gui_node = parent_node # 直接挂载到GUI元素 + parent_scale=parent_node.getScale() + relative_scale = ( + size/parent_scale[0] if parent_scale[0]!=0 else size, + size/parent_scale[1] if parent_scale[1]!=0 else size, + size/parent_scale[2] if parent_scale[2]!=0 else size + ) + else: + # 父节点是普通3D节点 - 使用屏幕坐标 + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + parent_gui_node = None # 使用默认的aspect2d + relative_scale = size + print(f"📎 挂载到3D父节点: {parent_item.text(0)}") + + button = DirectButton( + text=text, + pos=gui_pos, + scale=relative_scale, + command=self.onGUIButtonClick, + extraArgs=[f"button_{len(self.gui_elements)}"], + frameColor=(0.2, 0.6, 0.8, 1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None, + rolloverSound=None, + clickSound=None, + parent=parent_gui_node + ) + + if not hasattr(button,'_tags'): + button._tags = {} + + # button._tags["gui_type"] = "button" + # button._tags["gui_id"] = f"button_{len(self.gui_elements)}" + # button._tags["gui_text"] = text + # button._tags["is_gui_element"] = "1" + # button._tags["is_scene_element"] = "1" + # button._tags["saved_gui_type"] = "button" + # button._tags["gui_element_type"] = "button" + # button._tags["created_by_user"] = "1" + # button._tags["name"] = button_name + # button.setName(button_name) + + # 设置节点标签 + button.setTag("gui_type", "button") + button.setTag("gui_id", f"button_{len(self.gui_elements)}") + button.setTag("gui_text", text) + button.setTag("is_gui_element", "1") + button.setTag("is_scene_element", "1") # 确保这个标签被设置 + button.setTag("tree_item_type", "GUI_BUTTON") + button.setTag("saved_gui_type", "button") # 添加这个标签以确保兼容性 + button.setTag("gui_element_type", "button") + button.setTag("created_by_user", "1") + button.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") + button.setTag("name", button_name) + button.setName(button_name) + + # 如果有GUI父节点,建立引用关系 + if parent_gui_node: + parent_id = parent_gui_node.getTag("gui_id") if hasattr(parent_gui_node, 'getTag') else "" + button.setTag("gui_parent_id", parent_id) + + # 添加到GUI元素列表 + self.gui_elements.append(button) + button.reparentTo(self.world.aspect2d) + + print(f"✅ 为 {parent_item.text(0)} 创建GUI按钮成功: {button_name}") + + # 使用CustomTreeWidget的方法在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(button, parent_item, "GUI_BUTTON") + if qt_item: + created_buttons.append((button, qt_item)) + else: + created_buttons.append((button, None)) + print("⚠️ Qt树节点添加失败,但GUI对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建GUI按钮失败: {str(e)}") + continue + + # 处理创建结果 + if not created_buttons: + print("❌ 没有成功创建任何GUI按钮") + return None + + # 选中最后创建的按钮并更新场景树 + # if created_buttons: + # last_button, last_qt_item = created_buttons[-1] + # if last_qt_item: + # tree_widget.setCurrentItem(last_qt_item) + # tree_widget.update_selection_and_properties(last_button, last_qt_item) + + print(f"🎉 总共创建了 {len(created_buttons)} 个GUI按钮") + + # 返回值处理 + if len(created_buttons) == 1: + return created_buttons[0][0] + else: + return [button for button, _ in created_buttons] + + except Exception as e: + print(f"❌ 创建GUI按钮过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08): + """创建2D GUI标签 - 支持多选创建和GUI父子关系,优化版本""" + try: + from direct.gui.DirectGui import DirectLabel + from PyQt5.QtCore import Qt + + print(f"🏷️ 开始创建GUI标签,位置: {pos}, 文本: {text}, 尺寸: {size}") + + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + # 使用CustomTreeWidget的方法获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_gui_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None + + created_labels = [] + + # 为每个有效的父节点创建GUI标签 + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + label_name = f"GUILabel_{len(self.gui_elements)}" + + # 使用CustomTreeWidget的方法判断父节点类型并设置相应的挂载方式 + if tree_widget.is_gui_element(parent_node): + # 父节点是GUI元素 - 作为子GUI挂载 + gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node) + parent_gui_node = parent_node + + parent_scale = parent_node.getScale() + relative_scale = ( + size/parent_scale[0] if parent_scale[0]!= 0 else size, + size/parent_scale[1] if parent_scale[1]!= 0 else size, + size/parent_scale[2] if parent_scale[2]!= 0 else size + ) + else: + # 父节点是普通3D节点 - 使用屏幕坐标 + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + parent_gui_node = None + relative_scale = size + label = DirectLabel( + text=text, + pos=gui_pos, + scale=relative_scale, + frameColor=(0, 0, 0, 0), # 透明背景 + text_fg=(1, 1, 1, 1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None, + parent=parent_gui_node # 设置GUI父节点 + ) + + # 设置节点标签 + label.setTag("gui_type", "label") + label.setTag("gui_id", f"label_{len(self.gui_elements)}") + label.setTag("gui_text", text) + label.setTag("is_gui_element", "1") + label.setTag("tree_item_type", "GUI_LABEL") + label.setTag("is_scene_element", "1") + label.setTag("created_by_user", "1") + label.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") + label.setTag("name",label_name) + label.setName(label_name) + + # 如果有GUI父节点,建立引用关系 + if parent_gui_node: + parent_id = parent_gui_node.getTag("gui_id") if hasattr(parent_gui_node, 'getTag') else "" + label.setTag("gui_parent_id", parent_id) + + # 添加到GUI元素列表 + self.gui_elements.append(label) + + print(f"✅ 为 {parent_item.text(0)} 创建GUI标签成功: {label_name}") + + # 使用CustomTreeWidget的方法在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(label, parent_item, "GUI_LABEL") + if qt_item: + created_labels.append((label, qt_item)) + else: + created_labels.append((label, None)) + print("⚠️ Qt树节点添加失败,但GUI对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建GUI标签失败: {str(e)}") + continue + + # 处理创建结果 + if not created_labels: + print("❌ 没有成功创建任何GUI标签") + return None + + # 选中最后创建的标签并更新场景树 + # if created_labels: + # last_label, last_qt_item = created_labels[-1] + # if last_qt_item: + # tree_widget.setCurrentItem(last_qt_item) + # tree_widget.update_selection_and_properties(last_label, last_qt_item) + + print(f"🎉 总共创建了 {len(created_labels)} 个GUI标签") + + # 返回值处理 + if len(created_labels) == 1: + return created_labels[0][0] + else: + return [label for label, _ in created_labels] + + except Exception as e: + print(f"❌ 创建GUI标签过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08): + """创建2D GUI文本输入框 - 支持多选创建和GUI父子关系,优化版本""" + try: + from direct.gui.DirectGui import DirectEntry + from PyQt5.QtCore import Qt + + print(f"📝 开始创建GUI输入框,位置: {pos}, 占位符: {placeholder}, 尺寸: {size}") + + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + # 使用CustomTreeWidget的方法获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_gui_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None + + created_entries = [] + + # 为每个有效的父节点创建GUI输入框 + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + entry_name = f"GUIEntry_{len(self.gui_elements)}" + + # 使用CustomTreeWidget的方法判断父节点类型并设置相应的挂载方式 + if tree_widget.is_gui_element(parent_node): + # 父节点是GUI元素 - 作为子GUI挂载 + gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node) + parent_gui_node = parent_node + print(f"📎 挂载到GUI父节点: {parent_node.getName()}") + else: + # 父节点是普通3D节点 - 使用屏幕坐标 + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + parent_gui_node = None + print(f"📎 挂载到3D父节点: {parent_item.text(0)}") + + font = None + if hasattr(self.world,'getChineseFont'): + font = self.world.getChineseFont() + + entry = DirectEntry( + text="", + pos=(pos[0],pos[1],pos[2]), + scale=size, + command=self.onGUIEntrySubmit, + extraArgs=[f"entry_{len(self.gui_elements)}"], + initialText=placeholder, + numLines=1, + width=12, + focus=0, + frameColor = (0,0,0,0), + text_fg=(1,1,1,1), + text_align=TextNode.ACenter, + text_wordwrap=None, + rolloverSound=None, + clickSound=None, + parent=parent_gui_node, # 设置GUI父节点 + text_font = font, + frameSize=(-0.1,0.1,-0.05,0.05) + ) + + # 设置节点标签 + entry.setTag("gui_type", "entry") + entry.setTag("gui_id", f"entry_{len(self.gui_elements)}") + entry.setTag("gui_placeholder", placeholder) + entry.setTag("is_gui_element", "1") + entry.setTag("tree_item_type", "GUI_ENTRY") + entry.setTag("is_scene_element", "1") + entry.setTag("created_by_user", "1") + entry.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") + entry.setTag("name",entry_name) + entry.setName(entry_name) + + # 如果有GUI父节点,建立引用关系 + if parent_gui_node: + parent_id = parent_gui_node.getTag("gui_id") if hasattr(parent_gui_node, 'getTag') else "" + entry.setTag("gui_parent_id", parent_id) + + # 添加到GUI元素列表 + self.gui_elements.append(entry) + + print(f"✅ 为 {parent_item.text(0)} 创建GUI输入框成功: {entry_name}") + + # 使用CustomTreeWidget的方法在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(entry, parent_item, "GUI_ENTRY") + if qt_item: + created_entries.append((entry, qt_item)) + else: + created_entries.append((entry, None)) + print("⚠️ Qt树节点添加失败,但GUI对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建GUI输入框失败: {str(e)}") + continue + + # 处理创建结果 + if not created_entries: + print("❌ 没有成功创建任何GUI输入框") + return None + + # 选中最后创建的输入框并更新场景树 + # if created_entries: + # last_entry, last_qt_item = created_entries[-1] + # if last_qt_item: + # tree_widget.setCurrentItem(last_qt_item) + # tree_widget.update_selection_and_properties(last_entry, last_qt_item) + + print(f"🎉 总共创建了 {len(created_entries)} 个GUI输入框") + + # 返回值处理 + if len(created_entries) == 1: + return created_entries[0][0] + else: + return [entry for entry, _ in created_entries] + + except Exception as e: + print(f"❌ 创建GUI输入框过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=(1,1,1)): + """创建2D GUI图片""" + try: + from direct.gui.DirectGui import DirectButton + from PyQt5.QtCore import Qt + + print(f"🔘 开始创建GUI按钮,位置: {pos}, 图片路径: {image_path}, 尺寸: {size}") + + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + # 使用CustomTreeWidget的方法获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_gui_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None + + created_2dimage = [] + + # 为每个有效的父节点创建GUI按钮 + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + image_name = f"GUIImage_{len(self.gui_elements)}" + + # 使用CustomTreeWidget的方法判断父节点类型并设置相应的挂载方式 + if tree_widget.is_gui_element(parent_node): + # 父节点是GUI元素 - 作为子GUI挂载 + gui_pos = tree_widget.calculate_relative_gui_position(pos, parent_node) + parent_gui_node = parent_node # 直接挂载到GUI元素 + print(f"📎 挂载到GUI父节点: {parent_node.getName()}") + else: + # 父节点是普通3D节点 - 使用屏幕坐标 + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + parent_gui_node = None # 使用默认的aspect2d + print(f"📎 挂载到3D父节点: {parent_item.text(0)}") + + if isinstance(size, (list, tuple)) and len(size) >= 2: + # 分别处理宽度和高度的缩放 + width_scale = size[0] * 0.25 + height_scale = size[2] * 0.25 + else: + # 如果只提供了一个缩放值,则使用相同值 + width_scale = size * 0.1 if isinstance(size, (int, float)) else 0.2 + height_scale = width_scale + + cm = CardMaker("gui-2d-image") + cm.setFrame(-width_scale, width_scale, -height_scale, height_scale) + + # image_node = self.world.aspect2d.attachNewNode(cm.generate()) + if parent_gui_node: + image_node = parent_gui_node.attachNewNode(cm.generate()) + else: + image_node = self.world.aspect2d.attachNewNode(cm.generate()) + image_node.setPos(gui_pos) + image_node.setBin('fixed', 0) + image_node.setDepthWrite(False) + image_node.setDepthTest(False) + image_node.setColor(1, 1, 1, 1) + + # 设置透明度支持 + image_node.setTransparency(TransparencyAttrib.MAlpha) + + # 如果提供了图像路径,则加载纹理 + if image_path: + try: + image_node.setTag("image_path", image_path) + texture = self.world.loader.loadTexture(image_path) + if texture: + image_node.setTexture(texture, 1) + texture.setWrapU(Texture.WM_clamp) + texture.setWrapV(Texture.WM_clamp) + texture.setMinfilter(Texture.FT_linear) + texture.setMagfilter(Texture.FT_linear) + image_node.setColor(1, 1, 1, 1) + else: + print(f"⚠️ 无法加载2D图片纹理: {image_path}") + except Exception as e: + print(f"❌ 加载2D图片纹理失败: {e}") + + # 设置节点标签 + image_node.setTag("gui_type", "2d_image") + image_node.setTag("gui_id", f"2d_image_{len(self.gui_elements)}") + image_node.setTag("gui_text", f"2D图片_{len(self.gui_elements)}") + image_node.setTag("is_gui_element", "1") + image_node.setTag("is_scene_element", "1") + image_node.setTag("tree_item_type", "GUI_IMAGE") + image_node.setTag("created_by_user", "1") + image_node.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") + image_node.setTag("name",image_name) + image_node.setName(image_name) + + # 如果有GUI父节点,建立引用关系 + if parent_gui_node: + parent_id = parent_gui_node.getTag("gui_id") if hasattr(parent_gui_node, 'getTag') else "" + image_node.setTag("gui_parent_id", parent_id) + + # 添加到GUI元素列表 + self.gui_elements.append(image_node) + + print(f"✅ 为 {parent_item.text(0)} 创建GUI按钮成功: {image_name}") + + # 使用CustomTreeWidget的方法在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(image_node, parent_item, "GUI_IMAGE") + if qt_item: + created_2dimage.append((image_node, qt_item)) + else: + created_2dimage.append((image_node, None)) + print("⚠️ Qt树节点添加失败,但GUI对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建GUI按钮失败: {str(e)}") + continue + + # 处理创建结果 + if not created_2dimage: + print("❌ 没有成功创建任何GUI按钮") + return None + + # 选中最后创建的按钮并更新场景树 + # if created_2dimage: + # last_button, last_qt_item = created_2dimage[-1] + # if last_qt_item: + # tree_widget.setCurrentItem(last_qt_item) + # tree_widget.update_selection_and_properties(last_button, last_qt_item) + + print(f"🎉 总共创建了 {len(created_2dimage)} 个GUI按钮") + + # 返回值处理 + if len(created_2dimage) == 1: + return created_2dimage[0][0] + else: + return [button for button, _ in created_2dimage] + + except Exception as e: + print(f"❌ 创建GUI按钮过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def constrain2DPosition(self,gui_element,new_x=None,new_z=None): + """限制2dGUI元素位置在屏幕范围内""" + try: + from panda3d.core import Vec3 + + bounds = gui_element.getTightBounds() + element_width=0 + element_height=0 + + if bounds: + min_point,max_point = bounds + element_width = (max_point.getX() - min_point.getX())/2 + element_height = (max_point.getZ()-min_point.getZ())/2 + + #获取当前缩放 + scale = gui_element.getScale() + if hasattr(scale,'getX'): + scale_x = scale.getX() + scale_z = scale.getZ() if hasattr(scale,'getZ') else scale_x + else: + scale_x = scale_z = scale if isinstance(scale,(int,float)) else 1.0 + + actual_width = element_width * scale_x + actual_height = element_height * scale_z + + screen_width = 1.9 + screen_height = 0.9 + + min_x = -screen_width + actual_width + max_x = screen_width - actual_width + min_z = -screen_height + actual_height + max_z = screen_height - actual_height + + #获取当前位置 + current_pos = gui_element.getPos() + x = new_x if new_x is not None else current_pos.getX() + z = new_z if new_z is not None else current_pos.getZ() + + #应用边界限制 + x = max(min_x,min(max_x,x)) + z = max(min_z,min(max_z,z)) + + return Vec3(x,current_pos.getY(),z) + except Exception as e: + print(f"约束2D位置时出错: {e}") + # 出错时返回原始值 + current_pos = gui_element.getPos() + x = new_x if new_x is not None else current_pos.getX() + z = new_z if new_z is not None else current_pos.getZ() + return Vec3(x, current_pos.getY(), z) + + def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): + """创建3D空间文本 - 支持多选创建,优化版本""" + try: + from panda3d.core import TextNode + from PyQt5.QtCore import Qt + + print(f"📄 开始创建3D文本,位置: {pos}, 文本: {text}, 尺寸: {size}") + + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + # 获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None + + created_texts = [] + + # 为每个有效的父节点创建3D文本 + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + text_name = f"GUI3DText_{len(self.gui_elements)}" + + textNode = TextNode(f'3d-text-{len(self.gui_elements)}') + textNode.setText(text) + textNode.setAlign(TextNode.ACenter) + if self.world.getChineseFont(): + textNode.setFont(self.world.getChineseFont()) + + # 挂载到选中的父节点 + textNodePath = parent_node.attachNewNode(textNode) + textNodePath.setPos(*pos) + textNodePath.setScale(size) + textNodePath.setColor(1, 1, 0, 1) + #textNodePath.setBillboardAxis() # 让文本总是面向相机 + textNodePath.setName(text_name) + + # 设置节点标签 + textNodePath.setTag("gui_type", "3d_text") + textNodePath.setTag("gui_id", f"3d_text_{len(self.gui_elements)}") + textNodePath.setTag("gui_text", text) + textNodePath.setTag("is_gui_element", "1") + textNodePath.setTag("is_scene_element", "1") + textNodePath.setTag("tree_item_type", "GUI_3DTEXT") + textNodePath.setTag("created_by_user", "1") + textNodePath.setTag("name", text_name) + + # 添加到GUI元素列表 + self.gui_elements.append(textNodePath) + + print(f"✅ 为 {parent_item.text(0)} 创建3D文本成功: {text_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(textNodePath, parent_item, "GUI_3DTEXT") + if qt_item: + created_texts.append((textNodePath, qt_item)) + else: + created_texts.append((textNodePath, None)) + print("⚠️ Qt树节点添加失败,但GUI对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建3D文本失败: {str(e)}") + continue + + # 处理创建结果 + if not created_texts: + print("❌ 没有成功创建任何3D文本") + return None + + # 选中最后创建的文本并更新场景树 + # if created_texts: + # last_text, last_qt_item = created_texts[-1] + # if last_qt_item: + # tree_widget.setCurrentItem(last_qt_item) + # tree_widget.update_selection_and_properties(last_text, last_qt_item) + + print(f"🎉 总共创建了 {len(created_texts)} 个3D文本") + + # 返回值处理 + if len(created_texts) == 1: + return created_texts[0][0] + else: + return [text_np for text_np, _ in created_texts] + + except Exception as e: + print(f"❌ 创建3D文本过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def createGUI3DImage(self, pos=(0, 0, 0), image_path=None, size=(1.0,1.0,1.0)): + """创建3D空间图片""" + try: + + from panda3d.core import TextNode + from PyQt5.QtCore import Qt + + print(f"📄 开始创建3D文本,位置: {pos}, 3D图片位置: {image_path}, 尺寸: {size}") + + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + # 获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None + + created_3dimage = [] + + # 为每个有效的父节点创建3D文本 + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + image_name = f"GUI3DImage_{len(self.gui_elements)}" + + # 参数类型检查和转换 + if isinstance(size, (list, tuple)): + if len(size) >= 2: + x_size, y_size = float(size[0]), float(size[2]) + else: + x_size = y_size = float(size[0]) if size else 1.0 + else: + x_size = y_size = float(size) + + # 创建卡片 + cm = CardMaker('gui_3d_image') + cm.setFrame(-x_size, x_size, -y_size, y_size) + + # 创建3D图像节点 + # image_node = self.world.render.attachNewNode(cm.generate()) + image_node = parent_node.attachNewNode(cm.generate()) + image_node.setPos(*pos) + + # 为3D图像创建独立的材质 + material = Material(f"image-material-{len(self.gui_elements)}") + material.setBaseColor(LColor(1, 1, 1, 1)) + material.setDiffuse(LColor(1, 1, 1, 1)) + material.setAmbient(LColor(0.5, 0.5, 0.5, 1)) + material.setSpecular(LColor(0.1, 0.1, 0.1, 1.0)) + material.setShininess(10.0) + material.setEmission(LColor(0, 0, 0, 1)) # 无自发光 + image_node.setMaterial(material, 1) + + image_node.setTransparency(TransparencyAttrib.MAlpha) + + # 如果提供了图像路径,则加载纹理 + if image_path: + self.update3DImageTexture(image_node, image_path) + + # 应用PBR效果(如果可用) + try: + if hasattr(self, 'render_pipeline') and self.render_pipeline: + self.render_pipeline.set_effect( + image_node, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": False, + "render_envmap": True, + "disable_children_effects": True + }, + 50 + ) + print("✓ GUI 3D图像PBR效果已应用") + except Exception as e: + print(f"⚠️ GUI 3D图像PBR效果应用失败: {e}") + image_node.setName(image_name) + + # 设置节点标签 + image_node.setTag("gui_type", "3d_image") + image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}") + if image_path: + image_node.setTag("image_path", image_path) + image_node.setTag("is_gui_element", "1") + image_node.setTag("is_scene_element", "1") + image_node.setTag("tree_item_type", "GUI_3DIMAGE") + image_node.setTag("created_by_user", "1") + image_node.setTag("name",image_name) + + # 添加到GUI元素列表 + self.gui_elements.append(image_node) + + print(f"✅ 为 {parent_item.text(0)} 创建3D文本成功: {image_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(image_node, parent_item, "GUI_3DIMAGE") + if qt_item: + created_3dimage.append((image_node, qt_item)) + else: + created_3dimage.append((image_node, None)) + print("⚠️ Qt树节点添加失败,但GUI对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建3D文本失败: {str(e)}") + continue + + # 处理创建结果 + if not created_3dimage: + print("❌ 没有成功创建任何3D文本") + return None + + # 选中最后创建的文本并更新场景树 + # if created_3dimage: + # last_image, last_qt_item = created_3dimage[-1] + # if last_qt_item: + # tree_widget.setCurrentItem(last_qt_item) + # tree_widget.update_selection_and_properties(last_image, last_qt_item) + + print(f"🎉 总共创建了 {len(created_3dimage)} 个3D文本") + + # 返回值处理 + if len(created_3dimage) == 1: + return created_3dimage[0][0] + else: + return [text_np for text_np, _ in created_3dimage] + + except Exception as e: + print(f"❌ 创建3D文本过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def createVideoScreen(self, pos=(0, 0, 0), size=1, video_path=None): + """创建3D视频播放屏幕 - 添加占位符纹理支持""" + try: + from panda3d.core import CardMaker, TransparencyAttrib, Texture, TextureStage + import os + + # 确保 pos 是有效的三维坐标元组 + if not isinstance(pos, (tuple, list)) or len(pos) != 3: + print(f"⚠️ 位置参数无效,使用默认值 (0, 0, 0),原始值: {pos}") + pos = (0, 0, 0) + else: + # 确保所有坐标都是数值类型 + try: + pos = (float(pos[0]), float(pos[1]), float(pos[2])) + except (ValueError, TypeError): + print(f"⚠️ 位置参数包含非数值,使用默认值 (0, 0, 0),原始值: {pos}") + pos = (0, 0, 0) + + # 确保 size 是有效数值 + try: + size = float(size) + except (ValueError, TypeError): + print(f"⚠️ 尺寸参数无效,使用默认值 0.2,原始值: {size}") + size = 0.2*5 + + print(f"📺 开始创建视频屏幕,位置: {pos}, 尺寸: {size}, 视频路径: {video_path}") + + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + # 获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None + + created_videoscreens = [] + + # 为每个有效的父节点创建视频屏幕 + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + screen_name = f"VideoScreen_{len(self.gui_elements)}" + + # 使用CardMaker创建视频屏幕框架 + cm = CardMaker('video-screen') + cm.setFrame(-size, size, -size, size) + + # 创建挂载节点 - 挂载到选中的父节点 + video_screen = parent_node.attachNewNode(cm.generate()) + video_screen.setPos(*pos) + video_screen.setName(screen_name) + video_screen.setBin('fixed', 10) + + # 设置透明度支持 + video_screen.setTransparency(TransparencyAttrib.MAlpha) + + # 设置初始颜色为白色,确保纹理能正确显示 + video_screen.setColor(1, 1, 1, 1) + + # 确保视频屏幕有正确的材质 + self._ensureVideoScreenMaterial(video_screen) + + # 设置节点标签 + video_screen.setTag("gui_type", "video_screen") + video_screen.setTag("gui_id", f"video_screen_{len(self.gui_elements)}") + video_screen.setTag("gui_text", f"视频屏幕_{len(self.gui_elements)}") + video_screen.setTag("is_gui_element", "1") + video_screen.setTag("is_scene_element", "1") + video_screen.setTag("tree_item_type", "GUI_VIDEO_SCREEN") + video_screen.setTag("created_by_user", "1") + video_screen.setTag("name",screen_name) + + # 设置视频路径标签 + if video_path and os.path.exists(video_path): + video_screen.setTag("video_path", video_path) + else: + video_screen.setTag("video_path", "") + + # 关键修改:预先创建一个占位符纹理,为后续视频播放做准备 + placeholder_texture = Texture(f"placeholder_video_texture_3d_{len(self.gui_elements)}") + placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) + placeholder_data = b'\x19\x19\x4c' # 深蓝色占位符颜色 (25, 25, 76) + placeholder_texture.setRamImage(placeholder_data) + + # 创建纹理阶段并应用占位符纹理到视频屏幕 + texture_stage = TextureStage("video_placeholder") + texture_stage.setSort(0) + texture_stage.setMode(TextureStage.MModulate) + video_screen.setTexture(texture_stage, placeholder_texture) + + # 保存占位符纹理引用 + video_screen.setPythonTag("placeholder_texture", placeholder_texture) + + print(f"🔧 为3D视频屏幕创建了占位符纹理环境: {screen_name}") + + # 如果提供了视频路径,则加载视频纹理 + movie_texture = None + + if video_path: + if video_path.startswith(("http://","https://")): + try: + if hasattr(self.world,'property_panel'): + success = self.world.property_panel._loadVideoFromURLWithOpenCV_3D(video_screen,video_path) + if success: + print(f"✅ 视频流URL加载成功: {video_path}") + else: + print(f"⚠️ 视频流URL加载失败: {video_path}") + else: + print("⚠️ property_manager不可用,无法加载视频流") + except Exception as e: + print(f"❌ 加载视频流URL失败: {e}") + import traceback + traceback.print_exc() + elif os.path.exists(video_path): + # 对于本地视频文件,使用原有方法 + try: + print(f"🔍 尝试加载2D视频纹理: {video_path}") + # 加载视频纹理 + movie_texture = self._loadMovieTexture(video_path) + if movie_texture: + # 应用纹理到视频屏幕(替换占位符) + video_screen["frameTexture"] = movie_texture + print(f"✅ 2D视频纹理加载成功: {video_path}") + + # 保存视频纹理引用以便后续控制 + video_screen.setPythonTag("movie_texture", movie_texture) + + # 尝试自动播放视频(如果支持) + try: + if hasattr(movie_texture, 'play'): + movie_texture.play() + print("▶️ 2D视频已开始播放") + except Exception as play_error: + print(f"⚠️ 2D视频自动播放失败: {play_error}") + else: + print(f"⚠️ 无法加载2D视频纹理: {video_path}") + except Exception as e: + print(f"❌ 加载2D视频纹理失败: {e}") + import traceback + traceback.print_exc() + else: + print(f"⚠️ 2D视频文件不存在: {video_path}") + + # if video_path and os.path.exists(video_path): + # try: + # print(f"🔍 尝试加载视频纹理: {video_path}") + # # 加载视频纹理 + # movie_texture = self._loadMovieTexture(video_path) + # if movie_texture: + # # 创建纹理阶段 + # texture_stage = TextureStage("video") + # texture_stage.setSort(0) + # texture_stage.setMode(TextureStage.MModulate) + # video_screen.setTexture(texture_stage, movie_texture) + # + # print(f"✅ 视频纹理加载成功: {video_path}") + # + # # 保存视频纹理引用以便后续控制 + # video_screen.setPythonTag("movie_texture", movie_texture) + # + # # 尝试自动播放视频(如果支持) + # try: + # if hasattr(movie_texture, 'play'): + # movie_texture.play() + # print("▶️ 视频已开始播放") + # except Exception as play_error: + # print(f"⚠️ 视频自动播放失败: {play_error}") + # else: + # print(f"⚠️ 无法加载视频纹理: {video_path}") + # # 使用默认颜色作为占位符 + # video_screen.setColor(0.1, 0.1, 0.3, 0.8) + # except Exception as e: + # print(f"❌ 加载视频纹理失败: {e}") + # import traceback + # traceback.print_exc() + # # 使用默认颜色作为占位符 + # video_screen.setColor(0.1, 0.1, 0.3, 0.8) + # else: + # # 没有视频文件时显示默认颜色 + # video_screen.setColor(0.1, 0.1, 0.3, 0.8) + # if video_path: + # print(f"⚠️ 视频文件不存在: {video_path}") + # else: + # print("ℹ️ 未提供视频文件,显示默认占位符") + # + # # 保存视频纹理引用以便后续控制 + # if movie_texture: + # video_screen.setPythonTag("movie_texture", movie_texture) + + # 添加到GUI元素列表 + self.gui_elements.append(video_screen) + + print(f"✅ 为 {parent_item.text(0)} 创建视频屏幕成功: {screen_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(video_screen, parent_item, "GUI_VIDEO_SCREEN") + if qt_item: + created_videoscreens.append((video_screen, qt_item)) + else: + created_videoscreens.append((video_screen, None)) + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建视频屏幕失败: {str(e)}") + import traceback + traceback.print_exc() + continue + + # 处理创建结果 + if not created_videoscreens: + print("❌ 没有成功创建任何视频屏幕") + return None + + # 选中最后创建的视频屏幕 + # if created_videoscreens: + # last_screen_np, last_qt_item = created_videoscreens[-1] + # if last_qt_item: + # tree_widget.setCurrentItem(last_qt_item) + # # 更新选择和属性面板 + # tree_widget.update_selection_and_properties(last_screen_np, last_qt_item) + + print(f"🎉 总共创建了 {len(created_videoscreens)} 个视频屏幕") + + # 返回值处理 + if len(created_videoscreens) == 1: + return created_videoscreens[0][0] # 单个屏幕返回NodePath + else: + return [screen_np for screen_np, _ in created_videoscreens] # 多个屏幕返回列表 + + except Exception as e: + print(f"❌ 创建视频屏幕过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def _ensureVideoScreenMaterial(self, video_screen): + """确保视频屏幕有正确的材质设置""" + try: + from panda3d.core import Material, LColor + + # 如果还没有材质,则创建一个 + if not video_screen.hasMaterial(): + material = Material(f"video-material-{video_screen.getName()}") + material.setBaseColor(LColor(1, 1, 1, 1)) + material.setDiffuse(LColor(1, 1, 1, 1)) + material.setAmbient(LColor(1, 1, 1, 1)) # 确保环境光为白色 + material.setEmission(LColor(0, 0, 0, 1)) + material.setSpecular(LColor(0, 0, 0, 1)) + material.setShininess(0) + video_screen.setMaterial(material, 1) + print(f"✅ 为视频屏幕创建了新材质: {video_screen.getName()}") + else: + # 更新现有材质确保正确设置 + material = video_screen.getMaterial() + material.setBaseColor(LColor(1, 1, 1, 1)) + material.setAmbient(LColor(1, 1, 1, 1)) # 确保环境光为白色 + video_screen.setMaterial(material, 1) + print(f"✅ 更新了视频屏幕材质: {video_screen.getName()}") + + except Exception as e: + print(f"⚠️ 设置视频屏幕材质时出错: {e}") + + def _debugVideoScreenTextures(self, video_screen): + """调试视频屏幕的纹理状态""" + try: + print(f"调试视频屏幕 {video_screen.getName()}:") + + # 检查PythonTag + movie_texture = video_screen.getPythonTag("movie_texture") + if movie_texture: + print(f" - PythonTag movie_texture: {type(movie_texture)}") + if hasattr(movie_texture, 'is_playable'): + print(f" - is_playable: {movie_texture.is_playable()}") + else: + print(" - PythonTag movie_texture: None") + + # 检查所有纹理阶段 + texture_stages = video_screen.findAllTextureStages() + print(f" - 纹理阶段数: {texture_stages.getNumStages()}") + for i in range(texture_stages.getNumStages()): + stage = texture_stages.getStage(i) + texture = video_screen.getTexture(stage) + print(f" - 阶段 {i}: {stage.getName()}, 纹理: {texture.getName() if texture else 'None'}") + + except Exception as e: + print(f"调试视频屏幕纹理时出错: {e}") + + def playVideo(self, video_screen): + """播放视频 - 改进版本,支持从暂停处继续播放""" + try: + # 获取视频纹理 + movie_texture = self._getMovieTextureFromScreen(video_screen) + + if movie_texture: + # 检查是否有播放方法 + if hasattr(movie_texture, 'play'): + try: + self.loadVideoFile(video_screen, video_screen.getTag("video_path")) + movie_texture.play() + print(f"▶️ 继续播放视频: {video_screen.getName()}") + return True + except Exception as play_error: + print(f"⚠️ 播放视频时出错: {play_error}") + return False + else: + print(f"⚠️ 纹理对象没有播放方法: {video_screen.getName()}") + return False + else: + self.loadVideoFile(video_screen,video_screen.getTag("video_path")) + except Exception as e: + print(f"❌ 播放视频失败: {e}") + import traceback + traceback.print_exc() + return False + + def _getMovieTextureFromScreen(self, video_screen): + """从视频屏幕获取视频纹理""" + try: + # 方法1: 从PythonTag获取 + movie_texture = video_screen.getPythonTag("movie_texture") + if movie_texture: + return movie_texture + + # 方法2: 从纹理阶段获取 + from panda3d.core import TextureStage + texture_stage = video_screen.findTextureStage("video") + if texture_stage: + movie_texture = video_screen.getTexture(texture_stage) + if movie_texture: + return movie_texture + + # 方法3: 获取第一个纹理 + if video_screen.hasTexture(): + movie_texture = video_screen.getTexture() + if movie_texture: + return movie_texture + + return None + except Exception as e: + print(f"获取视频纹理时出错: {e}") + return None + + def pauseVideo(self, video_screen): + """暂停视频""" + try: + movie_texture = self._getMovieTextureFromScreen(video_screen) + + if movie_texture: + # 检查是否有暂停方法 + if hasattr(movie_texture, 'stop'): # MovieTexture使用stop来暂停 + try: + movie_texture.stop() + print(f"⏸️ 视频已暂停: {video_screen.getName()}") + return True + except Exception as stop_error: + print(f"⚠️ 暂停视频时出错: {stop_error}") + return False + elif hasattr(movie_texture, 'set play rate'): # 某些版本支持设置播放速率 + try: + movie_texture.setPlayRate(0.0) + print(f"⏸️ 视频已暂停(播放速率设为0): {video_screen.getName()}") + return True + except Exception as rate_error: + print(f"⚠️ 设置播放速率时出错: {rate_error}") + return False + else: + print(f"⚠️ 纹理对象没有暂停方法: {video_screen.getName()}") + return False + else: + print(f"❌ 视频屏幕没有关联的视频纹理: {video_screen.getName()}") + return False + except Exception as e: + print(f"❌ 暂停视频失败: {e}") + import traceback + traceback.print_exc() + return False + + def stopVideo(self, video_screen): + """停止视频(回到开头)""" + try: + movie_texture = self._getMovieTextureFromScreen(video_screen) + + if movie_texture: + # 停止并重置到开头 + if hasattr(movie_texture, 'stop'): + try: + movie_texture.stop() + # 如果有重置方法,调用它 + if hasattr(movie_texture, 'setTime'): + movie_texture.setTime(0.0) + print(f"⏹️ 视频已停止: {video_screen.getName()}") + return True + except Exception as stop_error: + print(f"⚠️ 停止视频时出错: {stop_error}") + return False + else: + print(f"⚠️ 纹理对象没有停止方法: {video_screen.getName()}") + return False + else: + print(f"❌ 视频屏幕没有关联的视频纹理: {video_screen.getName()}") + return False + except Exception as e: + print(f"❌ 停止视频失败: {e}") + import traceback + traceback.print_exc() + return False + def setVideoTime(self, video_screen, time_seconds): + """设置视频播放时间""" + try: + movie_texture = video_screen.getPythonTag("movie_texture") + + # 备用获取方法 + if not movie_texture: + from panda3d.core import TextureStage + texture_stage = video_screen.findTextureStage("video") + if texture_stage: + movie_texture = video_screen.getTexture(texture_stage) + + if not movie_texture and video_screen.hasTexture(): + movie_texture = video_screen.getTexture() + + if movie_texture and hasattr(movie_texture, 'set_time'): + movie_texture.set_time(time_seconds) + print(f"🕒 设置视频时间 {time_seconds}s: {video_screen.getName()}") + return True + else: + print(f"❌ 视频屏幕没有关联的视频纹理: {video_screen.getName()}") + return False + except Exception as e: + print(f"❌ 设置视频时间失败: {e}") + return False + + def loadVideoFile(self, video_screen, video_path): + """为视频屏幕加载新的视频文件""" + try: + import os + + if not os.path.exists(video_path): + print(f"❌ 视频文件不存在: {video_path}") + return False + + # Convert Windows path to Panda3D compatible path format + from panda3d.core import Filename + panda_path = Filename.fromOsSpecific(video_path) + converted_path = str(panda_path) + + # 加载新的视频纹理 + movie_texture = self._loadMovieTexture(video_path) # Pass original path for existence check + if movie_texture: + # 清除现有的纹理 + video_screen.clearTexture() + + # 设置视频纹理属性 + movie_texture.setWrapU(Texture.WM_clamp) + movie_texture.setWrapV(Texture.WM_clamp) + movie_texture.setMinfilter(Texture.FT_linear) + movie_texture.setMagfilter(Texture.FT_linear) + + # 如果视频纹理支持循环播放,设置循环 + if hasattr(movie_texture, 'set_loop'): + movie_texture.set_loop(True) + + # 如果视频纹理支持播放速率控制,设置正常速率 + if hasattr(movie_texture, 'set_play_rate'): + movie_texture.set_play_rate(1.0) + + # 重要:为视频纹理创建专用的纹理阶段 + texture_stage = TextureStage("video") + texture_stage.setSort(0) # 使用第一个纹理槽 + texture_stage.setMode(TextureStage.MModulate) + + # 应用纹理到视频屏幕 + video_screen.setTexture(texture_stage, movie_texture) + + # 保存新的视频纹理引用到PythonTag + video_screen.setPythonTag("movie_texture", movie_texture) + video_screen.setTag("video_path", converted_path) # Store converted path + + # 确保视频屏幕有正确的材质 + self._ensureVideoScreenMaterial(video_screen) + + print(f"✅ 成功加载新视频: {converted_path}") + return True + else: + print(f"❌ 无法加载视频文件: {converted_path}") + return False + + except Exception as e: + print(f"❌ 加载视频文件失败: {e}") + import traceback + traceback.print_exc() + return False + + def _loadMovieTexture(self, video_path): + """加载视频纹理的兼容方法""" + try: + from panda3d.core import Texture, MovieTexture, Filename + import os + + # 检查文件是否存在 + if not os.path.exists(video_path): + print(f"❌ 视频文件不存在: {video_path}") + return None + + # Convert Windows path to Panda3D compatible path format + panda_path = Filename.fromOsSpecific(video_path) + converted_path = str(panda_path) + + print(f"🔍 尝试加载视频文件: {converted_path}") + + # 方法1: 尝试使用 MovieTexture(专门用于视频) + try: + movie_texture = MovieTexture(converted_path) + if movie_texture.read(converted_path): + print("✅ 使用 MovieTexture 成功加载视频") + self._configureVideoTexture(movie_texture) + return movie_texture + else: + print("⚠️ MovieTexture.read() 返回失败") + except Exception as e: + print(f"⚠️ MovieTexture 方法失败: {e}") + + # 方法2: 尝试使用 loader.loadTexture + try: + movie_texture = self.world.loader.loadTexture(converted_path) + if movie_texture and hasattr(movie_texture, 'is_playable') and movie_texture.is_playable(): + print("✅ 使用 loader.loadTexture 成功加载视频纹理") + self._configureVideoTexture(movie_texture) + return movie_texture + else: + print("⚠️ loader.loadTexture 加载的不是可播放的视频纹理") + except Exception as e: + print(f"⚠️ loader.loadTexture 方法失败: {e}") + + # 方法3: 尝试使用 Texture.read(作为最后备选) + try: + texture = Texture() + if texture.read(converted_path): + print("✅ 使用 Texture.read 成功加载(可能作为静态纹理)") + self._configureVideoTexture(texture) + return texture + except Exception as e: + print(f"⚠️ Texture.read 方法失败: {e}") + + print("❌ 所有视频纹理加载方法都失败") + return None + + except Exception as e: + print(f"❌ 加载视频纹理时发生未知错误: {e}") + import traceback + traceback.print_exc() + return None + + def _configureVideoTexture(self, texture): + """配置视频纹理属性""" + try: + from panda3d.core import Texture + + # 设置纹理属性 + texture.setWrapU(Texture.WM_clamp) + texture.setWrapV(Texture.WM_clamp) + texture.setMinfilter(Texture.FT_linear) + texture.setMagfilter(Texture.FT_linear) + + # 如果是可播放的视频纹理,设置播放属性 + if hasattr(texture, 'set_loop') and hasattr(texture, 'set_play_rate'): + texture.set_loop(True) + texture.set_play_rate(1.0) + + print(f"✅ 视频纹理配置完成: {texture.getName()}") + + except Exception as e: + print(f"⚠️ 配置视频纹理时出错: {e}") + + def createGUI2DVideoScreen(self, pos=(0, 0), size=0.2, video_path=None): + """创建2D视频播放屏幕 - 使用2D坐标""" + try: + from direct.gui.DirectGui import DirectFrame + from panda3d.core import TransparencyAttrib, Texture, TextureStage + from PyQt5.QtCore import Qt + import os + + # 确保 pos 是有效的二维坐标元组 + if pos is None or pos is False or not isinstance(pos, (tuple, list)) or len(pos) != 2: + print(f"⚠️ 位置参数无效,使用默认值 (0, 0),原始值: {pos}") + pos = (0, 0) + else: + # 确保所有坐标都是数值类型 + try: + pos = (float(pos[0]), float(pos[1])) + except (ValueError, TypeError, IndexError) as e: + print(f"⚠️ 位置参数包含非数值或索引错误,使用默认值 (0, 0),原始值: {pos}, 错误: {e}") + pos = (0, 0) + + # 确保 size 是有效数值 + try: + size = float(size) + except (ValueError, TypeError) as e: + print(f"⚠️ 尺寸参数无效,使用默认值 0.2,原始值: {size}, 错误: {e}") + size = 0.2 + + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + # 获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_gui_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None + + created_videoscreens = [] + + # 为每个有效的父节点创建2D视频屏幕 + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + screen_name = f"GUI2DVideoScreen_{len(self.gui_elements)}" + + # 使用DirectFrame创建2D视频屏幕 + video_screen = DirectFrame( + frameSize=(-size, size, -size, size), + frameColor=(1, 1, 1, 1), # 默认背景色 + pos=(pos[0] * 0.1, 0, pos[1] * 0.1), # 转换为屏幕坐标 + parent=parent_node if tree_widget.is_gui_element(parent_node) else self.world.aspect2d, + suppressMouse=True, + ) + + video_screen.setName(screen_name) + + # 设置透明度支持 + video_screen.setTransparency(TransparencyAttrib.MAlpha) + + #设置2D视频屏幕特有的标签 + video_screen.setTag("gui_type", "2d_video_screen") + video_screen.setTag("gui_id", f"2d_video_screen_{len(self.gui_elements)}") + video_screen.setTag("gui_text", f"2D视频屏幕_{len(self.gui_elements)}") + video_screen.setTag("is_gui_element", "1") + video_screen.setTag("is_scene_element", "1") + video_screen.setTag("tree_item_type", "GUI_2D_VIDEO_SCREEN") + video_screen.setTag("created_by_user", "1") + video_screen.setTag("name",screen_name) + video_screen.setTag("video_path",video_path or "") + + # 关键修改:预先创建一个占位符纹理,为后续视频播放做准备 + placeholder_texture = Texture(f"placeholder_video_texture_{len(self.gui_elements)}") + placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) + placeholder_data = b'\x19\x19\x4c' # 深蓝色占位符颜色 (25, 25, 76) + placeholder_texture.setRamImage(placeholder_data) + + # 应用占位符纹理到视频屏幕 + video_screen["frameTexture"] = placeholder_texture + + # 保存占位符纹理引用 + video_screen.setPythonTag("placeholder_texture", placeholder_texture) + + print(f"🔧 为2D视频屏幕创建了占位符纹理环境: {screen_name}") + + # 如果提供了视频路径,则加载视频纹理 + movie_texture = None + + if video_path: + if video_path.startswith(("http://","https://")): + try: + if hasattr(self.world,'property_panel'): + success = self.world.property_panel._loadVideoFromURLWithOpenCV(video_screen,video_path) + if success: + print(f"✅ 视频流URL加载成功: {video_path}") + else: + print(f"⚠️ 视频流URL加载失败: {video_path}") + else: + print("⚠️ property_manager不可用,无法加载视频流") + except Exception as e: + print(f"❌ 加载视频流URL失败: {e}") + import traceback + traceback.print_exc() + elif os.path.exists(video_path): + try: + print(f"🔍 尝试加载2D视频纹理: {video_path}") + # 加载视频纹理 + movie_texture = self._loadMovieTexture(video_path) + if movie_texture: + # 应用纹理到视频屏幕(替换占位符) + video_screen["frameTexture"] = movie_texture + print(f"✅ 2D视频纹理加载成功: {video_path}") + + # 保存视频纹理引用以便后续控制 + video_screen.setPythonTag("movie_texture", movie_texture) + + # 尝试自动播放视频(如果支持) + try: + if hasattr(movie_texture, 'play'): + movie_texture.play() + print("▶️ 2D视频已开始播放") + except Exception as play_error: + print(f"⚠️ 2D视频自动播放失败: {play_error}") + else: + print(f"⚠️ 无法加载2D视频纹理: {video_path}") + except Exception as e: + print(f"❌ 加载2D视频纹理失败: {e}") + import traceback + traceback.print_exc() + else: + print(f"⚠️ 2D视频文件不存在: {video_path}") + + # if video_path and os.path.exists(video_path): + # try: + # print(f"🔍 尝试加载2D视频纹理: {video_path}") + # # 加载视频纹理 + # movie_texture = self._loadMovieTexture(video_path) + # if movie_texture: + # # 应用纹理到视频屏幕(替换占位符) + # video_screen["frameTexture"] = movie_texture + # print(f"✅ 2D视频纹理加载成功: {video_path}") + # + # # 保存视频纹理引用以便后续控制 + # video_screen.setPythonTag("movie_texture", movie_texture) + # + # # 尝试自动播放视频(如果支持) + # try: + # if hasattr(movie_texture, 'play'): + # movie_texture.play() + # print("▶️ 2D视频已开始播放") + # except Exception as play_error: + # print(f"⚠️ 2D视频自动播放失败: {play_error}") + # else: + # print(f"⚠️ 无法加载2D视频纹理: {video_path}") + # except Exception as e: + # print(f"❌ 加载2D视频纹理失败: {e}") + # import traceback + # traceback.print_exc() + # else: + # if not video_path: + # print(f"⚠️ 2D视频文件不存在: {video_path}") + + # 添加到GUI元素列表 + self.gui_elements.append(video_screen) + + print(f"✅ 为 {parent_item.text(0)} 创建2D视频屏幕成功: {screen_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(video_screen, parent_item, "GUI_2D_VIDEO_SCREEN") + if qt_item: + created_videoscreens.append((video_screen, qt_item)) + else: + created_videoscreens.append((video_screen, None)) + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建2D视频屏幕失败: {str(e)}") + import traceback + traceback.print_exc() + continue + + # 处理创建结果 + if not created_videoscreens: + print("❌ 没有成功创建任何2D视频屏幕") + return None + + # 选中最后创建的视频屏幕 + # if created_videoscreens: + # last_screen_np, last_qt_item = created_videoscreens[-1] + # if last_qt_item: + # tree_widget.setCurrentItem(last_qt_item) + # # 更新选择和属性面板 + # tree_widget.update_selection_and_properties(last_screen_np, last_qt_item) + + print(f"🎉 总共创建了 {len(created_videoscreens)} 个2D视频屏幕") + + # 返回值处理 + if len(created_videoscreens) == 1: + return created_videoscreens[0][0] # 单个屏幕返回NodePath + else: + return [screen_np for screen_np, _ in created_videoscreens] # 多个屏幕返回列表 + + except Exception as e: + print(f"❌ 创建2D视频屏幕过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def load2DVideoFile(self, video_screen, video_path): + """为2D视频屏幕加载新的视频文件""" + try: + import os + + video_screen.setTag("video_path",video_path) + path = video_screen.getTag("video_path") + print({video_screen.getTag("gui_type")}) + print(f"🔧 更新2D视频屏幕标签 - video_path: {path}") + + if not video_path or not os.path.exists(video_path): + print(f"❌ 2D视频文件不存在: {video_path}") + return False + + # 加载新的视频纹理 + movie_texture = self._loadMovieTexture(video_path) + if movie_texture: + # 应用纹理到2D视频屏幕 + video_screen["frameTexture"] = movie_texture + + # 保存视频纹理引用 + video_screen.setPythonTag("movie_texture", movie_texture) + + print(f"✅ 成功加载新2D视频: {video_path}") + return True + else: + print(f"❌ 无法加载2D视频文件: {video_path}") + return False + + except Exception as e: + print(f"❌ 加载2D视频文件失败: {e}") + import traceback + traceback.print_exc() + return False + + def play2DVideo(self, video_screen): + """播放2D视频""" + try: + if video_screen.hasPythonTag("video_capture"): + print("视频已在播放中") + return True + + # 获取视频路径并重新开始播放 + video_path = video_screen.getTag("video_path") + if video_path: + return self.world.property_manager._loadVideoFromURL(video_screen, video_path) + else: + print("❌ 没有找到视频源") + return False + + except Exception as e: + print(f"❌ 播放视频失败: {e}") + return False + + def pause2DVideo(self, video_screen): + """暂停2D视频""" + try: + # 在OpenCV中没有直接的暂停功能,我们通过停止线程来模拟暂停 + if video_screen.hasPythonTag("video_capture"): + cap = video_screen.getPythonTag("video_capture") + if cap: + cap.release() + video_screen.clearPythonTag("video_capture") + print("⏸️ 视频已暂停") + return True + return False + except Exception as e: + print(f"❌ 暂停视频失败: {e}") + return False + + def stop2DVideo(self, video_screen): + """停止2D视频""" + try: + # 停止视频捕获 + if video_screen.hasPythonTag("video_capture"): + cap = video_screen.getPythonTag("video_capture") + if cap: + cap.release() + video_screen.clearPythonTag("video_capture") + + # 清理纹理 + if video_screen.hasPythonTag("video_texture"): + video_screen.clearPythonTag("video_texture") + + # 清理视频路径标签 + video_screen.clearTag("video_path") + + print("⏹️ 视频已停止") + return True + except Exception as e: + print(f"❌ 停止视频失败: {e}") + return False + + def createSphericalVideo(self, pos=(0, 0, 0), radius=5.0, video_path=None): + """创建球形视频(360度视频)- 支持无初始视频文件""" + try: + from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter + from panda3d.core import Geom, GeomTriangles, GeomNode + from panda3d.core import TextureStage, Texture + import math + import os + + # 确保 pos 是有效的三维坐标元组 + if not isinstance(pos, (tuple, list)) or len(pos) != 3: + print(f"⚠️ 位置参数无效,使用默认值 (0, 0, 0),原始值: {pos}") + pos = (0, 0, 0) + else: + # 确保所有坐标都是数值类型 + try: + pos = (float(pos[0]), float(pos[1]), float(pos[2])) + except (ValueError, TypeError): + print(f"⚠️ 位置参数包含非数值,使用默认值 (0, 0, 0),原始值: {pos}") + pos = (0, 0, 0) + + # 确保 radius 是有效数值 + try: + radius = float(radius) + except (ValueError, TypeError): + print(f"⚠️ 半径参数无效,使用默认值 5.0,原始值: {radius}") + radius = 5.0 + + print(f"🌍 开始创建球形视频,位置: {pos}, 半径: {radius}, 视频路径: {video_path}") + + # 不再强制检查视频文件是否存在,允许创建空的球形视频 + if video_path and not os.path.exists(video_path): + print(f"⚠️ 视频文件不存在,将创建空的球形视频: {video_path}") + + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + # 获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None + + created_spherical_videos = [] + + # 为每个有效的父节点创建球形视频 + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + sphere_name = f"SphericalVideo_{len(self.gui_elements)}" + + # 创建球形几何体 + sphere_geom = self._createSphereGeometry(radius, segments=32) + + # 创建几何节点 + sphere_node = GeomNode('sphere_video') + sphere_node.addGeom(sphere_geom) + + # 创建节点路径并挂载 + sphere_np = parent_node.attachNewNode(sphere_node) + sphere_np.setPos(*pos) # 现在 pos 已经是有效的元组 + sphere_np.setName(sphere_name) + + # 翻转法线,使视频在球体内部显示 + sphere_np.setTwoSided(True) + sphere_np.setBin('background', 0) # 确保在背景层 + sphere_np.setDepthWrite(False) # 不写入深度缓冲 + + # 设置初始颜色为占位符颜色 + sphere_np.setColor(0.1, 0.1, 0.3, 0.8) # 深蓝色占位符 + + # 如果提供了视频路径且文件存在,则加载视频纹理 + movie_texture = None + if video_path and os.path.exists(video_path): + try: + # 加载视频纹理 + movie_texture = self._loadMovieTexture(video_path) + if movie_texture: + # 设置视频纹理属性 + movie_texture.setWrapU(Texture.WM_clamp) + movie_texture.setWrapV(Texture.WM_clamp) + movie_texture.setMinfilter(Texture.FT_linear) + movie_texture.setMagfilter(Texture.FT_linear) + + # 为视频纹理创建专用的纹理阶段 + texture_stage = TextureStage("spherical_video") + texture_stage.setMode(TextureStage.MModulate) + + # 应用纹理到球体 + sphere_np.setTexture(texture_stage, movie_texture) + + # 设置为白色以正确显示视频 + sphere_np.setColor(1, 1, 1, 1) + + print(f"✅ 视频纹理已应用到球形视频: {video_path}") + + # 尝试自动播放 + try: + if hasattr(movie_texture, 'play'): + movie_texture.play() + print("▶️ 球形视频已开始播放") + except Exception as play_error: + print(f"⚠️ 球形视频自动播放失败: {play_error}") + else: + print(f"⚠️ 无法加载视频纹理: {video_path}") + # 保持占位符颜色 + except Exception as e: + print(f"❌ 加载视频纹理失败: {e}") + import traceback + traceback.print_exc() + # 保持占位符颜色 + else: + if video_path: + print(f"⚠️ 视频文件不存在: {video_path}") + else: + print("ℹ️ 未提供视频文件,创建空的球形视频") + + # 设置标签以便识别和管理 + sphere_np.setTag("gui_type", "spherical_video") + sphere_np.setTag("is_gui_element", "1") + sphere_np.setTag("tree_item_type", "GUI_SPHERICAL_VIDEO") + sphere_np.setTag("video_path", video_path or "") + sphere_np.setTag("original_radius", str(radius)) + + # 保存视频纹理引用 + if movie_texture: + sphere_np.setPythonTag("movie_texture", movie_texture) + + # 添加到GUI元素列表 + self.gui_elements.append(sphere_np) + + print(f"✅ 为 {parent_item.text(0)} 创建球形视频成功: {sphere_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(sphere_np, parent_item, "GUI_SPHERICAL_VIDEO") + if qt_item: + created_spherical_videos.append((sphere_np, qt_item)) + else: + created_spherical_videos.append((sphere_np, None)) + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建球形视频失败: {str(e)}") + import traceback + traceback.print_exc() + continue + + # 处理创建结果 + if not created_spherical_videos: + print("❌ 没有成功创建任何球形视频") + return None + + # 选中最后创建的球形视频 + # if created_spherical_videos: + # last_sphere_np, last_qt_item = created_spherical_videos[-1] + # if last_qt_item: + # tree_widget.setCurrentItem(last_qt_item) + # # 更新选择和属性面板 + # tree_widget.update_selection_and_properties(last_sphere_np, last_qt_item) + + print(f"🎉 总共创建了 {len(created_spherical_videos)} 个球形视频") + + # 返回值处理 + if len(created_spherical_videos) == 1: + return created_spherical_videos[0][0] # 单个球形视频返回NodePath + else: + return [sphere_np for sphere_np, _ in created_spherical_videos] # 多个球形视频返回列表 + + except Exception as e: + print(f"❌ 创建球形视频过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def _createSphereGeometry(self, radius=5.0, segments=32): + """创建球形几何体""" + try: + from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter + from panda3d.core import Geom, GeomTriangles + import math + + # 创建顶点数据 + format = GeomVertexFormat.getV3n3t2() + vdata = GeomVertexData('sphere', format, Geom.UHStatic) + + vertex = GeomVertexWriter(vdata, 'vertex') + normal = GeomVertexWriter(vdata, 'normal') + texcoord = GeomVertexWriter(vdata, 'texcoord') + + # 生成球体顶点 + vertices = [] + for i in range(segments + 1): + phi = math.pi * i / segments # 从0到π + for j in range(segments * 2 + 1): + theta = 2 * math.pi * j / (segments * 2) # 从0到2π + + x = radius * math.sin(phi) * math.cos(theta) + y = radius * math.cos(phi) + z = radius * math.sin(phi) * math.sin(theta) + + # 纹理坐标 + u = j / (segments * 2) + v = i / segments + + # 修复:将坐标作为一个元组添加到列表中 + vertices.append((x, y, z, u, v)) + + # 添加顶点数据 + for vert in vertices: + vertex.addData3f(vert[0], vert[1], vert[2]) + # 法线(标准化顶点位置) + length = math.sqrt(vert[0] ** 2 + vert[1] ** 2 + vert[2] ** 2) + if length > 0: + normal.addData3f(vert[0] / length, vert[1] / length, vert[2] / length) + else: + normal.addData3f(0, 1, 0) + texcoord.addData2f(vert[3], vert[4]) + + # 创建几何体 + geom = Geom(vdata) + prim = GeomTriangles(Geom.UHStatic) + + # 生成三角形面 + for i in range(segments): + for j in range(segments * 2): + # 计算顶点索引 + v1 = i * (segments * 2 + 1) + j + v2 = (i + 1) * (segments * 2 + 1) + j + v3 = (i + 1) * (segments * 2 + 1) + (j + 1) + v4 = i * (segments * 2 + 1) + (j + 1) + + # 添加两个三角形 + prim.addVertices(v1, v2, v3) + prim.addVertices(v1, v3, v4) + + prim.closePrimitive() + geom.addPrimitive(prim) + + return geom + + except Exception as e: + print(f"❌ 创建球形几何体失败: {e}") + import traceback + traceback.print_exc() + raise + + def playSphericalVideo(self,spherical_video_node): + try: + if not spherical_video_node: + return False + texture = spherical_video_node.getTexture() + if texture and hasattr(texture,'play'): + texture.play() + return True + else: + return False + except Exception as e: + return False + + def pauseSphericalVideo(self,spherical_video_node): + try: + if not spherical_video_node: + return False + texture = spherical_video_node.getTexture() + if texture and hasattr(texture,'stop'): + texture.stop() + return True + else: + return False + except Exception as e: + return False + + def setSphericalVideoTime(self,spherical_video_node,time_seconds): + try: + if not spherical_video_node: + return False + texture = spherical_video_node.getTexture() + if texture and hasattr(texture,'set_time'): + texture.setTime(time_seconds) + return True + else: + return False + except Exception as e: + return False + + def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"): + """创建3D虚拟屏幕 - 支持多选创建,优化版本""" + try: + from panda3d.core import CardMaker, TransparencyAttrib, TextNode + from PyQt5.QtCore import Qt + + print(f"🖥️ 开始创建虚拟屏幕,位置: {pos}, 尺寸: {size}, 文本: {text}") + + # 获取树形控件 + tree_widget = self._get_tree_widget() + if not tree_widget: + print("❌ 无法访问树形控件") + return None + + # 获取目标父节点列表 + target_parents = tree_widget.get_target_parents_for_creation() + if not target_parents: + print("❌ 没有找到有效的父节点") + return None + + created_screens = [] + + # 为每个有效的父节点创建虚拟屏幕 + for parent_item, parent_node in target_parents: + try: + # 生成唯一名称 + screen_name = f"VirtualScreen_{len(self.gui_elements)}" + + # 创建虚拟屏幕几何体 + cm = CardMaker(f"virtual-screen-{len(self.gui_elements)}") + cm.setFrame(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2) + + # 创建挂载节点 - 挂载到选中的父节点 + virtual_screen = parent_node.attachNewNode(cm.generate()) + virtual_screen.setPos(*pos) + virtual_screen.setName(screen_name) + virtual_screen.setColor(0.2, 0.2, 0.2, 0.8) + virtual_screen.setTransparency(TransparencyAttrib.MAlpha) + + # 在虚拟屏幕上添加文本 + screen_text_node = self._create_screen_text(virtual_screen, text, len(self.gui_elements)) + + # 设置节点标签 + virtual_screen.setTag("gui_type", "virtual_screen") + virtual_screen.setTag("gui_id", f"virtual_screen_{len(self.gui_elements)}") + virtual_screen.setTag("gui_text", text) + virtual_screen.setTag("is_gui_element", "1") + virtual_screen.setTag("is_scene_element", "1") + virtual_screen.setTag("tree_item_type", "GUI_VirtualScreen") + virtual_screen.setTag("created_by_user", "1") + + # 添加到GUI元素列表 + self.gui_elements.append(virtual_screen) + + print(f"✅ 为 {parent_item.text(0)} 创建虚拟屏幕成功: {screen_name}") + + # 在Qt树形控件中添加对应节点 + qt_item = tree_widget.add_node_to_tree_widget(virtual_screen, parent_item, "GUI_VirtualScreen") + if qt_item: + created_screens.append((virtual_screen, qt_item)) + else: + created_screens.append((virtual_screen, None)) + print("⚠️ Qt树节点添加失败,但Panda3D对象已创建") + + except Exception as e: + print(f"❌ 为 {parent_item.text(0)} 创建虚拟屏幕失败: {str(e)}") + continue + + # 处理创建结果 + if not created_screens: + print("❌ 没有成功创建任何虚拟屏幕") + return None + + # 选中最后创建的虚拟屏幕 + # if created_screens: + # last_screen_np, last_qt_item = created_screens[-1] + # if last_qt_item: + # tree_widget.setCurrentItem(last_qt_item) + # # 更新选择和属性面板 + # tree_widget.update_selection_and_properties(last_screen_np, last_qt_item) + + print(f"🎉 总共创建了 {len(created_screens)} 个虚拟屏幕") + + # 返回值处理 + if len(created_screens) == 1: + return created_screens[0][0] # 单个屏幕返回NodePath + else: + return [screen_np for screen_np, _ in created_screens] # 多个屏幕返回列表 + + except Exception as e: + print(f"❌ 创建虚拟屏幕过程失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def _create_screen_text(self, virtual_screen, text, screen_index): + """为虚拟屏幕创建文本节点""" + try: + from panda3d.core import TextNode + + screen_text = TextNode(f'screen-text-{screen_index}') + screen_text.setText(text) + screen_text.setAlign(TextNode.ACenter) + + # 设置中文字体 + if hasattr(self.world, 'getChineseFont') and self.world.getChineseFont(): + screen_text.setFont(self.world.getChineseFont()) + + # 创建文本节点路径并设置属性 + screen_text_np = virtual_screen.attachNewNode(screen_text) + screen_text_np.setPos(0, 0.01, 0) # 稍微向前偏移避免Z-fighting + screen_text_np.setScale(0.3) + screen_text_np.setColor(0, 1, 0, 1) # 绿色文本 + + print(f"✅ 虚拟屏幕文本创建成功: {text}") + return screen_text_np + + except Exception as e: + print(f"❌ 创建虚拟屏幕文本失败: {str(e)}") + return None + + def _get_tree_widget(self): + """安全获取树形控件""" + try: + if (hasattr(self.world, 'interface_manager') and + hasattr(self.world.interface_manager, 'treeWidget')): + return self.world.interface_manager.treeWidget + except AttributeError: + pass + return None + + # 暂无滑块功能 + def createGUISlider(self, pos=(0, 0, 0), text="滑块", scale=0.3): + pass + """创建2D GUI滑块""" + from direct.gui.DirectGui import DirectSlider + + gui_pos = (pos[0] * 0.1, 0, pos[2] * 0.1) + + slider = DirectSlider( + pos=gui_pos, + scale=scale, + range=(0, 100), + value=50, + frameColor=(0.6, 0.6, 0.6, 1), + thumbColor=(0.2, 0.8, 0.2, 1) + ) + + slider.setTag("gui_type", "slider") + slider.setTag("gui_id", f"slider_{len(self.gui_elements)}") + slider.setTag("gui_text", text) + slider.setTag("is_gui_element", "1") + + self.gui_elements.append(slider) + # 安全地调用updateSceneTree + if hasattr(self.world, 'updateSceneTree'): + pass # CH + self.world.updateSceneTree() + + print(f"✓ 创建GUI滑块: {text} (逻辑位置: {pos}, 屏幕位置: {gui_pos})") + return slider + + # ==================== GUI元素管理方法 ==================== + + def deleteGUIElement(self, gui_element): + """删除GUI元素""" + try: + if gui_element in self.gui_elements: + # # 移除GUI元素 + # if hasattr(gui_element, 'removeNode'): + # gui_element.removeNode() + # elif hasattr(gui_element, 'destroy'): + # gui_element.destroy() + # + # # 从列表中移除 + # self.gui_elements.remove(gui_element) + + # 更新场景树 + # 安全地调用updateSceneTree + tree_widget = self._get_tree_widget() + if tree_widget: + tree_widget.delete_items(tree_widget.selectedItems()) + # if hasattr(self.world, 'updateSceneTree'): + # self.world.updateSceneTree() + + print(f"删除GUI元素: {gui_element}") + return True + except Exception as e: + print(f"删除GUI元素失败: {str(e)}") + return False + + # 在 gui_manager.py 中确保 editGUIElement 方法正确处理文本颜色 + def editGUIElement(self, gui_element, property_name, value): + """编辑GUI元素属性""" + try: + from panda3d.core import TextNode + + gui_type = gui_element.getTag("gui_type") if hasattr(gui_element, 'getTag') else "unknown" + print(f"开始编辑GUI元素: 类型={gui_type}, 属性={property_name}, 值={value}") + + if property_name == "text": + original_frame_size = None + if hasattr(gui_element,'getFrameSize'): + try: + original_frame_size = gui_element.getFrameSize() + except: + pass + if gui_type in ["button", "label"]: + gui_element['text'] = value + print(f"成功更新2D GUI文本: {value}") + # if gui_type == "button": + # self._resizeButtonToText(gui_element,value,original_frame_size) + elif gui_type == "entry": + gui_element.set(value) + print(f"成功更新输入框文本: {value}") + elif gui_type == "3d_text": + # 对于3D文本,直接修改自身的TextNode + if isinstance(gui_element.node(), TextNode): + gui_element.node().setText(value) + print(f"成功更新3D文本: {value}") + else: + print(f"警告: {gui_type}节点类型为{type(gui_element.node())},不是TextNode类型") + + elif gui_type == "virtual_screen": + # 对于虚拟屏幕,需要找到TextNode子节点 + print(f"虚拟屏幕有 {gui_element.getNumChildren()} 个子节点") + text_found = False + for i, child in enumerate(gui_element.getChildren()): + print(f"子节点 {i}: {child.getName()}, 类型: {type(child.node())}") + if isinstance(child.node(), TextNode): + child.node().setText(value) + text_found = True + print(f"成功更新虚拟屏幕文本: {value}") + break + + if not text_found: + print(f"警告: 在{gui_type}中未找到TextNode子节点") + + gui_element.setTag("gui_text", value) + + elif property_name == "color": # 添加颜色处理 + if isinstance(value, (list, tuple)) and len(value) >= 3: + # 对于2D GUI元素(button和label),使用frameColor属性 + if gui_type in ["button", "label"]: + # 设置背景颜色 + gui_element['frameColor'] = (value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0) + print(f"成功更新2D GUI背景颜色: {gui_type} -> {value}") + + # 对于3D元素,使用材质颜色 + elif gui_type in ["3d_text", "3d_image", "virtual_screen"]: + # 更新材质颜色 + if not gui_element.hasMaterial(): + material = Material(f"text-material-{gui_element.getName()}") + material.setBaseColor(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) + material.setDiffuse(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) + gui_element.setMaterial(material, 1) + else: + material = gui_element.getMaterial() + material.setBaseColor(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) + material.setDiffuse(Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) + gui_element.setMaterial(material, 1) + + # 更新 TextNode 的文本颜色 + if isinstance(gui_element.node(), TextNode): + gui_element.node().setTextColor( + Vec4(value[0], value[1], value[2], value[3] if len(value) > 3 else 1.0)) + print(f"成功更新3D GUI颜色: {gui_type} -> {value}") + + else: + print(f"警告: 未知的GUI类型 {gui_type},无法设置颜色") + + elif property_name == "position": + if isinstance(value, (list, tuple)) and len(value) >= 3: + gui_element.setPos(*value[:3]) + + elif property_name == "scale": + if isinstance(value, (int, float)): + gui_element.setScale(value) + elif isinstance(value, (list, tuple)) and len(value) >= 3: + gui_element.setScale(*value[:3]) + + print(f"编辑GUI元素 {gui_type}: {property_name} = {value}") + return True + + except Exception as e: + print(f"编辑GUI元素失败: {str(e)}") + import traceback + traceback.print_exc() + return False + + def _resizeButtonToText(self, button, text, original_frame_size=None): + """ + 根据文本内容调整按钮大小 + + Args: + button: DirectButton 对象 + text: 新的文本内容 + original_frame_size: 原始frameSize,用于计算合适的padding + """ + try: + # 获取按钮当前的文本缩放 + text_scale = 0.03 # 默认文本缩放 + padding = 0.2 # 默认边距 + + # 尝试从按钮获取实际的文本缩放值 + if hasattr(button, 'getTextScale'): + text_scale = button.getTextScale() + elif hasattr(button, 'textScale'): + text_scale = button.textScale + + # 根据原始frameSize计算合适的padding + if original_frame_size and len(original_frame_size) >= 4: + # 基于原始按钮宽度计算合适的padding + padding = max((original_frame_size[1] - original_frame_size[0]) * 0.15, 0.1) + + # 更精确的文本尺寸估算 + # 考虑中文字符和英文字符的不同宽度 + chinese_chars = len([c for c in text if ord(c) > 127]) + english_chars = len(text) - chinese_chars + + # 中文字符通常比英文字符宽 + char_width = text_scale * 0.6 + text_width = (chinese_chars * char_width * 1.5) + (english_chars * char_width) + text_height = text_scale * 1.2 # 文本高度 + + # 计算新的frameSize,确保有足够的边距 + half_width = max(text_width * 0.5 + padding, 0.3) # 最小宽度0.3 + half_height = max(text_height * 0.5 + padding * 0.5, 0.15) # 最小高度0.15 + + # 正确设置frameSize - 使用字典方式设置 + new_frame = (-half_width, half_width, -half_height, half_height) + button['frameSize'] = new_frame # 使用字典方式设置 + + print(f"按钮大小已调整: {new_frame} (文本: {text})") + + except Exception as e: + print(f"调整按钮大小失败: {e}") + # 如果自动调整失败,保持原有大小或设置一个合理的默认大小 + try: + if original_frame_size: + # 保持原有大小 + button['frameSize'] = original_frame_size + else: + # 设置一个合理的默认大小 + button['frameSize'] = (-0.5, 0.5, -0.15, 0.15) + except: + pass + + def duplicateGUIElement(self, gui_element): + """复制GUI元素""" + try: + gui_type = gui_element.getTag("gui_type") + gui_text = gui_element.getTag("gui_text") + + # 获取当前位置并偏移 + pos = gui_element.getPos() + new_pos = (pos.getX() + 0.2, pos.getY(), pos.getZ() + 0.2) + + # 根据类型创建新的GUI元素 + if gui_type == "button": + self.createGUIButton(new_pos, gui_text + "_副本") + elif gui_type == "label": + self.createGUILabel(new_pos, gui_text + "_副本") + elif gui_type == "entry": + self.createGUIEntry(new_pos, gui_text + "_副本") + elif gui_type == "3d_text": + self.createGUI3DText(new_pos, gui_text + "_副本") + elif gui_type == "3d_image": + image_path = gui_element.getTag("image_path") + self.createGUI3DImage(new_pos,image_path,size=(2,2)) + elif gui_type == "2d_image": + image_path = gui_element.getTag("image_path") + self.createGUI2DImage(new_pos, image_path, size=0.2) + elif gui_type == "virtual_screen": + self.createGUIVirtualScreen(new_pos, text=gui_text + "_副本") + + print(f"复制GUI元素: {gui_type} - {gui_text}") + + except Exception as e: + print(f"复制GUI元素失败: {str(e)}") + + def editGUIElementDialog(self, gui_element): + """显示GUI元素编辑对话框""" + dialog = QDialog() + dialog.setWindowTitle("编辑GUI元素") + dialog.setMinimumWidth(300) + + layout = QVBoxLayout(dialog) + form = QFormLayout() + + gui_type = gui_element.getTag("gui_type") + gui_text = gui_element.getTag("gui_text") + + # 文本编辑 + if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]: + textEdit = QLineEdit(gui_text or "") + form.addRow("文本:", textEdit) + + # 位置编辑 + if hasattr(gui_element, 'getPos'): + pos = gui_element.getPos() + xEdit = QDoubleSpinBox() + xEdit.setRange(-1000, 1000) + xEdit.setValue(pos.getX()) + form.addRow("位置 X:", xEdit) + + yEdit = QDoubleSpinBox() + yEdit.setRange(-1000, 1000) + yEdit.setValue(pos.getY()) + form.addRow("位置 Y:", yEdit) + + zEdit = QDoubleSpinBox() + zEdit.setRange(-1000, 1000) + zEdit.setValue(pos.getZ()) + form.addRow("位置 Z:", zEdit) + + # 缩放编辑 + if hasattr(gui_element, 'getScale'): + scale = gui_element.getScale() + scaleEdit = QDoubleSpinBox() + scaleEdit.setRange(0.01, 100) + scaleEdit.setSingleStep(0.1) + scaleEdit.setValue(scale.getX()) + form.addRow("缩放:", scaleEdit) + + layout.addLayout(form) + + # 按钮 + buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + buttonBox.accepted.connect(dialog.accept) + buttonBox.rejected.connect(dialog.reject) + layout.addWidget(buttonBox) + + # 执行对话框 + if dialog.exec_() == QDialog.Accepted: + try: + # 应用更改 + if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]: + self.editGUIElement(gui_element, "text", textEdit.text()) + + if hasattr(gui_element, 'getPos'): + self.editGUIElement(gui_element, "position", [xEdit.value(), yEdit.value(), zEdit.value()]) + + if hasattr(gui_element, 'getScale'): + self.editGUIElement(gui_element, "scale", scaleEdit.value()) + + # 更新属性面板 + if self.world.treeWidget: + currentItem = self.world.treeWidget.currentItem() + if currentItem: + self.world.updatePropertyPanel(currentItem) + + print("GUI元素编辑完成") + + except Exception as e: + print(f"应用GUI编辑失败: {str(e)}") + + # ==================== GUI事件处理方法 ==================== + + def onGUIButtonClick(self, button_id): + """GUI按钮点击事件处理""" + print(f"GUI按钮被点击: {button_id}") + + def onGUIEntrySubmit(self, text, entry_id): + """GUI输入框提交事件处理""" + print(f"GUI输入框提交: {entry_id} = {text}") + + # ==================== GUI编辑模式方法 ==================== + + def toggleGUIEditMode(self): + """切换GUI编辑模式""" + self.guiEditMode = not self.guiEditMode + + if self.guiEditMode: + self.enterGUIEditMode() + else: + self.exitGUIEditMode() + + def enterGUIEditMode(self): + """进入GUI编辑模式""" + print("\n=== 进入GUI编辑模式 ===") + + # 打开GUI预览窗口 + self.openGUIPreviewWindow() + + # 创建GUI编辑面板 + self.createGUIEditPanel() + + # 改变当前工具为GUI选择工具 + self.world.currentTool = "GUI编辑" + + print("GUI编辑模式已激活") + print("- 使用右侧工具栏创建GUI元素") + print("- 在独立预览窗口中查看效果") + print("- 左键点击现有GUI元素选择和编辑") + + def exitGUIEditMode(self): + """退出GUI编辑模式""" + print("\n=== 退出GUI编辑模式 ===") + + # 关闭GUI预览窗口 + self.closeGUIPreviewWindow() + + # 移除GUI编辑面板 + if self.guiEditPanel: + self.guiEditPanel.destroy() + self.guiEditPanel = None + + # 恢复普通工具 + self.world.currentTool = "选择" + + print("GUI编辑模式已关闭") + + def createGUIEditPanel(self): + """创建GUI编辑面板""" + from direct.gui.DirectGui import DirectFrame, DirectButton, DirectLabel + + # 创建主面板 + self.guiEditPanel = DirectFrame( + pos=(0.85, 0, 0), + frameSize=(-0.15, 0.15, -0.9, 0.9), + frameColor=(0.1, 0.1, 0.1, 0.8), + text="GUI编辑器", + text_pos=(0, 0.85), + text_scale=0.05, + text_fg=(1, 1, 1, 1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + + # 创建工具按钮 + y_pos = 0.7 + spacing = 0.12 + + # 2D GUI工具 + label_2d = DirectLabel( + parent=self.guiEditPanel, + text="2D GUI", + pos=(0, 0, y_pos), + scale=0.04, + text_fg=(1, 1, 0, 1), + frameColor=(0, 0, 0, 0), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= 0.08 + + # 按钮工具 + btn_button = DirectButton( + parent=self.guiEditPanel, + text="按钮", + pos=(0, 0, y_pos), + scale=0.04, + command=self.setGUICreateTool, + extraArgs=["button"], + frameColor=(0.2, 0.6, 0.8, 1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= spacing + + # 标签工具 + btn_label = DirectButton( + parent=self.guiEditPanel, + text="标签", + pos=(0, 0, y_pos), + scale=0.04, + command=self.setGUICreateTool, + extraArgs=["label"], + frameColor=(0.6, 0.8, 0.2, 1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= spacing + + # 输入框工具 + btn_entry = DirectButton( + parent=self.guiEditPanel, + text="输入框", + pos=(0, 0, y_pos), + scale=0.04, + command=self.setGUICreateTool, + extraArgs=["entry"], + frameColor=(0.8, 0.6, 0.2, 1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= spacing + + # 3D GUI工具 + label_3d = DirectLabel( + parent=self.guiEditPanel, + text="3D GUI", + pos=(0, 0, y_pos), + scale=0.04, + text_fg=(1, 1, 0, 1), + frameColor=(0, 0, 0, 0), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= 0.08 + + # 3D文本工具 + btn_3dtext = DirectButton( + parent=self.guiEditPanel, + text="3D文本", + pos=(0, 0, y_pos), + scale=0.04, + command=self.setGUICreateTool, + extraArgs=["3d_text"], + frameColor=(0.8, 0.2, 0.6, 1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= spacing + + #3D图片工具 + btn_image = DirectButton( + parent = self.guiEditPanel, + text="3D图片", + pos=(0,0,y_pos), + scale=0.04, + command=self.setGUICreateTool, + extraArgs=["3d_image"], + frameColor=(0.2,0.8,0.8,1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= spacing + + # 2D图片工具 + btn_2d_image = DirectButton( + parent=self.guiEditPanel, + text="2D图片", + pos=(0, 0, y_pos), + scale=0.04, + command=self.setGUICreateTool, + extraArgs=["2d_image"], + frameColor=(0.8, 0.6, 0.2, 1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= spacing + + # 虚拟屏幕工具 + btn_screen = DirectButton( + parent=self.guiEditPanel, + text="虚拟屏幕", + pos=(0, 0, y_pos), + scale=0.04, + command=self.setGUICreateTool, + extraArgs=["virtual_screen"], + frameColor=(0.6, 0.2, 0.8, 1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= spacing + + #Cesium 集成工具 (仅在Webengine 可用时显示) + if WEB_ENGINE_AVAILABLE: + label_cesium = DirectLabel( + parent=self.guiEditPanel, + text="Cesium 集成", + pos=(0, 0, y_pos), + scale=0.04, + text_fg=(1, 1, 0, 1), + frameColor=(0, 0, 0, 0), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= 0.08 + + # 切换 Cesium 视图按钮 + btn_toggle_cesium = DirectButton( + parent=self.guiEditPanel, + text="切换地图视图", + pos=(0, 0, y_pos), + scale=0.04, + command=self.toggleCesiumView, + frameColor=(0.2, 0.8, 0.6, 1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= spacing + + # 刷新 Cesium 视图按钮 + btn_refresh_cesium = DirectButton( + parent=self.guiEditPanel, + text="刷新地图", + pos=(0, 0, y_pos), + scale=0.04, + command=self.refreshCesiumView, + frameColor=(0.6, 0.8, 0.2, 1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= spacing + + # 分隔线 + y_pos -= 0.1 + + # 操作按钮 + label_ops = DirectLabel( + parent=self.guiEditPanel, + text="操作", + pos=(0, 0, y_pos), + scale=0.04, + text_fg=(1, 1, 0, 1), + frameColor=(0, 0, 0, 0), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= 0.08 + + # 删除工具 + btn_delete = DirectButton( + parent=self.guiEditPanel, + text="删除", + pos=(0, 0, y_pos), + scale=0.04, + command=self.deleteSelectedGUI, + frameColor=(0.8, 0.2, 0.2, 1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= spacing + + # 复制工具 + btn_copy = DirectButton( + parent=self.guiEditPanel, + text="复制", + pos=(0, 0, y_pos), + scale=0.04, + command=self.copySelectedGUI, + frameColor=(0.2, 0.8, 0.2, 1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + y_pos -= spacing + + # 退出GUI编辑模式 + btn_exit = DirectButton( + parent=self.guiEditPanel, + text="退出", + pos=(0, 0, -0.8), + scale=0.04, + command=self.toggleGUIEditMode, + frameColor=(0.5, 0.5, 0.5, 1), + text_font=self.world.getChineseFont() if self.world.getChineseFont() else None + ) + + # 存储当前的GUI创建工具 + self.currentGUITool = None + + def openGUIPreviewWindow(self): + """打开独立的GUI预览窗口""" + try: + from gui_preview_window import GUIPreviewWindow + + self.guiPreviewWindow = GUIPreviewWindow() + self.guiPreviewWindow.set_main_world(self.world) + + print("✓ GUI预览窗口已打开") + print("这个独立窗口会实时显示您创建的GUI元素") + + except ImportError: + print("错误: 无法导入GUI预览窗口模块") + except Exception as e: + print(f"打开GUI预览窗口失败: {str(e)}") + + def closeGUIPreviewWindow(self): + """关闭GUI预览窗口""" + if self.guiPreviewWindow: + self.guiPreviewWindow.destroy() + self.guiPreviewWindow = None + print("GUI预览窗口已关闭") + + # ==================== GUI工具和选择方法 ==================== + + def setGUICreateTool(self, tool_type): + """设置GUI创建工具""" + self.currentGUITool = tool_type + print(f"选择GUI创建工具: {tool_type}") + + def deleteSelectedGUI(self): + """删除选中的GUI元素""" + if self.world.selection.selectedNode and hasattr(self.world.selection.selectedNode, 'getTag'): + gui_type = self.world.selection.selectedNode.getTag("gui_type") + if gui_type: + success = self.deleteGUIElement(self.world.selection.selectedNode) + if success: + self.world.selection.updateSelection(None) + print("GUI元素已删除") + else: + print("删除GUI元素失败") + else: + print("选中的不是GUI元素") + else: + print("没有选中的GUI元素") + + def copySelectedGUI(self): + """复制选中的GUI元素""" + if self.world.selection.selectedNode and hasattr(self.world.selection.selectedNode, 'getTag'): + gui_type = self.world.selection.selectedNode.getTag("gui_type") + if gui_type: + self.duplicateGUIElement(self.world.selection.selectedNode) + print("GUI元素已复制") + else: + print("选中的不是GUI元素") + else: + print("没有选中的GUI元素") + + def handleGUIEditClick(self, hitPos): + """处理GUI编辑模式下的点击""" + if not self.guiEditMode: + return False + + if self.currentGUITool: + # 创建新的GUI元素 + self.createGUIAtPosition(hitPos, self.currentGUITool) + return True + + return False + + def createGUIAtPosition(self, world_pos, gui_type): + """在指定位置创建GUI元素""" + print(f"在位置 {world_pos} 创建 {gui_type}") + + # 根据GUI类型选择合适的坐标转换 + if gui_type in ["button", "label", "entry"]: + # 2D GUI - 将世界坐标转换为屏幕逻辑坐标 + screen_x = world_pos.getX() * 2 # 缩放因子 + screen_z = world_pos.getZ() * 2 + pos = (screen_x, 0, screen_z) + else: + # 3D GUI - 直接使用世界坐标 + pos = (world_pos.getX(), world_pos.getY(), world_pos.getZ()) + + # 创建不同类型的GUI元素 + if gui_type == "button": + element = self.createGUIButton(pos, f"按钮_{len(self.gui_elements)}") + elif gui_type == "label": + element = self.createGUILabel(pos, f"标签_{len(self.gui_elements)}") + elif gui_type == "entry": + element = self.createGUIEntry(pos, f"输入框_{len(self.gui_elements)}") + elif gui_type == "3d_text": + element = self.createGUI3DText(pos, f"3D文本_{len(self.gui_elements)}") + elif gui_type == "3d_image": + element = self.createGUI3DImage(pos) + elif gui_type == "2d_image": + element = self.createGUI2DImage(pos) + elif gui_type == "virtual_screen": + element = self.createGUIVirtualScreen(pos, text=f"屏幕_{len(self.gui_elements)}") + else: + print(f"未知的GUI类型: {gui_type}") + return + + # 自动选中新创建的元素 + self.world.selection.updateSelection(element) + self.selectGUIInTree(element) + print(f"创建并选中了新的{gui_type}元素") + + def findClickedGUI(self, hitNode): + """查找被点击的GUI元素""" + # 检查点击的节点是否是GUI元素 + current = hitNode + while current != self.world.render: + if hasattr(current, 'getTag') and current.getTag("gui_type"): + return current + current = current.getParent() + return None + + def selectGUIInTree(self, gui_element): + """在树形控件中选中GUI元素""" + if not self.world.treeWidget or not gui_element: + return + + def findGUIItem(item): + """递归查找GUI元素对应的树形项""" + if item.data(0, Qt.UserRole) == gui_element: + return item + + for i in range(item.childCount()): + child = item.child(i) + result = findGUIItem(child) + if result: + return result + return None + + # 从根开始查找 + root = self.world.treeWidget.invisibleRootItem() + for i in range(root.childCount()): + sceneItem = root.child(i) + if sceneItem.text(0) == "场景": + for j in range(sceneItem.childCount()): + childItem = sceneItem.child(j) + if childItem.text(0) == "GUI元素": + foundItem = findGUIItem(childItem) + if foundItem: + self.world.treeWidget.setCurrentItem(foundItem) + self.world.updatePropertyPanel(foundItem) + return + + def updateGUISelection(self, gui_element): + """更新GUI元素选择状态""" + self.world.selection.updateSelection(gui_element) + if gui_element and hasattr(gui_element, 'getTag'): + gui_type = gui_element.getTag("gui_type") + gui_text = gui_element.getTag("gui_text") + print(f"选中GUI元素: {gui_type} - {gui_text}") + + # 在树形控件中选中 + self.selectGUIInTree(gui_element) + + # ==================== GUI属性面板方法 ==================== + + def updateGUIPropertyPanel(self, gui_element, layout): + """更新GUI元素属性面板""" + gui_type = gui_element.getTag("gui_type") + gui_text = gui_element.getTag("gui_text") + + # GUI类型显示 + typeLabel = QLabel("GUI类型:") + typeValue = QLabel(gui_type) + typeValue.setStyleSheet("color: #00AAFF; font-weight: bold;") + layout.addRow(typeLabel, typeValue) + + # 文本属性(如果适用) + if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]: + textLabel = QLabel("文本:") + textEdit = QLineEdit(gui_text or "") + + # 创建一个更新函数来处理文本变化 + def updateText(text): + success = self.editGUIElement(gui_element, "text", text) + if success: + # 更新场景树显示的名称 + # 安全地调用updateSceneTree + if hasattr(self.world, 'updateSceneTree'): + self.world.updateSceneTree() + + textEdit.textChanged.connect(updateText) + layout.addRow(textLabel, textEdit) + + # 位置属性 + if hasattr(gui_element, 'getPos'): + # 根据GUI类型设置组名 + if gui_type in ["button", "label", "entry", "2d_image"]: + transform_group = QGroupBox("变换 Rect Transform") + else: + transform_group = QGroupBox("变换 Transform") + + transform_layout = QGridLayout() + + pos = gui_element.getPos() + + # 根据GUI类型决定位置编辑方式 + if gui_type in ["button", "label", "entry","2d_image"]: + # 2D GUI组件使用屏幕坐标 + logical_x = pos.getX() / 0.1 # 反向转换为逻辑坐标 + logical_z = pos.getZ() / 0.1 + + transform_layout.addWidget(QLabel("屏幕位置"), 0, 0) + + x_label = QLabel("X") + z_label = QLabel("z") + x_label.setAlignment(Qt.Aligncenter) + z_label.setAlignment(Qt.AlignCenter) + + transform_layout.addWidget(x_label, 0, 1) + transform_layout.addWidget(z_label, 0, 2) + + xPos = QDoubleSpinBox() + xPos.setRange(-1000, 1000) + xPos.setValue(logical_x) + xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "x", v)) + transform_layout.addWidget(xPos, 1, 1) + + zPos = QDoubleSpinBox() + zPos.setRange(-1000, 1000) + zPos.setValue(logical_z) + zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI2DPosition(gui_element, "z", v)) + transform_layout.addWidget(zPos, 1, 2) + + # 显示实际屏幕坐标(只读) + transform_layout.addWidget(QLabel("实际坐标"), 2, 0) + + actualXLabel = QLabel(f"{pos.getX():.3f}") + actualXLabel.setStyleSheet("color: gray; font-size: 10px;") + actualZLabel = QLabel(f"{pos.getZ():.3f}") + actualZLabel.setStyleSheet("color: gray; font-size: 10px;") + + transform_layout.addWidget(actualXLabel, 3, 1) + transform_layout.addWidget(actualZLabel, 3, 2) + # 添加宽度和高度控件(对于2D图像) + if gui_type == "2d_image": + + # 添加排序控制组 + sort_group = QGroupBox("渲染顺序") + sort_layout = QGridLayout() + + # 获取当前的sort值,如果没有设置则默认为0 + current_sort = int(gui_element.getTag("sort") or "0") + + sort_layout.addWidget(QLabel("层级:"), 0, 0) + + sort_spin = QSpinBox() + sort_spin.setRange(-1000, 1000) # 设置合理的范围 + sort_spin.setValue(current_sort) + + # 创建更新排序的函数 + def updateSort(value): + try: + # 设置标签 + gui_element.setTag("sort", str(value)) + # 应用sort到节点 - 使用fixed bin和指定的值 + gui_element.setBin("fixed", value) + print(f"✓ 更新2D图像渲染顺序: {value}") + except Exception as e: + print(f"✗ 更新2D图像渲染顺序失败: {e}") + + sort_spin.valueChanged.connect(updateSort) + sort_layout.addWidget(sort_spin, 0, 1) + + # 添加说明标签 + sort_help = QLabel("(数值越大越靠前)") + sort_help.setStyleSheet("font-size: 10px; color: gray;") + sort_layout.addWidget(sort_help, 1, 0, 1, 2) + + sort_group.setLayout(sort_layout) + layout.addWidget(sort_group) + + scale = gui_element.getScale() + width = scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, + (tuple, list)) else scale + height = scale.getZ() if hasattr(scale, 'getZ') else scale[1] if isinstance(scale, + (tuple, list)) and len( + scale) > 1 else scale + + # 宽度控件 + transform_layout.addWidget(QLabel("宽度"), 4, 0) + widthSpinBox = QDoubleSpinBox() + widthSpinBox.setRange(0.01, 100) + widthSpinBox.setSingleStep(0.1) + widthSpinBox.setValue(width) + widthSpinBox.valueChanged.connect( + lambda v: self.world.gui_manager.editGUIScale(gui_element, "x", v)) + transform_layout.addWidget(widthSpinBox, 4, 1) + + # 高度控件 + transform_layout.addWidget(QLabel("高度"), 4, 2) + heightSpinBox = QDoubleSpinBox() + heightSpinBox.setRange(0.01, 100) + heightSpinBox.setSingleStep(0.1) + heightSpinBox.setValue(height) + heightSpinBox.valueChanged.connect( + lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v)) + transform_layout.addWidget(heightSpinBox, 4, 3) + + else: + # 3D GUI组件使用世界坐标 + transform_layout.addWidget(QLabel("位置"), 0, 0) + + # X, Y, Z 标签居中 + x_label = QLabel("X") + y_label = QLabel("Y") + z_label = QLabel("Z") + x_label.setAlignment(Qt.AlignCenter) + y_label.setAlignment(Qt.AlignCenter) + z_label.setAlignment(Qt.AlignCenter) + + transform_layout.addWidget(x_label, 0, 1) + transform_layout.addWidget(y_label, 0, 2) + transform_layout.addWidget(z_label, 0, 3) + + # 位置数值输入框 + xPos = QDoubleSpinBox() + xPos.setRange(-100, 100) + xPos.setValue(pos.getX()) + xPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "x", v)) + transform_layout.addWidget(xPos, 1, 1) + + yPos = QDoubleSpinBox() + yPos.setRange(-100, 100) + yPos.setValue(pos.getY()) + yPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "y", v)) + transform_layout.addWidget(yPos, 1, 2) + + zPos = QDoubleSpinBox() + zPos.setRange(-100, 100) + zPos.setValue(pos.getZ()) + zPos.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "z", v)) + transform_layout.addWidget(zPos, 1, 3) + + # 缩放属性 + scale = gui_element.getScale() + transform_layout.addWidget(QLabel("缩放"), 2, 0) + + # X, Y, Z 缩放标签居中 + sx_label = QLabel("X") + sy_label = QLabel("Y") + sz_label = QLabel("Z") + sx_label.setAlignment(Qt.AlignCenter) + sy_label.setAlignment(Qt.AlignCenter) + sz_label.setAlignment(Qt.AlignCenter) + + transform_layout.addWidget(sx_label, 2, 1) + transform_layout.addWidget(sy_label, 2, 2) + transform_layout.addWidget(sz_label, 2, 3) + + # 缩放数值输入框 + scale_x = QDoubleSpinBox() + scale_x.setRange(0.01, 100) + scale_x.setSingleStep(0.1) + scale_x.setValue( + scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, (tuple, list)) else scale) + scale_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "x", v)) + transform_layout.addWidget(scale_x, 3, 1) + + scale_y = QDoubleSpinBox() + scale_y.setRange(0.01, 100) + scale_y.setSingleStep(0.1) + scale_y.setValue( + scale.getY() if hasattr(scale, 'getY') else scale[1] if isinstance(scale, (tuple, list)) and len( + scale) > 1 else scale) + scale_y.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "y", v)) + transform_layout.addWidget(scale_y, 3, 2) + + scale_z = QDoubleSpinBox() + scale_z.setRange(0.01, 100) + scale_z.setSingleStep(0.1) + scale_z.setValue( + scale.getZ() if hasattr(scale, 'getZ') else scale[2] if isinstance(scale, (tuple, list)) and len( + scale) > 2 else scale) + scale_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v)) + transform_layout.addWidget(scale_z, 3, 3) + transform_group.setLayout(transform_layout) + self._propertyLayout.addWidget(transform_group) + + # 缩放属性 + if hasattr(gui_element, 'getScale'): + scale = gui_element.getScale() + + scaleSpinBox = QDoubleSpinBox() + scaleSpinBox.setRange(0.01, 100) + scaleSpinBox.setSingleStep(0.1) + scaleSpinBox.setValue(scale.getX()) + scaleSpinBox.valueChanged.connect(lambda v: self.editGUIElement(gui_element, "scale", v)) + layout.addRow("缩放:", scaleSpinBox) + + # 颜色属性(针对2D GUI) + if gui_type in ["button", "label"]: + colorButton = QPushButton("选择颜色") + colorButton.clicked.connect(lambda: self.selectGUIColor(gui_element)) + layout.addRow("背景颜色:", colorButton) + + # 3D特有属性 + if gui_type in ["3d_text", "virtual_screen"]: + # 旋转属性 + if hasattr(gui_element, 'getHpr'): + hpr = gui_element.getHpr() + + hRot = QDoubleSpinBox() + hRot.setRange(-180, 180) + hRot.setValue(hpr.getX()) + hRot.valueChanged.connect(lambda v: gui_element.setH(v)) + layout.addRow("旋转 H:", hRot) + + pRot = QDoubleSpinBox() + pRot.setRange(-180, 180) + pRot.setValue(hpr.getY()) + pRot.valueChanged.connect(lambda v: gui_element.setP(v)) + layout.addRow("旋转 P:", pRot) + + rRot = QDoubleSpinBox() + rRot.setRange(-180, 180) + rRot.setValue(hpr.getZ()) + rRot.valueChanged.connect(lambda v: gui_element.setR(v)) + layout.addRow("旋转 R:", rRot) + + def selectGUIColor(self, gui_element): + """选择GUI元素颜色""" + color = QColorDialog.getColor(QColor(128, 128, 128), None, "选择颜色") + if color.isValid(): + r, g, b = color.red() / 255.0, color.green() / 255.0, color.blue() / 255.0 + self.editGUIElement(gui_element, "color", [r, g, b, 1.0]) + + def update3DImageTexture(self, model_nodepath, image_path): + from panda3d.core import Texture, TextureStage + + try: + # 加载新纹理 + new_texture = self.world.loader.loadTexture(image_path) + if new_texture: + # 确保纹理过滤质量 + new_texture.setMagfilter(Texture.FT_linear) + new_texture.setMinfilter(Texture.FT_linear_mipmap_linear) + + model_nodepath.clearTexture() + + # 为3D图像创建独立的纹理阶段 + image_stage = TextureStage("3d_image_texture") + image_stage.setSort(0) # 使用第一个纹理槽 + image_stage.setMode(TextureStage.MModulate) # 使用调制模式 + + # 应用纹理到模型,使用独立的纹理阶段 + model_nodepath.setTexture(image_stage, new_texture) + + # 更新标签 + model_nodepath.setTag("gui_image_path", image_path) + + # 确保材质设置正确 + if not model_nodepath.hasMaterial(): + from panda3d.core import Material, LColor + mat = Material() + mat.setName(f"image-material-{id(model_nodepath)}") + mat.setBaseColor(LColor(1, 1, 1, 1)) + mat.setDiffuse(LColor(1, 1, 1, 1)) + mat.setAmbient(LColor(0.5, 0.5, 0.5, 1)) + mat.setSpecular(LColor(0.1, 0.1, 0.1, 1.0)) + mat.setShininess(10.0) + model_nodepath.setMaterial(mat, 1) + + # 保护子节点的纹理(特别是3D文本) + self._preserveChildNodeTextures(model_nodepath) + + print(f"✅ 3D图像纹理已更新为: {image_path}") + return True + else: + print(f"❌ 无法加载纹理: {image_path}") + return False + except Exception as e: + print(f"❌ 更新纹理时出错: {e}") + return False + + def _preserveChildNodeTextures(self, parent_node): + """保护子节点的纹理不被父节点纹理影响""" + try: + # 遍历所有直接子节点 + for i in range(parent_node.getNumChildren()): + child = parent_node.getChild(i) + + # 检查子节点是否为3D文本或其他需要特殊处理的节点 + if self._is3DTextElement(child): + # 为3D文本创建独立的纹理阶段 + self._restore3DTextTexture(child) + elif self._isOtherSpecialElement(child): + # 为其他特殊元素恢复纹理 + self._restoreSpecialElementTexture(child) + + except Exception as e: + print(f"保护子节点纹理时出错: {e}") + + def _is3DTextElement(self, node): + """检查节点是否为3D文本元素""" + try: + return (hasattr(node, 'getTag') and + node.getTag("gui_type") == "3d_text") + except: + return False + + def _isOtherSpecialElement(self, node): + """检查节点是否为其他需要特殊处理的元素""" + try: + return (hasattr(node, 'getTag') and + node.getTag("gui_type") in ["3d_image", "button", "label"]) + except: + return False + + def _restore3DTextTexture(self, text_node): + """恢复3D文本的纹理设置""" + try: + from panda3d.core import TextureStage + + # 如果3D文本已经有字体纹理,确保它使用正确的纹理阶段 + if hasattr(text_node, 'getTexture') and text_node.getTexture(): + # 创建专门用于文本的纹理阶段 + text_stage = TextureStage("text_texture") + text_stage.setSort(10) # 使用较高的纹理槽索引 + text_stage.setMode(TextureStage.MModulate) + + # 重新应用文本纹理 + current_texture = text_node.getTexture() + text_node.setTexture(text_stage, current_texture) + + print(f"已恢复3D文本纹理: {text_node.getName()}") + + except Exception as e: + print(f"恢复3D文本纹理失败: {e}") + + def update2DImageTexture(self, gui_element, image_path): + """更新2D图片纹理""" + try: + # 加载新纹理 + new_texture = self.world.loader.loadTexture(image_path) + if new_texture: + new_texture.setMagfilter(Texture.FT_linear) + new_texture.setMinfilter(Texture.FT_linear_mipmap_linear) + + new_texture.setFormat(Texture.F_rgba) + new_texture.setWrapU(Texture.WM_clamp) + new_texture.setWrapV(Texture.WM_clamp) + + # 应用纹理到模型 + gui_element.setTexture(new_texture, 1) + + # 更新标签 + gui_element.setTag("texture_path", image_path) + gui_element.setTag("image_path", image_path) + + print(f"✅ 2D图像纹理已更新为: {image_path}") + return True + else: + print(f"❌ 无法加载2D图片纹理: {image_path}") + return False + except Exception as e: + print(f"❌ 更新2D图片纹理时出错: {e}") + return False + + # 在gui_manager.py或其他相关文件中添加以下方法 + + def editGUI2DPosition(self, gui_element, axis, value): + """编辑2D GUI元素位置并应用边界约束""" + try: + gui_type = gui_element.getTag("gui_type") + + if gui_type in ["button", "label", "entry", "2d_image","2d_video_screen"]: + # 2D元素使用屏幕坐标,需要转换 + current_pos = gui_element.getPos() + + if axis == "x": + # 转换逻辑坐标到屏幕坐标 + screen_x = value * 0.1 + new_pos = (screen_x,current_pos[1],current_pos[2]) + elif axis == "z": + screen_z = value * 0.1 + new_pos = (current_pos[0],current_pos[1],screen_z) + else: + return False + gui_element.setPos(*new_pos) + return True + else: + print(f"✗ 不支持的GUI类型进行2D位置编辑: {gui_type}") + return False + except Exception as e: + print(f"✗ 更新2D GUI元素位置失败: {e}") + import traceback + traceback.print_exc() + return False + + def editGUI3DPosition(self, gui_element, axis, value): + """编辑3D GUI元素位置""" + try: + gui_type = gui_element.getTag("gui_type") + + if gui_type in ["3d_text", "3d_image", "video_screen","info_panel","info_panel_3d"]: + current_pos = gui_element.getPos() + + if axis == "x": + new_pos = (value, current_pos.getY(), current_pos.getZ()) + elif axis == "y": + new_pos = (current_pos.getX(), value, current_pos.getZ()) + elif axis == "z": + new_pos = (current_pos.getX(), current_pos.getY(), value) + else: + return False + + gui_element.setPos(*new_pos) + #print(f"✓ 更新3D GUI元素位置: {axis}={value}") + return True + else: + print(f"✗ 不支持的GUI类型进行3D位置编辑: {gui_type}") + return False + except Exception as e: + print(f"✗ 更新3D GUI元素位置失败: {e}") + import traceback + traceback.print_exc() + return False + + def editGUIScale(self, gui_element, axis, value): + """编辑GUI元素缩放""" + try: + gui_type = gui_element.getTag("gui_type") + current_scale = gui_element.getScale() + + # 确保缩放值不为0 + if value == 0: + value = 0.01 + + if gui_type in ["3d_text", "3d_image","video_screen","virtual_screen","info_panel","info_panel_3d"]: + # 3D元素处理 + if axis == "x": + new_scale = (value, current_scale.getY(), current_scale.getZ()) + elif axis == "y": + new_scale = (current_scale.getX(), value, current_scale.getZ()) + elif axis == "z": + new_scale = (current_scale.getX(), current_scale.getY(), value) + else: + return False + + gui_element.setScale(*new_scale) + elif gui_type == "2d_image": + # 2D图像特殊处理 - 分别控制宽度和高度 + if axis == "x": + # X轴控制宽度 + gui_element.setScale(value, current_scale.getZ() if hasattr(current_scale, 'getZ') + else current_scale[1] if isinstance(current_scale, (list, tuple)) + else current_scale) + elif axis == "z": + # Z轴控制高度 + gui_element.setScale(current_scale.getX() if hasattr(current_scale, 'getX') + else current_scale[0] if isinstance(current_scale, (list, tuple)) + else current_scale, value) + else: + # 其他情况使用统一缩放 + gui_element.setScale(value) + gui_element.setTransparency(TransparencyAttrib.MAlpha) + else: + # 其他2D元素处理 + if axis in ["x", "z"]: # 对于2D图像,x和z分别代表宽度和高度 + # 保持原有缩放比例,仅调整指定轴 + if axis == "x": + gui_element.setScale(value, + current_scale.getZ() if hasattr(current_scale, 'getZ') + else current_scale[1] if isinstance(current_scale, (list, tuple)) + else current_scale) + elif axis == "z": + gui_element.setScale( + current_scale.getX() if hasattr(current_scale, 'getX') + else current_scale[0] if isinstance(current_scale, (list, tuple)) + else current_scale, value) + else: + # 对于其他2D元素,使用统一缩放 + gui_element.setScale(value) + + return True + except Exception as e: + print(f"✗ 更新GUI元素缩放失败: {e}") + import traceback + traceback.print_exc() + return False + + def createCesiumView(self, main_window=None): + """创建 Cesium 视图窗口(离线版本)""" + if not WEB_ENGINE_AVAILABLE: + print("❌ 无法创建Cesium视图: Web引擎不可用") + return None + + try: + from PyQt5.QtWebEngineWidgets import QWebEngineView + from PyQt5.QtWidgets import QDockWidget + from PyQt5.QtCore import QUrl + import os + + # 尝试获取主窗口引用 + if main_window is None: + print("🔍 尝试获取主窗口引用...") + + # 检查各种可能的主窗口引用 + if hasattr(self.world, 'interface_manager'): + print(f" - interface_manager 存在: {self.world.interface_manager}") + if hasattr(self.world.interface_manager, 'main_window'): + main_window = self.world.interface_manager.main_window + print(f" - interface_manager.main_window: {main_window}") + + if main_window is None and hasattr(self.world, 'main_window'): + main_window = self.world.main_window + print(f" - world.main_window: {main_window}") + + # 如果仍然没有主窗口,尝试从树形控件获取 + if main_window is None and self.world.treeWidget: + try: + main_window = self.world.treeWidget.window() + print(f" - 从 treeWidget 获取窗口: {main_window}") + except: + pass + + if main_window is None: + print("✗ 无法获取主窗口引用") + return None + else: + print(f"✅ 使用传入的主窗口引用: {main_window}") + + # 检查主窗口是否有效 + if not hasattr(main_window, 'addDockWidget'): + print(f"✗ 主窗口引用无效,缺少 addDockWidget 方法") + return None + + # 检查是否已经存在 Cesium 视图 + for element in self.gui_elements: + if hasattr(element, 'objectName') and element.objectName() == "CesiumView": + print("⚠ Cesium 视图已经存在") + # 将其前置显示 + element.show() + element.raise_() + return element + + # 创建停靠窗口 + print(f"🔧 创建 Cesium 停靠窗口,父窗口: {main_window}") + cesium_dock = QDockWidget("Cesium 地图视图(离线)", main_window) + cesium_dock.setObjectName("CesiumView") + + # 创建 Web 视图 + self.cesium_view = QWebEngineView() + + # 使用本地 HTML 文件(离线模式) + local_html_path = os.path.abspath("./cesium_offline.html") + if os.path.exists(local_html_path): + print(f"🌐 加载离线 Cesium: file://{local_html_path}") + self.cesium_view.load(QUrl(f"file://{local_html_path}")) + else: + print("⚠️ 离线文件不存在,使用在线版本") + self.cesium_view.load(QUrl("http://localhost:8080/Apps/HelloWorld.html")) + + # 设置内容 + cesium_dock.setWidget(self.cesium_view) + + # 添加到主窗口 + print("📍 将 Cesium 视图添加到主窗口") + main_window.addDockWidget(Qt.RightDockWidgetArea, cesium_dock) + + # 添加到GUI元素列表以便管理 + self.gui_elements.append(cesium_dock) + + print("✓ Cesium 离线视图已创建并集成到项目中") + return cesium_dock + + except Exception as e: + print(f"✗ 创建 Cesium 视图失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def toggleCesiumView(self): + """切换 Cesium 视图显示状态""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用,无法切换 Cesium 视图") + return None + + try: + # 查找现有的 Cesium 视图 + cesium_dock = None + cesium_index = -1 + for i, element in enumerate(self.gui_elements): + if hasattr(element, 'objectName') and element.objectName() == "CesiumView": + cesium_dock = element + cesium_index = i + break + + # 如果存在则移除,否则创建 + if cesium_dock: + # 获取主窗口引用以正确移除停靠窗口 + main_window = None + if (hasattr(self.world, 'interface_manager') and + hasattr(self.world.interface_manager, 'main_window') and + self.world.interface_manager.main_window): + main_window = self.world.interface_manager.main_window + elif hasattr(self.world, 'main_window') and self.world.main_window: + main_window = self.world.main_window + + if main_window and hasattr(main_window, 'removeDockWidget'): + main_window.removeDockWidget(cesium_dock) + + # 从列表中移除 + if cesium_index >= 0: + self.gui_elements.pop(cesium_index) + + print("✓ Cesium 视图已隐藏") + return None + else: + return self.createCesiumView() + + except Exception as e: + print(f"✗ 切换 Cesium 视图失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def refreshCesiumView(self): + """刷新 Cesium 视图""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用,无法刷新 Cesium 视图") + return False + + try: + for element in self.gui_elements: + if hasattr(element, 'objectName') and element.objectName() == "CesiumView": + web_view = element.widget() + if isinstance(web_view, QWebEngineView): + web_view.reload() + print("✓ Cesium 视图已刷新") + return True + print("⚠ 未找到 Cesium 视图") + return False + except Exception as e: + print(f"✗ 刷新 Cesium 视图失败: {str(e)}") + return False + + def updateCesiumURL(self, url): + """更新 Cesium 视图的 URL""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用,无法更新 Cesium URL") + return False + + try: + for element in self.gui_elements: + if hasattr(element, 'objectName') and element.objectName() == "CesiumView": + web_view = element.widget() + if isinstance(web_view, QWebEngineView): + from PyQt5.QtCore import QUrl + web_view.load(QUrl(url)) + print(f"✓ Cesium URL 已更新为: {url}") + return True + print("⚠ 未找到 Cesium 视图") + return False + except Exception as e: + print(f"✗ 更新 Cesium URL 失败: {str(e)}") + return False + + # 在 GUIManager 类中添加以下方法 + + def addModelToCesium(self, model_id, model_url, longitude, latitude, height=0, scale=1.0): + """向 Cesium 添加模型""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用,无法操作 Cesium") + return False + + try: + # 查找 Cesium 视图 + cesium_view = None + for element in self.gui_elements: + if (hasattr(element, 'objectName') and + element.objectName() == "CesiumView" and + hasattr(element, 'widget')): + cesium_view = element.widget() + break + + if not cesium_view: + print("✗ 未找到 Cesium 视图") + return False + + # 转义特殊字符以防止 JavaScript 语法错误 + escaped_model_id = str(model_id).replace("'", "\\'") + escaped_model_url = str(model_url).replace("'", "\\'").replace("\\", "/") + + # 构造 JavaScript 调用 + js_code = f""" + (function() {{ + if (window.CesiumAPI && typeof window.CesiumAPI.addModel === 'function') {{ + try {{ + var result = window.CesiumAPI.addModel( + '{escaped_model_id}', + '{escaped_model_url}', + {{ + longitude: {longitude}, + latitude: {latitude}, + height: {height} + }}, + {scale} + ); + console.log('Cesium 添加模型结果:', result); + return result || {{success: true, message: 'Model added'}}; + }} catch (error) {{ + console.error('JavaScript 错误:', error); + return {{success: false, message: 'JavaScript error: ' + error.message}}; + }} + }} else {{ + console.error('CesiumAPI.addModel 不可用'); + return {{success: false, message: 'CesiumAPI.addModel not available'}}; + }} + }})(); + """ + + # 定义回调函数处理结果 + def handle_result(result): + try: + if isinstance(result, dict): + if result.get('success', False): + print(f"✓ 成功在 Cesium 中添加模型: {model_id}") + else: + print(f"✗ 在 Cesium 中添加模型失败: {result.get('message', 'Unknown error')}") + else: + print(f"✓ 已发送添加模型请求: {model_id}") + except Exception as callback_error: + print(f"✗ 处理回调结果时出错: {callback_error}") + + # 执行 JavaScript 并获取结果 + cesium_view.page().runJavaScript(js_code, handle_result) + return True + + except Exception as e: + print(f"✗ 添加模型到 Cesium 失败: {e}") + import traceback + traceback.print_exc() + return False + + # 添加新的方法来集成 Panda3D 场景中的 Cesium Tiles + def addCesiumTilesetToScene(self, tileset_name, tileset_url, position=(0, 0, 0)): + """在 Panda3D 场景中添加 Cesium 3D Tiles""" + try: + # 使用场景管理器加载 tileset + tileset_node = self.world.scene_manager.load_cesium_tileset(tileset_url, position) + + if tileset_node: + # 添加到 GUI 元素列表以便管理 + self.gui_elements.append({ + 'type': 'cesium_tileset', + 'name': tileset_name, + 'node': tileset_node, + 'url': tileset_url + }) + + print(f"✓ 在场景中添加 Cesium tileset: {tileset_name}") + return tileset_node + else: + print(f"✗ 在场景中添加 Cesium tileset 失败: {tileset_name}") + return None + + except Exception as e: + print(f"✗ 在场景中添加 Cesium tileset 出错: {e}") + return None + + def removeModelFromCesium(self, model_id): + """从 Cesium 移除模型""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用") + return False + + try: + # 查找 Cesium 视图 + cesium_view = None + for element in self.gui_elements: + if (hasattr(element, 'objectName') and + element.objectName() == "CesiumView" and + hasattr(element, 'widget')): + cesium_view = element.widget() + break + + if not cesium_view: + print("✗ 未找到 Cesium 视图") + return False + + # 构造 JavaScript 调用 + js_code = f""" + if (window.CesiumAPI && typeof window.CesiumAPI.removeModel === 'function') {{ + var result = window.CesiumAPI.removeModel('{model_id}'); + result; + }} else {{ + {{success: false, message: 'CesiumAPI.removeModel not available'}}; + }} + """ + + # 定义回调函数处理结果 + def handle_result(result): + if result and isinstance(result, dict): + if result.get('success', False): + print(f"✓ 成功从 Cesium 中移除模型: {model_id}") + else: + print(f"✗ 从 Cesium 中移除模型失败: {result.get('message', 'Unknown error')}") + else: + print(f"✓ 已发送移除模型请求: {model_id} (无法获取详细结果)") + + # 执行 JavaScript 并获取结果 + cesium_view.page().runJavaScript(js_code, handle_result) + return True + + except Exception as e: + print(f"✗ 从 Cesium 移除模型失败: {e}") + return False + + def updateCesiumModelPosition(self, model_id, longitude, latitude, height=0): + """更新 Cesium 中模型的位置""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用") + return False + + try: + # 查找 Cesium 视图 + cesium_view = None + for element in self.gui_elements: + if (hasattr(element, 'objectName') and + element.objectName() == "CesiumView" and + hasattr(element, 'widget')): + cesium_view = element.widget() + break + + if not cesium_view: + print("✗ 未找到 Cesium 视图") + return False + + # 使用更安全的 JavaScript 字符串构造方式 + escaped_model_id = model_id.replace("'", "\\'") + + # 构造 JavaScript 调用 + js_code = f""" + (function() {{ + if (window.CesiumAPI && typeof window.CesiumAPI.updateModelPosition === 'function') {{ + try {{ + var result = window.CesiumAPI.updateModelPosition( + '{escaped_model_id}', + {{ + longitude: {longitude}, + latitude: {latitude}, + height: {height} + }} + ); + return result || {{success: true, message: 'Position updated'}}; + }} catch (error) {{ + return {{success: false, message: 'JavaScript error: ' + error.message}}; + }} + }} else {{ + return {{success: false, message: 'CesiumAPI.updateModelPosition not available'}}; + }} + }})(); + """ + + # 定义回调函数处理结果 + def handle_result(result): + try: + if isinstance(result, dict): + if result.get('success', False): + print(f"✓ 成功更新 Cesium 中模型位置: {model_id}") + else: + print(f"✗ 更新 Cesium 中模型位置失败: {result.get('message', 'Unknown error')}") + else: + print(f"✓ 已发送更新模型位置请求: {model_id}") + except Exception as callback_error: + print(f"✗ 处理回调结果时出错: {callback_error}") + + # 执行 JavaScript 并获取结果 + cesium_view.page().runJavaScript(js_code, handle_result) + return True + + except Exception as e: + print(f"✗ 更新 Cesium 中模型位置失败: {e}") + return False + + def getAllCesiumModels(self): + """获取 Cesium 中所有模型的列表""" + if not WEB_ENGINE_AVAILABLE: + print("✗ QtWebEngineWidgets 不可用") + return None + + try: + # 查找 Cesium 视图 + cesium_view = None + for element in self.gui_elements: + if (hasattr(element, 'objectName') and + element.objectName() == "CesiumView" and + hasattr(element, 'widget')): + cesium_view = element.widget() + break + + if not cesium_view: + print("✗ 未找到 Cesium 视图") + return None + + # 构造 JavaScript 调用 + js_code = """ + if (window.CesiumAPI && typeof window.CesiumAPI.getAllModels === 'function') { + var result = window.CesiumAPI.getAllModels(); + JSON.stringify(result); + } else { + JSON.stringify({success: false, message: 'CesiumAPI.getAllModels not available'}); + } + """ + + # 定义回调函数处理结果 + def handle_result(result): + try: + if isinstance(result, str): + import json + result = json.loads(result) + + if result and result.get('success', False): + models = result.get('models', []) + print(f"✓ Cesium 中的模型列表: {models}") + return models + else: + print(f"✗ 获取 Cesium 模型列表失败: {result.get('message', 'Unknown error')}") + return [] + except Exception as e: + print(f"✗ 解析 Cesium 模型列表结果失败: {e}") + return [] + + # 执行 JavaScript 并获取结果 + cesium_view.page().runJavaScript(js_code) + return None # 异步操作,实际结果通过回调处理 + + except Exception as e: + print(f"✗ 获取 Cesium 模型列表失败: {e}") + return None + + # 添加一个便捷方法来加载本地模型文件 + def addLocalModelToCesium(self, model_id, local_model_path, longitude, latitude, height=0, scale=1.0): + """向 Cesium 添加本地模型文件""" + try: + # 将本地路径转换为相对路径或 URL + import os + if os.path.exists(local_model_path): + # 如果 Cesium 服务器可以访问该路径,可以直接使用 + # 否则需要将模型文件放在 Cesium 的静态资源目录中 + model_url = local_model_path.replace('\\', '/') # 确保使用正斜杠 + return self.addModelToCesium(model_id, model_url, longitude, latitude, height, scale) + else: + print(f"✗ 模型文件不存在: {local_model_path}") + return False + except Exception as e: + print(f"✗ 添加本地模型失败: {e}") + return False diff --git a/gui_preview_window.py b/gui_preview_window.py index b68a7973..361a4dd2 100644 --- a/gui_preview_window.py +++ b/gui_preview_window.py @@ -1,540 +1,540 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -GUI预览窗口 - 使用现有ShowBase创建额外窗口,专门用来显示GUI元素 -解决Qt集成中2D GUI显示问题,避免多个ShowBase实例错误 -""" - -import warnings -warnings.filterwarnings("ignore", category=DeprecationWarning) - -from direct.gui.DirectGui import * -from panda3d.core import * -from direct.showbase.ShowBase import ShowBase - -class GUIPreviewWindow: - """使用现有ShowBase的额外窗口进行GUI预览""" - - def __init__(self): - self.preview_window = None - self.chinese_font = None - self.gui_elements = [] - self.main_world = None # 主程序world对象的引用 - self.sync_timer = None - - # 预览窗口的渲染节点 - self.preview_render2d = None - self.preview_camera2d = None - self.preview_aspect2d = None - - print("✓ GUI预览窗口类已初始化") - - def set_main_world(self, world): - """设置主程序world引用并初始化预览窗口""" - self.main_world = world - if world: - self.init_preview_window() - self.start_sync_timer() - - def init_preview_window(self): - """使用现有ShowBase创建预览窗口""" - try: - if not self.main_world: - print("错误: 没有主程序world引用") - return False - - # 使用主程序的base创建额外窗口 - base = self.main_world # MyWorld继承自ShowBase - - # 尝试从Qt部件获取渲染区域的实际尺寸 - main_width = 800 # 默认宽度 - main_height = 600 # 默认高度 - - # 检查是否有Qt应用程序实例 - try: - from PyQt5.QtWidgets import QApplication - app = QApplication.instance() - if app: - # 查找主窗口 - for widget in app.topLevelWidgets(): - if hasattr(widget, 'centralWidget'): - central_widget = widget.centralWidget() - if central_widget and hasattr(central_widget, 'world'): - # 找到了Panda3D部件 - qt_width = central_widget.width() - qt_height = central_widget.height() - if qt_width > 0 and qt_height > 0: - main_width = qt_width - main_height = qt_height - print(f"从Qt部件获取尺寸: {main_width} x {main_height}") - break - else: - print("未找到Qt Panda3D部件,使用默认尺寸") - else: - print("未找到Qt应用程序实例,使用默认尺寸") - except ImportError: - print("未找到PyQt5,使用默认尺寸") - except Exception as e: - print(f"从Qt获取尺寸失败: {str(e)},使用默认尺寸") - - # 如果Qt方法失败,尝试从Panda3D窗口获取 - if main_width == 800 and main_height == 600: - main_window = base.win - if main_window and main_window.hasSize(): - panda_width = main_window.getXSize() - panda_height = main_window.getYSize() - if panda_width > 0 and panda_height > 0: - main_width = panda_width - main_height = panda_height - print(f"从Panda3D窗口获取尺寸: {main_width} x {main_height}") - else: - print(f"Panda3D窗口尺寸无效,使用默认尺寸: {main_width} x {main_height}") - - # 设置窗口属性,使用获取到的尺寸 - wp = WindowProperties() - wp.setSize(main_width, main_height) - wp.setTitle("GUI预览窗口 - 2D GUI专用显示") - wp.setOrigin(main_width + 50, 50) # 放在主窗口右侧 - wp.setUndecorated(False) # 保持窗口装饰 - # 注意:WindowProperties没有setMaximized和setMinimized方法 - - print(f"设置预览窗口属性: 尺寸={main_width}x{main_height}, 位置=({main_width + 50}, 50)") - - # 获取图形管道 - pipe = base.pipe - if not pipe: - print("错误: 无法获取图形管道") - return False - - # 创建帧缓冲属性 - fbp = FrameBufferProperties.getDefault() - - # 使用makeOutput创建窗口,强制要求窗口类型 - self.preview_window = base.graphicsEngine.makeOutput( - pipe, "GUI预览窗口", 0, - fbp, wp, GraphicsPipe.BFRequireWindow - ) - - if not self.preview_window: - print("错误: 无法创建预览窗口") - return False - - print(f"预览窗口创建成功,类型: {type(self.preview_window).__name__}") - - # 强制打开窗口 - base.graphicsEngine.openWindows() - - # 验证创建的窗口尺寸 - if hasattr(self.preview_window, 'getXSize'): - actual_width = self.preview_window.getXSize() - actual_height = self.preview_window.getYSize() - print(f"实际创建的预览窗口尺寸: {actual_width} x {actual_height}") - - if actual_width != main_width or actual_height != main_height: - print(f"警告: 窗口尺寸不匹配!期望: {main_width}x{main_height}, 实际: {actual_width}x{actual_height}") - - # 尝试重新设置尺寸 - new_props = WindowProperties() - new_props.setSize(main_width, main_height) - self.preview_window.requestProperties(new_props) - base.graphicsEngine.renderFrame() - - # 再次检查 - final_width = self.preview_window.getXSize() - final_height = self.preview_window.getYSize() - print(f"重新设置后的尺寸: {final_width} x {final_height}") - - # 设置预览窗口的背景色 - base.setBackgroundColor(0.15, 0.15, 0.25, 1.0, self.preview_window) - - # 创建预览窗口的2D渲染节点 - self.preview_render2d = NodePath('preview_render2d') - - # 为预览窗口创建2D相机,使用正确的宽高比 - aspect_ratio = float(main_width) / float(main_height) - self.preview_camera2d = base.makeCamera2d(self.preview_window) - self.preview_camera2d.reparentTo(self.preview_render2d) - - # 设置2D渲染属性 - self.preview_render2d.setDepthWrite(0) - self.preview_render2d.setMaterialOff(1) - self.preview_render2d.setTwoSided(1) - - # 检查窗口类型并设置输入 - print(f"预览窗口类型: {type(self.preview_window)}") - print(f"是否为GraphicsWindow: {hasattr(self.preview_window, 'getNumInputDevices')}") - - # 只为真正的GraphicsWindow设置输入 - if hasattr(self.preview_window, 'getNumInputDevices') and self.preview_window.getNumInputDevices() > 0: - # 设置鼠标和键盘输入 - mk = base.dataRoot.attachNewNode(MouseAndKeyboard(self.preview_window, 0, 'previewMouse')) - mw = mk.attachNewNode(MouseWatcher('previewMouseWatcher')) - self.preview_aspect2d = self.preview_render2d.attachNewNode(PGTop('previewAspect2d')) - self.preview_aspect2d.node().setMouseWatcher(mw.node()) - print("✓ 已设置预览窗口的鼠标和键盘输入") - else: - print("⚠ 跳过鼠标/键盘设置(窗口不支持或没有输入设备)") - # 创建没有输入的aspect2d节点 - self.preview_aspect2d = self.preview_render2d.attachNewNode(PGTop('previewAspect2d')) - - # 加载中文字体 - self.load_font() - - # 创建界面说明 - self.create_ui_info() - - # 最终验证 - if hasattr(self.preview_window, 'getXSize'): - final_width = self.preview_window.getXSize() - final_height = self.preview_window.getYSize() - print(f"✓ GUI预览窗口已成功创建(最终尺寸: {final_width}x{final_height})") - else: - print(f"✓ GUI预览窗口已成功创建(期望尺寸: {main_width}x{main_height})") - - return True - - except Exception as e: - print(f"创建GUI预览窗口失败: {str(e)}") - import traceback - traceback.print_exc() - return False - - def load_font(self): - """加载中文字体""" - try: - if self.main_world and hasattr(self.main_world, 'loader'): - self.chinese_font = self.main_world.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') - if not self.chinese_font: - print("预览窗口: 无法加载中文字体,将使用默认字体") - except: - print("预览窗口: 无法加载中文字体,将使用默认字体") - self.chinese_font = None - - def create_ui_info(self): - """创建界面说明""" - if not self.preview_aspect2d: - return - - # # 创建欢迎信息 - # self.welcome_text = OnscreenText( - # text="GUI预览窗口\n\n这个窗口使用现有的ShowBase\n创建额外的显示窗口\n\n可以正确显示2D GUI元素", - # parent=self.preview_aspect2d, - # pos=(0, 0.2), - # scale=0.08, - # fg=(1, 1, 1, 1), - # align=TextNode.ACenter, - # font=self.chinese_font if self.chinese_font else None - # ) - - # # 创建提示信息 - # self.tip_text = OnscreenText( - # text="这个窗口与主程序共享ShowBase,避免了多实例错误", - # parent=self.preview_aspect2d, - # pos=(0, -0.7), - # scale=0.05, - # fg=(0.8, 0.8, 0.8, 1), - # align=TextNode.ACenter, - # font=self.chinese_font if self.chinese_font else None - # ) - - # # 创建同步状态显示 - # self.sync_text = OnscreenText( - # text="同步状态: 等待检测", - # parent=self.preview_aspect2d, - # pos=(-0.9, 0.8), - # scale=0.04, - # fg=(0, 1, 0, 1), - # align=TextNode.ALeft, - # font=self.chinese_font if self.chinese_font else None - # ) - - def start_sync_timer(self): - """启动同步定时器""" - if self.main_world and hasattr(self.main_world, 'taskMgr'): - # 每0.5秒检查一次GUI元素变化 - self.main_world.taskMgr.doMethodLater(0.5, self.sync_gui_elements, 'sync_preview_gui') - print("✓ GUI同步定时器已启动") - - def sync_gui_elements(self, task=None): - """同步主程序的GUI元素到预览窗口""" - try: - if not self.main_world or not self.preview_aspect2d: - return task.again if task else None - - # 获取主程序中的GUI元素 - main_gui_elements = getattr(self.main_world, 'gui_elements', []) - - # 检查是否需要重新创建所有元素 - current_count = len(main_gui_elements) - preview_count = len(self.gui_elements) - - need_full_sync = False - - # 如果数量有变化,需要完全重新同步 - if current_count != preview_count: - need_full_sync = True - print(f"GUI数量变化: {preview_count} -> {current_count}") - else: - # 检查现有元素的属性是否有变化 - for i, main_element in enumerate(main_gui_elements): - if i < len(self.gui_elements): - if self.has_element_changed(main_element, self.gui_elements[i]): - need_full_sync = True - print(f"GUI元素 {i} 属性发生变化") - break - - # 执行同步 - if need_full_sync: - print("执行完整同步...") - self.clear_preview_elements() - self.create_preview_elements(main_gui_elements) - - # 更新同步状态显示 - if hasattr(self, 'sync_text'): - self.sync_text.setText(f"同步状态: 已同步 {current_count} 个GUI元素") - - print(f"同步完成: {current_count} 个GUI元素") - - return task.again if task else None - - except Exception as e: - print(f"同步GUI元素失败: {str(e)}") - import traceback - traceback.print_exc() - return task.again if task else None - - def has_element_changed(self, main_element, preview_element): - """检查主程序GUI元素是否发生了变化""" - try: - if not hasattr(main_element, 'getTag') or not hasattr(preview_element, 'getTag'): - return True - - # 检查文本是否变化 - main_text = main_element.getTag("gui_text") - preview_text = getattr(preview_element, '_last_synced_text', "") - if main_text != preview_text: - return True - - # 检查位置是否变化 - if hasattr(main_element, 'getPos') and hasattr(preview_element, 'getPos'): - main_pos = main_element.getPos() - preview_pos = getattr(preview_element, '_last_synced_pos', Point3(999, 999, 999)) - if (abs(main_pos.getX() - preview_pos.getX()) > 0.001 or - abs(main_pos.getY() - preview_pos.getY()) > 0.001 or - abs(main_pos.getZ() - preview_pos.getZ()) > 0.001): - return True - - # 检查缩放是否变化 - if hasattr(main_element, 'getScale') and hasattr(preview_element, 'getScale'): - main_scale = main_element.getScale().getX() - preview_scale = getattr(preview_element, '_last_synced_scale', -1) - if abs(main_scale - preview_scale) > 0.001: - return True - - return False - - except Exception as e: - print(f"检查元素变化失败: {str(e)}") - return True # 出错时强制同步 - - def clear_preview_elements(self): - """清空预览窗口中的GUI元素""" - for element in self.gui_elements: - try: - if hasattr(element, 'removeNode'): - element.removeNode() - elif hasattr(element, 'destroy'): - element.destroy() - except: - pass - - self.gui_elements.clear() - - # 如果没有GUI元素了,显示说明文本 - if hasattr(self, 'welcome_text') and self.welcome_text: - self.welcome_text.show() - if hasattr(self, 'tip_text') and self.tip_text: - self.tip_text.show() - - def create_preview_elements(self, main_gui_elements): - """在预览窗口中创建GUI元素""" - if not main_gui_elements or not self.preview_aspect2d: - return - - # 隐藏说明文本 - if hasattr(self, 'welcome_text') and self.welcome_text: - self.welcome_text.hide() - if hasattr(self, 'tip_text') and self.tip_text: - self.tip_text.hide() - - for main_element in main_gui_elements: - try: - preview_element = self.create_preview_element(main_element) - if preview_element: - self.gui_elements.append(preview_element) - except Exception as e: - print(f"创建预览元素失败: {str(e)}") - - def create_preview_element(self, main_element): - """根据主程序GUI元素创建对应的预览元素""" - try: - if not hasattr(main_element, 'getTag'): - return None - - gui_type = main_element.getTag("gui_type") - gui_text = main_element.getTag("gui_text") - - if not gui_type: - return None - - # 获取位置和缩放 - pos = main_element.getPos() if hasattr(main_element, 'getPos') else (0, 0, 0) - scale = main_element.getScale().getX() if hasattr(main_element, 'getScale') else 0.1 - - preview_element = None - - if gui_type == "button": - preview_element = DirectButton( - parent=self.preview_aspect2d, - text=gui_text or "按钮", - pos=pos, - scale=scale, - frameColor=(0.2, 0.6, 0.8, 1), - text_font=self.chinese_font if self.chinese_font else None, - command=lambda: print(f"预览按钮点击: {gui_text}") - ) - - elif gui_type == "label": - preview_element = DirectLabel( - parent=self.preview_aspect2d, - text=gui_text or "标签", - pos=pos, - scale=scale, - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 1, 1), - text_font=self.chinese_font if self.chinese_font else None - ) - - elif gui_type == "entry": - preview_element = DirectEntry( - parent=self.preview_aspect2d, - text="", - pos=pos, - scale=scale, - initialText=gui_text or "输入框", - numLines=1, - width=12, - focus=0, - command=lambda text: print(f"预览输入框: {text}") - ) - - elif gui_type == "slider": - preview_element = DirectSlider( - parent=self.preview_aspect2d, - pos=pos, - scale=scale, - range=(0, 100), - value=50, - frameColor=(0.6, 0.6, 0.6, 1), - thumbColor=(0.2, 0.8, 0.2, 1), - command=lambda: print("预览滑块值变化") - ) - - elif gui_type == "3d_text": - # 3D文本需要特殊处理,在3D空间中显示 - textNode = TextNode(f'preview-3d-text-{len(self.gui_elements)}') - textNode.setText(gui_text or "3D文本") - textNode.setAlign(TextNode.ACenter) - if self.chinese_font: - textNode.setFont(self.chinese_font) - - # 需要一个3D渲染节点 - if not hasattr(self, 'preview_render3d'): - self.preview_render3d = NodePath('preview_render3d') - # 为预览窗口创建3D相机 - camera3d = self.main_world.makeCamera(self.preview_window) - camera3d.reparentTo(self.preview_render3d) - camera3d.setPos(0, -10, 0) - camera3d.lookAt(0, 0, 0) - - preview_element = self.preview_render3d.attachNewNode(textNode) - preview_element.setPos(*pos) - preview_element.setScale(scale) - preview_element.setColor(1, 1, 0, 1) - preview_element.setBillboardAxis() - - elif gui_type == "virtual_screen": - # 虚拟屏幕也需要3D渲染 - if not hasattr(self, 'preview_render3d'): - self.preview_render3d = NodePath('preview_render3d') - camera3d = self.main_world.makeCamera(self.preview_window) - camera3d.reparentTo(self.preview_render3d) - camera3d.setPos(0, -10, 0) - camera3d.lookAt(0, 0, 0) - - cm = CardMaker(f"preview-virtual-screen-{len(self.gui_elements)}") - cm.setFrame(-1, 1, -0.5, 0.5) - preview_element = self.preview_render3d.attachNewNode(cm.generate()) - preview_element.setPos(*pos) - preview_element.setColor(0.2, 0.2, 0.2, 0.8) - preview_element.setTransparency(TransparencyAttrib.MAlpha) - - # 在虚拟屏幕上添加文本 - screenText = TextNode(f'preview-screen-text-{len(self.gui_elements)}') - screenText.setText(gui_text or "虚拟屏幕") - screenText.setAlign(TextNode.ACenter) - if self.chinese_font: - screenText.setFont(self.chinese_font) - screenTextNP = preview_element.attachNewNode(screenText) - screenTextNP.setPos(0, 0.01, 0) - screenTextNP.setScale(0.3) - screenTextNP.setColor(0, 1, 0, 1) - - # 为预览元素记录同步状态,用于变化检测 - if preview_element: - preview_element._last_synced_text = gui_text or "" - preview_element._last_synced_pos = Point3(pos) if hasattr(main_element, 'getPos') else Point3(0, 0, 0) - preview_element._last_synced_scale = scale - print(f"创建预览元素: {gui_type} - {gui_text} (位置: {pos}, 缩放: {scale})") - - return preview_element - - except Exception as e: - print(f"创建预览元素失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def destroy(self): - """销毁预览窗口""" - try: - # 停止同步任务 - if self.main_world and hasattr(self.main_world, 'taskMgr'): - self.main_world.taskMgr.remove('sync_preview_gui') - - # 清空GUI元素 - self.clear_preview_elements() - - # 移除界面元素 - if hasattr(self, 'welcome_text') and self.welcome_text: - self.welcome_text.removeNode() - if hasattr(self, 'tip_text') and self.tip_text: - self.tip_text.removeNode() - if hasattr(self, 'sync_text') and self.sync_text: - self.sync_text.removeNode() - - # 关闭窗口 - if self.preview_window and self.main_world: - self.main_world.closeWindow(self.preview_window) - self.preview_window = None - - print("预览窗口已销毁") - - except Exception as e: - print(f"销毁预览窗口失败: {str(e)}") - -# 测试代码 -if __name__ == "__main__": - print("这是GUI预览窗口模块") +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +GUI预览窗口 - 使用现有ShowBase创建额外窗口,专门用来显示GUI元素 +解决Qt集成中2D GUI显示问题,避免多个ShowBase实例错误 +""" + +import warnings +warnings.filterwarnings("ignore", category=DeprecationWarning) + +from direct.gui.DirectGui import * +from panda3d.core import * +from direct.showbase.ShowBase import ShowBase + +class GUIPreviewWindow: + """使用现有ShowBase的额外窗口进行GUI预览""" + + def __init__(self): + self.preview_window = None + self.chinese_font = None + self.gui_elements = [] + self.main_world = None # 主程序world对象的引用 + self.sync_timer = None + + # 预览窗口的渲染节点 + self.preview_render2d = None + self.preview_camera2d = None + self.preview_aspect2d = None + + print("✓ GUI预览窗口类已初始化") + + def set_main_world(self, world): + """设置主程序world引用并初始化预览窗口""" + self.main_world = world + if world: + self.init_preview_window() + self.start_sync_timer() + + def init_preview_window(self): + """使用现有ShowBase创建预览窗口""" + try: + if not self.main_world: + print("错误: 没有主程序world引用") + return False + + # 使用主程序的base创建额外窗口 + base = self.main_world # MyWorld继承自ShowBase + + # 尝试从Qt部件获取渲染区域的实际尺寸 + main_width = 800 # 默认宽度 + main_height = 600 # 默认高度 + + # 检查是否有Qt应用程序实例 + try: + from PyQt5.QtWidgets import QApplication + app = QApplication.instance() + if app: + # 查找主窗口 + for widget in app.topLevelWidgets(): + if hasattr(widget, 'centralWidget'): + central_widget = widget.centralWidget() + if central_widget and hasattr(central_widget, 'world'): + # 找到了Panda3D部件 + qt_width = central_widget.width() + qt_height = central_widget.height() + if qt_width > 0 and qt_height > 0: + main_width = qt_width + main_height = qt_height + print(f"从Qt部件获取尺寸: {main_width} x {main_height}") + break + else: + print("未找到Qt Panda3D部件,使用默认尺寸") + else: + print("未找到Qt应用程序实例,使用默认尺寸") + except ImportError: + print("未找到PyQt5,使用默认尺寸") + except Exception as e: + print(f"从Qt获取尺寸失败: {str(e)},使用默认尺寸") + + # 如果Qt方法失败,尝试从Panda3D窗口获取 + if main_width == 800 and main_height == 600: + main_window = base.win + if main_window and main_window.hasSize(): + panda_width = main_window.getXSize() + panda_height = main_window.getYSize() + if panda_width > 0 and panda_height > 0: + main_width = panda_width + main_height = panda_height + print(f"从Panda3D窗口获取尺寸: {main_width} x {main_height}") + else: + print(f"Panda3D窗口尺寸无效,使用默认尺寸: {main_width} x {main_height}") + + # 设置窗口属性,使用获取到的尺寸 + wp = WindowProperties() + wp.setSize(main_width, main_height) + wp.setTitle("GUI预览窗口 - 2D GUI专用显示") + wp.setOrigin(main_width + 50, 50) # 放在主窗口右侧 + wp.setUndecorated(False) # 保持窗口装饰 + # 注意:WindowProperties没有setMaximized和setMinimized方法 + + print(f"设置预览窗口属性: 尺寸={main_width}x{main_height}, 位置=({main_width + 50}, 50)") + + # 获取图形管道 + pipe = base.pipe + if not pipe: + print("错误: 无法获取图形管道") + return False + + # 创建帧缓冲属性 + fbp = FrameBufferProperties.getDefault() + + # 使用makeOutput创建窗口,强制要求窗口类型 + self.preview_window = base.graphicsEngine.makeOutput( + pipe, "GUI预览窗口", 0, + fbp, wp, GraphicsPipe.BFRequireWindow + ) + + if not self.preview_window: + print("错误: 无法创建预览窗口") + return False + + print(f"预览窗口创建成功,类型: {type(self.preview_window).__name__}") + + # 强制打开窗口 + base.graphicsEngine.openWindows() + + # 验证创建的窗口尺寸 + if hasattr(self.preview_window, 'getXSize'): + actual_width = self.preview_window.getXSize() + actual_height = self.preview_window.getYSize() + print(f"实际创建的预览窗口尺寸: {actual_width} x {actual_height}") + + if actual_width != main_width or actual_height != main_height: + print(f"警告: 窗口尺寸不匹配!期望: {main_width}x{main_height}, 实际: {actual_width}x{actual_height}") + + # 尝试重新设置尺寸 + new_props = WindowProperties() + new_props.setSize(main_width, main_height) + self.preview_window.requestProperties(new_props) + base.graphicsEngine.renderFrame() + + # 再次检查 + final_width = self.preview_window.getXSize() + final_height = self.preview_window.getYSize() + print(f"重新设置后的尺寸: {final_width} x {final_height}") + + # 设置预览窗口的背景色 + base.setBackgroundColor(0.15, 0.15, 0.25, 1.0, self.preview_window) + + # 创建预览窗口的2D渲染节点 + self.preview_render2d = NodePath('preview_render2d') + + # 为预览窗口创建2D相机,使用正确的宽高比 + aspect_ratio = float(main_width) / float(main_height) + self.preview_camera2d = base.makeCamera2d(self.preview_window) + self.preview_camera2d.reparentTo(self.preview_render2d) + + # 设置2D渲染属性 + self.preview_render2d.setDepthWrite(0) + self.preview_render2d.setMaterialOff(1) + self.preview_render2d.setTwoSided(1) + + # 检查窗口类型并设置输入 + print(f"预览窗口类型: {type(self.preview_window)}") + print(f"是否为GraphicsWindow: {hasattr(self.preview_window, 'getNumInputDevices')}") + + # 只为真正的GraphicsWindow设置输入 + if hasattr(self.preview_window, 'getNumInputDevices') and self.preview_window.getNumInputDevices() > 0: + # 设置鼠标和键盘输入 + mk = base.dataRoot.attachNewNode(MouseAndKeyboard(self.preview_window, 0, 'previewMouse')) + mw = mk.attachNewNode(MouseWatcher('previewMouseWatcher')) + self.preview_aspect2d = self.preview_render2d.attachNewNode(PGTop('previewAspect2d')) + self.preview_aspect2d.node().setMouseWatcher(mw.node()) + print("✓ 已设置预览窗口的鼠标和键盘输入") + else: + print("⚠ 跳过鼠标/键盘设置(窗口不支持或没有输入设备)") + # 创建没有输入的aspect2d节点 + self.preview_aspect2d = self.preview_render2d.attachNewNode(PGTop('previewAspect2d')) + + # 加载中文字体 + self.load_font() + + # 创建界面说明 + self.create_ui_info() + + # 最终验证 + if hasattr(self.preview_window, 'getXSize'): + final_width = self.preview_window.getXSize() + final_height = self.preview_window.getYSize() + print(f"✓ GUI预览窗口已成功创建(最终尺寸: {final_width}x{final_height})") + else: + print(f"✓ GUI预览窗口已成功创建(期望尺寸: {main_width}x{main_height})") + + return True + + except Exception as e: + print(f"创建GUI预览窗口失败: {str(e)}") + import traceback + traceback.print_exc() + return False + + def load_font(self): + """加载中文字体""" + try: + if self.main_world and hasattr(self.main_world, 'loader'): + self.chinese_font = self.main_world.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') + if not self.chinese_font: + print("预览窗口: 无法加载中文字体,将使用默认字体") + except: + print("预览窗口: 无法加载中文字体,将使用默认字体") + self.chinese_font = None + + def create_ui_info(self): + """创建界面说明""" + if not self.preview_aspect2d: + return + + # # 创建欢迎信息 + # self.welcome_text = OnscreenText( + # text="GUI预览窗口\n\n这个窗口使用现有的ShowBase\n创建额外的显示窗口\n\n可以正确显示2D GUI元素", + # parent=self.preview_aspect2d, + # pos=(0, 0.2), + # scale=0.08, + # fg=(1, 1, 1, 1), + # align=TextNode.ACenter, + # font=self.chinese_font if self.chinese_font else None + # ) + + # # 创建提示信息 + # self.tip_text = OnscreenText( + # text="这个窗口与主程序共享ShowBase,避免了多实例错误", + # parent=self.preview_aspect2d, + # pos=(0, -0.7), + # scale=0.05, + # fg=(0.8, 0.8, 0.8, 1), + # align=TextNode.ACenter, + # font=self.chinese_font if self.chinese_font else None + # ) + + # # 创建同步状态显示 + # self.sync_text = OnscreenText( + # text="同步状态: 等待检测", + # parent=self.preview_aspect2d, + # pos=(-0.9, 0.8), + # scale=0.04, + # fg=(0, 1, 0, 1), + # align=TextNode.ALeft, + # font=self.chinese_font if self.chinese_font else None + # ) + + def start_sync_timer(self): + """启动同步定时器""" + if self.main_world and hasattr(self.main_world, 'taskMgr'): + # 每0.5秒检查一次GUI元素变化 + self.main_world.taskMgr.doMethodLater(0.5, self.sync_gui_elements, 'sync_preview_gui') + print("✓ GUI同步定时器已启动") + + def sync_gui_elements(self, task=None): + """同步主程序的GUI元素到预览窗口""" + try: + if not self.main_world or not self.preview_aspect2d: + return task.again if task else None + + # 获取主程序中的GUI元素 + main_gui_elements = getattr(self.main_world, 'gui_elements', []) + + # 检查是否需要重新创建所有元素 + current_count = len(main_gui_elements) + preview_count = len(self.gui_elements) + + need_full_sync = False + + # 如果数量有变化,需要完全重新同步 + if current_count != preview_count: + need_full_sync = True + print(f"GUI数量变化: {preview_count} -> {current_count}") + else: + # 检查现有元素的属性是否有变化 + for i, main_element in enumerate(main_gui_elements): + if i < len(self.gui_elements): + if self.has_element_changed(main_element, self.gui_elements[i]): + need_full_sync = True + print(f"GUI元素 {i} 属性发生变化") + break + + # 执行同步 + if need_full_sync: + print("执行完整同步...") + self.clear_preview_elements() + self.create_preview_elements(main_gui_elements) + + # 更新同步状态显示 + if hasattr(self, 'sync_text'): + self.sync_text.setText(f"同步状态: 已同步 {current_count} 个GUI元素") + + print(f"同步完成: {current_count} 个GUI元素") + + return task.again if task else None + + except Exception as e: + print(f"同步GUI元素失败: {str(e)}") + import traceback + traceback.print_exc() + return task.again if task else None + + def has_element_changed(self, main_element, preview_element): + """检查主程序GUI元素是否发生了变化""" + try: + if not hasattr(main_element, 'getTag') or not hasattr(preview_element, 'getTag'): + return True + + # 检查文本是否变化 + main_text = main_element.getTag("gui_text") + preview_text = getattr(preview_element, '_last_synced_text', "") + if main_text != preview_text: + return True + + # 检查位置是否变化 + if hasattr(main_element, 'getPos') and hasattr(preview_element, 'getPos'): + main_pos = main_element.getPos() + preview_pos = getattr(preview_element, '_last_synced_pos', Point3(999, 999, 999)) + if (abs(main_pos.getX() - preview_pos.getX()) > 0.001 or + abs(main_pos.getY() - preview_pos.getY()) > 0.001 or + abs(main_pos.getZ() - preview_pos.getZ()) > 0.001): + return True + + # 检查缩放是否变化 + if hasattr(main_element, 'getScale') and hasattr(preview_element, 'getScale'): + main_scale = main_element.getScale().getX() + preview_scale = getattr(preview_element, '_last_synced_scale', -1) + if abs(main_scale - preview_scale) > 0.001: + return True + + return False + + except Exception as e: + print(f"检查元素变化失败: {str(e)}") + return True # 出错时强制同步 + + def clear_preview_elements(self): + """清空预览窗口中的GUI元素""" + for element in self.gui_elements: + try: + if hasattr(element, 'removeNode'): + element.removeNode() + elif hasattr(element, 'destroy'): + element.destroy() + except: + pass + + self.gui_elements.clear() + + # 如果没有GUI元素了,显示说明文本 + if hasattr(self, 'welcome_text') and self.welcome_text: + self.welcome_text.show() + if hasattr(self, 'tip_text') and self.tip_text: + self.tip_text.show() + + def create_preview_elements(self, main_gui_elements): + """在预览窗口中创建GUI元素""" + if not main_gui_elements or not self.preview_aspect2d: + return + + # 隐藏说明文本 + if hasattr(self, 'welcome_text') and self.welcome_text: + self.welcome_text.hide() + if hasattr(self, 'tip_text') and self.tip_text: + self.tip_text.hide() + + for main_element in main_gui_elements: + try: + preview_element = self.create_preview_element(main_element) + if preview_element: + self.gui_elements.append(preview_element) + except Exception as e: + print(f"创建预览元素失败: {str(e)}") + + def create_preview_element(self, main_element): + """根据主程序GUI元素创建对应的预览元素""" + try: + if not hasattr(main_element, 'getTag'): + return None + + gui_type = main_element.getTag("gui_type") + gui_text = main_element.getTag("gui_text") + + if not gui_type: + return None + + # 获取位置和缩放 + pos = main_element.getPos() if hasattr(main_element, 'getPos') else (0, 0, 0) + scale = main_element.getScale().getX() if hasattr(main_element, 'getScale') else 0.1 + + preview_element = None + + if gui_type == "button": + preview_element = DirectButton( + parent=self.preview_aspect2d, + text=gui_text or "按钮", + pos=pos, + scale=scale, + frameColor=(0.2, 0.6, 0.8, 1), + text_font=self.chinese_font if self.chinese_font else None, + command=lambda: print(f"预览按钮点击: {gui_text}") + ) + + elif gui_type == "label": + preview_element = DirectLabel( + parent=self.preview_aspect2d, + text=gui_text or "标签", + pos=pos, + scale=scale, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1), + text_font=self.chinese_font if self.chinese_font else None + ) + + elif gui_type == "entry": + preview_element = DirectEntry( + parent=self.preview_aspect2d, + text="", + pos=pos, + scale=scale, + initialText=gui_text or "输入框", + numLines=1, + width=12, + focus=0, + command=lambda text: print(f"预览输入框: {text}") + ) + + elif gui_type == "slider": + preview_element = DirectSlider( + parent=self.preview_aspect2d, + pos=pos, + scale=scale, + range=(0, 100), + value=50, + frameColor=(0.6, 0.6, 0.6, 1), + thumbColor=(0.2, 0.8, 0.2, 1), + command=lambda: print("预览滑块值变化") + ) + + elif gui_type == "3d_text": + # 3D文本需要特殊处理,在3D空间中显示 + textNode = TextNode(f'preview-3d-text-{len(self.gui_elements)}') + textNode.setText(gui_text or "3D文本") + textNode.setAlign(TextNode.ACenter) + if self.chinese_font: + textNode.setFont(self.chinese_font) + + # 需要一个3D渲染节点 + if not hasattr(self, 'preview_render3d'): + self.preview_render3d = NodePath('preview_render3d') + # 为预览窗口创建3D相机 + camera3d = self.main_world.makeCamera(self.preview_window) + camera3d.reparentTo(self.preview_render3d) + camera3d.setPos(0, -10, 0) + camera3d.lookAt(0, 0, 0) + + preview_element = self.preview_render3d.attachNewNode(textNode) + preview_element.setPos(*pos) + preview_element.setScale(scale) + preview_element.setColor(1, 1, 0, 1) + preview_element.setBillboardAxis() + + elif gui_type == "virtual_screen": + # 虚拟屏幕也需要3D渲染 + if not hasattr(self, 'preview_render3d'): + self.preview_render3d = NodePath('preview_render3d') + camera3d = self.main_world.makeCamera(self.preview_window) + camera3d.reparentTo(self.preview_render3d) + camera3d.setPos(0, -10, 0) + camera3d.lookAt(0, 0, 0) + + cm = CardMaker(f"preview-virtual-screen-{len(self.gui_elements)}") + cm.setFrame(-1, 1, -0.5, 0.5) + preview_element = self.preview_render3d.attachNewNode(cm.generate()) + preview_element.setPos(*pos) + preview_element.setColor(0.2, 0.2, 0.2, 0.8) + preview_element.setTransparency(TransparencyAttrib.MAlpha) + + # 在虚拟屏幕上添加文本 + screenText = TextNode(f'preview-screen-text-{len(self.gui_elements)}') + screenText.setText(gui_text or "虚拟屏幕") + screenText.setAlign(TextNode.ACenter) + if self.chinese_font: + screenText.setFont(self.chinese_font) + screenTextNP = preview_element.attachNewNode(screenText) + screenTextNP.setPos(0, 0.01, 0) + screenTextNP.setScale(0.3) + screenTextNP.setColor(0, 1, 0, 1) + + # 为预览元素记录同步状态,用于变化检测 + if preview_element: + preview_element._last_synced_text = gui_text or "" + preview_element._last_synced_pos = Point3(pos) if hasattr(main_element, 'getPos') else Point3(0, 0, 0) + preview_element._last_synced_scale = scale + print(f"创建预览元素: {gui_type} - {gui_text} (位置: {pos}, 缩放: {scale})") + + return preview_element + + except Exception as e: + print(f"创建预览元素失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def destroy(self): + """销毁预览窗口""" + try: + # 停止同步任务 + if self.main_world and hasattr(self.main_world, 'taskMgr'): + self.main_world.taskMgr.remove('sync_preview_gui') + + # 清空GUI元素 + self.clear_preview_elements() + + # 移除界面元素 + if hasattr(self, 'welcome_text') and self.welcome_text: + self.welcome_text.removeNode() + if hasattr(self, 'tip_text') and self.tip_text: + self.tip_text.removeNode() + if hasattr(self, 'sync_text') and self.sync_text: + self.sync_text.removeNode() + + # 关闭窗口 + if self.preview_window and self.main_world: + self.main_world.closeWindow(self.preview_window) + self.preview_window = None + + print("预览窗口已销毁") + + except Exception as e: + print(f"销毁预览窗口失败: {str(e)}") + +# 测试代码 +if __name__ == "__main__": + print("这是GUI预览窗口模块") print("需要与主程序集成使用,不能独立运行") \ No newline at end of file diff --git a/main.py b/main.py index 944f8acf..bf0e87ea 100644 --- a/main.py +++ b/main.py @@ -1,818 +1,818 @@ -import warnings - -from core.Command_System import CommandManager -from core.InfoPanelManager import InfoPanelManager -from core.patrol_system import PatrolSystem -from demo.video_integration import VideoManager - -warnings.filterwarnings("ignore", category=DeprecationWarning) - -import sys -import builtins -from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction, - QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem, - QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QTreeView, QInputDialog, QFileDialog, QMessageBox, QDialog, QGroupBox, QHBoxLayout, QPushButton, QDialogButtonBox) -from PyQt5.QtCore import Qt, QDir, QUrl -from PyQt5.QtGui import QDrag, QPainter, QPixmap -from PyQt5.QtWidgets import QFileSystemModel -from QMeta3D.QMeta3DWidget import QMeta3DWidget -from panda3d.core import loadPrcFileData -loadPrcFileData("", "assertions 0") -from core.world import CoreWorld -from core.selection import SelectionSystem -from core.event_handler import EventHandler -from core.tool_manager import ToolManager -from core.script_system import ScriptManager -from core.patrol_system import PatrolSystem -from core.Command_System import CommandManager -from gui.gui_manager import GUIManager -from core.terrain_manager import TerrainManager -from scene.scene_manager import SceneManager -from project.project_manager import ProjectManager -from ui.widgets import CustomMeta3DWidget, CustomFileView, CustomTreeWidget -from ui.property_panel import PropertyPanelManager -from ui.interface_manager import InterfaceManager -from panda3d.core import (CardMaker, Vec4, Vec3, ColorAttrib, MaterialAttrib, - GeomNode, NodePath, Material, CollisionTraverser, - CollisionHandlerQueue, CollisionNode, CollisionRay, - Plane, Point3, Point2, BitMask32, CollisionSphere, - CollisionPlane, ModelPool, Filename, ModelRoot, - AmbientLight, DirectionalLight, TextureAttrib, DriveInterface, WindowProperties) -from panda3d.egg import EggData, EggVertexPool, EggPolygon -from direct.task import Task -from direct.task.TaskManagerGlobal import taskMgr -from direct.showbase.ShowBase import ShowBase -from direct.showbase.DirectObject import DirectObject -from direct.showbase.ShowBaseGlobal import globalClock, aspect2d -import os -import json -import datetime -from direct.actor.Actor import Actor -from PyQt5.sip import wrapinstance -#from RenderPipelineFile.toolkit.material_editor.main import MaterialEditor - -class MyWorld(CoreWorld): - def __init__(self): - super().__init__() - - # 初始化选择和变换系统 - self.selection = SelectionSystem(self) - - # 绑定F键用于聚焦选中节点 - self.accept("f", self.onFocusKeyPressed) - self.accept("F", self.onFocusKeyPressed) # 大写F - - #初始化巡检系统 - self.patrol_system = PatrolSystem(self) - self.accept("p",self.onPatrolKeyPressed) - self.accept("P",self.onPatrolKeyPressed) - - # 初始化事件处理系统 - self.event_handler = EventHandler(self) - - # 初始化工具管理系统 - self.tool_manager = ToolManager(self) - - # 初始化脚本管理系统 - self.script_manager = ScriptManager(self) - - # 初始化GUI管理系统 - self.gui_manager = GUIManager(self) - - # 初始化视频管理 - self.video_manager = VideoManager(self) - - # 初始化场景管理系统 - self.scene_manager = SceneManager(self) - - # 初始化项目管理系统 - self.project_manager = ProjectManager(self) - - # 初始化属性面板管理系统 - self.property_panel = PropertyPanelManager(self) - - # 初始化界面管理系统 - self.interface_manager = InterfaceManager(self) - - - # 启动脚本系统 - self.script_manager.start_system() - - #self.material_editor = None - self.terrain_manager = TerrainManager(self) - - self.terrain_edit_radius = 3.0 - self.terrain_edit_strength=0.3 - self.terrain_edit_operation = "add" - - self.info_panel_manager = InfoPanelManager(self) - - self.command_manager = CommandManager() - - # 初始化碰撞管理器 - from core.collision_manager import CollisionManager - self.collision_manager = CollisionManager(self) - - # 初始化VR管理器 - try: - from core.vr import VRManager - self.vr_manager = VRManager(self) - print("✓ VR管理器初始化完成") - except Exception as e: - print(f"⚠ VR管理器初始化失败: {e}") - self.vr_manager = None - - # 调试选项 - self.debug_collision = True # 是否显示碰撞体 - - # 默认启用模型间碰撞检测(可选) - self.enableModelCollisionDetection(enable=True, frequency=0.1, threshold=0.5) - - try: - self.property_panel = PropertyPanelManager(self) - print("✓ 属性面板管理器初始化完成") - except Exception as e: - print(f"⚠ 属性面板管理器初始化失败: {e}") - import traceback - traceback.print_exc() - self.property_panel = None - - print("✓ MyWorld 初始化完成") - print("✅ 碰撞管理器已初始化") - - # ==================== 兼容性属性 ==================== - - # 保留models属性以兼容现有代码 - @property - def models(self): - """模型列表的兼容性属性""" - return self.scene_manager.models - - @models.setter - def models(self, value): - """模型列表的兼容性设置器""" - self.scene_manager.models = value - - # 保留gui_elements属性以兼容现有代码 - @property - def gui_elements(self): - """GUI元素列表的兼容性属性""" - return self.gui_manager.gui_elements - - @gui_elements.setter - def gui_elements(self, value): - """GUI元素列表的兼容性设置器""" - self.gui_manager.gui_elements = value - - # 保留gui_elements属性以兼容现有代码 - @property - def Spotlight(self): - """GUI元素列表的兼容性属性""" - return self.scene_manager.Spotlight - - @Spotlight.setter - def Spotlight(self, value): - """GUI元素列表的兼容性设置器""" - self.scene_manager.Spotlight = value - - @property - def Pointlight(self): - return self.scene_manager.Pointlight - - @Pointlight.setter - def Pointlight(self,value): - self.scene_manager.Pointlight = value - - - # 保留guiEditMode属性以兼容现有代码 - @property - def guiEditMode(self): - """GUI编辑模式的兼容性属性""" - return self.gui_manager.guiEditMode - - @guiEditMode.setter - def guiEditMode(self, value): - """GUI编辑模式的兼容性设置器""" - self.gui_manager.guiEditMode = value - - # 保留currentTool属性以兼容现有代码 - @property - def currentTool(self): - """当前工具的兼容性属性""" - return self.tool_manager.currentTool - - @currentTool.setter - def currentTool(self, value): - """当前工具的兼容性设置器""" - self.tool_manager.currentTool = value - - # 保留treeWidget属性以兼容现有代码 - @property - def treeWidget(self): - """树形控件的兼容性属性""" - return self.interface_manager.treeWidget - - @treeWidget.setter - def treeWidget(self, value): - """树形控件的兼容性设置器""" - self.interface_manager.treeWidget = value - - #保留terrains属性以兼容现有代码 - @property - def terrains(self): - """地形列表的兼容性属性""" - return self.terrain_manager.terrains - - @terrains.setter - def terrains(self,value): - """地形列表的兼容性设置器""" - self.terrain_manager.terrains = value - - # ==================== GUI管理功能代理 ==================== - - # GUI元素创建方法 - 代理到gui_manager - def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1): - """创建2D GUI按钮""" - return self.gui_manager.createGUIButton(pos, text, size) - - def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08): - """创建2D GUI标签""" - return self.gui_manager.createGUILabel(pos, text, size) - - def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08): - """创建2D GUI文本输入框""" - return self.gui_manager.createGUIEntry(pos, placeholder, size) - - def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=1): - """创建3D空间文本""" - return self.gui_manager.createGUI3DText(pos, text, size) - - def createGUI3DImage(self,pos=(0,0,0),text="3D图片",size=(1,1,1)): - """创建3D图片""" - return self.gui_manager.createGUI3DImage(pos,text,size) - - def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=2): - """创建2D GUI图片""" - return self.gui_manager.createGUI2DImage(pos, image_path, size) - - def createVideoScreen(self,pos=(0,0,0),size=1,video_path=None): - """创建视频屏幕""" - return self.gui_manager.createVideoScreen(pos,size,video_path) - - def create2DVideoScreen(self,pos=(0,0,0),size=0.2,video_path=None): - """创建2D视频屏幕""" - return self.gui_manager.createGUI2DVideoScreen(pos,size,video_path) - - def createSphericalVideo(self,pos=(0,0,0),radius=5.0,video_path=None): - """创建360度视频""" - return self.gui_manager.createSphericalVideo(pos,radius,video_path) - - def playSphericalVideo(self,spherical_video_node): - return self.gui_manager.playSphericalVideo(spherical_video_node) - - def pauseSphericalVideo(self,spherical_video_node): - return self.gui_manager.pauseSphericalVideo(spherical_video_node) - - def setSphericalVideoTime(self,spherical_video_node,time_seconds): - return self.gui_manager.setSphericalVideoTime(spherical_video_node,time_seconds) - - def createSpotLight(self,pos=(0,0,5)): - """创建聚光灯""" - return self.scene_manager.createSpotLight(pos) - - def createPointLight(self,pos=(0,0,5)): - """创建点光源""" - return self.scene_manager.createPointLight(pos) - - def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"): - """创建3D虚拟屏幕""" - return self.gui_manager.createGUIVirtualScreen(pos, size, text) - - def createGUISlider(self, pos=(0, 0, 0), text="滑块", scale=0.3): - """创建2D GUI滑块""" - return self.gui_manager.createGUISlider(pos, text, scale) - - # GUI元素管理方法 - 代理到gui_manager - def deleteGUIElement(self, gui_element): - """删除GUI元素""" - return self.gui_manager.deleteGUIElement(gui_element) - - def editGUIElement(self, gui_element, property_name, value): - """编辑GUI元素属性""" - return self.gui_manager.editGUIElement(gui_element, property_name, value) - - def duplicateGUIElement(self, gui_element): - """复制GUI元素""" - return self.gui_manager.duplicateGUIElement(gui_element) - - def editGUIElementDialog(self, gui_element): - """显示GUI元素编辑对话框""" - return self.gui_manager.editGUIElementDialog(gui_element) - - # GUI事件处理方法 - 代理到gui_manager - def onGUIButtonClick(self, button_id): - """GUI按钮点击事件处理""" - return self.gui_manager.onGUIButtonClick(button_id) - - def onGUIEntrySubmit(self, text, entry_id): - """GUI输入框提交事件处理""" - return self.gui_manager.onGUIEntrySubmit(text, entry_id) - - # GUI编辑模式方法 - 代理到gui_manager - def toggleGUIEditMode(self): - """切换GUI编辑模式""" - return self.gui_manager.toggleGUIEditMode() - - def enterGUIEditMode(self): - """进入GUI编辑模式""" - return self.gui_manager.enterGUIEditMode() - - def exitGUIEditMode(self): - """退出GUI编辑模式""" - return self.gui_manager.exitGUIEditMode() - - def createGUIEditPanel(self): - """创建GUI编辑面板""" - return self.gui_manager.createGUIEditPanel() - - def openGUIPreviewWindow(self): - """打开独立的GUI预览窗口""" - return self.gui_manager.openGUIPreviewWindow() - - def closeGUIPreviewWindow(self): - """关闭GUI预览窗口""" - return self.gui_manager.closeGUIPreviewWindow() - - # GUI工具和选择方法 - 代理到gui_manager - def setGUICreateTool(self, tool_type): - """设置GUI创建工具""" - return self.gui_manager.setGUICreateTool(tool_type) - - def deleteSelectedGUI(self): - """删除选中的GUI元素""" - return self.gui_manager.deleteSelectedGUI() - - def copySelectedGUI(self): - """复制选中的GUI元素""" - return self.gui_manager.copySelectedGUI() - - def handleGUIEditClick(self, hitPos): - """处理GUI编辑模式下的点击""" - return self.gui_manager.handleGUIEditClick(hitPos) - - def createGUIAtPosition(self, world_pos, gui_type): - """在指定位置创建GUI元素""" - return self.gui_manager.createGUIAtPosition(world_pos, gui_type) - - def findClickedGUI(self, hitNode): - """查找被点击的GUI元素""" - return self.gui_manager.findClickedGUI(hitNode) - - def selectGUIInTree(self, gui_element): - """在树形控件中选中GUI元素""" - return self.gui_manager.selectGUIInTree(gui_element) - - def updateGUISelection(self, gui_element): - """更新GUI元素选择状态""" - return self.gui_manager.updateGUISelection(gui_element) - - # GUI属性面板方法 - 代理到gui_manager - def selectGUIColor(self, gui_element): - """选择GUI元素颜色""" - return self.gui_manager.selectGUIColor(gui_element) - - def editGUI2DPosition(self, gui_element, axis, value): - """编辑2D GUI元素位置""" - return self.gui_manager.editGUI2DPosition(gui_element, axis, value) - - # ==================== 事件处理代理 ==================== - - def onTreeItemClicked(self, item, column): - """处理树形控件项目点击事件 - 代理到interface_manager""" - return self.interface_manager.onTreeItemClicked(item, column) - - def setTreeWidget(self, treeWidget): - """设置树形控件引用 - 代理到interface_manager""" - return self.interface_manager.setTreeWidget(treeWidget) - - def mousePressEventLeft(self, evt): - """处理鼠标左键按下事件 - 代理到event_handler""" - return self.event_handler.mousePressEventLeft(evt) - - def mouseReleaseEventLeft(self, evt): - """处理鼠标左键释放事件 - 代理到event_handler""" - return self.event_handler.mouseReleaseEventLeft(evt) - - def wheelForward(self, data=None): - """处理滚轮向前滚动(前进) - 代理到event_handler""" - return self.event_handler.wheelForward(data) - - def wheelBackward(self, data=None): - """处理滚轮向后滚动(后退) - 代理到event_handler""" - return self.event_handler.wheelBackward(data) - - def mousePressEventMiddle(self, evt): - """处理鼠标中键按下事件 - 代理到event_handler""" - return self.event_handler.mousePressEventMiddle(evt) - - def mouseReleaseEventMiddle(self, evt): - """处理鼠标中键释放事件 - 代理到event_handler""" - return self.event_handler.mouseReleaseEventMiddle(evt) - - def mouseMoveEvent(self, evt): - """处理鼠标移动事件 - 代理到event_handler""" - return self.event_handler.mouseMoveEvent(evt) - - # ==================== 射线显示控制 ==================== - - def toggleRayDisplay(self): - """切换射线显示状态 - 代理到event_handler""" - return self.event_handler.toggleRayDisplay() - - def setRayDisplay(self, show=True): - """设置射线显示状态 - 代理到event_handler""" - self.event_handler.showRay = show - if not show: - self.event_handler.clearRay() - return show - - def getRayDisplay(self): - """获取射线显示状态 - 代理到event_handler""" - return self.event_handler.showRay - - def setRayLifetime(self, seconds): - """设置射线显示时长(秒) - 代理到event_handler""" - self.event_handler.rayLifetime = seconds - print(f"射线显示时长设置为: {seconds}秒") - - def getRayLifetime(self): - """获取射线显示时长 - 代理到event_handler""" - return self.event_handler.rayLifetime - - # ==================== 属性面板代理 ==================== - - def setPropertyLayout(self, layout): - """设置属性面板布局引用 - 代理到property_panel""" - return self.property_panel.setPropertyLayout(layout) - - def clearPropertyPanel(self): - """清空属性面板 - 代理到property_panel""" - return self.property_panel.clearPropertyPanel() - - def updatePropertyPanel(self, item): - """更新属性面板显示 - 代理到property_panel""" - return self.property_panel.updatePropertyPanel(item) - - def updateGUIPropertyPanel(self, gui_element): - """更新GUI元素属性面板 - 代理到property_panel""" - return self.property_panel.updateGUIPropertyPanel(gui_element) - - def removeActorForModel(self,model): - return self.property_panel.removeActorForModel( model) - - def updateNodeVisibilityAfterDrag(self,item): - return self.property_panel.updateNodeVisibilityAfterDrag(item) - - # ==================== 工具管理代理 ==================== - - def setCurrentTool(self, tool): - """设置当前工具 - 代理到tool_manager""" - return self.tool_manager.setCurrentTool(tool) - - # ==================== 场景管理功能代理 ==================== - - # 模型导入和处理方法 - 代理到scene_manager - def importModel(self, filepath): - """导入模型到场景""" - # 自动转换FBX等格式为GLB以获得更好的动画支持(不再弹窗询问) - # 如果你不想自动转换,可以将 auto_convert 改为 False - auto_convert = False - - print(f"[模型导入] 文件: {filepath}, 自动转换GLB: {auto_convert}") - - return self.scene_manager.importModel(filepath, auto_convert_to_glb=auto_convert) - - def importModelAsync(self, filepath): - """异步导入模型""" - return self.scene_manager.importModelAsync(filepath) - - # 材质和几何体处理方法 - 代理到scene_manager - def processMaterials(self, model): - """处理模型材质""" - return self.scene_manager.processMaterials(model) - - def processModelGeometry(self, model): - """处理模型几何体""" - return self.scene_manager.processModelGeometry(model) - - # 碰撞系统方法 - 代理到scene_manager - def setupCollision(self, model): - """为模型设置碰撞检测""" - return self.scene_manager.setupCollision(model) - - # 场景树管理方法 - 代理到scene_manager - def updateSceneTree(self): - """更新场景树显示""" - return self.scene_manager.updateSceneTree() - - # 场景保存和加载方法 - 代理到scene_manager - def saveScene(self, filename): - """保存场景到BAM文件""" - return self.scene_manager.saveScene(filename) - - def loadScene(self, filename): - """从BAM文件加载场景""" - return self.scene_manager.loadScene(filename) - - # 模型管理方法 - 代理到scene_manager - def deleteModel(self, model): - """删除模型""" - return self.scene_manager.deleteModel(model) - - def clearAllModels(self): - """清除所有模型""" - return self.scene_manager.clearAllModels() - - def getModels(self): - """获取模型列表""" - return self.scene_manager.getModels() - - def getModelCount(self): - """获取模型数量""" - return self.scene_manager.getModelCount() - - def findModelByName(self, name): - """根据名称查找模型""" - return self.scene_manager.findModelByName(name) - - # ==================== 脚本系统功能代理 ==================== - - # 脚本系统控制方法 - 代理到script_manager - def startScriptSystem(self): - """启动脚本系统""" - return self.script_manager.start_system() - - def stopScriptSystem(self): - """停止脚本系统""" - return self.script_manager.stop_system() - - def enableHotReload(self, enabled=True): - """启用/禁用热重载""" - self.script_manager.hot_reload_enabled = enabled - if enabled: - self.script_manager.start_hot_reload() - else: - self.script_manager.stop_hot_reload() - - # 脚本创建和加载方法 - 代理到script_manager - def createScript(self, script_name, template="basic"): - """创建新脚本文件""" - return self.script_manager.create_script_file(script_name, template) - - def loadScript(self, script_path): - """从文件加载脚本""" - return self.script_manager.load_script_from_file(script_path) - - def loadAllScripts(self, directory=None): - """从目录加载所有脚本""" - return self.script_manager.load_all_scripts_from_directory(directory) - - def reloadScript(self, script_name): - """重新加载脚本""" - return self.script_manager.reload_script(script_name) - - # 脚本挂载和管理方法 - 代理到script_manager - def addScript(self, game_object, script_name): - """为游戏对象添加脚本""" - return self.script_manager.add_script_to_object(game_object, script_name) - - def removeScript(self, game_object, script_name): - """从游戏对象移除脚本""" - return self.script_manager.remove_script_from_object(game_object, script_name) - - def getScripts(self, game_object): - """获取对象上的所有脚本""" - return self.script_manager.get_scripts_on_object(game_object) - - def getScript(self, game_object, script_name): - """获取对象上的特定脚本""" - return self.script_manager.get_script_on_object(game_object, script_name) - - # 脚本信息查询方法 - 代理到script_manager - def getAvailableScripts(self): - """获取所有可用的脚本名称""" - return self.script_manager.get_available_scripts() - - def getScriptInfo(self, script_name): - """获取脚本信息""" - return self.script_manager.get_script_info(script_name) - - def listAllScripts(self): - """列出所有脚本信息""" - return self.script_manager.list_all_scripts() - - def loadCesiumTileset(self,tileset_url,position=(0,0,0)): - return self.scene_manager.load_cesium_tileset(tileset_url,position) - - def addCesiumTileset(self,name,url,position=(0,0,0)): - if hasattr(self,'gui_manager') and self.gui_manager: - return self.gui_manager.addCesiumTilesetToScene(name,url,position) - else: - return self.scene_manager.load_cesium_tileset(url,position) - - # ==================== 地形管理功能代理 ==================== - # 地形创建方法 - 代理到terrain_manager - def createTerrainFromHeightMap(self, heightmap_path, scale=(1, 1, 1)): - """从高度图创建地形""" - return self.terrain_manager.createTerrainFromHeightMap(heightmap_path, scale) - - def createFlatTerrain(self, size=(0.3, 0.3), resolution=256): - """创建平面地形""" - return self.terrain_manager.createFlatTerrain(size, resolution) - - def updateTerrain(self): - """更新所有地形的LOD""" - return self.terrain_manager.updateTerrain() - - def getTerrainHeight(self, terrain_info, x, y): - """获取地形上指定点的高度""" - return self.terrain_manager.getTerrainHeight(terrain_info, x, y) - - def setTerrainEditParameters(self,radius=None,strength=None,operation=None): - if radius is not None: - self.terrain_edit_radius = float(radius) - if strength is not None: - self.terrain_edit_strenght = float(strength) - if operation is not None: - self.terrain_edit_operation = operation - print(f"地形编辑参数已更新: 半径={self.terrain_edit_radius}, 强度={self.terrain_edit_strength}, 操作={self.terrain_edit_operation}") - def getTerrainEditParameters(self): - """获取当前地形编辑参数""" - return { - 'radius': self.terrain_edit_radius, - 'strength': self.terrain_edit_strength, - 'operation': self.terrain_edit_operation - } - - def modifyTerrainHeight(self, terrain_info, x, y, radius, strength, operation="add"): - """修改地形高度的便捷方法""" - if hasattr(self, 'terrain_manager'): - return self.terrain_manager.modifyTerrainHeight(terrain_info, x, y, radius, strength, operation) - return False - - # 添加碰撞管理相关的代理方法 - def enableModelCollisionDetection(self, enable=True, frequency=0.1, threshold=0.5): - """启用模型间碰撞检测""" - return self.collision_manager.enableModelCollisionDetection(enable, frequency, threshold) - - def detectModelCollisions(self, specific_models=None, log_results=True): - """检测模型间碰撞""" - return self.collision_manager.detectModelCollisions(specific_models, log_results) - - def getCollisionHistory(self, limit=None): - """获取碰撞历史""" - return self.collision_manager.getCollisionHistory(limit) - - def getCollisionStatistics(self): - """获取碰撞统计""" - return self.collision_manager.getCollisionStatistics() - - def setupKeyboardEvents(self): - """设置键盘事件""" - try: - # 绑定 F 键用于聚焦选中节点 - self.accept("f", self.onFocusKeyPressed) - self.accept("F", self.onFocusKeyPressed) # 大写F - - print("✓ 键盘事件绑定完成") - - except Exception as e: - print(f"设置键盘事件失败: {e}") - - def onFocusKeyPressed(self): - """处理 F 键按下事件""" - try: - #print("检测到 F 键按下") - - # 检查是否有选中的节点 - if hasattr(self, 'selection') and self.selection.selectedNode: - #print(f"当前选中节点: {self.selection.selectedNode.getName()}") - # 调用选择系统的聚焦功能(可以选择带动画或不带动画的版本) - # self.selection.focusCameraOnSelectedNode() # 无动画版本 - self.selection.focusCameraOnSelectedNodeAdvanced() # 带动画版本 - else: - print("当前没有选中任何节点") - - except Exception as e: - print(f"处理 F 键事件失败: {e}") - - def onPatrolKeyPressed(self): - """处理 P 键按下事件 - 控制巡检系统""" - try: - print("检测到 P 键按下") - - if not self.patrol_system.is_patrolling: - # 如果巡检系统没有点,创建默认巡检路线 - if not self.patrol_system.patrol_points: - self.createDefaultPatrolRoute() - - # 开始巡检 - if self.patrol_system.start_patrol(): - print("✓ 巡检已开始") - else: - print("✗ 巡检启动失败") - else: - # 停止巡检 - if self.patrol_system.stop_patrol(): - print("✓ 巡检已停止") - else: - print("✗ 巡检停止失败") - - except Exception as e: - print(f"处理 P 键事件失败: {e}") - - def createDefaultPatrolRoute(self): - """创建默认巡检路线(使用自动朝向)""" - try: - # 清空现有巡检点 - self.patrol_system.clear_patrol_points() - - # 添加巡检点,使用None表示朝向下一个点 - self.patrol_system.add_patrol_point((0, -10, 2), (0,0,0), 1.5) - self.patrol_system.add_patrol_point((10, -10, 2), (0,0,0), 1.5) - self.patrol_system.add_patrol_point((10, 5, 2), (0,0,0), 1.5) - self.patrol_system.add_patrol_point((10, 0, 5), (0,0,0), 1.5) - - # 最后一个点可以指定特定的朝向,或者也设为None继续循环 - self.patrol_system.add_patrol_point((0, -10, 2), None, 2.5) - - print("✓ 默认自动朝向巡检路线已创建") - self.patrol_system.list_patrol_points() - - except Exception as e: - print(f"创建默认自动朝向巡检路线失败: {e}") - - def _serializeNode(self, node): - """序列化节点数据""" - try: - return self.world.scene_manager.serializeNode(node) - except Exception as e: - print(f"序列化节点失败: {e}") - return None - - def _deserializeNode(self, node_data, parent_node): - """反序列化节点数据""" - try: - return self.world.scene_manager.deserializeNode(node_data, parent_node) - except Exception as e: - print(f"反序列化节点失败: {e}") - return None - - -# ==================== 项目管理功能代理 ==================== -# 以下函数代理到project_manager模块的对应功能 - -def updateWindowTitle(window, project_name=None): - """更新窗口标题 - 代理到project_manager""" - from project.project_manager import updateWindowTitle as pm_updateWindowTitle - return pm_updateWindowTitle(window, project_name) - -def createNewProject(parent_window): - """创建新项目 - 代理到project_manager""" - world = parent_window.centralWidget().world - return world.project_manager.createNewProject(parent_window) - -def saveProject(appw): - """保存项目 - 代理到project_manager""" - - world = appw.centralWidget().world - return world.project_manager.saveProject(appw) - -def openProject(appw): - """打开项目 - 代理到project_manager""" - world = appw.centralWidget().world - return world.project_manager.openProject(appw) - -def openProjectForPath(project_path, appw): - """打开项目 - 代理到project_manager""" - world = appw.centralWidget().world - return world.project_manager.openProjectForPath(project_path, appw) - -def buildPackage(appw): - """打包项目 - 代理到project_manager""" - world = appw.centralWidget().world - return world.project_manager.buildPackage(appw) - -def run(args = None): - world = MyWorld() - - # 使用新的UI模块创建主窗口 - from ui.main_window import setup_main_window - print(f'Path is {args}') - app, main_window = setup_main_window(world, args) - - # 启动应用程序 - sys.exit(app.exec_()) - -if __name__ == "__main__": +import warnings + +from core.Command_System import CommandManager +from core.InfoPanelManager import InfoPanelManager +from core.patrol_system import PatrolSystem +from demo.video_integration import VideoManager + +warnings.filterwarnings("ignore", category=DeprecationWarning) + +import sys +import builtins +from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction, + QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem, + QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QTreeView, QInputDialog, QFileDialog, QMessageBox, QDialog, QGroupBox, QHBoxLayout, QPushButton, QDialogButtonBox) +from PyQt5.QtCore import Qt, QDir, QUrl +from PyQt5.QtGui import QDrag, QPainter, QPixmap +from PyQt5.QtWidgets import QFileSystemModel +from QMeta3D.QMeta3DWidget import QMeta3DWidget +from panda3d.core import loadPrcFileData +loadPrcFileData("", "assertions 0") +from core.world import CoreWorld +from core.selection import SelectionSystem +from core.event_handler import EventHandler +from core.tool_manager import ToolManager +from core.script_system import ScriptManager +from core.patrol_system import PatrolSystem +from core.Command_System import CommandManager +from gui.gui_manager import GUIManager +from core.terrain_manager import TerrainManager +from scene.scene_manager import SceneManager +from project.project_manager import ProjectManager +from ui.widgets import CustomMeta3DWidget, CustomFileView, CustomTreeWidget +from ui.property_panel import PropertyPanelManager +from ui.interface_manager import InterfaceManager +from panda3d.core import (CardMaker, Vec4, Vec3, ColorAttrib, MaterialAttrib, + GeomNode, NodePath, Material, CollisionTraverser, + CollisionHandlerQueue, CollisionNode, CollisionRay, + Plane, Point3, Point2, BitMask32, CollisionSphere, + CollisionPlane, ModelPool, Filename, ModelRoot, + AmbientLight, DirectionalLight, TextureAttrib, DriveInterface, WindowProperties) +from panda3d.egg import EggData, EggVertexPool, EggPolygon +from direct.task import Task +from direct.task.TaskManagerGlobal import taskMgr +from direct.showbase.ShowBase import ShowBase +from direct.showbase.DirectObject import DirectObject +from direct.showbase.ShowBaseGlobal import globalClock, aspect2d +import os +import json +import datetime +from direct.actor.Actor import Actor +from PyQt5.sip import wrapinstance +#from RenderPipelineFile.toolkit.material_editor.main import MaterialEditor + +class MyWorld(CoreWorld): + def __init__(self): + super().__init__() + + # 初始化选择和变换系统 + self.selection = SelectionSystem(self) + + # 绑定F键用于聚焦选中节点 + self.accept("f", self.onFocusKeyPressed) + self.accept("F", self.onFocusKeyPressed) # 大写F + + #初始化巡检系统 + self.patrol_system = PatrolSystem(self) + self.accept("p",self.onPatrolKeyPressed) + self.accept("P",self.onPatrolKeyPressed) + + # 初始化事件处理系统 + self.event_handler = EventHandler(self) + + # 初始化工具管理系统 + self.tool_manager = ToolManager(self) + + # 初始化脚本管理系统 + self.script_manager = ScriptManager(self) + + # 初始化GUI管理系统 + self.gui_manager = GUIManager(self) + + # 初始化视频管理 + self.video_manager = VideoManager(self) + + # 初始化场景管理系统 + self.scene_manager = SceneManager(self) + + # 初始化项目管理系统 + self.project_manager = ProjectManager(self) + + # 初始化属性面板管理系统 + self.property_panel = PropertyPanelManager(self) + + # 初始化界面管理系统 + self.interface_manager = InterfaceManager(self) + + + # 启动脚本系统 + self.script_manager.start_system() + + #self.material_editor = None + self.terrain_manager = TerrainManager(self) + + self.terrain_edit_radius = 3.0 + self.terrain_edit_strength=0.3 + self.terrain_edit_operation = "add" + + self.info_panel_manager = InfoPanelManager(self) + + self.command_manager = CommandManager() + + # 初始化碰撞管理器 + from core.collision_manager import CollisionManager + self.collision_manager = CollisionManager(self) + + # 初始化VR管理器 + try: + from core.vr import VRManager + self.vr_manager = VRManager(self) + print("✓ VR管理器初始化完成") + except Exception as e: + print(f"⚠ VR管理器初始化失败: {e}") + self.vr_manager = None + + # 调试选项 + self.debug_collision = True # 是否显示碰撞体 + + # 默认启用模型间碰撞检测(可选) + self.enableModelCollisionDetection(enable=True, frequency=0.1, threshold=0.5) + + try: + self.property_panel = PropertyPanelManager(self) + print("✓ 属性面板管理器初始化完成") + except Exception as e: + print(f"⚠ 属性面板管理器初始化失败: {e}") + import traceback + traceback.print_exc() + self.property_panel = None + + print("✓ MyWorld 初始化完成") + print("✅ 碰撞管理器已初始化") + + # ==================== 兼容性属性 ==================== + + # 保留models属性以兼容现有代码 + @property + def models(self): + """模型列表的兼容性属性""" + return self.scene_manager.models + + @models.setter + def models(self, value): + """模型列表的兼容性设置器""" + self.scene_manager.models = value + + # 保留gui_elements属性以兼容现有代码 + @property + def gui_elements(self): + """GUI元素列表的兼容性属性""" + return self.gui_manager.gui_elements + + @gui_elements.setter + def gui_elements(self, value): + """GUI元素列表的兼容性设置器""" + self.gui_manager.gui_elements = value + + # 保留gui_elements属性以兼容现有代码 + @property + def Spotlight(self): + """GUI元素列表的兼容性属性""" + return self.scene_manager.Spotlight + + @Spotlight.setter + def Spotlight(self, value): + """GUI元素列表的兼容性设置器""" + self.scene_manager.Spotlight = value + + @property + def Pointlight(self): + return self.scene_manager.Pointlight + + @Pointlight.setter + def Pointlight(self,value): + self.scene_manager.Pointlight = value + + + # 保留guiEditMode属性以兼容现有代码 + @property + def guiEditMode(self): + """GUI编辑模式的兼容性属性""" + return self.gui_manager.guiEditMode + + @guiEditMode.setter + def guiEditMode(self, value): + """GUI编辑模式的兼容性设置器""" + self.gui_manager.guiEditMode = value + + # 保留currentTool属性以兼容现有代码 + @property + def currentTool(self): + """当前工具的兼容性属性""" + return self.tool_manager.currentTool + + @currentTool.setter + def currentTool(self, value): + """当前工具的兼容性设置器""" + self.tool_manager.currentTool = value + + # 保留treeWidget属性以兼容现有代码 + @property + def treeWidget(self): + """树形控件的兼容性属性""" + return self.interface_manager.treeWidget + + @treeWidget.setter + def treeWidget(self, value): + """树形控件的兼容性设置器""" + self.interface_manager.treeWidget = value + + #保留terrains属性以兼容现有代码 + @property + def terrains(self): + """地形列表的兼容性属性""" + return self.terrain_manager.terrains + + @terrains.setter + def terrains(self,value): + """地形列表的兼容性设置器""" + self.terrain_manager.terrains = value + + # ==================== GUI管理功能代理 ==================== + + # GUI元素创建方法 - 代理到gui_manager + def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1): + """创建2D GUI按钮""" + return self.gui_manager.createGUIButton(pos, text, size) + + def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08): + """创建2D GUI标签""" + return self.gui_manager.createGUILabel(pos, text, size) + + def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08): + """创建2D GUI文本输入框""" + return self.gui_manager.createGUIEntry(pos, placeholder, size) + + def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=1): + """创建3D空间文本""" + return self.gui_manager.createGUI3DText(pos, text, size) + + def createGUI3DImage(self,pos=(0,0,0),text="3D图片",size=(1,1,1)): + """创建3D图片""" + return self.gui_manager.createGUI3DImage(pos,text,size) + + def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=2): + """创建2D GUI图片""" + return self.gui_manager.createGUI2DImage(pos, image_path, size) + + def createVideoScreen(self,pos=(0,0,0),size=1,video_path=None): + """创建视频屏幕""" + return self.gui_manager.createVideoScreen(pos,size,video_path) + + def create2DVideoScreen(self,pos=(0,0,0),size=0.2,video_path=None): + """创建2D视频屏幕""" + return self.gui_manager.createGUI2DVideoScreen(pos,size,video_path) + + def createSphericalVideo(self,pos=(0,0,0),radius=5.0,video_path=None): + """创建360度视频""" + return self.gui_manager.createSphericalVideo(pos,radius,video_path) + + def playSphericalVideo(self,spherical_video_node): + return self.gui_manager.playSphericalVideo(spherical_video_node) + + def pauseSphericalVideo(self,spherical_video_node): + return self.gui_manager.pauseSphericalVideo(spherical_video_node) + + def setSphericalVideoTime(self,spherical_video_node,time_seconds): + return self.gui_manager.setSphericalVideoTime(spherical_video_node,time_seconds) + + def createSpotLight(self,pos=(0,0,5)): + """创建聚光灯""" + return self.scene_manager.createSpotLight(pos) + + def createPointLight(self,pos=(0,0,5)): + """创建点光源""" + return self.scene_manager.createPointLight(pos) + + def createGUIVirtualScreen(self, pos=(0, 0, 0), size=(2, 1), text="虚拟屏幕"): + """创建3D虚拟屏幕""" + return self.gui_manager.createGUIVirtualScreen(pos, size, text) + + def createGUISlider(self, pos=(0, 0, 0), text="滑块", scale=0.3): + """创建2D GUI滑块""" + return self.gui_manager.createGUISlider(pos, text, scale) + + # GUI元素管理方法 - 代理到gui_manager + def deleteGUIElement(self, gui_element): + """删除GUI元素""" + return self.gui_manager.deleteGUIElement(gui_element) + + def editGUIElement(self, gui_element, property_name, value): + """编辑GUI元素属性""" + return self.gui_manager.editGUIElement(gui_element, property_name, value) + + def duplicateGUIElement(self, gui_element): + """复制GUI元素""" + return self.gui_manager.duplicateGUIElement(gui_element) + + def editGUIElementDialog(self, gui_element): + """显示GUI元素编辑对话框""" + return self.gui_manager.editGUIElementDialog(gui_element) + + # GUI事件处理方法 - 代理到gui_manager + def onGUIButtonClick(self, button_id): + """GUI按钮点击事件处理""" + return self.gui_manager.onGUIButtonClick(button_id) + + def onGUIEntrySubmit(self, text, entry_id): + """GUI输入框提交事件处理""" + return self.gui_manager.onGUIEntrySubmit(text, entry_id) + + # GUI编辑模式方法 - 代理到gui_manager + def toggleGUIEditMode(self): + """切换GUI编辑模式""" + return self.gui_manager.toggleGUIEditMode() + + def enterGUIEditMode(self): + """进入GUI编辑模式""" + return self.gui_manager.enterGUIEditMode() + + def exitGUIEditMode(self): + """退出GUI编辑模式""" + return self.gui_manager.exitGUIEditMode() + + def createGUIEditPanel(self): + """创建GUI编辑面板""" + return self.gui_manager.createGUIEditPanel() + + def openGUIPreviewWindow(self): + """打开独立的GUI预览窗口""" + return self.gui_manager.openGUIPreviewWindow() + + def closeGUIPreviewWindow(self): + """关闭GUI预览窗口""" + return self.gui_manager.closeGUIPreviewWindow() + + # GUI工具和选择方法 - 代理到gui_manager + def setGUICreateTool(self, tool_type): + """设置GUI创建工具""" + return self.gui_manager.setGUICreateTool(tool_type) + + def deleteSelectedGUI(self): + """删除选中的GUI元素""" + return self.gui_manager.deleteSelectedGUI() + + def copySelectedGUI(self): + """复制选中的GUI元素""" + return self.gui_manager.copySelectedGUI() + + def handleGUIEditClick(self, hitPos): + """处理GUI编辑模式下的点击""" + return self.gui_manager.handleGUIEditClick(hitPos) + + def createGUIAtPosition(self, world_pos, gui_type): + """在指定位置创建GUI元素""" + return self.gui_manager.createGUIAtPosition(world_pos, gui_type) + + def findClickedGUI(self, hitNode): + """查找被点击的GUI元素""" + return self.gui_manager.findClickedGUI(hitNode) + + def selectGUIInTree(self, gui_element): + """在树形控件中选中GUI元素""" + return self.gui_manager.selectGUIInTree(gui_element) + + def updateGUISelection(self, gui_element): + """更新GUI元素选择状态""" + return self.gui_manager.updateGUISelection(gui_element) + + # GUI属性面板方法 - 代理到gui_manager + def selectGUIColor(self, gui_element): + """选择GUI元素颜色""" + return self.gui_manager.selectGUIColor(gui_element) + + def editGUI2DPosition(self, gui_element, axis, value): + """编辑2D GUI元素位置""" + return self.gui_manager.editGUI2DPosition(gui_element, axis, value) + + # ==================== 事件处理代理 ==================== + + def onTreeItemClicked(self, item, column): + """处理树形控件项目点击事件 - 代理到interface_manager""" + return self.interface_manager.onTreeItemClicked(item, column) + + def setTreeWidget(self, treeWidget): + """设置树形控件引用 - 代理到interface_manager""" + return self.interface_manager.setTreeWidget(treeWidget) + + def mousePressEventLeft(self, evt): + """处理鼠标左键按下事件 - 代理到event_handler""" + return self.event_handler.mousePressEventLeft(evt) + + def mouseReleaseEventLeft(self, evt): + """处理鼠标左键释放事件 - 代理到event_handler""" + return self.event_handler.mouseReleaseEventLeft(evt) + + def wheelForward(self, data=None): + """处理滚轮向前滚动(前进) - 代理到event_handler""" + return self.event_handler.wheelForward(data) + + def wheelBackward(self, data=None): + """处理滚轮向后滚动(后退) - 代理到event_handler""" + return self.event_handler.wheelBackward(data) + + def mousePressEventMiddle(self, evt): + """处理鼠标中键按下事件 - 代理到event_handler""" + return self.event_handler.mousePressEventMiddle(evt) + + def mouseReleaseEventMiddle(self, evt): + """处理鼠标中键释放事件 - 代理到event_handler""" + return self.event_handler.mouseReleaseEventMiddle(evt) + + def mouseMoveEvent(self, evt): + """处理鼠标移动事件 - 代理到event_handler""" + return self.event_handler.mouseMoveEvent(evt) + + # ==================== 射线显示控制 ==================== + + def toggleRayDisplay(self): + """切换射线显示状态 - 代理到event_handler""" + return self.event_handler.toggleRayDisplay() + + def setRayDisplay(self, show=True): + """设置射线显示状态 - 代理到event_handler""" + self.event_handler.showRay = show + if not show: + self.event_handler.clearRay() + return show + + def getRayDisplay(self): + """获取射线显示状态 - 代理到event_handler""" + return self.event_handler.showRay + + def setRayLifetime(self, seconds): + """设置射线显示时长(秒) - 代理到event_handler""" + self.event_handler.rayLifetime = seconds + print(f"射线显示时长设置为: {seconds}秒") + + def getRayLifetime(self): + """获取射线显示时长 - 代理到event_handler""" + return self.event_handler.rayLifetime + + # ==================== 属性面板代理 ==================== + + def setPropertyLayout(self, layout): + """设置属性面板布局引用 - 代理到property_panel""" + return self.property_panel.setPropertyLayout(layout) + + def clearPropertyPanel(self): + """清空属性面板 - 代理到property_panel""" + return self.property_panel.clearPropertyPanel() + + def updatePropertyPanel(self, item): + """更新属性面板显示 - 代理到property_panel""" + return self.property_panel.updatePropertyPanel(item) + + def updateGUIPropertyPanel(self, gui_element): + """更新GUI元素属性面板 - 代理到property_panel""" + return self.property_panel.updateGUIPropertyPanel(gui_element) + + def removeActorForModel(self,model): + return self.property_panel.removeActorForModel( model) + + def updateNodeVisibilityAfterDrag(self,item): + return self.property_panel.updateNodeVisibilityAfterDrag(item) + + # ==================== 工具管理代理 ==================== + + def setCurrentTool(self, tool): + """设置当前工具 - 代理到tool_manager""" + return self.tool_manager.setCurrentTool(tool) + + # ==================== 场景管理功能代理 ==================== + + # 模型导入和处理方法 - 代理到scene_manager + def importModel(self, filepath): + """导入模型到场景""" + # 自动转换FBX等格式为GLB以获得更好的动画支持(不再弹窗询问) + # 如果你不想自动转换,可以将 auto_convert 改为 False + auto_convert = False + + print(f"[模型导入] 文件: {filepath}, 自动转换GLB: {auto_convert}") + + return self.scene_manager.importModel(filepath, auto_convert_to_glb=auto_convert) + + def importModelAsync(self, filepath): + """异步导入模型""" + return self.scene_manager.importModelAsync(filepath) + + # 材质和几何体处理方法 - 代理到scene_manager + def processMaterials(self, model): + """处理模型材质""" + return self.scene_manager.processMaterials(model) + + def processModelGeometry(self, model): + """处理模型几何体""" + return self.scene_manager.processModelGeometry(model) + + # 碰撞系统方法 - 代理到scene_manager + def setupCollision(self, model): + """为模型设置碰撞检测""" + return self.scene_manager.setupCollision(model) + + # 场景树管理方法 - 代理到scene_manager + def updateSceneTree(self): + """更新场景树显示""" + return self.scene_manager.updateSceneTree() + + # 场景保存和加载方法 - 代理到scene_manager + def saveScene(self, filename): + """保存场景到BAM文件""" + return self.scene_manager.saveScene(filename) + + def loadScene(self, filename): + """从BAM文件加载场景""" + return self.scene_manager.loadScene(filename) + + # 模型管理方法 - 代理到scene_manager + def deleteModel(self, model): + """删除模型""" + return self.scene_manager.deleteModel(model) + + def clearAllModels(self): + """清除所有模型""" + return self.scene_manager.clearAllModels() + + def getModels(self): + """获取模型列表""" + return self.scene_manager.getModels() + + def getModelCount(self): + """获取模型数量""" + return self.scene_manager.getModelCount() + + def findModelByName(self, name): + """根据名称查找模型""" + return self.scene_manager.findModelByName(name) + + # ==================== 脚本系统功能代理 ==================== + + # 脚本系统控制方法 - 代理到script_manager + def startScriptSystem(self): + """启动脚本系统""" + return self.script_manager.start_system() + + def stopScriptSystem(self): + """停止脚本系统""" + return self.script_manager.stop_system() + + def enableHotReload(self, enabled=True): + """启用/禁用热重载""" + self.script_manager.hot_reload_enabled = enabled + if enabled: + self.script_manager.start_hot_reload() + else: + self.script_manager.stop_hot_reload() + + # 脚本创建和加载方法 - 代理到script_manager + def createScript(self, script_name, template="basic"): + """创建新脚本文件""" + return self.script_manager.create_script_file(script_name, template) + + def loadScript(self, script_path): + """从文件加载脚本""" + return self.script_manager.load_script_from_file(script_path) + + def loadAllScripts(self, directory=None): + """从目录加载所有脚本""" + return self.script_manager.load_all_scripts_from_directory(directory) + + def reloadScript(self, script_name): + """重新加载脚本""" + return self.script_manager.reload_script(script_name) + + # 脚本挂载和管理方法 - 代理到script_manager + def addScript(self, game_object, script_name): + """为游戏对象添加脚本""" + return self.script_manager.add_script_to_object(game_object, script_name) + + def removeScript(self, game_object, script_name): + """从游戏对象移除脚本""" + return self.script_manager.remove_script_from_object(game_object, script_name) + + def getScripts(self, game_object): + """获取对象上的所有脚本""" + return self.script_manager.get_scripts_on_object(game_object) + + def getScript(self, game_object, script_name): + """获取对象上的特定脚本""" + return self.script_manager.get_script_on_object(game_object, script_name) + + # 脚本信息查询方法 - 代理到script_manager + def getAvailableScripts(self): + """获取所有可用的脚本名称""" + return self.script_manager.get_available_scripts() + + def getScriptInfo(self, script_name): + """获取脚本信息""" + return self.script_manager.get_script_info(script_name) + + def listAllScripts(self): + """列出所有脚本信息""" + return self.script_manager.list_all_scripts() + + def loadCesiumTileset(self,tileset_url,position=(0,0,0)): + return self.scene_manager.load_cesium_tileset(tileset_url,position) + + def addCesiumTileset(self,name,url,position=(0,0,0)): + if hasattr(self,'gui_manager') and self.gui_manager: + return self.gui_manager.addCesiumTilesetToScene(name,url,position) + else: + return self.scene_manager.load_cesium_tileset(url,position) + + # ==================== 地形管理功能代理 ==================== + # 地形创建方法 - 代理到terrain_manager + def createTerrainFromHeightMap(self, heightmap_path, scale=(1, 1, 1)): + """从高度图创建地形""" + return self.terrain_manager.createTerrainFromHeightMap(heightmap_path, scale) + + def createFlatTerrain(self, size=(0.3, 0.3), resolution=256): + """创建平面地形""" + return self.terrain_manager.createFlatTerrain(size, resolution) + + def updateTerrain(self): + """更新所有地形的LOD""" + return self.terrain_manager.updateTerrain() + + def getTerrainHeight(self, terrain_info, x, y): + """获取地形上指定点的高度""" + return self.terrain_manager.getTerrainHeight(terrain_info, x, y) + + def setTerrainEditParameters(self,radius=None,strength=None,operation=None): + if radius is not None: + self.terrain_edit_radius = float(radius) + if strength is not None: + self.terrain_edit_strenght = float(strength) + if operation is not None: + self.terrain_edit_operation = operation + print(f"地形编辑参数已更新: 半径={self.terrain_edit_radius}, 强度={self.terrain_edit_strength}, 操作={self.terrain_edit_operation}") + def getTerrainEditParameters(self): + """获取当前地形编辑参数""" + return { + 'radius': self.terrain_edit_radius, + 'strength': self.terrain_edit_strength, + 'operation': self.terrain_edit_operation + } + + def modifyTerrainHeight(self, terrain_info, x, y, radius, strength, operation="add"): + """修改地形高度的便捷方法""" + if hasattr(self, 'terrain_manager'): + return self.terrain_manager.modifyTerrainHeight(terrain_info, x, y, radius, strength, operation) + return False + + # 添加碰撞管理相关的代理方法 + def enableModelCollisionDetection(self, enable=True, frequency=0.1, threshold=0.5): + """启用模型间碰撞检测""" + return self.collision_manager.enableModelCollisionDetection(enable, frequency, threshold) + + def detectModelCollisions(self, specific_models=None, log_results=True): + """检测模型间碰撞""" + return self.collision_manager.detectModelCollisions(specific_models, log_results) + + def getCollisionHistory(self, limit=None): + """获取碰撞历史""" + return self.collision_manager.getCollisionHistory(limit) + + def getCollisionStatistics(self): + """获取碰撞统计""" + return self.collision_manager.getCollisionStatistics() + + def setupKeyboardEvents(self): + """设置键盘事件""" + try: + # 绑定 F 键用于聚焦选中节点 + self.accept("f", self.onFocusKeyPressed) + self.accept("F", self.onFocusKeyPressed) # 大写F + + print("✓ 键盘事件绑定完成") + + except Exception as e: + print(f"设置键盘事件失败: {e}") + + def onFocusKeyPressed(self): + """处理 F 键按下事件""" + try: + #print("检测到 F 键按下") + + # 检查是否有选中的节点 + if hasattr(self, 'selection') and self.selection.selectedNode: + #print(f"当前选中节点: {self.selection.selectedNode.getName()}") + # 调用选择系统的聚焦功能(可以选择带动画或不带动画的版本) + # self.selection.focusCameraOnSelectedNode() # 无动画版本 + self.selection.focusCameraOnSelectedNodeAdvanced() # 带动画版本 + else: + print("当前没有选中任何节点") + + except Exception as e: + print(f"处理 F 键事件失败: {e}") + + def onPatrolKeyPressed(self): + """处理 P 键按下事件 - 控制巡检系统""" + try: + print("检测到 P 键按下") + + if not self.patrol_system.is_patrolling: + # 如果巡检系统没有点,创建默认巡检路线 + if not self.patrol_system.patrol_points: + self.createDefaultPatrolRoute() + + # 开始巡检 + if self.patrol_system.start_patrol(): + print("✓ 巡检已开始") + else: + print("✗ 巡检启动失败") + else: + # 停止巡检 + if self.patrol_system.stop_patrol(): + print("✓ 巡检已停止") + else: + print("✗ 巡检停止失败") + + except Exception as e: + print(f"处理 P 键事件失败: {e}") + + def createDefaultPatrolRoute(self): + """创建默认巡检路线(使用自动朝向)""" + try: + # 清空现有巡检点 + self.patrol_system.clear_patrol_points() + + # 添加巡检点,使用None表示朝向下一个点 + self.patrol_system.add_patrol_point((0, -10, 2), (0,0,0), 1.5) + self.patrol_system.add_patrol_point((10, -10, 2), (0,0,0), 1.5) + self.patrol_system.add_patrol_point((10, 5, 2), (0,0,0), 1.5) + self.patrol_system.add_patrol_point((10, 0, 5), (0,0,0), 1.5) + + # 最后一个点可以指定特定的朝向,或者也设为None继续循环 + self.patrol_system.add_patrol_point((0, -10, 2), None, 2.5) + + print("✓ 默认自动朝向巡检路线已创建") + self.patrol_system.list_patrol_points() + + except Exception as e: + print(f"创建默认自动朝向巡检路线失败: {e}") + + def _serializeNode(self, node): + """序列化节点数据""" + try: + return self.world.scene_manager.serializeNode(node) + except Exception as e: + print(f"序列化节点失败: {e}") + return None + + def _deserializeNode(self, node_data, parent_node): + """反序列化节点数据""" + try: + return self.world.scene_manager.deserializeNode(node_data, parent_node) + except Exception as e: + print(f"反序列化节点失败: {e}") + return None + + +# ==================== 项目管理功能代理 ==================== +# 以下函数代理到project_manager模块的对应功能 + +def updateWindowTitle(window, project_name=None): + """更新窗口标题 - 代理到project_manager""" + from project.project_manager import updateWindowTitle as pm_updateWindowTitle + return pm_updateWindowTitle(window, project_name) + +def createNewProject(parent_window): + """创建新项目 - 代理到project_manager""" + world = parent_window.centralWidget().world + return world.project_manager.createNewProject(parent_window) + +def saveProject(appw): + """保存项目 - 代理到project_manager""" + + world = appw.centralWidget().world + return world.project_manager.saveProject(appw) + +def openProject(appw): + """打开项目 - 代理到project_manager""" + world = appw.centralWidget().world + return world.project_manager.openProject(appw) + +def openProjectForPath(project_path, appw): + """打开项目 - 代理到project_manager""" + world = appw.centralWidget().world + return world.project_manager.openProjectForPath(project_path, appw) + +def buildPackage(appw): + """打包项目 - 代理到project_manager""" + world = appw.centralWidget().world + return world.project_manager.buildPackage(appw) + +def run(args = None): + world = MyWorld() + + # 使用新的UI模块创建主窗口 + from ui.main_window import setup_main_window + print(f'Path is {args}') + app, main_window = setup_main_window(world, args) + + # 启动应用程序 + sys.exit(app.exec_()) + +if __name__ == "__main__": run() \ No newline at end of file diff --git a/new/project.json b/new/project.json index 5d10691e..f99e1ef3 100644 --- a/new/project.json +++ b/new/project.json @@ -1,8 +1,8 @@ -{ - "name": "new", - "path": "D:\\PythonProject\\CH_EG\\EG\\new", - "created_at": "2025-09-09 16:47:27", - "last_modified": "2025-09-11 09:28:24", - "version": "1.0.0", - "engine_version": "1.0.0" +{ + "name": "new", + "path": "D:\\PythonProject\\CH_EG\\EG\\new", + "created_at": "2025-09-09 16:47:27", + "last_modified": "2025-09-11 09:28:24", + "version": "1.0.0", + "engine_version": "1.0.0" } \ No newline at end of file diff --git a/plugins/__init__.py b/plugins/__init__.py index 0060e3f7..44a0bfab 100644 --- a/plugins/__init__.py +++ b/plugins/__init__.py @@ -1,3 +1,3 @@ -""" -插件系统初始化文件 +""" +插件系统初始化文件 """ \ No newline at end of file diff --git a/plugins/core/__init__.py b/plugins/core/__init__.py index 0e15d291..1ae88302 100644 --- a/plugins/core/__init__.py +++ b/plugins/core/__init__.py @@ -1,3 +1,3 @@ -""" -核心插件目录 +""" +核心插件目录 """ \ No newline at end of file diff --git a/plugins/plugin_interface_spec.py b/plugins/plugin_interface_spec.py index 0a15ae65..f2d2f580 100644 --- a/plugins/plugin_interface_spec.py +++ b/plugins/plugin_interface_spec.py @@ -1,89 +1,89 @@ -""" -插件接口规范示例 -此文件定义了插件应遵循的接口规范 -""" - -from plugins.plugin_manager import BasePlugin - -class ExamplePlugin(BasePlugin): - """ - 插件接口规范示例 - 所有插件都应该继承BasePlugin类并实现以下方法 - """ - - def __init__(self, plugin_manager, name): - super().__init__(plugin_manager, name) - # 设置插件配置 - self.config = { - "version": "1.0.0", - "author": "Your Name", - "description": "插件功能描述" - } - - def initialize(self) -> bool: - """ - 初始化插件 - 在插件加载时调用,用于执行一次性设置 - 返回True表示初始化成功,False表示失败 - """ - print(f"初始化插件: {self.name}") - # 在这里执行插件初始化逻辑 - return True - - def enable(self) -> bool: - """ - 启用插件 - 在插件启用时调用,用于注册事件处理器、创建GUI元素等 - 返回True表示启用成功,False表示失败 - """ - if not super().enable(): - return False - - print(f"启用插件: {self.name}") - # 在这里执行插件启用逻辑 - # 例如注册事件处理器: - # self.plugin_manager.world.accept("f1", self.on_f1_pressed) - return True - - def disable(self) -> bool: - """ - 禁用插件 - 在插件禁用时调用,用于清理资源、移除事件处理器等 - 返回True表示禁用成功,False表示失败 - """ - if not super().disable(): - return False - - print(f"禁用插件: {self.name}") - # 在这里执行插件禁用逻辑 - # 例如移除事件处理器: - # self.plugin_manager.world.ignore("f1") - return True - - def finalize(self): - """ - 清理插件资源 - 在插件卸载时调用,用于执行最终清理工作 - """ - print(f"清理插件资源: {self.name}") - # 在这里执行最终清理逻辑 - - def get_info(self) -> dict: - """ - 获取插件信息 - 返回包含插件信息的字典 - """ - info = super().get_info() - info.update({ - "version": self.config.get("version", "1.0.0"), - "author": self.config.get("author", "Unknown"), - "description": self.config.get("description", "Base plugin class") - }) - return info - -# 插件开发步骤说明: -# 1. 在plugins/user/目录下创建插件目录,如my_plugin -# 2. 在插件目录中创建plugin.py文件 -# 3. 在plugin.py中实现Plugin类,继承BasePlugin -# 4. 实现必要的方法(initialize, enable, disable, finalize) +""" +插件接口规范示例 +此文件定义了插件应遵循的接口规范 +""" + +from plugins.plugin_manager import BasePlugin + +class ExamplePlugin(BasePlugin): + """ + 插件接口规范示例 + 所有插件都应该继承BasePlugin类并实现以下方法 + """ + + def __init__(self, plugin_manager, name): + super().__init__(plugin_manager, name) + # 设置插件配置 + self.config = { + "version": "1.0.0", + "author": "Your Name", + "description": "插件功能描述" + } + + def initialize(self) -> bool: + """ + 初始化插件 + 在插件加载时调用,用于执行一次性设置 + 返回True表示初始化成功,False表示失败 + """ + print(f"初始化插件: {self.name}") + # 在这里执行插件初始化逻辑 + return True + + def enable(self) -> bool: + """ + 启用插件 + 在插件启用时调用,用于注册事件处理器、创建GUI元素等 + 返回True表示启用成功,False表示失败 + """ + if not super().enable(): + return False + + print(f"启用插件: {self.name}") + # 在这里执行插件启用逻辑 + # 例如注册事件处理器: + # self.plugin_manager.world.accept("f1", self.on_f1_pressed) + return True + + def disable(self) -> bool: + """ + 禁用插件 + 在插件禁用时调用,用于清理资源、移除事件处理器等 + 返回True表示禁用成功,False表示失败 + """ + if not super().disable(): + return False + + print(f"禁用插件: {self.name}") + # 在这里执行插件禁用逻辑 + # 例如移除事件处理器: + # self.plugin_manager.world.ignore("f1") + return True + + def finalize(self): + """ + 清理插件资源 + 在插件卸载时调用,用于执行最终清理工作 + """ + print(f"清理插件资源: {self.name}") + # 在这里执行最终清理逻辑 + + def get_info(self) -> dict: + """ + 获取插件信息 + 返回包含插件信息的字典 + """ + info = super().get_info() + info.update({ + "version": self.config.get("version", "1.0.0"), + "author": self.config.get("author", "Unknown"), + "description": self.config.get("description", "Base plugin class") + }) + return info + +# 插件开发步骤说明: +# 1. 在plugins/user/目录下创建插件目录,如my_plugin +# 2. 在插件目录中创建plugin.py文件 +# 3. 在plugin.py中实现Plugin类,继承BasePlugin +# 4. 实现必要的方法(initialize, enable, disable, finalize) # 5. 在MyWorld中通过plugin_manager加载和启用插件 \ No newline at end of file diff --git a/plugins/plugin_manager.py b/plugins/plugin_manager.py index 06544212..237b58f5 100644 --- a/plugins/plugin_manager.py +++ b/plugins/plugin_manager.py @@ -1,287 +1,287 @@ -""" -插件管理器 -负责插件的加载、卸载和管理 -""" -import os -import sys -import importlib.util -from typing import Dict, List, Optional, Any -import traceback - -class BasePlugin: - """ - 插件基类 - 所有插件必须继承此类并实现相应方法 - """ - - def __init__(self, plugin_manager, name: str): - self.plugin_manager = plugin_manager - self.name = name - self.enabled = False - self.config = {} - - def initialize(self) -> bool: - """ - 初始化插件 - 返回True表示初始化成功,False表示失败 - """ - return True - - def enable(self) -> bool: - """ - 启用插件 - 返回True表示启用成功,False表示失败 - """ - self.enabled = True - return True - - def disable(self) -> bool: - """ - 禁用插件 - 返回True表示禁用成功,False表示失败 - """ - self.enabled = False - return True - - def finalize(self): - """ - 清理插件资源 - """ - pass - - def get_info(self) -> Dict[str, Any]: - """ - 获取插件信息 - """ - return { - "name": self.name, - "enabled": self.enabled, - "version": "1.0.0", - "author": "Unknown", - "description": "Base plugin class" - } - - -class PluginManager: - """ - 插件管理器 - 负责插件的加载、卸载和管理 - """ - - def __init__(self, world): - self.world = world - self.plugins: Dict[str, BasePlugin] = {} - self.plugin_configs: Dict[str, Dict] = {} - self.plugin_dirs = [ - "plugins/core", - "plugins/third_party", - "plugins/user" - ] - self.enabled = False # 默认不启用插件系统 - - def enable_plugin_system(self): - """ - 启用插件系统 - """ - self.enabled = True - print("✓ 插件系统已启用") - - def disable_plugin_system(self): - """ - 禁用插件系统 - """ - # 禁用所有插件 - for plugin_name in list(self.plugins.keys()): - self.disable_plugin(plugin_name) - self.enabled = False - print("✓ 插件系统已禁用") - - def is_plugin_system_enabled(self) -> bool: - """ - 检查插件系统是否已启用 - """ - return self.enabled - - def discover_plugins(self) -> List[str]: - """ - 发现所有可用的插件 - 返回插件名称列表 - """ - if not self.enabled: - return [] - - plugin_names = [] - - # 获取项目根目录 - project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - - for plugin_dir in self.plugin_dirs: - full_path = os.path.join(project_root, plugin_dir) - if os.path.exists(full_path): - for item in os.listdir(full_path): - # 查找插件目录(包含plugin.py文件的目录) - item_path = os.path.join(full_path, item) - if os.path.isdir(item_path) and os.path.exists(os.path.join(item_path, "plugin.py")): - plugin_names.append(item) - - return plugin_names - - def load_plugin(self, plugin_name: str) -> Optional[BasePlugin]: - """ - 加载指定插件 - """ - if not self.enabled: - print(f"⚠ 插件系统未启用,无法加载插件: {plugin_name}") - return None - - if plugin_name in self.plugins: - print(f"⚠ 插件已加载: {plugin_name}") - return self.plugins[plugin_name] - - # 查找插件文件 - project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - plugin_file_path = None - - for plugin_dir in self.plugin_dirs: - full_path = os.path.join(project_root, plugin_dir, plugin_name, "plugin.py") - if os.path.exists(full_path): - plugin_file_path = full_path - break - - if not plugin_file_path: - print(f"❌ 未找到插件: {plugin_name}") - return None - - try: - # 动态加载插件模块 - spec = importlib.util.spec_from_file_location(f"plugin_{plugin_name}", plugin_file_path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - - # 检查模块中是否有Plugin类 - if not hasattr(module, 'Plugin'): - print(f"❌ 插件 {plugin_name} 缺少Plugin类") - return None - - # 创建插件实例 - plugin_class = getattr(module, 'Plugin') - plugin_instance = plugin_class(self, plugin_name) - - # 初始化插件 - if plugin_instance.initialize(): - self.plugins[plugin_name] = plugin_instance - print(f"✓ 插件加载成功: {plugin_name}") - return plugin_instance - else: - print(f"❌ 插件初始化失败: {plugin_name}") - return None - - except Exception as e: - print(f"❌ 加载插件 {plugin_name} 时出错: {e}") - traceback.print_exc() - return None - - def unload_plugin(self, plugin_name: str) -> bool: - """ - 卸载指定插件 - """ - if plugin_name not in self.plugins: - print(f"⚠ 插件未加载: {plugin_name}") - return False - - plugin = self.plugins[plugin_name] - - # 如果插件已启用,先禁用 - if plugin.enabled: - plugin.disable() - - # 清理插件资源 - plugin.finalize() - - # 从管理器中移除 - del self.plugins[plugin_name] - - print(f"✓ 插件卸载成功: {plugin_name}") - return True - - def enable_plugin(self, plugin_name: str) -> bool: - """ - 启用指定插件 - """ - if not self.enabled: - print(f"⚠ 插件系统未启用,无法启用插件: {plugin_name}") - return False - - if plugin_name not in self.plugins: - # 尝试加载插件 - plugin = self.load_plugin(plugin_name) - if not plugin: - return False - else: - plugin = self.plugins[plugin_name] - - if plugin.enabled: - print(f"⚠ 插件已启用: {plugin_name}") - return True - - if plugin.enable(): - print(f"✓ 插件启用成功: {plugin_name}") - return True - else: - print(f"❌ 插件启用失败: {plugin_name}") - return False - - def disable_plugin(self, plugin_name: str) -> bool: - """ - 禁用指定插件 - """ - if plugin_name not in self.plugins: - print(f"⚠ 插件未加载: {plugin_name}") - return False - - plugin = self.plugins[plugin_name] - - if not plugin.enabled: - print(f"⚠ 插件未启用: {plugin_name}") - return True - - if plugin.disable(): - print(f"✓ 插件禁用成功: {plugin_name}") - return True - else: - print(f"❌ 插件禁用失败: {plugin_name}") - return False - - def get_plugin(self, plugin_name: str) -> Optional[BasePlugin]: - """ - 获取指定插件实例 - """ - return self.plugins.get(plugin_name) - - def list_plugins(self) -> List[Dict[str, Any]]: - """ - 列出所有插件信息 - """ - plugin_info_list = [] - for plugin_name, plugin in self.plugins.items(): - plugin_info_list.append(plugin.get_info()) - return plugin_info_list - - def load_all_plugins(self): - """ - 加载所有发现的插件 - """ - if not self.enabled: - print("⚠ 插件系统未启用,跳过加载所有插件") - return - - plugin_names = self.discover_plugins() - for plugin_name in plugin_names: - self.load_plugin(plugin_name) - - def unload_all_plugins(self): - """ - 卸载所有插件 - """ - for plugin_name in list(self.plugins.keys()): +""" +插件管理器 +负责插件的加载、卸载和管理 +""" +import os +import sys +import importlib.util +from typing import Dict, List, Optional, Any +import traceback + +class BasePlugin: + """ + 插件基类 + 所有插件必须继承此类并实现相应方法 + """ + + def __init__(self, plugin_manager, name: str): + self.plugin_manager = plugin_manager + self.name = name + self.enabled = False + self.config = {} + + def initialize(self) -> bool: + """ + 初始化插件 + 返回True表示初始化成功,False表示失败 + """ + return True + + def enable(self) -> bool: + """ + 启用插件 + 返回True表示启用成功,False表示失败 + """ + self.enabled = True + return True + + def disable(self) -> bool: + """ + 禁用插件 + 返回True表示禁用成功,False表示失败 + """ + self.enabled = False + return True + + def finalize(self): + """ + 清理插件资源 + """ + pass + + def get_info(self) -> Dict[str, Any]: + """ + 获取插件信息 + """ + return { + "name": self.name, + "enabled": self.enabled, + "version": "1.0.0", + "author": "Unknown", + "description": "Base plugin class" + } + + +class PluginManager: + """ + 插件管理器 + 负责插件的加载、卸载和管理 + """ + + def __init__(self, world): + self.world = world + self.plugins: Dict[str, BasePlugin] = {} + self.plugin_configs: Dict[str, Dict] = {} + self.plugin_dirs = [ + "plugins/core", + "plugins/third_party", + "plugins/user" + ] + self.enabled = False # 默认不启用插件系统 + + def enable_plugin_system(self): + """ + 启用插件系统 + """ + self.enabled = True + print("✓ 插件系统已启用") + + def disable_plugin_system(self): + """ + 禁用插件系统 + """ + # 禁用所有插件 + for plugin_name in list(self.plugins.keys()): + self.disable_plugin(plugin_name) + self.enabled = False + print("✓ 插件系统已禁用") + + def is_plugin_system_enabled(self) -> bool: + """ + 检查插件系统是否已启用 + """ + return self.enabled + + def discover_plugins(self) -> List[str]: + """ + 发现所有可用的插件 + 返回插件名称列表 + """ + if not self.enabled: + return [] + + plugin_names = [] + + # 获取项目根目录 + project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + for plugin_dir in self.plugin_dirs: + full_path = os.path.join(project_root, plugin_dir) + if os.path.exists(full_path): + for item in os.listdir(full_path): + # 查找插件目录(包含plugin.py文件的目录) + item_path = os.path.join(full_path, item) + if os.path.isdir(item_path) and os.path.exists(os.path.join(item_path, "plugin.py")): + plugin_names.append(item) + + return plugin_names + + def load_plugin(self, plugin_name: str) -> Optional[BasePlugin]: + """ + 加载指定插件 + """ + if not self.enabled: + print(f"⚠ 插件系统未启用,无法加载插件: {plugin_name}") + return None + + if plugin_name in self.plugins: + print(f"⚠ 插件已加载: {plugin_name}") + return self.plugins[plugin_name] + + # 查找插件文件 + project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + plugin_file_path = None + + for plugin_dir in self.plugin_dirs: + full_path = os.path.join(project_root, plugin_dir, plugin_name, "plugin.py") + if os.path.exists(full_path): + plugin_file_path = full_path + break + + if not plugin_file_path: + print(f"❌ 未找到插件: {plugin_name}") + return None + + try: + # 动态加载插件模块 + spec = importlib.util.spec_from_file_location(f"plugin_{plugin_name}", plugin_file_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # 检查模块中是否有Plugin类 + if not hasattr(module, 'Plugin'): + print(f"❌ 插件 {plugin_name} 缺少Plugin类") + return None + + # 创建插件实例 + plugin_class = getattr(module, 'Plugin') + plugin_instance = plugin_class(self, plugin_name) + + # 初始化插件 + if plugin_instance.initialize(): + self.plugins[plugin_name] = plugin_instance + print(f"✓ 插件加载成功: {plugin_name}") + return plugin_instance + else: + print(f"❌ 插件初始化失败: {plugin_name}") + return None + + except Exception as e: + print(f"❌ 加载插件 {plugin_name} 时出错: {e}") + traceback.print_exc() + return None + + def unload_plugin(self, plugin_name: str) -> bool: + """ + 卸载指定插件 + """ + if plugin_name not in self.plugins: + print(f"⚠ 插件未加载: {plugin_name}") + return False + + plugin = self.plugins[plugin_name] + + # 如果插件已启用,先禁用 + if plugin.enabled: + plugin.disable() + + # 清理插件资源 + plugin.finalize() + + # 从管理器中移除 + del self.plugins[plugin_name] + + print(f"✓ 插件卸载成功: {plugin_name}") + return True + + def enable_plugin(self, plugin_name: str) -> bool: + """ + 启用指定插件 + """ + if not self.enabled: + print(f"⚠ 插件系统未启用,无法启用插件: {plugin_name}") + return False + + if plugin_name not in self.plugins: + # 尝试加载插件 + plugin = self.load_plugin(plugin_name) + if not plugin: + return False + else: + plugin = self.plugins[plugin_name] + + if plugin.enabled: + print(f"⚠ 插件已启用: {plugin_name}") + return True + + if plugin.enable(): + print(f"✓ 插件启用成功: {plugin_name}") + return True + else: + print(f"❌ 插件启用失败: {plugin_name}") + return False + + def disable_plugin(self, plugin_name: str) -> bool: + """ + 禁用指定插件 + """ + if plugin_name not in self.plugins: + print(f"⚠ 插件未加载: {plugin_name}") + return False + + plugin = self.plugins[plugin_name] + + if not plugin.enabled: + print(f"⚠ 插件未启用: {plugin_name}") + return True + + if plugin.disable(): + print(f"✓ 插件禁用成功: {plugin_name}") + return True + else: + print(f"❌ 插件禁用失败: {plugin_name}") + return False + + def get_plugin(self, plugin_name: str) -> Optional[BasePlugin]: + """ + 获取指定插件实例 + """ + return self.plugins.get(plugin_name) + + def list_plugins(self) -> List[Dict[str, Any]]: + """ + 列出所有插件信息 + """ + plugin_info_list = [] + for plugin_name, plugin in self.plugins.items(): + plugin_info_list.append(plugin.get_info()) + return plugin_info_list + + def load_all_plugins(self): + """ + 加载所有发现的插件 + """ + if not self.enabled: + print("⚠ 插件系统未启用,跳过加载所有插件") + return + + plugin_names = self.discover_plugins() + for plugin_name in plugin_names: + self.load_plugin(plugin_name) + + def unload_all_plugins(self): + """ + 卸载所有插件 + """ + for plugin_name in list(self.plugins.keys()): self.unload_plugin(plugin_name) \ No newline at end of file diff --git a/plugins/third_party/__init__.py b/plugins/third_party/__init__.py index e2f04b8c..8131353d 100644 --- a/plugins/third_party/__init__.py +++ b/plugins/third_party/__init__.py @@ -1,3 +1,3 @@ -""" -第三方插件目录 +""" +第三方插件目录 """ \ No newline at end of file diff --git a/plugins/user/USAGE_GUIDE.md b/plugins/user/USAGE_GUIDE.md index 5c8867ab..c1ff6f3a 100644 --- a/plugins/user/USAGE_GUIDE.md +++ b/plugins/user/USAGE_GUIDE.md @@ -1,166 +1,166 @@ -# 行为树和状态机插件使用指南 - -## 简介 - -本插件包提供了完整的行为树和状态机系统,用于实现复杂的AI决策逻辑和状态管理。 - -## 行为树插件 - -### 核心组件 - -#### 1. 节点系统 -- **基础节点**:BTNode(所有节点的基类) -- **控制节点**: - - SelectorNode:选择节点(OR逻辑) - - SequenceNode:序列节点(AND逻辑) - - ParallelNode:并行节点 - - RandomSelectorNode:随机选择节点 - - PrioritySelectorNode:优先级选择节点 -- **装饰节点**: - - RepeaterNode:重复节点 - - InverterNode:逆变节点 - - SucceederNode:成功节点 - - FailureNode:失败节点 - - TimerNode:计时器节点 - - CooldownNode:冷却节点 - - RandomizerNode:随机化节点 -- **叶子节点**: - - ConditionNode:条件节点 - - ActionNode:动作节点 - - WaitNode:等待节点 - - SubtreeNode:子树节点 - - ProbabilityNode:概率节点 - - CounterNode:计数器节点 - - TimeoutNode:超时节点 - -#### 2. 黑板系统 -- 数据共享和状态管理 -- 类型约束和数据验证 -- 历史记录和回调机制 -- 序列化支持(JSON/Pickle) - -#### 3. 管理器 -- BehaviorTreeManager:行为树管理器 -- BlackboardManager:黑板管理器 - -### 主要特性 -- 完整的节点内存管理系统 -- 支持中断和恢复机制 -- 丰富的统计和调试信息 -- 线程安全设计 -- 可扩展的自定义功能 - -## 状态机插件 - -### 核心组件 - -#### 1. 状态系统 -- **基础状态**:State(状态基类) -- **分层状态**:HierarchicalState(支持子状态) -- **状态上下文**:StateContext(状态数据管理) -- **状态转换**:StateTransition(状态间转换) - -#### 2. 状态机管理 -- StateMachine:状态机核心 -- StateMachineManager:状态机管理器 - -### 主要特性 -- 支持分层状态和子状态 -- 灵活的状态转换机制 -- 状态变更回调系统 -- 完整的统计信息 -- 线程安全设计 - -## 使用示例 - -### 行为树示例 -```python -# 创建行为树插件 -bt_plugin = plugin_manager.load_plugin("behavior_tree") -plugin_manager.enable_plugin("behavior_tree") - -# 创建黑板 -blackboard = bt_plugin.create_blackboard("ai_blackboard") - -# 创建节点 -root = SelectorNode("Root") -sequence = SequenceNode("Patrol") -condition = ConditionNode("NeedPatrol", lambda bb: bb.get("need_patrol", True)) -action = ActionNode("PatrolAction", lambda bb: print("巡逻中...") or True) - -# 组装行为树 -sequence.add_child(condition) -sequence.add_child(action) -root.add_child(sequence) - -# 创建行为树 -tree = bt_plugin.create_behavior_tree("patrol_ai", root) -tree.set_blackboard(blackboard) - -# 执行 -result = tree.run() -``` - -### 状态机示例 -```python -# 创建状态机插件 -sm_plugin = plugin_manager.load_plugin("state_machine") -plugin_manager.enable_plugin("state_machine") - -# 定义状态 -class IdleState(State): - def enter(self, context): - print("进入空闲状态") - - def execute(self, context): - if context.get("want_move", False): - return "Moving" - return None - - def exit(self, context): - print("退出空闲状态") - -# 创建状态机 -state_machine = sm_plugin.create_state_machine("character_ai") -context = StateContext() -state_machine.set_context(context) - -# 添加状态 -state_machine.add_state(IdleState("Idle")) -state_machine.add_state(MovingState("Moving")) - -# 设置初始状态并启动 -state_machine.change_state("Idle") -state_machine.start() - -# 更新 -state_machine.update() -``` - -## 高级功能 - -### 行为树高级功能 -1. **节点配置**:支持优先级、超时时间、中断控制等 -2. **内存管理**:节点状态记忆和恢复 -3. **统计系统**:详细的执行统计和性能分析 -4. **自定义扩展**:支持添加自定义函数和回调 - -### 状态机高级功能 -1. **分层状态**:支持复杂的状态嵌套 -2. **转换条件**:灵活的状态转换逻辑 -3. **回调系统**:状态变更通知机制 -4. **上下文管理**:丰富的状态数据管理 - -## 性能优化 - -1. **缓存机制**:条件节点支持结果缓存 -2. **线程安全**:所有组件都支持多线程环境 -3. **内存管理**:高效的内存使用和垃圾回收 -4. **批量操作**:支持批量更新和管理 - -## 扩展性 - -1. **插件架构**:基于插件管理器的模块化设计 -2. **接口标准**:遵循统一的插件接口规范 -3. **自定义节点**:支持创建自定义节点类型 +# 行为树和状态机插件使用指南 + +## 简介 + +本插件包提供了完整的行为树和状态机系统,用于实现复杂的AI决策逻辑和状态管理。 + +## 行为树插件 + +### 核心组件 + +#### 1. 节点系统 +- **基础节点**:BTNode(所有节点的基类) +- **控制节点**: + - SelectorNode:选择节点(OR逻辑) + - SequenceNode:序列节点(AND逻辑) + - ParallelNode:并行节点 + - RandomSelectorNode:随机选择节点 + - PrioritySelectorNode:优先级选择节点 +- **装饰节点**: + - RepeaterNode:重复节点 + - InverterNode:逆变节点 + - SucceederNode:成功节点 + - FailureNode:失败节点 + - TimerNode:计时器节点 + - CooldownNode:冷却节点 + - RandomizerNode:随机化节点 +- **叶子节点**: + - ConditionNode:条件节点 + - ActionNode:动作节点 + - WaitNode:等待节点 + - SubtreeNode:子树节点 + - ProbabilityNode:概率节点 + - CounterNode:计数器节点 + - TimeoutNode:超时节点 + +#### 2. 黑板系统 +- 数据共享和状态管理 +- 类型约束和数据验证 +- 历史记录和回调机制 +- 序列化支持(JSON/Pickle) + +#### 3. 管理器 +- BehaviorTreeManager:行为树管理器 +- BlackboardManager:黑板管理器 + +### 主要特性 +- 完整的节点内存管理系统 +- 支持中断和恢复机制 +- 丰富的统计和调试信息 +- 线程安全设计 +- 可扩展的自定义功能 + +## 状态机插件 + +### 核心组件 + +#### 1. 状态系统 +- **基础状态**:State(状态基类) +- **分层状态**:HierarchicalState(支持子状态) +- **状态上下文**:StateContext(状态数据管理) +- **状态转换**:StateTransition(状态间转换) + +#### 2. 状态机管理 +- StateMachine:状态机核心 +- StateMachineManager:状态机管理器 + +### 主要特性 +- 支持分层状态和子状态 +- 灵活的状态转换机制 +- 状态变更回调系统 +- 完整的统计信息 +- 线程安全设计 + +## 使用示例 + +### 行为树示例 +```python +# 创建行为树插件 +bt_plugin = plugin_manager.load_plugin("behavior_tree") +plugin_manager.enable_plugin("behavior_tree") + +# 创建黑板 +blackboard = bt_plugin.create_blackboard("ai_blackboard") + +# 创建节点 +root = SelectorNode("Root") +sequence = SequenceNode("Patrol") +condition = ConditionNode("NeedPatrol", lambda bb: bb.get("need_patrol", True)) +action = ActionNode("PatrolAction", lambda bb: print("巡逻中...") or True) + +# 组装行为树 +sequence.add_child(condition) +sequence.add_child(action) +root.add_child(sequence) + +# 创建行为树 +tree = bt_plugin.create_behavior_tree("patrol_ai", root) +tree.set_blackboard(blackboard) + +# 执行 +result = tree.run() +``` + +### 状态机示例 +```python +# 创建状态机插件 +sm_plugin = plugin_manager.load_plugin("state_machine") +plugin_manager.enable_plugin("state_machine") + +# 定义状态 +class IdleState(State): + def enter(self, context): + print("进入空闲状态") + + def execute(self, context): + if context.get("want_move", False): + return "Moving" + return None + + def exit(self, context): + print("退出空闲状态") + +# 创建状态机 +state_machine = sm_plugin.create_state_machine("character_ai") +context = StateContext() +state_machine.set_context(context) + +# 添加状态 +state_machine.add_state(IdleState("Idle")) +state_machine.add_state(MovingState("Moving")) + +# 设置初始状态并启动 +state_machine.change_state("Idle") +state_machine.start() + +# 更新 +state_machine.update() +``` + +## 高级功能 + +### 行为树高级功能 +1. **节点配置**:支持优先级、超时时间、中断控制等 +2. **内存管理**:节点状态记忆和恢复 +3. **统计系统**:详细的执行统计和性能分析 +4. **自定义扩展**:支持添加自定义函数和回调 + +### 状态机高级功能 +1. **分层状态**:支持复杂的状态嵌套 +2. **转换条件**:灵活的状态转换逻辑 +3. **回调系统**:状态变更通知机制 +4. **上下文管理**:丰富的状态数据管理 + +## 性能优化 + +1. **缓存机制**:条件节点支持结果缓存 +2. **线程安全**:所有组件都支持多线程环境 +3. **内存管理**:高效的内存使用和垃圾回收 +4. **批量操作**:支持批量更新和管理 + +## 扩展性 + +1. **插件架构**:基于插件管理器的模块化设计 +2. **接口标准**:遵循统一的插件接口规范 +3. **自定义节点**:支持创建自定义节点类型 4. **事件系统**:完善的事件和回调机制 \ No newline at end of file diff --git a/plugins/user/__init__.py b/plugins/user/__init__.py index 62d458e3..bf244c10 100644 --- a/plugins/user/__init__.py +++ b/plugins/user/__init__.py @@ -1,3 +1,3 @@ -""" -用户自定义插件目录 +""" +用户自定义插件目录 """ \ No newline at end of file diff --git a/plugins/user/advanced_example.py b/plugins/user/advanced_example.py index f45b1827..57bdbe08 100644 --- a/plugins/user/advanced_example.py +++ b/plugins/user/advanced_example.py @@ -1,569 +1,569 @@ -""" -行为树和状态机插件完整示例 -演示高级功能和复杂用法 -""" - -import sys -import os -import time -import random - -# 添加项目根目录到Python路径 -sys.path.insert(0, '/home/hello/EG') - -def advanced_behavior_tree_example(): - """ - 高级行为树示例 - 展示复杂的行为树功能 - """ - print("=== 高级行为树示例 ===") - - # 导入插件管理器 - from plugins.plugin_manager import PluginManager - - # 创建一个虚拟的世界对象 - class MockWorld: - def __init__(self): - self.render = None - self.accept = lambda event, handler: None - self.ignore = lambda event: None - self.taskMgr = None - - # 创建世界实例 - world = MockWorld() - - # 创建插件管理器 - plugin_manager = PluginManager(world) - plugin_manager.enable_plugin_system() - - # 加载行为树插件 - bt_plugin = plugin_manager.load_plugin("behavior_tree") - if not bt_plugin or not plugin_manager.enable_plugin("behavior_tree"): - print("❌ 无法加载或启用行为树插件") - return - - try: - # 导入行为树组件 - from plugins.user.behavior_tree import ( - SelectorNode, SequenceNode, ParallelNode, - RepeaterNode, InverterNode, TimerNode, - ConditionNode, ActionNode, WaitNode, ProbabilityNode, - BTNodeConfig - ) - - # 创建黑板 - blackboard = bt_plugin.create_blackboard("advanced_example", max_history_size=500) - - # 设置初始数据 - blackboard.set("player_health", 100) - blackboard.set("enemy_detected", False) - blackboard.set("ammo_count", 30) - blackboard.set("cover_available", True) - blackboard.set("target_distance", 15.0) - - print("创建复杂的行为树结构...") - - # 创建根节点 - 优先级选择器 - root_config = BTNodeConfig(name="Root", priority=10) - root = SelectorNode(root_config) - - # 1. 生存行为序列 - survival_sequence = SequenceNode(BTNodeConfig(name="SurvivalSequence")) - - # 检查生命值 - health_condition = ConditionNode( - "HealthCheck", - lambda bb: bb.get("player_health", 0) > 30 - ) - - # 寻找掩护动作 - find_cover_action = ActionNode( - "FindCover", - lambda bb: print("🛡️ 寻找掩护...") or - (bb.set("cover_available", random.random() > 0.3) or True) - ) - - survival_sequence.add_child(health_condition) - survival_sequence.add_child(find_cover_action) - - # 2. 战斗行为序列 - combat_sequence = SequenceNode(BTNodeConfig(name="CombatSequence")) - - # 检查敌人 - enemy_condition = ConditionNode( - "EnemyCheck", - lambda bb: bb.get("enemy_detected", False) - ) - - # 攻击并移动并行节点 - attack_move_parallel = ParallelNode( - BTNodeConfig(name="AttackMoveParallel"), - success_threshold=2, - failure_threshold=1 - ) - - # 攻击序列 - attack_sequence = SequenceNode(BTNodeConfig(name="AttackSequence")) - - # 检查弹药 - ammo_condition = ConditionNode( - "AmmoCheck", - lambda bb: bb.get("ammo_count", 0) > 0 - ) - - # 射击动作(带概率和超时) - shoot_action = ProbabilityNode( - BTNodeConfig(name="Shoot"), - success_probability=0.8, - action_func=lambda bb: ( - print(f"🔫 射击! 剩余弹药: {bb.get('ammo_count', 0)}") or - bb.set("ammo_count", bb.get("ammo_count", 0) - 1) or - True - ) - ) - - # 重新装弹动作 - reload_action = ActionNode( - "Reload", - lambda bb: ( - print("🔄 重新装弹...") or - time.sleep(0.1) or # 模拟装弹时间 - bb.set("ammo_count", 30) or - True - ) - ) - - # 装弹序列(带计时器) - reload_sequence = SequenceNode(BTNodeConfig(name="ReloadSequence")) - reload_timer = TimerNode(reload_action, BTNodeConfig(name="ReloadTimer"), timeout=2.0) - reload_sequence.add_child(reload_timer) - - # 弹药管理选择器 - ammo_selector = SelectorNode(BTNodeConfig(name="AmmoSelector")) - ammo_selector.add_child(shoot_action) - ammo_selector.add_child(reload_sequence) - - attack_sequence.add_child(ammo_condition) - attack_sequence.add_child(ammo_selector) - - # 移动动作 - move_action = ActionNode( - "Move", - lambda bb: print(f"🏃 移动中... 距离目标: {bb.get('target_distance', 0):.1f}m") or - bb.set("target_distance", max(0, bb.get('target_distance', 0) - random.uniform(1, 3))) or - True - ) - - attack_move_parallel.add_child(attack_sequence) - attack_move_parallel.add_child(move_action) - - combat_sequence.add_child(enemy_condition) - combat_sequence.add_child(attack_move_parallel) - - # 3. 巡逻行为(带重复和等待) - patrol_sequence = SequenceNode(BTNodeConfig(name="PatrolSequence")) - - # 巡逻动作(重复3次) - patrol_action = ActionNode( - "Patrol", - lambda bb: print("🔍 巡逻中...") or True - ) - patrol_repeater = RepeaterNode(patrol_action, BTNodeConfig(name="PatrolRepeater"), count=3) - - # 等待 - wait_action = WaitNode("PatrolWait", duration=1.0, random_variance=0.5) - - patrol_sequence.add_child(patrol_repeater) - patrol_sequence.add_child(wait_action) - - # 组装根节点 - root.add_child(survival_sequence) - root.add_child(combat_sequence) - root.add_child(patrol_sequence) - - # 创建行为树 - behavior_tree = bt_plugin.create_behavior_tree("advanced_ai", root) - behavior_tree.set_blackboard(blackboard) - - print("✅ 行为树创建成功") - print(f"行为树统计: {behavior_tree.get_statistics()}") - - # 执行行为树多次 - print("\n开始执行行为树...") - for i in range(10): - print(f"\n--- 第 {i+1} 次执行 ---") - - # 模拟环境变化 - if i == 3: - blackboard.set("enemy_detected", True) - print("🚨 发现敌人!") - elif i == 6: - blackboard.set("player_health", 20) - print("💔 受到伤害,生命值降低!") - elif i == 8: - blackboard.set("enemy_detected", False) - print("✅ 威胁解除!") - - # 执行行为树 - result = behavior_tree.run() - print(f"执行结果: {result.value}") - - # 显示黑板状态 - print(f"当前状态 - 生命值: {blackboard.get('player_health')}, " - f"弹药: {blackboard.get('ammo_count')}, " - f"敌人: {blackboard.get('enemy_detected')}") - - # 显示最终统计 - print(f"\n最终统计: {behavior_tree.get_statistics()}") - print(f"黑板统计: {blackboard.get_statistics()}") - - except Exception as e: - print(f"❌ 高级行为树示例执行失败: {e}") - import traceback - traceback.print_exc() - finally: - # 清理 - plugin_manager.disable_plugin("behavior_tree") - plugin_manager.unload_plugin("behavior_tree") - -def advanced_state_machine_example(): - """ - 高级状态机示例 - 展示复杂的状态机功能 - """ - print("\n=== 高级状态机示例 ===") - - # 导入插件管理器 - from plugins.plugin_manager import PluginManager - - # 创建一个虚拟的世界对象 - class MockWorld: - def __init__(self): - self.render = None - self.accept = lambda event, handler: None - self.ignore = lambda event: None - self.taskMgr = None - - # 创建世界实例 - world = MockWorld() - - # 创建插件管理器 - plugin_manager = PluginManager(world) - plugin_manager.enable_plugin_system() - - # 加载状态机插件 - sm_plugin = plugin_manager.load_plugin("state_machine") - if not sm_plugin or not plugin_manager.enable_plugin("state_machine"): - print("❌ 无法加载或启用状态机插件") - return - - try: - # 导入状态机组件 - from plugins.user.state_machine import ( - State, StateContext, StateTransition, - HierarchicalState, StateType - ) - - # 创建状态上下文 - context = StateContext() - context.set("player_health", 100) - context.set("player_level", 1) - context.set("experience", 0) - context.set("game_time", 0) - - print("创建复杂的状态机结构...") - - # 定义基础状态 - class IdleState(State): - def __init__(self): - super().__init__("Idle", StateType.ACTIVE) - - def enter(self, context): - print("🎮 进入空闲状态") - context.set("state", "idle") - - def execute(self, context): - print("⏸️ 空闲中...") - - # 添加转换条件 - if not self.transitions: - # 转换到移动状态 - move_condition = lambda ctx: ctx.get("want_move", False) - move_action = lambda ctx: print("🏃 准备移动") - self.add_transition_simple("Moving", move_condition, move_action) - - # 转换到战斗状态 - combat_condition = lambda ctx: ctx.get("enemy_nearby", False) - combat_action = lambda ctx: print("⚔️ 进入战斗") - self.add_transition_simple("Combat", combat_condition, combat_action) - - # 检查可用转换 - available = self.get_available_transitions(context) - if available: - return available[0].to_state - return None - - def exit(self, context): - print("🔚 退出空闲状态") - - class MovingState(HierarchicalState): - def __init__(self): - super().__init__("Moving", StateType.ACTIVE) - # 添加子状态 - self.add_sub_state(WalkingState()) - self.add_sub_state(RunningState()) - self.set_default_sub_state("Walking") - - def enter(self, context): - print("🚗 进入移动状态") - context.set("state", "moving") - # 进入默认子状态 - self.enter_sub_state(context, "Walking") - - def execute(self, context): - print("➡️ 移动中...") - - # 执行子状态 - if self.current_sub_state: - self.current_sub_state.execute(context) - - # 添加转换条件 - if not self.transitions: - # 转换到空闲状态 - idle_condition = lambda ctx: not ctx.get("want_move", True) - idle_action = lambda ctx: print("⏸️ 停止移动") - self.add_transition_simple("Idle", idle_condition, idle_action) - - # 转换到战斗状态 - combat_condition = lambda ctx: ctx.get("enemy_nearby", False) - combat_action = lambda ctx: print("⚔️ 移动中遭遇敌人") - self.add_transition_simple("Combat", combat_condition, combat_action) - - # 检查可用转换 - available = self.get_available_transitions(context) - if available: - return available[0].to_state - return None - - def exit(self, context): - print("🔚 退出移动状态") - # 退出子状态 - self.exit_sub_state(context) - - class WalkingState(State): - def __init__(self): - super().__init__("Walking", StateType.ACTIVE) - - def enter(self, context): - print("🚶 进入步行状态") - context.set("movement_type", "walking") - - def execute(self, context): - print("🚶 步行中...") - # 随机切换到跑步 - if random.random() < 0.1: - return "Running" - return None - - def exit(self, context): - print("🔚 退出步行状态") - - class RunningState(State): - def __init__(self): - super().__init__("Running", StateType.ACTIVE) - - def enter(self, context): - print("🏃 进入跑步状态") - context.set("movement_type", "running") - - def execute(self, context): - print("🏃 跑步中...") - # 随机切换到步行 - if random.random() < 0.05: - return "Walking" - return None - - def exit(self, context): - print("🔚 退出跑步状态") - - class CombatState(HierarchicalState): - def __init__(self): - super().__init__("Combat", StateType.ACTIVE) - # 添加战斗子状态 - self.add_sub_state(AttackingState()) - self.add_sub_state(DefendingState()) - self.add_sub_state(FleeingState()) - self.set_default_sub_state("Attacking") - - def enter(self, context): - print("⚔️ 进入战斗状态") - context.set("state", "combat") - context.set("combat_start_time", time.time()) - # 进入默认子状态 - self.enter_sub_state(context, "Attacking") - - def execute(self, context): - print("⚔️ 战斗中...") - - # 执行子状态 - if self.current_sub_state: - result = self.current_sub_state.execute(context) - if result: - # 切换子状态 - self.enter_sub_state(context, result) - - # 添加转换条件 - if not self.transitions: - # 转换到空闲状态 - idle_condition = lambda ctx: not ctx.get("enemy_nearby", True) - idle_action = lambda ctx: print("✅ 战斗结束") - self.add_transition_simple("Idle", idle_condition, idle_action) - - # 检查可用转换 - available = self.get_available_transitions(context) - if available: - return available[0].to_state - return None - - def exit(self, context): - print("🔚 退出战斗状态") - combat_time = time.time() - context.get("combat_start_time", time.time()) - print(f"战斗持续时间: {combat_time:.2f}秒") - # 退出子状态 - self.exit_sub_state(context) - - class AttackingState(State): - def __init__(self): - super().__init__("Attacking", StateType.ACTIVE) - - def enter(self, context): - print("⚔️ 进入攻击状态") - context.set("combat_action", "attacking") - - def execute(self, context): - print("⚔️ 攻击中...") - # 随机切换到防御或逃跑 - rand = random.random() - if rand < 0.05: - return "Defending" - elif rand < 0.1: - return "Fleeing" - return None - - def exit(self, context): - print("🔚 退出攻击状态") - - class DefendingState(State): - def __init__(self): - super().__init__("Defending", StateType.ACTIVE) - - def enter(self, context): - print("🛡️ 进入防御状态") - context.set("combat_action", "defending") - - def execute(self, context): - print("🛡️ 防御中...") - # 随机切换到攻击 - if random.random() < 0.2: - return "Attacking" - return None - - def exit(self, context): - print("🔚 退出防御状态") - - class FleeingState(State): - def __init__(self): - super().__init__("Fleeing", StateType.ACTIVE) - - def enter(self, context): - print("🏃 进入逃跑状态") - context.set("combat_action", "fleeing") - - def execute(self, context): - print("🏃 逃跑中...") - # 随机切换到攻击 - if random.random() < 0.3: - return "Attacking" - return None - - def exit(self, context): - print("🔚 退出逃跑状态") - - # 创建状态机 - state_machine = sm_plugin.create_state_machine("advanced_game_character") - state_machine.set_context(context) - - # 添加状态 - state_machine.add_state(IdleState()) - state_machine.add_state(MovingState()) - state_machine.add_state(CombatState()) - - # 设置初始状态 - state_machine.change_state("Idle") - state_machine.start() - - print("✅ 状态机创建成功") - - # 添加状态变更回调 - def state_change_callback(old_state, new_state): - old_name = old_state.name if old_state else "None" - new_name = new_state.name if new_state else "None" - print(f"🔄 状态变更: {old_name} -> {new_name}") - - state_machine.add_state_change_callback(state_change_callback) - - # 更新状态机多次 - print("\n开始更新状态机...") - for i in range(15): - print(f"\n--- 第 {i+1} 次更新 ---") - print(f"当前状态: {state_machine.get_current_state_name()}") - - # 模拟环境变化 - if i == 3: - context.set("want_move", True) - print("🎮 角色想要移动") - elif i == 5: - context.set("enemy_nearby", True) - print("🚨 附近出现敌人") - elif i == 10: - context.set("want_move", False) - context.set("enemy_nearby", False) - print("✅ 状态恢复正常") - - # 更新状态机 - state_machine.update() - - # 显示上下文状态 - print(f"上下文 - 移动: {context.get('want_move', False)}, " - f"敌人: {context.get('enemy_nearby', False)}, " - f"动作: {context.get('combat_action', 'none')}") - - # 显示最终统计 - print(f"\n最终统计: {state_machine.get_statistics()}") - - except Exception as e: - print(f"❌ 高级状态机示例执行失败: {e}") - import traceback - traceback.print_exc() - finally: - # 清理 - plugin_manager.disable_plugin("state_machine") - plugin_manager.unload_plugin("state_machine") - -def main(): - """ - 主函数 - """ - print("行为树和状态机插件完整示例") - print("=" * 50) - - # 运行高级行为树示例 - advanced_behavior_tree_example() - - # 运行高级状态机示例 - advanced_state_machine_example() - - print("\n🎉 所有示例执行完成!") - -if __name__ == "__main__": +""" +行为树和状态机插件完整示例 +演示高级功能和复杂用法 +""" + +import sys +import os +import time +import random + +# 添加项目根目录到Python路径 +sys.path.insert(0, '/home/hello/EG') + +def advanced_behavior_tree_example(): + """ + 高级行为树示例 + 展示复杂的行为树功能 + """ + print("=== 高级行为树示例 ===") + + # 导入插件管理器 + from plugins.plugin_manager import PluginManager + + # 创建一个虚拟的世界对象 + class MockWorld: + def __init__(self): + self.render = None + self.accept = lambda event, handler: None + self.ignore = lambda event: None + self.taskMgr = None + + # 创建世界实例 + world = MockWorld() + + # 创建插件管理器 + plugin_manager = PluginManager(world) + plugin_manager.enable_plugin_system() + + # 加载行为树插件 + bt_plugin = plugin_manager.load_plugin("behavior_tree") + if not bt_plugin or not plugin_manager.enable_plugin("behavior_tree"): + print("❌ 无法加载或启用行为树插件") + return + + try: + # 导入行为树组件 + from plugins.user.behavior_tree import ( + SelectorNode, SequenceNode, ParallelNode, + RepeaterNode, InverterNode, TimerNode, + ConditionNode, ActionNode, WaitNode, ProbabilityNode, + BTNodeConfig + ) + + # 创建黑板 + blackboard = bt_plugin.create_blackboard("advanced_example", max_history_size=500) + + # 设置初始数据 + blackboard.set("player_health", 100) + blackboard.set("enemy_detected", False) + blackboard.set("ammo_count", 30) + blackboard.set("cover_available", True) + blackboard.set("target_distance", 15.0) + + print("创建复杂的行为树结构...") + + # 创建根节点 - 优先级选择器 + root_config = BTNodeConfig(name="Root", priority=10) + root = SelectorNode(root_config) + + # 1. 生存行为序列 + survival_sequence = SequenceNode(BTNodeConfig(name="SurvivalSequence")) + + # 检查生命值 + health_condition = ConditionNode( + "HealthCheck", + lambda bb: bb.get("player_health", 0) > 30 + ) + + # 寻找掩护动作 + find_cover_action = ActionNode( + "FindCover", + lambda bb: print("🛡️ 寻找掩护...") or + (bb.set("cover_available", random.random() > 0.3) or True) + ) + + survival_sequence.add_child(health_condition) + survival_sequence.add_child(find_cover_action) + + # 2. 战斗行为序列 + combat_sequence = SequenceNode(BTNodeConfig(name="CombatSequence")) + + # 检查敌人 + enemy_condition = ConditionNode( + "EnemyCheck", + lambda bb: bb.get("enemy_detected", False) + ) + + # 攻击并移动并行节点 + attack_move_parallel = ParallelNode( + BTNodeConfig(name="AttackMoveParallel"), + success_threshold=2, + failure_threshold=1 + ) + + # 攻击序列 + attack_sequence = SequenceNode(BTNodeConfig(name="AttackSequence")) + + # 检查弹药 + ammo_condition = ConditionNode( + "AmmoCheck", + lambda bb: bb.get("ammo_count", 0) > 0 + ) + + # 射击动作(带概率和超时) + shoot_action = ProbabilityNode( + BTNodeConfig(name="Shoot"), + success_probability=0.8, + action_func=lambda bb: ( + print(f"🔫 射击! 剩余弹药: {bb.get('ammo_count', 0)}") or + bb.set("ammo_count", bb.get("ammo_count", 0) - 1) or + True + ) + ) + + # 重新装弹动作 + reload_action = ActionNode( + "Reload", + lambda bb: ( + print("🔄 重新装弹...") or + time.sleep(0.1) or # 模拟装弹时间 + bb.set("ammo_count", 30) or + True + ) + ) + + # 装弹序列(带计时器) + reload_sequence = SequenceNode(BTNodeConfig(name="ReloadSequence")) + reload_timer = TimerNode(reload_action, BTNodeConfig(name="ReloadTimer"), timeout=2.0) + reload_sequence.add_child(reload_timer) + + # 弹药管理选择器 + ammo_selector = SelectorNode(BTNodeConfig(name="AmmoSelector")) + ammo_selector.add_child(shoot_action) + ammo_selector.add_child(reload_sequence) + + attack_sequence.add_child(ammo_condition) + attack_sequence.add_child(ammo_selector) + + # 移动动作 + move_action = ActionNode( + "Move", + lambda bb: print(f"🏃 移动中... 距离目标: {bb.get('target_distance', 0):.1f}m") or + bb.set("target_distance", max(0, bb.get('target_distance', 0) - random.uniform(1, 3))) or + True + ) + + attack_move_parallel.add_child(attack_sequence) + attack_move_parallel.add_child(move_action) + + combat_sequence.add_child(enemy_condition) + combat_sequence.add_child(attack_move_parallel) + + # 3. 巡逻行为(带重复和等待) + patrol_sequence = SequenceNode(BTNodeConfig(name="PatrolSequence")) + + # 巡逻动作(重复3次) + patrol_action = ActionNode( + "Patrol", + lambda bb: print("🔍 巡逻中...") or True + ) + patrol_repeater = RepeaterNode(patrol_action, BTNodeConfig(name="PatrolRepeater"), count=3) + + # 等待 + wait_action = WaitNode("PatrolWait", duration=1.0, random_variance=0.5) + + patrol_sequence.add_child(patrol_repeater) + patrol_sequence.add_child(wait_action) + + # 组装根节点 + root.add_child(survival_sequence) + root.add_child(combat_sequence) + root.add_child(patrol_sequence) + + # 创建行为树 + behavior_tree = bt_plugin.create_behavior_tree("advanced_ai", root) + behavior_tree.set_blackboard(blackboard) + + print("✅ 行为树创建成功") + print(f"行为树统计: {behavior_tree.get_statistics()}") + + # 执行行为树多次 + print("\n开始执行行为树...") + for i in range(10): + print(f"\n--- 第 {i+1} 次执行 ---") + + # 模拟环境变化 + if i == 3: + blackboard.set("enemy_detected", True) + print("🚨 发现敌人!") + elif i == 6: + blackboard.set("player_health", 20) + print("💔 受到伤害,生命值降低!") + elif i == 8: + blackboard.set("enemy_detected", False) + print("✅ 威胁解除!") + + # 执行行为树 + result = behavior_tree.run() + print(f"执行结果: {result.value}") + + # 显示黑板状态 + print(f"当前状态 - 生命值: {blackboard.get('player_health')}, " + f"弹药: {blackboard.get('ammo_count')}, " + f"敌人: {blackboard.get('enemy_detected')}") + + # 显示最终统计 + print(f"\n最终统计: {behavior_tree.get_statistics()}") + print(f"黑板统计: {blackboard.get_statistics()}") + + except Exception as e: + print(f"❌ 高级行为树示例执行失败: {e}") + import traceback + traceback.print_exc() + finally: + # 清理 + plugin_manager.disable_plugin("behavior_tree") + plugin_manager.unload_plugin("behavior_tree") + +def advanced_state_machine_example(): + """ + 高级状态机示例 + 展示复杂的状态机功能 + """ + print("\n=== 高级状态机示例 ===") + + # 导入插件管理器 + from plugins.plugin_manager import PluginManager + + # 创建一个虚拟的世界对象 + class MockWorld: + def __init__(self): + self.render = None + self.accept = lambda event, handler: None + self.ignore = lambda event: None + self.taskMgr = None + + # 创建世界实例 + world = MockWorld() + + # 创建插件管理器 + plugin_manager = PluginManager(world) + plugin_manager.enable_plugin_system() + + # 加载状态机插件 + sm_plugin = plugin_manager.load_plugin("state_machine") + if not sm_plugin or not plugin_manager.enable_plugin("state_machine"): + print("❌ 无法加载或启用状态机插件") + return + + try: + # 导入状态机组件 + from plugins.user.state_machine import ( + State, StateContext, StateTransition, + HierarchicalState, StateType + ) + + # 创建状态上下文 + context = StateContext() + context.set("player_health", 100) + context.set("player_level", 1) + context.set("experience", 0) + context.set("game_time", 0) + + print("创建复杂的状态机结构...") + + # 定义基础状态 + class IdleState(State): + def __init__(self): + super().__init__("Idle", StateType.ACTIVE) + + def enter(self, context): + print("🎮 进入空闲状态") + context.set("state", "idle") + + def execute(self, context): + print("⏸️ 空闲中...") + + # 添加转换条件 + if not self.transitions: + # 转换到移动状态 + move_condition = lambda ctx: ctx.get("want_move", False) + move_action = lambda ctx: print("🏃 准备移动") + self.add_transition_simple("Moving", move_condition, move_action) + + # 转换到战斗状态 + combat_condition = lambda ctx: ctx.get("enemy_nearby", False) + combat_action = lambda ctx: print("⚔️ 进入战斗") + self.add_transition_simple("Combat", combat_condition, combat_action) + + # 检查可用转换 + available = self.get_available_transitions(context) + if available: + return available[0].to_state + return None + + def exit(self, context): + print("🔚 退出空闲状态") + + class MovingState(HierarchicalState): + def __init__(self): + super().__init__("Moving", StateType.ACTIVE) + # 添加子状态 + self.add_sub_state(WalkingState()) + self.add_sub_state(RunningState()) + self.set_default_sub_state("Walking") + + def enter(self, context): + print("🚗 进入移动状态") + context.set("state", "moving") + # 进入默认子状态 + self.enter_sub_state(context, "Walking") + + def execute(self, context): + print("➡️ 移动中...") + + # 执行子状态 + if self.current_sub_state: + self.current_sub_state.execute(context) + + # 添加转换条件 + if not self.transitions: + # 转换到空闲状态 + idle_condition = lambda ctx: not ctx.get("want_move", True) + idle_action = lambda ctx: print("⏸️ 停止移动") + self.add_transition_simple("Idle", idle_condition, idle_action) + + # 转换到战斗状态 + combat_condition = lambda ctx: ctx.get("enemy_nearby", False) + combat_action = lambda ctx: print("⚔️ 移动中遭遇敌人") + self.add_transition_simple("Combat", combat_condition, combat_action) + + # 检查可用转换 + available = self.get_available_transitions(context) + if available: + return available[0].to_state + return None + + def exit(self, context): + print("🔚 退出移动状态") + # 退出子状态 + self.exit_sub_state(context) + + class WalkingState(State): + def __init__(self): + super().__init__("Walking", StateType.ACTIVE) + + def enter(self, context): + print("🚶 进入步行状态") + context.set("movement_type", "walking") + + def execute(self, context): + print("🚶 步行中...") + # 随机切换到跑步 + if random.random() < 0.1: + return "Running" + return None + + def exit(self, context): + print("🔚 退出步行状态") + + class RunningState(State): + def __init__(self): + super().__init__("Running", StateType.ACTIVE) + + def enter(self, context): + print("🏃 进入跑步状态") + context.set("movement_type", "running") + + def execute(self, context): + print("🏃 跑步中...") + # 随机切换到步行 + if random.random() < 0.05: + return "Walking" + return None + + def exit(self, context): + print("🔚 退出跑步状态") + + class CombatState(HierarchicalState): + def __init__(self): + super().__init__("Combat", StateType.ACTIVE) + # 添加战斗子状态 + self.add_sub_state(AttackingState()) + self.add_sub_state(DefendingState()) + self.add_sub_state(FleeingState()) + self.set_default_sub_state("Attacking") + + def enter(self, context): + print("⚔️ 进入战斗状态") + context.set("state", "combat") + context.set("combat_start_time", time.time()) + # 进入默认子状态 + self.enter_sub_state(context, "Attacking") + + def execute(self, context): + print("⚔️ 战斗中...") + + # 执行子状态 + if self.current_sub_state: + result = self.current_sub_state.execute(context) + if result: + # 切换子状态 + self.enter_sub_state(context, result) + + # 添加转换条件 + if not self.transitions: + # 转换到空闲状态 + idle_condition = lambda ctx: not ctx.get("enemy_nearby", True) + idle_action = lambda ctx: print("✅ 战斗结束") + self.add_transition_simple("Idle", idle_condition, idle_action) + + # 检查可用转换 + available = self.get_available_transitions(context) + if available: + return available[0].to_state + return None + + def exit(self, context): + print("🔚 退出战斗状态") + combat_time = time.time() - context.get("combat_start_time", time.time()) + print(f"战斗持续时间: {combat_time:.2f}秒") + # 退出子状态 + self.exit_sub_state(context) + + class AttackingState(State): + def __init__(self): + super().__init__("Attacking", StateType.ACTIVE) + + def enter(self, context): + print("⚔️ 进入攻击状态") + context.set("combat_action", "attacking") + + def execute(self, context): + print("⚔️ 攻击中...") + # 随机切换到防御或逃跑 + rand = random.random() + if rand < 0.05: + return "Defending" + elif rand < 0.1: + return "Fleeing" + return None + + def exit(self, context): + print("🔚 退出攻击状态") + + class DefendingState(State): + def __init__(self): + super().__init__("Defending", StateType.ACTIVE) + + def enter(self, context): + print("🛡️ 进入防御状态") + context.set("combat_action", "defending") + + def execute(self, context): + print("🛡️ 防御中...") + # 随机切换到攻击 + if random.random() < 0.2: + return "Attacking" + return None + + def exit(self, context): + print("🔚 退出防御状态") + + class FleeingState(State): + def __init__(self): + super().__init__("Fleeing", StateType.ACTIVE) + + def enter(self, context): + print("🏃 进入逃跑状态") + context.set("combat_action", "fleeing") + + def execute(self, context): + print("🏃 逃跑中...") + # 随机切换到攻击 + if random.random() < 0.3: + return "Attacking" + return None + + def exit(self, context): + print("🔚 退出逃跑状态") + + # 创建状态机 + state_machine = sm_plugin.create_state_machine("advanced_game_character") + state_machine.set_context(context) + + # 添加状态 + state_machine.add_state(IdleState()) + state_machine.add_state(MovingState()) + state_machine.add_state(CombatState()) + + # 设置初始状态 + state_machine.change_state("Idle") + state_machine.start() + + print("✅ 状态机创建成功") + + # 添加状态变更回调 + def state_change_callback(old_state, new_state): + old_name = old_state.name if old_state else "None" + new_name = new_state.name if new_state else "None" + print(f"🔄 状态变更: {old_name} -> {new_name}") + + state_machine.add_state_change_callback(state_change_callback) + + # 更新状态机多次 + print("\n开始更新状态机...") + for i in range(15): + print(f"\n--- 第 {i+1} 次更新 ---") + print(f"当前状态: {state_machine.get_current_state_name()}") + + # 模拟环境变化 + if i == 3: + context.set("want_move", True) + print("🎮 角色想要移动") + elif i == 5: + context.set("enemy_nearby", True) + print("🚨 附近出现敌人") + elif i == 10: + context.set("want_move", False) + context.set("enemy_nearby", False) + print("✅ 状态恢复正常") + + # 更新状态机 + state_machine.update() + + # 显示上下文状态 + print(f"上下文 - 移动: {context.get('want_move', False)}, " + f"敌人: {context.get('enemy_nearby', False)}, " + f"动作: {context.get('combat_action', 'none')}") + + # 显示最终统计 + print(f"\n最终统计: {state_machine.get_statistics()}") + + except Exception as e: + print(f"❌ 高级状态机示例执行失败: {e}") + import traceback + traceback.print_exc() + finally: + # 清理 + plugin_manager.disable_plugin("state_machine") + plugin_manager.unload_plugin("state_machine") + +def main(): + """ + 主函数 + """ + print("行为树和状态机插件完整示例") + print("=" * 50) + + # 运行高级行为树示例 + advanced_behavior_tree_example() + + # 运行高级状态机示例 + advanced_state_machine_example() + + print("\n🎉 所有示例执行完成!") + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/plugins/user/advanced_skeletal_animation/README.md b/plugins/user/advanced_skeletal_animation/README.md index 19ba9c56..428748bb 100644 --- a/plugins/user/advanced_skeletal_animation/README.md +++ b/plugins/user/advanced_skeletal_animation/README.md @@ -1,524 +1,524 @@ -# 高级骨骼动画系统插件 - -## 概述 - -高级骨骼动画系统插件为EG项目提供了一个完整且功能丰富的骨骼动画解决方案。该插件基于Panda3D的Actor系统,提供了从基础动画播放到高级动画控制的全套功能,包括动画状态机、逆向运动学、物理集成、动画压缩、网络同步和性能优化等。 - -## 功能特性 - -### 核心功能 -1. **Actor模型加载** - 支持加载带有骨骼动画的3D模型 -2. **动画播放控制** - 播放、停止、暂停、恢复动画 -3. **速度调节** - 可调节动画播放速度 -4. **动画混合** - 支持多个动画之间的平滑混合 -5. **序列播放** - 支持动画序列的连续播放 -6. **事件驱动** - 通过事件系统控制动画播放 - -### 高级功能 -1. **动画状态机** - 管理复杂的动画状态转换和混合 -2. **逆向运动学系统** - 实现骨骼的IK控制 -3. **动画曲线编辑器** - 支持对动画属性进行曲线编辑和自定义 -4. **物理动画集成** - 将物理模拟与骨骼动画结合 - -### 性能优化 -1. **动画压缩** - 支持关键帧压缩、量化等多种压缩算法 -2. **LOD系统** - 根据距离和性能自动调整动画质量 -3. **智能缓存** - 提供高效的动画数据缓存机制 - -### 网络功能 -1. **动画状态同步** - 支持多人环境中动画状态的同步 -2. **分布式动画管理** - 管理网络环境中的动画同步 -3. **状态序列化** - 支持动画状态的序列化和反序列化 - -### 工具系统 -1. **动画分析器** - 提供动画性能、质量和内存使用分析 -2. **动画编辑器** - 支持关键帧编辑、曲线编辑等功能 -3. **调试可视化工具** - 提供骨骼、边界框等可视化功能 -4. **性能分析器** - 提供详细的性能分析和优化建议 - -## 系统架构 - -插件采用模块化设计,包含以下核心组件: - -1. **SkeletalAnimationCore** - 核心动画管理器,负责Actor和动画的管理 -2. **AnimationNetworkSync** - 网络同步系统,处理网络环境中的动画同步 -3. **AnimationStateMachine** - 动画状态机,管理复杂的动画状态转换 -4. **AnimationCurveEditor** - 动画曲线编辑器,支持动画属性的曲线控制 -5. **PhysicsAnimationIntegration** - 物理动画集成系统,结合物理模拟与动画 -6. **AnimationCompressor** - 动画压缩系统,减少动画数据大小 -7. **AnimationLODSystem** - 动画LOD系统,根据条件调整动画质量 -8. **AnimationCacheManager** - 动画缓存管理器,提高动画加载性能 -9. **AnimationAnalyzer** - 动画分析器,分析动画性能和质量 -10. **AnimationEditor** - 动画编辑器,提供关键帧编辑功能 -11. **DebugVisualizer** - 调试可视化工具,提供可视化调试功能 -12. **AnimationProfiler** - 性能分析器,分析动画系统性能 - -## 安装和启用 - -### 启用插件 - -在主程序中启用插件: - -```python -# 启用插件系统 -world.plugin_manager.enable_plugin_system() - -# 启用高级骨骼动画插件 -world.plugin_manager.enable_plugin("advanced_skeletal_animation") -``` - -## 使用方法 - -### 基本使用 - -```python -# 获取插件实例 -plugin = world.plugin_manager.get_plugin("advanced_skeletal_animation") -core_system = plugin.get_core_system() - -# 加载Actor模型 -actor, actor_id = core_system.load_actor( - model_path="path/to/model.egg", - anim_dict={ - "walk": "path/to/walk.egg", - "run": "path/to/run.egg" - }, - actor_id="my_hero", # 可选,自动生成如果未提供 - position=(0, 0, 0), # 可选 - scale=1.0 # 可选 -) - -# 播放动画 -if actor: - core_system.play_animation(actor_id, "walk", loop=True) -``` - -### 高级使用 - -```python -# 动画混合 -core_system.blend_animations( - actor_id, - "walk", - "run", - blend_time=2.0 -) - -# 获取动画信息 -anim_info = core_system.get_animation_info(actor_id, "walk") -actor_info = core_system.get_actor_info(actor_id) -``` - -### 状态机使用 - -```python -from plugins.user.advanced_skeletal_animation.advanced import AnimationStateMachine - -# 创建状态机 -state_machine = AnimationStateMachine(actor) - -# 添加状态 -state_machine.add_state("idle", "idle_anim", loop=True) -state_machine.add_state("walk", "walk_anim", loop=True) -state_machine.add_state("run", "run_anim", loop=True) - -# 设置状态转换 -state_machine.set_transition("idle", "walk") -state_machine.set_transition("walk", "run") -state_machine.set_transition("run", "idle") - -# 进入初始状态 -state_machine.enter_state("idle") - -# 在更新循环中更新状态机 -def state_machine_update(task): - dt = globalClock.getDt() - state_machine.update(dt) - return task.cont - -world.taskMgr.add(state_machine_update, "state_machine_update") -``` - -### 网络同步使用 - -```python -# 获取网络系统 -network_system = plugin.get_network_system() - -# 设置网络模式 -from plugins.user.advanced_skeletal_animation.network import NetworkMode, SyncMode -network_system.set_network_mode(NetworkMode.SERVER) -network_system.set_sync_mode(SyncMode.STATE_SYNC) - -# 注册Actor进行网络同步 -network_id = network_system.register_actor_for_sync(actor_id) -``` - -### 工具系统使用 - -```python -# 获取分析器 -analyzer = plugin.get_analyzer() -analyzer.start_analysis() - -# 分析动画 -analysis_result = analyzer.analyze_animation(actor_id, "walk", AnimationAnalysisType.PERFORMANCE) - -# 获取编辑器 -editor = plugin.get_editor() -editor.start_editing(actor_id, "walk") - -# 获取可视化工具 -visualizer = plugin.get_visualizer() -visualizer.enable_visualization(True) -visualizer.set_mode(DebugVisualizationMode.SKELETON) - -# 获取性能分析器 -profiler = plugin.get_profiler() -profiler.start_profiling() -``` - -## API参考 - -### SkeletalAnimationCore 类 - -#### load_actor(model_path, anim_dict=None, actor_id=None, position=None, scale=None) -加载Actor模型 - -**参数:** -- `model_path` (str): 模型文件路径 -- `anim_dict` (dict): 动画文件路径字典,格式 {'anim_name': 'anim_path'} -- `actor_id` (str): Actor标识符,如果为None则自动生成 -- `position` (tuple): 位置坐标 (x, y, z) -- `scale` (float or tuple): 缩放比例 - -**返回:** -- Actor对象和actor_id - -#### unload_actor(actor_id) -卸载Actor - -**参数:** -- `actor_id` (str): 要卸载的Actor标识符 - -#### play_animation(actor_id, anim_name, loop=False, play_rate=1.0, start_frame=None, end_frame=None) -播放动画 - -**参数:** -- `actor_id` (str): Actor标识符 -- `anim_name` (str): 动画名称 -- `loop` (bool): 是否循环播放 -- `play_rate` (float): 播放速度 -- `start_frame` (int): 起始帧 -- `end_frame` (int): 结束帧 - -#### stop_animation(actor_id, anim_name=None) -停止动画 - -**参数:** -- `actor_id` (str): Actor标识符 -- `anim_name` (str): 动画名称,如果为None则停止所有动画 - -#### set_animation_speed(actor_id, anim_name, speed) -设置动画播放速度 - -**参数:** -- `actor_id` (str): Actor标识符 -- `anim_name` (str): 动画名称 -- `speed` (float): 播放速度 - -#### pause_animation(actor_id, anim_name=None) -暂停动画 - -**参数:** -- `actor_id` (str): Actor标识符 -- `anim_name` (str): 动画名称,如果为None则暂停所有动画 - -#### resume_animation(actor_id, anim_name=None) -恢复动画播放 - -**参数:** -- `actor_id` (str): Actor标识符 -- `anim_name` (str): 动画名称,如果为None则恢复所有动画 - -#### blend_animations(actor_id, from_anim, to_anim, blend_time=1.0, blend_mode='linear') -混合两个动画 - -**参数:** -- `actor_id` (str): Actor标识符 -- `from_anim` (str): 起始动画名称 -- `to_anim` (str): 目标动画名称 -- `blend_time` (float): 混合时间 -- `blend_mode` (str): 混合模式 ('linear', 'ease_in', 'ease_out', 'ease_in_out') - -#### get_actor_animations(actor_id) -获取Actor的所有动画名称 - -**参数:** -- `actor_id` (str): Actor标识符 - -**返回:** -- 动画名称列表 - -#### get_animation_info(actor_id, anim_name) -获取动画信息 - -**参数:** -- `actor_id` (str): Actor标识符 -- `anim_name` (str): 动画名称 - -**返回:** -- 动画信息字典 - -#### get_actor_info(actor_id) -获取Actor详细信息 - -**参数:** -- `actor_id` (str): Actor标识符 - -**返回:** -- Actor信息字典 - -### AnimationStateMachine 类 - -#### add_state(state_name, animations, loop=True, play_rate=1.0, blend_time=0.3) -添加动画状态 - -**参数:** -- `state_name` (str): 状态名称 -- `animations` (str or list): 动画名称或列表 -- `loop` (bool): 是否循环 -- `play_rate` (float): 播放速度 -- `blend_time` (float): 状态切换混合时间 - -#### set_transition(from_state, to_state, transition_type='smooth', condition=None, transition_time=0.3) -设置状态转换 - -**参数:** -- `from_state` (str): 起始状态 -- `to_state` (str): 目标状态 -- `transition_type` (str): 转换类型 ('immediate', 'smooth', 'blend', 'conditional') -- `condition` (function): 转换条件函数 -- `transition_time` (float): 转换时间 - -#### enter_state(state_name) -进入指定状态 - -**参数:** -- `state_name` (str): 状态名称 - -### AnimationNetworkSync 类 - -#### set_network_mode(mode) -设置网络模式 - -**参数:** -- `mode` (NetworkMode): 网络模式 (DISABLED, SERVER, CLIENT, PEER_TO_PEER) - -#### set_sync_mode(mode) -设置同步模式 - -**参数:** -- `mode` (SyncMode): 同步模式 (STATE_SYNC, INPUT_SYNC, INTERPOLATION) - -#### register_actor_for_sync(actor_id, network_id=None) -注册Actor以进行网络同步 - -**参数:** -- `actor_id` (str): Actor ID -- `network_id` (str): 网络ID,如果为None则自动生成 - -**返回:** -- 网络ID - -### AnimationCompressor 类 - -#### compress_animation(actor, anim_name, method='keyframe_reduction') -压缩指定动画 - -**参数:** -- `actor` (Actor): Actor对象 -- `anim_name` (str): 动画名称 -- `method` (str): 压缩方法 ('keyframe_reduction', 'quantization', 'delta_compression', 'rle') - -**返回:** -- 压缩后的动画数据 - -### AnimationAnalyzer 类 - -#### analyze_animation(actor_id, anim_name, analysis_type=AnimationAnalysisType.PERFORMANCE) -分析指定动画 - -**参数:** -- `actor_id` (str): Actor ID -- `anim_name` (str): 动画名称 -- `analysis_type` (AnimationAnalysisType): 分析类型 (PERFORMANCE, QUALITY, MEMORY, COMPRESSION) - -**返回:** -- 分析结果 - -### AnimationEditor 类 - -#### start_editing(actor_id, anim_name) -开始编辑动画 - -**参数:** -- `actor_id` (str): Actor ID -- `anim_name` (str): 动画名称 - -**返回:** -- 是否成功开始编辑 - -#### add_keyframe(actor_id, anim_name, bone_name, frame, position=None, rotation=None, scale=None) -添加关键帧 - -**参数:** -- `actor_id` (str): Actor ID -- `anim_name` (str): 动画名称 -- `bone_name` (str): 骨骼名称 -- `frame` (int): 帧号 -- `position` (tuple): 位置 -- `rotation` (tuple): 旋转 -- `scale` (tuple): 缩放 - -### DebugVisualizer 类 - -#### set_mode(mode) -设置可视化模式 - -**参数:** -- `mode` (DebugVisualizationMode): 模式 (SKELETON, BOUNDS, NORMALS, WIREFRAME, COLLISION) - -#### enable_visualization(enabled=True) -启用/禁用可视化 - -**参数:** -- `enabled` (bool): 是否启用 - -### AnimationProfiler 类 - -#### start_profiling() -开始性能分析 - -#### stop_profiling() -停止性能分析 - -#### get_performance_report() -获取性能报告 - -**返回:** -- 性能报告字典 - -## 支持的文件格式 - -- 模型格式: .egg, .bam, .fbx, .gltf, .glb等Panda3D支持的格式 -- 动画格式: .egg, .bam等包含动画数据的格式 - -## 配置选项 - -插件支持以下配置选项(config.json): - -```json -{ - "modules": { - "core": { - "enabled": true, - "max_actors": 1000, - "default_play_rate": 1.0, - "default_loop": false, - "blend_time": 0.3 - }, - "network": { - "enabled": true, - "sync_interval": 0.1, - "interpolation_mode": "linear", - "enable_compression": true - }, - "advanced": { - "state_machine": true, - "curve_editor": true, - "physics_integration": true - }, - "optimization": { - "compression": true, - "lod": true, - "cache": true, - "cache_size": 500 - }, - "tools": { - "analyzer": true, - "editor": true, - "visualizer": true, - "profiler": true - } - }, - "performance": { - "target_fps": 60, - "lod_threshold": 30, - "max_buffer_size": 100 - } -} -``` - -## 性能优化建议 - -1. **使用LOD系统** - 根据Actor距离摄像机的远近自动调整动画质量 -2. **启用动画压缩** - 减少内存占用,提高加载速度 -3. **合理使用缓存** - 避免重复加载相同动画数据 -4. **优化网络同步** - 根据网络状况调整同步频率 -5. **及时释放资源** - 不需要的Actor及时调用unload_actor释放 -6. **使用工具系统** - 通过分析器和性能分析器找出性能瓶颈 - -## 故障排除 - -常见问题及解决方案: - -1. **模型加载失败**: 检查文件路径和格式支持 -2. **动画无法播放**: 确认动画名称正确且与模型匹配 -3. **性能问题**: 减少同时播放的动画数量,及时卸载不需要的Actor -4. **网络同步问题**: 确认网络模式设置正确,检查防火墙设置 -5. **状态机问题**: 检查状态和转换定义是否正确 -6. **工具系统问题**: 确认工具模块已正确初始化 - -## 扩展开发 - -插件采用模块化设计,可以轻松扩展: - -1. 继承现有类添加新功能 -2. 实现自定义的压缩算法 -3. 添加新的动画控制方式 -4. 扩展网络同步协议 -5. 集成其他物理引擎 -6. 开发新的工具模块 - -## 示例代码 - -查看 `examples.py` 文件获取详细的使用示例。 - -每个模块都有完整的使用示例,可以直接运行测试。 - -## 技术支持 - -如遇到问题,请查看以下资源: - -1. 官方文档和API参考 -2. 示例代码和使用说明 -3. 社区论坛和技术支持 -4. GitHub Issues页面 - -## 版本历史 - -### v1.0.0 (当前版本) -- 初始版本发布 -- 实现完整的骨骼动画系统 -- 包含所有核心功能和高级特性 -- 提供完整的文档和示例 -- 添加工具系统支持 - -## 许可证 - -本插件遵循EG项目的许可证条款。 - -## 联系方式 - +# 高级骨骼动画系统插件 + +## 概述 + +高级骨骼动画系统插件为EG项目提供了一个完整且功能丰富的骨骼动画解决方案。该插件基于Panda3D的Actor系统,提供了从基础动画播放到高级动画控制的全套功能,包括动画状态机、逆向运动学、物理集成、动画压缩、网络同步和性能优化等。 + +## 功能特性 + +### 核心功能 +1. **Actor模型加载** - 支持加载带有骨骼动画的3D模型 +2. **动画播放控制** - 播放、停止、暂停、恢复动画 +3. **速度调节** - 可调节动画播放速度 +4. **动画混合** - 支持多个动画之间的平滑混合 +5. **序列播放** - 支持动画序列的连续播放 +6. **事件驱动** - 通过事件系统控制动画播放 + +### 高级功能 +1. **动画状态机** - 管理复杂的动画状态转换和混合 +2. **逆向运动学系统** - 实现骨骼的IK控制 +3. **动画曲线编辑器** - 支持对动画属性进行曲线编辑和自定义 +4. **物理动画集成** - 将物理模拟与骨骼动画结合 + +### 性能优化 +1. **动画压缩** - 支持关键帧压缩、量化等多种压缩算法 +2. **LOD系统** - 根据距离和性能自动调整动画质量 +3. **智能缓存** - 提供高效的动画数据缓存机制 + +### 网络功能 +1. **动画状态同步** - 支持多人环境中动画状态的同步 +2. **分布式动画管理** - 管理网络环境中的动画同步 +3. **状态序列化** - 支持动画状态的序列化和反序列化 + +### 工具系统 +1. **动画分析器** - 提供动画性能、质量和内存使用分析 +2. **动画编辑器** - 支持关键帧编辑、曲线编辑等功能 +3. **调试可视化工具** - 提供骨骼、边界框等可视化功能 +4. **性能分析器** - 提供详细的性能分析和优化建议 + +## 系统架构 + +插件采用模块化设计,包含以下核心组件: + +1. **SkeletalAnimationCore** - 核心动画管理器,负责Actor和动画的管理 +2. **AnimationNetworkSync** - 网络同步系统,处理网络环境中的动画同步 +3. **AnimationStateMachine** - 动画状态机,管理复杂的动画状态转换 +4. **AnimationCurveEditor** - 动画曲线编辑器,支持动画属性的曲线控制 +5. **PhysicsAnimationIntegration** - 物理动画集成系统,结合物理模拟与动画 +6. **AnimationCompressor** - 动画压缩系统,减少动画数据大小 +7. **AnimationLODSystem** - 动画LOD系统,根据条件调整动画质量 +8. **AnimationCacheManager** - 动画缓存管理器,提高动画加载性能 +9. **AnimationAnalyzer** - 动画分析器,分析动画性能和质量 +10. **AnimationEditor** - 动画编辑器,提供关键帧编辑功能 +11. **DebugVisualizer** - 调试可视化工具,提供可视化调试功能 +12. **AnimationProfiler** - 性能分析器,分析动画系统性能 + +## 安装和启用 + +### 启用插件 + +在主程序中启用插件: + +```python +# 启用插件系统 +world.plugin_manager.enable_plugin_system() + +# 启用高级骨骼动画插件 +world.plugin_manager.enable_plugin("advanced_skeletal_animation") +``` + +## 使用方法 + +### 基本使用 + +```python +# 获取插件实例 +plugin = world.plugin_manager.get_plugin("advanced_skeletal_animation") +core_system = plugin.get_core_system() + +# 加载Actor模型 +actor, actor_id = core_system.load_actor( + model_path="path/to/model.egg", + anim_dict={ + "walk": "path/to/walk.egg", + "run": "path/to/run.egg" + }, + actor_id="my_hero", # 可选,自动生成如果未提供 + position=(0, 0, 0), # 可选 + scale=1.0 # 可选 +) + +# 播放动画 +if actor: + core_system.play_animation(actor_id, "walk", loop=True) +``` + +### 高级使用 + +```python +# 动画混合 +core_system.blend_animations( + actor_id, + "walk", + "run", + blend_time=2.0 +) + +# 获取动画信息 +anim_info = core_system.get_animation_info(actor_id, "walk") +actor_info = core_system.get_actor_info(actor_id) +``` + +### 状态机使用 + +```python +from plugins.user.advanced_skeletal_animation.advanced import AnimationStateMachine + +# 创建状态机 +state_machine = AnimationStateMachine(actor) + +# 添加状态 +state_machine.add_state("idle", "idle_anim", loop=True) +state_machine.add_state("walk", "walk_anim", loop=True) +state_machine.add_state("run", "run_anim", loop=True) + +# 设置状态转换 +state_machine.set_transition("idle", "walk") +state_machine.set_transition("walk", "run") +state_machine.set_transition("run", "idle") + +# 进入初始状态 +state_machine.enter_state("idle") + +# 在更新循环中更新状态机 +def state_machine_update(task): + dt = globalClock.getDt() + state_machine.update(dt) + return task.cont + +world.taskMgr.add(state_machine_update, "state_machine_update") +``` + +### 网络同步使用 + +```python +# 获取网络系统 +network_system = plugin.get_network_system() + +# 设置网络模式 +from plugins.user.advanced_skeletal_animation.network import NetworkMode, SyncMode +network_system.set_network_mode(NetworkMode.SERVER) +network_system.set_sync_mode(SyncMode.STATE_SYNC) + +# 注册Actor进行网络同步 +network_id = network_system.register_actor_for_sync(actor_id) +``` + +### 工具系统使用 + +```python +# 获取分析器 +analyzer = plugin.get_analyzer() +analyzer.start_analysis() + +# 分析动画 +analysis_result = analyzer.analyze_animation(actor_id, "walk", AnimationAnalysisType.PERFORMANCE) + +# 获取编辑器 +editor = plugin.get_editor() +editor.start_editing(actor_id, "walk") + +# 获取可视化工具 +visualizer = plugin.get_visualizer() +visualizer.enable_visualization(True) +visualizer.set_mode(DebugVisualizationMode.SKELETON) + +# 获取性能分析器 +profiler = plugin.get_profiler() +profiler.start_profiling() +``` + +## API参考 + +### SkeletalAnimationCore 类 + +#### load_actor(model_path, anim_dict=None, actor_id=None, position=None, scale=None) +加载Actor模型 + +**参数:** +- `model_path` (str): 模型文件路径 +- `anim_dict` (dict): 动画文件路径字典,格式 {'anim_name': 'anim_path'} +- `actor_id` (str): Actor标识符,如果为None则自动生成 +- `position` (tuple): 位置坐标 (x, y, z) +- `scale` (float or tuple): 缩放比例 + +**返回:** +- Actor对象和actor_id + +#### unload_actor(actor_id) +卸载Actor + +**参数:** +- `actor_id` (str): 要卸载的Actor标识符 + +#### play_animation(actor_id, anim_name, loop=False, play_rate=1.0, start_frame=None, end_frame=None) +播放动画 + +**参数:** +- `actor_id` (str): Actor标识符 +- `anim_name` (str): 动画名称 +- `loop` (bool): 是否循环播放 +- `play_rate` (float): 播放速度 +- `start_frame` (int): 起始帧 +- `end_frame` (int): 结束帧 + +#### stop_animation(actor_id, anim_name=None) +停止动画 + +**参数:** +- `actor_id` (str): Actor标识符 +- `anim_name` (str): 动画名称,如果为None则停止所有动画 + +#### set_animation_speed(actor_id, anim_name, speed) +设置动画播放速度 + +**参数:** +- `actor_id` (str): Actor标识符 +- `anim_name` (str): 动画名称 +- `speed` (float): 播放速度 + +#### pause_animation(actor_id, anim_name=None) +暂停动画 + +**参数:** +- `actor_id` (str): Actor标识符 +- `anim_name` (str): 动画名称,如果为None则暂停所有动画 + +#### resume_animation(actor_id, anim_name=None) +恢复动画播放 + +**参数:** +- `actor_id` (str): Actor标识符 +- `anim_name` (str): 动画名称,如果为None则恢复所有动画 + +#### blend_animations(actor_id, from_anim, to_anim, blend_time=1.0, blend_mode='linear') +混合两个动画 + +**参数:** +- `actor_id` (str): Actor标识符 +- `from_anim` (str): 起始动画名称 +- `to_anim` (str): 目标动画名称 +- `blend_time` (float): 混合时间 +- `blend_mode` (str): 混合模式 ('linear', 'ease_in', 'ease_out', 'ease_in_out') + +#### get_actor_animations(actor_id) +获取Actor的所有动画名称 + +**参数:** +- `actor_id` (str): Actor标识符 + +**返回:** +- 动画名称列表 + +#### get_animation_info(actor_id, anim_name) +获取动画信息 + +**参数:** +- `actor_id` (str): Actor标识符 +- `anim_name` (str): 动画名称 + +**返回:** +- 动画信息字典 + +#### get_actor_info(actor_id) +获取Actor详细信息 + +**参数:** +- `actor_id` (str): Actor标识符 + +**返回:** +- Actor信息字典 + +### AnimationStateMachine 类 + +#### add_state(state_name, animations, loop=True, play_rate=1.0, blend_time=0.3) +添加动画状态 + +**参数:** +- `state_name` (str): 状态名称 +- `animations` (str or list): 动画名称或列表 +- `loop` (bool): 是否循环 +- `play_rate` (float): 播放速度 +- `blend_time` (float): 状态切换混合时间 + +#### set_transition(from_state, to_state, transition_type='smooth', condition=None, transition_time=0.3) +设置状态转换 + +**参数:** +- `from_state` (str): 起始状态 +- `to_state` (str): 目标状态 +- `transition_type` (str): 转换类型 ('immediate', 'smooth', 'blend', 'conditional') +- `condition` (function): 转换条件函数 +- `transition_time` (float): 转换时间 + +#### enter_state(state_name) +进入指定状态 + +**参数:** +- `state_name` (str): 状态名称 + +### AnimationNetworkSync 类 + +#### set_network_mode(mode) +设置网络模式 + +**参数:** +- `mode` (NetworkMode): 网络模式 (DISABLED, SERVER, CLIENT, PEER_TO_PEER) + +#### set_sync_mode(mode) +设置同步模式 + +**参数:** +- `mode` (SyncMode): 同步模式 (STATE_SYNC, INPUT_SYNC, INTERPOLATION) + +#### register_actor_for_sync(actor_id, network_id=None) +注册Actor以进行网络同步 + +**参数:** +- `actor_id` (str): Actor ID +- `network_id` (str): 网络ID,如果为None则自动生成 + +**返回:** +- 网络ID + +### AnimationCompressor 类 + +#### compress_animation(actor, anim_name, method='keyframe_reduction') +压缩指定动画 + +**参数:** +- `actor` (Actor): Actor对象 +- `anim_name` (str): 动画名称 +- `method` (str): 压缩方法 ('keyframe_reduction', 'quantization', 'delta_compression', 'rle') + +**返回:** +- 压缩后的动画数据 + +### AnimationAnalyzer 类 + +#### analyze_animation(actor_id, anim_name, analysis_type=AnimationAnalysisType.PERFORMANCE) +分析指定动画 + +**参数:** +- `actor_id` (str): Actor ID +- `anim_name` (str): 动画名称 +- `analysis_type` (AnimationAnalysisType): 分析类型 (PERFORMANCE, QUALITY, MEMORY, COMPRESSION) + +**返回:** +- 分析结果 + +### AnimationEditor 类 + +#### start_editing(actor_id, anim_name) +开始编辑动画 + +**参数:** +- `actor_id` (str): Actor ID +- `anim_name` (str): 动画名称 + +**返回:** +- 是否成功开始编辑 + +#### add_keyframe(actor_id, anim_name, bone_name, frame, position=None, rotation=None, scale=None) +添加关键帧 + +**参数:** +- `actor_id` (str): Actor ID +- `anim_name` (str): 动画名称 +- `bone_name` (str): 骨骼名称 +- `frame` (int): 帧号 +- `position` (tuple): 位置 +- `rotation` (tuple): 旋转 +- `scale` (tuple): 缩放 + +### DebugVisualizer 类 + +#### set_mode(mode) +设置可视化模式 + +**参数:** +- `mode` (DebugVisualizationMode): 模式 (SKELETON, BOUNDS, NORMALS, WIREFRAME, COLLISION) + +#### enable_visualization(enabled=True) +启用/禁用可视化 + +**参数:** +- `enabled` (bool): 是否启用 + +### AnimationProfiler 类 + +#### start_profiling() +开始性能分析 + +#### stop_profiling() +停止性能分析 + +#### get_performance_report() +获取性能报告 + +**返回:** +- 性能报告字典 + +## 支持的文件格式 + +- 模型格式: .egg, .bam, .fbx, .gltf, .glb等Panda3D支持的格式 +- 动画格式: .egg, .bam等包含动画数据的格式 + +## 配置选项 + +插件支持以下配置选项(config.json): + +```json +{ + "modules": { + "core": { + "enabled": true, + "max_actors": 1000, + "default_play_rate": 1.0, + "default_loop": false, + "blend_time": 0.3 + }, + "network": { + "enabled": true, + "sync_interval": 0.1, + "interpolation_mode": "linear", + "enable_compression": true + }, + "advanced": { + "state_machine": true, + "curve_editor": true, + "physics_integration": true + }, + "optimization": { + "compression": true, + "lod": true, + "cache": true, + "cache_size": 500 + }, + "tools": { + "analyzer": true, + "editor": true, + "visualizer": true, + "profiler": true + } + }, + "performance": { + "target_fps": 60, + "lod_threshold": 30, + "max_buffer_size": 100 + } +} +``` + +## 性能优化建议 + +1. **使用LOD系统** - 根据Actor距离摄像机的远近自动调整动画质量 +2. **启用动画压缩** - 减少内存占用,提高加载速度 +3. **合理使用缓存** - 避免重复加载相同动画数据 +4. **优化网络同步** - 根据网络状况调整同步频率 +5. **及时释放资源** - 不需要的Actor及时调用unload_actor释放 +6. **使用工具系统** - 通过分析器和性能分析器找出性能瓶颈 + +## 故障排除 + +常见问题及解决方案: + +1. **模型加载失败**: 检查文件路径和格式支持 +2. **动画无法播放**: 确认动画名称正确且与模型匹配 +3. **性能问题**: 减少同时播放的动画数量,及时卸载不需要的Actor +4. **网络同步问题**: 确认网络模式设置正确,检查防火墙设置 +5. **状态机问题**: 检查状态和转换定义是否正确 +6. **工具系统问题**: 确认工具模块已正确初始化 + +## 扩展开发 + +插件采用模块化设计,可以轻松扩展: + +1. 继承现有类添加新功能 +2. 实现自定义的压缩算法 +3. 添加新的动画控制方式 +4. 扩展网络同步协议 +5. 集成其他物理引擎 +6. 开发新的工具模块 + +## 示例代码 + +查看 `examples.py` 文件获取详细的使用示例。 + +每个模块都有完整的使用示例,可以直接运行测试。 + +## 技术支持 + +如遇到问题,请查看以下资源: + +1. 官方文档和API参考 +2. 示例代码和使用说明 +3. 社区论坛和技术支持 +4. GitHub Issues页面 + +## 版本历史 + +### v1.0.0 (当前版本) +- 初始版本发布 +- 实现完整的骨骼动画系统 +- 包含所有核心功能和高级特性 +- 提供完整的文档和示例 +- 添加工具系统支持 + +## 许可证 + +本插件遵循EG项目的许可证条款。 + +## 联系方式 + 如有任何问题或建议,请联系开发团队。 \ No newline at end of file diff --git a/plugins/user/advanced_skeletal_animation/__init__.py b/plugins/user/advanced_skeletal_animation/__init__.py index dec8a6e7..c4e06a3b 100644 --- a/plugins/user/advanced_skeletal_animation/__init__.py +++ b/plugins/user/advanced_skeletal_animation/__init__.py @@ -1,4 +1,4 @@ -""" -高级骨骼动画系统插件 -完整的骨骼动画解决方案,包含动画播放、混合、IK、压缩、网络同步等高级功能 +""" +高级骨骼动画系统插件 +完整的骨骼动画解决方案,包含动画播放、混合、IK、压缩、网络同步等高级功能 """ \ No newline at end of file diff --git a/plugins/user/advanced_skeletal_animation/advanced.py b/plugins/user/advanced_skeletal_animation/advanced.py index b27bfe9a..a72b4772 100644 --- a/plugins/user/advanced_skeletal_animation/advanced.py +++ b/plugins/user/advanced_skeletal_animation/advanced.py @@ -1,924 +1,924 @@ -""" -高级骨骼动画系统 - 高级功能模块 -提供动画状态机、曲线编辑器、物理集成等高级功能 -""" - -import math -import json -from typing import Dict, List, Optional, Callable, Any, Tuple, Union -from dataclasses import dataclass, field -from enum import Enum - -from panda3d.core import * -from direct.actor.Actor import Actor -from direct.interval.IntervalGlobal import * - -# 定义常量 -ADVANCED_ANIMATION_SETTINGS = { - 'state_machine_max_transitions': 100, - 'curve_editor_max_keyframes': 1000, - 'physics_integration_enabled': True, - 'physics_update_rate': 60 -} - -# 枚举定义 -class StateTransitionType(Enum): - """状态转换类型枚举""" - IMMEDIATE = 0 - SMOOTH = 1 - BLEND = 2 - CONDITIONAL = 3 - -class CurveInterpolation(Enum): - """曲线插值类型枚举""" - LINEAR = 0 - BEZIER = 1 - HERMITE = 2 - STEP = 3 - SMOOTH = 4 - -class PhysicsIntegrationMode(Enum): - """物理集成模式枚举""" - KINEMATIC = 0 - DYNAMIC = 1 - CONSTRAINT = 2 - -@dataclass -class StateMachineState: - """状态机状态数据类""" - name: str - animations: List[str] - loop: bool = True - play_rate: float = 1.0 - blend_time: float = 0.3 - enter_callbacks: List[Callable] = field(default_factory=list) - exit_callbacks: List[Callable] = field(default_factory=list) - update_callbacks: List[Callable] = field(default_factory=list) - transitions: Dict[str, 'StateTransition'] = field(default_factory=dict) - -@dataclass -class StateTransition: - """状态转换数据类""" - to_state: str - transition_type: StateTransitionType - condition: Optional[Callable] = None - transition_time: float = 0.3 - blend_mode: str = 'linear' - -@dataclass -class AnimationCurve: - """动画曲线数据类""" - name: str - keyframes: List[Tuple[float, float]] = field(default_factory=list) - interpolation: CurveInterpolation = CurveInterpolation.LINEAR - min_value: float = 0.0 - max_value: float = 1.0 - loop: bool = False - -@dataclass -class PhysicsConstraint: - """物理约束数据类""" - bone_name: str - constraint_type: str # 'point', 'hinge', 'slider', 'fixed' - target_node: Optional[str] = None - stiffness: float = 1.0 - damping: float = 0.1 - max_force: float = 1000.0 - -class AnimationStateMachine: - """ - 动画状态机 - 管理复杂的动画状态转换和混合 - """ - - def __init__(self, actor: Actor): - """ - 初始化动画状态机 - :param actor: Actor对象 - """ - self.actor = actor - self.states: Dict[str, StateMachineState] = {} - self.current_state: Optional[str] = None - self.previous_state: Optional[str] = None - self.transition_in_progress = False - self.transition_timer = 0.0 - self.transition_duration = 0.0 - self.state_timers: Dict[str, float] = {} - self.global_timer = 0.0 - - print(f"动画状态机为Actor {actor.getName() if hasattr(actor, 'getName') else 'Unknown'} 初始化完成") - - def add_state(self, state_name: str, animations: Union[str, List[str]], - loop: bool = True, play_rate: float = 1.0, blend_time: float = 0.3) -> None: - """ - 添加动画状态 - :param state_name: 状态名称 - :param animations: 动画列表或单个动画名称 - :param loop: 是否循环 - :param play_rate: 播放速度 - :param blend_time: 状态切换混合时间 - """ - if isinstance(animations, str): - animations = [animations] - - # 验证动画是否存在 - valid_animations = [anim for anim in animations if anim in self.actor.getAnimNames()] - if len(valid_animations) != len(animations): - invalid_anims = set(animations) - set(valid_animations) - print(f"警告: 以下动画不存在: {invalid_anims}") - - state = StateMachineState( - name=state_name, - animations=valid_animations, - loop=loop, - play_rate=play_rate, - blend_time=blend_time - ) - - self.states[state_name] = state - self.state_timers[state_name] = 0.0 - print(f"添加动画状态: {state_name} (动画: {valid_animations})") - - def remove_state(self, state_name: str) -> None: - """ - 移除动画状态 - :param state_name: 状态名称 - """ - if state_name in self.states: - # 如果是当前状态,先退出 - if self.current_state == state_name: - self.exit_state(state_name) - del self.states[state_name] - if state_name in self.state_timers: - del self.state_timers[state_name] - print(f"移除动画状态: {state_name}") - - def set_transition(self, from_state: str, to_state: str, - transition_type: StateTransitionType = StateTransitionType.SMOOTH, - condition: Optional[Callable] = None, - transition_time: float = 0.3, - blend_mode: str = 'linear') -> None: - """ - 设置状态转换 - :param from_state: 起始状态 - :param to_state: 目标状态 - :param transition_type: 转换类型 - :param condition: 转换条件函数,返回True时执行转换 - :param transition_time: 转换时间 - :param blend_mode: 混合模式 - """ - if from_state not in self.states: - print(f"错误: 起始状态 {from_state} 不存在") - return - - if to_state not in self.states: - print(f"错误: 目标状态 {to_state} 不存在") - return - - transition = StateTransition( - to_state=to_state, - transition_type=transition_type, - condition=condition, - transition_time=transition_time, - blend_mode=blend_mode - ) - - self.states[from_state].transitions[to_state] = transition - print(f"设置状态转换: {from_state} -> {to_state}") - - def add_state_callback(self, state_name: str, callback_type: str, callback: Callable) -> None: - """ - 添加状态回调 - :param state_name: 状态名称 - :param callback_type: 回调类型 ('enter', 'exit', 'update') - :param callback: 回调函数 - """ - if state_name not in self.states: - print(f"错误: 状态 {state_name} 不存在") - return - - state = self.states[state_name] - if callback_type == 'enter': - state.enter_callbacks.append(callback) - elif callback_type == 'exit': - state.exit_callbacks.append(callback) - elif callback_type == 'update': - state.update_callbacks.append(callback) - else: - print(f"错误: 无效的回调类型 {callback_type}") - - def enter_state(self, state_name: str) -> bool: - """ - 进入指定状态 - :param state_name: 状态名称 - :return: 是否成功进入 - """ - if state_name not in self.states: - print(f"错误: 状态 {state_name} 不存在") - return False - - # 退出当前状态 - if self.current_state: - self.exit_state(self.current_state) - - # 进入新状态 - state = self.states[state_name] - - # 播放状态动画 - for anim_name in state.animations: - if state.loop: - self.actor.loop(anim_name, restart=False) - else: - self.actor.play(anim_name) - self.actor.setPlayRate(state.play_rate, anim_name) - - # 调用进入回调 - for callback in state.enter_callbacks: - try: - callback(self.actor, state_name) - except Exception as e: - print(f"执行状态进入回调失败: {e}") - - self.previous_state = self.current_state - self.current_state = state_name - self.state_timers[state_name] = 0.0 - - print(f"进入动画状态: {state_name}") - return True - - def exit_state(self, state_name: str) -> None: - """ - 退出指定状态 - :param state_name: 状态名称 - """ - if state_name not in self.states or state_name != self.current_state: - return - - state = self.states[state_name] - - # 停止状态动画 - for anim_name in state.animations: - self.actor.stop(anim_name) - - # 调用退出回调 - for callback in state.exit_callbacks: - try: - callback(self.actor, state_name) - except Exception as e: - print(f"执行状态退出回调失败: {e}") - - print(f"退出动画状态: {state_name}") - self.current_state = None - - def update(self, dt: float) -> None: - """ - 更新状态机 - :param dt: 时间增量 - """ - self.global_timer += dt - - if not self.current_state: - return - - # 更新当前状态计时器 - self.state_timers[self.current_state] += dt - - # 调用更新回调 - state = self.states[self.current_state] - for callback in state.update_callbacks: - try: - callback(self.actor, self.current_state, dt, self.state_timers[self.current_state]) - except Exception as e: - print(f"执行状态更新回调失败: {e}") - - # 检查状态转换 - self._check_transitions() - - def _check_transitions(self) -> None: - """ - 检查状态转换条件 - """ - if not self.current_state or self.transition_in_progress: - return - - state = self.states[self.current_state] - - # 检查所有可能的转换 - for transition in state.transitions.values(): - # 检查转换条件 - if transition.condition is None or transition.condition(self.actor, self.current_state): - self._start_transition(transition) - break - - def _start_transition(self, transition: StateTransition) -> None: - """ - 开始状态转换 - :param transition: 转换信息 - """ - if self.transition_in_progress: - return - - self.transition_in_progress = True - self.transition_timer = 0.0 - self.transition_duration = transition.transition_time - - print(f"开始状态转换: {self.current_state} -> {transition.to_state}") - - # 根据转换类型处理 - if transition.transition_type == StateTransitionType.IMMEDIATE: - self._complete_transition(transition) - elif transition.transition_type == StateTransitionType.SMOOTH: - # 平滑转换,开始混合 - self._start_smooth_transition(transition) - elif transition.transition_type == StateTransitionType.BLEND: - # 动画混合转换 - self._start_blend_transition(transition) - elif transition.transition_type == StateTransitionType.CONDITIONAL: - # 条件转换(已经在条件检查中处理) - self._complete_transition(transition) - - def _start_smooth_transition(self, transition: StateTransition) -> None: - """ - 开始平滑转换 - :param transition: 转换信息 - """ - # 这里可以实现更复杂的平滑转换逻辑 - # 简化处理:直接完成转换 - self._complete_transition(transition) - - def _start_blend_transition(self, transition: StateTransition) -> None: - """ - 开始混合转换 - :param transition: 转换信息 - """ - # 这里可以实现动画混合逻辑 - # 简化处理:直接完成转换 - self._complete_transition(transition) - - def _update_transition(self, dt: float) -> None: - """ - 更新转换过程 - :param dt: 时间增量 - """ - if not self.transition_in_progress: - return - - self.transition_timer += dt - - # 检查转换是否完成 - if self.transition_timer >= self.transition_duration: - # 查找当前转换的目标状态 - if self.current_state and self.current_state in self.states: - state = self.states[self.current_state] - for transition in state.transitions.values(): - if transition.to_state: - self._complete_transition(transition) - break - - def _complete_transition(self, transition: StateTransition) -> None: - """ - 完成状态转换 - :param transition: 转换信息 - """ - self.transition_in_progress = False - self.transition_timer = 0.0 - self.transition_duration = 0.0 - - # 进入目标状态 - self.enter_state(transition.to_state) - - def get_current_state(self) -> Optional[str]: - """ - 获取当前状态 - :return: 当前状态名称 - """ - return self.current_state - - def get_available_states(self) -> List[str]: - """ - 获取所有可用状态 - :return: 状态名称列表 - """ - return list(self.states.keys()) - - def get_state_info(self, state_name: str) -> Optional[Dict[str, Any]]: - """ - 获取状态信息 - :param state_name: 状态名称 - :return: 状态信息字典 - """ - if state_name not in self.states: - return None - - state = self.states[state_name] - return { - 'name': state.name, - 'animations': state.animations.copy(), - 'loop': state.loop, - 'play_rate': state.play_rate, - 'blend_time': state.blend_time, - 'transitions': list(state.transitions.keys()), - 'time_in_state': self.state_timers.get(state_name, 0.0) - } - -class AnimationCurveEditor: - """ - 动画曲线编辑器 - 支持对动画属性进行曲线编辑和自定义 - """ - - def __init__(self, actor: Actor): - """ - 初始化动画曲线编辑器 - :param actor: Actor对象 - """ - self.actor = actor - self.curves: Dict[str, AnimationCurve] = {} - self.curve_targets: Dict[str, Dict[str, Any]] = {} - self.active_curves: Dict[str, float] = {} # {curve_name: current_time} - self.curve_values: Dict[str, float] = {} # {target_name: current_value} - - print(f"动画曲线编辑器为Actor {actor.getName() if hasattr(actor, 'getName') else 'Unknown'} 初始化完成") - - def create_curve(self, curve_name: str, keyframes: List[Tuple[float, float]], - interpolation: CurveInterpolation = CurveInterpolation.LINEAR, - min_value: float = 0.0, max_value: float = 1.0, - loop: bool = False) -> None: - """ - 创建动画曲线 - :param curve_name: 曲线名称 - :param keyframes: 关键帧列表 [(time, value), ...] - :param interpolation: 插值方式 - :param min_value: 最小值 - :param max_value: 最大值 - :param loop: 是否循环 - """ - if len(keyframes) == 0: - print("错误: 关键帧列表不能为空") - return - - # 验证关键帧 - sorted_keyframes = sorted(keyframes, key=lambda x: x[0]) - - curve = AnimationCurve( - name=curve_name, - keyframes=sorted_keyframes, - interpolation=interpolation, - min_value=min_value, - max_value=max_value, - loop=loop - ) - - self.curves[curve_name] = curve - self.active_curves[curve_name] = 0.0 - print(f"创建动画曲线: {curve_name} ({len(keyframes)} 个关键帧)") - - def remove_curve(self, curve_name: str) -> None: - """ - 移除动画曲线 - :param curve_name: 曲线名称 - """ - if curve_name in self.curves: - del self.curves[curve_name] - if curve_name in self.active_curves: - del self.active_curves[curve_name] - print(f"移除动画曲线: {curve_name}") - - def add_curve_target(self, target_name: str, curve_name: str, - property_path: str, transform_type: str = 'scale') -> None: - """ - 添加曲线目标 - :param target_name: 目标名称 - :param curve_name: 曲线名称 - :param property_path: 属性路径(如骨骼名称) - :param transform_type: 变换类型 ('scale', 'position', 'rotation') - """ - if curve_name not in self.curves: - print(f"错误: 曲线 {curve_name} 不存在") - return - - self.curve_targets[target_name] = { - 'curve_name': curve_name, - 'property_path': property_path, - 'transform_type': transform_type - } - - print(f"添加曲线目标: {target_name} -> {curve_name}") - - def remove_curve_target(self, target_name: str) -> None: - """ - 移除曲线目标 - :param target_name: 目标名称 - """ - if target_name in self.curve_targets: - del self.curve_targets[target_name] - print(f"移除曲线目标: {target_name}") - - def evaluate_curve(self, curve_name: str, time: float) -> float: - """ - 计算曲线在指定时间的值 - :param curve_name: 曲线名称 - :param time: 时间 - :return: 曲线值 - """ - if curve_name not in self.curves: - return 0.0 - - curve = self.curves[curve_name] - keyframes = curve.keyframes - - if len(keyframes) == 0: - return 0.0 - - # 处理循环 - if curve.loop and len(keyframes) > 1: - duration = keyframes[-1][0] - keyframes[0][0] - if duration > 0: - time = keyframes[0][0] + ((time - keyframes[0][0]) % duration) - - # 边界情况 - if time <= keyframes[0][0]: - return max(curve.min_value, min(curve.max_value, keyframes[0][1])) - if time >= keyframes[-1][0]: - return max(curve.min_value, min(curve.max_value, keyframes[-1][1])) - - # 查找相邻关键帧 - for i in range(len(keyframes) - 1): - if keyframes[i][0] <= time <= keyframes[i + 1][0]: - t1, v1 = keyframes[i] - t2, v2 = keyframes[i + 1] - - # 根据插值方式计算值 - if curve.interpolation == CurveInterpolation.STEP: - return max(curve.min_value, min(curve.max_value, v1)) - elif curve.interpolation == CurveInterpolation.BEZIER: - # 简化的贝塞尔插值 - t = (time - t1) / (t2 - t1) if t2 != t1 else 0 - # 贝塞尔曲线计算 (简化版) - return max(curve.min_value, min(curve.max_value, v1 + (v2 - v1) * (3 * t * t - 2 * t * t * t))) - elif curve.interpolation == CurveInterpolation.HERMITE: - # 埃尔米特插值 - t = (time - t1) / (t2 - t1) if t2 != t1 else 0 - t2_sq = t * t - t3_sq = t2_sq * t - h00 = 2 * t3_sq - 3 * t2_sq + 1 - h10 = t3_sq - 2 * t2_sq + t - h01 = -2 * t3_sq + 3 * t2_sq - h11 = t3_sq - t2_sq - value = h00 * v1 + h10 * (t2 - t1) + h01 * v2 + h11 * (t2 - t1) - return max(curve.min_value, min(curve.max_value, value)) - elif curve.interpolation == CurveInterpolation.SMOOTH: - # 平滑插值 - t = (time - t1) / (t2 - t1) if t2 != t1 else 0 - # 使用平滑步进函数 - t = t * t * (3 - 2 * t) - return max(curve.min_value, min(curve.max_value, v1 + (v2 - v1) * t)) - else: # LINEAR - t = (time - t1) / (t2 - t1) if t2 != t1 else 0 - return max(curve.min_value, min(curve.max_value, v1 + (v2 - v1) * t)) - - return 0.0 - - def update_curves(self, dt: float) -> None: - """ - 更新所有曲线 - :param dt: 时间增量 - """ - # 更新活动曲线时间 - for curve_name in self.active_curves.keys(): - self.active_curves[curve_name] += dt - - # 计算曲线值并应用到目标 - for target_name, target_data in self.curve_targets.items(): - curve_name = target_data['curve_name'] - property_path = target_data['property_path'] - transform_type = target_data['transform_type'] - - # 获取当前时间 - current_time = self.active_curves.get(curve_name, 0.0) - - # 计算曲线值 - value = self.evaluate_curve(curve_name, current_time) - - # 应用到目标 - self._apply_curve_value(property_path, transform_type, value) - - # 存储当前值 - self.curve_values[target_name] = value - - def _apply_curve_value(self, property_path: str, transform_type: str, value: float) -> None: - """ - 应用曲线值到目标属性 - :param property_path: 属性路径 - :param transform_type: 变换类型 - :param value: 值 - """ - # 查找目标骨骼 - joint = self.actor.exposeJoint(None, "modelRoot", property_path) - if not joint: - return - - # 根据变换类型应用值 - if transform_type == 'scale': - joint.setScale(value, value, value) - elif transform_type == 'position': - current_pos = joint.getPos() - # 简化处理,实际应该根据具体轴向应用 - joint.setPos(current_pos.x + value * 0.01, current_pos.y, current_pos.z) - elif transform_type == 'rotation': - # 简化处理,实际应该分解为具体的旋转轴 - joint.setHpr(joint.getHpr() + Vec3(value * 10, value * 5, value * 2)) - - def get_curve_value(self, target_name: str) -> float: - """ - 获取曲线目标的当前值 - :param target_name: 目标名称 - :return: 当前值 - """ - return self.curve_values.get(target_name, 0.0) - - def get_curve_info(self, curve_name: str) -> Optional[Dict[str, Any]]: - """ - 获取曲线信息 - :param curve_name: 曲线名称 - :return: 曲线信息字典 - """ - if curve_name not in self.curves: - return None - - curve = self.curves[curve_name] - return { - 'name': curve.name, - 'keyframes_count': len(curve.keyframes), - 'interpolation': curve.interpolation.name, - 'min_value': curve.min_value, - 'max_value': curve.max_value, - 'loop': curve.loop, - 'current_time': self.active_curves.get(curve_name, 0.0) - } - -class PhysicsAnimationIntegration: - """ - 物理动画集成系统 - 将物理模拟与骨骼动画结合 - """ - - def __init__(self, actor: Actor): - """ - 初始化物理动画集成系统 - :param actor: Actor对象 - """ - self.actor = actor - self.constraints: Dict[str, PhysicsConstraint] = {} - self.physics_enabled = True - self.integration_mode = PhysicsIntegrationMode.KINEMATIC - self.update_rate = 60 - self.update_timer = 0.0 - - print(f"物理动画集成系统为Actor {actor.getName() if hasattr(actor, 'getName') else 'Unknown'} 初始化完成") - - def add_constraint(self, constraint_name: str, bone_name: str, - constraint_type: str, target_node: Optional[str] = None, - stiffness: float = 1.0, damping: float = 0.1, - max_force: float = 1000.0) -> None: - """ - 添加物理约束 - :param constraint_name: 约束名称 - :param bone_name: 骨骼名称 - :param constraint_type: 约束类型 ('point', 'hinge', 'slider', 'fixed') - :param target_node: 目标节点名称 - :param stiffness: 刚度 - :param damping: 阻尼 - :param max_force: 最大力 - """ - # 验证骨骼是否存在 - joint = self.actor.exposeJoint(None, "modelRoot", bone_name) - if not joint: - print(f"警告: 骨骼 {bone_name} 不存在") - - constraint = PhysicsConstraint( - bone_name=bone_name, - constraint_type=constraint_type, - target_node=target_node, - stiffness=stiffness, - damping=damping, - max_force=max_force - ) - - self.constraints[constraint_name] = constraint - print(f"添加物理约束: {constraint_name} ({constraint_type}) 到骨骼 {bone_name}") - - def remove_constraint(self, constraint_name: str) -> None: - """ - 移除物理约束 - :param constraint_name: 约束名称 - """ - if constraint_name in self.constraints: - del self.constraints[constraint_name] - print(f"移除物理约束: {constraint_name}") - - def set_integration_mode(self, mode: PhysicsIntegrationMode) -> None: - """ - 设置集成模式 - :param mode: 集成模式 - """ - self.integration_mode = mode - print(f"物理集成模式设置为: {mode.name}") - - def enable_physics(self, enabled: bool = True) -> None: - """ - 启用/禁用物理集成 - :param enabled: 是否启用 - """ - self.physics_enabled = enabled - status = "启用" if enabled else "禁用" - print(f"物理集成已{status}") - - def update(self, dt: float) -> None: - """ - 更新物理集成 - :param dt: 时间增量 - """ - if not self.physics_enabled: - return - - self.update_timer += dt - - # 根据更新率限制更新频率 - if self.update_timer < 1.0 / self.update_rate: - return - - self.update_timer = 0.0 - - # 更新所有约束 - for constraint_name, constraint in self.constraints.items(): - self._update_constraint(constraint) - - def _update_constraint(self, constraint: PhysicsConstraint) -> None: - """ - 更新物理约束 - :param constraint: 约束信息 - """ - # 获取骨骼关节 - joint = self.actor.exposeJoint(None, "modelRoot", constraint.bone_name) - if not joint: - return - - # 根据约束类型应用物理效果 - if constraint.constraint_type == 'point': - self._apply_point_constraint(joint, constraint) - elif constraint.constraint_type == 'hinge': - self._apply_hinge_constraint(joint, constraint) - elif constraint.constraint_type == 'slider': - self._apply_slider_constraint(joint, constraint) - elif constraint.constraint_type == 'fixed': - self._apply_fixed_constraint(joint, constraint) - - def _apply_point_constraint(self, joint, constraint: PhysicsConstraint) -> None: - """ - 应用点约束 - :param joint: 关节节点 - :param constraint: 约束信息 - """ - # 点约束实现 - 简化版本 - # 在实际实现中,这里会与物理引擎交互 - pass - - def _apply_hinge_constraint(self, joint, constraint: PhysicsConstraint) -> None: - """ - 应用铰链约束 - :param joint: 关节节点 - :param constraint: 约束信息 - """ - # 铰链约束实现 - 简化版本 - pass - - def _apply_slider_constraint(self, joint, constraint: PhysicsConstraint) -> None: - """ - 应用滑动约束 - :param joint: 关节节点 - :param constraint: 约束信息 - """ - # 滑动约束实现 - 简化版本 - pass - - def _apply_fixed_constraint(self, joint, constraint: PhysicsConstraint) -> None: - """ - 应用固定约束 - :param joint: 关节节点 - :param constraint: 约束信息 - """ - # 固定约束实现 - 简化版本 - pass - - def get_constraint_info(self, constraint_name: str) -> Optional[Dict[str, Any]]: - """ - 获取约束信息 - :param constraint_name: 约束名称 - :return: 约束信息字典 - """ - if constraint_name not in self.constraints: - return None - - constraint = self.constraints[constraint_name] - return { - 'name': constraint_name, - 'bone_name': constraint.bone_name, - 'constraint_type': constraint.constraint_type, - 'target_node': constraint.target_node, - 'stiffness': constraint.stiffness, - 'damping': constraint.damping, - 'max_force': constraint.max_force - } - -# 使用示例和测试代码 -def example_state_machine_usage(actor: Actor): - """ - 状态机使用示例 - """ - print("=== 动画状态机使用示例 ===") - - # 创建状态机 - state_machine = AnimationStateMachine(actor) - - # 添加状态 - state_machine.add_state("idle", "idle_anim", loop=True) - state_machine.add_state("walk", "walk_anim", loop=True) - state_machine.add_state("run", "run_anim", loop=True) - state_machine.add_state("jump", ["jump_start", "jump_loop", "jump_end"], loop=False) - - # 设置状态转换 - state_machine.set_transition("idle", "walk", StateTransitionType.SMOOTH) - state_machine.set_transition("walk", "run", StateTransitionType.SMOOTH) - state_machine.set_transition("run", "idle", StateTransitionType.SMOOTH) - - # 添加回调 - def on_enter_walk(actor, state_name): - print(f"进入行走状态: {state_name}") - - state_machine.add_state_callback("walk", "enter", on_enter_walk) - - # 进入初始状态 - state_machine.enter_state("idle") - - print("状态机示例设置完成") - return state_machine - -def example_curve_editor_usage(actor: Actor): - """ - 曲线编辑器使用示例 - """ - print("=== 动画曲线编辑器使用示例 ===") - - # 创建曲线编辑器 - curve_editor = AnimationCurveEditor(actor) - - # 创建曲线 - scale_curve_frames = [(0, 1.0), (1, 1.5), (2, 1.0), (3, 0.8), (4, 1.0)] - curve_editor.create_curve("bounce_scale", scale_curve_frames, CurveInterpolation.SMOOTH) - - position_curve_frames = [(0, 0), (1, 0.5), (2, 0), (3, -0.3), (4, 0)] - curve_editor.create_curve("bounce_position", position_curve_frames, CurveInterpolation.SMOOTH) - - # 添加曲线目标 - curve_editor.add_curve_target("character_scale", "bounce_scale", "character", "scale") - curve_editor.add_curve_target("character_height", "bounce_position", "character", "position") - - print("曲线编辑器示例设置完成") - return curve_editor - -def example_physics_integration_usage(actor: Actor): - """ - 物理集成使用示例 - """ - print("=== 物理动画集成使用示例 ===") - - # 创建物理集成系统 - physics_integration = PhysicsAnimationIntegration(actor) - - # 添加约束 - physics_integration.add_constraint( - "arm_constraint", - "right_arm", - "hinge", - stiffness=0.8, - damping=0.2 - ) - - physics_integration.add_constraint( - "leg_constraint", - "left_leg", - "point", - stiffness=0.9, - damping=0.1 - ) - - # 启用物理集成 - physics_integration.enable_physics(True) - physics_integration.set_integration_mode(PhysicsIntegrationMode.DYNAMIC) - - print("物理集成示例设置完成") - return physics_integration - -if __name__ == "__main__": +""" +高级骨骼动画系统 - 高级功能模块 +提供动画状态机、曲线编辑器、物理集成等高级功能 +""" + +import math +import json +from typing import Dict, List, Optional, Callable, Any, Tuple, Union +from dataclasses import dataclass, field +from enum import Enum + +from panda3d.core import * +from direct.actor.Actor import Actor +from direct.interval.IntervalGlobal import * + +# 定义常量 +ADVANCED_ANIMATION_SETTINGS = { + 'state_machine_max_transitions': 100, + 'curve_editor_max_keyframes': 1000, + 'physics_integration_enabled': True, + 'physics_update_rate': 60 +} + +# 枚举定义 +class StateTransitionType(Enum): + """状态转换类型枚举""" + IMMEDIATE = 0 + SMOOTH = 1 + BLEND = 2 + CONDITIONAL = 3 + +class CurveInterpolation(Enum): + """曲线插值类型枚举""" + LINEAR = 0 + BEZIER = 1 + HERMITE = 2 + STEP = 3 + SMOOTH = 4 + +class PhysicsIntegrationMode(Enum): + """物理集成模式枚举""" + KINEMATIC = 0 + DYNAMIC = 1 + CONSTRAINT = 2 + +@dataclass +class StateMachineState: + """状态机状态数据类""" + name: str + animations: List[str] + loop: bool = True + play_rate: float = 1.0 + blend_time: float = 0.3 + enter_callbacks: List[Callable] = field(default_factory=list) + exit_callbacks: List[Callable] = field(default_factory=list) + update_callbacks: List[Callable] = field(default_factory=list) + transitions: Dict[str, 'StateTransition'] = field(default_factory=dict) + +@dataclass +class StateTransition: + """状态转换数据类""" + to_state: str + transition_type: StateTransitionType + condition: Optional[Callable] = None + transition_time: float = 0.3 + blend_mode: str = 'linear' + +@dataclass +class AnimationCurve: + """动画曲线数据类""" + name: str + keyframes: List[Tuple[float, float]] = field(default_factory=list) + interpolation: CurveInterpolation = CurveInterpolation.LINEAR + min_value: float = 0.0 + max_value: float = 1.0 + loop: bool = False + +@dataclass +class PhysicsConstraint: + """物理约束数据类""" + bone_name: str + constraint_type: str # 'point', 'hinge', 'slider', 'fixed' + target_node: Optional[str] = None + stiffness: float = 1.0 + damping: float = 0.1 + max_force: float = 1000.0 + +class AnimationStateMachine: + """ + 动画状态机 + 管理复杂的动画状态转换和混合 + """ + + def __init__(self, actor: Actor): + """ + 初始化动画状态机 + :param actor: Actor对象 + """ + self.actor = actor + self.states: Dict[str, StateMachineState] = {} + self.current_state: Optional[str] = None + self.previous_state: Optional[str] = None + self.transition_in_progress = False + self.transition_timer = 0.0 + self.transition_duration = 0.0 + self.state_timers: Dict[str, float] = {} + self.global_timer = 0.0 + + print(f"动画状态机为Actor {actor.getName() if hasattr(actor, 'getName') else 'Unknown'} 初始化完成") + + def add_state(self, state_name: str, animations: Union[str, List[str]], + loop: bool = True, play_rate: float = 1.0, blend_time: float = 0.3) -> None: + """ + 添加动画状态 + :param state_name: 状态名称 + :param animations: 动画列表或单个动画名称 + :param loop: 是否循环 + :param play_rate: 播放速度 + :param blend_time: 状态切换混合时间 + """ + if isinstance(animations, str): + animations = [animations] + + # 验证动画是否存在 + valid_animations = [anim for anim in animations if anim in self.actor.getAnimNames()] + if len(valid_animations) != len(animations): + invalid_anims = set(animations) - set(valid_animations) + print(f"警告: 以下动画不存在: {invalid_anims}") + + state = StateMachineState( + name=state_name, + animations=valid_animations, + loop=loop, + play_rate=play_rate, + blend_time=blend_time + ) + + self.states[state_name] = state + self.state_timers[state_name] = 0.0 + print(f"添加动画状态: {state_name} (动画: {valid_animations})") + + def remove_state(self, state_name: str) -> None: + """ + 移除动画状态 + :param state_name: 状态名称 + """ + if state_name in self.states: + # 如果是当前状态,先退出 + if self.current_state == state_name: + self.exit_state(state_name) + del self.states[state_name] + if state_name in self.state_timers: + del self.state_timers[state_name] + print(f"移除动画状态: {state_name}") + + def set_transition(self, from_state: str, to_state: str, + transition_type: StateTransitionType = StateTransitionType.SMOOTH, + condition: Optional[Callable] = None, + transition_time: float = 0.3, + blend_mode: str = 'linear') -> None: + """ + 设置状态转换 + :param from_state: 起始状态 + :param to_state: 目标状态 + :param transition_type: 转换类型 + :param condition: 转换条件函数,返回True时执行转换 + :param transition_time: 转换时间 + :param blend_mode: 混合模式 + """ + if from_state not in self.states: + print(f"错误: 起始状态 {from_state} 不存在") + return + + if to_state not in self.states: + print(f"错误: 目标状态 {to_state} 不存在") + return + + transition = StateTransition( + to_state=to_state, + transition_type=transition_type, + condition=condition, + transition_time=transition_time, + blend_mode=blend_mode + ) + + self.states[from_state].transitions[to_state] = transition + print(f"设置状态转换: {from_state} -> {to_state}") + + def add_state_callback(self, state_name: str, callback_type: str, callback: Callable) -> None: + """ + 添加状态回调 + :param state_name: 状态名称 + :param callback_type: 回调类型 ('enter', 'exit', 'update') + :param callback: 回调函数 + """ + if state_name not in self.states: + print(f"错误: 状态 {state_name} 不存在") + return + + state = self.states[state_name] + if callback_type == 'enter': + state.enter_callbacks.append(callback) + elif callback_type == 'exit': + state.exit_callbacks.append(callback) + elif callback_type == 'update': + state.update_callbacks.append(callback) + else: + print(f"错误: 无效的回调类型 {callback_type}") + + def enter_state(self, state_name: str) -> bool: + """ + 进入指定状态 + :param state_name: 状态名称 + :return: 是否成功进入 + """ + if state_name not in self.states: + print(f"错误: 状态 {state_name} 不存在") + return False + + # 退出当前状态 + if self.current_state: + self.exit_state(self.current_state) + + # 进入新状态 + state = self.states[state_name] + + # 播放状态动画 + for anim_name in state.animations: + if state.loop: + self.actor.loop(anim_name, restart=False) + else: + self.actor.play(anim_name) + self.actor.setPlayRate(state.play_rate, anim_name) + + # 调用进入回调 + for callback in state.enter_callbacks: + try: + callback(self.actor, state_name) + except Exception as e: + print(f"执行状态进入回调失败: {e}") + + self.previous_state = self.current_state + self.current_state = state_name + self.state_timers[state_name] = 0.0 + + print(f"进入动画状态: {state_name}") + return True + + def exit_state(self, state_name: str) -> None: + """ + 退出指定状态 + :param state_name: 状态名称 + """ + if state_name not in self.states or state_name != self.current_state: + return + + state = self.states[state_name] + + # 停止状态动画 + for anim_name in state.animations: + self.actor.stop(anim_name) + + # 调用退出回调 + for callback in state.exit_callbacks: + try: + callback(self.actor, state_name) + except Exception as e: + print(f"执行状态退出回调失败: {e}") + + print(f"退出动画状态: {state_name}") + self.current_state = None + + def update(self, dt: float) -> None: + """ + 更新状态机 + :param dt: 时间增量 + """ + self.global_timer += dt + + if not self.current_state: + return + + # 更新当前状态计时器 + self.state_timers[self.current_state] += dt + + # 调用更新回调 + state = self.states[self.current_state] + for callback in state.update_callbacks: + try: + callback(self.actor, self.current_state, dt, self.state_timers[self.current_state]) + except Exception as e: + print(f"执行状态更新回调失败: {e}") + + # 检查状态转换 + self._check_transitions() + + def _check_transitions(self) -> None: + """ + 检查状态转换条件 + """ + if not self.current_state or self.transition_in_progress: + return + + state = self.states[self.current_state] + + # 检查所有可能的转换 + for transition in state.transitions.values(): + # 检查转换条件 + if transition.condition is None or transition.condition(self.actor, self.current_state): + self._start_transition(transition) + break + + def _start_transition(self, transition: StateTransition) -> None: + """ + 开始状态转换 + :param transition: 转换信息 + """ + if self.transition_in_progress: + return + + self.transition_in_progress = True + self.transition_timer = 0.0 + self.transition_duration = transition.transition_time + + print(f"开始状态转换: {self.current_state} -> {transition.to_state}") + + # 根据转换类型处理 + if transition.transition_type == StateTransitionType.IMMEDIATE: + self._complete_transition(transition) + elif transition.transition_type == StateTransitionType.SMOOTH: + # 平滑转换,开始混合 + self._start_smooth_transition(transition) + elif transition.transition_type == StateTransitionType.BLEND: + # 动画混合转换 + self._start_blend_transition(transition) + elif transition.transition_type == StateTransitionType.CONDITIONAL: + # 条件转换(已经在条件检查中处理) + self._complete_transition(transition) + + def _start_smooth_transition(self, transition: StateTransition) -> None: + """ + 开始平滑转换 + :param transition: 转换信息 + """ + # 这里可以实现更复杂的平滑转换逻辑 + # 简化处理:直接完成转换 + self._complete_transition(transition) + + def _start_blend_transition(self, transition: StateTransition) -> None: + """ + 开始混合转换 + :param transition: 转换信息 + """ + # 这里可以实现动画混合逻辑 + # 简化处理:直接完成转换 + self._complete_transition(transition) + + def _update_transition(self, dt: float) -> None: + """ + 更新转换过程 + :param dt: 时间增量 + """ + if not self.transition_in_progress: + return + + self.transition_timer += dt + + # 检查转换是否完成 + if self.transition_timer >= self.transition_duration: + # 查找当前转换的目标状态 + if self.current_state and self.current_state in self.states: + state = self.states[self.current_state] + for transition in state.transitions.values(): + if transition.to_state: + self._complete_transition(transition) + break + + def _complete_transition(self, transition: StateTransition) -> None: + """ + 完成状态转换 + :param transition: 转换信息 + """ + self.transition_in_progress = False + self.transition_timer = 0.0 + self.transition_duration = 0.0 + + # 进入目标状态 + self.enter_state(transition.to_state) + + def get_current_state(self) -> Optional[str]: + """ + 获取当前状态 + :return: 当前状态名称 + """ + return self.current_state + + def get_available_states(self) -> List[str]: + """ + 获取所有可用状态 + :return: 状态名称列表 + """ + return list(self.states.keys()) + + def get_state_info(self, state_name: str) -> Optional[Dict[str, Any]]: + """ + 获取状态信息 + :param state_name: 状态名称 + :return: 状态信息字典 + """ + if state_name not in self.states: + return None + + state = self.states[state_name] + return { + 'name': state.name, + 'animations': state.animations.copy(), + 'loop': state.loop, + 'play_rate': state.play_rate, + 'blend_time': state.blend_time, + 'transitions': list(state.transitions.keys()), + 'time_in_state': self.state_timers.get(state_name, 0.0) + } + +class AnimationCurveEditor: + """ + 动画曲线编辑器 + 支持对动画属性进行曲线编辑和自定义 + """ + + def __init__(self, actor: Actor): + """ + 初始化动画曲线编辑器 + :param actor: Actor对象 + """ + self.actor = actor + self.curves: Dict[str, AnimationCurve] = {} + self.curve_targets: Dict[str, Dict[str, Any]] = {} + self.active_curves: Dict[str, float] = {} # {curve_name: current_time} + self.curve_values: Dict[str, float] = {} # {target_name: current_value} + + print(f"动画曲线编辑器为Actor {actor.getName() if hasattr(actor, 'getName') else 'Unknown'} 初始化完成") + + def create_curve(self, curve_name: str, keyframes: List[Tuple[float, float]], + interpolation: CurveInterpolation = CurveInterpolation.LINEAR, + min_value: float = 0.0, max_value: float = 1.0, + loop: bool = False) -> None: + """ + 创建动画曲线 + :param curve_name: 曲线名称 + :param keyframes: 关键帧列表 [(time, value), ...] + :param interpolation: 插值方式 + :param min_value: 最小值 + :param max_value: 最大值 + :param loop: 是否循环 + """ + if len(keyframes) == 0: + print("错误: 关键帧列表不能为空") + return + + # 验证关键帧 + sorted_keyframes = sorted(keyframes, key=lambda x: x[0]) + + curve = AnimationCurve( + name=curve_name, + keyframes=sorted_keyframes, + interpolation=interpolation, + min_value=min_value, + max_value=max_value, + loop=loop + ) + + self.curves[curve_name] = curve + self.active_curves[curve_name] = 0.0 + print(f"创建动画曲线: {curve_name} ({len(keyframes)} 个关键帧)") + + def remove_curve(self, curve_name: str) -> None: + """ + 移除动画曲线 + :param curve_name: 曲线名称 + """ + if curve_name in self.curves: + del self.curves[curve_name] + if curve_name in self.active_curves: + del self.active_curves[curve_name] + print(f"移除动画曲线: {curve_name}") + + def add_curve_target(self, target_name: str, curve_name: str, + property_path: str, transform_type: str = 'scale') -> None: + """ + 添加曲线目标 + :param target_name: 目标名称 + :param curve_name: 曲线名称 + :param property_path: 属性路径(如骨骼名称) + :param transform_type: 变换类型 ('scale', 'position', 'rotation') + """ + if curve_name not in self.curves: + print(f"错误: 曲线 {curve_name} 不存在") + return + + self.curve_targets[target_name] = { + 'curve_name': curve_name, + 'property_path': property_path, + 'transform_type': transform_type + } + + print(f"添加曲线目标: {target_name} -> {curve_name}") + + def remove_curve_target(self, target_name: str) -> None: + """ + 移除曲线目标 + :param target_name: 目标名称 + """ + if target_name in self.curve_targets: + del self.curve_targets[target_name] + print(f"移除曲线目标: {target_name}") + + def evaluate_curve(self, curve_name: str, time: float) -> float: + """ + 计算曲线在指定时间的值 + :param curve_name: 曲线名称 + :param time: 时间 + :return: 曲线值 + """ + if curve_name not in self.curves: + return 0.0 + + curve = self.curves[curve_name] + keyframes = curve.keyframes + + if len(keyframes) == 0: + return 0.0 + + # 处理循环 + if curve.loop and len(keyframes) > 1: + duration = keyframes[-1][0] - keyframes[0][0] + if duration > 0: + time = keyframes[0][0] + ((time - keyframes[0][0]) % duration) + + # 边界情况 + if time <= keyframes[0][0]: + return max(curve.min_value, min(curve.max_value, keyframes[0][1])) + if time >= keyframes[-1][0]: + return max(curve.min_value, min(curve.max_value, keyframes[-1][1])) + + # 查找相邻关键帧 + for i in range(len(keyframes) - 1): + if keyframes[i][0] <= time <= keyframes[i + 1][0]: + t1, v1 = keyframes[i] + t2, v2 = keyframes[i + 1] + + # 根据插值方式计算值 + if curve.interpolation == CurveInterpolation.STEP: + return max(curve.min_value, min(curve.max_value, v1)) + elif curve.interpolation == CurveInterpolation.BEZIER: + # 简化的贝塞尔插值 + t = (time - t1) / (t2 - t1) if t2 != t1 else 0 + # 贝塞尔曲线计算 (简化版) + return max(curve.min_value, min(curve.max_value, v1 + (v2 - v1) * (3 * t * t - 2 * t * t * t))) + elif curve.interpolation == CurveInterpolation.HERMITE: + # 埃尔米特插值 + t = (time - t1) / (t2 - t1) if t2 != t1 else 0 + t2_sq = t * t + t3_sq = t2_sq * t + h00 = 2 * t3_sq - 3 * t2_sq + 1 + h10 = t3_sq - 2 * t2_sq + t + h01 = -2 * t3_sq + 3 * t2_sq + h11 = t3_sq - t2_sq + value = h00 * v1 + h10 * (t2 - t1) + h01 * v2 + h11 * (t2 - t1) + return max(curve.min_value, min(curve.max_value, value)) + elif curve.interpolation == CurveInterpolation.SMOOTH: + # 平滑插值 + t = (time - t1) / (t2 - t1) if t2 != t1 else 0 + # 使用平滑步进函数 + t = t * t * (3 - 2 * t) + return max(curve.min_value, min(curve.max_value, v1 + (v2 - v1) * t)) + else: # LINEAR + t = (time - t1) / (t2 - t1) if t2 != t1 else 0 + return max(curve.min_value, min(curve.max_value, v1 + (v2 - v1) * t)) + + return 0.0 + + def update_curves(self, dt: float) -> None: + """ + 更新所有曲线 + :param dt: 时间增量 + """ + # 更新活动曲线时间 + for curve_name in self.active_curves.keys(): + self.active_curves[curve_name] += dt + + # 计算曲线值并应用到目标 + for target_name, target_data in self.curve_targets.items(): + curve_name = target_data['curve_name'] + property_path = target_data['property_path'] + transform_type = target_data['transform_type'] + + # 获取当前时间 + current_time = self.active_curves.get(curve_name, 0.0) + + # 计算曲线值 + value = self.evaluate_curve(curve_name, current_time) + + # 应用到目标 + self._apply_curve_value(property_path, transform_type, value) + + # 存储当前值 + self.curve_values[target_name] = value + + def _apply_curve_value(self, property_path: str, transform_type: str, value: float) -> None: + """ + 应用曲线值到目标属性 + :param property_path: 属性路径 + :param transform_type: 变换类型 + :param value: 值 + """ + # 查找目标骨骼 + joint = self.actor.exposeJoint(None, "modelRoot", property_path) + if not joint: + return + + # 根据变换类型应用值 + if transform_type == 'scale': + joint.setScale(value, value, value) + elif transform_type == 'position': + current_pos = joint.getPos() + # 简化处理,实际应该根据具体轴向应用 + joint.setPos(current_pos.x + value * 0.01, current_pos.y, current_pos.z) + elif transform_type == 'rotation': + # 简化处理,实际应该分解为具体的旋转轴 + joint.setHpr(joint.getHpr() + Vec3(value * 10, value * 5, value * 2)) + + def get_curve_value(self, target_name: str) -> float: + """ + 获取曲线目标的当前值 + :param target_name: 目标名称 + :return: 当前值 + """ + return self.curve_values.get(target_name, 0.0) + + def get_curve_info(self, curve_name: str) -> Optional[Dict[str, Any]]: + """ + 获取曲线信息 + :param curve_name: 曲线名称 + :return: 曲线信息字典 + """ + if curve_name not in self.curves: + return None + + curve = self.curves[curve_name] + return { + 'name': curve.name, + 'keyframes_count': len(curve.keyframes), + 'interpolation': curve.interpolation.name, + 'min_value': curve.min_value, + 'max_value': curve.max_value, + 'loop': curve.loop, + 'current_time': self.active_curves.get(curve_name, 0.0) + } + +class PhysicsAnimationIntegration: + """ + 物理动画集成系统 + 将物理模拟与骨骼动画结合 + """ + + def __init__(self, actor: Actor): + """ + 初始化物理动画集成系统 + :param actor: Actor对象 + """ + self.actor = actor + self.constraints: Dict[str, PhysicsConstraint] = {} + self.physics_enabled = True + self.integration_mode = PhysicsIntegrationMode.KINEMATIC + self.update_rate = 60 + self.update_timer = 0.0 + + print(f"物理动画集成系统为Actor {actor.getName() if hasattr(actor, 'getName') else 'Unknown'} 初始化完成") + + def add_constraint(self, constraint_name: str, bone_name: str, + constraint_type: str, target_node: Optional[str] = None, + stiffness: float = 1.0, damping: float = 0.1, + max_force: float = 1000.0) -> None: + """ + 添加物理约束 + :param constraint_name: 约束名称 + :param bone_name: 骨骼名称 + :param constraint_type: 约束类型 ('point', 'hinge', 'slider', 'fixed') + :param target_node: 目标节点名称 + :param stiffness: 刚度 + :param damping: 阻尼 + :param max_force: 最大力 + """ + # 验证骨骼是否存在 + joint = self.actor.exposeJoint(None, "modelRoot", bone_name) + if not joint: + print(f"警告: 骨骼 {bone_name} 不存在") + + constraint = PhysicsConstraint( + bone_name=bone_name, + constraint_type=constraint_type, + target_node=target_node, + stiffness=stiffness, + damping=damping, + max_force=max_force + ) + + self.constraints[constraint_name] = constraint + print(f"添加物理约束: {constraint_name} ({constraint_type}) 到骨骼 {bone_name}") + + def remove_constraint(self, constraint_name: str) -> None: + """ + 移除物理约束 + :param constraint_name: 约束名称 + """ + if constraint_name in self.constraints: + del self.constraints[constraint_name] + print(f"移除物理约束: {constraint_name}") + + def set_integration_mode(self, mode: PhysicsIntegrationMode) -> None: + """ + 设置集成模式 + :param mode: 集成模式 + """ + self.integration_mode = mode + print(f"物理集成模式设置为: {mode.name}") + + def enable_physics(self, enabled: bool = True) -> None: + """ + 启用/禁用物理集成 + :param enabled: 是否启用 + """ + self.physics_enabled = enabled + status = "启用" if enabled else "禁用" + print(f"物理集成已{status}") + + def update(self, dt: float) -> None: + """ + 更新物理集成 + :param dt: 时间增量 + """ + if not self.physics_enabled: + return + + self.update_timer += dt + + # 根据更新率限制更新频率 + if self.update_timer < 1.0 / self.update_rate: + return + + self.update_timer = 0.0 + + # 更新所有约束 + for constraint_name, constraint in self.constraints.items(): + self._update_constraint(constraint) + + def _update_constraint(self, constraint: PhysicsConstraint) -> None: + """ + 更新物理约束 + :param constraint: 约束信息 + """ + # 获取骨骼关节 + joint = self.actor.exposeJoint(None, "modelRoot", constraint.bone_name) + if not joint: + return + + # 根据约束类型应用物理效果 + if constraint.constraint_type == 'point': + self._apply_point_constraint(joint, constraint) + elif constraint.constraint_type == 'hinge': + self._apply_hinge_constraint(joint, constraint) + elif constraint.constraint_type == 'slider': + self._apply_slider_constraint(joint, constraint) + elif constraint.constraint_type == 'fixed': + self._apply_fixed_constraint(joint, constraint) + + def _apply_point_constraint(self, joint, constraint: PhysicsConstraint) -> None: + """ + 应用点约束 + :param joint: 关节节点 + :param constraint: 约束信息 + """ + # 点约束实现 - 简化版本 + # 在实际实现中,这里会与物理引擎交互 + pass + + def _apply_hinge_constraint(self, joint, constraint: PhysicsConstraint) -> None: + """ + 应用铰链约束 + :param joint: 关节节点 + :param constraint: 约束信息 + """ + # 铰链约束实现 - 简化版本 + pass + + def _apply_slider_constraint(self, joint, constraint: PhysicsConstraint) -> None: + """ + 应用滑动约束 + :param joint: 关节节点 + :param constraint: 约束信息 + """ + # 滑动约束实现 - 简化版本 + pass + + def _apply_fixed_constraint(self, joint, constraint: PhysicsConstraint) -> None: + """ + 应用固定约束 + :param joint: 关节节点 + :param constraint: 约束信息 + """ + # 固定约束实现 - 简化版本 + pass + + def get_constraint_info(self, constraint_name: str) -> Optional[Dict[str, Any]]: + """ + 获取约束信息 + :param constraint_name: 约束名称 + :return: 约束信息字典 + """ + if constraint_name not in self.constraints: + return None + + constraint = self.constraints[constraint_name] + return { + 'name': constraint_name, + 'bone_name': constraint.bone_name, + 'constraint_type': constraint.constraint_type, + 'target_node': constraint.target_node, + 'stiffness': constraint.stiffness, + 'damping': constraint.damping, + 'max_force': constraint.max_force + } + +# 使用示例和测试代码 +def example_state_machine_usage(actor: Actor): + """ + 状态机使用示例 + """ + print("=== 动画状态机使用示例 ===") + + # 创建状态机 + state_machine = AnimationStateMachine(actor) + + # 添加状态 + state_machine.add_state("idle", "idle_anim", loop=True) + state_machine.add_state("walk", "walk_anim", loop=True) + state_machine.add_state("run", "run_anim", loop=True) + state_machine.add_state("jump", ["jump_start", "jump_loop", "jump_end"], loop=False) + + # 设置状态转换 + state_machine.set_transition("idle", "walk", StateTransitionType.SMOOTH) + state_machine.set_transition("walk", "run", StateTransitionType.SMOOTH) + state_machine.set_transition("run", "idle", StateTransitionType.SMOOTH) + + # 添加回调 + def on_enter_walk(actor, state_name): + print(f"进入行走状态: {state_name}") + + state_machine.add_state_callback("walk", "enter", on_enter_walk) + + # 进入初始状态 + state_machine.enter_state("idle") + + print("状态机示例设置完成") + return state_machine + +def example_curve_editor_usage(actor: Actor): + """ + 曲线编辑器使用示例 + """ + print("=== 动画曲线编辑器使用示例 ===") + + # 创建曲线编辑器 + curve_editor = AnimationCurveEditor(actor) + + # 创建曲线 + scale_curve_frames = [(0, 1.0), (1, 1.5), (2, 1.0), (3, 0.8), (4, 1.0)] + curve_editor.create_curve("bounce_scale", scale_curve_frames, CurveInterpolation.SMOOTH) + + position_curve_frames = [(0, 0), (1, 0.5), (2, 0), (3, -0.3), (4, 0)] + curve_editor.create_curve("bounce_position", position_curve_frames, CurveInterpolation.SMOOTH) + + # 添加曲线目标 + curve_editor.add_curve_target("character_scale", "bounce_scale", "character", "scale") + curve_editor.add_curve_target("character_height", "bounce_position", "character", "position") + + print("曲线编辑器示例设置完成") + return curve_editor + +def example_physics_integration_usage(actor: Actor): + """ + 物理集成使用示例 + """ + print("=== 物理动画集成使用示例 ===") + + # 创建物理集成系统 + physics_integration = PhysicsAnimationIntegration(actor) + + # 添加约束 + physics_integration.add_constraint( + "arm_constraint", + "right_arm", + "hinge", + stiffness=0.8, + damping=0.2 + ) + + physics_integration.add_constraint( + "leg_constraint", + "left_leg", + "point", + stiffness=0.9, + damping=0.1 + ) + + # 启用物理集成 + physics_integration.enable_physics(True) + physics_integration.set_integration_mode(PhysicsIntegrationMode.DYNAMIC) + + print("物理集成示例设置完成") + return physics_integration + +if __name__ == "__main__": print("高级动画功能模块加载完成") \ No newline at end of file diff --git a/plugins/user/advanced_skeletal_animation/config.json b/plugins/user/advanced_skeletal_animation/config.json index c107220f..d38e2e6e 100644 --- a/plugins/user/advanced_skeletal_animation/config.json +++ b/plugins/user/advanced_skeletal_animation/config.json @@ -1,53 +1,53 @@ -{ - "name": "advanced_skeletal_animation", - "version": "1.0.0", - "author": "Advanced Animation Team", - "description": "高级骨骼动画系统,支持模型动画加载、播放控制、混合、IK、压缩、网络同步等高级功能", - "dependencies": [ - "core" - ], - "enabled": true, - "autoload": false, - "modules": { - "core": { - "enabled": true, - "max_actors": 1000, - "default_play_rate": 1.0, - "default_loop": false, - "blend_time": 0.3 - }, - "network": { - "enabled": true, - "sync_interval": 0.1, - "interpolation_mode": "linear", - "enable_compression": true - }, - "advanced": { - "state_machine": true, - "curve_editor": true, - "physics_integration": true - }, - "optimization": { - "compression": true, - "lod": true, - "cache": true, - "cache_size": 500 - } - }, - "supported_formats": [ - ".egg", - ".bam", - ".fbx", - ".gltf", - ".glb" - ], - "animation_extensions": [ - ".egg", - ".bam" - ], - "performance": { - "target_fps": 60, - "lod_threshold": 30, - "max_buffer_size": 100 - } +{ + "name": "advanced_skeletal_animation", + "version": "1.0.0", + "author": "Advanced Animation Team", + "description": "高级骨骼动画系统,支持模型动画加载、播放控制、混合、IK、压缩、网络同步等高级功能", + "dependencies": [ + "core" + ], + "enabled": true, + "autoload": false, + "modules": { + "core": { + "enabled": true, + "max_actors": 1000, + "default_play_rate": 1.0, + "default_loop": false, + "blend_time": 0.3 + }, + "network": { + "enabled": true, + "sync_interval": 0.1, + "interpolation_mode": "linear", + "enable_compression": true + }, + "advanced": { + "state_machine": true, + "curve_editor": true, + "physics_integration": true + }, + "optimization": { + "compression": true, + "lod": true, + "cache": true, + "cache_size": 500 + } + }, + "supported_formats": [ + ".egg", + ".bam", + ".fbx", + ".gltf", + ".glb" + ], + "animation_extensions": [ + ".egg", + ".bam" + ], + "performance": { + "target_fps": 60, + "lod_threshold": 30, + "max_buffer_size": 100 + } } \ No newline at end of file diff --git a/plugins/user/advanced_skeletal_animation/core.py b/plugins/user/advanced_skeletal_animation/core.py index b0085a68..4c178e3d 100644 --- a/plugins/user/advanced_skeletal_animation/core.py +++ b/plugins/user/advanced_skeletal_animation/core.py @@ -1,1207 +1,1207 @@ -""" -高级骨骼动画系统 - 核心模块 -提供完整的骨骼动画支持,包括加载、播放、控制等核心功能 -""" - -import math -import json -import os -from collections import defaultdict, deque -from typing import Dict, List, Tuple, Optional, Union, Any, Callable -from dataclasses import dataclass, field -from enum import Enum - -from panda3d.core import * -from direct.actor.Actor import Actor -from direct.interval.IntervalGlobal import * -from direct.showbase.DirectObject import DirectObject -from direct.distributed.ClockDelta import globalClockDelta - -# 导入工具模块 -from .tools import AnimationAnalyzer, AnimationEditor, DebugVisualizer, AnimationProfiler - -# 定义常量 -DEFAULT_ANIMATION_SETTINGS = { - 'default_play_rate': 1.0, - 'default_loop': False, - 'blend_time': 0.3, - 'max_actors': 1000, - 'cache_size': 100, - 'lod_distance_thresholds': [10.0, 25.0, 50.0], - 'lod_frame_rates': [1.0, 0.5, 0.25, 0.1] -} - -# 枚举定义 -class AnimationState(Enum): - """动画状态枚举""" - STOPPED = 0 - PLAYING = 1 - PAUSED = 2 - BLENDING = 3 - LOOPING = 4 - -class BlendMode(Enum): - """混合模式枚举""" - LINEAR = 0 - EASE_IN = 1 - EASE_OUT = 2 - EASE_IN_OUT = 3 - OVERWRITE = 4 - -class LODLevel(Enum): - """LOD级别枚举""" - HIGH = 0 - MEDIUM = 1 - LOW = 2 - VERY_LOW = 3 - -@dataclass -class AnimationInfo: - """动画信息数据类""" - name: str - frame_rate: float - num_frames: int - play_rate: float - current_frame: float - state: AnimationState - loop: bool - start_time: float - duration: float - blend_weight: float = 1.0 - blend_in_time: float = 0.0 - blend_out_time: float = 0.0 - -@dataclass -class ActorInfo: - """Actor信息数据类""" - actor_id: str - actor: Actor - model_path: str - anim_paths: Dict[str, str] - position: Tuple[float, float, float] - scale: Union[float, Tuple[float, float, float]] - animations: Dict[str, AnimationInfo] = field(default_factory=dict) - active_animations: List[str] = field(default_factory=list) - lod_level: LODLevel = LODLevel.HIGH - last_update_time: float = 0.0 - is_networked: bool = False - network_id: str = "" - -@dataclass -class BlendInfo: - """混合信息数据类""" - from_anim: str - to_anim: str - blend_time: float - blend_mode: BlendMode - current_time: float = 0.0 - total_time: float = 0.0 - active: bool = False - -@dataclass -class IKChain: - """IK链数据类""" - chain_name: str - end_effector: str - pole_vector: Optional[Tuple[float, float, float]] - iterations: int - enabled: bool = True - target_position: Optional[Tuple[float, float, float]] = None - -class AnimationCache: - """动画缓存系统""" - - def __init__(self, max_size: int = 100): - self.max_size = max_size - self.cache: Dict[str, Any] = {} - self.access_order: deque = deque() - self.hits = 0 - self.misses = 0 - - def get(self, key: str) -> Optional[Any]: - """获取缓存项""" - if key in self.cache: - self.hits += 1 - # 更新访问顺序 - self.access_order.remove(key) - self.access_order.append(key) - return self.cache[key] - else: - self.misses += 1 - return None - - def put(self, key: str, value: Any) -> None: - """放入缓存项""" - # 如果缓存已满,移除最久未使用的项 - if len(self.cache) >= self.max_size: - oldest_key = self.access_order.popleft() - del self.cache[oldest_key] - - self.cache[key] = value - self.access_order.append(key) - - def invalidate(self, key: str) -> None: - """使缓存项失效""" - if key in self.cache: - del self.cache[key] - self.access_order.remove(key) - - def clear(self) -> None: - """清空缓存""" - self.cache.clear() - self.access_order.clear() - self.hits = 0 - self.misses = 0 - - def get_stats(self) -> Dict[str, Any]: - """获取缓存统计信息""" - total_requests = self.hits + self.misses - hit_rate = self.hits / total_requests if total_requests > 0 else 0 - return { - 'size': len(self.cache), - 'max_size': self.max_size, - 'hits': self.hits, - 'misses': self.misses, - 'hit_rate': hit_rate - } - -class AnimationCompressor: - """动画压缩系统""" - - def __init__(self): - self.position_threshold = 0.001 - self.rotation_threshold = 0.01 - self.scale_threshold = 0.001 - - def compress_keyframes(self, keyframes: List[Tuple[float, Tuple[float, float, float]]], - data_type: str = 'position') -> List[Tuple[float, Tuple[float, float, float]]]: - """ - 压缩关键帧数据 - :param keyframes: 关键帧列表 [(time, (x, y, z)), ...] - :param data_type: 数据类型 ('position', 'rotation', 'scale') - :return: 压缩后的关键帧列表 - """ - if len(keyframes) <= 2: - return keyframes - - compressed = [keyframes[0]] # 总是保留第一帧 - last_keyframe = keyframes[0] - - # 根据数据类型选择阈值 - threshold = getattr(self, f'{data_type}_threshold', 0.001) - - for i in range(1, len(keyframes) - 1): - current_keyframe = keyframes[i] - next_keyframe = keyframes[i + 1] - - # 检查当前帧是否可以被简化 - if not self._is_keyframe_necessary(last_keyframe, current_keyframe, next_keyframe, threshold, data_type): - continue - - compressed.append(current_keyframe) - last_keyframe = current_keyframe - - compressed.append(keyframes[-1]) # 总是保留最后一帧 - return compressed - - def _is_keyframe_necessary(self, prev_frame: Tuple[float, Tuple[float, float, float]], - current_frame: Tuple[float, Tuple[float, float, float]], - next_frame: Tuple[float, Tuple[float, float, float]], - threshold: float, data_type: str) -> bool: - """ - 判断关键帧是否必要 - :param prev_frame: 前一帧 - :param current_frame: 当前帧 - :param next_frame: 后一帧 - :param threshold: 阈值 - :param data_type: 数据类型 - :return: 是否必要 - """ - prev_time, prev_data = prev_frame - curr_time, curr_data = current_frame - next_time, next_data = next_frame - - # 计算线性插值 - t = (curr_time - prev_time) / (next_time - prev_time) - interpolated = tuple(prev_data[i] + t * (next_data[i] - prev_data[i]) for i in range(3)) - - # 计算与实际值的差异 - diff = math.sqrt(sum((curr_data[i] - interpolated[i]) ** 2 for i in range(3))) - - return diff > threshold - -class SkeletalAnimationCore(DirectObject): - """ - 骨骼动画核心系统 - 提供完整的骨骼动画管理功能 - """ - - def __init__(self, world): - """ - 初始化骨骼动画核心系统 - :param world: 世界对象 - """ - super().__init__() - self.world = world - self.actors: Dict[str, ActorInfo] = {} - self.actor_configs: Dict[str, Dict[str, Any]] = {} - self.blend_infos: Dict[str, Dict[str, BlendInfo]] = defaultdict(dict) - self.ik_chains: Dict[str, Dict[str, IKChain]] = defaultdict(dict) - self.animation_cache = AnimationCache(DEFAULT_ANIMATION_SETTINGS['cache_size']) - self.compressor = AnimationCompressor() - self.event_handlers: Dict[str, List[Callable]] = defaultdict(list) - self.is_updating = False - self.update_task = None - - # 工具系统 - self.analyzer = AnimationAnalyzer(self) - self.editor = AnimationEditor(self) - self.visualizer = DebugVisualizer(world) - self.profiler = AnimationProfiler(self) - - # 性能统计 - self.stats = { - 'actors_loaded': 0, - 'animations_played': 0, - 'blends_performed': 0, - 'ik_calculations': 0, - 'cache_hits': 0, - 'cache_misses': 0 - } - - print("骨骼动画核心系统初始化完成") - - def load_actor(self, model_path: str, anim_paths: Optional[Dict[str, str]] = None, - actor_id: Optional[str] = None, position: Tuple[float, float, float] = (0, 0, 0), - scale: Union[float, Tuple[float, float, float]] = 1.0) -> Tuple[Optional[Actor], str]: - """ - 加载Actor模型 - :param model_path: 模型文件路径 - :param anim_paths: 动画文件路径字典 {anim_name: anim_path} - :param actor_id: Actor标识符,如果为None则自动生成 - :param position: 位置坐标 (x, y, z) - :param scale: 缩放比例 - :return: (Actor对象, actor_id) - """ - try: - # 检查模型文件是否存在 - if not os.path.exists(model_path): - raise FileNotFoundError(f"模型文件不存在: {model_path}") - - # 生成Actor ID - if actor_id is None: - actor_id = f"actor_{len(self.actors)}" - - # 检查Actor是否已存在 - if actor_id in self.actors: - print(f"警告: Actor {actor_id} 已存在,将被替换") - self.unload_actor(actor_id) - - # 检查缓存 - cache_key = f"actor_{model_path}_{hash(str(anim_paths))}" - cached_actor = self.animation_cache.get(cache_key) - - if cached_actor: - actor = cached_actor - print(f"从缓存加载Actor: {actor_id}") - else: - # 创建Actor - if anim_paths: - # 验证动画文件 - valid_anims = {} - for anim_name, anim_path in anim_paths.items(): - if os.path.exists(anim_path): - valid_anims[anim_name] = anim_path - else: - print(f"警告: 动画文件不存在: {anim_path}") - actor = Actor(model_path, valid_anims) - else: - actor = Actor(model_path) - - # 缓存Actor - self.animation_cache.put(cache_key, actor) - - # 设置位置和缩放 - actor.setPos(*position) - if isinstance(scale, (int, float)): - actor.setScale(scale) - else: - actor.setScale(*scale) - - # 将Actor添加到场景中 - actor.reparentTo(self.world.render) - actor.setName(actor_id) - - # 创建Actor信息 - actor_info = ActorInfo( - actor_id=actor_id, - actor=actor, - model_path=model_path, - anim_paths=anim_paths or {}, - position=position, - scale=scale - ) - - # 初始化动画信息 - for anim_name in actor.getAnimNames(): - control = actor.getAnimControl(anim_name) - if control: - anim_info = AnimationInfo( - name=anim_name, - frame_rate=actor.getFrameRate(anim_name), - num_frames=actor.getNumFrames(anim_name), - play_rate=DEFAULT_ANIMATION_SETTINGS['default_play_rate'], - current_frame=0.0, - state=AnimationState.STOPPED, - loop=DEFAULT_ANIMATION_SETTINGS['default_loop'], - start_time=0.0, - duration=control.getDuration() - ) - actor_info.animations[anim_name] = anim_info - - # 存储Actor信息 - self.actors[actor_id] = actor_info - self.actor_configs[actor_id] = { - 'model_path': model_path, - 'anim_paths': anim_paths, - 'position': position, - 'scale': scale - } - - self.stats['actors_loaded'] += 1 - print(f"成功加载Actor: {actor_id}") - print(f" 模型: {model_path}") - print(f" 位置: {position}") - print(f" 缩放: {scale}") - print(f" 可用动画: {list(actor_info.animations.keys())}") - - return actor, actor_id - - except Exception as e: - print(f"加载Actor失败: {e}") - import traceback - traceback.print_exc() - return None, "" - - def unload_actor(self, actor_id: str) -> bool: - """ - 卸载Actor - :param actor_id: Actor标识符 - :return: 是否成功卸载 - """ - if actor_id not in self.actors: - print(f"错误: Actor {actor_id} 不存在") - return False - - try: - actor_info = self.actors[actor_id] - actor = actor_info.actor - - # 停止所有动画 - self.stop_all_animations(actor_id) - - # 清理混合信息 - if actor_id in self.blend_infos: - del self.blend_infos[actor_id] - - # 清理IK链 - if actor_id in self.ik_chains: - del self.ik_chains[actor_id] - - # 从场景中移除 - actor.cleanup() - - # 从管理器中移除 - del self.actors[actor_id] - if actor_id in self.actor_configs: - del self.actor_configs[actor_id] - - print(f"已卸载Actor: {actor_id}") - return True - - except Exception as e: - print(f"卸载Actor失败: {e}") - import traceback - traceback.print_exc() - return False - - def play_animation(self, actor_id: str, anim_name: str, loop: bool = False, - play_rate: float = 1.0, start_frame: Optional[int] = None, - end_frame: Optional[int] = None) -> bool: - """ - 播放动画 - :param actor_id: Actor标识符 - :param anim_name: 动画名称 - :param loop: 是否循环播放 - :param play_rate: 播放速度 - :param start_frame: 起始帧 - :param end_frame: 结束帧 - :return: 是否成功播放 - """ - if actor_id not in self.actors: - print(f"错误: Actor {actor_id} 不存在") - return False - - actor_info = self.actors[actor_id] - actor = actor_info.actor - - # 检查动画是否存在 - if anim_name not in actor.getAnimNames(): - print(f"错误: 动画 {anim_name} 不存在于Actor {actor_id} 中") - print(f" 可用动画: {actor.getAnimNames()}") - return False - - try: - # 设置播放速度 - actor.setPlayRate(play_rate, anim_name) - - # 播放动画 - if loop: - if start_frame is not None and end_frame is not None: - actor.loop(anim_name, restart=False, fromFrame=start_frame, toFrame=end_frame) - else: - actor.loop(anim_name, restart=False) - else: - if start_frame is not None and end_frame is not None: - actor.play(anim_name, fromFrame=start_frame, toFrame=end_frame) - else: - actor.play(anim_name) - - # 更新动画信息 - if anim_name in actor_info.animations: - anim_info = actor_info.animations[anim_name] - anim_info.state = AnimationState.LOOPING if loop else AnimationState.PLAYING - anim_info.loop = loop - anim_info.play_rate = play_rate - anim_info.start_time = globalClock.getRealTime() - - # 添加到活动动画列表 - if anim_name not in actor_info.active_animations: - actor_info.active_animations.append(anim_name) - - self.stats['animations_played'] += 1 - print(f"播放动画: {anim_name} (Actor: {actor_id}, 循环: {loop}, 速度: {play_rate}x)") - return True - - except Exception as e: - print(f"播放动画失败: {e}") - import traceback - traceback.print_exc() - return False - - def stop_animation(self, actor_id: str, anim_name: Optional[str] = None) -> bool: - """ - 停止动画 - :param actor_id: Actor标识符 - :param anim_name: 动画名称,如果为None则停止所有动画 - :return: 是否成功停止 - """ - if actor_id not in self.actors: - print(f"错误: Actor {actor_id} 不存在") - return False - - actor_info = self.actors[actor_id] - actor = actor_info.actor - - try: - if anim_name: - # 停止指定动画 - if anim_name in actor.getAnimNames(): - actor.stop(anim_name) - # 从活动动画列表中移除 - if anim_name in actor_info.active_animations: - actor_info.active_animations.remove(anim_name) - # 更新动画状态 - if anim_name in actor_info.animations: - actor_info.animations[anim_name].state = AnimationState.STOPPED - print(f"停止动画: {anim_name} (Actor: {actor_id})") - else: - print(f"警告: 动画 {anim_name} 不存在于Actor {actor_id} 中") - else: - # 停止所有动画 - actor.stop() - # 清空活动动画列表 - actor_info.active_animations.clear() - # 更新所有动画状态 - for anim_info in actor_info.animations.values(): - anim_info.state = AnimationState.STOPPED - print(f"停止所有动画 (Actor: {actor_id})") - - return True - - except Exception as e: - print(f"停止动画失败: {e}") - import traceback - traceback.print_exc() - return False - - def stop_all_animations(self, actor_id: str) -> bool: - """ - 停止指定Actor的所有动画 - :param actor_id: Actor标识符 - :return: 是否成功停止 - """ - return self.stop_animation(actor_id) - - def pause_animation(self, actor_id: str, anim_name: Optional[str] = None) -> bool: - """ - 暂停动画 - :param actor_id: Actor标识符 - :param anim_name: 动画名称,如果为None则暂停所有动画 - :return: 是否成功暂停 - """ - if actor_id not in self.actors: - print(f"错误: Actor {actor_id} 不存在") - return False - - actor_info = self.actors[actor_id] - actor = actor_info.actor - - try: - if anim_name: - if anim_name in actor.getAnimNames(): - control = actor.getAnimControl(anim_name) - if control and control.isPlaying(): - # 通过设置播放速度为0来模拟暂停 - current_rate = actor.getPlayRate(anim_name) - actor.setPlayRate(0, anim_name) - # 更新动画状态 - if anim_name in actor_info.animations: - actor_info.animations[anim_name].state = AnimationState.PAUSED - print(f"暂停动画: {anim_name} (Actor: {actor_id})") - return True - else: - print(f"警告: 动画 {anim_name} 不存在于Actor {actor_id} 中") - else: - # 暂停所有活动动画 - success = False - for active_anim in actor_info.active_animations: - control = actor.getAnimControl(active_anim) - if control and control.isPlaying(): - current_rate = actor.getPlayRate(active_anim) - actor.setPlayRate(0, active_anim) - if active_anim in actor_info.animations: - actor_info.animations[active_anim].state = AnimationState.PAUSED - success = True - if success: - print(f"暂停所有动画 (Actor: {actor_id})") - return success - - except Exception as e: - print(f"暂停动画失败: {e}") - import traceback - traceback.print_exc() - return False - - def resume_animation(self, actor_id: str, anim_name: Optional[str] = None) -> bool: - """ - 恢复动画播放 - :param actor_id: Actor标识符 - :param anim_name: 动画名称,如果为None则恢复所有动画 - :return: 是否成功恢复 - """ - if actor_id not in self.actors: - print(f"错误: Actor {actor_id} 不存在") - return False - - actor_info = self.actors[actor_id] - actor = actor_info.actor - - try: - if anim_name: - if anim_name in actor.getAnimNames(): - control = actor.getAnimControl(anim_name) - if control and control.getPlayRate() == 0: - # 恢复正常播放速度 - actor.setPlayRate(1.0, anim_name) - # 更新动画状态 - if anim_name in actor_info.animations: - anim_state = AnimationState.LOOPING if actor_info.animations[anim_name].loop else AnimationState.PLAYING - actor_info.animations[anim_name].state = anim_state - print(f"恢复动画: {anim_name} (Actor: {actor_id})") - return True - else: - print(f"警告: 动画 {anim_name} 不存在于Actor {actor_id} 中") - else: - # 恢复所有暂停的动画 - success = False - for active_anim in actor_info.active_animations: - control = actor.getAnimControl(active_anim) - if control and control.getPlayRate() == 0: - actor.setPlayRate(1.0, active_anim) - if active_anim in actor_info.animations: - anim_state = AnimationState.LOOPING if actor_info.animations[active_anim].loop else AnimationState.PLAYING - actor_info.animations[active_anim].state = anim_state - success = True - if success: - print(f"恢复所有动画 (Actor: {actor_id})") - return success - - except Exception as e: - print(f"恢复动画失败: {e}") - import traceback - traceback.print_exc() - return False - - def set_animation_speed(self, actor_id: str, anim_name: str, speed: float) -> bool: - """ - 设置动画播放速度 - :param actor_id: Actor标识符 - :param anim_name: 动画名称 - :param speed: 播放速度 - :return: 是否成功设置 - """ - if actor_id not in self.actors: - print(f"错误: Actor {actor_id} 不存在") - return False - - actor_info = self.actors[actor_id] - actor = actor_info.actor - - try: - if anim_name in actor.getAnimNames(): - actor.setPlayRate(speed, anim_name) - # 更新动画信息 - if anim_name in actor_info.animations: - actor_info.animations[anim_name].play_rate = speed - print(f"设置动画速度: {anim_name} 为 {speed}x (Actor: {actor_id})") - return True - else: - print(f"警告: 动画 {anim_name} 不存在于Actor {actor_id} 中") - return False - - except Exception as e: - print(f"设置动画速度失败: {e}") - import traceback - traceback.print_exc() - return False - - def blend_animations(self, actor_id: str, from_anim: str, to_anim: str, - blend_time: float = 0.3, blend_mode: BlendMode = BlendMode.LINEAR) -> bool: - """ - 混合两个动画 - :param actor_id: Actor标识符 - :param from_anim: 起始动画名称 - :param to_anim: 目标动画名称 - :param blend_time: 混合时间 - :param blend_mode: 混合模式 - :return: 是否成功开始混合 - """ - if actor_id not in self.actors: - print(f"错误: Actor {actor_id} 不存在") - return False - - actor_info = self.actors[actor_id] - actor = actor_info.actor - - # 检查动画是否存在 - if from_anim not in actor.getAnimNames(): - print(f"错误: 起始动画 {from_anim} 不存在于Actor {actor_id} 中") - return False - - if to_anim not in actor.getAnimNames(): - print(f"错误: 目标动画 {to_anim} 不存在于Actor {actor_id} 中") - return False - - try: - # 创建混合信息 - blend_info = BlendInfo( - from_anim=from_anim, - to_anim=to_anim, - blend_time=blend_time, - blend_mode=blend_mode, - current_time=0.0, - total_time=blend_time, - active=True - ) - - # 存储混合信息 - blend_id = f"{from_anim}_to_{to_anim}" - self.blend_infos[actor_id][blend_id] = blend_info - - # 开始混合过程 - self._start_animation_blend(actor_info, blend_info) - - self.stats['blends_performed'] += 1 - print(f"开始动画混合: {from_anim} -> {to_anim} (Actor: {actor_id}, 时间: {blend_time}s)") - return True - - except Exception as e: - print(f"动画混合失败: {e}") - import traceback - traceback.print_exc() - return False - - def _start_animation_blend(self, actor_info: ActorInfo, blend_info: BlendInfo) -> None: - """ - 开始动画混合过程 - :param actor_info: Actor信息 - :param blend_info: 混合信息 - """ - actor = actor_info.actor - from_anim = blend_info.from_anim - to_anim = blend_info.to_anim - - # 停止起始动画 - actor.stop(from_anim) - - # 播放目标动画 - actor.play(to_anim) - - # 设置混合权重(简化实现) - if to_anim in actor_info.animations: - actor_info.animations[to_anim].blend_weight = 1.0 - - def get_actor(self, actor_id: str) -> Optional[Actor]: - """ - 获取Actor对象 - :param actor_id: Actor标识符 - :return: Actor对象或None - """ - if actor_id in self.actors: - return self.actors[actor_id].actor - return None - - def get_actor_animations(self, actor_id: str) -> List[str]: - """ - 获取Actor的所有动画名称 - :param actor_id: Actor标识符 - :return: 动画名称列表 - """ - if actor_id in self.actors: - return list(self.actors[actor_id].animations.keys()) - return [] - - def get_animation_info(self, actor_id: str, anim_name: str) -> Optional[Dict[str, Any]]: - """ - 获取动画信息 - :param actor_id: Actor标识符 - :param anim_name: 动画名称 - :return: 动画信息字典 - """ - if actor_id not in self.actors: - print(f"错误: Actor {actor_id} 不存在") - return None - - actor_info = self.actors[actor_id] - - if anim_name not in actor_info.animations: - print(f"错误: 动画 {anim_name} 不存在于Actor {actor_id} 中") - return None - - anim_info = actor_info.animations[anim_name] - actor = actor_info.actor - control = actor.getAnimControl(anim_name) if anim_name in actor.getAnimNames() else None - - return { - 'name': anim_info.name, - 'frame_rate': anim_info.frame_rate, - 'num_frames': anim_info.num_frames, - 'play_rate': anim_info.play_rate, - 'current_frame': anim_info.current_frame, - 'state': anim_info.state.name, - 'loop': anim_info.loop, - 'start_time': anim_info.start_time, - 'duration': anim_info.duration, - 'is_playing': control.isPlaying() if control else False, - 'is_finished': control.isFinished() if control else True - } - - def get_actor_info(self, actor_id: str) -> Optional[Dict[str, Any]]: - """ - 获取Actor详细信息 - :param actor_id: Actor标识符 - :return: Actor信息字典 - """ - if actor_id not in self.actors: - print(f"错误: Actor {actor_id} 不存在") - return None - - actor_info = self.actors[actor_id] - actor = actor_info.actor - - return { - 'id': actor_info.actor_id, - 'model_path': actor_info.model_path, - 'anim_paths': actor_info.anim_paths, - 'position': actor.getPos().toTuple(), - 'scale': actor.getScale().toTuple(), - 'available_animations': list(actor_info.animations.keys()), - 'active_animations': actor_info.active_animations.copy(), - 'lod_level': actor_info.lod_level.name, - 'has_character': actor.hasCharacter(), - 'num_parts': actor.getNumParts(), - 'bounds': { - 'min': actor.getTightBounds()[0].toTuple() if actor.getTightBounds() else None, - 'max': actor.getTightBounds()[1].toTuple() if actor.getTightBounds() else None - } - } - - def set_actor_position(self, actor_id: str, x: float, y: float, z: float) -> bool: - """ - 设置Actor位置 - :param actor_id: Actor标识符 - :param x: X坐标 - :param y: Y坐标 - :param z: Z坐标 - :return: 是否成功设置 - """ - if actor_id in self.actors: - self.actors[actor_id].actor.setPos(x, y, z) - self.actors[actor_id].position = (x, y, z) - if actor_id in self.actor_configs: - self.actor_configs[actor_id]['position'] = (x, y, z) - print(f"设置Actor {actor_id} 位置: ({x}, {y}, {z})") - return True - return False - - def set_actor_scale(self, actor_id: str, scale: Union[float, Tuple[float, float, float]]) -> bool: - """ - 设置Actor缩放 - :param actor_id: Actor标识符 - :param scale: 缩放值 (数值或(x, y, z)元组) - :return: 是否成功设置 - """ - if actor_id in self.actors: - actor = self.actors[actor_id].actor - if isinstance(scale, (int, float)): - actor.setScale(scale) - else: - actor.setScale(*scale) - self.actors[actor_id].scale = scale - if actor_id in self.actor_configs: - self.actor_configs[actor_id]['scale'] = scale - print(f"设置Actor {actor_id} 缩放: {scale}") - return True - return False - - def update(self, dt: float) -> None: - """ - 更新动画系统 - :param dt: 时间增量 - """ - if not self.is_updating: - return - - current_time = globalClock.getRealTime() - - # 更新所有Actor - for actor_id, actor_info in self.actors.items(): - # 更新动画信息 - self._update_actor_animations(actor_info, dt, current_time) - - # 更新混合 - self._update_blends(actor_info, dt) - - # 更新IK - self._update_ik(actor_info) - - # 更新LOD - self._update_lod(actor_info, current_time) - - # 更新工具系统 - self._update_tools(dt) - - def _update_actor_animations(self, actor_info: ActorInfo, dt: float, current_time: float) -> None: - """ - 更新Actor动画 - :param actor_info: Actor信息 - :param dt: 时间增量 - :param current_time: 当前时间 - """ - actor = actor_info.actor - - # 更新活动动画的当前帧信息 - for anim_name in actor_info.active_animations: - if anim_name in actor_info.animations: - anim_info = actor_info.animations[anim_name] - control = actor.getAnimControl(anim_name) if anim_name in actor.getAnimNames() else None - - if control and control.isPlaying(): - anim_info.current_frame = actor.getCurrentFrame(anim_name) - anim_info.state = AnimationState.LOOPING if anim_info.loop else AnimationState.PLAYING - elif control and control.getPlayRate() == 0: - anim_info.state = AnimationState.PAUSED - else: - anim_info.state = AnimationState.STOPPED - - actor_info.last_update_time = current_time - - def _update_blends(self, actor_info: ActorInfo, dt: float) -> None: - """ - 更新动画混合 - :param actor_info: Actor信息 - :param dt: 时间增量 - """ - actor_id = actor_info.actor_id - - if actor_id not in self.blend_infos: - return - - # 更新所有混合 - finished_blends = [] - for blend_id, blend_info in self.blend_infos[actor_id].items(): - if not blend_info.active: - continue - - blend_info.current_time += dt - - # 计算混合权重 - t = min(1.0, blend_info.current_time / blend_info.total_time) - - # 根据混合模式应用缓动函数 - if blend_info.blend_mode == BlendMode.EASE_IN: - t = t * t - elif blend_info.blend_mode == BlendMode.EASE_OUT: - t = 1.0 - (1.0 - t) * (1.0 - t) - elif blend_info.blend_mode == BlendMode.EASE_IN_OUT: - t = t * t * (3.0 - 2.0 * t) - - # 更新目标动画的混合权重 - if blend_info.to_anim in actor_info.animations: - actor_info.animations[blend_info.to_anim].blend_weight = t - - # 检查混合是否完成 - if blend_info.current_time >= blend_info.total_time: - blend_info.active = False - finished_blends.append(blend_id) - - # 移除已完成的混合 - for blend_id in finished_blends: - del self.blend_infos[actor_id][blend_id] - - def _update_ik(self, actor_info: ActorInfo) -> None: - """ - 更新IK计算 - :param actor_info: Actor信息 - """ - actor_id = actor_info.actor_id - - if actor_id not in self.ik_chains: - return - - # 更新所有IK链 - for chain_name, ik_chain in self.ik_chains[actor_id].items(): - if not ik_chain.enabled or not ik_chain.target_position: - continue - - self._solve_ik_chain(actor_info, ik_chain) - self.stats['ik_calculations'] += 1 - - def _solve_ik_chain(self, actor_info: ActorInfo, ik_chain: IKChain) -> None: - """ - 解算IK链 - :param actor_info: Actor信息 - :param ik_chain: IK链信息 - """ - actor = actor_info.actor - - # 简化的CCD IK算法实现 - end_effector = actor.exposeJoint(None, "modelRoot", ik_chain.end_effector) - if not end_effector: - return - - target_pos = Point3(*ik_chain.target_position) - current_pos = end_effector.getPos(actor) - - # 如果已经足够接近,不需要调整 - distance = (target_pos - current_pos).length() - if distance < 0.001: - return - - # CCD算法迭代 - for _ in range(ik_chain.iterations): - # 从末端关节向前迭代调整 - # 这里简化处理,实际应该遍历整条骨骼链 - direction = target_pos - current_pos - direction.normalize() - - # 简单的朝向调整 - end_effector.lookAt(target_pos) - - # 更新位置 - current_pos = end_effector.getPos(actor) - distance = (target_pos - current_pos).length() - if distance < 0.001: - break - - def _update_lod(self, actor_info: ActorInfo, current_time: float) -> None: - """ - 更新LOD级别 - :param actor_info: Actor信息 - :param current_time: 当前时间 - """ - # LOD更新可以基于距离、性能或其他因素 - # 这里简化实现,实际项目中可能需要更复杂的逻辑 - pass - - def _update_tools(self, dt: float) -> None: - """ - 更新工具系统 - :param dt: 时间增量 - """ - # 更新性能分析器 - self.profiler.sample_performance() - - # 更新调试可视化 - self.visualizer.update_visualizations(self.actors) - - def add_ik_chain(self, actor_id: str, chain_name: str, end_effector: str, - pole_vector: Optional[Tuple[float, float, float]] = None, - iterations: int = 10) -> bool: - """ - 添加IK链 - :param actor_id: Actor标识符 - :param chain_name: 链名称 - :param end_effector: 终端效应器(骨骼名称) - :param pole_vector: 极向量(用于控制弯曲方向) - :param iterations: 迭代次数 - :return: 是否成功添加 - """ - if actor_id not in self.actors: - print(f"错误: Actor {actor_id} 不存在") - return False - - # 检查骨骼是否存在 - actor = self.actors[actor_id].actor - effector_joint = actor.exposeJoint(None, "modelRoot", end_effector) - if not effector_joint: - print(f"错误: 未找到骨骼 {end_effector}") - return False - - # 创建IK链 - ik_chain = IKChain( - chain_name=chain_name, - end_effector=end_effector, - pole_vector=pole_vector, - iterations=iterations, - enabled=True - ) - - # 存储IK链 - self.ik_chains[actor_id][chain_name] = ik_chain - print(f"添加IK链: {chain_name} -> {end_effector} (Actor: {actor_id})") - return True - - def set_ik_target(self, actor_id: str, chain_name: str, target_pos: Tuple[float, float, float]) -> bool: - """ - 设置IK目标位置 - :param actor_id: Actor标识符 - :param chain_name: 链名称 - :param target_pos: 目标位置 (x, y, z) - :return: 是否成功设置 - """ - if actor_id not in self.ik_chains or chain_name not in self.ik_chains[actor_id]: - print(f"错误: IK链 {chain_name} 不存在于Actor {actor_id} 中") - return False - - # 设置目标位置 - self.ik_chains[actor_id][chain_name].target_position = target_pos - return True - - def enable_ik_chain(self, actor_id: str, chain_name: str, enabled: bool = True) -> bool: - """ - 启用/禁用IK链 - :param actor_id: Actor标识符 - :param chain_name: 链名称 - :param enabled: 是否启用 - :return: 是否成功设置 - """ - if actor_id not in self.ik_chains or chain_name not in self.ik_chains[actor_id]: - print(f"错误: IK链 {chain_name} 不存在于Actor {actor_id} 中") - return False - - self.ik_chains[actor_id][chain_name].enabled = enabled - status = "启用" if enabled else "禁用" - print(f"{status}IK链: {chain_name} (Actor: {actor_id})") - return True - - def get_cache_stats(self) -> Dict[str, Any]: - """ - 获取缓存统计信息 - :return: 缓存统计 - """ - return self.animation_cache.get_stats() - - def get_system_stats(self) -> Dict[str, Any]: - """ - 获取系统统计信息 - :return: 系统统计 - """ - cache_stats = self.animation_cache.get_stats() - return { - 'actors_count': len(self.actors), - 'animations_played': self.stats['animations_played'], - 'blends_performed': self.stats['blends_performed'], - 'ik_calculations': self.stats['ik_calculations'], - 'cache_stats': cache_stats, - 'active_actors': len([a for a in self.actors.values() if a.active_animations]) - } - - def start_updating(self) -> None: - """ - 开始更新动画系统 - """ - if not self.is_updating: - self.is_updating = True - print("动画系统更新已启动") - - def stop_updating(self) -> None: - """ - 停止更新动画系统 - """ - if self.is_updating: - self.is_updating = False - print("动画系统更新已停止") - - def clear_cache(self) -> None: - """ - 清空动画缓存 - """ - self.animation_cache.clear() - print("动画缓存已清空") - - def reset_stats(self) -> None: - """ - 重置统计信息 - """ - self.stats = { - 'actors_loaded': 0, - 'animations_played': 0, - 'blends_performed': 0, - 'ik_calculations': 0, - 'cache_hits': 0, - 'cache_misses': 0 - } - self.animation_cache.hits = 0 - self.animation_cache.misses = 0 - print("统计信息已重置") - - # 工具系统访问方法 - def get_analyzer(self) -> AnimationAnalyzer: - """ - 获取动画分析器 - :return: AnimationAnalyzer实例 - """ - return self.analyzer - - def get_editor(self) -> AnimationEditor: - """ - 获取动画编辑器 - :return: AnimationEditor实例 - """ - return self.editor - - def get_visualizer(self) -> DebugVisualizer: - """ - 获取调试可视化工具 - :return: DebugVisualizer实例 - """ - return self.visualizer - - def get_profiler(self) -> AnimationProfiler: - """ - 获取性能分析器 - :return: AnimationProfiler实例 - """ +""" +高级骨骼动画系统 - 核心模块 +提供完整的骨骼动画支持,包括加载、播放、控制等核心功能 +""" + +import math +import json +import os +from collections import defaultdict, deque +from typing import Dict, List, Tuple, Optional, Union, Any, Callable +from dataclasses import dataclass, field +from enum import Enum + +from panda3d.core import * +from direct.actor.Actor import Actor +from direct.interval.IntervalGlobal import * +from direct.showbase.DirectObject import DirectObject +from direct.distributed.ClockDelta import globalClockDelta + +# 导入工具模块 +from .tools import AnimationAnalyzer, AnimationEditor, DebugVisualizer, AnimationProfiler + +# 定义常量 +DEFAULT_ANIMATION_SETTINGS = { + 'default_play_rate': 1.0, + 'default_loop': False, + 'blend_time': 0.3, + 'max_actors': 1000, + 'cache_size': 100, + 'lod_distance_thresholds': [10.0, 25.0, 50.0], + 'lod_frame_rates': [1.0, 0.5, 0.25, 0.1] +} + +# 枚举定义 +class AnimationState(Enum): + """动画状态枚举""" + STOPPED = 0 + PLAYING = 1 + PAUSED = 2 + BLENDING = 3 + LOOPING = 4 + +class BlendMode(Enum): + """混合模式枚举""" + LINEAR = 0 + EASE_IN = 1 + EASE_OUT = 2 + EASE_IN_OUT = 3 + OVERWRITE = 4 + +class LODLevel(Enum): + """LOD级别枚举""" + HIGH = 0 + MEDIUM = 1 + LOW = 2 + VERY_LOW = 3 + +@dataclass +class AnimationInfo: + """动画信息数据类""" + name: str + frame_rate: float + num_frames: int + play_rate: float + current_frame: float + state: AnimationState + loop: bool + start_time: float + duration: float + blend_weight: float = 1.0 + blend_in_time: float = 0.0 + blend_out_time: float = 0.0 + +@dataclass +class ActorInfo: + """Actor信息数据类""" + actor_id: str + actor: Actor + model_path: str + anim_paths: Dict[str, str] + position: Tuple[float, float, float] + scale: Union[float, Tuple[float, float, float]] + animations: Dict[str, AnimationInfo] = field(default_factory=dict) + active_animations: List[str] = field(default_factory=list) + lod_level: LODLevel = LODLevel.HIGH + last_update_time: float = 0.0 + is_networked: bool = False + network_id: str = "" + +@dataclass +class BlendInfo: + """混合信息数据类""" + from_anim: str + to_anim: str + blend_time: float + blend_mode: BlendMode + current_time: float = 0.0 + total_time: float = 0.0 + active: bool = False + +@dataclass +class IKChain: + """IK链数据类""" + chain_name: str + end_effector: str + pole_vector: Optional[Tuple[float, float, float]] + iterations: int + enabled: bool = True + target_position: Optional[Tuple[float, float, float]] = None + +class AnimationCache: + """动画缓存系统""" + + def __init__(self, max_size: int = 100): + self.max_size = max_size + self.cache: Dict[str, Any] = {} + self.access_order: deque = deque() + self.hits = 0 + self.misses = 0 + + def get(self, key: str) -> Optional[Any]: + """获取缓存项""" + if key in self.cache: + self.hits += 1 + # 更新访问顺序 + self.access_order.remove(key) + self.access_order.append(key) + return self.cache[key] + else: + self.misses += 1 + return None + + def put(self, key: str, value: Any) -> None: + """放入缓存项""" + # 如果缓存已满,移除最久未使用的项 + if len(self.cache) >= self.max_size: + oldest_key = self.access_order.popleft() + del self.cache[oldest_key] + + self.cache[key] = value + self.access_order.append(key) + + def invalidate(self, key: str) -> None: + """使缓存项失效""" + if key in self.cache: + del self.cache[key] + self.access_order.remove(key) + + def clear(self) -> None: + """清空缓存""" + self.cache.clear() + self.access_order.clear() + self.hits = 0 + self.misses = 0 + + def get_stats(self) -> Dict[str, Any]: + """获取缓存统计信息""" + total_requests = self.hits + self.misses + hit_rate = self.hits / total_requests if total_requests > 0 else 0 + return { + 'size': len(self.cache), + 'max_size': self.max_size, + 'hits': self.hits, + 'misses': self.misses, + 'hit_rate': hit_rate + } + +class AnimationCompressor: + """动画压缩系统""" + + def __init__(self): + self.position_threshold = 0.001 + self.rotation_threshold = 0.01 + self.scale_threshold = 0.001 + + def compress_keyframes(self, keyframes: List[Tuple[float, Tuple[float, float, float]]], + data_type: str = 'position') -> List[Tuple[float, Tuple[float, float, float]]]: + """ + 压缩关键帧数据 + :param keyframes: 关键帧列表 [(time, (x, y, z)), ...] + :param data_type: 数据类型 ('position', 'rotation', 'scale') + :return: 压缩后的关键帧列表 + """ + if len(keyframes) <= 2: + return keyframes + + compressed = [keyframes[0]] # 总是保留第一帧 + last_keyframe = keyframes[0] + + # 根据数据类型选择阈值 + threshold = getattr(self, f'{data_type}_threshold', 0.001) + + for i in range(1, len(keyframes) - 1): + current_keyframe = keyframes[i] + next_keyframe = keyframes[i + 1] + + # 检查当前帧是否可以被简化 + if not self._is_keyframe_necessary(last_keyframe, current_keyframe, next_keyframe, threshold, data_type): + continue + + compressed.append(current_keyframe) + last_keyframe = current_keyframe + + compressed.append(keyframes[-1]) # 总是保留最后一帧 + return compressed + + def _is_keyframe_necessary(self, prev_frame: Tuple[float, Tuple[float, float, float]], + current_frame: Tuple[float, Tuple[float, float, float]], + next_frame: Tuple[float, Tuple[float, float, float]], + threshold: float, data_type: str) -> bool: + """ + 判断关键帧是否必要 + :param prev_frame: 前一帧 + :param current_frame: 当前帧 + :param next_frame: 后一帧 + :param threshold: 阈值 + :param data_type: 数据类型 + :return: 是否必要 + """ + prev_time, prev_data = prev_frame + curr_time, curr_data = current_frame + next_time, next_data = next_frame + + # 计算线性插值 + t = (curr_time - prev_time) / (next_time - prev_time) + interpolated = tuple(prev_data[i] + t * (next_data[i] - prev_data[i]) for i in range(3)) + + # 计算与实际值的差异 + diff = math.sqrt(sum((curr_data[i] - interpolated[i]) ** 2 for i in range(3))) + + return diff > threshold + +class SkeletalAnimationCore(DirectObject): + """ + 骨骼动画核心系统 + 提供完整的骨骼动画管理功能 + """ + + def __init__(self, world): + """ + 初始化骨骼动画核心系统 + :param world: 世界对象 + """ + super().__init__() + self.world = world + self.actors: Dict[str, ActorInfo] = {} + self.actor_configs: Dict[str, Dict[str, Any]] = {} + self.blend_infos: Dict[str, Dict[str, BlendInfo]] = defaultdict(dict) + self.ik_chains: Dict[str, Dict[str, IKChain]] = defaultdict(dict) + self.animation_cache = AnimationCache(DEFAULT_ANIMATION_SETTINGS['cache_size']) + self.compressor = AnimationCompressor() + self.event_handlers: Dict[str, List[Callable]] = defaultdict(list) + self.is_updating = False + self.update_task = None + + # 工具系统 + self.analyzer = AnimationAnalyzer(self) + self.editor = AnimationEditor(self) + self.visualizer = DebugVisualizer(world) + self.profiler = AnimationProfiler(self) + + # 性能统计 + self.stats = { + 'actors_loaded': 0, + 'animations_played': 0, + 'blends_performed': 0, + 'ik_calculations': 0, + 'cache_hits': 0, + 'cache_misses': 0 + } + + print("骨骼动画核心系统初始化完成") + + def load_actor(self, model_path: str, anim_paths: Optional[Dict[str, str]] = None, + actor_id: Optional[str] = None, position: Tuple[float, float, float] = (0, 0, 0), + scale: Union[float, Tuple[float, float, float]] = 1.0) -> Tuple[Optional[Actor], str]: + """ + 加载Actor模型 + :param model_path: 模型文件路径 + :param anim_paths: 动画文件路径字典 {anim_name: anim_path} + :param actor_id: Actor标识符,如果为None则自动生成 + :param position: 位置坐标 (x, y, z) + :param scale: 缩放比例 + :return: (Actor对象, actor_id) + """ + try: + # 检查模型文件是否存在 + if not os.path.exists(model_path): + raise FileNotFoundError(f"模型文件不存在: {model_path}") + + # 生成Actor ID + if actor_id is None: + actor_id = f"actor_{len(self.actors)}" + + # 检查Actor是否已存在 + if actor_id in self.actors: + print(f"警告: Actor {actor_id} 已存在,将被替换") + self.unload_actor(actor_id) + + # 检查缓存 + cache_key = f"actor_{model_path}_{hash(str(anim_paths))}" + cached_actor = self.animation_cache.get(cache_key) + + if cached_actor: + actor = cached_actor + print(f"从缓存加载Actor: {actor_id}") + else: + # 创建Actor + if anim_paths: + # 验证动画文件 + valid_anims = {} + for anim_name, anim_path in anim_paths.items(): + if os.path.exists(anim_path): + valid_anims[anim_name] = anim_path + else: + print(f"警告: 动画文件不存在: {anim_path}") + actor = Actor(model_path, valid_anims) + else: + actor = Actor(model_path) + + # 缓存Actor + self.animation_cache.put(cache_key, actor) + + # 设置位置和缩放 + actor.setPos(*position) + if isinstance(scale, (int, float)): + actor.setScale(scale) + else: + actor.setScale(*scale) + + # 将Actor添加到场景中 + actor.reparentTo(self.world.render) + actor.setName(actor_id) + + # 创建Actor信息 + actor_info = ActorInfo( + actor_id=actor_id, + actor=actor, + model_path=model_path, + anim_paths=anim_paths or {}, + position=position, + scale=scale + ) + + # 初始化动画信息 + for anim_name in actor.getAnimNames(): + control = actor.getAnimControl(anim_name) + if control: + anim_info = AnimationInfo( + name=anim_name, + frame_rate=actor.getFrameRate(anim_name), + num_frames=actor.getNumFrames(anim_name), + play_rate=DEFAULT_ANIMATION_SETTINGS['default_play_rate'], + current_frame=0.0, + state=AnimationState.STOPPED, + loop=DEFAULT_ANIMATION_SETTINGS['default_loop'], + start_time=0.0, + duration=control.getDuration() + ) + actor_info.animations[anim_name] = anim_info + + # 存储Actor信息 + self.actors[actor_id] = actor_info + self.actor_configs[actor_id] = { + 'model_path': model_path, + 'anim_paths': anim_paths, + 'position': position, + 'scale': scale + } + + self.stats['actors_loaded'] += 1 + print(f"成功加载Actor: {actor_id}") + print(f" 模型: {model_path}") + print(f" 位置: {position}") + print(f" 缩放: {scale}") + print(f" 可用动画: {list(actor_info.animations.keys())}") + + return actor, actor_id + + except Exception as e: + print(f"加载Actor失败: {e}") + import traceback + traceback.print_exc() + return None, "" + + def unload_actor(self, actor_id: str) -> bool: + """ + 卸载Actor + :param actor_id: Actor标识符 + :return: 是否成功卸载 + """ + if actor_id not in self.actors: + print(f"错误: Actor {actor_id} 不存在") + return False + + try: + actor_info = self.actors[actor_id] + actor = actor_info.actor + + # 停止所有动画 + self.stop_all_animations(actor_id) + + # 清理混合信息 + if actor_id in self.blend_infos: + del self.blend_infos[actor_id] + + # 清理IK链 + if actor_id in self.ik_chains: + del self.ik_chains[actor_id] + + # 从场景中移除 + actor.cleanup() + + # 从管理器中移除 + del self.actors[actor_id] + if actor_id in self.actor_configs: + del self.actor_configs[actor_id] + + print(f"已卸载Actor: {actor_id}") + return True + + except Exception as e: + print(f"卸载Actor失败: {e}") + import traceback + traceback.print_exc() + return False + + def play_animation(self, actor_id: str, anim_name: str, loop: bool = False, + play_rate: float = 1.0, start_frame: Optional[int] = None, + end_frame: Optional[int] = None) -> bool: + """ + 播放动画 + :param actor_id: Actor标识符 + :param anim_name: 动画名称 + :param loop: 是否循环播放 + :param play_rate: 播放速度 + :param start_frame: 起始帧 + :param end_frame: 结束帧 + :return: 是否成功播放 + """ + if actor_id not in self.actors: + print(f"错误: Actor {actor_id} 不存在") + return False + + actor_info = self.actors[actor_id] + actor = actor_info.actor + + # 检查动画是否存在 + if anim_name not in actor.getAnimNames(): + print(f"错误: 动画 {anim_name} 不存在于Actor {actor_id} 中") + print(f" 可用动画: {actor.getAnimNames()}") + return False + + try: + # 设置播放速度 + actor.setPlayRate(play_rate, anim_name) + + # 播放动画 + if loop: + if start_frame is not None and end_frame is not None: + actor.loop(anim_name, restart=False, fromFrame=start_frame, toFrame=end_frame) + else: + actor.loop(anim_name, restart=False) + else: + if start_frame is not None and end_frame is not None: + actor.play(anim_name, fromFrame=start_frame, toFrame=end_frame) + else: + actor.play(anim_name) + + # 更新动画信息 + if anim_name in actor_info.animations: + anim_info = actor_info.animations[anim_name] + anim_info.state = AnimationState.LOOPING if loop else AnimationState.PLAYING + anim_info.loop = loop + anim_info.play_rate = play_rate + anim_info.start_time = globalClock.getRealTime() + + # 添加到活动动画列表 + if anim_name not in actor_info.active_animations: + actor_info.active_animations.append(anim_name) + + self.stats['animations_played'] += 1 + print(f"播放动画: {anim_name} (Actor: {actor_id}, 循环: {loop}, 速度: {play_rate}x)") + return True + + except Exception as e: + print(f"播放动画失败: {e}") + import traceback + traceback.print_exc() + return False + + def stop_animation(self, actor_id: str, anim_name: Optional[str] = None) -> bool: + """ + 停止动画 + :param actor_id: Actor标识符 + :param anim_name: 动画名称,如果为None则停止所有动画 + :return: 是否成功停止 + """ + if actor_id not in self.actors: + print(f"错误: Actor {actor_id} 不存在") + return False + + actor_info = self.actors[actor_id] + actor = actor_info.actor + + try: + if anim_name: + # 停止指定动画 + if anim_name in actor.getAnimNames(): + actor.stop(anim_name) + # 从活动动画列表中移除 + if anim_name in actor_info.active_animations: + actor_info.active_animations.remove(anim_name) + # 更新动画状态 + if anim_name in actor_info.animations: + actor_info.animations[anim_name].state = AnimationState.STOPPED + print(f"停止动画: {anim_name} (Actor: {actor_id})") + else: + print(f"警告: 动画 {anim_name} 不存在于Actor {actor_id} 中") + else: + # 停止所有动画 + actor.stop() + # 清空活动动画列表 + actor_info.active_animations.clear() + # 更新所有动画状态 + for anim_info in actor_info.animations.values(): + anim_info.state = AnimationState.STOPPED + print(f"停止所有动画 (Actor: {actor_id})") + + return True + + except Exception as e: + print(f"停止动画失败: {e}") + import traceback + traceback.print_exc() + return False + + def stop_all_animations(self, actor_id: str) -> bool: + """ + 停止指定Actor的所有动画 + :param actor_id: Actor标识符 + :return: 是否成功停止 + """ + return self.stop_animation(actor_id) + + def pause_animation(self, actor_id: str, anim_name: Optional[str] = None) -> bool: + """ + 暂停动画 + :param actor_id: Actor标识符 + :param anim_name: 动画名称,如果为None则暂停所有动画 + :return: 是否成功暂停 + """ + if actor_id not in self.actors: + print(f"错误: Actor {actor_id} 不存在") + return False + + actor_info = self.actors[actor_id] + actor = actor_info.actor + + try: + if anim_name: + if anim_name in actor.getAnimNames(): + control = actor.getAnimControl(anim_name) + if control and control.isPlaying(): + # 通过设置播放速度为0来模拟暂停 + current_rate = actor.getPlayRate(anim_name) + actor.setPlayRate(0, anim_name) + # 更新动画状态 + if anim_name in actor_info.animations: + actor_info.animations[anim_name].state = AnimationState.PAUSED + print(f"暂停动画: {anim_name} (Actor: {actor_id})") + return True + else: + print(f"警告: 动画 {anim_name} 不存在于Actor {actor_id} 中") + else: + # 暂停所有活动动画 + success = False + for active_anim in actor_info.active_animations: + control = actor.getAnimControl(active_anim) + if control and control.isPlaying(): + current_rate = actor.getPlayRate(active_anim) + actor.setPlayRate(0, active_anim) + if active_anim in actor_info.animations: + actor_info.animations[active_anim].state = AnimationState.PAUSED + success = True + if success: + print(f"暂停所有动画 (Actor: {actor_id})") + return success + + except Exception as e: + print(f"暂停动画失败: {e}") + import traceback + traceback.print_exc() + return False + + def resume_animation(self, actor_id: str, anim_name: Optional[str] = None) -> bool: + """ + 恢复动画播放 + :param actor_id: Actor标识符 + :param anim_name: 动画名称,如果为None则恢复所有动画 + :return: 是否成功恢复 + """ + if actor_id not in self.actors: + print(f"错误: Actor {actor_id} 不存在") + return False + + actor_info = self.actors[actor_id] + actor = actor_info.actor + + try: + if anim_name: + if anim_name in actor.getAnimNames(): + control = actor.getAnimControl(anim_name) + if control and control.getPlayRate() == 0: + # 恢复正常播放速度 + actor.setPlayRate(1.0, anim_name) + # 更新动画状态 + if anim_name in actor_info.animations: + anim_state = AnimationState.LOOPING if actor_info.animations[anim_name].loop else AnimationState.PLAYING + actor_info.animations[anim_name].state = anim_state + print(f"恢复动画: {anim_name} (Actor: {actor_id})") + return True + else: + print(f"警告: 动画 {anim_name} 不存在于Actor {actor_id} 中") + else: + # 恢复所有暂停的动画 + success = False + for active_anim in actor_info.active_animations: + control = actor.getAnimControl(active_anim) + if control and control.getPlayRate() == 0: + actor.setPlayRate(1.0, active_anim) + if active_anim in actor_info.animations: + anim_state = AnimationState.LOOPING if actor_info.animations[active_anim].loop else AnimationState.PLAYING + actor_info.animations[active_anim].state = anim_state + success = True + if success: + print(f"恢复所有动画 (Actor: {actor_id})") + return success + + except Exception as e: + print(f"恢复动画失败: {e}") + import traceback + traceback.print_exc() + return False + + def set_animation_speed(self, actor_id: str, anim_name: str, speed: float) -> bool: + """ + 设置动画播放速度 + :param actor_id: Actor标识符 + :param anim_name: 动画名称 + :param speed: 播放速度 + :return: 是否成功设置 + """ + if actor_id not in self.actors: + print(f"错误: Actor {actor_id} 不存在") + return False + + actor_info = self.actors[actor_id] + actor = actor_info.actor + + try: + if anim_name in actor.getAnimNames(): + actor.setPlayRate(speed, anim_name) + # 更新动画信息 + if anim_name in actor_info.animations: + actor_info.animations[anim_name].play_rate = speed + print(f"设置动画速度: {anim_name} 为 {speed}x (Actor: {actor_id})") + return True + else: + print(f"警告: 动画 {anim_name} 不存在于Actor {actor_id} 中") + return False + + except Exception as e: + print(f"设置动画速度失败: {e}") + import traceback + traceback.print_exc() + return False + + def blend_animations(self, actor_id: str, from_anim: str, to_anim: str, + blend_time: float = 0.3, blend_mode: BlendMode = BlendMode.LINEAR) -> bool: + """ + 混合两个动画 + :param actor_id: Actor标识符 + :param from_anim: 起始动画名称 + :param to_anim: 目标动画名称 + :param blend_time: 混合时间 + :param blend_mode: 混合模式 + :return: 是否成功开始混合 + """ + if actor_id not in self.actors: + print(f"错误: Actor {actor_id} 不存在") + return False + + actor_info = self.actors[actor_id] + actor = actor_info.actor + + # 检查动画是否存在 + if from_anim not in actor.getAnimNames(): + print(f"错误: 起始动画 {from_anim} 不存在于Actor {actor_id} 中") + return False + + if to_anim not in actor.getAnimNames(): + print(f"错误: 目标动画 {to_anim} 不存在于Actor {actor_id} 中") + return False + + try: + # 创建混合信息 + blend_info = BlendInfo( + from_anim=from_anim, + to_anim=to_anim, + blend_time=blend_time, + blend_mode=blend_mode, + current_time=0.0, + total_time=blend_time, + active=True + ) + + # 存储混合信息 + blend_id = f"{from_anim}_to_{to_anim}" + self.blend_infos[actor_id][blend_id] = blend_info + + # 开始混合过程 + self._start_animation_blend(actor_info, blend_info) + + self.stats['blends_performed'] += 1 + print(f"开始动画混合: {from_anim} -> {to_anim} (Actor: {actor_id}, 时间: {blend_time}s)") + return True + + except Exception as e: + print(f"动画混合失败: {e}") + import traceback + traceback.print_exc() + return False + + def _start_animation_blend(self, actor_info: ActorInfo, blend_info: BlendInfo) -> None: + """ + 开始动画混合过程 + :param actor_info: Actor信息 + :param blend_info: 混合信息 + """ + actor = actor_info.actor + from_anim = blend_info.from_anim + to_anim = blend_info.to_anim + + # 停止起始动画 + actor.stop(from_anim) + + # 播放目标动画 + actor.play(to_anim) + + # 设置混合权重(简化实现) + if to_anim in actor_info.animations: + actor_info.animations[to_anim].blend_weight = 1.0 + + def get_actor(self, actor_id: str) -> Optional[Actor]: + """ + 获取Actor对象 + :param actor_id: Actor标识符 + :return: Actor对象或None + """ + if actor_id in self.actors: + return self.actors[actor_id].actor + return None + + def get_actor_animations(self, actor_id: str) -> List[str]: + """ + 获取Actor的所有动画名称 + :param actor_id: Actor标识符 + :return: 动画名称列表 + """ + if actor_id in self.actors: + return list(self.actors[actor_id].animations.keys()) + return [] + + def get_animation_info(self, actor_id: str, anim_name: str) -> Optional[Dict[str, Any]]: + """ + 获取动画信息 + :param actor_id: Actor标识符 + :param anim_name: 动画名称 + :return: 动画信息字典 + """ + if actor_id not in self.actors: + print(f"错误: Actor {actor_id} 不存在") + return None + + actor_info = self.actors[actor_id] + + if anim_name not in actor_info.animations: + print(f"错误: 动画 {anim_name} 不存在于Actor {actor_id} 中") + return None + + anim_info = actor_info.animations[anim_name] + actor = actor_info.actor + control = actor.getAnimControl(anim_name) if anim_name in actor.getAnimNames() else None + + return { + 'name': anim_info.name, + 'frame_rate': anim_info.frame_rate, + 'num_frames': anim_info.num_frames, + 'play_rate': anim_info.play_rate, + 'current_frame': anim_info.current_frame, + 'state': anim_info.state.name, + 'loop': anim_info.loop, + 'start_time': anim_info.start_time, + 'duration': anim_info.duration, + 'is_playing': control.isPlaying() if control else False, + 'is_finished': control.isFinished() if control else True + } + + def get_actor_info(self, actor_id: str) -> Optional[Dict[str, Any]]: + """ + 获取Actor详细信息 + :param actor_id: Actor标识符 + :return: Actor信息字典 + """ + if actor_id not in self.actors: + print(f"错误: Actor {actor_id} 不存在") + return None + + actor_info = self.actors[actor_id] + actor = actor_info.actor + + return { + 'id': actor_info.actor_id, + 'model_path': actor_info.model_path, + 'anim_paths': actor_info.anim_paths, + 'position': actor.getPos().toTuple(), + 'scale': actor.getScale().toTuple(), + 'available_animations': list(actor_info.animations.keys()), + 'active_animations': actor_info.active_animations.copy(), + 'lod_level': actor_info.lod_level.name, + 'has_character': actor.hasCharacter(), + 'num_parts': actor.getNumParts(), + 'bounds': { + 'min': actor.getTightBounds()[0].toTuple() if actor.getTightBounds() else None, + 'max': actor.getTightBounds()[1].toTuple() if actor.getTightBounds() else None + } + } + + def set_actor_position(self, actor_id: str, x: float, y: float, z: float) -> bool: + """ + 设置Actor位置 + :param actor_id: Actor标识符 + :param x: X坐标 + :param y: Y坐标 + :param z: Z坐标 + :return: 是否成功设置 + """ + if actor_id in self.actors: + self.actors[actor_id].actor.setPos(x, y, z) + self.actors[actor_id].position = (x, y, z) + if actor_id in self.actor_configs: + self.actor_configs[actor_id]['position'] = (x, y, z) + print(f"设置Actor {actor_id} 位置: ({x}, {y}, {z})") + return True + return False + + def set_actor_scale(self, actor_id: str, scale: Union[float, Tuple[float, float, float]]) -> bool: + """ + 设置Actor缩放 + :param actor_id: Actor标识符 + :param scale: 缩放值 (数值或(x, y, z)元组) + :return: 是否成功设置 + """ + if actor_id in self.actors: + actor = self.actors[actor_id].actor + if isinstance(scale, (int, float)): + actor.setScale(scale) + else: + actor.setScale(*scale) + self.actors[actor_id].scale = scale + if actor_id in self.actor_configs: + self.actor_configs[actor_id]['scale'] = scale + print(f"设置Actor {actor_id} 缩放: {scale}") + return True + return False + + def update(self, dt: float) -> None: + """ + 更新动画系统 + :param dt: 时间增量 + """ + if not self.is_updating: + return + + current_time = globalClock.getRealTime() + + # 更新所有Actor + for actor_id, actor_info in self.actors.items(): + # 更新动画信息 + self._update_actor_animations(actor_info, dt, current_time) + + # 更新混合 + self._update_blends(actor_info, dt) + + # 更新IK + self._update_ik(actor_info) + + # 更新LOD + self._update_lod(actor_info, current_time) + + # 更新工具系统 + self._update_tools(dt) + + def _update_actor_animations(self, actor_info: ActorInfo, dt: float, current_time: float) -> None: + """ + 更新Actor动画 + :param actor_info: Actor信息 + :param dt: 时间增量 + :param current_time: 当前时间 + """ + actor = actor_info.actor + + # 更新活动动画的当前帧信息 + for anim_name in actor_info.active_animations: + if anim_name in actor_info.animations: + anim_info = actor_info.animations[anim_name] + control = actor.getAnimControl(anim_name) if anim_name in actor.getAnimNames() else None + + if control and control.isPlaying(): + anim_info.current_frame = actor.getCurrentFrame(anim_name) + anim_info.state = AnimationState.LOOPING if anim_info.loop else AnimationState.PLAYING + elif control and control.getPlayRate() == 0: + anim_info.state = AnimationState.PAUSED + else: + anim_info.state = AnimationState.STOPPED + + actor_info.last_update_time = current_time + + def _update_blends(self, actor_info: ActorInfo, dt: float) -> None: + """ + 更新动画混合 + :param actor_info: Actor信息 + :param dt: 时间增量 + """ + actor_id = actor_info.actor_id + + if actor_id not in self.blend_infos: + return + + # 更新所有混合 + finished_blends = [] + for blend_id, blend_info in self.blend_infos[actor_id].items(): + if not blend_info.active: + continue + + blend_info.current_time += dt + + # 计算混合权重 + t = min(1.0, blend_info.current_time / blend_info.total_time) + + # 根据混合模式应用缓动函数 + if blend_info.blend_mode == BlendMode.EASE_IN: + t = t * t + elif blend_info.blend_mode == BlendMode.EASE_OUT: + t = 1.0 - (1.0 - t) * (1.0 - t) + elif blend_info.blend_mode == BlendMode.EASE_IN_OUT: + t = t * t * (3.0 - 2.0 * t) + + # 更新目标动画的混合权重 + if blend_info.to_anim in actor_info.animations: + actor_info.animations[blend_info.to_anim].blend_weight = t + + # 检查混合是否完成 + if blend_info.current_time >= blend_info.total_time: + blend_info.active = False + finished_blends.append(blend_id) + + # 移除已完成的混合 + for blend_id in finished_blends: + del self.blend_infos[actor_id][blend_id] + + def _update_ik(self, actor_info: ActorInfo) -> None: + """ + 更新IK计算 + :param actor_info: Actor信息 + """ + actor_id = actor_info.actor_id + + if actor_id not in self.ik_chains: + return + + # 更新所有IK链 + for chain_name, ik_chain in self.ik_chains[actor_id].items(): + if not ik_chain.enabled or not ik_chain.target_position: + continue + + self._solve_ik_chain(actor_info, ik_chain) + self.stats['ik_calculations'] += 1 + + def _solve_ik_chain(self, actor_info: ActorInfo, ik_chain: IKChain) -> None: + """ + 解算IK链 + :param actor_info: Actor信息 + :param ik_chain: IK链信息 + """ + actor = actor_info.actor + + # 简化的CCD IK算法实现 + end_effector = actor.exposeJoint(None, "modelRoot", ik_chain.end_effector) + if not end_effector: + return + + target_pos = Point3(*ik_chain.target_position) + current_pos = end_effector.getPos(actor) + + # 如果已经足够接近,不需要调整 + distance = (target_pos - current_pos).length() + if distance < 0.001: + return + + # CCD算法迭代 + for _ in range(ik_chain.iterations): + # 从末端关节向前迭代调整 + # 这里简化处理,实际应该遍历整条骨骼链 + direction = target_pos - current_pos + direction.normalize() + + # 简单的朝向调整 + end_effector.lookAt(target_pos) + + # 更新位置 + current_pos = end_effector.getPos(actor) + distance = (target_pos - current_pos).length() + if distance < 0.001: + break + + def _update_lod(self, actor_info: ActorInfo, current_time: float) -> None: + """ + 更新LOD级别 + :param actor_info: Actor信息 + :param current_time: 当前时间 + """ + # LOD更新可以基于距离、性能或其他因素 + # 这里简化实现,实际项目中可能需要更复杂的逻辑 + pass + + def _update_tools(self, dt: float) -> None: + """ + 更新工具系统 + :param dt: 时间增量 + """ + # 更新性能分析器 + self.profiler.sample_performance() + + # 更新调试可视化 + self.visualizer.update_visualizations(self.actors) + + def add_ik_chain(self, actor_id: str, chain_name: str, end_effector: str, + pole_vector: Optional[Tuple[float, float, float]] = None, + iterations: int = 10) -> bool: + """ + 添加IK链 + :param actor_id: Actor标识符 + :param chain_name: 链名称 + :param end_effector: 终端效应器(骨骼名称) + :param pole_vector: 极向量(用于控制弯曲方向) + :param iterations: 迭代次数 + :return: 是否成功添加 + """ + if actor_id not in self.actors: + print(f"错误: Actor {actor_id} 不存在") + return False + + # 检查骨骼是否存在 + actor = self.actors[actor_id].actor + effector_joint = actor.exposeJoint(None, "modelRoot", end_effector) + if not effector_joint: + print(f"错误: 未找到骨骼 {end_effector}") + return False + + # 创建IK链 + ik_chain = IKChain( + chain_name=chain_name, + end_effector=end_effector, + pole_vector=pole_vector, + iterations=iterations, + enabled=True + ) + + # 存储IK链 + self.ik_chains[actor_id][chain_name] = ik_chain + print(f"添加IK链: {chain_name} -> {end_effector} (Actor: {actor_id})") + return True + + def set_ik_target(self, actor_id: str, chain_name: str, target_pos: Tuple[float, float, float]) -> bool: + """ + 设置IK目标位置 + :param actor_id: Actor标识符 + :param chain_name: 链名称 + :param target_pos: 目标位置 (x, y, z) + :return: 是否成功设置 + """ + if actor_id not in self.ik_chains or chain_name not in self.ik_chains[actor_id]: + print(f"错误: IK链 {chain_name} 不存在于Actor {actor_id} 中") + return False + + # 设置目标位置 + self.ik_chains[actor_id][chain_name].target_position = target_pos + return True + + def enable_ik_chain(self, actor_id: str, chain_name: str, enabled: bool = True) -> bool: + """ + 启用/禁用IK链 + :param actor_id: Actor标识符 + :param chain_name: 链名称 + :param enabled: 是否启用 + :return: 是否成功设置 + """ + if actor_id not in self.ik_chains or chain_name not in self.ik_chains[actor_id]: + print(f"错误: IK链 {chain_name} 不存在于Actor {actor_id} 中") + return False + + self.ik_chains[actor_id][chain_name].enabled = enabled + status = "启用" if enabled else "禁用" + print(f"{status}IK链: {chain_name} (Actor: {actor_id})") + return True + + def get_cache_stats(self) -> Dict[str, Any]: + """ + 获取缓存统计信息 + :return: 缓存统计 + """ + return self.animation_cache.get_stats() + + def get_system_stats(self) -> Dict[str, Any]: + """ + 获取系统统计信息 + :return: 系统统计 + """ + cache_stats = self.animation_cache.get_stats() + return { + 'actors_count': len(self.actors), + 'animations_played': self.stats['animations_played'], + 'blends_performed': self.stats['blends_performed'], + 'ik_calculations': self.stats['ik_calculations'], + 'cache_stats': cache_stats, + 'active_actors': len([a for a in self.actors.values() if a.active_animations]) + } + + def start_updating(self) -> None: + """ + 开始更新动画系统 + """ + if not self.is_updating: + self.is_updating = True + print("动画系统更新已启动") + + def stop_updating(self) -> None: + """ + 停止更新动画系统 + """ + if self.is_updating: + self.is_updating = False + print("动画系统更新已停止") + + def clear_cache(self) -> None: + """ + 清空动画缓存 + """ + self.animation_cache.clear() + print("动画缓存已清空") + + def reset_stats(self) -> None: + """ + 重置统计信息 + """ + self.stats = { + 'actors_loaded': 0, + 'animations_played': 0, + 'blends_performed': 0, + 'ik_calculations': 0, + 'cache_hits': 0, + 'cache_misses': 0 + } + self.animation_cache.hits = 0 + self.animation_cache.misses = 0 + print("统计信息已重置") + + # 工具系统访问方法 + def get_analyzer(self) -> AnimationAnalyzer: + """ + 获取动画分析器 + :return: AnimationAnalyzer实例 + """ + return self.analyzer + + def get_editor(self) -> AnimationEditor: + """ + 获取动画编辑器 + :return: AnimationEditor实例 + """ + return self.editor + + def get_visualizer(self) -> DebugVisualizer: + """ + 获取调试可视化工具 + :return: DebugVisualizer实例 + """ + return self.visualizer + + def get_profiler(self) -> AnimationProfiler: + """ + 获取性能分析器 + :return: AnimationProfiler实例 + """ return self.profiler \ No newline at end of file diff --git a/plugins/user/advanced_skeletal_animation/examples.py b/plugins/user/advanced_skeletal_animation/examples.py index 41a57963..6a26a0d6 100644 --- a/plugins/user/advanced_skeletal_animation/examples.py +++ b/plugins/user/advanced_skeletal_animation/examples.py @@ -1,387 +1,387 @@ -""" -高级骨骼动画系统使用示例 -提供各种使用场景的代码示例 -""" - -def example_basic_usage(world): - """ - 基本使用示例 - """ - print("=== 高级骨骼动画插件基本使用示例 ===") - - # 获取插件实例 - plugin = world.plugin_manager.get_plugin("advanced_skeletal_animation") - if not plugin: - print("错误: 高级骨骼动画插件未加载") - return - - core_system = plugin.get_core_system() - - # 1. 加载简单的Actor - print("1. 加载Actor...") - actor, actor_id = core_system.load_actor( - model_path="models/characters/robot.egg", - position=(0, 0, 0), - scale=1.0 - ) - - if actor: - print(f" 成功加载Actor: {actor_id}") - - # 2. 获取可用动画 - animations = core_system.get_actor_animations(actor_id) - print(f"2. 可用动画: {animations}") - - # 3. 播放动画 - if animations: - print("3. 播放第一个动画...") - core_system.play_animation( - actor_id=actor_id, - anim_name=animations[0], - loop=True, - play_rate=1.0 - ) - - # 4. 获取动画信息 - if animations: - info = core_system.get_animation_info(actor_id, animations[0]) - print(f"4. 动画信息: {info}") - - print("=== 基本使用示例结束 ===\n") - - -def example_advanced_usage(world): - """ - 高级使用示例 - """ - print("=== 高级骨骼动画插件高级使用示例 ===") - - # 获取插件实例 - plugin = world.plugin_manager.get_plugin("advanced_skeletal_animation") - if not plugin: - print("错误: 高级骨骼动画插件未加载") - return - - core_system = plugin.get_core_system() - - # 1. 加载带多个动画的Actor - print("1. 加载带多个动画的Actor...") - anim_dict = { - "walk": "models/anim/walk.egg", - "run": "models/anim/run.egg", - "jump": "models/anim/jump.egg", - "idle": "models/anim/idle.egg" - } - - actor, actor_id = core_system.load_actor( - model_path="models/characters/hero.egg", - anim_dict=anim_dict, - position=(5, 0, 0), - scale=1.2 - ) - - if actor: - print(f" 成功加载Actor: {actor_id}") - - # 2. 播放循环动画 - print("2. 播放循环idle动画...") - core_system.play_animation(actor_id, "idle", loop=True) - - # 3. 3秒后切换到walk动画 - def switch_to_walk(): - print("3. 切换到walk动画...") - core_system.stop_animation(actor_id, "idle") - core_system.play_animation(actor_id, "walk", loop=True) - - # 使用Panda3D的任务系统在3秒后执行 - def schedule_task(task): - if task.time >= 3.0: - switch_to_walk() - return task.done - return task.cont - - world.taskMgr.doMethodLater(3.0, schedule_task, "switch_animation_task") - - # 4. 动画混合示例 - print("4. 创建动画混合...") - core_system.blend_animations( - actor_id, - "walk", - "run", - blend_time=2.0 - ) - - print("=== 高级使用示例结束 ===\n") - - -def example_state_machine_usage(world): - """ - 状态机使用示例 - """ - print("=== 动画状态机使用示例 ===") - - # 获取插件实例 - plugin = world.plugin_manager.get_plugin("advanced_skeletal_animation") - if not plugin: - print("错误: 高级骨骼动画插件未加载") - return - - core_system = plugin.get_core_system() - - # 加载Actor - actor, actor_id = core_system.load_actor( - model_path="models/characters/character.egg", - anim_dict={ - "idle": "models/anim/idle.egg", - "walk": "models/anim/walk.egg", - "run": "models/anim/run.egg" - }, - position=(10, 0, 0) - ) - - if actor: - # 导入状态机类 - from .advanced import AnimationStateMachine - - # 创建状态机 - state_machine = AnimationStateMachine(actor) - - # 添加状态 - state_machine.add_state("idle", "idle", loop=True) - state_machine.add_state("walk", "walk", loop=True) - state_machine.add_state("run", "run", loop=True) - - # 设置转换 - state_machine.set_transition("idle", "walk") - state_machine.set_transition("walk", "run") - state_machine.set_transition("run", "idle") - - # 进入初始状态 - state_machine.enter_state("idle") - - # 添加到更新任务中 - def state_machine_update(task): - dt = globalClock.getDt() - state_machine.update(dt) - return task.cont - - world.taskMgr.add(state_machine_update, f"state_machine_update_{actor_id}") - - print(f"状态机已为Actor {actor_id} 创建并启动") - - print("=== 状态机使用示例结束 ===\n") - - -def example_network_sync_usage(world): - """ - 网络同步使用示例 - """ - print("=== 网络同步使用示例 ===") - - # 获取插件实例 - plugin = world.plugin_manager.get_plugin("advanced_skeletal_animation") - if not plugin: - print("错误: 高级骨骼动画插件未加载") - return - - core_system = plugin.get_core_system() - network_system = plugin.get_network_system() - - # 设置网络模式 - from .network import NetworkMode, SyncMode - network_system.set_network_mode(NetworkMode.SERVER) - network_system.set_sync_mode(SyncMode.STATE_SYNC) - - # 加载Actor并注册网络同步 - actor, actor_id = core_system.load_actor( - model_path="models/characters/player.egg", - anim_dict={ - "idle": "models/anim/idle.egg", - "walk": "models/anim/walk.egg" - }, - position=(15, 0, 0) - ) - - if actor: - network_id = network_system.register_actor_for_sync(actor_id) - print(f"Actor {actor_id} 已注册网络同步 (网络ID: {network_id})") - - # 播放动画 - core_system.play_animation(actor_id, "walk", loop=True) - - print("=== 网络同步使用示例结束 ===\n") - - -def example_lod_usage(world): - """ - LOD系统使用示例 - """ - print("=== LOD系统使用示例 ===") - - # 获取插件实例 - plugin = world.plugin_manager.get_plugin("advanced_skeletal_animation") - if not plugin: - print("错误: 高级骨骼动画插件未加载") - return - - core_system = plugin.get_core_system() - lod_system = plugin.get_lod_system() - - # 设置LOD参数 - from .optimization import LODStrategy - lod_system.set_lod_strategy(LODStrategy.DISTANCE_BASED) - lod_system.set_distance_thresholds([10.0, 25.0, 50.0]) - - # 加载多个Actor - actors_data = [ - {"pos": (-5, -5, 0), "name": "actor_1"}, - {"pos": (5, -5, 0), "name": "actor_2"}, - {"pos": (0, -10, 0), "name": "actor_3"} - ] - - for actor_data in actors_data: - actor, actor_id = core_system.load_actor( - model_path="models/characters/npc.egg", - anim_dict={"idle": "models/anim/idle.egg"}, - actor_id=actor_data["name"], - position=actor_data["pos"] - ) - - if actor: - core_system.play_animation(actor_id, "idle", loop=True) - print(f"加载Actor: {actor_id} 位置: {actor_data['pos']}") - - print("=== LOD系统使用示例结束 ===\n") - - -def example_compression_usage(world): - """ - 动画压缩使用示例 - """ - print("=== 动画压缩使用示例 ===") - - # 获取插件实例 - plugin = world.plugin_manager.get_plugin("advanced_skeletal_animation") - if not plugin: - print("错误: 高级骨骼动画插件未加载") - return - - core_system = plugin.get_core_system() - - # 加载Actor - actor, actor_id = core_system.load_actor( - model_path="models/characters/compress_test.egg", - anim_dict={"walk": "models/anim/walk.egg"}, - position=(20, 0, 0) - ) - - if actor: - # 导入压缩类 - from .optimization import AnimationCompressor, CompressionMethod - compressor = AnimationCompressor() - - # 压缩动画 - compressed_data = compressor.compress_animation( - actor, - "walk", - CompressionMethod.KEYFRAME_REDUCTION - ) - - if compressed_data: - stats = compressor.get_compression_stats(compressed_data) - print(f"动画压缩统计: {stats}") - - print("=== 动画压缩使用示例结束 ===\n") - - -def run_all_examples(world): - """ - 运行所有示例 - """ - print("开始运行高级骨骼动画插件所有示例...") - - example_basic_usage(world) - example_advanced_usage(world) - example_state_machine_usage(world) - example_network_sync_usage(world) - example_lod_usage(world) - example_compression_usage(world) - - print("所有示例运行完成!") - - -# 使用说明和API参考 -USAGE_GUIDE = """ -高级骨骼动画插件使用说明 -===================== - -1. 基本概念 ----------- -- Actor: 带有骨骼的3D模型对象 -- Animation: 应用于Actor的动画序列 -- Core System: 管理所有Actor和动画的中心控制器 - -2. 主要功能模块 --------------- -- Core System: 核心动画管理 -- Network System: 网络同步 -- LOD System: 细节层次管理 -- Cache Manager: 缓存管理 -- State Machine: 动画状态机 -- Curve Editor: 动画曲线编辑器 -- Physics Integration: 物理集成 -- Compressor: 动画压缩 - -3. 核心API ----------- -SkeletalAnimationCore: - - load_actor(): 加载Actor模型 - - unload_actor(): 卸载Actor - - play_animation(): 播放动画 - - stop_animation(): 停止动画 - - set_animation_speed(): 设置动画速度 - - pause_animation(): 暂停动画 - - resume_animation(): 恢复动画 - - blend_animations(): 混合动画 - -4. 网络同步 ----------- -AnimationNetworkSync: - - set_network_mode(): 设置网络模式 - - register_actor_for_sync(): 注册Actor同步 - - send_actor_state(): 发送状态 - - receive_actor_state(): 接收状态 - -5. 高级功能 ----------- -AnimationStateMachine: - - add_state(): 添加状态 - - set_transition(): 设置转换 - - enter_state(): 进入状态 - -AnimationLODSystem: - - set_lod_strategy(): 设置LOD策略 - - update_lod_levels(): 更新LOD级别 - -AnimationCompressor: - - compress_animation(): 压缩动画 - - decompress_animation(): 解压缩动画 - -6. 性能优化 ----------- -- 使用LOD系统根据距离调整动画质量 -- 使用缓存管理器提高加载性能 -- 使用动画压缩减少内存占用 -- 合理设置网络同步频率 - -7. 注意事项 ----------- -- 确保模型文件包含正确的骨骼信息 -- 动画文件需要与模型骨骼结构匹配 -- 插件会在禁用时自动清理所有加载的Actor -- 使用完毕后应调用unload_actor释放资源 -- 大量Actor可能影响性能,注意优化 -""" - -if __name__ == "__main__": +""" +高级骨骼动画系统使用示例 +提供各种使用场景的代码示例 +""" + +def example_basic_usage(world): + """ + 基本使用示例 + """ + print("=== 高级骨骼动画插件基本使用示例 ===") + + # 获取插件实例 + plugin = world.plugin_manager.get_plugin("advanced_skeletal_animation") + if not plugin: + print("错误: 高级骨骼动画插件未加载") + return + + core_system = plugin.get_core_system() + + # 1. 加载简单的Actor + print("1. 加载Actor...") + actor, actor_id = core_system.load_actor( + model_path="models/characters/robot.egg", + position=(0, 0, 0), + scale=1.0 + ) + + if actor: + print(f" 成功加载Actor: {actor_id}") + + # 2. 获取可用动画 + animations = core_system.get_actor_animations(actor_id) + print(f"2. 可用动画: {animations}") + + # 3. 播放动画 + if animations: + print("3. 播放第一个动画...") + core_system.play_animation( + actor_id=actor_id, + anim_name=animations[0], + loop=True, + play_rate=1.0 + ) + + # 4. 获取动画信息 + if animations: + info = core_system.get_animation_info(actor_id, animations[0]) + print(f"4. 动画信息: {info}") + + print("=== 基本使用示例结束 ===\n") + + +def example_advanced_usage(world): + """ + 高级使用示例 + """ + print("=== 高级骨骼动画插件高级使用示例 ===") + + # 获取插件实例 + plugin = world.plugin_manager.get_plugin("advanced_skeletal_animation") + if not plugin: + print("错误: 高级骨骼动画插件未加载") + return + + core_system = plugin.get_core_system() + + # 1. 加载带多个动画的Actor + print("1. 加载带多个动画的Actor...") + anim_dict = { + "walk": "models/anim/walk.egg", + "run": "models/anim/run.egg", + "jump": "models/anim/jump.egg", + "idle": "models/anim/idle.egg" + } + + actor, actor_id = core_system.load_actor( + model_path="models/characters/hero.egg", + anim_dict=anim_dict, + position=(5, 0, 0), + scale=1.2 + ) + + if actor: + print(f" 成功加载Actor: {actor_id}") + + # 2. 播放循环动画 + print("2. 播放循环idle动画...") + core_system.play_animation(actor_id, "idle", loop=True) + + # 3. 3秒后切换到walk动画 + def switch_to_walk(): + print("3. 切换到walk动画...") + core_system.stop_animation(actor_id, "idle") + core_system.play_animation(actor_id, "walk", loop=True) + + # 使用Panda3D的任务系统在3秒后执行 + def schedule_task(task): + if task.time >= 3.0: + switch_to_walk() + return task.done + return task.cont + + world.taskMgr.doMethodLater(3.0, schedule_task, "switch_animation_task") + + # 4. 动画混合示例 + print("4. 创建动画混合...") + core_system.blend_animations( + actor_id, + "walk", + "run", + blend_time=2.0 + ) + + print("=== 高级使用示例结束 ===\n") + + +def example_state_machine_usage(world): + """ + 状态机使用示例 + """ + print("=== 动画状态机使用示例 ===") + + # 获取插件实例 + plugin = world.plugin_manager.get_plugin("advanced_skeletal_animation") + if not plugin: + print("错误: 高级骨骼动画插件未加载") + return + + core_system = plugin.get_core_system() + + # 加载Actor + actor, actor_id = core_system.load_actor( + model_path="models/characters/character.egg", + anim_dict={ + "idle": "models/anim/idle.egg", + "walk": "models/anim/walk.egg", + "run": "models/anim/run.egg" + }, + position=(10, 0, 0) + ) + + if actor: + # 导入状态机类 + from .advanced import AnimationStateMachine + + # 创建状态机 + state_machine = AnimationStateMachine(actor) + + # 添加状态 + state_machine.add_state("idle", "idle", loop=True) + state_machine.add_state("walk", "walk", loop=True) + state_machine.add_state("run", "run", loop=True) + + # 设置转换 + state_machine.set_transition("idle", "walk") + state_machine.set_transition("walk", "run") + state_machine.set_transition("run", "idle") + + # 进入初始状态 + state_machine.enter_state("idle") + + # 添加到更新任务中 + def state_machine_update(task): + dt = globalClock.getDt() + state_machine.update(dt) + return task.cont + + world.taskMgr.add(state_machine_update, f"state_machine_update_{actor_id}") + + print(f"状态机已为Actor {actor_id} 创建并启动") + + print("=== 状态机使用示例结束 ===\n") + + +def example_network_sync_usage(world): + """ + 网络同步使用示例 + """ + print("=== 网络同步使用示例 ===") + + # 获取插件实例 + plugin = world.plugin_manager.get_plugin("advanced_skeletal_animation") + if not plugin: + print("错误: 高级骨骼动画插件未加载") + return + + core_system = plugin.get_core_system() + network_system = plugin.get_network_system() + + # 设置网络模式 + from .network import NetworkMode, SyncMode + network_system.set_network_mode(NetworkMode.SERVER) + network_system.set_sync_mode(SyncMode.STATE_SYNC) + + # 加载Actor并注册网络同步 + actor, actor_id = core_system.load_actor( + model_path="models/characters/player.egg", + anim_dict={ + "idle": "models/anim/idle.egg", + "walk": "models/anim/walk.egg" + }, + position=(15, 0, 0) + ) + + if actor: + network_id = network_system.register_actor_for_sync(actor_id) + print(f"Actor {actor_id} 已注册网络同步 (网络ID: {network_id})") + + # 播放动画 + core_system.play_animation(actor_id, "walk", loop=True) + + print("=== 网络同步使用示例结束 ===\n") + + +def example_lod_usage(world): + """ + LOD系统使用示例 + """ + print("=== LOD系统使用示例 ===") + + # 获取插件实例 + plugin = world.plugin_manager.get_plugin("advanced_skeletal_animation") + if not plugin: + print("错误: 高级骨骼动画插件未加载") + return + + core_system = plugin.get_core_system() + lod_system = plugin.get_lod_system() + + # 设置LOD参数 + from .optimization import LODStrategy + lod_system.set_lod_strategy(LODStrategy.DISTANCE_BASED) + lod_system.set_distance_thresholds([10.0, 25.0, 50.0]) + + # 加载多个Actor + actors_data = [ + {"pos": (-5, -5, 0), "name": "actor_1"}, + {"pos": (5, -5, 0), "name": "actor_2"}, + {"pos": (0, -10, 0), "name": "actor_3"} + ] + + for actor_data in actors_data: + actor, actor_id = core_system.load_actor( + model_path="models/characters/npc.egg", + anim_dict={"idle": "models/anim/idle.egg"}, + actor_id=actor_data["name"], + position=actor_data["pos"] + ) + + if actor: + core_system.play_animation(actor_id, "idle", loop=True) + print(f"加载Actor: {actor_id} 位置: {actor_data['pos']}") + + print("=== LOD系统使用示例结束 ===\n") + + +def example_compression_usage(world): + """ + 动画压缩使用示例 + """ + print("=== 动画压缩使用示例 ===") + + # 获取插件实例 + plugin = world.plugin_manager.get_plugin("advanced_skeletal_animation") + if not plugin: + print("错误: 高级骨骼动画插件未加载") + return + + core_system = plugin.get_core_system() + + # 加载Actor + actor, actor_id = core_system.load_actor( + model_path="models/characters/compress_test.egg", + anim_dict={"walk": "models/anim/walk.egg"}, + position=(20, 0, 0) + ) + + if actor: + # 导入压缩类 + from .optimization import AnimationCompressor, CompressionMethod + compressor = AnimationCompressor() + + # 压缩动画 + compressed_data = compressor.compress_animation( + actor, + "walk", + CompressionMethod.KEYFRAME_REDUCTION + ) + + if compressed_data: + stats = compressor.get_compression_stats(compressed_data) + print(f"动画压缩统计: {stats}") + + print("=== 动画压缩使用示例结束 ===\n") + + +def run_all_examples(world): + """ + 运行所有示例 + """ + print("开始运行高级骨骼动画插件所有示例...") + + example_basic_usage(world) + example_advanced_usage(world) + example_state_machine_usage(world) + example_network_sync_usage(world) + example_lod_usage(world) + example_compression_usage(world) + + print("所有示例运行完成!") + + +# 使用说明和API参考 +USAGE_GUIDE = """ +高级骨骼动画插件使用说明 +===================== + +1. 基本概念 +---------- +- Actor: 带有骨骼的3D模型对象 +- Animation: 应用于Actor的动画序列 +- Core System: 管理所有Actor和动画的中心控制器 + +2. 主要功能模块 +-------------- +- Core System: 核心动画管理 +- Network System: 网络同步 +- LOD System: 细节层次管理 +- Cache Manager: 缓存管理 +- State Machine: 动画状态机 +- Curve Editor: 动画曲线编辑器 +- Physics Integration: 物理集成 +- Compressor: 动画压缩 + +3. 核心API +---------- +SkeletalAnimationCore: + - load_actor(): 加载Actor模型 + - unload_actor(): 卸载Actor + - play_animation(): 播放动画 + - stop_animation(): 停止动画 + - set_animation_speed(): 设置动画速度 + - pause_animation(): 暂停动画 + - resume_animation(): 恢复动画 + - blend_animations(): 混合动画 + +4. 网络同步 +---------- +AnimationNetworkSync: + - set_network_mode(): 设置网络模式 + - register_actor_for_sync(): 注册Actor同步 + - send_actor_state(): 发送状态 + - receive_actor_state(): 接收状态 + +5. 高级功能 +---------- +AnimationStateMachine: + - add_state(): 添加状态 + - set_transition(): 设置转换 + - enter_state(): 进入状态 + +AnimationLODSystem: + - set_lod_strategy(): 设置LOD策略 + - update_lod_levels(): 更新LOD级别 + +AnimationCompressor: + - compress_animation(): 压缩动画 + - decompress_animation(): 解压缩动画 + +6. 性能优化 +---------- +- 使用LOD系统根据距离调整动画质量 +- 使用缓存管理器提高加载性能 +- 使用动画压缩减少内存占用 +- 合理设置网络同步频率 + +7. 注意事项 +---------- +- 确保模型文件包含正确的骨骼信息 +- 动画文件需要与模型骨骼结构匹配 +- 插件会在禁用时自动清理所有加载的Actor +- 使用完毕后应调用unload_actor释放资源 +- 大量Actor可能影响性能,注意优化 +""" + +if __name__ == "__main__": print(USAGE_GUIDE) \ No newline at end of file diff --git a/plugins/user/advanced_skeletal_animation/network.py b/plugins/user/advanced_skeletal_animation/network.py index 29a7c7d2..68e2116e 100644 --- a/plugins/user/advanced_skeletal_animation/network.py +++ b/plugins/user/advanced_skeletal_animation/network.py @@ -1,553 +1,553 @@ -""" -高级骨骼动画系统 - 网络同步模块 -提供动画状态网络同步、多人协作等功能 -""" - -import json -import hashlib -from typing import Dict, List, Optional, Any, Callable -from dataclasses import dataclass, field -from enum import Enum - -from panda3d.core import * -from direct.distributed.ClockDelta import globalClockDelta -from direct.showbase.DirectObject import DirectObject - -# 定义常量 -NETWORK_SETTINGS = { - 'enable_sync': True, - 'sync_interval': 0.1, # 同步间隔(秒) - 'interpolation_mode': 'linear', # 插值模式 - 'extrapolation_limit': 0.5, # 外推限制(秒) - 'compression_enabled': True, - 'delta_compression': True -} - -# 枚举定义 -class NetworkMode(Enum): - """网络模式枚举""" - DISABLED = 0 - SERVER = 1 - CLIENT = 2 - PEER_TO_PEER = 3 - -class SyncMode(Enum): - """同步模式枚举""" - STATE_SYNC = 0 # 状态同步 - INPUT_SYNC = 1 # 输入同步 - INTERPOLATION = 2 # 插值同步 - -@dataclass -class NetworkActorState: - """网络Actor状态数据类""" - actor_id: str - position: Tuple[float, float, float] - rotation: Tuple[float, float, float] - scale: Tuple[float, float, float] - active_animations: Dict[str, Dict[str, Any]] - timestamp: float - network_id: str = "" - checksum: str = "" - -@dataclass -class NetworkSyncConfig: - """网络同步配置数据类""" - mode: NetworkMode = NetworkMode.DISABLED - sync_mode: SyncMode = SyncMode.STATE_SYNC - sync_interval: float = 0.1 - interpolation_mode: str = 'linear' - enable_compression: bool = True - max_buffer_size: int = 100 - update_rate: int = 20 # 每秒更新次数 - -class NetworkSyncBuffer: - """网络同步缓冲区""" - - def __init__(self, max_size: int = 100): - self.max_size = max_size - self.buffer: List[NetworkActorState] = [] - self.last_processed_time = 0.0 - - def add_state(self, state: NetworkActorState) -> None: - """添加状态到缓冲区""" - self.buffer.append(state) - # 保持缓冲区大小 - if len(self.buffer) > self.max_size: - self.buffer.pop(0) - - def get_interpolated_state(self, target_time: float) -> Optional[NetworkActorState]: - """ - 获取插值状态 - :param target_time: 目标时间 - :return: 插值状态 - """ - if len(self.buffer) < 2: - return self.buffer[-1] if self.buffer else None - - # 找到时间范围内的两个状态 - prev_state = None - next_state = None - - for state in self.buffer: - if state.timestamp <= target_time: - prev_state = state - else: - next_state = state - break - - if not prev_state or not next_state: - return self.buffer[-1] if self.buffer else None - - # 计算插值因子 - time_range = next_state.timestamp - prev_state.timestamp - if time_range <= 0: - return prev_state - - t = (target_time - prev_state.timestamp) / time_range - - # 执行线性插值 - interpolated_pos = tuple( - prev_state.position[i] + t * (next_state.position[i] - prev_state.position[i]) - for i in range(3) - ) - - interpolated_rot = tuple( - prev_state.rotation[i] + t * (next_state.rotation[i] - prev_state.rotation[i]) - for i in range(3) - ) - - interpolated_scale = tuple( - prev_state.scale[i] + t * (next_state.scale[i] - prev_state.scale[i]) - for i in range(3) - ) - - # 创建插值状态 - interpolated_state = NetworkActorState( - actor_id=prev_state.actor_id, - position=interpolated_pos, - rotation=interpolated_rot, - scale=interpolated_scale, - active_animations=prev_state.active_animations, # 简化处理 - timestamp=target_time, - network_id=prev_state.network_id - ) - - return interpolated_state - - def clear(self) -> None: - """清空缓冲区""" - self.buffer.clear() - -class AnimationNetworkSync(DirectObject): - """ - 动画网络同步系统 - 支持多人环境中动画状态的同步 - """ - - def __init__(self, world, core_system): - """ - 初始化网络同步系统 - :param world: 世界对象 - :param core_system: 核心动画系统 - """ - super().__init__() - self.world = world - self.core_system = core_system - self.config = NetworkSyncConfig() - self.network_states: Dict[str, NetworkActorState] = {} - self.state_buffers: Dict[str, NetworkSyncBuffer] = {} - self.sync_callbacks: Dict[str, List[Callable]] = {} - self.is_connected = False - self.last_sync_time = 0.0 - self.sync_task = None - self.network_id_counter = 0 - - print("动画网络同步系统初始化完成") - - def set_network_mode(self, mode: NetworkMode) -> None: - """ - 设置网络模式 - :param mode: 网络模式 - """ - self.config.mode = mode - if mode != NetworkMode.DISABLED: - self.is_connected = True - self.start_sync_task() - else: - self.is_connected = False - self.stop_sync_task() - print(f"网络模式设置为: {mode.name}") - - def set_sync_mode(self, mode: SyncMode) -> None: - """ - 设置同步模式 - :param mode: 同步模式 - """ - self.config.sync_mode = mode - print(f"同步模式设置为: {mode.name}") - - def configure_sync(self, sync_interval: float = 0.1, interpolation_mode: str = 'linear', - enable_compression: bool = True) -> None: - """ - 配置同步参数 - :param sync_interval: 同步间隔 - :param interpolation_mode: 插值模式 - :param enable_compression: 是否启用压缩 - """ - self.config.sync_interval = sync_interval - self.config.interpolation_mode = interpolation_mode - self.config.enable_compression = enable_compression - print(f"同步配置更新: 间隔={sync_interval}s, 插值={interpolation_mode}, 压缩={enable_compression}") - - def register_actor_for_sync(self, actor_id: str, network_id: Optional[str] = None) -> str: - """ - 注册Actor以进行网络同步 - :param actor_id: Actor ID - :param network_id: 网络ID,如果为None则自动生成 - :return: 网络ID - """ - if network_id is None: - network_id = f"net_{self.network_id_counter}" - self.network_id_counter += 1 - - # 初始化状态缓冲区 - self.state_buffers[actor_id] = NetworkSyncBuffer(self.config.max_buffer_size) - - print(f"Actor {actor_id} 已注册网络同步 (网络ID: {network_id})") - return network_id - - def unregister_actor_from_sync(self, actor_id: str) -> None: - """ - 取消Actor的网络同步注册 - :param actor_id: Actor ID - """ - if actor_id in self.state_buffers: - self.state_buffers[actor_id].clear() - del self.state_buffers[actor_id] - - if actor_id in self.network_states: - del self.network_states[actor_id] - - print(f"Actor {actor_id} 已取消网络同步注册") - - def _capture_actor_state(self, actor_id: str) -> Optional[NetworkActorState]: - """ - 捕获Actor当前状态 - :param actor_id: Actor ID - :return: 状态数据 - """ - actor_info = self.core_system.actors.get(actor_id) - if not actor_info: - return None - - actor = actor_info.actor - - # 获取基本变换 - position = actor.getPos().toTuple() - rotation = actor.getHpr().toTuple() - scale = actor.getScale().toTuple() - - # 获取活动动画状态 - active_animations = {} - for anim_name in actor_info.active_animations: - if anim_name in actor_info.animations: - anim_info = actor_info.animations[anim_name] - control = actor.getAnimControl(anim_name) if anim_name in actor.getAnimNames() else None - - active_animations[anim_name] = { - 'frame': anim_info.current_frame, - 'play_rate': anim_info.play_rate, - 'loop': anim_info.loop, - 'state': anim_info.state.name, - 'blend_weight': anim_info.blend_weight - } - - # 创建状态对象 - state = NetworkActorState( - actor_id=actor_id, - position=position, - rotation=rotation, - scale=scale, - active_animations=active_animations, - timestamp=globalClock.getRealTime() - ) - - # 计算校验和 - state.checksum = self._calculate_checksum(state) - - return state - - def _calculate_checksum(self, state: NetworkActorState) -> str: - """ - 计算状态校验和 - :param state: 状态数据 - :return: 校验和 - """ - # 创建用于校验的数据 - checksum_data = { - 'position': state.position, - 'rotation': state.rotation, - 'scale': state.scale, - 'active_animations': state.active_animations, - 'timestamp': state.timestamp - } - - # 生成JSON字符串并计算MD5 - data_str = json.dumps(checksum_data, sort_keys=True) - return hashlib.md5(data_str.encode()).hexdigest() - - def _validate_checksum(self, state: NetworkActorState) -> bool: - """ - 验证状态校验和 - :param state: 状态数据 - :return: 是否有效 - """ - expected_checksum = state.checksum - state.checksum = "" # 临时清空校验和 - actual_checksum = self._calculate_checksum(state) - state.checksum = expected_checksum # 恢复校验和 - return expected_checksum == actual_checksum - - def send_actor_state(self, actor_id: str) -> None: - """ - 发送Actor状态(服务器模式) - :param actor_id: Actor ID - """ - if not self.is_connected or self.config.mode != NetworkMode.SERVER: - return - - # 捕获当前状态 - state = self._capture_actor_state(actor_id) - if not state: - return - - # 发送状态(在实际实现中,这里会通过网络发送) - self._broadcast_actor_state(state) - - def _broadcast_actor_state(self, state: NetworkActorState) -> None: - """ - 广播Actor状态 - :param state: 状态数据 - """ - # 在实际实现中,这里会通过网络发送数据 - # 为简化,我们直接更新本地网络状态 - self.network_states[state.actor_id] = state - - # 添加到缓冲区用于插值 - if state.actor_id in self.state_buffers: - self.state_buffers[state.actor_id].add_state(state) - - print(f"广播Actor {state.actor_id} 状态更新 (时间: {state.timestamp:.3f})") - - def receive_actor_state(self, state_data: Dict[str, Any]) -> None: - """ - 接收Actor状态(客户端模式) - :param state_data: 状态数据 - """ - if not self.is_connected or self.config.mode != NetworkMode.CLIENT: - return - - # 反序列化状态 - state = NetworkActorState( - actor_id=state_data['actor_id'], - position=tuple(state_data['position']), - rotation=tuple(state_data['rotation']), - scale=tuple(state_data['scale']), - active_animations=state_data['active_animations'], - timestamp=state_data['timestamp'], - network_id=state_data.get('network_id', ''), - checksum=state_data.get('checksum', '') - ) - - # 验证校验和 - if not self._validate_checksum(state): - print(f"警告: Actor {state.actor_id} 状态校验和不匹配,丢弃数据") - return - - # 存储接收到的状态 - self.network_states[state.actor_id] = state - - # 添加到缓冲区用于插值 - if state.actor_id in self.state_buffers: - self.state_buffers[state.actor_id].add_state(state) - - print(f"接收Actor {state.actor_id} 状态更新 (时间: {state.timestamp:.3f})") - - def _apply_actor_state(self, actor_id: str, state: NetworkActorState) -> None: - """ - 应用Actor状态 - :param actor_id: Actor ID - :param state: 状态数据 - """ - actor_info = self.core_system.actors.get(actor_id) - if not actor_info: - return - - actor = actor_info.actor - - # 应用变换 - actor.setPos(*state.position) - actor.setHpr(*state.rotation) - actor.setScale(*state.scale) - - # 应用动画状态 - for anim_name, anim_state in state.active_animations.items(): - if anim_name in actor.getAnimNames(): - # 设置动画帧 - if 'frame' in anim_state: - actor.pose(anim_name, anim_state['frame']) - - # 设置播放速度 - if 'play_rate' in anim_state: - actor.setPlayRate(anim_state['play_rate'], anim_name) - - # 恢复播放状态 - if anim_state.get('state') in ['PLAYING', 'LOOPING']: - if anim_state.get('loop', False): - actor.loop(anim_name, restart=False) - else: - actor.play(anim_name) - - print(f"应用Actor {actor_id} 网络状态 (时间: {state.timestamp:.3f})") - - def _apply_interpolated_state(self, actor_id: str, target_time: float) -> None: - """ - 应用插值状态 - :param actor_id: Actor ID - :param target_time: 目标时间 - """ - if actor_id not in self.state_buffers: - return - - # 获取插值状态 - interpolated_state = self.state_buffers[actor_id].get_interpolated_state(target_time) - if interpolated_state: - self._apply_actor_state(actor_id, interpolated_state) - - def update_network_sync(self, task) -> int: - """ - 更新网络同步 - :param task: 任务对象 - """ - current_time = globalClock.getRealTime() - - # 检查同步间隔 - if current_time - self.last_sync_time < self.config.sync_interval: - return task.cont - - self.last_sync_time = current_time - - if self.config.mode == NetworkMode.SERVER: - # 服务器:发送本地Actor状态 - for actor_id in self.core_system.actors.keys(): - self.send_actor_state(actor_id) - - elif self.config.mode == NetworkMode.CLIENT: - # 客户端:应用接收到的状态 - current_network_time = globalClock.getRealTime() - - # 根据同步模式应用状态 - if self.config.sync_mode == SyncMode.INTERPOLATION: - # 使用插值 - for actor_id in self.network_states.keys(): - self._apply_interpolated_state(actor_id, current_network_time) - else: - # 直接应用最新状态 - for actor_id, state in self.network_states.items(): - self._apply_actor_state(actor_id, state) - - return task.cont - - def start_sync_task(self) -> None: - """ - 启动同步任务 - """ - if self.sync_task is None: - self.sync_task = self.world.taskMgr.add( - self.update_network_sync, - "animationNetworkSyncTask", - sort=40 # 确保在网络更新之后执行 - ) - print("动画网络同步任务已启动") - - def stop_sync_task(self) -> None: - """ - 停止同步任务 - """ - if self.sync_task: - self.world.taskMgr.remove(self.sync_task) - self.sync_task = None - print("动画网络同步任务已停止") - - def get_network_stats(self) -> Dict[str, Any]: - """ - 获取网络同步统计信息 - :return: 统计信息 - """ - return { - 'mode': self.config.mode.name, - 'sync_mode': self.config.sync_mode.name, - 'connected': self.is_connected, - 'sync_interval': self.config.sync_interval, - 'interpolation_mode': self.config.interpolation_mode, - 'compression_enabled': self.config.enable_compression, - 'network_actors': len(self.network_states), - 'buffered_states': sum(len(buffer.buffer) for buffer in self.state_buffers.values()) - } - - def add_sync_callback(self, actor_id: str, callback: Callable) -> None: - """ - 添加同步回调 - :param actor_id: Actor ID - :param callback: 回调函数 - """ - if actor_id not in self.sync_callbacks: - self.sync_callbacks[actor_id] = [] - self.sync_callbacks[actor_id].append(callback) - - def remove_sync_callback(self, actor_id: str, callback: Callable) -> None: - """ - 移除同步回调 - :param actor_id: Actor ID - :param callback: 回调函数 - """ - if actor_id in self.sync_callbacks and callback in self.sync_callbacks[actor_id]: - self.sync_callbacks[actor_id].remove(callback) - - def _trigger_sync_callbacks(self, actor_id: str, state: NetworkActorState) -> None: - """ - 触发同步回调 - :param actor_id: Actor ID - :param state: 状态数据 - """ - if actor_id in self.sync_callbacks: - for callback in self.sync_callbacks[actor_id]: - try: - callback(actor_id, state) - except Exception as e: - print(f"执行同步回调失败: {e}") - -# 使用示例和测试代码 -def example_network_sync_usage(world, core_system): - """ - 网络同步使用示例 - """ - print("=== 动画网络同步使用示例 ===") - - # 创建网络同步系统 - network_sync = AnimationNetworkSync(world, core_system) - - # 设置为服务器模式 - network_sync.set_network_mode(NetworkMode.SERVER) - network_sync.set_sync_mode(SyncMode.STATE_SYNC) - network_sync.configure_sync(sync_interval=0.05, interpolation_mode='linear') - - # 获取网络统计 - stats = network_sync.get_network_stats() - print(f"网络统计: {stats}") - - print("网络同步示例完成") - return network_sync - -if __name__ == "__main__": +""" +高级骨骼动画系统 - 网络同步模块 +提供动画状态网络同步、多人协作等功能 +""" + +import json +import hashlib +from typing import Dict, List, Optional, Any, Callable +from dataclasses import dataclass, field +from enum import Enum + +from panda3d.core import * +from direct.distributed.ClockDelta import globalClockDelta +from direct.showbase.DirectObject import DirectObject + +# 定义常量 +NETWORK_SETTINGS = { + 'enable_sync': True, + 'sync_interval': 0.1, # 同步间隔(秒) + 'interpolation_mode': 'linear', # 插值模式 + 'extrapolation_limit': 0.5, # 外推限制(秒) + 'compression_enabled': True, + 'delta_compression': True +} + +# 枚举定义 +class NetworkMode(Enum): + """网络模式枚举""" + DISABLED = 0 + SERVER = 1 + CLIENT = 2 + PEER_TO_PEER = 3 + +class SyncMode(Enum): + """同步模式枚举""" + STATE_SYNC = 0 # 状态同步 + INPUT_SYNC = 1 # 输入同步 + INTERPOLATION = 2 # 插值同步 + +@dataclass +class NetworkActorState: + """网络Actor状态数据类""" + actor_id: str + position: Tuple[float, float, float] + rotation: Tuple[float, float, float] + scale: Tuple[float, float, float] + active_animations: Dict[str, Dict[str, Any]] + timestamp: float + network_id: str = "" + checksum: str = "" + +@dataclass +class NetworkSyncConfig: + """网络同步配置数据类""" + mode: NetworkMode = NetworkMode.DISABLED + sync_mode: SyncMode = SyncMode.STATE_SYNC + sync_interval: float = 0.1 + interpolation_mode: str = 'linear' + enable_compression: bool = True + max_buffer_size: int = 100 + update_rate: int = 20 # 每秒更新次数 + +class NetworkSyncBuffer: + """网络同步缓冲区""" + + def __init__(self, max_size: int = 100): + self.max_size = max_size + self.buffer: List[NetworkActorState] = [] + self.last_processed_time = 0.0 + + def add_state(self, state: NetworkActorState) -> None: + """添加状态到缓冲区""" + self.buffer.append(state) + # 保持缓冲区大小 + if len(self.buffer) > self.max_size: + self.buffer.pop(0) + + def get_interpolated_state(self, target_time: float) -> Optional[NetworkActorState]: + """ + 获取插值状态 + :param target_time: 目标时间 + :return: 插值状态 + """ + if len(self.buffer) < 2: + return self.buffer[-1] if self.buffer else None + + # 找到时间范围内的两个状态 + prev_state = None + next_state = None + + for state in self.buffer: + if state.timestamp <= target_time: + prev_state = state + else: + next_state = state + break + + if not prev_state or not next_state: + return self.buffer[-1] if self.buffer else None + + # 计算插值因子 + time_range = next_state.timestamp - prev_state.timestamp + if time_range <= 0: + return prev_state + + t = (target_time - prev_state.timestamp) / time_range + + # 执行线性插值 + interpolated_pos = tuple( + prev_state.position[i] + t * (next_state.position[i] - prev_state.position[i]) + for i in range(3) + ) + + interpolated_rot = tuple( + prev_state.rotation[i] + t * (next_state.rotation[i] - prev_state.rotation[i]) + for i in range(3) + ) + + interpolated_scale = tuple( + prev_state.scale[i] + t * (next_state.scale[i] - prev_state.scale[i]) + for i in range(3) + ) + + # 创建插值状态 + interpolated_state = NetworkActorState( + actor_id=prev_state.actor_id, + position=interpolated_pos, + rotation=interpolated_rot, + scale=interpolated_scale, + active_animations=prev_state.active_animations, # 简化处理 + timestamp=target_time, + network_id=prev_state.network_id + ) + + return interpolated_state + + def clear(self) -> None: + """清空缓冲区""" + self.buffer.clear() + +class AnimationNetworkSync(DirectObject): + """ + 动画网络同步系统 + 支持多人环境中动画状态的同步 + """ + + def __init__(self, world, core_system): + """ + 初始化网络同步系统 + :param world: 世界对象 + :param core_system: 核心动画系统 + """ + super().__init__() + self.world = world + self.core_system = core_system + self.config = NetworkSyncConfig() + self.network_states: Dict[str, NetworkActorState] = {} + self.state_buffers: Dict[str, NetworkSyncBuffer] = {} + self.sync_callbacks: Dict[str, List[Callable]] = {} + self.is_connected = False + self.last_sync_time = 0.0 + self.sync_task = None + self.network_id_counter = 0 + + print("动画网络同步系统初始化完成") + + def set_network_mode(self, mode: NetworkMode) -> None: + """ + 设置网络模式 + :param mode: 网络模式 + """ + self.config.mode = mode + if mode != NetworkMode.DISABLED: + self.is_connected = True + self.start_sync_task() + else: + self.is_connected = False + self.stop_sync_task() + print(f"网络模式设置为: {mode.name}") + + def set_sync_mode(self, mode: SyncMode) -> None: + """ + 设置同步模式 + :param mode: 同步模式 + """ + self.config.sync_mode = mode + print(f"同步模式设置为: {mode.name}") + + def configure_sync(self, sync_interval: float = 0.1, interpolation_mode: str = 'linear', + enable_compression: bool = True) -> None: + """ + 配置同步参数 + :param sync_interval: 同步间隔 + :param interpolation_mode: 插值模式 + :param enable_compression: 是否启用压缩 + """ + self.config.sync_interval = sync_interval + self.config.interpolation_mode = interpolation_mode + self.config.enable_compression = enable_compression + print(f"同步配置更新: 间隔={sync_interval}s, 插值={interpolation_mode}, 压缩={enable_compression}") + + def register_actor_for_sync(self, actor_id: str, network_id: Optional[str] = None) -> str: + """ + 注册Actor以进行网络同步 + :param actor_id: Actor ID + :param network_id: 网络ID,如果为None则自动生成 + :return: 网络ID + """ + if network_id is None: + network_id = f"net_{self.network_id_counter}" + self.network_id_counter += 1 + + # 初始化状态缓冲区 + self.state_buffers[actor_id] = NetworkSyncBuffer(self.config.max_buffer_size) + + print(f"Actor {actor_id} 已注册网络同步 (网络ID: {network_id})") + return network_id + + def unregister_actor_from_sync(self, actor_id: str) -> None: + """ + 取消Actor的网络同步注册 + :param actor_id: Actor ID + """ + if actor_id in self.state_buffers: + self.state_buffers[actor_id].clear() + del self.state_buffers[actor_id] + + if actor_id in self.network_states: + del self.network_states[actor_id] + + print(f"Actor {actor_id} 已取消网络同步注册") + + def _capture_actor_state(self, actor_id: str) -> Optional[NetworkActorState]: + """ + 捕获Actor当前状态 + :param actor_id: Actor ID + :return: 状态数据 + """ + actor_info = self.core_system.actors.get(actor_id) + if not actor_info: + return None + + actor = actor_info.actor + + # 获取基本变换 + position = actor.getPos().toTuple() + rotation = actor.getHpr().toTuple() + scale = actor.getScale().toTuple() + + # 获取活动动画状态 + active_animations = {} + for anim_name in actor_info.active_animations: + if anim_name in actor_info.animations: + anim_info = actor_info.animations[anim_name] + control = actor.getAnimControl(anim_name) if anim_name in actor.getAnimNames() else None + + active_animations[anim_name] = { + 'frame': anim_info.current_frame, + 'play_rate': anim_info.play_rate, + 'loop': anim_info.loop, + 'state': anim_info.state.name, + 'blend_weight': anim_info.blend_weight + } + + # 创建状态对象 + state = NetworkActorState( + actor_id=actor_id, + position=position, + rotation=rotation, + scale=scale, + active_animations=active_animations, + timestamp=globalClock.getRealTime() + ) + + # 计算校验和 + state.checksum = self._calculate_checksum(state) + + return state + + def _calculate_checksum(self, state: NetworkActorState) -> str: + """ + 计算状态校验和 + :param state: 状态数据 + :return: 校验和 + """ + # 创建用于校验的数据 + checksum_data = { + 'position': state.position, + 'rotation': state.rotation, + 'scale': state.scale, + 'active_animations': state.active_animations, + 'timestamp': state.timestamp + } + + # 生成JSON字符串并计算MD5 + data_str = json.dumps(checksum_data, sort_keys=True) + return hashlib.md5(data_str.encode()).hexdigest() + + def _validate_checksum(self, state: NetworkActorState) -> bool: + """ + 验证状态校验和 + :param state: 状态数据 + :return: 是否有效 + """ + expected_checksum = state.checksum + state.checksum = "" # 临时清空校验和 + actual_checksum = self._calculate_checksum(state) + state.checksum = expected_checksum # 恢复校验和 + return expected_checksum == actual_checksum + + def send_actor_state(self, actor_id: str) -> None: + """ + 发送Actor状态(服务器模式) + :param actor_id: Actor ID + """ + if not self.is_connected or self.config.mode != NetworkMode.SERVER: + return + + # 捕获当前状态 + state = self._capture_actor_state(actor_id) + if not state: + return + + # 发送状态(在实际实现中,这里会通过网络发送) + self._broadcast_actor_state(state) + + def _broadcast_actor_state(self, state: NetworkActorState) -> None: + """ + 广播Actor状态 + :param state: 状态数据 + """ + # 在实际实现中,这里会通过网络发送数据 + # 为简化,我们直接更新本地网络状态 + self.network_states[state.actor_id] = state + + # 添加到缓冲区用于插值 + if state.actor_id in self.state_buffers: + self.state_buffers[state.actor_id].add_state(state) + + print(f"广播Actor {state.actor_id} 状态更新 (时间: {state.timestamp:.3f})") + + def receive_actor_state(self, state_data: Dict[str, Any]) -> None: + """ + 接收Actor状态(客户端模式) + :param state_data: 状态数据 + """ + if not self.is_connected or self.config.mode != NetworkMode.CLIENT: + return + + # 反序列化状态 + state = NetworkActorState( + actor_id=state_data['actor_id'], + position=tuple(state_data['position']), + rotation=tuple(state_data['rotation']), + scale=tuple(state_data['scale']), + active_animations=state_data['active_animations'], + timestamp=state_data['timestamp'], + network_id=state_data.get('network_id', ''), + checksum=state_data.get('checksum', '') + ) + + # 验证校验和 + if not self._validate_checksum(state): + print(f"警告: Actor {state.actor_id} 状态校验和不匹配,丢弃数据") + return + + # 存储接收到的状态 + self.network_states[state.actor_id] = state + + # 添加到缓冲区用于插值 + if state.actor_id in self.state_buffers: + self.state_buffers[state.actor_id].add_state(state) + + print(f"接收Actor {state.actor_id} 状态更新 (时间: {state.timestamp:.3f})") + + def _apply_actor_state(self, actor_id: str, state: NetworkActorState) -> None: + """ + 应用Actor状态 + :param actor_id: Actor ID + :param state: 状态数据 + """ + actor_info = self.core_system.actors.get(actor_id) + if not actor_info: + return + + actor = actor_info.actor + + # 应用变换 + actor.setPos(*state.position) + actor.setHpr(*state.rotation) + actor.setScale(*state.scale) + + # 应用动画状态 + for anim_name, anim_state in state.active_animations.items(): + if anim_name in actor.getAnimNames(): + # 设置动画帧 + if 'frame' in anim_state: + actor.pose(anim_name, anim_state['frame']) + + # 设置播放速度 + if 'play_rate' in anim_state: + actor.setPlayRate(anim_state['play_rate'], anim_name) + + # 恢复播放状态 + if anim_state.get('state') in ['PLAYING', 'LOOPING']: + if anim_state.get('loop', False): + actor.loop(anim_name, restart=False) + else: + actor.play(anim_name) + + print(f"应用Actor {actor_id} 网络状态 (时间: {state.timestamp:.3f})") + + def _apply_interpolated_state(self, actor_id: str, target_time: float) -> None: + """ + 应用插值状态 + :param actor_id: Actor ID + :param target_time: 目标时间 + """ + if actor_id not in self.state_buffers: + return + + # 获取插值状态 + interpolated_state = self.state_buffers[actor_id].get_interpolated_state(target_time) + if interpolated_state: + self._apply_actor_state(actor_id, interpolated_state) + + def update_network_sync(self, task) -> int: + """ + 更新网络同步 + :param task: 任务对象 + """ + current_time = globalClock.getRealTime() + + # 检查同步间隔 + if current_time - self.last_sync_time < self.config.sync_interval: + return task.cont + + self.last_sync_time = current_time + + if self.config.mode == NetworkMode.SERVER: + # 服务器:发送本地Actor状态 + for actor_id in self.core_system.actors.keys(): + self.send_actor_state(actor_id) + + elif self.config.mode == NetworkMode.CLIENT: + # 客户端:应用接收到的状态 + current_network_time = globalClock.getRealTime() + + # 根据同步模式应用状态 + if self.config.sync_mode == SyncMode.INTERPOLATION: + # 使用插值 + for actor_id in self.network_states.keys(): + self._apply_interpolated_state(actor_id, current_network_time) + else: + # 直接应用最新状态 + for actor_id, state in self.network_states.items(): + self._apply_actor_state(actor_id, state) + + return task.cont + + def start_sync_task(self) -> None: + """ + 启动同步任务 + """ + if self.sync_task is None: + self.sync_task = self.world.taskMgr.add( + self.update_network_sync, + "animationNetworkSyncTask", + sort=40 # 确保在网络更新之后执行 + ) + print("动画网络同步任务已启动") + + def stop_sync_task(self) -> None: + """ + 停止同步任务 + """ + if self.sync_task: + self.world.taskMgr.remove(self.sync_task) + self.sync_task = None + print("动画网络同步任务已停止") + + def get_network_stats(self) -> Dict[str, Any]: + """ + 获取网络同步统计信息 + :return: 统计信息 + """ + return { + 'mode': self.config.mode.name, + 'sync_mode': self.config.sync_mode.name, + 'connected': self.is_connected, + 'sync_interval': self.config.sync_interval, + 'interpolation_mode': self.config.interpolation_mode, + 'compression_enabled': self.config.enable_compression, + 'network_actors': len(self.network_states), + 'buffered_states': sum(len(buffer.buffer) for buffer in self.state_buffers.values()) + } + + def add_sync_callback(self, actor_id: str, callback: Callable) -> None: + """ + 添加同步回调 + :param actor_id: Actor ID + :param callback: 回调函数 + """ + if actor_id not in self.sync_callbacks: + self.sync_callbacks[actor_id] = [] + self.sync_callbacks[actor_id].append(callback) + + def remove_sync_callback(self, actor_id: str, callback: Callable) -> None: + """ + 移除同步回调 + :param actor_id: Actor ID + :param callback: 回调函数 + """ + if actor_id in self.sync_callbacks and callback in self.sync_callbacks[actor_id]: + self.sync_callbacks[actor_id].remove(callback) + + def _trigger_sync_callbacks(self, actor_id: str, state: NetworkActorState) -> None: + """ + 触发同步回调 + :param actor_id: Actor ID + :param state: 状态数据 + """ + if actor_id in self.sync_callbacks: + for callback in self.sync_callbacks[actor_id]: + try: + callback(actor_id, state) + except Exception as e: + print(f"执行同步回调失败: {e}") + +# 使用示例和测试代码 +def example_network_sync_usage(world, core_system): + """ + 网络同步使用示例 + """ + print("=== 动画网络同步使用示例 ===") + + # 创建网络同步系统 + network_sync = AnimationNetworkSync(world, core_system) + + # 设置为服务器模式 + network_sync.set_network_mode(NetworkMode.SERVER) + network_sync.set_sync_mode(SyncMode.STATE_SYNC) + network_sync.configure_sync(sync_interval=0.05, interpolation_mode='linear') + + # 获取网络统计 + stats = network_sync.get_network_stats() + print(f"网络统计: {stats}") + + print("网络同步示例完成") + return network_sync + +if __name__ == "__main__": print("动画网络同步模块加载完成") \ No newline at end of file diff --git a/plugins/user/advanced_skeletal_animation/optimization.py b/plugins/user/advanced_skeletal_animation/optimization.py index c6d35b23..98a07d4e 100644 --- a/plugins/user/advanced_skeletal_animation/optimization.py +++ b/plugins/user/advanced_skeletal_animation/optimization.py @@ -1,763 +1,763 @@ -""" -高级骨骼动画系统 - 优化和压缩模块 -提供动画压缩、LOD、缓存优化等功能 -""" - -import math -import json -import os -from typing import Dict, List, Optional, Tuple, Any, Callable -from dataclasses import dataclass, field -from enum import Enum - -from panda3d.core import * -from direct.actor.Actor import Actor - -# 定义常量 -OPTIMIZATION_SETTINGS = { - 'compression_threshold': 0.001, - 'lod_distance_thresholds': [10.0, 25.0, 50.0], - 'lod_frame_rates': [1.0, 0.5, 0.25, 0.1], - 'cache_max_size': 500, - 'cache_cleanup_interval': 30.0 -} - -# 枚举定义 -class CompressionMethod(Enum): - """压缩方法枚举""" - KEYFRAME_REDUCTION = 0 - QUANTIZATION = 1 - DELTA_COMPRESSION = 2 - RUN_LENGTH_ENCODING = 3 - -class LODStrategy(Enum): - """LOD策略枚举""" - DISTANCE_BASED = 0 - PERFORMANCE_BASED = 1 - HYBRID = 2 - -class CachePolicy(Enum): - """缓存策略枚举""" - LRU = 0 # 最近最少使用 - LFU = 1 # 最不经常使用 - FIFO = 2 # 先进先出 - -@dataclass -class CompressedAnimationData: - """压缩动画数据类""" - name: str - frame_rate: float - duration: float - bone_data: Dict[str, List[Tuple[float, Tuple[float, float, float, float, float, float, float]]]] # 时间, (位置+旋转+缩放) - compression_ratio: float = 1.0 - original_size: int = 0 - compressed_size: int = 0 - -@dataclass -class LODSettings: - """LOD设置数据类""" - strategy: LODStrategy - distance_thresholds: List[float] - frame_rates: List[float] - enable_auto_lod: bool = True - performance_threshold: float = 30.0 # FPS阈值 - -@dataclass -class CacheEntry: - """缓存条目数据类""" - key: str - data: Any - timestamp: float - access_count: int = 1 - size: int = 0 - -class AnimationCompressor: - """ - 动画压缩系统 - 支持多种压缩算法 - """ - - def __init__(self): - """初始化动画压缩系统""" - self.settings = { - 'position_threshold': 0.001, - 'rotation_threshold': 0.01, - 'scale_threshold': 0.001, - 'quantization_bits': 16, - 'enable_quantization': True - } - - print("动画压缩系统初始化完成") - - def compress_animation(self, actor: Actor, anim_name: str, - method: CompressionMethod = CompressionMethod.KEYFRAME_REDUCTION) -> Optional[CompressedAnimationData]: - """ - 压缩指定动画 - :param actor: Actor对象 - :param anim_name: 动画名称 - :param method: 压缩方法 - :return: 压缩后的动画数据 - """ - if anim_name not in actor.getAnimNames(): - print(f"错误: 动画 {anim_name} 不存在") - return None - - print(f"开始压缩动画: {anim_name} (方法: {method.name})") - - # 获取动画控制 - anim_control = actor.getAnimControl(anim_name) - if not anim_control: - print(f"错误: 无法获取动画控制 {anim_name}") - return None - - # 获取动画数据 - num_frames = anim_control.getNumFrames() - frame_rate = anim_control.getFrameRate() - duration = anim_control.getDuration() - - print(f" 动画信息: {num_frames} 帧, {frame_rate} FPS, {duration:.2f} 秒") - - # 提取骨骼动画数据 - bone_data = self._extract_bone_animation_data(actor, anim_name, num_frames) - - # 根据方法进行压缩 - if method == CompressionMethod.KEYFRAME_REDUCTION: - compressed_bone_data = self._compress_keyframe_reduction(bone_data) - elif method == CompressionMethod.QUANTIZATION: - compressed_bone_data = self._compress_quantization(bone_data) - elif method == CompressionMethod.DELTA_COMPRESSION: - compressed_bone_data = self._compress_delta(bone_data) - elif method == CompressionMethod.RUN_LENGTH_ENCODING: - compressed_bone_data = self._compress_rle(bone_data) - else: - compressed_bone_data = bone_data - - # 计算压缩比 - original_size = self._calculate_data_size(bone_data) - compressed_size = self._calculate_data_size(compressed_bone_data) - compression_ratio = original_size / compressed_size if compressed_size > 0 else 1.0 - - # 创建压缩数据 - compressed_data = CompressedAnimationData( - name=anim_name, - frame_rate=frame_rate, - duration=duration, - bone_data=compressed_bone_data, - compression_ratio=compression_ratio, - original_size=original_size, - compressed_size=compressed_size - ) - - print(f" 压缩完成: 原始大小={original_size}, 压缩大小={compressed_size}, 压缩比={compression_ratio:.2f}:1") - return compressed_data - - def _extract_bone_animation_data(self, actor: Actor, anim_name: str, num_frames: int) -> Dict[str, List]: - """ - 提取骨骼动画数据 - :param actor: Actor对象 - :param anim_name: 动画名称 - :param num_frames: 帧数 - :return: 骨骼动画数据 - """ - bone_data = {} - - # 获取所有骨骼 - bundle = actor.getPartBundleDict().get('modelRoot') - if not bundle: - return bone_data - - # 遍历骨骼 - for i in range(bundle.getNumChildren()): - child = bundle.getChild(i) - if hasattr(child, 'getName'): - bone_name = child.getName() - bone_data[bone_name] = self._extract_bone_frames(actor, bone_name, anim_name, num_frames) - - return bone_data - - def _extract_bone_frames(self, actor: Actor, bone_name: str, anim_name: str, num_frames: int) -> List: - """ - 提取单个骨骼的帧数据 - :param actor: Actor对象 - :param bone_name: 骨骼名称 - :param anim_name: 动画名称 - :param num_frames: 帧数 - :return: 帧数据列表 - """ - frames = [] - - # 为每一帧提取数据 - for frame in range(num_frames): - # 设置到指定帧 - actor.pose(anim_name, frame) - - # 获取骨骼变换 - joint = actor.exposeJoint(None, "modelRoot", bone_name) - if joint: - pos = joint.getPos() - hpr = joint.getHpr() - scale = joint.getScale() - - # 存储为 (时间, (px, py, pz, rx, ry, rz, sx, sy, sz)) - time = frame / actor.getFrameRate(anim_name) - frame_data = ( - time, - (pos.x, pos.y, pos.z, - hpr.x, hpr.y, hpr.z, - scale.x, scale.y, scale.z) - ) - frames.append(frame_data) - - return frames - - def _compress_keyframe_reduction(self, bone_data: Dict[str, List]) -> Dict[str, List]: - """ - 关键帧简化压缩 - :param bone_data: 骨骼数据 - :return: 压缩后的骨骼数据 - """ - compressed_data = {} - - for bone_name, frames in bone_data.items(): - if len(frames) <= 2: - compressed_data[bone_name] = frames - continue - - compressed_frames = [frames[0]] # 总是保留第一帧 - last_frame = frames[0] - - for i in range(1, len(frames) - 1): - current_frame = frames[i] - next_frame = frames[i + 1] - - # 检查当前帧是否可以被简化 - if not self._is_frame_necessary(last_frame, current_frame, next_frame): - continue - - compressed_frames.append(current_frame) - last_frame = current_frame - - compressed_frames.append(frames[-1]) # 总是保留最后一帧 - compressed_data[bone_name] = compressed_frames - - return compressed_data - - def _is_frame_necessary(self, prev_frame: Tuple, current_frame: Tuple, next_frame: Tuple) -> bool: - """ - 判断帧是否必要 - :param prev_frame: 前一帧 - :param current_frame: 当前帧 - :param next_frame: 后一帧 - :return: 是否必要 - """ - prev_time, prev_data = prev_frame - curr_time, curr_data = current_frame - next_time, next_data = next_frame - - # 计算线性插值 - t = (curr_time - prev_time) / (next_time - prev_time) if (next_time - prev_time) > 0 else 0 - interpolated = tuple(prev_data[i] + t * (next_data[i] - prev_data[i]) for i in range(len(prev_data))) - - # 计算与实际值的差异 - total_diff = sum(abs(curr_data[i] - interpolated[i]) for i in range(len(curr_data))) - - return total_diff > self.settings['position_threshold'] * 3 # 简化阈值检查 - - def _compress_quantization(self, bone_data: Dict[str, List]) -> Dict[str, List]: - """ - 量化压缩 - :param bone_data: 骨骼数据 - :return: 量化后的骨骼数据 - """ - # 量化实现 - 简化版本 - return bone_data # 实际项目中需要实现具体的量化算法 - - def _compress_delta(self, bone_data: Dict[str, List]) -> Dict[str, List]: - """ - 差分压缩 - :param bone_data: 骨骼数据 - :return: 差分压缩后的骨骼数据 - """ - # 差分压缩实现 - 简化版本 - return bone_data # 实际项目中需要实现具体的差分算法 - - def _compress_rle(self, bone_data: Dict[str, List]) -> Dict[str, List]: - """ - 游程编码压缩 - :param bone_data: 骨骼数据 - :return: RLE压缩后的骨骼数据 - """ - # RLE压缩实现 - 简化版本 - return bone_data # 实际项目中需要实现具体的RLE算法 - - def _calculate_data_size(self, bone_data: Dict[str, List]) -> int: - """ - 计算数据大小 - :param bone_data: 骨骼数据 - :return: 数据大小(字节) - """ - size = 0 - for frames in bone_data.values(): - size += len(frames) * 100 # 简化估算,每帧约100字节 - return size - - def decompress_animation(self, compressed_data: CompressedAnimationData) -> Dict[str, List]: - """ - 解压缩动画数据 - :param compressed_data: 压缩数据 - :return: 解压缩后的骨骼数据 - """ - # 解压缩实现 - 简化版本 - return compressed_data.bone_data - - def get_compression_stats(self, compressed_data: CompressedAnimationData) -> Dict[str, Any]: - """ - 获取压缩统计信息 - :param compressed_data: 压缩数据 - :return: 统计信息 - """ - return { - 'name': compressed_data.name, - 'compression_ratio': compressed_data.compression_ratio, - 'original_size': compressed_data.original_size, - 'compressed_size': compressed_data.compressed_size, - 'space_saved': compressed_data.original_size - compressed_data.compressed_size, - 'percentage_saved': (1 - compressed_data.compressed_size / compressed_data.original_size) * 100 if compressed_data.original_size > 0 else 0 - } - -class AnimationLODSystem: - """ - 动画LOD系统 - 根据距离和性能自动调整动画质量 - """ - - def __init__(self, world): - """ - 初始化LOD系统 - :param world: 世界对象 - """ - self.world = world - self.settings = LODSettings( - strategy=LODStrategy.DISTANCE_BASED, - distance_thresholds=[10.0, 25.0, 50.0], - frame_rates=[1.0, 0.5, 0.25, 0.1], - enable_auto_lod=True, - performance_threshold=30.0 - ) - self.actor_lod_levels = {} # {actor_id: lod_level} - self.performance_monitor = { - 'last_fps': 60.0, - 'frame_times': [], - 'last_update': 0.0 - } - self.camera_node = None - - print("动画LOD系统初始化完成") - - def set_lod_strategy(self, strategy: LODStrategy) -> None: - """ - 设置LOD策略 - :param strategy: LOD策略 - """ - self.settings.strategy = strategy - print(f"LOD策略设置为: {strategy.name}") - - def set_distance_thresholds(self, thresholds: List[float]) -> None: - """ - 设置距离阈值 - :param thresholds: 距离阈值列表 - """ - self.settings.distance_thresholds = sorted(thresholds) - print(f"距离阈值设置为: {thresholds}") - - def set_frame_rates(self, frame_rates: List[float]) -> None: - """ - 设置帧率级别 - :param frame_rates: 帧率列表 - """ - self.settings.frame_rates = frame_rates - print(f"帧率级别设置为: {frame_rates}") - - def enable_auto_lod(self, enabled: bool = True) -> None: - """ - 启用/禁用自动LOD - :param enabled: 是否启用 - """ - self.settings.enable_auto_lod = enabled - status = "启用" if enabled else "禁用" - print(f"自动LOD已{status}") - - def set_performance_threshold(self, threshold: float) -> None: - """ - 设置性能阈值 - :param threshold: FPS阈值 - """ - self.settings.performance_threshold = threshold - print(f"性能阈值设置为: {threshold} FPS") - - def set_camera_node(self, camera_node) -> None: - """ - 设置摄像机节点 - :param camera_node: 摄像机节点 - """ - self.camera_node = camera_node - - def update_lod_levels(self, actors: Dict[str, Any]) -> None: - """ - 更新所有Actor的LOD级别 - :param actors: Actor字典 {actor_id: actor_info} - """ - if not self.settings.enable_auto_lod: - return - - current_time = globalClock.getRealTime() - - # 每隔一定时间更新一次 - if current_time - self.performance_monitor['last_update'] < 0.5: - return - - self.performance_monitor['last_update'] = current_time - - # 获取摄像机位置 - camera_pos = Point3(0, 0, 0) - if self.camera_node: - camera_pos = self.camera_node.getPos() - elif hasattr(self.world, 'camera') and self.world.camera: - camera_pos = self.world.camera.getPos() - - # 根据策略更新LOD - if self.settings.strategy == LODStrategy.DISTANCE_BASED: - self._update_distance_based_lod(actors, camera_pos) - elif self.settings.strategy == LODStrategy.PERFORMANCE_BASED: - self._update_performance_based_lod(actors) - elif self.settings.strategy == LODStrategy.HYBRID: - self._update_hybrid_lod(actors, camera_pos) - - def _update_distance_based_lod(self, actors: Dict[str, Any], camera_pos: Point3) -> None: - """ - 更新基于距离的LOD - :param actors: Actor字典 - :param camera_pos: 摄像机位置 - """ - for actor_id, actor_info in actors.items(): - actor = actor_info.actor - - # 计算距离 - distance = (actor.getPos() - camera_pos).length() - - # 确定LOD级别 - lod_level = 0 - for i, threshold in enumerate(self.settings.distance_thresholds): - if distance > threshold: - lod_level = i + 1 - - # 应用LOD设置 - self._apply_lod_settings(actor_info, lod_level) - self.actor_lod_levels[actor_id] = lod_level - - def _update_performance_based_lod(self, actors: Dict[str, Any]) -> None: - """ - 更新基于性能的LOD - :param actors: Actor字典 - """ - # 根据FPS调整LOD - fps = self.performance_monitor['last_fps'] - if fps < self.settings.performance_threshold: - # 降低所有Actor的LOD级别 - for actor_id, actor_info in actors.items(): - current_lod = self.actor_lod_levels.get(actor_id, 0) - new_lod = min(current_lod + 1, len(self.settings.frame_rates) - 1) - self._apply_lod_settings(actor_info, new_lod) - self.actor_lod_levels[actor_id] = new_lod - - def _update_hybrid_lod(self, actors: Dict[str, Any], camera_pos: Point3) -> None: - """ - 更新混合LOD - :param actors: Actor字典 - :param camera_pos: 摄像机位置 - """ - # 结合距离和性能的LOD策略 - self._update_distance_based_lod(actors, camera_pos) - self._update_performance_based_lod(actors) - - def _apply_lod_settings(self, actor_info: Any, lod_level: int) -> None: - """ - 应用LOD设置到Actor - :param actor_info: Actor信息 - :param lod_level: LOD级别 - """ - actor = actor_info.actor - frame_rate_multiplier = self.settings.frame_rates[lod_level] if lod_level < len(self.settings.frame_rates) else 0.1 - - # 调整所有动画的播放速度 - for anim_name in actor.getAnimNames(): - current_rate = actor.getPlayRate(anim_name) - new_rate = current_rate * frame_rate_multiplier - actor.setPlayRate(new_rate, anim_name) - - # 更新Actor信息中的LOD级别 - actor_info.lod_level = lod_level - - def update_performance_stats(self, current_fps: float, frame_time: float) -> None: - """ - 更新性能统计 - :param current_fps: 当前FPS - :param frame_time: 帧时间 - """ - self.performance_monitor['last_fps'] = current_fps - self.performance_monitor['frame_times'].append(frame_time) - - # 保持最近30帧的统计数据 - if len(self.performance_monitor['frame_times']) > 30: - self.performance_monitor['frame_times'] = self.performance_monitor['frame_times'][-30:] - - def get_lod_stats(self) -> Dict[str, Any]: - """ - 获取LOD统计信息 - :return: 统计信息 - """ - lod_counts = {} - for lod_level in self.actor_lod_levels.values(): - if lod_level not in lod_counts: - lod_counts[lod_level] = 0 - lod_counts[lod_level] += 1 - - return { - 'actor_lod_distribution': lod_counts, - 'current_fps': self.performance_monitor['last_fps'], - 'average_frame_time': sum(self.performance_monitor['frame_times']) / len(self.performance_monitor['frame_times']) - if self.performance_monitor['frame_times'] else 0, - 'lod_settings': { - 'strategy': self.settings.strategy.name, - 'distance_thresholds': self.settings.distance_thresholds, - 'frame_rates': self.settings.frame_rates - } - } - -class AnimationCacheManager: - """ - 动画缓存管理器 - 提供高效的动画数据缓存 - """ - - def __init__(self, max_size: int = 500): - """ - 初始化缓存管理器 - :param max_size: 最大缓存大小 - """ - self.max_size = max_size - self.cache: Dict[str, CacheEntry] = {} - self.policy = CachePolicy.LRU - self.cleanup_interval = 30.0 - self.last_cleanup_time = 0.0 - - print(f"动画缓存管理器初始化完成 (最大大小: {max_size})") - - def set_cache_policy(self, policy: CachePolicy) -> None: - """ - 设置缓存策略 - :param policy: 缓存策略 - """ - self.policy = policy - print(f"缓存策略设置为: {policy.name}") - - def set_max_size(self, max_size: int) -> None: - """ - 设置最大缓存大小 - :param max_size: 最大大小 - """ - self.max_size = max_size - self._cleanup_cache() - print(f"最大缓存大小设置为: {max_size}") - - def put(self, key: str, data: Any, size: int = 0) -> None: - """ - 放入缓存项 - :param key: 键 - :param data: 数据 - :param size: 数据大小 - """ - current_time = globalClock.getRealTime() - - if key in self.cache: - # 更新现有条目 - entry = self.cache[key] - entry.data = data - entry.timestamp = current_time - entry.access_count += 1 - entry.size = size or entry.size - else: - # 添加新条目 - entry = CacheEntry( - key=key, - data=data, - timestamp=current_time, - access_count=1, - size=size - ) - self.cache[key] = entry - - # 检查是否需要清理缓存 - self._check_cleanup() - - def get(self, key: str) -> Optional[Any]: - """ - 获取缓存项 - :param key: 键 - :return: 数据或None - """ - if key in self.cache: - entry = self.cache[key] - entry.access_count += 1 - entry.timestamp = globalClock.getRealTime() - return entry.data - return None - - def remove(self, key: str) -> bool: - """ - 移除缓存项 - :param key: 键 - :return: 是否成功移除 - """ - if key in self.cache: - del self.cache[key] - return True - return False - - def clear(self) -> None: - """清空缓存""" - self.cache.clear() - print("动画缓存已清空") - - def _check_cleanup(self) -> None: - """ - 检查是否需要清理缓存 - """ - current_time = globalClock.getRealTime() - if current_time - self.last_cleanup_time > self.cleanup_interval: - self._cleanup_cache() - self.last_cleanup_time = current_time - - def _cleanup_cache(self) -> None: - """ - 清理缓存 - """ - if len(self.cache) <= self.max_size: - return - - # 根据策略选择要移除的条目 - entries_to_remove = len(self.cache) - self.max_size - - if self.policy == CachePolicy.LRU: - # 移除最近最少使用的条目 - sorted_entries = sorted(self.cache.values(), key=lambda x: x.timestamp) - elif self.policy == CachePolicy.LFU: - # 移除最不经常使用的条目 - sorted_entries = sorted(self.cache.values(), key=lambda x: x.access_count) - else: # FIFO - # 移除最早添加的条目 - sorted_entries = sorted(self.cache.values(), key=lambda x: x.timestamp) - - # 移除条目 - for i in range(min(entries_to_remove, len(sorted_entries))): - entry = sorted_entries[i] - del self.cache[entry.key] - - print(f"缓存清理完成,移除了 {entries_to_remove} 个条目") - - def get_cache_stats(self) -> Dict[str, Any]: - """ - 获取缓存统计信息 - :return: 统计信息 - """ - total_size = sum(entry.size for entry in self.cache.values()) - total_access = sum(entry.access_count for entry in self.cache.values()) - avg_access = total_access / len(self.cache) if self.cache else 0 - - return { - 'size': len(self.cache), - 'max_size': self.max_size, - 'total_size': total_size, - 'hit_rate': 0, # 简化实现,实际项目中需要跟踪命中率 - 'average_access_count': avg_access, - 'policy': self.policy.name - } - -# 使用示例和测试代码 -def example_compression_usage(actor: Actor): - """ - 动画压缩使用示例 - """ - print("=== 动画压缩使用示例 ===") - - compressor = AnimationCompressor() - - # 压缩所有动画 - for anim_name in actor.getAnimNames(): - print(f"压缩动画: {anim_name}") - compressed_data = compressor.compress_animation( - actor, - anim_name, - CompressionMethod.KEYFRAME_REDUCTION - ) - if compressed_data: - stats = compressor.get_compression_stats(compressed_data) - print(f" 压缩统计: {stats}") - - print("动画压缩示例完成") - -def example_lod_usage(world, actors: Dict[str, Any]): - """ - LOD系统使用示例 - """ - print("=== 动画LOD使用示例 ===") - - lod_system = AnimationLODSystem(world) - - # 设置LOD参数 - lod_system.set_lod_strategy(LODStrategy.HYBRID) - lod_system.set_distance_thresholds([15.0, 30.0, 60.0]) - lod_system.set_frame_rates([1.0, 0.7, 0.4, 0.1]) - - # 更新LOD级别 - lod_system.update_lod_levels(actors) - - # 更新性能统计 - lod_system.update_performance_stats(45, 0.022) # 45 FPS, 22ms per frame - - # 获取LOD统计 - stats = lod_system.get_lod_stats() - print(f" LOD统计: {stats}") - - print("动画LOD示例完成") - -def example_cache_usage(): - """ - 缓存管理使用示例 - """ - print("=== 动画缓存管理使用示例 ===") - - cache_manager = AnimationCacheManager(max_size=100) - - # 设置缓存策略 - cache_manager.set_cache_policy(CachePolicy.LRU) - - # 添加缓存项 - dummy_data = {'frames': 100, 'bones': 20, 'data': 'animation_data'} - cache_manager.put("actor_1_walk", dummy_data, size=1024) - cache_manager.put("actor_1_run", dummy_data, size=2048) - cache_manager.put("actor_2_idle", dummy_data, size=512) - - # 获取缓存项 - data = cache_manager.get("actor_1_walk") - if data: - print(" 成功获取缓存数据") - - # 查看缓存统计 - stats = cache_manager.get_cache_stats() - print(f" 缓存统计: {stats}") - - print("动画缓存管理示例完成") - -if __name__ == "__main__": +""" +高级骨骼动画系统 - 优化和压缩模块 +提供动画压缩、LOD、缓存优化等功能 +""" + +import math +import json +import os +from typing import Dict, List, Optional, Tuple, Any, Callable +from dataclasses import dataclass, field +from enum import Enum + +from panda3d.core import * +from direct.actor.Actor import Actor + +# 定义常量 +OPTIMIZATION_SETTINGS = { + 'compression_threshold': 0.001, + 'lod_distance_thresholds': [10.0, 25.0, 50.0], + 'lod_frame_rates': [1.0, 0.5, 0.25, 0.1], + 'cache_max_size': 500, + 'cache_cleanup_interval': 30.0 +} + +# 枚举定义 +class CompressionMethod(Enum): + """压缩方法枚举""" + KEYFRAME_REDUCTION = 0 + QUANTIZATION = 1 + DELTA_COMPRESSION = 2 + RUN_LENGTH_ENCODING = 3 + +class LODStrategy(Enum): + """LOD策略枚举""" + DISTANCE_BASED = 0 + PERFORMANCE_BASED = 1 + HYBRID = 2 + +class CachePolicy(Enum): + """缓存策略枚举""" + LRU = 0 # 最近最少使用 + LFU = 1 # 最不经常使用 + FIFO = 2 # 先进先出 + +@dataclass +class CompressedAnimationData: + """压缩动画数据类""" + name: str + frame_rate: float + duration: float + bone_data: Dict[str, List[Tuple[float, Tuple[float, float, float, float, float, float, float]]]] # 时间, (位置+旋转+缩放) + compression_ratio: float = 1.0 + original_size: int = 0 + compressed_size: int = 0 + +@dataclass +class LODSettings: + """LOD设置数据类""" + strategy: LODStrategy + distance_thresholds: List[float] + frame_rates: List[float] + enable_auto_lod: bool = True + performance_threshold: float = 30.0 # FPS阈值 + +@dataclass +class CacheEntry: + """缓存条目数据类""" + key: str + data: Any + timestamp: float + access_count: int = 1 + size: int = 0 + +class AnimationCompressor: + """ + 动画压缩系统 + 支持多种压缩算法 + """ + + def __init__(self): + """初始化动画压缩系统""" + self.settings = { + 'position_threshold': 0.001, + 'rotation_threshold': 0.01, + 'scale_threshold': 0.001, + 'quantization_bits': 16, + 'enable_quantization': True + } + + print("动画压缩系统初始化完成") + + def compress_animation(self, actor: Actor, anim_name: str, + method: CompressionMethod = CompressionMethod.KEYFRAME_REDUCTION) -> Optional[CompressedAnimationData]: + """ + 压缩指定动画 + :param actor: Actor对象 + :param anim_name: 动画名称 + :param method: 压缩方法 + :return: 压缩后的动画数据 + """ + if anim_name not in actor.getAnimNames(): + print(f"错误: 动画 {anim_name} 不存在") + return None + + print(f"开始压缩动画: {anim_name} (方法: {method.name})") + + # 获取动画控制 + anim_control = actor.getAnimControl(anim_name) + if not anim_control: + print(f"错误: 无法获取动画控制 {anim_name}") + return None + + # 获取动画数据 + num_frames = anim_control.getNumFrames() + frame_rate = anim_control.getFrameRate() + duration = anim_control.getDuration() + + print(f" 动画信息: {num_frames} 帧, {frame_rate} FPS, {duration:.2f} 秒") + + # 提取骨骼动画数据 + bone_data = self._extract_bone_animation_data(actor, anim_name, num_frames) + + # 根据方法进行压缩 + if method == CompressionMethod.KEYFRAME_REDUCTION: + compressed_bone_data = self._compress_keyframe_reduction(bone_data) + elif method == CompressionMethod.QUANTIZATION: + compressed_bone_data = self._compress_quantization(bone_data) + elif method == CompressionMethod.DELTA_COMPRESSION: + compressed_bone_data = self._compress_delta(bone_data) + elif method == CompressionMethod.RUN_LENGTH_ENCODING: + compressed_bone_data = self._compress_rle(bone_data) + else: + compressed_bone_data = bone_data + + # 计算压缩比 + original_size = self._calculate_data_size(bone_data) + compressed_size = self._calculate_data_size(compressed_bone_data) + compression_ratio = original_size / compressed_size if compressed_size > 0 else 1.0 + + # 创建压缩数据 + compressed_data = CompressedAnimationData( + name=anim_name, + frame_rate=frame_rate, + duration=duration, + bone_data=compressed_bone_data, + compression_ratio=compression_ratio, + original_size=original_size, + compressed_size=compressed_size + ) + + print(f" 压缩完成: 原始大小={original_size}, 压缩大小={compressed_size}, 压缩比={compression_ratio:.2f}:1") + return compressed_data + + def _extract_bone_animation_data(self, actor: Actor, anim_name: str, num_frames: int) -> Dict[str, List]: + """ + 提取骨骼动画数据 + :param actor: Actor对象 + :param anim_name: 动画名称 + :param num_frames: 帧数 + :return: 骨骼动画数据 + """ + bone_data = {} + + # 获取所有骨骼 + bundle = actor.getPartBundleDict().get('modelRoot') + if not bundle: + return bone_data + + # 遍历骨骼 + for i in range(bundle.getNumChildren()): + child = bundle.getChild(i) + if hasattr(child, 'getName'): + bone_name = child.getName() + bone_data[bone_name] = self._extract_bone_frames(actor, bone_name, anim_name, num_frames) + + return bone_data + + def _extract_bone_frames(self, actor: Actor, bone_name: str, anim_name: str, num_frames: int) -> List: + """ + 提取单个骨骼的帧数据 + :param actor: Actor对象 + :param bone_name: 骨骼名称 + :param anim_name: 动画名称 + :param num_frames: 帧数 + :return: 帧数据列表 + """ + frames = [] + + # 为每一帧提取数据 + for frame in range(num_frames): + # 设置到指定帧 + actor.pose(anim_name, frame) + + # 获取骨骼变换 + joint = actor.exposeJoint(None, "modelRoot", bone_name) + if joint: + pos = joint.getPos() + hpr = joint.getHpr() + scale = joint.getScale() + + # 存储为 (时间, (px, py, pz, rx, ry, rz, sx, sy, sz)) + time = frame / actor.getFrameRate(anim_name) + frame_data = ( + time, + (pos.x, pos.y, pos.z, + hpr.x, hpr.y, hpr.z, + scale.x, scale.y, scale.z) + ) + frames.append(frame_data) + + return frames + + def _compress_keyframe_reduction(self, bone_data: Dict[str, List]) -> Dict[str, List]: + """ + 关键帧简化压缩 + :param bone_data: 骨骼数据 + :return: 压缩后的骨骼数据 + """ + compressed_data = {} + + for bone_name, frames in bone_data.items(): + if len(frames) <= 2: + compressed_data[bone_name] = frames + continue + + compressed_frames = [frames[0]] # 总是保留第一帧 + last_frame = frames[0] + + for i in range(1, len(frames) - 1): + current_frame = frames[i] + next_frame = frames[i + 1] + + # 检查当前帧是否可以被简化 + if not self._is_frame_necessary(last_frame, current_frame, next_frame): + continue + + compressed_frames.append(current_frame) + last_frame = current_frame + + compressed_frames.append(frames[-1]) # 总是保留最后一帧 + compressed_data[bone_name] = compressed_frames + + return compressed_data + + def _is_frame_necessary(self, prev_frame: Tuple, current_frame: Tuple, next_frame: Tuple) -> bool: + """ + 判断帧是否必要 + :param prev_frame: 前一帧 + :param current_frame: 当前帧 + :param next_frame: 后一帧 + :return: 是否必要 + """ + prev_time, prev_data = prev_frame + curr_time, curr_data = current_frame + next_time, next_data = next_frame + + # 计算线性插值 + t = (curr_time - prev_time) / (next_time - prev_time) if (next_time - prev_time) > 0 else 0 + interpolated = tuple(prev_data[i] + t * (next_data[i] - prev_data[i]) for i in range(len(prev_data))) + + # 计算与实际值的差异 + total_diff = sum(abs(curr_data[i] - interpolated[i]) for i in range(len(curr_data))) + + return total_diff > self.settings['position_threshold'] * 3 # 简化阈值检查 + + def _compress_quantization(self, bone_data: Dict[str, List]) -> Dict[str, List]: + """ + 量化压缩 + :param bone_data: 骨骼数据 + :return: 量化后的骨骼数据 + """ + # 量化实现 - 简化版本 + return bone_data # 实际项目中需要实现具体的量化算法 + + def _compress_delta(self, bone_data: Dict[str, List]) -> Dict[str, List]: + """ + 差分压缩 + :param bone_data: 骨骼数据 + :return: 差分压缩后的骨骼数据 + """ + # 差分压缩实现 - 简化版本 + return bone_data # 实际项目中需要实现具体的差分算法 + + def _compress_rle(self, bone_data: Dict[str, List]) -> Dict[str, List]: + """ + 游程编码压缩 + :param bone_data: 骨骼数据 + :return: RLE压缩后的骨骼数据 + """ + # RLE压缩实现 - 简化版本 + return bone_data # 实际项目中需要实现具体的RLE算法 + + def _calculate_data_size(self, bone_data: Dict[str, List]) -> int: + """ + 计算数据大小 + :param bone_data: 骨骼数据 + :return: 数据大小(字节) + """ + size = 0 + for frames in bone_data.values(): + size += len(frames) * 100 # 简化估算,每帧约100字节 + return size + + def decompress_animation(self, compressed_data: CompressedAnimationData) -> Dict[str, List]: + """ + 解压缩动画数据 + :param compressed_data: 压缩数据 + :return: 解压缩后的骨骼数据 + """ + # 解压缩实现 - 简化版本 + return compressed_data.bone_data + + def get_compression_stats(self, compressed_data: CompressedAnimationData) -> Dict[str, Any]: + """ + 获取压缩统计信息 + :param compressed_data: 压缩数据 + :return: 统计信息 + """ + return { + 'name': compressed_data.name, + 'compression_ratio': compressed_data.compression_ratio, + 'original_size': compressed_data.original_size, + 'compressed_size': compressed_data.compressed_size, + 'space_saved': compressed_data.original_size - compressed_data.compressed_size, + 'percentage_saved': (1 - compressed_data.compressed_size / compressed_data.original_size) * 100 if compressed_data.original_size > 0 else 0 + } + +class AnimationLODSystem: + """ + 动画LOD系统 + 根据距离和性能自动调整动画质量 + """ + + def __init__(self, world): + """ + 初始化LOD系统 + :param world: 世界对象 + """ + self.world = world + self.settings = LODSettings( + strategy=LODStrategy.DISTANCE_BASED, + distance_thresholds=[10.0, 25.0, 50.0], + frame_rates=[1.0, 0.5, 0.25, 0.1], + enable_auto_lod=True, + performance_threshold=30.0 + ) + self.actor_lod_levels = {} # {actor_id: lod_level} + self.performance_monitor = { + 'last_fps': 60.0, + 'frame_times': [], + 'last_update': 0.0 + } + self.camera_node = None + + print("动画LOD系统初始化完成") + + def set_lod_strategy(self, strategy: LODStrategy) -> None: + """ + 设置LOD策略 + :param strategy: LOD策略 + """ + self.settings.strategy = strategy + print(f"LOD策略设置为: {strategy.name}") + + def set_distance_thresholds(self, thresholds: List[float]) -> None: + """ + 设置距离阈值 + :param thresholds: 距离阈值列表 + """ + self.settings.distance_thresholds = sorted(thresholds) + print(f"距离阈值设置为: {thresholds}") + + def set_frame_rates(self, frame_rates: List[float]) -> None: + """ + 设置帧率级别 + :param frame_rates: 帧率列表 + """ + self.settings.frame_rates = frame_rates + print(f"帧率级别设置为: {frame_rates}") + + def enable_auto_lod(self, enabled: bool = True) -> None: + """ + 启用/禁用自动LOD + :param enabled: 是否启用 + """ + self.settings.enable_auto_lod = enabled + status = "启用" if enabled else "禁用" + print(f"自动LOD已{status}") + + def set_performance_threshold(self, threshold: float) -> None: + """ + 设置性能阈值 + :param threshold: FPS阈值 + """ + self.settings.performance_threshold = threshold + print(f"性能阈值设置为: {threshold} FPS") + + def set_camera_node(self, camera_node) -> None: + """ + 设置摄像机节点 + :param camera_node: 摄像机节点 + """ + self.camera_node = camera_node + + def update_lod_levels(self, actors: Dict[str, Any]) -> None: + """ + 更新所有Actor的LOD级别 + :param actors: Actor字典 {actor_id: actor_info} + """ + if not self.settings.enable_auto_lod: + return + + current_time = globalClock.getRealTime() + + # 每隔一定时间更新一次 + if current_time - self.performance_monitor['last_update'] < 0.5: + return + + self.performance_monitor['last_update'] = current_time + + # 获取摄像机位置 + camera_pos = Point3(0, 0, 0) + if self.camera_node: + camera_pos = self.camera_node.getPos() + elif hasattr(self.world, 'camera') and self.world.camera: + camera_pos = self.world.camera.getPos() + + # 根据策略更新LOD + if self.settings.strategy == LODStrategy.DISTANCE_BASED: + self._update_distance_based_lod(actors, camera_pos) + elif self.settings.strategy == LODStrategy.PERFORMANCE_BASED: + self._update_performance_based_lod(actors) + elif self.settings.strategy == LODStrategy.HYBRID: + self._update_hybrid_lod(actors, camera_pos) + + def _update_distance_based_lod(self, actors: Dict[str, Any], camera_pos: Point3) -> None: + """ + 更新基于距离的LOD + :param actors: Actor字典 + :param camera_pos: 摄像机位置 + """ + for actor_id, actor_info in actors.items(): + actor = actor_info.actor + + # 计算距离 + distance = (actor.getPos() - camera_pos).length() + + # 确定LOD级别 + lod_level = 0 + for i, threshold in enumerate(self.settings.distance_thresholds): + if distance > threshold: + lod_level = i + 1 + + # 应用LOD设置 + self._apply_lod_settings(actor_info, lod_level) + self.actor_lod_levels[actor_id] = lod_level + + def _update_performance_based_lod(self, actors: Dict[str, Any]) -> None: + """ + 更新基于性能的LOD + :param actors: Actor字典 + """ + # 根据FPS调整LOD + fps = self.performance_monitor['last_fps'] + if fps < self.settings.performance_threshold: + # 降低所有Actor的LOD级别 + for actor_id, actor_info in actors.items(): + current_lod = self.actor_lod_levels.get(actor_id, 0) + new_lod = min(current_lod + 1, len(self.settings.frame_rates) - 1) + self._apply_lod_settings(actor_info, new_lod) + self.actor_lod_levels[actor_id] = new_lod + + def _update_hybrid_lod(self, actors: Dict[str, Any], camera_pos: Point3) -> None: + """ + 更新混合LOD + :param actors: Actor字典 + :param camera_pos: 摄像机位置 + """ + # 结合距离和性能的LOD策略 + self._update_distance_based_lod(actors, camera_pos) + self._update_performance_based_lod(actors) + + def _apply_lod_settings(self, actor_info: Any, lod_level: int) -> None: + """ + 应用LOD设置到Actor + :param actor_info: Actor信息 + :param lod_level: LOD级别 + """ + actor = actor_info.actor + frame_rate_multiplier = self.settings.frame_rates[lod_level] if lod_level < len(self.settings.frame_rates) else 0.1 + + # 调整所有动画的播放速度 + for anim_name in actor.getAnimNames(): + current_rate = actor.getPlayRate(anim_name) + new_rate = current_rate * frame_rate_multiplier + actor.setPlayRate(new_rate, anim_name) + + # 更新Actor信息中的LOD级别 + actor_info.lod_level = lod_level + + def update_performance_stats(self, current_fps: float, frame_time: float) -> None: + """ + 更新性能统计 + :param current_fps: 当前FPS + :param frame_time: 帧时间 + """ + self.performance_monitor['last_fps'] = current_fps + self.performance_monitor['frame_times'].append(frame_time) + + # 保持最近30帧的统计数据 + if len(self.performance_monitor['frame_times']) > 30: + self.performance_monitor['frame_times'] = self.performance_monitor['frame_times'][-30:] + + def get_lod_stats(self) -> Dict[str, Any]: + """ + 获取LOD统计信息 + :return: 统计信息 + """ + lod_counts = {} + for lod_level in self.actor_lod_levels.values(): + if lod_level not in lod_counts: + lod_counts[lod_level] = 0 + lod_counts[lod_level] += 1 + + return { + 'actor_lod_distribution': lod_counts, + 'current_fps': self.performance_monitor['last_fps'], + 'average_frame_time': sum(self.performance_monitor['frame_times']) / len(self.performance_monitor['frame_times']) + if self.performance_monitor['frame_times'] else 0, + 'lod_settings': { + 'strategy': self.settings.strategy.name, + 'distance_thresholds': self.settings.distance_thresholds, + 'frame_rates': self.settings.frame_rates + } + } + +class AnimationCacheManager: + """ + 动画缓存管理器 + 提供高效的动画数据缓存 + """ + + def __init__(self, max_size: int = 500): + """ + 初始化缓存管理器 + :param max_size: 最大缓存大小 + """ + self.max_size = max_size + self.cache: Dict[str, CacheEntry] = {} + self.policy = CachePolicy.LRU + self.cleanup_interval = 30.0 + self.last_cleanup_time = 0.0 + + print(f"动画缓存管理器初始化完成 (最大大小: {max_size})") + + def set_cache_policy(self, policy: CachePolicy) -> None: + """ + 设置缓存策略 + :param policy: 缓存策略 + """ + self.policy = policy + print(f"缓存策略设置为: {policy.name}") + + def set_max_size(self, max_size: int) -> None: + """ + 设置最大缓存大小 + :param max_size: 最大大小 + """ + self.max_size = max_size + self._cleanup_cache() + print(f"最大缓存大小设置为: {max_size}") + + def put(self, key: str, data: Any, size: int = 0) -> None: + """ + 放入缓存项 + :param key: 键 + :param data: 数据 + :param size: 数据大小 + """ + current_time = globalClock.getRealTime() + + if key in self.cache: + # 更新现有条目 + entry = self.cache[key] + entry.data = data + entry.timestamp = current_time + entry.access_count += 1 + entry.size = size or entry.size + else: + # 添加新条目 + entry = CacheEntry( + key=key, + data=data, + timestamp=current_time, + access_count=1, + size=size + ) + self.cache[key] = entry + + # 检查是否需要清理缓存 + self._check_cleanup() + + def get(self, key: str) -> Optional[Any]: + """ + 获取缓存项 + :param key: 键 + :return: 数据或None + """ + if key in self.cache: + entry = self.cache[key] + entry.access_count += 1 + entry.timestamp = globalClock.getRealTime() + return entry.data + return None + + def remove(self, key: str) -> bool: + """ + 移除缓存项 + :param key: 键 + :return: 是否成功移除 + """ + if key in self.cache: + del self.cache[key] + return True + return False + + def clear(self) -> None: + """清空缓存""" + self.cache.clear() + print("动画缓存已清空") + + def _check_cleanup(self) -> None: + """ + 检查是否需要清理缓存 + """ + current_time = globalClock.getRealTime() + if current_time - self.last_cleanup_time > self.cleanup_interval: + self._cleanup_cache() + self.last_cleanup_time = current_time + + def _cleanup_cache(self) -> None: + """ + 清理缓存 + """ + if len(self.cache) <= self.max_size: + return + + # 根据策略选择要移除的条目 + entries_to_remove = len(self.cache) - self.max_size + + if self.policy == CachePolicy.LRU: + # 移除最近最少使用的条目 + sorted_entries = sorted(self.cache.values(), key=lambda x: x.timestamp) + elif self.policy == CachePolicy.LFU: + # 移除最不经常使用的条目 + sorted_entries = sorted(self.cache.values(), key=lambda x: x.access_count) + else: # FIFO + # 移除最早添加的条目 + sorted_entries = sorted(self.cache.values(), key=lambda x: x.timestamp) + + # 移除条目 + for i in range(min(entries_to_remove, len(sorted_entries))): + entry = sorted_entries[i] + del self.cache[entry.key] + + print(f"缓存清理完成,移除了 {entries_to_remove} 个条目") + + def get_cache_stats(self) -> Dict[str, Any]: + """ + 获取缓存统计信息 + :return: 统计信息 + """ + total_size = sum(entry.size for entry in self.cache.values()) + total_access = sum(entry.access_count for entry in self.cache.values()) + avg_access = total_access / len(self.cache) if self.cache else 0 + + return { + 'size': len(self.cache), + 'max_size': self.max_size, + 'total_size': total_size, + 'hit_rate': 0, # 简化实现,实际项目中需要跟踪命中率 + 'average_access_count': avg_access, + 'policy': self.policy.name + } + +# 使用示例和测试代码 +def example_compression_usage(actor: Actor): + """ + 动画压缩使用示例 + """ + print("=== 动画压缩使用示例 ===") + + compressor = AnimationCompressor() + + # 压缩所有动画 + for anim_name in actor.getAnimNames(): + print(f"压缩动画: {anim_name}") + compressed_data = compressor.compress_animation( + actor, + anim_name, + CompressionMethod.KEYFRAME_REDUCTION + ) + if compressed_data: + stats = compressor.get_compression_stats(compressed_data) + print(f" 压缩统计: {stats}") + + print("动画压缩示例完成") + +def example_lod_usage(world, actors: Dict[str, Any]): + """ + LOD系统使用示例 + """ + print("=== 动画LOD使用示例 ===") + + lod_system = AnimationLODSystem(world) + + # 设置LOD参数 + lod_system.set_lod_strategy(LODStrategy.HYBRID) + lod_system.set_distance_thresholds([15.0, 30.0, 60.0]) + lod_system.set_frame_rates([1.0, 0.7, 0.4, 0.1]) + + # 更新LOD级别 + lod_system.update_lod_levels(actors) + + # 更新性能统计 + lod_system.update_performance_stats(45, 0.022) # 45 FPS, 22ms per frame + + # 获取LOD统计 + stats = lod_system.get_lod_stats() + print(f" LOD统计: {stats}") + + print("动画LOD示例完成") + +def example_cache_usage(): + """ + 缓存管理使用示例 + """ + print("=== 动画缓存管理使用示例 ===") + + cache_manager = AnimationCacheManager(max_size=100) + + # 设置缓存策略 + cache_manager.set_cache_policy(CachePolicy.LRU) + + # 添加缓存项 + dummy_data = {'frames': 100, 'bones': 20, 'data': 'animation_data'} + cache_manager.put("actor_1_walk", dummy_data, size=1024) + cache_manager.put("actor_1_run", dummy_data, size=2048) + cache_manager.put("actor_2_idle", dummy_data, size=512) + + # 获取缓存项 + data = cache_manager.get("actor_1_walk") + if data: + print(" 成功获取缓存数据") + + # 查看缓存统计 + stats = cache_manager.get_cache_stats() + print(f" 缓存统计: {stats}") + + print("动画缓存管理示例完成") + +if __name__ == "__main__": print("动画优化和压缩模块加载完成") \ No newline at end of file diff --git a/plugins/user/advanced_skeletal_animation/plugin.py b/plugins/user/advanced_skeletal_animation/plugin.py index 01c2db9f..25ea41fd 100644 --- a/plugins/user/advanced_skeletal_animation/plugin.py +++ b/plugins/user/advanced_skeletal_animation/plugin.py @@ -1,281 +1,281 @@ -""" -高级骨骼动画系统插件 -完整的骨骼动画解决方案,包含动画播放、混合、IK、压缩、网络同步等高级功能 -""" - -from plugins.plugin_manager import BasePlugin -from direct.showbase.DirectObject import DirectObject - -# 导入核心模块 -from .core import SkeletalAnimationCore -from .network import AnimationNetworkSync -from .advanced import AnimationStateMachine, AnimationCurveEditor, PhysicsAnimationIntegration -from .optimization import AnimationCompressor, AnimationLODSystem, AnimationCacheManager -from .tools import AnimationAnalyzer, AnimationEditor, DebugVisualizer, AnimationProfiler - -class AdvancedSkeletalAnimationPlugin(BasePlugin): - """ - 高级骨骼动画系统插件 - 提供完整的骨骼动画支持,包括加载、播放、控制、混合等高级功能 - """ - - def __init__(self, plugin_manager, name): - """ - 初始化高级骨骼动画插件 - :param plugin_manager: 插件管理器 - :param name: 插件名称 - """ - super().__init__(plugin_manager, name) - self.config = { - "version": "1.0.0", - "author": "Advanced Animation Team", - "description": "高级骨骼动画系统,支持模型动画加载、播放控制、混合、IK、压缩、网络同步等高级功能" - } - - # 核心系统组件 - self.core_system = None - self.network_system = None - self.lod_system = None - self.cache_manager = None - self.is_enabled = False - self.update_task = None - - print(f"高级骨骼动画插件初始化: {self.name}") - - def initialize(self) -> bool: - """ - 初始化插件 - :return: 是否初始化成功 - """ - try: - world = self.plugin_manager.world - - # 创建核心动画系统 - self.core_system = SkeletalAnimationCore(world) - - # 创建网络同步系统 - self.network_system = AnimationNetworkSync(world, self.core_system) - - # 创建LOD系统 - self.lod_system = AnimationLODSystem(world) - - # 创建缓存管理器 - self.cache_manager = AnimationCacheManager() - - print(f"高级骨骼动画插件初始化成功: {self.name}") - return True - - except Exception as e: - print(f"高级骨骼动画插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - :return: 是否启用成功 - """ - if not super().enable(): - return False - - if self.is_enabled: - print(f"高级骨骼动画插件已启用: {self.name}") - return True - - try: - # 启动核心系统更新 - self.core_system.start_updating() - - # 启动网络同步 - self.network_system.start_sync_task() - - # 启动更新任务 - self.update_task = self.plugin_manager.world.taskMgr.add( - self._update_task, - f"advanced_skeletal_animation_update_{self.name}", - sort=30 - ) - - # 标记为已启用 - self.is_enabled = True - - print(f"高级骨骼动画插件启用成功: {self.name}") - return True - - except Exception as e: - print(f"高级骨骼动画插件启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self) -> bool: - """ - 禁用插件 - :return: 是否禁用成功 - """ - if not super().disable(): - return False - - if not self.is_enabled: - print(f"高级骨骼动画插件已禁用: {self.name}") - return True - - try: - # 停止更新任务 - if self.update_task: - self.plugin_manager.world.taskMgr.remove(self.update_task) - self.update_task = None - - # 停止核心系统更新 - self.core_system.stop_updating() - - # 停止网络同步 - self.network_system.stop_sync_task() - - # 标记为已禁用 - self.is_enabled = False - - print(f"高级骨骼动画插件禁用成功: {self.name}") - return True - - except Exception as e: - print(f"高级骨骼动画插件禁用失败: {e}") - import traceback - traceback.print_exc() - return False - - def finalize(self) -> None: - """ - 清理插件资源 - """ - print(f"清理高级骨骼动画插件资源: {self.name}") - - try: - # 停止更新任务 - if self.update_task: - self.plugin_manager.world.taskMgr.remove(self.update_task) - self.update_task = None - - # 停止核心系统 - if self.core_system: - self.core_system.stop_updating() - - # 停止网络同步 - if self.network_system: - self.network_system.stop_sync_task() - - # 清理资源 - self.core_system = None - self.network_system = None - self.lod_system = None - self.cache_manager = None - - print(f"高级骨骼动画插件资源清理完成: {self.name}") - - except Exception as e: - print(f"清理高级骨骼动画插件资源失败: {e}") - import traceback - traceback.print_exc() - - def _update_task(self, task) -> int: - """ - 插件更新任务 - :param task: 任务对象 - :return: 任务状态 - """ - try: - dt = globalClock.getDt() - - # 更新核心系统 - if self.core_system: - self.core_system.update(dt) - - # 更新LOD系统 - if self.lod_system and self.core_system: - self.lod_system.update_lod_levels(self.core_system.actors) - - # 更新网络同步 - # (网络同步有独立的任务) - - except Exception as e: - print(f"插件更新任务出错: {e}") - - return task.cont - - # 核心系统访问方法 - def get_core_system(self): - """ - 获取核心动画系统 - :return: SkeletalAnimationCore实例 - """ - return self.core_system - - def get_network_system(self): - """ - 获取网络同步系统 - :return: AnimationNetworkSync实例 - """ - return self.network_system - - def get_lod_system(self): - """ - 获取LOD系统 - :return: AnimationLODSystem实例 - """ - return self.lod_system - - def get_cache_manager(self): - """ - 获取缓存管理器 - :return: AnimationCacheManager实例 - """ - return self.cache_manager - - def get_analyzer(self): - """ - 获取动画分析器 - :return: AnimationAnalyzer实例 - """ - return self.core_system.get_analyzer() if self.core_system else None - - def get_editor(self): - """ - 获取动画编辑器 - :return: AnimationEditor实例 - """ - return self.core_system.get_editor() if self.core_system else None - - def get_visualizer(self): - """ - 获取调试可视化工具 - :return: DebugVisualizer实例 - """ - return self.core_system.get_visualizer() if self.core_system else None - - def get_profiler(self): - """ - 获取性能分析器 - :return: AnimationProfiler实例 - """ - return self.core_system.get_profiler() if self.core_system else None - -# 兼容性接口 -Plugin = AdvancedSkeletalAnimationPlugin - -__all__ = [ - 'AdvancedSkeletalAnimationPlugin', - 'Plugin', - 'SkeletalAnimationCore', - 'AnimationNetworkSync', - 'AnimationStateMachine', - 'AnimationCurveEditor', - 'PhysicsAnimationIntegration', - 'AnimationCompressor', - 'AnimationLODSystem', - 'AnimationCacheManager', - 'AnimationAnalyzer', - 'AnimationEditor', - 'DebugVisualizer', - 'AnimationProfiler' +""" +高级骨骼动画系统插件 +完整的骨骼动画解决方案,包含动画播放、混合、IK、压缩、网络同步等高级功能 +""" + +from plugins.plugin_manager import BasePlugin +from direct.showbase.DirectObject import DirectObject + +# 导入核心模块 +from .core import SkeletalAnimationCore +from .network import AnimationNetworkSync +from .advanced import AnimationStateMachine, AnimationCurveEditor, PhysicsAnimationIntegration +from .optimization import AnimationCompressor, AnimationLODSystem, AnimationCacheManager +from .tools import AnimationAnalyzer, AnimationEditor, DebugVisualizer, AnimationProfiler + +class AdvancedSkeletalAnimationPlugin(BasePlugin): + """ + 高级骨骼动画系统插件 + 提供完整的骨骼动画支持,包括加载、播放、控制、混合等高级功能 + """ + + def __init__(self, plugin_manager, name): + """ + 初始化高级骨骼动画插件 + :param plugin_manager: 插件管理器 + :param name: 插件名称 + """ + super().__init__(plugin_manager, name) + self.config = { + "version": "1.0.0", + "author": "Advanced Animation Team", + "description": "高级骨骼动画系统,支持模型动画加载、播放控制、混合、IK、压缩、网络同步等高级功能" + } + + # 核心系统组件 + self.core_system = None + self.network_system = None + self.lod_system = None + self.cache_manager = None + self.is_enabled = False + self.update_task = None + + print(f"高级骨骼动画插件初始化: {self.name}") + + def initialize(self) -> bool: + """ + 初始化插件 + :return: 是否初始化成功 + """ + try: + world = self.plugin_manager.world + + # 创建核心动画系统 + self.core_system = SkeletalAnimationCore(world) + + # 创建网络同步系统 + self.network_system = AnimationNetworkSync(world, self.core_system) + + # 创建LOD系统 + self.lod_system = AnimationLODSystem(world) + + # 创建缓存管理器 + self.cache_manager = AnimationCacheManager() + + print(f"高级骨骼动画插件初始化成功: {self.name}") + return True + + except Exception as e: + print(f"高级骨骼动画插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + :return: 是否启用成功 + """ + if not super().enable(): + return False + + if self.is_enabled: + print(f"高级骨骼动画插件已启用: {self.name}") + return True + + try: + # 启动核心系统更新 + self.core_system.start_updating() + + # 启动网络同步 + self.network_system.start_sync_task() + + # 启动更新任务 + self.update_task = self.plugin_manager.world.taskMgr.add( + self._update_task, + f"advanced_skeletal_animation_update_{self.name}", + sort=30 + ) + + # 标记为已启用 + self.is_enabled = True + + print(f"高级骨骼动画插件启用成功: {self.name}") + return True + + except Exception as e: + print(f"高级骨骼动画插件启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self) -> bool: + """ + 禁用插件 + :return: 是否禁用成功 + """ + if not super().disable(): + return False + + if not self.is_enabled: + print(f"高级骨骼动画插件已禁用: {self.name}") + return True + + try: + # 停止更新任务 + if self.update_task: + self.plugin_manager.world.taskMgr.remove(self.update_task) + self.update_task = None + + # 停止核心系统更新 + self.core_system.stop_updating() + + # 停止网络同步 + self.network_system.stop_sync_task() + + # 标记为已禁用 + self.is_enabled = False + + print(f"高级骨骼动画插件禁用成功: {self.name}") + return True + + except Exception as e: + print(f"高级骨骼动画插件禁用失败: {e}") + import traceback + traceback.print_exc() + return False + + def finalize(self) -> None: + """ + 清理插件资源 + """ + print(f"清理高级骨骼动画插件资源: {self.name}") + + try: + # 停止更新任务 + if self.update_task: + self.plugin_manager.world.taskMgr.remove(self.update_task) + self.update_task = None + + # 停止核心系统 + if self.core_system: + self.core_system.stop_updating() + + # 停止网络同步 + if self.network_system: + self.network_system.stop_sync_task() + + # 清理资源 + self.core_system = None + self.network_system = None + self.lod_system = None + self.cache_manager = None + + print(f"高级骨骼动画插件资源清理完成: {self.name}") + + except Exception as e: + print(f"清理高级骨骼动画插件资源失败: {e}") + import traceback + traceback.print_exc() + + def _update_task(self, task) -> int: + """ + 插件更新任务 + :param task: 任务对象 + :return: 任务状态 + """ + try: + dt = globalClock.getDt() + + # 更新核心系统 + if self.core_system: + self.core_system.update(dt) + + # 更新LOD系统 + if self.lod_system and self.core_system: + self.lod_system.update_lod_levels(self.core_system.actors) + + # 更新网络同步 + # (网络同步有独立的任务) + + except Exception as e: + print(f"插件更新任务出错: {e}") + + return task.cont + + # 核心系统访问方法 + def get_core_system(self): + """ + 获取核心动画系统 + :return: SkeletalAnimationCore实例 + """ + return self.core_system + + def get_network_system(self): + """ + 获取网络同步系统 + :return: AnimationNetworkSync实例 + """ + return self.network_system + + def get_lod_system(self): + """ + 获取LOD系统 + :return: AnimationLODSystem实例 + """ + return self.lod_system + + def get_cache_manager(self): + """ + 获取缓存管理器 + :return: AnimationCacheManager实例 + """ + return self.cache_manager + + def get_analyzer(self): + """ + 获取动画分析器 + :return: AnimationAnalyzer实例 + """ + return self.core_system.get_analyzer() if self.core_system else None + + def get_editor(self): + """ + 获取动画编辑器 + :return: AnimationEditor实例 + """ + return self.core_system.get_editor() if self.core_system else None + + def get_visualizer(self): + """ + 获取调试可视化工具 + :return: DebugVisualizer实例 + """ + return self.core_system.get_visualizer() if self.core_system else None + + def get_profiler(self): + """ + 获取性能分析器 + :return: AnimationProfiler实例 + """ + return self.core_system.get_profiler() if self.core_system else None + +# 兼容性接口 +Plugin = AdvancedSkeletalAnimationPlugin + +__all__ = [ + 'AdvancedSkeletalAnimationPlugin', + 'Plugin', + 'SkeletalAnimationCore', + 'AnimationNetworkSync', + 'AnimationStateMachine', + 'AnimationCurveEditor', + 'PhysicsAnimationIntegration', + 'AnimationCompressor', + 'AnimationLODSystem', + 'AnimationCacheManager', + 'AnimationAnalyzer', + 'AnimationEditor', + 'DebugVisualizer', + 'AnimationProfiler' ] \ No newline at end of file diff --git a/plugins/user/advanced_skeletal_animation/tools.py b/plugins/user/advanced_skeletal_animation/tools.py index 1a81faf6..2988f1d0 100644 --- a/plugins/user/advanced_skeletal_animation/tools.py +++ b/plugins/user/advanced_skeletal_animation/tools.py @@ -1,1019 +1,1019 @@ -""" -高级骨骼动画系统 - 工具模块 -提供动画编辑、调试和分析工具 -""" - -import math -import json -from typing import Dict, List, Optional, Tuple, Any, Callable -from dataclasses import dataclass, field -from enum import Enum - -from panda3d.core import * -from direct.actor.Actor import Actor -from direct.interval.IntervalGlobal import * - -# 定义常量 -TOOL_SETTINGS = { - 'analyzer_update_rate': 10, - 'debug_visualization_enabled': True, - 'profiler_sampling_rate': 60 -} - -# 枚举定义 -class AnimationAnalysisType(Enum): - """动画分析类型枚举""" - PERFORMANCE = 0 - QUALITY = 1 - MEMORY = 2 - COMPRESSION = 3 - -class DebugVisualizationMode(Enum): - """调试可视化模式枚举""" - SKELETON = 0 - BOUNDS = 1 - NORMALS = 2 - WIREFRAME = 3 - COLLISION = 4 - -class AnimationEditMode(Enum): - """动画编辑模式枚举""" - VIEW = 0 - EDIT_KEYFRAME = 1 - EDIT_CURVE = 2 - EDIT_BONE = 3 - -@dataclass -class AnimationAnalysisResult: - """动画分析结果数据类""" - actor_id: str - anim_name: str - analysis_type: AnimationAnalysisType - metrics: Dict[str, Any] - timestamp: float - recommendations: List[str] = field(default_factory=list) - -@dataclass -class AnimationEditorState: - """动画编辑器状态数据类""" - actor_id: str - anim_name: str - current_frame: int - selected_bones: List[str] - edit_mode: AnimationEditMode - keyframe_selection: List[int] - curve_editor_state: Dict[str, Any] - -@dataclass -class PerformanceMetrics: - """性能指标数据类""" - fps: float - frame_time: float - animation_update_time: float - memory_usage: int - actor_count: int - active_animations: int - -class AnimationAnalyzer: - """ - 动画分析器 - 提供动画性能、质量和内存使用分析 - """ - - def __init__(self, core_system): - """ - 初始化动画分析器 - :param core_system: 核心动画系统 - """ - self.core_system = core_system - self.analysis_history: List[AnimationAnalysisResult] = [] - self.is_analyzing = False - self.update_rate = TOOL_SETTINGS['analyzer_update_rate'] - self.last_update_time = 0.0 - - print("动画分析器初始化完成") - - def start_analysis(self) -> None: - """ - 开始分析 - """ - self.is_analyzing = True - print("动画分析已启动") - - def stop_analysis(self) -> None: - """ - 停止分析 - """ - self.is_analyzing = False - print("动画分析已停止") - - def analyze_animation(self, actor_id: str, anim_name: str, - analysis_type: AnimationAnalysisType = AnimationAnalysisType.PERFORMANCE) -> Optional[AnimationAnalysisResult]: - """ - 分析指定动画 - :param actor_id: Actor ID - :param anim_name: 动画名称 - :param analysis_type: 分析类型 - :return: 分析结果 - """ - if actor_id not in self.core_system.actors: - print(f"错误: Actor {actor_id} 不存在") - return None - - actor_info = self.core_system.actors[actor_id] - actor = actor_info.actor - - if anim_name not in actor.getAnimNames(): - print(f"错误: 动画 {anim_name} 不存在于Actor {actor_id} 中") - return None - - metrics = {} - recommendations = [] - - # 根据分析类型执行不同的分析 - if analysis_type == AnimationAnalysisType.PERFORMANCE: - result = self._analyze_performance(actor_info, anim_name) - metrics.update(result[0]) - recommendations.extend(result[1]) - elif analysis_type == AnimationAnalysisType.QUALITY: - result = self._analyze_quality(actor_info, anim_name) - metrics.update(result[0]) - recommendations.extend(result[1]) - elif analysis_type == AnimationAnalysisType.MEMORY: - result = self._analyze_memory(actor_info, anim_name) - metrics.update(result[0]) - recommendations.extend(result[1]) - elif analysis_type == AnimationAnalysisType.COMPRESSION: - result = self._analyze_compression(actor_info, anim_name) - metrics.update(result[0]) - recommendations.extend(result[1]) - - # 创建分析结果 - analysis_result = AnimationAnalysisResult( - actor_id=actor_id, - anim_name=anim_name, - analysis_type=analysis_type, - metrics=metrics, - timestamp=globalClock.getRealTime(), - recommendations=recommendations - ) - - # 添加到历史记录 - self.analysis_history.append(analysis_result) - if len(self.analysis_history) > 100: # 限制历史记录大小 - self.analysis_history.pop(0) - - return analysis_result - - def _analyze_performance(self, actor_info: Any, anim_name: str) -> Tuple[Dict[str, Any], List[str]]: - """ - 分析动画性能 - :param actor_info: Actor信息 - :param anim_name: 动画名称 - :return: (指标, 建议) - """ - actor = actor_info.actor - metrics = {} - recommendations = [] - - # 获取动画控制 - control = actor.getAnimControl(anim_name) - if control: - # 基本性能指标 - metrics['frame_rate'] = actor.getFrameRate(anim_name) - metrics['num_frames'] = actor.getNumFrames(anim_name) - metrics['duration'] = control.getDuration() - metrics['is_looping'] = control.getLoop() - - # 性能建议 - if metrics['frame_rate'] and metrics['frame_rate'] < 15: - recommendations.append("动画帧率过低,建议优化关键帧") - - if metrics['num_frames'] and metrics['num_frames'] > 1000: - recommendations.append("动画帧数过多,考虑分割或简化") - - # 骨骼数量分析 - bundle = actor.getPartBundleDict().get('modelRoot') - if bundle: - bone_count = bundle.getNumChildren() - metrics['bone_count'] = bone_count - - if bone_count > 100: - recommendations.append("骨骼数量较多,可能影响性能") - - return metrics, recommendations - - def _analyze_quality(self, actor_info: Any, anim_name: str) -> Tuple[Dict[str, Any], List[str]]: - """ - 分析动画质量 - :param actor_info: Actor信息 - :param anim_name: 动画名称 - :return: (指标, 建议) - """ - actor = actor_info.actor - metrics = {} - recommendations = [] - - # 检查动画是否存在突变 - prev_frame_data = None - large_changes = 0 - - control = actor.getAnimControl(anim_name) - if control: - num_frames = control.getNumFrames() - for frame in range(min(100, num_frames)): # 采样前100帧 - actor.pose(anim_name, frame) - - # 检查所有骨骼的变化 - bundle = actor.getPartBundleDict().get('modelRoot') - if bundle: - for i in range(min(10, bundle.getNumChildren())): # 检查前10个骨骼 - child = bundle.getChild(i) - if hasattr(child, 'getName'): - bone_name = child.getName() - joint = actor.exposeJoint(None, "modelRoot", bone_name) - if joint: - current_pos = joint.getPos() - if prev_frame_data and bone_name in prev_frame_data: - prev_pos = prev_frame_data[bone_name] - distance = (current_pos - prev_pos).length() - if distance > 1.0: # 阈值 - large_changes += 1 - - if bone_name not in prev_frame_data: - prev_frame_data = {} - prev_frame_data[bone_name] = current_pos - - metrics['large_position_changes'] = large_changes - - if large_changes > 10: - recommendations.append("检测到大量位置突变,可能存在动画质量问题") - - return metrics, recommendations - - def _analyze_memory(self, actor_info: Any, anim_name: str) -> Tuple[Dict[str, Any], List[str]]: - """ - 分析动画内存使用 - :param actor_info: Actor信息 - :param anim_name: 动画名称 - :return: (指标, 建议) - """ - actor = actor_info.actor - metrics = {} - recommendations = [] - - # 估算内存使用 (简化) - control = actor.getAnimControl(anim_name) - if control: - num_frames = control.getNumFrames() - bundle = actor.getPartBundleDict().get('modelRoot') - bone_count = bundle.getNumChildren() if bundle else 0 - - # 简化的内存估算公式 - estimated_memory = num_frames * bone_count * 64 # 每个骨骼关键帧约64字节 - metrics['estimated_memory_bytes'] = estimated_memory - metrics['estimated_memory_kb'] = estimated_memory / 1024 - - if estimated_memory > 1024 * 1024: # 超过1MB - recommendations.append("动画内存使用较大,考虑压缩或简化") - - return metrics, recommendations - - def _analyze_compression(self, actor_info: Any, anim_name: str) -> Tuple[Dict[str, Any], List[str]]: - """ - 分析动画压缩潜力 - :param actor_info: Actor信息 - :param anim_name: 动画名称 - :return: (指标, 建议) - """ - actor = actor_info.actor - metrics = {} - recommendations = [] - - # 检查关键帧密度 - control = actor.getAnimControl(anim_name) - if control: - num_frames = control.getNumFrames() - duration = control.getDuration() - - if duration > 0: - fps = num_frames / duration - metrics['actual_fps'] = fps - - # 检查是否可以降低帧率 - if fps > 30: - metrics['recommended_fps'] = 30 - recommendations.append(f"可以将帧率从{fps:.1f}降低到30以节省内存") - elif fps > 15: - metrics['recommended_fps'] = 15 - recommendations.append(f"可以将帧率从{fps:.1f}降低到15以节省内存") - - return metrics, recommendations - - def get_actor_analysis_summary(self, actor_id: str) -> Optional[Dict[str, Any]]: - """ - 获取Actor分析摘要 - :param actor_id: Actor ID - :return: 分析摘要 - """ - if actor_id not in self.core_system.actors: - return None - - # 获取最新的分析结果 - actor_analyses = [a for a in self.analysis_history if a.actor_id == actor_id] - if not actor_analyses: - return None - - # 统计各类分析结果 - performance_issues = len([a for a in actor_analyses - if a.analysis_type == AnimationAnalysisType.PERFORMANCE and a.recommendations]) - quality_issues = len([a for a in actor_analyses - if a.analysis_type == AnimationAnalysisType.QUALITY and a.recommendations]) - memory_issues = len([a for a in actor_analyses - if a.analysis_type == AnimationAnalysisType.MEMORY and a.recommendations]) - - return { - 'actor_id': actor_id, - 'total_analyses': len(actor_analyses), - 'performance_issues': performance_issues, - 'quality_issues': quality_issues, - 'memory_issues': memory_issues, - 'last_analysis_time': actor_analyses[-1].timestamp if actor_analyses else 0 - } - - def get_system_analysis_summary(self) -> Dict[str, Any]: - """ - 获取系统分析摘要 - :return: 分析摘要 - """ - total_analyses = len(self.analysis_history) - actors_with_issues = len(set(a.actor_id for a in self.analysis_history if a.recommendations)) - - return { - 'total_analyses': total_analyses, - 'actors_analyzed': len(set(a.actor_id for a in self.analysis_history)), - 'actors_with_issues': actors_with_issues, - 'performance_analyses': len([a for a in self.analysis_history - if a.analysis_type == AnimationAnalysisType.PERFORMANCE]), - 'quality_analyses': len([a for a in self.analysis_history - if a.analysis_type == AnimationAnalysisType.QUALITY]), - 'memory_analyses': len([a for a in self.analysis_history - if a.analysis_type == AnimationAnalysisType.MEMORY]) - } - -class AnimationEditor: - """ - 动画编辑器 - 提供关键帧编辑、曲线编辑等功能 - """ - - def __init__(self, core_system): - """ - 初始化动画编辑器 - :param core_system: 核心动画系统 - """ - self.core_system = core_system - self.editor_states: Dict[str, AnimationEditorState] = {} - self.is_editing = False - self.edit_callbacks: List[Callable] = [] - - print("动画编辑器初始化完成") - - def start_editing(self, actor_id: str, anim_name: str) -> bool: - """ - 开始编辑动画 - :param actor_id: Actor ID - :param anim_name: 动画名称 - :return: 是否成功开始编辑 - """ - if actor_id not in self.core_system.actors: - print(f"错误: Actor {actor_id} 不存在") - return False - - actor_info = self.core_system.actors[actor_id] - actor = actor_info.actor - - if anim_name not in actor.getAnimNames(): - print(f"错误: 动画 {anim_name} 不存在于Actor {actor_id} 中") - return False - - # 创建编辑器状态 - editor_state = AnimationEditorState( - actor_id=actor_id, - anim_name=anim_name, - current_frame=0, - selected_bones=[], - edit_mode=AnimationEditMode.VIEW, - keyframe_selection=[], - curve_editor_state={} - ) - - self.editor_states[f"{actor_id}:{anim_name}"] = editor_state - self.is_editing = True - - print(f"开始编辑动画: {anim_name} (Actor: {actor_id})") - return True - - def stop_editing(self, actor_id: str, anim_name: str) -> None: - """ - 停止编辑动画 - :param actor_id: Actor ID - :param anim_name: 动画名称 - """ - key = f"{actor_id}:{anim_name}" - if key in self.editor_states: - del self.editor_states[key] - - if not self.editor_states: - self.is_editing = False - - print(f"停止编辑动画: {anim_name} (Actor: {actor_id})") - - def set_edit_mode(self, actor_id: str, anim_name: str, mode: AnimationEditMode) -> None: - """ - 设置编辑模式 - :param actor_id: Actor ID - :param anim_name: 动画名称 - :param mode: 编辑模式 - """ - key = f"{actor_id}:{anim_name}" - if key in self.editor_states: - self.editor_states[key].edit_mode = mode - print(f"设置编辑模式为: {mode.name} (Actor: {actor_id}, 动画: {anim_name})") - - def select_bones(self, actor_id: str, anim_name: str, bone_names: List[str]) -> None: - """ - 选择骨骼 - :param actor_id: Actor ID - :param anim_name: 动画名称 - :param bone_names: 骨骼名称列表 - """ - key = f"{actor_id}:{anim_name}" - if key in self.editor_states: - self.editor_states[key].selected_bones = bone_names - print(f"选择骨骼: {bone_names} (Actor: {actor_id}, 动画: {anim_name})") - - def set_current_frame(self, actor_id: str, anim_name: str, frame: int) -> None: - """ - 设置当前帧 - :param actor_id: Actor ID - :param anim_name: 动画名称 - :param frame: 帧号 - """ - key = f"{actor_id}:{anim_name}" - if key in self.editor_states: - editor_state = self.editor_states[key] - editor_state.current_frame = frame - - # 定位到指定帧 - actor_info = self.core_system.actors[actor_id] - actor_info.actor.pose(anim_name, frame) - - print(f"设置当前帧为: {frame} (Actor: {actor_id}, 动画: {anim_name})") - - def add_keyframe(self, actor_id: str, anim_name: str, bone_name: str, - frame: int, position: Optional[Tuple[float, float, float]] = None, - rotation: Optional[Tuple[float, float, float]] = None, - scale: Optional[Tuple[float, float, float]] = None) -> None: - """ - 添加关键帧 - :param actor_id: Actor ID - :param anim_name: 动画名称 - :param bone_name: 骨骼名称 - :param frame: 帧号 - :param position: 位置 - :param rotation: 旋转 - :param scale: 缩放 - """ - # 这是一个简化的实现,实际项目中需要直接操作动画数据 - print(f"添加关键帧: 骨骼={bone_name}, 帧={frame} (Actor: {actor_id}, 动画: {anim_name})") - print(f" 位置: {position}") - print(f" 旋转: {rotation}") - print(f" 缩放: {scale}") - - # 触发编辑回调 - for callback in self.edit_callbacks: - try: - callback('add_keyframe', { - 'actor_id': actor_id, - 'anim_name': anim_name, - 'bone_name': bone_name, - 'frame': frame, - 'position': position, - 'rotation': rotation, - 'scale': scale - }) - except Exception as e: - print(f"执行编辑回调失败: {e}") - - def remove_keyframe(self, actor_id: str, anim_name: str, bone_name: str, frame: int) -> None: - """ - 移除关键帧 - :param actor_id: Actor ID - :param anim_name: 动画名称 - :param bone_name: 骨骼名称 - :param frame: 帧号 - """ - print(f"移除关键帧: 骨骼={bone_name}, 帧={frame} (Actor: {actor_id}, 动画: {anim_name})") - - # 触发编辑回调 - for callback in self.edit_callbacks: - try: - callback('remove_keyframe', { - 'actor_id': actor_id, - 'anim_name': anim_name, - 'bone_name': bone_name, - 'frame': frame - }) - except Exception as e: - print(f"执行编辑回调失败: {e}") - - def modify_keyframe(self, actor_id: str, anim_name: str, bone_name: str, frame: int, - position: Optional[Tuple[float, float, float]] = None, - rotation: Optional[Tuple[float, float, float]] = None, - scale: Optional[Tuple[float, float, float]] = None) -> None: - """ - 修改关键帧 - :param actor_id: Actor ID - :param anim_name: 动画名称 - :param bone_name: 骨骼名称 - :param frame: 帧号 - :param position: 新位置 - :param rotation: 新旋转 - :param scale: 新缩放 - """ - print(f"修改关键帧: 骨骼={bone_name}, 帧={frame} (Actor: {actor_id}, 动画: {anim_name})") - print(f" 新位置: {position}") - print(f" 新旋转: {rotation}") - print(f" 新缩放: {scale}") - - # 触发编辑回调 - for callback in self.edit_callbacks: - try: - callback('modify_keyframe', { - 'actor_id': actor_id, - 'anim_name': anim_name, - 'bone_name': bone_name, - 'frame': frame, - 'position': position, - 'rotation': rotation, - 'scale': scale - }) - except Exception as e: - print(f"执行编辑回调失败: {e}") - - def add_edit_callback(self, callback: Callable) -> None: - """ - 添加编辑回调 - :param callback: 回调函数 - """ - self.edit_callbacks.append(callback) - print("添加编辑回调") - - def remove_edit_callback(self, callback: Callable) -> None: - """ - 移除编辑回调 - :param callback: 回调函数 - """ - if callback in self.edit_callbacks: - self.edit_callbacks.remove(callback) - print("移除编辑回调") - -class DebugVisualizer: - """ - 调试可视化工具 - 提供骨骼、边界框等可视化功能 - """ - - def __init__(self, world): - """ - 初始化调试可视化工具 - :param world: 世界对象 - """ - self.world = world - self.visualizations: Dict[str, NodePath] = {} - self.enabled = TOOL_SETTINGS['debug_visualization_enabled'] - self.mode = DebugVisualizationMode.SKELETON - - print("调试可视化工具初始化完成") - - def set_mode(self, mode: DebugVisualizationMode) -> None: - """ - 设置可视化模式 - :param mode: 模式 - """ - self.mode = mode - print(f"调试可视化模式设置为: {mode.name}") - - def enable_visualization(self, enabled: bool = True) -> None: - """ - 启用/禁用可视化 - :param enabled: 是否启用 - """ - self.enabled = enabled - if not enabled: - self.clear_all_visualizations() - print(f"调试可视化已{'启用' if enabled else '禁用'}") - - def visualize_actor_skeleton(self, actor_id: str, actor: Actor) -> None: - """ - 可视化Actor骨骼 - :param actor_id: Actor ID - :param actor: Actor对象 - """ - if not self.enabled or self.mode != DebugVisualizationMode.SKELETON: - return - - # 清除旧的可视化 - self.clear_visualization(actor_id) - - # 创建骨骼可视化节点 - skeleton_node = self.world.render.attachNewNode(f"skeleton_vis_{actor_id}") - self.visualizations[actor_id] = skeleton_node - - # 获取骨骼信息并绘制连线 - bundle = actor.getPartBundleDict().get('modelRoot') - if bundle: - self._draw_skeleton_hierarchy(actor, bundle, skeleton_node) - - def _draw_skeleton_hierarchy(self, actor: Actor, bundle, parent_node: NodePath) -> None: - """ - 绘制骨骼层级 - :param actor: Actor对象 - :param bundle: 骨骼包 - :param parent_node: 父节点 - """ - # 这里简化实现,实际项目中需要遍历骨骼树并绘制连线 - # 可以使用LineSegs或其他Panda3D绘图功能 - - # 示例:绘制一个简单的可视化表示 - from panda3d.core import LineSegs - - segs = LineSegs(f"skeleton_lines_{actor.getName()}") - segs.setColor(0, 1, 0, 1) # 绿色 - segs.setThickness(2.0) - - # 从根节点到各个子节点绘制线段(简化) - root_pos = Point3(0, 0, 0) - segs.moveTo(root_pos) - - for i in range(min(5, bundle.getNumChildren())): # 只绘制前5个骨骼 - child = bundle.getChild(i) - if hasattr(child, 'getName'): - bone_name = child.getName() - joint = actor.exposeJoint(None, "modelRoot", bone_name) - if joint: - joint_pos = joint.getPos() - segs.drawTo(joint_pos) - segs.moveTo(root_pos) - - # 创建节点并附加到场景 - lines_node = segs.create() - np = parent_node.attachNewNode(lines_node) - np.setBin("fixed", 40) - np.setDepthTest(False) - np.setDepthWrite(False) - - def visualize_actor_bounds(self, actor_id: str, actor: Actor) -> None: - """ - 可视化Actor边界框 - :param actor_id: Actor ID - :param actor: Actor对象 - """ - if not self.enabled or self.mode != DebugVisualizationMode.BOUNDS: - return - - # 清除旧的可视化 - self.clear_visualization(actor_id) - - # 获取边界框 - bounds = actor.getTightBounds() - if bounds: - min_point, max_point = bounds - - # 创建边界框可视化 - from panda3d.core import LineSegs - - segs = LineSegs(f"bounds_vis_{actor_id}") - segs.setColor(1, 0, 0, 1) # 红色 - segs.setThickness(1.0) - - # 绘制立方体边界框 - self._draw_wire_cube(segs, min_point, max_point) - - # 创建节点并附加到场景 - bounds_node = self.world.render.attachNewNode(segs.create()) - bounds_node.setBin("fixed", 40) - bounds_node.setDepthTest(False) - bounds_node.setDepthWrite(False) - - self.visualizations[actor_id] = bounds_node - - def _draw_wire_cube(self, segs: LineSegs, min_point: Point3, max_point: Point3) -> None: - """ - 绘制线框立方体 - :param segs: LineSegs对象 - :param min_point: 最小点 - :param max_point: 最大点 - """ - # 获取立方体的8个顶点 - vertices = [ - Point3(min_point.x, min_point.y, min_point.z), - Point3(max_point.x, min_point.y, min_point.z), - Point3(max_point.x, max_point.y, min_point.z), - Point3(min_point.x, max_point.y, min_point.z), - Point3(min_point.x, min_point.y, max_point.z), - Point3(max_point.x, min_point.y, max_point.z), - Point3(max_point.x, max_point.y, max_point.z), - Point3(min_point.x, max_point.y, max_point.z) - ] - - # 绘制12条边 - edges = [ - (0, 1), (1, 2), (2, 3), (3, 0), # 底面 - (4, 5), (5, 6), (6, 7), (7, 4), # 顶面 - (0, 4), (1, 5), (2, 6), (3, 7) # 垂直边 - ] - - for start, end in edges: - segs.moveTo(vertices[start]) - segs.drawTo(vertices[end]) - - def clear_visualization(self, actor_id: str) -> None: - """ - 清除指定Actor的可视化 - :param actor_id: Actor ID - """ - if actor_id in self.visualizations: - self.visualizations[actor_id].removeNode() - del self.visualizations[actor_id] - - def clear_all_visualizations(self) -> None: - """ - 清除所有可视化 - """ - for np in self.visualizations.values(): - np.removeNode() - self.visualizations.clear() - - def update_visualizations(self, actors: Dict[str, Any]) -> None: - """ - 更新所有可视化 - :param actors: Actor字典 - """ - if not self.enabled: - return - - for actor_id, actor_info in actors.items(): - actor = actor_info.actor - - if self.mode == DebugVisualizationMode.SKELETON: - self.visualize_actor_skeleton(actor_id, actor) - elif self.mode == DebugVisualizationMode.BOUNDS: - self.visualize_actor_bounds(actor_id, actor) - -class AnimationProfiler: - """ - 动画性能分析器 - 提供详细的性能分析和优化建议 - """ - - def __init__(self, core_system): - """ - 初始化动画性能分析器 - :param core_system: 核心动画系统 - """ - self.core_system = core_system - self.sampling_rate = TOOL_SETTINGS['profiler_sampling_rate'] - self.is_profiling = False - self.profile_data: List[PerformanceMetrics] = [] - self.last_sample_time = 0.0 - - print("动画性能分析器初始化完成") - - def start_profiling(self) -> None: - """ - 开始性能分析 - """ - self.is_profiling = True - print("动画性能分析已启动") - - def stop_profiling(self) -> None: - """ - 停止性能分析 - """ - self.is_profiling = False - print("动画性能分析已停止") - - def sample_performance(self) -> None: - """ - 采样性能数据 - """ - if not self.is_profiling: - return - - current_time = globalClock.getRealTime() - if current_time - self.last_sample_time < 1.0 / self.sampling_rate: - return - - self.last_sample_time = current_time - - # 收集性能指标 - metrics = self._collect_metrics() - self.profile_data.append(metrics) - - # 限制数据大小 - if len(self.profile_data) > 1000: - self.profile_data.pop(0) - - def _collect_metrics(self) -> PerformanceMetrics: - """ - 收集性能指标 - :return: 性能指标 - """ - # 获取FPS和帧时间 - fps = globalClock.getAverageFrameRate() - frame_time = globalClock.getDt() - - # 估算动画更新时间(简化) - animation_update_time = frame_time * 0.3 # 假设动画更新占总帧时间的30% - - # 估算内存使用(简化) - memory_usage = len(self.core_system.actors) * 1024 * 1024 # 假设每个Actor约1MB - - # 统计Actor和动画数量 - actor_count = len(self.core_system.actors) - active_animations = sum(len(info.active_animations) for info in self.core_system.actors.values()) - - return PerformanceMetrics( - fps=fps, - frame_time=frame_time, - animation_update_time=animation_update_time, - memory_usage=memory_usage, - actor_count=actor_count, - active_animations=active_animations - ) - - def get_performance_report(self) -> Dict[str, Any]: - """ - 获取性能报告 - :return: 性能报告 - """ - if not self.profile_data: - return {} - - # 计算统计数据 - fps_values = [m.fps for m in self.profile_data] - frame_time_values = [m.frame_time for m in self.profile_data] - update_time_values = [m.animation_update_time for m in self.profile_data] - memory_values = [m.memory_usage for m in self.profile_data] - - return { - 'sample_count': len(self.profile_data), - 'duration_seconds': len(self.profile_data) / self.sampling_rate, - 'fps': { - 'average': sum(fps_values) / len(fps_values), - 'min': min(fps_values), - 'max': max(fps_values), - 'std_dev': self._calculate_std_dev(fps_values) - }, - 'frame_time': { - 'average': sum(frame_time_values) / len(frame_time_values), - 'min': min(frame_time_values), - 'max': max(frame_time_values), - 'std_dev': self._calculate_std_dev(frame_time_values) - }, - 'animation_update_time': { - 'average': sum(update_time_values) / len(update_time_values), - 'min': min(update_time_values), - 'max': max(update_time_values), - 'std_dev': self._calculate_std_dev(update_time_values) - }, - 'memory_usage': { - 'average': sum(memory_values) / len(memory_values), - 'min': min(memory_values), - 'max': max(memory_values), - 'current': memory_values[-1] if memory_values else 0 - }, - 'actor_stats': { - 'current_actor_count': self.profile_data[-1].actor_count if self.profile_data else 0, - 'current_active_animations': self.profile_data[-1].active_animations if self.profile_data else 0 - } - } - - def _calculate_std_dev(self, values: List[float]) -> float: - """ - 计算标准差 - :param values: 数值列表 - :return: 标准差 - """ - if len(values) < 2: - return 0.0 - - mean = sum(values) / len(values) - variance = sum((x - mean) ** 2 for x in values) / (len(values) - 1) - return math.sqrt(variance) - - def get_optimization_recommendations(self) -> List[str]: - """ - 获取优化建议 - :return: 优化建议列表 - """ - if not self.profile_data: - return [] - - latest = self.profile_data[-1] - recommendations = [] - - # FPS相关建议 - if latest.fps < 30: - recommendations.append("FPS过低,建议优化动画系统或减少同时播放的动画数量") - - # 内存相关建议 - if latest.memory_usage > 512 * 1024 * 1024: # 超过512MB - recommendations.append("内存使用过高,建议检查Actor加载和卸载逻辑") - - # Actor数量建议 - if latest.actor_count > 100: - recommendations.append("Actor数量较多,建议使用LOD或视锥剔除优化") - - # 动画数量建议 - if latest.active_animations > 200: - recommendations.append("同时播放的动画过多,建议限制同时活动的动画数量") - - return recommendations - -# 使用示例和测试代码 -def example_analyzer_usage(core_system): - """ - 动画分析器使用示例 - """ - print("=== 动画分析器使用示例 ===") - - # 创建分析器 - analyzer = AnimationAnalyzer(core_system) - analyzer.start_analysis() - - # 分析示例(假设有Actor) - # 在实际使用中,这里会分析真实的Actor和动画 - print("动画分析器示例完成") - return analyzer - -def example_editor_usage(core_system): - """ - 动画编辑器使用示例 - """ - print("=== 动画编辑器使用示例 ===") - - # 创建编辑器 - editor = AnimationEditor(core_system) - - # 添加编辑回调 - def edit_callback(event_type, data): - print(f"编辑事件: {event_type}, 数据: {data}") - - editor.add_edit_callback(edit_callback) - - print("动画编辑器示例完成") - return editor - -def example_visualizer_usage(world): - """ - 调试可视化工具使用示例 - """ - print("=== 调试可视化工具使用示例 ===") - - # 创建可视化工具 - visualizer = DebugVisualizer(world) - visualizer.enable_visualization(True) - visualizer.set_mode(DebugVisualizationMode.SKELETON) - - print("调试可视化工具示例完成") - return visualizer - -def example_profiler_usage(core_system): - """ - 动画性能分析器使用示例 - """ - print("=== 动画性能分析器使用示例 ===") - - # 创建分析器 - profiler = AnimationProfiler(core_system) - profiler.start_profiling() - - # 采样性能数据 - profiler.sample_performance() - - # 获取性能报告 - report = profiler.get_performance_report() - print(f"性能报告: {report}") - - # 获取优化建议 - recommendations = profiler.get_optimization_recommendations() - print(f"优化建议: {recommendations}") - - print("动画性能分析器示例完成") - return profiler - -if __name__ == "__main__": +""" +高级骨骼动画系统 - 工具模块 +提供动画编辑、调试和分析工具 +""" + +import math +import json +from typing import Dict, List, Optional, Tuple, Any, Callable +from dataclasses import dataclass, field +from enum import Enum + +from panda3d.core import * +from direct.actor.Actor import Actor +from direct.interval.IntervalGlobal import * + +# 定义常量 +TOOL_SETTINGS = { + 'analyzer_update_rate': 10, + 'debug_visualization_enabled': True, + 'profiler_sampling_rate': 60 +} + +# 枚举定义 +class AnimationAnalysisType(Enum): + """动画分析类型枚举""" + PERFORMANCE = 0 + QUALITY = 1 + MEMORY = 2 + COMPRESSION = 3 + +class DebugVisualizationMode(Enum): + """调试可视化模式枚举""" + SKELETON = 0 + BOUNDS = 1 + NORMALS = 2 + WIREFRAME = 3 + COLLISION = 4 + +class AnimationEditMode(Enum): + """动画编辑模式枚举""" + VIEW = 0 + EDIT_KEYFRAME = 1 + EDIT_CURVE = 2 + EDIT_BONE = 3 + +@dataclass +class AnimationAnalysisResult: + """动画分析结果数据类""" + actor_id: str + anim_name: str + analysis_type: AnimationAnalysisType + metrics: Dict[str, Any] + timestamp: float + recommendations: List[str] = field(default_factory=list) + +@dataclass +class AnimationEditorState: + """动画编辑器状态数据类""" + actor_id: str + anim_name: str + current_frame: int + selected_bones: List[str] + edit_mode: AnimationEditMode + keyframe_selection: List[int] + curve_editor_state: Dict[str, Any] + +@dataclass +class PerformanceMetrics: + """性能指标数据类""" + fps: float + frame_time: float + animation_update_time: float + memory_usage: int + actor_count: int + active_animations: int + +class AnimationAnalyzer: + """ + 动画分析器 + 提供动画性能、质量和内存使用分析 + """ + + def __init__(self, core_system): + """ + 初始化动画分析器 + :param core_system: 核心动画系统 + """ + self.core_system = core_system + self.analysis_history: List[AnimationAnalysisResult] = [] + self.is_analyzing = False + self.update_rate = TOOL_SETTINGS['analyzer_update_rate'] + self.last_update_time = 0.0 + + print("动画分析器初始化完成") + + def start_analysis(self) -> None: + """ + 开始分析 + """ + self.is_analyzing = True + print("动画分析已启动") + + def stop_analysis(self) -> None: + """ + 停止分析 + """ + self.is_analyzing = False + print("动画分析已停止") + + def analyze_animation(self, actor_id: str, anim_name: str, + analysis_type: AnimationAnalysisType = AnimationAnalysisType.PERFORMANCE) -> Optional[AnimationAnalysisResult]: + """ + 分析指定动画 + :param actor_id: Actor ID + :param anim_name: 动画名称 + :param analysis_type: 分析类型 + :return: 分析结果 + """ + if actor_id not in self.core_system.actors: + print(f"错误: Actor {actor_id} 不存在") + return None + + actor_info = self.core_system.actors[actor_id] + actor = actor_info.actor + + if anim_name not in actor.getAnimNames(): + print(f"错误: 动画 {anim_name} 不存在于Actor {actor_id} 中") + return None + + metrics = {} + recommendations = [] + + # 根据分析类型执行不同的分析 + if analysis_type == AnimationAnalysisType.PERFORMANCE: + result = self._analyze_performance(actor_info, anim_name) + metrics.update(result[0]) + recommendations.extend(result[1]) + elif analysis_type == AnimationAnalysisType.QUALITY: + result = self._analyze_quality(actor_info, anim_name) + metrics.update(result[0]) + recommendations.extend(result[1]) + elif analysis_type == AnimationAnalysisType.MEMORY: + result = self._analyze_memory(actor_info, anim_name) + metrics.update(result[0]) + recommendations.extend(result[1]) + elif analysis_type == AnimationAnalysisType.COMPRESSION: + result = self._analyze_compression(actor_info, anim_name) + metrics.update(result[0]) + recommendations.extend(result[1]) + + # 创建分析结果 + analysis_result = AnimationAnalysisResult( + actor_id=actor_id, + anim_name=anim_name, + analysis_type=analysis_type, + metrics=metrics, + timestamp=globalClock.getRealTime(), + recommendations=recommendations + ) + + # 添加到历史记录 + self.analysis_history.append(analysis_result) + if len(self.analysis_history) > 100: # 限制历史记录大小 + self.analysis_history.pop(0) + + return analysis_result + + def _analyze_performance(self, actor_info: Any, anim_name: str) -> Tuple[Dict[str, Any], List[str]]: + """ + 分析动画性能 + :param actor_info: Actor信息 + :param anim_name: 动画名称 + :return: (指标, 建议) + """ + actor = actor_info.actor + metrics = {} + recommendations = [] + + # 获取动画控制 + control = actor.getAnimControl(anim_name) + if control: + # 基本性能指标 + metrics['frame_rate'] = actor.getFrameRate(anim_name) + metrics['num_frames'] = actor.getNumFrames(anim_name) + metrics['duration'] = control.getDuration() + metrics['is_looping'] = control.getLoop() + + # 性能建议 + if metrics['frame_rate'] and metrics['frame_rate'] < 15: + recommendations.append("动画帧率过低,建议优化关键帧") + + if metrics['num_frames'] and metrics['num_frames'] > 1000: + recommendations.append("动画帧数过多,考虑分割或简化") + + # 骨骼数量分析 + bundle = actor.getPartBundleDict().get('modelRoot') + if bundle: + bone_count = bundle.getNumChildren() + metrics['bone_count'] = bone_count + + if bone_count > 100: + recommendations.append("骨骼数量较多,可能影响性能") + + return metrics, recommendations + + def _analyze_quality(self, actor_info: Any, anim_name: str) -> Tuple[Dict[str, Any], List[str]]: + """ + 分析动画质量 + :param actor_info: Actor信息 + :param anim_name: 动画名称 + :return: (指标, 建议) + """ + actor = actor_info.actor + metrics = {} + recommendations = [] + + # 检查动画是否存在突变 + prev_frame_data = None + large_changes = 0 + + control = actor.getAnimControl(anim_name) + if control: + num_frames = control.getNumFrames() + for frame in range(min(100, num_frames)): # 采样前100帧 + actor.pose(anim_name, frame) + + # 检查所有骨骼的变化 + bundle = actor.getPartBundleDict().get('modelRoot') + if bundle: + for i in range(min(10, bundle.getNumChildren())): # 检查前10个骨骼 + child = bundle.getChild(i) + if hasattr(child, 'getName'): + bone_name = child.getName() + joint = actor.exposeJoint(None, "modelRoot", bone_name) + if joint: + current_pos = joint.getPos() + if prev_frame_data and bone_name in prev_frame_data: + prev_pos = prev_frame_data[bone_name] + distance = (current_pos - prev_pos).length() + if distance > 1.0: # 阈值 + large_changes += 1 + + if bone_name not in prev_frame_data: + prev_frame_data = {} + prev_frame_data[bone_name] = current_pos + + metrics['large_position_changes'] = large_changes + + if large_changes > 10: + recommendations.append("检测到大量位置突变,可能存在动画质量问题") + + return metrics, recommendations + + def _analyze_memory(self, actor_info: Any, anim_name: str) -> Tuple[Dict[str, Any], List[str]]: + """ + 分析动画内存使用 + :param actor_info: Actor信息 + :param anim_name: 动画名称 + :return: (指标, 建议) + """ + actor = actor_info.actor + metrics = {} + recommendations = [] + + # 估算内存使用 (简化) + control = actor.getAnimControl(anim_name) + if control: + num_frames = control.getNumFrames() + bundle = actor.getPartBundleDict().get('modelRoot') + bone_count = bundle.getNumChildren() if bundle else 0 + + # 简化的内存估算公式 + estimated_memory = num_frames * bone_count * 64 # 每个骨骼关键帧约64字节 + metrics['estimated_memory_bytes'] = estimated_memory + metrics['estimated_memory_kb'] = estimated_memory / 1024 + + if estimated_memory > 1024 * 1024: # 超过1MB + recommendations.append("动画内存使用较大,考虑压缩或简化") + + return metrics, recommendations + + def _analyze_compression(self, actor_info: Any, anim_name: str) -> Tuple[Dict[str, Any], List[str]]: + """ + 分析动画压缩潜力 + :param actor_info: Actor信息 + :param anim_name: 动画名称 + :return: (指标, 建议) + """ + actor = actor_info.actor + metrics = {} + recommendations = [] + + # 检查关键帧密度 + control = actor.getAnimControl(anim_name) + if control: + num_frames = control.getNumFrames() + duration = control.getDuration() + + if duration > 0: + fps = num_frames / duration + metrics['actual_fps'] = fps + + # 检查是否可以降低帧率 + if fps > 30: + metrics['recommended_fps'] = 30 + recommendations.append(f"可以将帧率从{fps:.1f}降低到30以节省内存") + elif fps > 15: + metrics['recommended_fps'] = 15 + recommendations.append(f"可以将帧率从{fps:.1f}降低到15以节省内存") + + return metrics, recommendations + + def get_actor_analysis_summary(self, actor_id: str) -> Optional[Dict[str, Any]]: + """ + 获取Actor分析摘要 + :param actor_id: Actor ID + :return: 分析摘要 + """ + if actor_id not in self.core_system.actors: + return None + + # 获取最新的分析结果 + actor_analyses = [a for a in self.analysis_history if a.actor_id == actor_id] + if not actor_analyses: + return None + + # 统计各类分析结果 + performance_issues = len([a for a in actor_analyses + if a.analysis_type == AnimationAnalysisType.PERFORMANCE and a.recommendations]) + quality_issues = len([a for a in actor_analyses + if a.analysis_type == AnimationAnalysisType.QUALITY and a.recommendations]) + memory_issues = len([a for a in actor_analyses + if a.analysis_type == AnimationAnalysisType.MEMORY and a.recommendations]) + + return { + 'actor_id': actor_id, + 'total_analyses': len(actor_analyses), + 'performance_issues': performance_issues, + 'quality_issues': quality_issues, + 'memory_issues': memory_issues, + 'last_analysis_time': actor_analyses[-1].timestamp if actor_analyses else 0 + } + + def get_system_analysis_summary(self) -> Dict[str, Any]: + """ + 获取系统分析摘要 + :return: 分析摘要 + """ + total_analyses = len(self.analysis_history) + actors_with_issues = len(set(a.actor_id for a in self.analysis_history if a.recommendations)) + + return { + 'total_analyses': total_analyses, + 'actors_analyzed': len(set(a.actor_id for a in self.analysis_history)), + 'actors_with_issues': actors_with_issues, + 'performance_analyses': len([a for a in self.analysis_history + if a.analysis_type == AnimationAnalysisType.PERFORMANCE]), + 'quality_analyses': len([a for a in self.analysis_history + if a.analysis_type == AnimationAnalysisType.QUALITY]), + 'memory_analyses': len([a for a in self.analysis_history + if a.analysis_type == AnimationAnalysisType.MEMORY]) + } + +class AnimationEditor: + """ + 动画编辑器 + 提供关键帧编辑、曲线编辑等功能 + """ + + def __init__(self, core_system): + """ + 初始化动画编辑器 + :param core_system: 核心动画系统 + """ + self.core_system = core_system + self.editor_states: Dict[str, AnimationEditorState] = {} + self.is_editing = False + self.edit_callbacks: List[Callable] = [] + + print("动画编辑器初始化完成") + + def start_editing(self, actor_id: str, anim_name: str) -> bool: + """ + 开始编辑动画 + :param actor_id: Actor ID + :param anim_name: 动画名称 + :return: 是否成功开始编辑 + """ + if actor_id not in self.core_system.actors: + print(f"错误: Actor {actor_id} 不存在") + return False + + actor_info = self.core_system.actors[actor_id] + actor = actor_info.actor + + if anim_name not in actor.getAnimNames(): + print(f"错误: 动画 {anim_name} 不存在于Actor {actor_id} 中") + return False + + # 创建编辑器状态 + editor_state = AnimationEditorState( + actor_id=actor_id, + anim_name=anim_name, + current_frame=0, + selected_bones=[], + edit_mode=AnimationEditMode.VIEW, + keyframe_selection=[], + curve_editor_state={} + ) + + self.editor_states[f"{actor_id}:{anim_name}"] = editor_state + self.is_editing = True + + print(f"开始编辑动画: {anim_name} (Actor: {actor_id})") + return True + + def stop_editing(self, actor_id: str, anim_name: str) -> None: + """ + 停止编辑动画 + :param actor_id: Actor ID + :param anim_name: 动画名称 + """ + key = f"{actor_id}:{anim_name}" + if key in self.editor_states: + del self.editor_states[key] + + if not self.editor_states: + self.is_editing = False + + print(f"停止编辑动画: {anim_name} (Actor: {actor_id})") + + def set_edit_mode(self, actor_id: str, anim_name: str, mode: AnimationEditMode) -> None: + """ + 设置编辑模式 + :param actor_id: Actor ID + :param anim_name: 动画名称 + :param mode: 编辑模式 + """ + key = f"{actor_id}:{anim_name}" + if key in self.editor_states: + self.editor_states[key].edit_mode = mode + print(f"设置编辑模式为: {mode.name} (Actor: {actor_id}, 动画: {anim_name})") + + def select_bones(self, actor_id: str, anim_name: str, bone_names: List[str]) -> None: + """ + 选择骨骼 + :param actor_id: Actor ID + :param anim_name: 动画名称 + :param bone_names: 骨骼名称列表 + """ + key = f"{actor_id}:{anim_name}" + if key in self.editor_states: + self.editor_states[key].selected_bones = bone_names + print(f"选择骨骼: {bone_names} (Actor: {actor_id}, 动画: {anim_name})") + + def set_current_frame(self, actor_id: str, anim_name: str, frame: int) -> None: + """ + 设置当前帧 + :param actor_id: Actor ID + :param anim_name: 动画名称 + :param frame: 帧号 + """ + key = f"{actor_id}:{anim_name}" + if key in self.editor_states: + editor_state = self.editor_states[key] + editor_state.current_frame = frame + + # 定位到指定帧 + actor_info = self.core_system.actors[actor_id] + actor_info.actor.pose(anim_name, frame) + + print(f"设置当前帧为: {frame} (Actor: {actor_id}, 动画: {anim_name})") + + def add_keyframe(self, actor_id: str, anim_name: str, bone_name: str, + frame: int, position: Optional[Tuple[float, float, float]] = None, + rotation: Optional[Tuple[float, float, float]] = None, + scale: Optional[Tuple[float, float, float]] = None) -> None: + """ + 添加关键帧 + :param actor_id: Actor ID + :param anim_name: 动画名称 + :param bone_name: 骨骼名称 + :param frame: 帧号 + :param position: 位置 + :param rotation: 旋转 + :param scale: 缩放 + """ + # 这是一个简化的实现,实际项目中需要直接操作动画数据 + print(f"添加关键帧: 骨骼={bone_name}, 帧={frame} (Actor: {actor_id}, 动画: {anim_name})") + print(f" 位置: {position}") + print(f" 旋转: {rotation}") + print(f" 缩放: {scale}") + + # 触发编辑回调 + for callback in self.edit_callbacks: + try: + callback('add_keyframe', { + 'actor_id': actor_id, + 'anim_name': anim_name, + 'bone_name': bone_name, + 'frame': frame, + 'position': position, + 'rotation': rotation, + 'scale': scale + }) + except Exception as e: + print(f"执行编辑回调失败: {e}") + + def remove_keyframe(self, actor_id: str, anim_name: str, bone_name: str, frame: int) -> None: + """ + 移除关键帧 + :param actor_id: Actor ID + :param anim_name: 动画名称 + :param bone_name: 骨骼名称 + :param frame: 帧号 + """ + print(f"移除关键帧: 骨骼={bone_name}, 帧={frame} (Actor: {actor_id}, 动画: {anim_name})") + + # 触发编辑回调 + for callback in self.edit_callbacks: + try: + callback('remove_keyframe', { + 'actor_id': actor_id, + 'anim_name': anim_name, + 'bone_name': bone_name, + 'frame': frame + }) + except Exception as e: + print(f"执行编辑回调失败: {e}") + + def modify_keyframe(self, actor_id: str, anim_name: str, bone_name: str, frame: int, + position: Optional[Tuple[float, float, float]] = None, + rotation: Optional[Tuple[float, float, float]] = None, + scale: Optional[Tuple[float, float, float]] = None) -> None: + """ + 修改关键帧 + :param actor_id: Actor ID + :param anim_name: 动画名称 + :param bone_name: 骨骼名称 + :param frame: 帧号 + :param position: 新位置 + :param rotation: 新旋转 + :param scale: 新缩放 + """ + print(f"修改关键帧: 骨骼={bone_name}, 帧={frame} (Actor: {actor_id}, 动画: {anim_name})") + print(f" 新位置: {position}") + print(f" 新旋转: {rotation}") + print(f" 新缩放: {scale}") + + # 触发编辑回调 + for callback in self.edit_callbacks: + try: + callback('modify_keyframe', { + 'actor_id': actor_id, + 'anim_name': anim_name, + 'bone_name': bone_name, + 'frame': frame, + 'position': position, + 'rotation': rotation, + 'scale': scale + }) + except Exception as e: + print(f"执行编辑回调失败: {e}") + + def add_edit_callback(self, callback: Callable) -> None: + """ + 添加编辑回调 + :param callback: 回调函数 + """ + self.edit_callbacks.append(callback) + print("添加编辑回调") + + def remove_edit_callback(self, callback: Callable) -> None: + """ + 移除编辑回调 + :param callback: 回调函数 + """ + if callback in self.edit_callbacks: + self.edit_callbacks.remove(callback) + print("移除编辑回调") + +class DebugVisualizer: + """ + 调试可视化工具 + 提供骨骼、边界框等可视化功能 + """ + + def __init__(self, world): + """ + 初始化调试可视化工具 + :param world: 世界对象 + """ + self.world = world + self.visualizations: Dict[str, NodePath] = {} + self.enabled = TOOL_SETTINGS['debug_visualization_enabled'] + self.mode = DebugVisualizationMode.SKELETON + + print("调试可视化工具初始化完成") + + def set_mode(self, mode: DebugVisualizationMode) -> None: + """ + 设置可视化模式 + :param mode: 模式 + """ + self.mode = mode + print(f"调试可视化模式设置为: {mode.name}") + + def enable_visualization(self, enabled: bool = True) -> None: + """ + 启用/禁用可视化 + :param enabled: 是否启用 + """ + self.enabled = enabled + if not enabled: + self.clear_all_visualizations() + print(f"调试可视化已{'启用' if enabled else '禁用'}") + + def visualize_actor_skeleton(self, actor_id: str, actor: Actor) -> None: + """ + 可视化Actor骨骼 + :param actor_id: Actor ID + :param actor: Actor对象 + """ + if not self.enabled or self.mode != DebugVisualizationMode.SKELETON: + return + + # 清除旧的可视化 + self.clear_visualization(actor_id) + + # 创建骨骼可视化节点 + skeleton_node = self.world.render.attachNewNode(f"skeleton_vis_{actor_id}") + self.visualizations[actor_id] = skeleton_node + + # 获取骨骼信息并绘制连线 + bundle = actor.getPartBundleDict().get('modelRoot') + if bundle: + self._draw_skeleton_hierarchy(actor, bundle, skeleton_node) + + def _draw_skeleton_hierarchy(self, actor: Actor, bundle, parent_node: NodePath) -> None: + """ + 绘制骨骼层级 + :param actor: Actor对象 + :param bundle: 骨骼包 + :param parent_node: 父节点 + """ + # 这里简化实现,实际项目中需要遍历骨骼树并绘制连线 + # 可以使用LineSegs或其他Panda3D绘图功能 + + # 示例:绘制一个简单的可视化表示 + from panda3d.core import LineSegs + + segs = LineSegs(f"skeleton_lines_{actor.getName()}") + segs.setColor(0, 1, 0, 1) # 绿色 + segs.setThickness(2.0) + + # 从根节点到各个子节点绘制线段(简化) + root_pos = Point3(0, 0, 0) + segs.moveTo(root_pos) + + for i in range(min(5, bundle.getNumChildren())): # 只绘制前5个骨骼 + child = bundle.getChild(i) + if hasattr(child, 'getName'): + bone_name = child.getName() + joint = actor.exposeJoint(None, "modelRoot", bone_name) + if joint: + joint_pos = joint.getPos() + segs.drawTo(joint_pos) + segs.moveTo(root_pos) + + # 创建节点并附加到场景 + lines_node = segs.create() + np = parent_node.attachNewNode(lines_node) + np.setBin("fixed", 40) + np.setDepthTest(False) + np.setDepthWrite(False) + + def visualize_actor_bounds(self, actor_id: str, actor: Actor) -> None: + """ + 可视化Actor边界框 + :param actor_id: Actor ID + :param actor: Actor对象 + """ + if not self.enabled or self.mode != DebugVisualizationMode.BOUNDS: + return + + # 清除旧的可视化 + self.clear_visualization(actor_id) + + # 获取边界框 + bounds = actor.getTightBounds() + if bounds: + min_point, max_point = bounds + + # 创建边界框可视化 + from panda3d.core import LineSegs + + segs = LineSegs(f"bounds_vis_{actor_id}") + segs.setColor(1, 0, 0, 1) # 红色 + segs.setThickness(1.0) + + # 绘制立方体边界框 + self._draw_wire_cube(segs, min_point, max_point) + + # 创建节点并附加到场景 + bounds_node = self.world.render.attachNewNode(segs.create()) + bounds_node.setBin("fixed", 40) + bounds_node.setDepthTest(False) + bounds_node.setDepthWrite(False) + + self.visualizations[actor_id] = bounds_node + + def _draw_wire_cube(self, segs: LineSegs, min_point: Point3, max_point: Point3) -> None: + """ + 绘制线框立方体 + :param segs: LineSegs对象 + :param min_point: 最小点 + :param max_point: 最大点 + """ + # 获取立方体的8个顶点 + vertices = [ + Point3(min_point.x, min_point.y, min_point.z), + Point3(max_point.x, min_point.y, min_point.z), + Point3(max_point.x, max_point.y, min_point.z), + Point3(min_point.x, max_point.y, min_point.z), + Point3(min_point.x, min_point.y, max_point.z), + Point3(max_point.x, min_point.y, max_point.z), + Point3(max_point.x, max_point.y, max_point.z), + Point3(min_point.x, max_point.y, max_point.z) + ] + + # 绘制12条边 + edges = [ + (0, 1), (1, 2), (2, 3), (3, 0), # 底面 + (4, 5), (5, 6), (6, 7), (7, 4), # 顶面 + (0, 4), (1, 5), (2, 6), (3, 7) # 垂直边 + ] + + for start, end in edges: + segs.moveTo(vertices[start]) + segs.drawTo(vertices[end]) + + def clear_visualization(self, actor_id: str) -> None: + """ + 清除指定Actor的可视化 + :param actor_id: Actor ID + """ + if actor_id in self.visualizations: + self.visualizations[actor_id].removeNode() + del self.visualizations[actor_id] + + def clear_all_visualizations(self) -> None: + """ + 清除所有可视化 + """ + for np in self.visualizations.values(): + np.removeNode() + self.visualizations.clear() + + def update_visualizations(self, actors: Dict[str, Any]) -> None: + """ + 更新所有可视化 + :param actors: Actor字典 + """ + if not self.enabled: + return + + for actor_id, actor_info in actors.items(): + actor = actor_info.actor + + if self.mode == DebugVisualizationMode.SKELETON: + self.visualize_actor_skeleton(actor_id, actor) + elif self.mode == DebugVisualizationMode.BOUNDS: + self.visualize_actor_bounds(actor_id, actor) + +class AnimationProfiler: + """ + 动画性能分析器 + 提供详细的性能分析和优化建议 + """ + + def __init__(self, core_system): + """ + 初始化动画性能分析器 + :param core_system: 核心动画系统 + """ + self.core_system = core_system + self.sampling_rate = TOOL_SETTINGS['profiler_sampling_rate'] + self.is_profiling = False + self.profile_data: List[PerformanceMetrics] = [] + self.last_sample_time = 0.0 + + print("动画性能分析器初始化完成") + + def start_profiling(self) -> None: + """ + 开始性能分析 + """ + self.is_profiling = True + print("动画性能分析已启动") + + def stop_profiling(self) -> None: + """ + 停止性能分析 + """ + self.is_profiling = False + print("动画性能分析已停止") + + def sample_performance(self) -> None: + """ + 采样性能数据 + """ + if not self.is_profiling: + return + + current_time = globalClock.getRealTime() + if current_time - self.last_sample_time < 1.0 / self.sampling_rate: + return + + self.last_sample_time = current_time + + # 收集性能指标 + metrics = self._collect_metrics() + self.profile_data.append(metrics) + + # 限制数据大小 + if len(self.profile_data) > 1000: + self.profile_data.pop(0) + + def _collect_metrics(self) -> PerformanceMetrics: + """ + 收集性能指标 + :return: 性能指标 + """ + # 获取FPS和帧时间 + fps = globalClock.getAverageFrameRate() + frame_time = globalClock.getDt() + + # 估算动画更新时间(简化) + animation_update_time = frame_time * 0.3 # 假设动画更新占总帧时间的30% + + # 估算内存使用(简化) + memory_usage = len(self.core_system.actors) * 1024 * 1024 # 假设每个Actor约1MB + + # 统计Actor和动画数量 + actor_count = len(self.core_system.actors) + active_animations = sum(len(info.active_animations) for info in self.core_system.actors.values()) + + return PerformanceMetrics( + fps=fps, + frame_time=frame_time, + animation_update_time=animation_update_time, + memory_usage=memory_usage, + actor_count=actor_count, + active_animations=active_animations + ) + + def get_performance_report(self) -> Dict[str, Any]: + """ + 获取性能报告 + :return: 性能报告 + """ + if not self.profile_data: + return {} + + # 计算统计数据 + fps_values = [m.fps for m in self.profile_data] + frame_time_values = [m.frame_time for m in self.profile_data] + update_time_values = [m.animation_update_time for m in self.profile_data] + memory_values = [m.memory_usage for m in self.profile_data] + + return { + 'sample_count': len(self.profile_data), + 'duration_seconds': len(self.profile_data) / self.sampling_rate, + 'fps': { + 'average': sum(fps_values) / len(fps_values), + 'min': min(fps_values), + 'max': max(fps_values), + 'std_dev': self._calculate_std_dev(fps_values) + }, + 'frame_time': { + 'average': sum(frame_time_values) / len(frame_time_values), + 'min': min(frame_time_values), + 'max': max(frame_time_values), + 'std_dev': self._calculate_std_dev(frame_time_values) + }, + 'animation_update_time': { + 'average': sum(update_time_values) / len(update_time_values), + 'min': min(update_time_values), + 'max': max(update_time_values), + 'std_dev': self._calculate_std_dev(update_time_values) + }, + 'memory_usage': { + 'average': sum(memory_values) / len(memory_values), + 'min': min(memory_values), + 'max': max(memory_values), + 'current': memory_values[-1] if memory_values else 0 + }, + 'actor_stats': { + 'current_actor_count': self.profile_data[-1].actor_count if self.profile_data else 0, + 'current_active_animations': self.profile_data[-1].active_animations if self.profile_data else 0 + } + } + + def _calculate_std_dev(self, values: List[float]) -> float: + """ + 计算标准差 + :param values: 数值列表 + :return: 标准差 + """ + if len(values) < 2: + return 0.0 + + mean = sum(values) / len(values) + variance = sum((x - mean) ** 2 for x in values) / (len(values) - 1) + return math.sqrt(variance) + + def get_optimization_recommendations(self) -> List[str]: + """ + 获取优化建议 + :return: 优化建议列表 + """ + if not self.profile_data: + return [] + + latest = self.profile_data[-1] + recommendations = [] + + # FPS相关建议 + if latest.fps < 30: + recommendations.append("FPS过低,建议优化动画系统或减少同时播放的动画数量") + + # 内存相关建议 + if latest.memory_usage > 512 * 1024 * 1024: # 超过512MB + recommendations.append("内存使用过高,建议检查Actor加载和卸载逻辑") + + # Actor数量建议 + if latest.actor_count > 100: + recommendations.append("Actor数量较多,建议使用LOD或视锥剔除优化") + + # 动画数量建议 + if latest.active_animations > 200: + recommendations.append("同时播放的动画过多,建议限制同时活动的动画数量") + + return recommendations + +# 使用示例和测试代码 +def example_analyzer_usage(core_system): + """ + 动画分析器使用示例 + """ + print("=== 动画分析器使用示例 ===") + + # 创建分析器 + analyzer = AnimationAnalyzer(core_system) + analyzer.start_analysis() + + # 分析示例(假设有Actor) + # 在实际使用中,这里会分析真实的Actor和动画 + print("动画分析器示例完成") + return analyzer + +def example_editor_usage(core_system): + """ + 动画编辑器使用示例 + """ + print("=== 动画编辑器使用示例 ===") + + # 创建编辑器 + editor = AnimationEditor(core_system) + + # 添加编辑回调 + def edit_callback(event_type, data): + print(f"编辑事件: {event_type}, 数据: {data}") + + editor.add_edit_callback(edit_callback) + + print("动画编辑器示例完成") + return editor + +def example_visualizer_usage(world): + """ + 调试可视化工具使用示例 + """ + print("=== 调试可视化工具使用示例 ===") + + # 创建可视化工具 + visualizer = DebugVisualizer(world) + visualizer.enable_visualization(True) + visualizer.set_mode(DebugVisualizationMode.SKELETON) + + print("调试可视化工具示例完成") + return visualizer + +def example_profiler_usage(core_system): + """ + 动画性能分析器使用示例 + """ + print("=== 动画性能分析器使用示例 ===") + + # 创建分析器 + profiler = AnimationProfiler(core_system) + profiler.start_profiling() + + # 采样性能数据 + profiler.sample_performance() + + # 获取性能报告 + report = profiler.get_performance_report() + print(f"性能报告: {report}") + + # 获取优化建议 + recommendations = profiler.get_optimization_recommendations() + print(f"优化建议: {recommendations}") + + print("动画性能分析器示例完成") + return profiler + +if __name__ == "__main__": print("动画工具模块加载完成") \ No newline at end of file diff --git a/plugins/user/audio_reverb_effects/README.md b/plugins/user/audio_reverb_effects/README.md index 7727e0d5..60957015 100644 --- a/plugins/user/audio_reverb_effects/README.md +++ b/plugins/user/audio_reverb_effects/README.md @@ -1,455 +1,455 @@ -# 音频混响和效果插件 - -一个功能完整的专业音频效果处理系统插件,为EG引擎提供基于Panda3D的高级音频效果处理功能,支持混响、均衡器、压缩器、调制效果和失真等多种音频效果。 - -## 功能特性 - -### 核心功能 -- **专业混响处理**: 基于物理模型的高质量混响效果 -- **多频段均衡器**: 精确的频段控制和滤波处理 -- **动态范围压缩**: 专业级压缩器效果处理 -- **调制效果**: 合唱、镶边、移相等多种调制效果 -- **失真效果**: 过载、失真、模糊等多种失真类型 -- **3D空间音频**: 真实的3D音频定位和空间化处理 -- **效果链处理**: 支持多个效果的串联和并联处理 -- **实时参数控制**: 所有效果参数支持实时调整 - -### 混响效果 -- **房间建模**: 支持多种预设房间类型(小房间、大厅、教堂等) -- **参数控制**: 房间大小、阻尼、湿信号电平、干信号电平、宽度等 -- **高级参数**: 早期反射、晚期混响、扩散、密度等 -- **冲激响应**: 支持基于房间几何的冲激响应生成 -- **预设系统**: 内置多种专业混响预设 - -### 均衡器效果 -- **多频段控制**: 支持最多32个独立频段调节 -- **滤波器类型**: 峰值滤波器、低频搁架、高频搁架 -- **参数控制**: 频率、增益、Q因子、主增益等 -- **专业功能**: 独奏模式、频段绕过、频段添加/移除 -- **预设系统**: 内置多种音乐风格EQ预设 - -### 压缩器效果 -- **动态控制**: 阈值、压缩比、启动时间、释放时间 -- **高级参数**: 膝点、输出增益、自动补偿增益 -- **专业功能**: 侧链处理、RMS窗口、预测处理 -- **可视化表**: 增益减少量表、压缩比率表、包络跟随器 -- **预设系统**: 内置多种应用场景压缩器预设 - -### 调制效果 -- **效果类型**: 合唱、镶边、移相器 -- **参数控制**: 速率、深度、反馈、湿信号混合、延迟时间 -- **波形选择**: 正弦波、三角波、方波、锯齿波 -- **立体声处理**: 立体声扩散、相位控制 -- **滤波器**: 高频截止、低频截止 -- **预设系统**: 内置多种调制效果预设 - -### 失真效果 -- **效果类型**: 过载、失真、模糊、削波器 -- **参数控制**: 驱动、音调、输出电平、偏置 -- **整流模式**: 全波整流、半波整流、无整流 -- **不对称处理**: 可调不对称度 -- **滤波器**: 预滤波器、后滤波器 -- **预设系统**: 内置多种失真效果预设 - -### 空间音频效果 -- **3D定位**: 基于听者位置的精确3D音频定位 -- **距离衰减**: 可配置的最小/最大距离和衰减因子 -- **方向性**: 锥形效应、方位角、仰角计算 -- **物理效应**: 多普勒效应、空气吸收 -- **遮挡处理**: 遮挡和阻挡效果模拟 -- **HRTF支持**: 头部相关传递函数实现更真实的3D音效 -- **混响发送**: 可配置的混响发送量 - -### 效果管理 -- **效果链**: 支持多个效果的串联处理 -- **参数自动化**: 所有参数支持实时调整 -- **预设系统**: 效果预设的创建、保存、加载 -- **状态监控**: 实时效果处理状态和性能统计 -- **资源管理**: 智能的效果资源管理和清理 - -### 性能优化 -- **高效处理**: 优化的音频处理算法 -- **内存管理**: 智能缓冲区管理和内存复用 -- **多线程支持**: 支持多线程音频处理 -- **采样率适配**: 自动适配不同采样率 -- **质量控制**: 可配置的处理质量等级 - -## 使用方法 - -### 快捷键操作 -- **F1**: 切换调试模式 -- **F2**: 切换效果处理 -- **F3**: 应用测试混响效果 -- **F4**: 应用测试均衡器效果 -- **F5**: 显示效果统计信息 -- **F6**: 应用测试压缩器效果 -- **F7**: 应用测试调制效果 -- **F8**: 应用测试失真效果 -- **F9**: 创建测试预设 -- **F10**: 切换音质设置 -- **F11**: 显示详细统计信息 -- **F12**: 重置所有效果 - -### GUI界面操作 -插件会在屏幕右上角创建控制面板,可以通过按钮直接操作音频效果功能。 - -### 编程接口 -```python -# 获取插件实例 -audio_effects_plugin = world.plugin_manager.get_plugin("audio_reverb_effects") - -# 创建混响效果 -reverb_params = { - 'room_size': 0.8, - 'damping': 0.5, - 'wet_level': 0.3, - 'dry_level': 0.7, - 'width': 1.0 -} -reverb_effect_id = audio_effects_plugin.create_reverb_effect(reverb_params) - -# 创建均衡器效果 -eq_params = { - 'bands': { - 100: 2.0, # 100Hz 提升2dB - 500: -1.0, # 500Hz 降低1dB - 2000: 3.0, # 2kHz 提升3dB - 8000: -2.0 # 8kHz 降低2dB - } -} -eq_effect_id = audio_effects_plugin.create_eq_effect(eq_params) - -# 创建压缩器效果 -compressor_params = { - 'threshold': -20.0, - 'ratio': 4.0, - 'attack': 0.01, - 'release': 0.1, - 'gain': 2.0 -} -compressor_effect_id = audio_effects_plugin.create_compressor_effect(compressor_params) - -# 创建调制效果 -modulation_params = { - 'type': 'chorus', - 'rate': 1.0, - 'depth': 0.5, - 'feedback': 0.3, - 'mix': 0.5 -} -modulation_effect_id = audio_effects_plugin.create_modulation_effect(modulation_params) - -# 创建失真效果 -distortion_params = { - 'drive': 0.5, - 'tone': 0.8, - 'level': 0.7, - 'type': 'overdrive' -} -distortion_effect_id = audio_effects_plugin.create_distortion_effect(distortion_params) - -# 应用效果到音效 -audio_effects_plugin.apply_effect_to_sound(sound_id, reverb_effect_id) -audio_effects_plugin.apply_effect_to_sound(sound_id, eq_effect_id) -audio_effects_plugin.apply_effect_to_sound(sound_id, compressor_effect_id) - -# 创建效果预设 -preset_data = { - 'reverb': reverb_params, - 'eq': eq_params, - 'compressor': compressor_params -} -audio_effects_plugin.create_preset("my_preset", preset_data) - -# 应用预设到音效 -audio_effects_plugin.apply_preset_to_sound(sound_id, "my_preset") - -# 设置效果参数 -audio_effects_plugin.set_effect_parameter(reverb_effect_id, 'room_size', 0.6) -audio_effects_plugin.set_effect_parameter(eq_effect_id, 'bands', {100: 3.0, 1000: -1.0}) - -# 启用/禁用效果 -audio_effects_plugin.enable_effect(reverb_effect_id) -audio_effects_plugin.disable_effect(eq_effect_id) - -# 获取效果类型 -effect_type = audio_effects_plugin.get_effect_type(compressor_effect_id) - -# 获取音效应用的效果列表 -applied_effects = audio_effects_plugin.get_sound_effects(sound_id) - -# 从音效移除效果 -audio_effects_plugin.remove_effect_from_sound(sound_id, reverb_effect_id) - -# 删除效果 -audio_effects_plugin.delete_effect(modulation_effect_id) -audio_effects_plugin.delete_effect(distortion_effect_id) - -# 设置主音量 -audio_effects_plugin.set_master_volume(0.8) - -# 设置音质等级 -audio_effects_plugin.set_quality_settings("high") - -# 获取统计信息 -stats = audio_effects_plugin.get_stats() -``` - -## 核心组件 - -### 混响处理器 -负责专业混响效果的处理和管理。 - -主要功能: -- 多种混响算法实现 -- 丰富的混响参数控制 -- 房间冲激响应生成 -- 预设管理系统 - -### 均衡器处理器 -负责多频段均衡器效果的处理和管理。 - -主要功能: -- 多频段参数控制 -- 多种滤波器类型 -- 专业EQ功能实现 -- 音乐风格预设 - -### 压缩器处理器 -负责动态范围压缩效果的处理和管理。 - -主要功能: -- 专业压缩参数控制 -- 侧链处理支持 -- 实时可视化表 -- 应用场景预设 - -### 调制处理器 -负责合唱、镶边、移相等调制效果的处理和管理。 - -主要功能: -- 多种调制效果类型 -- 波形选择和控制 -- 立体声处理 -- 效果预设系统 - -### 失真处理器 -负责过载、失真、模糊等失真效果的处理和管理。 - -主要功能: -- 多种失真类型 -- 整流模式控制 -- 滤波器处理 -- 失真预设系统 - -### 空间处理器 -负责3D空间音频效果的处理和管理。 - -主要功能: -- 3D音频定位 -- 距离衰减处理 -- 物理效应模拟 -- HRTF支持 - -## 效果预设 - -### 混响预设 -- `small_room`: 小房间混响 -- `medium_room`: 中等房间混响 -- `large_room`: 大房间混响 -- `large_hall`: 大厅混响 -- `cathedral`: 教堂混响 -- `bathroom`: 浴室混响 - -### 均衡器预设 -- `flat`: 平衡设置 -- `pop`: 流行音乐风格 -- `rock`: 摇滚音乐风格 -- `jazz`: 爵士音乐风格 -- `classical`: 古典音乐风格 -- `vocal_booster`: 人声增强 -- `bass_boost`: 低音增强 -- `treble_boost`: 高音增强 - -### 压缩器预设 -- `vocal_compressor`: 人声压缩 -- `bass_compressor`: 贝斯压缩 -- `drum_compressor`: 鼓组压缩 -- `master_bus`: 总线压缩 -- `aggressive`: 强烈压缩 -- `gentle`: 温和压缩 - -### 调制预设 -- `chorus_light`: 轻柔合唱 -- `chorus_medium`: 中等合唱 -- `chorus_heavy`: 强烈合唱 -- `flanger_light`: 轻柔镶边 -- `flanger_medium`: 中等镶边 -- `flanger_heavy`: 强烈镶边 -- `phaser_light`: 轻柔移相 -- `phaser_medium`: 中等移相 -- `phaser_heavy`: 强烈移相 - -### 失真预设 -- `mild_overdrive`: 轻度过载 -- `heavy_overdrive`: 重度过载 -- `classic_distortion`: 经典失真 -- `metal_distortion`: 金属失真 -- `fuzz_box`: 模糊效果 -- `bass_overdrive`: 贝斯过载 -- `tube_amp`: 胆机模拟 -- `bit_crusher`: 位破碎 - -### 空间音频预设 -- `close`: 近距离设置 -- `medium`: 中距离设置 -- `far`: 远距离设置 -- `outdoor`: 户外设置 -- `indoor`: 室内设置 -- `underwater`: 水下设置 - -## 性能优化 - -### 算法优化 -- **高效滤波器**: 优化的数字滤波器实现 -- **快速傅里叶变换**: 频域处理优化 -- **缓冲区管理**: 智能缓冲区复用 -- **内存池**: 减少内存分配开销 - -### 处理优化 -- **并行处理**: 支持多线程音频处理 -- **质量等级**: 可配置的处理质量 -- **动态调整**: 根据系统负载动态调整处理复杂度 -- **效果链优化**: 优化的效果处理顺序 - -### 资源管理 -- **智能加载**: 根据需要动态加载效果资源 -- **自动清理**: 不使用的资源自动清理 -- **缓存系统**: 效果参数和处理结果缓存 -- **引用计数**: 资源引用计数管理 - -## 扩展开发 - -### 添加新的效果类型 -1. 在[effects](file:///home/hello/EG/plugins/user/audio_reverb_effects/effects)目录中创建新的效果处理器 -2. 实现效果处理逻辑 -3. 在[Plugin](file:///home/hello/EG/plugins/user/audio_reverb_effects/plugin.py#L15-L1344)类中注册新效果 - -### 添加新的预设 -1. 在对应的效果处理器中添加预设数据 -2. 实现预设创建方法 -3. 在插件中提供预设应用接口 - -### 添加新的处理算法 -1. 在现有处理器中实现新的算法 -2. 添加算法选择参数 -3. 实现算法切换逻辑 - -## 故障排除 - -### 效果不生效 -1. 检查效果是否已启用 -2. 确认效果已正确应用到音效 -3. 验证参数设置是否正确 -4. 检查音效是否正在播放 - -### 性能问题 -1. 使用统计信息分析性能瓶颈 -2. 降低处理质量等级 -3. 减少同时应用的效果数量 -4. 优化效果参数设置 - -### 音质问题 -1. 检查采样率设置是否正确 -2. 确认缓冲区大小是否合适 -3. 验证效果参数是否在合理范围 -4. 检查是否出现削波现象 - -### 内存泄漏 -1. 使用统计信息监控内存使用 -2. 确认效果是否正确删除 -3. 检查资源是否正确释放 -4. 验证引用计数是否正确 - -## API参考 - -### Plugin类主要方法 - -#### 初始化和控制 -- `initialize()` - 初始化插件 -- `enable()` - 启用插件 -- `disable()` - 禁用插件 -- `finalize()` - 清理插件资源 -- `update(dt)` - 更新插件状态 - -#### 效果创建和管理 -- `create_reverb_effect(parameters)` - 创建混响效果 -- `create_eq_effect(parameters)` - 创建均衡器效果 -- `create_compressor_effect(parameters)` - 创建压缩器效果 -- `create_modulation_effect(parameters)` - 创建调制效果 -- `create_distortion_effect(parameters)` - 创建失真效果 -- `create_spatial_effect(parameters)` - 创建空间效果 -- `delete_effect(effect_id)` - 删除效果 -- `apply_effect_to_sound(sound_id, effect_id)` - 应用效果到音效 -- `remove_effect_from_sound(sound_id, effect_id)` - 从音效移除效果 - -#### 效果参数控制 -- `set_effect_parameter(effect_id, parameter, value)` - 设置效果参数 -- `get_effect_parameters(effect_id)` - 获取效果参数 -- `enable_effect(effect_id)` - 启用效果 -- `disable_effect(effect_id)` - 禁用效果 -- `get_effect_type(effect_id)` - 获取效果类型 -- `get_sound_effects(sound_id)` - 获取音效应用的效果列表 - -#### 预设系统 -- `create_preset(preset_name, preset_data)` - 创建效果预设 -- `apply_preset_to_sound(sound_id, preset_name)` - 应用预设到音效 -- `get_preset_names()` - 获取所有预设名称 -- `delete_preset(preset_name)` - 删除预设 - -#### 系统控制 -- `set_master_volume(volume)` - 设置主音量 -- `get_master_volume()` - 获取主音量 -- `set_quality_settings(quality)` - 设置音质等级 -- `get_quality_settings()` - 获取音质等级 -- `process_audio_with_effects(sound_id, audio_data)` - 使用效果处理音频数据 - -#### 统计和信息 -- `get_stats()` - 获取插件统计信息 - -## 音频格式支持 - -支持的音频格式: -- **WAV**: 未压缩的PCM音频 -- **OGG**: Ogg Vorbis压缩音频 -- **MP3**: MPEG音频层III - -## 性能调优建议 - -### 效果处理优化 -1. 合理控制同时处理的效果数量 -2. 根据应用场景选择合适的效果预设 -3. 及时删除不需要的效果 -4. 使用预设减少参数配置时间 - -### 资源管理优化 -1. 定期清理未使用的效果 -2. 合理设置缓冲区大小 -3. 使用对象池管理效果对象 -4. 预加载常用的效果预设 - -### 系统性能优化 -1. 根据硬件性能选择合适的质量等级 -2. 监控系统资源使用情况 -3. 动态调整处理复杂度 -4. 使用多线程处理提高性能 - -## 版本信息 - -- 版本: 1.0.0 -- 作者: EG Team -- 发布日期: 2024年 - -## 许可证 - +# 音频混响和效果插件 + +一个功能完整的专业音频效果处理系统插件,为EG引擎提供基于Panda3D的高级音频效果处理功能,支持混响、均衡器、压缩器、调制效果和失真等多种音频效果。 + +## 功能特性 + +### 核心功能 +- **专业混响处理**: 基于物理模型的高质量混响效果 +- **多频段均衡器**: 精确的频段控制和滤波处理 +- **动态范围压缩**: 专业级压缩器效果处理 +- **调制效果**: 合唱、镶边、移相等多种调制效果 +- **失真效果**: 过载、失真、模糊等多种失真类型 +- **3D空间音频**: 真实的3D音频定位和空间化处理 +- **效果链处理**: 支持多个效果的串联和并联处理 +- **实时参数控制**: 所有效果参数支持实时调整 + +### 混响效果 +- **房间建模**: 支持多种预设房间类型(小房间、大厅、教堂等) +- **参数控制**: 房间大小、阻尼、湿信号电平、干信号电平、宽度等 +- **高级参数**: 早期反射、晚期混响、扩散、密度等 +- **冲激响应**: 支持基于房间几何的冲激响应生成 +- **预设系统**: 内置多种专业混响预设 + +### 均衡器效果 +- **多频段控制**: 支持最多32个独立频段调节 +- **滤波器类型**: 峰值滤波器、低频搁架、高频搁架 +- **参数控制**: 频率、增益、Q因子、主增益等 +- **专业功能**: 独奏模式、频段绕过、频段添加/移除 +- **预设系统**: 内置多种音乐风格EQ预设 + +### 压缩器效果 +- **动态控制**: 阈值、压缩比、启动时间、释放时间 +- **高级参数**: 膝点、输出增益、自动补偿增益 +- **专业功能**: 侧链处理、RMS窗口、预测处理 +- **可视化表**: 增益减少量表、压缩比率表、包络跟随器 +- **预设系统**: 内置多种应用场景压缩器预设 + +### 调制效果 +- **效果类型**: 合唱、镶边、移相器 +- **参数控制**: 速率、深度、反馈、湿信号混合、延迟时间 +- **波形选择**: 正弦波、三角波、方波、锯齿波 +- **立体声处理**: 立体声扩散、相位控制 +- **滤波器**: 高频截止、低频截止 +- **预设系统**: 内置多种调制效果预设 + +### 失真效果 +- **效果类型**: 过载、失真、模糊、削波器 +- **参数控制**: 驱动、音调、输出电平、偏置 +- **整流模式**: 全波整流、半波整流、无整流 +- **不对称处理**: 可调不对称度 +- **滤波器**: 预滤波器、后滤波器 +- **预设系统**: 内置多种失真效果预设 + +### 空间音频效果 +- **3D定位**: 基于听者位置的精确3D音频定位 +- **距离衰减**: 可配置的最小/最大距离和衰减因子 +- **方向性**: 锥形效应、方位角、仰角计算 +- **物理效应**: 多普勒效应、空气吸收 +- **遮挡处理**: 遮挡和阻挡效果模拟 +- **HRTF支持**: 头部相关传递函数实现更真实的3D音效 +- **混响发送**: 可配置的混响发送量 + +### 效果管理 +- **效果链**: 支持多个效果的串联处理 +- **参数自动化**: 所有参数支持实时调整 +- **预设系统**: 效果预设的创建、保存、加载 +- **状态监控**: 实时效果处理状态和性能统计 +- **资源管理**: 智能的效果资源管理和清理 + +### 性能优化 +- **高效处理**: 优化的音频处理算法 +- **内存管理**: 智能缓冲区管理和内存复用 +- **多线程支持**: 支持多线程音频处理 +- **采样率适配**: 自动适配不同采样率 +- **质量控制**: 可配置的处理质量等级 + +## 使用方法 + +### 快捷键操作 +- **F1**: 切换调试模式 +- **F2**: 切换效果处理 +- **F3**: 应用测试混响效果 +- **F4**: 应用测试均衡器效果 +- **F5**: 显示效果统计信息 +- **F6**: 应用测试压缩器效果 +- **F7**: 应用测试调制效果 +- **F8**: 应用测试失真效果 +- **F9**: 创建测试预设 +- **F10**: 切换音质设置 +- **F11**: 显示详细统计信息 +- **F12**: 重置所有效果 + +### GUI界面操作 +插件会在屏幕右上角创建控制面板,可以通过按钮直接操作音频效果功能。 + +### 编程接口 +```python +# 获取插件实例 +audio_effects_plugin = world.plugin_manager.get_plugin("audio_reverb_effects") + +# 创建混响效果 +reverb_params = { + 'room_size': 0.8, + 'damping': 0.5, + 'wet_level': 0.3, + 'dry_level': 0.7, + 'width': 1.0 +} +reverb_effect_id = audio_effects_plugin.create_reverb_effect(reverb_params) + +# 创建均衡器效果 +eq_params = { + 'bands': { + 100: 2.0, # 100Hz 提升2dB + 500: -1.0, # 500Hz 降低1dB + 2000: 3.0, # 2kHz 提升3dB + 8000: -2.0 # 8kHz 降低2dB + } +} +eq_effect_id = audio_effects_plugin.create_eq_effect(eq_params) + +# 创建压缩器效果 +compressor_params = { + 'threshold': -20.0, + 'ratio': 4.0, + 'attack': 0.01, + 'release': 0.1, + 'gain': 2.0 +} +compressor_effect_id = audio_effects_plugin.create_compressor_effect(compressor_params) + +# 创建调制效果 +modulation_params = { + 'type': 'chorus', + 'rate': 1.0, + 'depth': 0.5, + 'feedback': 0.3, + 'mix': 0.5 +} +modulation_effect_id = audio_effects_plugin.create_modulation_effect(modulation_params) + +# 创建失真效果 +distortion_params = { + 'drive': 0.5, + 'tone': 0.8, + 'level': 0.7, + 'type': 'overdrive' +} +distortion_effect_id = audio_effects_plugin.create_distortion_effect(distortion_params) + +# 应用效果到音效 +audio_effects_plugin.apply_effect_to_sound(sound_id, reverb_effect_id) +audio_effects_plugin.apply_effect_to_sound(sound_id, eq_effect_id) +audio_effects_plugin.apply_effect_to_sound(sound_id, compressor_effect_id) + +# 创建效果预设 +preset_data = { + 'reverb': reverb_params, + 'eq': eq_params, + 'compressor': compressor_params +} +audio_effects_plugin.create_preset("my_preset", preset_data) + +# 应用预设到音效 +audio_effects_plugin.apply_preset_to_sound(sound_id, "my_preset") + +# 设置效果参数 +audio_effects_plugin.set_effect_parameter(reverb_effect_id, 'room_size', 0.6) +audio_effects_plugin.set_effect_parameter(eq_effect_id, 'bands', {100: 3.0, 1000: -1.0}) + +# 启用/禁用效果 +audio_effects_plugin.enable_effect(reverb_effect_id) +audio_effects_plugin.disable_effect(eq_effect_id) + +# 获取效果类型 +effect_type = audio_effects_plugin.get_effect_type(compressor_effect_id) + +# 获取音效应用的效果列表 +applied_effects = audio_effects_plugin.get_sound_effects(sound_id) + +# 从音效移除效果 +audio_effects_plugin.remove_effect_from_sound(sound_id, reverb_effect_id) + +# 删除效果 +audio_effects_plugin.delete_effect(modulation_effect_id) +audio_effects_plugin.delete_effect(distortion_effect_id) + +# 设置主音量 +audio_effects_plugin.set_master_volume(0.8) + +# 设置音质等级 +audio_effects_plugin.set_quality_settings("high") + +# 获取统计信息 +stats = audio_effects_plugin.get_stats() +``` + +## 核心组件 + +### 混响处理器 +负责专业混响效果的处理和管理。 + +主要功能: +- 多种混响算法实现 +- 丰富的混响参数控制 +- 房间冲激响应生成 +- 预设管理系统 + +### 均衡器处理器 +负责多频段均衡器效果的处理和管理。 + +主要功能: +- 多频段参数控制 +- 多种滤波器类型 +- 专业EQ功能实现 +- 音乐风格预设 + +### 压缩器处理器 +负责动态范围压缩效果的处理和管理。 + +主要功能: +- 专业压缩参数控制 +- 侧链处理支持 +- 实时可视化表 +- 应用场景预设 + +### 调制处理器 +负责合唱、镶边、移相等调制效果的处理和管理。 + +主要功能: +- 多种调制效果类型 +- 波形选择和控制 +- 立体声处理 +- 效果预设系统 + +### 失真处理器 +负责过载、失真、模糊等失真效果的处理和管理。 + +主要功能: +- 多种失真类型 +- 整流模式控制 +- 滤波器处理 +- 失真预设系统 + +### 空间处理器 +负责3D空间音频效果的处理和管理。 + +主要功能: +- 3D音频定位 +- 距离衰减处理 +- 物理效应模拟 +- HRTF支持 + +## 效果预设 + +### 混响预设 +- `small_room`: 小房间混响 +- `medium_room`: 中等房间混响 +- `large_room`: 大房间混响 +- `large_hall`: 大厅混响 +- `cathedral`: 教堂混响 +- `bathroom`: 浴室混响 + +### 均衡器预设 +- `flat`: 平衡设置 +- `pop`: 流行音乐风格 +- `rock`: 摇滚音乐风格 +- `jazz`: 爵士音乐风格 +- `classical`: 古典音乐风格 +- `vocal_booster`: 人声增强 +- `bass_boost`: 低音增强 +- `treble_boost`: 高音增强 + +### 压缩器预设 +- `vocal_compressor`: 人声压缩 +- `bass_compressor`: 贝斯压缩 +- `drum_compressor`: 鼓组压缩 +- `master_bus`: 总线压缩 +- `aggressive`: 强烈压缩 +- `gentle`: 温和压缩 + +### 调制预设 +- `chorus_light`: 轻柔合唱 +- `chorus_medium`: 中等合唱 +- `chorus_heavy`: 强烈合唱 +- `flanger_light`: 轻柔镶边 +- `flanger_medium`: 中等镶边 +- `flanger_heavy`: 强烈镶边 +- `phaser_light`: 轻柔移相 +- `phaser_medium`: 中等移相 +- `phaser_heavy`: 强烈移相 + +### 失真预设 +- `mild_overdrive`: 轻度过载 +- `heavy_overdrive`: 重度过载 +- `classic_distortion`: 经典失真 +- `metal_distortion`: 金属失真 +- `fuzz_box`: 模糊效果 +- `bass_overdrive`: 贝斯过载 +- `tube_amp`: 胆机模拟 +- `bit_crusher`: 位破碎 + +### 空间音频预设 +- `close`: 近距离设置 +- `medium`: 中距离设置 +- `far`: 远距离设置 +- `outdoor`: 户外设置 +- `indoor`: 室内设置 +- `underwater`: 水下设置 + +## 性能优化 + +### 算法优化 +- **高效滤波器**: 优化的数字滤波器实现 +- **快速傅里叶变换**: 频域处理优化 +- **缓冲区管理**: 智能缓冲区复用 +- **内存池**: 减少内存分配开销 + +### 处理优化 +- **并行处理**: 支持多线程音频处理 +- **质量等级**: 可配置的处理质量 +- **动态调整**: 根据系统负载动态调整处理复杂度 +- **效果链优化**: 优化的效果处理顺序 + +### 资源管理 +- **智能加载**: 根据需要动态加载效果资源 +- **自动清理**: 不使用的资源自动清理 +- **缓存系统**: 效果参数和处理结果缓存 +- **引用计数**: 资源引用计数管理 + +## 扩展开发 + +### 添加新的效果类型 +1. 在[effects](file:///home/hello/EG/plugins/user/audio_reverb_effects/effects)目录中创建新的效果处理器 +2. 实现效果处理逻辑 +3. 在[Plugin](file:///home/hello/EG/plugins/user/audio_reverb_effects/plugin.py#L15-L1344)类中注册新效果 + +### 添加新的预设 +1. 在对应的效果处理器中添加预设数据 +2. 实现预设创建方法 +3. 在插件中提供预设应用接口 + +### 添加新的处理算法 +1. 在现有处理器中实现新的算法 +2. 添加算法选择参数 +3. 实现算法切换逻辑 + +## 故障排除 + +### 效果不生效 +1. 检查效果是否已启用 +2. 确认效果已正确应用到音效 +3. 验证参数设置是否正确 +4. 检查音效是否正在播放 + +### 性能问题 +1. 使用统计信息分析性能瓶颈 +2. 降低处理质量等级 +3. 减少同时应用的效果数量 +4. 优化效果参数设置 + +### 音质问题 +1. 检查采样率设置是否正确 +2. 确认缓冲区大小是否合适 +3. 验证效果参数是否在合理范围 +4. 检查是否出现削波现象 + +### 内存泄漏 +1. 使用统计信息监控内存使用 +2. 确认效果是否正确删除 +3. 检查资源是否正确释放 +4. 验证引用计数是否正确 + +## API参考 + +### Plugin类主要方法 + +#### 初始化和控制 +- `initialize()` - 初始化插件 +- `enable()` - 启用插件 +- `disable()` - 禁用插件 +- `finalize()` - 清理插件资源 +- `update(dt)` - 更新插件状态 + +#### 效果创建和管理 +- `create_reverb_effect(parameters)` - 创建混响效果 +- `create_eq_effect(parameters)` - 创建均衡器效果 +- `create_compressor_effect(parameters)` - 创建压缩器效果 +- `create_modulation_effect(parameters)` - 创建调制效果 +- `create_distortion_effect(parameters)` - 创建失真效果 +- `create_spatial_effect(parameters)` - 创建空间效果 +- `delete_effect(effect_id)` - 删除效果 +- `apply_effect_to_sound(sound_id, effect_id)` - 应用效果到音效 +- `remove_effect_from_sound(sound_id, effect_id)` - 从音效移除效果 + +#### 效果参数控制 +- `set_effect_parameter(effect_id, parameter, value)` - 设置效果参数 +- `get_effect_parameters(effect_id)` - 获取效果参数 +- `enable_effect(effect_id)` - 启用效果 +- `disable_effect(effect_id)` - 禁用效果 +- `get_effect_type(effect_id)` - 获取效果类型 +- `get_sound_effects(sound_id)` - 获取音效应用的效果列表 + +#### 预设系统 +- `create_preset(preset_name, preset_data)` - 创建效果预设 +- `apply_preset_to_sound(sound_id, preset_name)` - 应用预设到音效 +- `get_preset_names()` - 获取所有预设名称 +- `delete_preset(preset_name)` - 删除预设 + +#### 系统控制 +- `set_master_volume(volume)` - 设置主音量 +- `get_master_volume()` - 获取主音量 +- `set_quality_settings(quality)` - 设置音质等级 +- `get_quality_settings()` - 获取音质等级 +- `process_audio_with_effects(sound_id, audio_data)` - 使用效果处理音频数据 + +#### 统计和信息 +- `get_stats()` - 获取插件统计信息 + +## 音频格式支持 + +支持的音频格式: +- **WAV**: 未压缩的PCM音频 +- **OGG**: Ogg Vorbis压缩音频 +- **MP3**: MPEG音频层III + +## 性能调优建议 + +### 效果处理优化 +1. 合理控制同时处理的效果数量 +2. 根据应用场景选择合适的效果预设 +3. 及时删除不需要的效果 +4. 使用预设减少参数配置时间 + +### 资源管理优化 +1. 定期清理未使用的效果 +2. 合理设置缓冲区大小 +3. 使用对象池管理效果对象 +4. 预加载常用的效果预设 + +### 系统性能优化 +1. 根据硬件性能选择合适的质量等级 +2. 监控系统资源使用情况 +3. 动态调整处理复杂度 +4. 使用多线程处理提高性能 + +## 版本信息 + +- 版本: 1.0.0 +- 作者: EG Team +- 发布日期: 2024年 + +## 许可证 + 本插件遵循EG引擎许可证协议。 \ No newline at end of file diff --git a/plugins/user/audio_reverb_effects/effects/compressor_processor.py b/plugins/user/audio_reverb_effects/effects/compressor_processor.py index c7a42273..2aedce09 100644 --- a/plugins/user/audio_reverb_effects/effects/compressor_processor.py +++ b/plugins/user/audio_reverb_effects/effects/compressor_processor.py @@ -1,711 +1,711 @@ -""" -压缩器效果处理器 -实现专业的音频压缩器效果处理 -""" - -import uuid -import math -import numpy as np -from typing import Dict, List, Any, Optional - -class CompressorProcessor: - """ - 压缩器效果处理器 - 实现专业的音频压缩器效果处理 - """ - - def __init__(self, plugin): - """ - 初始化压缩器效果处理器 - - Args: - plugin: 音频效果插件实例 - """ - self.plugin = plugin - self.effects: Dict[str, Dict[str, Any]] = {} - self.stats = { - 'total_effects': 0, - 'active_effects': 0, - 'effects_processed': 0, - 'processing_time': 0.0, - 'memory_usage': 0 - } - self.buffer_size = plugin.buffer_size if plugin else 4096 - self.sample_rate = 44100 # 默认采样率 - - def create_effect(self, parameters: Dict[str, Any]) -> str: - """ - 创建压缩器效果 - - Args: - parameters: 压缩器参数 - - Returns: - 效果ID - """ - try: - # 生成唯一ID - effect_id = str(uuid.uuid4()) - - # 默认参数 - default_params = { - 'threshold': -20.0, # 阈值 (dB) - 'ratio': 4.0, # 压缩比 (例如 4:1) - 'attack': 0.01, # 启动时间 (秒) - 'release': 0.1, # 释放时间 (秒) - 'gain': 0.0, # 输出增益 (dB) - 'knee': 5.0, # 膝点 (dB) - 'makeup_gain': True, # 自动补偿增益 - 'sidechain': False, # 侧链 - 'rms_window': 0.01, # RMS窗口时间 (秒) - 'lookahead': 0.005 # 预测时间 (秒) - } - - # 合并参数 - effect_params = default_params.copy() - effect_params.update(parameters) - - # 创建效果对象 - effect = { - 'id': effect_id, - 'type': 'compressor', - 'parameters': effect_params, - 'active': True, - 'buffers': {}, # 缓冲区 - 'envelope': 0.0, # 包络状态 - 'gain_reduction': 0.0, # 增益减少量 - 'created_time': self._get_current_time(), - 'process_count': 0 - } - - # 初始化缓冲区 - self._initialize_buffers(effect) - - # 存储效果 - self.effects[effect_id] = effect - - # 更新统计信息 - self.stats['total_effects'] += 1 - self.stats['active_effects'] += 1 - - # 在插件中注册效果 - if self.plugin: - self.plugin.effects[effect_id] = effect.copy() - - print(f"✓ 压缩器效果创建成功: {effect_id}") - return effect_id - - except Exception as e: - print(f"✗ 创建压缩器效果失败: {e}") - import traceback - traceback.print_exc() - return "" - - def _initialize_buffers(self, effect: Dict[str, Any]): - """ - 初始化缓冲区 - - Args: - effect: 效果对象 - """ - effect_id = effect['id'] - buffers = {} - - # 输入和输出缓冲区 - buffers['input'] = np.zeros(self.buffer_size) - buffers['output'] = np.zeros(self.buffer_size) - - # 侧链缓冲区 - buffers['sidechain'] = np.zeros(self.buffer_size) - - # 预测缓冲区 - lookahead_samples = int(effect['parameters'].get('lookahead', 0.005) * self.sample_rate) - buffers['lookahead'] = np.zeros(lookahead_samples) - - # RMS窗口缓冲区 - rms_window_samples = int(effect['parameters'].get('rms_window', 0.01) * self.sample_rate) - buffers['rms_window'] = np.zeros(rms_window_samples) - - effect['buffers'] = buffers - - def delete_effect(self, effect_id: str) -> bool: - """ - 删除压缩器效果 - - Args: - effect_id: 效果ID - - Returns: - 是否删除成功 - """ - if effect_id not in self.effects: - print(f"✗ 压缩器效果不存在: {effect_id}") - return False - - try: - # 删除效果 - del self.effects[effect_id] - - # 更新统计信息 - self.stats['active_effects'] -= 1 - - # 从插件中移除效果 - if self.plugin and effect_id in self.plugin.effects: - del self.plugin.effects[effect_id] - - print(f"✓ 压缩器效果已删除: {effect_id}") - return True - - except Exception as e: - print(f"✗ 删除压缩器效果失败: {e}") - return False - - def process(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: - """ - 处理压缩器效果 - - Args: - audio_data: 输入音频数据 - parameters: 效果参数 - - Returns: - 处理后的音频数据 - """ - try: - import time - process_start = time.time() - - # 如果输入是NumPy数组 - if isinstance(audio_data, np.ndarray): - processed_data = self._process_numpy_array(audio_data, parameters) - else: - # 对于其他类型的数据,返回原始数据 - processed_data = audio_data - - # 更新统计信息 - self.stats['effects_processed'] += 1 - self.stats['processing_time'] += (time.time() - process_start) - - return processed_data - - except Exception as e: - print(f"✗ 压缩器效果处理失败: {e}") - return audio_data - - def _process_numpy_array(self, audio_data: np.ndarray, parameters: Dict[str, Any]) -> np.ndarray: - """ - 处理NumPy数组音频数据 - - Args: - audio_data: 输入音频数据 - parameters: 效果参数 - - Returns: - 处理后的音频数据 - """ - # 获取参数 - threshold = parameters.get('threshold', -20.0) - ratio = max(1.0, parameters.get('ratio', 4.0)) - attack = max(0.001, parameters.get('attack', 0.01)) - release = max(0.01, parameters.get('release', 0.1)) - gain = parameters.get('gain', 0.0) - knee = max(0.0, parameters.get('knee', 5.0)) - makeup_gain = parameters.get('makeup_gain', True) - - # 计算时间系数 - attack_coeff = np.exp(-1.0 / (attack * self.sample_rate)) - release_coeff = np.exp(-1.0 / (release * self.sample_rate)) - - # 创建输出数组 - if audio_data.ndim == 1: - # 单声道处理 - output_data = np.zeros_like(audio_data) - envelope = 0.0 - - for i in range(len(audio_data)): - # 计算输入信号的幅度(RMS) - sample = audio_data[i] - amplitude = abs(sample) - - # 包络检测(峰值检测) - if amplitude > envelope: - envelope = attack_coeff * envelope + (1 - attack_coeff) * amplitude - else: - envelope = release_coeff * envelope + (1 - release_coeff) * amplitude - - # 转换为dB - envelope_db = 20 * np.log10(max(1e-10, envelope)) - - # 计算增益减少量 - gain_reduction = self._calculate_gain_reduction( - envelope_db, threshold, ratio, knee) - - # 应用增益减少 - linear_gain_reduction = 10 ** (gain_reduction / 20.0) - - # 应用压缩 - compressed_sample = sample * linear_gain_reduction - - # 应用输出增益 - if gain != 0.0: - output_gain = 10 ** (gain / 20.0) - compressed_sample *= output_gain - - # 自动补偿增益 - if makeup_gain and gain_reduction < 0: - makeup = -gain_reduction # 补偿减少的增益 - makeup_linear = 10 ** (makeup / 20.0) - compressed_sample *= makeup_linear - - output_data[i] = compressed_sample - - else: - # 多声道处理 - output_data = np.zeros_like(audio_data) - envelope = 0.0 - - for channel in range(audio_data.shape[0]): - for i in range(audio_data.shape[1]): - # 计算输入信号的幅度(RMS) - sample = audio_data[channel, i] - amplitude = abs(sample) - - # 包络检测(峰值检测) - if amplitude > envelope: - envelope = attack_coeff * envelope + (1 - attack_coeff) * amplitude - else: - envelope = release_coeff * envelope + (1 - release_coeff) * amplitude - - # 转换为dB - envelope_db = 20 * np.log10(max(1e-10, envelope)) - - # 计算增益减少量 - gain_reduction = self._calculate_gain_reduction( - envelope_db, threshold, ratio, knee) - - # 应用增益减少 - linear_gain_reduction = 10 ** (gain_reduction / 20.0) - - # 应用压缩 - compressed_sample = sample * linear_gain_reduction - - # 应用输出增益 - if gain != 0.0: - output_gain = 10 ** (gain / 20.0) - compressed_sample *= output_gain - - # 自动补偿增益 - if makeup_gain and gain_reduction < 0: - makeup = -gain_reduction # 补偿减少的增益 - makeup_linear = 10 ** (makeup / 20.0) - compressed_sample *= makeup_linear - - output_data[channel, i] = compressed_sample - - # 确保输出在有效范围内 - processed_data = np.clip(output_data, -1.0, 1.0) - return processed_data - - def _calculate_gain_reduction(self, input_level: float, threshold: float, - ratio: float, knee: float) -> float: - """ - 计算增益减少量 - - Args: - input_level: 输入电平 (dB) - threshold: 阈值 (dB) - ratio: 压缩比 - knee: 膝点 (dB) - - Returns: - 增益减少量 (dB) - """ - # 如果输入低于阈值,不进行压缩 - if input_level <= threshold - knee/2: - return 0.0 - - # 软膝压缩 - if input_level <= threshold + knee/2: - # 在膝点范围内,使用二次曲线 - x = input_level - threshold + knee/2 - gain_reduction = -((x**2) / (2 * knee)) / ratio - else: - # 超过膝点,使用线性压缩 - gain_reduction = (threshold - input_level) * (1 - 1/ratio) - - return gain_reduction - - def set_parameter(self, effect_id: str, parameter: str, value: Any) -> bool: - """ - 设置效果参数 - - Args: - effect_id: 效果ID - parameter: 参数名 - value: 参数值 - - Returns: - 是否设置成功 - """ - if effect_id not in self.effects: - print(f"✗ 压缩器效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['parameters'][parameter] = value - - # 如果是插件中的效果,也更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['parameters'][parameter] = value - - print(f"✓ 压缩器效果参数已设置: {effect_id}.{parameter} = {value}") - return True - - except Exception as e: - print(f"✗ 设置压缩器效果参数失败: {e}") - return False - - def get_parameters(self, effect_id: str) -> Dict[str, Any]: - """ - 获取效果参数 - - Args: - effect_id: 效果ID - - Returns: - 参数字典 - """ - if effect_id not in self.effects: - return {} - - return self.effects[effect_id]['parameters'].copy() - - def enable_effect(self, effect_id: str) -> bool: - """ - 启用压缩器效果 - - Args: - effect_id: 效果ID - - Returns: - 是否启用成功 - """ - if effect_id not in self.effects: - print(f"✗ 压缩器效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['active'] = True - self.stats['active_effects'] += 1 - - # 如果是插件中的效果,也更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['active'] = True - - print(f"✓ 压缩器效果已启用: {effect_id}") - return True - - except Exception as e: - print(f"✗ 启用压缩器效果失败: {e}") - return False - - def disable_effect(self, effect_id: str) -> bool: - """ - 禁用压缩器效果 - - Args: - effect_id: 效果ID - - Returns: - 是否禁用成功 - """ - if effect_id not in self.effects: - print(f"✗ 压缩器效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['active'] = False - self.stats['active_effects'] -= 1 - - # 如果是插件中的效果,也更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['active'] = False - - print(f"✓ 压缩器效果已禁用: {effect_id}") - return True - - except Exception as e: - print(f"✗ 禁用压缩器效果失败: {e}") - return False - - def cleanup(self): - """清理所有资源""" - self.effects.clear() - self.stats = { - 'total_effects': 0, - 'active_effects': 0, - 'effects_processed': 0, - 'processing_time': 0.0, - 'memory_usage': 0 - } - print("✓ 压缩器效果处理器资源已清理") - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def _get_current_time(self) -> float: - """ - 获取当前时间(秒) - - Returns: - 当前时间 - """ - if self.plugin and self.plugin.plugin_manager and self.plugin.plugin_manager.world: - return self.plugin.plugin_manager.world.globalClock.getFrameTime() - import time - return time.time() - - def update(self, dt: float): - """ - 更新处理器状态 - - Args: - dt: 时间增量 - """ - # 这里可以更新任何需要定期更新的状态 - pass - - def set_sample_rate(self, sample_rate: int): - """ - 设置采样率 - - Args: - sample_rate: 采样率 (Hz) - """ - self.sample_rate = sample_rate - print(f"✓ 压缩器处理器采样率已设置: {sample_rate} Hz") - - def set_threshold(self, effect_id: str, threshold: float) -> bool: - """ - 设置阈值 - - Args: - effect_id: 效果ID - threshold: 阈值 (dB) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'threshold', max(-60, min(0, threshold))) - - def set_ratio(self, effect_id: str, ratio: float) -> bool: - """ - 设置压缩比 - - Args: - effect_id: 效果ID - ratio: 压缩比 - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'ratio', max(1.0, min(20.0, ratio))) - - def set_attack(self, effect_id: str, attack: float) -> bool: - """ - 设置启动时间 - - Args: - effect_id: 效果ID - attack: 启动时间 (秒) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'attack', max(0.001, min(1.0, attack))) - - def set_release(self, effect_id: str, release: float) -> bool: - """ - 设置释放时间 - - Args: - effect_id: 效果ID - release: 释放时间 (秒) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'release', max(0.01, min(5.0, release))) - - def set_gain(self, effect_id: str, gain: float) -> bool: - """ - 设置输出增益 - - Args: - effect_id: 效果ID - gain: 输出增益 (dB) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'gain', max(-20, min(20, gain))) - - def set_knee(self, effect_id: str, knee: float) -> bool: - """ - 设置膝点 - - Args: - effect_id: 效果ID - knee: 膝点 (dB) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'knee', max(0, min(20, knee))) - - def enable_makeup_gain(self, effect_id: str, enable: bool) -> bool: - """ - 启用/禁用自动补偿增益 - - Args: - effect_id: 效果ID - enable: 是否启用 - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'makeup_gain', enable) - - def enable_sidechain(self, effect_id: str, enable: bool) -> bool: - """ - 启用/禁用侧链 - - Args: - effect_id: 效果ID - enable: 是否启用 - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'sidechain', enable) - - def get_gain_reduction_meter(self, effect_id: str) -> float: - """ - 获取增益减少量表 - - Args: - effect_id: 效果ID - - Returns: - 当前增益减少量 (dB) - """ - if effect_id not in self.effects: - return 0.0 - - return self.effects[effect_id].get('gain_reduction', 0.0) - - def create_preset(self, preset_name: str) -> Dict[str, Any]: - """ - 创建压缩器预设 - - Args: - preset_name: 预设名称 - - Returns: - 预设参数字典 - """ - presets = { - 'vocal_compressor': { - 'threshold': -24.0, - 'ratio': 4.0, - 'attack': 0.005, - 'release': 0.05, - 'gain': 3.0, - 'knee': 6.0, - 'makeup_gain': True - }, - 'bass_compressor': { - 'threshold': -18.0, - 'ratio': 6.0, - 'attack': 0.015, - 'release': 0.2, - 'gain': 2.0, - 'knee': 8.0, - 'makeup_gain': True - }, - 'drum_compressor': { - 'threshold': -15.0, - 'ratio': 8.0, - 'attack': 0.003, - 'release': 0.08, - 'gain': 1.0, - 'knee': 3.0, - 'makeup_gain': True - }, - 'master_bus': { - 'threshold': -12.0, - 'ratio': 3.0, - 'attack': 0.02, - 'release': 0.3, - 'gain': 1.5, - 'knee': 10.0, - 'makeup_gain': True - }, - 'aggressive': { - 'threshold': -10.0, - 'ratio': 10.0, - 'attack': 0.001, - 'release': 0.05, - 'gain': 4.0, - 'knee': 2.0, - 'makeup_gain': True - }, - 'gentle': { - 'threshold': -28.0, - 'ratio': 2.0, - 'attack': 0.05, - 'release': 0.5, - 'gain': 0.0, - 'knee': 12.0, - 'makeup_gain': True - } - } - - return presets.get(preset_name, {}).copy() - - def get_compression_ratio_meter(self, effect_id: str) -> float: - """ - 获取压缩比率表 - - Args: - effect_id: 效果ID - - Returns: - 当前压缩比率 - """ - if effect_id not in self.effects: - return 1.0 - - return self.effects[effect_id]['parameters'].get('ratio', 1.0) - - def get_envelope_follower(self, effect_id: str) -> float: - """ - 获取包络跟随器值 - - Args: - effect_id: 效果ID - - Returns: - 当前包络值 - """ - if effect_id not in self.effects: - return 0.0 - - return self.effects[effect_id].get('envelope', 0.0) - +""" +压缩器效果处理器 +实现专业的音频压缩器效果处理 +""" + +import uuid +import math +import numpy as np +from typing import Dict, List, Any, Optional + +class CompressorProcessor: + """ + 压缩器效果处理器 + 实现专业的音频压缩器效果处理 + """ + + def __init__(self, plugin): + """ + 初始化压缩器效果处理器 + + Args: + plugin: 音频效果插件实例 + """ + self.plugin = plugin + self.effects: Dict[str, Dict[str, Any]] = {} + self.stats = { + 'total_effects': 0, + 'active_effects': 0, + 'effects_processed': 0, + 'processing_time': 0.0, + 'memory_usage': 0 + } + self.buffer_size = plugin.buffer_size if plugin else 4096 + self.sample_rate = 44100 # 默认采样率 + + def create_effect(self, parameters: Dict[str, Any]) -> str: + """ + 创建压缩器效果 + + Args: + parameters: 压缩器参数 + + Returns: + 效果ID + """ + try: + # 生成唯一ID + effect_id = str(uuid.uuid4()) + + # 默认参数 + default_params = { + 'threshold': -20.0, # 阈值 (dB) + 'ratio': 4.0, # 压缩比 (例如 4:1) + 'attack': 0.01, # 启动时间 (秒) + 'release': 0.1, # 释放时间 (秒) + 'gain': 0.0, # 输出增益 (dB) + 'knee': 5.0, # 膝点 (dB) + 'makeup_gain': True, # 自动补偿增益 + 'sidechain': False, # 侧链 + 'rms_window': 0.01, # RMS窗口时间 (秒) + 'lookahead': 0.005 # 预测时间 (秒) + } + + # 合并参数 + effect_params = default_params.copy() + effect_params.update(parameters) + + # 创建效果对象 + effect = { + 'id': effect_id, + 'type': 'compressor', + 'parameters': effect_params, + 'active': True, + 'buffers': {}, # 缓冲区 + 'envelope': 0.0, # 包络状态 + 'gain_reduction': 0.0, # 增益减少量 + 'created_time': self._get_current_time(), + 'process_count': 0 + } + + # 初始化缓冲区 + self._initialize_buffers(effect) + + # 存储效果 + self.effects[effect_id] = effect + + # 更新统计信息 + self.stats['total_effects'] += 1 + self.stats['active_effects'] += 1 + + # 在插件中注册效果 + if self.plugin: + self.plugin.effects[effect_id] = effect.copy() + + print(f"✓ 压缩器效果创建成功: {effect_id}") + return effect_id + + except Exception as e: + print(f"✗ 创建压缩器效果失败: {e}") + import traceback + traceback.print_exc() + return "" + + def _initialize_buffers(self, effect: Dict[str, Any]): + """ + 初始化缓冲区 + + Args: + effect: 效果对象 + """ + effect_id = effect['id'] + buffers = {} + + # 输入和输出缓冲区 + buffers['input'] = np.zeros(self.buffer_size) + buffers['output'] = np.zeros(self.buffer_size) + + # 侧链缓冲区 + buffers['sidechain'] = np.zeros(self.buffer_size) + + # 预测缓冲区 + lookahead_samples = int(effect['parameters'].get('lookahead', 0.005) * self.sample_rate) + buffers['lookahead'] = np.zeros(lookahead_samples) + + # RMS窗口缓冲区 + rms_window_samples = int(effect['parameters'].get('rms_window', 0.01) * self.sample_rate) + buffers['rms_window'] = np.zeros(rms_window_samples) + + effect['buffers'] = buffers + + def delete_effect(self, effect_id: str) -> bool: + """ + 删除压缩器效果 + + Args: + effect_id: 效果ID + + Returns: + 是否删除成功 + """ + if effect_id not in self.effects: + print(f"✗ 压缩器效果不存在: {effect_id}") + return False + + try: + # 删除效果 + del self.effects[effect_id] + + # 更新统计信息 + self.stats['active_effects'] -= 1 + + # 从插件中移除效果 + if self.plugin and effect_id in self.plugin.effects: + del self.plugin.effects[effect_id] + + print(f"✓ 压缩器效果已删除: {effect_id}") + return True + + except Exception as e: + print(f"✗ 删除压缩器效果失败: {e}") + return False + + def process(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: + """ + 处理压缩器效果 + + Args: + audio_data: 输入音频数据 + parameters: 效果参数 + + Returns: + 处理后的音频数据 + """ + try: + import time + process_start = time.time() + + # 如果输入是NumPy数组 + if isinstance(audio_data, np.ndarray): + processed_data = self._process_numpy_array(audio_data, parameters) + else: + # 对于其他类型的数据,返回原始数据 + processed_data = audio_data + + # 更新统计信息 + self.stats['effects_processed'] += 1 + self.stats['processing_time'] += (time.time() - process_start) + + return processed_data + + except Exception as e: + print(f"✗ 压缩器效果处理失败: {e}") + return audio_data + + def _process_numpy_array(self, audio_data: np.ndarray, parameters: Dict[str, Any]) -> np.ndarray: + """ + 处理NumPy数组音频数据 + + Args: + audio_data: 输入音频数据 + parameters: 效果参数 + + Returns: + 处理后的音频数据 + """ + # 获取参数 + threshold = parameters.get('threshold', -20.0) + ratio = max(1.0, parameters.get('ratio', 4.0)) + attack = max(0.001, parameters.get('attack', 0.01)) + release = max(0.01, parameters.get('release', 0.1)) + gain = parameters.get('gain', 0.0) + knee = max(0.0, parameters.get('knee', 5.0)) + makeup_gain = parameters.get('makeup_gain', True) + + # 计算时间系数 + attack_coeff = np.exp(-1.0 / (attack * self.sample_rate)) + release_coeff = np.exp(-1.0 / (release * self.sample_rate)) + + # 创建输出数组 + if audio_data.ndim == 1: + # 单声道处理 + output_data = np.zeros_like(audio_data) + envelope = 0.0 + + for i in range(len(audio_data)): + # 计算输入信号的幅度(RMS) + sample = audio_data[i] + amplitude = abs(sample) + + # 包络检测(峰值检测) + if amplitude > envelope: + envelope = attack_coeff * envelope + (1 - attack_coeff) * amplitude + else: + envelope = release_coeff * envelope + (1 - release_coeff) * amplitude + + # 转换为dB + envelope_db = 20 * np.log10(max(1e-10, envelope)) + + # 计算增益减少量 + gain_reduction = self._calculate_gain_reduction( + envelope_db, threshold, ratio, knee) + + # 应用增益减少 + linear_gain_reduction = 10 ** (gain_reduction / 20.0) + + # 应用压缩 + compressed_sample = sample * linear_gain_reduction + + # 应用输出增益 + if gain != 0.0: + output_gain = 10 ** (gain / 20.0) + compressed_sample *= output_gain + + # 自动补偿增益 + if makeup_gain and gain_reduction < 0: + makeup = -gain_reduction # 补偿减少的增益 + makeup_linear = 10 ** (makeup / 20.0) + compressed_sample *= makeup_linear + + output_data[i] = compressed_sample + + else: + # 多声道处理 + output_data = np.zeros_like(audio_data) + envelope = 0.0 + + for channel in range(audio_data.shape[0]): + for i in range(audio_data.shape[1]): + # 计算输入信号的幅度(RMS) + sample = audio_data[channel, i] + amplitude = abs(sample) + + # 包络检测(峰值检测) + if amplitude > envelope: + envelope = attack_coeff * envelope + (1 - attack_coeff) * amplitude + else: + envelope = release_coeff * envelope + (1 - release_coeff) * amplitude + + # 转换为dB + envelope_db = 20 * np.log10(max(1e-10, envelope)) + + # 计算增益减少量 + gain_reduction = self._calculate_gain_reduction( + envelope_db, threshold, ratio, knee) + + # 应用增益减少 + linear_gain_reduction = 10 ** (gain_reduction / 20.0) + + # 应用压缩 + compressed_sample = sample * linear_gain_reduction + + # 应用输出增益 + if gain != 0.0: + output_gain = 10 ** (gain / 20.0) + compressed_sample *= output_gain + + # 自动补偿增益 + if makeup_gain and gain_reduction < 0: + makeup = -gain_reduction # 补偿减少的增益 + makeup_linear = 10 ** (makeup / 20.0) + compressed_sample *= makeup_linear + + output_data[channel, i] = compressed_sample + + # 确保输出在有效范围内 + processed_data = np.clip(output_data, -1.0, 1.0) + return processed_data + + def _calculate_gain_reduction(self, input_level: float, threshold: float, + ratio: float, knee: float) -> float: + """ + 计算增益减少量 + + Args: + input_level: 输入电平 (dB) + threshold: 阈值 (dB) + ratio: 压缩比 + knee: 膝点 (dB) + + Returns: + 增益减少量 (dB) + """ + # 如果输入低于阈值,不进行压缩 + if input_level <= threshold - knee/2: + return 0.0 + + # 软膝压缩 + if input_level <= threshold + knee/2: + # 在膝点范围内,使用二次曲线 + x = input_level - threshold + knee/2 + gain_reduction = -((x**2) / (2 * knee)) / ratio + else: + # 超过膝点,使用线性压缩 + gain_reduction = (threshold - input_level) * (1 - 1/ratio) + + return gain_reduction + + def set_parameter(self, effect_id: str, parameter: str, value: Any) -> bool: + """ + 设置效果参数 + + Args: + effect_id: 效果ID + parameter: 参数名 + value: 参数值 + + Returns: + 是否设置成功 + """ + if effect_id not in self.effects: + print(f"✗ 压缩器效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['parameters'][parameter] = value + + # 如果是插件中的效果,也更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['parameters'][parameter] = value + + print(f"✓ 压缩器效果参数已设置: {effect_id}.{parameter} = {value}") + return True + + except Exception as e: + print(f"✗ 设置压缩器效果参数失败: {e}") + return False + + def get_parameters(self, effect_id: str) -> Dict[str, Any]: + """ + 获取效果参数 + + Args: + effect_id: 效果ID + + Returns: + 参数字典 + """ + if effect_id not in self.effects: + return {} + + return self.effects[effect_id]['parameters'].copy() + + def enable_effect(self, effect_id: str) -> bool: + """ + 启用压缩器效果 + + Args: + effect_id: 效果ID + + Returns: + 是否启用成功 + """ + if effect_id not in self.effects: + print(f"✗ 压缩器效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['active'] = True + self.stats['active_effects'] += 1 + + # 如果是插件中的效果,也更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['active'] = True + + print(f"✓ 压缩器效果已启用: {effect_id}") + return True + + except Exception as e: + print(f"✗ 启用压缩器效果失败: {e}") + return False + + def disable_effect(self, effect_id: str) -> bool: + """ + 禁用压缩器效果 + + Args: + effect_id: 效果ID + + Returns: + 是否禁用成功 + """ + if effect_id not in self.effects: + print(f"✗ 压缩器效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['active'] = False + self.stats['active_effects'] -= 1 + + # 如果是插件中的效果,也更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['active'] = False + + print(f"✓ 压缩器效果已禁用: {effect_id}") + return True + + except Exception as e: + print(f"✗ 禁用压缩器效果失败: {e}") + return False + + def cleanup(self): + """清理所有资源""" + self.effects.clear() + self.stats = { + 'total_effects': 0, + 'active_effects': 0, + 'effects_processed': 0, + 'processing_time': 0.0, + 'memory_usage': 0 + } + print("✓ 压缩器效果处理器资源已清理") + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def _get_current_time(self) -> float: + """ + 获取当前时间(秒) + + Returns: + 当前时间 + """ + if self.plugin and self.plugin.plugin_manager and self.plugin.plugin_manager.world: + return self.plugin.plugin_manager.world.globalClock.getFrameTime() + import time + return time.time() + + def update(self, dt: float): + """ + 更新处理器状态 + + Args: + dt: 时间增量 + """ + # 这里可以更新任何需要定期更新的状态 + pass + + def set_sample_rate(self, sample_rate: int): + """ + 设置采样率 + + Args: + sample_rate: 采样率 (Hz) + """ + self.sample_rate = sample_rate + print(f"✓ 压缩器处理器采样率已设置: {sample_rate} Hz") + + def set_threshold(self, effect_id: str, threshold: float) -> bool: + """ + 设置阈值 + + Args: + effect_id: 效果ID + threshold: 阈值 (dB) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'threshold', max(-60, min(0, threshold))) + + def set_ratio(self, effect_id: str, ratio: float) -> bool: + """ + 设置压缩比 + + Args: + effect_id: 效果ID + ratio: 压缩比 + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'ratio', max(1.0, min(20.0, ratio))) + + def set_attack(self, effect_id: str, attack: float) -> bool: + """ + 设置启动时间 + + Args: + effect_id: 效果ID + attack: 启动时间 (秒) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'attack', max(0.001, min(1.0, attack))) + + def set_release(self, effect_id: str, release: float) -> bool: + """ + 设置释放时间 + + Args: + effect_id: 效果ID + release: 释放时间 (秒) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'release', max(0.01, min(5.0, release))) + + def set_gain(self, effect_id: str, gain: float) -> bool: + """ + 设置输出增益 + + Args: + effect_id: 效果ID + gain: 输出增益 (dB) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'gain', max(-20, min(20, gain))) + + def set_knee(self, effect_id: str, knee: float) -> bool: + """ + 设置膝点 + + Args: + effect_id: 效果ID + knee: 膝点 (dB) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'knee', max(0, min(20, knee))) + + def enable_makeup_gain(self, effect_id: str, enable: bool) -> bool: + """ + 启用/禁用自动补偿增益 + + Args: + effect_id: 效果ID + enable: 是否启用 + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'makeup_gain', enable) + + def enable_sidechain(self, effect_id: str, enable: bool) -> bool: + """ + 启用/禁用侧链 + + Args: + effect_id: 效果ID + enable: 是否启用 + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'sidechain', enable) + + def get_gain_reduction_meter(self, effect_id: str) -> float: + """ + 获取增益减少量表 + + Args: + effect_id: 效果ID + + Returns: + 当前增益减少量 (dB) + """ + if effect_id not in self.effects: + return 0.0 + + return self.effects[effect_id].get('gain_reduction', 0.0) + + def create_preset(self, preset_name: str) -> Dict[str, Any]: + """ + 创建压缩器预设 + + Args: + preset_name: 预设名称 + + Returns: + 预设参数字典 + """ + presets = { + 'vocal_compressor': { + 'threshold': -24.0, + 'ratio': 4.0, + 'attack': 0.005, + 'release': 0.05, + 'gain': 3.0, + 'knee': 6.0, + 'makeup_gain': True + }, + 'bass_compressor': { + 'threshold': -18.0, + 'ratio': 6.0, + 'attack': 0.015, + 'release': 0.2, + 'gain': 2.0, + 'knee': 8.0, + 'makeup_gain': True + }, + 'drum_compressor': { + 'threshold': -15.0, + 'ratio': 8.0, + 'attack': 0.003, + 'release': 0.08, + 'gain': 1.0, + 'knee': 3.0, + 'makeup_gain': True + }, + 'master_bus': { + 'threshold': -12.0, + 'ratio': 3.0, + 'attack': 0.02, + 'release': 0.3, + 'gain': 1.5, + 'knee': 10.0, + 'makeup_gain': True + }, + 'aggressive': { + 'threshold': -10.0, + 'ratio': 10.0, + 'attack': 0.001, + 'release': 0.05, + 'gain': 4.0, + 'knee': 2.0, + 'makeup_gain': True + }, + 'gentle': { + 'threshold': -28.0, + 'ratio': 2.0, + 'attack': 0.05, + 'release': 0.5, + 'gain': 0.0, + 'knee': 12.0, + 'makeup_gain': True + } + } + + return presets.get(preset_name, {}).copy() + + def get_compression_ratio_meter(self, effect_id: str) -> float: + """ + 获取压缩比率表 + + Args: + effect_id: 效果ID + + Returns: + 当前压缩比率 + """ + if effect_id not in self.effects: + return 1.0 + + return self.effects[effect_id]['parameters'].get('ratio', 1.0) + + def get_envelope_follower(self, effect_id: str) -> float: + """ + 获取包络跟随器值 + + Args: + effect_id: 效果ID + + Returns: + 当前包络值 + """ + if effect_id not in self.effects: + return 0.0 + + return self.effects[effect_id].get('envelope', 0.0) + diff --git a/plugins/user/audio_reverb_effects/effects/distortion_processor.py b/plugins/user/audio_reverb_effects/effects/distortion_processor.py index 7a00a359..27f824bf 100644 --- a/plugins/user/audio_reverb_effects/effects/distortion_processor.py +++ b/plugins/user/audio_reverb_effects/effects/distortion_processor.py @@ -1,841 +1,841 @@ -""" -失真效果处理器 -实现专业的音频失真效果处理 -""" - -import uuid -import math -import numpy as np -from typing import Dict, List, Any, Optional - -class DistortionProcessor: - """ - 失真效果处理器 - 实现专业的音频失真效果处理 - """ - - def __init__(self, plugin): - """ - 初始化失真效果处理器 - - Args: - plugin: 音频效果插件实例 - """ - self.plugin = plugin - self.effects: Dict[str, Dict[str, Any]] = {} - self.stats = { - 'total_effects': 0, - 'active_effects': 0, - 'effects_processed': 0, - 'processing_time': 0.0, - 'memory_usage': 0 - } - self.buffer_size = plugin.buffer_size if plugin else 4096 - self.sample_rate = 44100 # 默认采样率 - - def create_effect(self, parameters: Dict[str, Any]) -> str: - """ - 创建失真效果 - - Args: - parameters: 失真效果参数 - - Returns: - 效果ID - """ - try: - # 生成唯一ID - effect_id = str(uuid.uuid4()) - - # 默认参数 - default_params = { - 'drive': 0.5, # 驱动 (0.0-1.0) - 'tone': 0.5, # 音调 (0.0-1.0) - 'level': 0.5, # 输出电平 (0.0-1.0) - 'bias': 0.0, # 偏置 (-1.0-1.0) - 'type': 'overdrive', # 类型: overdrive, distortion, fuzz, clipper - 'pre_filter': 1000.0, # 预滤波器频率 (Hz) - 'post_filter': 5000.0, # 后滤波器频率 (Hz) - 'rectification': 'full', # 整流: full, half, none - 'asymmetry': 0.0, # 不对称度 (0.0-1.0) - 'mix': 1.0 # 湿信号混合 (0.0-1.0) - } - - # 合并参数 - effect_params = default_params.copy() - effect_params.update(parameters) - - # 创建效果对象 - effect = { - 'id': effect_id, - 'type': 'distortion', - 'parameters': effect_params, - 'active': True, - 'buffers': {}, # 缓冲区 - 'filter_states': {}, # 滤波器状态 - 'created_time': self._get_current_time(), - 'process_count': 0 - } - - # 初始化缓冲区 - self._initialize_buffers(effect) - - # 存储效果 - self.effects[effect_id] = effect - - # 更新统计信息 - self.stats['total_effects'] += 1 - self.stats['active_effects'] += 1 - - # 在插件中注册效果 - if self.plugin: - self.plugin.effects[effect_id] = effect.copy() - - print(f"✓ 失真效果创建成功: {effect_id}") - return effect_id - - except Exception as e: - print(f"✗ 创建失真效果失败: {e}") - import traceback - traceback.print_exc() - return "" - - def _initialize_buffers(self, effect: Dict[str, Any]): - """ - 初始化缓冲区 - - Args: - effect: 效果对象 - """ - effect_id = effect['id'] - buffers = {} - - # 输入和输出缓冲区 - buffers['input'] = np.zeros(self.buffer_size) - buffers['output'] = np.zeros(self.buffer_size) - - # 滤波器状态缓冲区 - buffers['pre_filter_state'] = np.zeros(2) # 双通道滤波器状态 - buffers['post_filter_state'] = np.zeros(2) # 双通道滤波器状态 - - effect['buffers'] = buffers - - def delete_effect(self, effect_id: str) -> bool: - """ - 删除失真效果 - - Args: - effect_id: 效果ID - - Returns: - 是否删除成功 - """ - if effect_id not in self.effects: - print(f"✗ 失真效果不存在: {effect_id}") - return False - - try: - # 删除效果 - del self.effects[effect_id] - - # 更新统计信息 - self.stats['active_effects'] -= 1 - - # 从插件中移除效果 - if self.plugin and effect_id in self.plugin.effects: - del self.plugin.effects[effect_id] - - print(f"✓ 失真效果已删除: {effect_id}") - return True - - except Exception as e: - print(f"✗ 删除失真效果失败: {e}") - return False - - def process(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: - """ - 处理失真效果 - - Args: - audio_data: 输入音频数据 - parameters: 效果参数 - - Returns: - 处理后的音频数据 - """ - try: - import time - process_start = time.time() - - # 如果输入是NumPy数组 - if isinstance(audio_data, np.ndarray): - processed_data = self._process_numpy_array(audio_data, parameters) - else: - # 对于其他类型的数据,返回原始数据 - processed_data = audio_data - - # 更新统计信息 - self.stats['effects_processed'] += 1 - self.stats['processing_time'] += (time.time() - process_start) - - return processed_data - - except Exception as e: - print(f"✗ 失真效果处理失败: {e}") - return audio_data - - def _process_numpy_array(self, audio_data: np.ndarray, parameters: Dict[str, Any]) -> np.ndarray: - """ - 处理NumPy数组音频数据 - - Args: - audio_data: 输入音频数据 - parameters: 效果参数 - - Returns: - 处理后的音频数据 - """ - # 获取参数 - drive = max(0.0, min(1.0, parameters.get('drive', 0.5))) - tone = max(0.0, min(1.0, parameters.get('tone', 0.5))) - level = max(0.0, min(1.0, parameters.get('level', 0.5))) - bias = max(-1.0, min(1.0, parameters.get('bias', 0.0))) - distortion_type = parameters.get('type', 'overdrive') - pre_filter = max(100.0, min(10000.0, parameters.get('pre_filter', 1000.0))) - post_filter = max(1000.0, min(20000.0, parameters.get('post_filter', 5000.0))) - rectification = parameters.get('rectification', 'full') - asymmetry = max(0.0, min(1.0, parameters.get('asymmetry', 0.0))) - mix = max(0.0, min(1.0, parameters.get('mix', 1.0))) - - # 创建输出数组 - if audio_data.ndim == 1: - # 单声道处理 - output_data = np.zeros_like(audio_data) - - for i in range(len(audio_data)): - sample = audio_data[i] - - # 应用偏置 - biased_sample = sample + bias - - # 预滤波 - filtered_sample = self._apply_pre_filter(biased_sample, pre_filter) - - # 应用失真 - distorted_sample = self._apply_distortion( - filtered_sample, drive, distortion_type, rectification, asymmetry) - - # 后滤波 - filtered_distorted = self._apply_post_filter(distorted_sample, post_filter, tone) - - # 应用输出电平 - leveled_sample = filtered_distorted * level - - # 混合干湿信号 - output_data[i] = sample * (1.0 - mix) + leveled_sample * mix - - else: - # 多声道处理 - output_data = np.zeros_like(audio_data) - - for channel in range(audio_data.shape[0]): - for i in range(audio_data.shape[1]): - sample = audio_data[channel, i] - - # 应用偏置 - biased_sample = sample + bias - - # 预滤波 - filtered_sample = self._apply_pre_filter(biased_sample, pre_filter) - - # 应用失真 - distorted_sample = self._apply_distortion( - filtered_sample, drive, distortion_type, rectification, asymmetry) - - # 后滤波 - filtered_distorted = self._apply_post_filter(distorted_sample, post_filter, tone) - - # 应用输出电平 - leveled_sample = filtered_distorted * level - - # 混合干湿信号 - output_data[channel, i] = sample * (1.0 - mix) + leveled_sample * mix - - # 确保输出在有效范围内 - processed_data = np.clip(output_data, -1.0, 1.0) - return processed_data - - def _apply_pre_filter(self, sample: float, cutoff: float) -> float: - """ - 应用预滤波器 - - Args: - sample: 输入样本 - cutoff: 截止频率 (Hz) - - Returns: - 滤波后的样本 - """ - # 简化的低通滤波器 - # 实际应用中会使用更复杂的滤波器设计 - - # 计算滤波系数 - rc = 1.0 / (2.0 * math.pi * cutoff) - dt = 1.0 / self.sample_rate - alpha = dt / (rc + dt) - - # 简单的一阶低通滤波 - filtered = alpha * sample + (1.0 - alpha) * sample # 简化实现 - return filtered - - def _apply_distortion(self, sample: float, drive: float, - distortion_type: str, rectification: str, asymmetry: float) -> float: - """ - 应用失真效果 - - Args: - sample: 输入样本 - drive: 驱动强度 - distortion_type: 失真类型 - rectification: 整流类型 - asymmetry: 不对称度 - - Returns: - 失真后的样本 - """ - # 应用驱动 - driven_sample = sample * (1.0 + drive * 10.0) - - # 根据类型应用失真 - if distortion_type == 'overdrive': - distorted = self._soft_clip(driven_sample) - elif distortion_type == 'distortion': - distorted = self._hard_clip(driven_sample) - elif distortion_type == 'fuzz': - distorted = self._fuzz_distort(driven_sample) - elif distortion_type == 'clipper': - distorted = self._asymmetric_clip(driven_sample, asymmetry) - else: - distorted = self._soft_clip(driven_sample) - - # 应用整流 - if rectification == 'full': - rectified = abs(distorted) - elif rectification == 'half': - rectified = max(0.0, distorted) - else: - rectified = distorted - - return rectified - - def _soft_clip(self, sample: float) -> float: - """ - 软削波 - - Args: - sample: 输入样本 - - Returns: - 削波后的样本 - """ - # 双曲正切软削波 - return math.tanh(sample) - - def _hard_clip(self, sample: float) -> float: - """ - 硬削波 - - Args: - sample: 输入样本 - - Returns: - 削波后的样本 - """ - # 简单的硬削波 - return max(-1.0, min(1.0, sample)) - - def _fuzz_distort(self, sample: float) -> float: - """ - 模糊失真 - - Args: - sample: 输入样本 - - Returns: - 失真后的样本 - """ - # 强烈的非线性失真 - amplified = sample * 10.0 - if amplified > 0: - fuzzed = 1.0 - math.exp(-amplified) - else: - fuzzed = -1.0 + math.exp(amplified) - return fuzzed - - def _asymmetric_clip(self, sample: float, asymmetry: float) -> float: - """ - 不对称削波 - - Args: - sample: 输入样本 - asymmetry: 不对称度 - - Returns: - 削波后的样本 - """ - positive_limit = 1.0 - negative_limit = -1.0 + asymmetry * 0.5 - - if sample > positive_limit: - return positive_limit - elif sample < negative_limit: - return negative_limit - else: - return sample - - def _apply_post_filter(self, sample: float, cutoff: float, tone: float) -> float: - """ - 应用后滤波器 - - Args: - sample: 输入样本 - cutoff: 截止频率 (Hz) - tone: 音调控制 - - Returns: - 滤波后的样本 - """ - # 结合音调控制的滤波器 - adjusted_cutoff = cutoff * (0.5 + tone * 0.5) - - # 简化的滤波器 - rc = 1.0 / (2.0 * math.pi * adjusted_cutoff) - dt = 1.0 / self.sample_rate - alpha = dt / (rc + dt) - - # 简单的一阶滤波 - filtered = alpha * sample + (1.0 - alpha) * sample # 简化实现 - return filtered - - def set_parameter(self, effect_id: str, parameter: str, value: Any) -> bool: - """ - 设置效果参数 - - Args: - effect_id: 效果ID - parameter: 参数名 - value: 参数值 - - Returns: - 是否设置成功 - """ - if effect_id not in self.effects: - print(f"✗ 失真效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['parameters'][parameter] = value - - # 如果是插件中的效果,也更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['parameters'][parameter] = value - - print(f"✓ 失真效果参数已设置: {effect_id}.{parameter} = {value}") - return True - - except Exception as e: - print(f"✗ 设置失真效果参数失败: {e}") - return False - - def get_parameters(self, effect_id: str) -> Dict[str, Any]: - """ - 获取效果参数 - - Args: - effect_id: 效果ID - - Returns: - 参数字典 - """ - if effect_id not in self.effects: - return {} - - return self.effects[effect_id]['parameters'].copy() - - def enable_effect(self, effect_id: str) -> bool: - """ - 启用失真效果 - - Args: - effect_id: 效果ID - - Returns: - 是否启用成功 - """ - if effect_id not in self.effects: - print(f"✗ 失真效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['active'] = True - self.stats['active_effects'] += 1 - - # 如果是插件中的效果,也更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['active'] = True - - print(f"✓ 失真效果已启用: {effect_id}") - return True - - except Exception as e: - print(f"✗ 启用失真效果失败: {e}") - return False - - def disable_effect(self, effect_id: str) -> bool: - """ - 禁用失真效果 - - Args: - effect_id: 效果ID - - Returns: - 是否禁用成功 - """ - if effect_id not in self.effects: - print(f"✗ 失真效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['active'] = False - self.stats['active_effects'] -= 1 - - # 如果是插件中的效果,也更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['active'] = False - - print(f"✓ 失真效果已禁用: {effect_id}") - return True - - except Exception as e: - print(f"✗ 禁用失真效果失败: {e}") - return False - - def cleanup(self): - """清理所有资源""" - self.effects.clear() - self.stats = { - 'total_effects': 0, - 'active_effects': 0, - 'effects_processed': 0, - 'processing_time': 0.0, - 'memory_usage': 0 - } - print("✓ 失真效果处理器资源已清理") - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def _get_current_time(self) -> float: - """ - 获取当前时间(秒) - - Returns: - 当前时间 - """ - if self.plugin and self.plugin.plugin_manager and self.plugin.plugin_manager.world: - return self.plugin.plugin_manager.world.globalClock.getFrameTime() - import time - return time.time() - - def update(self, dt: float): - """ - 更新处理器状态 - - Args: - dt: 时间增量 - """ - # 这里可以更新任何需要定期更新的状态 - pass - - def set_sample_rate(self, sample_rate: int): - """ - 设置采样率 - - Args: - sample_rate: 采样率 (Hz) - """ - self.sample_rate = sample_rate - print(f"✓ 失真效果处理器采样率已设置: {sample_rate} Hz") - - def set_drive(self, effect_id: str, drive: float) -> bool: - """ - 设置驱动强度 - - Args: - effect_id: 效果ID - drive: 驱动强度 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'drive', max(0.0, min(1.0, drive))) - - def set_tone(self, effect_id: str, tone: float) -> bool: - """ - 设置音调 - - Args: - effect_id: 效果ID - tone: 音调 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'tone', max(0.0, min(1.0, tone))) - - def set_level(self, effect_id: str, level: float) -> bool: - """ - 设置输出电平 - - Args: - effect_id: 效果ID - level: 输出电平 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'level', max(0.0, min(1.0, level))) - - def set_bias(self, effect_id: str, bias: float) -> bool: - """ - 设置偏置 - - Args: - effect_id: 效果ID - bias: 偏置 (-1.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'bias', max(-1.0, min(1.0, bias))) - - def set_type(self, effect_id: str, distortion_type: str) -> bool: - """ - 设置失真类型 - - Args: - effect_id: 效果ID - distortion_type: 失真类型 (overdrive, distortion, fuzz, clipper) - - Returns: - 是否设置成功 - """ - valid_types = ['overdrive', 'distortion', 'fuzz', 'clipper'] - if distortion_type not in valid_types: - print(f"✗ 无效的失真类型: {distortion_type}") - return False - - return self.set_parameter(effect_id, 'type', distortion_type) - - def set_pre_filter(self, effect_id: str, frequency: float) -> bool: - """ - 设置预滤波器频率 - - Args: - effect_id: 效果ID - frequency: 频率 (Hz) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'pre_filter', max(100.0, min(10000.0, frequency))) - - def set_post_filter(self, effect_id: str, frequency: float) -> bool: - """ - 设置后滤波器频率 - - Args: - effect_id: 效果ID - frequency: 频率 (Hz) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'post_filter', max(1000.0, min(20000.0, frequency))) - - def set_rectification(self, effect_id: str, rectification: str) -> bool: - """ - 设置整流类型 - - Args: - effect_id: 效果ID - rectification: 整流类型 (full, half, none) - - Returns: - 是否设置成功 - """ - valid_rectifications = ['full', 'half', 'none'] - if rectification not in valid_rectifications: - print(f"✗ 无效的整流类型: {rectification}") - return False - - return self.set_parameter(effect_id, 'rectification', rectification) - - def set_asymmetry(self, effect_id: str, asymmetry: float) -> bool: - """ - 设置不对称度 - - Args: - effect_id: 效果ID - asymmetry: 不对称度 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'asymmetry', max(0.0, min(1.0, asymmetry))) - - def set_mix(self, effect_id: str, mix: float) -> bool: - """ - 设置湿信号混合 - - Args: - effect_id: 效果ID - mix: 湿信号混合 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'mix', max(0.0, min(1.0, mix))) - - def create_preset(self, preset_name: str) -> Dict[str, Any]: - """ - 创建失真效果预设 - - Args: - preset_name: 预设名称 - - Returns: - 预设参数字典 - """ - presets = { - 'mild_overdrive': { - 'drive': 0.3, - 'tone': 0.7, - 'level': 0.6, - 'type': 'overdrive', - 'pre_filter': 2000.0, - 'post_filter': 6000.0, - 'rectification': 'none', - 'mix': 1.0 - }, - 'heavy_overdrive': { - 'drive': 0.7, - 'tone': 0.5, - 'level': 0.8, - 'type': 'overdrive', - 'pre_filter': 1000.0, - 'post_filter': 4000.0, - 'rectification': 'none', - 'mix': 1.0 - }, - 'classic_distortion': { - 'drive': 0.8, - 'tone': 0.6, - 'level': 0.7, - 'type': 'distortion', - 'pre_filter': 800.0, - 'post_filter': 3000.0, - 'rectification': 'none', - 'mix': 1.0 - }, - 'metal_distortion': { - 'drive': 1.0, - 'tone': 0.4, - 'level': 0.9, - 'type': 'distortion', - 'pre_filter': 500.0, - 'post_filter': 2000.0, - 'rectification': 'none', - 'mix': 1.0 - }, - 'fuzz_box': { - 'drive': 0.9, - 'tone': 0.3, - 'level': 0.8, - 'type': 'fuzz', - 'pre_filter': 300.0, - 'post_filter': 1500.0, - 'rectification': 'full', - 'mix': 1.0 - }, - 'bass_overdrive': { - 'drive': 0.4, - 'tone': 0.8, - 'level': 0.7, - 'type': 'overdrive', - 'pre_filter': 3000.0, - 'post_filter': 8000.0, - 'rectification': 'none', - 'mix': 1.0 - }, - 'tube_amp': { - 'drive': 0.6, - 'tone': 0.7, - 'level': 0.75, - 'type': 'overdrive', - 'pre_filter': 1500.0, - 'post_filter': 5000.0, - 'rectification': 'none', - 'asymmetry': 0.3, - 'mix': 1.0 - }, - 'bit_crusher': { - 'drive': 0.5, - 'tone': 0.4, - 'level': 0.6, - 'type': 'distortion', - 'pre_filter': 4000.0, - 'post_filter': 10000.0, - 'rectification': 'none', - 'mix': 1.0 - } - } - - return presets.get(preset_name, {}).copy() - - def get_drive_meter(self, effect_id: str) -> float: - """ - 获取驱动表 - - Args: - effect_id: 效果ID - - Returns: - 当前驱动值 (0.0-1.0) - """ - if effect_id not in self.effects: - return 0.0 - - return self.effects[effect_id]['parameters'].get('drive', 0.0) - - def get_tone_meter(self, effect_id: str) -> float: - """ - 获取音调表 - - Args: - effect_id: 效果ID - - Returns: - 当前音调值 (0.0-1.0) - """ - if effect_id not in self.effects: - return 0.0 - - return self.effects[effect_id]['parameters'].get('tone', 0.0) - +""" +失真效果处理器 +实现专业的音频失真效果处理 +""" + +import uuid +import math +import numpy as np +from typing import Dict, List, Any, Optional + +class DistortionProcessor: + """ + 失真效果处理器 + 实现专业的音频失真效果处理 + """ + + def __init__(self, plugin): + """ + 初始化失真效果处理器 + + Args: + plugin: 音频效果插件实例 + """ + self.plugin = plugin + self.effects: Dict[str, Dict[str, Any]] = {} + self.stats = { + 'total_effects': 0, + 'active_effects': 0, + 'effects_processed': 0, + 'processing_time': 0.0, + 'memory_usage': 0 + } + self.buffer_size = plugin.buffer_size if plugin else 4096 + self.sample_rate = 44100 # 默认采样率 + + def create_effect(self, parameters: Dict[str, Any]) -> str: + """ + 创建失真效果 + + Args: + parameters: 失真效果参数 + + Returns: + 效果ID + """ + try: + # 生成唯一ID + effect_id = str(uuid.uuid4()) + + # 默认参数 + default_params = { + 'drive': 0.5, # 驱动 (0.0-1.0) + 'tone': 0.5, # 音调 (0.0-1.0) + 'level': 0.5, # 输出电平 (0.0-1.0) + 'bias': 0.0, # 偏置 (-1.0-1.0) + 'type': 'overdrive', # 类型: overdrive, distortion, fuzz, clipper + 'pre_filter': 1000.0, # 预滤波器频率 (Hz) + 'post_filter': 5000.0, # 后滤波器频率 (Hz) + 'rectification': 'full', # 整流: full, half, none + 'asymmetry': 0.0, # 不对称度 (0.0-1.0) + 'mix': 1.0 # 湿信号混合 (0.0-1.0) + } + + # 合并参数 + effect_params = default_params.copy() + effect_params.update(parameters) + + # 创建效果对象 + effect = { + 'id': effect_id, + 'type': 'distortion', + 'parameters': effect_params, + 'active': True, + 'buffers': {}, # 缓冲区 + 'filter_states': {}, # 滤波器状态 + 'created_time': self._get_current_time(), + 'process_count': 0 + } + + # 初始化缓冲区 + self._initialize_buffers(effect) + + # 存储效果 + self.effects[effect_id] = effect + + # 更新统计信息 + self.stats['total_effects'] += 1 + self.stats['active_effects'] += 1 + + # 在插件中注册效果 + if self.plugin: + self.plugin.effects[effect_id] = effect.copy() + + print(f"✓ 失真效果创建成功: {effect_id}") + return effect_id + + except Exception as e: + print(f"✗ 创建失真效果失败: {e}") + import traceback + traceback.print_exc() + return "" + + def _initialize_buffers(self, effect: Dict[str, Any]): + """ + 初始化缓冲区 + + Args: + effect: 效果对象 + """ + effect_id = effect['id'] + buffers = {} + + # 输入和输出缓冲区 + buffers['input'] = np.zeros(self.buffer_size) + buffers['output'] = np.zeros(self.buffer_size) + + # 滤波器状态缓冲区 + buffers['pre_filter_state'] = np.zeros(2) # 双通道滤波器状态 + buffers['post_filter_state'] = np.zeros(2) # 双通道滤波器状态 + + effect['buffers'] = buffers + + def delete_effect(self, effect_id: str) -> bool: + """ + 删除失真效果 + + Args: + effect_id: 效果ID + + Returns: + 是否删除成功 + """ + if effect_id not in self.effects: + print(f"✗ 失真效果不存在: {effect_id}") + return False + + try: + # 删除效果 + del self.effects[effect_id] + + # 更新统计信息 + self.stats['active_effects'] -= 1 + + # 从插件中移除效果 + if self.plugin and effect_id in self.plugin.effects: + del self.plugin.effects[effect_id] + + print(f"✓ 失真效果已删除: {effect_id}") + return True + + except Exception as e: + print(f"✗ 删除失真效果失败: {e}") + return False + + def process(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: + """ + 处理失真效果 + + Args: + audio_data: 输入音频数据 + parameters: 效果参数 + + Returns: + 处理后的音频数据 + """ + try: + import time + process_start = time.time() + + # 如果输入是NumPy数组 + if isinstance(audio_data, np.ndarray): + processed_data = self._process_numpy_array(audio_data, parameters) + else: + # 对于其他类型的数据,返回原始数据 + processed_data = audio_data + + # 更新统计信息 + self.stats['effects_processed'] += 1 + self.stats['processing_time'] += (time.time() - process_start) + + return processed_data + + except Exception as e: + print(f"✗ 失真效果处理失败: {e}") + return audio_data + + def _process_numpy_array(self, audio_data: np.ndarray, parameters: Dict[str, Any]) -> np.ndarray: + """ + 处理NumPy数组音频数据 + + Args: + audio_data: 输入音频数据 + parameters: 效果参数 + + Returns: + 处理后的音频数据 + """ + # 获取参数 + drive = max(0.0, min(1.0, parameters.get('drive', 0.5))) + tone = max(0.0, min(1.0, parameters.get('tone', 0.5))) + level = max(0.0, min(1.0, parameters.get('level', 0.5))) + bias = max(-1.0, min(1.0, parameters.get('bias', 0.0))) + distortion_type = parameters.get('type', 'overdrive') + pre_filter = max(100.0, min(10000.0, parameters.get('pre_filter', 1000.0))) + post_filter = max(1000.0, min(20000.0, parameters.get('post_filter', 5000.0))) + rectification = parameters.get('rectification', 'full') + asymmetry = max(0.0, min(1.0, parameters.get('asymmetry', 0.0))) + mix = max(0.0, min(1.0, parameters.get('mix', 1.0))) + + # 创建输出数组 + if audio_data.ndim == 1: + # 单声道处理 + output_data = np.zeros_like(audio_data) + + for i in range(len(audio_data)): + sample = audio_data[i] + + # 应用偏置 + biased_sample = sample + bias + + # 预滤波 + filtered_sample = self._apply_pre_filter(biased_sample, pre_filter) + + # 应用失真 + distorted_sample = self._apply_distortion( + filtered_sample, drive, distortion_type, rectification, asymmetry) + + # 后滤波 + filtered_distorted = self._apply_post_filter(distorted_sample, post_filter, tone) + + # 应用输出电平 + leveled_sample = filtered_distorted * level + + # 混合干湿信号 + output_data[i] = sample * (1.0 - mix) + leveled_sample * mix + + else: + # 多声道处理 + output_data = np.zeros_like(audio_data) + + for channel in range(audio_data.shape[0]): + for i in range(audio_data.shape[1]): + sample = audio_data[channel, i] + + # 应用偏置 + biased_sample = sample + bias + + # 预滤波 + filtered_sample = self._apply_pre_filter(biased_sample, pre_filter) + + # 应用失真 + distorted_sample = self._apply_distortion( + filtered_sample, drive, distortion_type, rectification, asymmetry) + + # 后滤波 + filtered_distorted = self._apply_post_filter(distorted_sample, post_filter, tone) + + # 应用输出电平 + leveled_sample = filtered_distorted * level + + # 混合干湿信号 + output_data[channel, i] = sample * (1.0 - mix) + leveled_sample * mix + + # 确保输出在有效范围内 + processed_data = np.clip(output_data, -1.0, 1.0) + return processed_data + + def _apply_pre_filter(self, sample: float, cutoff: float) -> float: + """ + 应用预滤波器 + + Args: + sample: 输入样本 + cutoff: 截止频率 (Hz) + + Returns: + 滤波后的样本 + """ + # 简化的低通滤波器 + # 实际应用中会使用更复杂的滤波器设计 + + # 计算滤波系数 + rc = 1.0 / (2.0 * math.pi * cutoff) + dt = 1.0 / self.sample_rate + alpha = dt / (rc + dt) + + # 简单的一阶低通滤波 + filtered = alpha * sample + (1.0 - alpha) * sample # 简化实现 + return filtered + + def _apply_distortion(self, sample: float, drive: float, + distortion_type: str, rectification: str, asymmetry: float) -> float: + """ + 应用失真效果 + + Args: + sample: 输入样本 + drive: 驱动强度 + distortion_type: 失真类型 + rectification: 整流类型 + asymmetry: 不对称度 + + Returns: + 失真后的样本 + """ + # 应用驱动 + driven_sample = sample * (1.0 + drive * 10.0) + + # 根据类型应用失真 + if distortion_type == 'overdrive': + distorted = self._soft_clip(driven_sample) + elif distortion_type == 'distortion': + distorted = self._hard_clip(driven_sample) + elif distortion_type == 'fuzz': + distorted = self._fuzz_distort(driven_sample) + elif distortion_type == 'clipper': + distorted = self._asymmetric_clip(driven_sample, asymmetry) + else: + distorted = self._soft_clip(driven_sample) + + # 应用整流 + if rectification == 'full': + rectified = abs(distorted) + elif rectification == 'half': + rectified = max(0.0, distorted) + else: + rectified = distorted + + return rectified + + def _soft_clip(self, sample: float) -> float: + """ + 软削波 + + Args: + sample: 输入样本 + + Returns: + 削波后的样本 + """ + # 双曲正切软削波 + return math.tanh(sample) + + def _hard_clip(self, sample: float) -> float: + """ + 硬削波 + + Args: + sample: 输入样本 + + Returns: + 削波后的样本 + """ + # 简单的硬削波 + return max(-1.0, min(1.0, sample)) + + def _fuzz_distort(self, sample: float) -> float: + """ + 模糊失真 + + Args: + sample: 输入样本 + + Returns: + 失真后的样本 + """ + # 强烈的非线性失真 + amplified = sample * 10.0 + if amplified > 0: + fuzzed = 1.0 - math.exp(-amplified) + else: + fuzzed = -1.0 + math.exp(amplified) + return fuzzed + + def _asymmetric_clip(self, sample: float, asymmetry: float) -> float: + """ + 不对称削波 + + Args: + sample: 输入样本 + asymmetry: 不对称度 + + Returns: + 削波后的样本 + """ + positive_limit = 1.0 + negative_limit = -1.0 + asymmetry * 0.5 + + if sample > positive_limit: + return positive_limit + elif sample < negative_limit: + return negative_limit + else: + return sample + + def _apply_post_filter(self, sample: float, cutoff: float, tone: float) -> float: + """ + 应用后滤波器 + + Args: + sample: 输入样本 + cutoff: 截止频率 (Hz) + tone: 音调控制 + + Returns: + 滤波后的样本 + """ + # 结合音调控制的滤波器 + adjusted_cutoff = cutoff * (0.5 + tone * 0.5) + + # 简化的滤波器 + rc = 1.0 / (2.0 * math.pi * adjusted_cutoff) + dt = 1.0 / self.sample_rate + alpha = dt / (rc + dt) + + # 简单的一阶滤波 + filtered = alpha * sample + (1.0 - alpha) * sample # 简化实现 + return filtered + + def set_parameter(self, effect_id: str, parameter: str, value: Any) -> bool: + """ + 设置效果参数 + + Args: + effect_id: 效果ID + parameter: 参数名 + value: 参数值 + + Returns: + 是否设置成功 + """ + if effect_id not in self.effects: + print(f"✗ 失真效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['parameters'][parameter] = value + + # 如果是插件中的效果,也更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['parameters'][parameter] = value + + print(f"✓ 失真效果参数已设置: {effect_id}.{parameter} = {value}") + return True + + except Exception as e: + print(f"✗ 设置失真效果参数失败: {e}") + return False + + def get_parameters(self, effect_id: str) -> Dict[str, Any]: + """ + 获取效果参数 + + Args: + effect_id: 效果ID + + Returns: + 参数字典 + """ + if effect_id not in self.effects: + return {} + + return self.effects[effect_id]['parameters'].copy() + + def enable_effect(self, effect_id: str) -> bool: + """ + 启用失真效果 + + Args: + effect_id: 效果ID + + Returns: + 是否启用成功 + """ + if effect_id not in self.effects: + print(f"✗ 失真效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['active'] = True + self.stats['active_effects'] += 1 + + # 如果是插件中的效果,也更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['active'] = True + + print(f"✓ 失真效果已启用: {effect_id}") + return True + + except Exception as e: + print(f"✗ 启用失真效果失败: {e}") + return False + + def disable_effect(self, effect_id: str) -> bool: + """ + 禁用失真效果 + + Args: + effect_id: 效果ID + + Returns: + 是否禁用成功 + """ + if effect_id not in self.effects: + print(f"✗ 失真效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['active'] = False + self.stats['active_effects'] -= 1 + + # 如果是插件中的效果,也更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['active'] = False + + print(f"✓ 失真效果已禁用: {effect_id}") + return True + + except Exception as e: + print(f"✗ 禁用失真效果失败: {e}") + return False + + def cleanup(self): + """清理所有资源""" + self.effects.clear() + self.stats = { + 'total_effects': 0, + 'active_effects': 0, + 'effects_processed': 0, + 'processing_time': 0.0, + 'memory_usage': 0 + } + print("✓ 失真效果处理器资源已清理") + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def _get_current_time(self) -> float: + """ + 获取当前时间(秒) + + Returns: + 当前时间 + """ + if self.plugin and self.plugin.plugin_manager and self.plugin.plugin_manager.world: + return self.plugin.plugin_manager.world.globalClock.getFrameTime() + import time + return time.time() + + def update(self, dt: float): + """ + 更新处理器状态 + + Args: + dt: 时间增量 + """ + # 这里可以更新任何需要定期更新的状态 + pass + + def set_sample_rate(self, sample_rate: int): + """ + 设置采样率 + + Args: + sample_rate: 采样率 (Hz) + """ + self.sample_rate = sample_rate + print(f"✓ 失真效果处理器采样率已设置: {sample_rate} Hz") + + def set_drive(self, effect_id: str, drive: float) -> bool: + """ + 设置驱动强度 + + Args: + effect_id: 效果ID + drive: 驱动强度 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'drive', max(0.0, min(1.0, drive))) + + def set_tone(self, effect_id: str, tone: float) -> bool: + """ + 设置音调 + + Args: + effect_id: 效果ID + tone: 音调 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'tone', max(0.0, min(1.0, tone))) + + def set_level(self, effect_id: str, level: float) -> bool: + """ + 设置输出电平 + + Args: + effect_id: 效果ID + level: 输出电平 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'level', max(0.0, min(1.0, level))) + + def set_bias(self, effect_id: str, bias: float) -> bool: + """ + 设置偏置 + + Args: + effect_id: 效果ID + bias: 偏置 (-1.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'bias', max(-1.0, min(1.0, bias))) + + def set_type(self, effect_id: str, distortion_type: str) -> bool: + """ + 设置失真类型 + + Args: + effect_id: 效果ID + distortion_type: 失真类型 (overdrive, distortion, fuzz, clipper) + + Returns: + 是否设置成功 + """ + valid_types = ['overdrive', 'distortion', 'fuzz', 'clipper'] + if distortion_type not in valid_types: + print(f"✗ 无效的失真类型: {distortion_type}") + return False + + return self.set_parameter(effect_id, 'type', distortion_type) + + def set_pre_filter(self, effect_id: str, frequency: float) -> bool: + """ + 设置预滤波器频率 + + Args: + effect_id: 效果ID + frequency: 频率 (Hz) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'pre_filter', max(100.0, min(10000.0, frequency))) + + def set_post_filter(self, effect_id: str, frequency: float) -> bool: + """ + 设置后滤波器频率 + + Args: + effect_id: 效果ID + frequency: 频率 (Hz) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'post_filter', max(1000.0, min(20000.0, frequency))) + + def set_rectification(self, effect_id: str, rectification: str) -> bool: + """ + 设置整流类型 + + Args: + effect_id: 效果ID + rectification: 整流类型 (full, half, none) + + Returns: + 是否设置成功 + """ + valid_rectifications = ['full', 'half', 'none'] + if rectification not in valid_rectifications: + print(f"✗ 无效的整流类型: {rectification}") + return False + + return self.set_parameter(effect_id, 'rectification', rectification) + + def set_asymmetry(self, effect_id: str, asymmetry: float) -> bool: + """ + 设置不对称度 + + Args: + effect_id: 效果ID + asymmetry: 不对称度 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'asymmetry', max(0.0, min(1.0, asymmetry))) + + def set_mix(self, effect_id: str, mix: float) -> bool: + """ + 设置湿信号混合 + + Args: + effect_id: 效果ID + mix: 湿信号混合 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'mix', max(0.0, min(1.0, mix))) + + def create_preset(self, preset_name: str) -> Dict[str, Any]: + """ + 创建失真效果预设 + + Args: + preset_name: 预设名称 + + Returns: + 预设参数字典 + """ + presets = { + 'mild_overdrive': { + 'drive': 0.3, + 'tone': 0.7, + 'level': 0.6, + 'type': 'overdrive', + 'pre_filter': 2000.0, + 'post_filter': 6000.0, + 'rectification': 'none', + 'mix': 1.0 + }, + 'heavy_overdrive': { + 'drive': 0.7, + 'tone': 0.5, + 'level': 0.8, + 'type': 'overdrive', + 'pre_filter': 1000.0, + 'post_filter': 4000.0, + 'rectification': 'none', + 'mix': 1.0 + }, + 'classic_distortion': { + 'drive': 0.8, + 'tone': 0.6, + 'level': 0.7, + 'type': 'distortion', + 'pre_filter': 800.0, + 'post_filter': 3000.0, + 'rectification': 'none', + 'mix': 1.0 + }, + 'metal_distortion': { + 'drive': 1.0, + 'tone': 0.4, + 'level': 0.9, + 'type': 'distortion', + 'pre_filter': 500.0, + 'post_filter': 2000.0, + 'rectification': 'none', + 'mix': 1.0 + }, + 'fuzz_box': { + 'drive': 0.9, + 'tone': 0.3, + 'level': 0.8, + 'type': 'fuzz', + 'pre_filter': 300.0, + 'post_filter': 1500.0, + 'rectification': 'full', + 'mix': 1.0 + }, + 'bass_overdrive': { + 'drive': 0.4, + 'tone': 0.8, + 'level': 0.7, + 'type': 'overdrive', + 'pre_filter': 3000.0, + 'post_filter': 8000.0, + 'rectification': 'none', + 'mix': 1.0 + }, + 'tube_amp': { + 'drive': 0.6, + 'tone': 0.7, + 'level': 0.75, + 'type': 'overdrive', + 'pre_filter': 1500.0, + 'post_filter': 5000.0, + 'rectification': 'none', + 'asymmetry': 0.3, + 'mix': 1.0 + }, + 'bit_crusher': { + 'drive': 0.5, + 'tone': 0.4, + 'level': 0.6, + 'type': 'distortion', + 'pre_filter': 4000.0, + 'post_filter': 10000.0, + 'rectification': 'none', + 'mix': 1.0 + } + } + + return presets.get(preset_name, {}).copy() + + def get_drive_meter(self, effect_id: str) -> float: + """ + 获取驱动表 + + Args: + effect_id: 效果ID + + Returns: + 当前驱动值 (0.0-1.0) + """ + if effect_id not in self.effects: + return 0.0 + + return self.effects[effect_id]['parameters'].get('drive', 0.0) + + def get_tone_meter(self, effect_id: str) -> float: + """ + 获取音调表 + + Args: + effect_id: 效果ID + + Returns: + 当前音调值 (0.0-1.0) + """ + if effect_id not in self.effects: + return 0.0 + + return self.effects[effect_id]['parameters'].get('tone', 0.0) + diff --git a/plugins/user/audio_reverb_effects/effects/eq_processor.py b/plugins/user/audio_reverb_effects/effects/eq_processor.py index 0e98beb9..57c78c88 100644 --- a/plugins/user/audio_reverb_effects/effects/eq_processor.py +++ b/plugins/user/audio_reverb_effects/effects/eq_processor.py @@ -1,881 +1,881 @@ -""" -均衡器效果处理器 -实现专业的音频均衡器效果处理 -""" - -import uuid -import math -import numpy as np -from typing import Dict, List, Any, Optional -from scipy import signal - -class EQProcessor: - """ - 均衡器效果处理器 - 实现专业的音频均衡器效果处理 - """ - - def __init__(self, plugin): - """ - 初始化均衡器效果处理器 - - Args: - plugin: 音频效果插件实例 - """ - self.plugin = plugin - self.effects: Dict[str, Dict[str, Any]] = {} - self.filters = {} # 滤波器缓存 - self.stats = { - 'total_effects': 0, - 'active_effects': 0, - 'effects_processed': 0, - 'processing_time': 0.0, - 'memory_usage': 0 - } - self.buffer_size = plugin.buffer_size if plugin else 4096 - self.sample_rate = 44100 # 默认采样率 - self.max_bands = 32 # 最大频段数 - - def create_effect(self, parameters: Dict[str, Any]) -> str: - """ - 创建均衡器效果 - - Args: - parameters: 均衡器参数 - - Returns: - 效果ID - """ - try: - # 生成唯一ID - effect_id = str(uuid.uuid4()) - - # 默认参数 - default_params = { - 'bands': { - 60: 0.0, # 60Hz - 低频 - 200: 0.0, # 200Hz - 低中频 - 500: 0.0, # 500Hz - 中频 - 1000: 0.0, # 1kHz - 中高频 - 3000: 0.0, # 3kHz - 高频 - 8000: 0.0, # 8kHz - 超高频 - 12000: 0.0 # 12kHz - 极高频 - }, - 'master_gain': 0.0, # 主增益 (dB) - 'filter_type': 'peaking', # 滤波器类型: peaking, low_shelf, high_shelf - 'q_factor': 1.0, # Q因子 - 'solo_band': None, # 独奏频段 - 'bypass_bands': [] # 绕过频段 - } - - # 合并参数 - effect_params = default_params.copy() - effect_params.update(parameters) - - # 验证频段数 - if len(effect_params['bands']) > self.max_bands: - # 限制频段数 - bands = dict(list(effect_params['bands'].items())[:self.max_bands]) - effect_params['bands'] = bands - print(f"⚠ 频段数超过限制,已限制为 {self.max_bands} 个频段") - - # 创建效果对象 - effect = { - 'id': effect_id, - 'type': 'eq', - 'parameters': effect_params, - 'active': True, - 'filters': {}, # 各频段滤波器 - 'buffers': {}, # 缓冲区 - 'created_time': self._get_current_time(), - 'process_count': 0 - } - - # 初始化滤波器 - self._initialize_filters(effect) - - # 初始化缓冲区 - self._initialize_buffers(effect) - - # 存储效果 - self.effects[effect_id] = effect - - # 更新统计信息 - self.stats['total_effects'] += 1 - self.stats['active_effects'] += 1 - - # 在插件中注册效果 - if self.plugin: - self.plugin.effects[effect_id] = effect.copy() - - print(f"✓ 均衡器效果创建成功: {effect_id}") - return effect_id - - except Exception as e: - print(f"✗ 创建均衡器效果失败: {e}") - import traceback - traceback.print_exc() - return "" - - def _initialize_filters(self, effect: Dict[str, Any]): - """ - 初始化均衡器滤波器 - - Args: - effect: 效果对象 - """ - effect_id = effect['id'] - parameters = effect['parameters'] - bands = parameters['bands'] - filter_type = parameters.get('filter_type', 'peaking') - q_factor = parameters.get('q_factor', 1.0) - - filters = {} - - # 为每个频段创建滤波器 - for frequency, gain in bands.items(): - # 限制频率范围 - frequency = max(20, min(20000, frequency)) - - # 限制增益范围 - gain = max(-24, min(24, gain)) - - # 创建滤波器系数 - filter_coeffs = self._create_filter_coefficients( - frequency, gain, q_factor, filter_type) - - filters[frequency] = { - 'frequency': frequency, - 'gain': gain, - 'q_factor': q_factor, - 'type': filter_type, - 'coeffs': filter_coeffs, - 'state': np.zeros(2) # 滤波器状态 - } - - effect['filters'] = filters - - def _create_filter_coefficients(self, frequency: float, gain: float, - q_factor: float, filter_type: str) -> Dict[str, float]: - """ - 创建滤波器系数 - - Args: - frequency: 频率 (Hz) - gain: 增益 (dB) - q_factor: Q因子 - filter_type: 滤波器类型 - - Returns: - 滤波器系数字典 - """ - # 将增益从dB转换为线性 - linear_gain = 10 ** (gain / 20.0) - - # 归一化频率 - nyquist = self.sample_rate / 2.0 - normalized_freq = frequency / nyquist - - # 根据滤波器类型创建系数 - if filter_type == 'peaking': - # 峰值滤波器 - coeffs = self._design_peaking_filter(normalized_freq, linear_gain, q_factor) - elif filter_type == 'low_shelf': - # 低频搁架滤波器 - coeffs = self._design_low_shelf_filter(normalized_freq, linear_gain, q_factor) - elif filter_type == 'high_shelf': - # 高频搁架滤波器 - coeffs = self._design_high_shelf_filter(normalized_freq, linear_gain, q_factor) - else: - # 默认使用峰值滤波器 - coeffs = self._design_peaking_filter(normalized_freq, linear_gain, q_factor) - - return coeffs - - def _design_peaking_filter(self, freq: float, gain: float, q: float) -> Dict[str, float]: - """ - 设计峰值滤波器 - - Args: - freq: 归一化频率 - gain: 线性增益 - q: Q因子 - - Returns: - 滤波器系数 - """ - # 这里使用简化的设计方法 - # 实际应用中会使用更精确的双线性变换设计 - - # 计算带宽 - bandwidth = freq / q - - # 简化的系数计算 - alpha = bandwidth / 2.0 - beta = 0.5 * ((1.0 - alpha) / (1.0 + alpha)) - gamma = (0.5 + beta) * math.cos(2 * math.pi * freq) - - if gain >= 1.0: - # 提升 - delta = (0.5 + beta) * gain - (0.5 - beta) / gain - epsilon = ((0.5 + beta) / gain - (0.5 - beta) * gain) * math.cos(2 * math.pi * freq) - else: - # 衰减 - delta = (0.5 + beta) / gain - (0.5 - beta) * gain - epsilon = ((0.5 + beta) * gain - (0.5 - beta) / gain) * math.cos(2 * math.pi * freq) - - # 滤波器系数 - a0 = (0.5 + beta) * gain - a1 = -gamma - a2 = (0.5 - beta) * gain - b0 = (0.5 + beta) - b1 = -delta - b2 = (0.5 - beta) - - return { - 'a0': a0, 'a1': a1, 'a2': a2, - 'b0': b0, 'b1': b1, 'b2': b2 - } - - def _design_low_shelf_filter(self, freq: float, gain: float, q: float) -> Dict[str, float]: - """ - 设计低频搁架滤波器 - - Args: - freq: 归一化频率 - gain: 线性增益 - q: Q因子 - - Returns: - 滤波器系数 - """ - # 简化的低频搁架滤波器设计 - return self._design_peaking_filter(freq, gain, q) - - def _design_high_shelf_filter(self, freq: float, gain: float, q: float) -> Dict[str, float]: - """ - 设计高频搁架滤波器 - - Args: - freq: 归一化频率 - gain: 线性增益 - q: Q因子 - - Returns: - 滤波器系数 - """ - # 简化的高频搁架滤波器设计 - return self._design_peaking_filter(freq, gain, q) - - def _initialize_buffers(self, effect: Dict[str, Any]): - """ - 初始化缓冲区 - - Args: - effect: 效果对象 - """ - effect_id = effect['id'] - buffers = {} - - # 输入和输出缓冲区 - buffers['input'] = np.zeros(self.buffer_size) - buffers['output'] = np.zeros(self.buffer_size) - - effect['buffers'] = buffers - - def delete_effect(self, effect_id: str) -> bool: - """ - 删除均衡器效果 - - Args: - effect_id: 效果ID - - Returns: - 是否删除成功 - """ - if effect_id not in self.effects: - print(f"✗ 均衡器效果不存在: {effect_id}") - return False - - try: - # 删除效果 - del self.effects[effect_id] - - # 更新统计信息 - self.stats['active_effects'] -= 1 - - # 从插件中移除效果 - if self.plugin and effect_id in self.plugin.effects: - del self.plugin.effects[effect_id] - - print(f"✓ 均衡器效果已删除: {effect_id}") - return True - - except Exception as e: - print(f"✗ 删除均衡器效果失败: {e}") - return False - - def process(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: - """ - 处理均衡器效果 - - Args: - audio_data: 输入音频数据 - parameters: 效果参数 - - Returns: - 处理后的音频数据 - """ - try: - import time - process_start = time.time() - - # 如果输入是NumPy数组 - if isinstance(audio_data, np.ndarray): - processed_data = self._process_numpy_array(audio_data, parameters) - else: - # 对于其他类型的数据,返回原始数据 - processed_data = audio_data - - # 更新统计信息 - self.stats['effects_processed'] += 1 - self.stats['processing_time'] += (time.time() - process_start) - - return processed_data - - except Exception as e: - print(f"✗ 均衡器效果处理失败: {e}") - return audio_data - - def _process_numpy_array(self, audio_data: np.ndarray, parameters: Dict[str, Any]) -> np.ndarray: - """ - 处理NumPy数组音频数据 - - Args: - audio_data: 输入音频数据 - parameters: 效果参数 - - Returns: - 处理后的音频数据 - """ - # 获取参数 - bands = parameters.get('bands', {}) - master_gain = parameters.get('master_gain', 0.0) - solo_band = parameters.get('solo_band', None) - bypass_bands = parameters.get('bypass_bands', []) - - # 应用主增益 - if master_gain != 0.0: - linear_master_gain = 10 ** (master_gain / 20.0) - audio_data = audio_data * linear_master_gain - - # 应用各频段增益 - if bands: - # 如果有独奏频段,只处理该频段 - if solo_band is not None and solo_band in bands: - frequency = solo_band - gain = bands[frequency] - if frequency not in bypass_bands: - audio_data = self._apply_band_filter(audio_data, frequency, gain) - else: - # 处理所有频段 - for frequency, gain in bands.items(): - if frequency not in bypass_bands: - audio_data = self._apply_band_filter(audio_data, frequency, gain) - - # 确保输出在有效范围内 - processed_data = np.clip(audio_data, -1.0, 1.0) - return processed_data - - def _apply_band_filter(self, audio_data: np.ndarray, frequency: float, gain: float) -> np.ndarray: - """ - 应用单个频段滤波器 - - Args: - audio_data: 输入音频数据 - frequency: 频率 (Hz) - gain: 增益 (dB) - - Returns: - 处理后的音频数据 - """ - # 这里实现简化的频段处理 - # 实际应用中会使用更复杂的滤波器设计 - - if gain == 0.0: - return audio_data - - # 将增益从dB转换为线性 - linear_gain = 10 ** (gain / 20.0) - - # 简化的频率响应应用 - # 根据频率调整处理方式 - if frequency < 200: - # 低频 - 更平滑的处理 - processed = audio_data * (0.5 + 0.5 * linear_gain) - elif frequency < 2000: - # 中频 - 标准处理 - processed = audio_data * linear_gain - else: - # 高频 - 更锐利的处理 - processed = audio_data * (1.0 + 0.2 * (linear_gain - 1.0)) - - return processed - - def set_parameter(self, effect_id: str, parameter: str, value: Any) -> bool: - """ - 设置效果参数 - - Args: - effect_id: 效果ID - parameter: 参数名 - value: 参数值 - - Returns: - 是否设置成功 - """ - if effect_id not in self.effects: - print(f"✗ 均衡器效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['parameters'][parameter] = value - - # 如果是滤波器参数,重新初始化滤波器 - if parameter == 'bands': - self._initialize_filters(self.effects[effect_id]) - - # 如果是插件中的效果,也更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['parameters'][parameter] = value - if parameter == 'bands': - self.plugin.effects[effect_id]['filters'] = self.effects[effect_id]['filters'].copy() - - print(f"✓ 均衡器效果参数已设置: {effect_id}.{parameter} = {value}") - return True - - except Exception as e: - print(f"✗ 设置均衡器效果参数失败: {e}") - return False - - def get_parameters(self, effect_id: str) -> Dict[str, Any]: - """ - 获取效果参数 - - Args: - effect_id: 效果ID - - Returns: - 参数字典 - """ - if effect_id not in self.effects: - return {} - - return self.effects[effect_id]['parameters'].copy() - - def enable_effect(self, effect_id: str) -> bool: - """ - 启用均衡器效果 - - Args: - effect_id: 效果ID - - Returns: - 是否启用成功 - """ - if effect_id not in self.effects: - print(f"✗ 均衡器效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['active'] = True - self.stats['active_effects'] += 1 - - # 如果是插件中的效果,也更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['active'] = True - - print(f"✓ 均衡器效果已启用: {effect_id}") - return True - - except Exception as e: - print(f"✗ 启用均衡器效果失败: {e}") - return False - - def disable_effect(self, effect_id: str) -> bool: - """ - 禁用均衡器效果 - - Args: - effect_id: 效果ID - - Returns: - 是否禁用成功 - """ - if effect_id not in self.effects: - print(f"✗ 均衡器效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['active'] = False - self.stats['active_effects'] -= 1 - - # 如果是插件中的效果,也更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['active'] = False - - print(f"✓ 均衡器效果已禁用: {effect_id}") - return True - - except Exception as e: - print(f"✗ 禁用均衡器效果失败: {e}") - return False - - def cleanup(self): - """清理所有资源""" - self.effects.clear() - self.filters.clear() - self.stats = { - 'total_effects': 0, - 'active_effects': 0, - 'effects_processed': 0, - 'processing_time': 0.0, - 'memory_usage': 0 - } - print("✓ 均衡器效果处理器资源已清理") - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def _get_current_time(self) -> float: - """ - 获取当前时间(秒) - - Returns: - 当前时间 - """ - if self.plugin and self.plugin.plugin_manager and self.plugin.plugin_manager.world: - return self.plugin.plugin_manager.world.globalClock.getFrameTime() - import time - return time.time() - - def update(self, dt: float): - """ - 更新处理器状态 - - Args: - dt: 时间增量 - """ - # 这里可以更新任何需要定期更新的状态 - pass - - def set_sample_rate(self, sample_rate: int): - """ - 设置采样率 - - Args: - sample_rate: 采样率 (Hz) - """ - old_sample_rate = self.sample_rate - self.sample_rate = sample_rate - print(f"✓ 均衡器处理器采样率已从 {old_sample_rate} 更新为: {sample_rate} Hz") - - # 重新初始化所有滤波器以适应新的采样率 - for effect_id, effect in self.effects.items(): - self._initialize_filters(effect) - - def set_band_gain(self, effect_id: str, frequency: int, gain: float) -> bool: - """ - 设置频段增益 - - Args: - effect_id: 效果ID - frequency: 频率 (Hz) - gain: 增益 (dB) - - Returns: - 是否设置成功 - """ - if effect_id not in self.effects: - print(f"✗ 均衡器效果不存在: {effect_id}") - return False - - try: - # 限制增益范围 - gain = max(-24, min(24, gain)) - - # 更新频段增益 - parameters = self.effects[effect_id]['parameters'] - parameters['bands'][frequency] = gain - - # 更新滤波器 - self._initialize_filters(self.effects[effect_id]) - - # 更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['parameters']['bands'][frequency] = gain - self.plugin.effects[effect_id]['filters'] = self.effects[effect_id]['filters'].copy() - - print(f"✓ 频段增益已设置: {effect_id} -> {frequency}Hz: {gain}dB") - return True - - except Exception as e: - print(f"✗ 设置频段增益失败: {e}") - return False - - def set_master_gain(self, effect_id: str, gain: float) -> bool: - """ - 设置主增益 - - Args: - effect_id: 效果ID - gain: 主增益 (dB) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'master_gain', max(-24, min(24, gain))) - - def set_solo_band(self, effect_id: str, frequency: Optional[int]) -> bool: - """ - 设置独奏频段 - - Args: - effect_id: 效果ID - frequency: 频率 (Hz),None表示取消独奏 - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'solo_band', frequency) - - def bypass_band(self, effect_id: str, frequency: int, bypass: bool = True) -> bool: - """ - 绕过频段 - - Args: - effect_id: 效果ID - frequency: 频率 (Hz) - bypass: 是否绕过 - - Returns: - 是否设置成功 - """ - if effect_id not in self.effects: - print(f"✗ 均衡器效果不存在: {effect_id}") - return False - - try: - parameters = self.effects[effect_id]['parameters'] - bypass_bands = parameters.get('bypass_bands', []) - - if bypass and frequency not in bypass_bands: - bypass_bands.append(frequency) - elif not bypass and frequency in bypass_bands: - bypass_bands.remove(frequency) - - parameters['bypass_bands'] = bypass_bands - - # 更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['parameters']['bypass_bands'] = bypass_bands.copy() - - print(f"✓ 频段绕过已设置: {effect_id} -> {frequency}Hz: {'是' if bypass else '否'}") - return True - - except Exception as e: - print(f"✗ 设置频段绕过失败: {e}") - return False - - def add_band(self, effect_id: str, frequency: int, gain: float = 0.0) -> bool: - """ - 添加频段 - - Args: - effect_id: 效果ID - frequency: 频率 (Hz) - gain: 增益 (dB) - - Returns: - 是否添加成功 - """ - if effect_id not in self.effects: - print(f"✗ 均衡器效果不存在: {effect_id}") - return False - - try: - # 检查频段数是否超过限制 - parameters = self.effects[effect_id]['parameters'] - bands = parameters['bands'] - - if len(bands) >= self.max_bands: - print(f"✗ 频段数已达到最大限制: {self.max_bands}") - return False - - # 添加频段 - bands[frequency] = gain - - # 重新初始化滤波器 - self._initialize_filters(self.effects[effect_id]) - - # 更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['parameters']['bands'] = bands.copy() - self.plugin.effects[effect_id]['filters'] = self.effects[effect_id]['filters'].copy() - - print(f"✓ 频段已添加: {effect_id} -> {frequency}Hz: {gain}dB") - return True - - except Exception as e: - print(f"✗ 添加频段失败: {e}") - return False - - def remove_band(self, effect_id: str, frequency: int) -> bool: - """ - 移除频段 - - Args: - effect_id: 效果ID - frequency: 频率 (Hz) - - Returns: - 是否移除成功 - """ - if effect_id not in self.effects: - print(f"✗ 均衡器效果不存在: {effect_id}") - return False - - try: - parameters = self.effects[effect_id]['parameters'] - bands = parameters['bands'] - - if frequency in bands: - del bands[frequency] - - # 重新初始化滤波器 - self._initialize_filters(self.effects[effect_id]) - - # 更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['parameters']['bands'] = bands.copy() - self.plugin.effects[effect_id]['filters'] = self.effects[effect_id]['filters'].copy() - - print(f"✓ 频段已移除: {effect_id} -> {frequency}Hz") - return True - else: - print(f"⚠ 频段不存在: {effect_id} -> {frequency}Hz") - return False - - except Exception as e: - print(f"✗ 移除频段失败: {e}") - return False - - def get_frequency_response(self, effect_id: str, frequencies: List[float] = None) -> Dict[str, Any]: - """ - 获取频率响应 - - Args: - effect_id: 效果ID - frequencies: 频率点列表,如果为None则使用默认频率点 - - Returns: - 频率响应数据 - """ - if effect_id not in self.effects: - return {} - - if frequencies is None: - # 默认频率点(对数分布) - frequencies = np.logspace(np.log10(20), np.log10(20000), 1000) - - parameters = self.effects[effect_id]['parameters'] - bands = parameters['bands'] - - # 计算每个频率点的响应 - response = [] - for freq in frequencies: - gain = 0.0 - # 计算所有频段在该频率点的贡献 - for band_freq, band_gain in bands.items(): - # 简化的频率响应计算 - distance = abs(freq - band_freq) / band_freq - if distance < 1.0: # 在影响范围内 - influence = 1.0 - distance - gain += band_gain * influence - - response.append(gain) - - return { - 'frequencies': frequencies.tolist(), - 'response': response, - 'master_gain': parameters.get('master_gain', 0.0) - } - - def create_preset(self, preset_name: str) -> Dict[str, Any]: - """ - 创建均衡器预设 - - Args: - preset_name: 预设名称 - - Returns: - 预设参数字典 - """ - presets = { - 'flat': { - 'bands': { - 60: 0.0, 200: 0.0, 500: 0.0, 1000: 0.0, 3000: 0.0, 8000: 0.0, 12000: 0.0 - } - }, - 'pop': { - 'bands': { - 60: -1.0, 200: 2.0, 500: 3.0, 1000: 1.0, 3000: 2.0, 8000: 3.0, 12000: 2.0 - }, - 'master_gain': 1.0 - }, - 'rock': { - 'bands': { - 60: 3.0, 200: 2.0, 500: -1.0, 1000: -2.0, 3000: 1.0, 8000: 3.0, 12000: 2.0 - }, - 'master_gain': 2.0 - }, - 'jazz': { - 'bands': { - 60: 2.0, 200: 1.0, 500: 0.0, 1000: -1.0, 3000: 1.0, 8000: 2.0, 12000: 1.0 - }, - 'master_gain': 1.0 - }, - 'classical': { - 'bands': { - 60: -2.0, 200: -1.0, 500: 0.0, 1000: 1.0, 3000: 2.0, 8000: 1.0, 12000: 0.0 - }, - 'master_gain': 0.0 - }, - 'vocal_booster': { - 'bands': { - 60: -2.0, 200: -1.0, 500: 1.0, 1000: 3.0, 3000: 2.0, 8000: 1.0, 12000: 0.0 - }, - 'master_gain': 1.0 - }, - 'bass_boost': { - 'bands': { - 60: 5.0, 200: 3.0, 500: 1.0, 1000: 0.0, 3000: -1.0, 8000: -2.0, 12000: -3.0 - }, - 'master_gain': 2.0 - }, - 'treble_boost': { - 'bands': { - 60: -3.0, 200: -2.0, 500: -1.0, 1000: 0.0, 3000: 1.0, 8000: 3.0, 12000: 5.0 - }, - 'master_gain': 1.0 - } - } - - return presets.get(preset_name, {}).copy() - +""" +均衡器效果处理器 +实现专业的音频均衡器效果处理 +""" + +import uuid +import math +import numpy as np +from typing import Dict, List, Any, Optional +from scipy import signal + +class EQProcessor: + """ + 均衡器效果处理器 + 实现专业的音频均衡器效果处理 + """ + + def __init__(self, plugin): + """ + 初始化均衡器效果处理器 + + Args: + plugin: 音频效果插件实例 + """ + self.plugin = plugin + self.effects: Dict[str, Dict[str, Any]] = {} + self.filters = {} # 滤波器缓存 + self.stats = { + 'total_effects': 0, + 'active_effects': 0, + 'effects_processed': 0, + 'processing_time': 0.0, + 'memory_usage': 0 + } + self.buffer_size = plugin.buffer_size if plugin else 4096 + self.sample_rate = 44100 # 默认采样率 + self.max_bands = 32 # 最大频段数 + + def create_effect(self, parameters: Dict[str, Any]) -> str: + """ + 创建均衡器效果 + + Args: + parameters: 均衡器参数 + + Returns: + 效果ID + """ + try: + # 生成唯一ID + effect_id = str(uuid.uuid4()) + + # 默认参数 + default_params = { + 'bands': { + 60: 0.0, # 60Hz - 低频 + 200: 0.0, # 200Hz - 低中频 + 500: 0.0, # 500Hz - 中频 + 1000: 0.0, # 1kHz - 中高频 + 3000: 0.0, # 3kHz - 高频 + 8000: 0.0, # 8kHz - 超高频 + 12000: 0.0 # 12kHz - 极高频 + }, + 'master_gain': 0.0, # 主增益 (dB) + 'filter_type': 'peaking', # 滤波器类型: peaking, low_shelf, high_shelf + 'q_factor': 1.0, # Q因子 + 'solo_band': None, # 独奏频段 + 'bypass_bands': [] # 绕过频段 + } + + # 合并参数 + effect_params = default_params.copy() + effect_params.update(parameters) + + # 验证频段数 + if len(effect_params['bands']) > self.max_bands: + # 限制频段数 + bands = dict(list(effect_params['bands'].items())[:self.max_bands]) + effect_params['bands'] = bands + print(f"⚠ 频段数超过限制,已限制为 {self.max_bands} 个频段") + + # 创建效果对象 + effect = { + 'id': effect_id, + 'type': 'eq', + 'parameters': effect_params, + 'active': True, + 'filters': {}, # 各频段滤波器 + 'buffers': {}, # 缓冲区 + 'created_time': self._get_current_time(), + 'process_count': 0 + } + + # 初始化滤波器 + self._initialize_filters(effect) + + # 初始化缓冲区 + self._initialize_buffers(effect) + + # 存储效果 + self.effects[effect_id] = effect + + # 更新统计信息 + self.stats['total_effects'] += 1 + self.stats['active_effects'] += 1 + + # 在插件中注册效果 + if self.plugin: + self.plugin.effects[effect_id] = effect.copy() + + print(f"✓ 均衡器效果创建成功: {effect_id}") + return effect_id + + except Exception as e: + print(f"✗ 创建均衡器效果失败: {e}") + import traceback + traceback.print_exc() + return "" + + def _initialize_filters(self, effect: Dict[str, Any]): + """ + 初始化均衡器滤波器 + + Args: + effect: 效果对象 + """ + effect_id = effect['id'] + parameters = effect['parameters'] + bands = parameters['bands'] + filter_type = parameters.get('filter_type', 'peaking') + q_factor = parameters.get('q_factor', 1.0) + + filters = {} + + # 为每个频段创建滤波器 + for frequency, gain in bands.items(): + # 限制频率范围 + frequency = max(20, min(20000, frequency)) + + # 限制增益范围 + gain = max(-24, min(24, gain)) + + # 创建滤波器系数 + filter_coeffs = self._create_filter_coefficients( + frequency, gain, q_factor, filter_type) + + filters[frequency] = { + 'frequency': frequency, + 'gain': gain, + 'q_factor': q_factor, + 'type': filter_type, + 'coeffs': filter_coeffs, + 'state': np.zeros(2) # 滤波器状态 + } + + effect['filters'] = filters + + def _create_filter_coefficients(self, frequency: float, gain: float, + q_factor: float, filter_type: str) -> Dict[str, float]: + """ + 创建滤波器系数 + + Args: + frequency: 频率 (Hz) + gain: 增益 (dB) + q_factor: Q因子 + filter_type: 滤波器类型 + + Returns: + 滤波器系数字典 + """ + # 将增益从dB转换为线性 + linear_gain = 10 ** (gain / 20.0) + + # 归一化频率 + nyquist = self.sample_rate / 2.0 + normalized_freq = frequency / nyquist + + # 根据滤波器类型创建系数 + if filter_type == 'peaking': + # 峰值滤波器 + coeffs = self._design_peaking_filter(normalized_freq, linear_gain, q_factor) + elif filter_type == 'low_shelf': + # 低频搁架滤波器 + coeffs = self._design_low_shelf_filter(normalized_freq, linear_gain, q_factor) + elif filter_type == 'high_shelf': + # 高频搁架滤波器 + coeffs = self._design_high_shelf_filter(normalized_freq, linear_gain, q_factor) + else: + # 默认使用峰值滤波器 + coeffs = self._design_peaking_filter(normalized_freq, linear_gain, q_factor) + + return coeffs + + def _design_peaking_filter(self, freq: float, gain: float, q: float) -> Dict[str, float]: + """ + 设计峰值滤波器 + + Args: + freq: 归一化频率 + gain: 线性增益 + q: Q因子 + + Returns: + 滤波器系数 + """ + # 这里使用简化的设计方法 + # 实际应用中会使用更精确的双线性变换设计 + + # 计算带宽 + bandwidth = freq / q + + # 简化的系数计算 + alpha = bandwidth / 2.0 + beta = 0.5 * ((1.0 - alpha) / (1.0 + alpha)) + gamma = (0.5 + beta) * math.cos(2 * math.pi * freq) + + if gain >= 1.0: + # 提升 + delta = (0.5 + beta) * gain - (0.5 - beta) / gain + epsilon = ((0.5 + beta) / gain - (0.5 - beta) * gain) * math.cos(2 * math.pi * freq) + else: + # 衰减 + delta = (0.5 + beta) / gain - (0.5 - beta) * gain + epsilon = ((0.5 + beta) * gain - (0.5 - beta) / gain) * math.cos(2 * math.pi * freq) + + # 滤波器系数 + a0 = (0.5 + beta) * gain + a1 = -gamma + a2 = (0.5 - beta) * gain + b0 = (0.5 + beta) + b1 = -delta + b2 = (0.5 - beta) + + return { + 'a0': a0, 'a1': a1, 'a2': a2, + 'b0': b0, 'b1': b1, 'b2': b2 + } + + def _design_low_shelf_filter(self, freq: float, gain: float, q: float) -> Dict[str, float]: + """ + 设计低频搁架滤波器 + + Args: + freq: 归一化频率 + gain: 线性增益 + q: Q因子 + + Returns: + 滤波器系数 + """ + # 简化的低频搁架滤波器设计 + return self._design_peaking_filter(freq, gain, q) + + def _design_high_shelf_filter(self, freq: float, gain: float, q: float) -> Dict[str, float]: + """ + 设计高频搁架滤波器 + + Args: + freq: 归一化频率 + gain: 线性增益 + q: Q因子 + + Returns: + 滤波器系数 + """ + # 简化的高频搁架滤波器设计 + return self._design_peaking_filter(freq, gain, q) + + def _initialize_buffers(self, effect: Dict[str, Any]): + """ + 初始化缓冲区 + + Args: + effect: 效果对象 + """ + effect_id = effect['id'] + buffers = {} + + # 输入和输出缓冲区 + buffers['input'] = np.zeros(self.buffer_size) + buffers['output'] = np.zeros(self.buffer_size) + + effect['buffers'] = buffers + + def delete_effect(self, effect_id: str) -> bool: + """ + 删除均衡器效果 + + Args: + effect_id: 效果ID + + Returns: + 是否删除成功 + """ + if effect_id not in self.effects: + print(f"✗ 均衡器效果不存在: {effect_id}") + return False + + try: + # 删除效果 + del self.effects[effect_id] + + # 更新统计信息 + self.stats['active_effects'] -= 1 + + # 从插件中移除效果 + if self.plugin and effect_id in self.plugin.effects: + del self.plugin.effects[effect_id] + + print(f"✓ 均衡器效果已删除: {effect_id}") + return True + + except Exception as e: + print(f"✗ 删除均衡器效果失败: {e}") + return False + + def process(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: + """ + 处理均衡器效果 + + Args: + audio_data: 输入音频数据 + parameters: 效果参数 + + Returns: + 处理后的音频数据 + """ + try: + import time + process_start = time.time() + + # 如果输入是NumPy数组 + if isinstance(audio_data, np.ndarray): + processed_data = self._process_numpy_array(audio_data, parameters) + else: + # 对于其他类型的数据,返回原始数据 + processed_data = audio_data + + # 更新统计信息 + self.stats['effects_processed'] += 1 + self.stats['processing_time'] += (time.time() - process_start) + + return processed_data + + except Exception as e: + print(f"✗ 均衡器效果处理失败: {e}") + return audio_data + + def _process_numpy_array(self, audio_data: np.ndarray, parameters: Dict[str, Any]) -> np.ndarray: + """ + 处理NumPy数组音频数据 + + Args: + audio_data: 输入音频数据 + parameters: 效果参数 + + Returns: + 处理后的音频数据 + """ + # 获取参数 + bands = parameters.get('bands', {}) + master_gain = parameters.get('master_gain', 0.0) + solo_band = parameters.get('solo_band', None) + bypass_bands = parameters.get('bypass_bands', []) + + # 应用主增益 + if master_gain != 0.0: + linear_master_gain = 10 ** (master_gain / 20.0) + audio_data = audio_data * linear_master_gain + + # 应用各频段增益 + if bands: + # 如果有独奏频段,只处理该频段 + if solo_band is not None and solo_band in bands: + frequency = solo_band + gain = bands[frequency] + if frequency not in bypass_bands: + audio_data = self._apply_band_filter(audio_data, frequency, gain) + else: + # 处理所有频段 + for frequency, gain in bands.items(): + if frequency not in bypass_bands: + audio_data = self._apply_band_filter(audio_data, frequency, gain) + + # 确保输出在有效范围内 + processed_data = np.clip(audio_data, -1.0, 1.0) + return processed_data + + def _apply_band_filter(self, audio_data: np.ndarray, frequency: float, gain: float) -> np.ndarray: + """ + 应用单个频段滤波器 + + Args: + audio_data: 输入音频数据 + frequency: 频率 (Hz) + gain: 增益 (dB) + + Returns: + 处理后的音频数据 + """ + # 这里实现简化的频段处理 + # 实际应用中会使用更复杂的滤波器设计 + + if gain == 0.0: + return audio_data + + # 将增益从dB转换为线性 + linear_gain = 10 ** (gain / 20.0) + + # 简化的频率响应应用 + # 根据频率调整处理方式 + if frequency < 200: + # 低频 - 更平滑的处理 + processed = audio_data * (0.5 + 0.5 * linear_gain) + elif frequency < 2000: + # 中频 - 标准处理 + processed = audio_data * linear_gain + else: + # 高频 - 更锐利的处理 + processed = audio_data * (1.0 + 0.2 * (linear_gain - 1.0)) + + return processed + + def set_parameter(self, effect_id: str, parameter: str, value: Any) -> bool: + """ + 设置效果参数 + + Args: + effect_id: 效果ID + parameter: 参数名 + value: 参数值 + + Returns: + 是否设置成功 + """ + if effect_id not in self.effects: + print(f"✗ 均衡器效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['parameters'][parameter] = value + + # 如果是滤波器参数,重新初始化滤波器 + if parameter == 'bands': + self._initialize_filters(self.effects[effect_id]) + + # 如果是插件中的效果,也更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['parameters'][parameter] = value + if parameter == 'bands': + self.plugin.effects[effect_id]['filters'] = self.effects[effect_id]['filters'].copy() + + print(f"✓ 均衡器效果参数已设置: {effect_id}.{parameter} = {value}") + return True + + except Exception as e: + print(f"✗ 设置均衡器效果参数失败: {e}") + return False + + def get_parameters(self, effect_id: str) -> Dict[str, Any]: + """ + 获取效果参数 + + Args: + effect_id: 效果ID + + Returns: + 参数字典 + """ + if effect_id not in self.effects: + return {} + + return self.effects[effect_id]['parameters'].copy() + + def enable_effect(self, effect_id: str) -> bool: + """ + 启用均衡器效果 + + Args: + effect_id: 效果ID + + Returns: + 是否启用成功 + """ + if effect_id not in self.effects: + print(f"✗ 均衡器效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['active'] = True + self.stats['active_effects'] += 1 + + # 如果是插件中的效果,也更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['active'] = True + + print(f"✓ 均衡器效果已启用: {effect_id}") + return True + + except Exception as e: + print(f"✗ 启用均衡器效果失败: {e}") + return False + + def disable_effect(self, effect_id: str) -> bool: + """ + 禁用均衡器效果 + + Args: + effect_id: 效果ID + + Returns: + 是否禁用成功 + """ + if effect_id not in self.effects: + print(f"✗ 均衡器效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['active'] = False + self.stats['active_effects'] -= 1 + + # 如果是插件中的效果,也更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['active'] = False + + print(f"✓ 均衡器效果已禁用: {effect_id}") + return True + + except Exception as e: + print(f"✗ 禁用均衡器效果失败: {e}") + return False + + def cleanup(self): + """清理所有资源""" + self.effects.clear() + self.filters.clear() + self.stats = { + 'total_effects': 0, + 'active_effects': 0, + 'effects_processed': 0, + 'processing_time': 0.0, + 'memory_usage': 0 + } + print("✓ 均衡器效果处理器资源已清理") + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def _get_current_time(self) -> float: + """ + 获取当前时间(秒) + + Returns: + 当前时间 + """ + if self.plugin and self.plugin.plugin_manager and self.plugin.plugin_manager.world: + return self.plugin.plugin_manager.world.globalClock.getFrameTime() + import time + return time.time() + + def update(self, dt: float): + """ + 更新处理器状态 + + Args: + dt: 时间增量 + """ + # 这里可以更新任何需要定期更新的状态 + pass + + def set_sample_rate(self, sample_rate: int): + """ + 设置采样率 + + Args: + sample_rate: 采样率 (Hz) + """ + old_sample_rate = self.sample_rate + self.sample_rate = sample_rate + print(f"✓ 均衡器处理器采样率已从 {old_sample_rate} 更新为: {sample_rate} Hz") + + # 重新初始化所有滤波器以适应新的采样率 + for effect_id, effect in self.effects.items(): + self._initialize_filters(effect) + + def set_band_gain(self, effect_id: str, frequency: int, gain: float) -> bool: + """ + 设置频段增益 + + Args: + effect_id: 效果ID + frequency: 频率 (Hz) + gain: 增益 (dB) + + Returns: + 是否设置成功 + """ + if effect_id not in self.effects: + print(f"✗ 均衡器效果不存在: {effect_id}") + return False + + try: + # 限制增益范围 + gain = max(-24, min(24, gain)) + + # 更新频段增益 + parameters = self.effects[effect_id]['parameters'] + parameters['bands'][frequency] = gain + + # 更新滤波器 + self._initialize_filters(self.effects[effect_id]) + + # 更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['parameters']['bands'][frequency] = gain + self.plugin.effects[effect_id]['filters'] = self.effects[effect_id]['filters'].copy() + + print(f"✓ 频段增益已设置: {effect_id} -> {frequency}Hz: {gain}dB") + return True + + except Exception as e: + print(f"✗ 设置频段增益失败: {e}") + return False + + def set_master_gain(self, effect_id: str, gain: float) -> bool: + """ + 设置主增益 + + Args: + effect_id: 效果ID + gain: 主增益 (dB) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'master_gain', max(-24, min(24, gain))) + + def set_solo_band(self, effect_id: str, frequency: Optional[int]) -> bool: + """ + 设置独奏频段 + + Args: + effect_id: 效果ID + frequency: 频率 (Hz),None表示取消独奏 + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'solo_band', frequency) + + def bypass_band(self, effect_id: str, frequency: int, bypass: bool = True) -> bool: + """ + 绕过频段 + + Args: + effect_id: 效果ID + frequency: 频率 (Hz) + bypass: 是否绕过 + + Returns: + 是否设置成功 + """ + if effect_id not in self.effects: + print(f"✗ 均衡器效果不存在: {effect_id}") + return False + + try: + parameters = self.effects[effect_id]['parameters'] + bypass_bands = parameters.get('bypass_bands', []) + + if bypass and frequency not in bypass_bands: + bypass_bands.append(frequency) + elif not bypass and frequency in bypass_bands: + bypass_bands.remove(frequency) + + parameters['bypass_bands'] = bypass_bands + + # 更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['parameters']['bypass_bands'] = bypass_bands.copy() + + print(f"✓ 频段绕过已设置: {effect_id} -> {frequency}Hz: {'是' if bypass else '否'}") + return True + + except Exception as e: + print(f"✗ 设置频段绕过失败: {e}") + return False + + def add_band(self, effect_id: str, frequency: int, gain: float = 0.0) -> bool: + """ + 添加频段 + + Args: + effect_id: 效果ID + frequency: 频率 (Hz) + gain: 增益 (dB) + + Returns: + 是否添加成功 + """ + if effect_id not in self.effects: + print(f"✗ 均衡器效果不存在: {effect_id}") + return False + + try: + # 检查频段数是否超过限制 + parameters = self.effects[effect_id]['parameters'] + bands = parameters['bands'] + + if len(bands) >= self.max_bands: + print(f"✗ 频段数已达到最大限制: {self.max_bands}") + return False + + # 添加频段 + bands[frequency] = gain + + # 重新初始化滤波器 + self._initialize_filters(self.effects[effect_id]) + + # 更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['parameters']['bands'] = bands.copy() + self.plugin.effects[effect_id]['filters'] = self.effects[effect_id]['filters'].copy() + + print(f"✓ 频段已添加: {effect_id} -> {frequency}Hz: {gain}dB") + return True + + except Exception as e: + print(f"✗ 添加频段失败: {e}") + return False + + def remove_band(self, effect_id: str, frequency: int) -> bool: + """ + 移除频段 + + Args: + effect_id: 效果ID + frequency: 频率 (Hz) + + Returns: + 是否移除成功 + """ + if effect_id not in self.effects: + print(f"✗ 均衡器效果不存在: {effect_id}") + return False + + try: + parameters = self.effects[effect_id]['parameters'] + bands = parameters['bands'] + + if frequency in bands: + del bands[frequency] + + # 重新初始化滤波器 + self._initialize_filters(self.effects[effect_id]) + + # 更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['parameters']['bands'] = bands.copy() + self.plugin.effects[effect_id]['filters'] = self.effects[effect_id]['filters'].copy() + + print(f"✓ 频段已移除: {effect_id} -> {frequency}Hz") + return True + else: + print(f"⚠ 频段不存在: {effect_id} -> {frequency}Hz") + return False + + except Exception as e: + print(f"✗ 移除频段失败: {e}") + return False + + def get_frequency_response(self, effect_id: str, frequencies: List[float] = None) -> Dict[str, Any]: + """ + 获取频率响应 + + Args: + effect_id: 效果ID + frequencies: 频率点列表,如果为None则使用默认频率点 + + Returns: + 频率响应数据 + """ + if effect_id not in self.effects: + return {} + + if frequencies is None: + # 默认频率点(对数分布) + frequencies = np.logspace(np.log10(20), np.log10(20000), 1000) + + parameters = self.effects[effect_id]['parameters'] + bands = parameters['bands'] + + # 计算每个频率点的响应 + response = [] + for freq in frequencies: + gain = 0.0 + # 计算所有频段在该频率点的贡献 + for band_freq, band_gain in bands.items(): + # 简化的频率响应计算 + distance = abs(freq - band_freq) / band_freq + if distance < 1.0: # 在影响范围内 + influence = 1.0 - distance + gain += band_gain * influence + + response.append(gain) + + return { + 'frequencies': frequencies.tolist(), + 'response': response, + 'master_gain': parameters.get('master_gain', 0.0) + } + + def create_preset(self, preset_name: str) -> Dict[str, Any]: + """ + 创建均衡器预设 + + Args: + preset_name: 预设名称 + + Returns: + 预设参数字典 + """ + presets = { + 'flat': { + 'bands': { + 60: 0.0, 200: 0.0, 500: 0.0, 1000: 0.0, 3000: 0.0, 8000: 0.0, 12000: 0.0 + } + }, + 'pop': { + 'bands': { + 60: -1.0, 200: 2.0, 500: 3.0, 1000: 1.0, 3000: 2.0, 8000: 3.0, 12000: 2.0 + }, + 'master_gain': 1.0 + }, + 'rock': { + 'bands': { + 60: 3.0, 200: 2.0, 500: -1.0, 1000: -2.0, 3000: 1.0, 8000: 3.0, 12000: 2.0 + }, + 'master_gain': 2.0 + }, + 'jazz': { + 'bands': { + 60: 2.0, 200: 1.0, 500: 0.0, 1000: -1.0, 3000: 1.0, 8000: 2.0, 12000: 1.0 + }, + 'master_gain': 1.0 + }, + 'classical': { + 'bands': { + 60: -2.0, 200: -1.0, 500: 0.0, 1000: 1.0, 3000: 2.0, 8000: 1.0, 12000: 0.0 + }, + 'master_gain': 0.0 + }, + 'vocal_booster': { + 'bands': { + 60: -2.0, 200: -1.0, 500: 1.0, 1000: 3.0, 3000: 2.0, 8000: 1.0, 12000: 0.0 + }, + 'master_gain': 1.0 + }, + 'bass_boost': { + 'bands': { + 60: 5.0, 200: 3.0, 500: 1.0, 1000: 0.0, 3000: -1.0, 8000: -2.0, 12000: -3.0 + }, + 'master_gain': 2.0 + }, + 'treble_boost': { + 'bands': { + 60: -3.0, 200: -2.0, 500: -1.0, 1000: 0.0, 3000: 1.0, 8000: 3.0, 12000: 5.0 + }, + 'master_gain': 1.0 + } + } + + return presets.get(preset_name, {}).copy() + diff --git a/plugins/user/audio_reverb_effects/effects/modulation_processor.py b/plugins/user/audio_reverb_effects/effects/modulation_processor.py index 691e46f2..2aaa3d5d 100644 --- a/plugins/user/audio_reverb_effects/effects/modulation_processor.py +++ b/plugins/user/audio_reverb_effects/effects/modulation_processor.py @@ -1,827 +1,827 @@ -""" -调制效果处理器 -实现专业的音频调制效果处理(合唱、镶边、移相等) -""" - -import uuid -import math -import numpy as np -from typing import Dict, List, Any, Optional - -class ModulationProcessor: - """ - 调制效果处理器 - 实现专业的音频调制效果处理(合唱、镶边、移相等) - """ - - def __init__(self, plugin): - """ - 初始化调制效果处理器 - - Args: - plugin: 音频效果插件实例 - """ - self.plugin = plugin - self.effects: Dict[str, Dict[str, Any]] = {} - self.stats = { - 'total_effects': 0, - 'active_effects': 0, - 'effects_processed': 0, - 'processing_time': 0.0, - 'memory_usage': 0 - } - self.buffer_size = plugin.buffer_size if plugin else 4096 - self.sample_rate = 44100 # 默认采样率 - - def create_effect(self, parameters: Dict[str, Any]) -> str: - """ - 创建调制效果 - - Args: - parameters: 调制效果参数 - - Returns: - 效果ID - """ - try: - # 生成唯一ID - effect_id = str(uuid.uuid4()) - - # 默认参数 - default_params = { - 'type': 'chorus', # 效果类型: chorus, flanger, phaser - 'rate': 1.0, # 速率 (Hz) - 'depth': 0.5, # 深度 (0.0-1.0) - 'feedback': 0.3, # 反馈 (-1.0-1.0) - 'mix': 0.5, # 湿信号混合 (0.0-1.0) - 'delay': 0.002, # 延迟时间 (秒) - 'center_delay': 0.002, # 中心延迟 (秒) - 'spread': 0.5, # 扩散 (0.0-1.0) - 'stereo': True, # 立体声 - 'lfo_waveform': 'sine', # LFO波形: sine, triangle, square, sawtooth - 'lfo_phase': 0.0, # LFO相位 (度) - 'high_cut': 8000.0, # 高频截止 (Hz) - 'low_cut': 100.0 # 低频截止 (Hz) - } - - # 合并参数 - effect_params = default_params.copy() - effect_params.update(parameters) - - # 创建效果对象 - effect = { - 'id': effect_id, - 'type': 'modulation', - 'parameters': effect_params, - 'active': True, - 'buffers': {}, # 缓冲区 - 'lfo_phase': 0.0, # LFO相位状态 - 'delay_buffers': {}, # 延迟缓冲区 - 'filter_states': {}, # 滤波器状态 - 'created_time': self._get_current_time(), - 'process_count': 0 - } - - # 初始化缓冲区 - self._initialize_buffers(effect) - - # 存储效果 - self.effects[effect_id] = effect - - # 更新统计信息 - self.stats['total_effects'] += 1 - self.stats['active_effects'] += 1 - - # 在插件中注册效果 - if self.plugin: - self.plugin.effects[effect_id] = effect.copy() - - print(f"✓ 调制效果创建成功: {effect_id}") - return effect_id - - except Exception as e: - print(f"✗ 创建调制效果失败: {e}") - import traceback - traceback.print_exc() - return "" - - def _initialize_buffers(self, effect: Dict[str, Any]): - """ - 初始化缓冲区 - - Args: - effect: 效果对象 - """ - effect_id = effect['id'] - parameters = effect['parameters'] - buffers = {} - - # 输入和输出缓冲区 - buffers['input'] = np.zeros(self.buffer_size) - buffers['output'] = np.zeros(self.buffer_size) - - # 延迟缓冲区 - max_delay_samples = int(0.1 * self.sample_rate) # 最大100ms延迟 - buffers['delay_line'] = np.zeros(max_delay_samples) - buffers['delay_index'] = 0 - - # 立体声延迟缓冲区 - if parameters.get('stereo', True): - buffers['delay_line_right'] = np.zeros(max_delay_samples) - buffers['delay_index_right'] = 0 - - # 滤波器状态 - buffers['high_pass_state'] = np.zeros(2) # 双通道高通滤波器状态 - buffers['low_pass_state'] = np.zeros(2) # 双通道低通滤波器状态 - - effect['buffers'] = buffers - - def delete_effect(self, effect_id: str) -> bool: - """ - 删除调制效果 - - Args: - effect_id: 效果ID - - Returns: - 是否删除成功 - """ - if effect_id not in self.effects: - print(f"✗ 调制效果不存在: {effect_id}") - return False - - try: - # 删除效果 - del self.effects[effect_id] - - # 更新统计信息 - self.stats['active_effects'] -= 1 - - # 从插件中移除效果 - if self.plugin and effect_id in self.plugin.effects: - del self.plugin.effects[effect_id] - - print(f"✓ 调制效果已删除: {effect_id}") - return True - - except Exception as e: - print(f"✗ 删除调制效果失败: {e}") - return False - - def process(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: - """ - 处理调制效果 - - Args: - audio_data: 输入音频数据 - parameters: 效果参数 - - Returns: - 处理后的音频数据 - """ - try: - import time - process_start = time.time() - - # 如果输入是NumPy数组 - if isinstance(audio_data, np.ndarray): - processed_data = self._process_numpy_array(audio_data, parameters) - else: - # 对于其他类型的数据,返回原始数据 - processed_data = audio_data - - # 更新统计信息 - self.stats['effects_processed'] += 1 - self.stats['processing_time'] += (time.time() - process_start) - - return processed_data - - except Exception as e: - print(f"✗ 调制效果处理失败: {e}") - return audio_data - - def _process_numpy_array(self, audio_data: np.ndarray, parameters: Dict[str, Any]) -> np.ndarray: - """ - 处理NumPy数组音频数据 - - Args: - audio_data: 输入音频数据 - parameters: 效果参数 - - Returns: - 处理后的音频数据 - """ - # 获取参数 - effect_type = parameters.get('type', 'chorus') - rate = max(0.01, min(10.0, parameters.get('rate', 1.0))) - depth = max(0.0, min(1.0, parameters.get('depth', 0.5))) - feedback = max(-1.0, min(1.0, parameters.get('feedback', 0.3))) - mix = max(0.0, min(1.0, parameters.get('mix', 0.5))) - delay = max(0.0001, min(0.05, parameters.get('delay', 0.002))) - center_delay = max(0.0001, min(0.05, parameters.get('center_delay', 0.002))) - spread = max(0.0, min(1.0, parameters.get('spread', 0.5))) - stereo = parameters.get('stereo', True) - lfo_waveform = parameters.get('lfo_waveform', 'sine') - high_cut = max(100.0, min(20000.0, parameters.get('high_cut', 8000.0))) - low_cut = max(20.0, min(10000.0, parameters.get('low_cut', 100.0))) - - # 创建输出数组 - if audio_data.ndim == 1: - # 单声道处理 - output_data = np.zeros_like(audio_data) - - # 初始化LFO相位 - lfo_phase = 0.0 - lfo_increment = 2.0 * math.pi * rate / self.sample_rate - - for i in range(len(audio_data)): - sample = audio_data[i] - - # 生成LFO值 - lfo_value = self._generate_lfo_value(lfo_waveform, lfo_phase) - - # 计算调制延迟 - if effect_type == 'chorus': - mod_delay = center_delay + depth * 0.005 * lfo_value - elif effect_type == 'flanger': - mod_delay = delay + depth * 0.005 * lfo_value - else: # phaser - mod_delay = delay - - # 限制延迟范围 - mod_delay = max(0.0001, min(0.05, mod_delay)) - - # 读取延迟样本(简化插值) - delayed_sample = self._read_delayed_sample(sample, mod_delay, feedback) - - # 应用滤波器(如果是镶边或移相器) - if effect_type in ['flanger', 'phaser']: - delayed_sample = self._apply_filter(delayed_sample, high_cut, low_cut) - - # 混合干湿信号 - output_data[i] = sample * (1.0 - mix) + delayed_sample * mix - - # 更新LFO相位 - lfo_phase += lfo_increment - if lfo_phase >= 2.0 * math.pi: - lfo_phase -= 2.0 * math.pi - - else: - # 多声道处理(立体声) - output_data = np.zeros_like(audio_data) - - # 初始化LFO相位 - lfo_phase_left = 0.0 - lfo_phase_right = parameters.get('lfo_phase', 0.0) * math.pi / 180.0 # 转换为弧度 - lfo_increment = 2.0 * math.pi * rate / self.sample_rate - - for channel in range(min(2, audio_data.shape[0])): # 最多处理两个声道 - lfo_phase = lfo_phase_left if channel == 0 else lfo_phase_right - - for i in range(audio_data.shape[1]): - sample = audio_data[channel, i] - - # 生成LFO值 - lfo_value = self._generate_lfo_value(lfo_waveform, lfo_phase) - - # 计算调制延迟 - if effect_type == 'chorus': - if channel == 0: # 左声道 - mod_delay = center_delay + depth * 0.005 * lfo_value * (1.0 - spread) - else: # 右声道 - mod_delay = center_delay + depth * 0.005 * lfo_value * (1.0 + spread) - elif effect_type == 'flanger': - mod_delay = delay + depth * 0.005 * lfo_value - else: # phaser - mod_delay = delay - - # 限制延迟范围 - mod_delay = max(0.0001, min(0.05, mod_delay)) - - # 读取延迟样本(简化插值) - delayed_sample = self._read_delayed_sample(sample, mod_delay, feedback) - - # 应用滤波器(如果是镶边或移相器) - if effect_type in ['flanger', 'phaser']: - delayed_sample = self._apply_filter(delayed_sample, high_cut, low_cut) - - # 混合干湿信号 - output_data[channel, i] = sample * (1.0 - mix) + delayed_sample * mix - - # 更新LFO相位 - lfo_phase += lfo_increment - if lfo_phase >= 2.0 * math.pi: - lfo_phase -= 2.0 * math.pi - - # 确保输出在有效范围内 - processed_data = np.clip(output_data, -1.0, 1.0) - return processed_data - - def _generate_lfo_value(self, waveform: str, phase: float) -> float: - """ - 生成LFO值 - - Args: - waveform: 波形类型 - phase: 相位(弧度) - - Returns: - LFO值 (-1.0 到 1.0) - """ - if waveform == 'sine': - return math.sin(phase) - elif waveform == 'triangle': - # 三角波 - normalized_phase = phase / (2.0 * math.pi) - if normalized_phase < 0.25: - return 4.0 * normalized_phase - elif normalized_phase < 0.75: - return 2.0 - 4.0 * normalized_phase - else: - return 4.0 * normalized_phase - 4.0 - elif waveform == 'square': - # 方波 - return 1.0 if math.sin(phase) >= 0.0 else -1.0 - elif waveform == 'sawtooth': - # 锯齿波 - normalized_phase = phase / (2.0 * math.pi) - return 2.0 * normalized_phase - 1.0 - else: - # 默认使用正弦波 - return math.sin(phase) - - def _read_delayed_sample(self, input_sample: float, delay_time: float, feedback: float) -> float: - """ - 读取延迟样本 - - Args: - input_sample: 输入样本 - delay_time: 延迟时间(秒) - feedback: 反馈系数 - - Returns: - 延迟样本 - """ - # 这里使用简化的延迟处理 - # 实际应用中会使用更复杂的延迟线和插值 - - # 计算延迟样本数 - delay_samples = delay_time * self.sample_rate - - # 简化的延迟效果(使用单一样本延迟) - delayed_sample = input_sample * 0.5 # 简化的延迟 - - # 应用反馈 - if abs(feedback) > 0.01: - delayed_sample += delayed_sample * feedback * 0.3 - - return delayed_sample - - def _apply_filter(self, sample: float, high_cut: float, low_cut: float) -> float: - """ - 应用滤波器 - - Args: - sample: 输入样本 - high_cut: 高频截止频率 - low_cut: 低频截止频率 - - Returns: - 滤波后的样本 - """ - # 这里使用简化的滤波器 - # 实际应用中会使用更复杂的滤波器设计 - - # 简单的一阶滤波器 - filtered_sample = sample - - # 高频截止(低通滤波器效果) - if high_cut < 10000.0: - filter_factor = high_cut / 10000.0 - filtered_sample *= (0.5 + 0.5 * filter_factor) - - # 低频截止(高通滤波器效果) - if low_cut > 100.0: - filter_factor = low_cut / 10000.0 - filtered_sample *= (0.5 + 0.5 * filter_factor) - - return filtered_sample - - def set_parameter(self, effect_id: str, parameter: str, value: Any) -> bool: - """ - 设置效果参数 - - Args: - effect_id: 效果ID - parameter: 参数名 - value: 参数值 - - Returns: - 是否设置成功 - """ - if effect_id not in self.effects: - print(f"✗ 调制效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['parameters'][parameter] = value - - # 如果是插件中的效果,也更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['parameters'][parameter] = value - - print(f"✓ 调制效果参数已设置: {effect_id}.{parameter} = {value}") - return True - - except Exception as e: - print(f"✗ 设置调制效果参数失败: {e}") - return False - - def get_parameters(self, effect_id: str) -> Dict[str, Any]: - """ - 获取效果参数 - - Args: - effect_id: 效果ID - - Returns: - 参数字典 - """ - if effect_id not in self.effects: - return {} - - return self.effects[effect_id]['parameters'].copy() - - def enable_effect(self, effect_id: str) -> bool: - """ - 启用调制效果 - - Args: - effect_id: 效果ID - - Returns: - 是否启用成功 - """ - if effect_id not in self.effects: - print(f"✗ 调制效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['active'] = True - self.stats['active_effects'] += 1 - - # 如果是插件中的效果,也更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['active'] = True - - print(f"✓ 调制效果已启用: {effect_id}") - return True - - except Exception as e: - print(f"✗ 启用调制效果失败: {e}") - return False - - def disable_effect(self, effect_id: str) -> bool: - """ - 禁用调制效果 - - Args: - effect_id: 效果ID - - Returns: - 是否禁用成功 - """ - if effect_id not in self.effects: - print(f"✗ 调制效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['active'] = False - self.stats['active_effects'] -= 1 - - # 如果是插件中的效果,也更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['active'] = False - - print(f"✓ 调制效果已禁用: {effect_id}") - return True - - except Exception as e: - print(f"✗ 禁用调制效果失败: {e}") - return False - - def cleanup(self): - """清理所有资源""" - self.effects.clear() - self.stats = { - 'total_effects': 0, - 'active_effects': 0, - 'effects_processed': 0, - 'processing_time': 0.0, - 'memory_usage': 0 - } - print("✓ 调制效果处理器资源已清理") - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def _get_current_time(self) -> float: - """ - 获取当前时间(秒) - - Returns: - 当前时间 - """ - if self.plugin and self.plugin.plugin_manager and self.plugin.plugin_manager.world: - return self.plugin.plugin_manager.world.globalClock.getFrameTime() - import time - return time.time() - - def update(self, dt: float): - """ - 更新处理器状态 - - Args: - dt: 时间增量 - """ - # 这里可以更新任何需要定期更新的状态 - pass - - def set_sample_rate(self, sample_rate: int): - """ - 设置采样率 - - Args: - sample_rate: 采样率 (Hz) - """ - self.sample_rate = sample_rate - print(f"✓ 调制效果处理器采样率已设置: {sample_rate} Hz") - - def set_rate(self, effect_id: str, rate: float) -> bool: - """ - 设置LFO速率 - - Args: - effect_id: 效果ID - rate: 速率 (Hz) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'rate', max(0.01, min(10.0, rate))) - - def set_depth(self, effect_id: str, depth: float) -> bool: - """ - 设置深度 - - Args: - effect_id: 效果ID - depth: 深度 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'depth', max(0.0, min(1.0, depth))) - - def set_feedback(self, effect_id: str, feedback: float) -> bool: - """ - 设置反馈 - - Args: - effect_id: 效果ID - feedback: 反馈 (-1.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'feedback', max(-1.0, min(1.0, feedback))) - - def set_mix(self, effect_id: str, mix: float) -> bool: - """ - 设置湿信号混合 - - Args: - effect_id: 效果ID - mix: 湿信号混合 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'mix', max(0.0, min(1.0, mix))) - - def set_delay(self, effect_id: str, delay: float) -> bool: - """ - 设置延迟时间 - - Args: - effect_id: 效果ID - delay: 延迟时间 (秒) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'delay', max(0.0001, min(0.05, delay))) - - def set_center_delay(self, effect_id: str, delay: float) -> bool: - """ - 设置中心延迟时间 - - Args: - effect_id: 效果ID - delay: 中心延迟时间 (秒) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'center_delay', max(0.0001, min(0.05, delay))) - - def set_spread(self, effect_id: str, spread: float) -> bool: - """ - 设置扩散 - - Args: - effect_id: 效果ID - spread: 扩散 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'spread', max(0.0, min(1.0, spread))) - - def set_lfo_waveform(self, effect_id: str, waveform: str) -> bool: - """ - 设置LFO波形 - - Args: - effect_id: 效果ID - waveform: 波形类型 (sine, triangle, square, sawtooth) - - Returns: - 是否设置成功 - """ - valid_waveforms = ['sine', 'triangle', 'square', 'sawtooth'] - if waveform not in valid_waveforms: - print(f"✗ 无效的LFO波形: {waveform}") - return False - - return self.set_parameter(effect_id, 'lfo_waveform', waveform) - - def enable_stereo(self, effect_id: str, enable: bool) -> bool: - """ - 启用/禁用立体声 - - Args: - effect_id: 效果ID - enable: 是否启用 - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'stereo', enable) - - def create_preset(self, preset_name: str) -> Dict[str, Any]: - """ - 创建调制效果预设 - - Args: - preset_name: 预设名称 - - Returns: - 预设参数字典 - """ - presets = { - 'chorus_light': { - 'type': 'chorus', - 'rate': 0.8, - 'depth': 0.3, - 'feedback': 0.2, - 'mix': 0.4, - 'center_delay': 0.004, - 'spread': 0.3, - 'lfo_waveform': 'sine' - }, - 'chorus_medium': { - 'type': 'chorus', - 'rate': 1.2, - 'depth': 0.5, - 'feedback': 0.3, - 'mix': 0.6, - 'center_delay': 0.006, - 'spread': 0.5, - 'lfo_waveform': 'sine' - }, - 'chorus_heavy': { - 'type': 'chorus', - 'rate': 1.8, - 'depth': 0.7, - 'feedback': 0.4, - 'mix': 0.8, - 'center_delay': 0.008, - 'spread': 0.7, - 'lfo_waveform': 'triangle' - }, - 'flanger_light': { - 'type': 'flanger', - 'rate': 0.5, - 'depth': 0.4, - 'feedback': 0.5, - 'mix': 0.5, - 'delay': 0.001, - 'lfo_waveform': 'sine' - }, - 'flanger_medium': { - 'type': 'flanger', - 'rate': 0.8, - 'depth': 0.6, - 'feedback': 0.7, - 'mix': 0.7, - 'delay': 0.002, - 'lfo_waveform': 'sine' - }, - 'flanger_heavy': { - 'type': 'flanger', - 'rate': 1.2, - 'depth': 0.8, - 'feedback': 0.9, - 'mix': 0.9, - 'delay': 0.003, - 'lfo_waveform': 'triangle' - }, - 'phaser_light': { - 'type': 'phaser', - 'rate': 0.6, - 'depth': 0.3, - 'feedback': 0.2, - 'mix': 0.4, - 'delay': 0.0005, - 'lfo_waveform': 'sine', - 'high_cut': 6000.0, - 'low_cut': 200.0 - }, - 'phaser_medium': { - 'type': 'phaser', - 'rate': 1.0, - 'depth': 0.5, - 'feedback': 0.4, - 'mix': 0.6, - 'delay': 0.001, - 'lfo_waveform': 'sine', - 'high_cut': 8000.0, - 'low_cut': 100.0 - }, - 'phaser_heavy': { - 'type': 'phaser', - 'rate': 1.5, - 'depth': 0.7, - 'feedback': 0.6, - 'mix': 0.8, - 'delay': 0.002, - 'lfo_waveform': 'triangle', - 'high_cut': 10000.0, - 'low_cut': 50.0 - } - } - - return presets.get(preset_name, {}).copy() - - def get_lfo_meter(self, effect_id: str) -> float: - """ - 获取LFO表 - - Args: - effect_id: 效果ID - - Returns: - 当前LFO值 (-1.0 到 1.0) - """ - if effect_id not in self.effects: - return 0.0 - - effect = self.effects[effect_id] - parameters = effect['parameters'] - lfo_waveform = parameters.get('lfo_waveform', 'sine') - lfo_phase = effect.get('lfo_phase', 0.0) - - return self._generate_lfo_value(lfo_waveform, lfo_phase) - - def set_lfo_phase(self, effect_id: str, phase: float) -> bool: - """ - 设置LFO相位 - - Args: - effect_id: 效果ID - phase: 相位 (度) - - Returns: - 是否设置成功 - """ - # 转换为弧度并规范化 - phase_rad = (phase * math.pi / 180.0) % (2.0 * math.pi) - return self.set_parameter(effect_id, 'lfo_phase', phase) - +""" +调制效果处理器 +实现专业的音频调制效果处理(合唱、镶边、移相等) +""" + +import uuid +import math +import numpy as np +from typing import Dict, List, Any, Optional + +class ModulationProcessor: + """ + 调制效果处理器 + 实现专业的音频调制效果处理(合唱、镶边、移相等) + """ + + def __init__(self, plugin): + """ + 初始化调制效果处理器 + + Args: + plugin: 音频效果插件实例 + """ + self.plugin = plugin + self.effects: Dict[str, Dict[str, Any]] = {} + self.stats = { + 'total_effects': 0, + 'active_effects': 0, + 'effects_processed': 0, + 'processing_time': 0.0, + 'memory_usage': 0 + } + self.buffer_size = plugin.buffer_size if plugin else 4096 + self.sample_rate = 44100 # 默认采样率 + + def create_effect(self, parameters: Dict[str, Any]) -> str: + """ + 创建调制效果 + + Args: + parameters: 调制效果参数 + + Returns: + 效果ID + """ + try: + # 生成唯一ID + effect_id = str(uuid.uuid4()) + + # 默认参数 + default_params = { + 'type': 'chorus', # 效果类型: chorus, flanger, phaser + 'rate': 1.0, # 速率 (Hz) + 'depth': 0.5, # 深度 (0.0-1.0) + 'feedback': 0.3, # 反馈 (-1.0-1.0) + 'mix': 0.5, # 湿信号混合 (0.0-1.0) + 'delay': 0.002, # 延迟时间 (秒) + 'center_delay': 0.002, # 中心延迟 (秒) + 'spread': 0.5, # 扩散 (0.0-1.0) + 'stereo': True, # 立体声 + 'lfo_waveform': 'sine', # LFO波形: sine, triangle, square, sawtooth + 'lfo_phase': 0.0, # LFO相位 (度) + 'high_cut': 8000.0, # 高频截止 (Hz) + 'low_cut': 100.0 # 低频截止 (Hz) + } + + # 合并参数 + effect_params = default_params.copy() + effect_params.update(parameters) + + # 创建效果对象 + effect = { + 'id': effect_id, + 'type': 'modulation', + 'parameters': effect_params, + 'active': True, + 'buffers': {}, # 缓冲区 + 'lfo_phase': 0.0, # LFO相位状态 + 'delay_buffers': {}, # 延迟缓冲区 + 'filter_states': {}, # 滤波器状态 + 'created_time': self._get_current_time(), + 'process_count': 0 + } + + # 初始化缓冲区 + self._initialize_buffers(effect) + + # 存储效果 + self.effects[effect_id] = effect + + # 更新统计信息 + self.stats['total_effects'] += 1 + self.stats['active_effects'] += 1 + + # 在插件中注册效果 + if self.plugin: + self.plugin.effects[effect_id] = effect.copy() + + print(f"✓ 调制效果创建成功: {effect_id}") + return effect_id + + except Exception as e: + print(f"✗ 创建调制效果失败: {e}") + import traceback + traceback.print_exc() + return "" + + def _initialize_buffers(self, effect: Dict[str, Any]): + """ + 初始化缓冲区 + + Args: + effect: 效果对象 + """ + effect_id = effect['id'] + parameters = effect['parameters'] + buffers = {} + + # 输入和输出缓冲区 + buffers['input'] = np.zeros(self.buffer_size) + buffers['output'] = np.zeros(self.buffer_size) + + # 延迟缓冲区 + max_delay_samples = int(0.1 * self.sample_rate) # 最大100ms延迟 + buffers['delay_line'] = np.zeros(max_delay_samples) + buffers['delay_index'] = 0 + + # 立体声延迟缓冲区 + if parameters.get('stereo', True): + buffers['delay_line_right'] = np.zeros(max_delay_samples) + buffers['delay_index_right'] = 0 + + # 滤波器状态 + buffers['high_pass_state'] = np.zeros(2) # 双通道高通滤波器状态 + buffers['low_pass_state'] = np.zeros(2) # 双通道低通滤波器状态 + + effect['buffers'] = buffers + + def delete_effect(self, effect_id: str) -> bool: + """ + 删除调制效果 + + Args: + effect_id: 效果ID + + Returns: + 是否删除成功 + """ + if effect_id not in self.effects: + print(f"✗ 调制效果不存在: {effect_id}") + return False + + try: + # 删除效果 + del self.effects[effect_id] + + # 更新统计信息 + self.stats['active_effects'] -= 1 + + # 从插件中移除效果 + if self.plugin and effect_id in self.plugin.effects: + del self.plugin.effects[effect_id] + + print(f"✓ 调制效果已删除: {effect_id}") + return True + + except Exception as e: + print(f"✗ 删除调制效果失败: {e}") + return False + + def process(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: + """ + 处理调制效果 + + Args: + audio_data: 输入音频数据 + parameters: 效果参数 + + Returns: + 处理后的音频数据 + """ + try: + import time + process_start = time.time() + + # 如果输入是NumPy数组 + if isinstance(audio_data, np.ndarray): + processed_data = self._process_numpy_array(audio_data, parameters) + else: + # 对于其他类型的数据,返回原始数据 + processed_data = audio_data + + # 更新统计信息 + self.stats['effects_processed'] += 1 + self.stats['processing_time'] += (time.time() - process_start) + + return processed_data + + except Exception as e: + print(f"✗ 调制效果处理失败: {e}") + return audio_data + + def _process_numpy_array(self, audio_data: np.ndarray, parameters: Dict[str, Any]) -> np.ndarray: + """ + 处理NumPy数组音频数据 + + Args: + audio_data: 输入音频数据 + parameters: 效果参数 + + Returns: + 处理后的音频数据 + """ + # 获取参数 + effect_type = parameters.get('type', 'chorus') + rate = max(0.01, min(10.0, parameters.get('rate', 1.0))) + depth = max(0.0, min(1.0, parameters.get('depth', 0.5))) + feedback = max(-1.0, min(1.0, parameters.get('feedback', 0.3))) + mix = max(0.0, min(1.0, parameters.get('mix', 0.5))) + delay = max(0.0001, min(0.05, parameters.get('delay', 0.002))) + center_delay = max(0.0001, min(0.05, parameters.get('center_delay', 0.002))) + spread = max(0.0, min(1.0, parameters.get('spread', 0.5))) + stereo = parameters.get('stereo', True) + lfo_waveform = parameters.get('lfo_waveform', 'sine') + high_cut = max(100.0, min(20000.0, parameters.get('high_cut', 8000.0))) + low_cut = max(20.0, min(10000.0, parameters.get('low_cut', 100.0))) + + # 创建输出数组 + if audio_data.ndim == 1: + # 单声道处理 + output_data = np.zeros_like(audio_data) + + # 初始化LFO相位 + lfo_phase = 0.0 + lfo_increment = 2.0 * math.pi * rate / self.sample_rate + + for i in range(len(audio_data)): + sample = audio_data[i] + + # 生成LFO值 + lfo_value = self._generate_lfo_value(lfo_waveform, lfo_phase) + + # 计算调制延迟 + if effect_type == 'chorus': + mod_delay = center_delay + depth * 0.005 * lfo_value + elif effect_type == 'flanger': + mod_delay = delay + depth * 0.005 * lfo_value + else: # phaser + mod_delay = delay + + # 限制延迟范围 + mod_delay = max(0.0001, min(0.05, mod_delay)) + + # 读取延迟样本(简化插值) + delayed_sample = self._read_delayed_sample(sample, mod_delay, feedback) + + # 应用滤波器(如果是镶边或移相器) + if effect_type in ['flanger', 'phaser']: + delayed_sample = self._apply_filter(delayed_sample, high_cut, low_cut) + + # 混合干湿信号 + output_data[i] = sample * (1.0 - mix) + delayed_sample * mix + + # 更新LFO相位 + lfo_phase += lfo_increment + if lfo_phase >= 2.0 * math.pi: + lfo_phase -= 2.0 * math.pi + + else: + # 多声道处理(立体声) + output_data = np.zeros_like(audio_data) + + # 初始化LFO相位 + lfo_phase_left = 0.0 + lfo_phase_right = parameters.get('lfo_phase', 0.0) * math.pi / 180.0 # 转换为弧度 + lfo_increment = 2.0 * math.pi * rate / self.sample_rate + + for channel in range(min(2, audio_data.shape[0])): # 最多处理两个声道 + lfo_phase = lfo_phase_left if channel == 0 else lfo_phase_right + + for i in range(audio_data.shape[1]): + sample = audio_data[channel, i] + + # 生成LFO值 + lfo_value = self._generate_lfo_value(lfo_waveform, lfo_phase) + + # 计算调制延迟 + if effect_type == 'chorus': + if channel == 0: # 左声道 + mod_delay = center_delay + depth * 0.005 * lfo_value * (1.0 - spread) + else: # 右声道 + mod_delay = center_delay + depth * 0.005 * lfo_value * (1.0 + spread) + elif effect_type == 'flanger': + mod_delay = delay + depth * 0.005 * lfo_value + else: # phaser + mod_delay = delay + + # 限制延迟范围 + mod_delay = max(0.0001, min(0.05, mod_delay)) + + # 读取延迟样本(简化插值) + delayed_sample = self._read_delayed_sample(sample, mod_delay, feedback) + + # 应用滤波器(如果是镶边或移相器) + if effect_type in ['flanger', 'phaser']: + delayed_sample = self._apply_filter(delayed_sample, high_cut, low_cut) + + # 混合干湿信号 + output_data[channel, i] = sample * (1.0 - mix) + delayed_sample * mix + + # 更新LFO相位 + lfo_phase += lfo_increment + if lfo_phase >= 2.0 * math.pi: + lfo_phase -= 2.0 * math.pi + + # 确保输出在有效范围内 + processed_data = np.clip(output_data, -1.0, 1.0) + return processed_data + + def _generate_lfo_value(self, waveform: str, phase: float) -> float: + """ + 生成LFO值 + + Args: + waveform: 波形类型 + phase: 相位(弧度) + + Returns: + LFO值 (-1.0 到 1.0) + """ + if waveform == 'sine': + return math.sin(phase) + elif waveform == 'triangle': + # 三角波 + normalized_phase = phase / (2.0 * math.pi) + if normalized_phase < 0.25: + return 4.0 * normalized_phase + elif normalized_phase < 0.75: + return 2.0 - 4.0 * normalized_phase + else: + return 4.0 * normalized_phase - 4.0 + elif waveform == 'square': + # 方波 + return 1.0 if math.sin(phase) >= 0.0 else -1.0 + elif waveform == 'sawtooth': + # 锯齿波 + normalized_phase = phase / (2.0 * math.pi) + return 2.0 * normalized_phase - 1.0 + else: + # 默认使用正弦波 + return math.sin(phase) + + def _read_delayed_sample(self, input_sample: float, delay_time: float, feedback: float) -> float: + """ + 读取延迟样本 + + Args: + input_sample: 输入样本 + delay_time: 延迟时间(秒) + feedback: 反馈系数 + + Returns: + 延迟样本 + """ + # 这里使用简化的延迟处理 + # 实际应用中会使用更复杂的延迟线和插值 + + # 计算延迟样本数 + delay_samples = delay_time * self.sample_rate + + # 简化的延迟效果(使用单一样本延迟) + delayed_sample = input_sample * 0.5 # 简化的延迟 + + # 应用反馈 + if abs(feedback) > 0.01: + delayed_sample += delayed_sample * feedback * 0.3 + + return delayed_sample + + def _apply_filter(self, sample: float, high_cut: float, low_cut: float) -> float: + """ + 应用滤波器 + + Args: + sample: 输入样本 + high_cut: 高频截止频率 + low_cut: 低频截止频率 + + Returns: + 滤波后的样本 + """ + # 这里使用简化的滤波器 + # 实际应用中会使用更复杂的滤波器设计 + + # 简单的一阶滤波器 + filtered_sample = sample + + # 高频截止(低通滤波器效果) + if high_cut < 10000.0: + filter_factor = high_cut / 10000.0 + filtered_sample *= (0.5 + 0.5 * filter_factor) + + # 低频截止(高通滤波器效果) + if low_cut > 100.0: + filter_factor = low_cut / 10000.0 + filtered_sample *= (0.5 + 0.5 * filter_factor) + + return filtered_sample + + def set_parameter(self, effect_id: str, parameter: str, value: Any) -> bool: + """ + 设置效果参数 + + Args: + effect_id: 效果ID + parameter: 参数名 + value: 参数值 + + Returns: + 是否设置成功 + """ + if effect_id not in self.effects: + print(f"✗ 调制效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['parameters'][parameter] = value + + # 如果是插件中的效果,也更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['parameters'][parameter] = value + + print(f"✓ 调制效果参数已设置: {effect_id}.{parameter} = {value}") + return True + + except Exception as e: + print(f"✗ 设置调制效果参数失败: {e}") + return False + + def get_parameters(self, effect_id: str) -> Dict[str, Any]: + """ + 获取效果参数 + + Args: + effect_id: 效果ID + + Returns: + 参数字典 + """ + if effect_id not in self.effects: + return {} + + return self.effects[effect_id]['parameters'].copy() + + def enable_effect(self, effect_id: str) -> bool: + """ + 启用调制效果 + + Args: + effect_id: 效果ID + + Returns: + 是否启用成功 + """ + if effect_id not in self.effects: + print(f"✗ 调制效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['active'] = True + self.stats['active_effects'] += 1 + + # 如果是插件中的效果,也更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['active'] = True + + print(f"✓ 调制效果已启用: {effect_id}") + return True + + except Exception as e: + print(f"✗ 启用调制效果失败: {e}") + return False + + def disable_effect(self, effect_id: str) -> bool: + """ + 禁用调制效果 + + Args: + effect_id: 效果ID + + Returns: + 是否禁用成功 + """ + if effect_id not in self.effects: + print(f"✗ 调制效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['active'] = False + self.stats['active_effects'] -= 1 + + # 如果是插件中的效果,也更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['active'] = False + + print(f"✓ 调制效果已禁用: {effect_id}") + return True + + except Exception as e: + print(f"✗ 禁用调制效果失败: {e}") + return False + + def cleanup(self): + """清理所有资源""" + self.effects.clear() + self.stats = { + 'total_effects': 0, + 'active_effects': 0, + 'effects_processed': 0, + 'processing_time': 0.0, + 'memory_usage': 0 + } + print("✓ 调制效果处理器资源已清理") + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def _get_current_time(self) -> float: + """ + 获取当前时间(秒) + + Returns: + 当前时间 + """ + if self.plugin and self.plugin.plugin_manager and self.plugin.plugin_manager.world: + return self.plugin.plugin_manager.world.globalClock.getFrameTime() + import time + return time.time() + + def update(self, dt: float): + """ + 更新处理器状态 + + Args: + dt: 时间增量 + """ + # 这里可以更新任何需要定期更新的状态 + pass + + def set_sample_rate(self, sample_rate: int): + """ + 设置采样率 + + Args: + sample_rate: 采样率 (Hz) + """ + self.sample_rate = sample_rate + print(f"✓ 调制效果处理器采样率已设置: {sample_rate} Hz") + + def set_rate(self, effect_id: str, rate: float) -> bool: + """ + 设置LFO速率 + + Args: + effect_id: 效果ID + rate: 速率 (Hz) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'rate', max(0.01, min(10.0, rate))) + + def set_depth(self, effect_id: str, depth: float) -> bool: + """ + 设置深度 + + Args: + effect_id: 效果ID + depth: 深度 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'depth', max(0.0, min(1.0, depth))) + + def set_feedback(self, effect_id: str, feedback: float) -> bool: + """ + 设置反馈 + + Args: + effect_id: 效果ID + feedback: 反馈 (-1.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'feedback', max(-1.0, min(1.0, feedback))) + + def set_mix(self, effect_id: str, mix: float) -> bool: + """ + 设置湿信号混合 + + Args: + effect_id: 效果ID + mix: 湿信号混合 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'mix', max(0.0, min(1.0, mix))) + + def set_delay(self, effect_id: str, delay: float) -> bool: + """ + 设置延迟时间 + + Args: + effect_id: 效果ID + delay: 延迟时间 (秒) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'delay', max(0.0001, min(0.05, delay))) + + def set_center_delay(self, effect_id: str, delay: float) -> bool: + """ + 设置中心延迟时间 + + Args: + effect_id: 效果ID + delay: 中心延迟时间 (秒) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'center_delay', max(0.0001, min(0.05, delay))) + + def set_spread(self, effect_id: str, spread: float) -> bool: + """ + 设置扩散 + + Args: + effect_id: 效果ID + spread: 扩散 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'spread', max(0.0, min(1.0, spread))) + + def set_lfo_waveform(self, effect_id: str, waveform: str) -> bool: + """ + 设置LFO波形 + + Args: + effect_id: 效果ID + waveform: 波形类型 (sine, triangle, square, sawtooth) + + Returns: + 是否设置成功 + """ + valid_waveforms = ['sine', 'triangle', 'square', 'sawtooth'] + if waveform not in valid_waveforms: + print(f"✗ 无效的LFO波形: {waveform}") + return False + + return self.set_parameter(effect_id, 'lfo_waveform', waveform) + + def enable_stereo(self, effect_id: str, enable: bool) -> bool: + """ + 启用/禁用立体声 + + Args: + effect_id: 效果ID + enable: 是否启用 + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'stereo', enable) + + def create_preset(self, preset_name: str) -> Dict[str, Any]: + """ + 创建调制效果预设 + + Args: + preset_name: 预设名称 + + Returns: + 预设参数字典 + """ + presets = { + 'chorus_light': { + 'type': 'chorus', + 'rate': 0.8, + 'depth': 0.3, + 'feedback': 0.2, + 'mix': 0.4, + 'center_delay': 0.004, + 'spread': 0.3, + 'lfo_waveform': 'sine' + }, + 'chorus_medium': { + 'type': 'chorus', + 'rate': 1.2, + 'depth': 0.5, + 'feedback': 0.3, + 'mix': 0.6, + 'center_delay': 0.006, + 'spread': 0.5, + 'lfo_waveform': 'sine' + }, + 'chorus_heavy': { + 'type': 'chorus', + 'rate': 1.8, + 'depth': 0.7, + 'feedback': 0.4, + 'mix': 0.8, + 'center_delay': 0.008, + 'spread': 0.7, + 'lfo_waveform': 'triangle' + }, + 'flanger_light': { + 'type': 'flanger', + 'rate': 0.5, + 'depth': 0.4, + 'feedback': 0.5, + 'mix': 0.5, + 'delay': 0.001, + 'lfo_waveform': 'sine' + }, + 'flanger_medium': { + 'type': 'flanger', + 'rate': 0.8, + 'depth': 0.6, + 'feedback': 0.7, + 'mix': 0.7, + 'delay': 0.002, + 'lfo_waveform': 'sine' + }, + 'flanger_heavy': { + 'type': 'flanger', + 'rate': 1.2, + 'depth': 0.8, + 'feedback': 0.9, + 'mix': 0.9, + 'delay': 0.003, + 'lfo_waveform': 'triangle' + }, + 'phaser_light': { + 'type': 'phaser', + 'rate': 0.6, + 'depth': 0.3, + 'feedback': 0.2, + 'mix': 0.4, + 'delay': 0.0005, + 'lfo_waveform': 'sine', + 'high_cut': 6000.0, + 'low_cut': 200.0 + }, + 'phaser_medium': { + 'type': 'phaser', + 'rate': 1.0, + 'depth': 0.5, + 'feedback': 0.4, + 'mix': 0.6, + 'delay': 0.001, + 'lfo_waveform': 'sine', + 'high_cut': 8000.0, + 'low_cut': 100.0 + }, + 'phaser_heavy': { + 'type': 'phaser', + 'rate': 1.5, + 'depth': 0.7, + 'feedback': 0.6, + 'mix': 0.8, + 'delay': 0.002, + 'lfo_waveform': 'triangle', + 'high_cut': 10000.0, + 'low_cut': 50.0 + } + } + + return presets.get(preset_name, {}).copy() + + def get_lfo_meter(self, effect_id: str) -> float: + """ + 获取LFO表 + + Args: + effect_id: 效果ID + + Returns: + 当前LFO值 (-1.0 到 1.0) + """ + if effect_id not in self.effects: + return 0.0 + + effect = self.effects[effect_id] + parameters = effect['parameters'] + lfo_waveform = parameters.get('lfo_waveform', 'sine') + lfo_phase = effect.get('lfo_phase', 0.0) + + return self._generate_lfo_value(lfo_waveform, lfo_phase) + + def set_lfo_phase(self, effect_id: str, phase: float) -> bool: + """ + 设置LFO相位 + + Args: + effect_id: 效果ID + phase: 相位 (度) + + Returns: + 是否设置成功 + """ + # 转换为弧度并规范化 + phase_rad = (phase * math.pi / 180.0) % (2.0 * math.pi) + return self.set_parameter(effect_id, 'lfo_phase', phase) + diff --git a/plugins/user/audio_reverb_effects/effects/reverb_processor.py b/plugins/user/audio_reverb_effects/effects/reverb_processor.py index 87c200a8..8ea863f9 100644 --- a/plugins/user/audio_reverb_effects/effects/reverb_processor.py +++ b/plugins/user/audio_reverb_effects/effects/reverb_processor.py @@ -1,1038 +1,1038 @@ -""" -混响效果处理器 -实现专业的音频混响效果处理 -""" - -import uuid -import math -import numpy as np -from typing import Dict, List, Any, Optional -from scipy import signal - -class ReverbProcessor: - """ - 混响效果处理器 - 实现专业的音频混响效果处理 - """ - - def __init__(self, plugin): - """ - 初始化混响效果处理器 - - Args: - plugin: 音频效果插件实例 - """ - self.plugin = plugin - self.effects: Dict[str, Dict[str, Any]] = {} - self.comb_filters = {} # 梳状滤波器 - self.allpass_filters = {} # 全通滤波器 - self.stats = { - 'total_effects': 0, - 'active_effects': 0, - 'effects_processed': 0, - 'processing_time': 0.0, - 'memory_usage': 0 - } - self.buffer_size = plugin.buffer_size if plugin else 4096 - self.sample_rate = 44100 # 默认采样率 - - # 初始化混响参数 - self._initialize_reverb_parameters() - - def _initialize_reverb_parameters(self): - """初始化混响参数""" - # 梳状滤波器延迟时间(毫秒) - self.comb_delays = [1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617] - - # 全通滤波器延迟时间(毫秒) - self.allpass_delays = [556, 441, 341, 225] - - # 混响衰减系数 - self.comb_decay_factors = [0.8, 0.75, 0.7, 0.65, 0.6, 0.55, 0.5, 0.45] - - # 全通滤波器系数 - self.allpass_feedback = 0.7 - - def create_effect(self, parameters: Dict[str, Any]) -> str: - """ - 创建混响效果 - - Args: - parameters: 混响参数 - - Returns: - 效果ID - """ - try: - # 生成唯一ID - effect_id = str(uuid.uuid4()) - - # 默认参数 - default_params = { - 'room_size': 0.5, # 房间大小 (0.0-1.0) - 'damping': 0.5, # 阻尼 (0.0-1.0) - 'wet_level': 0.33, # 湿信号电平 (0.0-1.0) - 'dry_level': 0.4, # 干信号电平 (0.0-1.0) - 'width': 1.0, # 宽度 (0.0-1.0) - 'freeze_mode': 0.0, # 冻结模式 (0.0-1.0) - 'early_reflections': 0.5, # 早期反射 (0.0-1.0) - 'late_reverb': 0.5, # 晚期混响 (0.0-1.0) - 'diffusion': 0.7, # 扩散 (0.0-1.0) - 'density': 0.5, # 密度 (0.0-1.0) - 'predelay': 0.0, # 预延迟 (秒) - 'high_freq_damping': 0.5, # 高频阻尼 (0.0-1.0) - 'low_freq_damping': 0.3, # 低频阻尼 (0.0-1.0) - 'stereo_spread': 0.5, # 立体声扩散 (0.0-1.0) - 'reverb_time': 2.0, # 混响时间 (秒) - 'tail_density': 0.8, # 尾部密度 (0.0-1.0) - 'modulation_rate': 0.1, # 调制速率 (Hz) - 'modulation_depth': 0.02 # 调制深度 (0.0-1.0) - } - - # 合并参数 - effect_params = default_params.copy() - effect_params.update(parameters) - - # 创建效果对象 - effect = { - 'id': effect_id, - 'type': 'reverb', - 'parameters': effect_params, - 'active': True, - 'buffers': {}, # 各种缓冲区 - 'comb_filters': [], # 梳状滤波器数组 - 'allpass_filters': [], # 全通滤波器数组 - 'modulation_phase': 0.0, # 调制相位 - 'created_time': self._get_current_time(), - 'process_count': 0 - } - - # 初始化滤波器 - self._initialize_filters(effect) - - # 初始化缓冲区 - self._initialize_buffers(effect) - - # 存储效果 - self.effects[effect_id] = effect - - # 更新统计信息 - self.stats['total_effects'] += 1 - self.stats['active_effects'] += 1 - - # 在插件中注册效果 - if self.plugin: - self.plugin.effects[effect_id] = effect.copy() - - print(f"✓ 混响效果创建成功: {effect_id}") - return effect_id - - except Exception as e: - print(f"✗ 创建混响效果失败: {e}") - import traceback - traceback.print_exc() - return "" - - def _initialize_filters(self, effect: Dict[str, Any]): - """ - 初始化混响滤波器 - - Args: - effect: 效果对象 - """ - parameters = effect['parameters'] - room_size = parameters.get('room_size', 0.5) - damping = parameters.get('damping', 0.5) - diffusion = parameters.get('diffusion', 0.7) - density = parameters.get('density', 0.5) - - # 创建梳状滤波器 - comb_filters = [] - for i, (delay_ms, decay_factor) in enumerate(zip(self.comb_delays, self.comb_decay_factors)): - # 根据房间大小调整延迟时间 - adjusted_delay = delay_ms * (0.5 + room_size * 0.5) - - # 根据密度调整延迟时间 - adjusted_delay *= (0.8 + density * 0.4) - - # 计算延迟样本数 - delay_samples = int(adjusted_delay * self.sample_rate / 1000) - - # 根据扩散调整反馈系数 - feedback = decay_factor * (0.7 + diffusion * 0.3) - - # 根据阻尼调整高频衰减 - damp_factor = 0.3 + damping * 0.5 - - comb_filter = { - 'buffer': np.zeros(max(1, delay_samples)), - 'index': 0, - 'feedback': feedback, - 'damp1': damp_factor, - 'damp2': 0.0, - 'filter_store': 0.0 - } - comb_filters.append(comb_filter) - - effect['comb_filters'] = comb_filters - - # 创建全通滤波器 - allpass_filters = [] - for i, delay_ms in enumerate(self.allpass_delays): - # 根据房间大小调整延迟时间 - adjusted_delay = delay_ms * (0.5 + room_size * 0.5) - - # 计算延迟样本数 - delay_samples = int(adjusted_delay * self.sample_rate / 1000) - - allpass_filter = { - 'buffer': np.zeros(max(1, delay_samples)), - 'index': 0, - 'feedback': self.allpass_feedback - } - allpass_filters.append(allpass_filter) - - effect['allpass_filters'] = allpass_filters - - def _initialize_buffers(self, effect: Dict[str, Any]): - """ - 初始化混响缓冲区 - - Args: - effect: 效果对象 - """ - effect_id = effect['id'] - buffers = {} - - # 输入和输出缓冲区 - buffers['input'] = np.zeros(self.buffer_size) - buffers['output_left'] = np.zeros(self.buffer_size) - buffers['output_right'] = np.zeros(self.buffer_size) - buffers['mono_input'] = np.zeros(self.buffer_size) - - # 预延迟缓冲区 - predelay = effect['parameters'].get('predelay', 0.0) - predelay_samples = int(predelay * self.sample_rate) - buffers['predelay_buffer'] = np.zeros(max(1, predelay_samples)) - buffers['predelay_index'] = 0 - - effect['buffers'] = buffers - - def delete_effect(self, effect_id: str) -> bool: - """ - 删除混响效果 - - Args: - effect_id: 效果ID - - Returns: - 是否删除成功 - """ - if effect_id not in self.effects: - print(f"✗ 混响效果不存在: {effect_id}") - return False - - try: - # 删除效果 - del self.effects[effect_id] - - # 更新统计信息 - self.stats['active_effects'] -= 1 - - # 从插件中移除效果 - if self.plugin and effect_id in self.plugin.effects: - del self.plugin.effects[effect_id] - - print(f"✓ 混响效果已删除: {effect_id}") - return True - - except Exception as e: - print(f"✗ 删除混响效果失败: {e}") - return False - - def process(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: - """ - 处理混响效果 - - Args: - audio_data: 输入音频数据 - parameters: 效果参数 - - Returns: - 处理后的音频数据 - """ - try: - import time - process_start = time.time() - - # 如果输入是NumPy数组 - if isinstance(audio_data, np.ndarray): - processed_data = self._process_numpy_array(audio_data, parameters) - else: - # 对于其他类型的数据,返回原始数据 - processed_data = audio_data - - # 更新统计信息 - self.stats['effects_processed'] += 1 - self.stats['processing_time'] += (time.time() - process_start) - - return processed_data - - except Exception as e: - print(f"✗ 混响效果处理失败: {e}") - return audio_data - - def _process_numpy_array(self, audio_data: np.ndarray, parameters: Dict[str, Any]) -> np.ndarray: - """ - 处理NumPy数组音频数据 - - Args: - audio_data: 输入音频数据 - parameters: 效果参数 - - Returns: - 处理后的音频数据 - """ - # 获取参数 - room_size = parameters.get('room_size', 0.5) - damping = parameters.get('damping', 0.5) - wet_level = parameters.get('wet_level', 0.33) - dry_level = parameters.get('dry_level', 0.4) - width = parameters.get('width', 1.0) - freeze_mode = parameters.get('freeze_mode', 0.0) - early_reflections = parameters.get('early_reflections', 0.5) - late_reverb = parameters.get('late_reverb', 0.5) - diffusion = parameters.get('diffusion', 0.7) - density = parameters.get('density', 0.5) - predelay = parameters.get('predelay', 0.0) - high_freq_damping = parameters.get('high_freq_damping', 0.5) - low_freq_damping = parameters.get('low_freq_damping', 0.3) - stereo_spread = parameters.get('stereo_spread', 0.5) - reverb_time = parameters.get('reverb_time', 2.0) - tail_density = parameters.get('tail_density', 0.8) - modulation_rate = parameters.get('modulation_rate', 0.1) - modulation_depth = parameters.get('modulation_depth', 0.02) - - # 如果是冻结模式,增加反馈 - if freeze_mode > 0.5: - room_size = min(1.0, room_size + 0.2) - - # 创建输出数组 - if audio_data.ndim == 1: - # 单声道输入,生成立体声输出 - output_left = np.zeros_like(audio_data) - output_right = np.zeros_like(audio_data) - - # 处理每个样本 - for i in range(len(audio_data)): - input_sample = audio_data[i] - - # 预延迟处理 - delayed_sample = self._apply_predelay(input_sample, predelay) - - # 混响处理 - reverb_left, reverb_right = self._apply_reverb( - delayed_sample, room_size, damping, diffusion, density, - stereo_spread, modulation_rate, modulation_depth) - - # 应用早期反射和晚期混响权重 - reverb_left *= (early_reflections * 0.3 + late_reverb * 0.7) - reverb_right *= (early_reflections * 0.3 + late_reverb * 0.7) - - # 混合干湿信号 - output_left[i] = input_sample * dry_level + reverb_left * wet_level - output_right[i] = input_sample * dry_level + reverb_right * wet_level - - # 合并为立体声数组 - output_data = np.array([output_left, output_right]) - - else: - # 多声道处理(假设已经是立体声) - if audio_data.shape[0] >= 2: - output_left = np.zeros_like(audio_data[0]) - output_right = np.zeros_like(audio_data[1]) - - for i in range(audio_data.shape[1]): - # 将左右声道混合为单声道输入 - mono_input = (audio_data[0, i] + audio_data[1, i]) * 0.5 - - # 预延迟处理 - delayed_sample = self._apply_predelay(mono_input, predelay) - - # 混响处理 - reverb_left, reverb_right = self._apply_reverb( - delayed_sample, room_size, damping, diffusion, density, - stereo_spread, modulation_rate, modulation_depth) - - # 应用早期反射和晚期混响权重 - reverb_left *= (early_reflections * 0.3 + late_reverb * 0.7) - reverb_right *= (early_reflections * 0.3 + late_reverb * 0.7) - - # 混合干湿信号 - output_left[i] = audio_data[0, i] * dry_level + reverb_left * wet_level - output_right[i] = audio_data[1, i] * dry_level + reverb_right * wet_level - - output_data = np.array([output_left, output_right]) - else: - # 其他多声道情况,返回原始数据 - output_data = audio_data - - # 确保输出在有效范围内 - processed_data = np.clip(output_data, -1.0, 1.0) - return processed_data - - def _apply_predelay(self, sample: float, predelay: float) -> float: - """ - 应用预延迟 - - Args: - sample: 输入样本 - predelay: 预延迟时间 (秒) - - Returns: - 延迟后的样本 - """ - # 简化的预延迟实现 - # 实际应用中会使用延迟线 - return sample # 简化实现 - - def _apply_reverb(self, sample: float, room_size: float, damping: float, - diffusion: float, density: float, stereo_spread: float, - modulation_rate: float, modulation_depth: float) -> tuple: - """ - 应用混响处理 - - Args: - sample: 输入样本 - room_size: 房间大小 - damping: 阻尼 - diffusion: 扩散 - density: 密度 - stereo_spread: 立体声扩散 - modulation_rate: 调制速率 - modulation_depth: 调制深度 - - Returns: - (左声道混响, 右声道混响) - """ - # 简化的混响实现 - # 实际应用中会使用梳状滤波器和全通滤波器链 - - # 生成基于参数的混响效果 - reverb_amount = 0.2 + room_size * 0.5 - damp_factor = 0.2 + damping * 0.6 - diffuse_factor = 0.5 + diffusion * 0.3 - - # 左声道混响 - left_reverb = sample * reverb_amount * (1.0 - damp_factor) - left_reverb *= (0.7 + diffuse_factor * 0.3) - - # 右声道混响(添加立体声扩散) - stereo_offset = stereo_spread * 0.3 - right_reverb = sample * (reverb_amount + stereo_offset) * (1.0 - damp_factor * 0.8) - right_reverb *= (0.7 + diffuse_factor * 0.3) - - return left_reverb, right_reverb - - def set_parameter(self, effect_id: str, parameter: str, value: Any) -> bool: - """ - 设置效果参数 - - Args: - effect_id: 效果ID - parameter: 参数名 - value: 参数值 - - Returns: - 是否设置成功 - """ - if effect_id not in self.effects: - print(f"✗ 混响效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['parameters'][parameter] = value - - # 如果是影响滤波器的参数,重新初始化滤波器 - filter_params = ['room_size', 'damping', 'diffusion', 'density'] - if parameter in filter_params: - self._initialize_filters(self.effects[effect_id]) - - # 如果是插件中的效果,也更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['parameters'][parameter] = value - if parameter in filter_params: - self.plugin.effects[effect_id]['comb_filters'] = \ - self.effects[effect_id]['comb_filters'].copy() - self.plugin.effects[effect_id]['allpass_filters'] = \ - self.effects[effect_id]['allpass_filters'].copy() - - print(f"✓ 混响效果参数已设置: {effect_id}.{parameter} = {value}") - return True - - except Exception as e: - print(f"✗ 设置混响效果参数失败: {e}") - return False - - def get_parameters(self, effect_id: str) -> Dict[str, Any]: - """ - 获取效果参数 - - Args: - effect_id: 效果ID - - Returns: - 参数字典 - """ - if effect_id not in self.effects: - return {} - - return self.effects[effect_id]['parameters'].copy() - - def enable_effect(self, effect_id: str) -> bool: - """ - 启用混响效果 - - Args: - effect_id: 效果ID - - Returns: - 是否启用成功 - """ - if effect_id not in self.effects: - print(f"✗ 混响效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['active'] = True - self.stats['active_effects'] += 1 - - # 如果是插件中的效果,也更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['active'] = True - - print(f"✓ 混响效果已启用: {effect_id}") - return True - - except Exception as e: - print(f"✗ 启用混响效果失败: {e}") - return False - - def disable_effect(self, effect_id: str) -> bool: - """ - 禁用混响效果 - - Args: - effect_id: 效果ID - - Returns: - 是否禁用成功 - """ - if effect_id not in self.effects: - print(f"✗ 混响效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['active'] = False - self.stats['active_effects'] -= 1 - - # 如果是插件中的效果,也更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['active'] = False - - print(f"✓ 混响效果已禁用: {effect_id}") - return True - - except Exception as e: - print(f"✗ 禁用混响效果失败: {e}") - return False - - def cleanup(self): - """清理所有资源""" - self.effects.clear() - self.comb_filters.clear() - self.allpass_filters.clear() - self.stats = { - 'total_effects': 0, - 'active_effects': 0, - 'effects_processed': 0, - 'processing_time': 0.0, - 'memory_usage': 0 - } - print("✓ 混响效果处理器资源已清理") - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def _get_current_time(self) -> float: - """ - 获取当前时间(秒) - - Returns: - 当前时间 - """ - if self.plugin and self.plugin.plugin_manager and self.plugin.plugin_manager.world: - return self.plugin.plugin_manager.world.globalClock.getFrameTime() - import time - return time.time() - - def update(self, dt: float): - """ - 更新处理器状态 - - Args: - dt: 时间增量 - """ - # 更新调制相位 - for effect_id, effect in self.effects.items(): - modulation_rate = effect['parameters'].get('modulation_rate', 0.1) - effect['modulation_phase'] += 2.0 * math.pi * modulation_rate * dt - if effect['modulation_phase'] >= 2.0 * math.pi: - effect['modulation_phase'] -= 2.0 * math.pi - - def set_sample_rate(self, sample_rate: int): - """ - 设置采样率 - - Args: - sample_rate: 采样率 (Hz) - """ - old_sample_rate = self.sample_rate - self.sample_rate = sample_rate - print(f"✓ 混响处理器采样率已从 {old_sample_rate} 更新为: {sample_rate} Hz") - - # 重新初始化所有滤波器以适应新的采样率 - for effect_id, effect in self.effects.items(): - self._initialize_filters(effect) - self._initialize_buffers(effect) - - def set_room_size(self, effect_id: str, room_size: float) -> bool: - """ - 设置房间大小 - - Args: - effect_id: 效果ID - room_size: 房间大小 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'room_size', max(0.0, min(1.0, room_size))) - - def set_damping(self, effect_id: str, damping: float) -> bool: - """ - 设置阻尼 - - Args: - effect_id: 效果ID - damping: 阻尼 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'damping', max(0.0, min(1.0, damping))) - - def set_wet_level(self, effect_id: str, wet_level: float) -> bool: - """ - 设置湿信号电平 - - Args: - effect_id: 效果ID - wet_level: 湿信号电平 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'wet_level', max(0.0, min(1.0, wet_level))) - - def set_dry_level(self, effect_id: str, dry_level: float) -> bool: - """ - 设置干信号电平 - - Args: - effect_id: 效果ID - dry_level: 干信号电平 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'dry_level', max(0.0, min(1.0, dry_level))) - - def set_width(self, effect_id: str, width: float) -> bool: - """ - 设置宽度 - - Args: - effect_id: 效果ID - width: 宽度 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'width', max(0.0, min(1.0, width))) - - def set_freeze_mode(self, effect_id: str, freeze_mode: float) -> bool: - """ - 设置冻结模式 - - Args: - effect_id: 效果ID - freeze_mode: 冻结模式 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'freeze_mode', max(0.0, min(1.0, freeze_mode))) - - def set_early_reflections(self, effect_id: str, reflections: float) -> bool: - """ - 设置早期反射 - - Args: - effect_id: 效果ID - reflections: 早期反射 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'early_reflections', max(0.0, min(1.0, reflections))) - - def set_late_reverb(self, effect_id: str, reverb: float) -> bool: - """ - 设置晚期混响 - - Args: - effect_id: 效果ID - reverb: 晚期混响 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'late_reverb', max(0.0, min(1.0, reverb))) - - def set_diffusion(self, effect_id: str, diffusion: float) -> bool: - """ - 设置扩散参数 - - Args: - effect_id: 效果ID - diffusion: 扩散值 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'diffusion', max(0.0, min(1.0, diffusion))) - - def set_density(self, effect_id: str, density: float) -> bool: - """ - 设置密度参数 - - Args: - effect_id: 效果ID - density: 密度值 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'density', max(0.0, min(1.0, density))) - - def set_predelay(self, effect_id: str, predelay: float) -> bool: - """ - 设置预延迟 - - Args: - effect_id: 效果ID - predelay: 预延迟 (秒) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'predelay', max(0.0, min(0.5, predelay))) - - def set_high_freq_damping(self, effect_id: str, damping: float) -> bool: - """ - 设置高频阻尼 - - Args: - effect_id: 效果ID - damping: 高频阻尼 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'high_freq_damping', max(0.0, min(1.0, damping))) - - def set_low_freq_damping(self, effect_id: str, damping: float) -> bool: - """ - 设置低频阻尼 - - Args: - effect_id: 效果ID - damping: 低频阻尼 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'low_freq_damping', max(0.0, min(1.0, damping))) - - def set_stereo_spread(self, effect_id: str, spread: float) -> bool: - """ - 设置立体声扩散 - - Args: - effect_id: 效果ID - spread: 立体声扩散 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'stereo_spread', max(0.0, min(1.0, spread))) - - def set_reverb_time(self, effect_id: str, time: float) -> bool: - """ - 设置混响时间 - - Args: - effect_id: 效果ID - time: 混响时间 (秒) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'reverb_time', max(0.1, min(30.0, time))) - - def set_tail_density(self, effect_id: str, density: float) -> bool: - """ - 设置尾部密度 - - Args: - effect_id: 效果ID - density: 尾部密度 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'tail_density', max(0.0, min(1.0, density))) - - def set_modulation_rate(self, effect_id: str, rate: float) -> bool: - """ - 设置调制速率 - - Args: - effect_id: 效果ID - rate: 调制速率 (Hz) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'modulation_rate', max(0.01, min(10.0, rate))) - - def set_modulation_depth(self, effect_id: str, depth: float) -> bool: - """ - 设置调制深度 - - Args: - effect_id: 效果ID - depth: 调制深度 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'modulation_depth', max(0.0, min(1.0, depth))) - - def get_reverb_time(self, effect_id: str) -> float: - """ - 获取混响时间表 - - Args: - effect_id: 效果ID - - Returns: - 当前混响时间 (秒) - """ - if effect_id not in self.effects: - return 0.0 - - return self.effects[effect_id]['parameters'].get('reverb_time', 2.0) - - def get_room_size_meter(self, effect_id: str) -> float: - """ - 获取房间大小表 - - Args: - effect_id: 效果ID - - Returns: - 当前房间大小 (0.0-1.0) - """ - if effect_id not in self.effects: - return 0.0 - - return self.effects[effect_id]['parameters'].get('room_size', 0.5) - - def create_preset(self, preset_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]: - """ - 创建混响预设 - - Args: - preset_name: 预设名称 - parameters: 参数 - - Returns: - 预设参数字典 - """ - presets = { - 'small_room': { - 'room_size': 0.2, - 'damping': 0.7, - 'wet_level': 0.2, - 'dry_level': 0.8, - 'width': 0.8, - 'early_reflections': 0.6, - 'late_reverb': 0.4, - 'diffusion': 0.6, - 'density': 0.3, - 'reverb_time': 1.0 - }, - 'medium_room': { - 'room_size': 0.5, - 'damping': 0.5, - 'wet_level': 0.3, - 'dry_level': 0.7, - 'width': 0.9, - 'early_reflections': 0.7, - 'late_reverb': 0.5, - 'diffusion': 0.7, - 'density': 0.5, - 'reverb_time': 1.5 - }, - 'large_room': { - 'room_size': 0.7, - 'damping': 0.4, - 'wet_level': 0.4, - 'dry_level': 0.6, - 'width': 1.0, - 'early_reflections': 0.8, - 'late_reverb': 0.6, - 'diffusion': 0.8, - 'density': 0.7, - 'reverb_time': 2.0 - }, - 'large_hall': { - 'room_size': 0.9, - 'damping': 0.3, - 'wet_level': 0.5, - 'dry_level': 0.5, - 'width': 1.0, - 'early_reflections': 0.9, - 'late_reverb': 0.7, - 'diffusion': 0.9, - 'density': 0.8, - 'reverb_time': 3.0 - }, - 'cathedral': { - 'room_size': 1.0, - 'damping': 0.2, - 'wet_level': 0.7, - 'dry_level': 0.3, - 'width': 1.0, - 'early_reflections': 1.0, - 'late_reverb': 0.8, - 'diffusion': 1.0, - 'density': 1.0, - 'reverb_time': 5.0 - }, - 'bathroom': { - 'room_size': 0.2, - 'damping': 0.1, - 'wet_level': 0.8, - 'dry_level': 0.2, - 'width': 0.9, - 'early_reflections': 0.5, - 'late_reverb': 0.9, - 'diffusion': 0.5, - 'density': 0.2, - 'reverb_time': 0.8 - }, - 'tunnel': { - 'room_size': 0.8, - 'damping': 0.6, - 'wet_level': 0.6, - 'dry_level': 0.4, - 'width': 0.7, - 'early_reflections': 0.7, - 'late_reverb': 0.7, - 'diffusion': 0.6, - 'density': 0.6, - 'reverb_time': 2.5 - }, - 'arena': { - 'room_size': 0.95, - 'damping': 0.25, - 'wet_level': 0.6, - 'dry_level': 0.4, - 'width': 1.0, - 'early_reflections': 0.95, - 'late_reverb': 0.75, - 'diffusion': 0.95, - 'density': 0.9, - 'reverb_time': 4.0 - } - } - - if preset_name in presets: - preset_params = presets[preset_name].copy() - preset_params.update(parameters) - return preset_params - else: - return parameters.copy() - - def get_frequency_response(self, effect_id: str) -> Dict[str, Any]: - """ - 获取频率响应 - - Args: - effect_id: 效果ID - - Returns: - 频率响应数据 - """ - if effect_id not in self.effects: - return {} - - parameters = self.effects[effect_id]['parameters'] - - # 生成简化的频率响应数据 - frequencies = np.logspace(np.log10(20), np.log10(20000), 100) - response = [] - - room_size = parameters.get('room_size', 0.5) - damping = parameters.get('damping', 0.5) - high_freq_damping = parameters.get('high_freq_damping', 0.5) - low_freq_damping = parameters.get('low_freq_damping', 0.3) - - for freq in frequencies: - # 计算基于参数的频率响应 - if freq < 200: # 低频 - gain = 1.0 - low_freq_damping * 0.3 - elif freq > 5000: # 高频 - gain = 1.0 - high_freq_damping * 0.5 - else: # 中频 - gain = 1.0 - damping * 0.2 - - # 房间大小影响 - gain *= (0.8 + room_size * 0.4) - - response.append(gain) - - return { - 'frequencies': frequencies.tolist(), - 'response': response - } - +""" +混响效果处理器 +实现专业的音频混响效果处理 +""" + +import uuid +import math +import numpy as np +from typing import Dict, List, Any, Optional +from scipy import signal + +class ReverbProcessor: + """ + 混响效果处理器 + 实现专业的音频混响效果处理 + """ + + def __init__(self, plugin): + """ + 初始化混响效果处理器 + + Args: + plugin: 音频效果插件实例 + """ + self.plugin = plugin + self.effects: Dict[str, Dict[str, Any]] = {} + self.comb_filters = {} # 梳状滤波器 + self.allpass_filters = {} # 全通滤波器 + self.stats = { + 'total_effects': 0, + 'active_effects': 0, + 'effects_processed': 0, + 'processing_time': 0.0, + 'memory_usage': 0 + } + self.buffer_size = plugin.buffer_size if plugin else 4096 + self.sample_rate = 44100 # 默认采样率 + + # 初始化混响参数 + self._initialize_reverb_parameters() + + def _initialize_reverb_parameters(self): + """初始化混响参数""" + # 梳状滤波器延迟时间(毫秒) + self.comb_delays = [1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617] + + # 全通滤波器延迟时间(毫秒) + self.allpass_delays = [556, 441, 341, 225] + + # 混响衰减系数 + self.comb_decay_factors = [0.8, 0.75, 0.7, 0.65, 0.6, 0.55, 0.5, 0.45] + + # 全通滤波器系数 + self.allpass_feedback = 0.7 + + def create_effect(self, parameters: Dict[str, Any]) -> str: + """ + 创建混响效果 + + Args: + parameters: 混响参数 + + Returns: + 效果ID + """ + try: + # 生成唯一ID + effect_id = str(uuid.uuid4()) + + # 默认参数 + default_params = { + 'room_size': 0.5, # 房间大小 (0.0-1.0) + 'damping': 0.5, # 阻尼 (0.0-1.0) + 'wet_level': 0.33, # 湿信号电平 (0.0-1.0) + 'dry_level': 0.4, # 干信号电平 (0.0-1.0) + 'width': 1.0, # 宽度 (0.0-1.0) + 'freeze_mode': 0.0, # 冻结模式 (0.0-1.0) + 'early_reflections': 0.5, # 早期反射 (0.0-1.0) + 'late_reverb': 0.5, # 晚期混响 (0.0-1.0) + 'diffusion': 0.7, # 扩散 (0.0-1.0) + 'density': 0.5, # 密度 (0.0-1.0) + 'predelay': 0.0, # 预延迟 (秒) + 'high_freq_damping': 0.5, # 高频阻尼 (0.0-1.0) + 'low_freq_damping': 0.3, # 低频阻尼 (0.0-1.0) + 'stereo_spread': 0.5, # 立体声扩散 (0.0-1.0) + 'reverb_time': 2.0, # 混响时间 (秒) + 'tail_density': 0.8, # 尾部密度 (0.0-1.0) + 'modulation_rate': 0.1, # 调制速率 (Hz) + 'modulation_depth': 0.02 # 调制深度 (0.0-1.0) + } + + # 合并参数 + effect_params = default_params.copy() + effect_params.update(parameters) + + # 创建效果对象 + effect = { + 'id': effect_id, + 'type': 'reverb', + 'parameters': effect_params, + 'active': True, + 'buffers': {}, # 各种缓冲区 + 'comb_filters': [], # 梳状滤波器数组 + 'allpass_filters': [], # 全通滤波器数组 + 'modulation_phase': 0.0, # 调制相位 + 'created_time': self._get_current_time(), + 'process_count': 0 + } + + # 初始化滤波器 + self._initialize_filters(effect) + + # 初始化缓冲区 + self._initialize_buffers(effect) + + # 存储效果 + self.effects[effect_id] = effect + + # 更新统计信息 + self.stats['total_effects'] += 1 + self.stats['active_effects'] += 1 + + # 在插件中注册效果 + if self.plugin: + self.plugin.effects[effect_id] = effect.copy() + + print(f"✓ 混响效果创建成功: {effect_id}") + return effect_id + + except Exception as e: + print(f"✗ 创建混响效果失败: {e}") + import traceback + traceback.print_exc() + return "" + + def _initialize_filters(self, effect: Dict[str, Any]): + """ + 初始化混响滤波器 + + Args: + effect: 效果对象 + """ + parameters = effect['parameters'] + room_size = parameters.get('room_size', 0.5) + damping = parameters.get('damping', 0.5) + diffusion = parameters.get('diffusion', 0.7) + density = parameters.get('density', 0.5) + + # 创建梳状滤波器 + comb_filters = [] + for i, (delay_ms, decay_factor) in enumerate(zip(self.comb_delays, self.comb_decay_factors)): + # 根据房间大小调整延迟时间 + adjusted_delay = delay_ms * (0.5 + room_size * 0.5) + + # 根据密度调整延迟时间 + adjusted_delay *= (0.8 + density * 0.4) + + # 计算延迟样本数 + delay_samples = int(adjusted_delay * self.sample_rate / 1000) + + # 根据扩散调整反馈系数 + feedback = decay_factor * (0.7 + diffusion * 0.3) + + # 根据阻尼调整高频衰减 + damp_factor = 0.3 + damping * 0.5 + + comb_filter = { + 'buffer': np.zeros(max(1, delay_samples)), + 'index': 0, + 'feedback': feedback, + 'damp1': damp_factor, + 'damp2': 0.0, + 'filter_store': 0.0 + } + comb_filters.append(comb_filter) + + effect['comb_filters'] = comb_filters + + # 创建全通滤波器 + allpass_filters = [] + for i, delay_ms in enumerate(self.allpass_delays): + # 根据房间大小调整延迟时间 + adjusted_delay = delay_ms * (0.5 + room_size * 0.5) + + # 计算延迟样本数 + delay_samples = int(adjusted_delay * self.sample_rate / 1000) + + allpass_filter = { + 'buffer': np.zeros(max(1, delay_samples)), + 'index': 0, + 'feedback': self.allpass_feedback + } + allpass_filters.append(allpass_filter) + + effect['allpass_filters'] = allpass_filters + + def _initialize_buffers(self, effect: Dict[str, Any]): + """ + 初始化混响缓冲区 + + Args: + effect: 效果对象 + """ + effect_id = effect['id'] + buffers = {} + + # 输入和输出缓冲区 + buffers['input'] = np.zeros(self.buffer_size) + buffers['output_left'] = np.zeros(self.buffer_size) + buffers['output_right'] = np.zeros(self.buffer_size) + buffers['mono_input'] = np.zeros(self.buffer_size) + + # 预延迟缓冲区 + predelay = effect['parameters'].get('predelay', 0.0) + predelay_samples = int(predelay * self.sample_rate) + buffers['predelay_buffer'] = np.zeros(max(1, predelay_samples)) + buffers['predelay_index'] = 0 + + effect['buffers'] = buffers + + def delete_effect(self, effect_id: str) -> bool: + """ + 删除混响效果 + + Args: + effect_id: 效果ID + + Returns: + 是否删除成功 + """ + if effect_id not in self.effects: + print(f"✗ 混响效果不存在: {effect_id}") + return False + + try: + # 删除效果 + del self.effects[effect_id] + + # 更新统计信息 + self.stats['active_effects'] -= 1 + + # 从插件中移除效果 + if self.plugin and effect_id in self.plugin.effects: + del self.plugin.effects[effect_id] + + print(f"✓ 混响效果已删除: {effect_id}") + return True + + except Exception as e: + print(f"✗ 删除混响效果失败: {e}") + return False + + def process(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: + """ + 处理混响效果 + + Args: + audio_data: 输入音频数据 + parameters: 效果参数 + + Returns: + 处理后的音频数据 + """ + try: + import time + process_start = time.time() + + # 如果输入是NumPy数组 + if isinstance(audio_data, np.ndarray): + processed_data = self._process_numpy_array(audio_data, parameters) + else: + # 对于其他类型的数据,返回原始数据 + processed_data = audio_data + + # 更新统计信息 + self.stats['effects_processed'] += 1 + self.stats['processing_time'] += (time.time() - process_start) + + return processed_data + + except Exception as e: + print(f"✗ 混响效果处理失败: {e}") + return audio_data + + def _process_numpy_array(self, audio_data: np.ndarray, parameters: Dict[str, Any]) -> np.ndarray: + """ + 处理NumPy数组音频数据 + + Args: + audio_data: 输入音频数据 + parameters: 效果参数 + + Returns: + 处理后的音频数据 + """ + # 获取参数 + room_size = parameters.get('room_size', 0.5) + damping = parameters.get('damping', 0.5) + wet_level = parameters.get('wet_level', 0.33) + dry_level = parameters.get('dry_level', 0.4) + width = parameters.get('width', 1.0) + freeze_mode = parameters.get('freeze_mode', 0.0) + early_reflections = parameters.get('early_reflections', 0.5) + late_reverb = parameters.get('late_reverb', 0.5) + diffusion = parameters.get('diffusion', 0.7) + density = parameters.get('density', 0.5) + predelay = parameters.get('predelay', 0.0) + high_freq_damping = parameters.get('high_freq_damping', 0.5) + low_freq_damping = parameters.get('low_freq_damping', 0.3) + stereo_spread = parameters.get('stereo_spread', 0.5) + reverb_time = parameters.get('reverb_time', 2.0) + tail_density = parameters.get('tail_density', 0.8) + modulation_rate = parameters.get('modulation_rate', 0.1) + modulation_depth = parameters.get('modulation_depth', 0.02) + + # 如果是冻结模式,增加反馈 + if freeze_mode > 0.5: + room_size = min(1.0, room_size + 0.2) + + # 创建输出数组 + if audio_data.ndim == 1: + # 单声道输入,生成立体声输出 + output_left = np.zeros_like(audio_data) + output_right = np.zeros_like(audio_data) + + # 处理每个样本 + for i in range(len(audio_data)): + input_sample = audio_data[i] + + # 预延迟处理 + delayed_sample = self._apply_predelay(input_sample, predelay) + + # 混响处理 + reverb_left, reverb_right = self._apply_reverb( + delayed_sample, room_size, damping, diffusion, density, + stereo_spread, modulation_rate, modulation_depth) + + # 应用早期反射和晚期混响权重 + reverb_left *= (early_reflections * 0.3 + late_reverb * 0.7) + reverb_right *= (early_reflections * 0.3 + late_reverb * 0.7) + + # 混合干湿信号 + output_left[i] = input_sample * dry_level + reverb_left * wet_level + output_right[i] = input_sample * dry_level + reverb_right * wet_level + + # 合并为立体声数组 + output_data = np.array([output_left, output_right]) + + else: + # 多声道处理(假设已经是立体声) + if audio_data.shape[0] >= 2: + output_left = np.zeros_like(audio_data[0]) + output_right = np.zeros_like(audio_data[1]) + + for i in range(audio_data.shape[1]): + # 将左右声道混合为单声道输入 + mono_input = (audio_data[0, i] + audio_data[1, i]) * 0.5 + + # 预延迟处理 + delayed_sample = self._apply_predelay(mono_input, predelay) + + # 混响处理 + reverb_left, reverb_right = self._apply_reverb( + delayed_sample, room_size, damping, diffusion, density, + stereo_spread, modulation_rate, modulation_depth) + + # 应用早期反射和晚期混响权重 + reverb_left *= (early_reflections * 0.3 + late_reverb * 0.7) + reverb_right *= (early_reflections * 0.3 + late_reverb * 0.7) + + # 混合干湿信号 + output_left[i] = audio_data[0, i] * dry_level + reverb_left * wet_level + output_right[i] = audio_data[1, i] * dry_level + reverb_right * wet_level + + output_data = np.array([output_left, output_right]) + else: + # 其他多声道情况,返回原始数据 + output_data = audio_data + + # 确保输出在有效范围内 + processed_data = np.clip(output_data, -1.0, 1.0) + return processed_data + + def _apply_predelay(self, sample: float, predelay: float) -> float: + """ + 应用预延迟 + + Args: + sample: 输入样本 + predelay: 预延迟时间 (秒) + + Returns: + 延迟后的样本 + """ + # 简化的预延迟实现 + # 实际应用中会使用延迟线 + return sample # 简化实现 + + def _apply_reverb(self, sample: float, room_size: float, damping: float, + diffusion: float, density: float, stereo_spread: float, + modulation_rate: float, modulation_depth: float) -> tuple: + """ + 应用混响处理 + + Args: + sample: 输入样本 + room_size: 房间大小 + damping: 阻尼 + diffusion: 扩散 + density: 密度 + stereo_spread: 立体声扩散 + modulation_rate: 调制速率 + modulation_depth: 调制深度 + + Returns: + (左声道混响, 右声道混响) + """ + # 简化的混响实现 + # 实际应用中会使用梳状滤波器和全通滤波器链 + + # 生成基于参数的混响效果 + reverb_amount = 0.2 + room_size * 0.5 + damp_factor = 0.2 + damping * 0.6 + diffuse_factor = 0.5 + diffusion * 0.3 + + # 左声道混响 + left_reverb = sample * reverb_amount * (1.0 - damp_factor) + left_reverb *= (0.7 + diffuse_factor * 0.3) + + # 右声道混响(添加立体声扩散) + stereo_offset = stereo_spread * 0.3 + right_reverb = sample * (reverb_amount + stereo_offset) * (1.0 - damp_factor * 0.8) + right_reverb *= (0.7 + diffuse_factor * 0.3) + + return left_reverb, right_reverb + + def set_parameter(self, effect_id: str, parameter: str, value: Any) -> bool: + """ + 设置效果参数 + + Args: + effect_id: 效果ID + parameter: 参数名 + value: 参数值 + + Returns: + 是否设置成功 + """ + if effect_id not in self.effects: + print(f"✗ 混响效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['parameters'][parameter] = value + + # 如果是影响滤波器的参数,重新初始化滤波器 + filter_params = ['room_size', 'damping', 'diffusion', 'density'] + if parameter in filter_params: + self._initialize_filters(self.effects[effect_id]) + + # 如果是插件中的效果,也更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['parameters'][parameter] = value + if parameter in filter_params: + self.plugin.effects[effect_id]['comb_filters'] = \ + self.effects[effect_id]['comb_filters'].copy() + self.plugin.effects[effect_id]['allpass_filters'] = \ + self.effects[effect_id]['allpass_filters'].copy() + + print(f"✓ 混响效果参数已设置: {effect_id}.{parameter} = {value}") + return True + + except Exception as e: + print(f"✗ 设置混响效果参数失败: {e}") + return False + + def get_parameters(self, effect_id: str) -> Dict[str, Any]: + """ + 获取效果参数 + + Args: + effect_id: 效果ID + + Returns: + 参数字典 + """ + if effect_id not in self.effects: + return {} + + return self.effects[effect_id]['parameters'].copy() + + def enable_effect(self, effect_id: str) -> bool: + """ + 启用混响效果 + + Args: + effect_id: 效果ID + + Returns: + 是否启用成功 + """ + if effect_id not in self.effects: + print(f"✗ 混响效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['active'] = True + self.stats['active_effects'] += 1 + + # 如果是插件中的效果,也更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['active'] = True + + print(f"✓ 混响效果已启用: {effect_id}") + return True + + except Exception as e: + print(f"✗ 启用混响效果失败: {e}") + return False + + def disable_effect(self, effect_id: str) -> bool: + """ + 禁用混响效果 + + Args: + effect_id: 效果ID + + Returns: + 是否禁用成功 + """ + if effect_id not in self.effects: + print(f"✗ 混响效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['active'] = False + self.stats['active_effects'] -= 1 + + # 如果是插件中的效果,也更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['active'] = False + + print(f"✓ 混响效果已禁用: {effect_id}") + return True + + except Exception as e: + print(f"✗ 禁用混响效果失败: {e}") + return False + + def cleanup(self): + """清理所有资源""" + self.effects.clear() + self.comb_filters.clear() + self.allpass_filters.clear() + self.stats = { + 'total_effects': 0, + 'active_effects': 0, + 'effects_processed': 0, + 'processing_time': 0.0, + 'memory_usage': 0 + } + print("✓ 混响效果处理器资源已清理") + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def _get_current_time(self) -> float: + """ + 获取当前时间(秒) + + Returns: + 当前时间 + """ + if self.plugin and self.plugin.plugin_manager and self.plugin.plugin_manager.world: + return self.plugin.plugin_manager.world.globalClock.getFrameTime() + import time + return time.time() + + def update(self, dt: float): + """ + 更新处理器状态 + + Args: + dt: 时间增量 + """ + # 更新调制相位 + for effect_id, effect in self.effects.items(): + modulation_rate = effect['parameters'].get('modulation_rate', 0.1) + effect['modulation_phase'] += 2.0 * math.pi * modulation_rate * dt + if effect['modulation_phase'] >= 2.0 * math.pi: + effect['modulation_phase'] -= 2.0 * math.pi + + def set_sample_rate(self, sample_rate: int): + """ + 设置采样率 + + Args: + sample_rate: 采样率 (Hz) + """ + old_sample_rate = self.sample_rate + self.sample_rate = sample_rate + print(f"✓ 混响处理器采样率已从 {old_sample_rate} 更新为: {sample_rate} Hz") + + # 重新初始化所有滤波器以适应新的采样率 + for effect_id, effect in self.effects.items(): + self._initialize_filters(effect) + self._initialize_buffers(effect) + + def set_room_size(self, effect_id: str, room_size: float) -> bool: + """ + 设置房间大小 + + Args: + effect_id: 效果ID + room_size: 房间大小 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'room_size', max(0.0, min(1.0, room_size))) + + def set_damping(self, effect_id: str, damping: float) -> bool: + """ + 设置阻尼 + + Args: + effect_id: 效果ID + damping: 阻尼 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'damping', max(0.0, min(1.0, damping))) + + def set_wet_level(self, effect_id: str, wet_level: float) -> bool: + """ + 设置湿信号电平 + + Args: + effect_id: 效果ID + wet_level: 湿信号电平 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'wet_level', max(0.0, min(1.0, wet_level))) + + def set_dry_level(self, effect_id: str, dry_level: float) -> bool: + """ + 设置干信号电平 + + Args: + effect_id: 效果ID + dry_level: 干信号电平 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'dry_level', max(0.0, min(1.0, dry_level))) + + def set_width(self, effect_id: str, width: float) -> bool: + """ + 设置宽度 + + Args: + effect_id: 效果ID + width: 宽度 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'width', max(0.0, min(1.0, width))) + + def set_freeze_mode(self, effect_id: str, freeze_mode: float) -> bool: + """ + 设置冻结模式 + + Args: + effect_id: 效果ID + freeze_mode: 冻结模式 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'freeze_mode', max(0.0, min(1.0, freeze_mode))) + + def set_early_reflections(self, effect_id: str, reflections: float) -> bool: + """ + 设置早期反射 + + Args: + effect_id: 效果ID + reflections: 早期反射 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'early_reflections', max(0.0, min(1.0, reflections))) + + def set_late_reverb(self, effect_id: str, reverb: float) -> bool: + """ + 设置晚期混响 + + Args: + effect_id: 效果ID + reverb: 晚期混响 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'late_reverb', max(0.0, min(1.0, reverb))) + + def set_diffusion(self, effect_id: str, diffusion: float) -> bool: + """ + 设置扩散参数 + + Args: + effect_id: 效果ID + diffusion: 扩散值 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'diffusion', max(0.0, min(1.0, diffusion))) + + def set_density(self, effect_id: str, density: float) -> bool: + """ + 设置密度参数 + + Args: + effect_id: 效果ID + density: 密度值 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'density', max(0.0, min(1.0, density))) + + def set_predelay(self, effect_id: str, predelay: float) -> bool: + """ + 设置预延迟 + + Args: + effect_id: 效果ID + predelay: 预延迟 (秒) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'predelay', max(0.0, min(0.5, predelay))) + + def set_high_freq_damping(self, effect_id: str, damping: float) -> bool: + """ + 设置高频阻尼 + + Args: + effect_id: 效果ID + damping: 高频阻尼 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'high_freq_damping', max(0.0, min(1.0, damping))) + + def set_low_freq_damping(self, effect_id: str, damping: float) -> bool: + """ + 设置低频阻尼 + + Args: + effect_id: 效果ID + damping: 低频阻尼 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'low_freq_damping', max(0.0, min(1.0, damping))) + + def set_stereo_spread(self, effect_id: str, spread: float) -> bool: + """ + 设置立体声扩散 + + Args: + effect_id: 效果ID + spread: 立体声扩散 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'stereo_spread', max(0.0, min(1.0, spread))) + + def set_reverb_time(self, effect_id: str, time: float) -> bool: + """ + 设置混响时间 + + Args: + effect_id: 效果ID + time: 混响时间 (秒) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'reverb_time', max(0.1, min(30.0, time))) + + def set_tail_density(self, effect_id: str, density: float) -> bool: + """ + 设置尾部密度 + + Args: + effect_id: 效果ID + density: 尾部密度 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'tail_density', max(0.0, min(1.0, density))) + + def set_modulation_rate(self, effect_id: str, rate: float) -> bool: + """ + 设置调制速率 + + Args: + effect_id: 效果ID + rate: 调制速率 (Hz) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'modulation_rate', max(0.01, min(10.0, rate))) + + def set_modulation_depth(self, effect_id: str, depth: float) -> bool: + """ + 设置调制深度 + + Args: + effect_id: 效果ID + depth: 调制深度 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'modulation_depth', max(0.0, min(1.0, depth))) + + def get_reverb_time(self, effect_id: str) -> float: + """ + 获取混响时间表 + + Args: + effect_id: 效果ID + + Returns: + 当前混响时间 (秒) + """ + if effect_id not in self.effects: + return 0.0 + + return self.effects[effect_id]['parameters'].get('reverb_time', 2.0) + + def get_room_size_meter(self, effect_id: str) -> float: + """ + 获取房间大小表 + + Args: + effect_id: 效果ID + + Returns: + 当前房间大小 (0.0-1.0) + """ + if effect_id not in self.effects: + return 0.0 + + return self.effects[effect_id]['parameters'].get('room_size', 0.5) + + def create_preset(self, preset_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]: + """ + 创建混响预设 + + Args: + preset_name: 预设名称 + parameters: 参数 + + Returns: + 预设参数字典 + """ + presets = { + 'small_room': { + 'room_size': 0.2, + 'damping': 0.7, + 'wet_level': 0.2, + 'dry_level': 0.8, + 'width': 0.8, + 'early_reflections': 0.6, + 'late_reverb': 0.4, + 'diffusion': 0.6, + 'density': 0.3, + 'reverb_time': 1.0 + }, + 'medium_room': { + 'room_size': 0.5, + 'damping': 0.5, + 'wet_level': 0.3, + 'dry_level': 0.7, + 'width': 0.9, + 'early_reflections': 0.7, + 'late_reverb': 0.5, + 'diffusion': 0.7, + 'density': 0.5, + 'reverb_time': 1.5 + }, + 'large_room': { + 'room_size': 0.7, + 'damping': 0.4, + 'wet_level': 0.4, + 'dry_level': 0.6, + 'width': 1.0, + 'early_reflections': 0.8, + 'late_reverb': 0.6, + 'diffusion': 0.8, + 'density': 0.7, + 'reverb_time': 2.0 + }, + 'large_hall': { + 'room_size': 0.9, + 'damping': 0.3, + 'wet_level': 0.5, + 'dry_level': 0.5, + 'width': 1.0, + 'early_reflections': 0.9, + 'late_reverb': 0.7, + 'diffusion': 0.9, + 'density': 0.8, + 'reverb_time': 3.0 + }, + 'cathedral': { + 'room_size': 1.0, + 'damping': 0.2, + 'wet_level': 0.7, + 'dry_level': 0.3, + 'width': 1.0, + 'early_reflections': 1.0, + 'late_reverb': 0.8, + 'diffusion': 1.0, + 'density': 1.0, + 'reverb_time': 5.0 + }, + 'bathroom': { + 'room_size': 0.2, + 'damping': 0.1, + 'wet_level': 0.8, + 'dry_level': 0.2, + 'width': 0.9, + 'early_reflections': 0.5, + 'late_reverb': 0.9, + 'diffusion': 0.5, + 'density': 0.2, + 'reverb_time': 0.8 + }, + 'tunnel': { + 'room_size': 0.8, + 'damping': 0.6, + 'wet_level': 0.6, + 'dry_level': 0.4, + 'width': 0.7, + 'early_reflections': 0.7, + 'late_reverb': 0.7, + 'diffusion': 0.6, + 'density': 0.6, + 'reverb_time': 2.5 + }, + 'arena': { + 'room_size': 0.95, + 'damping': 0.25, + 'wet_level': 0.6, + 'dry_level': 0.4, + 'width': 1.0, + 'early_reflections': 0.95, + 'late_reverb': 0.75, + 'diffusion': 0.95, + 'density': 0.9, + 'reverb_time': 4.0 + } + } + + if preset_name in presets: + preset_params = presets[preset_name].copy() + preset_params.update(parameters) + return preset_params + else: + return parameters.copy() + + def get_frequency_response(self, effect_id: str) -> Dict[str, Any]: + """ + 获取频率响应 + + Args: + effect_id: 效果ID + + Returns: + 频率响应数据 + """ + if effect_id not in self.effects: + return {} + + parameters = self.effects[effect_id]['parameters'] + + # 生成简化的频率响应数据 + frequencies = np.logspace(np.log10(20), np.log10(20000), 100) + response = [] + + room_size = parameters.get('room_size', 0.5) + damping = parameters.get('damping', 0.5) + high_freq_damping = parameters.get('high_freq_damping', 0.5) + low_freq_damping = parameters.get('low_freq_damping', 0.3) + + for freq in frequencies: + # 计算基于参数的频率响应 + if freq < 200: # 低频 + gain = 1.0 - low_freq_damping * 0.3 + elif freq > 5000: # 高频 + gain = 1.0 - high_freq_damping * 0.5 + else: # 中频 + gain = 1.0 - damping * 0.2 + + # 房间大小影响 + gain *= (0.8 + room_size * 0.4) + + response.append(gain) + + return { + 'frequencies': frequencies.tolist(), + 'response': response + } + diff --git a/plugins/user/audio_reverb_effects/effects/spatial_processor.py b/plugins/user/audio_reverb_effects/effects/spatial_processor.py index 61cdcb30..e4d7ff91 100644 --- a/plugins/user/audio_reverb_effects/effects/spatial_processor.py +++ b/plugins/user/audio_reverb_effects/effects/spatial_processor.py @@ -1,980 +1,980 @@ -""" -空间音频效果处理器 -实现专业的3D空间音频效果处理 -""" - -import uuid -import math -import numpy as np -from typing import Dict, List, Any, Optional, Tuple -from panda3d.core import Vec3 - -class SpatialProcessor: - """ - 空间音频效果处理器 - 实现专业的3D空间音频效果处理 - """ - - def __init__(self, plugin): - """ - 初始化空间音频效果处理器 - - Args: - plugin: 音频效果插件实例 - """ - self.plugin = plugin - self.effects: Dict[str, Dict[str, Any]] = {} - self.stats = { - 'total_effects': 0, - 'active_effects': 0, - 'effects_processed': 0, - 'processing_time': 0.0, - 'memory_usage': 0 - } - self.buffer_size = plugin.buffer_size if plugin else 4096 - self.sample_rate = 44100 # 默认采样率 - self.speed_of_sound = 343.0 # 声速 (米/秒) - - def create_effect(self, parameters: Dict[str, Any]) -> str: - """ - 创建空间音频效果 - - Args: - parameters: 空间音频效果参数 - - Returns: - 效果ID - """ - try: - # 生成唯一ID - effect_id = str(uuid.uuid4()) - - # 默认参数 - default_params = { - 'source_position': (0.0, 0.0, 0.0), # 音源位置 (x, y, z) - 'listener_position': (0.0, 0.0, 0.0), # 听者位置 (x, y, z) - 'listener_orientation': (0.0, 0.0, 0.0), # 听者方向 (heading, pitch, roll) - 'min_distance': 1.0, # 最小距离 - 'max_distance': 100.0, # 最大距离 - 'rolloff_factor': 1.0, # 衰减因子 - 'cone_inner_angle': 360.0, # 内锥角 (度) - 'cone_outer_angle': 360.0, # 外锥角 (度) - 'cone_outer_gain': 1.0, # 外锥增益 - 'doppler_factor': 1.0, # 多普勒因子 - 'spread': 0.0, # 扩散角度 (度) - 'air_absorption': 0.0, # 空气吸收 - 'room_rolloff': 0.0, # 房间衰减 - 'occlusion': 0.0, # 遮挡 (0.0-1.0) - 'obstruction': 0.0, # 阻挡 (0.0-1.0) - 'hrtf_enabled': False, # HRTF启用 - 'reverb_send': 0.0 # 混响发送 (0.0-1.0) - } - - # 合并参数 - effect_params = default_params.copy() - effect_params.update(parameters) - - # 创建效果对象 - effect = { - 'id': effect_id, - 'type': 'spatial', - 'parameters': effect_params, - 'active': True, - 'buffers': {}, # 缓冲区 - 'hrtf_filters': {}, # HRTF滤波器 - 'distance': 0.0, # 当前距离 - 'azimuth': 0.0, # 方位角 - 'elevation': 0.0, # 仰角 - 'created_time': self._get_current_time(), - 'process_count': 0 - } - - # 初始化缓冲区 - self._initialize_buffers(effect) - - # 初始化HRTF滤波器 - if effect_params.get('hrtf_enabled', False): - self._initialize_hrtf_filters(effect) - - # 存储效果 - self.effects[effect_id] = effect - - # 更新统计信息 - self.stats['total_effects'] += 1 - self.stats['active_effects'] += 1 - - # 在插件中注册效果 - if self.plugin: - self.plugin.effects[effect_id] = effect.copy() - - print(f"✓ 空间音频效果创建成功: {effect_id}") - return effect_id - - except Exception as e: - print(f"✗ 创建空间音频效果失败: {e}") - import traceback - traceback.print_exc() - return "" - - def _initialize_buffers(self, effect: Dict[str, Any]): - """ - 初始化缓冲区 - - Args: - effect: 效果对象 - """ - effect_id = effect['id'] - buffers = {} - - # 输入和输出缓冲区 - buffers['input'] = np.zeros(self.buffer_size) - buffers['output_left'] = np.zeros(self.buffer_size) - buffers['output_right'] = np.zeros(self.buffer_size) - - # 延迟缓冲区(用于多普勒效应) - buffers['delay_line'] = np.zeros(int(0.1 * self.sample_rate)) # 100ms延迟线 - buffers['delay_index'] = 0 - - effect['buffers'] = buffers - - def _initialize_hrtf_filters(self, effect: Dict[str, Any]): - """ - 初始化HRTF滤波器 - - Args: - effect: 效果对象 - """ - # 这里应该加载真实的HRTF数据 - # 简化实现使用生成的滤波器 - - hrtf_filters = {} - - # 为不同的角度生成简化的HRTF滤波器 - for azimuth in range(-180, 181, 15): # 每15度一个滤波器 - for elevation in range(-90, 91, 15): - # 生成简化的HRTF滤波器系数 - left_filter = self._generate_simple_hrtf_filter(azimuth, elevation, 'left') - right_filter = self._generate_simple_hrtf_filter(azimuth, elevation, 'right') - - hrtf_filters[(azimuth, elevation)] = { - 'left': left_filter, - 'right': right_filter - } - - effect['hrtf_filters'] = hrtf_filters - - def _generate_simple_hrtf_filter(self, azimuth: int, elevation: int, channel: str) -> np.ndarray: - """ - 生成简化的HRTF滤波器 - - Args: - azimuth: 方位角 (度) - elevation: 仰角 (度) - channel: 声道 ('left' 或 'right') - - Returns: - 滤波器系数数组 - """ - # 简化的HRTF滤波器生成 - # 实际应用中会使用真实的HRTF测量数据 - - # 滤波器长度 - filter_length = 64 - - # 生成基于角度的简化的滤波器 - filter_coeffs = np.zeros(filter_length) - - # 基于角度计算延迟和幅度 - if channel == 'left': - # 左耳滤波器 - delay = max(0, int((1.0 - math.cos(math.radians(azimuth))) * 10)) - amplitude = 0.5 + 0.5 * math.cos(math.radians(azimuth)) - else: - # 右耳滤波器 - delay = max(0, int((1.0 - math.cos(math.radians(-azimuth))) * 10)) - amplitude = 0.5 + 0.5 * math.cos(math.radians(-azimuth)) - - # 应用延迟和幅度 - if delay < filter_length: - filter_coeffs[delay] = amplitude - - return filter_coeffs - - def delete_effect(self, effect_id: str) -> bool: - """ - 删除空间音频效果 - - Args: - effect_id: 效果ID - - Returns: - 是否删除成功 - """ - if effect_id not in self.effects: - print(f"✗ 空间音频效果不存在: {effect_id}") - return False - - try: - # 删除效果 - del self.effects[effect_id] - - # 更新统计信息 - self.stats['active_effects'] -= 1 - - # 从插件中移除效果 - if self.plugin and effect_id in self.plugin.effects: - del self.plugin.effects[effect_id] - - print(f"✓ 空间音频效果已删除: {effect_id}") - return True - - except Exception as e: - print(f"✗ 删除空间音频效果失败: {e}") - return False - - def process(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: - """ - 处理空间音频效果 - - Args: - audio_data: 输入音频数据 - parameters: 效果参数 - - Returns: - 处理后的音频数据 - """ - try: - import time - process_start = time.time() - - # 如果输入是NumPy数组 - if isinstance(audio_data, np.ndarray): - processed_data = self._process_numpy_array(audio_data, parameters) - else: - # 对于其他类型的数据,返回原始数据 - processed_data = audio_data - - # 更新统计信息 - self.stats['effects_processed'] += 1 - self.stats['processing_time'] += (time.time() - process_start) - - return processed_data - - except Exception as e: - print(f"✗ 空间音频效果处理失败: {e}") - return audio_data - - def _process_numpy_array(self, audio_data: np.ndarray, parameters: Dict[str, Any]) -> np.ndarray: - """ - 处理NumPy数组音频数据 - - Args: - audio_data: 输入音频数据 - parameters: 效果参数 - - Returns: - 处理后的音频数据 - """ - # 获取参数 - source_pos = Vec3(*parameters.get('source_position', (0.0, 0.0, 0.0))) - listener_pos = Vec3(*parameters.get('listener_position', (0.0, 0.0, 0.0))) - listener_orient = parameters.get('listener_orientation', (0.0, 0.0, 0.0)) - min_distance = max(0.1, parameters.get('min_distance', 1.0)) - max_distance = max(min_distance, parameters.get('max_distance', 100.0)) - rolloff_factor = max(0.0, parameters.get('rolloff_factor', 1.0)) - cone_inner_angle = max(0.0, min(360.0, parameters.get('cone_inner_angle', 360.0))) - cone_outer_angle = max(cone_inner_angle, min(360.0, parameters.get('cone_outer_angle', 360.0))) - cone_outer_gain = max(0.0, min(1.0, parameters.get('cone_outer_gain', 1.0))) - doppler_factor = max(0.0, parameters.get('doppler_factor', 1.0)) - spread = max(0.0, parameters.get('spread', 0.0)) - air_absorption = max(0.0, parameters.get('air_absorption', 0.0)) - occlusion = max(0.0, min(1.0, parameters.get('occlusion', 0.0))) - obstruction = max(0.0, min(1.0, parameters.get('obstruction', 0.0))) - hrtf_enabled = parameters.get('hrtf_enabled', False) - reverb_send = max(0.0, min(1.0, parameters.get('reverb_send', 0.0))) - - # 计算距离 - distance = (source_pos - listener_pos).length() - - # 计算方位角和仰角 - azimuth, elevation = self._calculate_angles(source_pos, listener_pos, listener_orient) - - # 计算音量衰减 - volume = self._calculate_volume_attenuation( - distance, min_distance, max_distance, rolloff_factor) - - # 计算锥形效应 - cone_gain = self._calculate_cone_gain( - source_pos, listener_pos, listener_orient, - cone_inner_angle, cone_outer_angle, cone_outer_gain) - - # 计算遮挡和阻挡 - occlusion_gain = 1.0 - occlusion * 0.8 # 最多减少80%音量 - obstruction_gain = 1.0 - obstruction * 0.5 # 最多减少50%音量 - - # 计算多普勒效应 - doppler_shift = self._calculate_doppler_shift( - source_pos, listener_pos, doppler_factor) - - # 计算空气吸收 - air_absorption_gain = 1.0 - (distance / max_distance) * air_absorption * 0.5 - - # 综合增益 - total_gain = (volume * cone_gain * occlusion_gain * - obstruction_gain * air_absorption_gain) - - # 创建输出数组 - if audio_data.ndim == 1: - # 单声道输入,生成立体声输出 - output_left = np.zeros_like(audio_data) - output_right = np.zeros_like(audio_data) - - for i in range(len(audio_data)): - sample = audio_data[i] - - # 应用增益 - processed_sample = sample * total_gain - - # 应用HRTF或简单立体声处理 - if hrtf_enabled: - # 简化的HRTF应用 - left_sample, right_sample = self._apply_hrtf( - processed_sample, azimuth, elevation) - else: - # 简单的立体声平移 - left_sample, right_sample = self._simple_stereo_pan( - processed_sample, azimuth, spread) - - output_left[i] = left_sample - output_right[i] = right_sample - - # 合并为立体声数组 - output_data = np.array([output_left, output_right]) - - else: - # 多声道处理 - if audio_data.shape[0] >= 2: - # 已经是立体声,应用空间效果 - output_left = np.zeros_like(audio_data[0]) - output_right = np.zeros_like(audio_data[1]) - - for i in range(audio_data.shape[1]): - left_sample = audio_data[0, i] * total_gain - right_sample = audio_data[1, i] * total_gain - - # 应用HRTF或简单立体声处理 - if hrtf_enabled: - # 简化的HRTF应用 - new_left, new_right = self._apply_hrtf( - (left_sample + right_sample) * 0.5, azimuth, elevation) - output_left[i] = new_left - output_right[i] = new_right - else: - # 简单的立体声平移 - new_left, new_right = self._simple_stereo_pan( - (left_sample + right_sample) * 0.5, azimuth, spread) - output_left[i] = new_left - output_right[i] = new_right - - output_data = np.array([output_left, output_right]) - else: - # 其他多声道情况,返回原始数据 - output_data = audio_data - - # 确保输出在有效范围内 - processed_data = np.clip(output_data, -1.0, 1.0) - return processed_data - - def _calculate_angles(self, source_pos: Vec3, listener_pos: Vec3, - listener_orient: Tuple[float, float, float]) -> Tuple[float, float]: - """ - 计算方位角和仰角 - - Args: - source_pos: 音源位置 - listener_pos: 听者位置 - listener_orient: 听者方向 - - Returns: - (方位角, 仰角) (度) - """ - # 计算相对位置 - relative_pos = source_pos - listener_pos - - # 计算方位角 (水平角度) - azimuth = math.degrees(math.atan2(relative_pos.x, -relative_pos.y)) - - # 计算仰角 (垂直角度) - distance_xy = math.sqrt(relative_pos.x**2 + relative_pos.y**2) - elevation = math.degrees(math.atan2(relative_pos.z, distance_xy)) - - return azimuth, elevation - - def _calculate_volume_attenuation(self, distance: float, min_distance: float, - max_distance: float, rolloff_factor: float) -> float: - """ - 计算音量衰减 - - Args: - distance: 距离 - min_distance: 最小距离 - max_distance: 最大距离 - rolloff_factor: 衰减因子 - - Returns: - 音量增益 (0.0-1.0) - """ - if distance <= min_distance: - return 1.0 - elif distance >= max_distance: - return 0.0 - else: - # 反距离衰减模型 - gain = min_distance / (min_distance + rolloff_factor * (distance - min_distance)) - return max(0.0, min(1.0, gain)) - - def _calculate_cone_gain(self, source_pos: Vec3, listener_pos: Vec3, - listener_orient: Tuple[float, float, float], - inner_angle: float, outer_angle: float, outer_gain: float) -> float: - """ - 计算锥形效应增益 - - Args: - source_pos: 音源位置 - listener_pos: 听者位置 - listener_orient: 听者方向 - inner_angle: 内锥角 (度) - outer_angle: 外锥角 (度) - outer_gain: 外锥增益 - - Returns: - 锥形增益 (0.0-1.0) - """ - # 简化的锥形效应计算 - # 实际应用中需要考虑音源的朝向 - - if inner_angle >= 360.0 and outer_angle >= 360.0: - return 1.0 - - # 计算听者相对于音源的角度 - relative_pos = listener_pos - source_pos - angle = math.degrees(math.acos(relative_pos.normalized().dot(Vec3(0, -1, 0)))) - - if angle <= inner_angle / 2.0: - return 1.0 - elif angle >= outer_angle / 2.0: - return outer_gain - else: - # 线性插值 - t = (angle - inner_angle / 2.0) / (outer_angle / 2.0 - inner_angle / 2.0) - return 1.0 - t * (1.0 - outer_gain) - - def _calculate_doppler_shift(self, source_pos: Vec3, listener_pos: Vec3, - doppler_factor: float) -> float: - """ - 计算多普勒频移 - - Args: - source_pos: 音源位置 - listener_pos: 听者位置 - doppler_factor: 多普勒因子 - - Returns: - 频移比率 - """ - # 简化的多普勒效应计算 - # 实际应用中需要考虑音源和听者的速度 - - if doppler_factor <= 0.0: - return 1.0 - - # 计算相对速度(简化为距离变化率) - relative_velocity = (source_pos - listener_pos).length() / self.sample_rate - ratio = (self.speed_of_sound - doppler_factor * relative_velocity) / self.speed_of_sound - return max(0.5, min(2.0, ratio)) - - def _apply_hrtf(self, sample: float, azimuth: float, elevation: float) -> Tuple[float, float]: - """ - 应用HRTF - - Args: - sample: 输入样本 - azimuth: 方位角 (度) - elevation: 仰角 (度) - - Returns: - (左声道样本, 右声道样本) - """ - # 简化的HRTF应用 - # 实际应用中会使用真实的HRTF滤波器 - - # 量化角度以匹配预生成的滤波器 - azimuth_quantized = int(round(azimuth / 15.0)) * 15 - elevation_quantized = int(round(elevation / 15.0)) * 15 - - # 限制角度范围 - azimuth_quantized = max(-180, min(180, azimuth_quantized)) - elevation_quantized = max(-90, min(90, elevation_quantized)) - - # 简化的HRTF效果 - left_gain = 0.5 + 0.5 * math.cos(math.radians(azimuth)) - right_gain = 0.5 + 0.5 * math.cos(math.radians(-azimuth)) - - # 应用仰角影响 - elevation_factor = math.cos(math.radians(elevation)) - left_gain *= elevation_factor - right_gain *= elevation_factor - - return sample * left_gain, sample * right_gain - - def _simple_stereo_pan(self, sample: float, azimuth: float, spread: float) -> Tuple[float, float]: - """ - 简单的立体声平移 - - Args: - sample: 输入样本 - azimuth: 方位角 (度) - spread: 扩散角度 - - Returns: - (左声道样本, 右声道样本) - """ - # 将角度转换为-1到1的平移值 - pan = math.sin(math.radians(azimuth)) - - # 应用扩散 - if spread > 0: - spread_factor = 1.0 - spread / 180.0 - pan *= spread_factor - - # 计算左右声道增益 - left_gain = 0.5 - 0.5 * pan - right_gain = 0.5 + 0.5 * pan - - return sample * left_gain, sample * right_gain - - def set_parameter(self, effect_id: str, parameter: str, value: Any) -> bool: - """ - 设置效果参数 - - Args: - effect_id: 效果ID - parameter: 参数名 - value: 参数值 - - Returns: - 是否设置成功 - """ - if effect_id not in self.effects: - print(f"✗ 空间音频效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['parameters'][parameter] = value - - # 如果HRTF启用状态改变,重新初始化滤波器 - if parameter == 'hrtf_enabled': - if value: - self._initialize_hrtf_filters(self.effects[effect_id]) - else: - self.effects[effect_id]['hrtf_filters'] = {} - - # 如果是插件中的效果,也更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['parameters'][parameter] = value - if parameter == 'hrtf_enabled': - self.plugin.effects[effect_id]['hrtf_filters'] = \ - self.effects[effect_id]['hrtf_filters'].copy() - - print(f"✓ 空间音频效果参数已设置: {effect_id}.{parameter} = {value}") - return True - - except Exception as e: - print(f"✗ 设置空间音频效果参数失败: {e}") - return False - - def get_parameters(self, effect_id: str) -> Dict[str, Any]: - """ - 获取效果参数 - - Args: - effect_id: 效果ID - - Returns: - 参数字典 - """ - if effect_id not in self.effects: - return {} - - return self.effects[effect_id]['parameters'].copy() - - def enable_effect(self, effect_id: str) -> bool: - """ - 启用空间音频效果 - - Args: - effect_id: 效果ID - - Returns: - 是否启用成功 - """ - if effect_id not in self.effects: - print(f"✗ 空间音频效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['active'] = True - self.stats['active_effects'] += 1 - - # 如果是插件中的效果,也更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['active'] = True - - print(f"✓ 空间音频效果已启用: {effect_id}") - return True - - except Exception as e: - print(f"✗ 启用空间音频效果失败: {e}") - return False - - def disable_effect(self, effect_id: str) -> bool: - """ - 禁用空间音频效果 - - Args: - effect_id: 效果ID - - Returns: - 是否禁用成功 - """ - if effect_id not in self.effects: - print(f"✗ 空间音频效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['active'] = False - self.stats['active_effects'] -= 1 - - # 如果是插件中的效果,也更新插件中的副本 - if self.plugin and effect_id in self.plugin.effects: - self.plugin.effects[effect_id]['active'] = False - - print(f"✓ 空间音频效果已禁用: {effect_id}") - return True - - except Exception as e: - print(f"✗ 禁用空间音频效果失败: {e}") - return False - - def cleanup(self): - """清理所有资源""" - self.effects.clear() - self.stats = { - 'total_effects': 0, - 'active_effects': 0, - 'effects_processed': 0, - 'processing_time': 0.0, - 'memory_usage': 0 - } - print("✓ 空间音频效果处理器资源已清理") - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def _get_current_time(self) -> float: - """ - 获取当前时间(秒) - - Returns: - 当前时间 - """ - if self.plugin and self.plugin.plugin_manager and self.plugin.plugin_manager.world: - return self.plugin.plugin_manager.world.globalClock.getFrameTime() - import time - return time.time() - - def update(self, dt: float): - """ - 更新处理器状态 - - Args: - dt: 时间增量 - """ - # 这里可以更新任何需要定期更新的状态 - pass - - def set_sample_rate(self, sample_rate: int): - """ - 设置采样率 - - Args: - sample_rate: 采样率 (Hz) - """ - self.sample_rate = sample_rate - print(f"✓ 空间音频效果处理器采样率已设置: {sample_rate} Hz") - - def set_source_position(self, effect_id: str, position: Tuple[float, float, float]) -> bool: - """ - 设置音源位置 - - Args: - effect_id: 效果ID - position: 位置 (x, y, z) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'source_position', position) - - def set_listener_position(self, effect_id: str, position: Tuple[float, float, float]) -> bool: - """ - 设置听者位置 - - Args: - effect_id: 效果ID - position: 位置 (x, y, z) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'listener_position', position) - - def set_listener_orientation(self, effect_id: str, orientation: Tuple[float, float, float]) -> bool: - """ - 设置听者方向 - - Args: - effect_id: 效果ID - orientation: 方向 (heading, pitch, roll) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'listener_orientation', orientation) - - def set_distance_model(self, effect_id: str, min_distance: float, - max_distance: float, rolloff_factor: float) -> bool: - """ - 设置距离模型参数 - - Args: - effect_id: 效果ID - min_distance: 最小距离 - max_distance: 最大距离 - rolloff_factor: 衰减因子 - - Returns: - 是否设置成功 - """ - success = True - success &= self.set_parameter(effect_id, 'min_distance', max(0.1, min_distance)) - success &= self.set_parameter(effect_id, 'max_distance', max(min_distance, max_distance)) - success &= self.set_parameter(effect_id, 'rolloff_factor', max(0.0, rolloff_factor)) - return success - - def set_cone_effect(self, effect_id: str, inner_angle: float, - outer_angle: float, outer_gain: float) -> bool: - """ - 设置锥形效应参数 - - Args: - effect_id: 效果ID - inner_angle: 内锥角 (度) - outer_angle: 外锥角 (度) - outer_gain: 外锥增益 - - Returns: - 是否设置成功 - """ - success = True - success &= self.set_parameter(effect_id, 'cone_inner_angle', max(0.0, min(360.0, inner_angle))) - success &= self.set_parameter(effect_id, 'cone_outer_angle', max(inner_angle, min(360.0, outer_angle))) - success &= self.set_parameter(effect_id, 'cone_outer_gain', max(0.0, min(1.0, outer_gain))) - return success - - def set_doppler_factor(self, effect_id: str, factor: float) -> bool: - """ - 设置多普勒因子 - - Args: - effect_id: 效果ID - factor: 多普勒因子 - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'doppler_factor', max(0.0, factor)) - - def set_spread(self, effect_id: str, spread: float) -> bool: - """ - 设置扩散角度 - - Args: - effect_id: 效果ID - spread: 扩散角度 (度) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'spread', max(0.0, spread)) - - def set_air_absorption(self, effect_id: str, absorption: float) -> bool: - """ - 设置空气吸收 - - Args: - effect_id: 效果ID - absorption: 空气吸收系数 - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'air_absorption', max(0.0, min(1.0, absorption))) - - def set_occlusion(self, effect_id: str, occlusion: float) -> bool: - """ - 设置遮挡系数 - - Args: - effect_id: 效果ID - occlusion: 遮挡系数 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'occlusion', max(0.0, min(1.0, occlusion))) - - def set_obstruction(self, effect_id: str, obstruction: float) -> bool: - """ - 设置阻挡系数 - - Args: - effect_id: 效果ID - obstruction: 阻挡系数 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'obstruction', max(0.0, min(1.0, obstruction))) - - def enable_hrtf(self, effect_id: str, enable: bool) -> bool: - """ - 启用/禁用HRTF - - Args: - effect_id: 效果ID - enable: 是否启用 - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'hrtf_enabled', enable) - - def set_reverb_send(self, effect_id: str, send: float) -> bool: - """ - 设置混响发送量 - - Args: - effect_id: 效果ID - send: 混响发送量 (0.0-1.0) - - Returns: - 是否设置成功 - """ - return self.set_parameter(effect_id, 'reverb_send', max(0.0, min(1.0, send))) - - def get_distance(self, effect_id: str) -> float: - """ - 获取当前距离 - - Args: - effect_id: 效果ID - - Returns: - 距离值 - """ - if effect_id not in self.effects: - return 0.0 - - return self.effects[effect_id].get('distance', 0.0) - - def get_angles(self, effect_id: str) -> Tuple[float, float]: - """ - 获取当前角度 - - Args: - effect_id: 效果ID - - Returns: - (方位角, 仰角) (度) - """ - if effect_id not in self.effects: - return (0.0, 0.0) - - effect = self.effects[effect_id] - return (effect.get('azimuth', 0.0), effect.get('elevation', 0.0)) - - def create_preset(self, preset_name: str) -> Dict[str, Any]: - """ - 创建空间音频效果预设 - - Args: - preset_name: 预设名称 - - Returns: - 预设参数字典 - """ - presets = { - 'close': { - 'min_distance': 0.5, - 'max_distance': 20.0, - 'rolloff_factor': 1.0, - 'doppler_factor': 1.0, - 'air_absorption': 0.0 - }, - 'medium': { - 'min_distance': 1.0, - 'max_distance': 50.0, - 'rolloff_factor': 0.8, - 'doppler_factor': 0.8, - 'air_absorption': 0.1 - }, - 'far': { - 'min_distance': 5.0, - 'max_distance': 100.0, - 'rolloff_factor': 0.5, - 'doppler_factor': 0.5, - 'air_absorption': 0.3 - }, - 'outdoor': { - 'min_distance': 2.0, - 'max_distance': 200.0, - 'rolloff_factor': 0.3, - 'doppler_factor': 1.0, - 'air_absorption': 0.5 - }, - 'indoor': { - 'min_distance': 0.5, - 'max_distance': 30.0, - 'rolloff_factor': 1.2, - 'doppler_factor': 0.7, - 'air_absorption': 0.0 - }, - 'underwater': { - 'min_distance': 1.0, - 'max_distance': 40.0, - 'rolloff_factor': 0.2, - 'doppler_factor': 0.3, - 'air_absorption': 0.8, - 'speed_of_sound': 1500.0 # 水中声速 - } - } - - return presets.get(preset_name, {}).copy() - +""" +空间音频效果处理器 +实现专业的3D空间音频效果处理 +""" + +import uuid +import math +import numpy as np +from typing import Dict, List, Any, Optional, Tuple +from panda3d.core import Vec3 + +class SpatialProcessor: + """ + 空间音频效果处理器 + 实现专业的3D空间音频效果处理 + """ + + def __init__(self, plugin): + """ + 初始化空间音频效果处理器 + + Args: + plugin: 音频效果插件实例 + """ + self.plugin = plugin + self.effects: Dict[str, Dict[str, Any]] = {} + self.stats = { + 'total_effects': 0, + 'active_effects': 0, + 'effects_processed': 0, + 'processing_time': 0.0, + 'memory_usage': 0 + } + self.buffer_size = plugin.buffer_size if plugin else 4096 + self.sample_rate = 44100 # 默认采样率 + self.speed_of_sound = 343.0 # 声速 (米/秒) + + def create_effect(self, parameters: Dict[str, Any]) -> str: + """ + 创建空间音频效果 + + Args: + parameters: 空间音频效果参数 + + Returns: + 效果ID + """ + try: + # 生成唯一ID + effect_id = str(uuid.uuid4()) + + # 默认参数 + default_params = { + 'source_position': (0.0, 0.0, 0.0), # 音源位置 (x, y, z) + 'listener_position': (0.0, 0.0, 0.0), # 听者位置 (x, y, z) + 'listener_orientation': (0.0, 0.0, 0.0), # 听者方向 (heading, pitch, roll) + 'min_distance': 1.0, # 最小距离 + 'max_distance': 100.0, # 最大距离 + 'rolloff_factor': 1.0, # 衰减因子 + 'cone_inner_angle': 360.0, # 内锥角 (度) + 'cone_outer_angle': 360.0, # 外锥角 (度) + 'cone_outer_gain': 1.0, # 外锥增益 + 'doppler_factor': 1.0, # 多普勒因子 + 'spread': 0.0, # 扩散角度 (度) + 'air_absorption': 0.0, # 空气吸收 + 'room_rolloff': 0.0, # 房间衰减 + 'occlusion': 0.0, # 遮挡 (0.0-1.0) + 'obstruction': 0.0, # 阻挡 (0.0-1.0) + 'hrtf_enabled': False, # HRTF启用 + 'reverb_send': 0.0 # 混响发送 (0.0-1.0) + } + + # 合并参数 + effect_params = default_params.copy() + effect_params.update(parameters) + + # 创建效果对象 + effect = { + 'id': effect_id, + 'type': 'spatial', + 'parameters': effect_params, + 'active': True, + 'buffers': {}, # 缓冲区 + 'hrtf_filters': {}, # HRTF滤波器 + 'distance': 0.0, # 当前距离 + 'azimuth': 0.0, # 方位角 + 'elevation': 0.0, # 仰角 + 'created_time': self._get_current_time(), + 'process_count': 0 + } + + # 初始化缓冲区 + self._initialize_buffers(effect) + + # 初始化HRTF滤波器 + if effect_params.get('hrtf_enabled', False): + self._initialize_hrtf_filters(effect) + + # 存储效果 + self.effects[effect_id] = effect + + # 更新统计信息 + self.stats['total_effects'] += 1 + self.stats['active_effects'] += 1 + + # 在插件中注册效果 + if self.plugin: + self.plugin.effects[effect_id] = effect.copy() + + print(f"✓ 空间音频效果创建成功: {effect_id}") + return effect_id + + except Exception as e: + print(f"✗ 创建空间音频效果失败: {e}") + import traceback + traceback.print_exc() + return "" + + def _initialize_buffers(self, effect: Dict[str, Any]): + """ + 初始化缓冲区 + + Args: + effect: 效果对象 + """ + effect_id = effect['id'] + buffers = {} + + # 输入和输出缓冲区 + buffers['input'] = np.zeros(self.buffer_size) + buffers['output_left'] = np.zeros(self.buffer_size) + buffers['output_right'] = np.zeros(self.buffer_size) + + # 延迟缓冲区(用于多普勒效应) + buffers['delay_line'] = np.zeros(int(0.1 * self.sample_rate)) # 100ms延迟线 + buffers['delay_index'] = 0 + + effect['buffers'] = buffers + + def _initialize_hrtf_filters(self, effect: Dict[str, Any]): + """ + 初始化HRTF滤波器 + + Args: + effect: 效果对象 + """ + # 这里应该加载真实的HRTF数据 + # 简化实现使用生成的滤波器 + + hrtf_filters = {} + + # 为不同的角度生成简化的HRTF滤波器 + for azimuth in range(-180, 181, 15): # 每15度一个滤波器 + for elevation in range(-90, 91, 15): + # 生成简化的HRTF滤波器系数 + left_filter = self._generate_simple_hrtf_filter(azimuth, elevation, 'left') + right_filter = self._generate_simple_hrtf_filter(azimuth, elevation, 'right') + + hrtf_filters[(azimuth, elevation)] = { + 'left': left_filter, + 'right': right_filter + } + + effect['hrtf_filters'] = hrtf_filters + + def _generate_simple_hrtf_filter(self, azimuth: int, elevation: int, channel: str) -> np.ndarray: + """ + 生成简化的HRTF滤波器 + + Args: + azimuth: 方位角 (度) + elevation: 仰角 (度) + channel: 声道 ('left' 或 'right') + + Returns: + 滤波器系数数组 + """ + # 简化的HRTF滤波器生成 + # 实际应用中会使用真实的HRTF测量数据 + + # 滤波器长度 + filter_length = 64 + + # 生成基于角度的简化的滤波器 + filter_coeffs = np.zeros(filter_length) + + # 基于角度计算延迟和幅度 + if channel == 'left': + # 左耳滤波器 + delay = max(0, int((1.0 - math.cos(math.radians(azimuth))) * 10)) + amplitude = 0.5 + 0.5 * math.cos(math.radians(azimuth)) + else: + # 右耳滤波器 + delay = max(0, int((1.0 - math.cos(math.radians(-azimuth))) * 10)) + amplitude = 0.5 + 0.5 * math.cos(math.radians(-azimuth)) + + # 应用延迟和幅度 + if delay < filter_length: + filter_coeffs[delay] = amplitude + + return filter_coeffs + + def delete_effect(self, effect_id: str) -> bool: + """ + 删除空间音频效果 + + Args: + effect_id: 效果ID + + Returns: + 是否删除成功 + """ + if effect_id not in self.effects: + print(f"✗ 空间音频效果不存在: {effect_id}") + return False + + try: + # 删除效果 + del self.effects[effect_id] + + # 更新统计信息 + self.stats['active_effects'] -= 1 + + # 从插件中移除效果 + if self.plugin and effect_id in self.plugin.effects: + del self.plugin.effects[effect_id] + + print(f"✓ 空间音频效果已删除: {effect_id}") + return True + + except Exception as e: + print(f"✗ 删除空间音频效果失败: {e}") + return False + + def process(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: + """ + 处理空间音频效果 + + Args: + audio_data: 输入音频数据 + parameters: 效果参数 + + Returns: + 处理后的音频数据 + """ + try: + import time + process_start = time.time() + + # 如果输入是NumPy数组 + if isinstance(audio_data, np.ndarray): + processed_data = self._process_numpy_array(audio_data, parameters) + else: + # 对于其他类型的数据,返回原始数据 + processed_data = audio_data + + # 更新统计信息 + self.stats['effects_processed'] += 1 + self.stats['processing_time'] += (time.time() - process_start) + + return processed_data + + except Exception as e: + print(f"✗ 空间音频效果处理失败: {e}") + return audio_data + + def _process_numpy_array(self, audio_data: np.ndarray, parameters: Dict[str, Any]) -> np.ndarray: + """ + 处理NumPy数组音频数据 + + Args: + audio_data: 输入音频数据 + parameters: 效果参数 + + Returns: + 处理后的音频数据 + """ + # 获取参数 + source_pos = Vec3(*parameters.get('source_position', (0.0, 0.0, 0.0))) + listener_pos = Vec3(*parameters.get('listener_position', (0.0, 0.0, 0.0))) + listener_orient = parameters.get('listener_orientation', (0.0, 0.0, 0.0)) + min_distance = max(0.1, parameters.get('min_distance', 1.0)) + max_distance = max(min_distance, parameters.get('max_distance', 100.0)) + rolloff_factor = max(0.0, parameters.get('rolloff_factor', 1.0)) + cone_inner_angle = max(0.0, min(360.0, parameters.get('cone_inner_angle', 360.0))) + cone_outer_angle = max(cone_inner_angle, min(360.0, parameters.get('cone_outer_angle', 360.0))) + cone_outer_gain = max(0.0, min(1.0, parameters.get('cone_outer_gain', 1.0))) + doppler_factor = max(0.0, parameters.get('doppler_factor', 1.0)) + spread = max(0.0, parameters.get('spread', 0.0)) + air_absorption = max(0.0, parameters.get('air_absorption', 0.0)) + occlusion = max(0.0, min(1.0, parameters.get('occlusion', 0.0))) + obstruction = max(0.0, min(1.0, parameters.get('obstruction', 0.0))) + hrtf_enabled = parameters.get('hrtf_enabled', False) + reverb_send = max(0.0, min(1.0, parameters.get('reverb_send', 0.0))) + + # 计算距离 + distance = (source_pos - listener_pos).length() + + # 计算方位角和仰角 + azimuth, elevation = self._calculate_angles(source_pos, listener_pos, listener_orient) + + # 计算音量衰减 + volume = self._calculate_volume_attenuation( + distance, min_distance, max_distance, rolloff_factor) + + # 计算锥形效应 + cone_gain = self._calculate_cone_gain( + source_pos, listener_pos, listener_orient, + cone_inner_angle, cone_outer_angle, cone_outer_gain) + + # 计算遮挡和阻挡 + occlusion_gain = 1.0 - occlusion * 0.8 # 最多减少80%音量 + obstruction_gain = 1.0 - obstruction * 0.5 # 最多减少50%音量 + + # 计算多普勒效应 + doppler_shift = self._calculate_doppler_shift( + source_pos, listener_pos, doppler_factor) + + # 计算空气吸收 + air_absorption_gain = 1.0 - (distance / max_distance) * air_absorption * 0.5 + + # 综合增益 + total_gain = (volume * cone_gain * occlusion_gain * + obstruction_gain * air_absorption_gain) + + # 创建输出数组 + if audio_data.ndim == 1: + # 单声道输入,生成立体声输出 + output_left = np.zeros_like(audio_data) + output_right = np.zeros_like(audio_data) + + for i in range(len(audio_data)): + sample = audio_data[i] + + # 应用增益 + processed_sample = sample * total_gain + + # 应用HRTF或简单立体声处理 + if hrtf_enabled: + # 简化的HRTF应用 + left_sample, right_sample = self._apply_hrtf( + processed_sample, azimuth, elevation) + else: + # 简单的立体声平移 + left_sample, right_sample = self._simple_stereo_pan( + processed_sample, azimuth, spread) + + output_left[i] = left_sample + output_right[i] = right_sample + + # 合并为立体声数组 + output_data = np.array([output_left, output_right]) + + else: + # 多声道处理 + if audio_data.shape[0] >= 2: + # 已经是立体声,应用空间效果 + output_left = np.zeros_like(audio_data[0]) + output_right = np.zeros_like(audio_data[1]) + + for i in range(audio_data.shape[1]): + left_sample = audio_data[0, i] * total_gain + right_sample = audio_data[1, i] * total_gain + + # 应用HRTF或简单立体声处理 + if hrtf_enabled: + # 简化的HRTF应用 + new_left, new_right = self._apply_hrtf( + (left_sample + right_sample) * 0.5, azimuth, elevation) + output_left[i] = new_left + output_right[i] = new_right + else: + # 简单的立体声平移 + new_left, new_right = self._simple_stereo_pan( + (left_sample + right_sample) * 0.5, azimuth, spread) + output_left[i] = new_left + output_right[i] = new_right + + output_data = np.array([output_left, output_right]) + else: + # 其他多声道情况,返回原始数据 + output_data = audio_data + + # 确保输出在有效范围内 + processed_data = np.clip(output_data, -1.0, 1.0) + return processed_data + + def _calculate_angles(self, source_pos: Vec3, listener_pos: Vec3, + listener_orient: Tuple[float, float, float]) -> Tuple[float, float]: + """ + 计算方位角和仰角 + + Args: + source_pos: 音源位置 + listener_pos: 听者位置 + listener_orient: 听者方向 + + Returns: + (方位角, 仰角) (度) + """ + # 计算相对位置 + relative_pos = source_pos - listener_pos + + # 计算方位角 (水平角度) + azimuth = math.degrees(math.atan2(relative_pos.x, -relative_pos.y)) + + # 计算仰角 (垂直角度) + distance_xy = math.sqrt(relative_pos.x**2 + relative_pos.y**2) + elevation = math.degrees(math.atan2(relative_pos.z, distance_xy)) + + return azimuth, elevation + + def _calculate_volume_attenuation(self, distance: float, min_distance: float, + max_distance: float, rolloff_factor: float) -> float: + """ + 计算音量衰减 + + Args: + distance: 距离 + min_distance: 最小距离 + max_distance: 最大距离 + rolloff_factor: 衰减因子 + + Returns: + 音量增益 (0.0-1.0) + """ + if distance <= min_distance: + return 1.0 + elif distance >= max_distance: + return 0.0 + else: + # 反距离衰减模型 + gain = min_distance / (min_distance + rolloff_factor * (distance - min_distance)) + return max(0.0, min(1.0, gain)) + + def _calculate_cone_gain(self, source_pos: Vec3, listener_pos: Vec3, + listener_orient: Tuple[float, float, float], + inner_angle: float, outer_angle: float, outer_gain: float) -> float: + """ + 计算锥形效应增益 + + Args: + source_pos: 音源位置 + listener_pos: 听者位置 + listener_orient: 听者方向 + inner_angle: 内锥角 (度) + outer_angle: 外锥角 (度) + outer_gain: 外锥增益 + + Returns: + 锥形增益 (0.0-1.0) + """ + # 简化的锥形效应计算 + # 实际应用中需要考虑音源的朝向 + + if inner_angle >= 360.0 and outer_angle >= 360.0: + return 1.0 + + # 计算听者相对于音源的角度 + relative_pos = listener_pos - source_pos + angle = math.degrees(math.acos(relative_pos.normalized().dot(Vec3(0, -1, 0)))) + + if angle <= inner_angle / 2.0: + return 1.0 + elif angle >= outer_angle / 2.0: + return outer_gain + else: + # 线性插值 + t = (angle - inner_angle / 2.0) / (outer_angle / 2.0 - inner_angle / 2.0) + return 1.0 - t * (1.0 - outer_gain) + + def _calculate_doppler_shift(self, source_pos: Vec3, listener_pos: Vec3, + doppler_factor: float) -> float: + """ + 计算多普勒频移 + + Args: + source_pos: 音源位置 + listener_pos: 听者位置 + doppler_factor: 多普勒因子 + + Returns: + 频移比率 + """ + # 简化的多普勒效应计算 + # 实际应用中需要考虑音源和听者的速度 + + if doppler_factor <= 0.0: + return 1.0 + + # 计算相对速度(简化为距离变化率) + relative_velocity = (source_pos - listener_pos).length() / self.sample_rate + ratio = (self.speed_of_sound - doppler_factor * relative_velocity) / self.speed_of_sound + return max(0.5, min(2.0, ratio)) + + def _apply_hrtf(self, sample: float, azimuth: float, elevation: float) -> Tuple[float, float]: + """ + 应用HRTF + + Args: + sample: 输入样本 + azimuth: 方位角 (度) + elevation: 仰角 (度) + + Returns: + (左声道样本, 右声道样本) + """ + # 简化的HRTF应用 + # 实际应用中会使用真实的HRTF滤波器 + + # 量化角度以匹配预生成的滤波器 + azimuth_quantized = int(round(azimuth / 15.0)) * 15 + elevation_quantized = int(round(elevation / 15.0)) * 15 + + # 限制角度范围 + azimuth_quantized = max(-180, min(180, azimuth_quantized)) + elevation_quantized = max(-90, min(90, elevation_quantized)) + + # 简化的HRTF效果 + left_gain = 0.5 + 0.5 * math.cos(math.radians(azimuth)) + right_gain = 0.5 + 0.5 * math.cos(math.radians(-azimuth)) + + # 应用仰角影响 + elevation_factor = math.cos(math.radians(elevation)) + left_gain *= elevation_factor + right_gain *= elevation_factor + + return sample * left_gain, sample * right_gain + + def _simple_stereo_pan(self, sample: float, azimuth: float, spread: float) -> Tuple[float, float]: + """ + 简单的立体声平移 + + Args: + sample: 输入样本 + azimuth: 方位角 (度) + spread: 扩散角度 + + Returns: + (左声道样本, 右声道样本) + """ + # 将角度转换为-1到1的平移值 + pan = math.sin(math.radians(azimuth)) + + # 应用扩散 + if spread > 0: + spread_factor = 1.0 - spread / 180.0 + pan *= spread_factor + + # 计算左右声道增益 + left_gain = 0.5 - 0.5 * pan + right_gain = 0.5 + 0.5 * pan + + return sample * left_gain, sample * right_gain + + def set_parameter(self, effect_id: str, parameter: str, value: Any) -> bool: + """ + 设置效果参数 + + Args: + effect_id: 效果ID + parameter: 参数名 + value: 参数值 + + Returns: + 是否设置成功 + """ + if effect_id not in self.effects: + print(f"✗ 空间音频效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['parameters'][parameter] = value + + # 如果HRTF启用状态改变,重新初始化滤波器 + if parameter == 'hrtf_enabled': + if value: + self._initialize_hrtf_filters(self.effects[effect_id]) + else: + self.effects[effect_id]['hrtf_filters'] = {} + + # 如果是插件中的效果,也更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['parameters'][parameter] = value + if parameter == 'hrtf_enabled': + self.plugin.effects[effect_id]['hrtf_filters'] = \ + self.effects[effect_id]['hrtf_filters'].copy() + + print(f"✓ 空间音频效果参数已设置: {effect_id}.{parameter} = {value}") + return True + + except Exception as e: + print(f"✗ 设置空间音频效果参数失败: {e}") + return False + + def get_parameters(self, effect_id: str) -> Dict[str, Any]: + """ + 获取效果参数 + + Args: + effect_id: 效果ID + + Returns: + 参数字典 + """ + if effect_id not in self.effects: + return {} + + return self.effects[effect_id]['parameters'].copy() + + def enable_effect(self, effect_id: str) -> bool: + """ + 启用空间音频效果 + + Args: + effect_id: 效果ID + + Returns: + 是否启用成功 + """ + if effect_id not in self.effects: + print(f"✗ 空间音频效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['active'] = True + self.stats['active_effects'] += 1 + + # 如果是插件中的效果,也更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['active'] = True + + print(f"✓ 空间音频效果已启用: {effect_id}") + return True + + except Exception as e: + print(f"✗ 启用空间音频效果失败: {e}") + return False + + def disable_effect(self, effect_id: str) -> bool: + """ + 禁用空间音频效果 + + Args: + effect_id: 效果ID + + Returns: + 是否禁用成功 + """ + if effect_id not in self.effects: + print(f"✗ 空间音频效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['active'] = False + self.stats['active_effects'] -= 1 + + # 如果是插件中的效果,也更新插件中的副本 + if self.plugin and effect_id in self.plugin.effects: + self.plugin.effects[effect_id]['active'] = False + + print(f"✓ 空间音频效果已禁用: {effect_id}") + return True + + except Exception as e: + print(f"✗ 禁用空间音频效果失败: {e}") + return False + + def cleanup(self): + """清理所有资源""" + self.effects.clear() + self.stats = { + 'total_effects': 0, + 'active_effects': 0, + 'effects_processed': 0, + 'processing_time': 0.0, + 'memory_usage': 0 + } + print("✓ 空间音频效果处理器资源已清理") + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def _get_current_time(self) -> float: + """ + 获取当前时间(秒) + + Returns: + 当前时间 + """ + if self.plugin and self.plugin.plugin_manager and self.plugin.plugin_manager.world: + return self.plugin.plugin_manager.world.globalClock.getFrameTime() + import time + return time.time() + + def update(self, dt: float): + """ + 更新处理器状态 + + Args: + dt: 时间增量 + """ + # 这里可以更新任何需要定期更新的状态 + pass + + def set_sample_rate(self, sample_rate: int): + """ + 设置采样率 + + Args: + sample_rate: 采样率 (Hz) + """ + self.sample_rate = sample_rate + print(f"✓ 空间音频效果处理器采样率已设置: {sample_rate} Hz") + + def set_source_position(self, effect_id: str, position: Tuple[float, float, float]) -> bool: + """ + 设置音源位置 + + Args: + effect_id: 效果ID + position: 位置 (x, y, z) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'source_position', position) + + def set_listener_position(self, effect_id: str, position: Tuple[float, float, float]) -> bool: + """ + 设置听者位置 + + Args: + effect_id: 效果ID + position: 位置 (x, y, z) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'listener_position', position) + + def set_listener_orientation(self, effect_id: str, orientation: Tuple[float, float, float]) -> bool: + """ + 设置听者方向 + + Args: + effect_id: 效果ID + orientation: 方向 (heading, pitch, roll) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'listener_orientation', orientation) + + def set_distance_model(self, effect_id: str, min_distance: float, + max_distance: float, rolloff_factor: float) -> bool: + """ + 设置距离模型参数 + + Args: + effect_id: 效果ID + min_distance: 最小距离 + max_distance: 最大距离 + rolloff_factor: 衰减因子 + + Returns: + 是否设置成功 + """ + success = True + success &= self.set_parameter(effect_id, 'min_distance', max(0.1, min_distance)) + success &= self.set_parameter(effect_id, 'max_distance', max(min_distance, max_distance)) + success &= self.set_parameter(effect_id, 'rolloff_factor', max(0.0, rolloff_factor)) + return success + + def set_cone_effect(self, effect_id: str, inner_angle: float, + outer_angle: float, outer_gain: float) -> bool: + """ + 设置锥形效应参数 + + Args: + effect_id: 效果ID + inner_angle: 内锥角 (度) + outer_angle: 外锥角 (度) + outer_gain: 外锥增益 + + Returns: + 是否设置成功 + """ + success = True + success &= self.set_parameter(effect_id, 'cone_inner_angle', max(0.0, min(360.0, inner_angle))) + success &= self.set_parameter(effect_id, 'cone_outer_angle', max(inner_angle, min(360.0, outer_angle))) + success &= self.set_parameter(effect_id, 'cone_outer_gain', max(0.0, min(1.0, outer_gain))) + return success + + def set_doppler_factor(self, effect_id: str, factor: float) -> bool: + """ + 设置多普勒因子 + + Args: + effect_id: 效果ID + factor: 多普勒因子 + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'doppler_factor', max(0.0, factor)) + + def set_spread(self, effect_id: str, spread: float) -> bool: + """ + 设置扩散角度 + + Args: + effect_id: 效果ID + spread: 扩散角度 (度) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'spread', max(0.0, spread)) + + def set_air_absorption(self, effect_id: str, absorption: float) -> bool: + """ + 设置空气吸收 + + Args: + effect_id: 效果ID + absorption: 空气吸收系数 + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'air_absorption', max(0.0, min(1.0, absorption))) + + def set_occlusion(self, effect_id: str, occlusion: float) -> bool: + """ + 设置遮挡系数 + + Args: + effect_id: 效果ID + occlusion: 遮挡系数 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'occlusion', max(0.0, min(1.0, occlusion))) + + def set_obstruction(self, effect_id: str, obstruction: float) -> bool: + """ + 设置阻挡系数 + + Args: + effect_id: 效果ID + obstruction: 阻挡系数 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'obstruction', max(0.0, min(1.0, obstruction))) + + def enable_hrtf(self, effect_id: str, enable: bool) -> bool: + """ + 启用/禁用HRTF + + Args: + effect_id: 效果ID + enable: 是否启用 + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'hrtf_enabled', enable) + + def set_reverb_send(self, effect_id: str, send: float) -> bool: + """ + 设置混响发送量 + + Args: + effect_id: 效果ID + send: 混响发送量 (0.0-1.0) + + Returns: + 是否设置成功 + """ + return self.set_parameter(effect_id, 'reverb_send', max(0.0, min(1.0, send))) + + def get_distance(self, effect_id: str) -> float: + """ + 获取当前距离 + + Args: + effect_id: 效果ID + + Returns: + 距离值 + """ + if effect_id not in self.effects: + return 0.0 + + return self.effects[effect_id].get('distance', 0.0) + + def get_angles(self, effect_id: str) -> Tuple[float, float]: + """ + 获取当前角度 + + Args: + effect_id: 效果ID + + Returns: + (方位角, 仰角) (度) + """ + if effect_id not in self.effects: + return (0.0, 0.0) + + effect = self.effects[effect_id] + return (effect.get('azimuth', 0.0), effect.get('elevation', 0.0)) + + def create_preset(self, preset_name: str) -> Dict[str, Any]: + """ + 创建空间音频效果预设 + + Args: + preset_name: 预设名称 + + Returns: + 预设参数字典 + """ + presets = { + 'close': { + 'min_distance': 0.5, + 'max_distance': 20.0, + 'rolloff_factor': 1.0, + 'doppler_factor': 1.0, + 'air_absorption': 0.0 + }, + 'medium': { + 'min_distance': 1.0, + 'max_distance': 50.0, + 'rolloff_factor': 0.8, + 'doppler_factor': 0.8, + 'air_absorption': 0.1 + }, + 'far': { + 'min_distance': 5.0, + 'max_distance': 100.0, + 'rolloff_factor': 0.5, + 'doppler_factor': 0.5, + 'air_absorption': 0.3 + }, + 'outdoor': { + 'min_distance': 2.0, + 'max_distance': 200.0, + 'rolloff_factor': 0.3, + 'doppler_factor': 1.0, + 'air_absorption': 0.5 + }, + 'indoor': { + 'min_distance': 0.5, + 'max_distance': 30.0, + 'rolloff_factor': 1.2, + 'doppler_factor': 0.7, + 'air_absorption': 0.0 + }, + 'underwater': { + 'min_distance': 1.0, + 'max_distance': 40.0, + 'rolloff_factor': 0.2, + 'doppler_factor': 0.3, + 'air_absorption': 0.8, + 'speed_of_sound': 1500.0 # 水中声速 + } + } + + return presets.get(preset_name, {}).copy() + diff --git a/plugins/user/audio_reverb_effects/plugin.py b/plugins/user/audio_reverb_effects/plugin.py index 502a8965..aa0b3983 100644 --- a/plugins/user/audio_reverb_effects/plugin.py +++ b/plugins/user/audio_reverb_effects/plugin.py @@ -1,1131 +1,1131 @@ -""" -音频混响和效果插件主文件 -提供专业的音频混响和效果处理功能 -""" - -import math -import time -import uuid -from typing import Dict, List, Optional, Any, Callable -from plugins.plugin_manager import BasePlugin -from panda3d.core import AudioSound, NodePath - -class Plugin(BasePlugin): - """ - 音频混响和效果插件 - 提供专业的音频混响和效果处理功能 - """ - - def __init__(self, plugin_manager, name): - super().__init__(plugin_manager, name) - self.config = { - "version": "1.0.0", - "author": "EG Team", - "description": "专业的音频混响和效果处理插件,支持多种高级音频效果" - } - - # 核心组件 - self.audio_manager = None - self.listener = None - - # 效果处理器 - self.reverb_processor = None - self.eq_processor = None - self.compressor_processor = None - self.modulation_processor = None - self.distortion_processor = None - self.spatial_processor = None - - # 效果管理 - self.effects = {} # 所有效果 - self.sound_effects = {} # 音效与效果的关联 - self.presets = {} # 效果预设 - - # GUI元素 - self.gui_elements = [] - - # 事件处理器 - self.event_handlers = {} - - # 插件状态 - self.is_initialized = False - self.is_enabled = False - self.processing_enabled = True - - # 性能监控 - self.performance_stats = { - 'initialization_time': 0.0, - 'enable_time': 0.0, - 'last_update_time': 0.0, - 'total_updates': 0, - 'total_processing_time': 0.0, - 'effects_processed': 0, - 'memory_usage': 0 - } - - # 配置参数 - self.master_volume = 1.0 - self.quality_settings = 'high' # low, medium, high - self.max_effects_per_sound = 8 - self.buffer_size = 4096 - - def initialize(self) -> bool: - """ - 初始化插件 - """ - try: - if self.is_initialized: - return True - - init_start_time = time.time() - print(f"初始化音频混响和效果插件: {self.name}") - - # 初始化音频管理器 - self.audio_manager = self.plugin_manager.world.audio_manager - - # 初始化听者 - self.listener = self.plugin_manager.world.camera - - # 初始化各效果处理器 - from .effects.reverb_processor import ReverbProcessor - from .effects.eq_processor import EQProcessor - from .effects.compressor_processor import CompressorProcessor - from .effects.modulation_processor import ModulationProcessor - from .effects.distortion_processor import DistortionProcessor - from .effects.spatial_processor import SpatialProcessor - - self.reverb_processor = ReverbProcessor(self) - self.eq_processor = EQProcessor(self) - self.compressor_processor = CompressorProcessor(self) - self.modulation_processor = ModulationProcessor(self) - self.distortion_processor = DistortionProcessor(self) - self.spatial_processor = SpatialProcessor(self) - - # 加载预设 - self._load_presets() - - self.is_initialized = True - self.performance_stats['initialization_time'] = time.time() - init_start_time - print("✓ 音频混响和效果插件初始化完成") - return True - - except Exception as e: - print(f"✗ 音频混响和效果插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - """ - if not super().enable(): - return False - - if self.is_enabled: - return True - - try: - enable_start_time = time.time() - print(f"启用音频混响和效果插件: {self.name}") - - # 注册事件处理器 - self._register_event_handlers() - - # 创建GUI界面 - self._create_gui() - - self.is_enabled = True - self.performance_stats['enable_time'] = time.time() - enable_start_time - print("✓ 音频混响和效果插件启用成功") - return True - - except Exception as e: - print(f"✗ 音频混响和效果插件启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self) -> bool: - """ - 禁用插件 - """ - if not super().disable(): - return False - - if not self.is_enabled: - return True - - try: - print(f"禁用音频混响和效果插件: {self.name}") - - # 清理GUI界面 - self._cleanup_gui() - - # 移除事件处理器 - self._unregister_event_handlers() - - # 停止所有效果处理 - self._stop_all_processing() - - self.is_enabled = False - print("✓ 音频混响和效果插件禁用成功") - return True - - except Exception as e: - print(f"✗ 音频混响和效果插件禁用失败: {e}") - import traceback - traceback.print_exc() - return False - - def finalize(self): - """ - 清理插件资源 - """ - print(f"清理音频混响和效果插件资源: {self.name}") - - # 停止所有处理 - self._stop_all_processing() - - # 清理所有组件 - processors = [ - self.reverb_processor, - self.eq_processor, - self.compressor_processor, - self.modulation_processor, - self.distortion_processor, - self.spatial_processor - ] - - for processor in processors: - if processor: - processor.cleanup() - - self.reverb_processor = None - self.eq_processor = None - self.compressor_processor = None - self.modulation_processor = None - self.distortion_processor = None - self.spatial_processor = None - - self.audio_manager = None - self.listener = None - self.effects.clear() - self.sound_effects.clear() - self.presets.clear() - - self.is_initialized = False - self.is_enabled = False - - def _register_event_handlers(self): - """ - 注册事件处理器 - """ - world = self.plugin_manager.world - - # 音频效果控制快捷键 - self.event_handlers['f1'] = self.toggle_debug_mode - self.event_handlers['f2'] = self.toggle_processing - self.event_handlers['f3'] = self.apply_test_reverb - self.event_handlers['f4'] = self.apply_test_eq - self.event_handlers['f5'] = self.show_stats - self.event_handlers['f6'] = self.apply_test_compressor - self.event_handlers['f7'] = self.apply_test_modulation - self.event_handlers['f8'] = self.apply_test_distortion - self.event_handlers['f9'] = self.create_test_preset - self.event_handlers['f10'] = self.toggle_quality - self.event_handlers['f11'] = self.show_detailed_stats - self.event_handlers['f12'] = self.reset_all_effects - - # 注册事件 - for event, handler in self.event_handlers.items(): - world.accept(event, handler) - world.accept(event.upper(), handler) # 同时注册大写事件 - - def _unregister_event_handlers(self): - """ - 移除事件处理器 - """ - world = self.plugin_manager.world - - for event in self.event_handlers.keys(): - world.ignore(event) - world.ignore(event.upper()) - - self.event_handlers.clear() - - def _create_gui(self): - """ - 创建GUI界面 - """ - try: - world = self.plugin_manager.world - - # 检查是否有GUI管理器 - if not hasattr(world, 'gui_manager') or not world.gui_manager: - print("⚠ GUI管理器不可用,跳过GUI创建") - return - - gui_manager = world.gui_manager - - # 创建音频效果控制面板 - panel_button = gui_manager.createGUIButton( - pos=(0.7, 0, 0.35), - text="音频效果", - size=0.06 - ) - self.gui_elements.append(panel_button) - - # 创建功能按钮 - functions = [ - ("调试模式 [F1]", self.toggle_debug_mode), - ("切换处理 [F2]", self.toggle_processing), - ("测试混响 [F3]", self.apply_test_reverb), - ("测试EQ [F4]", self.apply_test_eq), - ("效果统计 [F5]", self.show_stats), - ("测试压缩 [F6]", self.apply_test_compressor), - ("测试调制 [F7]", self.apply_test_modulation), - ("测试失真 [F8]", self.apply_test_distortion), - ("测试预设 [F9]", self.create_test_preset), - ("切换质量 [F10]", self.toggle_quality), - ("详细统计 [F11]", self.show_detailed_stats), - ("重置效果 [F12]", self.reset_all_effects) - ] - - for i, (name, callback) in enumerate(functions): - button = gui_manager.createGUIButton( - pos=(0.7, 0, 0.28 - i * 0.055), - text=name, - size=0.045 - ) - self.gui_elements.append(button) - world.accept(f"audio_effect_function_{i}", callback) - - except Exception as e: - print(f"⚠ GUI创建失败: {e}") - - def _cleanup_gui(self): - """ - 清理GUI界面 - """ - try: - world = self.plugin_manager.world - - if hasattr(world, 'gui_manager') and world.gui_manager: - gui_manager = world.gui_manager - - # 删除所有GUI元素 - for element in self.gui_elements: - gui_manager.deleteGUIElement(element) - - self.gui_elements.clear() - - except Exception as e: - print(f"⚠ GUI清理失败: {e}") - - # 事件处理方法 - def toggle_debug_mode(self): - """切换调试模式""" - print("✓ 音频效果调试模式切换") - - def toggle_processing(self): - """切换效果处理""" - self.processing_enabled = not self.processing_enabled - print(f"✓ 音频效果处理已{'启用' if self.processing_enabled else '禁用'}") - - def apply_test_reverb(self): - """应用测试混响效果""" - print("✓ 应用测试混响效果") - try: - # 创建一个测试混响效果 - reverb_params = { - 'room_size': 0.8, - 'damping': 0.5, - 'wet_level': 0.3, - 'dry_level': 0.7, - 'width': 1.0, - 'freeze_mode': 0.0 - } - effect_id = self.create_reverb_effect(reverb_params) - if effect_id: - print(f"✓ 测试混响效果创建成功: {effect_id}") - else: - print("✗ 测试混响效果创建失败") - except Exception as e: - print(f"✗ 应用测试混响效果失败: {e}") - - def apply_test_eq(self): - """应用测试均衡器效果""" - print("✓ 应用测试均衡器效果") - try: - # 创建一个测试均衡器效果 - eq_params = { - 'bands': { - 60: 2.0, # 60Hz 提升2dB - 200: -1.0, # 200Hz 降低1dB - 1000: 3.0, # 1kHz 提升3dB - 5000: -2.0, # 5kHz 降低2dB - 10000: 1.0 # 10kHz 提升1dB - } - } - effect_id = self.create_eq_effect(eq_params) - if effect_id: - print(f"✓ 测试均衡器效果创建成功: {effect_id}") - else: - print("✗ 测试均衡器效果创建失败") - except Exception as e: - print(f"✗ 应用测试均衡器效果失败: {e}") - - def show_stats(self): - """显示效果统计信息""" - stats = self.get_stats() - print("=== 音频效果统计信息 ===") - for key, value in stats.items(): - print(f" {key}: {value}") - - def apply_test_compressor(self): - """应用测试压缩器效果""" - print("✓ 应用测试压缩器效果") - try: - # 创建一个测试压缩器效果 - compressor_params = { - 'threshold': -20.0, - 'ratio': 4.0, - 'attack': 0.01, - 'release': 0.1, - 'gain': 2.0, - 'knee': 5.0 - } - effect_id = self.create_compressor_effect(compressor_params) - if effect_id: - print(f"✓ 测试压缩器效果创建成功: {effect_id}") - else: - print("✗ 测试压缩器效果创建失败") - except Exception as e: - print(f"✗ 应用测试压缩器效果失败: {e}") - - def apply_test_modulation(self): - """应用测试调制效果""" - print("✓ 应用测试调制效果") - try: - # 创建一个测试合唱效果 - modulation_params = { - 'type': 'chorus', - 'rate': 1.0, - 'depth': 0.5, - 'feedback': 0.3, - 'mix': 0.5, - 'delay': 0.01 - } - effect_id = self.create_modulation_effect(modulation_params) - if effect_id: - print(f"✓ 测试调制效果创建成功: {effect_id}") - else: - print("✗ 测试调制效果创建失败") - except Exception as e: - print(f"✗ 应用测试调制效果失败: {e}") - - def apply_test_distortion(self): - """应用测试失真效果""" - print("✓ 应用测试失真效果") - try: - # 创建一个测试失真效果 - distortion_params = { - 'drive': 0.5, - 'tone': 0.8, - 'level': 0.7, - 'bias': 0.0 - } - effect_id = self.create_distortion_effect(distortion_params) - if effect_id: - print(f"✓ 测试失真效果创建成功: {effect_id}") - else: - print("✗ 测试失真效果创建失败") - except Exception as e: - print(f"✗ 应用测试失真效果失败: {e}") - - def create_test_preset(self): - """创建测试预设""" - print("✓ 创建测试预设") - try: - preset_name = f"test_preset_{int(time.time())}" - preset_data = { - 'reverb': { - 'room_size': 0.7, - 'damping': 0.6, - 'wet_level': 0.4, - 'dry_level': 0.6 - }, - 'eq': { - 'bands': { - 100: 2.0, - 1000: -1.0, - 8000: 1.5 - } - }, - 'compressor': { - 'threshold': -15.0, - 'ratio': 3.0 - } - } - - success = self.create_preset(preset_name, preset_data) - if success: - print(f"✓ 测试预设创建成功: {preset_name}") - else: - print("✗ 测试预设创建失败") - except Exception as e: - print(f"✗ 创建测试预设失败: {e}") - - def toggle_quality(self): - """切换音质设置""" - quality_levels = ['low', 'medium', 'high'] - current_index = quality_levels.index(self.quality_settings) - next_index = (current_index + 1) % len(quality_levels) - self.quality_settings = quality_levels[next_index] - print(f"✓ 音质设置已切换到: {self.quality_settings}") - - def show_detailed_stats(self): - """显示详细统计信息""" - print("=== 音频效果详细统计信息 ===") - - # 显示各处理器统计 - processors = [ - ('混响处理器', self.reverb_processor), - ('均衡器处理器', self.eq_processor), - ('压缩器处理器', self.compressor_processor), - ('调制处理器', self.modulation_processor), - ('失真处理器', self.distortion_processor), - ('空间处理器', self.spatial_processor) - ] - - for name, processor in processors: - if processor: - processor_stats = processor.get_stats() - print(f"{name}统计:") - for key, value in processor_stats.items(): - print(f" {key}: {value}") - - # 显示性能统计 - perf_stats = self.performance_stats - print("性能统计:") - for key, value in perf_stats.items(): - print(f" {key}: {value}") - - def reset_all_effects(self): - """重置所有效果""" - print("✓ 重置所有音频效果") - self.effects.clear() - self.sound_effects.clear() - print("✓ 所有音频效果已重置") - - def _stop_all_processing(self): - """停止所有效果处理""" - print("✓ 停止所有音频效果处理") - - def _load_presets(self): - """加载预设效果""" - # 默认预设 - self.presets['small_room'] = { - 'reverb': { - 'room_size': 0.3, - 'damping': 0.7, - 'wet_level': 0.2, - 'dry_level': 0.8, - 'width': 0.8 - } - } - - self.presets['large_hall'] = { - 'reverb': { - 'room_size': 0.9, - 'damping': 0.4, - 'wet_level': 0.5, - 'dry_level': 0.5, - 'width': 1.0 - } - } - - self.presets['cathedral'] = { - 'reverb': { - 'room_size': 1.0, - 'damping': 0.2, - 'wet_level': 0.7, - 'dry_level': 0.3, - 'width': 1.0 - } - } - - self.presets['bathroom'] = { - 'reverb': { - 'room_size': 0.2, - 'damping': 0.1, - 'wet_level': 0.8, - 'dry_level': 0.2, - 'width': 0.9 - } - } - - print("✓ 预设效果加载完成") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取插件统计信息 - - Returns: - 统计信息字典 - """ - stats = self.performance_stats.copy() - - # 收集各处理器统计 - processors = [ - self.reverb_processor, - self.eq_processor, - self.compressor_processor, - self.modulation_processor, - self.distortion_processor, - self.spatial_processor - ] - - processor_names = [ - 'reverb', 'eq', 'compressor', 'modulation', 'distortion', 'spatial' - ] - - for i, processor in enumerate(processors): - if processor: - processor_stats = processor.get_stats() - for key, value in processor_stats.items(): - stats[f"{processor_names[i]}_{key}"] = value - - # 添加效果统计 - stats['total_effects'] = len(self.effects) - stats['active_effects'] = sum(1 for effect in self.effects.values() if effect.get('active', True)) - stats['effect_assignments'] = sum(len(effect_list) for effect_list in self.sound_effects.values()) - - return stats - - # 公共接口方法 - def create_reverb_effect(self, parameters: Dict[str, Any]) -> str: - """ - 创建混响效果 - - Args: - parameters: 混响参数 - - Returns: - 效果ID - """ - if self.reverb_processor: - return self.reverb_processor.create_effect(parameters) - return "" - - def create_eq_effect(self, parameters: Dict[str, Any]) -> str: - """ - 创建均衡器效果 - - Args: - parameters: 均衡器参数 - - Returns: - 效果ID - """ - if self.eq_processor: - return self.eq_processor.create_effect(parameters) - return "" - - def create_compressor_effect(self, parameters: Dict[str, Any]) -> str: - """ - 创建压缩器效果 - - Args: - parameters: 压缩器参数 - - Returns: - 效果ID - """ - if self.compressor_processor: - return self.compressor_processor.create_effect(parameters) - return "" - - def create_modulation_effect(self, parameters: Dict[str, Any]) -> str: - """ - 创建调制效果 - - Args: - parameters: 调制参数 - - Returns: - 效果ID - """ - if self.modulation_processor: - return self.modulation_processor.create_effect(parameters) - return "" - - def create_distortion_effect(self, parameters: Dict[str, Any]) -> str: - """ - 创建失真效果 - - Args: - parameters: 失真参数 - - Returns: - 效果ID - """ - if self.distortion_processor: - return self.distortion_processor.create_effect(parameters) - return "" - - def create_spatial_effect(self, parameters: Dict[str, Any]) -> str: - """ - 创建空间效果 - - Args: - parameters: 空间效果参数 - - Returns: - 效果ID - """ - if self.spatial_processor: - return self.spatial_processor.create_effect(parameters) - return "" - - def delete_effect(self, effect_id: str) -> bool: - """ - 删除效果 - - Args: - effect_id: 效果ID - - Returns: - 是否删除成功 - """ - if effect_id not in self.effects: - print(f"✗ 效果不存在: {effect_id}") - return False - - try: - # 从所有音效中移除该效果 - for sound_id, effect_list in self.sound_effects.items(): - if effect_id in effect_list: - effect_list.remove(effect_id) - - # 删除效果 - del self.effects[effect_id] - - print(f"✓ 音频效果已删除: {effect_id}") - return True - - except Exception as e: - print(f"✗ 删除音频效果失败: {e}") - return False - - def apply_effect_to_sound(self, sound_id: str, effect_id: str) -> bool: - """ - 应用效果到音效 - - Args: - sound_id: 音源ID - effect_id: 效果ID - - Returns: - 是否应用成功 - """ - # 检查音源和效果是否存在 - # 注意:这里需要与主音频系统集成 - if effect_id not in self.effects: - print(f"✗ 效果不存在: {effect_id}") - return False - - try: - # 初始化音效的效果列表 - if sound_id not in self.sound_effects: - self.sound_effects[sound_id] = [] - - # 检查是否超过最大效果数 - if len(self.sound_effects[sound_id]) >= self.max_effects_per_sound: - print(f"✗ 音效已达到最大效果数限制: {self.max_effects_per_sound}") - return False - - # 添加效果到音效 - if effect_id not in self.sound_effects[sound_id]: - self.sound_effects[sound_id].append(effect_id) - - effect_type = self.effects[effect_id]['type'] - print(f"✓ 效果已应用到音效: {effect_id} ({effect_type}) -> {sound_id}") - return True - else: - print(f"⚠ 效果已应用于该音效: {effect_id} -> {sound_id}") - return True - - except Exception as e: - print(f"✗ 应用效果到音效失败: {e}") - return False - - def remove_effect_from_sound(self, sound_id: str, effect_id: str) -> bool: - """ - 从音效移除效果 - - Args: - sound_id: 音源ID - effect_id: 效果ID - - Returns: - 是否移除成功 - """ - # 检查音源和效果是否存在 - if effect_id not in self.effects: - print(f"✗ 效果不存在: {effect_id}") - return False - - try: - # 检查效果是否已应用于该音效 - if sound_id in self.sound_effects and effect_id in self.sound_effects[sound_id]: - self.sound_effects[sound_id].remove(effect_id) - - effect_type = self.effects[effect_id]['type'] - print(f"✓ 效果已从音效移除: {effect_id} ({effect_type}) -> {sound_id}") - return True - else: - print(f"⚠ 效果未应用于该音效: {effect_id} -> {sound_id}") - return True - - except Exception as e: - print(f"✗ 从音效移除效果失败: {e}") - return False - - def set_effect_parameter(self, effect_id: str, parameter: str, value: Any) -> bool: - """ - 设置效果参数 - - Args: - effect_id: 效果ID - parameter: 参数名 - value: 参数值 - - Returns: - 是否设置成功 - """ - if effect_id not in self.effects: - print(f"✗ 效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['parameters'][parameter] = value - print(f"✓ 效果参数已设置: {effect_id}.{parameter} = {value}") - return True - - except Exception as e: - print(f"✗ 设置效果参数失败: {e}") - return False - - def get_effect_parameters(self, effect_id: str) -> Dict[str, Any]: - """ - 获取效果参数 - - Args: - effect_id: 效果ID - - Returns: - 参数字典 - """ - if effect_id not in self.effects: - return {} - - return self.effects[effect_id]['parameters'].copy() - - def enable_effect(self, effect_id: str) -> bool: - """ - 启用效果 - - Args: - effect_id: 效果ID - - Returns: - 是否启用成功 - """ - if effect_id not in self.effects: - print(f"✗ 效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['active'] = True - print(f"✓ 效果已启用: {effect_id}") - return True - - except Exception as e: - print(f"✗ 启用效果失败: {e}") - return False - - def disable_effect(self, effect_id: str) -> bool: - """ - 禁用效果 - - Args: - effect_id: 效果ID - - Returns: - 是否禁用成功 - """ - if effect_id not in self.effects: - print(f"✗ 效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['active'] = False - print(f"✓ 效果已禁用: {effect_id}") - return True - - except Exception as e: - print(f"✗ 禁用效果失败: {e}") - return False - - def get_effect_type(self, effect_id: str) -> str: - """ - 获取效果类型 - - Args: - effect_id: 效果ID - - Returns: - 效果类型 - """ - if effect_id not in self.effects: - return "" - - return self.effects[effect_id]['type'] - - def get_sound_effects(self, sound_id: str) -> List[str]: - """ - 获取音效应用的效果列表 - - Args: - sound_id: 音源ID - - Returns: - 效果ID列表 - """ - return self.sound_effects.get(sound_id, []).copy() - - def create_preset(self, preset_name: str, preset_data: Dict[str, Any]) -> bool: - """ - 创建效果预设 - - Args: - preset_name: 预设名称 - preset_data: 预设数据 - - Returns: - 是否创建成功 - """ - try: - self.presets[preset_name] = preset_data.copy() - print(f"✓ 效果预设创建成功: {preset_name}") - return True - except Exception as e: - print(f"✗ 创建效果预设失败: {e}") - return False - - def apply_preset_to_sound(self, sound_id: str, preset_name: str) -> bool: - """ - 应用预设到音效 - - Args: - sound_id: 音源ID - preset_name: 预设名称 - - Returns: - 是否应用成功 - """ - if preset_name not in self.presets: - print(f"✗ 预设不存在: {preset_name}") - return False - - try: - preset_data = self.presets[preset_name] - created_effects = [] - - # 应用混响预设 - if 'reverb' in preset_data: - effect_id = self.create_reverb_effect(preset_data['reverb']) - if effect_id: - created_effects.append(('reverb', effect_id)) - - # 应用均衡器预设 - if 'eq' in preset_data: - effect_id = self.create_eq_effect(preset_data['eq']) - if effect_id: - created_effects.append(('eq', effect_id)) - - # 应用压缩器预设 - if 'compressor' in preset_data: - effect_id = self.create_compressor_effect(preset_data['compressor']) - if effect_id: - created_effects.append(('compressor', effect_id)) - - # 将所有创建的效果应用到音效 - success_count = 0 - for effect_type, effect_id in created_effects: - if self.apply_effect_to_sound(sound_id, effect_id): - success_count += 1 - - print(f"✓ 预设 {preset_name} 已应用到音效 {sound_id} ({success_count}/{len(created_effects)} 效果)") - return success_count > 0 - - except Exception as e: - print(f"✗ 应用预设到音效失败: {e}") - return False - - def get_preset_names(self) -> List[str]: - """ - 获取所有预设名称 - - Returns: - 预设名称列表 - """ - return list(self.presets.keys()) - - def delete_preset(self, preset_name: str) -> bool: - """ - 删除预设 - - Args: - preset_name: 预设名称 - - Returns: - 是否删除成功 - """ - if preset_name not in self.presets: - print(f"✗ 预设不存在: {preset_name}") - return False - - try: - del self.presets[preset_name] - print(f"✓ 预设已删除: {preset_name}") - return True - except Exception as e: - print(f"✗ 删除预设失败: {e}") - return False - - def set_master_volume(self, volume: float): - """ - 设置主音量 - - Args: - volume: 主音量 (0.0-1.0) - """ - self.master_volume = max(0.0, min(1.0, volume)) - print(f"✓ 主音量已设置: {self.master_volume}") - - def get_master_volume(self) -> float: - """ - 获取主音量 - - Returns: - 主音量 (0.0-1.0) - """ - return self.master_volume - - def set_quality_settings(self, quality: str): - """ - 设置音质设置 - - Args: - quality: 音质等级 ('low', 'medium', 'high') - """ - if quality in ['low', 'medium', 'high']: - self.quality_settings = quality - print(f"✓ 音质设置已更新: {quality}") - else: - print(f"✗ 无效的音质设置: {quality}") - - def get_quality_settings(self) -> str: - """ - 获取音质设置 - - Returns: - 音质等级 - """ - return self.quality_settings - - def update(self, dt: float): - """ - 更新插件状态 - - Args: - dt: 时间增量 - """ - update_start_time = time.time() - - # 更新各效果处理器 - processors = [ - self.reverb_processor, - self.eq_processor, - self.compressor_processor, - self.modulation_processor, - self.distortion_processor, - self.spatial_processor - ] - - for processor in processors: - if processor: - processor.update(dt) - - # 更新性能统计 - self.performance_stats['total_updates'] += 1 - self.performance_stats['last_update_time'] = time.time() - self.performance_stats['total_processing_time'] += (time.time() - update_start_time) - - def process_audio_with_effects(self, sound_id: str, audio_data: Any) -> Any: - """ - 使用效果处理音频数据 - - Args: - sound_id: 音源ID - audio_data: 音频数据 - - Returns: - 处理后的音频数据 - """ - if not self.processing_enabled: - return audio_data - - # 获取应用到该音效的所有效果 - effect_ids = self.sound_effects.get(sound_id, []) - - if not effect_ids: - return audio_data - - # 按顺序应用效果 - processed_data = audio_data - effects_processed = 0 - - for effect_id in effect_ids: - if effect_id in self.effects and self.effects[effect_id].get('active', True): - effect = self.effects[effect_id] - effect_type = effect['type'] - - # 根据效果类型调用相应的处理器 - if effect_type == 'reverb' and self.reverb_processor: - processed_data = self.reverb_processor.process(processed_data, effect['parameters']) - effects_processed += 1 - elif effect_type == 'eq' and self.eq_processor: - processed_data = self.eq_processor.process(processed_data, effect['parameters']) - effects_processed += 1 - elif effect_type == 'compressor' and self.compressor_processor: - processed_data = self.compressor_processor.process(processed_data, effect['parameters']) - effects_processed += 1 - elif effect_type == 'modulation' and self.modulation_processor: - processed_data = self.modulation_processor.process(processed_data, effect['parameters']) - effects_processed += 1 - elif effect_type == 'distortion' and self.distortion_processor: - processed_data = self.distortion_processor.process(processed_data, effect['parameters']) - effects_processed += 1 - elif effect_type == 'spatial' and self.spatial_processor: - processed_data = self.spatial_processor.process(processed_data, effect['parameters']) - effects_processed += 1 - - # 更新统计 - self.performance_stats['effects_processed'] += effects_processed - - return processed_data - +""" +音频混响和效果插件主文件 +提供专业的音频混响和效果处理功能 +""" + +import math +import time +import uuid +from typing import Dict, List, Optional, Any, Callable +from plugins.plugin_manager import BasePlugin +from panda3d.core import AudioSound, NodePath + +class Plugin(BasePlugin): + """ + 音频混响和效果插件 + 提供专业的音频混响和效果处理功能 + """ + + def __init__(self, plugin_manager, name): + super().__init__(plugin_manager, name) + self.config = { + "version": "1.0.0", + "author": "EG Team", + "description": "专业的音频混响和效果处理插件,支持多种高级音频效果" + } + + # 核心组件 + self.audio_manager = None + self.listener = None + + # 效果处理器 + self.reverb_processor = None + self.eq_processor = None + self.compressor_processor = None + self.modulation_processor = None + self.distortion_processor = None + self.spatial_processor = None + + # 效果管理 + self.effects = {} # 所有效果 + self.sound_effects = {} # 音效与效果的关联 + self.presets = {} # 效果预设 + + # GUI元素 + self.gui_elements = [] + + # 事件处理器 + self.event_handlers = {} + + # 插件状态 + self.is_initialized = False + self.is_enabled = False + self.processing_enabled = True + + # 性能监控 + self.performance_stats = { + 'initialization_time': 0.0, + 'enable_time': 0.0, + 'last_update_time': 0.0, + 'total_updates': 0, + 'total_processing_time': 0.0, + 'effects_processed': 0, + 'memory_usage': 0 + } + + # 配置参数 + self.master_volume = 1.0 + self.quality_settings = 'high' # low, medium, high + self.max_effects_per_sound = 8 + self.buffer_size = 4096 + + def initialize(self) -> bool: + """ + 初始化插件 + """ + try: + if self.is_initialized: + return True + + init_start_time = time.time() + print(f"初始化音频混响和效果插件: {self.name}") + + # 初始化音频管理器 + self.audio_manager = self.plugin_manager.world.audio_manager + + # 初始化听者 + self.listener = self.plugin_manager.world.camera + + # 初始化各效果处理器 + from .effects.reverb_processor import ReverbProcessor + from .effects.eq_processor import EQProcessor + from .effects.compressor_processor import CompressorProcessor + from .effects.modulation_processor import ModulationProcessor + from .effects.distortion_processor import DistortionProcessor + from .effects.spatial_processor import SpatialProcessor + + self.reverb_processor = ReverbProcessor(self) + self.eq_processor = EQProcessor(self) + self.compressor_processor = CompressorProcessor(self) + self.modulation_processor = ModulationProcessor(self) + self.distortion_processor = DistortionProcessor(self) + self.spatial_processor = SpatialProcessor(self) + + # 加载预设 + self._load_presets() + + self.is_initialized = True + self.performance_stats['initialization_time'] = time.time() - init_start_time + print("✓ 音频混响和效果插件初始化完成") + return True + + except Exception as e: + print(f"✗ 音频混响和效果插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + """ + if not super().enable(): + return False + + if self.is_enabled: + return True + + try: + enable_start_time = time.time() + print(f"启用音频混响和效果插件: {self.name}") + + # 注册事件处理器 + self._register_event_handlers() + + # 创建GUI界面 + self._create_gui() + + self.is_enabled = True + self.performance_stats['enable_time'] = time.time() - enable_start_time + print("✓ 音频混响和效果插件启用成功") + return True + + except Exception as e: + print(f"✗ 音频混响和效果插件启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self) -> bool: + """ + 禁用插件 + """ + if not super().disable(): + return False + + if not self.is_enabled: + return True + + try: + print(f"禁用音频混响和效果插件: {self.name}") + + # 清理GUI界面 + self._cleanup_gui() + + # 移除事件处理器 + self._unregister_event_handlers() + + # 停止所有效果处理 + self._stop_all_processing() + + self.is_enabled = False + print("✓ 音频混响和效果插件禁用成功") + return True + + except Exception as e: + print(f"✗ 音频混响和效果插件禁用失败: {e}") + import traceback + traceback.print_exc() + return False + + def finalize(self): + """ + 清理插件资源 + """ + print(f"清理音频混响和效果插件资源: {self.name}") + + # 停止所有处理 + self._stop_all_processing() + + # 清理所有组件 + processors = [ + self.reverb_processor, + self.eq_processor, + self.compressor_processor, + self.modulation_processor, + self.distortion_processor, + self.spatial_processor + ] + + for processor in processors: + if processor: + processor.cleanup() + + self.reverb_processor = None + self.eq_processor = None + self.compressor_processor = None + self.modulation_processor = None + self.distortion_processor = None + self.spatial_processor = None + + self.audio_manager = None + self.listener = None + self.effects.clear() + self.sound_effects.clear() + self.presets.clear() + + self.is_initialized = False + self.is_enabled = False + + def _register_event_handlers(self): + """ + 注册事件处理器 + """ + world = self.plugin_manager.world + + # 音频效果控制快捷键 + self.event_handlers['f1'] = self.toggle_debug_mode + self.event_handlers['f2'] = self.toggle_processing + self.event_handlers['f3'] = self.apply_test_reverb + self.event_handlers['f4'] = self.apply_test_eq + self.event_handlers['f5'] = self.show_stats + self.event_handlers['f6'] = self.apply_test_compressor + self.event_handlers['f7'] = self.apply_test_modulation + self.event_handlers['f8'] = self.apply_test_distortion + self.event_handlers['f9'] = self.create_test_preset + self.event_handlers['f10'] = self.toggle_quality + self.event_handlers['f11'] = self.show_detailed_stats + self.event_handlers['f12'] = self.reset_all_effects + + # 注册事件 + for event, handler in self.event_handlers.items(): + world.accept(event, handler) + world.accept(event.upper(), handler) # 同时注册大写事件 + + def _unregister_event_handlers(self): + """ + 移除事件处理器 + """ + world = self.plugin_manager.world + + for event in self.event_handlers.keys(): + world.ignore(event) + world.ignore(event.upper()) + + self.event_handlers.clear() + + def _create_gui(self): + """ + 创建GUI界面 + """ + try: + world = self.plugin_manager.world + + # 检查是否有GUI管理器 + if not hasattr(world, 'gui_manager') or not world.gui_manager: + print("⚠ GUI管理器不可用,跳过GUI创建") + return + + gui_manager = world.gui_manager + + # 创建音频效果控制面板 + panel_button = gui_manager.createGUIButton( + pos=(0.7, 0, 0.35), + text="音频效果", + size=0.06 + ) + self.gui_elements.append(panel_button) + + # 创建功能按钮 + functions = [ + ("调试模式 [F1]", self.toggle_debug_mode), + ("切换处理 [F2]", self.toggle_processing), + ("测试混响 [F3]", self.apply_test_reverb), + ("测试EQ [F4]", self.apply_test_eq), + ("效果统计 [F5]", self.show_stats), + ("测试压缩 [F6]", self.apply_test_compressor), + ("测试调制 [F7]", self.apply_test_modulation), + ("测试失真 [F8]", self.apply_test_distortion), + ("测试预设 [F9]", self.create_test_preset), + ("切换质量 [F10]", self.toggle_quality), + ("详细统计 [F11]", self.show_detailed_stats), + ("重置效果 [F12]", self.reset_all_effects) + ] + + for i, (name, callback) in enumerate(functions): + button = gui_manager.createGUIButton( + pos=(0.7, 0, 0.28 - i * 0.055), + text=name, + size=0.045 + ) + self.gui_elements.append(button) + world.accept(f"audio_effect_function_{i}", callback) + + except Exception as e: + print(f"⚠ GUI创建失败: {e}") + + def _cleanup_gui(self): + """ + 清理GUI界面 + """ + try: + world = self.plugin_manager.world + + if hasattr(world, 'gui_manager') and world.gui_manager: + gui_manager = world.gui_manager + + # 删除所有GUI元素 + for element in self.gui_elements: + gui_manager.deleteGUIElement(element) + + self.gui_elements.clear() + + except Exception as e: + print(f"⚠ GUI清理失败: {e}") + + # 事件处理方法 + def toggle_debug_mode(self): + """切换调试模式""" + print("✓ 音频效果调试模式切换") + + def toggle_processing(self): + """切换效果处理""" + self.processing_enabled = not self.processing_enabled + print(f"✓ 音频效果处理已{'启用' if self.processing_enabled else '禁用'}") + + def apply_test_reverb(self): + """应用测试混响效果""" + print("✓ 应用测试混响效果") + try: + # 创建一个测试混响效果 + reverb_params = { + 'room_size': 0.8, + 'damping': 0.5, + 'wet_level': 0.3, + 'dry_level': 0.7, + 'width': 1.0, + 'freeze_mode': 0.0 + } + effect_id = self.create_reverb_effect(reverb_params) + if effect_id: + print(f"✓ 测试混响效果创建成功: {effect_id}") + else: + print("✗ 测试混响效果创建失败") + except Exception as e: + print(f"✗ 应用测试混响效果失败: {e}") + + def apply_test_eq(self): + """应用测试均衡器效果""" + print("✓ 应用测试均衡器效果") + try: + # 创建一个测试均衡器效果 + eq_params = { + 'bands': { + 60: 2.0, # 60Hz 提升2dB + 200: -1.0, # 200Hz 降低1dB + 1000: 3.0, # 1kHz 提升3dB + 5000: -2.0, # 5kHz 降低2dB + 10000: 1.0 # 10kHz 提升1dB + } + } + effect_id = self.create_eq_effect(eq_params) + if effect_id: + print(f"✓ 测试均衡器效果创建成功: {effect_id}") + else: + print("✗ 测试均衡器效果创建失败") + except Exception as e: + print(f"✗ 应用测试均衡器效果失败: {e}") + + def show_stats(self): + """显示效果统计信息""" + stats = self.get_stats() + print("=== 音频效果统计信息 ===") + for key, value in stats.items(): + print(f" {key}: {value}") + + def apply_test_compressor(self): + """应用测试压缩器效果""" + print("✓ 应用测试压缩器效果") + try: + # 创建一个测试压缩器效果 + compressor_params = { + 'threshold': -20.0, + 'ratio': 4.0, + 'attack': 0.01, + 'release': 0.1, + 'gain': 2.0, + 'knee': 5.0 + } + effect_id = self.create_compressor_effect(compressor_params) + if effect_id: + print(f"✓ 测试压缩器效果创建成功: {effect_id}") + else: + print("✗ 测试压缩器效果创建失败") + except Exception as e: + print(f"✗ 应用测试压缩器效果失败: {e}") + + def apply_test_modulation(self): + """应用测试调制效果""" + print("✓ 应用测试调制效果") + try: + # 创建一个测试合唱效果 + modulation_params = { + 'type': 'chorus', + 'rate': 1.0, + 'depth': 0.5, + 'feedback': 0.3, + 'mix': 0.5, + 'delay': 0.01 + } + effect_id = self.create_modulation_effect(modulation_params) + if effect_id: + print(f"✓ 测试调制效果创建成功: {effect_id}") + else: + print("✗ 测试调制效果创建失败") + except Exception as e: + print(f"✗ 应用测试调制效果失败: {e}") + + def apply_test_distortion(self): + """应用测试失真效果""" + print("✓ 应用测试失真效果") + try: + # 创建一个测试失真效果 + distortion_params = { + 'drive': 0.5, + 'tone': 0.8, + 'level': 0.7, + 'bias': 0.0 + } + effect_id = self.create_distortion_effect(distortion_params) + if effect_id: + print(f"✓ 测试失真效果创建成功: {effect_id}") + else: + print("✗ 测试失真效果创建失败") + except Exception as e: + print(f"✗ 应用测试失真效果失败: {e}") + + def create_test_preset(self): + """创建测试预设""" + print("✓ 创建测试预设") + try: + preset_name = f"test_preset_{int(time.time())}" + preset_data = { + 'reverb': { + 'room_size': 0.7, + 'damping': 0.6, + 'wet_level': 0.4, + 'dry_level': 0.6 + }, + 'eq': { + 'bands': { + 100: 2.0, + 1000: -1.0, + 8000: 1.5 + } + }, + 'compressor': { + 'threshold': -15.0, + 'ratio': 3.0 + } + } + + success = self.create_preset(preset_name, preset_data) + if success: + print(f"✓ 测试预设创建成功: {preset_name}") + else: + print("✗ 测试预设创建失败") + except Exception as e: + print(f"✗ 创建测试预设失败: {e}") + + def toggle_quality(self): + """切换音质设置""" + quality_levels = ['low', 'medium', 'high'] + current_index = quality_levels.index(self.quality_settings) + next_index = (current_index + 1) % len(quality_levels) + self.quality_settings = quality_levels[next_index] + print(f"✓ 音质设置已切换到: {self.quality_settings}") + + def show_detailed_stats(self): + """显示详细统计信息""" + print("=== 音频效果详细统计信息 ===") + + # 显示各处理器统计 + processors = [ + ('混响处理器', self.reverb_processor), + ('均衡器处理器', self.eq_processor), + ('压缩器处理器', self.compressor_processor), + ('调制处理器', self.modulation_processor), + ('失真处理器', self.distortion_processor), + ('空间处理器', self.spatial_processor) + ] + + for name, processor in processors: + if processor: + processor_stats = processor.get_stats() + print(f"{name}统计:") + for key, value in processor_stats.items(): + print(f" {key}: {value}") + + # 显示性能统计 + perf_stats = self.performance_stats + print("性能统计:") + for key, value in perf_stats.items(): + print(f" {key}: {value}") + + def reset_all_effects(self): + """重置所有效果""" + print("✓ 重置所有音频效果") + self.effects.clear() + self.sound_effects.clear() + print("✓ 所有音频效果已重置") + + def _stop_all_processing(self): + """停止所有效果处理""" + print("✓ 停止所有音频效果处理") + + def _load_presets(self): + """加载预设效果""" + # 默认预设 + self.presets['small_room'] = { + 'reverb': { + 'room_size': 0.3, + 'damping': 0.7, + 'wet_level': 0.2, + 'dry_level': 0.8, + 'width': 0.8 + } + } + + self.presets['large_hall'] = { + 'reverb': { + 'room_size': 0.9, + 'damping': 0.4, + 'wet_level': 0.5, + 'dry_level': 0.5, + 'width': 1.0 + } + } + + self.presets['cathedral'] = { + 'reverb': { + 'room_size': 1.0, + 'damping': 0.2, + 'wet_level': 0.7, + 'dry_level': 0.3, + 'width': 1.0 + } + } + + self.presets['bathroom'] = { + 'reverb': { + 'room_size': 0.2, + 'damping': 0.1, + 'wet_level': 0.8, + 'dry_level': 0.2, + 'width': 0.9 + } + } + + print("✓ 预设效果加载完成") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取插件统计信息 + + Returns: + 统计信息字典 + """ + stats = self.performance_stats.copy() + + # 收集各处理器统计 + processors = [ + self.reverb_processor, + self.eq_processor, + self.compressor_processor, + self.modulation_processor, + self.distortion_processor, + self.spatial_processor + ] + + processor_names = [ + 'reverb', 'eq', 'compressor', 'modulation', 'distortion', 'spatial' + ] + + for i, processor in enumerate(processors): + if processor: + processor_stats = processor.get_stats() + for key, value in processor_stats.items(): + stats[f"{processor_names[i]}_{key}"] = value + + # 添加效果统计 + stats['total_effects'] = len(self.effects) + stats['active_effects'] = sum(1 for effect in self.effects.values() if effect.get('active', True)) + stats['effect_assignments'] = sum(len(effect_list) for effect_list in self.sound_effects.values()) + + return stats + + # 公共接口方法 + def create_reverb_effect(self, parameters: Dict[str, Any]) -> str: + """ + 创建混响效果 + + Args: + parameters: 混响参数 + + Returns: + 效果ID + """ + if self.reverb_processor: + return self.reverb_processor.create_effect(parameters) + return "" + + def create_eq_effect(self, parameters: Dict[str, Any]) -> str: + """ + 创建均衡器效果 + + Args: + parameters: 均衡器参数 + + Returns: + 效果ID + """ + if self.eq_processor: + return self.eq_processor.create_effect(parameters) + return "" + + def create_compressor_effect(self, parameters: Dict[str, Any]) -> str: + """ + 创建压缩器效果 + + Args: + parameters: 压缩器参数 + + Returns: + 效果ID + """ + if self.compressor_processor: + return self.compressor_processor.create_effect(parameters) + return "" + + def create_modulation_effect(self, parameters: Dict[str, Any]) -> str: + """ + 创建调制效果 + + Args: + parameters: 调制参数 + + Returns: + 效果ID + """ + if self.modulation_processor: + return self.modulation_processor.create_effect(parameters) + return "" + + def create_distortion_effect(self, parameters: Dict[str, Any]) -> str: + """ + 创建失真效果 + + Args: + parameters: 失真参数 + + Returns: + 效果ID + """ + if self.distortion_processor: + return self.distortion_processor.create_effect(parameters) + return "" + + def create_spatial_effect(self, parameters: Dict[str, Any]) -> str: + """ + 创建空间效果 + + Args: + parameters: 空间效果参数 + + Returns: + 效果ID + """ + if self.spatial_processor: + return self.spatial_processor.create_effect(parameters) + return "" + + def delete_effect(self, effect_id: str) -> bool: + """ + 删除效果 + + Args: + effect_id: 效果ID + + Returns: + 是否删除成功 + """ + if effect_id not in self.effects: + print(f"✗ 效果不存在: {effect_id}") + return False + + try: + # 从所有音效中移除该效果 + for sound_id, effect_list in self.sound_effects.items(): + if effect_id in effect_list: + effect_list.remove(effect_id) + + # 删除效果 + del self.effects[effect_id] + + print(f"✓ 音频效果已删除: {effect_id}") + return True + + except Exception as e: + print(f"✗ 删除音频效果失败: {e}") + return False + + def apply_effect_to_sound(self, sound_id: str, effect_id: str) -> bool: + """ + 应用效果到音效 + + Args: + sound_id: 音源ID + effect_id: 效果ID + + Returns: + 是否应用成功 + """ + # 检查音源和效果是否存在 + # 注意:这里需要与主音频系统集成 + if effect_id not in self.effects: + print(f"✗ 效果不存在: {effect_id}") + return False + + try: + # 初始化音效的效果列表 + if sound_id not in self.sound_effects: + self.sound_effects[sound_id] = [] + + # 检查是否超过最大效果数 + if len(self.sound_effects[sound_id]) >= self.max_effects_per_sound: + print(f"✗ 音效已达到最大效果数限制: {self.max_effects_per_sound}") + return False + + # 添加效果到音效 + if effect_id not in self.sound_effects[sound_id]: + self.sound_effects[sound_id].append(effect_id) + + effect_type = self.effects[effect_id]['type'] + print(f"✓ 效果已应用到音效: {effect_id} ({effect_type}) -> {sound_id}") + return True + else: + print(f"⚠ 效果已应用于该音效: {effect_id} -> {sound_id}") + return True + + except Exception as e: + print(f"✗ 应用效果到音效失败: {e}") + return False + + def remove_effect_from_sound(self, sound_id: str, effect_id: str) -> bool: + """ + 从音效移除效果 + + Args: + sound_id: 音源ID + effect_id: 效果ID + + Returns: + 是否移除成功 + """ + # 检查音源和效果是否存在 + if effect_id not in self.effects: + print(f"✗ 效果不存在: {effect_id}") + return False + + try: + # 检查效果是否已应用于该音效 + if sound_id in self.sound_effects and effect_id in self.sound_effects[sound_id]: + self.sound_effects[sound_id].remove(effect_id) + + effect_type = self.effects[effect_id]['type'] + print(f"✓ 效果已从音效移除: {effect_id} ({effect_type}) -> {sound_id}") + return True + else: + print(f"⚠ 效果未应用于该音效: {effect_id} -> {sound_id}") + return True + + except Exception as e: + print(f"✗ 从音效移除效果失败: {e}") + return False + + def set_effect_parameter(self, effect_id: str, parameter: str, value: Any) -> bool: + """ + 设置效果参数 + + Args: + effect_id: 效果ID + parameter: 参数名 + value: 参数值 + + Returns: + 是否设置成功 + """ + if effect_id not in self.effects: + print(f"✗ 效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['parameters'][parameter] = value + print(f"✓ 效果参数已设置: {effect_id}.{parameter} = {value}") + return True + + except Exception as e: + print(f"✗ 设置效果参数失败: {e}") + return False + + def get_effect_parameters(self, effect_id: str) -> Dict[str, Any]: + """ + 获取效果参数 + + Args: + effect_id: 效果ID + + Returns: + 参数字典 + """ + if effect_id not in self.effects: + return {} + + return self.effects[effect_id]['parameters'].copy() + + def enable_effect(self, effect_id: str) -> bool: + """ + 启用效果 + + Args: + effect_id: 效果ID + + Returns: + 是否启用成功 + """ + if effect_id not in self.effects: + print(f"✗ 效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['active'] = True + print(f"✓ 效果已启用: {effect_id}") + return True + + except Exception as e: + print(f"✗ 启用效果失败: {e}") + return False + + def disable_effect(self, effect_id: str) -> bool: + """ + 禁用效果 + + Args: + effect_id: 效果ID + + Returns: + 是否禁用成功 + """ + if effect_id not in self.effects: + print(f"✗ 效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['active'] = False + print(f"✓ 效果已禁用: {effect_id}") + return True + + except Exception as e: + print(f"✗ 禁用效果失败: {e}") + return False + + def get_effect_type(self, effect_id: str) -> str: + """ + 获取效果类型 + + Args: + effect_id: 效果ID + + Returns: + 效果类型 + """ + if effect_id not in self.effects: + return "" + + return self.effects[effect_id]['type'] + + def get_sound_effects(self, sound_id: str) -> List[str]: + """ + 获取音效应用的效果列表 + + Args: + sound_id: 音源ID + + Returns: + 效果ID列表 + """ + return self.sound_effects.get(sound_id, []).copy() + + def create_preset(self, preset_name: str, preset_data: Dict[str, Any]) -> bool: + """ + 创建效果预设 + + Args: + preset_name: 预设名称 + preset_data: 预设数据 + + Returns: + 是否创建成功 + """ + try: + self.presets[preset_name] = preset_data.copy() + print(f"✓ 效果预设创建成功: {preset_name}") + return True + except Exception as e: + print(f"✗ 创建效果预设失败: {e}") + return False + + def apply_preset_to_sound(self, sound_id: str, preset_name: str) -> bool: + """ + 应用预设到音效 + + Args: + sound_id: 音源ID + preset_name: 预设名称 + + Returns: + 是否应用成功 + """ + if preset_name not in self.presets: + print(f"✗ 预设不存在: {preset_name}") + return False + + try: + preset_data = self.presets[preset_name] + created_effects = [] + + # 应用混响预设 + if 'reverb' in preset_data: + effect_id = self.create_reverb_effect(preset_data['reverb']) + if effect_id: + created_effects.append(('reverb', effect_id)) + + # 应用均衡器预设 + if 'eq' in preset_data: + effect_id = self.create_eq_effect(preset_data['eq']) + if effect_id: + created_effects.append(('eq', effect_id)) + + # 应用压缩器预设 + if 'compressor' in preset_data: + effect_id = self.create_compressor_effect(preset_data['compressor']) + if effect_id: + created_effects.append(('compressor', effect_id)) + + # 将所有创建的效果应用到音效 + success_count = 0 + for effect_type, effect_id in created_effects: + if self.apply_effect_to_sound(sound_id, effect_id): + success_count += 1 + + print(f"✓ 预设 {preset_name} 已应用到音效 {sound_id} ({success_count}/{len(created_effects)} 效果)") + return success_count > 0 + + except Exception as e: + print(f"✗ 应用预设到音效失败: {e}") + return False + + def get_preset_names(self) -> List[str]: + """ + 获取所有预设名称 + + Returns: + 预设名称列表 + """ + return list(self.presets.keys()) + + def delete_preset(self, preset_name: str) -> bool: + """ + 删除预设 + + Args: + preset_name: 预设名称 + + Returns: + 是否删除成功 + """ + if preset_name not in self.presets: + print(f"✗ 预设不存在: {preset_name}") + return False + + try: + del self.presets[preset_name] + print(f"✓ 预设已删除: {preset_name}") + return True + except Exception as e: + print(f"✗ 删除预设失败: {e}") + return False + + def set_master_volume(self, volume: float): + """ + 设置主音量 + + Args: + volume: 主音量 (0.0-1.0) + """ + self.master_volume = max(0.0, min(1.0, volume)) + print(f"✓ 主音量已设置: {self.master_volume}") + + def get_master_volume(self) -> float: + """ + 获取主音量 + + Returns: + 主音量 (0.0-1.0) + """ + return self.master_volume + + def set_quality_settings(self, quality: str): + """ + 设置音质设置 + + Args: + quality: 音质等级 ('low', 'medium', 'high') + """ + if quality in ['low', 'medium', 'high']: + self.quality_settings = quality + print(f"✓ 音质设置已更新: {quality}") + else: + print(f"✗ 无效的音质设置: {quality}") + + def get_quality_settings(self) -> str: + """ + 获取音质设置 + + Returns: + 音质等级 + """ + return self.quality_settings + + def update(self, dt: float): + """ + 更新插件状态 + + Args: + dt: 时间增量 + """ + update_start_time = time.time() + + # 更新各效果处理器 + processors = [ + self.reverb_processor, + self.eq_processor, + self.compressor_processor, + self.modulation_processor, + self.distortion_processor, + self.spatial_processor + ] + + for processor in processors: + if processor: + processor.update(dt) + + # 更新性能统计 + self.performance_stats['total_updates'] += 1 + self.performance_stats['last_update_time'] = time.time() + self.performance_stats['total_processing_time'] += (time.time() - update_start_time) + + def process_audio_with_effects(self, sound_id: str, audio_data: Any) -> Any: + """ + 使用效果处理音频数据 + + Args: + sound_id: 音源ID + audio_data: 音频数据 + + Returns: + 处理后的音频数据 + """ + if not self.processing_enabled: + return audio_data + + # 获取应用到该音效的所有效果 + effect_ids = self.sound_effects.get(sound_id, []) + + if not effect_ids: + return audio_data + + # 按顺序应用效果 + processed_data = audio_data + effects_processed = 0 + + for effect_id in effect_ids: + if effect_id in self.effects and self.effects[effect_id].get('active', True): + effect = self.effects[effect_id] + effect_type = effect['type'] + + # 根据效果类型调用相应的处理器 + if effect_type == 'reverb' and self.reverb_processor: + processed_data = self.reverb_processor.process(processed_data, effect['parameters']) + effects_processed += 1 + elif effect_type == 'eq' and self.eq_processor: + processed_data = self.eq_processor.process(processed_data, effect['parameters']) + effects_processed += 1 + elif effect_type == 'compressor' and self.compressor_processor: + processed_data = self.compressor_processor.process(processed_data, effect['parameters']) + effects_processed += 1 + elif effect_type == 'modulation' and self.modulation_processor: + processed_data = self.modulation_processor.process(processed_data, effect['parameters']) + effects_processed += 1 + elif effect_type == 'distortion' and self.distortion_processor: + processed_data = self.distortion_processor.process(processed_data, effect['parameters']) + effects_processed += 1 + elif effect_type == 'spatial' and self.spatial_processor: + processed_data = self.spatial_processor.process(processed_data, effect['parameters']) + effects_processed += 1 + + # 更新统计 + self.performance_stats['effects_processed'] += effects_processed + + return processed_data + diff --git a/plugins/user/behavior_tree/README.md b/plugins/user/behavior_tree/README.md index c9b975c9..021b518b 100644 --- a/plugins/user/behavior_tree/README.md +++ b/plugins/user/behavior_tree/README.md @@ -1,87 +1,87 @@ -# 行为树插件使用文档 - -## 简介 - -行为树插件提供了一套完整的行为树系统,用于实现复杂的AI决策逻辑。行为树是一种广泛应用于游戏开发中的AI编程模式,它通过树状结构组织和管理AI的行为逻辑。 - -## 核心概念 - -### 节点类型 - -1. **控制节点**:控制行为树的执行流程 - - 选择节点(Selector):从左到右执行子节点,直到有一个成功 - - 序列节点(Sequence):从左到右执行子节点,直到有一个失败 - - 并行节点(Parallel):并行执行所有子节点 - -2. **装饰节点**:修改子节点行为的节点 - - 重复节点(Repeater):重复执行子节点指定次数 - - 逆变节点(Inverter):反转子节点的执行结果 - - 成功节点(Succeeder):总是返回成功 - - 失败节点(Failure):总是返回失败 - -3. **叶子节点**:执行具体行为的节点 - - 条件节点(Condition):检查条件是否满足 - - 动作节点(Action):执行具体行为 - - 等待节点(Wait):等待指定时间 - -### 黑板系统 - -黑板是行为树节点间共享数据的机制,类似于全局变量存储区。 - -## 使用方法 - -### 1. 加载和启用插件 - -```python -# 在主程序中加载插件 -plugin_manager.load_plugin("behavior_tree") -plugin_manager.enable_plugin("behavior_tree") - -# 获取插件实例 -bt_plugin = plugin_manager.get_plugin("behavior_tree") -``` - -### 2. 创建行为树 - -```python -# 创建根节点 -root = SelectorNode("Root") - -# 创建序列节点 -sequence = SequenceNode("Patrol") - -# 创建条件节点 -condition = ConditionNode("NeedPatrol", lambda bb: bb.get("need_patrol", True)) - -# 创建动作节点 -action = ActionNode("PatrolAction", lambda bb: print("执行巡逻") or True) - -# 组装行为树结构 -sequence.add_child(condition) -sequence.add_child(action) -root.add_child(sequence) - -# 创建行为树 -tree = bt_plugin.create_behavior_tree("patrol_tree", root) - -# 创建黑板 -blackboard = bt_plugin.create_blackboard("patrol_blackboard") - -# 设置黑板数据 -blackboard.set("need_patrol", True) - -# 关联黑板到行为树 -tree.set_blackboard(blackboard) -``` - -### 3. 执行行为树 - -```python -# 执行行为树 -result = tree.run() -print(f"执行结果: {result.value}") -``` - -## 示例 - +# 行为树插件使用文档 + +## 简介 + +行为树插件提供了一套完整的行为树系统,用于实现复杂的AI决策逻辑。行为树是一种广泛应用于游戏开发中的AI编程模式,它通过树状结构组织和管理AI的行为逻辑。 + +## 核心概念 + +### 节点类型 + +1. **控制节点**:控制行为树的执行流程 + - 选择节点(Selector):从左到右执行子节点,直到有一个成功 + - 序列节点(Sequence):从左到右执行子节点,直到有一个失败 + - 并行节点(Parallel):并行执行所有子节点 + +2. **装饰节点**:修改子节点行为的节点 + - 重复节点(Repeater):重复执行子节点指定次数 + - 逆变节点(Inverter):反转子节点的执行结果 + - 成功节点(Succeeder):总是返回成功 + - 失败节点(Failure):总是返回失败 + +3. **叶子节点**:执行具体行为的节点 + - 条件节点(Condition):检查条件是否满足 + - 动作节点(Action):执行具体行为 + - 等待节点(Wait):等待指定时间 + +### 黑板系统 + +黑板是行为树节点间共享数据的机制,类似于全局变量存储区。 + +## 使用方法 + +### 1. 加载和启用插件 + +```python +# 在主程序中加载插件 +plugin_manager.load_plugin("behavior_tree") +plugin_manager.enable_plugin("behavior_tree") + +# 获取插件实例 +bt_plugin = plugin_manager.get_plugin("behavior_tree") +``` + +### 2. 创建行为树 + +```python +# 创建根节点 +root = SelectorNode("Root") + +# 创建序列节点 +sequence = SequenceNode("Patrol") + +# 创建条件节点 +condition = ConditionNode("NeedPatrol", lambda bb: bb.get("need_patrol", True)) + +# 创建动作节点 +action = ActionNode("PatrolAction", lambda bb: print("执行巡逻") or True) + +# 组装行为树结构 +sequence.add_child(condition) +sequence.add_child(action) +root.add_child(sequence) + +# 创建行为树 +tree = bt_plugin.create_behavior_tree("patrol_tree", root) + +# 创建黑板 +blackboard = bt_plugin.create_blackboard("patrol_blackboard") + +# 设置黑板数据 +blackboard.set("need_patrol", True) + +# 关联黑板到行为树 +tree.set_blackboard(blackboard) +``` + +### 3. 执行行为树 + +```python +# 执行行为树 +result = tree.run() +print(f"执行结果: {result.value}") +``` + +## 示例 + 查看 `examples/patrol_example.py` 文件了解完整的使用示例。 \ No newline at end of file diff --git a/plugins/user/behavior_tree/__init__.py b/plugins/user/behavior_tree/__init__.py index 5c67f5ff..c65f6045 100644 --- a/plugins/user/behavior_tree/__init__.py +++ b/plugins/user/behavior_tree/__init__.py @@ -1,26 +1,26 @@ -""" -行为树插件__init__.py -""" - -# 导入主要的类和函数,方便外部使用 -from .plugin import Plugin -from .core.behavior_tree import ( - BTNode, NodeStatus, BehaviorTree, - BTNodeConfig, NodeMemory, BehaviorTreeManager -) -from .core.blackboard import Blackboard, BlackboardManager -from .nodes.control_nodes import ( - SelectorNode, SequenceNode, ParallelNode, - RandomSelectorNode, PrioritySelectorNode -) -from .nodes.decorator_nodes import ( - RepeaterNode, InverterNode, SucceederNode, - FailureNode, TimerNode, CooldownNode, RandomizerNode -) -from .nodes.leaf_nodes import ( - ConditionNode, ActionNode, WaitNode, - SubtreeNode, ProbabilityNode, CounterNode, TimeoutNode -) - -# 定义插件类,保持与plugin.py中的一致 +""" +行为树插件__init__.py +""" + +# 导入主要的类和函数,方便外部使用 +from .plugin import Plugin +from .core.behavior_tree import ( + BTNode, NodeStatus, BehaviorTree, + BTNodeConfig, NodeMemory, BehaviorTreeManager +) +from .core.blackboard import Blackboard, BlackboardManager +from .nodes.control_nodes import ( + SelectorNode, SequenceNode, ParallelNode, + RandomSelectorNode, PrioritySelectorNode +) +from .nodes.decorator_nodes import ( + RepeaterNode, InverterNode, SucceederNode, + FailureNode, TimerNode, CooldownNode, RandomizerNode +) +from .nodes.leaf_nodes import ( + ConditionNode, ActionNode, WaitNode, + SubtreeNode, ProbabilityNode, CounterNode, TimeoutNode +) + +# 定义插件类,保持与plugin.py中的一致 Plugin = Plugin \ No newline at end of file diff --git a/plugins/user/behavior_tree/core/behavior_tree.py b/plugins/user/behavior_tree/core/behavior_tree.py index 6f0163b0..19bcbdc1 100644 --- a/plugins/user/behavior_tree/core/behavior_tree.py +++ b/plugins/user/behavior_tree/core/behavior_tree.py @@ -1,463 +1,463 @@ -""" -行为树核心组件 -定义行为树的基本结构和执行逻辑 -包含完整的节点系统、黑板系统和执行管理 -""" - -import time -import math -from enum import Enum -from abc import ABC, abstractmethod -from typing import Dict, List, Any, Optional, Callable, Union -from dataclasses import dataclass, field - -class NodeStatus(Enum): - """ - 节点执行状态 - """ - SUCCESS = "success" - FAILURE = "failure" - RUNNING = "running" - -class NodeMemory: - """ - 节点内存 - 存储节点状态信息,支持节点间的记忆功能 - """ - - def __init__(self): - self.data: Dict[str, Any] = {} - self.last_execution_time: float = 0.0 - self.execution_count: int = 0 - self.running_child_index: int = 0 # 用于控制节点跟踪当前执行的子节点 - - def set(self, key: str, value: Any) -> None: - """设置内存值""" - self.data[key] = value - - def get(self, key: str, default: Any = None) -> Any: - """获取内存值""" - return self.data.get(key, default) - - def has(self, key: str) -> bool: - """检查内存是否存在键""" - return key in self.data - - def clear(self) -> None: - """清空内存""" - self.data.clear() - - def increment_execution_count(self) -> int: - """增加执行计数并返回新计数""" - self.execution_count += 1 - return self.execution_count - -@dataclass -class BTNodeConfig: - """ - 节点配置 - """ - name: str = "BTNode" - priority: int = 0 # 优先级,数字越大优先级越高 - max_execution_time: Optional[float] = None # 最大执行时间 - can_be_interrupted: bool = True # 是否可被中断 - -class BTNode(ABC): - """ - 行为树节点基类 - """ - - def __init__(self, config: Union[BTNodeConfig, str] = "BTNode"): - if isinstance(config, str): - config = BTNodeConfig(name=config) - self.config = config - self.children: List['BTNode'] = [] - self.status = NodeStatus.RUNNING - self.memory = NodeMemory() - self.parent: Optional['BTNode'] = None - self.last_execution_time: float = 0.0 - self.execution_count: int = 0 - - @property - def name(self) -> str: - """获取节点名称""" - return self.config.name - - def add_child(self, child: 'BTNode') -> None: - """添加子节点""" - child.parent = self - self.children.append(child) - - def insert_child(self, index: int, child: 'BTNode') -> None: - """在指定位置插入子节点""" - child.parent = self - self.children.insert(index, child) - - def remove_child(self, child: 'BTNode') -> bool: - """移除子节点""" - if child in self.children: - child.parent = None - self.children.remove(child) - return True - return False - - def get_children(self) -> List['BTNode']: - """获取子节点列表""" - return self.children.copy() - - def get_child_count(self) -> int: - """获取子节点数量""" - return len(self.children) - - def clear_children(self) -> None: - """清空所有子节点""" - for child in self.children: - child.parent = None - self.children.clear() - - def get_root(self) -> 'BTNode': - """获取根节点""" - current = self - while current.parent: - current = current.parent - return current - - def get_path(self) -> List[int]: - """获取从根节点到当前节点的路径索引列表""" - path = [] - current = self - while current.parent: - index = current.parent.children.index(current) - path.append(index) - current = current.parent - path.reverse() - return path - - def find_child_by_name(self, name: str) -> Optional['BTNode']: - """根据名称查找子节点""" - for child in self.children: - if child.name == name: - return child - # 递归搜索 - result = child.find_child_by_name(name) - if result: - return result - return None - - def reset(self) -> None: - """重置节点状态""" - self.status = NodeStatus.RUNNING - self.memory.clear() - self.last_execution_time = 0.0 - self.execution_count = 0 - # 递归重置子节点 - for child in self.children: - child.reset() - - @abstractmethod - def execute(self, blackboard) -> NodeStatus: - """ - 执行节点逻辑 - :param blackboard: 黑板对象,用于数据共享 - :return: NodeStatus 执行状态 - """ - start_time = time.time() - self.execution_count += 1 - - # 检查是否超过最大执行时间 - if self.config.max_execution_time: - if time.time() - start_time > self.config.max_execution_time: - return NodeStatus.FAILURE - - self.last_execution_time = time.time() - start_time - return NodeStatus.SUCCESS - - def on_enter(self, blackboard) -> None: - """节点进入时调用""" - pass - - def on_exit(self, blackboard, status: NodeStatus) -> None: - """节点退出时调用""" - pass - - def on_abort(self, blackboard) -> None: - """节点被中断时调用""" - pass - -class DecoratorNode(BTNode): - """ - 装饰节点基类 - 只能有一个子节点,用于修饰子节点的行为 - """ - - def __init__(self, child: Optional['BTNode'] = None, config: Union[BTNodeConfig, str] = "DecoratorNode"): - super().__init__(config) - self.child = child - if child: - child.parent = self - - def set_child(self, child: 'BTNode') -> None: - """设置子节点""" - if self.child: - self.child.parent = None - self.child = child - if child: - child.parent = self - - def get_children(self) -> List['BTNode']: - """获取子节点列表""" - if self.child: - return [self.child] - return [] - - def get_child_count(self) -> int: - """获取子节点数量""" - return 1 if self.child else 0 - - def execute(self, blackboard) -> NodeStatus: - """执行装饰节点逻辑""" - start_time = time.time() - self.execution_count += 1 - - # 检查是否超过最大执行时间 - if self.config.max_execution_time: - if time.time() - start_time > self.config.max_execution_time: - return NodeStatus.FAILURE - - if not self.child: - self.last_execution_time = time.time() - start_time - return NodeStatus.FAILURE - - child_status = self.child.execute(blackboard) - result = self._decorate(child_status) - - self.last_execution_time = time.time() - start_time - return result - - @abstractmethod - def _decorate(self, child_status: NodeStatus) -> NodeStatus: - """ - 装饰子节点的执行结果 - :param child_status: 子节点执行状态 - :return: 装饰后的执行状态 - """ - pass - -class CompositeNode(BTNode): - """ - 组合节点基类 - 有多个子节点,定义子节点的执行顺序和逻辑 - """ - - def __init__(self, config: Union[BTNodeConfig, str] = "CompositeNode"): - super().__init__(config) - self.current_child_index = 0 - - def execute(self, blackboard) -> NodeStatus: - """执行组合节点逻辑""" - start_time = time.time() - self.execution_count += 1 - - # 检查是否超过最大执行时间 - if self.config.max_execution_time: - if time.time() - start_time > self.config.max_execution_time: - return NodeStatus.FAILURE - - if not self.children: - self.last_execution_time = time.time() - start_time - return NodeStatus.FAILURE - - result = self._execute_child_logic(blackboard) - - self.last_execution_time = time.time() - start_time - return result - - @abstractmethod - def _execute_child_logic(self, blackboard) -> NodeStatus: - """ - 执行子节点逻辑 - :param blackboard: 黑板对象 - :return: 执行结果状态 - """ - pass - -class BehaviorTree: - """ - 行为树类 - """ - - def __init__(self, name: str, root_node: BTNode): - self.name = name - self.root_node = root_node - self.blackboard = None - self.is_running = False - self.start_time = 0.0 - self.last_execution_time = 0.0 - self.execution_count = 0 - self.status = NodeStatus.RUNNING - self.custom_functions: Dict[str, Callable] = {} - - def set_blackboard(self, blackboard) -> None: - """设置黑板""" - self.blackboard = blackboard - - def run(self) -> NodeStatus: - """运行行为树""" - if not self.root_node or not self.blackboard: - return NodeStatus.FAILURE - - self.is_running = True - self.start_time = time.time() - self.execution_count += 1 - - try: - self.status = self.root_node.execute(self.blackboard) - except Exception as e: - print(f"行为树执行出错: {e}") - self.status = NodeStatus.FAILURE - import traceback - traceback.print_exc() - - self.last_execution_time = time.time() - self.start_time - self.is_running = False - - return self.status - - def execute(self) -> NodeStatus: - """执行行为树(同run方法)""" - return self.run() - - def reset(self) -> None: - """重置行为树""" - if self.root_node: - self.root_node.reset() - self.is_running = False - self.status = NodeStatus.RUNNING - self.execution_count = 0 - - def add_custom_function(self, name: str, func: Callable) -> None: - """添加自定义函数""" - self.custom_functions[name] = func - - def get_custom_function(self, name: str) -> Optional[Callable]: - """获取自定义函数""" - return self.custom_functions.get(name) - - def get_node_by_path(self, path: List[int]) -> Optional[BTNode]: - """根据路径获取节点""" - current = self.root_node - for index in path: - if 0 <= index < len(current.children): - current = current.children[index] - else: - return None - return current - - def get_statistics(self) -> Dict[str, Any]: - """获取行为树统计信息""" - def collect_node_stats(node: BTNode, stats: Dict[str, Any]) -> None: - node_type = type(node).__name__ - if node_type not in stats['node_types']: - stats['node_types'][node_type] = {'count': 0, 'total_executions': 0, 'avg_execution_time': 0} - - stats['node_types'][node_type]['count'] += 1 - stats['node_types'][node_type]['total_executions'] += node.execution_count - stats['total_nodes'] += 1 - stats['total_executions'] += node.execution_count - - for child in node.children: - collect_node_stats(child, stats) - - stats = { - 'name': self.name, - 'status': self.status.value, - 'is_running': self.is_running, - 'execution_count': self.execution_count, - 'last_execution_time': self.last_execution_time, - 'total_nodes': 0, - 'total_executions': 0, - 'node_types': {} - } - - if self.root_node: - collect_node_stats(self.root_node, stats) - - return stats - -class BehaviorTreeManager: - """ - 行为树管理器 - 用于管理多个行为树的生命周期 - """ - - def __init__(self): - self.trees: Dict[str, BehaviorTree] = {} - self.active_trees: List[str] = [] - self.update_interval = 0.016 # 默认每帧更新(假设60FPS) - self.last_update_time = 0.0 - - def add_tree(self, name: str, tree: BehaviorTree) -> bool: - """添加行为树""" - if name in self.trees: - return False - self.trees[name] = tree - return True - - def remove_tree(self, name: str) -> bool: - """移除行为树""" - if name in self.trees: - del self.trees[name] - if name in self.active_trees: - self.active_trees.remove(name) - return True - return False - - def get_tree(self, name: str) -> Optional[BehaviorTree]: - """获取行为树""" - return self.trees.get(name) - - def activate_tree(self, name: str) -> bool: - """激活行为树""" - if name in self.trees and name not in self.active_trees: - self.active_trees.append(name) - return True - return False - - def deactivate_tree(self, name: str) -> bool: - """停用行为树""" - if name in self.active_trees: - self.active_trees.remove(name) - return True - return False - - def update_all(self, dt: float = None) -> None: - """更新所有激活的行为树""" - current_time = time.time() - if dt is None: - dt = current_time - self.last_update_time - self.last_update_time = current_time - - # 只更新激活的树 - for tree_name in self.active_trees.copy(): - tree = self.trees.get(tree_name) - if tree and not tree.is_running: - tree.run() - - def get_tree_stats(self, name: str) -> Optional[Dict[str, Any]]: - """获取特定树的统计信息""" - tree = self.get_tree(name) - if tree: - return tree.get_statistics() - return None - - def get_all_stats(self) -> Dict[str, Dict[str, Any]]: - """获取所有树的统计信息""" - stats = {} - for name, tree in self.trees.items(): - stats[name] = tree.get_statistics() - return stats - - def reset_all(self) -> None: - """重置所有树""" - for tree in self.trees.values(): +""" +行为树核心组件 +定义行为树的基本结构和执行逻辑 +包含完整的节点系统、黑板系统和执行管理 +""" + +import time +import math +from enum import Enum +from abc import ABC, abstractmethod +from typing import Dict, List, Any, Optional, Callable, Union +from dataclasses import dataclass, field + +class NodeStatus(Enum): + """ + 节点执行状态 + """ + SUCCESS = "success" + FAILURE = "failure" + RUNNING = "running" + +class NodeMemory: + """ + 节点内存 + 存储节点状态信息,支持节点间的记忆功能 + """ + + def __init__(self): + self.data: Dict[str, Any] = {} + self.last_execution_time: float = 0.0 + self.execution_count: int = 0 + self.running_child_index: int = 0 # 用于控制节点跟踪当前执行的子节点 + + def set(self, key: str, value: Any) -> None: + """设置内存值""" + self.data[key] = value + + def get(self, key: str, default: Any = None) -> Any: + """获取内存值""" + return self.data.get(key, default) + + def has(self, key: str) -> bool: + """检查内存是否存在键""" + return key in self.data + + def clear(self) -> None: + """清空内存""" + self.data.clear() + + def increment_execution_count(self) -> int: + """增加执行计数并返回新计数""" + self.execution_count += 1 + return self.execution_count + +@dataclass +class BTNodeConfig: + """ + 节点配置 + """ + name: str = "BTNode" + priority: int = 0 # 优先级,数字越大优先级越高 + max_execution_time: Optional[float] = None # 最大执行时间 + can_be_interrupted: bool = True # 是否可被中断 + +class BTNode(ABC): + """ + 行为树节点基类 + """ + + def __init__(self, config: Union[BTNodeConfig, str] = "BTNode"): + if isinstance(config, str): + config = BTNodeConfig(name=config) + self.config = config + self.children: List['BTNode'] = [] + self.status = NodeStatus.RUNNING + self.memory = NodeMemory() + self.parent: Optional['BTNode'] = None + self.last_execution_time: float = 0.0 + self.execution_count: int = 0 + + @property + def name(self) -> str: + """获取节点名称""" + return self.config.name + + def add_child(self, child: 'BTNode') -> None: + """添加子节点""" + child.parent = self + self.children.append(child) + + def insert_child(self, index: int, child: 'BTNode') -> None: + """在指定位置插入子节点""" + child.parent = self + self.children.insert(index, child) + + def remove_child(self, child: 'BTNode') -> bool: + """移除子节点""" + if child in self.children: + child.parent = None + self.children.remove(child) + return True + return False + + def get_children(self) -> List['BTNode']: + """获取子节点列表""" + return self.children.copy() + + def get_child_count(self) -> int: + """获取子节点数量""" + return len(self.children) + + def clear_children(self) -> None: + """清空所有子节点""" + for child in self.children: + child.parent = None + self.children.clear() + + def get_root(self) -> 'BTNode': + """获取根节点""" + current = self + while current.parent: + current = current.parent + return current + + def get_path(self) -> List[int]: + """获取从根节点到当前节点的路径索引列表""" + path = [] + current = self + while current.parent: + index = current.parent.children.index(current) + path.append(index) + current = current.parent + path.reverse() + return path + + def find_child_by_name(self, name: str) -> Optional['BTNode']: + """根据名称查找子节点""" + for child in self.children: + if child.name == name: + return child + # 递归搜索 + result = child.find_child_by_name(name) + if result: + return result + return None + + def reset(self) -> None: + """重置节点状态""" + self.status = NodeStatus.RUNNING + self.memory.clear() + self.last_execution_time = 0.0 + self.execution_count = 0 + # 递归重置子节点 + for child in self.children: + child.reset() + + @abstractmethod + def execute(self, blackboard) -> NodeStatus: + """ + 执行节点逻辑 + :param blackboard: 黑板对象,用于数据共享 + :return: NodeStatus 执行状态 + """ + start_time = time.time() + self.execution_count += 1 + + # 检查是否超过最大执行时间 + if self.config.max_execution_time: + if time.time() - start_time > self.config.max_execution_time: + return NodeStatus.FAILURE + + self.last_execution_time = time.time() - start_time + return NodeStatus.SUCCESS + + def on_enter(self, blackboard) -> None: + """节点进入时调用""" + pass + + def on_exit(self, blackboard, status: NodeStatus) -> None: + """节点退出时调用""" + pass + + def on_abort(self, blackboard) -> None: + """节点被中断时调用""" + pass + +class DecoratorNode(BTNode): + """ + 装饰节点基类 + 只能有一个子节点,用于修饰子节点的行为 + """ + + def __init__(self, child: Optional['BTNode'] = None, config: Union[BTNodeConfig, str] = "DecoratorNode"): + super().__init__(config) + self.child = child + if child: + child.parent = self + + def set_child(self, child: 'BTNode') -> None: + """设置子节点""" + if self.child: + self.child.parent = None + self.child = child + if child: + child.parent = self + + def get_children(self) -> List['BTNode']: + """获取子节点列表""" + if self.child: + return [self.child] + return [] + + def get_child_count(self) -> int: + """获取子节点数量""" + return 1 if self.child else 0 + + def execute(self, blackboard) -> NodeStatus: + """执行装饰节点逻辑""" + start_time = time.time() + self.execution_count += 1 + + # 检查是否超过最大执行时间 + if self.config.max_execution_time: + if time.time() - start_time > self.config.max_execution_time: + return NodeStatus.FAILURE + + if not self.child: + self.last_execution_time = time.time() - start_time + return NodeStatus.FAILURE + + child_status = self.child.execute(blackboard) + result = self._decorate(child_status) + + self.last_execution_time = time.time() - start_time + return result + + @abstractmethod + def _decorate(self, child_status: NodeStatus) -> NodeStatus: + """ + 装饰子节点的执行结果 + :param child_status: 子节点执行状态 + :return: 装饰后的执行状态 + """ + pass + +class CompositeNode(BTNode): + """ + 组合节点基类 + 有多个子节点,定义子节点的执行顺序和逻辑 + """ + + def __init__(self, config: Union[BTNodeConfig, str] = "CompositeNode"): + super().__init__(config) + self.current_child_index = 0 + + def execute(self, blackboard) -> NodeStatus: + """执行组合节点逻辑""" + start_time = time.time() + self.execution_count += 1 + + # 检查是否超过最大执行时间 + if self.config.max_execution_time: + if time.time() - start_time > self.config.max_execution_time: + return NodeStatus.FAILURE + + if not self.children: + self.last_execution_time = time.time() - start_time + return NodeStatus.FAILURE + + result = self._execute_child_logic(blackboard) + + self.last_execution_time = time.time() - start_time + return result + + @abstractmethod + def _execute_child_logic(self, blackboard) -> NodeStatus: + """ + 执行子节点逻辑 + :param blackboard: 黑板对象 + :return: 执行结果状态 + """ + pass + +class BehaviorTree: + """ + 行为树类 + """ + + def __init__(self, name: str, root_node: BTNode): + self.name = name + self.root_node = root_node + self.blackboard = None + self.is_running = False + self.start_time = 0.0 + self.last_execution_time = 0.0 + self.execution_count = 0 + self.status = NodeStatus.RUNNING + self.custom_functions: Dict[str, Callable] = {} + + def set_blackboard(self, blackboard) -> None: + """设置黑板""" + self.blackboard = blackboard + + def run(self) -> NodeStatus: + """运行行为树""" + if not self.root_node or not self.blackboard: + return NodeStatus.FAILURE + + self.is_running = True + self.start_time = time.time() + self.execution_count += 1 + + try: + self.status = self.root_node.execute(self.blackboard) + except Exception as e: + print(f"行为树执行出错: {e}") + self.status = NodeStatus.FAILURE + import traceback + traceback.print_exc() + + self.last_execution_time = time.time() - self.start_time + self.is_running = False + + return self.status + + def execute(self) -> NodeStatus: + """执行行为树(同run方法)""" + return self.run() + + def reset(self) -> None: + """重置行为树""" + if self.root_node: + self.root_node.reset() + self.is_running = False + self.status = NodeStatus.RUNNING + self.execution_count = 0 + + def add_custom_function(self, name: str, func: Callable) -> None: + """添加自定义函数""" + self.custom_functions[name] = func + + def get_custom_function(self, name: str) -> Optional[Callable]: + """获取自定义函数""" + return self.custom_functions.get(name) + + def get_node_by_path(self, path: List[int]) -> Optional[BTNode]: + """根据路径获取节点""" + current = self.root_node + for index in path: + if 0 <= index < len(current.children): + current = current.children[index] + else: + return None + return current + + def get_statistics(self) -> Dict[str, Any]: + """获取行为树统计信息""" + def collect_node_stats(node: BTNode, stats: Dict[str, Any]) -> None: + node_type = type(node).__name__ + if node_type not in stats['node_types']: + stats['node_types'][node_type] = {'count': 0, 'total_executions': 0, 'avg_execution_time': 0} + + stats['node_types'][node_type]['count'] += 1 + stats['node_types'][node_type]['total_executions'] += node.execution_count + stats['total_nodes'] += 1 + stats['total_executions'] += node.execution_count + + for child in node.children: + collect_node_stats(child, stats) + + stats = { + 'name': self.name, + 'status': self.status.value, + 'is_running': self.is_running, + 'execution_count': self.execution_count, + 'last_execution_time': self.last_execution_time, + 'total_nodes': 0, + 'total_executions': 0, + 'node_types': {} + } + + if self.root_node: + collect_node_stats(self.root_node, stats) + + return stats + +class BehaviorTreeManager: + """ + 行为树管理器 + 用于管理多个行为树的生命周期 + """ + + def __init__(self): + self.trees: Dict[str, BehaviorTree] = {} + self.active_trees: List[str] = [] + self.update_interval = 0.016 # 默认每帧更新(假设60FPS) + self.last_update_time = 0.0 + + def add_tree(self, name: str, tree: BehaviorTree) -> bool: + """添加行为树""" + if name in self.trees: + return False + self.trees[name] = tree + return True + + def remove_tree(self, name: str) -> bool: + """移除行为树""" + if name in self.trees: + del self.trees[name] + if name in self.active_trees: + self.active_trees.remove(name) + return True + return False + + def get_tree(self, name: str) -> Optional[BehaviorTree]: + """获取行为树""" + return self.trees.get(name) + + def activate_tree(self, name: str) -> bool: + """激活行为树""" + if name in self.trees and name not in self.active_trees: + self.active_trees.append(name) + return True + return False + + def deactivate_tree(self, name: str) -> bool: + """停用行为树""" + if name in self.active_trees: + self.active_trees.remove(name) + return True + return False + + def update_all(self, dt: float = None) -> None: + """更新所有激活的行为树""" + current_time = time.time() + if dt is None: + dt = current_time - self.last_update_time + self.last_update_time = current_time + + # 只更新激活的树 + for tree_name in self.active_trees.copy(): + tree = self.trees.get(tree_name) + if tree and not tree.is_running: + tree.run() + + def get_tree_stats(self, name: str) -> Optional[Dict[str, Any]]: + """获取特定树的统计信息""" + tree = self.get_tree(name) + if tree: + return tree.get_statistics() + return None + + def get_all_stats(self) -> Dict[str, Dict[str, Any]]: + """获取所有树的统计信息""" + stats = {} + for name, tree in self.trees.items(): + stats[name] = tree.get_statistics() + return stats + + def reset_all(self) -> None: + """重置所有树""" + for tree in self.trees.values(): tree.reset() \ No newline at end of file diff --git a/plugins/user/behavior_tree/core/blackboard.py b/plugins/user/behavior_tree/core/blackboard.py index b3f27b41..35e5ad92 100644 --- a/plugins/user/behavior_tree/core/blackboard.py +++ b/plugins/user/behavior_tree/core/blackboard.py @@ -1,466 +1,466 @@ -""" -黑板系统 -用于行为树节点间的数据共享 -包含高级数据管理功能和事件系统 -""" - -import json -import pickle -import threading -from typing import Any, Dict, List, Optional, Callable, Union -from datetime import datetime -from copy import deepcopy - -class DataChangeType: - """数据变更类型枚举""" - SET = "set" - UPDATE = "update" - DELETE = "delete" - CLEAR = "clear" - -class DataChangeEvent: - """数据变更事件""" - - def __init__(self, key: str, old_value: Any, new_value: Any, - change_type: str, timestamp: datetime = None): - self.key = key - self.old_value = old_value - self.new_value = new_value - self.change_type = change_type - self.timestamp = timestamp or datetime.now() - - def to_dict(self) -> Dict[str, Any]: - """转换为字典""" - return { - 'key': self.key, - 'old_value': self.old_value, - 'new_value': self.new_value, - 'change_type': self.change_type, - 'timestamp': self.timestamp.isoformat() - } - -class Blackboard: - """ - 黑板系统 - 用于行为树节点间的数据共享 - """ - - def __init__(self, name: str = "GlobalBlackboard", - max_history_size: int = 1000, - enable_history: bool = True, - enable_validation: bool = False): - self.name = name - self.data: Dict[str, Any] = {} - self.lock = threading.RLock() # 线程安全锁 - self.data_types: Dict[str, type] = {} # 数据类型约束 - self.validators: Dict[str, Callable[[Any], bool]] = {} # 数据验证器 - self.callbacks: Dict[str, List[Callable]] = {} # 数据变更回调 - self.history: List[DataChangeEvent] = [] # 数据变更历史 - self.max_history_size = max_history_size - self.enable_history = enable_history - self.enable_validation = enable_validation - self.timestamp = datetime.now() - - def set(self, key: str, value: Any, validate: bool = True) -> bool: - """设置键值对""" - with self.lock: - old_value = self.data.get(key) - - # 验证数据类型 - if self.enable_validation and key in self.data_types: - if not isinstance(value, self.data_types[key]): - raise TypeError(f"Key '{key}' requires value of type {self.data_types[key]}, got {type(value)}") - - # 验证数据 - if validate and key in self.validators: - if not self.validators[key](value): - raise ValueError(f"Value for key '{key}' failed validation") - - self.data[key] = value - - # 记录历史 - if self.enable_history: - event = DataChangeEvent(key, old_value, value, DataChangeType.SET) - self.history.append(event) - if len(self.history) > self.max_history_size: - self.history.pop(0) - - # 触发回调 - self._trigger_callbacks(key, old_value, value, DataChangeType.SET) - - return True - - def get(self, key: str, default: Any = None) -> Any: - """获取键对应的值""" - with self.lock: - return self.data.get(key, default) - - def get_typed(self, key: str, expected_type: type, default: Any = None) -> Any: - """获取特定类型的值""" - with self.lock: - value = self.data.get(key, default) - if value is not None and not isinstance(value, expected_type): - try: - # 尝试类型转换 - if expected_type == int: - return int(value) - elif expected_type == float: - return float(value) - elif expected_type == str: - return str(value) - elif expected_type == bool: - if isinstance(value, str): - return value.lower() in ['true', '1', 'yes', 'on'] - return bool(value) - else: - # 尝试直接转换 - return expected_type(value) - except (ValueError, TypeError): - return default - return value - - def has(self, key: str) -> bool: - """检查键是否存在""" - with self.lock: - return key in self.data - - def remove(self, key: str) -> bool: - """移除键值对""" - with self.lock: - if key in self.data: - old_value = self.data[key] - del self.data[key] - - # 记录历史 - if self.enable_history: - event = DataChangeEvent(key, old_value, None, DataChangeType.DELETE) - self.history.append(event) - if len(self.history) > self.max_history_size: - self.history.pop(0) - - # 触发回调 - self._trigger_callbacks(key, old_value, None, DataChangeType.DELETE) - - return True - return False - - def clear(self) -> None: - """清空所有数据""" - with self.lock: - old_data = self.data.copy() - self.data.clear() - - # 记录历史 - if self.enable_history: - for key, value in old_data.items(): - event = DataChangeEvent(key, value, None, DataChangeType.CLEAR) - self.history.append(event) - if len(self.history) > self.max_history_size: - self.history.pop(0) - - # 触发回调 - for key, value in old_data.items(): - self._trigger_callbacks(key, value, None, DataChangeType.CLEAR) - - def update(self, data_dict: Dict[str, Any], validate: bool = True) -> int: - """批量更新数据,返回成功更新的项数""" - with self.lock: - success_count = 0 - for key, value in data_dict.items(): - try: - self.set(key, value, validate=validate) - success_count += 1 - except (TypeError, ValueError): - continue # 跳过验证失败的项 - return success_count - - def get_sub_blackboard(self, prefix: str, include_nested: bool = True) -> 'Blackboard': - """获取子黑板,包含指定前缀的所有键""" - with self.lock: - sub_board = Blackboard(f"{self.name}_{prefix}_sub", - max_history_size=self.max_history_size, - enable_history=self.enable_history) - - for key, value in self.data.items(): - if key.startswith(prefix): - # 如果不包含嵌套键,只获取直接匹配的键 - if not include_nested and '.' in key[len(prefix):]: - continue - sub_key = key[len(prefix):].lstrip('.') - sub_board.set(sub_key, value) - - return sub_board - - def set_type_constraint(self, key: str, data_type: type) -> None: - """设置键的数据类型约束""" - with self.lock: - self.data_types[key] = data_type - - def set_validator(self, key: str, validator: Callable[[Any], bool]) -> None: - """设置键的数据验证器""" - with self.lock: - self.validators[key] = validator - - def add_callback(self, key: str, callback: Callable[[str, Any, Any, str], None]) -> None: - """添加数据变更回调""" - with self.lock: - if key not in self.callbacks: - self.callbacks[key] = [] - self.callbacks[key].append(callback) - - def remove_callback(self, key: str, callback: Callable[[str, Any, Any, str], None]) -> bool: - """移除数据变更回调""" - with self.lock: - if key in self.callbacks and callback in self.callbacks[key]: - self.callbacks[key].remove(callback) - return True - return False - - def _trigger_callbacks(self, key: str, old_value: Any, new_value: Any, change_type: str) -> None: - """触发回调函数""" - if key in self.callbacks: - for callback in self.callbacks[key]: - try: - callback(key, old_value, new_value, change_type) - except Exception as e: - print(f"回调执行出错: {e}") - - # 触发通配符回调('__all__'键表示所有变更) - if '__all__' in self.callbacks: - for callback in self.callbacks['__all__']: - try: - callback(key, old_value, new_value, change_type) - except Exception as e: - print(f"通配符回调执行出错: {e}") - - def get_keys_by_pattern(self, pattern: str) -> List[str]: - """根据模式获取匹配的键列表(支持简单通配符)""" - import fnmatch - with self.lock: - return [key for key in self.data.keys() if fnmatch.fnmatch(key, pattern)] - - def get_keys_by_prefix(self, prefix: str) -> List[str]: - """根据前缀获取键列表""" - with self.lock: - return [key for key in self.data.keys() if key.startswith(prefix)] - - def get_keys_by_type(self, data_type: type) -> List[str]: - """根据类型获取键列表""" - with self.lock: - result = [] - for key, value in self.data.items(): - if isinstance(value, data_type): - result.append(key) - return result - - def get_history(self, key: str = None, limit: int = None) -> List[DataChangeEvent]: - """获取变更历史""" - with self.lock: - if key: - history = [event for event in self.history if event.key == key] - else: - history = self.history.copy() - - if limit: - return history[-limit:] - return history - - def clear_history(self) -> None: - """清空历史记录""" - with self.lock: - self.history.clear() - - def get_size(self) -> int: - """获取数据大小(项数)""" - with self.lock: - return len(self.data) - - def get_memory_usage(self) -> int: - """获取内存使用量估算(字节)""" - with self.lock: - import sys - size = sys.getsizeof(self.data) - for key, value in self.data.items(): - size += sys.getsizeof(key) - size += sys.getsizeof(value) - return size - - def to_dict(self) -> Dict[str, Any]: - """转换为字典(深拷贝)""" - with self.lock: - return deepcopy(self.data) - - def from_dict(self, data: Dict[str, Any], clear_first: bool = False) -> None: - """从字典加载数据""" - with self.lock: - if clear_first: - self.data.clear() - self.data.update(data) - - def to_json(self, indent: int = 2) -> str: - """转换为JSON字符串""" - with self.lock: - try: - # 首先尝试使用深拷贝来防止序列化问题 - data_copy = deepcopy(self.data) - return json.dumps(data_copy, indent=indent, default=str) - except TypeError: - # 如果某些对象无法序列化,转换为字符串 - safe_data = {} - for k, v in self.data.items(): - try: - json.dumps(v) # 测试是否可序列化 - safe_data[k] = v - except TypeError: - safe_data[k] = str(v) # 转换为字符串 - return json.dumps(safe_data, indent=indent) - - def from_json(self, json_str: str, clear_first: bool = False) -> bool: - """从JSON字符串加载数据""" - with self.lock: - try: - data = json.loads(json_str) - if clear_first: - self.data.clear() - self.data.update(data) - return True - except json.JSONDecodeError: - return False - - def to_pickle(self) -> bytes: - """序列化为pickle格式""" - with self.lock: - return pickle.dumps(self.data) - - def from_pickle(self, pickle_bytes: bytes, clear_first: bool = False) -> bool: - """从pickle格式反序列化""" - with self.lock: - try: - data = pickle.loads(pickle_bytes) - if clear_first: - self.data.clear() - self.data.update(data) - return True - except pickle.PickleError: - return False - - def get_timestamp(self) -> datetime: - """获取时间戳""" - with self.lock: - return self.timestamp - - def update_timestamp(self) -> None: - """更新时间戳""" - with self.lock: - self.timestamp = datetime.now() - - def get_statistics(self) -> Dict[str, Any]: - """获取黑板统计信息""" - with self.lock: - type_counts = {} - for value in self.data.values(): - type_name = type(value).__name__ - type_counts[type_name] = type_counts.get(type_name, 0) + 1 - - return { - 'name': self.name, - 'size': self.get_size(), - 'memory_usage': self.get_memory_usage(), - 'timestamp': self.timestamp.isoformat() if self.timestamp else None, - 'type_distribution': type_counts, - 'history_size': len(self.history), - 'max_history_size': self.max_history_size, - 'data_types_count': len(self.data_types), - 'validators_count': len(self.validators), - 'callbacks_count': sum(len(cbs) for cbs in self.callbacks.values()) - } - - def copy(self) -> 'Blackboard': - """创建黑板副本""" - with self.lock: - new_board = Blackboard( - name=f"{self.name}_copy", - max_history_size=self.max_history_size, - enable_history=self.enable_history, - enable_validation=self.enable_validation - ) - new_board.data = deepcopy(self.data) - new_board.data_types = self.data_types.copy() - new_board.validators = self.validators.copy() - new_board.callbacks = {k: v.copy() for k, v in self.callbacks.items()} - return new_board - -class BlackboardManager: - """ - 黑板管理器 - 用于管理多个黑板实例 - """ - - def __init__(self): - self.blackboards: Dict[str, Blackboard] = {} - self.default_blackboard: Optional[Blackboard] = None - - def create_blackboard(self, name: str, **kwargs) -> Blackboard: - """创建新的黑板""" - if name in self.blackboards: - raise ValueError(f"Blackboard with name '{name}' already exists") - - blackboard = Blackboard(name, **kwargs) - self.blackboards[name] = blackboard - - # 如果是第一个黑板,设为默认 - if not self.default_blackboard: - self.default_blackboard = blackboard - - return blackboard - - def get_blackboard(self, name: str) -> Optional[Blackboard]: - """获取指定名称的黑板""" - return self.blackboards.get(name) - - def get_or_create(self, name: str, **kwargs) -> Blackboard: - """获取或创建黑板""" - if name in self.blackboards: - return self.blackboards[name] - return self.create_blackboard(name, **kwargs) - - def set_default(self, name: str) -> bool: - """设置默认黑板""" - if name in self.blackboards: - self.default_blackboard = self.blackboards[name] - return True - return False - - def get_default(self) -> Optional[Blackboard]: - """获取默认黑板""" - return self.default_blackboard - - def remove_blackboard(self, name: str) -> bool: - """移除黑板""" - if name in self.blackboards: - del self.blackboards[name] - # 如果删除的是默认黑板,重新设置默认值 - if self.default_blackboard and self.default_blackboard.name == name: - if self.blackboards: - self.default_blackboard = next(iter(self.blackboards.values())) - else: - self.default_blackboard = None - return True - return False - - def get_all_names(self) -> List[str]: - """获取所有黑板名称""" - return list(self.blackboards.keys()) - - def get_statistics(self) -> Dict[str, Any]: - """获取管理器统计信息""" - stats = { - 'total_blackboards': len(self.blackboards), - 'default_blackboard': self.default_blackboard.name if self.default_blackboard else None, - 'blackboard_stats': {} - } - - for name, board in self.blackboards.items(): - stats['blackboard_stats'][name] = board.get_statistics() - +""" +黑板系统 +用于行为树节点间的数据共享 +包含高级数据管理功能和事件系统 +""" + +import json +import pickle +import threading +from typing import Any, Dict, List, Optional, Callable, Union +from datetime import datetime +from copy import deepcopy + +class DataChangeType: + """数据变更类型枚举""" + SET = "set" + UPDATE = "update" + DELETE = "delete" + CLEAR = "clear" + +class DataChangeEvent: + """数据变更事件""" + + def __init__(self, key: str, old_value: Any, new_value: Any, + change_type: str, timestamp: datetime = None): + self.key = key + self.old_value = old_value + self.new_value = new_value + self.change_type = change_type + self.timestamp = timestamp or datetime.now() + + def to_dict(self) -> Dict[str, Any]: + """转换为字典""" + return { + 'key': self.key, + 'old_value': self.old_value, + 'new_value': self.new_value, + 'change_type': self.change_type, + 'timestamp': self.timestamp.isoformat() + } + +class Blackboard: + """ + 黑板系统 + 用于行为树节点间的数据共享 + """ + + def __init__(self, name: str = "GlobalBlackboard", + max_history_size: int = 1000, + enable_history: bool = True, + enable_validation: bool = False): + self.name = name + self.data: Dict[str, Any] = {} + self.lock = threading.RLock() # 线程安全锁 + self.data_types: Dict[str, type] = {} # 数据类型约束 + self.validators: Dict[str, Callable[[Any], bool]] = {} # 数据验证器 + self.callbacks: Dict[str, List[Callable]] = {} # 数据变更回调 + self.history: List[DataChangeEvent] = [] # 数据变更历史 + self.max_history_size = max_history_size + self.enable_history = enable_history + self.enable_validation = enable_validation + self.timestamp = datetime.now() + + def set(self, key: str, value: Any, validate: bool = True) -> bool: + """设置键值对""" + with self.lock: + old_value = self.data.get(key) + + # 验证数据类型 + if self.enable_validation and key in self.data_types: + if not isinstance(value, self.data_types[key]): + raise TypeError(f"Key '{key}' requires value of type {self.data_types[key]}, got {type(value)}") + + # 验证数据 + if validate and key in self.validators: + if not self.validators[key](value): + raise ValueError(f"Value for key '{key}' failed validation") + + self.data[key] = value + + # 记录历史 + if self.enable_history: + event = DataChangeEvent(key, old_value, value, DataChangeType.SET) + self.history.append(event) + if len(self.history) > self.max_history_size: + self.history.pop(0) + + # 触发回调 + self._trigger_callbacks(key, old_value, value, DataChangeType.SET) + + return True + + def get(self, key: str, default: Any = None) -> Any: + """获取键对应的值""" + with self.lock: + return self.data.get(key, default) + + def get_typed(self, key: str, expected_type: type, default: Any = None) -> Any: + """获取特定类型的值""" + with self.lock: + value = self.data.get(key, default) + if value is not None and not isinstance(value, expected_type): + try: + # 尝试类型转换 + if expected_type == int: + return int(value) + elif expected_type == float: + return float(value) + elif expected_type == str: + return str(value) + elif expected_type == bool: + if isinstance(value, str): + return value.lower() in ['true', '1', 'yes', 'on'] + return bool(value) + else: + # 尝试直接转换 + return expected_type(value) + except (ValueError, TypeError): + return default + return value + + def has(self, key: str) -> bool: + """检查键是否存在""" + with self.lock: + return key in self.data + + def remove(self, key: str) -> bool: + """移除键值对""" + with self.lock: + if key in self.data: + old_value = self.data[key] + del self.data[key] + + # 记录历史 + if self.enable_history: + event = DataChangeEvent(key, old_value, None, DataChangeType.DELETE) + self.history.append(event) + if len(self.history) > self.max_history_size: + self.history.pop(0) + + # 触发回调 + self._trigger_callbacks(key, old_value, None, DataChangeType.DELETE) + + return True + return False + + def clear(self) -> None: + """清空所有数据""" + with self.lock: + old_data = self.data.copy() + self.data.clear() + + # 记录历史 + if self.enable_history: + for key, value in old_data.items(): + event = DataChangeEvent(key, value, None, DataChangeType.CLEAR) + self.history.append(event) + if len(self.history) > self.max_history_size: + self.history.pop(0) + + # 触发回调 + for key, value in old_data.items(): + self._trigger_callbacks(key, value, None, DataChangeType.CLEAR) + + def update(self, data_dict: Dict[str, Any], validate: bool = True) -> int: + """批量更新数据,返回成功更新的项数""" + with self.lock: + success_count = 0 + for key, value in data_dict.items(): + try: + self.set(key, value, validate=validate) + success_count += 1 + except (TypeError, ValueError): + continue # 跳过验证失败的项 + return success_count + + def get_sub_blackboard(self, prefix: str, include_nested: bool = True) -> 'Blackboard': + """获取子黑板,包含指定前缀的所有键""" + with self.lock: + sub_board = Blackboard(f"{self.name}_{prefix}_sub", + max_history_size=self.max_history_size, + enable_history=self.enable_history) + + for key, value in self.data.items(): + if key.startswith(prefix): + # 如果不包含嵌套键,只获取直接匹配的键 + if not include_nested and '.' in key[len(prefix):]: + continue + sub_key = key[len(prefix):].lstrip('.') + sub_board.set(sub_key, value) + + return sub_board + + def set_type_constraint(self, key: str, data_type: type) -> None: + """设置键的数据类型约束""" + with self.lock: + self.data_types[key] = data_type + + def set_validator(self, key: str, validator: Callable[[Any], bool]) -> None: + """设置键的数据验证器""" + with self.lock: + self.validators[key] = validator + + def add_callback(self, key: str, callback: Callable[[str, Any, Any, str], None]) -> None: + """添加数据变更回调""" + with self.lock: + if key not in self.callbacks: + self.callbacks[key] = [] + self.callbacks[key].append(callback) + + def remove_callback(self, key: str, callback: Callable[[str, Any, Any, str], None]) -> bool: + """移除数据变更回调""" + with self.lock: + if key in self.callbacks and callback in self.callbacks[key]: + self.callbacks[key].remove(callback) + return True + return False + + def _trigger_callbacks(self, key: str, old_value: Any, new_value: Any, change_type: str) -> None: + """触发回调函数""" + if key in self.callbacks: + for callback in self.callbacks[key]: + try: + callback(key, old_value, new_value, change_type) + except Exception as e: + print(f"回调执行出错: {e}") + + # 触发通配符回调('__all__'键表示所有变更) + if '__all__' in self.callbacks: + for callback in self.callbacks['__all__']: + try: + callback(key, old_value, new_value, change_type) + except Exception as e: + print(f"通配符回调执行出错: {e}") + + def get_keys_by_pattern(self, pattern: str) -> List[str]: + """根据模式获取匹配的键列表(支持简单通配符)""" + import fnmatch + with self.lock: + return [key for key in self.data.keys() if fnmatch.fnmatch(key, pattern)] + + def get_keys_by_prefix(self, prefix: str) -> List[str]: + """根据前缀获取键列表""" + with self.lock: + return [key for key in self.data.keys() if key.startswith(prefix)] + + def get_keys_by_type(self, data_type: type) -> List[str]: + """根据类型获取键列表""" + with self.lock: + result = [] + for key, value in self.data.items(): + if isinstance(value, data_type): + result.append(key) + return result + + def get_history(self, key: str = None, limit: int = None) -> List[DataChangeEvent]: + """获取变更历史""" + with self.lock: + if key: + history = [event for event in self.history if event.key == key] + else: + history = self.history.copy() + + if limit: + return history[-limit:] + return history + + def clear_history(self) -> None: + """清空历史记录""" + with self.lock: + self.history.clear() + + def get_size(self) -> int: + """获取数据大小(项数)""" + with self.lock: + return len(self.data) + + def get_memory_usage(self) -> int: + """获取内存使用量估算(字节)""" + with self.lock: + import sys + size = sys.getsizeof(self.data) + for key, value in self.data.items(): + size += sys.getsizeof(key) + size += sys.getsizeof(value) + return size + + def to_dict(self) -> Dict[str, Any]: + """转换为字典(深拷贝)""" + with self.lock: + return deepcopy(self.data) + + def from_dict(self, data: Dict[str, Any], clear_first: bool = False) -> None: + """从字典加载数据""" + with self.lock: + if clear_first: + self.data.clear() + self.data.update(data) + + def to_json(self, indent: int = 2) -> str: + """转换为JSON字符串""" + with self.lock: + try: + # 首先尝试使用深拷贝来防止序列化问题 + data_copy = deepcopy(self.data) + return json.dumps(data_copy, indent=indent, default=str) + except TypeError: + # 如果某些对象无法序列化,转换为字符串 + safe_data = {} + for k, v in self.data.items(): + try: + json.dumps(v) # 测试是否可序列化 + safe_data[k] = v + except TypeError: + safe_data[k] = str(v) # 转换为字符串 + return json.dumps(safe_data, indent=indent) + + def from_json(self, json_str: str, clear_first: bool = False) -> bool: + """从JSON字符串加载数据""" + with self.lock: + try: + data = json.loads(json_str) + if clear_first: + self.data.clear() + self.data.update(data) + return True + except json.JSONDecodeError: + return False + + def to_pickle(self) -> bytes: + """序列化为pickle格式""" + with self.lock: + return pickle.dumps(self.data) + + def from_pickle(self, pickle_bytes: bytes, clear_first: bool = False) -> bool: + """从pickle格式反序列化""" + with self.lock: + try: + data = pickle.loads(pickle_bytes) + if clear_first: + self.data.clear() + self.data.update(data) + return True + except pickle.PickleError: + return False + + def get_timestamp(self) -> datetime: + """获取时间戳""" + with self.lock: + return self.timestamp + + def update_timestamp(self) -> None: + """更新时间戳""" + with self.lock: + self.timestamp = datetime.now() + + def get_statistics(self) -> Dict[str, Any]: + """获取黑板统计信息""" + with self.lock: + type_counts = {} + for value in self.data.values(): + type_name = type(value).__name__ + type_counts[type_name] = type_counts.get(type_name, 0) + 1 + + return { + 'name': self.name, + 'size': self.get_size(), + 'memory_usage': self.get_memory_usage(), + 'timestamp': self.timestamp.isoformat() if self.timestamp else None, + 'type_distribution': type_counts, + 'history_size': len(self.history), + 'max_history_size': self.max_history_size, + 'data_types_count': len(self.data_types), + 'validators_count': len(self.validators), + 'callbacks_count': sum(len(cbs) for cbs in self.callbacks.values()) + } + + def copy(self) -> 'Blackboard': + """创建黑板副本""" + with self.lock: + new_board = Blackboard( + name=f"{self.name}_copy", + max_history_size=self.max_history_size, + enable_history=self.enable_history, + enable_validation=self.enable_validation + ) + new_board.data = deepcopy(self.data) + new_board.data_types = self.data_types.copy() + new_board.validators = self.validators.copy() + new_board.callbacks = {k: v.copy() for k, v in self.callbacks.items()} + return new_board + +class BlackboardManager: + """ + 黑板管理器 + 用于管理多个黑板实例 + """ + + def __init__(self): + self.blackboards: Dict[str, Blackboard] = {} + self.default_blackboard: Optional[Blackboard] = None + + def create_blackboard(self, name: str, **kwargs) -> Blackboard: + """创建新的黑板""" + if name in self.blackboards: + raise ValueError(f"Blackboard with name '{name}' already exists") + + blackboard = Blackboard(name, **kwargs) + self.blackboards[name] = blackboard + + # 如果是第一个黑板,设为默认 + if not self.default_blackboard: + self.default_blackboard = blackboard + + return blackboard + + def get_blackboard(self, name: str) -> Optional[Blackboard]: + """获取指定名称的黑板""" + return self.blackboards.get(name) + + def get_or_create(self, name: str, **kwargs) -> Blackboard: + """获取或创建黑板""" + if name in self.blackboards: + return self.blackboards[name] + return self.create_blackboard(name, **kwargs) + + def set_default(self, name: str) -> bool: + """设置默认黑板""" + if name in self.blackboards: + self.default_blackboard = self.blackboards[name] + return True + return False + + def get_default(self) -> Optional[Blackboard]: + """获取默认黑板""" + return self.default_blackboard + + def remove_blackboard(self, name: str) -> bool: + """移除黑板""" + if name in self.blackboards: + del self.blackboards[name] + # 如果删除的是默认黑板,重新设置默认值 + if self.default_blackboard and self.default_blackboard.name == name: + if self.blackboards: + self.default_blackboard = next(iter(self.blackboards.values())) + else: + self.default_blackboard = None + return True + return False + + def get_all_names(self) -> List[str]: + """获取所有黑板名称""" + return list(self.blackboards.keys()) + + def get_statistics(self) -> Dict[str, Any]: + """获取管理器统计信息""" + stats = { + 'total_blackboards': len(self.blackboards), + 'default_blackboard': self.default_blackboard.name if self.default_blackboard else None, + 'blackboard_stats': {} + } + + for name, board in self.blackboards.items(): + stats['blackboard_stats'][name] = board.get_statistics() + return stats \ No newline at end of file diff --git a/plugins/user/behavior_tree/examples/patrol_example.py b/plugins/user/behavior_tree/examples/patrol_example.py index 470ffa59..5d9b9f06 100644 --- a/plugins/user/behavior_tree/examples/patrol_example.py +++ b/plugins/user/behavior_tree/examples/patrol_example.py @@ -1,89 +1,89 @@ -""" -行为树示例 -演示如何使用行为树插件创建和运行行为树 -""" - -def create_patrol_behavior_tree(plugin): - """ - 创建巡逻行为树示例 - """ - # 导入必要的节点类 - from ..nodes.control_nodes import SelectorNode, SequenceNode - from ..nodes.decorator_nodes import RepeaterNode - from ..nodes.leaf_nodes import ConditionNode, ActionNode - - # 创建根选择节点 - root = SelectorNode("Root") - - # 创建巡逻序列节点 - patrol_sequence = SequenceNode("Patrol") - - # 创建条件节点:检查是否需要巡逻 - need_patrol_condition = ConditionNode("NeedPatrol", - lambda bb: bb.get("need_patrol", True)) - - # 创建动作节点:执行巡逻 - patrol_action = ActionNode("PatrolAction", - lambda bb: print("执行巡逻动作") or True) - - # 创建重复节点:重复巡逻5次 - patrol_repeater = RepeaterNode(patrol_sequence, "PatrolRepeater", count=5) - - # 组装巡逻序列 - patrol_sequence.add_child(need_patrol_condition) - patrol_sequence.add_child(patrol_action) - - # 创建休息序列节点 - rest_sequence = SequenceNode("Rest") - - # 创建条件节点:检查是否需要休息 - need_rest_condition = ConditionNode("NeedRest", - lambda bb: bb.get("need_rest", False)) - - # 创建动作节点:执行休息 - rest_action = ActionNode("RestAction", - lambda bb: print("执行休息动作") or True) - - # 组装休息序列 - rest_sequence.add_child(need_rest_condition) - rest_sequence.add_child(rest_action) - - # 将子节点添加到根节点 - root.add_child(patrol_repeater) - root.add_child(rest_sequence) - - # 创建行为树 - tree = plugin.create_behavior_tree("patrol_tree", root) - - # 创建黑板 - blackboard = plugin.create_blackboard("patrol_blackboard") - - # 设置黑板初始值 - blackboard.set("need_patrol", True) - blackboard.set("need_rest", False) - - # 将黑板关联到行为树 - tree.set_blackboard(blackboard) - - return tree, blackboard - -def run_patrol_example(plugin): - """ - 运行巡逻行为树示例 - """ - print("=== 行为树示例:巡逻AI ===") - - # 创建行为树和黑板 - tree, blackboard = create_patrol_behavior_tree(plugin) - - # 运行行为树 - for i in range(10): - print(f"\n--- 第 {i+1} 次执行 ---") - status = tree.run() - print(f"行为树执行结果: {status.value}") - - # 每次执行后切换状态 - if i % 3 == 2: - blackboard.set("need_patrol", not blackboard.get("need_patrol", True)) - blackboard.set("need_rest", not blackboard.get("need_rest", False)) +""" +行为树示例 +演示如何使用行为树插件创建和运行行为树 +""" + +def create_patrol_behavior_tree(plugin): + """ + 创建巡逻行为树示例 + """ + # 导入必要的节点类 + from ..nodes.control_nodes import SelectorNode, SequenceNode + from ..nodes.decorator_nodes import RepeaterNode + from ..nodes.leaf_nodes import ConditionNode, ActionNode + + # 创建根选择节点 + root = SelectorNode("Root") + + # 创建巡逻序列节点 + patrol_sequence = SequenceNode("Patrol") + + # 创建条件节点:检查是否需要巡逻 + need_patrol_condition = ConditionNode("NeedPatrol", + lambda bb: bb.get("need_patrol", True)) + + # 创建动作节点:执行巡逻 + patrol_action = ActionNode("PatrolAction", + lambda bb: print("执行巡逻动作") or True) + + # 创建重复节点:重复巡逻5次 + patrol_repeater = RepeaterNode(patrol_sequence, "PatrolRepeater", count=5) + + # 组装巡逻序列 + patrol_sequence.add_child(need_patrol_condition) + patrol_sequence.add_child(patrol_action) + + # 创建休息序列节点 + rest_sequence = SequenceNode("Rest") + + # 创建条件节点:检查是否需要休息 + need_rest_condition = ConditionNode("NeedRest", + lambda bb: bb.get("need_rest", False)) + + # 创建动作节点:执行休息 + rest_action = ActionNode("RestAction", + lambda bb: print("执行休息动作") or True) + + # 组装休息序列 + rest_sequence.add_child(need_rest_condition) + rest_sequence.add_child(rest_action) + + # 将子节点添加到根节点 + root.add_child(patrol_repeater) + root.add_child(rest_sequence) + + # 创建行为树 + tree = plugin.create_behavior_tree("patrol_tree", root) + + # 创建黑板 + blackboard = plugin.create_blackboard("patrol_blackboard") + + # 设置黑板初始值 + blackboard.set("need_patrol", True) + blackboard.set("need_rest", False) + + # 将黑板关联到行为树 + tree.set_blackboard(blackboard) + + return tree, blackboard + +def run_patrol_example(plugin): + """ + 运行巡逻行为树示例 + """ + print("=== 行为树示例:巡逻AI ===") + + # 创建行为树和黑板 + tree, blackboard = create_patrol_behavior_tree(plugin) + + # 运行行为树 + for i in range(10): + print(f"\n--- 第 {i+1} 次执行 ---") + status = tree.run() + print(f"行为树执行结果: {status.value}") + + # 每次执行后切换状态 + if i % 3 == 2: + blackboard.set("need_patrol", not blackboard.get("need_patrol", True)) + blackboard.set("need_rest", not blackboard.get("need_rest", False)) print("切换巡逻/休息状态") \ No newline at end of file diff --git a/plugins/user/behavior_tree/nodes/control_nodes.py b/plugins/user/behavior_tree/nodes/control_nodes.py index 62e3b59b..f9cc4c51 100644 --- a/plugins/user/behavior_tree/nodes/control_nodes.py +++ b/plugins/user/behavior_tree/nodes/control_nodes.py @@ -1,396 +1,396 @@ -""" -行为树控制节点 -包括选择节点、序列节点、并行节点等 -提供多种控制流逻辑实现 -""" - -import time -import random -from typing import List, Optional -from ..core.behavior_tree import BTNode, NodeStatus, CompositeNode, BTNodeConfig - -class SelectorNode(CompositeNode): - """ - 选择节点(OR节点) - 从左到右执行子节点,直到有一个成功为止 - 如果所有子节点都失败,则返回失败 - 支持优先级排序和中断机制 - """ - - def __init__(self, config: BTNodeConfig = None): - if config is None: - config = BTNodeConfig(name="Selector") - super().__init__(config) - self.current_child_index = 0 - self.interruptible_children: List[int] = [] # 可中断的子节点索引 - - def add_interruptible_child(self, index: int) -> None: - """添加可中断的子节点索引""" - if index not in self.interruptible_children: - self.interruptible_children.append(index) - - def _execute_child_logic(self, blackboard) -> NodeStatus: - """执行选择节点逻辑""" - # 从当前子节点开始执行 - for i in range(self.current_child_index, len(self.children)): - child = self.children[i] - - # 检查是否可以中断当前运行的子节点 - if (self.current_child_index != i and - self.current_child_index in self.interruptible_children and - hasattr(child, 'config') and child.config.can_be_interrupted): - # 中断之前的子节点 - if hasattr(self.children[self.current_child_index], 'on_abort'): - self.children[self.current_child_index].on_abort(blackboard) - - status = child.execute(blackboard) - - if status == NodeStatus.RUNNING: - # 记录当前正在运行的子节点索引 - self.current_child_index = i - self.memory.set('running_child_index', i) - return NodeStatus.RUNNING - elif status == NodeStatus.SUCCESS: - # 重置当前子节点索引 - self.current_child_index = 0 - self.memory.set('running_child_index', -1) - return NodeStatus.SUCCESS - # 如果是FAILURE,继续执行下一个子节点 - - # 所有子节点都失败 - self.current_child_index = 0 - self.memory.set('running_child_index', -1) - return NodeStatus.FAILURE - - def reset(self) -> None: - """重置节点状态""" - super().reset() - self.current_child_index = 0 - self.memory.set('running_child_index', -1) - -class SequenceNode(CompositeNode): - """ - 序列节点(AND节点) - 从左到右执行子节点,直到有一个失败为止 - 如果所有子节点都成功,则返回成功 - 支持失败重试和条件检查 - """ - - def __init__(self, config: BTNodeConfig = None, max_retries: int = 0): - if config is None: - config = BTNodeConfig(name="Sequence") - super().__init__(config) - self.current_child_index = 0 - self.max_retries = max_retries # 最大重试次数 - self.retry_count = 0 # 当前重试次数 - self.failed_child_index = -1 # 最后失败的子节点索引 - - def _execute_child_logic(self, blackboard) -> NodeStatus: - """执行序列节点逻辑""" - # 检查是否需要重置(所有子节点都成功后再次执行) - if self.current_child_index >= len(self.children): - self.current_child_index = 0 - self.retry_count = 0 - - # 从当前子节点开始执行 - for i in range(self.current_child_index, len(self.children)): - child = self.children[i] - status = child.execute(blackboard) - - if status == NodeStatus.RUNNING: - # 记录当前正在运行的子节点索引 - self.current_child_index = i - self.memory.set('running_child_index', i) - return NodeStatus.RUNNING - elif status == NodeStatus.FAILURE: - # 记录失败的子节点 - self.failed_child_index = i - - # 检查是否可以重试 - if self.retry_count < self.max_retries: - self.retry_count += 1 - # 重置到失败的子节点重新开始 - self.current_child_index = i - return NodeStatus.RUNNING - else: - # 重置状态并返回失败 - self.current_child_index = 0 - self.memory.set('running_child_index', -1) - return NodeStatus.FAILURE - # 如果是SUCCESS,继续执行下一个子节点 - - # 所有子节点都成功 - self.current_child_index = 0 - self.memory.set('running_child_index', -1) - return NodeStatus.SUCCESS - - def reset(self) -> None: - """重置节点状态""" - super().reset() - self.current_child_index = 0 - self.retry_count = 0 - self.failed_child_index = -1 - self.memory.set('running_child_index', -1) - -class ParallelNode(CompositeNode): - """ - 并行节点 - 同时执行所有子节点 - 根据成功和失败的阈值决定返回状态 - 支持多种并行模式 - """ - - class ParallelMode: - """并行模式枚举""" - SEQUENCE = "sequence" # 顺序启动 - ALL = "all" # 同时启动所有 - RACE = "race" # 竞速模式,第一个完成的决定结果 - - def __init__(self, config: BTNodeConfig = None, - success_threshold: int = 1, - failure_threshold: int = 1, - mode: str = ParallelMode.ALL): - if config is None: - config = BTNodeConfig(name="Parallel") - super().__init__(config) - self.success_threshold = success_threshold # 成功阈值 - self.failure_threshold = failure_threshold # 失败阈值 - self.mode = mode # 并行模式 - self.child_results: List[NodeStatus] = [] # 子节点执行结果 - self.child_start_times: List[float] = [] # 子节点开始时间 - self.completed_count = 0 # 已完成的子节点数量 - - def _execute_child_logic(self, blackboard) -> NodeStatus: - """执行并行节点逻辑""" - success_count = 0 - failure_count = 0 - running_count = 0 - - current_time = time.time() - - # 初始化结果列表 - while len(self.child_results) < len(self.children): - self.child_results.append(NodeStatus.RUNNING) - self.child_start_times.append(0.0) - - # 根据模式执行子节点 - if self.mode == self.ParallelMode.SEQUENCE: - self._execute_sequence_mode(blackboard, current_time) - elif self.mode == self.ParallelMode.RACE: - self._execute_race_mode(blackboard, current_time) - else: # ALL模式 - self._execute_all_mode(blackboard, current_time) - - # 统计结果 - for status in self.child_results: - if status == NodeStatus.SUCCESS: - success_count += 1 - elif status == NodeStatus.FAILURE: - failure_count += 1 - elif status == NodeStatus.RUNNING: - running_count += 1 - - # 检查是否达到阈值 - if success_count >= self.success_threshold: - # 重置状态 - self._reset_state() - return NodeStatus.SUCCESS - elif failure_count >= self.failure_threshold: - # 重置状态 - self._reset_state() - return NodeStatus.FAILURE - elif running_count == 0: - # 所有子节点都已完成但未达到阈值 - self._reset_state() - return NodeStatus.FAILURE - else: - return NodeStatus.RUNNING - - def _execute_all_mode(self, blackboard, current_time: float) -> None: - """执行ALL模式""" - for i, child in enumerate(self.children): - # 如果该子节点还没有结果或者之前是RUNNING状态,则执行它 - if (i >= len(self.child_results) or - self.child_results[i] == NodeStatus.RUNNING): - # 记录开始时间 - if self.child_start_times[i] == 0.0: - self.child_start_times[i] = current_time - - status = child.execute(blackboard) - # 确保child_results列表足够长 - while len(self.child_results) <= i: - self.child_results.append(NodeStatus.RUNNING) - self.child_start_times.append(0.0) - self.child_results[i] = status - - def _execute_sequence_mode(self, blackboard, current_time: float) -> None: - """执行SEQUENCE模式""" - # 只执行未完成的子节点,按顺序启动 - for i, child in enumerate(self.children): - if i >= self.completed_count: - # 启动下一个子节点 - if (i >= len(self.child_results) or - self.child_results[i] == NodeStatus.RUNNING): - # 记录开始时间 - if self.child_start_times[i] == 0.0: - self.child_start_times[i] = current_time - - status = child.execute(blackboard) - # 确保child_results列表足够长 - while len(self.child_results) <= i: - self.child_results.append(NodeStatus.RUNNING) - self.child_start_times.append(0.0) - self.child_results[i] = status - - # 如果子节点已完成,增加完成计数 - if status != NodeStatus.RUNNING: - self.completed_count = i + 1 - - def _execute_race_mode(self, blackboard, current_time: float) -> None: - """执行RACE模式""" - # 执行所有未完成的子节点,第一个完成的决定结果 - for i, child in enumerate(self.children): - if (i >= len(self.child_results) or - self.child_results[i] == NodeStatus.RUNNING): - # 记录开始时间 - if self.child_start_times[i] == 0.0: - self.child_start_times[i] = current_time - - status = child.execute(blackboard) - # 确保child_results列表足够长 - while len(self.child_results) <= i: - self.child_results.append(NodeStatus.RUNNING) - self.child_start_times.append(0.0) - self.child_results[i] = status - - def _reset_state(self) -> None: - """重置节点状态""" - self.child_results = [] - self.child_start_times = [] - self.completed_count = 0 - - def reset(self) -> None: - """重置节点状态""" - super().reset() - self._reset_state() - -class RandomSelectorNode(SelectorNode): - """ - 随机选择节点 - 随机选择一个子节点执行,而不是按顺序 - """ - - def __init__(self, config: BTNodeConfig = None, - weighted: bool = False): - if config is None: - config = BTNodeConfig(name="RandomSelector") - super().__init__(config) - self.weighted = weighted # 是否使用权重 - self.weights: List[float] = [] # 子节点权重 - self.selected_index = -1 # 当前选择的索引 - - def add_weighted_child(self, child: BTNode, weight: float = 1.0) -> None: - """添加带权重的子节点""" - self.add_child(child) - self.weights.append(weight) - - def _execute_child_logic(self, blackboard) -> NodeStatus: - """执行随机选择节点逻辑""" - if not self.children: - return NodeStatus.FAILURE - - # 如果还没有选择子节点,随机选择一个 - if self.selected_index == -1: - if self.weighted and self.weights: - # 使用权重随机选择 - total_weight = sum(self.weights) - if total_weight > 0: - random_value = random.uniform(0, total_weight) - cumulative_weight = 0 - for i, weight in enumerate(self.weights): - cumulative_weight += weight - if random_value <= cumulative_weight: - self.selected_index = i - break - # 确保选择了有效索引 - if self.selected_index == -1: - self.selected_index = len(self.children) - 1 - else: - # 权重总和为0,均匀随机选择 - self.selected_index = random.randint(0, len(self.children) - 1) - else: - # 均匀随机选择 - self.selected_index = random.randint(0, len(self.children) - 1) - - # 执行选中的子节点 - selected_child = self.children[self.selected_index] - status = selected_child.execute(blackboard) - - if status != NodeStatus.RUNNING: - # 子节点完成,重置选择 - self.selected_index = -1 - - return status - - def reset(self) -> None: - """重置节点状态""" - super().reset() - self.selected_index = -1 - -class PrioritySelectorNode(SelectorNode): - """ - 优先级选择节点 - 根据优先级选择子节点执行,优先级高的先执行 - """ - - def __init__(self, config: BTNodeConfig = None): - if config is None: - config = BTNodeConfig(name="PrioritySelector") - super().__init__(config) - self.priorities: List[int] = [] # 子节点优先级 - self.sorted_indices: List[int] = [] # 按优先级排序的索引 - - def add_prioritized_child(self, child: BTNode, priority: int = 0) -> None: - """添加带优先级的子节点""" - self.add_child(child) - self.priorities.append(priority) - self._sort_children() - - def _sort_children(self) -> None: - """根据优先级对子节点进行排序""" - # 创建索引列表并按优先级排序(优先级高的在前) - self.sorted_indices = list(range(len(self.children))) - self.sorted_indices.sort(key=lambda i: self.priorities[i], reverse=True) - - def _execute_child_logic(self, blackboard) -> NodeStatus: - """执行优先级选择节点逻辑""" - if not self.children: - return NodeStatus.FAILURE - - # 按优先级顺序执行子节点 - for i in self.sorted_indices: - child = self.children[i] - status = child.execute(blackboard) - - if status == NodeStatus.RUNNING: - # 记录当前正在运行的子节点索引 - self.current_child_index = i - self.memory.set('running_child_index', i) - return NodeStatus.RUNNING - elif status == NodeStatus.SUCCESS: - # 重置当前子节点索引 - self.current_child_index = 0 - self.memory.set('running_child_index', -1) - return NodeStatus.SUCCESS - # 如果是FAILURE,继续执行下一个优先级的子节点 - - # 所有子节点都失败 - self.current_child_index = 0 - self.memory.set('running_child_index', -1) - return NodeStatus.FAILURE - - def reset(self) -> None: - """重置节点状态""" - super().reset() - self.current_child_index = 0 +""" +行为树控制节点 +包括选择节点、序列节点、并行节点等 +提供多种控制流逻辑实现 +""" + +import time +import random +from typing import List, Optional +from ..core.behavior_tree import BTNode, NodeStatus, CompositeNode, BTNodeConfig + +class SelectorNode(CompositeNode): + """ + 选择节点(OR节点) + 从左到右执行子节点,直到有一个成功为止 + 如果所有子节点都失败,则返回失败 + 支持优先级排序和中断机制 + """ + + def __init__(self, config: BTNodeConfig = None): + if config is None: + config = BTNodeConfig(name="Selector") + super().__init__(config) + self.current_child_index = 0 + self.interruptible_children: List[int] = [] # 可中断的子节点索引 + + def add_interruptible_child(self, index: int) -> None: + """添加可中断的子节点索引""" + if index not in self.interruptible_children: + self.interruptible_children.append(index) + + def _execute_child_logic(self, blackboard) -> NodeStatus: + """执行选择节点逻辑""" + # 从当前子节点开始执行 + for i in range(self.current_child_index, len(self.children)): + child = self.children[i] + + # 检查是否可以中断当前运行的子节点 + if (self.current_child_index != i and + self.current_child_index in self.interruptible_children and + hasattr(child, 'config') and child.config.can_be_interrupted): + # 中断之前的子节点 + if hasattr(self.children[self.current_child_index], 'on_abort'): + self.children[self.current_child_index].on_abort(blackboard) + + status = child.execute(blackboard) + + if status == NodeStatus.RUNNING: + # 记录当前正在运行的子节点索引 + self.current_child_index = i + self.memory.set('running_child_index', i) + return NodeStatus.RUNNING + elif status == NodeStatus.SUCCESS: + # 重置当前子节点索引 + self.current_child_index = 0 + self.memory.set('running_child_index', -1) + return NodeStatus.SUCCESS + # 如果是FAILURE,继续执行下一个子节点 + + # 所有子节点都失败 + self.current_child_index = 0 + self.memory.set('running_child_index', -1) + return NodeStatus.FAILURE + + def reset(self) -> None: + """重置节点状态""" + super().reset() + self.current_child_index = 0 + self.memory.set('running_child_index', -1) + +class SequenceNode(CompositeNode): + """ + 序列节点(AND节点) + 从左到右执行子节点,直到有一个失败为止 + 如果所有子节点都成功,则返回成功 + 支持失败重试和条件检查 + """ + + def __init__(self, config: BTNodeConfig = None, max_retries: int = 0): + if config is None: + config = BTNodeConfig(name="Sequence") + super().__init__(config) + self.current_child_index = 0 + self.max_retries = max_retries # 最大重试次数 + self.retry_count = 0 # 当前重试次数 + self.failed_child_index = -1 # 最后失败的子节点索引 + + def _execute_child_logic(self, blackboard) -> NodeStatus: + """执行序列节点逻辑""" + # 检查是否需要重置(所有子节点都成功后再次执行) + if self.current_child_index >= len(self.children): + self.current_child_index = 0 + self.retry_count = 0 + + # 从当前子节点开始执行 + for i in range(self.current_child_index, len(self.children)): + child = self.children[i] + status = child.execute(blackboard) + + if status == NodeStatus.RUNNING: + # 记录当前正在运行的子节点索引 + self.current_child_index = i + self.memory.set('running_child_index', i) + return NodeStatus.RUNNING + elif status == NodeStatus.FAILURE: + # 记录失败的子节点 + self.failed_child_index = i + + # 检查是否可以重试 + if self.retry_count < self.max_retries: + self.retry_count += 1 + # 重置到失败的子节点重新开始 + self.current_child_index = i + return NodeStatus.RUNNING + else: + # 重置状态并返回失败 + self.current_child_index = 0 + self.memory.set('running_child_index', -1) + return NodeStatus.FAILURE + # 如果是SUCCESS,继续执行下一个子节点 + + # 所有子节点都成功 + self.current_child_index = 0 + self.memory.set('running_child_index', -1) + return NodeStatus.SUCCESS + + def reset(self) -> None: + """重置节点状态""" + super().reset() + self.current_child_index = 0 + self.retry_count = 0 + self.failed_child_index = -1 + self.memory.set('running_child_index', -1) + +class ParallelNode(CompositeNode): + """ + 并行节点 + 同时执行所有子节点 + 根据成功和失败的阈值决定返回状态 + 支持多种并行模式 + """ + + class ParallelMode: + """并行模式枚举""" + SEQUENCE = "sequence" # 顺序启动 + ALL = "all" # 同时启动所有 + RACE = "race" # 竞速模式,第一个完成的决定结果 + + def __init__(self, config: BTNodeConfig = None, + success_threshold: int = 1, + failure_threshold: int = 1, + mode: str = ParallelMode.ALL): + if config is None: + config = BTNodeConfig(name="Parallel") + super().__init__(config) + self.success_threshold = success_threshold # 成功阈值 + self.failure_threshold = failure_threshold # 失败阈值 + self.mode = mode # 并行模式 + self.child_results: List[NodeStatus] = [] # 子节点执行结果 + self.child_start_times: List[float] = [] # 子节点开始时间 + self.completed_count = 0 # 已完成的子节点数量 + + def _execute_child_logic(self, blackboard) -> NodeStatus: + """执行并行节点逻辑""" + success_count = 0 + failure_count = 0 + running_count = 0 + + current_time = time.time() + + # 初始化结果列表 + while len(self.child_results) < len(self.children): + self.child_results.append(NodeStatus.RUNNING) + self.child_start_times.append(0.0) + + # 根据模式执行子节点 + if self.mode == self.ParallelMode.SEQUENCE: + self._execute_sequence_mode(blackboard, current_time) + elif self.mode == self.ParallelMode.RACE: + self._execute_race_mode(blackboard, current_time) + else: # ALL模式 + self._execute_all_mode(blackboard, current_time) + + # 统计结果 + for status in self.child_results: + if status == NodeStatus.SUCCESS: + success_count += 1 + elif status == NodeStatus.FAILURE: + failure_count += 1 + elif status == NodeStatus.RUNNING: + running_count += 1 + + # 检查是否达到阈值 + if success_count >= self.success_threshold: + # 重置状态 + self._reset_state() + return NodeStatus.SUCCESS + elif failure_count >= self.failure_threshold: + # 重置状态 + self._reset_state() + return NodeStatus.FAILURE + elif running_count == 0: + # 所有子节点都已完成但未达到阈值 + self._reset_state() + return NodeStatus.FAILURE + else: + return NodeStatus.RUNNING + + def _execute_all_mode(self, blackboard, current_time: float) -> None: + """执行ALL模式""" + for i, child in enumerate(self.children): + # 如果该子节点还没有结果或者之前是RUNNING状态,则执行它 + if (i >= len(self.child_results) or + self.child_results[i] == NodeStatus.RUNNING): + # 记录开始时间 + if self.child_start_times[i] == 0.0: + self.child_start_times[i] = current_time + + status = child.execute(blackboard) + # 确保child_results列表足够长 + while len(self.child_results) <= i: + self.child_results.append(NodeStatus.RUNNING) + self.child_start_times.append(0.0) + self.child_results[i] = status + + def _execute_sequence_mode(self, blackboard, current_time: float) -> None: + """执行SEQUENCE模式""" + # 只执行未完成的子节点,按顺序启动 + for i, child in enumerate(self.children): + if i >= self.completed_count: + # 启动下一个子节点 + if (i >= len(self.child_results) or + self.child_results[i] == NodeStatus.RUNNING): + # 记录开始时间 + if self.child_start_times[i] == 0.0: + self.child_start_times[i] = current_time + + status = child.execute(blackboard) + # 确保child_results列表足够长 + while len(self.child_results) <= i: + self.child_results.append(NodeStatus.RUNNING) + self.child_start_times.append(0.0) + self.child_results[i] = status + + # 如果子节点已完成,增加完成计数 + if status != NodeStatus.RUNNING: + self.completed_count = i + 1 + + def _execute_race_mode(self, blackboard, current_time: float) -> None: + """执行RACE模式""" + # 执行所有未完成的子节点,第一个完成的决定结果 + for i, child in enumerate(self.children): + if (i >= len(self.child_results) or + self.child_results[i] == NodeStatus.RUNNING): + # 记录开始时间 + if self.child_start_times[i] == 0.0: + self.child_start_times[i] = current_time + + status = child.execute(blackboard) + # 确保child_results列表足够长 + while len(self.child_results) <= i: + self.child_results.append(NodeStatus.RUNNING) + self.child_start_times.append(0.0) + self.child_results[i] = status + + def _reset_state(self) -> None: + """重置节点状态""" + self.child_results = [] + self.child_start_times = [] + self.completed_count = 0 + + def reset(self) -> None: + """重置节点状态""" + super().reset() + self._reset_state() + +class RandomSelectorNode(SelectorNode): + """ + 随机选择节点 + 随机选择一个子节点执行,而不是按顺序 + """ + + def __init__(self, config: BTNodeConfig = None, + weighted: bool = False): + if config is None: + config = BTNodeConfig(name="RandomSelector") + super().__init__(config) + self.weighted = weighted # 是否使用权重 + self.weights: List[float] = [] # 子节点权重 + self.selected_index = -1 # 当前选择的索引 + + def add_weighted_child(self, child: BTNode, weight: float = 1.0) -> None: + """添加带权重的子节点""" + self.add_child(child) + self.weights.append(weight) + + def _execute_child_logic(self, blackboard) -> NodeStatus: + """执行随机选择节点逻辑""" + if not self.children: + return NodeStatus.FAILURE + + # 如果还没有选择子节点,随机选择一个 + if self.selected_index == -1: + if self.weighted and self.weights: + # 使用权重随机选择 + total_weight = sum(self.weights) + if total_weight > 0: + random_value = random.uniform(0, total_weight) + cumulative_weight = 0 + for i, weight in enumerate(self.weights): + cumulative_weight += weight + if random_value <= cumulative_weight: + self.selected_index = i + break + # 确保选择了有效索引 + if self.selected_index == -1: + self.selected_index = len(self.children) - 1 + else: + # 权重总和为0,均匀随机选择 + self.selected_index = random.randint(0, len(self.children) - 1) + else: + # 均匀随机选择 + self.selected_index = random.randint(0, len(self.children) - 1) + + # 执行选中的子节点 + selected_child = self.children[self.selected_index] + status = selected_child.execute(blackboard) + + if status != NodeStatus.RUNNING: + # 子节点完成,重置选择 + self.selected_index = -1 + + return status + + def reset(self) -> None: + """重置节点状态""" + super().reset() + self.selected_index = -1 + +class PrioritySelectorNode(SelectorNode): + """ + 优先级选择节点 + 根据优先级选择子节点执行,优先级高的先执行 + """ + + def __init__(self, config: BTNodeConfig = None): + if config is None: + config = BTNodeConfig(name="PrioritySelector") + super().__init__(config) + self.priorities: List[int] = [] # 子节点优先级 + self.sorted_indices: List[int] = [] # 按优先级排序的索引 + + def add_prioritized_child(self, child: BTNode, priority: int = 0) -> None: + """添加带优先级的子节点""" + self.add_child(child) + self.priorities.append(priority) + self._sort_children() + + def _sort_children(self) -> None: + """根据优先级对子节点进行排序""" + # 创建索引列表并按优先级排序(优先级高的在前) + self.sorted_indices = list(range(len(self.children))) + self.sorted_indices.sort(key=lambda i: self.priorities[i], reverse=True) + + def _execute_child_logic(self, blackboard) -> NodeStatus: + """执行优先级选择节点逻辑""" + if not self.children: + return NodeStatus.FAILURE + + # 按优先级顺序执行子节点 + for i in self.sorted_indices: + child = self.children[i] + status = child.execute(blackboard) + + if status == NodeStatus.RUNNING: + # 记录当前正在运行的子节点索引 + self.current_child_index = i + self.memory.set('running_child_index', i) + return NodeStatus.RUNNING + elif status == NodeStatus.SUCCESS: + # 重置当前子节点索引 + self.current_child_index = 0 + self.memory.set('running_child_index', -1) + return NodeStatus.SUCCESS + # 如果是FAILURE,继续执行下一个优先级的子节点 + + # 所有子节点都失败 + self.current_child_index = 0 + self.memory.set('running_child_index', -1) + return NodeStatus.FAILURE + + def reset(self) -> None: + """重置节点状态""" + super().reset() + self.current_child_index = 0 self.memory.set('running_child_index', -1) \ No newline at end of file diff --git a/plugins/user/behavior_tree/nodes/decorator_nodes.py b/plugins/user/behavior_tree/nodes/decorator_nodes.py index e9a33bb8..e2b49719 100644 --- a/plugins/user/behavior_tree/nodes/decorator_nodes.py +++ b/plugins/user/behavior_tree/nodes/decorator_nodes.py @@ -1,346 +1,346 @@ -""" -行为树装饰节点 -包括重复节点、逆变节点、成功节点等 -提供多种装饰逻辑实现 -""" - -import time -import random -from typing import Optional, Callable, Any -from ..core.behavior_tree import DecoratorNode, NodeStatus, BTNodeConfig - -class RepeaterNode(DecoratorNode): - """ - 重复节点 - 重复执行子节点指定次数 - 支持条件检查和间隔时间 - """ - - def __init__(self, child: Optional['BTNode'] = None, - config: BTNodeConfig = None, - count: int = -1, # 重复次数,-1表示无限重复 - interval: float = 0.0, # 重复间隔时间(秒) - condition: Optional[Callable[[], bool]] = None): # 重复条件 - if config is None: - config = BTNodeConfig(name="Repeater") - super().__init__(child, config) - self.count = count - self.interval = interval - self.condition = condition - self.current_count = 0 # 当前已重复次数 - self.last_execution_time = 0.0 # 上次执行时间 - - def _decorate(self, child_status: NodeStatus) -> NodeStatus: - """装饰子节点的执行结果""" - current_time = time.time() - - # 检查间隔时间 - if self.interval > 0 and self.current_count > 0: - time_since_last = current_time - self.last_execution_time - if time_since_last < self.interval: - return NodeStatus.RUNNING - - # 检查重复条件 - if self.condition and not self.condition(): - # 重置计数器 - self.current_count = 0 - self.last_execution_time = 0.0 - return child_status - - if child_status == NodeStatus.RUNNING: - return NodeStatus.RUNNING - - # 子节点执行完成(SUCCESS或FAILURE) - self.current_count += 1 - self.last_execution_time = current_time - - # 检查是否达到重复次数 - if self.count != -1 and self.current_count >= self.count: - # 重置计数器 - self.current_count = 0 - self.last_execution_time = 0.0 - return child_status - else: - # 继续重复执行 - return NodeStatus.RUNNING - - def reset(self) -> None: - """重置节点状态""" - super().reset() - self.current_count = 0 - self.last_execution_time = 0.0 - -class InverterNode(DecoratorNode): - """ - 逆变节点 - 反转子节点的执行结果 - SUCCESS变为FAILURE,FAILURE变为SUCCESS - RUNNING保持不变 - 支持条件反转和延迟反转 - """ - - def __init__(self, child: Optional['BTNode'] = None, - config: BTNodeConfig = None, - delay: float = 0.0, # 延迟反转时间 - condition: Optional[Callable[[], bool]] = None): # 反转条件 - if config is None: - config = BTNodeConfig(name="Inverter") - super().__init__(child, config) - self.delay = delay - self.condition = condition - self.inversion_start_time = 0.0 # 反转开始时间 - - def _decorate(self, child_status: NodeStatus) -> NodeStatus: - """装饰子节点的执行结果""" - # 检查反转条件 - should_invert = True - if self.condition: - should_invert = self.condition() - - if not should_invert: - return child_status - - # 检查延迟反转 - if self.delay > 0 and child_status != NodeStatus.RUNNING: - current_time = time.time() - if self.inversion_start_time == 0.0: - self.inversion_start_time = current_time - return child_status # 暂时不反转 - - if current_time - self.inversion_start_time < self.delay: - return child_status # 延迟未到,暂不反转 - - # 执行反转 - if child_status == NodeStatus.SUCCESS: - return NodeStatus.FAILURE - elif child_status == NodeStatus.FAILURE: - return NodeStatus.SUCCESS - else: # RUNNING - return NodeStatus.RUNNING - - def reset(self) -> None: - """重置节点状态""" - super().reset() - self.inversion_start_time = 0.0 - -class SucceederNode(DecoratorNode): - """ - 成功节点 - 总是返回SUCCESS,无论子节点返回什么 - 支持条件成功和延迟成功 - """ - - def __init__(self, child: Optional['BTNode'] = None, - config: BTNodeConfig = None, - delay: float = 0.0, # 延迟成功时间 - condition: Optional[Callable[[], bool]] = None): # 成功条件 - if config is None: - config = BTNodeConfig(name="Succeeder") - super().__init__(child, config) - self.delay = delay - self.condition = condition - self.success_start_time = 0.0 # 成功开始时间 - - def _decorate(self, child_status: NodeStatus) -> NodeStatus: - """装饰子节点的执行结果""" - # 检查成功条件 - should_succeed = True - if self.condition: - should_succeed = self.condition() - - if not should_succeed: - return child_status - - # 检查延迟成功 - if self.delay > 0 and child_status != NodeStatus.RUNNING: - current_time = time.time() - if self.success_start_time == 0.0: - self.success_start_time = current_time - return child_status # 暂时不成功 - - if current_time - self.success_start_time < self.delay: - return child_status # 延迟未到,暂不成功 - - # 总是返回成功 - if child_status == NodeStatus.RUNNING: - return NodeStatus.RUNNING - else: # SUCCESS或FAILURE - return NodeStatus.SUCCESS - - def reset(self) -> None: - """重置节点状态""" - super().reset() - self.success_start_time = 0.0 - -class FailureNode(DecoratorNode): - """ - 失败节点 - 总是返回FAILURE,无论子节点返回什么 - 支持条件失败和延迟失败 - """ - - def __init__(self, child: Optional['BTNode'] = None, - config: BTNodeConfig = None, - delay: float = 0.0, # 延迟失败时间 - condition: Optional[Callable[[], bool]] = None): # 失败条件 - if config is None: - config = BTNodeConfig(name="Failure") - super().__init__(child, config) - self.delay = delay - self.condition = condition - self.failure_start_time = 0.0 # 失败开始时间 - - def _decorate(self, child_status: NodeStatus) -> NodeStatus: - """装饰子节点的执行结果""" - # 检查失败条件 - should_fail = True - if self.condition: - should_fail = self.condition() - - if not should_fail: - return child_status - - # 检查延迟失败 - if self.delay > 0 and child_status != NodeStatus.RUNNING: - current_time = time.time() - if self.failure_start_time == 0.0: - self.failure_start_time = current_time - return child_status # 暂时不失败 - - if current_time - self.failure_start_time < self.delay: - return child_status # 延迟未到,暂不失败 - - # 总是返回失败 - if child_status == NodeStatus.RUNNING: - return NodeStatus.RUNNING - else: # SUCCESS或FAILURE - return NodeStatus.FAILURE - - def reset(self) -> None: - """重置节点状态""" - super().reset() - self.failure_start_time = 0.0 - -class TimerNode(DecoratorNode): - """ - 计时器节点 - 限制子节点的执行时间 - """ - - def __init__(self, child: Optional['BTNode'] = None, - config: BTNodeConfig = None, - timeout: float = 5.0): # 超时时间(秒) - if config is None: - config = BTNodeConfig(name="Timer") - super().__init__(child, config) - self.timeout = timeout - self.start_time = 0.0 # 开始时间 - - def _decorate(self, child_status: NodeStatus) -> NodeStatus: - """装饰子节点的执行结果""" - current_time = time.time() - - # 记录开始时间 - if self.start_time == 0.0: - self.start_time = current_time - - # 检查是否超时 - if current_time - self.start_time >= self.timeout: - # 超时,强制返回失败 - self.start_time = 0.0 - if hasattr(self.child, 'on_abort'): - self.child.on_abort(None) # 中断子节点 - return NodeStatus.FAILURE - - # 如果子节点完成,重置计时器 - if child_status != NodeStatus.RUNNING: - self.start_time = 0.0 - - return child_status - - def reset(self) -> None: - """重置节点状态""" - super().reset() - self.start_time = 0.0 - -class CooldownNode(DecoratorNode): - """ - 冷却节点 - 在子节点执行后添加冷却时间 - """ - - def __init__(self, child: Optional['BTNode'] = None, - config: BTNodeConfig = None, - cooldown_time: float = 1.0): # 冷却时间(秒) - if config is None: - config = BTNodeConfig(name="Cooldown") - super().__init__(child, config) - self.cooldown_time = cooldown_time - self.last_execution_time = 0.0 # 上次执行完成时间 - self.is_on_cooldown = False # 是否在冷却中 - - def _decorate(self, child_status: NodeStatus) -> NodeStatus: - """装饰子节点的执行结果""" - current_time = time.time() - - # 检查是否在冷却中 - if self.is_on_cooldown: - if current_time - self.last_execution_time >= self.cooldown_time: - # 冷却结束 - self.is_on_cooldown = False - self.last_execution_time = 0.0 - return NodeStatus.FAILURE # 冷却结束后返回失败,等待下次执行 - else: - # 仍在冷却中 - return NodeStatus.FAILURE - - # 子节点正在运行 - if child_status == NodeStatus.RUNNING: - return NodeStatus.RUNNING - - # 子节点完成,开始冷却 - self.is_on_cooldown = True - self.last_execution_time = current_time - return child_status - - def reset(self) -> None: - """重置节点状态""" - super().reset() - self.last_execution_time = 0.0 - self.is_on_cooldown = False - -class RandomizerNode(DecoratorNode): - """ - 随机化节点 - 随机决定是否执行子节点 - """ - - def __init__(self, child: Optional['BTNode'] = None, - config: BTNodeConfig = None, - success_rate: float = 0.5): # 成功率(0.0-1.0) - if config is None: - config = BTNodeConfig(name="Randomizer") - super().__init__(child, config) - self.success_rate = max(0.0, min(1.0, success_rate)) # 限制在0-1之间 - self.should_execute = False # 是否应该执行子节点 - self.has_decided = False # 是否已经做出决定 - - def _decorate(self, child_status: NodeStatus) -> NodeStatus: - """装饰子节点的执行结果""" - # 如果还没有做出决定,随机决定是否执行 - if not self.has_decided: - self.should_execute = random.random() < self.success_rate - self.has_decided = True - - # 如果决定不执行,直接返回成功 - if not self.should_execute: - return NodeStatus.SUCCESS - - # 执行子节点 - return child_status - - def reset(self) -> None: - """重置节点状态""" - super().reset() - self.should_execute = False +""" +行为树装饰节点 +包括重复节点、逆变节点、成功节点等 +提供多种装饰逻辑实现 +""" + +import time +import random +from typing import Optional, Callable, Any +from ..core.behavior_tree import DecoratorNode, NodeStatus, BTNodeConfig + +class RepeaterNode(DecoratorNode): + """ + 重复节点 + 重复执行子节点指定次数 + 支持条件检查和间隔时间 + """ + + def __init__(self, child: Optional['BTNode'] = None, + config: BTNodeConfig = None, + count: int = -1, # 重复次数,-1表示无限重复 + interval: float = 0.0, # 重复间隔时间(秒) + condition: Optional[Callable[[], bool]] = None): # 重复条件 + if config is None: + config = BTNodeConfig(name="Repeater") + super().__init__(child, config) + self.count = count + self.interval = interval + self.condition = condition + self.current_count = 0 # 当前已重复次数 + self.last_execution_time = 0.0 # 上次执行时间 + + def _decorate(self, child_status: NodeStatus) -> NodeStatus: + """装饰子节点的执行结果""" + current_time = time.time() + + # 检查间隔时间 + if self.interval > 0 and self.current_count > 0: + time_since_last = current_time - self.last_execution_time + if time_since_last < self.interval: + return NodeStatus.RUNNING + + # 检查重复条件 + if self.condition and not self.condition(): + # 重置计数器 + self.current_count = 0 + self.last_execution_time = 0.0 + return child_status + + if child_status == NodeStatus.RUNNING: + return NodeStatus.RUNNING + + # 子节点执行完成(SUCCESS或FAILURE) + self.current_count += 1 + self.last_execution_time = current_time + + # 检查是否达到重复次数 + if self.count != -1 and self.current_count >= self.count: + # 重置计数器 + self.current_count = 0 + self.last_execution_time = 0.0 + return child_status + else: + # 继续重复执行 + return NodeStatus.RUNNING + + def reset(self) -> None: + """重置节点状态""" + super().reset() + self.current_count = 0 + self.last_execution_time = 0.0 + +class InverterNode(DecoratorNode): + """ + 逆变节点 + 反转子节点的执行结果 + SUCCESS变为FAILURE,FAILURE变为SUCCESS + RUNNING保持不变 + 支持条件反转和延迟反转 + """ + + def __init__(self, child: Optional['BTNode'] = None, + config: BTNodeConfig = None, + delay: float = 0.0, # 延迟反转时间 + condition: Optional[Callable[[], bool]] = None): # 反转条件 + if config is None: + config = BTNodeConfig(name="Inverter") + super().__init__(child, config) + self.delay = delay + self.condition = condition + self.inversion_start_time = 0.0 # 反转开始时间 + + def _decorate(self, child_status: NodeStatus) -> NodeStatus: + """装饰子节点的执行结果""" + # 检查反转条件 + should_invert = True + if self.condition: + should_invert = self.condition() + + if not should_invert: + return child_status + + # 检查延迟反转 + if self.delay > 0 and child_status != NodeStatus.RUNNING: + current_time = time.time() + if self.inversion_start_time == 0.0: + self.inversion_start_time = current_time + return child_status # 暂时不反转 + + if current_time - self.inversion_start_time < self.delay: + return child_status # 延迟未到,暂不反转 + + # 执行反转 + if child_status == NodeStatus.SUCCESS: + return NodeStatus.FAILURE + elif child_status == NodeStatus.FAILURE: + return NodeStatus.SUCCESS + else: # RUNNING + return NodeStatus.RUNNING + + def reset(self) -> None: + """重置节点状态""" + super().reset() + self.inversion_start_time = 0.0 + +class SucceederNode(DecoratorNode): + """ + 成功节点 + 总是返回SUCCESS,无论子节点返回什么 + 支持条件成功和延迟成功 + """ + + def __init__(self, child: Optional['BTNode'] = None, + config: BTNodeConfig = None, + delay: float = 0.0, # 延迟成功时间 + condition: Optional[Callable[[], bool]] = None): # 成功条件 + if config is None: + config = BTNodeConfig(name="Succeeder") + super().__init__(child, config) + self.delay = delay + self.condition = condition + self.success_start_time = 0.0 # 成功开始时间 + + def _decorate(self, child_status: NodeStatus) -> NodeStatus: + """装饰子节点的执行结果""" + # 检查成功条件 + should_succeed = True + if self.condition: + should_succeed = self.condition() + + if not should_succeed: + return child_status + + # 检查延迟成功 + if self.delay > 0 and child_status != NodeStatus.RUNNING: + current_time = time.time() + if self.success_start_time == 0.0: + self.success_start_time = current_time + return child_status # 暂时不成功 + + if current_time - self.success_start_time < self.delay: + return child_status # 延迟未到,暂不成功 + + # 总是返回成功 + if child_status == NodeStatus.RUNNING: + return NodeStatus.RUNNING + else: # SUCCESS或FAILURE + return NodeStatus.SUCCESS + + def reset(self) -> None: + """重置节点状态""" + super().reset() + self.success_start_time = 0.0 + +class FailureNode(DecoratorNode): + """ + 失败节点 + 总是返回FAILURE,无论子节点返回什么 + 支持条件失败和延迟失败 + """ + + def __init__(self, child: Optional['BTNode'] = None, + config: BTNodeConfig = None, + delay: float = 0.0, # 延迟失败时间 + condition: Optional[Callable[[], bool]] = None): # 失败条件 + if config is None: + config = BTNodeConfig(name="Failure") + super().__init__(child, config) + self.delay = delay + self.condition = condition + self.failure_start_time = 0.0 # 失败开始时间 + + def _decorate(self, child_status: NodeStatus) -> NodeStatus: + """装饰子节点的执行结果""" + # 检查失败条件 + should_fail = True + if self.condition: + should_fail = self.condition() + + if not should_fail: + return child_status + + # 检查延迟失败 + if self.delay > 0 and child_status != NodeStatus.RUNNING: + current_time = time.time() + if self.failure_start_time == 0.0: + self.failure_start_time = current_time + return child_status # 暂时不失败 + + if current_time - self.failure_start_time < self.delay: + return child_status # 延迟未到,暂不失败 + + # 总是返回失败 + if child_status == NodeStatus.RUNNING: + return NodeStatus.RUNNING + else: # SUCCESS或FAILURE + return NodeStatus.FAILURE + + def reset(self) -> None: + """重置节点状态""" + super().reset() + self.failure_start_time = 0.0 + +class TimerNode(DecoratorNode): + """ + 计时器节点 + 限制子节点的执行时间 + """ + + def __init__(self, child: Optional['BTNode'] = None, + config: BTNodeConfig = None, + timeout: float = 5.0): # 超时时间(秒) + if config is None: + config = BTNodeConfig(name="Timer") + super().__init__(child, config) + self.timeout = timeout + self.start_time = 0.0 # 开始时间 + + def _decorate(self, child_status: NodeStatus) -> NodeStatus: + """装饰子节点的执行结果""" + current_time = time.time() + + # 记录开始时间 + if self.start_time == 0.0: + self.start_time = current_time + + # 检查是否超时 + if current_time - self.start_time >= self.timeout: + # 超时,强制返回失败 + self.start_time = 0.0 + if hasattr(self.child, 'on_abort'): + self.child.on_abort(None) # 中断子节点 + return NodeStatus.FAILURE + + # 如果子节点完成,重置计时器 + if child_status != NodeStatus.RUNNING: + self.start_time = 0.0 + + return child_status + + def reset(self) -> None: + """重置节点状态""" + super().reset() + self.start_time = 0.0 + +class CooldownNode(DecoratorNode): + """ + 冷却节点 + 在子节点执行后添加冷却时间 + """ + + def __init__(self, child: Optional['BTNode'] = None, + config: BTNodeConfig = None, + cooldown_time: float = 1.0): # 冷却时间(秒) + if config is None: + config = BTNodeConfig(name="Cooldown") + super().__init__(child, config) + self.cooldown_time = cooldown_time + self.last_execution_time = 0.0 # 上次执行完成时间 + self.is_on_cooldown = False # 是否在冷却中 + + def _decorate(self, child_status: NodeStatus) -> NodeStatus: + """装饰子节点的执行结果""" + current_time = time.time() + + # 检查是否在冷却中 + if self.is_on_cooldown: + if current_time - self.last_execution_time >= self.cooldown_time: + # 冷却结束 + self.is_on_cooldown = False + self.last_execution_time = 0.0 + return NodeStatus.FAILURE # 冷却结束后返回失败,等待下次执行 + else: + # 仍在冷却中 + return NodeStatus.FAILURE + + # 子节点正在运行 + if child_status == NodeStatus.RUNNING: + return NodeStatus.RUNNING + + # 子节点完成,开始冷却 + self.is_on_cooldown = True + self.last_execution_time = current_time + return child_status + + def reset(self) -> None: + """重置节点状态""" + super().reset() + self.last_execution_time = 0.0 + self.is_on_cooldown = False + +class RandomizerNode(DecoratorNode): + """ + 随机化节点 + 随机决定是否执行子节点 + """ + + def __init__(self, child: Optional['BTNode'] = None, + config: BTNodeConfig = None, + success_rate: float = 0.5): # 成功率(0.0-1.0) + if config is None: + config = BTNodeConfig(name="Randomizer") + super().__init__(child, config) + self.success_rate = max(0.0, min(1.0, success_rate)) # 限制在0-1之间 + self.should_execute = False # 是否应该执行子节点 + self.has_decided = False # 是否已经做出决定 + + def _decorate(self, child_status: NodeStatus) -> NodeStatus: + """装饰子节点的执行结果""" + # 如果还没有做出决定,随机决定是否执行 + if not self.has_decided: + self.should_execute = random.random() < self.success_rate + self.has_decided = True + + # 如果决定不执行,直接返回成功 + if not self.should_execute: + return NodeStatus.SUCCESS + + # 执行子节点 + return child_status + + def reset(self) -> None: + """重置节点状态""" + super().reset() + self.should_execute = False self.has_decided = False \ No newline at end of file diff --git a/plugins/user/behavior_tree/nodes/leaf_nodes.py b/plugins/user/behavior_tree/nodes/leaf_nodes.py index a0b5149f..b8fd45bb 100644 --- a/plugins/user/behavior_tree/nodes/leaf_nodes.py +++ b/plugins/user/behavior_tree/nodes/leaf_nodes.py @@ -1,427 +1,427 @@ -""" -行为树叶子节点 -包括条件节点、动作节点、等待节点等 -提供多种叶子节点实现 -""" - -import time -import random -import math -from typing import Optional, Callable, Any, Union -from ..core.behavior_tree import BTNode, NodeStatus, BTNodeConfig - -class ConditionNode(BTNode): - """ - 条件节点 - 检查条件是否满足 - 支持多种条件检查方式和缓存机制 - """ - - def __init__(self, config: Union[BTNodeConfig, str] = "Condition", - condition_func: Optional[Callable[[Any], bool]] = None, - cache_result: bool = False, # 是否缓存结果 - cache_duration: float = 0.0): # 缓存持续时间(秒) - super().__init__(config) - self.condition_func = condition_func - self.cache_result = cache_result - self.cache_duration = cache_duration - self.cached_result: Optional[bool] = None - self.cache_time: float = 0.0 - - def execute(self, blackboard) -> NodeStatus: - """执行条件检查""" - current_time = time.time() - - # 检查缓存 - if (self.cache_result and - self.cached_result is not None and - (self.cache_duration <= 0 or - current_time - self.cache_time < self.cache_duration)): - # 使用缓存结果 - return NodeStatus.SUCCESS if self.cached_result else NodeStatus.FAILURE - - # 执行条件检查 - result = self._evaluate_condition(blackboard) - - # 更新缓存 - if self.cache_result: - self.cached_result = result - self.cache_time = current_time - - return NodeStatus.SUCCESS if result else NodeStatus.FAILURE - - def _evaluate_condition(self, blackboard) -> bool: - """评估条件""" - if self.condition_func: - try: - return bool(self.condition_func(blackboard)) - except Exception as e: - print(f"条件节点执行出错: {e}") - return False - else: - # 如果没有提供条件函数,默认返回成功 - return True - - def invalidate_cache(self) -> None: - """使缓存失效""" - self.cached_result = None - self.cache_time = 0.0 - - def reset(self) -> None: - """重置节点状态""" - super().reset() - self.invalidate_cache() - -class ActionNode(BTNode): - """ - 动作节点 - 执行具体行为 - 支持异步执行和进度跟踪 - """ - - def __init__(self, config: Union[BTNodeConfig, str] = "Action", - action_func: Optional[Callable[[Any], Any]] = None, - async_action: bool = False, # 是否异步执行 - progress_callback: Optional[Callable[[float], None]] = None): # 进度回调 - super().__init__(config) - self.action_func = action_func - self.async_action = async_action - self.progress_callback = progress_callback - self.is_running = False - self.start_time = 0.0 - self.estimated_duration = 0.0 # 估计执行时间 - - def execute(self, blackboard) -> NodeStatus: - """执行动作""" - current_time = time.time() - - # 如果是异步动作且正在运行 - if self.async_action and self.is_running: - # 检查动作是否完成 - if self._is_async_action_complete(blackboard): - self.is_running = False - self.start_time = 0.0 - return NodeStatus.SUCCESS - else: - # 更新进度 - if self.progress_callback: - progress = self._calculate_progress(current_time) - try: - self.progress_callback(progress) - except Exception as e: - print(f"进度回调执行出错: {e}") - return NodeStatus.RUNNING - - # 执行动作 - if self.action_func: - try: - # 记录开始时间 - self.start_time = current_time - - if self.async_action: - # 异步执行 - result = self._start_async_action(blackboard) - if result == NodeStatus.RUNNING: - self.is_running = True - return NodeStatus.RUNNING - else: - self.start_time = 0.0 - return result - else: - # 同步执行 - result = self.action_func(blackboard) - self.start_time = 0.0 - - if result is True: - return NodeStatus.SUCCESS - elif result is False: - return NodeStatus.FAILURE - elif result == NodeStatus.RUNNING: - return NodeStatus.RUNNING - else: - # 默认返回成功 - return NodeStatus.SUCCESS - - except Exception as e: - print(f"动作节点执行出错: {e}") - self.is_running = False - self.start_time = 0.0 - return NodeStatus.FAILURE - else: - # 如果没有提供动作函数,默认返回成功 - return NodeStatus.SUCCESS - - def _start_async_action(self, blackboard) -> NodeStatus: - """启动异步动作""" - # 这里应该启动异步操作,例如创建线程或任务 - # 为了简化,我们假设异步动作立即开始并需要继续运行 - return NodeStatus.RUNNING - - def _is_async_action_complete(self, blackboard) -> bool: - """检查异步动作是否完成""" - # 这里应该检查异步操作是否完成 - # 为了简化,我们随机决定是否完成 - if self.estimated_duration > 0: - current_time = time.time() - elapsed = current_time - self.start_time - return elapsed >= self.estimated_duration - else: - # 随机完成(用于演示) - return random.random() < 0.1 # 10%概率完成 - - def _calculate_progress(self, current_time: float) -> float: - """计算进度""" - if self.estimated_duration > 0: - elapsed = current_time - self.start_time - progress = min(1.0, elapsed / self.estimated_duration) - return max(0.0, progress) - else: - return 0.0 - - def reset(self) -> None: - """重置节点状态""" - super().reset() - self.is_running = False - self.start_time = 0.0 - -class WaitNode(ActionNode): - """ - 等待节点 - 等待指定时间后返回成功 - 支持随机等待时间和进度回调 - """ - - def __init__(self, config: Union[BTNodeConfig, str] = "Wait", - duration: float = 1.0, - random_variance: float = 0.0, # 随机变化范围(秒) - progress_callback: Optional[Callable[[float], None]] = None): - super().__init__(config, None, False, progress_callback) - self.base_duration = duration - self.random_variance = random_variance - self.actual_duration = duration # 实际等待时间 - self.start_time = 0.0 - - def execute(self, blackboard) -> NodeStatus: - """执行等待""" - current_time = time.time() - - # 如果是第一次执行,计算实际等待时间 - if self.start_time == 0.0: - self.start_time = current_time - # 添加随机变化 - if self.random_variance > 0: - variance = random.uniform(-self.random_variance, self.random_variance) - self.actual_duration = max(0.0, self.base_duration + variance) - else: - self.actual_duration = self.base_duration - - # 检查是否等待完成 - elapsed = current_time - self.start_time - if elapsed >= self.actual_duration: - # 等待完成,重置开始时间 - self.start_time = 0.0 - return NodeStatus.SUCCESS - else: - # 更新进度 - if self.progress_callback: - progress = min(1.0, elapsed / self.actual_duration) - try: - self.progress_callback(progress) - except Exception as e: - print(f"进度回调执行出错: {e}") - return NodeStatus.RUNNING - - def reset(self) -> None: - """重置节点状态""" - super().reset() - self.start_time = 0.0 - self.actual_duration = self.base_duration - -class SubtreeNode(BTNode): - """ - 子树节点 - 执行另一个行为树作为子树 - """ - - def __init__(self, config: Union[BTNodeConfig, str] = "Subtree", - subtree: Optional['BehaviorTree'] = None): - super().__init__(config) - self.subtree = subtree - self.is_running = False - - def execute(self, blackboard) -> NodeStatus: - """执行子树""" - if not self.subtree: - return NodeStatus.FAILURE - - # 设置子树的黑板 - self.subtree.set_blackboard(blackboard) - - # 执行子树 - try: - result = self.subtree.run() - return result - except Exception as e: - print(f"子树执行出错: {e}") - return NodeStatus.FAILURE - - def set_subtree(self, subtree: 'BehaviorTree') -> None: - """设置子树""" - self.subtree = subtree - - def get_subtree(self) -> Optional['BehaviorTree']: - """获取子树""" - return self.subtree - -class ProbabilityNode(ActionNode): - """ - 概率节点 - 根据概率决定执行结果 - """ - - def __init__(self, config: Union[BTNodeConfig, str] = "Probability", - success_probability: float = 0.5, # 成功概率 - action_func: Optional[Callable[[Any], Any]] = None): - super().__init__(config, action_func) - self.success_probability = max(0.0, min(1.0, success_probability)) - self.has_executed = False - self.result = NodeStatus.FAILURE - - def execute(self, blackboard) -> NodeStatus: - """执行概率动作""" - # 如果已经执行过,返回之前的结果 - if self.has_executed: - return self.result - - # 执行实际动作(如果提供了) - if self.action_func: - try: - action_result = self.action_func(blackboard) - # 如果动作返回了具体状态,使用该状态 - if action_result == NodeStatus.SUCCESS: - self.result = NodeStatus.SUCCESS - elif action_result == NodeStatus.FAILURE: - self.result = NodeStatus.FAILURE - elif action_result == NodeStatus.RUNNING: - return NodeStatus.RUNNING - except Exception as e: - print(f"概率节点动作执行出错: {e}") - self.result = NodeStatus.FAILURE - else: - # 根据概率决定结果 - if random.random() < self.success_probability: - self.result = NodeStatus.SUCCESS - else: - self.result = NodeStatus.FAILURE - - self.has_executed = True - return self.result - - def reset(self) -> None: - """重置节点状态""" - super().reset() - self.has_executed = False - self.result = NodeStatus.FAILURE - -class CounterNode(ConditionNode): - """ - 计数器节点 - 根据执行次数决定结果 - """ - - def __init__(self, config: Union[BTNodeConfig, str] = "Counter", - target_count: int = 1, # 目标计数 - reset_on_target: bool = True, # 达到目标后是否重置 - condition_func: Optional[Callable[[Any], bool]] = None): - super().__init__(config, condition_func) - self.target_count = max(1, target_count) - self.reset_on_target = reset_on_target - self.current_count = 0 - - def _evaluate_condition(self, blackboard) -> bool: - """评估条件""" - # 首先检查提供的条件函数 - if self.condition_func: - try: - if not self.condition_func(blackboard): - return False - except Exception as e: - print(f"计数器节点条件检查出错: {e}") - return False - - # 增加计数 - self.current_count += 1 - - # 检查是否达到目标计数 - result = self.current_count >= self.target_count - - # 如果达到目标且需要重置,重置计数器 - if result and self.reset_on_target: - self.current_count = 0 - - return result - - def reset(self) -> None: - """重置节点状态""" - super().reset() - self.current_count = 0 - -class TimeoutNode(ActionNode): - """ - 超时节点 - 限制动作执行时间 - """ - - def __init__(self, config: Union[BTNodeConfig, str] = "Timeout", - action_func: Optional[Callable[[Any], Any]] = None, - timeout: float = 5.0): # 超时时间(秒) - super().__init__(config, action_func) - self.timeout = timeout - self.start_time = 0.0 - - def execute(self, blackboard) -> NodeStatus: - """执行带超时的动作""" - current_time = time.time() - - # 记录开始时间 - if self.start_time == 0.0: - self.start_time = current_time - - # 检查是否超时 - if current_time - self.start_time >= self.timeout: - # 超时,返回失败 - self.start_time = 0.0 - return NodeStatus.FAILURE - - # 执行动作 - if self.action_func: - try: - result = self.action_func(blackboard) - - # 如果动作完成,重置开始时间 - if result != NodeStatus.RUNNING: - self.start_time = 0.0 - - if result is True: - return NodeStatus.SUCCESS - elif result is False: - return NodeStatus.FAILURE - elif result == NodeStatus.RUNNING: - return NodeStatus.RUNNING - else: - # 默认返回成功 - self.start_time = 0.0 - return NodeStatus.SUCCESS - except Exception as e: - print(f"超时节点动作执行出错: {e}") - self.start_time = 0.0 - return NodeStatus.FAILURE - else: - # 如果没有提供动作函数,默认返回成功 - self.start_time = 0.0 - return NodeStatus.SUCCESS - - def reset(self) -> None: - """重置节点状态""" - super().reset() +""" +行为树叶子节点 +包括条件节点、动作节点、等待节点等 +提供多种叶子节点实现 +""" + +import time +import random +import math +from typing import Optional, Callable, Any, Union +from ..core.behavior_tree import BTNode, NodeStatus, BTNodeConfig + +class ConditionNode(BTNode): + """ + 条件节点 + 检查条件是否满足 + 支持多种条件检查方式和缓存机制 + """ + + def __init__(self, config: Union[BTNodeConfig, str] = "Condition", + condition_func: Optional[Callable[[Any], bool]] = None, + cache_result: bool = False, # 是否缓存结果 + cache_duration: float = 0.0): # 缓存持续时间(秒) + super().__init__(config) + self.condition_func = condition_func + self.cache_result = cache_result + self.cache_duration = cache_duration + self.cached_result: Optional[bool] = None + self.cache_time: float = 0.0 + + def execute(self, blackboard) -> NodeStatus: + """执行条件检查""" + current_time = time.time() + + # 检查缓存 + if (self.cache_result and + self.cached_result is not None and + (self.cache_duration <= 0 or + current_time - self.cache_time < self.cache_duration)): + # 使用缓存结果 + return NodeStatus.SUCCESS if self.cached_result else NodeStatus.FAILURE + + # 执行条件检查 + result = self._evaluate_condition(blackboard) + + # 更新缓存 + if self.cache_result: + self.cached_result = result + self.cache_time = current_time + + return NodeStatus.SUCCESS if result else NodeStatus.FAILURE + + def _evaluate_condition(self, blackboard) -> bool: + """评估条件""" + if self.condition_func: + try: + return bool(self.condition_func(blackboard)) + except Exception as e: + print(f"条件节点执行出错: {e}") + return False + else: + # 如果没有提供条件函数,默认返回成功 + return True + + def invalidate_cache(self) -> None: + """使缓存失效""" + self.cached_result = None + self.cache_time = 0.0 + + def reset(self) -> None: + """重置节点状态""" + super().reset() + self.invalidate_cache() + +class ActionNode(BTNode): + """ + 动作节点 + 执行具体行为 + 支持异步执行和进度跟踪 + """ + + def __init__(self, config: Union[BTNodeConfig, str] = "Action", + action_func: Optional[Callable[[Any], Any]] = None, + async_action: bool = False, # 是否异步执行 + progress_callback: Optional[Callable[[float], None]] = None): # 进度回调 + super().__init__(config) + self.action_func = action_func + self.async_action = async_action + self.progress_callback = progress_callback + self.is_running = False + self.start_time = 0.0 + self.estimated_duration = 0.0 # 估计执行时间 + + def execute(self, blackboard) -> NodeStatus: + """执行动作""" + current_time = time.time() + + # 如果是异步动作且正在运行 + if self.async_action and self.is_running: + # 检查动作是否完成 + if self._is_async_action_complete(blackboard): + self.is_running = False + self.start_time = 0.0 + return NodeStatus.SUCCESS + else: + # 更新进度 + if self.progress_callback: + progress = self._calculate_progress(current_time) + try: + self.progress_callback(progress) + except Exception as e: + print(f"进度回调执行出错: {e}") + return NodeStatus.RUNNING + + # 执行动作 + if self.action_func: + try: + # 记录开始时间 + self.start_time = current_time + + if self.async_action: + # 异步执行 + result = self._start_async_action(blackboard) + if result == NodeStatus.RUNNING: + self.is_running = True + return NodeStatus.RUNNING + else: + self.start_time = 0.0 + return result + else: + # 同步执行 + result = self.action_func(blackboard) + self.start_time = 0.0 + + if result is True: + return NodeStatus.SUCCESS + elif result is False: + return NodeStatus.FAILURE + elif result == NodeStatus.RUNNING: + return NodeStatus.RUNNING + else: + # 默认返回成功 + return NodeStatus.SUCCESS + + except Exception as e: + print(f"动作节点执行出错: {e}") + self.is_running = False + self.start_time = 0.0 + return NodeStatus.FAILURE + else: + # 如果没有提供动作函数,默认返回成功 + return NodeStatus.SUCCESS + + def _start_async_action(self, blackboard) -> NodeStatus: + """启动异步动作""" + # 这里应该启动异步操作,例如创建线程或任务 + # 为了简化,我们假设异步动作立即开始并需要继续运行 + return NodeStatus.RUNNING + + def _is_async_action_complete(self, blackboard) -> bool: + """检查异步动作是否完成""" + # 这里应该检查异步操作是否完成 + # 为了简化,我们随机决定是否完成 + if self.estimated_duration > 0: + current_time = time.time() + elapsed = current_time - self.start_time + return elapsed >= self.estimated_duration + else: + # 随机完成(用于演示) + return random.random() < 0.1 # 10%概率完成 + + def _calculate_progress(self, current_time: float) -> float: + """计算进度""" + if self.estimated_duration > 0: + elapsed = current_time - self.start_time + progress = min(1.0, elapsed / self.estimated_duration) + return max(0.0, progress) + else: + return 0.0 + + def reset(self) -> None: + """重置节点状态""" + super().reset() + self.is_running = False + self.start_time = 0.0 + +class WaitNode(ActionNode): + """ + 等待节点 + 等待指定时间后返回成功 + 支持随机等待时间和进度回调 + """ + + def __init__(self, config: Union[BTNodeConfig, str] = "Wait", + duration: float = 1.0, + random_variance: float = 0.0, # 随机变化范围(秒) + progress_callback: Optional[Callable[[float], None]] = None): + super().__init__(config, None, False, progress_callback) + self.base_duration = duration + self.random_variance = random_variance + self.actual_duration = duration # 实际等待时间 + self.start_time = 0.0 + + def execute(self, blackboard) -> NodeStatus: + """执行等待""" + current_time = time.time() + + # 如果是第一次执行,计算实际等待时间 + if self.start_time == 0.0: + self.start_time = current_time + # 添加随机变化 + if self.random_variance > 0: + variance = random.uniform(-self.random_variance, self.random_variance) + self.actual_duration = max(0.0, self.base_duration + variance) + else: + self.actual_duration = self.base_duration + + # 检查是否等待完成 + elapsed = current_time - self.start_time + if elapsed >= self.actual_duration: + # 等待完成,重置开始时间 + self.start_time = 0.0 + return NodeStatus.SUCCESS + else: + # 更新进度 + if self.progress_callback: + progress = min(1.0, elapsed / self.actual_duration) + try: + self.progress_callback(progress) + except Exception as e: + print(f"进度回调执行出错: {e}") + return NodeStatus.RUNNING + + def reset(self) -> None: + """重置节点状态""" + super().reset() + self.start_time = 0.0 + self.actual_duration = self.base_duration + +class SubtreeNode(BTNode): + """ + 子树节点 + 执行另一个行为树作为子树 + """ + + def __init__(self, config: Union[BTNodeConfig, str] = "Subtree", + subtree: Optional['BehaviorTree'] = None): + super().__init__(config) + self.subtree = subtree + self.is_running = False + + def execute(self, blackboard) -> NodeStatus: + """执行子树""" + if not self.subtree: + return NodeStatus.FAILURE + + # 设置子树的黑板 + self.subtree.set_blackboard(blackboard) + + # 执行子树 + try: + result = self.subtree.run() + return result + except Exception as e: + print(f"子树执行出错: {e}") + return NodeStatus.FAILURE + + def set_subtree(self, subtree: 'BehaviorTree') -> None: + """设置子树""" + self.subtree = subtree + + def get_subtree(self) -> Optional['BehaviorTree']: + """获取子树""" + return self.subtree + +class ProbabilityNode(ActionNode): + """ + 概率节点 + 根据概率决定执行结果 + """ + + def __init__(self, config: Union[BTNodeConfig, str] = "Probability", + success_probability: float = 0.5, # 成功概率 + action_func: Optional[Callable[[Any], Any]] = None): + super().__init__(config, action_func) + self.success_probability = max(0.0, min(1.0, success_probability)) + self.has_executed = False + self.result = NodeStatus.FAILURE + + def execute(self, blackboard) -> NodeStatus: + """执行概率动作""" + # 如果已经执行过,返回之前的结果 + if self.has_executed: + return self.result + + # 执行实际动作(如果提供了) + if self.action_func: + try: + action_result = self.action_func(blackboard) + # 如果动作返回了具体状态,使用该状态 + if action_result == NodeStatus.SUCCESS: + self.result = NodeStatus.SUCCESS + elif action_result == NodeStatus.FAILURE: + self.result = NodeStatus.FAILURE + elif action_result == NodeStatus.RUNNING: + return NodeStatus.RUNNING + except Exception as e: + print(f"概率节点动作执行出错: {e}") + self.result = NodeStatus.FAILURE + else: + # 根据概率决定结果 + if random.random() < self.success_probability: + self.result = NodeStatus.SUCCESS + else: + self.result = NodeStatus.FAILURE + + self.has_executed = True + return self.result + + def reset(self) -> None: + """重置节点状态""" + super().reset() + self.has_executed = False + self.result = NodeStatus.FAILURE + +class CounterNode(ConditionNode): + """ + 计数器节点 + 根据执行次数决定结果 + """ + + def __init__(self, config: Union[BTNodeConfig, str] = "Counter", + target_count: int = 1, # 目标计数 + reset_on_target: bool = True, # 达到目标后是否重置 + condition_func: Optional[Callable[[Any], bool]] = None): + super().__init__(config, condition_func) + self.target_count = max(1, target_count) + self.reset_on_target = reset_on_target + self.current_count = 0 + + def _evaluate_condition(self, blackboard) -> bool: + """评估条件""" + # 首先检查提供的条件函数 + if self.condition_func: + try: + if not self.condition_func(blackboard): + return False + except Exception as e: + print(f"计数器节点条件检查出错: {e}") + return False + + # 增加计数 + self.current_count += 1 + + # 检查是否达到目标计数 + result = self.current_count >= self.target_count + + # 如果达到目标且需要重置,重置计数器 + if result and self.reset_on_target: + self.current_count = 0 + + return result + + def reset(self) -> None: + """重置节点状态""" + super().reset() + self.current_count = 0 + +class TimeoutNode(ActionNode): + """ + 超时节点 + 限制动作执行时间 + """ + + def __init__(self, config: Union[BTNodeConfig, str] = "Timeout", + action_func: Optional[Callable[[Any], Any]] = None, + timeout: float = 5.0): # 超时时间(秒) + super().__init__(config, action_func) + self.timeout = timeout + self.start_time = 0.0 + + def execute(self, blackboard) -> NodeStatus: + """执行带超时的动作""" + current_time = time.time() + + # 记录开始时间 + if self.start_time == 0.0: + self.start_time = current_time + + # 检查是否超时 + if current_time - self.start_time >= self.timeout: + # 超时,返回失败 + self.start_time = 0.0 + return NodeStatus.FAILURE + + # 执行动作 + if self.action_func: + try: + result = self.action_func(blackboard) + + # 如果动作完成,重置开始时间 + if result != NodeStatus.RUNNING: + self.start_time = 0.0 + + if result is True: + return NodeStatus.SUCCESS + elif result is False: + return NodeStatus.FAILURE + elif result == NodeStatus.RUNNING: + return NodeStatus.RUNNING + else: + # 默认返回成功 + self.start_time = 0.0 + return NodeStatus.SUCCESS + except Exception as e: + print(f"超时节点动作执行出错: {e}") + self.start_time = 0.0 + return NodeStatus.FAILURE + else: + # 如果没有提供动作函数,默认返回成功 + self.start_time = 0.0 + return NodeStatus.SUCCESS + + def reset(self) -> None: + """重置节点状态""" + super().reset() self.start_time = 0.0 \ No newline at end of file diff --git a/plugins/user/behavior_tree/plugin.py b/plugins/user/behavior_tree/plugin.py index f68c8ce9..32d1e0e2 100644 --- a/plugins/user/behavior_tree/plugin.py +++ b/plugins/user/behavior_tree/plugin.py @@ -1,229 +1,229 @@ -""" -行为树插件 -提供完整的行为树功能用于实现AI决策逻辑 -包含节点系统、黑板系统、管理器等完整功能 -""" - -from plugins.plugin_manager import BasePlugin -from typing import Dict, Optional, Any - -class Plugin(BasePlugin): - """ - 行为树插件 - 提供完整的行为树功能用于实现AI决策逻辑 - """ - - def __init__(self, plugin_manager, name): - super().__init__(plugin_manager, name) - self.config = { - "version": "1.0.0", - "author": "EG Team", - "description": "行为树插件,用于实现AI决策逻辑,包含完整的节点系统、黑板系统和管理器" - } - self.behavior_trees = {} - self.blackboards = {} - self.tree_manager = None - self.blackboard_manager = None - self.is_initialized = False - - def initialize(self) -> bool: - """ - 初始化插件 - """ - if self.is_initialized: - return True - - try: - # 导入核心模块 - from plugins.user.behavior_tree.core.behavior_tree import BehaviorTreeManager - from plugins.user.behavior_tree.core.blackboard import BlackboardManager - - # 创建管理器实例 - self.tree_manager = BehaviorTreeManager() - self.blackboard_manager = BlackboardManager() - - print(f"初始化行为树插件: {self.name}") - self.is_initialized = True - return True - - except Exception as e: - print(f"行为树插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - """ - if not super().enable(): - return False - - if not self.is_initialized: - if not self.initialize(): - return False - - print(f"启用行为树插件: {self.name}") - return True - - def disable(self) -> bool: - """ - 禁用插件 - """ - if not super().disable(): - return False - - # 停止所有激活的行为树 - if self.tree_manager: - self.tree_manager.active_trees.clear() - - print(f"禁用行为树插件: {self.name}") - return True - - def finalize(self) -> None: - """ - 清理插件资源 - """ - print(f"清理行为树插件资源: {self.name}") - - try: - # 停止所有激活的行为树 - if self.tree_manager: - self.tree_manager.active_trees.clear() - - # 清理资源 - self.behavior_trees.clear() - self.blackboards.clear() - self.tree_manager = None - self.blackboard_manager = None - self.is_initialized = False - - print(f"行为树插件资源清理完成: {self.name}") - - except Exception as e: - print(f"清理行为树插件资源失败: {e}") - import traceback - traceback.print_exc() - - def create_behavior_tree(self, name: str, root_node) -> Optional['BehaviorTree']: - """ - 创建行为树 - """ - try: - from plugins.user.behavior_tree.core.behavior_tree import BehaviorTree - tree = BehaviorTree(name, root_node) - self.behavior_trees[name] = tree - - # 添加到管理器 - if self.tree_manager: - self.tree_manager.add_tree(name, tree) - - return tree - except Exception as e: - print(f"创建行为树失败: {e}") - return None - - def create_blackboard(self, name: str, **kwargs) -> Optional['Blackboard']: - """ - 创建黑板 - """ - try: - if self.blackboard_manager: - blackboard = self.blackboard_manager.create_blackboard(name, **kwargs) - self.blackboards[name] = blackboard - return blackboard - else: - from plugins.user.behavior_tree.core.blackboard import Blackboard - blackboard = Blackboard(name, **kwargs) - self.blackboards[name] = blackboard - return blackboard - except Exception as e: - print(f"创建黑板失败: {e}") - return None - - def get_behavior_tree(self, name: str) -> Optional['BehaviorTree']: - """ - 获取行为树实例 - """ - return self.behavior_trees.get(name) - - def get_blackboard(self, name: str) -> Optional['Blackboard']: - """ - 获取黑板实例 - """ - return self.blackboards.get(name) - - def remove_behavior_tree(self, name: str) -> bool: - """ - 移除行为树 - """ - if name in self.behavior_trees: - # 从管理器移除 - if self.tree_manager: - self.tree_manager.remove_tree(name) - del self.behavior_trees[name] - return True - return False - - def remove_blackboard(self, name: str) -> bool: - """ - 移除黑板 - """ - if name in self.blackboards: - # 从管理器移除 - if self.blackboard_manager: - self.blackboard_manager.remove_blackboard(name) - del self.blackboards[name] - return True - return False - - def get_tree_manager(self) -> Optional['BehaviorTreeManager']: - """ - 获取行为树管理器 - """ - return self.tree_manager - - def get_blackboard_manager(self) -> Optional['BlackboardManager']: - """ - 获取黑板管理器 - """ - return self.blackboard_manager - - def update_all_trees(self, dt: float = None) -> None: - """ - 更新所有激活的行为树 - """ - if self.tree_manager: - self.tree_manager.update_all(dt) - - def get_tree_statistics(self, name: str) -> Optional[Dict[str, Any]]: - """ - 获取行为树统计信息 - """ - if self.tree_manager: - return self.tree_manager.get_tree_stats(name) - return None - - def get_all_statistics(self) -> Dict[str, Dict[str, Any]]: - """ - 获取所有行为树统计信息 - """ - if self.tree_manager: - return self.tree_manager.get_all_stats() - return {} - - def reset_all_trees(self) -> None: - """ - 重置所有行为树 - """ - if self.tree_manager: - # 重置管理器中的所有树 - for tree in self.tree_manager.trees.values(): - tree.reset() - - # 重置插件中的所有树 - for tree in self.behavior_trees.values(): - tree.reset() - -# 兼容性接口 +""" +行为树插件 +提供完整的行为树功能用于实现AI决策逻辑 +包含节点系统、黑板系统、管理器等完整功能 +""" + +from plugins.plugin_manager import BasePlugin +from typing import Dict, Optional, Any + +class Plugin(BasePlugin): + """ + 行为树插件 + 提供完整的行为树功能用于实现AI决策逻辑 + """ + + def __init__(self, plugin_manager, name): + super().__init__(plugin_manager, name) + self.config = { + "version": "1.0.0", + "author": "EG Team", + "description": "行为树插件,用于实现AI决策逻辑,包含完整的节点系统、黑板系统和管理器" + } + self.behavior_trees = {} + self.blackboards = {} + self.tree_manager = None + self.blackboard_manager = None + self.is_initialized = False + + def initialize(self) -> bool: + """ + 初始化插件 + """ + if self.is_initialized: + return True + + try: + # 导入核心模块 + from plugins.user.behavior_tree.core.behavior_tree import BehaviorTreeManager + from plugins.user.behavior_tree.core.blackboard import BlackboardManager + + # 创建管理器实例 + self.tree_manager = BehaviorTreeManager() + self.blackboard_manager = BlackboardManager() + + print(f"初始化行为树插件: {self.name}") + self.is_initialized = True + return True + + except Exception as e: + print(f"行为树插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + """ + if not super().enable(): + return False + + if not self.is_initialized: + if not self.initialize(): + return False + + print(f"启用行为树插件: {self.name}") + return True + + def disable(self) -> bool: + """ + 禁用插件 + """ + if not super().disable(): + return False + + # 停止所有激活的行为树 + if self.tree_manager: + self.tree_manager.active_trees.clear() + + print(f"禁用行为树插件: {self.name}") + return True + + def finalize(self) -> None: + """ + 清理插件资源 + """ + print(f"清理行为树插件资源: {self.name}") + + try: + # 停止所有激活的行为树 + if self.tree_manager: + self.tree_manager.active_trees.clear() + + # 清理资源 + self.behavior_trees.clear() + self.blackboards.clear() + self.tree_manager = None + self.blackboard_manager = None + self.is_initialized = False + + print(f"行为树插件资源清理完成: {self.name}") + + except Exception as e: + print(f"清理行为树插件资源失败: {e}") + import traceback + traceback.print_exc() + + def create_behavior_tree(self, name: str, root_node) -> Optional['BehaviorTree']: + """ + 创建行为树 + """ + try: + from plugins.user.behavior_tree.core.behavior_tree import BehaviorTree + tree = BehaviorTree(name, root_node) + self.behavior_trees[name] = tree + + # 添加到管理器 + if self.tree_manager: + self.tree_manager.add_tree(name, tree) + + return tree + except Exception as e: + print(f"创建行为树失败: {e}") + return None + + def create_blackboard(self, name: str, **kwargs) -> Optional['Blackboard']: + """ + 创建黑板 + """ + try: + if self.blackboard_manager: + blackboard = self.blackboard_manager.create_blackboard(name, **kwargs) + self.blackboards[name] = blackboard + return blackboard + else: + from plugins.user.behavior_tree.core.blackboard import Blackboard + blackboard = Blackboard(name, **kwargs) + self.blackboards[name] = blackboard + return blackboard + except Exception as e: + print(f"创建黑板失败: {e}") + return None + + def get_behavior_tree(self, name: str) -> Optional['BehaviorTree']: + """ + 获取行为树实例 + """ + return self.behavior_trees.get(name) + + def get_blackboard(self, name: str) -> Optional['Blackboard']: + """ + 获取黑板实例 + """ + return self.blackboards.get(name) + + def remove_behavior_tree(self, name: str) -> bool: + """ + 移除行为树 + """ + if name in self.behavior_trees: + # 从管理器移除 + if self.tree_manager: + self.tree_manager.remove_tree(name) + del self.behavior_trees[name] + return True + return False + + def remove_blackboard(self, name: str) -> bool: + """ + 移除黑板 + """ + if name in self.blackboards: + # 从管理器移除 + if self.blackboard_manager: + self.blackboard_manager.remove_blackboard(name) + del self.blackboards[name] + return True + return False + + def get_tree_manager(self) -> Optional['BehaviorTreeManager']: + """ + 获取行为树管理器 + """ + return self.tree_manager + + def get_blackboard_manager(self) -> Optional['BlackboardManager']: + """ + 获取黑板管理器 + """ + return self.blackboard_manager + + def update_all_trees(self, dt: float = None) -> None: + """ + 更新所有激活的行为树 + """ + if self.tree_manager: + self.tree_manager.update_all(dt) + + def get_tree_statistics(self, name: str) -> Optional[Dict[str, Any]]: + """ + 获取行为树统计信息 + """ + if self.tree_manager: + return self.tree_manager.get_tree_stats(name) + return None + + def get_all_statistics(self) -> Dict[str, Dict[str, Any]]: + """ + 获取所有行为树统计信息 + """ + if self.tree_manager: + return self.tree_manager.get_all_stats() + return {} + + def reset_all_trees(self) -> None: + """ + 重置所有行为树 + """ + if self.tree_manager: + # 重置管理器中的所有树 + for tree in self.tree_manager.trees.values(): + tree.reset() + + # 重置插件中的所有树 + for tree in self.behavior_trees.values(): + tree.reset() + +# 兼容性接口 Plugin = Plugin \ No newline at end of file diff --git a/plugins/user/dynamic_music_system/README.md b/plugins/user/dynamic_music_system/README.md index 556cc57e..8df6d55b 100644 --- a/plugins/user/dynamic_music_system/README.md +++ b/plugins/user/dynamic_music_system/README.md @@ -1,425 +1,425 @@ -# 动态音乐系统插件 - -一个功能完整的动态音乐系统插件,为EG引擎提供基于游戏状态和环境条件的实时音乐生成和控制系统,支持音乐主题管理、智能过渡、程序化作曲、状态分析和音频效果处理。 - -## 功能特性 - -### 核心功能 -- **智能音乐主题管理**: 支持音乐主题的加载、播放、停止和控制 -- **平滑音乐过渡**: 提供多种过渡类型(淡入淡出、直接切换、刺音过渡等) -- **程序化音乐作曲**: 实时生成旋律、和声、节奏和贝斯线 -- **游戏状态分析**: 分析游戏状态、环境条件和玩家状态,自动调整音乐 -- **音频效果处理**: 提供多种音频效果(混响、合唱、延迟、滤波器等) -- **动态音乐控制**: 根据游戏情境实时调整音乐强度和风格 -- **性能优化**: 高效的音乐处理算法和资源管理 - -### 音乐主题系统 -- **主题管理**: 创建、加载、播放和控制音乐主题 -- **层次化结构**: 支持多层音乐结构(基础层、旋律层、节奏层等) -- **动态音量控制**: 实时调整各音乐层的音量 -- **淡入淡出**: 支持平滑的音乐淡入淡出效果 -- **预加载机制**: 提前加载音乐资源,减少播放延迟 -- **主题状态跟踪**: 实时监控音乐主题的播放状态 - -### 音乐过渡系统 -- **多种过渡类型**: - - 交叉淡入淡出:平滑过渡两个音乐主题 - - 直接切换:立即切换音乐主题 - - 刺音过渡:使用刺音效果连接两个主题 -- **过渡参数控制**: 可自定义过渡持续时间、曲线类型和优先级 -- **智能过渡管理**: 自动处理过渡冲突和优先级 -- **动态过渡创建**: 根据游戏状态动态创建过渡效果 -- **过渡状态监控**: 实时跟踪过渡执行状态和进度 - -### 程序化作曲系统 -- **音乐生成器**: 支持旋律、和声、节奏和贝斯等多种音乐生成器 -- **音乐理论支持**: 内置音阶、和弦和音乐理论知识 -- **参数化作曲**: 通过复杂度、变化度等参数控制音乐生成 -- **实时作曲**: 动态生成音乐片段,避免重复 -- **自适应作曲**: 根据游戏状态调整音乐风格和强度 -- **音色支持**: 支持多种乐器音色模拟 - -### 游戏状态分析系统 -- **多维度分析**: 分析战斗、探索、环境、玩家状态和叙事节奏 -- **权重系统**: 可配置不同分析维度的权重 -- **历史数据分析**: 基于历史数据预测音乐趋势 -- **自定义分析器**: 支持创建自定义游戏状态分析器 -- **强度计算**: 综合计算音乐强度值,指导音乐选择 -- **趋势预测**: 预测音乐强度变化趋势 - -### 音频效果系统 -- **丰富效果类型**: - - 混响:营造不同空间感 - - 合唱:增加音乐厚度 - - 延迟:创建回声效果 - - 滤波器:调整频率响应 - - 失真:添加过载效果 - - 移相器/镶边:创建调制效果 - - 压缩器:控制动态范围 - - 均衡器:精确频段调整 -- **效果链处理**: 支持多个效果的串联处理 -- **动态效果**: 效果参数可根据游戏状态动态调整 -- **湿/干混合**: 精确控制效果强度 -- **效果旁路**: 实时启用/禁用特定效果 - -### 动态控制 -- **强度系统**: 0.0-1.0范围的音乐强度控制 -- **自动强度调整**: 根据游戏状态自动调整音乐强度 -- **手动强度控制**: 支持手动设置音乐强度 -- **平滑过渡**: 强度变化平滑过渡,避免突兀变化 -- **情境匹配**: 根据游戏情境自动选择合适的音乐主题 - -### 性能优化 -- **资源管理**: 智能的音频资源加载和卸载 -- **内存优化**: 高效的内存使用和垃圾回收 -- **计算优化**: 优化的音频处理算法 -- **多线程支持**: 支持后台音乐生成和处理 -- **缓存机制**: 缓存常用音乐片段和效果参数 - -## 使用方法 - -### 快捷键操作 -- **F1**: 切换调试模式 -- **F2**: 切换音乐系统开关 -- **F3**: 播放测试音乐主题 -- **F4**: 增加音乐强度 -- **F5**: 降低音乐强度 -- **F6**: 触发战斗音乐 -- **F7**: 触发探索音乐 -- **F8**: 触发环境音乐 -- **F9**: 显示音乐系统统计信息 -- **F10**: 切换自动强度控制 -- **F11**: 显示详细统计信息 -- **F12**: 重置音乐系统 - -### GUI界面操作 -插件会在屏幕右上角创建控制面板,可以通过按钮直接操作音乐系统功能。 - -### 编程接口 -```python -# 获取插件实例 -music_plugin = world.plugin_manager.get_plugin("dynamic_music_system") - -# 创建音乐主题 -theme_data = { - 'layers': ['ambient_base', 'ambient_high'], - 'bpm': 60, - 'key': 'Am', - 'mood': 'calm' -} -music_plugin.create_theme("ambient_theme", theme_data) - -# 播放音乐主题 -music_plugin.play_theme("ambient_theme") - -# 设置音乐强度 -music_plugin.set_intensity(0.7) - -# 获取当前强度 -current_intensity = music_plugin.get_intensity() - -# 创建音乐过渡 -transition_data = { - 'from_theme': 'ambient_theme', - 'to_theme': 'battle_theme', - 'duration': 3.0, - 'type': 'crossfade' -} -music_plugin.create_transition("ambient_to_battle", transition_data) - -# 触发过渡 -music_plugin.trigger_transition("ambient_to_battle") - -# 设置游戏状态 -music_plugin.set_game_state({ - 'in_combat': False, - 'exploring': True, - 'combat_intensity': 0.3 -}) - -# 设置环境条件 -music_plugin.set_environment_conditions({ - 'weather': 'rain', - 'time_of_day': 'night', - 'location_type': 'outdoor' -}) - -# 设置玩家状态 -music_plugin.set_player_state({ - 'health': 0.8, - 'stamina': 0.6, - 'emotional_state': 'curious' -}) - -# 设置主音量 -music_plugin.set_master_volume(0.8) - -# 获取主音量 -master_volume = music_plugin.get_master_volume() - -# 创建音频效果 -reverb_params = { - 'room_size': 0.7, - 'damping': 0.5, - 'wet_level': 0.4 -} -music_plugin.effect_processor.create_effect("hall_reverb", "reverb", reverb_params) - -# 激活效果 -music_plugin.effect_processor.activate_effect("hall_reverb") - -# 创建效果链 -music_plugin.effect_processor.create_effect_chain("ambient_chain", ["hall_reverb"]) - -# 应用效果链 -processed_audio = music_plugin.effect_processor.apply_effects(raw_audio, "ambient_chain") -``` - -## 核心组件 - -### 音乐管理器 -负责音乐主题的加载、播放和控制。 - -主要功能: -- 音乐主题管理 -- 音乐层控制 -- 音量和淡入淡出控制 -- 资源预加载 - -### 过渡管理器 -负责音乐主题之间的平滑过渡。 - -主要功能: -- 多种过渡类型支持 -- 过渡优先级管理 -- 动态过渡创建 -- 过渡状态监控 - -### 作曲引擎 -负责程序化音乐生成。 - -主要功能: -- 实时音乐生成 -- 音乐理论支持 -- 多类型生成器 -- 自适应作曲 - -### 分析引擎 -负责分析游戏状态和环境条件。 - -主要功能: -- 多维度状态分析 -- 强度计算和趋势预测 -- 自定义分析器 -- 历史数据分析 - -### 效果处理器 -负责音频效果的处理和应用。 - -主要功能: -- 多种音频效果 -- 效果链处理 -- 动态效果参数 -- 湿/干混合控制 - -## 音乐主题类型 - -### 环境音乐 (Ambient) -- **特点**: 轻柔、氛围感强、循环播放 -- **适用场景**: 主菜单、平静的探索、安全区域 -- **强度范围**: 0.0-0.4 - -### 探索音乐 (Exploration) -- **特点**: 节奏适中、富有动感、引导性强 -- **适用场景**: 世界探索、任务进行、一般移动 -- **强度范围**: 0.3-0.7 - -### 战斗音乐 (Battle) -- **特点**: 节奏快、强度高、紧张感强 -- **适用场景**: 战斗、追逐、危险情况 -- **强度范围**: 0.6-1.0 - -### 胜利音乐 (Victory) -- **特点**: 欢快、庆祝感、高潮性强 -- **适用场景**: 任务完成、战斗胜利、成就达成 -- **强度范围**: 0.8-1.0 - -### 失败音乐 (Defeat) -- **特点**: 低沉、悲伤、反思性强 -- **适用场景**: 任务失败、角色死亡、挫折情况 -- **强度范围**: 0.2-0.5 - -## 音乐强度等级 - -### 低强度 (0.0-0.3) -- **特征**: 安静、舒缓、氛围为主 -- **适用**: 安全区域、对话、菜单 -- **效果**: 最小化音频效果,轻柔混响 - -### 中低强度 (0.3-0.5) -- **特征**: 轻松、引导性、背景为主 -- **适用**: 一般探索、日常活动 -- **效果**: 适度混响和合唱效果 - -### 中等强度 (0.5-0.7) -- **特征**: 活跃、动感、注意力引导 -- **适用**: 轻度挑战、重要场景 -- **效果**: 增强节奏感,添加延迟效果 - -### 中高强度 (0.7-0.9) -- **特征**: 紧张、激烈、情绪推动 -- **适用**: 战斗、追逐、关键时刻 -- **效果**: 强烈效果链,高混响和失真 - -### 高强度 (0.9-1.0) -- **特征**: 激烈、高潮、情绪爆发 -- **适用**: BOSS战、剧情高潮、胜利时刻 -- **效果**: 最大化所有效果,创造强烈冲击 - -## 性能调优 - -### 资源管理 -1. **智能预加载**: 根据游戏状态预测并预加载可能需要的音乐 -2. **动态卸载**: 自动卸载长时间未使用的音乐资源 -3. **内存池**: 使用内存池管理音频缓冲区 -4. **流式加载**: 支持大音频文件的流式加载 - -### 计算优化 -1. **多线程处理**: 音乐生成和效果处理在后台线程执行 -2. **算法优化**: 使用优化的音频处理算法 -3. **缓存机制**: 缓存常用音频片段和处理结果 -4. **批量处理**: 批量处理音频数据以提高效率 - -### 效果优化 -1. **质量等级**: 可配置效果处理质量等级 -2. **动态调整**: 根据系统负载动态调整效果复杂度 -3. **效果链优化**: 优化效果链的处理顺序 -4. **参数插值**: 平滑效果参数变化,避免突兀 - -## 扩展开发 - -### 添加新的音乐主题 -1. 在[themes](file:///home/hello/EG/plugins/user/navigation_mesh/core/navmesh_manager.py#L20-L20)字典中添加主题数据 -2. 定义主题的音乐层和参数 -3. 实现主题的加载和播放逻辑 - -### 添加新的过渡类型 -1. 在[TransitionManager](file:///home/hello/EG/plugins/user/audio_reverb_effects/effects/spatial_processor.py#L15-L1056)中实现新的过渡方法 -2. 注册新的过渡类型 -3. 实现过渡的参数控制 - -### 添加新的音频效果 -1. 在[EffectProcessor](file:///home/hello/EG/plugins/user/audio_reverb_effects/effects/spatial_processor.py#L15-L1056)中实现新的效果处理方法 -2. 添加效果参数验证 -3. 实现效果的应用逻辑 - -### 添加新的分析维度 -1. 在[AnalysisEngine](file:///home/hello/EG/plugins/user/audio_reverb_effects/effects/spatial_processor.py#L15-L1056)中创建新的分析器 -2. 实现分析算法 -3. 注册分析器到权重系统 - -## 故障排除 - -### 音乐无法播放 -1. 检查音乐主题是否正确创建和加载 -2. 确认音乐系统是否已启用 -3. 验证音乐文件路径是否正确 -4. 检查音频设备是否正常工作 - -### 过渡效果异常 -1. 检查过渡参数是否正确设置 -2. 确认源主题和目标主题是否存在 -3. 验证过渡类型是否支持 -4. 检查是否有冲突的过渡正在进行 - -### 性能问题 -1. 使用统计信息分析性能瓶颈 -2. 降低音频效果质量等级 -3. 减少同时激活的效果数量 -4. 优化音乐生成参数 - -### 音质问题 -1. 检查采样率设置是否正确 -2. 确认音频文件质量 -3. 验证效果参数是否在合理范围 -4. 检查是否有削波现象 - -## API参考 - -### Plugin类主要方法 - -#### 初始化和控制 -- `initialize()` - 初始化插件 -- `enable()` - 启用插件 -- `disable()` - 禁用插件 -- `finalize()` - 清理插件资源 -- `update(dt)` - 更新插件状态 - -#### 音乐主题管理 -- `create_theme(theme_name, theme_data)` - 创建音乐主题 -- `play_theme(theme_name, force=False)` - 播放音乐主题 -- `stop_theme(theme_name)` - 停止音乐主题 -- `stop_all_music()` - 停止所有音乐 -- `set_layer_volume(layer_name, volume)` - 设置音乐层音量 - -#### 音乐过渡控制 -- `create_transition(transition_name, transition_data)` - 创建音乐过渡 -- `trigger_transition(transition_name)` - 触发音乐过渡 - -#### 状态控制 -- `set_intensity(intensity)` - 设置音乐强度 -- `get_intensity()` - 获取当前音乐强度 -- `set_game_state(state)` - 设置游戏状态 -- `set_environment_conditions(conditions)` - 设置环境条件 -- `set_player_state(state)` - 设置玩家状态 - -#### 音量控制 -- `set_master_volume(volume)` - 设置主音量 -- `get_master_volume()` - 获取主音量 - -#### 统计和信息 -- `get_stats()` - 获取插件统计信息 - -### 核心组件类 - -#### MusicManager -- `load_theme(theme_name, theme_data)` - 加载音乐主题 -- `play_theme(theme_name, force=False)` - 播放音乐主题 -- `stop_theme(theme_name)` - 停止音乐主题 -- `set_theme_volume(theme_name, volume)` - 设置主题音量 - -#### TransitionManager -- `register_transition(transition_name, transition_data)` - 注册音乐过渡 -- `trigger_transition(transition_name)` - 触发音乐过渡 - -#### CompositionEngine -- `create_generator(generator_name, generator_params)` - 创建音乐生成器 -- `activate_generator(generator_name)` - 激活音乐生成器 -- `generate_theme(theme_name, theme_params)` - 生成完整音乐主题 - -#### AnalysisEngine -- `create_analyzer(analyzer_name, analyzer_params)` - 创建分析器 -- `perform_analysis()` - 执行分析 -- `get_intensity_trend()` - 获取强度趋势 - -#### EffectProcessor -- `create_effect(effect_name, effect_type, effect_params)` - 创建音频效果 -- `activate_effect(effect_name)` - 激活音频效果 -- `apply_effects(audio_data, chain_name)` - 应用效果链 - -## 音频格式支持 - -支持的音频格式: -- **WAV**: 未压缩的PCM音频 -- **OGG**: Ogg Vorbis压缩音频 -- **MP3**: MPEG音频层III - -## 版本信息 - -- 版本: 1.0.0 -- 作者: EG Team -- 发布日期: 2024年 - -## 许可证 - +# 动态音乐系统插件 + +一个功能完整的动态音乐系统插件,为EG引擎提供基于游戏状态和环境条件的实时音乐生成和控制系统,支持音乐主题管理、智能过渡、程序化作曲、状态分析和音频效果处理。 + +## 功能特性 + +### 核心功能 +- **智能音乐主题管理**: 支持音乐主题的加载、播放、停止和控制 +- **平滑音乐过渡**: 提供多种过渡类型(淡入淡出、直接切换、刺音过渡等) +- **程序化音乐作曲**: 实时生成旋律、和声、节奏和贝斯线 +- **游戏状态分析**: 分析游戏状态、环境条件和玩家状态,自动调整音乐 +- **音频效果处理**: 提供多种音频效果(混响、合唱、延迟、滤波器等) +- **动态音乐控制**: 根据游戏情境实时调整音乐强度和风格 +- **性能优化**: 高效的音乐处理算法和资源管理 + +### 音乐主题系统 +- **主题管理**: 创建、加载、播放和控制音乐主题 +- **层次化结构**: 支持多层音乐结构(基础层、旋律层、节奏层等) +- **动态音量控制**: 实时调整各音乐层的音量 +- **淡入淡出**: 支持平滑的音乐淡入淡出效果 +- **预加载机制**: 提前加载音乐资源,减少播放延迟 +- **主题状态跟踪**: 实时监控音乐主题的播放状态 + +### 音乐过渡系统 +- **多种过渡类型**: + - 交叉淡入淡出:平滑过渡两个音乐主题 + - 直接切换:立即切换音乐主题 + - 刺音过渡:使用刺音效果连接两个主题 +- **过渡参数控制**: 可自定义过渡持续时间、曲线类型和优先级 +- **智能过渡管理**: 自动处理过渡冲突和优先级 +- **动态过渡创建**: 根据游戏状态动态创建过渡效果 +- **过渡状态监控**: 实时跟踪过渡执行状态和进度 + +### 程序化作曲系统 +- **音乐生成器**: 支持旋律、和声、节奏和贝斯等多种音乐生成器 +- **音乐理论支持**: 内置音阶、和弦和音乐理论知识 +- **参数化作曲**: 通过复杂度、变化度等参数控制音乐生成 +- **实时作曲**: 动态生成音乐片段,避免重复 +- **自适应作曲**: 根据游戏状态调整音乐风格和强度 +- **音色支持**: 支持多种乐器音色模拟 + +### 游戏状态分析系统 +- **多维度分析**: 分析战斗、探索、环境、玩家状态和叙事节奏 +- **权重系统**: 可配置不同分析维度的权重 +- **历史数据分析**: 基于历史数据预测音乐趋势 +- **自定义分析器**: 支持创建自定义游戏状态分析器 +- **强度计算**: 综合计算音乐强度值,指导音乐选择 +- **趋势预测**: 预测音乐强度变化趋势 + +### 音频效果系统 +- **丰富效果类型**: + - 混响:营造不同空间感 + - 合唱:增加音乐厚度 + - 延迟:创建回声效果 + - 滤波器:调整频率响应 + - 失真:添加过载效果 + - 移相器/镶边:创建调制效果 + - 压缩器:控制动态范围 + - 均衡器:精确频段调整 +- **效果链处理**: 支持多个效果的串联处理 +- **动态效果**: 效果参数可根据游戏状态动态调整 +- **湿/干混合**: 精确控制效果强度 +- **效果旁路**: 实时启用/禁用特定效果 + +### 动态控制 +- **强度系统**: 0.0-1.0范围的音乐强度控制 +- **自动强度调整**: 根据游戏状态自动调整音乐强度 +- **手动强度控制**: 支持手动设置音乐强度 +- **平滑过渡**: 强度变化平滑过渡,避免突兀变化 +- **情境匹配**: 根据游戏情境自动选择合适的音乐主题 + +### 性能优化 +- **资源管理**: 智能的音频资源加载和卸载 +- **内存优化**: 高效的内存使用和垃圾回收 +- **计算优化**: 优化的音频处理算法 +- **多线程支持**: 支持后台音乐生成和处理 +- **缓存机制**: 缓存常用音乐片段和效果参数 + +## 使用方法 + +### 快捷键操作 +- **F1**: 切换调试模式 +- **F2**: 切换音乐系统开关 +- **F3**: 播放测试音乐主题 +- **F4**: 增加音乐强度 +- **F5**: 降低音乐强度 +- **F6**: 触发战斗音乐 +- **F7**: 触发探索音乐 +- **F8**: 触发环境音乐 +- **F9**: 显示音乐系统统计信息 +- **F10**: 切换自动强度控制 +- **F11**: 显示详细统计信息 +- **F12**: 重置音乐系统 + +### GUI界面操作 +插件会在屏幕右上角创建控制面板,可以通过按钮直接操作音乐系统功能。 + +### 编程接口 +```python +# 获取插件实例 +music_plugin = world.plugin_manager.get_plugin("dynamic_music_system") + +# 创建音乐主题 +theme_data = { + 'layers': ['ambient_base', 'ambient_high'], + 'bpm': 60, + 'key': 'Am', + 'mood': 'calm' +} +music_plugin.create_theme("ambient_theme", theme_data) + +# 播放音乐主题 +music_plugin.play_theme("ambient_theme") + +# 设置音乐强度 +music_plugin.set_intensity(0.7) + +# 获取当前强度 +current_intensity = music_plugin.get_intensity() + +# 创建音乐过渡 +transition_data = { + 'from_theme': 'ambient_theme', + 'to_theme': 'battle_theme', + 'duration': 3.0, + 'type': 'crossfade' +} +music_plugin.create_transition("ambient_to_battle", transition_data) + +# 触发过渡 +music_plugin.trigger_transition("ambient_to_battle") + +# 设置游戏状态 +music_plugin.set_game_state({ + 'in_combat': False, + 'exploring': True, + 'combat_intensity': 0.3 +}) + +# 设置环境条件 +music_plugin.set_environment_conditions({ + 'weather': 'rain', + 'time_of_day': 'night', + 'location_type': 'outdoor' +}) + +# 设置玩家状态 +music_plugin.set_player_state({ + 'health': 0.8, + 'stamina': 0.6, + 'emotional_state': 'curious' +}) + +# 设置主音量 +music_plugin.set_master_volume(0.8) + +# 获取主音量 +master_volume = music_plugin.get_master_volume() + +# 创建音频效果 +reverb_params = { + 'room_size': 0.7, + 'damping': 0.5, + 'wet_level': 0.4 +} +music_plugin.effect_processor.create_effect("hall_reverb", "reverb", reverb_params) + +# 激活效果 +music_plugin.effect_processor.activate_effect("hall_reverb") + +# 创建效果链 +music_plugin.effect_processor.create_effect_chain("ambient_chain", ["hall_reverb"]) + +# 应用效果链 +processed_audio = music_plugin.effect_processor.apply_effects(raw_audio, "ambient_chain") +``` + +## 核心组件 + +### 音乐管理器 +负责音乐主题的加载、播放和控制。 + +主要功能: +- 音乐主题管理 +- 音乐层控制 +- 音量和淡入淡出控制 +- 资源预加载 + +### 过渡管理器 +负责音乐主题之间的平滑过渡。 + +主要功能: +- 多种过渡类型支持 +- 过渡优先级管理 +- 动态过渡创建 +- 过渡状态监控 + +### 作曲引擎 +负责程序化音乐生成。 + +主要功能: +- 实时音乐生成 +- 音乐理论支持 +- 多类型生成器 +- 自适应作曲 + +### 分析引擎 +负责分析游戏状态和环境条件。 + +主要功能: +- 多维度状态分析 +- 强度计算和趋势预测 +- 自定义分析器 +- 历史数据分析 + +### 效果处理器 +负责音频效果的处理和应用。 + +主要功能: +- 多种音频效果 +- 效果链处理 +- 动态效果参数 +- 湿/干混合控制 + +## 音乐主题类型 + +### 环境音乐 (Ambient) +- **特点**: 轻柔、氛围感强、循环播放 +- **适用场景**: 主菜单、平静的探索、安全区域 +- **强度范围**: 0.0-0.4 + +### 探索音乐 (Exploration) +- **特点**: 节奏适中、富有动感、引导性强 +- **适用场景**: 世界探索、任务进行、一般移动 +- **强度范围**: 0.3-0.7 + +### 战斗音乐 (Battle) +- **特点**: 节奏快、强度高、紧张感强 +- **适用场景**: 战斗、追逐、危险情况 +- **强度范围**: 0.6-1.0 + +### 胜利音乐 (Victory) +- **特点**: 欢快、庆祝感、高潮性强 +- **适用场景**: 任务完成、战斗胜利、成就达成 +- **强度范围**: 0.8-1.0 + +### 失败音乐 (Defeat) +- **特点**: 低沉、悲伤、反思性强 +- **适用场景**: 任务失败、角色死亡、挫折情况 +- **强度范围**: 0.2-0.5 + +## 音乐强度等级 + +### 低强度 (0.0-0.3) +- **特征**: 安静、舒缓、氛围为主 +- **适用**: 安全区域、对话、菜单 +- **效果**: 最小化音频效果,轻柔混响 + +### 中低强度 (0.3-0.5) +- **特征**: 轻松、引导性、背景为主 +- **适用**: 一般探索、日常活动 +- **效果**: 适度混响和合唱效果 + +### 中等强度 (0.5-0.7) +- **特征**: 活跃、动感、注意力引导 +- **适用**: 轻度挑战、重要场景 +- **效果**: 增强节奏感,添加延迟效果 + +### 中高强度 (0.7-0.9) +- **特征**: 紧张、激烈、情绪推动 +- **适用**: 战斗、追逐、关键时刻 +- **效果**: 强烈效果链,高混响和失真 + +### 高强度 (0.9-1.0) +- **特征**: 激烈、高潮、情绪爆发 +- **适用**: BOSS战、剧情高潮、胜利时刻 +- **效果**: 最大化所有效果,创造强烈冲击 + +## 性能调优 + +### 资源管理 +1. **智能预加载**: 根据游戏状态预测并预加载可能需要的音乐 +2. **动态卸载**: 自动卸载长时间未使用的音乐资源 +3. **内存池**: 使用内存池管理音频缓冲区 +4. **流式加载**: 支持大音频文件的流式加载 + +### 计算优化 +1. **多线程处理**: 音乐生成和效果处理在后台线程执行 +2. **算法优化**: 使用优化的音频处理算法 +3. **缓存机制**: 缓存常用音频片段和处理结果 +4. **批量处理**: 批量处理音频数据以提高效率 + +### 效果优化 +1. **质量等级**: 可配置效果处理质量等级 +2. **动态调整**: 根据系统负载动态调整效果复杂度 +3. **效果链优化**: 优化效果链的处理顺序 +4. **参数插值**: 平滑效果参数变化,避免突兀 + +## 扩展开发 + +### 添加新的音乐主题 +1. 在[themes](file:///home/hello/EG/plugins/user/navigation_mesh/core/navmesh_manager.py#L20-L20)字典中添加主题数据 +2. 定义主题的音乐层和参数 +3. 实现主题的加载和播放逻辑 + +### 添加新的过渡类型 +1. 在[TransitionManager](file:///home/hello/EG/plugins/user/audio_reverb_effects/effects/spatial_processor.py#L15-L1056)中实现新的过渡方法 +2. 注册新的过渡类型 +3. 实现过渡的参数控制 + +### 添加新的音频效果 +1. 在[EffectProcessor](file:///home/hello/EG/plugins/user/audio_reverb_effects/effects/spatial_processor.py#L15-L1056)中实现新的效果处理方法 +2. 添加效果参数验证 +3. 实现效果的应用逻辑 + +### 添加新的分析维度 +1. 在[AnalysisEngine](file:///home/hello/EG/plugins/user/audio_reverb_effects/effects/spatial_processor.py#L15-L1056)中创建新的分析器 +2. 实现分析算法 +3. 注册分析器到权重系统 + +## 故障排除 + +### 音乐无法播放 +1. 检查音乐主题是否正确创建和加载 +2. 确认音乐系统是否已启用 +3. 验证音乐文件路径是否正确 +4. 检查音频设备是否正常工作 + +### 过渡效果异常 +1. 检查过渡参数是否正确设置 +2. 确认源主题和目标主题是否存在 +3. 验证过渡类型是否支持 +4. 检查是否有冲突的过渡正在进行 + +### 性能问题 +1. 使用统计信息分析性能瓶颈 +2. 降低音频效果质量等级 +3. 减少同时激活的效果数量 +4. 优化音乐生成参数 + +### 音质问题 +1. 检查采样率设置是否正确 +2. 确认音频文件质量 +3. 验证效果参数是否在合理范围 +4. 检查是否有削波现象 + +## API参考 + +### Plugin类主要方法 + +#### 初始化和控制 +- `initialize()` - 初始化插件 +- `enable()` - 启用插件 +- `disable()` - 禁用插件 +- `finalize()` - 清理插件资源 +- `update(dt)` - 更新插件状态 + +#### 音乐主题管理 +- `create_theme(theme_name, theme_data)` - 创建音乐主题 +- `play_theme(theme_name, force=False)` - 播放音乐主题 +- `stop_theme(theme_name)` - 停止音乐主题 +- `stop_all_music()` - 停止所有音乐 +- `set_layer_volume(layer_name, volume)` - 设置音乐层音量 + +#### 音乐过渡控制 +- `create_transition(transition_name, transition_data)` - 创建音乐过渡 +- `trigger_transition(transition_name)` - 触发音乐过渡 + +#### 状态控制 +- `set_intensity(intensity)` - 设置音乐强度 +- `get_intensity()` - 获取当前音乐强度 +- `set_game_state(state)` - 设置游戏状态 +- `set_environment_conditions(conditions)` - 设置环境条件 +- `set_player_state(state)` - 设置玩家状态 + +#### 音量控制 +- `set_master_volume(volume)` - 设置主音量 +- `get_master_volume()` - 获取主音量 + +#### 统计和信息 +- `get_stats()` - 获取插件统计信息 + +### 核心组件类 + +#### MusicManager +- `load_theme(theme_name, theme_data)` - 加载音乐主题 +- `play_theme(theme_name, force=False)` - 播放音乐主题 +- `stop_theme(theme_name)` - 停止音乐主题 +- `set_theme_volume(theme_name, volume)` - 设置主题音量 + +#### TransitionManager +- `register_transition(transition_name, transition_data)` - 注册音乐过渡 +- `trigger_transition(transition_name)` - 触发音乐过渡 + +#### CompositionEngine +- `create_generator(generator_name, generator_params)` - 创建音乐生成器 +- `activate_generator(generator_name)` - 激活音乐生成器 +- `generate_theme(theme_name, theme_params)` - 生成完整音乐主题 + +#### AnalysisEngine +- `create_analyzer(analyzer_name, analyzer_params)` - 创建分析器 +- `perform_analysis()` - 执行分析 +- `get_intensity_trend()` - 获取强度趋势 + +#### EffectProcessor +- `create_effect(effect_name, effect_type, effect_params)` - 创建音频效果 +- `activate_effect(effect_name)` - 激活音频效果 +- `apply_effects(audio_data, chain_name)` - 应用效果链 + +## 音频格式支持 + +支持的音频格式: +- **WAV**: 未压缩的PCM音频 +- **OGG**: Ogg Vorbis压缩音频 +- **MP3**: MPEG音频层III + +## 版本信息 + +- 版本: 1.0.0 +- 作者: EG Team +- 发布日期: 2024年 + +## 许可证 + 本插件遵循EG引擎许可证协议。 \ No newline at end of file diff --git a/plugins/user/dynamic_music_system/analysis/analysis_engine.py b/plugins/user/dynamic_music_system/analysis/analysis_engine.py index 048705e4..7c7da0cf 100644 --- a/plugins/user/dynamic_music_system/analysis/analysis_engine.py +++ b/plugins/user/dynamic_music_system/analysis/analysis_engine.py @@ -1,1566 +1,1566 @@ -""" -分析引擎 -负责分析游戏状态和环境条件,为音乐系统提供决策支持 -""" - -import uuid -import math -import time -import numpy as np -from typing import Dict, List, Any, Optional, Callable -from collections import deque - -class AnalysisEngine: - """ - 分析引擎 - 负责分析游戏状态和环境条件,为音乐系统提供决策支持 - """ - - def __init__(self, plugin): - """ - 初始化分析引擎 - - Args: - plugin: 动态音乐系统插件实例 - """ - self.plugin = plugin - self.analyzers: Dict[str, Dict[str, Any]] = {} - self.analysis_results: Dict[str, Any] = {} - self.history: Dict[str, deque] = {} - - self.stats = { - 'analyzers_created': 0, - 'analyses_performed': 0, - 'analysis_time': 0.0, - 'data_points_processed': 0, - 'prediction_accuracy': 0.0 - } - - self.buffer_size = plugin.buffer_size if plugin else 4096 - self.analysis_interval = 1.0 # 默认分析间隔(秒) - self.last_analysis_time = 0.0 - self.last_update_time = 0.0 - - # 分析权重 - self.weights = { - 'combat': 1.0, - 'exploration': 0.8, - 'environment': 0.6, - 'player_state': 0.7, - 'narrative': 0.5 - } - - # 历史数据长度 - self.history_length = 100 - - # 预测参数 - self.prediction_settings = { - 'lookahead_time': 5.0, # 预测时间窗口 - 'confidence_threshold': 0.7, # 置信度阈值 - 'trend_sensitivity': 0.3, # 趋势敏感度 - 'prediction_smoothing': 0.1 # 预测平滑系数 - } - - # 情绪分析参数 - self.emotion_analysis = { - 'valence_range': (-1.0, 1.0), # 情绪效价范围 - 'arousal_range': (0.0, 1.0), # 情绪唤醒度范围 - 'tension_range': (0.0, 1.0), # 紧张度范围 - 'complexity_range': (0.0, 1.0) # 复杂度范围 - } - - # 初始化分析引擎 - self._initialize_analysis_engine() - - def _initialize_analysis_engine(self): - """初始化分析引擎""" - # 初始化历史记录 - history_keys = [ - 'intensity', 'combat_level', 'exploration_level', - 'environment_changes', 'player_emotion', 'narrative_pacing', - 'music_intensity', 'player_health', 'player_stamina' - ] - - for key in history_keys: - self.history[key] = deque(maxlen=self.history_length) - - # 创建默认分析器 - self._create_default_analyzers() - print("✓ 分析引擎初始化完成") - - def _create_default_analyzers(self): - """创建默认分析器""" - # 战斗强度分析器 - self.create_analyzer('combat_analyzer', { - 'type': 'combat', - 'callback': self._analyze_combat_state - }) - - # 探索状态分析器 - self.create_analyzer('exploration_analyzer', { - 'type': 'exploration', - 'callback': self._analyze_exploration_state - }) - - # 环境条件分析器 - self.create_analyzer('environment_analyzer', { - 'type': 'environment', - 'callback': self._analyze_environment_conditions - }) - - # 玩家状态分析器 - self.create_analyzer('player_analyzer', { - 'type': 'player_state', - 'callback': self._analyze_player_state - }) - - # 叙事节奏分析器 - self.create_analyzer('narrative_analyzer', { - 'type': 'narrative', - 'callback': self._analyze_narrative_pacing - }) - - # 音乐反馈分析器 - self.create_analyzer('music_feedback_analyzer', { - 'type': 'music_feedback', - 'callback': self._analyze_music_feedback - }) - - def create_analyzer(self, analyzer_name: str, analyzer_params: Dict[str, Any]) -> bool: - """ - 创建分析器 - - Args: - analyzer_name: 分析器名称 - analyzer_params: 分析器参数 - - Returns: - 是否创建成功 - """ - try: - # 设置默认参数 - default_params = { - 'type': 'generic', - 'enabled': True, - 'interval': self.analysis_interval, - 'weight': 1.0, - 'history_size': 50, - 'sensitivity': 1.0, - 'smoothing': 0.2, - 'thresholds': { - 'low': 0.3, - 'medium': 0.6, - 'high': 0.8 - } - } - - # 合并参数 - params = default_params.copy() - params.update(analyzer_params) - - # 验证回调函数 - if 'callback' not in params or not callable(params['callback']): - print(f"✗ 分析器缺少有效的回调函数: {analyzer_name}") - return False - - # 创建分析器 - analyzer = { - 'name': analyzer_name, - 'params': params, - 'state': 'created', - 'last_analysis': 0.0, - 'results': {}, - 'history': deque(maxlen=params['history_size']), - 'trend': 0.0, # 趋势值 - 'volatility': 0.0, # 波动性 - 'confidence': 1.0 # 置信度 - } - - self.analyzers[analyzer_name] = analyzer - self.stats['analyzers_created'] += 1 - - print(f"✓ 分析器创建成功: {analyzer_name}") - return True - - except Exception as e: - print(f"✗ 创建分析器失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable_analyzer(self, analyzer_name: str, enable: bool = True) -> bool: - """ - 启用/禁用分析器 - - Args: - analyzer_name: 分析器名称 - enable: 是否启用 - - Returns: - 是否设置成功 - """ - try: - if analyzer_name not in self.analyzers: - print(f"✗ 分析器不存在: {analyzer_name}") - return False - - self.analyzers[analyzer_name]['params']['enabled'] = enable - state = "启用" if enable else "禁用" - print(f"✓ 分析器已{state}: {analyzer_name}") - return True - - except Exception as e: - print(f"✗ 设置分析器状态失败: {e}") - import traceback - traceback.print_exc() - return False - - def perform_analysis(self) -> Dict[str, Any]: - """ - 执行分析 - - Returns: - 分析结果 - """ - try: - analysis_start_time = time.time() - current_time = time.time() - - # 收集所有分析器的结果 - combined_results = {} - total_weight = 0.0 - weighted_intensity = 0.0 - analyzer_contributions = {} - - for analyzer_name, analyzer in self.analyzers.items(): - params = analyzer['params'] - - # 检查分析器是否启用 - if not params.get('enabled', True): - continue - - # 检查分析间隔 - interval = params.get('interval', self.analysis_interval) - if current_time - analyzer.get('last_analysis', 0.0) < interval: - # 使用之前的分析结果 - if analyzer['results']: - result = analyzer['results'] - else: - continue - else: - # 执行新的分析 - try: - callback = params['callback'] - result = callback() - analyzer['results'] = result - analyzer['last_analysis'] = current_time - - # 更新历史记录 - analyzer['history'].append(result) - - # 计算趋势和波动性 - self._update_analyzer_trends(analyzer) - - self.stats['analyses_performed'] += 1 - except Exception as e: - print(f"✗ 分析器 {analyzer_name} 执行失败: {e}") - import traceback - traceback.print_exc() - continue - - # 合并结果 - analyzer_type = params.get('type', 'generic') - weight = params.get('weight', 1.0) * self.weights.get(analyzer_type, 1.0) - - # 应用灵敏度调整 - sensitivity = params.get('sensitivity', 1.0) - if isinstance(result, dict): - adjusted_result = {} - for key, value in result.items(): - if isinstance(value, (int, float)): - adjusted_result[key] = value * sensitivity - else: - adjusted_result[key] = value - result = adjusted_result - elif isinstance(result, (int, float)): - result = result * sensitivity - - if isinstance(result, dict): - for key, value in result.items(): - if key not in combined_results: - combined_results[key] = [] - combined_results[key].append((value, weight)) - else: - # 处理单一值结果 - if 'generic' not in combined_results: - combined_results['generic'] = [] - combined_results['generic'].append((result, weight)) - - # 计算加权强度 - if 'intensity' in result: - if isinstance(result['intensity'], (int, float)): - weighted_intensity += result['intensity'] * weight - total_weight += weight - analyzer_contributions[analyzer_name] = result['intensity'] * weight - - # 更新分析器置信度 - self._update_analyzer_confidence(analyzer, result) - - # 计算平均强度 - if total_weight > 0: - average_intensity = weighted_intensity / total_weight - else: - average_intensity = 0.0 - - # 应用平滑处理 - smoothing = 0.1 - if 'intensity' in self.analysis_results: - previous_intensity = self.analysis_results['intensity'] - average_intensity = previous_intensity * smoothing + average_intensity * (1 - smoothing) - - # 整合分析结果 - final_results = { - 'timestamp': current_time, - 'intensity': average_intensity, - 'raw_intensity': weighted_intensity / total_weight if total_weight > 0 else 0.0, - 'details': combined_results, - 'confidence': min(1.0, len(combined_results) / max(1, len(self.analyzers))), - 'analyzer_contributions': analyzer_contributions, - 'trends': self._get_overall_trends(), - 'predictions': self._make_predictions(average_intensity) - } - - # 更新历史记录 - self.history['intensity'].append(average_intensity) - if 'combat_level' in combined_results: - combat_values = [r[0] for r in combined_results.get('combat_level', [])] - if combat_values: - self.history['combat_level'].append(np.mean(combat_values)) - if 'exploration_level' in combined_results: - exploration_values = [r[0] for r in combined_results.get('exploration_level', [])] - if exploration_values: - self.history['exploration_level'].append(np.mean(exploration_values)) - - # 存储结果 - self.analysis_results = final_results - - # 更新性能统计 - self.stats['analysis_time'] += (time.time() - analysis_start_time) - self.stats['data_points_processed'] += len(combined_results) - - return final_results - - except Exception as e: - print(f"✗ 执行分析失败: {e}") - import traceback - traceback.print_exc() - return {} - - def _update_analyzer_trends(self, analyzer: Dict[str, Any]): - """ - 更新分析器趋势 - - Args: - analyzer: 分析器数据 - """ - try: - if len(analyzer['history']) < 2: - return - - # 计算强度趋势 - intensity_values = [] - for result in analyzer['history']: - if isinstance(result, dict) and 'intensity' in result: - intensity_values.append(result['intensity']) - - if len(intensity_values) >= 2: - # 线性回归计算趋势 - x = np.arange(len(intensity_values)) - y = np.array(intensity_values) - if len(x) > 1 and np.std(x) > 0 and np.std(y) > 0: - slope, _ = np.polyfit(x, y, 1) - analyzer['trend'] = slope - - # 计算波动性 - analyzer['volatility'] = np.std(y) - - except Exception as e: - print(f"✗ 更新分析器趋势失败: {e}") - - def _update_analyzer_confidence(self, analyzer: Dict[str, Any], result: Any): - """ - 更新分析器置信度 - - Args: - analyzer: 分析器数据 - result: 分析结果 - """ - try: - # 基于历史一致性更新置信度 - if len(analyzer['history']) >= 3: - recent_values = [] - for hist_result in list(analyzer['history'])[-3:]: - if isinstance(hist_result, dict) and 'intensity' in hist_result: - recent_values.append(hist_result['intensity']) - - if len(recent_values) >= 2: - # 计算变化率 - changes = [abs(recent_values[i] - recent_values[i-1]) for i in range(1, len(recent_values))] - avg_change = np.mean(changes) - - # 基于稳定性调整置信度 - stability = 1.0 / (1.0 + avg_change * 10) - analyzer['confidence'] = 0.7 * analyzer['confidence'] + 0.3 * stability - - # 确保置信度在合理范围内 - analyzer['confidence'] = max(0.1, min(1.0, analyzer['confidence'])) - - except Exception as e: - print(f"✗ 更新分析器置信度失败: {e}") - - def _get_overall_trends(self) -> Dict[str, float]: - """ - 获取整体趋势 - - Returns: - 趋势字典 - """ - try: - trends = {} - - # 计算整体强度趋势 - if len(self.history['intensity']) >= 5: - recent_values = list(self.history['intensity'])[-5:] - x = np.arange(len(recent_values)) - y = np.array(recent_values) - if len(x) > 1 and np.std(x) > 0 and np.std(y) > 0: - slope, _ = np.polyfit(x, y, 1) - trends['intensity'] = slope - - # 计算战斗趋势 - if len(self.history['combat_level']) >= 5: - recent_values = list(self.history['combat_level'])[-5:] - x = np.arange(len(recent_values)) - y = np.array(recent_values) - if len(x) > 1 and np.std(x) > 0 and np.std(y) > 0: - slope, _ = np.polyfit(x, y, 1) - trends['combat'] = slope - - # 计算探索趋势 - if len(self.history['exploration_level']) >= 5: - recent_values = list(self.history['exploration_level'])[-5:] - x = np.arange(len(recent_values)) - y = np.array(recent_values) - if len(x) > 1 and np.std(x) > 0 and np.std(y) > 0: - slope, _ = np.polyfit(x, y, 1) - trends['exploration'] = slope - - return trends - - except Exception as e: - print(f"✗ 获取整体趋势失败: {e}") - return {} - - def _make_predictions(self, current_intensity: float) -> Dict[str, Any]: - """ - 做出预测 - - Args: - current_intensity: 当前强度 - - Returns: - 预测结果 - """ - try: - predictions = {} - lookahead_time = self.prediction_settings['lookahead_time'] - - # 基于趋势预测未来强度 - overall_trend = self._get_overall_trends().get('intensity', 0.0) - predicted_intensity = current_intensity + overall_trend * lookahead_time - - # 限制在合理范围内 - predicted_intensity = max(0.0, min(1.0, predicted_intensity)) - - # 计算预测置信度 - trend_magnitude = abs(overall_trend) - confidence = min(1.0, trend_magnitude * 10) # 趋势越大,置信度越高 - - predictions['intensity'] = { - 'value': predicted_intensity, - 'confidence': confidence, - 'time_horizon': lookahead_time - } - - # 基于历史模式预测事件 - predictions['events'] = self._predict_events() - - return predictions - - except Exception as e: - print(f"✗ 做出预测失败: {e}") - return {} - - def _predict_events(self) -> List[Dict[str, Any]]: - """ - 预测事件 - - Returns: - 预测事件列表 - """ - try: - events = [] - - # 检查战斗强度是否即将上升 - if len(self.history['combat_level']) >= 10: - recent_combat = list(self.history['combat_level'])[-10:] - trend = np.polyfit(np.arange(len(recent_combat)), np.array(recent_combat), 1)[0] - - if trend > 0.1 and recent_combat[-1] > 0.3: # 上升趋势且当前强度较高 - events.append({ - 'type': 'combat_escalation', - 'probability': min(1.0, trend * 5), - 'time_to_event': max(1.0, 10 - recent_combat[-1] * 10) - }) - - # 检查探索活动是否减少 - if len(self.history['exploration_level']) >= 10: - recent_exploration = list(self.history['exploration_level'])[-10:] - trend = np.polyfit(np.arange(len(recent_exploration)), np.array(recent_exploration), 1)[0] - - if trend < -0.1 and recent_exploration[-1] < 0.5: # 下降趋势且当前强度较低 - events.append({ - 'type': 'exploration_decrease', - 'probability': min(1.0, abs(trend) * 5), - 'time_to_event': max(1.0, recent_exploration[-1] * 10) - }) - - return events - - except Exception as e: - print(f"✗ 预测事件失败: {e}") - return [] - - def _analyze_combat_state(self) -> Dict[str, Any]: - """ - 分析战斗状态 - - Returns: - 战斗状态分析结果 - """ - try: - # 获取游戏状态 - game_state = self.plugin.game_state if self.plugin else {} - - # 分析战斗相关指标 - in_combat = game_state.get('in_combat', False) - enemies_nearby = game_state.get('enemies_nearby', 0) - player_health = game_state.get('player_health', 1.0) - combat_intensity = game_state.get('combat_intensity', 0.0) - player_damage = game_state.get('player_damage', 0.0) - enemy_damage = game_state.get('enemy_damage', 0.0) - combat_duration = game_state.get('combat_duration', 0.0) - - # 计算战斗强度 - combat_level = 0.0 - if in_combat: - # 基于敌人数量和强度 - enemy_factor = min(1.0, enemies_nearby / 10.0) - # 基于玩家生命值 - health_factor = 1.0 - player_health - # 基于战斗强度 - intensity_factor = combat_intensity - # 基于伤害交换 - damage_factor = min(1.0, (player_damage + enemy_damage) / 100.0) - # 基于战斗持续时间 - duration_factor = min(1.0, combat_duration / 60.0) - - combat_level = (enemy_factor + health_factor + intensity_factor + damage_factor + duration_factor) / 5.0 - - # 计算战斗紧张度 - tension = 0.0 - if in_combat: - # 基于生命值差异 - health_tension = 1.0 - player_health - # 基于敌人数 - enemy_tension = min(1.0, enemies_nearby / 5.0) - # 综合紧张度 - tension = (health_tension + enemy_tension) / 2.0 - - # 更新历史记录 - self.history['combat_level'].append(combat_level) - self.history['player_health'].append(player_health) - - return { - 'intensity': combat_level, - 'in_combat': in_combat, - 'enemies_nearby': enemies_nearby, - 'player_health': player_health, - 'combat_intensity': combat_intensity, - 'tension': tension, - 'damage_exchange': player_damage + enemy_damage, - 'duration': combat_duration - } - - except Exception as e: - print(f"✗ 分析战斗状态失败: {e}") - import traceback - traceback.print_exc() - return { - 'intensity': 0.0, - 'in_combat': False, - 'enemies_nearby': 0, - 'player_health': 1.0, - 'combat_intensity': 0.0, - 'tension': 0.0, - 'damage_exchange': 0.0, - 'duration': 0.0 - } - - def _analyze_exploration_state(self) -> Dict[str, Any]: - """ - 分析探索状态 - - Returns: - 探索状态分析结果 - """ - try: - # 获取游戏状态 - game_state = self.plugin.game_state if self.plugin else {} - - # 分析探索相关指标 - exploring = game_state.get('exploring', False) - areas_discovered = game_state.get('areas_discovered', 0) - exploration_progress = game_state.get('exploration_progress', 0.0) - time_since_discovery = game_state.get('time_since_discovery', 0.0) - movement_speed = game_state.get('movement_speed', 0.0) - player_stealth = game_state.get('player_stealth', 0.0) - secrets_found = game_state.get('secrets_found', 0) - total_secrets = game_state.get('total_secrets', 1) - - # 计算探索强度 - exploration_level = 0.0 - if exploring: - # 基于探索进度 - progress_factor = exploration_progress - # 基于发现区域数量 - discovery_factor = min(1.0, areas_discovered / 20.0) - # 基于发现时间(新发现时强度更高) - time_factor = max(0.0, min(1.0, 1.0 - time_since_discovery / 60.0)) - # 基于移动速度 - speed_factor = min(1.0, movement_speed / 10.0) - # 基于隐秘程度 - stealth_factor = player_stealth - # 基于秘密发现率 - secrets_factor = min(1.0, secrets_found / max(1, total_secrets)) - - exploration_level = (progress_factor + discovery_factor + time_factor + - speed_factor + stealth_factor + secrets_factor) / 6.0 - - # 计算探索好奇心 - curiosity = 0.0 - if exploring: - # 基于未发现区域 - undiscovered_factor = 1.0 - exploration_progress - # 基于秘密发现率 - secrets_undiscovered = max(0, total_secrets - secrets_found) - secrets_factor = min(1.0, secrets_undiscovered / max(1, total_secrets)) - # 综合好奇心 - curiosity = (undiscovered_factor + secrets_factor) / 2.0 - - # 更新历史记录 - self.history['exploration_level'].append(exploration_level) - - return { - 'intensity': exploration_level, - 'exploring': exploring, - 'areas_discovered': areas_discovered, - 'exploration_progress': exploration_progress, - 'time_since_discovery': time_since_discovery, - 'movement_speed': movement_speed, - 'stealth': player_stealth, - 'curiosity': curiosity, - 'secrets_completion': secrets_found / max(1, total_secrets) - } - - except Exception as e: - print(f"✗ 分析探索状态失败: {e}") - import traceback - traceback.print_exc() - return { - 'intensity': 0.0, - 'exploring': False, - 'areas_discovered': 0, - 'exploration_progress': 0.0, - 'time_since_discovery': 0.0, - 'movement_speed': 0.0, - 'stealth': 0.0, - 'curiosity': 0.0, - 'secrets_completion': 0.0 - } - - def _analyze_environment_conditions(self) -> Dict[str, Any]: - """ - 分析环境条件 - - Returns: - 环境条件分析结果 - """ - try: - # 获取环境条件 - environment = self.plugin.environment_conditions if self.plugin else {} - - # 分析环境相关指标 - weather = environment.get('weather', 'clear') - time_of_day = environment.get('time_of_day', 'day') - location_type = environment.get('location_type', 'outdoor') - danger_level = environment.get('danger_level', 0.0) - atmosphere = environment.get('atmosphere', 0.5) - temperature = environment.get('temperature', 20.0) - visibility = environment.get('visibility', 1.0) - lighting = environment.get('lighting', 1.0) - weather_intensity = environment.get('weather_intensity', 0.0) - - # 计算环境强度 - environment_level = 0.0 - - # 天气影响 - weather_factors = { - 'clear': 0.2, - 'cloudy': 0.3, - 'rain': 0.5, - 'storm': 0.8, - 'fog': 0.6, - 'snow': 0.4, - 'windy': 0.4 - } - weather_factor = weather_factors.get(weather, 0.3) - - # 时间影响 - time_factors = { - 'day': 0.3, - 'dusk': 0.5, - 'night': 0.7, - 'dawn': 0.4 - } - time_factor = time_factors.get(time_of_day, 0.3) - - # 地点类型影响 - location_factors = { - 'indoor': 0.4, - 'outdoor': 0.6, - 'underground': 0.8, - 'underwater': 0.7, - 'cave': 0.7, - 'forest': 0.5, - 'desert': 0.4 - } - location_factor = location_factors.get(location_type, 0.5) - - # 综合计算 - environment_level = (weather_factor + time_factor + location_factor + - danger_level + atmosphere + weather_intensity) / 6.0 - - # 计算环境紧张度 - environmental_tension = (danger_level + (1.0 - visibility) + (1.0 - lighting)) / 3.0 - - # 计算环境氛围 - environmental_mood = atmosphere - - # 更新历史记录 - self.history['environment_changes'].append(environment_level) - - return { - 'intensity': environment_level, - 'weather': weather, - 'time_of_day': time_of_day, - 'location_type': location_type, - 'danger_level': danger_level, - 'atmosphere': atmosphere, - 'temperature': temperature, - 'visibility': visibility, - 'lighting': lighting, - 'weather_intensity': weather_intensity, - 'tension': environmental_tension, - 'mood': environmental_mood - } - - except Exception as e: - print(f"✗ 分析环境条件失败: {e}") - import traceback - traceback.print_exc() - return { - 'intensity': 0.3, - 'weather': 'clear', - 'time_of_day': 'day', - 'location_type': 'outdoor', - 'danger_level': 0.0, - 'atmosphere': 0.5, - 'temperature': 20.0, - 'visibility': 1.0, - 'lighting': 1.0, - 'weather_intensity': 0.0, - 'tension': 0.0, - 'mood': 0.5 - } - - def _analyze_player_state(self) -> Dict[str, Any]: - """ - 分析玩家状态 - - Returns: - 玩家状态分析结果 - """ - try: - # 获取玩家状态 - player_state = self.plugin.player_state if self.plugin else {} - - # 分析玩家相关指标 - health = player_state.get('health', 1.0) - stamina = player_state.get('stamina', 1.0) - experience = player_state.get('experience', 0) - level = player_state.get('level', 1) - emotional_state = player_state.get('emotional_state', 'neutral') - skill_level = player_state.get('skill_level', 0.5) - fatigue = player_state.get('fatigue', 0.0) - hunger = player_state.get('hunger', 0.0) - stress = player_state.get('stress', 0.0) - focus = player_state.get('focus', 0.5) - courage = player_state.get('courage', 0.5) - items_collected = player_state.get('items_collected', 0) - quests_completed = player_state.get('quests_completed', 0) - - # 计算玩家状态强度 - # 基于健康和耐力 - physical_factor = (health + stamina) / 2.0 - # 基于经验和等级 - progression_factor = min(1.0, (experience / 1000.0 + level / 50.0) / 2.0) - # 基于技能水平 - skill_factor = skill_level - # 基于疲劳和饥饿 - condition_factor = 1.0 - (fatigue + hunger) / 2.0 - - # 综合计算 - player_level = (physical_factor + progression_factor + skill_factor + condition_factor) / 4.0 - - # 情感状态影响 - emotion_factors = { - 'excited': 0.8, - 'happy': 0.6, - 'neutral': 0.5, - 'sad': 0.3, - 'afraid': 0.7, - 'angry': 0.9, - 'calm': 0.4, - 'curious': 0.6 - } - emotion_factor = emotion_factors.get(emotional_state, 0.5) - - # 计算玩家情绪效价(正负情绪) - valence_map = { - 'excited': 0.7, 'happy': 0.8, 'neutral': 0.0, - 'sad': -0.6, 'afraid': -0.5, 'angry': -0.3, - 'calm': 0.2, 'curious': 0.4 - } - valence = valence_map.get(emotional_state, 0.0) - - # 计算玩家唤醒度(兴奋程度) - arousal_map = { - 'excited': 0.9, 'happy': 0.6, 'neutral': 0.4, - 'sad': 0.3, 'afraid': 0.8, 'angry': 0.7, - 'calm': 0.2, 'curious': 0.5 - } - arousal = arousal_map.get(emotional_state, 0.4) - - # 计算玩家压力水平 - stress_level = (stress + (1.0 - health) + fatigue) / 3.0 - - # 更新历史记录 - self.history['player_emotion'].append(emotion_factor) - self.history['player_health'].append(health) - self.history['player_stamina'].append(stamina) - - return { - 'intensity': player_level, - 'health': health, - 'stamina': stamina, - 'experience': experience, - 'level': level, - 'emotional_state': emotional_state, - 'skill_level': skill_level, - 'fatigue': fatigue, - 'hunger': hunger, - 'stress': stress, - 'focus': focus, - 'courage': courage, - 'items_collected': items_collected, - 'quests_completed': quests_completed, - 'valence': valence, # 情绪效价 - 'arousal': arousal, # 唤醒度 - 'stress_level': stress_level # 压力水平 - } - - except Exception as e: - print(f"✗ 分析玩家状态失败: {e}") - import traceback - traceback.print_exc() - return { - 'intensity': 0.5, - 'health': 1.0, - 'stamina': 1.0, - 'experience': 0, - 'level': 1, - 'emotional_state': 'neutral', - 'skill_level': 0.5, - 'fatigue': 0.0, - 'hunger': 0.0, - 'stress': 0.0, - 'focus': 0.5, - 'courage': 0.5, - 'items_collected': 0, - 'quests_completed': 0, - 'valence': 0.0, - 'arousal': 0.4, - 'stress_level': 0.0 - } - - def _analyze_narrative_pacing(self) -> Dict[str, Any]: - """ - 分析叙事节奏 - - Returns: - 叙事节奏分析结果 - """ - try: - # 获取游戏状态 - game_state = self.plugin.game_state if self.plugin else {} - - # 分析叙事相关指标 - story_phase = game_state.get('story_phase', 'opening') - quest_progress = game_state.get('quest_progress', 0.0) - recent_events = game_state.get('recent_events', []) - tension_level = game_state.get('tension_level', 0.0) - player_choices = game_state.get('player_choices', 0) - dialogue_intensity = game_state.get('dialogue_intensity', 0.0) - cutscene_time = game_state.get('cutscene_time', 0.0) - player_agency = game_state.get('player_agency', 0.5) - narrative_complexity = game_state.get('narrative_complexity', 0.3) - character_development = game_state.get('character_development', 0.0) - plot_twists = game_state.get('plot_twists', 0) - - # 计算叙事强度 - narrative_level = 0.0 - - # 故事阶段影响 - phase_factors = { - 'opening': 0.3, - 'rising_action': 0.6, - 'climax': 0.9, - 'falling_action': 0.5, - 'resolution': 0.2 - } - phase_factor = phase_factors.get(story_phase, 0.5) - - # 任务进度影响 - progress_factor = quest_progress - - # 紧张程度影响 - tension_factor = tension_level - - # 事件密度影响(最近事件数量) - event_density = min(1.0, len(recent_events) / 10.0) - - # 对话强度影响 - dialogue_factor = dialogue_intensity - - # 玩家选择影响 - choice_factor = min(1.0, player_choices / 20.0) - - # 综合计算 - narrative_level = (phase_factor + progress_factor + tension_factor + - event_density + dialogue_factor + choice_factor) / 6.0 - - # 计算叙事复杂度 - complexity = narrative_complexity - - # 计算角色发展强度 - character_factor = character_development - - # 计算剧情转折强度 - twist_factor = min(1.0, plot_twists / 5.0) - - # 更新历史记录 - self.history['narrative_pacing'].append(narrative_level) - - return { - 'intensity': narrative_level, - 'story_phase': story_phase, - 'quest_progress': quest_progress, - 'recent_events_count': len(recent_events), - 'tension_level': tension_level, - 'player_choices': player_choices, - 'dialogue_intensity': dialogue_intensity, - 'cutscene_time': cutscene_time, - 'player_agency': player_agency, - 'complexity': complexity, - 'character_development': character_development, - 'plot_twists': plot_twists, - 'event_density': event_density, - 'choice_impact': choice_factor - } - - except Exception as e: - print(f"✗ 分析叙事节奏失败: {e}") - import traceback - traceback.print_exc() - return { - 'intensity': 0.5, - 'story_phase': 'opening', - 'quest_progress': 0.0, - 'recent_events_count': 0, - 'tension_level': 0.0, - 'player_choices': 0, - 'dialogue_intensity': 0.0, - 'cutscene_time': 0.0, - 'player_agency': 0.5, - 'complexity': 0.3, - 'character_development': 0.0, - 'plot_twists': 0, - 'event_density': 0.0, - 'choice_impact': 0.0 - } - - def _analyze_music_feedback(self) -> Dict[str, Any]: - """ - 分析音乐反馈 - - Returns: - 音乐反馈分析结果 - """ - try: - # 获取当前音乐状态 - current_intensity = self.plugin.get_intensity() if self.plugin else 0.0 - target_intensity = self.plugin.target_intensity if self.plugin else 0.0 - - # 分析音乐反馈相关指标 - intensity_difference = abs(current_intensity - target_intensity) - transition_in_progress = hasattr(self.plugin, 'transition_manager') and \ - len(self.plugin.transition_manager.active_transitions) > 0 if self.plugin else False - layer_count = len(self.plugin.music_manager.active_themes) if self.plugin and hasattr(self.plugin, 'music_manager') else 0 - effect_count = len(self.plugin.effect_processor.active_effects) if self.plugin and hasattr(self.plugin, 'effect_processor') else 0 - - # 计算音乐反馈强度 - feedback_level = 0.0 - - # 基于强度差异 - difference_factor = intensity_difference - - # 基于过渡状态 - transition_factor = 1.0 if transition_in_progress else 0.0 - - # 基于复杂度(层数和效果数) - complexity_factor = min(1.0, (layer_count + effect_count) / 10.0) - - # 综合计算 - feedback_level = (difference_factor + transition_factor + complexity_factor) / 3.0 - - # 计算音乐适应性 - adaptation = 1.0 - intensity_difference - - # 更新历史记录 - self.history['music_intensity'].append(current_intensity) - - return { - 'intensity': feedback_level, - 'intensity_difference': intensity_difference, - 'transition_in_progress': transition_in_progress, - 'layer_count': layer_count, - 'effect_count': effect_count, - 'adaptation': adaptation, - 'complexity': complexity_factor - } - - except Exception as e: - print(f"✗ 分析音乐反馈失败: {e}") - import traceback - traceback.print_exc() - return { - 'intensity': 0.0, - 'intensity_difference': 0.0, - 'transition_in_progress': False, - 'layer_count': 0, - 'effect_count': 0, - 'adaptation': 1.0, - 'complexity': 0.0 - } - - def update(self, dt: float): - """ - 更新分析引擎状态 - - Args: - dt: 时间增量 - """ - update_start_time = time.time() - current_time = time.time() - - # 定期执行分析 - if current_time - self.last_analysis_time >= self.analysis_interval: - self.perform_analysis() - self.last_analysis_time = current_time - - self.last_update_time = current_time - - # 更新性能统计 - self.stats['analysis_time'] += (time.time() - update_start_time) - - def cleanup(self): - """清理所有资源""" - self.analyzers.clear() - self.analysis_results.clear() - self.history.clear() - - self.stats = { - 'analyzers_created': 0, - 'analyses_performed': 0, - 'analysis_time': 0.0, - 'data_points_processed': 0, - 'prediction_accuracy': 0.0 - } - - print("✓ 分析引擎资源已清理") - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def get_analysis_results(self) -> Dict[str, Any]: - """ - 获取分析结果 - - Returns: - 分析结果 - """ - return self.analysis_results.copy() - - def get_history(self, data_type: str = 'intensity') -> List[Any]: - """ - 获取历史数据 - - Args: - data_type: 数据类型 - - Returns: - 历史数据列表 - """ - if data_type in self.history: - return list(self.history[data_type]) - return [] - - def set_analysis_interval(self, interval: float): - """ - 设置分析间隔 - - Args: - interval: 分析间隔(秒) - """ - self.analysis_interval = max(0.1, interval) - print(f"✓ 分析间隔设置为: {self.analysis_interval:.2f}秒") - - def set_analyzer_weight(self, analyzer_type: str, weight: float): - """ - 设置分析器权重 - - Args: - analyzer_type: 分析器类型 - weight: 权重值 - """ - if analyzer_type in self.weights: - self.weights[analyzer_type] = max(0.0, weight) - print(f"✓ {analyzer_type}分析器权重设置为: {self.weights[analyzer_type]:.2f}") - else: - print(f"✗ 无效的分析器类型: {analyzer_type}") - - def get_intensity_trend(self) -> float: - """ - 获取强度趋势 - - Returns: - 强度趋势值(正数表示上升,负数表示下降) - """ - try: - if len(self.history['intensity']) >= 2: - recent_values = list(self.history['intensity'])[-5:] # 最近5个值 - if len(recent_values) >= 2: - # 计算线性趋势 - x = np.arange(len(recent_values)) - y = np.array(recent_values) - if len(x) > 1 and np.std(x) > 0 and np.std(y) > 0: - slope = np.polyfit(x, y, 1)[0] # 线性拟合的斜率 - return slope - return 0.0 - except Exception as e: - print(f"✗ 计算强度趋势失败: {e}") - return 0.0 - - def predict_next_intensity(self) -> float: - """ - 预测下一个强度值 - - Returns: - 预测的强度值 - """ - try: - trend = self.get_intensity_trend() - current_intensity = self.analysis_results.get('intensity', 0.0) - predicted_intensity = current_intensity + trend - - # 限制在0-1范围内 - return max(0.0, min(1.0, predicted_intensity)) - except Exception as e: - print(f"✗ 预测强度值失败: {e}") - return self.analysis_results.get('intensity', 0.0) - - def get_dominant_factor(self) -> str: - """ - 获取主导因素 - - Returns: - 主导因素名称 - """ - if not self.analysis_results or 'details' not in self.analysis_results: - return 'unknown' - - details = self.analysis_results['details'] - max_intensity = 0.0 - dominant_factor = 'unknown' - - for factor_type, values in details.items(): - if values: - # 计算平均强度 - try: - avg_intensity = sum(value[0].get('intensity', 0.0) * value[1] - for value in values if isinstance(value[0], dict)) / len(values) - if avg_intensity > max_intensity: - max_intensity = avg_intensity - dominant_factor = factor_type - except Exception: - pass # 忽略计算错误 - - return dominant_factor - - def create_custom_analyzer(self, analyzer_name: str, - analysis_function: Callable[[], Dict[str, Any]]) -> bool: - """ - 创建自定义分析器 - - Args: - analyzer_name: 分析器名称 - analysis_function: 分析函数 - - Returns: - 是否创建成功 - """ - try: - analyzer_params = { - 'type': 'custom', - 'callback': analysis_function - } - - return self.create_analyzer(analyzer_name, analyzer_params) - - except Exception as e: - print(f"✗ 创建自定义分析器失败: {e}") - import traceback - traceback.print_exc() - return False - - def get_analyzer_status(self, analyzer_name: str) -> Dict[str, Any]: - """ - 获取分析器状态 - - Args: - analyzer_name: 分析器名称 - - Returns: - 分析器状态字典 - """ - if analyzer_name in self.analyzers: - analyzer = self.analyzers[analyzer_name] - return { - 'name': analyzer_name, - 'state': analyzer['state'], - 'type': analyzer['params']['type'], - 'enabled': analyzer['params']['enabled'], - 'last_analysis': analyzer.get('last_analysis', 0.0), - 'results_count': len(analyzer['history']), - 'trend': analyzer.get('trend', 0.0), - 'confidence': analyzer.get('confidence', 1.0), - 'volatility': analyzer.get('volatility', 0.0) - } - else: - return { - 'name': analyzer_name, - 'state': 'unknown' - } - - def set_prediction_settings(self, settings: Dict[str, Any]): - """ - 设置预测参数 - - Args: - settings: 预测参数字典 - """ - self.prediction_settings.update(settings) - print("✓ 预测参数已更新") - - def get_prediction_settings(self) -> Dict[str, Any]: - """ - 获取预测参数 - - Returns: - 预测参数字典 - """ - return self.prediction_settings.copy() - - def set_emotion_analysis_ranges(self, ranges: Dict[str, tuple]): - """ - 设置情绪分析范围 - - Args: - ranges: 范围字典 - """ - self.emotion_analysis.update(ranges) - print("✓ 情绪分析范围已更新") - - def get_emotion_analysis_ranges(self) -> Dict[str, tuple]: - """ - 获取情绪分析范围 - - Returns: - 范围字典 - """ - return self.emotion_analysis.copy() - - def get_analyzer_trend(self, analyzer_name: str) -> float: - """ - 获取分析器趋势 - - Args: - analyzer_name: 分析器名称 - - Returns: - 趋势值 - """ - if analyzer_name in self.analyzers: - return self.analyzers[analyzer_name].get('trend', 0.0) - return 0.0 - - def get_analyzer_confidence(self, analyzer_name: str) -> float: - """ - 获取分析器置信度 - - Args: - analyzer_name: 分析器名称 - - Returns: - 置信度值 - """ - if analyzer_name in self.analyzers: - return self.analyzers[analyzer_name].get('confidence', 1.0) - return 1.0 - - def get_overall_confidence(self) -> float: - """ - 获取整体置信度 - - Returns: - 整体置信度 - """ - if not self.analyzers: - return 1.0 - - total_confidence = sum(analyzer.get('confidence', 1.0) for analyzer in self.analyzers.values()) - return total_confidence / len(self.analyzers) - - def force_analysis(self) -> Dict[str, Any]: - """ - 强制执行分析(忽略时间间隔) - - Returns: - 分析结果 - """ - print("✓ 执行强制分析") - return self.perform_analysis() - - def get_analyzer_history(self, analyzer_name: str) -> List[Dict[str, Any]]: - """ - 获取分析器历史数据 - - Args: - analyzer_name: 分析器名称 - - Returns: - 历史数据列表 - """ - if analyzer_name in self.analyzers: - return list(self.analyzers[analyzer_name]['history']) - return [] - - def clear_analyzer_history(self, analyzer_name: str): - """ - 清空分析器历史数据 - - Args: - analyzer_name: 分析器名称 - """ - if analyzer_name in self.analyzers: - self.analyzers[analyzer_name]['history'].clear() - print(f"✓ 分析器 {analyzer_name} 历史数据已清空") - - def set_history_length(self, length: int): - """ - 设置历史数据长度 - - Args: - length: 历史数据长度 - """ - self.history_length = max(10, length) - # 更新所有历史队列的长度 - for key in self.history: - self.history[key] = deque(list(self.history[key])[-self.history_length:], maxlen=self.history_length) - print(f"✓ 历史数据长度设置为: {self.history_length}") - - def get_dominant_emotion(self) -> str: - """ - 获取主导情绪 - - Returns: - 主导情绪标签 - """ - try: - player_state = self.plugin.player_state if self.plugin else {} - emotional_state = player_state.get('emotional_state', 'neutral') - return emotional_state - except Exception as e: - print(f"✗ 获取主导情绪失败: {e}") - return 'neutral' - - def get_emotional_valence(self) -> float: - """ - 获取情绪效价(正负情绪) - - Returns: - 情绪效价值 (-1.0 到 1.0) - """ - try: - # 从玩家状态分析器获取效价 - player_results = self.analysis_results.get('details', {}).get('player_state', []) - if player_results: - for result, _ in player_results: - if isinstance(result, dict) and 'valence' in result: - return result['valence'] - return 0.0 - except Exception as e: - print(f"✗ 获取情绪效价失败: {e}") - return 0.0 - - def get_emotional_arousal(self) -> float: - """ - 获取情绪唤醒度(兴奋程度) - - Returns: - 情绪唤醒度值 (0.0 到 1.0) - """ - try: - # 从玩家状态分析器获取唤醒度 - player_results = self.analysis_results.get('details', {}).get('player_state', []) - if player_results: - for result, _ in player_results: - if isinstance(result, dict) and 'arousal' in result: - return result['arousal'] - return 0.4 # 默认中等唤醒度 - except Exception as e: - print(f"✗ 获取情绪唤醒度失败: {e}") - return 0.4 - - def get_narrative_tension(self) -> float: - """ - 获取叙事紧张度 - - Returns: - 叙事紧张度值 (0.0 到 1.0) - """ - try: - # 从叙事分析器获取紧张度 - narrative_results = self.analysis_results.get('details', {}).get('narrative', []) - if narrative_results: - for result, _ in narrative_results: - if isinstance(result, dict) and 'tension_level' in result: - return result['tension_level'] - return 0.0 - except Exception as e: - print(f"✗ 获取叙事紧张度失败: {e}") - return 0.0 - - def get_environmental_mood(self) -> float: - """ - 获取环境氛围 - - Returns: - 环境氛围值 (0.0 到 1.0) - """ - try: - # 从环境分析器获取氛围 - environment_results = self.analysis_results.get('details', {}).get('environment', []) - if environment_results: - for result, _ in environment_results: - if isinstance(result, dict) and 'mood' in result: - return result['mood'] - return 0.5 # 默认中性氛围 - except Exception as e: - print(f"✗ 获取环境氛围失败: {e}") - return 0.5 - - def get_combat_tension(self) -> float: - """ - 获取战斗紧张度 - - Returns: - 战斗紧张度值 (0.0 到 1.0) - """ - try: - # 从战斗分析器获取紧张度 - combat_results = self.analysis_results.get('details', {}).get('combat', []) - if combat_results: - for result, _ in combat_results: - if isinstance(result, dict) and 'tension' in result: - return result['tension'] - return 0.0 - except Exception as e: - print(f"✗ 获取战斗紧张度失败: {e}") - return 0.0 - - def get_exploration_curiosity(self) -> float: - """ - 获取探索好奇心 - - Returns: - 探索好奇心值 (0.0 到 1.0) - """ - try: - # 从探索分析器获取好奇心 - exploration_results = self.analysis_results.get('details', {}).get('exploration', []) - if exploration_results: - for result, _ in exploration_results: - if isinstance(result, dict) and 'curiosity' in result: - return result['curiosity'] - return 0.0 - except Exception as e: - print(f"✗ 获取探索好奇心失败: {e}") +""" +分析引擎 +负责分析游戏状态和环境条件,为音乐系统提供决策支持 +""" + +import uuid +import math +import time +import numpy as np +from typing import Dict, List, Any, Optional, Callable +from collections import deque + +class AnalysisEngine: + """ + 分析引擎 + 负责分析游戏状态和环境条件,为音乐系统提供决策支持 + """ + + def __init__(self, plugin): + """ + 初始化分析引擎 + + Args: + plugin: 动态音乐系统插件实例 + """ + self.plugin = plugin + self.analyzers: Dict[str, Dict[str, Any]] = {} + self.analysis_results: Dict[str, Any] = {} + self.history: Dict[str, deque] = {} + + self.stats = { + 'analyzers_created': 0, + 'analyses_performed': 0, + 'analysis_time': 0.0, + 'data_points_processed': 0, + 'prediction_accuracy': 0.0 + } + + self.buffer_size = plugin.buffer_size if plugin else 4096 + self.analysis_interval = 1.0 # 默认分析间隔(秒) + self.last_analysis_time = 0.0 + self.last_update_time = 0.0 + + # 分析权重 + self.weights = { + 'combat': 1.0, + 'exploration': 0.8, + 'environment': 0.6, + 'player_state': 0.7, + 'narrative': 0.5 + } + + # 历史数据长度 + self.history_length = 100 + + # 预测参数 + self.prediction_settings = { + 'lookahead_time': 5.0, # 预测时间窗口 + 'confidence_threshold': 0.7, # 置信度阈值 + 'trend_sensitivity': 0.3, # 趋势敏感度 + 'prediction_smoothing': 0.1 # 预测平滑系数 + } + + # 情绪分析参数 + self.emotion_analysis = { + 'valence_range': (-1.0, 1.0), # 情绪效价范围 + 'arousal_range': (0.0, 1.0), # 情绪唤醒度范围 + 'tension_range': (0.0, 1.0), # 紧张度范围 + 'complexity_range': (0.0, 1.0) # 复杂度范围 + } + + # 初始化分析引擎 + self._initialize_analysis_engine() + + def _initialize_analysis_engine(self): + """初始化分析引擎""" + # 初始化历史记录 + history_keys = [ + 'intensity', 'combat_level', 'exploration_level', + 'environment_changes', 'player_emotion', 'narrative_pacing', + 'music_intensity', 'player_health', 'player_stamina' + ] + + for key in history_keys: + self.history[key] = deque(maxlen=self.history_length) + + # 创建默认分析器 + self._create_default_analyzers() + print("✓ 分析引擎初始化完成") + + def _create_default_analyzers(self): + """创建默认分析器""" + # 战斗强度分析器 + self.create_analyzer('combat_analyzer', { + 'type': 'combat', + 'callback': self._analyze_combat_state + }) + + # 探索状态分析器 + self.create_analyzer('exploration_analyzer', { + 'type': 'exploration', + 'callback': self._analyze_exploration_state + }) + + # 环境条件分析器 + self.create_analyzer('environment_analyzer', { + 'type': 'environment', + 'callback': self._analyze_environment_conditions + }) + + # 玩家状态分析器 + self.create_analyzer('player_analyzer', { + 'type': 'player_state', + 'callback': self._analyze_player_state + }) + + # 叙事节奏分析器 + self.create_analyzer('narrative_analyzer', { + 'type': 'narrative', + 'callback': self._analyze_narrative_pacing + }) + + # 音乐反馈分析器 + self.create_analyzer('music_feedback_analyzer', { + 'type': 'music_feedback', + 'callback': self._analyze_music_feedback + }) + + def create_analyzer(self, analyzer_name: str, analyzer_params: Dict[str, Any]) -> bool: + """ + 创建分析器 + + Args: + analyzer_name: 分析器名称 + analyzer_params: 分析器参数 + + Returns: + 是否创建成功 + """ + try: + # 设置默认参数 + default_params = { + 'type': 'generic', + 'enabled': True, + 'interval': self.analysis_interval, + 'weight': 1.0, + 'history_size': 50, + 'sensitivity': 1.0, + 'smoothing': 0.2, + 'thresholds': { + 'low': 0.3, + 'medium': 0.6, + 'high': 0.8 + } + } + + # 合并参数 + params = default_params.copy() + params.update(analyzer_params) + + # 验证回调函数 + if 'callback' not in params or not callable(params['callback']): + print(f"✗ 分析器缺少有效的回调函数: {analyzer_name}") + return False + + # 创建分析器 + analyzer = { + 'name': analyzer_name, + 'params': params, + 'state': 'created', + 'last_analysis': 0.0, + 'results': {}, + 'history': deque(maxlen=params['history_size']), + 'trend': 0.0, # 趋势值 + 'volatility': 0.0, # 波动性 + 'confidence': 1.0 # 置信度 + } + + self.analyzers[analyzer_name] = analyzer + self.stats['analyzers_created'] += 1 + + print(f"✓ 分析器创建成功: {analyzer_name}") + return True + + except Exception as e: + print(f"✗ 创建分析器失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable_analyzer(self, analyzer_name: str, enable: bool = True) -> bool: + """ + 启用/禁用分析器 + + Args: + analyzer_name: 分析器名称 + enable: 是否启用 + + Returns: + 是否设置成功 + """ + try: + if analyzer_name not in self.analyzers: + print(f"✗ 分析器不存在: {analyzer_name}") + return False + + self.analyzers[analyzer_name]['params']['enabled'] = enable + state = "启用" if enable else "禁用" + print(f"✓ 分析器已{state}: {analyzer_name}") + return True + + except Exception as e: + print(f"✗ 设置分析器状态失败: {e}") + import traceback + traceback.print_exc() + return False + + def perform_analysis(self) -> Dict[str, Any]: + """ + 执行分析 + + Returns: + 分析结果 + """ + try: + analysis_start_time = time.time() + current_time = time.time() + + # 收集所有分析器的结果 + combined_results = {} + total_weight = 0.0 + weighted_intensity = 0.0 + analyzer_contributions = {} + + for analyzer_name, analyzer in self.analyzers.items(): + params = analyzer['params'] + + # 检查分析器是否启用 + if not params.get('enabled', True): + continue + + # 检查分析间隔 + interval = params.get('interval', self.analysis_interval) + if current_time - analyzer.get('last_analysis', 0.0) < interval: + # 使用之前的分析结果 + if analyzer['results']: + result = analyzer['results'] + else: + continue + else: + # 执行新的分析 + try: + callback = params['callback'] + result = callback() + analyzer['results'] = result + analyzer['last_analysis'] = current_time + + # 更新历史记录 + analyzer['history'].append(result) + + # 计算趋势和波动性 + self._update_analyzer_trends(analyzer) + + self.stats['analyses_performed'] += 1 + except Exception as e: + print(f"✗ 分析器 {analyzer_name} 执行失败: {e}") + import traceback + traceback.print_exc() + continue + + # 合并结果 + analyzer_type = params.get('type', 'generic') + weight = params.get('weight', 1.0) * self.weights.get(analyzer_type, 1.0) + + # 应用灵敏度调整 + sensitivity = params.get('sensitivity', 1.0) + if isinstance(result, dict): + adjusted_result = {} + for key, value in result.items(): + if isinstance(value, (int, float)): + adjusted_result[key] = value * sensitivity + else: + adjusted_result[key] = value + result = adjusted_result + elif isinstance(result, (int, float)): + result = result * sensitivity + + if isinstance(result, dict): + for key, value in result.items(): + if key not in combined_results: + combined_results[key] = [] + combined_results[key].append((value, weight)) + else: + # 处理单一值结果 + if 'generic' not in combined_results: + combined_results['generic'] = [] + combined_results['generic'].append((result, weight)) + + # 计算加权强度 + if 'intensity' in result: + if isinstance(result['intensity'], (int, float)): + weighted_intensity += result['intensity'] * weight + total_weight += weight + analyzer_contributions[analyzer_name] = result['intensity'] * weight + + # 更新分析器置信度 + self._update_analyzer_confidence(analyzer, result) + + # 计算平均强度 + if total_weight > 0: + average_intensity = weighted_intensity / total_weight + else: + average_intensity = 0.0 + + # 应用平滑处理 + smoothing = 0.1 + if 'intensity' in self.analysis_results: + previous_intensity = self.analysis_results['intensity'] + average_intensity = previous_intensity * smoothing + average_intensity * (1 - smoothing) + + # 整合分析结果 + final_results = { + 'timestamp': current_time, + 'intensity': average_intensity, + 'raw_intensity': weighted_intensity / total_weight if total_weight > 0 else 0.0, + 'details': combined_results, + 'confidence': min(1.0, len(combined_results) / max(1, len(self.analyzers))), + 'analyzer_contributions': analyzer_contributions, + 'trends': self._get_overall_trends(), + 'predictions': self._make_predictions(average_intensity) + } + + # 更新历史记录 + self.history['intensity'].append(average_intensity) + if 'combat_level' in combined_results: + combat_values = [r[0] for r in combined_results.get('combat_level', [])] + if combat_values: + self.history['combat_level'].append(np.mean(combat_values)) + if 'exploration_level' in combined_results: + exploration_values = [r[0] for r in combined_results.get('exploration_level', [])] + if exploration_values: + self.history['exploration_level'].append(np.mean(exploration_values)) + + # 存储结果 + self.analysis_results = final_results + + # 更新性能统计 + self.stats['analysis_time'] += (time.time() - analysis_start_time) + self.stats['data_points_processed'] += len(combined_results) + + return final_results + + except Exception as e: + print(f"✗ 执行分析失败: {e}") + import traceback + traceback.print_exc() + return {} + + def _update_analyzer_trends(self, analyzer: Dict[str, Any]): + """ + 更新分析器趋势 + + Args: + analyzer: 分析器数据 + """ + try: + if len(analyzer['history']) < 2: + return + + # 计算强度趋势 + intensity_values = [] + for result in analyzer['history']: + if isinstance(result, dict) and 'intensity' in result: + intensity_values.append(result['intensity']) + + if len(intensity_values) >= 2: + # 线性回归计算趋势 + x = np.arange(len(intensity_values)) + y = np.array(intensity_values) + if len(x) > 1 and np.std(x) > 0 and np.std(y) > 0: + slope, _ = np.polyfit(x, y, 1) + analyzer['trend'] = slope + + # 计算波动性 + analyzer['volatility'] = np.std(y) + + except Exception as e: + print(f"✗ 更新分析器趋势失败: {e}") + + def _update_analyzer_confidence(self, analyzer: Dict[str, Any], result: Any): + """ + 更新分析器置信度 + + Args: + analyzer: 分析器数据 + result: 分析结果 + """ + try: + # 基于历史一致性更新置信度 + if len(analyzer['history']) >= 3: + recent_values = [] + for hist_result in list(analyzer['history'])[-3:]: + if isinstance(hist_result, dict) and 'intensity' in hist_result: + recent_values.append(hist_result['intensity']) + + if len(recent_values) >= 2: + # 计算变化率 + changes = [abs(recent_values[i] - recent_values[i-1]) for i in range(1, len(recent_values))] + avg_change = np.mean(changes) + + # 基于稳定性调整置信度 + stability = 1.0 / (1.0 + avg_change * 10) + analyzer['confidence'] = 0.7 * analyzer['confidence'] + 0.3 * stability + + # 确保置信度在合理范围内 + analyzer['confidence'] = max(0.1, min(1.0, analyzer['confidence'])) + + except Exception as e: + print(f"✗ 更新分析器置信度失败: {e}") + + def _get_overall_trends(self) -> Dict[str, float]: + """ + 获取整体趋势 + + Returns: + 趋势字典 + """ + try: + trends = {} + + # 计算整体强度趋势 + if len(self.history['intensity']) >= 5: + recent_values = list(self.history['intensity'])[-5:] + x = np.arange(len(recent_values)) + y = np.array(recent_values) + if len(x) > 1 and np.std(x) > 0 and np.std(y) > 0: + slope, _ = np.polyfit(x, y, 1) + trends['intensity'] = slope + + # 计算战斗趋势 + if len(self.history['combat_level']) >= 5: + recent_values = list(self.history['combat_level'])[-5:] + x = np.arange(len(recent_values)) + y = np.array(recent_values) + if len(x) > 1 and np.std(x) > 0 and np.std(y) > 0: + slope, _ = np.polyfit(x, y, 1) + trends['combat'] = slope + + # 计算探索趋势 + if len(self.history['exploration_level']) >= 5: + recent_values = list(self.history['exploration_level'])[-5:] + x = np.arange(len(recent_values)) + y = np.array(recent_values) + if len(x) > 1 and np.std(x) > 0 and np.std(y) > 0: + slope, _ = np.polyfit(x, y, 1) + trends['exploration'] = slope + + return trends + + except Exception as e: + print(f"✗ 获取整体趋势失败: {e}") + return {} + + def _make_predictions(self, current_intensity: float) -> Dict[str, Any]: + """ + 做出预测 + + Args: + current_intensity: 当前强度 + + Returns: + 预测结果 + """ + try: + predictions = {} + lookahead_time = self.prediction_settings['lookahead_time'] + + # 基于趋势预测未来强度 + overall_trend = self._get_overall_trends().get('intensity', 0.0) + predicted_intensity = current_intensity + overall_trend * lookahead_time + + # 限制在合理范围内 + predicted_intensity = max(0.0, min(1.0, predicted_intensity)) + + # 计算预测置信度 + trend_magnitude = abs(overall_trend) + confidence = min(1.0, trend_magnitude * 10) # 趋势越大,置信度越高 + + predictions['intensity'] = { + 'value': predicted_intensity, + 'confidence': confidence, + 'time_horizon': lookahead_time + } + + # 基于历史模式预测事件 + predictions['events'] = self._predict_events() + + return predictions + + except Exception as e: + print(f"✗ 做出预测失败: {e}") + return {} + + def _predict_events(self) -> List[Dict[str, Any]]: + """ + 预测事件 + + Returns: + 预测事件列表 + """ + try: + events = [] + + # 检查战斗强度是否即将上升 + if len(self.history['combat_level']) >= 10: + recent_combat = list(self.history['combat_level'])[-10:] + trend = np.polyfit(np.arange(len(recent_combat)), np.array(recent_combat), 1)[0] + + if trend > 0.1 and recent_combat[-1] > 0.3: # 上升趋势且当前强度较高 + events.append({ + 'type': 'combat_escalation', + 'probability': min(1.0, trend * 5), + 'time_to_event': max(1.0, 10 - recent_combat[-1] * 10) + }) + + # 检查探索活动是否减少 + if len(self.history['exploration_level']) >= 10: + recent_exploration = list(self.history['exploration_level'])[-10:] + trend = np.polyfit(np.arange(len(recent_exploration)), np.array(recent_exploration), 1)[0] + + if trend < -0.1 and recent_exploration[-1] < 0.5: # 下降趋势且当前强度较低 + events.append({ + 'type': 'exploration_decrease', + 'probability': min(1.0, abs(trend) * 5), + 'time_to_event': max(1.0, recent_exploration[-1] * 10) + }) + + return events + + except Exception as e: + print(f"✗ 预测事件失败: {e}") + return [] + + def _analyze_combat_state(self) -> Dict[str, Any]: + """ + 分析战斗状态 + + Returns: + 战斗状态分析结果 + """ + try: + # 获取游戏状态 + game_state = self.plugin.game_state if self.plugin else {} + + # 分析战斗相关指标 + in_combat = game_state.get('in_combat', False) + enemies_nearby = game_state.get('enemies_nearby', 0) + player_health = game_state.get('player_health', 1.0) + combat_intensity = game_state.get('combat_intensity', 0.0) + player_damage = game_state.get('player_damage', 0.0) + enemy_damage = game_state.get('enemy_damage', 0.0) + combat_duration = game_state.get('combat_duration', 0.0) + + # 计算战斗强度 + combat_level = 0.0 + if in_combat: + # 基于敌人数量和强度 + enemy_factor = min(1.0, enemies_nearby / 10.0) + # 基于玩家生命值 + health_factor = 1.0 - player_health + # 基于战斗强度 + intensity_factor = combat_intensity + # 基于伤害交换 + damage_factor = min(1.0, (player_damage + enemy_damage) / 100.0) + # 基于战斗持续时间 + duration_factor = min(1.0, combat_duration / 60.0) + + combat_level = (enemy_factor + health_factor + intensity_factor + damage_factor + duration_factor) / 5.0 + + # 计算战斗紧张度 + tension = 0.0 + if in_combat: + # 基于生命值差异 + health_tension = 1.0 - player_health + # 基于敌人数 + enemy_tension = min(1.0, enemies_nearby / 5.0) + # 综合紧张度 + tension = (health_tension + enemy_tension) / 2.0 + + # 更新历史记录 + self.history['combat_level'].append(combat_level) + self.history['player_health'].append(player_health) + + return { + 'intensity': combat_level, + 'in_combat': in_combat, + 'enemies_nearby': enemies_nearby, + 'player_health': player_health, + 'combat_intensity': combat_intensity, + 'tension': tension, + 'damage_exchange': player_damage + enemy_damage, + 'duration': combat_duration + } + + except Exception as e: + print(f"✗ 分析战斗状态失败: {e}") + import traceback + traceback.print_exc() + return { + 'intensity': 0.0, + 'in_combat': False, + 'enemies_nearby': 0, + 'player_health': 1.0, + 'combat_intensity': 0.0, + 'tension': 0.0, + 'damage_exchange': 0.0, + 'duration': 0.0 + } + + def _analyze_exploration_state(self) -> Dict[str, Any]: + """ + 分析探索状态 + + Returns: + 探索状态分析结果 + """ + try: + # 获取游戏状态 + game_state = self.plugin.game_state if self.plugin else {} + + # 分析探索相关指标 + exploring = game_state.get('exploring', False) + areas_discovered = game_state.get('areas_discovered', 0) + exploration_progress = game_state.get('exploration_progress', 0.0) + time_since_discovery = game_state.get('time_since_discovery', 0.0) + movement_speed = game_state.get('movement_speed', 0.0) + player_stealth = game_state.get('player_stealth', 0.0) + secrets_found = game_state.get('secrets_found', 0) + total_secrets = game_state.get('total_secrets', 1) + + # 计算探索强度 + exploration_level = 0.0 + if exploring: + # 基于探索进度 + progress_factor = exploration_progress + # 基于发现区域数量 + discovery_factor = min(1.0, areas_discovered / 20.0) + # 基于发现时间(新发现时强度更高) + time_factor = max(0.0, min(1.0, 1.0 - time_since_discovery / 60.0)) + # 基于移动速度 + speed_factor = min(1.0, movement_speed / 10.0) + # 基于隐秘程度 + stealth_factor = player_stealth + # 基于秘密发现率 + secrets_factor = min(1.0, secrets_found / max(1, total_secrets)) + + exploration_level = (progress_factor + discovery_factor + time_factor + + speed_factor + stealth_factor + secrets_factor) / 6.0 + + # 计算探索好奇心 + curiosity = 0.0 + if exploring: + # 基于未发现区域 + undiscovered_factor = 1.0 - exploration_progress + # 基于秘密发现率 + secrets_undiscovered = max(0, total_secrets - secrets_found) + secrets_factor = min(1.0, secrets_undiscovered / max(1, total_secrets)) + # 综合好奇心 + curiosity = (undiscovered_factor + secrets_factor) / 2.0 + + # 更新历史记录 + self.history['exploration_level'].append(exploration_level) + + return { + 'intensity': exploration_level, + 'exploring': exploring, + 'areas_discovered': areas_discovered, + 'exploration_progress': exploration_progress, + 'time_since_discovery': time_since_discovery, + 'movement_speed': movement_speed, + 'stealth': player_stealth, + 'curiosity': curiosity, + 'secrets_completion': secrets_found / max(1, total_secrets) + } + + except Exception as e: + print(f"✗ 分析探索状态失败: {e}") + import traceback + traceback.print_exc() + return { + 'intensity': 0.0, + 'exploring': False, + 'areas_discovered': 0, + 'exploration_progress': 0.0, + 'time_since_discovery': 0.0, + 'movement_speed': 0.0, + 'stealth': 0.0, + 'curiosity': 0.0, + 'secrets_completion': 0.0 + } + + def _analyze_environment_conditions(self) -> Dict[str, Any]: + """ + 分析环境条件 + + Returns: + 环境条件分析结果 + """ + try: + # 获取环境条件 + environment = self.plugin.environment_conditions if self.plugin else {} + + # 分析环境相关指标 + weather = environment.get('weather', 'clear') + time_of_day = environment.get('time_of_day', 'day') + location_type = environment.get('location_type', 'outdoor') + danger_level = environment.get('danger_level', 0.0) + atmosphere = environment.get('atmosphere', 0.5) + temperature = environment.get('temperature', 20.0) + visibility = environment.get('visibility', 1.0) + lighting = environment.get('lighting', 1.0) + weather_intensity = environment.get('weather_intensity', 0.0) + + # 计算环境强度 + environment_level = 0.0 + + # 天气影响 + weather_factors = { + 'clear': 0.2, + 'cloudy': 0.3, + 'rain': 0.5, + 'storm': 0.8, + 'fog': 0.6, + 'snow': 0.4, + 'windy': 0.4 + } + weather_factor = weather_factors.get(weather, 0.3) + + # 时间影响 + time_factors = { + 'day': 0.3, + 'dusk': 0.5, + 'night': 0.7, + 'dawn': 0.4 + } + time_factor = time_factors.get(time_of_day, 0.3) + + # 地点类型影响 + location_factors = { + 'indoor': 0.4, + 'outdoor': 0.6, + 'underground': 0.8, + 'underwater': 0.7, + 'cave': 0.7, + 'forest': 0.5, + 'desert': 0.4 + } + location_factor = location_factors.get(location_type, 0.5) + + # 综合计算 + environment_level = (weather_factor + time_factor + location_factor + + danger_level + atmosphere + weather_intensity) / 6.0 + + # 计算环境紧张度 + environmental_tension = (danger_level + (1.0 - visibility) + (1.0 - lighting)) / 3.0 + + # 计算环境氛围 + environmental_mood = atmosphere + + # 更新历史记录 + self.history['environment_changes'].append(environment_level) + + return { + 'intensity': environment_level, + 'weather': weather, + 'time_of_day': time_of_day, + 'location_type': location_type, + 'danger_level': danger_level, + 'atmosphere': atmosphere, + 'temperature': temperature, + 'visibility': visibility, + 'lighting': lighting, + 'weather_intensity': weather_intensity, + 'tension': environmental_tension, + 'mood': environmental_mood + } + + except Exception as e: + print(f"✗ 分析环境条件失败: {e}") + import traceback + traceback.print_exc() + return { + 'intensity': 0.3, + 'weather': 'clear', + 'time_of_day': 'day', + 'location_type': 'outdoor', + 'danger_level': 0.0, + 'atmosphere': 0.5, + 'temperature': 20.0, + 'visibility': 1.0, + 'lighting': 1.0, + 'weather_intensity': 0.0, + 'tension': 0.0, + 'mood': 0.5 + } + + def _analyze_player_state(self) -> Dict[str, Any]: + """ + 分析玩家状态 + + Returns: + 玩家状态分析结果 + """ + try: + # 获取玩家状态 + player_state = self.plugin.player_state if self.plugin else {} + + # 分析玩家相关指标 + health = player_state.get('health', 1.0) + stamina = player_state.get('stamina', 1.0) + experience = player_state.get('experience', 0) + level = player_state.get('level', 1) + emotional_state = player_state.get('emotional_state', 'neutral') + skill_level = player_state.get('skill_level', 0.5) + fatigue = player_state.get('fatigue', 0.0) + hunger = player_state.get('hunger', 0.0) + stress = player_state.get('stress', 0.0) + focus = player_state.get('focus', 0.5) + courage = player_state.get('courage', 0.5) + items_collected = player_state.get('items_collected', 0) + quests_completed = player_state.get('quests_completed', 0) + + # 计算玩家状态强度 + # 基于健康和耐力 + physical_factor = (health + stamina) / 2.0 + # 基于经验和等级 + progression_factor = min(1.0, (experience / 1000.0 + level / 50.0) / 2.0) + # 基于技能水平 + skill_factor = skill_level + # 基于疲劳和饥饿 + condition_factor = 1.0 - (fatigue + hunger) / 2.0 + + # 综合计算 + player_level = (physical_factor + progression_factor + skill_factor + condition_factor) / 4.0 + + # 情感状态影响 + emotion_factors = { + 'excited': 0.8, + 'happy': 0.6, + 'neutral': 0.5, + 'sad': 0.3, + 'afraid': 0.7, + 'angry': 0.9, + 'calm': 0.4, + 'curious': 0.6 + } + emotion_factor = emotion_factors.get(emotional_state, 0.5) + + # 计算玩家情绪效价(正负情绪) + valence_map = { + 'excited': 0.7, 'happy': 0.8, 'neutral': 0.0, + 'sad': -0.6, 'afraid': -0.5, 'angry': -0.3, + 'calm': 0.2, 'curious': 0.4 + } + valence = valence_map.get(emotional_state, 0.0) + + # 计算玩家唤醒度(兴奋程度) + arousal_map = { + 'excited': 0.9, 'happy': 0.6, 'neutral': 0.4, + 'sad': 0.3, 'afraid': 0.8, 'angry': 0.7, + 'calm': 0.2, 'curious': 0.5 + } + arousal = arousal_map.get(emotional_state, 0.4) + + # 计算玩家压力水平 + stress_level = (stress + (1.0 - health) + fatigue) / 3.0 + + # 更新历史记录 + self.history['player_emotion'].append(emotion_factor) + self.history['player_health'].append(health) + self.history['player_stamina'].append(stamina) + + return { + 'intensity': player_level, + 'health': health, + 'stamina': stamina, + 'experience': experience, + 'level': level, + 'emotional_state': emotional_state, + 'skill_level': skill_level, + 'fatigue': fatigue, + 'hunger': hunger, + 'stress': stress, + 'focus': focus, + 'courage': courage, + 'items_collected': items_collected, + 'quests_completed': quests_completed, + 'valence': valence, # 情绪效价 + 'arousal': arousal, # 唤醒度 + 'stress_level': stress_level # 压力水平 + } + + except Exception as e: + print(f"✗ 分析玩家状态失败: {e}") + import traceback + traceback.print_exc() + return { + 'intensity': 0.5, + 'health': 1.0, + 'stamina': 1.0, + 'experience': 0, + 'level': 1, + 'emotional_state': 'neutral', + 'skill_level': 0.5, + 'fatigue': 0.0, + 'hunger': 0.0, + 'stress': 0.0, + 'focus': 0.5, + 'courage': 0.5, + 'items_collected': 0, + 'quests_completed': 0, + 'valence': 0.0, + 'arousal': 0.4, + 'stress_level': 0.0 + } + + def _analyze_narrative_pacing(self) -> Dict[str, Any]: + """ + 分析叙事节奏 + + Returns: + 叙事节奏分析结果 + """ + try: + # 获取游戏状态 + game_state = self.plugin.game_state if self.plugin else {} + + # 分析叙事相关指标 + story_phase = game_state.get('story_phase', 'opening') + quest_progress = game_state.get('quest_progress', 0.0) + recent_events = game_state.get('recent_events', []) + tension_level = game_state.get('tension_level', 0.0) + player_choices = game_state.get('player_choices', 0) + dialogue_intensity = game_state.get('dialogue_intensity', 0.0) + cutscene_time = game_state.get('cutscene_time', 0.0) + player_agency = game_state.get('player_agency', 0.5) + narrative_complexity = game_state.get('narrative_complexity', 0.3) + character_development = game_state.get('character_development', 0.0) + plot_twists = game_state.get('plot_twists', 0) + + # 计算叙事强度 + narrative_level = 0.0 + + # 故事阶段影响 + phase_factors = { + 'opening': 0.3, + 'rising_action': 0.6, + 'climax': 0.9, + 'falling_action': 0.5, + 'resolution': 0.2 + } + phase_factor = phase_factors.get(story_phase, 0.5) + + # 任务进度影响 + progress_factor = quest_progress + + # 紧张程度影响 + tension_factor = tension_level + + # 事件密度影响(最近事件数量) + event_density = min(1.0, len(recent_events) / 10.0) + + # 对话强度影响 + dialogue_factor = dialogue_intensity + + # 玩家选择影响 + choice_factor = min(1.0, player_choices / 20.0) + + # 综合计算 + narrative_level = (phase_factor + progress_factor + tension_factor + + event_density + dialogue_factor + choice_factor) / 6.0 + + # 计算叙事复杂度 + complexity = narrative_complexity + + # 计算角色发展强度 + character_factor = character_development + + # 计算剧情转折强度 + twist_factor = min(1.0, plot_twists / 5.0) + + # 更新历史记录 + self.history['narrative_pacing'].append(narrative_level) + + return { + 'intensity': narrative_level, + 'story_phase': story_phase, + 'quest_progress': quest_progress, + 'recent_events_count': len(recent_events), + 'tension_level': tension_level, + 'player_choices': player_choices, + 'dialogue_intensity': dialogue_intensity, + 'cutscene_time': cutscene_time, + 'player_agency': player_agency, + 'complexity': complexity, + 'character_development': character_development, + 'plot_twists': plot_twists, + 'event_density': event_density, + 'choice_impact': choice_factor + } + + except Exception as e: + print(f"✗ 分析叙事节奏失败: {e}") + import traceback + traceback.print_exc() + return { + 'intensity': 0.5, + 'story_phase': 'opening', + 'quest_progress': 0.0, + 'recent_events_count': 0, + 'tension_level': 0.0, + 'player_choices': 0, + 'dialogue_intensity': 0.0, + 'cutscene_time': 0.0, + 'player_agency': 0.5, + 'complexity': 0.3, + 'character_development': 0.0, + 'plot_twists': 0, + 'event_density': 0.0, + 'choice_impact': 0.0 + } + + def _analyze_music_feedback(self) -> Dict[str, Any]: + """ + 分析音乐反馈 + + Returns: + 音乐反馈分析结果 + """ + try: + # 获取当前音乐状态 + current_intensity = self.plugin.get_intensity() if self.plugin else 0.0 + target_intensity = self.plugin.target_intensity if self.plugin else 0.0 + + # 分析音乐反馈相关指标 + intensity_difference = abs(current_intensity - target_intensity) + transition_in_progress = hasattr(self.plugin, 'transition_manager') and \ + len(self.plugin.transition_manager.active_transitions) > 0 if self.plugin else False + layer_count = len(self.plugin.music_manager.active_themes) if self.plugin and hasattr(self.plugin, 'music_manager') else 0 + effect_count = len(self.plugin.effect_processor.active_effects) if self.plugin and hasattr(self.plugin, 'effect_processor') else 0 + + # 计算音乐反馈强度 + feedback_level = 0.0 + + # 基于强度差异 + difference_factor = intensity_difference + + # 基于过渡状态 + transition_factor = 1.0 if transition_in_progress else 0.0 + + # 基于复杂度(层数和效果数) + complexity_factor = min(1.0, (layer_count + effect_count) / 10.0) + + # 综合计算 + feedback_level = (difference_factor + transition_factor + complexity_factor) / 3.0 + + # 计算音乐适应性 + adaptation = 1.0 - intensity_difference + + # 更新历史记录 + self.history['music_intensity'].append(current_intensity) + + return { + 'intensity': feedback_level, + 'intensity_difference': intensity_difference, + 'transition_in_progress': transition_in_progress, + 'layer_count': layer_count, + 'effect_count': effect_count, + 'adaptation': adaptation, + 'complexity': complexity_factor + } + + except Exception as e: + print(f"✗ 分析音乐反馈失败: {e}") + import traceback + traceback.print_exc() + return { + 'intensity': 0.0, + 'intensity_difference': 0.0, + 'transition_in_progress': False, + 'layer_count': 0, + 'effect_count': 0, + 'adaptation': 1.0, + 'complexity': 0.0 + } + + def update(self, dt: float): + """ + 更新分析引擎状态 + + Args: + dt: 时间增量 + """ + update_start_time = time.time() + current_time = time.time() + + # 定期执行分析 + if current_time - self.last_analysis_time >= self.analysis_interval: + self.perform_analysis() + self.last_analysis_time = current_time + + self.last_update_time = current_time + + # 更新性能统计 + self.stats['analysis_time'] += (time.time() - update_start_time) + + def cleanup(self): + """清理所有资源""" + self.analyzers.clear() + self.analysis_results.clear() + self.history.clear() + + self.stats = { + 'analyzers_created': 0, + 'analyses_performed': 0, + 'analysis_time': 0.0, + 'data_points_processed': 0, + 'prediction_accuracy': 0.0 + } + + print("✓ 分析引擎资源已清理") + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def get_analysis_results(self) -> Dict[str, Any]: + """ + 获取分析结果 + + Returns: + 分析结果 + """ + return self.analysis_results.copy() + + def get_history(self, data_type: str = 'intensity') -> List[Any]: + """ + 获取历史数据 + + Args: + data_type: 数据类型 + + Returns: + 历史数据列表 + """ + if data_type in self.history: + return list(self.history[data_type]) + return [] + + def set_analysis_interval(self, interval: float): + """ + 设置分析间隔 + + Args: + interval: 分析间隔(秒) + """ + self.analysis_interval = max(0.1, interval) + print(f"✓ 分析间隔设置为: {self.analysis_interval:.2f}秒") + + def set_analyzer_weight(self, analyzer_type: str, weight: float): + """ + 设置分析器权重 + + Args: + analyzer_type: 分析器类型 + weight: 权重值 + """ + if analyzer_type in self.weights: + self.weights[analyzer_type] = max(0.0, weight) + print(f"✓ {analyzer_type}分析器权重设置为: {self.weights[analyzer_type]:.2f}") + else: + print(f"✗ 无效的分析器类型: {analyzer_type}") + + def get_intensity_trend(self) -> float: + """ + 获取强度趋势 + + Returns: + 强度趋势值(正数表示上升,负数表示下降) + """ + try: + if len(self.history['intensity']) >= 2: + recent_values = list(self.history['intensity'])[-5:] # 最近5个值 + if len(recent_values) >= 2: + # 计算线性趋势 + x = np.arange(len(recent_values)) + y = np.array(recent_values) + if len(x) > 1 and np.std(x) > 0 and np.std(y) > 0: + slope = np.polyfit(x, y, 1)[0] # 线性拟合的斜率 + return slope + return 0.0 + except Exception as e: + print(f"✗ 计算强度趋势失败: {e}") + return 0.0 + + def predict_next_intensity(self) -> float: + """ + 预测下一个强度值 + + Returns: + 预测的强度值 + """ + try: + trend = self.get_intensity_trend() + current_intensity = self.analysis_results.get('intensity', 0.0) + predicted_intensity = current_intensity + trend + + # 限制在0-1范围内 + return max(0.0, min(1.0, predicted_intensity)) + except Exception as e: + print(f"✗ 预测强度值失败: {e}") + return self.analysis_results.get('intensity', 0.0) + + def get_dominant_factor(self) -> str: + """ + 获取主导因素 + + Returns: + 主导因素名称 + """ + if not self.analysis_results or 'details' not in self.analysis_results: + return 'unknown' + + details = self.analysis_results['details'] + max_intensity = 0.0 + dominant_factor = 'unknown' + + for factor_type, values in details.items(): + if values: + # 计算平均强度 + try: + avg_intensity = sum(value[0].get('intensity', 0.0) * value[1] + for value in values if isinstance(value[0], dict)) / len(values) + if avg_intensity > max_intensity: + max_intensity = avg_intensity + dominant_factor = factor_type + except Exception: + pass # 忽略计算错误 + + return dominant_factor + + def create_custom_analyzer(self, analyzer_name: str, + analysis_function: Callable[[], Dict[str, Any]]) -> bool: + """ + 创建自定义分析器 + + Args: + analyzer_name: 分析器名称 + analysis_function: 分析函数 + + Returns: + 是否创建成功 + """ + try: + analyzer_params = { + 'type': 'custom', + 'callback': analysis_function + } + + return self.create_analyzer(analyzer_name, analyzer_params) + + except Exception as e: + print(f"✗ 创建自定义分析器失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_analyzer_status(self, analyzer_name: str) -> Dict[str, Any]: + """ + 获取分析器状态 + + Args: + analyzer_name: 分析器名称 + + Returns: + 分析器状态字典 + """ + if analyzer_name in self.analyzers: + analyzer = self.analyzers[analyzer_name] + return { + 'name': analyzer_name, + 'state': analyzer['state'], + 'type': analyzer['params']['type'], + 'enabled': analyzer['params']['enabled'], + 'last_analysis': analyzer.get('last_analysis', 0.0), + 'results_count': len(analyzer['history']), + 'trend': analyzer.get('trend', 0.0), + 'confidence': analyzer.get('confidence', 1.0), + 'volatility': analyzer.get('volatility', 0.0) + } + else: + return { + 'name': analyzer_name, + 'state': 'unknown' + } + + def set_prediction_settings(self, settings: Dict[str, Any]): + """ + 设置预测参数 + + Args: + settings: 预测参数字典 + """ + self.prediction_settings.update(settings) + print("✓ 预测参数已更新") + + def get_prediction_settings(self) -> Dict[str, Any]: + """ + 获取预测参数 + + Returns: + 预测参数字典 + """ + return self.prediction_settings.copy() + + def set_emotion_analysis_ranges(self, ranges: Dict[str, tuple]): + """ + 设置情绪分析范围 + + Args: + ranges: 范围字典 + """ + self.emotion_analysis.update(ranges) + print("✓ 情绪分析范围已更新") + + def get_emotion_analysis_ranges(self) -> Dict[str, tuple]: + """ + 获取情绪分析范围 + + Returns: + 范围字典 + """ + return self.emotion_analysis.copy() + + def get_analyzer_trend(self, analyzer_name: str) -> float: + """ + 获取分析器趋势 + + Args: + analyzer_name: 分析器名称 + + Returns: + 趋势值 + """ + if analyzer_name in self.analyzers: + return self.analyzers[analyzer_name].get('trend', 0.0) + return 0.0 + + def get_analyzer_confidence(self, analyzer_name: str) -> float: + """ + 获取分析器置信度 + + Args: + analyzer_name: 分析器名称 + + Returns: + 置信度值 + """ + if analyzer_name in self.analyzers: + return self.analyzers[analyzer_name].get('confidence', 1.0) + return 1.0 + + def get_overall_confidence(self) -> float: + """ + 获取整体置信度 + + Returns: + 整体置信度 + """ + if not self.analyzers: + return 1.0 + + total_confidence = sum(analyzer.get('confidence', 1.0) for analyzer in self.analyzers.values()) + return total_confidence / len(self.analyzers) + + def force_analysis(self) -> Dict[str, Any]: + """ + 强制执行分析(忽略时间间隔) + + Returns: + 分析结果 + """ + print("✓ 执行强制分析") + return self.perform_analysis() + + def get_analyzer_history(self, analyzer_name: str) -> List[Dict[str, Any]]: + """ + 获取分析器历史数据 + + Args: + analyzer_name: 分析器名称 + + Returns: + 历史数据列表 + """ + if analyzer_name in self.analyzers: + return list(self.analyzers[analyzer_name]['history']) + return [] + + def clear_analyzer_history(self, analyzer_name: str): + """ + 清空分析器历史数据 + + Args: + analyzer_name: 分析器名称 + """ + if analyzer_name in self.analyzers: + self.analyzers[analyzer_name]['history'].clear() + print(f"✓ 分析器 {analyzer_name} 历史数据已清空") + + def set_history_length(self, length: int): + """ + 设置历史数据长度 + + Args: + length: 历史数据长度 + """ + self.history_length = max(10, length) + # 更新所有历史队列的长度 + for key in self.history: + self.history[key] = deque(list(self.history[key])[-self.history_length:], maxlen=self.history_length) + print(f"✓ 历史数据长度设置为: {self.history_length}") + + def get_dominant_emotion(self) -> str: + """ + 获取主导情绪 + + Returns: + 主导情绪标签 + """ + try: + player_state = self.plugin.player_state if self.plugin else {} + emotional_state = player_state.get('emotional_state', 'neutral') + return emotional_state + except Exception as e: + print(f"✗ 获取主导情绪失败: {e}") + return 'neutral' + + def get_emotional_valence(self) -> float: + """ + 获取情绪效价(正负情绪) + + Returns: + 情绪效价值 (-1.0 到 1.0) + """ + try: + # 从玩家状态分析器获取效价 + player_results = self.analysis_results.get('details', {}).get('player_state', []) + if player_results: + for result, _ in player_results: + if isinstance(result, dict) and 'valence' in result: + return result['valence'] + return 0.0 + except Exception as e: + print(f"✗ 获取情绪效价失败: {e}") + return 0.0 + + def get_emotional_arousal(self) -> float: + """ + 获取情绪唤醒度(兴奋程度) + + Returns: + 情绪唤醒度值 (0.0 到 1.0) + """ + try: + # 从玩家状态分析器获取唤醒度 + player_results = self.analysis_results.get('details', {}).get('player_state', []) + if player_results: + for result, _ in player_results: + if isinstance(result, dict) and 'arousal' in result: + return result['arousal'] + return 0.4 # 默认中等唤醒度 + except Exception as e: + print(f"✗ 获取情绪唤醒度失败: {e}") + return 0.4 + + def get_narrative_tension(self) -> float: + """ + 获取叙事紧张度 + + Returns: + 叙事紧张度值 (0.0 到 1.0) + """ + try: + # 从叙事分析器获取紧张度 + narrative_results = self.analysis_results.get('details', {}).get('narrative', []) + if narrative_results: + for result, _ in narrative_results: + if isinstance(result, dict) and 'tension_level' in result: + return result['tension_level'] + return 0.0 + except Exception as e: + print(f"✗ 获取叙事紧张度失败: {e}") + return 0.0 + + def get_environmental_mood(self) -> float: + """ + 获取环境氛围 + + Returns: + 环境氛围值 (0.0 到 1.0) + """ + try: + # 从环境分析器获取氛围 + environment_results = self.analysis_results.get('details', {}).get('environment', []) + if environment_results: + for result, _ in environment_results: + if isinstance(result, dict) and 'mood' in result: + return result['mood'] + return 0.5 # 默认中性氛围 + except Exception as e: + print(f"✗ 获取环境氛围失败: {e}") + return 0.5 + + def get_combat_tension(self) -> float: + """ + 获取战斗紧张度 + + Returns: + 战斗紧张度值 (0.0 到 1.0) + """ + try: + # 从战斗分析器获取紧张度 + combat_results = self.analysis_results.get('details', {}).get('combat', []) + if combat_results: + for result, _ in combat_results: + if isinstance(result, dict) and 'tension' in result: + return result['tension'] + return 0.0 + except Exception as e: + print(f"✗ 获取战斗紧张度失败: {e}") + return 0.0 + + def get_exploration_curiosity(self) -> float: + """ + 获取探索好奇心 + + Returns: + 探索好奇心值 (0.0 到 1.0) + """ + try: + # 从探索分析器获取好奇心 + exploration_results = self.analysis_results.get('details', {}).get('exploration', []) + if exploration_results: + for result, _ in exploration_results: + if isinstance(result, dict) and 'curiosity' in result: + return result['curiosity'] + return 0.0 + except Exception as e: + print(f"✗ 获取探索好奇心失败: {e}") return 0.0 \ No newline at end of file diff --git a/plugins/user/dynamic_music_system/composition/composition_engine.py b/plugins/user/dynamic_music_system/composition/composition_engine.py index e5404bc0..7278209c 100644 --- a/plugins/user/dynamic_music_system/composition/composition_engine.py +++ b/plugins/user/dynamic_music_system/composition/composition_engine.py @@ -1,1581 +1,1581 @@ -""" -作曲引擎 -负责动态生成音乐内容 -""" - -import uuid -import math -import time -import numpy as np -from typing import Dict, List, Any, Optional, Callable -from scipy import signal -import threading -import queue - -class CompositionEngine: - """ - 作曲引擎 - 负责动态生成音乐内容 - """ - - def __init__(self, plugin): - """ - 初始化作曲引擎 - - Args: - plugin: 动态音乐系统插件实例 - """ - self.plugin = plugin - self.generators: Dict[str, Dict[str, Any]] = {} - self.active_generators: Dict[str, Dict[str, Any]] = {} - self.composed_segments: Dict[str, Dict[str, Any]] = {} - self.generator_threads: Dict[str, threading.Thread] = {} - self.generator_queues: Dict[str, queue.Queue] = {} - - self.stats = { - 'generators_created': 0, - 'generators_active': 0, - 'segments_composed': 0, - 'composition_time': 0.0, - 'memory_usage': 0, - 'failed_compositions': 0 - } - - self.sample_rate = 44100 - self.buffer_size = plugin.buffer_size if plugin else 4096 - self.master_key = 'C' - self.master_scale = 'major' - self.master_bpm = 120 - self.max_polyphony = 16 - - # 音乐理论数据 - self._initialize_music_theory() - - # 作曲参数 - self.composition_settings = { - 'max_segment_duration': 30.0, # 最大片段时长 - 'min_segment_duration': 5.0, # 最小片段时长 - 'segment_overlap': 0.5, # 片段重叠比例 - 'lookahead_time': 2.0, # 预生成时间 - 'complexity_range': (0.2, 0.8) # 复杂度范围 - } - - # 音色库 - self.instrument_presets = { - 'piano': {'attack': 0.01, 'decay': 0.1, 'sustain': 0.7, 'release': 0.3}, - 'strings': {'attack': 0.2, 'decay': 0.1, 'sustain': 0.8, 'release': 0.5}, - 'brass': {'attack': 0.05, 'decay': 0.1, 'sustain': 0.9, 'release': 0.2}, - 'woodwind': {'attack': 0.1, 'decay': 0.05, 'sustain': 0.6, 'release': 0.3}, - 'guitar': {'attack': 0.02, 'decay': 0.2, 'sustain': 0.5, 'release': 0.4}, - 'synth': {'attack': 0.005, 'decay': 0.05, 'sustain': 0.9, 'release': 0.1} - } - - # 和声进行数据库 - self.chord_progressions = { - 'major': [ - ['I', 'V', 'vi', 'IV'], # 流行进行 - ['I', 'vi', 'IV', 'V'], # 古典进行 - ['I', 'IV', 'V', 'I'], # 终止进行 - ['I', 'iii', 'IV', 'V'], # 扩展进行 - ['I', 'vi', 'ii', 'V'] # 2-5-1进行 - ], - 'minor': [ - ['i', 'V', 'VI', 'III'], # 小调流行进行 - ['i', 'bVII', 'VI', 'VII'], # 模态进行 - ['i', 'iv', 'V', 'i'], # 小调终止进行 - ['i', 'bVI', 'bIII', 'V'], # 情感进行 - ['i', 'bVII', 'bVI', 'V'] # 蓝调进行 - ] - } - - # 初始化作曲引擎 - self._initialize_composition_engine() - - def _initialize_music_theory(self): - """初始化音乐理论数据""" - # 音符频率表 (基于A4=440Hz) - self.note_frequencies = {} - a4_freq = 440.0 - for i in range(-60, 60): # 支持多个八度 - note_name = self._get_note_name(i) - self.note_frequencies[note_name] = a4_freq * (2 ** (i / 12)) - - # 音阶模式 - self.scale_patterns = { - 'major': [0, 2, 4, 5, 7, 9, 11], - 'minor': [0, 2, 3, 5, 7, 8, 10], - 'harmonic_minor': [0, 2, 3, 5, 7, 8, 11], - 'melodic_minor': [0, 2, 3, 5, 7, 9, 11], - 'dorian': [0, 2, 3, 5, 7, 9, 10], - 'phrygian': [0, 1, 3, 5, 7, 8, 10], - 'lydian': [0, 2, 4, 6, 7, 9, 11], - 'mixolydian': [0, 2, 4, 5, 7, 9, 10], - 'aeolian': [0, 2, 3, 5, 7, 8, 10], - 'locrian': [0, 1, 3, 5, 6, 8, 10] - } - - # 和弦模式 - self.chord_patterns = { - 'major': [0, 4, 7], - 'minor': [0, 3, 7], - 'diminished': [0, 3, 6], - 'augmented': [0, 4, 8], - 'sus2': [0, 2, 7], - 'sus4': [0, 5, 7], - '7th': [0, 4, 7, 10], - 'maj7': [0, 4, 7, 11], - 'min7': [0, 3, 7, 10], - 'dim7': [0, 3, 6, 9], - 'm7b5': [0, 3, 6, 10] - } - - # 罗马数字和弦映射 - self.roman_numerals = { - 'I': 0, 'II': 1, 'III': 2, 'IV': 3, 'V': 4, 'VI': 5, 'VII': 6, - 'i': 0, 'ii': 1, 'iii': 2, 'iv': 3, 'v': 4, 'vi': 5, 'vii': 6, - 'bII': 10, 'bIII': 11, 'bVI': 14, 'bVII': 15 # 用于小调 - } - - # 音符名称 - self.note_names = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] - - # 节拍模式 - self.rhythm_patterns = { - '4/4': [1.0, 0.5, 0.5, 1.0, 0.5, 0.5, 0.5, 0.5], # 基本4/4拍 - '3/4': [1.0, 0.5, 0.5], # 华尔兹 - '6/8': [1.5, 0.5, 0.5, 1.5, 0.5, 0.5], # 6/8拍 - 'syncopated': [0.5, 0.5, 1.0, 0.5, 0.5, 1.0], # 切分音 - 'swing': [1.33, 0.67, 1.33, 0.67] # 摇摆节奏 - } - - def _get_note_name(self, semitone_offset: int) -> str: - """ - 根据半音偏移量获取音符名称 - - Args: - semitone_offset: 相对于A4的半音偏移量 - - Returns: - 音符名称 - """ - note_index = (9 + semitone_offset) % 12 # A是第9个音符 - octave = 4 + (9 + semitone_offset) // 12 - return f"{self.note_names[note_index]}{octave}" - - def _initialize_composition_engine(self): - """初始化作曲引擎""" - print("✓ 作曲引擎初始化完成") - - def create_generator(self, generator_name: str, generator_params: Dict[str, Any]) -> bool: - """ - 创建音乐生成器 - - Args: - generator_name: 生成器名称 - generator_params: 生成器参数 - - Returns: - 是否创建成功 - """ - try: - # 设置默认参数 - default_params = { - 'type': 'melody', # melody, harmony, rhythm, bass - 'key': self.master_key, - 'scale': self.master_scale, - 'bpm': self.master_bpm, - 'octave': 4, - 'complexity': 0.5, # 0.0-1.0 - 'variation': 0.3, # 0.0-1.0 - 'length': 8, # 小节数 - 'instrument': 'piano', - 'arpeggiate': False, - 'syncopation': 0.0, # 0.0-1.0 - 'polyphony': 1, # 同时发声数 - 'mode': 'loop', # loop, once, random - 'tempo_sync': True, # 是否与主BPM同步 - 'dynamic_range': (0.6, 1.0) # 音量范围 - } - - # 合并参数 - params = default_params.copy() - params.update(generator_params) - - # 验证参数 - if params['type'] not in ['melody', 'harmony', 'rhythm', 'bass']: - print(f"✗ 无效的生成器类型: {params['type']}") - return False - - if params['scale'] not in self.scale_patterns: - print(f"✗ 无效的音阶: {params['scale']}") - return False - - if params['instrument'] not in self.instrument_presets: - print(f"⚠ 未知乐器: {params['instrument']},使用默认钢琴音色") - params['instrument'] = 'piano' - - # 验证复杂度和变化度范围 - params['complexity'] = max(0.0, min(1.0, params['complexity'])) - params['variation'] = max(0.0, min(1.0, params['variation'])) - params['syncopation'] = max(0.0, min(1.0, params['syncopation'])) - params['polyphony'] = max(1, min(self.max_polyphony, params['polyphony'])) - - # 创建生成器 - generator = { - 'name': generator_name, - 'params': params, - 'state': 'created', - 'segments': [], - 'current_segment': 0, - 'playback_position': 0.0, - 'last_generated_time': 0.0, - 'is_generating': False, - 'thread': None, - 'queue': queue.Queue() - } - - self.generators[generator_name] = generator - self.generator_queues[generator_name] = generator['queue'] - self.stats['generators_created'] += 1 - - print(f"✓ 音乐生成器创建成功: {generator_name}") - return True - - except Exception as e: - print(f"✗ 创建音乐生成器失败: {e}") - import traceback - traceback.print_exc() - return False - - def activate_generator(self, generator_name: str) -> bool: - """ - 激活音乐生成器 - - Args: - generator_name: 生成器名称 - - Returns: - 是否激活成功 - """ - try: - if generator_name not in self.generators: - print(f"✗ 音乐生成器不存在: {generator_name}") - return False - - generator = self.generators[generator_name] - - # 启动生成器线程 - if generator['thread'] is None or not generator['thread'].is_alive(): - generator['thread'] = threading.Thread( - target=self._generator_worker, - args=(generator_name,), - daemon=True - ) - generator['thread'].start() - - # 生成初始音乐片段 - if self._generate_segment(generator_name): - self.active_generators[generator_name] = self.generators[generator_name] - self.stats['generators_active'] = len(self.active_generators) - generator['state'] = 'active' - print(f"✓ 音乐生成器已激活: {generator_name}") - return True - else: - print(f"✗ 生成音乐片段失败: {generator_name}") - return False - - except Exception as e: - print(f"✗ 激活音乐生成器失败: {e}") - import traceback - traceback.print_exc() - return False - - def deactivate_generator(self, generator_name: str) -> bool: - """ - 停用音乐生成器 - - Args: - generator_name: 生成器名称 - - Returns: - 是否停用成功 - """ - try: - if generator_name not in self.active_generators: - print(f"⚠ 音乐生成器未激活: {generator_name}") - return True - - generator = self.generators[generator_name] - generator['state'] = 'inactive' - - del self.active_generators[generator_name] - self.stats['generators_active'] = len(self.active_generators) - - print(f"✓ 音乐生成器已停用: {generator_name}") - return True - - except Exception as e: - print(f"✗ 停用音乐生成器失败: {e}") - import traceback - traceback.print_exc() - return False - - def _generator_worker(self, generator_name: str): - """ - 生成器工作线程 - - Args: - generator_name: 生成器名称 - """ - try: - while generator_name in self.generators: - generator = self.generators[generator_name] - if generator['state'] == 'active': - # 检查是否需要生成新片段 - if self._should_generate_new_segment(generator): - self._generate_segment(generator_name) - - # 处理队列中的请求 - try: - request = generator['queue'].get(timeout=0.1) - if request['type'] == 'generate': - self._generate_segment(generator_name) - elif request['type'] == 'stop': - break - except queue.Empty: - pass - - time.sleep(0.01) # 10ms延迟 - - except Exception as e: - print(f"✗ 生成器工作线程错误: {e}") - import traceback - traceback.print_exc() - - def _generate_segment(self, generator_name: str) -> bool: - """ - 生成音乐片段 - - Args: - generator_name: 生成器名称 - - Returns: - 是否生成成功 - """ - try: - if generator_name not in self.generators: - return False - - generator = self.generators[generator_name] - params = generator['params'] - - # 检查是否正在生成 - if generator['is_generating']: - return True # 避免重复生成 - - generator['is_generating'] = True - - # 生成唯一片段ID - segment_id = f"{generator_name}_{int(time.time() * 1000000)}" - - # 根据生成器类型生成音乐数据 - if params['type'] == 'melody': - segment_data = self._generate_melody_segment(params) - elif params['type'] == 'harmony': - segment_data = self._generate_harmony_segment(params) - elif params['type'] == 'rhythm': - segment_data = self._generate_rhythm_segment(params) - elif params['type'] == 'bass': - segment_data = self._generate_bass_segment(params) - else: - segment_data = self._generate_melody_segment(params) # 默认生成旋律 - - # 创建片段 - segment = { - 'id': segment_id, - 'generator': generator_name, - 'data': segment_data, - 'created_time': time.time(), - 'duration': params['length'] * (60.0 / params['bpm']) * 4, # 假设4/4拍 - 'bpm': params['bpm'], - 'key': params['key'], - 'scale': params['scale'], - 'type': params['type'] - } - - # 存储片段 - self.composed_segments[segment_id] = segment - generator['segments'].append(segment_id) - - # 限制片段数量 - if len(generator['segments']) > 10: # 最多保留10个片段 - old_segment_id = generator['segments'].pop(0) - if old_segment_id in self.composed_segments: - del self.composed_segments[old_segment_id] - - self.stats['segments_composed'] += 1 - generator['last_generated_time'] = time.time() - generator['is_generating'] = False - - print(f"✓ 音乐片段生成成功: {segment_id}") - return True - - except Exception as e: - print(f"✗ 生成音乐片段失败: {e}") - import traceback - traceback.print_exc() - if generator_name in self.generators: - self.generators[generator_name]['is_generating'] = False - self.stats['failed_compositions'] += 1 - return False - - def _generate_melody_segment(self, params: Dict[str, Any]) -> Dict[str, Any]: - """ - 生成旋律片段 - - Args: - params: 生成参数 - - Returns: - 旋律片段数据 - """ - try: - # 获取音阶音符 - scale_notes = self._get_scale_notes(params['key'], params['scale'], params['octave']) - - # 计算片段长度(以拍为单位) - beats_per_measure = 4 # 假设4/4拍 - total_beats = params['length'] * beats_per_measure - - # 选择节拍模式 - time_signature = '4/4' - if params['syncopation'] > 0.7: - rhythm_pattern = self.rhythm_patterns['syncopated'] - elif params['syncopation'] > 0.4: - rhythm_pattern = self.rhythm_patterns['swing'] - else: - rhythm_pattern = self.rhythm_patterns[time_signature] - - # 生成音符序列 - notes = [] - current_time = 0.0 - beat_index = 0 - - while current_time < total_beats: - # 选择音符 - note_index = self._choose_note(scale_notes, params['complexity'], params['variation']) - note_name = scale_notes[note_index] - note_freq = self.note_frequencies.get(note_name, 440.0) - - # 选择时值 - duration = rhythm_pattern[beat_index % len(rhythm_pattern)] - - # 应用复杂度调整时值 - if params['complexity'] > 0.7: - # 增加装饰音和复杂节奏 - if np.random.random() < 0.3: - duration *= 0.5 # 八分音符 - # 添加装饰音 - if note_index > 0: - grace_note_index = max(0, note_index - 1) - grace_note_name = scale_notes[grace_note_index] - grace_note_freq = self.note_frequencies.get(grace_note_name, 440.0) - notes.append({ - 'frequency': grace_note_freq, - 'duration': duration * 0.3, - 'start_time': current_time, - 'velocity': 0.4 + np.random.random() * 0.2, - 'type': 'grace' - }) - current_time += duration * 0.3 - - # 添加主要音符 - velocity_range = params.get('dynamic_range', (0.6, 1.0)) - velocity = velocity_range[0] + np.random.random() * (velocity_range[1] - velocity_range[0]) - - notes.append({ - 'frequency': note_freq, - 'duration': duration, - 'start_time': current_time, - 'velocity': velocity, - 'type': 'main' - }) - - current_time += duration - beat_index += 1 - - # 避免无限循环 - if beat_index > total_beats * 4: # 限制最多4倍拍数 - break - - # 应用琶音效果 - if params['arpeggiate']: - notes = self._apply_arpeggiation(notes, params['syncopation']) - - return { - 'type': 'melody', - 'notes': notes, - 'bpm': params['bpm'], - 'key': params['key'], - 'scale': params['scale'], - 'instrument': params['instrument'] - } - - except Exception as e: - print(f"✗ 生成旋律片段失败: {e}") - import traceback - traceback.print_exc() - return { - 'type': 'melody', - 'notes': [], - 'bpm': params['bpm'], - 'key': params['key'], - 'scale': params['scale'], - 'instrument': params['instrument'] - } - - def _generate_harmony_segment(self, params: Dict[str, Any]) -> Dict[str, Any]: - """ - 生成和声片段 - - Args: - params: 生成参数 - - Returns: - 和声片段数据 - """ - try: - # 选择和弦进行 - progressions = self.chord_progressions.get(params['scale'], self.chord_progressions['major']) - chord_progression = np.random.choice(progressions) - - # 计算片段长度 - beats_per_measure = 4 - total_measures = params['length'] - - # 生成和弦序列 - chords = [] - current_time = 0.0 - measure_duration = beats_per_measure * (60.0 / params['bpm']) - - for measure in range(total_measures): - # 获取和弦 - chord_index = measure % len(chord_progression) - roman_numeral = chord_progression[chord_index] - - # 转换罗马数字为和弦 - scale_degree = self.roman_numerals.get(roman_numeral, 0) - chord_type = 'major' if roman_numeral.isupper() else 'minor' - - # 特殊处理小调中的降号和弦 - if roman_numeral.startswith('b'): - scale_degree = self.roman_numerals.get(roman_numeral, scale_degree) - if 'VII' in roman_numeral: - chord_type = 'major' - elif 'VI' in roman_numeral or 'III' in roman_numeral: - chord_type = 'major' - - # 获取和弦音符 - scale_notes = self._get_scale_notes(params['key'], params['scale'], params['octave'] - 1) - chord_root_index = scale_degree % len(scale_notes) - chord_root = scale_notes[chord_root_index] - chord_notes = self._get_chord_notes(chord_root, chord_type, params['octave'] - 1) - - # 计算和弦频率 - frequencies = [self.note_frequencies.get(note, 440.0) for note in chord_notes] - - # 应用琶音效果 - arpeggiated = params['arpeggiate'] and np.random.random() < 0.5 - if arpeggiated: - # 生成琶音音符 - arp_notes = [] - arp_duration = measure_duration / len(chord_notes) - for i, freq in enumerate(frequencies): - arp_notes.append({ - 'frequency': freq, - 'duration': arp_duration, - 'start_time': current_time + i * arp_duration, - 'velocity': 0.5 + np.random.random() * 0.3, - 'type': 'arpeggio' - }) - chords.extend(arp_notes) - else: - # 生成和弦 - velocity_range = params.get('dynamic_range', (0.5, 0.9)) - velocity = velocity_range[0] + np.random.random() * (velocity_range[1] - velocity_range[0]) - - chords.append({ - 'notes': chord_notes, - 'frequencies': frequencies, - 'duration': measure_duration, - 'start_time': current_time, - 'velocity': velocity, - 'type': 'chord' - }) - - current_time += measure_duration - - return { - 'type': 'harmony', - 'chords': chords, - 'progression': chord_progression, - 'bpm': params['bpm'], - 'key': params['key'], - 'scale': params['scale'], - 'instrument': params['instrument'] - } - - except Exception as e: - print(f"✗ 生成和声片段失败: {e}") - import traceback - traceback.print_exc() - return { - 'type': 'harmony', - 'chords': [], - 'progression': [], - 'bpm': params['bpm'], - 'key': params['key'], - 'scale': params['scale'], - 'instrument': params['instrument'] - } - - def _generate_rhythm_segment(self, params: Dict[str, Any]) -> Dict[str, Any]: - """ - 生成节奏片段 - - Args: - params: 生成参数 - - Returns: - 节奏片段数据 - """ - try: - # 生成鼓组节奏 - drums = ['kick', 'snare', 'hihat', 'crash', 'tom_low', 'tom_mid', 'tom_high'] - drum_frequencies = { - 'kick': 60.0, - 'snare': 200.0, - 'hihat': 8000.0, - 'crash': 4000.0, - 'tom_low': 120.0, - 'tom_mid': 250.0, - 'tom_high': 500.0 - } - - beats_per_measure = 4 - total_beats = params['length'] * beats_per_measure - - hits = [] - current_time = 0.0 - beat_duration = 60.0 / params['bpm'] - - # 基本节奏模式 - basic_pattern = { - 'kick': [0, 2], # 强拍 - 'snare': [1, 3], # 弱拍 - 'hihat': [0, 1, 2, 3] # 每拍 - } - - # 根据复杂度调整模式 - if params['complexity'] > 0.7: - # 复杂节奏 - basic_pattern['kick'] = [0, 0.5, 2, 2.5, 3.5] - basic_pattern['snare'] = [1, 3] - basic_pattern['hihat'] = [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5] - basic_pattern['tom_low'] = [1.5] - basic_pattern['tom_mid'] = [2.5] - basic_pattern['tom_high'] = [3.5] - elif params['complexity'] > 0.4: - # 中等复杂度 - basic_pattern['kick'] = [0, 2] - basic_pattern['snare'] = [1, 3] - basic_pattern['hihat'] = [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5] - - # 生成节奏 - for measure in range(params['length']): - measure_start = measure * beats_per_measure - - for drum_type, pattern in basic_pattern.items(): - if drum_type in drum_frequencies: - for beat_offset in pattern: - hit_time = (measure_start + beat_offset) * beat_duration - velocity_range = params.get('dynamic_range', (0.6, 1.0)) - velocity = velocity_range[0] + np.random.random() * (velocity_range[1] - velocity_range[0]) - - # 应用切分音效果 - if params['syncopation'] > 0.5 and np.random.random() < 0.3: - # 偶尔偏移时间 - hit_time += (np.random.random() - 0.5) * beat_duration * 0.2 - - hits.append({ - 'drum': drum_type, - 'frequency': drum_frequencies[drum_type], - 'time': hit_time, - 'velocity': velocity, - 'duration': beat_duration * 0.1 # 短促的打击乐 - }) - - # 添加填充(fills) - if np.random.random() < 0.3: # 30%概率添加填充 - fill_start = (params['length'] - 1) * beats_per_measure * beat_duration - for i in range(4): # 4个填充音符 - drum_type = np.random.choice(['snare', 'tom_low', 'tom_mid', 'tom_high']) - hit_time = fill_start + i * beat_duration * 0.25 - hits.append({ - 'drum': drum_type, - 'frequency': drum_frequencies[drum_type], - 'time': hit_time, - 'velocity': 0.8 + np.random.random() * 0.2, - 'duration': beat_duration * 0.05 - }) - - return { - 'type': 'rhythm', - 'hits': hits, - 'bpm': params['bpm'], - 'pattern_complexity': params['complexity'] - } - - except Exception as e: - print(f"✗ 生成节奏片段失败: {e}") - import traceback - traceback.print_exc() - return { - 'type': 'rhythm', - 'hits': [], - 'bpm': params['bpm'], - 'pattern_complexity': params['complexity'] - } - - def _generate_bass_segment(self, params: Dict[str, Any]) -> Dict[str, Any]: - """ - 生成贝斯片段 - - Args: - params: 生成参数 - - Returns: - 贝斯片段数据 - """ - try: - # 获取低音音阶音符 - scale_notes = self._get_scale_notes(params['key'], params['scale'], params['octave'] - 1) - - # 计算片段长度 - beats_per_measure = 4 - total_beats = params['length'] * beats_per_measure - beat_duration = 60.0 / params['bpm'] - - # 生成贝斯线 - notes = [] - current_time = 0.0 - beat_count = 0 - - # 使用和弦进行作为贝斯线基础 - progressions = self.chord_progressions.get(params['scale'], self.chord_progressions['major']) - chord_progression = np.random.choice(progressions) - - while current_time < total_beats * beat_duration and beat_count < total_beats: - # 确定当前小节和和弦 - measure = beat_count // beats_per_measure - chord_index = measure % len(chord_progression) - roman_numeral = chord_progression[chord_index] - - # 获取和弦根音作为贝斯音符 - scale_degree = self.roman_numerals.get(roman_numeral, 0) - bass_note_index = scale_degree % len(scale_notes) - bass_note = scale_notes[bass_note_index] - bass_freq = self.note_frequencies.get(bass_note, 65.41) # C2 - - # 确定音符时值 - if np.random.random() < params['complexity'] * 0.5: - # 复杂节奏:使用八分音符 - note_duration = beat_duration * 0.5 - subdivisions = 2 - else: - # 简单节奏:使用四分音符 - note_duration = beat_duration - subdivisions = 1 - - # 生成音符 - for sub in range(subdivisions): - if beat_count + sub * 0.5 < total_beats: - note_time = current_time + sub * note_duration - - # 应用滑音和装饰 - if params['complexity'] > 0.6 and np.random.random() < 0.2: - # 添加经过音 - if bass_note_index < len(scale_notes) - 1: - passing_note_index = bass_note_index + 1 - passing_note = scale_notes[passing_note_index] - passing_freq = self.note_frequencies.get(passing_note, 65.41) - - notes.append({ - 'frequency': passing_freq, - 'duration': note_duration * 0.3, - 'start_time': note_time, - 'velocity': 0.4 + np.random.random() * 0.2, - 'type': 'passing' - }) - note_time += note_duration * 0.3 - - # 主要贝斯音符 - velocity_range = params.get('dynamic_range', (0.7, 1.0)) - velocity = velocity_range[0] + np.random.random() * (velocity_range[1] - velocity_range[0]) - - notes.append({ - 'frequency': bass_freq, - 'duration': note_duration * (1 - 0.3 * (sub > 0)), # 经过音后缩短 - 'start_time': note_time, - 'velocity': velocity, - 'type': 'bass' - }) - - current_time += beat_duration - beat_count += 1 - - return { - 'type': 'bass', - 'notes': notes, - 'bpm': params['bpm'], - 'key': params['key'], - 'scale': params['scale'], - 'instrument': params['instrument'], - 'progression': chord_progression - } - - except Exception as e: - print(f"✗ 生成贝斯片段失败: {e}") - import traceback - traceback.print_exc() - return { - 'type': 'bass', - 'notes': [], - 'bpm': params['bpm'], - 'key': params['key'], - 'scale': params['scale'], - 'instrument': params['instrument'], - 'progression': [] - } - - def _get_scale_notes(self, key: str, scale: str, octave: int) -> List[str]: - """ - 获取音阶中的音符 - - Args: - key: 调号 - scale: 音阶类型 - octave: 八度 - - Returns: - 音符列表 - """ - try: - # 找到调号在音符列表中的索引 - key_index = self.note_names.index(key.upper()) - - # 获取音阶模式 - pattern = self.scale_patterns[scale] - - # 生成音符 - notes = [] - for semitone in pattern: - note_index = (key_index + semitone) % 12 - note_octave = octave + (key_index + semitone) // 12 - note_name = f"{self.note_names[note_index]}{note_octave}" - notes.append(note_name) - - return notes - - except Exception as e: - print(f"✗ 获取音阶音符失败: {e}") - # 返回默认C大调音阶 - return [f"{note}{octave}" for note in ['C', 'D', 'E', 'F', 'G', 'A', 'B']] - - def _get_chord_notes(self, root: str, chord_type: str, octave: int) -> List[str]: - """ - 获取和弦音符 - - Args: - root: 根音 - chord_type: 和弦类型 - octave: 八度 - - Returns: - 和弦音符列表 - """ - try: - # 找到根音在音符列表中的索引 - root_index = self.note_names.index(root[:-1]) # 去掉八度数字 - root_octave = int(root[-1]) - - # 获取和弦模式 - pattern = self.chord_patterns.get(chord_type, self.chord_patterns['major']) - - # 生成和弦音符 - notes = [] - for i, semitone in enumerate(pattern): - note_index = (root_index + semitone) % 12 - note_octave = root_octave + (root_index + semitone) // 12 - # 保证和弦音符在合理的八度范围内 - if i > 0 and note_octave <= root_octave: - note_octave += 1 - note_name = f"{self.note_names[note_index]}{note_octave}" - notes.append(note_name) - - return notes - - except Exception as e: - print(f"✗ 获取和弦音符失败: {e}") - # 返回默认三和弦 - return [root, f"{root[:-1]}{int(root[-1])+1}", f"{root[:-1]}{int(root[-1])+2}"] - - def _choose_note(self, scale_notes: List[str], complexity: float, variation: float) -> int: - """ - 选择音符 - - Args: - scale_notes: 音阶音符列表 - complexity: 复杂度 (0.0-1.0) - variation: 变化度 (0.0-1.0) - - Returns: - 音符索引 - """ - # 基于复杂度和变化度选择音符 - if np.random.random() < complexity: - # 复杂选择:可以跳到任何音符 - return np.random.randint(0, len(scale_notes)) - else: - # 简单选择:倾向于选择相邻音符 - if len(scale_notes) > 1: - current_index = np.random.randint(0, len(scale_notes)) - if np.random.random() < 0.7: # 70%概率选择相邻音符 - offset = np.random.choice([-1, 1]) - new_index = current_index + offset - if 0 <= new_index < len(scale_notes): - return new_index - return current_index - else: - return 0 - - def _apply_arpeggiation(self, notes: List[Dict[str, Any]], syncopation: float) -> List[Dict[str, Any]]: - """ - 应用琶音效果 - - Args: - notes: 音符列表 - syncopation: 切分音程度 - - Returns: - 处理后的音符列表 - """ - try: - arpeggiated_notes = [] - for note in notes: - if note['type'] == 'chord': - # 将和弦分解为琶音 - frequencies = note.get('frequencies', []) - duration = note['duration'] / len(frequencies) - for i, freq in enumerate(frequencies): - arpeggiated_notes.append({ - 'frequency': freq, - 'duration': duration, - 'start_time': note['start_time'] + i * duration, - 'velocity': note['velocity'] * (0.8 + 0.2 * (i % 2)), - 'type': 'arpeggio' - }) - else: - arpeggiated_notes.append(note) - return arpeggiated_notes - except Exception as e: - print(f"✗ 应用琶音效果失败: {e}") - return notes - - def update(self, dt: float): - """ - 更新作曲引擎状态 - - Args: - dt: 时间增量 - """ - update_start_time = time.time() - - # 更新活动生成器 - for generator_name, generator in self.active_generators.items(): - # 更新播放位置 - generator['playback_position'] += dt - - # 检查是否需要生成新片段 - if self._should_generate_new_segment(generator): - # 异步请求生成新片段 - if generator['queue']: - try: - generator['queue'].put({'type': 'generate'}, block=False) - except queue.Full: - pass # 队列满时跳过 - - # 更新待处理生成器请求 - self._process_generator_queues() - - # 更新性能统计 - self.stats['composition_time'] += (time.time() - update_start_time) - - def _should_generate_new_segment(self, generator: Dict[str, Any]) -> bool: - """ - 判断是否需要生成新片段 - - Args: - generator: 生成器数据 - - Returns: - 是否需要生成 - """ - try: - # 检查生成器状态 - if generator['state'] != 'active': - return False - - # 检查是否正在生成 - if generator['is_generating']: - return False - - # 检查时间条件 - current_time = time.time() - time_since_last_generation = current_time - generator['last_generated_time'] - - # 如果没有片段或距离上次生成时间较长,则需要生成 - if len(generator['segments']) < 2 or time_since_last_generation > 5.0: - return True - - return False - except Exception as e: - print(f"✗ 检查生成条件失败: {e}") - return False - - def _process_generator_queues(self): - """处理生成器队列""" - # 这里可以处理一些队列相关的逻辑 - pass - - def cleanup(self): - """清理所有资源""" - # 停止所有生成器线程 - for generator_name, generator in self.generators.items(): - if generator['queue']: - try: - generator['queue'].put({'type': 'stop'}, block=False) - except queue.Full: - pass - - # 清理数据结构 - self.generators.clear() - self.active_generators.clear() - self.composed_segments.clear() - self.generator_threads.clear() - self.generator_queues.clear() - - self.stats = { - 'generators_created': 0, - 'generators_active': 0, - 'segments_composed': 0, - 'composition_time': 0.0, - 'memory_usage': 0, - 'failed_compositions': 0 - } - - print("✓ 作曲引擎资源已清理") - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def get_active_generators(self) -> List[str]: - """ - 获取活动生成器列表 - - Returns: - 活动生成器名称列表 - """ - return list(self.active_generators.keys()) - - def get_generator_status(self, generator_name: str) -> Dict[str, Any]: - """ - 获取生成器状态 - - Args: - generator_name: 生成器名称 - - Returns: - 生成器状态字典 - """ - if generator_name in self.active_generators: - generator = self.active_generators[generator_name] - return { - 'name': generator_name, - 'state': generator['state'], - 'type': generator['params']['type'], - 'segments': len(generator['segments']), - 'active': True, - 'playback_position': generator['playback_position'] - } - elif generator_name in self.generators: - generator = self.generators[generator_name] - return { - 'name': generator_name, - 'state': generator['state'], - 'type': generator['params']['type'], - 'segments': len(generator['segments']), - 'active': False, - 'playback_position': generator['playback_position'] - } - else: - return { - 'name': generator_name, - 'state': 'unknown' - } - - def set_master_key(self, key: str): - """ - 设置主调 - - Args: - key: 调号 - """ - if key.upper() in self.note_names: - self.master_key = key.upper() - print(f"✓ 主调设置为: {self.master_key}") - else: - print(f"✗ 无效的调号: {key}") - - def set_master_scale(self, scale: str): - """ - 设置主音阶 - - Args: - scale: 音阶类型 - """ - if scale in self.scale_patterns: - self.master_scale = scale - print(f"✓ 主音阶设置为: {self.master_scale}") - else: - print(f"✗ 无效的音阶: {scale}") - - def set_master_bpm(self, bpm: float): - """ - 设置主BPM - - Args: - bpm: 每分钟节拍数 - """ - self.master_bpm = max(30, min(300, bpm)) # 限制在合理范围内 - print(f"✓ 主BPM设置为: {self.master_bpm}") - - def generate_theme(self, theme_name: str, theme_params: Dict[str, Any]) -> bool: - """ - 生成完整音乐主题 - - Args: - theme_name: 主题名称 - theme_params: 主题参数 - - Returns: - 是否生成成功 - """ - try: - # 创建多个生成器来组成完整主题 - generators_to_create = theme_params.get('generators', ['melody', 'harmony', 'rhythm']) - - for generator_type in generators_to_create: - generator_name = f"{theme_name}_{generator_type}" - generator_params = { - 'type': generator_type, - 'key': theme_params.get('key', self.master_key), - 'scale': theme_params.get('scale', self.master_scale), - 'bpm': theme_params.get('bpm', self.master_bpm), - 'length': theme_params.get('length', 8), - 'complexity': theme_params.get('complexity', 0.5), - 'variation': theme_params.get('variation', 0.3), - 'instrument': theme_params.get('instrument_' + generator_type, 'piano'), - 'arpeggiate': theme_params.get('arpeggiate', False), - 'syncopation': theme_params.get('syncopation', 0.0) - } - - self.create_generator(generator_name, generator_params) - - print(f"✓ 音乐主题生成成功: {theme_name}") - return True - - except Exception as e: - print(f"✗ 生成音乐主题失败: {e}") - import traceback - traceback.print_exc() - return False - - def get_segment_data(self, segment_id: str) -> Dict[str, Any]: - """ - 获取片段数据 - - Args: - segment_id: 片段ID - - Returns: - 片段数据 - """ - return self.composed_segments.get(segment_id, {}).get('data', {}) - - def set_sample_rate(self, sample_rate: int): - """ - 设置采样率 - - Args: - sample_rate: 采样率 (Hz) - """ - self.sample_rate = sample_rate - print(f"✓ 作曲引擎采样率设置为: {self.sample_rate} Hz") - - def create_adaptive_generator(self, generator_name: str, - game_state_callback: Callable[[], Dict[str, Any]]) -> bool: - """ - 创建自适应音乐生成器 - - Args: - generator_name: 生成器名称 - game_state_callback: 游戏状态回调函数 - - Returns: - 是否创建成功 - """ - try: - # 创建自适应生成器参数 - adaptive_params = { - 'type': 'melody', - 'key': self.master_key, - 'scale': self.master_scale, - 'bpm': self.master_bpm, - 'octave': 4, - 'complexity': 0.5, - 'variation': 0.3, - 'length': 8, - 'instrument': 'piano', - 'arpeggiate': False, - 'syncopation': 0.0, - 'adaptive': True, - 'game_state_callback': game_state_callback - } - - # 创建生成器 - generator = { - 'name': generator_name, - 'params': adaptive_params, - 'state': 'created', - 'segments': [], - 'current_segment': 0, - 'playback_position': 0.0, - 'last_generated_time': 0.0, - 'is_generating': False, - 'thread': None, - 'queue': queue.Queue(), - 'adaptive_callback': game_state_callback - } - - self.generators[generator_name] = generator - self.generator_queues[generator_name] = generator['queue'] - self.stats['generators_created'] += 1 - - print(f"✓ 自适应音乐生成器创建成功: {generator_name}") - return True - - except Exception as e: - print(f"✗ 创建自适应音乐生成器失败: {e}") - import traceback - traceback.print_exc() - return False - - def set_generator_parameter(self, generator_name: str, param_name: str, param_value: Any) -> bool: - """ - 设置生成器参数 - - Args: - generator_name: 生成器名称 - param_name: 参数名称 - param_value: 参数值 - - Returns: - 是否设置成功 - """ - try: - if generator_name not in self.generators: - print(f"✗ 生成器不存在: {generator_name}") - return False - - generator = self.generators[generator_name] - - # 更新参数 - generator['params'][param_name] = param_value - print(f"✓ 生成器 {generator_name} 参数 {param_name} 设置为: {param_value}") - return True - - except Exception as e: - print(f"✗ 设置生成器参数失败: {e}") - import traceback - traceback.print_exc() - return False - - def get_generator_parameters(self, generator_name: str) -> Dict[str, Any]: - """ - 获取生成器参数 - - Args: - generator_name: 生成器名称 - - Returns: - 参数字典 - """ - if generator_name in self.generators: - return self.generators[generator_name]['params'].copy() - return {} - - def regenerate_segment(self, generator_name: str) -> bool: - """ - 重新生成片段 - - Args: - generator_name: 生成器名称 - - Returns: - 是否重新生成成功 - """ - try: - if generator_name not in self.generators: - print(f"✗ 生成器不存在: {generator_name}") - return False - - return self._generate_segment(generator_name) - - except Exception as e: - print(f"✗ 重新生成片段失败: {e}") - import traceback - traceback.print_exc() - return False - - def set_instrument_preset(self, instrument_name: str, preset: Dict[str, float]): - """ - 设置乐器预设 - - Args: - instrument_name: 乐器名称 - preset: 预设参数字典 (attack, decay, sustain, release) - """ - required_params = ['attack', 'decay', 'sustain', 'release'] - for param in required_params: - if param not in preset: - print(f"✗ 乐器预设缺少必需参数: {param}") - return - - self.instrument_presets[instrument_name] = preset.copy() - print(f"✓ 乐器预设 {instrument_name} 已更新") - - def get_instrument_preset(self, instrument_name: str) -> Dict[str, float]: - """ - 获取乐器预设 - - Args: - instrument_name: 乐器名称 - - Returns: - 预设参数字典 - """ - return self.instrument_presets.get(instrument_name, self.instrument_presets['piano']).copy() - - def set_composition_setting(self, setting_name: str, value: Any): - """ - 设置作曲参数 - - Args: - setting_name: 参数名称 - value: 参数值 - """ - self.composition_settings[setting_name] = value - print(f"✓ 作曲参数 {setting_name} 设置为: {value}") - - def get_composition_setting(self, setting_name: str) -> Any: - """ - 获取作曲参数 - - Args: - setting_name: 参数名称 - - Returns: - 参数值 - """ - return self.composition_settings.get(setting_name, None) - - def generate_audio_buffer(self, generator_name: str, buffer_size: int = None) -> np.ndarray: - """ - 生成音频缓冲区数据 - - Args: - generator_name: 生成器名称 - buffer_size: 缓冲区大小 - - Returns: - 音频数据数组 - """ - try: - if buffer_size is None: - buffer_size = self.buffer_size - - if generator_name not in self.active_generators: - return np.zeros(buffer_size) - - generator = self.active_generators[generator_name] - if not generator['segments']: - return np.zeros(buffer_size) - - # 获取当前片段 - current_segment_id = generator['segments'][generator['current_segment'] % len(generator['segments'])] - segment_data = self.get_segment_data(current_segment_id) - - # 生成音频数据 - audio_buffer = np.zeros(buffer_size) - t = np.linspace(0, buffer_size / self.sample_rate, buffer_size, False) - - if segment_data.get('type') == 'melody': - notes = segment_data.get('notes', []) - for note in notes: - freq = note.get('frequency', 440.0) - duration = note.get('duration', 1.0) - start_time = note.get('start_time', 0.0) - velocity = note.get('velocity', 0.7) - - # 计算样本索引 - start_sample = int(start_time * self.sample_rate) - end_sample = min(buffer_size, int((start_time + duration) * self.sample_rate)) - - if start_sample < buffer_size and end_sample > start_sample: - # 生成正弦波 - note_samples = end_sample - start_sample - note_t = np.linspace(0, duration, note_samples, False) - wave = velocity * np.sin(2 * np.pi * freq * note_t) - - # 应用包络 - envelope = self._create_envelope(note_samples, 'piano') - wave *= envelope - - audio_buffer[start_sample:end_sample] += wave - - elif segment_data.get('type') == 'rhythm': - hits = segment_data.get('hits', []) - for hit in hits: - freq = hit.get('frequency', 200.0) - hit_time = hit.get('time', 0.0) - duration = hit.get('duration', 0.1) - velocity = hit.get('velocity', 0.8) - - # 计算样本索引 - start_sample = int(hit_time * self.sample_rate) - end_sample = min(buffer_size, int((hit_time + duration) * self.sample_rate)) - - if start_sample < buffer_size and end_sample > start_sample: - # 生成打击乐声音 - hit_samples = end_sample - start_sample - hit_t = np.linspace(0, duration, hit_samples, False) - - # 生成噪声为基础的打击乐 - noise = np.random.random(hit_samples) * 2 - 1 - # 应用滤波器模拟不同打击乐器 - b, a = signal.butter(4, freq / (self.sample_rate / 2), 'low') - filtered_noise = signal.lfilter(b, a, noise) - - # 应用包络 - envelope = self._create_envelope(hit_samples, 'percussion') - wave = velocity * filtered_noise * envelope - - audio_buffer[start_sample:end_sample] += wave - - # 限制幅度防止削波 - audio_buffer = np.clip(audio_buffer, -1.0, 1.0) - - return audio_buffer - - except Exception as e: - print(f"✗ 生成音频缓冲区失败: {e}") - import traceback - traceback.print_exc() - return np.zeros(buffer_size if buffer_size else self.buffer_size) - - def _create_envelope(self, sample_count: int, instrument_type: str) -> np.ndarray: - """ - 创建包络 - - Args: - sample_count: 样本数量 - instrument_type: 乐器类型 - - Returns: - 包络数组 - """ - try: - preset = self.instrument_presets.get(instrument_type, self.instrument_presets['piano']) - - attack_samples = int(preset['attack'] * self.sample_rate) - decay_samples = int(preset['decay'] * self.sample_rate) - release_samples = int(preset['release'] * self.sample_rate) - sustain_level = preset['sustain'] - - envelope = np.ones(sample_count) - - # 攻击阶段 - if attack_samples > 0 and attack_samples < sample_count: - envelope[:attack_samples] = np.linspace(0, 1, attack_samples) - - # 衰减阶段 - decay_start = attack_samples - decay_end = attack_samples + decay_samples - if decay_end < sample_count: - envelope[decay_start:decay_end] = np.linspace(1, sustain_level, decay_samples) - elif decay_start < sample_count: - envelope[decay_start:] = np.linspace(1, sustain_level, sample_count - decay_start) - - # 释放阶段(如果空间足够) - release_start = max(0, sample_count - release_samples) - if release_start < sample_count: - envelope[release_start:] = np.linspace(sustain_level, 0, sample_count - release_start) - - return envelope - - except Exception as e: - print(f"✗ 创建包络失败: {e}") - return np.ones(sample_count) - - def set_max_polyphony(self, max_voices: int): - """ - 设置最大复调数 - - Args: - max_voices: 最大同时发声数 - """ - self.max_polyphony = max(1, max_voices) - print(f"✓ 最大复调数设置为: {self.max_polyphony}") - - def get_active_segment(self, generator_name: str) -> str: - """ - 获取生成器当前活动片段 - - Args: - generator_name: 生成器名称 - - Returns: - 片段ID - """ - if generator_name in self.active_generators: - generator = self.active_generators[generator_name] - if generator['segments']: - return generator['segments'][generator['current_segment'] % len(generator['segments'])] +""" +作曲引擎 +负责动态生成音乐内容 +""" + +import uuid +import math +import time +import numpy as np +from typing import Dict, List, Any, Optional, Callable +from scipy import signal +import threading +import queue + +class CompositionEngine: + """ + 作曲引擎 + 负责动态生成音乐内容 + """ + + def __init__(self, plugin): + """ + 初始化作曲引擎 + + Args: + plugin: 动态音乐系统插件实例 + """ + self.plugin = plugin + self.generators: Dict[str, Dict[str, Any]] = {} + self.active_generators: Dict[str, Dict[str, Any]] = {} + self.composed_segments: Dict[str, Dict[str, Any]] = {} + self.generator_threads: Dict[str, threading.Thread] = {} + self.generator_queues: Dict[str, queue.Queue] = {} + + self.stats = { + 'generators_created': 0, + 'generators_active': 0, + 'segments_composed': 0, + 'composition_time': 0.0, + 'memory_usage': 0, + 'failed_compositions': 0 + } + + self.sample_rate = 44100 + self.buffer_size = plugin.buffer_size if plugin else 4096 + self.master_key = 'C' + self.master_scale = 'major' + self.master_bpm = 120 + self.max_polyphony = 16 + + # 音乐理论数据 + self._initialize_music_theory() + + # 作曲参数 + self.composition_settings = { + 'max_segment_duration': 30.0, # 最大片段时长 + 'min_segment_duration': 5.0, # 最小片段时长 + 'segment_overlap': 0.5, # 片段重叠比例 + 'lookahead_time': 2.0, # 预生成时间 + 'complexity_range': (0.2, 0.8) # 复杂度范围 + } + + # 音色库 + self.instrument_presets = { + 'piano': {'attack': 0.01, 'decay': 0.1, 'sustain': 0.7, 'release': 0.3}, + 'strings': {'attack': 0.2, 'decay': 0.1, 'sustain': 0.8, 'release': 0.5}, + 'brass': {'attack': 0.05, 'decay': 0.1, 'sustain': 0.9, 'release': 0.2}, + 'woodwind': {'attack': 0.1, 'decay': 0.05, 'sustain': 0.6, 'release': 0.3}, + 'guitar': {'attack': 0.02, 'decay': 0.2, 'sustain': 0.5, 'release': 0.4}, + 'synth': {'attack': 0.005, 'decay': 0.05, 'sustain': 0.9, 'release': 0.1} + } + + # 和声进行数据库 + self.chord_progressions = { + 'major': [ + ['I', 'V', 'vi', 'IV'], # 流行进行 + ['I', 'vi', 'IV', 'V'], # 古典进行 + ['I', 'IV', 'V', 'I'], # 终止进行 + ['I', 'iii', 'IV', 'V'], # 扩展进行 + ['I', 'vi', 'ii', 'V'] # 2-5-1进行 + ], + 'minor': [ + ['i', 'V', 'VI', 'III'], # 小调流行进行 + ['i', 'bVII', 'VI', 'VII'], # 模态进行 + ['i', 'iv', 'V', 'i'], # 小调终止进行 + ['i', 'bVI', 'bIII', 'V'], # 情感进行 + ['i', 'bVII', 'bVI', 'V'] # 蓝调进行 + ] + } + + # 初始化作曲引擎 + self._initialize_composition_engine() + + def _initialize_music_theory(self): + """初始化音乐理论数据""" + # 音符频率表 (基于A4=440Hz) + self.note_frequencies = {} + a4_freq = 440.0 + for i in range(-60, 60): # 支持多个八度 + note_name = self._get_note_name(i) + self.note_frequencies[note_name] = a4_freq * (2 ** (i / 12)) + + # 音阶模式 + self.scale_patterns = { + 'major': [0, 2, 4, 5, 7, 9, 11], + 'minor': [0, 2, 3, 5, 7, 8, 10], + 'harmonic_minor': [0, 2, 3, 5, 7, 8, 11], + 'melodic_minor': [0, 2, 3, 5, 7, 9, 11], + 'dorian': [0, 2, 3, 5, 7, 9, 10], + 'phrygian': [0, 1, 3, 5, 7, 8, 10], + 'lydian': [0, 2, 4, 6, 7, 9, 11], + 'mixolydian': [0, 2, 4, 5, 7, 9, 10], + 'aeolian': [0, 2, 3, 5, 7, 8, 10], + 'locrian': [0, 1, 3, 5, 6, 8, 10] + } + + # 和弦模式 + self.chord_patterns = { + 'major': [0, 4, 7], + 'minor': [0, 3, 7], + 'diminished': [0, 3, 6], + 'augmented': [0, 4, 8], + 'sus2': [0, 2, 7], + 'sus4': [0, 5, 7], + '7th': [0, 4, 7, 10], + 'maj7': [0, 4, 7, 11], + 'min7': [0, 3, 7, 10], + 'dim7': [0, 3, 6, 9], + 'm7b5': [0, 3, 6, 10] + } + + # 罗马数字和弦映射 + self.roman_numerals = { + 'I': 0, 'II': 1, 'III': 2, 'IV': 3, 'V': 4, 'VI': 5, 'VII': 6, + 'i': 0, 'ii': 1, 'iii': 2, 'iv': 3, 'v': 4, 'vi': 5, 'vii': 6, + 'bII': 10, 'bIII': 11, 'bVI': 14, 'bVII': 15 # 用于小调 + } + + # 音符名称 + self.note_names = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] + + # 节拍模式 + self.rhythm_patterns = { + '4/4': [1.0, 0.5, 0.5, 1.0, 0.5, 0.5, 0.5, 0.5], # 基本4/4拍 + '3/4': [1.0, 0.5, 0.5], # 华尔兹 + '6/8': [1.5, 0.5, 0.5, 1.5, 0.5, 0.5], # 6/8拍 + 'syncopated': [0.5, 0.5, 1.0, 0.5, 0.5, 1.0], # 切分音 + 'swing': [1.33, 0.67, 1.33, 0.67] # 摇摆节奏 + } + + def _get_note_name(self, semitone_offset: int) -> str: + """ + 根据半音偏移量获取音符名称 + + Args: + semitone_offset: 相对于A4的半音偏移量 + + Returns: + 音符名称 + """ + note_index = (9 + semitone_offset) % 12 # A是第9个音符 + octave = 4 + (9 + semitone_offset) // 12 + return f"{self.note_names[note_index]}{octave}" + + def _initialize_composition_engine(self): + """初始化作曲引擎""" + print("✓ 作曲引擎初始化完成") + + def create_generator(self, generator_name: str, generator_params: Dict[str, Any]) -> bool: + """ + 创建音乐生成器 + + Args: + generator_name: 生成器名称 + generator_params: 生成器参数 + + Returns: + 是否创建成功 + """ + try: + # 设置默认参数 + default_params = { + 'type': 'melody', # melody, harmony, rhythm, bass + 'key': self.master_key, + 'scale': self.master_scale, + 'bpm': self.master_bpm, + 'octave': 4, + 'complexity': 0.5, # 0.0-1.0 + 'variation': 0.3, # 0.0-1.0 + 'length': 8, # 小节数 + 'instrument': 'piano', + 'arpeggiate': False, + 'syncopation': 0.0, # 0.0-1.0 + 'polyphony': 1, # 同时发声数 + 'mode': 'loop', # loop, once, random + 'tempo_sync': True, # 是否与主BPM同步 + 'dynamic_range': (0.6, 1.0) # 音量范围 + } + + # 合并参数 + params = default_params.copy() + params.update(generator_params) + + # 验证参数 + if params['type'] not in ['melody', 'harmony', 'rhythm', 'bass']: + print(f"✗ 无效的生成器类型: {params['type']}") + return False + + if params['scale'] not in self.scale_patterns: + print(f"✗ 无效的音阶: {params['scale']}") + return False + + if params['instrument'] not in self.instrument_presets: + print(f"⚠ 未知乐器: {params['instrument']},使用默认钢琴音色") + params['instrument'] = 'piano' + + # 验证复杂度和变化度范围 + params['complexity'] = max(0.0, min(1.0, params['complexity'])) + params['variation'] = max(0.0, min(1.0, params['variation'])) + params['syncopation'] = max(0.0, min(1.0, params['syncopation'])) + params['polyphony'] = max(1, min(self.max_polyphony, params['polyphony'])) + + # 创建生成器 + generator = { + 'name': generator_name, + 'params': params, + 'state': 'created', + 'segments': [], + 'current_segment': 0, + 'playback_position': 0.0, + 'last_generated_time': 0.0, + 'is_generating': False, + 'thread': None, + 'queue': queue.Queue() + } + + self.generators[generator_name] = generator + self.generator_queues[generator_name] = generator['queue'] + self.stats['generators_created'] += 1 + + print(f"✓ 音乐生成器创建成功: {generator_name}") + return True + + except Exception as e: + print(f"✗ 创建音乐生成器失败: {e}") + import traceback + traceback.print_exc() + return False + + def activate_generator(self, generator_name: str) -> bool: + """ + 激活音乐生成器 + + Args: + generator_name: 生成器名称 + + Returns: + 是否激活成功 + """ + try: + if generator_name not in self.generators: + print(f"✗ 音乐生成器不存在: {generator_name}") + return False + + generator = self.generators[generator_name] + + # 启动生成器线程 + if generator['thread'] is None or not generator['thread'].is_alive(): + generator['thread'] = threading.Thread( + target=self._generator_worker, + args=(generator_name,), + daemon=True + ) + generator['thread'].start() + + # 生成初始音乐片段 + if self._generate_segment(generator_name): + self.active_generators[generator_name] = self.generators[generator_name] + self.stats['generators_active'] = len(self.active_generators) + generator['state'] = 'active' + print(f"✓ 音乐生成器已激活: {generator_name}") + return True + else: + print(f"✗ 生成音乐片段失败: {generator_name}") + return False + + except Exception as e: + print(f"✗ 激活音乐生成器失败: {e}") + import traceback + traceback.print_exc() + return False + + def deactivate_generator(self, generator_name: str) -> bool: + """ + 停用音乐生成器 + + Args: + generator_name: 生成器名称 + + Returns: + 是否停用成功 + """ + try: + if generator_name not in self.active_generators: + print(f"⚠ 音乐生成器未激活: {generator_name}") + return True + + generator = self.generators[generator_name] + generator['state'] = 'inactive' + + del self.active_generators[generator_name] + self.stats['generators_active'] = len(self.active_generators) + + print(f"✓ 音乐生成器已停用: {generator_name}") + return True + + except Exception as e: + print(f"✗ 停用音乐生成器失败: {e}") + import traceback + traceback.print_exc() + return False + + def _generator_worker(self, generator_name: str): + """ + 生成器工作线程 + + Args: + generator_name: 生成器名称 + """ + try: + while generator_name in self.generators: + generator = self.generators[generator_name] + if generator['state'] == 'active': + # 检查是否需要生成新片段 + if self._should_generate_new_segment(generator): + self._generate_segment(generator_name) + + # 处理队列中的请求 + try: + request = generator['queue'].get(timeout=0.1) + if request['type'] == 'generate': + self._generate_segment(generator_name) + elif request['type'] == 'stop': + break + except queue.Empty: + pass + + time.sleep(0.01) # 10ms延迟 + + except Exception as e: + print(f"✗ 生成器工作线程错误: {e}") + import traceback + traceback.print_exc() + + def _generate_segment(self, generator_name: str) -> bool: + """ + 生成音乐片段 + + Args: + generator_name: 生成器名称 + + Returns: + 是否生成成功 + """ + try: + if generator_name not in self.generators: + return False + + generator = self.generators[generator_name] + params = generator['params'] + + # 检查是否正在生成 + if generator['is_generating']: + return True # 避免重复生成 + + generator['is_generating'] = True + + # 生成唯一片段ID + segment_id = f"{generator_name}_{int(time.time() * 1000000)}" + + # 根据生成器类型生成音乐数据 + if params['type'] == 'melody': + segment_data = self._generate_melody_segment(params) + elif params['type'] == 'harmony': + segment_data = self._generate_harmony_segment(params) + elif params['type'] == 'rhythm': + segment_data = self._generate_rhythm_segment(params) + elif params['type'] == 'bass': + segment_data = self._generate_bass_segment(params) + else: + segment_data = self._generate_melody_segment(params) # 默认生成旋律 + + # 创建片段 + segment = { + 'id': segment_id, + 'generator': generator_name, + 'data': segment_data, + 'created_time': time.time(), + 'duration': params['length'] * (60.0 / params['bpm']) * 4, # 假设4/4拍 + 'bpm': params['bpm'], + 'key': params['key'], + 'scale': params['scale'], + 'type': params['type'] + } + + # 存储片段 + self.composed_segments[segment_id] = segment + generator['segments'].append(segment_id) + + # 限制片段数量 + if len(generator['segments']) > 10: # 最多保留10个片段 + old_segment_id = generator['segments'].pop(0) + if old_segment_id in self.composed_segments: + del self.composed_segments[old_segment_id] + + self.stats['segments_composed'] += 1 + generator['last_generated_time'] = time.time() + generator['is_generating'] = False + + print(f"✓ 音乐片段生成成功: {segment_id}") + return True + + except Exception as e: + print(f"✗ 生成音乐片段失败: {e}") + import traceback + traceback.print_exc() + if generator_name in self.generators: + self.generators[generator_name]['is_generating'] = False + self.stats['failed_compositions'] += 1 + return False + + def _generate_melody_segment(self, params: Dict[str, Any]) -> Dict[str, Any]: + """ + 生成旋律片段 + + Args: + params: 生成参数 + + Returns: + 旋律片段数据 + """ + try: + # 获取音阶音符 + scale_notes = self._get_scale_notes(params['key'], params['scale'], params['octave']) + + # 计算片段长度(以拍为单位) + beats_per_measure = 4 # 假设4/4拍 + total_beats = params['length'] * beats_per_measure + + # 选择节拍模式 + time_signature = '4/4' + if params['syncopation'] > 0.7: + rhythm_pattern = self.rhythm_patterns['syncopated'] + elif params['syncopation'] > 0.4: + rhythm_pattern = self.rhythm_patterns['swing'] + else: + rhythm_pattern = self.rhythm_patterns[time_signature] + + # 生成音符序列 + notes = [] + current_time = 0.0 + beat_index = 0 + + while current_time < total_beats: + # 选择音符 + note_index = self._choose_note(scale_notes, params['complexity'], params['variation']) + note_name = scale_notes[note_index] + note_freq = self.note_frequencies.get(note_name, 440.0) + + # 选择时值 + duration = rhythm_pattern[beat_index % len(rhythm_pattern)] + + # 应用复杂度调整时值 + if params['complexity'] > 0.7: + # 增加装饰音和复杂节奏 + if np.random.random() < 0.3: + duration *= 0.5 # 八分音符 + # 添加装饰音 + if note_index > 0: + grace_note_index = max(0, note_index - 1) + grace_note_name = scale_notes[grace_note_index] + grace_note_freq = self.note_frequencies.get(grace_note_name, 440.0) + notes.append({ + 'frequency': grace_note_freq, + 'duration': duration * 0.3, + 'start_time': current_time, + 'velocity': 0.4 + np.random.random() * 0.2, + 'type': 'grace' + }) + current_time += duration * 0.3 + + # 添加主要音符 + velocity_range = params.get('dynamic_range', (0.6, 1.0)) + velocity = velocity_range[0] + np.random.random() * (velocity_range[1] - velocity_range[0]) + + notes.append({ + 'frequency': note_freq, + 'duration': duration, + 'start_time': current_time, + 'velocity': velocity, + 'type': 'main' + }) + + current_time += duration + beat_index += 1 + + # 避免无限循环 + if beat_index > total_beats * 4: # 限制最多4倍拍数 + break + + # 应用琶音效果 + if params['arpeggiate']: + notes = self._apply_arpeggiation(notes, params['syncopation']) + + return { + 'type': 'melody', + 'notes': notes, + 'bpm': params['bpm'], + 'key': params['key'], + 'scale': params['scale'], + 'instrument': params['instrument'] + } + + except Exception as e: + print(f"✗ 生成旋律片段失败: {e}") + import traceback + traceback.print_exc() + return { + 'type': 'melody', + 'notes': [], + 'bpm': params['bpm'], + 'key': params['key'], + 'scale': params['scale'], + 'instrument': params['instrument'] + } + + def _generate_harmony_segment(self, params: Dict[str, Any]) -> Dict[str, Any]: + """ + 生成和声片段 + + Args: + params: 生成参数 + + Returns: + 和声片段数据 + """ + try: + # 选择和弦进行 + progressions = self.chord_progressions.get(params['scale'], self.chord_progressions['major']) + chord_progression = np.random.choice(progressions) + + # 计算片段长度 + beats_per_measure = 4 + total_measures = params['length'] + + # 生成和弦序列 + chords = [] + current_time = 0.0 + measure_duration = beats_per_measure * (60.0 / params['bpm']) + + for measure in range(total_measures): + # 获取和弦 + chord_index = measure % len(chord_progression) + roman_numeral = chord_progression[chord_index] + + # 转换罗马数字为和弦 + scale_degree = self.roman_numerals.get(roman_numeral, 0) + chord_type = 'major' if roman_numeral.isupper() else 'minor' + + # 特殊处理小调中的降号和弦 + if roman_numeral.startswith('b'): + scale_degree = self.roman_numerals.get(roman_numeral, scale_degree) + if 'VII' in roman_numeral: + chord_type = 'major' + elif 'VI' in roman_numeral or 'III' in roman_numeral: + chord_type = 'major' + + # 获取和弦音符 + scale_notes = self._get_scale_notes(params['key'], params['scale'], params['octave'] - 1) + chord_root_index = scale_degree % len(scale_notes) + chord_root = scale_notes[chord_root_index] + chord_notes = self._get_chord_notes(chord_root, chord_type, params['octave'] - 1) + + # 计算和弦频率 + frequencies = [self.note_frequencies.get(note, 440.0) for note in chord_notes] + + # 应用琶音效果 + arpeggiated = params['arpeggiate'] and np.random.random() < 0.5 + if arpeggiated: + # 生成琶音音符 + arp_notes = [] + arp_duration = measure_duration / len(chord_notes) + for i, freq in enumerate(frequencies): + arp_notes.append({ + 'frequency': freq, + 'duration': arp_duration, + 'start_time': current_time + i * arp_duration, + 'velocity': 0.5 + np.random.random() * 0.3, + 'type': 'arpeggio' + }) + chords.extend(arp_notes) + else: + # 生成和弦 + velocity_range = params.get('dynamic_range', (0.5, 0.9)) + velocity = velocity_range[0] + np.random.random() * (velocity_range[1] - velocity_range[0]) + + chords.append({ + 'notes': chord_notes, + 'frequencies': frequencies, + 'duration': measure_duration, + 'start_time': current_time, + 'velocity': velocity, + 'type': 'chord' + }) + + current_time += measure_duration + + return { + 'type': 'harmony', + 'chords': chords, + 'progression': chord_progression, + 'bpm': params['bpm'], + 'key': params['key'], + 'scale': params['scale'], + 'instrument': params['instrument'] + } + + except Exception as e: + print(f"✗ 生成和声片段失败: {e}") + import traceback + traceback.print_exc() + return { + 'type': 'harmony', + 'chords': [], + 'progression': [], + 'bpm': params['bpm'], + 'key': params['key'], + 'scale': params['scale'], + 'instrument': params['instrument'] + } + + def _generate_rhythm_segment(self, params: Dict[str, Any]) -> Dict[str, Any]: + """ + 生成节奏片段 + + Args: + params: 生成参数 + + Returns: + 节奏片段数据 + """ + try: + # 生成鼓组节奏 + drums = ['kick', 'snare', 'hihat', 'crash', 'tom_low', 'tom_mid', 'tom_high'] + drum_frequencies = { + 'kick': 60.0, + 'snare': 200.0, + 'hihat': 8000.0, + 'crash': 4000.0, + 'tom_low': 120.0, + 'tom_mid': 250.0, + 'tom_high': 500.0 + } + + beats_per_measure = 4 + total_beats = params['length'] * beats_per_measure + + hits = [] + current_time = 0.0 + beat_duration = 60.0 / params['bpm'] + + # 基本节奏模式 + basic_pattern = { + 'kick': [0, 2], # 强拍 + 'snare': [1, 3], # 弱拍 + 'hihat': [0, 1, 2, 3] # 每拍 + } + + # 根据复杂度调整模式 + if params['complexity'] > 0.7: + # 复杂节奏 + basic_pattern['kick'] = [0, 0.5, 2, 2.5, 3.5] + basic_pattern['snare'] = [1, 3] + basic_pattern['hihat'] = [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5] + basic_pattern['tom_low'] = [1.5] + basic_pattern['tom_mid'] = [2.5] + basic_pattern['tom_high'] = [3.5] + elif params['complexity'] > 0.4: + # 中等复杂度 + basic_pattern['kick'] = [0, 2] + basic_pattern['snare'] = [1, 3] + basic_pattern['hihat'] = [0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5] + + # 生成节奏 + for measure in range(params['length']): + measure_start = measure * beats_per_measure + + for drum_type, pattern in basic_pattern.items(): + if drum_type in drum_frequencies: + for beat_offset in pattern: + hit_time = (measure_start + beat_offset) * beat_duration + velocity_range = params.get('dynamic_range', (0.6, 1.0)) + velocity = velocity_range[0] + np.random.random() * (velocity_range[1] - velocity_range[0]) + + # 应用切分音效果 + if params['syncopation'] > 0.5 and np.random.random() < 0.3: + # 偶尔偏移时间 + hit_time += (np.random.random() - 0.5) * beat_duration * 0.2 + + hits.append({ + 'drum': drum_type, + 'frequency': drum_frequencies[drum_type], + 'time': hit_time, + 'velocity': velocity, + 'duration': beat_duration * 0.1 # 短促的打击乐 + }) + + # 添加填充(fills) + if np.random.random() < 0.3: # 30%概率添加填充 + fill_start = (params['length'] - 1) * beats_per_measure * beat_duration + for i in range(4): # 4个填充音符 + drum_type = np.random.choice(['snare', 'tom_low', 'tom_mid', 'tom_high']) + hit_time = fill_start + i * beat_duration * 0.25 + hits.append({ + 'drum': drum_type, + 'frequency': drum_frequencies[drum_type], + 'time': hit_time, + 'velocity': 0.8 + np.random.random() * 0.2, + 'duration': beat_duration * 0.05 + }) + + return { + 'type': 'rhythm', + 'hits': hits, + 'bpm': params['bpm'], + 'pattern_complexity': params['complexity'] + } + + except Exception as e: + print(f"✗ 生成节奏片段失败: {e}") + import traceback + traceback.print_exc() + return { + 'type': 'rhythm', + 'hits': [], + 'bpm': params['bpm'], + 'pattern_complexity': params['complexity'] + } + + def _generate_bass_segment(self, params: Dict[str, Any]) -> Dict[str, Any]: + """ + 生成贝斯片段 + + Args: + params: 生成参数 + + Returns: + 贝斯片段数据 + """ + try: + # 获取低音音阶音符 + scale_notes = self._get_scale_notes(params['key'], params['scale'], params['octave'] - 1) + + # 计算片段长度 + beats_per_measure = 4 + total_beats = params['length'] * beats_per_measure + beat_duration = 60.0 / params['bpm'] + + # 生成贝斯线 + notes = [] + current_time = 0.0 + beat_count = 0 + + # 使用和弦进行作为贝斯线基础 + progressions = self.chord_progressions.get(params['scale'], self.chord_progressions['major']) + chord_progression = np.random.choice(progressions) + + while current_time < total_beats * beat_duration and beat_count < total_beats: + # 确定当前小节和和弦 + measure = beat_count // beats_per_measure + chord_index = measure % len(chord_progression) + roman_numeral = chord_progression[chord_index] + + # 获取和弦根音作为贝斯音符 + scale_degree = self.roman_numerals.get(roman_numeral, 0) + bass_note_index = scale_degree % len(scale_notes) + bass_note = scale_notes[bass_note_index] + bass_freq = self.note_frequencies.get(bass_note, 65.41) # C2 + + # 确定音符时值 + if np.random.random() < params['complexity'] * 0.5: + # 复杂节奏:使用八分音符 + note_duration = beat_duration * 0.5 + subdivisions = 2 + else: + # 简单节奏:使用四分音符 + note_duration = beat_duration + subdivisions = 1 + + # 生成音符 + for sub in range(subdivisions): + if beat_count + sub * 0.5 < total_beats: + note_time = current_time + sub * note_duration + + # 应用滑音和装饰 + if params['complexity'] > 0.6 and np.random.random() < 0.2: + # 添加经过音 + if bass_note_index < len(scale_notes) - 1: + passing_note_index = bass_note_index + 1 + passing_note = scale_notes[passing_note_index] + passing_freq = self.note_frequencies.get(passing_note, 65.41) + + notes.append({ + 'frequency': passing_freq, + 'duration': note_duration * 0.3, + 'start_time': note_time, + 'velocity': 0.4 + np.random.random() * 0.2, + 'type': 'passing' + }) + note_time += note_duration * 0.3 + + # 主要贝斯音符 + velocity_range = params.get('dynamic_range', (0.7, 1.0)) + velocity = velocity_range[0] + np.random.random() * (velocity_range[1] - velocity_range[0]) + + notes.append({ + 'frequency': bass_freq, + 'duration': note_duration * (1 - 0.3 * (sub > 0)), # 经过音后缩短 + 'start_time': note_time, + 'velocity': velocity, + 'type': 'bass' + }) + + current_time += beat_duration + beat_count += 1 + + return { + 'type': 'bass', + 'notes': notes, + 'bpm': params['bpm'], + 'key': params['key'], + 'scale': params['scale'], + 'instrument': params['instrument'], + 'progression': chord_progression + } + + except Exception as e: + print(f"✗ 生成贝斯片段失败: {e}") + import traceback + traceback.print_exc() + return { + 'type': 'bass', + 'notes': [], + 'bpm': params['bpm'], + 'key': params['key'], + 'scale': params['scale'], + 'instrument': params['instrument'], + 'progression': [] + } + + def _get_scale_notes(self, key: str, scale: str, octave: int) -> List[str]: + """ + 获取音阶中的音符 + + Args: + key: 调号 + scale: 音阶类型 + octave: 八度 + + Returns: + 音符列表 + """ + try: + # 找到调号在音符列表中的索引 + key_index = self.note_names.index(key.upper()) + + # 获取音阶模式 + pattern = self.scale_patterns[scale] + + # 生成音符 + notes = [] + for semitone in pattern: + note_index = (key_index + semitone) % 12 + note_octave = octave + (key_index + semitone) // 12 + note_name = f"{self.note_names[note_index]}{note_octave}" + notes.append(note_name) + + return notes + + except Exception as e: + print(f"✗ 获取音阶音符失败: {e}") + # 返回默认C大调音阶 + return [f"{note}{octave}" for note in ['C', 'D', 'E', 'F', 'G', 'A', 'B']] + + def _get_chord_notes(self, root: str, chord_type: str, octave: int) -> List[str]: + """ + 获取和弦音符 + + Args: + root: 根音 + chord_type: 和弦类型 + octave: 八度 + + Returns: + 和弦音符列表 + """ + try: + # 找到根音在音符列表中的索引 + root_index = self.note_names.index(root[:-1]) # 去掉八度数字 + root_octave = int(root[-1]) + + # 获取和弦模式 + pattern = self.chord_patterns.get(chord_type, self.chord_patterns['major']) + + # 生成和弦音符 + notes = [] + for i, semitone in enumerate(pattern): + note_index = (root_index + semitone) % 12 + note_octave = root_octave + (root_index + semitone) // 12 + # 保证和弦音符在合理的八度范围内 + if i > 0 and note_octave <= root_octave: + note_octave += 1 + note_name = f"{self.note_names[note_index]}{note_octave}" + notes.append(note_name) + + return notes + + except Exception as e: + print(f"✗ 获取和弦音符失败: {e}") + # 返回默认三和弦 + return [root, f"{root[:-1]}{int(root[-1])+1}", f"{root[:-1]}{int(root[-1])+2}"] + + def _choose_note(self, scale_notes: List[str], complexity: float, variation: float) -> int: + """ + 选择音符 + + Args: + scale_notes: 音阶音符列表 + complexity: 复杂度 (0.0-1.0) + variation: 变化度 (0.0-1.0) + + Returns: + 音符索引 + """ + # 基于复杂度和变化度选择音符 + if np.random.random() < complexity: + # 复杂选择:可以跳到任何音符 + return np.random.randint(0, len(scale_notes)) + else: + # 简单选择:倾向于选择相邻音符 + if len(scale_notes) > 1: + current_index = np.random.randint(0, len(scale_notes)) + if np.random.random() < 0.7: # 70%概率选择相邻音符 + offset = np.random.choice([-1, 1]) + new_index = current_index + offset + if 0 <= new_index < len(scale_notes): + return new_index + return current_index + else: + return 0 + + def _apply_arpeggiation(self, notes: List[Dict[str, Any]], syncopation: float) -> List[Dict[str, Any]]: + """ + 应用琶音效果 + + Args: + notes: 音符列表 + syncopation: 切分音程度 + + Returns: + 处理后的音符列表 + """ + try: + arpeggiated_notes = [] + for note in notes: + if note['type'] == 'chord': + # 将和弦分解为琶音 + frequencies = note.get('frequencies', []) + duration = note['duration'] / len(frequencies) + for i, freq in enumerate(frequencies): + arpeggiated_notes.append({ + 'frequency': freq, + 'duration': duration, + 'start_time': note['start_time'] + i * duration, + 'velocity': note['velocity'] * (0.8 + 0.2 * (i % 2)), + 'type': 'arpeggio' + }) + else: + arpeggiated_notes.append(note) + return arpeggiated_notes + except Exception as e: + print(f"✗ 应用琶音效果失败: {e}") + return notes + + def update(self, dt: float): + """ + 更新作曲引擎状态 + + Args: + dt: 时间增量 + """ + update_start_time = time.time() + + # 更新活动生成器 + for generator_name, generator in self.active_generators.items(): + # 更新播放位置 + generator['playback_position'] += dt + + # 检查是否需要生成新片段 + if self._should_generate_new_segment(generator): + # 异步请求生成新片段 + if generator['queue']: + try: + generator['queue'].put({'type': 'generate'}, block=False) + except queue.Full: + pass # 队列满时跳过 + + # 更新待处理生成器请求 + self._process_generator_queues() + + # 更新性能统计 + self.stats['composition_time'] += (time.time() - update_start_time) + + def _should_generate_new_segment(self, generator: Dict[str, Any]) -> bool: + """ + 判断是否需要生成新片段 + + Args: + generator: 生成器数据 + + Returns: + 是否需要生成 + """ + try: + # 检查生成器状态 + if generator['state'] != 'active': + return False + + # 检查是否正在生成 + if generator['is_generating']: + return False + + # 检查时间条件 + current_time = time.time() + time_since_last_generation = current_time - generator['last_generated_time'] + + # 如果没有片段或距离上次生成时间较长,则需要生成 + if len(generator['segments']) < 2 or time_since_last_generation > 5.0: + return True + + return False + except Exception as e: + print(f"✗ 检查生成条件失败: {e}") + return False + + def _process_generator_queues(self): + """处理生成器队列""" + # 这里可以处理一些队列相关的逻辑 + pass + + def cleanup(self): + """清理所有资源""" + # 停止所有生成器线程 + for generator_name, generator in self.generators.items(): + if generator['queue']: + try: + generator['queue'].put({'type': 'stop'}, block=False) + except queue.Full: + pass + + # 清理数据结构 + self.generators.clear() + self.active_generators.clear() + self.composed_segments.clear() + self.generator_threads.clear() + self.generator_queues.clear() + + self.stats = { + 'generators_created': 0, + 'generators_active': 0, + 'segments_composed': 0, + 'composition_time': 0.0, + 'memory_usage': 0, + 'failed_compositions': 0 + } + + print("✓ 作曲引擎资源已清理") + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def get_active_generators(self) -> List[str]: + """ + 获取活动生成器列表 + + Returns: + 活动生成器名称列表 + """ + return list(self.active_generators.keys()) + + def get_generator_status(self, generator_name: str) -> Dict[str, Any]: + """ + 获取生成器状态 + + Args: + generator_name: 生成器名称 + + Returns: + 生成器状态字典 + """ + if generator_name in self.active_generators: + generator = self.active_generators[generator_name] + return { + 'name': generator_name, + 'state': generator['state'], + 'type': generator['params']['type'], + 'segments': len(generator['segments']), + 'active': True, + 'playback_position': generator['playback_position'] + } + elif generator_name in self.generators: + generator = self.generators[generator_name] + return { + 'name': generator_name, + 'state': generator['state'], + 'type': generator['params']['type'], + 'segments': len(generator['segments']), + 'active': False, + 'playback_position': generator['playback_position'] + } + else: + return { + 'name': generator_name, + 'state': 'unknown' + } + + def set_master_key(self, key: str): + """ + 设置主调 + + Args: + key: 调号 + """ + if key.upper() in self.note_names: + self.master_key = key.upper() + print(f"✓ 主调设置为: {self.master_key}") + else: + print(f"✗ 无效的调号: {key}") + + def set_master_scale(self, scale: str): + """ + 设置主音阶 + + Args: + scale: 音阶类型 + """ + if scale in self.scale_patterns: + self.master_scale = scale + print(f"✓ 主音阶设置为: {self.master_scale}") + else: + print(f"✗ 无效的音阶: {scale}") + + def set_master_bpm(self, bpm: float): + """ + 设置主BPM + + Args: + bpm: 每分钟节拍数 + """ + self.master_bpm = max(30, min(300, bpm)) # 限制在合理范围内 + print(f"✓ 主BPM设置为: {self.master_bpm}") + + def generate_theme(self, theme_name: str, theme_params: Dict[str, Any]) -> bool: + """ + 生成完整音乐主题 + + Args: + theme_name: 主题名称 + theme_params: 主题参数 + + Returns: + 是否生成成功 + """ + try: + # 创建多个生成器来组成完整主题 + generators_to_create = theme_params.get('generators', ['melody', 'harmony', 'rhythm']) + + for generator_type in generators_to_create: + generator_name = f"{theme_name}_{generator_type}" + generator_params = { + 'type': generator_type, + 'key': theme_params.get('key', self.master_key), + 'scale': theme_params.get('scale', self.master_scale), + 'bpm': theme_params.get('bpm', self.master_bpm), + 'length': theme_params.get('length', 8), + 'complexity': theme_params.get('complexity', 0.5), + 'variation': theme_params.get('variation', 0.3), + 'instrument': theme_params.get('instrument_' + generator_type, 'piano'), + 'arpeggiate': theme_params.get('arpeggiate', False), + 'syncopation': theme_params.get('syncopation', 0.0) + } + + self.create_generator(generator_name, generator_params) + + print(f"✓ 音乐主题生成成功: {theme_name}") + return True + + except Exception as e: + print(f"✗ 生成音乐主题失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_segment_data(self, segment_id: str) -> Dict[str, Any]: + """ + 获取片段数据 + + Args: + segment_id: 片段ID + + Returns: + 片段数据 + """ + return self.composed_segments.get(segment_id, {}).get('data', {}) + + def set_sample_rate(self, sample_rate: int): + """ + 设置采样率 + + Args: + sample_rate: 采样率 (Hz) + """ + self.sample_rate = sample_rate + print(f"✓ 作曲引擎采样率设置为: {self.sample_rate} Hz") + + def create_adaptive_generator(self, generator_name: str, + game_state_callback: Callable[[], Dict[str, Any]]) -> bool: + """ + 创建自适应音乐生成器 + + Args: + generator_name: 生成器名称 + game_state_callback: 游戏状态回调函数 + + Returns: + 是否创建成功 + """ + try: + # 创建自适应生成器参数 + adaptive_params = { + 'type': 'melody', + 'key': self.master_key, + 'scale': self.master_scale, + 'bpm': self.master_bpm, + 'octave': 4, + 'complexity': 0.5, + 'variation': 0.3, + 'length': 8, + 'instrument': 'piano', + 'arpeggiate': False, + 'syncopation': 0.0, + 'adaptive': True, + 'game_state_callback': game_state_callback + } + + # 创建生成器 + generator = { + 'name': generator_name, + 'params': adaptive_params, + 'state': 'created', + 'segments': [], + 'current_segment': 0, + 'playback_position': 0.0, + 'last_generated_time': 0.0, + 'is_generating': False, + 'thread': None, + 'queue': queue.Queue(), + 'adaptive_callback': game_state_callback + } + + self.generators[generator_name] = generator + self.generator_queues[generator_name] = generator['queue'] + self.stats['generators_created'] += 1 + + print(f"✓ 自适应音乐生成器创建成功: {generator_name}") + return True + + except Exception as e: + print(f"✗ 创建自适应音乐生成器失败: {e}") + import traceback + traceback.print_exc() + return False + + def set_generator_parameter(self, generator_name: str, param_name: str, param_value: Any) -> bool: + """ + 设置生成器参数 + + Args: + generator_name: 生成器名称 + param_name: 参数名称 + param_value: 参数值 + + Returns: + 是否设置成功 + """ + try: + if generator_name not in self.generators: + print(f"✗ 生成器不存在: {generator_name}") + return False + + generator = self.generators[generator_name] + + # 更新参数 + generator['params'][param_name] = param_value + print(f"✓ 生成器 {generator_name} 参数 {param_name} 设置为: {param_value}") + return True + + except Exception as e: + print(f"✗ 设置生成器参数失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_generator_parameters(self, generator_name: str) -> Dict[str, Any]: + """ + 获取生成器参数 + + Args: + generator_name: 生成器名称 + + Returns: + 参数字典 + """ + if generator_name in self.generators: + return self.generators[generator_name]['params'].copy() + return {} + + def regenerate_segment(self, generator_name: str) -> bool: + """ + 重新生成片段 + + Args: + generator_name: 生成器名称 + + Returns: + 是否重新生成成功 + """ + try: + if generator_name not in self.generators: + print(f"✗ 生成器不存在: {generator_name}") + return False + + return self._generate_segment(generator_name) + + except Exception as e: + print(f"✗ 重新生成片段失败: {e}") + import traceback + traceback.print_exc() + return False + + def set_instrument_preset(self, instrument_name: str, preset: Dict[str, float]): + """ + 设置乐器预设 + + Args: + instrument_name: 乐器名称 + preset: 预设参数字典 (attack, decay, sustain, release) + """ + required_params = ['attack', 'decay', 'sustain', 'release'] + for param in required_params: + if param not in preset: + print(f"✗ 乐器预设缺少必需参数: {param}") + return + + self.instrument_presets[instrument_name] = preset.copy() + print(f"✓ 乐器预设 {instrument_name} 已更新") + + def get_instrument_preset(self, instrument_name: str) -> Dict[str, float]: + """ + 获取乐器预设 + + Args: + instrument_name: 乐器名称 + + Returns: + 预设参数字典 + """ + return self.instrument_presets.get(instrument_name, self.instrument_presets['piano']).copy() + + def set_composition_setting(self, setting_name: str, value: Any): + """ + 设置作曲参数 + + Args: + setting_name: 参数名称 + value: 参数值 + """ + self.composition_settings[setting_name] = value + print(f"✓ 作曲参数 {setting_name} 设置为: {value}") + + def get_composition_setting(self, setting_name: str) -> Any: + """ + 获取作曲参数 + + Args: + setting_name: 参数名称 + + Returns: + 参数值 + """ + return self.composition_settings.get(setting_name, None) + + def generate_audio_buffer(self, generator_name: str, buffer_size: int = None) -> np.ndarray: + """ + 生成音频缓冲区数据 + + Args: + generator_name: 生成器名称 + buffer_size: 缓冲区大小 + + Returns: + 音频数据数组 + """ + try: + if buffer_size is None: + buffer_size = self.buffer_size + + if generator_name not in self.active_generators: + return np.zeros(buffer_size) + + generator = self.active_generators[generator_name] + if not generator['segments']: + return np.zeros(buffer_size) + + # 获取当前片段 + current_segment_id = generator['segments'][generator['current_segment'] % len(generator['segments'])] + segment_data = self.get_segment_data(current_segment_id) + + # 生成音频数据 + audio_buffer = np.zeros(buffer_size) + t = np.linspace(0, buffer_size / self.sample_rate, buffer_size, False) + + if segment_data.get('type') == 'melody': + notes = segment_data.get('notes', []) + for note in notes: + freq = note.get('frequency', 440.0) + duration = note.get('duration', 1.0) + start_time = note.get('start_time', 0.0) + velocity = note.get('velocity', 0.7) + + # 计算样本索引 + start_sample = int(start_time * self.sample_rate) + end_sample = min(buffer_size, int((start_time + duration) * self.sample_rate)) + + if start_sample < buffer_size and end_sample > start_sample: + # 生成正弦波 + note_samples = end_sample - start_sample + note_t = np.linspace(0, duration, note_samples, False) + wave = velocity * np.sin(2 * np.pi * freq * note_t) + + # 应用包络 + envelope = self._create_envelope(note_samples, 'piano') + wave *= envelope + + audio_buffer[start_sample:end_sample] += wave + + elif segment_data.get('type') == 'rhythm': + hits = segment_data.get('hits', []) + for hit in hits: + freq = hit.get('frequency', 200.0) + hit_time = hit.get('time', 0.0) + duration = hit.get('duration', 0.1) + velocity = hit.get('velocity', 0.8) + + # 计算样本索引 + start_sample = int(hit_time * self.sample_rate) + end_sample = min(buffer_size, int((hit_time + duration) * self.sample_rate)) + + if start_sample < buffer_size and end_sample > start_sample: + # 生成打击乐声音 + hit_samples = end_sample - start_sample + hit_t = np.linspace(0, duration, hit_samples, False) + + # 生成噪声为基础的打击乐 + noise = np.random.random(hit_samples) * 2 - 1 + # 应用滤波器模拟不同打击乐器 + b, a = signal.butter(4, freq / (self.sample_rate / 2), 'low') + filtered_noise = signal.lfilter(b, a, noise) + + # 应用包络 + envelope = self._create_envelope(hit_samples, 'percussion') + wave = velocity * filtered_noise * envelope + + audio_buffer[start_sample:end_sample] += wave + + # 限制幅度防止削波 + audio_buffer = np.clip(audio_buffer, -1.0, 1.0) + + return audio_buffer + + except Exception as e: + print(f"✗ 生成音频缓冲区失败: {e}") + import traceback + traceback.print_exc() + return np.zeros(buffer_size if buffer_size else self.buffer_size) + + def _create_envelope(self, sample_count: int, instrument_type: str) -> np.ndarray: + """ + 创建包络 + + Args: + sample_count: 样本数量 + instrument_type: 乐器类型 + + Returns: + 包络数组 + """ + try: + preset = self.instrument_presets.get(instrument_type, self.instrument_presets['piano']) + + attack_samples = int(preset['attack'] * self.sample_rate) + decay_samples = int(preset['decay'] * self.sample_rate) + release_samples = int(preset['release'] * self.sample_rate) + sustain_level = preset['sustain'] + + envelope = np.ones(sample_count) + + # 攻击阶段 + if attack_samples > 0 and attack_samples < sample_count: + envelope[:attack_samples] = np.linspace(0, 1, attack_samples) + + # 衰减阶段 + decay_start = attack_samples + decay_end = attack_samples + decay_samples + if decay_end < sample_count: + envelope[decay_start:decay_end] = np.linspace(1, sustain_level, decay_samples) + elif decay_start < sample_count: + envelope[decay_start:] = np.linspace(1, sustain_level, sample_count - decay_start) + + # 释放阶段(如果空间足够) + release_start = max(0, sample_count - release_samples) + if release_start < sample_count: + envelope[release_start:] = np.linspace(sustain_level, 0, sample_count - release_start) + + return envelope + + except Exception as e: + print(f"✗ 创建包络失败: {e}") + return np.ones(sample_count) + + def set_max_polyphony(self, max_voices: int): + """ + 设置最大复调数 + + Args: + max_voices: 最大同时发声数 + """ + self.max_polyphony = max(1, max_voices) + print(f"✓ 最大复调数设置为: {self.max_polyphony}") + + def get_active_segment(self, generator_name: str) -> str: + """ + 获取生成器当前活动片段 + + Args: + generator_name: 生成器名称 + + Returns: + 片段ID + """ + if generator_name in self.active_generators: + generator = self.active_generators[generator_name] + if generator['segments']: + return generator['segments'][generator['current_segment'] % len(generator['segments'])] return "" \ No newline at end of file diff --git a/plugins/user/dynamic_music_system/core/music_manager.py b/plugins/user/dynamic_music_system/core/music_manager.py index 8fa262e8..4c57a7e5 100644 --- a/plugins/user/dynamic_music_system/core/music_manager.py +++ b/plugins/user/dynamic_music_system/core/music_manager.py @@ -1,981 +1,981 @@ -""" -音乐管理器 -负责音乐主题的加载、播放和控制 -""" - -import uuid -import math -import time -import numpy as np -from typing import Dict, List, Any, Optional -from panda3d.core import AudioSound -import scipy.signal as signal - -class MusicManager: - """ - 音乐管理器 - 负责音乐主题的加载、播放和控制 - """ - - def __init__(self, plugin): - """ - 初始化音乐管理器 - - Args: - plugin: 动态音乐系统插件实例 - """ - self.plugin = plugin - self.themes: Dict[str, Dict[str, Any]] = {} - self.layers: Dict[str, Dict[str, Any]] = {} - self.active_themes: Dict[str, Dict[str, Any]] = {} - self.layer_volumes: Dict[str, float] = {} - self.sound_cache: Dict[str, AudioSound] = {} - - self.stats = { - 'themes_loaded': 0, - 'themes_active': 0, - 'layers_active': 0, - 'sounds_played': 0, - 'processing_time': 0.0, - 'memory_usage': 0 - } - - self.master_volume = 1.0 - self.crossfade_duration = 2.0 - self.buffer_size = plugin.buffer_size if plugin else 4096 - self.sample_rate = 44100 - - # 音乐处理参数 - self.equalizer_settings = { - 'low_freq': 100.0, - 'mid_freq': 1000.0, - 'high_freq': 8000.0, - 'low_gain': 0.0, - 'mid_gain': 0.0, - 'high_gain': 0.0 - } - - # 音乐播放状态 - self.playback_state = { - 'current_position': 0.0, - 'total_duration': 0.0, - 'is_paused': False, - 'loop_count': 0 - } - - # 音乐缓冲区 - self.audio_buffers = {} - self.fade_buffers = {} - - # 初始化音乐管理器 - self._initialize_music_manager() - - def _initialize_music_manager(self): - """初始化音乐管理器""" - print("✓ 音乐管理器初始化完成") - - def load_theme(self, theme_name: str, theme_data: Dict[str, Any]) -> bool: - """ - 加载音乐主题 - - Args: - theme_name: 主题名称 - theme_data: 主题数据 - - Returns: - 是否加载成功 - """ - try: - # 存储主题数据 - self.themes[theme_name] = theme_data.copy() - - # 加载主题中的所有层 - layers = theme_data.get('layers', []) - for layer_name in layers: - if layer_name not in self.layers: - self.load_layer(layer_name) - - self.stats['themes_loaded'] += 1 - print(f"✓ 音乐主题加载成功: {theme_name}") - return True - - except Exception as e: - print(f"✗ 加载音乐主题失败: {e}") - import traceback - traceback.print_exc() - return False - - def load_layer(self, layer_name: str, layer_data: Dict[str, Any] = None) -> bool: - """ - 加载音乐层 - - Args: - layer_name: 层名称 - layer_data: 层数据(可选) - - Returns: - 是否加载成功 - """ - try: - # 如果没有提供层数据,创建默认数据 - if layer_data is None: - layer_data = { - 'volume': 1.0, - 'loop': True, - 'priority': 1, - 'filepath': f"music/layers/{layer_name}.wav", - 'format': 'wav', - 'channels': 2, - 'sample_rate': 44100, - 'bit_depth': 16 - } - - # 存储层数据 - self.layers[layer_name] = layer_data.copy() - - # 设置默认音量 - self.layer_volumes[layer_name] = layer_data.get('volume', 1.0) - - # 创建音频缓冲区 - buffer_size = self.buffer_size - self.audio_buffers[layer_name] = np.zeros(buffer_size) - - print(f"✓ 音乐层加载成功: {layer_name}") - return True - - except Exception as e: - print(f"✗ 加载音乐层失败: {e}") - import traceback - traceback.print_exc() - return False - - def play_theme(self, theme_name: str, force: bool = False) -> bool: - """ - 播放音乐主题 - - Args: - theme_name: 主题名称 - force: 是否强制播放(跳过过渡) - - Returns: - 是否播放成功 - """ - try: - if theme_name not in self.themes: - print(f"✗ 音乐主题不存在: {theme_name}") - return False - - theme_data = self.themes[theme_name] - layers = theme_data.get('layers', []) - - # 创建主题实例 - theme_instance = { - 'name': theme_name, - 'layers': {}, - 'start_time': time.time(), - 'volume': 1.0, - 'state': 'playing', - 'bpm': theme_data.get('bpm', 120), - 'key': theme_data.get('key', 'C'), - 'mood': theme_data.get('mood', 'neutral'), - 'loop': theme_data.get('loop', True), - 'fade_in_progress': 0.0, - 'fade_in_duration': 0.0 - } - - # 加载并播放所有层 - for layer_name in layers: - if self.play_layer(layer_name): - theme_instance['layers'][layer_name] = { - 'name': layer_name, - 'volume': self.layer_volumes.get(layer_name, 1.0), - 'state': 'playing', - 'position': 0.0, - 'duration': 0.0 - } - - # 存储活动主题 - self.active_themes[theme_name] = theme_instance - self.stats['themes_active'] = len(self.active_themes) - self.stats['layers_active'] = sum(len(theme['layers']) for theme in self.active_themes.values()) - - print(f"✓ 音乐主题开始播放: {theme_name}") - return True - - except Exception as e: - print(f"✗ 播放音乐主题失败: {e}") - import traceback - traceback.print_exc() - return False - - def play_layer(self, layer_name: str) -> bool: - """ - 播放音乐层 - - Args: - layer_name: 层名称 - - Returns: - 是否播放成功 - """ - try: - if layer_name not in self.layers: - # 如果层不存在,创建一个虚拟层 - self.load_layer(layer_name) - - # 模拟播放音频文件 - # 在实际实现中,这里会使用Panda3D的音频系统 - layer_data = self.layers[layer_name] - - # 初始化音频缓冲区 - buffer_size = self.buffer_size - if layer_name not in self.audio_buffers: - self.audio_buffers[layer_name] = np.zeros(buffer_size) - - # 生成测试音频信号 - self._generate_test_audio(layer_name, layer_data) - - print(f"✓ 音乐层开始播放: {layer_name}") - self.stats['sounds_played'] += 1 - return True - - except Exception as e: - print(f"✗ 播放音乐层失败: {e}") - import traceback - traceback.print_exc() - return False - - def _generate_test_audio(self, layer_name: str, layer_data: Dict[str, Any]): - """ - 生成测试音频信号 - - Args: - layer_name: 层名称 - layer_data: 层数据 - """ - try: - # 获取层的音频参数 - sample_rate = layer_data.get('sample_rate', self.sample_rate) - duration = 10.0 # 10秒测试音频 - samples = int(sample_rate * duration) - - # 生成不同频率的正弦波组合来模拟音乐层 - t = np.linspace(0, duration, samples, False) - - # 基频基于层名称和参数 - base_freq = 220.0 # A3音符 - if 'bass' in layer_name.lower(): - base_freq = 110.0 # A2音符(低音) - elif 'high' in layer_name.lower(): - base_freq = 440.0 # A4音符(高音) - elif 'melody' in layer_name.lower(): - base_freq = 330.0 # E4音符(中音) - - # 生成音频信号 - audio_signal = np.sin(2 * np.pi * base_freq * t) - - # 添加谐波丰富音色 - harmonics = [0.5, 0.3, 0.2, 0.1] - for i, harmonic in enumerate(harmonics, 2): - audio_signal += harmonic * np.sin(2 * np.pi * base_freq * i * t) - - # 应用包络(模拟音符起音和释放) - envelope = np.ones_like(t) - attack_time = int(0.1 * sample_rate) # 0.1秒起音 - release_time = int(0.2 * sample_rate) # 0.2秒释放 - - if len(envelope) > attack_time: - envelope[:attack_time] = np.linspace(0, 1, attack_time) - if len(envelope) > release_time: - envelope[-release_time:] = np.linspace(1, 0, release_time) - - audio_signal *= envelope - - # 应用层音量 - volume = self.layer_volumes.get(layer_name, 1.0) - audio_signal *= volume - - # 存储到缓冲区(循环存储) - buffer = self.audio_buffers[layer_name] - for i in range(min(len(buffer), len(audio_signal))): - buffer[i] = audio_signal[i % len(audio_signal)] - - except Exception as e: - print(f"✗ 生成测试音频失败: {e}") - - def stop_theme(self, theme_name: str) -> bool: - """ - 停止音乐主题 - - Args: - theme_name: 主题名称 - - Returns: - 是否停止成功 - """ - try: - if theme_name not in self.active_themes: - print(f"⚠ 音乐主题未在播放: {theme_name}") - return True - - # 停止主题中的所有层 - theme_instance = self.active_themes[theme_name] - for layer_name in theme_instance['layers']: - self.stop_layer(layer_name) - - # 移除活动主题 - del self.active_themes[theme_name] - self.stats['themes_active'] = len(self.active_themes) - self.stats['layers_active'] = sum(len(theme['layers']) for theme in self.active_themes.values()) - - print(f"✓ 音乐主题已停止: {theme_name}") - return True - - except Exception as e: - print(f"✗ 停止音乐主题失败: {e}") - import traceback - traceback.print_exc() - return False - - def stop_layer(self, layer_name: str) -> bool: - """ - 停止音乐层 - - Args: - layer_name: 层名称 - - Returns: - 是否停止成功 - """ - try: - # 模拟停止音频文件 - # 在实际实现中,这里会使用Panda3D的音频系统 - if layer_name in self.audio_buffers: - # 清空音频缓冲区 - self.audio_buffers[layer_name].fill(0.0) - - print(f"✓ 音乐层已停止: {layer_name}") - return True - - except Exception as e: - print(f"✗ 停止音乐层失败: {e}") - import traceback - traceback.print_exc() - return False - - def stop_all(self): - """停止所有音乐""" - try: - # 停止所有活动主题 - active_themes = list(self.active_themes.keys()) - for theme_name in active_themes: - self.stop_theme(theme_name) - - print("✓ 所有音乐已停止") - - except Exception as e: - print(f"✗ 停止所有音乐失败: {e}") - import traceback - traceback.print_exc() - - def set_theme_volume(self, theme_name: str, volume: float): - """ - 设置主题音量 - - Args: - theme_name: 主题名称 - volume: 音量值 (0.0-1.0) - """ - volume = max(0.0, min(1.0, volume)) - - if theme_name in self.active_themes: - theme_instance = self.active_themes[theme_name] - theme_instance['volume'] = volume - - # 更新主题中所有层的音量 - for layer_name in theme_instance['layers']: - self.set_layer_volume(layer_name, volume) - - print(f"✓ 主题 {theme_name} 音量设置为: {volume:.2f}") - - def set_layer_volume(self, layer_name: str, volume: float): - """ - 设置层音量 - - Args: - layer_name: 层名称 - volume: 音量值 (0.0-1.0) - """ - volume = max(0.0, min(1.0, volume)) - self.layer_volumes[layer_name] = volume - - # 在实际实现中,这里会更新音频实例的音量 - # 对于测试音频,我们调整缓冲区的音量 - if layer_name in self.audio_buffers: - # 这里简化处理,实际应用中会更复杂 - pass - - print(f"✓ 层 {layer_name} 音量设置为: {volume:.2f}") - - def set_master_volume(self, volume: float): - """ - 设置主音量 - - Args: - volume: 主音量 (0.0-1.0) - """ - self.master_volume = max(0.0, min(1.0, volume)) - print(f"✓ 主音量设置为: {self.master_volume:.2f}") - - def fade_out_theme(self, theme_name: str, duration: float = 2.0): - """ - 淡出音乐主题 - - Args: - theme_name: 主题名称 - duration: 淡出持续时间(秒) - """ - try: - if theme_name in self.active_themes: - theme_instance = self.active_themes[theme_name] - theme_instance['state'] = 'fading_out' - theme_instance['fade_start_time'] = time.time() - theme_instance['fade_duration'] = duration - theme_instance['fade_start_volume'] = theme_instance['volume'] - - print(f"✓ 主题 {theme_name} 开始淡出,持续时间: {duration:.2f}秒") - - except Exception as e: - print(f"✗ 淡出主题失败: {e}") - import traceback - traceback.print_exc() - - def fade_in_theme(self, theme_name: str, duration: float = 2.0): - """ - 淡入音乐主题 - - Args: - theme_name: 主题名称 - duration: 淡入持续时间(秒) - """ - try: - if theme_name in self.themes and theme_name not in self.active_themes: - # 先播放主题 - self.play_theme(theme_name) - - # 设置淡入状态 - if theme_name in self.active_themes: - theme_instance = self.active_themes[theme_name] - theme_instance['state'] = 'fading_in' - theme_instance['fade_start_time'] = time.time() - theme_instance['fade_duration'] = duration - theme_instance['fade_start_volume'] = 0.0 - theme_instance['target_volume'] = theme_instance['volume'] - theme_instance['volume'] = 0.0 - self.set_theme_volume(theme_name, 0.0) - - print(f"✓ 主题 {theme_name} 开始淡入,持续时间: {duration:.2f}秒") - - except Exception as e: - print(f"✗ 淡入主题失败: {e}") - import traceback - traceback.print_exc() - - def get_active_themes(self) -> List[str]: - """ - 获取活动主题列表 - - Returns: - 活动主题名称列表 - """ - return list(self.active_themes.keys()) - - def get_theme_status(self, theme_name: str) -> Dict[str, Any]: - """ - 获取主题状态 - - Args: - theme_name: 主题名称 - - Returns: - 主题状态字典 - """ - if theme_name in self.active_themes: - theme_instance = self.active_themes[theme_name] - return { - 'name': theme_name, - 'state': theme_instance['state'], - 'volume': theme_instance['volume'], - 'layers': list(theme_instance['layers'].keys()), - 'start_time': theme_instance['start_time'], - 'bpm': theme_instance['bpm'], - 'key': theme_instance['key'], - 'mood': theme_instance['mood'] - } - else: - return { - 'name': theme_name, - 'state': 'stopped', - 'volume': 0.0, - 'layers': [], - 'start_time': 0.0, - 'bpm': 0, - 'key': '', - 'mood': '' - } - - def get_layer_status(self, layer_name: str) -> Dict[str, Any]: - """ - 获取层状态 - - Args: - layer_name: 层名称 - - Returns: - 层状态字典 - """ - return { - 'name': layer_name, - 'volume': self.layer_volumes.get(layer_name, 0.0), - 'loaded': layer_name in self.layers, - 'active': any( - layer_name in theme['layers'] - for theme in self.active_themes.values() - ) - } - - def update(self, dt: float): - """ - 更新音乐管理器状态 - - Args: - dt: 时间增量 - """ - update_start_time = time.time() - - # 更新淡入淡出效果 - current_time = time.time() - themes_to_remove = [] - - for theme_name, theme_instance in self.active_themes.items(): - if theme_instance['state'] in ['fading_in', 'fading_out']: - fade_start_time = theme_instance.get('fade_start_time', current_time) - fade_duration = theme_instance.get('fade_duration', 1.0) - fade_start_volume = theme_instance.get('fade_start_volume', 0.0) - elapsed_time = current_time - fade_start_time - - if elapsed_time >= fade_duration: - # 淡入淡出完成 - if theme_instance['state'] == 'fading_out': - themes_to_remove.append(theme_name) - theme_instance['state'] = 'stopped' - self.set_theme_volume(theme_name, 0.0) - else: # fading_in - theme_instance['state'] = 'playing' - target_volume = theme_instance.get('target_volume', 1.0) - self.set_theme_volume(theme_name, target_volume) - else: - # 计算当前音量 - progress = elapsed_time / fade_duration - if theme_instance['state'] == 'fading_out': - current_volume = fade_start_volume * (1.0 - progress) - else: # fading_in - target_volume = theme_instance.get('target_volume', 1.0) - current_volume = fade_start_volume + (target_volume - fade_start_volume) * progress - - self.set_theme_volume(theme_name, current_volume) - - # 移除已停止的主题 - for theme_name in themes_to_remove: - if theme_name in self.active_themes: - del self.active_themes[theme_name] - - self.stats['themes_active'] = len(self.active_themes) - self.stats['layers_active'] = sum(len(theme['layers']) for theme in self.active_themes.values()) - - # 更新音频播放位置 - self._update_playback_positions(dt) - - # 更新性能统计 - self.stats['processing_time'] += (time.time() - update_start_time) - - def _update_playback_positions(self, dt: float): - """ - 更新音频播放位置 - - Args: - dt: 时间增量 - """ - # 更新播放状态 - self.playback_state['current_position'] += dt - if self.playback_state['current_position'] > self.playback_state['total_duration']: - self.playback_state['current_position'] = 0.0 - self.playback_state['loop_count'] += 1 - - def cleanup(self): - """清理所有资源""" - self.stop_all() - self.themes.clear() - self.layers.clear() - self.active_themes.clear() - self.layer_volumes.clear() - self.sound_cache.clear() - self.audio_buffers.clear() - self.fade_buffers.clear() - - self.stats = { - 'themes_loaded': 0, - 'themes_active': 0, - 'layers_active': 0, - 'sounds_played': 0, - 'processing_time': 0.0, - 'memory_usage': 0 - } - - print("✓ 音乐管理器资源已清理") - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def preload_theme(self, theme_name: str) -> bool: - """ - 预加载音乐主题 - - Args: - theme_name: 主题名称 - - Returns: - 是否预加载成功 - """ - try: - if theme_name not in self.themes: - print(f"✗ 音乐主题不存在: {theme_name}") - return False - - theme_data = self.themes[theme_name] - layers = theme_data.get('layers', []) - - # 预加载所有层 - for layer_name in layers: - if layer_name not in self.sound_cache: - # 在实际实现中,这里会加载音频文件到缓存 - self.sound_cache[layer_name] = None # 占位符 - # 同时预加载层数据 - if layer_name not in self.layers: - self.load_layer(layer_name) - - print(f"✓ 音乐主题预加载成功: {theme_name}") - return True - - except Exception as e: - print(f"✗ 预加载音乐主题失败: {e}") - import traceback - traceback.print_exc() - return False - - def set_crossfade_duration(self, duration: float): - """ - 设置交叉淡入淡出持续时间 - - Args: - duration: 持续时间(秒) - """ - self.crossfade_duration = max(0.1, duration) - print(f"✓ 交叉淡入淡出持续时间设置为: {self.crossfade_duration:.2f}秒") - - def get_master_volume(self) -> float: - """ - 获取主音量 - - Returns: - 主音量 (0.0-1.0) - """ - return self.master_volume - - def is_theme_playing(self, theme_name: str) -> bool: - """ - 检查主题是否正在播放 - - Args: - theme_name: 主题名称 - - Returns: - 是否正在播放 - """ - return theme_name in self.active_themes - - def set_equalizer(self, low_gain: float = 0.0, mid_gain: float = 0.0, high_gain: float = 0.0): - """ - 设置均衡器参数 - - Args: - low_gain: 低频增益 (-24.0 to 24.0 dB) - mid_gain: 中频增益 (-24.0 to 24.0 dB) - high_gain: 高频增益 (-24.0 to 24.0 dB) - """ - self.equalizer_settings['low_gain'] = max(-24.0, min(24.0, low_gain)) - self.equalizer_settings['mid_gain'] = max(-24.0, min(24.0, mid_gain)) - self.equalizer_settings['high_gain'] = max(-24.0, min(24.0, high_gain)) - print(f"✓ 均衡器设置: 低频={low_gain:.1f}dB, 中频={mid_gain:.1f}dB, 高频={high_gain:.1f}dB") - - def get_audio_data(self, layer_name: str, num_samples: int = None) -> np.ndarray: - """ - 获取音频数据 - - Args: - layer_name: 层名称 - num_samples: 样本数量 - - Returns: - 音频数据数组 - """ - if layer_name in self.audio_buffers: - buffer = self.audio_buffers[layer_name] - if num_samples is None: - return buffer.copy() - else: - return buffer[:min(num_samples, len(buffer))].copy() - else: - return np.zeros(num_samples if num_samples else self.buffer_size) - - def set_layer_position(self, layer_name: str, position: float): - """ - 设置层播放位置 - - Args: - layer_name: 层名称 - position: 播放位置(秒) - """ - if layer_name in self.active_themes: - for theme in self.active_themes.values(): - if layer_name in theme['layers']: - theme['layers'][layer_name]['position'] = max(0.0, position) - print(f"✓ 层 {layer_name} 播放位置设置为: {position:.2f}秒") - return - print(f"⚠ 层 {layer_name} 未在播放,无法设置位置") - - def get_layer_position(self, layer_name: str) -> float: - """ - 获取层播放位置 - - Args: - layer_name: 层名称 - - Returns: - 播放位置(秒) - """ - for theme in self.active_themes.values(): - if layer_name in theme['layers']: - return theme['layers'][layer_name].get('position', 0.0) - return 0.0 - - def set_layer_loop(self, layer_name: str, loop: bool): - """ - 设置层循环播放 - - Args: - layer_name: 层名称 - loop: 是否循环播放 - """ - if layer_name in self.layers: - self.layers[layer_name]['loop'] = loop - print(f"✓ 层 {layer_name} 循环播放已{'启用' if loop else '禁用'}") - else: - print(f"✗ 层 {layer_name} 不存在") - - def is_layer_looping(self, layer_name: str) -> bool: - """ - 检查层是否循环播放 - - Args: - layer_name: 层名称 - - Returns: - 是否循环播放 - """ - return self.layers.get(layer_name, {}).get('loop', False) - - def set_theme_bpm(self, theme_name: str, bpm: float): - """ - 设置主题BPM - - Args: - theme_name: 主题名称 - bpm: 每分钟节拍数 - """ - if theme_name in self.themes: - self.themes[theme_name]['bpm'] = max(30, min(300, bpm)) - if theme_name in self.active_themes: - self.active_themes[theme_name]['bpm'] = bpm - print(f"✓ 主题 {theme_name} BPM设置为: {bpm}") - else: - print(f"✗ 主题 {theme_name} 不存在") - - def get_theme_bpm(self, theme_name: str) -> float: - """ - 获取主题BPM - - Args: - theme_name: 主题名称 - - Returns: - BPM值 - """ - theme_bpm = self.themes.get(theme_name, {}).get('bpm', 120.0) - if theme_name in self.active_themes: - return self.active_themes[theme_name].get('bpm', theme_bpm) - return theme_bpm - - def set_theme_key(self, theme_name: str, key: str): - """ - 设置主题调号 - - Args: - theme_name: 主题名称 - key: 调号 - """ - valid_keys = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] - if key.upper() not in valid_keys: - print(f"✗ 无效的调号: {key}") - return - - if theme_name in self.themes: - self.themes[theme_name]['key'] = key.upper() - if theme_name in self.active_themes: - self.active_themes[theme_name]['key'] = key.upper() - print(f"✓ 主题 {theme_name} 调号设置为: {key.upper()}") - else: - print(f"✗ 主题 {theme_name} 不存在") - - def get_theme_key(self, theme_name: str) -> str: - """ - 获取主题调号 - - Args: - theme_name: 主题名称 - - Returns: - 调号 - """ - theme_key = self.themes.get(theme_name, {}).get('key', 'C') - if theme_name in self.active_themes: - return self.active_themes[theme_name].get('key', theme_key) - return theme_key - - def set_theme_mood(self, theme_name: str, mood: str): - """ - 设置主题情绪 - - Args: - theme_name: 主题名称 - mood: 情绪标签 - """ - valid_moods = ['calm', 'tense', 'epic', 'melancholic', 'joyful', 'mysterious', 'heroic', 'romantic'] - if mood.lower() not in valid_moods: - print(f"⚠ 未知的情绪标签: {mood},将使用自定义标签") - - if theme_name in self.themes: - self.themes[theme_name]['mood'] = mood.lower() - if theme_name in self.active_themes: - self.active_themes[theme_name]['mood'] = mood.lower() - print(f"✓ 主题 {theme_name} 情绪设置为: {mood.lower()}") - else: - print(f"✗ 主题 {theme_name} 不存在") - - def get_theme_mood(self, theme_name: str) -> str: - """ - 获取主题情绪 - - Args: - theme_name: 主题名称 - - Returns: - 情绪标签 - """ - theme_mood = self.themes.get(theme_name, {}).get('mood', 'neutral') - if theme_name in self.active_themes: - return self.active_themes[theme_name].get('mood', theme_mood) - return theme_mood - - def get_playback_state(self) -> Dict[str, Any]: - """ - 获取播放状态 - - Returns: - 播放状态字典 - """ - return self.playback_state.copy() - - def pause_theme(self, theme_name: str) -> bool: - """ - 暂停主题播放 - - Args: - theme_name: 主题名称 - - Returns: - 是否暂停成功 - """ - if theme_name in self.active_themes: - theme_instance = self.active_themes[theme_name] - if theme_instance['state'] == 'playing': - theme_instance['state'] = 'paused' - theme_instance['pause_time'] = time.time() - print(f"✓ 主题 {theme_name} 已暂停") - return True - else: - print(f"⚠ 主题 {theme_name} 不在播放状态") - return False - else: - print(f"✗ 主题 {theme_name} 未在播放") - return False - - def resume_theme(self, theme_name: str) -> bool: - """ - 恢复主题播放 - - Args: - theme_name: 主题名称 - - Returns: - 是否恢复成功 - """ - if theme_name in self.active_themes: - theme_instance = self.active_themes[theme_name] - if theme_instance['state'] == 'paused': - theme_instance['state'] = 'playing' - # 计算暂停时间并调整开始时间 - if 'pause_time' in theme_instance: - pause_duration = time.time() - theme_instance['pause_time'] - if 'fade_start_time' in theme_instance: - theme_instance['fade_start_time'] += pause_duration - print(f"✓ 主题 {theme_name} 已恢复播放") - return True - else: - print(f"⚠ 主题 {theme_name} 不在暂停状态") - return False - else: - print(f"✗ 主题 {theme_name} 未在播放") - return False - - def set_sample_rate(self, sample_rate: int): - """ - 设置采样率 - - Args: - sample_rate: 采样率 (Hz) - """ - old_sample_rate = self.sample_rate - self.sample_rate = sample_rate - print(f"✓ 音乐管理器采样率已从 {old_sample_rate} 更新为: {sample_rate} Hz") - - # 重新生成所有音频缓冲区 - for layer_name in self.audio_buffers.keys(): - if layer_name in self.layers: +""" +音乐管理器 +负责音乐主题的加载、播放和控制 +""" + +import uuid +import math +import time +import numpy as np +from typing import Dict, List, Any, Optional +from panda3d.core import AudioSound +import scipy.signal as signal + +class MusicManager: + """ + 音乐管理器 + 负责音乐主题的加载、播放和控制 + """ + + def __init__(self, plugin): + """ + 初始化音乐管理器 + + Args: + plugin: 动态音乐系统插件实例 + """ + self.plugin = plugin + self.themes: Dict[str, Dict[str, Any]] = {} + self.layers: Dict[str, Dict[str, Any]] = {} + self.active_themes: Dict[str, Dict[str, Any]] = {} + self.layer_volumes: Dict[str, float] = {} + self.sound_cache: Dict[str, AudioSound] = {} + + self.stats = { + 'themes_loaded': 0, + 'themes_active': 0, + 'layers_active': 0, + 'sounds_played': 0, + 'processing_time': 0.0, + 'memory_usage': 0 + } + + self.master_volume = 1.0 + self.crossfade_duration = 2.0 + self.buffer_size = plugin.buffer_size if plugin else 4096 + self.sample_rate = 44100 + + # 音乐处理参数 + self.equalizer_settings = { + 'low_freq': 100.0, + 'mid_freq': 1000.0, + 'high_freq': 8000.0, + 'low_gain': 0.0, + 'mid_gain': 0.0, + 'high_gain': 0.0 + } + + # 音乐播放状态 + self.playback_state = { + 'current_position': 0.0, + 'total_duration': 0.0, + 'is_paused': False, + 'loop_count': 0 + } + + # 音乐缓冲区 + self.audio_buffers = {} + self.fade_buffers = {} + + # 初始化音乐管理器 + self._initialize_music_manager() + + def _initialize_music_manager(self): + """初始化音乐管理器""" + print("✓ 音乐管理器初始化完成") + + def load_theme(self, theme_name: str, theme_data: Dict[str, Any]) -> bool: + """ + 加载音乐主题 + + Args: + theme_name: 主题名称 + theme_data: 主题数据 + + Returns: + 是否加载成功 + """ + try: + # 存储主题数据 + self.themes[theme_name] = theme_data.copy() + + # 加载主题中的所有层 + layers = theme_data.get('layers', []) + for layer_name in layers: + if layer_name not in self.layers: + self.load_layer(layer_name) + + self.stats['themes_loaded'] += 1 + print(f"✓ 音乐主题加载成功: {theme_name}") + return True + + except Exception as e: + print(f"✗ 加载音乐主题失败: {e}") + import traceback + traceback.print_exc() + return False + + def load_layer(self, layer_name: str, layer_data: Dict[str, Any] = None) -> bool: + """ + 加载音乐层 + + Args: + layer_name: 层名称 + layer_data: 层数据(可选) + + Returns: + 是否加载成功 + """ + try: + # 如果没有提供层数据,创建默认数据 + if layer_data is None: + layer_data = { + 'volume': 1.0, + 'loop': True, + 'priority': 1, + 'filepath': f"music/layers/{layer_name}.wav", + 'format': 'wav', + 'channels': 2, + 'sample_rate': 44100, + 'bit_depth': 16 + } + + # 存储层数据 + self.layers[layer_name] = layer_data.copy() + + # 设置默认音量 + self.layer_volumes[layer_name] = layer_data.get('volume', 1.0) + + # 创建音频缓冲区 + buffer_size = self.buffer_size + self.audio_buffers[layer_name] = np.zeros(buffer_size) + + print(f"✓ 音乐层加载成功: {layer_name}") + return True + + except Exception as e: + print(f"✗ 加载音乐层失败: {e}") + import traceback + traceback.print_exc() + return False + + def play_theme(self, theme_name: str, force: bool = False) -> bool: + """ + 播放音乐主题 + + Args: + theme_name: 主题名称 + force: 是否强制播放(跳过过渡) + + Returns: + 是否播放成功 + """ + try: + if theme_name not in self.themes: + print(f"✗ 音乐主题不存在: {theme_name}") + return False + + theme_data = self.themes[theme_name] + layers = theme_data.get('layers', []) + + # 创建主题实例 + theme_instance = { + 'name': theme_name, + 'layers': {}, + 'start_time': time.time(), + 'volume': 1.0, + 'state': 'playing', + 'bpm': theme_data.get('bpm', 120), + 'key': theme_data.get('key', 'C'), + 'mood': theme_data.get('mood', 'neutral'), + 'loop': theme_data.get('loop', True), + 'fade_in_progress': 0.0, + 'fade_in_duration': 0.0 + } + + # 加载并播放所有层 + for layer_name in layers: + if self.play_layer(layer_name): + theme_instance['layers'][layer_name] = { + 'name': layer_name, + 'volume': self.layer_volumes.get(layer_name, 1.0), + 'state': 'playing', + 'position': 0.0, + 'duration': 0.0 + } + + # 存储活动主题 + self.active_themes[theme_name] = theme_instance + self.stats['themes_active'] = len(self.active_themes) + self.stats['layers_active'] = sum(len(theme['layers']) for theme in self.active_themes.values()) + + print(f"✓ 音乐主题开始播放: {theme_name}") + return True + + except Exception as e: + print(f"✗ 播放音乐主题失败: {e}") + import traceback + traceback.print_exc() + return False + + def play_layer(self, layer_name: str) -> bool: + """ + 播放音乐层 + + Args: + layer_name: 层名称 + + Returns: + 是否播放成功 + """ + try: + if layer_name not in self.layers: + # 如果层不存在,创建一个虚拟层 + self.load_layer(layer_name) + + # 模拟播放音频文件 + # 在实际实现中,这里会使用Panda3D的音频系统 + layer_data = self.layers[layer_name] + + # 初始化音频缓冲区 + buffer_size = self.buffer_size + if layer_name not in self.audio_buffers: + self.audio_buffers[layer_name] = np.zeros(buffer_size) + + # 生成测试音频信号 + self._generate_test_audio(layer_name, layer_data) + + print(f"✓ 音乐层开始播放: {layer_name}") + self.stats['sounds_played'] += 1 + return True + + except Exception as e: + print(f"✗ 播放音乐层失败: {e}") + import traceback + traceback.print_exc() + return False + + def _generate_test_audio(self, layer_name: str, layer_data: Dict[str, Any]): + """ + 生成测试音频信号 + + Args: + layer_name: 层名称 + layer_data: 层数据 + """ + try: + # 获取层的音频参数 + sample_rate = layer_data.get('sample_rate', self.sample_rate) + duration = 10.0 # 10秒测试音频 + samples = int(sample_rate * duration) + + # 生成不同频率的正弦波组合来模拟音乐层 + t = np.linspace(0, duration, samples, False) + + # 基频基于层名称和参数 + base_freq = 220.0 # A3音符 + if 'bass' in layer_name.lower(): + base_freq = 110.0 # A2音符(低音) + elif 'high' in layer_name.lower(): + base_freq = 440.0 # A4音符(高音) + elif 'melody' in layer_name.lower(): + base_freq = 330.0 # E4音符(中音) + + # 生成音频信号 + audio_signal = np.sin(2 * np.pi * base_freq * t) + + # 添加谐波丰富音色 + harmonics = [0.5, 0.3, 0.2, 0.1] + for i, harmonic in enumerate(harmonics, 2): + audio_signal += harmonic * np.sin(2 * np.pi * base_freq * i * t) + + # 应用包络(模拟音符起音和释放) + envelope = np.ones_like(t) + attack_time = int(0.1 * sample_rate) # 0.1秒起音 + release_time = int(0.2 * sample_rate) # 0.2秒释放 + + if len(envelope) > attack_time: + envelope[:attack_time] = np.linspace(0, 1, attack_time) + if len(envelope) > release_time: + envelope[-release_time:] = np.linspace(1, 0, release_time) + + audio_signal *= envelope + + # 应用层音量 + volume = self.layer_volumes.get(layer_name, 1.0) + audio_signal *= volume + + # 存储到缓冲区(循环存储) + buffer = self.audio_buffers[layer_name] + for i in range(min(len(buffer), len(audio_signal))): + buffer[i] = audio_signal[i % len(audio_signal)] + + except Exception as e: + print(f"✗ 生成测试音频失败: {e}") + + def stop_theme(self, theme_name: str) -> bool: + """ + 停止音乐主题 + + Args: + theme_name: 主题名称 + + Returns: + 是否停止成功 + """ + try: + if theme_name not in self.active_themes: + print(f"⚠ 音乐主题未在播放: {theme_name}") + return True + + # 停止主题中的所有层 + theme_instance = self.active_themes[theme_name] + for layer_name in theme_instance['layers']: + self.stop_layer(layer_name) + + # 移除活动主题 + del self.active_themes[theme_name] + self.stats['themes_active'] = len(self.active_themes) + self.stats['layers_active'] = sum(len(theme['layers']) for theme in self.active_themes.values()) + + print(f"✓ 音乐主题已停止: {theme_name}") + return True + + except Exception as e: + print(f"✗ 停止音乐主题失败: {e}") + import traceback + traceback.print_exc() + return False + + def stop_layer(self, layer_name: str) -> bool: + """ + 停止音乐层 + + Args: + layer_name: 层名称 + + Returns: + 是否停止成功 + """ + try: + # 模拟停止音频文件 + # 在实际实现中,这里会使用Panda3D的音频系统 + if layer_name in self.audio_buffers: + # 清空音频缓冲区 + self.audio_buffers[layer_name].fill(0.0) + + print(f"✓ 音乐层已停止: {layer_name}") + return True + + except Exception as e: + print(f"✗ 停止音乐层失败: {e}") + import traceback + traceback.print_exc() + return False + + def stop_all(self): + """停止所有音乐""" + try: + # 停止所有活动主题 + active_themes = list(self.active_themes.keys()) + for theme_name in active_themes: + self.stop_theme(theme_name) + + print("✓ 所有音乐已停止") + + except Exception as e: + print(f"✗ 停止所有音乐失败: {e}") + import traceback + traceback.print_exc() + + def set_theme_volume(self, theme_name: str, volume: float): + """ + 设置主题音量 + + Args: + theme_name: 主题名称 + volume: 音量值 (0.0-1.0) + """ + volume = max(0.0, min(1.0, volume)) + + if theme_name in self.active_themes: + theme_instance = self.active_themes[theme_name] + theme_instance['volume'] = volume + + # 更新主题中所有层的音量 + for layer_name in theme_instance['layers']: + self.set_layer_volume(layer_name, volume) + + print(f"✓ 主题 {theme_name} 音量设置为: {volume:.2f}") + + def set_layer_volume(self, layer_name: str, volume: float): + """ + 设置层音量 + + Args: + layer_name: 层名称 + volume: 音量值 (0.0-1.0) + """ + volume = max(0.0, min(1.0, volume)) + self.layer_volumes[layer_name] = volume + + # 在实际实现中,这里会更新音频实例的音量 + # 对于测试音频,我们调整缓冲区的音量 + if layer_name in self.audio_buffers: + # 这里简化处理,实际应用中会更复杂 + pass + + print(f"✓ 层 {layer_name} 音量设置为: {volume:.2f}") + + def set_master_volume(self, volume: float): + """ + 设置主音量 + + Args: + volume: 主音量 (0.0-1.0) + """ + self.master_volume = max(0.0, min(1.0, volume)) + print(f"✓ 主音量设置为: {self.master_volume:.2f}") + + def fade_out_theme(self, theme_name: str, duration: float = 2.0): + """ + 淡出音乐主题 + + Args: + theme_name: 主题名称 + duration: 淡出持续时间(秒) + """ + try: + if theme_name in self.active_themes: + theme_instance = self.active_themes[theme_name] + theme_instance['state'] = 'fading_out' + theme_instance['fade_start_time'] = time.time() + theme_instance['fade_duration'] = duration + theme_instance['fade_start_volume'] = theme_instance['volume'] + + print(f"✓ 主题 {theme_name} 开始淡出,持续时间: {duration:.2f}秒") + + except Exception as e: + print(f"✗ 淡出主题失败: {e}") + import traceback + traceback.print_exc() + + def fade_in_theme(self, theme_name: str, duration: float = 2.0): + """ + 淡入音乐主题 + + Args: + theme_name: 主题名称 + duration: 淡入持续时间(秒) + """ + try: + if theme_name in self.themes and theme_name not in self.active_themes: + # 先播放主题 + self.play_theme(theme_name) + + # 设置淡入状态 + if theme_name in self.active_themes: + theme_instance = self.active_themes[theme_name] + theme_instance['state'] = 'fading_in' + theme_instance['fade_start_time'] = time.time() + theme_instance['fade_duration'] = duration + theme_instance['fade_start_volume'] = 0.0 + theme_instance['target_volume'] = theme_instance['volume'] + theme_instance['volume'] = 0.0 + self.set_theme_volume(theme_name, 0.0) + + print(f"✓ 主题 {theme_name} 开始淡入,持续时间: {duration:.2f}秒") + + except Exception as e: + print(f"✗ 淡入主题失败: {e}") + import traceback + traceback.print_exc() + + def get_active_themes(self) -> List[str]: + """ + 获取活动主题列表 + + Returns: + 活动主题名称列表 + """ + return list(self.active_themes.keys()) + + def get_theme_status(self, theme_name: str) -> Dict[str, Any]: + """ + 获取主题状态 + + Args: + theme_name: 主题名称 + + Returns: + 主题状态字典 + """ + if theme_name in self.active_themes: + theme_instance = self.active_themes[theme_name] + return { + 'name': theme_name, + 'state': theme_instance['state'], + 'volume': theme_instance['volume'], + 'layers': list(theme_instance['layers'].keys()), + 'start_time': theme_instance['start_time'], + 'bpm': theme_instance['bpm'], + 'key': theme_instance['key'], + 'mood': theme_instance['mood'] + } + else: + return { + 'name': theme_name, + 'state': 'stopped', + 'volume': 0.0, + 'layers': [], + 'start_time': 0.0, + 'bpm': 0, + 'key': '', + 'mood': '' + } + + def get_layer_status(self, layer_name: str) -> Dict[str, Any]: + """ + 获取层状态 + + Args: + layer_name: 层名称 + + Returns: + 层状态字典 + """ + return { + 'name': layer_name, + 'volume': self.layer_volumes.get(layer_name, 0.0), + 'loaded': layer_name in self.layers, + 'active': any( + layer_name in theme['layers'] + for theme in self.active_themes.values() + ) + } + + def update(self, dt: float): + """ + 更新音乐管理器状态 + + Args: + dt: 时间增量 + """ + update_start_time = time.time() + + # 更新淡入淡出效果 + current_time = time.time() + themes_to_remove = [] + + for theme_name, theme_instance in self.active_themes.items(): + if theme_instance['state'] in ['fading_in', 'fading_out']: + fade_start_time = theme_instance.get('fade_start_time', current_time) + fade_duration = theme_instance.get('fade_duration', 1.0) + fade_start_volume = theme_instance.get('fade_start_volume', 0.0) + elapsed_time = current_time - fade_start_time + + if elapsed_time >= fade_duration: + # 淡入淡出完成 + if theme_instance['state'] == 'fading_out': + themes_to_remove.append(theme_name) + theme_instance['state'] = 'stopped' + self.set_theme_volume(theme_name, 0.0) + else: # fading_in + theme_instance['state'] = 'playing' + target_volume = theme_instance.get('target_volume', 1.0) + self.set_theme_volume(theme_name, target_volume) + else: + # 计算当前音量 + progress = elapsed_time / fade_duration + if theme_instance['state'] == 'fading_out': + current_volume = fade_start_volume * (1.0 - progress) + else: # fading_in + target_volume = theme_instance.get('target_volume', 1.0) + current_volume = fade_start_volume + (target_volume - fade_start_volume) * progress + + self.set_theme_volume(theme_name, current_volume) + + # 移除已停止的主题 + for theme_name in themes_to_remove: + if theme_name in self.active_themes: + del self.active_themes[theme_name] + + self.stats['themes_active'] = len(self.active_themes) + self.stats['layers_active'] = sum(len(theme['layers']) for theme in self.active_themes.values()) + + # 更新音频播放位置 + self._update_playback_positions(dt) + + # 更新性能统计 + self.stats['processing_time'] += (time.time() - update_start_time) + + def _update_playback_positions(self, dt: float): + """ + 更新音频播放位置 + + Args: + dt: 时间增量 + """ + # 更新播放状态 + self.playback_state['current_position'] += dt + if self.playback_state['current_position'] > self.playback_state['total_duration']: + self.playback_state['current_position'] = 0.0 + self.playback_state['loop_count'] += 1 + + def cleanup(self): + """清理所有资源""" + self.stop_all() + self.themes.clear() + self.layers.clear() + self.active_themes.clear() + self.layer_volumes.clear() + self.sound_cache.clear() + self.audio_buffers.clear() + self.fade_buffers.clear() + + self.stats = { + 'themes_loaded': 0, + 'themes_active': 0, + 'layers_active': 0, + 'sounds_played': 0, + 'processing_time': 0.0, + 'memory_usage': 0 + } + + print("✓ 音乐管理器资源已清理") + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def preload_theme(self, theme_name: str) -> bool: + """ + 预加载音乐主题 + + Args: + theme_name: 主题名称 + + Returns: + 是否预加载成功 + """ + try: + if theme_name not in self.themes: + print(f"✗ 音乐主题不存在: {theme_name}") + return False + + theme_data = self.themes[theme_name] + layers = theme_data.get('layers', []) + + # 预加载所有层 + for layer_name in layers: + if layer_name not in self.sound_cache: + # 在实际实现中,这里会加载音频文件到缓存 + self.sound_cache[layer_name] = None # 占位符 + # 同时预加载层数据 + if layer_name not in self.layers: + self.load_layer(layer_name) + + print(f"✓ 音乐主题预加载成功: {theme_name}") + return True + + except Exception as e: + print(f"✗ 预加载音乐主题失败: {e}") + import traceback + traceback.print_exc() + return False + + def set_crossfade_duration(self, duration: float): + """ + 设置交叉淡入淡出持续时间 + + Args: + duration: 持续时间(秒) + """ + self.crossfade_duration = max(0.1, duration) + print(f"✓ 交叉淡入淡出持续时间设置为: {self.crossfade_duration:.2f}秒") + + def get_master_volume(self) -> float: + """ + 获取主音量 + + Returns: + 主音量 (0.0-1.0) + """ + return self.master_volume + + def is_theme_playing(self, theme_name: str) -> bool: + """ + 检查主题是否正在播放 + + Args: + theme_name: 主题名称 + + Returns: + 是否正在播放 + """ + return theme_name in self.active_themes + + def set_equalizer(self, low_gain: float = 0.0, mid_gain: float = 0.0, high_gain: float = 0.0): + """ + 设置均衡器参数 + + Args: + low_gain: 低频增益 (-24.0 to 24.0 dB) + mid_gain: 中频增益 (-24.0 to 24.0 dB) + high_gain: 高频增益 (-24.0 to 24.0 dB) + """ + self.equalizer_settings['low_gain'] = max(-24.0, min(24.0, low_gain)) + self.equalizer_settings['mid_gain'] = max(-24.0, min(24.0, mid_gain)) + self.equalizer_settings['high_gain'] = max(-24.0, min(24.0, high_gain)) + print(f"✓ 均衡器设置: 低频={low_gain:.1f}dB, 中频={mid_gain:.1f}dB, 高频={high_gain:.1f}dB") + + def get_audio_data(self, layer_name: str, num_samples: int = None) -> np.ndarray: + """ + 获取音频数据 + + Args: + layer_name: 层名称 + num_samples: 样本数量 + + Returns: + 音频数据数组 + """ + if layer_name in self.audio_buffers: + buffer = self.audio_buffers[layer_name] + if num_samples is None: + return buffer.copy() + else: + return buffer[:min(num_samples, len(buffer))].copy() + else: + return np.zeros(num_samples if num_samples else self.buffer_size) + + def set_layer_position(self, layer_name: str, position: float): + """ + 设置层播放位置 + + Args: + layer_name: 层名称 + position: 播放位置(秒) + """ + if layer_name in self.active_themes: + for theme in self.active_themes.values(): + if layer_name in theme['layers']: + theme['layers'][layer_name]['position'] = max(0.0, position) + print(f"✓ 层 {layer_name} 播放位置设置为: {position:.2f}秒") + return + print(f"⚠ 层 {layer_name} 未在播放,无法设置位置") + + def get_layer_position(self, layer_name: str) -> float: + """ + 获取层播放位置 + + Args: + layer_name: 层名称 + + Returns: + 播放位置(秒) + """ + for theme in self.active_themes.values(): + if layer_name in theme['layers']: + return theme['layers'][layer_name].get('position', 0.0) + return 0.0 + + def set_layer_loop(self, layer_name: str, loop: bool): + """ + 设置层循环播放 + + Args: + layer_name: 层名称 + loop: 是否循环播放 + """ + if layer_name in self.layers: + self.layers[layer_name]['loop'] = loop + print(f"✓ 层 {layer_name} 循环播放已{'启用' if loop else '禁用'}") + else: + print(f"✗ 层 {layer_name} 不存在") + + def is_layer_looping(self, layer_name: str) -> bool: + """ + 检查层是否循环播放 + + Args: + layer_name: 层名称 + + Returns: + 是否循环播放 + """ + return self.layers.get(layer_name, {}).get('loop', False) + + def set_theme_bpm(self, theme_name: str, bpm: float): + """ + 设置主题BPM + + Args: + theme_name: 主题名称 + bpm: 每分钟节拍数 + """ + if theme_name in self.themes: + self.themes[theme_name]['bpm'] = max(30, min(300, bpm)) + if theme_name in self.active_themes: + self.active_themes[theme_name]['bpm'] = bpm + print(f"✓ 主题 {theme_name} BPM设置为: {bpm}") + else: + print(f"✗ 主题 {theme_name} 不存在") + + def get_theme_bpm(self, theme_name: str) -> float: + """ + 获取主题BPM + + Args: + theme_name: 主题名称 + + Returns: + BPM值 + """ + theme_bpm = self.themes.get(theme_name, {}).get('bpm', 120.0) + if theme_name in self.active_themes: + return self.active_themes[theme_name].get('bpm', theme_bpm) + return theme_bpm + + def set_theme_key(self, theme_name: str, key: str): + """ + 设置主题调号 + + Args: + theme_name: 主题名称 + key: 调号 + """ + valid_keys = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] + if key.upper() not in valid_keys: + print(f"✗ 无效的调号: {key}") + return + + if theme_name in self.themes: + self.themes[theme_name]['key'] = key.upper() + if theme_name in self.active_themes: + self.active_themes[theme_name]['key'] = key.upper() + print(f"✓ 主题 {theme_name} 调号设置为: {key.upper()}") + else: + print(f"✗ 主题 {theme_name} 不存在") + + def get_theme_key(self, theme_name: str) -> str: + """ + 获取主题调号 + + Args: + theme_name: 主题名称 + + Returns: + 调号 + """ + theme_key = self.themes.get(theme_name, {}).get('key', 'C') + if theme_name in self.active_themes: + return self.active_themes[theme_name].get('key', theme_key) + return theme_key + + def set_theme_mood(self, theme_name: str, mood: str): + """ + 设置主题情绪 + + Args: + theme_name: 主题名称 + mood: 情绪标签 + """ + valid_moods = ['calm', 'tense', 'epic', 'melancholic', 'joyful', 'mysterious', 'heroic', 'romantic'] + if mood.lower() not in valid_moods: + print(f"⚠ 未知的情绪标签: {mood},将使用自定义标签") + + if theme_name in self.themes: + self.themes[theme_name]['mood'] = mood.lower() + if theme_name in self.active_themes: + self.active_themes[theme_name]['mood'] = mood.lower() + print(f"✓ 主题 {theme_name} 情绪设置为: {mood.lower()}") + else: + print(f"✗ 主题 {theme_name} 不存在") + + def get_theme_mood(self, theme_name: str) -> str: + """ + 获取主题情绪 + + Args: + theme_name: 主题名称 + + Returns: + 情绪标签 + """ + theme_mood = self.themes.get(theme_name, {}).get('mood', 'neutral') + if theme_name in self.active_themes: + return self.active_themes[theme_name].get('mood', theme_mood) + return theme_mood + + def get_playback_state(self) -> Dict[str, Any]: + """ + 获取播放状态 + + Returns: + 播放状态字典 + """ + return self.playback_state.copy() + + def pause_theme(self, theme_name: str) -> bool: + """ + 暂停主题播放 + + Args: + theme_name: 主题名称 + + Returns: + 是否暂停成功 + """ + if theme_name in self.active_themes: + theme_instance = self.active_themes[theme_name] + if theme_instance['state'] == 'playing': + theme_instance['state'] = 'paused' + theme_instance['pause_time'] = time.time() + print(f"✓ 主题 {theme_name} 已暂停") + return True + else: + print(f"⚠ 主题 {theme_name} 不在播放状态") + return False + else: + print(f"✗ 主题 {theme_name} 未在播放") + return False + + def resume_theme(self, theme_name: str) -> bool: + """ + 恢复主题播放 + + Args: + theme_name: 主题名称 + + Returns: + 是否恢复成功 + """ + if theme_name in self.active_themes: + theme_instance = self.active_themes[theme_name] + if theme_instance['state'] == 'paused': + theme_instance['state'] = 'playing' + # 计算暂停时间并调整开始时间 + if 'pause_time' in theme_instance: + pause_duration = time.time() - theme_instance['pause_time'] + if 'fade_start_time' in theme_instance: + theme_instance['fade_start_time'] += pause_duration + print(f"✓ 主题 {theme_name} 已恢复播放") + return True + else: + print(f"⚠ 主题 {theme_name} 不在暂停状态") + return False + else: + print(f"✗ 主题 {theme_name} 未在播放") + return False + + def set_sample_rate(self, sample_rate: int): + """ + 设置采样率 + + Args: + sample_rate: 采样率 (Hz) + """ + old_sample_rate = self.sample_rate + self.sample_rate = sample_rate + print(f"✓ 音乐管理器采样率已从 {old_sample_rate} 更新为: {sample_rate} Hz") + + # 重新生成所有音频缓冲区 + for layer_name in self.audio_buffers.keys(): + if layer_name in self.layers: self._generate_test_audio(layer_name, self.layers[layer_name]) \ No newline at end of file diff --git a/plugins/user/dynamic_music_system/effects/effect_processor.py b/plugins/user/dynamic_music_system/effects/effect_processor.py index 55fed114..473ea34c 100644 --- a/plugins/user/dynamic_music_system/effects/effect_processor.py +++ b/plugins/user/dynamic_music_system/effects/effect_processor.py @@ -1,941 +1,941 @@ -""" -效果处理器 -负责为动态音乐添加各种音频效果 -""" - -import uuid -import math -import time -import numpy as np -from typing import Dict, List, Any, Optional - -class EffectProcessor: - """ - 效果处理器 - 负责为动态音乐添加各种音频效果 - """ - - def __init__(self, plugin): - """ - 初始化效果处理器 - - Args: - plugin: 动态音乐系统插件实例 - """ - self.plugin = plugin - self.effects: Dict[str, Dict[str, Any]] = {} - self.active_effects: Dict[str, Dict[str, Any]] = {} - self.effect_chains: Dict[str, List[str]] = {} - - self.stats = { - 'effects_created': 0, - 'effects_active': 0, - 'effects_processed': 0, - 'processing_time': 0.0, - 'memory_usage': 0 - } - - self.sample_rate = 44100 - self.buffer_size = plugin.buffer_size if plugin else 4096 - - # 效果参数范围 - self.effect_ranges = { - 'reverb': {'room_size': (0.0, 1.0), 'damping': (0.0, 1.0), 'wet_level': (0.0, 1.0)}, - 'chorus': {'rate': (0.1, 5.0), 'depth': (0.0, 1.0), 'mix': (0.0, 1.0)}, - 'delay': {'time': (0.01, 2.0), 'feedback': (0.0, 1.0), 'mix': (0.0, 1.0)}, - 'filter': {'frequency': (20.0, 20000.0), 'q': (0.1, 10.0), 'gain': (-24.0, 24.0)}, - 'distortion': {'drive': (0.0, 1.0), 'tone': (0.0, 1.0), 'mix': (0.0, 1.0)}, - 'phaser': {'rate': (0.1, 10.0), 'depth': (0.0, 1.0), 'feedback': (-0.9, 0.9)}, - 'flanger': {'rate': (0.1, 10.0), 'depth': (0.0, 0.005), 'feedback': (-0.9, 0.9)}, - 'compressor': {'threshold': (-60.0, 0.0), 'ratio': (1.0, 20.0), 'attack': (0.001, 0.1)}, - 'eq': {'frequency': (20.0, 20000.0), 'gain': (-24.0, 24.0), 'q': (0.1, 10.0)} - } - - # 初始化效果处理器 - self._initialize_effect_processor() - - def _initialize_effect_processor(self): - """初始化效果处理器""" - print("✓ 效果处理器初始化完成") - - def create_effect(self, effect_name: str, effect_type: str, effect_params: Dict[str, Any]) -> bool: - """ - 创建音频效果 - - Args: - effect_name: 效果名称 - effect_type: 效果类型 - effect_params: 效果参数 - - Returns: - 是否创建成功 - """ - try: - # 验证效果类型 - valid_effects = [ - 'reverb', 'chorus', 'delay', 'filter', 'distortion', - 'phaser', 'flanger', 'compressor', 'eq', 'limiter', - 'tremolo', 'vibrato', 'pitch_shift', 'stereo_widen' - ] - - if effect_type not in valid_effects: - print(f"✗ 无效的效果类型: {effect_type}") - return False - - # 设置默认参数 - default_params = self._get_default_effect_params(effect_type) - - # 合并参数 - params = default_params.copy() - params.update(effect_params) - - # 验证参数范围 - if not self._validate_effect_params(effect_type, params): - print(f"✗ 效果参数超出有效范围: {effect_type}") - return False - - # 创建效果 - effect = { - 'name': effect_name, - 'type': effect_type, - 'params': params, - 'state': 'created', - 'bypass': False, - 'wet_dry_mix': params.get('mix', 1.0), - 'created_time': time.time() - } - - self.effects[effect_name] = effect - self.stats['effects_created'] += 1 - - print(f"✓ 音频效果创建成功: {effect_name} ({effect_type})") - return True - - except Exception as e: - print(f"✗ 创建音频效果失败: {e}") - return False - - def _get_default_effect_params(self, effect_type: str) -> Dict[str, Any]: - """ - 获取默认效果参数 - - Args: - effect_type: 效果类型 - - Returns: - 默认参数字典 - """ - defaults = { - 'reverb': { - 'room_size': 0.5, - 'damping': 0.5, - 'wet_level': 0.3, - 'dry_level': 0.7, - 'width': 1.0, - 'freeze_mode': 0.0 - }, - 'chorus': { - 'rate': 1.0, - 'depth': 0.5, - 'feedback': 0.3, - 'mix': 0.5, - 'delay': 0.01 - }, - 'delay': { - 'time': 0.3, - 'feedback': 0.4, - 'mix': 0.5, - 'sync': False, - 'sync_note': '1/4' - }, - 'filter': { - 'type': 'lowpass', - 'frequency': 1000.0, - 'q': 1.0, - 'gain': 0.0 - }, - 'distortion': { - 'drive': 0.5, - 'tone': 0.5, - 'mix': 0.5 - }, - 'phaser': { - 'rate': 1.0, - 'depth': 0.5, - 'feedback': 0.3, - 'mix': 0.5 - }, - 'flanger': { - 'rate': 1.0, - 'depth': 0.002, - 'feedback': 0.3, - 'mix': 0.5 - }, - 'compressor': { - 'threshold': -20.0, - 'ratio': 4.0, - 'attack': 0.01, - 'release': 0.1, - 'gain': 0.0, - 'knee': 5.0 - }, - 'eq': { - 'type': 'peaking', - 'frequency': 1000.0, - 'gain': 0.0, - 'q': 1.0 - }, - 'limiter': { - 'threshold': -0.1, - 'release': 0.05, - 'mix': 1.0 - }, - 'tremolo': { - 'rate': 5.0, - 'depth': 0.5, - 'mix': 1.0, - 'waveform': 'sine' - }, - 'vibrato': { - 'rate': 5.0, - 'depth': 0.5, - 'mix': 1.0, - 'delay': 0.01 - }, - 'pitch_shift': { - 'semitones': 0.0, - 'mix': 1.0, - 'formant_preserve': True - }, - 'stereo_widen': { - 'width': 1.0, - 'mix': 1.0 - } - } - - return defaults.get(effect_type, {}) - - def _validate_effect_params(self, effect_type: str, params: Dict[str, Any]) -> bool: - """ - 验证效果参数范围 - - Args: - effect_type: 效果类型 - params: 参数字典 - - Returns: - 是否验证通过 - """ - if effect_type not in self.effect_ranges: - return True # 没有定义范围的效果默认通过验证 - - ranges = self.effect_ranges[effect_type] - for param_name, (min_val, max_val) in ranges.items(): - if param_name in params: - param_value = params[param_name] - if not (min_val <= param_value <= max_val): - print(f"✗ 参数 {param_name} 超出范围 [{min_val}, {max_val}]: {param_value}") - return False - - return True - - def activate_effect(self, effect_name: str) -> bool: - """ - 激活音频效果 - - Args: - effect_name: 效果名称 - - Returns: - 是否激活成功 - """ - try: - if effect_name not in self.effects: - print(f"✗ 音频效果不存在: {effect_name}") - return False - - self.active_effects[effect_name] = self.effects[effect_name] - self.stats['effects_active'] = len(self.active_effects) - - print(f"✓ 音频效果已激活: {effect_name}") - return True - - except Exception as e: - print(f"✗ 激活音频效果失败: {e}") - return False - - def deactivate_effect(self, effect_name: str) -> bool: - """ - 停用音频效果 - - Args: - effect_name: 效果名称 - - Returns: - 是否停用成功 - """ - try: - if effect_name not in self.active_effects: - print(f"⚠ 音频效果未激活: {effect_name}") - return True - - del self.active_effects[effect_name] - self.stats['effects_active'] = len(self.active_effects) - - print(f"✓ 音频效果已停用: {effect_name}") - return True - - except Exception as e: - print(f"✗ 停用音频效果失败: {e}") - return False - - def apply_effects(self, audio_data: Any, chain_name: str = 'default') -> Any: - """ - 应用效果链到音频数据 - - Args: - audio_data: 输入音频数据 - chain_name: 效果链名称 - - Returns: - 处理后的音频数据 - """ - try: - process_start_time = time.time() - - # 获取效果链 - effect_chain = self.effect_chains.get(chain_name, []) - if not effect_chain: - # 如果没有指定效果链,使用所有激活的效果 - effect_chain = list(self.active_effects.keys()) - - # 依次应用效果 - processed_data = audio_data - effects_applied = 0 - - for effect_name in effect_chain: - if effect_name in self.active_effects: - effect = self.active_effects[effect_name] - if not effect.get('bypass', False): - processed_data = self._apply_single_effect(processed_data, effect) - effects_applied += 1 - - # 更新统计信息 - self.stats['effects_processed'] += effects_applied - self.stats['processing_time'] += (time.time() - process_start_time) - - return processed_data - - except Exception as e: - print(f"✗ 应用效果链失败: {e}") - return audio_data - - def _apply_single_effect(self, audio_data: Any, effect: Dict[str, Any]) -> Any: - """ - 应用单个效果到音频数据 - - Args: - audio_data: 输入音频数据 - effect: 效果数据 - - Returns: - 处理后的音频数据 - """ - try: - effect_type = effect['type'] - params = effect['params'] - wet_dry_mix = effect.get('wet_dry_mix', 1.0) - - # 根据效果类型应用处理 - if effect_type == 'reverb': - processed_data = self._apply_reverb(audio_data, params) - elif effect_type == 'chorus': - processed_data = self._apply_chorus(audio_data, params) - elif effect_type == 'delay': - processed_data = self._apply_delay(audio_data, params) - elif effect_type == 'filter': - processed_data = self._apply_filter(audio_data, params) - elif effect_type == 'distortion': - processed_data = self._apply_distortion(audio_data, params) - elif effect_type == 'phaser': - processed_data = self._apply_phaser(audio_data, params) - elif effect_type == 'flanger': - processed_data = self._apply_flanger(audio_data, params) - elif effect_type == 'compressor': - processed_data = self._apply_compressor(audio_data, params) - elif effect_type == 'eq': - processed_data = self._apply_eq(audio_data, params) - else: - # 未知效果类型,不处理 - processed_data = audio_data - - # 应用湿/干混合 - if wet_dry_mix < 1.0: - processed_data = audio_data * (1.0 - wet_dry_mix) + processed_data * wet_dry_mix - - return processed_data - - except Exception as e: - print(f"✗ 应用单个效果失败: {e}") - return audio_data - - def _apply_reverb(self, audio_data: Any, params: Dict[str, Any]) -> Any: - """ - 应用混响效果 - - Args: - audio_data: 输入音频数据 - params: 效果参数 - - Returns: - 处理后的音频数据 - """ - # 简化的混响实现 - # 实际应用中会使用更复杂的算法 - - room_size = params.get('room_size', 0.5) - damping = params.get('damping', 0.5) - wet_level = params.get('wet_level', 0.3) - - # 模拟混响效果 - reverb_amount = wet_level * room_size - damp_factor = 1.0 - damping * 0.5 - - if isinstance(audio_data, np.ndarray): - # 对NumPy数组应用简化效果 - processed = audio_data.copy() - # 添加延迟和衰减效果 - for i in range(1, min(100, len(processed))): - processed[i:] += processed[:-i] * reverb_amount * damp_factor * (0.95 ** i) - return np.clip(processed, -1.0, 1.0) - else: - return audio_data - - def _apply_chorus(self, audio_data: Any, params: Dict[str, Any]) -> Any: - """ - 应用合唱效果 - - Args: - audio_data: 输入音频数据 - params: 效果参数 - - Returns: - 处理后的音频数据 - """ - # 简化的合唱实现 - rate = params.get('rate', 1.0) - depth = params.get('depth', 0.5) - - if isinstance(audio_data, np.ndarray): - processed = audio_data.copy() - # 添加基于正弦波的调制 - t = np.arange(len(processed)) / self.sample_rate - modulation = np.sin(2 * np.pi * rate * t) * depth - # 简化的延迟效果 - delay_samples = (modulation * self.sample_rate * 0.01).astype(int) - for i in range(len(processed)): - delay_idx = max(0, i - abs(delay_samples[i])) - processed[i] += processed[delay_idx] * 0.3 - return np.clip(processed, -1.0, 1.0) - else: - return audio_data - - def _apply_delay(self, audio_data: Any, params: Dict[str, Any]) -> Any: - """ - 应用延迟效果 - - Args: - audio_data: 输入音频数据 - params: 效果参数 - - Returns: - 处理后的音频数据 - """ - delay_time = params.get('time', 0.3) - feedback = params.get('feedback', 0.4) - - if isinstance(audio_data, np.ndarray): - processed = audio_data.copy() - delay_samples = int(delay_time * self.sample_rate) - if delay_samples > 0: - for i in range(delay_samples, len(processed)): - processed[i] += processed[i - delay_samples] * feedback - return np.clip(processed, -1.0, 1.0) - else: - return audio_data - - def _apply_filter(self, audio_data: Any, params: Dict[str, Any]) -> Any: - """ - 应用滤波器效果 - - Args: - audio_data: 输入音频数据 - params: 效果参数 - - Returns: - 处理后的音频数据 - """ - filter_type = params.get('type', 'lowpass') - frequency = params.get('frequency', 1000.0) - - if isinstance(audio_data, np.ndarray): - processed = audio_data.copy() - # 简化的滤波器实现 - if filter_type == 'lowpass': - # 简单的低通滤波 - rc = 1.0 / (2 * np.pi * frequency) - dt = 1.0 / self.sample_rate - alpha = dt / (rc + dt) - for i in range(1, len(processed)): - processed[i] = alpha * processed[i] + (1 - alpha) * processed[i-1] - elif filter_type == 'highpass': - # 简单的高通滤波 - rc = 1.0 / (2 * np.pi * frequency) - dt = 1.0 / self.sample_rate - alpha = rc / (rc + dt) - for i in range(1, len(processed)): - processed[i] = alpha * (processed[i-1] + processed[i] - processed[i-1]) - return processed - else: - return audio_data - - def _apply_distortion(self, audio_data: Any, params: Dict[str, Any]) -> Any: - """ - 应用失真效果 - - Args: - audio_data: 输入音频数据 - params: 效果参数 - - Returns: - 处理后的音频数据 - """ - drive = params.get('drive', 0.5) - - if isinstance(audio_data, np.ndarray): - processed = audio_data.copy() - # 应用饱和失真 - drive_factor = 1.0 + drive * 10.0 - processed = np.tanh(processed * drive_factor) - return processed - else: - return audio_data - - def _apply_phaser(self, audio_data: Any, params: Dict[str, Any]) -> Any: - """ - 应用移相器效果 - - Args: - audio_data: 输入音频数据 - params: 效果参数 - - Returns: - 处理后的音频数据 - """ - rate = params.get('rate', 1.0) - depth = params.get('depth', 0.5) - - if isinstance(audio_data, np.ndarray): - processed = audio_data.copy() - # 简化的移相器实现 - t = np.arange(len(processed)) / self.sample_rate - lfo = np.sin(2 * np.pi * rate * t) * depth - # 应用相位调制 - processed += processed * lfo * 0.5 - return np.clip(processed, -1.0, 1.0) - else: - return audio_data - - def _apply_flanger(self, audio_data: Any, params: Dict[str, Any]) -> Any: - """ - 应用镶边效果 - - Args: - audio_data: 输入音频数据 - params: 效果参数 - - Returns: - 处理后的音频数据 - """ - rate = params.get('rate', 1.0) - depth = params.get('depth', 0.002) - - if isinstance(audio_data, np.ndarray): - processed = audio_data.copy() - # 简化的镶边实现 - t = np.arange(len(processed)) / self.sample_rate - delay = np.sin(2 * np.pi * rate * t) * depth - delay_samples = (delay * self.sample_rate).astype(int) - for i in range(len(processed)): - delay_idx = max(0, i - abs(delay_samples[i])) - processed[i] += processed[delay_idx] * 0.7 - return np.clip(processed, -1.0, 1.0) - else: - return audio_data - - def _apply_compressor(self, audio_data: Any, params: Dict[str, Any]) -> Any: - """ - 应用压缩器效果 - - Args: - audio_data: 输入音频数据 - params: 效果参数 - - Returns: - 处理后的音频数据 - """ - threshold = params.get('threshold', -20.0) - ratio = params.get('ratio', 4.0) - - if isinstance(audio_data, np.ndarray): - processed = audio_data.copy() - # 简化的压缩器实现 - threshold_linear = 10 ** (threshold / 20.0) - # 对超过阈值的信号进行压缩 - abs_signal = np.abs(processed) - compressed = np.where( - abs_signal > threshold_linear, - np.sign(processed) * (threshold_linear + (abs_signal - threshold_linear) / ratio), - processed - ) - return compressed - else: - return audio_data - - def _apply_eq(self, audio_data: Any, params: Dict[str, Any]) -> Any: - """ - 应用均衡器效果 - - Args: - audio_data: 输入音频数据 - params: 效果参数 - - Returns: - 处理后的音频数据 - """ - frequency = params.get('frequency', 1000.0) - gain = params.get('gain', 0.0) - - if isinstance(audio_data, np.ndarray): - processed = audio_data.copy() - # 简化的均衡器实现 - if gain != 0.0: - gain_factor = 10 ** (gain / 20.0) - # 根据频率调整增益(简化实现) - if frequency < 500: - # 低频增强/衰减 - processed *= 0.8 + 0.2 * gain_factor - elif frequency < 2000: - # 中频增强/衰减 - processed *= gain_factor - else: - # 高频增强/衰减 - processed *= 1.0 + 0.1 * (gain_factor - 1.0) - return processed - else: - return audio_data - - def create_effect_chain(self, chain_name: str, effect_names: List[str]) -> bool: - """ - 创建效果链 - - Args: - chain_name: 效果链名称 - effect_names: 效果名称列表 - - Returns: - 是否创建成功 - """ - try: - # 验证效果是否存在 - for effect_name in effect_names: - if effect_name not in self.effects: - print(f"✗ 效果不存在: {effect_name}") - return False - - # 创建效果链 - self.effect_chains[chain_name] = effect_names.copy() - print(f"✓ 效果链创建成功: {chain_name}") - return True - - except Exception as e: - print(f"✗ 创建效果链失败: {e}") - return False - - def update(self, dt: float): - """ - 更新效果处理器状态 - - Args: - dt: 时间增量 - """ - # 这里可以更新任何需要定期更新的状态 - # 例如:LFO相位更新、动态参数调整等 - pass - - def cleanup(self): - """清理所有资源""" - self.effects.clear() - self.active_effects.clear() - self.effect_chains.clear() - - self.stats = { - 'effects_created': 0, - 'effects_active': 0, - 'effects_processed': 0, - 'processing_time': 0.0, - 'memory_usage': 0 - } - - print("✓ 效果处理器资源已清理") - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def get_active_effects(self) -> List[str]: - """ - 获取激活的效果列表 - - Returns: - 激活的效果名称列表 - """ - return list(self.active_effects.keys()) - - def get_effect_status(self, effect_name: str) -> Dict[str, Any]: - """ - 获取效果状态 - - Args: - effect_name: 效果名称 - - Returns: - 效果状态字典 - """ - if effect_name in self.active_effects: - effect = self.active_effects[effect_name] - return { - 'name': effect_name, - 'type': effect['type'], - 'state': 'active', - 'bypass': effect.get('bypass', False), - 'wet_dry_mix': effect.get('wet_dry_mix', 1.0) - } - elif effect_name in self.effects: - effect = self.effects[effect_name] - return { - 'name': effect_name, - 'type': effect['type'], - 'state': 'inactive', - 'bypass': effect.get('bypass', False), - 'wet_dry_mix': effect.get('wet_dry_mix', 1.0) - } - else: - return { - 'name': effect_name, - 'state': 'unknown' - } - - def bypass_effect(self, effect_name: str, bypass: bool = True) -> bool: - """ - 旁路/启用效果 - - Args: - effect_name: 效果名称 - bypass: 是否旁路 - - Returns: - 是否设置成功 - """ - try: - if effect_name not in self.effects: - print(f"✗ 效果不存在: {effect_name}") - return False - - self.effects[effect_name]['bypass'] = bypass - if effect_name in self.active_effects: - self.active_effects[effect_name]['bypass'] = bypass - - state = "旁路" if bypass else "启用" - print(f"✓ 效果已{state}: {effect_name}") - return True - - except Exception as e: - print(f"✗ 设置效果旁路状态失败: {e}") - return False - - def set_effect_parameter(self, effect_name: str, parameter: str, value: Any) -> bool: - """ - 设置效果参数 - - Args: - effect_name: 效果名称 - parameter: 参数名称 - value: 参数值 - - Returns: - 是否设置成功 - """ - try: - if effect_name not in self.effects: - print(f"✗ 效果不存在: {effect_name}") - return False - - # 验证参数范围 - effect = self.effects[effect_name] - effect_type = effect['type'] - - if effect_type in self.effect_ranges and parameter in self.effect_ranges[effect_type]: - min_val, max_val = self.effect_ranges[effect_type][parameter] - if not (min_val <= value <= max_val): - print(f"✗ 参数 {parameter} 超出范围 [{min_val}, {max_val}]: {value}") - return False - - # 设置参数 - self.effects[effect_name]['params'][parameter] = value - if effect_name in self.active_effects: - self.active_effects[effect_name]['params'][parameter] = value - - print(f"✓ 效果参数已设置: {effect_name}.{parameter} = {value}") - return True - - except Exception as e: - print(f"✗ 设置效果参数失败: {e}") - return False - - def get_effect_parameters(self, effect_name: str) -> Dict[str, Any]: - """ - 获取效果参数 - - Args: - effect_name: 效果名称 - - Returns: - 参数字典 - """ - if effect_name in self.effects: - return self.effects[effect_name]['params'].copy() - return {} - - def set_wet_dry_mix(self, effect_name: str, mix: float) -> bool: - """ - 设置湿/干混合比例 - - Args: - effect_name: 效果名称 - mix: 混合比例 (0.0-1.0) - - Returns: - 是否设置成功 - """ - mix = max(0.0, min(1.0, mix)) - return self.set_effect_parameter(effect_name, 'mix', mix) - - def set_sample_rate(self, sample_rate: int): - """ - 设置采样率 - - Args: - sample_rate: 采样率 (Hz) - """ - self.sample_rate = sample_rate - print(f"✓ 效果处理器采样率设置为: {self.sample_rate} Hz") - - def create_modulation_effect(self, effect_name: str, effect_type: str, - mod_rate: float, mod_depth: float) -> bool: - """ - 创建调制效果 - - Args: - effect_name: 效果名称 - effect_type: 效果类型 (chorus, flanger, phaser, tremolo, vibrato) - mod_rate: 调制速率 (Hz) - mod_depth: 调制深度 (0.0-1.0) - - Returns: - 是否创建成功 - """ - try: - # 验证调制效果类型 - modulation_effects = ['chorus', 'flanger', 'phaser', 'tremolo', 'vibrato'] - if effect_type not in modulation_effects: - print(f"✗ 无效的调制效果类型: {effect_type}") - return False - - # 创建效果参数 - effect_params = { - 'rate': mod_rate, - 'depth': mod_depth, - 'mix': 0.5 - } - - return self.create_effect(effect_name, effect_type, effect_params) - - except Exception as e: - print(f"✗ 创建调制效果失败: {e}") - return False - - def create_dynamic_effect(self, effect_name: str, effect_type: str, - intensity_callback: callable) -> bool: - """ - 创建动态效果(参数随游戏状态变化) - - Args: - effect_name: 效果名称 - effect_type: 效果类型 - intensity_callback: 强度回调函数 - - Returns: - 是否创建成功 - """ - try: - # 创建效果 - if self.create_effect(effect_name, effect_type, {}): - # 添加动态参数支持 - self.effects[effect_name]['dynamic'] = True - self.effects[effect_name]['intensity_callback'] = intensity_callback - return True - return False - - except Exception as e: - print(f"✗ 创建动态效果失败: {e}") - return False - - def update_dynamic_effects(self): - """ - 更新动态效果参数 - """ - try: - for effect_name, effect in self.active_effects.items(): - if effect.get('dynamic', False) and 'intensity_callback' in effect: - try: - # 获取当前强度 - intensity = effect['intensity_callback']() - - # 根据强度调整效果参数 - effect_type = effect['type'] - params = effect['params'] - - if effect_type == 'reverb': - params['room_size'] = intensity - params['wet_level'] = intensity * 0.5 - elif effect_type == 'chorus': - params['rate'] = 0.5 + intensity * 4.5 - params['depth'] = intensity - elif effect_type == 'delay': - params['time'] = 0.1 + intensity * 0.9 - params['feedback'] = intensity * 0.8 - - # 更新效果参数 - self.active_effects[effect_name]['params'] = params - - except Exception as e: - print(f"✗ 更新动态效果参数失败: {effect_name} - {e}") - - except Exception as e: +""" +效果处理器 +负责为动态音乐添加各种音频效果 +""" + +import uuid +import math +import time +import numpy as np +from typing import Dict, List, Any, Optional + +class EffectProcessor: + """ + 效果处理器 + 负责为动态音乐添加各种音频效果 + """ + + def __init__(self, plugin): + """ + 初始化效果处理器 + + Args: + plugin: 动态音乐系统插件实例 + """ + self.plugin = plugin + self.effects: Dict[str, Dict[str, Any]] = {} + self.active_effects: Dict[str, Dict[str, Any]] = {} + self.effect_chains: Dict[str, List[str]] = {} + + self.stats = { + 'effects_created': 0, + 'effects_active': 0, + 'effects_processed': 0, + 'processing_time': 0.0, + 'memory_usage': 0 + } + + self.sample_rate = 44100 + self.buffer_size = plugin.buffer_size if plugin else 4096 + + # 效果参数范围 + self.effect_ranges = { + 'reverb': {'room_size': (0.0, 1.0), 'damping': (0.0, 1.0), 'wet_level': (0.0, 1.0)}, + 'chorus': {'rate': (0.1, 5.0), 'depth': (0.0, 1.0), 'mix': (0.0, 1.0)}, + 'delay': {'time': (0.01, 2.0), 'feedback': (0.0, 1.0), 'mix': (0.0, 1.0)}, + 'filter': {'frequency': (20.0, 20000.0), 'q': (0.1, 10.0), 'gain': (-24.0, 24.0)}, + 'distortion': {'drive': (0.0, 1.0), 'tone': (0.0, 1.0), 'mix': (0.0, 1.0)}, + 'phaser': {'rate': (0.1, 10.0), 'depth': (0.0, 1.0), 'feedback': (-0.9, 0.9)}, + 'flanger': {'rate': (0.1, 10.0), 'depth': (0.0, 0.005), 'feedback': (-0.9, 0.9)}, + 'compressor': {'threshold': (-60.0, 0.0), 'ratio': (1.0, 20.0), 'attack': (0.001, 0.1)}, + 'eq': {'frequency': (20.0, 20000.0), 'gain': (-24.0, 24.0), 'q': (0.1, 10.0)} + } + + # 初始化效果处理器 + self._initialize_effect_processor() + + def _initialize_effect_processor(self): + """初始化效果处理器""" + print("✓ 效果处理器初始化完成") + + def create_effect(self, effect_name: str, effect_type: str, effect_params: Dict[str, Any]) -> bool: + """ + 创建音频效果 + + Args: + effect_name: 效果名称 + effect_type: 效果类型 + effect_params: 效果参数 + + Returns: + 是否创建成功 + """ + try: + # 验证效果类型 + valid_effects = [ + 'reverb', 'chorus', 'delay', 'filter', 'distortion', + 'phaser', 'flanger', 'compressor', 'eq', 'limiter', + 'tremolo', 'vibrato', 'pitch_shift', 'stereo_widen' + ] + + if effect_type not in valid_effects: + print(f"✗ 无效的效果类型: {effect_type}") + return False + + # 设置默认参数 + default_params = self._get_default_effect_params(effect_type) + + # 合并参数 + params = default_params.copy() + params.update(effect_params) + + # 验证参数范围 + if not self._validate_effect_params(effect_type, params): + print(f"✗ 效果参数超出有效范围: {effect_type}") + return False + + # 创建效果 + effect = { + 'name': effect_name, + 'type': effect_type, + 'params': params, + 'state': 'created', + 'bypass': False, + 'wet_dry_mix': params.get('mix', 1.0), + 'created_time': time.time() + } + + self.effects[effect_name] = effect + self.stats['effects_created'] += 1 + + print(f"✓ 音频效果创建成功: {effect_name} ({effect_type})") + return True + + except Exception as e: + print(f"✗ 创建音频效果失败: {e}") + return False + + def _get_default_effect_params(self, effect_type: str) -> Dict[str, Any]: + """ + 获取默认效果参数 + + Args: + effect_type: 效果类型 + + Returns: + 默认参数字典 + """ + defaults = { + 'reverb': { + 'room_size': 0.5, + 'damping': 0.5, + 'wet_level': 0.3, + 'dry_level': 0.7, + 'width': 1.0, + 'freeze_mode': 0.0 + }, + 'chorus': { + 'rate': 1.0, + 'depth': 0.5, + 'feedback': 0.3, + 'mix': 0.5, + 'delay': 0.01 + }, + 'delay': { + 'time': 0.3, + 'feedback': 0.4, + 'mix': 0.5, + 'sync': False, + 'sync_note': '1/4' + }, + 'filter': { + 'type': 'lowpass', + 'frequency': 1000.0, + 'q': 1.0, + 'gain': 0.0 + }, + 'distortion': { + 'drive': 0.5, + 'tone': 0.5, + 'mix': 0.5 + }, + 'phaser': { + 'rate': 1.0, + 'depth': 0.5, + 'feedback': 0.3, + 'mix': 0.5 + }, + 'flanger': { + 'rate': 1.0, + 'depth': 0.002, + 'feedback': 0.3, + 'mix': 0.5 + }, + 'compressor': { + 'threshold': -20.0, + 'ratio': 4.0, + 'attack': 0.01, + 'release': 0.1, + 'gain': 0.0, + 'knee': 5.0 + }, + 'eq': { + 'type': 'peaking', + 'frequency': 1000.0, + 'gain': 0.0, + 'q': 1.0 + }, + 'limiter': { + 'threshold': -0.1, + 'release': 0.05, + 'mix': 1.0 + }, + 'tremolo': { + 'rate': 5.0, + 'depth': 0.5, + 'mix': 1.0, + 'waveform': 'sine' + }, + 'vibrato': { + 'rate': 5.0, + 'depth': 0.5, + 'mix': 1.0, + 'delay': 0.01 + }, + 'pitch_shift': { + 'semitones': 0.0, + 'mix': 1.0, + 'formant_preserve': True + }, + 'stereo_widen': { + 'width': 1.0, + 'mix': 1.0 + } + } + + return defaults.get(effect_type, {}) + + def _validate_effect_params(self, effect_type: str, params: Dict[str, Any]) -> bool: + """ + 验证效果参数范围 + + Args: + effect_type: 效果类型 + params: 参数字典 + + Returns: + 是否验证通过 + """ + if effect_type not in self.effect_ranges: + return True # 没有定义范围的效果默认通过验证 + + ranges = self.effect_ranges[effect_type] + for param_name, (min_val, max_val) in ranges.items(): + if param_name in params: + param_value = params[param_name] + if not (min_val <= param_value <= max_val): + print(f"✗ 参数 {param_name} 超出范围 [{min_val}, {max_val}]: {param_value}") + return False + + return True + + def activate_effect(self, effect_name: str) -> bool: + """ + 激活音频效果 + + Args: + effect_name: 效果名称 + + Returns: + 是否激活成功 + """ + try: + if effect_name not in self.effects: + print(f"✗ 音频效果不存在: {effect_name}") + return False + + self.active_effects[effect_name] = self.effects[effect_name] + self.stats['effects_active'] = len(self.active_effects) + + print(f"✓ 音频效果已激活: {effect_name}") + return True + + except Exception as e: + print(f"✗ 激活音频效果失败: {e}") + return False + + def deactivate_effect(self, effect_name: str) -> bool: + """ + 停用音频效果 + + Args: + effect_name: 效果名称 + + Returns: + 是否停用成功 + """ + try: + if effect_name not in self.active_effects: + print(f"⚠ 音频效果未激活: {effect_name}") + return True + + del self.active_effects[effect_name] + self.stats['effects_active'] = len(self.active_effects) + + print(f"✓ 音频效果已停用: {effect_name}") + return True + + except Exception as e: + print(f"✗ 停用音频效果失败: {e}") + return False + + def apply_effects(self, audio_data: Any, chain_name: str = 'default') -> Any: + """ + 应用效果链到音频数据 + + Args: + audio_data: 输入音频数据 + chain_name: 效果链名称 + + Returns: + 处理后的音频数据 + """ + try: + process_start_time = time.time() + + # 获取效果链 + effect_chain = self.effect_chains.get(chain_name, []) + if not effect_chain: + # 如果没有指定效果链,使用所有激活的效果 + effect_chain = list(self.active_effects.keys()) + + # 依次应用效果 + processed_data = audio_data + effects_applied = 0 + + for effect_name in effect_chain: + if effect_name in self.active_effects: + effect = self.active_effects[effect_name] + if not effect.get('bypass', False): + processed_data = self._apply_single_effect(processed_data, effect) + effects_applied += 1 + + # 更新统计信息 + self.stats['effects_processed'] += effects_applied + self.stats['processing_time'] += (time.time() - process_start_time) + + return processed_data + + except Exception as e: + print(f"✗ 应用效果链失败: {e}") + return audio_data + + def _apply_single_effect(self, audio_data: Any, effect: Dict[str, Any]) -> Any: + """ + 应用单个效果到音频数据 + + Args: + audio_data: 输入音频数据 + effect: 效果数据 + + Returns: + 处理后的音频数据 + """ + try: + effect_type = effect['type'] + params = effect['params'] + wet_dry_mix = effect.get('wet_dry_mix', 1.0) + + # 根据效果类型应用处理 + if effect_type == 'reverb': + processed_data = self._apply_reverb(audio_data, params) + elif effect_type == 'chorus': + processed_data = self._apply_chorus(audio_data, params) + elif effect_type == 'delay': + processed_data = self._apply_delay(audio_data, params) + elif effect_type == 'filter': + processed_data = self._apply_filter(audio_data, params) + elif effect_type == 'distortion': + processed_data = self._apply_distortion(audio_data, params) + elif effect_type == 'phaser': + processed_data = self._apply_phaser(audio_data, params) + elif effect_type == 'flanger': + processed_data = self._apply_flanger(audio_data, params) + elif effect_type == 'compressor': + processed_data = self._apply_compressor(audio_data, params) + elif effect_type == 'eq': + processed_data = self._apply_eq(audio_data, params) + else: + # 未知效果类型,不处理 + processed_data = audio_data + + # 应用湿/干混合 + if wet_dry_mix < 1.0: + processed_data = audio_data * (1.0 - wet_dry_mix) + processed_data * wet_dry_mix + + return processed_data + + except Exception as e: + print(f"✗ 应用单个效果失败: {e}") + return audio_data + + def _apply_reverb(self, audio_data: Any, params: Dict[str, Any]) -> Any: + """ + 应用混响效果 + + Args: + audio_data: 输入音频数据 + params: 效果参数 + + Returns: + 处理后的音频数据 + """ + # 简化的混响实现 + # 实际应用中会使用更复杂的算法 + + room_size = params.get('room_size', 0.5) + damping = params.get('damping', 0.5) + wet_level = params.get('wet_level', 0.3) + + # 模拟混响效果 + reverb_amount = wet_level * room_size + damp_factor = 1.0 - damping * 0.5 + + if isinstance(audio_data, np.ndarray): + # 对NumPy数组应用简化效果 + processed = audio_data.copy() + # 添加延迟和衰减效果 + for i in range(1, min(100, len(processed))): + processed[i:] += processed[:-i] * reverb_amount * damp_factor * (0.95 ** i) + return np.clip(processed, -1.0, 1.0) + else: + return audio_data + + def _apply_chorus(self, audio_data: Any, params: Dict[str, Any]) -> Any: + """ + 应用合唱效果 + + Args: + audio_data: 输入音频数据 + params: 效果参数 + + Returns: + 处理后的音频数据 + """ + # 简化的合唱实现 + rate = params.get('rate', 1.0) + depth = params.get('depth', 0.5) + + if isinstance(audio_data, np.ndarray): + processed = audio_data.copy() + # 添加基于正弦波的调制 + t = np.arange(len(processed)) / self.sample_rate + modulation = np.sin(2 * np.pi * rate * t) * depth + # 简化的延迟效果 + delay_samples = (modulation * self.sample_rate * 0.01).astype(int) + for i in range(len(processed)): + delay_idx = max(0, i - abs(delay_samples[i])) + processed[i] += processed[delay_idx] * 0.3 + return np.clip(processed, -1.0, 1.0) + else: + return audio_data + + def _apply_delay(self, audio_data: Any, params: Dict[str, Any]) -> Any: + """ + 应用延迟效果 + + Args: + audio_data: 输入音频数据 + params: 效果参数 + + Returns: + 处理后的音频数据 + """ + delay_time = params.get('time', 0.3) + feedback = params.get('feedback', 0.4) + + if isinstance(audio_data, np.ndarray): + processed = audio_data.copy() + delay_samples = int(delay_time * self.sample_rate) + if delay_samples > 0: + for i in range(delay_samples, len(processed)): + processed[i] += processed[i - delay_samples] * feedback + return np.clip(processed, -1.0, 1.0) + else: + return audio_data + + def _apply_filter(self, audio_data: Any, params: Dict[str, Any]) -> Any: + """ + 应用滤波器效果 + + Args: + audio_data: 输入音频数据 + params: 效果参数 + + Returns: + 处理后的音频数据 + """ + filter_type = params.get('type', 'lowpass') + frequency = params.get('frequency', 1000.0) + + if isinstance(audio_data, np.ndarray): + processed = audio_data.copy() + # 简化的滤波器实现 + if filter_type == 'lowpass': + # 简单的低通滤波 + rc = 1.0 / (2 * np.pi * frequency) + dt = 1.0 / self.sample_rate + alpha = dt / (rc + dt) + for i in range(1, len(processed)): + processed[i] = alpha * processed[i] + (1 - alpha) * processed[i-1] + elif filter_type == 'highpass': + # 简单的高通滤波 + rc = 1.0 / (2 * np.pi * frequency) + dt = 1.0 / self.sample_rate + alpha = rc / (rc + dt) + for i in range(1, len(processed)): + processed[i] = alpha * (processed[i-1] + processed[i] - processed[i-1]) + return processed + else: + return audio_data + + def _apply_distortion(self, audio_data: Any, params: Dict[str, Any]) -> Any: + """ + 应用失真效果 + + Args: + audio_data: 输入音频数据 + params: 效果参数 + + Returns: + 处理后的音频数据 + """ + drive = params.get('drive', 0.5) + + if isinstance(audio_data, np.ndarray): + processed = audio_data.copy() + # 应用饱和失真 + drive_factor = 1.0 + drive * 10.0 + processed = np.tanh(processed * drive_factor) + return processed + else: + return audio_data + + def _apply_phaser(self, audio_data: Any, params: Dict[str, Any]) -> Any: + """ + 应用移相器效果 + + Args: + audio_data: 输入音频数据 + params: 效果参数 + + Returns: + 处理后的音频数据 + """ + rate = params.get('rate', 1.0) + depth = params.get('depth', 0.5) + + if isinstance(audio_data, np.ndarray): + processed = audio_data.copy() + # 简化的移相器实现 + t = np.arange(len(processed)) / self.sample_rate + lfo = np.sin(2 * np.pi * rate * t) * depth + # 应用相位调制 + processed += processed * lfo * 0.5 + return np.clip(processed, -1.0, 1.0) + else: + return audio_data + + def _apply_flanger(self, audio_data: Any, params: Dict[str, Any]) -> Any: + """ + 应用镶边效果 + + Args: + audio_data: 输入音频数据 + params: 效果参数 + + Returns: + 处理后的音频数据 + """ + rate = params.get('rate', 1.0) + depth = params.get('depth', 0.002) + + if isinstance(audio_data, np.ndarray): + processed = audio_data.copy() + # 简化的镶边实现 + t = np.arange(len(processed)) / self.sample_rate + delay = np.sin(2 * np.pi * rate * t) * depth + delay_samples = (delay * self.sample_rate).astype(int) + for i in range(len(processed)): + delay_idx = max(0, i - abs(delay_samples[i])) + processed[i] += processed[delay_idx] * 0.7 + return np.clip(processed, -1.0, 1.0) + else: + return audio_data + + def _apply_compressor(self, audio_data: Any, params: Dict[str, Any]) -> Any: + """ + 应用压缩器效果 + + Args: + audio_data: 输入音频数据 + params: 效果参数 + + Returns: + 处理后的音频数据 + """ + threshold = params.get('threshold', -20.0) + ratio = params.get('ratio', 4.0) + + if isinstance(audio_data, np.ndarray): + processed = audio_data.copy() + # 简化的压缩器实现 + threshold_linear = 10 ** (threshold / 20.0) + # 对超过阈值的信号进行压缩 + abs_signal = np.abs(processed) + compressed = np.where( + abs_signal > threshold_linear, + np.sign(processed) * (threshold_linear + (abs_signal - threshold_linear) / ratio), + processed + ) + return compressed + else: + return audio_data + + def _apply_eq(self, audio_data: Any, params: Dict[str, Any]) -> Any: + """ + 应用均衡器效果 + + Args: + audio_data: 输入音频数据 + params: 效果参数 + + Returns: + 处理后的音频数据 + """ + frequency = params.get('frequency', 1000.0) + gain = params.get('gain', 0.0) + + if isinstance(audio_data, np.ndarray): + processed = audio_data.copy() + # 简化的均衡器实现 + if gain != 0.0: + gain_factor = 10 ** (gain / 20.0) + # 根据频率调整增益(简化实现) + if frequency < 500: + # 低频增强/衰减 + processed *= 0.8 + 0.2 * gain_factor + elif frequency < 2000: + # 中频增强/衰减 + processed *= gain_factor + else: + # 高频增强/衰减 + processed *= 1.0 + 0.1 * (gain_factor - 1.0) + return processed + else: + return audio_data + + def create_effect_chain(self, chain_name: str, effect_names: List[str]) -> bool: + """ + 创建效果链 + + Args: + chain_name: 效果链名称 + effect_names: 效果名称列表 + + Returns: + 是否创建成功 + """ + try: + # 验证效果是否存在 + for effect_name in effect_names: + if effect_name not in self.effects: + print(f"✗ 效果不存在: {effect_name}") + return False + + # 创建效果链 + self.effect_chains[chain_name] = effect_names.copy() + print(f"✓ 效果链创建成功: {chain_name}") + return True + + except Exception as e: + print(f"✗ 创建效果链失败: {e}") + return False + + def update(self, dt: float): + """ + 更新效果处理器状态 + + Args: + dt: 时间增量 + """ + # 这里可以更新任何需要定期更新的状态 + # 例如:LFO相位更新、动态参数调整等 + pass + + def cleanup(self): + """清理所有资源""" + self.effects.clear() + self.active_effects.clear() + self.effect_chains.clear() + + self.stats = { + 'effects_created': 0, + 'effects_active': 0, + 'effects_processed': 0, + 'processing_time': 0.0, + 'memory_usage': 0 + } + + print("✓ 效果处理器资源已清理") + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def get_active_effects(self) -> List[str]: + """ + 获取激活的效果列表 + + Returns: + 激活的效果名称列表 + """ + return list(self.active_effects.keys()) + + def get_effect_status(self, effect_name: str) -> Dict[str, Any]: + """ + 获取效果状态 + + Args: + effect_name: 效果名称 + + Returns: + 效果状态字典 + """ + if effect_name in self.active_effects: + effect = self.active_effects[effect_name] + return { + 'name': effect_name, + 'type': effect['type'], + 'state': 'active', + 'bypass': effect.get('bypass', False), + 'wet_dry_mix': effect.get('wet_dry_mix', 1.0) + } + elif effect_name in self.effects: + effect = self.effects[effect_name] + return { + 'name': effect_name, + 'type': effect['type'], + 'state': 'inactive', + 'bypass': effect.get('bypass', False), + 'wet_dry_mix': effect.get('wet_dry_mix', 1.0) + } + else: + return { + 'name': effect_name, + 'state': 'unknown' + } + + def bypass_effect(self, effect_name: str, bypass: bool = True) -> bool: + """ + 旁路/启用效果 + + Args: + effect_name: 效果名称 + bypass: 是否旁路 + + Returns: + 是否设置成功 + """ + try: + if effect_name not in self.effects: + print(f"✗ 效果不存在: {effect_name}") + return False + + self.effects[effect_name]['bypass'] = bypass + if effect_name in self.active_effects: + self.active_effects[effect_name]['bypass'] = bypass + + state = "旁路" if bypass else "启用" + print(f"✓ 效果已{state}: {effect_name}") + return True + + except Exception as e: + print(f"✗ 设置效果旁路状态失败: {e}") + return False + + def set_effect_parameter(self, effect_name: str, parameter: str, value: Any) -> bool: + """ + 设置效果参数 + + Args: + effect_name: 效果名称 + parameter: 参数名称 + value: 参数值 + + Returns: + 是否设置成功 + """ + try: + if effect_name not in self.effects: + print(f"✗ 效果不存在: {effect_name}") + return False + + # 验证参数范围 + effect = self.effects[effect_name] + effect_type = effect['type'] + + if effect_type in self.effect_ranges and parameter in self.effect_ranges[effect_type]: + min_val, max_val = self.effect_ranges[effect_type][parameter] + if not (min_val <= value <= max_val): + print(f"✗ 参数 {parameter} 超出范围 [{min_val}, {max_val}]: {value}") + return False + + # 设置参数 + self.effects[effect_name]['params'][parameter] = value + if effect_name in self.active_effects: + self.active_effects[effect_name]['params'][parameter] = value + + print(f"✓ 效果参数已设置: {effect_name}.{parameter} = {value}") + return True + + except Exception as e: + print(f"✗ 设置效果参数失败: {e}") + return False + + def get_effect_parameters(self, effect_name: str) -> Dict[str, Any]: + """ + 获取效果参数 + + Args: + effect_name: 效果名称 + + Returns: + 参数字典 + """ + if effect_name in self.effects: + return self.effects[effect_name]['params'].copy() + return {} + + def set_wet_dry_mix(self, effect_name: str, mix: float) -> bool: + """ + 设置湿/干混合比例 + + Args: + effect_name: 效果名称 + mix: 混合比例 (0.0-1.0) + + Returns: + 是否设置成功 + """ + mix = max(0.0, min(1.0, mix)) + return self.set_effect_parameter(effect_name, 'mix', mix) + + def set_sample_rate(self, sample_rate: int): + """ + 设置采样率 + + Args: + sample_rate: 采样率 (Hz) + """ + self.sample_rate = sample_rate + print(f"✓ 效果处理器采样率设置为: {self.sample_rate} Hz") + + def create_modulation_effect(self, effect_name: str, effect_type: str, + mod_rate: float, mod_depth: float) -> bool: + """ + 创建调制效果 + + Args: + effect_name: 效果名称 + effect_type: 效果类型 (chorus, flanger, phaser, tremolo, vibrato) + mod_rate: 调制速率 (Hz) + mod_depth: 调制深度 (0.0-1.0) + + Returns: + 是否创建成功 + """ + try: + # 验证调制效果类型 + modulation_effects = ['chorus', 'flanger', 'phaser', 'tremolo', 'vibrato'] + if effect_type not in modulation_effects: + print(f"✗ 无效的调制效果类型: {effect_type}") + return False + + # 创建效果参数 + effect_params = { + 'rate': mod_rate, + 'depth': mod_depth, + 'mix': 0.5 + } + + return self.create_effect(effect_name, effect_type, effect_params) + + except Exception as e: + print(f"✗ 创建调制效果失败: {e}") + return False + + def create_dynamic_effect(self, effect_name: str, effect_type: str, + intensity_callback: callable) -> bool: + """ + 创建动态效果(参数随游戏状态变化) + + Args: + effect_name: 效果名称 + effect_type: 效果类型 + intensity_callback: 强度回调函数 + + Returns: + 是否创建成功 + """ + try: + # 创建效果 + if self.create_effect(effect_name, effect_type, {}): + # 添加动态参数支持 + self.effects[effect_name]['dynamic'] = True + self.effects[effect_name]['intensity_callback'] = intensity_callback + return True + return False + + except Exception as e: + print(f"✗ 创建动态效果失败: {e}") + return False + + def update_dynamic_effects(self): + """ + 更新动态效果参数 + """ + try: + for effect_name, effect in self.active_effects.items(): + if effect.get('dynamic', False) and 'intensity_callback' in effect: + try: + # 获取当前强度 + intensity = effect['intensity_callback']() + + # 根据强度调整效果参数 + effect_type = effect['type'] + params = effect['params'] + + if effect_type == 'reverb': + params['room_size'] = intensity + params['wet_level'] = intensity * 0.5 + elif effect_type == 'chorus': + params['rate'] = 0.5 + intensity * 4.5 + params['depth'] = intensity + elif effect_type == 'delay': + params['time'] = 0.1 + intensity * 0.9 + params['feedback'] = intensity * 0.8 + + # 更新效果参数 + self.active_effects[effect_name]['params'] = params + + except Exception as e: + print(f"✗ 更新动态效果参数失败: {effect_name} - {e}") + + except Exception as e: print(f"✗ 更新动态效果失败: {e}") \ No newline at end of file diff --git a/plugins/user/dynamic_music_system/plugin.py b/plugins/user/dynamic_music_system/plugin.py index 4913748a..bfe7c0b8 100644 --- a/plugins/user/dynamic_music_system/plugin.py +++ b/plugins/user/dynamic_music_system/plugin.py @@ -1,832 +1,832 @@ -""" -动态音乐系统插件 -根据游戏状态和环境条件动态生成和控制音乐 -""" - -import math -import time -import uuid -from typing import Dict, List, Optional, Any, Callable -from plugins.plugin_manager import BasePlugin -from panda3d.core import AudioSound, NodePath - -class Plugin(BasePlugin): - """ - 动态音乐系统插件 - 根据游戏状态和环境条件动态生成和控制音乐 - """ - - def __init__(self, plugin_manager, name): - super().__init__(plugin_manager, name) - self.config = { - "version": "1.0.0", - "author": "EG Team", - "description": "动态音乐系统,支持根据游戏状态和环境条件实时生成和控制音乐" - } - - # 核心组件 - self.music_manager = None - self.transition_manager = None - self.composition_engine = None - self.analysis_engine = None - self.effect_processor = None - - # 音乐状态 - self.current_theme = None - self.current_intensity = 0.0 - self.target_intensity = 0.0 - self.intensity_change_rate = 1.0 - self.current_layer_volumes = {} - - # 游戏状态跟踪 - self.game_state = {} - self.environment_conditions = {} - self.player_state = {} - - # 音乐库 - self.themes = {} - self.layers = {} - self.transitions = {} - self.sound_effects = {} - - # GUI元素 - self.gui_elements = [] - - # 事件处理器 - self.event_handlers = {} - - # 插件状态 - self.is_initialized = False - self.is_enabled = False - self.is_playing = False - - # 性能监控 - self.performance_stats = { - 'initialization_time': 0.0, - 'enable_time': 0.0, - 'last_update_time': 0.0, - 'total_updates': 0, - 'total_processing_time': 0.0, - 'themes_loaded': 0, - 'transitions_processed': 0 - } - - # 配置参数 - self.master_volume = 1.0 - self.crossfade_duration = 2.0 - self.intensity_smoothing = 0.1 - self.auto_intensity = True - - def initialize(self) -> bool: - """ - 初始化插件 - """ - try: - if self.is_initialized: - return True - - init_start_time = time.time() - print(f"初始化动态音乐系统插件: {self.name}") - - # 初始化核心组件 - from .core.music_manager import MusicManager - from .transitions.transition_manager import TransitionManager - from .composition.composition_engine import CompositionEngine - from .analysis.analysis_engine import AnalysisEngine - from .effects.effect_processor import EffectProcessor - - self.music_manager = MusicManager(self) - self.transition_manager = TransitionManager(self) - self.composition_engine = CompositionEngine(self) - self.analysis_engine = AnalysisEngine(self) - self.effect_processor = EffectProcessor(self) - - # 加载音乐资源 - self._load_music_resources() - - self.is_initialized = True - self.performance_stats['initialization_time'] = time.time() - init_start_time - print("✓ 动态音乐系统插件初始化完成") - return True - - except Exception as e: - print(f"✗ 动态音乐系统插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - """ - if not super().enable(): - return False - - if self.is_enabled: - return True - - try: - enable_start_time = time.time() - print(f"启用动态音乐系统插件: {self.name}") - - # 注册事件处理器 - self._register_event_handlers() - - # 创建GUI界面 - self._create_gui() - - self.is_enabled = True - self.performance_stats['enable_time'] = time.time() - enable_start_time - print("✓ 动态音乐系统插件启用成功") - return True - - except Exception as e: - print(f"✗ 动态音乐系统插件启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self) -> bool: - """ - 禁用插件 - """ - if not super().disable(): - return False - - if not self.is_enabled: - return True - - try: - print(f"禁用动态音乐系统插件: {self.name}") - - # 清理GUI界面 - self._cleanup_gui() - - # 移除事件处理器 - self._unregister_event_handlers() - - # 停止所有音乐 - self.stop_all_music() - - self.is_enabled = False - print("✓ 动态音乐系统插件禁用成功") - return True - - except Exception as e: - print(f"✗ 动态音乐系统插件禁用失败: {e}") - import traceback - traceback.print_exc() - return False - - def finalize(self): - """ - 清理插件资源 - """ - print(f"清理动态音乐系统插件资源: {self.name}") - - # 停止所有音乐 - self.stop_all_music() - - # 清理所有组件 - components = [ - self.music_manager, - self.transition_manager, - self.composition_engine, - self.analysis_engine, - self.effect_processor - ] - - for component in components: - if component: - component.cleanup() - - self.music_manager = None - self.transition_manager = None - self.composition_engine = None - self.analysis_engine = None - self.effect_processor = None - - self.themes.clear() - self.layers.clear() - self.transitions.clear() - self.sound_effects.clear() - - self.is_initialized = False - self.is_enabled = False - - def _register_event_handlers(self): - """ - 注册事件处理器 - """ - world = self.plugin_manager.world - - # 音乐控制快捷键 - self.event_handlers['f1'] = self.toggle_debug_mode - self.event_handlers['f2'] = self.toggle_music_system - self.event_handlers['f3'] = self.play_test_theme - self.event_handlers['f4'] = self.increase_intensity - self.event_handlers['f5'] = self.decrease_intensity - self.event_handlers['f6'] = self.trigger_battle_music - self.event_handlers['f7'] = self.trigger_exploration_music - self.event_handlers['f8'] = self.trigger_ambient_music - self.event_handlers['f9'] = self.show_stats - self.event_handlers['f10'] = self.toggle_auto_intensity - self.event_handlers['f11'] = self.show_detailed_stats - self.event_handlers['f12'] = self.reset_music_system - - # 注册事件 - for event, handler in self.event_handlers.items(): - world.accept(event, handler) - world.accept(event.upper(), handler) # 同时注册大写事件 - - def _unregister_event_handlers(self): - """ - 移除事件处理器 - """ - world = self.plugin_manager.world - - for event in self.event_handlers.keys(): - world.ignore(event) - world.ignore(event.upper()) - - self.event_handlers.clear() - - def _create_gui(self): - """ - 创建GUI界面 - """ - try: - world = self.plugin_manager.world - - # 检查是否有GUI管理器 - if not hasattr(world, 'gui_manager') or not world.gui_manager: - print("⚠ GUI管理器不可用,跳过GUI创建") - return - - gui_manager = world.gui_manager - - # 创建音乐系统控制面板 - panel_button = gui_manager.createGUIButton( - pos=(0.7, 0, 0.35), - text="动态音乐", - size=0.06 - ) - self.gui_elements.append(panel_button) - - # 创建功能按钮 - functions = [ - ("调试模式 [F1]", self.toggle_debug_mode), - ("切换系统 [F2]", self.toggle_music_system), - ("测试主题 [F3]", self.play_test_theme), - ("增加强度 [F4]", self.increase_intensity), - ("降低强度 [F5]", self.decrease_intensity), - ("战斗音乐 [F6]", self.trigger_battle_music), - ("探索音乐 [F7]", self.trigger_exploration_music), - ("环境音乐 [F8]", self.trigger_ambient_music), - ("音乐统计 [F9]", self.show_stats), - ("自动强度 [F10]", self.toggle_auto_intensity), - ("详细统计 [F11]", self.show_detailed_stats), - ("重置系统 [F12]", self.reset_music_system) - ] - - for i, (name, callback) in enumerate(functions): - button = gui_manager.createGUIButton( - pos=(0.7, 0, 0.28 - i * 0.055), - text=name, - size=0.045 - ) - self.gui_elements.append(button) - world.accept(f"music_system_function_{i}", callback) - - except Exception as e: - print(f"⚠ GUI创建失败: {e}") - - def _cleanup_gui(self): - """ - 清理GUI界面 - """ - try: - world = self.plugin_manager.world - - if hasattr(world, 'gui_manager') and world.gui_manager: - gui_manager = world.gui_manager - - # 删除所有GUI元素 - for element in self.gui_elements: - gui_manager.deleteGUIElement(element) - - self.gui_elements.clear() - - except Exception as e: - print(f"⚠ GUI清理失败: {e}") - - # 事件处理方法 - def toggle_debug_mode(self): - """切换调试模式""" - print("✓ 动态音乐系统调试模式切换") - - def toggle_music_system(self): - """切换音乐系统""" - if self.is_playing: - self.stop_all_music() - print("✓ 动态音乐系统已停止") - else: - self.play_theme("ambient") - print("✓ 动态音乐系统已启动") - - def play_test_theme(self): - """播放测试主题""" - print("✓ 播放测试音乐主题") - try: - # 创建一个测试主题 - self.create_theme("test_theme", { - 'layers': ['test_layer_1', 'test_layer_2'], - 'bpm': 120, - 'key': 'C', - 'mood': 'neutral' - }) - self.play_theme("test_theme") - except Exception as e: - print(f"✗ 播放测试主题失败: {e}") - - def increase_intensity(self): - """增加音乐强度""" - self.target_intensity = min(1.0, self.target_intensity + 0.1) - print(f"✓ 音乐强度增加到: {self.target_intensity:.2f}") - - def decrease_intensity(self): - """降低音乐强度""" - self.target_intensity = max(0.0, self.target_intensity - 0.1) - print(f"✓ 音乐强度降低到: {self.target_intensity:.2f}") - - def trigger_battle_music(self): - """触发战斗音乐""" - print("✓ 触发战斗音乐") - self.target_intensity = 1.0 - # 这里可以添加更多战斗音乐相关的逻辑 - - def trigger_exploration_music(self): - """触发探索音乐""" - print("✓ 触发探索音乐") - self.target_intensity = 0.6 - # 这里可以添加更多探索音乐相关的逻辑 - - def trigger_ambient_music(self): - """触发环境音乐""" - print("✓ 触发环境音乐") - self.target_intensity = 0.3 - # 这里可以添加更多环境音乐相关的逻辑 - - def show_stats(self): - """显示音乐系统统计信息""" - stats = self.get_stats() - print("=== 动态音乐系统统计信息 ===") - for key, value in stats.items(): - print(f" {key}: {value}") - - def toggle_auto_intensity(self): - """切换自动强度控制""" - self.auto_intensity = not self.auto_intensity - print(f"✓ 自动强度控制已{'启用' if self.auto_intensity else '禁用'}") - - def show_detailed_stats(self): - """显示详细统计信息""" - print("=== 动态音乐系统详细统计信息 ===") - - # 显示各组件统计 - components = [ - ('音乐管理器', self.music_manager), - ('过渡管理器', self.transition_manager), - ('作曲引擎', self.composition_engine), - ('分析引擎', self.analysis_engine), - ('效果处理器', self.effect_processor) - ] - - for name, component in components: - if component: - component_stats = component.get_stats() - print(f"{name}统计:") - for key, value in component_stats.items(): - print(f" {key}: {value}") - - # 显示性能统计 - perf_stats = self.performance_stats - print("性能统计:") - for key, value in perf_stats.items(): - print(f" {key}: {value}") - - def reset_music_system(self): - """重置音乐系统""" - print("✓ 重置动态音乐系统") - self.current_theme = None - self.current_intensity = 0.0 - self.target_intensity = 0.0 - self.current_layer_volumes.clear() - self.stop_all_music() - print("✓ 动态音乐系统已重置") - - def _load_music_resources(self): - """加载音乐资源""" - # 这里应该加载实际的音乐文件 - # 为演示目的,我们创建一些虚拟的音乐主题和层 - - # 创建示例主题 - self.themes = { - 'ambient': { - 'layers': ['ambient_base', 'ambient_high'], - 'bpm': 60, - 'key': 'Am', - 'mood': 'calm' - }, - 'exploration': { - 'layers': ['exploration_base', 'exploration_melody'], - 'bpm': 90, - 'key': 'C', - 'mood': 'adventurous' - }, - 'battle': { - 'layers': ['battle_drums', 'battle_melody', 'battle_bass'], - 'bpm': 140, - 'key': 'Em', - 'mood': 'intense' - }, - 'victory': { - 'layers': ['victory_fanfare', 'victory_melody'], - 'bpm': 120, - 'key': 'C', - 'mood': 'triumphant' - }, - 'defeat': { - 'layers': ['defeat_ambient', 'defeat_melody'], - 'bpm': 50, - 'key': 'Am', - 'mood': 'melancholic' - } - } - - # 创建示例层 - self.layers = { - 'ambient_base': {'volume': 0.6, 'loop': True, 'priority': 1}, - 'ambient_high': {'volume': 0.3, 'loop': True, 'priority': 2}, - 'exploration_base': {'volume': 0.7, 'loop': True, 'priority': 1}, - 'exploration_melody': {'volume': 0.5, 'loop': True, 'priority': 2}, - 'battle_drums': {'volume': 0.8, 'loop': True, 'priority': 1}, - 'battle_melody': {'volume': 0.7, 'loop': True, 'priority': 2}, - 'battle_bass': {'volume': 0.6, 'loop': True, 'priority': 3}, - 'victory_fanfare': {'volume': 1.0, 'loop': False, 'priority': 1}, - 'victory_melody': {'volume': 0.8, 'loop': False, 'priority': 2}, - 'defeat_ambient': {'volume': 0.4, 'loop': True, 'priority': 1}, - 'defeat_melody': {'volume': 0.6, 'loop': True, 'priority': 2} - } - - # 创建示例过渡 - self.transitions = { - 'ambient_to_exploration': { - 'from_theme': 'ambient', - 'to_theme': 'exploration', - 'duration': 3.0, - 'type': 'crossfade' - }, - 'exploration_to_battle': { - 'from_theme': 'exploration', - 'to_theme': 'battle', - 'duration': 2.0, - 'type': 'crossfade' - }, - 'battle_to_victory': { - 'from_theme': 'battle', - 'to_theme': 'victory', - 'duration': 1.5, - 'type': 'cut' - }, - 'battle_to_defeat': { - 'from_theme': 'battle', - 'to_theme': 'defeat', - 'duration': 1.5, - 'type': 'cut' - } - } - - self.performance_stats['themes_loaded'] = len(self.themes) - print("✓ 音乐资源加载完成") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取插件统计信息 - - Returns: - 统计信息字典 - """ - stats = self.performance_stats.copy() - - # 收集各组件统计 - components = [ - self.music_manager, - self.transition_manager, - self.composition_engine, - self.analysis_engine, - self.effect_processor - ] - - component_names = [ - 'music', 'transition', 'composition', 'analysis', 'effect' - ] - - for i, component in enumerate(components): - if component: - component_stats = component.get_stats() - for key, value in component_stats.items(): - stats[f"{component_names[i]}_{key}"] = value - - # 添加音乐系统统计 - stats['current_theme'] = self.current_theme - stats['current_intensity'] = self.current_intensity - stats['target_intensity'] = self.target_intensity - stats['is_playing'] = self.is_playing - stats['auto_intensity'] = self.auto_intensity - - return stats - - # 公共接口方法 - def create_theme(self, theme_name: str, theme_data: Dict[str, Any]) -> bool: - """ - 创建音乐主题 - - Args: - theme_name: 主题名称 - theme_data: 主题数据 - - Returns: - 是否创建成功 - """ - try: - self.themes[theme_name] = theme_data.copy() - print(f"✓ 音乐主题创建成功: {theme_name}") - return True - except Exception as e: - print(f"✗ 创建音乐主题失败: {e}") - return False - - def play_theme(self, theme_name: str, force: bool = False) -> bool: - """ - 播放音乐主题 - - Args: - theme_name: 主题名称 - force: 是否强制播放(跳过过渡) - - Returns: - 是否播放成功 - """ - if theme_name not in self.themes: - print(f"✗ 音乐主题不存在: {theme_name}") - return False - - try: - if self.music_manager: - success = self.music_manager.play_theme(theme_name, force) - if success: - self.current_theme = theme_name - self.is_playing = True - print(f"✓ 音乐主题开始播放: {theme_name}") - return success - return False - except Exception as e: - print(f"✗ 播放音乐主题失败: {e}") - return False - - def stop_theme(self, theme_name: str) -> bool: - """ - 停止音乐主题 - - Args: - theme_name: 主题名称 - - Returns: - 是否停止成功 - """ - if theme_name not in self.themes: - print(f"✗ 音乐主题不存在: {theme_name}") - return False - - try: - if self.music_manager: - success = self.music_manager.stop_theme(theme_name) - if success and self.current_theme == theme_name: - self.current_theme = None - if not self.music_manager.get_active_themes(): - self.is_playing = False - print(f"✓ 音乐主题已停止: {theme_name}") - return success - return False - except Exception as e: - print(f"✗ 停止音乐主题失败: {e}") - return False - - def stop_all_music(self): - """停止所有音乐""" - try: - if self.music_manager: - self.music_manager.stop_all() - self.current_theme = None - self.is_playing = False - print("✓ 所有音乐已停止") - except Exception as e: - print(f"✗ 停止所有音乐失败: {e}") - - def set_intensity(self, intensity: float): - """ - 设置音乐强度 - - Args: - intensity: 强度值 (0.0-1.0) - """ - self.target_intensity = max(0.0, min(1.0, intensity)) - print(f"✓ 音乐强度设置为: {self.target_intensity:.2f}") - - def get_intensity(self) -> float: - """ - 获取当前音乐强度 - - Returns: - 当前强度值 (0.0-1.0) - """ - return self.current_intensity - - def set_layer_volume(self, layer_name: str, volume: float): - """ - 设置音乐层音量 - - Args: - layer_name: 层名称 - volume: 音量值 (0.0-1.0) - """ - if self.music_manager: - self.music_manager.set_layer_volume(layer_name, volume) - self.current_layer_volumes[layer_name] = volume - print(f"✓ 音乐层 {layer_name} 音量设置为: {volume:.2f}") - - def get_layer_volume(self, layer_name: str) -> float: - """ - 获取音乐层音量 - - Args: - layer_name: 层名称 - - Returns: - 音量值 (0.0-1.0) - """ - return self.current_layer_volumes.get(layer_name, 1.0) - - def create_transition(self, transition_name: str, transition_data: Dict[str, Any]) -> bool: - """ - 创建音乐过渡 - - Args: - transition_name: 过渡名称 - transition_data: 过渡数据 - - Returns: - 是否创建成功 - """ - try: - self.transitions[transition_name] = transition_data.copy() - if self.transition_manager: - self.transition_manager.register_transition(transition_name, transition_data) - print(f"✓ 音乐过渡创建成功: {transition_name}") - return True - except Exception as e: - print(f"✗ 创建音乐过渡失败: {e}") - return False - - def trigger_transition(self, transition_name: str) -> bool: - """ - 触发音乐过渡 - - Args: - transition_name: 过渡名称 - - Returns: - 是否触发成功 - """ - if transition_name not in self.transitions: - print(f"✗ 音乐过渡不存在: {transition_name}") - return False - - try: - if self.transition_manager: - success = self.transition_manager.trigger_transition(transition_name) - if success: - self.performance_stats['transitions_processed'] += 1 - print(f"✓ 音乐过渡已触发: {transition_name}") - return success - return False - except Exception as e: - print(f"✗ 触发音乐过渡失败: {e}") - return False - - def set_game_state(self, state: Dict[str, Any]): - """ - 设置游戏状态 - - Args: - state: 游戏状态字典 - """ - self.game_state.update(state) - print(f"✓ 游戏状态已更新: {state}") - - def set_environment_conditions(self, conditions: Dict[str, Any]): - """ - 设置环境条件 - - Args: - conditions: 环境条件字典 - """ - self.environment_conditions.update(conditions) - print(f"✓ 环境条件已更新: {conditions}") - - def set_player_state(self, state: Dict[str, Any]): - """ - 设置玩家状态 - - Args: - state: 玩家状态字典 - """ - self.player_state.update(state) - print(f"✓ 玩家状态已更新: {state}") - - def set_master_volume(self, volume: float): - """ - 设置主音量 - - Args: - volume: 主音量 (0.0-1.0) - """ - self.master_volume = max(0.0, min(1.0, volume)) - if self.music_manager: - self.music_manager.set_master_volume(self.master_volume) - print(f"✓ 主音量已设置: {self.master_volume:.2f}") - - def get_master_volume(self) -> float: - """ - 获取主音量 - - Returns: - 主音量 (0.0-1.0) - """ - return self.master_volume - - def update(self, dt: float): - """ - 更新插件状态 - - Args: - dt: 时间增量 - """ - update_start_time = time.time() - - # 更新当前强度(平滑过渡到目标强度) - if self.current_intensity != self.target_intensity: - intensity_diff = self.target_intensity - self.current_intensity - self.current_intensity += intensity_diff * self.intensity_smoothing - # 确保不会超过目标值 - if (intensity_diff > 0 and self.current_intensity > self.target_intensity) or \ - (intensity_diff < 0 and self.current_intensity < self.target_intensity): - self.current_intensity = self.target_intensity - - # 更新各组件 - components = [ - self.music_manager, - self.transition_manager, - self.composition_engine, - self.analysis_engine, - self.effect_processor - ] - - for component in components: - if component: - component.update(dt) - - # 更新性能统计 - self.performance_stats['total_updates'] += 1 - self.performance_stats['last_update_time'] = time.time() - self.performance_stats['total_processing_time'] += (time.time() - update_start_time) - - # 自动强度控制 - if self.auto_intensity: - self._update_auto_intensity() - - def _update_auto_intensity(self): - """更新自动强度控制""" - # 基于游戏状态自动调整音乐强度 - # 这里是一个简化的实现,实际应用中会更复杂 - - # 检查是否有战斗状态 - if self.game_state.get('in_combat', False): - self.target_intensity = 1.0 - # 检查是否有探索状态 - elif self.game_state.get('exploring', False): - self.target_intensity = 0.6 - # 默认环境音乐 - else: +""" +动态音乐系统插件 +根据游戏状态和环境条件动态生成和控制音乐 +""" + +import math +import time +import uuid +from typing import Dict, List, Optional, Any, Callable +from plugins.plugin_manager import BasePlugin +from panda3d.core import AudioSound, NodePath + +class Plugin(BasePlugin): + """ + 动态音乐系统插件 + 根据游戏状态和环境条件动态生成和控制音乐 + """ + + def __init__(self, plugin_manager, name): + super().__init__(plugin_manager, name) + self.config = { + "version": "1.0.0", + "author": "EG Team", + "description": "动态音乐系统,支持根据游戏状态和环境条件实时生成和控制音乐" + } + + # 核心组件 + self.music_manager = None + self.transition_manager = None + self.composition_engine = None + self.analysis_engine = None + self.effect_processor = None + + # 音乐状态 + self.current_theme = None + self.current_intensity = 0.0 + self.target_intensity = 0.0 + self.intensity_change_rate = 1.0 + self.current_layer_volumes = {} + + # 游戏状态跟踪 + self.game_state = {} + self.environment_conditions = {} + self.player_state = {} + + # 音乐库 + self.themes = {} + self.layers = {} + self.transitions = {} + self.sound_effects = {} + + # GUI元素 + self.gui_elements = [] + + # 事件处理器 + self.event_handlers = {} + + # 插件状态 + self.is_initialized = False + self.is_enabled = False + self.is_playing = False + + # 性能监控 + self.performance_stats = { + 'initialization_time': 0.0, + 'enable_time': 0.0, + 'last_update_time': 0.0, + 'total_updates': 0, + 'total_processing_time': 0.0, + 'themes_loaded': 0, + 'transitions_processed': 0 + } + + # 配置参数 + self.master_volume = 1.0 + self.crossfade_duration = 2.0 + self.intensity_smoothing = 0.1 + self.auto_intensity = True + + def initialize(self) -> bool: + """ + 初始化插件 + """ + try: + if self.is_initialized: + return True + + init_start_time = time.time() + print(f"初始化动态音乐系统插件: {self.name}") + + # 初始化核心组件 + from .core.music_manager import MusicManager + from .transitions.transition_manager import TransitionManager + from .composition.composition_engine import CompositionEngine + from .analysis.analysis_engine import AnalysisEngine + from .effects.effect_processor import EffectProcessor + + self.music_manager = MusicManager(self) + self.transition_manager = TransitionManager(self) + self.composition_engine = CompositionEngine(self) + self.analysis_engine = AnalysisEngine(self) + self.effect_processor = EffectProcessor(self) + + # 加载音乐资源 + self._load_music_resources() + + self.is_initialized = True + self.performance_stats['initialization_time'] = time.time() - init_start_time + print("✓ 动态音乐系统插件初始化完成") + return True + + except Exception as e: + print(f"✗ 动态音乐系统插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + """ + if not super().enable(): + return False + + if self.is_enabled: + return True + + try: + enable_start_time = time.time() + print(f"启用动态音乐系统插件: {self.name}") + + # 注册事件处理器 + self._register_event_handlers() + + # 创建GUI界面 + self._create_gui() + + self.is_enabled = True + self.performance_stats['enable_time'] = time.time() - enable_start_time + print("✓ 动态音乐系统插件启用成功") + return True + + except Exception as e: + print(f"✗ 动态音乐系统插件启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self) -> bool: + """ + 禁用插件 + """ + if not super().disable(): + return False + + if not self.is_enabled: + return True + + try: + print(f"禁用动态音乐系统插件: {self.name}") + + # 清理GUI界面 + self._cleanup_gui() + + # 移除事件处理器 + self._unregister_event_handlers() + + # 停止所有音乐 + self.stop_all_music() + + self.is_enabled = False + print("✓ 动态音乐系统插件禁用成功") + return True + + except Exception as e: + print(f"✗ 动态音乐系统插件禁用失败: {e}") + import traceback + traceback.print_exc() + return False + + def finalize(self): + """ + 清理插件资源 + """ + print(f"清理动态音乐系统插件资源: {self.name}") + + # 停止所有音乐 + self.stop_all_music() + + # 清理所有组件 + components = [ + self.music_manager, + self.transition_manager, + self.composition_engine, + self.analysis_engine, + self.effect_processor + ] + + for component in components: + if component: + component.cleanup() + + self.music_manager = None + self.transition_manager = None + self.composition_engine = None + self.analysis_engine = None + self.effect_processor = None + + self.themes.clear() + self.layers.clear() + self.transitions.clear() + self.sound_effects.clear() + + self.is_initialized = False + self.is_enabled = False + + def _register_event_handlers(self): + """ + 注册事件处理器 + """ + world = self.plugin_manager.world + + # 音乐控制快捷键 + self.event_handlers['f1'] = self.toggle_debug_mode + self.event_handlers['f2'] = self.toggle_music_system + self.event_handlers['f3'] = self.play_test_theme + self.event_handlers['f4'] = self.increase_intensity + self.event_handlers['f5'] = self.decrease_intensity + self.event_handlers['f6'] = self.trigger_battle_music + self.event_handlers['f7'] = self.trigger_exploration_music + self.event_handlers['f8'] = self.trigger_ambient_music + self.event_handlers['f9'] = self.show_stats + self.event_handlers['f10'] = self.toggle_auto_intensity + self.event_handlers['f11'] = self.show_detailed_stats + self.event_handlers['f12'] = self.reset_music_system + + # 注册事件 + for event, handler in self.event_handlers.items(): + world.accept(event, handler) + world.accept(event.upper(), handler) # 同时注册大写事件 + + def _unregister_event_handlers(self): + """ + 移除事件处理器 + """ + world = self.plugin_manager.world + + for event in self.event_handlers.keys(): + world.ignore(event) + world.ignore(event.upper()) + + self.event_handlers.clear() + + def _create_gui(self): + """ + 创建GUI界面 + """ + try: + world = self.plugin_manager.world + + # 检查是否有GUI管理器 + if not hasattr(world, 'gui_manager') or not world.gui_manager: + print("⚠ GUI管理器不可用,跳过GUI创建") + return + + gui_manager = world.gui_manager + + # 创建音乐系统控制面板 + panel_button = gui_manager.createGUIButton( + pos=(0.7, 0, 0.35), + text="动态音乐", + size=0.06 + ) + self.gui_elements.append(panel_button) + + # 创建功能按钮 + functions = [ + ("调试模式 [F1]", self.toggle_debug_mode), + ("切换系统 [F2]", self.toggle_music_system), + ("测试主题 [F3]", self.play_test_theme), + ("增加强度 [F4]", self.increase_intensity), + ("降低强度 [F5]", self.decrease_intensity), + ("战斗音乐 [F6]", self.trigger_battle_music), + ("探索音乐 [F7]", self.trigger_exploration_music), + ("环境音乐 [F8]", self.trigger_ambient_music), + ("音乐统计 [F9]", self.show_stats), + ("自动强度 [F10]", self.toggle_auto_intensity), + ("详细统计 [F11]", self.show_detailed_stats), + ("重置系统 [F12]", self.reset_music_system) + ] + + for i, (name, callback) in enumerate(functions): + button = gui_manager.createGUIButton( + pos=(0.7, 0, 0.28 - i * 0.055), + text=name, + size=0.045 + ) + self.gui_elements.append(button) + world.accept(f"music_system_function_{i}", callback) + + except Exception as e: + print(f"⚠ GUI创建失败: {e}") + + def _cleanup_gui(self): + """ + 清理GUI界面 + """ + try: + world = self.plugin_manager.world + + if hasattr(world, 'gui_manager') and world.gui_manager: + gui_manager = world.gui_manager + + # 删除所有GUI元素 + for element in self.gui_elements: + gui_manager.deleteGUIElement(element) + + self.gui_elements.clear() + + except Exception as e: + print(f"⚠ GUI清理失败: {e}") + + # 事件处理方法 + def toggle_debug_mode(self): + """切换调试模式""" + print("✓ 动态音乐系统调试模式切换") + + def toggle_music_system(self): + """切换音乐系统""" + if self.is_playing: + self.stop_all_music() + print("✓ 动态音乐系统已停止") + else: + self.play_theme("ambient") + print("✓ 动态音乐系统已启动") + + def play_test_theme(self): + """播放测试主题""" + print("✓ 播放测试音乐主题") + try: + # 创建一个测试主题 + self.create_theme("test_theme", { + 'layers': ['test_layer_1', 'test_layer_2'], + 'bpm': 120, + 'key': 'C', + 'mood': 'neutral' + }) + self.play_theme("test_theme") + except Exception as e: + print(f"✗ 播放测试主题失败: {e}") + + def increase_intensity(self): + """增加音乐强度""" + self.target_intensity = min(1.0, self.target_intensity + 0.1) + print(f"✓ 音乐强度增加到: {self.target_intensity:.2f}") + + def decrease_intensity(self): + """降低音乐强度""" + self.target_intensity = max(0.0, self.target_intensity - 0.1) + print(f"✓ 音乐强度降低到: {self.target_intensity:.2f}") + + def trigger_battle_music(self): + """触发战斗音乐""" + print("✓ 触发战斗音乐") + self.target_intensity = 1.0 + # 这里可以添加更多战斗音乐相关的逻辑 + + def trigger_exploration_music(self): + """触发探索音乐""" + print("✓ 触发探索音乐") + self.target_intensity = 0.6 + # 这里可以添加更多探索音乐相关的逻辑 + + def trigger_ambient_music(self): + """触发环境音乐""" + print("✓ 触发环境音乐") + self.target_intensity = 0.3 + # 这里可以添加更多环境音乐相关的逻辑 + + def show_stats(self): + """显示音乐系统统计信息""" + stats = self.get_stats() + print("=== 动态音乐系统统计信息 ===") + for key, value in stats.items(): + print(f" {key}: {value}") + + def toggle_auto_intensity(self): + """切换自动强度控制""" + self.auto_intensity = not self.auto_intensity + print(f"✓ 自动强度控制已{'启用' if self.auto_intensity else '禁用'}") + + def show_detailed_stats(self): + """显示详细统计信息""" + print("=== 动态音乐系统详细统计信息 ===") + + # 显示各组件统计 + components = [ + ('音乐管理器', self.music_manager), + ('过渡管理器', self.transition_manager), + ('作曲引擎', self.composition_engine), + ('分析引擎', self.analysis_engine), + ('效果处理器', self.effect_processor) + ] + + for name, component in components: + if component: + component_stats = component.get_stats() + print(f"{name}统计:") + for key, value in component_stats.items(): + print(f" {key}: {value}") + + # 显示性能统计 + perf_stats = self.performance_stats + print("性能统计:") + for key, value in perf_stats.items(): + print(f" {key}: {value}") + + def reset_music_system(self): + """重置音乐系统""" + print("✓ 重置动态音乐系统") + self.current_theme = None + self.current_intensity = 0.0 + self.target_intensity = 0.0 + self.current_layer_volumes.clear() + self.stop_all_music() + print("✓ 动态音乐系统已重置") + + def _load_music_resources(self): + """加载音乐资源""" + # 这里应该加载实际的音乐文件 + # 为演示目的,我们创建一些虚拟的音乐主题和层 + + # 创建示例主题 + self.themes = { + 'ambient': { + 'layers': ['ambient_base', 'ambient_high'], + 'bpm': 60, + 'key': 'Am', + 'mood': 'calm' + }, + 'exploration': { + 'layers': ['exploration_base', 'exploration_melody'], + 'bpm': 90, + 'key': 'C', + 'mood': 'adventurous' + }, + 'battle': { + 'layers': ['battle_drums', 'battle_melody', 'battle_bass'], + 'bpm': 140, + 'key': 'Em', + 'mood': 'intense' + }, + 'victory': { + 'layers': ['victory_fanfare', 'victory_melody'], + 'bpm': 120, + 'key': 'C', + 'mood': 'triumphant' + }, + 'defeat': { + 'layers': ['defeat_ambient', 'defeat_melody'], + 'bpm': 50, + 'key': 'Am', + 'mood': 'melancholic' + } + } + + # 创建示例层 + self.layers = { + 'ambient_base': {'volume': 0.6, 'loop': True, 'priority': 1}, + 'ambient_high': {'volume': 0.3, 'loop': True, 'priority': 2}, + 'exploration_base': {'volume': 0.7, 'loop': True, 'priority': 1}, + 'exploration_melody': {'volume': 0.5, 'loop': True, 'priority': 2}, + 'battle_drums': {'volume': 0.8, 'loop': True, 'priority': 1}, + 'battle_melody': {'volume': 0.7, 'loop': True, 'priority': 2}, + 'battle_bass': {'volume': 0.6, 'loop': True, 'priority': 3}, + 'victory_fanfare': {'volume': 1.0, 'loop': False, 'priority': 1}, + 'victory_melody': {'volume': 0.8, 'loop': False, 'priority': 2}, + 'defeat_ambient': {'volume': 0.4, 'loop': True, 'priority': 1}, + 'defeat_melody': {'volume': 0.6, 'loop': True, 'priority': 2} + } + + # 创建示例过渡 + self.transitions = { + 'ambient_to_exploration': { + 'from_theme': 'ambient', + 'to_theme': 'exploration', + 'duration': 3.0, + 'type': 'crossfade' + }, + 'exploration_to_battle': { + 'from_theme': 'exploration', + 'to_theme': 'battle', + 'duration': 2.0, + 'type': 'crossfade' + }, + 'battle_to_victory': { + 'from_theme': 'battle', + 'to_theme': 'victory', + 'duration': 1.5, + 'type': 'cut' + }, + 'battle_to_defeat': { + 'from_theme': 'battle', + 'to_theme': 'defeat', + 'duration': 1.5, + 'type': 'cut' + } + } + + self.performance_stats['themes_loaded'] = len(self.themes) + print("✓ 音乐资源加载完成") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取插件统计信息 + + Returns: + 统计信息字典 + """ + stats = self.performance_stats.copy() + + # 收集各组件统计 + components = [ + self.music_manager, + self.transition_manager, + self.composition_engine, + self.analysis_engine, + self.effect_processor + ] + + component_names = [ + 'music', 'transition', 'composition', 'analysis', 'effect' + ] + + for i, component in enumerate(components): + if component: + component_stats = component.get_stats() + for key, value in component_stats.items(): + stats[f"{component_names[i]}_{key}"] = value + + # 添加音乐系统统计 + stats['current_theme'] = self.current_theme + stats['current_intensity'] = self.current_intensity + stats['target_intensity'] = self.target_intensity + stats['is_playing'] = self.is_playing + stats['auto_intensity'] = self.auto_intensity + + return stats + + # 公共接口方法 + def create_theme(self, theme_name: str, theme_data: Dict[str, Any]) -> bool: + """ + 创建音乐主题 + + Args: + theme_name: 主题名称 + theme_data: 主题数据 + + Returns: + 是否创建成功 + """ + try: + self.themes[theme_name] = theme_data.copy() + print(f"✓ 音乐主题创建成功: {theme_name}") + return True + except Exception as e: + print(f"✗ 创建音乐主题失败: {e}") + return False + + def play_theme(self, theme_name: str, force: bool = False) -> bool: + """ + 播放音乐主题 + + Args: + theme_name: 主题名称 + force: 是否强制播放(跳过过渡) + + Returns: + 是否播放成功 + """ + if theme_name not in self.themes: + print(f"✗ 音乐主题不存在: {theme_name}") + return False + + try: + if self.music_manager: + success = self.music_manager.play_theme(theme_name, force) + if success: + self.current_theme = theme_name + self.is_playing = True + print(f"✓ 音乐主题开始播放: {theme_name}") + return success + return False + except Exception as e: + print(f"✗ 播放音乐主题失败: {e}") + return False + + def stop_theme(self, theme_name: str) -> bool: + """ + 停止音乐主题 + + Args: + theme_name: 主题名称 + + Returns: + 是否停止成功 + """ + if theme_name not in self.themes: + print(f"✗ 音乐主题不存在: {theme_name}") + return False + + try: + if self.music_manager: + success = self.music_manager.stop_theme(theme_name) + if success and self.current_theme == theme_name: + self.current_theme = None + if not self.music_manager.get_active_themes(): + self.is_playing = False + print(f"✓ 音乐主题已停止: {theme_name}") + return success + return False + except Exception as e: + print(f"✗ 停止音乐主题失败: {e}") + return False + + def stop_all_music(self): + """停止所有音乐""" + try: + if self.music_manager: + self.music_manager.stop_all() + self.current_theme = None + self.is_playing = False + print("✓ 所有音乐已停止") + except Exception as e: + print(f"✗ 停止所有音乐失败: {e}") + + def set_intensity(self, intensity: float): + """ + 设置音乐强度 + + Args: + intensity: 强度值 (0.0-1.0) + """ + self.target_intensity = max(0.0, min(1.0, intensity)) + print(f"✓ 音乐强度设置为: {self.target_intensity:.2f}") + + def get_intensity(self) -> float: + """ + 获取当前音乐强度 + + Returns: + 当前强度值 (0.0-1.0) + """ + return self.current_intensity + + def set_layer_volume(self, layer_name: str, volume: float): + """ + 设置音乐层音量 + + Args: + layer_name: 层名称 + volume: 音量值 (0.0-1.0) + """ + if self.music_manager: + self.music_manager.set_layer_volume(layer_name, volume) + self.current_layer_volumes[layer_name] = volume + print(f"✓ 音乐层 {layer_name} 音量设置为: {volume:.2f}") + + def get_layer_volume(self, layer_name: str) -> float: + """ + 获取音乐层音量 + + Args: + layer_name: 层名称 + + Returns: + 音量值 (0.0-1.0) + """ + return self.current_layer_volumes.get(layer_name, 1.0) + + def create_transition(self, transition_name: str, transition_data: Dict[str, Any]) -> bool: + """ + 创建音乐过渡 + + Args: + transition_name: 过渡名称 + transition_data: 过渡数据 + + Returns: + 是否创建成功 + """ + try: + self.transitions[transition_name] = transition_data.copy() + if self.transition_manager: + self.transition_manager.register_transition(transition_name, transition_data) + print(f"✓ 音乐过渡创建成功: {transition_name}") + return True + except Exception as e: + print(f"✗ 创建音乐过渡失败: {e}") + return False + + def trigger_transition(self, transition_name: str) -> bool: + """ + 触发音乐过渡 + + Args: + transition_name: 过渡名称 + + Returns: + 是否触发成功 + """ + if transition_name not in self.transitions: + print(f"✗ 音乐过渡不存在: {transition_name}") + return False + + try: + if self.transition_manager: + success = self.transition_manager.trigger_transition(transition_name) + if success: + self.performance_stats['transitions_processed'] += 1 + print(f"✓ 音乐过渡已触发: {transition_name}") + return success + return False + except Exception as e: + print(f"✗ 触发音乐过渡失败: {e}") + return False + + def set_game_state(self, state: Dict[str, Any]): + """ + 设置游戏状态 + + Args: + state: 游戏状态字典 + """ + self.game_state.update(state) + print(f"✓ 游戏状态已更新: {state}") + + def set_environment_conditions(self, conditions: Dict[str, Any]): + """ + 设置环境条件 + + Args: + conditions: 环境条件字典 + """ + self.environment_conditions.update(conditions) + print(f"✓ 环境条件已更新: {conditions}") + + def set_player_state(self, state: Dict[str, Any]): + """ + 设置玩家状态 + + Args: + state: 玩家状态字典 + """ + self.player_state.update(state) + print(f"✓ 玩家状态已更新: {state}") + + def set_master_volume(self, volume: float): + """ + 设置主音量 + + Args: + volume: 主音量 (0.0-1.0) + """ + self.master_volume = max(0.0, min(1.0, volume)) + if self.music_manager: + self.music_manager.set_master_volume(self.master_volume) + print(f"✓ 主音量已设置: {self.master_volume:.2f}") + + def get_master_volume(self) -> float: + """ + 获取主音量 + + Returns: + 主音量 (0.0-1.0) + """ + return self.master_volume + + def update(self, dt: float): + """ + 更新插件状态 + + Args: + dt: 时间增量 + """ + update_start_time = time.time() + + # 更新当前强度(平滑过渡到目标强度) + if self.current_intensity != self.target_intensity: + intensity_diff = self.target_intensity - self.current_intensity + self.current_intensity += intensity_diff * self.intensity_smoothing + # 确保不会超过目标值 + if (intensity_diff > 0 and self.current_intensity > self.target_intensity) or \ + (intensity_diff < 0 and self.current_intensity < self.target_intensity): + self.current_intensity = self.target_intensity + + # 更新各组件 + components = [ + self.music_manager, + self.transition_manager, + self.composition_engine, + self.analysis_engine, + self.effect_processor + ] + + for component in components: + if component: + component.update(dt) + + # 更新性能统计 + self.performance_stats['total_updates'] += 1 + self.performance_stats['last_update_time'] = time.time() + self.performance_stats['total_processing_time'] += (time.time() - update_start_time) + + # 自动强度控制 + if self.auto_intensity: + self._update_auto_intensity() + + def _update_auto_intensity(self): + """更新自动强度控制""" + # 基于游戏状态自动调整音乐强度 + # 这里是一个简化的实现,实际应用中会更复杂 + + # 检查是否有战斗状态 + if self.game_state.get('in_combat', False): + self.target_intensity = 1.0 + # 检查是否有探索状态 + elif self.game_state.get('exploring', False): + self.target_intensity = 0.6 + # 默认环境音乐 + else: self.target_intensity = 0.3 \ No newline at end of file diff --git a/plugins/user/dynamic_music_system/transitions/transition_manager.py b/plugins/user/dynamic_music_system/transitions/transition_manager.py index 52b74356..dffe9d03 100644 --- a/plugins/user/dynamic_music_system/transitions/transition_manager.py +++ b/plugins/user/dynamic_music_system/transitions/transition_manager.py @@ -1,560 +1,560 @@ -""" -过渡管理器 -负责音乐主题之间的平滑过渡 -""" - -import uuid -import math -import time -import numpy as np -from typing import Dict, List, Any, Optional - -class TransitionManager: - """ - 过渡管理器 - 负责音乐主题之间的平滑过渡 - """ - - def __init__(self, plugin): - """ - 初始化过渡管理器 - - Args: - plugin: 动态音乐系统插件实例 - """ - self.plugin = plugin - self.transitions: Dict[str, Dict[str, Any]] = {} - self.active_transitions: Dict[str, Dict[str, Any]] = {} - self.pending_transitions: List[Dict[str, Any]] = [] - - self.stats = { - 'transitions_registered': 0, - 'transitions_active': 0, - 'transitions_completed': 0, - 'transitions_failed': 0, - 'processing_time': 0.0 - } - - self.default_duration = 2.0 - self.default_curve = 'linear' - - # 初始化过渡管理器 - self._initialize_transition_manager() - - def _initialize_transition_manager(self): - """初始化过渡管理器""" - print("✓ 过渡管理器初始化完成") - - def register_transition(self, transition_name: str, transition_data: Dict[str, Any]) -> bool: - """ - 注册音乐过渡 - - Args: - transition_name: 过渡名称 - transition_data: 过渡数据 - - Returns: - 是否注册成功 - """ - try: - # 验证必需字段 - required_fields = ['from_theme', 'to_theme'] - for field in required_fields: - if field not in transition_data: - print(f"✗ 过渡数据缺少必需字段: {field}") - return False - - # 设置默认值 - transition_data.setdefault('duration', self.default_duration) - transition_data.setdefault('type', 'crossfade') - transition_data.setdefault('curve', self.default_curve) - transition_data.setdefault('interruptible', True) - transition_data.setdefault('priority', 1) - - # 存储过渡数据 - self.transitions[transition_name] = transition_data.copy() - self.stats['transitions_registered'] += 1 - - print(f"✓ 音乐过渡注册成功: {transition_name}") - return True - - except Exception as e: - print(f"✗ 注册音乐过渡失败: {e}") - return False - - def trigger_transition(self, transition_name: str) -> bool: - """ - 触发音乐过渡 - - Args: - transition_name: 过渡名称 - - Returns: - 是否触发成功 - """ - try: - if transition_name not in self.transitions: - print(f"✗ 音乐过渡不存在: {transition_name}") - return False - - transition_data = self.transitions[transition_name] - - # 检查是否可以触发过渡 - if not self._can_trigger_transition(transition_name, transition_data): - print(f"⚠ 音乐过渡无法触发: {transition_name}") - self.stats['transitions_failed'] += 1 - return False - - # 创建过渡实例 - transition_instance = { - 'name': transition_name, - 'from_theme': transition_data['from_theme'], - 'to_theme': transition_data['to_theme'], - 'type': transition_data['type'], - 'duration': transition_data['duration'], - 'curve': transition_data['curve'], - 'start_time': time.time(), - 'elapsed_time': 0.0, - 'state': 'starting' - } - - # 根据过渡类型处理 - if transition_data['type'] == 'crossfade': - success = self._start_crossfade(transition_instance) - elif transition_data['type'] == 'cut': - success = self._start_cut(transition_instance) - elif transition_data['type'] == 'stinger': - success = self._start_stinger(transition_instance) - else: - print(f"✗ 不支持的过渡类型: {transition_data['type']}") - self.stats['transitions_failed'] += 1 - return False - - if success: - # 存储活动过渡 - self.active_transitions[transition_name] = transition_instance - self.stats['transitions_active'] = len(self.active_transitions) - print(f"✓ 音乐过渡已触发: {transition_name}") - return True - else: - self.stats['transitions_failed'] += 1 - return False - - except Exception as e: - print(f"✗ 触发音乐过渡失败: {e}") - self.stats['transitions_failed'] += 1 - return False - - def _can_trigger_transition(self, transition_name: str, transition_data: Dict[str, Any]) -> bool: - """ - 检查是否可以触发过渡 - - Args: - transition_name: 过渡名称 - transition_data: 过渡数据 - - Returns: - 是否可以触发 - """ - # 检查来源主题是否正在播放 - from_theme = transition_data['from_theme'] - if from_theme != 'any' and self.plugin.music_manager: - if not self.plugin.music_manager.is_theme_playing(from_theme): - print(f"⚠ 来源主题未在播放: {from_theme}") - return False - - # 检查是否有冲突的活动过渡 - for active_name, active_transition in self.active_transitions.items(): - # 检查是否涉及相同的主题 - if (active_transition['from_theme'] == from_theme or - active_transition['to_theme'] == from_theme or - active_transition['from_theme'] == transition_data['to_theme'] or - active_transition['to_theme'] == transition_data['to_theme']): - - # 检查优先级 - active_priority = self.transitions[active_name].get('priority', 1) - new_priority = transition_data.get('priority', 1) - - if new_priority < active_priority: - print(f"⚠ 更高优先级的过渡正在进行: {active_name}") - return False - elif not self.transitions[active_name].get('interruptible', True): - print(f"⚠ 当前过渡不可中断: {active_name}") - return False - - return True - - def _start_crossfade(self, transition_instance: Dict[str, Any]) -> bool: - """ - 开始交叉淡入淡出过渡 - - Args: - transition_instance: 过渡实例 - - Returns: - 是否开始成功 - """ - try: - from_theme = transition_instance['from_theme'] - to_theme = transition_instance['to_theme'] - duration = transition_instance['duration'] - - # 淡出来源主题 - if from_theme != 'any' and self.plugin.music_manager: - self.plugin.music_manager.fade_out_theme(from_theme, duration) - - # 淡入目标主题 - if self.plugin.music_manager: - self.plugin.music_manager.fade_in_theme(to_theme, duration) - - transition_instance['state'] = 'active' - return True - - except Exception as e: - print(f"✗ 开始交叉淡入淡出过渡失败: {e}") - return False - - def _start_cut(self, transition_instance: Dict[str, Any]) -> bool: - """ - 开始直接切换过渡 - - Args: - transition_instance: 过渡实例 - - Returns: - 是否开始成功 - """ - try: - from_theme = transition_instance['from_theme'] - to_theme = transition_instance['to_theme'] - - # 停止来源主题 - if from_theme != 'any' and self.plugin.music_manager: - self.plugin.music_manager.stop_theme(from_theme) - - # 播放目标主题 - if self.plugin.music_manager: - self.plugin.music_manager.play_theme(to_theme, force=True) - - transition_instance['state'] = 'completed' - self.stats['transitions_completed'] += 1 - return True - - except Exception as e: - print(f"✗ 开始直接切换过渡失败: {e}") - return False - - def _start_stinger(self, transition_instance: Dict[str, Any]) -> bool: - """ - 开始使用刺音过渡 - - Args: - transition_instance: 过渡实例 - - Returns: - 是否开始成功 - """ - try: - from_theme = transition_instance['from_theme'] - to_theme = transition_instance['to_theme'] - duration = transition_instance['duration'] - - # 淡出来源主题 - if from_theme != 'any' and self.plugin.music_manager: - self.plugin.music_manager.fade_out_theme(from_theme, duration * 0.5) - - # 播放刺音效果(这里简化处理) - # 在实际实现中,会播放一个短的音频文件 - print("✓ 播放刺音效果") - - # 淡入目标主题 - if self.plugin.music_manager: - # 延迟淡入目标主题 - def delayed_fade_in(): - time.sleep(duration * 0.5) - self.plugin.music_manager.fade_in_theme(to_theme, duration * 0.5) - - import threading - threading.Thread(target=delayed_fade_in, daemon=True).start() - - transition_instance['state'] = 'active' - return True - - except Exception as e: - print(f"✗ 开始刺音过渡失败: {e}") - return False - - def cancel_transition(self, transition_name: str) -> bool: - """ - 取消音乐过渡 - - Args: - transition_name: 过渡名称 - - Returns: - 是否取消成功 - """ - try: - if transition_name not in self.active_transitions: - print(f"⚠ 过渡未在进行: {transition_name}") - return True - - # 取消过渡 - del self.active_transitions[transition_name] - self.stats['transitions_active'] = len(self.active_transitions) - - print(f"✓ 音乐过渡已取消: {transition_name}") - return True - - except Exception as e: - print(f"✗ 取消音乐过渡失败: {e}") - return False - - def update(self, dt: float): - """ - 更新过渡管理器状态 - - Args: - dt: 时间增量 - """ - update_start_time = time.time() - - # 更新活动过渡 - current_time = time.time() - completed_transitions = [] - - for transition_name, transition_instance in self.active_transitions.items(): - if transition_instance['state'] == 'active': - elapsed_time = current_time - transition_instance['start_time'] - duration = transition_instance['duration'] - - if elapsed_time >= duration: - # 过渡完成 - transition_instance['state'] = 'completed' - completed_transitions.append(transition_name) - self.stats['transitions_completed'] += 1 - - # 移除已完成的过渡 - for transition_name in completed_transitions: - if transition_name in self.active_transitions: - del self.active_transitions[transition_name] - - self.stats['transitions_active'] = len(self.active_transitions) - - # 更新性能统计 - self.stats['processing_time'] += (time.time() - update_start_time) - - def cleanup(self): - """清理所有资源""" - self.transitions.clear() - self.active_transitions.clear() - self.pending_transitions.clear() - - self.stats = { - 'transitions_registered': 0, - 'transitions_active': 0, - 'transitions_completed': 0, - 'transitions_failed': 0, - 'processing_time': 0.0 - } - - print("✓ 过渡管理器资源已清理") - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def get_active_transitions(self) -> List[str]: - """ - 获取活动过渡列表 - - Returns: - 活动过渡名称列表 - """ - return list(self.active_transitions.keys()) - - def get_transition_status(self, transition_name: str) -> Dict[str, Any]: - """ - 获取过渡状态 - - Args: - transition_name: 过渡名称 - - Returns: - 过渡状态字典 - """ - if transition_name in self.active_transitions: - transition_instance = self.active_transitions[transition_name] - return { - 'name': transition_name, - 'state': transition_instance['state'], - 'from_theme': transition_instance['from_theme'], - 'to_theme': transition_instance['to_theme'], - 'elapsed_time': transition_instance['elapsed_time'], - 'duration': transition_instance['duration'] - } - elif transition_name in self.transitions: - return { - 'name': transition_name, - 'state': 'registered', - 'from_theme': self.transitions[transition_name]['from_theme'], - 'to_theme': self.transitions[transition_name]['to_theme'] - } - else: - return { - 'name': transition_name, - 'state': 'unknown' - } - - def set_default_duration(self, duration: float): - """ - 设置默认过渡持续时间 - - Args: - duration: 持续时间(秒) - """ - self.default_duration = max(0.1, duration) - print(f"✓ 默认过渡持续时间设置为: {self.default_duration:.2f}秒") - - def set_default_curve(self, curve: str): - """ - 设置默认过渡曲线 - - Args: - curve: 曲线类型 (linear, exponential, logarithmic, ease_in, ease_out, ease_in_out) - """ - valid_curves = ['linear', 'exponential', 'logarithmic', 'ease_in', 'ease_out', 'ease_in_out'] - if curve not in valid_curves: - print(f"✗ 无效的曲线类型: {curve}") - return - - self.default_curve = curve - print(f"✓ 默认过渡曲线设置为: {self.default_curve}") - - def create_dynamic_transition(self, from_theme: str, to_theme: str, - transition_type: str = 'crossfade', duration: float = None) -> str: - """ - 创建动态过渡 - - Args: - from_theme: 来源主题 - to_theme: 目标主题 - transition_type: 过渡类型 - duration: 持续时间(秒) - - Returns: - 过渡名称 - """ - try: - # 生成唯一过渡名称 - transition_name = f"dynamic_{from_theme}_to_{to_theme}_{int(time.time())}" - - # 创建过渡数据 - transition_data = { - 'from_theme': from_theme, - 'to_theme': to_theme, - 'type': transition_type, - 'duration': duration if duration is not None else self.default_duration - } - - # 注册过渡 - if self.register_transition(transition_name, transition_data): - return transition_name - else: - return "" - - except Exception as e: - print(f"✗ 创建动态过渡失败: {e}") - return "" - - def is_transition_active(self, transition_name: str) -> bool: - """ - 检查过渡是否正在进行 - - Args: - transition_name: 过渡名称 - - Returns: - 是否正在进行 - """ - return transition_name in self.active_transitions - - def get_transition_progress(self, transition_name: str) -> float: - """ - 获取过渡进度 - - Args: - transition_name: 过渡名称 - - Returns: - 过渡进度 (0.0-1.0) - """ - if transition_name in self.active_transitions: - transition_instance = self.active_transitions[transition_name] - elapsed_time = time.time() - transition_instance['start_time'] - duration = transition_instance['duration'] - return min(1.0, elapsed_time / duration) - return 0.0 - - def pause_transition(self, transition_name: str) -> bool: - """ - 暂停过渡 - - Args: - transition_name: 过渡名称 - - Returns: - 是否暂停成功 - """ - try: - if transition_name not in self.active_transitions: - print(f"⚠ 过渡未在进行: {transition_name}") - return False - - transition_instance = self.active_transitions[transition_name] - transition_instance['state'] = 'paused' - transition_instance['pause_time'] = time.time() - - print(f"✓ 过渡已暂停: {transition_name}") - return True - - except Exception as e: - print(f"✗ 暂停过渡失败: {e}") - return False - - def resume_transition(self, transition_name: str) -> bool: - """ - 恢复过渡 - - Args: - transition_name: 过渡名称 - - Returns: - 是否恢复成功 - """ - try: - if transition_name not in self.active_transitions: - print(f"⚠ 过渡未在进行: {transition_name}") - return False - - transition_instance = self.active_transitions[transition_name] - if transition_instance['state'] == 'paused': - # 计算暂停时间并调整开始时间 - pause_duration = time.time() - transition_instance.get('pause_time', time.time()) - transition_instance['start_time'] += pause_duration - transition_instance['state'] = 'active' - - print(f"✓ 过渡已恢复: {transition_name}") - return True - else: - print(f"⚠ 过渡未暂停: {transition_name}") - return False - - except Exception as e: - print(f"✗ 恢复过渡失败: {e}") +""" +过渡管理器 +负责音乐主题之间的平滑过渡 +""" + +import uuid +import math +import time +import numpy as np +from typing import Dict, List, Any, Optional + +class TransitionManager: + """ + 过渡管理器 + 负责音乐主题之间的平滑过渡 + """ + + def __init__(self, plugin): + """ + 初始化过渡管理器 + + Args: + plugin: 动态音乐系统插件实例 + """ + self.plugin = plugin + self.transitions: Dict[str, Dict[str, Any]] = {} + self.active_transitions: Dict[str, Dict[str, Any]] = {} + self.pending_transitions: List[Dict[str, Any]] = [] + + self.stats = { + 'transitions_registered': 0, + 'transitions_active': 0, + 'transitions_completed': 0, + 'transitions_failed': 0, + 'processing_time': 0.0 + } + + self.default_duration = 2.0 + self.default_curve = 'linear' + + # 初始化过渡管理器 + self._initialize_transition_manager() + + def _initialize_transition_manager(self): + """初始化过渡管理器""" + print("✓ 过渡管理器初始化完成") + + def register_transition(self, transition_name: str, transition_data: Dict[str, Any]) -> bool: + """ + 注册音乐过渡 + + Args: + transition_name: 过渡名称 + transition_data: 过渡数据 + + Returns: + 是否注册成功 + """ + try: + # 验证必需字段 + required_fields = ['from_theme', 'to_theme'] + for field in required_fields: + if field not in transition_data: + print(f"✗ 过渡数据缺少必需字段: {field}") + return False + + # 设置默认值 + transition_data.setdefault('duration', self.default_duration) + transition_data.setdefault('type', 'crossfade') + transition_data.setdefault('curve', self.default_curve) + transition_data.setdefault('interruptible', True) + transition_data.setdefault('priority', 1) + + # 存储过渡数据 + self.transitions[transition_name] = transition_data.copy() + self.stats['transitions_registered'] += 1 + + print(f"✓ 音乐过渡注册成功: {transition_name}") + return True + + except Exception as e: + print(f"✗ 注册音乐过渡失败: {e}") + return False + + def trigger_transition(self, transition_name: str) -> bool: + """ + 触发音乐过渡 + + Args: + transition_name: 过渡名称 + + Returns: + 是否触发成功 + """ + try: + if transition_name not in self.transitions: + print(f"✗ 音乐过渡不存在: {transition_name}") + return False + + transition_data = self.transitions[transition_name] + + # 检查是否可以触发过渡 + if not self._can_trigger_transition(transition_name, transition_data): + print(f"⚠ 音乐过渡无法触发: {transition_name}") + self.stats['transitions_failed'] += 1 + return False + + # 创建过渡实例 + transition_instance = { + 'name': transition_name, + 'from_theme': transition_data['from_theme'], + 'to_theme': transition_data['to_theme'], + 'type': transition_data['type'], + 'duration': transition_data['duration'], + 'curve': transition_data['curve'], + 'start_time': time.time(), + 'elapsed_time': 0.0, + 'state': 'starting' + } + + # 根据过渡类型处理 + if transition_data['type'] == 'crossfade': + success = self._start_crossfade(transition_instance) + elif transition_data['type'] == 'cut': + success = self._start_cut(transition_instance) + elif transition_data['type'] == 'stinger': + success = self._start_stinger(transition_instance) + else: + print(f"✗ 不支持的过渡类型: {transition_data['type']}") + self.stats['transitions_failed'] += 1 + return False + + if success: + # 存储活动过渡 + self.active_transitions[transition_name] = transition_instance + self.stats['transitions_active'] = len(self.active_transitions) + print(f"✓ 音乐过渡已触发: {transition_name}") + return True + else: + self.stats['transitions_failed'] += 1 + return False + + except Exception as e: + print(f"✗ 触发音乐过渡失败: {e}") + self.stats['transitions_failed'] += 1 + return False + + def _can_trigger_transition(self, transition_name: str, transition_data: Dict[str, Any]) -> bool: + """ + 检查是否可以触发过渡 + + Args: + transition_name: 过渡名称 + transition_data: 过渡数据 + + Returns: + 是否可以触发 + """ + # 检查来源主题是否正在播放 + from_theme = transition_data['from_theme'] + if from_theme != 'any' and self.plugin.music_manager: + if not self.plugin.music_manager.is_theme_playing(from_theme): + print(f"⚠ 来源主题未在播放: {from_theme}") + return False + + # 检查是否有冲突的活动过渡 + for active_name, active_transition in self.active_transitions.items(): + # 检查是否涉及相同的主题 + if (active_transition['from_theme'] == from_theme or + active_transition['to_theme'] == from_theme or + active_transition['from_theme'] == transition_data['to_theme'] or + active_transition['to_theme'] == transition_data['to_theme']): + + # 检查优先级 + active_priority = self.transitions[active_name].get('priority', 1) + new_priority = transition_data.get('priority', 1) + + if new_priority < active_priority: + print(f"⚠ 更高优先级的过渡正在进行: {active_name}") + return False + elif not self.transitions[active_name].get('interruptible', True): + print(f"⚠ 当前过渡不可中断: {active_name}") + return False + + return True + + def _start_crossfade(self, transition_instance: Dict[str, Any]) -> bool: + """ + 开始交叉淡入淡出过渡 + + Args: + transition_instance: 过渡实例 + + Returns: + 是否开始成功 + """ + try: + from_theme = transition_instance['from_theme'] + to_theme = transition_instance['to_theme'] + duration = transition_instance['duration'] + + # 淡出来源主题 + if from_theme != 'any' and self.plugin.music_manager: + self.plugin.music_manager.fade_out_theme(from_theme, duration) + + # 淡入目标主题 + if self.plugin.music_manager: + self.plugin.music_manager.fade_in_theme(to_theme, duration) + + transition_instance['state'] = 'active' + return True + + except Exception as e: + print(f"✗ 开始交叉淡入淡出过渡失败: {e}") + return False + + def _start_cut(self, transition_instance: Dict[str, Any]) -> bool: + """ + 开始直接切换过渡 + + Args: + transition_instance: 过渡实例 + + Returns: + 是否开始成功 + """ + try: + from_theme = transition_instance['from_theme'] + to_theme = transition_instance['to_theme'] + + # 停止来源主题 + if from_theme != 'any' and self.plugin.music_manager: + self.plugin.music_manager.stop_theme(from_theme) + + # 播放目标主题 + if self.plugin.music_manager: + self.plugin.music_manager.play_theme(to_theme, force=True) + + transition_instance['state'] = 'completed' + self.stats['transitions_completed'] += 1 + return True + + except Exception as e: + print(f"✗ 开始直接切换过渡失败: {e}") + return False + + def _start_stinger(self, transition_instance: Dict[str, Any]) -> bool: + """ + 开始使用刺音过渡 + + Args: + transition_instance: 过渡实例 + + Returns: + 是否开始成功 + """ + try: + from_theme = transition_instance['from_theme'] + to_theme = transition_instance['to_theme'] + duration = transition_instance['duration'] + + # 淡出来源主题 + if from_theme != 'any' and self.plugin.music_manager: + self.plugin.music_manager.fade_out_theme(from_theme, duration * 0.5) + + # 播放刺音效果(这里简化处理) + # 在实际实现中,会播放一个短的音频文件 + print("✓ 播放刺音效果") + + # 淡入目标主题 + if self.plugin.music_manager: + # 延迟淡入目标主题 + def delayed_fade_in(): + time.sleep(duration * 0.5) + self.plugin.music_manager.fade_in_theme(to_theme, duration * 0.5) + + import threading + threading.Thread(target=delayed_fade_in, daemon=True).start() + + transition_instance['state'] = 'active' + return True + + except Exception as e: + print(f"✗ 开始刺音过渡失败: {e}") + return False + + def cancel_transition(self, transition_name: str) -> bool: + """ + 取消音乐过渡 + + Args: + transition_name: 过渡名称 + + Returns: + 是否取消成功 + """ + try: + if transition_name not in self.active_transitions: + print(f"⚠ 过渡未在进行: {transition_name}") + return True + + # 取消过渡 + del self.active_transitions[transition_name] + self.stats['transitions_active'] = len(self.active_transitions) + + print(f"✓ 音乐过渡已取消: {transition_name}") + return True + + except Exception as e: + print(f"✗ 取消音乐过渡失败: {e}") + return False + + def update(self, dt: float): + """ + 更新过渡管理器状态 + + Args: + dt: 时间增量 + """ + update_start_time = time.time() + + # 更新活动过渡 + current_time = time.time() + completed_transitions = [] + + for transition_name, transition_instance in self.active_transitions.items(): + if transition_instance['state'] == 'active': + elapsed_time = current_time - transition_instance['start_time'] + duration = transition_instance['duration'] + + if elapsed_time >= duration: + # 过渡完成 + transition_instance['state'] = 'completed' + completed_transitions.append(transition_name) + self.stats['transitions_completed'] += 1 + + # 移除已完成的过渡 + for transition_name in completed_transitions: + if transition_name in self.active_transitions: + del self.active_transitions[transition_name] + + self.stats['transitions_active'] = len(self.active_transitions) + + # 更新性能统计 + self.stats['processing_time'] += (time.time() - update_start_time) + + def cleanup(self): + """清理所有资源""" + self.transitions.clear() + self.active_transitions.clear() + self.pending_transitions.clear() + + self.stats = { + 'transitions_registered': 0, + 'transitions_active': 0, + 'transitions_completed': 0, + 'transitions_failed': 0, + 'processing_time': 0.0 + } + + print("✓ 过渡管理器资源已清理") + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def get_active_transitions(self) -> List[str]: + """ + 获取活动过渡列表 + + Returns: + 活动过渡名称列表 + """ + return list(self.active_transitions.keys()) + + def get_transition_status(self, transition_name: str) -> Dict[str, Any]: + """ + 获取过渡状态 + + Args: + transition_name: 过渡名称 + + Returns: + 过渡状态字典 + """ + if transition_name in self.active_transitions: + transition_instance = self.active_transitions[transition_name] + return { + 'name': transition_name, + 'state': transition_instance['state'], + 'from_theme': transition_instance['from_theme'], + 'to_theme': transition_instance['to_theme'], + 'elapsed_time': transition_instance['elapsed_time'], + 'duration': transition_instance['duration'] + } + elif transition_name in self.transitions: + return { + 'name': transition_name, + 'state': 'registered', + 'from_theme': self.transitions[transition_name]['from_theme'], + 'to_theme': self.transitions[transition_name]['to_theme'] + } + else: + return { + 'name': transition_name, + 'state': 'unknown' + } + + def set_default_duration(self, duration: float): + """ + 设置默认过渡持续时间 + + Args: + duration: 持续时间(秒) + """ + self.default_duration = max(0.1, duration) + print(f"✓ 默认过渡持续时间设置为: {self.default_duration:.2f}秒") + + def set_default_curve(self, curve: str): + """ + 设置默认过渡曲线 + + Args: + curve: 曲线类型 (linear, exponential, logarithmic, ease_in, ease_out, ease_in_out) + """ + valid_curves = ['linear', 'exponential', 'logarithmic', 'ease_in', 'ease_out', 'ease_in_out'] + if curve not in valid_curves: + print(f"✗ 无效的曲线类型: {curve}") + return + + self.default_curve = curve + print(f"✓ 默认过渡曲线设置为: {self.default_curve}") + + def create_dynamic_transition(self, from_theme: str, to_theme: str, + transition_type: str = 'crossfade', duration: float = None) -> str: + """ + 创建动态过渡 + + Args: + from_theme: 来源主题 + to_theme: 目标主题 + transition_type: 过渡类型 + duration: 持续时间(秒) + + Returns: + 过渡名称 + """ + try: + # 生成唯一过渡名称 + transition_name = f"dynamic_{from_theme}_to_{to_theme}_{int(time.time())}" + + # 创建过渡数据 + transition_data = { + 'from_theme': from_theme, + 'to_theme': to_theme, + 'type': transition_type, + 'duration': duration if duration is not None else self.default_duration + } + + # 注册过渡 + if self.register_transition(transition_name, transition_data): + return transition_name + else: + return "" + + except Exception as e: + print(f"✗ 创建动态过渡失败: {e}") + return "" + + def is_transition_active(self, transition_name: str) -> bool: + """ + 检查过渡是否正在进行 + + Args: + transition_name: 过渡名称 + + Returns: + 是否正在进行 + """ + return transition_name in self.active_transitions + + def get_transition_progress(self, transition_name: str) -> float: + """ + 获取过渡进度 + + Args: + transition_name: 过渡名称 + + Returns: + 过渡进度 (0.0-1.0) + """ + if transition_name in self.active_transitions: + transition_instance = self.active_transitions[transition_name] + elapsed_time = time.time() - transition_instance['start_time'] + duration = transition_instance['duration'] + return min(1.0, elapsed_time / duration) + return 0.0 + + def pause_transition(self, transition_name: str) -> bool: + """ + 暂停过渡 + + Args: + transition_name: 过渡名称 + + Returns: + 是否暂停成功 + """ + try: + if transition_name not in self.active_transitions: + print(f"⚠ 过渡未在进行: {transition_name}") + return False + + transition_instance = self.active_transitions[transition_name] + transition_instance['state'] = 'paused' + transition_instance['pause_time'] = time.time() + + print(f"✓ 过渡已暂停: {transition_name}") + return True + + except Exception as e: + print(f"✗ 暂停过渡失败: {e}") + return False + + def resume_transition(self, transition_name: str) -> bool: + """ + 恢复过渡 + + Args: + transition_name: 过渡名称 + + Returns: + 是否恢复成功 + """ + try: + if transition_name not in self.active_transitions: + print(f"⚠ 过渡未在进行: {transition_name}") + return False + + transition_instance = self.active_transitions[transition_name] + if transition_instance['state'] == 'paused': + # 计算暂停时间并调整开始时间 + pause_duration = time.time() - transition_instance.get('pause_time', time.time()) + transition_instance['start_time'] += pause_duration + transition_instance['state'] = 'active' + + print(f"✓ 过渡已恢复: {transition_name}") + return True + else: + print(f"⚠ 过渡未暂停: {transition_name}") + return False + + except Exception as e: + print(f"✗ 恢复过渡失败: {e}") return False \ No newline at end of file diff --git a/plugins/user/example_plugin/__init__.py b/plugins/user/example_plugin/__init__.py index 2b3755ca..c4531d36 100644 --- a/plugins/user/example_plugin/__init__.py +++ b/plugins/user/example_plugin/__init__.py @@ -1,3 +1,3 @@ -""" -示例插件初始化文件 +""" +示例插件初始化文件 """ \ No newline at end of file diff --git a/plugins/user/example_plugin/plugin.py b/plugins/user/example_plugin/plugin.py index 3c4a0d16..dd1a91f0 100644 --- a/plugins/user/example_plugin/plugin.py +++ b/plugins/user/example_plugin/plugin.py @@ -1,96 +1,96 @@ -""" -示例插件 -演示如何创建一个简单的插件 -""" - -from plugins.plugin_manager import BasePlugin - -class Plugin(BasePlugin): - """ - 示例插件 - 演示如何创建一个简单的插件 - """ - - def __init__(self, plugin_manager, name): - super().__init__(plugin_manager, name) - self.config = { - "version": "1.0.0", - "author": "EG Team", - "description": "示例插件,演示插件基本功能" - } - - def initialize(self) -> bool: - """ - 初始化插件 - """ - print(f"初始化示例插件: {self.name}") - return True - - def enable(self) -> bool: - """ - 启用插件 - """ - if not super().enable(): - return False - - print(f"启用示例插件: {self.name}") - - # 注册一个示例事件处理器 - self.plugin_manager.world.accept("f10", self.on_f10_pressed) - - # 在场景中添加一个示例文本 - self.example_text = self.plugin_manager.world.render.attachNewNode("example_text") - self.example_text.setPos(0, 0, 5) - - # 如果有GUI管理器,添加一个示例按钮 - if hasattr(self.plugin_manager.world, 'gui_manager') and self.plugin_manager.world.gui_manager: - self.example_button = self.plugin_manager.world.gui_manager.createGUIButton( - pos=(0.5, 0, 0.9), - text="示例插件按钮", - size=0.08 - ) - # 注册按钮点击事件 - self.plugin_manager.world.accept("example_button_click", self.on_button_click) - - return True - - def disable(self) -> bool: - """ - 禁用插件 - """ - if not super().disable(): - return False - - print(f"禁用示例插件: {self.name}") - - # 移除事件处理器 - self.plugin_manager.world.ignore("f10") - - # 清理示例文本 - if hasattr(self, 'example_text'): - self.example_text.removeNode() - - # 清理示例按钮 - if hasattr(self, 'example_button'): - self.plugin_manager.world.gui_manager.deleteGUIElement(self.example_button) - self.plugin_manager.world.ignore("example_button_click") - - return True - - def finalize(self): - """ - 清理插件资源 - """ - print(f"清理示例插件资源: {self.name}") - - def on_f10_pressed(self): - """ - F10按键事件处理器 - """ - print("示例插件: F10按键被按下") - - def on_button_click(self): - """ - 按钮点击事件处理器 - """ +""" +示例插件 +演示如何创建一个简单的插件 +""" + +from plugins.plugin_manager import BasePlugin + +class Plugin(BasePlugin): + """ + 示例插件 + 演示如何创建一个简单的插件 + """ + + def __init__(self, plugin_manager, name): + super().__init__(plugin_manager, name) + self.config = { + "version": "1.0.0", + "author": "EG Team", + "description": "示例插件,演示插件基本功能" + } + + def initialize(self) -> bool: + """ + 初始化插件 + """ + print(f"初始化示例插件: {self.name}") + return True + + def enable(self) -> bool: + """ + 启用插件 + """ + if not super().enable(): + return False + + print(f"启用示例插件: {self.name}") + + # 注册一个示例事件处理器 + self.plugin_manager.world.accept("f10", self.on_f10_pressed) + + # 在场景中添加一个示例文本 + self.example_text = self.plugin_manager.world.render.attachNewNode("example_text") + self.example_text.setPos(0, 0, 5) + + # 如果有GUI管理器,添加一个示例按钮 + if hasattr(self.plugin_manager.world, 'gui_manager') and self.plugin_manager.world.gui_manager: + self.example_button = self.plugin_manager.world.gui_manager.createGUIButton( + pos=(0.5, 0, 0.9), + text="示例插件按钮", + size=0.08 + ) + # 注册按钮点击事件 + self.plugin_manager.world.accept("example_button_click", self.on_button_click) + + return True + + def disable(self) -> bool: + """ + 禁用插件 + """ + if not super().disable(): + return False + + print(f"禁用示例插件: {self.name}") + + # 移除事件处理器 + self.plugin_manager.world.ignore("f10") + + # 清理示例文本 + if hasattr(self, 'example_text'): + self.example_text.removeNode() + + # 清理示例按钮 + if hasattr(self, 'example_button'): + self.plugin_manager.world.gui_manager.deleteGUIElement(self.example_button) + self.plugin_manager.world.ignore("example_button_click") + + return True + + def finalize(self): + """ + 清理插件资源 + """ + print(f"清理示例插件资源: {self.name}") + + def on_f10_pressed(self): + """ + F10按键事件处理器 + """ + print("示例插件: F10按键被按下") + + def on_button_click(self): + """ + 按钮点击事件处理器 + """ print("示例插件: 按钮被点击") \ No newline at end of file diff --git a/plugins/user/example_usage.py b/plugins/user/example_usage.py index c1f9970a..3983ac65 100644 --- a/plugins/user/example_usage.py +++ b/plugins/user/example_usage.py @@ -1,227 +1,227 @@ -""" -行为树和状态机插件使用示例 -演示如何在项目中使用行为树和状态机插件 -""" - -import sys -import os - -# 添加项目根目录到Python路径 -sys.path.insert(0, '/home/hello/EG') - -def main(): - """ - 主函数,演示插件的使用方法 - """ - print("行为树和状态机插件使用示例") - print("=" * 40) - - # 导入插件管理器 - from plugins.plugin_manager import PluginManager - - # 创建一个虚拟的世界对象(模拟项目中的世界对象) - class MockWorld: - def __init__(self): - # 模拟世界对象的属性和方法 - self.render = None - self.accept = lambda event, handler: None - self.ignore = lambda event: None - - # 创建世界实例 - world = MockWorld() - - # 创建插件管理器 - plugin_manager = PluginManager(world) - - # 启用插件系统 - plugin_manager.enable_plugin_system() - - # 加载行为树插件 - print("\n1. 加载行为树插件...") - bt_plugin = plugin_manager.load_plugin("behavior_tree") - if bt_plugin: - print("✅ 行为树插件加载成功") - # 启用行为树插件 - if plugin_manager.enable_plugin("behavior_tree"): - print("✅ 行为树插件启用成功") - else: - print("❌ 行为树插件启用失败") - else: - print("❌ 行为树插件加载失败") - return - - # 加载状态机插件 - print("\n2. 加载状态机插件...") - sm_plugin = plugin_manager.load_plugin("state_machine") - if sm_plugin: - print("✅ 状态机插件加载成功") - # 启用状态机插件 - if plugin_manager.enable_plugin("state_machine"): - print("✅ 状态机插件启用成功") - else: - print("❌ 状态机插件启用失败") - else: - print("❌ 状态机插件加载失败") - return - - # 使用行为树插件创建一个简单的AI行为 - print("\n3. 使用行为树插件创建AI行为...") - try: - # 导入行为树节点类 - from plugins.user.behavior_tree import ( - SelectorNode, SequenceNode, - ConditionNode, ActionNode, - RepeaterNode - ) - - # 创建行为树结构 - # 根节点:选择节点 - root = SelectorNode("Root") - - # 创建巡逻行为序列 - patrol_sequence = SequenceNode("Patrol") - patrol_condition = ConditionNode("PatrolCondition", - lambda bb: bb.get("should_patrol", True)) - patrol_action = ActionNode("PatrolAction", - lambda bb: print("🤖 AI正在巡逻...") or True) - - patrol_sequence.add_child(patrol_condition) - patrol_sequence.add_child(patrol_action) - - # 创建重复节点,重复巡逻3次 - patrol_repeater = RepeaterNode(patrol_sequence, count=3) - - # 创建休息行为序列 - rest_sequence = SequenceNode("Rest") - rest_condition = ConditionNode("RestCondition", - lambda bb: bb.get("should_rest", False)) - rest_action = ActionNode("RestAction", - lambda bb: print("😴 AI正在休息...") or True) - - rest_sequence.add_child(rest_condition) - rest_sequence.add_child(rest_action) - - # 组装行为树 - root.add_child(patrol_repeater) - root.add_child(rest_sequence) - - # 创建行为树实例 - behavior_tree = bt_plugin.create_behavior_tree("ai_behavior", root) - - # 创建黑板并设置初始数据 - blackboard = bt_plugin.create_blackboard("ai_blackboard") - blackboard.set("should_patrol", True) - blackboard.set("should_rest", False) - - # 关联黑板到行为树 - behavior_tree.set_blackboard(blackboard) - - print("✅ 行为树创建成功") - - # 执行行为树 - print("\n4. 执行行为树...") - for i in range(5): - print(f"\n--- 第 {i+1} 次执行 ---") - result = behavior_tree.run() - print(f"执行结果: {result.value}") - - # 每隔几次切换状态 - if i == 2: - blackboard.set("should_patrol", False) - blackboard.set("should_rest", True) - print("🔄 切换到休息状态") - - except Exception as e: - print(f"❌ 行为树使用示例失败: {e}") - import traceback - traceback.print_exc() - - # 使用状态机插件创建一个简单的状态管理 - print("\n5. 使用状态机插件创建状态管理...") - try: - # 导入状态机类 - from plugins.user.state_machine import State - - # 定义状态类 - class IdleState(State): - def enter(self, context): - print("🎮 进入空闲状态") - - def execute(self, context): - print("⏸️ 空闲中...") - # 检查是否需要切换到移动状态 - if context.get("want_move", False): - return context["states"]["move"] - return None - - def exit(self, context): - print("🔚 退出空闲状态") - - class MoveState(State): - def enter(self, context): - print("🚗 进入移动状态") - - def execute(self, context): - print("➡️ 移动中...") - # 检查是否需要切换到空闲状态 - if not context.get("want_move", True): - return context["states"]["idle"] - return None - - def exit(self, context): - print("🔚 退出移动状态") - - # 创建状态机 - state_machine = sm_plugin.create_state_machine("game_character") - - # 创建状态实例 - states = { - "idle": IdleState("Idle"), - "move": MoveState("Move") - } - - # 创建上下文 - context = { - "want_move": False, - "states": states - } - - # 设置状态机上下文 - state_machine.set_context(context) - - # 设置初始状态 - state_machine.change_state(states["idle"]) - - print("✅ 状态机创建成功") - - # 更新状态机 - print("\n6. 更新状态机...") - for i in range(4): - print(f"\n--- 第 {i+1} 次更新 ---") - print(f"当前状态: {state_machine.get_current_state().name}") - state_machine.update() - - # 模拟状态切换 - if i == 1: - context["want_move"] = True - print("🔄 设置为想要移动") - elif i == 2: - context["want_move"] = False - print("🔄 设置为想要空闲") - - except Exception as e: - print(f"❌ 状态机使用示例失败: {e}") - import traceback - traceback.print_exc() - - # 禁用和卸载插件 - print("\n7. 清理资源...") - plugin_manager.disable_plugin("behavior_tree") - plugin_manager.disable_plugin("state_machine") - plugin_manager.unload_plugin("behavior_tree") - plugin_manager.unload_plugin("state_machine") - - print("\n🎉 示例运行完成!") - -if __name__ == "__main__": +""" +行为树和状态机插件使用示例 +演示如何在项目中使用行为树和状态机插件 +""" + +import sys +import os + +# 添加项目根目录到Python路径 +sys.path.insert(0, '/home/hello/EG') + +def main(): + """ + 主函数,演示插件的使用方法 + """ + print("行为树和状态机插件使用示例") + print("=" * 40) + + # 导入插件管理器 + from plugins.plugin_manager import PluginManager + + # 创建一个虚拟的世界对象(模拟项目中的世界对象) + class MockWorld: + def __init__(self): + # 模拟世界对象的属性和方法 + self.render = None + self.accept = lambda event, handler: None + self.ignore = lambda event: None + + # 创建世界实例 + world = MockWorld() + + # 创建插件管理器 + plugin_manager = PluginManager(world) + + # 启用插件系统 + plugin_manager.enable_plugin_system() + + # 加载行为树插件 + print("\n1. 加载行为树插件...") + bt_plugin = plugin_manager.load_plugin("behavior_tree") + if bt_plugin: + print("✅ 行为树插件加载成功") + # 启用行为树插件 + if plugin_manager.enable_plugin("behavior_tree"): + print("✅ 行为树插件启用成功") + else: + print("❌ 行为树插件启用失败") + else: + print("❌ 行为树插件加载失败") + return + + # 加载状态机插件 + print("\n2. 加载状态机插件...") + sm_plugin = plugin_manager.load_plugin("state_machine") + if sm_plugin: + print("✅ 状态机插件加载成功") + # 启用状态机插件 + if plugin_manager.enable_plugin("state_machine"): + print("✅ 状态机插件启用成功") + else: + print("❌ 状态机插件启用失败") + else: + print("❌ 状态机插件加载失败") + return + + # 使用行为树插件创建一个简单的AI行为 + print("\n3. 使用行为树插件创建AI行为...") + try: + # 导入行为树节点类 + from plugins.user.behavior_tree import ( + SelectorNode, SequenceNode, + ConditionNode, ActionNode, + RepeaterNode + ) + + # 创建行为树结构 + # 根节点:选择节点 + root = SelectorNode("Root") + + # 创建巡逻行为序列 + patrol_sequence = SequenceNode("Patrol") + patrol_condition = ConditionNode("PatrolCondition", + lambda bb: bb.get("should_patrol", True)) + patrol_action = ActionNode("PatrolAction", + lambda bb: print("🤖 AI正在巡逻...") or True) + + patrol_sequence.add_child(patrol_condition) + patrol_sequence.add_child(patrol_action) + + # 创建重复节点,重复巡逻3次 + patrol_repeater = RepeaterNode(patrol_sequence, count=3) + + # 创建休息行为序列 + rest_sequence = SequenceNode("Rest") + rest_condition = ConditionNode("RestCondition", + lambda bb: bb.get("should_rest", False)) + rest_action = ActionNode("RestAction", + lambda bb: print("😴 AI正在休息...") or True) + + rest_sequence.add_child(rest_condition) + rest_sequence.add_child(rest_action) + + # 组装行为树 + root.add_child(patrol_repeater) + root.add_child(rest_sequence) + + # 创建行为树实例 + behavior_tree = bt_plugin.create_behavior_tree("ai_behavior", root) + + # 创建黑板并设置初始数据 + blackboard = bt_plugin.create_blackboard("ai_blackboard") + blackboard.set("should_patrol", True) + blackboard.set("should_rest", False) + + # 关联黑板到行为树 + behavior_tree.set_blackboard(blackboard) + + print("✅ 行为树创建成功") + + # 执行行为树 + print("\n4. 执行行为树...") + for i in range(5): + print(f"\n--- 第 {i+1} 次执行 ---") + result = behavior_tree.run() + print(f"执行结果: {result.value}") + + # 每隔几次切换状态 + if i == 2: + blackboard.set("should_patrol", False) + blackboard.set("should_rest", True) + print("🔄 切换到休息状态") + + except Exception as e: + print(f"❌ 行为树使用示例失败: {e}") + import traceback + traceback.print_exc() + + # 使用状态机插件创建一个简单的状态管理 + print("\n5. 使用状态机插件创建状态管理...") + try: + # 导入状态机类 + from plugins.user.state_machine import State + + # 定义状态类 + class IdleState(State): + def enter(self, context): + print("🎮 进入空闲状态") + + def execute(self, context): + print("⏸️ 空闲中...") + # 检查是否需要切换到移动状态 + if context.get("want_move", False): + return context["states"]["move"] + return None + + def exit(self, context): + print("🔚 退出空闲状态") + + class MoveState(State): + def enter(self, context): + print("🚗 进入移动状态") + + def execute(self, context): + print("➡️ 移动中...") + # 检查是否需要切换到空闲状态 + if not context.get("want_move", True): + return context["states"]["idle"] + return None + + def exit(self, context): + print("🔚 退出移动状态") + + # 创建状态机 + state_machine = sm_plugin.create_state_machine("game_character") + + # 创建状态实例 + states = { + "idle": IdleState("Idle"), + "move": MoveState("Move") + } + + # 创建上下文 + context = { + "want_move": False, + "states": states + } + + # 设置状态机上下文 + state_machine.set_context(context) + + # 设置初始状态 + state_machine.change_state(states["idle"]) + + print("✅ 状态机创建成功") + + # 更新状态机 + print("\n6. 更新状态机...") + for i in range(4): + print(f"\n--- 第 {i+1} 次更新 ---") + print(f"当前状态: {state_machine.get_current_state().name}") + state_machine.update() + + # 模拟状态切换 + if i == 1: + context["want_move"] = True + print("🔄 设置为想要移动") + elif i == 2: + context["want_move"] = False + print("🔄 设置为想要空闲") + + except Exception as e: + print(f"❌ 状态机使用示例失败: {e}") + import traceback + traceback.print_exc() + + # 禁用和卸载插件 + print("\n7. 清理资源...") + plugin_manager.disable_plugin("behavior_tree") + plugin_manager.disable_plugin("state_machine") + plugin_manager.unload_plugin("behavior_tree") + plugin_manager.unload_plugin("state_machine") + + print("\n🎉 示例运行完成!") + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/plugins/user/fluid_dynamics/README.md b/plugins/user/fluid_dynamics/README.md index 2f1c2c14..f61f50f0 100644 --- a/plugins/user/fluid_dynamics/README.md +++ b/plugins/user/fluid_dynamics/README.md @@ -1,241 +1,241 @@ -# 流体动力学插件使用指南 - -## 概述 - -流体动力学插件提供基于物理的流体模拟功能,支持水、油、熔岩等多种流体类型。插件采用SPH(光滑粒子流体动力学)方法实现,能够模拟流体的基本物理行为,包括流动、碰撞、压力和粘性效应。 - -## 安装和启用 - -### 1. 插件结构 - -``` -fluid_dynamics/ -├── __init__.py -├── plugin.py # 插件主入口 -├── core/ -│ ├── fluid_world.py # 流体世界管理 -│ ├── fluid_solver.py # 流体求解器 -│ ├── fluid_material.py # 流体材质系统 -│ └── fluid_visualizer.py # 流体可视化 -├── components/ -│ ├── fluid_emitter.py # 流体发射器 -│ ├── fluid_obstacle.py # 流体障碍物 -│ └── fluid_domain.py # 流体域 -├── utils/ -│ ├── math_utils.py # 数学工具 -│ └── performance_utils.py # 性能工具 -└── config/ - └── default_config.json # 默认配置 -``` - -### 2. 启用插件 - -在主程序中启用插件: - -```python -# 加载插件 -fluid_plugin = world.plugin_manager.load_plugin("fluid_dynamics") - -# 启用插件 -world.plugin_manager.enable_plugin("fluid_dynamics") -``` - -## 核心功能 - -### 1. 流体域 (FluidDomain) - -定义流体模拟的边界和空间: - -```python -from plugins.user.fluid_dynamics.components.fluid_domain import FluidDomain - -# 创建流体域 -domain = FluidDomain( - min_bound=Point3(-10, -10, 0), - max_bound=Point3(10, 10, 20) -) -fluid_world.add_fluid_domain(domain) -``` - -### 2. 流体发射器 (FluidEmitter) - -生成流体粒子: - -```python -from plugins.user.fluid_dynamics.components.fluid_emitter import FluidEmitter - -# 创建发射器 -emitter = FluidEmitter( - position=Point3(0, 0, 10), - fluid_world=fluid_world -) -emitter.set_emit_rate(50) # 每秒发射50个粒子 -emitter.set_emit_velocity(Vec3(0, 0, 2)) # 向上发射 -fluid_world.add_fluid_emitter(emitter) -``` - -### 3. 流体障碍物 (FluidObstacle) - -定义流体模拟中的障碍物: - -```python -from plugins.user.fluid_dynamics.components.fluid_obstacle import FluidObstacle - -# 创建障碍物 -obstacle = FluidObstacle( - position=Point3(0, 0, 5), - size=Vec3(2, 2, 2), - obstacle_type="box" -) -fluid_world.add_fluid_obstacle(obstacle) -``` - -### 4. 流体材质 (FluidMaterial) - -定义流体的物理和视觉属性: - -```python -from plugins.user.fluid_dynamics.core.fluid_material import FluidMaterial - -# 创建水材质 -water_material = FluidMaterial( - name="water", - fluid_type=FluidMaterial.TYPE_WATER -) - -# 创建熔岩材质 -lava_material = FluidMaterial( - name="lava", - fluid_type=FluidMaterial.TYPE_LAVA -) -``` - -## 高级功能 - -### 1. 性能优化 - -插件提供自适应性能优化: - -```python -from plugins.user.fluid_dynamics.utils.performance_utils import FluidPerformanceOptimizer - -# 创建性能优化器 -optimizer = FluidPerformanceOptimizer(fluid_world) - -# 启用性能优化 -optimizer.optimize_for_performance() - -# 获取性能报告 -report = optimizer.get_performance_report() -print(f"当前FPS: {report['current_fps']}") -``` - -### 2. 可视化控制 - -调整流体的视觉效果: - -```python -# 设置流体颜色 -visualizer.set_fluid_color(Vec4(0.0, 0.8, 1.0, 0.7)) - -# 创建粒子系统 -particle_system = visualizer.create_particle_system("fluid_particles", 2000) -``` - -## API参考 - -### FluidWorld类 - -- `add_fluid_domain(domain)`: 添加流体域 -- `remove_fluid_domain(domain)`: 移除流体域 -- `add_fluid_emitter(emitter)`: 添加发射器 -- `remove_fluid_emitter(emitter)`: 移除发射器 -- `add_fluid_obstacle(obstacle)`: 添加障碍物 -- `remove_fluid_obstacle(obstacle)`: 移除障碍物 -- `update(dt)`: 更新流体世界 -- `pause(paused)`: 暂停/恢复模拟 -- `set_time_scale(scale)`: 设置时间缩放 - -### FluidSolver类 - -- `set_viscosity(viscosity)`: 设置粘度 -- `set_density(density)`: 设置密度 -- `set_gravity(gravity)`: 设置重力 -- `solve(dt)`: 执行流体求解 - -### FluidMaterial类 - -- `TYPE_WATER`: 水类型 -- `TYPE_OIL`: 油类型 -- `TYPE_LAVA`: 熔岩类型 -- `TYPE_SLIME`: 粘液类型 -- `TYPE_BLOOD`: 血液类型 -- `get_density()`: 获取密度 -- `get_viscosity()`: 获取粘度 -- `get_color()`: 获取颜色 - -## 示例场景 - -创建一个简单的流体演示: - -```python -def create_fluid_demo(world): - # 1. 获取插件实例 - fluid_plugin = world.plugin_manager.get_plugin("fluid_dynamics") - fluid_world = fluid_plugin.fluid_world - - # 2. 创建流体域 - domain = FluidDomain(Point3(-5, -5, 0), Point3(5, 5, 10)) - fluid_world.add_fluid_domain(domain) - - # 3. 创建发射器 - emitter = FluidEmitter(Point3(0, 0, 8), fluid_world) - emitter.set_emit_rate(30) - fluid_world.add_fluid_emitter(emitter) - - # 4. 创建障碍物 - obstacle = FluidObstacle(Point3(0, 0, 3), Vec3(1, 1, 3), "box") - fluid_world.add_fluid_obstacle(obstacle) - - # 5. 设置求解器 - solver = FluidSolver(fluid_world) - fluid_world.solver = solver -``` - -## 故障排除 - -### 常见问题 - -1. **性能问题**: - - 减少粒子数量 - - 降低发射器速率 - - 使用性能优化器 - -2. **稳定性问题**: - - 调整时间步长 - - 检查边界条件 - - 验证物理参数 - -3. **可视化问题**: - - 检查粒子系统配置 - - 确认渲染节点正确附着 - - 调整透明度和颜色设置 - -## 配置选项 - -编辑 `config/default_config.json` 文件可以调整默认参数: - -```json -{ - "simulation": { - "time_step": 0.016, - "gravity": [0, 0, -9.81], - "viscosity": 0.001, - "density": 1000.0 - }, - "performance": { - "max_particles": 5000, - "update_frequency": 60 - } -} +# 流体动力学插件使用指南 + +## 概述 + +流体动力学插件提供基于物理的流体模拟功能,支持水、油、熔岩等多种流体类型。插件采用SPH(光滑粒子流体动力学)方法实现,能够模拟流体的基本物理行为,包括流动、碰撞、压力和粘性效应。 + +## 安装和启用 + +### 1. 插件结构 + +``` +fluid_dynamics/ +├── __init__.py +├── plugin.py # 插件主入口 +├── core/ +│ ├── fluid_world.py # 流体世界管理 +│ ├── fluid_solver.py # 流体求解器 +│ ├── fluid_material.py # 流体材质系统 +│ └── fluid_visualizer.py # 流体可视化 +├── components/ +│ ├── fluid_emitter.py # 流体发射器 +│ ├── fluid_obstacle.py # 流体障碍物 +│ └── fluid_domain.py # 流体域 +├── utils/ +│ ├── math_utils.py # 数学工具 +│ └── performance_utils.py # 性能工具 +└── config/ + └── default_config.json # 默认配置 +``` + +### 2. 启用插件 + +在主程序中启用插件: + +```python +# 加载插件 +fluid_plugin = world.plugin_manager.load_plugin("fluid_dynamics") + +# 启用插件 +world.plugin_manager.enable_plugin("fluid_dynamics") +``` + +## 核心功能 + +### 1. 流体域 (FluidDomain) + +定义流体模拟的边界和空间: + +```python +from plugins.user.fluid_dynamics.components.fluid_domain import FluidDomain + +# 创建流体域 +domain = FluidDomain( + min_bound=Point3(-10, -10, 0), + max_bound=Point3(10, 10, 20) +) +fluid_world.add_fluid_domain(domain) +``` + +### 2. 流体发射器 (FluidEmitter) + +生成流体粒子: + +```python +from plugins.user.fluid_dynamics.components.fluid_emitter import FluidEmitter + +# 创建发射器 +emitter = FluidEmitter( + position=Point3(0, 0, 10), + fluid_world=fluid_world +) +emitter.set_emit_rate(50) # 每秒发射50个粒子 +emitter.set_emit_velocity(Vec3(0, 0, 2)) # 向上发射 +fluid_world.add_fluid_emitter(emitter) +``` + +### 3. 流体障碍物 (FluidObstacle) + +定义流体模拟中的障碍物: + +```python +from plugins.user.fluid_dynamics.components.fluid_obstacle import FluidObstacle + +# 创建障碍物 +obstacle = FluidObstacle( + position=Point3(0, 0, 5), + size=Vec3(2, 2, 2), + obstacle_type="box" +) +fluid_world.add_fluid_obstacle(obstacle) +``` + +### 4. 流体材质 (FluidMaterial) + +定义流体的物理和视觉属性: + +```python +from plugins.user.fluid_dynamics.core.fluid_material import FluidMaterial + +# 创建水材质 +water_material = FluidMaterial( + name="water", + fluid_type=FluidMaterial.TYPE_WATER +) + +# 创建熔岩材质 +lava_material = FluidMaterial( + name="lava", + fluid_type=FluidMaterial.TYPE_LAVA +) +``` + +## 高级功能 + +### 1. 性能优化 + +插件提供自适应性能优化: + +```python +from plugins.user.fluid_dynamics.utils.performance_utils import FluidPerformanceOptimizer + +# 创建性能优化器 +optimizer = FluidPerformanceOptimizer(fluid_world) + +# 启用性能优化 +optimizer.optimize_for_performance() + +# 获取性能报告 +report = optimizer.get_performance_report() +print(f"当前FPS: {report['current_fps']}") +``` + +### 2. 可视化控制 + +调整流体的视觉效果: + +```python +# 设置流体颜色 +visualizer.set_fluid_color(Vec4(0.0, 0.8, 1.0, 0.7)) + +# 创建粒子系统 +particle_system = visualizer.create_particle_system("fluid_particles", 2000) +``` + +## API参考 + +### FluidWorld类 + +- `add_fluid_domain(domain)`: 添加流体域 +- `remove_fluid_domain(domain)`: 移除流体域 +- `add_fluid_emitter(emitter)`: 添加发射器 +- `remove_fluid_emitter(emitter)`: 移除发射器 +- `add_fluid_obstacle(obstacle)`: 添加障碍物 +- `remove_fluid_obstacle(obstacle)`: 移除障碍物 +- `update(dt)`: 更新流体世界 +- `pause(paused)`: 暂停/恢复模拟 +- `set_time_scale(scale)`: 设置时间缩放 + +### FluidSolver类 + +- `set_viscosity(viscosity)`: 设置粘度 +- `set_density(density)`: 设置密度 +- `set_gravity(gravity)`: 设置重力 +- `solve(dt)`: 执行流体求解 + +### FluidMaterial类 + +- `TYPE_WATER`: 水类型 +- `TYPE_OIL`: 油类型 +- `TYPE_LAVA`: 熔岩类型 +- `TYPE_SLIME`: 粘液类型 +- `TYPE_BLOOD`: 血液类型 +- `get_density()`: 获取密度 +- `get_viscosity()`: 获取粘度 +- `get_color()`: 获取颜色 + +## 示例场景 + +创建一个简单的流体演示: + +```python +def create_fluid_demo(world): + # 1. 获取插件实例 + fluid_plugin = world.plugin_manager.get_plugin("fluid_dynamics") + fluid_world = fluid_plugin.fluid_world + + # 2. 创建流体域 + domain = FluidDomain(Point3(-5, -5, 0), Point3(5, 5, 10)) + fluid_world.add_fluid_domain(domain) + + # 3. 创建发射器 + emitter = FluidEmitter(Point3(0, 0, 8), fluid_world) + emitter.set_emit_rate(30) + fluid_world.add_fluid_emitter(emitter) + + # 4. 创建障碍物 + obstacle = FluidObstacle(Point3(0, 0, 3), Vec3(1, 1, 3), "box") + fluid_world.add_fluid_obstacle(obstacle) + + # 5. 设置求解器 + solver = FluidSolver(fluid_world) + fluid_world.solver = solver +``` + +## 故障排除 + +### 常见问题 + +1. **性能问题**: + - 减少粒子数量 + - 降低发射器速率 + - 使用性能优化器 + +2. **稳定性问题**: + - 调整时间步长 + - 检查边界条件 + - 验证物理参数 + +3. **可视化问题**: + - 检查粒子系统配置 + - 确认渲染节点正确附着 + - 调整透明度和颜色设置 + +## 配置选项 + +编辑 `config/default_config.json` 文件可以调整默认参数: + +```json +{ + "simulation": { + "time_step": 0.016, + "gravity": [0, 0, -9.81], + "viscosity": 0.001, + "density": 1000.0 + }, + "performance": { + "max_particles": 5000, + "update_frequency": 60 + } +} ``` \ No newline at end of file diff --git a/plugins/user/fluid_dynamics/__init__.py b/plugins/user/fluid_dynamics/__init__.py index d35a819a..7b5d8670 100644 --- a/plugins/user/fluid_dynamics/__init__.py +++ b/plugins/user/fluid_dynamics/__init__.py @@ -1,4 +1,4 @@ -""" -流体动力学插件 -提供基于物理的流体模拟功能 +""" +流体动力学插件 +提供基于物理的流体模拟功能 """ \ No newline at end of file diff --git a/plugins/user/fluid_dynamics/components/fluid_domain.py b/plugins/user/fluid_dynamics/components/fluid_domain.py index 70cdc06c..fc00d93d 100644 --- a/plugins/user/fluid_dynamics/components/fluid_domain.py +++ b/plugins/user/fluid_dynamics/components/fluid_domain.py @@ -1,606 +1,606 @@ -""" -流体域组件 -定义流体模拟的边界和域 -""" - -from panda3d.core import Vec3, Point3, CollisionNode, CollisionSphere, CollisionBox -import math - -class FluidDomain: - """ - 流体域类 - 定义流体模拟的边界和域 - """ - - # 边界类型 - BOUNDARY_BOX = "box" - BOUNDARY_SPHERE = "sphere" - BOUNDARY_CYLINDER = "cylinder" - BOUNDARY_CONE = "cone" - - # 边界条件 - CONDITION_CLOSED = "closed" # 封闭边界 - CONDITION_OPEN = "open" # 开放边界 - CONDITION_PERIODIC = "periodic" # 周期性边界 - - def __init__(self, min_bound, max_bound, boundary_type=BOUNDARY_BOX, boundary_condition=CONDITION_CLOSED): - """ - 初始化流体域 - - Args: - min_bound (Point3): 最小边界点 - max_bound (Point3): 最大边界点 - boundary_type (str): 边界类型 - boundary_condition (str): 边界条件 - """ - self.min_bound = min_bound - self.max_bound = max_bound - self.size = max_bound - min_bound - self.center = (min_bound + max_bound) * 0.5 - self.boundary_type = boundary_type - self.boundary_condition = boundary_condition - - # 为不同边界类型计算额外参数 - if boundary_type == self.BOUNDARY_SPHERE: - # 计算球体半径 - self.radius = max(self.size.getX(), self.size.getY(), self.size.getZ()) / 2.0 - elif boundary_type == self.BOUNDARY_CYLINDER: - # 计算圆柱体半径和高度 - self.radius = max(self.size.getX(), self.size.getY()) / 2.0 - self.height = self.size.getZ() - elif boundary_type == self.BOUNDARY_CONE: - # 计算圆锥体半径和高度 - self.radius = max(self.size.getX(), self.size.getY()) / 2.0 - self.height = self.size.getZ() - - # 边界属性 - self.restitution = 0.3 # 恢复系数 - self.friction = 0.1 # 摩擦系数 - self.surface_roughness = 0.01 # 表面粗糙度 - - # 碰撞检测相关 - self.collision_margin = 0.05 # 碰撞边缘 - self.corner_radius = 0.05 # 角点半径 - - # 动态边界支持 - self.is_dynamic = False - self.velocity = Vec3(0, 0, 0) - self.rotation = Vec3(0, 0, 0) - - # 边界厚度 - self.boundary_thickness = 0.05 - - print(f"流体域创建完成: {min_bound} to {max_bound}, 类型: {boundary_type}, 条件: {boundary_condition}") - - def update(self, dt): - """ - 更新流体域状态 - - Args: - dt (float): 时间步长 - """ - if self.is_dynamic: - # 更新动态边界的位置和旋转 - self.center += self.velocity * dt - - # 在这里可以添加边界旋转的逻辑 - # self.rotation += self.angular_velocity * dt - - def handle_particle_collision(self, particle): - """ - 处理粒子与域边界的碰撞 - - Args: - particle: 流体粒子对象(字典格式) - """ - pos = particle['position'] - - if self.boundary_type == self.BOUNDARY_BOX: - self._handle_box_collision(particle) - elif self.boundary_type == self.BOUNDARY_SPHERE: - self._handle_sphere_collision(particle) - elif self.boundary_type == self.BOUNDARY_CYLINDER: - self._handle_cylinder_collision(particle) - elif self.boundary_type == self.BOUNDARY_CONE: - self._handle_cone_collision(particle) - - def _handle_box_collision(self, particle): - """ - 处理盒状边界碰撞 - - Args: - particle: 流体粒子对象 - """ - pos = particle['position'] - vel = particle['velocity'] - - # 获取边界值 - min_x, max_x = self.min_bound.getX(), self.max_bound.getX() - min_y, max_y = self.min_bound.getY(), self.max_bound.getY() - min_z, max_z = self.min_bound.getZ(), self.max_bound.getZ() - - # 检查X轴边界 - if pos.getX() <= min_x + self.collision_margin: - pos.setX(min_x + self.collision_margin) - vel.setX(-vel.getX() * (1 - self.friction) * self.restitution) - elif pos.getX() >= max_x - self.collision_margin: - pos.setX(max_x - self.collision_margin) - vel.setX(-vel.getX() * (1 - self.friction) * self.restitution) - - # 检查Y轴边界 - if pos.getY() <= min_y + self.collision_margin: - pos.setY(min_y + self.collision_margin) - vel.setY(-vel.getY() * (1 - self.friction) * self.restitution) - elif pos.getY() >= max_y - self.collision_margin: - pos.setY(max_y - self.collision_margin) - vel.setY(-vel.getY() * (1 - self.friction) * self.restitution) - - # 检查Z轴边界 - if pos.getZ() <= min_z + self.collision_margin: - pos.setZ(min_z + self.collision_margin) - vel.setZ(-vel.getZ() * (1 - self.friction) * self.restitution) - elif pos.getZ() >= max_z - self.collision_margin: - pos.setZ(max_z - self.collision_margin) - vel.setZ(-vel.getZ() * (1 - self.friction) * self.restitution) - - # 处理周期性边界条件 - if self.boundary_condition == self.CONDITION_PERIODIC: - self._apply_periodic_boundary_conditions(particle) - - def _handle_sphere_collision(self, particle): - """ - 处理球状边界碰撞 - - Args: - particle: 流体粒子对象 - """ - pos = particle['position'] - vel = particle['velocity'] - - # 计算粒子到球心的距离 - center_to_particle = pos - self.center - distance = center_to_particle.length() - - # 检查是否超出球体边界 - if distance > self.radius - self.collision_margin: - # 计算碰撞点 - normal = center_to_particle.normalized() - collision_point = self.center + normal * (self.radius - self.collision_margin) - - # 调整粒子位置 - particle['position'] = collision_point - - # 计算碰撞响应 - v_normal = normal.dot(vel) * normal # 法向速度 - v_tangent = vel - v_normal # 切向速度 - - # 应用反弹和摩擦 - new_v_normal = -v_normal * self.restitution - new_v_tangent = v_tangent * (1 - self.friction) - - particle['velocity'] = new_v_normal + new_v_tangent - - def _handle_cylinder_collision(self, particle): - """ - 处理圆柱边界碰撞 - - Args: - particle: 流体粒子对象 - """ - pos = particle['position'] - vel = particle['velocity'] - - # 计算在X-Y平面上到圆柱轴的距离 - dx = pos.getX() - self.center.getX() - dy = pos.getY() - self.center.getY() - distance_xy = math.sqrt(dx*dx + dy*dy) - - # 检查径向边界 - if distance_xy > self.radius - self.collision_margin: - # 计算径向方向 - radial_dir = Vec3(dx, dy, 0).normalized() - - # 调整位置 - new_x = self.center.getX() + radial_dir.getX() * (self.radius - self.collision_margin) - new_y = self.center.getY() + radial_dir.getY() * (self.radius - self.collision_margin) - pos.setX(new_x) - pos.setY(new_y) - - # 计算碰撞响应 - v_radial = radial_dir.dot(vel) * radial_dir - v_tangent = vel - v_radial - - # 应用反弹和摩擦 - new_v_radial = -v_radial * self.restitution - particle['velocity'] = new_v_radial + v_tangent * (1 - self.friction) - - # 检查Z轴边界 - min_z = self.center.getZ() - self.height/2.0 - max_z = self.center.getZ() + self.height/2.0 - - if pos.getZ() <= min_z + self.collision_margin: - pos.setZ(min_z + self.collision_margin) - vel.setZ(-vel.getZ() * (1 - self.friction) * self.restitution) - elif pos.getZ() >= max_z - self.collision_margin: - pos.setZ(max_z - self.collision_margin) - vel.setZ(-vel.getZ() * (1 - self.friction) * self.restitution) - - def _handle_cone_collision(self, particle): - """ - 处理圆锥边界碰撞 - - Args: - particle: 流体粒子对象 - """ - pos = particle['position'] - vel = particle['velocity'] - - # 计算相对于圆锥顶点的位置 - rel_pos = pos - self.center - rel_pos.setZ(rel_pos.getZ() + self.height/2) # 调整Z轴使顶点在底部 - - # 计算在X-Y平面上到圆锥轴的距离 - distance_xy = math.sqrt(rel_pos.getX()**2 + rel_pos.getY()**2) - - # 计算圆锥在当前高度的半径 - cone_radius_at_height = (rel_pos.getZ() / self.height) * self.radius - - # 检查是否超出圆锥边界 - if distance_xy > cone_radius_at_height - self.collision_margin and rel_pos.getZ() > 0: - # 计算圆锥表面的法向量 - radial_dir = Vec3(rel_pos.getX(), rel_pos.getY(), 0).normalized() - surface_normal = Vec3( - radial_dir.getX(), - radial_dir.getY(), - -self.radius / self.height # 圆锥的斜率 - ).normalized() - - # 调整粒子位置到表面 - adjusted_radius = cone_radius_at_height - self.collision_margin - particle['position'] = Point3( - self.center.getX() + radial_dir.getX() * adjusted_radius, - self.center.getY() + radial_dir.getY() * adjusted_radius, - self.center.getZ() + rel_pos.getZ() - self.height/2 - ) - - # 计算碰撞响应 - v_normal = surface_normal.dot(vel) * surface_normal - v_tangent = vel - v_normal - - # 应用反弹和摩擦 - new_v_normal = -v_normal * self.restitution - particle['velocity'] = new_v_normal + v_tangent * (1 - self.friction) - - # 检查Z轴边界 - if rel_pos.getZ() < 0: # 底部 - pos.setZ(self.center.getZ() - self.height/2 + self.collision_margin) - vel.setZ(-vel.getZ() * (1 - self.friction) * self.restitution) - elif rel_pos.getZ() > self.height: # 顶部(圆锥尖端) - pos.setZ(self.center.getZ() + self.height/2 - self.collision_margin) - vel.setZ(-vel.getZ() * (1 - self.friction) * self.restitution) - - def _apply_periodic_boundary_conditions(self, particle): - """ - 应用周期性边界条件 - - Args: - particle: 流体粒子对象 - """ - pos = particle['position'] - - # X轴周期性 - if pos.getX() < self.min_bound.getX(): - particle['position'].setX(self.max_bound.getX() - (self.min_bound.getX() - pos.getX())) - elif pos.getX() > self.max_bound.getX(): - particle['position'].setX(self.min_bound.getX() + (pos.getX() - self.max_bound.getX())) - - # Y轴周期性 - if pos.getY() < self.min_bound.getY(): - particle['position'].setY(self.max_bound.getY() - (self.min_bound.getY() - pos.getY())) - elif pos.getY() > self.max_bound.getY(): - particle['position'].setY(self.min_bound.getY() + (pos.getY() - self.max_bound.getY())) - - # Z轴周期性 - if pos.getZ() < self.min_bound.getZ(): - particle['position'].setZ(self.max_bound.getZ() - (self.min_bound.getZ() - pos.getZ())) - elif pos.getZ() > self.max_bound.getZ(): - particle['position'].setZ(self.min_bound.getZ() + (pos.getZ() - self.max_bound.getZ())) - - def is_point_inside(self, point): - """ - 检查点是否在域内 - - Args: - point (Point3): 检查点 - - Returns: - bool: 是否在域内 - """ - if self.boundary_type == self.BOUNDARY_BOX: - return (self.min_bound.getX() <= point.getX() <= self.max_bound.getX() and - self.min_bound.getY() <= point.getY() <= self.max_bound.getY() and - self.min_bound.getZ() <= point.getZ() <= self.max_bound.getZ()) - elif self.boundary_type == self.BOUNDARY_SPHERE: - distance = (point - self.center).length() - return distance <= self.radius - elif self.boundary_type == self.BOUNDARY_CYLINDER: - dx = point.getX() - self.center.getX() - dy = point.getY() - self.center.getY() - distance_xy = math.sqrt(dx*dx + dy*dy) - z_in_range = self.center.getZ() - self.height/2 <= point.getZ() <= self.center.getZ() + self.height/2 - return distance_xy <= self.radius and z_in_range - elif self.boundary_type == self.BOUNDARY_CONE: - rel_point = point - self.center - rel_point.setZ(rel_point.getZ() + self.height/2) - dx = rel_point.getX() - dy = rel_point.getY() - distance_xy = math.sqrt(dx*dx + dy*dy) - cone_radius_at_height = (rel_point.getZ() / self.height) * self.radius - return distance_xy <= cone_radius_at_height and 0 <= rel_point.getZ() <= self.height - - return False # 默认返回False - - def get_volume(self): - """ - 计算域体积 - - Returns: - float: 域体积 - """ - if self.boundary_type == self.BOUNDARY_BOX: - return self.size.getX() * self.size.getY() * self.size.getZ() - elif self.boundary_type == self.BOUNDARY_SPHERE: - return (4.0/3.0) * math.pi * (self.radius ** 3) - elif self.boundary_type == self.BOUNDARY_CYLINDER: - return math.pi * (self.radius ** 2) * self.height - elif self.boundary_type == self.BOUNDARY_CONE: - return (1.0/3.0) * math.pi * (self.radius ** 2) * self.height - else: - # 默认按盒状计算 - return self.size.getX() * self.size.getY() * self.size.getZ() - - def set_boundary_condition(self, condition): - """ - 设置边界条件 - - Args: - condition (str): 边界条件类型 - """ - self.boundary_condition = condition - - def set_material_properties(self, restitution=None, friction=None, surface_roughness=None): - """ - 设置边界材料属性 - - Args: - restitution (float): 恢复系数 - friction (float): 摩擦系数 - surface_roughness (float): 表面粗糙度 - """ - if restitution is not None: - self.restitution = max(0.0, min(1.0, restitution)) - if friction is not None: - self.friction = max(0.0, min(1.0, friction)) - if surface_roughness is not None: - self.surface_roughness = max(0.0, surface_roughness) - - def set_dynamic(self, is_dynamic, velocity=None, rotation=None): - """ - 设置边界是否为动态 - - Args: - is_dynamic (bool): 是否为动态边界 - velocity (Vec3): 速度 - rotation (Vec3): 旋转 - """ - self.is_dynamic = is_dynamic - if velocity: - self.velocity = velocity - if rotation: - self.rotation = rotation - - def get_surface_area(self): - """ - 计算边界表面积 - - Returns: - float: 表面积 - """ - if self.boundary_type == self.BOUNDARY_BOX: - return 2 * (self.size.getX() * self.size.getY() + - self.size.getX() * self.size.getZ() + - self.size.getY() * self.size.getZ()) - elif self.boundary_type == self.BOUNDARY_SPHERE: - return 4 * math.pi * (self.radius ** 2) - elif self.boundary_type == self.BOUNDARY_CYLINDER: - return 2 * math.pi * self.radius * (self.radius + self.height) - elif self.boundary_type == self.BOUNDARY_CONE: - slant_height = math.sqrt(self.radius**2 + self.height**2) - return math.pi * self.radius * (self.radius + slant_height) - else: - # 默认按盒状计算 - return 2 * (self.size.getX() * self.size.getY() + - self.size.getX() * self.size.getZ() + - self.size.getY() * self.size.getZ()) - - def get_bounding_box(self): - """ - 获取边界框 - - Returns: - tuple: (min_bound, max_bound) - """ - return (self.min_bound, self.max_bound) - - def get_boundary_mesh(self): - """ - 获取边界网格(用于可视化) - - Returns: - dict: 包含网格顶点和面的字典 - """ - if self.boundary_type == self.BOUNDARY_BOX: - return self._get_box_mesh() - elif self.boundary_type == self.BOUNDARY_SPHERE: - return self._get_sphere_mesh() - elif self.boundary_type == self.BOUNDARY_CYLINDER: - return self._get_cylinder_mesh() - elif self.boundary_type == self.BOUNDARY_CONE: - return self._get_cone_mesh() - else: - return self._get_box_mesh() # 默认返回盒状网格 - - def _get_box_mesh(self): - """ - 获取盒状边界网格 - - Returns: - dict: 网格数据 - """ - vertices = [ - Point3(self.min_bound.getX(), self.min_bound.getY(), self.min_bound.getZ()), - Point3(self.max_bound.getX(), self.min_bound.getY(), self.min_bound.getZ()), - Point3(self.max_bound.getX(), self.max_bound.getY(), self.min_bound.getZ()), - Point3(self.min_bound.getX(), self.max_bound.getY(), self.min_bound.getZ()), - Point3(self.min_bound.getX(), self.min_bound.getY(), self.max_bound.getZ()), - Point3(self.max_bound.getX(), self.min_bound.getY(), self.max_bound.getZ()), - Point3(self.max_bound.getX(), self.max_bound.getY(), self.max_bound.getZ()), - Point3(self.min_bound.getX(), self.max_bound.getY(), self.max_bound.getZ()), - ] - - faces = [ - [0, 1, 2, 3], # 底面 - [4, 7, 6, 5], # 顶面 - [0, 4, 5, 1], # 前面 - [2, 6, 7, 3], # 后面 - [0, 3, 7, 4], # 左面 - [1, 5, 6, 2], # 右面 - ] - - return {"vertices": vertices, "faces": faces} - - def _get_sphere_mesh(self, resolution=16): - """ - 获取球状边界网格 - - Args: - resolution (int): 网格分辨率 - - Returns: - dict: 网格数据 - """ - vertices = [] - faces = [] - - # 生成球面顶点 - for i in range(resolution + 1): - phi = math.pi * i / resolution # 0 到 π - for j in range(resolution + 1): - theta = 2 * math.pi * j / resolution # 0 到 2π - - x = self.center.getX() + self.radius * math.sin(phi) * math.cos(theta) - y = self.center.getY() + self.radius * math.sin(phi) * math.sin(theta) - z = self.center.getZ() + self.radius * math.cos(phi) - - vertices.append(Point3(x, y, z)) - - # 生成面 - for i in range(resolution): - for j in range(resolution): - first = i * (resolution + 1) + j - second = first + resolution + 1 - - # 添加两个三角形形成一个四边形面 - faces.append([first, second, first + 1]) - faces.append([second, second + 1, first + 1]) - - return {"vertices": vertices, "faces": faces} - - def _get_cylinder_mesh(self, resolution=16): - """ - 获取圆柱边界网格 - - Args: - resolution (int): 网格分辨率 - - Returns: - dict: 网格数据 - """ - vertices = [] - faces = [] - - # 生成底部圆环 - base_center = Point3(self.center.getX(), self.center.getY(), self.center.getZ() - self.height/2) - for i in range(resolution): - theta = 2 * math.pi * i / resolution - x = base_center.getX() + self.radius * math.cos(theta) - y = base_center.getY() + self.radius * math.sin(theta) - z = base_center.getZ() - vertices.append(Point3(x, y, z)) - - # 添加底部中心点 - vertices.append(base_center) - - # 生成顶部圆环 - top_center = Point3(self.center.getX(), self.center.getY(), self.center.getZ() + self.height/2) - for i in range(resolution): - theta = 2 * math.pi * i / resolution - x = top_center.getX() + self.radius * math.cos(theta) - y = top_center.getY() + self.radius * math.sin(theta) - z = top_center.getZ() - vertices.append(Point3(x, y, z)) - - # 添加顶部中心点 - vertices.append(top_center) - - # 生成侧面 - for i in range(resolution): - next_i = (i + 1) % resolution - # 底面 - faces.append([i, resolution, (next_i if next_i != 0 else resolution-1)]) - # 顶面 - faces.append([i + resolution + 1, resolution * 2 + 1, (next_i + resolution + 1) if next_i != 0 else resolution]) - # 侧面 - faces.append([i, next_i, i + resolution + 1]) - faces.append([next_i, next_i + resolution + 1, i + resolution + 1]) - - return {"vertices": vertices, "faces": faces} - - def _get_cone_mesh(self, resolution=16): - """ - 获取圆锥边界网格 - - Args: - resolution (int): 网格分辨率 - - Returns: - dict: 网格数据 - """ - vertices = [] - faces = [] - - # 生成底部圆环 - base_center = Point3(self.center.getX(), self.center.getY(), self.center.getZ() - self.height/2) - for i in range(resolution): - theta = 2 * math.pi * i / resolution - x = base_center.getX() + self.radius * math.cos(theta) - y = base_center.getY() + self.radius * math.sin(theta) - z = base_center.getZ() - vertices.append(Point3(x, y, z)) - - # 添加底部中心点 - vertices.append(base_center) - - # 圆锥顶点 - apex = Point3(self.center.getX(), self.center.getY(), self.center.getZ() + self.height/2) - vertices.append(apex) - - # 生成侧面 - for i in range(resolution): - next_i = (i + 1) % resolution - # 底面 - faces.append([i, resolution, (next_i if next_i != 0 else resolution-1)]) - # 侧面 - faces.append([i, (next_i if next_i != 0 else resolution-1), resolution + 1]) - +""" +流体域组件 +定义流体模拟的边界和域 +""" + +from panda3d.core import Vec3, Point3, CollisionNode, CollisionSphere, CollisionBox +import math + +class FluidDomain: + """ + 流体域类 + 定义流体模拟的边界和域 + """ + + # 边界类型 + BOUNDARY_BOX = "box" + BOUNDARY_SPHERE = "sphere" + BOUNDARY_CYLINDER = "cylinder" + BOUNDARY_CONE = "cone" + + # 边界条件 + CONDITION_CLOSED = "closed" # 封闭边界 + CONDITION_OPEN = "open" # 开放边界 + CONDITION_PERIODIC = "periodic" # 周期性边界 + + def __init__(self, min_bound, max_bound, boundary_type=BOUNDARY_BOX, boundary_condition=CONDITION_CLOSED): + """ + 初始化流体域 + + Args: + min_bound (Point3): 最小边界点 + max_bound (Point3): 最大边界点 + boundary_type (str): 边界类型 + boundary_condition (str): 边界条件 + """ + self.min_bound = min_bound + self.max_bound = max_bound + self.size = max_bound - min_bound + self.center = (min_bound + max_bound) * 0.5 + self.boundary_type = boundary_type + self.boundary_condition = boundary_condition + + # 为不同边界类型计算额外参数 + if boundary_type == self.BOUNDARY_SPHERE: + # 计算球体半径 + self.radius = max(self.size.getX(), self.size.getY(), self.size.getZ()) / 2.0 + elif boundary_type == self.BOUNDARY_CYLINDER: + # 计算圆柱体半径和高度 + self.radius = max(self.size.getX(), self.size.getY()) / 2.0 + self.height = self.size.getZ() + elif boundary_type == self.BOUNDARY_CONE: + # 计算圆锥体半径和高度 + self.radius = max(self.size.getX(), self.size.getY()) / 2.0 + self.height = self.size.getZ() + + # 边界属性 + self.restitution = 0.3 # 恢复系数 + self.friction = 0.1 # 摩擦系数 + self.surface_roughness = 0.01 # 表面粗糙度 + + # 碰撞检测相关 + self.collision_margin = 0.05 # 碰撞边缘 + self.corner_radius = 0.05 # 角点半径 + + # 动态边界支持 + self.is_dynamic = False + self.velocity = Vec3(0, 0, 0) + self.rotation = Vec3(0, 0, 0) + + # 边界厚度 + self.boundary_thickness = 0.05 + + print(f"流体域创建完成: {min_bound} to {max_bound}, 类型: {boundary_type}, 条件: {boundary_condition}") + + def update(self, dt): + """ + 更新流体域状态 + + Args: + dt (float): 时间步长 + """ + if self.is_dynamic: + # 更新动态边界的位置和旋转 + self.center += self.velocity * dt + + # 在这里可以添加边界旋转的逻辑 + # self.rotation += self.angular_velocity * dt + + def handle_particle_collision(self, particle): + """ + 处理粒子与域边界的碰撞 + + Args: + particle: 流体粒子对象(字典格式) + """ + pos = particle['position'] + + if self.boundary_type == self.BOUNDARY_BOX: + self._handle_box_collision(particle) + elif self.boundary_type == self.BOUNDARY_SPHERE: + self._handle_sphere_collision(particle) + elif self.boundary_type == self.BOUNDARY_CYLINDER: + self._handle_cylinder_collision(particle) + elif self.boundary_type == self.BOUNDARY_CONE: + self._handle_cone_collision(particle) + + def _handle_box_collision(self, particle): + """ + 处理盒状边界碰撞 + + Args: + particle: 流体粒子对象 + """ + pos = particle['position'] + vel = particle['velocity'] + + # 获取边界值 + min_x, max_x = self.min_bound.getX(), self.max_bound.getX() + min_y, max_y = self.min_bound.getY(), self.max_bound.getY() + min_z, max_z = self.min_bound.getZ(), self.max_bound.getZ() + + # 检查X轴边界 + if pos.getX() <= min_x + self.collision_margin: + pos.setX(min_x + self.collision_margin) + vel.setX(-vel.getX() * (1 - self.friction) * self.restitution) + elif pos.getX() >= max_x - self.collision_margin: + pos.setX(max_x - self.collision_margin) + vel.setX(-vel.getX() * (1 - self.friction) * self.restitution) + + # 检查Y轴边界 + if pos.getY() <= min_y + self.collision_margin: + pos.setY(min_y + self.collision_margin) + vel.setY(-vel.getY() * (1 - self.friction) * self.restitution) + elif pos.getY() >= max_y - self.collision_margin: + pos.setY(max_y - self.collision_margin) + vel.setY(-vel.getY() * (1 - self.friction) * self.restitution) + + # 检查Z轴边界 + if pos.getZ() <= min_z + self.collision_margin: + pos.setZ(min_z + self.collision_margin) + vel.setZ(-vel.getZ() * (1 - self.friction) * self.restitution) + elif pos.getZ() >= max_z - self.collision_margin: + pos.setZ(max_z - self.collision_margin) + vel.setZ(-vel.getZ() * (1 - self.friction) * self.restitution) + + # 处理周期性边界条件 + if self.boundary_condition == self.CONDITION_PERIODIC: + self._apply_periodic_boundary_conditions(particle) + + def _handle_sphere_collision(self, particle): + """ + 处理球状边界碰撞 + + Args: + particle: 流体粒子对象 + """ + pos = particle['position'] + vel = particle['velocity'] + + # 计算粒子到球心的距离 + center_to_particle = pos - self.center + distance = center_to_particle.length() + + # 检查是否超出球体边界 + if distance > self.radius - self.collision_margin: + # 计算碰撞点 + normal = center_to_particle.normalized() + collision_point = self.center + normal * (self.radius - self.collision_margin) + + # 调整粒子位置 + particle['position'] = collision_point + + # 计算碰撞响应 + v_normal = normal.dot(vel) * normal # 法向速度 + v_tangent = vel - v_normal # 切向速度 + + # 应用反弹和摩擦 + new_v_normal = -v_normal * self.restitution + new_v_tangent = v_tangent * (1 - self.friction) + + particle['velocity'] = new_v_normal + new_v_tangent + + def _handle_cylinder_collision(self, particle): + """ + 处理圆柱边界碰撞 + + Args: + particle: 流体粒子对象 + """ + pos = particle['position'] + vel = particle['velocity'] + + # 计算在X-Y平面上到圆柱轴的距离 + dx = pos.getX() - self.center.getX() + dy = pos.getY() - self.center.getY() + distance_xy = math.sqrt(dx*dx + dy*dy) + + # 检查径向边界 + if distance_xy > self.radius - self.collision_margin: + # 计算径向方向 + radial_dir = Vec3(dx, dy, 0).normalized() + + # 调整位置 + new_x = self.center.getX() + radial_dir.getX() * (self.radius - self.collision_margin) + new_y = self.center.getY() + radial_dir.getY() * (self.radius - self.collision_margin) + pos.setX(new_x) + pos.setY(new_y) + + # 计算碰撞响应 + v_radial = radial_dir.dot(vel) * radial_dir + v_tangent = vel - v_radial + + # 应用反弹和摩擦 + new_v_radial = -v_radial * self.restitution + particle['velocity'] = new_v_radial + v_tangent * (1 - self.friction) + + # 检查Z轴边界 + min_z = self.center.getZ() - self.height/2.0 + max_z = self.center.getZ() + self.height/2.0 + + if pos.getZ() <= min_z + self.collision_margin: + pos.setZ(min_z + self.collision_margin) + vel.setZ(-vel.getZ() * (1 - self.friction) * self.restitution) + elif pos.getZ() >= max_z - self.collision_margin: + pos.setZ(max_z - self.collision_margin) + vel.setZ(-vel.getZ() * (1 - self.friction) * self.restitution) + + def _handle_cone_collision(self, particle): + """ + 处理圆锥边界碰撞 + + Args: + particle: 流体粒子对象 + """ + pos = particle['position'] + vel = particle['velocity'] + + # 计算相对于圆锥顶点的位置 + rel_pos = pos - self.center + rel_pos.setZ(rel_pos.getZ() + self.height/2) # 调整Z轴使顶点在底部 + + # 计算在X-Y平面上到圆锥轴的距离 + distance_xy = math.sqrt(rel_pos.getX()**2 + rel_pos.getY()**2) + + # 计算圆锥在当前高度的半径 + cone_radius_at_height = (rel_pos.getZ() / self.height) * self.radius + + # 检查是否超出圆锥边界 + if distance_xy > cone_radius_at_height - self.collision_margin and rel_pos.getZ() > 0: + # 计算圆锥表面的法向量 + radial_dir = Vec3(rel_pos.getX(), rel_pos.getY(), 0).normalized() + surface_normal = Vec3( + radial_dir.getX(), + radial_dir.getY(), + -self.radius / self.height # 圆锥的斜率 + ).normalized() + + # 调整粒子位置到表面 + adjusted_radius = cone_radius_at_height - self.collision_margin + particle['position'] = Point3( + self.center.getX() + radial_dir.getX() * adjusted_radius, + self.center.getY() + radial_dir.getY() * adjusted_radius, + self.center.getZ() + rel_pos.getZ() - self.height/2 + ) + + # 计算碰撞响应 + v_normal = surface_normal.dot(vel) * surface_normal + v_tangent = vel - v_normal + + # 应用反弹和摩擦 + new_v_normal = -v_normal * self.restitution + particle['velocity'] = new_v_normal + v_tangent * (1 - self.friction) + + # 检查Z轴边界 + if rel_pos.getZ() < 0: # 底部 + pos.setZ(self.center.getZ() - self.height/2 + self.collision_margin) + vel.setZ(-vel.getZ() * (1 - self.friction) * self.restitution) + elif rel_pos.getZ() > self.height: # 顶部(圆锥尖端) + pos.setZ(self.center.getZ() + self.height/2 - self.collision_margin) + vel.setZ(-vel.getZ() * (1 - self.friction) * self.restitution) + + def _apply_periodic_boundary_conditions(self, particle): + """ + 应用周期性边界条件 + + Args: + particle: 流体粒子对象 + """ + pos = particle['position'] + + # X轴周期性 + if pos.getX() < self.min_bound.getX(): + particle['position'].setX(self.max_bound.getX() - (self.min_bound.getX() - pos.getX())) + elif pos.getX() > self.max_bound.getX(): + particle['position'].setX(self.min_bound.getX() + (pos.getX() - self.max_bound.getX())) + + # Y轴周期性 + if pos.getY() < self.min_bound.getY(): + particle['position'].setY(self.max_bound.getY() - (self.min_bound.getY() - pos.getY())) + elif pos.getY() > self.max_bound.getY(): + particle['position'].setY(self.min_bound.getY() + (pos.getY() - self.max_bound.getY())) + + # Z轴周期性 + if pos.getZ() < self.min_bound.getZ(): + particle['position'].setZ(self.max_bound.getZ() - (self.min_bound.getZ() - pos.getZ())) + elif pos.getZ() > self.max_bound.getZ(): + particle['position'].setZ(self.min_bound.getZ() + (pos.getZ() - self.max_bound.getZ())) + + def is_point_inside(self, point): + """ + 检查点是否在域内 + + Args: + point (Point3): 检查点 + + Returns: + bool: 是否在域内 + """ + if self.boundary_type == self.BOUNDARY_BOX: + return (self.min_bound.getX() <= point.getX() <= self.max_bound.getX() and + self.min_bound.getY() <= point.getY() <= self.max_bound.getY() and + self.min_bound.getZ() <= point.getZ() <= self.max_bound.getZ()) + elif self.boundary_type == self.BOUNDARY_SPHERE: + distance = (point - self.center).length() + return distance <= self.radius + elif self.boundary_type == self.BOUNDARY_CYLINDER: + dx = point.getX() - self.center.getX() + dy = point.getY() - self.center.getY() + distance_xy = math.sqrt(dx*dx + dy*dy) + z_in_range = self.center.getZ() - self.height/2 <= point.getZ() <= self.center.getZ() + self.height/2 + return distance_xy <= self.radius and z_in_range + elif self.boundary_type == self.BOUNDARY_CONE: + rel_point = point - self.center + rel_point.setZ(rel_point.getZ() + self.height/2) + dx = rel_point.getX() + dy = rel_point.getY() + distance_xy = math.sqrt(dx*dx + dy*dy) + cone_radius_at_height = (rel_point.getZ() / self.height) * self.radius + return distance_xy <= cone_radius_at_height and 0 <= rel_point.getZ() <= self.height + + return False # 默认返回False + + def get_volume(self): + """ + 计算域体积 + + Returns: + float: 域体积 + """ + if self.boundary_type == self.BOUNDARY_BOX: + return self.size.getX() * self.size.getY() * self.size.getZ() + elif self.boundary_type == self.BOUNDARY_SPHERE: + return (4.0/3.0) * math.pi * (self.radius ** 3) + elif self.boundary_type == self.BOUNDARY_CYLINDER: + return math.pi * (self.radius ** 2) * self.height + elif self.boundary_type == self.BOUNDARY_CONE: + return (1.0/3.0) * math.pi * (self.radius ** 2) * self.height + else: + # 默认按盒状计算 + return self.size.getX() * self.size.getY() * self.size.getZ() + + def set_boundary_condition(self, condition): + """ + 设置边界条件 + + Args: + condition (str): 边界条件类型 + """ + self.boundary_condition = condition + + def set_material_properties(self, restitution=None, friction=None, surface_roughness=None): + """ + 设置边界材料属性 + + Args: + restitution (float): 恢复系数 + friction (float): 摩擦系数 + surface_roughness (float): 表面粗糙度 + """ + if restitution is not None: + self.restitution = max(0.0, min(1.0, restitution)) + if friction is not None: + self.friction = max(0.0, min(1.0, friction)) + if surface_roughness is not None: + self.surface_roughness = max(0.0, surface_roughness) + + def set_dynamic(self, is_dynamic, velocity=None, rotation=None): + """ + 设置边界是否为动态 + + Args: + is_dynamic (bool): 是否为动态边界 + velocity (Vec3): 速度 + rotation (Vec3): 旋转 + """ + self.is_dynamic = is_dynamic + if velocity: + self.velocity = velocity + if rotation: + self.rotation = rotation + + def get_surface_area(self): + """ + 计算边界表面积 + + Returns: + float: 表面积 + """ + if self.boundary_type == self.BOUNDARY_BOX: + return 2 * (self.size.getX() * self.size.getY() + + self.size.getX() * self.size.getZ() + + self.size.getY() * self.size.getZ()) + elif self.boundary_type == self.BOUNDARY_SPHERE: + return 4 * math.pi * (self.radius ** 2) + elif self.boundary_type == self.BOUNDARY_CYLINDER: + return 2 * math.pi * self.radius * (self.radius + self.height) + elif self.boundary_type == self.BOUNDARY_CONE: + slant_height = math.sqrt(self.radius**2 + self.height**2) + return math.pi * self.radius * (self.radius + slant_height) + else: + # 默认按盒状计算 + return 2 * (self.size.getX() * self.size.getY() + + self.size.getX() * self.size.getZ() + + self.size.getY() * self.size.getZ()) + + def get_bounding_box(self): + """ + 获取边界框 + + Returns: + tuple: (min_bound, max_bound) + """ + return (self.min_bound, self.max_bound) + + def get_boundary_mesh(self): + """ + 获取边界网格(用于可视化) + + Returns: + dict: 包含网格顶点和面的字典 + """ + if self.boundary_type == self.BOUNDARY_BOX: + return self._get_box_mesh() + elif self.boundary_type == self.BOUNDARY_SPHERE: + return self._get_sphere_mesh() + elif self.boundary_type == self.BOUNDARY_CYLINDER: + return self._get_cylinder_mesh() + elif self.boundary_type == self.BOUNDARY_CONE: + return self._get_cone_mesh() + else: + return self._get_box_mesh() # 默认返回盒状网格 + + def _get_box_mesh(self): + """ + 获取盒状边界网格 + + Returns: + dict: 网格数据 + """ + vertices = [ + Point3(self.min_bound.getX(), self.min_bound.getY(), self.min_bound.getZ()), + Point3(self.max_bound.getX(), self.min_bound.getY(), self.min_bound.getZ()), + Point3(self.max_bound.getX(), self.max_bound.getY(), self.min_bound.getZ()), + Point3(self.min_bound.getX(), self.max_bound.getY(), self.min_bound.getZ()), + Point3(self.min_bound.getX(), self.min_bound.getY(), self.max_bound.getZ()), + Point3(self.max_bound.getX(), self.min_bound.getY(), self.max_bound.getZ()), + Point3(self.max_bound.getX(), self.max_bound.getY(), self.max_bound.getZ()), + Point3(self.min_bound.getX(), self.max_bound.getY(), self.max_bound.getZ()), + ] + + faces = [ + [0, 1, 2, 3], # 底面 + [4, 7, 6, 5], # 顶面 + [0, 4, 5, 1], # 前面 + [2, 6, 7, 3], # 后面 + [0, 3, 7, 4], # 左面 + [1, 5, 6, 2], # 右面 + ] + + return {"vertices": vertices, "faces": faces} + + def _get_sphere_mesh(self, resolution=16): + """ + 获取球状边界网格 + + Args: + resolution (int): 网格分辨率 + + Returns: + dict: 网格数据 + """ + vertices = [] + faces = [] + + # 生成球面顶点 + for i in range(resolution + 1): + phi = math.pi * i / resolution # 0 到 π + for j in range(resolution + 1): + theta = 2 * math.pi * j / resolution # 0 到 2π + + x = self.center.getX() + self.radius * math.sin(phi) * math.cos(theta) + y = self.center.getY() + self.radius * math.sin(phi) * math.sin(theta) + z = self.center.getZ() + self.radius * math.cos(phi) + + vertices.append(Point3(x, y, z)) + + # 生成面 + for i in range(resolution): + for j in range(resolution): + first = i * (resolution + 1) + j + second = first + resolution + 1 + + # 添加两个三角形形成一个四边形面 + faces.append([first, second, first + 1]) + faces.append([second, second + 1, first + 1]) + + return {"vertices": vertices, "faces": faces} + + def _get_cylinder_mesh(self, resolution=16): + """ + 获取圆柱边界网格 + + Args: + resolution (int): 网格分辨率 + + Returns: + dict: 网格数据 + """ + vertices = [] + faces = [] + + # 生成底部圆环 + base_center = Point3(self.center.getX(), self.center.getY(), self.center.getZ() - self.height/2) + for i in range(resolution): + theta = 2 * math.pi * i / resolution + x = base_center.getX() + self.radius * math.cos(theta) + y = base_center.getY() + self.radius * math.sin(theta) + z = base_center.getZ() + vertices.append(Point3(x, y, z)) + + # 添加底部中心点 + vertices.append(base_center) + + # 生成顶部圆环 + top_center = Point3(self.center.getX(), self.center.getY(), self.center.getZ() + self.height/2) + for i in range(resolution): + theta = 2 * math.pi * i / resolution + x = top_center.getX() + self.radius * math.cos(theta) + y = top_center.getY() + self.radius * math.sin(theta) + z = top_center.getZ() + vertices.append(Point3(x, y, z)) + + # 添加顶部中心点 + vertices.append(top_center) + + # 生成侧面 + for i in range(resolution): + next_i = (i + 1) % resolution + # 底面 + faces.append([i, resolution, (next_i if next_i != 0 else resolution-1)]) + # 顶面 + faces.append([i + resolution + 1, resolution * 2 + 1, (next_i + resolution + 1) if next_i != 0 else resolution]) + # 侧面 + faces.append([i, next_i, i + resolution + 1]) + faces.append([next_i, next_i + resolution + 1, i + resolution + 1]) + + return {"vertices": vertices, "faces": faces} + + def _get_cone_mesh(self, resolution=16): + """ + 获取圆锥边界网格 + + Args: + resolution (int): 网格分辨率 + + Returns: + dict: 网格数据 + """ + vertices = [] + faces = [] + + # 生成底部圆环 + base_center = Point3(self.center.getX(), self.center.getY(), self.center.getZ() - self.height/2) + for i in range(resolution): + theta = 2 * math.pi * i / resolution + x = base_center.getX() + self.radius * math.cos(theta) + y = base_center.getY() + self.radius * math.sin(theta) + z = base_center.getZ() + vertices.append(Point3(x, y, z)) + + # 添加底部中心点 + vertices.append(base_center) + + # 圆锥顶点 + apex = Point3(self.center.getX(), self.center.getY(), self.center.getZ() + self.height/2) + vertices.append(apex) + + # 生成侧面 + for i in range(resolution): + next_i = (i + 1) % resolution + # 底面 + faces.append([i, resolution, (next_i if next_i != 0 else resolution-1)]) + # 侧面 + faces.append([i, (next_i if next_i != 0 else resolution-1), resolution + 1]) + return {"vertices": vertices, "faces": faces} \ No newline at end of file diff --git a/plugins/user/fluid_dynamics/components/fluid_emitter.py b/plugins/user/fluid_dynamics/components/fluid_emitter.py index 0d5fab33..b24c961d 100644 --- a/plugins/user/fluid_dynamics/components/fluid_emitter.py +++ b/plugins/user/fluid_dynamics/components/fluid_emitter.py @@ -1,140 +1,140 @@ -""" -流体发射器组件 -负责生成流体粒子 -""" - -from panda3d.core import Vec3, Point3 -import random -import math - -class FluidEmitter: - """ - 流体发射器类 - 负责生成流体粒子 - """ - - def __init__(self, position, fluid_world): - """ - 初始化流体发射器 - - Args: - position (Point3): 发射器位置 - fluid_world: 流体世界对象的引用 - """ - self.position = position - self.fluid_world = fluid_world - - # 发射器属性 - self.emit_rate = 10 # 每秒发射粒子数 - self.emit_velocity = Vec3(0, 0, 5) # 发射速度 - self.emit_spread = 0.1 # 发射扩散角度 - self.emit_count = 0 # 已发射粒子数 - self.is_active = True # 是否激活 - - # 粒子属性 - self.particle_mass = 1.0 # 粒子质量 - self.particle_lifespan = 5.0 # 粒子寿命 - - print(f"流体发射器创建完成: {position}") - - def update(self, dt): - """ - 更新发射器状态 - - Args: - dt (float): 时间步长 - """ - if not self.is_active: - return - - # 计算应该发射的粒子数 - particles_to_emit = int(self.emit_rate * dt) - - # 发射粒子 - for _ in range(particles_to_emit): - self._emit_particle() - - def _emit_particle(self): - """ - 发射单个粒子 - """ - # 创建粒子对象(这里简化为字典,实际实现中应该是专门的粒子类) - particle = { - 'position': self.position + Vec3( - random.uniform(-0.1, 0.1), - random.uniform(-0.1, 0.1), - random.uniform(-0.1, 0.1) - ), - 'velocity': self._get_random_velocity(), - 'mass': self.particle_mass, - 'lifespan': self.particle_lifespan, - 'age': 0.0 - } - - # 添加到流体世界 - self.fluid_world.add_fluid_particle(particle) - self.emit_count += 1 - - def _get_random_velocity(self): - """ - 获取随机发射速度 - - Returns: - Vec3: 随机速度向量 - """ - # 基础发射方向 - base_direction = self.emit_velocity.normalized() - - # 添加随机扩散 - angle = random.uniform(0, 2 * math.pi) - spread = random.uniform(0, self.emit_spread) - - # 简化的扩散计算 - spread_x = math.cos(angle) * spread - spread_y = math.sin(angle) * spread - - # 应用扩散到速度向量 - velocity = base_direction + Vec3(spread_x, spread_y, 0) - velocity.normalize() - velocity *= self.emit_velocity.length() - - return velocity - - def set_emit_rate(self, rate): - """ - 设置发射速率 - - Args: - rate (float): 每秒发射粒子数 - """ - self.emit_rate = rate - - def set_emit_velocity(self, velocity): - """ - 设置发射速度 - - Args: - velocity (Vec3): 发射速度向量 - """ - self.emit_velocity = velocity - - def set_emit_spread(self, spread): - """ - 设置发射扩散角度 - - Args: - spread (float): 扩散角度(弧度) - """ - self.emit_spread = spread - - def activate(self): - """ - 激活发射器 - """ - self.is_active = True - - def deactivate(self): - """ - 停用发射器 - """ +""" +流体发射器组件 +负责生成流体粒子 +""" + +from panda3d.core import Vec3, Point3 +import random +import math + +class FluidEmitter: + """ + 流体发射器类 + 负责生成流体粒子 + """ + + def __init__(self, position, fluid_world): + """ + 初始化流体发射器 + + Args: + position (Point3): 发射器位置 + fluid_world: 流体世界对象的引用 + """ + self.position = position + self.fluid_world = fluid_world + + # 发射器属性 + self.emit_rate = 10 # 每秒发射粒子数 + self.emit_velocity = Vec3(0, 0, 5) # 发射速度 + self.emit_spread = 0.1 # 发射扩散角度 + self.emit_count = 0 # 已发射粒子数 + self.is_active = True # 是否激活 + + # 粒子属性 + self.particle_mass = 1.0 # 粒子质量 + self.particle_lifespan = 5.0 # 粒子寿命 + + print(f"流体发射器创建完成: {position}") + + def update(self, dt): + """ + 更新发射器状态 + + Args: + dt (float): 时间步长 + """ + if not self.is_active: + return + + # 计算应该发射的粒子数 + particles_to_emit = int(self.emit_rate * dt) + + # 发射粒子 + for _ in range(particles_to_emit): + self._emit_particle() + + def _emit_particle(self): + """ + 发射单个粒子 + """ + # 创建粒子对象(这里简化为字典,实际实现中应该是专门的粒子类) + particle = { + 'position': self.position + Vec3( + random.uniform(-0.1, 0.1), + random.uniform(-0.1, 0.1), + random.uniform(-0.1, 0.1) + ), + 'velocity': self._get_random_velocity(), + 'mass': self.particle_mass, + 'lifespan': self.particle_lifespan, + 'age': 0.0 + } + + # 添加到流体世界 + self.fluid_world.add_fluid_particle(particle) + self.emit_count += 1 + + def _get_random_velocity(self): + """ + 获取随机发射速度 + + Returns: + Vec3: 随机速度向量 + """ + # 基础发射方向 + base_direction = self.emit_velocity.normalized() + + # 添加随机扩散 + angle = random.uniform(0, 2 * math.pi) + spread = random.uniform(0, self.emit_spread) + + # 简化的扩散计算 + spread_x = math.cos(angle) * spread + spread_y = math.sin(angle) * spread + + # 应用扩散到速度向量 + velocity = base_direction + Vec3(spread_x, spread_y, 0) + velocity.normalize() + velocity *= self.emit_velocity.length() + + return velocity + + def set_emit_rate(self, rate): + """ + 设置发射速率 + + Args: + rate (float): 每秒发射粒子数 + """ + self.emit_rate = rate + + def set_emit_velocity(self, velocity): + """ + 设置发射速度 + + Args: + velocity (Vec3): 发射速度向量 + """ + self.emit_velocity = velocity + + def set_emit_spread(self, spread): + """ + 设置发射扩散角度 + + Args: + spread (float): 扩散角度(弧度) + """ + self.emit_spread = spread + + def activate(self): + """ + 激活发射器 + """ + self.is_active = True + + def deactivate(self): + """ + 停用发射器 + """ self.is_active = False \ No newline at end of file diff --git a/plugins/user/fluid_dynamics/components/fluid_obstacle.py b/plugins/user/fluid_dynamics/components/fluid_obstacle.py index e296ad11..35df2af3 100644 --- a/plugins/user/fluid_dynamics/components/fluid_obstacle.py +++ b/plugins/user/fluid_dynamics/components/fluid_obstacle.py @@ -1,631 +1,631 @@ -""" -流体障碍物组件 -定义流体模拟中的静态或动态障碍物,支持多种形状和复杂的碰撞检测 -""" - -from panda3d.core import Vec3, Point3 -import math - -class FluidObstacle: - """ - 流体障碍物类 - 定义流体模拟中的静态或动态障碍物,支持复杂的碰撞检测和响应 - """ - - # 障碍物类型 - TYPE_BOX = "box" - TYPE_SPHERE = "sphere" - TYPE_CYLINDER = "cylinder" - TYPE_CONE = "cone" - TYPE_MESH = "mesh" # 三角网格 - - def __init__(self, position, size, obstacle_type=TYPE_BOX, mesh_data=None): - """ - 初始化流体障碍物 - - Args: - position (Point3): 障碍物位置 - size (Vec3): 障碍物尺寸 - obstacle_type (str): 障碍物类型 - mesh_data (dict): 网格数据(仅用于网格类型障碍物) - """ - self.position = position - self.size = size - self.obstacle_type = obstacle_type - self.mesh_data = mesh_data # 用于网格类型障碍物 - - # 障碍物属性 - self.is_dynamic = False # 是否为动态障碍物 - self.velocity = Vec3(0, 0, 0) # 动态障碍物速度 - self.angular_velocity = Vec3(0, 0, 0) # 角速度 - self.rotation = Vec3(0, 0, 0) # 旋转角度 - - # 物理属性 - self.friction = 0.5 # 摩擦系数 - self.restitution = 0.3 # 恢复系数 - self.density = 1000.0 # 密度 - self.mass = 1.0 # 质量 - - # 碰撞属性 - self.collision_margin = 0.05 # 碰撞边缘 - self.is_trigger = False # 是否为触发器(不产生物理反应) - - # 阻力系数 - self.drag_coefficient = 0.47 # 阻力系数(球体) - - print(f"流体障碍物创建完成: {position}, 类型: {obstacle_type}") - - def update(self, dt): - """ - 更新障碍物状态 - - Args: - dt (float): 时间步长 - """ - if self.is_dynamic: - # 更新动态障碍物位置 - self.position += self.velocity * dt - - # 更新旋转 - self.rotation += self.angular_velocity * dt - - # 应用重力(如果需要) - # self.velocity += Vec3(0, 0, -9.81) * dt - - # 应用阻尼 - self.velocity *= 0.99 - self.angular_velocity *= 0.99 - - def handle_fluid_interaction(self, particle, dt): - """ - 处理与流体粒子的交互 - - Args: - particle: 流体粒子对象 - dt (float): 时间步长 - """ - # 如果是触发器,只检测不产生物理反应 - if self.is_trigger: - return self._check_collision(particle) - - # 检查粒子是否与障碍物相交 - collision_info = self._check_collision_detailed(particle) - if collision_info['is_colliding']: - # 计算碰撞响应 - self._resolve_collision_advanced(particle, collision_info) - - # 应用摩擦力 - particle['velocity'] *= (1.0 - self.friction * dt) - - # 应用阻力 - self._apply_drag_force(particle, dt) - - return collision_info['is_colliding'] - - def _check_collision_detailed(self, particle): - """ - 详细的碰撞检测,返回碰撞信息 - - Args: - particle: 流体粒子对象 - - Returns: - dict: 碰撞信息 - """ - collision_info = { - 'is_colliding': False, - 'normal': Vec3(0, 0, 0), - 'penetration_depth': 0.0, - 'contact_point': Point3(0, 0, 0) - } - - if self.obstacle_type == self.TYPE_BOX: - return self._check_box_collision(particle) - elif self.obstacle_type == self.TYPE_SPHERE: - return self._check_sphere_collision(particle) - elif self.obstacle_type == self.TYPE_CYLINDER: - return self._check_cylinder_collision(particle) - elif self.obstacle_type == self.TYPE_CONE: - return self._check_cone_collision(particle) - elif self.obstacle_type == self.TYPE_MESH and self.mesh_data: - return self._check_mesh_collision(particle) - - return collision_info - - def _check_box_collision(self, particle): - """ - 盒状碰撞检测 - - Args: - particle: 流体粒子对象 - - Returns: - dict: 碰撞信息 - """ - collision_info = { - 'is_colliding': False, - 'normal': Vec3(0, 0, 0), - 'penetration_depth': 0.0, - 'contact_point': Point3(0, 0, 0) - } - - pos = particle['position'] - half_size = self.size * 0.5 - min_bound = self.position - half_size - max_bound = self.position + half_size - - # 检查是否在盒子内 - if (min_bound.getX() - self.collision_margin <= pos.getX() <= max_bound.getX() + self.collision_margin and - min_bound.getY() - self.collision_margin <= pos.getY() <= max_bound.getY() + self.collision_margin and - min_bound.getZ() - self.collision_margin <= pos.getZ() <= max_bound.getZ() + self.collision_margin): - - collision_info['is_colliding'] = True - - # 计算法向量和穿透深度 - # 找到最近的面 - dx_min = abs(pos.getX() - min_bound.getX()) - dx_max = abs(pos.getX() - max_bound.getX()) - dy_min = abs(pos.getY() - min_bound.getY()) - dy_max = abs(pos.getY() - max_bound.getY()) - dz_min = abs(pos.getZ() - min_bound.getZ()) - dz_max = abs(pos.getZ() - max_bound.getZ()) - - min_dist = min(dx_min, dx_max, dy_min, dy_max, dz_min, dz_max) - - if min_dist == dx_min: - collision_info['normal'] = Vec3(-1, 0, 0) - collision_info['penetration_depth'] = dx_min - collision_info['contact_point'] = Point3(min_bound.getX(), pos.getY(), pos.getZ()) - elif min_dist == dx_max: - collision_info['normal'] = Vec3(1, 0, 0) - collision_info['penetration_depth'] = dx_max - collision_info['contact_point'] = Point3(max_bound.getX(), pos.getY(), pos.getZ()) - elif min_dist == dy_min: - collision_info['normal'] = Vec3(0, -1, 0) - collision_info['penetration_depth'] = dy_min - collision_info['contact_point'] = Point3(pos.getX(), min_bound.getY(), pos.getZ()) - elif min_dist == dy_max: - collision_info['normal'] = Vec3(0, 1, 0) - collision_info['penetration_depth'] = dy_max - collision_info['contact_point'] = Point3(pos.getX(), max_bound.getY(), pos.getZ()) - elif min_dist == dz_min: - collision_info['normal'] = Vec3(0, 0, -1) - collision_info['penetration_depth'] = dz_min - collision_info['contact_point'] = Point3(pos.getX(), pos.getY(), min_bound.getZ()) - else: # dz_max - collision_info['normal'] = Vec3(0, 0, 1) - collision_info['penetration_depth'] = dz_max - collision_info['contact_point'] = Point3(pos.getX(), pos.getY(), max_bound.getZ()) - - return collision_info - - def _check_sphere_collision(self, particle): - """ - 球状碰撞检测 - - Args: - particle: 流体粒子对象 - - Returns: - dict: 碰撞信息 - """ - collision_info = { - 'is_colliding': False, - 'normal': Vec3(0, 0, 0), - 'penetration_depth': 0.0, - 'contact_point': Point3(0, 0, 0) - } - - pos = particle['position'] - radius = self.size.getX() * 0.5 - center = self.position - - # 计算粒子到球心的距离 - distance_vec = pos - center - distance = distance_vec.length() - - # 检查是否碰撞 - if distance <= radius + self.collision_margin: - collision_info['is_colliding'] = True - - # 计算法向量 - if distance > 0: - collision_info['normal'] = distance_vec.normalized() - else: - # 如果粒子在球心,使用默认法向量 - collision_info['normal'] = Vec3(0, 0, 1) - - # 计算穿透深度 - collision_info['penetration_depth'] = radius + self.collision_margin - distance - - # 计算接触点 - collision_info['contact_point'] = center + collision_info['normal'] * radius - - return collision_info - - def _check_cylinder_collision(self, particle): - """ - 圆柱碰撞检测 - - Args: - particle: 流体粒子对象 - - Returns: - dict: 碰撞信息 - """ - collision_info = { - 'is_colliding': False, - 'normal': Vec3(0, 0, 0), - 'penetration_depth': 0.0, - 'contact_point': Point3(0, 0, 0) - } - - pos = particle['position'] - radius = self.size.getX() * 0.5 - height = self.size.getZ() - center = self.position - - # 计算在XY平面上到圆柱轴的距离 - dx = pos.getX() - center.getX() - dy = pos.getY() - center.getY() - distance_xy = math.sqrt(dx*dx + dy*dy) - - # 检查Z轴范围 - min_z = center.getZ() - height/2.0 - max_z = center.getZ() + height/2.0 - in_z_range = min_z - self.collision_margin <= pos.getZ() <= max_z + self.collision_margin - - # 检查是否在圆柱范围内 - if distance_xy <= radius + self.collision_margin and in_z_range: - collision_info['is_colliding'] = True - - # 计算法向量 - if distance_xy > 0: - # 径向法向量 - radial_normal = Vec3(dx, dy, 0).normalized() - - # 检查是径向碰撞还是轴向碰撞 - if pos.getZ() < min_z or pos.getZ() > max_z: - # 轴向碰撞 - if pos.getZ() < min_z: - collision_info['normal'] = Vec3(0, 0, -1) - collision_info['contact_point'] = Point3(pos.getX(), pos.getY(), min_z) - else: - collision_info['normal'] = Vec3(0, 0, 1) - collision_info['contact_point'] = Point3(pos.getX(), pos.getY(), max_z) - collision_info['penetration_depth'] = min(abs(pos.getZ() - min_z), abs(pos.getZ() - max_z)) - else: - # 径向碰撞 - collision_info['normal'] = radial_normal - collision_info['contact_point'] = Point3( - center.getX() + radial_normal.getX() * radius, - center.getY() + radial_normal.getY() * radius, - pos.getZ() - ) - collision_info['penetration_depth'] = radius + self.collision_margin - distance_xy - else: - # 粒子在圆柱轴上,使用默认法向量 - collision_info['normal'] = Vec3(1, 0, 0) - collision_info['contact_point'] = Point3(center.getX() + radius, center.getY(), pos.getZ()) - collision_info['penetration_depth'] = radius + self.collision_margin - - return collision_info - - def _check_cone_collision(self, particle): - """ - 圆锥碰撞检测 - - Args: - particle: 流体粒子对象 - - Returns: - dict: 碰撞信息 - """ - collision_info = { - 'is_colliding': False, - 'normal': Vec3(0, 0, 0), - 'penetration_depth': 0.0, - 'contact_point': Point3(0, 0, 0) - } - - pos = particle['position'] - radius = self.size.getX() * 0.5 - height = self.size.getZ() - center = self.position - - # 相对于圆锥底部的位置 - rel_pos = pos - center - rel_pos.setZ(rel_pos.getZ() + height/2.0) # 调整坐标系使底部在z=0 - - # 计算在XY平面上到圆锥轴的距离 - distance_xy = math.sqrt(rel_pos.getX()**2 + rel_pos.getY()**2) - - # 检查是否在圆锥高度范围内 - if 0 <= rel_pos.getZ() <= height + self.collision_margin: - # 计算圆锥在当前高度的半径 - cone_radius_at_height = (rel_pos.getZ() / height) * radius - - # 检查是否在圆锥内部 - if distance_xy <= cone_radius_at_height + self.collision_margin: - collision_info['is_colliding'] = True - - # 计算法向量和接触点 - if rel_pos.getZ() <= self.collision_margin: - # 底面碰撞 - collision_info['normal'] = Vec3(0, 0, -1) - collision_info['contact_point'] = Point3(pos.getX(), pos.getY(), center.getZ() - height/2.0) - collision_info['penetration_depth'] = self.collision_margin - rel_pos.getZ() - elif distance_xy <= self.collision_margin: - # 中心轴附近,特殊处理 - collision_info['normal'] = Vec3(0, 0, 1) - collision_info['contact_point'] = Point3(center.getX(), center.getY(), pos.getZ()) - collision_info['penetration_depth'] = self.collision_margin - else: - # 侧面碰撞 - # 计算圆锥表面的法向量 - radial_dir = Vec3(rel_pos.getX(), rel_pos.getY(), 0).normalized() - surface_normal = Vec3( - radial_dir.getX(), - radial_dir.getY(), - -radius / height # 圆锥的斜率 - ).normalized() - - collision_info['normal'] = surface_normal - # 计算接触点 - contact_radius = cone_radius_at_height - collision_info['contact_point'] = Point3( - center.getX() + radial_dir.getX() * contact_radius, - center.getY() + radial_dir.getY() * contact_radius, - pos.getZ() - ) - collision_info['penetration_depth'] = cone_radius_at_height + self.collision_margin - distance_xy - - return collision_info - - def _check_mesh_collision(self, particle): - """ - 网格碰撞检测(简化实现) - - Args: - particle: 流体粒子对象 - - Returns: - dict: 碰撞信息 - """ - collision_info = { - 'is_colliding': False, - 'normal': Vec3(0, 0, 0), - 'penetration_depth': 0.0, - 'contact_point': Point3(0, 0, 0) - } - - # 对于网格碰撞检测,需要使用更复杂的算法 - # 这里使用简化的包围盒检测 - if self.mesh_data and 'vertices' in self.mesh_data: - vertices = self.mesh_data['vertices'] - if vertices: - # 计算网格的包围盒 - min_bound = Point3(float('inf'), float('inf'), float('inf')) - max_bound = Point3(float('-inf'), float('-inf'), float('-inf')) - - for vertex in vertices: - min_bound.setX(min(min_bound.getX(), vertex.getX())) - min_bound.setY(min(min_bound.getY(), vertex.getY())) - min_bound.setZ(min(min_bound.getZ(), vertex.getZ())) - max_bound.setX(max(max_bound.getX(), vertex.getX())) - max_bound.setY(max(max_bound.getY(), vertex.getY())) - max_bound.setZ(max(max_bound.getZ(), vertex.getZ())) - - # 应用位置偏移 - min_bound += self.position - max_bound += self.position - - pos = particle['position'] - - # 简单的包围盒检测 - if (min_bound.getX() - self.collision_margin <= pos.getX() <= max_bound.getX() + self.collision_margin and - min_bound.getY() - self.collision_margin <= pos.getY() <= max_bound.getY() + self.collision_margin and - min_bound.getZ() - self.collision_margin <= pos.getZ() <= max_bound.getZ() + self.collision_margin): - - collision_info['is_colliding'] = True - # 简化的法向量计算 - collision_info['normal'] = Vec3(0, 0, 1) - collision_info['contact_point'] = pos - collision_info['penetration_depth'] = 0.1 - - return collision_info - - def _resolve_collision_advanced(self, particle, collision_info): - """ - 高级碰撞响应 - - Args: - particle: 流体粒子对象 - collision_info (dict): 碰撞信息 - """ - if not collision_info['is_colliding']: - return - - # 获取碰撞信息 - normal = collision_info['normal'] - penetration_depth = collision_info['penetration_depth'] - contact_point = collision_info['contact_point'] - - # 调整粒子位置以解决穿透 - particle['position'] += normal * penetration_depth - - # 计算碰撞响应 - velocity = particle['velocity'] - velocity_normal = normal * velocity.dot(normal) - velocity_tangent = velocity - velocity_normal - - # 应用恢复系数 - new_velocity_normal = -velocity_normal * self.restitution - new_velocity_tangent = velocity_tangent * (1.0 - self.friction) - - # 更新粒子速度 - particle['velocity'] = new_velocity_normal + new_velocity_tangent - - # 如果是动态障碍物,也要更新障碍物的速度 - if self.is_dynamic: - # 简化的动量交换 - impulse = (velocity_normal - new_velocity_normal) * particle.get('mass', 1.0) - self.velocity -= impulse / self.mass - - def _apply_drag_force(self, particle, dt): - """ - 应用阻力 - - Args: - particle: 流体粒子对象 - dt (float): 时间步长 - """ - # 计算相对速度 - relative_velocity = particle['velocity'] - self.velocity - speed = relative_velocity.length() - - if speed > 0: - # 计算阻力大小 - # F = 0.5 * ρ * v² * Cd * A - fluid_density = 1000.0 # 水的密度 - cross_sectional_area = 0.01 # 简化的横截面积 - drag_force_magnitude = 0.5 * fluid_density * speed * speed * self.drag_coefficient * cross_sectional_area - - # 计算阻力方向(与速度方向相反) - drag_direction = -relative_velocity.normalized() - - # 应用阻力到粒子 - drag_force = drag_direction * drag_force_magnitude - particle['velocity'] += drag_force * dt / particle.get('mass', 1.0) - - def set_dynamic(self, is_dynamic, velocity=None, angular_velocity=None): - """ - 设置障碍物是否为动态 - - Args: - is_dynamic (bool): 是否为动态 - velocity (Vec3): 线速度 - angular_velocity (Vec3): 角速度 - """ - self.is_dynamic = is_dynamic - if velocity: - self.velocity = velocity - if angular_velocity: - self.angular_velocity = angular_velocity - - def set_physics_properties(self, friction=None, restitution=None, density=None, mass=None): - """ - 设置物理属性 - - Args: - friction (float): 摩擦系数 - restitution (float): 恢复系数 - density (float): 密度 - mass (float): 质量 - """ - if friction is not None: - self.friction = max(0.0, min(1.0, friction)) - if restitution is not None: - self.restitution = max(0.0, min(1.0, restitution)) - if density is not None: - self.density = density - if mass is not None: - self.mass = mass - - def set_collision_properties(self, margin=None, is_trigger=None): - """ - 设置碰撞属性 - - Args: - margin (float): 碰撞边缘 - is_trigger (bool): 是否为触发器 - """ - if margin is not None: - self.collision_margin = max(0.0, margin) - if is_trigger is not None: - self.is_trigger = is_trigger - - def apply_force(self, force, dt): - """ - 对动态障碍物施加力 - - Args: - force (Vec3): 力向量 - dt (float): 时间步长 - """ - if self.is_dynamic: - # F = ma => a = F/m - acceleration = force / self.mass - self.velocity += acceleration * dt - - def apply_torque(self, torque, dt): - """ - 对动态障碍物施加扭矩 - - Args: - torque (Vec3): 扭矩向量 - dt (float): 时间步长 - """ - if self.is_dynamic: - # 简化的扭矩应用 - self.angular_velocity += torque * dt / self.mass - - def get_bounding_box(self): - """ - 获取障碍物的包围盒 - - Returns: - tuple: (min_bound, max_bound) - """ - if self.obstacle_type == self.TYPE_BOX: - half_size = self.size * 0.5 - min_bound = self.position - half_size - max_bound = self.position + half_size - return (min_bound, max_bound) - elif self.obstacle_type == self.TYPE_SPHERE: - radius = self.size.getX() * 0.5 - min_bound = self.position - Vec3(radius, radius, radius) - max_bound = self.position + Vec3(radius, radius, radius) - return (min_bound, max_bound) - elif self.obstacle_type == self.TYPE_CYLINDER: - radius = self.size.getX() * 0.5 - height = self.size.getZ() * 0.5 - min_bound = self.position - Vec3(radius, radius, height) - max_bound = self.position + Vec3(radius, radius, height) - return (min_bound, max_bound) - elif self.obstacle_type == self.TYPE_CONE: - radius = self.size.getX() * 0.5 - height = self.size.getZ() * 0.5 - min_bound = self.position - Vec3(radius, radius, height) - max_bound = self.position + Vec3(radius, radius, height) - return (min_bound, max_bound) - else: - # 默认包围盒 - half_size = self.size * 0.5 - min_bound = self.position - half_size - max_bound = self.position + half_size - return (min_bound, max_bound) - - def get_volume(self): - """ - 计算障碍物体积 - - Returns: - float: 体积 - """ - if self.obstacle_type == self.TYPE_BOX: - return self.size.getX() * self.size.getY() * self.size.getZ() - elif self.obstacle_type == self.TYPE_SPHERE: - radius = self.size.getX() * 0.5 - return (4.0/3.0) * math.pi * (radius ** 3) - elif self.obstacle_type == self.TYPE_CYLINDER: - radius = self.size.getX() * 0.5 - height = self.size.getZ() - return math.pi * (radius ** 2) * height - elif self.obstacle_type == self.TYPE_CONE: - radius = self.size.getX() * 0.5 - height = self.size.getZ() - return (1.0/3.0) * math.pi * (radius ** 2) * height - else: - # 默认按盒状计算 +""" +流体障碍物组件 +定义流体模拟中的静态或动态障碍物,支持多种形状和复杂的碰撞检测 +""" + +from panda3d.core import Vec3, Point3 +import math + +class FluidObstacle: + """ + 流体障碍物类 + 定义流体模拟中的静态或动态障碍物,支持复杂的碰撞检测和响应 + """ + + # 障碍物类型 + TYPE_BOX = "box" + TYPE_SPHERE = "sphere" + TYPE_CYLINDER = "cylinder" + TYPE_CONE = "cone" + TYPE_MESH = "mesh" # 三角网格 + + def __init__(self, position, size, obstacle_type=TYPE_BOX, mesh_data=None): + """ + 初始化流体障碍物 + + Args: + position (Point3): 障碍物位置 + size (Vec3): 障碍物尺寸 + obstacle_type (str): 障碍物类型 + mesh_data (dict): 网格数据(仅用于网格类型障碍物) + """ + self.position = position + self.size = size + self.obstacle_type = obstacle_type + self.mesh_data = mesh_data # 用于网格类型障碍物 + + # 障碍物属性 + self.is_dynamic = False # 是否为动态障碍物 + self.velocity = Vec3(0, 0, 0) # 动态障碍物速度 + self.angular_velocity = Vec3(0, 0, 0) # 角速度 + self.rotation = Vec3(0, 0, 0) # 旋转角度 + + # 物理属性 + self.friction = 0.5 # 摩擦系数 + self.restitution = 0.3 # 恢复系数 + self.density = 1000.0 # 密度 + self.mass = 1.0 # 质量 + + # 碰撞属性 + self.collision_margin = 0.05 # 碰撞边缘 + self.is_trigger = False # 是否为触发器(不产生物理反应) + + # 阻力系数 + self.drag_coefficient = 0.47 # 阻力系数(球体) + + print(f"流体障碍物创建完成: {position}, 类型: {obstacle_type}") + + def update(self, dt): + """ + 更新障碍物状态 + + Args: + dt (float): 时间步长 + """ + if self.is_dynamic: + # 更新动态障碍物位置 + self.position += self.velocity * dt + + # 更新旋转 + self.rotation += self.angular_velocity * dt + + # 应用重力(如果需要) + # self.velocity += Vec3(0, 0, -9.81) * dt + + # 应用阻尼 + self.velocity *= 0.99 + self.angular_velocity *= 0.99 + + def handle_fluid_interaction(self, particle, dt): + """ + 处理与流体粒子的交互 + + Args: + particle: 流体粒子对象 + dt (float): 时间步长 + """ + # 如果是触发器,只检测不产生物理反应 + if self.is_trigger: + return self._check_collision(particle) + + # 检查粒子是否与障碍物相交 + collision_info = self._check_collision_detailed(particle) + if collision_info['is_colliding']: + # 计算碰撞响应 + self._resolve_collision_advanced(particle, collision_info) + + # 应用摩擦力 + particle['velocity'] *= (1.0 - self.friction * dt) + + # 应用阻力 + self._apply_drag_force(particle, dt) + + return collision_info['is_colliding'] + + def _check_collision_detailed(self, particle): + """ + 详细的碰撞检测,返回碰撞信息 + + Args: + particle: 流体粒子对象 + + Returns: + dict: 碰撞信息 + """ + collision_info = { + 'is_colliding': False, + 'normal': Vec3(0, 0, 0), + 'penetration_depth': 0.0, + 'contact_point': Point3(0, 0, 0) + } + + if self.obstacle_type == self.TYPE_BOX: + return self._check_box_collision(particle) + elif self.obstacle_type == self.TYPE_SPHERE: + return self._check_sphere_collision(particle) + elif self.obstacle_type == self.TYPE_CYLINDER: + return self._check_cylinder_collision(particle) + elif self.obstacle_type == self.TYPE_CONE: + return self._check_cone_collision(particle) + elif self.obstacle_type == self.TYPE_MESH and self.mesh_data: + return self._check_mesh_collision(particle) + + return collision_info + + def _check_box_collision(self, particle): + """ + 盒状碰撞检测 + + Args: + particle: 流体粒子对象 + + Returns: + dict: 碰撞信息 + """ + collision_info = { + 'is_colliding': False, + 'normal': Vec3(0, 0, 0), + 'penetration_depth': 0.0, + 'contact_point': Point3(0, 0, 0) + } + + pos = particle['position'] + half_size = self.size * 0.5 + min_bound = self.position - half_size + max_bound = self.position + half_size + + # 检查是否在盒子内 + if (min_bound.getX() - self.collision_margin <= pos.getX() <= max_bound.getX() + self.collision_margin and + min_bound.getY() - self.collision_margin <= pos.getY() <= max_bound.getY() + self.collision_margin and + min_bound.getZ() - self.collision_margin <= pos.getZ() <= max_bound.getZ() + self.collision_margin): + + collision_info['is_colliding'] = True + + # 计算法向量和穿透深度 + # 找到最近的面 + dx_min = abs(pos.getX() - min_bound.getX()) + dx_max = abs(pos.getX() - max_bound.getX()) + dy_min = abs(pos.getY() - min_bound.getY()) + dy_max = abs(pos.getY() - max_bound.getY()) + dz_min = abs(pos.getZ() - min_bound.getZ()) + dz_max = abs(pos.getZ() - max_bound.getZ()) + + min_dist = min(dx_min, dx_max, dy_min, dy_max, dz_min, dz_max) + + if min_dist == dx_min: + collision_info['normal'] = Vec3(-1, 0, 0) + collision_info['penetration_depth'] = dx_min + collision_info['contact_point'] = Point3(min_bound.getX(), pos.getY(), pos.getZ()) + elif min_dist == dx_max: + collision_info['normal'] = Vec3(1, 0, 0) + collision_info['penetration_depth'] = dx_max + collision_info['contact_point'] = Point3(max_bound.getX(), pos.getY(), pos.getZ()) + elif min_dist == dy_min: + collision_info['normal'] = Vec3(0, -1, 0) + collision_info['penetration_depth'] = dy_min + collision_info['contact_point'] = Point3(pos.getX(), min_bound.getY(), pos.getZ()) + elif min_dist == dy_max: + collision_info['normal'] = Vec3(0, 1, 0) + collision_info['penetration_depth'] = dy_max + collision_info['contact_point'] = Point3(pos.getX(), max_bound.getY(), pos.getZ()) + elif min_dist == dz_min: + collision_info['normal'] = Vec3(0, 0, -1) + collision_info['penetration_depth'] = dz_min + collision_info['contact_point'] = Point3(pos.getX(), pos.getY(), min_bound.getZ()) + else: # dz_max + collision_info['normal'] = Vec3(0, 0, 1) + collision_info['penetration_depth'] = dz_max + collision_info['contact_point'] = Point3(pos.getX(), pos.getY(), max_bound.getZ()) + + return collision_info + + def _check_sphere_collision(self, particle): + """ + 球状碰撞检测 + + Args: + particle: 流体粒子对象 + + Returns: + dict: 碰撞信息 + """ + collision_info = { + 'is_colliding': False, + 'normal': Vec3(0, 0, 0), + 'penetration_depth': 0.0, + 'contact_point': Point3(0, 0, 0) + } + + pos = particle['position'] + radius = self.size.getX() * 0.5 + center = self.position + + # 计算粒子到球心的距离 + distance_vec = pos - center + distance = distance_vec.length() + + # 检查是否碰撞 + if distance <= radius + self.collision_margin: + collision_info['is_colliding'] = True + + # 计算法向量 + if distance > 0: + collision_info['normal'] = distance_vec.normalized() + else: + # 如果粒子在球心,使用默认法向量 + collision_info['normal'] = Vec3(0, 0, 1) + + # 计算穿透深度 + collision_info['penetration_depth'] = radius + self.collision_margin - distance + + # 计算接触点 + collision_info['contact_point'] = center + collision_info['normal'] * radius + + return collision_info + + def _check_cylinder_collision(self, particle): + """ + 圆柱碰撞检测 + + Args: + particle: 流体粒子对象 + + Returns: + dict: 碰撞信息 + """ + collision_info = { + 'is_colliding': False, + 'normal': Vec3(0, 0, 0), + 'penetration_depth': 0.0, + 'contact_point': Point3(0, 0, 0) + } + + pos = particle['position'] + radius = self.size.getX() * 0.5 + height = self.size.getZ() + center = self.position + + # 计算在XY平面上到圆柱轴的距离 + dx = pos.getX() - center.getX() + dy = pos.getY() - center.getY() + distance_xy = math.sqrt(dx*dx + dy*dy) + + # 检查Z轴范围 + min_z = center.getZ() - height/2.0 + max_z = center.getZ() + height/2.0 + in_z_range = min_z - self.collision_margin <= pos.getZ() <= max_z + self.collision_margin + + # 检查是否在圆柱范围内 + if distance_xy <= radius + self.collision_margin and in_z_range: + collision_info['is_colliding'] = True + + # 计算法向量 + if distance_xy > 0: + # 径向法向量 + radial_normal = Vec3(dx, dy, 0).normalized() + + # 检查是径向碰撞还是轴向碰撞 + if pos.getZ() < min_z or pos.getZ() > max_z: + # 轴向碰撞 + if pos.getZ() < min_z: + collision_info['normal'] = Vec3(0, 0, -1) + collision_info['contact_point'] = Point3(pos.getX(), pos.getY(), min_z) + else: + collision_info['normal'] = Vec3(0, 0, 1) + collision_info['contact_point'] = Point3(pos.getX(), pos.getY(), max_z) + collision_info['penetration_depth'] = min(abs(pos.getZ() - min_z), abs(pos.getZ() - max_z)) + else: + # 径向碰撞 + collision_info['normal'] = radial_normal + collision_info['contact_point'] = Point3( + center.getX() + radial_normal.getX() * radius, + center.getY() + radial_normal.getY() * radius, + pos.getZ() + ) + collision_info['penetration_depth'] = radius + self.collision_margin - distance_xy + else: + # 粒子在圆柱轴上,使用默认法向量 + collision_info['normal'] = Vec3(1, 0, 0) + collision_info['contact_point'] = Point3(center.getX() + radius, center.getY(), pos.getZ()) + collision_info['penetration_depth'] = radius + self.collision_margin + + return collision_info + + def _check_cone_collision(self, particle): + """ + 圆锥碰撞检测 + + Args: + particle: 流体粒子对象 + + Returns: + dict: 碰撞信息 + """ + collision_info = { + 'is_colliding': False, + 'normal': Vec3(0, 0, 0), + 'penetration_depth': 0.0, + 'contact_point': Point3(0, 0, 0) + } + + pos = particle['position'] + radius = self.size.getX() * 0.5 + height = self.size.getZ() + center = self.position + + # 相对于圆锥底部的位置 + rel_pos = pos - center + rel_pos.setZ(rel_pos.getZ() + height/2.0) # 调整坐标系使底部在z=0 + + # 计算在XY平面上到圆锥轴的距离 + distance_xy = math.sqrt(rel_pos.getX()**2 + rel_pos.getY()**2) + + # 检查是否在圆锥高度范围内 + if 0 <= rel_pos.getZ() <= height + self.collision_margin: + # 计算圆锥在当前高度的半径 + cone_radius_at_height = (rel_pos.getZ() / height) * radius + + # 检查是否在圆锥内部 + if distance_xy <= cone_radius_at_height + self.collision_margin: + collision_info['is_colliding'] = True + + # 计算法向量和接触点 + if rel_pos.getZ() <= self.collision_margin: + # 底面碰撞 + collision_info['normal'] = Vec3(0, 0, -1) + collision_info['contact_point'] = Point3(pos.getX(), pos.getY(), center.getZ() - height/2.0) + collision_info['penetration_depth'] = self.collision_margin - rel_pos.getZ() + elif distance_xy <= self.collision_margin: + # 中心轴附近,特殊处理 + collision_info['normal'] = Vec3(0, 0, 1) + collision_info['contact_point'] = Point3(center.getX(), center.getY(), pos.getZ()) + collision_info['penetration_depth'] = self.collision_margin + else: + # 侧面碰撞 + # 计算圆锥表面的法向量 + radial_dir = Vec3(rel_pos.getX(), rel_pos.getY(), 0).normalized() + surface_normal = Vec3( + radial_dir.getX(), + radial_dir.getY(), + -radius / height # 圆锥的斜率 + ).normalized() + + collision_info['normal'] = surface_normal + # 计算接触点 + contact_radius = cone_radius_at_height + collision_info['contact_point'] = Point3( + center.getX() + radial_dir.getX() * contact_radius, + center.getY() + radial_dir.getY() * contact_radius, + pos.getZ() + ) + collision_info['penetration_depth'] = cone_radius_at_height + self.collision_margin - distance_xy + + return collision_info + + def _check_mesh_collision(self, particle): + """ + 网格碰撞检测(简化实现) + + Args: + particle: 流体粒子对象 + + Returns: + dict: 碰撞信息 + """ + collision_info = { + 'is_colliding': False, + 'normal': Vec3(0, 0, 0), + 'penetration_depth': 0.0, + 'contact_point': Point3(0, 0, 0) + } + + # 对于网格碰撞检测,需要使用更复杂的算法 + # 这里使用简化的包围盒检测 + if self.mesh_data and 'vertices' in self.mesh_data: + vertices = self.mesh_data['vertices'] + if vertices: + # 计算网格的包围盒 + min_bound = Point3(float('inf'), float('inf'), float('inf')) + max_bound = Point3(float('-inf'), float('-inf'), float('-inf')) + + for vertex in vertices: + min_bound.setX(min(min_bound.getX(), vertex.getX())) + min_bound.setY(min(min_bound.getY(), vertex.getY())) + min_bound.setZ(min(min_bound.getZ(), vertex.getZ())) + max_bound.setX(max(max_bound.getX(), vertex.getX())) + max_bound.setY(max(max_bound.getY(), vertex.getY())) + max_bound.setZ(max(max_bound.getZ(), vertex.getZ())) + + # 应用位置偏移 + min_bound += self.position + max_bound += self.position + + pos = particle['position'] + + # 简单的包围盒检测 + if (min_bound.getX() - self.collision_margin <= pos.getX() <= max_bound.getX() + self.collision_margin and + min_bound.getY() - self.collision_margin <= pos.getY() <= max_bound.getY() + self.collision_margin and + min_bound.getZ() - self.collision_margin <= pos.getZ() <= max_bound.getZ() + self.collision_margin): + + collision_info['is_colliding'] = True + # 简化的法向量计算 + collision_info['normal'] = Vec3(0, 0, 1) + collision_info['contact_point'] = pos + collision_info['penetration_depth'] = 0.1 + + return collision_info + + def _resolve_collision_advanced(self, particle, collision_info): + """ + 高级碰撞响应 + + Args: + particle: 流体粒子对象 + collision_info (dict): 碰撞信息 + """ + if not collision_info['is_colliding']: + return + + # 获取碰撞信息 + normal = collision_info['normal'] + penetration_depth = collision_info['penetration_depth'] + contact_point = collision_info['contact_point'] + + # 调整粒子位置以解决穿透 + particle['position'] += normal * penetration_depth + + # 计算碰撞响应 + velocity = particle['velocity'] + velocity_normal = normal * velocity.dot(normal) + velocity_tangent = velocity - velocity_normal + + # 应用恢复系数 + new_velocity_normal = -velocity_normal * self.restitution + new_velocity_tangent = velocity_tangent * (1.0 - self.friction) + + # 更新粒子速度 + particle['velocity'] = new_velocity_normal + new_velocity_tangent + + # 如果是动态障碍物,也要更新障碍物的速度 + if self.is_dynamic: + # 简化的动量交换 + impulse = (velocity_normal - new_velocity_normal) * particle.get('mass', 1.0) + self.velocity -= impulse / self.mass + + def _apply_drag_force(self, particle, dt): + """ + 应用阻力 + + Args: + particle: 流体粒子对象 + dt (float): 时间步长 + """ + # 计算相对速度 + relative_velocity = particle['velocity'] - self.velocity + speed = relative_velocity.length() + + if speed > 0: + # 计算阻力大小 + # F = 0.5 * ρ * v² * Cd * A + fluid_density = 1000.0 # 水的密度 + cross_sectional_area = 0.01 # 简化的横截面积 + drag_force_magnitude = 0.5 * fluid_density * speed * speed * self.drag_coefficient * cross_sectional_area + + # 计算阻力方向(与速度方向相反) + drag_direction = -relative_velocity.normalized() + + # 应用阻力到粒子 + drag_force = drag_direction * drag_force_magnitude + particle['velocity'] += drag_force * dt / particle.get('mass', 1.0) + + def set_dynamic(self, is_dynamic, velocity=None, angular_velocity=None): + """ + 设置障碍物是否为动态 + + Args: + is_dynamic (bool): 是否为动态 + velocity (Vec3): 线速度 + angular_velocity (Vec3): 角速度 + """ + self.is_dynamic = is_dynamic + if velocity: + self.velocity = velocity + if angular_velocity: + self.angular_velocity = angular_velocity + + def set_physics_properties(self, friction=None, restitution=None, density=None, mass=None): + """ + 设置物理属性 + + Args: + friction (float): 摩擦系数 + restitution (float): 恢复系数 + density (float): 密度 + mass (float): 质量 + """ + if friction is not None: + self.friction = max(0.0, min(1.0, friction)) + if restitution is not None: + self.restitution = max(0.0, min(1.0, restitution)) + if density is not None: + self.density = density + if mass is not None: + self.mass = mass + + def set_collision_properties(self, margin=None, is_trigger=None): + """ + 设置碰撞属性 + + Args: + margin (float): 碰撞边缘 + is_trigger (bool): 是否为触发器 + """ + if margin is not None: + self.collision_margin = max(0.0, margin) + if is_trigger is not None: + self.is_trigger = is_trigger + + def apply_force(self, force, dt): + """ + 对动态障碍物施加力 + + Args: + force (Vec3): 力向量 + dt (float): 时间步长 + """ + if self.is_dynamic: + # F = ma => a = F/m + acceleration = force / self.mass + self.velocity += acceleration * dt + + def apply_torque(self, torque, dt): + """ + 对动态障碍物施加扭矩 + + Args: + torque (Vec3): 扭矩向量 + dt (float): 时间步长 + """ + if self.is_dynamic: + # 简化的扭矩应用 + self.angular_velocity += torque * dt / self.mass + + def get_bounding_box(self): + """ + 获取障碍物的包围盒 + + Returns: + tuple: (min_bound, max_bound) + """ + if self.obstacle_type == self.TYPE_BOX: + half_size = self.size * 0.5 + min_bound = self.position - half_size + max_bound = self.position + half_size + return (min_bound, max_bound) + elif self.obstacle_type == self.TYPE_SPHERE: + radius = self.size.getX() * 0.5 + min_bound = self.position - Vec3(radius, radius, radius) + max_bound = self.position + Vec3(radius, radius, radius) + return (min_bound, max_bound) + elif self.obstacle_type == self.TYPE_CYLINDER: + radius = self.size.getX() * 0.5 + height = self.size.getZ() * 0.5 + min_bound = self.position - Vec3(radius, radius, height) + max_bound = self.position + Vec3(radius, radius, height) + return (min_bound, max_bound) + elif self.obstacle_type == self.TYPE_CONE: + radius = self.size.getX() * 0.5 + height = self.size.getZ() * 0.5 + min_bound = self.position - Vec3(radius, radius, height) + max_bound = self.position + Vec3(radius, radius, height) + return (min_bound, max_bound) + else: + # 默认包围盒 + half_size = self.size * 0.5 + min_bound = self.position - half_size + max_bound = self.position + half_size + return (min_bound, max_bound) + + def get_volume(self): + """ + 计算障碍物体积 + + Returns: + float: 体积 + """ + if self.obstacle_type == self.TYPE_BOX: + return self.size.getX() * self.size.getY() * self.size.getZ() + elif self.obstacle_type == self.TYPE_SPHERE: + radius = self.size.getX() * 0.5 + return (4.0/3.0) * math.pi * (radius ** 3) + elif self.obstacle_type == self.TYPE_CYLINDER: + radius = self.size.getX() * 0.5 + height = self.size.getZ() + return math.pi * (radius ** 2) * height + elif self.obstacle_type == self.TYPE_CONE: + radius = self.size.getX() * 0.5 + height = self.size.getZ() + return (1.0/3.0) * math.pi * (radius ** 2) * height + else: + # 默认按盒状计算 return self.size.getX() * self.size.getY() * self.size.getZ() \ No newline at end of file diff --git a/plugins/user/fluid_dynamics/config/default_config.json b/plugins/user/fluid_dynamics/config/default_config.json index c89e36b2..da237e6e 100644 --- a/plugins/user/fluid_dynamics/config/default_config.json +++ b/plugins/user/fluid_dynamics/config/default_config.json @@ -1,36 +1,36 @@ -""" -流体动力学插件配置文件 -定义插件的默认配置 -""" - -default_config = { - # 流体模拟参数 - "simulation": { - "time_step": 0.016, # 时间步长 (秒) - "max_substeps": 10, # 最大子步数 - "gravity": [0, 0, -9.81], # 重力向量 [x, y, z] - "viscosity": 0.001, # 粘度 - "density": 1000.0, # 密度 - }, - - # 性能参数 - "performance": { - "max_particles": 5000, # 最大粒子数 - "update_frequency": 60, # 更新频率 (Hz) - "multithreading": False, # 是否启用多线程 - }, - - # 可视化参数 - "visualization": { - "particle_size": 2.0, # 粒子大小 - "transparency": 0.8, # 透明度 - "color": [0.0, 0.5, 1.0, 0.8], # 颜色 [r, g, b, a] - }, - - # 物理参数 - "physics": { - "surface_tension": 0.072, # 表面张力 - "compressibility": 4.5e-10, # 压缩性 - "thermal_conductivity": 0.6, # 热导率 - } +""" +流体动力学插件配置文件 +定义插件的默认配置 +""" + +default_config = { + # 流体模拟参数 + "simulation": { + "time_step": 0.016, # 时间步长 (秒) + "max_substeps": 10, # 最大子步数 + "gravity": [0, 0, -9.81], # 重力向量 [x, y, z] + "viscosity": 0.001, # 粘度 + "density": 1000.0, # 密度 + }, + + # 性能参数 + "performance": { + "max_particles": 5000, # 最大粒子数 + "update_frequency": 60, # 更新频率 (Hz) + "multithreading": False, # 是否启用多线程 + }, + + # 可视化参数 + "visualization": { + "particle_size": 2.0, # 粒子大小 + "transparency": 0.8, # 透明度 + "color": [0.0, 0.5, 1.0, 0.8], # 颜色 [r, g, b, a] + }, + + # 物理参数 + "physics": { + "surface_tension": 0.072, # 表面张力 + "compressibility": 4.5e-10, # 压缩性 + "thermal_conductivity": 0.6, # 热导率 + } } \ No newline at end of file diff --git a/plugins/user/fluid_dynamics/core/fluid_material.py b/plugins/user/fluid_dynamics/core/fluid_material.py index 7623b675..50e6efae 100644 --- a/plugins/user/fluid_dynamics/core/fluid_material.py +++ b/plugins/user/fluid_dynamics/core/fluid_material.py @@ -1,185 +1,185 @@ -""" -流体材质系统模块 -负责流体材质的定义和管理 -""" - -from panda3d.core import Vec3, Vec4 -import time - -class FluidMaterial: - """ - 流体材质类 - 定义流体的物理属性和视觉属性 - """ - - # 预定义流体类型 - TYPE_WATER = 'water' - TYPE_OIL = 'oil' - TYPE_LAVA = 'lava' - TYPE_SLIME = 'slime' - TYPE_BLOOD = 'blood' - - def __init__(self, name, properties=None, fluid_type=TYPE_WATER): - """ - 初始化流体材质 - - Args: - name (str): 材质名称 - properties (dict): 材质属性字典 - fluid_type (str): 流体类型 - """ - self.name = name - self.fluid_type = fluid_type - self.creation_time = time.time() - - # 默认流体属性 - self.properties = { - # 物理属性 - 'density': 1000.0, # 密度 (kg/m³) - 'viscosity': 0.001, # 动力粘度 (Pa·s) - 'kinematic_viscosity': 1e-6, # 运动粘度 (m²/s) - 'surface_tension': 0.072, # 表面张力 (N/m) - 'compressibility': 4.5e-10, # 压缩系数 (Pa⁻¹) - 'bulk_modulus': 2.2e9, # 体积模量 (Pa) - 'vapor_pressure': 2338.0, # 蒸汽压 (Pa) - 'thermal_conductivity': 0.6, # 热导率 (W/(m·K)) - 'specific_heat': 4182.0, # 比热容 (J/(kg·K)) - - # 视觉属性 - 'color': Vec4(0.0, 0.5, 1.0, 0.8), # 颜色 (RGBA) - 'emissive': Vec3(0, 0, 0), # 自发光 - 'roughness': 0.1, # 粗糙度 - 'metallic': 0.0, # 金属度 - 'specular': 0.9, # 镜面反射 - 'transparency': 0.8, # 透明度 - 'refraction_index': 1.33, # 折射率 - - # 特殊属性 - 'flammability': 0.0, # 易燃性 - 'toxicity': 0.0, # 毒性 - 'temperature': 20.0, # 温度 (°C) - 'phase': 'liquid', # 相态 (liquid, gas, solid) - } - - # 应用自定义属性 - if properties: - self.properties.update(properties) - - # 根据流体类型设置默认属性 - self._set_defaults_by_type() - - print(f"流体材质创建完成: {name} ({fluid_type})") - - def _set_defaults_by_type(self): - """ - 根据流体类型设置默认属性 - """ - if self.fluid_type == self.TYPE_WATER: - self.properties.update({ - 'density': 1000.0, - 'viscosity': 0.001, - 'color': Vec4(0.0, 0.5, 1.0, 0.8), - 'refraction_index': 1.33, - 'temperature': 20.0, - }) - elif self.fluid_type == self.TYPE_OIL: - self.properties.update({ - 'density': 900.0, - 'viscosity': 0.1, - 'color': Vec4(0.8, 0.6, 0.2, 0.9), - 'refraction_index': 1.45, - 'temperature': 20.0, - }) - elif self.fluid_type == self.TYPE_LAVA: - self.properties.update({ - 'density': 3000.0, - 'viscosity': 100.0, - 'color': Vec4(1.0, 0.3, 0.0, 0.9), - 'emissive': Vec3(1.0, 0.2, 0.0), - 'temperature': 1000.0, - 'flammability': 0.9, - }) - elif self.fluid_type == self.TYPE_SLIME: - self.properties.update({ - 'density': 1200.0, - 'viscosity': 10.0, - 'color': Vec4(0.2, 0.8, 0.2, 0.9), - 'specular': 0.3, - 'temperature': 20.0, - }) - elif self.fluid_type == self.TYPE_BLOOD: - self.properties.update({ - 'density': 1060.0, - 'viscosity': 0.004, - 'color': Vec4(0.8, 0.0, 0.0, 0.9), - 'specular': 0.2, - 'temperature': 37.0, - }) - - def set_property(self, name, value): - """ - 设置材质属性 - - Args: - name (str): 属性名称 - value: 属性值 - """ - self.properties[name] = value - - def get_property(self, name, default=None): - """ - 获取材质属性 - - Args: - name (str): 属性名称 - default: 默认值 - - Returns: - 属性值 - """ - return self.properties.get(name, default) - - def get_density(self): - """ - 获取密度 - - Returns: - float: 密度值 - """ - return self.properties['density'] - - def get_viscosity(self): - """ - 获取粘度 - - Returns: - float: 粘度值 - """ - return self.properties['viscosity'] - - def get_color(self): - """ - 获取颜色 - - Returns: - Vec4: 颜色值 - """ - return self.properties['color'] - - def is_flammable(self): - """ - 检查是否易燃 - - Returns: - bool: 是否易燃 - """ - return self.properties['flammability'] > 0.5 - - def is_toxic(self): - """ - 检查是否有毒 - - Returns: - bool: 是否有毒 - """ +""" +流体材质系统模块 +负责流体材质的定义和管理 +""" + +from panda3d.core import Vec3, Vec4 +import time + +class FluidMaterial: + """ + 流体材质类 + 定义流体的物理属性和视觉属性 + """ + + # 预定义流体类型 + TYPE_WATER = 'water' + TYPE_OIL = 'oil' + TYPE_LAVA = 'lava' + TYPE_SLIME = 'slime' + TYPE_BLOOD = 'blood' + + def __init__(self, name, properties=None, fluid_type=TYPE_WATER): + """ + 初始化流体材质 + + Args: + name (str): 材质名称 + properties (dict): 材质属性字典 + fluid_type (str): 流体类型 + """ + self.name = name + self.fluid_type = fluid_type + self.creation_time = time.time() + + # 默认流体属性 + self.properties = { + # 物理属性 + 'density': 1000.0, # 密度 (kg/m³) + 'viscosity': 0.001, # 动力粘度 (Pa·s) + 'kinematic_viscosity': 1e-6, # 运动粘度 (m²/s) + 'surface_tension': 0.072, # 表面张力 (N/m) + 'compressibility': 4.5e-10, # 压缩系数 (Pa⁻¹) + 'bulk_modulus': 2.2e9, # 体积模量 (Pa) + 'vapor_pressure': 2338.0, # 蒸汽压 (Pa) + 'thermal_conductivity': 0.6, # 热导率 (W/(m·K)) + 'specific_heat': 4182.0, # 比热容 (J/(kg·K)) + + # 视觉属性 + 'color': Vec4(0.0, 0.5, 1.0, 0.8), # 颜色 (RGBA) + 'emissive': Vec3(0, 0, 0), # 自发光 + 'roughness': 0.1, # 粗糙度 + 'metallic': 0.0, # 金属度 + 'specular': 0.9, # 镜面反射 + 'transparency': 0.8, # 透明度 + 'refraction_index': 1.33, # 折射率 + + # 特殊属性 + 'flammability': 0.0, # 易燃性 + 'toxicity': 0.0, # 毒性 + 'temperature': 20.0, # 温度 (°C) + 'phase': 'liquid', # 相态 (liquid, gas, solid) + } + + # 应用自定义属性 + if properties: + self.properties.update(properties) + + # 根据流体类型设置默认属性 + self._set_defaults_by_type() + + print(f"流体材质创建完成: {name} ({fluid_type})") + + def _set_defaults_by_type(self): + """ + 根据流体类型设置默认属性 + """ + if self.fluid_type == self.TYPE_WATER: + self.properties.update({ + 'density': 1000.0, + 'viscosity': 0.001, + 'color': Vec4(0.0, 0.5, 1.0, 0.8), + 'refraction_index': 1.33, + 'temperature': 20.0, + }) + elif self.fluid_type == self.TYPE_OIL: + self.properties.update({ + 'density': 900.0, + 'viscosity': 0.1, + 'color': Vec4(0.8, 0.6, 0.2, 0.9), + 'refraction_index': 1.45, + 'temperature': 20.0, + }) + elif self.fluid_type == self.TYPE_LAVA: + self.properties.update({ + 'density': 3000.0, + 'viscosity': 100.0, + 'color': Vec4(1.0, 0.3, 0.0, 0.9), + 'emissive': Vec3(1.0, 0.2, 0.0), + 'temperature': 1000.0, + 'flammability': 0.9, + }) + elif self.fluid_type == self.TYPE_SLIME: + self.properties.update({ + 'density': 1200.0, + 'viscosity': 10.0, + 'color': Vec4(0.2, 0.8, 0.2, 0.9), + 'specular': 0.3, + 'temperature': 20.0, + }) + elif self.fluid_type == self.TYPE_BLOOD: + self.properties.update({ + 'density': 1060.0, + 'viscosity': 0.004, + 'color': Vec4(0.8, 0.0, 0.0, 0.9), + 'specular': 0.2, + 'temperature': 37.0, + }) + + def set_property(self, name, value): + """ + 设置材质属性 + + Args: + name (str): 属性名称 + value: 属性值 + """ + self.properties[name] = value + + def get_property(self, name, default=None): + """ + 获取材质属性 + + Args: + name (str): 属性名称 + default: 默认值 + + Returns: + 属性值 + """ + return self.properties.get(name, default) + + def get_density(self): + """ + 获取密度 + + Returns: + float: 密度值 + """ + return self.properties['density'] + + def get_viscosity(self): + """ + 获取粘度 + + Returns: + float: 粘度值 + """ + return self.properties['viscosity'] + + def get_color(self): + """ + 获取颜色 + + Returns: + Vec4: 颜色值 + """ + return self.properties['color'] + + def is_flammable(self): + """ + 检查是否易燃 + + Returns: + bool: 是否易燃 + """ + return self.properties['flammability'] > 0.5 + + def is_toxic(self): + """ + 检查是否有毒 + + Returns: + bool: 是否有毒 + """ return self.properties['toxicity'] > 0.5 \ No newline at end of file diff --git a/plugins/user/fluid_dynamics/core/fluid_solver.py b/plugins/user/fluid_dynamics/core/fluid_solver.py index 61c3e6ca..207e6b50 100644 --- a/plugins/user/fluid_dynamics/core/fluid_solver.py +++ b/plugins/user/fluid_dynamics/core/fluid_solver.py @@ -1,593 +1,593 @@ -""" -流体求解器模块 -负责流体动力学方程的求解,实现基于SPH的流体模拟 -""" - -from panda3d.core import Vec3, Point3 -import math -import numpy as np -from scipy.spatial import cKDTree - -class FluidSolver: - """ - 流体求解器类 - 实现基于SPH(光滑粒子流体动力学)的流体模拟 - """ - - def __init__(self, fluid_world): - """ - 初始化流体求解器 - - Args: - fluid_world: 流体世界对象的引用 - """ - self.fluid_world = fluid_world - - # SPH参数 - self.smoothing_radius = 1.0 # 光滑半径 - self.rest_density = 1000.0 # 静止密度 - self.stiffness = 1000.0 # 刚度系数 - self.viscosity = 0.001 # 粘度 - self.surface_tension = 0.01 # 表面张力 - self.gravity = Vec3(0, 0, -9.81) # 重力 - - # 数值参数 - self.time_step = 0.016 # 时间步长 - self.speed_limit = 100.0 # 速度限制 - - # 粒子数据结构 - self.particle_positions = [] # 粒子位置数组 - self.particle_velocities = [] # 粒子速度数组 - self.particle_densities = [] # 粒子密度数组 - self.particle_pressures = [] # 粒子压力数组 - - # KD树用于近邻搜索 - self.kd_tree = None - self.particle_indices = [] - - print("SPH流体求解器初始化完成") - - def set_smoothing_radius(self, radius): - """ - 设置光滑半径 - - Args: - radius (float): 光滑半径 - """ - self.smoothing_radius = radius - - def set_rest_density(self, density): - """ - 设置静止密度 - - Args: - density (float): 静止密度 - """ - self.rest_density = density - - def set_stiffness(self, stiffness): - """ - 设置刚度系数 - - Args: - stiffness (float): 刚度系数 - """ - self.stiffness = stiffness - - def set_viscosity(self, viscosity): - """ - 设置流体粘度 - - Args: - viscosity (float): 粘度值 - """ - self.viscosity = viscosity - - def set_surface_tension(self, tension): - """ - 设置表面张力 - - Args: - tension (float): 表面张力 - """ - self.surface_tension = tension - - def set_gravity(self, gravity): - """ - 设置重力 - - Args: - gravity (Vec3): 重力向量 - """ - self.gravity = gravity - - def solve(self, dt): - """ - 执行流体求解 - - Args: - dt (float): 时间步长 - """ - # 更新粒子数据结构 - self._update_particle_data() - - # 构建KD树用于近邻搜索 - self._build_kd_tree() - - # 计算粒子密度 - self._calculate_densities() - - # 计算粒子压力 - self._calculate_pressures() - - # 计算作用力 - self._calculate_forces() - - # 更新速度 - self._update_velocities(dt) - - # 处理碰撞 - self._handle_collisions() - - # 更新位置 - self._update_positions(dt) - - # 处理边界条件 - self._handle_boundary_conditions() - - def _update_particle_data(self): - """ - 更新粒子数据结构 - """ - # 清空旧数据 - self.particle_positions.clear() - self.particle_velocities.clear() - self.particle_densities.clear() - self.particle_pressures.clear() - self.particle_indices.clear() - - # 填充新数据 - for i, particle in enumerate(self.fluid_world.fluid_particles): - self.particle_positions.append(particle['position']) - self.particle_velocities.append(particle['velocity']) - self.particle_densities.append(0.0) - self.particle_pressures.append(0.0) - self.particle_indices.append(i) - - def _build_kd_tree(self): - """ - 构建KD树用于近邻搜索 - """ - if len(self.particle_positions) > 0: - # 将位置转换为numpy数组 - positions = np.array([[p.getX(), p.getY(), p.getZ()] for p in self.particle_positions]) - self.kd_tree = cKDTree(positions) - else: - self.kd_tree = None - - def _calculate_densities(self): - """ - 计算每个粒子的密度 - """ - if not self.kd_tree or len(self.particle_positions) == 0: - return - - # 对每个粒子计算密度 - for i, position in enumerate(self.particle_positions): - # 使用KD树查找近邻粒子 - neighbors = self.kd_tree.query_ball_point( - [position.getX(), position.getY(), position.getZ()], - self.smoothing_radius - ) - - # 计算密度 - density = 0.0 - for j in neighbors: - # 计算距离 - r = (position - self.particle_positions[j]).length() - if r < self.smoothing_radius: - # 使用SPH核函数计算贡献 - density += self._sph_kernel(r) - - # 乘以粒子质量(假设所有粒子质量相等) - self.particle_densities[i] = density * 1.0 # 假设粒子质量为1.0 - - def _calculate_pressures(self): - """ - 计算每个粒子的压力 - """ - # 使用状态方程计算压力 - for i, density in enumerate(self.particle_densities): - # 状态方程: P = k * (ρ - ρ₀) - pressure = self.stiffness * (density - self.rest_density) - # 确保压力非负 - self.particle_pressures[i] = max(0.0, pressure) - - def _calculate_forces(self): - """ - 计算作用在每个粒子上的力 - """ - if not self.kd_tree or len(self.particle_positions) == 0: - return - - # 对每个粒子计算力 - for i, position in enumerate(self.particle_positions): - # 初始化力 - pressure_force = Vec3(0, 0, 0) - viscosity_force = Vec3(0, 0, 0) - surface_tension_force = Vec3(0, 0, 0) - gravity_force = self.gravity * self.particle_densities[i] # F = m * g - - # 查找近邻粒子 - neighbors = self.kd_tree.query_ball_point( - [position.getX(), position.getY(), position.getZ()], - self.smoothing_radius - ) - - # 计算与近邻粒子的相互作用力 - for j in neighbors: - if i == j: # 跳过自己 - continue - - # 计算相对位置和距离 - r_vec = position - self.particle_positions[j] - r = r_vec.length() - - if 0 < r < self.smoothing_radius: - # 归一化方向向量 - r_norm = r_vec / r if r > 0 else Vec3(0, 0, 0) - - # 计算压力力 - pressure_force += self._pressure_force(i, j, r, r_norm) - - # 计算粘性力 - viscosity_force += self._viscosity_force(i, j, r) - - # 计算表面张力 - surface_tension_force += self._surface_tension_force(i, j, r, r_norm) - - # 合成总力 - total_force = pressure_force + viscosity_force + surface_tension_force + gravity_force - - # 应用力到粒子(F = m * a => a = F / m) - if self.particle_densities[i] > 0: - acceleration = total_force / self.particle_densities[i] - # 限制加速度 - if acceleration.length() > 1000.0: - acceleration.normalize() - acceleration *= 1000.0 - - # 更新粒子速度(临时存储) - self.particle_velocities[i] += acceleration * self.time_step - - def _pressure_force(self, i, j, r, r_norm): - """ - 计算压力力 - - Args: - i (int): 粒子i索引 - j (int): 粒子j索引 - r (float): 距离 - r_norm (Vec3): 归一化方向向量 - - Returns: - Vec3: 压力力 - """ - # SPH压力梯度核函数 - pressure_grad = self._sph_pressure_kernel_gradient(r) - - # 压力力计算 - force = -r_norm * pressure_grad * ( - self.particle_pressures[i] + self.particle_pressures[j] - ) / (2.0 * self.particle_densities[j]) - - return force - - def _viscosity_force(self, i, j, r): - """ - 计算粘性力(使用Monaghan粘性模型) - - Args: - i (int): 粒子i索引 - j (int): 粒子j索引 - r (float): 距离 - - Returns: - Vec3: 粘性力 - """ - # 确保不计算自己与自己的相互作用 - if i == j: - return Vec3(0, 0, 0) - - # 获取粒子速度 - velocity_i = self.particle_velocities[i] - velocity_j = self.particle_velocities[j] - - # 计算速度差 - velocity_diff = velocity_j - velocity_i - - # 使用Monaghan粘性模型 - # 核函数拉普拉斯近似 - viscosity_laplacian = self._sph_viscosity_kernel_laplacian(r) - - # 粘性力计算 - # F_viscosity = μ * (v_j - v_i) * ∇²W_ij / ρ_j - force_magnitude = self.viscosity * viscosity_laplacian / self.particle_densities[j] - force = velocity_diff * force_magnitude - - return force - - def _surface_tension_force(self, i, j, r, r_norm): - """ - 计算表面张力(使用CSF模型 - Continuum Surface Force) - - Args: - i (int): 粒子i索引 - j (int): 粒子j索引 - r (float): 距离 - r_norm (Vec3): 归一化方向向量 - - Returns: - Vec3: 表面张力 - """ - # 为避免除零错误,确保距离不为零 - if r <= 0: - return Vec3(0, 0, 0) - - # 计算表面张力需要更复杂的算法,这里使用简化的模型 - # 首先计算密度梯度来估计表面 - density_gradient = self._calculate_density_gradient(i, j, r, r_norm) - - # 使用连续表面力模型 (CSF) - # 表面张力力 = σ * κ * n * δ(Γ) - # 其中 σ 是表面张力系数,κ 是曲率,n 是表面法向量,δ(Γ) 是界面delta函数 - - # 简化的表面张力计算 - # 基于密度变化来估算是否在表面 - density_diff = abs(self.particle_densities[i] - self.particle_densities[j]) - - # 只有在密度变化显著时才应用表面张力(即在表面附近) - if density_diff > self.rest_density * 0.1: # 表面判断阈值 - # 计算表面曲率(简化计算) - curvature = self._estimate_curvature(i, j, r, r_norm) - - # 表面张力力计算 - force = r_norm * self.surface_tension * curvature - else: - # 不在表面附近,表面张力影响较小 - force = Vec3(0, 0, 0) - - return force - - def _calculate_density_gradient(self, i, j, r, r_norm): - """ - 计算密度梯度 - - Args: - i (int): 粒子i索引 - j (int): 粒子j索引 - r (float): 距离 - r_norm (Vec3): 归一化方向向量 - - Returns: - Vec3: 密度梯度 - """ - # 密度梯度 = Σ m_j * (ρ_j - ρ_i) / ρ_j * ∇W_ij - if r <= 0: - return Vec3(0, 0, 0) - - # 获取粒子i和j的密度 - rho_i = self.particle_densities[i] - rho_j = self.particle_densities[j] - - # 计算SPH核函数梯度 - grad_w = r_norm * self._sph_kernel_gradient(r) - - # 密度梯度的近似计算 - if rho_j > 0: - density_grad = (rho_j - rho_i) / rho_j * grad_w - else: - density_grad = Vec3(0, 0, 0) - - return density_grad - - def _estimate_curvature(self, i, j, r, r_norm): - """ - 估算表面曲率 - - Args: - i (int): 粒子i索引 - j (int): 粒子j索引 - r (float): 距离 - r_norm (Vec3): 归一化方向向量 - - Returns: - float: 曲率值 - """ - # 简化的曲率估算方法 - # 基于密度梯度的幅度 - density_grad = self._calculate_density_gradient(i, j, r, r_norm) - grad_magnitude = density_grad.length() - - # 基于密度梯度估算曲率 - # 这只是一个近似,实际的曲率计算更复杂 - if grad_magnitude > 0: - # 使用密度梯度幅度作为曲率的粗略估计 - # 在更精确的实现中,这将涉及计算拉普拉斯算子 - curvature = grad_magnitude / r if r > 0 else 0 - else: - curvature = 0 - - return curvature - - def _sph_kernel(self, r): - """ - SPH核函数 (Poly6 kernel) - - Args: - r (float): 距离 - - Returns: - float: 核函数值 - """ - if r >= self.smoothing_radius or r < 0: - return 0.0 - - h = self.smoothing_radius - coefficient = 315.0 / (64.0 * math.pi * h**9) - return coefficient * (h*h - r*r)**3 - - def _sph_kernel_gradient(self, r): - """ - SPH核函数梯度 (Poly6 kernel gradient) - - Args: - r (float): 距离 - - Returns: - float: 核函数梯度值 - """ - if r >= self.smoothing_radius or r <= 0: - return 0.0 - - h = self.smoothing_radius - coefficient = -945.0 / (32.0 * math.pi * h**9) - return coefficient * (h*h - r*r)**2 * r - - def _sph_pressure_kernel_gradient(self, r): - """ - SPH压力核函数梯度 (Spiky kernel gradient) - 这是用于压力力计算的核函数 - - Args: - r (float): 距离 - - Returns: - float: 核函数梯度值 - """ - if r >= self.smoothing_radius or r <= 0: - return 0.0 - - h = self.smoothing_radius - coefficient = -45.0 / (math.pi * h**6) - - # 确保核函数值在定义域内 - if r <= h: - return coefficient * (h - r)**2 - else: - return 0.0 - - def _sph_viscosity_kernel_laplacian(self, r): - """ - SPH粘性核函数拉普拉斯 (Viscosity kernel laplacian) - 用于粘性力计算的核函数 - - Args: - r (float): 距离 - - Returns: - float: 核函数拉普拉斯值 - """ - if r >= self.smoothing_radius or r < 0: - return 0.0 - - h = self.smoothing_radius - coefficient = 45.0 / (math.pi * h**6) - - # 确保核函数值在定义域内 - if r <= h: - return coefficient * (h - r) - else: - return 0.0 - - def _sph_surface_tension_kernel(self, r): - """ - SPH表面张力核函数 (Poly6 kernel) - 用于表面张力计算的核函数 - - Args: - r (float): 距离 - - Returns: - float: 核函数值 - """ - if r >= self.smoothing_radius or r < 0: - return 0.0 - - h = self.smoothing_radius - coefficient = 315.0 / (64.0 * math.pi * h**9) - - # 确保核函数值在定义域内 - if r <= h: - return coefficient * (h*h - r*r)**3 - else: - return 0.0 - - def _update_velocities(self, dt): - """ - 更新粒子速度 - - Args: - dt (float): 时间步长 - """ - # 将计算得到的速度应用到粒子 - for i, particle in enumerate(self.fluid_world.fluid_particles): - # 限制速度 - if self.particle_velocities[i].length() > self.speed_limit: - self.particle_velocities[i].normalize() - self.particle_velocities[i] *= self.speed_limit - - particle['velocity'] = self.particle_velocities[i] - - def _handle_collisions(self): - """ - 处理粒子与边界和其他粒子的碰撞 - """ - for particle in self.fluid_world.fluid_particles: - # 与边界碰撞检测 - for domain in self.fluid_world.fluid_domains: - domain.handle_particle_collision(particle) - - # 与其他粒子碰撞检测(使用KD树加速) - if self.kd_tree and len(self.particle_positions) > 0: - pos = particle['position'] - neighbors = self.kd_tree.query_ball_point( - [pos.getX(), pos.getY(), pos.getZ()], - 0.5 # 碰撞检测半径 - ) - - # 处理与近邻粒子的碰撞 - for j in neighbors: - other_particle = self.fluid_world.fluid_particles[j] - if particle != other_particle: - distance = (particle['position'] - other_particle['position']).length() - if distance < 0.2: # 碰撞距离 - # 碰撞响应 - direction = particle['position'] - other_particle['position'] - if direction.length() > 0: - direction.normalize() - particle['velocity'] += direction * 0.5 - - def _update_positions(self, dt): - """ - 更新粒子位置 - - Args: - dt (float): 时间步长 - """ - for particle in self.fluid_world.fluid_particles: - # 更新位置 - particle['position'] += particle['velocity'] * dt - - # 简单的速度衰减 - particle['velocity'] *= 0.99 - - def _handle_boundary_conditions(self): - """ - 处理边界条件 - """ - # 确保粒子不会获得过大的速度 - for particle in self.fluid_world.fluid_particles: - if particle['velocity'].length() > self.speed_limit: - particle['velocity'].normalize() +""" +流体求解器模块 +负责流体动力学方程的求解,实现基于SPH的流体模拟 +""" + +from panda3d.core import Vec3, Point3 +import math +import numpy as np +from scipy.spatial import cKDTree + +class FluidSolver: + """ + 流体求解器类 + 实现基于SPH(光滑粒子流体动力学)的流体模拟 + """ + + def __init__(self, fluid_world): + """ + 初始化流体求解器 + + Args: + fluid_world: 流体世界对象的引用 + """ + self.fluid_world = fluid_world + + # SPH参数 + self.smoothing_radius = 1.0 # 光滑半径 + self.rest_density = 1000.0 # 静止密度 + self.stiffness = 1000.0 # 刚度系数 + self.viscosity = 0.001 # 粘度 + self.surface_tension = 0.01 # 表面张力 + self.gravity = Vec3(0, 0, -9.81) # 重力 + + # 数值参数 + self.time_step = 0.016 # 时间步长 + self.speed_limit = 100.0 # 速度限制 + + # 粒子数据结构 + self.particle_positions = [] # 粒子位置数组 + self.particle_velocities = [] # 粒子速度数组 + self.particle_densities = [] # 粒子密度数组 + self.particle_pressures = [] # 粒子压力数组 + + # KD树用于近邻搜索 + self.kd_tree = None + self.particle_indices = [] + + print("SPH流体求解器初始化完成") + + def set_smoothing_radius(self, radius): + """ + 设置光滑半径 + + Args: + radius (float): 光滑半径 + """ + self.smoothing_radius = radius + + def set_rest_density(self, density): + """ + 设置静止密度 + + Args: + density (float): 静止密度 + """ + self.rest_density = density + + def set_stiffness(self, stiffness): + """ + 设置刚度系数 + + Args: + stiffness (float): 刚度系数 + """ + self.stiffness = stiffness + + def set_viscosity(self, viscosity): + """ + 设置流体粘度 + + Args: + viscosity (float): 粘度值 + """ + self.viscosity = viscosity + + def set_surface_tension(self, tension): + """ + 设置表面张力 + + Args: + tension (float): 表面张力 + """ + self.surface_tension = tension + + def set_gravity(self, gravity): + """ + 设置重力 + + Args: + gravity (Vec3): 重力向量 + """ + self.gravity = gravity + + def solve(self, dt): + """ + 执行流体求解 + + Args: + dt (float): 时间步长 + """ + # 更新粒子数据结构 + self._update_particle_data() + + # 构建KD树用于近邻搜索 + self._build_kd_tree() + + # 计算粒子密度 + self._calculate_densities() + + # 计算粒子压力 + self._calculate_pressures() + + # 计算作用力 + self._calculate_forces() + + # 更新速度 + self._update_velocities(dt) + + # 处理碰撞 + self._handle_collisions() + + # 更新位置 + self._update_positions(dt) + + # 处理边界条件 + self._handle_boundary_conditions() + + def _update_particle_data(self): + """ + 更新粒子数据结构 + """ + # 清空旧数据 + self.particle_positions.clear() + self.particle_velocities.clear() + self.particle_densities.clear() + self.particle_pressures.clear() + self.particle_indices.clear() + + # 填充新数据 + for i, particle in enumerate(self.fluid_world.fluid_particles): + self.particle_positions.append(particle['position']) + self.particle_velocities.append(particle['velocity']) + self.particle_densities.append(0.0) + self.particle_pressures.append(0.0) + self.particle_indices.append(i) + + def _build_kd_tree(self): + """ + 构建KD树用于近邻搜索 + """ + if len(self.particle_positions) > 0: + # 将位置转换为numpy数组 + positions = np.array([[p.getX(), p.getY(), p.getZ()] for p in self.particle_positions]) + self.kd_tree = cKDTree(positions) + else: + self.kd_tree = None + + def _calculate_densities(self): + """ + 计算每个粒子的密度 + """ + if not self.kd_tree or len(self.particle_positions) == 0: + return + + # 对每个粒子计算密度 + for i, position in enumerate(self.particle_positions): + # 使用KD树查找近邻粒子 + neighbors = self.kd_tree.query_ball_point( + [position.getX(), position.getY(), position.getZ()], + self.smoothing_radius + ) + + # 计算密度 + density = 0.0 + for j in neighbors: + # 计算距离 + r = (position - self.particle_positions[j]).length() + if r < self.smoothing_radius: + # 使用SPH核函数计算贡献 + density += self._sph_kernel(r) + + # 乘以粒子质量(假设所有粒子质量相等) + self.particle_densities[i] = density * 1.0 # 假设粒子质量为1.0 + + def _calculate_pressures(self): + """ + 计算每个粒子的压力 + """ + # 使用状态方程计算压力 + for i, density in enumerate(self.particle_densities): + # 状态方程: P = k * (ρ - ρ₀) + pressure = self.stiffness * (density - self.rest_density) + # 确保压力非负 + self.particle_pressures[i] = max(0.0, pressure) + + def _calculate_forces(self): + """ + 计算作用在每个粒子上的力 + """ + if not self.kd_tree or len(self.particle_positions) == 0: + return + + # 对每个粒子计算力 + for i, position in enumerate(self.particle_positions): + # 初始化力 + pressure_force = Vec3(0, 0, 0) + viscosity_force = Vec3(0, 0, 0) + surface_tension_force = Vec3(0, 0, 0) + gravity_force = self.gravity * self.particle_densities[i] # F = m * g + + # 查找近邻粒子 + neighbors = self.kd_tree.query_ball_point( + [position.getX(), position.getY(), position.getZ()], + self.smoothing_radius + ) + + # 计算与近邻粒子的相互作用力 + for j in neighbors: + if i == j: # 跳过自己 + continue + + # 计算相对位置和距离 + r_vec = position - self.particle_positions[j] + r = r_vec.length() + + if 0 < r < self.smoothing_radius: + # 归一化方向向量 + r_norm = r_vec / r if r > 0 else Vec3(0, 0, 0) + + # 计算压力力 + pressure_force += self._pressure_force(i, j, r, r_norm) + + # 计算粘性力 + viscosity_force += self._viscosity_force(i, j, r) + + # 计算表面张力 + surface_tension_force += self._surface_tension_force(i, j, r, r_norm) + + # 合成总力 + total_force = pressure_force + viscosity_force + surface_tension_force + gravity_force + + # 应用力到粒子(F = m * a => a = F / m) + if self.particle_densities[i] > 0: + acceleration = total_force / self.particle_densities[i] + # 限制加速度 + if acceleration.length() > 1000.0: + acceleration.normalize() + acceleration *= 1000.0 + + # 更新粒子速度(临时存储) + self.particle_velocities[i] += acceleration * self.time_step + + def _pressure_force(self, i, j, r, r_norm): + """ + 计算压力力 + + Args: + i (int): 粒子i索引 + j (int): 粒子j索引 + r (float): 距离 + r_norm (Vec3): 归一化方向向量 + + Returns: + Vec3: 压力力 + """ + # SPH压力梯度核函数 + pressure_grad = self._sph_pressure_kernel_gradient(r) + + # 压力力计算 + force = -r_norm * pressure_grad * ( + self.particle_pressures[i] + self.particle_pressures[j] + ) / (2.0 * self.particle_densities[j]) + + return force + + def _viscosity_force(self, i, j, r): + """ + 计算粘性力(使用Monaghan粘性模型) + + Args: + i (int): 粒子i索引 + j (int): 粒子j索引 + r (float): 距离 + + Returns: + Vec3: 粘性力 + """ + # 确保不计算自己与自己的相互作用 + if i == j: + return Vec3(0, 0, 0) + + # 获取粒子速度 + velocity_i = self.particle_velocities[i] + velocity_j = self.particle_velocities[j] + + # 计算速度差 + velocity_diff = velocity_j - velocity_i + + # 使用Monaghan粘性模型 + # 核函数拉普拉斯近似 + viscosity_laplacian = self._sph_viscosity_kernel_laplacian(r) + + # 粘性力计算 + # F_viscosity = μ * (v_j - v_i) * ∇²W_ij / ρ_j + force_magnitude = self.viscosity * viscosity_laplacian / self.particle_densities[j] + force = velocity_diff * force_magnitude + + return force + + def _surface_tension_force(self, i, j, r, r_norm): + """ + 计算表面张力(使用CSF模型 - Continuum Surface Force) + + Args: + i (int): 粒子i索引 + j (int): 粒子j索引 + r (float): 距离 + r_norm (Vec3): 归一化方向向量 + + Returns: + Vec3: 表面张力 + """ + # 为避免除零错误,确保距离不为零 + if r <= 0: + return Vec3(0, 0, 0) + + # 计算表面张力需要更复杂的算法,这里使用简化的模型 + # 首先计算密度梯度来估计表面 + density_gradient = self._calculate_density_gradient(i, j, r, r_norm) + + # 使用连续表面力模型 (CSF) + # 表面张力力 = σ * κ * n * δ(Γ) + # 其中 σ 是表面张力系数,κ 是曲率,n 是表面法向量,δ(Γ) 是界面delta函数 + + # 简化的表面张力计算 + # 基于密度变化来估算是否在表面 + density_diff = abs(self.particle_densities[i] - self.particle_densities[j]) + + # 只有在密度变化显著时才应用表面张力(即在表面附近) + if density_diff > self.rest_density * 0.1: # 表面判断阈值 + # 计算表面曲率(简化计算) + curvature = self._estimate_curvature(i, j, r, r_norm) + + # 表面张力力计算 + force = r_norm * self.surface_tension * curvature + else: + # 不在表面附近,表面张力影响较小 + force = Vec3(0, 0, 0) + + return force + + def _calculate_density_gradient(self, i, j, r, r_norm): + """ + 计算密度梯度 + + Args: + i (int): 粒子i索引 + j (int): 粒子j索引 + r (float): 距离 + r_norm (Vec3): 归一化方向向量 + + Returns: + Vec3: 密度梯度 + """ + # 密度梯度 = Σ m_j * (ρ_j - ρ_i) / ρ_j * ∇W_ij + if r <= 0: + return Vec3(0, 0, 0) + + # 获取粒子i和j的密度 + rho_i = self.particle_densities[i] + rho_j = self.particle_densities[j] + + # 计算SPH核函数梯度 + grad_w = r_norm * self._sph_kernel_gradient(r) + + # 密度梯度的近似计算 + if rho_j > 0: + density_grad = (rho_j - rho_i) / rho_j * grad_w + else: + density_grad = Vec3(0, 0, 0) + + return density_grad + + def _estimate_curvature(self, i, j, r, r_norm): + """ + 估算表面曲率 + + Args: + i (int): 粒子i索引 + j (int): 粒子j索引 + r (float): 距离 + r_norm (Vec3): 归一化方向向量 + + Returns: + float: 曲率值 + """ + # 简化的曲率估算方法 + # 基于密度梯度的幅度 + density_grad = self._calculate_density_gradient(i, j, r, r_norm) + grad_magnitude = density_grad.length() + + # 基于密度梯度估算曲率 + # 这只是一个近似,实际的曲率计算更复杂 + if grad_magnitude > 0: + # 使用密度梯度幅度作为曲率的粗略估计 + # 在更精确的实现中,这将涉及计算拉普拉斯算子 + curvature = grad_magnitude / r if r > 0 else 0 + else: + curvature = 0 + + return curvature + + def _sph_kernel(self, r): + """ + SPH核函数 (Poly6 kernel) + + Args: + r (float): 距离 + + Returns: + float: 核函数值 + """ + if r >= self.smoothing_radius or r < 0: + return 0.0 + + h = self.smoothing_radius + coefficient = 315.0 / (64.0 * math.pi * h**9) + return coefficient * (h*h - r*r)**3 + + def _sph_kernel_gradient(self, r): + """ + SPH核函数梯度 (Poly6 kernel gradient) + + Args: + r (float): 距离 + + Returns: + float: 核函数梯度值 + """ + if r >= self.smoothing_radius or r <= 0: + return 0.0 + + h = self.smoothing_radius + coefficient = -945.0 / (32.0 * math.pi * h**9) + return coefficient * (h*h - r*r)**2 * r + + def _sph_pressure_kernel_gradient(self, r): + """ + SPH压力核函数梯度 (Spiky kernel gradient) + 这是用于压力力计算的核函数 + + Args: + r (float): 距离 + + Returns: + float: 核函数梯度值 + """ + if r >= self.smoothing_radius or r <= 0: + return 0.0 + + h = self.smoothing_radius + coefficient = -45.0 / (math.pi * h**6) + + # 确保核函数值在定义域内 + if r <= h: + return coefficient * (h - r)**2 + else: + return 0.0 + + def _sph_viscosity_kernel_laplacian(self, r): + """ + SPH粘性核函数拉普拉斯 (Viscosity kernel laplacian) + 用于粘性力计算的核函数 + + Args: + r (float): 距离 + + Returns: + float: 核函数拉普拉斯值 + """ + if r >= self.smoothing_radius or r < 0: + return 0.0 + + h = self.smoothing_radius + coefficient = 45.0 / (math.pi * h**6) + + # 确保核函数值在定义域内 + if r <= h: + return coefficient * (h - r) + else: + return 0.0 + + def _sph_surface_tension_kernel(self, r): + """ + SPH表面张力核函数 (Poly6 kernel) + 用于表面张力计算的核函数 + + Args: + r (float): 距离 + + Returns: + float: 核函数值 + """ + if r >= self.smoothing_radius or r < 0: + return 0.0 + + h = self.smoothing_radius + coefficient = 315.0 / (64.0 * math.pi * h**9) + + # 确保核函数值在定义域内 + if r <= h: + return coefficient * (h*h - r*r)**3 + else: + return 0.0 + + def _update_velocities(self, dt): + """ + 更新粒子速度 + + Args: + dt (float): 时间步长 + """ + # 将计算得到的速度应用到粒子 + for i, particle in enumerate(self.fluid_world.fluid_particles): + # 限制速度 + if self.particle_velocities[i].length() > self.speed_limit: + self.particle_velocities[i].normalize() + self.particle_velocities[i] *= self.speed_limit + + particle['velocity'] = self.particle_velocities[i] + + def _handle_collisions(self): + """ + 处理粒子与边界和其他粒子的碰撞 + """ + for particle in self.fluid_world.fluid_particles: + # 与边界碰撞检测 + for domain in self.fluid_world.fluid_domains: + domain.handle_particle_collision(particle) + + # 与其他粒子碰撞检测(使用KD树加速) + if self.kd_tree and len(self.particle_positions) > 0: + pos = particle['position'] + neighbors = self.kd_tree.query_ball_point( + [pos.getX(), pos.getY(), pos.getZ()], + 0.5 # 碰撞检测半径 + ) + + # 处理与近邻粒子的碰撞 + for j in neighbors: + other_particle = self.fluid_world.fluid_particles[j] + if particle != other_particle: + distance = (particle['position'] - other_particle['position']).length() + if distance < 0.2: # 碰撞距离 + # 碰撞响应 + direction = particle['position'] - other_particle['position'] + if direction.length() > 0: + direction.normalize() + particle['velocity'] += direction * 0.5 + + def _update_positions(self, dt): + """ + 更新粒子位置 + + Args: + dt (float): 时间步长 + """ + for particle in self.fluid_world.fluid_particles: + # 更新位置 + particle['position'] += particle['velocity'] * dt + + # 简单的速度衰减 + particle['velocity'] *= 0.99 + + def _handle_boundary_conditions(self): + """ + 处理边界条件 + """ + # 确保粒子不会获得过大的速度 + for particle in self.fluid_world.fluid_particles: + if particle['velocity'].length() > self.speed_limit: + particle['velocity'].normalize() particle['velocity'] *= self.speed_limit \ No newline at end of file diff --git a/plugins/user/fluid_dynamics/core/fluid_visualizer.py b/plugins/user/fluid_dynamics/core/fluid_visualizer.py index 6df9ba69..da7857e5 100644 --- a/plugins/user/fluid_dynamics/core/fluid_visualizer.py +++ b/plugins/user/fluid_dynamics/core/fluid_visualizer.py @@ -1,774 +1,774 @@ -""" -流体可视化模块 -负责流体的可视化渲染,包括粒子系统、表面网格、流线等 -""" - -from panda3d.core import Vec3, Point3, NodePath, Vec4, LVector3f -from panda3d.core import GeomVertexFormat, GeomVertexData, Geom, GeomVertexWriter, GeomTriangles -from panda3d.core import GeomNode, TransparencyAttrib, ColorAttrib -from panda3d.core import Texture, TexGenAttrib, Shader -from panda3d.core import PerspectiveLens, PointLight, AmbientLight -import math -import numpy as np -from scipy.spatial import cKDTree - -class FluidVisualizer: - """ - 流体可视化类 - 提供多种流体可视化方式 - """ - - # 可视化模式 - VISUAL_MODE_PARTICLES = "particles" # 粒子模式 - VISUAL_MODE_SURFACE_MESH = "surface_mesh" # 表面网格模式 - VISUAL_MODE_SPLASH_MESH = "splash_mesh" # 飞溅网格模式 - VISUAL_MODE_STREAMLINES = "streamlines" # 流线模式 - VISUAL_MODE_VELOCITY_FIELD = "velocity_field" # 速度场模式 - - def __init__(self, fluid_world, world): - """ - 初始化流体可视化器 - - Args: - fluid_world: 流体世界对象的引用 - world: 主世界对象的引用 - """ - self.fluid_world = fluid_world - self.world = world # 主世界对象引用 - - # 可视化设置 - self.visual_mode = self.VISUAL_MODE_PARTICLES - self.particle_size = 2.0 - self.transparency = 0.8 - self.color = Vec4(0.0, 0.5, 1.0, 0.8) - self.smoothing_radius = 1.0 # 用于表面重建的半径 - - # 渲染节点 - self.fluid_nodes = [] # 所有流体相关节点 - self.particle_nodes = [] # 粒子节点 - self.surface_nodes = [] # 表面节点 - self.splash_nodes = [] # 飞溅节点 - self.streamline_nodes = [] # 流线节点 - self.velocity_field_nodes = [] # 速度场节点 - - # 着色器 - self.surface_shader = None - self.particle_shader = None - - # 表面重建参数 - self.surface_reconstruction_resolution = 64 # 用于网格重建的分辨率 - self.surface_isolevel = 0.5 # 等值面水平 - - # 性能优化 - self.max_visible_particles = 10000 # 最大可见粒子数 - self.update_frequency = 60 # 更新频率 - self.frame_counter = 0 - - # 材质设置 - self.roughness = 0.1 - self.metallic = 0.0 - self.specular = 0.9 - self.refraction_index = 1.33 - - # 流体材质 - self.fluid_material = None - - print("流体可视化器初始化完成") - - def set_visual_mode(self, mode): - """ - 设置可视化模式 - - Args: - mode (str): 可视化模式 - """ - self.visual_mode = mode - - def set_particle_size(self, size): - """ - 设置粒子大小 - - Args: - size (float): 粒子大小 - """ - self.particle_size = size - # 更新所有粒子节点 - for node in self.particle_nodes: - # 在Panda3D中,粒子大小通常在粒子系统中设置 - pass - - def set_transparency(self, transparency): - """ - 设置透明度 - - Args: - transparency (float): 透明度值 (0-1) - """ - self.transparency = max(0.0, min(1.0, transparency)) - # 更新所有节点的透明度 - for node in self.fluid_nodes: - node.setTransparency(TransparencyAttrib.MAlpha) - current_color = node.getColor() - node.setColor(Vec4(current_color[0], current_color[1], current_color[2], self.transparency)) - - def set_color(self, color): - """ - 设置流体颜色 - - Args: - color (Vec4): 颜色值 (RGBA) - """ - self.color = Vec4(color[0], color[1], color[2], self.transparency) - # 更新所有节点的颜色 - for node in self.fluid_nodes: - node.setColor(self.color) - - def create_particle_visualization(self, name="fluid_particles", max_particles=2000): - """ - 创建粒子可视化 - - Args: - name (str): 节点名称 - max_particles (int): 最大粒子数 - - Returns: - NodePath: 粒子节点路径 - """ - # 在Panda3D中,我们需要使用几何体来表示粒子 - # 创建顶点数据格式 - format = GeomVertexFormat.getV3c4() # 位置和颜色 - - # 为每个粒子创建一个四边形面片(billboard) - vdata = GeomVertexData(name, format, Geom.UHDynamic) # 动态更新 - vdata.setNumRows(max_particles * 4) # 每个粒子4个顶点 - - vertex = GeomVertexWriter(vdata, 'vertex') - color = GeomVertexWriter(vdata, 'color') - - # 为所有粒子添加初始顶点(简化:只添加一个粒子的顶点) - for i in range(max_particles): - # 每个粒子用一个点表示(实际上需要创建一个面片,但这里简化处理) - # 真实实现中,我们需要为每个粒子创建一个面向摄像机的四边形 - pass - - # 创建几何体 - geom = Geom(vdata) - - # 创建几何节点 - geom_node = GeomNode(name) - geom_node.addGeom(geom) - - # 创建节点路径 - node_path = self.world.render.attachNewNode(geom_node) - - # 设置渲染状态 - node_path.setTransparency(TransparencyAttrib.MAlpha) - node_path.setColor(self.color) - - # 添加到管理系统 - self.particle_nodes.append(node_path) - self.fluid_nodes.append(node_path) - - return node_path - - def create_surface_mesh(self, name="fluid_surface", resolution=32): - """ - 创建流体表面网格(使用Marching Cubes算法) - - Args: - name (str): 网格名称 - resolution (int): 网格分辨率 - - Returns: - NodePath: 网格节点路径 - """ - # 创建体素网格用于表面重建 - self.surface_reconstruction_resolution = resolution - - # 创建顶点数据格式 - format = GeomVertexFormat.getV3n3c4() # 位置、法线、颜色 - vdata = GeomVertexData(name, format, Geom.UHDynamic) - - # 预分配顶点(最坏情况下的顶点数) - max_vertices = resolution * resolution * resolution * 5 # 估算值 - vdata.setNumRows(max_vertices) - - # 创建几何体 - geom = Geom(vdata) - - # 创建几何节点 - geom_node = GeomNode(name) - geom_node.addGeom(geom) - - # 创建节点路径 - node_path = self.world.render.attachNewNode(geom_node) - - # 设置渲染状态 - node_path.setTransparency(TransparencyAttrib.MAlpha) - node_path.setColor(self.color) - - # 添加材质属性 - node_path.setTwoSided(True) # 双面渲染 - - # 添加到管理系统 - self.surface_nodes.append(node_path) - self.fluid_nodes.append(node_path) - - return node_path - - def create_splash_mesh(self, name="fluid_splash"): - """ - 创建流体飞溅效果网格 - - Args: - name (str): 网格名称 - - Returns: - NodePath: 网格节点路径 - """ - # 专门用于渲染飞溅和波纹效果的网格 - format = GeomVertexFormat.getV3n3c4t2() # 位置、法线、颜色、纹理坐标 - vdata = GeomVertexData(name, format, Geom.UHDynamic) - - # 对于飞溅效果,我们可以使用较少的顶点 - vdata.setNumRows(1000) # 预分配1000个顶点 - - # 创建几何体 - geom = Geom(vdata) - - # 创建几何节点 - geom_node = GeomNode(name) - geom_node.addGeom(geom) - - # 创建节点路径 - node_path = self.world.render.attachNewNode(geom_node) - - # 设置渲染状态 - node_path.setTransparency(TransparencyAttrib.MAlpha) - node_path.setColor(self.color) - - # 添加到管理系统 - self.splash_nodes.append(node_path) - self.fluid_nodes.append(node_path) - - return node_path - - def create_streamlines(self, name="fluid_streamlines", num_lines=50): - """ - 创建流线可视化 - - Args: - name (str): 流线名称 - num_lines (int): 流线数量 - - Returns: - NodePath: 流线节点路径 - """ - # 流线是显示流体流动方向的线 - format = GeomVertexFormat.getV3c4() # 位置、颜色 - vdata = GeomVertexData(name, format, Geom.UHDynamic) - - # 每条流线最多100个点 - max_vertices = num_lines * 100 - vdata.setNumRows(max_vertices) - - # 创建几何体 - geom = Geom(vdata) - - # 创建几何节点 - geom_node = GeomNode(name) - geom_node.addGeom(geom) - - # 创建节点路径 - node_path = self.world.render.attachNewNode(geom_node) - - # 设置渲染状态 - node_path.setTransparency(TransparencyAttrib.MAlpha) - node_path.setColor(Vec4(1.0, 1.0, 0.0, 0.8)) # 黄色流线 - - # 添加到管理系统 - self.streamline_nodes.append(node_path) - self.fluid_nodes.append(node_path) - - return node_path - - def create_velocity_field(self, name="fluid_velocity_field", grid_density=10): - """ - 创建速度场可视化 - - Args: - name (str): 速度场名称 - grid_density (int): 网格密度 - - Returns: - NodePath: 速度场节点路径 - """ - # 速度场显示流体中各点的速度方向和大小 - format = GeomVertexFormat.getV3c4() # 位置、颜色 - vdata = GeomVertexData(name, format, Geom.UHDynamic) - - # 3D网格,每条线2个点 - max_vertices = grid_density * grid_density * grid_density * 2 - vdata.setNumRows(max_vertices) - - # 创建几何体 - geom = Geom(vdata) - - # 创建几何节点 - geom_node = GeomNode(name) - geom_node.addGeom(geom) - - # 创建节点路径 - node_path = self.world.render.attachNewNode(geom_node) - - # 设置渲染状态 - node_path.setTransparency(TransparencyAttrib.MAlpha) - node_path.setColor(Vec4(1.0, 0.0, 0.0, 0.8)) # 红色速度向量 - - # 添加到管理系统 - self.velocity_field_nodes.append(node_path) - self.fluid_nodes.append(node_path) - - return node_path - - def update_visualization(self, dt): - """ - 更新流体可视化 - - Args: - dt (float): 时间步长 - """ - self.frame_counter += 1 - - # 根据可视化模式更新相应的可视化组件 - if self.visual_mode == self.VISUAL_MODE_PARTICLES: - self._update_particle_visualization() - elif self.visual_mode == self.VISUAL_MODE_SURFACE_MESH: - self._update_surface_mesh() - elif self.visual_mode == self.VISUAL_MODE_SPLASH_MESH: - self._update_splash_mesh() - elif self.visual_mode == self.VISUAL_MODE_STREAMLINES: - self._update_streamlines() - elif self.visual_mode == self.VISUAL_MODE_VELOCITY_FIELD: - self._update_velocity_field() - else: - # 默认更新粒子可视化 - self._update_particle_visualization() - - def _update_particle_visualization(self): - """ - 更新粒子可视化 - """ - if not self.fluid_world.fluid_particles or not self.particle_nodes: - return - - # 限制更新的粒子数量以提高性能 - particles_to_update = min(len(self.fluid_world.fluid_particles), - self.max_visible_particles) - - for node in self.particle_nodes: - # 获取几何体 - geom = node.node().getGeom(0) - vdata = geom.modifyVertexData() - - # 获取顶点写入器 - vertex = GeomVertexWriter(vdata, 'vertex') - - # 更新粒子位置(这里简化处理,实际需要为每个粒子创建面片) - for i in range(min(particles_to_update, vdata.getNumRows() // 4)): - particle = self.fluid_world.fluid_particles[i] - pos = particle['position'] - - # 为粒子创建一个面向摄像机的四边形(简化为单个点) - # 在真实实现中,这里需要创建billboard四边形 - vertex.setData3f(pos) - - def _update_surface_mesh(self): - """ - 更新表面网格(使用Marching Cubes算法) - """ - if not self.fluid_world.fluid_particles or not self.surface_nodes: - return - - # 创建密度场 - resolution = self.surface_reconstruction_resolution - density_grid = self._create_density_grid(resolution) - - # 使用Marching Cubes算法生成网格 - vertices, triangles = self._marching_cubes(density_grid, self.surface_isolevel) - - # 更新几何体 - if vertices and triangles and self.surface_nodes: - node = self.surface_nodes[0] # 使用第一个表面节点 - geom = node.node().getGeom(0) - vdata = geom.modifyVertexData() - - # 确保有足够的行数 - if vdata.getNumRows() < len(vertices) * 3: # 位置、法线、颜色 - vdata.setNumRows(len(vertices) * 3) - - vertex_writer = GeomVertexWriter(vdata, 'vertex') - normal_writer = GeomVertexWriter(vdata, 'normal') - color_writer = GeomVertexWriter(vdata, 'color') - - # 写入顶点数据 - for v in vertices: - vertex_writer.setData3f(v[0], v[1], v[2]) - # 这里应该计算法线,简化为(0,0,1) - normal_writer.setData3f(0, 0, 1) - color_writer.setData4f(self.color) - - # 更新图元 - prim = GeomTriangles(Geom.UHDynamic) - for t in triangles: - prim.addVertices(t[0], t[1], t[2]) - prim.closePrimitive() - - # 替换旧的图元 - geom.removePrimitive(0) - geom.addPrimitive(prim) - - def _create_density_grid(self, resolution): - """ - 创建流体密度网格用于表面重建 - - Args: - resolution (int): 网格分辨率 - - Returns: - numpy.ndarray: 3D密度网格 - """ - if not self.fluid_world.fluid_particles: - return np.zeros((resolution, resolution, resolution)) - - # 确定网格边界 - min_bound = Point3(float('inf'), float('inf'), float('inf')) - max_bound = Point3(float('-inf'), float('-inf'), float('-inf')) - - for particle in self.fluid_world.fluid_particles: - pos = particle['position'] - min_bound.setX(min(min_bound.getX(), pos.getX())) - min_bound.setY(min(min_bound.getY(), pos.getY())) - min_bound.setZ(min(min_bound.getZ(), pos.getZ())) - max_bound.setX(max(max_bound.getX(), pos.getX())) - max_bound.setY(max(max_bound.getY(), pos.getY())) - max_bound.setZ(max(max_bound.getZ(), pos.getZ())) - - # 添加边界填充 - padding = self.smoothing_radius - min_bound -= Vec3(padding, padding, padding) - max_bound += Vec3(padding, padding, padding) - - # 创建密度网格 - grid = np.zeros((resolution, resolution, resolution), dtype=np.float32) - - # 计算网格步长 - step_x = (max_bound.getX() - min_bound.getX()) / resolution - step_y = (max_bound.getY() - min_bound.getY()) / resolution - step_z = (max_bound.getZ() - min_bound.getZ()) / resolution - - # 对每个粒子贡献到网格 - for particle in self.fluid_world.fluid_particles: - pos = particle['position'] - - # 计算粒子在网格中的位置 - grid_x = int((pos.getX() - min_bound.getX()) / step_x) - grid_y = int((pos.getY() - min_bound.getY()) / step_y) - grid_z = int((pos.getZ() - min_bound.getZ()) / step_z) - - # 确保索引在范围内 - if 0 <= grid_x < resolution and 0 <= grid_y < resolution and 0 <= grid_z < resolution: - # 使用SPH核函数计算密度贡献 - for dx in range(-2, 3): - for dy in range(-2, 3): - for dz in range(-2, 3): - nx, ny, nz = grid_x + dx, grid_y + dy, grid_z + dz - if 0 <= nx < resolution and 0 <= ny < resolution and 0 <= nz < resolution: - # 计算网格点的实际位置 - gx = min_bound.getX() + nx * step_x - gy = min_bound.getY() + ny * step_y - gz = min_bound.getZ() + nz * step_z - - # 计算距离 - dist = math.sqrt((gx - pos.getX())**2 + - (gy - pos.getY())**2 + - (gz - pos.getZ())**2) - - if dist < self.smoothing_radius: - # 使用核函数 - kernel_value = self._sph_kernel(dist) - grid[nz, ny, nx] += kernel_value - - return grid - - def _sph_kernel(self, r): - """ - SPH核函数 (Poly6 kernel) - - Args: - r (float): 距离 - - Returns: - float: 核函数值 - """ - if r >= self.smoothing_radius: - return 0.0 - - h = self.smoothing_radius - coefficient = 315.0 / (64.0 * math.pi * h**9) - return coefficient * (h*h - r*r)**3 - - def _marching_cubes(self, density_grid, isolevel): - """ - Marching Cubes算法生成等值面网格 - - Args: - density_grid (numpy.ndarray): 3D密度网格 - isolevel (float): 等值面水平 - - Returns: - tuple: (vertices, triangles) - """ - # 这是一个简化的Marching Cubes实现 - # 实际应用中可能需要更完整的实现 - vertices = [] - triangles = [] - - shape = density_grid.shape - for z in range(shape[0] - 1): - for y in range(shape[1] - 1): - for x in range(shape[2] - 1): - # 获取立方体8个角点的密度值 - cube = [ - density_grid[z, y, x], - density_grid[z, y, x+1], - density_grid[z, y+1, x+1], - density_grid[z, y+1, x], - density_grid[z+1, y, x], - density_grid[z+1, y, x+1], - density_grid[z+1, y+1, x+1], - density_grid[z+1, y+1, x] - ] - - # 检查是否与等值面相交 - bits = 0 - for i, value in enumerate(cube): - if value > isolevel: - bits |= (1 << i) - - # 如果相交,生成三角形(这里使用预定义的配置表) - # 为了简化,我们只生成一个示例三角形 - if bits != 0 and bits != 0xFF: - # 在实际实现中,这里会根据bits值查找预定义的三角形配置 - # 并在边的交点处插入顶点 - center_x = x + 0.5 - center_y = y + 0.5 - center_z = z + 0.5 - vertices.append([center_x, center_y, center_z]) - triangles.append([len(vertices)-1, len(vertices)-1, len(vertices)-1]) - - return vertices, triangles - - def _update_splash_mesh(self): - """ - 更新飞溅网格 - """ - # 飞溅效果通常在高流速或碰撞区域显示 - if not self.fluid_world.fluid_particles or not self.splash_nodes: - return - - # 检测高流速粒子(可能产生飞溅) - splash_particles = [] - for particle in self.fluid_world.fluid_particles: - if particle['velocity'].length() > 5.0: # 速度阈值 - splash_particles.append(particle) - - # 更新飞溅几何体 - if splash_particles and self.splash_nodes: - node = self.splash_nodes[0] - geom = node.node().getGeom(0) - vdata = geom.modifyVertexData() - - if vdata.getNumRows() < len(splash_particles) * 4: # 预分配 - vdata.setNumRows(len(splash_particles) * 4) - - vertex_writer = GeomVertexWriter(vdata, 'vertex') - color_writer = GeomVertexWriter(vdata, 'color') - - for particle in splash_particles: - pos = particle['position'] - # 创建一个小的四边形代表飞溅 - size = 0.2 - vertex_writer.setData3f(pos.getX() - size, pos.getY() - size, pos.getZ()) - vertex_writer.setData3f(pos.getX() + size, pos.getY() - size, pos.getZ()) - vertex_writer.setData3f(pos.getX() + size, pos.getY() + size, pos.getZ()) - vertex_writer.setData3f(pos.getX() - size, pos.getY() + size, pos.getZ()) - - # 飞溅使用更亮的颜色 - splash_color = Vec4(1.0, 1.0, 1.0, self.transparency) - for _ in range(4): - color_writer.setData4f(splash_color) - - def _update_streamlines(self): - """ - 更新流线 - """ - if not self.fluid_world.fluid_particles or not self.streamline_nodes: - return - - # 简化的流线更新 - # 在实际实现中,这将追踪流体中的轨迹线 - if self.streamline_nodes: - node = self.streamline_nodes[0] - geom = node.node().getGeom(0) - vdata = geom.modifyVertexData() - - # 创建一些示例流线 - if len(self.fluid_world.fluid_particles) > 0: - # 使用前几个粒子作为流线起点 - for i in range(min(10, len(self.fluid_world.fluid_particles))): - particle = self.fluid_world.fluid_particles[i] - start_pos = particle['position'] - end_pos = start_pos + particle['velocity'] * 0.1 # 简化的方向线 - - vertex_writer = GeomVertexWriter(vdata, 'vertex') - color_writer = GeomVertexWriter(vdata, 'color') - - vertex_writer.setData3f(start_pos) - vertex_writer.setData3f(end_pos) - - color_writer.setData4f(Vec4(1.0, 1.0, 0.0, self.transparency)) - color_writer.setData4f(Vec4(1.0, 1.0, 0.0, self.transparency)) - - def _update_velocity_field(self): - """ - 更新速度场 - """ - if not self.fluid_world.fluid_particles or not self.velocity_field_nodes: - return - - # 创建规则网格上的速度向量 - if self.velocity_field_nodes: - node = self.velocity_field_nodes[0] - geom = node.node().getGeom(0) - vdata = geom.modifyVertexData() - - # 简化的速度场显示 - if len(self.fluid_world.fluid_particles) > 0: - # 显示部分粒子的速度向量 - step = max(1, len(self.fluid_world.fluid_particles) // 20) # 限制显示数量 - - vertex_writer = GeomVertexWriter(vdata, 'vertex') - color_writer = GeomVertexWriter(vdata, 'color') - - for i in range(0, len(self.fluid_world.fluid_particles), step): - if i >= len(self.fluid_world.fluid_particles): - break - - particle = self.fluid_world.fluid_particles[i] - pos = particle['position'] - vel = particle['velocity'] - - # 标准化速度向量以便可视化 - normalized_vel = vel.normalized() * 0.5 # 缩放以便可视化 - - # 创建速度向量线段 - start_pos = pos - end_pos = pos + normalized_vel - - vertex_writer.setData3f(start_pos) - vertex_writer.setData3f(end_pos) - - # 根据速度大小设置颜色 - speed = vel.length() - speed_color = min(1.0, speed / 10.0) # 假设最大速度为10 - color = Vec4(speed_color, 0.0, 1.0 - speed_color, self.transparency) - - color_writer.setData4f(color) - color_writer.setData4f(color) - - def set_material_properties(self, roughness=None, metallic=None, specular=None, refraction_index=None): - """ - 设置流体材质属性 - - Args: - roughness (float): 粗糙度 - metallic (float): 金属度 - specular (float): 镜面反射 - refraction_index (float): 折射率 - """ - if roughness is not None: - self.roughness = max(0.0, min(1.0, roughness)) - if metallic is not None: - self.metallic = max(0.0, min(1.0, metallic)) - if specular is not None: - self.specular = max(0.0, min(1.0, specular)) - if refraction_index is not None: - self.refraction_index = max(1.0, refraction_index) - - # 应用到所有节点 - for node in self.fluid_nodes: - # 在Panda3D中,这些属性通常通过着色器或材质节点应用 - pass - - def set_surface_reconstruction_params(self, resolution=None, isolevel=None): - """ - 设置表面重建参数 - - Args: - resolution (int): 重建分辨率 - isolevel (float): 等值面水平 - """ - if resolution is not None: - self.surface_reconstruction_resolution = resolution - if isolevel is not None: - self.surface_isolevel = isolevel - - def set_performance_params(self, max_visible_particles=None, update_frequency=None): - """ - 设置性能参数 - - Args: - max_visible_particles (int): 最大可见粒子数 - update_frequency (int): 更新频率 - """ - if max_visible_particles is not None: - self.max_visible_particles = max_visible_particles - if update_frequency is not None: - self.update_frequency = update_frequency - - def get_visualization_stats(self): - """ - 获取可视化统计信息 - - Returns: - dict: 统计信息 - """ - stats = { - 'active_nodes': len(self.fluid_nodes), - 'particle_nodes': len(self.particle_nodes), - 'surface_nodes': len(self.surface_nodes), - 'splash_nodes': len(self.splash_nodes), - 'streamline_nodes': len(self.streamline_nodes), - 'velocity_field_nodes': len(self.velocity_field_nodes), - 'visual_mode': self.visual_mode, - 'particle_count': len(self.fluid_world.fluid_particles) if self.fluid_world else 0 - } - - return stats - - def cleanup(self): - """ - 清理可视化资源 - """ - # 清理所有节点 - for node_list in [self.fluid_nodes, self.particle_nodes, self.surface_nodes, - self.splash_nodes, self.streamline_nodes, self.velocity_field_nodes]: - for node in node_list: - node.removeNode() - node_list.clear() - - # 清空总节点列表 - self.fluid_nodes.clear() - +""" +流体可视化模块 +负责流体的可视化渲染,包括粒子系统、表面网格、流线等 +""" + +from panda3d.core import Vec3, Point3, NodePath, Vec4, LVector3f +from panda3d.core import GeomVertexFormat, GeomVertexData, Geom, GeomVertexWriter, GeomTriangles +from panda3d.core import GeomNode, TransparencyAttrib, ColorAttrib +from panda3d.core import Texture, TexGenAttrib, Shader +from panda3d.core import PerspectiveLens, PointLight, AmbientLight +import math +import numpy as np +from scipy.spatial import cKDTree + +class FluidVisualizer: + """ + 流体可视化类 + 提供多种流体可视化方式 + """ + + # 可视化模式 + VISUAL_MODE_PARTICLES = "particles" # 粒子模式 + VISUAL_MODE_SURFACE_MESH = "surface_mesh" # 表面网格模式 + VISUAL_MODE_SPLASH_MESH = "splash_mesh" # 飞溅网格模式 + VISUAL_MODE_STREAMLINES = "streamlines" # 流线模式 + VISUAL_MODE_VELOCITY_FIELD = "velocity_field" # 速度场模式 + + def __init__(self, fluid_world, world): + """ + 初始化流体可视化器 + + Args: + fluid_world: 流体世界对象的引用 + world: 主世界对象的引用 + """ + self.fluid_world = fluid_world + self.world = world # 主世界对象引用 + + # 可视化设置 + self.visual_mode = self.VISUAL_MODE_PARTICLES + self.particle_size = 2.0 + self.transparency = 0.8 + self.color = Vec4(0.0, 0.5, 1.0, 0.8) + self.smoothing_radius = 1.0 # 用于表面重建的半径 + + # 渲染节点 + self.fluid_nodes = [] # 所有流体相关节点 + self.particle_nodes = [] # 粒子节点 + self.surface_nodes = [] # 表面节点 + self.splash_nodes = [] # 飞溅节点 + self.streamline_nodes = [] # 流线节点 + self.velocity_field_nodes = [] # 速度场节点 + + # 着色器 + self.surface_shader = None + self.particle_shader = None + + # 表面重建参数 + self.surface_reconstruction_resolution = 64 # 用于网格重建的分辨率 + self.surface_isolevel = 0.5 # 等值面水平 + + # 性能优化 + self.max_visible_particles = 10000 # 最大可见粒子数 + self.update_frequency = 60 # 更新频率 + self.frame_counter = 0 + + # 材质设置 + self.roughness = 0.1 + self.metallic = 0.0 + self.specular = 0.9 + self.refraction_index = 1.33 + + # 流体材质 + self.fluid_material = None + + print("流体可视化器初始化完成") + + def set_visual_mode(self, mode): + """ + 设置可视化模式 + + Args: + mode (str): 可视化模式 + """ + self.visual_mode = mode + + def set_particle_size(self, size): + """ + 设置粒子大小 + + Args: + size (float): 粒子大小 + """ + self.particle_size = size + # 更新所有粒子节点 + for node in self.particle_nodes: + # 在Panda3D中,粒子大小通常在粒子系统中设置 + pass + + def set_transparency(self, transparency): + """ + 设置透明度 + + Args: + transparency (float): 透明度值 (0-1) + """ + self.transparency = max(0.0, min(1.0, transparency)) + # 更新所有节点的透明度 + for node in self.fluid_nodes: + node.setTransparency(TransparencyAttrib.MAlpha) + current_color = node.getColor() + node.setColor(Vec4(current_color[0], current_color[1], current_color[2], self.transparency)) + + def set_color(self, color): + """ + 设置流体颜色 + + Args: + color (Vec4): 颜色值 (RGBA) + """ + self.color = Vec4(color[0], color[1], color[2], self.transparency) + # 更新所有节点的颜色 + for node in self.fluid_nodes: + node.setColor(self.color) + + def create_particle_visualization(self, name="fluid_particles", max_particles=2000): + """ + 创建粒子可视化 + + Args: + name (str): 节点名称 + max_particles (int): 最大粒子数 + + Returns: + NodePath: 粒子节点路径 + """ + # 在Panda3D中,我们需要使用几何体来表示粒子 + # 创建顶点数据格式 + format = GeomVertexFormat.getV3c4() # 位置和颜色 + + # 为每个粒子创建一个四边形面片(billboard) + vdata = GeomVertexData(name, format, Geom.UHDynamic) # 动态更新 + vdata.setNumRows(max_particles * 4) # 每个粒子4个顶点 + + vertex = GeomVertexWriter(vdata, 'vertex') + color = GeomVertexWriter(vdata, 'color') + + # 为所有粒子添加初始顶点(简化:只添加一个粒子的顶点) + for i in range(max_particles): + # 每个粒子用一个点表示(实际上需要创建一个面片,但这里简化处理) + # 真实实现中,我们需要为每个粒子创建一个面向摄像机的四边形 + pass + + # 创建几何体 + geom = Geom(vdata) + + # 创建几何节点 + geom_node = GeomNode(name) + geom_node.addGeom(geom) + + # 创建节点路径 + node_path = self.world.render.attachNewNode(geom_node) + + # 设置渲染状态 + node_path.setTransparency(TransparencyAttrib.MAlpha) + node_path.setColor(self.color) + + # 添加到管理系统 + self.particle_nodes.append(node_path) + self.fluid_nodes.append(node_path) + + return node_path + + def create_surface_mesh(self, name="fluid_surface", resolution=32): + """ + 创建流体表面网格(使用Marching Cubes算法) + + Args: + name (str): 网格名称 + resolution (int): 网格分辨率 + + Returns: + NodePath: 网格节点路径 + """ + # 创建体素网格用于表面重建 + self.surface_reconstruction_resolution = resolution + + # 创建顶点数据格式 + format = GeomVertexFormat.getV3n3c4() # 位置、法线、颜色 + vdata = GeomVertexData(name, format, Geom.UHDynamic) + + # 预分配顶点(最坏情况下的顶点数) + max_vertices = resolution * resolution * resolution * 5 # 估算值 + vdata.setNumRows(max_vertices) + + # 创建几何体 + geom = Geom(vdata) + + # 创建几何节点 + geom_node = GeomNode(name) + geom_node.addGeom(geom) + + # 创建节点路径 + node_path = self.world.render.attachNewNode(geom_node) + + # 设置渲染状态 + node_path.setTransparency(TransparencyAttrib.MAlpha) + node_path.setColor(self.color) + + # 添加材质属性 + node_path.setTwoSided(True) # 双面渲染 + + # 添加到管理系统 + self.surface_nodes.append(node_path) + self.fluid_nodes.append(node_path) + + return node_path + + def create_splash_mesh(self, name="fluid_splash"): + """ + 创建流体飞溅效果网格 + + Args: + name (str): 网格名称 + + Returns: + NodePath: 网格节点路径 + """ + # 专门用于渲染飞溅和波纹效果的网格 + format = GeomVertexFormat.getV3n3c4t2() # 位置、法线、颜色、纹理坐标 + vdata = GeomVertexData(name, format, Geom.UHDynamic) + + # 对于飞溅效果,我们可以使用较少的顶点 + vdata.setNumRows(1000) # 预分配1000个顶点 + + # 创建几何体 + geom = Geom(vdata) + + # 创建几何节点 + geom_node = GeomNode(name) + geom_node.addGeom(geom) + + # 创建节点路径 + node_path = self.world.render.attachNewNode(geom_node) + + # 设置渲染状态 + node_path.setTransparency(TransparencyAttrib.MAlpha) + node_path.setColor(self.color) + + # 添加到管理系统 + self.splash_nodes.append(node_path) + self.fluid_nodes.append(node_path) + + return node_path + + def create_streamlines(self, name="fluid_streamlines", num_lines=50): + """ + 创建流线可视化 + + Args: + name (str): 流线名称 + num_lines (int): 流线数量 + + Returns: + NodePath: 流线节点路径 + """ + # 流线是显示流体流动方向的线 + format = GeomVertexFormat.getV3c4() # 位置、颜色 + vdata = GeomVertexData(name, format, Geom.UHDynamic) + + # 每条流线最多100个点 + max_vertices = num_lines * 100 + vdata.setNumRows(max_vertices) + + # 创建几何体 + geom = Geom(vdata) + + # 创建几何节点 + geom_node = GeomNode(name) + geom_node.addGeom(geom) + + # 创建节点路径 + node_path = self.world.render.attachNewNode(geom_node) + + # 设置渲染状态 + node_path.setTransparency(TransparencyAttrib.MAlpha) + node_path.setColor(Vec4(1.0, 1.0, 0.0, 0.8)) # 黄色流线 + + # 添加到管理系统 + self.streamline_nodes.append(node_path) + self.fluid_nodes.append(node_path) + + return node_path + + def create_velocity_field(self, name="fluid_velocity_field", grid_density=10): + """ + 创建速度场可视化 + + Args: + name (str): 速度场名称 + grid_density (int): 网格密度 + + Returns: + NodePath: 速度场节点路径 + """ + # 速度场显示流体中各点的速度方向和大小 + format = GeomVertexFormat.getV3c4() # 位置、颜色 + vdata = GeomVertexData(name, format, Geom.UHDynamic) + + # 3D网格,每条线2个点 + max_vertices = grid_density * grid_density * grid_density * 2 + vdata.setNumRows(max_vertices) + + # 创建几何体 + geom = Geom(vdata) + + # 创建几何节点 + geom_node = GeomNode(name) + geom_node.addGeom(geom) + + # 创建节点路径 + node_path = self.world.render.attachNewNode(geom_node) + + # 设置渲染状态 + node_path.setTransparency(TransparencyAttrib.MAlpha) + node_path.setColor(Vec4(1.0, 0.0, 0.0, 0.8)) # 红色速度向量 + + # 添加到管理系统 + self.velocity_field_nodes.append(node_path) + self.fluid_nodes.append(node_path) + + return node_path + + def update_visualization(self, dt): + """ + 更新流体可视化 + + Args: + dt (float): 时间步长 + """ + self.frame_counter += 1 + + # 根据可视化模式更新相应的可视化组件 + if self.visual_mode == self.VISUAL_MODE_PARTICLES: + self._update_particle_visualization() + elif self.visual_mode == self.VISUAL_MODE_SURFACE_MESH: + self._update_surface_mesh() + elif self.visual_mode == self.VISUAL_MODE_SPLASH_MESH: + self._update_splash_mesh() + elif self.visual_mode == self.VISUAL_MODE_STREAMLINES: + self._update_streamlines() + elif self.visual_mode == self.VISUAL_MODE_VELOCITY_FIELD: + self._update_velocity_field() + else: + # 默认更新粒子可视化 + self._update_particle_visualization() + + def _update_particle_visualization(self): + """ + 更新粒子可视化 + """ + if not self.fluid_world.fluid_particles or not self.particle_nodes: + return + + # 限制更新的粒子数量以提高性能 + particles_to_update = min(len(self.fluid_world.fluid_particles), + self.max_visible_particles) + + for node in self.particle_nodes: + # 获取几何体 + geom = node.node().getGeom(0) + vdata = geom.modifyVertexData() + + # 获取顶点写入器 + vertex = GeomVertexWriter(vdata, 'vertex') + + # 更新粒子位置(这里简化处理,实际需要为每个粒子创建面片) + for i in range(min(particles_to_update, vdata.getNumRows() // 4)): + particle = self.fluid_world.fluid_particles[i] + pos = particle['position'] + + # 为粒子创建一个面向摄像机的四边形(简化为单个点) + # 在真实实现中,这里需要创建billboard四边形 + vertex.setData3f(pos) + + def _update_surface_mesh(self): + """ + 更新表面网格(使用Marching Cubes算法) + """ + if not self.fluid_world.fluid_particles or not self.surface_nodes: + return + + # 创建密度场 + resolution = self.surface_reconstruction_resolution + density_grid = self._create_density_grid(resolution) + + # 使用Marching Cubes算法生成网格 + vertices, triangles = self._marching_cubes(density_grid, self.surface_isolevel) + + # 更新几何体 + if vertices and triangles and self.surface_nodes: + node = self.surface_nodes[0] # 使用第一个表面节点 + geom = node.node().getGeom(0) + vdata = geom.modifyVertexData() + + # 确保有足够的行数 + if vdata.getNumRows() < len(vertices) * 3: # 位置、法线、颜色 + vdata.setNumRows(len(vertices) * 3) + + vertex_writer = GeomVertexWriter(vdata, 'vertex') + normal_writer = GeomVertexWriter(vdata, 'normal') + color_writer = GeomVertexWriter(vdata, 'color') + + # 写入顶点数据 + for v in vertices: + vertex_writer.setData3f(v[0], v[1], v[2]) + # 这里应该计算法线,简化为(0,0,1) + normal_writer.setData3f(0, 0, 1) + color_writer.setData4f(self.color) + + # 更新图元 + prim = GeomTriangles(Geom.UHDynamic) + for t in triangles: + prim.addVertices(t[0], t[1], t[2]) + prim.closePrimitive() + + # 替换旧的图元 + geom.removePrimitive(0) + geom.addPrimitive(prim) + + def _create_density_grid(self, resolution): + """ + 创建流体密度网格用于表面重建 + + Args: + resolution (int): 网格分辨率 + + Returns: + numpy.ndarray: 3D密度网格 + """ + if not self.fluid_world.fluid_particles: + return np.zeros((resolution, resolution, resolution)) + + # 确定网格边界 + min_bound = Point3(float('inf'), float('inf'), float('inf')) + max_bound = Point3(float('-inf'), float('-inf'), float('-inf')) + + for particle in self.fluid_world.fluid_particles: + pos = particle['position'] + min_bound.setX(min(min_bound.getX(), pos.getX())) + min_bound.setY(min(min_bound.getY(), pos.getY())) + min_bound.setZ(min(min_bound.getZ(), pos.getZ())) + max_bound.setX(max(max_bound.getX(), pos.getX())) + max_bound.setY(max(max_bound.getY(), pos.getY())) + max_bound.setZ(max(max_bound.getZ(), pos.getZ())) + + # 添加边界填充 + padding = self.smoothing_radius + min_bound -= Vec3(padding, padding, padding) + max_bound += Vec3(padding, padding, padding) + + # 创建密度网格 + grid = np.zeros((resolution, resolution, resolution), dtype=np.float32) + + # 计算网格步长 + step_x = (max_bound.getX() - min_bound.getX()) / resolution + step_y = (max_bound.getY() - min_bound.getY()) / resolution + step_z = (max_bound.getZ() - min_bound.getZ()) / resolution + + # 对每个粒子贡献到网格 + for particle in self.fluid_world.fluid_particles: + pos = particle['position'] + + # 计算粒子在网格中的位置 + grid_x = int((pos.getX() - min_bound.getX()) / step_x) + grid_y = int((pos.getY() - min_bound.getY()) / step_y) + grid_z = int((pos.getZ() - min_bound.getZ()) / step_z) + + # 确保索引在范围内 + if 0 <= grid_x < resolution and 0 <= grid_y < resolution and 0 <= grid_z < resolution: + # 使用SPH核函数计算密度贡献 + for dx in range(-2, 3): + for dy in range(-2, 3): + for dz in range(-2, 3): + nx, ny, nz = grid_x + dx, grid_y + dy, grid_z + dz + if 0 <= nx < resolution and 0 <= ny < resolution and 0 <= nz < resolution: + # 计算网格点的实际位置 + gx = min_bound.getX() + nx * step_x + gy = min_bound.getY() + ny * step_y + gz = min_bound.getZ() + nz * step_z + + # 计算距离 + dist = math.sqrt((gx - pos.getX())**2 + + (gy - pos.getY())**2 + + (gz - pos.getZ())**2) + + if dist < self.smoothing_radius: + # 使用核函数 + kernel_value = self._sph_kernel(dist) + grid[nz, ny, nx] += kernel_value + + return grid + + def _sph_kernel(self, r): + """ + SPH核函数 (Poly6 kernel) + + Args: + r (float): 距离 + + Returns: + float: 核函数值 + """ + if r >= self.smoothing_radius: + return 0.0 + + h = self.smoothing_radius + coefficient = 315.0 / (64.0 * math.pi * h**9) + return coefficient * (h*h - r*r)**3 + + def _marching_cubes(self, density_grid, isolevel): + """ + Marching Cubes算法生成等值面网格 + + Args: + density_grid (numpy.ndarray): 3D密度网格 + isolevel (float): 等值面水平 + + Returns: + tuple: (vertices, triangles) + """ + # 这是一个简化的Marching Cubes实现 + # 实际应用中可能需要更完整的实现 + vertices = [] + triangles = [] + + shape = density_grid.shape + for z in range(shape[0] - 1): + for y in range(shape[1] - 1): + for x in range(shape[2] - 1): + # 获取立方体8个角点的密度值 + cube = [ + density_grid[z, y, x], + density_grid[z, y, x+1], + density_grid[z, y+1, x+1], + density_grid[z, y+1, x], + density_grid[z+1, y, x], + density_grid[z+1, y, x+1], + density_grid[z+1, y+1, x+1], + density_grid[z+1, y+1, x] + ] + + # 检查是否与等值面相交 + bits = 0 + for i, value in enumerate(cube): + if value > isolevel: + bits |= (1 << i) + + # 如果相交,生成三角形(这里使用预定义的配置表) + # 为了简化,我们只生成一个示例三角形 + if bits != 0 and bits != 0xFF: + # 在实际实现中,这里会根据bits值查找预定义的三角形配置 + # 并在边的交点处插入顶点 + center_x = x + 0.5 + center_y = y + 0.5 + center_z = z + 0.5 + vertices.append([center_x, center_y, center_z]) + triangles.append([len(vertices)-1, len(vertices)-1, len(vertices)-1]) + + return vertices, triangles + + def _update_splash_mesh(self): + """ + 更新飞溅网格 + """ + # 飞溅效果通常在高流速或碰撞区域显示 + if not self.fluid_world.fluid_particles or not self.splash_nodes: + return + + # 检测高流速粒子(可能产生飞溅) + splash_particles = [] + for particle in self.fluid_world.fluid_particles: + if particle['velocity'].length() > 5.0: # 速度阈值 + splash_particles.append(particle) + + # 更新飞溅几何体 + if splash_particles and self.splash_nodes: + node = self.splash_nodes[0] + geom = node.node().getGeom(0) + vdata = geom.modifyVertexData() + + if vdata.getNumRows() < len(splash_particles) * 4: # 预分配 + vdata.setNumRows(len(splash_particles) * 4) + + vertex_writer = GeomVertexWriter(vdata, 'vertex') + color_writer = GeomVertexWriter(vdata, 'color') + + for particle in splash_particles: + pos = particle['position'] + # 创建一个小的四边形代表飞溅 + size = 0.2 + vertex_writer.setData3f(pos.getX() - size, pos.getY() - size, pos.getZ()) + vertex_writer.setData3f(pos.getX() + size, pos.getY() - size, pos.getZ()) + vertex_writer.setData3f(pos.getX() + size, pos.getY() + size, pos.getZ()) + vertex_writer.setData3f(pos.getX() - size, pos.getY() + size, pos.getZ()) + + # 飞溅使用更亮的颜色 + splash_color = Vec4(1.0, 1.0, 1.0, self.transparency) + for _ in range(4): + color_writer.setData4f(splash_color) + + def _update_streamlines(self): + """ + 更新流线 + """ + if not self.fluid_world.fluid_particles or not self.streamline_nodes: + return + + # 简化的流线更新 + # 在实际实现中,这将追踪流体中的轨迹线 + if self.streamline_nodes: + node = self.streamline_nodes[0] + geom = node.node().getGeom(0) + vdata = geom.modifyVertexData() + + # 创建一些示例流线 + if len(self.fluid_world.fluid_particles) > 0: + # 使用前几个粒子作为流线起点 + for i in range(min(10, len(self.fluid_world.fluid_particles))): + particle = self.fluid_world.fluid_particles[i] + start_pos = particle['position'] + end_pos = start_pos + particle['velocity'] * 0.1 # 简化的方向线 + + vertex_writer = GeomVertexWriter(vdata, 'vertex') + color_writer = GeomVertexWriter(vdata, 'color') + + vertex_writer.setData3f(start_pos) + vertex_writer.setData3f(end_pos) + + color_writer.setData4f(Vec4(1.0, 1.0, 0.0, self.transparency)) + color_writer.setData4f(Vec4(1.0, 1.0, 0.0, self.transparency)) + + def _update_velocity_field(self): + """ + 更新速度场 + """ + if not self.fluid_world.fluid_particles or not self.velocity_field_nodes: + return + + # 创建规则网格上的速度向量 + if self.velocity_field_nodes: + node = self.velocity_field_nodes[0] + geom = node.node().getGeom(0) + vdata = geom.modifyVertexData() + + # 简化的速度场显示 + if len(self.fluid_world.fluid_particles) > 0: + # 显示部分粒子的速度向量 + step = max(1, len(self.fluid_world.fluid_particles) // 20) # 限制显示数量 + + vertex_writer = GeomVertexWriter(vdata, 'vertex') + color_writer = GeomVertexWriter(vdata, 'color') + + for i in range(0, len(self.fluid_world.fluid_particles), step): + if i >= len(self.fluid_world.fluid_particles): + break + + particle = self.fluid_world.fluid_particles[i] + pos = particle['position'] + vel = particle['velocity'] + + # 标准化速度向量以便可视化 + normalized_vel = vel.normalized() * 0.5 # 缩放以便可视化 + + # 创建速度向量线段 + start_pos = pos + end_pos = pos + normalized_vel + + vertex_writer.setData3f(start_pos) + vertex_writer.setData3f(end_pos) + + # 根据速度大小设置颜色 + speed = vel.length() + speed_color = min(1.0, speed / 10.0) # 假设最大速度为10 + color = Vec4(speed_color, 0.0, 1.0 - speed_color, self.transparency) + + color_writer.setData4f(color) + color_writer.setData4f(color) + + def set_material_properties(self, roughness=None, metallic=None, specular=None, refraction_index=None): + """ + 设置流体材质属性 + + Args: + roughness (float): 粗糙度 + metallic (float): 金属度 + specular (float): 镜面反射 + refraction_index (float): 折射率 + """ + if roughness is not None: + self.roughness = max(0.0, min(1.0, roughness)) + if metallic is not None: + self.metallic = max(0.0, min(1.0, metallic)) + if specular is not None: + self.specular = max(0.0, min(1.0, specular)) + if refraction_index is not None: + self.refraction_index = max(1.0, refraction_index) + + # 应用到所有节点 + for node in self.fluid_nodes: + # 在Panda3D中,这些属性通常通过着色器或材质节点应用 + pass + + def set_surface_reconstruction_params(self, resolution=None, isolevel=None): + """ + 设置表面重建参数 + + Args: + resolution (int): 重建分辨率 + isolevel (float): 等值面水平 + """ + if resolution is not None: + self.surface_reconstruction_resolution = resolution + if isolevel is not None: + self.surface_isolevel = isolevel + + def set_performance_params(self, max_visible_particles=None, update_frequency=None): + """ + 设置性能参数 + + Args: + max_visible_particles (int): 最大可见粒子数 + update_frequency (int): 更新频率 + """ + if max_visible_particles is not None: + self.max_visible_particles = max_visible_particles + if update_frequency is not None: + self.update_frequency = update_frequency + + def get_visualization_stats(self): + """ + 获取可视化统计信息 + + Returns: + dict: 统计信息 + """ + stats = { + 'active_nodes': len(self.fluid_nodes), + 'particle_nodes': len(self.particle_nodes), + 'surface_nodes': len(self.surface_nodes), + 'splash_nodes': len(self.splash_nodes), + 'streamline_nodes': len(self.streamline_nodes), + 'velocity_field_nodes': len(self.velocity_field_nodes), + 'visual_mode': self.visual_mode, + 'particle_count': len(self.fluid_world.fluid_particles) if self.fluid_world else 0 + } + + return stats + + def cleanup(self): + """ + 清理可视化资源 + """ + # 清理所有节点 + for node_list in [self.fluid_nodes, self.particle_nodes, self.surface_nodes, + self.splash_nodes, self.streamline_nodes, self.velocity_field_nodes]: + for node in node_list: + node.removeNode() + node_list.clear() + + # 清空总节点列表 + self.fluid_nodes.clear() + print("流体可视化资源已清理") \ No newline at end of file diff --git a/plugins/user/fluid_dynamics/core/fluid_world.py b/plugins/user/fluid_dynamics/core/fluid_world.py index 948ab7ca..85c6c78f 100644 --- a/plugins/user/fluid_dynamics/core/fluid_world.py +++ b/plugins/user/fluid_dynamics/core/fluid_world.py @@ -1,657 +1,657 @@ -""" -流体世界管理模块 -负责流体世界的创建、配置、更新和销毁,支持高级流体效果 -""" - -from panda3d.core import Vec3, Point3 -import math -import time -import random -import numpy as np - -class FluidWorld: - """ - 流体世界管理类 - 管理整个流体模拟环境,包括流体域、求解器、更新循环等,支持高级流体效果 - """ - - # 流体类型 - FLUID_TYPE_WATER = "water" - FLUID_TYPE_LAVA = "lava" - FLUID_TYPE_SLIME = "slime" - FLUID_TYPE_OIL = "oil" - FLUID_TYPE_BLOOD = "blood" - - # 模拟模式 - SIM_MODE_REALISTIC = "realistic" # 真实物理模拟 - SIM_MODE_STYLIZED = "stylized" # 风格化模拟 - SIM_MODE_PERFORMANCE = "performance" # 性能优先模拟 - - def __init__(self, world): - """ - 初始化流体世界 - - Args: - world: 主世界对象的引用 - """ - self.world = world # 主世界对象引用 - - # 流体参数 - self.time_step = 1.0/60.0 # 默认60FPS - self.simulation_time = 0.0 # 模拟时间 - self.simulation_mode = self.SIM_MODE_REALISTIC # 模拟模式 - - # 流体对象容器 - self.fluid_domains = [] # 流体域 - self.fluid_emitters = [] # 流体发射器 - self.fluid_obstacles = [] # 流体障碍物 - self.fluid_particles = [] # 流体粒子 - - # 高级效果系统 - self.foam_particles = [] # 泡沫粒子 - self.splash_particles = [] # 飞溅粒子 - self.bubbles = [] # 气泡 - self.tracers = [] # 轨迹线 - - # 特殊效果参数 - self.foam_generation_rate = 0.1 # 泡沫生成率 - self.splash_threshold = 5.0 # 飞溅速度阈值 - self.bubble_probability = 0.05 # 气泡生成概率 - - # 环境效果 - self.wind_velocity = Vec3(0, 0, 0) # 风速 - self.temperature = 20.0 # 温度(摄氏度) - self.pressure = 101.325 # 大气压(kPa) - - # 求解器 - self.solver = None - - # 可视化器 - self.visualizer = None - - # 性能优化器 - self.performance_optimizer = None - - # 性能监控 - self.performance_stats = { - 'update_count': 0, - 'last_update_time': 0.0, - 'avg_update_time': 0.0, - 'particle_count': 0, - 'foam_count': 0, - 'splash_count': 0, - 'bubble_count': 0 - } - - # 时间累积器 - self.time_accumulator = 0.0 - - # 流体世界状态 - self.is_paused = False - self.time_scale = 1.0 - - # 流体材质系统 - self.fluid_materials = {} # 流体材质字典 - - # 交互系统 - self.interaction_forces = [] # 外部交互力 - - print("流体世界初始化完成") - - def set_time_step(self, time_step): - """ - 设置时间步长参数 - - Args: - time_step (float): 时间步长 - """ - self.time_step = time_step - if self.solver: - self.solver.time_step = time_step - - def set_simulation_mode(self, mode): - """ - 设置模拟模式 - - Args: - mode (str): 模拟模式 - """ - self.simulation_mode = mode - - # 根据模式调整参数 - if mode == self.SIM_MODE_PERFORMANCE: - self.foam_generation_rate = 0.01 - self.bubble_probability = 0.01 - elif mode == self.SIM_MODE_STYLIZED: - self.foam_generation_rate = 0.2 - self.bubble_probability = 0.1 - else: # REALISTIC - self.foam_generation_rate = 0.1 - self.bubble_probability = 0.05 - - def pause(self, paused=True): - """ - 暂停或恢复流体模拟 - - Args: - paused (bool): 是否暂停 - """ - self.is_paused = paused - - def set_time_scale(self, scale): - """ - 设置时间缩放 - - Args: - scale (float): 时间缩放因子 - """ - self.time_scale = max(0.0, scale) - - def update(self, dt): - """ - 更新流体世界 - - Args: - dt (float): 帧时间增量 - """ - # 如果暂停,不更新流体 - if self.is_paused: - return - - # 应用时间缩放 - dt *= self.time_scale - - try: - # 性能监控开始 - start_time = time.time() - - # 更新性能优化器 - if self.performance_optimizer: - self.performance_optimizer.update_performance_stats() - - # 累积时间 - self.time_accumulator += dt - - # 计算子步数 - num_substeps = int(self.time_accumulator / self.time_step) - - # 执行子步长模拟 - for i in range(num_substeps): - # 更新流体域 - for domain in self.fluid_domains: - domain.update(self.time_step) - - # 更新流体发射器 - for emitter in self.fluid_emitters: - emitter.update(self.time_step) - - # 更新流体障碍物 - for obstacle in self.fluid_obstacles: - obstacle.update(self.time_step) - - # 更新特殊效果粒子 - self._update_foam_particles() - self._update_splash_particles() - self._update_bubbles() - self._update_tracers() - - # 执行求解器 - if self.solver: - self.solver.solve(self.time_step) - - # 生成特殊效果 - self._generate_foam() - self._generate_splash() - self._generate_bubbles() - - # 应用环境效果 - self._apply_wind_effect() - self._apply_temperature_effect() - - # 更新模拟时间 - self.simulation_time += self.time_step - - # 减少累积时间 - if num_substeps > 0: - self.time_accumulator -= num_substeps * self.time_step - - # 更新可视化 - if self.visualizer: - self.visualizer.update_visualization(dt) - - # 更新性能优化器 - if self.performance_optimizer: - self.performance_optimizer.optimize_for_performance() - - # 更新性能统计 - end_time = time.time() - update_time = end_time - start_time - self.performance_stats['last_update_time'] = update_time - self.performance_stats['update_count'] += 1 - self.performance_stats['particle_count'] = len(self.fluid_particles) - self.performance_stats['foam_count'] = len(self.foam_particles) - self.performance_stats['splash_count'] = len(self.splash_particles) - self.performance_stats['bubble_count'] = len(self.bubbles) - - # 计算平均更新时间 - if self.performance_stats['update_count'] > 1: - alpha = 0.1 - self.performance_stats['avg_update_time'] = ( - alpha * update_time + - (1 - alpha) * self.performance_stats['avg_update_time'] - ) - else: - self.performance_stats['avg_update_time'] = update_time - - except Exception as e: - print(f"更新流体世界时出错: {e}") - import traceback - traceback.print_exc() - - def _update_foam_particles(self): - """ - 更新泡沫粒子 - """ - for foam in self.foam_particles[:]: - foam['lifetime'] -= self.time_step - if foam['lifetime'] <= 0: - self.foam_particles.remove(foam) - else: - # 泡沫随水流移动 - # 简化处理:泡沫会逐渐上升 - foam['position'] += Vec3(0, 0, 0.5) * self.time_step - - def _update_splash_particles(self): - """ - 更新飞溅粒子 - """ - for splash in self.splash_particles[:]: - splash['lifetime'] -= self.time_step - if splash['lifetime'] <= 0: - self.splash_particles.remove(splash) - else: - # 应用重力和初始速度 - splash['velocity'] += Vec3(0, 0, -9.81) * self.time_step - splash['position'] += splash['velocity'] * self.time_step - - def _update_bubbles(self): - """ - 更新气泡 - """ - for bubble in self.bubbles[:]: - bubble['lifetime'] -= self.time_step - if bubble['lifetime'] <= 0: - self.bubbles.remove(bubble) - else: - # 气泡上升 - bubble['position'] += Vec3(0, 0, 1.0) * self.time_step - # 气泡可能随水流摆动 - bubble['position'] += Vec3( - random.uniform(-0.1, 0.1), - random.uniform(-0.1, 0.1), - 0 - ) * self.time_step - - def _update_tracers(self): - """ - 更新轨迹线 - """ - for tracer in self.tracers[:]: - tracer['lifetime'] -= self.time_step - if tracer['lifetime'] <= 0: - self.tracers.remove(tracer) - - def _generate_foam(self): - """ - 生成泡沫粒子 - """ - # 在高速流动区域生成泡沫 - for particle in self.fluid_particles: - speed = particle['velocity'].length() - if speed > 2.0 and random.random() < self.foam_generation_rate * self.time_step: - foam = { - 'position': particle['position'] + Vec3(0, 0, 0.1), # 在表面生成 - 'lifetime': random.uniform(1.0, 3.0), - 'size': random.uniform(0.05, 0.2) - } - self.foam_particles.append(foam) - - def _generate_splash(self): - """ - 生成飞溅效果 - """ - # 在碰撞或高速区域生成飞溅 - for particle in self.fluid_particles: - speed = particle['velocity'].length() - if speed > self.splash_threshold and random.random() < 0.1 * self.time_step: - # 生成多个飞溅粒子 - for _ in range(random.randint(1, 3)): - splash = { - 'position': particle['position'], - 'velocity': particle['velocity'] + Vec3( - random.uniform(-2, 2), - random.uniform(-2, 2), - random.uniform(1, 5) - ), - 'lifetime': random.uniform(0.5, 2.0), - 'size': random.uniform(0.02, 0.1) - } - self.splash_particles.append(splash) - - def _generate_bubbles(self): - """ - 生成气泡 - """ - # 随机生成气泡 - if random.random() < self.bubble_probability * self.time_step: - # 在流体域内随机位置生成气泡 - if self.fluid_domains: - domain = random.choice(self.fluid_domains) - min_bound, max_bound = domain.get_bounding_box() - position = Point3( - random.uniform(min_bound.getX(), max_bound.getX()), - random.uniform(min_bound.getY(), max_bound.getY()), - random.uniform(min_bound.getZ(), max_bound.getZ()) - ) - - bubble = { - 'position': position, - 'lifetime': random.uniform(2.0, 5.0), - 'size': random.uniform(0.05, 0.3) - } - self.bubbles.append(bubble) - - def _apply_wind_effect(self): - """ - 应用风力效果 - """ - if self.wind_velocity.length() > 0: - # 风力主要影响表面粒子 - for particle in self.fluid_particles: - # 简化:所有粒子都受风力影响 - particle['velocity'] += self.wind_velocity * 0.1 * self.time_step - - def _apply_temperature_effect(self): - """ - 应用温度效果 - """ - # 温度影响流体粘度 - if self.solver and hasattr(self.solver, 'viscosity'): - # 简化的温度-粘度关系 - base_viscosity = 0.001 # 水在20°C时的粘度 - # 温度越高,粘度越低 - temp_factor = max(0.1, 1.0 - (self.temperature - 20.0) * 0.01) - self.solver.viscosity = base_viscosity * temp_factor - - def add_fluid_domain(self, domain): - """ - 添加流体域到流体世界 - - Args: - domain: 流体域对象 - """ - self.fluid_domains.append(domain) - - def remove_fluid_domain(self, domain): - """ - 从流体世界移除流体域 - - Args: - domain: 流体域对象 - """ - if domain in self.fluid_domains: - self.fluid_domains.remove(domain) - - def add_fluid_emitter(self, emitter): - """ - 添加流体发射器到流体世界 - - Args: - emitter: 流体发射器对象 - """ - self.fluid_emitters.append(emitter) - - def remove_fluid_emitter(self, emitter): - """ - 从流体世界移除流体发射器 - - Args: - emitter: 流体发射器对象 - """ - if emitter in self.fluid_emitters: - self.fluid_emitters.remove(emitter) - - def add_fluid_obstacle(self, obstacle): - """ - 添加流体障碍物到流体世界 - - Args: - obstacle: 流体障碍物对象 - """ - self.fluid_obstacles.append(obstacle) - - def remove_fluid_obstacle(self, obstacle): - """ - 从流体世界移除流体障碍物 - - Args: - obstacle: 流体障碍物对象 - """ - if obstacle in self.fluid_obstacles: - self.fluid_obstacles.remove(obstacle) - - def add_fluid_particle(self, particle): - """ - 添加流体粒子到流体世界 - - Args: - particle: 流体粒子对象 - """ - self.fluid_particles.append(particle) - self.performance_stats['particle_count'] = len(self.fluid_particles) - - def remove_fluid_particle(self, particle): - """ - 从流体世界移除流体粒子 - - Args: - particle: 流体粒子对象 - """ - if particle in self.fluid_particles: - self.fluid_particles.remove(particle) - self.performance_stats['particle_count'] = len(self.fluid_particles) - - def add_foam_particle(self, foam): - """ - 添加泡沫粒子 - - Args: - foam (dict): 泡沫粒子数据 - """ - self.foam_particles.append(foam) - self.performance_stats['foam_count'] = len(self.foam_particles) - - def add_splash_particle(self, splash): - """ - 添加飞溅粒子 - - Args: - splash (dict): 飞溅粒子数据 - """ - self.splash_particles.append(splash) - self.performance_stats['splash_count'] = len(self.splash_particles) - - def add_bubble(self, bubble): - """ - 添加气泡 - - Args: - bubble (dict): 气泡数据 - """ - self.bubbles.append(bubble) - self.performance_stats['bubble_count'] = len(self.bubbles) - - def set_wind_velocity(self, velocity): - """ - 设置风速 - - Args: - velocity (Vec3): 风速向量 - """ - self.wind_velocity = velocity - - def set_temperature(self, temperature): - """ - 设置温度 - - Args: - temperature (float): 温度(摄氏度) - """ - self.temperature = temperature - - def set_pressure(self, pressure): - """ - 设置大气压 - - Args: - pressure (float): 大气压(kPa) - """ - self.pressure = pressure - - def get_performance_stats(self): - """ - 获取性能统计信息 - - Returns: - dict: 性能统计信息 - """ - stats = self.performance_stats.copy() - stats['objects'] = { - 'fluid_domains': len(self.fluid_domains), - 'fluid_emitters': len(self.fluid_emitters), - 'fluid_obstacles': len(self.fluid_obstacles), - 'fluid_particles': len(self.fluid_particles), - 'foam_particles': len(self.foam_particles), - 'splash_particles': len(self.splash_particles), - 'bubbles': len(self.bubbles) - } - stats['environment'] = { - 'temperature': self.temperature, - 'pressure': self.pressure, - 'wind_velocity': self.wind_velocity.length() - } - - return stats - - def clear(self): - """ - 清除流体世界中的所有对象 - """ - # 移除所有流体域 - for domain in self.fluid_domains[:]: - self.remove_fluid_domain(domain) - - # 移除所有流体发射器 - for emitter in self.fluid_emitters[:]: - self.remove_fluid_emitter(emitter) - - # 移除所有流体障碍物 - for obstacle in self.fluid_obstacles[:]: - self.remove_fluid_obstacle(obstacle) - - # 移除所有流体粒子 - for particle in self.fluid_particles[:]: - self.remove_fluid_particle(particle) - - # 清除特殊效果粒子 - self.foam_particles.clear() - self.splash_particles.clear() - self.bubbles.clear() - self.tracers.clear() - - # 重置统计信息 - self.performance_stats['update_count'] = 0 - self.performance_stats['particle_count'] = 0 - self.performance_stats['foam_count'] = 0 - self.performance_stats['splash_count'] = 0 - self.performance_stats['bubble_count'] = 0 - - # 重置时间相关参数 - self.time_accumulator = 0.0 - self.simulation_time = 0.0 - - print("流体世界已清除") - - def reset(self): - """ - 重置流体世界到初始状态 - """ - self.clear() - self.is_paused = False - self.time_scale = 1.0 - self.temperature = 20.0 - self.pressure = 101.325 - self.wind_velocity = Vec3(0, 0, 0) - - def cleanup(self): - """ - 清理流体世界资源 - """ - self.clear() - - # 清理求解器 - if self.solver: - self.solver = None - - # 清理可视化器 - if self.visualizer: - self.visualizer.cleanup() - self.visualizer = None - - # 清理性能优化器 - if self.performance_optimizer: - self.performance_optimizer.cleanup() - self.performance_optimizer = None - - print("流体世界资源已清理") - - def add_interaction_force(self, force_data): - """ - 添加交互力 - - Args: - force_data (dict): 力数据,包含位置、力向量等信息 - """ - self.interaction_forces.append(force_data) - - def remove_interaction_force(self, force_data): - """ - 移除交互力 - - Args: - force_data (dict): 力数据 - """ - if force_data in self.interaction_forces: - self.interaction_forces.remove(force_data) - - def set_fluid_material(self, fluid_type, material): - """ - 设置流体材质 - - Args: - fluid_type (str): 流体类型 - material (dict): 材质数据 - """ - self.fluid_materials[fluid_type] = material - - def get_fluid_material(self, fluid_type): - """ - 获取流体材质 - - Args: - fluid_type (str): 流体类型 - - Returns: - dict: 材质数据 - """ +""" +流体世界管理模块 +负责流体世界的创建、配置、更新和销毁,支持高级流体效果 +""" + +from panda3d.core import Vec3, Point3 +import math +import time +import random +import numpy as np + +class FluidWorld: + """ + 流体世界管理类 + 管理整个流体模拟环境,包括流体域、求解器、更新循环等,支持高级流体效果 + """ + + # 流体类型 + FLUID_TYPE_WATER = "water" + FLUID_TYPE_LAVA = "lava" + FLUID_TYPE_SLIME = "slime" + FLUID_TYPE_OIL = "oil" + FLUID_TYPE_BLOOD = "blood" + + # 模拟模式 + SIM_MODE_REALISTIC = "realistic" # 真实物理模拟 + SIM_MODE_STYLIZED = "stylized" # 风格化模拟 + SIM_MODE_PERFORMANCE = "performance" # 性能优先模拟 + + def __init__(self, world): + """ + 初始化流体世界 + + Args: + world: 主世界对象的引用 + """ + self.world = world # 主世界对象引用 + + # 流体参数 + self.time_step = 1.0/60.0 # 默认60FPS + self.simulation_time = 0.0 # 模拟时间 + self.simulation_mode = self.SIM_MODE_REALISTIC # 模拟模式 + + # 流体对象容器 + self.fluid_domains = [] # 流体域 + self.fluid_emitters = [] # 流体发射器 + self.fluid_obstacles = [] # 流体障碍物 + self.fluid_particles = [] # 流体粒子 + + # 高级效果系统 + self.foam_particles = [] # 泡沫粒子 + self.splash_particles = [] # 飞溅粒子 + self.bubbles = [] # 气泡 + self.tracers = [] # 轨迹线 + + # 特殊效果参数 + self.foam_generation_rate = 0.1 # 泡沫生成率 + self.splash_threshold = 5.0 # 飞溅速度阈值 + self.bubble_probability = 0.05 # 气泡生成概率 + + # 环境效果 + self.wind_velocity = Vec3(0, 0, 0) # 风速 + self.temperature = 20.0 # 温度(摄氏度) + self.pressure = 101.325 # 大气压(kPa) + + # 求解器 + self.solver = None + + # 可视化器 + self.visualizer = None + + # 性能优化器 + self.performance_optimizer = None + + # 性能监控 + self.performance_stats = { + 'update_count': 0, + 'last_update_time': 0.0, + 'avg_update_time': 0.0, + 'particle_count': 0, + 'foam_count': 0, + 'splash_count': 0, + 'bubble_count': 0 + } + + # 时间累积器 + self.time_accumulator = 0.0 + + # 流体世界状态 + self.is_paused = False + self.time_scale = 1.0 + + # 流体材质系统 + self.fluid_materials = {} # 流体材质字典 + + # 交互系统 + self.interaction_forces = [] # 外部交互力 + + print("流体世界初始化完成") + + def set_time_step(self, time_step): + """ + 设置时间步长参数 + + Args: + time_step (float): 时间步长 + """ + self.time_step = time_step + if self.solver: + self.solver.time_step = time_step + + def set_simulation_mode(self, mode): + """ + 设置模拟模式 + + Args: + mode (str): 模拟模式 + """ + self.simulation_mode = mode + + # 根据模式调整参数 + if mode == self.SIM_MODE_PERFORMANCE: + self.foam_generation_rate = 0.01 + self.bubble_probability = 0.01 + elif mode == self.SIM_MODE_STYLIZED: + self.foam_generation_rate = 0.2 + self.bubble_probability = 0.1 + else: # REALISTIC + self.foam_generation_rate = 0.1 + self.bubble_probability = 0.05 + + def pause(self, paused=True): + """ + 暂停或恢复流体模拟 + + Args: + paused (bool): 是否暂停 + """ + self.is_paused = paused + + def set_time_scale(self, scale): + """ + 设置时间缩放 + + Args: + scale (float): 时间缩放因子 + """ + self.time_scale = max(0.0, scale) + + def update(self, dt): + """ + 更新流体世界 + + Args: + dt (float): 帧时间增量 + """ + # 如果暂停,不更新流体 + if self.is_paused: + return + + # 应用时间缩放 + dt *= self.time_scale + + try: + # 性能监控开始 + start_time = time.time() + + # 更新性能优化器 + if self.performance_optimizer: + self.performance_optimizer.update_performance_stats() + + # 累积时间 + self.time_accumulator += dt + + # 计算子步数 + num_substeps = int(self.time_accumulator / self.time_step) + + # 执行子步长模拟 + for i in range(num_substeps): + # 更新流体域 + for domain in self.fluid_domains: + domain.update(self.time_step) + + # 更新流体发射器 + for emitter in self.fluid_emitters: + emitter.update(self.time_step) + + # 更新流体障碍物 + for obstacle in self.fluid_obstacles: + obstacle.update(self.time_step) + + # 更新特殊效果粒子 + self._update_foam_particles() + self._update_splash_particles() + self._update_bubbles() + self._update_tracers() + + # 执行求解器 + if self.solver: + self.solver.solve(self.time_step) + + # 生成特殊效果 + self._generate_foam() + self._generate_splash() + self._generate_bubbles() + + # 应用环境效果 + self._apply_wind_effect() + self._apply_temperature_effect() + + # 更新模拟时间 + self.simulation_time += self.time_step + + # 减少累积时间 + if num_substeps > 0: + self.time_accumulator -= num_substeps * self.time_step + + # 更新可视化 + if self.visualizer: + self.visualizer.update_visualization(dt) + + # 更新性能优化器 + if self.performance_optimizer: + self.performance_optimizer.optimize_for_performance() + + # 更新性能统计 + end_time = time.time() + update_time = end_time - start_time + self.performance_stats['last_update_time'] = update_time + self.performance_stats['update_count'] += 1 + self.performance_stats['particle_count'] = len(self.fluid_particles) + self.performance_stats['foam_count'] = len(self.foam_particles) + self.performance_stats['splash_count'] = len(self.splash_particles) + self.performance_stats['bubble_count'] = len(self.bubbles) + + # 计算平均更新时间 + if self.performance_stats['update_count'] > 1: + alpha = 0.1 + self.performance_stats['avg_update_time'] = ( + alpha * update_time + + (1 - alpha) * self.performance_stats['avg_update_time'] + ) + else: + self.performance_stats['avg_update_time'] = update_time + + except Exception as e: + print(f"更新流体世界时出错: {e}") + import traceback + traceback.print_exc() + + def _update_foam_particles(self): + """ + 更新泡沫粒子 + """ + for foam in self.foam_particles[:]: + foam['lifetime'] -= self.time_step + if foam['lifetime'] <= 0: + self.foam_particles.remove(foam) + else: + # 泡沫随水流移动 + # 简化处理:泡沫会逐渐上升 + foam['position'] += Vec3(0, 0, 0.5) * self.time_step + + def _update_splash_particles(self): + """ + 更新飞溅粒子 + """ + for splash in self.splash_particles[:]: + splash['lifetime'] -= self.time_step + if splash['lifetime'] <= 0: + self.splash_particles.remove(splash) + else: + # 应用重力和初始速度 + splash['velocity'] += Vec3(0, 0, -9.81) * self.time_step + splash['position'] += splash['velocity'] * self.time_step + + def _update_bubbles(self): + """ + 更新气泡 + """ + for bubble in self.bubbles[:]: + bubble['lifetime'] -= self.time_step + if bubble['lifetime'] <= 0: + self.bubbles.remove(bubble) + else: + # 气泡上升 + bubble['position'] += Vec3(0, 0, 1.0) * self.time_step + # 气泡可能随水流摆动 + bubble['position'] += Vec3( + random.uniform(-0.1, 0.1), + random.uniform(-0.1, 0.1), + 0 + ) * self.time_step + + def _update_tracers(self): + """ + 更新轨迹线 + """ + for tracer in self.tracers[:]: + tracer['lifetime'] -= self.time_step + if tracer['lifetime'] <= 0: + self.tracers.remove(tracer) + + def _generate_foam(self): + """ + 生成泡沫粒子 + """ + # 在高速流动区域生成泡沫 + for particle in self.fluid_particles: + speed = particle['velocity'].length() + if speed > 2.0 and random.random() < self.foam_generation_rate * self.time_step: + foam = { + 'position': particle['position'] + Vec3(0, 0, 0.1), # 在表面生成 + 'lifetime': random.uniform(1.0, 3.0), + 'size': random.uniform(0.05, 0.2) + } + self.foam_particles.append(foam) + + def _generate_splash(self): + """ + 生成飞溅效果 + """ + # 在碰撞或高速区域生成飞溅 + for particle in self.fluid_particles: + speed = particle['velocity'].length() + if speed > self.splash_threshold and random.random() < 0.1 * self.time_step: + # 生成多个飞溅粒子 + for _ in range(random.randint(1, 3)): + splash = { + 'position': particle['position'], + 'velocity': particle['velocity'] + Vec3( + random.uniform(-2, 2), + random.uniform(-2, 2), + random.uniform(1, 5) + ), + 'lifetime': random.uniform(0.5, 2.0), + 'size': random.uniform(0.02, 0.1) + } + self.splash_particles.append(splash) + + def _generate_bubbles(self): + """ + 生成气泡 + """ + # 随机生成气泡 + if random.random() < self.bubble_probability * self.time_step: + # 在流体域内随机位置生成气泡 + if self.fluid_domains: + domain = random.choice(self.fluid_domains) + min_bound, max_bound = domain.get_bounding_box() + position = Point3( + random.uniform(min_bound.getX(), max_bound.getX()), + random.uniform(min_bound.getY(), max_bound.getY()), + random.uniform(min_bound.getZ(), max_bound.getZ()) + ) + + bubble = { + 'position': position, + 'lifetime': random.uniform(2.0, 5.0), + 'size': random.uniform(0.05, 0.3) + } + self.bubbles.append(bubble) + + def _apply_wind_effect(self): + """ + 应用风力效果 + """ + if self.wind_velocity.length() > 0: + # 风力主要影响表面粒子 + for particle in self.fluid_particles: + # 简化:所有粒子都受风力影响 + particle['velocity'] += self.wind_velocity * 0.1 * self.time_step + + def _apply_temperature_effect(self): + """ + 应用温度效果 + """ + # 温度影响流体粘度 + if self.solver and hasattr(self.solver, 'viscosity'): + # 简化的温度-粘度关系 + base_viscosity = 0.001 # 水在20°C时的粘度 + # 温度越高,粘度越低 + temp_factor = max(0.1, 1.0 - (self.temperature - 20.0) * 0.01) + self.solver.viscosity = base_viscosity * temp_factor + + def add_fluid_domain(self, domain): + """ + 添加流体域到流体世界 + + Args: + domain: 流体域对象 + """ + self.fluid_domains.append(domain) + + def remove_fluid_domain(self, domain): + """ + 从流体世界移除流体域 + + Args: + domain: 流体域对象 + """ + if domain in self.fluid_domains: + self.fluid_domains.remove(domain) + + def add_fluid_emitter(self, emitter): + """ + 添加流体发射器到流体世界 + + Args: + emitter: 流体发射器对象 + """ + self.fluid_emitters.append(emitter) + + def remove_fluid_emitter(self, emitter): + """ + 从流体世界移除流体发射器 + + Args: + emitter: 流体发射器对象 + """ + if emitter in self.fluid_emitters: + self.fluid_emitters.remove(emitter) + + def add_fluid_obstacle(self, obstacle): + """ + 添加流体障碍物到流体世界 + + Args: + obstacle: 流体障碍物对象 + """ + self.fluid_obstacles.append(obstacle) + + def remove_fluid_obstacle(self, obstacle): + """ + 从流体世界移除流体障碍物 + + Args: + obstacle: 流体障碍物对象 + """ + if obstacle in self.fluid_obstacles: + self.fluid_obstacles.remove(obstacle) + + def add_fluid_particle(self, particle): + """ + 添加流体粒子到流体世界 + + Args: + particle: 流体粒子对象 + """ + self.fluid_particles.append(particle) + self.performance_stats['particle_count'] = len(self.fluid_particles) + + def remove_fluid_particle(self, particle): + """ + 从流体世界移除流体粒子 + + Args: + particle: 流体粒子对象 + """ + if particle in self.fluid_particles: + self.fluid_particles.remove(particle) + self.performance_stats['particle_count'] = len(self.fluid_particles) + + def add_foam_particle(self, foam): + """ + 添加泡沫粒子 + + Args: + foam (dict): 泡沫粒子数据 + """ + self.foam_particles.append(foam) + self.performance_stats['foam_count'] = len(self.foam_particles) + + def add_splash_particle(self, splash): + """ + 添加飞溅粒子 + + Args: + splash (dict): 飞溅粒子数据 + """ + self.splash_particles.append(splash) + self.performance_stats['splash_count'] = len(self.splash_particles) + + def add_bubble(self, bubble): + """ + 添加气泡 + + Args: + bubble (dict): 气泡数据 + """ + self.bubbles.append(bubble) + self.performance_stats['bubble_count'] = len(self.bubbles) + + def set_wind_velocity(self, velocity): + """ + 设置风速 + + Args: + velocity (Vec3): 风速向量 + """ + self.wind_velocity = velocity + + def set_temperature(self, temperature): + """ + 设置温度 + + Args: + temperature (float): 温度(摄氏度) + """ + self.temperature = temperature + + def set_pressure(self, pressure): + """ + 设置大气压 + + Args: + pressure (float): 大气压(kPa) + """ + self.pressure = pressure + + def get_performance_stats(self): + """ + 获取性能统计信息 + + Returns: + dict: 性能统计信息 + """ + stats = self.performance_stats.copy() + stats['objects'] = { + 'fluid_domains': len(self.fluid_domains), + 'fluid_emitters': len(self.fluid_emitters), + 'fluid_obstacles': len(self.fluid_obstacles), + 'fluid_particles': len(self.fluid_particles), + 'foam_particles': len(self.foam_particles), + 'splash_particles': len(self.splash_particles), + 'bubbles': len(self.bubbles) + } + stats['environment'] = { + 'temperature': self.temperature, + 'pressure': self.pressure, + 'wind_velocity': self.wind_velocity.length() + } + + return stats + + def clear(self): + """ + 清除流体世界中的所有对象 + """ + # 移除所有流体域 + for domain in self.fluid_domains[:]: + self.remove_fluid_domain(domain) + + # 移除所有流体发射器 + for emitter in self.fluid_emitters[:]: + self.remove_fluid_emitter(emitter) + + # 移除所有流体障碍物 + for obstacle in self.fluid_obstacles[:]: + self.remove_fluid_obstacle(obstacle) + + # 移除所有流体粒子 + for particle in self.fluid_particles[:]: + self.remove_fluid_particle(particle) + + # 清除特殊效果粒子 + self.foam_particles.clear() + self.splash_particles.clear() + self.bubbles.clear() + self.tracers.clear() + + # 重置统计信息 + self.performance_stats['update_count'] = 0 + self.performance_stats['particle_count'] = 0 + self.performance_stats['foam_count'] = 0 + self.performance_stats['splash_count'] = 0 + self.performance_stats['bubble_count'] = 0 + + # 重置时间相关参数 + self.time_accumulator = 0.0 + self.simulation_time = 0.0 + + print("流体世界已清除") + + def reset(self): + """ + 重置流体世界到初始状态 + """ + self.clear() + self.is_paused = False + self.time_scale = 1.0 + self.temperature = 20.0 + self.pressure = 101.325 + self.wind_velocity = Vec3(0, 0, 0) + + def cleanup(self): + """ + 清理流体世界资源 + """ + self.clear() + + # 清理求解器 + if self.solver: + self.solver = None + + # 清理可视化器 + if self.visualizer: + self.visualizer.cleanup() + self.visualizer = None + + # 清理性能优化器 + if self.performance_optimizer: + self.performance_optimizer.cleanup() + self.performance_optimizer = None + + print("流体世界资源已清理") + + def add_interaction_force(self, force_data): + """ + 添加交互力 + + Args: + force_data (dict): 力数据,包含位置、力向量等信息 + """ + self.interaction_forces.append(force_data) + + def remove_interaction_force(self, force_data): + """ + 移除交互力 + + Args: + force_data (dict): 力数据 + """ + if force_data in self.interaction_forces: + self.interaction_forces.remove(force_data) + + def set_fluid_material(self, fluid_type, material): + """ + 设置流体材质 + + Args: + fluid_type (str): 流体类型 + material (dict): 材质数据 + """ + self.fluid_materials[fluid_type] = material + + def get_fluid_material(self, fluid_type): + """ + 获取流体材质 + + Args: + fluid_type (str): 流体类型 + + Returns: + dict: 材质数据 + """ return self.fluid_materials.get(fluid_type, {}) \ No newline at end of file diff --git a/plugins/user/fluid_dynamics/demo_fluid_scene.py b/plugins/user/fluid_dynamics/demo_fluid_scene.py index c1552765..04f67c74 100644 --- a/plugins/user/fluid_dynamics/demo_fluid_scene.py +++ b/plugins/user/fluid_dynamics/demo_fluid_scene.py @@ -1,84 +1,84 @@ -""" -流体动力学插件测试示例 -此文件演示如何使用流体动力学插件 -""" - -def create_fluid_demo(world): - """ - 创建流体动力学演示场景 - - Args: - world: 主世界对象 - """ - try: - # 检查插件是否已加载 - if not hasattr(world, 'plugin_manager') or not world.plugin_manager: - print("插件管理器未初始化") - return - - # 加载流体动力学插件 - fluid_plugin = world.plugin_manager.load_plugin("fluid_dynamics") - if not fluid_plugin: - print("流体动力学插件加载失败") - return - - # 启用插件 - if not world.plugin_manager.enable_plugin("fluid_dynamics"): - print("流体动力学插件启用失败") - return - - # 获取流体世界实例 - fluid_world = fluid_plugin.fluid_world - if not fluid_world: - print("流体世界实例获取失败") - return - - # 创建流体域 - from .components.fluid_domain import FluidDomain - domain = FluidDomain( - Point3(-10, -10, 0), - Point3(10, 10, 20) - ) - fluid_world.add_fluid_domain(domain) - - # 创建流体发射器 - from .components.fluid_emitter import FluidEmitter - emitter = FluidEmitter( - Point3(0, 0, 10), - fluid_world - ) - emitter.set_emit_rate(50) # 每秒发射50个粒子 - emitter.set_emit_velocity(Vec3(0, 0, 2)) # 向上发射 - fluid_world.add_fluid_emitter(emitter) - - # 创建障碍物 - from .components.fluid_obstacle import FluidObstacle - obstacle = FluidObstacle( - Point3(0, 0, 5), - Vec3(2, 2, 2), - "box" - ) - fluid_world.add_fluid_obstacle(obstacle) - - # 设置求解器 - from .core.fluid_solver import FluidSolver - solver = FluidSolver(fluid_world) - fluid_world.solver = solver - - # 设置可视化器 - from .core.fluid_visualizer import FluidVisualizer - visualizer = FluidVisualizer(fluid_world, world) - - # 创建粒子系统用于可视化 - particle_system = visualizer.create_particle_system("fluid_particles", 2000) - - print("流体动力学演示场景创建完成") - print("使用方法:") - print("1. 调整发射器参数:emitter.set_emit_rate(粒子数)") - print("2. 暂停/恢复模拟:fluid_world.pause(True/False)") - print("3. 调整时间缩放:fluid_world.set_time_scale(缩放因子)") - - except Exception as e: - print(f"创建流体动力学演示场景时出错: {e}") - import traceback +""" +流体动力学插件测试示例 +此文件演示如何使用流体动力学插件 +""" + +def create_fluid_demo(world): + """ + 创建流体动力学演示场景 + + Args: + world: 主世界对象 + """ + try: + # 检查插件是否已加载 + if not hasattr(world, 'plugin_manager') or not world.plugin_manager: + print("插件管理器未初始化") + return + + # 加载流体动力学插件 + fluid_plugin = world.plugin_manager.load_plugin("fluid_dynamics") + if not fluid_plugin: + print("流体动力学插件加载失败") + return + + # 启用插件 + if not world.plugin_manager.enable_plugin("fluid_dynamics"): + print("流体动力学插件启用失败") + return + + # 获取流体世界实例 + fluid_world = fluid_plugin.fluid_world + if not fluid_world: + print("流体世界实例获取失败") + return + + # 创建流体域 + from .components.fluid_domain import FluidDomain + domain = FluidDomain( + Point3(-10, -10, 0), + Point3(10, 10, 20) + ) + fluid_world.add_fluid_domain(domain) + + # 创建流体发射器 + from .components.fluid_emitter import FluidEmitter + emitter = FluidEmitter( + Point3(0, 0, 10), + fluid_world + ) + emitter.set_emit_rate(50) # 每秒发射50个粒子 + emitter.set_emit_velocity(Vec3(0, 0, 2)) # 向上发射 + fluid_world.add_fluid_emitter(emitter) + + # 创建障碍物 + from .components.fluid_obstacle import FluidObstacle + obstacle = FluidObstacle( + Point3(0, 0, 5), + Vec3(2, 2, 2), + "box" + ) + fluid_world.add_fluid_obstacle(obstacle) + + # 设置求解器 + from .core.fluid_solver import FluidSolver + solver = FluidSolver(fluid_world) + fluid_world.solver = solver + + # 设置可视化器 + from .core.fluid_visualizer import FluidVisualizer + visualizer = FluidVisualizer(fluid_world, world) + + # 创建粒子系统用于可视化 + particle_system = visualizer.create_particle_system("fluid_particles", 2000) + + print("流体动力学演示场景创建完成") + print("使用方法:") + print("1. 调整发射器参数:emitter.set_emit_rate(粒子数)") + print("2. 暂停/恢复模拟:fluid_world.pause(True/False)") + print("3. 调整时间缩放:fluid_world.set_time_scale(缩放因子)") + + except Exception as e: + print(f"创建流体动力学演示场景时出错: {e}") + import traceback traceback.print_exc() \ No newline at end of file diff --git a/plugins/user/fluid_dynamics/plugin.py b/plugins/user/fluid_dynamics/plugin.py index a5abff84..a36cc07c 100644 --- a/plugins/user/fluid_dynamics/plugin.py +++ b/plugins/user/fluid_dynamics/plugin.py @@ -1,99 +1,99 @@ -""" -流体动力学插件 -此文件定义了插件的主类和基本接口 -""" - -from plugins.plugin_manager import BasePlugin -from .core.fluid_world import FluidWorld - -class Plugin(BasePlugin): - """ - 流体动力学插件主类 - """ - - def __init__(self, plugin_manager, name): - super().__init__(plugin_manager, name) - # 设置插件配置 - self.config = { - "version": "1.0.0", - "author": "Your Name", - "description": "基于物理的流体动力学模拟插件" - } - - # 流体世界实例 - self.fluid_world = None - - def initialize(self) -> bool: - """ - 初始化插件 - 在插件加载时调用,用于执行一次性设置 - 返回True表示初始化成功,False表示失败 - """ - print(f"初始化流体动力学插件: {self.name}") - # 在这里执行插件初始化逻辑 - return True - - def enable(self) -> bool: - """ - 启用插件 - 在插件启用时调用,用于注册事件处理器、创建GUI元素等 - 返回True表示启用成功,False表示失败 - """ - if not super().enable(): - return False - - print(f"启用流体动力学插件: {self.name}") - # 创建流体世界实例 - try: - self.fluid_world = FluidWorld(self.plugin_manager.world) - print("流体世界创建成功") - except Exception as e: - print(f"流体世界创建失败: {e}") - return False - - # 在这里执行插件启用逻辑 - # 例如注册事件处理器: - # self.plugin_manager.world.accept("f1", self.on_f1_pressed) - return True - - def disable(self) -> bool: - """ - 禁用插件 - 在插件禁用时调用,用于清理资源、移除事件处理器等 - 返回True表示禁用成功,False表示失败 - """ - if not super().disable(): - return False - - print(f"禁用流体动力学插件: {self.name}") - # 清理流体世界资源 - if self.fluid_world: - self.fluid_world.cleanup() - - # 在这里执行插件禁用逻辑 - # 例如移除事件处理器: - # self.plugin_manager.world.ignore("f1") - return True - - def finalize(self): - """ - 清理插件资源 - 在插件卸载时调用,用于执行最终清理工作 - """ - print(f"清理流体动力学插件资源: {self.name}") - # 在这里执行最终清理逻辑 - if self.fluid_world: - self.fluid_world.cleanup() - - def get_info(self) -> dict: - """ - 获取插件信息 - 返回包含插件信息的字典 - """ - info = super().get_info() - info.update({ - "version": self.config.get("version", "1.0.0"), - "author": self.config.get("author", "Unknown"), - "description": self.config.get("description", "Fluid dynamics plugin") - }) +""" +流体动力学插件 +此文件定义了插件的主类和基本接口 +""" + +from plugins.plugin_manager import BasePlugin +from .core.fluid_world import FluidWorld + +class Plugin(BasePlugin): + """ + 流体动力学插件主类 + """ + + def __init__(self, plugin_manager, name): + super().__init__(plugin_manager, name) + # 设置插件配置 + self.config = { + "version": "1.0.0", + "author": "Your Name", + "description": "基于物理的流体动力学模拟插件" + } + + # 流体世界实例 + self.fluid_world = None + + def initialize(self) -> bool: + """ + 初始化插件 + 在插件加载时调用,用于执行一次性设置 + 返回True表示初始化成功,False表示失败 + """ + print(f"初始化流体动力学插件: {self.name}") + # 在这里执行插件初始化逻辑 + return True + + def enable(self) -> bool: + """ + 启用插件 + 在插件启用时调用,用于注册事件处理器、创建GUI元素等 + 返回True表示启用成功,False表示失败 + """ + if not super().enable(): + return False + + print(f"启用流体动力学插件: {self.name}") + # 创建流体世界实例 + try: + self.fluid_world = FluidWorld(self.plugin_manager.world) + print("流体世界创建成功") + except Exception as e: + print(f"流体世界创建失败: {e}") + return False + + # 在这里执行插件启用逻辑 + # 例如注册事件处理器: + # self.plugin_manager.world.accept("f1", self.on_f1_pressed) + return True + + def disable(self) -> bool: + """ + 禁用插件 + 在插件禁用时调用,用于清理资源、移除事件处理器等 + 返回True表示禁用成功,False表示失败 + """ + if not super().disable(): + return False + + print(f"禁用流体动力学插件: {self.name}") + # 清理流体世界资源 + if self.fluid_world: + self.fluid_world.cleanup() + + # 在这里执行插件禁用逻辑 + # 例如移除事件处理器: + # self.plugin_manager.world.ignore("f1") + return True + + def finalize(self): + """ + 清理插件资源 + 在插件卸载时调用,用于执行最终清理工作 + """ + print(f"清理流体动力学插件资源: {self.name}") + # 在这里执行最终清理逻辑 + if self.fluid_world: + self.fluid_world.cleanup() + + def get_info(self) -> dict: + """ + 获取插件信息 + 返回包含插件信息的字典 + """ + info = super().get_info() + info.update({ + "version": self.config.get("version", "1.0.0"), + "author": self.config.get("author", "Unknown"), + "description": self.config.get("description", "Fluid dynamics plugin") + }) return info \ No newline at end of file diff --git a/plugins/user/fluid_dynamics/utils/math_utils.py b/plugins/user/fluid_dynamics/utils/math_utils.py index 4cd802b4..8645feae 100644 --- a/plugins/user/fluid_dynamics/utils/math_utils.py +++ b/plugins/user/fluid_dynamics/utils/math_utils.py @@ -1,217 +1,217 @@ -""" -流体动力学插件数学工具模块 -提供流体模拟所需的数学计算功能 -""" - -import math -from panda3d.core import Vec3, Point3 - -class FluidMathUtils: - """ - 流体数学工具类 - 提供流体模拟所需的数学计算功能 - """ - - @staticmethod - def dot_product(a, b): - """ - 计算两个向量的点积 - - Args: - a (Vec3): 向量a - b (Vec3): 向量b - - Returns: - float: 点积结果 - """ - return a.dot(b) - - @staticmethod - def cross_product(a, b): - """ - 计算两个向量的叉积 - - Args: - a (Vec3): 向量a - b (Vec3): 向量b - - Returns: - Vec3: 叉积结果 - """ - return a.cross(b) - - @staticmethod - def vector_length(v): - """ - 计算向量长度 - - Args: - v (Vec3): 向量 - - Returns: - float: 向量长度 - """ - return v.length() - - @staticmethod - def normalize_vector(v): - """ - 归一化向量 - - Args: - v (Vec3): 向量 - - Returns: - Vec3: 归一化后的向量 - """ - if v.length() > 0: - return v.normalized() - return Vec3(0, 0, 0) - - @staticmethod - def distance(a, b): - """ - 计算两点间距离 - - Args: - a (Point3): 点a - b (Point3): 点b - - Returns: - float: 距离 - """ - return (a - b).length() - - @staticmethod - def interpolate(a, b, t): - """ - 线性插值 - - Args: - a (float): 起始值 - b (float): 终止值 - t (float): 插值参数 (0-1) - - Returns: - float: 插值结果 - """ - return a + (b - a) * t - - @staticmethod - def clamp(value, min_val, max_val): - """ - 限制值在指定范围内 - - Args: - value (float): 值 - min_val (float): 最小值 - max_val (float): 最大值 - - Returns: - float: 限制后的值 - """ - return max(min_val, min(max_val, value)) - - @staticmethod - def smoothstep(edge0, edge1, x): - """ - 平滑步进函数 - - Args: - edge0 (float): 起始边缘 - edge1 (float): 终止边缘 - x (float): 输入值 - - Returns: - float: 平滑步进结果 - """ - t = FluidMathUtils.clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0) - return t * t * (3.0 - 2.0 * t) - - @staticmethod - def calculate_pressure(density, rest_density, gas_constant): - """ - 计算压力(状态方程) - - Args: - density (float): 当前密度 - rest_density (float): 静止密度 - gas_constant (float): 气体常数 - - Returns: - float: 压力值 - """ - # 简单的状态方程: P = k * (ρ - ρ₀) - return gas_constant * (density - rest_density) - - @staticmethod - def calculate_viscosity_force(velocity_diff, viscosity, distance, radius): - """ - 计算粘性力 - - Args: - velocity_diff (Vec3): 速度差 - viscosity (float): 粘度 - distance (float): 距离 - radius (float): 影响半径 - - Returns: - Vec3: 粘性力 - """ - # 基于距离的权重 - weight = 1.0 - distance / radius if distance < radius else 0.0 - # 粘性力与速度差和粘度成正比 - return velocity_diff * viscosity * weight - - @staticmethod - def calculate_surface_tension_force(normal, surface_tension, curvature): - """ - 计算表面张力力 - - Args: - normal (Vec3): 表面法线 - surface_tension (float): 表面张力系数 - curvature (float): 曲率 - - Returns: - Vec3: 表面张力力 - """ - # 表面张力力与曲率和法线方向有关 - return normal * surface_tension * curvature - - @staticmethod - def calculate_buoyancy(density_diff, gravity): - """ - 计算浮力 - - Args: - density_diff (float): 密度差 - gravity (Vec3): 重力向量 - - Returns: - Vec3: 浮力 - """ - # 浮力与密度差和重力成正比 - return gravity * density_diff - - @staticmethod - def calculate_drag_force(velocity, drag_coefficient, fluid_density, area): - """ - 计算阻力 - - Args: - velocity (Vec3): 速度向量 - drag_coefficient (float): 阻力系数 - fluid_density (float): 流体密度 - area (float): 参考面积 - - Returns: - Vec3: 阻力向量 - """ - speed = velocity.length() - if speed > 0: - # 阻力公式: F = 0.5 * ρ * v² * Cd * A - drag_magnitude = 0.5 * fluid_density * speed * speed * drag_coefficient * area - drag_direction = -velocity.normalized() - return drag_direction * drag_magnitude +""" +流体动力学插件数学工具模块 +提供流体模拟所需的数学计算功能 +""" + +import math +from panda3d.core import Vec3, Point3 + +class FluidMathUtils: + """ + 流体数学工具类 + 提供流体模拟所需的数学计算功能 + """ + + @staticmethod + def dot_product(a, b): + """ + 计算两个向量的点积 + + Args: + a (Vec3): 向量a + b (Vec3): 向量b + + Returns: + float: 点积结果 + """ + return a.dot(b) + + @staticmethod + def cross_product(a, b): + """ + 计算两个向量的叉积 + + Args: + a (Vec3): 向量a + b (Vec3): 向量b + + Returns: + Vec3: 叉积结果 + """ + return a.cross(b) + + @staticmethod + def vector_length(v): + """ + 计算向量长度 + + Args: + v (Vec3): 向量 + + Returns: + float: 向量长度 + """ + return v.length() + + @staticmethod + def normalize_vector(v): + """ + 归一化向量 + + Args: + v (Vec3): 向量 + + Returns: + Vec3: 归一化后的向量 + """ + if v.length() > 0: + return v.normalized() + return Vec3(0, 0, 0) + + @staticmethod + def distance(a, b): + """ + 计算两点间距离 + + Args: + a (Point3): 点a + b (Point3): 点b + + Returns: + float: 距离 + """ + return (a - b).length() + + @staticmethod + def interpolate(a, b, t): + """ + 线性插值 + + Args: + a (float): 起始值 + b (float): 终止值 + t (float): 插值参数 (0-1) + + Returns: + float: 插值结果 + """ + return a + (b - a) * t + + @staticmethod + def clamp(value, min_val, max_val): + """ + 限制值在指定范围内 + + Args: + value (float): 值 + min_val (float): 最小值 + max_val (float): 最大值 + + Returns: + float: 限制后的值 + """ + return max(min_val, min(max_val, value)) + + @staticmethod + def smoothstep(edge0, edge1, x): + """ + 平滑步进函数 + + Args: + edge0 (float): 起始边缘 + edge1 (float): 终止边缘 + x (float): 输入值 + + Returns: + float: 平滑步进结果 + """ + t = FluidMathUtils.clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0) + return t * t * (3.0 - 2.0 * t) + + @staticmethod + def calculate_pressure(density, rest_density, gas_constant): + """ + 计算压力(状态方程) + + Args: + density (float): 当前密度 + rest_density (float): 静止密度 + gas_constant (float): 气体常数 + + Returns: + float: 压力值 + """ + # 简单的状态方程: P = k * (ρ - ρ₀) + return gas_constant * (density - rest_density) + + @staticmethod + def calculate_viscosity_force(velocity_diff, viscosity, distance, radius): + """ + 计算粘性力 + + Args: + velocity_diff (Vec3): 速度差 + viscosity (float): 粘度 + distance (float): 距离 + radius (float): 影响半径 + + Returns: + Vec3: 粘性力 + """ + # 基于距离的权重 + weight = 1.0 - distance / radius if distance < radius else 0.0 + # 粘性力与速度差和粘度成正比 + return velocity_diff * viscosity * weight + + @staticmethod + def calculate_surface_tension_force(normal, surface_tension, curvature): + """ + 计算表面张力力 + + Args: + normal (Vec3): 表面法线 + surface_tension (float): 表面张力系数 + curvature (float): 曲率 + + Returns: + Vec3: 表面张力力 + """ + # 表面张力力与曲率和法线方向有关 + return normal * surface_tension * curvature + + @staticmethod + def calculate_buoyancy(density_diff, gravity): + """ + 计算浮力 + + Args: + density_diff (float): 密度差 + gravity (Vec3): 重力向量 + + Returns: + Vec3: 浮力 + """ + # 浮力与密度差和重力成正比 + return gravity * density_diff + + @staticmethod + def calculate_drag_force(velocity, drag_coefficient, fluid_density, area): + """ + 计算阻力 + + Args: + velocity (Vec3): 速度向量 + drag_coefficient (float): 阻力系数 + fluid_density (float): 流体密度 + area (float): 参考面积 + + Returns: + Vec3: 阻力向量 + """ + speed = velocity.length() + if speed > 0: + # 阻力公式: F = 0.5 * ρ * v² * Cd * A + drag_magnitude = 0.5 * fluid_density * speed * speed * drag_coefficient * area + drag_direction = -velocity.normalized() + return drag_direction * drag_magnitude return Vec3(0, 0, 0) \ No newline at end of file diff --git a/plugins/user/fluid_dynamics/utils/performance_utils.py b/plugins/user/fluid_dynamics/utils/performance_utils.py index ea22a2c2..4b07258a 100644 --- a/plugins/user/fluid_dynamics/utils/performance_utils.py +++ b/plugins/user/fluid_dynamics/utils/performance_utils.py @@ -1,597 +1,597 @@ -""" -流体动力学插件性能优化模块 -提供性能优化、内存管理和调试功能 -""" - -import time -import psutil -import threading -import numpy as np -from collections import deque, defaultdict -import gc - -class FluidPerformanceOptimizer: - """ - 流体性能优化器 - 提供性能监控、优化和调试功能,包括内存管理、多线程处理和自适应优化 - """ - - def __init__(self, fluid_world): - """ - 初始化性能优化器 - - Args: - fluid_world: 流体世界对象的引用 - """ - self.fluid_world = fluid_world - self.optimization_enabled = True - - # 性能监控 - self.frame_times = deque(maxlen=100) - self.particle_counts = deque(maxlen=100) - self.update_times = deque(maxlen=100) - self.memory_usage = deque(maxlen=100) - - # 优化参数 - self.max_particles = 5000 - self.target_fps = 60 - self.adaptive_resolution = True - self.quality_level = 1.0 # 1.0为最高质量,0.1为最低质量 - - # 多线程支持 - self.use_multithreading = False - self.worker_threads = [] - self.thread_lock = threading.Lock() - self.thread_count = 2 - - # 内存管理 - self.object_pools = defaultdict(list) # 对象池 - self.max_pool_size = 1000 # 每个池的最大大小 - self.memory_limit_mb = 500 # 内存限制(MB) - - # 自适应优化 - self.adaptive_optimization = True - self.optimization_history = deque(maxlen=50) # 优化历史 - self.performance_trend = 0 # 性能趋势:正数表示改善,负数表示恶化 - - # 缓存系统 - self.cache_enabled = True - self.spatial_cache = {} # 空间缓存 - self.cache_ttl = 5.0 # 缓存生存时间(秒) - self.last_cache_cleanup = time.time() - - # 调试和分析 - self.debug_mode = False - self.profiling_enabled = False - self.profiling_data = defaultdict(list) - - print("流体性能优化器初始化完成") - - def update_performance_stats(self, update_time=0): - """ - 更新性能统计信息 - - Args: - update_time (float): 更新耗时 - """ - # 记录帧时间 - current_time = time.time() - if hasattr(self, 'last_update_time'): - frame_time = current_time - self.last_update_time - self.frame_times.append(frame_time) - - self.last_update_time = current_time - - # 记录粒子数量 - particle_count = len(self.fluid_world.fluid_particles) - self.particle_counts.append(particle_count) - - # 记录更新时间 - self.update_times.append(update_time) - - # 记录内存使用情况 - memory_mb = psutil.Process().memory_info().rss / 1024 / 1024 - self.memory_usage.append(memory_mb) - - # 更新优化历史 - self._update_optimization_history() - - def optimize_for_performance(self): - """ - 根据当前性能自适应优化 - """ - if not self.optimization_enabled: - return - - # 获取当前性能指标 - avg_frame_time = sum(self.frame_times) / len(self.frame_times) if self.frame_times else 0 - current_fps = 1.0 / avg_frame_time if avg_frame_time > 0 else 0 - current_particles = len(self.fluid_world.fluid_particles) - current_memory = self.memory_usage[-1] if self.memory_usage else 0 - - # 检查内存使用情况 - if current_memory > self.memory_limit_mb: - self._trigger_memory_cleanup() - - # 检查缓存是否需要清理 - if time.time() - self.last_cache_cleanup > self.cache_ttl: - self._cleanup_cache() - - # 根据FPS调整质量级别 - if current_fps < self.target_fps * 0.7: # 性能严重不足 - self.quality_level = max(0.1, self.quality_level * 0.9) - self._reduce_simulation_quality() - elif current_fps > self.target_fps * 1.1: # 性能良好 - self.quality_level = min(1.0, self.quality_level * 1.05) - self._increase_simulation_quality() - - # 根据粒子数量调整 - if current_particles > self.max_particles * 0.9: - self._reduce_particle_count() - elif current_particles < self.max_particles * 0.5: - self._increase_particle_count() - - # 根据系统资源调整 - cpu_percent = psutil.cpu_percent() - memory_percent = psutil.virtual_memory().percent - - if cpu_percent > 85: - self._reduce_computation_load() - if memory_percent > 85: - self._trigger_memory_cleanup() - - def _reduce_simulation_quality(self): - """ - 降低模拟质量以提高性能 - """ - # 降低求解器精度 - if hasattr(self.fluid_world, 'solver'): - solver = self.fluid_world.solver - solver.smoothing_radius *= 1.1 # 增大光滑半径减少计算量 - solver.time_step = min(solver.time_step * 1.1, 0.05) # 增大时间步长 - - # 降低可视化质量 - if hasattr(self.fluid_world, 'visualizer'): - visualizer = self.fluid_world.visualizer - visualizer.max_visible_particles = int(visualizer.max_visible_particles * 0.9) - - def _increase_simulation_quality(self): - """ - 提高模拟质量 - """ - # 提高求解器精度 - if hasattr(self.fluid_world, 'solver'): - solver = self.fluid_world.solver - solver.smoothing_radius = max(solver.smoothing_radius * 0.95, 0.5) - solver.time_step = max(solver.time_step * 0.95, 0.001) - - # 提高可视化质量 - if hasattr(self.fluid_world, 'visualizer'): - visualizer = self.fluid_world.visualizer - visualizer.max_visible_particles = min(int(visualizer.max_visible_particles * 1.1), 20000) - - def _reduce_particle_count(self): - """ - 减少粒子数量 - """ - current_particles = len(self.fluid_world.fluid_particles) - target_particles = int(current_particles * 0.9) - - # 移除多余的粒子 - while len(self.fluid_world.fluid_particles) > target_particles: - if self.fluid_world.fluid_particles: - # 回收粒子对象到对象池 - particle = self.fluid_world.fluid_particles.pop() - self._recycle_object('particle', particle) - - def _increase_particle_count(self): - """ - 增加粒子数量 - """ - current_particles = len(self.fluid_world.fluid_particles) - target_particles = int(current_particles * 1.1) - - # 从对象池获取粒子或创建新粒子 - for _ in range(target_particles - current_particles): - particle = self._get_object('particle') - if particle is None: - # 创建新粒子 - particle = { - 'position': Point3(0, 0, 0), - 'velocity': Vec3(0, 0, 0), - 'mass': 1.0, - 'density': 1000.0, - 'pressure': 0.0 - } - self.fluid_world.fluid_particles.append(particle) - - def _reduce_computation_load(self): - """ - 减少计算负载 - """ - # 降低更新频率 - if hasattr(self.fluid_world, 'visualizer'): - visualizer = self.fluid_world.visualizer - visualizer.update_frequency = max(visualizer.update_frequency * 0.9, 15) - - # 简化物理计算 - if hasattr(self.fluid_world, 'solver'): - solver = self.fluid_world.solver - solver.stiffness *= 0.95 # 降低刚度 - solver.viscosity *= 0.95 # 降低粘度计算复杂度 - - def adaptive_substepping(self, dt): - """ - 自适应子步长 - - Args: - dt (float): 帧时间增量 - - Returns: - int: 子步数 - """ - # 根据当前性能调整子步数 - avg_frame_time = sum(self.frame_times) / len(self.frame_times) if self.frame_times else 0 - current_fps = 1.0 / avg_frame_time if avg_frame_time > 0 else 0 - - # 如果性能良好,使用更多子步以提高精度 - if current_fps > self.target_fps * 0.9: - return min(10, int(self.fluid_world.time_step / dt) + 1) - else: - # 如果性能不佳,减少子步以保持稳定 - return max(1, int(self.fluid_world.time_step / dt * 0.5)) - - def get_performance_report(self): - """ - 获取详细的性能报告 - - Returns: - dict: 性能报告 - """ - avg_frame_time = sum(self.frame_times) / len(self.frame_times) if self.frame_times else 0 - current_fps = 1.0 / avg_frame_time if avg_frame_time > 0 else 0 - avg_update_time = sum(self.update_times) / len(self.update_times) if self.update_times else 0 - - # 获取系统资源使用情况 - cpu_percent = psutil.cpu_percent() - memory_percent = psutil.virtual_memory().percent - process_memory = psutil.Process().memory_info().rss / 1024 / 1024 # MB - - particle_count = len(self.fluid_world.fluid_particles) - - return { - 'performance': { - 'current_fps': current_fps, - 'avg_frame_time': avg_frame_time, - 'avg_update_time': avg_update_time, - 'quality_level': self.quality_level - }, - 'resources': { - 'cpu_percent': cpu_percent, - 'memory_percent': memory_percent, - 'process_memory_mb': process_memory - }, - 'fluid_objects_count': { - 'particles': particle_count, - 'domains': len(self.fluid_world.fluid_domains), - 'emitters': len(self.fluid_world.fluid_emitters), - 'obstacles': len(self.fluid_world.fluid_obstacles), - }, - 'optimization': { - 'enabled': self.optimization_enabled, - 'adaptive': self.adaptive_optimization, - 'multithreading': self.use_multithreading, - 'thread_count': len(self.worker_threads) - }, - 'memory': { - 'usage_mb': process_memory, - 'limit_mb': self.memory_limit_mb, - 'pool_sizes': {k: len(v) for k, v in self.object_pools.items()} - }, - 'recommended_actions': self._get_recommendations() - } - - def _get_recommendations(self): - """ - 获取性能优化建议 - - Returns: - list: 优化建议列表 - """ - recommendations = [] - - avg_frame_time = sum(self.frame_times) / len(self.frame_times) if self.frame_times else 0 - current_fps = 1.0 / avg_frame_time if avg_frame_time > 0 else 0 - particle_count = len(self.fluid_world.fluid_particles) - process_memory = psutil.Process().memory_info().rss / 1024 / 1024 # MB - - if current_fps < self.target_fps * 0.7: - recommendations.append(f"性能严重不足,当前FPS: {current_fps:.1f}") - - if particle_count > self.max_particles * 0.9: - recommendations.append(f"粒子数量过多 ({particle_count}),考虑减少粒子数量") - - if process_memory > self.memory_limit_mb * 0.9: - recommendations.append(f"内存使用接近限制 ({process_memory:.1f}MB / {self.memory_limit_mb}MB)") - - if psutil.cpu_percent() > 85: - recommendations.append("CPU使用率过高,考虑启用多线程") - - if psutil.virtual_memory().percent > 85: - recommendations.append("系统内存使用率过高") - - return recommendations - - def enable_multithreading(self, enabled=True, thread_count=2): - """ - 启用多线程处理 - - Args: - enabled (bool): 是否启用多线程 - thread_count (int): 线程数量 - """ - self.use_multithreading = enabled - self.thread_count = thread_count - - if enabled: - # 停止现有线程 - self._stop_worker_threads() - - # 创建工作线程 - for i in range(thread_count): - thread = threading.Thread(target=self._worker_thread, args=(i,)) - thread.daemon = True - thread.start() - self.worker_threads.append(thread) - - print(f"已启用多线程处理,线程数: {thread_count}") - else: - # 停止所有工作线程 - self._stop_worker_threads() - print("已禁用多线程处理") - - def _stop_worker_threads(self): - """ - 停止所有工作线程 - """ - self.use_multithreading = False - for thread in self.worker_threads: - thread.join(timeout=1.0) - self.worker_threads.clear() - - def _worker_thread(self, thread_id): - """ - 工作线程函数 - - Args: - thread_id (int): 线程ID - """ - while self.use_multithreading: - try: - # 处理流体粒子更新 - with self.thread_lock: - # 将粒子分配给不同线程处理 - particles = self.fluid_world.fluid_particles - if particles: - particles_per_thread = max(1, len(particles) // len(self.worker_threads)) - start_idx = thread_id * particles_per_thread - end_idx = min(start_idx + particles_per_thread, len(particles)) - - # 更新分配给此线程的粒子 - for i in range(start_idx, end_idx): - if i < len(particles): - particle = particles[i] - # 在这里执行粒子更新 - # 例如:应用重力、更新位置等 - pass - - # 短暂休眠以避免过度占用CPU - time.sleep(0.001) - except Exception as e: - if self.debug_mode: - print(f"工作线程 {thread_id} 出错: {e}") - time.sleep(0.01) # 出错后短暂休眠 - - def _update_optimization_history(self): - """ - 更新优化历史记录 - """ - if self.frame_times: - current_fps = 1.0 / self.frame_times[-1] if self.frame_times[-1] > 0 else 0 - self.optimization_history.append({ - 'timestamp': time.time(), - 'fps': current_fps, - 'particle_count': len(self.fluid_world.fluid_particles), - 'memory_mb': self.memory_usage[-1] if self.memory_usage else 0 - }) - - # 计算性能趋势 - if len(self.optimization_history) > 5: - recent_fps = [record['fps'] for record in list(self.optimization_history)[-5:]] - self.performance_trend = np.mean(np.diff(recent_fps)) if len(recent_fps) > 1 else 0 - - def enable_profiling(self, enabled=True): - """ - 启用性能分析 - - Args: - enabled (bool): 是否启用性能分析 - """ - self.profiling_enabled = enabled - if enabled: - print("性能分析已启用") - else: - print("性能分析已禁用") - - def profile_function(self, func_name, duration): - """ - 记录函数执行时间 - - Args: - func_name (str): 函数名称 - duration (float): 执行时间(秒) - """ - if self.profiling_enabled: - self.profiling_data[func_name].append(duration) - - def get_profiling_report(self): - """ - 获取性能分析报告 - - Returns: - dict: 性能分析报告 - """ - if not self.profiling_enabled: - return {} - - report = {} - for func_name, durations in self.profiling_data.items(): - if durations: - report[func_name] = { - 'count': len(durations), - 'total_time': sum(durations), - 'avg_time': np.mean(durations), - 'min_time': min(durations), - 'max_time': max(durations) - } - - return report - - def _trigger_memory_cleanup(self): - """ - 触发内存清理 - """ - # 清理对象池中过多的对象 - for obj_type, pool in self.object_pools.items(): - if len(pool) > self.max_pool_size: - # 保留一部分对象,删除多余的 - del pool[self.max_pool_size:] - - # 强制进行垃圾回收 - gc.collect() - - if self.debug_mode: - print("内存清理完成") - - def _cleanup_cache(self): - """ - 清理过期缓存 - """ - current_time = time.time() - expired_keys = [] - - for key, (value, timestamp) in self.spatial_cache.items(): - if current_time - timestamp > self.cache_ttl: - expired_keys.append(key) - - for key in expired_keys: - del self.spatial_cache[key] - - self.last_cache_cleanup = current_time - - def cache_spatial_query(self, query_key, result): - """ - 缓存空间查询结果 - - Args: - query_key: 查询键 - result: 查询结果 - """ - if self.cache_enabled: - self.spatial_cache[query_key] = (result, time.time()) - - def get_cached_spatial_query(self, query_key): - """ - 获取缓存的空间查询结果 - - Args: - query_key: 查询键 - - Returns: - 查询结果或None - """ - if not self.cache_enabled: - return None - - if query_key in self.spatial_cache: - result, timestamp = self.spatial_cache[query_key] - if time.time() - timestamp <= self.cache_ttl: - return result - else: - # 缓存过期,删除 - del self.spatial_cache[query_key] - - return None - - def _get_object(self, obj_type): - """ - 从对象池获取对象 - - Args: - obj_type (str): 对象类型 - - Returns: - 对象或None - """ - if obj_type in self.object_pools and self.object_pools[obj_type]: - return self.object_pools[obj_type].pop() - return None - - def _recycle_object(self, obj_type, obj): - """ - 回收对象到对象池 - - Args: - obj_type (str): 对象类型 - obj: 要回收的对象 - """ - if len(self.object_pools[obj_type]) < self.max_pool_size: - # 重置对象状态 - if obj_type == 'particle': - obj['position'] = Point3(0, 0, 0) - obj['velocity'] = Vec3(0, 0, 0) - obj['density'] = 1000.0 - obj['pressure'] = 0.0 - - self.object_pools[obj_type].append(obj) - - def set_memory_limit(self, limit_mb): - """ - 设置内存限制 - - Args: - limit_mb (float): 内存限制(MB) - """ - self.memory_limit_mb = limit_mb - - def set_quality_level(self, level): - """ - 设置质量级别 - - Args: - level (float): 质量级别 (0.1-1.0) - """ - self.quality_level = max(0.1, min(1.0, level)) - - def cleanup(self): - """ - 清理性能优化器资源 - """ - # 停止多线程 - self._stop_worker_threads() - - # 清理对象池 - self.object_pools.clear() - - # 清理缓存 - self.spatial_cache.clear() - - # 清理性能数据 - self.frame_times.clear() - self.particle_counts.clear() - self.update_times.clear() - self.memory_usage.clear() - self.optimization_history.clear() - self.profiling_data.clear() - +""" +流体动力学插件性能优化模块 +提供性能优化、内存管理和调试功能 +""" + +import time +import psutil +import threading +import numpy as np +from collections import deque, defaultdict +import gc + +class FluidPerformanceOptimizer: + """ + 流体性能优化器 + 提供性能监控、优化和调试功能,包括内存管理、多线程处理和自适应优化 + """ + + def __init__(self, fluid_world): + """ + 初始化性能优化器 + + Args: + fluid_world: 流体世界对象的引用 + """ + self.fluid_world = fluid_world + self.optimization_enabled = True + + # 性能监控 + self.frame_times = deque(maxlen=100) + self.particle_counts = deque(maxlen=100) + self.update_times = deque(maxlen=100) + self.memory_usage = deque(maxlen=100) + + # 优化参数 + self.max_particles = 5000 + self.target_fps = 60 + self.adaptive_resolution = True + self.quality_level = 1.0 # 1.0为最高质量,0.1为最低质量 + + # 多线程支持 + self.use_multithreading = False + self.worker_threads = [] + self.thread_lock = threading.Lock() + self.thread_count = 2 + + # 内存管理 + self.object_pools = defaultdict(list) # 对象池 + self.max_pool_size = 1000 # 每个池的最大大小 + self.memory_limit_mb = 500 # 内存限制(MB) + + # 自适应优化 + self.adaptive_optimization = True + self.optimization_history = deque(maxlen=50) # 优化历史 + self.performance_trend = 0 # 性能趋势:正数表示改善,负数表示恶化 + + # 缓存系统 + self.cache_enabled = True + self.spatial_cache = {} # 空间缓存 + self.cache_ttl = 5.0 # 缓存生存时间(秒) + self.last_cache_cleanup = time.time() + + # 调试和分析 + self.debug_mode = False + self.profiling_enabled = False + self.profiling_data = defaultdict(list) + + print("流体性能优化器初始化完成") + + def update_performance_stats(self, update_time=0): + """ + 更新性能统计信息 + + Args: + update_time (float): 更新耗时 + """ + # 记录帧时间 + current_time = time.time() + if hasattr(self, 'last_update_time'): + frame_time = current_time - self.last_update_time + self.frame_times.append(frame_time) + + self.last_update_time = current_time + + # 记录粒子数量 + particle_count = len(self.fluid_world.fluid_particles) + self.particle_counts.append(particle_count) + + # 记录更新时间 + self.update_times.append(update_time) + + # 记录内存使用情况 + memory_mb = psutil.Process().memory_info().rss / 1024 / 1024 + self.memory_usage.append(memory_mb) + + # 更新优化历史 + self._update_optimization_history() + + def optimize_for_performance(self): + """ + 根据当前性能自适应优化 + """ + if not self.optimization_enabled: + return + + # 获取当前性能指标 + avg_frame_time = sum(self.frame_times) / len(self.frame_times) if self.frame_times else 0 + current_fps = 1.0 / avg_frame_time if avg_frame_time > 0 else 0 + current_particles = len(self.fluid_world.fluid_particles) + current_memory = self.memory_usage[-1] if self.memory_usage else 0 + + # 检查内存使用情况 + if current_memory > self.memory_limit_mb: + self._trigger_memory_cleanup() + + # 检查缓存是否需要清理 + if time.time() - self.last_cache_cleanup > self.cache_ttl: + self._cleanup_cache() + + # 根据FPS调整质量级别 + if current_fps < self.target_fps * 0.7: # 性能严重不足 + self.quality_level = max(0.1, self.quality_level * 0.9) + self._reduce_simulation_quality() + elif current_fps > self.target_fps * 1.1: # 性能良好 + self.quality_level = min(1.0, self.quality_level * 1.05) + self._increase_simulation_quality() + + # 根据粒子数量调整 + if current_particles > self.max_particles * 0.9: + self._reduce_particle_count() + elif current_particles < self.max_particles * 0.5: + self._increase_particle_count() + + # 根据系统资源调整 + cpu_percent = psutil.cpu_percent() + memory_percent = psutil.virtual_memory().percent + + if cpu_percent > 85: + self._reduce_computation_load() + if memory_percent > 85: + self._trigger_memory_cleanup() + + def _reduce_simulation_quality(self): + """ + 降低模拟质量以提高性能 + """ + # 降低求解器精度 + if hasattr(self.fluid_world, 'solver'): + solver = self.fluid_world.solver + solver.smoothing_radius *= 1.1 # 增大光滑半径减少计算量 + solver.time_step = min(solver.time_step * 1.1, 0.05) # 增大时间步长 + + # 降低可视化质量 + if hasattr(self.fluid_world, 'visualizer'): + visualizer = self.fluid_world.visualizer + visualizer.max_visible_particles = int(visualizer.max_visible_particles * 0.9) + + def _increase_simulation_quality(self): + """ + 提高模拟质量 + """ + # 提高求解器精度 + if hasattr(self.fluid_world, 'solver'): + solver = self.fluid_world.solver + solver.smoothing_radius = max(solver.smoothing_radius * 0.95, 0.5) + solver.time_step = max(solver.time_step * 0.95, 0.001) + + # 提高可视化质量 + if hasattr(self.fluid_world, 'visualizer'): + visualizer = self.fluid_world.visualizer + visualizer.max_visible_particles = min(int(visualizer.max_visible_particles * 1.1), 20000) + + def _reduce_particle_count(self): + """ + 减少粒子数量 + """ + current_particles = len(self.fluid_world.fluid_particles) + target_particles = int(current_particles * 0.9) + + # 移除多余的粒子 + while len(self.fluid_world.fluid_particles) > target_particles: + if self.fluid_world.fluid_particles: + # 回收粒子对象到对象池 + particle = self.fluid_world.fluid_particles.pop() + self._recycle_object('particle', particle) + + def _increase_particle_count(self): + """ + 增加粒子数量 + """ + current_particles = len(self.fluid_world.fluid_particles) + target_particles = int(current_particles * 1.1) + + # 从对象池获取粒子或创建新粒子 + for _ in range(target_particles - current_particles): + particle = self._get_object('particle') + if particle is None: + # 创建新粒子 + particle = { + 'position': Point3(0, 0, 0), + 'velocity': Vec3(0, 0, 0), + 'mass': 1.0, + 'density': 1000.0, + 'pressure': 0.0 + } + self.fluid_world.fluid_particles.append(particle) + + def _reduce_computation_load(self): + """ + 减少计算负载 + """ + # 降低更新频率 + if hasattr(self.fluid_world, 'visualizer'): + visualizer = self.fluid_world.visualizer + visualizer.update_frequency = max(visualizer.update_frequency * 0.9, 15) + + # 简化物理计算 + if hasattr(self.fluid_world, 'solver'): + solver = self.fluid_world.solver + solver.stiffness *= 0.95 # 降低刚度 + solver.viscosity *= 0.95 # 降低粘度计算复杂度 + + def adaptive_substepping(self, dt): + """ + 自适应子步长 + + Args: + dt (float): 帧时间增量 + + Returns: + int: 子步数 + """ + # 根据当前性能调整子步数 + avg_frame_time = sum(self.frame_times) / len(self.frame_times) if self.frame_times else 0 + current_fps = 1.0 / avg_frame_time if avg_frame_time > 0 else 0 + + # 如果性能良好,使用更多子步以提高精度 + if current_fps > self.target_fps * 0.9: + return min(10, int(self.fluid_world.time_step / dt) + 1) + else: + # 如果性能不佳,减少子步以保持稳定 + return max(1, int(self.fluid_world.time_step / dt * 0.5)) + + def get_performance_report(self): + """ + 获取详细的性能报告 + + Returns: + dict: 性能报告 + """ + avg_frame_time = sum(self.frame_times) / len(self.frame_times) if self.frame_times else 0 + current_fps = 1.0 / avg_frame_time if avg_frame_time > 0 else 0 + avg_update_time = sum(self.update_times) / len(self.update_times) if self.update_times else 0 + + # 获取系统资源使用情况 + cpu_percent = psutil.cpu_percent() + memory_percent = psutil.virtual_memory().percent + process_memory = psutil.Process().memory_info().rss / 1024 / 1024 # MB + + particle_count = len(self.fluid_world.fluid_particles) + + return { + 'performance': { + 'current_fps': current_fps, + 'avg_frame_time': avg_frame_time, + 'avg_update_time': avg_update_time, + 'quality_level': self.quality_level + }, + 'resources': { + 'cpu_percent': cpu_percent, + 'memory_percent': memory_percent, + 'process_memory_mb': process_memory + }, + 'fluid_objects_count': { + 'particles': particle_count, + 'domains': len(self.fluid_world.fluid_domains), + 'emitters': len(self.fluid_world.fluid_emitters), + 'obstacles': len(self.fluid_world.fluid_obstacles), + }, + 'optimization': { + 'enabled': self.optimization_enabled, + 'adaptive': self.adaptive_optimization, + 'multithreading': self.use_multithreading, + 'thread_count': len(self.worker_threads) + }, + 'memory': { + 'usage_mb': process_memory, + 'limit_mb': self.memory_limit_mb, + 'pool_sizes': {k: len(v) for k, v in self.object_pools.items()} + }, + 'recommended_actions': self._get_recommendations() + } + + def _get_recommendations(self): + """ + 获取性能优化建议 + + Returns: + list: 优化建议列表 + """ + recommendations = [] + + avg_frame_time = sum(self.frame_times) / len(self.frame_times) if self.frame_times else 0 + current_fps = 1.0 / avg_frame_time if avg_frame_time > 0 else 0 + particle_count = len(self.fluid_world.fluid_particles) + process_memory = psutil.Process().memory_info().rss / 1024 / 1024 # MB + + if current_fps < self.target_fps * 0.7: + recommendations.append(f"性能严重不足,当前FPS: {current_fps:.1f}") + + if particle_count > self.max_particles * 0.9: + recommendations.append(f"粒子数量过多 ({particle_count}),考虑减少粒子数量") + + if process_memory > self.memory_limit_mb * 0.9: + recommendations.append(f"内存使用接近限制 ({process_memory:.1f}MB / {self.memory_limit_mb}MB)") + + if psutil.cpu_percent() > 85: + recommendations.append("CPU使用率过高,考虑启用多线程") + + if psutil.virtual_memory().percent > 85: + recommendations.append("系统内存使用率过高") + + return recommendations + + def enable_multithreading(self, enabled=True, thread_count=2): + """ + 启用多线程处理 + + Args: + enabled (bool): 是否启用多线程 + thread_count (int): 线程数量 + """ + self.use_multithreading = enabled + self.thread_count = thread_count + + if enabled: + # 停止现有线程 + self._stop_worker_threads() + + # 创建工作线程 + for i in range(thread_count): + thread = threading.Thread(target=self._worker_thread, args=(i,)) + thread.daemon = True + thread.start() + self.worker_threads.append(thread) + + print(f"已启用多线程处理,线程数: {thread_count}") + else: + # 停止所有工作线程 + self._stop_worker_threads() + print("已禁用多线程处理") + + def _stop_worker_threads(self): + """ + 停止所有工作线程 + """ + self.use_multithreading = False + for thread in self.worker_threads: + thread.join(timeout=1.0) + self.worker_threads.clear() + + def _worker_thread(self, thread_id): + """ + 工作线程函数 + + Args: + thread_id (int): 线程ID + """ + while self.use_multithreading: + try: + # 处理流体粒子更新 + with self.thread_lock: + # 将粒子分配给不同线程处理 + particles = self.fluid_world.fluid_particles + if particles: + particles_per_thread = max(1, len(particles) // len(self.worker_threads)) + start_idx = thread_id * particles_per_thread + end_idx = min(start_idx + particles_per_thread, len(particles)) + + # 更新分配给此线程的粒子 + for i in range(start_idx, end_idx): + if i < len(particles): + particle = particles[i] + # 在这里执行粒子更新 + # 例如:应用重力、更新位置等 + pass + + # 短暂休眠以避免过度占用CPU + time.sleep(0.001) + except Exception as e: + if self.debug_mode: + print(f"工作线程 {thread_id} 出错: {e}") + time.sleep(0.01) # 出错后短暂休眠 + + def _update_optimization_history(self): + """ + 更新优化历史记录 + """ + if self.frame_times: + current_fps = 1.0 / self.frame_times[-1] if self.frame_times[-1] > 0 else 0 + self.optimization_history.append({ + 'timestamp': time.time(), + 'fps': current_fps, + 'particle_count': len(self.fluid_world.fluid_particles), + 'memory_mb': self.memory_usage[-1] if self.memory_usage else 0 + }) + + # 计算性能趋势 + if len(self.optimization_history) > 5: + recent_fps = [record['fps'] for record in list(self.optimization_history)[-5:]] + self.performance_trend = np.mean(np.diff(recent_fps)) if len(recent_fps) > 1 else 0 + + def enable_profiling(self, enabled=True): + """ + 启用性能分析 + + Args: + enabled (bool): 是否启用性能分析 + """ + self.profiling_enabled = enabled + if enabled: + print("性能分析已启用") + else: + print("性能分析已禁用") + + def profile_function(self, func_name, duration): + """ + 记录函数执行时间 + + Args: + func_name (str): 函数名称 + duration (float): 执行时间(秒) + """ + if self.profiling_enabled: + self.profiling_data[func_name].append(duration) + + def get_profiling_report(self): + """ + 获取性能分析报告 + + Returns: + dict: 性能分析报告 + """ + if not self.profiling_enabled: + return {} + + report = {} + for func_name, durations in self.profiling_data.items(): + if durations: + report[func_name] = { + 'count': len(durations), + 'total_time': sum(durations), + 'avg_time': np.mean(durations), + 'min_time': min(durations), + 'max_time': max(durations) + } + + return report + + def _trigger_memory_cleanup(self): + """ + 触发内存清理 + """ + # 清理对象池中过多的对象 + for obj_type, pool in self.object_pools.items(): + if len(pool) > self.max_pool_size: + # 保留一部分对象,删除多余的 + del pool[self.max_pool_size:] + + # 强制进行垃圾回收 + gc.collect() + + if self.debug_mode: + print("内存清理完成") + + def _cleanup_cache(self): + """ + 清理过期缓存 + """ + current_time = time.time() + expired_keys = [] + + for key, (value, timestamp) in self.spatial_cache.items(): + if current_time - timestamp > self.cache_ttl: + expired_keys.append(key) + + for key in expired_keys: + del self.spatial_cache[key] + + self.last_cache_cleanup = current_time + + def cache_spatial_query(self, query_key, result): + """ + 缓存空间查询结果 + + Args: + query_key: 查询键 + result: 查询结果 + """ + if self.cache_enabled: + self.spatial_cache[query_key] = (result, time.time()) + + def get_cached_spatial_query(self, query_key): + """ + 获取缓存的空间查询结果 + + Args: + query_key: 查询键 + + Returns: + 查询结果或None + """ + if not self.cache_enabled: + return None + + if query_key in self.spatial_cache: + result, timestamp = self.spatial_cache[query_key] + if time.time() - timestamp <= self.cache_ttl: + return result + else: + # 缓存过期,删除 + del self.spatial_cache[query_key] + + return None + + def _get_object(self, obj_type): + """ + 从对象池获取对象 + + Args: + obj_type (str): 对象类型 + + Returns: + 对象或None + """ + if obj_type in self.object_pools and self.object_pools[obj_type]: + return self.object_pools[obj_type].pop() + return None + + def _recycle_object(self, obj_type, obj): + """ + 回收对象到对象池 + + Args: + obj_type (str): 对象类型 + obj: 要回收的对象 + """ + if len(self.object_pools[obj_type]) < self.max_pool_size: + # 重置对象状态 + if obj_type == 'particle': + obj['position'] = Point3(0, 0, 0) + obj['velocity'] = Vec3(0, 0, 0) + obj['density'] = 1000.0 + obj['pressure'] = 0.0 + + self.object_pools[obj_type].append(obj) + + def set_memory_limit(self, limit_mb): + """ + 设置内存限制 + + Args: + limit_mb (float): 内存限制(MB) + """ + self.memory_limit_mb = limit_mb + + def set_quality_level(self, level): + """ + 设置质量级别 + + Args: + level (float): 质量级别 (0.1-1.0) + """ + self.quality_level = max(0.1, min(1.0, level)) + + def cleanup(self): + """ + 清理性能优化器资源 + """ + # 停止多线程 + self._stop_worker_threads() + + # 清理对象池 + self.object_pools.clear() + + # 清理缓存 + self.spatial_cache.clear() + + # 清理性能数据 + self.frame_times.clear() + self.particle_counts.clear() + self.update_times.clear() + self.memory_usage.clear() + self.optimization_history.clear() + self.profiling_data.clear() + print("流体性能优化器资源已清理") \ No newline at end of file diff --git a/plugins/user/fluid_simulation/README.md b/plugins/user/fluid_simulation/README.md index 02d36e5f..c4fb7bbc 100644 --- a/plugins/user/fluid_simulation/README.md +++ b/plugins/user/fluid_simulation/README.md @@ -1,156 +1,156 @@ -# 水体和流体模拟插件 (Fluid Simulation Plugin) - -## 概述 - -水体和流体模拟插件为EG引擎提供了高质量的水体和流体模拟功能。该插件支持真实的流体物理模拟、动态波浪系统、高级渲染效果以及完整的编辑器工具。 - -## 功能特性 - -### 核心功能 -- **真实流体物理模拟**:基于Navier-Stokes方程的流体动力学模拟 -- **多种流体类型**:支持水、油、蜂蜜、酒精、岩浆等多种流体 -- **动态波浪系统**:风浪、涌浪、涟漪、海啸等多种波浪类型 -- **粒子系统**:基于粒子的流体模拟和渲染 -- **体积渲染**:高质量的体积流体渲染效果 - -### 渲染特性 -- **基于物理的渲染(PBR)**:真实的材质和光照效果 -- **动态反射和折射**:实时环境反射和光线折射 -- **焦散效果**:光线通过水体产生的聚焦效果 -- **泡沫系统**:动态泡沫和浪花效果 -- **水下效果**:水下雾化和光线效果 - -### 环境交互 -- **天气系统集成**:风力、降雨对水体的影响 -- **物体交互**:物体与流体的碰撞和交互 -- **地形适配**:自动适配复杂地形的水体表面 -- **性能优化**:LOD系统和动态细节调整 - -### 编辑工具 -- **可视化编辑器**:直观的参数调整界面 -- **预设管理系统**:保存和加载流体配置 -- **实时预览**:编辑时的实时效果预览 -- **性能监控**:实时性能统计和优化建议 - -## 目录结构 - -``` -fluid_simulation/ -├── plugin.py # 插件主文件 -├── core/ # 核心模块 -│ ├── fluid_manager.py # 流体管理器 -│ └── water_manager.py # 水体管理器 -├── fluids/ # 流体模拟模块 -│ ├── fluid_dynamics.py # 流体动力学 -│ └── fluid_solver.py # 流体求解器 -├── water/ # 水体模块 -│ ├── water_surface.py # 水体表面 -│ └── wave_system.py # 波浪系统 -├── rendering/ # 渲染模块 -│ ├── fluid_renderer.py # 流体渲染器 -│ └── water_renderer.py # 水体渲染器 -├── effects/ # 特效模块 -│ ├── fluid_effects.py # 流体特效 -│ └── water_effects.py # 水体特效 -├── interaction/ # 交互模块 -│ ├── fluid_interaction.py # 流体交互 -│ └── object_interaction.py # 物体交互 -├── audio/ # 音频模块 -│ └── fluid_audio.py # 流体音频 -├── particles/ # 粒子模块 -│ └── fluid_particles.py # 流体粒子 -├── editor/ # 编辑器模块 -│ ├── fluid_editor.py # 流体编辑器 -│ └── preset_manager.py # 预设管理器 -├── physics/ # 物理模块 -│ └── fluid_physics.py # 流体物理 -└── utils/ # 工具模块 - └── fluid_utils.py # 流体工具 -``` - -## 快速开始 - -### 安装插件 -将插件目录复制到EG引擎的插件目录中: - -``` -cp -r fluid_simulation /path/to/eg-engine/plugins/ -``` - -### 启用插件 -在EG引擎中启用插件: - -```python -# 在引擎初始化时加载插件 -engine.load_plugin('fluid_simulation') -``` - -### 基本使用 -```python -# 创建水体 -water_body = plugin.water_manager.create_water_body( - water_type='ocean', - position=(0, 0, 0), - size=(100, 100), - depth=10.0 -) - -# 创建流体区域 -fluid_region = plugin.fluid_manager.create_fluid_region( - fluid_type='water', - position=(0, 5, 0), - size=(10, 10, 10) -) - -# 设置波浪参数 -plugin.wave_system.create_wave( - wave_type='wind_wave', - parameters={ - 'amplitude': 1.0, - 'wavelength': 10.0, - 'speed': 2.0, - 'direction': math.pi/4 - } -) -``` - -## 高级功能 - -### 流体交互 -插件支持多种流体交互效果: -- 物体溅起水花 -- 流体对物体的浮力 -- 流体之间的混合 -- 热对流和密度差异 - -### 性能优化 -- 自适应时间步长 -- 空间分割优化 -- 多层次细节(LOD) -- GPU加速计算 - -### 自定义扩展 -插件采用模块化设计,支持: -- 自定义流体类型 -- 自定义波浪模型 -- 自定义渲染效果 -- 自定义交互行为 - -## API参考 - -详细API文档请参考各模块的源代码注释。 - -## 性能建议 - -1. **网格分辨率**:根据目标平台调整网格分辨率 -2. **特效设置**:在移动设备上适当降低特效质量 -3. **LOD使用**:启用LOD系统以优化远距离渲染 -4. **粒子数量**:控制粒子系统中的粒子数量 - -## 许可证 - -本插件遵循EG引擎的许可证协议。 - -## 支持与反馈 - +# 水体和流体模拟插件 (Fluid Simulation Plugin) + +## 概述 + +水体和流体模拟插件为EG引擎提供了高质量的水体和流体模拟功能。该插件支持真实的流体物理模拟、动态波浪系统、高级渲染效果以及完整的编辑器工具。 + +## 功能特性 + +### 核心功能 +- **真实流体物理模拟**:基于Navier-Stokes方程的流体动力学模拟 +- **多种流体类型**:支持水、油、蜂蜜、酒精、岩浆等多种流体 +- **动态波浪系统**:风浪、涌浪、涟漪、海啸等多种波浪类型 +- **粒子系统**:基于粒子的流体模拟和渲染 +- **体积渲染**:高质量的体积流体渲染效果 + +### 渲染特性 +- **基于物理的渲染(PBR)**:真实的材质和光照效果 +- **动态反射和折射**:实时环境反射和光线折射 +- **焦散效果**:光线通过水体产生的聚焦效果 +- **泡沫系统**:动态泡沫和浪花效果 +- **水下效果**:水下雾化和光线效果 + +### 环境交互 +- **天气系统集成**:风力、降雨对水体的影响 +- **物体交互**:物体与流体的碰撞和交互 +- **地形适配**:自动适配复杂地形的水体表面 +- **性能优化**:LOD系统和动态细节调整 + +### 编辑工具 +- **可视化编辑器**:直观的参数调整界面 +- **预设管理系统**:保存和加载流体配置 +- **实时预览**:编辑时的实时效果预览 +- **性能监控**:实时性能统计和优化建议 + +## 目录结构 + +``` +fluid_simulation/ +├── plugin.py # 插件主文件 +├── core/ # 核心模块 +│ ├── fluid_manager.py # 流体管理器 +│ └── water_manager.py # 水体管理器 +├── fluids/ # 流体模拟模块 +│ ├── fluid_dynamics.py # 流体动力学 +│ └── fluid_solver.py # 流体求解器 +├── water/ # 水体模块 +│ ├── water_surface.py # 水体表面 +│ └── wave_system.py # 波浪系统 +├── rendering/ # 渲染模块 +│ ├── fluid_renderer.py # 流体渲染器 +│ └── water_renderer.py # 水体渲染器 +├── effects/ # 特效模块 +│ ├── fluid_effects.py # 流体特效 +│ └── water_effects.py # 水体特效 +├── interaction/ # 交互模块 +│ ├── fluid_interaction.py # 流体交互 +│ └── object_interaction.py # 物体交互 +├── audio/ # 音频模块 +│ └── fluid_audio.py # 流体音频 +├── particles/ # 粒子模块 +│ └── fluid_particles.py # 流体粒子 +├── editor/ # 编辑器模块 +│ ├── fluid_editor.py # 流体编辑器 +│ └── preset_manager.py # 预设管理器 +├── physics/ # 物理模块 +│ └── fluid_physics.py # 流体物理 +└── utils/ # 工具模块 + └── fluid_utils.py # 流体工具 +``` + +## 快速开始 + +### 安装插件 +将插件目录复制到EG引擎的插件目录中: + +``` +cp -r fluid_simulation /path/to/eg-engine/plugins/ +``` + +### 启用插件 +在EG引擎中启用插件: + +```python +# 在引擎初始化时加载插件 +engine.load_plugin('fluid_simulation') +``` + +### 基本使用 +```python +# 创建水体 +water_body = plugin.water_manager.create_water_body( + water_type='ocean', + position=(0, 0, 0), + size=(100, 100), + depth=10.0 +) + +# 创建流体区域 +fluid_region = plugin.fluid_manager.create_fluid_region( + fluid_type='water', + position=(0, 5, 0), + size=(10, 10, 10) +) + +# 设置波浪参数 +plugin.wave_system.create_wave( + wave_type='wind_wave', + parameters={ + 'amplitude': 1.0, + 'wavelength': 10.0, + 'speed': 2.0, + 'direction': math.pi/4 + } +) +``` + +## 高级功能 + +### 流体交互 +插件支持多种流体交互效果: +- 物体溅起水花 +- 流体对物体的浮力 +- 流体之间的混合 +- 热对流和密度差异 + +### 性能优化 +- 自适应时间步长 +- 空间分割优化 +- 多层次细节(LOD) +- GPU加速计算 + +### 自定义扩展 +插件采用模块化设计,支持: +- 自定义流体类型 +- 自定义波浪模型 +- 自定义渲染效果 +- 自定义交互行为 + +## API参考 + +详细API文档请参考各模块的源代码注释。 + +## 性能建议 + +1. **网格分辨率**:根据目标平台调整网格分辨率 +2. **特效设置**:在移动设备上适当降低特效质量 +3. **LOD使用**:启用LOD系统以优化远距离渲染 +4. **粒子数量**:控制粒子系统中的粒子数量 + +## 许可证 + +本插件遵循EG引擎的许可证协议。 + +## 支持与反馈 + 如有问题或建议,请联系开发团队或在社区论坛中讨论。 \ No newline at end of file diff --git a/plugins/user/fluid_simulation/core/fluid_manager.py b/plugins/user/fluid_simulation/core/fluid_manager.py index 2deee3d4..1ab47d4d 100644 --- a/plugins/user/fluid_simulation/core/fluid_manager.py +++ b/plugins/user/fluid_simulation/core/fluid_manager.py @@ -1,473 +1,473 @@ -""" -流体管理器 -负责管理流体模拟的整体状态和配置 -""" - -import time -from typing import Dict, Any, List, Optional -import math - -class FluidManager: - """ - 流体管理器 - 负责管理流体模拟的整体状态、配置和基本操作 - """ - - def __init__(self, plugin): - """ - 初始化流体管理器 - - Args: - plugin: 水体和流体模拟插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 流体类型定义 - self.fluid_types = { - 'water': { - 'name': '水', - 'description': '标准水体', - 'density': 1000.0, # kg/m³ - 'viscosity': 0.001, # Pa·s (动力粘度) - 'surface_tension': 0.072, # N/m - 'compressibility': 4.5e-10, # 1/Pa - 'color': (0.0, 0.3, 0.8, 0.7), # RGBA - 'refractive_index': 1.333 - }, - 'oil': { - 'name': '油', - 'description': '石油类产品', - 'density': 850.0, - 'viscosity': 0.1, - 'surface_tension': 0.03, - 'compressibility': 1.0e-9, - 'color': (0.2, 0.1, 0.05, 0.8), - 'refractive_index': 1.45 - }, - 'honey': { - 'name': '蜂蜜', - 'description': '高粘度流体', - 'density': 1420.0, - 'viscosity': 10.0, - 'surface_tension': 0.06, - 'compressibility': 2.0e-10, - 'color': (0.8, 0.6, 0.1, 0.9), - 'refractive_index': 1.50 - }, - 'alcohol': { - 'name': '酒精', - 'description': '低粘度流体', - 'density': 789.0, - 'viscosity': 0.0012, - 'surface_tension': 0.023, - 'compressibility': 1.1e-9, - 'color': (0.8, 0.9, 0.95, 0.6), - 'refractive_index': 1.36 - }, - 'lava': { - 'name': '岩浆', - 'description': '高温熔岩', - 'density': 3100.0, - 'viscosity': 1000.0, - 'surface_tension': 0.35, - 'compressibility': 1.0e-11, - 'color': (0.9, 0.3, 0.1, 0.8), - 'refractive_index': 1.8 - } - } - - # 流体模拟配置 - self.simulation_config = { - 'grid_resolution': 64, # 网格分辨率 - 'time_step': 0.016, # 时间步长(秒) - 'solver_iterations': 10, # 求解器迭代次数 - 'gravity': 9.81, # 重力加速度(m/s²) - 'boundary_condition': 'closed', # 边界条件 - 'simulation_speed': 1.0 # 模拟速度倍数 - } - - # 流体区域 - self.fluid_regions = {} - self.region_counter = 0 - - # 流体状态 - self.fluid_states = { - 'total_volume': 0.0, - 'total_mass': 0.0, - 'average_velocity': (0.0, 0.0, 0.0), - 'max_velocity': 0.0, - 'pressure_points': [] - } - - # 环境影响因子 - self.environment_factors = { - 'temperature': 20.0, # 温度(°C) - 'pressure': 101325.0, # 大气压(Pa) - 'wind_velocity': (0.0, 0.0, 0.0), # 风速(m/s) - 'gravity_direction': (0.0, -1.0, 0.0) # 重力方向 - } - - # 统计信息 - self.stats = { - 'regions_created': 0, - 'simulation_steps': 0, - 'solver_calls': 0, - 'boundary_collisions': 0 - } - - print("✓ 流体管理器已创建") - - def initialize(self) -> bool: - """ - 初始化流体管理器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 流体管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 流体管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用流体管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 流体管理器未初始化") - return False - - self.enabled = True - print("✓ 流体管理器已启用") - return True - - except Exception as e: - print(f"✗ 流体管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用流体管理器""" - try: - self.enabled = False - print("✓ 流体管理器已禁用") - - except Exception as e: - print(f"✗ 流体管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理流体管理器资源""" - try: - self.disable() - self.fluid_regions.clear() - self.initialized = False - print("✓ 流体管理器资源已清理") - - except Exception as e: - print(f"✗ 流体管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新流体管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新统计信息 - self.stats['simulation_steps'] += 1 - - except Exception as e: - print(f"✗ 流体管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def get_fluid_types(self) -> List[str]: - """ - 获取所有流体类型 - - Returns: - 流体类型列表 - """ - return list(self.fluid_types.keys()) - - def get_fluid_info(self, fluid_type: str) -> Optional[Dict[str, Any]]: - """ - 获取流体类型信息 - - Args: - fluid_type: 流体类型 - - Returns: - 流体信息字典或None - """ - return self.fluid_types.get(fluid_type) - - def create_fluid_region(self, fluid_type: str, position: tuple, - size: tuple, initial_velocity: tuple = (0.0, 0.0, 0.0)) -> int: - """ - 创建流体区域 - - Args: - fluid_type: 流体类型 - position: 位置坐标 (x, y, z) - size: 区域大小 (width, height, depth) - initial_velocity: 初始速度 (vx, vy, vz) - - Returns: - 流体区域ID - """ - try: - if fluid_type not in self.fluid_types: - print(f"✗ 无效的流体类型: {fluid_type}") - return -1 - - region_id = self.region_counter - self.region_counter += 1 - - # 获取流体类型信息 - fluid_info = self.fluid_types[fluid_type] - - # 计算体积和质量 - volume = size[0] * size[1] * size[2] - mass = volume * fluid_info['density'] - - # 创建区域数据 - region_data = { - 'id': region_id, - 'type': fluid_type, - 'position': position, - 'size': size, - 'volume': volume, - 'mass': mass, - 'velocity': initial_velocity, - 'active': True, - 'creation_time': time.time(), - 'last_update': time.time() - } - - # 存储区域 - self.fluid_regions[region_id] = region_data - - # 更新统计数据 - self.stats['regions_created'] += 1 - self.fluid_states['total_volume'] += volume - self.fluid_states['total_mass'] += mass - - print(f"✓ 流体区域已创建: {fluid_info['name']} (ID: {region_id})") - return region_id - - except Exception as e: - print(f"✗ 流体区域创建失败: {e}") - import traceback - traceback.print_exc() - return -1 - - def remove_fluid_region(self, region_id: int) -> bool: - """ - 移除流体区域 - - Args: - region_id: 流体区域ID - - Returns: - 是否移除成功 - """ - try: - if region_id not in self.fluid_regions: - print(f"✗ 流体区域不存在: {region_id}") - return False - - region_data = self.fluid_regions[region_id] - - # 更新统计数据 - self.fluid_states['total_volume'] -= region_data['volume'] - self.fluid_states['total_mass'] -= region_data['mass'] - - # 移除区域 - del self.fluid_regions[region_id] - - print(f"✓ 流体区域已移除: ID {region_id}") - return True - - except Exception as e: - print(f"✗ 流体区域移除失败: {e}") - import traceback - traceback.print_exc() - return False - - def get_fluid_region(self, region_id: int) -> Optional[Dict[str, Any]]: - """ - 获取流体区域信息 - - Args: - region_id: 流体区域ID - - Returns: - 流体区域信息或None - """ - return self.fluid_regions.get(region_id) - - def get_all_fluid_regions(self) -> Dict[int, Dict[str, Any]]: - """ - 获取所有流体区域 - - Returns: - 所有流体区域字典 - """ - return self.fluid_regions.copy() - - def update_fluid_region_velocity(self, region_id: int, velocity: tuple): - """ - 更新流体区域速度 - - Args: - region_id: 流体区域ID - velocity: 速度向量 (vx, vy, vz) - """ - try: - if region_id not in self.fluid_regions: - return - - self.fluid_regions[region_id]['velocity'] = velocity - self.fluid_regions[region_id]['last_update'] = time.time() - - except Exception as e: - print(f"✗ 流体区域速度更新失败: {e}") - - def update_environment_factors(self, factors: Dict[str, Any]): - """ - 更新环境因子 - - Args: - factors: 环境因子字典 - """ - self.environment_factors.update(factors) - print(f"✓ 环境因子已更新: {factors}") - - def get_environment_factors(self) -> Dict[str, Any]: - """ - 获取环境因子 - - Returns: - 环境因子字典 - """ - return self.environment_factors.copy() - - def set_simulation_config(self, config: Dict[str, Any]): - """ - 设置模拟配置 - - Args: - config: 配置字典 - """ - self.simulation_config.update(config) - print(f"✓ 模拟配置已更新: {self.simulation_config}") - - def get_simulation_config(self) -> Dict[str, Any]: - """ - 获取模拟配置 - - Returns: - 模拟配置字典 - """ - return self.simulation_config.copy() - - def get_fluid_states(self) -> Dict[str, Any]: - """ - 获取流体状态 - - Returns: - 流体状态字典 - """ - return self.fluid_states.copy() - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'regions_created': 0, - 'simulation_steps': 0, - 'solver_calls': 0, - 'boundary_collisions': 0 - } - print("✓ 流体管理器统计信息已重置") - - def calculate_reynolds_number(self, fluid_type: str, velocity: float, - characteristic_length: float) -> float: - """ - 计算雷诺数 - - Args: - fluid_type: 流体类型 - velocity: 流速 - characteristic_length: 特征长度 - - Returns: - 雷诺数 - """ - try: - if fluid_type not in self.fluid_types: - return 0.0 - - fluid_info = self.fluid_types[fluid_type] - density = fluid_info['density'] - viscosity = fluid_info['viscosity'] - - # 雷诺数 = (密度 × 速度 × 特征长度) / 动力粘度 - reynolds = (density * velocity * characteristic_length) / viscosity - return reynolds - - except Exception as e: - print(f"✗ 雷诺数计算失败: {e}") - return 0.0 - - def calculate_flow_regime(self, reynolds_number: float) -> str: - """ - 根据雷诺数判断流动状态 - - Args: - reynolds_number: 雷诺数 - - Returns: - 流动状态 ('laminar', 'transitional', 'turbulent') - """ - try: - if reynolds_number < 2000: - return 'laminar' # 层流 - elif reynolds_number < 4000: - return 'transitional' # 过渡流 - else: - return 'turbulent' # 湍流 - - except Exception as e: - print(f"✗ 流动状态判断失败: {e}") +""" +流体管理器 +负责管理流体模拟的整体状态和配置 +""" + +import time +from typing import Dict, Any, List, Optional +import math + +class FluidManager: + """ + 流体管理器 + 负责管理流体模拟的整体状态、配置和基本操作 + """ + + def __init__(self, plugin): + """ + 初始化流体管理器 + + Args: + plugin: 水体和流体模拟插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 流体类型定义 + self.fluid_types = { + 'water': { + 'name': '水', + 'description': '标准水体', + 'density': 1000.0, # kg/m³ + 'viscosity': 0.001, # Pa·s (动力粘度) + 'surface_tension': 0.072, # N/m + 'compressibility': 4.5e-10, # 1/Pa + 'color': (0.0, 0.3, 0.8, 0.7), # RGBA + 'refractive_index': 1.333 + }, + 'oil': { + 'name': '油', + 'description': '石油类产品', + 'density': 850.0, + 'viscosity': 0.1, + 'surface_tension': 0.03, + 'compressibility': 1.0e-9, + 'color': (0.2, 0.1, 0.05, 0.8), + 'refractive_index': 1.45 + }, + 'honey': { + 'name': '蜂蜜', + 'description': '高粘度流体', + 'density': 1420.0, + 'viscosity': 10.0, + 'surface_tension': 0.06, + 'compressibility': 2.0e-10, + 'color': (0.8, 0.6, 0.1, 0.9), + 'refractive_index': 1.50 + }, + 'alcohol': { + 'name': '酒精', + 'description': '低粘度流体', + 'density': 789.0, + 'viscosity': 0.0012, + 'surface_tension': 0.023, + 'compressibility': 1.1e-9, + 'color': (0.8, 0.9, 0.95, 0.6), + 'refractive_index': 1.36 + }, + 'lava': { + 'name': '岩浆', + 'description': '高温熔岩', + 'density': 3100.0, + 'viscosity': 1000.0, + 'surface_tension': 0.35, + 'compressibility': 1.0e-11, + 'color': (0.9, 0.3, 0.1, 0.8), + 'refractive_index': 1.8 + } + } + + # 流体模拟配置 + self.simulation_config = { + 'grid_resolution': 64, # 网格分辨率 + 'time_step': 0.016, # 时间步长(秒) + 'solver_iterations': 10, # 求解器迭代次数 + 'gravity': 9.81, # 重力加速度(m/s²) + 'boundary_condition': 'closed', # 边界条件 + 'simulation_speed': 1.0 # 模拟速度倍数 + } + + # 流体区域 + self.fluid_regions = {} + self.region_counter = 0 + + # 流体状态 + self.fluid_states = { + 'total_volume': 0.0, + 'total_mass': 0.0, + 'average_velocity': (0.0, 0.0, 0.0), + 'max_velocity': 0.0, + 'pressure_points': [] + } + + # 环境影响因子 + self.environment_factors = { + 'temperature': 20.0, # 温度(°C) + 'pressure': 101325.0, # 大气压(Pa) + 'wind_velocity': (0.0, 0.0, 0.0), # 风速(m/s) + 'gravity_direction': (0.0, -1.0, 0.0) # 重力方向 + } + + # 统计信息 + self.stats = { + 'regions_created': 0, + 'simulation_steps': 0, + 'solver_calls': 0, + 'boundary_collisions': 0 + } + + print("✓ 流体管理器已创建") + + def initialize(self) -> bool: + """ + 初始化流体管理器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 流体管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 流体管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用流体管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 流体管理器未初始化") + return False + + self.enabled = True + print("✓ 流体管理器已启用") + return True + + except Exception as e: + print(f"✗ 流体管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用流体管理器""" + try: + self.enabled = False + print("✓ 流体管理器已禁用") + + except Exception as e: + print(f"✗ 流体管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理流体管理器资源""" + try: + self.disable() + self.fluid_regions.clear() + self.initialized = False + print("✓ 流体管理器资源已清理") + + except Exception as e: + print(f"✗ 流体管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新流体管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新统计信息 + self.stats['simulation_steps'] += 1 + + except Exception as e: + print(f"✗ 流体管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def get_fluid_types(self) -> List[str]: + """ + 获取所有流体类型 + + Returns: + 流体类型列表 + """ + return list(self.fluid_types.keys()) + + def get_fluid_info(self, fluid_type: str) -> Optional[Dict[str, Any]]: + """ + 获取流体类型信息 + + Args: + fluid_type: 流体类型 + + Returns: + 流体信息字典或None + """ + return self.fluid_types.get(fluid_type) + + def create_fluid_region(self, fluid_type: str, position: tuple, + size: tuple, initial_velocity: tuple = (0.0, 0.0, 0.0)) -> int: + """ + 创建流体区域 + + Args: + fluid_type: 流体类型 + position: 位置坐标 (x, y, z) + size: 区域大小 (width, height, depth) + initial_velocity: 初始速度 (vx, vy, vz) + + Returns: + 流体区域ID + """ + try: + if fluid_type not in self.fluid_types: + print(f"✗ 无效的流体类型: {fluid_type}") + return -1 + + region_id = self.region_counter + self.region_counter += 1 + + # 获取流体类型信息 + fluid_info = self.fluid_types[fluid_type] + + # 计算体积和质量 + volume = size[0] * size[1] * size[2] + mass = volume * fluid_info['density'] + + # 创建区域数据 + region_data = { + 'id': region_id, + 'type': fluid_type, + 'position': position, + 'size': size, + 'volume': volume, + 'mass': mass, + 'velocity': initial_velocity, + 'active': True, + 'creation_time': time.time(), + 'last_update': time.time() + } + + # 存储区域 + self.fluid_regions[region_id] = region_data + + # 更新统计数据 + self.stats['regions_created'] += 1 + self.fluid_states['total_volume'] += volume + self.fluid_states['total_mass'] += mass + + print(f"✓ 流体区域已创建: {fluid_info['name']} (ID: {region_id})") + return region_id + + except Exception as e: + print(f"✗ 流体区域创建失败: {e}") + import traceback + traceback.print_exc() + return -1 + + def remove_fluid_region(self, region_id: int) -> bool: + """ + 移除流体区域 + + Args: + region_id: 流体区域ID + + Returns: + 是否移除成功 + """ + try: + if region_id not in self.fluid_regions: + print(f"✗ 流体区域不存在: {region_id}") + return False + + region_data = self.fluid_regions[region_id] + + # 更新统计数据 + self.fluid_states['total_volume'] -= region_data['volume'] + self.fluid_states['total_mass'] -= region_data['mass'] + + # 移除区域 + del self.fluid_regions[region_id] + + print(f"✓ 流体区域已移除: ID {region_id}") + return True + + except Exception as e: + print(f"✗ 流体区域移除失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_fluid_region(self, region_id: int) -> Optional[Dict[str, Any]]: + """ + 获取流体区域信息 + + Args: + region_id: 流体区域ID + + Returns: + 流体区域信息或None + """ + return self.fluid_regions.get(region_id) + + def get_all_fluid_regions(self) -> Dict[int, Dict[str, Any]]: + """ + 获取所有流体区域 + + Returns: + 所有流体区域字典 + """ + return self.fluid_regions.copy() + + def update_fluid_region_velocity(self, region_id: int, velocity: tuple): + """ + 更新流体区域速度 + + Args: + region_id: 流体区域ID + velocity: 速度向量 (vx, vy, vz) + """ + try: + if region_id not in self.fluid_regions: + return + + self.fluid_regions[region_id]['velocity'] = velocity + self.fluid_regions[region_id]['last_update'] = time.time() + + except Exception as e: + print(f"✗ 流体区域速度更新失败: {e}") + + def update_environment_factors(self, factors: Dict[str, Any]): + """ + 更新环境因子 + + Args: + factors: 环境因子字典 + """ + self.environment_factors.update(factors) + print(f"✓ 环境因子已更新: {factors}") + + def get_environment_factors(self) -> Dict[str, Any]: + """ + 获取环境因子 + + Returns: + 环境因子字典 + """ + return self.environment_factors.copy() + + def set_simulation_config(self, config: Dict[str, Any]): + """ + 设置模拟配置 + + Args: + config: 配置字典 + """ + self.simulation_config.update(config) + print(f"✓ 模拟配置已更新: {self.simulation_config}") + + def get_simulation_config(self) -> Dict[str, Any]: + """ + 获取模拟配置 + + Returns: + 模拟配置字典 + """ + return self.simulation_config.copy() + + def get_fluid_states(self) -> Dict[str, Any]: + """ + 获取流体状态 + + Returns: + 流体状态字典 + """ + return self.fluid_states.copy() + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'regions_created': 0, + 'simulation_steps': 0, + 'solver_calls': 0, + 'boundary_collisions': 0 + } + print("✓ 流体管理器统计信息已重置") + + def calculate_reynolds_number(self, fluid_type: str, velocity: float, + characteristic_length: float) -> float: + """ + 计算雷诺数 + + Args: + fluid_type: 流体类型 + velocity: 流速 + characteristic_length: 特征长度 + + Returns: + 雷诺数 + """ + try: + if fluid_type not in self.fluid_types: + return 0.0 + + fluid_info = self.fluid_types[fluid_type] + density = fluid_info['density'] + viscosity = fluid_info['viscosity'] + + # 雷诺数 = (密度 × 速度 × 特征长度) / 动力粘度 + reynolds = (density * velocity * characteristic_length) / viscosity + return reynolds + + except Exception as e: + print(f"✗ 雷诺数计算失败: {e}") + return 0.0 + + def calculate_flow_regime(self, reynolds_number: float) -> str: + """ + 根据雷诺数判断流动状态 + + Args: + reynolds_number: 雷诺数 + + Returns: + 流动状态 ('laminar', 'transitional', 'turbulent') + """ + try: + if reynolds_number < 2000: + return 'laminar' # 层流 + elif reynolds_number < 4000: + return 'transitional' # 过渡流 + else: + return 'turbulent' # 湍流 + + except Exception as e: + print(f"✗ 流动状态判断失败: {e}") return 'unknown' \ No newline at end of file diff --git a/plugins/user/fluid_simulation/core/water_manager.py b/plugins/user/fluid_simulation/core/water_manager.py index 6a8a931a..3d3ff921 100644 --- a/plugins/user/fluid_simulation/core/water_manager.py +++ b/plugins/user/fluid_simulation/core/water_manager.py @@ -1,535 +1,535 @@ -""" -水体管理器 -负责管理水体的整体状态和配置 -""" - -import time -from typing import Dict, Any, List, Optional -import math - -class WaterManager: - """ - 水体管理器 - 负责管理水体的整体状态、配置和基本操作 - """ - - def __init__(self, plugin): - """ - 初始化水体管理器 - - Args: - plugin: 水体和流体模拟插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 水体类型定义 - self.water_types = { - 'ocean': { - 'name': '海洋', - 'description': '广阔的海水', - 'depth': 3000.0, # 平均深度(米) - 'wave_height': 2.0, # 平均波高(米) - 'wave_length': 50.0, # 平均波长(米) - 'wave_speed': 5.0, # 波速(m/s) - 'color': (0.0, 0.2, 0.6, 0.9), # RGBA - 'refractive_index': 1.333, - 'absorption_coefficient': 0.02 # 光吸收系数 - }, - 'lake': { - 'name': '湖泊', - 'description': '相对平静的淡水湖', - 'depth': 10.0, - 'wave_height': 0.3, - 'wave_length': 10.0, - 'wave_speed': 1.0, - 'color': (0.0, 0.4, 0.8, 0.85), - 'refractive_index': 1.333, - 'absorption_coefficient': 0.01 - }, - 'river': { - 'name': '河流', - 'description': '流动的淡水', - 'depth': 2.0, - 'wave_height': 0.1, - 'wave_length': 5.0, - 'wave_speed': 2.0, - 'color': (0.0, 0.5, 0.9, 0.8), - 'refractive_index': 1.333, - 'absorption_coefficient': 0.005 - }, - 'pool': { - 'name': '水池', - 'description': '人工水池', - 'depth': 1.5, - 'wave_height': 0.05, - 'wave_length': 2.0, - 'wave_speed': 0.5, - 'color': (0.1, 0.6, 0.9, 0.9), - 'refractive_index': 1.333, - 'absorption_coefficient': 0.002 - }, - 'swamp': { - 'name': '沼泽', - 'description': '浅水湿地', - 'depth': 0.5, - 'wave_height': 0.02, - 'wave_length': 1.0, - 'wave_speed': 0.2, - 'color': (0.1, 0.3, 0.2, 0.7), - 'refractive_index': 1.333, - 'absorption_coefficient': 0.05 - } - } - - # 水体配置 - self.water_config = { - 'resolution': 128, # 网格分辨率 - 'tessellation_factor': 4.0, # 细分因子 - 'lod_distance': 100.0, # LOD距离 - 'max_waves': 8, # 最大波浪数 - 'foam_threshold': 0.7, # 泡沫阈值 - 'reflection_quality': 'medium', # 反射质量 - 'refraction_quality': 'medium' # 折射质量 - } - - # 水体区域 - self.water_bodies = {} - self.body_counter = 0 - - # 水体状态 - self.water_states = { - 'total_area': 0.0, - 'total_volume': 0.0, - 'average_depth': 0.0, - 'surface_points': [], - 'underwater_points': [] - } - - # 环境影响因子 - self.environment_factors = { - 'wind_speed': 5.0, # 风速(m/s) - 'wind_direction': 0.0, # 风向(度) - 'temperature': 20.0, # 温度(°C) - 'pressure': 101325.0, # 大气压(Pa) - 'time_of_day': 12.0, # 时间(小时) - 'season': 'summer' # 季节 - } - - # 天气影响 - self.weather_effects = { - 'rain_intensity': 0.0, # 雨强度(0-1) - 'storm_intensity': 0.0, # 风暴强度(0-1) - 'fog_density': 0.0 # 雾密度(0-1) - } - - # 统计信息 - self.stats = { - 'bodies_created': 0, - 'vertices_generated': 0, - 'wave_updates': 0, - 'reflection_updates': 0 - } - - print("✓ 水体管理器已创建") - - def initialize(self) -> bool: - """ - 初始化水体管理器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 水体管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 水体管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用水体管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 水体管理器未初始化") - return False - - self.enabled = True - print("✓ 水体管理器已启用") - return True - - except Exception as e: - print(f"✗ 水体管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用水体管理器""" - try: - self.enabled = False - print("✓ 水体管理器已禁用") - - except Exception as e: - print(f"✗ 水体管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理水体管理器资源""" - try: - self.disable() - self.water_bodies.clear() - self.initialized = False - print("✓ 水体管理器资源已清理") - - except Exception as e: - print(f"✗ 水体管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新水体管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新统计信息 - self.stats['wave_updates'] += 1 - - except Exception as e: - print(f"✗ 水体管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def get_water_types(self) -> List[str]: - """ - 获取所有水体类型 - - Returns: - 水体类型列表 - """ - return list(self.water_types.keys()) - - def get_water_info(self, water_type: str) -> Optional[Dict[str, Any]]: - """ - 获取水体类型信息 - - Args: - water_type: 水体类型 - - Returns: - 水体信息字典或None - """ - return self.water_types.get(water_type) - - def create_water_body(self, water_type: str, position: tuple, - size: tuple, depth: float = None) -> int: - """ - 创建水体 - - Args: - water_type: 水体类型 - position: 位置坐标 (x, y, z) - size: 区域大小 (width, height) - depth: 深度(如果为None则使用默认值) - - Returns: - 水体ID - """ - try: - if water_type not in self.water_types: - print(f"✗ 无效的水体类型: {water_type}") - return -1 - - body_id = self.body_counter - self.body_counter += 1 - - # 获取水体类型信息 - water_info = self.water_types[water_type] - - # 使用指定深度或默认深度 - if depth is None: - depth = water_info['depth'] - - # 计算面积和体积 - area = size[0] * size[1] - volume = area * depth - - # 创建水体数据 - body_data = { - 'id': body_id, - 'type': water_type, - 'position': position, - 'size': size, - 'depth': depth, - 'area': area, - 'volume': volume, - 'active': True, - 'creation_time': time.time(), - 'last_update': time.time(), - 'wave_parameters': { - 'height': water_info['wave_height'], - 'length': water_info['wave_length'], - 'speed': water_info['wave_speed'] - } - } - - # 存储水体 - self.water_bodies[body_id] = body_data - - # 更新统计数据 - self.stats['bodies_created'] += 1 - self.water_states['total_area'] += area - self.water_states['total_volume'] += volume - - print(f"✓ 水体已创建: {water_info['name']} (ID: {body_id})") - return body_id - - except Exception as e: - print(f"✗ 水体创建失败: {e}") - import traceback - traceback.print_exc() - return -1 - - def remove_water_body(self, body_id: int) -> bool: - """ - 移除水体 - - Args: - body_id: 水体ID - - Returns: - 是否移除成功 - """ - try: - if body_id not in self.water_bodies: - print(f"✗ 水体不存在: {body_id}") - return False - - body_data = self.water_bodies[body_id] - - # 更新统计数据 - self.water_states['total_area'] -= body_data['area'] - self.water_states['total_volume'] -= body_data['volume'] - - # 移除水体 - del self.water_bodies[body_id] - - print(f"✓ 水体已移除: ID {body_id}") - return True - - except Exception as e: - print(f"✗ 水体移除失败: {e}") - import traceback - traceback.print_exc() - return False - - def get_water_body(self, body_id: int) -> Optional[Dict[str, Any]]: - """ - 获取水体信息 - - Args: - body_id: 水体ID - - Returns: - 水体信息或None - """ - return self.water_bodies.get(body_id) - - def get_all_water_bodies(self) -> Dict[int, Dict[str, Any]]: - """ - 获取所有水体 - - Returns: - 所有水体字典 - """ - return self.water_bodies.copy() - - def update_water_wave_parameters(self, body_id: int, parameters: Dict[str, float]): - """ - 更新水体波浪参数 - - Args: - body_id: 水体ID - parameters: 波浪参数字典 - """ - try: - if body_id not in self.water_bodies: - return - - if 'wave_parameters' not in self.water_bodies[body_id]: - self.water_bodies[body_id]['wave_parameters'] = {} - - self.water_bodies[body_id]['wave_parameters'].update(parameters) - self.water_bodies[body_id]['last_update'] = time.time() - - except Exception as e: - print(f"✗ 水体波浪参数更新失败: {e}") - - def update_environment_factors(self, factors: Dict[str, Any]): - """ - 更新环境因子 - - Args: - factors: 环境因子字典 - """ - self.environment_factors.update(factors) - print(f"✓ 环境因子已更新: {factors}") - - def get_environment_factors(self) -> Dict[str, Any]: - """ - 获取环境因子 - - Returns: - 环境因子字典 - """ - return self.environment_factors.copy() - - def update_weather_effects(self, effects: Dict[str, float]): - """ - 更新天气效果 - - Args: - effects: 天气效果字典 - """ - self.weather_effects.update(effects) - print(f"✓ 天气效果已更新: {effects}") - - def get_weather_effects(self) -> Dict[str, float]: - """ - 获取天气效果 - - Returns: - 天气效果字典 - """ - return self.weather_effects.copy() - - def set_water_config(self, config: Dict[str, Any]): - """ - 设置水体配置 - - Args: - config: 配置字典 - """ - self.water_config.update(config) - print(f"✓ 水体配置已更新: {self.water_config}") - - def get_water_config(self) -> Dict[str, Any]: - """ - 获取水体配置 - - Returns: - 水体配置字典 - """ - return self.water_config.copy() - - def get_water_states(self) -> Dict[str, Any]: - """ - 获取水体状态 - - Returns: - 水体状态字典 - """ - return self.water_states.copy() - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'bodies_created': 0, - 'vertices_generated': 0, - 'wave_updates': 0, - 'reflection_updates': 0 - } - print("✓ 水体管理器统计信息已重置") - - def calculate_wave_energy(self, body_id: int) -> float: - """ - 计算波浪能量 - - Args: - body_id: 水体ID - - Returns: - 波浪能量 - """ - try: - if body_id not in self.water_bodies: - return 0.0 - - body_data = self.water_bodies[body_id] - wave_params = body_data.get('wave_parameters', {}) - - height = wave_params.get('height', 0.0) - speed = wave_params.get('speed', 0.0) - - # 简化的波浪能量计算 (与波高的平方和波速成正比) - energy = 0.5 * 1000.0 * 9.81 * height * height * speed # ρgh²v/2 - return energy - - except Exception as e: - print(f"✗ 波浪能量计算失败: {e}") - return 0.0 - - def calculate_water_clarity(self, body_id: int) -> float: - """ - 计算水体清澈度 - - Args: - body_id: 水体ID - - Returns: - 水体清澈度 (0.0-1.0) - """ - try: - if body_id not in self.water_bodies: - return 0.0 - - body_data = self.water_bodies[body_id] - water_info = self.water_types[body_data['type']] - - # 基础清澈度 - clarity = 1.0 - water_info['absorption_coefficient'] * 10 - - # 受天气影响 - rain_factor = self.weather_effects['rain_intensity'] - storm_factor = self.weather_effects['storm_intensity'] - fog_factor = self.weather_effects['fog_density'] - - # 降雨和风暴会降低清澈度 - clarity *= (1.0 - rain_factor * 0.3) - clarity *= (1.0 - storm_factor * 0.5) - clarity *= (1.0 - fog_factor * 0.4) - - # 确保在有效范围内 - clarity = max(0.0, min(1.0, clarity)) - - return clarity - - except Exception as e: - print(f"✗ 水体清澈度计算失败: {e}") +""" +水体管理器 +负责管理水体的整体状态和配置 +""" + +import time +from typing import Dict, Any, List, Optional +import math + +class WaterManager: + """ + 水体管理器 + 负责管理水体的整体状态、配置和基本操作 + """ + + def __init__(self, plugin): + """ + 初始化水体管理器 + + Args: + plugin: 水体和流体模拟插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 水体类型定义 + self.water_types = { + 'ocean': { + 'name': '海洋', + 'description': '广阔的海水', + 'depth': 3000.0, # 平均深度(米) + 'wave_height': 2.0, # 平均波高(米) + 'wave_length': 50.0, # 平均波长(米) + 'wave_speed': 5.0, # 波速(m/s) + 'color': (0.0, 0.2, 0.6, 0.9), # RGBA + 'refractive_index': 1.333, + 'absorption_coefficient': 0.02 # 光吸收系数 + }, + 'lake': { + 'name': '湖泊', + 'description': '相对平静的淡水湖', + 'depth': 10.0, + 'wave_height': 0.3, + 'wave_length': 10.0, + 'wave_speed': 1.0, + 'color': (0.0, 0.4, 0.8, 0.85), + 'refractive_index': 1.333, + 'absorption_coefficient': 0.01 + }, + 'river': { + 'name': '河流', + 'description': '流动的淡水', + 'depth': 2.0, + 'wave_height': 0.1, + 'wave_length': 5.0, + 'wave_speed': 2.0, + 'color': (0.0, 0.5, 0.9, 0.8), + 'refractive_index': 1.333, + 'absorption_coefficient': 0.005 + }, + 'pool': { + 'name': '水池', + 'description': '人工水池', + 'depth': 1.5, + 'wave_height': 0.05, + 'wave_length': 2.0, + 'wave_speed': 0.5, + 'color': (0.1, 0.6, 0.9, 0.9), + 'refractive_index': 1.333, + 'absorption_coefficient': 0.002 + }, + 'swamp': { + 'name': '沼泽', + 'description': '浅水湿地', + 'depth': 0.5, + 'wave_height': 0.02, + 'wave_length': 1.0, + 'wave_speed': 0.2, + 'color': (0.1, 0.3, 0.2, 0.7), + 'refractive_index': 1.333, + 'absorption_coefficient': 0.05 + } + } + + # 水体配置 + self.water_config = { + 'resolution': 128, # 网格分辨率 + 'tessellation_factor': 4.0, # 细分因子 + 'lod_distance': 100.0, # LOD距离 + 'max_waves': 8, # 最大波浪数 + 'foam_threshold': 0.7, # 泡沫阈值 + 'reflection_quality': 'medium', # 反射质量 + 'refraction_quality': 'medium' # 折射质量 + } + + # 水体区域 + self.water_bodies = {} + self.body_counter = 0 + + # 水体状态 + self.water_states = { + 'total_area': 0.0, + 'total_volume': 0.0, + 'average_depth': 0.0, + 'surface_points': [], + 'underwater_points': [] + } + + # 环境影响因子 + self.environment_factors = { + 'wind_speed': 5.0, # 风速(m/s) + 'wind_direction': 0.0, # 风向(度) + 'temperature': 20.0, # 温度(°C) + 'pressure': 101325.0, # 大气压(Pa) + 'time_of_day': 12.0, # 时间(小时) + 'season': 'summer' # 季节 + } + + # 天气影响 + self.weather_effects = { + 'rain_intensity': 0.0, # 雨强度(0-1) + 'storm_intensity': 0.0, # 风暴强度(0-1) + 'fog_density': 0.0 # 雾密度(0-1) + } + + # 统计信息 + self.stats = { + 'bodies_created': 0, + 'vertices_generated': 0, + 'wave_updates': 0, + 'reflection_updates': 0 + } + + print("✓ 水体管理器已创建") + + def initialize(self) -> bool: + """ + 初始化水体管理器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 水体管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 水体管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用水体管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 水体管理器未初始化") + return False + + self.enabled = True + print("✓ 水体管理器已启用") + return True + + except Exception as e: + print(f"✗ 水体管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用水体管理器""" + try: + self.enabled = False + print("✓ 水体管理器已禁用") + + except Exception as e: + print(f"✗ 水体管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理水体管理器资源""" + try: + self.disable() + self.water_bodies.clear() + self.initialized = False + print("✓ 水体管理器资源已清理") + + except Exception as e: + print(f"✗ 水体管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新水体管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新统计信息 + self.stats['wave_updates'] += 1 + + except Exception as e: + print(f"✗ 水体管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def get_water_types(self) -> List[str]: + """ + 获取所有水体类型 + + Returns: + 水体类型列表 + """ + return list(self.water_types.keys()) + + def get_water_info(self, water_type: str) -> Optional[Dict[str, Any]]: + """ + 获取水体类型信息 + + Args: + water_type: 水体类型 + + Returns: + 水体信息字典或None + """ + return self.water_types.get(water_type) + + def create_water_body(self, water_type: str, position: tuple, + size: tuple, depth: float = None) -> int: + """ + 创建水体 + + Args: + water_type: 水体类型 + position: 位置坐标 (x, y, z) + size: 区域大小 (width, height) + depth: 深度(如果为None则使用默认值) + + Returns: + 水体ID + """ + try: + if water_type not in self.water_types: + print(f"✗ 无效的水体类型: {water_type}") + return -1 + + body_id = self.body_counter + self.body_counter += 1 + + # 获取水体类型信息 + water_info = self.water_types[water_type] + + # 使用指定深度或默认深度 + if depth is None: + depth = water_info['depth'] + + # 计算面积和体积 + area = size[0] * size[1] + volume = area * depth + + # 创建水体数据 + body_data = { + 'id': body_id, + 'type': water_type, + 'position': position, + 'size': size, + 'depth': depth, + 'area': area, + 'volume': volume, + 'active': True, + 'creation_time': time.time(), + 'last_update': time.time(), + 'wave_parameters': { + 'height': water_info['wave_height'], + 'length': water_info['wave_length'], + 'speed': water_info['wave_speed'] + } + } + + # 存储水体 + self.water_bodies[body_id] = body_data + + # 更新统计数据 + self.stats['bodies_created'] += 1 + self.water_states['total_area'] += area + self.water_states['total_volume'] += volume + + print(f"✓ 水体已创建: {water_info['name']} (ID: {body_id})") + return body_id + + except Exception as e: + print(f"✗ 水体创建失败: {e}") + import traceback + traceback.print_exc() + return -1 + + def remove_water_body(self, body_id: int) -> bool: + """ + 移除水体 + + Args: + body_id: 水体ID + + Returns: + 是否移除成功 + """ + try: + if body_id not in self.water_bodies: + print(f"✗ 水体不存在: {body_id}") + return False + + body_data = self.water_bodies[body_id] + + # 更新统计数据 + self.water_states['total_area'] -= body_data['area'] + self.water_states['total_volume'] -= body_data['volume'] + + # 移除水体 + del self.water_bodies[body_id] + + print(f"✓ 水体已移除: ID {body_id}") + return True + + except Exception as e: + print(f"✗ 水体移除失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_water_body(self, body_id: int) -> Optional[Dict[str, Any]]: + """ + 获取水体信息 + + Args: + body_id: 水体ID + + Returns: + 水体信息或None + """ + return self.water_bodies.get(body_id) + + def get_all_water_bodies(self) -> Dict[int, Dict[str, Any]]: + """ + 获取所有水体 + + Returns: + 所有水体字典 + """ + return self.water_bodies.copy() + + def update_water_wave_parameters(self, body_id: int, parameters: Dict[str, float]): + """ + 更新水体波浪参数 + + Args: + body_id: 水体ID + parameters: 波浪参数字典 + """ + try: + if body_id not in self.water_bodies: + return + + if 'wave_parameters' not in self.water_bodies[body_id]: + self.water_bodies[body_id]['wave_parameters'] = {} + + self.water_bodies[body_id]['wave_parameters'].update(parameters) + self.water_bodies[body_id]['last_update'] = time.time() + + except Exception as e: + print(f"✗ 水体波浪参数更新失败: {e}") + + def update_environment_factors(self, factors: Dict[str, Any]): + """ + 更新环境因子 + + Args: + factors: 环境因子字典 + """ + self.environment_factors.update(factors) + print(f"✓ 环境因子已更新: {factors}") + + def get_environment_factors(self) -> Dict[str, Any]: + """ + 获取环境因子 + + Returns: + 环境因子字典 + """ + return self.environment_factors.copy() + + def update_weather_effects(self, effects: Dict[str, float]): + """ + 更新天气效果 + + Args: + effects: 天气效果字典 + """ + self.weather_effects.update(effects) + print(f"✓ 天气效果已更新: {effects}") + + def get_weather_effects(self) -> Dict[str, float]: + """ + 获取天气效果 + + Returns: + 天气效果字典 + """ + return self.weather_effects.copy() + + def set_water_config(self, config: Dict[str, Any]): + """ + 设置水体配置 + + Args: + config: 配置字典 + """ + self.water_config.update(config) + print(f"✓ 水体配置已更新: {self.water_config}") + + def get_water_config(self) -> Dict[str, Any]: + """ + 获取水体配置 + + Returns: + 水体配置字典 + """ + return self.water_config.copy() + + def get_water_states(self) -> Dict[str, Any]: + """ + 获取水体状态 + + Returns: + 水体状态字典 + """ + return self.water_states.copy() + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'bodies_created': 0, + 'vertices_generated': 0, + 'wave_updates': 0, + 'reflection_updates': 0 + } + print("✓ 水体管理器统计信息已重置") + + def calculate_wave_energy(self, body_id: int) -> float: + """ + 计算波浪能量 + + Args: + body_id: 水体ID + + Returns: + 波浪能量 + """ + try: + if body_id not in self.water_bodies: + return 0.0 + + body_data = self.water_bodies[body_id] + wave_params = body_data.get('wave_parameters', {}) + + height = wave_params.get('height', 0.0) + speed = wave_params.get('speed', 0.0) + + # 简化的波浪能量计算 (与波高的平方和波速成正比) + energy = 0.5 * 1000.0 * 9.81 * height * height * speed # ρgh²v/2 + return energy + + except Exception as e: + print(f"✗ 波浪能量计算失败: {e}") + return 0.0 + + def calculate_water_clarity(self, body_id: int) -> float: + """ + 计算水体清澈度 + + Args: + body_id: 水体ID + + Returns: + 水体清澈度 (0.0-1.0) + """ + try: + if body_id not in self.water_bodies: + return 0.0 + + body_data = self.water_bodies[body_id] + water_info = self.water_types[body_data['type']] + + # 基础清澈度 + clarity = 1.0 - water_info['absorption_coefficient'] * 10 + + # 受天气影响 + rain_factor = self.weather_effects['rain_intensity'] + storm_factor = self.weather_effects['storm_intensity'] + fog_factor = self.weather_effects['fog_density'] + + # 降雨和风暴会降低清澈度 + clarity *= (1.0 - rain_factor * 0.3) + clarity *= (1.0 - storm_factor * 0.5) + clarity *= (1.0 - fog_factor * 0.4) + + # 确保在有效范围内 + clarity = max(0.0, min(1.0, clarity)) + + return clarity + + except Exception as e: + print(f"✗ 水体清澈度计算失败: {e}") return 0.5 \ No newline at end of file diff --git a/plugins/user/fluid_simulation/fluids/fluid_dynamics.py b/plugins/user/fluid_simulation/fluids/fluid_dynamics.py index 319ac8bb..774accab 100644 --- a/plugins/user/fluid_simulation/fluids/fluid_dynamics.py +++ b/plugins/user/fluid_simulation/fluids/fluid_dynamics.py @@ -1,707 +1,707 @@ -""" -流体动力学模块 -负责实现流体的基本物理模拟和动力学计算 -""" - -import time -from typing import Dict, Any, List, Optional, Tuple -import math -import numpy as np - -class FluidDynamics: - """ - 流体动力学模拟器 - 负责实现流体的基本物理模拟和动力学计算 - """ - - def __init__(self, plugin): - """ - 初始化流体动力学模拟器 - - Args: - plugin: 水体和流体模拟插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 流体网格配置 - self.grid_config = { - 'resolution_x': 64, - 'resolution_y': 64, - 'resolution_z': 64, - 'cell_size': 1.0, # 网格单元大小 - 'boundary_condition': 'closed' # 边界条件 - } - - # 物理参数 - self.physics_params = { - 'gravity': 9.81, # 重力加速度 (m/s²) - 'viscosity': 0.001, # 动力粘度 (Pa·s) - 'density': 1000.0, # 密度 (kg/m³) - 'time_step': 0.016, # 时间步长 (秒) - 'solver_iterations': 10 # 求解器迭代次数 - } - - # 流体状态场 - self.velocity_field = None # 速度场 - self.pressure_field = None # 压力场 - self.density_field = None # 密度场 - self.temperature_field = None # 温度场 - - # 粒子系统 - self.fluid_particles = [] - self.particle_counter = 0 - self.max_particles = 10000 - - # 边界条件 - self.boundary_conditions = { - 'left': 'closed', - 'right': 'closed', - 'top': 'closed', - 'bottom': 'closed', - 'front': 'closed', - 'back': 'closed' - } - - # 外力影响 - self.external_forces = { - 'gravity': (0.0, -9.81, 0.0), - 'wind': (0.0, 0.0, 0.0), - 'buoyancy': (0.0, 0.0, 0.0) - } - - # 模拟统计 - self.simulation_stats = { - 'steps_completed': 0, - 'particles_updated': 0, - 'solver_iterations': 0, - 'collision_count': 0 - } - - # 性能优化 - self.optimization_settings = { - 'adaptive_timestep': True, - 'spatial_partitioning': True, - 'particle_culling': True, - 'lod_simulation': True - } - - print("✓ 流体动力学模拟器已创建") - - def initialize(self) -> bool: - """ - 初始化流体动力学模拟器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化流体场 - self._initialize_fluid_fields() - - # 初始化粒子系统 - self._initialize_particle_system() - - self.initialized = True - print("✓ 流体动力学模拟器初始化完成") - return True - - except Exception as e: - print(f"✗ 流体动力学模拟器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用流体动力学模拟器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 流体动力学模拟器未初始化") - return False - - self.enabled = True - print("✓ 流体动力学模拟器已启用") - return True - - except Exception as e: - print(f"✗ 流体动力学模拟器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用流体动力学模拟器""" - try: - self.enabled = False - print("✓ 流体动力学模拟器已禁用") - - except Exception as e: - print(f"✗ 流体动力学模拟器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理流体动力学模拟器资源""" - try: - self.disable() - self._clear_fluid_fields() - self.fluid_particles.clear() - self.initialized = False - print("✓ 流体动力学模拟器资源已清理") - - except Exception as e: - print(f"✗ 流体动力学模拟器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新流体动力学模拟器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 根据实际时间步长调整模拟步长 - actual_dt = dt * self.plugin.fluid_manager.simulation_config['simulation_speed'] - - # 执行流体模拟步骤 - self._simulate_fluid_dynamics(actual_dt) - - # 更新统计信息 - self.simulation_stats['steps_completed'] += 1 - - except Exception as e: - print(f"✗ 流体动力学模拟器更新失败: {e}") - import traceback - traceback.print_exc() - - def _initialize_fluid_fields(self): - """初始化流体场""" - try: - res_x = self.grid_config['resolution_x'] - res_y = self.grid_config['resolution_y'] - res_z = self.grid_config['resolution_z'] - - # 初始化速度场 (每个维度一个分量) - self.velocity_field = { - 'u': np.zeros((res_x, res_y, res_z)), # x方向速度 - 'v': np.zeros((res_x, res_y, res_z)), # y方向速度 - 'w': np.zeros((res_x, res_y, res_z)) # z方向速度 - } - - # 初始化压力场 - self.pressure_field = np.zeros((res_x, res_y, res_z)) - - # 初始化密度场 - self.density_field = np.full((res_x, res_y, res_z), self.physics_params['density']) - - # 初始化温度场 - self.temperature_field = np.full((res_x, res_y, res_z), 20.0) # 20°C - - print("✓ 流体场初始化完成") - - except Exception as e: - print(f"✗ 流体场初始化失败: {e}") - raise - - def _clear_fluid_fields(self): - """清理流体场""" - try: - if self.velocity_field: - for component in self.velocity_field.values(): - component.fill(0) - self.velocity_field = None - - if self.pressure_field is not None: - self.pressure_field.fill(0) - self.pressure_field = None - - if self.density_field is not None: - self.density_field.fill(0) - self.density_field = None - - if self.temperature_field is not None: - self.temperature_field.fill(0) - self.temperature_field = None - - except Exception as e: - print(f"✗ 流体场清理失败: {e}") - - def _initialize_particle_system(self): - """初始化粒子系统""" - try: - # 预分配粒子数组 - self.fluid_particles = [] - self.particle_counter = 0 - - print("✓ 粒子系统初始化完成") - - except Exception as e: - print(f"✗ 粒子系统初始化失败: {e}") - raise - - def _simulate_fluid_dynamics(self, dt: float): - """ - 执行流体动力学模拟 - - Args: - dt: 时间步长 - """ - try: - # 应用外力 - self._apply_external_forces(dt) - - # 更新速度场 - self._update_velocity_field(dt) - - # 求解压力场 (确保不可压缩性) - self._solve_pressure_field() - - # 更新粒子 - self._update_fluid_particles(dt) - - # 处理边界条件 - self._apply_boundary_conditions() - - except Exception as e: - print(f"✗ 流体动力学模拟失败: {e}") - - def _apply_external_forces(self, dt: float): - """ - 应用外力到流体 - - Args: - dt: 时间步长 - """ - try: - if not self.velocity_field: - return - - res_x, res_y, res_z = self.velocity_field['u'].shape - - # 应用重力 - gravity = self.external_forces['gravity'] - self.velocity_field['v'] += gravity[1] * dt - - # 应用风力 - wind = self.external_forces['wind'] - self.velocity_field['u'] += wind[0] * dt - self.velocity_field['w'] += wind[2] * dt - - # 应用浮力 (基于温度和密度差异) - buoyancy = self.external_forces['buoyancy'] - self.velocity_field['v'] += buoyancy[1] * dt - - except Exception as e: - print(f"✗ 外力应用失败: {e}") - - def _update_velocity_field(self, dt: float): - """ - 更新速度场 - - Args: - dt: 时间步长 - """ - try: - if not self.velocity_field: - return - - # 添加粘性扩散项 - self._apply_viscosity(dt) - - # 添加对流项 - self._apply_advection(dt) - - except Exception as e: - print(f"✗ 速度场更新失败: {e}") - - def _apply_viscosity(self, dt: float): - """ - 应用粘性扩散 - - Args: - dt: 时间步长 - """ - try: - if not self.velocity_field: - return - - viscosity = self.physics_params['viscosity'] - cell_size = self.grid_config['cell_size'] - - # 简化的粘性扩散计算 (拉普拉斯算子) - # 在实际实现中,这会是一个更复杂的计算 - pass # 简化处理,实际实现中会计算粘性扩散 - - except Exception as e: - print(f"✗ 粘性扩散应用失败: {e}") - - def _apply_advection(self, dt: float): - """ - 应用对流项 - - Args: - dt: 时间步长 - """ - try: - if not self.velocity_field: - return - - # 简化的对流计算 - # 在实际实现中,这会使用半拉格朗日方法追踪粒子轨迹 - pass # 简化处理,实际实现中会计算对流 - - except Exception as e: - print(f"✗ 对流项应用失败: {e}") - - def _solve_pressure_field(self): - """求解压力场""" - try: - # 使用投影方法确保不可压缩性 - # 在实际实现中,这会使用泊松方程求解器 - self.simulation_stats['solver_iterations'] += self.physics_params['solver_iterations'] - - except Exception as e: - print(f"✗ 压力场求解失败: {e}") - - def _update_fluid_particles(self, dt: float): - """ - 更新流体粒子 - - Args: - dt: 时间步长 - """ - try: - updated_count = 0 - for particle in self.fluid_particles: - if particle['active']: - # 更新粒子位置 - velocity = particle['velocity'] - particle['position'] = ( - particle['position'][0] + velocity[0] * dt, - particle['position'][1] + velocity[1] * dt, - particle['position'][2] + velocity[2] * dt - ) - - # 应用重力 - particle['velocity'] = ( - velocity[0], - velocity[1] - self.physics_params['gravity'] * dt, - velocity[2] - ) - - updated_count += 1 - - self.simulation_stats['particles_updated'] += updated_count - - except Exception as e: - print(f"✗ 流体粒子更新失败: {e}") - - def _apply_boundary_conditions(self): - """应用边界条件""" - try: - # 根据边界条件设置边界值 - # 在实际实现中,这会处理各种边界条件 - pass - - except Exception as e: - print(f"✗ 边界条件应用失败: {e}") - - def add_fluid_particle(self, position: Tuple[float, float, float], - velocity: Tuple[float, float, float] = (0.0, 0.0, 0.0), - density: float = None) -> int: - """ - 添加流体粒子 - - Args: - position: 粒子位置 (x, y, z) - velocity: 粒子速度 (vx, vy, vz) - density: 粒子密度 - - Returns: - 粒子ID - """ - try: - if len(self.fluid_particles) >= self.max_particles: - print("✗ 粒子数量已达上限") - return -1 - - if density is None: - density = self.physics_params['density'] - - particle_id = self.particle_counter - self.particle_counter += 1 - - particle = { - 'id': particle_id, - 'position': position, - 'velocity': velocity, - 'density': density, - 'temperature': 20.0, - 'active': True, - 'creation_time': time.time() - } - - self.fluid_particles.append(particle) - - return particle_id - - except Exception as e: - print(f"✗ 流体粒子添加失败: {e}") - return -1 - - def remove_fluid_particle(self, particle_id: int) -> bool: - """ - 移除流体粒子 - - Args: - particle_id: 粒子ID - - Returns: - 是否移除成功 - """ - try: - for i, particle in enumerate(self.fluid_particles): - if particle['id'] == particle_id: - self.fluid_particles.pop(i) - return True - - print(f"✗ 粒子不存在: {particle_id}") - return False - - except Exception as e: - print(f"✗ 流体粒子移除失败: {e}") - return False - - def get_fluid_particle(self, particle_id: int) -> Optional[Dict[str, Any]]: - """ - 获取流体粒子信息 - - Args: - particle_id: 粒子ID - - Returns: - 粒子信息或None - """ - try: - for particle in self.fluid_particles: - if particle['id'] == particle_id: - return particle.copy() - return None - - except Exception as e: - print(f"✗ 流体粒子获取失败: {e}") - return None - - def set_external_force(self, force_name: str, force_vector: Tuple[float, float, float]): - """ - 设置外力 - - Args: - force_name: 力的名称 - force_vector: 力向量 (fx, fy, fz) - """ - try: - if force_name in self.external_forces: - self.external_forces[force_name] = force_vector - print(f"✓ 外力 '{force_name}' 已设置: {force_vector}") - else: - print(f"✗ 无效的外力名称: {force_name}") - - except Exception as e: - print(f"✗ 外力设置失败: {e}") - - def get_external_forces(self) -> Dict[str, Tuple[float, float, float]]: - """ - 获取所有外力 - - Returns: - 外力字典 - """ - return self.external_forces.copy() - - def set_physics_parameters(self, params: Dict[str, float]): - """ - 设置物理参数 - - Args: - params: 物理参数字典 - """ - try: - self.physics_params.update(params) - print(f"✓ 物理参数已更新: {self.physics_params}") - except Exception as e: - print(f"✗ 物理参数设置失败: {e}") - - def get_physics_parameters(self) -> Dict[str, float]: - """ - 获取物理参数 - - Returns: - 物理参数字典 - """ - return self.physics_params.copy() - - def set_grid_configuration(self, config: Dict[str, Any]): - """ - 设置网格配置 - - Args: - config: 网格配置字典 - """ - try: - old_resolution = ( - self.grid_config['resolution_x'], - self.grid_config['resolution_y'], - self.grid_config['resolution_z'] - ) - - self.grid_config.update(config) - - new_resolution = ( - self.grid_config['resolution_x'], - self.grid_config['resolution_y'], - self.grid_config['resolution_z'] - ) - - # 如果分辨率改变,重新初始化流体场 - if old_resolution != new_resolution: - self._initialize_fluid_fields() - - print(f"✓ 网格配置已更新: {self.grid_config}") - except Exception as e: - print(f"✗ 网格配置设置失败: {e}") - - def get_grid_configuration(self) -> Dict[str, Any]: - """ - 获取网格配置 - - Returns: - 网格配置字典 - """ - return self.grid_config.copy() - - def get_simulation_stats(self) -> Dict[str, int]: - """ - 获取模拟统计信息 - - Returns: - 统计信息字典 - """ - return self.simulation_stats.copy() - - def reset_simulation_stats(self): - """重置模拟统计信息""" - try: - self.simulation_stats = { - 'steps_completed': 0, - 'particles_updated': 0, - 'solver_iterations': 0, - 'collision_count': 0 - } - print("✓ 模拟统计信息已重置") - except Exception as e: - print(f"✗ 模拟统计信息重置失败: {e}") - - def get_fluid_velocity_at(self, x: int, y: int, z: int) -> Optional[Tuple[float, float, float]]: - """ - 获取指定位置的流体速度 - - Args: - x, y, z: 网格坐标 - - Returns: - 速度向量 (vx, vy, vz) 或 None - """ - try: - if not self.velocity_field: - return None - - shape = self.velocity_field['u'].shape - if 0 <= x < shape[0] and 0 <= y < shape[1] and 0 <= z < shape[2]: - vx = self.velocity_field['u'][x, y, z] - vy = self.velocity_field['v'][x, y, z] - vz = self.velocity_field['w'][x, y, z] - return (float(vx), float(vy), float(vz)) - - return None - - except Exception as e: - print(f"✗ 速度获取失败: {e}") - return None - - def set_fluid_velocity_at(self, x: int, y: int, z: int, velocity: Tuple[float, float, float]): - """ - 设置指定位置的流体速度 - - Args: - x, y, z: 网格坐标 - velocity: 速度向量 (vx, vy, vz) - """ - try: - if not self.velocity_field: - return - - shape = self.velocity_field['u'].shape - if 0 <= x < shape[0] and 0 <= y < shape[1] and 0 <= z < shape[2]: - self.velocity_field['u'][x, y, z] = velocity[0] - self.velocity_field['v'][x, y, z] = velocity[1] - self.velocity_field['w'][x, y, z] = velocity[2] - - except Exception as e: - print(f"✗ 速度设置失败: {e}") - - def calculate_cfl_condition(self) -> float: - """ - 计算CFL条件(柯朗-弗里德里希-李维条件) - - Returns: - CFL数 - """ - try: - if not self.velocity_field: - return 0.0 - - max_velocity = 0.0 - # 计算最大速度 - u_max = np.max(np.abs(self.velocity_field['u'])) - v_max = np.max(np.abs(self.velocity_field['v'])) - w_max = np.max(np.abs(self.velocity_field['w'])) - max_velocity = max(u_max, v_max, w_max) - - cell_size = self.grid_config['cell_size'] - dt = self.physics_params['time_step'] - - # CFL = (v * dt) / dx - cfl = (max_velocity * dt) / cell_size if cell_size > 0 else 0.0 - return cfl - - except Exception as e: - print(f"✗ CFL条件计算失败: {e}") - return 0.0 - - def is_stable(self) -> bool: - """ - 检查模拟是否稳定 - - Returns: - 是否稳定 - """ - try: - # CFL条件应小于等于1以保证稳定性 - cfl = self.calculate_cfl_condition() - return cfl <= 1.0 - - except Exception as e: - print(f"✗ 稳定性检查失败: {e}") +""" +流体动力学模块 +负责实现流体的基本物理模拟和动力学计算 +""" + +import time +from typing import Dict, Any, List, Optional, Tuple +import math +import numpy as np + +class FluidDynamics: + """ + 流体动力学模拟器 + 负责实现流体的基本物理模拟和动力学计算 + """ + + def __init__(self, plugin): + """ + 初始化流体动力学模拟器 + + Args: + plugin: 水体和流体模拟插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 流体网格配置 + self.grid_config = { + 'resolution_x': 64, + 'resolution_y': 64, + 'resolution_z': 64, + 'cell_size': 1.0, # 网格单元大小 + 'boundary_condition': 'closed' # 边界条件 + } + + # 物理参数 + self.physics_params = { + 'gravity': 9.81, # 重力加速度 (m/s²) + 'viscosity': 0.001, # 动力粘度 (Pa·s) + 'density': 1000.0, # 密度 (kg/m³) + 'time_step': 0.016, # 时间步长 (秒) + 'solver_iterations': 10 # 求解器迭代次数 + } + + # 流体状态场 + self.velocity_field = None # 速度场 + self.pressure_field = None # 压力场 + self.density_field = None # 密度场 + self.temperature_field = None # 温度场 + + # 粒子系统 + self.fluid_particles = [] + self.particle_counter = 0 + self.max_particles = 10000 + + # 边界条件 + self.boundary_conditions = { + 'left': 'closed', + 'right': 'closed', + 'top': 'closed', + 'bottom': 'closed', + 'front': 'closed', + 'back': 'closed' + } + + # 外力影响 + self.external_forces = { + 'gravity': (0.0, -9.81, 0.0), + 'wind': (0.0, 0.0, 0.0), + 'buoyancy': (0.0, 0.0, 0.0) + } + + # 模拟统计 + self.simulation_stats = { + 'steps_completed': 0, + 'particles_updated': 0, + 'solver_iterations': 0, + 'collision_count': 0 + } + + # 性能优化 + self.optimization_settings = { + 'adaptive_timestep': True, + 'spatial_partitioning': True, + 'particle_culling': True, + 'lod_simulation': True + } + + print("✓ 流体动力学模拟器已创建") + + def initialize(self) -> bool: + """ + 初始化流体动力学模拟器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化流体场 + self._initialize_fluid_fields() + + # 初始化粒子系统 + self._initialize_particle_system() + + self.initialized = True + print("✓ 流体动力学模拟器初始化完成") + return True + + except Exception as e: + print(f"✗ 流体动力学模拟器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用流体动力学模拟器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 流体动力学模拟器未初始化") + return False + + self.enabled = True + print("✓ 流体动力学模拟器已启用") + return True + + except Exception as e: + print(f"✗ 流体动力学模拟器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用流体动力学模拟器""" + try: + self.enabled = False + print("✓ 流体动力学模拟器已禁用") + + except Exception as e: + print(f"✗ 流体动力学模拟器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理流体动力学模拟器资源""" + try: + self.disable() + self._clear_fluid_fields() + self.fluid_particles.clear() + self.initialized = False + print("✓ 流体动力学模拟器资源已清理") + + except Exception as e: + print(f"✗ 流体动力学模拟器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新流体动力学模拟器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 根据实际时间步长调整模拟步长 + actual_dt = dt * self.plugin.fluid_manager.simulation_config['simulation_speed'] + + # 执行流体模拟步骤 + self._simulate_fluid_dynamics(actual_dt) + + # 更新统计信息 + self.simulation_stats['steps_completed'] += 1 + + except Exception as e: + print(f"✗ 流体动力学模拟器更新失败: {e}") + import traceback + traceback.print_exc() + + def _initialize_fluid_fields(self): + """初始化流体场""" + try: + res_x = self.grid_config['resolution_x'] + res_y = self.grid_config['resolution_y'] + res_z = self.grid_config['resolution_z'] + + # 初始化速度场 (每个维度一个分量) + self.velocity_field = { + 'u': np.zeros((res_x, res_y, res_z)), # x方向速度 + 'v': np.zeros((res_x, res_y, res_z)), # y方向速度 + 'w': np.zeros((res_x, res_y, res_z)) # z方向速度 + } + + # 初始化压力场 + self.pressure_field = np.zeros((res_x, res_y, res_z)) + + # 初始化密度场 + self.density_field = np.full((res_x, res_y, res_z), self.physics_params['density']) + + # 初始化温度场 + self.temperature_field = np.full((res_x, res_y, res_z), 20.0) # 20°C + + print("✓ 流体场初始化完成") + + except Exception as e: + print(f"✗ 流体场初始化失败: {e}") + raise + + def _clear_fluid_fields(self): + """清理流体场""" + try: + if self.velocity_field: + for component in self.velocity_field.values(): + component.fill(0) + self.velocity_field = None + + if self.pressure_field is not None: + self.pressure_field.fill(0) + self.pressure_field = None + + if self.density_field is not None: + self.density_field.fill(0) + self.density_field = None + + if self.temperature_field is not None: + self.temperature_field.fill(0) + self.temperature_field = None + + except Exception as e: + print(f"✗ 流体场清理失败: {e}") + + def _initialize_particle_system(self): + """初始化粒子系统""" + try: + # 预分配粒子数组 + self.fluid_particles = [] + self.particle_counter = 0 + + print("✓ 粒子系统初始化完成") + + except Exception as e: + print(f"✗ 粒子系统初始化失败: {e}") + raise + + def _simulate_fluid_dynamics(self, dt: float): + """ + 执行流体动力学模拟 + + Args: + dt: 时间步长 + """ + try: + # 应用外力 + self._apply_external_forces(dt) + + # 更新速度场 + self._update_velocity_field(dt) + + # 求解压力场 (确保不可压缩性) + self._solve_pressure_field() + + # 更新粒子 + self._update_fluid_particles(dt) + + # 处理边界条件 + self._apply_boundary_conditions() + + except Exception as e: + print(f"✗ 流体动力学模拟失败: {e}") + + def _apply_external_forces(self, dt: float): + """ + 应用外力到流体 + + Args: + dt: 时间步长 + """ + try: + if not self.velocity_field: + return + + res_x, res_y, res_z = self.velocity_field['u'].shape + + # 应用重力 + gravity = self.external_forces['gravity'] + self.velocity_field['v'] += gravity[1] * dt + + # 应用风力 + wind = self.external_forces['wind'] + self.velocity_field['u'] += wind[0] * dt + self.velocity_field['w'] += wind[2] * dt + + # 应用浮力 (基于温度和密度差异) + buoyancy = self.external_forces['buoyancy'] + self.velocity_field['v'] += buoyancy[1] * dt + + except Exception as e: + print(f"✗ 外力应用失败: {e}") + + def _update_velocity_field(self, dt: float): + """ + 更新速度场 + + Args: + dt: 时间步长 + """ + try: + if not self.velocity_field: + return + + # 添加粘性扩散项 + self._apply_viscosity(dt) + + # 添加对流项 + self._apply_advection(dt) + + except Exception as e: + print(f"✗ 速度场更新失败: {e}") + + def _apply_viscosity(self, dt: float): + """ + 应用粘性扩散 + + Args: + dt: 时间步长 + """ + try: + if not self.velocity_field: + return + + viscosity = self.physics_params['viscosity'] + cell_size = self.grid_config['cell_size'] + + # 简化的粘性扩散计算 (拉普拉斯算子) + # 在实际实现中,这会是一个更复杂的计算 + pass # 简化处理,实际实现中会计算粘性扩散 + + except Exception as e: + print(f"✗ 粘性扩散应用失败: {e}") + + def _apply_advection(self, dt: float): + """ + 应用对流项 + + Args: + dt: 时间步长 + """ + try: + if not self.velocity_field: + return + + # 简化的对流计算 + # 在实际实现中,这会使用半拉格朗日方法追踪粒子轨迹 + pass # 简化处理,实际实现中会计算对流 + + except Exception as e: + print(f"✗ 对流项应用失败: {e}") + + def _solve_pressure_field(self): + """求解压力场""" + try: + # 使用投影方法确保不可压缩性 + # 在实际实现中,这会使用泊松方程求解器 + self.simulation_stats['solver_iterations'] += self.physics_params['solver_iterations'] + + except Exception as e: + print(f"✗ 压力场求解失败: {e}") + + def _update_fluid_particles(self, dt: float): + """ + 更新流体粒子 + + Args: + dt: 时间步长 + """ + try: + updated_count = 0 + for particle in self.fluid_particles: + if particle['active']: + # 更新粒子位置 + velocity = particle['velocity'] + particle['position'] = ( + particle['position'][0] + velocity[0] * dt, + particle['position'][1] + velocity[1] * dt, + particle['position'][2] + velocity[2] * dt + ) + + # 应用重力 + particle['velocity'] = ( + velocity[0], + velocity[1] - self.physics_params['gravity'] * dt, + velocity[2] + ) + + updated_count += 1 + + self.simulation_stats['particles_updated'] += updated_count + + except Exception as e: + print(f"✗ 流体粒子更新失败: {e}") + + def _apply_boundary_conditions(self): + """应用边界条件""" + try: + # 根据边界条件设置边界值 + # 在实际实现中,这会处理各种边界条件 + pass + + except Exception as e: + print(f"✗ 边界条件应用失败: {e}") + + def add_fluid_particle(self, position: Tuple[float, float, float], + velocity: Tuple[float, float, float] = (0.0, 0.0, 0.0), + density: float = None) -> int: + """ + 添加流体粒子 + + Args: + position: 粒子位置 (x, y, z) + velocity: 粒子速度 (vx, vy, vz) + density: 粒子密度 + + Returns: + 粒子ID + """ + try: + if len(self.fluid_particles) >= self.max_particles: + print("✗ 粒子数量已达上限") + return -1 + + if density is None: + density = self.physics_params['density'] + + particle_id = self.particle_counter + self.particle_counter += 1 + + particle = { + 'id': particle_id, + 'position': position, + 'velocity': velocity, + 'density': density, + 'temperature': 20.0, + 'active': True, + 'creation_time': time.time() + } + + self.fluid_particles.append(particle) + + return particle_id + + except Exception as e: + print(f"✗ 流体粒子添加失败: {e}") + return -1 + + def remove_fluid_particle(self, particle_id: int) -> bool: + """ + 移除流体粒子 + + Args: + particle_id: 粒子ID + + Returns: + 是否移除成功 + """ + try: + for i, particle in enumerate(self.fluid_particles): + if particle['id'] == particle_id: + self.fluid_particles.pop(i) + return True + + print(f"✗ 粒子不存在: {particle_id}") + return False + + except Exception as e: + print(f"✗ 流体粒子移除失败: {e}") + return False + + def get_fluid_particle(self, particle_id: int) -> Optional[Dict[str, Any]]: + """ + 获取流体粒子信息 + + Args: + particle_id: 粒子ID + + Returns: + 粒子信息或None + """ + try: + for particle in self.fluid_particles: + if particle['id'] == particle_id: + return particle.copy() + return None + + except Exception as e: + print(f"✗ 流体粒子获取失败: {e}") + return None + + def set_external_force(self, force_name: str, force_vector: Tuple[float, float, float]): + """ + 设置外力 + + Args: + force_name: 力的名称 + force_vector: 力向量 (fx, fy, fz) + """ + try: + if force_name in self.external_forces: + self.external_forces[force_name] = force_vector + print(f"✓ 外力 '{force_name}' 已设置: {force_vector}") + else: + print(f"✗ 无效的外力名称: {force_name}") + + except Exception as e: + print(f"✗ 外力设置失败: {e}") + + def get_external_forces(self) -> Dict[str, Tuple[float, float, float]]: + """ + 获取所有外力 + + Returns: + 外力字典 + """ + return self.external_forces.copy() + + def set_physics_parameters(self, params: Dict[str, float]): + """ + 设置物理参数 + + Args: + params: 物理参数字典 + """ + try: + self.physics_params.update(params) + print(f"✓ 物理参数已更新: {self.physics_params}") + except Exception as e: + print(f"✗ 物理参数设置失败: {e}") + + def get_physics_parameters(self) -> Dict[str, float]: + """ + 获取物理参数 + + Returns: + 物理参数字典 + """ + return self.physics_params.copy() + + def set_grid_configuration(self, config: Dict[str, Any]): + """ + 设置网格配置 + + Args: + config: 网格配置字典 + """ + try: + old_resolution = ( + self.grid_config['resolution_x'], + self.grid_config['resolution_y'], + self.grid_config['resolution_z'] + ) + + self.grid_config.update(config) + + new_resolution = ( + self.grid_config['resolution_x'], + self.grid_config['resolution_y'], + self.grid_config['resolution_z'] + ) + + # 如果分辨率改变,重新初始化流体场 + if old_resolution != new_resolution: + self._initialize_fluid_fields() + + print(f"✓ 网格配置已更新: {self.grid_config}") + except Exception as e: + print(f"✗ 网格配置设置失败: {e}") + + def get_grid_configuration(self) -> Dict[str, Any]: + """ + 获取网格配置 + + Returns: + 网格配置字典 + """ + return self.grid_config.copy() + + def get_simulation_stats(self) -> Dict[str, int]: + """ + 获取模拟统计信息 + + Returns: + 统计信息字典 + """ + return self.simulation_stats.copy() + + def reset_simulation_stats(self): + """重置模拟统计信息""" + try: + self.simulation_stats = { + 'steps_completed': 0, + 'particles_updated': 0, + 'solver_iterations': 0, + 'collision_count': 0 + } + print("✓ 模拟统计信息已重置") + except Exception as e: + print(f"✗ 模拟统计信息重置失败: {e}") + + def get_fluid_velocity_at(self, x: int, y: int, z: int) -> Optional[Tuple[float, float, float]]: + """ + 获取指定位置的流体速度 + + Args: + x, y, z: 网格坐标 + + Returns: + 速度向量 (vx, vy, vz) 或 None + """ + try: + if not self.velocity_field: + return None + + shape = self.velocity_field['u'].shape + if 0 <= x < shape[0] and 0 <= y < shape[1] and 0 <= z < shape[2]: + vx = self.velocity_field['u'][x, y, z] + vy = self.velocity_field['v'][x, y, z] + vz = self.velocity_field['w'][x, y, z] + return (float(vx), float(vy), float(vz)) + + return None + + except Exception as e: + print(f"✗ 速度获取失败: {e}") + return None + + def set_fluid_velocity_at(self, x: int, y: int, z: int, velocity: Tuple[float, float, float]): + """ + 设置指定位置的流体速度 + + Args: + x, y, z: 网格坐标 + velocity: 速度向量 (vx, vy, vz) + """ + try: + if not self.velocity_field: + return + + shape = self.velocity_field['u'].shape + if 0 <= x < shape[0] and 0 <= y < shape[1] and 0 <= z < shape[2]: + self.velocity_field['u'][x, y, z] = velocity[0] + self.velocity_field['v'][x, y, z] = velocity[1] + self.velocity_field['w'][x, y, z] = velocity[2] + + except Exception as e: + print(f"✗ 速度设置失败: {e}") + + def calculate_cfl_condition(self) -> float: + """ + 计算CFL条件(柯朗-弗里德里希-李维条件) + + Returns: + CFL数 + """ + try: + if not self.velocity_field: + return 0.0 + + max_velocity = 0.0 + # 计算最大速度 + u_max = np.max(np.abs(self.velocity_field['u'])) + v_max = np.max(np.abs(self.velocity_field['v'])) + w_max = np.max(np.abs(self.velocity_field['w'])) + max_velocity = max(u_max, v_max, w_max) + + cell_size = self.grid_config['cell_size'] + dt = self.physics_params['time_step'] + + # CFL = (v * dt) / dx + cfl = (max_velocity * dt) / cell_size if cell_size > 0 else 0.0 + return cfl + + except Exception as e: + print(f"✗ CFL条件计算失败: {e}") + return 0.0 + + def is_stable(self) -> bool: + """ + 检查模拟是否稳定 + + Returns: + 是否稳定 + """ + try: + # CFL条件应小于等于1以保证稳定性 + cfl = self.calculate_cfl_condition() + return cfl <= 1.0 + + except Exception as e: + print(f"✗ 稳定性检查失败: {e}") return False \ No newline at end of file diff --git a/plugins/user/fluid_simulation/fluids/fluid_solver.py b/plugins/user/fluid_simulation/fluids/fluid_solver.py index 3176e978..a8749fd8 100644 --- a/plugins/user/fluid_simulation/fluids/fluid_solver.py +++ b/plugins/user/fluid_simulation/fluids/fluid_solver.py @@ -1,819 +1,819 @@ -""" -流体求解器模块 -负责实现流体模拟的数值求解算法 -""" - -import time -from typing import Dict, Any, List, Optional, Tuple -import math -import numpy as np -from scipy.sparse import diags -from scipy.sparse.linalg import spsolve - -class FluidSolver: - """ - 流体求解器 - 负责实现流体模拟的数值求解算法,包括压力求解、速度更新等 - """ - - def __init__(self, plugin): - """ - 初始化流体求解器 - - Args: - plugin: 水体和流体模拟插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 求解器配置 - self.solver_config = { - 'solver_type': 'pcg', # 求解器类型: 'jacobi', 'gauss_seidel', 'pcg', 'multigrid' - 'max_iterations': 100, # 最大迭代次数 - 'tolerance': 1e-6, # 收敛容差 - 'relaxation_factor': 1.0, # 松弛因子 - 'preconditioner': 'none' # 预条件子: 'jacobi', 'ilu', 'none' - } - - # 数值方法配置 - self.numerical_methods = { - 'advection_scheme': 'semi_lagrangian', # 对流格式: 'euler', 'rk2', 'rk3', 'semi_lagrangian' - 'diffusion_scheme': 'implicit', # 扩散格式: 'explicit', 'implicit' - 'projection_method': 'chorin', # 投影方法: 'chorin', 'kim_moin' - 'time_integration': 'euler' # 时间积分: 'euler', 'rk2', 'rk4' - } - - # 网格配置 - self.grid_config = { - 'resolution_x': 64, - 'resolution_y': 64, - 'resolution_z': 64, - 'cell_size': 1.0 - } - - # 边界条件 - self.boundary_conditions = { - 'left': 'neumann', # 'dirichlet', 'neumann', 'periodic' - 'right': 'neumann', - 'top': 'neumann', - 'bottom': 'dirichlet', - 'front': 'neumann', - 'back': 'neumann' - } - - # 求解器状态 - self.solver_state = { - 'current_iteration': 0, - 'residual': 0.0, - 'converged': False, - 'solve_time': 0.0 - } - - # 性能统计 - self.performance_stats = { - 'pressure_solves': 0, - 'velocity_updates': 0, - 'total_solve_time': 0.0, - 'average_solve_time': 0.0 - } - - # 缓存矩阵(用于预计算) - self.cached_matrices = {} - - print("✓ 流体求解器已创建") - - def initialize(self) -> bool: - """ - 初始化流体求解器 - - Returns: - 是否初始化成功 - """ - try: - # 预计算矩阵 - self._precompute_matrices() - - self.initialized = True - print("✓ 流体求解器初始化完成") - return True - - except Exception as e: - print(f"✗ 流体求解器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用流体求解器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 流体求解器未初始化") - return False - - self.enabled = True - print("✓ 流体求解器已启用") - return True - - except Exception as e: - print(f"✗ 流体求解器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用流体求解器""" - try: - self.enabled = False - print("✓ 流体求解器已禁用") - - except Exception as e: - print(f"✗ 流体求解器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理流体求解器资源""" - try: - self.disable() - self.cached_matrices.clear() - self.initialized = False - print("✓ 流体求解器资源已清理") - - except Exception as e: - print(f"✗ 流体求解器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新流体求解器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 在这里可以执行周期性任务 - pass - - except Exception as e: - print(f"✗ 流体求解器更新失败: {e}") - import traceback - traceback.print_exc() - - def _precompute_matrices(self): - """预计算常用矩阵""" - try: - # 预计算拉普拉斯矩阵等 - print("✓ 矩阵预计算完成") - except Exception as e: - print(f"✗ 矩阵预计算失败: {e}") - - def solve_pressure_field(self, velocity_field: Dict[str, np.ndarray], - density_field: np.ndarray) -> Tuple[np.ndarray, bool]: - """ - 求解压力场 - - Args: - velocity_field: 速度场字典 {'u', 'v', 'w'} - density_field: 密度场 - - Returns: - (压力场, 是否收敛) - """ - try: - start_time = time.time() - - # 根据求解器类型选择算法 - solver_type = self.solver_config['solver_type'] - - if solver_type == 'jacobi': - pressure_field, converged = self._solve_pressure_jacobi(velocity_field, density_field) - elif solver_type == 'gauss_seidel': - pressure_field, converged = self._solve_pressure_gauss_seidel(velocity_field, density_field) - elif solver_type == 'pcg': - pressure_field, converged = self._solve_pressure_pcg(velocity_field, density_field) - else: - # 默认使用雅可比方法 - pressure_field, converged = self._solve_pressure_jacobi(velocity_field, density_field) - - # 更新统计信息 - solve_time = time.time() - start_time - self.performance_stats['pressure_solves'] += 1 - self.performance_stats['total_solve_time'] += solve_time - - if self.performance_stats['pressure_solves'] > 0: - self.performance_stats['average_solve_time'] = ( - self.performance_stats['total_solve_time'] / - self.performance_stats['pressure_solves'] - ) - - return pressure_field, converged - - except Exception as e: - print(f"✗ 压力场求解失败: {e}") - import traceback - traceback.print_exc() - # 返回零压力场和未收敛状态 - shape = velocity_field['u'].shape if velocity_field else (1, 1, 1) - return np.zeros(shape), False - - def _solve_pressure_jacobi(self, velocity_field: Dict[str, np.ndarray], - density_field: np.ndarray) -> Tuple[np.ndarray, bool]: - """ - 使用雅可比方法求解压力场 - - Args: - velocity_field: 速度场 - density_field: 密度场 - - Returns: - (压力场, 是否收敛) - """ - try: - # 获取网格尺寸 - nx, ny, nz = velocity_field['u'].shape - - # 初始化压力场 - pressure = np.zeros((nx, ny, nz)) - pressure_new = np.zeros((nx, ny, nz)) - - # 计算网格步长 - dx = self.grid_config['cell_size'] - dy = dx - dz = dx - - # 计算系数 - dx2 = dx * dx - dy2 = dy * dy - dz2 = dz * dz - - # 系数 - coeff_x = 1.0 / dx2 - coeff_y = 1.0 / dy2 - coeff_z = 1.0 / dz2 - center_coeff = 2.0 * (coeff_x + coeff_y + coeff_z) - - # 预计算用于收敛检查的系数 - tolerance = self.solver_config['tolerance'] - max_iterations = self.solver_config['max_iterations'] - - # 雅可比迭代 - converged = False - for iteration in range(max_iterations): - # 更新内部点 - for i in range(1, nx-1): - for j in range(1, ny-1): - for k in range(1, nz-1): - # 五点拉普拉斯算子 - laplacian = ( - (pressure[i+1, j, k] + pressure[i-1, j, k]) * coeff_x + - (pressure[i, j+1, k] + pressure[i, j-1, k]) * coeff_y + - (pressure[i, j, k+1] + pressure[i, j, k-1]) * coeff_z - ) - - # 右手边项(速度散度) - divergence = ( - (velocity_field['u'][i+1, j, k] - velocity_field['u'][i, j, k]) / dx + - (velocity_field['v'][i, j+1, k] - velocity_field['v'][i, j, k]) / dy + - (velocity_field['w'][i, j, k+1] - velocity_field['w'][i, j, k]) / dz - ) - - # 更新压力 - pressure_new[i, j, k] = (laplacian - divergence) / center_coeff - - # 检查收敛性 - residual = np.max(np.abs(pressure_new - pressure)) - if residual < tolerance: - converged = True - break - - # 更新压力场 - pressure[:, :, :] = pressure_new[:, :, :] - - return pressure, converged - - except Exception as e: - print(f"✗ 雅可比压力求解失败: {e}") - raise - - def _solve_pressure_gauss_seidel(self, velocity_field: Dict[str, np.ndarray], - density_field: np.ndarray) -> Tuple[np.ndarray, bool]: - """ - 使用高斯-赛德尔方法求解压力场 - - Args: - velocity_field: 速度场 - density_field: 密度场 - - Returns: - (压力场, 是否收敛) - """ - try: - # 获取网格尺寸 - nx, ny, nz = velocity_field['u'].shape - - # 初始化压力场 - pressure = np.zeros((nx, ny, nz)) - - # 计算网格步长 - dx = self.grid_config['cell_size'] - dy = dx - dz = dx - - # 计算系数 - dx2 = dx * dx - dy2 = dy * dy - dz2 = dz * dz - - # 系数 - coeff_x = 1.0 / dx2 - coeff_y = 1.0 / dy2 - coeff_z = 1.0 / dz2 - center_coeff = 2.0 * (coeff_x + coeff_y + coeff_z) - - # 预计算用于收敛检查的系数 - tolerance = self.solver_config['tolerance'] - max_iterations = self.solver_config['max_iterations'] - - # 高斯-赛德尔迭代 - converged = False - for iteration in range(max_iterations): - max_residual = 0.0 - - # 更新内部点 - for i in range(1, nx-1): - for j in range(1, ny-1): - for k in range(1, nz-1): - # 保存旧值 - old_pressure = pressure[i, j, k] - - # 五点拉普拉斯算子(使用最新值) - laplacian = ( - (pressure[i+1, j, k] + pressure[i-1, j, k]) * coeff_x + - (pressure[i, j+1, k] + pressure[i, j-1, k]) * coeff_y + - (pressure[i, j, k+1] + pressure[i, j, k-1]) * coeff_z - ) - - # 右手边项(速度散度) - divergence = ( - (velocity_field['u'][i+1, j, k] - velocity_field['u'][i, j, k]) / dx + - (velocity_field['v'][i, j+1, k] - velocity_field['v'][i, j, k]) / dy + - (velocity_field['w'][i, j, k+1] - velocity_field['w'][i, j, k]) / dz - ) - - # 更新压力 - new_pressure = (laplacian - divergence) / center_coeff - pressure[i, j, k] = new_pressure - - # 计算残差 - residual = abs(new_pressure - old_pressure) - max_residual = max(max_residual, residual) - - # 检查收敛性 - if max_residual < tolerance: - converged = True - break - - return pressure, converged - - except Exception as e: - print(f"✗ 高斯-赛德尔压力求解失败: {e}") - raise - - def _solve_pressure_pcg(self, velocity_field: Dict[str, np.ndarray], - density_field: np.ndarray) -> Tuple[np.ndarray, bool]: - """ - 使用预条件共轭梯度法求解压力场 - - Args: - velocity_field: 速度场 - density_field: 密度场 - - Returns: - (压力场, 是否收敛) - """ - try: - # 获取网格尺寸 - nx, ny, nz = velocity_field['u'].shape - n = nx * ny * nz - - # 简化版本 - 实际实现中会构建稀疏矩阵并使用scipy求解器 - pressure = np.zeros((nx, ny, nz)) - converged = True # 简化处理 - - # 在实际实现中,这里会: - # 1. 构建泊松方程的稀疏矩阵 - # 2. 构建右手边向量(速度散度) - # 3. 使用预条件共轭梯度法求解 - # 4. 返回结果和收敛状态 - - return pressure, converged - - except Exception as e: - print(f"✗ PCG压力求解失败: {e}") - raise - - def project_velocity_field(self, velocity_field: Dict[str, np.ndarray], - pressure_field: np.ndarray) -> Dict[str, np.ndarray]: - """ - 投影速度场以确保不可压缩性 - - Args: - velocity_field: 速度场 - pressure_field: 压力场 - - Returns: - 投影后的速度场 - """ - try: - # 创建新的速度场副本 - projected_velocity = { - 'u': velocity_field['u'].copy(), - 'v': velocity_field['v'].copy(), - 'w': velocity_field['w'].copy() - } - - # 获取网格尺寸 - nx, ny, nz = pressure_field.shape - - # 计算网格步长 - dx = self.grid_config['cell_size'] - dy = dx - dz = dx - - # 投影步骤:v_new = v_old - dt * grad(p) / rho - # 这里简化处理,假设dt和rho为1 - - # 更新u分量 (x方向速度) - for i in range(1, nx-1): - for j in range(ny): - for k in range(nz): - dp_dx = (pressure_field[i, j, k] - pressure_field[i-1, j, k]) / dx - projected_velocity['u'][i, j, k] -= dp_dx - - # 更新v分量 (y方向速度) - for i in range(nx): - for j in range(1, ny-1): - for k in range(nz): - dp_dy = (pressure_field[i, j, k] - pressure_field[i, j-1, k]) / dy - projected_velocity['v'][i, j, k] -= dp_dy - - # 更新w分量 (z方向速度) - for i in range(nx): - for j in range(ny): - for k in range(1, nz-1): - dp_dz = (pressure_field[i, j, k] - pressure_field[i, j, k-1]) / dz - projected_velocity['w'][i, j, k] -= dp_dz - - # 更新统计信息 - self.performance_stats['velocity_updates'] += 1 - - return projected_velocity - - except Exception as e: - print(f"✗ 速度场投影失败: {e}") - # 返回原始速度场 - return velocity_field - - def advect_field(self, field: np.ndarray, velocity_field: Dict[str, np.ndarray], - dt: float) -> np.ndarray: - """ - 对流场量 - - Args: - field: 要对流的场 - velocity_field: 速度场 - dt: 时间步长 - - Returns: - 对流后的场 - """ - try: - # 根据配置选择对流格式 - scheme = self.numerical_methods['advection_scheme'] - - if scheme == 'euler': - return self._advect_euler(field, velocity_field, dt) - elif scheme == 'rk2': - return self._advect_rk2(field, velocity_field, dt) - elif scheme == 'semi_lagrangian': - return self._advect_semi_lagrangian(field, velocity_field, dt) - else: - # 默认使用欧拉方法 - return self._advect_euler(field, velocity_field, dt) - - except Exception as e: - print(f"✗ 场对流失败: {e}") - return field - - def _advect_euler(self, field: np.ndarray, velocity_field: Dict[str, np.ndarray], - dt: float) -> np.ndarray: - """ - 使用前向欧拉方法对流场 - - Args: - field: 要对流的场 - velocity_field: 速度场 - dt: 时间步长 - - Returns: - 对流后的场 - """ - try: - # 获取网格尺寸 - nx, ny, nz = field.shape - - # 计算网格步长 - dx = self.grid_config['cell_size'] - dy = dx - dz = dx - - # 创建新场 - new_field = field.copy() - - # 前向欧拉对流 - for i in range(1, nx-1): - for j in range(1, ny-1): - for k in range(1, nz-1): - # 获取速度 - u = velocity_field['u'][i, j, k] - v = velocity_field['v'][i, j, k] - w = velocity_field['w'][i, j, k] - - # 计算追踪点 - x_trace = i - u * dt / dx - y_trace = j - v * dt / dy - z_trace = k - w * dt / dz - - # 线性插值获取追踪点的场值 - new_field[i, j, k] = self._trilinear_interpolate(field, x_trace, y_trace, z_trace) - - return new_field - - except Exception as e: - print(f"✗ 欧拉对流失败: {e}") - return field - - def _advect_rk2(self, field: np.ndarray, velocity_field: Dict[str, np.ndarray], - dt: float) -> np.ndarray: - """ - 使用二阶Runge-Kutta方法对流场 - - Args: - field: 要对流的场 - velocity_field: 速度场 - dt: 时间步长 - - Returns: - 对流后的场 - """ - try: - # RK2实现 - # 中点方法: k1 = f(x_n, t_n), k2 = f(x_n + dt/2 * k1, t_n + dt/2) - # x_{n+1} = x_n + dt * k2 - return field # 简化处理 - - except Exception as e: - print(f"✗ RK2对流失败: {e}") - return field - - def _advect_semi_lagrangian(self, field: np.ndarray, velocity_field: Dict[str, np.ndarray], - dt: float) -> np.ndarray: - """ - 使用半拉格朗日方法对流场 - - Args: - field: 要对流的场 - velocity_field: 速度场 - dt: 时间步长 - - Returns: - 对流后的场 - """ - try: - # 半拉格朗日方法实现 - # 从终点追踪到起点,然后插值 - return field # 简化处理 - - except Exception as e: - print(f"✗ 半拉格朗日对流失败: {e}") - return field - - def _trilinear_interpolate(self, field: np.ndarray, x: float, y: float, z: float) -> float: - """ - 三线性插值 - - Args: - field: 场数据 - x, y, z: 插值点坐标 - - Returns: - 插值结果 - """ - try: - # 获取整数部分和小数部分 - i = int(x) - j = int(y) - k = int(z) - - fx = x - i - fy = y - j - fz = z - k - - # 边界检查 - nx, ny, nz = field.shape - if i < 0 or i >= nx-1 or j < 0 or j >= ny-1 or k < 0 or k >= nz-1: - return 0.0 - - # 三线性插值 - c00 = field[i, j, k] * (1 - fx) + field[i+1, j, k] * fx - c01 = field[i, j, k+1] * (1 - fx) + field[i+1, j, k+1] * fx - c10 = field[i, j+1, k] * (1 - fx) + field[i+1, j+1, k] * fx - c11 = field[i, j+1, k+1] * (1 - fx) + field[i+1, j+1, k+1] * fx - - c0 = c00 * (1 - fy) + c10 * fy - c1 = c01 * (1 - fy) + c11 * fy - - return c0 * (1 - fz) + c1 * fz - - except Exception as e: - return 0.0 - - def diffuse_field(self, field: np.ndarray, diffusion_coeff: float, - dt: float) -> np.ndarray: - """ - 扩散场量 - - Args: - field: 要扩散的场 - diffusion_coeff: 扩散系数 - dt: 时间步长 - - Returns: - 扩散后的场 - """ - try: - # 根据配置选择扩散格式 - scheme = self.numerical_methods['diffusion_scheme'] - - if scheme == 'explicit': - return self._diffuse_explicit(field, diffusion_coeff, dt) - elif scheme == 'implicit': - return self._diffuse_implicit(field, diffusion_coeff, dt) - else: - # 默认使用显式方法 - return self._diffuse_explicit(field, diffusion_coeff, dt) - - except Exception as e: - print(f"✗ 场扩散失败: {e}") - return field - - def _diffuse_explicit(self, field: np.ndarray, diffusion_coeff: float, - dt: float) -> np.ndarray: - """ - 显式扩散 - - Args: - field: 要扩散的场 - diffusion_coeff: 扩散系数 - dt: 时间步长 - - Returns: - 扩散后的场 - """ - try: - # 拉普拉斯算子的显式更新 - nx, ny, nz = field.shape - dx = self.grid_config['cell_size'] - - # 扩散系数 - alpha = diffusion_coeff * dt / (dx * dx) - - # 确保稳定性 (CFL条件) - if alpha > 0.166667: # 1/6 for 3D - alpha = 0.166667 - - new_field = field.copy() - - for i in range(1, nx-1): - for j in range(1, ny-1): - for k in range(1, nz-1): - laplacian = ( - field[i+1, j, k] + field[i-1, j, k] + - field[i, j+1, k] + field[i, j-1, k] + - field[i, j, k+1] + field[i, j, k-1] - - 6 * field[i, j, k] - ) - new_field[i, j, k] = field[i, j, k] + alpha * laplacian - - return new_field - - except Exception as e: - print(f"✗ 显式扩散失败: {e}") - return field - - def _diffuse_implicit(self, field: np.ndarray, diffusion_coeff: float, - dt: float) -> np.ndarray: - """ - 隐式扩散 - - Args: - field: 要扩散的场 - diffusion_coeff: 扩散系数 - dt: 时间步长 - - Returns: - 扩散后的场 - """ - try: - # 隐式扩散需要求解线性系统 - # (I - alpha * L) * field_new = field_old - return field # 简化处理 - - except Exception as e: - print(f"✗ 隐式扩散失败: {e}") - return field - - def set_solver_config(self, config: Dict[str, Any]): - """ - 设置求解器配置 - - Args: - config: 求解器配置字典 - """ - try: - self.solver_config.update(config) - print(f"✓ 求解器配置已更新: {self.solver_config}") - except Exception as e: - print(f"✗ 求解器配置设置失败: {e}") - - def get_solver_config(self) -> Dict[str, Any]: - """ - 获取求解器配置 - - Returns: - 求解器配置字典 - """ - return self.solver_config.copy() - - def set_numerical_methods(self, methods: Dict[str, str]): - """ - 设置数值方法 - - Args: - methods: 数值方法字典 - """ - try: - self.numerical_methods.update(methods) - print(f"✓ 数值方法已更新: {self.numerical_methods}") - except Exception as e: - print(f"✗ 数值方法设置失败: {e}") - - def get_numerical_methods(self) -> Dict[str, str]: - """ - 获取数值方法 - - Returns: - 数值方法字典 - """ - return self.numerical_methods.copy() - - def get_performance_stats(self) -> Dict[str, Any]: - """ - 获取性能统计信息 - - Returns: - 性能统计字典 - """ - return self.performance_stats.copy() - - def reset_performance_stats(self): - """重置性能统计信息""" - try: - self.performance_stats = { - 'pressure_solves': 0, - 'velocity_updates': 0, - 'total_solve_time': 0.0, - 'average_solve_time': 0.0 - } - print("✓ 性能统计信息已重置") - except Exception as e: - print(f"✗ 性能统计信息重置失败: {e}") - - def get_solver_state(self) -> Dict[str, Any]: - """ - 获取求解器状态 - - Returns: - 求解器状态字典 - """ +""" +流体求解器模块 +负责实现流体模拟的数值求解算法 +""" + +import time +from typing import Dict, Any, List, Optional, Tuple +import math +import numpy as np +from scipy.sparse import diags +from scipy.sparse.linalg import spsolve + +class FluidSolver: + """ + 流体求解器 + 负责实现流体模拟的数值求解算法,包括压力求解、速度更新等 + """ + + def __init__(self, plugin): + """ + 初始化流体求解器 + + Args: + plugin: 水体和流体模拟插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 求解器配置 + self.solver_config = { + 'solver_type': 'pcg', # 求解器类型: 'jacobi', 'gauss_seidel', 'pcg', 'multigrid' + 'max_iterations': 100, # 最大迭代次数 + 'tolerance': 1e-6, # 收敛容差 + 'relaxation_factor': 1.0, # 松弛因子 + 'preconditioner': 'none' # 预条件子: 'jacobi', 'ilu', 'none' + } + + # 数值方法配置 + self.numerical_methods = { + 'advection_scheme': 'semi_lagrangian', # 对流格式: 'euler', 'rk2', 'rk3', 'semi_lagrangian' + 'diffusion_scheme': 'implicit', # 扩散格式: 'explicit', 'implicit' + 'projection_method': 'chorin', # 投影方法: 'chorin', 'kim_moin' + 'time_integration': 'euler' # 时间积分: 'euler', 'rk2', 'rk4' + } + + # 网格配置 + self.grid_config = { + 'resolution_x': 64, + 'resolution_y': 64, + 'resolution_z': 64, + 'cell_size': 1.0 + } + + # 边界条件 + self.boundary_conditions = { + 'left': 'neumann', # 'dirichlet', 'neumann', 'periodic' + 'right': 'neumann', + 'top': 'neumann', + 'bottom': 'dirichlet', + 'front': 'neumann', + 'back': 'neumann' + } + + # 求解器状态 + self.solver_state = { + 'current_iteration': 0, + 'residual': 0.0, + 'converged': False, + 'solve_time': 0.0 + } + + # 性能统计 + self.performance_stats = { + 'pressure_solves': 0, + 'velocity_updates': 0, + 'total_solve_time': 0.0, + 'average_solve_time': 0.0 + } + + # 缓存矩阵(用于预计算) + self.cached_matrices = {} + + print("✓ 流体求解器已创建") + + def initialize(self) -> bool: + """ + 初始化流体求解器 + + Returns: + 是否初始化成功 + """ + try: + # 预计算矩阵 + self._precompute_matrices() + + self.initialized = True + print("✓ 流体求解器初始化完成") + return True + + except Exception as e: + print(f"✗ 流体求解器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用流体求解器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 流体求解器未初始化") + return False + + self.enabled = True + print("✓ 流体求解器已启用") + return True + + except Exception as e: + print(f"✗ 流体求解器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用流体求解器""" + try: + self.enabled = False + print("✓ 流体求解器已禁用") + + except Exception as e: + print(f"✗ 流体求解器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理流体求解器资源""" + try: + self.disable() + self.cached_matrices.clear() + self.initialized = False + print("✓ 流体求解器资源已清理") + + except Exception as e: + print(f"✗ 流体求解器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新流体求解器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 在这里可以执行周期性任务 + pass + + except Exception as e: + print(f"✗ 流体求解器更新失败: {e}") + import traceback + traceback.print_exc() + + def _precompute_matrices(self): + """预计算常用矩阵""" + try: + # 预计算拉普拉斯矩阵等 + print("✓ 矩阵预计算完成") + except Exception as e: + print(f"✗ 矩阵预计算失败: {e}") + + def solve_pressure_field(self, velocity_field: Dict[str, np.ndarray], + density_field: np.ndarray) -> Tuple[np.ndarray, bool]: + """ + 求解压力场 + + Args: + velocity_field: 速度场字典 {'u', 'v', 'w'} + density_field: 密度场 + + Returns: + (压力场, 是否收敛) + """ + try: + start_time = time.time() + + # 根据求解器类型选择算法 + solver_type = self.solver_config['solver_type'] + + if solver_type == 'jacobi': + pressure_field, converged = self._solve_pressure_jacobi(velocity_field, density_field) + elif solver_type == 'gauss_seidel': + pressure_field, converged = self._solve_pressure_gauss_seidel(velocity_field, density_field) + elif solver_type == 'pcg': + pressure_field, converged = self._solve_pressure_pcg(velocity_field, density_field) + else: + # 默认使用雅可比方法 + pressure_field, converged = self._solve_pressure_jacobi(velocity_field, density_field) + + # 更新统计信息 + solve_time = time.time() - start_time + self.performance_stats['pressure_solves'] += 1 + self.performance_stats['total_solve_time'] += solve_time + + if self.performance_stats['pressure_solves'] > 0: + self.performance_stats['average_solve_time'] = ( + self.performance_stats['total_solve_time'] / + self.performance_stats['pressure_solves'] + ) + + return pressure_field, converged + + except Exception as e: + print(f"✗ 压力场求解失败: {e}") + import traceback + traceback.print_exc() + # 返回零压力场和未收敛状态 + shape = velocity_field['u'].shape if velocity_field else (1, 1, 1) + return np.zeros(shape), False + + def _solve_pressure_jacobi(self, velocity_field: Dict[str, np.ndarray], + density_field: np.ndarray) -> Tuple[np.ndarray, bool]: + """ + 使用雅可比方法求解压力场 + + Args: + velocity_field: 速度场 + density_field: 密度场 + + Returns: + (压力场, 是否收敛) + """ + try: + # 获取网格尺寸 + nx, ny, nz = velocity_field['u'].shape + + # 初始化压力场 + pressure = np.zeros((nx, ny, nz)) + pressure_new = np.zeros((nx, ny, nz)) + + # 计算网格步长 + dx = self.grid_config['cell_size'] + dy = dx + dz = dx + + # 计算系数 + dx2 = dx * dx + dy2 = dy * dy + dz2 = dz * dz + + # 系数 + coeff_x = 1.0 / dx2 + coeff_y = 1.0 / dy2 + coeff_z = 1.0 / dz2 + center_coeff = 2.0 * (coeff_x + coeff_y + coeff_z) + + # 预计算用于收敛检查的系数 + tolerance = self.solver_config['tolerance'] + max_iterations = self.solver_config['max_iterations'] + + # 雅可比迭代 + converged = False + for iteration in range(max_iterations): + # 更新内部点 + for i in range(1, nx-1): + for j in range(1, ny-1): + for k in range(1, nz-1): + # 五点拉普拉斯算子 + laplacian = ( + (pressure[i+1, j, k] + pressure[i-1, j, k]) * coeff_x + + (pressure[i, j+1, k] + pressure[i, j-1, k]) * coeff_y + + (pressure[i, j, k+1] + pressure[i, j, k-1]) * coeff_z + ) + + # 右手边项(速度散度) + divergence = ( + (velocity_field['u'][i+1, j, k] - velocity_field['u'][i, j, k]) / dx + + (velocity_field['v'][i, j+1, k] - velocity_field['v'][i, j, k]) / dy + + (velocity_field['w'][i, j, k+1] - velocity_field['w'][i, j, k]) / dz + ) + + # 更新压力 + pressure_new[i, j, k] = (laplacian - divergence) / center_coeff + + # 检查收敛性 + residual = np.max(np.abs(pressure_new - pressure)) + if residual < tolerance: + converged = True + break + + # 更新压力场 + pressure[:, :, :] = pressure_new[:, :, :] + + return pressure, converged + + except Exception as e: + print(f"✗ 雅可比压力求解失败: {e}") + raise + + def _solve_pressure_gauss_seidel(self, velocity_field: Dict[str, np.ndarray], + density_field: np.ndarray) -> Tuple[np.ndarray, bool]: + """ + 使用高斯-赛德尔方法求解压力场 + + Args: + velocity_field: 速度场 + density_field: 密度场 + + Returns: + (压力场, 是否收敛) + """ + try: + # 获取网格尺寸 + nx, ny, nz = velocity_field['u'].shape + + # 初始化压力场 + pressure = np.zeros((nx, ny, nz)) + + # 计算网格步长 + dx = self.grid_config['cell_size'] + dy = dx + dz = dx + + # 计算系数 + dx2 = dx * dx + dy2 = dy * dy + dz2 = dz * dz + + # 系数 + coeff_x = 1.0 / dx2 + coeff_y = 1.0 / dy2 + coeff_z = 1.0 / dz2 + center_coeff = 2.0 * (coeff_x + coeff_y + coeff_z) + + # 预计算用于收敛检查的系数 + tolerance = self.solver_config['tolerance'] + max_iterations = self.solver_config['max_iterations'] + + # 高斯-赛德尔迭代 + converged = False + for iteration in range(max_iterations): + max_residual = 0.0 + + # 更新内部点 + for i in range(1, nx-1): + for j in range(1, ny-1): + for k in range(1, nz-1): + # 保存旧值 + old_pressure = pressure[i, j, k] + + # 五点拉普拉斯算子(使用最新值) + laplacian = ( + (pressure[i+1, j, k] + pressure[i-1, j, k]) * coeff_x + + (pressure[i, j+1, k] + pressure[i, j-1, k]) * coeff_y + + (pressure[i, j, k+1] + pressure[i, j, k-1]) * coeff_z + ) + + # 右手边项(速度散度) + divergence = ( + (velocity_field['u'][i+1, j, k] - velocity_field['u'][i, j, k]) / dx + + (velocity_field['v'][i, j+1, k] - velocity_field['v'][i, j, k]) / dy + + (velocity_field['w'][i, j, k+1] - velocity_field['w'][i, j, k]) / dz + ) + + # 更新压力 + new_pressure = (laplacian - divergence) / center_coeff + pressure[i, j, k] = new_pressure + + # 计算残差 + residual = abs(new_pressure - old_pressure) + max_residual = max(max_residual, residual) + + # 检查收敛性 + if max_residual < tolerance: + converged = True + break + + return pressure, converged + + except Exception as e: + print(f"✗ 高斯-赛德尔压力求解失败: {e}") + raise + + def _solve_pressure_pcg(self, velocity_field: Dict[str, np.ndarray], + density_field: np.ndarray) -> Tuple[np.ndarray, bool]: + """ + 使用预条件共轭梯度法求解压力场 + + Args: + velocity_field: 速度场 + density_field: 密度场 + + Returns: + (压力场, 是否收敛) + """ + try: + # 获取网格尺寸 + nx, ny, nz = velocity_field['u'].shape + n = nx * ny * nz + + # 简化版本 - 实际实现中会构建稀疏矩阵并使用scipy求解器 + pressure = np.zeros((nx, ny, nz)) + converged = True # 简化处理 + + # 在实际实现中,这里会: + # 1. 构建泊松方程的稀疏矩阵 + # 2. 构建右手边向量(速度散度) + # 3. 使用预条件共轭梯度法求解 + # 4. 返回结果和收敛状态 + + return pressure, converged + + except Exception as e: + print(f"✗ PCG压力求解失败: {e}") + raise + + def project_velocity_field(self, velocity_field: Dict[str, np.ndarray], + pressure_field: np.ndarray) -> Dict[str, np.ndarray]: + """ + 投影速度场以确保不可压缩性 + + Args: + velocity_field: 速度场 + pressure_field: 压力场 + + Returns: + 投影后的速度场 + """ + try: + # 创建新的速度场副本 + projected_velocity = { + 'u': velocity_field['u'].copy(), + 'v': velocity_field['v'].copy(), + 'w': velocity_field['w'].copy() + } + + # 获取网格尺寸 + nx, ny, nz = pressure_field.shape + + # 计算网格步长 + dx = self.grid_config['cell_size'] + dy = dx + dz = dx + + # 投影步骤:v_new = v_old - dt * grad(p) / rho + # 这里简化处理,假设dt和rho为1 + + # 更新u分量 (x方向速度) + for i in range(1, nx-1): + for j in range(ny): + for k in range(nz): + dp_dx = (pressure_field[i, j, k] - pressure_field[i-1, j, k]) / dx + projected_velocity['u'][i, j, k] -= dp_dx + + # 更新v分量 (y方向速度) + for i in range(nx): + for j in range(1, ny-1): + for k in range(nz): + dp_dy = (pressure_field[i, j, k] - pressure_field[i, j-1, k]) / dy + projected_velocity['v'][i, j, k] -= dp_dy + + # 更新w分量 (z方向速度) + for i in range(nx): + for j in range(ny): + for k in range(1, nz-1): + dp_dz = (pressure_field[i, j, k] - pressure_field[i, j, k-1]) / dz + projected_velocity['w'][i, j, k] -= dp_dz + + # 更新统计信息 + self.performance_stats['velocity_updates'] += 1 + + return projected_velocity + + except Exception as e: + print(f"✗ 速度场投影失败: {e}") + # 返回原始速度场 + return velocity_field + + def advect_field(self, field: np.ndarray, velocity_field: Dict[str, np.ndarray], + dt: float) -> np.ndarray: + """ + 对流场量 + + Args: + field: 要对流的场 + velocity_field: 速度场 + dt: 时间步长 + + Returns: + 对流后的场 + """ + try: + # 根据配置选择对流格式 + scheme = self.numerical_methods['advection_scheme'] + + if scheme == 'euler': + return self._advect_euler(field, velocity_field, dt) + elif scheme == 'rk2': + return self._advect_rk2(field, velocity_field, dt) + elif scheme == 'semi_lagrangian': + return self._advect_semi_lagrangian(field, velocity_field, dt) + else: + # 默认使用欧拉方法 + return self._advect_euler(field, velocity_field, dt) + + except Exception as e: + print(f"✗ 场对流失败: {e}") + return field + + def _advect_euler(self, field: np.ndarray, velocity_field: Dict[str, np.ndarray], + dt: float) -> np.ndarray: + """ + 使用前向欧拉方法对流场 + + Args: + field: 要对流的场 + velocity_field: 速度场 + dt: 时间步长 + + Returns: + 对流后的场 + """ + try: + # 获取网格尺寸 + nx, ny, nz = field.shape + + # 计算网格步长 + dx = self.grid_config['cell_size'] + dy = dx + dz = dx + + # 创建新场 + new_field = field.copy() + + # 前向欧拉对流 + for i in range(1, nx-1): + for j in range(1, ny-1): + for k in range(1, nz-1): + # 获取速度 + u = velocity_field['u'][i, j, k] + v = velocity_field['v'][i, j, k] + w = velocity_field['w'][i, j, k] + + # 计算追踪点 + x_trace = i - u * dt / dx + y_trace = j - v * dt / dy + z_trace = k - w * dt / dz + + # 线性插值获取追踪点的场值 + new_field[i, j, k] = self._trilinear_interpolate(field, x_trace, y_trace, z_trace) + + return new_field + + except Exception as e: + print(f"✗ 欧拉对流失败: {e}") + return field + + def _advect_rk2(self, field: np.ndarray, velocity_field: Dict[str, np.ndarray], + dt: float) -> np.ndarray: + """ + 使用二阶Runge-Kutta方法对流场 + + Args: + field: 要对流的场 + velocity_field: 速度场 + dt: 时间步长 + + Returns: + 对流后的场 + """ + try: + # RK2实现 + # 中点方法: k1 = f(x_n, t_n), k2 = f(x_n + dt/2 * k1, t_n + dt/2) + # x_{n+1} = x_n + dt * k2 + return field # 简化处理 + + except Exception as e: + print(f"✗ RK2对流失败: {e}") + return field + + def _advect_semi_lagrangian(self, field: np.ndarray, velocity_field: Dict[str, np.ndarray], + dt: float) -> np.ndarray: + """ + 使用半拉格朗日方法对流场 + + Args: + field: 要对流的场 + velocity_field: 速度场 + dt: 时间步长 + + Returns: + 对流后的场 + """ + try: + # 半拉格朗日方法实现 + # 从终点追踪到起点,然后插值 + return field # 简化处理 + + except Exception as e: + print(f"✗ 半拉格朗日对流失败: {e}") + return field + + def _trilinear_interpolate(self, field: np.ndarray, x: float, y: float, z: float) -> float: + """ + 三线性插值 + + Args: + field: 场数据 + x, y, z: 插值点坐标 + + Returns: + 插值结果 + """ + try: + # 获取整数部分和小数部分 + i = int(x) + j = int(y) + k = int(z) + + fx = x - i + fy = y - j + fz = z - k + + # 边界检查 + nx, ny, nz = field.shape + if i < 0 or i >= nx-1 or j < 0 or j >= ny-1 or k < 0 or k >= nz-1: + return 0.0 + + # 三线性插值 + c00 = field[i, j, k] * (1 - fx) + field[i+1, j, k] * fx + c01 = field[i, j, k+1] * (1 - fx) + field[i+1, j, k+1] * fx + c10 = field[i, j+1, k] * (1 - fx) + field[i+1, j+1, k] * fx + c11 = field[i, j+1, k+1] * (1 - fx) + field[i+1, j+1, k+1] * fx + + c0 = c00 * (1 - fy) + c10 * fy + c1 = c01 * (1 - fy) + c11 * fy + + return c0 * (1 - fz) + c1 * fz + + except Exception as e: + return 0.0 + + def diffuse_field(self, field: np.ndarray, diffusion_coeff: float, + dt: float) -> np.ndarray: + """ + 扩散场量 + + Args: + field: 要扩散的场 + diffusion_coeff: 扩散系数 + dt: 时间步长 + + Returns: + 扩散后的场 + """ + try: + # 根据配置选择扩散格式 + scheme = self.numerical_methods['diffusion_scheme'] + + if scheme == 'explicit': + return self._diffuse_explicit(field, diffusion_coeff, dt) + elif scheme == 'implicit': + return self._diffuse_implicit(field, diffusion_coeff, dt) + else: + # 默认使用显式方法 + return self._diffuse_explicit(field, diffusion_coeff, dt) + + except Exception as e: + print(f"✗ 场扩散失败: {e}") + return field + + def _diffuse_explicit(self, field: np.ndarray, diffusion_coeff: float, + dt: float) -> np.ndarray: + """ + 显式扩散 + + Args: + field: 要扩散的场 + diffusion_coeff: 扩散系数 + dt: 时间步长 + + Returns: + 扩散后的场 + """ + try: + # 拉普拉斯算子的显式更新 + nx, ny, nz = field.shape + dx = self.grid_config['cell_size'] + + # 扩散系数 + alpha = diffusion_coeff * dt / (dx * dx) + + # 确保稳定性 (CFL条件) + if alpha > 0.166667: # 1/6 for 3D + alpha = 0.166667 + + new_field = field.copy() + + for i in range(1, nx-1): + for j in range(1, ny-1): + for k in range(1, nz-1): + laplacian = ( + field[i+1, j, k] + field[i-1, j, k] + + field[i, j+1, k] + field[i, j-1, k] + + field[i, j, k+1] + field[i, j, k-1] - + 6 * field[i, j, k] + ) + new_field[i, j, k] = field[i, j, k] + alpha * laplacian + + return new_field + + except Exception as e: + print(f"✗ 显式扩散失败: {e}") + return field + + def _diffuse_implicit(self, field: np.ndarray, diffusion_coeff: float, + dt: float) -> np.ndarray: + """ + 隐式扩散 + + Args: + field: 要扩散的场 + diffusion_coeff: 扩散系数 + dt: 时间步长 + + Returns: + 扩散后的场 + """ + try: + # 隐式扩散需要求解线性系统 + # (I - alpha * L) * field_new = field_old + return field # 简化处理 + + except Exception as e: + print(f"✗ 隐式扩散失败: {e}") + return field + + def set_solver_config(self, config: Dict[str, Any]): + """ + 设置求解器配置 + + Args: + config: 求解器配置字典 + """ + try: + self.solver_config.update(config) + print(f"✓ 求解器配置已更新: {self.solver_config}") + except Exception as e: + print(f"✗ 求解器配置设置失败: {e}") + + def get_solver_config(self) -> Dict[str, Any]: + """ + 获取求解器配置 + + Returns: + 求解器配置字典 + """ + return self.solver_config.copy() + + def set_numerical_methods(self, methods: Dict[str, str]): + """ + 设置数值方法 + + Args: + methods: 数值方法字典 + """ + try: + self.numerical_methods.update(methods) + print(f"✓ 数值方法已更新: {self.numerical_methods}") + except Exception as e: + print(f"✗ 数值方法设置失败: {e}") + + def get_numerical_methods(self) -> Dict[str, str]: + """ + 获取数值方法 + + Returns: + 数值方法字典 + """ + return self.numerical_methods.copy() + + def get_performance_stats(self) -> Dict[str, Any]: + """ + 获取性能统计信息 + + Returns: + 性能统计字典 + """ + return self.performance_stats.copy() + + def reset_performance_stats(self): + """重置性能统计信息""" + try: + self.performance_stats = { + 'pressure_solves': 0, + 'velocity_updates': 0, + 'total_solve_time': 0.0, + 'average_solve_time': 0.0 + } + print("✓ 性能统计信息已重置") + except Exception as e: + print(f"✗ 性能统计信息重置失败: {e}") + + def get_solver_state(self) -> Dict[str, Any]: + """ + 获取求解器状态 + + Returns: + 求解器状态字典 + """ return self.solver_state.copy() \ No newline at end of file diff --git a/plugins/user/fluid_simulation/plugin.py b/plugins/user/fluid_simulation/plugin.py index befe3400..e4a69db0 100644 --- a/plugins/user/fluid_simulation/plugin.py +++ b/plugins/user/fluid_simulation/plugin.py @@ -1,340 +1,340 @@ -""" -水体和流体模拟插件 -为EG引擎提供完整的水体和流体模拟功能 -""" - -from .core.fluid_manager import FluidManager -from .core.water_manager import WaterManager -from .fluids.fluid_dynamics import FluidDynamics -from .fluids.fluid_solver import FluidSolver -from .water.water_surface import WaterSurface -from .water.wave_system import WaveSystem -from .rendering.fluid_renderer import FluidRenderer -from .rendering.water_renderer import WaterRenderer -from .effects.fluid_effects import FluidEffects -from .effects.water_effects import WaterEffects -from .interaction.fluid_interaction import FluidInteraction -from .interaction.object_interaction import ObjectInteraction -from .audio.fluid_audio import FluidAudio -from .particles.fluid_particles import FluidParticles -from .editor.fluid_editor import FluidEditor -from .editor.preset_manager import PresetManager -from .utils.fluid_utils import FluidUtils -from .physics.fluid_physics import FluidPhysics - -class FluidSimulationPlugin: - """ - 水体和流体模拟插件主类 - 整合所有水体和流体模拟功能模块 - """ - - def __init__(self, engine): - """ - 初始化水体和流体模拟插件 - - Args: - engine: EG引擎实例 - """ - self.engine = engine - self.name = "FluidSimulation" - self.version = "1.0.0" - self.author = "EG Plugin System" - - # 功能模块 - self.fluid_manager = None - self.water_manager = None - self.fluid_dynamics = None - self.fluid_solver = None - self.water_surface = None - self.wave_system = None - self.fluid_renderer = None - self.water_renderer = None - self.fluid_effects = None - self.water_effects = None - self.fluid_interaction = None - self.object_interaction = None - self.fluid_audio = None - self.fluid_particles = None - self.fluid_editor = None - self.preset_manager = None - self.fluid_utils = None - self.fluid_physics = None - - # 插件状态 - self.enabled = False - self.initialized = False - - # 性能统计 - self.stats = { - 'fluid_cells': 0, - 'water_vertices': 0, - 'simulation_updates': 0, - 'render_calls': 0, - 'particles_spawned': 0 - } - - print(f"✓ 水体和流体模拟插件 v{self.version} 已创建") - - def initialize(self) -> bool: - """ - 初始化插件 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化水体和流体模拟插件...") - - # 初始化核心组件 - self.fluid_manager = FluidManager(self) - self.water_manager = WaterManager(self) - self.fluid_dynamics = FluidDynamics(self) - self.fluid_solver = FluidSolver(self) - self.water_surface = WaterSurface(self) - self.wave_system = WaveSystem(self) - self.fluid_renderer = FluidRenderer(self) - self.water_renderer = WaterRenderer(self) - self.fluid_effects = FluidEffects(self) - self.water_effects = WaterEffects(self) - self.fluid_interaction = FluidInteraction(self) - self.object_interaction = ObjectInteraction(self) - self.fluid_audio = FluidAudio(self) - self.fluid_particles = FluidParticles(self) - self.fluid_editor = FluidEditor(self) - self.preset_manager = PresetManager(self) - self.fluid_utils = FluidUtils(self) - self.fluid_physics = FluidPhysics(self) - - # 初始化各个模块 - modules = [ - self.fluid_manager, - self.water_manager, - self.fluid_dynamics, - self.fluid_solver, - self.water_surface, - self.wave_system, - self.fluid_renderer, - self.water_renderer, - self.fluid_effects, - self.water_effects, - self.fluid_interaction, - self.object_interaction, - self.fluid_audio, - self.fluid_particles, - self.fluid_editor, - self.preset_manager, - self.fluid_utils, - self.fluid_physics - ] - - for module in modules: - if module and not module.initialize(): - print(f"✗ 模块初始化失败: {module.__class__.__name__}") - return False - - self.initialized = True - print("✓ 水体和流体模拟插件初始化完成") - return True - - except Exception as e: - print(f"✗ 水体和流体模拟插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 插件未初始化") - return False - - print("正在启用水体和流体模拟插件...") - - # 启用各个模块 - modules = [ - self.fluid_manager, - self.water_manager, - self.fluid_dynamics, - self.fluid_solver, - self.water_surface, - self.wave_system, - self.fluid_renderer, - self.water_renderer, - self.fluid_effects, - self.water_effects, - self.fluid_interaction, - self.object_interaction, - self.fluid_audio, - self.fluid_particles, - self.fluid_editor, - self.preset_manager, - self.fluid_utils, - self.fluid_physics - ] - - for module in modules: - if module and not module.enable(): - print(f"✗ 模块启用失败: {module.__class__.__name__}") - return False - - self.enabled = True - print("✓ 水体和流体模拟插件已启用") - return True - - except Exception as e: - print(f"✗ 水体和流体模拟插件启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用插件""" - try: - if not self.enabled: - return - - print("正在禁用水体和流体模拟插件...") - - # 禁用各个模块 - modules = [ - self.fluid_manager, - self.water_manager, - self.fluid_dynamics, - self.fluid_solver, - self.water_surface, - self.wave_system, - self.fluid_renderer, - self.water_renderer, - self.fluid_effects, - self.water_effects, - self.fluid_interaction, - self.object_interaction, - self.fluid_audio, - self.fluid_particles, - self.fluid_editor, - self.preset_manager, - self.fluid_utils, - self.fluid_physics - ] - - for module in modules: - if module: - module.disable() - - self.enabled = False - print("✓ 水体和流体模拟插件已禁用") - - except Exception as e: - print(f"✗ 水体和流体模拟插件禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理插件资源""" - try: - print("正在清理水体和流体模拟插件资源...") - - # 清理各个模块 - modules = [ - self.fluid_manager, - self.water_manager, - self.fluid_dynamics, - self.fluid_solver, - self.water_surface, - self.wave_system, - self.fluid_renderer, - self.water_renderer, - self.fluid_effects, - self.water_effects, - self.fluid_interaction, - self.object_interaction, - self.fluid_audio, - self.fluid_particles, - self.fluid_editor, - self.preset_manager, - self.fluid_utils, - self.fluid_physics - ] - - for module in modules: - if module: - module.finalize() - - self.initialized = False - print("✓ 水体和流体模拟插件资源已清理") - - except Exception as e: - print(f"✗ 水体和流体模拟插件资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新插件状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - # 更新各个模块 - modules = [ - self.fluid_manager, - self.water_manager, - self.fluid_dynamics, - self.fluid_solver, - self.water_surface, - self.wave_system, - self.fluid_renderer, - self.water_renderer, - self.fluid_effects, - self.water_effects, - self.fluid_interaction, - self.object_interaction, - self.fluid_audio, - self.fluid_particles, - self.fluid_editor, - self.preset_manager, - self.fluid_utils, - self.fluid_physics - ] - - for module in modules: - if module: - module.update(dt) - - # 更新统计信息 - self.stats['simulation_updates'] += 1 - - except Exception as e: - print(f"✗ 水体和流体模拟插件更新失败: {e}") - import traceback - traceback.print_exc() - - def get_stats(self) -> dict: - """ - 获取插件统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'fluid_cells': 0, - 'water_vertices': 0, - 'simulation_updates': 0, - 'render_calls': 0, - 'particles_spawned': 0 - } +""" +水体和流体模拟插件 +为EG引擎提供完整的水体和流体模拟功能 +""" + +from .core.fluid_manager import FluidManager +from .core.water_manager import WaterManager +from .fluids.fluid_dynamics import FluidDynamics +from .fluids.fluid_solver import FluidSolver +from .water.water_surface import WaterSurface +from .water.wave_system import WaveSystem +from .rendering.fluid_renderer import FluidRenderer +from .rendering.water_renderer import WaterRenderer +from .effects.fluid_effects import FluidEffects +from .effects.water_effects import WaterEffects +from .interaction.fluid_interaction import FluidInteraction +from .interaction.object_interaction import ObjectInteraction +from .audio.fluid_audio import FluidAudio +from .particles.fluid_particles import FluidParticles +from .editor.fluid_editor import FluidEditor +from .editor.preset_manager import PresetManager +from .utils.fluid_utils import FluidUtils +from .physics.fluid_physics import FluidPhysics + +class FluidSimulationPlugin: + """ + 水体和流体模拟插件主类 + 整合所有水体和流体模拟功能模块 + """ + + def __init__(self, engine): + """ + 初始化水体和流体模拟插件 + + Args: + engine: EG引擎实例 + """ + self.engine = engine + self.name = "FluidSimulation" + self.version = "1.0.0" + self.author = "EG Plugin System" + + # 功能模块 + self.fluid_manager = None + self.water_manager = None + self.fluid_dynamics = None + self.fluid_solver = None + self.water_surface = None + self.wave_system = None + self.fluid_renderer = None + self.water_renderer = None + self.fluid_effects = None + self.water_effects = None + self.fluid_interaction = None + self.object_interaction = None + self.fluid_audio = None + self.fluid_particles = None + self.fluid_editor = None + self.preset_manager = None + self.fluid_utils = None + self.fluid_physics = None + + # 插件状态 + self.enabled = False + self.initialized = False + + # 性能统计 + self.stats = { + 'fluid_cells': 0, + 'water_vertices': 0, + 'simulation_updates': 0, + 'render_calls': 0, + 'particles_spawned': 0 + } + + print(f"✓ 水体和流体模拟插件 v{self.version} 已创建") + + def initialize(self) -> bool: + """ + 初始化插件 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化水体和流体模拟插件...") + + # 初始化核心组件 + self.fluid_manager = FluidManager(self) + self.water_manager = WaterManager(self) + self.fluid_dynamics = FluidDynamics(self) + self.fluid_solver = FluidSolver(self) + self.water_surface = WaterSurface(self) + self.wave_system = WaveSystem(self) + self.fluid_renderer = FluidRenderer(self) + self.water_renderer = WaterRenderer(self) + self.fluid_effects = FluidEffects(self) + self.water_effects = WaterEffects(self) + self.fluid_interaction = FluidInteraction(self) + self.object_interaction = ObjectInteraction(self) + self.fluid_audio = FluidAudio(self) + self.fluid_particles = FluidParticles(self) + self.fluid_editor = FluidEditor(self) + self.preset_manager = PresetManager(self) + self.fluid_utils = FluidUtils(self) + self.fluid_physics = FluidPhysics(self) + + # 初始化各个模块 + modules = [ + self.fluid_manager, + self.water_manager, + self.fluid_dynamics, + self.fluid_solver, + self.water_surface, + self.wave_system, + self.fluid_renderer, + self.water_renderer, + self.fluid_effects, + self.water_effects, + self.fluid_interaction, + self.object_interaction, + self.fluid_audio, + self.fluid_particles, + self.fluid_editor, + self.preset_manager, + self.fluid_utils, + self.fluid_physics + ] + + for module in modules: + if module and not module.initialize(): + print(f"✗ 模块初始化失败: {module.__class__.__name__}") + return False + + self.initialized = True + print("✓ 水体和流体模拟插件初始化完成") + return True + + except Exception as e: + print(f"✗ 水体和流体模拟插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 插件未初始化") + return False + + print("正在启用水体和流体模拟插件...") + + # 启用各个模块 + modules = [ + self.fluid_manager, + self.water_manager, + self.fluid_dynamics, + self.fluid_solver, + self.water_surface, + self.wave_system, + self.fluid_renderer, + self.water_renderer, + self.fluid_effects, + self.water_effects, + self.fluid_interaction, + self.object_interaction, + self.fluid_audio, + self.fluid_particles, + self.fluid_editor, + self.preset_manager, + self.fluid_utils, + self.fluid_physics + ] + + for module in modules: + if module and not module.enable(): + print(f"✗ 模块启用失败: {module.__class__.__name__}") + return False + + self.enabled = True + print("✓ 水体和流体模拟插件已启用") + return True + + except Exception as e: + print(f"✗ 水体和流体模拟插件启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用插件""" + try: + if not self.enabled: + return + + print("正在禁用水体和流体模拟插件...") + + # 禁用各个模块 + modules = [ + self.fluid_manager, + self.water_manager, + self.fluid_dynamics, + self.fluid_solver, + self.water_surface, + self.wave_system, + self.fluid_renderer, + self.water_renderer, + self.fluid_effects, + self.water_effects, + self.fluid_interaction, + self.object_interaction, + self.fluid_audio, + self.fluid_particles, + self.fluid_editor, + self.preset_manager, + self.fluid_utils, + self.fluid_physics + ] + + for module in modules: + if module: + module.disable() + + self.enabled = False + print("✓ 水体和流体模拟插件已禁用") + + except Exception as e: + print(f"✗ 水体和流体模拟插件禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理插件资源""" + try: + print("正在清理水体和流体模拟插件资源...") + + # 清理各个模块 + modules = [ + self.fluid_manager, + self.water_manager, + self.fluid_dynamics, + self.fluid_solver, + self.water_surface, + self.wave_system, + self.fluid_renderer, + self.water_renderer, + self.fluid_effects, + self.water_effects, + self.fluid_interaction, + self.object_interaction, + self.fluid_audio, + self.fluid_particles, + self.fluid_editor, + self.preset_manager, + self.fluid_utils, + self.fluid_physics + ] + + for module in modules: + if module: + module.finalize() + + self.initialized = False + print("✓ 水体和流体模拟插件资源已清理") + + except Exception as e: + print(f"✗ 水体和流体模拟插件资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新插件状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + # 更新各个模块 + modules = [ + self.fluid_manager, + self.water_manager, + self.fluid_dynamics, + self.fluid_solver, + self.water_surface, + self.wave_system, + self.fluid_renderer, + self.water_renderer, + self.fluid_effects, + self.water_effects, + self.fluid_interaction, + self.object_interaction, + self.fluid_audio, + self.fluid_particles, + self.fluid_editor, + self.preset_manager, + self.fluid_utils, + self.fluid_physics + ] + + for module in modules: + if module: + module.update(dt) + + # 更新统计信息 + self.stats['simulation_updates'] += 1 + + except Exception as e: + print(f"✗ 水体和流体模拟插件更新失败: {e}") + import traceback + traceback.print_exc() + + def get_stats(self) -> dict: + """ + 获取插件统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'fluid_cells': 0, + 'water_vertices': 0, + 'simulation_updates': 0, + 'render_calls': 0, + 'particles_spawned': 0 + } print("✓ 水体和流体模拟统计信息已重置") \ No newline at end of file diff --git a/plugins/user/fluid_simulation/rendering/fluid_renderer.py b/plugins/user/fluid_simulation/rendering/fluid_renderer.py index 72dcdbcf..da49b00d 100644 --- a/plugins/user/fluid_simulation/rendering/fluid_renderer.py +++ b/plugins/user/fluid_simulation/rendering/fluid_renderer.py @@ -1,776 +1,776 @@ -""" -流体渲染器模块 -负责流体的可视化渲染和效果处理 -""" - -import time -from typing import Dict, Any, List, Optional -import math -import numpy as np - -class FluidRenderer: - """ - 流体渲染器 - 负责流体的可视化渲染和效果处理 - """ - - def __init__(self, plugin): - """ - 初始化流体渲染器 - - Args: - plugin: 水体和流体模拟插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 渲染配置 - self.render_config = { - 'render_mode': 'volume', # 'volume', 'particles', 'surface' - 'quality_level': 'medium', # 'low', 'medium', 'high', 'ultra' - 'max_particles': 10000, - 'particle_size': 0.1, - 'render_distance': 100.0, - 'lod_enabled': True, - 'lod_distance': 50.0 - } - - # 着色器配置 - self.shader_config = { - 'fluid_shader': 'standard', - 'lighting_model': 'blinn_phong', - 'normal_mapping': True, - 'specular_mapping': True, - 'reflection_enabled': True, - 'refraction_enabled': True, - 'caustics_enabled': False, - 'foam_enabled': True - } - - # 材质属性 - self.material_properties = { - 'color': (0.0, 0.3, 0.8, 0.7), # RGBA - 'specular_color': (1.0, 1.0, 1.0), - 'shininess': 64.0, - 'reflectivity': 0.8, - 'refractive_index': 1.333, - 'absorption_coefficient': 0.1, - 'scattering_coefficient': 0.5 - } - - # 光照配置 - self.lighting_config = { - 'ambient_light': (0.2, 0.2, 0.2), - 'diffuse_light': (0.8, 0.8, 0.8), - 'specular_light': (1.0, 1.0, 1.0), - 'light_direction': (0.5, 1.0, 0.3) - } - - # 体积渲染配置 - self.volume_config = { - 'sampling_rate': 64, - 'step_size': 0.1, - 'density_scale': 1.0, - 'gradient_factor': 1.0, - 'shadow_enabled': True, - 'shadow_steps': 16 - } - - # 粒子渲染配置 - self.particle_config = { - 'sprite_type': 'quad', # 'quad', 'billboard', 'sphere' - 'blend_mode': 'additive', # 'alpha', 'additive', 'subtractive' - 'size_variation': 0.5, - 'velocity_scale': 1.0, - 'life_scale': True - } - - # 渲染状态 - self.render_state = { - 'last_render_time': 0.0, - 'frames_rendered': 0, - 'particles_rendered': 0, - 'triangles_rendered': 0 - } - - # 性能统计 - self.performance_stats = { - 'render_calls': 0, - 'render_time_total': 0.0, - 'average_render_time': 0.0, - 'particles_drawn': 0, - 'volume_samples': 0 - } - - # 特效配置 - self.effect_config = { - 'motion_blur': False, - 'motion_blur_samples': 8, - 'anti_aliasing': 'fxaa', # 'none', 'fxaa', 'ssaa', 'msaa' - 'bloom_enabled': True, - 'bloom_intensity': 0.5, - 'depth_of_field': False, - 'dof_focus_distance': 10.0 - } - - # 相机和视图 - self.camera_state = { - 'position': (0.0, 0.0, 0.0), - 'view_matrix': None, - 'projection_matrix': None, - 'fov': 60.0 - } - - print("✓ 流体渲染器已创建") - - def initialize(self) -> bool: - """ - 初始化流体渲染器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化渲染资源 - self._initialize_render_resources() - - self.initialized = True - print("✓ 流体渲染器初始化完成") - return True - - except Exception as e: - print(f"✗ 流体渲染器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用流体渲染器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 流体渲染器未初始化") - return False - - self.enabled = True - print("✓ 流体渲染器已启用") - return True - - except Exception as e: - print(f"✗ 流体渲染器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用流体渲染器""" - try: - self.enabled = False - print("✓ 流体渲染器已禁用") - - except Exception as e: - print(f"✗ 流体渲染器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理流体渲染器资源""" - try: - self.disable() - self._cleanup_render_resources() - self.initialized = False - print("✓ 流体渲染器资源已清理") - - except Exception as e: - print(f"✗ 流体渲染器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新流体渲染器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新渲染状态 - self.render_state['last_render_time'] += dt - self.render_state['frames_rendered'] += 1 - - except Exception as e: - print(f"✗ 流体渲染器更新失败: {e}") - import traceback - traceback.print_exc() - - def _initialize_render_resources(self): - """初始化渲染资源""" - try: - # 在实际实现中,这里会初始化着色器、纹理、缓冲区等 - print("✓ 渲染资源初始化完成") - except Exception as e: - print(f"✗ 渲染资源初始化失败: {e}") - raise - - def _cleanup_render_resources(self): - """清理渲染资源""" - try: - # 在实际实现中,这里会释放着色器、纹理、缓冲区等资源 - print("✓ 渲染资源清理完成") - except Exception as e: - print(f"✗ 渲染资源清理失败: {e}") - - def render_fluid(self, fluid_data: Dict[str, Any], camera_position: tuple): - """ - 渲染流体 - - Args: - fluid_data: 流体数据字典 - camera_position: 相机位置 - """ - try: - start_time = time.time() - - # 更新相机状态 - self.camera_state['position'] = camera_position - - # 根据渲染模式选择渲染方法 - render_mode = self.render_config['render_mode'] - - if render_mode == 'volume': - self._render_volume_fluid(fluid_data) - elif render_mode == 'particles': - self._render_particle_fluid(fluid_data) - elif render_mode == 'surface': - self._render_surface_fluid(fluid_data) - else: - # 默认使用体积渲染 - self._render_volume_fluid(fluid_data) - - # 更新性能统计 - render_time = time.time() - start_time - self.performance_stats['render_calls'] += 1 - self.performance_stats['render_time_total'] += render_time - - if self.performance_stats['render_calls'] > 0: - self.performance_stats['average_render_time'] = ( - self.performance_stats['render_time_total'] / - self.performance_stats['render_calls'] - ) - - # 更新渲染状态 - self.render_state['frames_rendered'] += 1 - - except Exception as e: - print(f"✗ 流体渲染失败: {e}") - import traceback - traceback.print_exc() - - def _render_volume_fluid(self, fluid_data: Dict[str, Any]): - """ - 体积渲染流体 - - Args: - fluid_data: 流体数据 - """ - try: - # 获取流体场数据 - velocity_field = fluid_data.get('velocity_field') - density_field = fluid_data.get('density_field') - temperature_field = fluid_data.get('temperature_field') - - if not density_field: - return - - # 获取体积渲染配置 - sampling_rate = self.volume_config['sampling_rate'] - step_size = self.volume_config['step_size'] - density_scale = self.volume_config['density_scale'] - - # 在实际实现中,这里会: - # 1. 设置体积着色器参数 - # 2. 绑定体积纹理 - # 3. 执行光线行进算法 - # 4. 应用光照和材质属性 - # 5. 渲染到屏幕 - - # 更新统计信息 - self.performance_stats['volume_samples'] += sampling_rate - - print("✓ 体积流体渲染完成") - - except Exception as e: - print(f"✗ 体积流体渲染失败: {e}") - - def _render_particle_fluid(self, fluid_data: Dict[str, Any]): - """ - 粒子渲染流体 - - Args: - fluid_data: 流体数据 - """ - try: - # 获取粒子数据 - particles = fluid_data.get('particles', []) - - if not particles: - return - - # 获取粒子渲染配置 - sprite_type = self.particle_config['sprite_type'] - blend_mode = self.particle_config['blend_mode'] - particle_size = self.render_config['particle_size'] - - # 在实际实现中,这里会: - # 1. 设置粒子着色器参数 - # 2. 准备粒子顶点数据 - # 3. 应用材质属性 - # 4. 执行粒子渲染 - - # 更新统计信息 - particles_count = len(particles) - self.performance_stats['particles_drawn'] += particles_count - self.render_state['particles_rendered'] += particles_count - - print(f"✓ 粒子流体渲染完成 ({particles_count} 粒子)") - - except Exception as e: - print(f"✗ 粒子流体渲染失败: {e}") - - def _render_surface_fluid(self, fluid_data: Dict[str, Any]): - """ - 表面渲染流体 - - Args: - fluid_data: 流体数据 - """ - try: - # 获取表面网格数据 - vertices = fluid_data.get('vertices', []) - normals = fluid_data.get('normals', []) - indices = fluid_data.get('indices', []) - - if not vertices or not indices: - return - - # 在实际实现中,这里会: - # 1. 设置表面着色器参数 - # 2. 上传网格数据到GPU - # 3. 应用材质和光照 - # 4. 执行网格渲染 - - # 更新统计信息 - triangle_count = len(indices) // 3 - self.render_state['triangles_rendered'] += triangle_count - - print(f"✓ 表面流体渲染完成 ({triangle_count} 三角形)") - - except Exception as e: - print(f"✗ 表面流体渲染失败: {e}") - - def set_render_config(self, config: Dict[str, Any]): - """ - 设置渲染配置 - - Args: - config: 渲染配置字典 - """ - try: - self.render_config.update(config) - print(f"✓ 渲染配置已更新: {self.render_config}") - except Exception as e: - print(f"✗ 渲染配置设置失败: {e}") - - def get_render_config(self) -> Dict[str, Any]: - """ - 获取渲染配置 - - Returns: - 渲染配置字典 - """ - return self.render_config.copy() - - def set_shader_config(self, config: Dict[str, Any]): - """ - 设置着色器配置 - - Args: - config: 着色器配置字典 - """ - try: - self.shader_config.update(config) - print(f"✓ 着色器配置已更新: {self.shader_config}") - except Exception as e: - print(f"✗ 着色器配置设置失败: {e}") - - def get_shader_config(self) -> Dict[str, Any]: - """ - 获取着色器配置 - - Returns: - 着色器配置字典 - """ - return self.shader_config.copy() - - def set_material_properties(self, properties: Dict[str, Any]): - """ - 设置材质属性 - - Args: - properties: 材质属性字典 - """ - try: - self.material_properties.update(properties) - print(f"✓ 材质属性已更新: {self.material_properties}") - except Exception as e: - print(f"✗ 材质属性设置失败: {e}") - - def get_material_properties(self) -> Dict[str, Any]: - """ - 获取材质属性 - - Returns: - 材质属性字典 - """ - return self.material_properties.copy() - - def set_lighting_config(self, config: Dict[str, Any]): - """ - 设置光照配置 - - Args: - config: 光照配置字典 - """ - try: - self.lighting_config.update(config) - print(f"✓ 光照配置已更新: {self.lighting_config}") - except Exception as e: - print(f"✗ 光照配置设置失败: {e}") - - def get_lighting_config(self) -> Dict[str, Any]: - """ - 获取光照配置 - - Returns: - 光照配置字典 - """ - return self.lighting_config.copy() - - def set_volume_config(self, config: Dict[str, Any]): - """ - 设置体积渲染配置 - - Args: - config: 体积渲染配置字典 - """ - try: - self.volume_config.update(config) - print(f"✓ 体积渲染配置已更新: {self.volume_config}") - except Exception as e: - print(f"✗ 体积渲染配置设置失败: {e}") - - def get_volume_config(self) -> Dict[str, Any]: - """ - 获取体积渲染配置 - - Returns: - 体积渲染配置字典 - """ - return self.volume_config.copy() - - def set_particle_config(self, config: Dict[str, Any]): - """ - 设置粒子渲染配置 - - Args: - config: 粒子渲染配置字典 - """ - try: - self.particle_config.update(config) - print(f"✓ 粒子渲染配置已更新: {self.particle_config}") - except Exception as e: - print(f"✗ 粒子渲染配置设置失败: {e}") - - def get_particle_config(self) -> Dict[str, Any]: - """ - 获取粒子渲染配置 - - Returns: - 粒子渲染配置字典 - """ - return self.particle_config.copy() - - def set_effect_config(self, config: Dict[str, Any]): - """ - 设置特效配置 - - Args: - config: 特效配置字典 - """ - try: - self.effect_config.update(config) - print(f"✓ 特效配置已更新: {self.effect_config}") - except Exception as e: - print(f"✗ 特效配置设置失败: {e}") - - def get_effect_config(self) -> Dict[str, Any]: - """ - 获取特效配置 - - Returns: - 特效配置字典 - """ - return self.effect_config.copy() - - def get_render_state(self) -> Dict[str, Any]: - """ - 获取渲染状态 - - Returns: - 渲染状态字典 - """ - return self.render_state.copy() - - def get_performance_stats(self) -> Dict[str, Any]: - """ - 获取性能统计信息 - - Returns: - 性能统计字典 - """ - return self.performance_stats.copy() - - def reset_performance_stats(self): - """重置性能统计信息""" - try: - self.performance_stats = { - 'render_calls': 0, - 'render_time_total': 0.0, - 'average_render_time': 0.0, - 'particles_drawn': 0, - 'volume_samples': 0 - } - print("✓ 性能统计信息已重置") - except Exception as e: - print(f"✗ 性能统计信息重置失败: {e}") - - def update_camera_state(self, position: tuple, view_matrix: Any = None, - projection_matrix: Any = None, fov: float = 60.0): - """ - 更新相机状态 - - Args: - position: 相机位置 - view_matrix: 视图矩阵 - projection_matrix: 投影矩阵 - fov: 视野角度 - """ - try: - self.camera_state['position'] = position - self.camera_state['view_matrix'] = view_matrix - self.camera_state['projection_matrix'] = projection_matrix - self.camera_state['fov'] = fov - - except Exception as e: - print(f"✗ 相机状态更新失败: {e}") - - def apply_post_processing(self, render_target: Any) -> Any: - """ - 应用后处理效果 - - Args: - render_target: 渲染目标 - - Returns: - 处理后的渲染目标 - """ - try: - # 在实际实现中,这里会应用各种后处理效果 - # 如抗锯齿、泛光、景深、运动模糊等 - - if self.effect_config['anti_aliasing'] != 'none': - render_target = self._apply_anti_aliasing(render_target) - - if self.effect_config['bloom_enabled']: - render_target = self._apply_bloom(render_target) - - if self.effect_config['motion_blur']: - render_target = self._apply_motion_blur(render_target) - - if self.effect_config['depth_of_field']: - render_target = self._apply_depth_of_field(render_target) - - return render_target - - except Exception as e: - print(f"✗ 后处理应用失败: {e}") - return render_target - - def _apply_anti_aliasing(self, render_target: Any) -> Any: - """ - 应用抗锯齿 - - Args: - render_target: 渲染目标 - - Returns: - 处理后的渲染目标 - """ - try: - # 在实际实现中,这里会应用FXAA、SSAA或MSAA等抗锯齿技术 - aa_type = self.effect_config['anti_aliasing'] - print(f"✓ 应用抗锯齿: {aa_type}") - return render_target - - except Exception as e: - print(f"✗ 抗锯齿应用失败: {e}") - return render_target - - def _apply_bloom(self, render_target: Any) -> Any: - """ - 应用泛光效果 - - Args: - render_target: 渲染目标 - - Returns: - 处理后的渲染目标 - """ - try: - # 在实际实现中,这里会应用泛光效果 - intensity = self.effect_config['bloom_intensity'] - print(f"✓ 应用泛光效果 (强度: {intensity})") - return render_target - - except Exception as e: - print(f"✗ 泛光效果应用失败: {e}") - return render_target - - def _apply_motion_blur(self, render_target: Any) -> Any: - """ - 应用运动模糊 - - Args: - render_target: 渲染目标 - - Returns: - 处理后的渲染目标 - """ - try: - # 在实际实现中,这里会应用运动模糊效果 - samples = self.effect_config['motion_blur_samples'] - print(f"✓ 应用运动模糊 ({samples} 采样)") - return render_target - - except Exception as e: - print(f"✗ 运动模糊应用失败: {e}") - return render_target - - def _apply_depth_of_field(self, render_target: Any) -> Any: - """ - 应用景深效果 - - Args: - render_target: 渲染目标 - - Returns: - 处理后的渲染目标 - """ - try: - # 在实际实现中,这里会应用景深效果 - focus_distance = self.effect_config['dof_focus_distance'] - print(f"✓ 应用景深效果 (焦点距离: {focus_distance})") - return render_target - - except Exception as e: - print(f"✗ 景深效果应用失败: {e}") - return render_target - - def calculate_fluid_lighting(self, position: tuple, normal: tuple) -> tuple: - """ - 计算流体光照 - - Args: - position: 位置 - normal: 法线 - - Returns: - 光照颜色 (r, g, b) - """ - try: - # 获取光照配置 - ambient = self.lighting_config['ambient_light'] - diffuse = self.lighting_config['diffuse_light'] - specular = self.lighting_config['specular_light'] - light_dir = self.lighting_config['light_direction'] - - # 获取材质属性 - material_color = self.material_properties['color'][:3] # RGB - specular_color = self.material_properties['specular_color'] - shininess = self.material_properties['shininess'] - - # 简化的光照计算 - # 环境光 - ambient_contrib = ( - ambient[0] * material_color[0], - ambient[1] * material_color[1], - ambient[2] * material_color[2] - ) - - # 漫反射光 - # 归一化法线和光线方向 - norm_normal = np.array(normal) / np.linalg.norm(normal) - norm_light_dir = np.array(light_dir) / np.linalg.norm(light_dir) - - # 计算漫反射因子 - diffuse_factor = max(0.0, np.dot(norm_normal, norm_light_dir)) - - diffuse_contrib = ( - diffuse_factor * diffuse[0] * material_color[0], - diffuse_factor * diffuse[1] * material_color[1], - diffuse_factor * diffuse[2] * material_color[2] - ) - - # 镜面反射光 - specular_contrib = (0.0, 0.0, 0.0) - if diffuse_factor > 0: - # 计算反射向量 R = 2(N·L)N - L - reflect_dir = 2 * diffuse_factor * norm_normal - norm_light_dir - # 计算视线方向(简化为从原点到点的方向) - view_dir = np.array(position) / np.linalg.norm(position) - # 计算镜面反射因子 - specular_factor = max(0.0, np.dot(reflect_dir, view_dir)) ** shininess - - specular_contrib = ( - specular_factor * specular[0] * specular_color[0], - specular_factor * specular[1] * specular_color[1], - specular_factor * specular[2] * specular_color[2] - ) - - # 合成最终颜色 - final_color = ( - ambient_contrib[0] + diffuse_contrib[0] + specular_contrib[0], - ambient_contrib[1] + diffuse_contrib[1] + specular_contrib[1], - ambient_contrib[2] + diffuse_contrib[2] + specular_contrib[2] - ) - - # 确保颜色值在有效范围内 - final_color = ( - max(0.0, min(1.0, final_color[0])), - max(0.0, min(1.0, final_color[1])), - max(0.0, min(1.0, final_color[2])) - ) - - return final_color - - except Exception as e: - print(f"✗ 流体光照计算失败: {e}") +""" +流体渲染器模块 +负责流体的可视化渲染和效果处理 +""" + +import time +from typing import Dict, Any, List, Optional +import math +import numpy as np + +class FluidRenderer: + """ + 流体渲染器 + 负责流体的可视化渲染和效果处理 + """ + + def __init__(self, plugin): + """ + 初始化流体渲染器 + + Args: + plugin: 水体和流体模拟插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 渲染配置 + self.render_config = { + 'render_mode': 'volume', # 'volume', 'particles', 'surface' + 'quality_level': 'medium', # 'low', 'medium', 'high', 'ultra' + 'max_particles': 10000, + 'particle_size': 0.1, + 'render_distance': 100.0, + 'lod_enabled': True, + 'lod_distance': 50.0 + } + + # 着色器配置 + self.shader_config = { + 'fluid_shader': 'standard', + 'lighting_model': 'blinn_phong', + 'normal_mapping': True, + 'specular_mapping': True, + 'reflection_enabled': True, + 'refraction_enabled': True, + 'caustics_enabled': False, + 'foam_enabled': True + } + + # 材质属性 + self.material_properties = { + 'color': (0.0, 0.3, 0.8, 0.7), # RGBA + 'specular_color': (1.0, 1.0, 1.0), + 'shininess': 64.0, + 'reflectivity': 0.8, + 'refractive_index': 1.333, + 'absorption_coefficient': 0.1, + 'scattering_coefficient': 0.5 + } + + # 光照配置 + self.lighting_config = { + 'ambient_light': (0.2, 0.2, 0.2), + 'diffuse_light': (0.8, 0.8, 0.8), + 'specular_light': (1.0, 1.0, 1.0), + 'light_direction': (0.5, 1.0, 0.3) + } + + # 体积渲染配置 + self.volume_config = { + 'sampling_rate': 64, + 'step_size': 0.1, + 'density_scale': 1.0, + 'gradient_factor': 1.0, + 'shadow_enabled': True, + 'shadow_steps': 16 + } + + # 粒子渲染配置 + self.particle_config = { + 'sprite_type': 'quad', # 'quad', 'billboard', 'sphere' + 'blend_mode': 'additive', # 'alpha', 'additive', 'subtractive' + 'size_variation': 0.5, + 'velocity_scale': 1.0, + 'life_scale': True + } + + # 渲染状态 + self.render_state = { + 'last_render_time': 0.0, + 'frames_rendered': 0, + 'particles_rendered': 0, + 'triangles_rendered': 0 + } + + # 性能统计 + self.performance_stats = { + 'render_calls': 0, + 'render_time_total': 0.0, + 'average_render_time': 0.0, + 'particles_drawn': 0, + 'volume_samples': 0 + } + + # 特效配置 + self.effect_config = { + 'motion_blur': False, + 'motion_blur_samples': 8, + 'anti_aliasing': 'fxaa', # 'none', 'fxaa', 'ssaa', 'msaa' + 'bloom_enabled': True, + 'bloom_intensity': 0.5, + 'depth_of_field': False, + 'dof_focus_distance': 10.0 + } + + # 相机和视图 + self.camera_state = { + 'position': (0.0, 0.0, 0.0), + 'view_matrix': None, + 'projection_matrix': None, + 'fov': 60.0 + } + + print("✓ 流体渲染器已创建") + + def initialize(self) -> bool: + """ + 初始化流体渲染器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化渲染资源 + self._initialize_render_resources() + + self.initialized = True + print("✓ 流体渲染器初始化完成") + return True + + except Exception as e: + print(f"✗ 流体渲染器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用流体渲染器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 流体渲染器未初始化") + return False + + self.enabled = True + print("✓ 流体渲染器已启用") + return True + + except Exception as e: + print(f"✗ 流体渲染器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用流体渲染器""" + try: + self.enabled = False + print("✓ 流体渲染器已禁用") + + except Exception as e: + print(f"✗ 流体渲染器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理流体渲染器资源""" + try: + self.disable() + self._cleanup_render_resources() + self.initialized = False + print("✓ 流体渲染器资源已清理") + + except Exception as e: + print(f"✗ 流体渲染器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新流体渲染器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新渲染状态 + self.render_state['last_render_time'] += dt + self.render_state['frames_rendered'] += 1 + + except Exception as e: + print(f"✗ 流体渲染器更新失败: {e}") + import traceback + traceback.print_exc() + + def _initialize_render_resources(self): + """初始化渲染资源""" + try: + # 在实际实现中,这里会初始化着色器、纹理、缓冲区等 + print("✓ 渲染资源初始化完成") + except Exception as e: + print(f"✗ 渲染资源初始化失败: {e}") + raise + + def _cleanup_render_resources(self): + """清理渲染资源""" + try: + # 在实际实现中,这里会释放着色器、纹理、缓冲区等资源 + print("✓ 渲染资源清理完成") + except Exception as e: + print(f"✗ 渲染资源清理失败: {e}") + + def render_fluid(self, fluid_data: Dict[str, Any], camera_position: tuple): + """ + 渲染流体 + + Args: + fluid_data: 流体数据字典 + camera_position: 相机位置 + """ + try: + start_time = time.time() + + # 更新相机状态 + self.camera_state['position'] = camera_position + + # 根据渲染模式选择渲染方法 + render_mode = self.render_config['render_mode'] + + if render_mode == 'volume': + self._render_volume_fluid(fluid_data) + elif render_mode == 'particles': + self._render_particle_fluid(fluid_data) + elif render_mode == 'surface': + self._render_surface_fluid(fluid_data) + else: + # 默认使用体积渲染 + self._render_volume_fluid(fluid_data) + + # 更新性能统计 + render_time = time.time() - start_time + self.performance_stats['render_calls'] += 1 + self.performance_stats['render_time_total'] += render_time + + if self.performance_stats['render_calls'] > 0: + self.performance_stats['average_render_time'] = ( + self.performance_stats['render_time_total'] / + self.performance_stats['render_calls'] + ) + + # 更新渲染状态 + self.render_state['frames_rendered'] += 1 + + except Exception as e: + print(f"✗ 流体渲染失败: {e}") + import traceback + traceback.print_exc() + + def _render_volume_fluid(self, fluid_data: Dict[str, Any]): + """ + 体积渲染流体 + + Args: + fluid_data: 流体数据 + """ + try: + # 获取流体场数据 + velocity_field = fluid_data.get('velocity_field') + density_field = fluid_data.get('density_field') + temperature_field = fluid_data.get('temperature_field') + + if not density_field: + return + + # 获取体积渲染配置 + sampling_rate = self.volume_config['sampling_rate'] + step_size = self.volume_config['step_size'] + density_scale = self.volume_config['density_scale'] + + # 在实际实现中,这里会: + # 1. 设置体积着色器参数 + # 2. 绑定体积纹理 + # 3. 执行光线行进算法 + # 4. 应用光照和材质属性 + # 5. 渲染到屏幕 + + # 更新统计信息 + self.performance_stats['volume_samples'] += sampling_rate + + print("✓ 体积流体渲染完成") + + except Exception as e: + print(f"✗ 体积流体渲染失败: {e}") + + def _render_particle_fluid(self, fluid_data: Dict[str, Any]): + """ + 粒子渲染流体 + + Args: + fluid_data: 流体数据 + """ + try: + # 获取粒子数据 + particles = fluid_data.get('particles', []) + + if not particles: + return + + # 获取粒子渲染配置 + sprite_type = self.particle_config['sprite_type'] + blend_mode = self.particle_config['blend_mode'] + particle_size = self.render_config['particle_size'] + + # 在实际实现中,这里会: + # 1. 设置粒子着色器参数 + # 2. 准备粒子顶点数据 + # 3. 应用材质属性 + # 4. 执行粒子渲染 + + # 更新统计信息 + particles_count = len(particles) + self.performance_stats['particles_drawn'] += particles_count + self.render_state['particles_rendered'] += particles_count + + print(f"✓ 粒子流体渲染完成 ({particles_count} 粒子)") + + except Exception as e: + print(f"✗ 粒子流体渲染失败: {e}") + + def _render_surface_fluid(self, fluid_data: Dict[str, Any]): + """ + 表面渲染流体 + + Args: + fluid_data: 流体数据 + """ + try: + # 获取表面网格数据 + vertices = fluid_data.get('vertices', []) + normals = fluid_data.get('normals', []) + indices = fluid_data.get('indices', []) + + if not vertices or not indices: + return + + # 在实际实现中,这里会: + # 1. 设置表面着色器参数 + # 2. 上传网格数据到GPU + # 3. 应用材质和光照 + # 4. 执行网格渲染 + + # 更新统计信息 + triangle_count = len(indices) // 3 + self.render_state['triangles_rendered'] += triangle_count + + print(f"✓ 表面流体渲染完成 ({triangle_count} 三角形)") + + except Exception as e: + print(f"✗ 表面流体渲染失败: {e}") + + def set_render_config(self, config: Dict[str, Any]): + """ + 设置渲染配置 + + Args: + config: 渲染配置字典 + """ + try: + self.render_config.update(config) + print(f"✓ 渲染配置已更新: {self.render_config}") + except Exception as e: + print(f"✗ 渲染配置设置失败: {e}") + + def get_render_config(self) -> Dict[str, Any]: + """ + 获取渲染配置 + + Returns: + 渲染配置字典 + """ + return self.render_config.copy() + + def set_shader_config(self, config: Dict[str, Any]): + """ + 设置着色器配置 + + Args: + config: 着色器配置字典 + """ + try: + self.shader_config.update(config) + print(f"✓ 着色器配置已更新: {self.shader_config}") + except Exception as e: + print(f"✗ 着色器配置设置失败: {e}") + + def get_shader_config(self) -> Dict[str, Any]: + """ + 获取着色器配置 + + Returns: + 着色器配置字典 + """ + return self.shader_config.copy() + + def set_material_properties(self, properties: Dict[str, Any]): + """ + 设置材质属性 + + Args: + properties: 材质属性字典 + """ + try: + self.material_properties.update(properties) + print(f"✓ 材质属性已更新: {self.material_properties}") + except Exception as e: + print(f"✗ 材质属性设置失败: {e}") + + def get_material_properties(self) -> Dict[str, Any]: + """ + 获取材质属性 + + Returns: + 材质属性字典 + """ + return self.material_properties.copy() + + def set_lighting_config(self, config: Dict[str, Any]): + """ + 设置光照配置 + + Args: + config: 光照配置字典 + """ + try: + self.lighting_config.update(config) + print(f"✓ 光照配置已更新: {self.lighting_config}") + except Exception as e: + print(f"✗ 光照配置设置失败: {e}") + + def get_lighting_config(self) -> Dict[str, Any]: + """ + 获取光照配置 + + Returns: + 光照配置字典 + """ + return self.lighting_config.copy() + + def set_volume_config(self, config: Dict[str, Any]): + """ + 设置体积渲染配置 + + Args: + config: 体积渲染配置字典 + """ + try: + self.volume_config.update(config) + print(f"✓ 体积渲染配置已更新: {self.volume_config}") + except Exception as e: + print(f"✗ 体积渲染配置设置失败: {e}") + + def get_volume_config(self) -> Dict[str, Any]: + """ + 获取体积渲染配置 + + Returns: + 体积渲染配置字典 + """ + return self.volume_config.copy() + + def set_particle_config(self, config: Dict[str, Any]): + """ + 设置粒子渲染配置 + + Args: + config: 粒子渲染配置字典 + """ + try: + self.particle_config.update(config) + print(f"✓ 粒子渲染配置已更新: {self.particle_config}") + except Exception as e: + print(f"✗ 粒子渲染配置设置失败: {e}") + + def get_particle_config(self) -> Dict[str, Any]: + """ + 获取粒子渲染配置 + + Returns: + 粒子渲染配置字典 + """ + return self.particle_config.copy() + + def set_effect_config(self, config: Dict[str, Any]): + """ + 设置特效配置 + + Args: + config: 特效配置字典 + """ + try: + self.effect_config.update(config) + print(f"✓ 特效配置已更新: {self.effect_config}") + except Exception as e: + print(f"✗ 特效配置设置失败: {e}") + + def get_effect_config(self) -> Dict[str, Any]: + """ + 获取特效配置 + + Returns: + 特效配置字典 + """ + return self.effect_config.copy() + + def get_render_state(self) -> Dict[str, Any]: + """ + 获取渲染状态 + + Returns: + 渲染状态字典 + """ + return self.render_state.copy() + + def get_performance_stats(self) -> Dict[str, Any]: + """ + 获取性能统计信息 + + Returns: + 性能统计字典 + """ + return self.performance_stats.copy() + + def reset_performance_stats(self): + """重置性能统计信息""" + try: + self.performance_stats = { + 'render_calls': 0, + 'render_time_total': 0.0, + 'average_render_time': 0.0, + 'particles_drawn': 0, + 'volume_samples': 0 + } + print("✓ 性能统计信息已重置") + except Exception as e: + print(f"✗ 性能统计信息重置失败: {e}") + + def update_camera_state(self, position: tuple, view_matrix: Any = None, + projection_matrix: Any = None, fov: float = 60.0): + """ + 更新相机状态 + + Args: + position: 相机位置 + view_matrix: 视图矩阵 + projection_matrix: 投影矩阵 + fov: 视野角度 + """ + try: + self.camera_state['position'] = position + self.camera_state['view_matrix'] = view_matrix + self.camera_state['projection_matrix'] = projection_matrix + self.camera_state['fov'] = fov + + except Exception as e: + print(f"✗ 相机状态更新失败: {e}") + + def apply_post_processing(self, render_target: Any) -> Any: + """ + 应用后处理效果 + + Args: + render_target: 渲染目标 + + Returns: + 处理后的渲染目标 + """ + try: + # 在实际实现中,这里会应用各种后处理效果 + # 如抗锯齿、泛光、景深、运动模糊等 + + if self.effect_config['anti_aliasing'] != 'none': + render_target = self._apply_anti_aliasing(render_target) + + if self.effect_config['bloom_enabled']: + render_target = self._apply_bloom(render_target) + + if self.effect_config['motion_blur']: + render_target = self._apply_motion_blur(render_target) + + if self.effect_config['depth_of_field']: + render_target = self._apply_depth_of_field(render_target) + + return render_target + + except Exception as e: + print(f"✗ 后处理应用失败: {e}") + return render_target + + def _apply_anti_aliasing(self, render_target: Any) -> Any: + """ + 应用抗锯齿 + + Args: + render_target: 渲染目标 + + Returns: + 处理后的渲染目标 + """ + try: + # 在实际实现中,这里会应用FXAA、SSAA或MSAA等抗锯齿技术 + aa_type = self.effect_config['anti_aliasing'] + print(f"✓ 应用抗锯齿: {aa_type}") + return render_target + + except Exception as e: + print(f"✗ 抗锯齿应用失败: {e}") + return render_target + + def _apply_bloom(self, render_target: Any) -> Any: + """ + 应用泛光效果 + + Args: + render_target: 渲染目标 + + Returns: + 处理后的渲染目标 + """ + try: + # 在实际实现中,这里会应用泛光效果 + intensity = self.effect_config['bloom_intensity'] + print(f"✓ 应用泛光效果 (强度: {intensity})") + return render_target + + except Exception as e: + print(f"✗ 泛光效果应用失败: {e}") + return render_target + + def _apply_motion_blur(self, render_target: Any) -> Any: + """ + 应用运动模糊 + + Args: + render_target: 渲染目标 + + Returns: + 处理后的渲染目标 + """ + try: + # 在实际实现中,这里会应用运动模糊效果 + samples = self.effect_config['motion_blur_samples'] + print(f"✓ 应用运动模糊 ({samples} 采样)") + return render_target + + except Exception as e: + print(f"✗ 运动模糊应用失败: {e}") + return render_target + + def _apply_depth_of_field(self, render_target: Any) -> Any: + """ + 应用景深效果 + + Args: + render_target: 渲染目标 + + Returns: + 处理后的渲染目标 + """ + try: + # 在实际实现中,这里会应用景深效果 + focus_distance = self.effect_config['dof_focus_distance'] + print(f"✓ 应用景深效果 (焦点距离: {focus_distance})") + return render_target + + except Exception as e: + print(f"✗ 景深效果应用失败: {e}") + return render_target + + def calculate_fluid_lighting(self, position: tuple, normal: tuple) -> tuple: + """ + 计算流体光照 + + Args: + position: 位置 + normal: 法线 + + Returns: + 光照颜色 (r, g, b) + """ + try: + # 获取光照配置 + ambient = self.lighting_config['ambient_light'] + diffuse = self.lighting_config['diffuse_light'] + specular = self.lighting_config['specular_light'] + light_dir = self.lighting_config['light_direction'] + + # 获取材质属性 + material_color = self.material_properties['color'][:3] # RGB + specular_color = self.material_properties['specular_color'] + shininess = self.material_properties['shininess'] + + # 简化的光照计算 + # 环境光 + ambient_contrib = ( + ambient[0] * material_color[0], + ambient[1] * material_color[1], + ambient[2] * material_color[2] + ) + + # 漫反射光 + # 归一化法线和光线方向 + norm_normal = np.array(normal) / np.linalg.norm(normal) + norm_light_dir = np.array(light_dir) / np.linalg.norm(light_dir) + + # 计算漫反射因子 + diffuse_factor = max(0.0, np.dot(norm_normal, norm_light_dir)) + + diffuse_contrib = ( + diffuse_factor * diffuse[0] * material_color[0], + diffuse_factor * diffuse[1] * material_color[1], + diffuse_factor * diffuse[2] * material_color[2] + ) + + # 镜面反射光 + specular_contrib = (0.0, 0.0, 0.0) + if diffuse_factor > 0: + # 计算反射向量 R = 2(N·L)N - L + reflect_dir = 2 * diffuse_factor * norm_normal - norm_light_dir + # 计算视线方向(简化为从原点到点的方向) + view_dir = np.array(position) / np.linalg.norm(position) + # 计算镜面反射因子 + specular_factor = max(0.0, np.dot(reflect_dir, view_dir)) ** shininess + + specular_contrib = ( + specular_factor * specular[0] * specular_color[0], + specular_factor * specular[1] * specular_color[1], + specular_factor * specular[2] * specular_color[2] + ) + + # 合成最终颜色 + final_color = ( + ambient_contrib[0] + diffuse_contrib[0] + specular_contrib[0], + ambient_contrib[1] + diffuse_contrib[1] + specular_contrib[1], + ambient_contrib[2] + diffuse_contrib[2] + specular_contrib[2] + ) + + # 确保颜色值在有效范围内 + final_color = ( + max(0.0, min(1.0, final_color[0])), + max(0.0, min(1.0, final_color[1])), + max(0.0, min(1.0, final_color[2])) + ) + + return final_color + + except Exception as e: + print(f"✗ 流体光照计算失败: {e}") return (0.5, 0.5, 0.5) \ No newline at end of file diff --git a/plugins/user/fluid_simulation/rendering/water_renderer.py b/plugins/user/fluid_simulation/rendering/water_renderer.py index f1c438de..b7b10a1a 100644 --- a/plugins/user/fluid_simulation/rendering/water_renderer.py +++ b/plugins/user/fluid_simulation/rendering/water_renderer.py @@ -1,978 +1,978 @@ -""" -水体渲染器模块 -负责水体的高质量渲染和视觉效果 -""" - -import time -from typing import Dict, Any, List, Optional, Tuple -import math -import numpy as np - -class WaterRenderer: - """ - 水体渲染器 - 负责水体的高质量渲染和视觉效果,包括反射、折射、泡沫等 - """ - - def __init__(self, plugin): - """ - 初始化水体渲染器 - - Args: - plugin: 水体和流体模拟插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 渲染配置 - self.render_config = { - 'quality_level': 'medium', # 'low', 'medium', 'high', 'ultra' - 'tessellation_factor': 4.0, - 'lod_enabled': True, - 'lod_distance': 100.0, - 'max_patch_size': 50.0, - 'render_distance': 500.0 - } - - # 着色器配置 - self.shader_config = { - 'water_shader': 'pbr', # 'standard', 'pbr', 'advanced' - 'lighting_model': 'cook_torrance', # 'blinn_phong', 'cook_torrance', 'ward' - 'normal_mapping': True, - 'detail_mapping': True, - 'reflection_enabled': True, - 'refraction_enabled': True, - 'caustics_enabled': True, - 'foam_enabled': True, - 'underwater_effects': True - } - - # 材质属性 - self.material_properties = { - 'base_color': (0.0, 0.2, 0.5, 0.9), # RGBA - 'specular_color': (0.8, 0.8, 0.9), - 'roughness': 0.05, - 'metallic': 0.0, - 'reflectance': 0.95, - 'refractive_index': 1.333, - 'absorption_color': (0.0, 0.05, 0.15), - 'absorption_coefficient': 0.1 - } - - # 光照配置 - self.lighting_config = { - 'ambient_light': (0.1, 0.1, 0.15), - 'diffuse_light': (0.9, 0.9, 0.95), - 'specular_light': (1.0, 1.0, 1.0), - 'light_direction': (0.5, 1.0, 0.3), - 'shadow_enabled': True, - 'shadow_quality': 'medium' - } - - # 波浪渲染配置 - self.wave_render_config = { - 'normal_strength': 1.0, - 'fresnel_power': 5.0, - 'fresnel_bias': 0.2, - 'wave_scale': 1.0, - 'wave_speed': 1.0, - 'wave_detail': 0.5 - } - - # 反射配置 - self.reflection_config = { - 'reflection_quality': 'medium', # 'low', 'medium', 'high', 'screen' - 'planar_reflections': True, - 'sky_reflections': True, - 'reflection_distance': 200.0, - 'update_frequency': 30.0 # Hz - } - - # 折射配置 - self.refraction_config = { - 'refraction_quality': 'medium', # 'low', 'medium', 'high' - 'chromatic_aberration': True, - 'chromatic_intensity': 0.02 - } - - # 泡沫配置 - self.foam_config = { - 'foam_enabled': True, - 'foam_threshold': 0.7, - 'foam_smoothness': 0.3, - 'foam_speed': 0.5, - 'foam_color': (0.95, 0.95, 0.95, 0.8) - } - - # 焦散配置 - self.caustics_config = { - 'caustics_enabled': True, - 'caustics_intensity': 1.0, - 'caustics_scale': 5.0, - 'caustics_speed': 0.2, - 'caustics_color': (0.9, 0.8, 0.6) - } - - # 水下效果配置 - self.underwater_config = { - 'underwater_fog': True, - 'fog_density': 0.05, - 'fog_color': (0.0, 0.1, 0.2), - 'god_rays_enabled': True, - 'god_rays_intensity': 0.5 - } - - # 渲染状态 - self.render_state = { - 'last_render_time': 0.0, - 'frames_rendered': 0, - 'patches_rendered': 0, - 'triangles_rendered': 0 - } - - # 性能统计 - self.performance_stats = { - 'render_calls': 0, - 'render_time_total': 0.0, - 'average_render_time': 0.0, - 'patches_drawn': 0, - 'vertices_processed': 0 - } - - # 特效配置 - self.effect_config = { - 'motion_blur': False, - 'anti_aliasing': 'fxaa', # 'none', 'fxaa', 'ssaa', 'msaa' - 'bloom_enabled': True, - 'bloom_intensity': 0.3, - 'depth_of_field': False, - 'dof_focus_distance': 50.0 - } - - # 相机和视图 - self.camera_state = { - 'position': (0.0, 0.0, 0.0), - 'view_matrix': None, - 'projection_matrix': None, - 'fov': 60.0 - } - - # 环境映射 - self.environment_mapping = { - 'skybox_enabled': True, - 'skybox_texture': None, - 'irradiance_map': None, - 'prefiltered_map': None - } - - print("✓ 水体渲染器已创建") - - def initialize(self) -> bool: - """ - 初始化水体渲染器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化渲染资源 - self._initialize_render_resources() - - self.initialized = True - print("✓ 水体渲染器初始化完成") - return True - - except Exception as e: - print(f"✗ 水体渲染器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用水体渲染器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 水体渲染器未初始化") - return False - - self.enabled = True - print("✓ 水体渲染器已启用") - return True - - except Exception as e: - print(f"✗ 水体渲染器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用水体渲染器""" - try: - self.enabled = False - print("✓ 水体渲染器已禁用") - - except Exception as e: - print(f"✗ 水体渲染器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理水体渲染器资源""" - try: - self.disable() - self._cleanup_render_resources() - self.initialized = False - print("✓ 水体渲染器资源已清理") - - except Exception as e: - print(f"✗ 水体渲染器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新水体渲染器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新渲染状态 - self.render_state['last_render_time'] += dt - self.render_state['frames_rendered'] += 1 - - # 更新波浪动画 - self._update_wave_animation(dt) - - except Exception as e: - print(f"✗ 水体渲染器更新失败: {e}") - import traceback - traceback.print_exc() - - def _initialize_render_resources(self): - """初始化渲染资源""" - try: - # 在实际实现中,这里会初始化着色器、纹理、缓冲区等 - print("✓ 水体渲染资源初始化完成") - except Exception as e: - print(f"✗ 水体渲染资源初始化失败: {e}") - raise - - def _cleanup_render_resources(self): - """清理渲染资源""" - try: - # 在实际实现中,这里会释放着色器、纹理、缓冲区等资源 - print("✓ 水体渲染资源清理完成") - except Exception as e: - print(f"✗ 水体渲染资源清理失败: {e}") - - def _update_wave_animation(self, dt: float): - """ - 更新波浪动画 - - Args: - dt: 时间增量 - """ - try: - # 在实际实现中,这里会更新波浪动画参数 - # 用于纹理坐标滚动、法线贴图动画等 - pass - except Exception as e: - print(f"✗ 波浪动画更新失败: {e}") - - def render_water(self, water_data: Dict[str, Any], camera_position: tuple): - """ - 渲染水体 - - Args: - water_data: 水体数据字典 - camera_position: 相机位置 - """ - try: - start_time = time.time() - - # 更新相机状态 - self.camera_state['position'] = camera_position - - # 获取水体网格数据 - vertices = water_data.get('vertices', []) - normals = water_data.get('normals', []) - uvs = water_data.get('uvs', []) - indices = water_data.get('indices', []) - - if not vertices or not indices: - return - - # 应用LOD(如果启用) - if self.render_config['lod_enabled']: - vertices, normals, uvs, indices = self._apply_lod( - vertices, normals, uvs, indices, camera_position - ) - - # 在实际实现中,这里会: - # 1. 设置水体着色器参数 - # 2. 上传网格数据到GPU - # 3. 应用材质和光照 - # 4. 处理反射/折射效果 - # 5. 执行网格渲染 - - # 更新统计信息 - triangle_count = len(indices) // 3 - self.performance_stats['render_calls'] += 1 - self.performance_stats['patches_drawn'] += 1 - self.performance_stats['vertices_processed'] += len(vertices) - self.render_state['patches_rendered'] += 1 - self.render_state['triangles_rendered'] += triangle_count - - render_time = time.time() - start_time - self.performance_stats['render_time_total'] += render_time - - if self.performance_stats['render_calls'] > 0: - self.performance_stats['average_render_time'] = ( - self.performance_stats['render_time_total'] / - self.performance_stats['render_calls'] - ) - - print(f"✓ 水体渲染完成 ({triangle_count} 三角形)") - - except Exception as e: - print(f"✗ 水体渲染失败: {e}") - import traceback - traceback.print_exc() - - def _apply_lod(self, vertices: List[Tuple[float, float, float]], - normals: List[Tuple[float, float, float]], - uvs: List[Tuple[float, float]], - indices: List[int], - camera_position: Tuple[float, float, float]) -> Tuple[List, List, List, List]: - """ - 应用LOD(细节层次) - - Args: - vertices: 顶点列表 - normals: 法线列表 - uvs: UV坐标列表 - indices: 索引列表 - camera_position: 相机位置 - - Returns: - 简化后的网格数据 - """ - try: - # 计算到相机的距离 - # 在实际实现中,这里会根据距离简化网格 - lod_distance = self.render_config['lod_distance'] - - # 简化处理(示例) - return vertices, normals, uvs, indices - - except Exception as e: - print(f"✗ LOD应用失败: {e}") - return vertices, normals, uvs, indices - - def set_render_config(self, config: Dict[str, Any]): - """ - 设置渲染配置 - - Args: - config: 渲染配置字典 - """ - try: - self.render_config.update(config) - print(f"✓ 渲染配置已更新: {self.render_config}") - except Exception as e: - print(f"✗ 渲染配置设置失败: {e}") - - def get_render_config(self) -> Dict[str, Any]: - """ - 获取渲染配置 - - Returns: - 渲染配置字典 - """ - return self.render_config.copy() - - def set_shader_config(self, config: Dict[str, Any]): - """ - 设置着色器配置 - - Args: - config: 着色器配置字典 - """ - try: - self.shader_config.update(config) - print(f"✓ 着色器配置已更新: {self.shader_config}") - except Exception as e: - print(f"✗ 着色器配置设置失败: {e}") - - def get_shader_config(self) -> Dict[str, Any]: - """ - 获取着色器配置 - - Returns: - 着色器配置字典 - """ - return self.shader_config.copy() - - def set_material_properties(self, properties: Dict[str, Any]): - """ - 设置材质属性 - - Args: - properties: 材质属性字典 - """ - try: - self.material_properties.update(properties) - print(f"✓ 材质属性已更新: {self.material_properties}") - except Exception as e: - print(f"✗ 材质属性设置失败: {e}") - - def get_material_properties(self) -> Dict[str, Any]: - """ - 获取材质属性 - - Returns: - 材质属性字典 - """ - return self.material_properties.copy() - - def set_lighting_config(self, config: Dict[str, Any]): - """ - 设置光照配置 - - Args: - config: 光照配置字典 - """ - try: - self.lighting_config.update(config) - print(f"✓ 光照配置已更新: {self.lighting_config}") - except Exception as e: - print(f"✗ 光照配置设置失败: {e}") - - def get_lighting_config(self) -> Dict[str, Any]: - """ - 获取光照配置 - - Returns: - 光照配置字典 - """ - return self.lighting_config.copy() - - def set_wave_render_config(self, config: Dict[str, Any]): - """ - 设置波浪渲染配置 - - Args: - config: 波浪渲染配置字典 - """ - try: - self.wave_render_config.update(config) - print(f"✓ 波浪渲染配置已更新: {self.wave_render_config}") - except Exception as e: - print(f"✗ 波浪渲染配置设置失败: {e}") - - def get_wave_render_config(self) -> Dict[str, Any]: - """ - 获取波浪渲染配置 - - Returns: - 波浪渲染配置字典 - """ - return self.wave_render_config.copy() - - def set_reflection_config(self, config: Dict[str, Any]): - """ - 设置反射配置 - - Args: - config: 反射配置字典 - """ - try: - self.reflection_config.update(config) - print(f"✓ 反射配置已更新: {self.reflection_config}") - except Exception as e: - print(f"✗ 反射配置设置失败: {e}") - - def get_reflection_config(self) -> Dict[str, Any]: - """ - 获取反射配置 - - Returns: - 反射配置字典 - """ - return self.reflection_config.copy() - - def set_refraction_config(self, config: Dict[str, Any]): - """ - 设置折射配置 - - Args: - config: 折射配置字典 - """ - try: - self.refraction_config.update(config) - print(f"✓ 折射配置已更新: {self.refraction_config}") - except Exception as e: - print(f"✗ 折射配置设置失败: {e}") - - def get_refraction_config(self) -> Dict[str, Any]: - """ - 获取折射配置 - - Returns: - 折射配置字典 - """ - return self.refraction_config.copy() - - def set_foam_config(self, config: Dict[str, Any]): - """ - 设置泡沫配置 - - Args: - config: 泡沫配置字典 - """ - try: - self.foam_config.update(config) - print(f"✓ 泡沫配置已更新: {self.foam_config}") - except Exception as e: - print(f"✗ 泡沫配置设置失败: {e}") - - def get_foam_config(self) -> Dict[str, Any]: - """ - 获取泡沫配置 - - Returns: - 泡沫配置字典 - """ - return self.foam_config.copy() - - def set_caustics_config(self, config: Dict[str, Any]): - """ - 设置焦散配置 - - Args: - config: 焦散配置字典 - """ - try: - self.caustics_config.update(config) - print(f"✓ 焦散配置已更新: {self.caustics_config}") - except Exception as e: - print(f"✗ 焦散配置设置失败: {e}") - - def get_caustics_config(self) -> Dict[str, Any]: - """ - 获取焦散配置 - - Returns: - 焦散配置字典 - """ - return self.caustics_config.copy() - - def set_underwater_config(self, config: Dict[str, Any]): - """ - 设置水下效果配置 - - Args: - config: 水下效果配置字典 - """ - try: - self.underwater_config.update(config) - print(f"✓ 水下效果配置已更新: {self.underwater_config}") - except Exception as e: - print(f"✗ 水下效果配置设置失败: {e}") - - def get_underwater_config(self) -> Dict[str, Any]: - """ - 获取水下效果配置 - - Returns: - 水下效果配置字典 - """ - return self.underwater_config.copy() - - def set_effect_config(self, config: Dict[str, Any]): - """ - 设置特效配置 - - Args: - config: 特效配置字典 - """ - try: - self.effect_config.update(config) - print(f"✓ 特效配置已更新: {self.effect_config}") - except Exception as e: - print(f"✗ 特效配置设置失败: {e}") - - def get_effect_config(self) -> Dict[str, Any]: - """ - 获取特效配置 - - Returns: - 特效配置字典 - """ - return self.effect_config.copy() - - def get_render_state(self) -> Dict[str, Any]: - """ - 获取渲染状态 - - Returns: - 渲染状态字典 - """ - return self.render_state.copy() - - def get_performance_stats(self) -> Dict[str, Any]: - """ - 获取性能统计信息 - - Returns: - 性能统计字典 - """ - return self.performance_stats.copy() - - def reset_performance_stats(self): - """重置性能统计信息""" - try: - self.performance_stats = { - 'render_calls': 0, - 'render_time_total': 0.0, - 'average_render_time': 0.0, - 'patches_drawn': 0, - 'vertices_processed': 0 - } - print("✓ 性能统计信息已重置") - except Exception as e: - print(f"✗ 性能统计信息重置失败: {e}") - - def update_camera_state(self, position: tuple, view_matrix: Any = None, - projection_matrix: Any = None, fov: float = 60.0): - """ - 更新相机状态 - - Args: - position: 相机位置 - view_matrix: 视图矩阵 - projection_matrix: 投影矩阵 - fov: 视野角度 - """ - try: - self.camera_state['position'] = position - self.camera_state['view_matrix'] = view_matrix - self.camera_state['projection_matrix'] = projection_matrix - self.camera_state['fov'] = fov - - except Exception as e: - print(f"✗ 相机状态更新失败: {e}") - - def calculate_water_lighting(self, position: tuple, normal: tuple, - view_direction: tuple) -> tuple: - """ - 计算水体光照(基于物理的渲染) - - Args: - position: 位置 - normal: 波浪法线 - view_direction: 视线方向 - - Returns: - 光照颜色 (r, g, b) - """ - try: - # 获取光照配置 - ambient = self.lighting_config['ambient_light'] - diffuse = self.lighting_config['diffuse_light'] - light_dir = self.lighting_config['light_direction'] - - # 获取材质属性 - base_color = self.material_properties['base_color'][:3] - specular_color = self.material_properties['specular_color'] - roughness = self.material_properties['roughness'] - metallic = self.material_properties['metallic'] - - # 归一化向量 - norm_normal = np.array(normal) / np.linalg.norm(normal) - norm_light_dir = np.array(light_dir) / np.linalg.norm(light_dir) - norm_view_dir = np.array(view_direction) / np.linalg.norm(view_direction) - - # 计算半角向量 - half_vector = norm_light_dir + norm_view_dir - norm_half_vector = half_vector / np.linalg.norm(half_vector) - - # 计算Fresnel效应 - fresnel = self._calculate_fresnel(norm_normal, norm_view_dir) - - # 漫反射项 (Lambertian) - ndotl = max(0.0, np.dot(norm_normal, norm_light_dir)) - diffuse_contrib = ( - ndotl * diffuse[0] * base_color[0], - ndotl * diffuse[1] * base_color[1], - ndotl * diffuse[2] * base_color[2] - ) - - # 镜面反射项 (Cook-Torrance) - specular_contrib = (0.0, 0.0, 0.0) - if ndotl > 0: - # 法线分布函数 (Trowbridge-Reitz GGX) - ndoth = max(0.0, np.dot(norm_normal, norm_half_vector)) - alpha = roughness * roughness - alpha2 = alpha * alpha - denom = ndoth * ndoth * (alpha2 - 1.0) + 1.0 - d = alpha2 / (math.pi * denom * denom) - - # 几何函数 - ndotv = max(0.0, np.dot(norm_normal, norm_view_dir)) - k = (roughness + 1.0) * (roughness + 1.0) / 8.0 - g1l = ndotl / (ndotl * (1.0 - k) + k) - g1v = ndotv / (ndotv * (1.0 - k) + k) - g = g1l * g1v - - # Fresnel项 (Schlick近似) - f0 = np.array(specular_color) if metallic > 0.5 else np.array([0.04, 0.04, 0.04]) - f = f0 + (1.0 - f0) * pow(1.0 - np.dot(norm_half_vector, norm_view_dir), 5.0) - - # 完整的镜面反射贡献 - if (ndotl * ndotv) > 0: - specular = (d * g * f) / (4.0 * ndotl * ndotv) - specular_contrib = ( - specular[0] * diffuse[0], - specular[1] * diffuse[1], - specular[2] * diffuse[2] - ) - - # 环境光项 - ambient_contrib = ( - ambient[0] * base_color[0], - ambient[1] * base_color[1], - ambient[2] * base_color[2] - ) - - # 合成最终颜色 - final_color = ( - ambient_contrib[0] + diffuse_contrib[0] + specular_contrib[0], - ambient_contrib[1] + diffuse_contrib[1] + specular_contrib[1], - ambient_contrib[2] + diffuse_contrib[2] + specular_contrib[2] - ) - - # 应用Fresnel效应 - final_color = ( - final_color[0] * (1.0 - fresnel) + fresnel, - final_color[1] * (1.0 - fresnel) + fresnel, - final_color[2] * (1.0 - fresnel) + fresnel - ) - - # 确保颜色值在有效范围内 - final_color = ( - max(0.0, min(1.0, final_color[0])), - max(0.0, min(1.0, final_color[1])), - max(0.0, min(1.0, final_color[2])) - ) - - return final_color - - except Exception as e: - print(f"✗ 水体光照计算失败: {e}") - return (0.1, 0.2, 0.5) - - def _calculate_fresnel(self, normal: np.ndarray, view_dir: np.ndarray) -> float: - """ - 计算Fresnel反射系数 - - Args: - normal: 表面法线 - view_dir: 视线方向 - - Returns: - Fresnel系数 - """ - try: - # 简化的Fresnel计算 - ndotv = max(0.0, np.dot(normal, view_dir)) - fresnel = pow(1.0 - ndotv, self.wave_render_config['fresnel_power']) - fresnel = max(0.0, min(1.0, fresnel + self.wave_render_config['fresnel_bias'])) - return fresnel - - except Exception as e: - return 0.5 - - def calculate_wave_normal(self, position: tuple, time_value: float) -> tuple: - """ - 计算波浪法线 - - Args: - position: 位置 (x, z) - time_value: 时间值 - - Returns: - 波浪法线 (nx, ny, nz) - """ - try: - # 在实际实现中,这里会根据波浪系统计算精确的法线 - # 简化处理,返回一个基于位置的法线 - x, z = position - - # 简单的波浪法线计算(示例) - wave_height_x = 0.1 * math.sin(x * 0.1 + time_value) - wave_height_z = 0.1 * math.sin(z * 0.1 + time_value) - - # 计算法线 - normal = np.array([-wave_height_x, 1.0, -wave_height_z]) - normal = normal / np.linalg.norm(normal) - - return tuple(normal) - - except Exception as e: - print(f"✗ 波浪法线计算失败: {e}") - return (0.0, 1.0, 0.0) - - def generate_water_foam(self, wave_height: float, velocity: float) -> float: - """ - 生成泡沫效果 - - Args: - wave_height: 波浪高度 - velocity: 水流速度 - - Returns: - 泡沫强度 (0.0-1.0) - """ - try: - # 获取泡沫配置 - foam_threshold = self.foam_config['foam_threshold'] - foam_smoothness = self.foam_config['foam_smoothness'] - - # 基于波浪高度和速度计算泡沫 - foam_factor = max(0.0, wave_height - foam_threshold) / (1.0 - foam_threshold) - velocity_factor = min(1.0, velocity / 5.0) - - # 合成泡沫强度 - foam_intensity = foam_factor * (1.0 - velocity_factor * 0.5) - - # 应用平滑处理 - foam_intensity = pow(foam_intensity, foam_smoothness) - - return max(0.0, min(1.0, foam_intensity)) - - except Exception as e: - print(f"✗ 泡沫效果计算失败: {e}") - return 0.0 - - def apply_post_processing(self, render_target: Any) -> Any: - """ - 应用后处理效果 - - Args: - render_target: 渲染目标 - - Returns: - 处理后的渲染目标 - """ - try: - # 在实际实现中,这里会应用各种后处理效果 - # 如抗锯齿、泛光、景深、运动模糊等 - - if self.effect_config['anti_aliasing'] != 'none': - render_target = self._apply_anti_aliasing(render_target) - - if self.effect_config['bloom_enabled']: - render_target = self._apply_bloom(render_target) - - if self.effect_config['motion_blur']: - render_target = self._apply_motion_blur(render_target) - - if self.effect_config['depth_of_field']: - render_target = self._apply_depth_of_field(render_target) - - return render_target - - except Exception as e: - print(f"✗ 后处理应用失败: {e}") - return render_target - - def _apply_anti_aliasing(self, render_target: Any) -> Any: - """ - 应用抗锯齿 - - Args: - render_target: 渲染目标 - - Returns: - 处理后的渲染目标 - """ - try: - # 在实际实现中,这里会应用FXAA、SSAA或MSAA等抗锯齿技术 - aa_type = self.effect_config['anti_aliasing'] - print(f"✓ 应用抗锯齿: {aa_type}") - return render_target - - except Exception as e: - print(f"✗ 抗锯齿应用失败: {e}") - return render_target - - def _apply_bloom(self, render_target: Any) -> Any: - """ - 应用泛光效果 - - Args: - render_target: 渲染目标 - - Returns: - 处理后的渲染目标 - """ - try: - # 在实际实现中,这里会应用泛光效果 - intensity = self.effect_config['bloom_intensity'] - print(f"✓ 应用泛光效果 (强度: {intensity})") - return render_target - - except Exception as e: - print(f"✗ 泛光效果应用失败: {e}") - return render_target - - def _apply_motion_blur(self, render_target: Any) -> Any: - """ - 应用运动模糊 - - Args: - render_target: 渲染目标 - - Returns: - 处理后的渲染目标 - """ - try: - # 在实际实现中,这里会应用运动模糊效果 - samples = 8 if self.effect_config['motion_blur'] else 0 - print(f"✓ 应用运动模糊 ({samples} 采样)") - return render_target - - except Exception as e: - print(f"✗ 运动模糊应用失败: {e}") - return render_target - - def _apply_depth_of_field(self, render_target: Any) -> Any: - """ - 应用景深效果 - - Args: - render_target: 渲染目标 - - Returns: - 处理后的渲染目标 - """ - try: - # 在实际实现中,这里会应用景深效果 - focus_distance = self.effect_config['dof_focus_distance'] - print(f"✓ 应用景深效果 (焦点距离: {focus_distance})") - return render_target - - except Exception as e: - print(f"✗ 景深效果应用失败: {e}") +""" +水体渲染器模块 +负责水体的高质量渲染和视觉效果 +""" + +import time +from typing import Dict, Any, List, Optional, Tuple +import math +import numpy as np + +class WaterRenderer: + """ + 水体渲染器 + 负责水体的高质量渲染和视觉效果,包括反射、折射、泡沫等 + """ + + def __init__(self, plugin): + """ + 初始化水体渲染器 + + Args: + plugin: 水体和流体模拟插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 渲染配置 + self.render_config = { + 'quality_level': 'medium', # 'low', 'medium', 'high', 'ultra' + 'tessellation_factor': 4.0, + 'lod_enabled': True, + 'lod_distance': 100.0, + 'max_patch_size': 50.0, + 'render_distance': 500.0 + } + + # 着色器配置 + self.shader_config = { + 'water_shader': 'pbr', # 'standard', 'pbr', 'advanced' + 'lighting_model': 'cook_torrance', # 'blinn_phong', 'cook_torrance', 'ward' + 'normal_mapping': True, + 'detail_mapping': True, + 'reflection_enabled': True, + 'refraction_enabled': True, + 'caustics_enabled': True, + 'foam_enabled': True, + 'underwater_effects': True + } + + # 材质属性 + self.material_properties = { + 'base_color': (0.0, 0.2, 0.5, 0.9), # RGBA + 'specular_color': (0.8, 0.8, 0.9), + 'roughness': 0.05, + 'metallic': 0.0, + 'reflectance': 0.95, + 'refractive_index': 1.333, + 'absorption_color': (0.0, 0.05, 0.15), + 'absorption_coefficient': 0.1 + } + + # 光照配置 + self.lighting_config = { + 'ambient_light': (0.1, 0.1, 0.15), + 'diffuse_light': (0.9, 0.9, 0.95), + 'specular_light': (1.0, 1.0, 1.0), + 'light_direction': (0.5, 1.0, 0.3), + 'shadow_enabled': True, + 'shadow_quality': 'medium' + } + + # 波浪渲染配置 + self.wave_render_config = { + 'normal_strength': 1.0, + 'fresnel_power': 5.0, + 'fresnel_bias': 0.2, + 'wave_scale': 1.0, + 'wave_speed': 1.0, + 'wave_detail': 0.5 + } + + # 反射配置 + self.reflection_config = { + 'reflection_quality': 'medium', # 'low', 'medium', 'high', 'screen' + 'planar_reflections': True, + 'sky_reflections': True, + 'reflection_distance': 200.0, + 'update_frequency': 30.0 # Hz + } + + # 折射配置 + self.refraction_config = { + 'refraction_quality': 'medium', # 'low', 'medium', 'high' + 'chromatic_aberration': True, + 'chromatic_intensity': 0.02 + } + + # 泡沫配置 + self.foam_config = { + 'foam_enabled': True, + 'foam_threshold': 0.7, + 'foam_smoothness': 0.3, + 'foam_speed': 0.5, + 'foam_color': (0.95, 0.95, 0.95, 0.8) + } + + # 焦散配置 + self.caustics_config = { + 'caustics_enabled': True, + 'caustics_intensity': 1.0, + 'caustics_scale': 5.0, + 'caustics_speed': 0.2, + 'caustics_color': (0.9, 0.8, 0.6) + } + + # 水下效果配置 + self.underwater_config = { + 'underwater_fog': True, + 'fog_density': 0.05, + 'fog_color': (0.0, 0.1, 0.2), + 'god_rays_enabled': True, + 'god_rays_intensity': 0.5 + } + + # 渲染状态 + self.render_state = { + 'last_render_time': 0.0, + 'frames_rendered': 0, + 'patches_rendered': 0, + 'triangles_rendered': 0 + } + + # 性能统计 + self.performance_stats = { + 'render_calls': 0, + 'render_time_total': 0.0, + 'average_render_time': 0.0, + 'patches_drawn': 0, + 'vertices_processed': 0 + } + + # 特效配置 + self.effect_config = { + 'motion_blur': False, + 'anti_aliasing': 'fxaa', # 'none', 'fxaa', 'ssaa', 'msaa' + 'bloom_enabled': True, + 'bloom_intensity': 0.3, + 'depth_of_field': False, + 'dof_focus_distance': 50.0 + } + + # 相机和视图 + self.camera_state = { + 'position': (0.0, 0.0, 0.0), + 'view_matrix': None, + 'projection_matrix': None, + 'fov': 60.0 + } + + # 环境映射 + self.environment_mapping = { + 'skybox_enabled': True, + 'skybox_texture': None, + 'irradiance_map': None, + 'prefiltered_map': None + } + + print("✓ 水体渲染器已创建") + + def initialize(self) -> bool: + """ + 初始化水体渲染器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化渲染资源 + self._initialize_render_resources() + + self.initialized = True + print("✓ 水体渲染器初始化完成") + return True + + except Exception as e: + print(f"✗ 水体渲染器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用水体渲染器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 水体渲染器未初始化") + return False + + self.enabled = True + print("✓ 水体渲染器已启用") + return True + + except Exception as e: + print(f"✗ 水体渲染器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用水体渲染器""" + try: + self.enabled = False + print("✓ 水体渲染器已禁用") + + except Exception as e: + print(f"✗ 水体渲染器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理水体渲染器资源""" + try: + self.disable() + self._cleanup_render_resources() + self.initialized = False + print("✓ 水体渲染器资源已清理") + + except Exception as e: + print(f"✗ 水体渲染器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新水体渲染器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新渲染状态 + self.render_state['last_render_time'] += dt + self.render_state['frames_rendered'] += 1 + + # 更新波浪动画 + self._update_wave_animation(dt) + + except Exception as e: + print(f"✗ 水体渲染器更新失败: {e}") + import traceback + traceback.print_exc() + + def _initialize_render_resources(self): + """初始化渲染资源""" + try: + # 在实际实现中,这里会初始化着色器、纹理、缓冲区等 + print("✓ 水体渲染资源初始化完成") + except Exception as e: + print(f"✗ 水体渲染资源初始化失败: {e}") + raise + + def _cleanup_render_resources(self): + """清理渲染资源""" + try: + # 在实际实现中,这里会释放着色器、纹理、缓冲区等资源 + print("✓ 水体渲染资源清理完成") + except Exception as e: + print(f"✗ 水体渲染资源清理失败: {e}") + + def _update_wave_animation(self, dt: float): + """ + 更新波浪动画 + + Args: + dt: 时间增量 + """ + try: + # 在实际实现中,这里会更新波浪动画参数 + # 用于纹理坐标滚动、法线贴图动画等 + pass + except Exception as e: + print(f"✗ 波浪动画更新失败: {e}") + + def render_water(self, water_data: Dict[str, Any], camera_position: tuple): + """ + 渲染水体 + + Args: + water_data: 水体数据字典 + camera_position: 相机位置 + """ + try: + start_time = time.time() + + # 更新相机状态 + self.camera_state['position'] = camera_position + + # 获取水体网格数据 + vertices = water_data.get('vertices', []) + normals = water_data.get('normals', []) + uvs = water_data.get('uvs', []) + indices = water_data.get('indices', []) + + if not vertices or not indices: + return + + # 应用LOD(如果启用) + if self.render_config['lod_enabled']: + vertices, normals, uvs, indices = self._apply_lod( + vertices, normals, uvs, indices, camera_position + ) + + # 在实际实现中,这里会: + # 1. 设置水体着色器参数 + # 2. 上传网格数据到GPU + # 3. 应用材质和光照 + # 4. 处理反射/折射效果 + # 5. 执行网格渲染 + + # 更新统计信息 + triangle_count = len(indices) // 3 + self.performance_stats['render_calls'] += 1 + self.performance_stats['patches_drawn'] += 1 + self.performance_stats['vertices_processed'] += len(vertices) + self.render_state['patches_rendered'] += 1 + self.render_state['triangles_rendered'] += triangle_count + + render_time = time.time() - start_time + self.performance_stats['render_time_total'] += render_time + + if self.performance_stats['render_calls'] > 0: + self.performance_stats['average_render_time'] = ( + self.performance_stats['render_time_total'] / + self.performance_stats['render_calls'] + ) + + print(f"✓ 水体渲染完成 ({triangle_count} 三角形)") + + except Exception as e: + print(f"✗ 水体渲染失败: {e}") + import traceback + traceback.print_exc() + + def _apply_lod(self, vertices: List[Tuple[float, float, float]], + normals: List[Tuple[float, float, float]], + uvs: List[Tuple[float, float]], + indices: List[int], + camera_position: Tuple[float, float, float]) -> Tuple[List, List, List, List]: + """ + 应用LOD(细节层次) + + Args: + vertices: 顶点列表 + normals: 法线列表 + uvs: UV坐标列表 + indices: 索引列表 + camera_position: 相机位置 + + Returns: + 简化后的网格数据 + """ + try: + # 计算到相机的距离 + # 在实际实现中,这里会根据距离简化网格 + lod_distance = self.render_config['lod_distance'] + + # 简化处理(示例) + return vertices, normals, uvs, indices + + except Exception as e: + print(f"✗ LOD应用失败: {e}") + return vertices, normals, uvs, indices + + def set_render_config(self, config: Dict[str, Any]): + """ + 设置渲染配置 + + Args: + config: 渲染配置字典 + """ + try: + self.render_config.update(config) + print(f"✓ 渲染配置已更新: {self.render_config}") + except Exception as e: + print(f"✗ 渲染配置设置失败: {e}") + + def get_render_config(self) -> Dict[str, Any]: + """ + 获取渲染配置 + + Returns: + 渲染配置字典 + """ + return self.render_config.copy() + + def set_shader_config(self, config: Dict[str, Any]): + """ + 设置着色器配置 + + Args: + config: 着色器配置字典 + """ + try: + self.shader_config.update(config) + print(f"✓ 着色器配置已更新: {self.shader_config}") + except Exception as e: + print(f"✗ 着色器配置设置失败: {e}") + + def get_shader_config(self) -> Dict[str, Any]: + """ + 获取着色器配置 + + Returns: + 着色器配置字典 + """ + return self.shader_config.copy() + + def set_material_properties(self, properties: Dict[str, Any]): + """ + 设置材质属性 + + Args: + properties: 材质属性字典 + """ + try: + self.material_properties.update(properties) + print(f"✓ 材质属性已更新: {self.material_properties}") + except Exception as e: + print(f"✗ 材质属性设置失败: {e}") + + def get_material_properties(self) -> Dict[str, Any]: + """ + 获取材质属性 + + Returns: + 材质属性字典 + """ + return self.material_properties.copy() + + def set_lighting_config(self, config: Dict[str, Any]): + """ + 设置光照配置 + + Args: + config: 光照配置字典 + """ + try: + self.lighting_config.update(config) + print(f"✓ 光照配置已更新: {self.lighting_config}") + except Exception as e: + print(f"✗ 光照配置设置失败: {e}") + + def get_lighting_config(self) -> Dict[str, Any]: + """ + 获取光照配置 + + Returns: + 光照配置字典 + """ + return self.lighting_config.copy() + + def set_wave_render_config(self, config: Dict[str, Any]): + """ + 设置波浪渲染配置 + + Args: + config: 波浪渲染配置字典 + """ + try: + self.wave_render_config.update(config) + print(f"✓ 波浪渲染配置已更新: {self.wave_render_config}") + except Exception as e: + print(f"✗ 波浪渲染配置设置失败: {e}") + + def get_wave_render_config(self) -> Dict[str, Any]: + """ + 获取波浪渲染配置 + + Returns: + 波浪渲染配置字典 + """ + return self.wave_render_config.copy() + + def set_reflection_config(self, config: Dict[str, Any]): + """ + 设置反射配置 + + Args: + config: 反射配置字典 + """ + try: + self.reflection_config.update(config) + print(f"✓ 反射配置已更新: {self.reflection_config}") + except Exception as e: + print(f"✗ 反射配置设置失败: {e}") + + def get_reflection_config(self) -> Dict[str, Any]: + """ + 获取反射配置 + + Returns: + 反射配置字典 + """ + return self.reflection_config.copy() + + def set_refraction_config(self, config: Dict[str, Any]): + """ + 设置折射配置 + + Args: + config: 折射配置字典 + """ + try: + self.refraction_config.update(config) + print(f"✓ 折射配置已更新: {self.refraction_config}") + except Exception as e: + print(f"✗ 折射配置设置失败: {e}") + + def get_refraction_config(self) -> Dict[str, Any]: + """ + 获取折射配置 + + Returns: + 折射配置字典 + """ + return self.refraction_config.copy() + + def set_foam_config(self, config: Dict[str, Any]): + """ + 设置泡沫配置 + + Args: + config: 泡沫配置字典 + """ + try: + self.foam_config.update(config) + print(f"✓ 泡沫配置已更新: {self.foam_config}") + except Exception as e: + print(f"✗ 泡沫配置设置失败: {e}") + + def get_foam_config(self) -> Dict[str, Any]: + """ + 获取泡沫配置 + + Returns: + 泡沫配置字典 + """ + return self.foam_config.copy() + + def set_caustics_config(self, config: Dict[str, Any]): + """ + 设置焦散配置 + + Args: + config: 焦散配置字典 + """ + try: + self.caustics_config.update(config) + print(f"✓ 焦散配置已更新: {self.caustics_config}") + except Exception as e: + print(f"✗ 焦散配置设置失败: {e}") + + def get_caustics_config(self) -> Dict[str, Any]: + """ + 获取焦散配置 + + Returns: + 焦散配置字典 + """ + return self.caustics_config.copy() + + def set_underwater_config(self, config: Dict[str, Any]): + """ + 设置水下效果配置 + + Args: + config: 水下效果配置字典 + """ + try: + self.underwater_config.update(config) + print(f"✓ 水下效果配置已更新: {self.underwater_config}") + except Exception as e: + print(f"✗ 水下效果配置设置失败: {e}") + + def get_underwater_config(self) -> Dict[str, Any]: + """ + 获取水下效果配置 + + Returns: + 水下效果配置字典 + """ + return self.underwater_config.copy() + + def set_effect_config(self, config: Dict[str, Any]): + """ + 设置特效配置 + + Args: + config: 特效配置字典 + """ + try: + self.effect_config.update(config) + print(f"✓ 特效配置已更新: {self.effect_config}") + except Exception as e: + print(f"✗ 特效配置设置失败: {e}") + + def get_effect_config(self) -> Dict[str, Any]: + """ + 获取特效配置 + + Returns: + 特效配置字典 + """ + return self.effect_config.copy() + + def get_render_state(self) -> Dict[str, Any]: + """ + 获取渲染状态 + + Returns: + 渲染状态字典 + """ + return self.render_state.copy() + + def get_performance_stats(self) -> Dict[str, Any]: + """ + 获取性能统计信息 + + Returns: + 性能统计字典 + """ + return self.performance_stats.copy() + + def reset_performance_stats(self): + """重置性能统计信息""" + try: + self.performance_stats = { + 'render_calls': 0, + 'render_time_total': 0.0, + 'average_render_time': 0.0, + 'patches_drawn': 0, + 'vertices_processed': 0 + } + print("✓ 性能统计信息已重置") + except Exception as e: + print(f"✗ 性能统计信息重置失败: {e}") + + def update_camera_state(self, position: tuple, view_matrix: Any = None, + projection_matrix: Any = None, fov: float = 60.0): + """ + 更新相机状态 + + Args: + position: 相机位置 + view_matrix: 视图矩阵 + projection_matrix: 投影矩阵 + fov: 视野角度 + """ + try: + self.camera_state['position'] = position + self.camera_state['view_matrix'] = view_matrix + self.camera_state['projection_matrix'] = projection_matrix + self.camera_state['fov'] = fov + + except Exception as e: + print(f"✗ 相机状态更新失败: {e}") + + def calculate_water_lighting(self, position: tuple, normal: tuple, + view_direction: tuple) -> tuple: + """ + 计算水体光照(基于物理的渲染) + + Args: + position: 位置 + normal: 波浪法线 + view_direction: 视线方向 + + Returns: + 光照颜色 (r, g, b) + """ + try: + # 获取光照配置 + ambient = self.lighting_config['ambient_light'] + diffuse = self.lighting_config['diffuse_light'] + light_dir = self.lighting_config['light_direction'] + + # 获取材质属性 + base_color = self.material_properties['base_color'][:3] + specular_color = self.material_properties['specular_color'] + roughness = self.material_properties['roughness'] + metallic = self.material_properties['metallic'] + + # 归一化向量 + norm_normal = np.array(normal) / np.linalg.norm(normal) + norm_light_dir = np.array(light_dir) / np.linalg.norm(light_dir) + norm_view_dir = np.array(view_direction) / np.linalg.norm(view_direction) + + # 计算半角向量 + half_vector = norm_light_dir + norm_view_dir + norm_half_vector = half_vector / np.linalg.norm(half_vector) + + # 计算Fresnel效应 + fresnel = self._calculate_fresnel(norm_normal, norm_view_dir) + + # 漫反射项 (Lambertian) + ndotl = max(0.0, np.dot(norm_normal, norm_light_dir)) + diffuse_contrib = ( + ndotl * diffuse[0] * base_color[0], + ndotl * diffuse[1] * base_color[1], + ndotl * diffuse[2] * base_color[2] + ) + + # 镜面反射项 (Cook-Torrance) + specular_contrib = (0.0, 0.0, 0.0) + if ndotl > 0: + # 法线分布函数 (Trowbridge-Reitz GGX) + ndoth = max(0.0, np.dot(norm_normal, norm_half_vector)) + alpha = roughness * roughness + alpha2 = alpha * alpha + denom = ndoth * ndoth * (alpha2 - 1.0) + 1.0 + d = alpha2 / (math.pi * denom * denom) + + # 几何函数 + ndotv = max(0.0, np.dot(norm_normal, norm_view_dir)) + k = (roughness + 1.0) * (roughness + 1.0) / 8.0 + g1l = ndotl / (ndotl * (1.0 - k) + k) + g1v = ndotv / (ndotv * (1.0 - k) + k) + g = g1l * g1v + + # Fresnel项 (Schlick近似) + f0 = np.array(specular_color) if metallic > 0.5 else np.array([0.04, 0.04, 0.04]) + f = f0 + (1.0 - f0) * pow(1.0 - np.dot(norm_half_vector, norm_view_dir), 5.0) + + # 完整的镜面反射贡献 + if (ndotl * ndotv) > 0: + specular = (d * g * f) / (4.0 * ndotl * ndotv) + specular_contrib = ( + specular[0] * diffuse[0], + specular[1] * diffuse[1], + specular[2] * diffuse[2] + ) + + # 环境光项 + ambient_contrib = ( + ambient[0] * base_color[0], + ambient[1] * base_color[1], + ambient[2] * base_color[2] + ) + + # 合成最终颜色 + final_color = ( + ambient_contrib[0] + diffuse_contrib[0] + specular_contrib[0], + ambient_contrib[1] + diffuse_contrib[1] + specular_contrib[1], + ambient_contrib[2] + diffuse_contrib[2] + specular_contrib[2] + ) + + # 应用Fresnel效应 + final_color = ( + final_color[0] * (1.0 - fresnel) + fresnel, + final_color[1] * (1.0 - fresnel) + fresnel, + final_color[2] * (1.0 - fresnel) + fresnel + ) + + # 确保颜色值在有效范围内 + final_color = ( + max(0.0, min(1.0, final_color[0])), + max(0.0, min(1.0, final_color[1])), + max(0.0, min(1.0, final_color[2])) + ) + + return final_color + + except Exception as e: + print(f"✗ 水体光照计算失败: {e}") + return (0.1, 0.2, 0.5) + + def _calculate_fresnel(self, normal: np.ndarray, view_dir: np.ndarray) -> float: + """ + 计算Fresnel反射系数 + + Args: + normal: 表面法线 + view_dir: 视线方向 + + Returns: + Fresnel系数 + """ + try: + # 简化的Fresnel计算 + ndotv = max(0.0, np.dot(normal, view_dir)) + fresnel = pow(1.0 - ndotv, self.wave_render_config['fresnel_power']) + fresnel = max(0.0, min(1.0, fresnel + self.wave_render_config['fresnel_bias'])) + return fresnel + + except Exception as e: + return 0.5 + + def calculate_wave_normal(self, position: tuple, time_value: float) -> tuple: + """ + 计算波浪法线 + + Args: + position: 位置 (x, z) + time_value: 时间值 + + Returns: + 波浪法线 (nx, ny, nz) + """ + try: + # 在实际实现中,这里会根据波浪系统计算精确的法线 + # 简化处理,返回一个基于位置的法线 + x, z = position + + # 简单的波浪法线计算(示例) + wave_height_x = 0.1 * math.sin(x * 0.1 + time_value) + wave_height_z = 0.1 * math.sin(z * 0.1 + time_value) + + # 计算法线 + normal = np.array([-wave_height_x, 1.0, -wave_height_z]) + normal = normal / np.linalg.norm(normal) + + return tuple(normal) + + except Exception as e: + print(f"✗ 波浪法线计算失败: {e}") + return (0.0, 1.0, 0.0) + + def generate_water_foam(self, wave_height: float, velocity: float) -> float: + """ + 生成泡沫效果 + + Args: + wave_height: 波浪高度 + velocity: 水流速度 + + Returns: + 泡沫强度 (0.0-1.0) + """ + try: + # 获取泡沫配置 + foam_threshold = self.foam_config['foam_threshold'] + foam_smoothness = self.foam_config['foam_smoothness'] + + # 基于波浪高度和速度计算泡沫 + foam_factor = max(0.0, wave_height - foam_threshold) / (1.0 - foam_threshold) + velocity_factor = min(1.0, velocity / 5.0) + + # 合成泡沫强度 + foam_intensity = foam_factor * (1.0 - velocity_factor * 0.5) + + # 应用平滑处理 + foam_intensity = pow(foam_intensity, foam_smoothness) + + return max(0.0, min(1.0, foam_intensity)) + + except Exception as e: + print(f"✗ 泡沫效果计算失败: {e}") + return 0.0 + + def apply_post_processing(self, render_target: Any) -> Any: + """ + 应用后处理效果 + + Args: + render_target: 渲染目标 + + Returns: + 处理后的渲染目标 + """ + try: + # 在实际实现中,这里会应用各种后处理效果 + # 如抗锯齿、泛光、景深、运动模糊等 + + if self.effect_config['anti_aliasing'] != 'none': + render_target = self._apply_anti_aliasing(render_target) + + if self.effect_config['bloom_enabled']: + render_target = self._apply_bloom(render_target) + + if self.effect_config['motion_blur']: + render_target = self._apply_motion_blur(render_target) + + if self.effect_config['depth_of_field']: + render_target = self._apply_depth_of_field(render_target) + + return render_target + + except Exception as e: + print(f"✗ 后处理应用失败: {e}") + return render_target + + def _apply_anti_aliasing(self, render_target: Any) -> Any: + """ + 应用抗锯齿 + + Args: + render_target: 渲染目标 + + Returns: + 处理后的渲染目标 + """ + try: + # 在实际实现中,这里会应用FXAA、SSAA或MSAA等抗锯齿技术 + aa_type = self.effect_config['anti_aliasing'] + print(f"✓ 应用抗锯齿: {aa_type}") + return render_target + + except Exception as e: + print(f"✗ 抗锯齿应用失败: {e}") + return render_target + + def _apply_bloom(self, render_target: Any) -> Any: + """ + 应用泛光效果 + + Args: + render_target: 渲染目标 + + Returns: + 处理后的渲染目标 + """ + try: + # 在实际实现中,这里会应用泛光效果 + intensity = self.effect_config['bloom_intensity'] + print(f"✓ 应用泛光效果 (强度: {intensity})") + return render_target + + except Exception as e: + print(f"✗ 泛光效果应用失败: {e}") + return render_target + + def _apply_motion_blur(self, render_target: Any) -> Any: + """ + 应用运动模糊 + + Args: + render_target: 渲染目标 + + Returns: + 处理后的渲染目标 + """ + try: + # 在实际实现中,这里会应用运动模糊效果 + samples = 8 if self.effect_config['motion_blur'] else 0 + print(f"✓ 应用运动模糊 ({samples} 采样)") + return render_target + + except Exception as e: + print(f"✗ 运动模糊应用失败: {e}") + return render_target + + def _apply_depth_of_field(self, render_target: Any) -> Any: + """ + 应用景深效果 + + Args: + render_target: 渲染目标 + + Returns: + 处理后的渲染目标 + """ + try: + # 在实际实现中,这里会应用景深效果 + focus_distance = self.effect_config['dof_focus_distance'] + print(f"✓ 应用景深效果 (焦点距离: {focus_distance})") + return render_target + + except Exception as e: + print(f"✗ 景深效果应用失败: {e}") return render_target \ No newline at end of file diff --git a/plugins/user/fluid_simulation/water/water_surface.py b/plugins/user/fluid_simulation/water/water_surface.py index 1dcb43ea..c9967aa3 100644 --- a/plugins/user/fluid_simulation/water/water_surface.py +++ b/plugins/user/fluid_simulation/water/water_surface.py @@ -1,676 +1,676 @@ -""" -水体表面模块 -负责水体表面的几何生成和动态更新 -""" - -import time -from typing import Dict, Any, List, Optional, Tuple -import math -import numpy as np - -class WaterSurface: - """ - 水体表面管理器 - 负责水体表面的几何生成、动态更新和渲染准备 - """ - - def __init__(self, plugin): - """ - 初始化水体表面管理器 - - Args: - plugin: 水体和流体模拟插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 网格配置 - self.mesh_config = { - 'resolution_x': 128, - 'resolution_z': 128, - 'tessellation_level': 1, - 'patch_size': 10.0, - 'lod_enabled': True, - 'lod_distance': 50.0 - } - - # 表面顶点数据 - self.vertices = [] - self.normals = [] - self.uvs = [] - self.indices = [] - - # 表面高度场 - self.height_field = None - self.normal_field = None - - # 动态更新配置 - self.update_config = { - 'update_frequency': 60.0, # Hz - 'last_update_time': 0.0, - 'needs_update': True - } - - # 纹理坐标配置 - self.uv_config = { - 'scale_u': 1.0, - 'scale_v': 1.0, - 'scroll_u': 0.0, - 'scroll_v': 0.0, - 'scroll_speed_u': 0.1, - 'scroll_speed_v': 0.05 - } - - # 几何统计 - self.geometry_stats = { - 'vertex_count': 0, - 'triangle_count': 0, - 'patch_count': 0, - 'updates_performed': 0 - } - - # 性能优化 - self.optimization_settings = { - 'frustum_culling': True, - 'occlusion_culling': False, - 'dynamic_lod': True, - 'vertex_cache': True - } - - print("✓ 水体表面管理器已创建") - - def initialize(self) -> bool: - """ - 初始化水体表面管理器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化高度场 - self._initialize_height_field() - - # 生成初始网格 - self._generate_initial_mesh() - - self.initialized = True - print("✓ 水体表面管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 水体表面管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用水体表面管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 水体表面管理器未初始化") - return False - - self.enabled = True - print("✓ 水体表面管理器已启用") - return True - - except Exception as e: - print(f"✗ 水体表面管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用水体表面管理器""" - try: - self.enabled = False - print("✓ 水体表面管理器已禁用") - - except Exception as e: - print(f"✗ 水体表面管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理水体表面管理器资源""" - try: - self.disable() - self._clear_mesh_data() - self.initialized = False - print("✓ 水体表面管理器资源已清理") - - except Exception as e: - print(f"✗ 水体表面管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新水体表面状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新时间 - self.update_config['last_update_time'] += dt - - # 检查是否需要更新 - update_interval = 1.0 / self.update_config['update_frequency'] - if self.update_config['last_update_time'] >= update_interval: - # 更新表面几何 - self._update_surface_geometry(dt) - - # 更新纹理坐标 - self._update_texture_coordinates(dt) - - # 重置更新计时器 - self.update_config['last_update_time'] = 0.0 - self.update_config['needs_update'] = False - - # 更新统计信息 - self.geometry_stats['updates_performed'] += 1 - - except Exception as e: - print(f"✗ 水体表面管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _initialize_height_field(self): - """初始化高度场""" - try: - res_x = self.mesh_config['resolution_x'] - res_z = self.mesh_config['resolution_z'] - - # 初始化高度场 - self.height_field = np.zeros((res_x, res_z)) - - # 初始化法线场 - self.normal_field = np.zeros((res_x, res_z, 3)) - - print("✓ 高度场初始化完成") - - except Exception as e: - print(f"✗ 高度场初始化失败: {e}") - raise - - def _generate_initial_mesh(self): - """生成初始网格""" - try: - self._clear_mesh_data() - - res_x = self.mesh_config['resolution_x'] - res_z = self.mesh_config['resolution_z'] - patch_size = self.mesh_config['patch_size'] - - # 生成顶点 - for i in range(res_x): - for j in range(res_z): - x = (i - res_x / 2) * patch_size / res_x - z = (j - res_z / 2) * patch_size / res_z - y = self.height_field[i, j] if self.height_field is not None else 0.0 - - self.vertices.append((x, y, z)) - - # 生成UV坐标 - u = i / (res_x - 1) * self.uv_config['scale_u'] - v = j / (res_z - 1) * self.uv_config['scale_v'] - self.uvs.append((u, v)) - - # 生成索引(三角形列表) - for i in range(res_x - 1): - for j in range(res_z - 1): - # 第一个三角形 - self.indices.append(i * res_z + j) - self.indices.append((i + 1) * res_z + j) - self.indices.append(i * res_z + (j + 1)) - - # 第二个三角形 - self.indices.append((i + 1) * res_z + j) - self.indices.append((i + 1) * res_z + (j + 1)) - self.indices.append(i * res_z + (j + 1)) - - # 计算法线 - self._calculate_normals() - - # 更新统计信息 - self.geometry_stats['vertex_count'] = len(self.vertices) - self.geometry_stats['triangle_count'] = len(self.indices) // 3 - self.geometry_stats['patch_count'] = (res_x - 1) * (res_z - 1) - - print("✓ 初始网格生成完成") - - except Exception as e: - print(f"✗ 初始网格生成失败: {e}") - raise - - def _clear_mesh_data(self): - """清理网格数据""" - try: - self.vertices.clear() - self.normals.clear() - self.uvs.clear() - self.indices.clear() - - if self.height_field is not None: - self.height_field.fill(0) - - if self.normal_field is not None: - self.normal_field.fill(0) - - except Exception as e: - print(f"✗ 网格数据清理失败: {e}") - - def _update_surface_geometry(self, dt: float): - """ - 更新表面几何 - - Args: - dt: 时间增量 - """ - try: - if not self.vertices or self.height_field is None: - return - - res_x = self.mesh_config['resolution_x'] - res_z = self.mesh_config['resolution_z'] - - # 更新顶点高度 - for i in range(res_x): - for j in range(res_z): - vertex_index = i * res_z + j - if vertex_index < len(self.vertices): - x, y, z = self.vertices[vertex_index] - # 从高度场获取新高度 - new_height = self.height_field[i, j] - self.vertices[vertex_index] = (x, new_height, z) - - # 重新计算法线 - self._calculate_normals() - - except Exception as e: - print(f"✗ 表面几何更新失败: {e}") - - def _calculate_normals(self): - """计算顶点法线""" - try: - if not self.vertices or not self.indices: - return - - # 清除旧法线 - self.normals.clear() - - # 初始化法线数组 - vertex_normals = [np.array([0.0, 0.0, 0.0]) for _ in self.vertices] - - # 计算每个三角形的法线并累加到顶点 - for i in range(0, len(self.indices), 3): - i0 = self.indices[i] - i1 = self.indices[i + 1] - i2 = self.indices[i + 2] - - # 获取三角形顶点 - v0 = np.array(self.vertices[i0]) - v1 = np.array(self.vertices[i1]) - v2 = np.array(self.vertices[i2]) - - # 计算边向量 - edge1 = v1 - v0 - edge2 = v2 - v0 - - # 计算法线(叉积) - normal = np.cross(edge1, edge2) - - # 归一化 - normal_length = np.linalg.norm(normal) - if normal_length > 0: - normal = normal / normal_length - - # 累加到顶点法线 - vertex_normals[i0] += normal - vertex_normals[i1] += normal - vertex_normals[i2] += normal - - # 归一化顶点法线并存储 - for normal in vertex_normals: - normal_length = np.linalg.norm(normal) - if normal_length > 0: - normal = normal / normal_length - self.normals.append(tuple(normal)) - - except Exception as e: - print(f"✗ 法线计算失败: {e}") - - def _update_texture_coordinates(self, dt: float): - """ - 更新纹理坐标 - - Args: - dt: 时间增量 - """ - try: - # 更新滚动偏移 - self.uv_config['scroll_u'] += self.uv_config['scroll_speed_u'] * dt - self.uv_config['scroll_v'] += self.uv_config['scroll_speed_v'] * dt - - # 保持在[0,1)范围内 - self.uv_config['scroll_u'] %= 1.0 - self.uv_config['scroll_v'] %= 1.0 - - # 更新UV坐标 - if self.uvs: - res_x = self.mesh_config['resolution_x'] - res_z = self.mesh_config['resolution_z'] - - for i in range(res_x): - for j in range(res_z): - vertex_index = i * res_z + j - if vertex_index < len(self.uvs): - u = (i / (res_x - 1) + self.uv_config['scroll_u']) * self.uv_config['scale_u'] - v = (j / (res_z - 1) + self.uv_config['scroll_v']) * self.uv_config['scale_v'] - self.uvs[vertex_index] = (u % 1.0, v % 1.0) - - except Exception as e: - print(f"✗ 纹理坐标更新失败: {e}") - - def update_height_field(self, new_height_field: np.ndarray): - """ - 更新高度场 - - Args: - new_height_field: 新的高度场数据 - """ - try: - if self.height_field is None: - self._initialize_height_field() - - # 复制新高度场数据 - if new_height_field.shape == self.height_field.shape: - self.height_field[:, :] = new_height_field[:, :] - else: - # 如果尺寸不匹配,进行插值 - from scipy.interpolate import interp2d - - old_x = np.linspace(0, 1, self.height_field.shape[0]) - old_y = np.linspace(0, 1, self.height_field.shape[1]) - new_x = np.linspace(0, 1, new_height_field.shape[0]) - new_y = np.linspace(0, 1, new_height_field.shape[1]) - - interpolator = interp2d(old_x, old_y, new_height_field, kind='linear') - self.height_field[:, :] = interpolator(old_x, old_y) - - # 标记需要更新 - self.update_config['needs_update'] = True - - except Exception as e: - print(f"✗ 高度场更新失败: {e}") - - def generate_wave_pattern(self, wave_parameters: Dict[str, float], - time_value: float) -> np.ndarray: - """ - 生成波浪模式 - - Args: - wave_parameters: 波浪参数字典 - time_value: 时间值 - - Returns: - 高度场数组 - """ - try: - if self.height_field is None: - self._initialize_height_field() - - res_x, res_z = self.height_field.shape - patch_size = self.mesh_config['patch_size'] - - # 获取波浪参数 - amplitude = wave_parameters.get('amplitude', 1.0) - wavelength = wave_parameters.get('wavelength', 10.0) - speed = wave_parameters.get('speed', 1.0) - direction = wave_parameters.get('direction', 0.0) # 弧度 - - # 计算波向量 - wave_vector_x = 2 * math.pi / wavelength * math.cos(direction) - wave_vector_z = 2 * math.pi / wavelength * math.sin(direction) - - # 生成波浪 - for i in range(res_x): - for j in range(res_z): - # 计算世界坐标 - x = (i - res_x / 2) * patch_size / res_x - z = (j - res_z / 2) * patch_size / res_z - - # 计算相位 - phase = wave_vector_x * x + wave_vector_z * z - speed * time_value - - # 计算波浪高度 - height = amplitude * math.sin(phase) - self.height_field[i, j] = height - - return self.height_field.copy() - - except Exception as e: - print(f"✗ 波浪模式生成失败: {e}") - # 返回零高度场 - return np.zeros((res_x, res_z)) if self.height_field is not None else np.zeros((1, 1)) - - def generate_gerstner_waves(self, waves: List[Dict[str, float]], - time_value: float) -> np.ndarray: - """ - 生成Gerstner波浪(更真实的波浪模型) - - Args: - waves: 波浪参数列表,每个包含'amplitude', 'wavelength', 'speed', 'direction' - time_value: 时间值 - - Returns: - 高度场数组 - """ - try: - if self.height_field is None: - self._initialize_height_field() - - res_x, res_z = self.height_field.shape - patch_size = self.mesh_config['patch_size'] - - # 清零高度场 - self.height_field.fill(0) - - # 为每个波浪累加贡献 - for wave in waves: - amplitude = wave.get('amplitude', 1.0) - wavelength = wave.get('wavelength', 10.0) - speed = wave.get('speed', 1.0) - direction = wave.get('direction', 0.0) - - # 计算波数和频率 - wavenumber = 2 * math.pi / wavelength - frequency = speed * wavenumber - - # 计算波向量 - kx = wavenumber * math.cos(direction) - kz = wavenumber * math.sin(direction) - - # 为每个点计算波浪贡献 - for i in range(res_x): - for j in range(res_z): - # 计算世界坐标 - x = (i - res_x / 2) * patch_size / res_x - z = (j - res_z / 2) * patch_size / res_z - - # 计算相位 - phase = kx * x + kz * z - frequency * time_value - - # Gerstner波浪公式 - height_contribution = amplitude * math.sin(phase) - self.height_field[i, j] += height_contribution - - return self.height_field.copy() - - except Exception as e: - print(f"✗ Gerstner波浪生成失败: {e}") - return np.zeros((res_x, res_z)) if self.height_field is not None else np.zeros((1, 1)) - - def get_mesh_data(self) -> Dict[str, List[Tuple[float, float, float]]]: - """ - 获取网格数据 - - Returns: - 包含顶点、法线、UV坐标和索引的字典 - """ - return { - 'vertices': self.vertices.copy(), - 'normals': self.normals.copy(), - 'uvs': self.uvs.copy(), - 'indices': self.indices.copy() - } - - def get_height_at(self, x: float, z: float) -> float: - """ - 获取指定位置的高度 - - Args: - x, z: 世界坐标 - - Returns: - 高度值 - """ - try: - if self.height_field is None: - return 0.0 - - res_x, res_z = self.height_field.shape - patch_size = self.mesh_config['patch_size'] - - # 转换到网格坐标 - grid_x = (x / patch_size + 0.5) * res_x - grid_z = (z / patch_size + 0.5) * res_z - - # 边界检查 - if grid_x < 0 or grid_x >= res_x or grid_z < 0 or grid_z >= res_z: - return 0.0 - - # 简单的双线性插值 - i = int(grid_x) - j = int(grid_z) - fx = grid_x - i - fz = grid_z - j - - # 边界处理 - i1 = min(i + 1, res_x - 1) - j1 = min(j + 1, res_z - 1) - - # 双线性插值 - h00 = self.height_field[i, j] - h10 = self.height_field[i1, j] - h01 = self.height_field[i, j1] - h11 = self.height_field[i1, j1] - - h0 = h00 * (1 - fx) + h10 * fx - h1 = h01 * (1 - fx) + h11 * fx - - return h0 * (1 - fz) + h1 * fz - - except Exception as e: - print(f"✗ 高度获取失败: {e}") - return 0.0 - - def set_mesh_config(self, config: Dict[str, Any]): - """ - 设置网格配置 - - Args: - config: 网格配置字典 - """ - try: - old_res_x = self.mesh_config['resolution_x'] - old_res_z = self.mesh_config['resolution_z'] - - self.mesh_config.update(config) - - new_res_x = self.mesh_config['resolution_x'] - new_res_z = self.mesh_config['resolution_z'] - - # 如果分辨率改变,重新生成网格 - if old_res_x != new_res_x or old_res_z != new_res_z: - self._generate_initial_mesh() - - print(f"✓ 网格配置已更新: {self.mesh_config}") - except Exception as e: - print(f"✗ 网格配置设置失败: {e}") - - def get_mesh_config(self) -> Dict[str, Any]: - """ - 获取网格配置 - - Returns: - 网格配置字典 - """ - return self.mesh_config.copy() - - def set_uv_config(self, config: Dict[str, float]): - """ - 设置UV配置 - - Args: - config: UV配置字典 - """ - try: - self.uv_config.update(config) - print(f"✓ UV配置已更新: {self.uv_config}") - except Exception as e: - print(f"✗ UV配置设置失败: {e}") - - def get_uv_config(self) -> Dict[str, float]: - """ - 获取UV配置 - - Returns: - UV配置字典 - """ - return self.uv_config.copy() - - def get_geometry_stats(self) -> Dict[str, int]: - """ - 获取几何统计信息 - - Returns: - 几何统计字典 - """ - return self.geometry_stats.copy() - - def reset_geometry_stats(self): - """重置几何统计信息""" - try: - self.geometry_stats = { - 'vertex_count': len(self.vertices), - 'triangle_count': len(self.indices) // 3, - 'patch_count': (self.mesh_config['resolution_x'] - 1) * (self.mesh_config['resolution_z'] - 1), - 'updates_performed': 0 - } - print("✓ 几何统计信息已重置") - except Exception as e: - print(f"✗ 几何统计信息重置失败: {e}") - - def apply_displacement_map(self, displacement_map: np.ndarray): - """ - 应用位移贴图 - - Args: - displacement_map: 位移贴图数据 - """ - try: - self.update_height_field(displacement_map) - print("✓ 位移贴图已应用") - except Exception as e: +""" +水体表面模块 +负责水体表面的几何生成和动态更新 +""" + +import time +from typing import Dict, Any, List, Optional, Tuple +import math +import numpy as np + +class WaterSurface: + """ + 水体表面管理器 + 负责水体表面的几何生成、动态更新和渲染准备 + """ + + def __init__(self, plugin): + """ + 初始化水体表面管理器 + + Args: + plugin: 水体和流体模拟插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 网格配置 + self.mesh_config = { + 'resolution_x': 128, + 'resolution_z': 128, + 'tessellation_level': 1, + 'patch_size': 10.0, + 'lod_enabled': True, + 'lod_distance': 50.0 + } + + # 表面顶点数据 + self.vertices = [] + self.normals = [] + self.uvs = [] + self.indices = [] + + # 表面高度场 + self.height_field = None + self.normal_field = None + + # 动态更新配置 + self.update_config = { + 'update_frequency': 60.0, # Hz + 'last_update_time': 0.0, + 'needs_update': True + } + + # 纹理坐标配置 + self.uv_config = { + 'scale_u': 1.0, + 'scale_v': 1.0, + 'scroll_u': 0.0, + 'scroll_v': 0.0, + 'scroll_speed_u': 0.1, + 'scroll_speed_v': 0.05 + } + + # 几何统计 + self.geometry_stats = { + 'vertex_count': 0, + 'triangle_count': 0, + 'patch_count': 0, + 'updates_performed': 0 + } + + # 性能优化 + self.optimization_settings = { + 'frustum_culling': True, + 'occlusion_culling': False, + 'dynamic_lod': True, + 'vertex_cache': True + } + + print("✓ 水体表面管理器已创建") + + def initialize(self) -> bool: + """ + 初始化水体表面管理器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化高度场 + self._initialize_height_field() + + # 生成初始网格 + self._generate_initial_mesh() + + self.initialized = True + print("✓ 水体表面管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 水体表面管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用水体表面管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 水体表面管理器未初始化") + return False + + self.enabled = True + print("✓ 水体表面管理器已启用") + return True + + except Exception as e: + print(f"✗ 水体表面管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用水体表面管理器""" + try: + self.enabled = False + print("✓ 水体表面管理器已禁用") + + except Exception as e: + print(f"✗ 水体表面管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理水体表面管理器资源""" + try: + self.disable() + self._clear_mesh_data() + self.initialized = False + print("✓ 水体表面管理器资源已清理") + + except Exception as e: + print(f"✗ 水体表面管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新水体表面状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新时间 + self.update_config['last_update_time'] += dt + + # 检查是否需要更新 + update_interval = 1.0 / self.update_config['update_frequency'] + if self.update_config['last_update_time'] >= update_interval: + # 更新表面几何 + self._update_surface_geometry(dt) + + # 更新纹理坐标 + self._update_texture_coordinates(dt) + + # 重置更新计时器 + self.update_config['last_update_time'] = 0.0 + self.update_config['needs_update'] = False + + # 更新统计信息 + self.geometry_stats['updates_performed'] += 1 + + except Exception as e: + print(f"✗ 水体表面管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _initialize_height_field(self): + """初始化高度场""" + try: + res_x = self.mesh_config['resolution_x'] + res_z = self.mesh_config['resolution_z'] + + # 初始化高度场 + self.height_field = np.zeros((res_x, res_z)) + + # 初始化法线场 + self.normal_field = np.zeros((res_x, res_z, 3)) + + print("✓ 高度场初始化完成") + + except Exception as e: + print(f"✗ 高度场初始化失败: {e}") + raise + + def _generate_initial_mesh(self): + """生成初始网格""" + try: + self._clear_mesh_data() + + res_x = self.mesh_config['resolution_x'] + res_z = self.mesh_config['resolution_z'] + patch_size = self.mesh_config['patch_size'] + + # 生成顶点 + for i in range(res_x): + for j in range(res_z): + x = (i - res_x / 2) * patch_size / res_x + z = (j - res_z / 2) * patch_size / res_z + y = self.height_field[i, j] if self.height_field is not None else 0.0 + + self.vertices.append((x, y, z)) + + # 生成UV坐标 + u = i / (res_x - 1) * self.uv_config['scale_u'] + v = j / (res_z - 1) * self.uv_config['scale_v'] + self.uvs.append((u, v)) + + # 生成索引(三角形列表) + for i in range(res_x - 1): + for j in range(res_z - 1): + # 第一个三角形 + self.indices.append(i * res_z + j) + self.indices.append((i + 1) * res_z + j) + self.indices.append(i * res_z + (j + 1)) + + # 第二个三角形 + self.indices.append((i + 1) * res_z + j) + self.indices.append((i + 1) * res_z + (j + 1)) + self.indices.append(i * res_z + (j + 1)) + + # 计算法线 + self._calculate_normals() + + # 更新统计信息 + self.geometry_stats['vertex_count'] = len(self.vertices) + self.geometry_stats['triangle_count'] = len(self.indices) // 3 + self.geometry_stats['patch_count'] = (res_x - 1) * (res_z - 1) + + print("✓ 初始网格生成完成") + + except Exception as e: + print(f"✗ 初始网格生成失败: {e}") + raise + + def _clear_mesh_data(self): + """清理网格数据""" + try: + self.vertices.clear() + self.normals.clear() + self.uvs.clear() + self.indices.clear() + + if self.height_field is not None: + self.height_field.fill(0) + + if self.normal_field is not None: + self.normal_field.fill(0) + + except Exception as e: + print(f"✗ 网格数据清理失败: {e}") + + def _update_surface_geometry(self, dt: float): + """ + 更新表面几何 + + Args: + dt: 时间增量 + """ + try: + if not self.vertices or self.height_field is None: + return + + res_x = self.mesh_config['resolution_x'] + res_z = self.mesh_config['resolution_z'] + + # 更新顶点高度 + for i in range(res_x): + for j in range(res_z): + vertex_index = i * res_z + j + if vertex_index < len(self.vertices): + x, y, z = self.vertices[vertex_index] + # 从高度场获取新高度 + new_height = self.height_field[i, j] + self.vertices[vertex_index] = (x, new_height, z) + + # 重新计算法线 + self._calculate_normals() + + except Exception as e: + print(f"✗ 表面几何更新失败: {e}") + + def _calculate_normals(self): + """计算顶点法线""" + try: + if not self.vertices or not self.indices: + return + + # 清除旧法线 + self.normals.clear() + + # 初始化法线数组 + vertex_normals = [np.array([0.0, 0.0, 0.0]) for _ in self.vertices] + + # 计算每个三角形的法线并累加到顶点 + for i in range(0, len(self.indices), 3): + i0 = self.indices[i] + i1 = self.indices[i + 1] + i2 = self.indices[i + 2] + + # 获取三角形顶点 + v0 = np.array(self.vertices[i0]) + v1 = np.array(self.vertices[i1]) + v2 = np.array(self.vertices[i2]) + + # 计算边向量 + edge1 = v1 - v0 + edge2 = v2 - v0 + + # 计算法线(叉积) + normal = np.cross(edge1, edge2) + + # 归一化 + normal_length = np.linalg.norm(normal) + if normal_length > 0: + normal = normal / normal_length + + # 累加到顶点法线 + vertex_normals[i0] += normal + vertex_normals[i1] += normal + vertex_normals[i2] += normal + + # 归一化顶点法线并存储 + for normal in vertex_normals: + normal_length = np.linalg.norm(normal) + if normal_length > 0: + normal = normal / normal_length + self.normals.append(tuple(normal)) + + except Exception as e: + print(f"✗ 法线计算失败: {e}") + + def _update_texture_coordinates(self, dt: float): + """ + 更新纹理坐标 + + Args: + dt: 时间增量 + """ + try: + # 更新滚动偏移 + self.uv_config['scroll_u'] += self.uv_config['scroll_speed_u'] * dt + self.uv_config['scroll_v'] += self.uv_config['scroll_speed_v'] * dt + + # 保持在[0,1)范围内 + self.uv_config['scroll_u'] %= 1.0 + self.uv_config['scroll_v'] %= 1.0 + + # 更新UV坐标 + if self.uvs: + res_x = self.mesh_config['resolution_x'] + res_z = self.mesh_config['resolution_z'] + + for i in range(res_x): + for j in range(res_z): + vertex_index = i * res_z + j + if vertex_index < len(self.uvs): + u = (i / (res_x - 1) + self.uv_config['scroll_u']) * self.uv_config['scale_u'] + v = (j / (res_z - 1) + self.uv_config['scroll_v']) * self.uv_config['scale_v'] + self.uvs[vertex_index] = (u % 1.0, v % 1.0) + + except Exception as e: + print(f"✗ 纹理坐标更新失败: {e}") + + def update_height_field(self, new_height_field: np.ndarray): + """ + 更新高度场 + + Args: + new_height_field: 新的高度场数据 + """ + try: + if self.height_field is None: + self._initialize_height_field() + + # 复制新高度场数据 + if new_height_field.shape == self.height_field.shape: + self.height_field[:, :] = new_height_field[:, :] + else: + # 如果尺寸不匹配,进行插值 + from scipy.interpolate import interp2d + + old_x = np.linspace(0, 1, self.height_field.shape[0]) + old_y = np.linspace(0, 1, self.height_field.shape[1]) + new_x = np.linspace(0, 1, new_height_field.shape[0]) + new_y = np.linspace(0, 1, new_height_field.shape[1]) + + interpolator = interp2d(old_x, old_y, new_height_field, kind='linear') + self.height_field[:, :] = interpolator(old_x, old_y) + + # 标记需要更新 + self.update_config['needs_update'] = True + + except Exception as e: + print(f"✗ 高度场更新失败: {e}") + + def generate_wave_pattern(self, wave_parameters: Dict[str, float], + time_value: float) -> np.ndarray: + """ + 生成波浪模式 + + Args: + wave_parameters: 波浪参数字典 + time_value: 时间值 + + Returns: + 高度场数组 + """ + try: + if self.height_field is None: + self._initialize_height_field() + + res_x, res_z = self.height_field.shape + patch_size = self.mesh_config['patch_size'] + + # 获取波浪参数 + amplitude = wave_parameters.get('amplitude', 1.0) + wavelength = wave_parameters.get('wavelength', 10.0) + speed = wave_parameters.get('speed', 1.0) + direction = wave_parameters.get('direction', 0.0) # 弧度 + + # 计算波向量 + wave_vector_x = 2 * math.pi / wavelength * math.cos(direction) + wave_vector_z = 2 * math.pi / wavelength * math.sin(direction) + + # 生成波浪 + for i in range(res_x): + for j in range(res_z): + # 计算世界坐标 + x = (i - res_x / 2) * patch_size / res_x + z = (j - res_z / 2) * patch_size / res_z + + # 计算相位 + phase = wave_vector_x * x + wave_vector_z * z - speed * time_value + + # 计算波浪高度 + height = amplitude * math.sin(phase) + self.height_field[i, j] = height + + return self.height_field.copy() + + except Exception as e: + print(f"✗ 波浪模式生成失败: {e}") + # 返回零高度场 + return np.zeros((res_x, res_z)) if self.height_field is not None else np.zeros((1, 1)) + + def generate_gerstner_waves(self, waves: List[Dict[str, float]], + time_value: float) -> np.ndarray: + """ + 生成Gerstner波浪(更真实的波浪模型) + + Args: + waves: 波浪参数列表,每个包含'amplitude', 'wavelength', 'speed', 'direction' + time_value: 时间值 + + Returns: + 高度场数组 + """ + try: + if self.height_field is None: + self._initialize_height_field() + + res_x, res_z = self.height_field.shape + patch_size = self.mesh_config['patch_size'] + + # 清零高度场 + self.height_field.fill(0) + + # 为每个波浪累加贡献 + for wave in waves: + amplitude = wave.get('amplitude', 1.0) + wavelength = wave.get('wavelength', 10.0) + speed = wave.get('speed', 1.0) + direction = wave.get('direction', 0.0) + + # 计算波数和频率 + wavenumber = 2 * math.pi / wavelength + frequency = speed * wavenumber + + # 计算波向量 + kx = wavenumber * math.cos(direction) + kz = wavenumber * math.sin(direction) + + # 为每个点计算波浪贡献 + for i in range(res_x): + for j in range(res_z): + # 计算世界坐标 + x = (i - res_x / 2) * patch_size / res_x + z = (j - res_z / 2) * patch_size / res_z + + # 计算相位 + phase = kx * x + kz * z - frequency * time_value + + # Gerstner波浪公式 + height_contribution = amplitude * math.sin(phase) + self.height_field[i, j] += height_contribution + + return self.height_field.copy() + + except Exception as e: + print(f"✗ Gerstner波浪生成失败: {e}") + return np.zeros((res_x, res_z)) if self.height_field is not None else np.zeros((1, 1)) + + def get_mesh_data(self) -> Dict[str, List[Tuple[float, float, float]]]: + """ + 获取网格数据 + + Returns: + 包含顶点、法线、UV坐标和索引的字典 + """ + return { + 'vertices': self.vertices.copy(), + 'normals': self.normals.copy(), + 'uvs': self.uvs.copy(), + 'indices': self.indices.copy() + } + + def get_height_at(self, x: float, z: float) -> float: + """ + 获取指定位置的高度 + + Args: + x, z: 世界坐标 + + Returns: + 高度值 + """ + try: + if self.height_field is None: + return 0.0 + + res_x, res_z = self.height_field.shape + patch_size = self.mesh_config['patch_size'] + + # 转换到网格坐标 + grid_x = (x / patch_size + 0.5) * res_x + grid_z = (z / patch_size + 0.5) * res_z + + # 边界检查 + if grid_x < 0 or grid_x >= res_x or grid_z < 0 or grid_z >= res_z: + return 0.0 + + # 简单的双线性插值 + i = int(grid_x) + j = int(grid_z) + fx = grid_x - i + fz = grid_z - j + + # 边界处理 + i1 = min(i + 1, res_x - 1) + j1 = min(j + 1, res_z - 1) + + # 双线性插值 + h00 = self.height_field[i, j] + h10 = self.height_field[i1, j] + h01 = self.height_field[i, j1] + h11 = self.height_field[i1, j1] + + h0 = h00 * (1 - fx) + h10 * fx + h1 = h01 * (1 - fx) + h11 * fx + + return h0 * (1 - fz) + h1 * fz + + except Exception as e: + print(f"✗ 高度获取失败: {e}") + return 0.0 + + def set_mesh_config(self, config: Dict[str, Any]): + """ + 设置网格配置 + + Args: + config: 网格配置字典 + """ + try: + old_res_x = self.mesh_config['resolution_x'] + old_res_z = self.mesh_config['resolution_z'] + + self.mesh_config.update(config) + + new_res_x = self.mesh_config['resolution_x'] + new_res_z = self.mesh_config['resolution_z'] + + # 如果分辨率改变,重新生成网格 + if old_res_x != new_res_x or old_res_z != new_res_z: + self._generate_initial_mesh() + + print(f"✓ 网格配置已更新: {self.mesh_config}") + except Exception as e: + print(f"✗ 网格配置设置失败: {e}") + + def get_mesh_config(self) -> Dict[str, Any]: + """ + 获取网格配置 + + Returns: + 网格配置字典 + """ + return self.mesh_config.copy() + + def set_uv_config(self, config: Dict[str, float]): + """ + 设置UV配置 + + Args: + config: UV配置字典 + """ + try: + self.uv_config.update(config) + print(f"✓ UV配置已更新: {self.uv_config}") + except Exception as e: + print(f"✗ UV配置设置失败: {e}") + + def get_uv_config(self) -> Dict[str, float]: + """ + 获取UV配置 + + Returns: + UV配置字典 + """ + return self.uv_config.copy() + + def get_geometry_stats(self) -> Dict[str, int]: + """ + 获取几何统计信息 + + Returns: + 几何统计字典 + """ + return self.geometry_stats.copy() + + def reset_geometry_stats(self): + """重置几何统计信息""" + try: + self.geometry_stats = { + 'vertex_count': len(self.vertices), + 'triangle_count': len(self.indices) // 3, + 'patch_count': (self.mesh_config['resolution_x'] - 1) * (self.mesh_config['resolution_z'] - 1), + 'updates_performed': 0 + } + print("✓ 几何统计信息已重置") + except Exception as e: + print(f"✗ 几何统计信息重置失败: {e}") + + def apply_displacement_map(self, displacement_map: np.ndarray): + """ + 应用位移贴图 + + Args: + displacement_map: 位移贴图数据 + """ + try: + self.update_height_field(displacement_map) + print("✓ 位移贴图已应用") + except Exception as e: print(f"✗ 位移贴图应用失败: {e}") \ No newline at end of file diff --git a/plugins/user/fluid_simulation/water/wave_system.py b/plugins/user/fluid_simulation/water/wave_system.py index 6829122a..ac73b2d4 100644 --- a/plugins/user/fluid_simulation/water/wave_system.py +++ b/plugins/user/fluid_simulation/water/wave_system.py @@ -1,808 +1,808 @@ -""" -波浪系统模块 -负责生成和管理各种类型的波浪效果 -""" - -import time -from typing import Dict, Any, List, Optional -import math -import numpy as np - -class WaveSystem: - """ - 波浪系统 - 负责生成和管理各种类型的波浪效果,包括风浪、涌浪、涟漪等 - """ - - def __init__(self, plugin): - """ - 初始化波浪系统 - - Args: - plugin: 水体和流体模拟插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 波浪类型定义 - self.wave_types = { - 'wind_wave': { - 'name': '风浪', - 'description': '由风力产生的波浪', - 'frequency_range': (0.1, 1.0), # Hz - 'amplitude_range': (0.01, 2.0), # 米 - 'speed_range': (1.0, 10.0), # m/s - 'typical_wavelength': 10.0 # 米 - }, - 'swell_wave': { - 'name': '涌浪', - 'description': '远处风暴产生的长周期波浪', - 'frequency_range': (0.05, 0.2), - 'amplitude_range': (0.5, 5.0), - 'speed_range': (5.0, 20.0), - 'typical_wavelength': 100.0 - }, - 'ripple': { - 'name': '涟漪', - 'description': '小尺度的表面波纹', - 'frequency_range': (1.0, 10.0), - 'amplitude_range': (0.001, 0.05), - 'speed_range': (0.1, 1.0), - 'typical_wavelength': 0.1 - }, - 'tsunami': { - 'name': '海啸', - 'description': '由地震或海底滑坡产生的巨大波浪', - 'frequency_range': (0.001, 0.01), - 'amplitude_range': (1.0, 50.0), - 'speed_range': (50.0, 200.0), - 'typical_wavelength': 100000.0 - }, - 'ship_wave': { - 'name': '船行波', - 'description': '船舶运动产生的波浪', - 'frequency_range': (0.1, 2.0), - 'amplitude_range': (0.1, 1.0), - 'speed_range': (0.5, 5.0), - 'typical_wavelength': 5.0 - } - } - - # 活跃波浪列表 - self.active_waves = [] - self.wave_counter = 0 - - # 波浪谱配置 - self.wave_spectrum = { - 'type': 'pierson_moskowitz', # 'pierson_moskowitz', 'jonswap', 'custom' - 'wind_speed': 10.0, # m/s - 'fetch': 10000.0, # 风区长度(米) - 'duration': 3600.0, # 风持续时间(秒) - 'peak_frequency': 0.2, # Hz - 'significant_wave_height': 1.0 # 有效波高(米) - } - - # 动态波浪配置 - self.dynamic_config = { - 'max_waves': 32, - 'update_frequency': 60.0, # Hz - 'last_update_time': 0.0, - 'time_scale': 1.0 - } - - # 环境影响因子 - self.environment_factors = { - 'wind_speed': 5.0, # m/s - 'wind_direction': 0.0, # 弧度 - 'water_depth': 10.0, # 米 - 'current_velocity': (0.0, 0.0), # (vx, vz) m/s - 'temperature': 20.0, # °C - 'salinity': 35.0 # ppt - } - - # 波浪统计 - self.wave_stats = { - 'total_waves': 0, - 'active_waves': 0, - 'total_energy': 0.0, - 'peak_wave_height': 0.0, - 'average_wave_period': 0.0 - } - - # 交互效果 - self.interaction_effects = { - 'splash_damping': 0.9, # 溅起波浪的阻尼系数 - 'wave_interference': True, # 是否启用波浪干涉 - 'nonlinear_effects': True, # 是否启用非线性效应 - 'breaking_waves': True # 是否启用碎浪效果 - } - - print("✓ 波浪系统已创建") - - def initialize(self) -> bool: - """ - 初始化波浪系统 - - Returns: - 是否初始化成功 - """ - try: - # 创建一些初始波浪 - self._create_initial_waves() - - self.initialized = True - print("✓ 波浪系统初始化完成") - return True - - except Exception as e: - print(f"✗ 波浪系统初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用波浪系统 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 波浪系统未初始化") - return False - - self.enabled = True - print("✓ 波浪系统已启用") - return True - - except Exception as e: - print(f"✗ 波浪系统启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用波浪系统""" - try: - self.enabled = False - print("✓ 波浪系统已禁用") - - except Exception as e: - print(f"✗ 波浪系统禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理波浪系统资源""" - try: - self.disable() - self.active_waves.clear() - self.initialized = False - print("✓ 波浪系统资源已清理") - - except Exception as e: - print(f"✗ 波浪系统资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新波浪系统状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新时间 - self.dynamic_config['last_update_time'] += dt * self.dynamic_config['time_scale'] - - # 更新活跃波浪 - self._update_active_waves(dt) - - # 根据环境条件生成新波浪 - self._generate_environment_waves(dt) - - # 更新统计信息 - self._update_wave_statistics() - - except Exception as e: - print(f"✗ 波浪系统更新失败: {e}") - import traceback - traceback.print_exc() - - def _create_initial_waves(self): - """创建初始波浪""" - try: - # 创建几个默认波浪 - for i in range(3): - wave_params = { - 'type': 'wind_wave', - 'amplitude': 0.5 + i * 0.2, - 'wavelength': 10.0 + i * 5.0, - 'speed': 2.0 + i * 0.5, - 'direction': i * math.pi / 3, - 'frequency': 0.3 + i * 0.1, - 'phase': 0.0, - 'damping': 0.01, - 'lifetime': float('inf'), - 'position': (0.0, 0.0) - } - - self._add_wave(wave_params) - - print("✓ 初始波浪创建完成") - - except Exception as e: - print(f"✗ 初始波浪创建失败: {e}") - - def _update_active_waves(self, dt: float): - """ - 更新活跃波浪 - - Args: - dt: 时间增量 - """ - try: - time_value = self.dynamic_config['last_update_time'] - - # 更新每个波浪 - waves_to_remove = [] - for wave in self.active_waves: - # 更新相位 - wave['phase'] += wave['frequency'] * 2 * math.pi * dt - - # 应用阻尼 - wave['amplitude'] *= (1.0 - wave['damping'] * dt) - - # 检查生命周期 - if wave['lifetime'] != float('inf'): - wave['lifetime'] -= dt - if wave['lifetime'] <= 0: - waves_to_remove.append(wave) - - # 更新位置(考虑水流速度) - current_vx, current_vz = self.environment_factors['current_velocity'] - wave['position'] = ( - wave['position'][0] + current_vx * dt, - wave['position'][1] + current_vz * dt - ) - - # 移除过期波浪 - for wave in waves_to_remove: - self.active_waves.remove(wave) - - except Exception as e: - print(f"✗ 活跃波浪更新失败: {e}") - - def _generate_environment_waves(self, dt: float): - """ - 根据环境条件生成波浪 - - Args: - dt: 时间增量 - """ - try: - # 基于风速生成波浪 - wind_speed = self.environment_factors['wind_speed'] - wind_direction = self.environment_factors['wind_direction'] - - # 简化的波浪生成逻辑 - # 实际实现中会使用更复杂的波浪谱模型 - wave_generation_probability = min(1.0, wind_speed / 20.0) * dt - - if np.random.random() < wave_generation_probability and len(self.active_waves) < self.dynamic_config['max_waves']: - # 生成新波浪 - wave_type = 'wind_wave' - wave_info = self.wave_types[wave_type] - - # 根据风速确定波浪参数 - amplitude = np.random.uniform(0.1, wind_speed / 5.0) - wavelength = np.random.uniform(5.0, max(5.0, wind_speed * 2.0)) - speed = min(wind_speed, math.sqrt(9.81 * wavelength / (2 * math.pi))) # 深水波速 - direction = wind_direction + np.random.uniform(-0.2, 0.2) # 小角度偏移 - - wave_params = { - 'type': wave_type, - 'amplitude': amplitude, - 'wavelength': wavelength, - 'speed': speed, - 'direction': direction, - 'frequency': speed / wavelength, - 'phase': np.random.uniform(0, 2 * math.pi), - 'damping': np.random.uniform(0.001, 0.01), - 'lifetime': np.random.uniform(10.0, 60.0), - 'position': (0.0, 0.0) - } - - self._add_wave(wave_params) - - except Exception as e: - print(f"✗ 环境波浪生成失败: {e}") - - def _add_wave(self, wave_params: Dict[str, Any]) -> int: - """ - 添加波浪 - - Args: - wave_params: 波浪参数 - - Returns: - 波浪ID - """ - try: - wave_id = self.wave_counter - self.wave_counter += 1 - - wave_params['id'] = wave_id - wave_params['creation_time'] = self.dynamic_config['last_update_time'] - - self.active_waves.append(wave_params) - - return wave_id - - except Exception as e: - print(f"✗ 波浪添加失败: {e}") - return -1 - - def _update_wave_statistics(self): - """更新波浪统计信息""" - try: - total_energy = 0.0 - peak_height = 0.0 - total_period = 0.0 - - for wave in self.active_waves: - # 波浪能量 (简化的计算) - energy = 0.5 * 1000.0 * 9.81 * wave['amplitude'] ** 2 - total_energy += energy - - # 峰值波高 - peak_height = max(peak_height, wave['amplitude'] * 2) # 波峰到波谷 - - # 平均周期 - if wave['frequency'] > 0: - total_period += 1.0 / wave['frequency'] - - self.wave_stats['total_waves'] = self.wave_counter - self.wave_stats['active_waves'] = len(self.active_waves) - self.wave_stats['total_energy'] = total_energy - self.wave_stats['peak_wave_height'] = peak_height - - if len(self.active_waves) > 0: - self.wave_stats['average_wave_period'] = total_period / len(self.active_waves) - else: - self.wave_stats['average_wave_period'] = 0.0 - - except Exception as e: - print(f"✗ 波浪统计更新失败: {e}") - - def create_wave(self, wave_type: str, parameters: Dict[str, Any]) -> int: - """ - 创建指定类型的波浪 - - Args: - wave_type: 波浪类型 - parameters: 波浪参数 - - Returns: - 波浪ID - """ - try: - if wave_type not in self.wave_types: - print(f"✗ 无效的波浪类型: {wave_type}") - return -1 - - # 获取波浪类型信息 - wave_info = self.wave_types[wave_type] - - # 默认参数 - default_params = { - 'type': wave_type, - 'amplitude': np.random.uniform(*wave_info['amplitude_range']), - 'wavelength': wave_info['typical_wavelength'], - 'speed': np.random.uniform(*wave_info['speed_range']), - 'direction': np.random.uniform(0, 2 * math.pi), - 'frequency': 0.0, - 'phase': 0.0, - 'damping': 0.01, - 'lifetime': float('inf'), - 'position': (0.0, 0.0) - } - - # 更新默认参数 - default_params.update(parameters) - - # 计算频率(如果未提供) - if default_params['frequency'] <= 0: - if default_params['wavelength'] > 0: - default_params['frequency'] = default_params['speed'] / default_params['wavelength'] - else: - default_params['frequency'] = 0.1 - - # 添加波浪 - wave_id = self._add_wave(default_params) - - if wave_id >= 0: - print(f"✓ {wave_info['name']} 已创建 (ID: {wave_id})") - - return wave_id - - except Exception as e: - print(f"✗ 波浪创建失败: {e}") - return -1 - - def remove_wave(self, wave_id: int) -> bool: - """ - 移除波浪 - - Args: - wave_id: 波浪ID - - Returns: - 是否移除成功 - """ - try: - for wave in self.active_waves: - if wave['id'] == wave_id: - self.active_waves.remove(wave) - print(f"✓ 波浪已移除 (ID: {wave_id})") - return True - - print(f"✗ 波浪不存在 (ID: {wave_id})") - return False - - except Exception as e: - print(f"✗ 波浪移除失败: {e}") - return False - - def get_wave(self, wave_id: int) -> Optional[Dict[str, Any]]: - """ - 获取波浪信息 - - Args: - wave_id: 波浪ID - - Returns: - 波浪信息或None - """ - try: - for wave in self.active_waves: - if wave['id'] == wave_id: - return wave.copy() - return None - - except Exception as e: - print(f"✗ 波浪信息获取失败: {e}") - return None - - def get_all_waves(self) -> List[Dict[str, Any]]: - """ - 获取所有活跃波浪 - - Returns: - 波浪列表 - """ - return [wave.copy() for wave in self.active_waves] - - def calculate_wave_height_at(self, x: float, z: float, time_value: float = None) -> float: - """ - 计算指定位置和时间的波浪高度 - - Args: - x, z: 位置坐标 - time_value: 时间值(如果为None则使用当前时间) - - Returns: - 波浪高度 - """ - try: - if time_value is None: - time_value = self.dynamic_config['last_update_time'] - - total_height = 0.0 - - # 计算每个波浪在该点的贡献 - for wave in self.active_waves: - # 计算相对于波浪位置的距离 - dx = x - wave['position'][0] - dz = z - wave['position'][1] - - # 计算在波浪传播方向上的投影距离 - direction = wave['direction'] - projection = dx * math.cos(direction) + dz * math.sin(direction) - - # 计算相位 - phase = wave['phase'] + 2 * math.pi * projection / wave['wavelength'] - - # 计算波浪高度(正弦波) - height = wave['amplitude'] * math.sin(phase) - - # 如果启用波浪干涉,直接叠加 - if self.interaction_effects['wave_interference']: - total_height += height - else: - # 否则取最大值 - total_height = max(total_height, height) - - return total_height - - except Exception as e: - print(f"✗ 波浪高度计算失败: {e}") - return 0.0 - - def calculate_wave_velocity_at(self, x: float, z: float, time_value: float = None) -> tuple: - """ - 计算指定位置和时间的波浪速度 - - Args: - x, z: 位置坐标 - time_value: 时间值(如果为None则使用当前时间) - - Returns: - 波浪速度 (vx, vz) - """ - try: - if time_value is None: - time_value = self.dynamic_config['last_update_time'] - - total_vx = 0.0 - total_vz = 0.0 - - # 计算每个波浪在该点的速度贡献 - for wave in self.active_waves: - # 计算相对于波浪位置的距离 - dx = x - wave['position'][0] - dz = z - wave['position'][1] - - # 计算在波浪传播方向上的投影距离 - direction = wave['direction'] - projection = dx * math.cos(direction) + dz * math.sin(direction) - - # 计算相位 - phase = wave['phase'] + 2 * math.pi * projection / wave['wavelength'] - - # 计算速度分量 - speed = wave['speed'] - vx = speed * math.cos(direction) - vz = speed * math.sin(direction) - - # 考虑波浪振幅的影响 - velocity_factor = wave['amplitude'] * math.cos(phase) - total_vx += vx * velocity_factor - total_vz += vz * velocity_factor - - return (total_vx, total_vz) - - except Exception as e: - print(f"✗ 波浪速度计算失败: {e}") - return (0.0, 0.0) - - def generate_wave_spectrum(self) -> Dict[str, List[float]]: - """ - 生成波浪谱 - - Returns: - 包含频率和能量密度的字典 - """ - try: - spectrum_type = self.wave_spectrum['type'] - wind_speed = self.wave_spectrum['wind_speed'] - - # 频率范围 - freq_min, freq_max = 0.01, 2.0 - num_points = 100 - frequencies = np.linspace(freq_min, freq_max, num_points) - energy_densities = np.zeros(num_points) - - if spectrum_type == 'pierson_moskowitz': - # Pierson-Moskowitz谱 - alpha = 8.1e-3 - beta = 0.74 - g = 9.81 - - for i, f in enumerate(frequencies): - if f > 0: - omega = 2 * math.pi * f - energy_densities[i] = alpha * g**2 / omega**5 * math.exp(-beta * (g / (omega * wind_speed))**4) - - elif spectrum_type == 'jonswap': - # JONSWAP谱 - alpha = 0.076 * (wind_speed**2 / (g * 100))**0.22 # 简化计算 - beta = 0.74 - gamma = 3.3 # 峰值增强因子 - sigma = 0.07 # 谱宽参数 - - fp = self.wave_spectrum['peak_frequency'] - omega_p = 2 * math.pi * fp - - for i, f in enumerate(frequencies): - if f > 0: - omega = 2 * math.pi * f - ratio = omega / omega_p - - # 谱宽参数 - if ratio <= 1: - sigma_val = 0.07 - else: - sigma_val = 0.09 - - # 峰值增强函数 - gamma_val = gamma ** math.exp(-0.5 * ((ratio - 1) / sigma_val)**2) - - energy_densities[i] = alpha * g**2 / omega**5 * math.exp(-beta * (g / (omega * wind_speed))**4) * gamma_val - - return { - 'frequencies': frequencies.tolist(), - 'energy_densities': energy_densities.tolist() - } - - except Exception as e: - print(f"✗ 波浪谱生成失败: {e}") - return { - 'frequencies': [], - 'energy_densities': [] - } - - def create_splash_wave(self, position: tuple, energy: float) -> List[int]: - """ - 创建溅起波浪效果 - - Args: - position: 溅起位置 (x, z) - energy: 溅起能量 - - Returns: - 创建的波浪ID列表 - """ - try: - created_waves = [] - - # 创建多个同心圆波浪来模拟溅起效果 - num_ripples = min(5, int(energy / 0.1)) - - for i in range(num_ripples): - amplitude = energy * 0.1 * (1.0 - i * 0.15) - wavelength = 0.5 + i * 0.3 - speed = 1.0 + i * 0.5 - lifetime = 2.0 + i * 0.5 - - wave_params = { - 'type': 'ripple', - 'amplitude': amplitude, - 'wavelength': wavelength, - 'speed': speed, - 'direction': 0.0, # 径向波 - 'frequency': speed / wavelength, - 'phase': 0.0, - 'damping': 0.5, - 'lifetime': lifetime, - 'position': position - } - - wave_id = self._add_wave(wave_params) - if wave_id >= 0: - created_waves.append(wave_id) - - if created_waves: - print(f"✓ 溅起波浪已创建 ({len(created_waves)} 个波浪)") - - return created_waves - - except Exception as e: - print(f"✗ 溅起波浪创建失败: {e}") - return [] - - def update_environment_factors(self, factors: Dict[str, Any]): - """ - 更新环境因子 - - Args: - factors: 环境因子字典 - """ - try: - self.environment_factors.update(factors) - print(f"✓ 环境因子已更新: {factors}") - except Exception as e: - print(f"✗ 环境因子更新失败: {e}") - - def get_environment_factors(self) -> Dict[str, Any]: - """ - 获取环境因子 - - Returns: - 环境因子字典 - """ - return self.environment_factors.copy() - - def set_wave_spectrum(self, spectrum: Dict[str, Any]): - """ - 设置波浪谱参数 - - Args: - spectrum: 波浪谱参数字典 - """ - try: - self.wave_spectrum.update(spectrum) - print(f"✓ 波浪谱参数已更新: {self.wave_spectrum}") - except Exception as e: - print(f"✗ 波浪谱参数设置失败: {e}") - - def get_wave_spectrum(self) -> Dict[str, Any]: - """ - 获取波浪谱参数 - - Returns: - 波浪谱参数字典 - """ - return self.wave_spectrum.copy() - - def set_dynamic_config(self, config: Dict[str, Any]): - """ - 设置动态配置 - - Args: - config: 动态配置字典 - """ - try: - self.dynamic_config.update(config) - print(f"✓ 动态配置已更新: {self.dynamic_config}") - except Exception as e: - print(f"✗ 动态配置设置失败: {e}") - - def get_dynamic_config(self) -> Dict[str, Any]: - """ - 获取动态配置 - - Returns: - 动态配置字典 - """ - return self.dynamic_config.copy() - - def get_wave_statistics(self) -> Dict[str, Any]: - """ - 获取波浪统计信息 - - Returns: - 波浪统计字典 - """ - return self.wave_stats.copy() - - def reset_wave_statistics(self): - """重置波浪统计信息""" - try: - self.wave_stats = { - 'total_waves': self.wave_counter, - 'active_waves': len(self.active_waves), - 'total_energy': 0.0, - 'peak_wave_height': 0.0, - 'average_wave_period': 0.0 - } - print("✓ 波浪统计信息已重置") - except Exception as e: - print(f"✗ 波浪统计信息重置失败: {e}") - - def get_wave_types(self) -> List[str]: - """ - 获取所有波浪类型 - - Returns: - 波浪类型列表 - """ - return list(self.wave_types.keys()) - - def get_wave_type_info(self, wave_type: str) -> Optional[Dict[str, Any]]: - """ - 获取波浪类型信息 - - Args: - wave_type: 波浪类型 - - Returns: - 波浪类型信息或None - """ +""" +波浪系统模块 +负责生成和管理各种类型的波浪效果 +""" + +import time +from typing import Dict, Any, List, Optional +import math +import numpy as np + +class WaveSystem: + """ + 波浪系统 + 负责生成和管理各种类型的波浪效果,包括风浪、涌浪、涟漪等 + """ + + def __init__(self, plugin): + """ + 初始化波浪系统 + + Args: + plugin: 水体和流体模拟插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 波浪类型定义 + self.wave_types = { + 'wind_wave': { + 'name': '风浪', + 'description': '由风力产生的波浪', + 'frequency_range': (0.1, 1.0), # Hz + 'amplitude_range': (0.01, 2.0), # 米 + 'speed_range': (1.0, 10.0), # m/s + 'typical_wavelength': 10.0 # 米 + }, + 'swell_wave': { + 'name': '涌浪', + 'description': '远处风暴产生的长周期波浪', + 'frequency_range': (0.05, 0.2), + 'amplitude_range': (0.5, 5.0), + 'speed_range': (5.0, 20.0), + 'typical_wavelength': 100.0 + }, + 'ripple': { + 'name': '涟漪', + 'description': '小尺度的表面波纹', + 'frequency_range': (1.0, 10.0), + 'amplitude_range': (0.001, 0.05), + 'speed_range': (0.1, 1.0), + 'typical_wavelength': 0.1 + }, + 'tsunami': { + 'name': '海啸', + 'description': '由地震或海底滑坡产生的巨大波浪', + 'frequency_range': (0.001, 0.01), + 'amplitude_range': (1.0, 50.0), + 'speed_range': (50.0, 200.0), + 'typical_wavelength': 100000.0 + }, + 'ship_wave': { + 'name': '船行波', + 'description': '船舶运动产生的波浪', + 'frequency_range': (0.1, 2.0), + 'amplitude_range': (0.1, 1.0), + 'speed_range': (0.5, 5.0), + 'typical_wavelength': 5.0 + } + } + + # 活跃波浪列表 + self.active_waves = [] + self.wave_counter = 0 + + # 波浪谱配置 + self.wave_spectrum = { + 'type': 'pierson_moskowitz', # 'pierson_moskowitz', 'jonswap', 'custom' + 'wind_speed': 10.0, # m/s + 'fetch': 10000.0, # 风区长度(米) + 'duration': 3600.0, # 风持续时间(秒) + 'peak_frequency': 0.2, # Hz + 'significant_wave_height': 1.0 # 有效波高(米) + } + + # 动态波浪配置 + self.dynamic_config = { + 'max_waves': 32, + 'update_frequency': 60.0, # Hz + 'last_update_time': 0.0, + 'time_scale': 1.0 + } + + # 环境影响因子 + self.environment_factors = { + 'wind_speed': 5.0, # m/s + 'wind_direction': 0.0, # 弧度 + 'water_depth': 10.0, # 米 + 'current_velocity': (0.0, 0.0), # (vx, vz) m/s + 'temperature': 20.0, # °C + 'salinity': 35.0 # ppt + } + + # 波浪统计 + self.wave_stats = { + 'total_waves': 0, + 'active_waves': 0, + 'total_energy': 0.0, + 'peak_wave_height': 0.0, + 'average_wave_period': 0.0 + } + + # 交互效果 + self.interaction_effects = { + 'splash_damping': 0.9, # 溅起波浪的阻尼系数 + 'wave_interference': True, # 是否启用波浪干涉 + 'nonlinear_effects': True, # 是否启用非线性效应 + 'breaking_waves': True # 是否启用碎浪效果 + } + + print("✓ 波浪系统已创建") + + def initialize(self) -> bool: + """ + 初始化波浪系统 + + Returns: + 是否初始化成功 + """ + try: + # 创建一些初始波浪 + self._create_initial_waves() + + self.initialized = True + print("✓ 波浪系统初始化完成") + return True + + except Exception as e: + print(f"✗ 波浪系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用波浪系统 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 波浪系统未初始化") + return False + + self.enabled = True + print("✓ 波浪系统已启用") + return True + + except Exception as e: + print(f"✗ 波浪系统启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用波浪系统""" + try: + self.enabled = False + print("✓ 波浪系统已禁用") + + except Exception as e: + print(f"✗ 波浪系统禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理波浪系统资源""" + try: + self.disable() + self.active_waves.clear() + self.initialized = False + print("✓ 波浪系统资源已清理") + + except Exception as e: + print(f"✗ 波浪系统资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新波浪系统状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新时间 + self.dynamic_config['last_update_time'] += dt * self.dynamic_config['time_scale'] + + # 更新活跃波浪 + self._update_active_waves(dt) + + # 根据环境条件生成新波浪 + self._generate_environment_waves(dt) + + # 更新统计信息 + self._update_wave_statistics() + + except Exception as e: + print(f"✗ 波浪系统更新失败: {e}") + import traceback + traceback.print_exc() + + def _create_initial_waves(self): + """创建初始波浪""" + try: + # 创建几个默认波浪 + for i in range(3): + wave_params = { + 'type': 'wind_wave', + 'amplitude': 0.5 + i * 0.2, + 'wavelength': 10.0 + i * 5.0, + 'speed': 2.0 + i * 0.5, + 'direction': i * math.pi / 3, + 'frequency': 0.3 + i * 0.1, + 'phase': 0.0, + 'damping': 0.01, + 'lifetime': float('inf'), + 'position': (0.0, 0.0) + } + + self._add_wave(wave_params) + + print("✓ 初始波浪创建完成") + + except Exception as e: + print(f"✗ 初始波浪创建失败: {e}") + + def _update_active_waves(self, dt: float): + """ + 更新活跃波浪 + + Args: + dt: 时间增量 + """ + try: + time_value = self.dynamic_config['last_update_time'] + + # 更新每个波浪 + waves_to_remove = [] + for wave in self.active_waves: + # 更新相位 + wave['phase'] += wave['frequency'] * 2 * math.pi * dt + + # 应用阻尼 + wave['amplitude'] *= (1.0 - wave['damping'] * dt) + + # 检查生命周期 + if wave['lifetime'] != float('inf'): + wave['lifetime'] -= dt + if wave['lifetime'] <= 0: + waves_to_remove.append(wave) + + # 更新位置(考虑水流速度) + current_vx, current_vz = self.environment_factors['current_velocity'] + wave['position'] = ( + wave['position'][0] + current_vx * dt, + wave['position'][1] + current_vz * dt + ) + + # 移除过期波浪 + for wave in waves_to_remove: + self.active_waves.remove(wave) + + except Exception as e: + print(f"✗ 活跃波浪更新失败: {e}") + + def _generate_environment_waves(self, dt: float): + """ + 根据环境条件生成波浪 + + Args: + dt: 时间增量 + """ + try: + # 基于风速生成波浪 + wind_speed = self.environment_factors['wind_speed'] + wind_direction = self.environment_factors['wind_direction'] + + # 简化的波浪生成逻辑 + # 实际实现中会使用更复杂的波浪谱模型 + wave_generation_probability = min(1.0, wind_speed / 20.0) * dt + + if np.random.random() < wave_generation_probability and len(self.active_waves) < self.dynamic_config['max_waves']: + # 生成新波浪 + wave_type = 'wind_wave' + wave_info = self.wave_types[wave_type] + + # 根据风速确定波浪参数 + amplitude = np.random.uniform(0.1, wind_speed / 5.0) + wavelength = np.random.uniform(5.0, max(5.0, wind_speed * 2.0)) + speed = min(wind_speed, math.sqrt(9.81 * wavelength / (2 * math.pi))) # 深水波速 + direction = wind_direction + np.random.uniform(-0.2, 0.2) # 小角度偏移 + + wave_params = { + 'type': wave_type, + 'amplitude': amplitude, + 'wavelength': wavelength, + 'speed': speed, + 'direction': direction, + 'frequency': speed / wavelength, + 'phase': np.random.uniform(0, 2 * math.pi), + 'damping': np.random.uniform(0.001, 0.01), + 'lifetime': np.random.uniform(10.0, 60.0), + 'position': (0.0, 0.0) + } + + self._add_wave(wave_params) + + except Exception as e: + print(f"✗ 环境波浪生成失败: {e}") + + def _add_wave(self, wave_params: Dict[str, Any]) -> int: + """ + 添加波浪 + + Args: + wave_params: 波浪参数 + + Returns: + 波浪ID + """ + try: + wave_id = self.wave_counter + self.wave_counter += 1 + + wave_params['id'] = wave_id + wave_params['creation_time'] = self.dynamic_config['last_update_time'] + + self.active_waves.append(wave_params) + + return wave_id + + except Exception as e: + print(f"✗ 波浪添加失败: {e}") + return -1 + + def _update_wave_statistics(self): + """更新波浪统计信息""" + try: + total_energy = 0.0 + peak_height = 0.0 + total_period = 0.0 + + for wave in self.active_waves: + # 波浪能量 (简化的计算) + energy = 0.5 * 1000.0 * 9.81 * wave['amplitude'] ** 2 + total_energy += energy + + # 峰值波高 + peak_height = max(peak_height, wave['amplitude'] * 2) # 波峰到波谷 + + # 平均周期 + if wave['frequency'] > 0: + total_period += 1.0 / wave['frequency'] + + self.wave_stats['total_waves'] = self.wave_counter + self.wave_stats['active_waves'] = len(self.active_waves) + self.wave_stats['total_energy'] = total_energy + self.wave_stats['peak_wave_height'] = peak_height + + if len(self.active_waves) > 0: + self.wave_stats['average_wave_period'] = total_period / len(self.active_waves) + else: + self.wave_stats['average_wave_period'] = 0.0 + + except Exception as e: + print(f"✗ 波浪统计更新失败: {e}") + + def create_wave(self, wave_type: str, parameters: Dict[str, Any]) -> int: + """ + 创建指定类型的波浪 + + Args: + wave_type: 波浪类型 + parameters: 波浪参数 + + Returns: + 波浪ID + """ + try: + if wave_type not in self.wave_types: + print(f"✗ 无效的波浪类型: {wave_type}") + return -1 + + # 获取波浪类型信息 + wave_info = self.wave_types[wave_type] + + # 默认参数 + default_params = { + 'type': wave_type, + 'amplitude': np.random.uniform(*wave_info['amplitude_range']), + 'wavelength': wave_info['typical_wavelength'], + 'speed': np.random.uniform(*wave_info['speed_range']), + 'direction': np.random.uniform(0, 2 * math.pi), + 'frequency': 0.0, + 'phase': 0.0, + 'damping': 0.01, + 'lifetime': float('inf'), + 'position': (0.0, 0.0) + } + + # 更新默认参数 + default_params.update(parameters) + + # 计算频率(如果未提供) + if default_params['frequency'] <= 0: + if default_params['wavelength'] > 0: + default_params['frequency'] = default_params['speed'] / default_params['wavelength'] + else: + default_params['frequency'] = 0.1 + + # 添加波浪 + wave_id = self._add_wave(default_params) + + if wave_id >= 0: + print(f"✓ {wave_info['name']} 已创建 (ID: {wave_id})") + + return wave_id + + except Exception as e: + print(f"✗ 波浪创建失败: {e}") + return -1 + + def remove_wave(self, wave_id: int) -> bool: + """ + 移除波浪 + + Args: + wave_id: 波浪ID + + Returns: + 是否移除成功 + """ + try: + for wave in self.active_waves: + if wave['id'] == wave_id: + self.active_waves.remove(wave) + print(f"✓ 波浪已移除 (ID: {wave_id})") + return True + + print(f"✗ 波浪不存在 (ID: {wave_id})") + return False + + except Exception as e: + print(f"✗ 波浪移除失败: {e}") + return False + + def get_wave(self, wave_id: int) -> Optional[Dict[str, Any]]: + """ + 获取波浪信息 + + Args: + wave_id: 波浪ID + + Returns: + 波浪信息或None + """ + try: + for wave in self.active_waves: + if wave['id'] == wave_id: + return wave.copy() + return None + + except Exception as e: + print(f"✗ 波浪信息获取失败: {e}") + return None + + def get_all_waves(self) -> List[Dict[str, Any]]: + """ + 获取所有活跃波浪 + + Returns: + 波浪列表 + """ + return [wave.copy() for wave in self.active_waves] + + def calculate_wave_height_at(self, x: float, z: float, time_value: float = None) -> float: + """ + 计算指定位置和时间的波浪高度 + + Args: + x, z: 位置坐标 + time_value: 时间值(如果为None则使用当前时间) + + Returns: + 波浪高度 + """ + try: + if time_value is None: + time_value = self.dynamic_config['last_update_time'] + + total_height = 0.0 + + # 计算每个波浪在该点的贡献 + for wave in self.active_waves: + # 计算相对于波浪位置的距离 + dx = x - wave['position'][0] + dz = z - wave['position'][1] + + # 计算在波浪传播方向上的投影距离 + direction = wave['direction'] + projection = dx * math.cos(direction) + dz * math.sin(direction) + + # 计算相位 + phase = wave['phase'] + 2 * math.pi * projection / wave['wavelength'] + + # 计算波浪高度(正弦波) + height = wave['amplitude'] * math.sin(phase) + + # 如果启用波浪干涉,直接叠加 + if self.interaction_effects['wave_interference']: + total_height += height + else: + # 否则取最大值 + total_height = max(total_height, height) + + return total_height + + except Exception as e: + print(f"✗ 波浪高度计算失败: {e}") + return 0.0 + + def calculate_wave_velocity_at(self, x: float, z: float, time_value: float = None) -> tuple: + """ + 计算指定位置和时间的波浪速度 + + Args: + x, z: 位置坐标 + time_value: 时间值(如果为None则使用当前时间) + + Returns: + 波浪速度 (vx, vz) + """ + try: + if time_value is None: + time_value = self.dynamic_config['last_update_time'] + + total_vx = 0.0 + total_vz = 0.0 + + # 计算每个波浪在该点的速度贡献 + for wave in self.active_waves: + # 计算相对于波浪位置的距离 + dx = x - wave['position'][0] + dz = z - wave['position'][1] + + # 计算在波浪传播方向上的投影距离 + direction = wave['direction'] + projection = dx * math.cos(direction) + dz * math.sin(direction) + + # 计算相位 + phase = wave['phase'] + 2 * math.pi * projection / wave['wavelength'] + + # 计算速度分量 + speed = wave['speed'] + vx = speed * math.cos(direction) + vz = speed * math.sin(direction) + + # 考虑波浪振幅的影响 + velocity_factor = wave['amplitude'] * math.cos(phase) + total_vx += vx * velocity_factor + total_vz += vz * velocity_factor + + return (total_vx, total_vz) + + except Exception as e: + print(f"✗ 波浪速度计算失败: {e}") + return (0.0, 0.0) + + def generate_wave_spectrum(self) -> Dict[str, List[float]]: + """ + 生成波浪谱 + + Returns: + 包含频率和能量密度的字典 + """ + try: + spectrum_type = self.wave_spectrum['type'] + wind_speed = self.wave_spectrum['wind_speed'] + + # 频率范围 + freq_min, freq_max = 0.01, 2.0 + num_points = 100 + frequencies = np.linspace(freq_min, freq_max, num_points) + energy_densities = np.zeros(num_points) + + if spectrum_type == 'pierson_moskowitz': + # Pierson-Moskowitz谱 + alpha = 8.1e-3 + beta = 0.74 + g = 9.81 + + for i, f in enumerate(frequencies): + if f > 0: + omega = 2 * math.pi * f + energy_densities[i] = alpha * g**2 / omega**5 * math.exp(-beta * (g / (omega * wind_speed))**4) + + elif spectrum_type == 'jonswap': + # JONSWAP谱 + alpha = 0.076 * (wind_speed**2 / (g * 100))**0.22 # 简化计算 + beta = 0.74 + gamma = 3.3 # 峰值增强因子 + sigma = 0.07 # 谱宽参数 + + fp = self.wave_spectrum['peak_frequency'] + omega_p = 2 * math.pi * fp + + for i, f in enumerate(frequencies): + if f > 0: + omega = 2 * math.pi * f + ratio = omega / omega_p + + # 谱宽参数 + if ratio <= 1: + sigma_val = 0.07 + else: + sigma_val = 0.09 + + # 峰值增强函数 + gamma_val = gamma ** math.exp(-0.5 * ((ratio - 1) / sigma_val)**2) + + energy_densities[i] = alpha * g**2 / omega**5 * math.exp(-beta * (g / (omega * wind_speed))**4) * gamma_val + + return { + 'frequencies': frequencies.tolist(), + 'energy_densities': energy_densities.tolist() + } + + except Exception as e: + print(f"✗ 波浪谱生成失败: {e}") + return { + 'frequencies': [], + 'energy_densities': [] + } + + def create_splash_wave(self, position: tuple, energy: float) -> List[int]: + """ + 创建溅起波浪效果 + + Args: + position: 溅起位置 (x, z) + energy: 溅起能量 + + Returns: + 创建的波浪ID列表 + """ + try: + created_waves = [] + + # 创建多个同心圆波浪来模拟溅起效果 + num_ripples = min(5, int(energy / 0.1)) + + for i in range(num_ripples): + amplitude = energy * 0.1 * (1.0 - i * 0.15) + wavelength = 0.5 + i * 0.3 + speed = 1.0 + i * 0.5 + lifetime = 2.0 + i * 0.5 + + wave_params = { + 'type': 'ripple', + 'amplitude': amplitude, + 'wavelength': wavelength, + 'speed': speed, + 'direction': 0.0, # 径向波 + 'frequency': speed / wavelength, + 'phase': 0.0, + 'damping': 0.5, + 'lifetime': lifetime, + 'position': position + } + + wave_id = self._add_wave(wave_params) + if wave_id >= 0: + created_waves.append(wave_id) + + if created_waves: + print(f"✓ 溅起波浪已创建 ({len(created_waves)} 个波浪)") + + return created_waves + + except Exception as e: + print(f"✗ 溅起波浪创建失败: {e}") + return [] + + def update_environment_factors(self, factors: Dict[str, Any]): + """ + 更新环境因子 + + Args: + factors: 环境因子字典 + """ + try: + self.environment_factors.update(factors) + print(f"✓ 环境因子已更新: {factors}") + except Exception as e: + print(f"✗ 环境因子更新失败: {e}") + + def get_environment_factors(self) -> Dict[str, Any]: + """ + 获取环境因子 + + Returns: + 环境因子字典 + """ + return self.environment_factors.copy() + + def set_wave_spectrum(self, spectrum: Dict[str, Any]): + """ + 设置波浪谱参数 + + Args: + spectrum: 波浪谱参数字典 + """ + try: + self.wave_spectrum.update(spectrum) + print(f"✓ 波浪谱参数已更新: {self.wave_spectrum}") + except Exception as e: + print(f"✗ 波浪谱参数设置失败: {e}") + + def get_wave_spectrum(self) -> Dict[str, Any]: + """ + 获取波浪谱参数 + + Returns: + 波浪谱参数字典 + """ + return self.wave_spectrum.copy() + + def set_dynamic_config(self, config: Dict[str, Any]): + """ + 设置动态配置 + + Args: + config: 动态配置字典 + """ + try: + self.dynamic_config.update(config) + print(f"✓ 动态配置已更新: {self.dynamic_config}") + except Exception as e: + print(f"✗ 动态配置设置失败: {e}") + + def get_dynamic_config(self) -> Dict[str, Any]: + """ + 获取动态配置 + + Returns: + 动态配置字典 + """ + return self.dynamic_config.copy() + + def get_wave_statistics(self) -> Dict[str, Any]: + """ + 获取波浪统计信息 + + Returns: + 波浪统计字典 + """ + return self.wave_stats.copy() + + def reset_wave_statistics(self): + """重置波浪统计信息""" + try: + self.wave_stats = { + 'total_waves': self.wave_counter, + 'active_waves': len(self.active_waves), + 'total_energy': 0.0, + 'peak_wave_height': 0.0, + 'average_wave_period': 0.0 + } + print("✓ 波浪统计信息已重置") + except Exception as e: + print(f"✗ 波浪统计信息重置失败: {e}") + + def get_wave_types(self) -> List[str]: + """ + 获取所有波浪类型 + + Returns: + 波浪类型列表 + """ + return list(self.wave_types.keys()) + + def get_wave_type_info(self, wave_type: str) -> Optional[Dict[str, Any]]: + """ + 获取波浪类型信息 + + Args: + wave_type: 波浪类型 + + Returns: + 波浪类型信息或None + """ return self.wave_types.get(wave_type) \ No newline at end of file diff --git a/plugins/user/haptic_feedback_system/README.md b/plugins/user/haptic_feedback_system/README.md index 4a2ef520..3085c918 100644 --- a/plugins/user/haptic_feedback_system/README.md +++ b/plugins/user/haptic_feedback_system/README.md @@ -1,184 +1,184 @@ -# 触觉反馈系统插件 - -## 简介 - -触觉反馈系统插件为EG引擎提供全面的触觉反馈支持,包括设备管理、效果处理、物理集成、音频同步等功能。该插件支持多种触觉设备,如游戏手柄、VR控制器、触觉手套、震动背心等,并提供丰富的API供开发者集成到自己的应用中。 - -## 功能特性 - -### 1. 设备管理 -- 自动发现和管理多种触觉设备 -- 支持USB、蓝牙、无线等多种连接方式 -- 实时监控设备状态和电池电量 -- 支持热插拔设备检测 - -### 2. 效果系统 -- 预定义多种触觉效果(脉冲、震动、爆炸等) -- 支持自定义效果创建和编辑 -- 效果混合和层叠播放 -- 实时效果参数调整 - -### 3. 物理引擎集成 -- 碰撞反馈:根据碰撞冲量生成触觉效果 -- 力反馈:实时力反馈模拟 -- 摩擦反馈:基于表面材质的纹理反馈 -- 材质系统:不同材质对应不同触觉反馈 - -### 4. 音频同步 -- 音频特征分析和触觉映射 -- 节拍检测和同步反馈 -- 频谱分析和多频段反馈 -- 音量驱动的动态触觉效果 - -### 5. 事件系统 -- 游戏事件到触觉效果的映射 -- 事件优先级和抑制机制 -- 事件组合和复合反馈 -- 可自定义的事件映射规则 - -### 6. 配置管理 -- 全局配置和用户偏好设置 -- 多种预设配置文件 -- 实时配置更新和保存 -- 配置验证和错误处理 - -### 7. 编辑器支持 -- 可视化设备管理界面 -- 效果编辑和实时预览 -- 配置文件管理 -- 实时监控和调试工具 - -## 目录结构 - -``` -haptic_feedback_system/ -├── plugin.py # 插件主文件 -├── core/ -│ └── haptic_manager.py # 触觉管理器 -├── effects/ -│ └── effect_processor.py # 效果处理器 -├── devices/ -│ └── device_manager.py # 设备管理器 -├── physics/ -│ └── physics_integration.py# 物理引擎集成 -├── audio/ -│ └── audio_haptics.py # 音频触觉反馈 -├── events/ -│ └── event_handler.py # 事件处理器 -├── config/ -│ └── config_manager.py # 配置管理器 -├── editor/ -│ └── haptic_editor.py # 触觉编辑器 -└── utils/ - └── haptic_utils.py # 工具类 -``` - -## 核心模块 - -### 触觉管理器 (HapticManager) -负责管理触觉效果的播放和设备控制,提供效果播放、停止、暂停、恢复等功能。 - -### 效果处理器 (EffectProcessor) -处理各种触觉效果,包括预定义效果和自定义效果,支持效果混合和参数调整。 - -### 设备管理器 (HapticDeviceManager) -管理各种触觉设备,包括设备发现、连接、状态监控和控制。 - -### 物理引擎集成 (PhysicsIntegration) -将物理引擎中的碰撞、力和摩擦转换为触觉反馈。 - -### 音频触觉反馈 (AudioHaptics) -将音频特征转换为触觉效果,支持节拍检测和频谱分析。 - -### 事件处理器 (HapticEventHandler) -处理游戏事件并触发相应的触觉反馈。 - -### 配置管理器 (HapticConfigManager) -管理插件配置和用户偏好设置。 - -### 触觉编辑器 (HapticEditor) -提供可视化界面用于配置和测试触觉反馈效果。 - -### 工具类 (HapticUtils) -提供实用工具函数,包括性能监控、日志记录、数据转换等。 - -## 使用示例 - -```python -# 初始化插件 -plugin = HapticFeedbackSystemPlugin(engine) -plugin.initialize() -plugin.enable() - -# 播放简单触觉效果 -plugin.haptic_manager.play_effect( - effect_type='click', - intensity=0.8, - duration=0.1, - frequency=50.0 -) - -# 处理碰撞事件 -collision_data = { - 'impulse': 5.0, - 'contact_point': (0, 0, 0), - 'object_a': 'player', - 'object_b': 'wall', - 'material_a': 'flesh', - 'material_b': 'stone' -} -plugin.physics_integration.process_collision(collision_data) - -# 处理游戏事件 -plugin.event_handler.handle_event('player_take_damage', {'intensity': 0.9}) - -# 应用配置预设 -plugin.config_manager.apply_profile('gaming') -``` - -## API参考 - -详细的API文档请参考各模块的源代码注释。 - -## 配置说明 - -插件配置文件位于 `config/haptic_config.json`,支持以下配置项: - -- 全局设置:启用状态、强度缩放、频率缩放等 -- 设备设置:默认强度、最大并发效果数等 -- 效果设置:预处理、后处理、最大持续时间等 -- 物理设置:碰撞反馈、力反馈、摩擦反馈等 -- 音频设置:音频触觉反馈、节拍检测等 -- 事件设置:事件处理、事件抑制等 - -## 支持的设备 - -- Xbox/PlayStation 游戏手柄 -- Oculus Touch/Valve Index VR控制器 -- 触觉反馈手套 -- 震动背心和座椅 -- 力反馈方向盘 - -## 性能优化 - -- 使用对象池减少内存分配 -- 多线程处理避免阻塞主线程 -- 智能设备检测减少CPU占用 -- 效果缓存和复用机制 - -## 扩展开发 - -插件采用模块化设计,易于扩展新功能: - -1. 添加新的效果类型 -2. 支持新的设备类型 -3. 扩展物理反馈类型 -4. 添加新的音频映射算法 - -## 许可证 - -本插件遵循EG引擎许可证协议。 - -## 支持与反馈 - +# 触觉反馈系统插件 + +## 简介 + +触觉反馈系统插件为EG引擎提供全面的触觉反馈支持,包括设备管理、效果处理、物理集成、音频同步等功能。该插件支持多种触觉设备,如游戏手柄、VR控制器、触觉手套、震动背心等,并提供丰富的API供开发者集成到自己的应用中。 + +## 功能特性 + +### 1. 设备管理 +- 自动发现和管理多种触觉设备 +- 支持USB、蓝牙、无线等多种连接方式 +- 实时监控设备状态和电池电量 +- 支持热插拔设备检测 + +### 2. 效果系统 +- 预定义多种触觉效果(脉冲、震动、爆炸等) +- 支持自定义效果创建和编辑 +- 效果混合和层叠播放 +- 实时效果参数调整 + +### 3. 物理引擎集成 +- 碰撞反馈:根据碰撞冲量生成触觉效果 +- 力反馈:实时力反馈模拟 +- 摩擦反馈:基于表面材质的纹理反馈 +- 材质系统:不同材质对应不同触觉反馈 + +### 4. 音频同步 +- 音频特征分析和触觉映射 +- 节拍检测和同步反馈 +- 频谱分析和多频段反馈 +- 音量驱动的动态触觉效果 + +### 5. 事件系统 +- 游戏事件到触觉效果的映射 +- 事件优先级和抑制机制 +- 事件组合和复合反馈 +- 可自定义的事件映射规则 + +### 6. 配置管理 +- 全局配置和用户偏好设置 +- 多种预设配置文件 +- 实时配置更新和保存 +- 配置验证和错误处理 + +### 7. 编辑器支持 +- 可视化设备管理界面 +- 效果编辑和实时预览 +- 配置文件管理 +- 实时监控和调试工具 + +## 目录结构 + +``` +haptic_feedback_system/ +├── plugin.py # 插件主文件 +├── core/ +│ └── haptic_manager.py # 触觉管理器 +├── effects/ +│ └── effect_processor.py # 效果处理器 +├── devices/ +│ └── device_manager.py # 设备管理器 +├── physics/ +│ └── physics_integration.py# 物理引擎集成 +├── audio/ +│ └── audio_haptics.py # 音频触觉反馈 +├── events/ +│ └── event_handler.py # 事件处理器 +├── config/ +│ └── config_manager.py # 配置管理器 +├── editor/ +│ └── haptic_editor.py # 触觉编辑器 +└── utils/ + └── haptic_utils.py # 工具类 +``` + +## 核心模块 + +### 触觉管理器 (HapticManager) +负责管理触觉效果的播放和设备控制,提供效果播放、停止、暂停、恢复等功能。 + +### 效果处理器 (EffectProcessor) +处理各种触觉效果,包括预定义效果和自定义效果,支持效果混合和参数调整。 + +### 设备管理器 (HapticDeviceManager) +管理各种触觉设备,包括设备发现、连接、状态监控和控制。 + +### 物理引擎集成 (PhysicsIntegration) +将物理引擎中的碰撞、力和摩擦转换为触觉反馈。 + +### 音频触觉反馈 (AudioHaptics) +将音频特征转换为触觉效果,支持节拍检测和频谱分析。 + +### 事件处理器 (HapticEventHandler) +处理游戏事件并触发相应的触觉反馈。 + +### 配置管理器 (HapticConfigManager) +管理插件配置和用户偏好设置。 + +### 触觉编辑器 (HapticEditor) +提供可视化界面用于配置和测试触觉反馈效果。 + +### 工具类 (HapticUtils) +提供实用工具函数,包括性能监控、日志记录、数据转换等。 + +## 使用示例 + +```python +# 初始化插件 +plugin = HapticFeedbackSystemPlugin(engine) +plugin.initialize() +plugin.enable() + +# 播放简单触觉效果 +plugin.haptic_manager.play_effect( + effect_type='click', + intensity=0.8, + duration=0.1, + frequency=50.0 +) + +# 处理碰撞事件 +collision_data = { + 'impulse': 5.0, + 'contact_point': (0, 0, 0), + 'object_a': 'player', + 'object_b': 'wall', + 'material_a': 'flesh', + 'material_b': 'stone' +} +plugin.physics_integration.process_collision(collision_data) + +# 处理游戏事件 +plugin.event_handler.handle_event('player_take_damage', {'intensity': 0.9}) + +# 应用配置预设 +plugin.config_manager.apply_profile('gaming') +``` + +## API参考 + +详细的API文档请参考各模块的源代码注释。 + +## 配置说明 + +插件配置文件位于 `config/haptic_config.json`,支持以下配置项: + +- 全局设置:启用状态、强度缩放、频率缩放等 +- 设备设置:默认强度、最大并发效果数等 +- 效果设置:预处理、后处理、最大持续时间等 +- 物理设置:碰撞反馈、力反馈、摩擦反馈等 +- 音频设置:音频触觉反馈、节拍检测等 +- 事件设置:事件处理、事件抑制等 + +## 支持的设备 + +- Xbox/PlayStation 游戏手柄 +- Oculus Touch/Valve Index VR控制器 +- 触觉反馈手套 +- 震动背心和座椅 +- 力反馈方向盘 + +## 性能优化 + +- 使用对象池减少内存分配 +- 多线程处理避免阻塞主线程 +- 智能设备检测减少CPU占用 +- 效果缓存和复用机制 + +## 扩展开发 + +插件采用模块化设计,易于扩展新功能: + +1. 添加新的效果类型 +2. 支持新的设备类型 +3. 扩展物理反馈类型 +4. 添加新的音频映射算法 + +## 许可证 + +本插件遵循EG引擎许可证协议。 + +## 支持与反馈 + 如有问题或建议,请联系开发团队或提交GitHub issue。 \ No newline at end of file diff --git a/plugins/user/haptic_feedback_system/audio/audio_haptics.py b/plugins/user/haptic_feedback_system/audio/audio_haptics.py index be4fb1bb..42ca5e72 100644 --- a/plugins/user/haptic_feedback_system/audio/audio_haptics.py +++ b/plugins/user/haptic_feedback_system/audio/audio_haptics.py @@ -1,703 +1,703 @@ -""" -音频触觉反馈模块 -负责将音频特征转换为触觉效果 -""" - -import time -from typing import Dict, Any, List, Optional -import math - -class AudioHaptics: - """ - 音频触觉反馈 - 负责将音频特征转换为触觉效果 - """ - - def __init__(self, plugin): - """ - 初始化音频触觉反馈 - - Args: - plugin: 触觉反馈系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 音频分析配置 - self.audio_config = { - 'enable_audio_haptics': True, - 'analysis_rate': 30.0, # Hz - 'frequency_bands': 8, - 'min_frequency': 20.0, - 'max_frequency': 20000.0, - 'intensity_scale': 1.0, - 'frequency_scale': 1.0, - 'smoothing_factor': 0.3 - } - - # 频带映射配置 - self.band_mapping = { - 'sub_bass': {'range': (20, 60), 'haptic_freq': 20.0, 'intensity': 1.0}, - 'bass': {'range': (60, 250), 'haptic_freq': 30.0, 'intensity': 0.9}, - 'low_mids': {'range': (250, 500), 'haptic_freq': 40.0, 'intensity': 0.7}, - 'mids': {'range': (500, 2000), 'haptic_freq': 50.0, 'intensity': 0.5}, - 'high_mids': {'range': (2000, 4000), 'haptic_freq': 60.0, 'intensity': 0.3}, - 'presence': {'range': (4000, 6000), 'haptic_freq': 70.0, 'intensity': 0.2}, - 'brilliance': {'range': (6000, 12000), 'haptic_freq': 80.0, 'intensity': 0.1}, - 'air': {'range': (12000, 20000), 'haptic_freq': 90.0, 'intensity': 0.05} - } - - # 节拍检测配置 - self.beat_config = { - 'enable_beat_detection': True, - 'min_bpm': 60, - 'max_bpm': 180, - 'sensitivity': 0.7, - 'history_size': 30 - } - - # 音频特征配置 - self.feature_config = { - 'enable_volume_haptics': True, - 'enable_spectral_haptics': True, - 'enable_temporal_haptics': True, - 'enable_rhythm_haptics': True - } - - # 状态跟踪 - self.audio_state = { - 'last_analysis_time': 0.0, - 'current_features': {}, - 'beat_history': [], - 'intensity_history': [] - } - - # 设备映射 - self.device_mapping = { - 'low_frequencies': 'body', # 低频到身体设备 - 'mid_frequencies': 'hands', # 中频到手部设备 - 'high_frequencies': 'fingers' # 高频到手指设备 - } - - # 统计信息 - self.stats = { - 'audio_frames_analyzed': 0, - 'haptic_effects_generated': 0, - 'beats_detected': 0, - 'spectral_events': 0 - } - - # 回调函数 - self.audio_callbacks = { - 'audio_analyzed': [], - 'beat_detected': [], - 'feature_extracted': [], - 'haptic_effect_generated': [] - } - - print("✓ 音频触觉反馈已创建") - - def initialize(self) -> bool: - """ - 初始化音频触觉反馈 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 音频触觉反馈初始化完成") - return True - - except Exception as e: - print(f"✗ 音频触觉反馈初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用音频触觉反馈 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 音频触觉反馈未初始化") - return False - - self.enabled = True - print("✓ 音频触觉反馈已启用") - return True - - except Exception as e: - print(f"✗ 音频触觉反馈启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用音频触觉反馈""" - try: - self.enabled = False - print("✓ 音频触觉反馈已禁用") - - except Exception as e: - print(f"✗ 音频触觉反馈禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理音频触觉反馈资源""" - try: - self.disable() - self.audio_state.clear() - self.audio_callbacks.clear() - self.initialized = False - print("✓ 音频触觉反馈资源已清理") - - except Exception as e: - print(f"✗ 音频触觉反馈资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新音频触觉反馈状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 检查是否需要进行音频分析 - current_time = time.time() - analysis_interval = 1.0 / self.audio_config['analysis_rate'] - - if current_time - self.audio_state['last_analysis_time'] >= analysis_interval: - # 模拟音频分析 - self._analyze_audio_frame() - self.audio_state['last_analysis_time'] = current_time - - except Exception as e: - print(f"✗ 音频触觉反馈更新失败: {e}") - import traceback - traceback.print_exc() - - def _analyze_audio_frame(self): - """分析音频帧并生成触觉效果""" - try: - if not self.enabled or not self.audio_config['enable_audio_haptics']: - return - - # 模拟音频特征提取 - audio_features = self._extract_audio_features() - - # 更新当前特征 - self.audio_state['current_features'] = audio_features - - # 触发音频分析回调 - self._trigger_audio_callback('audio_analyzed', { - 'features': audio_features, - 'timestamp': time.time() - }) - - # 处理音频特征并生成触觉效果 - self._process_audio_features(audio_features) - - # 检测节拍 - if self.beat_config['enable_beat_detection']: - self._detect_beat(audio_features) - - self.stats['audio_frames_analyzed'] += 1 - - except Exception as e: - print(f"✗ 音频帧分析失败: {e}") - - def _extract_audio_features(self) -> Dict[str, Any]: - """ - 提取音频特征 - - Returns: - 音频特征字典 - """ - try: - # 模拟音频特征提取 - features = { - 'timestamp': time.time(), - 'rms': 0.5 + 0.5 * math.sin(time.time()), # 模拟音量 - 'spectral_centroid': 5000.0, # 模拟频谱质心 - 'spectral_rolloff': 10000.0, # 模拟频谱滚降点 - 'zero_crossing_rate': 0.1, # 模拟过零率 - 'temporal_flux': 0.3, # 模拟时间通量 - 'bpm': 120.0 # 模拟节拍 - } - - # 生成频带能量(模拟) - band_energies = [] - for i in range(self.audio_config['frequency_bands']): - energy = 0.3 + 0.7 * math.sin(time.time() + i * 0.5) - band_energies.append(max(0.0, min(1.0, energy))) - - features['band_energies'] = band_energies - - # 触发特征提取回调 - self._trigger_audio_callback('feature_extracted', { - 'features': features - }) - - return features - - except Exception as e: - print(f"✗ 音频特征提取失败: {e}") - return {} - - def _process_audio_features(self, features: Dict[str, Any]): - """ - 处理音频特征并生成触觉效果 - - Args: - features: 音频特征字典 - """ - try: - if not features: - return - - # 处理频带能量 - if self.feature_config['enable_spectral_haptics']: - self._process_spectral_features(features) - - # 处理音量 - if self.feature_config['enable_volume_haptics']: - self._process_volume_features(features) - - # 处理时间特征 - if self.feature_config['enable_temporal_haptics']: - self._process_temporal_features(features) - - except Exception as e: - print(f"✗ 音频特征处理失败: {e}") - - def _process_spectral_features(self, features: Dict[str, Any]): - """ - 处理频谱特征并生成触觉效果 - - Args: - features: 音频特征字典 - """ - try: - band_energies = features.get('band_energies', []) - - for i, energy in enumerate(band_energies): - if energy > 0.1: # 能量阈值 - # 获取频带信息 - band_names = list(self.band_mapping.keys()) - if i < len(band_names): - band_name = band_names[i] - band_info = self.band_mapping[band_name] - - # 计算触觉效果参数 - intensity = energy * band_info['intensity'] * self.audio_config['intensity_scale'] - frequency = band_info['haptic_freq'] * self.audio_config['frequency_scale'] - duration = 0.05 + energy * 0.1 # 基于能量的持续时间 - - # 确定目标设备 - device_type = self._map_frequency_to_device(band_info['haptic_freq']) - device_id = self._get_device_id_by_type(device_type) - - # 生成触觉效果 - self._generate_haptic_effect( - effect_type='periodic', - device_id=device_id, - intensity=intensity, - frequency=frequency, - duration=duration, - parameters={ - 'source': 'spectral', - 'band': band_name, - 'energy': energy - } - ) - - self.stats['spectral_events'] += 1 - - except Exception as e: - print(f"✗ 频谱特征处理失败: {e}") - - def _process_volume_features(self, features: Dict[str, Any]): - """ - 处理音量特征并生成触觉效果 - - Args: - features: 音频特征字典 - """ - try: - rms = features.get('rms', 0.0) - - if rms > 0.05: # 音量阈值 - # 计算触觉效果参数 - intensity = rms * self.audio_config['intensity_scale'] - frequency = 20.0 + rms * 30.0 # 基于音量的频率 - duration = 0.02 # 短持续时间以保持响应性 - - # 生成触觉效果 - self._generate_haptic_effect( - effect_type='periodic', - device_id=None, # 所有设备 - intensity=intensity, - frequency=frequency, - duration=duration, - parameters={ - 'source': 'volume', - 'rms': rms - } - ) - - except Exception as e: - print(f"✗ 音量特征处理失败: {e}") - - def _process_temporal_features(self, features: Dict[str, Any]): - """ - 处理时间特征并生成触觉效果 - - Args: - features: 音频特征字典 - """ - try: - temporal_flux = features.get('temporal_flux', 0.0) - - if temporal_flux > 0.2: # 变化阈值 - # 计算触觉效果参数 - intensity = temporal_flux * self.audio_config['intensity_scale'] - frequency = 40.0 + temporal_flux * 40.0 - duration = 0.03 + temporal_flux * 0.07 - - # 生成触觉效果 - self._generate_haptic_effect( - effect_type='noise', - device_id=None, - intensity=intensity, - frequency=frequency, - duration=duration, - parameters={ - 'source': 'temporal', - 'flux': temporal_flux - } - ) - - except Exception as e: - print(f"✗ 时间特征处理失败: {e}") - - def _detect_beat(self, features: Dict[str, Any]): - """ - 检测节拍并生成触觉效果 - - Args: - features: 音频特征字典 - """ - try: - if not self.beat_config['enable_beat_detection']: - return - - # 添加到节拍历史 - timestamp = features.get('timestamp', time.time()) - rms = features.get('rms', 0.0) - - self.audio_state['beat_history'].append({ - 'timestamp': timestamp, - 'energy': rms - }) - - # 保持历史记录大小 - if len(self.audio_state['beat_history']) > self.beat_config['history_size']: - self.audio_state['beat_history'].pop(0) - - # 检测节拍(简化实现) - if len(self.audio_state['beat_history']) > 2: - current_energy = self.audio_state['beat_history'][-1]['energy'] - previous_energy = self.audio_state['beat_history'][-2]['energy'] - - # 简单的能量峰值检测 - if current_energy > previous_energy * (1.0 + self.beat_config['sensitivity']): - # 生成节拍触觉效果 - self._generate_haptic_effect( - effect_type='impulse', - device_id=None, - intensity=current_energy * self.audio_config['intensity_scale'], - frequency=60.0, - duration=0.05, - parameters={ - 'source': 'beat', - 'energy': current_energy - } - ) - - # 触发节拍检测回调 - self._trigger_audio_callback('beat_detected', { - 'timestamp': timestamp, - 'energy': current_energy - }) - - self.stats['beats_detected'] += 1 - - except Exception as e: - print(f"✗ 节拍检测失败: {e}") - - def _map_frequency_to_device(self, frequency: float) -> str: - """ - 将频率映射到设备类型 - - Args: - frequency: 频率值 - - Returns: - 设备类型 - """ - try: - if frequency < 40.0: - return 'body' - elif frequency < 80.0: - return 'hands' - else: - return 'fingers' - except Exception as e: - print(f"✗ 频率到设备映射失败: {e}") - return 'hands' - - def _get_device_id_by_type(self, device_type: str) -> Optional[int]: - """ - 根据设备类型获取设备ID - - Args: - device_type: 设备类型 - - Returns: - 设备ID或None - """ - try: - if not self.plugin.device_manager: - return None - - # 查找匹配类型的设备 - devices = self.plugin.device_manager.get_devices_by_type(device_type) - if devices: - return devices[0]['id'] # 返回第一个匹配的设备 - - return None - except Exception as e: - print(f"✗ 设备ID获取失败: {e}") - return None - - def _generate_haptic_effect(self, effect_type: str, device_id: Optional[int], - intensity: float, frequency: float, duration: float, - parameters: Dict[str, Any]) -> bool: - """ - 生成触觉效果 - - Args: - effect_type: 效果类型 - device_id: 设备ID - intensity: 强度 - frequency: 频率 - duration: 持续时间 - parameters: 参数 - - Returns: - 是否生成成功 - """ - try: - if not self.plugin.haptic_manager: - return False - - # 限制参数范围 - intensity = max(0.0, min(1.0, intensity)) - frequency = max(1.0, min(100.0, frequency)) - duration = max(0.01, min(2.0, duration)) - - # 生成触觉效果 - effect_id = self.plugin.haptic_manager.play_effect( - effect_type=effect_type, - device_id=device_id, - intensity=intensity, - duration=duration, - frequency=frequency, - parameters=parameters - ) - - if effect_id >= 0: - # 触发效果生成回调 - self._trigger_audio_callback('haptic_effect_generated', { - 'effect_id': effect_id, - 'effect_type': effect_type, - 'intensity': intensity, - 'frequency': frequency, - 'duration': duration, - 'parameters': parameters - }) - - self.stats['haptic_effects_generated'] += 1 - return True - - return False - - except Exception as e: - print(f"✗ 触觉效果生成失败: {e}") - return False - - def _trigger_audio_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发音频回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.audio_callbacks: - for callback in self.audio_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 音频回调执行失败: {e}") - except Exception as e: - print(f"✗ 音频回调触发失败: {e}") - - def register_audio_callback(self, callback_type: str, callback: callable): - """ - 注册音频回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.audio_callbacks: - self.audio_callbacks[callback_type].append(callback) - print(f"✓ 音频回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 音频回调注册失败: {e}") - - def unregister_audio_callback(self, callback_type: str, callback: callable): - """ - 注销音频回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.audio_callbacks: - if callback in self.audio_callbacks[callback_type]: - self.audio_callbacks[callback_type].remove(callback) - print(f"✓ 音频回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 音频回调注销失败: {e}") - - def set_audio_config(self, config: Dict[str, Any]) -> bool: - """ - 设置音频配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.audio_config.update(config) - print(f"✓ 音频配置已更新: {self.audio_config}") - return True - except Exception as e: - print(f"✗ 音频配置设置失败: {e}") - return False - - def set_band_mapping(self, mapping: Dict[str, Dict[str, Any]]) -> bool: - """ - 设置频带映射 - - Args: - mapping: 映射字典 - - Returns: - 是否设置成功 - """ - try: - self.band_mapping.update(mapping) - print(f"✓ 频带映射已更新") - return True - except Exception as e: - print(f"✗ 频带映射设置失败: {e}") - return False - - def set_beat_config(self, config: Dict[str, Any]) -> bool: - """ - 设置节拍检测配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.beat_config.update(config) - print(f"✓ 节拍检测配置已更新: {self.beat_config}") - return True - except Exception as e: - print(f"✗ 节拍检测配置设置失败: {e}") - return False - - def get_audio_config(self) -> Dict[str, Any]: - """ - 获取音频配置 - - Returns: - 配置字典 - """ - return self.audio_config.copy() - - def get_band_mapping(self) -> Dict[str, Dict[str, Any]]: - """ - 获取频带映射 - - Returns: - 映射字典 - """ - return self.band_mapping.copy() - - def get_beat_config(self) -> Dict[str, Any]: - """ - 获取节拍检测配置 - - Returns: - 配置字典 - """ - return self.beat_config.copy() - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - try: - self.stats = { - 'audio_frames_analyzed': 0, - 'haptic_effects_generated': 0, - 'beats_detected': 0, - 'spectral_events': 0 - } - print("✓ 音频触觉反馈统计信息已重置") - except Exception as e: +""" +音频触觉反馈模块 +负责将音频特征转换为触觉效果 +""" + +import time +from typing import Dict, Any, List, Optional +import math + +class AudioHaptics: + """ + 音频触觉反馈 + 负责将音频特征转换为触觉效果 + """ + + def __init__(self, plugin): + """ + 初始化音频触觉反馈 + + Args: + plugin: 触觉反馈系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 音频分析配置 + self.audio_config = { + 'enable_audio_haptics': True, + 'analysis_rate': 30.0, # Hz + 'frequency_bands': 8, + 'min_frequency': 20.0, + 'max_frequency': 20000.0, + 'intensity_scale': 1.0, + 'frequency_scale': 1.0, + 'smoothing_factor': 0.3 + } + + # 频带映射配置 + self.band_mapping = { + 'sub_bass': {'range': (20, 60), 'haptic_freq': 20.0, 'intensity': 1.0}, + 'bass': {'range': (60, 250), 'haptic_freq': 30.0, 'intensity': 0.9}, + 'low_mids': {'range': (250, 500), 'haptic_freq': 40.0, 'intensity': 0.7}, + 'mids': {'range': (500, 2000), 'haptic_freq': 50.0, 'intensity': 0.5}, + 'high_mids': {'range': (2000, 4000), 'haptic_freq': 60.0, 'intensity': 0.3}, + 'presence': {'range': (4000, 6000), 'haptic_freq': 70.0, 'intensity': 0.2}, + 'brilliance': {'range': (6000, 12000), 'haptic_freq': 80.0, 'intensity': 0.1}, + 'air': {'range': (12000, 20000), 'haptic_freq': 90.0, 'intensity': 0.05} + } + + # 节拍检测配置 + self.beat_config = { + 'enable_beat_detection': True, + 'min_bpm': 60, + 'max_bpm': 180, + 'sensitivity': 0.7, + 'history_size': 30 + } + + # 音频特征配置 + self.feature_config = { + 'enable_volume_haptics': True, + 'enable_spectral_haptics': True, + 'enable_temporal_haptics': True, + 'enable_rhythm_haptics': True + } + + # 状态跟踪 + self.audio_state = { + 'last_analysis_time': 0.0, + 'current_features': {}, + 'beat_history': [], + 'intensity_history': [] + } + + # 设备映射 + self.device_mapping = { + 'low_frequencies': 'body', # 低频到身体设备 + 'mid_frequencies': 'hands', # 中频到手部设备 + 'high_frequencies': 'fingers' # 高频到手指设备 + } + + # 统计信息 + self.stats = { + 'audio_frames_analyzed': 0, + 'haptic_effects_generated': 0, + 'beats_detected': 0, + 'spectral_events': 0 + } + + # 回调函数 + self.audio_callbacks = { + 'audio_analyzed': [], + 'beat_detected': [], + 'feature_extracted': [], + 'haptic_effect_generated': [] + } + + print("✓ 音频触觉反馈已创建") + + def initialize(self) -> bool: + """ + 初始化音频触觉反馈 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 音频触觉反馈初始化完成") + return True + + except Exception as e: + print(f"✗ 音频触觉反馈初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用音频触觉反馈 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 音频触觉反馈未初始化") + return False + + self.enabled = True + print("✓ 音频触觉反馈已启用") + return True + + except Exception as e: + print(f"✗ 音频触觉反馈启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用音频触觉反馈""" + try: + self.enabled = False + print("✓ 音频触觉反馈已禁用") + + except Exception as e: + print(f"✗ 音频触觉反馈禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理音频触觉反馈资源""" + try: + self.disable() + self.audio_state.clear() + self.audio_callbacks.clear() + self.initialized = False + print("✓ 音频触觉反馈资源已清理") + + except Exception as e: + print(f"✗ 音频触觉反馈资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新音频触觉反馈状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 检查是否需要进行音频分析 + current_time = time.time() + analysis_interval = 1.0 / self.audio_config['analysis_rate'] + + if current_time - self.audio_state['last_analysis_time'] >= analysis_interval: + # 模拟音频分析 + self._analyze_audio_frame() + self.audio_state['last_analysis_time'] = current_time + + except Exception as e: + print(f"✗ 音频触觉反馈更新失败: {e}") + import traceback + traceback.print_exc() + + def _analyze_audio_frame(self): + """分析音频帧并生成触觉效果""" + try: + if not self.enabled or not self.audio_config['enable_audio_haptics']: + return + + # 模拟音频特征提取 + audio_features = self._extract_audio_features() + + # 更新当前特征 + self.audio_state['current_features'] = audio_features + + # 触发音频分析回调 + self._trigger_audio_callback('audio_analyzed', { + 'features': audio_features, + 'timestamp': time.time() + }) + + # 处理音频特征并生成触觉效果 + self._process_audio_features(audio_features) + + # 检测节拍 + if self.beat_config['enable_beat_detection']: + self._detect_beat(audio_features) + + self.stats['audio_frames_analyzed'] += 1 + + except Exception as e: + print(f"✗ 音频帧分析失败: {e}") + + def _extract_audio_features(self) -> Dict[str, Any]: + """ + 提取音频特征 + + Returns: + 音频特征字典 + """ + try: + # 模拟音频特征提取 + features = { + 'timestamp': time.time(), + 'rms': 0.5 + 0.5 * math.sin(time.time()), # 模拟音量 + 'spectral_centroid': 5000.0, # 模拟频谱质心 + 'spectral_rolloff': 10000.0, # 模拟频谱滚降点 + 'zero_crossing_rate': 0.1, # 模拟过零率 + 'temporal_flux': 0.3, # 模拟时间通量 + 'bpm': 120.0 # 模拟节拍 + } + + # 生成频带能量(模拟) + band_energies = [] + for i in range(self.audio_config['frequency_bands']): + energy = 0.3 + 0.7 * math.sin(time.time() + i * 0.5) + band_energies.append(max(0.0, min(1.0, energy))) + + features['band_energies'] = band_energies + + # 触发特征提取回调 + self._trigger_audio_callback('feature_extracted', { + 'features': features + }) + + return features + + except Exception as e: + print(f"✗ 音频特征提取失败: {e}") + return {} + + def _process_audio_features(self, features: Dict[str, Any]): + """ + 处理音频特征并生成触觉效果 + + Args: + features: 音频特征字典 + """ + try: + if not features: + return + + # 处理频带能量 + if self.feature_config['enable_spectral_haptics']: + self._process_spectral_features(features) + + # 处理音量 + if self.feature_config['enable_volume_haptics']: + self._process_volume_features(features) + + # 处理时间特征 + if self.feature_config['enable_temporal_haptics']: + self._process_temporal_features(features) + + except Exception as e: + print(f"✗ 音频特征处理失败: {e}") + + def _process_spectral_features(self, features: Dict[str, Any]): + """ + 处理频谱特征并生成触觉效果 + + Args: + features: 音频特征字典 + """ + try: + band_energies = features.get('band_energies', []) + + for i, energy in enumerate(band_energies): + if energy > 0.1: # 能量阈值 + # 获取频带信息 + band_names = list(self.band_mapping.keys()) + if i < len(band_names): + band_name = band_names[i] + band_info = self.band_mapping[band_name] + + # 计算触觉效果参数 + intensity = energy * band_info['intensity'] * self.audio_config['intensity_scale'] + frequency = band_info['haptic_freq'] * self.audio_config['frequency_scale'] + duration = 0.05 + energy * 0.1 # 基于能量的持续时间 + + # 确定目标设备 + device_type = self._map_frequency_to_device(band_info['haptic_freq']) + device_id = self._get_device_id_by_type(device_type) + + # 生成触觉效果 + self._generate_haptic_effect( + effect_type='periodic', + device_id=device_id, + intensity=intensity, + frequency=frequency, + duration=duration, + parameters={ + 'source': 'spectral', + 'band': band_name, + 'energy': energy + } + ) + + self.stats['spectral_events'] += 1 + + except Exception as e: + print(f"✗ 频谱特征处理失败: {e}") + + def _process_volume_features(self, features: Dict[str, Any]): + """ + 处理音量特征并生成触觉效果 + + Args: + features: 音频特征字典 + """ + try: + rms = features.get('rms', 0.0) + + if rms > 0.05: # 音量阈值 + # 计算触觉效果参数 + intensity = rms * self.audio_config['intensity_scale'] + frequency = 20.0 + rms * 30.0 # 基于音量的频率 + duration = 0.02 # 短持续时间以保持响应性 + + # 生成触觉效果 + self._generate_haptic_effect( + effect_type='periodic', + device_id=None, # 所有设备 + intensity=intensity, + frequency=frequency, + duration=duration, + parameters={ + 'source': 'volume', + 'rms': rms + } + ) + + except Exception as e: + print(f"✗ 音量特征处理失败: {e}") + + def _process_temporal_features(self, features: Dict[str, Any]): + """ + 处理时间特征并生成触觉效果 + + Args: + features: 音频特征字典 + """ + try: + temporal_flux = features.get('temporal_flux', 0.0) + + if temporal_flux > 0.2: # 变化阈值 + # 计算触觉效果参数 + intensity = temporal_flux * self.audio_config['intensity_scale'] + frequency = 40.0 + temporal_flux * 40.0 + duration = 0.03 + temporal_flux * 0.07 + + # 生成触觉效果 + self._generate_haptic_effect( + effect_type='noise', + device_id=None, + intensity=intensity, + frequency=frequency, + duration=duration, + parameters={ + 'source': 'temporal', + 'flux': temporal_flux + } + ) + + except Exception as e: + print(f"✗ 时间特征处理失败: {e}") + + def _detect_beat(self, features: Dict[str, Any]): + """ + 检测节拍并生成触觉效果 + + Args: + features: 音频特征字典 + """ + try: + if not self.beat_config['enable_beat_detection']: + return + + # 添加到节拍历史 + timestamp = features.get('timestamp', time.time()) + rms = features.get('rms', 0.0) + + self.audio_state['beat_history'].append({ + 'timestamp': timestamp, + 'energy': rms + }) + + # 保持历史记录大小 + if len(self.audio_state['beat_history']) > self.beat_config['history_size']: + self.audio_state['beat_history'].pop(0) + + # 检测节拍(简化实现) + if len(self.audio_state['beat_history']) > 2: + current_energy = self.audio_state['beat_history'][-1]['energy'] + previous_energy = self.audio_state['beat_history'][-2]['energy'] + + # 简单的能量峰值检测 + if current_energy > previous_energy * (1.0 + self.beat_config['sensitivity']): + # 生成节拍触觉效果 + self._generate_haptic_effect( + effect_type='impulse', + device_id=None, + intensity=current_energy * self.audio_config['intensity_scale'], + frequency=60.0, + duration=0.05, + parameters={ + 'source': 'beat', + 'energy': current_energy + } + ) + + # 触发节拍检测回调 + self._trigger_audio_callback('beat_detected', { + 'timestamp': timestamp, + 'energy': current_energy + }) + + self.stats['beats_detected'] += 1 + + except Exception as e: + print(f"✗ 节拍检测失败: {e}") + + def _map_frequency_to_device(self, frequency: float) -> str: + """ + 将频率映射到设备类型 + + Args: + frequency: 频率值 + + Returns: + 设备类型 + """ + try: + if frequency < 40.0: + return 'body' + elif frequency < 80.0: + return 'hands' + else: + return 'fingers' + except Exception as e: + print(f"✗ 频率到设备映射失败: {e}") + return 'hands' + + def _get_device_id_by_type(self, device_type: str) -> Optional[int]: + """ + 根据设备类型获取设备ID + + Args: + device_type: 设备类型 + + Returns: + 设备ID或None + """ + try: + if not self.plugin.device_manager: + return None + + # 查找匹配类型的设备 + devices = self.plugin.device_manager.get_devices_by_type(device_type) + if devices: + return devices[0]['id'] # 返回第一个匹配的设备 + + return None + except Exception as e: + print(f"✗ 设备ID获取失败: {e}") + return None + + def _generate_haptic_effect(self, effect_type: str, device_id: Optional[int], + intensity: float, frequency: float, duration: float, + parameters: Dict[str, Any]) -> bool: + """ + 生成触觉效果 + + Args: + effect_type: 效果类型 + device_id: 设备ID + intensity: 强度 + frequency: 频率 + duration: 持续时间 + parameters: 参数 + + Returns: + 是否生成成功 + """ + try: + if not self.plugin.haptic_manager: + return False + + # 限制参数范围 + intensity = max(0.0, min(1.0, intensity)) + frequency = max(1.0, min(100.0, frequency)) + duration = max(0.01, min(2.0, duration)) + + # 生成触觉效果 + effect_id = self.plugin.haptic_manager.play_effect( + effect_type=effect_type, + device_id=device_id, + intensity=intensity, + duration=duration, + frequency=frequency, + parameters=parameters + ) + + if effect_id >= 0: + # 触发效果生成回调 + self._trigger_audio_callback('haptic_effect_generated', { + 'effect_id': effect_id, + 'effect_type': effect_type, + 'intensity': intensity, + 'frequency': frequency, + 'duration': duration, + 'parameters': parameters + }) + + self.stats['haptic_effects_generated'] += 1 + return True + + return False + + except Exception as e: + print(f"✗ 触觉效果生成失败: {e}") + return False + + def _trigger_audio_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发音频回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.audio_callbacks: + for callback in self.audio_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 音频回调执行失败: {e}") + except Exception as e: + print(f"✗ 音频回调触发失败: {e}") + + def register_audio_callback(self, callback_type: str, callback: callable): + """ + 注册音频回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.audio_callbacks: + self.audio_callbacks[callback_type].append(callback) + print(f"✓ 音频回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 音频回调注册失败: {e}") + + def unregister_audio_callback(self, callback_type: str, callback: callable): + """ + 注销音频回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.audio_callbacks: + if callback in self.audio_callbacks[callback_type]: + self.audio_callbacks[callback_type].remove(callback) + print(f"✓ 音频回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 音频回调注销失败: {e}") + + def set_audio_config(self, config: Dict[str, Any]) -> bool: + """ + 设置音频配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.audio_config.update(config) + print(f"✓ 音频配置已更新: {self.audio_config}") + return True + except Exception as e: + print(f"✗ 音频配置设置失败: {e}") + return False + + def set_band_mapping(self, mapping: Dict[str, Dict[str, Any]]) -> bool: + """ + 设置频带映射 + + Args: + mapping: 映射字典 + + Returns: + 是否设置成功 + """ + try: + self.band_mapping.update(mapping) + print(f"✓ 频带映射已更新") + return True + except Exception as e: + print(f"✗ 频带映射设置失败: {e}") + return False + + def set_beat_config(self, config: Dict[str, Any]) -> bool: + """ + 设置节拍检测配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.beat_config.update(config) + print(f"✓ 节拍检测配置已更新: {self.beat_config}") + return True + except Exception as e: + print(f"✗ 节拍检测配置设置失败: {e}") + return False + + def get_audio_config(self) -> Dict[str, Any]: + """ + 获取音频配置 + + Returns: + 配置字典 + """ + return self.audio_config.copy() + + def get_band_mapping(self) -> Dict[str, Dict[str, Any]]: + """ + 获取频带映射 + + Returns: + 映射字典 + """ + return self.band_mapping.copy() + + def get_beat_config(self) -> Dict[str, Any]: + """ + 获取节拍检测配置 + + Returns: + 配置字典 + """ + return self.beat_config.copy() + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + try: + self.stats = { + 'audio_frames_analyzed': 0, + 'haptic_effects_generated': 0, + 'beats_detected': 0, + 'spectral_events': 0 + } + print("✓ 音频触觉反馈统计信息已重置") + except Exception as e: print(f"✗ 音频触觉反馈统计信息重置失败: {e}") \ No newline at end of file diff --git a/plugins/user/haptic_feedback_system/config/config_manager.py b/plugins/user/haptic_feedback_system/config/config_manager.py index f22a6bfe..6047759a 100644 --- a/plugins/user/haptic_feedback_system/config/config_manager.py +++ b/plugins/user/haptic_feedback_system/config/config_manager.py @@ -1,693 +1,693 @@ -""" -配置管理器模块 -负责管理触觉反馈系统的配置和用户偏好设置 -""" - -import time -import json -import os -from typing import Dict, Any, List, Optional -from pathlib import Path - -class HapticConfigManager: - """ - 触觉配置管理器 - 负责管理触觉反馈系统的配置和用户偏好设置 - """ - - def __init__(self, plugin): - """ - 初始化触觉配置管理器 - - Args: - plugin: 触觉反馈系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 配置文件路径 - self.config_path = "/home/hello/EG/plugins/user/haptic_feedback_system/config/haptic_config.json" - - # 默认配置 - self.default_config = { - 'global': { - 'enabled': True, - 'intensity_scale': 1.0, - 'frequency_scale': 1.0, - 'duration_scale': 1.0, - 'master_volume': 1.0, - 'enable_device_detection': True, - 'auto_enable_new_devices': True - }, - 'devices': { - 'default_intensity': 0.8, - 'default_frequency': 50.0, - 'max_concurrent_effects': 16, - 'crossfade_time': 0.1, - 'device_timeout': 30.0 - }, - 'effects': { - 'enable_preprocessing': True, - 'enable_postprocessing': True, - 'max_effect_duration': 10.0, - 'effect_smoothing': 0.1, - 'enable_custom_effects': True - }, - 'physics': { - 'enable_collision_feedback': True, - 'enable_force_feedback': True, - 'enable_friction_feedback': True, - 'collision_intensity_scale': 1.0, - 'force_intensity_scale': 1.0, - 'friction_intensity_scale': 1.0 - }, - 'audio': { - 'enable_audio_haptics': True, - 'enable_beat_detection': True, - 'audio_analysis_rate': 30.0, - 'frequency_bands': 8, - 'audio_intensity_scale': 1.0 - }, - 'events': { - 'enable_event_processing': True, - 'enable_event_suppression': True, - 'enable_event_combination': True, - 'event_queue_size': 100, - 'event_suppression_time': 0.1 - }, - 'profiles': { - 'current_profile': 'default', - 'profiles': { - 'default': { - 'name': 'Default', - 'description': 'Default haptic profile', - 'settings': {} - }, - 'cinematic': { - 'name': 'Cinematic', - 'description': 'Enhanced haptic feedback for cinematic experiences', - 'settings': { - 'intensity_scale': 1.2, - 'frequency_scale': 1.1 - } - }, - 'gaming': { - 'name': 'Gaming', - 'description': 'Optimized haptic feedback for gaming', - 'settings': { - 'intensity_scale': 1.0, - 'response_time': 0.01 - } - }, - 'subtle': { - 'name': 'Subtle', - 'description': 'Subtle haptic feedback for casual use', - 'settings': { - 'intensity_scale': 0.5, - 'frequency_scale': 0.8 - } - } - } - }, - 'user_preferences': { - 'enable_haptics': True, - 'preferred_devices': [], - 'blocked_devices': [], - 'custom_mappings': {}, - 'sensitivity': 1.0, - 'comfort_mode': False - } - } - - # 当前配置 - self.current_config = self.default_config.copy() - - # 配置变更监听器 - self.config_listeners = {} - - # 配置验证规则 - self.validation_rules = { - 'global.intensity_scale': (0.0, 2.0), - 'global.frequency_scale': (0.1, 5.0), - 'global.duration_scale': (0.1, 5.0), - 'global.master_volume': (0.0, 1.0), - 'devices.default_intensity': (0.0, 1.0), - 'devices.default_frequency': (1.0, 100.0), - 'devices.max_concurrent_effects': (1, 64), - 'devices.crossfade_time': (0.01, 1.0), - 'devices.device_timeout': (1.0, 300.0), - 'audio.audio_analysis_rate': (1.0, 120.0), - 'audio.frequency_bands': (1, 32) - } - - # 配置历史 - self.config_history = [] - self.max_history_size = 50 - - # 统计信息 - self.stats = { - 'config_loads': 0, - 'config_saves': 0, - 'config_changes': 0, - 'validation_errors': 0 - } - - print("✓ 触觉配置管理器已创建") - - def initialize(self) -> bool: - """ - 初始化触觉配置管理器 - - Returns: - 是否初始化成功 - """ - try: - # 创建配置目录 - config_dir = Path(self.config_path).parent - config_dir.mkdir(parents=True, exist_ok=True) - - # 加载配置 - self.load_config() - - self.initialized = True - print("✓ 触觉配置管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 触觉配置管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用触觉配置管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 触觉配置管理器未初始化") - return False - - self.enabled = True - print("✓ 触觉配置管理器已启用") - return True - - except Exception as e: - print(f"✗ 触觉配置管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用触觉配置管理器""" - try: - self.enabled = False - print("✓ 触觉配置管理器已禁用") - - except Exception as e: - print(f"✗ 触觉配置管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理触觉配置管理器资源""" - try: - self.disable() - self.config_listeners.clear() - self.config_history.clear() - self.initialized = False - print("✓ 触觉配置管理器资源已清理") - - except Exception as e: - print(f"✗ 触觉配置管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新触觉配置管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 定期保存配置(如果需要) - pass - - except Exception as e: - print(f"✗ 触觉配置管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def load_config(self) -> bool: - """ - 加载配置 - - Returns: - 是否加载成功 - """ - try: - if os.path.exists(self.config_path): - with open(self.config_path, 'r', encoding='utf-8') as f: - loaded_config = json.load(f) - - # 合并配置(保留默认配置结构) - self._merge_config(self.current_config, loaded_config) - print(f"✓ 配置已从 {self.config_path} 加载") - else: - print("⚠ 配置文件不存在,使用默认配置") - - self.stats['config_loads'] += 1 - return True - - except Exception as e: - print(f"✗ 配置加载失败: {e}") - return False - - def save_config(self) -> bool: - """ - 保存配置 - - Returns: - 是否保存成功 - """ - try: - with open(self.config_path, 'w', encoding='utf-8') as f: - json.dump(self.current_config, f, indent=2, ensure_ascii=False) - - self.stats['config_saves'] += 1 - print(f"✓ 配置已保存到 {self.config_path}") - return True - - except Exception as e: - print(f"✗ 配置保存失败: {e}") - return False - - def _merge_config(self, base_config: Dict[str, Any], new_config: Dict[str, Any]): - """ - 合并配置 - - Args: - base_config: 基础配置 - new_config: 新配置 - """ - try: - for key, value in new_config.items(): - if key in base_config and isinstance(base_config[key], dict) and isinstance(value, dict): - self._merge_config(base_config[key], value) - else: - base_config[key] = value - except Exception as e: - print(f"✗ 配置合并失败: {e}") - - def get_config_value(self, key_path: str, default_value: Any = None) -> Any: - """ - 获取配置值 - - Args: - key_path: 配置键路径(如 'global.intensity_scale') - default_value: 默认值 - - Returns: - 配置值 - """ - try: - keys = key_path.split('.') - config = self.current_config - - for key in keys: - if isinstance(config, dict) and key in config: - config = config[key] - else: - return default_value - - return config - except Exception as e: - print(f"✗ 配置值获取失败: {e}") - return default_value - - def set_config_value(self, key_path: str, value: Any) -> bool: - """ - 设置配置值 - - Args: - key_path: 配置键路径 - value: 配置值 - - Returns: - 是否设置成功 - """ - try: - # 验证配置值 - if not self._validate_config_value(key_path, value): - print(f"✗ 配置值验证失败: {key_path} = {value}") - self.stats['validation_errors'] += 1 - return False - - # 保存到历史记录 - old_value = self.get_config_value(key_path) - self._add_to_history(key_path, old_value, value) - - # 设置新值 - keys = key_path.split('.') - config = self.current_config - - for key in keys[:-1]: - if key not in config or not isinstance(config[key], dict): - config[key] = {} - config = config[key] - - config[keys[-1]] = value - - # 触发配置变更回调 - self._trigger_config_change(key_path, old_value, value) - - self.stats['config_changes'] += 1 - print(f"✓ 配置已更新: {key_path} = {value}") - return True - - except Exception as e: - print(f"✗ 配置值设置失败: {e}") - return False - - def _validate_config_value(self, key_path: str, value: Any) -> bool: - """ - 验证配置值 - - Args: - key_path: 配置键路径 - value: 配置值 - - Returns: - 是否验证通过 - """ - try: - if key_path in self.validation_rules: - rule = self.validation_rules[key_path] - if isinstance(rule, tuple) and len(rule) == 2: - min_val, max_val = rule - if isinstance(value, (int, float)): - return min_val <= value <= max_val - # 可以添加更多验证规则 - return True # 默认通过验证 - except Exception as e: - print(f"✗ 配置值验证异常: {e}") - return False - - def _add_to_history(self, key_path: str, old_value: Any, new_value: Any): - """ - 添加到配置历史 - - Args: - key_path: 配置键路径 - old_value: 旧值 - new_value: 新值 - """ - try: - history_entry = { - 'key_path': key_path, - 'old_value': old_value, - 'new_value': new_value, - 'timestamp': time.time() - } - - self.config_history.append(history_entry) - - # 保持历史记录大小 - if len(self.config_history) > self.max_history_size: - self.config_history.pop(0) - - except Exception as e: - print(f"✗ 配置历史记录失败: {e}") - - def reset_to_default(self) -> bool: - """ - 重置为默认配置 - - Returns: - 是否重置成功 - """ - try: - self.current_config = self.default_config.copy() - print("✓ 配置已重置为默认值") - return True - except Exception as e: - print(f"✗ 配置重置失败: {e}") - return False - - def get_current_config(self) -> Dict[str, Any]: - """ - 获取当前配置 - - Returns: - 当前配置字典 - """ - return self.current_config.copy() - - def get_default_config(self) -> Dict[str, Any]: - """ - 获取默认配置 - - Returns: - 默认配置字典 - """ - return self.default_config.copy() - - def export_config(self, file_path: str) -> bool: - """ - 导出配置到文件 - - Args: - file_path: 导出文件路径 - - Returns: - 是否导出成功 - """ - try: - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(self.current_config, f, indent=2, ensure_ascii=False) - - print(f"✓ 配置已导出到 {file_path}") - return True - except Exception as e: - print(f"✗ 配置导出失败: {e}") - return False - - def import_config(self, file_path: str) -> bool: - """ - 从文件导入配置 - - Args: - file_path: 导入文件路径 - - Returns: - 是否导入成功 - """ - try: - with open(file_path, 'r', encoding='utf-8') as f: - imported_config = json.load(f) - - self._merge_config(self.current_config, imported_config) - print(f"✓ 配置已从 {file_path} 导入") - return True - except Exception as e: - print(f"✗ 配置导入失败: {e}") - return False - - def apply_profile(self, profile_name: str) -> bool: - """ - 应用配置预设 - - Args: - profile_name: 预设名称 - - Returns: - 是否应用成功 - """ - try: - profiles = self.current_config.get('profiles', {}).get('profiles', {}) - if profile_name not in profiles: - print(f"✗ 预设不存在: {profile_name}") - return False - - profile = profiles[profile_name] - settings = profile.get('settings', {}) - - # 应用预设设置 - for key, value in settings.items(): - self.set_config_value(key, value) - - # 更新当前预设 - self.set_config_value('profiles.current_profile', profile_name) - - print(f"✓ 预设已应用: {profile_name}") - return True - except Exception as e: - print(f"✗ 预设应用失败: {e}") - return False - - def create_profile(self, profile_name: str, settings: Dict[str, Any], - name: str = None, description: str = None) -> bool: - """ - 创建配置预设 - - Args: - profile_name: 预设名称 - settings: 预设设置 - name: 显示名称 - description: 描述 - - Returns: - 是否创建成功 - """ - try: - profiles = self.current_config.setdefault('profiles', {}).setdefault('profiles', {}) - - profiles[profile_name] = { - 'name': name or profile_name, - 'description': description or f'Custom profile: {profile_name}', - 'settings': settings - } - - print(f"✓ 预设已创建: {profile_name}") - return True - except Exception as e: - print(f"✗ 预设创建失败: {e}") - return False - - def delete_profile(self, profile_name: str) -> bool: - """ - 删除配置预设 - - Args: - profile_name: 预设名称 - - Returns: - 是否删除成功 - """ - try: - profiles = self.current_config.get('profiles', {}).get('profiles', {}) - if profile_name in profiles: - del profiles[profile_name] - print(f"✓ 预设已删除: {profile_name}") - return True - else: - print(f"✗ 预设不存在: {profile_name}") - return False - except Exception as e: - print(f"✗ 预设删除失败: {e}") - return False - - def get_available_profiles(self) -> List[Dict[str, Any]]: - """ - 获取可用预设列表 - - Returns: - 预设列表 - """ - try: - profiles = self.current_config.get('profiles', {}).get('profiles', {}) - result = [] - - for name, profile in profiles.items(): - result.append({ - 'name': name, - 'display_name': profile.get('name', name), - 'description': profile.get('description', ''), - 'settings_count': len(profile.get('settings', {})) - }) - - return result - except Exception as e: - print(f"✗ 预设列表获取失败: {e}") - return [] - - def _trigger_config_change(self, key_path: str, old_value: Any, new_value: Any): - """ - 触发配置变更回调 - - Args: - key_path: 配置键路径 - old_value: 旧值 - new_value: 新值 - """ - try: - # 通知监听器 - if key_path in self.config_listeners: - for callback in self.config_listeners[key_path]: - try: - callback(key_path, old_value, new_value) - except Exception as e: - print(f"✗ 配置变更回调执行失败: {e}") - - # 通知全局监听器 - if '*' in self.config_listeners: - for callback in self.config_listeners['*']: - try: - callback(key_path, old_value, new_value) - except Exception as e: - print(f"✗ 全局配置变更回调执行失败: {e}") - except Exception as e: - print(f"✗ 配置变更回调触发失败: {e}") - - def register_config_listener(self, key_path: str, callback: callable): - """ - 注册配置变更监听器 - - Args: - key_path: 配置键路径('*' 表示监听所有变更) - callback: 回调函数 - """ - try: - if key_path not in self.config_listeners: - self.config_listeners[key_path] = [] - self.config_listeners[key_path].append(callback) - print(f"✓ 配置监听器已注册: {key_path}") - except Exception as e: - print(f"✗ 配置监听器注册失败: {e}") - - def unregister_config_listener(self, key_path: str, callback: callable): - """ - 注销配置变更监听器 - - Args: - key_path: 配置键路径 - callback: 回调函数 - """ - try: - if key_path in self.config_listeners: - if callback in self.config_listeners[key_path]: - self.config_listeners[key_path].remove(callback) - print(f"✓ 配置监听器已注销: {key_path}") - except Exception as e: - print(f"✗ 配置监听器注销失败: {e}") - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - try: - self.stats = { - 'config_loads': 0, - 'config_saves': 0, - 'config_changes': 0, - 'validation_errors': 0 - } - print("✓ 配置管理器统计信息已重置") - except Exception as e: - print(f"✗ 配置管理器统计信息重置失败: {e}") - +""" +配置管理器模块 +负责管理触觉反馈系统的配置和用户偏好设置 +""" + +import time +import json +import os +from typing import Dict, Any, List, Optional +from pathlib import Path + +class HapticConfigManager: + """ + 触觉配置管理器 + 负责管理触觉反馈系统的配置和用户偏好设置 + """ + + def __init__(self, plugin): + """ + 初始化触觉配置管理器 + + Args: + plugin: 触觉反馈系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 配置文件路径 + self.config_path = "/home/hello/EG/plugins/user/haptic_feedback_system/config/haptic_config.json" + + # 默认配置 + self.default_config = { + 'global': { + 'enabled': True, + 'intensity_scale': 1.0, + 'frequency_scale': 1.0, + 'duration_scale': 1.0, + 'master_volume': 1.0, + 'enable_device_detection': True, + 'auto_enable_new_devices': True + }, + 'devices': { + 'default_intensity': 0.8, + 'default_frequency': 50.0, + 'max_concurrent_effects': 16, + 'crossfade_time': 0.1, + 'device_timeout': 30.0 + }, + 'effects': { + 'enable_preprocessing': True, + 'enable_postprocessing': True, + 'max_effect_duration': 10.0, + 'effect_smoothing': 0.1, + 'enable_custom_effects': True + }, + 'physics': { + 'enable_collision_feedback': True, + 'enable_force_feedback': True, + 'enable_friction_feedback': True, + 'collision_intensity_scale': 1.0, + 'force_intensity_scale': 1.0, + 'friction_intensity_scale': 1.0 + }, + 'audio': { + 'enable_audio_haptics': True, + 'enable_beat_detection': True, + 'audio_analysis_rate': 30.0, + 'frequency_bands': 8, + 'audio_intensity_scale': 1.0 + }, + 'events': { + 'enable_event_processing': True, + 'enable_event_suppression': True, + 'enable_event_combination': True, + 'event_queue_size': 100, + 'event_suppression_time': 0.1 + }, + 'profiles': { + 'current_profile': 'default', + 'profiles': { + 'default': { + 'name': 'Default', + 'description': 'Default haptic profile', + 'settings': {} + }, + 'cinematic': { + 'name': 'Cinematic', + 'description': 'Enhanced haptic feedback for cinematic experiences', + 'settings': { + 'intensity_scale': 1.2, + 'frequency_scale': 1.1 + } + }, + 'gaming': { + 'name': 'Gaming', + 'description': 'Optimized haptic feedback for gaming', + 'settings': { + 'intensity_scale': 1.0, + 'response_time': 0.01 + } + }, + 'subtle': { + 'name': 'Subtle', + 'description': 'Subtle haptic feedback for casual use', + 'settings': { + 'intensity_scale': 0.5, + 'frequency_scale': 0.8 + } + } + } + }, + 'user_preferences': { + 'enable_haptics': True, + 'preferred_devices': [], + 'blocked_devices': [], + 'custom_mappings': {}, + 'sensitivity': 1.0, + 'comfort_mode': False + } + } + + # 当前配置 + self.current_config = self.default_config.copy() + + # 配置变更监听器 + self.config_listeners = {} + + # 配置验证规则 + self.validation_rules = { + 'global.intensity_scale': (0.0, 2.0), + 'global.frequency_scale': (0.1, 5.0), + 'global.duration_scale': (0.1, 5.0), + 'global.master_volume': (0.0, 1.0), + 'devices.default_intensity': (0.0, 1.0), + 'devices.default_frequency': (1.0, 100.0), + 'devices.max_concurrent_effects': (1, 64), + 'devices.crossfade_time': (0.01, 1.0), + 'devices.device_timeout': (1.0, 300.0), + 'audio.audio_analysis_rate': (1.0, 120.0), + 'audio.frequency_bands': (1, 32) + } + + # 配置历史 + self.config_history = [] + self.max_history_size = 50 + + # 统计信息 + self.stats = { + 'config_loads': 0, + 'config_saves': 0, + 'config_changes': 0, + 'validation_errors': 0 + } + + print("✓ 触觉配置管理器已创建") + + def initialize(self) -> bool: + """ + 初始化触觉配置管理器 + + Returns: + 是否初始化成功 + """ + try: + # 创建配置目录 + config_dir = Path(self.config_path).parent + config_dir.mkdir(parents=True, exist_ok=True) + + # 加载配置 + self.load_config() + + self.initialized = True + print("✓ 触觉配置管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 触觉配置管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用触觉配置管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 触觉配置管理器未初始化") + return False + + self.enabled = True + print("✓ 触觉配置管理器已启用") + return True + + except Exception as e: + print(f"✗ 触觉配置管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用触觉配置管理器""" + try: + self.enabled = False + print("✓ 触觉配置管理器已禁用") + + except Exception as e: + print(f"✗ 触觉配置管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理触觉配置管理器资源""" + try: + self.disable() + self.config_listeners.clear() + self.config_history.clear() + self.initialized = False + print("✓ 触觉配置管理器资源已清理") + + except Exception as e: + print(f"✗ 触觉配置管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新触觉配置管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 定期保存配置(如果需要) + pass + + except Exception as e: + print(f"✗ 触觉配置管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def load_config(self) -> bool: + """ + 加载配置 + + Returns: + 是否加载成功 + """ + try: + if os.path.exists(self.config_path): + with open(self.config_path, 'r', encoding='utf-8') as f: + loaded_config = json.load(f) + + # 合并配置(保留默认配置结构) + self._merge_config(self.current_config, loaded_config) + print(f"✓ 配置已从 {self.config_path} 加载") + else: + print("⚠ 配置文件不存在,使用默认配置") + + self.stats['config_loads'] += 1 + return True + + except Exception as e: + print(f"✗ 配置加载失败: {e}") + return False + + def save_config(self) -> bool: + """ + 保存配置 + + Returns: + 是否保存成功 + """ + try: + with open(self.config_path, 'w', encoding='utf-8') as f: + json.dump(self.current_config, f, indent=2, ensure_ascii=False) + + self.stats['config_saves'] += 1 + print(f"✓ 配置已保存到 {self.config_path}") + return True + + except Exception as e: + print(f"✗ 配置保存失败: {e}") + return False + + def _merge_config(self, base_config: Dict[str, Any], new_config: Dict[str, Any]): + """ + 合并配置 + + Args: + base_config: 基础配置 + new_config: 新配置 + """ + try: + for key, value in new_config.items(): + if key in base_config and isinstance(base_config[key], dict) and isinstance(value, dict): + self._merge_config(base_config[key], value) + else: + base_config[key] = value + except Exception as e: + print(f"✗ 配置合并失败: {e}") + + def get_config_value(self, key_path: str, default_value: Any = None) -> Any: + """ + 获取配置值 + + Args: + key_path: 配置键路径(如 'global.intensity_scale') + default_value: 默认值 + + Returns: + 配置值 + """ + try: + keys = key_path.split('.') + config = self.current_config + + for key in keys: + if isinstance(config, dict) and key in config: + config = config[key] + else: + return default_value + + return config + except Exception as e: + print(f"✗ 配置值获取失败: {e}") + return default_value + + def set_config_value(self, key_path: str, value: Any) -> bool: + """ + 设置配置值 + + Args: + key_path: 配置键路径 + value: 配置值 + + Returns: + 是否设置成功 + """ + try: + # 验证配置值 + if not self._validate_config_value(key_path, value): + print(f"✗ 配置值验证失败: {key_path} = {value}") + self.stats['validation_errors'] += 1 + return False + + # 保存到历史记录 + old_value = self.get_config_value(key_path) + self._add_to_history(key_path, old_value, value) + + # 设置新值 + keys = key_path.split('.') + config = self.current_config + + for key in keys[:-1]: + if key not in config or not isinstance(config[key], dict): + config[key] = {} + config = config[key] + + config[keys[-1]] = value + + # 触发配置变更回调 + self._trigger_config_change(key_path, old_value, value) + + self.stats['config_changes'] += 1 + print(f"✓ 配置已更新: {key_path} = {value}") + return True + + except Exception as e: + print(f"✗ 配置值设置失败: {e}") + return False + + def _validate_config_value(self, key_path: str, value: Any) -> bool: + """ + 验证配置值 + + Args: + key_path: 配置键路径 + value: 配置值 + + Returns: + 是否验证通过 + """ + try: + if key_path in self.validation_rules: + rule = self.validation_rules[key_path] + if isinstance(rule, tuple) and len(rule) == 2: + min_val, max_val = rule + if isinstance(value, (int, float)): + return min_val <= value <= max_val + # 可以添加更多验证规则 + return True # 默认通过验证 + except Exception as e: + print(f"✗ 配置值验证异常: {e}") + return False + + def _add_to_history(self, key_path: str, old_value: Any, new_value: Any): + """ + 添加到配置历史 + + Args: + key_path: 配置键路径 + old_value: 旧值 + new_value: 新值 + """ + try: + history_entry = { + 'key_path': key_path, + 'old_value': old_value, + 'new_value': new_value, + 'timestamp': time.time() + } + + self.config_history.append(history_entry) + + # 保持历史记录大小 + if len(self.config_history) > self.max_history_size: + self.config_history.pop(0) + + except Exception as e: + print(f"✗ 配置历史记录失败: {e}") + + def reset_to_default(self) -> bool: + """ + 重置为默认配置 + + Returns: + 是否重置成功 + """ + try: + self.current_config = self.default_config.copy() + print("✓ 配置已重置为默认值") + return True + except Exception as e: + print(f"✗ 配置重置失败: {e}") + return False + + def get_current_config(self) -> Dict[str, Any]: + """ + 获取当前配置 + + Returns: + 当前配置字典 + """ + return self.current_config.copy() + + def get_default_config(self) -> Dict[str, Any]: + """ + 获取默认配置 + + Returns: + 默认配置字典 + """ + return self.default_config.copy() + + def export_config(self, file_path: str) -> bool: + """ + 导出配置到文件 + + Args: + file_path: 导出文件路径 + + Returns: + 是否导出成功 + """ + try: + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(self.current_config, f, indent=2, ensure_ascii=False) + + print(f"✓ 配置已导出到 {file_path}") + return True + except Exception as e: + print(f"✗ 配置导出失败: {e}") + return False + + def import_config(self, file_path: str) -> bool: + """ + 从文件导入配置 + + Args: + file_path: 导入文件路径 + + Returns: + 是否导入成功 + """ + try: + with open(file_path, 'r', encoding='utf-8') as f: + imported_config = json.load(f) + + self._merge_config(self.current_config, imported_config) + print(f"✓ 配置已从 {file_path} 导入") + return True + except Exception as e: + print(f"✗ 配置导入失败: {e}") + return False + + def apply_profile(self, profile_name: str) -> bool: + """ + 应用配置预设 + + Args: + profile_name: 预设名称 + + Returns: + 是否应用成功 + """ + try: + profiles = self.current_config.get('profiles', {}).get('profiles', {}) + if profile_name not in profiles: + print(f"✗ 预设不存在: {profile_name}") + return False + + profile = profiles[profile_name] + settings = profile.get('settings', {}) + + # 应用预设设置 + for key, value in settings.items(): + self.set_config_value(key, value) + + # 更新当前预设 + self.set_config_value('profiles.current_profile', profile_name) + + print(f"✓ 预设已应用: {profile_name}") + return True + except Exception as e: + print(f"✗ 预设应用失败: {e}") + return False + + def create_profile(self, profile_name: str, settings: Dict[str, Any], + name: str = None, description: str = None) -> bool: + """ + 创建配置预设 + + Args: + profile_name: 预设名称 + settings: 预设设置 + name: 显示名称 + description: 描述 + + Returns: + 是否创建成功 + """ + try: + profiles = self.current_config.setdefault('profiles', {}).setdefault('profiles', {}) + + profiles[profile_name] = { + 'name': name or profile_name, + 'description': description or f'Custom profile: {profile_name}', + 'settings': settings + } + + print(f"✓ 预设已创建: {profile_name}") + return True + except Exception as e: + print(f"✗ 预设创建失败: {e}") + return False + + def delete_profile(self, profile_name: str) -> bool: + """ + 删除配置预设 + + Args: + profile_name: 预设名称 + + Returns: + 是否删除成功 + """ + try: + profiles = self.current_config.get('profiles', {}).get('profiles', {}) + if profile_name in profiles: + del profiles[profile_name] + print(f"✓ 预设已删除: {profile_name}") + return True + else: + print(f"✗ 预设不存在: {profile_name}") + return False + except Exception as e: + print(f"✗ 预设删除失败: {e}") + return False + + def get_available_profiles(self) -> List[Dict[str, Any]]: + """ + 获取可用预设列表 + + Returns: + 预设列表 + """ + try: + profiles = self.current_config.get('profiles', {}).get('profiles', {}) + result = [] + + for name, profile in profiles.items(): + result.append({ + 'name': name, + 'display_name': profile.get('name', name), + 'description': profile.get('description', ''), + 'settings_count': len(profile.get('settings', {})) + }) + + return result + except Exception as e: + print(f"✗ 预设列表获取失败: {e}") + return [] + + def _trigger_config_change(self, key_path: str, old_value: Any, new_value: Any): + """ + 触发配置变更回调 + + Args: + key_path: 配置键路径 + old_value: 旧值 + new_value: 新值 + """ + try: + # 通知监听器 + if key_path in self.config_listeners: + for callback in self.config_listeners[key_path]: + try: + callback(key_path, old_value, new_value) + except Exception as e: + print(f"✗ 配置变更回调执行失败: {e}") + + # 通知全局监听器 + if '*' in self.config_listeners: + for callback in self.config_listeners['*']: + try: + callback(key_path, old_value, new_value) + except Exception as e: + print(f"✗ 全局配置变更回调执行失败: {e}") + except Exception as e: + print(f"✗ 配置变更回调触发失败: {e}") + + def register_config_listener(self, key_path: str, callback: callable): + """ + 注册配置变更监听器 + + Args: + key_path: 配置键路径('*' 表示监听所有变更) + callback: 回调函数 + """ + try: + if key_path not in self.config_listeners: + self.config_listeners[key_path] = [] + self.config_listeners[key_path].append(callback) + print(f"✓ 配置监听器已注册: {key_path}") + except Exception as e: + print(f"✗ 配置监听器注册失败: {e}") + + def unregister_config_listener(self, key_path: str, callback: callable): + """ + 注销配置变更监听器 + + Args: + key_path: 配置键路径 + callback: 回调函数 + """ + try: + if key_path in self.config_listeners: + if callback in self.config_listeners[key_path]: + self.config_listeners[key_path].remove(callback) + print(f"✓ 配置监听器已注销: {key_path}") + except Exception as e: + print(f"✗ 配置监听器注销失败: {e}") + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + try: + self.stats = { + 'config_loads': 0, + 'config_saves': 0, + 'config_changes': 0, + 'validation_errors': 0 + } + print("✓ 配置管理器统计信息已重置") + except Exception as e: + print(f"✗ 配置管理器统计信息重置失败: {e}") + diff --git a/plugins/user/haptic_feedback_system/core/haptic_manager.py b/plugins/user/haptic_feedback_system/core/haptic_manager.py index 3d487280..da61f024 100644 --- a/plugins/user/haptic_feedback_system/core/haptic_manager.py +++ b/plugins/user/haptic_feedback_system/core/haptic_manager.py @@ -1,823 +1,823 @@ -""" -触觉管理器模块 -负责管理触觉效果的播放和设备控制 -""" - -import time -from typing import Dict, Any, List, Optional -import threading -import math - -class HapticManager: - """ - 触觉管理器 - 负责管理触觉效果的播放和设备控制 - """ - - def __init__(self, plugin): - """ - 初始化触觉管理器 - - Args: - plugin: 触觉反馈系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 触觉效果播放器 - self.effect_players = {} - self.player_counter = 0 - - # 活跃效果列表 - self.active_effects = {} - - # 设备控制 - self.device_controls = {} - - # 配置参数 - self.haptic_config = { - 'global_intensity': 1.0, - 'frequency_scale': 1.0, - 'duration_scale': 1.0, - 'crossfade_time': 0.1, - 'max_concurrent_effects': 16, - 'default_falloff': 0.1, - 'enable_spatial_haptics': True, - 'spatial_attenuation': 0.5, - 'enable_temperature_feedback': True, - 'temperature_sensitivity': 1.0 - } - - # 空间触觉参数 - self.spatial_haptics = { - 'listener_position': (0.0, 0.0, 0.0), - 'listener_orientation': (0.0, 0.0, 0.0, 1.0), # 四元数 - 'distance_model': 'inverse', - 'reference_distance': 1.0, - 'max_distance': 100.0, - 'rolloff_factor': 1.0 - } - - # 温度反馈参数 - self.temperature_feedback = { - 'min_temperature': 20.0, # 摄氏度 - 'max_temperature': 60.0, - 'current_temperature': 25.0, - 'heating_rate': 2.0, # 每秒升温速度 - 'cooling_rate': 1.0, # 每秒降温速度 - 'thermal_inertia': 0.5 # 热惯性系数 - } - - # 统计信息 - self.stats = { - 'effects_played': 0, - 'effects_stopped': 0, - 'effects_paused': 0, - 'effects_resumed': 0, - 'active_effects': 0, - 'spatial_effects': 0, - 'temperature_effects': 0 - } - - # 线程管理 - self.update_thread = None - self.running = False - - # 回调函数 - self.haptic_callbacks = { - 'effect_started': [], - 'effect_stopped': [], - 'effect_updated': [], - 'spatial_effect_processed': [], - 'temperature_effect_processed': [] - } - - # 效果池 - self.effect_pool = {} - - # 时间戳记录 - self.last_update_time = 0.0 - - print("✓ 触觉管理器已创建") - - def initialize(self) -> bool: - """ - 初始化触觉管理器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化效果池 - self._initialize_effect_pool() - - self.initialized = True - print("✓ 触觉管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 触觉管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用触觉管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 触觉管理器未初始化") - return False - - self.enabled = True - self.running = True - - # 启动更新线程 - self.update_thread = threading.Thread(target=self._update_loop, daemon=True) - self.update_thread.start() - - print("✓ 触觉管理器已启用") - return True - - except Exception as e: - print(f"✗ 触觉管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用触觉管理器""" - try: - self.enabled = False - self.running = False - - # 等待更新线程结束 - if self.update_thread and self.update_thread.is_alive(): - self.update_thread.join(timeout=1.0) - - # 停止所有效果 - self.stop_all_effects() - - print("✓ 触觉管理器已禁用") - - except Exception as e: - print(f"✗ 触觉管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理触觉管理器资源""" - try: - self.disable() - self.effect_players.clear() - self.active_effects.clear() - self.device_controls.clear() - self.haptic_callbacks.clear() - self.effect_pool.clear() - self.initialized = False - print("✓ 触觉管理器资源已清理") - - except Exception as e: - print(f"✗ 触觉管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新触觉管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 更新温度反馈 - self._update_temperature_feedback(dt) - - # 更新统计信息 - self.stats['active_effects'] = len(self.active_effects) - - self.last_update_time = current_time - - except Exception as e: - print(f"✗ 触觉管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _initialize_effect_pool(self): - """初始化效果池""" - try: - # 创建常用效果的预设实例以提高性能 - base_effects = [ - 'click', 'buzz', 'rumble', 'pulse', 'heartbeat', - 'explosion', 'impact', 'texture', 'wind', 'water' - ] - - for effect_name in base_effects: - self.effect_pool[effect_name] = [] - - print("✓ 效果池初始化完成") - except Exception as e: - print(f"✗ 效果池初始化失败: {e}") - - def _update_loop(self): - """更新循环线程""" - try: - update_interval = 1.0 / 120.0 # 120Hz - - while self.running: - if self.enabled: - self._update_active_effects() - - time.sleep(update_interval) - - except Exception as e: - print(f"✗ 触觉管理器更新循环失败: {e}") - - def _update_active_effects(self): - """更新活跃效果""" - try: - current_time = time.time() - finished_effects = [] - - # 更新所有活跃效果 - for effect_id, effect_data in list(self.active_effects.items()): - # 检查效果是否已完成 - if effect_data['start_time'] + effect_data['duration'] <= current_time: - finished_effects.append(effect_id) - else: - # 更新效果 - elapsed_time = current_time - effect_data['start_time'] - progress = elapsed_time / effect_data['duration'] - - # 应用效果到设备 - self._apply_effect_to_devices(effect_id, progress, effect_data) - - # 移除已完成的效果 - for effect_id in finished_effects: - self._finish_effect(effect_id) - - except Exception as e: - print(f"✗ 活跃效果更新失败: {e}") - - def _apply_effect_to_devices(self, effect_id: int, progress: float, effect_data: Dict[str, Any]): - """ - 应用效果到设备 - - Args: - effect_id: 效果ID - progress: 效果进度(0.0-1.0) - effect_data: 效果数据 - """ - try: - # 获取效果参数 - intensity = effect_data.get('intensity', 1.0) - frequency = effect_data.get('frequency', 1.0) - device_id = effect_data.get('device_id') - position = effect_data.get('position') - - # 应用全局配置 - intensity *= self.haptic_config['global_intensity'] - frequency *= self.haptic_config['frequency_scale'] - - # 处理空间效果 - if position and self.haptic_config['enable_spatial_haptics']: - intensity = self._calculate_spatial_intensity(position, intensity) - - # 计算渐变 - if progress < 0.1: # 淡入 - fade_factor = progress / 0.1 - intensity *= fade_factor - elif progress > 0.9: # 淡出 - fade_factor = (1.0 - progress) / 0.1 - intensity *= fade_factor - - # 处理温度反馈 - if self.haptic_config['enable_temperature_feedback']: - temperature_effect = self._calculate_temperature_effect(effect_data) - intensity *= temperature_effect - - # 发送到设备(实际实现中会调用具体设备API) - if self.plugin.device_manager: - self.plugin.device_manager.apply_haptic_effect( - device_id, intensity, frequency, progress) - - except Exception as e: - print(f"✗ 效果应用到设备失败: {e}") - - def _calculate_spatial_intensity(self, position: tuple, base_intensity: float) -> float: - """ - 计算空间强度衰减 - - Args: - position: 效果位置 - base_intensity: 基础强度 - - Returns: - 衰减后的强度 - """ - try: - # 计算距离 - listener_pos = self.spatial_haptics['listener_position'] - dx = position[0] - listener_pos[0] - dy = position[1] - listener_pos[1] - dz = position[2] - listener_pos[2] - distance = math.sqrt(dx*dx + dy*dy + dz*dz) - - # 根据距离模型计算衰减 - model = self.spatial_haptics['distance_model'] - ref_distance = self.spatial_haptics['reference_distance'] - max_distance = self.spatial_haptics['max_distance'] - rolloff = self.spatial_haptics['rolloff_factor'] - - if distance > max_distance: - return 0.0 - - if model == 'linear': - # 线性衰减 - attenuation = 1.0 - rolloff * (distance - ref_distance) / (max_distance - ref_distance) - elif model == 'inverse': - # 反比衰减 - attenuation = ref_distance / (ref_distance + rolloff * (distance - ref_distance)) - else: # exponential - # 指数衰减 - attenuation = math.pow(distance / ref_distance, -rolloff) - - # 应用空间衰减 - spatial_intensity = base_intensity * max(0.0, min(1.0, attenuation)) - - # 触发空间效果回调 - self._trigger_haptic_callback('spatial_effect_processed', { - 'position': position, - 'distance': distance, - 'attenuation': attenuation, - 'intensity': spatial_intensity - }) - - self.stats['spatial_effects'] += 1 - return spatial_intensity - - except Exception as e: - print(f"✗ 空间强度计算失败: {e}") - return base_intensity - - def _calculate_temperature_effect(self, effect_data: Dict[str, Any]) -> float: - """ - 计算温度效果 - - Args: - effect_data: 效果数据 - - Returns: - 温度系数 - """ - try: - # 获取效果温度参数 - effect_temperature = effect_data.get('temperature', 25.0) - current_temp = self.temperature_feedback['current_temperature'] - - # 计算温差 - temp_diff = abs(effect_temperature - current_temp) - sensitivity = self.haptic_config['temperature_sensitivity'] - - # 计算温度系数 (温差越大,触觉效果越强) - temp_factor = 1.0 + (temp_diff / 20.0) * sensitivity - temp_factor = max(0.5, min(2.0, temp_factor)) - - # 触发温度效果回调 - self._trigger_haptic_callback('temperature_effect_processed', { - 'effect_temperature': effect_temperature, - 'current_temperature': current_temp, - 'temperature_difference': temp_diff, - 'factor': temp_factor - }) - - self.stats['temperature_effects'] += 1 - return temp_factor - - except Exception as e: - print(f"✗ 温度效果计算失败: {e}") - return 1.0 - - def _update_temperature_feedback(self, dt: float): - """更新温度反馈""" - try: - temp_data = self.temperature_feedback - target_temp = temp_data.get('target_temperature', temp_data['current_temperature']) - - # 根据目标温度调整当前温度 - if temp_data['current_temperature'] < target_temp: - # 加热 - heating_rate = temp_data['heating_rate'] * temp_data['thermal_inertia'] - temp_data['current_temperature'] += heating_rate * dt - temp_data['current_temperature'] = min(temp_data['current_temperature'], target_temp) - elif temp_data['current_temperature'] > target_temp: - # 冷却 - cooling_rate = temp_data['cooling_rate'] * temp_data['thermal_inertia'] - temp_data['current_temperature'] -= cooling_rate * dt - temp_data['current_temperature'] = max(temp_data['current_temperature'], target_temp) - - except Exception as e: - print(f"✗ 温度反馈更新失败: {e}") - - def _finish_effect(self, effect_id: int): - """ - 完成效果播放 - - Args: - effect_id: 效果ID - """ - try: - if effect_id in self.active_effects: - effect_data = self.active_effects[effect_id] - - # 触发完成回调 - self._trigger_haptic_callback('effect_stopped', { - 'effect_id': effect_id, - 'effect_data': effect_data - }) - - # 移除效果 - del self.active_effects[effect_id] - self.stats['effects_stopped'] += 1 - - print(f"✓ 触觉效果已完成: ID {effect_id}") - - except Exception as e: - print(f"✗ 效果完成处理失败: {e}") - - def play_effect(self, effect_type: str, device_id: int = None, - intensity: float = 1.0, duration: float = 1.0, - frequency: float = 1.0, parameters: Dict[str, Any] = None) -> int: - """ - 播放触觉效果 - - Args: - effect_type: 效果类型 - device_id: 设备ID(None表示所有设备) - intensity: 强度(0.0-1.0) - duration: 持续时间(秒) - frequency: 频率 - parameters: 额外参数 - - Returns: - 效果ID - """ - try: - if not self.enabled: - print("✗ 触觉管理器未启用") - return -1 - - # 检查并发限制 - if len(self.active_effects) >= self.haptic_config['max_concurrent_effects']: - print("✗ 达到最大并发效果数") - return -1 - - # 限制参数范围 - intensity = max(0.0, min(1.0, intensity)) - duration = max(0.01, min(10.0, duration)) - frequency = max(0.1, min(1000.0, frequency)) - - # 创建效果数据 - effect_id = int(time.time() * 1000000) % 1000000 # 简单ID生成 - - effect_data = { - 'id': effect_id, - 'type': effect_type, - 'device_id': device_id, - 'intensity': intensity, - 'duration': duration * self.haptic_config['duration_scale'], - 'frequency': frequency * self.haptic_config['frequency_scale'], - 'parameters': parameters or {}, - 'start_time': time.time(), - 'status': 'playing', - 'position': parameters.get('position') if parameters else None, - 'temperature': parameters.get('temperature', 25.0) if parameters else 25.0 - } - - # 添加到活跃效果列表 - self.active_effects[effect_id] = effect_data - - # 触发开始回调 - self._trigger_haptic_callback('effect_started', { - 'effect_id': effect_id, - 'effect_data': effect_data - }) - - self.stats['effects_played'] += 1 - print(f"✓ 触觉效果已播放: {effect_type} (ID: {effect_id})") - return effect_id - - except Exception as e: - print(f"✗ 触觉效果播放失败: {e}") - return -1 - - def stop_effect(self, effect_id: int) -> bool: - """ - 停止触觉效果 - - Args: - effect_id: 效果ID - - Returns: - 是否停止成功 - """ - try: - if effect_id in self.active_effects: - effect_data = self.active_effects[effect_id] - effect_data['status'] = 'stopped' - - # 触发停止回调 - self._trigger_haptic_callback('effect_stopped', { - 'effect_id': effect_id, - 'effect_data': effect_data - }) - - # 移除效果 - del self.active_effects[effect_id] - self.stats['effects_stopped'] += 1 - - print(f"✓ 触觉效果已停止: ID {effect_id}") - return True - else: - print(f"✗ 触觉效果不存在: ID {effect_id}") - return False - - except Exception as e: - print(f"✗ 触觉效果停止失败: {e}") - return False - - def stop_all_effects(self) -> bool: - """ - 停止所有触觉效果 - - Returns: - 是否停止成功 - """ - try: - effect_ids = list(self.active_effects.keys()) - for effect_id in effect_ids: - self.stop_effect(effect_id) - - print("✓ 所有触觉效果已停止") - return True - - except Exception as e: - print(f"✗ 停止所有触觉效果失败: {e}") - return False - - def pause_effect(self, effect_id: int) -> bool: - """ - 暂停触觉效果 - - Args: - effect_id: 效果ID - - Returns: - 是否暂停成功 - """ - try: - if effect_id in self.active_effects: - self.active_effects[effect_id]['status'] = 'paused' - self.stats['effects_paused'] += 1 - print(f"✓ 触觉效果已暂停: ID {effect_id}") - return True - else: - print(f"✗ 触觉效果不存在: ID {effect_id}") - return False - - except Exception as e: - print(f"✗ 触觉效果暂停失败: {e}") - return False - - def resume_effect(self, effect_id: int) -> bool: - """ - 恢复触觉效果 - - Args: - effect_id: 效果ID - - Returns: - 是否恢复成功 - """ - try: - if effect_id in self.active_effects: - self.active_effects[effect_id]['status'] = 'playing' - self.stats['effects_resumed'] += 1 - print(f"✓ 触觉效果已恢复: ID {effect_id}") - return True - else: - print(f"✗ 触觉效果不存在: ID {effect_id}") - return False - - except Exception as e: - print(f"✗ 触觉效果恢复失败: {e}") - return False - - def get_effect_status(self, effect_id: int) -> Optional[str]: - """ - 获取效果状态 - - Args: - effect_id: 效果ID - - Returns: - 效果状态或None - """ - try: - if effect_id in self.active_effects: - return self.active_effects[effect_id]['status'] - return None - except Exception as e: - print(f"✗ 效果状态获取失败: {e}") - return None - - def set_haptic_config(self, config: Dict[str, Any]) -> bool: - """ - 设置触觉配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.haptic_config.update(config) - print(f"✓ 触觉配置已更新: {self.haptic_config}") - return True - except Exception as e: - print(f"✗ 触觉配置设置失败: {e}") - return False - - def get_haptic_config(self) -> Dict[str, Any]: - """ - 获取触觉配置 - - Returns: - 配置字典 - """ - return self.haptic_config.copy() - - def set_spatial_parameters(self, params: Dict[str, Any]) -> bool: - """ - 设置空间参数 - - Args: - params: 空间参数字典 - - Returns: - 是否设置成功 - """ - try: - self.spatial_haptics.update(params) - print(f"✓ 空间参数已更新: {self.spatial_haptics}") - return True - except Exception as e: - print(f"✗ 空间参数设置失败: {e}") - return False - - def get_spatial_parameters(self) -> Dict[str, Any]: - """ - 获取空间参数 - - Returns: - 空间参数字典 - """ - return self.spatial_haptics.copy() - - def set_temperature_parameters(self, params: Dict[str, Any]) -> bool: - """ - 设置温度参数 - - Args: - params: 温度参数字典 - - Returns: - 是否设置成功 - """ - try: - self.temperature_feedback.update(params) - print(f"✓ 温度参数已更新: {self.temperature_feedback}") - return True - except Exception as e: - print(f"✗ 温度参数设置失败: {e}") - return False - - def get_temperature_parameters(self) -> Dict[str, Any]: - """ - 获取温度参数 - - Returns: - 温度参数字典 - """ - return self.temperature_feedback.copy() - - def set_listener_position(self, position: tuple): - """ - 设置监听器位置 - - Args: - position: 位置坐标 (x, y, z) - """ - try: - self.spatial_haptics['listener_position'] = position - except Exception as e: - print(f"✗ 监听器位置设置失败: {e}") - - def set_listener_orientation(self, orientation: tuple): - """ - 设置监听器方向 - - Args: - orientation: 方向四元数 (x, y, z, w) - """ - try: - self.spatial_haptics['listener_orientation'] = orientation - except Exception as e: - print(f"✗ 监听器方向设置失败: {e}") - - def _trigger_haptic_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发触觉回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.haptic_callbacks: - for callback in self.haptic_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 触觉回调执行失败: {e}") - except Exception as e: - print(f"✗ 触觉回调触发失败: {e}") - - def register_haptic_callback(self, callback_type: str, callback: callable): - """ - 注册触觉回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.haptic_callbacks: - self.haptic_callbacks[callback_type].append(callback) - print(f"✓ 触觉回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 触觉回调注册失败: {e}") - - def unregister_haptic_callback(self, callback_type: str, callback: callable): - """ - 注销触觉回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.haptic_callbacks: - if callback in self.haptic_callbacks[callback_type]: - self.haptic_callbacks[callback_type].remove(callback) - print(f"✓ 触觉回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 触觉回调注销失败: {e}") - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - try: - self.stats = { - 'effects_played': 0, - 'effects_stopped': 0, - 'effects_paused': 0, - 'effects_resumed': 0, - 'active_effects': 0, - 'spatial_effects': 0, - 'temperature_effects': 0 - } - print("✓ 触觉管理器统计信息已重置") - except Exception as e: +""" +触觉管理器模块 +负责管理触觉效果的播放和设备控制 +""" + +import time +from typing import Dict, Any, List, Optional +import threading +import math + +class HapticManager: + """ + 触觉管理器 + 负责管理触觉效果的播放和设备控制 + """ + + def __init__(self, plugin): + """ + 初始化触觉管理器 + + Args: + plugin: 触觉反馈系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 触觉效果播放器 + self.effect_players = {} + self.player_counter = 0 + + # 活跃效果列表 + self.active_effects = {} + + # 设备控制 + self.device_controls = {} + + # 配置参数 + self.haptic_config = { + 'global_intensity': 1.0, + 'frequency_scale': 1.0, + 'duration_scale': 1.0, + 'crossfade_time': 0.1, + 'max_concurrent_effects': 16, + 'default_falloff': 0.1, + 'enable_spatial_haptics': True, + 'spatial_attenuation': 0.5, + 'enable_temperature_feedback': True, + 'temperature_sensitivity': 1.0 + } + + # 空间触觉参数 + self.spatial_haptics = { + 'listener_position': (0.0, 0.0, 0.0), + 'listener_orientation': (0.0, 0.0, 0.0, 1.0), # 四元数 + 'distance_model': 'inverse', + 'reference_distance': 1.0, + 'max_distance': 100.0, + 'rolloff_factor': 1.0 + } + + # 温度反馈参数 + self.temperature_feedback = { + 'min_temperature': 20.0, # 摄氏度 + 'max_temperature': 60.0, + 'current_temperature': 25.0, + 'heating_rate': 2.0, # 每秒升温速度 + 'cooling_rate': 1.0, # 每秒降温速度 + 'thermal_inertia': 0.5 # 热惯性系数 + } + + # 统计信息 + self.stats = { + 'effects_played': 0, + 'effects_stopped': 0, + 'effects_paused': 0, + 'effects_resumed': 0, + 'active_effects': 0, + 'spatial_effects': 0, + 'temperature_effects': 0 + } + + # 线程管理 + self.update_thread = None + self.running = False + + # 回调函数 + self.haptic_callbacks = { + 'effect_started': [], + 'effect_stopped': [], + 'effect_updated': [], + 'spatial_effect_processed': [], + 'temperature_effect_processed': [] + } + + # 效果池 + self.effect_pool = {} + + # 时间戳记录 + self.last_update_time = 0.0 + + print("✓ 触觉管理器已创建") + + def initialize(self) -> bool: + """ + 初始化触觉管理器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化效果池 + self._initialize_effect_pool() + + self.initialized = True + print("✓ 触觉管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 触觉管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用触觉管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 触觉管理器未初始化") + return False + + self.enabled = True + self.running = True + + # 启动更新线程 + self.update_thread = threading.Thread(target=self._update_loop, daemon=True) + self.update_thread.start() + + print("✓ 触觉管理器已启用") + return True + + except Exception as e: + print(f"✗ 触觉管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用触觉管理器""" + try: + self.enabled = False + self.running = False + + # 等待更新线程结束 + if self.update_thread and self.update_thread.is_alive(): + self.update_thread.join(timeout=1.0) + + # 停止所有效果 + self.stop_all_effects() + + print("✓ 触觉管理器已禁用") + + except Exception as e: + print(f"✗ 触觉管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理触觉管理器资源""" + try: + self.disable() + self.effect_players.clear() + self.active_effects.clear() + self.device_controls.clear() + self.haptic_callbacks.clear() + self.effect_pool.clear() + self.initialized = False + print("✓ 触觉管理器资源已清理") + + except Exception as e: + print(f"✗ 触觉管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新触觉管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 更新温度反馈 + self._update_temperature_feedback(dt) + + # 更新统计信息 + self.stats['active_effects'] = len(self.active_effects) + + self.last_update_time = current_time + + except Exception as e: + print(f"✗ 触觉管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _initialize_effect_pool(self): + """初始化效果池""" + try: + # 创建常用效果的预设实例以提高性能 + base_effects = [ + 'click', 'buzz', 'rumble', 'pulse', 'heartbeat', + 'explosion', 'impact', 'texture', 'wind', 'water' + ] + + for effect_name in base_effects: + self.effect_pool[effect_name] = [] + + print("✓ 效果池初始化完成") + except Exception as e: + print(f"✗ 效果池初始化失败: {e}") + + def _update_loop(self): + """更新循环线程""" + try: + update_interval = 1.0 / 120.0 # 120Hz + + while self.running: + if self.enabled: + self._update_active_effects() + + time.sleep(update_interval) + + except Exception as e: + print(f"✗ 触觉管理器更新循环失败: {e}") + + def _update_active_effects(self): + """更新活跃效果""" + try: + current_time = time.time() + finished_effects = [] + + # 更新所有活跃效果 + for effect_id, effect_data in list(self.active_effects.items()): + # 检查效果是否已完成 + if effect_data['start_time'] + effect_data['duration'] <= current_time: + finished_effects.append(effect_id) + else: + # 更新效果 + elapsed_time = current_time - effect_data['start_time'] + progress = elapsed_time / effect_data['duration'] + + # 应用效果到设备 + self._apply_effect_to_devices(effect_id, progress, effect_data) + + # 移除已完成的效果 + for effect_id in finished_effects: + self._finish_effect(effect_id) + + except Exception as e: + print(f"✗ 活跃效果更新失败: {e}") + + def _apply_effect_to_devices(self, effect_id: int, progress: float, effect_data: Dict[str, Any]): + """ + 应用效果到设备 + + Args: + effect_id: 效果ID + progress: 效果进度(0.0-1.0) + effect_data: 效果数据 + """ + try: + # 获取效果参数 + intensity = effect_data.get('intensity', 1.0) + frequency = effect_data.get('frequency', 1.0) + device_id = effect_data.get('device_id') + position = effect_data.get('position') + + # 应用全局配置 + intensity *= self.haptic_config['global_intensity'] + frequency *= self.haptic_config['frequency_scale'] + + # 处理空间效果 + if position and self.haptic_config['enable_spatial_haptics']: + intensity = self._calculate_spatial_intensity(position, intensity) + + # 计算渐变 + if progress < 0.1: # 淡入 + fade_factor = progress / 0.1 + intensity *= fade_factor + elif progress > 0.9: # 淡出 + fade_factor = (1.0 - progress) / 0.1 + intensity *= fade_factor + + # 处理温度反馈 + if self.haptic_config['enable_temperature_feedback']: + temperature_effect = self._calculate_temperature_effect(effect_data) + intensity *= temperature_effect + + # 发送到设备(实际实现中会调用具体设备API) + if self.plugin.device_manager: + self.plugin.device_manager.apply_haptic_effect( + device_id, intensity, frequency, progress) + + except Exception as e: + print(f"✗ 效果应用到设备失败: {e}") + + def _calculate_spatial_intensity(self, position: tuple, base_intensity: float) -> float: + """ + 计算空间强度衰减 + + Args: + position: 效果位置 + base_intensity: 基础强度 + + Returns: + 衰减后的强度 + """ + try: + # 计算距离 + listener_pos = self.spatial_haptics['listener_position'] + dx = position[0] - listener_pos[0] + dy = position[1] - listener_pos[1] + dz = position[2] - listener_pos[2] + distance = math.sqrt(dx*dx + dy*dy + dz*dz) + + # 根据距离模型计算衰减 + model = self.spatial_haptics['distance_model'] + ref_distance = self.spatial_haptics['reference_distance'] + max_distance = self.spatial_haptics['max_distance'] + rolloff = self.spatial_haptics['rolloff_factor'] + + if distance > max_distance: + return 0.0 + + if model == 'linear': + # 线性衰减 + attenuation = 1.0 - rolloff * (distance - ref_distance) / (max_distance - ref_distance) + elif model == 'inverse': + # 反比衰减 + attenuation = ref_distance / (ref_distance + rolloff * (distance - ref_distance)) + else: # exponential + # 指数衰减 + attenuation = math.pow(distance / ref_distance, -rolloff) + + # 应用空间衰减 + spatial_intensity = base_intensity * max(0.0, min(1.0, attenuation)) + + # 触发空间效果回调 + self._trigger_haptic_callback('spatial_effect_processed', { + 'position': position, + 'distance': distance, + 'attenuation': attenuation, + 'intensity': spatial_intensity + }) + + self.stats['spatial_effects'] += 1 + return spatial_intensity + + except Exception as e: + print(f"✗ 空间强度计算失败: {e}") + return base_intensity + + def _calculate_temperature_effect(self, effect_data: Dict[str, Any]) -> float: + """ + 计算温度效果 + + Args: + effect_data: 效果数据 + + Returns: + 温度系数 + """ + try: + # 获取效果温度参数 + effect_temperature = effect_data.get('temperature', 25.0) + current_temp = self.temperature_feedback['current_temperature'] + + # 计算温差 + temp_diff = abs(effect_temperature - current_temp) + sensitivity = self.haptic_config['temperature_sensitivity'] + + # 计算温度系数 (温差越大,触觉效果越强) + temp_factor = 1.0 + (temp_diff / 20.0) * sensitivity + temp_factor = max(0.5, min(2.0, temp_factor)) + + # 触发温度效果回调 + self._trigger_haptic_callback('temperature_effect_processed', { + 'effect_temperature': effect_temperature, + 'current_temperature': current_temp, + 'temperature_difference': temp_diff, + 'factor': temp_factor + }) + + self.stats['temperature_effects'] += 1 + return temp_factor + + except Exception as e: + print(f"✗ 温度效果计算失败: {e}") + return 1.0 + + def _update_temperature_feedback(self, dt: float): + """更新温度反馈""" + try: + temp_data = self.temperature_feedback + target_temp = temp_data.get('target_temperature', temp_data['current_temperature']) + + # 根据目标温度调整当前温度 + if temp_data['current_temperature'] < target_temp: + # 加热 + heating_rate = temp_data['heating_rate'] * temp_data['thermal_inertia'] + temp_data['current_temperature'] += heating_rate * dt + temp_data['current_temperature'] = min(temp_data['current_temperature'], target_temp) + elif temp_data['current_temperature'] > target_temp: + # 冷却 + cooling_rate = temp_data['cooling_rate'] * temp_data['thermal_inertia'] + temp_data['current_temperature'] -= cooling_rate * dt + temp_data['current_temperature'] = max(temp_data['current_temperature'], target_temp) + + except Exception as e: + print(f"✗ 温度反馈更新失败: {e}") + + def _finish_effect(self, effect_id: int): + """ + 完成效果播放 + + Args: + effect_id: 效果ID + """ + try: + if effect_id in self.active_effects: + effect_data = self.active_effects[effect_id] + + # 触发完成回调 + self._trigger_haptic_callback('effect_stopped', { + 'effect_id': effect_id, + 'effect_data': effect_data + }) + + # 移除效果 + del self.active_effects[effect_id] + self.stats['effects_stopped'] += 1 + + print(f"✓ 触觉效果已完成: ID {effect_id}") + + except Exception as e: + print(f"✗ 效果完成处理失败: {e}") + + def play_effect(self, effect_type: str, device_id: int = None, + intensity: float = 1.0, duration: float = 1.0, + frequency: float = 1.0, parameters: Dict[str, Any] = None) -> int: + """ + 播放触觉效果 + + Args: + effect_type: 效果类型 + device_id: 设备ID(None表示所有设备) + intensity: 强度(0.0-1.0) + duration: 持续时间(秒) + frequency: 频率 + parameters: 额外参数 + + Returns: + 效果ID + """ + try: + if not self.enabled: + print("✗ 触觉管理器未启用") + return -1 + + # 检查并发限制 + if len(self.active_effects) >= self.haptic_config['max_concurrent_effects']: + print("✗ 达到最大并发效果数") + return -1 + + # 限制参数范围 + intensity = max(0.0, min(1.0, intensity)) + duration = max(0.01, min(10.0, duration)) + frequency = max(0.1, min(1000.0, frequency)) + + # 创建效果数据 + effect_id = int(time.time() * 1000000) % 1000000 # 简单ID生成 + + effect_data = { + 'id': effect_id, + 'type': effect_type, + 'device_id': device_id, + 'intensity': intensity, + 'duration': duration * self.haptic_config['duration_scale'], + 'frequency': frequency * self.haptic_config['frequency_scale'], + 'parameters': parameters or {}, + 'start_time': time.time(), + 'status': 'playing', + 'position': parameters.get('position') if parameters else None, + 'temperature': parameters.get('temperature', 25.0) if parameters else 25.0 + } + + # 添加到活跃效果列表 + self.active_effects[effect_id] = effect_data + + # 触发开始回调 + self._trigger_haptic_callback('effect_started', { + 'effect_id': effect_id, + 'effect_data': effect_data + }) + + self.stats['effects_played'] += 1 + print(f"✓ 触觉效果已播放: {effect_type} (ID: {effect_id})") + return effect_id + + except Exception as e: + print(f"✗ 触觉效果播放失败: {e}") + return -1 + + def stop_effect(self, effect_id: int) -> bool: + """ + 停止触觉效果 + + Args: + effect_id: 效果ID + + Returns: + 是否停止成功 + """ + try: + if effect_id in self.active_effects: + effect_data = self.active_effects[effect_id] + effect_data['status'] = 'stopped' + + # 触发停止回调 + self._trigger_haptic_callback('effect_stopped', { + 'effect_id': effect_id, + 'effect_data': effect_data + }) + + # 移除效果 + del self.active_effects[effect_id] + self.stats['effects_stopped'] += 1 + + print(f"✓ 触觉效果已停止: ID {effect_id}") + return True + else: + print(f"✗ 触觉效果不存在: ID {effect_id}") + return False + + except Exception as e: + print(f"✗ 触觉效果停止失败: {e}") + return False + + def stop_all_effects(self) -> bool: + """ + 停止所有触觉效果 + + Returns: + 是否停止成功 + """ + try: + effect_ids = list(self.active_effects.keys()) + for effect_id in effect_ids: + self.stop_effect(effect_id) + + print("✓ 所有触觉效果已停止") + return True + + except Exception as e: + print(f"✗ 停止所有触觉效果失败: {e}") + return False + + def pause_effect(self, effect_id: int) -> bool: + """ + 暂停触觉效果 + + Args: + effect_id: 效果ID + + Returns: + 是否暂停成功 + """ + try: + if effect_id in self.active_effects: + self.active_effects[effect_id]['status'] = 'paused' + self.stats['effects_paused'] += 1 + print(f"✓ 触觉效果已暂停: ID {effect_id}") + return True + else: + print(f"✗ 触觉效果不存在: ID {effect_id}") + return False + + except Exception as e: + print(f"✗ 触觉效果暂停失败: {e}") + return False + + def resume_effect(self, effect_id: int) -> bool: + """ + 恢复触觉效果 + + Args: + effect_id: 效果ID + + Returns: + 是否恢复成功 + """ + try: + if effect_id in self.active_effects: + self.active_effects[effect_id]['status'] = 'playing' + self.stats['effects_resumed'] += 1 + print(f"✓ 触觉效果已恢复: ID {effect_id}") + return True + else: + print(f"✗ 触觉效果不存在: ID {effect_id}") + return False + + except Exception as e: + print(f"✗ 触觉效果恢复失败: {e}") + return False + + def get_effect_status(self, effect_id: int) -> Optional[str]: + """ + 获取效果状态 + + Args: + effect_id: 效果ID + + Returns: + 效果状态或None + """ + try: + if effect_id in self.active_effects: + return self.active_effects[effect_id]['status'] + return None + except Exception as e: + print(f"✗ 效果状态获取失败: {e}") + return None + + def set_haptic_config(self, config: Dict[str, Any]) -> bool: + """ + 设置触觉配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.haptic_config.update(config) + print(f"✓ 触觉配置已更新: {self.haptic_config}") + return True + except Exception as e: + print(f"✗ 触觉配置设置失败: {e}") + return False + + def get_haptic_config(self) -> Dict[str, Any]: + """ + 获取触觉配置 + + Returns: + 配置字典 + """ + return self.haptic_config.copy() + + def set_spatial_parameters(self, params: Dict[str, Any]) -> bool: + """ + 设置空间参数 + + Args: + params: 空间参数字典 + + Returns: + 是否设置成功 + """ + try: + self.spatial_haptics.update(params) + print(f"✓ 空间参数已更新: {self.spatial_haptics}") + return True + except Exception as e: + print(f"✗ 空间参数设置失败: {e}") + return False + + def get_spatial_parameters(self) -> Dict[str, Any]: + """ + 获取空间参数 + + Returns: + 空间参数字典 + """ + return self.spatial_haptics.copy() + + def set_temperature_parameters(self, params: Dict[str, Any]) -> bool: + """ + 设置温度参数 + + Args: + params: 温度参数字典 + + Returns: + 是否设置成功 + """ + try: + self.temperature_feedback.update(params) + print(f"✓ 温度参数已更新: {self.temperature_feedback}") + return True + except Exception as e: + print(f"✗ 温度参数设置失败: {e}") + return False + + def get_temperature_parameters(self) -> Dict[str, Any]: + """ + 获取温度参数 + + Returns: + 温度参数字典 + """ + return self.temperature_feedback.copy() + + def set_listener_position(self, position: tuple): + """ + 设置监听器位置 + + Args: + position: 位置坐标 (x, y, z) + """ + try: + self.spatial_haptics['listener_position'] = position + except Exception as e: + print(f"✗ 监听器位置设置失败: {e}") + + def set_listener_orientation(self, orientation: tuple): + """ + 设置监听器方向 + + Args: + orientation: 方向四元数 (x, y, z, w) + """ + try: + self.spatial_haptics['listener_orientation'] = orientation + except Exception as e: + print(f"✗ 监听器方向设置失败: {e}") + + def _trigger_haptic_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发触觉回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.haptic_callbacks: + for callback in self.haptic_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 触觉回调执行失败: {e}") + except Exception as e: + print(f"✗ 触觉回调触发失败: {e}") + + def register_haptic_callback(self, callback_type: str, callback: callable): + """ + 注册触觉回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.haptic_callbacks: + self.haptic_callbacks[callback_type].append(callback) + print(f"✓ 触觉回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 触觉回调注册失败: {e}") + + def unregister_haptic_callback(self, callback_type: str, callback: callable): + """ + 注销触觉回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.haptic_callbacks: + if callback in self.haptic_callbacks[callback_type]: + self.haptic_callbacks[callback_type].remove(callback) + print(f"✓ 触觉回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 触觉回调注销失败: {e}") + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + try: + self.stats = { + 'effects_played': 0, + 'effects_stopped': 0, + 'effects_paused': 0, + 'effects_resumed': 0, + 'active_effects': 0, + 'spatial_effects': 0, + 'temperature_effects': 0 + } + print("✓ 触觉管理器统计信息已重置") + except Exception as e: print(f"✗ 触觉管理器统计信息重置失败: {e}") \ No newline at end of file diff --git a/plugins/user/haptic_feedback_system/devices/device_manager.py b/plugins/user/haptic_feedback_system/devices/device_manager.py index 7c692410..8192f1ea 100644 --- a/plugins/user/haptic_feedback_system/devices/device_manager.py +++ b/plugins/user/haptic_feedback_system/devices/device_manager.py @@ -1,1096 +1,1096 @@ -""" -设备管理器模块 -负责发现、管理和控制各种触觉反馈设备 -""" - -import time -from typing import Dict, Any, List, Optional -import platform -import threading -import math - -class DeviceType: - """设备类型常量""" - GAMEPAD = "gamepad" - VR_CONTROLLER = "vr_controller" - HAPTIC_GLOVE = "haptic_glove" - VEST = "vest" - SEAT = "seat" - WHEEL = "wheel" - CUSTOM = "custom" - SMARTPHONE = "smartphone" - WATCH = "watch" - EXOSUIT = "exosuit" - -class ConnectionType: - """连接类型常量""" - USB = "usb" - BLUETOOTH = "bluetooth" - WIRELESS = "wireless" - BUILT_IN = "built_in" - NETWORK = "network" - -class HapticDeviceManager: - """ - 触觉设备管理器 - 负责发现、管理和控制各种触觉反馈设备 - """ - - def __init__(self, plugin): - """ - 初始化触觉设备管理器 - - Args: - plugin: 触觉反馈系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 设备字典 - self.devices = {} - self.device_counter = 0 - - # 已连接设备 - self.connected_devices = {} - - # 设备发现配置 - self.discovery_config = { - 'auto_discovery': True, - 'discovery_interval': 5.0, - 'supported_platforms': ['Windows', 'Linux', 'Darwin'], - 'supported_devices': [ - DeviceType.GAMEPAD, - DeviceType.VR_CONTROLLER, - DeviceType.HAPTIC_GLOVE, - DeviceType.VEST, - DeviceType.SEAT, - DeviceType.WHEEL, - DeviceType.SMARTPHONE, - DeviceType.WATCH, - DeviceType.EXOSUIT - ], - 'bluetooth_discovery': True, - 'usb_discovery': True, - 'network_discovery': True - } - - # 平台特定配置 - self.platform_config = { - 'Windows': { - 'api': 'xinput', - 'libraries': ['pydirectinput', 'xinput'], - 'features': ['xinput', 'directinput'], - 'device_paths': ['\\\\.\\PhysicalDrive0'] - }, - 'Linux': { - 'api': 'udev', - 'libraries': ['evdev', 'pygame'], - 'features': ['udev', 'evdev'], - 'device_paths': ['/dev/input', '/dev/hidraw'] - }, - 'Darwin': { # macOS - 'api': 'iokit', - 'libraries': ['pyobjc'], - 'features': ['corehaptics', 'iokit'], - 'device_paths': ['/dev/'] - } - } - - # 设备能力映射 - self.device_capabilities = { - DeviceType.GAMEPAD: [ - 'left_rumble', 'right_rumble', 'trigger_rumble', - 'high_frequency', 'low_frequency', 'adaptive_triggers' - ], - DeviceType.VR_CONTROLLER: [ - 'rumble', 'texture_feedback', 'temperature_feedback', - 'force_feedback', 'vibration_patterns', 'haptic_positioning' - ], - DeviceType.HAPTIC_GLOVE: [ - 'finger_vibration', 'palm_vibration', 'force_feedback', - 'texture_simulation', 'temperature_simulation', 'gesture_feedback' - ], - DeviceType.VEST: [ - 'full_body_vibration', 'localized_vibration', - 'directional_feedback', 'pressure_points', 'impact_simulation' - ], - DeviceType.SEAT: [ - 'low_frequency_rumble', 'vibration_patterns', - 'directional_feedback', 'motion_simulation', 'bass_shakers' - ], - DeviceType.WHEEL: [ - 'force_feedback', 'vibration', 'road_texture', - 'collision_feedback', 'gear_shift', 'damper_effects' - ], - DeviceType.SMARTPHONE: [ - 'simple_vibration', 'pattern_vibration', 'amplitude_control' - ], - DeviceType.WATCH: [ - 'taptic_engine', 'notification_feedback', 'haptic_notifications' - ], - DeviceType.EXOSUIT: [ - 'full_body_actuation', 'muscle_simulation', 'balance_feedback', - 'force_amplification', 'movement_guidance' - ] - } - - # 设备区域映射 - self.device_zones = { - DeviceType.GAMEPAD: ['left_handle', 'right_handle', 'triggers'], - DeviceType.VR_CONTROLLER: ['handle', 'buttons', 'touchpad'], - DeviceType.HAPTIC_GLOVE: ['thumb', 'index', 'middle', 'ring', 'pinky', 'palm'], - DeviceType.VEST: ['front', 'back', 'left', 'right', 'top', 'bottom'], - DeviceType.SEAT: ['seat_base', 'seat_back', 'left_side', 'right_side'], - DeviceType.WHEEL: ['wheel', 'pedals', 'shifter'], - DeviceType.SMARTPHONE: ['back_panel'], - DeviceType.WATCH: ['back_panel'], - DeviceType.EXOSUIT: ['legs', 'arms', 'torso', 'feet'] - } - - # 设备状态 - self.device_states = { - 'scanning': False, - 'last_scan': 0.0, - 'devices_found': 0, - 'bluetooth_connected': 0, - 'usb_connected': 0 - } - - # 统计信息 - self.stats = { - 'devices_registered': 0, - 'devices_connected': 0, - 'devices_disconnected': 0, - 'scan_operations': 0, - 'haptic_effects': 0, - 'device_errors': 0, - 'data_transmitted': 0 - } - - # 线程管理 - self.discovery_thread = None - self.running = False - - # 回调函数 - self.device_callbacks = { - 'device_connected': [], - 'device_disconnected': [], - 'device_updated': [], - 'haptic_effect_applied': [], - 'device_error': [] - } - - # 设备配置 - self.device_config = { - 'default_intensity': 0.8, - 'default_frequency': 50.0, - 'max_intensity': 1.0, - 'min_frequency': 1.0, - 'max_frequency': 300.0, - 'response_time': 0.01, # 10ms响应时间 - 'enable_safety_limits': True - } - - # 设备缓存 - self.device_cache = {} - - # 时间戳记录 - self.last_discovery_time = 0.0 - self.last_state_update = 0.0 - - print("✓ 触觉设备管理器已创建") - - def initialize(self) -> bool: - """ - 初始化触觉设备管理器 - - Returns: - 是否初始化成功 - """ - try: - # 检查平台支持 - current_platform = platform.system() - if current_platform not in self.discovery_config['supported_platforms']: - print(f"⚠ 平台未完全支持: {current_platform}") - - # 初始化平台特定功能 - self._initialize_platform_features() - - # 创建初始设备 - self._create_default_devices() - - # 初始化设备缓存 - self._initialize_device_cache() - - self.initialized = True - print("✓ 触觉设备管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 触觉设备管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用触觉设备管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 触觉设备管理器未初始化") - return False - - self.enabled = True - self.running = True - - # 启动设备发现线程 - if self.discovery_config['auto_discovery']: - self.discovery_thread = threading.Thread(target=self._discovery_loop, daemon=True) - self.discovery_thread.start() - - print("✓ 触觉设备管理器已启用") - return True - - except Exception as e: - print(f"✗ 触觉设备管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用触觉设备管理器""" - try: - self.enabled = False - self.running = False - - # 等待发现线程结束 - if self.discovery_thread and self.discovery_thread.is_alive(): - self.discovery_thread.join(timeout=2.0) - - # 断开所有设备 - self._disconnect_all_devices() - - print("✓ 触觉设备管理器已禁用") - - except Exception as e: - print(f"✗ 触觉设备管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理触觉设备管理器资源""" - try: - self.disable() - self.devices.clear() - self.connected_devices.clear() - self.device_states.clear() - self.device_callbacks.clear() - self.device_cache.clear() - self.initialized = False - print("✓ 触觉设备管理器资源已清理") - - except Exception as e: - print(f"✗ 触觉设备管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新触觉设备管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 定期更新设备状态 - if current_time - self.last_state_update > 1.0: # 每秒更新一次 - self._update_device_states() - self.last_state_update = current_time - - # 更新统计信息 - self.stats['devices_connected'] = len(self.connected_devices) - - except Exception as e: - print(f"✗ 触觉设备管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _initialize_platform_features(self): - """初始化平台特定功能""" - try: - current_platform = platform.system() - platform_info = self.platform_config.get(current_platform, {}) - - print(f"✓ 平台初始化: {current_platform}") - print(f" API: {platform_info.get('api', 'unknown')}") - print(f" 特性: {', '.join(platform_info.get('features', []))}") - - except Exception as e: - print(f"✗ 平台功能初始化失败: {e}") - - def _initialize_device_cache(self): - """初始化设备缓存""" - try: - # 预创建常用设备的配置模板 - for device_type in self.device_capabilities: - self.device_cache[device_type] = { - 'capabilities': self.device_capabilities[device_type], - 'zones': self.device_zones.get(device_type, []), - 'default_config': self._get_default_device_config(device_type) - } - - print("✓ 设备缓存初始化完成") - except Exception as e: - print(f"✗ 设备缓存初始化失败: {e}") - - def _get_default_device_config(self, device_type: str) -> Dict[str, Any]: - """ - 获取设备默认配置 - - Args: - device_type: 设备类型 - - Returns: - 默认配置字典 - """ - base_config = { - 'intensity_scale': 1.0, - 'frequency_scale': 1.0, - 'response_curve': 'linear', - 'max_actuators': 1, - 'min_update_interval': 0.01 - } - - # 根据设备类型添加特定配置 - if device_type == DeviceType.HAPTIC_GLOVE: - base_config.update({ - 'max_actuators': 6, # 5个手指+手掌 - 'independent_control': True - }) - elif device_type == DeviceType.VEST: - base_config.update({ - 'max_actuators': 6, # 6个区域 - 'spatial_resolution': True - }) - elif device_type == DeviceType.EXOSUIT: - base_config.update({ - 'max_actuators': 16, # 多部位控制 - 'force_feedback': True, - 'position_tracking': True - }) - - return base_config - - def _create_default_devices(self): - """创建默认设备""" - try: - current_platform = platform.system() - - # 创建默认游戏手柄 - gamepad_id = self.register_device( - device_type=DeviceType.GAMEPAD, - name=f"系统游戏手柄 ({current_platform})", - connection_type=ConnectionType.BUILT_IN, - vendor_id="system", - product_id="gamepad", - actuators=2 # 左右马达 - ) - - # 创建默认智能手机 - phone_id = self.register_device( - device_type=DeviceType.SMARTPHONE, - name=f"系统智能手机 ({current_platform})", - connection_type=ConnectionType.BUILT_IN, - vendor_id="system", - product_id="smartphone", - actuators=1 # 单一振动马达 - ) - - # 标记为已连接 - if gamepad_id >= 0: - self.connect_device(gamepad_id) - if phone_id >= 0: - self.connect_device(phone_id) - - print("✓ 默认设备创建完成") - - except Exception as e: - print(f"✗ 默认设备创建失败: {e}") - - def _discovery_loop(self): - """设备发现循环""" - try: - discovery_interval = self.discovery_config['discovery_interval'] - - while self.running: - if self.enabled and self.discovery_config['auto_discovery']: - self.scan_for_devices() - - time.sleep(discovery_interval) - - except Exception as e: - print(f"✗ 设备发现循环失败: {e}") - - def _update_device_states(self): - """更新设备状态""" - try: - current_time = time.time() - - # 检查已连接设备的状态 - devices_to_disconnect = [] - for device_id, device_info in self.connected_devices.items(): - # 检查设备是否仍然连接 - if not self._is_device_connected(device_id): - devices_to_disconnect.append(device_id) - - # 断开已丢失的设备 - for device_id in devices_to_disconnect: - self.disconnect_device(device_id) - - except Exception as e: - print(f"✗ 设备状态更新失败: {e}") - self.stats['device_errors'] += 1 - - def _is_device_connected(self, device_id: int) -> bool: - """ - 检查设备是否仍然连接 - - Args: - device_id: 设备ID - - Returns: - 是否连接 - """ - try: - # 这里应该实现实际的设备连接检查逻辑 - # 简化处理,假设设备一直连接 - if device_id in self.devices: - # 模拟设备连接状态检查 - device_info = self.devices[device_id] - last_seen = device_info.get('last_seen', 0) - # 如果超过10秒没有更新,认为设备断开 - return (time.time() - last_seen) < 10.0 - return False - except Exception as e: - print(f"✗ 设备连接检查失败: {e}") - return True - - def _disconnect_all_devices(self): - """断开所有设备""" - try: - device_ids = list(self.connected_devices.keys()) - for device_id in device_ids: - self.disconnect_device(device_id) - except Exception as e: - print(f"✗ 断开所有设备失败: {e}") - - def register_device(self, device_type: str, name: str, connection_type: str, - vendor_id: str, product_id: str, - capabilities: List[str] = None, actuators: int = 1) -> int: - """ - 注册设备 - - Args: - device_type: 设备类型 - name: 设备名称 - connection_type: 连接类型 - vendor_id: 厂商ID - product_id: 产品ID - capabilities: 设备功能列表 - actuators: 执行器数量 - - Returns: - 设备ID - """ - try: - device_id = self.device_counter - self.device_counter += 1 - - # 如果未提供功能列表,使用默认功能 - if capabilities is None: - capabilities = self.device_capabilities.get(device_type, []) - - device_info = { - 'id': device_id, - 'type': device_type, - 'name': name, - 'connection_type': connection_type, - 'vendor_id': vendor_id, - 'product_id': product_id, - 'capabilities': capabilities, - 'actuators': actuators, - 'registered_time': time.time(), - 'connected': False, - 'last_seen': 0.0, - 'battery_level': 100.0, # 电池电量百分比 - 'signal_strength': 100.0, # 信号强度百分比 - 'firmware_version': '1.0.0', - 'hardware_version': '1.0.0', - 'haptic_state': {}, # 触觉状态 - 'zones': self.device_zones.get(device_type, []), - 'config': self.device_cache.get(device_type, {}).get('default_config', {}), - 'supported_effects': ['simple', 'periodic', 'envelope'], - 'max_intensity': 1.0, - 'min_frequency': 1.0, - 'max_frequency': 300.0, - 'response_time': 0.01 - } - - # 根据设备类型添加特定支持的效果 - if device_type in [DeviceType.HAPTIC_GLOVE, DeviceType.VEST, DeviceType.EXOSUIT]: - device_info['supported_effects'].extend(['texture', 'wind', 'water']) - if device_type in [DeviceType.VR_CONTROLLER, DeviceType.WHEEL]: - device_info['supported_effects'].extend(['impulse', 'explosion']) - - self.devices[device_id] = device_info - self.stats['devices_registered'] += 1 - - print(f"✓ 设备已注册: {name} (ID: {device_id})") - return device_id - - except Exception as e: - print(f"✗ 设备注册失败: {e}") - self.stats['device_errors'] += 1 - return -1 - - def unregister_device(self, device_id: int) -> bool: - """ - 注销设备 - - Args: - device_id: 设备ID - - Returns: - 是否注销成功 - """ - try: - if device_id in self.devices: - device_info = self.devices[device_id] - device_name = device_info['name'] - - # 如果设备已连接,先断开连接 - if device_id in self.connected_devices: - self.disconnect_device(device_id) - - del self.devices[device_id] - print(f"✓ 设备已注销: {device_name} (ID: {device_id})") - return True - else: - print(f"✗ 设备不存在: ID {device_id}") - return False - - except Exception as e: - print(f"✗ 设备注销失败: {e}") - self.stats['device_errors'] += 1 - return False - - def connect_device(self, device_id: int) -> bool: - """ - 连接设备 - - Args: - device_id: 设备ID - - Returns: - 是否连接成功 - """ - try: - if device_id not in self.devices: - print(f"✗ 设备不存在: ID {device_id}") - return False - - device_info = self.devices[device_id] - if device_info['connected']: - print(f"⚠ 设备已连接: {device_info['name']} (ID: {device_id})") - return True - - # 模拟设备连接过程 - device_info['connected'] = True - device_info['last_seen'] = time.time() - self.connected_devices[device_id] = device_info - - # 更新连接统计 - conn_type = device_info['connection_type'] - if conn_type == ConnectionType.BLUETOOTH: - self.device_states['bluetooth_connected'] += 1 - elif conn_type == ConnectionType.USB: - self.device_states['usb_connected'] += 1 - - # 触发连接回调 - self._trigger_device_callback('device_connected', { - 'device_id': device_id, - 'device_info': device_info - }) - - self.stats['devices_connected'] += 1 - print(f"✓ 设备已连接: {device_info['name']} (ID: {device_id})") - return True - - except Exception as e: - print(f"✗ 设备连接失败: {e}") - self.stats['device_errors'] += 1 - return False - - def disconnect_device(self, device_id: int) -> bool: - """ - 断开设备连接 - - Args: - device_id: 设备ID - - Returns: - 是否断开成功 - """ - try: - if device_id not in self.devices: - print(f"✗ 设备不存在: ID {device_id}") - return False - - device_info = self.devices[device_id] - if not device_info['connected']: - print(f"⚠ 设备未连接: {device_info['name']} (ID: {device_id})") - return True - - # 断开设备连接 - device_info['connected'] = False - if device_id in self.connected_devices: - del self.connected_devices[device_id] - - # 更新连接统计 - conn_type = device_info['connection_type'] - if conn_type == ConnectionType.BLUETOOTH: - self.device_states['bluetooth_connected'] = max(0, self.device_states['bluetooth_connected'] - 1) - elif conn_type == ConnectionType.USB: - self.device_states['usb_connected'] = max(0, self.device_states['usb_connected'] - 1) - - # 触发断开回调 - self._trigger_device_callback('device_disconnected', { - 'device_id': device_id, - 'device_info': device_info - }) - - self.stats['devices_disconnected'] += 1 - print(f"✓ 设备已断开: {device_info['name']} (ID: {device_id})") - return True - - except Exception as e: - print(f"✗ 设备断开失败: {e}") - self.stats['device_errors'] += 1 - return False - - def scan_for_devices(self) -> List[int]: - """ - 扫描设备 - - Returns: - 发现的设备ID列表 - """ - try: - self.device_states['scanning'] = True - self.device_states['last_scan'] = time.time() - - discovered_devices = [] - - # 模拟设备发现过程 - # 在实际实现中,这里会根据平台调用相应的API来发现设备 - - # 检查现有设备的状态 - for device_id, device_info in self.devices.items(): - # 模拟设备发现 - if not device_info['connected']: - # 有一定概率"发现"设备 - import random - if random.random() < 0.1: # 10%概率 - discovered_devices.append(device_id) - - self.device_states['devices_found'] = len(discovered_devices) - self.stats['scan_operations'] += 1 - - if discovered_devices: - print(f"✓ 发现 {len(discovered_devices)} 个设备") - - return discovered_devices - - except Exception as e: - print(f"✗ 设备扫描失败: {e}") - self.stats['device_errors'] += 1 - return [] - finally: - self.device_states['scanning'] = False - - def apply_haptic_effect(self, device_id: int, intensity: float, - frequency: float, progress: float, - zone: str = None, effect_type: str = 'simple') -> bool: - """ - 应用触觉效果到设备 - - Args: - device_id: 设备ID - intensity: 强度(0.0-1.0) - frequency: 频率 - progress: 进度(0.0-1.0) - zone: 设备区域 - effect_type: 效果类型 - - Returns: - 是否应用成功 - """ - try: - # 检查设备是否存在 - if device_id is not None and device_id not in self.devices: - print(f"✗ 设备不存在: ID {device_id}") - return False - - # 检查设备是否连接 - if device_id is not None and not self.devices[device_id]['connected']: - print(f"✗ 设备未连接: ID {device_id}") - return False - - # 限制参数范围 - intensity = max(0.0, min(1.0, intensity)) - frequency = max(0.0, frequency) - progress = max(0.0, min(1.0, progress)) - - # 应用设备配置限制 - if device_id is not None: - device_info = self.devices[device_id] - intensity *= device_info.get('max_intensity', 1.0) - frequency = max( - device_info.get('min_frequency', 1.0), - min(device_info.get('max_frequency', 300.0), frequency) - ) - - # 检查设备是否支持该效果类型 - supported_effects = device_info.get('supported_effects', ['simple']) - if effect_type not in supported_effects: - print(f"⚠ 设备不支持效果类型: {effect_type}") - # 降级到简单效果 - effect_type = 'simple' - - # 更新设备触觉状态 - if device_id is not None: - device_info = self.devices[device_id] - device_info['haptic_state'] = { - 'intensity': intensity, - 'frequency': frequency, - 'progress': progress, - 'zone': zone, - 'effect_type': effect_type, - 'timestamp': time.time() - } - device_info['last_seen'] = time.time() - - # 模拟数据传输 - data_size = 32 # 模拟32字节数据 - self.stats['data_transmitted'] += data_size - - # 触发触觉效果应用回调 - self._trigger_device_callback('haptic_effect_applied', { - 'device_id': device_id, - 'intensity': intensity, - 'frequency': frequency, - 'progress': progress, - 'zone': zone, - 'effect_type': effect_type, - 'data_size': data_size - }) - - self.stats['haptic_effects'] += 1 - return True - - except Exception as e: - print(f"✗ 触觉效果应用失败: {e}") - self.stats['device_errors'] += 1 - return False - - def get_device(self, device_id: int) -> Optional[Dict[str, Any]]: - """ - 获取设备信息 - - Args: - device_id: 设备ID - - Returns: - 设备信息字典或None - """ - return self.devices.get(device_id) - - def get_connected_devices(self) -> Dict[int, Dict[str, Any]]: - """ - 获取所有已连接的设备 - - Returns: - 已连接设备字典 - """ - return self.connected_devices.copy() - - def get_devices_by_type(self, device_type: str) -> List[Dict[str, Any]]: - """ - 根据类型获取设备列表 - - Args: - device_type: 设备类型 - - Returns: - 设备信息列表 - """ - try: - devices = [] - for device_info in self.devices.values(): - if device_info['type'] == device_type: - devices.append(device_info) - return devices - except Exception as e: - print(f"✗ 按类型获取设备失败: {e}") - return [] - - def update_device_battery(self, device_id: int, battery_level: float): - """ - 更新设备电池电量 - - Args: - device_id: 设备ID - battery_level: 电池电量百分比(0-100) - """ - try: - if device_id in self.devices: - self.devices[device_id]['battery_level'] = max(0.0, min(100.0, battery_level)) - except Exception as e: - print(f"✗ 设备电池电量更新失败: {e}") - self.stats['device_errors'] += 1 - - def update_device_signal(self, device_id: int, signal_strength: float): - """ - 更新设备信号强度 - - Args: - device_id: 设备ID - signal_strength: 信号强度百分比(0-100) - """ - try: - if device_id in self.devices: - self.devices[device_id]['signal_strength'] = max(0.0, min(100.0, signal_strength)) - except Exception as e: - print(f"✗ 设备信号强度更新失败: {e}") - self.stats['device_errors'] += 1 - - def set_device_config(self, device_id: int, config: Dict[str, Any]) -> bool: - """ - 设置设备配置 - - Args: - device_id: 设备ID - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - if device_id in self.devices: - self.devices[device_id]['config'].update(config) - print(f"✓ 设备配置已更新: ID {device_id}") - return True - else: - print(f"✗ 设备不存在: ID {device_id}") - return False - except Exception as e: - print(f"✗ 设备配置设置失败: {e}") - self.stats['device_errors'] += 1 - return False - - def get_device_config(self, device_id: int) -> Optional[Dict[str, Any]]: - """ - 获取设备配置 - - Args: - device_id: 设备ID - - Returns: - 配置字典或None - """ - try: - if device_id in self.devices: - return self.devices[device_id].get('config', {}) - return None - except Exception as e: - print(f"✗ 设备配置获取失败: {e}") - return None - - def calibrate_device(self, device_id: int) -> bool: - """ - 校准设备 - - Args: - device_id: 设备ID - - Returns: - 是否校准成功 - """ - try: - if device_id not in self.devices: - print(f"✗ 设备不存在: ID {device_id}") - return False - - device_info = self.devices[device_id] - print(f"✓ 设备校准完成: {device_info['name']} (ID: {device_id})") - - # 更新校准时间 - device_info['last_calibrated'] = time.time() - return True - - except Exception as e: - print(f"✗ 设备校准失败: {e}") - self.stats['device_errors'] += 1 - return False - - def _trigger_device_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发设备回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.device_callbacks: - for callback in self.device_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 设备回调执行失败: {e}") - except Exception as e: - print(f"✗ 设备回调触发失败: {e}") - - def register_device_callback(self, callback_type: str, callback: callable): - """ - 注册设备回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.device_callbacks: - self.device_callbacks[callback_type].append(callback) - print(f"✓ 设备回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 设备回调注册失败: {e}") - - def unregister_device_callback(self, callback_type: str, callback: callable): - """ - 注销设备回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.device_callbacks: - if callback in self.device_callbacks[callback_type]: - self.device_callbacks[callback_type].remove(callback) - print(f"✓ 设备回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 设备回调注销失败: {e}") - - def get_device_capabilities(self, device_type: str) -> List[str]: - """ - 获取设备类型的功能列表 - - Args: - device_type: 设备类型 - - Returns: - 功能列表 - """ - return self.device_capabilities.get(device_type, []) - - def get_device_zones(self, device_type: str) -> List[str]: - """ - 获取设备区域列表 - - Args: - device_type: 设备类型 - - Returns: - 区域列表 - """ - return self.device_zones.get(device_type, []) - - def get_platform_config(self) -> Dict[str, Any]: - """ - 获取平台配置 - - Returns: - 平台配置字典 - """ - current_platform = platform.system() - return self.platform_config.get(current_platform, {}) - - def set_discovery_config(self, config: Dict[str, Any]): - """ - 设置设备发现配置 - - Args: - config: 发现配置字典 - """ - try: - self.discovery_config.update(config) - print(f"✓ 设备发现配置已更新: {self.discovery_config}") - except Exception as e: - print(f"✗ 设备发现配置设置失败: {e}") - self.stats['device_errors'] += 1 - - def get_discovery_config(self) -> Dict[str, Any]: - """ - 获取设备发现配置 - - Returns: - 发现配置字典 - """ - return self.discovery_config.copy() - - def get_device_stats(self) -> Dict[str, Any]: - """ - 获取设备统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_device_stats(self): - """重置设备统计信息""" - try: - self.stats = { - 'devices_registered': 0, - 'devices_connected': 0, - 'devices_disconnected': 0, - 'scan_operations': 0, - 'haptic_effects': 0, - 'device_errors': 0, - 'data_transmitted': 0 - } - print("✓ 设备统计信息已重置") - except Exception as e: - print(f"✗ 设备统计信息重置失败: {e}") - - def get_device_states(self) -> Dict[str, Any]: - """ - 获取设备状态 - - Returns: - 设备状态字典 - """ +""" +设备管理器模块 +负责发现、管理和控制各种触觉反馈设备 +""" + +import time +from typing import Dict, Any, List, Optional +import platform +import threading +import math + +class DeviceType: + """设备类型常量""" + GAMEPAD = "gamepad" + VR_CONTROLLER = "vr_controller" + HAPTIC_GLOVE = "haptic_glove" + VEST = "vest" + SEAT = "seat" + WHEEL = "wheel" + CUSTOM = "custom" + SMARTPHONE = "smartphone" + WATCH = "watch" + EXOSUIT = "exosuit" + +class ConnectionType: + """连接类型常量""" + USB = "usb" + BLUETOOTH = "bluetooth" + WIRELESS = "wireless" + BUILT_IN = "built_in" + NETWORK = "network" + +class HapticDeviceManager: + """ + 触觉设备管理器 + 负责发现、管理和控制各种触觉反馈设备 + """ + + def __init__(self, plugin): + """ + 初始化触觉设备管理器 + + Args: + plugin: 触觉反馈系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 设备字典 + self.devices = {} + self.device_counter = 0 + + # 已连接设备 + self.connected_devices = {} + + # 设备发现配置 + self.discovery_config = { + 'auto_discovery': True, + 'discovery_interval': 5.0, + 'supported_platforms': ['Windows', 'Linux', 'Darwin'], + 'supported_devices': [ + DeviceType.GAMEPAD, + DeviceType.VR_CONTROLLER, + DeviceType.HAPTIC_GLOVE, + DeviceType.VEST, + DeviceType.SEAT, + DeviceType.WHEEL, + DeviceType.SMARTPHONE, + DeviceType.WATCH, + DeviceType.EXOSUIT + ], + 'bluetooth_discovery': True, + 'usb_discovery': True, + 'network_discovery': True + } + + # 平台特定配置 + self.platform_config = { + 'Windows': { + 'api': 'xinput', + 'libraries': ['pydirectinput', 'xinput'], + 'features': ['xinput', 'directinput'], + 'device_paths': ['\\\\.\\PhysicalDrive0'] + }, + 'Linux': { + 'api': 'udev', + 'libraries': ['evdev', 'pygame'], + 'features': ['udev', 'evdev'], + 'device_paths': ['/dev/input', '/dev/hidraw'] + }, + 'Darwin': { # macOS + 'api': 'iokit', + 'libraries': ['pyobjc'], + 'features': ['corehaptics', 'iokit'], + 'device_paths': ['/dev/'] + } + } + + # 设备能力映射 + self.device_capabilities = { + DeviceType.GAMEPAD: [ + 'left_rumble', 'right_rumble', 'trigger_rumble', + 'high_frequency', 'low_frequency', 'adaptive_triggers' + ], + DeviceType.VR_CONTROLLER: [ + 'rumble', 'texture_feedback', 'temperature_feedback', + 'force_feedback', 'vibration_patterns', 'haptic_positioning' + ], + DeviceType.HAPTIC_GLOVE: [ + 'finger_vibration', 'palm_vibration', 'force_feedback', + 'texture_simulation', 'temperature_simulation', 'gesture_feedback' + ], + DeviceType.VEST: [ + 'full_body_vibration', 'localized_vibration', + 'directional_feedback', 'pressure_points', 'impact_simulation' + ], + DeviceType.SEAT: [ + 'low_frequency_rumble', 'vibration_patterns', + 'directional_feedback', 'motion_simulation', 'bass_shakers' + ], + DeviceType.WHEEL: [ + 'force_feedback', 'vibration', 'road_texture', + 'collision_feedback', 'gear_shift', 'damper_effects' + ], + DeviceType.SMARTPHONE: [ + 'simple_vibration', 'pattern_vibration', 'amplitude_control' + ], + DeviceType.WATCH: [ + 'taptic_engine', 'notification_feedback', 'haptic_notifications' + ], + DeviceType.EXOSUIT: [ + 'full_body_actuation', 'muscle_simulation', 'balance_feedback', + 'force_amplification', 'movement_guidance' + ] + } + + # 设备区域映射 + self.device_zones = { + DeviceType.GAMEPAD: ['left_handle', 'right_handle', 'triggers'], + DeviceType.VR_CONTROLLER: ['handle', 'buttons', 'touchpad'], + DeviceType.HAPTIC_GLOVE: ['thumb', 'index', 'middle', 'ring', 'pinky', 'palm'], + DeviceType.VEST: ['front', 'back', 'left', 'right', 'top', 'bottom'], + DeviceType.SEAT: ['seat_base', 'seat_back', 'left_side', 'right_side'], + DeviceType.WHEEL: ['wheel', 'pedals', 'shifter'], + DeviceType.SMARTPHONE: ['back_panel'], + DeviceType.WATCH: ['back_panel'], + DeviceType.EXOSUIT: ['legs', 'arms', 'torso', 'feet'] + } + + # 设备状态 + self.device_states = { + 'scanning': False, + 'last_scan': 0.0, + 'devices_found': 0, + 'bluetooth_connected': 0, + 'usb_connected': 0 + } + + # 统计信息 + self.stats = { + 'devices_registered': 0, + 'devices_connected': 0, + 'devices_disconnected': 0, + 'scan_operations': 0, + 'haptic_effects': 0, + 'device_errors': 0, + 'data_transmitted': 0 + } + + # 线程管理 + self.discovery_thread = None + self.running = False + + # 回调函数 + self.device_callbacks = { + 'device_connected': [], + 'device_disconnected': [], + 'device_updated': [], + 'haptic_effect_applied': [], + 'device_error': [] + } + + # 设备配置 + self.device_config = { + 'default_intensity': 0.8, + 'default_frequency': 50.0, + 'max_intensity': 1.0, + 'min_frequency': 1.0, + 'max_frequency': 300.0, + 'response_time': 0.01, # 10ms响应时间 + 'enable_safety_limits': True + } + + # 设备缓存 + self.device_cache = {} + + # 时间戳记录 + self.last_discovery_time = 0.0 + self.last_state_update = 0.0 + + print("✓ 触觉设备管理器已创建") + + def initialize(self) -> bool: + """ + 初始化触觉设备管理器 + + Returns: + 是否初始化成功 + """ + try: + # 检查平台支持 + current_platform = platform.system() + if current_platform not in self.discovery_config['supported_platforms']: + print(f"⚠ 平台未完全支持: {current_platform}") + + # 初始化平台特定功能 + self._initialize_platform_features() + + # 创建初始设备 + self._create_default_devices() + + # 初始化设备缓存 + self._initialize_device_cache() + + self.initialized = True + print("✓ 触觉设备管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 触觉设备管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用触觉设备管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 触觉设备管理器未初始化") + return False + + self.enabled = True + self.running = True + + # 启动设备发现线程 + if self.discovery_config['auto_discovery']: + self.discovery_thread = threading.Thread(target=self._discovery_loop, daemon=True) + self.discovery_thread.start() + + print("✓ 触觉设备管理器已启用") + return True + + except Exception as e: + print(f"✗ 触觉设备管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用触觉设备管理器""" + try: + self.enabled = False + self.running = False + + # 等待发现线程结束 + if self.discovery_thread and self.discovery_thread.is_alive(): + self.discovery_thread.join(timeout=2.0) + + # 断开所有设备 + self._disconnect_all_devices() + + print("✓ 触觉设备管理器已禁用") + + except Exception as e: + print(f"✗ 触觉设备管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理触觉设备管理器资源""" + try: + self.disable() + self.devices.clear() + self.connected_devices.clear() + self.device_states.clear() + self.device_callbacks.clear() + self.device_cache.clear() + self.initialized = False + print("✓ 触觉设备管理器资源已清理") + + except Exception as e: + print(f"✗ 触觉设备管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新触觉设备管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 定期更新设备状态 + if current_time - self.last_state_update > 1.0: # 每秒更新一次 + self._update_device_states() + self.last_state_update = current_time + + # 更新统计信息 + self.stats['devices_connected'] = len(self.connected_devices) + + except Exception as e: + print(f"✗ 触觉设备管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _initialize_platform_features(self): + """初始化平台特定功能""" + try: + current_platform = platform.system() + platform_info = self.platform_config.get(current_platform, {}) + + print(f"✓ 平台初始化: {current_platform}") + print(f" API: {platform_info.get('api', 'unknown')}") + print(f" 特性: {', '.join(platform_info.get('features', []))}") + + except Exception as e: + print(f"✗ 平台功能初始化失败: {e}") + + def _initialize_device_cache(self): + """初始化设备缓存""" + try: + # 预创建常用设备的配置模板 + for device_type in self.device_capabilities: + self.device_cache[device_type] = { + 'capabilities': self.device_capabilities[device_type], + 'zones': self.device_zones.get(device_type, []), + 'default_config': self._get_default_device_config(device_type) + } + + print("✓ 设备缓存初始化完成") + except Exception as e: + print(f"✗ 设备缓存初始化失败: {e}") + + def _get_default_device_config(self, device_type: str) -> Dict[str, Any]: + """ + 获取设备默认配置 + + Args: + device_type: 设备类型 + + Returns: + 默认配置字典 + """ + base_config = { + 'intensity_scale': 1.0, + 'frequency_scale': 1.0, + 'response_curve': 'linear', + 'max_actuators': 1, + 'min_update_interval': 0.01 + } + + # 根据设备类型添加特定配置 + if device_type == DeviceType.HAPTIC_GLOVE: + base_config.update({ + 'max_actuators': 6, # 5个手指+手掌 + 'independent_control': True + }) + elif device_type == DeviceType.VEST: + base_config.update({ + 'max_actuators': 6, # 6个区域 + 'spatial_resolution': True + }) + elif device_type == DeviceType.EXOSUIT: + base_config.update({ + 'max_actuators': 16, # 多部位控制 + 'force_feedback': True, + 'position_tracking': True + }) + + return base_config + + def _create_default_devices(self): + """创建默认设备""" + try: + current_platform = platform.system() + + # 创建默认游戏手柄 + gamepad_id = self.register_device( + device_type=DeviceType.GAMEPAD, + name=f"系统游戏手柄 ({current_platform})", + connection_type=ConnectionType.BUILT_IN, + vendor_id="system", + product_id="gamepad", + actuators=2 # 左右马达 + ) + + # 创建默认智能手机 + phone_id = self.register_device( + device_type=DeviceType.SMARTPHONE, + name=f"系统智能手机 ({current_platform})", + connection_type=ConnectionType.BUILT_IN, + vendor_id="system", + product_id="smartphone", + actuators=1 # 单一振动马达 + ) + + # 标记为已连接 + if gamepad_id >= 0: + self.connect_device(gamepad_id) + if phone_id >= 0: + self.connect_device(phone_id) + + print("✓ 默认设备创建完成") + + except Exception as e: + print(f"✗ 默认设备创建失败: {e}") + + def _discovery_loop(self): + """设备发现循环""" + try: + discovery_interval = self.discovery_config['discovery_interval'] + + while self.running: + if self.enabled and self.discovery_config['auto_discovery']: + self.scan_for_devices() + + time.sleep(discovery_interval) + + except Exception as e: + print(f"✗ 设备发现循环失败: {e}") + + def _update_device_states(self): + """更新设备状态""" + try: + current_time = time.time() + + # 检查已连接设备的状态 + devices_to_disconnect = [] + for device_id, device_info in self.connected_devices.items(): + # 检查设备是否仍然连接 + if not self._is_device_connected(device_id): + devices_to_disconnect.append(device_id) + + # 断开已丢失的设备 + for device_id in devices_to_disconnect: + self.disconnect_device(device_id) + + except Exception as e: + print(f"✗ 设备状态更新失败: {e}") + self.stats['device_errors'] += 1 + + def _is_device_connected(self, device_id: int) -> bool: + """ + 检查设备是否仍然连接 + + Args: + device_id: 设备ID + + Returns: + 是否连接 + """ + try: + # 这里应该实现实际的设备连接检查逻辑 + # 简化处理,假设设备一直连接 + if device_id in self.devices: + # 模拟设备连接状态检查 + device_info = self.devices[device_id] + last_seen = device_info.get('last_seen', 0) + # 如果超过10秒没有更新,认为设备断开 + return (time.time() - last_seen) < 10.0 + return False + except Exception as e: + print(f"✗ 设备连接检查失败: {e}") + return True + + def _disconnect_all_devices(self): + """断开所有设备""" + try: + device_ids = list(self.connected_devices.keys()) + for device_id in device_ids: + self.disconnect_device(device_id) + except Exception as e: + print(f"✗ 断开所有设备失败: {e}") + + def register_device(self, device_type: str, name: str, connection_type: str, + vendor_id: str, product_id: str, + capabilities: List[str] = None, actuators: int = 1) -> int: + """ + 注册设备 + + Args: + device_type: 设备类型 + name: 设备名称 + connection_type: 连接类型 + vendor_id: 厂商ID + product_id: 产品ID + capabilities: 设备功能列表 + actuators: 执行器数量 + + Returns: + 设备ID + """ + try: + device_id = self.device_counter + self.device_counter += 1 + + # 如果未提供功能列表,使用默认功能 + if capabilities is None: + capabilities = self.device_capabilities.get(device_type, []) + + device_info = { + 'id': device_id, + 'type': device_type, + 'name': name, + 'connection_type': connection_type, + 'vendor_id': vendor_id, + 'product_id': product_id, + 'capabilities': capabilities, + 'actuators': actuators, + 'registered_time': time.time(), + 'connected': False, + 'last_seen': 0.0, + 'battery_level': 100.0, # 电池电量百分比 + 'signal_strength': 100.0, # 信号强度百分比 + 'firmware_version': '1.0.0', + 'hardware_version': '1.0.0', + 'haptic_state': {}, # 触觉状态 + 'zones': self.device_zones.get(device_type, []), + 'config': self.device_cache.get(device_type, {}).get('default_config', {}), + 'supported_effects': ['simple', 'periodic', 'envelope'], + 'max_intensity': 1.0, + 'min_frequency': 1.0, + 'max_frequency': 300.0, + 'response_time': 0.01 + } + + # 根据设备类型添加特定支持的效果 + if device_type in [DeviceType.HAPTIC_GLOVE, DeviceType.VEST, DeviceType.EXOSUIT]: + device_info['supported_effects'].extend(['texture', 'wind', 'water']) + if device_type in [DeviceType.VR_CONTROLLER, DeviceType.WHEEL]: + device_info['supported_effects'].extend(['impulse', 'explosion']) + + self.devices[device_id] = device_info + self.stats['devices_registered'] += 1 + + print(f"✓ 设备已注册: {name} (ID: {device_id})") + return device_id + + except Exception as e: + print(f"✗ 设备注册失败: {e}") + self.stats['device_errors'] += 1 + return -1 + + def unregister_device(self, device_id: int) -> bool: + """ + 注销设备 + + Args: + device_id: 设备ID + + Returns: + 是否注销成功 + """ + try: + if device_id in self.devices: + device_info = self.devices[device_id] + device_name = device_info['name'] + + # 如果设备已连接,先断开连接 + if device_id in self.connected_devices: + self.disconnect_device(device_id) + + del self.devices[device_id] + print(f"✓ 设备已注销: {device_name} (ID: {device_id})") + return True + else: + print(f"✗ 设备不存在: ID {device_id}") + return False + + except Exception as e: + print(f"✗ 设备注销失败: {e}") + self.stats['device_errors'] += 1 + return False + + def connect_device(self, device_id: int) -> bool: + """ + 连接设备 + + Args: + device_id: 设备ID + + Returns: + 是否连接成功 + """ + try: + if device_id not in self.devices: + print(f"✗ 设备不存在: ID {device_id}") + return False + + device_info = self.devices[device_id] + if device_info['connected']: + print(f"⚠ 设备已连接: {device_info['name']} (ID: {device_id})") + return True + + # 模拟设备连接过程 + device_info['connected'] = True + device_info['last_seen'] = time.time() + self.connected_devices[device_id] = device_info + + # 更新连接统计 + conn_type = device_info['connection_type'] + if conn_type == ConnectionType.BLUETOOTH: + self.device_states['bluetooth_connected'] += 1 + elif conn_type == ConnectionType.USB: + self.device_states['usb_connected'] += 1 + + # 触发连接回调 + self._trigger_device_callback('device_connected', { + 'device_id': device_id, + 'device_info': device_info + }) + + self.stats['devices_connected'] += 1 + print(f"✓ 设备已连接: {device_info['name']} (ID: {device_id})") + return True + + except Exception as e: + print(f"✗ 设备连接失败: {e}") + self.stats['device_errors'] += 1 + return False + + def disconnect_device(self, device_id: int) -> bool: + """ + 断开设备连接 + + Args: + device_id: 设备ID + + Returns: + 是否断开成功 + """ + try: + if device_id not in self.devices: + print(f"✗ 设备不存在: ID {device_id}") + return False + + device_info = self.devices[device_id] + if not device_info['connected']: + print(f"⚠ 设备未连接: {device_info['name']} (ID: {device_id})") + return True + + # 断开设备连接 + device_info['connected'] = False + if device_id in self.connected_devices: + del self.connected_devices[device_id] + + # 更新连接统计 + conn_type = device_info['connection_type'] + if conn_type == ConnectionType.BLUETOOTH: + self.device_states['bluetooth_connected'] = max(0, self.device_states['bluetooth_connected'] - 1) + elif conn_type == ConnectionType.USB: + self.device_states['usb_connected'] = max(0, self.device_states['usb_connected'] - 1) + + # 触发断开回调 + self._trigger_device_callback('device_disconnected', { + 'device_id': device_id, + 'device_info': device_info + }) + + self.stats['devices_disconnected'] += 1 + print(f"✓ 设备已断开: {device_info['name']} (ID: {device_id})") + return True + + except Exception as e: + print(f"✗ 设备断开失败: {e}") + self.stats['device_errors'] += 1 + return False + + def scan_for_devices(self) -> List[int]: + """ + 扫描设备 + + Returns: + 发现的设备ID列表 + """ + try: + self.device_states['scanning'] = True + self.device_states['last_scan'] = time.time() + + discovered_devices = [] + + # 模拟设备发现过程 + # 在实际实现中,这里会根据平台调用相应的API来发现设备 + + # 检查现有设备的状态 + for device_id, device_info in self.devices.items(): + # 模拟设备发现 + if not device_info['connected']: + # 有一定概率"发现"设备 + import random + if random.random() < 0.1: # 10%概率 + discovered_devices.append(device_id) + + self.device_states['devices_found'] = len(discovered_devices) + self.stats['scan_operations'] += 1 + + if discovered_devices: + print(f"✓ 发现 {len(discovered_devices)} 个设备") + + return discovered_devices + + except Exception as e: + print(f"✗ 设备扫描失败: {e}") + self.stats['device_errors'] += 1 + return [] + finally: + self.device_states['scanning'] = False + + def apply_haptic_effect(self, device_id: int, intensity: float, + frequency: float, progress: float, + zone: str = None, effect_type: str = 'simple') -> bool: + """ + 应用触觉效果到设备 + + Args: + device_id: 设备ID + intensity: 强度(0.0-1.0) + frequency: 频率 + progress: 进度(0.0-1.0) + zone: 设备区域 + effect_type: 效果类型 + + Returns: + 是否应用成功 + """ + try: + # 检查设备是否存在 + if device_id is not None and device_id not in self.devices: + print(f"✗ 设备不存在: ID {device_id}") + return False + + # 检查设备是否连接 + if device_id is not None and not self.devices[device_id]['connected']: + print(f"✗ 设备未连接: ID {device_id}") + return False + + # 限制参数范围 + intensity = max(0.0, min(1.0, intensity)) + frequency = max(0.0, frequency) + progress = max(0.0, min(1.0, progress)) + + # 应用设备配置限制 + if device_id is not None: + device_info = self.devices[device_id] + intensity *= device_info.get('max_intensity', 1.0) + frequency = max( + device_info.get('min_frequency', 1.0), + min(device_info.get('max_frequency', 300.0), frequency) + ) + + # 检查设备是否支持该效果类型 + supported_effects = device_info.get('supported_effects', ['simple']) + if effect_type not in supported_effects: + print(f"⚠ 设备不支持效果类型: {effect_type}") + # 降级到简单效果 + effect_type = 'simple' + + # 更新设备触觉状态 + if device_id is not None: + device_info = self.devices[device_id] + device_info['haptic_state'] = { + 'intensity': intensity, + 'frequency': frequency, + 'progress': progress, + 'zone': zone, + 'effect_type': effect_type, + 'timestamp': time.time() + } + device_info['last_seen'] = time.time() + + # 模拟数据传输 + data_size = 32 # 模拟32字节数据 + self.stats['data_transmitted'] += data_size + + # 触发触觉效果应用回调 + self._trigger_device_callback('haptic_effect_applied', { + 'device_id': device_id, + 'intensity': intensity, + 'frequency': frequency, + 'progress': progress, + 'zone': zone, + 'effect_type': effect_type, + 'data_size': data_size + }) + + self.stats['haptic_effects'] += 1 + return True + + except Exception as e: + print(f"✗ 触觉效果应用失败: {e}") + self.stats['device_errors'] += 1 + return False + + def get_device(self, device_id: int) -> Optional[Dict[str, Any]]: + """ + 获取设备信息 + + Args: + device_id: 设备ID + + Returns: + 设备信息字典或None + """ + return self.devices.get(device_id) + + def get_connected_devices(self) -> Dict[int, Dict[str, Any]]: + """ + 获取所有已连接的设备 + + Returns: + 已连接设备字典 + """ + return self.connected_devices.copy() + + def get_devices_by_type(self, device_type: str) -> List[Dict[str, Any]]: + """ + 根据类型获取设备列表 + + Args: + device_type: 设备类型 + + Returns: + 设备信息列表 + """ + try: + devices = [] + for device_info in self.devices.values(): + if device_info['type'] == device_type: + devices.append(device_info) + return devices + except Exception as e: + print(f"✗ 按类型获取设备失败: {e}") + return [] + + def update_device_battery(self, device_id: int, battery_level: float): + """ + 更新设备电池电量 + + Args: + device_id: 设备ID + battery_level: 电池电量百分比(0-100) + """ + try: + if device_id in self.devices: + self.devices[device_id]['battery_level'] = max(0.0, min(100.0, battery_level)) + except Exception as e: + print(f"✗ 设备电池电量更新失败: {e}") + self.stats['device_errors'] += 1 + + def update_device_signal(self, device_id: int, signal_strength: float): + """ + 更新设备信号强度 + + Args: + device_id: 设备ID + signal_strength: 信号强度百分比(0-100) + """ + try: + if device_id in self.devices: + self.devices[device_id]['signal_strength'] = max(0.0, min(100.0, signal_strength)) + except Exception as e: + print(f"✗ 设备信号强度更新失败: {e}") + self.stats['device_errors'] += 1 + + def set_device_config(self, device_id: int, config: Dict[str, Any]) -> bool: + """ + 设置设备配置 + + Args: + device_id: 设备ID + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + if device_id in self.devices: + self.devices[device_id]['config'].update(config) + print(f"✓ 设备配置已更新: ID {device_id}") + return True + else: + print(f"✗ 设备不存在: ID {device_id}") + return False + except Exception as e: + print(f"✗ 设备配置设置失败: {e}") + self.stats['device_errors'] += 1 + return False + + def get_device_config(self, device_id: int) -> Optional[Dict[str, Any]]: + """ + 获取设备配置 + + Args: + device_id: 设备ID + + Returns: + 配置字典或None + """ + try: + if device_id in self.devices: + return self.devices[device_id].get('config', {}) + return None + except Exception as e: + print(f"✗ 设备配置获取失败: {e}") + return None + + def calibrate_device(self, device_id: int) -> bool: + """ + 校准设备 + + Args: + device_id: 设备ID + + Returns: + 是否校准成功 + """ + try: + if device_id not in self.devices: + print(f"✗ 设备不存在: ID {device_id}") + return False + + device_info = self.devices[device_id] + print(f"✓ 设备校准完成: {device_info['name']} (ID: {device_id})") + + # 更新校准时间 + device_info['last_calibrated'] = time.time() + return True + + except Exception as e: + print(f"✗ 设备校准失败: {e}") + self.stats['device_errors'] += 1 + return False + + def _trigger_device_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发设备回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.device_callbacks: + for callback in self.device_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 设备回调执行失败: {e}") + except Exception as e: + print(f"✗ 设备回调触发失败: {e}") + + def register_device_callback(self, callback_type: str, callback: callable): + """ + 注册设备回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.device_callbacks: + self.device_callbacks[callback_type].append(callback) + print(f"✓ 设备回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 设备回调注册失败: {e}") + + def unregister_device_callback(self, callback_type: str, callback: callable): + """ + 注销设备回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.device_callbacks: + if callback in self.device_callbacks[callback_type]: + self.device_callbacks[callback_type].remove(callback) + print(f"✓ 设备回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 设备回调注销失败: {e}") + + def get_device_capabilities(self, device_type: str) -> List[str]: + """ + 获取设备类型的功能列表 + + Args: + device_type: 设备类型 + + Returns: + 功能列表 + """ + return self.device_capabilities.get(device_type, []) + + def get_device_zones(self, device_type: str) -> List[str]: + """ + 获取设备区域列表 + + Args: + device_type: 设备类型 + + Returns: + 区域列表 + """ + return self.device_zones.get(device_type, []) + + def get_platform_config(self) -> Dict[str, Any]: + """ + 获取平台配置 + + Returns: + 平台配置字典 + """ + current_platform = platform.system() + return self.platform_config.get(current_platform, {}) + + def set_discovery_config(self, config: Dict[str, Any]): + """ + 设置设备发现配置 + + Args: + config: 发现配置字典 + """ + try: + self.discovery_config.update(config) + print(f"✓ 设备发现配置已更新: {self.discovery_config}") + except Exception as e: + print(f"✗ 设备发现配置设置失败: {e}") + self.stats['device_errors'] += 1 + + def get_discovery_config(self) -> Dict[str, Any]: + """ + 获取设备发现配置 + + Returns: + 发现配置字典 + """ + return self.discovery_config.copy() + + def get_device_stats(self) -> Dict[str, Any]: + """ + 获取设备统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_device_stats(self): + """重置设备统计信息""" + try: + self.stats = { + 'devices_registered': 0, + 'devices_connected': 0, + 'devices_disconnected': 0, + 'scan_operations': 0, + 'haptic_effects': 0, + 'device_errors': 0, + 'data_transmitted': 0 + } + print("✓ 设备统计信息已重置") + except Exception as e: + print(f"✗ 设备统计信息重置失败: {e}") + + def get_device_states(self) -> Dict[str, Any]: + """ + 获取设备状态 + + Returns: + 设备状态字典 + """ return self.device_states.copy() \ No newline at end of file diff --git a/plugins/user/haptic_feedback_system/editor/haptic_editor.py b/plugins/user/haptic_feedback_system/editor/haptic_editor.py index aad1b523..e5789077 100644 --- a/plugins/user/haptic_feedback_system/editor/haptic_editor.py +++ b/plugins/user/haptic_feedback_system/editor/haptic_editor.py @@ -1,957 +1,957 @@ -""" -触觉编辑器模块 -提供可视化界面用于配置和测试触觉反馈效果 -""" - -import time -from typing import Dict, Any, List, Optional -from enum import Enum -import threading - -class EditorMode(Enum): - """编辑器模式枚举""" - DEVICE_MANAGER = "device_manager" - EFFECT_EDITOR = "effect_editor" - PROFILE_MANAGER = "profile_manager" - REALTIME_MONITOR = "realtime_monitor" - PHYSICS_INTEGRATION = "physics_integration" - AUDIO_MAPPING = "audio_mapping" - EVENT_MAPPING = "event_mapping" - -class HapticEditor: - """ - 触觉编辑器 - 提供可视化界面用于配置和测试触觉反馈效果 - """ - - def __init__(self, plugin): - """ - 初始化触觉编辑器 - - Args: - plugin: 触觉反馈系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 编辑器配置 - self.editor_config = { - 'window_width': 1024, - 'window_height': 768, - 'refresh_rate': 30.0, - 'enable_preview': True, - 'enable_realtime_update': True, - 'show_debug_info': False, - 'theme': 'dark' - } - - # 当前编辑器状态 - self.editor_state = { - 'current_mode': EditorMode.EFFECT_EDITOR, - 'selected_device': None, - 'selected_effect': None, - 'selected_profile': None, - 'is_recording': False, - 'is_testing': False, - 'preview_active': False - } - - # UI组件 - self.ui_components = { - 'main_window': None, - 'toolbar': None, - 'property_panel': None, - 'preview_panel': None, - 'status_bar': None, - 'device_view': None, - 'effect_editor': None, - 'profile_manager': None, - 'monitor_panel': None - } - - # 编辑器数据 - self.editor_data = { - 'device_cache': {}, - 'effect_cache': {}, - 'profile_cache': {}, - 'live_data': {}, - 'event_mapping_cache': {} - } - - # 编辑器操作历史 - self.operation_history = [] - self.history_pointer = -1 - self.max_history_size = 100 - - # 实时监控数据 - self.monitor_data = { - 'active_effects': [], - 'device_status': {}, - 'performance_metrics': {}, - 'event_log': [] - } - - # 回调函数 - self.editor_callbacks = { - 'device_selected': [], - 'effect_selected': [], - 'profile_selected': [], - 'mode_changed': [], - 'data_updated': [], - 'effect_tested': [] - } - - # 统计信息 - self.editor_stats = { - 'windows_opened': 0, - 'operations_performed': 0, - 'undo_operations': 0, - 'redo_operations': 0, - 'data_refreshed': 0, - 'preview_updates': 0 - } - - # 线程管理 - self.editor_thread = None - self.monitor_thread = None - self.running = False - - # 时间戳记录 - self.last_refresh_time = 0.0 - self.last_preview_update = 0.0 - - print("✓ 触觉编辑器已创建") - - def initialize(self) -> bool: - """ - 初始化触觉编辑器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化UI组件(模拟) - self._initialize_ui_components() - - # 加载缓存数据 - self._load_editor_cache() - - self.initialized = True - print("✓ 触觉编辑器初始化完成") - return True - - except Exception as e: - print(f"✗ 触觉编辑器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用触觉编辑器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 触觉编辑器未初始化") - return False - - self.enabled = True - self.running = True - - # 启动编辑器线程 - self.editor_thread = threading.Thread(target=self._editor_update_loop, daemon=True) - self.editor_thread.start() - - # 启动监控线程 - if self.editor_config['enable_realtime_update']: - self.monitor_thread = threading.Thread(target=self._monitor_update_loop, daemon=True) - self.monitor_thread.start() - - print("✓ 触觉编辑器已启用") - return True - - except Exception as e: - print(f"✗ 触觉编辑器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用触觉编辑器""" - try: - self.enabled = False - self.running = False - - # 等待线程结束 - if self.editor_thread and self.editor_thread.is_alive(): - self.editor_thread.join(timeout=2.0) - - if self.monitor_thread and self.monitor_thread.is_alive(): - self.monitor_thread.join(timeout=2.0) - - # 关闭UI组件 - self._close_ui_components() - - print("✓ 触觉编辑器已禁用") - - except Exception as e: - print(f"✗ 触觉编辑器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理触觉编辑器资源""" - try: - self.disable() - self.ui_components.clear() - self.editor_data.clear() - self.operation_history.clear() - self.monitor_data.clear() - self.editor_callbacks.clear() - self.initialized = False - print("✓ 触觉编辑器资源已清理") - - except Exception as e: - print(f"✗ 触觉编辑器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新触觉编辑器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新统计信息 - pass - - except Exception as e: - print(f"✗ 触觉编辑器更新失败: {e}") - import traceback - traceback.print_exc() - - def _editor_update_loop(self): - """编辑器更新循环""" - try: - refresh_interval = 1.0 / self.editor_config['refresh_rate'] - - while self.running: - if self.enabled: - self._update_editor_display() - - time.sleep(refresh_interval) - - except Exception as e: - print(f"✗ 编辑器更新循环失败: {e}") - - def _monitor_update_loop(self): - """监控更新循环""" - try: - monitor_interval = 0.1 # 100ms - - while self.running: - if self.enabled and self.editor_config['enable_realtime_update']: - self._update_monitor_data() - - time.sleep(monitor_interval) - - except Exception as e: - print(f"✗ 监控更新循环失败: {e}") - - def _initialize_ui_components(self): - """初始化UI组件""" - try: - # 在实际实现中,这里会创建实际的UI组件 - # 由于这是模拟实现,我们只创建占位符 - self.ui_components['main_window'] = "MainWindow" - self.ui_components['toolbar'] = "Toolbar" - self.ui_components['property_panel'] = "PropertyPanel" - self.ui_components['preview_panel'] = "PreviewPanel" - self.ui_components['status_bar'] = "StatusBar" - self.ui_components['device_view'] = "DeviceView" - self.ui_components['effect_editor'] = "EffectEditor" - self.ui_components['profile_manager'] = "ProfileManager" - self.ui_components['monitor_panel'] = "MonitorPanel" - - print("✓ UI组件初始化完成") - except Exception as e: - print(f"✗ UI组件初始化失败: {e}") - - def _close_ui_components(self): - """关闭UI组件""" - try: - # 在实际实现中,这里会关闭实际的UI组件 - self.ui_components.clear() - print("✓ UI组件已关闭") - except Exception as e: - print(f"✗ UI组件关闭失败: {e}") - - def _load_editor_cache(self): - """加载编辑器缓存""" - try: - # 模拟加载缓存数据 - if self.plugin.device_manager: - self.editor_data['device_cache'] = self.plugin.device_manager.get_all_input_devices() - - if self.plugin.effect_processor: - self.editor_data['effect_cache'] = self.plugin.effect_processor.get_all_custom_effects() - - if self.plugin.config_manager: - self.editor_data['profile_cache'] = self.plugin.config_manager.get_available_profiles() - - if self.plugin.event_handler: - self.editor_data['event_mapping_cache'] = self.plugin.event_handler.get_event_mapping() - - print("✓ 编辑器缓存加载完成") - except Exception as e: - print(f"✗ 编辑器缓存加载失败: {e}") - - def _update_editor_display(self): - """更新编辑器显示""" - try: - current_time = time.time() - - # 检查是否需要刷新 - if current_time - self.last_refresh_time >= 1.0 / self.editor_config['refresh_rate']: - # 更新设备列表 - self._refresh_device_list() - - # 更新属性面板 - self._refresh_property_panel() - - # 更新预览面板 - if self.editor_config['enable_preview']: - self._update_preview_panel() - - # 更新状态栏 - self._update_status_bar() - - self.last_refresh_time = current_time - self.editor_stats['data_refreshed'] += 1 - - except Exception as e: - print(f"✗ 编辑器显示更新失败: {e}") - - def _update_monitor_data(self): - """更新监控数据""" - try: - # 收集实时数据 - if self.plugin.haptic_manager: - active_effects = self.plugin.haptic_manager.active_effects - self.monitor_data['active_effects'] = list(active_effects.values()) - - if self.plugin.device_manager: - device_status = self.plugin.device_manager.get_connected_devices() - self.monitor_data['device_status'] = device_status - - if self.plugin.event_handler: - event_stats = self.plugin.event_handler.get_stats() - self.monitor_data['performance_metrics'] = event_stats - - # 保持事件日志大小 - max_log_size = 100 - if len(self.monitor_data['event_log']) > max_log_size: - self.monitor_data['event_log'] = self.monitor_data['event_log'][-max_log_size:] - - except Exception as e: - print(f"✗ 监控数据更新失败: {e}") - - def _refresh_device_list(self): - """刷新设备列表""" - try: - if self.plugin.device_manager: - devices = self.plugin.device_manager.get_all_input_devices() - self.editor_data['device_cache'] = devices - # 在实际实现中,这里会更新UI组件 - except Exception as e: - print(f"✗ 设备列表刷新失败: {e}") - - def _refresh_property_panel(self): - """刷新属性面板""" - try: - # 根据当前选择更新属性面板 - selected_device = self.editor_state['selected_device'] - selected_effect = self.editor_state['selected_effect'] - - # 在实际实现中,这里会更新属性面板的显示内容 - pass - except Exception as e: - print(f"✗ 属性面板刷新失败: {e}") - - def _update_preview_panel(self): - """更新预览面板""" - try: - current_time = time.time() - if current_time - self.last_preview_update >= 0.1: # 100ms - # 更新预览数据 - self.editor_data['live_data'] = self._collect_live_data() - - # 在实际实现中,这里会更新预览面板的显示 - self.editor_stats['preview_updates'] += 1 - self.last_preview_update = current_time - - except Exception as e: - print(f"✗ 预览面板更新失败: {e}") - - def _collect_live_data(self) -> Dict[str, Any]: - """ - 收集实时数据 - - Returns: - 实时数据字典 - """ - try: - live_data = { - 'timestamp': time.time(), - 'devices': {}, - 'effects': {}, - 'performance': {} - } - - # 收集设备数据 - if self.plugin.device_manager: - live_data['devices'] = self.plugin.device_manager.get_device_stats() - - # 收集效果数据 - if self.plugin.effect_processor: - live_data['effects'] = self.plugin.effect_processor.get_stats() - - # 收集性能数据 - if self.plugin.haptic_manager: - live_data['performance'] = self.plugin.haptic_manager.get_stats() - - return live_data - - except Exception as e: - print(f"✗ 实时数据收集失败: {e}") - return {} - - def _update_status_bar(self): - """更新状态栏""" - try: - # 在实际实现中,这里会更新状态栏的显示内容 - pass - except Exception as e: - print(f"✗ 状态栏更新失败: {e}") - - def set_editor_mode(self, mode: EditorMode) -> bool: - """ - 设置编辑器模式 - - Args: - mode: 编辑器模式 - - Returns: - 是否设置成功 - """ - try: - old_mode = self.editor_state['current_mode'] - self.editor_state['current_mode'] = mode - - # 触发模式变更回调 - self._trigger_editor_callback('mode_changed', { - 'old_mode': old_mode.value, - 'new_mode': mode.value - }) - - print(f"✓ 编辑器模式已设置: {mode.value}") - return True - - except Exception as e: - print(f"✗ 编辑器模式设置失败: {e}") - return False - - def select_device(self, device_id: int) -> bool: - """ - 选择设备 - - Args: - device_id: 设备ID - - Returns: - 是否选择成功 - """ - try: - self.editor_state['selected_device'] = device_id - - # 触发设备选择回调 - self._trigger_editor_callback('device_selected', { - 'device_id': device_id - }) - - print(f"✓ 设备已选择: ID {device_id}") - return True - - except Exception as e: - print(f"✗ 设备选择失败: {e}") - return False - - def select_effect(self, effect_id: int) -> bool: - """ - 选择效果 - - Args: - effect_id: 效果ID - - Returns: - 是否选择成功 - """ - try: - self.editor_state['selected_effect'] = effect_id - - # 触发效果选择回调 - self._trigger_editor_callback('effect_selected', { - 'effect_id': effect_id - }) - - print(f"✓ 效果已选择: ID {effect_id}") - return True - - except Exception as e: - print(f"✗ 效果选择失败: {e}") - return False - - def select_profile(self, profile_name: str) -> bool: - """ - 选择预设 - - Args: - profile_name: 预设名称 - - Returns: - 是否选择成功 - """ - try: - self.editor_state['selected_profile'] = profile_name - - # 触发预设选择回调 - self._trigger_editor_callback('profile_selected', { - 'profile_name': profile_name - }) - - print(f"✓ 预设已选择: {profile_name}") - return True - - except Exception as e: - print(f"✗ 预设选择失败: {e}") - return False - - def start_recording(self) -> bool: - """ - 开始录制 - - Returns: - 是否开始成功 - """ - try: - self.editor_state['is_recording'] = True - print("✓ 录制已开始") - return True - - except Exception as e: - print(f"✗ 录制开始失败: {e}") - return False - - def stop_recording(self) -> bool: - """ - 停止录制 - - Returns: - 是否停止成功 - """ - try: - self.editor_state['is_recording'] = False - print("✓ 录制已停止") - return True - - except Exception as e: - print(f"✗ 录制停止失败: {e}") - return False - - def start_testing(self) -> bool: - """ - 开始测试 - - Returns: - 是否开始成功 - """ - try: - self.editor_state['is_testing'] = True - print("✓ 测试已开始") - return True - - except Exception as e: - print(f"✗ 测试开始失败: {e}") - return False - - def stop_testing(self) -> bool: - """ - 停止测试 - - Returns: - 是否停止成功 - """ - try: - self.editor_state['is_testing'] = False - print("✓ 测试已停止") - return True - - except Exception as e: - print(f"✗ 测试停止失败: {e}") - return False - - def test_effect(self, effect_data: Dict[str, Any]) -> bool: - """ - 测试效果 - - Args: - effect_data: 效果数据 - - Returns: - 是否测试成功 - """ - try: - if not self.plugin.haptic_manager: - print("✗ 触觉管理器未初始化") - return False - - # 播放测试效果 - effect_id = self.plugin.haptic_manager.play_effect( - effect_type=effect_data.get('type', 'simple'), - device_id=self.editor_state['selected_device'], - intensity=effect_data.get('intensity', 1.0), - duration=effect_data.get('duration', 1.0), - frequency=effect_data.get('frequency', 1.0), - parameters=effect_data.get('parameters', {}) - ) - - if effect_id >= 0: - # 触发效果测试回调 - self._trigger_editor_callback('effect_tested', { - 'effect_id': effect_id, - 'effect_data': effect_data - }) - - print("✓ 效果测试已开始") - return True - else: - print("✗ 效果测试失败") - return False - - except Exception as e: - print(f"✗ 效果测试失败: {e}") - return False - - def toggle_preview(self, enable: bool = None) -> bool: - """ - 切换预览 - - Args: - enable: 是否启用预览(None表示切换) - - Returns: - 是否切换成功 - """ - try: - if enable is None: - enable = not self.editor_state['preview_active'] - - self.editor_state['preview_active'] = enable - self.editor_config['enable_preview'] = enable - - status = "启用" if enable else "禁用" - print(f"✓ 预览已{status}") - return True - - except Exception as e: - print(f"✗ 预览切换失败: {e}") - return False - - def undo_operation(self) -> bool: - """ - 撤销操作 - - Returns: - 是否撤销成功 - """ - try: - if self.history_pointer >= 0 and self.operation_history: - # 执行撤销操作 - operation = self.operation_history[self.history_pointer] - self.history_pointer -= 1 - self.editor_stats['undo_operations'] += 1 - - print("✓ 操作已撤销") - return True - else: - print("⚠ 没有可撤销的操作") - return False - - except Exception as e: - print(f"✗ 操作撤销失败: {e}") - return False - - def redo_operation(self) -> bool: - """ - 重做操作 - - Returns: - 是否重做成功 - """ - try: - if self.history_pointer < len(self.operation_history) - 1: - # 执行重做操作 - self.history_pointer += 1 - operation = self.operation_history[self.history_pointer] - self.editor_stats['redo_operations'] += 1 - - print("✓ 操作已重做") - return True - else: - print("⚠ 没有可重做的操作") - return False - - except Exception as e: - print(f"✗ 操作重做失败: {e}") - return False - - def add_operation_to_history(self, operation: Dict[str, Any]): - """ - 添加操作到历史 - - Args: - operation: 操作数据 - """ - try: - # 添加到历史记录 - self.operation_history.append(operation) - self.history_pointer = len(self.operation_history) - 1 - - # 保持历史记录大小 - if len(self.operation_history) > self.max_history_size: - remove_count = len(self.operation_history) - self.max_history_size - self.operation_history = self.operation_history[remove_count:] - self.history_pointer -= remove_count - self.history_pointer = max(0, self.history_pointer) - - self.editor_stats['operations_performed'] += 1 - - except Exception as e: - print(f"✗ 操作添加到历史失败: {e}") - - def _trigger_editor_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发编辑器回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.editor_callbacks: - for callback in self.editor_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 编辑器回调执行失败: {e}") - except Exception as e: - print(f"✗ 编辑器回调触发失败: {e}") - - def register_editor_callback(self, callback_type: str, callback: callable): - """ - 注册编辑器回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.editor_callbacks: - self.editor_callbacks[callback_type].append(callback) - print(f"✓ 编辑器回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 编辑器回调注册失败: {e}") - - def unregister_editor_callback(self, callback_type: str, callback: callable): - """ - 注销编辑器回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.editor_callbacks: - if callback in self.editor_callbacks[callback_type]: - self.editor_callbacks[callback_type].remove(callback) - print(f"✓ 编辑器回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 编辑器回调注销失败: {e}") - - def get_editor_state(self) -> Dict[str, Any]: - """ - 获取编辑器状态 - - Returns: - 编辑器状态字典 - """ - return self.editor_state.copy() - - def get_editor_config(self) -> Dict[str, Any]: - """ - 获取编辑器配置 - - Returns: - 编辑器配置字典 - """ - return self.editor_config.copy() - - def set_editor_config(self, config: Dict[str, Any]) -> bool: - """ - 设置编辑器配置 - - Args: - config: 编辑器配置字典 - - Returns: - 是否设置成功 - """ - try: - self.editor_config.update(config) - print(f"✓ 编辑器配置已更新: {self.editor_config}") - return True - except Exception as e: - print(f"✗ 编辑器配置设置失败: {e}") - return False - - def get_editor_data(self, data_type: str) -> Optional[Dict[str, Any]]: - """ - 获取编辑器数据 - - Args: - data_type: 数据类型 - - Returns: - 数据字典或None - """ - return self.editor_data.get(data_type) - - def get_monitor_data(self) -> Dict[str, Any]: - """ - 获取监控数据 - - Returns: - 监控数据字典 - """ - return self.monitor_data.copy() - - def get_editor_stats(self) -> Dict[str, int]: - """ - 获取编辑器统计信息 - - Returns: - 编辑器统计字典 - """ - return self.editor_stats.copy() - - def reset_editor_stats(self): - """重置编辑器统计信息""" - try: - self.editor_stats = { - 'windows_opened': 0, - 'operations_performed': 0, - 'undo_operations': 0, - 'redo_operations': 0, - 'data_refreshed': 0, - 'preview_updates': 0 - } - print("✓ 编辑器统计信息已重置") - except Exception as e: - print(f"✗ 编辑器统计信息重置失败: {e}") - - def refresh_editor_data(self): - """刷新编辑器数据""" - try: - self._load_editor_cache() - self._update_editor_display() - print("✓ 编辑器数据已刷新") - except Exception as e: - print(f"✗ 编辑器数据刷新失败: {e}") - - def show_editor_window(self) -> bool: - """ - 显示编辑器窗口 - - Returns: - 是否显示成功 - """ - try: - # 在实际实现中,这里会显示实际的编辑器窗口 - self.editor_stats['windows_opened'] += 1 - print("✓ 编辑器窗口已显示") - return True - except Exception as e: - print(f"✗ 编辑器窗口显示失败: {e}") - return False - - def hide_editor_window(self) -> bool: - """ - 隐藏编辑器窗口 - - Returns: - 是否隐藏成功 - """ - try: - # 在实际实现中,这里会隐藏实际的编辑器窗口 - print("✓ 编辑器窗口已隐藏") - return True - except Exception as e: - print(f"✗ 编辑器窗口隐藏失败: {e}") - return False - - def export_editor_data(self, file_path: str) -> bool: - """ - 导出编辑器数据 - - Args: - file_path: 导出文件路径 - - Returns: - 是否导出成功 - """ - try: - # 在实际实现中,这里会导出编辑器数据到文件 - print(f"✓ 编辑器数据已导出: {file_path}") - return True - except Exception as e: - print(f"✗ 编辑器数据导出失败: {e}") - return False - - def import_editor_data(self, file_path: str) -> bool: - """ - 导入编辑器数据 - - Args: - file_path: 导入文件路径 - - Returns: - 是否导入成功 - """ - try: - # 在实际实现中,这里会从文件导入编辑器数据 - print(f"✓ 编辑器数据已导入: {file_path}") - return True - except Exception as e: - print(f"✗ 编辑器数据导入失败: {e}") +""" +触觉编辑器模块 +提供可视化界面用于配置和测试触觉反馈效果 +""" + +import time +from typing import Dict, Any, List, Optional +from enum import Enum +import threading + +class EditorMode(Enum): + """编辑器模式枚举""" + DEVICE_MANAGER = "device_manager" + EFFECT_EDITOR = "effect_editor" + PROFILE_MANAGER = "profile_manager" + REALTIME_MONITOR = "realtime_monitor" + PHYSICS_INTEGRATION = "physics_integration" + AUDIO_MAPPING = "audio_mapping" + EVENT_MAPPING = "event_mapping" + +class HapticEditor: + """ + 触觉编辑器 + 提供可视化界面用于配置和测试触觉反馈效果 + """ + + def __init__(self, plugin): + """ + 初始化触觉编辑器 + + Args: + plugin: 触觉反馈系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 编辑器配置 + self.editor_config = { + 'window_width': 1024, + 'window_height': 768, + 'refresh_rate': 30.0, + 'enable_preview': True, + 'enable_realtime_update': True, + 'show_debug_info': False, + 'theme': 'dark' + } + + # 当前编辑器状态 + self.editor_state = { + 'current_mode': EditorMode.EFFECT_EDITOR, + 'selected_device': None, + 'selected_effect': None, + 'selected_profile': None, + 'is_recording': False, + 'is_testing': False, + 'preview_active': False + } + + # UI组件 + self.ui_components = { + 'main_window': None, + 'toolbar': None, + 'property_panel': None, + 'preview_panel': None, + 'status_bar': None, + 'device_view': None, + 'effect_editor': None, + 'profile_manager': None, + 'monitor_panel': None + } + + # 编辑器数据 + self.editor_data = { + 'device_cache': {}, + 'effect_cache': {}, + 'profile_cache': {}, + 'live_data': {}, + 'event_mapping_cache': {} + } + + # 编辑器操作历史 + self.operation_history = [] + self.history_pointer = -1 + self.max_history_size = 100 + + # 实时监控数据 + self.monitor_data = { + 'active_effects': [], + 'device_status': {}, + 'performance_metrics': {}, + 'event_log': [] + } + + # 回调函数 + self.editor_callbacks = { + 'device_selected': [], + 'effect_selected': [], + 'profile_selected': [], + 'mode_changed': [], + 'data_updated': [], + 'effect_tested': [] + } + + # 统计信息 + self.editor_stats = { + 'windows_opened': 0, + 'operations_performed': 0, + 'undo_operations': 0, + 'redo_operations': 0, + 'data_refreshed': 0, + 'preview_updates': 0 + } + + # 线程管理 + self.editor_thread = None + self.monitor_thread = None + self.running = False + + # 时间戳记录 + self.last_refresh_time = 0.0 + self.last_preview_update = 0.0 + + print("✓ 触觉编辑器已创建") + + def initialize(self) -> bool: + """ + 初始化触觉编辑器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化UI组件(模拟) + self._initialize_ui_components() + + # 加载缓存数据 + self._load_editor_cache() + + self.initialized = True + print("✓ 触觉编辑器初始化完成") + return True + + except Exception as e: + print(f"✗ 触觉编辑器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用触觉编辑器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 触觉编辑器未初始化") + return False + + self.enabled = True + self.running = True + + # 启动编辑器线程 + self.editor_thread = threading.Thread(target=self._editor_update_loop, daemon=True) + self.editor_thread.start() + + # 启动监控线程 + if self.editor_config['enable_realtime_update']: + self.monitor_thread = threading.Thread(target=self._monitor_update_loop, daemon=True) + self.monitor_thread.start() + + print("✓ 触觉编辑器已启用") + return True + + except Exception as e: + print(f"✗ 触觉编辑器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用触觉编辑器""" + try: + self.enabled = False + self.running = False + + # 等待线程结束 + if self.editor_thread and self.editor_thread.is_alive(): + self.editor_thread.join(timeout=2.0) + + if self.monitor_thread and self.monitor_thread.is_alive(): + self.monitor_thread.join(timeout=2.0) + + # 关闭UI组件 + self._close_ui_components() + + print("✓ 触觉编辑器已禁用") + + except Exception as e: + print(f"✗ 触觉编辑器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理触觉编辑器资源""" + try: + self.disable() + self.ui_components.clear() + self.editor_data.clear() + self.operation_history.clear() + self.monitor_data.clear() + self.editor_callbacks.clear() + self.initialized = False + print("✓ 触觉编辑器资源已清理") + + except Exception as e: + print(f"✗ 触觉编辑器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新触觉编辑器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新统计信息 + pass + + except Exception as e: + print(f"✗ 触觉编辑器更新失败: {e}") + import traceback + traceback.print_exc() + + def _editor_update_loop(self): + """编辑器更新循环""" + try: + refresh_interval = 1.0 / self.editor_config['refresh_rate'] + + while self.running: + if self.enabled: + self._update_editor_display() + + time.sleep(refresh_interval) + + except Exception as e: + print(f"✗ 编辑器更新循环失败: {e}") + + def _monitor_update_loop(self): + """监控更新循环""" + try: + monitor_interval = 0.1 # 100ms + + while self.running: + if self.enabled and self.editor_config['enable_realtime_update']: + self._update_monitor_data() + + time.sleep(monitor_interval) + + except Exception as e: + print(f"✗ 监控更新循环失败: {e}") + + def _initialize_ui_components(self): + """初始化UI组件""" + try: + # 在实际实现中,这里会创建实际的UI组件 + # 由于这是模拟实现,我们只创建占位符 + self.ui_components['main_window'] = "MainWindow" + self.ui_components['toolbar'] = "Toolbar" + self.ui_components['property_panel'] = "PropertyPanel" + self.ui_components['preview_panel'] = "PreviewPanel" + self.ui_components['status_bar'] = "StatusBar" + self.ui_components['device_view'] = "DeviceView" + self.ui_components['effect_editor'] = "EffectEditor" + self.ui_components['profile_manager'] = "ProfileManager" + self.ui_components['monitor_panel'] = "MonitorPanel" + + print("✓ UI组件初始化完成") + except Exception as e: + print(f"✗ UI组件初始化失败: {e}") + + def _close_ui_components(self): + """关闭UI组件""" + try: + # 在实际实现中,这里会关闭实际的UI组件 + self.ui_components.clear() + print("✓ UI组件已关闭") + except Exception as e: + print(f"✗ UI组件关闭失败: {e}") + + def _load_editor_cache(self): + """加载编辑器缓存""" + try: + # 模拟加载缓存数据 + if self.plugin.device_manager: + self.editor_data['device_cache'] = self.plugin.device_manager.get_all_input_devices() + + if self.plugin.effect_processor: + self.editor_data['effect_cache'] = self.plugin.effect_processor.get_all_custom_effects() + + if self.plugin.config_manager: + self.editor_data['profile_cache'] = self.plugin.config_manager.get_available_profiles() + + if self.plugin.event_handler: + self.editor_data['event_mapping_cache'] = self.plugin.event_handler.get_event_mapping() + + print("✓ 编辑器缓存加载完成") + except Exception as e: + print(f"✗ 编辑器缓存加载失败: {e}") + + def _update_editor_display(self): + """更新编辑器显示""" + try: + current_time = time.time() + + # 检查是否需要刷新 + if current_time - self.last_refresh_time >= 1.0 / self.editor_config['refresh_rate']: + # 更新设备列表 + self._refresh_device_list() + + # 更新属性面板 + self._refresh_property_panel() + + # 更新预览面板 + if self.editor_config['enable_preview']: + self._update_preview_panel() + + # 更新状态栏 + self._update_status_bar() + + self.last_refresh_time = current_time + self.editor_stats['data_refreshed'] += 1 + + except Exception as e: + print(f"✗ 编辑器显示更新失败: {e}") + + def _update_monitor_data(self): + """更新监控数据""" + try: + # 收集实时数据 + if self.plugin.haptic_manager: + active_effects = self.plugin.haptic_manager.active_effects + self.monitor_data['active_effects'] = list(active_effects.values()) + + if self.plugin.device_manager: + device_status = self.plugin.device_manager.get_connected_devices() + self.monitor_data['device_status'] = device_status + + if self.plugin.event_handler: + event_stats = self.plugin.event_handler.get_stats() + self.monitor_data['performance_metrics'] = event_stats + + # 保持事件日志大小 + max_log_size = 100 + if len(self.monitor_data['event_log']) > max_log_size: + self.monitor_data['event_log'] = self.monitor_data['event_log'][-max_log_size:] + + except Exception as e: + print(f"✗ 监控数据更新失败: {e}") + + def _refresh_device_list(self): + """刷新设备列表""" + try: + if self.plugin.device_manager: + devices = self.plugin.device_manager.get_all_input_devices() + self.editor_data['device_cache'] = devices + # 在实际实现中,这里会更新UI组件 + except Exception as e: + print(f"✗ 设备列表刷新失败: {e}") + + def _refresh_property_panel(self): + """刷新属性面板""" + try: + # 根据当前选择更新属性面板 + selected_device = self.editor_state['selected_device'] + selected_effect = self.editor_state['selected_effect'] + + # 在实际实现中,这里会更新属性面板的显示内容 + pass + except Exception as e: + print(f"✗ 属性面板刷新失败: {e}") + + def _update_preview_panel(self): + """更新预览面板""" + try: + current_time = time.time() + if current_time - self.last_preview_update >= 0.1: # 100ms + # 更新预览数据 + self.editor_data['live_data'] = self._collect_live_data() + + # 在实际实现中,这里会更新预览面板的显示 + self.editor_stats['preview_updates'] += 1 + self.last_preview_update = current_time + + except Exception as e: + print(f"✗ 预览面板更新失败: {e}") + + def _collect_live_data(self) -> Dict[str, Any]: + """ + 收集实时数据 + + Returns: + 实时数据字典 + """ + try: + live_data = { + 'timestamp': time.time(), + 'devices': {}, + 'effects': {}, + 'performance': {} + } + + # 收集设备数据 + if self.plugin.device_manager: + live_data['devices'] = self.plugin.device_manager.get_device_stats() + + # 收集效果数据 + if self.plugin.effect_processor: + live_data['effects'] = self.plugin.effect_processor.get_stats() + + # 收集性能数据 + if self.plugin.haptic_manager: + live_data['performance'] = self.plugin.haptic_manager.get_stats() + + return live_data + + except Exception as e: + print(f"✗ 实时数据收集失败: {e}") + return {} + + def _update_status_bar(self): + """更新状态栏""" + try: + # 在实际实现中,这里会更新状态栏的显示内容 + pass + except Exception as e: + print(f"✗ 状态栏更新失败: {e}") + + def set_editor_mode(self, mode: EditorMode) -> bool: + """ + 设置编辑器模式 + + Args: + mode: 编辑器模式 + + Returns: + 是否设置成功 + """ + try: + old_mode = self.editor_state['current_mode'] + self.editor_state['current_mode'] = mode + + # 触发模式变更回调 + self._trigger_editor_callback('mode_changed', { + 'old_mode': old_mode.value, + 'new_mode': mode.value + }) + + print(f"✓ 编辑器模式已设置: {mode.value}") + return True + + except Exception as e: + print(f"✗ 编辑器模式设置失败: {e}") + return False + + def select_device(self, device_id: int) -> bool: + """ + 选择设备 + + Args: + device_id: 设备ID + + Returns: + 是否选择成功 + """ + try: + self.editor_state['selected_device'] = device_id + + # 触发设备选择回调 + self._trigger_editor_callback('device_selected', { + 'device_id': device_id + }) + + print(f"✓ 设备已选择: ID {device_id}") + return True + + except Exception as e: + print(f"✗ 设备选择失败: {e}") + return False + + def select_effect(self, effect_id: int) -> bool: + """ + 选择效果 + + Args: + effect_id: 效果ID + + Returns: + 是否选择成功 + """ + try: + self.editor_state['selected_effect'] = effect_id + + # 触发效果选择回调 + self._trigger_editor_callback('effect_selected', { + 'effect_id': effect_id + }) + + print(f"✓ 效果已选择: ID {effect_id}") + return True + + except Exception as e: + print(f"✗ 效果选择失败: {e}") + return False + + def select_profile(self, profile_name: str) -> bool: + """ + 选择预设 + + Args: + profile_name: 预设名称 + + Returns: + 是否选择成功 + """ + try: + self.editor_state['selected_profile'] = profile_name + + # 触发预设选择回调 + self._trigger_editor_callback('profile_selected', { + 'profile_name': profile_name + }) + + print(f"✓ 预设已选择: {profile_name}") + return True + + except Exception as e: + print(f"✗ 预设选择失败: {e}") + return False + + def start_recording(self) -> bool: + """ + 开始录制 + + Returns: + 是否开始成功 + """ + try: + self.editor_state['is_recording'] = True + print("✓ 录制已开始") + return True + + except Exception as e: + print(f"✗ 录制开始失败: {e}") + return False + + def stop_recording(self) -> bool: + """ + 停止录制 + + Returns: + 是否停止成功 + """ + try: + self.editor_state['is_recording'] = False + print("✓ 录制已停止") + return True + + except Exception as e: + print(f"✗ 录制停止失败: {e}") + return False + + def start_testing(self) -> bool: + """ + 开始测试 + + Returns: + 是否开始成功 + """ + try: + self.editor_state['is_testing'] = True + print("✓ 测试已开始") + return True + + except Exception as e: + print(f"✗ 测试开始失败: {e}") + return False + + def stop_testing(self) -> bool: + """ + 停止测试 + + Returns: + 是否停止成功 + """ + try: + self.editor_state['is_testing'] = False + print("✓ 测试已停止") + return True + + except Exception as e: + print(f"✗ 测试停止失败: {e}") + return False + + def test_effect(self, effect_data: Dict[str, Any]) -> bool: + """ + 测试效果 + + Args: + effect_data: 效果数据 + + Returns: + 是否测试成功 + """ + try: + if not self.plugin.haptic_manager: + print("✗ 触觉管理器未初始化") + return False + + # 播放测试效果 + effect_id = self.plugin.haptic_manager.play_effect( + effect_type=effect_data.get('type', 'simple'), + device_id=self.editor_state['selected_device'], + intensity=effect_data.get('intensity', 1.0), + duration=effect_data.get('duration', 1.0), + frequency=effect_data.get('frequency', 1.0), + parameters=effect_data.get('parameters', {}) + ) + + if effect_id >= 0: + # 触发效果测试回调 + self._trigger_editor_callback('effect_tested', { + 'effect_id': effect_id, + 'effect_data': effect_data + }) + + print("✓ 效果测试已开始") + return True + else: + print("✗ 效果测试失败") + return False + + except Exception as e: + print(f"✗ 效果测试失败: {e}") + return False + + def toggle_preview(self, enable: bool = None) -> bool: + """ + 切换预览 + + Args: + enable: 是否启用预览(None表示切换) + + Returns: + 是否切换成功 + """ + try: + if enable is None: + enable = not self.editor_state['preview_active'] + + self.editor_state['preview_active'] = enable + self.editor_config['enable_preview'] = enable + + status = "启用" if enable else "禁用" + print(f"✓ 预览已{status}") + return True + + except Exception as e: + print(f"✗ 预览切换失败: {e}") + return False + + def undo_operation(self) -> bool: + """ + 撤销操作 + + Returns: + 是否撤销成功 + """ + try: + if self.history_pointer >= 0 and self.operation_history: + # 执行撤销操作 + operation = self.operation_history[self.history_pointer] + self.history_pointer -= 1 + self.editor_stats['undo_operations'] += 1 + + print("✓ 操作已撤销") + return True + else: + print("⚠ 没有可撤销的操作") + return False + + except Exception as e: + print(f"✗ 操作撤销失败: {e}") + return False + + def redo_operation(self) -> bool: + """ + 重做操作 + + Returns: + 是否重做成功 + """ + try: + if self.history_pointer < len(self.operation_history) - 1: + # 执行重做操作 + self.history_pointer += 1 + operation = self.operation_history[self.history_pointer] + self.editor_stats['redo_operations'] += 1 + + print("✓ 操作已重做") + return True + else: + print("⚠ 没有可重做的操作") + return False + + except Exception as e: + print(f"✗ 操作重做失败: {e}") + return False + + def add_operation_to_history(self, operation: Dict[str, Any]): + """ + 添加操作到历史 + + Args: + operation: 操作数据 + """ + try: + # 添加到历史记录 + self.operation_history.append(operation) + self.history_pointer = len(self.operation_history) - 1 + + # 保持历史记录大小 + if len(self.operation_history) > self.max_history_size: + remove_count = len(self.operation_history) - self.max_history_size + self.operation_history = self.operation_history[remove_count:] + self.history_pointer -= remove_count + self.history_pointer = max(0, self.history_pointer) + + self.editor_stats['operations_performed'] += 1 + + except Exception as e: + print(f"✗ 操作添加到历史失败: {e}") + + def _trigger_editor_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发编辑器回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.editor_callbacks: + for callback in self.editor_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 编辑器回调执行失败: {e}") + except Exception as e: + print(f"✗ 编辑器回调触发失败: {e}") + + def register_editor_callback(self, callback_type: str, callback: callable): + """ + 注册编辑器回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.editor_callbacks: + self.editor_callbacks[callback_type].append(callback) + print(f"✓ 编辑器回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 编辑器回调注册失败: {e}") + + def unregister_editor_callback(self, callback_type: str, callback: callable): + """ + 注销编辑器回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.editor_callbacks: + if callback in self.editor_callbacks[callback_type]: + self.editor_callbacks[callback_type].remove(callback) + print(f"✓ 编辑器回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 编辑器回调注销失败: {e}") + + def get_editor_state(self) -> Dict[str, Any]: + """ + 获取编辑器状态 + + Returns: + 编辑器状态字典 + """ + return self.editor_state.copy() + + def get_editor_config(self) -> Dict[str, Any]: + """ + 获取编辑器配置 + + Returns: + 编辑器配置字典 + """ + return self.editor_config.copy() + + def set_editor_config(self, config: Dict[str, Any]) -> bool: + """ + 设置编辑器配置 + + Args: + config: 编辑器配置字典 + + Returns: + 是否设置成功 + """ + try: + self.editor_config.update(config) + print(f"✓ 编辑器配置已更新: {self.editor_config}") + return True + except Exception as e: + print(f"✗ 编辑器配置设置失败: {e}") + return False + + def get_editor_data(self, data_type: str) -> Optional[Dict[str, Any]]: + """ + 获取编辑器数据 + + Args: + data_type: 数据类型 + + Returns: + 数据字典或None + """ + return self.editor_data.get(data_type) + + def get_monitor_data(self) -> Dict[str, Any]: + """ + 获取监控数据 + + Returns: + 监控数据字典 + """ + return self.monitor_data.copy() + + def get_editor_stats(self) -> Dict[str, int]: + """ + 获取编辑器统计信息 + + Returns: + 编辑器统计字典 + """ + return self.editor_stats.copy() + + def reset_editor_stats(self): + """重置编辑器统计信息""" + try: + self.editor_stats = { + 'windows_opened': 0, + 'operations_performed': 0, + 'undo_operations': 0, + 'redo_operations': 0, + 'data_refreshed': 0, + 'preview_updates': 0 + } + print("✓ 编辑器统计信息已重置") + except Exception as e: + print(f"✗ 编辑器统计信息重置失败: {e}") + + def refresh_editor_data(self): + """刷新编辑器数据""" + try: + self._load_editor_cache() + self._update_editor_display() + print("✓ 编辑器数据已刷新") + except Exception as e: + print(f"✗ 编辑器数据刷新失败: {e}") + + def show_editor_window(self) -> bool: + """ + 显示编辑器窗口 + + Returns: + 是否显示成功 + """ + try: + # 在实际实现中,这里会显示实际的编辑器窗口 + self.editor_stats['windows_opened'] += 1 + print("✓ 编辑器窗口已显示") + return True + except Exception as e: + print(f"✗ 编辑器窗口显示失败: {e}") + return False + + def hide_editor_window(self) -> bool: + """ + 隐藏编辑器窗口 + + Returns: + 是否隐藏成功 + """ + try: + # 在实际实现中,这里会隐藏实际的编辑器窗口 + print("✓ 编辑器窗口已隐藏") + return True + except Exception as e: + print(f"✗ 编辑器窗口隐藏失败: {e}") + return False + + def export_editor_data(self, file_path: str) -> bool: + """ + 导出编辑器数据 + + Args: + file_path: 导出文件路径 + + Returns: + 是否导出成功 + """ + try: + # 在实际实现中,这里会导出编辑器数据到文件 + print(f"✓ 编辑器数据已导出: {file_path}") + return True + except Exception as e: + print(f"✗ 编辑器数据导出失败: {e}") + return False + + def import_editor_data(self, file_path: str) -> bool: + """ + 导入编辑器数据 + + Args: + file_path: 导入文件路径 + + Returns: + 是否导入成功 + """ + try: + # 在实际实现中,这里会从文件导入编辑器数据 + print(f"✓ 编辑器数据已导入: {file_path}") + return True + except Exception as e: + print(f"✗ 编辑器数据导入失败: {e}") return False \ No newline at end of file diff --git a/plugins/user/haptic_feedback_system/effects/effect_processor.py b/plugins/user/haptic_feedback_system/effects/effect_processor.py index de1015b7..d761a5e6 100644 --- a/plugins/user/haptic_feedback_system/effects/effect_processor.py +++ b/plugins/user/haptic_feedback_system/effects/effect_processor.py @@ -1,1180 +1,1180 @@ -""" -效果处理器模块 -负责创建和处理各种触觉效果 -""" - -import time -import math -from typing import Dict, Any, List, Optional -import threading -import random - -class EffectType: - """效果类型常量""" - SIMPLE = "simple" - PERIODIC = "periodic" - ENVELOPE = "envelope" - RAMP = "ramp" - CUSTOM = "custom" - NOISE = "noise" - IMPULSE = "impulse" - EXPLOSION = "explosion" - TEXTURE = "texture" - WIND = "wind" - WATER = "water" - HEAT = "heat" - COLD = "cold" - VIBRATION = "vibration" - PULSE = "pulse" - -class EffectProcessor: - """ - 效果处理器 - 负责创建和处理各种触觉效果 - """ - - def __init__(self, plugin): - """ - 初始化效果处理器 - - Args: - plugin: 触觉反馈系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 效果模板 - self.effect_templates = {} - - # 自定义效果 - self.custom_effects = {} - self.effect_counter = 0 - - # 效果参数 - self.effect_parameters = { - 'simple': { - 'intensity': (0.0, 1.0), - 'duration': (0.01, 10.0) - }, - 'periodic': { - 'intensity': (0.0, 1.0), - 'duration': (0.01, 10.0), - 'frequency': (1.0, 100.0), - 'amplitude': (0.0, 1.0), - 'offset': (0.0, 1.0), - 'phase': (0.0, 360.0), - 'waveform': ['sine', 'square', 'triangle', 'sawtooth'] - }, - 'envelope': { - 'intensity': (0.0, 1.0), - 'duration': (0.01, 10.0), - 'attack_time': (0.0, 2.0), - 'attack_level': (0.0, 1.0), - 'fade_time': (0.0, 2.0), - 'fade_level': (0.0, 1.0), - 'sustain_level': (0.0, 1.0) - }, - 'ramp': { - 'intensity': (0.0, 1.0), - 'duration': (0.01, 10.0), - 'start_level': (0.0, 1.0), - 'end_level': (0.0, 1.0), - 'curve_type': ['linear', 'exponential', 'logarithmic'] - }, - 'noise': { - 'intensity': (0.0, 1.0), - 'duration': (0.01, 10.0), - 'frequency': (1.0, 100.0), - 'noise_type': ['white', 'pink', 'brown'] - }, - 'impulse': { - 'intensity': (0.0, 1.0), - 'duration': (0.001, 1.0), - 'sharpness': (0.0, 1.0), - 'shape': ['gaussian', 'exponential', 'linear'] - }, - 'explosion': { - 'intensity': (0.0, 1.0), - 'duration': (0.1, 5.0), - 'rise_time': (0.01, 1.0), - 'fall_time': (0.1, 2.0), - 'frequency_start': (20.0, 100.0), - 'frequency_end': (5.0, 50.0) - }, - 'texture': { - 'intensity': (0.0, 1.0), - 'duration': (0.1, 10.0), - 'frequency': (5.0, 50.0), - 'roughness': (0.0, 1.0), - 'spacing': (0.01, 1.0) - }, - 'wind': { - 'intensity': (0.0, 1.0), - 'duration': (0.5, 30.0), - 'frequency': (0.1, 10.0), - 'turbulence': (0.0, 1.0), - 'direction_variability': (0.0, 1.0) - }, - 'water': { - 'intensity': (0.0, 1.0), - 'duration': (0.5, 30.0), - 'frequency': (0.5, 20.0), - 'wave_height': (0.1, 5.0), - 'viscosity': (0.1, 10.0) - } - } - - # 预定义效果 - self.predefined_effects = { - 'click': { - 'type': EffectType.IMPULSE, - 'intensity': 0.8, - 'duration': 0.05, - 'sharpness': 0.9, - 'shape': 'gaussian' - }, - 'buzz': { - 'type': EffectType.PERIODIC, - 'intensity': 0.6, - 'duration': 0.5, - 'frequency': 50.0, - 'amplitude': 0.5, - 'waveform': 'sine' - }, - 'rumble': { - 'type': EffectType.PERIODIC, - 'intensity': 0.7, - 'duration': 1.0, - 'frequency': 20.0, - 'amplitude': 0.6, - 'waveform': 'sine' - }, - 'pulse': { - 'type': EffectType.PERIODIC, - 'intensity': 0.5, - 'duration': 0.3, - 'frequency': 30.0, - 'amplitude': 0.4, - 'waveform': 'sine' - }, - 'heartbeat': { - 'type': EffectType.ENVELOPE, - 'intensity': 0.6, - 'duration': 1.5, - 'attack_time': 0.1, - 'attack_level': 0.8, - 'fade_time': 0.3, - 'fade_level': 0.2, - 'sustain_level': 0.5 - }, - 'explosion': { - 'type': EffectType.EXPLOSION, - 'intensity': 1.0, - 'duration': 2.0, - 'rise_time': 0.05, - 'fall_time': 0.8, - 'frequency_start': 80.0, - 'frequency_end': 10.0 - }, - 'impact': { - 'type': EffectType.IMPULSE, - 'intensity': 0.9, - 'duration': 0.1, - 'sharpness': 0.7, - 'shape': 'exponential' - }, - 'texture_rough': { - 'type': EffectType.TEXTURE, - 'intensity': 0.4, - 'duration': 2.0, - 'frequency': 15.0, - 'roughness': 0.8, - 'spacing': 0.05 - }, - 'texture_smooth': { - 'type': EffectType.TEXTURE, - 'intensity': 0.3, - 'duration': 2.0, - 'frequency': 8.0, - 'roughness': 0.2, - 'spacing': 0.1 - }, - 'wind_light': { - 'type': EffectType.WIND, - 'intensity': 0.2, - 'duration': 5.0, - 'frequency': 0.5, - 'turbulence': 0.3, - 'direction_variability': 0.2 - }, - 'wind_strong': { - 'type': EffectType.WIND, - 'intensity': 0.7, - 'duration': 5.0, - 'frequency': 2.0, - 'turbulence': 0.8, - 'direction_variability': 0.5 - }, - 'water_shallow': { - 'type': EffectType.WATER, - 'intensity': 0.3, - 'duration': 3.0, - 'frequency': 2.0, - 'wave_height': 0.5, - 'viscosity': 1.0 - }, - 'water_deep': { - 'type': EffectType.WATER, - 'intensity': 0.6, - 'duration': 3.0, - 'frequency': 0.8, - 'wave_height': 2.0, - 'viscosity': 3.0 - } - } - - # 效果混合 - self.effect_mixing = { - 'max': self._mix_max, - 'add': self._mix_add, - 'multiply': self._mix_multiply, - 'average': self._mix_average, - 'weighted': self._mix_weighted - } - - # 波形生成器 - self.waveform_generators = { - 'sine': self._generate_sine_wave, - 'square': self._generate_square_wave, - 'triangle': self._generate_triangle_wave, - 'sawtooth': self._generate_sawtooth_wave - } - - # 噪声生成器 - self.noise_generators = { - 'white': self._generate_white_noise, - 'pink': self._generate_pink_noise, - 'brown': self._generate_brown_noise - } - - # 脉冲形状生成器 - self.impulse_shapes = { - 'gaussian': self._generate_gaussian_impulse, - 'exponential': self._generate_exponential_impulse, - 'linear': self._generate_linear_impulse - } - - # 统计信息 - self.stats = { - 'templates_created': 0, - 'effects_generated': 0, - 'effects_processed': 0, - 'custom_effects': 0, - 'waveforms_generated': 0, - 'noise_samples': 0 - } - - # 回调函数 - self.effect_callbacks = { - 'effect_created': [], - 'effect_processed': [], - 'template_registered': [], - 'waveform_generated': [] - } - - # 缓存 - self.waveform_cache = {} - self.noise_cache = {} - - # 随机数生成器 - self.random_generator = random.Random() - - print("✓ 效果处理器已创建") - - def initialize(self) -> bool: - """ - 初始化效果处理器 - - Returns: - 是否初始化成功 - """ - try: - # 注册预定义效果为模板 - for name, effect_data in self.predefined_effects.items(): - self.register_effect_template(name, effect_data) - - # 初始化缓存 - self._initialize_cache() - - self.initialized = True - print("✓ 效果处理器初始化完成") - return True - - except Exception as e: - print(f"✗ 效果处理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用效果处理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 效果处理器未初始化") - return False - - self.enabled = True - print("✓ 效果处理器已启用") - return True - - except Exception as e: - print(f"✗ 效果处理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用效果处理器""" - try: - self.enabled = False - print("✓ 效果处理器已禁用") - - except Exception as e: - print(f"✗ 效果处理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理效果处理器资源""" - try: - self.disable() - self.effect_templates.clear() - self.custom_effects.clear() - self.effect_callbacks.clear() - self.waveform_cache.clear() - self.noise_cache.clear() - self.initialized = False - print("✓ 效果处理器资源已清理") - - except Exception as e: - print(f"✗ 效果处理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新效果处理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 清理缓存(定期清理以节省内存) - self._cleanup_cache() - - except Exception as e: - print(f"✗ 效果处理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _initialize_cache(self): - """初始化缓存""" - try: - # 预生成常用波形 - common_frequencies = [10, 20, 30, 50, 100] - common_durations = [0.1, 0.5, 1.0, 2.0] - - for freq in common_frequencies: - for duration in common_durations: - cache_key = f"sine_{freq}_{duration}" - self.waveform_cache[cache_key] = self._generate_sine_wave(freq, duration, 100) - - print("✓ 波形缓存初始化完成") - except Exception as e: - print(f"✗ 波形缓存初始化失败: {e}") - - def _cleanup_cache(self): - """清理缓存""" - try: - # 保持缓存大小在合理范围内 - max_cache_size = 1000 - - if len(self.waveform_cache) > max_cache_size: - # 移除最旧的一半缓存 - keys_to_remove = list(self.waveform_cache.keys())[:max_cache_size//2] - for key in keys_to_remove: - del self.waveform_cache[key] - - if len(self.noise_cache) > max_cache_size: - # 移除最旧的一半缓存 - keys_to_remove = list(self.noise_cache.keys())[:max_cache_size//2] - for key in keys_to_remove: - del self.noise_cache[key] - - except Exception as e: - print(f"✗ 缓存清理失败: {e}") - - def register_effect_template(self, name: str, template: Dict[str, Any]) -> bool: - """ - 注册效果模板 - - Args: - name: 模板名称 - template: 模板数据 - - Returns: - 是否注册成功 - """ - try: - self.effect_templates[name] = template.copy() - self.stats['templates_created'] += 1 - - # 触发模板注册回调 - self._trigger_effect_callback('template_registered', { - 'template_name': name, - 'template_data': template - }) - - print(f"✓ 效果模板已注册: {name}") - return True - - except Exception as e: - print(f"✗ 效果模板注册失败: {e}") - return False - - def unregister_effect_template(self, name: str) -> bool: - """ - 注销效果模板 - - Args: - name: 模板名称 - - Returns: - 是否注销成功 - """ - try: - if name in self.effect_templates: - del self.effect_templates[name] - print(f"✓ 效果模板已注销: {name}") - return True - else: - print(f"✗ 效果模板不存在: {name}") - return False - - except Exception as e: - print(f"✗ 效果模板注销失败: {e}") - return False - - def create_effect_from_template(self, template_name: str, - overrides: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: - """ - 从模板创建效果 - - Args: - template_name: 模板名称 - overrides: 覆盖参数 - - Returns: - 效果数据字典或None - """ - try: - if template_name not in self.effect_templates: - print(f"✗ 效果模板不存在: {template_name}") - return None - - # 获取模板数据 - template = self.effect_templates[template_name].copy() - - # 应用覆盖参数 - if overrides: - template.update(overrides) - - # 添加创建时间戳 - template['created_time'] = time.time() - - # 触发效果创建回调 - self._trigger_effect_callback('effect_created', { - 'template_name': template_name, - 'effect_data': template - }) - - self.stats['effects_generated'] += 1 - print(f"✓ 效果已从模板创建: {template_name}") - return template - - except Exception as e: - print(f"✗ 从模板创建效果失败: {e}") - return None - - def create_custom_effect(self, effect_type: str, parameters: Dict[str, Any]) -> int: - """ - 创建自定义效果 - - Args: - effect_type: 效果类型 - parameters: 效果参数 - - Returns: - 效果ID - """ - try: - effect_id = self.effect_counter - self.effect_counter += 1 - - custom_effect = { - 'id': effect_id, - 'type': effect_type, - 'parameters': parameters, - 'created_time': time.time() - } - - self.custom_effects[effect_id] = custom_effect - self.stats['custom_effects'] += 1 - - # 触发效果创建回调 - self._trigger_effect_callback('effect_created', { - 'effect_id': effect_id, - 'effect_data': custom_effect - }) - - print(f"✓ 自定义效果已创建: {effect_type} (ID: {effect_id})") - return effect_id - - except Exception as e: - print(f"✗ 自定义效果创建失败: {e}") - return -1 - - def remove_custom_effect(self, effect_id: int) -> bool: - """ - 移除自定义效果 - - Args: - effect_id: 效果ID - - Returns: - 是否移除成功 - """ - try: - if effect_id in self.custom_effects: - del self.custom_effects[effect_id] - self.stats['custom_effects'] -= 1 - print(f"✓ 自定义效果已移除: ID {effect_id}") - return True - else: - print(f"✗ 自定义效果不存在: ID {effect_id}") - return False - - except Exception as e: - print(f"✗ 自定义效果移除失败: {e}") - return False - - def get_custom_effect(self, effect_id: int) -> Optional[Dict[str, Any]]: - """ - 获取自定义效果 - - Args: - effect_id: 效果ID - - Returns: - 效果数据字典或None - """ - return self.custom_effects.get(effect_id) - - def get_all_custom_effects(self) -> Dict[int, Dict[str, Any]]: - """ - 获取所有自定义效果 - - Returns: - 所有自定义效果字典 - """ - return self.custom_effects.copy() - - def process_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: - """ - 处理效果并计算当前强度 - - Args: - effect_data: 效果数据 - time_progress: 时间进度(0.0-1.0) - - Returns: - 当前强度值(0.0-1.0) - """ - try: - effect_type = effect_data.get('type', EffectType.SIMPLE) - intensity = effect_data.get('intensity', 1.0) - - # 根据效果类型计算强度 - if effect_type == EffectType.SIMPLE: - current_intensity = self._process_simple_effect(effect_data, time_progress) - elif effect_type == EffectType.PERIODIC: - current_intensity = self._process_periodic_effect(effect_data, time_progress) - elif effect_type == EffectType.ENVELOPE: - current_intensity = self._process_envelope_effect(effect_data, time_progress) - elif effect_type == EffectType.RAMP: - current_intensity = self._process_ramp_effect(effect_data, time_progress) - elif effect_type == EffectType.NOISE: - current_intensity = self._process_noise_effect(effect_data, time_progress) - elif effect_type == EffectType.IMPULSE: - current_intensity = self._process_impulse_effect(effect_data, time_progress) - elif effect_type == EffectType.EXPLOSION: - current_intensity = self._process_explosion_effect(effect_data, time_progress) - elif effect_type == EffectType.TEXTURE: - current_intensity = self._process_texture_effect(effect_data, time_progress) - elif effect_type == EffectType.WIND: - current_intensity = self._process_wind_effect(effect_data, time_progress) - elif effect_type == EffectType.WATER: - current_intensity = self._process_water_effect(effect_data, time_progress) - elif effect_type == EffectType.CUSTOM: - current_intensity = self._process_custom_effect(effect_data, time_progress) - else: - current_intensity = intensity # 默认返回固定强度 - - # 应用总体强度 - current_intensity *= intensity - - # 触发效果处理回调 - self._trigger_effect_callback('effect_processed', { - 'effect_data': effect_data, - 'time_progress': time_progress, - 'current_intensity': current_intensity - }) - - self.stats['effects_processed'] += 1 - return max(0.0, min(1.0, current_intensity)) - - except Exception as e: - print(f"✗ 效果处理失败: {e}") - return 0.0 - - def _process_simple_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: - """处理简单效果""" - return 1.0 # 固定强度 - - def _process_periodic_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: - """处理周期性效果""" - try: - frequency = effect_data.get('frequency', 10.0) - amplitude = effect_data.get('amplitude', 1.0) - offset = effect_data.get('offset', 0.0) - phase = effect_data.get('phase', 0.0) - waveform = effect_data.get('waveform', 'sine') - duration = effect_data.get('duration', 1.0) - - # 生成波形 - generator = self.waveform_generators.get(waveform, self._generate_sine_wave) - cache_key = f"{waveform}_{frequency}_{duration}" - - # 检查缓存 - if cache_key in self.waveform_cache: - waveform_data = self.waveform_cache[cache_key] - else: - # 生成波形数据 - sample_count = max(100, int(frequency * duration * 10)) - waveform_data = generator(frequency, duration, sample_count) - self.waveform_cache[cache_key] = waveform_data - self.stats['waveforms_generated'] += 1 - - # 计算当前样本索引 - sample_index = int(time_progress * (len(waveform_data) - 1)) - sample_index = max(0, min(len(waveform_data) - 1, sample_index)) - - # 获取波形值 - waveform_value = waveform_data[sample_index] - - # 应用振幅和偏移 - result = offset + amplitude * waveform_value - - return max(0.0, min(1.0, result)) - except Exception as e: - print(f"✗ 周期性效果处理失败: {e}") - return 0.0 - - def _process_envelope_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: - """处理包络效果""" - try: - attack_time = effect_data.get('attack_time', 0.1) - attack_level = effect_data.get('attack_level', 1.0) - fade_time = effect_data.get('fade_time', 0.1) - fade_level = effect_data.get('fade_level', 0.0) - sustain_level = effect_data.get('sustain_level', 0.5) - duration = effect_data.get('duration', 1.0) - - if time_progress <= attack_time / duration: - # 攻击阶段 - attack_progress = time_progress / (attack_time / duration) - return attack_progress * attack_level - elif time_progress >= 1.0 - (fade_time / duration): - # 衰减阶段 - fade_progress = (time_progress - (1.0 - (fade_time / duration))) / (fade_time / duration) - return fade_level + (1.0 - fade_progress) * (sustain_level - fade_level) - else: - # 保持阶段 - return sustain_level - except Exception as e: - print(f"✗ 包络效果处理失败: {e}") - return 0.0 - - def _process_ramp_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: - """处理斜坡效果""" - try: - start_level = effect_data.get('start_level', 0.0) - end_level = effect_data.get('end_level', 1.0) - curve_type = effect_data.get('curve_type', 'linear') - - if curve_type == 'exponential': - # 指数曲线 - return start_level + (end_level - start_level) * (time_progress ** 2) - elif curve_type == 'logarithmic': - # 对数曲线 - if time_progress <= 0: - return start_level - return start_level + (end_level - start_level) * math.log10(1 + 9 * time_progress) - else: - # 线性曲线 - return start_level + (end_level - start_level) * time_progress - except Exception as e: - print(f"✗ 斜坡效果处理失败: {e}") - return 0.0 - - def _process_noise_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: - """处理噪声效果""" - try: - frequency = effect_data.get('frequency', 10.0) - noise_type = effect_data.get('noise_type', 'white') - duration = effect_data.get('duration', 1.0) - - # 生成噪声 - generator = self.noise_generators.get(noise_type, self._generate_white_noise) - cache_key = f"{noise_type}_{frequency}_{duration}" - - # 检查缓存 - if cache_key in self.noise_cache: - noise_data = self.noise_cache[cache_key] - else: - # 生成噪声数据 - sample_count = max(100, int(frequency * duration * 10)) - noise_data = generator(sample_count) - self.noise_cache[cache_key] = noise_data - self.stats['noise_samples'] += sample_count - - # 计算当前样本索引 - sample_index = int(time_progress * (len(noise_data) - 1)) - sample_index = max(0, min(len(noise_data) - 1, sample_index)) - - # 获取噪声值 - noise_value = noise_data[sample_index] - - return max(0.0, min(1.0, noise_value)) - except Exception as e: - print(f"✗ 噪声效果处理失败: {e}") - return 0.0 - - def _process_impulse_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: - """处理脉冲效果""" - try: - duration = effect_data.get('duration', 0.1) - sharpness = effect_data.get('sharpness', 0.5) - shape = effect_data.get('shape', 'gaussian') - - # 生成脉冲 - generator = self.impulse_shapes.get(shape, self._generate_gaussian_impulse) - - # 计算脉冲值 - impulse_value = generator(time_progress, sharpness) - - return max(0.0, min(1.0, impulse_value)) - except Exception as e: - print(f"✗ 脉冲效果处理失败: {e}") - return 0.0 - - def _process_explosion_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: - """处理爆炸效果""" - try: - rise_time = effect_data.get('rise_time', 0.05) - fall_time = effect_data.get('fall_time', 0.5) - frequency_start = effect_data.get('frequency_start', 80.0) - frequency_end = effect_data.get('frequency_end', 10.0) - duration = effect_data.get('duration', 1.0) - - # 计算频率变化 - frequency = frequency_start + (frequency_end - frequency_start) * time_progress - - # 计算振幅包络 - if time_progress <= rise_time / duration: - # 上升阶段 - envelope = time_progress / (rise_time / duration) - else: - # 下降阶段 - fall_progress = (time_progress - (rise_time / duration)) / ((duration - rise_time) / duration) - envelope = max(0.0, 1.0 - fall_progress) - - # 生成振动 - vibration = math.sin(2 * math.pi * frequency * time_progress * duration) - result = envelope * (vibration + 1.0) / 2.0 # 转换到0-1范围 - - return max(0.0, min(1.0, result)) - except Exception as e: - print(f"✗ 爆炸效果处理失败: {e}") - return 0.0 - - def _process_texture_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: - """处理纹理效果""" - try: - frequency = effect_data.get('frequency', 10.0) - roughness = effect_data.get('roughness', 0.5) - spacing = effect_data.get('spacing', 0.1) - - # 生成纹理模式 - texture_position = time_progress * frequency - texture_phase = texture_position - math.floor(texture_position) - - # 根据粗糙度调整强度 - if texture_phase < spacing: - # 纹理凸起 - intensity = 1.0 - (texture_phase / spacing) * roughness - else: - # 纹理凹陷 - intensity = 0.5 + 0.5 * roughness * math.sin(2 * math.pi * texture_phase) - - return max(0.0, min(1.0, intensity)) - except Exception as e: - print(f"✗ 纹理效果处理失败: {e}") - return 0.0 - - def _process_wind_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: - """处理风效果""" - try: - frequency = effect_data.get('frequency', 1.0) - turbulence = effect_data.get('turbulence', 0.5) - direction_variability = effect_data.get('direction_variability', 0.3) - - # 基础风模式 - base_wind = math.sin(2 * math.pi * frequency * time_progress) - - # 添加湍流 - turbulence_effect = turbulence * math.sin(2 * math.pi * frequency * 5 * time_progress) - - # 添加方向变化 - direction_effect = direction_variability * math.sin(2 * math.pi * frequency * 0.2 * time_progress) - - # 组合效果 - result = (base_wind + turbulence_effect + direction_effect) / 3.0 - result = (result + 1.0) / 2.0 # 转换到0-1范围 - - return max(0.0, min(1.0, result)) - except Exception as e: - print(f"✗ 风效果处理失败: {e}") - return 0.0 - - def _process_water_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: - """处理水效果""" - try: - frequency = effect_data.get('frequency', 1.0) - wave_height = effect_data.get('wave_height', 1.0) - viscosity = effect_data.get('viscosity', 1.0) - - # 主波浪 - main_wave = math.sin(2 * math.pi * frequency * time_progress) - - # 次级波浪 - secondary_wave = 0.5 * math.sin(2 * math.pi * frequency * 2.5 * time_progress + 0.5) - - # 粘性阻尼 - damping = math.exp(-viscosity * time_progress) - - # 组合效果 - result = (main_wave + secondary_wave) * damping * wave_height - result = (result + 1.0) / 2.0 # 转换到0-1范围 - - return max(0.0, min(1.0, result)) - except Exception as e: - print(f"✗ 水效果处理失败: {e}") - return 0.0 - - def _process_custom_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: - """处理自定义效果""" - # 这里可以实现更复杂的自定义效果处理逻辑 - parameters = effect_data.get('parameters', {}) - custom_intensity = parameters.get('intensity_multiplier', 1.0) - return custom_intensity # 默认返回固定强度 - - def _generate_sine_wave(self, frequency: float, duration: float, samples: int) -> List[float]: - """生成正弦波""" - wave = [] - for i in range(samples): - t = i / (samples - 1) * duration - value = math.sin(2 * math.pi * frequency * t) - wave.append(value) - return wave - - def _generate_square_wave(self, frequency: float, duration: float, samples: int) -> List[float]: - """生成方波""" - wave = [] - for i in range(samples): - t = i / (samples - 1) * duration - value = 1.0 if math.sin(2 * math.pi * frequency * t) >= 0 else -1.0 - wave.append(value) - return wave - - def _generate_triangle_wave(self, frequency: float, duration: float, samples: int) -> List[float]: - """生成三角波""" - wave = [] - for i in range(samples): - t = i / (samples - 1) * duration - value = 2 * abs(2 * (frequency * t - math.floor(frequency * t + 0.5))) - 1 - wave.append(value) - return wave - - def _generate_sawtooth_wave(self, frequency: float, duration: float, samples: int) -> List[float]: - """生成锯齿波""" - wave = [] - for i in range(samples): - t = i / (samples - 1) * duration - value = 2 * (frequency * t - math.floor(frequency * t + 0.5)) - wave.append(value) - return wave - - def _generate_white_noise(self, samples: int) -> List[float]: - """生成白噪声""" - noise = [] - for _ in range(samples): - value = self.random_generator.uniform(-1.0, 1.0) - noise.append(value) - return noise - - def _generate_pink_noise(self, samples: int) -> List[float]: - """生成粉红噪声""" - noise = [] - b = [0.0] * 7 - for _ in range(samples): - white = self.random_generator.uniform(-1.0, 1.0) - b[0] = 0.99886 * b[0] + white * 0.0555179 - b[1] = 0.99332 * b[1] + white * 0.0750759 - b[2] = 0.96900 * b[2] + white * 0.1538520 - b[3] = 0.86650 * b[3] + white * 0.3104856 - b[4] = 0.55000 * b[4] + white * 0.5329522 - b[5] = -0.7616 * b[5] - white * 0.0168980 - pink = b[0] + b[1] + b[2] + b[3] + b[4] + b[5] + b[6] + white * 0.5362 - b[6] = white * 0.115926 - noise.append(pink / 5.0) # 归一化 - return noise - - def _generate_brown_noise(self, samples: int) -> List[float]: - """生成布朗噪声""" - noise = [] - last = 0.0 - for _ in range(samples): - white = self.random_generator.uniform(-1.0, 1.0) - brown = (last + (0.02 * white)) / 1.02 - last = brown - noise.append(brown * 3.5) # 补偿幅度 - return noise - - def _generate_gaussian_impulse(self, time_progress: float, sharpness: float) -> float: - """生成高斯脉冲""" - try: - # 高斯函数: exp(-(x^2)/(2*sigma^2)) - sigma = 0.2 * (1.0 - sharpness * 0.8) # 根据锐度调整宽度 - x = (time_progress - 0.5) * 2 # 调整到-1到1范围 - value = math.exp(-(x*x) / (2 * sigma * sigma)) - return value - except Exception: - return 0.0 - - def _generate_exponential_impulse(self, time_progress: float, sharpness: float) -> float: - """生成指数脉冲""" - try: - if time_progress < 0.5: - # 上升阶段 - rise_factor = 1.0 + 9.0 * sharpness # 1到10 - value = 1.0 - math.exp(-rise_factor * time_progress * 2) - else: - # 下降阶段 - fall_factor = 1.0 + 9.0 * (1.0 - sharpness) # 1到10 - value = math.exp(-fall_factor * (time_progress - 0.5) * 2) - return value - except Exception: - return 0.0 - - def _generate_linear_impulse(self, time_progress: float, sharpness: float) -> float: - """生成线性脉冲""" - try: - peak_position = 0.3 + 0.4 * sharpness # 0.3到0.7 - if time_progress < peak_position: - # 上升阶段 - value = time_progress / peak_position - else: - # 下降阶段 - value = 1.0 - (time_progress - peak_position) / (1.0 - peak_position) - return max(0.0, value) - except Exception: - return 0.0 - - def mix_effects(self, effects: List[Dict[str, Any]], time_progress: float, - mix_mode: str = 'max', weights: List[float] = None) -> float: - """ - 混合多个效果 - - Args: - effects: 效果数据列表 - time_progress: 时间进度(0.0-1.0) - mix_mode: 混合模式('max', 'add', 'multiply', 'average', 'weighted') - weights: 权重列表(用于加权混合) - - Returns: - 混合后的强度值(0.0-1.0) - """ - try: - intensities = [] - for effect_data in effects: - intensity = self.process_effect(effect_data, time_progress) - intensities.append(intensity) - - if not intensities: - return 0.0 - - # 根据混合模式计算最终强度 - if mix_mode == 'weighted' and weights: - mix_function = self._mix_weighted - return mix_function(intensities, weights) - else: - mix_function = self.effect_mixing.get(mix_mode, self._mix_max) - return mix_function(intensities) - - except Exception as e: - print(f"✗ 效果混合失败: {e}") - return 0.0 - - def _mix_max(self, intensities: List[float]) -> float: - """最大值混合""" - return max(intensities) - - def _mix_add(self, intensities: List[float]) -> float: - """加法混合""" - return min(1.0, sum(intensities)) - - def _mix_multiply(self, intensities: List[float]) -> float: - """乘法混合""" - result = 1.0 - for intensity in intensities: - result *= max(0.0, intensity) # 避免负值影响 - return result - - def _mix_average(self, intensities: List[float]) -> float: - """平均值混合""" - return sum(intensities) / len(intensities) if intensities else 0.0 - - def _mix_weighted(self, intensities: List[float], weights: List[float]) -> float: - """加权混合""" - if len(intensities) != len(weights): - return self._mix_average(intensities) - - weighted_sum = sum(intensity * weight for intensity, weight in zip(intensities, weights)) - total_weight = sum(weights) - return weighted_sum / total_weight if total_weight > 0 else 0.0 - - def get_effect_parameters(self, effect_type: str) -> Dict[str, tuple]: - """ - 获取效果参数信息 - - Args: - effect_type: 效果类型 - - Returns: - 参数信息字典 - """ - return self.effect_parameters.get(effect_type, {}) - - def get_predefined_effects(self) -> Dict[str, Dict[str, Any]]: - """ - 获取预定义效果 - - Returns: - 预定义效果字典 - """ - return self.predefined_effects.copy() - - def _trigger_effect_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发效果回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.effect_callbacks: - for callback in self.effect_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 效果回调执行失败: {e}") - except Exception as e: - print(f"✗ 效果回调触发失败: {e}") - - def register_effect_callback(self, callback_type: str, callback: callable): - """ - 注册效果回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.effect_callbacks: - self.effect_callbacks[callback_type].append(callback) - print(f"✓ 效果回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 效果回调注册失败: {e}") - - def unregister_effect_callback(self, callback_type: str, callback: callable): - """ - 注销效果回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.effect_callbacks: - if callback in self.effect_callbacks[callback_type]: - self.effect_callbacks[callback_type].remove(callback) - print(f"✓ 效果回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 效果回调注销失败: {e}") - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - try: - self.stats = { - 'templates_created': 0, - 'effects_generated': 0, - 'effects_processed': 0, - 'custom_effects': 0, - 'waveforms_generated': 0, - 'noise_samples': 0 - } - print("✓ 效果处理器统计信息已重置") - except Exception as e: +""" +效果处理器模块 +负责创建和处理各种触觉效果 +""" + +import time +import math +from typing import Dict, Any, List, Optional +import threading +import random + +class EffectType: + """效果类型常量""" + SIMPLE = "simple" + PERIODIC = "periodic" + ENVELOPE = "envelope" + RAMP = "ramp" + CUSTOM = "custom" + NOISE = "noise" + IMPULSE = "impulse" + EXPLOSION = "explosion" + TEXTURE = "texture" + WIND = "wind" + WATER = "water" + HEAT = "heat" + COLD = "cold" + VIBRATION = "vibration" + PULSE = "pulse" + +class EffectProcessor: + """ + 效果处理器 + 负责创建和处理各种触觉效果 + """ + + def __init__(self, plugin): + """ + 初始化效果处理器 + + Args: + plugin: 触觉反馈系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 效果模板 + self.effect_templates = {} + + # 自定义效果 + self.custom_effects = {} + self.effect_counter = 0 + + # 效果参数 + self.effect_parameters = { + 'simple': { + 'intensity': (0.0, 1.0), + 'duration': (0.01, 10.0) + }, + 'periodic': { + 'intensity': (0.0, 1.0), + 'duration': (0.01, 10.0), + 'frequency': (1.0, 100.0), + 'amplitude': (0.0, 1.0), + 'offset': (0.0, 1.0), + 'phase': (0.0, 360.0), + 'waveform': ['sine', 'square', 'triangle', 'sawtooth'] + }, + 'envelope': { + 'intensity': (0.0, 1.0), + 'duration': (0.01, 10.0), + 'attack_time': (0.0, 2.0), + 'attack_level': (0.0, 1.0), + 'fade_time': (0.0, 2.0), + 'fade_level': (0.0, 1.0), + 'sustain_level': (0.0, 1.0) + }, + 'ramp': { + 'intensity': (0.0, 1.0), + 'duration': (0.01, 10.0), + 'start_level': (0.0, 1.0), + 'end_level': (0.0, 1.0), + 'curve_type': ['linear', 'exponential', 'logarithmic'] + }, + 'noise': { + 'intensity': (0.0, 1.0), + 'duration': (0.01, 10.0), + 'frequency': (1.0, 100.0), + 'noise_type': ['white', 'pink', 'brown'] + }, + 'impulse': { + 'intensity': (0.0, 1.0), + 'duration': (0.001, 1.0), + 'sharpness': (0.0, 1.0), + 'shape': ['gaussian', 'exponential', 'linear'] + }, + 'explosion': { + 'intensity': (0.0, 1.0), + 'duration': (0.1, 5.0), + 'rise_time': (0.01, 1.0), + 'fall_time': (0.1, 2.0), + 'frequency_start': (20.0, 100.0), + 'frequency_end': (5.0, 50.0) + }, + 'texture': { + 'intensity': (0.0, 1.0), + 'duration': (0.1, 10.0), + 'frequency': (5.0, 50.0), + 'roughness': (0.0, 1.0), + 'spacing': (0.01, 1.0) + }, + 'wind': { + 'intensity': (0.0, 1.0), + 'duration': (0.5, 30.0), + 'frequency': (0.1, 10.0), + 'turbulence': (0.0, 1.0), + 'direction_variability': (0.0, 1.0) + }, + 'water': { + 'intensity': (0.0, 1.0), + 'duration': (0.5, 30.0), + 'frequency': (0.5, 20.0), + 'wave_height': (0.1, 5.0), + 'viscosity': (0.1, 10.0) + } + } + + # 预定义效果 + self.predefined_effects = { + 'click': { + 'type': EffectType.IMPULSE, + 'intensity': 0.8, + 'duration': 0.05, + 'sharpness': 0.9, + 'shape': 'gaussian' + }, + 'buzz': { + 'type': EffectType.PERIODIC, + 'intensity': 0.6, + 'duration': 0.5, + 'frequency': 50.0, + 'amplitude': 0.5, + 'waveform': 'sine' + }, + 'rumble': { + 'type': EffectType.PERIODIC, + 'intensity': 0.7, + 'duration': 1.0, + 'frequency': 20.0, + 'amplitude': 0.6, + 'waveform': 'sine' + }, + 'pulse': { + 'type': EffectType.PERIODIC, + 'intensity': 0.5, + 'duration': 0.3, + 'frequency': 30.0, + 'amplitude': 0.4, + 'waveform': 'sine' + }, + 'heartbeat': { + 'type': EffectType.ENVELOPE, + 'intensity': 0.6, + 'duration': 1.5, + 'attack_time': 0.1, + 'attack_level': 0.8, + 'fade_time': 0.3, + 'fade_level': 0.2, + 'sustain_level': 0.5 + }, + 'explosion': { + 'type': EffectType.EXPLOSION, + 'intensity': 1.0, + 'duration': 2.0, + 'rise_time': 0.05, + 'fall_time': 0.8, + 'frequency_start': 80.0, + 'frequency_end': 10.0 + }, + 'impact': { + 'type': EffectType.IMPULSE, + 'intensity': 0.9, + 'duration': 0.1, + 'sharpness': 0.7, + 'shape': 'exponential' + }, + 'texture_rough': { + 'type': EffectType.TEXTURE, + 'intensity': 0.4, + 'duration': 2.0, + 'frequency': 15.0, + 'roughness': 0.8, + 'spacing': 0.05 + }, + 'texture_smooth': { + 'type': EffectType.TEXTURE, + 'intensity': 0.3, + 'duration': 2.0, + 'frequency': 8.0, + 'roughness': 0.2, + 'spacing': 0.1 + }, + 'wind_light': { + 'type': EffectType.WIND, + 'intensity': 0.2, + 'duration': 5.0, + 'frequency': 0.5, + 'turbulence': 0.3, + 'direction_variability': 0.2 + }, + 'wind_strong': { + 'type': EffectType.WIND, + 'intensity': 0.7, + 'duration': 5.0, + 'frequency': 2.0, + 'turbulence': 0.8, + 'direction_variability': 0.5 + }, + 'water_shallow': { + 'type': EffectType.WATER, + 'intensity': 0.3, + 'duration': 3.0, + 'frequency': 2.0, + 'wave_height': 0.5, + 'viscosity': 1.0 + }, + 'water_deep': { + 'type': EffectType.WATER, + 'intensity': 0.6, + 'duration': 3.0, + 'frequency': 0.8, + 'wave_height': 2.0, + 'viscosity': 3.0 + } + } + + # 效果混合 + self.effect_mixing = { + 'max': self._mix_max, + 'add': self._mix_add, + 'multiply': self._mix_multiply, + 'average': self._mix_average, + 'weighted': self._mix_weighted + } + + # 波形生成器 + self.waveform_generators = { + 'sine': self._generate_sine_wave, + 'square': self._generate_square_wave, + 'triangle': self._generate_triangle_wave, + 'sawtooth': self._generate_sawtooth_wave + } + + # 噪声生成器 + self.noise_generators = { + 'white': self._generate_white_noise, + 'pink': self._generate_pink_noise, + 'brown': self._generate_brown_noise + } + + # 脉冲形状生成器 + self.impulse_shapes = { + 'gaussian': self._generate_gaussian_impulse, + 'exponential': self._generate_exponential_impulse, + 'linear': self._generate_linear_impulse + } + + # 统计信息 + self.stats = { + 'templates_created': 0, + 'effects_generated': 0, + 'effects_processed': 0, + 'custom_effects': 0, + 'waveforms_generated': 0, + 'noise_samples': 0 + } + + # 回调函数 + self.effect_callbacks = { + 'effect_created': [], + 'effect_processed': [], + 'template_registered': [], + 'waveform_generated': [] + } + + # 缓存 + self.waveform_cache = {} + self.noise_cache = {} + + # 随机数生成器 + self.random_generator = random.Random() + + print("✓ 效果处理器已创建") + + def initialize(self) -> bool: + """ + 初始化效果处理器 + + Returns: + 是否初始化成功 + """ + try: + # 注册预定义效果为模板 + for name, effect_data in self.predefined_effects.items(): + self.register_effect_template(name, effect_data) + + # 初始化缓存 + self._initialize_cache() + + self.initialized = True + print("✓ 效果处理器初始化完成") + return True + + except Exception as e: + print(f"✗ 效果处理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用效果处理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 效果处理器未初始化") + return False + + self.enabled = True + print("✓ 效果处理器已启用") + return True + + except Exception as e: + print(f"✗ 效果处理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用效果处理器""" + try: + self.enabled = False + print("✓ 效果处理器已禁用") + + except Exception as e: + print(f"✗ 效果处理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理效果处理器资源""" + try: + self.disable() + self.effect_templates.clear() + self.custom_effects.clear() + self.effect_callbacks.clear() + self.waveform_cache.clear() + self.noise_cache.clear() + self.initialized = False + print("✓ 效果处理器资源已清理") + + except Exception as e: + print(f"✗ 效果处理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新效果处理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 清理缓存(定期清理以节省内存) + self._cleanup_cache() + + except Exception as e: + print(f"✗ 效果处理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _initialize_cache(self): + """初始化缓存""" + try: + # 预生成常用波形 + common_frequencies = [10, 20, 30, 50, 100] + common_durations = [0.1, 0.5, 1.0, 2.0] + + for freq in common_frequencies: + for duration in common_durations: + cache_key = f"sine_{freq}_{duration}" + self.waveform_cache[cache_key] = self._generate_sine_wave(freq, duration, 100) + + print("✓ 波形缓存初始化完成") + except Exception as e: + print(f"✗ 波形缓存初始化失败: {e}") + + def _cleanup_cache(self): + """清理缓存""" + try: + # 保持缓存大小在合理范围内 + max_cache_size = 1000 + + if len(self.waveform_cache) > max_cache_size: + # 移除最旧的一半缓存 + keys_to_remove = list(self.waveform_cache.keys())[:max_cache_size//2] + for key in keys_to_remove: + del self.waveform_cache[key] + + if len(self.noise_cache) > max_cache_size: + # 移除最旧的一半缓存 + keys_to_remove = list(self.noise_cache.keys())[:max_cache_size//2] + for key in keys_to_remove: + del self.noise_cache[key] + + except Exception as e: + print(f"✗ 缓存清理失败: {e}") + + def register_effect_template(self, name: str, template: Dict[str, Any]) -> bool: + """ + 注册效果模板 + + Args: + name: 模板名称 + template: 模板数据 + + Returns: + 是否注册成功 + """ + try: + self.effect_templates[name] = template.copy() + self.stats['templates_created'] += 1 + + # 触发模板注册回调 + self._trigger_effect_callback('template_registered', { + 'template_name': name, + 'template_data': template + }) + + print(f"✓ 效果模板已注册: {name}") + return True + + except Exception as e: + print(f"✗ 效果模板注册失败: {e}") + return False + + def unregister_effect_template(self, name: str) -> bool: + """ + 注销效果模板 + + Args: + name: 模板名称 + + Returns: + 是否注销成功 + """ + try: + if name in self.effect_templates: + del self.effect_templates[name] + print(f"✓ 效果模板已注销: {name}") + return True + else: + print(f"✗ 效果模板不存在: {name}") + return False + + except Exception as e: + print(f"✗ 效果模板注销失败: {e}") + return False + + def create_effect_from_template(self, template_name: str, + overrides: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: + """ + 从模板创建效果 + + Args: + template_name: 模板名称 + overrides: 覆盖参数 + + Returns: + 效果数据字典或None + """ + try: + if template_name not in self.effect_templates: + print(f"✗ 效果模板不存在: {template_name}") + return None + + # 获取模板数据 + template = self.effect_templates[template_name].copy() + + # 应用覆盖参数 + if overrides: + template.update(overrides) + + # 添加创建时间戳 + template['created_time'] = time.time() + + # 触发效果创建回调 + self._trigger_effect_callback('effect_created', { + 'template_name': template_name, + 'effect_data': template + }) + + self.stats['effects_generated'] += 1 + print(f"✓ 效果已从模板创建: {template_name}") + return template + + except Exception as e: + print(f"✗ 从模板创建效果失败: {e}") + return None + + def create_custom_effect(self, effect_type: str, parameters: Dict[str, Any]) -> int: + """ + 创建自定义效果 + + Args: + effect_type: 效果类型 + parameters: 效果参数 + + Returns: + 效果ID + """ + try: + effect_id = self.effect_counter + self.effect_counter += 1 + + custom_effect = { + 'id': effect_id, + 'type': effect_type, + 'parameters': parameters, + 'created_time': time.time() + } + + self.custom_effects[effect_id] = custom_effect + self.stats['custom_effects'] += 1 + + # 触发效果创建回调 + self._trigger_effect_callback('effect_created', { + 'effect_id': effect_id, + 'effect_data': custom_effect + }) + + print(f"✓ 自定义效果已创建: {effect_type} (ID: {effect_id})") + return effect_id + + except Exception as e: + print(f"✗ 自定义效果创建失败: {e}") + return -1 + + def remove_custom_effect(self, effect_id: int) -> bool: + """ + 移除自定义效果 + + Args: + effect_id: 效果ID + + Returns: + 是否移除成功 + """ + try: + if effect_id in self.custom_effects: + del self.custom_effects[effect_id] + self.stats['custom_effects'] -= 1 + print(f"✓ 自定义效果已移除: ID {effect_id}") + return True + else: + print(f"✗ 自定义效果不存在: ID {effect_id}") + return False + + except Exception as e: + print(f"✗ 自定义效果移除失败: {e}") + return False + + def get_custom_effect(self, effect_id: int) -> Optional[Dict[str, Any]]: + """ + 获取自定义效果 + + Args: + effect_id: 效果ID + + Returns: + 效果数据字典或None + """ + return self.custom_effects.get(effect_id) + + def get_all_custom_effects(self) -> Dict[int, Dict[str, Any]]: + """ + 获取所有自定义效果 + + Returns: + 所有自定义效果字典 + """ + return self.custom_effects.copy() + + def process_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: + """ + 处理效果并计算当前强度 + + Args: + effect_data: 效果数据 + time_progress: 时间进度(0.0-1.0) + + Returns: + 当前强度值(0.0-1.0) + """ + try: + effect_type = effect_data.get('type', EffectType.SIMPLE) + intensity = effect_data.get('intensity', 1.0) + + # 根据效果类型计算强度 + if effect_type == EffectType.SIMPLE: + current_intensity = self._process_simple_effect(effect_data, time_progress) + elif effect_type == EffectType.PERIODIC: + current_intensity = self._process_periodic_effect(effect_data, time_progress) + elif effect_type == EffectType.ENVELOPE: + current_intensity = self._process_envelope_effect(effect_data, time_progress) + elif effect_type == EffectType.RAMP: + current_intensity = self._process_ramp_effect(effect_data, time_progress) + elif effect_type == EffectType.NOISE: + current_intensity = self._process_noise_effect(effect_data, time_progress) + elif effect_type == EffectType.IMPULSE: + current_intensity = self._process_impulse_effect(effect_data, time_progress) + elif effect_type == EffectType.EXPLOSION: + current_intensity = self._process_explosion_effect(effect_data, time_progress) + elif effect_type == EffectType.TEXTURE: + current_intensity = self._process_texture_effect(effect_data, time_progress) + elif effect_type == EffectType.WIND: + current_intensity = self._process_wind_effect(effect_data, time_progress) + elif effect_type == EffectType.WATER: + current_intensity = self._process_water_effect(effect_data, time_progress) + elif effect_type == EffectType.CUSTOM: + current_intensity = self._process_custom_effect(effect_data, time_progress) + else: + current_intensity = intensity # 默认返回固定强度 + + # 应用总体强度 + current_intensity *= intensity + + # 触发效果处理回调 + self._trigger_effect_callback('effect_processed', { + 'effect_data': effect_data, + 'time_progress': time_progress, + 'current_intensity': current_intensity + }) + + self.stats['effects_processed'] += 1 + return max(0.0, min(1.0, current_intensity)) + + except Exception as e: + print(f"✗ 效果处理失败: {e}") + return 0.0 + + def _process_simple_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: + """处理简单效果""" + return 1.0 # 固定强度 + + def _process_periodic_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: + """处理周期性效果""" + try: + frequency = effect_data.get('frequency', 10.0) + amplitude = effect_data.get('amplitude', 1.0) + offset = effect_data.get('offset', 0.0) + phase = effect_data.get('phase', 0.0) + waveform = effect_data.get('waveform', 'sine') + duration = effect_data.get('duration', 1.0) + + # 生成波形 + generator = self.waveform_generators.get(waveform, self._generate_sine_wave) + cache_key = f"{waveform}_{frequency}_{duration}" + + # 检查缓存 + if cache_key in self.waveform_cache: + waveform_data = self.waveform_cache[cache_key] + else: + # 生成波形数据 + sample_count = max(100, int(frequency * duration * 10)) + waveform_data = generator(frequency, duration, sample_count) + self.waveform_cache[cache_key] = waveform_data + self.stats['waveforms_generated'] += 1 + + # 计算当前样本索引 + sample_index = int(time_progress * (len(waveform_data) - 1)) + sample_index = max(0, min(len(waveform_data) - 1, sample_index)) + + # 获取波形值 + waveform_value = waveform_data[sample_index] + + # 应用振幅和偏移 + result = offset + amplitude * waveform_value + + return max(0.0, min(1.0, result)) + except Exception as e: + print(f"✗ 周期性效果处理失败: {e}") + return 0.0 + + def _process_envelope_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: + """处理包络效果""" + try: + attack_time = effect_data.get('attack_time', 0.1) + attack_level = effect_data.get('attack_level', 1.0) + fade_time = effect_data.get('fade_time', 0.1) + fade_level = effect_data.get('fade_level', 0.0) + sustain_level = effect_data.get('sustain_level', 0.5) + duration = effect_data.get('duration', 1.0) + + if time_progress <= attack_time / duration: + # 攻击阶段 + attack_progress = time_progress / (attack_time / duration) + return attack_progress * attack_level + elif time_progress >= 1.0 - (fade_time / duration): + # 衰减阶段 + fade_progress = (time_progress - (1.0 - (fade_time / duration))) / (fade_time / duration) + return fade_level + (1.0 - fade_progress) * (sustain_level - fade_level) + else: + # 保持阶段 + return sustain_level + except Exception as e: + print(f"✗ 包络效果处理失败: {e}") + return 0.0 + + def _process_ramp_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: + """处理斜坡效果""" + try: + start_level = effect_data.get('start_level', 0.0) + end_level = effect_data.get('end_level', 1.0) + curve_type = effect_data.get('curve_type', 'linear') + + if curve_type == 'exponential': + # 指数曲线 + return start_level + (end_level - start_level) * (time_progress ** 2) + elif curve_type == 'logarithmic': + # 对数曲线 + if time_progress <= 0: + return start_level + return start_level + (end_level - start_level) * math.log10(1 + 9 * time_progress) + else: + # 线性曲线 + return start_level + (end_level - start_level) * time_progress + except Exception as e: + print(f"✗ 斜坡效果处理失败: {e}") + return 0.0 + + def _process_noise_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: + """处理噪声效果""" + try: + frequency = effect_data.get('frequency', 10.0) + noise_type = effect_data.get('noise_type', 'white') + duration = effect_data.get('duration', 1.0) + + # 生成噪声 + generator = self.noise_generators.get(noise_type, self._generate_white_noise) + cache_key = f"{noise_type}_{frequency}_{duration}" + + # 检查缓存 + if cache_key in self.noise_cache: + noise_data = self.noise_cache[cache_key] + else: + # 生成噪声数据 + sample_count = max(100, int(frequency * duration * 10)) + noise_data = generator(sample_count) + self.noise_cache[cache_key] = noise_data + self.stats['noise_samples'] += sample_count + + # 计算当前样本索引 + sample_index = int(time_progress * (len(noise_data) - 1)) + sample_index = max(0, min(len(noise_data) - 1, sample_index)) + + # 获取噪声值 + noise_value = noise_data[sample_index] + + return max(0.0, min(1.0, noise_value)) + except Exception as e: + print(f"✗ 噪声效果处理失败: {e}") + return 0.0 + + def _process_impulse_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: + """处理脉冲效果""" + try: + duration = effect_data.get('duration', 0.1) + sharpness = effect_data.get('sharpness', 0.5) + shape = effect_data.get('shape', 'gaussian') + + # 生成脉冲 + generator = self.impulse_shapes.get(shape, self._generate_gaussian_impulse) + + # 计算脉冲值 + impulse_value = generator(time_progress, sharpness) + + return max(0.0, min(1.0, impulse_value)) + except Exception as e: + print(f"✗ 脉冲效果处理失败: {e}") + return 0.0 + + def _process_explosion_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: + """处理爆炸效果""" + try: + rise_time = effect_data.get('rise_time', 0.05) + fall_time = effect_data.get('fall_time', 0.5) + frequency_start = effect_data.get('frequency_start', 80.0) + frequency_end = effect_data.get('frequency_end', 10.0) + duration = effect_data.get('duration', 1.0) + + # 计算频率变化 + frequency = frequency_start + (frequency_end - frequency_start) * time_progress + + # 计算振幅包络 + if time_progress <= rise_time / duration: + # 上升阶段 + envelope = time_progress / (rise_time / duration) + else: + # 下降阶段 + fall_progress = (time_progress - (rise_time / duration)) / ((duration - rise_time) / duration) + envelope = max(0.0, 1.0 - fall_progress) + + # 生成振动 + vibration = math.sin(2 * math.pi * frequency * time_progress * duration) + result = envelope * (vibration + 1.0) / 2.0 # 转换到0-1范围 + + return max(0.0, min(1.0, result)) + except Exception as e: + print(f"✗ 爆炸效果处理失败: {e}") + return 0.0 + + def _process_texture_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: + """处理纹理效果""" + try: + frequency = effect_data.get('frequency', 10.0) + roughness = effect_data.get('roughness', 0.5) + spacing = effect_data.get('spacing', 0.1) + + # 生成纹理模式 + texture_position = time_progress * frequency + texture_phase = texture_position - math.floor(texture_position) + + # 根据粗糙度调整强度 + if texture_phase < spacing: + # 纹理凸起 + intensity = 1.0 - (texture_phase / spacing) * roughness + else: + # 纹理凹陷 + intensity = 0.5 + 0.5 * roughness * math.sin(2 * math.pi * texture_phase) + + return max(0.0, min(1.0, intensity)) + except Exception as e: + print(f"✗ 纹理效果处理失败: {e}") + return 0.0 + + def _process_wind_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: + """处理风效果""" + try: + frequency = effect_data.get('frequency', 1.0) + turbulence = effect_data.get('turbulence', 0.5) + direction_variability = effect_data.get('direction_variability', 0.3) + + # 基础风模式 + base_wind = math.sin(2 * math.pi * frequency * time_progress) + + # 添加湍流 + turbulence_effect = turbulence * math.sin(2 * math.pi * frequency * 5 * time_progress) + + # 添加方向变化 + direction_effect = direction_variability * math.sin(2 * math.pi * frequency * 0.2 * time_progress) + + # 组合效果 + result = (base_wind + turbulence_effect + direction_effect) / 3.0 + result = (result + 1.0) / 2.0 # 转换到0-1范围 + + return max(0.0, min(1.0, result)) + except Exception as e: + print(f"✗ 风效果处理失败: {e}") + return 0.0 + + def _process_water_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: + """处理水效果""" + try: + frequency = effect_data.get('frequency', 1.0) + wave_height = effect_data.get('wave_height', 1.0) + viscosity = effect_data.get('viscosity', 1.0) + + # 主波浪 + main_wave = math.sin(2 * math.pi * frequency * time_progress) + + # 次级波浪 + secondary_wave = 0.5 * math.sin(2 * math.pi * frequency * 2.5 * time_progress + 0.5) + + # 粘性阻尼 + damping = math.exp(-viscosity * time_progress) + + # 组合效果 + result = (main_wave + secondary_wave) * damping * wave_height + result = (result + 1.0) / 2.0 # 转换到0-1范围 + + return max(0.0, min(1.0, result)) + except Exception as e: + print(f"✗ 水效果处理失败: {e}") + return 0.0 + + def _process_custom_effect(self, effect_data: Dict[str, Any], time_progress: float) -> float: + """处理自定义效果""" + # 这里可以实现更复杂的自定义效果处理逻辑 + parameters = effect_data.get('parameters', {}) + custom_intensity = parameters.get('intensity_multiplier', 1.0) + return custom_intensity # 默认返回固定强度 + + def _generate_sine_wave(self, frequency: float, duration: float, samples: int) -> List[float]: + """生成正弦波""" + wave = [] + for i in range(samples): + t = i / (samples - 1) * duration + value = math.sin(2 * math.pi * frequency * t) + wave.append(value) + return wave + + def _generate_square_wave(self, frequency: float, duration: float, samples: int) -> List[float]: + """生成方波""" + wave = [] + for i in range(samples): + t = i / (samples - 1) * duration + value = 1.0 if math.sin(2 * math.pi * frequency * t) >= 0 else -1.0 + wave.append(value) + return wave + + def _generate_triangle_wave(self, frequency: float, duration: float, samples: int) -> List[float]: + """生成三角波""" + wave = [] + for i in range(samples): + t = i / (samples - 1) * duration + value = 2 * abs(2 * (frequency * t - math.floor(frequency * t + 0.5))) - 1 + wave.append(value) + return wave + + def _generate_sawtooth_wave(self, frequency: float, duration: float, samples: int) -> List[float]: + """生成锯齿波""" + wave = [] + for i in range(samples): + t = i / (samples - 1) * duration + value = 2 * (frequency * t - math.floor(frequency * t + 0.5)) + wave.append(value) + return wave + + def _generate_white_noise(self, samples: int) -> List[float]: + """生成白噪声""" + noise = [] + for _ in range(samples): + value = self.random_generator.uniform(-1.0, 1.0) + noise.append(value) + return noise + + def _generate_pink_noise(self, samples: int) -> List[float]: + """生成粉红噪声""" + noise = [] + b = [0.0] * 7 + for _ in range(samples): + white = self.random_generator.uniform(-1.0, 1.0) + b[0] = 0.99886 * b[0] + white * 0.0555179 + b[1] = 0.99332 * b[1] + white * 0.0750759 + b[2] = 0.96900 * b[2] + white * 0.1538520 + b[3] = 0.86650 * b[3] + white * 0.3104856 + b[4] = 0.55000 * b[4] + white * 0.5329522 + b[5] = -0.7616 * b[5] - white * 0.0168980 + pink = b[0] + b[1] + b[2] + b[3] + b[4] + b[5] + b[6] + white * 0.5362 + b[6] = white * 0.115926 + noise.append(pink / 5.0) # 归一化 + return noise + + def _generate_brown_noise(self, samples: int) -> List[float]: + """生成布朗噪声""" + noise = [] + last = 0.0 + for _ in range(samples): + white = self.random_generator.uniform(-1.0, 1.0) + brown = (last + (0.02 * white)) / 1.02 + last = brown + noise.append(brown * 3.5) # 补偿幅度 + return noise + + def _generate_gaussian_impulse(self, time_progress: float, sharpness: float) -> float: + """生成高斯脉冲""" + try: + # 高斯函数: exp(-(x^2)/(2*sigma^2)) + sigma = 0.2 * (1.0 - sharpness * 0.8) # 根据锐度调整宽度 + x = (time_progress - 0.5) * 2 # 调整到-1到1范围 + value = math.exp(-(x*x) / (2 * sigma * sigma)) + return value + except Exception: + return 0.0 + + def _generate_exponential_impulse(self, time_progress: float, sharpness: float) -> float: + """生成指数脉冲""" + try: + if time_progress < 0.5: + # 上升阶段 + rise_factor = 1.0 + 9.0 * sharpness # 1到10 + value = 1.0 - math.exp(-rise_factor * time_progress * 2) + else: + # 下降阶段 + fall_factor = 1.0 + 9.0 * (1.0 - sharpness) # 1到10 + value = math.exp(-fall_factor * (time_progress - 0.5) * 2) + return value + except Exception: + return 0.0 + + def _generate_linear_impulse(self, time_progress: float, sharpness: float) -> float: + """生成线性脉冲""" + try: + peak_position = 0.3 + 0.4 * sharpness # 0.3到0.7 + if time_progress < peak_position: + # 上升阶段 + value = time_progress / peak_position + else: + # 下降阶段 + value = 1.0 - (time_progress - peak_position) / (1.0 - peak_position) + return max(0.0, value) + except Exception: + return 0.0 + + def mix_effects(self, effects: List[Dict[str, Any]], time_progress: float, + mix_mode: str = 'max', weights: List[float] = None) -> float: + """ + 混合多个效果 + + Args: + effects: 效果数据列表 + time_progress: 时间进度(0.0-1.0) + mix_mode: 混合模式('max', 'add', 'multiply', 'average', 'weighted') + weights: 权重列表(用于加权混合) + + Returns: + 混合后的强度值(0.0-1.0) + """ + try: + intensities = [] + for effect_data in effects: + intensity = self.process_effect(effect_data, time_progress) + intensities.append(intensity) + + if not intensities: + return 0.0 + + # 根据混合模式计算最终强度 + if mix_mode == 'weighted' and weights: + mix_function = self._mix_weighted + return mix_function(intensities, weights) + else: + mix_function = self.effect_mixing.get(mix_mode, self._mix_max) + return mix_function(intensities) + + except Exception as e: + print(f"✗ 效果混合失败: {e}") + return 0.0 + + def _mix_max(self, intensities: List[float]) -> float: + """最大值混合""" + return max(intensities) + + def _mix_add(self, intensities: List[float]) -> float: + """加法混合""" + return min(1.0, sum(intensities)) + + def _mix_multiply(self, intensities: List[float]) -> float: + """乘法混合""" + result = 1.0 + for intensity in intensities: + result *= max(0.0, intensity) # 避免负值影响 + return result + + def _mix_average(self, intensities: List[float]) -> float: + """平均值混合""" + return sum(intensities) / len(intensities) if intensities else 0.0 + + def _mix_weighted(self, intensities: List[float], weights: List[float]) -> float: + """加权混合""" + if len(intensities) != len(weights): + return self._mix_average(intensities) + + weighted_sum = sum(intensity * weight for intensity, weight in zip(intensities, weights)) + total_weight = sum(weights) + return weighted_sum / total_weight if total_weight > 0 else 0.0 + + def get_effect_parameters(self, effect_type: str) -> Dict[str, tuple]: + """ + 获取效果参数信息 + + Args: + effect_type: 效果类型 + + Returns: + 参数信息字典 + """ + return self.effect_parameters.get(effect_type, {}) + + def get_predefined_effects(self) -> Dict[str, Dict[str, Any]]: + """ + 获取预定义效果 + + Returns: + 预定义效果字典 + """ + return self.predefined_effects.copy() + + def _trigger_effect_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发效果回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.effect_callbacks: + for callback in self.effect_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 效果回调执行失败: {e}") + except Exception as e: + print(f"✗ 效果回调触发失败: {e}") + + def register_effect_callback(self, callback_type: str, callback: callable): + """ + 注册效果回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.effect_callbacks: + self.effect_callbacks[callback_type].append(callback) + print(f"✓ 效果回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 效果回调注册失败: {e}") + + def unregister_effect_callback(self, callback_type: str, callback: callable): + """ + 注销效果回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.effect_callbacks: + if callback in self.effect_callbacks[callback_type]: + self.effect_callbacks[callback_type].remove(callback) + print(f"✓ 效果回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 效果回调注销失败: {e}") + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + try: + self.stats = { + 'templates_created': 0, + 'effects_generated': 0, + 'effects_processed': 0, + 'custom_effects': 0, + 'waveforms_generated': 0, + 'noise_samples': 0 + } + print("✓ 效果处理器统计信息已重置") + except Exception as e: print(f"✗ 效果处理器统计信息重置失败: {e}") \ No newline at end of file diff --git a/plugins/user/haptic_feedback_system/events/event_handler.py b/plugins/user/haptic_feedback_system/events/event_handler.py index 8879a47f..68b3247f 100644 --- a/plugins/user/haptic_feedback_system/events/event_handler.py +++ b/plugins/user/haptic_feedback_system/events/event_handler.py @@ -1,708 +1,708 @@ -""" -事件处理器模块 -负责处理游戏事件并触发相应的触觉反馈 -""" - -import time -from typing import Dict, Any, List, Optional, Callable -import threading - -class HapticEventHandler: - """ - 触觉事件处理器 - 负责处理游戏事件并触发相应的触觉反馈 - """ - - def __init__(self, plugin): - """ - 初始化触觉事件处理器 - - Args: - plugin: 触觉反馈系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 事件映射配置 - self.event_mapping = { - # 游戏事件到触觉效果的映射 - 'player_jump': { - 'effect': 'impulse', - 'intensity': 0.6, - 'frequency': 40.0, - 'duration': 0.1, - 'device': 'feet' - }, - 'player_land': { - 'effect': 'rumble', - 'intensity': 0.8, - 'frequency': 30.0, - 'duration': 0.2, - 'device': 'feet' - }, - 'player_take_damage': { - 'effect': 'explosion', - 'intensity': 1.0, - 'frequency': 50.0, - 'duration': 0.5, - 'device': 'body' - }, - 'player_heal': { - 'effect': 'pulse', - 'intensity': 0.5, - 'frequency': 25.0, - 'duration': 0.3, - 'device': 'chest' - }, - 'weapon_fire': { - 'effect': 'click', - 'intensity': 0.7, - 'frequency': 80.0, - 'duration': 0.05, - 'device': 'hands' - }, - 'weapon_reload': { - 'effect': 'buzz', - 'intensity': 0.4, - 'frequency': 35.0, - 'duration': 0.4, - 'device': 'hands' - }, - 'enemy_hit': { - 'effect': 'click', - 'intensity': 0.5, - 'frequency': 60.0, - 'duration': 0.08, - 'device': 'hands' - }, - 'enemy_death': { - 'effect': 'explosion', - 'intensity': 0.9, - 'frequency': 45.0, - 'duration': 0.6, - 'device': 'body' - }, - 'pickup_item': { - 'effect': 'pulse', - 'intensity': 0.3, - 'frequency': 20.0, - 'duration': 0.2, - 'device': 'hands' - }, - 'door_open': { - 'effect': 'buzz', - 'intensity': 0.3, - 'frequency': 15.0, - 'duration': 0.3, - 'device': 'hands' - }, - 'door_close': { - 'effect': 'click', - 'intensity': 0.4, - 'frequency': 25.0, - 'duration': 0.1, - 'device': 'hands' - }, - 'explosion': { - 'effect': 'explosion', - 'intensity': 1.0, - 'frequency': 35.0, - 'duration': 1.0, - 'device': 'body' - }, - 'ui_button_click': { - 'effect': 'click', - 'intensity': 0.3, - 'frequency': 100.0, - 'duration': 0.05, - 'device': 'hands' - }, - 'ui_button_hover': { - 'effect': 'soft_rumble', - 'intensity': 0.1, - 'frequency': 10.0, - 'duration': 0.1, - 'device': 'hands' - } - } - - # 事件优先级配置 - self.event_priority = { - 'player_take_damage': 100, - 'explosion': 90, - 'enemy_death': 80, - 'player_land': 70, - 'weapon_fire': 60, - 'player_jump': 50, - 'enemy_hit': 40, - 'pickup_item': 30, - 'door_open': 20, - 'door_close': 20, - 'ui_button_click': 10, - 'ui_button_hover': 5, - 'player_heal': 40, - 'weapon_reload': 30 - } - - # 事件队列 - self.event_queue = [] - self.event_queue_lock = threading.Lock() - - # 事件抑制配置 - self.event_suppression = { - 'enable_suppression': True, - 'suppression_time': 0.1, # 秒 - 'suppressed_events': set() - } - - # 事件组合配置 - self.event_combination = { - 'enable_combination': True, - 'combination_window': 0.5, # 秒 - 'max_combined_events': 5 - } - - # 状态跟踪 - self.event_state = { - 'last_event_time': {}, - 'combined_events': [], - 'active_events': {} - } - - # 统计信息 - self.stats = { - 'events_received': 0, - 'events_processed': 0, - 'events_suppressed': 0, - 'events_combined': 0, - 'haptic_effects_triggered': 0 - } - - # 回调函数 - self.event_callbacks = { - 'event_received': [], - 'event_processed': [], - 'event_suppressed': [], - 'event_combined': [] - } - - print("✓ 触觉事件处理器已创建") - - def initialize(self) -> bool: - """ - 初始化触觉事件处理器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 触觉事件处理器初始化完成") - return True - - except Exception as e: - print(f"✗ 触觉事件处理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用触觉事件处理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 触觉事件处理器未初始化") - return False - - self.enabled = True - print("✓ 触觉事件处理器已启用") - return True - - except Exception as e: - print(f"✗ 触觉事件处理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用触觉事件处理器""" - try: - self.enabled = False - print("✓ 触觉事件处理器已禁用") - - except Exception as e: - print(f"✗ 触觉事件处理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理触觉事件处理器资源""" - try: - self.disable() - self.event_mapping.clear() - self.event_queue.clear() - self.event_state.clear() - self.event_callbacks.clear() - self.initialized = False - print("✓ 触觉事件处理器资源已清理") - - except Exception as e: - print(f"✗ 触觉事件处理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新触觉事件处理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 处理事件队列 - self._process_event_queue() - - # 更新事件状态 - self._update_event_state() - - except Exception as e: - print(f"✗ 触觉事件处理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _process_event_queue(self): - """处理事件队列""" - try: - with self.event_queue_lock: - if not self.event_queue: - return - - # 获取事件并清空队列 - events_to_process = self.event_queue[:] - self.event_queue.clear() - - # 处理每个事件 - for event in events_to_process: - self._process_event(event) - - except Exception as e: - print(f"✗ 事件队列处理失败: {e}") - - def _update_event_state(self): - """更新事件状态""" - try: - current_time = time.time() - - # 清理过期的抑制事件 - suppression_time = self.event_suppression['suppression_time'] - expired_suppressions = [ - event_type for event_type, timestamp in - self.event_suppression['suppressed_events'] - if current_time - timestamp > suppression_time - ] - - for event_type in expired_suppressions: - self.event_suppression['suppressed_events'].discard(event_type) - - # 清理过期的组合事件 - combination_window = self.event_combination['combination_window'] - self.event_state['combined_events'] = [ - event for event in self.event_state['combined_events'] - if current_time - event.get('timestamp', 0) <= combination_window - ] - - except Exception as e: - print(f"✗ 事件状态更新失败: {e}") - - def handle_event(self, event_type: str, event_data: Dict[str, Any] = None) -> bool: - """ - 处理事件 - - Args: - event_type: 事件类型 - event_data: 事件数据 - - Returns: - 是否处理成功 - """ - try: - if not self.enabled: - return False - - # 创建事件对象 - event = { - 'type': event_type, - 'data': event_data or {}, - 'timestamp': time.time(), - 'priority': self.event_priority.get(event_type, 50) - } - - # 添加到事件队列 - with self.event_queue_lock: - self.event_queue.append(event) - - # 触发事件接收回调 - self._trigger_event_callback('event_received', { - 'event': event - }) - - self.stats['events_received'] += 1 - return True - - except Exception as e: - print(f"✗ 事件处理失败: {e}") - return False - - def _process_event(self, event: Dict[str, Any]): - """ - 处理单个事件 - - Args: - event: 事件对象 - """ - try: - event_type = event['type'] - event_data = event['data'] - timestamp = event['timestamp'] - - # 检查事件抑制 - if self.event_suppression['enable_suppression']: - if event_type in [e[0] for e in self.event_suppression['suppressed_events']]: - # 事件被抑制 - self.stats['events_suppressed'] += 1 - - # 触发抑制回调 - self._trigger_event_callback('event_suppressed', { - 'event': event - }) - return - - # 添加到抑制列表 - self.event_suppression['suppressed_events'].add((event_type, timestamp)) - - # 检查事件组合 - if self.event_combination['enable_combination']: - self._handle_event_combination(event) - - # 处理事件映射 - if event_type in self.event_mapping: - self._trigger_haptic_effect(event_type, event_data) - - # 触发事件处理回调 - self._trigger_event_callback('event_processed', { - 'event': event - }) - - self.stats['events_processed'] += 1 - - except Exception as e: - print(f"✗ 单个事件处理失败: {e}") - - def _handle_event_combination(self, event: Dict[str, Any]): - """ - 处理事件组合 - - Args: - event: 事件对象 - """ - try: - current_time = time.time() - combination_window = self.event_combination['combination_window'] - - # 添加到组合事件列表 - self.event_state['combined_events'].append(event) - - # 保持列表大小 - if len(self.event_state['combined_events']) > self.event_combination['max_combined_events']: - self.event_state['combined_events'].pop(0) - - # 检查是否达到组合条件 - if len(self.event_state['combined_events']) >= 3: - # 检查时间窗口 - oldest_event = self.event_state['combined_events'][0] - if current_time - oldest_event['timestamp'] <= combination_window: - # 触发组合事件回调 - self._trigger_event_callback('event_combined', { - 'events': self.event_state['combined_events'].copy() - }) - - self.stats['events_combined'] += 1 - - # 清空组合事件列表 - self.event_state['combined_events'].clear() - - except Exception as e: - print(f"✗ 事件组合处理失败: {e}") - - def _trigger_haptic_effect(self, event_type: str, event_data: Dict[str, Any]): - """ - 触发触觉效果 - - Args: - event_type: 事件类型 - event_data: 事件数据 - """ - try: - if not self.plugin.haptic_manager: - return - - # 获取效果配置 - effect_config = self.event_mapping.get(event_type) - if not effect_config: - return - - # 提取效果参数 - effect_type = effect_config['effect'] - intensity = effect_config['intensity'] - frequency = effect_config['frequency'] - duration = effect_config['duration'] - device_type = effect_config.get('device') - - # 获取设备ID - device_id = None - if device_type and self.plugin.device_manager: - devices = self.plugin.device_manager.get_devices_by_type(device_type) - if devices: - device_id = devices[0]['id'] - - # 应用事件数据中的参数覆盖 - if 'intensity' in event_data: - intensity *= event_data['intensity'] - if 'frequency' in event_data: - frequency *= event_data['frequency'] - if 'duration' in event_data: - duration *= event_data['duration'] - - # 播放触觉效果 - effect_id = self.plugin.haptic_manager.play_effect( - effect_type=effect_type, - device_id=device_id, - intensity=intensity, - duration=duration, - frequency=frequency, - parameters={ - 'event_type': event_type, - 'event_data': event_data - } - ) - - if effect_id >= 0: - self.stats['haptic_effects_triggered'] += 1 - print(f"✓ 触觉效果已触发: {event_type} -> {effect_type}") - - except Exception as e: - print(f"✗ 触觉效果触发失败: {e}") - - def register_event_mapping(self, event_type: str, effect_config: Dict[str, Any]) -> bool: - """ - 注册事件映射 - - Args: - event_type: 事件类型 - effect_config: 效果配置 - - Returns: - 是否注册成功 - """ - try: - self.event_mapping[event_type] = effect_config - print(f"✓ 事件映射已注册: {event_type}") - return True - except Exception as e: - print(f"✗ 事件映射注册失败: {e}") - return False - - def unregister_event_mapping(self, event_type: str) -> bool: - """ - 注销事件映射 - - Args: - event_type: 事件类型 - - Returns: - 是否注销成功 - """ - try: - if event_type in self.event_mapping: - del self.event_mapping[event_type] - print(f"✓ 事件映射已注销: {event_type}") - return True - else: - print(f"✗ 事件映射不存在: {event_type}") - return False - except Exception as e: - print(f"✗ 事件映射注销失败: {e}") - return False - - def set_event_priority(self, event_type: str, priority: int) -> bool: - """ - 设置事件优先级 - - Args: - event_type: 事件类型 - priority: 优先级值 - - Returns: - 是否设置成功 - """ - try: - self.event_priority[event_type] = priority - print(f"✓ 事件优先级已设置: {event_type} = {priority}") - return True - except Exception as e: - print(f"✗ 事件优先级设置失败: {e}") - return False - - def set_event_suppression(self, config: Dict[str, Any]) -> bool: - """ - 设置事件抑制配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.event_suppression.update(config) - print(f"✓ 事件抑制配置已更新: {self.event_suppression}") - return True - except Exception as e: - print(f"✗ 事件抑制配置设置失败: {e}") - return False - - def set_event_combination(self, config: Dict[str, Any]) -> bool: - """ - 设置事件组合配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.event_combination.update(config) - print(f"✓ 事件组合配置已更新: {self.event_combination}") - return True - except Exception as e: - print(f"✗ 事件组合配置设置失败: {e}") - return False - - def get_event_mapping(self) -> Dict[str, Dict[str, Any]]: - """ - 获取事件映射 - - Returns: - 事件映射字典 - """ - return self.event_mapping.copy() - - def get_event_priority(self) -> Dict[str, int]: - """ - 获取事件优先级 - - Returns: - 事件优先级字典 - """ - return self.event_priority.copy() - - def get_event_suppression_config(self) -> Dict[str, Any]: - """ - 获取事件抑制配置 - - Returns: - 配置字典 - """ - return self.event_suppression.copy() - - def get_event_combination_config(self) -> Dict[str, Any]: - """ - 获取事件组合配置 - - Returns: - 配置字典 - """ - return self.event_combination.copy() - - def _trigger_event_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发事件回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.event_callbacks: - for callback in self.event_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 事件回调执行失败: {e}") - except Exception as e: - print(f"✗ 事件回调触发失败: {e}") - - def register_event_callback(self, callback_type: str, callback: Callable): - """ - 注册事件回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.event_callbacks: - self.event_callbacks[callback_type].append(callback) - print(f"✓ 事件回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 事件回调注册失败: {e}") - - def unregister_event_callback(self, callback_type: str, callback: Callable): - """ - 注销事件回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.event_callbacks: - if callback in self.event_callbacks[callback_type]: - self.event_callbacks[callback_type].remove(callback) - print(f"✓ 事件回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 事件回调注销失败: {e}") - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - try: - self.stats = { - 'events_received': 0, - 'events_processed': 0, - 'events_suppressed': 0, - 'events_combined': 0, - 'haptic_effects_triggered': 0 - } - print("✓ 事件处理器统计信息已重置") - except Exception as e: +""" +事件处理器模块 +负责处理游戏事件并触发相应的触觉反馈 +""" + +import time +from typing import Dict, Any, List, Optional, Callable +import threading + +class HapticEventHandler: + """ + 触觉事件处理器 + 负责处理游戏事件并触发相应的触觉反馈 + """ + + def __init__(self, plugin): + """ + 初始化触觉事件处理器 + + Args: + plugin: 触觉反馈系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 事件映射配置 + self.event_mapping = { + # 游戏事件到触觉效果的映射 + 'player_jump': { + 'effect': 'impulse', + 'intensity': 0.6, + 'frequency': 40.0, + 'duration': 0.1, + 'device': 'feet' + }, + 'player_land': { + 'effect': 'rumble', + 'intensity': 0.8, + 'frequency': 30.0, + 'duration': 0.2, + 'device': 'feet' + }, + 'player_take_damage': { + 'effect': 'explosion', + 'intensity': 1.0, + 'frequency': 50.0, + 'duration': 0.5, + 'device': 'body' + }, + 'player_heal': { + 'effect': 'pulse', + 'intensity': 0.5, + 'frequency': 25.0, + 'duration': 0.3, + 'device': 'chest' + }, + 'weapon_fire': { + 'effect': 'click', + 'intensity': 0.7, + 'frequency': 80.0, + 'duration': 0.05, + 'device': 'hands' + }, + 'weapon_reload': { + 'effect': 'buzz', + 'intensity': 0.4, + 'frequency': 35.0, + 'duration': 0.4, + 'device': 'hands' + }, + 'enemy_hit': { + 'effect': 'click', + 'intensity': 0.5, + 'frequency': 60.0, + 'duration': 0.08, + 'device': 'hands' + }, + 'enemy_death': { + 'effect': 'explosion', + 'intensity': 0.9, + 'frequency': 45.0, + 'duration': 0.6, + 'device': 'body' + }, + 'pickup_item': { + 'effect': 'pulse', + 'intensity': 0.3, + 'frequency': 20.0, + 'duration': 0.2, + 'device': 'hands' + }, + 'door_open': { + 'effect': 'buzz', + 'intensity': 0.3, + 'frequency': 15.0, + 'duration': 0.3, + 'device': 'hands' + }, + 'door_close': { + 'effect': 'click', + 'intensity': 0.4, + 'frequency': 25.0, + 'duration': 0.1, + 'device': 'hands' + }, + 'explosion': { + 'effect': 'explosion', + 'intensity': 1.0, + 'frequency': 35.0, + 'duration': 1.0, + 'device': 'body' + }, + 'ui_button_click': { + 'effect': 'click', + 'intensity': 0.3, + 'frequency': 100.0, + 'duration': 0.05, + 'device': 'hands' + }, + 'ui_button_hover': { + 'effect': 'soft_rumble', + 'intensity': 0.1, + 'frequency': 10.0, + 'duration': 0.1, + 'device': 'hands' + } + } + + # 事件优先级配置 + self.event_priority = { + 'player_take_damage': 100, + 'explosion': 90, + 'enemy_death': 80, + 'player_land': 70, + 'weapon_fire': 60, + 'player_jump': 50, + 'enemy_hit': 40, + 'pickup_item': 30, + 'door_open': 20, + 'door_close': 20, + 'ui_button_click': 10, + 'ui_button_hover': 5, + 'player_heal': 40, + 'weapon_reload': 30 + } + + # 事件队列 + self.event_queue = [] + self.event_queue_lock = threading.Lock() + + # 事件抑制配置 + self.event_suppression = { + 'enable_suppression': True, + 'suppression_time': 0.1, # 秒 + 'suppressed_events': set() + } + + # 事件组合配置 + self.event_combination = { + 'enable_combination': True, + 'combination_window': 0.5, # 秒 + 'max_combined_events': 5 + } + + # 状态跟踪 + self.event_state = { + 'last_event_time': {}, + 'combined_events': [], + 'active_events': {} + } + + # 统计信息 + self.stats = { + 'events_received': 0, + 'events_processed': 0, + 'events_suppressed': 0, + 'events_combined': 0, + 'haptic_effects_triggered': 0 + } + + # 回调函数 + self.event_callbacks = { + 'event_received': [], + 'event_processed': [], + 'event_suppressed': [], + 'event_combined': [] + } + + print("✓ 触觉事件处理器已创建") + + def initialize(self) -> bool: + """ + 初始化触觉事件处理器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 触觉事件处理器初始化完成") + return True + + except Exception as e: + print(f"✗ 触觉事件处理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用触觉事件处理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 触觉事件处理器未初始化") + return False + + self.enabled = True + print("✓ 触觉事件处理器已启用") + return True + + except Exception as e: + print(f"✗ 触觉事件处理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用触觉事件处理器""" + try: + self.enabled = False + print("✓ 触觉事件处理器已禁用") + + except Exception as e: + print(f"✗ 触觉事件处理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理触觉事件处理器资源""" + try: + self.disable() + self.event_mapping.clear() + self.event_queue.clear() + self.event_state.clear() + self.event_callbacks.clear() + self.initialized = False + print("✓ 触觉事件处理器资源已清理") + + except Exception as e: + print(f"✗ 触觉事件处理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新触觉事件处理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 处理事件队列 + self._process_event_queue() + + # 更新事件状态 + self._update_event_state() + + except Exception as e: + print(f"✗ 触觉事件处理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _process_event_queue(self): + """处理事件队列""" + try: + with self.event_queue_lock: + if not self.event_queue: + return + + # 获取事件并清空队列 + events_to_process = self.event_queue[:] + self.event_queue.clear() + + # 处理每个事件 + for event in events_to_process: + self._process_event(event) + + except Exception as e: + print(f"✗ 事件队列处理失败: {e}") + + def _update_event_state(self): + """更新事件状态""" + try: + current_time = time.time() + + # 清理过期的抑制事件 + suppression_time = self.event_suppression['suppression_time'] + expired_suppressions = [ + event_type for event_type, timestamp in + self.event_suppression['suppressed_events'] + if current_time - timestamp > suppression_time + ] + + for event_type in expired_suppressions: + self.event_suppression['suppressed_events'].discard(event_type) + + # 清理过期的组合事件 + combination_window = self.event_combination['combination_window'] + self.event_state['combined_events'] = [ + event for event in self.event_state['combined_events'] + if current_time - event.get('timestamp', 0) <= combination_window + ] + + except Exception as e: + print(f"✗ 事件状态更新失败: {e}") + + def handle_event(self, event_type: str, event_data: Dict[str, Any] = None) -> bool: + """ + 处理事件 + + Args: + event_type: 事件类型 + event_data: 事件数据 + + Returns: + 是否处理成功 + """ + try: + if not self.enabled: + return False + + # 创建事件对象 + event = { + 'type': event_type, + 'data': event_data or {}, + 'timestamp': time.time(), + 'priority': self.event_priority.get(event_type, 50) + } + + # 添加到事件队列 + with self.event_queue_lock: + self.event_queue.append(event) + + # 触发事件接收回调 + self._trigger_event_callback('event_received', { + 'event': event + }) + + self.stats['events_received'] += 1 + return True + + except Exception as e: + print(f"✗ 事件处理失败: {e}") + return False + + def _process_event(self, event: Dict[str, Any]): + """ + 处理单个事件 + + Args: + event: 事件对象 + """ + try: + event_type = event['type'] + event_data = event['data'] + timestamp = event['timestamp'] + + # 检查事件抑制 + if self.event_suppression['enable_suppression']: + if event_type in [e[0] for e in self.event_suppression['suppressed_events']]: + # 事件被抑制 + self.stats['events_suppressed'] += 1 + + # 触发抑制回调 + self._trigger_event_callback('event_suppressed', { + 'event': event + }) + return + + # 添加到抑制列表 + self.event_suppression['suppressed_events'].add((event_type, timestamp)) + + # 检查事件组合 + if self.event_combination['enable_combination']: + self._handle_event_combination(event) + + # 处理事件映射 + if event_type in self.event_mapping: + self._trigger_haptic_effect(event_type, event_data) + + # 触发事件处理回调 + self._trigger_event_callback('event_processed', { + 'event': event + }) + + self.stats['events_processed'] += 1 + + except Exception as e: + print(f"✗ 单个事件处理失败: {e}") + + def _handle_event_combination(self, event: Dict[str, Any]): + """ + 处理事件组合 + + Args: + event: 事件对象 + """ + try: + current_time = time.time() + combination_window = self.event_combination['combination_window'] + + # 添加到组合事件列表 + self.event_state['combined_events'].append(event) + + # 保持列表大小 + if len(self.event_state['combined_events']) > self.event_combination['max_combined_events']: + self.event_state['combined_events'].pop(0) + + # 检查是否达到组合条件 + if len(self.event_state['combined_events']) >= 3: + # 检查时间窗口 + oldest_event = self.event_state['combined_events'][0] + if current_time - oldest_event['timestamp'] <= combination_window: + # 触发组合事件回调 + self._trigger_event_callback('event_combined', { + 'events': self.event_state['combined_events'].copy() + }) + + self.stats['events_combined'] += 1 + + # 清空组合事件列表 + self.event_state['combined_events'].clear() + + except Exception as e: + print(f"✗ 事件组合处理失败: {e}") + + def _trigger_haptic_effect(self, event_type: str, event_data: Dict[str, Any]): + """ + 触发触觉效果 + + Args: + event_type: 事件类型 + event_data: 事件数据 + """ + try: + if not self.plugin.haptic_manager: + return + + # 获取效果配置 + effect_config = self.event_mapping.get(event_type) + if not effect_config: + return + + # 提取效果参数 + effect_type = effect_config['effect'] + intensity = effect_config['intensity'] + frequency = effect_config['frequency'] + duration = effect_config['duration'] + device_type = effect_config.get('device') + + # 获取设备ID + device_id = None + if device_type and self.plugin.device_manager: + devices = self.plugin.device_manager.get_devices_by_type(device_type) + if devices: + device_id = devices[0]['id'] + + # 应用事件数据中的参数覆盖 + if 'intensity' in event_data: + intensity *= event_data['intensity'] + if 'frequency' in event_data: + frequency *= event_data['frequency'] + if 'duration' in event_data: + duration *= event_data['duration'] + + # 播放触觉效果 + effect_id = self.plugin.haptic_manager.play_effect( + effect_type=effect_type, + device_id=device_id, + intensity=intensity, + duration=duration, + frequency=frequency, + parameters={ + 'event_type': event_type, + 'event_data': event_data + } + ) + + if effect_id >= 0: + self.stats['haptic_effects_triggered'] += 1 + print(f"✓ 触觉效果已触发: {event_type} -> {effect_type}") + + except Exception as e: + print(f"✗ 触觉效果触发失败: {e}") + + def register_event_mapping(self, event_type: str, effect_config: Dict[str, Any]) -> bool: + """ + 注册事件映射 + + Args: + event_type: 事件类型 + effect_config: 效果配置 + + Returns: + 是否注册成功 + """ + try: + self.event_mapping[event_type] = effect_config + print(f"✓ 事件映射已注册: {event_type}") + return True + except Exception as e: + print(f"✗ 事件映射注册失败: {e}") + return False + + def unregister_event_mapping(self, event_type: str) -> bool: + """ + 注销事件映射 + + Args: + event_type: 事件类型 + + Returns: + 是否注销成功 + """ + try: + if event_type in self.event_mapping: + del self.event_mapping[event_type] + print(f"✓ 事件映射已注销: {event_type}") + return True + else: + print(f"✗ 事件映射不存在: {event_type}") + return False + except Exception as e: + print(f"✗ 事件映射注销失败: {e}") + return False + + def set_event_priority(self, event_type: str, priority: int) -> bool: + """ + 设置事件优先级 + + Args: + event_type: 事件类型 + priority: 优先级值 + + Returns: + 是否设置成功 + """ + try: + self.event_priority[event_type] = priority + print(f"✓ 事件优先级已设置: {event_type} = {priority}") + return True + except Exception as e: + print(f"✗ 事件优先级设置失败: {e}") + return False + + def set_event_suppression(self, config: Dict[str, Any]) -> bool: + """ + 设置事件抑制配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.event_suppression.update(config) + print(f"✓ 事件抑制配置已更新: {self.event_suppression}") + return True + except Exception as e: + print(f"✗ 事件抑制配置设置失败: {e}") + return False + + def set_event_combination(self, config: Dict[str, Any]) -> bool: + """ + 设置事件组合配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.event_combination.update(config) + print(f"✓ 事件组合配置已更新: {self.event_combination}") + return True + except Exception as e: + print(f"✗ 事件组合配置设置失败: {e}") + return False + + def get_event_mapping(self) -> Dict[str, Dict[str, Any]]: + """ + 获取事件映射 + + Returns: + 事件映射字典 + """ + return self.event_mapping.copy() + + def get_event_priority(self) -> Dict[str, int]: + """ + 获取事件优先级 + + Returns: + 事件优先级字典 + """ + return self.event_priority.copy() + + def get_event_suppression_config(self) -> Dict[str, Any]: + """ + 获取事件抑制配置 + + Returns: + 配置字典 + """ + return self.event_suppression.copy() + + def get_event_combination_config(self) -> Dict[str, Any]: + """ + 获取事件组合配置 + + Returns: + 配置字典 + """ + return self.event_combination.copy() + + def _trigger_event_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发事件回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.event_callbacks: + for callback in self.event_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 事件回调执行失败: {e}") + except Exception as e: + print(f"✗ 事件回调触发失败: {e}") + + def register_event_callback(self, callback_type: str, callback: Callable): + """ + 注册事件回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.event_callbacks: + self.event_callbacks[callback_type].append(callback) + print(f"✓ 事件回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 事件回调注册失败: {e}") + + def unregister_event_callback(self, callback_type: str, callback: Callable): + """ + 注销事件回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.event_callbacks: + if callback in self.event_callbacks[callback_type]: + self.event_callbacks[callback_type].remove(callback) + print(f"✓ 事件回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 事件回调注销失败: {e}") + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + try: + self.stats = { + 'events_received': 0, + 'events_processed': 0, + 'events_suppressed': 0, + 'events_combined': 0, + 'haptic_effects_triggered': 0 + } + print("✓ 事件处理器统计信息已重置") + except Exception as e: print(f"✗ 事件处理器统计信息重置失败: {e}") \ No newline at end of file diff --git a/plugins/user/haptic_feedback_system/physics/physics_integration.py b/plugins/user/haptic_feedback_system/physics/physics_integration.py index 3332e145..0dbcc8d7 100644 --- a/plugins/user/haptic_feedback_system/physics/physics_integration.py +++ b/plugins/user/haptic_feedback_system/physics/physics_integration.py @@ -1,732 +1,732 @@ -""" -物理引擎集成模块 -负责将物理引擎中的碰撞和力反馈转换为触觉效果 -""" - -import time -from typing import Dict, Any, List, Optional -import math - -class PhysicsIntegration: - """ - 物理引擎集成 - 负责将物理引擎中的碰撞和力反馈转换为触觉效果 - """ - - def __init__(self, plugin): - """ - 初始化物理引擎集成 - - Args: - plugin: 触觉反馈系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 物理事件监听器 - self.physics_listeners = {} - - # 碰撞效果配置 - self.collision_config = { - 'enable_collision_feedback': True, - 'min_impulse_threshold': 0.1, - 'max_impulse_threshold': 100.0, - 'intensity_scale': 1.0, - 'frequency_scale': 1.0, - 'duration_scale': 1.0, - 'effect_type': 'impulse' - } - - # 力反馈配置 - self.force_config = { - 'enable_force_feedback': True, - 'min_force_threshold': 0.01, - 'max_force_threshold': 10.0, - 'intensity_scale': 1.0, - 'frequency_scale': 1.0, - 'smoothing_factor': 0.1 - } - - # 摩擦效果配置 - self.friction_config = { - 'enable_friction_feedback': True, - 'min_velocity_threshold': 0.1, - 'texture_sensitivity': 1.0, - 'vibration_frequency': 20.0 - } - - # 材质反馈配置 - self.material_config = { - 'enable_material_feedback': True, - 'material_effects': { - 'metal': {'intensity': 0.8, 'frequency': 60.0, 'type': 'buzz'}, - 'wood': {'intensity': 0.6, 'frequency': 30.0, 'type': 'rumble'}, - 'stone': {'intensity': 0.9, 'frequency': 80.0, 'type': 'click'}, - 'fabric': {'intensity': 0.3, 'frequency': 15.0, 'type': 'soft_rumble'}, - 'rubber': {'intensity': 0.5, 'frequency': 25.0, 'type': 'bounce'} - } - } - - # 状态跟踪 - self.contact_points = {} - self.force_feedback_state = {} - self.friction_state = {} - - # 统计信息 - self.stats = { - 'collisions_detected': 0, - 'forces_processed': 0, - 'friction_effects': 0, - 'material_effects': 0, - 'haptic_effects_generated': 0 - } - - # 回调函数 - self.physics_callbacks = { - 'collision_detected': [], - 'force_applied': [], - 'friction_updated': [], - 'haptic_effect_generated': [] - } - - print("✓ 物理引擎集成已创建") - - def initialize(self) -> bool: - """ - 初始化物理引擎集成 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 物理引擎集成初始化完成") - return True - - except Exception as e: - print(f"✗ 物理引擎集成初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用物理引擎集成 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 物理引擎集成未初始化") - return False - - self.enabled = True - print("✓ 物理引擎集成已启用") - return True - - except Exception as e: - print(f"✗ 物理引擎集成启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用物理引擎集成""" - try: - self.enabled = False - print("✓ 物理引擎集成已禁用") - - except Exception as e: - print(f"✗ 物理引擎集成禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理物理引擎集成资源""" - try: - self.disable() - self.physics_listeners.clear() - self.contact_points.clear() - self.force_feedback_state.clear() - self.friction_state.clear() - self.physics_callbacks.clear() - self.initialized = False - print("✓ 物理引擎集成资源已清理") - - except Exception as e: - print(f"✗ 物理引擎集成资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新物理引擎集成状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新摩擦状态 - self._update_friction_state(dt) - - # 更新力反馈状态 - self._update_force_feedback_state(dt) - - except Exception as e: - print(f"✗ 物理引擎集成更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_friction_state(self, dt: float): - """更新摩擦状态""" - try: - # 减少摩擦状态值(衰减) - decay_rate = 0.95 - keys_to_remove = [] - - for key, state in self.friction_state.items(): - state['intensity'] *= decay_rate - if state['intensity'] < 0.01: - keys_to_remove.append(key) - - # 移除已衰减完毕的状态 - for key in keys_to_remove: - del self.friction_state[key] - - except Exception as e: - print(f"✗ 摩擦状态更新失败: {e}") - - def _update_force_feedback_state(self, dt: float): - """更新力反馈状态""" - try: - # 减少力反馈状态值(平滑) - smoothing = self.force_config['smoothing_factor'] - keys_to_remove = [] - - for key, state in self.force_feedback_state.items(): - state['current_force'] = ( - state['current_force'] * (1 - smoothing) + - state['target_force'] * smoothing - ) - if abs(state['current_force']) < 0.001: - keys_to_remove.append(key) - - # 移除已平滑完毕的状态 - for key in keys_to_remove: - del self.force_feedback_state[key] - - except Exception as e: - print(f"✗ 力反馈状态更新失败: {e}") - - def process_collision(self, collision_data: Dict[str, Any]) -> bool: - """ - 处理碰撞事件并生成触觉效果 - - Args: - collision_data: 碰撞数据 - - Returns: - 是否处理成功 - """ - try: - if not self.enabled or not self.collision_config['enable_collision_feedback']: - return False - - # 提取碰撞信息 - impulse = collision_data.get('impulse', 0.0) - contact_point = collision_data.get('contact_point', (0, 0, 0)) - object_a = collision_data.get('object_a', 'unknown') - object_b = collision_data.get('object_b', 'unknown') - material_a = collision_data.get('material_a', 'default') - material_b = collision_data.get('material_b', 'default') - device_id = collision_data.get('device_id') - - # 检查冲量阈值 - if impulse < self.collision_config['min_impulse_threshold']: - return False - - if impulse > self.collision_config['max_impulse_threshold']: - impulse = self.collision_config['max_impulse_threshold'] - - # 计算触觉效果强度 - intensity = ( - impulse / self.collision_config['max_impulse_threshold'] * - self.collision_config['intensity_scale'] - ) - intensity = max(0.0, min(1.0, intensity)) - - # 计算频率 - frequency = 20.0 + (impulse * self.collision_config['frequency_scale']) - frequency = max(1.0, min(100.0, frequency)) - - # 计算持续时间 - duration = 0.05 + (impulse * 0.01 * self.collision_config['duration_scale']) - duration = max(0.01, min(1.0, duration)) - - # 获取材质效果 - material_effect = self._get_material_effect(material_a, material_b) - - # 生成触觉效果 - effect_type = self.collision_config['effect_type'] - if material_effect: - effect_type = material_effect.get('type', effect_type) - intensity *= material_effect.get('intensity', 1.0) - frequency = material_effect.get('frequency', frequency) - - # 播放触觉效果 - if self.plugin.haptic_manager: - effect_id = self.plugin.haptic_manager.play_effect( - effect_type=effect_type, - device_id=device_id, - intensity=intensity, - duration=duration, - frequency=frequency, - parameters={ - 'collision_impulse': impulse, - 'contact_point': contact_point, - 'objects': [object_a, object_b], - 'materials': [material_a, material_b] - } - ) - - if effect_id >= 0: - # 触发效果生成回调 - self._trigger_physics_callback('haptic_effect_generated', { - 'effect_id': effect_id, - 'effect_type': effect_type, - 'source': 'collision', - 'intensity': intensity, - 'frequency': frequency, - 'duration': duration - }) - - self.stats['haptic_effects_generated'] += 1 - self.stats['collisions_detected'] += 1 - - print(f"✓ 碰撞触觉效果已生成: {effect_type}") - return True - - return False - - except Exception as e: - print(f"✗ 碰撞处理失败: {e}") - return False - - def process_force_feedback(self, force_data: Dict[str, Any]) -> bool: - """ - 处理力反馈并生成连续触觉效果 - - Args: - force_data: 力反馈数据 - - Returns: - 是否处理成功 - """ - try: - if not self.enabled or not self.force_config['enable_force_feedback']: - return False - - # 提取力反馈信息 - force = force_data.get('force', 0.0) - force_direction = force_data.get('direction', (0, 0, 0)) - object_id = force_data.get('object_id', 'unknown') - device_id = force_data.get('device_id') - - # 检查力阈值 - if abs(force) < self.force_config['min_force_threshold']: - return False - - if abs(force) > self.force_config['max_force_threshold']: - force = math.copysign(self.force_config['max_force_threshold'], force) - - # 计算触觉效果强度 - intensity = ( - abs(force) / self.force_config['max_force_threshold'] * - self.force_config['intensity_scale'] - ) - intensity = max(0.0, min(1.0, intensity)) - - # 计算频率 - frequency = 10.0 + (abs(force) * self.force_config['frequency_scale']) - frequency = max(1.0, min(100.0, frequency)) - - # 更新力反馈状态 - state_key = f"force_{object_id}" - if state_key not in self.force_feedback_state: - self.force_feedback_state[state_key] = { - 'current_force': 0.0, - 'target_force': force - } - else: - self.force_feedback_state[state_key]['target_force'] = force - - # 生成连续触觉效果 - if self.plugin.haptic_manager: - # 播放连续效果 - effect_id = self.plugin.haptic_manager.play_effect( - effect_type='periodic', - device_id=device_id, - intensity=intensity, - duration=0.1, # 短持续时间,循环播放 - frequency=frequency, - parameters={ - 'force': force, - 'direction': force_direction, - 'object_id': object_id - } - ) - - if effect_id >= 0: - # 触发效果生成回调 - self._trigger_physics_callback('haptic_effect_generated', { - 'effect_id': effect_id, - 'effect_type': 'periodic', - 'source': 'force', - 'intensity': intensity, - 'frequency': frequency - }) - - self.stats['haptic_effects_generated'] += 1 - self.stats['forces_processed'] += 1 - - print(f"✓ 力反馈触觉效果已生成: 强度 {intensity:.2f}") - return True - - return False - - except Exception as e: - print(f"✗ 力反馈处理失败: {e}") - return False - - def process_friction(self, friction_data: Dict[str, Any]) -> bool: - """ - 处理摩擦并生成纹理触觉效果 - - Args: - friction_data: 摩擦数据 - - Returns: - 是否处理成功 - """ - try: - if not self.enabled or not self.friction_config['enable_friction_feedback']: - return False - - # 提取摩擦信息 - velocity = friction_data.get('velocity', 0.0) - surface_normal = friction_data.get('normal', (0, 0, 1)) - object_id = friction_data.get('object_id', 'unknown') - material = friction_data.get('material', 'default') - device_id = friction_data.get('device_id') - - # 检查速度阈值 - if abs(velocity) < self.friction_config['min_velocity_threshold']: - return False - - # 计算触觉效果强度 - intensity = min(1.0, abs(velocity) * 0.1 * self.friction_config['texture_sensitivity']) - - # 获取材质效果 - material_effect = self._get_material_effect(material) - - # 计算频率 - frequency = self.friction_config['vibration_frequency'] - if material_effect: - frequency = material_effect.get('frequency', frequency) - - # 更新摩擦状态 - state_key = f"friction_{object_id}" - if state_key not in self.friction_state: - self.friction_state[state_key] = { - 'intensity': intensity, - 'velocity': velocity - } - else: - self.friction_state[state_key]['intensity'] = max( - self.friction_state[state_key]['intensity'], - intensity - ) - - # 生成摩擦触觉效果 - if self.plugin.haptic_manager: - effect_id = self.plugin.haptic_manager.play_effect( - effect_type='noise', - device_id=device_id, - intensity=intensity, - duration=0.05, - frequency=frequency, - parameters={ - 'velocity': velocity, - 'material': material, - 'object_id': object_id - } - ) - - if effect_id >= 0: - # 触发效果生成回调 - self._trigger_physics_callback('haptic_effect_generated', { - 'effect_id': effect_id, - 'effect_type': 'noise', - 'source': 'friction', - 'intensity': intensity, - 'frequency': frequency - }) - - self.stats['haptic_effects_generated'] += 1 - self.stats['friction_effects'] += 1 - - print(f"✓ 摩擦触觉效果已生成: 强度 {intensity:.2f}") - return True - - return False - - except Exception as e: - print(f"✗ 摩擦处理失败: {e}") - return False - - def _get_material_effect(self, *materials) -> Optional[Dict[str, Any]]: - """ - 获取材质效果配置 - - Args: - *materials: 材质名称 - - Returns: - 材质效果配置或None - """ - try: - if not self.material_config['enable_material_feedback']: - return None - - material_effects = self.material_config['material_effects'] - - # 尝试匹配每个材质 - for material in materials: - if material in material_effects: - return material_effects[material] - - return None - except Exception as e: - print(f"✗ 材质效果获取失败: {e}") - return None - - def register_physics_listener(self, event_type: str, callback: callable): - """ - 注册物理事件监听器 - - Args: - event_type: 事件类型 - callback: 回调函数 - """ - try: - if event_type in self.physics_listeners: - self.physics_listeners[event_type].append(callback) - else: - self.physics_listeners[event_type] = [callback] - - print(f"✓ 物理事件监听器已注册: {event_type}") - except Exception as e: - print(f"✗ 物理事件监听器注册失败: {e}") - - def unregister_physics_listener(self, event_type: str, callback: callable): - """ - 注销物理事件监听器 - - Args: - event_type: 事件类型 - callback: 回调函数 - """ - try: - if event_type in self.physics_listeners: - if callback in self.physics_listeners[event_type]: - self.physics_listeners[event_type].remove(callback) - print(f"✓ 物理事件监听器已注销: {event_type}") - except Exception as e: - print(f"✗ 物理事件监听器注销失败: {e}") - - def _trigger_physics_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发物理回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.physics_callbacks: - for callback in self.physics_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 物理回调执行失败: {e}") - except Exception as e: - print(f"✗ 物理回调触发失败: {e}") - - def register_physics_callback(self, callback_type: str, callback: callable): - """ - 注册物理回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.physics_callbacks: - self.physics_callbacks[callback_type].append(callback) - print(f"✓ 物理回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 物理回调注册失败: {e}") - - def unregister_physics_callback(self, callback_type: str, callback: callable): - """ - 注销物理回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.physics_callbacks: - if callback in self.physics_callbacks[callback_type]: - self.physics_callbacks[callback_type].remove(callback) - print(f"✓ 物理回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 物理回调注销失败: {e}") - - def set_collision_config(self, config: Dict[str, Any]) -> bool: - """ - 设置碰撞配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.collision_config.update(config) - print(f"✓ 碰撞配置已更新: {self.collision_config}") - return True - except Exception as e: - print(f"✗ 碰撞配置设置失败: {e}") - return False - - def set_force_config(self, config: Dict[str, Any]) -> bool: - """ - 设置力反馈配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.force_config.update(config) - print(f"✓ 力反馈配置已更新: {self.force_config}") - return True - except Exception as e: - print(f"✗ 力反馈配置设置失败: {e}") - return False - - def set_friction_config(self, config: Dict[str, Any]) -> bool: - """ - 设置摩擦配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.friction_config.update(config) - print(f"✓ 摩擦配置已更新: {self.friction_config}") - return True - except Exception as e: - print(f"✗ 摩擦配置设置失败: {e}") - return False - - def set_material_config(self, config: Dict[str, Any]) -> bool: - """ - 设置材质配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.material_config.update(config) - print(f"✓ 材质配置已更新: {self.material_config}") - return True - except Exception as e: - print(f"✗ 材质配置设置失败: {e}") - return False - - def get_collision_config(self) -> Dict[str, Any]: - """ - 获取碰撞配置 - - Returns: - 配置字典 - """ - return self.collision_config.copy() - - def get_force_config(self) -> Dict[str, Any]: - """ - 获取力反馈配置 - - Returns: - 配置字典 - """ - return self.force_config.copy() - - def get_friction_config(self) -> Dict[str, Any]: - """ - 获取摩擦配置 - - Returns: - 配置字典 - """ - return self.friction_config.copy() - - def get_material_config(self) -> Dict[str, Any]: - """ - 获取材质配置 - - Returns: - 配置字典 - """ - return self.material_config.copy() - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - try: - self.stats = { - 'collisions_detected': 0, - 'forces_processed': 0, - 'friction_effects': 0, - 'material_effects': 0, - 'haptic_effects_generated': 0 - } - print("✓ 物理引擎集成统计信息已重置") - except Exception as e: +""" +物理引擎集成模块 +负责将物理引擎中的碰撞和力反馈转换为触觉效果 +""" + +import time +from typing import Dict, Any, List, Optional +import math + +class PhysicsIntegration: + """ + 物理引擎集成 + 负责将物理引擎中的碰撞和力反馈转换为触觉效果 + """ + + def __init__(self, plugin): + """ + 初始化物理引擎集成 + + Args: + plugin: 触觉反馈系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 物理事件监听器 + self.physics_listeners = {} + + # 碰撞效果配置 + self.collision_config = { + 'enable_collision_feedback': True, + 'min_impulse_threshold': 0.1, + 'max_impulse_threshold': 100.0, + 'intensity_scale': 1.0, + 'frequency_scale': 1.0, + 'duration_scale': 1.0, + 'effect_type': 'impulse' + } + + # 力反馈配置 + self.force_config = { + 'enable_force_feedback': True, + 'min_force_threshold': 0.01, + 'max_force_threshold': 10.0, + 'intensity_scale': 1.0, + 'frequency_scale': 1.0, + 'smoothing_factor': 0.1 + } + + # 摩擦效果配置 + self.friction_config = { + 'enable_friction_feedback': True, + 'min_velocity_threshold': 0.1, + 'texture_sensitivity': 1.0, + 'vibration_frequency': 20.0 + } + + # 材质反馈配置 + self.material_config = { + 'enable_material_feedback': True, + 'material_effects': { + 'metal': {'intensity': 0.8, 'frequency': 60.0, 'type': 'buzz'}, + 'wood': {'intensity': 0.6, 'frequency': 30.0, 'type': 'rumble'}, + 'stone': {'intensity': 0.9, 'frequency': 80.0, 'type': 'click'}, + 'fabric': {'intensity': 0.3, 'frequency': 15.0, 'type': 'soft_rumble'}, + 'rubber': {'intensity': 0.5, 'frequency': 25.0, 'type': 'bounce'} + } + } + + # 状态跟踪 + self.contact_points = {} + self.force_feedback_state = {} + self.friction_state = {} + + # 统计信息 + self.stats = { + 'collisions_detected': 0, + 'forces_processed': 0, + 'friction_effects': 0, + 'material_effects': 0, + 'haptic_effects_generated': 0 + } + + # 回调函数 + self.physics_callbacks = { + 'collision_detected': [], + 'force_applied': [], + 'friction_updated': [], + 'haptic_effect_generated': [] + } + + print("✓ 物理引擎集成已创建") + + def initialize(self) -> bool: + """ + 初始化物理引擎集成 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 物理引擎集成初始化完成") + return True + + except Exception as e: + print(f"✗ 物理引擎集成初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用物理引擎集成 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 物理引擎集成未初始化") + return False + + self.enabled = True + print("✓ 物理引擎集成已启用") + return True + + except Exception as e: + print(f"✗ 物理引擎集成启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用物理引擎集成""" + try: + self.enabled = False + print("✓ 物理引擎集成已禁用") + + except Exception as e: + print(f"✗ 物理引擎集成禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理物理引擎集成资源""" + try: + self.disable() + self.physics_listeners.clear() + self.contact_points.clear() + self.force_feedback_state.clear() + self.friction_state.clear() + self.physics_callbacks.clear() + self.initialized = False + print("✓ 物理引擎集成资源已清理") + + except Exception as e: + print(f"✗ 物理引擎集成资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新物理引擎集成状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新摩擦状态 + self._update_friction_state(dt) + + # 更新力反馈状态 + self._update_force_feedback_state(dt) + + except Exception as e: + print(f"✗ 物理引擎集成更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_friction_state(self, dt: float): + """更新摩擦状态""" + try: + # 减少摩擦状态值(衰减) + decay_rate = 0.95 + keys_to_remove = [] + + for key, state in self.friction_state.items(): + state['intensity'] *= decay_rate + if state['intensity'] < 0.01: + keys_to_remove.append(key) + + # 移除已衰减完毕的状态 + for key in keys_to_remove: + del self.friction_state[key] + + except Exception as e: + print(f"✗ 摩擦状态更新失败: {e}") + + def _update_force_feedback_state(self, dt: float): + """更新力反馈状态""" + try: + # 减少力反馈状态值(平滑) + smoothing = self.force_config['smoothing_factor'] + keys_to_remove = [] + + for key, state in self.force_feedback_state.items(): + state['current_force'] = ( + state['current_force'] * (1 - smoothing) + + state['target_force'] * smoothing + ) + if abs(state['current_force']) < 0.001: + keys_to_remove.append(key) + + # 移除已平滑完毕的状态 + for key in keys_to_remove: + del self.force_feedback_state[key] + + except Exception as e: + print(f"✗ 力反馈状态更新失败: {e}") + + def process_collision(self, collision_data: Dict[str, Any]) -> bool: + """ + 处理碰撞事件并生成触觉效果 + + Args: + collision_data: 碰撞数据 + + Returns: + 是否处理成功 + """ + try: + if not self.enabled or not self.collision_config['enable_collision_feedback']: + return False + + # 提取碰撞信息 + impulse = collision_data.get('impulse', 0.0) + contact_point = collision_data.get('contact_point', (0, 0, 0)) + object_a = collision_data.get('object_a', 'unknown') + object_b = collision_data.get('object_b', 'unknown') + material_a = collision_data.get('material_a', 'default') + material_b = collision_data.get('material_b', 'default') + device_id = collision_data.get('device_id') + + # 检查冲量阈值 + if impulse < self.collision_config['min_impulse_threshold']: + return False + + if impulse > self.collision_config['max_impulse_threshold']: + impulse = self.collision_config['max_impulse_threshold'] + + # 计算触觉效果强度 + intensity = ( + impulse / self.collision_config['max_impulse_threshold'] * + self.collision_config['intensity_scale'] + ) + intensity = max(0.0, min(1.0, intensity)) + + # 计算频率 + frequency = 20.0 + (impulse * self.collision_config['frequency_scale']) + frequency = max(1.0, min(100.0, frequency)) + + # 计算持续时间 + duration = 0.05 + (impulse * 0.01 * self.collision_config['duration_scale']) + duration = max(0.01, min(1.0, duration)) + + # 获取材质效果 + material_effect = self._get_material_effect(material_a, material_b) + + # 生成触觉效果 + effect_type = self.collision_config['effect_type'] + if material_effect: + effect_type = material_effect.get('type', effect_type) + intensity *= material_effect.get('intensity', 1.0) + frequency = material_effect.get('frequency', frequency) + + # 播放触觉效果 + if self.plugin.haptic_manager: + effect_id = self.plugin.haptic_manager.play_effect( + effect_type=effect_type, + device_id=device_id, + intensity=intensity, + duration=duration, + frequency=frequency, + parameters={ + 'collision_impulse': impulse, + 'contact_point': contact_point, + 'objects': [object_a, object_b], + 'materials': [material_a, material_b] + } + ) + + if effect_id >= 0: + # 触发效果生成回调 + self._trigger_physics_callback('haptic_effect_generated', { + 'effect_id': effect_id, + 'effect_type': effect_type, + 'source': 'collision', + 'intensity': intensity, + 'frequency': frequency, + 'duration': duration + }) + + self.stats['haptic_effects_generated'] += 1 + self.stats['collisions_detected'] += 1 + + print(f"✓ 碰撞触觉效果已生成: {effect_type}") + return True + + return False + + except Exception as e: + print(f"✗ 碰撞处理失败: {e}") + return False + + def process_force_feedback(self, force_data: Dict[str, Any]) -> bool: + """ + 处理力反馈并生成连续触觉效果 + + Args: + force_data: 力反馈数据 + + Returns: + 是否处理成功 + """ + try: + if not self.enabled or not self.force_config['enable_force_feedback']: + return False + + # 提取力反馈信息 + force = force_data.get('force', 0.0) + force_direction = force_data.get('direction', (0, 0, 0)) + object_id = force_data.get('object_id', 'unknown') + device_id = force_data.get('device_id') + + # 检查力阈值 + if abs(force) < self.force_config['min_force_threshold']: + return False + + if abs(force) > self.force_config['max_force_threshold']: + force = math.copysign(self.force_config['max_force_threshold'], force) + + # 计算触觉效果强度 + intensity = ( + abs(force) / self.force_config['max_force_threshold'] * + self.force_config['intensity_scale'] + ) + intensity = max(0.0, min(1.0, intensity)) + + # 计算频率 + frequency = 10.0 + (abs(force) * self.force_config['frequency_scale']) + frequency = max(1.0, min(100.0, frequency)) + + # 更新力反馈状态 + state_key = f"force_{object_id}" + if state_key not in self.force_feedback_state: + self.force_feedback_state[state_key] = { + 'current_force': 0.0, + 'target_force': force + } + else: + self.force_feedback_state[state_key]['target_force'] = force + + # 生成连续触觉效果 + if self.plugin.haptic_manager: + # 播放连续效果 + effect_id = self.plugin.haptic_manager.play_effect( + effect_type='periodic', + device_id=device_id, + intensity=intensity, + duration=0.1, # 短持续时间,循环播放 + frequency=frequency, + parameters={ + 'force': force, + 'direction': force_direction, + 'object_id': object_id + } + ) + + if effect_id >= 0: + # 触发效果生成回调 + self._trigger_physics_callback('haptic_effect_generated', { + 'effect_id': effect_id, + 'effect_type': 'periodic', + 'source': 'force', + 'intensity': intensity, + 'frequency': frequency + }) + + self.stats['haptic_effects_generated'] += 1 + self.stats['forces_processed'] += 1 + + print(f"✓ 力反馈触觉效果已生成: 强度 {intensity:.2f}") + return True + + return False + + except Exception as e: + print(f"✗ 力反馈处理失败: {e}") + return False + + def process_friction(self, friction_data: Dict[str, Any]) -> bool: + """ + 处理摩擦并生成纹理触觉效果 + + Args: + friction_data: 摩擦数据 + + Returns: + 是否处理成功 + """ + try: + if not self.enabled or not self.friction_config['enable_friction_feedback']: + return False + + # 提取摩擦信息 + velocity = friction_data.get('velocity', 0.0) + surface_normal = friction_data.get('normal', (0, 0, 1)) + object_id = friction_data.get('object_id', 'unknown') + material = friction_data.get('material', 'default') + device_id = friction_data.get('device_id') + + # 检查速度阈值 + if abs(velocity) < self.friction_config['min_velocity_threshold']: + return False + + # 计算触觉效果强度 + intensity = min(1.0, abs(velocity) * 0.1 * self.friction_config['texture_sensitivity']) + + # 获取材质效果 + material_effect = self._get_material_effect(material) + + # 计算频率 + frequency = self.friction_config['vibration_frequency'] + if material_effect: + frequency = material_effect.get('frequency', frequency) + + # 更新摩擦状态 + state_key = f"friction_{object_id}" + if state_key not in self.friction_state: + self.friction_state[state_key] = { + 'intensity': intensity, + 'velocity': velocity + } + else: + self.friction_state[state_key]['intensity'] = max( + self.friction_state[state_key]['intensity'], + intensity + ) + + # 生成摩擦触觉效果 + if self.plugin.haptic_manager: + effect_id = self.plugin.haptic_manager.play_effect( + effect_type='noise', + device_id=device_id, + intensity=intensity, + duration=0.05, + frequency=frequency, + parameters={ + 'velocity': velocity, + 'material': material, + 'object_id': object_id + } + ) + + if effect_id >= 0: + # 触发效果生成回调 + self._trigger_physics_callback('haptic_effect_generated', { + 'effect_id': effect_id, + 'effect_type': 'noise', + 'source': 'friction', + 'intensity': intensity, + 'frequency': frequency + }) + + self.stats['haptic_effects_generated'] += 1 + self.stats['friction_effects'] += 1 + + print(f"✓ 摩擦触觉效果已生成: 强度 {intensity:.2f}") + return True + + return False + + except Exception as e: + print(f"✗ 摩擦处理失败: {e}") + return False + + def _get_material_effect(self, *materials) -> Optional[Dict[str, Any]]: + """ + 获取材质效果配置 + + Args: + *materials: 材质名称 + + Returns: + 材质效果配置或None + """ + try: + if not self.material_config['enable_material_feedback']: + return None + + material_effects = self.material_config['material_effects'] + + # 尝试匹配每个材质 + for material in materials: + if material in material_effects: + return material_effects[material] + + return None + except Exception as e: + print(f"✗ 材质效果获取失败: {e}") + return None + + def register_physics_listener(self, event_type: str, callback: callable): + """ + 注册物理事件监听器 + + Args: + event_type: 事件类型 + callback: 回调函数 + """ + try: + if event_type in self.physics_listeners: + self.physics_listeners[event_type].append(callback) + else: + self.physics_listeners[event_type] = [callback] + + print(f"✓ 物理事件监听器已注册: {event_type}") + except Exception as e: + print(f"✗ 物理事件监听器注册失败: {e}") + + def unregister_physics_listener(self, event_type: str, callback: callable): + """ + 注销物理事件监听器 + + Args: + event_type: 事件类型 + callback: 回调函数 + """ + try: + if event_type in self.physics_listeners: + if callback in self.physics_listeners[event_type]: + self.physics_listeners[event_type].remove(callback) + print(f"✓ 物理事件监听器已注销: {event_type}") + except Exception as e: + print(f"✗ 物理事件监听器注销失败: {e}") + + def _trigger_physics_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发物理回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.physics_callbacks: + for callback in self.physics_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 物理回调执行失败: {e}") + except Exception as e: + print(f"✗ 物理回调触发失败: {e}") + + def register_physics_callback(self, callback_type: str, callback: callable): + """ + 注册物理回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.physics_callbacks: + self.physics_callbacks[callback_type].append(callback) + print(f"✓ 物理回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 物理回调注册失败: {e}") + + def unregister_physics_callback(self, callback_type: str, callback: callable): + """ + 注销物理回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.physics_callbacks: + if callback in self.physics_callbacks[callback_type]: + self.physics_callbacks[callback_type].remove(callback) + print(f"✓ 物理回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 物理回调注销失败: {e}") + + def set_collision_config(self, config: Dict[str, Any]) -> bool: + """ + 设置碰撞配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.collision_config.update(config) + print(f"✓ 碰撞配置已更新: {self.collision_config}") + return True + except Exception as e: + print(f"✗ 碰撞配置设置失败: {e}") + return False + + def set_force_config(self, config: Dict[str, Any]) -> bool: + """ + 设置力反馈配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.force_config.update(config) + print(f"✓ 力反馈配置已更新: {self.force_config}") + return True + except Exception as e: + print(f"✗ 力反馈配置设置失败: {e}") + return False + + def set_friction_config(self, config: Dict[str, Any]) -> bool: + """ + 设置摩擦配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.friction_config.update(config) + print(f"✓ 摩擦配置已更新: {self.friction_config}") + return True + except Exception as e: + print(f"✗ 摩擦配置设置失败: {e}") + return False + + def set_material_config(self, config: Dict[str, Any]) -> bool: + """ + 设置材质配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.material_config.update(config) + print(f"✓ 材质配置已更新: {self.material_config}") + return True + except Exception as e: + print(f"✗ 材质配置设置失败: {e}") + return False + + def get_collision_config(self) -> Dict[str, Any]: + """ + 获取碰撞配置 + + Returns: + 配置字典 + """ + return self.collision_config.copy() + + def get_force_config(self) -> Dict[str, Any]: + """ + 获取力反馈配置 + + Returns: + 配置字典 + """ + return self.force_config.copy() + + def get_friction_config(self) -> Dict[str, Any]: + """ + 获取摩擦配置 + + Returns: + 配置字典 + """ + return self.friction_config.copy() + + def get_material_config(self) -> Dict[str, Any]: + """ + 获取材质配置 + + Returns: + 配置字典 + """ + return self.material_config.copy() + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + try: + self.stats = { + 'collisions_detected': 0, + 'forces_processed': 0, + 'friction_effects': 0, + 'material_effects': 0, + 'haptic_effects_generated': 0 + } + print("✓ 物理引擎集成统计信息已重置") + except Exception as e: print(f"✗ 物理引擎集成统计信息重置失败: {e}") \ No newline at end of file diff --git a/plugins/user/haptic_feedback_system/plugin.py b/plugins/user/haptic_feedback_system/plugin.py index c2e4bf24..d35a3c21 100644 --- a/plugins/user/haptic_feedback_system/plugin.py +++ b/plugins/user/haptic_feedback_system/plugin.py @@ -1,265 +1,265 @@ -""" -触觉反馈系统插件 -为EG引擎提供高质量的触觉反馈支持 -""" - -from .core.haptic_manager import HapticManager -from .effects.effect_processor import EffectProcessor -from .devices.device_manager import HapticDeviceManager -from .physics.physics_integration import PhysicsIntegration -from .audio.audio_haptics import AudioHaptics -from .events.event_handler import HapticEventHandler -from .config.config_manager import HapticConfigManager -from .editor.haptic_editor import HapticEditor -from .utils.haptic_utils import HapticUtils - -class HapticFeedbackSystemPlugin: - """ - 触觉反馈系统插件主类 - 整合所有触觉反馈功能模块 - """ - - def __init__(self, engine): - """ - 初始化触觉反馈系统插件 - - Args: - engine: EG引擎实例 - """ - self.engine = engine - self.name = "HapticFeedbackSystem" - self.version = "1.0.0" - self.author = "EG Plugin System" - - # 功能模块 - self.haptic_manager = None - self.effect_processor = None - self.device_manager = None - self.physics_integration = None - self.audio_haptics = None - self.event_handler = None - self.config_manager = None - self.haptic_editor = None - self.haptic_utils = None - - # 插件状态 - self.enabled = False - self.initialized = False - - # 性能统计 - self.stats = { - 'effects_played': 0, - 'devices_connected': 0, - 'events_handled': 0, - 'audio_haptics_generated': 0, - 'physics_haptics_generated': 0 - } - - print(f"✓ 触觉反馈系统插件 v{self.version} 已创建") - - def initialize(self) -> bool: - """ - 初始化插件 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化触觉反馈系统插件...") - - # 初始化核心组件 - self.haptic_manager = HapticManager(self) - self.effect_processor = EffectProcessor(self) - self.device_manager = HapticDeviceManager(self) - self.physics_integration = PhysicsIntegration(self) - self.audio_haptics = AudioHaptics(self) - self.event_handler = HapticEventHandler(self) - self.config_manager = HapticConfigManager(self) - self.haptic_editor = HapticEditor(self) - self.haptic_utils = HapticUtils(self) - - # 初始化各个模块 - modules = [ - self.haptic_manager, - self.effect_processor, - self.device_manager, - self.physics_integration, - self.audio_haptics, - self.event_handler, - self.config_manager, - self.haptic_editor, - self.haptic_utils - ] - - for module in modules: - if module and not module.initialize(): - print(f"✗ 模块初始化失败: {module.__class__.__name__}") - return False - - self.initialized = True - print("✓ 触觉反馈系统插件初始化完成") - return True - - except Exception as e: - print(f"✗ 触觉反馈系统插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 插件未初始化") - return False - - print("正在启用触觉反馈系统插件...") - - # 启用各个模块 - modules = [ - self.haptic_manager, - self.effect_processor, - self.device_manager, - self.physics_integration, - self.audio_haptics, - self.event_handler, - self.config_manager, - self.haptic_editor, - self.haptic_utils - ] - - for module in modules: - if module and not module.enable(): - print(f"✗ 模块启用失败: {module.__class__.__name__}") - return False - - self.enabled = True - print("✓ 触觉反馈系统插件已启用") - return True - - except Exception as e: - print(f"✗ 触觉反馈系统插件启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用插件""" - try: - if not self.enabled: - return - - print("正在禁用触觉反馈系统插件...") - - # 禁用各个模块 - modules = [ - self.haptic_manager, - self.effect_processor, - self.device_manager, - self.physics_integration, - self.audio_haptics, - self.event_handler, - self.config_manager, - self.haptic_editor, - self.haptic_utils - ] - - for module in modules: - if module: - module.disable() - - self.enabled = False - print("✓ 触觉反馈系统插件已禁用") - - except Exception as e: - print(f"✗ 触觉反馈系统插件禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理插件资源""" - try: - print("正在清理触觉反馈系统插件资源...") - - # 清理各个模块 - modules = [ - self.haptic_manager, - self.effect_processor, - self.device_manager, - self.physics_integration, - self.audio_haptics, - self.event_handler, - self.config_manager, - self.haptic_editor, - self.haptic_utils - ] - - for module in modules: - if module: - module.finalize() - - self.initialized = False - print("✓ 触觉反馈系统插件资源已清理") - - except Exception as e: - print(f"✗ 触觉反馈系统插件资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新插件状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - # 更新各个模块 - modules = [ - self.haptic_manager, - self.effect_processor, - self.device_manager, - self.physics_integration, - self.audio_haptics, - self.event_handler, - self.config_manager, - self.haptic_editor, - self.haptic_utils - ] - - for module in modules: - if module: - module.update(dt) - - except Exception as e: - print(f"✗ 触觉反馈系统插件更新失败: {e}") - import traceback - traceback.print_exc() - - def get_stats(self) -> dict: - """ - 获取插件统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'effects_played': 0, - 'devices_connected': 0, - 'events_handled': 0, - 'audio_haptics_generated': 0, - 'physics_haptics_generated': 0 - } +""" +触觉反馈系统插件 +为EG引擎提供高质量的触觉反馈支持 +""" + +from .core.haptic_manager import HapticManager +from .effects.effect_processor import EffectProcessor +from .devices.device_manager import HapticDeviceManager +from .physics.physics_integration import PhysicsIntegration +from .audio.audio_haptics import AudioHaptics +from .events.event_handler import HapticEventHandler +from .config.config_manager import HapticConfigManager +from .editor.haptic_editor import HapticEditor +from .utils.haptic_utils import HapticUtils + +class HapticFeedbackSystemPlugin: + """ + 触觉反馈系统插件主类 + 整合所有触觉反馈功能模块 + """ + + def __init__(self, engine): + """ + 初始化触觉反馈系统插件 + + Args: + engine: EG引擎实例 + """ + self.engine = engine + self.name = "HapticFeedbackSystem" + self.version = "1.0.0" + self.author = "EG Plugin System" + + # 功能模块 + self.haptic_manager = None + self.effect_processor = None + self.device_manager = None + self.physics_integration = None + self.audio_haptics = None + self.event_handler = None + self.config_manager = None + self.haptic_editor = None + self.haptic_utils = None + + # 插件状态 + self.enabled = False + self.initialized = False + + # 性能统计 + self.stats = { + 'effects_played': 0, + 'devices_connected': 0, + 'events_handled': 0, + 'audio_haptics_generated': 0, + 'physics_haptics_generated': 0 + } + + print(f"✓ 触觉反馈系统插件 v{self.version} 已创建") + + def initialize(self) -> bool: + """ + 初始化插件 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化触觉反馈系统插件...") + + # 初始化核心组件 + self.haptic_manager = HapticManager(self) + self.effect_processor = EffectProcessor(self) + self.device_manager = HapticDeviceManager(self) + self.physics_integration = PhysicsIntegration(self) + self.audio_haptics = AudioHaptics(self) + self.event_handler = HapticEventHandler(self) + self.config_manager = HapticConfigManager(self) + self.haptic_editor = HapticEditor(self) + self.haptic_utils = HapticUtils(self) + + # 初始化各个模块 + modules = [ + self.haptic_manager, + self.effect_processor, + self.device_manager, + self.physics_integration, + self.audio_haptics, + self.event_handler, + self.config_manager, + self.haptic_editor, + self.haptic_utils + ] + + for module in modules: + if module and not module.initialize(): + print(f"✗ 模块初始化失败: {module.__class__.__name__}") + return False + + self.initialized = True + print("✓ 触觉反馈系统插件初始化完成") + return True + + except Exception as e: + print(f"✗ 触觉反馈系统插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 插件未初始化") + return False + + print("正在启用触觉反馈系统插件...") + + # 启用各个模块 + modules = [ + self.haptic_manager, + self.effect_processor, + self.device_manager, + self.physics_integration, + self.audio_haptics, + self.event_handler, + self.config_manager, + self.haptic_editor, + self.haptic_utils + ] + + for module in modules: + if module and not module.enable(): + print(f"✗ 模块启用失败: {module.__class__.__name__}") + return False + + self.enabled = True + print("✓ 触觉反馈系统插件已启用") + return True + + except Exception as e: + print(f"✗ 触觉反馈系统插件启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用插件""" + try: + if not self.enabled: + return + + print("正在禁用触觉反馈系统插件...") + + # 禁用各个模块 + modules = [ + self.haptic_manager, + self.effect_processor, + self.device_manager, + self.physics_integration, + self.audio_haptics, + self.event_handler, + self.config_manager, + self.haptic_editor, + self.haptic_utils + ] + + for module in modules: + if module: + module.disable() + + self.enabled = False + print("✓ 触觉反馈系统插件已禁用") + + except Exception as e: + print(f"✗ 触觉反馈系统插件禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理插件资源""" + try: + print("正在清理触觉反馈系统插件资源...") + + # 清理各个模块 + modules = [ + self.haptic_manager, + self.effect_processor, + self.device_manager, + self.physics_integration, + self.audio_haptics, + self.event_handler, + self.config_manager, + self.haptic_editor, + self.haptic_utils + ] + + for module in modules: + if module: + module.finalize() + + self.initialized = False + print("✓ 触觉反馈系统插件资源已清理") + + except Exception as e: + print(f"✗ 触觉反馈系统插件资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新插件状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + # 更新各个模块 + modules = [ + self.haptic_manager, + self.effect_processor, + self.device_manager, + self.physics_integration, + self.audio_haptics, + self.event_handler, + self.config_manager, + self.haptic_editor, + self.haptic_utils + ] + + for module in modules: + if module: + module.update(dt) + + except Exception as e: + print(f"✗ 触觉反馈系统插件更新失败: {e}") + import traceback + traceback.print_exc() + + def get_stats(self) -> dict: + """ + 获取插件统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'effects_played': 0, + 'devices_connected': 0, + 'events_handled': 0, + 'audio_haptics_generated': 0, + 'physics_haptics_generated': 0 + } print("✓ 触觉反馈系统统计信息已重置") \ No newline at end of file diff --git a/plugins/user/haptic_feedback_system/utils/haptic_utils.py b/plugins/user/haptic_feedback_system/utils/haptic_utils.py index 52539e78..30106d6f 100644 --- a/plugins/user/haptic_feedback_system/utils/haptic_utils.py +++ b/plugins/user/haptic_feedback_system/utils/haptic_utils.py @@ -1,624 +1,624 @@ -""" -工具类模块 -提供触觉反馈系统所需的实用工具函数 -""" - -import time -import math -from typing import Dict, Any, List, Optional, Union -import threading -import json - -class HapticUtils: - """ - 触觉工具类 - 提供触觉反馈系统所需的实用工具函数 - """ - - def __init__(self, plugin): - """ - 初始化触觉工具类 - - Args: - plugin: 触觉反馈系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 性能监控 - self.performance_metrics = { - 'update_times': [], - 'effect_processing_times': [], - 'device_response_times': [], - 'memory_usage': 0 - } - - # 日志配置 - self.log_config = { - 'enable_logging': True, - 'log_level': 'INFO', - 'log_file': '/home/hello/EG/plugins/user/haptic_feedback_system/logs/haptic.log', - 'max_log_size': 10 * 1024 * 1024, # 10MB - 'log_format': '[{timestamp}] {level}: {message}' - } - - # 数据转换配置 - self.conversion_config = { - 'intensity_curve': 'linear', # linear, exponential, logarithmic - 'frequency_mapping': 'linear', - 'duration_scaling': 'linear' - } - - # 数学工具 - self.math_utils = MathUtils() - - # 缓存管理 - self.cache_manager = CacheManager() - - # 调试工具 - self.debug_tools = DebugTools() - - # 统计信息 - self.stats = { - 'functions_called': 0, - 'data_processed': 0, - 'cache_hits': 0, - 'cache_misses': 0 - } - - print("✓ 触觉工具类已创建") - - def initialize(self) -> bool: - """ - 初始化触觉工具类 - - Returns: - 是否初始化成功 - """ - try: - # 创建日志目录 - import os - log_dir = "/home/hello/EG/plugins/user/haptic_feedback_system/logs" - os.makedirs(log_dir, exist_ok=True) - - self.initialized = True - print("✓ 触觉工具类初始化完成") - return True - - except Exception as e: - print(f"✗ 触觉工具类初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用触觉工具类 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 触觉工具类未初始化") - return False - - self.enabled = True - print("✓ 触觉工具类已启用") - return True - - except Exception as e: - print(f"✗ 触觉工具类启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用触觉工具类""" - try: - self.enabled = False - print("✓ 触觉工具类已禁用") - - except Exception as e: - print(f"✗ 触觉工具类禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理触觉工具类资源""" - try: - self.disable() - self.performance_metrics.clear() - self.cache_manager.clear() - self.initialized = False - print("✓ 触觉工具类资源已清理") - - except Exception as e: - print(f"✗ 触觉工具类资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新触觉工具类状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新性能指标 - self._update_performance_metrics(dt) - - except Exception as e: - print(f"✗ 触觉工具类更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_performance_metrics(self, dt: float): - """更新性能指标""" - try: - # 记录更新时间 - self.performance_metrics['update_times'].append(dt) - - # 保持历史记录大小 - if len(self.performance_metrics['update_times']) > 1000: - self.performance_metrics['update_times'] = self.performance_metrics['update_times'][-1000:] - - except Exception as e: - print(f"✗ 性能指标更新失败: {e}") - - def log_message(self, message: str, level: str = 'INFO'): - """ - 记录日志消息 - - Args: - message: 日志消息 - level: 日志级别 (DEBUG, INFO, WARNING, ERROR) - """ - try: - if not self.log_config['enable_logging']: - return - - # 检查日志级别 - level_priority = {'DEBUG': 0, 'INFO': 1, 'WARNING': 2, 'ERROR': 3} - config_level = self.log_config['log_level'] - - if level_priority.get(level, 1) < level_priority.get(config_level, 1): - return - - # 格式化日志消息 - timestamp = time.strftime('%Y-%m-%d %H:%M:%S') - formatted_message = self.log_config['log_format'].format( - timestamp=timestamp, - level=level, - message=message - ) - - # 输出到控制台 - print(formatted_message) - - # 写入日志文件 - try: - with open(self.log_config['log_file'], 'a', encoding='utf-8') as f: - f.write(formatted_message + '\n') - except Exception as e: - pass # 忽略文件写入错误 - - except Exception as e: - pass # 忽略日志记录错误 - - def measure_performance(self, func: callable, *args, **kwargs) -> tuple: - """ - 测量函数性能 - - Args: - func: 要测量的函数 - *args: 函数参数 - **kwargs: 函数关键字参数 - - Returns: - (结果, 执行时间) - """ - try: - start_time = time.perf_counter() - result = func(*args, **kwargs) - end_time = time.perf_counter() - - execution_time = end_time - start_time - - # 记录到性能指标 - metric_name = f"{func.__name__}_times" - if metric_name not in self.performance_metrics: - self.performance_metrics[metric_name] = [] - - self.performance_metrics[metric_name].append(execution_time) - - # 保持历史记录大小 - if len(self.performance_metrics[metric_name]) > 1000: - self.performance_metrics[metric_name] = self.performance_metrics[metric_name][-1000:] - - self.stats['functions_called'] += 1 - return (result, execution_time) - - except Exception as e: - self.log_message(f"性能测量失败: {e}", 'ERROR') - raise - - def convert_intensity(self, intensity: float, curve: str = None) -> float: - """ - 转换强度值 - - Args: - intensity: 原始强度值 (0.0-1.0) - curve: 转换曲线类型 - - Returns: - 转换后的强度值 - """ - try: - curve_type = curve or self.conversion_config['intensity_curve'] - intensity = max(0.0, min(1.0, intensity)) - - if curve_type == 'exponential': - # 指数曲线 - return math.pow(intensity, 2) - elif curve_type == 'logarithmic': - # 对数曲线 - return math.log(intensity + 1) / math.log(2) - else: - # 线性曲线 - return intensity - - except Exception as e: - self.log_message(f"强度转换失败: {e}", 'ERROR') - return intensity - - def convert_frequency(self, frequency: float, min_freq: float = 1.0, - max_freq: float = 100.0, mapping: str = None) -> float: - """ - 转换频率值 - - Args: - frequency: 原始频率值 - min_freq: 最小频率 - max_freq: 最大频率 - mapping: 映射类型 - - Returns: - 转换后的频率值 - """ - try: - mapping_type = mapping or self.conversion_config['frequency_mapping'] - frequency = max(0.0, frequency) - - if mapping_type == 'logarithmic': - # 对数映射 - log_min = math.log(max(min_freq, 1.0)) - log_max = math.log(max(max_freq, 1.0)) - log_freq = math.log(max(frequency, 1.0)) - normalized = (log_freq - log_min) / (log_max - log_min) - return min_freq + normalized * (max_freq - min_freq) - else: - # 线性映射 - return max(min_freq, min(max_freq, frequency)) - - except Exception as e: - self.log_message(f"频率转换失败: {e}", 'ERROR') - return frequency - - def scale_duration(self, duration: float, scale_factor: float = 1.0, - scaling_type: str = None) -> float: - """ - 缩放持续时间 - - Args: - duration: 原始持续时间 - scale_factor: 缩放因子 - scaling_type: 缩放类型 - - Returns: - 缩放后的持续时间 - """ - try: - scaling = scaling_type or self.conversion_config['duration_scaling'] - duration = max(0.0, duration) - scale_factor = max(0.0, scale_factor) - - if scaling == 'exponential': - return duration * math.pow(scale_factor, 2) - elif scaling == 'logarithmic': - return duration * math.log(scale_factor + 1) - else: - return duration * scale_factor - - except Exception as e: - self.log_message(f"持续时间缩放失败: {e}", 'ERROR') - return duration - - def interpolate_values(self, start_value: float, end_value: float, - progress: float, interpolation_type: str = 'linear') -> float: - """ - 插值计算 - - Args: - start_value: 起始值 - end_value: 结束值 - progress: 进度 (0.0-1.0) - interpolation_type: 插值类型 - - Returns: - 插值结果 - """ - try: - progress = max(0.0, min(1.0, progress)) - - if interpolation_type == 'ease_in': - progress = math.pow(progress, 2) - elif interpolation_type == 'ease_out': - progress = 1.0 - math.pow(1.0 - progress, 2) - elif interpolation_type == 'ease_in_out': - progress = 0.5 * (1 - math.cos(progress * math.pi)) - # linear 为默认情况,无需特殊处理 - - return start_value + (end_value - start_value) * progress - - except Exception as e: - self.log_message(f"插值计算失败: {e}", 'ERROR') - return start_value - - def calculate_distance(self, pos1: tuple, pos2: tuple) -> float: - """ - 计算两点间距离 - - Args: - pos1: 第一个点 (x, y, z) - pos2: 第二个点 (x, y, z) - - Returns: - 距离值 - """ - try: - dx = pos2[0] - pos1[0] - dy = pos2[1] - pos1[1] - dz = pos2[2] - pos1[2] - return math.sqrt(dx*dx + dy*dy + dz*dz) - except Exception as e: - self.log_message(f"距离计算失败: {e}", 'ERROR') - return 0.0 - - def normalize_vector(self, vector: tuple) -> tuple: - """ - 归一化向量 - - Args: - vector: 输入向量 (x, y, z) - - Returns: - 归一化后的向量 - """ - try: - magnitude = math.sqrt(vector[0]**2 + vector[1]**2 + vector[2]**2) - if magnitude > 0: - return (vector[0]/magnitude, vector[1]/magnitude, vector[2]/magnitude) - else: - return (0, 0, 0) - except Exception as e: - self.log_message(f"向量归一化失败: {e}", 'ERROR') - return (0, 0, 0) - - def get_performance_stats(self) -> Dict[str, Any]: - """ - 获取性能统计信息 - - Returns: - 性能统计字典 - """ - try: - stats = {} - for key, times in self.performance_metrics.items(): - if times: - stats[key] = { - 'count': len(times), - 'average': sum(times) / len(times), - 'min': min(times), - 'max': max(times), - 'total': sum(times) - } - return stats - except Exception as e: - self.log_message(f"性能统计获取失败: {e}", 'ERROR') - return {} - - def get_system_info(self) -> Dict[str, Any]: - """ - 获取系统信息 - - Returns: - 系统信息字典 - """ - try: - import platform - import psutil - - return { - 'platform': platform.system(), - 'platform_version': platform.version(), - 'processor': platform.processor(), - 'cpu_count': psutil.cpu_count(), - 'memory_total': psutil.virtual_memory().total, - 'memory_available': psutil.virtual_memory().available, - 'plugin_version': getattr(self.plugin, 'version', 'unknown') - } - except Exception as e: - self.log_message(f"系统信息获取失败: {e}", 'ERROR') - return {} - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - try: - self.stats = { - 'functions_called': 0, - 'data_processed': 0, - 'cache_hits': 0, - 'cache_misses': 0 - } - print("✓ 工具类统计信息已重置") - except Exception as e: - self.log_message(f"统计信息重置失败: {e}", 'ERROR') - - def set_log_config(self, config: Dict[str, Any]) -> bool: - """ - 设置日志配置 - - Args: - config: 日志配置字典 - - Returns: - 是否设置成功 - """ - try: - self.log_config.update(config) - self.log_message(f"日志配置已更新: {self.log_config}", 'INFO') - return True - except Exception as e: - self.log_message(f"日志配置设置失败: {e}", 'ERROR') - return False - - def set_conversion_config(self, config: Dict[str, Any]) -> bool: - """ - 设置转换配置 - - Args: - config: 转换配置字典 - - Returns: - 是否设置成功 - """ - try: - self.conversion_config.update(config) - self.log_message(f"转换配置已更新: {self.conversion_config}", 'INFO') - return True - except Exception as e: - self.log_message(f"转换配置设置失败: {e}", 'ERROR') - return False - - def get_cache_manager(self): - """ - 获取缓存管理器 - - Returns: - 缓存管理器实例 - """ - return self.cache_manager - - def get_debug_tools(self): - """ - 获取调试工具 - - Returns: - 调试工具实例 - """ - return self.debug_tools - -class MathUtils: - """数学工具类""" - - @staticmethod - def clamp(value: float, min_value: float, max_value: float) -> float: - """限制值在指定范围内""" - return max(min_value, min(max_value, value)) - - @staticmethod - def lerp(start: float, end: float, t: float) -> float: - """线性插值""" - return start + (end - start) * t - - @staticmethod - def smoothstep(edge0: float, edge1: float, x: float) -> float: - """平滑插值""" - t = MathUtils.clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0) - return t * t * (3.0 - 2.0 * t) - -class CacheManager: - """缓存管理器""" - - def __init__(self): - self.cache = {} - self.access_times = {} - self.max_size = 1000 - - def get(self, key: str) -> Optional[Any]: - """获取缓存值""" - if key in self.cache: - self.access_times[key] = time.time() - return self.cache[key] - return None - - def set(self, key: str, value: Any, ttl: float = 300.0): - """设置缓存值""" - self.cache[key] = value - self.access_times[key] = time.time() - - # 检查缓存大小 - if len(self.cache) > self.max_size: - self._cleanup() - - def _cleanup(self): - """清理过期缓存""" - current_time = time.time() - expired_keys = [ - key for key, access_time in self.access_times.items() - if current_time - access_time > 300.0 - ] - - for key in expired_keys: - del self.cache[key] - del self.access_times[key] - - def clear(self): - """清空缓存""" - self.cache.clear() - self.access_times.clear() - -class DebugTools: - """调试工具类""" - - def __init__(self): - self.debug_enabled = False - self.debug_data = {} - - def enable_debug(self): - """启用调试模式""" - self.debug_enabled = True - - def disable_debug(self): - """禁用调试模式""" - self.debug_enabled = False - - def log_debug_data(self, key: str, data: Any): - """记录调试数据""" - if self.debug_enabled: - self.debug_data[key] = data - - def get_debug_data(self, key: str = None) -> Union[Dict[str, Any], Any]: - """获取调试数据""" - if key: - return self.debug_data.get(key) - return self.debug_data.copy() - - def clear_debug_data(self): - """清空调试数据""" +""" +工具类模块 +提供触觉反馈系统所需的实用工具函数 +""" + +import time +import math +from typing import Dict, Any, List, Optional, Union +import threading +import json + +class HapticUtils: + """ + 触觉工具类 + 提供触觉反馈系统所需的实用工具函数 + """ + + def __init__(self, plugin): + """ + 初始化触觉工具类 + + Args: + plugin: 触觉反馈系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 性能监控 + self.performance_metrics = { + 'update_times': [], + 'effect_processing_times': [], + 'device_response_times': [], + 'memory_usage': 0 + } + + # 日志配置 + self.log_config = { + 'enable_logging': True, + 'log_level': 'INFO', + 'log_file': '/home/hello/EG/plugins/user/haptic_feedback_system/logs/haptic.log', + 'max_log_size': 10 * 1024 * 1024, # 10MB + 'log_format': '[{timestamp}] {level}: {message}' + } + + # 数据转换配置 + self.conversion_config = { + 'intensity_curve': 'linear', # linear, exponential, logarithmic + 'frequency_mapping': 'linear', + 'duration_scaling': 'linear' + } + + # 数学工具 + self.math_utils = MathUtils() + + # 缓存管理 + self.cache_manager = CacheManager() + + # 调试工具 + self.debug_tools = DebugTools() + + # 统计信息 + self.stats = { + 'functions_called': 0, + 'data_processed': 0, + 'cache_hits': 0, + 'cache_misses': 0 + } + + print("✓ 触觉工具类已创建") + + def initialize(self) -> bool: + """ + 初始化触觉工具类 + + Returns: + 是否初始化成功 + """ + try: + # 创建日志目录 + import os + log_dir = "/home/hello/EG/plugins/user/haptic_feedback_system/logs" + os.makedirs(log_dir, exist_ok=True) + + self.initialized = True + print("✓ 触觉工具类初始化完成") + return True + + except Exception as e: + print(f"✗ 触觉工具类初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用触觉工具类 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 触觉工具类未初始化") + return False + + self.enabled = True + print("✓ 触觉工具类已启用") + return True + + except Exception as e: + print(f"✗ 触觉工具类启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用触觉工具类""" + try: + self.enabled = False + print("✓ 触觉工具类已禁用") + + except Exception as e: + print(f"✗ 触觉工具类禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理触觉工具类资源""" + try: + self.disable() + self.performance_metrics.clear() + self.cache_manager.clear() + self.initialized = False + print("✓ 触觉工具类资源已清理") + + except Exception as e: + print(f"✗ 触觉工具类资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新触觉工具类状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新性能指标 + self._update_performance_metrics(dt) + + except Exception as e: + print(f"✗ 触觉工具类更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_performance_metrics(self, dt: float): + """更新性能指标""" + try: + # 记录更新时间 + self.performance_metrics['update_times'].append(dt) + + # 保持历史记录大小 + if len(self.performance_metrics['update_times']) > 1000: + self.performance_metrics['update_times'] = self.performance_metrics['update_times'][-1000:] + + except Exception as e: + print(f"✗ 性能指标更新失败: {e}") + + def log_message(self, message: str, level: str = 'INFO'): + """ + 记录日志消息 + + Args: + message: 日志消息 + level: 日志级别 (DEBUG, INFO, WARNING, ERROR) + """ + try: + if not self.log_config['enable_logging']: + return + + # 检查日志级别 + level_priority = {'DEBUG': 0, 'INFO': 1, 'WARNING': 2, 'ERROR': 3} + config_level = self.log_config['log_level'] + + if level_priority.get(level, 1) < level_priority.get(config_level, 1): + return + + # 格式化日志消息 + timestamp = time.strftime('%Y-%m-%d %H:%M:%S') + formatted_message = self.log_config['log_format'].format( + timestamp=timestamp, + level=level, + message=message + ) + + # 输出到控制台 + print(formatted_message) + + # 写入日志文件 + try: + with open(self.log_config['log_file'], 'a', encoding='utf-8') as f: + f.write(formatted_message + '\n') + except Exception as e: + pass # 忽略文件写入错误 + + except Exception as e: + pass # 忽略日志记录错误 + + def measure_performance(self, func: callable, *args, **kwargs) -> tuple: + """ + 测量函数性能 + + Args: + func: 要测量的函数 + *args: 函数参数 + **kwargs: 函数关键字参数 + + Returns: + (结果, 执行时间) + """ + try: + start_time = time.perf_counter() + result = func(*args, **kwargs) + end_time = time.perf_counter() + + execution_time = end_time - start_time + + # 记录到性能指标 + metric_name = f"{func.__name__}_times" + if metric_name not in self.performance_metrics: + self.performance_metrics[metric_name] = [] + + self.performance_metrics[metric_name].append(execution_time) + + # 保持历史记录大小 + if len(self.performance_metrics[metric_name]) > 1000: + self.performance_metrics[metric_name] = self.performance_metrics[metric_name][-1000:] + + self.stats['functions_called'] += 1 + return (result, execution_time) + + except Exception as e: + self.log_message(f"性能测量失败: {e}", 'ERROR') + raise + + def convert_intensity(self, intensity: float, curve: str = None) -> float: + """ + 转换强度值 + + Args: + intensity: 原始强度值 (0.0-1.0) + curve: 转换曲线类型 + + Returns: + 转换后的强度值 + """ + try: + curve_type = curve or self.conversion_config['intensity_curve'] + intensity = max(0.0, min(1.0, intensity)) + + if curve_type == 'exponential': + # 指数曲线 + return math.pow(intensity, 2) + elif curve_type == 'logarithmic': + # 对数曲线 + return math.log(intensity + 1) / math.log(2) + else: + # 线性曲线 + return intensity + + except Exception as e: + self.log_message(f"强度转换失败: {e}", 'ERROR') + return intensity + + def convert_frequency(self, frequency: float, min_freq: float = 1.0, + max_freq: float = 100.0, mapping: str = None) -> float: + """ + 转换频率值 + + Args: + frequency: 原始频率值 + min_freq: 最小频率 + max_freq: 最大频率 + mapping: 映射类型 + + Returns: + 转换后的频率值 + """ + try: + mapping_type = mapping or self.conversion_config['frequency_mapping'] + frequency = max(0.0, frequency) + + if mapping_type == 'logarithmic': + # 对数映射 + log_min = math.log(max(min_freq, 1.0)) + log_max = math.log(max(max_freq, 1.0)) + log_freq = math.log(max(frequency, 1.0)) + normalized = (log_freq - log_min) / (log_max - log_min) + return min_freq + normalized * (max_freq - min_freq) + else: + # 线性映射 + return max(min_freq, min(max_freq, frequency)) + + except Exception as e: + self.log_message(f"频率转换失败: {e}", 'ERROR') + return frequency + + def scale_duration(self, duration: float, scale_factor: float = 1.0, + scaling_type: str = None) -> float: + """ + 缩放持续时间 + + Args: + duration: 原始持续时间 + scale_factor: 缩放因子 + scaling_type: 缩放类型 + + Returns: + 缩放后的持续时间 + """ + try: + scaling = scaling_type or self.conversion_config['duration_scaling'] + duration = max(0.0, duration) + scale_factor = max(0.0, scale_factor) + + if scaling == 'exponential': + return duration * math.pow(scale_factor, 2) + elif scaling == 'logarithmic': + return duration * math.log(scale_factor + 1) + else: + return duration * scale_factor + + except Exception as e: + self.log_message(f"持续时间缩放失败: {e}", 'ERROR') + return duration + + def interpolate_values(self, start_value: float, end_value: float, + progress: float, interpolation_type: str = 'linear') -> float: + """ + 插值计算 + + Args: + start_value: 起始值 + end_value: 结束值 + progress: 进度 (0.0-1.0) + interpolation_type: 插值类型 + + Returns: + 插值结果 + """ + try: + progress = max(0.0, min(1.0, progress)) + + if interpolation_type == 'ease_in': + progress = math.pow(progress, 2) + elif interpolation_type == 'ease_out': + progress = 1.0 - math.pow(1.0 - progress, 2) + elif interpolation_type == 'ease_in_out': + progress = 0.5 * (1 - math.cos(progress * math.pi)) + # linear 为默认情况,无需特殊处理 + + return start_value + (end_value - start_value) * progress + + except Exception as e: + self.log_message(f"插值计算失败: {e}", 'ERROR') + return start_value + + def calculate_distance(self, pos1: tuple, pos2: tuple) -> float: + """ + 计算两点间距离 + + Args: + pos1: 第一个点 (x, y, z) + pos2: 第二个点 (x, y, z) + + Returns: + 距离值 + """ + try: + dx = pos2[0] - pos1[0] + dy = pos2[1] - pos1[1] + dz = pos2[2] - pos1[2] + return math.sqrt(dx*dx + dy*dy + dz*dz) + except Exception as e: + self.log_message(f"距离计算失败: {e}", 'ERROR') + return 0.0 + + def normalize_vector(self, vector: tuple) -> tuple: + """ + 归一化向量 + + Args: + vector: 输入向量 (x, y, z) + + Returns: + 归一化后的向量 + """ + try: + magnitude = math.sqrt(vector[0]**2 + vector[1]**2 + vector[2]**2) + if magnitude > 0: + return (vector[0]/magnitude, vector[1]/magnitude, vector[2]/magnitude) + else: + return (0, 0, 0) + except Exception as e: + self.log_message(f"向量归一化失败: {e}", 'ERROR') + return (0, 0, 0) + + def get_performance_stats(self) -> Dict[str, Any]: + """ + 获取性能统计信息 + + Returns: + 性能统计字典 + """ + try: + stats = {} + for key, times in self.performance_metrics.items(): + if times: + stats[key] = { + 'count': len(times), + 'average': sum(times) / len(times), + 'min': min(times), + 'max': max(times), + 'total': sum(times) + } + return stats + except Exception as e: + self.log_message(f"性能统计获取失败: {e}", 'ERROR') + return {} + + def get_system_info(self) -> Dict[str, Any]: + """ + 获取系统信息 + + Returns: + 系统信息字典 + """ + try: + import platform + import psutil + + return { + 'platform': platform.system(), + 'platform_version': platform.version(), + 'processor': platform.processor(), + 'cpu_count': psutil.cpu_count(), + 'memory_total': psutil.virtual_memory().total, + 'memory_available': psutil.virtual_memory().available, + 'plugin_version': getattr(self.plugin, 'version', 'unknown') + } + except Exception as e: + self.log_message(f"系统信息获取失败: {e}", 'ERROR') + return {} + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + try: + self.stats = { + 'functions_called': 0, + 'data_processed': 0, + 'cache_hits': 0, + 'cache_misses': 0 + } + print("✓ 工具类统计信息已重置") + except Exception as e: + self.log_message(f"统计信息重置失败: {e}", 'ERROR') + + def set_log_config(self, config: Dict[str, Any]) -> bool: + """ + 设置日志配置 + + Args: + config: 日志配置字典 + + Returns: + 是否设置成功 + """ + try: + self.log_config.update(config) + self.log_message(f"日志配置已更新: {self.log_config}", 'INFO') + return True + except Exception as e: + self.log_message(f"日志配置设置失败: {e}", 'ERROR') + return False + + def set_conversion_config(self, config: Dict[str, Any]) -> bool: + """ + 设置转换配置 + + Args: + config: 转换配置字典 + + Returns: + 是否设置成功 + """ + try: + self.conversion_config.update(config) + self.log_message(f"转换配置已更新: {self.conversion_config}", 'INFO') + return True + except Exception as e: + self.log_message(f"转换配置设置失败: {e}", 'ERROR') + return False + + def get_cache_manager(self): + """ + 获取缓存管理器 + + Returns: + 缓存管理器实例 + """ + return self.cache_manager + + def get_debug_tools(self): + """ + 获取调试工具 + + Returns: + 调试工具实例 + """ + return self.debug_tools + +class MathUtils: + """数学工具类""" + + @staticmethod + def clamp(value: float, min_value: float, max_value: float) -> float: + """限制值在指定范围内""" + return max(min_value, min(max_value, value)) + + @staticmethod + def lerp(start: float, end: float, t: float) -> float: + """线性插值""" + return start + (end - start) * t + + @staticmethod + def smoothstep(edge0: float, edge1: float, x: float) -> float: + """平滑插值""" + t = MathUtils.clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0) + return t * t * (3.0 - 2.0 * t) + +class CacheManager: + """缓存管理器""" + + def __init__(self): + self.cache = {} + self.access_times = {} + self.max_size = 1000 + + def get(self, key: str) -> Optional[Any]: + """获取缓存值""" + if key in self.cache: + self.access_times[key] = time.time() + return self.cache[key] + return None + + def set(self, key: str, value: Any, ttl: float = 300.0): + """设置缓存值""" + self.cache[key] = value + self.access_times[key] = time.time() + + # 检查缓存大小 + if len(self.cache) > self.max_size: + self._cleanup() + + def _cleanup(self): + """清理过期缓存""" + current_time = time.time() + expired_keys = [ + key for key, access_time in self.access_times.items() + if current_time - access_time > 300.0 + ] + + for key in expired_keys: + del self.cache[key] + del self.access_times[key] + + def clear(self): + """清空缓存""" + self.cache.clear() + self.access_times.clear() + +class DebugTools: + """调试工具类""" + + def __init__(self): + self.debug_enabled = False + self.debug_data = {} + + def enable_debug(self): + """启用调试模式""" + self.debug_enabled = True + + def disable_debug(self): + """禁用调试模式""" + self.debug_enabled = False + + def log_debug_data(self, key: str, data: Any): + """记录调试数据""" + if self.debug_enabled: + self.debug_data[key] = data + + def get_debug_data(self, key: str = None) -> Union[Dict[str, Any], Any]: + """获取调试数据""" + if key: + return self.debug_data.get(key) + return self.debug_data.copy() + + def clear_debug_data(self): + """清空调试数据""" self.debug_data.clear() \ No newline at end of file diff --git a/plugins/user/matchmaking_system/README.md b/plugins/user/matchmaking_system/README.md index 8e7530b5..1ee0f401 100644 --- a/plugins/user/matchmaking_system/README.md +++ b/plugins/user/matchmaking_system/README.md @@ -1,383 +1,383 @@ -# 匹配系统插件 - -为EG引擎提供完整的玩家匹配功能,包括队列管理、算法匹配、房间分配、规则配置、实时监控和数据分析等核心功能。 - -## 功能特性 - -### 1. 匹配管理 -- 智能匹配过程管理 -- 多种匹配模式支持 -- 匹配状态跟踪和控制 -- 匹配结果处理和回调 - -### 2. 队列系统 -- 玩家排队管理 -- 队列优先级处理 -- 超时自动清理 -- 实时队列状态监控 - -### 3. 匹配算法 -- 技能匹配算法 -- 快速匹配算法 -- 团队平衡算法 -- 自定义算法支持 -- 匹配质量评估 - -### 4. 房间分配 -- 动态房间创建和销毁 -- 玩家房间分配管理 -- 房间状态跟踪 -- 房间密码保护 - -### 5. 规则配置 -- 灵活的匹配规则系统 -- 游戏模式特定规则 -- 自定义规则集支持 -- 实时规则验证 - -### 6. 实时监控 -- 性能指标收集 -- 实时数据监控 -- 警报系统 -- 系统健康状态检查 - -### 7. 事件系统 -- 事件驱动架构 -- 异步事件处理 -- 事件过滤和优先级 -- 事件监听器系统 - -### 8. 配置管理 -- 持久化配置存储 -- 动态配置更新 -- 配置变更监听 -- 配置导入导出 - -### 9. 可视化编辑器 -- 实时数据展示 -- 配置参数调整 -- 统计图表显示 -- 数据导出功能 - -### 10. 数据统计 -- 实时数据收集 -- 历史数据分析 -- 统计报告生成 -- 数据导出支持 - -## 系统架构 - -``` -匹配系统插件 -├── 核心模块 -│ └── 匹配管理器 (core/match_manager.py) -├── 队列系统 -│ └── 队列管理器 (queue/queue_manager.py) -├── 算法系统 -│ └── 算法管理器 (algorithms/algorithm_manager.py) -├── 房间系统 -│ └── 房间分配器 (rooms/room_allocator.py) -├── 规则系统 -│ └── 规则管理器 (rules/rule_manager.py) -├── 监控系统 -│ └── 匹配监控器 (monitoring/match_monitor.py) -├── 事件系统 -│ └── 事件处理器 (events/event_handler.py) -├── 配置系统 -│ └── 配置管理器 (config/match_config.py) -├── 编辑器系统 -│ └── 匹配编辑器 (editor/match_editor.py) -├── 统计系统 -│ └── 统计管理器 (stats/stats_manager.py) -└── 插件主文件 - └── 插件入口 (plugin.py) -``` - -## 安装和使用 - -### 安装依赖 - -```bash -# 无特殊依赖,使用标准Python库 -``` - -### 配置插件 - -插件配置文件位于 `config/matchmaking_config.json`,包含以下主要配置项: - -```json -{ - "system": { - "enable_matchmaking": true, - "max_concurrent_matches": 1000, - "default_match_timeout": 300.0 - }, - "queue": { - "default_queue_timeout": 300.0, - "queue_processing_interval": 2.0 - }, - "matching": { - "default_algorithm": "skill_based", - "skill_range": 100 - }, - "room": { - "default_max_players": 8, - "enable_room_passwords": true - } -} -``` - -### 启动插件 - -```python -# 初始化插件 -plugin = MatchmakingSystemPlugin(engine) -plugin.initialize() -plugin.enable() - -# 插件会自动处理匹配过程 -``` - -## API参考 - -### 匹配管理 - -```python -# 开始匹配 -match_id = plugin.match_manager.start_matchmaking(player_ids, match_params) - -# 取消匹配 -plugin.match_manager.cancel_matchmaking(match_id) - -# 获取匹配统计 -stats = plugin.match_manager.get_match_stats() -``` - -### 队列管理 - -```python -# 添加玩家到队列 -plugin.queue_manager.add_players_to_queue(player_ids, queue_params) - -# 从队列移除玩家 -plugin.queue_manager.remove_players_from_queue(player_ids=player_ids) - -# 获取队列信息 -queue_info = plugin.queue_manager.get_queue_info(queue_name) -``` - -### 算法管理 - -```python -# 设置活动算法 -plugin.algorithm_manager.set_active_algorithm("skill_based") - -# 运行匹配算法 -result = plugin.algorithm_manager.run_matching_algorithm(players) - -# 更新玩家技能 -plugin.algorithm_manager.update_player_skill(player_id, skill_level) -``` - -### 房间分配 - -```python -# 创建房间 -room_id = plugin.room_allocator.create_room(room_name, room_settings) - -# 添加玩家到房间 -plugin.room_allocator.add_client_to_room(room_id, player_id) - -# 从房间移除玩家 -plugin.room_allocator.remove_client_from_room(room_id, player_id) -``` - -### 规则管理 - -```python -# 应用规则 -is_valid = plugin.rule_manager.apply_rules(players, game_mode) - -# 添加自定义规则集 -plugin.rule_manager.add_custom_rule_set("custom_rules", rules) - -# 设置活动规则集 -plugin.rule_manager.set_active_rules("custom_rules") -``` - -### 监控系统 - -```python -# 获取性能指标 -metrics = plugin.monitor.get_performance_metrics() - -# 获取活动警报 -alerts = plugin.monitor.get_active_alerts() - -# 记录匹配队列时间 -plugin.monitor.record_match_queue_time(queue_time) -``` - -### 事件系统 - -```python -# 发出事件 -plugin.event_handler.emit_event("match_found", payload) - -# 注册事件处理器 -plugin.event_handler.register_event_handler("match_found", handler_func) - -# 注册事件过滤器 -plugin.event_handler.register_event_filter("match_found", filter_func) -``` - -### 配置管理 - -```python -# 获取配置 -config_value = plugin.config_manager.get_config("section", "key", default_value) - -# 设置配置 -plugin.config_manager.set_config("section", "key", value) - -# 保存配置 -plugin.config_manager.save_config() - -# 加载配置 -plugin.config_manager.load_config() -``` - -### 编辑器系统 - -```python -# 显示编辑器 -plugin.editor.show_editor() - -# 隐藏编辑器 -plugin.editor.hide_editor() - -# 切换标签页 -plugin.editor.switch_tab("overview") - -# 更新配置 -plugin.editor.update_config("section", "key", value) -``` - -### 统计系统 - -```python -# 获取实时统计数据 -realtime_stats = plugin.stats_manager.get_realtime_stats() - -# 生成报告 -report = plugin.stats_manager.generate_report("summary", "24h") - -# 导出统计数据 -plugin.stats_manager.export_stats("stats.json") - -# 重置统计数据 -plugin.stats_manager.reset_stats() -``` - -## 匹配流程 - -### 1. 玩家加入队列 -``` -玩家 -> 队列管理器 -> 添加到匹配队列 -``` - -### 2. 队列处理 -``` -队列管理器 -> 算法管理器 -> 运行匹配算法 -``` - -### 3. 匹配创建 -``` -算法管理器 -> 房间分配器 -> 创建匹配房间 -``` - -### 4. 玩家分配 -``` -房间分配器 -> 玩家 -> 加入匹配房间 -``` - -### 5. 匹配完成 -``` -匹配管理器 -> 事件系统 -> 发出匹配完成事件 -``` - -## 性能优化 - -1. **异步处理**:采用多线程和异步I/O提高并发性能 -2. **智能队列**:高效的队列管理和玩家匹配算法 -3. **内存优化**:及时清理无用数据和对象 -4. **缓存机制**:对频繁访问的数据进行缓存 -5. **批量处理**:批量处理队列和匹配操作 - -## 扩展开发 - -### 添加新的匹配算法 - -```python -# 在算法管理器中实现新的匹配算法 -def _run_custom_matching(self, players): - # 实现自定义匹配逻辑 - pass - -# 注册算法 -plugin.algorithm_manager.register_algorithm("custom", _run_custom_matching) -``` - -### 添加新的事件类型 - -```python -# 注册事件处理器 -plugin.event_handler.register_event_handler("custom_event", custom_handler) - -# 发出事件 -plugin.event_handler.emit_event("custom_event", payload) -``` - -### 添加自定义规则 - -```python -# 添加自定义规则集 -plugin.rule_manager.add_custom_rule_set("my_rules", { - "min_players": 2, - "max_players": 8, - "custom_rule": True -}) - -# 应用规则 -plugin.rule_manager.apply_rules(players, "my_rules") -``` - -## 故障排除 - -### 常见问题 - -1. **匹配失败**:检查匹配规则和算法配置 -2. **队列超时**:调整队列超时时间和匹配参数 -3. **性能问题**:检查系统资源使用情况,优化配置 -4. **配置错误**:验证配置文件格式和参数 - -### 日志分析 - -查看日志文件以诊断问题: -``` -logs/matchmaking_system.log -``` - -## 版本信息 - -- 版本:1.0.0 -- 作者:EG Plugin System -- 许可证:MIT - -## 贡献指南 - -欢迎提交Issue和Pull Request来改进这个插件。 - -## 支持 - +# 匹配系统插件 + +为EG引擎提供完整的玩家匹配功能,包括队列管理、算法匹配、房间分配、规则配置、实时监控和数据分析等核心功能。 + +## 功能特性 + +### 1. 匹配管理 +- 智能匹配过程管理 +- 多种匹配模式支持 +- 匹配状态跟踪和控制 +- 匹配结果处理和回调 + +### 2. 队列系统 +- 玩家排队管理 +- 队列优先级处理 +- 超时自动清理 +- 实时队列状态监控 + +### 3. 匹配算法 +- 技能匹配算法 +- 快速匹配算法 +- 团队平衡算法 +- 自定义算法支持 +- 匹配质量评估 + +### 4. 房间分配 +- 动态房间创建和销毁 +- 玩家房间分配管理 +- 房间状态跟踪 +- 房间密码保护 + +### 5. 规则配置 +- 灵活的匹配规则系统 +- 游戏模式特定规则 +- 自定义规则集支持 +- 实时规则验证 + +### 6. 实时监控 +- 性能指标收集 +- 实时数据监控 +- 警报系统 +- 系统健康状态检查 + +### 7. 事件系统 +- 事件驱动架构 +- 异步事件处理 +- 事件过滤和优先级 +- 事件监听器系统 + +### 8. 配置管理 +- 持久化配置存储 +- 动态配置更新 +- 配置变更监听 +- 配置导入导出 + +### 9. 可视化编辑器 +- 实时数据展示 +- 配置参数调整 +- 统计图表显示 +- 数据导出功能 + +### 10. 数据统计 +- 实时数据收集 +- 历史数据分析 +- 统计报告生成 +- 数据导出支持 + +## 系统架构 + +``` +匹配系统插件 +├── 核心模块 +│ └── 匹配管理器 (core/match_manager.py) +├── 队列系统 +│ └── 队列管理器 (queue/queue_manager.py) +├── 算法系统 +│ └── 算法管理器 (algorithms/algorithm_manager.py) +├── 房间系统 +│ └── 房间分配器 (rooms/room_allocator.py) +├── 规则系统 +│ └── 规则管理器 (rules/rule_manager.py) +├── 监控系统 +│ └── 匹配监控器 (monitoring/match_monitor.py) +├── 事件系统 +│ └── 事件处理器 (events/event_handler.py) +├── 配置系统 +│ └── 配置管理器 (config/match_config.py) +├── 编辑器系统 +│ └── 匹配编辑器 (editor/match_editor.py) +├── 统计系统 +│ └── 统计管理器 (stats/stats_manager.py) +└── 插件主文件 + └── 插件入口 (plugin.py) +``` + +## 安装和使用 + +### 安装依赖 + +```bash +# 无特殊依赖,使用标准Python库 +``` + +### 配置插件 + +插件配置文件位于 `config/matchmaking_config.json`,包含以下主要配置项: + +```json +{ + "system": { + "enable_matchmaking": true, + "max_concurrent_matches": 1000, + "default_match_timeout": 300.0 + }, + "queue": { + "default_queue_timeout": 300.0, + "queue_processing_interval": 2.0 + }, + "matching": { + "default_algorithm": "skill_based", + "skill_range": 100 + }, + "room": { + "default_max_players": 8, + "enable_room_passwords": true + } +} +``` + +### 启动插件 + +```python +# 初始化插件 +plugin = MatchmakingSystemPlugin(engine) +plugin.initialize() +plugin.enable() + +# 插件会自动处理匹配过程 +``` + +## API参考 + +### 匹配管理 + +```python +# 开始匹配 +match_id = plugin.match_manager.start_matchmaking(player_ids, match_params) + +# 取消匹配 +plugin.match_manager.cancel_matchmaking(match_id) + +# 获取匹配统计 +stats = plugin.match_manager.get_match_stats() +``` + +### 队列管理 + +```python +# 添加玩家到队列 +plugin.queue_manager.add_players_to_queue(player_ids, queue_params) + +# 从队列移除玩家 +plugin.queue_manager.remove_players_from_queue(player_ids=player_ids) + +# 获取队列信息 +queue_info = plugin.queue_manager.get_queue_info(queue_name) +``` + +### 算法管理 + +```python +# 设置活动算法 +plugin.algorithm_manager.set_active_algorithm("skill_based") + +# 运行匹配算法 +result = plugin.algorithm_manager.run_matching_algorithm(players) + +# 更新玩家技能 +plugin.algorithm_manager.update_player_skill(player_id, skill_level) +``` + +### 房间分配 + +```python +# 创建房间 +room_id = plugin.room_allocator.create_room(room_name, room_settings) + +# 添加玩家到房间 +plugin.room_allocator.add_client_to_room(room_id, player_id) + +# 从房间移除玩家 +plugin.room_allocator.remove_client_from_room(room_id, player_id) +``` + +### 规则管理 + +```python +# 应用规则 +is_valid = plugin.rule_manager.apply_rules(players, game_mode) + +# 添加自定义规则集 +plugin.rule_manager.add_custom_rule_set("custom_rules", rules) + +# 设置活动规则集 +plugin.rule_manager.set_active_rules("custom_rules") +``` + +### 监控系统 + +```python +# 获取性能指标 +metrics = plugin.monitor.get_performance_metrics() + +# 获取活动警报 +alerts = plugin.monitor.get_active_alerts() + +# 记录匹配队列时间 +plugin.monitor.record_match_queue_time(queue_time) +``` + +### 事件系统 + +```python +# 发出事件 +plugin.event_handler.emit_event("match_found", payload) + +# 注册事件处理器 +plugin.event_handler.register_event_handler("match_found", handler_func) + +# 注册事件过滤器 +plugin.event_handler.register_event_filter("match_found", filter_func) +``` + +### 配置管理 + +```python +# 获取配置 +config_value = plugin.config_manager.get_config("section", "key", default_value) + +# 设置配置 +plugin.config_manager.set_config("section", "key", value) + +# 保存配置 +plugin.config_manager.save_config() + +# 加载配置 +plugin.config_manager.load_config() +``` + +### 编辑器系统 + +```python +# 显示编辑器 +plugin.editor.show_editor() + +# 隐藏编辑器 +plugin.editor.hide_editor() + +# 切换标签页 +plugin.editor.switch_tab("overview") + +# 更新配置 +plugin.editor.update_config("section", "key", value) +``` + +### 统计系统 + +```python +# 获取实时统计数据 +realtime_stats = plugin.stats_manager.get_realtime_stats() + +# 生成报告 +report = plugin.stats_manager.generate_report("summary", "24h") + +# 导出统计数据 +plugin.stats_manager.export_stats("stats.json") + +# 重置统计数据 +plugin.stats_manager.reset_stats() +``` + +## 匹配流程 + +### 1. 玩家加入队列 +``` +玩家 -> 队列管理器 -> 添加到匹配队列 +``` + +### 2. 队列处理 +``` +队列管理器 -> 算法管理器 -> 运行匹配算法 +``` + +### 3. 匹配创建 +``` +算法管理器 -> 房间分配器 -> 创建匹配房间 +``` + +### 4. 玩家分配 +``` +房间分配器 -> 玩家 -> 加入匹配房间 +``` + +### 5. 匹配完成 +``` +匹配管理器 -> 事件系统 -> 发出匹配完成事件 +``` + +## 性能优化 + +1. **异步处理**:采用多线程和异步I/O提高并发性能 +2. **智能队列**:高效的队列管理和玩家匹配算法 +3. **内存优化**:及时清理无用数据和对象 +4. **缓存机制**:对频繁访问的数据进行缓存 +5. **批量处理**:批量处理队列和匹配操作 + +## 扩展开发 + +### 添加新的匹配算法 + +```python +# 在算法管理器中实现新的匹配算法 +def _run_custom_matching(self, players): + # 实现自定义匹配逻辑 + pass + +# 注册算法 +plugin.algorithm_manager.register_algorithm("custom", _run_custom_matching) +``` + +### 添加新的事件类型 + +```python +# 注册事件处理器 +plugin.event_handler.register_event_handler("custom_event", custom_handler) + +# 发出事件 +plugin.event_handler.emit_event("custom_event", payload) +``` + +### 添加自定义规则 + +```python +# 添加自定义规则集 +plugin.rule_manager.add_custom_rule_set("my_rules", { + "min_players": 2, + "max_players": 8, + "custom_rule": True +}) + +# 应用规则 +plugin.rule_manager.apply_rules(players, "my_rules") +``` + +## 故障排除 + +### 常见问题 + +1. **匹配失败**:检查匹配规则和算法配置 +2. **队列超时**:调整队列超时时间和匹配参数 +3. **性能问题**:检查系统资源使用情况,优化配置 +4. **配置错误**:验证配置文件格式和参数 + +### 日志分析 + +查看日志文件以诊断问题: +``` +logs/matchmaking_system.log +``` + +## 版本信息 + +- 版本:1.0.0 +- 作者:EG Plugin System +- 许可证:MIT + +## 贡献指南 + +欢迎提交Issue和Pull Request来改进这个插件。 + +## 支持 + 如需技术支持,请联系插件维护团队或查看相关文档。 \ No newline at end of file diff --git a/plugins/user/matchmaking_system/algorithms/algorithm_manager.py b/plugins/user/matchmaking_system/algorithms/algorithm_manager.py index b770cd26..d35e2b41 100644 --- a/plugins/user/matchmaking_system/algorithms/algorithm_manager.py +++ b/plugins/user/matchmaking_system/algorithms/algorithm_manager.py @@ -1,616 +1,616 @@ -""" -算法管理器模块 -实现不同的匹配算法(技能匹配、快速匹配等) -""" - -import time -import random -from typing import Dict, Any, List, Optional -from collections import defaultdict - -class AlgorithmManager: - """ - 算法管理器 - 实现不同的匹配算法(技能匹配、快速匹配等) - """ - - def __init__(self, plugin): - """ - 初始化算法管理器 - - Args: - plugin: 匹配系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 算法配置 - self.algorithm_config = { - "default_algorithm": "skill_based", - "available_algorithms": ["skill_based", "quick", "team_balanced", "custom"], - "skill_based_config": { - "skill_range": 100, # 技能匹配范围 - "max_wait_time": 120.0, # 最大等待时间(秒) - "expand_range_over_time": True, # 随时间扩展匹配范围 - "range_expansion_rate": 10 # 每秒扩展范围 - }, - "team_balanced_config": { - "balance_threshold": 0.1, # 平衡阈值 - "max_balance_attempts": 5 # 最大平衡尝试次数 - }, - "quick_match_config": { - "max_queue_time": 30.0, # 最大排队时间 - "min_players": 2, # 最少玩家数 - "max_players": 8 # 最多玩家数 - } - } - - # 算法状态 - self.algorithm_state = { - "active_algorithm": "skill_based", - "total_matches": 0, - "successful_matches": 0, - "failed_matches": 0, - "average_match_quality": 0.0 - } - - # 玩家数据存储 - self.player_data = {} # 玩家技能、统计数据等 - - # 算法统计 - self.algorithm_stats = { - "algorithms_run": 0, - "matches_created": 0, - "players_matched": 0, - "algorithm_errors": 0 - } - - # 回调函数 - self.algorithm_callbacks = { - "algorithm_selected": [], - "match_quality_calculated": [], - "algorithm_error": [] - } - - # 时间戳记录 - self.last_algorithm_run = 0.0 - self.last_stats_reset = 0.0 - - print("✓ 算法管理器已创建") - - def initialize(self) -> bool: - """ - 初始化算法管理器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化算法管理器...") - - # 注册默认算法 - self._register_default_algorithms() - - self.initialized = True - print("✓ 算法管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 算法管理器初始化失败: {e}") - self.algorithm_stats["algorithm_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def _register_default_algorithms(self): - """注册默认算法""" - try: - print("✓ 默认算法已注册") - except Exception as e: - print(f"✗ 默认算法注册失败: {e}") - self.algorithm_stats["algorithm_errors"] += 1 - - def enable(self) -> bool: - """ - 启用算法管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 算法管理器未初始化") - return False - - self.enabled = True - print("✓ 算法管理器已启用") - return True - - except Exception as e: - print(f"✗ 算法管理器启用失败: {e}") - self.algorithm_stats["algorithm_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用算法管理器""" - try: - self.enabled = False - print("✓ 算法管理器已禁用") - - except Exception as e: - print(f"✗ 算法管理器禁用失败: {e}") - self.algorithm_stats["algorithm_errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理算法管理器资源""" - try: - # 禁用算法管理器 - if self.enabled: - self.disable() - - # 清理回调 - self.algorithm_callbacks.clear() - - self.initialized = False - print("✓ 算法管理器资源已清理") - - except Exception as e: - print(f"✗ 算法管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新算法管理器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - self.last_algorithm_run = time.time() - - except Exception as e: - print(f"✗ 算法管理器更新失败: {e}") - self.algorithm_stats["algorithm_errors"] += 1 - import traceback - traceback.print_exc() - - def set_active_algorithm(self, algorithm_name: str) -> bool: - """ - 设置活动算法 - - Args: - algorithm_name: 算法名称 - - Returns: - 是否设置成功 - """ - try: - if algorithm_name not in self.algorithm_config["available_algorithms"]: - print(f"✗ 不支持的算法: {algorithm_name}") - return False - - self.algorithm_state["active_algorithm"] = algorithm_name - print(f"✓ 活动算法已设置为: {algorithm_name}") - - # 触发算法选择回调 - self._trigger_algorithm_callback("algorithm_selected", { - "algorithm": algorithm_name, - "timestamp": time.time() - }) - - return True - - except Exception as e: - print(f"✗ 活动算法设置失败: {e}") - self.algorithm_stats["algorithm_errors"] += 1 - return False - - def run_matching_algorithm(self, players: List[Dict[str, Any]], algorithm: str = None) -> Optional[List[List[Dict[str, Any]]]]: - """ - 运行匹配算法 - - Args: - players: 玩家列表 - algorithm: 算法名称(可选,默认使用活动算法) - - Returns: - 匹配结果(玩家分组列表)或None - """ - try: - if not self.enabled: - return None - - # 使用指定算法或活动算法 - if algorithm is None: - algorithm = self.algorithm_state["active_algorithm"] - - # 验证算法支持 - if algorithm not in self.algorithm_config["available_algorithms"]: - print(f"✗ 不支持的算法: {algorithm}") - self.algorithm_stats["algorithm_errors"] += 1 - return None - - self.algorithm_stats["algorithms_run"] += 1 - - # 根据算法类型运行相应算法 - if algorithm == "skill_based": - result = self._run_skill_based_matching(players) - elif algorithm == "quick": - result = self._run_quick_matching(players) - elif algorithm == "team_balanced": - result = self._run_team_balanced_matching(players) - else: - # 默认使用技能匹配 - result = self._run_skill_based_matching(players) - - if result: - self.algorithm_state["successful_matches"] += 1 - self.algorithm_state["total_matches"] += 1 - self.algorithm_stats["matches_created"] += 1 - - # 计算匹配质量 - match_quality = self._calculate_match_quality(result) - self.algorithm_state["average_match_quality"] = ( - self.algorithm_state["average_match_quality"] * (self.algorithm_state["successful_matches"] - 1) + - match_quality - ) / self.algorithm_state["successful_matches"] - - # 触发匹配质量计算回调 - self._trigger_algorithm_callback("match_quality_calculated", { - "quality": match_quality, - "algorithm": algorithm, - "timestamp": time.time() - }) - - else: - self.algorithm_state["failed_matches"] += 1 - - return result - - except Exception as e: - print(f"✗ 匹配算法运行失败: {e}") - self.algorithm_stats["algorithm_errors"] += 1 - return None - - def _run_skill_based_matching(self, players: List[Dict[str, Any]]) -> Optional[List[List[Dict[str, Any]]]]: - """ - 运行基于技能的匹配算法 - - Args: - players: 玩家列表 - - Returns: - 匹配结果或None - """ - try: - if not players: - return None - - config = self.algorithm_config["skill_based_config"] - skill_range = config["skill_range"] - - # 按技能水平排序玩家 - sorted_players = sorted(players, key=lambda p: p.get("skill_level", 0)) - - # 简化实现:将技能相近的玩家分组 - groups = [] - current_group = [sorted_players[0]] - - for i in range(1, len(sorted_players)): - current_player = sorted_players[i] - last_player = current_group[-1] - - # 检查技能差异是否在范围内 - skill_diff = abs(current_player.get("skill_level", 0) - last_player.get("skill_level", 0)) - - if skill_diff <= skill_range and len(current_group) < 8: # 最多8人一组 - current_group.append(current_player) - else: - # 创建新组 - groups.append(current_group) - current_group = [current_player] - - # 添加最后一组 - if current_group: - groups.append(current_group) - - print(f"✓ 技能匹配完成: 创建了 {len(groups)} 个组") - return groups - - except Exception as e: - print(f"✗ 技能匹配算法失败: {e}") - self.algorithm_stats["algorithm_errors"] += 1 - return None - - def _run_quick_matching(self, players: List[Dict[str, Any]]) -> Optional[List[List[Dict[str, Any]]]]: - """ - 运行快速匹配算法 - - Args: - players: 玩家列表 - - Returns: - 匹配结果或None - """ - try: - if not players: - return None - - config = self.algorithm_config["quick_match_config"] - min_players = config["min_players"] - max_players = config["max_players"] - - # 简化实现:随机分组 - groups = [] - player_pool = players.copy() - random.shuffle(player_pool) - - while player_pool: - group_size = min(random.randint(min_players, max_players), len(player_pool)) - group = player_pool[:group_size] - groups.append(group) - player_pool = player_pool[group_size:] - - print(f"✓ 快速匹配完成: 创建了 {len(groups)} 个组") - return groups - - except Exception as e: - print(f"✗ 快速匹配算法失败: {e}") - self.algorithm_stats["algorithm_errors"] += 1 - return None - - def _run_team_balanced_matching(self, players: List[Dict[str, Any]]) -> Optional[List[List[Dict[str, Any]]]]: - """ - 运行团队平衡匹配算法 - - Args: - players: 玩家列表 - - Returns: - 匹配结果或None - """ - try: - if not players or len(players) < 2: - return None - - # 简化实现:创建两个平衡的团队 - # 按技能排序 - sorted_players = sorted(players, key=lambda p: p.get("skill_level", 0), reverse=True) - - team1 = [] - team2 = [] - team1_skill = 0 - team2_skill = 0 - - # 贪心算法分配玩家到技能平衡的团队 - for player in sorted_players: - player_skill = player.get("skill_level", 0) - - if team1_skill <= team2_skill: - team1.append(player) - team1_skill += player_skill - else: - team2.append(player) - team2_skill += player_skill - - groups = [team1, team2] if team1 and team2 else [sorted_players] - - print(f"✓ 团队平衡匹配完成: 创建了 {len(groups)} 个组") - return groups - - except Exception as e: - print(f"✗ 团队平衡匹配算法失败: {e}") - self.algorithm_stats["algorithm_errors"] += 1 - return None - - def _calculate_match_quality(self, groups: List[List[Dict[str, Any]]]) -> float: - """ - 计算匹配质量 - - Args: - groups: 玩家分组 - - Returns: - 匹配质量分数(0-1) - """ - try: - if not groups: - return 0.0 - - total_quality = 0.0 - group_count = 0 - - for group in groups: - if len(group) < 2: - continue - - # 计算组内技能差异 - skills = [p.get("skill_level", 0) for p in group] - avg_skill = sum(skills) / len(skills) - variance = sum((s - avg_skill) ** 2 for s in skills) / len(skills) - std_dev = variance ** 0.5 - - # 转换为质量分数(标准差越小质量越高) - max_expected_std = 100.0 # 假设最大预期标准差 - quality = max(0.0, 1.0 - (std_dev / max_expected_std)) - - total_quality += quality - group_count += 1 - - return total_quality / group_count if group_count > 0 else 0.0 - - except Exception as e: - print(f"✗ 匹配质量计算失败: {e}") - self.algorithm_stats["algorithm_errors"] += 1 - return 0.0 - - def update_player_skill(self, player_id: str, skill_level: float, stats: Dict[str, Any] = None): - """ - 更新玩家技能 - - Args: - player_id: 玩家ID - skill_level: 技能等级 - stats: 玩家统计数据 - """ - try: - if player_id not in self.player_data: - self.player_data[player_id] = { - "skill_history": [], - "match_stats": {} - } - - player_info = self.player_data[player_id] - player_info["current_skill"] = skill_level - player_info["skill_history"].append({ - "skill": skill_level, - "timestamp": time.time() - }) - - if stats: - player_info["match_stats"].update(stats) - - except Exception as e: - print(f"✗ 玩家技能更新失败: {e}") - self.algorithm_stats["algorithm_errors"] += 1 - - def get_player_skill(self, player_id: str) -> Optional[float]: - """ - 获取玩家技能 - - Args: - player_id: 玩家ID - - Returns: - 玩家技能等级或None - """ - try: - if player_id in self.player_data: - return self.player_data[player_id].get("current_skill") - return None - except Exception as e: - print(f"✗ 玩家技能获取失败: {e}") - self.algorithm_stats["algorithm_errors"] += 1 - return None - - def get_algorithm_stats(self) -> Dict[str, Any]: - """ - 获取算法统计信息 - - Returns: - 算法统计字典 - """ - return { - "state": self.algorithm_state.copy(), - "stats": self.algorithm_stats.copy(), - "config": self.algorithm_config.copy(), - "tracked_players": len(self.player_data) - } - - def reset_stats(self): - """重置算法统计信息""" - try: - self.algorithm_stats = { - "algorithms_run": 0, - "matches_created": 0, - "players_matched": 0, - "algorithm_errors": 0 - } - - self.algorithm_state["total_matches"] = 0 - self.algorithm_state["successful_matches"] = 0 - self.algorithm_state["failed_matches"] = 0 - self.algorithm_state["average_match_quality"] = 0.0 - - print("✓ 算法统计信息已重置") - except Exception as e: - print(f"✗ 算法统计信息重置失败: {e}") - - def set_algorithm_config(self, config: Dict[str, Any]) -> bool: - """ - 设置算法配置 - - Args: - config: 算法配置字典 - - Returns: - 是否设置成功 - """ - try: - self.algorithm_config.update(config) - print(f"✓ 算法配置已更新: {self.algorithm_config}") - return True - except Exception as e: - print(f"✗ 算法配置设置失败: {e}") - return False - - def get_algorithm_config(self) -> Dict[str, Any]: - """ - 获取算法配置 - - Returns: - 算法配置字典 - """ - return self.algorithm_config.copy() - - def _trigger_algorithm_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发算法回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.algorithm_callbacks: - for callback in self.algorithm_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 算法回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 算法回调触发失败: {e}") - - def register_algorithm_callback(self, callback_type: str, callback: callable): - """ - 注册算法回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.algorithm_callbacks: - self.algorithm_callbacks[callback_type].append(callback) - print(f"✓ 算法回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 算法回调注册失败: {e}") - - def unregister_algorithm_callback(self, callback_type: str, callback: callable): - """ - 注销算法回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.algorithm_callbacks: - if callback in self.algorithm_callbacks[callback_type]: - self.algorithm_callbacks[callback_type].remove(callback) - print(f"✓ 算法回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +算法管理器模块 +实现不同的匹配算法(技能匹配、快速匹配等) +""" + +import time +import random +from typing import Dict, Any, List, Optional +from collections import defaultdict + +class AlgorithmManager: + """ + 算法管理器 + 实现不同的匹配算法(技能匹配、快速匹配等) + """ + + def __init__(self, plugin): + """ + 初始化算法管理器 + + Args: + plugin: 匹配系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 算法配置 + self.algorithm_config = { + "default_algorithm": "skill_based", + "available_algorithms": ["skill_based", "quick", "team_balanced", "custom"], + "skill_based_config": { + "skill_range": 100, # 技能匹配范围 + "max_wait_time": 120.0, # 最大等待时间(秒) + "expand_range_over_time": True, # 随时间扩展匹配范围 + "range_expansion_rate": 10 # 每秒扩展范围 + }, + "team_balanced_config": { + "balance_threshold": 0.1, # 平衡阈值 + "max_balance_attempts": 5 # 最大平衡尝试次数 + }, + "quick_match_config": { + "max_queue_time": 30.0, # 最大排队时间 + "min_players": 2, # 最少玩家数 + "max_players": 8 # 最多玩家数 + } + } + + # 算法状态 + self.algorithm_state = { + "active_algorithm": "skill_based", + "total_matches": 0, + "successful_matches": 0, + "failed_matches": 0, + "average_match_quality": 0.0 + } + + # 玩家数据存储 + self.player_data = {} # 玩家技能、统计数据等 + + # 算法统计 + self.algorithm_stats = { + "algorithms_run": 0, + "matches_created": 0, + "players_matched": 0, + "algorithm_errors": 0 + } + + # 回调函数 + self.algorithm_callbacks = { + "algorithm_selected": [], + "match_quality_calculated": [], + "algorithm_error": [] + } + + # 时间戳记录 + self.last_algorithm_run = 0.0 + self.last_stats_reset = 0.0 + + print("✓ 算法管理器已创建") + + def initialize(self) -> bool: + """ + 初始化算法管理器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化算法管理器...") + + # 注册默认算法 + self._register_default_algorithms() + + self.initialized = True + print("✓ 算法管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 算法管理器初始化失败: {e}") + self.algorithm_stats["algorithm_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def _register_default_algorithms(self): + """注册默认算法""" + try: + print("✓ 默认算法已注册") + except Exception as e: + print(f"✗ 默认算法注册失败: {e}") + self.algorithm_stats["algorithm_errors"] += 1 + + def enable(self) -> bool: + """ + 启用算法管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 算法管理器未初始化") + return False + + self.enabled = True + print("✓ 算法管理器已启用") + return True + + except Exception as e: + print(f"✗ 算法管理器启用失败: {e}") + self.algorithm_stats["algorithm_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用算法管理器""" + try: + self.enabled = False + print("✓ 算法管理器已禁用") + + except Exception as e: + print(f"✗ 算法管理器禁用失败: {e}") + self.algorithm_stats["algorithm_errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理算法管理器资源""" + try: + # 禁用算法管理器 + if self.enabled: + self.disable() + + # 清理回调 + self.algorithm_callbacks.clear() + + self.initialized = False + print("✓ 算法管理器资源已清理") + + except Exception as e: + print(f"✗ 算法管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新算法管理器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + self.last_algorithm_run = time.time() + + except Exception as e: + print(f"✗ 算法管理器更新失败: {e}") + self.algorithm_stats["algorithm_errors"] += 1 + import traceback + traceback.print_exc() + + def set_active_algorithm(self, algorithm_name: str) -> bool: + """ + 设置活动算法 + + Args: + algorithm_name: 算法名称 + + Returns: + 是否设置成功 + """ + try: + if algorithm_name not in self.algorithm_config["available_algorithms"]: + print(f"✗ 不支持的算法: {algorithm_name}") + return False + + self.algorithm_state["active_algorithm"] = algorithm_name + print(f"✓ 活动算法已设置为: {algorithm_name}") + + # 触发算法选择回调 + self._trigger_algorithm_callback("algorithm_selected", { + "algorithm": algorithm_name, + "timestamp": time.time() + }) + + return True + + except Exception as e: + print(f"✗ 活动算法设置失败: {e}") + self.algorithm_stats["algorithm_errors"] += 1 + return False + + def run_matching_algorithm(self, players: List[Dict[str, Any]], algorithm: str = None) -> Optional[List[List[Dict[str, Any]]]]: + """ + 运行匹配算法 + + Args: + players: 玩家列表 + algorithm: 算法名称(可选,默认使用活动算法) + + Returns: + 匹配结果(玩家分组列表)或None + """ + try: + if not self.enabled: + return None + + # 使用指定算法或活动算法 + if algorithm is None: + algorithm = self.algorithm_state["active_algorithm"] + + # 验证算法支持 + if algorithm not in self.algorithm_config["available_algorithms"]: + print(f"✗ 不支持的算法: {algorithm}") + self.algorithm_stats["algorithm_errors"] += 1 + return None + + self.algorithm_stats["algorithms_run"] += 1 + + # 根据算法类型运行相应算法 + if algorithm == "skill_based": + result = self._run_skill_based_matching(players) + elif algorithm == "quick": + result = self._run_quick_matching(players) + elif algorithm == "team_balanced": + result = self._run_team_balanced_matching(players) + else: + # 默认使用技能匹配 + result = self._run_skill_based_matching(players) + + if result: + self.algorithm_state["successful_matches"] += 1 + self.algorithm_state["total_matches"] += 1 + self.algorithm_stats["matches_created"] += 1 + + # 计算匹配质量 + match_quality = self._calculate_match_quality(result) + self.algorithm_state["average_match_quality"] = ( + self.algorithm_state["average_match_quality"] * (self.algorithm_state["successful_matches"] - 1) + + match_quality + ) / self.algorithm_state["successful_matches"] + + # 触发匹配质量计算回调 + self._trigger_algorithm_callback("match_quality_calculated", { + "quality": match_quality, + "algorithm": algorithm, + "timestamp": time.time() + }) + + else: + self.algorithm_state["failed_matches"] += 1 + + return result + + except Exception as e: + print(f"✗ 匹配算法运行失败: {e}") + self.algorithm_stats["algorithm_errors"] += 1 + return None + + def _run_skill_based_matching(self, players: List[Dict[str, Any]]) -> Optional[List[List[Dict[str, Any]]]]: + """ + 运行基于技能的匹配算法 + + Args: + players: 玩家列表 + + Returns: + 匹配结果或None + """ + try: + if not players: + return None + + config = self.algorithm_config["skill_based_config"] + skill_range = config["skill_range"] + + # 按技能水平排序玩家 + sorted_players = sorted(players, key=lambda p: p.get("skill_level", 0)) + + # 简化实现:将技能相近的玩家分组 + groups = [] + current_group = [sorted_players[0]] + + for i in range(1, len(sorted_players)): + current_player = sorted_players[i] + last_player = current_group[-1] + + # 检查技能差异是否在范围内 + skill_diff = abs(current_player.get("skill_level", 0) - last_player.get("skill_level", 0)) + + if skill_diff <= skill_range and len(current_group) < 8: # 最多8人一组 + current_group.append(current_player) + else: + # 创建新组 + groups.append(current_group) + current_group = [current_player] + + # 添加最后一组 + if current_group: + groups.append(current_group) + + print(f"✓ 技能匹配完成: 创建了 {len(groups)} 个组") + return groups + + except Exception as e: + print(f"✗ 技能匹配算法失败: {e}") + self.algorithm_stats["algorithm_errors"] += 1 + return None + + def _run_quick_matching(self, players: List[Dict[str, Any]]) -> Optional[List[List[Dict[str, Any]]]]: + """ + 运行快速匹配算法 + + Args: + players: 玩家列表 + + Returns: + 匹配结果或None + """ + try: + if not players: + return None + + config = self.algorithm_config["quick_match_config"] + min_players = config["min_players"] + max_players = config["max_players"] + + # 简化实现:随机分组 + groups = [] + player_pool = players.copy() + random.shuffle(player_pool) + + while player_pool: + group_size = min(random.randint(min_players, max_players), len(player_pool)) + group = player_pool[:group_size] + groups.append(group) + player_pool = player_pool[group_size:] + + print(f"✓ 快速匹配完成: 创建了 {len(groups)} 个组") + return groups + + except Exception as e: + print(f"✗ 快速匹配算法失败: {e}") + self.algorithm_stats["algorithm_errors"] += 1 + return None + + def _run_team_balanced_matching(self, players: List[Dict[str, Any]]) -> Optional[List[List[Dict[str, Any]]]]: + """ + 运行团队平衡匹配算法 + + Args: + players: 玩家列表 + + Returns: + 匹配结果或None + """ + try: + if not players or len(players) < 2: + return None + + # 简化实现:创建两个平衡的团队 + # 按技能排序 + sorted_players = sorted(players, key=lambda p: p.get("skill_level", 0), reverse=True) + + team1 = [] + team2 = [] + team1_skill = 0 + team2_skill = 0 + + # 贪心算法分配玩家到技能平衡的团队 + for player in sorted_players: + player_skill = player.get("skill_level", 0) + + if team1_skill <= team2_skill: + team1.append(player) + team1_skill += player_skill + else: + team2.append(player) + team2_skill += player_skill + + groups = [team1, team2] if team1 and team2 else [sorted_players] + + print(f"✓ 团队平衡匹配完成: 创建了 {len(groups)} 个组") + return groups + + except Exception as e: + print(f"✗ 团队平衡匹配算法失败: {e}") + self.algorithm_stats["algorithm_errors"] += 1 + return None + + def _calculate_match_quality(self, groups: List[List[Dict[str, Any]]]) -> float: + """ + 计算匹配质量 + + Args: + groups: 玩家分组 + + Returns: + 匹配质量分数(0-1) + """ + try: + if not groups: + return 0.0 + + total_quality = 0.0 + group_count = 0 + + for group in groups: + if len(group) < 2: + continue + + # 计算组内技能差异 + skills = [p.get("skill_level", 0) for p in group] + avg_skill = sum(skills) / len(skills) + variance = sum((s - avg_skill) ** 2 for s in skills) / len(skills) + std_dev = variance ** 0.5 + + # 转换为质量分数(标准差越小质量越高) + max_expected_std = 100.0 # 假设最大预期标准差 + quality = max(0.0, 1.0 - (std_dev / max_expected_std)) + + total_quality += quality + group_count += 1 + + return total_quality / group_count if group_count > 0 else 0.0 + + except Exception as e: + print(f"✗ 匹配质量计算失败: {e}") + self.algorithm_stats["algorithm_errors"] += 1 + return 0.0 + + def update_player_skill(self, player_id: str, skill_level: float, stats: Dict[str, Any] = None): + """ + 更新玩家技能 + + Args: + player_id: 玩家ID + skill_level: 技能等级 + stats: 玩家统计数据 + """ + try: + if player_id not in self.player_data: + self.player_data[player_id] = { + "skill_history": [], + "match_stats": {} + } + + player_info = self.player_data[player_id] + player_info["current_skill"] = skill_level + player_info["skill_history"].append({ + "skill": skill_level, + "timestamp": time.time() + }) + + if stats: + player_info["match_stats"].update(stats) + + except Exception as e: + print(f"✗ 玩家技能更新失败: {e}") + self.algorithm_stats["algorithm_errors"] += 1 + + def get_player_skill(self, player_id: str) -> Optional[float]: + """ + 获取玩家技能 + + Args: + player_id: 玩家ID + + Returns: + 玩家技能等级或None + """ + try: + if player_id in self.player_data: + return self.player_data[player_id].get("current_skill") + return None + except Exception as e: + print(f"✗ 玩家技能获取失败: {e}") + self.algorithm_stats["algorithm_errors"] += 1 + return None + + def get_algorithm_stats(self) -> Dict[str, Any]: + """ + 获取算法统计信息 + + Returns: + 算法统计字典 + """ + return { + "state": self.algorithm_state.copy(), + "stats": self.algorithm_stats.copy(), + "config": self.algorithm_config.copy(), + "tracked_players": len(self.player_data) + } + + def reset_stats(self): + """重置算法统计信息""" + try: + self.algorithm_stats = { + "algorithms_run": 0, + "matches_created": 0, + "players_matched": 0, + "algorithm_errors": 0 + } + + self.algorithm_state["total_matches"] = 0 + self.algorithm_state["successful_matches"] = 0 + self.algorithm_state["failed_matches"] = 0 + self.algorithm_state["average_match_quality"] = 0.0 + + print("✓ 算法统计信息已重置") + except Exception as e: + print(f"✗ 算法统计信息重置失败: {e}") + + def set_algorithm_config(self, config: Dict[str, Any]) -> bool: + """ + 设置算法配置 + + Args: + config: 算法配置字典 + + Returns: + 是否设置成功 + """ + try: + self.algorithm_config.update(config) + print(f"✓ 算法配置已更新: {self.algorithm_config}") + return True + except Exception as e: + print(f"✗ 算法配置设置失败: {e}") + return False + + def get_algorithm_config(self) -> Dict[str, Any]: + """ + 获取算法配置 + + Returns: + 算法配置字典 + """ + return self.algorithm_config.copy() + + def _trigger_algorithm_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发算法回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.algorithm_callbacks: + for callback in self.algorithm_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 算法回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 算法回调触发失败: {e}") + + def register_algorithm_callback(self, callback_type: str, callback: callable): + """ + 注册算法回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.algorithm_callbacks: + self.algorithm_callbacks[callback_type].append(callback) + print(f"✓ 算法回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 算法回调注册失败: {e}") + + def unregister_algorithm_callback(self, callback_type: str, callback: callable): + """ + 注销算法回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.algorithm_callbacks: + if callback in self.algorithm_callbacks[callback_type]: + self.algorithm_callbacks[callback_type].remove(callback) + print(f"✓ 算法回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 算法回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/matchmaking_system/config/match_config.py b/plugins/user/matchmaking_system/config/match_config.py index b25434c8..de510a28 100644 --- a/plugins/user/matchmaking_system/config/match_config.py +++ b/plugins/user/matchmaking_system/config/match_config.py @@ -1,712 +1,712 @@ -""" -匹配配置管理器模块 -管理匹配系统配置 -""" - -import time -import json -import os -from typing import Dict, Any, List, Optional - -class MatchConfig: - """ - 匹配配置管理器 - 管理匹配系统配置 - """ - - def __init__(self, plugin): - """ - 初始化匹配配置管理器 - - Args: - plugin: 匹配系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 配置文件路径 - self.config_file_path = "/home/hello/EG/plugins/user/matchmaking_system/config/matchmaking_config.json" - - # 默认配置 - self.default_config = { - "system": { - "enable_matchmaking": True, - "max_concurrent_matches": 1000, - "default_match_timeout": 300.0, - "enable_cross_platform_matching": True - }, - "queue": { - "default_queue_timeout": 300.0, - "queue_processing_interval": 2.0, - "max_players_per_match": 100, - "min_players_per_match": 2, - "enable_queue_prioritization": True - }, - "matching": { - "default_algorithm": "skill_based", - "skill_range": 100, - "max_wait_time": 120.0, - "enable_skill_based_matching": True, - "enable_team_balancing": True - }, - "room": { - "default_max_players": 8, - "enable_room_passwords": True, - "enable_room_visibility": True, - "room_timeout": 3600, - "max_rooms": 1000 - }, - "monitoring": { - "enable_monitoring": True, - "monitoring_interval": 5.0, - "log_level": "INFO", - "enable_performance_monitoring": True - }, - "events": { - "enable_event_system": True, - "max_event_queue_size": 10000, - "enable_event_filtering": True - } - } - - # 当前配置 - self.current_config = self.default_config.copy() - - # 配置状态 - self.config_state = { - "last_loaded": 0.0, - "last_saved": 0.0, - "config_version": "1.0.0", - "is_dirty": False - } - - # 配置统计 - self.config_stats = { - "loads": 0, - "saves": 0, - "updates": 0, - "config_errors": 0 - } - - # 配置监听器 - self.config_listeners = {} - - # 回调函数 - self.config_callbacks = { - "config_loaded": [], - "config_saved": [], - "config_updated": [], - "config_error": [] - } - - print("✓ 匹配配置管理器已创建") - - def initialize(self) -> bool: - """ - 初始化匹配配置管理器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化匹配配置管理器...") - - # 创建配置目录 - config_dir = os.path.dirname(self.config_file_path) - if not os.path.exists(config_dir): - os.makedirs(config_dir) - - # 加载配置 - self.load_config() - - self.initialized = True - print("✓ 匹配配置管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 匹配配置管理器初始化失败: {e}") - self.config_stats["config_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用匹配配置管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 匹配配置管理器未初始化") - return False - - self.enabled = True - print("✓ 匹配配置管理器已启用") - return True - - except Exception as e: - print(f"✗ 匹配配置管理器启用失败: {e}") - self.config_stats["config_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用匹配配置管理器""" - try: - self.enabled = False - - # 保存配置(如果有更改) - if self.config_state["is_dirty"]: - self.save_config() - - print("✓ 匹配配置管理器已禁用") - - except Exception as e: - print(f"✗ 匹配配置管理器禁用失败: {e}") - self.config_stats["config_errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理匹配配置管理器资源""" - try: - # 禁用匹配配置管理器 - if self.enabled: - self.disable() - - # 清理回调 - self.config_callbacks.clear() - self.config_listeners.clear() - - self.initialized = False - print("✓ 匹配配置管理器资源已清理") - - except Exception as e: - print(f"✗ 匹配配置管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新匹配配置管理器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - # 检查配置文件是否被外部修改 - self._check_config_file_changes() - - except Exception as e: - print(f"✗ 匹配配置管理器更新失败: {e}") - self.config_stats["config_errors"] += 1 - import traceback - traceback.print_exc() - - def _check_config_file_changes(self): - """检查配置文件是否被外部修改""" - try: - if not os.path.exists(self.config_file_path): - return - - last_modified = os.path.getmtime(self.config_file_path) - if last_modified > self.config_state["last_loaded"]: - # 配置文件已被修改,重新加载 - print("检测到配置文件被外部修改,正在重新加载...") - self.load_config() - - except Exception as e: - print(f"✗ 配置文件变更检查失败: {e}") - self.config_stats["config_errors"] += 1 - - def load_config(self) -> bool: - """ - 加载配置 - - Returns: - 是否加载成功 - """ - try: - # 检查配置文件是否存在 - if os.path.exists(self.config_file_path): - with open(self.config_file_path, 'r', encoding='utf-8') as f: - loaded_config = json.load(f) - - # 合并配置(使用默认配置作为基础) - self.current_config = self._merge_config(self.default_config, loaded_config) - - print(f"✓ 配置已从文件加载: {self.config_file_path}") - else: - # 使用默认配置 - self.current_config = self.default_config.copy() - print("✓ 使用默认配置") - - self.config_state["last_loaded"] = time.time() - self.config_state["is_dirty"] = False - self.config_stats["loads"] += 1 - - # 触发配置加载回调 - self._trigger_config_callback("config_loaded", { - "config": self.current_config, - "timestamp": self.config_state["last_loaded"] - }) - - return True - - except Exception as e: - print(f"✗ 配置加载失败: {e}") - self.config_stats["config_errors"] += 1 - - # 使用默认配置 - self.current_config = self.default_config.copy() - return False - - def _merge_config(self, base_config: Dict[str, Any], override_config: Dict[str, Any]) -> Dict[str, Any]: - """ - 合并配置(递归) - - Args: - base_config: 基础配置 - override_config: 覆盖配置 - - Returns: - 合并后的配置 - """ - try: - merged = base_config.copy() - - for key, value in override_config.items(): - if key in merged and isinstance(merged[key], dict) and isinstance(value, dict): - # 递归合并嵌套字典 - merged[key] = self._merge_config(merged[key], value) - else: - # 直接覆盖 - merged[key] = value - - return merged - - except Exception as e: - print(f"✗ 配置合并失败: {e}") - self.config_stats["config_errors"] += 1 - return base_config - - def save_config(self) -> bool: - """ - 保存配置 - - Returns: - 是否保存成功 - """ - try: - # 创建配置目录(如果不存在) - config_dir = os.path.dirname(self.config_file_path) - if not os.path.exists(config_dir): - os.makedirs(config_dir) - - # 保存配置到文件 - with open(self.config_file_path, 'w', encoding='utf-8') as f: - json.dump(self.current_config, f, indent=2, ensure_ascii=False) - - self.config_state["last_saved"] = time.time() - self.config_state["is_dirty"] = False - self.config_stats["saves"] += 1 - - # 触发配置保存回调 - self._trigger_config_callback("config_saved", { - "config": self.current_config, - "timestamp": self.config_state["last_saved"] - }) - - print(f"✓ 配置已保存到文件: {self.config_file_path}") - return True - - except Exception as e: - print(f"✗ 配置保存失败: {e}") - self.config_stats["config_errors"] += 1 - return False - - def get_config(self, section: str = None, key: str = None, default_value: Any = None) -> Any: - """ - 获取配置值 - - Args: - section: 配置节(可选) - key: 配置键(可选) - default_value: 默认值 - - Returns: - 配置值 - """ - try: - if section is None: - return self.current_config.copy() - - if section not in self.current_config: - return default_value - - if key is None: - return self.current_config[section].copy() - - if key not in self.current_config[section]: - return default_value - - return self.current_config[section][key] - - except Exception as e: - print(f"✗ 获取配置失败: {e}") - self.config_stats["config_errors"] += 1 - return default_value - - def set_config(self, section: str, key: str, value: Any) -> bool: - """ - 设置配置值 - - Args: - section: 配置节 - key: 配置键 - value: 配置值 - - Returns: - 是否设置成功 - """ - try: - if section not in self.current_config: - self.current_config[section] = {} - - self.current_config[section][key] = value - self.config_state["is_dirty"] = True - self.config_stats["updates"] += 1 - - # 通知配置监听器 - self._notify_config_listeners(section, key, value) - - # 触发配置更新回调 - self._trigger_config_callback("config_updated", { - "section": section, - "key": key, - "value": value, - "timestamp": time.time() - }) - - return True - - except Exception as e: - print(f"✗ 设置配置失败: {e}") - self.config_stats["config_errors"] += 1 - return False - - def update_config_section(self, section: str, config_data: Dict[str, Any]) -> bool: - """ - 更新配置节 - - Args: - section: 配置节 - config_data: 配置数据 - - Returns: - 是否更新成功 - """ - try: - if section not in self.current_config: - self.current_config[section] = {} - - self.current_config[section].update(config_data) - self.config_state["is_dirty"] = True - self.config_stats["updates"] += 1 - - # 通知配置监听器 - for key, value in config_data.items(): - self._notify_config_listeners(section, key, value) - - # 触发配置更新回调 - self._trigger_config_callback("config_updated", { - "section": section, - "config_data": config_data, - "timestamp": time.time() - }) - - return True - - except Exception as e: - print(f"✗ 更新配置节失败: {e}") - self.config_stats["config_errors"] += 1 - return False - - def reset_config(self) -> bool: - """ - 重置配置为默认值 - - Returns: - 是否重置成功 - """ - try: - self.current_config = self.default_config.copy() - self.config_state["is_dirty"] = True - - # 通知所有配置监听器 - for section_name, section_data in self.current_config.items(): - if isinstance(section_data, dict): - for key, value in section_data.items(): - self._notify_config_listeners(section_name, key, value) - - print("✓ 配置已重置为默认值") - return True - - except Exception as e: - print(f"✗ 配置重置失败: {e}") - self.config_stats["config_errors"] += 1 - return False - - def register_config_listener(self, section: str, key: str, listener: callable): - """ - 注册配置监听器 - - Args: - section: 配置节 - key: 配置键 - listener: 监听器函数 - """ - try: - listener_key = f"{section}.{key}" - if listener_key not in self.config_listeners: - self.config_listeners[listener_key] = [] - - self.config_listeners[listener_key].append(listener) - print(f"✓ 配置监听器已注册: {listener_key}") - - except Exception as e: - print(f"✗ 配置监听器注册失败: {e}") - self.config_stats["config_errors"] += 1 - - def unregister_config_listener(self, section: str, key: str, listener: callable): - """ - 注销配置监听器 - - Args: - section: 配置节 - key: 配置键 - listener: 监听器函数 - """ - try: - listener_key = f"{section}.{key}" - if listener_key in self.config_listeners: - if listener in self.config_listeners[listener_key]: - self.config_listeners[listener_key].remove(listener) - print(f"✓ 配置监听器已注销: {listener_key}") - - except Exception as e: - print(f"✗ 配置监听器注销失败: {e}") - self.config_stats["config_errors"] += 1 - - def _notify_config_listeners(self, section: str, key: str, value: Any): - """ - 通知配置监听器 - - Args: - section: 配置节 - key: 配置键 - value: 配置值 - """ - try: - listener_key = f"{section}.{key}" - if listener_key in self.config_listeners: - for listener in self.config_listeners[listener_key]: - try: - listener(section, key, value) - except Exception as e: - print(f"✗ 配置监听器执行失败: {listener_key} - {e}") - - except Exception as e: - print(f"✗ 配置监听器通知失败: {e}") - self.config_stats["config_errors"] += 1 - - def get_config_stats(self) -> Dict[str, Any]: - """ - 获取配置统计信息 - - Returns: - 配置统计字典 - """ - return { - "state": self.config_state.copy(), - "stats": self.config_stats.copy(), - "config_sections": list(self.current_config.keys()) - } - - def reset_stats(self): - """重置配置统计信息""" - try: - self.config_stats = { - "loads": 0, - "saves": 0, - "updates": 0, - "config_errors": 0 - } - print("✓ 配置统计信息已重置") - except Exception as e: - print(f"✗ 配置统计信息重置失败: {e}") - - def validate_config(self) -> bool: - """ - 验证配置 - - Returns: - 配置是否有效 - """ - try: - # 检查必需的配置项 - required_sections = ["system", "queue", "matching", "room", "monitoring", "events"] - for section in required_sections: - if section not in self.current_config: - print(f"✗ 缺少必需的配置节: {section}") - return False - - # 检查系统配置 - system_config = self.current_config["system"] - if "max_concurrent_matches" not in system_config or not isinstance(system_config["max_concurrent_matches"], int): - print("✗ 无效的最大并发匹配数配置") - return False - - # 检查队列配置 - queue_config = self.current_config["queue"] - if "max_players_per_match" not in queue_config or not isinstance(queue_config["max_players_per_match"], int): - print("✗ 无效的最大玩家数配置") - return False - - print("✓ 配置验证通过") - return True - - except Exception as e: - print(f"✗ 配置验证失败: {e}") - self.config_stats["config_errors"] += 1 - return False - - def export_config(self, file_path: str) -> bool: - """ - 导出配置到文件 - - Args: - file_path: 导出文件路径 - - Returns: - 是否导出成功 - """ - try: - # 创建导出目录 - export_dir = os.path.dirname(file_path) - if not os.path.exists(export_dir): - os.makedirs(export_dir) - - # 导出配置 - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(self.current_config, f, indent=2, ensure_ascii=False) - - print(f"✓ 配置已导出到: {file_path}") - return True - - except Exception as e: - print(f"✗ 配置导出失败: {e}") - self.config_stats["config_errors"] += 1 - return False - - def import_config(self, file_path: str) -> bool: - """ - 从文件导入配置 - - Args: - file_path: 导入文件路径 - - Returns: - 是否导入成功 - """ - try: - if not os.path.exists(file_path): - print(f"✗ 配置文件不存在: {file_path}") - return False - - # 读取配置文件 - with open(file_path, 'r', encoding='utf-8') as f: - imported_config = json.load(f) - - # 验证配置 - if not isinstance(imported_config, dict): - print("✗ 无效的配置文件格式") - return False - - # 合并配置 - self.current_config = self._merge_config(self.default_config, imported_config) - self.config_state["is_dirty"] = True - - # 通知配置监听器 - for section_name, section_data in self.current_config.items(): - if isinstance(section_data, dict): - for key, value in section_data.items(): - self._notify_config_listeners(section_name, key, value) - - print(f"✓ 配置已从文件导入: {file_path}") - return True - - except Exception as e: - print(f"✗ 配置导入失败: {e}") - self.config_stats["config_errors"] += 1 - return False - - def _trigger_config_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发配置回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.config_callbacks: - for callback in self.config_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 配置回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 配置回调触发失败: {e}") - - def register_config_callback(self, callback_type: str, callback: callable): - """ - 注册配置回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.config_callbacks: - self.config_callbacks[callback_type].append(callback) - print(f"✓ 配置回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 配置回调注册失败: {e}") - - def unregister_config_callback(self, callback_type: str, callback: callable): - """ - 注销配置回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.config_callbacks: - if callback in self.config_callbacks[callback_type]: - self.config_callbacks[callback_type].remove(callback) - print(f"✓ 配置回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +匹配配置管理器模块 +管理匹配系统配置 +""" + +import time +import json +import os +from typing import Dict, Any, List, Optional + +class MatchConfig: + """ + 匹配配置管理器 + 管理匹配系统配置 + """ + + def __init__(self, plugin): + """ + 初始化匹配配置管理器 + + Args: + plugin: 匹配系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 配置文件路径 + self.config_file_path = "/home/hello/EG/plugins/user/matchmaking_system/config/matchmaking_config.json" + + # 默认配置 + self.default_config = { + "system": { + "enable_matchmaking": True, + "max_concurrent_matches": 1000, + "default_match_timeout": 300.0, + "enable_cross_platform_matching": True + }, + "queue": { + "default_queue_timeout": 300.0, + "queue_processing_interval": 2.0, + "max_players_per_match": 100, + "min_players_per_match": 2, + "enable_queue_prioritization": True + }, + "matching": { + "default_algorithm": "skill_based", + "skill_range": 100, + "max_wait_time": 120.0, + "enable_skill_based_matching": True, + "enable_team_balancing": True + }, + "room": { + "default_max_players": 8, + "enable_room_passwords": True, + "enable_room_visibility": True, + "room_timeout": 3600, + "max_rooms": 1000 + }, + "monitoring": { + "enable_monitoring": True, + "monitoring_interval": 5.0, + "log_level": "INFO", + "enable_performance_monitoring": True + }, + "events": { + "enable_event_system": True, + "max_event_queue_size": 10000, + "enable_event_filtering": True + } + } + + # 当前配置 + self.current_config = self.default_config.copy() + + # 配置状态 + self.config_state = { + "last_loaded": 0.0, + "last_saved": 0.0, + "config_version": "1.0.0", + "is_dirty": False + } + + # 配置统计 + self.config_stats = { + "loads": 0, + "saves": 0, + "updates": 0, + "config_errors": 0 + } + + # 配置监听器 + self.config_listeners = {} + + # 回调函数 + self.config_callbacks = { + "config_loaded": [], + "config_saved": [], + "config_updated": [], + "config_error": [] + } + + print("✓ 匹配配置管理器已创建") + + def initialize(self) -> bool: + """ + 初始化匹配配置管理器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化匹配配置管理器...") + + # 创建配置目录 + config_dir = os.path.dirname(self.config_file_path) + if not os.path.exists(config_dir): + os.makedirs(config_dir) + + # 加载配置 + self.load_config() + + self.initialized = True + print("✓ 匹配配置管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 匹配配置管理器初始化失败: {e}") + self.config_stats["config_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用匹配配置管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 匹配配置管理器未初始化") + return False + + self.enabled = True + print("✓ 匹配配置管理器已启用") + return True + + except Exception as e: + print(f"✗ 匹配配置管理器启用失败: {e}") + self.config_stats["config_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用匹配配置管理器""" + try: + self.enabled = False + + # 保存配置(如果有更改) + if self.config_state["is_dirty"]: + self.save_config() + + print("✓ 匹配配置管理器已禁用") + + except Exception as e: + print(f"✗ 匹配配置管理器禁用失败: {e}") + self.config_stats["config_errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理匹配配置管理器资源""" + try: + # 禁用匹配配置管理器 + if self.enabled: + self.disable() + + # 清理回调 + self.config_callbacks.clear() + self.config_listeners.clear() + + self.initialized = False + print("✓ 匹配配置管理器资源已清理") + + except Exception as e: + print(f"✗ 匹配配置管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新匹配配置管理器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + # 检查配置文件是否被外部修改 + self._check_config_file_changes() + + except Exception as e: + print(f"✗ 匹配配置管理器更新失败: {e}") + self.config_stats["config_errors"] += 1 + import traceback + traceback.print_exc() + + def _check_config_file_changes(self): + """检查配置文件是否被外部修改""" + try: + if not os.path.exists(self.config_file_path): + return + + last_modified = os.path.getmtime(self.config_file_path) + if last_modified > self.config_state["last_loaded"]: + # 配置文件已被修改,重新加载 + print("检测到配置文件被外部修改,正在重新加载...") + self.load_config() + + except Exception as e: + print(f"✗ 配置文件变更检查失败: {e}") + self.config_stats["config_errors"] += 1 + + def load_config(self) -> bool: + """ + 加载配置 + + Returns: + 是否加载成功 + """ + try: + # 检查配置文件是否存在 + if os.path.exists(self.config_file_path): + with open(self.config_file_path, 'r', encoding='utf-8') as f: + loaded_config = json.load(f) + + # 合并配置(使用默认配置作为基础) + self.current_config = self._merge_config(self.default_config, loaded_config) + + print(f"✓ 配置已从文件加载: {self.config_file_path}") + else: + # 使用默认配置 + self.current_config = self.default_config.copy() + print("✓ 使用默认配置") + + self.config_state["last_loaded"] = time.time() + self.config_state["is_dirty"] = False + self.config_stats["loads"] += 1 + + # 触发配置加载回调 + self._trigger_config_callback("config_loaded", { + "config": self.current_config, + "timestamp": self.config_state["last_loaded"] + }) + + return True + + except Exception as e: + print(f"✗ 配置加载失败: {e}") + self.config_stats["config_errors"] += 1 + + # 使用默认配置 + self.current_config = self.default_config.copy() + return False + + def _merge_config(self, base_config: Dict[str, Any], override_config: Dict[str, Any]) -> Dict[str, Any]: + """ + 合并配置(递归) + + Args: + base_config: 基础配置 + override_config: 覆盖配置 + + Returns: + 合并后的配置 + """ + try: + merged = base_config.copy() + + for key, value in override_config.items(): + if key in merged and isinstance(merged[key], dict) and isinstance(value, dict): + # 递归合并嵌套字典 + merged[key] = self._merge_config(merged[key], value) + else: + # 直接覆盖 + merged[key] = value + + return merged + + except Exception as e: + print(f"✗ 配置合并失败: {e}") + self.config_stats["config_errors"] += 1 + return base_config + + def save_config(self) -> bool: + """ + 保存配置 + + Returns: + 是否保存成功 + """ + try: + # 创建配置目录(如果不存在) + config_dir = os.path.dirname(self.config_file_path) + if not os.path.exists(config_dir): + os.makedirs(config_dir) + + # 保存配置到文件 + with open(self.config_file_path, 'w', encoding='utf-8') as f: + json.dump(self.current_config, f, indent=2, ensure_ascii=False) + + self.config_state["last_saved"] = time.time() + self.config_state["is_dirty"] = False + self.config_stats["saves"] += 1 + + # 触发配置保存回调 + self._trigger_config_callback("config_saved", { + "config": self.current_config, + "timestamp": self.config_state["last_saved"] + }) + + print(f"✓ 配置已保存到文件: {self.config_file_path}") + return True + + except Exception as e: + print(f"✗ 配置保存失败: {e}") + self.config_stats["config_errors"] += 1 + return False + + def get_config(self, section: str = None, key: str = None, default_value: Any = None) -> Any: + """ + 获取配置值 + + Args: + section: 配置节(可选) + key: 配置键(可选) + default_value: 默认值 + + Returns: + 配置值 + """ + try: + if section is None: + return self.current_config.copy() + + if section not in self.current_config: + return default_value + + if key is None: + return self.current_config[section].copy() + + if key not in self.current_config[section]: + return default_value + + return self.current_config[section][key] + + except Exception as e: + print(f"✗ 获取配置失败: {e}") + self.config_stats["config_errors"] += 1 + return default_value + + def set_config(self, section: str, key: str, value: Any) -> bool: + """ + 设置配置值 + + Args: + section: 配置节 + key: 配置键 + value: 配置值 + + Returns: + 是否设置成功 + """ + try: + if section not in self.current_config: + self.current_config[section] = {} + + self.current_config[section][key] = value + self.config_state["is_dirty"] = True + self.config_stats["updates"] += 1 + + # 通知配置监听器 + self._notify_config_listeners(section, key, value) + + # 触发配置更新回调 + self._trigger_config_callback("config_updated", { + "section": section, + "key": key, + "value": value, + "timestamp": time.time() + }) + + return True + + except Exception as e: + print(f"✗ 设置配置失败: {e}") + self.config_stats["config_errors"] += 1 + return False + + def update_config_section(self, section: str, config_data: Dict[str, Any]) -> bool: + """ + 更新配置节 + + Args: + section: 配置节 + config_data: 配置数据 + + Returns: + 是否更新成功 + """ + try: + if section not in self.current_config: + self.current_config[section] = {} + + self.current_config[section].update(config_data) + self.config_state["is_dirty"] = True + self.config_stats["updates"] += 1 + + # 通知配置监听器 + for key, value in config_data.items(): + self._notify_config_listeners(section, key, value) + + # 触发配置更新回调 + self._trigger_config_callback("config_updated", { + "section": section, + "config_data": config_data, + "timestamp": time.time() + }) + + return True + + except Exception as e: + print(f"✗ 更新配置节失败: {e}") + self.config_stats["config_errors"] += 1 + return False + + def reset_config(self) -> bool: + """ + 重置配置为默认值 + + Returns: + 是否重置成功 + """ + try: + self.current_config = self.default_config.copy() + self.config_state["is_dirty"] = True + + # 通知所有配置监听器 + for section_name, section_data in self.current_config.items(): + if isinstance(section_data, dict): + for key, value in section_data.items(): + self._notify_config_listeners(section_name, key, value) + + print("✓ 配置已重置为默认值") + return True + + except Exception as e: + print(f"✗ 配置重置失败: {e}") + self.config_stats["config_errors"] += 1 + return False + + def register_config_listener(self, section: str, key: str, listener: callable): + """ + 注册配置监听器 + + Args: + section: 配置节 + key: 配置键 + listener: 监听器函数 + """ + try: + listener_key = f"{section}.{key}" + if listener_key not in self.config_listeners: + self.config_listeners[listener_key] = [] + + self.config_listeners[listener_key].append(listener) + print(f"✓ 配置监听器已注册: {listener_key}") + + except Exception as e: + print(f"✗ 配置监听器注册失败: {e}") + self.config_stats["config_errors"] += 1 + + def unregister_config_listener(self, section: str, key: str, listener: callable): + """ + 注销配置监听器 + + Args: + section: 配置节 + key: 配置键 + listener: 监听器函数 + """ + try: + listener_key = f"{section}.{key}" + if listener_key in self.config_listeners: + if listener in self.config_listeners[listener_key]: + self.config_listeners[listener_key].remove(listener) + print(f"✓ 配置监听器已注销: {listener_key}") + + except Exception as e: + print(f"✗ 配置监听器注销失败: {e}") + self.config_stats["config_errors"] += 1 + + def _notify_config_listeners(self, section: str, key: str, value: Any): + """ + 通知配置监听器 + + Args: + section: 配置节 + key: 配置键 + value: 配置值 + """ + try: + listener_key = f"{section}.{key}" + if listener_key in self.config_listeners: + for listener in self.config_listeners[listener_key]: + try: + listener(section, key, value) + except Exception as e: + print(f"✗ 配置监听器执行失败: {listener_key} - {e}") + + except Exception as e: + print(f"✗ 配置监听器通知失败: {e}") + self.config_stats["config_errors"] += 1 + + def get_config_stats(self) -> Dict[str, Any]: + """ + 获取配置统计信息 + + Returns: + 配置统计字典 + """ + return { + "state": self.config_state.copy(), + "stats": self.config_stats.copy(), + "config_sections": list(self.current_config.keys()) + } + + def reset_stats(self): + """重置配置统计信息""" + try: + self.config_stats = { + "loads": 0, + "saves": 0, + "updates": 0, + "config_errors": 0 + } + print("✓ 配置统计信息已重置") + except Exception as e: + print(f"✗ 配置统计信息重置失败: {e}") + + def validate_config(self) -> bool: + """ + 验证配置 + + Returns: + 配置是否有效 + """ + try: + # 检查必需的配置项 + required_sections = ["system", "queue", "matching", "room", "monitoring", "events"] + for section in required_sections: + if section not in self.current_config: + print(f"✗ 缺少必需的配置节: {section}") + return False + + # 检查系统配置 + system_config = self.current_config["system"] + if "max_concurrent_matches" not in system_config or not isinstance(system_config["max_concurrent_matches"], int): + print("✗ 无效的最大并发匹配数配置") + return False + + # 检查队列配置 + queue_config = self.current_config["queue"] + if "max_players_per_match" not in queue_config or not isinstance(queue_config["max_players_per_match"], int): + print("✗ 无效的最大玩家数配置") + return False + + print("✓ 配置验证通过") + return True + + except Exception as e: + print(f"✗ 配置验证失败: {e}") + self.config_stats["config_errors"] += 1 + return False + + def export_config(self, file_path: str) -> bool: + """ + 导出配置到文件 + + Args: + file_path: 导出文件路径 + + Returns: + 是否导出成功 + """ + try: + # 创建导出目录 + export_dir = os.path.dirname(file_path) + if not os.path.exists(export_dir): + os.makedirs(export_dir) + + # 导出配置 + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(self.current_config, f, indent=2, ensure_ascii=False) + + print(f"✓ 配置已导出到: {file_path}") + return True + + except Exception as e: + print(f"✗ 配置导出失败: {e}") + self.config_stats["config_errors"] += 1 + return False + + def import_config(self, file_path: str) -> bool: + """ + 从文件导入配置 + + Args: + file_path: 导入文件路径 + + Returns: + 是否导入成功 + """ + try: + if not os.path.exists(file_path): + print(f"✗ 配置文件不存在: {file_path}") + return False + + # 读取配置文件 + with open(file_path, 'r', encoding='utf-8') as f: + imported_config = json.load(f) + + # 验证配置 + if not isinstance(imported_config, dict): + print("✗ 无效的配置文件格式") + return False + + # 合并配置 + self.current_config = self._merge_config(self.default_config, imported_config) + self.config_state["is_dirty"] = True + + # 通知配置监听器 + for section_name, section_data in self.current_config.items(): + if isinstance(section_data, dict): + for key, value in section_data.items(): + self._notify_config_listeners(section_name, key, value) + + print(f"✓ 配置已从文件导入: {file_path}") + return True + + except Exception as e: + print(f"✗ 配置导入失败: {e}") + self.config_stats["config_errors"] += 1 + return False + + def _trigger_config_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发配置回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.config_callbacks: + for callback in self.config_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 配置回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 配置回调触发失败: {e}") + + def register_config_callback(self, callback_type: str, callback: callable): + """ + 注册配置回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.config_callbacks: + self.config_callbacks[callback_type].append(callback) + print(f"✓ 配置回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 配置回调注册失败: {e}") + + def unregister_config_callback(self, callback_type: str, callback: callable): + """ + 注销配置回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.config_callbacks: + if callback in self.config_callbacks[callback_type]: + self.config_callbacks[callback_type].remove(callback) + print(f"✓ 配置回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 配置回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/matchmaking_system/core/match_manager.py b/plugins/user/matchmaking_system/core/match_manager.py index f86aa16f..d160c6f5 100644 --- a/plugins/user/matchmaking_system/core/match_manager.py +++ b/plugins/user/matchmaking_system/core/match_manager.py @@ -1,417 +1,417 @@ -""" -匹配管理器模块 -管理匹配过程和规则 -""" - -import time -import threading -from typing import Dict, Any, List, Optional - -class MatchManager: - """ - 匹配管理器 - 管理匹配过程和规则 - """ - - def __init__(self, plugin): - """ - 初始化匹配管理器 - - Args: - plugin: 匹配系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 匹配配置 - self.match_config = { - "enable_matchmaking": True, - "matchmaking_interval": 1.0, - "max_match_attempts": 3, - "match_timeout": 30.0, - "enable_skill_based_matching": True, - "enable_team_balancing": True, - "enable_cross_platform_matching": True - } - - # 匹配状态 - self.match_state = { - "is_matching": False, - "active_matches": 0, - "successful_matches": 0, - "failed_matches": 0, - "last_match_time": 0.0 - } - - # 匹配统计 - self.match_stats = { - "matches_started": 0, - "matches_completed": 0, - "matches_cancelled": 0, - "average_match_time": 0.0, - "match_errors": 0 - } - - # 匹配锁 - self.match_lock = threading.RLock() - - # 回调函数 - self.match_callbacks = { - "match_started": [], - "match_completed": [], - "match_cancelled": [], - "match_error": [] - } - - # 时间戳记录 - self.last_matchmaking_run = 0.0 - self.last_stats_reset = 0.0 - - print("✓ 匹配管理器已创建") - - def initialize(self) -> bool: - """ - 初始化匹配管理器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化匹配管理器...") - - self.initialized = True - print("✓ 匹配管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 匹配管理器初始化失败: {e}") - self.match_stats["match_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用匹配管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 匹配管理器未初始化") - return False - - self.enabled = True - print("✓ 匹配管理器已启用") - return True - - except Exception as e: - print(f"✗ 匹配管理器启用失败: {e}") - self.match_stats["match_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用匹配管理器""" - try: - self.enabled = False - print("✓ 匹配管理器已禁用") - - except Exception as e: - print(f"✗ 匹配管理器禁用失败: {e}") - self.match_stats["match_errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理匹配管理器资源""" - try: - # 禁用匹配管理器 - if self.enabled: - self.disable() - - # 清理回调 - self.match_callbacks.clear() - - self.initialized = False - print("✓ 匹配管理器资源已清理") - - except Exception as e: - print(f"✗ 匹配管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新匹配管理器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 定期运行匹配算法 - if current_time - self.last_matchmaking_run >= self.match_config["matchmaking_interval"]: - self._run_matchmaking() - self.last_matchmaking_run = current_time - - self.match_state["last_match_time"] = current_time - - except Exception as e: - print(f"✗ 匹配管理器更新失败: {e}") - self.match_stats["match_errors"] += 1 - import traceback - traceback.print_exc() - - def _run_matchmaking(self): - """运行匹配算法""" - try: - if not self.match_config["enable_matchmaking"]: - return - - # 通知队列管理器运行匹配 - if self.plugin.queue_manager: - self.plugin.queue_manager.process_queues() - - except Exception as e: - print(f"✗ 匹配算法运行失败: {e}") - self.match_stats["match_errors"] += 1 - - def start_matchmaking(self, player_ids: List[str], match_params: Dict[str, Any] = None) -> str: - """ - 开始匹配 - - Args: - player_ids: 玩家ID列表 - match_params: 匹配参数 - - Returns: - 匹配ID - """ - try: - if not self.enabled: - raise Exception("匹配管理器未启用") - - # 生成匹配ID - match_id = f"match_{int(time.time() * 1000000)}" - - # 添加玩家到队列 - if self.plugin.queue_manager: - queue_success = self.plugin.queue_manager.add_players_to_queue( - player_ids, match_params or {}) - - if queue_success: - self.match_stats["matches_started"] += 1 - self.match_state["active_matches"] += 1 - - # 触发匹配开始回调 - self._trigger_match_callback("match_started", { - "match_id": match_id, - "player_ids": player_ids, - "match_params": match_params, - "timestamp": time.time() - }) - - print(f"✓ 匹配已开始: {match_id}") - return match_id - else: - raise Exception("添加玩家到队列失败") - else: - raise Exception("队列管理器不可用") - - except Exception as e: - print(f"✗ 匹配开始失败: {e}") - self.match_stats["match_errors"] += 1 - - # 触发匹配错误回调 - self._trigger_match_callback("match_error", { - "player_ids": player_ids, - "error": str(e), - "timestamp": time.time() - }) - - return "" - - def cancel_matchmaking(self, match_id: str) -> bool: - """ - 取消匹配 - - Args: - match_id: 匹配ID - - Returns: - 是否取消成功 - """ - try: - # 从队列中移除玩家 - if self.plugin.queue_manager: - cancel_success = self.plugin.queue_manager.remove_players_from_queue(match_id) - - if cancel_success: - self.match_stats["matches_cancelled"] += 1 - if self.match_state["active_matches"] > 0: - self.match_state["active_matches"] -= 1 - - # 触发匹配取消回调 - self._trigger_match_callback("match_cancelled", { - "match_id": match_id, - "timestamp": time.time() - }) - - print(f"✓ 匹配已取消: {match_id}") - return True - else: - return False - else: - return False - - except Exception as e: - print(f"✗ 匹配取消失败: {e}") - self.match_stats["match_errors"] += 1 - return False - - def complete_match(self, match_id: str, room_id: str) -> bool: - """ - 完成匹配 - - Args: - match_id: 匹配ID - room_id: 房间ID - - Returns: - 是否完成成功 - """ - try: - self.match_stats["matches_completed"] += 1 - self.match_state["successful_matches"] += 1 - if self.match_state["active_matches"] > 0: - self.match_state["active_matches"] -= 1 - - # 触发匹配完成回调 - self._trigger_match_callback("match_completed", { - "match_id": match_id, - "room_id": room_id, - "timestamp": time.time() - }) - - print(f"✓ 匹配已完成: {match_id} -> 房间: {room_id}") - return True - - except Exception as e: - print(f"✗ 匹配完成失败: {e}") - self.match_stats["match_errors"] += 1 - return False - - def get_match_stats(self) -> Dict[str, Any]: - """ - 获取匹配统计信息 - - Returns: - 匹配统计字典 - """ - return { - "state": self.match_state.copy(), - "stats": self.match_stats.copy(), - "config": self.match_config.copy() - } - - def reset_stats(self): - """重置匹配统计信息""" - try: - self.match_stats = { - "matches_started": 0, - "matches_completed": 0, - "matches_cancelled": 0, - "average_match_time": 0.0, - "match_errors": 0 - } - print("✓ 匹配统计信息已重置") - except Exception as e: - print(f"✗ 匹配统计信息重置失败: {e}") - - def set_match_config(self, config: Dict[str, Any]) -> bool: - """ - 设置匹配配置 - - Args: - config: 匹配配置字典 - - Returns: - 是否设置成功 - """ - try: - self.match_config.update(config) - print(f"✓ 匹配配置已更新: {self.match_config}") - return True - except Exception as e: - print(f"✗ 匹配配置设置失败: {e}") - return False - - def get_match_config(self) -> Dict[str, Any]: - """ - 获取匹配配置 - - Returns: - 匹配配置字典 - """ - return self.match_config.copy() - - def _trigger_match_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发匹配回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.match_callbacks: - for callback in self.match_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 匹配回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 匹配回调触发失败: {e}") - - def register_match_callback(self, callback_type: str, callback: callable): - """ - 注册匹配回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.match_callbacks: - self.match_callbacks[callback_type].append(callback) - print(f"✓ 匹配回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 匹配回调注册失败: {e}") - - def unregister_match_callback(self, callback_type: str, callback: callable): - """ - 注销匹配回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.match_callbacks: - if callback in self.match_callbacks[callback_type]: - self.match_callbacks[callback_type].remove(callback) - print(f"✓ 匹配回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +匹配管理器模块 +管理匹配过程和规则 +""" + +import time +import threading +from typing import Dict, Any, List, Optional + +class MatchManager: + """ + 匹配管理器 + 管理匹配过程和规则 + """ + + def __init__(self, plugin): + """ + 初始化匹配管理器 + + Args: + plugin: 匹配系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 匹配配置 + self.match_config = { + "enable_matchmaking": True, + "matchmaking_interval": 1.0, + "max_match_attempts": 3, + "match_timeout": 30.0, + "enable_skill_based_matching": True, + "enable_team_balancing": True, + "enable_cross_platform_matching": True + } + + # 匹配状态 + self.match_state = { + "is_matching": False, + "active_matches": 0, + "successful_matches": 0, + "failed_matches": 0, + "last_match_time": 0.0 + } + + # 匹配统计 + self.match_stats = { + "matches_started": 0, + "matches_completed": 0, + "matches_cancelled": 0, + "average_match_time": 0.0, + "match_errors": 0 + } + + # 匹配锁 + self.match_lock = threading.RLock() + + # 回调函数 + self.match_callbacks = { + "match_started": [], + "match_completed": [], + "match_cancelled": [], + "match_error": [] + } + + # 时间戳记录 + self.last_matchmaking_run = 0.0 + self.last_stats_reset = 0.0 + + print("✓ 匹配管理器已创建") + + def initialize(self) -> bool: + """ + 初始化匹配管理器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化匹配管理器...") + + self.initialized = True + print("✓ 匹配管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 匹配管理器初始化失败: {e}") + self.match_stats["match_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用匹配管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 匹配管理器未初始化") + return False + + self.enabled = True + print("✓ 匹配管理器已启用") + return True + + except Exception as e: + print(f"✗ 匹配管理器启用失败: {e}") + self.match_stats["match_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用匹配管理器""" + try: + self.enabled = False + print("✓ 匹配管理器已禁用") + + except Exception as e: + print(f"✗ 匹配管理器禁用失败: {e}") + self.match_stats["match_errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理匹配管理器资源""" + try: + # 禁用匹配管理器 + if self.enabled: + self.disable() + + # 清理回调 + self.match_callbacks.clear() + + self.initialized = False + print("✓ 匹配管理器资源已清理") + + except Exception as e: + print(f"✗ 匹配管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新匹配管理器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 定期运行匹配算法 + if current_time - self.last_matchmaking_run >= self.match_config["matchmaking_interval"]: + self._run_matchmaking() + self.last_matchmaking_run = current_time + + self.match_state["last_match_time"] = current_time + + except Exception as e: + print(f"✗ 匹配管理器更新失败: {e}") + self.match_stats["match_errors"] += 1 + import traceback + traceback.print_exc() + + def _run_matchmaking(self): + """运行匹配算法""" + try: + if not self.match_config["enable_matchmaking"]: + return + + # 通知队列管理器运行匹配 + if self.plugin.queue_manager: + self.plugin.queue_manager.process_queues() + + except Exception as e: + print(f"✗ 匹配算法运行失败: {e}") + self.match_stats["match_errors"] += 1 + + def start_matchmaking(self, player_ids: List[str], match_params: Dict[str, Any] = None) -> str: + """ + 开始匹配 + + Args: + player_ids: 玩家ID列表 + match_params: 匹配参数 + + Returns: + 匹配ID + """ + try: + if not self.enabled: + raise Exception("匹配管理器未启用") + + # 生成匹配ID + match_id = f"match_{int(time.time() * 1000000)}" + + # 添加玩家到队列 + if self.plugin.queue_manager: + queue_success = self.plugin.queue_manager.add_players_to_queue( + player_ids, match_params or {}) + + if queue_success: + self.match_stats["matches_started"] += 1 + self.match_state["active_matches"] += 1 + + # 触发匹配开始回调 + self._trigger_match_callback("match_started", { + "match_id": match_id, + "player_ids": player_ids, + "match_params": match_params, + "timestamp": time.time() + }) + + print(f"✓ 匹配已开始: {match_id}") + return match_id + else: + raise Exception("添加玩家到队列失败") + else: + raise Exception("队列管理器不可用") + + except Exception as e: + print(f"✗ 匹配开始失败: {e}") + self.match_stats["match_errors"] += 1 + + # 触发匹配错误回调 + self._trigger_match_callback("match_error", { + "player_ids": player_ids, + "error": str(e), + "timestamp": time.time() + }) + + return "" + + def cancel_matchmaking(self, match_id: str) -> bool: + """ + 取消匹配 + + Args: + match_id: 匹配ID + + Returns: + 是否取消成功 + """ + try: + # 从队列中移除玩家 + if self.plugin.queue_manager: + cancel_success = self.plugin.queue_manager.remove_players_from_queue(match_id) + + if cancel_success: + self.match_stats["matches_cancelled"] += 1 + if self.match_state["active_matches"] > 0: + self.match_state["active_matches"] -= 1 + + # 触发匹配取消回调 + self._trigger_match_callback("match_cancelled", { + "match_id": match_id, + "timestamp": time.time() + }) + + print(f"✓ 匹配已取消: {match_id}") + return True + else: + return False + else: + return False + + except Exception as e: + print(f"✗ 匹配取消失败: {e}") + self.match_stats["match_errors"] += 1 + return False + + def complete_match(self, match_id: str, room_id: str) -> bool: + """ + 完成匹配 + + Args: + match_id: 匹配ID + room_id: 房间ID + + Returns: + 是否完成成功 + """ + try: + self.match_stats["matches_completed"] += 1 + self.match_state["successful_matches"] += 1 + if self.match_state["active_matches"] > 0: + self.match_state["active_matches"] -= 1 + + # 触发匹配完成回调 + self._trigger_match_callback("match_completed", { + "match_id": match_id, + "room_id": room_id, + "timestamp": time.time() + }) + + print(f"✓ 匹配已完成: {match_id} -> 房间: {room_id}") + return True + + except Exception as e: + print(f"✗ 匹配完成失败: {e}") + self.match_stats["match_errors"] += 1 + return False + + def get_match_stats(self) -> Dict[str, Any]: + """ + 获取匹配统计信息 + + Returns: + 匹配统计字典 + """ + return { + "state": self.match_state.copy(), + "stats": self.match_stats.copy(), + "config": self.match_config.copy() + } + + def reset_stats(self): + """重置匹配统计信息""" + try: + self.match_stats = { + "matches_started": 0, + "matches_completed": 0, + "matches_cancelled": 0, + "average_match_time": 0.0, + "match_errors": 0 + } + print("✓ 匹配统计信息已重置") + except Exception as e: + print(f"✗ 匹配统计信息重置失败: {e}") + + def set_match_config(self, config: Dict[str, Any]) -> bool: + """ + 设置匹配配置 + + Args: + config: 匹配配置字典 + + Returns: + 是否设置成功 + """ + try: + self.match_config.update(config) + print(f"✓ 匹配配置已更新: {self.match_config}") + return True + except Exception as e: + print(f"✗ 匹配配置设置失败: {e}") + return False + + def get_match_config(self) -> Dict[str, Any]: + """ + 获取匹配配置 + + Returns: + 匹配配置字典 + """ + return self.match_config.copy() + + def _trigger_match_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发匹配回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.match_callbacks: + for callback in self.match_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 匹配回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 匹配回调触发失败: {e}") + + def register_match_callback(self, callback_type: str, callback: callable): + """ + 注册匹配回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.match_callbacks: + self.match_callbacks[callback_type].append(callback) + print(f"✓ 匹配回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 匹配回调注册失败: {e}") + + def unregister_match_callback(self, callback_type: str, callback: callable): + """ + 注销匹配回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.match_callbacks: + if callback in self.match_callbacks[callback_type]: + self.match_callbacks[callback_type].remove(callback) + print(f"✓ 匹配回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 匹配回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/matchmaking_system/editor/match_editor.py b/plugins/user/matchmaking_system/editor/match_editor.py index eaabdfcb..1ecc1f14 100644 --- a/plugins/user/matchmaking_system/editor/match_editor.py +++ b/plugins/user/matchmaking_system/editor/match_editor.py @@ -1,606 +1,606 @@ -""" -匹配编辑器模块 -提供图形界面配置匹配参数 -""" - -import time -from typing import Dict, Any, List, Optional - -class MatchEditor: - """ - 匹配编辑器 - 提供图形界面配置匹配参数 - """ - - def __init__(self, plugin): - """ - 初始化匹配编辑器 - - Args: - plugin: 匹配系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 编辑器配置 - self.editor_config = { - "enable_editor": True, - "enable_realtime_monitoring": True, - "enable_performance_display": True, - "enable_config_editor": True, - "enable_statistics_display": True, - "refresh_interval": 1.0, - "max_data_points": 100 - } - - # 编辑器状态 - self.editor_state = { - "is_visible": False, - "last_update": 0.0, - "active_tab": "overview", - "selected_queue": None, - "selected_algorithm": None - } - - # UI组件 - self.ui_components = { - "main_window": None, - "tabs": {}, - "panels": {}, - "charts": {}, - "controls": {} - } - - # 编辑器统计 - self.editor_stats = { - "updates": 0, - "ui_refreshes": 0, - "data_points": 0, - "editor_errors": 0 - } - - # 实时数据缓存 - self.realtime_data = { - "performance_metrics": {}, - "queue_data": {}, - "match_data": {}, - "algorithm_stats": {}, - "system_stats": {} - } - - # 回调函数 - self.editor_callbacks = { - "ui_updated": [], - "data_refreshed": [], - "config_changed": [], - "editor_error": [] - } - - # 时间戳记录 - self.last_ui_update = 0.0 - self.last_data_refresh = 0.0 - - print("✓ 匹配编辑器已创建") - - def initialize(self) -> bool: - """ - 初始化匹配编辑器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化匹配编辑器...") - - # 初始化UI组件(模拟) - self._initialize_ui_components() - - self.initialized = True - print("✓ 匹配编辑器初始化完成") - return True - - except Exception as e: - print(f"✗ 匹配编辑器初始化失败: {e}") - self.editor_stats["editor_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def _initialize_ui_components(self): - """初始化UI组件""" - try: - # 创建主窗口 - self.ui_components["main_window"] = { - "title": "匹配系统编辑器", - "size": (1200, 800), - "position": (100, 100) - } - - # 创建标签页 - self.ui_components["tabs"] = { - "overview": {"name": "概览", "icon": "dashboard"}, - "queues": {"name": "队列", "icon": "queue"}, - "matches": {"name": "匹配", "icon": "gamepad"}, - "algorithms": {"name": "算法", "icon": "calculate"}, - "rooms": {"name": "房间", "icon": "meeting_room"}, - "config": {"name": "配置", "icon": "settings"}, - "statistics": {"name": "统计", "icon": "bar_chart"} - } - - # 创建面板 - self.ui_components["panels"] = { - "system_status": {"title": "系统状态", "visible": True}, - "performance": {"title": "性能指标", "visible": True}, - "queues_list": {"title": "队列列表", "visible": True}, - "matches_list": {"title": "匹配列表", "visible": True}, - "algorithm_stats": {"title": "算法统计", "visible": True} - } - - print("✓ UI组件已初始化") - - except Exception as e: - print(f"✗ UI组件初始化失败: {e}") - self.editor_stats["editor_errors"] += 1 - - def enable(self) -> bool: - """ - 启用匹配编辑器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 匹配编辑器未初始化") - return False - - self.enabled = True - print("✓ 匹配编辑器已启用") - return True - - except Exception as e: - print(f"✗ 匹配编辑器启用失败: {e}") - self.editor_stats["editor_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用匹配编辑器""" - try: - self.enabled = False - - # 隐藏UI - self.hide_editor() - - print("✓ 匹配编辑器已禁用") - - except Exception as e: - print(f"✗ 匹配编辑器禁用失败: {e}") - self.editor_stats["editor_errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理匹配编辑器资源""" - try: - # 禁用匹配编辑器 - if self.enabled: - self.disable() - - # 清理回调 - self.editor_callbacks.clear() - - self.initialized = False - print("✓ 匹配编辑器资源已清理") - - except Exception as e: - print(f"✗ 匹配编辑器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新匹配编辑器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 定期刷新UI - if current_time - self.last_ui_update >= self.editor_config["refresh_interval"]: - self._refresh_ui() - self.last_ui_update = current_time - - # 定期更新数据 - if current_time - self.last_data_refresh >= 0.5: # 每0.5秒更新一次数据 - self._update_realtime_data() - self.last_data_refresh = current_time - - self.editor_state["last_update"] = current_time - - except Exception as e: - print(f"✗ 匹配编辑器更新失败: {e}") - self.editor_stats["editor_errors"] += 1 - import traceback - traceback.print_exc() - - def _refresh_ui(self): - """刷新UI""" - try: - # 更新UI组件状态 - self._update_ui_components() - - # 更新统计信息 - self.editor_stats["ui_refreshes"] += 1 - - # 触发UI更新回调 - self._trigger_editor_callback("ui_updated", { - "timestamp": time.time() - }) - - except Exception as e: - print(f"✗ UI刷新失败: {e}") - self.editor_stats["editor_errors"] += 1 - - def _update_ui_components(self): - """更新UI组件""" - try: - # 更新系统状态面板 - system_stats = {} - if self.plugin.match_manager: - system_stats.update(self.plugin.match_manager.get_match_stats()) - if self.plugin.queue_manager: - system_stats.update(self.plugin.queue_manager.get_queue_stats()) - if self.plugin.algorithm_manager: - system_stats.update(self.plugin.algorithm_manager.get_algorithm_stats()) - - self.ui_components["panels"]["system_status"]["data"] = system_stats - - # 更新队列列表面板 - if self.plugin.queue_manager: - queues = self.plugin.queue_manager.get_queue_info() - self.ui_components["panels"]["queues_list"]["data"] = queues - - # 更新匹配列表面板 - if self.plugin.match_manager: - match_stats = self.plugin.match_manager.get_match_stats() - self.ui_components["panels"]["matches_list"]["data"] = match_stats - - # 更新算法统计面板 - if self.plugin.algorithm_manager: - algorithm_stats = self.plugin.algorithm_manager.get_algorithm_stats() - self.ui_components["panels"]["algorithm_stats"]["data"] = algorithm_stats - - except Exception as e: - print(f"✗ UI组件更新失败: {e}") - self.editor_stats["editor_errors"] += 1 - - def _update_realtime_data(self): - """更新实时数据""" - try: - current_time = time.time() - - # 更新性能指标 - if self.plugin.monitor: - metrics = self.plugin.monitor.get_performance_metrics() - self.realtime_data["performance_metrics"] = metrics - - # 更新队列数据 - if self.plugin.queue_manager: - queue_info = self.plugin.queue_manager.get_queue_info() - self.realtime_data["queue_data"] = queue_info - - # 更新匹配数据 - if self.plugin.match_manager: - match_stats = self.plugin.match_manager.get_match_stats() - self.realtime_data["match_data"] = match_stats - - # 更新算法统计 - if self.plugin.algorithm_manager: - algorithm_stats = self.plugin.algorithm_manager.get_algorithm_stats() - self.realtime_data["algorithm_stats"] = algorithm_stats - - # 更新系统统计 - system_stats = {} - if self.plugin.config_manager: - system_stats["config"] = self.plugin.config_manager.get_config_stats() - if self.plugin.event_handler: - system_stats["events"] = self.plugin.event_handler.get_event_stats() - - self.realtime_data["system_stats"] = system_stats - - # 触发数据刷新回调 - self._trigger_editor_callback("data_refreshed", { - "data": self.realtime_data, - "timestamp": current_time - }) - - except Exception as e: - print(f"✗ 实时数据更新失败: {e}") - self.editor_stats["editor_errors"] += 1 - - def show_editor(self): - """显示编辑器""" - try: - self.editor_state["is_visible"] = True - print("✓ 匹配编辑器已显示") - - # 刷新数据 - self._update_realtime_data() - - except Exception as e: - print(f"✗ 编辑器显示失败: {e}") - self.editor_stats["editor_errors"] += 1 - - def hide_editor(self): - """隐藏编辑器""" - try: - self.editor_state["is_visible"] = False - print("✓ 匹配编辑器已隐藏") - - except Exception as e: - print(f"✗ 编辑器隐藏失败: {e}") - self.editor_stats["editor_errors"] += 1 - - def switch_tab(self, tab_name: str): - """ - 切换标签页 - - Args: - tab_name: 标签页名称 - """ - try: - if tab_name in self.ui_components["tabs"]: - self.editor_state["active_tab"] = tab_name - print(f"✓ 切换到标签页: {tab_name}") - else: - print(f"✗ 无效的标签页: {tab_name}") - - except Exception as e: - print(f"✗ 标签页切换失败: {e}") - self.editor_stats["editor_errors"] += 1 - - def select_queue(self, queue_name: str): - """ - 选择队列 - - Args: - queue_name: 队列名称 - """ - try: - self.editor_state["selected_queue"] = queue_name - print(f"✓ 选择队列: {queue_name}") - - except Exception as e: - print(f"✗ 队列选择失败: {e}") - self.editor_stats["editor_errors"] += 1 - - def select_algorithm(self, algorithm_name: str): - """ - 选择算法 - - Args: - algorithm_name: 算法名称 - """ - try: - self.editor_state["selected_algorithm"] = algorithm_name - print(f"✓ 选择算法: {algorithm_name}") - - except Exception as e: - print(f"✗ 算法选择失败: {e}") - self.editor_stats["editor_errors"] += 1 - - def update_config(self, section: str, key: str, value: Any) -> bool: - """ - 更新配置 - - Args: - section: 配置节 - key: 配置键 - value: 配置值 - - Returns: - 是否更新成功 - """ - try: - # 通过配置管理器更新配置 - if self.plugin.config_manager: - result = self.plugin.config_manager.set_config(section, key, value) - - if result: - self.editor_stats["updates"] += 1 - - # 触发配置更改回调 - self._trigger_editor_callback("config_changed", { - "section": section, - "key": key, - "value": value, - "timestamp": time.time() - }) - - print(f"✓ 配置已更新: {section}.{key} = {value}") - return True - else: - print(f"✗ 配置更新失败: {section}.{key}") - return False - else: - print("✗ 配置管理器不可用") - return False - - except Exception as e: - print(f"✗ 配置更新失败: {e}") - self.editor_stats["editor_errors"] += 1 - return False - - def get_editor_stats(self) -> Dict[str, Any]: - """ - 获取编辑器统计信息 - - Returns: - 编辑器统计字典 - """ - return { - "state": self.editor_state.copy(), - "stats": self.editor_stats.copy(), - "config": self.editor_config.copy(), - "ui_components": len(self.ui_components["tabs"]) - } - - def reset_stats(self): - """重置编辑器统计信息""" - try: - self.editor_stats = { - "updates": 0, - "ui_refreshes": 0, - "data_points": 0, - "editor_errors": 0 - } - print("✓ 编辑器统计信息已重置") - except Exception as e: - print(f"✗ 编辑器统计信息重置失败: {e}") - - def set_editor_config(self, config: Dict[str, Any]) -> bool: - """ - 设置编辑器配置 - - Args: - config: 编辑器配置字典 - - Returns: - 是否设置成功 - """ - try: - self.editor_config.update(config) - print(f"✓ 编辑器配置已更新: {self.editor_config}") - return True - except Exception as e: - print(f"✗ 编辑器配置设置失败: {e}") - return False - - def get_editor_config(self) -> Dict[str, Any]: - """ - 获取编辑器配置 - - Returns: - 编辑器配置字典 - """ - return self.editor_config.copy() - - def get_realtime_data(self) -> Dict[str, Any]: - """ - 获取实时数据 - - Returns: - 实时数据字典 - """ - return self.realtime_data.copy() - - def export_data(self, data_type: str, file_path: str) -> bool: - """ - 导出数据 - - Args: - data_type: 数据类型 - file_path: 导出文件路径 - - Returns: - 是否导出成功 - """ - try: - import json - import os - - # 创建导出目录 - export_dir = os.path.dirname(file_path) - if not os.path.exists(export_dir): - os.makedirs(export_dir) - - # 获取要导出的数据 - export_data = None - if data_type == "performance": - export_data = self.realtime_data["performance_metrics"] - elif data_type == "queues": - export_data = self.realtime_data["queue_data"] - elif data_type == "matches": - export_data = self.realtime_data["match_data"] - elif data_type == "algorithms": - export_data = self.realtime_data["algorithm_stats"] - elif data_type == "system": - export_data = self.realtime_data["system_stats"] - elif data_type == "all": - export_data = self.realtime_data - else: - print(f"✗ 不支持的数据类型: {data_type}") - return False - - # 导出数据 - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(export_data, f, indent=2, ensure_ascii=False, default=str) - - print(f"✓ 数据已导出到: {file_path}") - return True - - except Exception as e: - print(f"✗ 数据导出失败: {e}") - self.editor_stats["editor_errors"] += 1 - return False - - def _trigger_editor_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发编辑器回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.editor_callbacks: - for callback in self.editor_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 编辑器回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 编辑器回调触发失败: {e}") - - def register_editor_callback(self, callback_type: str, callback: callable): - """ - 注册编辑器回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.editor_callbacks: - self.editor_callbacks[callback_type].append(callback) - print(f"✓ 编辑器回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 编辑器回调注册失败: {e}") - - def unregister_editor_callback(self, callback_type: str, callback: callable): - """ - 注销编辑器回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.editor_callbacks: - if callback in self.editor_callbacks[callback_type]: - self.editor_callbacks[callback_type].remove(callback) - print(f"✓ 编辑器回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +匹配编辑器模块 +提供图形界面配置匹配参数 +""" + +import time +from typing import Dict, Any, List, Optional + +class MatchEditor: + """ + 匹配编辑器 + 提供图形界面配置匹配参数 + """ + + def __init__(self, plugin): + """ + 初始化匹配编辑器 + + Args: + plugin: 匹配系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 编辑器配置 + self.editor_config = { + "enable_editor": True, + "enable_realtime_monitoring": True, + "enable_performance_display": True, + "enable_config_editor": True, + "enable_statistics_display": True, + "refresh_interval": 1.0, + "max_data_points": 100 + } + + # 编辑器状态 + self.editor_state = { + "is_visible": False, + "last_update": 0.0, + "active_tab": "overview", + "selected_queue": None, + "selected_algorithm": None + } + + # UI组件 + self.ui_components = { + "main_window": None, + "tabs": {}, + "panels": {}, + "charts": {}, + "controls": {} + } + + # 编辑器统计 + self.editor_stats = { + "updates": 0, + "ui_refreshes": 0, + "data_points": 0, + "editor_errors": 0 + } + + # 实时数据缓存 + self.realtime_data = { + "performance_metrics": {}, + "queue_data": {}, + "match_data": {}, + "algorithm_stats": {}, + "system_stats": {} + } + + # 回调函数 + self.editor_callbacks = { + "ui_updated": [], + "data_refreshed": [], + "config_changed": [], + "editor_error": [] + } + + # 时间戳记录 + self.last_ui_update = 0.0 + self.last_data_refresh = 0.0 + + print("✓ 匹配编辑器已创建") + + def initialize(self) -> bool: + """ + 初始化匹配编辑器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化匹配编辑器...") + + # 初始化UI组件(模拟) + self._initialize_ui_components() + + self.initialized = True + print("✓ 匹配编辑器初始化完成") + return True + + except Exception as e: + print(f"✗ 匹配编辑器初始化失败: {e}") + self.editor_stats["editor_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def _initialize_ui_components(self): + """初始化UI组件""" + try: + # 创建主窗口 + self.ui_components["main_window"] = { + "title": "匹配系统编辑器", + "size": (1200, 800), + "position": (100, 100) + } + + # 创建标签页 + self.ui_components["tabs"] = { + "overview": {"name": "概览", "icon": "dashboard"}, + "queues": {"name": "队列", "icon": "queue"}, + "matches": {"name": "匹配", "icon": "gamepad"}, + "algorithms": {"name": "算法", "icon": "calculate"}, + "rooms": {"name": "房间", "icon": "meeting_room"}, + "config": {"name": "配置", "icon": "settings"}, + "statistics": {"name": "统计", "icon": "bar_chart"} + } + + # 创建面板 + self.ui_components["panels"] = { + "system_status": {"title": "系统状态", "visible": True}, + "performance": {"title": "性能指标", "visible": True}, + "queues_list": {"title": "队列列表", "visible": True}, + "matches_list": {"title": "匹配列表", "visible": True}, + "algorithm_stats": {"title": "算法统计", "visible": True} + } + + print("✓ UI组件已初始化") + + except Exception as e: + print(f"✗ UI组件初始化失败: {e}") + self.editor_stats["editor_errors"] += 1 + + def enable(self) -> bool: + """ + 启用匹配编辑器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 匹配编辑器未初始化") + return False + + self.enabled = True + print("✓ 匹配编辑器已启用") + return True + + except Exception as e: + print(f"✗ 匹配编辑器启用失败: {e}") + self.editor_stats["editor_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用匹配编辑器""" + try: + self.enabled = False + + # 隐藏UI + self.hide_editor() + + print("✓ 匹配编辑器已禁用") + + except Exception as e: + print(f"✗ 匹配编辑器禁用失败: {e}") + self.editor_stats["editor_errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理匹配编辑器资源""" + try: + # 禁用匹配编辑器 + if self.enabled: + self.disable() + + # 清理回调 + self.editor_callbacks.clear() + + self.initialized = False + print("✓ 匹配编辑器资源已清理") + + except Exception as e: + print(f"✗ 匹配编辑器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新匹配编辑器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 定期刷新UI + if current_time - self.last_ui_update >= self.editor_config["refresh_interval"]: + self._refresh_ui() + self.last_ui_update = current_time + + # 定期更新数据 + if current_time - self.last_data_refresh >= 0.5: # 每0.5秒更新一次数据 + self._update_realtime_data() + self.last_data_refresh = current_time + + self.editor_state["last_update"] = current_time + + except Exception as e: + print(f"✗ 匹配编辑器更新失败: {e}") + self.editor_stats["editor_errors"] += 1 + import traceback + traceback.print_exc() + + def _refresh_ui(self): + """刷新UI""" + try: + # 更新UI组件状态 + self._update_ui_components() + + # 更新统计信息 + self.editor_stats["ui_refreshes"] += 1 + + # 触发UI更新回调 + self._trigger_editor_callback("ui_updated", { + "timestamp": time.time() + }) + + except Exception as e: + print(f"✗ UI刷新失败: {e}") + self.editor_stats["editor_errors"] += 1 + + def _update_ui_components(self): + """更新UI组件""" + try: + # 更新系统状态面板 + system_stats = {} + if self.plugin.match_manager: + system_stats.update(self.plugin.match_manager.get_match_stats()) + if self.plugin.queue_manager: + system_stats.update(self.plugin.queue_manager.get_queue_stats()) + if self.plugin.algorithm_manager: + system_stats.update(self.plugin.algorithm_manager.get_algorithm_stats()) + + self.ui_components["panels"]["system_status"]["data"] = system_stats + + # 更新队列列表面板 + if self.plugin.queue_manager: + queues = self.plugin.queue_manager.get_queue_info() + self.ui_components["panels"]["queues_list"]["data"] = queues + + # 更新匹配列表面板 + if self.plugin.match_manager: + match_stats = self.plugin.match_manager.get_match_stats() + self.ui_components["panels"]["matches_list"]["data"] = match_stats + + # 更新算法统计面板 + if self.plugin.algorithm_manager: + algorithm_stats = self.plugin.algorithm_manager.get_algorithm_stats() + self.ui_components["panels"]["algorithm_stats"]["data"] = algorithm_stats + + except Exception as e: + print(f"✗ UI组件更新失败: {e}") + self.editor_stats["editor_errors"] += 1 + + def _update_realtime_data(self): + """更新实时数据""" + try: + current_time = time.time() + + # 更新性能指标 + if self.plugin.monitor: + metrics = self.plugin.monitor.get_performance_metrics() + self.realtime_data["performance_metrics"] = metrics + + # 更新队列数据 + if self.plugin.queue_manager: + queue_info = self.plugin.queue_manager.get_queue_info() + self.realtime_data["queue_data"] = queue_info + + # 更新匹配数据 + if self.plugin.match_manager: + match_stats = self.plugin.match_manager.get_match_stats() + self.realtime_data["match_data"] = match_stats + + # 更新算法统计 + if self.plugin.algorithm_manager: + algorithm_stats = self.plugin.algorithm_manager.get_algorithm_stats() + self.realtime_data["algorithm_stats"] = algorithm_stats + + # 更新系统统计 + system_stats = {} + if self.plugin.config_manager: + system_stats["config"] = self.plugin.config_manager.get_config_stats() + if self.plugin.event_handler: + system_stats["events"] = self.plugin.event_handler.get_event_stats() + + self.realtime_data["system_stats"] = system_stats + + # 触发数据刷新回调 + self._trigger_editor_callback("data_refreshed", { + "data": self.realtime_data, + "timestamp": current_time + }) + + except Exception as e: + print(f"✗ 实时数据更新失败: {e}") + self.editor_stats["editor_errors"] += 1 + + def show_editor(self): + """显示编辑器""" + try: + self.editor_state["is_visible"] = True + print("✓ 匹配编辑器已显示") + + # 刷新数据 + self._update_realtime_data() + + except Exception as e: + print(f"✗ 编辑器显示失败: {e}") + self.editor_stats["editor_errors"] += 1 + + def hide_editor(self): + """隐藏编辑器""" + try: + self.editor_state["is_visible"] = False + print("✓ 匹配编辑器已隐藏") + + except Exception as e: + print(f"✗ 编辑器隐藏失败: {e}") + self.editor_stats["editor_errors"] += 1 + + def switch_tab(self, tab_name: str): + """ + 切换标签页 + + Args: + tab_name: 标签页名称 + """ + try: + if tab_name in self.ui_components["tabs"]: + self.editor_state["active_tab"] = tab_name + print(f"✓ 切换到标签页: {tab_name}") + else: + print(f"✗ 无效的标签页: {tab_name}") + + except Exception as e: + print(f"✗ 标签页切换失败: {e}") + self.editor_stats["editor_errors"] += 1 + + def select_queue(self, queue_name: str): + """ + 选择队列 + + Args: + queue_name: 队列名称 + """ + try: + self.editor_state["selected_queue"] = queue_name + print(f"✓ 选择队列: {queue_name}") + + except Exception as e: + print(f"✗ 队列选择失败: {e}") + self.editor_stats["editor_errors"] += 1 + + def select_algorithm(self, algorithm_name: str): + """ + 选择算法 + + Args: + algorithm_name: 算法名称 + """ + try: + self.editor_state["selected_algorithm"] = algorithm_name + print(f"✓ 选择算法: {algorithm_name}") + + except Exception as e: + print(f"✗ 算法选择失败: {e}") + self.editor_stats["editor_errors"] += 1 + + def update_config(self, section: str, key: str, value: Any) -> bool: + """ + 更新配置 + + Args: + section: 配置节 + key: 配置键 + value: 配置值 + + Returns: + 是否更新成功 + """ + try: + # 通过配置管理器更新配置 + if self.plugin.config_manager: + result = self.plugin.config_manager.set_config(section, key, value) + + if result: + self.editor_stats["updates"] += 1 + + # 触发配置更改回调 + self._trigger_editor_callback("config_changed", { + "section": section, + "key": key, + "value": value, + "timestamp": time.time() + }) + + print(f"✓ 配置已更新: {section}.{key} = {value}") + return True + else: + print(f"✗ 配置更新失败: {section}.{key}") + return False + else: + print("✗ 配置管理器不可用") + return False + + except Exception as e: + print(f"✗ 配置更新失败: {e}") + self.editor_stats["editor_errors"] += 1 + return False + + def get_editor_stats(self) -> Dict[str, Any]: + """ + 获取编辑器统计信息 + + Returns: + 编辑器统计字典 + """ + return { + "state": self.editor_state.copy(), + "stats": self.editor_stats.copy(), + "config": self.editor_config.copy(), + "ui_components": len(self.ui_components["tabs"]) + } + + def reset_stats(self): + """重置编辑器统计信息""" + try: + self.editor_stats = { + "updates": 0, + "ui_refreshes": 0, + "data_points": 0, + "editor_errors": 0 + } + print("✓ 编辑器统计信息已重置") + except Exception as e: + print(f"✗ 编辑器统计信息重置失败: {e}") + + def set_editor_config(self, config: Dict[str, Any]) -> bool: + """ + 设置编辑器配置 + + Args: + config: 编辑器配置字典 + + Returns: + 是否设置成功 + """ + try: + self.editor_config.update(config) + print(f"✓ 编辑器配置已更新: {self.editor_config}") + return True + except Exception as e: + print(f"✗ 编辑器配置设置失败: {e}") + return False + + def get_editor_config(self) -> Dict[str, Any]: + """ + 获取编辑器配置 + + Returns: + 编辑器配置字典 + """ + return self.editor_config.copy() + + def get_realtime_data(self) -> Dict[str, Any]: + """ + 获取实时数据 + + Returns: + 实时数据字典 + """ + return self.realtime_data.copy() + + def export_data(self, data_type: str, file_path: str) -> bool: + """ + 导出数据 + + Args: + data_type: 数据类型 + file_path: 导出文件路径 + + Returns: + 是否导出成功 + """ + try: + import json + import os + + # 创建导出目录 + export_dir = os.path.dirname(file_path) + if not os.path.exists(export_dir): + os.makedirs(export_dir) + + # 获取要导出的数据 + export_data = None + if data_type == "performance": + export_data = self.realtime_data["performance_metrics"] + elif data_type == "queues": + export_data = self.realtime_data["queue_data"] + elif data_type == "matches": + export_data = self.realtime_data["match_data"] + elif data_type == "algorithms": + export_data = self.realtime_data["algorithm_stats"] + elif data_type == "system": + export_data = self.realtime_data["system_stats"] + elif data_type == "all": + export_data = self.realtime_data + else: + print(f"✗ 不支持的数据类型: {data_type}") + return False + + # 导出数据 + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(export_data, f, indent=2, ensure_ascii=False, default=str) + + print(f"✓ 数据已导出到: {file_path}") + return True + + except Exception as e: + print(f"✗ 数据导出失败: {e}") + self.editor_stats["editor_errors"] += 1 + return False + + def _trigger_editor_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发编辑器回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.editor_callbacks: + for callback in self.editor_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 编辑器回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 编辑器回调触发失败: {e}") + + def register_editor_callback(self, callback_type: str, callback: callable): + """ + 注册编辑器回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.editor_callbacks: + self.editor_callbacks[callback_type].append(callback) + print(f"✓ 编辑器回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 编辑器回调注册失败: {e}") + + def unregister_editor_callback(self, callback_type: str, callback: callable): + """ + 注销编辑器回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.editor_callbacks: + if callback in self.editor_callbacks[callback_type]: + self.editor_callbacks[callback_type].remove(callback) + print(f"✓ 编辑器回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 编辑器回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/matchmaking_system/events/event_handler.py b/plugins/user/matchmaking_system/events/event_handler.py index 109bac74..7eb1de46 100644 --- a/plugins/user/matchmaking_system/events/event_handler.py +++ b/plugins/user/matchmaking_system/events/event_handler.py @@ -1,699 +1,699 @@ -""" -事件处理器模块 -处理匹配相关的事件 -""" - -import time -import threading -import queue -from typing import Dict, Any, List, Optional, Callable - -class EventHandler: - """ - 事件处理器 - 处理匹配相关的事件 - """ - - def __init__(self, plugin): - """ - 初始化事件处理器 - - Args: - plugin: 匹配系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 事件配置 - self.event_config = { - "enable_event_system": True, - "max_event_queue_size": 10000, - "enable_event_filtering": True, - "event_processing_interval": 0.01, # 10ms - "enable_async_processing": True, - "max_concurrent_events": 100, - "enable_event_prioritization": True - } - - # 事件状态 - self.event_state = { - "is_processing": False, - "pending_events": 0, - "processed_events": 0, - "dropped_events": 0, - "last_event_time": 0.0 - } - - # 事件队列 - self.event_queue = queue.Queue(maxsize=self.event_config["max_event_queue_size"]) - - # 事件处理器存储 - self.event_handlers = {} - - # 事件统计 - self.event_stats = { - "events_received": 0, - "events_processed": 0, - "events_dropped": 0, - "handler_errors": 0, - "async_events": 0 - } - - # 事件过滤器 - self.event_filters = {} - - # 回调函数 - self.event_callbacks = { - "event_received": [], - "event_processed": [], - "event_dropped": [], - "event_error": [] - } - - # 事件处理线程 - self.event_thread = None - self.event_thread_running = False - - # 时间戳记录 - self.last_event_process = 0.0 - self.last_stats_reset = 0.0 - - print("✓ 事件处理器已创建") - - def initialize(self) -> bool: - """ - 初始化事件处理器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化事件处理器...") - - # 注册默认事件处理器 - self._register_default_handlers() - - # 启动事件处理线程 - self._start_event_thread() - - self.initialized = True - print("✓ 事件处理器初始化完成") - return True - - except Exception as e: - print(f"✗ 事件处理器初始化失败: {e}") - self.event_stats["handler_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def _register_default_handlers(self): - """注册默认事件处理器""" - try: - # 注册系统事件处理器 - self.register_event_handler("player_queued", self._handle_player_queued) - self.register_event_handler("player_dequeued", self._handle_player_dequeued) - self.register_event_handler("match_found", self._handle_match_found) - self.register_event_handler("match_completed", self._handle_match_completed) - self.register_event_handler("match_cancelled", self._handle_match_cancelled) - self.register_event_handler("queue_timeout", self._handle_queue_timeout) - - print("✓ 默认事件处理器已注册") - - except Exception as e: - print(f"✗ 默认事件处理器注册失败: {e}") - self.event_stats["handler_errors"] += 1 - - def _start_event_thread(self): - """启动事件处理线程""" - try: - self.event_thread_running = True - self.event_thread = threading.Thread(target=self._event_loop, daemon=True) - self.event_thread.start() - print("✓ 事件处理线程已启动") - except Exception as e: - print(f"✗ 事件处理线程启动失败: {e}") - self.event_stats["handler_errors"] += 1 - - def _event_loop(self): - """事件处理循环""" - try: - while self.event_thread_running: - try: - if self.enabled and self.event_config["enable_event_system"]: - # 处理事件队列 - self._process_event_queue() - - # 短暂休眠 - time.sleep(self.event_config["event_processing_interval"]) - - except Exception as e: - print(f"✗ 事件处理循环错误: {e}") - self.event_stats["handler_errors"] += 1 - time.sleep(1.0) # 出错时延长休眠 - - except Exception as e: - print(f"✗ 事件处理线程失败: {e}") - self.event_stats["handler_errors"] += 1 - - def enable(self) -> bool: - """ - 启用事件处理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 事件处理器未初始化") - return False - - self.enabled = True - print("✓ 事件处理器已启用") - return True - - except Exception as e: - print(f"✗ 事件处理器启用失败: {e}") - self.event_stats["handler_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用事件处理器""" - try: - self.enabled = False - - # 停止事件处理线程 - if self.event_thread_running: - self.event_thread_running = False - if self.event_thread and self.event_thread.is_alive(): - self.event_thread.join(timeout=5.0) - - print("✓ 事件处理器已禁用") - - except Exception as e: - print(f"✗ 事件处理器禁用失败: {e}") - self.event_stats["handler_errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理事件处理器资源""" - try: - # 禁用事件处理器 - if self.enabled: - self.disable() - - # 清理回调和处理器 - self.event_callbacks.clear() - self.event_handlers.clear() - self.event_filters.clear() - - self.initialized = False - print("✓ 事件处理器资源已清理") - - except Exception as e: - print(f"✗ 事件处理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新事件处理器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.event_state["last_event_time"] = current_time - - except Exception as e: - print(f"✗ 事件处理器更新失败: {e}") - self.event_stats["handler_errors"] += 1 - import traceback - traceback.print_exc() - - def _process_event_queue(self): - """处理事件队列""" - try: - processed_count = 0 - max_process_per_loop = 100 # 每次循环最多处理100个事件 - - while not self.event_queue.empty() and processed_count < max_process_per_loop: - try: - # 从队列获取事件 - event_data = self.event_queue.get_nowait() - - # 处理事件 - self._handle_event_internal(event_data) - - processed_count += 1 - self.event_stats["events_processed"] += 1 - - except queue.Empty: - break - except Exception as e: - print(f"✗ 事件队列处理失败: {e}") - self.event_stats["handler_errors"] += 1 - - self.event_state["pending_events"] = self.event_queue.qsize() - - except Exception as e: - print(f"✗ 事件队列处理失败: {e}") - self.event_stats["handler_errors"] += 1 - - def _handle_event_internal(self, event_data: Dict[str, Any]): - """ - 内部事件处理 - - Args: - event_data: 事件数据 - """ - try: - event_type = event_data.get("type") - event_payload = event_data.get("payload", {}) - event_priority = event_data.get("priority", 0) - event_timestamp = event_data.get("timestamp", time.time()) - - # 应用事件过滤器 - if self.event_config["enable_event_filtering"]: - if not self._apply_event_filters(event_type, event_payload): - # 事件被过滤掉 - return - - # 查找事件处理器 - if event_type in self.event_handlers: - handlers = self.event_handlers[event_type] - - # 按优先级排序处理器 - if self.event_config["enable_event_prioritization"]: - handlers = sorted(handlers, key=lambda x: x.get("priority", 0), reverse=True) - - # 调用所有处理器 - for handler_info in handlers: - try: - handler_func = handler_info["function"] - handler_result = handler_func(event_payload) - - # 检查是否需要停止传播 - if handler_result is False: - break - - except Exception as e: - print(f"✗ 事件处理器执行失败: {event_type} - {e}") - self.event_stats["handler_errors"] += 1 - - # 触发事件处理回调 - self._trigger_event_callback("event_processed", { - "event_type": event_type, - "event_payload": event_payload, - "processing_time": time.time() - event_timestamp, - "timestamp": time.time() - }) - - except Exception as e: - print(f"✗ 内部事件处理失败: {e}") - self.event_stats["handler_errors"] += 1 - - def _apply_event_filters(self, event_type: str, event_payload: Dict[str, Any]) -> bool: - """ - 应用事件过滤器 - - Args: - event_type: 事件类型 - event_payload: 事件载荷 - - Returns: - 是否通过过滤器 - """ - try: - if event_type in self.event_filters: - filters = self.event_filters[event_type] - for filter_func in filters: - if not filter_func(event_payload): - return False - - return True - - except Exception as e: - print(f"✗ 事件过滤器应用失败: {e}") - self.event_stats["handler_errors"] += 1 - return True - - def emit_event(self, event_type: str, event_payload: Dict[str, Any] = None, - priority: int = 0, async_process: bool = None) -> bool: - """ - 发出事件 - - Args: - event_type: 事件类型 - event_payload: 事件载荷 - priority: 事件优先级 - async_process: 是否异步处理 - - Returns: - 是否发出成功 - """ - try: - if not self.enabled or not self.event_config["enable_event_system"]: - return False - - # 使用默认异步处理设置或指定设置 - if async_process is None: - async_process = self.event_config["enable_async_processing"] - - event_data = { - "type": event_type, - "payload": event_payload or {}, - "priority": priority, - "timestamp": time.time(), - "async": async_process - } - - # 触发事件接收回调 - self._trigger_event_callback("event_received", { - "event_type": event_type, - "event_payload": event_payload, - "priority": priority, - "timestamp": time.time() - }) - - # 更新统计 - self.event_stats["events_received"] += 1 - - if async_process: - # 异步处理:添加到队列 - try: - self.event_queue.put_nowait(event_data) - self.event_stats["async_events"] += 1 - return True - except queue.Full: - # 队列已满,丢弃事件 - self.event_stats["events_dropped"] += 1 - self.event_state["dropped_events"] += 1 - - # 触发事件丢弃回调 - self._trigger_event_callback("event_dropped", { - "event_type": event_type, - "event_payload": event_payload, - "reason": "queue_full", - "timestamp": time.time() - }) - - return False - else: - # 同步处理:立即处理 - self._handle_event_internal(event_data) - return True - - except Exception as e: - print(f"✗ 事件发出失败: {e}") - self.event_stats["handler_errors"] += 1 - return False - - def register_event_handler(self, event_type: str, handler: Callable, priority: int = 0): - """ - 注册事件处理器 - - Args: - event_type: 事件类型 - handler: 处理器函数 - priority: 处理器优先级 - """ - try: - if event_type not in self.event_handlers: - self.event_handlers[event_type] = [] - - handler_info = { - "function": handler, - "priority": priority - } - - self.event_handlers[event_type].append(handler_info) - print(f"✓ 事件处理器已注册: {event_type} (优先级: {priority})") - - except Exception as e: - print(f"✗ 事件处理器注册失败: {e}") - self.event_stats["handler_errors"] += 1 - - def unregister_event_handler(self, event_type: str, handler: Callable): - """ - 注销事件处理器 - - Args: - event_type: 事件类型 - handler: 处理器函数 - """ - try: - if event_type in self.event_handlers: - handlers = self.event_handlers[event_type] - for i, handler_info in enumerate(handlers): - if handler_info["function"] == handler: - del handlers[i] - print(f"✓ 事件处理器已注销: {event_type}") - return - - print(f"✗ 事件处理器不存在: {event_type}") - - except Exception as e: - print(f"✗ 事件处理器注销失败: {e}") - self.event_stats["handler_errors"] += 1 - - def register_event_filter(self, event_type: str, filter_func: Callable): - """ - 注册事件过滤器 - - Args: - event_type: 事件类型 - filter_func: 过滤器函数 - """ - try: - if event_type not in self.event_filters: - self.event_filters[event_type] = [] - - self.event_filters[event_type].append(filter_func) - print(f"✓ 事件过滤器已注册: {event_type}") - - except Exception as e: - print(f"✗ 事件过滤器注册失败: {e}") - self.event_stats["handler_errors"] += 1 - - def unregister_event_filter(self, event_type: str, filter_func: Callable): - """ - 注销事件过滤器 - - Args: - event_type: 事件类型 - filter_func: 过滤器函数 - """ - try: - if event_type in self.event_filters: - filters = self.event_filters[event_type] - if filter_func in filters: - filters.remove(filter_func) - print(f"✓ 事件过滤器已注销: {event_type}") - return - - print(f"✗ 事件过滤器不存在: {event_type}") - - except Exception as e: - print(f"✗ 事件过滤器注销失败: {e}") - self.event_stats["handler_errors"] += 1 - - # 默认事件处理器 - def _handle_player_queued(self, payload: Dict[str, Any]): - """处理玩家排队事件""" - try: - player_ids = payload.get("player_ids", []) - queue_name = payload.get("queue_name", "default") - - print(f".players queued: {len(player_ids)} players in queue '{queue_name}'") - - # 可以在这里添加额外的处理逻辑 - # 例如:记录排队时间、更新统计等 - - except Exception as e: - print(f"✗ 玩家排队事件处理失败: {e}") - self.event_stats["handler_errors"] += 1 - - def _handle_player_dequeued(self, payload: Dict[str, Any]): - """处理玩家离队事件""" - try: - player_ids = payload.get("player_ids", []) - match_id = payload.get("match_id") - - print(f".players dequeued: {len(player_ids)} players") - - # 可以在这里添加额外的处理逻辑 - # 例如:更新统计、清理资源等 - - except Exception as e: - print(f"✗ 玩家离队事件处理失败: {e}") - self.event_stats["handler_errors"] += 1 - - def _handle_match_found(self, payload: Dict[str, Any]): - """处理匹配找到事件""" - try: - match_id = payload.get("match_id") - room_id = payload.get("room_id") - player_ids = payload.get("player_ids", []) - queue_name = payload.get("queue_name", "default") - - print(f"🎉 Match found: {match_id} with {len(player_ids)} players in queue '{queue_name}' -> Room: {room_id}") - - # 记录匹配队列时间 - if self.plugin.monitor: - queue_time = time.time() - payload.get("timestamp", time.time()) - self.plugin.monitor.record_match_queue_time(queue_time) - - except Exception as e: - print(f"✗ 匹配找到事件处理失败: {e}") - self.event_stats["handler_errors"] += 1 - - def _handle_match_completed(self, payload: Dict[str, Any]): - """处理匹配完成事件""" - try: - match_id = payload.get("match_id") - room_id = payload.get("room_id") - - print(f"✅ Match completed: {match_id} -> Room: {room_id}") - - except Exception as e: - print(f"✗ 匹配完成事件处理失败: {e}") - self.event_stats["handler_errors"] += 1 - - def _handle_match_cancelled(self, payload: Dict[str, Any]): - """处理匹配取消事件""" - try: - match_id = payload.get("match_id") - - print(f"❌ Match cancelled: {match_id}") - - except Exception as e: - print(f"✗ 匹配取消事件处理失败: {e}") - self.event_stats["handler_errors"] += 1 - - def _handle_queue_timeout(self, payload: Dict[str, Any]): - """处理队列超时事件""" - try: - player_id = payload.get("player_id") - - print(f"⏰ Queue timeout for player: {player_id}") - - except Exception as e: - print(f"✗ 队列超时事件处理失败: {e}") - self.event_stats["handler_errors"] += 1 - - def get_event_stats(self) -> Dict[str, Any]: - """ - 获取事件统计信息 - - Returns: - 事件统计字典 - """ - return { - "state": self.event_state.copy(), - "stats": self.event_stats.copy(), - "config": self.event_config.copy(), - "pending_events": self.event_queue.qsize() - } - - def reset_stats(self): - """重置事件统计信息""" - try: - self.event_stats = { - "events_received": 0, - "events_processed": 0, - "events_dropped": 0, - "handler_errors": 0, - "async_events": 0 - } - print("✓ 事件统计信息已重置") - except Exception as e: - print(f"✗ 事件统计信息重置失败: {e}") - - def set_event_config(self, config: Dict[str, Any]) -> bool: - """ - 设置事件配置 - - Args: - config: 事件配置字典 - - Returns: - 是否设置成功 - """ - try: - self.event_config.update(config) - print(f"✓ 事件配置已更新: {self.event_config}") - return True - except Exception as e: - print(f"✗ 事件配置设置失败: {e}") - return False - - def get_event_config(self) -> Dict[str, Any]: - """ - 获取事件配置 - - Returns: - 事件配置字典 - """ - return self.event_config.copy() - - def _trigger_event_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发事件回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.event_callbacks: - for callback in self.event_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 事件回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 事件回调触发失败: {e}") - - def register_event_callback(self, callback_type: str, callback: callable): - """ - 注册事件回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.event_callbacks: - self.event_callbacks[callback_type].append(callback) - print(f"✓ 事件回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 事件回调注册失败: {e}") - - def unregister_event_callback(self, callback_type: str, callback: callable): - """ - 注销事件回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.event_callbacks: - if callback in self.event_callbacks[callback_type]: - self.event_callbacks[callback_type].remove(callback) - print(f"✓ 事件回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +事件处理器模块 +处理匹配相关的事件 +""" + +import time +import threading +import queue +from typing import Dict, Any, List, Optional, Callable + +class EventHandler: + """ + 事件处理器 + 处理匹配相关的事件 + """ + + def __init__(self, plugin): + """ + 初始化事件处理器 + + Args: + plugin: 匹配系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 事件配置 + self.event_config = { + "enable_event_system": True, + "max_event_queue_size": 10000, + "enable_event_filtering": True, + "event_processing_interval": 0.01, # 10ms + "enable_async_processing": True, + "max_concurrent_events": 100, + "enable_event_prioritization": True + } + + # 事件状态 + self.event_state = { + "is_processing": False, + "pending_events": 0, + "processed_events": 0, + "dropped_events": 0, + "last_event_time": 0.0 + } + + # 事件队列 + self.event_queue = queue.Queue(maxsize=self.event_config["max_event_queue_size"]) + + # 事件处理器存储 + self.event_handlers = {} + + # 事件统计 + self.event_stats = { + "events_received": 0, + "events_processed": 0, + "events_dropped": 0, + "handler_errors": 0, + "async_events": 0 + } + + # 事件过滤器 + self.event_filters = {} + + # 回调函数 + self.event_callbacks = { + "event_received": [], + "event_processed": [], + "event_dropped": [], + "event_error": [] + } + + # 事件处理线程 + self.event_thread = None + self.event_thread_running = False + + # 时间戳记录 + self.last_event_process = 0.0 + self.last_stats_reset = 0.0 + + print("✓ 事件处理器已创建") + + def initialize(self) -> bool: + """ + 初始化事件处理器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化事件处理器...") + + # 注册默认事件处理器 + self._register_default_handlers() + + # 启动事件处理线程 + self._start_event_thread() + + self.initialized = True + print("✓ 事件处理器初始化完成") + return True + + except Exception as e: + print(f"✗ 事件处理器初始化失败: {e}") + self.event_stats["handler_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def _register_default_handlers(self): + """注册默认事件处理器""" + try: + # 注册系统事件处理器 + self.register_event_handler("player_queued", self._handle_player_queued) + self.register_event_handler("player_dequeued", self._handle_player_dequeued) + self.register_event_handler("match_found", self._handle_match_found) + self.register_event_handler("match_completed", self._handle_match_completed) + self.register_event_handler("match_cancelled", self._handle_match_cancelled) + self.register_event_handler("queue_timeout", self._handle_queue_timeout) + + print("✓ 默认事件处理器已注册") + + except Exception as e: + print(f"✗ 默认事件处理器注册失败: {e}") + self.event_stats["handler_errors"] += 1 + + def _start_event_thread(self): + """启动事件处理线程""" + try: + self.event_thread_running = True + self.event_thread = threading.Thread(target=self._event_loop, daemon=True) + self.event_thread.start() + print("✓ 事件处理线程已启动") + except Exception as e: + print(f"✗ 事件处理线程启动失败: {e}") + self.event_stats["handler_errors"] += 1 + + def _event_loop(self): + """事件处理循环""" + try: + while self.event_thread_running: + try: + if self.enabled and self.event_config["enable_event_system"]: + # 处理事件队列 + self._process_event_queue() + + # 短暂休眠 + time.sleep(self.event_config["event_processing_interval"]) + + except Exception as e: + print(f"✗ 事件处理循环错误: {e}") + self.event_stats["handler_errors"] += 1 + time.sleep(1.0) # 出错时延长休眠 + + except Exception as e: + print(f"✗ 事件处理线程失败: {e}") + self.event_stats["handler_errors"] += 1 + + def enable(self) -> bool: + """ + 启用事件处理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 事件处理器未初始化") + return False + + self.enabled = True + print("✓ 事件处理器已启用") + return True + + except Exception as e: + print(f"✗ 事件处理器启用失败: {e}") + self.event_stats["handler_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用事件处理器""" + try: + self.enabled = False + + # 停止事件处理线程 + if self.event_thread_running: + self.event_thread_running = False + if self.event_thread and self.event_thread.is_alive(): + self.event_thread.join(timeout=5.0) + + print("✓ 事件处理器已禁用") + + except Exception as e: + print(f"✗ 事件处理器禁用失败: {e}") + self.event_stats["handler_errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理事件处理器资源""" + try: + # 禁用事件处理器 + if self.enabled: + self.disable() + + # 清理回调和处理器 + self.event_callbacks.clear() + self.event_handlers.clear() + self.event_filters.clear() + + self.initialized = False + print("✓ 事件处理器资源已清理") + + except Exception as e: + print(f"✗ 事件处理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新事件处理器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.event_state["last_event_time"] = current_time + + except Exception as e: + print(f"✗ 事件处理器更新失败: {e}") + self.event_stats["handler_errors"] += 1 + import traceback + traceback.print_exc() + + def _process_event_queue(self): + """处理事件队列""" + try: + processed_count = 0 + max_process_per_loop = 100 # 每次循环最多处理100个事件 + + while not self.event_queue.empty() and processed_count < max_process_per_loop: + try: + # 从队列获取事件 + event_data = self.event_queue.get_nowait() + + # 处理事件 + self._handle_event_internal(event_data) + + processed_count += 1 + self.event_stats["events_processed"] += 1 + + except queue.Empty: + break + except Exception as e: + print(f"✗ 事件队列处理失败: {e}") + self.event_stats["handler_errors"] += 1 + + self.event_state["pending_events"] = self.event_queue.qsize() + + except Exception as e: + print(f"✗ 事件队列处理失败: {e}") + self.event_stats["handler_errors"] += 1 + + def _handle_event_internal(self, event_data: Dict[str, Any]): + """ + 内部事件处理 + + Args: + event_data: 事件数据 + """ + try: + event_type = event_data.get("type") + event_payload = event_data.get("payload", {}) + event_priority = event_data.get("priority", 0) + event_timestamp = event_data.get("timestamp", time.time()) + + # 应用事件过滤器 + if self.event_config["enable_event_filtering"]: + if not self._apply_event_filters(event_type, event_payload): + # 事件被过滤掉 + return + + # 查找事件处理器 + if event_type in self.event_handlers: + handlers = self.event_handlers[event_type] + + # 按优先级排序处理器 + if self.event_config["enable_event_prioritization"]: + handlers = sorted(handlers, key=lambda x: x.get("priority", 0), reverse=True) + + # 调用所有处理器 + for handler_info in handlers: + try: + handler_func = handler_info["function"] + handler_result = handler_func(event_payload) + + # 检查是否需要停止传播 + if handler_result is False: + break + + except Exception as e: + print(f"✗ 事件处理器执行失败: {event_type} - {e}") + self.event_stats["handler_errors"] += 1 + + # 触发事件处理回调 + self._trigger_event_callback("event_processed", { + "event_type": event_type, + "event_payload": event_payload, + "processing_time": time.time() - event_timestamp, + "timestamp": time.time() + }) + + except Exception as e: + print(f"✗ 内部事件处理失败: {e}") + self.event_stats["handler_errors"] += 1 + + def _apply_event_filters(self, event_type: str, event_payload: Dict[str, Any]) -> bool: + """ + 应用事件过滤器 + + Args: + event_type: 事件类型 + event_payload: 事件载荷 + + Returns: + 是否通过过滤器 + """ + try: + if event_type in self.event_filters: + filters = self.event_filters[event_type] + for filter_func in filters: + if not filter_func(event_payload): + return False + + return True + + except Exception as e: + print(f"✗ 事件过滤器应用失败: {e}") + self.event_stats["handler_errors"] += 1 + return True + + def emit_event(self, event_type: str, event_payload: Dict[str, Any] = None, + priority: int = 0, async_process: bool = None) -> bool: + """ + 发出事件 + + Args: + event_type: 事件类型 + event_payload: 事件载荷 + priority: 事件优先级 + async_process: 是否异步处理 + + Returns: + 是否发出成功 + """ + try: + if not self.enabled or not self.event_config["enable_event_system"]: + return False + + # 使用默认异步处理设置或指定设置 + if async_process is None: + async_process = self.event_config["enable_async_processing"] + + event_data = { + "type": event_type, + "payload": event_payload or {}, + "priority": priority, + "timestamp": time.time(), + "async": async_process + } + + # 触发事件接收回调 + self._trigger_event_callback("event_received", { + "event_type": event_type, + "event_payload": event_payload, + "priority": priority, + "timestamp": time.time() + }) + + # 更新统计 + self.event_stats["events_received"] += 1 + + if async_process: + # 异步处理:添加到队列 + try: + self.event_queue.put_nowait(event_data) + self.event_stats["async_events"] += 1 + return True + except queue.Full: + # 队列已满,丢弃事件 + self.event_stats["events_dropped"] += 1 + self.event_state["dropped_events"] += 1 + + # 触发事件丢弃回调 + self._trigger_event_callback("event_dropped", { + "event_type": event_type, + "event_payload": event_payload, + "reason": "queue_full", + "timestamp": time.time() + }) + + return False + else: + # 同步处理:立即处理 + self._handle_event_internal(event_data) + return True + + except Exception as e: + print(f"✗ 事件发出失败: {e}") + self.event_stats["handler_errors"] += 1 + return False + + def register_event_handler(self, event_type: str, handler: Callable, priority: int = 0): + """ + 注册事件处理器 + + Args: + event_type: 事件类型 + handler: 处理器函数 + priority: 处理器优先级 + """ + try: + if event_type not in self.event_handlers: + self.event_handlers[event_type] = [] + + handler_info = { + "function": handler, + "priority": priority + } + + self.event_handlers[event_type].append(handler_info) + print(f"✓ 事件处理器已注册: {event_type} (优先级: {priority})") + + except Exception as e: + print(f"✗ 事件处理器注册失败: {e}") + self.event_stats["handler_errors"] += 1 + + def unregister_event_handler(self, event_type: str, handler: Callable): + """ + 注销事件处理器 + + Args: + event_type: 事件类型 + handler: 处理器函数 + """ + try: + if event_type in self.event_handlers: + handlers = self.event_handlers[event_type] + for i, handler_info in enumerate(handlers): + if handler_info["function"] == handler: + del handlers[i] + print(f"✓ 事件处理器已注销: {event_type}") + return + + print(f"✗ 事件处理器不存在: {event_type}") + + except Exception as e: + print(f"✗ 事件处理器注销失败: {e}") + self.event_stats["handler_errors"] += 1 + + def register_event_filter(self, event_type: str, filter_func: Callable): + """ + 注册事件过滤器 + + Args: + event_type: 事件类型 + filter_func: 过滤器函数 + """ + try: + if event_type not in self.event_filters: + self.event_filters[event_type] = [] + + self.event_filters[event_type].append(filter_func) + print(f"✓ 事件过滤器已注册: {event_type}") + + except Exception as e: + print(f"✗ 事件过滤器注册失败: {e}") + self.event_stats["handler_errors"] += 1 + + def unregister_event_filter(self, event_type: str, filter_func: Callable): + """ + 注销事件过滤器 + + Args: + event_type: 事件类型 + filter_func: 过滤器函数 + """ + try: + if event_type in self.event_filters: + filters = self.event_filters[event_type] + if filter_func in filters: + filters.remove(filter_func) + print(f"✓ 事件过滤器已注销: {event_type}") + return + + print(f"✗ 事件过滤器不存在: {event_type}") + + except Exception as e: + print(f"✗ 事件过滤器注销失败: {e}") + self.event_stats["handler_errors"] += 1 + + # 默认事件处理器 + def _handle_player_queued(self, payload: Dict[str, Any]): + """处理玩家排队事件""" + try: + player_ids = payload.get("player_ids", []) + queue_name = payload.get("queue_name", "default") + + print(f".players queued: {len(player_ids)} players in queue '{queue_name}'") + + # 可以在这里添加额外的处理逻辑 + # 例如:记录排队时间、更新统计等 + + except Exception as e: + print(f"✗ 玩家排队事件处理失败: {e}") + self.event_stats["handler_errors"] += 1 + + def _handle_player_dequeued(self, payload: Dict[str, Any]): + """处理玩家离队事件""" + try: + player_ids = payload.get("player_ids", []) + match_id = payload.get("match_id") + + print(f".players dequeued: {len(player_ids)} players") + + # 可以在这里添加额外的处理逻辑 + # 例如:更新统计、清理资源等 + + except Exception as e: + print(f"✗ 玩家离队事件处理失败: {e}") + self.event_stats["handler_errors"] += 1 + + def _handle_match_found(self, payload: Dict[str, Any]): + """处理匹配找到事件""" + try: + match_id = payload.get("match_id") + room_id = payload.get("room_id") + player_ids = payload.get("player_ids", []) + queue_name = payload.get("queue_name", "default") + + print(f"🎉 Match found: {match_id} with {len(player_ids)} players in queue '{queue_name}' -> Room: {room_id}") + + # 记录匹配队列时间 + if self.plugin.monitor: + queue_time = time.time() - payload.get("timestamp", time.time()) + self.plugin.monitor.record_match_queue_time(queue_time) + + except Exception as e: + print(f"✗ 匹配找到事件处理失败: {e}") + self.event_stats["handler_errors"] += 1 + + def _handle_match_completed(self, payload: Dict[str, Any]): + """处理匹配完成事件""" + try: + match_id = payload.get("match_id") + room_id = payload.get("room_id") + + print(f"✅ Match completed: {match_id} -> Room: {room_id}") + + except Exception as e: + print(f"✗ 匹配完成事件处理失败: {e}") + self.event_stats["handler_errors"] += 1 + + def _handle_match_cancelled(self, payload: Dict[str, Any]): + """处理匹配取消事件""" + try: + match_id = payload.get("match_id") + + print(f"❌ Match cancelled: {match_id}") + + except Exception as e: + print(f"✗ 匹配取消事件处理失败: {e}") + self.event_stats["handler_errors"] += 1 + + def _handle_queue_timeout(self, payload: Dict[str, Any]): + """处理队列超时事件""" + try: + player_id = payload.get("player_id") + + print(f"⏰ Queue timeout for player: {player_id}") + + except Exception as e: + print(f"✗ 队列超时事件处理失败: {e}") + self.event_stats["handler_errors"] += 1 + + def get_event_stats(self) -> Dict[str, Any]: + """ + 获取事件统计信息 + + Returns: + 事件统计字典 + """ + return { + "state": self.event_state.copy(), + "stats": self.event_stats.copy(), + "config": self.event_config.copy(), + "pending_events": self.event_queue.qsize() + } + + def reset_stats(self): + """重置事件统计信息""" + try: + self.event_stats = { + "events_received": 0, + "events_processed": 0, + "events_dropped": 0, + "handler_errors": 0, + "async_events": 0 + } + print("✓ 事件统计信息已重置") + except Exception as e: + print(f"✗ 事件统计信息重置失败: {e}") + + def set_event_config(self, config: Dict[str, Any]) -> bool: + """ + 设置事件配置 + + Args: + config: 事件配置字典 + + Returns: + 是否设置成功 + """ + try: + self.event_config.update(config) + print(f"✓ 事件配置已更新: {self.event_config}") + return True + except Exception as e: + print(f"✗ 事件配置设置失败: {e}") + return False + + def get_event_config(self) -> Dict[str, Any]: + """ + 获取事件配置 + + Returns: + 事件配置字典 + """ + return self.event_config.copy() + + def _trigger_event_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发事件回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.event_callbacks: + for callback in self.event_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 事件回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 事件回调触发失败: {e}") + + def register_event_callback(self, callback_type: str, callback: callable): + """ + 注册事件回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.event_callbacks: + self.event_callbacks[callback_type].append(callback) + print(f"✓ 事件回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 事件回调注册失败: {e}") + + def unregister_event_callback(self, callback_type: str, callback: callable): + """ + 注销事件回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.event_callbacks: + if callback in self.event_callbacks[callback_type]: + self.event_callbacks[callback_type].remove(callback) + print(f"✓ 事件回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 事件回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/matchmaking_system/monitoring/match_monitor.py b/plugins/user/matchmaking_system/monitoring/match_monitor.py index 602603a9..ecdb0018 100644 --- a/plugins/user/matchmaking_system/monitoring/match_monitor.py +++ b/plugins/user/matchmaking_system/monitoring/match_monitor.py @@ -1,600 +1,600 @@ -""" -匹配监控系统模块 -监控匹配过程和性能 -""" - -import time -from typing import Dict, Any, List, Optional -from collections import deque - -class MatchMonitor: - """ - 匹配监控器 - 监控匹配过程和性能 - """ - - def __init__(self, plugin): - """ - 初始化匹配监控器 - - Args: - plugin: 匹配系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 监控配置 - self.monitor_config = { - "enable_monitoring": True, - "monitoring_interval": 5.0, - "log_level": "INFO", - "enable_performance_monitoring": True, - "enable_alerts": True, - "alert_thresholds": { - "match_queue_time": 60.0, - "match_failure_rate": 10.0, - "system_latency": 100.0 - }, - "enable_metrics_collection": True, - "metrics_retention_period": 3600 # 1小时 - } - - # 性能指标存储 - self.performance_metrics = { - "match_queue_times": deque(maxlen=1000), - "match_success_rates": deque(maxlen=1000), - "system_latencies": deque(maxlen=1000), - "player_wait_times": deque(maxlen=1000), - "algorithm_performance": deque(maxlen=1000) - } - - # 监控状态 - self.monitor_state = { - "last_monitoring_update": 0.0, - "last_performance_check": 0.0, - "total_monitored_matches": 0, - "active_alerts": 0 - } - - # 警报系统 - self.active_alerts = {} - self.alert_history = deque(maxlen=1000) - - # 监控统计 - self.monitor_stats = { - "metrics_collected": 0, - "alerts_generated": 0, - "alerts_resolved": 0, - "performance_checks": 0, - "monitor_errors": 0 - } - - # 回调函数 - self.monitor_callbacks = { - "metric_collected": [], - "alert_triggered": [], - "alert_resolved": [], - "performance_degraded": [] - } - - # 时间戳记录 - self.last_metric_collection = 0.0 - self.last_alert_check = 0.0 - - print("✓ 匹配监控器已创建") - - def initialize(self) -> bool: - """ - 初始化匹配监控器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化匹配监控器...") - - self.initialized = True - print("✓ 匹配监控器初始化完成") - return True - - except Exception as e: - print(f"✗ 匹配监控器初始化失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用匹配监控器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 匹配监控器未初始化") - return False - - self.enabled = True - print("✓ 匹配监控器已启用") - return True - - except Exception as e: - print(f"✗ 匹配监控器启用失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用匹配监控器""" - try: - self.enabled = False - print("✓ 匹配监控器已禁用") - - except Exception as e: - print(f"✗ 匹配监控器禁用失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理匹配监控器资源""" - try: - # 禁用匹配监控器 - if self.enabled: - self.disable() - - # 清理回调 - self.monitor_callbacks.clear() - - self.initialized = False - print("✓ 匹配监控器资源已清理") - - except Exception as e: - print(f"✗ 匹配监控器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新匹配监控器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.monitor_state["last_monitoring_update"] = current_time - - # 定期收集性能指标 - if current_time - self.last_metric_collection >= self.monitor_config["monitoring_interval"]: - self._collect_performance_metrics() - self.last_metric_collection = current_time - - # 定期检查警报 - if current_time - self.last_alert_check >= self.monitor_config["monitoring_interval"]: - self._check_alerts() - self.last_alert_check = current_time - - except Exception as e: - print(f"✗ 匹配监控器更新失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - import traceback - traceback.print_exc() - - def _collect_performance_metrics(self): - """收集性能指标""" - try: - current_time = time.time() - metrics = {} - - # 收集匹配管理器指标 - if self.plugin.match_manager: - match_stats = self.plugin.match_manager.get_match_stats() - match_state = match_stats.get("state", {}) - - metrics["active_matches"] = match_state.get("active_matches", 0) - metrics["successful_matches"] = match_state.get("successful_matches", 0) - metrics["failed_matches"] = match_state.get("failed_matches", 0) - - # 计算成功率 - total_matches = metrics["successful_matches"] + metrics["failed_matches"] - if total_matches > 0: - success_rate = (metrics["successful_matches"] / total_matches) * 100 - metrics["match_success_rate"] = success_rate - self.performance_metrics["match_success_rates"].append((current_time, success_rate)) - - # 收集队列管理器指标 - if self.plugin.queue_manager: - queue_stats = self.plugin.queue_manager.get_queue_stats() - queue_state = queue_stats.get("state", {}) - - metrics["total_players"] = queue_state.get("total_players", 0) - metrics["players_matched"] = queue_state.get("players_matched", 0) - metrics["players_timed_out"] = queue_state.get("players_timed_out", 0) - - # 计算超时率 - total_players = metrics["players_matched"] + metrics["players_timed_out"] - if total_players > 0: - timeout_rate = (metrics["players_timed_out"] / total_players) * 100 - metrics["queue_timeout_rate"] = timeout_rate - - # 收集算法管理器指标 - if self.plugin.algorithm_manager: - algorithm_stats = self.plugin.algorithm_manager.get_algorithm_stats() - algorithm_state = algorithm_stats.get("state", {}) - - metrics["total_algorithms"] = algorithm_state.get("total_matches", 0) - metrics["successful_algorithms"] = algorithm_state.get("successful_matches", 0) - metrics["average_match_quality"] = algorithm_state.get("average_match_quality", 0) - - self.performance_metrics["algorithm_performance"].append( - (current_time, metrics["average_match_quality"]) - ) - - # 收集房间分配器指标 - if self.plugin.room_allocator: - room_stats = self.plugin.room_allocator.get_room_stats() - room_state = room_stats.get("state", {}) - - metrics["active_rooms"] = room_state.get("active_rooms", 0) - metrics["total_rooms"] = room_state.get("total_rooms", 0) - - # 更新统计 - self.monitor_stats["metrics_collected"] += 1 - self.monitor_state["total_monitored_matches"] += 1 - - # 触发指标收集回调 - self._trigger_monitor_callback("metric_collected", { - "metrics": metrics, - "timestamp": current_time - }) - - except Exception as e: - print(f"✗ 性能指标收集失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - - def _check_alerts(self): - """检查警报条件""" - try: - if not self.monitor_config["enable_alerts"]: - return - - current_time = time.time() - thresholds = self.monitor_config["alert_thresholds"] - - # 检查匹配队列时间 - if self.performance_metrics["match_queue_times"]: - latest_queue_time = self.performance_metrics["match_queue_times"][-1][1] - if latest_queue_time > thresholds["match_queue_time"]: - self._trigger_alert("high_queue_time", f"匹配队列时间过长: {latest_queue_time:.2f}秒", current_time) - - # 检查匹配失败率 - if self.performance_metrics["match_success_rates"]: - latest_success_rate = self.performance_metrics["match_success_rates"][-1][1] - failure_rate = 100 - latest_success_rate - if failure_rate > thresholds["match_failure_rate"]: - self._trigger_alert("high_failure_rate", f"匹配失败率过高: {failure_rate:.2f}%", current_time) - - # 检查系统延迟 - if self.performance_metrics["system_latencies"]: - latest_latency = self.performance_metrics["system_latencies"][-1][1] - if latest_latency > thresholds["system_latency"]: - self._trigger_alert("high_latency", f"系统延迟过高: {latest_latency:.2f}ms", current_time) - - except Exception as e: - print(f"✗ 警报检查失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - - def _trigger_alert(self, alert_type: str, message: str, timestamp: float = None): - """ - 触发警报 - - Args: - alert_type: 警报类型 - message: 警报消息 - timestamp: 时间戳 - """ - try: - if timestamp is None: - timestamp = time.time() - - alert_id = f"{alert_type}_{int(timestamp)}" - - # 检查是否已存在相同类型的活动警报 - if alert_type in self.active_alerts: - # 更新现有警报 - self.active_alerts[alert_type]["count"] += 1 - self.active_alerts[alert_type]["last_triggered"] = timestamp - else: - # 创建新警报 - self.active_alerts[alert_type] = { - "id": alert_id, - "type": alert_type, - "message": message, - "first_triggered": timestamp, - "last_triggered": timestamp, - "count": 1, - "resolved": False - } - - self.monitor_state["active_alerts"] += 1 - self.monitor_stats["alerts_generated"] += 1 - - # 添加到警报历史 - self.alert_history.append({ - "id": alert_id, - "type": alert_type, - "message": message, - "timestamp": timestamp, - "resolved": False - }) - - # 触发警报回调 - self._trigger_monitor_callback("alert_triggered", { - "alert_id": alert_id, - "alert_type": alert_type, - "message": message, - "timestamp": timestamp - }) - - print(f"⚠️ 警报触发 [{alert_type}]: {message}") - - except Exception as e: - print(f"✗ 警报触发失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - - def resolve_alert(self, alert_type: str): - """ - 解决警报 - - Args: - alert_type: 警报类型 - """ - try: - if alert_type in self.active_alerts: - alert_data = self.active_alerts[alert_type] - alert_data["resolved"] = True - alert_data["resolved_time"] = time.time() - - self.monitor_state["active_alerts"] -= 1 - self.monitor_stats["alerts_resolved"] += 1 - - # 触发警报解决回调 - self._trigger_monitor_callback("alert_resolved", { - "alert_type": alert_type, - "alert_data": alert_data - }) - - # 从活动警报中移除 - del self.active_alerts[alert_type] - - print(f"✅ 警报已解决: {alert_type}") - - except Exception as e: - print(f"✗ 警报解决失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - - def record_match_queue_time(self, queue_time: float): - """ - 记录匹配队列时间 - - Args: - queue_time: 队列时间(秒) - """ - try: - current_time = time.time() - self.performance_metrics["match_queue_times"].append((current_time, queue_time)) - except Exception as e: - print(f"✗ 匹配队列时间记录失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - - def record_player_wait_time(self, wait_time: float): - """ - 记录玩家等待时间 - - Args: - wait_time: 等待时间(秒) - """ - try: - current_time = time.time() - self.performance_metrics["player_wait_times"].append((current_time, wait_time)) - except Exception as e: - print(f"✗ 玩家等待时间记录失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - - def record_system_latency(self, latency: float): - """ - 记录系统延迟 - - Args: - latency: 延迟(毫秒) - """ - try: - current_time = time.time() - self.performance_metrics["system_latencies"].append((current_time, latency)) - except Exception as e: - print(f"✗ 系统延迟记录失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - - def get_performance_metrics(self, metric_name: str = None, limit: int = 100) -> Any: - """ - 获取性能指标 - - Args: - metric_name: 指标名称(可选) - limit: 限制返回的数量 - - Returns: - 性能指标数据 - """ - try: - if metric_name: - if metric_name in self.performance_metrics: - metrics = list(self.performance_metrics[metric_name]) - return metrics[-limit:] if len(metrics) > limit else metrics - else: - return [] - else: - # 返回所有指标 - result = {} - for name, metrics in self.performance_metrics.items(): - metric_list = list(metrics) - result[name] = metric_list[-limit:] if len(metric_list) > limit else metric_list - return result - - except Exception as e: - print(f"✗ 获取性能指标失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - return {} - - def get_active_alerts(self) -> Dict[str, Any]: - """ - 获取活动警报 - - Returns: - 活动警报字典 - """ - try: - return self.active_alerts.copy() - except Exception as e: - print(f"✗ 获取活动警报失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - return {} - - def get_alert_history(self, limit: int = 100) -> List[Dict[str, Any]]: - """ - 获取警报历史 - - Args: - limit: 限制返回的数量 - - Returns: - 警报历史列表 - """ - try: - history = list(self.alert_history) - return history[-limit:] if len(history) > limit else history - except Exception as e: - print(f"✗ 获取警报历史失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - return [] - - def get_monitor_stats(self) -> Dict[str, Any]: - """ - 获取监控统计信息 - - Returns: - 监控统计字典 - """ - return { - "state": self.monitor_state.copy(), - "stats": self.monitor_stats.copy(), - "config": self.monitor_config.copy(), - "active_alerts_count": len(self.active_alerts), - "alert_history_count": len(self.alert_history) - } - - def reset_stats(self): - """重置监控统计信息""" - try: - self.monitor_stats = { - "metrics_collected": 0, - "alerts_generated": 0, - "alerts_resolved": 0, - "performance_checks": 0, - "monitor_errors": 0 - } - - self.monitor_state["total_monitored_matches"] = 0 - self.monitor_state["active_alerts"] = 0 - - print("✓ 监控统计信息已重置") - except Exception as e: - print(f"✗ 监控统计信息重置失败: {e}") - - def set_monitor_config(self, config: Dict[str, Any]) -> bool: - """ - 设置监控配置 - - Args: - config: 监控配置字典 - - Returns: - 是否设置成功 - """ - try: - self.monitor_config.update(config) - print(f"✓ 监控配置已更新: {self.monitor_config}") - return True - except Exception as e: - print(f"✗ 监控配置设置失败: {e}") - return False - - def get_monitor_config(self) -> Dict[str, Any]: - """ - 获取监控配置 - - Returns: - 监控配置字典 - """ - return self.monitor_config.copy() - - def _trigger_monitor_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发监控回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.monitor_callbacks: - for callback in self.monitor_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 监控回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 监控回调触发失败: {e}") - - def register_monitor_callback(self, callback_type: str, callback: callable): - """ - 注册监控回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.monitor_callbacks: - self.monitor_callbacks[callback_type].append(callback) - print(f"✓ 监控回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 监控回调注册失败: {e}") - - def unregister_monitor_callback(self, callback_type: str, callback: callable): - """ - 注销监控回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.monitor_callbacks: - if callback in self.monitor_callbacks[callback_type]: - self.monitor_callbacks[callback_type].remove(callback) - print(f"✓ 监控回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +匹配监控系统模块 +监控匹配过程和性能 +""" + +import time +from typing import Dict, Any, List, Optional +from collections import deque + +class MatchMonitor: + """ + 匹配监控器 + 监控匹配过程和性能 + """ + + def __init__(self, plugin): + """ + 初始化匹配监控器 + + Args: + plugin: 匹配系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 监控配置 + self.monitor_config = { + "enable_monitoring": True, + "monitoring_interval": 5.0, + "log_level": "INFO", + "enable_performance_monitoring": True, + "enable_alerts": True, + "alert_thresholds": { + "match_queue_time": 60.0, + "match_failure_rate": 10.0, + "system_latency": 100.0 + }, + "enable_metrics_collection": True, + "metrics_retention_period": 3600 # 1小时 + } + + # 性能指标存储 + self.performance_metrics = { + "match_queue_times": deque(maxlen=1000), + "match_success_rates": deque(maxlen=1000), + "system_latencies": deque(maxlen=1000), + "player_wait_times": deque(maxlen=1000), + "algorithm_performance": deque(maxlen=1000) + } + + # 监控状态 + self.monitor_state = { + "last_monitoring_update": 0.0, + "last_performance_check": 0.0, + "total_monitored_matches": 0, + "active_alerts": 0 + } + + # 警报系统 + self.active_alerts = {} + self.alert_history = deque(maxlen=1000) + + # 监控统计 + self.monitor_stats = { + "metrics_collected": 0, + "alerts_generated": 0, + "alerts_resolved": 0, + "performance_checks": 0, + "monitor_errors": 0 + } + + # 回调函数 + self.monitor_callbacks = { + "metric_collected": [], + "alert_triggered": [], + "alert_resolved": [], + "performance_degraded": [] + } + + # 时间戳记录 + self.last_metric_collection = 0.0 + self.last_alert_check = 0.0 + + print("✓ 匹配监控器已创建") + + def initialize(self) -> bool: + """ + 初始化匹配监控器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化匹配监控器...") + + self.initialized = True + print("✓ 匹配监控器初始化完成") + return True + + except Exception as e: + print(f"✗ 匹配监控器初始化失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用匹配监控器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 匹配监控器未初始化") + return False + + self.enabled = True + print("✓ 匹配监控器已启用") + return True + + except Exception as e: + print(f"✗ 匹配监控器启用失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用匹配监控器""" + try: + self.enabled = False + print("✓ 匹配监控器已禁用") + + except Exception as e: + print(f"✗ 匹配监控器禁用失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理匹配监控器资源""" + try: + # 禁用匹配监控器 + if self.enabled: + self.disable() + + # 清理回调 + self.monitor_callbacks.clear() + + self.initialized = False + print("✓ 匹配监控器资源已清理") + + except Exception as e: + print(f"✗ 匹配监控器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新匹配监控器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.monitor_state["last_monitoring_update"] = current_time + + # 定期收集性能指标 + if current_time - self.last_metric_collection >= self.monitor_config["monitoring_interval"]: + self._collect_performance_metrics() + self.last_metric_collection = current_time + + # 定期检查警报 + if current_time - self.last_alert_check >= self.monitor_config["monitoring_interval"]: + self._check_alerts() + self.last_alert_check = current_time + + except Exception as e: + print(f"✗ 匹配监控器更新失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + import traceback + traceback.print_exc() + + def _collect_performance_metrics(self): + """收集性能指标""" + try: + current_time = time.time() + metrics = {} + + # 收集匹配管理器指标 + if self.plugin.match_manager: + match_stats = self.plugin.match_manager.get_match_stats() + match_state = match_stats.get("state", {}) + + metrics["active_matches"] = match_state.get("active_matches", 0) + metrics["successful_matches"] = match_state.get("successful_matches", 0) + metrics["failed_matches"] = match_state.get("failed_matches", 0) + + # 计算成功率 + total_matches = metrics["successful_matches"] + metrics["failed_matches"] + if total_matches > 0: + success_rate = (metrics["successful_matches"] / total_matches) * 100 + metrics["match_success_rate"] = success_rate + self.performance_metrics["match_success_rates"].append((current_time, success_rate)) + + # 收集队列管理器指标 + if self.plugin.queue_manager: + queue_stats = self.plugin.queue_manager.get_queue_stats() + queue_state = queue_stats.get("state", {}) + + metrics["total_players"] = queue_state.get("total_players", 0) + metrics["players_matched"] = queue_state.get("players_matched", 0) + metrics["players_timed_out"] = queue_state.get("players_timed_out", 0) + + # 计算超时率 + total_players = metrics["players_matched"] + metrics["players_timed_out"] + if total_players > 0: + timeout_rate = (metrics["players_timed_out"] / total_players) * 100 + metrics["queue_timeout_rate"] = timeout_rate + + # 收集算法管理器指标 + if self.plugin.algorithm_manager: + algorithm_stats = self.plugin.algorithm_manager.get_algorithm_stats() + algorithm_state = algorithm_stats.get("state", {}) + + metrics["total_algorithms"] = algorithm_state.get("total_matches", 0) + metrics["successful_algorithms"] = algorithm_state.get("successful_matches", 0) + metrics["average_match_quality"] = algorithm_state.get("average_match_quality", 0) + + self.performance_metrics["algorithm_performance"].append( + (current_time, metrics["average_match_quality"]) + ) + + # 收集房间分配器指标 + if self.plugin.room_allocator: + room_stats = self.plugin.room_allocator.get_room_stats() + room_state = room_stats.get("state", {}) + + metrics["active_rooms"] = room_state.get("active_rooms", 0) + metrics["total_rooms"] = room_state.get("total_rooms", 0) + + # 更新统计 + self.monitor_stats["metrics_collected"] += 1 + self.monitor_state["total_monitored_matches"] += 1 + + # 触发指标收集回调 + self._trigger_monitor_callback("metric_collected", { + "metrics": metrics, + "timestamp": current_time + }) + + except Exception as e: + print(f"✗ 性能指标收集失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + + def _check_alerts(self): + """检查警报条件""" + try: + if not self.monitor_config["enable_alerts"]: + return + + current_time = time.time() + thresholds = self.monitor_config["alert_thresholds"] + + # 检查匹配队列时间 + if self.performance_metrics["match_queue_times"]: + latest_queue_time = self.performance_metrics["match_queue_times"][-1][1] + if latest_queue_time > thresholds["match_queue_time"]: + self._trigger_alert("high_queue_time", f"匹配队列时间过长: {latest_queue_time:.2f}秒", current_time) + + # 检查匹配失败率 + if self.performance_metrics["match_success_rates"]: + latest_success_rate = self.performance_metrics["match_success_rates"][-1][1] + failure_rate = 100 - latest_success_rate + if failure_rate > thresholds["match_failure_rate"]: + self._trigger_alert("high_failure_rate", f"匹配失败率过高: {failure_rate:.2f}%", current_time) + + # 检查系统延迟 + if self.performance_metrics["system_latencies"]: + latest_latency = self.performance_metrics["system_latencies"][-1][1] + if latest_latency > thresholds["system_latency"]: + self._trigger_alert("high_latency", f"系统延迟过高: {latest_latency:.2f}ms", current_time) + + except Exception as e: + print(f"✗ 警报检查失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + + def _trigger_alert(self, alert_type: str, message: str, timestamp: float = None): + """ + 触发警报 + + Args: + alert_type: 警报类型 + message: 警报消息 + timestamp: 时间戳 + """ + try: + if timestamp is None: + timestamp = time.time() + + alert_id = f"{alert_type}_{int(timestamp)}" + + # 检查是否已存在相同类型的活动警报 + if alert_type in self.active_alerts: + # 更新现有警报 + self.active_alerts[alert_type]["count"] += 1 + self.active_alerts[alert_type]["last_triggered"] = timestamp + else: + # 创建新警报 + self.active_alerts[alert_type] = { + "id": alert_id, + "type": alert_type, + "message": message, + "first_triggered": timestamp, + "last_triggered": timestamp, + "count": 1, + "resolved": False + } + + self.monitor_state["active_alerts"] += 1 + self.monitor_stats["alerts_generated"] += 1 + + # 添加到警报历史 + self.alert_history.append({ + "id": alert_id, + "type": alert_type, + "message": message, + "timestamp": timestamp, + "resolved": False + }) + + # 触发警报回调 + self._trigger_monitor_callback("alert_triggered", { + "alert_id": alert_id, + "alert_type": alert_type, + "message": message, + "timestamp": timestamp + }) + + print(f"⚠️ 警报触发 [{alert_type}]: {message}") + + except Exception as e: + print(f"✗ 警报触发失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + + def resolve_alert(self, alert_type: str): + """ + 解决警报 + + Args: + alert_type: 警报类型 + """ + try: + if alert_type in self.active_alerts: + alert_data = self.active_alerts[alert_type] + alert_data["resolved"] = True + alert_data["resolved_time"] = time.time() + + self.monitor_state["active_alerts"] -= 1 + self.monitor_stats["alerts_resolved"] += 1 + + # 触发警报解决回调 + self._trigger_monitor_callback("alert_resolved", { + "alert_type": alert_type, + "alert_data": alert_data + }) + + # 从活动警报中移除 + del self.active_alerts[alert_type] + + print(f"✅ 警报已解决: {alert_type}") + + except Exception as e: + print(f"✗ 警报解决失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + + def record_match_queue_time(self, queue_time: float): + """ + 记录匹配队列时间 + + Args: + queue_time: 队列时间(秒) + """ + try: + current_time = time.time() + self.performance_metrics["match_queue_times"].append((current_time, queue_time)) + except Exception as e: + print(f"✗ 匹配队列时间记录失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + + def record_player_wait_time(self, wait_time: float): + """ + 记录玩家等待时间 + + Args: + wait_time: 等待时间(秒) + """ + try: + current_time = time.time() + self.performance_metrics["player_wait_times"].append((current_time, wait_time)) + except Exception as e: + print(f"✗ 玩家等待时间记录失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + + def record_system_latency(self, latency: float): + """ + 记录系统延迟 + + Args: + latency: 延迟(毫秒) + """ + try: + current_time = time.time() + self.performance_metrics["system_latencies"].append((current_time, latency)) + except Exception as e: + print(f"✗ 系统延迟记录失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + + def get_performance_metrics(self, metric_name: str = None, limit: int = 100) -> Any: + """ + 获取性能指标 + + Args: + metric_name: 指标名称(可选) + limit: 限制返回的数量 + + Returns: + 性能指标数据 + """ + try: + if metric_name: + if metric_name in self.performance_metrics: + metrics = list(self.performance_metrics[metric_name]) + return metrics[-limit:] if len(metrics) > limit else metrics + else: + return [] + else: + # 返回所有指标 + result = {} + for name, metrics in self.performance_metrics.items(): + metric_list = list(metrics) + result[name] = metric_list[-limit:] if len(metric_list) > limit else metric_list + return result + + except Exception as e: + print(f"✗ 获取性能指标失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + return {} + + def get_active_alerts(self) -> Dict[str, Any]: + """ + 获取活动警报 + + Returns: + 活动警报字典 + """ + try: + return self.active_alerts.copy() + except Exception as e: + print(f"✗ 获取活动警报失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + return {} + + def get_alert_history(self, limit: int = 100) -> List[Dict[str, Any]]: + """ + 获取警报历史 + + Args: + limit: 限制返回的数量 + + Returns: + 警报历史列表 + """ + try: + history = list(self.alert_history) + return history[-limit:] if len(history) > limit else history + except Exception as e: + print(f"✗ 获取警报历史失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + return [] + + def get_monitor_stats(self) -> Dict[str, Any]: + """ + 获取监控统计信息 + + Returns: + 监控统计字典 + """ + return { + "state": self.monitor_state.copy(), + "stats": self.monitor_stats.copy(), + "config": self.monitor_config.copy(), + "active_alerts_count": len(self.active_alerts), + "alert_history_count": len(self.alert_history) + } + + def reset_stats(self): + """重置监控统计信息""" + try: + self.monitor_stats = { + "metrics_collected": 0, + "alerts_generated": 0, + "alerts_resolved": 0, + "performance_checks": 0, + "monitor_errors": 0 + } + + self.monitor_state["total_monitored_matches"] = 0 + self.monitor_state["active_alerts"] = 0 + + print("✓ 监控统计信息已重置") + except Exception as e: + print(f"✗ 监控统计信息重置失败: {e}") + + def set_monitor_config(self, config: Dict[str, Any]) -> bool: + """ + 设置监控配置 + + Args: + config: 监控配置字典 + + Returns: + 是否设置成功 + """ + try: + self.monitor_config.update(config) + print(f"✓ 监控配置已更新: {self.monitor_config}") + return True + except Exception as e: + print(f"✗ 监控配置设置失败: {e}") + return False + + def get_monitor_config(self) -> Dict[str, Any]: + """ + 获取监控配置 + + Returns: + 监控配置字典 + """ + return self.monitor_config.copy() + + def _trigger_monitor_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发监控回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.monitor_callbacks: + for callback in self.monitor_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 监控回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 监控回调触发失败: {e}") + + def register_monitor_callback(self, callback_type: str, callback: callable): + """ + 注册监控回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.monitor_callbacks: + self.monitor_callbacks[callback_type].append(callback) + print(f"✓ 监控回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 监控回调注册失败: {e}") + + def unregister_monitor_callback(self, callback_type: str, callback: callable): + """ + 注销监控回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.monitor_callbacks: + if callback in self.monitor_callbacks[callback_type]: + self.monitor_callbacks[callback_type].remove(callback) + print(f"✓ 监控回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 监控回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/matchmaking_system/plugin.py b/plugins/user/matchmaking_system/plugin.py index 146716a9..40e04ebe 100644 --- a/plugins/user/matchmaking_system/plugin.py +++ b/plugins/user/matchmaking_system/plugin.py @@ -1,225 +1,225 @@ -""" -匹配系统插件主文件 -""" - -class MatchmakingSystemPlugin: - """ - 匹配系统插件 - 为EG引擎提供玩家匹配功能 - """ - - def __init__(self, engine): - """ - 初始化匹配系统插件 - - Args: - engine: EG引擎实例 - """ - self.engine = engine - self.name = "MatchmakingSystem" - self.version = "1.0.0" - - # 插件组件 - self.match_manager = None - self.queue_manager = None - self.algorithm_manager = None - self.room_allocator = None - self.rule_manager = None - self.monitor = None - self.event_handler = None - self.config_manager = None - self.editor = None - self.stats_manager = None - - print("✓ 匹配系统插件已创建") - - def initialize(self) -> bool: - """ - 初始化插件 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化匹配系统插件...") - - # 导入模块 - from .core.match_manager import MatchManager - from .queue.queue_manager import QueueManager - from .algorithms.algorithm_manager import AlgorithmManager - from .rooms.room_allocator import RoomAllocator - from .rules.rule_manager import RuleManager - from .monitoring.match_monitor import MatchMonitor - from .events.event_handler import EventHandler - from .config.match_config import MatchConfig - from .editor.match_editor import MatchEditor - from .stats.stats_manager import StatsManager - - # 创建组件 - self.config_manager = MatchConfig(self) - self.event_handler = EventHandler(self) - self.rule_manager = RuleManager(self) - self.algorithm_manager = AlgorithmManager(self) - self.queue_manager = QueueManager(self) - self.room_allocator = RoomAllocator(self) - self.match_manager = MatchManager(self) - self.monitor = MatchMonitor(self) - self.editor = MatchEditor(self) - self.stats_manager = StatsManager(self) - - # 初始化组件 - components = [ - self.config_manager, - self.event_handler, - self.rule_manager, - self.algorithm_manager, - self.queue_manager, - self.room_allocator, - self.match_manager, - self.monitor, - self.editor, - self.stats_manager - ] - - for component in components: - if hasattr(component, 'initialize') and not component.initialize(): - print(f"✗ 组件初始化失败: {component.__class__.__name__}") - return False - - print("✓ 匹配系统插件初始化完成") - return True - - except Exception as e: - print(f"✗ 匹配系统插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - - Returns: - 是否启用成功 - """ - try: - print("正在启用匹配系统插件...") - - # 启用组件 - components = [ - self.config_manager, - self.event_handler, - self.rule_manager, - self.algorithm_manager, - self.queue_manager, - self.room_allocator, - self.match_manager, - self.monitor, - self.editor, - self.stats_manager - ] - - for component in components: - if hasattr(component, 'enable') and not component.enable(): - print(f"✗ 组件启用失败: {component.__class__.__name__}") - return False - - print("✓ 匹配系统插件已启用") - return True - - except Exception as e: - print(f"✗ 匹配系统插件启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用插件""" - try: - print("正在禁用匹配系统插件...") - - # 禁用组件 - components = [ - self.stats_manager, - self.editor, - self.monitor, - self.match_manager, - self.room_allocator, - self.queue_manager, - self.algorithm_manager, - self.rule_manager, - self.event_handler, - self.config_manager - ] - - for component in components: - if hasattr(component, 'disable'): - component.disable() - - print("✓ 匹配系统插件已禁用") - - except Exception as e: - print(f"✗ 匹配系统插件禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理插件资源""" - try: - print("正在清理匹配系统插件资源...") - - # 清理组件 - components = [ - self.stats_manager, - self.editor, - self.monitor, - self.match_manager, - self.room_allocator, - self.queue_manager, - self.algorithm_manager, - self.rule_manager, - self.event_handler, - self.config_manager - ] - - for component in components: - if hasattr(component, 'finalize'): - component.finalize() - - print("✓ 匹配系统插件资源已清理") - - except Exception as e: - print(f"✗ 匹配系统插件资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新插件状态 - - Args: - dt: 时间增量(秒) - """ - try: - # 更新组件 - components = [ - self.config_manager, - self.event_handler, - self.rule_manager, - self.algorithm_manager, - self.queue_manager, - self.room_allocator, - self.match_manager, - self.monitor, - self.editor, - self.stats_manager - ] - - for component in components: - if hasattr(component, 'update'): - component.update(dt) - - except Exception as e: - print(f"✗ 匹配系统插件更新失败: {e}") - import traceback +""" +匹配系统插件主文件 +""" + +class MatchmakingSystemPlugin: + """ + 匹配系统插件 + 为EG引擎提供玩家匹配功能 + """ + + def __init__(self, engine): + """ + 初始化匹配系统插件 + + Args: + engine: EG引擎实例 + """ + self.engine = engine + self.name = "MatchmakingSystem" + self.version = "1.0.0" + + # 插件组件 + self.match_manager = None + self.queue_manager = None + self.algorithm_manager = None + self.room_allocator = None + self.rule_manager = None + self.monitor = None + self.event_handler = None + self.config_manager = None + self.editor = None + self.stats_manager = None + + print("✓ 匹配系统插件已创建") + + def initialize(self) -> bool: + """ + 初始化插件 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化匹配系统插件...") + + # 导入模块 + from .core.match_manager import MatchManager + from .queue.queue_manager import QueueManager + from .algorithms.algorithm_manager import AlgorithmManager + from .rooms.room_allocator import RoomAllocator + from .rules.rule_manager import RuleManager + from .monitoring.match_monitor import MatchMonitor + from .events.event_handler import EventHandler + from .config.match_config import MatchConfig + from .editor.match_editor import MatchEditor + from .stats.stats_manager import StatsManager + + # 创建组件 + self.config_manager = MatchConfig(self) + self.event_handler = EventHandler(self) + self.rule_manager = RuleManager(self) + self.algorithm_manager = AlgorithmManager(self) + self.queue_manager = QueueManager(self) + self.room_allocator = RoomAllocator(self) + self.match_manager = MatchManager(self) + self.monitor = MatchMonitor(self) + self.editor = MatchEditor(self) + self.stats_manager = StatsManager(self) + + # 初始化组件 + components = [ + self.config_manager, + self.event_handler, + self.rule_manager, + self.algorithm_manager, + self.queue_manager, + self.room_allocator, + self.match_manager, + self.monitor, + self.editor, + self.stats_manager + ] + + for component in components: + if hasattr(component, 'initialize') and not component.initialize(): + print(f"✗ 组件初始化失败: {component.__class__.__name__}") + return False + + print("✓ 匹配系统插件初始化完成") + return True + + except Exception as e: + print(f"✗ 匹配系统插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + + Returns: + 是否启用成功 + """ + try: + print("正在启用匹配系统插件...") + + # 启用组件 + components = [ + self.config_manager, + self.event_handler, + self.rule_manager, + self.algorithm_manager, + self.queue_manager, + self.room_allocator, + self.match_manager, + self.monitor, + self.editor, + self.stats_manager + ] + + for component in components: + if hasattr(component, 'enable') and not component.enable(): + print(f"✗ 组件启用失败: {component.__class__.__name__}") + return False + + print("✓ 匹配系统插件已启用") + return True + + except Exception as e: + print(f"✗ 匹配系统插件启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用插件""" + try: + print("正在禁用匹配系统插件...") + + # 禁用组件 + components = [ + self.stats_manager, + self.editor, + self.monitor, + self.match_manager, + self.room_allocator, + self.queue_manager, + self.algorithm_manager, + self.rule_manager, + self.event_handler, + self.config_manager + ] + + for component in components: + if hasattr(component, 'disable'): + component.disable() + + print("✓ 匹配系统插件已禁用") + + except Exception as e: + print(f"✗ 匹配系统插件禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理插件资源""" + try: + print("正在清理匹配系统插件资源...") + + # 清理组件 + components = [ + self.stats_manager, + self.editor, + self.monitor, + self.match_manager, + self.room_allocator, + self.queue_manager, + self.algorithm_manager, + self.rule_manager, + self.event_handler, + self.config_manager + ] + + for component in components: + if hasattr(component, 'finalize'): + component.finalize() + + print("✓ 匹配系统插件资源已清理") + + except Exception as e: + print(f"✗ 匹配系统插件资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新插件状态 + + Args: + dt: 时间增量(秒) + """ + try: + # 更新组件 + components = [ + self.config_manager, + self.event_handler, + self.rule_manager, + self.algorithm_manager, + self.queue_manager, + self.room_allocator, + self.match_manager, + self.monitor, + self.editor, + self.stats_manager + ] + + for component in components: + if hasattr(component, 'update'): + component.update(dt) + + except Exception as e: + print(f"✗ 匹配系统插件更新失败: {e}") + import traceback traceback.print_exc() \ No newline at end of file diff --git a/plugins/user/matchmaking_system/queue/queue_manager.py b/plugins/user/matchmaking_system/queue/queue_manager.py index 408309b9..43269f50 100644 --- a/plugins/user/matchmaking_system/queue/queue_manager.py +++ b/plugins/user/matchmaking_system/queue/queue_manager.py @@ -1,664 +1,664 @@ -""" -队列管理器模块 -管理等待匹配的玩家队列 -""" - -import time -import threading -import uuid -from typing import Dict, Any, List, Optional -from collections import deque - -class QueueManager: - """ - 队列管理器 - 管理等待匹配的玩家队列 - """ - - def __init__(self, plugin): - """ - 初始化队列管理器 - - Args: - plugin: 匹配系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 队列配置 - self.queue_config = { - "default_queue_timeout": 300.0, # 5分钟 - "queue_processing_interval": 2.0, - "max_players_per_match": 100, - "min_players_per_match": 2, - "enable_queue_prioritization": True, - "enable_skill_based_queue": True, - "enable_region_based_queue": True - } - - # 队列状态 - self.queue_state = { - "total_players": 0, - "active_queues": 0, - "players_matched": 0, - "players_timed_out": 0 - } - - # 队列存储 - self.queues = {} # 按游戏模式分类的队列 - self.player_queues = {} # 玩家所在的队列映射 - self.queue_lock = threading.RLock() - - # 队列统计 - self.queue_stats = { - "players_added": 0, - "players_removed": 0, - "matches_created": 0, - "queue_timeouts": 0, - "queue_errors": 0 - } - - # 回调函数 - self.queue_callbacks = { - "player_queued": [], - "player_dequeued": [], - "match_found": [], - "queue_timeout": [], - "queue_error": [] - } - - # 时间戳记录 - self.last_queue_process = 0.0 - self.last_cleanup = 0.0 - - print("✓ 队列管理器已创建") - - def initialize(self) -> bool: - """ - 初始化队列管理器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化队列管理器...") - - self.initialized = True - print("✓ 队列管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 队列管理器初始化失败: {e}") - self.queue_stats["queue_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用队列管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 队列管理器未初始化") - return False - - self.enabled = True - print("✓ 队列管理器已启用") - return True - - except Exception as e: - print(f"✗ 队列管理器启用失败: {e}") - self.queue_stats["queue_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用队列管理器""" - try: - self.enabled = False - - # 清空所有队列 - with self.queue_lock: - self.queues.clear() - self.player_queues.clear() - - print("✓ 队列管理器已禁用") - - except Exception as e: - print(f"✗ 队列管理器禁用失败: {e}") - self.queue_stats["queue_errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理队列管理器资源""" - try: - # 禁用队列管理器 - if self.enabled: - self.disable() - - # 清理回调 - self.queue_callbacks.clear() - - self.initialized = False - print("✓ 队列管理器资源已清理") - - except Exception as e: - print(f"✗ 队列管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新队列管理器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 定期处理队列 - if current_time - self.last_queue_process >= self.queue_config["queue_processing_interval"]: - self.process_queues() - self.last_queue_process = current_time - - # 定期清理 - if current_time - self.last_cleanup >= 60.0: # 每分钟清理一次 - self._cleanup_expired_players(current_time) - self.last_cleanup = current_time - - except Exception as e: - print(f"✗ 队列管理器更新失败: {e}") - self.queue_stats["queue_errors"] += 1 - import traceback - traceback.print_exc() - - def _cleanup_expired_players(self, current_time: float): - """清理过期玩家""" - try: - expired_players = [] - with self.queue_lock: - for player_id, queue_info in self.player_queues.items(): - join_time = queue_info["join_time"] - timeout = queue_info.get("timeout", self.queue_config["default_queue_timeout"]) - - if current_time - join_time > timeout: - expired_players.append(player_id) - - # 移除过期玩家 - for player_id in expired_players: - self._remove_player_from_queue(player_id, "timeout") - self.queue_stats["queue_timeouts"] += 1 - - # 触发队列超时回调 - self._trigger_queue_callback("queue_timeout", { - "player_id": player_id, - "timestamp": current_time - }) - - except Exception as e: - print(f"✗ 过期玩家清理失败: {e}") - self.queue_stats["queue_errors"] += 1 - - def add_players_to_queue(self, player_ids: List[str], queue_params: Dict[str, Any]) -> bool: - """ - 添加玩家到队列 - - Args: - player_ids: 玩家ID列表 - queue_params: 队列参数 - - Returns: - 是否添加成功 - """ - try: - if not self.enabled: - return False - - queue_name = queue_params.get("queue_name", "default") - game_mode = queue_params.get("game_mode", "default") - region = queue_params.get("region", "global") - skill_level = queue_params.get("skill_level", 0) - timeout = queue_params.get("timeout", self.queue_config["default_queue_timeout"]) - - with self.queue_lock: - # 创建队列(如果不存在) - if queue_name not in self.queues: - self.queues[queue_name] = { - "name": queue_name, - "game_mode": game_mode, - "region": region, - "players": deque(), - "created_time": time.time() - } - self.queue_state["active_queues"] += 1 - - # 添加玩家到队列 - queue = self.queues[queue_name] - for player_id in player_ids: - player_info = { - "player_id": player_id, - "skill_level": skill_level, - "region": region, - "join_time": time.time(), - "timeout": timeout, - "queue_params": queue_params - } - - queue["players"].append(player_info) - self.player_queues[player_id] = { - "queue_name": queue_name, - "join_time": time.time(), - "timeout": timeout - } - - self.queue_state["total_players"] += len(player_ids) - self.queue_stats["players_added"] += len(player_ids) - - # 触发玩家入队回调 - self._trigger_queue_callback("player_queued", { - "player_ids": player_ids, - "queue_name": queue_name, - "game_mode": game_mode, - "timestamp": time.time() - }) - - print(f"✓ {len(player_ids)}名玩家已添加到队列: {queue_name}") - return True - - except Exception as e: - print(f"✗ 玩家添加到队列失败: {e}") - self.queue_stats["queue_errors"] += 1 - return False - - def remove_players_from_queue(self, match_id: str = None, player_ids: List[str] = None) -> bool: - """ - 从队列移除玩家 - - Args: - match_id: 匹配ID(可选) - player_ids: 玩家ID列表(可选) - - Returns: - 是否移除成功 - """ - try: - if not self.enabled: - return False - - removed_count = 0 - - if player_ids: - # 移除指定玩家 - for player_id in player_ids: - if self._remove_player_from_queue(player_id): - removed_count += 1 - else: - # 移除所有玩家(这种情况较少使用) - with self.queue_lock: - player_ids = list(self.player_queues.keys()) - for player_id in player_ids: - if self._remove_player_from_queue(player_id): - removed_count += 1 - - self.queue_stats["players_removed"] += removed_count - - # 触发玩家离队回调 - if player_ids: - self._trigger_queue_callback("player_dequeued", { - "player_ids": player_ids, - "match_id": match_id, - "timestamp": time.time() - }) - - print(f"✓ {removed_count}名玩家已从队列移除") - return removed_count > 0 - - except Exception as e: - print(f"✗ 玩家从队列移除失败: {e}") - self.queue_stats["queue_errors"] += 1 - return False - - def _remove_player_from_queue(self, player_id: str, reason: str = "manual") -> bool: - """ - 从队列移除单个玩家 - - Args: - player_id: 玩家ID - reason: 移除原因 - - Returns: - 是否移除成功 - """ - try: - with self.queue_lock: - if player_id not in self.player_queues: - return False - - queue_name = self.player_queues[player_id]["queue_name"] - del self.player_queues[player_id] - - # 从队列中移除玩家 - if queue_name in self.queues: - queue = self.queues[queue_name] - players = queue["players"] - - # 查找并移除玩家 - player_found = False - for i, player_info in enumerate(list(players)): - if player_info["player_id"] == player_id: - del players[i] - player_found = True - break - - # 如果队列为空,移除队列 - if len(players) == 0: - del self.queues[queue_name] - self.queue_state["active_queues"] -= 1 - - if player_found: - self.queue_state["total_players"] -= 1 - return True - - return False - - except Exception as e: - print(f"✗ 玩家从队列移除失败: {e}") - self.queue_stats["queue_errors"] += 1 - return False - - def process_queues(self): - """处理队列(寻找匹配)""" - try: - if not self.enabled: - return - - with self.queue_lock: - queue_names = list(self.queues.keys()) - - for queue_name in queue_names: - self._process_queue(queue_name) - - except Exception as e: - print(f"✗ 队列处理失败: {e}") - self.queue_stats["queue_errors"] += 1 - - def _process_queue(self, queue_name: str): - """ - 处理单个队列 - - Args: - queue_name: 队列名称 - """ - try: - with self.queue_lock: - if queue_name not in self.queues: - return - - queue = self.queues[queue_name] - players = list(queue["players"]) - - # 检查是否有足够的玩家进行匹配 - min_players = self.queue_config["min_players_per_match"] - max_players = self.queue_config["max_players_per_match"] - - if len(players) >= min_players: - # 确定匹配的玩家数量 - match_player_count = min(len(players), max_players) - - # 选择玩家(简化实现,实际中可能需要考虑技能匹配等因素) - selected_players = players[:match_player_count] - - # 创建匹配 - if self._create_match(selected_players, queue): - # 从队列中移除已匹配的玩家 - for player_info in selected_players: - self._remove_player_from_queue(player_info["player_id"]) - - self.queue_state["players_matched"] += len(selected_players) - self.queue_stats["matches_created"] += 1 - - except Exception as e: - print(f"✗ 队列处理失败: {e}") - self.queue_stats["queue_errors"] += 1 - - def _create_match(self, players: List[Dict[str, Any]], queue: Dict[str, Any]) -> bool: - """ - 创建匹配 - - Args: - players: 玩家列表 - queue: 队列信息 - - Returns: - 是否创建成功 - """ - try: - # 生成匹配ID - match_id = f"match_{int(time.time() * 1000000)}" - - # 提取玩家ID - player_ids = [player["player_id"] for player in players] - - # 创建房间 - room_id = None - if self.plugin.room_allocator: - room_settings = { - "game_mode": queue["game_mode"], - "region": queue["region"], - "max_players": len(players) - } - - room_id = self.plugin.room_allocator.create_room( - room_name=f"Match_{match_id[:8]}", - room_settings=room_settings - ) - - # 添加玩家到房间 - if room_id and self.plugin.room_allocator: - for player_id in player_ids: - self.plugin.room_allocator.add_client_to_room(room_id, player_id) - - # 通知匹配管理器匹配完成 - if self.plugin.match_manager: - self.plugin.match_manager.complete_match(match_id, room_id or "") - - # 触发匹配找到回调 - self._trigger_queue_callback("match_found", { - "match_id": match_id, - "room_id": room_id, - "player_ids": player_ids, - "queue_name": queue["name"], - "timestamp": time.time() - }) - - print(f"✓ 匹配已创建: {match_id} ({len(player_ids)}名玩家)") - return True - - except Exception as e: - print(f"✗ 匹配创建失败: {e}") - self.queue_stats["queue_errors"] += 1 - return False - - def get_queue_info(self, queue_name: str = None) -> Dict[str, Any]: - """ - 获取队列信息 - - Args: - queue_name: 队列名称(可选,如果未指定则返回所有队列信息) - - Returns: - 队列信息字典 - """ - try: - with self.queue_lock: - if queue_name: - if queue_name in self.queues: - queue = self.queues[queue_name] - return { - "name": queue["name"], - "game_mode": queue["game_mode"], - "region": queue["region"], - "player_count": len(queue["players"]), - "created_time": queue["created_time"] - } - else: - return {} - else: - # 返回所有队列信息 - queue_info = {} - for name, queue in self.queues.items(): - queue_info[name] = { - "name": queue["name"], - "game_mode": queue["game_mode"], - "region": queue["region"], - "player_count": len(queue["players"]), - "created_time": queue["created_time"] - } - return queue_info - - except Exception as e: - print(f"✗ 队列信息获取失败: {e}") - self.queue_stats["queue_errors"] += 1 - return {} - - def get_player_queue_info(self, player_id: str) -> Optional[Dict[str, Any]]: - """ - 获取玩家队列信息 - - Args: - player_id: 玩家ID - - Returns: - 玩家队列信息或None - """ - try: - with self.queue_lock: - if player_id in self.player_queues: - return self.player_queues[player_id].copy() - else: - return None - - except Exception as e: - print(f"✗ 玩家队列信息获取失败: {e}") - self.queue_stats["queue_errors"] += 1 - return None - - def get_queue_stats(self) -> Dict[str, Any]: - """ - 获取队列统计信息 - - Returns: - 队列统计字典 - """ - return { - "state": self.queue_state.copy(), - "stats": self.queue_stats.copy(), - "config": self.queue_config.copy(), - "current_queues": len(self.queues), - "current_players": len(self.player_queues) - } - - def reset_stats(self): - """重置队列统计信息""" - try: - self.queue_stats = { - "players_added": 0, - "players_removed": 0, - "matches_created": 0, - "queue_timeouts": 0, - "queue_errors": 0 - } - print("✓ 队列统计信息已重置") - except Exception as e: - print(f"✗ 队列统计信息重置失败: {e}") - - def set_queue_config(self, config: Dict[str, Any]) -> bool: - """ - 设置队列配置 - - Args: - config: 队列配置字典 - - Returns: - 是否设置成功 - """ - try: - self.queue_config.update(config) - print(f"✓ 队列配置已更新: {self.queue_config}") - return True - except Exception as e: - print(f"✗ 队列配置设置失败: {e}") - return False - - def get_queue_config(self) -> Dict[str, Any]: - """ - 获取队列配置 - - Returns: - 队列配置字典 - """ - return self.queue_config.copy() - - def _trigger_queue_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发队列回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.queue_callbacks: - for callback in self.queue_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 队列回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 队列回调触发失败: {e}") - - def register_queue_callback(self, callback_type: str, callback: callable): - """ - 注册队列回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.queue_callbacks: - self.queue_callbacks[callback_type].append(callback) - print(f"✓ 队列回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 队列回调注册失败: {e}") - - def unregister_queue_callback(self, callback_type: str, callback: callable): - """ - 注销队列回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.queue_callbacks: - if callback in self.queue_callbacks[callback_type]: - self.queue_callbacks[callback_type].remove(callback) - print(f"✓ 队列回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +队列管理器模块 +管理等待匹配的玩家队列 +""" + +import time +import threading +import uuid +from typing import Dict, Any, List, Optional +from collections import deque + +class QueueManager: + """ + 队列管理器 + 管理等待匹配的玩家队列 + """ + + def __init__(self, plugin): + """ + 初始化队列管理器 + + Args: + plugin: 匹配系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 队列配置 + self.queue_config = { + "default_queue_timeout": 300.0, # 5分钟 + "queue_processing_interval": 2.0, + "max_players_per_match": 100, + "min_players_per_match": 2, + "enable_queue_prioritization": True, + "enable_skill_based_queue": True, + "enable_region_based_queue": True + } + + # 队列状态 + self.queue_state = { + "total_players": 0, + "active_queues": 0, + "players_matched": 0, + "players_timed_out": 0 + } + + # 队列存储 + self.queues = {} # 按游戏模式分类的队列 + self.player_queues = {} # 玩家所在的队列映射 + self.queue_lock = threading.RLock() + + # 队列统计 + self.queue_stats = { + "players_added": 0, + "players_removed": 0, + "matches_created": 0, + "queue_timeouts": 0, + "queue_errors": 0 + } + + # 回调函数 + self.queue_callbacks = { + "player_queued": [], + "player_dequeued": [], + "match_found": [], + "queue_timeout": [], + "queue_error": [] + } + + # 时间戳记录 + self.last_queue_process = 0.0 + self.last_cleanup = 0.0 + + print("✓ 队列管理器已创建") + + def initialize(self) -> bool: + """ + 初始化队列管理器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化队列管理器...") + + self.initialized = True + print("✓ 队列管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 队列管理器初始化失败: {e}") + self.queue_stats["queue_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用队列管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 队列管理器未初始化") + return False + + self.enabled = True + print("✓ 队列管理器已启用") + return True + + except Exception as e: + print(f"✗ 队列管理器启用失败: {e}") + self.queue_stats["queue_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用队列管理器""" + try: + self.enabled = False + + # 清空所有队列 + with self.queue_lock: + self.queues.clear() + self.player_queues.clear() + + print("✓ 队列管理器已禁用") + + except Exception as e: + print(f"✗ 队列管理器禁用失败: {e}") + self.queue_stats["queue_errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理队列管理器资源""" + try: + # 禁用队列管理器 + if self.enabled: + self.disable() + + # 清理回调 + self.queue_callbacks.clear() + + self.initialized = False + print("✓ 队列管理器资源已清理") + + except Exception as e: + print(f"✗ 队列管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新队列管理器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 定期处理队列 + if current_time - self.last_queue_process >= self.queue_config["queue_processing_interval"]: + self.process_queues() + self.last_queue_process = current_time + + # 定期清理 + if current_time - self.last_cleanup >= 60.0: # 每分钟清理一次 + self._cleanup_expired_players(current_time) + self.last_cleanup = current_time + + except Exception as e: + print(f"✗ 队列管理器更新失败: {e}") + self.queue_stats["queue_errors"] += 1 + import traceback + traceback.print_exc() + + def _cleanup_expired_players(self, current_time: float): + """清理过期玩家""" + try: + expired_players = [] + with self.queue_lock: + for player_id, queue_info in self.player_queues.items(): + join_time = queue_info["join_time"] + timeout = queue_info.get("timeout", self.queue_config["default_queue_timeout"]) + + if current_time - join_time > timeout: + expired_players.append(player_id) + + # 移除过期玩家 + for player_id in expired_players: + self._remove_player_from_queue(player_id, "timeout") + self.queue_stats["queue_timeouts"] += 1 + + # 触发队列超时回调 + self._trigger_queue_callback("queue_timeout", { + "player_id": player_id, + "timestamp": current_time + }) + + except Exception as e: + print(f"✗ 过期玩家清理失败: {e}") + self.queue_stats["queue_errors"] += 1 + + def add_players_to_queue(self, player_ids: List[str], queue_params: Dict[str, Any]) -> bool: + """ + 添加玩家到队列 + + Args: + player_ids: 玩家ID列表 + queue_params: 队列参数 + + Returns: + 是否添加成功 + """ + try: + if not self.enabled: + return False + + queue_name = queue_params.get("queue_name", "default") + game_mode = queue_params.get("game_mode", "default") + region = queue_params.get("region", "global") + skill_level = queue_params.get("skill_level", 0) + timeout = queue_params.get("timeout", self.queue_config["default_queue_timeout"]) + + with self.queue_lock: + # 创建队列(如果不存在) + if queue_name not in self.queues: + self.queues[queue_name] = { + "name": queue_name, + "game_mode": game_mode, + "region": region, + "players": deque(), + "created_time": time.time() + } + self.queue_state["active_queues"] += 1 + + # 添加玩家到队列 + queue = self.queues[queue_name] + for player_id in player_ids: + player_info = { + "player_id": player_id, + "skill_level": skill_level, + "region": region, + "join_time": time.time(), + "timeout": timeout, + "queue_params": queue_params + } + + queue["players"].append(player_info) + self.player_queues[player_id] = { + "queue_name": queue_name, + "join_time": time.time(), + "timeout": timeout + } + + self.queue_state["total_players"] += len(player_ids) + self.queue_stats["players_added"] += len(player_ids) + + # 触发玩家入队回调 + self._trigger_queue_callback("player_queued", { + "player_ids": player_ids, + "queue_name": queue_name, + "game_mode": game_mode, + "timestamp": time.time() + }) + + print(f"✓ {len(player_ids)}名玩家已添加到队列: {queue_name}") + return True + + except Exception as e: + print(f"✗ 玩家添加到队列失败: {e}") + self.queue_stats["queue_errors"] += 1 + return False + + def remove_players_from_queue(self, match_id: str = None, player_ids: List[str] = None) -> bool: + """ + 从队列移除玩家 + + Args: + match_id: 匹配ID(可选) + player_ids: 玩家ID列表(可选) + + Returns: + 是否移除成功 + """ + try: + if not self.enabled: + return False + + removed_count = 0 + + if player_ids: + # 移除指定玩家 + for player_id in player_ids: + if self._remove_player_from_queue(player_id): + removed_count += 1 + else: + # 移除所有玩家(这种情况较少使用) + with self.queue_lock: + player_ids = list(self.player_queues.keys()) + for player_id in player_ids: + if self._remove_player_from_queue(player_id): + removed_count += 1 + + self.queue_stats["players_removed"] += removed_count + + # 触发玩家离队回调 + if player_ids: + self._trigger_queue_callback("player_dequeued", { + "player_ids": player_ids, + "match_id": match_id, + "timestamp": time.time() + }) + + print(f"✓ {removed_count}名玩家已从队列移除") + return removed_count > 0 + + except Exception as e: + print(f"✗ 玩家从队列移除失败: {e}") + self.queue_stats["queue_errors"] += 1 + return False + + def _remove_player_from_queue(self, player_id: str, reason: str = "manual") -> bool: + """ + 从队列移除单个玩家 + + Args: + player_id: 玩家ID + reason: 移除原因 + + Returns: + 是否移除成功 + """ + try: + with self.queue_lock: + if player_id not in self.player_queues: + return False + + queue_name = self.player_queues[player_id]["queue_name"] + del self.player_queues[player_id] + + # 从队列中移除玩家 + if queue_name in self.queues: + queue = self.queues[queue_name] + players = queue["players"] + + # 查找并移除玩家 + player_found = False + for i, player_info in enumerate(list(players)): + if player_info["player_id"] == player_id: + del players[i] + player_found = True + break + + # 如果队列为空,移除队列 + if len(players) == 0: + del self.queues[queue_name] + self.queue_state["active_queues"] -= 1 + + if player_found: + self.queue_state["total_players"] -= 1 + return True + + return False + + except Exception as e: + print(f"✗ 玩家从队列移除失败: {e}") + self.queue_stats["queue_errors"] += 1 + return False + + def process_queues(self): + """处理队列(寻找匹配)""" + try: + if not self.enabled: + return + + with self.queue_lock: + queue_names = list(self.queues.keys()) + + for queue_name in queue_names: + self._process_queue(queue_name) + + except Exception as e: + print(f"✗ 队列处理失败: {e}") + self.queue_stats["queue_errors"] += 1 + + def _process_queue(self, queue_name: str): + """ + 处理单个队列 + + Args: + queue_name: 队列名称 + """ + try: + with self.queue_lock: + if queue_name not in self.queues: + return + + queue = self.queues[queue_name] + players = list(queue["players"]) + + # 检查是否有足够的玩家进行匹配 + min_players = self.queue_config["min_players_per_match"] + max_players = self.queue_config["max_players_per_match"] + + if len(players) >= min_players: + # 确定匹配的玩家数量 + match_player_count = min(len(players), max_players) + + # 选择玩家(简化实现,实际中可能需要考虑技能匹配等因素) + selected_players = players[:match_player_count] + + # 创建匹配 + if self._create_match(selected_players, queue): + # 从队列中移除已匹配的玩家 + for player_info in selected_players: + self._remove_player_from_queue(player_info["player_id"]) + + self.queue_state["players_matched"] += len(selected_players) + self.queue_stats["matches_created"] += 1 + + except Exception as e: + print(f"✗ 队列处理失败: {e}") + self.queue_stats["queue_errors"] += 1 + + def _create_match(self, players: List[Dict[str, Any]], queue: Dict[str, Any]) -> bool: + """ + 创建匹配 + + Args: + players: 玩家列表 + queue: 队列信息 + + Returns: + 是否创建成功 + """ + try: + # 生成匹配ID + match_id = f"match_{int(time.time() * 1000000)}" + + # 提取玩家ID + player_ids = [player["player_id"] for player in players] + + # 创建房间 + room_id = None + if self.plugin.room_allocator: + room_settings = { + "game_mode": queue["game_mode"], + "region": queue["region"], + "max_players": len(players) + } + + room_id = self.plugin.room_allocator.create_room( + room_name=f"Match_{match_id[:8]}", + room_settings=room_settings + ) + + # 添加玩家到房间 + if room_id and self.plugin.room_allocator: + for player_id in player_ids: + self.plugin.room_allocator.add_client_to_room(room_id, player_id) + + # 通知匹配管理器匹配完成 + if self.plugin.match_manager: + self.plugin.match_manager.complete_match(match_id, room_id or "") + + # 触发匹配找到回调 + self._trigger_queue_callback("match_found", { + "match_id": match_id, + "room_id": room_id, + "player_ids": player_ids, + "queue_name": queue["name"], + "timestamp": time.time() + }) + + print(f"✓ 匹配已创建: {match_id} ({len(player_ids)}名玩家)") + return True + + except Exception as e: + print(f"✗ 匹配创建失败: {e}") + self.queue_stats["queue_errors"] += 1 + return False + + def get_queue_info(self, queue_name: str = None) -> Dict[str, Any]: + """ + 获取队列信息 + + Args: + queue_name: 队列名称(可选,如果未指定则返回所有队列信息) + + Returns: + 队列信息字典 + """ + try: + with self.queue_lock: + if queue_name: + if queue_name in self.queues: + queue = self.queues[queue_name] + return { + "name": queue["name"], + "game_mode": queue["game_mode"], + "region": queue["region"], + "player_count": len(queue["players"]), + "created_time": queue["created_time"] + } + else: + return {} + else: + # 返回所有队列信息 + queue_info = {} + for name, queue in self.queues.items(): + queue_info[name] = { + "name": queue["name"], + "game_mode": queue["game_mode"], + "region": queue["region"], + "player_count": len(queue["players"]), + "created_time": queue["created_time"] + } + return queue_info + + except Exception as e: + print(f"✗ 队列信息获取失败: {e}") + self.queue_stats["queue_errors"] += 1 + return {} + + def get_player_queue_info(self, player_id: str) -> Optional[Dict[str, Any]]: + """ + 获取玩家队列信息 + + Args: + player_id: 玩家ID + + Returns: + 玩家队列信息或None + """ + try: + with self.queue_lock: + if player_id in self.player_queues: + return self.player_queues[player_id].copy() + else: + return None + + except Exception as e: + print(f"✗ 玩家队列信息获取失败: {e}") + self.queue_stats["queue_errors"] += 1 + return None + + def get_queue_stats(self) -> Dict[str, Any]: + """ + 获取队列统计信息 + + Returns: + 队列统计字典 + """ + return { + "state": self.queue_state.copy(), + "stats": self.queue_stats.copy(), + "config": self.queue_config.copy(), + "current_queues": len(self.queues), + "current_players": len(self.player_queues) + } + + def reset_stats(self): + """重置队列统计信息""" + try: + self.queue_stats = { + "players_added": 0, + "players_removed": 0, + "matches_created": 0, + "queue_timeouts": 0, + "queue_errors": 0 + } + print("✓ 队列统计信息已重置") + except Exception as e: + print(f"✗ 队列统计信息重置失败: {e}") + + def set_queue_config(self, config: Dict[str, Any]) -> bool: + """ + 设置队列配置 + + Args: + config: 队列配置字典 + + Returns: + 是否设置成功 + """ + try: + self.queue_config.update(config) + print(f"✓ 队列配置已更新: {self.queue_config}") + return True + except Exception as e: + print(f"✗ 队列配置设置失败: {e}") + return False + + def get_queue_config(self) -> Dict[str, Any]: + """ + 获取队列配置 + + Returns: + 队列配置字典 + """ + return self.queue_config.copy() + + def _trigger_queue_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发队列回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.queue_callbacks: + for callback in self.queue_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 队列回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 队列回调触发失败: {e}") + + def register_queue_callback(self, callback_type: str, callback: callable): + """ + 注册队列回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.queue_callbacks: + self.queue_callbacks[callback_type].append(callback) + print(f"✓ 队列回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 队列回调注册失败: {e}") + + def unregister_queue_callback(self, callback_type: str, callback: callable): + """ + 注销队列回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.queue_callbacks: + if callback in self.queue_callbacks[callback_type]: + self.queue_callbacks[callback_type].remove(callback) + print(f"✓ 队列回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 队列回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/matchmaking_system/rooms/room_allocator.py b/plugins/user/matchmaking_system/rooms/room_allocator.py index b35cc1c9..aa63731b 100644 --- a/plugins/user/matchmaking_system/rooms/room_allocator.py +++ b/plugins/user/matchmaking_system/rooms/room_allocator.py @@ -1,752 +1,752 @@ -""" -房间分配器模块 -为匹配成功的玩家创建和分配房间 -""" - -import time -import uuid -from typing import Dict, Any, List, Optional - -class RoomAllocator: - """ - 房间分配器 - 为匹配成功的玩家创建和分配房间 - """ - - def __init__(self, plugin): - """ - 初始化房间分配器 - - Args: - plugin: 匹配系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 房间配置 - self.room_config = { - "default_max_players": 8, - "enable_room_passwords": True, - "enable_room_visibility": True, - "room_timeout": 3600, # 1小时无活动房间自动关闭 - "max_rooms": 1000, - "enable_persistent_rooms": False - } - - # 房间状态 - self.room_state = { - "total_rooms": 0, - "active_rooms": 0, - "occupied_rooms": 0, - "available_rooms": 0 - } - - # 房间存储 - self.rooms = {} # 房间数据 - self.player_rooms = {} # 玩家所在房间映射 - - # 房间统计 - self.room_stats = { - "rooms_created": 0, - "rooms_destroyed": 0, - "players_added": 0, - "players_removed": 0, - "room_errors": 0 - } - - # 回调函数 - self.room_callbacks = { - "room_created": [], - "room_destroyed": [], - "player_added": [], - "player_removed": [], - "room_error": [] - } - - # 时间戳记录 - self.last_cleanup = 0.0 - self.last_stats_reset = 0.0 - - print("✓ 房间分配器已创建") - - def initialize(self) -> bool: - """ - 初始化房间分配器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化房间分配器...") - - self.initialized = True - print("✓ 房间分配器初始化完成") - return True - - except Exception as e: - print(f"✗ 房间分配器初始化失败: {e}") - self.room_stats["room_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用房间分配器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 房间分配器未初始化") - return False - - self.enabled = True - print("✓ 房间分配器已启用") - return True - - except Exception as e: - print(f"✗ 房间分配器启用失败: {e}") - self.room_stats["room_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用房间分配器""" - try: - self.enabled = False - - # 销毁所有房间 - self._destroy_all_rooms() - - print("✓ 房间分配器已禁用") - - except Exception as e: - print(f"✗ 房间分配器禁用失败: {e}") - self.room_stats["room_errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理房间分配器资源""" - try: - # 禁用房间分配器 - if self.enabled: - self.disable() - - # 清理回调 - self.room_callbacks.clear() - - self.initialized = False - print("✓ 房间分配器资源已清理") - - except Exception as e: - print(f"✗ 房间分配器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新房间分配器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 定期清理过期房间 - if current_time - self.last_cleanup >= 300.0: # 每5分钟清理一次 - self._cleanup_expired_rooms(current_time) - self.last_cleanup = current_time - - except Exception as e: - print(f"✗ 房间分配器更新失败: {e}") - self.room_stats["room_errors"] += 1 - import traceback - traceback.print_exc() - - def _cleanup_expired_rooms(self, current_time: float): - """清理过期房间""" - try: - expired_rooms = [] - for room_id, room_data in self.rooms.items(): - # 检查房间是否超时 - if current_time - room_data.get("last_activity", 0) > self.room_config["room_timeout"]: - # 检查房间是否为空 - if len(room_data.get("players", {})) == 0: - expired_rooms.append(room_id) - - # 销毁过期房间 - for room_id in expired_rooms: - self.destroy_room(room_id) - - except Exception as e: - print(f"✗ 过期房间清理失败: {e}") - self.room_stats["room_errors"] += 1 - - def _destroy_all_rooms(self): - """销毁所有房间""" - try: - room_ids = list(self.rooms.keys()) - for room_id in room_ids: - self.destroy_room(room_id) - except Exception as e: - print(f"✗ 所有房间销毁失败: {e}") - self.room_stats["room_errors"] += 1 - - def create_room(self, room_name: str = None, room_settings: Dict[str, Any] = None, - room_password: str = None) -> Optional[str]: - """ - 创建房间 - - Args: - room_name: 房间名称 - room_settings: 房间设置 - room_password: 房间密码 - - Returns: - 房间ID或None - """ - try: - if not self.enabled: - return None - - # 检查房间数量限制 - if len(self.rooms) >= self.room_config["max_rooms"]: - print("✗ 房间数量已达上限") - self.room_stats["room_errors"] += 1 - return None - - # 生成房间ID - room_id = f"room_{int(time.time() * 1000000)}" - - # 使用默认设置或传入设置 - settings = { - "max_players": self.room_config["default_max_players"], - "is_public": True, - "allow_spectators": True - } - - if room_settings: - settings.update(room_settings) - - # 创建房间数据 - room_data = { - "room_id": room_id, - "room_name": room_name or f"房间-{room_id[:8]}", - "settings": settings, - "password": room_password, - "players": {}, # 玩家ID到玩家数据的映射 - "host_player_id": None, - "created_time": time.time(), - "last_activity": time.time(), - "room_state": {}, # 房间状态数据 - "custom_data": {} # 自定义房间数据 - } - - # 添加到房间列表 - self.rooms[room_id] = room_data - - self.room_state["total_rooms"] += 1 - self.room_state["active_rooms"] += 1 - self.room_stats["rooms_created"] += 1 - - # 触发房间创建回调 - self._trigger_room_callback("room_created", { - "room_id": room_id, - "room_name": room_data["room_name"], - "settings": settings, - "timestamp": room_data["created_time"] - }) - - print(f"✓ 房间已创建: {room_id} ({room_data['room_name']})") - return room_id - - except Exception as e: - print(f"✗ 房间创建失败: {e}") - self.room_stats["room_errors"] += 1 - return None - - def destroy_room(self, room_id: str) -> bool: - """ - 销毁房间 - - Args: - room_id: 房间ID - - Returns: - 是否销毁成功 - """ - try: - if not self.enabled: - return False - - # 检查房间是否存在 - if room_id not in self.rooms: - print(f"✗ 房间不存在: {room_id}") - return False - - room_data = self.rooms[room_id] - - # 通知房间内所有玩家 - player_ids = list(room_data["players"].keys()) - for player_id in player_ids: - self._notify_player_room_destroyed(player_id, room_id) - - # 移除房间 - del self.rooms[room_id] - - self.room_state["active_rooms"] -= 1 - self.room_stats["rooms_destroyed"] += 1 - - # 触发房间销毁回调 - self._trigger_room_callback("room_destroyed", { - "room_id": room_id, - "room_name": room_data["room_name"], - "timestamp": time.time() - }) - - print(f"✓ 房间已销毁: {room_id}") - return True - - except Exception as e: - print(f"✗ 房间销毁失败: {e}") - self.room_stats["room_errors"] += 1 - return False - - def _notify_player_room_destroyed(self, player_id: str, room_id: str): - """ - 通知玩家房间被销毁 - - Args: - player_id: 玩家ID - room_id: 房间ID - """ - try: - # 从玩家房间映射中移除 - if player_id in self.player_rooms: - del self.player_rooms[player_id] - - except Exception as e: - print(f"✗ 玩家房间销毁通知失败: {e}") - self.room_stats["room_errors"] += 1 - - def add_client_to_room(self, room_id: str, player_id: str, player_data: Dict[str, Any] = None) -> bool: - """ - 添加玩家到房间 - - Args: - room_id: 房间ID - player_id: 玩家ID - player_data: 玩家数据 - - Returns: - 是否添加成功 - """ - try: - if not self.enabled: - return False - - # 检查房间是否存在 - if room_id not in self.rooms: - print(f"✗ 房间不存在: {room_id}") - self.room_stats["room_errors"] += 1 - return False - - room_data = self.rooms[room_id] - - # 检查房间是否已满 - if len(room_data["players"]) >= room_data["settings"]["max_players"]: - print(f"✗ 房间已满: {room_id}") - return False - - # 检查房间密码 - if room_data["password"] and not self._verify_room_password(room_id, player_data): - print(f"✗ 房间密码错误: {room_id}") - return False - - # 添加玩家到房间 - player_info = player_data or {} - player_info["join_time"] = time.time() - player_info["last_activity"] = time.time() - - room_data["players"][player_id] = player_info - room_data["last_activity"] = time.time() - - # 设置房间主机(如果是第一个玩家) - if room_data["host_player_id"] is None: - room_data["host_player_id"] = player_id - - # 更新玩家房间映射 - self.player_rooms[player_id] = room_id - - self.room_stats["players_added"] += 1 - - # 触发玩家添加回调 - self._trigger_room_callback("player_added", { - "room_id": room_id, - "room_name": room_data["room_name"], - "player_id": player_id, - "timestamp": time.time() - }) - - print(f"✓ 玩家已加入房间: {player_id} -> {room_id}") - return True - - except Exception as e: - print(f"✗ 玩家加入房间失败: {e}") - self.room_stats["room_errors"] += 1 - return False - - def remove_client_from_room(self, room_id: str, player_id: str) -> bool: - """ - 从房间移除玩家 - - Args: - room_id: 房间ID - player_id: 玩家ID - - Returns: - 是否移除成功 - """ - try: - if not self.enabled: - return False - - # 检查房间是否存在 - if room_id not in self.rooms: - print(f"✗ 房间不存在: {room_id}") - return False - - room_data = self.rooms[room_id] - - # 检查玩家是否在房间中 - if player_id not in room_data["players"]: - print(f"✗ 玩家不在房间中: {player_id}") - return False - - # 移除玩家 - del room_data["players"][player_id] - room_data["last_activity"] = time.time() - - # 更新主机(如果移除的是主机) - if room_data["host_player_id"] == player_id: - if room_data["players"]: - # 选择第一个玩家作为新主机 - room_data["host_player_id"] = next(iter(room_data["players"])) - else: - room_data["host_player_id"] = None - - # 移除玩家房间映射 - if player_id in self.player_rooms: - del self.player_rooms[player_id] - - self.room_stats["players_removed"] += 1 - - # 触发玩家移除回调 - self._trigger_room_callback("player_removed", { - "room_id": room_id, - "room_name": room_data["room_name"], - "player_id": player_id, - "timestamp": time.time() - }) - - # 如果房间为空,销毁房间 - if len(room_data["players"]) == 0: - self.destroy_room(room_id) - - print(f"✓ 玩家已离开房间: {player_id} <- {room_id}") - return True - - except Exception as e: - print(f"✗ 玩家离开房间失败: {e}") - self.room_stats["room_errors"] += 1 - return False - - def _verify_room_password(self, room_id: str, player_data: Dict[str, Any]) -> bool: - """ - 验证房间密码 - - Args: - room_id: 房间ID - player_data: 玩家数据 - - Returns: - 密码是否正确 - """ - try: - if not self.room_config["enable_room_passwords"]: - return True - - if room_id not in self.rooms: - return False - - room_data = self.rooms[room_id] - if not room_data["password"]: - return True - - player_password = player_data.get("password") if player_data else None - return player_password == room_data["password"] - - except Exception as e: - print(f"✗ 房间密码验证失败: {e}") - self.room_stats["room_errors"] += 1 - return False - - def get_room_info(self, room_id: str) -> Optional[Dict[str, Any]]: - """ - 获取房间信息 - - Args: - room_id: 房间ID - - Returns: - 房间信息或None - """ - try: - if room_id in self.rooms: - room_data = self.rooms[room_id] - # 返回不包含敏感信息的房间信息 - safe_room_data = { - "room_id": room_data["room_id"], - "room_name": room_data["room_name"], - "settings": room_data["settings"], - "player_count": len(room_data["players"]), - "has_password": bool(room_data["password"]), - "host_player_id": room_data["host_player_id"], - "created_time": room_data["created_time"], - "last_activity": room_data["last_activity"] - } - return safe_room_data - else: - return None - - except Exception as e: - print(f"✗ 获取房间信息失败: {e}") - self.room_stats["room_errors"] += 1 - return None - - def get_player_room(self, player_id: str) -> Optional[str]: - """ - 获取玩家所在房间 - - Args: - player_id: 玩家ID - - Returns: - 房间ID或None - """ - try: - return self.player_rooms.get(player_id) - except Exception as e: - print(f"✗ 获取玩家房间失败: {e}") - self.room_stats["room_errors"] += 1 - return None - - def get_room_players(self, room_id: str) -> List[str]: - """ - 获取房间内所有玩家 - - Args: - room_id: 房间ID - - Returns: - 玩家ID列表 - """ - try: - if room_id in self.rooms: - return list(self.rooms[room_id]["players"].keys()) - else: - return [] - - except Exception as e: - print(f"✗ 获取房间玩家失败: {e}") - self.room_stats["room_errors"] += 1 - return [] - - def update_room_state(self, room_id: str, state_data: Dict[str, Any]) -> bool: - """ - 更新房间状态 - - Args: - room_id: 房间ID - state_data: 状态数据 - - Returns: - 是否更新成功 - """ - try: - if room_id not in self.rooms: - return False - - self.rooms[room_id]["room_state"].update(state_data) - self.rooms[room_id]["last_activity"] = time.time() - return True - - except Exception as e: - print(f"✗ 更新房间状态失败: {e}") - self.room_stats["room_errors"] += 1 - return False - - def set_room_custom_data(self, room_id: str, key: str, value: Any) -> bool: - """ - 设置房间自定义数据 - - Args: - room_id: 房间ID - key: 数据键 - value: 数据值 - - Returns: - 是否设置成功 - """ - try: - if room_id not in self.rooms: - return False - - self.rooms[room_id]["custom_data"][key] = value - return True - - except Exception as e: - print(f"✗ 设置房间自定义数据失败: {e}") - self.room_stats["room_errors"] += 1 - return False - - def get_room_custom_data(self, room_id: str, key: str, default: Any = None) -> Any: - """ - 获取房间自定义数据 - - Args: - room_id: 房间ID - key: 数据键 - default: 默认值 - - Returns: - 数据值或默认值 - """ - try: - if room_id not in self.rooms: - return default - - return self.rooms[room_id]["custom_data"].get(key, default) - - except Exception as e: - print(f"✗ 获取房间自定义数据失败: {e}") - self.room_stats["room_errors"] += 1 - return default - - def get_room_stats(self) -> Dict[str, Any]: - """ - 获取房间统计信息 - - Returns: - 房间统计字典 - """ - return { - "state": self.room_state.copy(), - "stats": self.room_stats.copy(), - "config": self.room_config.copy(), - "current_rooms": len(self.rooms), - "current_players": len(self.player_rooms) - } - - def reset_stats(self): - """重置房间统计信息""" - try: - self.room_stats = { - "rooms_created": 0, - "rooms_destroyed": 0, - "players_added": 0, - "players_removed": 0, - "room_errors": 0 - } - print("✓ 房间统计信息已重置") - except Exception as e: - print(f"✗ 房间统计信息重置失败: {e}") - - def set_room_config(self, config: Dict[str, Any]) -> bool: - """ - 设置房间配置 - - Args: - config: 房间配置字典 - - Returns: - 是否设置成功 - """ - try: - self.room_config.update(config) - print(f"✓ 房间配置已更新: {self.room_config}") - return True - except Exception as e: - print(f"✗ 房间配置设置失败: {e}") - return False - - def get_room_config(self) -> Dict[str, Any]: - """ - 获取房间配置 - - Returns: - 房间配置字典 - """ - return self.room_config.copy() - - def _trigger_room_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发房间回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.room_callbacks: - for callback in self.room_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 房间回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 房间回调触发失败: {e}") - - def register_room_callback(self, callback_type: str, callback: callable): - """ - 注册房间回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.room_callbacks: - self.room_callbacks[callback_type].append(callback) - print(f"✓ 房间回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 房间回调注册失败: {e}") - - def unregister_room_callback(self, callback_type: str, callback: callable): - """ - 注销房间回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.room_callbacks: - if callback in self.room_callbacks[callback_type]: - self.room_callbacks[callback_type].remove(callback) - print(f"✓ 房间回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +房间分配器模块 +为匹配成功的玩家创建和分配房间 +""" + +import time +import uuid +from typing import Dict, Any, List, Optional + +class RoomAllocator: + """ + 房间分配器 + 为匹配成功的玩家创建和分配房间 + """ + + def __init__(self, plugin): + """ + 初始化房间分配器 + + Args: + plugin: 匹配系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 房间配置 + self.room_config = { + "default_max_players": 8, + "enable_room_passwords": True, + "enable_room_visibility": True, + "room_timeout": 3600, # 1小时无活动房间自动关闭 + "max_rooms": 1000, + "enable_persistent_rooms": False + } + + # 房间状态 + self.room_state = { + "total_rooms": 0, + "active_rooms": 0, + "occupied_rooms": 0, + "available_rooms": 0 + } + + # 房间存储 + self.rooms = {} # 房间数据 + self.player_rooms = {} # 玩家所在房间映射 + + # 房间统计 + self.room_stats = { + "rooms_created": 0, + "rooms_destroyed": 0, + "players_added": 0, + "players_removed": 0, + "room_errors": 0 + } + + # 回调函数 + self.room_callbacks = { + "room_created": [], + "room_destroyed": [], + "player_added": [], + "player_removed": [], + "room_error": [] + } + + # 时间戳记录 + self.last_cleanup = 0.0 + self.last_stats_reset = 0.0 + + print("✓ 房间分配器已创建") + + def initialize(self) -> bool: + """ + 初始化房间分配器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化房间分配器...") + + self.initialized = True + print("✓ 房间分配器初始化完成") + return True + + except Exception as e: + print(f"✗ 房间分配器初始化失败: {e}") + self.room_stats["room_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用房间分配器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 房间分配器未初始化") + return False + + self.enabled = True + print("✓ 房间分配器已启用") + return True + + except Exception as e: + print(f"✗ 房间分配器启用失败: {e}") + self.room_stats["room_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用房间分配器""" + try: + self.enabled = False + + # 销毁所有房间 + self._destroy_all_rooms() + + print("✓ 房间分配器已禁用") + + except Exception as e: + print(f"✗ 房间分配器禁用失败: {e}") + self.room_stats["room_errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理房间分配器资源""" + try: + # 禁用房间分配器 + if self.enabled: + self.disable() + + # 清理回调 + self.room_callbacks.clear() + + self.initialized = False + print("✓ 房间分配器资源已清理") + + except Exception as e: + print(f"✗ 房间分配器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新房间分配器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 定期清理过期房间 + if current_time - self.last_cleanup >= 300.0: # 每5分钟清理一次 + self._cleanup_expired_rooms(current_time) + self.last_cleanup = current_time + + except Exception as e: + print(f"✗ 房间分配器更新失败: {e}") + self.room_stats["room_errors"] += 1 + import traceback + traceback.print_exc() + + def _cleanup_expired_rooms(self, current_time: float): + """清理过期房间""" + try: + expired_rooms = [] + for room_id, room_data in self.rooms.items(): + # 检查房间是否超时 + if current_time - room_data.get("last_activity", 0) > self.room_config["room_timeout"]: + # 检查房间是否为空 + if len(room_data.get("players", {})) == 0: + expired_rooms.append(room_id) + + # 销毁过期房间 + for room_id in expired_rooms: + self.destroy_room(room_id) + + except Exception as e: + print(f"✗ 过期房间清理失败: {e}") + self.room_stats["room_errors"] += 1 + + def _destroy_all_rooms(self): + """销毁所有房间""" + try: + room_ids = list(self.rooms.keys()) + for room_id in room_ids: + self.destroy_room(room_id) + except Exception as e: + print(f"✗ 所有房间销毁失败: {e}") + self.room_stats["room_errors"] += 1 + + def create_room(self, room_name: str = None, room_settings: Dict[str, Any] = None, + room_password: str = None) -> Optional[str]: + """ + 创建房间 + + Args: + room_name: 房间名称 + room_settings: 房间设置 + room_password: 房间密码 + + Returns: + 房间ID或None + """ + try: + if not self.enabled: + return None + + # 检查房间数量限制 + if len(self.rooms) >= self.room_config["max_rooms"]: + print("✗ 房间数量已达上限") + self.room_stats["room_errors"] += 1 + return None + + # 生成房间ID + room_id = f"room_{int(time.time() * 1000000)}" + + # 使用默认设置或传入设置 + settings = { + "max_players": self.room_config["default_max_players"], + "is_public": True, + "allow_spectators": True + } + + if room_settings: + settings.update(room_settings) + + # 创建房间数据 + room_data = { + "room_id": room_id, + "room_name": room_name or f"房间-{room_id[:8]}", + "settings": settings, + "password": room_password, + "players": {}, # 玩家ID到玩家数据的映射 + "host_player_id": None, + "created_time": time.time(), + "last_activity": time.time(), + "room_state": {}, # 房间状态数据 + "custom_data": {} # 自定义房间数据 + } + + # 添加到房间列表 + self.rooms[room_id] = room_data + + self.room_state["total_rooms"] += 1 + self.room_state["active_rooms"] += 1 + self.room_stats["rooms_created"] += 1 + + # 触发房间创建回调 + self._trigger_room_callback("room_created", { + "room_id": room_id, + "room_name": room_data["room_name"], + "settings": settings, + "timestamp": room_data["created_time"] + }) + + print(f"✓ 房间已创建: {room_id} ({room_data['room_name']})") + return room_id + + except Exception as e: + print(f"✗ 房间创建失败: {e}") + self.room_stats["room_errors"] += 1 + return None + + def destroy_room(self, room_id: str) -> bool: + """ + 销毁房间 + + Args: + room_id: 房间ID + + Returns: + 是否销毁成功 + """ + try: + if not self.enabled: + return False + + # 检查房间是否存在 + if room_id not in self.rooms: + print(f"✗ 房间不存在: {room_id}") + return False + + room_data = self.rooms[room_id] + + # 通知房间内所有玩家 + player_ids = list(room_data["players"].keys()) + for player_id in player_ids: + self._notify_player_room_destroyed(player_id, room_id) + + # 移除房间 + del self.rooms[room_id] + + self.room_state["active_rooms"] -= 1 + self.room_stats["rooms_destroyed"] += 1 + + # 触发房间销毁回调 + self._trigger_room_callback("room_destroyed", { + "room_id": room_id, + "room_name": room_data["room_name"], + "timestamp": time.time() + }) + + print(f"✓ 房间已销毁: {room_id}") + return True + + except Exception as e: + print(f"✗ 房间销毁失败: {e}") + self.room_stats["room_errors"] += 1 + return False + + def _notify_player_room_destroyed(self, player_id: str, room_id: str): + """ + 通知玩家房间被销毁 + + Args: + player_id: 玩家ID + room_id: 房间ID + """ + try: + # 从玩家房间映射中移除 + if player_id in self.player_rooms: + del self.player_rooms[player_id] + + except Exception as e: + print(f"✗ 玩家房间销毁通知失败: {e}") + self.room_stats["room_errors"] += 1 + + def add_client_to_room(self, room_id: str, player_id: str, player_data: Dict[str, Any] = None) -> bool: + """ + 添加玩家到房间 + + Args: + room_id: 房间ID + player_id: 玩家ID + player_data: 玩家数据 + + Returns: + 是否添加成功 + """ + try: + if not self.enabled: + return False + + # 检查房间是否存在 + if room_id not in self.rooms: + print(f"✗ 房间不存在: {room_id}") + self.room_stats["room_errors"] += 1 + return False + + room_data = self.rooms[room_id] + + # 检查房间是否已满 + if len(room_data["players"]) >= room_data["settings"]["max_players"]: + print(f"✗ 房间已满: {room_id}") + return False + + # 检查房间密码 + if room_data["password"] and not self._verify_room_password(room_id, player_data): + print(f"✗ 房间密码错误: {room_id}") + return False + + # 添加玩家到房间 + player_info = player_data or {} + player_info["join_time"] = time.time() + player_info["last_activity"] = time.time() + + room_data["players"][player_id] = player_info + room_data["last_activity"] = time.time() + + # 设置房间主机(如果是第一个玩家) + if room_data["host_player_id"] is None: + room_data["host_player_id"] = player_id + + # 更新玩家房间映射 + self.player_rooms[player_id] = room_id + + self.room_stats["players_added"] += 1 + + # 触发玩家添加回调 + self._trigger_room_callback("player_added", { + "room_id": room_id, + "room_name": room_data["room_name"], + "player_id": player_id, + "timestamp": time.time() + }) + + print(f"✓ 玩家已加入房间: {player_id} -> {room_id}") + return True + + except Exception as e: + print(f"✗ 玩家加入房间失败: {e}") + self.room_stats["room_errors"] += 1 + return False + + def remove_client_from_room(self, room_id: str, player_id: str) -> bool: + """ + 从房间移除玩家 + + Args: + room_id: 房间ID + player_id: 玩家ID + + Returns: + 是否移除成功 + """ + try: + if not self.enabled: + return False + + # 检查房间是否存在 + if room_id not in self.rooms: + print(f"✗ 房间不存在: {room_id}") + return False + + room_data = self.rooms[room_id] + + # 检查玩家是否在房间中 + if player_id not in room_data["players"]: + print(f"✗ 玩家不在房间中: {player_id}") + return False + + # 移除玩家 + del room_data["players"][player_id] + room_data["last_activity"] = time.time() + + # 更新主机(如果移除的是主机) + if room_data["host_player_id"] == player_id: + if room_data["players"]: + # 选择第一个玩家作为新主机 + room_data["host_player_id"] = next(iter(room_data["players"])) + else: + room_data["host_player_id"] = None + + # 移除玩家房间映射 + if player_id in self.player_rooms: + del self.player_rooms[player_id] + + self.room_stats["players_removed"] += 1 + + # 触发玩家移除回调 + self._trigger_room_callback("player_removed", { + "room_id": room_id, + "room_name": room_data["room_name"], + "player_id": player_id, + "timestamp": time.time() + }) + + # 如果房间为空,销毁房间 + if len(room_data["players"]) == 0: + self.destroy_room(room_id) + + print(f"✓ 玩家已离开房间: {player_id} <- {room_id}") + return True + + except Exception as e: + print(f"✗ 玩家离开房间失败: {e}") + self.room_stats["room_errors"] += 1 + return False + + def _verify_room_password(self, room_id: str, player_data: Dict[str, Any]) -> bool: + """ + 验证房间密码 + + Args: + room_id: 房间ID + player_data: 玩家数据 + + Returns: + 密码是否正确 + """ + try: + if not self.room_config["enable_room_passwords"]: + return True + + if room_id not in self.rooms: + return False + + room_data = self.rooms[room_id] + if not room_data["password"]: + return True + + player_password = player_data.get("password") if player_data else None + return player_password == room_data["password"] + + except Exception as e: + print(f"✗ 房间密码验证失败: {e}") + self.room_stats["room_errors"] += 1 + return False + + def get_room_info(self, room_id: str) -> Optional[Dict[str, Any]]: + """ + 获取房间信息 + + Args: + room_id: 房间ID + + Returns: + 房间信息或None + """ + try: + if room_id in self.rooms: + room_data = self.rooms[room_id] + # 返回不包含敏感信息的房间信息 + safe_room_data = { + "room_id": room_data["room_id"], + "room_name": room_data["room_name"], + "settings": room_data["settings"], + "player_count": len(room_data["players"]), + "has_password": bool(room_data["password"]), + "host_player_id": room_data["host_player_id"], + "created_time": room_data["created_time"], + "last_activity": room_data["last_activity"] + } + return safe_room_data + else: + return None + + except Exception as e: + print(f"✗ 获取房间信息失败: {e}") + self.room_stats["room_errors"] += 1 + return None + + def get_player_room(self, player_id: str) -> Optional[str]: + """ + 获取玩家所在房间 + + Args: + player_id: 玩家ID + + Returns: + 房间ID或None + """ + try: + return self.player_rooms.get(player_id) + except Exception as e: + print(f"✗ 获取玩家房间失败: {e}") + self.room_stats["room_errors"] += 1 + return None + + def get_room_players(self, room_id: str) -> List[str]: + """ + 获取房间内所有玩家 + + Args: + room_id: 房间ID + + Returns: + 玩家ID列表 + """ + try: + if room_id in self.rooms: + return list(self.rooms[room_id]["players"].keys()) + else: + return [] + + except Exception as e: + print(f"✗ 获取房间玩家失败: {e}") + self.room_stats["room_errors"] += 1 + return [] + + def update_room_state(self, room_id: str, state_data: Dict[str, Any]) -> bool: + """ + 更新房间状态 + + Args: + room_id: 房间ID + state_data: 状态数据 + + Returns: + 是否更新成功 + """ + try: + if room_id not in self.rooms: + return False + + self.rooms[room_id]["room_state"].update(state_data) + self.rooms[room_id]["last_activity"] = time.time() + return True + + except Exception as e: + print(f"✗ 更新房间状态失败: {e}") + self.room_stats["room_errors"] += 1 + return False + + def set_room_custom_data(self, room_id: str, key: str, value: Any) -> bool: + """ + 设置房间自定义数据 + + Args: + room_id: 房间ID + key: 数据键 + value: 数据值 + + Returns: + 是否设置成功 + """ + try: + if room_id not in self.rooms: + return False + + self.rooms[room_id]["custom_data"][key] = value + return True + + except Exception as e: + print(f"✗ 设置房间自定义数据失败: {e}") + self.room_stats["room_errors"] += 1 + return False + + def get_room_custom_data(self, room_id: str, key: str, default: Any = None) -> Any: + """ + 获取房间自定义数据 + + Args: + room_id: 房间ID + key: 数据键 + default: 默认值 + + Returns: + 数据值或默认值 + """ + try: + if room_id not in self.rooms: + return default + + return self.rooms[room_id]["custom_data"].get(key, default) + + except Exception as e: + print(f"✗ 获取房间自定义数据失败: {e}") + self.room_stats["room_errors"] += 1 + return default + + def get_room_stats(self) -> Dict[str, Any]: + """ + 获取房间统计信息 + + Returns: + 房间统计字典 + """ + return { + "state": self.room_state.copy(), + "stats": self.room_stats.copy(), + "config": self.room_config.copy(), + "current_rooms": len(self.rooms), + "current_players": len(self.player_rooms) + } + + def reset_stats(self): + """重置房间统计信息""" + try: + self.room_stats = { + "rooms_created": 0, + "rooms_destroyed": 0, + "players_added": 0, + "players_removed": 0, + "room_errors": 0 + } + print("✓ 房间统计信息已重置") + except Exception as e: + print(f"✗ 房间统计信息重置失败: {e}") + + def set_room_config(self, config: Dict[str, Any]) -> bool: + """ + 设置房间配置 + + Args: + config: 房间配置字典 + + Returns: + 是否设置成功 + """ + try: + self.room_config.update(config) + print(f"✓ 房间配置已更新: {self.room_config}") + return True + except Exception as e: + print(f"✗ 房间配置设置失败: {e}") + return False + + def get_room_config(self) -> Dict[str, Any]: + """ + 获取房间配置 + + Returns: + 房间配置字典 + """ + return self.room_config.copy() + + def _trigger_room_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发房间回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.room_callbacks: + for callback in self.room_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 房间回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 房间回调触发失败: {e}") + + def register_room_callback(self, callback_type: str, callback: callable): + """ + 注册房间回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.room_callbacks: + self.room_callbacks[callback_type].append(callback) + print(f"✓ 房间回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 房间回调注册失败: {e}") + + def unregister_room_callback(self, callback_type: str, callback: callable): + """ + 注销房间回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.room_callbacks: + if callback in self.room_callbacks[callback_type]: + self.room_callbacks[callback_type].remove(callback) + print(f"✓ 房间回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 房间回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/matchmaking_system/rules/rule_manager.py b/plugins/user/matchmaking_system/rules/rule_manager.py index 31d5d21a..5c7172d9 100644 --- a/plugins/user/matchmaking_system/rules/rule_manager.py +++ b/plugins/user/matchmaking_system/rules/rule_manager.py @@ -1,633 +1,633 @@ -""" -匹配规则管理器模块 -管理匹配规则和参数 -""" - -import time -from typing import Dict, Any, List, Optional - -class RuleManager: - """ - 匹配规则管理器 - 管理匹配规则和参数 - """ - - def __init__(self, plugin): - """ - 初始化匹配规则管理器 - - Args: - plugin: 匹配系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 规则配置 - self.rule_config = { - "default_rules": { - "min_players": 2, - "max_players": 8, - "skill_range": 100, - "max_wait_time": 120.0, - "region_matching": True, - "platform_matching": True, - "language_matching": True - }, - "game_mode_rules": {}, # 不同游戏模式的规则 - "custom_rules": {}, # 自定义规则 - "enable_rule_validation": True, - "enable_dynamic_rules": True - } - - # 规则状态 - self.rule_state = { - "active_rules": "default", - "total_rules": 1, # 默认规则 - "custom_rules_count": 0, - "rules_modified": 0 - } - - # 规则统计 - self.rule_stats = { - "rules_applied": 0, - "rules_violated": 0, - "rule_errors": 0 - } - - # 回调函数 - self.rule_callbacks = { - "rule_applied": [], - "rule_violated": [], - "rule_updated": [], - "rule_error": [] - } - - # 时间戳记录 - self.last_rule_application = 0.0 - self.last_stats_reset = 0.0 - - print("✓ 匹配规则管理器已创建") - - def initialize(self) -> bool: - """ - 初始化匹配规则管理器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化匹配规则管理器...") - - self.initialized = True - print("✓ 匹配规则管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 匹配规则管理器初始化失败: {e}") - self.rule_stats["rule_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用匹配规则管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 匹配规则管理器未初始化") - return False - - self.enabled = True - print("✓ 匹配规则管理器已启用") - return True - - except Exception as e: - print(f"✗ 匹配规则管理器启用失败: {e}") - self.rule_stats["rule_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用匹配规则管理器""" - try: - self.enabled = False - print("✓ 匹配规则管理器已禁用") - - except Exception as e: - print(f"✗ 匹配规则管理器禁用失败: {e}") - self.rule_stats["rule_errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理匹配规则管理器资源""" - try: - # 禁用匹配规则管理器 - if self.enabled: - self.disable() - - # 清理回调 - self.rule_callbacks.clear() - - self.initialized = False - print("✓ 匹配规则管理器资源已清理") - - except Exception as e: - print(f"✗ 匹配规则管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新匹配规则管理器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - self.last_rule_application = time.time() - - except Exception as e: - print(f"✗ 匹配规则管理器更新失败: {e}") - self.rule_stats["rule_errors"] += 1 - import traceback - traceback.print_exc() - - def apply_rules(self, players: List[Dict[str, Any]], game_mode: str = "default") -> bool: - """ - 应用匹配规则 - - Args: - players: 玩家列表 - game_mode: 游戏模式 - - Returns: - 是否符合规则 - """ - try: - if not self.enabled: - return True # 如果未启用,跳过规则检查 - - self.rule_stats["rules_applied"] += 1 - - # 获取适用的规则 - rules = self.get_active_rules(game_mode) - - # 检查玩家数量 - min_players = rules.get("min_players", 2) - max_players = rules.get("max_players", 8) - - if len(players) < min_players: - print(f"✗ 玩家数量不足: {len(players)} < {min_players}") - self.rule_stats["rules_violated"] += 1 - - # 触发规则违反回调 - self._trigger_rule_callback("rule_violated", { - "rule": "min_players", - "required": min_players, - "actual": len(players), - "timestamp": time.time() - }) - - return False - - if len(players) > max_players: - print(f"✗ 玩家数量超限: {len(players)} > {max_players}") - self.rule_stats["rules_violated"] += 1 - - # 触发规则违反回调 - self._trigger_rule_callback("rule_violated", { - "rule": "max_players", - "required": max_players, - "actual": len(players), - "timestamp": time.time() - }) - - return False - - # 检查技能范围 - if rules.get("skill_range") is not None: - skill_range = rules["skill_range"] - skills = [p.get("skill_level", 0) for p in players] - min_skill = min(skills) - max_skill = max(skills) - - if max_skill - min_skill > skill_range: - print(f"✗ 技能范围超限: {max_skill - min_skill} > {skill_range}") - self.rule_stats["rules_violated"] += 1 - - # 触发规则违反回调 - self._trigger_rule_callback("rule_violated", { - "rule": "skill_range", - "required": skill_range, - "actual": max_skill - min_skill, - "timestamp": time.time() - }) - - return False - - # 检查区域匹配 - if rules.get("region_matching", False): - regions = [p.get("region", "global") for p in players] - if len(set(regions)) > 1: - print(f"✗ 区域不匹配: {set(regions)}") - self.rule_stats["rules_violated"] += 1 - - # 触发规则违反回调 - self._trigger_rule_callback("rule_violated", { - "rule": "region_matching", - "regions": list(set(regions)), - "timestamp": time.time() - }) - - return False - - # 检查平台匹配 - if rules.get("platform_matching", False): - platforms = [p.get("platform", "pc") for p in players] - if len(set(platforms)) > 1: - print(f"✗ 平台不匹配: {set(platforms)}") - self.rule_stats["rules_violated"] += 1 - - # 触发规则违反回调 - self._trigger_rule_callback("rule_violated", { - "rule": "platform_matching", - "platforms": list(set(platforms)), - "timestamp": time.time() - }) - - return False - - # 检查语言匹配 - if rules.get("language_matching", False): - languages = [p.get("language", "en") for p in players] - if len(set(languages)) > 1: - print(f"✗ 语言不匹配: {set(languages)}") - self.rule_stats["rules_violated"] += 1 - - # 触发规则违反回调 - self._trigger_rule_callback("rule_violated", { - "rule": "language_matching", - "languages": list(set(languages)), - "timestamp": time.time() - }) - - return False - - # 触发规则应用回调 - self._trigger_rule_callback("rule_applied", { - "players": len(players), - "game_mode": game_mode, - "rules": rules, - "timestamp": time.time() - }) - - print(f"✓ 规则检查通过: {len(players)}名玩家, 游戏模式: {game_mode}") - return True - - except Exception as e: - print(f"✗ 规则应用失败: {e}") - self.rule_stats["rule_errors"] += 1 - return False - - def get_active_rules(self, game_mode: str = "default") -> Dict[str, Any]: - """ - 获取活动规则 - - Args: - game_mode: 游戏模式 - - Returns: - 规则字典 - """ - try: - # 检查是否有特定游戏模式的规则 - if game_mode in self.rule_config["game_mode_rules"]: - return self.rule_config["game_mode_rules"][game_mode] - - # 检查是否有自定义规则 - if self.rule_state["active_rules"] in self.rule_config["custom_rules"]: - return self.rule_config["custom_rules"][self.rule_state["active_rules"]] - - # 返回默认规则 - return self.rule_config["default_rules"] - - except Exception as e: - print(f"✗ 获取活动规则失败: {e}") - self.rule_stats["rule_errors"] += 1 - return self.rule_config["default_rules"] - - def set_active_rules(self, rule_set_name: str) -> bool: - """ - 设置活动规则集 - - Args: - rule_set_name: 规则集名称 - - Returns: - 是否设置成功 - """ - try: - # 验证规则集存在 - if (rule_set_name != "default" and - rule_set_name not in self.rule_config["custom_rules"] and - rule_set_name not in self.rule_config["game_mode_rules"]): - print(f"✗ 规则集不存在: {rule_set_name}") - return False - - self.rule_state["active_rules"] = rule_set_name - - # 触发规则更新回调 - self._trigger_rule_callback("rule_updated", { - "rule_set": rule_set_name, - "timestamp": time.time() - }) - - print(f"✓ 活动规则集已设置为: {rule_set_name}") - return True - - except Exception as e: - print(f"✗ 活动规则集设置失败: {e}") - self.rule_stats["rule_errors"] += 1 - return False - - def add_custom_rule_set(self, rule_set_name: str, rules: Dict[str, Any]) -> bool: - """ - 添加自定义规则集 - - Args: - rule_set_name: 规则集名称 - rules: 规则字典 - - Returns: - 是否添加成功 - """ - try: - if not self.enabled: - return False - - # 验证规则 - if self.rule_config["enable_rule_validation"]: - if not self._validate_rules(rules): - print(f"✗ 规则验证失败: {rule_set_name}") - self.rule_stats["rule_errors"] += 1 - return False - - self.rule_config["custom_rules"][rule_set_name] = rules - self.rule_state["custom_rules_count"] = len(self.rule_config["custom_rules"]) - self.rule_state["total_rules"] += 1 - self.rule_state["rules_modified"] += 1 - - print(f"✓ 自定义规则集已添加: {rule_set_name}") - return True - - except Exception as e: - print(f"✗ 自定义规则集添加失败: {e}") - self.rule_stats["rule_errors"] += 1 - return False - - def remove_custom_rule_set(self, rule_set_name: str) -> bool: - """ - 移除自定义规则集 - - Args: - rule_set_name: 规则集名称 - - Returns: - 是否移除成功 - """ - try: - if rule_set_name in self.rule_config["custom_rules"]: - del self.rule_config["custom_rules"][rule_set_name] - self.rule_state["custom_rules_count"] = len(self.rule_config["custom_rules"]) - self.rule_state["total_rules"] -= 1 - self.rule_state["rules_modified"] += 1 - - # 如果删除的是活动规则集,切换回默认规则 - if self.rule_state["active_rules"] == rule_set_name: - self.rule_state["active_rules"] = "default" - - print(f"✓ 自定义规则集已移除: {rule_set_name}") - return True - else: - print(f"✗ 自定义规则集不存在: {rule_set_name}") - return False - - except Exception as e: - print(f"✗ 自定义规则集移除失败: {e}") - self.rule_stats["rule_errors"] += 1 - return False - - def _validate_rules(self, rules: Dict[str, Any]) -> bool: - """ - 验证规则 - - Args: - rules: 规则字典 - - Returns: - 是否验证通过 - """ - try: - # 检查基本规则参数 - if "min_players" in rules: - if not isinstance(rules["min_players"], int) or rules["min_players"] < 1: - return False - - if "max_players" in rules: - if not isinstance(rules["max_players"], int) or rules["max_players"] < 1: - return False - - if "min_players" in rules and "max_players" in rules: - if rules["min_players"] > rules["max_players"]: - return False - - if "skill_range" in rules: - if not isinstance(rules["skill_range"], (int, float)) or rules["skill_range"] < 0: - return False - - if "max_wait_time" in rules: - if not isinstance(rules["max_wait_time"], (int, float)) or rules["max_wait_time"] < 0: - return False - - return True - - except Exception as e: - print(f"✗ 规则验证失败: {e}") - self.rule_stats["rule_errors"] += 1 - return False - - def add_game_mode_rules(self, game_mode: str, rules: Dict[str, Any]) -> bool: - """ - 添加游戏模式规则 - - Args: - game_mode: 游戏模式 - rules: 规则字典 - - Returns: - 是否添加成功 - """ - try: - if not self.enabled: - return False - - # 验证规则 - if self.rule_config["enable_rule_validation"]: - if not self._validate_rules(rules): - print(f"✗ 游戏模式规则验证失败: {game_mode}") - self.rule_stats["rule_errors"] += 1 - return False - - self.rule_config["game_mode_rules"][game_mode] = rules - self.rule_state["total_rules"] += 1 - self.rule_state["rules_modified"] += 1 - - print(f"✓ 游戏模式规则已添加: {game_mode}") - return True - - except Exception as e: - print(f"✗ 游戏模式规则添加失败: {e}") - self.rule_stats["rule_errors"] += 1 - return False - - def remove_game_mode_rules(self, game_mode: str) -> bool: - """ - 移除游戏模式规则 - - Args: - game_mode: 游戏模式 - - Returns: - 是否移除成功 - """ - try: - if game_mode in self.rule_config["game_mode_rules"]: - del self.rule_config["game_mode_rules"][game_mode] - self.rule_state["total_rules"] -= 1 - self.rule_state["rules_modified"] += 1 - - print(f"✓ 游戏模式规则已移除: {game_mode}") - return True - else: - print(f"✗ 游戏模式规则不存在: {game_mode}") - return False - - except Exception as e: - print(f"✗ 游戏模式规则移除失败: {e}") - self.rule_stats["rule_errors"] += 1 - return False - - def get_rule_stats(self) -> Dict[str, Any]: - """ - 获取规则统计信息 - - Returns: - 规则统计字典 - """ - return { - "state": self.rule_state.copy(), - "stats": self.rule_stats.copy(), - "config": self.rule_config.copy() - } - - def reset_stats(self): - """重置规则统计信息""" - try: - self.rule_stats = { - "rules_applied": 0, - "rules_violated": 0, - "rule_errors": 0 - } - print("✓ 规则统计信息已重置") - except Exception as e: - print(f"✗ 规则统计信息重置失败: {e}") - - def set_rule_config(self, config: Dict[str, Any]) -> bool: - """ - 设置规则配置 - - Args: - config: 规则配置字典 - - Returns: - 是否设置成功 - """ - try: - self.rule_config.update(config) - print(f"✓ 规则配置已更新: {self.rule_config}") - return True - except Exception as e: - print(f"✗ 规则配置设置失败: {e}") - return False - - def get_rule_config(self) -> Dict[str, Any]: - """ - 获取规则配置 - - Returns: - 规则配置字典 - """ - return self.rule_config.copy() - - def _trigger_rule_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发规则回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.rule_callbacks: - for callback in self.rule_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 规则回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 规则回调触发失败: {e}") - - def register_rule_callback(self, callback_type: str, callback: callable): - """ - 注册规则回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.rule_callbacks: - self.rule_callbacks[callback_type].append(callback) - print(f"✓ 规则回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 规则回调注册失败: {e}") - - def unregister_rule_callback(self, callback_type: str, callback: callable): - """ - 注销规则回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.rule_callbacks: - if callback in self.rule_callbacks[callback_type]: - self.rule_callbacks[callback_type].remove(callback) - print(f"✓ 规则回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +匹配规则管理器模块 +管理匹配规则和参数 +""" + +import time +from typing import Dict, Any, List, Optional + +class RuleManager: + """ + 匹配规则管理器 + 管理匹配规则和参数 + """ + + def __init__(self, plugin): + """ + 初始化匹配规则管理器 + + Args: + plugin: 匹配系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 规则配置 + self.rule_config = { + "default_rules": { + "min_players": 2, + "max_players": 8, + "skill_range": 100, + "max_wait_time": 120.0, + "region_matching": True, + "platform_matching": True, + "language_matching": True + }, + "game_mode_rules": {}, # 不同游戏模式的规则 + "custom_rules": {}, # 自定义规则 + "enable_rule_validation": True, + "enable_dynamic_rules": True + } + + # 规则状态 + self.rule_state = { + "active_rules": "default", + "total_rules": 1, # 默认规则 + "custom_rules_count": 0, + "rules_modified": 0 + } + + # 规则统计 + self.rule_stats = { + "rules_applied": 0, + "rules_violated": 0, + "rule_errors": 0 + } + + # 回调函数 + self.rule_callbacks = { + "rule_applied": [], + "rule_violated": [], + "rule_updated": [], + "rule_error": [] + } + + # 时间戳记录 + self.last_rule_application = 0.0 + self.last_stats_reset = 0.0 + + print("✓ 匹配规则管理器已创建") + + def initialize(self) -> bool: + """ + 初始化匹配规则管理器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化匹配规则管理器...") + + self.initialized = True + print("✓ 匹配规则管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 匹配规则管理器初始化失败: {e}") + self.rule_stats["rule_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用匹配规则管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 匹配规则管理器未初始化") + return False + + self.enabled = True + print("✓ 匹配规则管理器已启用") + return True + + except Exception as e: + print(f"✗ 匹配规则管理器启用失败: {e}") + self.rule_stats["rule_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用匹配规则管理器""" + try: + self.enabled = False + print("✓ 匹配规则管理器已禁用") + + except Exception as e: + print(f"✗ 匹配规则管理器禁用失败: {e}") + self.rule_stats["rule_errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理匹配规则管理器资源""" + try: + # 禁用匹配规则管理器 + if self.enabled: + self.disable() + + # 清理回调 + self.rule_callbacks.clear() + + self.initialized = False + print("✓ 匹配规则管理器资源已清理") + + except Exception as e: + print(f"✗ 匹配规则管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新匹配规则管理器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + self.last_rule_application = time.time() + + except Exception as e: + print(f"✗ 匹配规则管理器更新失败: {e}") + self.rule_stats["rule_errors"] += 1 + import traceback + traceback.print_exc() + + def apply_rules(self, players: List[Dict[str, Any]], game_mode: str = "default") -> bool: + """ + 应用匹配规则 + + Args: + players: 玩家列表 + game_mode: 游戏模式 + + Returns: + 是否符合规则 + """ + try: + if not self.enabled: + return True # 如果未启用,跳过规则检查 + + self.rule_stats["rules_applied"] += 1 + + # 获取适用的规则 + rules = self.get_active_rules(game_mode) + + # 检查玩家数量 + min_players = rules.get("min_players", 2) + max_players = rules.get("max_players", 8) + + if len(players) < min_players: + print(f"✗ 玩家数量不足: {len(players)} < {min_players}") + self.rule_stats["rules_violated"] += 1 + + # 触发规则违反回调 + self._trigger_rule_callback("rule_violated", { + "rule": "min_players", + "required": min_players, + "actual": len(players), + "timestamp": time.time() + }) + + return False + + if len(players) > max_players: + print(f"✗ 玩家数量超限: {len(players)} > {max_players}") + self.rule_stats["rules_violated"] += 1 + + # 触发规则违反回调 + self._trigger_rule_callback("rule_violated", { + "rule": "max_players", + "required": max_players, + "actual": len(players), + "timestamp": time.time() + }) + + return False + + # 检查技能范围 + if rules.get("skill_range") is not None: + skill_range = rules["skill_range"] + skills = [p.get("skill_level", 0) for p in players] + min_skill = min(skills) + max_skill = max(skills) + + if max_skill - min_skill > skill_range: + print(f"✗ 技能范围超限: {max_skill - min_skill} > {skill_range}") + self.rule_stats["rules_violated"] += 1 + + # 触发规则违反回调 + self._trigger_rule_callback("rule_violated", { + "rule": "skill_range", + "required": skill_range, + "actual": max_skill - min_skill, + "timestamp": time.time() + }) + + return False + + # 检查区域匹配 + if rules.get("region_matching", False): + regions = [p.get("region", "global") for p in players] + if len(set(regions)) > 1: + print(f"✗ 区域不匹配: {set(regions)}") + self.rule_stats["rules_violated"] += 1 + + # 触发规则违反回调 + self._trigger_rule_callback("rule_violated", { + "rule": "region_matching", + "regions": list(set(regions)), + "timestamp": time.time() + }) + + return False + + # 检查平台匹配 + if rules.get("platform_matching", False): + platforms = [p.get("platform", "pc") for p in players] + if len(set(platforms)) > 1: + print(f"✗ 平台不匹配: {set(platforms)}") + self.rule_stats["rules_violated"] += 1 + + # 触发规则违反回调 + self._trigger_rule_callback("rule_violated", { + "rule": "platform_matching", + "platforms": list(set(platforms)), + "timestamp": time.time() + }) + + return False + + # 检查语言匹配 + if rules.get("language_matching", False): + languages = [p.get("language", "en") for p in players] + if len(set(languages)) > 1: + print(f"✗ 语言不匹配: {set(languages)}") + self.rule_stats["rules_violated"] += 1 + + # 触发规则违反回调 + self._trigger_rule_callback("rule_violated", { + "rule": "language_matching", + "languages": list(set(languages)), + "timestamp": time.time() + }) + + return False + + # 触发规则应用回调 + self._trigger_rule_callback("rule_applied", { + "players": len(players), + "game_mode": game_mode, + "rules": rules, + "timestamp": time.time() + }) + + print(f"✓ 规则检查通过: {len(players)}名玩家, 游戏模式: {game_mode}") + return True + + except Exception as e: + print(f"✗ 规则应用失败: {e}") + self.rule_stats["rule_errors"] += 1 + return False + + def get_active_rules(self, game_mode: str = "default") -> Dict[str, Any]: + """ + 获取活动规则 + + Args: + game_mode: 游戏模式 + + Returns: + 规则字典 + """ + try: + # 检查是否有特定游戏模式的规则 + if game_mode in self.rule_config["game_mode_rules"]: + return self.rule_config["game_mode_rules"][game_mode] + + # 检查是否有自定义规则 + if self.rule_state["active_rules"] in self.rule_config["custom_rules"]: + return self.rule_config["custom_rules"][self.rule_state["active_rules"]] + + # 返回默认规则 + return self.rule_config["default_rules"] + + except Exception as e: + print(f"✗ 获取活动规则失败: {e}") + self.rule_stats["rule_errors"] += 1 + return self.rule_config["default_rules"] + + def set_active_rules(self, rule_set_name: str) -> bool: + """ + 设置活动规则集 + + Args: + rule_set_name: 规则集名称 + + Returns: + 是否设置成功 + """ + try: + # 验证规则集存在 + if (rule_set_name != "default" and + rule_set_name not in self.rule_config["custom_rules"] and + rule_set_name not in self.rule_config["game_mode_rules"]): + print(f"✗ 规则集不存在: {rule_set_name}") + return False + + self.rule_state["active_rules"] = rule_set_name + + # 触发规则更新回调 + self._trigger_rule_callback("rule_updated", { + "rule_set": rule_set_name, + "timestamp": time.time() + }) + + print(f"✓ 活动规则集已设置为: {rule_set_name}") + return True + + except Exception as e: + print(f"✗ 活动规则集设置失败: {e}") + self.rule_stats["rule_errors"] += 1 + return False + + def add_custom_rule_set(self, rule_set_name: str, rules: Dict[str, Any]) -> bool: + """ + 添加自定义规则集 + + Args: + rule_set_name: 规则集名称 + rules: 规则字典 + + Returns: + 是否添加成功 + """ + try: + if not self.enabled: + return False + + # 验证规则 + if self.rule_config["enable_rule_validation"]: + if not self._validate_rules(rules): + print(f"✗ 规则验证失败: {rule_set_name}") + self.rule_stats["rule_errors"] += 1 + return False + + self.rule_config["custom_rules"][rule_set_name] = rules + self.rule_state["custom_rules_count"] = len(self.rule_config["custom_rules"]) + self.rule_state["total_rules"] += 1 + self.rule_state["rules_modified"] += 1 + + print(f"✓ 自定义规则集已添加: {rule_set_name}") + return True + + except Exception as e: + print(f"✗ 自定义规则集添加失败: {e}") + self.rule_stats["rule_errors"] += 1 + return False + + def remove_custom_rule_set(self, rule_set_name: str) -> bool: + """ + 移除自定义规则集 + + Args: + rule_set_name: 规则集名称 + + Returns: + 是否移除成功 + """ + try: + if rule_set_name in self.rule_config["custom_rules"]: + del self.rule_config["custom_rules"][rule_set_name] + self.rule_state["custom_rules_count"] = len(self.rule_config["custom_rules"]) + self.rule_state["total_rules"] -= 1 + self.rule_state["rules_modified"] += 1 + + # 如果删除的是活动规则集,切换回默认规则 + if self.rule_state["active_rules"] == rule_set_name: + self.rule_state["active_rules"] = "default" + + print(f"✓ 自定义规则集已移除: {rule_set_name}") + return True + else: + print(f"✗ 自定义规则集不存在: {rule_set_name}") + return False + + except Exception as e: + print(f"✗ 自定义规则集移除失败: {e}") + self.rule_stats["rule_errors"] += 1 + return False + + def _validate_rules(self, rules: Dict[str, Any]) -> bool: + """ + 验证规则 + + Args: + rules: 规则字典 + + Returns: + 是否验证通过 + """ + try: + # 检查基本规则参数 + if "min_players" in rules: + if not isinstance(rules["min_players"], int) or rules["min_players"] < 1: + return False + + if "max_players" in rules: + if not isinstance(rules["max_players"], int) or rules["max_players"] < 1: + return False + + if "min_players" in rules and "max_players" in rules: + if rules["min_players"] > rules["max_players"]: + return False + + if "skill_range" in rules: + if not isinstance(rules["skill_range"], (int, float)) or rules["skill_range"] < 0: + return False + + if "max_wait_time" in rules: + if not isinstance(rules["max_wait_time"], (int, float)) or rules["max_wait_time"] < 0: + return False + + return True + + except Exception as e: + print(f"✗ 规则验证失败: {e}") + self.rule_stats["rule_errors"] += 1 + return False + + def add_game_mode_rules(self, game_mode: str, rules: Dict[str, Any]) -> bool: + """ + 添加游戏模式规则 + + Args: + game_mode: 游戏模式 + rules: 规则字典 + + Returns: + 是否添加成功 + """ + try: + if not self.enabled: + return False + + # 验证规则 + if self.rule_config["enable_rule_validation"]: + if not self._validate_rules(rules): + print(f"✗ 游戏模式规则验证失败: {game_mode}") + self.rule_stats["rule_errors"] += 1 + return False + + self.rule_config["game_mode_rules"][game_mode] = rules + self.rule_state["total_rules"] += 1 + self.rule_state["rules_modified"] += 1 + + print(f"✓ 游戏模式规则已添加: {game_mode}") + return True + + except Exception as e: + print(f"✗ 游戏模式规则添加失败: {e}") + self.rule_stats["rule_errors"] += 1 + return False + + def remove_game_mode_rules(self, game_mode: str) -> bool: + """ + 移除游戏模式规则 + + Args: + game_mode: 游戏模式 + + Returns: + 是否移除成功 + """ + try: + if game_mode in self.rule_config["game_mode_rules"]: + del self.rule_config["game_mode_rules"][game_mode] + self.rule_state["total_rules"] -= 1 + self.rule_state["rules_modified"] += 1 + + print(f"✓ 游戏模式规则已移除: {game_mode}") + return True + else: + print(f"✗ 游戏模式规则不存在: {game_mode}") + return False + + except Exception as e: + print(f"✗ 游戏模式规则移除失败: {e}") + self.rule_stats["rule_errors"] += 1 + return False + + def get_rule_stats(self) -> Dict[str, Any]: + """ + 获取规则统计信息 + + Returns: + 规则统计字典 + """ + return { + "state": self.rule_state.copy(), + "stats": self.rule_stats.copy(), + "config": self.rule_config.copy() + } + + def reset_stats(self): + """重置规则统计信息""" + try: + self.rule_stats = { + "rules_applied": 0, + "rules_violated": 0, + "rule_errors": 0 + } + print("✓ 规则统计信息已重置") + except Exception as e: + print(f"✗ 规则统计信息重置失败: {e}") + + def set_rule_config(self, config: Dict[str, Any]) -> bool: + """ + 设置规则配置 + + Args: + config: 规则配置字典 + + Returns: + 是否设置成功 + """ + try: + self.rule_config.update(config) + print(f"✓ 规则配置已更新: {self.rule_config}") + return True + except Exception as e: + print(f"✗ 规则配置设置失败: {e}") + return False + + def get_rule_config(self) -> Dict[str, Any]: + """ + 获取规则配置 + + Returns: + 规则配置字典 + """ + return self.rule_config.copy() + + def _trigger_rule_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发规则回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.rule_callbacks: + for callback in self.rule_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 规则回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 规则回调触发失败: {e}") + + def register_rule_callback(self, callback_type: str, callback: callable): + """ + 注册规则回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.rule_callbacks: + self.rule_callbacks[callback_type].append(callback) + print(f"✓ 规则回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 规则回调注册失败: {e}") + + def unregister_rule_callback(self, callback_type: str, callback: callable): + """ + 注销规则回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.rule_callbacks: + if callback in self.rule_callbacks[callback_type]: + self.rule_callbacks[callback_type].remove(callback) + print(f"✓ 规则回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 规则回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/matchmaking_system/stats/stats_manager.py b/plugins/user/matchmaking_system/stats/stats_manager.py index 74e57670..2d48513c 100644 --- a/plugins/user/matchmaking_system/stats/stats_manager.py +++ b/plugins/user/matchmaking_system/stats/stats_manager.py @@ -1,657 +1,657 @@ -""" -统计数据管理器模块 -收集和分析匹配数据 -""" - -import time -import json -from typing import Dict, Any, List, Optional -from collections import defaultdict, deque - -class StatsManager: - """ - 统计数据管理器 - 收集和分析匹配数据 - """ - - def __init__(self, plugin): - """ - 初始化统计数据管理器 - - Args: - plugin: 匹配系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 统计配置 - self.stats_config = { - "enable_statistics": True, - "data_retention_period": 86400, # 24小时 - "max_data_points": 10000, - "enable_realtime_stats": True, - "stats_collection_interval": 10.0, - "enable_export": True, - "export_format": "json" - } - - # 统计数据存储 - self.statistics = { - "match_times": deque(maxlen=1000), - "queue_times": deque(maxlen=1000), - "success_rates": deque(maxlen=1000), - "player_counts": deque(maxlen=1000), - "algorithm_performance": deque(maxlen=1000), - "system_performance": deque(maxlen=1000) - } - - # 统计状态 - self.stats_state = { - "total_matches": 0, - "successful_matches": 0, - "failed_matches": 0, - "total_players": 0, - "matched_players": 0, - "last_stats_update": 0.0 - } - - # 历史统计数据 - self.historical_stats = { - "daily": defaultdict(dict), - "weekly": defaultdict(dict), - "monthly": defaultdict(dict) - } - - # 统计报告 - self.stat_reports = { - "last_report": None, - "report_cache": {} - } - - # 统计回调 - self.stats_callbacks = { - "stats_updated": [], - "report_generated": [], - "data_exported": [], - "stats_error": [] - } - - # 时间戳记录 - self.last_stats_collection = 0.0 - self.last_report_generation = 0.0 - - print("✓ 统计数据管理器已创建") - - def initialize(self) -> bool: - """ - 初始化统计数据管理器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化统计数据管理器...") - - self.initialized = True - print("✓ 统计数据管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 统计数据管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用统计数据管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 统计数据管理器未初始化") - return False - - self.enabled = True - print("✓ 统计数据管理器已启用") - return True - - except Exception as e: - print(f"✗ 统计数据管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用统计数据管理器""" - try: - self.enabled = False - print("✓ 统计数据管理器已禁用") - - except Exception as e: - print(f"✗ 统计数据管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理统计数据管理器资源""" - try: - # 禁用统计数据管理器 - if self.enabled: - self.disable() - - # 清理回调 - self.stats_callbacks.clear() - - self.initialized = False - print("✓ 统计数据管理器资源已清理") - - except Exception as e: - print(f"✗ 统计数据管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新统计数据管理器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 定期收集统计数据 - if current_time - self.last_stats_collection >= self.stats_config["stats_collection_interval"]: - self._collect_statistics() - self.last_stats_collection = current_time - - self.stats_state["last_stats_update"] = current_time - - except Exception as e: - print(f"✗ 统计数据管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _collect_statistics(self): - """收集统计数据""" - try: - current_time = time.time() - stats_data = {} - - # 收集匹配管理器统计数据 - if self.plugin.match_manager: - match_stats = self.plugin.match_manager.get_match_stats() - match_state = match_stats.get("state", {}) - - stats_data["active_matches"] = match_state.get("active_matches", 0) - stats_data["successful_matches"] = match_state.get("successful_matches", 0) - stats_data["failed_matches"] = match_state.get("failed_matches", 0) - - # 更新总计数 - self.stats_state["total_matches"] = ( - match_state.get("successful_matches", 0) + - match_state.get("failed_matches", 0) - ) - self.stats_state["successful_matches"] = match_state.get("successful_matches", 0) - self.stats_state["failed_matches"] = match_state.get("failed_matches", 0) - - # 收集队列管理器统计数据 - if self.plugin.queue_manager: - queue_stats = self.plugin.queue_manager.get_queue_stats() - queue_state = queue_stats.get("state", {}) - - stats_data["total_players"] = queue_state.get("total_players", 0) - stats_data["players_matched"] = queue_state.get("players_matched", 0) - stats_data["players_timed_out"] = queue_state.get("players_timed_out", 0) - - # 更新总计数 - self.stats_state["total_players"] = queue_state.get("total_players", 0) - self.stats_state["matched_players"] = queue_state.get("players_matched", 0) - - # 收集算法管理器统计数据 - if self.plugin.algorithm_manager: - algorithm_stats = self.plugin.algorithm_manager.get_algorithm_stats() - algorithm_state = algorithm_stats.get("state", {}) - - stats_data["total_algorithms"] = algorithm_state.get("total_matches", 0) - stats_data["successful_algorithms"] = algorithm_state.get("successful_matches", 0) - stats_data["average_match_quality"] = algorithm_state.get("average_match_quality", 0) - - # 记录算法性能 - self.statistics["algorithm_performance"].append( - (current_time, stats_data["average_match_quality"]) - ) - - # 收集房间分配器统计数据 - if self.plugin.room_allocator: - room_stats = self.plugin.room_allocator.get_room_stats() - room_state = room_stats.get("state", {}) - - stats_data["active_rooms"] = room_state.get("active_rooms", 0) - stats_data["total_rooms"] = room_state.get("total_rooms", 0) - - # 记录统计数据 - self.statistics["player_counts"].append((current_time, stats_data.get("total_players", 0))) - - # 触发统计更新回调 - self._trigger_stats_callback("stats_updated", { - "stats_data": stats_data, - "timestamp": current_time - }) - - except Exception as e: - print(f"✗ 统计数据收集失败: {e}") - self._trigger_stats_callback("stats_error", { - "error": str(e), - "timestamp": time.time() - }) - - def record_match_time(self, match_time: float): - """ - 记录匹配时间 - - Args: - match_time: 匹配时间(秒) - """ - try: - current_time = time.time() - self.statistics["match_times"].append((current_time, match_time)) - except Exception as e: - print(f"✗ 匹配时间记录失败: {e}") - - def record_queue_time(self, queue_time: float): - """ - 记录队列时间 - - Args: - queue_time: 队列时间(秒) - """ - try: - current_time = time.time() - self.statistics["queue_times"].append((current_time, queue_time)) - except Exception as e: - print(f"✗ 队列时间记录失败: {e}") - - def record_success_rate(self, success_rate: float): - """ - 记录成功率 - - Args: - success_rate: 成功率(百分比) - """ - try: - current_time = time.time() - self.statistics["success_rates"].append((current_time, success_rate)) - except Exception as e: - print(f"✗ 成功率记录失败: {e}") - - def get_realtime_stats(self) -> Dict[str, Any]: - """ - 获取实时统计数据 - - Returns: - 实时统计数据字典 - """ - try: - realtime_stats = { - "current_time": time.time(), - "state": self.stats_state.copy(), - "statistics": {} - } - - # 获取最新的统计数据 - for stat_name, stat_data in self.statistics.items(): - if stat_data: - realtime_stats["statistics"][stat_name] = list(stat_data)[-10:] # 最近10个数据点 - else: - realtime_stats["statistics"][stat_name] = [] - - return realtime_stats - - except Exception as e: - print(f"✗ 实时统计数据获取失败: {e}") - return {} - - def generate_report(self, report_type: str = "summary", time_range: str = "24h") -> Dict[str, Any]: - """ - 生成统计报告 - - Args: - report_type: 报告类型(summary, detailed, custom) - time_range: 时间范围(24h, 7d, 30d) - - Returns: - 统计报告字典 - """ - try: - current_time = time.time() - report = { - "report_type": report_type, - "time_range": time_range, - "generated_time": current_time, - "data": {} - } - - # 根据报告类型生成不同内容 - if report_type == "summary": - report["data"] = self._generate_summary_report() - elif report_type == "detailed": - report["data"] = self._generate_detailed_report() - else: - report["data"] = self._generate_custom_report(report_type) - - self.stat_reports["last_report"] = report - self.stat_reports["report_cache"][f"{report_type}_{time_range}"] = report - - # 触发报告生成回调 - self._trigger_stats_callback("report_generated", { - "report": report, - "timestamp": current_time - }) - - return report - - except Exception as e: - print(f"✗ 统计报告生成失败: {e}") - self._trigger_stats_callback("stats_error", { - "error": str(e), - "timestamp": time.time() - }) - return {} - - def _generate_summary_report(self) -> Dict[str, Any]: - """生成摘要报告""" - try: - summary = { - "total_matches": self.stats_state["total_matches"], - "successful_matches": self.stats_state["successful_matches"], - "failed_matches": self.stats_state["failed_matches"], - "match_success_rate": 0.0, - "total_players": self.stats_state["total_players"], - "matched_players": self.stats_state["matched_players"], - "player_match_rate": 0.0, - "average_match_time": 0.0, - "average_queue_time": 0.0 - } - - # 计算成功率 - if self.stats_state["total_matches"] > 0: - summary["match_success_rate"] = ( - self.stats_state["successful_matches"] / self.stats_state["total_matches"] - ) * 100 - - # 计算玩家匹配率 - if self.stats_state["total_players"] > 0: - summary["player_match_rate"] = ( - self.stats_state["matched_players"] / self.stats_state["total_players"] - ) * 100 - - # 计算平均匹配时间 - if self.statistics["match_times"]: - times = [t[1] for t in self.statistics["match_times"]] - summary["average_match_time"] = sum(times) / len(times) - - # 计算平均队列时间 - if self.statistics["queue_times"]: - times = [t[1] for t in self.statistics["queue_times"]] - summary["average_queue_time"] = sum(times) / len(times) - - return summary - - except Exception as e: - print(f"✗ 摘要报告生成失败: {e}") - return {} - - def _generate_detailed_report(self) -> Dict[str, Any]: - """生成详细报告""" - try: - detailed = { - "summary": self._generate_summary_report(), - "trends": self._generate_trend_data(), - "performance": self._generate_performance_data(), - "historical": self._get_historical_data() - } - - return detailed - - except Exception as e: - print(f"✗ 详细报告生成失败: {e}") - return {} - - def _generate_custom_report(self, report_type: str) -> Dict[str, Any]: - """生成自定义报告""" - try: - # 简化实现,返回摘要报告 - return self._generate_summary_report() - except Exception as e: - print(f"✗ 自定义报告生成失败: {e}") - return {} - - def _generate_trend_data(self) -> Dict[str, Any]: - """生成趋势数据""" - try: - trends = {} - - # 匹配趋势 - if self.statistics["match_times"]: - recent_matches = list(self.statistics["match_times"])[-100:] # 最近100个数据点 - trends["match_times"] = [t[1] for t in recent_matches] - - # 成功率趋势 - if self.statistics["success_rates"]: - recent_rates = list(self.statistics["success_rates"])[-100:] - trends["success_rates"] = [r[1] for r in recent_rates] - - # 玩家数量趋势 - if self.statistics["player_counts"]: - recent_players = list(self.statistics["player_counts"])[-100:] - trends["player_counts"] = [p[1] for p in recent_players] - - return trends - - except Exception as e: - print(f"✗ 趋势数据生成失败: {e}") - return {} - - def _generate_performance_data(self) -> Dict[str, Any]: - """生成性能数据""" - try: - performance = {} - - # 算法性能 - if self.statistics["algorithm_performance"]: - recent_performance = list(self.statistics["algorithm_performance"])[-100:] - performance["algorithm_performance"] = [p[1] for p in recent_performance] - - # 系统性能 - if self.statistics["system_performance"]: - recent_system = list(self.statistics["system_performance"])[-100:] - performance["system_performance"] = [s[1] for s in recent_system] - - return performance - - except Exception as e: - print(f"✗ 性能数据生成失败: {e}") - return {} - - def _get_historical_data(self) -> Dict[str, Any]: - """获取历史数据""" - try: - return { - "daily": dict(self.historical_stats["daily"]), - "weekly": dict(self.historical_stats["weekly"]), - "monthly": dict(self.historical_stats["monthly"]) - } - except Exception as e: - print(f"✗ 历史数据获取失败: {e}") - return {} - - def export_stats(self, file_path: str, format: str = "json") -> bool: - """ - 导出统计数据 - - Args: - file_path: 导出文件路径 - format: 导出格式(json, csv) - - Returns: - 是否导出成功 - """ - try: - import os - - # 创建导出目录 - export_dir = os.path.dirname(file_path) - if not os.path.exists(export_dir): - os.makedirs(export_dir) - - # 生成统计数据 - stats_data = { - "statistics": self.get_realtime_stats(), - "reports": self.stat_reports, - "config": self.stats_config - } - - if format.lower() == "json": - # 导出为JSON格式 - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(stats_data, f, indent=2, ensure_ascii=False, default=str) - else: - # 默认导出为JSON格式 - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(stats_data, f, indent=2, ensure_ascii=False, default=str) - - # 触发数据导出回调 - self._trigger_stats_callback("data_exported", { - "file_path": file_path, - "format": format, - "timestamp": time.time() - }) - - print(f"✓ 统计数据已导出到: {file_path}") - return True - - except Exception as e: - print(f"✗ 统计数据导出失败: {e}") - self._trigger_stats_callback("stats_error", { - "error": str(e), - "timestamp": time.time() - }) - return False - - def get_stats_config(self) -> Dict[str, Any]: - """ - 获取统计配置 - - Returns: - 统计配置字典 - """ - return self.stats_config.copy() - - def set_stats_config(self, config: Dict[str, Any]) -> bool: - """ - 设置统计配置 - - Args: - config: 统计配置字典 - - Returns: - 是否设置成功 - """ - try: - self.stats_config.update(config) - print(f"✓ 统计配置已更新: {self.stats_config}") - return True - except Exception as e: - print(f"✗ 统计配置设置失败: {e}") - return False - - def get_stats_state(self) -> Dict[str, Any]: - """ - 获取统计状态 - - Returns: - 统计状态字典 - """ - return self.stats_state.copy() - - def reset_stats(self): - """重置统计数据""" - try: - # 重置统计数据 - for stat_name in self.statistics: - self.statistics[stat_name].clear() - - # 重置状态 - self.stats_state = { - "total_matches": 0, - "successful_matches": 0, - "failed_matches": 0, - "total_players": 0, - "matched_players": 0, - "last_stats_update": 0.0 - } - - print("✓ 统计数据已重置") - except Exception as e: - print(f"✗ 统计数据重置失败: {e}") - - def _trigger_stats_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发统计回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.stats_callbacks: - for callback in self.stats_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 统计回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 统计回调触发失败: {e}") - - def register_stats_callback(self, callback_type: str, callback: callable): - """ - 注册统计回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.stats_callbacks: - self.stats_callbacks[callback_type].append(callback) - print(f"✓ 统计回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 统计回调注册失败: {e}") - - def unregister_stats_callback(self, callback_type: str, callback: callable): - """ - 注销统计回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.stats_callbacks: - if callback in self.stats_callbacks[callback_type]: - self.stats_callbacks[callback_type].remove(callback) - print(f"✓ 统计回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +统计数据管理器模块 +收集和分析匹配数据 +""" + +import time +import json +from typing import Dict, Any, List, Optional +from collections import defaultdict, deque + +class StatsManager: + """ + 统计数据管理器 + 收集和分析匹配数据 + """ + + def __init__(self, plugin): + """ + 初始化统计数据管理器 + + Args: + plugin: 匹配系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 统计配置 + self.stats_config = { + "enable_statistics": True, + "data_retention_period": 86400, # 24小时 + "max_data_points": 10000, + "enable_realtime_stats": True, + "stats_collection_interval": 10.0, + "enable_export": True, + "export_format": "json" + } + + # 统计数据存储 + self.statistics = { + "match_times": deque(maxlen=1000), + "queue_times": deque(maxlen=1000), + "success_rates": deque(maxlen=1000), + "player_counts": deque(maxlen=1000), + "algorithm_performance": deque(maxlen=1000), + "system_performance": deque(maxlen=1000) + } + + # 统计状态 + self.stats_state = { + "total_matches": 0, + "successful_matches": 0, + "failed_matches": 0, + "total_players": 0, + "matched_players": 0, + "last_stats_update": 0.0 + } + + # 历史统计数据 + self.historical_stats = { + "daily": defaultdict(dict), + "weekly": defaultdict(dict), + "monthly": defaultdict(dict) + } + + # 统计报告 + self.stat_reports = { + "last_report": None, + "report_cache": {} + } + + # 统计回调 + self.stats_callbacks = { + "stats_updated": [], + "report_generated": [], + "data_exported": [], + "stats_error": [] + } + + # 时间戳记录 + self.last_stats_collection = 0.0 + self.last_report_generation = 0.0 + + print("✓ 统计数据管理器已创建") + + def initialize(self) -> bool: + """ + 初始化统计数据管理器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化统计数据管理器...") + + self.initialized = True + print("✓ 统计数据管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 统计数据管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用统计数据管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 统计数据管理器未初始化") + return False + + self.enabled = True + print("✓ 统计数据管理器已启用") + return True + + except Exception as e: + print(f"✗ 统计数据管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用统计数据管理器""" + try: + self.enabled = False + print("✓ 统计数据管理器已禁用") + + except Exception as e: + print(f"✗ 统计数据管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理统计数据管理器资源""" + try: + # 禁用统计数据管理器 + if self.enabled: + self.disable() + + # 清理回调 + self.stats_callbacks.clear() + + self.initialized = False + print("✓ 统计数据管理器资源已清理") + + except Exception as e: + print(f"✗ 统计数据管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新统计数据管理器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 定期收集统计数据 + if current_time - self.last_stats_collection >= self.stats_config["stats_collection_interval"]: + self._collect_statistics() + self.last_stats_collection = current_time + + self.stats_state["last_stats_update"] = current_time + + except Exception as e: + print(f"✗ 统计数据管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _collect_statistics(self): + """收集统计数据""" + try: + current_time = time.time() + stats_data = {} + + # 收集匹配管理器统计数据 + if self.plugin.match_manager: + match_stats = self.plugin.match_manager.get_match_stats() + match_state = match_stats.get("state", {}) + + stats_data["active_matches"] = match_state.get("active_matches", 0) + stats_data["successful_matches"] = match_state.get("successful_matches", 0) + stats_data["failed_matches"] = match_state.get("failed_matches", 0) + + # 更新总计数 + self.stats_state["total_matches"] = ( + match_state.get("successful_matches", 0) + + match_state.get("failed_matches", 0) + ) + self.stats_state["successful_matches"] = match_state.get("successful_matches", 0) + self.stats_state["failed_matches"] = match_state.get("failed_matches", 0) + + # 收集队列管理器统计数据 + if self.plugin.queue_manager: + queue_stats = self.plugin.queue_manager.get_queue_stats() + queue_state = queue_stats.get("state", {}) + + stats_data["total_players"] = queue_state.get("total_players", 0) + stats_data["players_matched"] = queue_state.get("players_matched", 0) + stats_data["players_timed_out"] = queue_state.get("players_timed_out", 0) + + # 更新总计数 + self.stats_state["total_players"] = queue_state.get("total_players", 0) + self.stats_state["matched_players"] = queue_state.get("players_matched", 0) + + # 收集算法管理器统计数据 + if self.plugin.algorithm_manager: + algorithm_stats = self.plugin.algorithm_manager.get_algorithm_stats() + algorithm_state = algorithm_stats.get("state", {}) + + stats_data["total_algorithms"] = algorithm_state.get("total_matches", 0) + stats_data["successful_algorithms"] = algorithm_state.get("successful_matches", 0) + stats_data["average_match_quality"] = algorithm_state.get("average_match_quality", 0) + + # 记录算法性能 + self.statistics["algorithm_performance"].append( + (current_time, stats_data["average_match_quality"]) + ) + + # 收集房间分配器统计数据 + if self.plugin.room_allocator: + room_stats = self.plugin.room_allocator.get_room_stats() + room_state = room_stats.get("state", {}) + + stats_data["active_rooms"] = room_state.get("active_rooms", 0) + stats_data["total_rooms"] = room_state.get("total_rooms", 0) + + # 记录统计数据 + self.statistics["player_counts"].append((current_time, stats_data.get("total_players", 0))) + + # 触发统计更新回调 + self._trigger_stats_callback("stats_updated", { + "stats_data": stats_data, + "timestamp": current_time + }) + + except Exception as e: + print(f"✗ 统计数据收集失败: {e}") + self._trigger_stats_callback("stats_error", { + "error": str(e), + "timestamp": time.time() + }) + + def record_match_time(self, match_time: float): + """ + 记录匹配时间 + + Args: + match_time: 匹配时间(秒) + """ + try: + current_time = time.time() + self.statistics["match_times"].append((current_time, match_time)) + except Exception as e: + print(f"✗ 匹配时间记录失败: {e}") + + def record_queue_time(self, queue_time: float): + """ + 记录队列时间 + + Args: + queue_time: 队列时间(秒) + """ + try: + current_time = time.time() + self.statistics["queue_times"].append((current_time, queue_time)) + except Exception as e: + print(f"✗ 队列时间记录失败: {e}") + + def record_success_rate(self, success_rate: float): + """ + 记录成功率 + + Args: + success_rate: 成功率(百分比) + """ + try: + current_time = time.time() + self.statistics["success_rates"].append((current_time, success_rate)) + except Exception as e: + print(f"✗ 成功率记录失败: {e}") + + def get_realtime_stats(self) -> Dict[str, Any]: + """ + 获取实时统计数据 + + Returns: + 实时统计数据字典 + """ + try: + realtime_stats = { + "current_time": time.time(), + "state": self.stats_state.copy(), + "statistics": {} + } + + # 获取最新的统计数据 + for stat_name, stat_data in self.statistics.items(): + if stat_data: + realtime_stats["statistics"][stat_name] = list(stat_data)[-10:] # 最近10个数据点 + else: + realtime_stats["statistics"][stat_name] = [] + + return realtime_stats + + except Exception as e: + print(f"✗ 实时统计数据获取失败: {e}") + return {} + + def generate_report(self, report_type: str = "summary", time_range: str = "24h") -> Dict[str, Any]: + """ + 生成统计报告 + + Args: + report_type: 报告类型(summary, detailed, custom) + time_range: 时间范围(24h, 7d, 30d) + + Returns: + 统计报告字典 + """ + try: + current_time = time.time() + report = { + "report_type": report_type, + "time_range": time_range, + "generated_time": current_time, + "data": {} + } + + # 根据报告类型生成不同内容 + if report_type == "summary": + report["data"] = self._generate_summary_report() + elif report_type == "detailed": + report["data"] = self._generate_detailed_report() + else: + report["data"] = self._generate_custom_report(report_type) + + self.stat_reports["last_report"] = report + self.stat_reports["report_cache"][f"{report_type}_{time_range}"] = report + + # 触发报告生成回调 + self._trigger_stats_callback("report_generated", { + "report": report, + "timestamp": current_time + }) + + return report + + except Exception as e: + print(f"✗ 统计报告生成失败: {e}") + self._trigger_stats_callback("stats_error", { + "error": str(e), + "timestamp": time.time() + }) + return {} + + def _generate_summary_report(self) -> Dict[str, Any]: + """生成摘要报告""" + try: + summary = { + "total_matches": self.stats_state["total_matches"], + "successful_matches": self.stats_state["successful_matches"], + "failed_matches": self.stats_state["failed_matches"], + "match_success_rate": 0.0, + "total_players": self.stats_state["total_players"], + "matched_players": self.stats_state["matched_players"], + "player_match_rate": 0.0, + "average_match_time": 0.0, + "average_queue_time": 0.0 + } + + # 计算成功率 + if self.stats_state["total_matches"] > 0: + summary["match_success_rate"] = ( + self.stats_state["successful_matches"] / self.stats_state["total_matches"] + ) * 100 + + # 计算玩家匹配率 + if self.stats_state["total_players"] > 0: + summary["player_match_rate"] = ( + self.stats_state["matched_players"] / self.stats_state["total_players"] + ) * 100 + + # 计算平均匹配时间 + if self.statistics["match_times"]: + times = [t[1] for t in self.statistics["match_times"]] + summary["average_match_time"] = sum(times) / len(times) + + # 计算平均队列时间 + if self.statistics["queue_times"]: + times = [t[1] for t in self.statistics["queue_times"]] + summary["average_queue_time"] = sum(times) / len(times) + + return summary + + except Exception as e: + print(f"✗ 摘要报告生成失败: {e}") + return {} + + def _generate_detailed_report(self) -> Dict[str, Any]: + """生成详细报告""" + try: + detailed = { + "summary": self._generate_summary_report(), + "trends": self._generate_trend_data(), + "performance": self._generate_performance_data(), + "historical": self._get_historical_data() + } + + return detailed + + except Exception as e: + print(f"✗ 详细报告生成失败: {e}") + return {} + + def _generate_custom_report(self, report_type: str) -> Dict[str, Any]: + """生成自定义报告""" + try: + # 简化实现,返回摘要报告 + return self._generate_summary_report() + except Exception as e: + print(f"✗ 自定义报告生成失败: {e}") + return {} + + def _generate_trend_data(self) -> Dict[str, Any]: + """生成趋势数据""" + try: + trends = {} + + # 匹配趋势 + if self.statistics["match_times"]: + recent_matches = list(self.statistics["match_times"])[-100:] # 最近100个数据点 + trends["match_times"] = [t[1] for t in recent_matches] + + # 成功率趋势 + if self.statistics["success_rates"]: + recent_rates = list(self.statistics["success_rates"])[-100:] + trends["success_rates"] = [r[1] for r in recent_rates] + + # 玩家数量趋势 + if self.statistics["player_counts"]: + recent_players = list(self.statistics["player_counts"])[-100:] + trends["player_counts"] = [p[1] for p in recent_players] + + return trends + + except Exception as e: + print(f"✗ 趋势数据生成失败: {e}") + return {} + + def _generate_performance_data(self) -> Dict[str, Any]: + """生成性能数据""" + try: + performance = {} + + # 算法性能 + if self.statistics["algorithm_performance"]: + recent_performance = list(self.statistics["algorithm_performance"])[-100:] + performance["algorithm_performance"] = [p[1] for p in recent_performance] + + # 系统性能 + if self.statistics["system_performance"]: + recent_system = list(self.statistics["system_performance"])[-100:] + performance["system_performance"] = [s[1] for s in recent_system] + + return performance + + except Exception as e: + print(f"✗ 性能数据生成失败: {e}") + return {} + + def _get_historical_data(self) -> Dict[str, Any]: + """获取历史数据""" + try: + return { + "daily": dict(self.historical_stats["daily"]), + "weekly": dict(self.historical_stats["weekly"]), + "monthly": dict(self.historical_stats["monthly"]) + } + except Exception as e: + print(f"✗ 历史数据获取失败: {e}") + return {} + + def export_stats(self, file_path: str, format: str = "json") -> bool: + """ + 导出统计数据 + + Args: + file_path: 导出文件路径 + format: 导出格式(json, csv) + + Returns: + 是否导出成功 + """ + try: + import os + + # 创建导出目录 + export_dir = os.path.dirname(file_path) + if not os.path.exists(export_dir): + os.makedirs(export_dir) + + # 生成统计数据 + stats_data = { + "statistics": self.get_realtime_stats(), + "reports": self.stat_reports, + "config": self.stats_config + } + + if format.lower() == "json": + # 导出为JSON格式 + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(stats_data, f, indent=2, ensure_ascii=False, default=str) + else: + # 默认导出为JSON格式 + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(stats_data, f, indent=2, ensure_ascii=False, default=str) + + # 触发数据导出回调 + self._trigger_stats_callback("data_exported", { + "file_path": file_path, + "format": format, + "timestamp": time.time() + }) + + print(f"✓ 统计数据已导出到: {file_path}") + return True + + except Exception as e: + print(f"✗ 统计数据导出失败: {e}") + self._trigger_stats_callback("stats_error", { + "error": str(e), + "timestamp": time.time() + }) + return False + + def get_stats_config(self) -> Dict[str, Any]: + """ + 获取统计配置 + + Returns: + 统计配置字典 + """ + return self.stats_config.copy() + + def set_stats_config(self, config: Dict[str, Any]) -> bool: + """ + 设置统计配置 + + Args: + config: 统计配置字典 + + Returns: + 是否设置成功 + """ + try: + self.stats_config.update(config) + print(f"✓ 统计配置已更新: {self.stats_config}") + return True + except Exception as e: + print(f"✗ 统计配置设置失败: {e}") + return False + + def get_stats_state(self) -> Dict[str, Any]: + """ + 获取统计状态 + + Returns: + 统计状态字典 + """ + return self.stats_state.copy() + + def reset_stats(self): + """重置统计数据""" + try: + # 重置统计数据 + for stat_name in self.statistics: + self.statistics[stat_name].clear() + + # 重置状态 + self.stats_state = { + "total_matches": 0, + "successful_matches": 0, + "failed_matches": 0, + "total_players": 0, + "matched_players": 0, + "last_stats_update": 0.0 + } + + print("✓ 统计数据已重置") + except Exception as e: + print(f"✗ 统计数据重置失败: {e}") + + def _trigger_stats_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发统计回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.stats_callbacks: + for callback in self.stats_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 统计回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 统计回调触发失败: {e}") + + def register_stats_callback(self, callback_type: str, callback: callable): + """ + 注册统计回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.stats_callbacks: + self.stats_callbacks[callback_type].append(callback) + print(f"✓ 统计回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 统计回调注册失败: {e}") + + def unregister_stats_callback(self, callback_type: str, callback: callable): + """ + 注销统计回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.stats_callbacks: + if callback in self.stats_callbacks[callback_type]: + self.stats_callbacks[callback_type].remove(callback) + print(f"✓ 统计回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 统计回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/morphing_animation/README.md b/plugins/user/morphing_animation/README.md index a6db69df..771f946e 100644 --- a/plugins/user/morphing_animation/README.md +++ b/plugins/user/morphing_animation/README.md @@ -1,443 +1,443 @@ -# Morphing和变形动画插件 - -## 概述 - -Morphing和变形动画插件为EG项目提供了一个完整且功能丰富的顶点变形、morphing目标和网格变形解决方案。该插件支持模型的顶点级动画控制,包括Morph目标创建、动画状态管理、物理集成、性能优化和网络同步等高级功能。 - -## 功能特性 - -### 核心功能 -1. **模型注册与管理** - 支持注册3D模型以进行morphing操作 -2. **Morph目标创建** - 创建和管理Morph目标,包括顶点、法线和切线偏移 -3. **权重控制** - 精确控制每个Morph目标的权重 -4. **顶点变形** - 支持直接顶点位置变形动画 -5. **程序化变形** - 支持自定义变形函数 - -### 高级功能 -1. **动画状态机** - 管理复杂的动画状态转换和混合 -2. **动画曲线编辑器** - 支持对动画属性进行曲线编辑和自定义 -3. **物理动画集成** - 将物理模拟与顶点变形结合 - -### 性能优化 -1. **动画压缩** - 支持多种压缩算法减少动画数据大小 -2. **LOD系统** - 根据距离和性能自动调整动画质量 -3. **智能缓存** - 提供高效的动画数据缓存机制 - -### 网络功能 -1. **动画状态同步** - 支持多人环境中动画状态的同步 -2. **分布式动画管理** - 管理网络环境中的动画同步 -3. **状态序列化** - 支持动画状态的序列化和反序列化 - -## 系统架构 - -插件采用模块化设计,包含以下核心组件: - -1. **MorphingCore** - 核心变形管理器,负责模型和Morph目标的管理 -2. **AnimationStateMachine** - 动画状态机,管理复杂的动画状态转换 -3. **AnimationCurveEditor** - 动画曲线编辑器,支持动画属性的曲线控制 -4. **PhysicsAnimationIntegration** - 物理动画集成系统,结合物理模拟与变形 -5. **AnimationCompressor** - 动画压缩系统,减少动画数据大小 -6. **AnimationLODSystem** - 动画LOD系统,根据条件调整动画质量 -7. **AnimationCacheManager** - 动画缓存管理器,提高动画加载性能 -8. **AnimationNetworkSync** - 网络同步系统,处理网络环境中的动画同步 - -## 安装和启用 - -### 启用插件 - -在主程序中启用插件: - -```python -# 启用插件系统 -world.plugin_manager.enable_plugin_system() - -# 启用Morphing动画插件 -world.plugin_manager.enable_plugin('morphing_animation') -``` - -## 使用方法 - -### 基本使用 - -```python -# 获取插件实例 -plugin = world.plugin_manager.get_plugin('morphing_animation') -core_system = plugin.get_core_system() - -# 注册模型 -model_id = core_system.register_model(model_node, 'my_model') - -# 创建Morph目标 -vertex_offsets = [(0.1, 0.0, 0.0)] * vertex_count # 顶点偏移数据 -core_system.create_morph_target(model_id, 'smile', vertex_offsets) - -# 设置Morph目标权重 -core_system.set_morph_target_weight(model_id, 'smile', 0.5) -``` - -### 高级使用 - -```python -# 使用状态机 -state_machine = plugin.get_state_machine_system(model_id) -state_machine.add_state('idle', {'neutral': 1.0}, loop=True) -state_machine.add_state('happy', {'smile': 1.0}, duration=2.0) -state_machine.set_transition('idle', 'happy') -state_machine.enter_state('idle') -state_machine.start_updating() - -# 使用曲线编辑器 -curve_editor = plugin.get_curve_editor_system() -control_points = [(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)] -curve_editor.create_curve('bounce', CurveType.BEZIER, control_points) -curve_editor.apply_curve_to_morph_target('bounce', model_id, 'smile', 0.5) - -# 使用物理集成 -physics_system = plugin.get_physics_integration_system() -physics_system.add_constraint(model_id, 'spring_smile', 'spring', 'smile', stiffness=2.0) -physics_system.start_updating() -``` - -### 性能优化 - -```python -# 使用压缩系统 -compressor = plugin.get_compressor_system() -compressed_data = compressor.compress_morph_data(morph_data, CompressionMethod.KEYFRAME_REDUCTION) - -# 使用LOD系统 -lod_system = plugin.get_lod_system() -lod_system.set_camera(world.camera) -lod_system.update_lod_levels(models) - -# 使用缓存管理 -cache_manager = plugin.get_cache_manager_system() -cache_manager.put('key', data) -cached_data = cache_manager.get('key') -``` - -### 网络同步 - -```python -# 获取网络系统 -network_system = plugin.get_network_system() - -# 设置网络模式 -network_system.set_network_mode(NetworkMode.SERVER) -network_system.set_sync_mode(SyncMode.STATE_SYNC) - -# 注册模型进行网络同步 -network_id = network_system.register_model_for_sync(model_id) - -# 启动同步任务 -network_system.start_sync_task() -``` - -## API参考 - -### MorphingCore 类 - -#### register_model(model_node, model_id=None) -注册模型以支持morphing - -**参数:** -- `model_node` (NodePath): 模型节点 -- `model_id` (str): 模型ID,如果为None则自动生成 - -**返回:** -- 模型ID - -#### unregister_model(model_id) -注销模型 - -**参数:** -- `model_id` (str): 模型ID - -#### create_morph_target(model_id, target_name, vertex_offsets, normal_offsets=None, tangent_offsets=None) -创建Morph目标 - -**参数:** -- `model_id` (str): 模型ID -- `target_name` (str): 目标名称 -- `vertex_offsets` (list): 顶点偏移列表 -- `normal_offsets` (list): 法线偏移列表(可选) -- `tangent_offsets` (list): 切线偏移列表(可选) - -#### set_morph_target_weight(model_id, target_name, weight) -设置Morph目标权重 - -**参数:** -- `model_id` (str): 模型ID -- `target_name` (str): 目标名称 -- `weight` (float): 权重值 (0.0-1.0) - -#### create_morphing_animation(model_id, anim_name, target_weights, duration, interpolation=InterpolationMode.LINEAR, loop=False) -创建Morphing动画 - -**参数:** -- `model_id` (str): 模型ID -- `anim_name` (str): 动画名称 -- `target_weights` (dict): 目标权重字典 {target_name: weight} -- `duration` (float): 持续时间 -- `interpolation` (InterpolationMode): 插值模式 -- `loop` (bool): 是否循环 - -#### play_morphing_animation(model_id, anim_name) -播放Morphing动画 - -**参数:** -- `model_id` (str): 模型ID -- `anim_name` (str): 动画名称 - -### AnimationStateMachine 类 - -#### add_state(state_name, morph_targets, duration=0.0, loop=False, play_rate=1.0) -添加动画状态 - -**参数:** -- `state_name` (str): 状态名称 -- `morph_targets` (dict): Morph目标权重字典 {target_name: weight} -- `duration` (float): 持续时间 -- `loop` (bool): 是否循环 -- `play_rate` (float): 播放速度 - -#### set_transition(from_state, to_state, transition_type=TransitionType.SMOOTH, condition=None, transition_time=0.3, blend_mode=InterpolationMode.LINEAR) -设置状态转换 - -**参数:** -- `from_state` (str): 起始状态 -- `to_state` (str): 目标状态 -- `transition_type` (TransitionType): 转换类型 -- `condition` (function): 转换条件函数 -- `transition_time` (float): 转换时间 -- `blend_mode` (InterpolationMode): 混合模式 - -#### enter_state(state_name) -进入指定状态 - -**参数:** -- `state_name` (str): 状态名称 - -### AnimationCurveEditor 类 - -#### create_curve(curve_name, curve_type, control_points, interpolation=InterpolationMode.LINEAR) -创建动画曲线 - -**参数:** -- `curve_name` (str): 曲线名称 -- `curve_type` (CurveType): 曲线类型 -- `control_points` (list): 控制点列表 [(time, value)] -- `interpolation` (InterpolationMode): 插值模式 - -#### apply_curve_to_morph_target(curve_name, model_id, target_name, time, base_weight=1.0) -将曲线应用到Morph目标 - -**参数:** -- `curve_name` (str): 曲线名称 -- `model_id` (str): 模型ID -- `target_name` (str): Morph目标名称 -- `time` (float): 时间 -- `base_weight` (float): 基础权重 - -### PhysicsAnimationIntegration 类 - -#### add_constraint(model_id, constraint_name, constraint_type, target_bone, stiffness=1.0, damping=0.1, rest_value=0.0) -添加物理约束 - -**参数:** -- `model_id` (str): 模型ID -- `constraint_name` (str): 约束名称 -- `constraint_type` (str): 约束类型 ('spring', 'distance', 'angle') -- `target_bone` (str): 目标骨骼 -- `stiffness` (float): 刚度 -- `damping` (float): 阻尼 -- `rest_value` (float): 静止值 - -### AnimationCompressor 类 - -#### compress_morph_data(morph_data, method=CompressionMethod.KEYFRAME_REDUCTION) -压缩Morph数据 - -**参数:** -- `morph_data` (list): Morph数据列表 -- `method` (CompressionMethod): 压缩方法 - -**返回:** -- CompressedMorphData对象 - -### AnimationLODSystem 类 - -#### set_lod_strategy(strategy) -设置LOD策略 - -**参数:** -- `strategy` (LODStrategy): LOD策略 - -#### set_distance_thresholds(thresholds) -设置距离阈值 - -**参数:** -- `thresholds` (list): 距离阈值列表 - -#### update_lod_levels(models) -更新LOD级别 - -**参数:** -- `models` (dict): 模型字典 {model_id: model_data} - -### AnimationCacheManager 类 - -#### get(key) -获取缓存项 - -**参数:** -- `key` (str): 缓存键 - -**返回:** -- 缓存数据 - -#### put(key, data, size=1) -放入缓存项 - -**参数:** -- `key` (str): 缓存键 -- `data` (any): 缓存数据 -- `size` (int): 数据大小 - -### AnimationNetworkSync 类 - -#### set_network_mode(mode) -设置网络模式 - -**参数:** -- `mode` (NetworkMode): 网络模式 - -#### set_sync_mode(mode) -设置同步模式 - -**参数:** -- `mode` (SyncMode): 同步模式 - -#### register_model_for_sync(model_id, network_id=None) -注册模型以进行网络同步 - -**参数:** -- `model_id` (str): 本地模型ID -- `network_id` (str): 网络ID,如果为None则自动生成 - -**返回:** -- 网络ID - -## 支持的文件格式 - -- 模型格式: .egg, .bam 等Panda3D支持的格式 -- Morph数据可以以多种格式存储和加载 - -## 配置选项 - -插件支持以下配置选项(config.json): - -```json -{ - "modules": { - "core": { - "enabled": true, - "max_morph_targets": 100, - "default_interpolation": "linear", - "update_rate": 60, - "cache_size": 200 - }, - "advanced": { - "state_machine": true, - "curve_editor": true, - "physics_integration": true, - "state_machine_update_rate": 30, - "curve_editor_points": 100, - "physics_update_rate": 60 - }, - "optimization": { - "compression": true, - "lod": true, - "cache": true, - "compression_threshold": 0.001, - "cache_size": 500, - "lod_distance_thresholds": [10.0, 25.0, 50.0], - "lod_frame_rates": [1.0, 0.5, 0.25, 0.1] - }, - "network": { - "enabled": true, - "sync_interval": 0.1, - "interpolation_mode": "linear", - "enable_compression": true, - "max_buffer_size": 100 - } - }, - "performance": { - "target_fps": 60, - "lod_threshold": 30, - "max_buffer_size": 100 - } -} -``` - -## 性能优化建议 - -1. **使用LOD系统** - 根据模型距离摄像机的远近自动调整变形质量 -2. **启用动画压缩** - 减少内存占用,提高加载速度 -3. **合理使用缓存** - 避免重复计算相同变形 -4. **优化网络同步** - 根据网络状况调整同步频率 -5. **及时注销模型** - 不需要的模型及时调用unregister_model释放 -6. **使用状态机** - 管理复杂动画状态,避免手动控制的复杂性 - -## 故障排除 - -常见问题及解决方案: - -1. **模型注册失败**: 检查模型节点是否有效 -2. **Morph目标创建失败**: 确认顶点偏移数据与模型顶点数量匹配 -3. **变形效果不明显**: 检查权重值是否正确设置 -4. **性能问题**: 减少同时变形的模型数量,启用LOD和压缩 -5. **网络同步问题**: 确认网络模式设置正确,检查防火墙设置 -6. **状态机问题**: 检查状态和转换定义是否正确 - -## 扩展开发 - -插件采用模块化设计,可以轻松扩展: - -1. 继承现有类添加新功能 -2. 实现自定义的压缩算法 -3. 添加新的变形控制方式 -4. 扩展网络同步协议 -5. 集成其他物理引擎 -6. 开发新的工具模块 - -## 示例代码 - -查看 `examples.py` 文件获取详细的使用示例。 - -每个模块都有完整的使用示例,可以直接运行测试。 - -## 技术支持 - -如遇到问题,请查看以下资源: - -1. 官方文档和API参考 -2. 示例代码和使用说明 -3. 社区论坛和技术支持 -4. GitHub Issues页面 - -## 版本历史 - -### v1.0.0 (当前版本) -- 初始版本发布 -- 实现完整的Morphing和变形动画系统 -- 包含所有核心功能和高级特性 -- 提供完整的文档和示例 - -## 许可证 - -本插件遵循EG项目的许可证条款。 - -## 联系方式 - +# Morphing和变形动画插件 + +## 概述 + +Morphing和变形动画插件为EG项目提供了一个完整且功能丰富的顶点变形、morphing目标和网格变形解决方案。该插件支持模型的顶点级动画控制,包括Morph目标创建、动画状态管理、物理集成、性能优化和网络同步等高级功能。 + +## 功能特性 + +### 核心功能 +1. **模型注册与管理** - 支持注册3D模型以进行morphing操作 +2. **Morph目标创建** - 创建和管理Morph目标,包括顶点、法线和切线偏移 +3. **权重控制** - 精确控制每个Morph目标的权重 +4. **顶点变形** - 支持直接顶点位置变形动画 +5. **程序化变形** - 支持自定义变形函数 + +### 高级功能 +1. **动画状态机** - 管理复杂的动画状态转换和混合 +2. **动画曲线编辑器** - 支持对动画属性进行曲线编辑和自定义 +3. **物理动画集成** - 将物理模拟与顶点变形结合 + +### 性能优化 +1. **动画压缩** - 支持多种压缩算法减少动画数据大小 +2. **LOD系统** - 根据距离和性能自动调整动画质量 +3. **智能缓存** - 提供高效的动画数据缓存机制 + +### 网络功能 +1. **动画状态同步** - 支持多人环境中动画状态的同步 +2. **分布式动画管理** - 管理网络环境中的动画同步 +3. **状态序列化** - 支持动画状态的序列化和反序列化 + +## 系统架构 + +插件采用模块化设计,包含以下核心组件: + +1. **MorphingCore** - 核心变形管理器,负责模型和Morph目标的管理 +2. **AnimationStateMachine** - 动画状态机,管理复杂的动画状态转换 +3. **AnimationCurveEditor** - 动画曲线编辑器,支持动画属性的曲线控制 +4. **PhysicsAnimationIntegration** - 物理动画集成系统,结合物理模拟与变形 +5. **AnimationCompressor** - 动画压缩系统,减少动画数据大小 +6. **AnimationLODSystem** - 动画LOD系统,根据条件调整动画质量 +7. **AnimationCacheManager** - 动画缓存管理器,提高动画加载性能 +8. **AnimationNetworkSync** - 网络同步系统,处理网络环境中的动画同步 + +## 安装和启用 + +### 启用插件 + +在主程序中启用插件: + +```python +# 启用插件系统 +world.plugin_manager.enable_plugin_system() + +# 启用Morphing动画插件 +world.plugin_manager.enable_plugin('morphing_animation') +``` + +## 使用方法 + +### 基本使用 + +```python +# 获取插件实例 +plugin = world.plugin_manager.get_plugin('morphing_animation') +core_system = plugin.get_core_system() + +# 注册模型 +model_id = core_system.register_model(model_node, 'my_model') + +# 创建Morph目标 +vertex_offsets = [(0.1, 0.0, 0.0)] * vertex_count # 顶点偏移数据 +core_system.create_morph_target(model_id, 'smile', vertex_offsets) + +# 设置Morph目标权重 +core_system.set_morph_target_weight(model_id, 'smile', 0.5) +``` + +### 高级使用 + +```python +# 使用状态机 +state_machine = plugin.get_state_machine_system(model_id) +state_machine.add_state('idle', {'neutral': 1.0}, loop=True) +state_machine.add_state('happy', {'smile': 1.0}, duration=2.0) +state_machine.set_transition('idle', 'happy') +state_machine.enter_state('idle') +state_machine.start_updating() + +# 使用曲线编辑器 +curve_editor = plugin.get_curve_editor_system() +control_points = [(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)] +curve_editor.create_curve('bounce', CurveType.BEZIER, control_points) +curve_editor.apply_curve_to_morph_target('bounce', model_id, 'smile', 0.5) + +# 使用物理集成 +physics_system = plugin.get_physics_integration_system() +physics_system.add_constraint(model_id, 'spring_smile', 'spring', 'smile', stiffness=2.0) +physics_system.start_updating() +``` + +### 性能优化 + +```python +# 使用压缩系统 +compressor = plugin.get_compressor_system() +compressed_data = compressor.compress_morph_data(morph_data, CompressionMethod.KEYFRAME_REDUCTION) + +# 使用LOD系统 +lod_system = plugin.get_lod_system() +lod_system.set_camera(world.camera) +lod_system.update_lod_levels(models) + +# 使用缓存管理 +cache_manager = plugin.get_cache_manager_system() +cache_manager.put('key', data) +cached_data = cache_manager.get('key') +``` + +### 网络同步 + +```python +# 获取网络系统 +network_system = plugin.get_network_system() + +# 设置网络模式 +network_system.set_network_mode(NetworkMode.SERVER) +network_system.set_sync_mode(SyncMode.STATE_SYNC) + +# 注册模型进行网络同步 +network_id = network_system.register_model_for_sync(model_id) + +# 启动同步任务 +network_system.start_sync_task() +``` + +## API参考 + +### MorphingCore 类 + +#### register_model(model_node, model_id=None) +注册模型以支持morphing + +**参数:** +- `model_node` (NodePath): 模型节点 +- `model_id` (str): 模型ID,如果为None则自动生成 + +**返回:** +- 模型ID + +#### unregister_model(model_id) +注销模型 + +**参数:** +- `model_id` (str): 模型ID + +#### create_morph_target(model_id, target_name, vertex_offsets, normal_offsets=None, tangent_offsets=None) +创建Morph目标 + +**参数:** +- `model_id` (str): 模型ID +- `target_name` (str): 目标名称 +- `vertex_offsets` (list): 顶点偏移列表 +- `normal_offsets` (list): 法线偏移列表(可选) +- `tangent_offsets` (list): 切线偏移列表(可选) + +#### set_morph_target_weight(model_id, target_name, weight) +设置Morph目标权重 + +**参数:** +- `model_id` (str): 模型ID +- `target_name` (str): 目标名称 +- `weight` (float): 权重值 (0.0-1.0) + +#### create_morphing_animation(model_id, anim_name, target_weights, duration, interpolation=InterpolationMode.LINEAR, loop=False) +创建Morphing动画 + +**参数:** +- `model_id` (str): 模型ID +- `anim_name` (str): 动画名称 +- `target_weights` (dict): 目标权重字典 {target_name: weight} +- `duration` (float): 持续时间 +- `interpolation` (InterpolationMode): 插值模式 +- `loop` (bool): 是否循环 + +#### play_morphing_animation(model_id, anim_name) +播放Morphing动画 + +**参数:** +- `model_id` (str): 模型ID +- `anim_name` (str): 动画名称 + +### AnimationStateMachine 类 + +#### add_state(state_name, morph_targets, duration=0.0, loop=False, play_rate=1.0) +添加动画状态 + +**参数:** +- `state_name` (str): 状态名称 +- `morph_targets` (dict): Morph目标权重字典 {target_name: weight} +- `duration` (float): 持续时间 +- `loop` (bool): 是否循环 +- `play_rate` (float): 播放速度 + +#### set_transition(from_state, to_state, transition_type=TransitionType.SMOOTH, condition=None, transition_time=0.3, blend_mode=InterpolationMode.LINEAR) +设置状态转换 + +**参数:** +- `from_state` (str): 起始状态 +- `to_state` (str): 目标状态 +- `transition_type` (TransitionType): 转换类型 +- `condition` (function): 转换条件函数 +- `transition_time` (float): 转换时间 +- `blend_mode` (InterpolationMode): 混合模式 + +#### enter_state(state_name) +进入指定状态 + +**参数:** +- `state_name` (str): 状态名称 + +### AnimationCurveEditor 类 + +#### create_curve(curve_name, curve_type, control_points, interpolation=InterpolationMode.LINEAR) +创建动画曲线 + +**参数:** +- `curve_name` (str): 曲线名称 +- `curve_type` (CurveType): 曲线类型 +- `control_points` (list): 控制点列表 [(time, value)] +- `interpolation` (InterpolationMode): 插值模式 + +#### apply_curve_to_morph_target(curve_name, model_id, target_name, time, base_weight=1.0) +将曲线应用到Morph目标 + +**参数:** +- `curve_name` (str): 曲线名称 +- `model_id` (str): 模型ID +- `target_name` (str): Morph目标名称 +- `time` (float): 时间 +- `base_weight` (float): 基础权重 + +### PhysicsAnimationIntegration 类 + +#### add_constraint(model_id, constraint_name, constraint_type, target_bone, stiffness=1.0, damping=0.1, rest_value=0.0) +添加物理约束 + +**参数:** +- `model_id` (str): 模型ID +- `constraint_name` (str): 约束名称 +- `constraint_type` (str): 约束类型 ('spring', 'distance', 'angle') +- `target_bone` (str): 目标骨骼 +- `stiffness` (float): 刚度 +- `damping` (float): 阻尼 +- `rest_value` (float): 静止值 + +### AnimationCompressor 类 + +#### compress_morph_data(morph_data, method=CompressionMethod.KEYFRAME_REDUCTION) +压缩Morph数据 + +**参数:** +- `morph_data` (list): Morph数据列表 +- `method` (CompressionMethod): 压缩方法 + +**返回:** +- CompressedMorphData对象 + +### AnimationLODSystem 类 + +#### set_lod_strategy(strategy) +设置LOD策略 + +**参数:** +- `strategy` (LODStrategy): LOD策略 + +#### set_distance_thresholds(thresholds) +设置距离阈值 + +**参数:** +- `thresholds` (list): 距离阈值列表 + +#### update_lod_levels(models) +更新LOD级别 + +**参数:** +- `models` (dict): 模型字典 {model_id: model_data} + +### AnimationCacheManager 类 + +#### get(key) +获取缓存项 + +**参数:** +- `key` (str): 缓存键 + +**返回:** +- 缓存数据 + +#### put(key, data, size=1) +放入缓存项 + +**参数:** +- `key` (str): 缓存键 +- `data` (any): 缓存数据 +- `size` (int): 数据大小 + +### AnimationNetworkSync 类 + +#### set_network_mode(mode) +设置网络模式 + +**参数:** +- `mode` (NetworkMode): 网络模式 + +#### set_sync_mode(mode) +设置同步模式 + +**参数:** +- `mode` (SyncMode): 同步模式 + +#### register_model_for_sync(model_id, network_id=None) +注册模型以进行网络同步 + +**参数:** +- `model_id` (str): 本地模型ID +- `network_id` (str): 网络ID,如果为None则自动生成 + +**返回:** +- 网络ID + +## 支持的文件格式 + +- 模型格式: .egg, .bam 等Panda3D支持的格式 +- Morph数据可以以多种格式存储和加载 + +## 配置选项 + +插件支持以下配置选项(config.json): + +```json +{ + "modules": { + "core": { + "enabled": true, + "max_morph_targets": 100, + "default_interpolation": "linear", + "update_rate": 60, + "cache_size": 200 + }, + "advanced": { + "state_machine": true, + "curve_editor": true, + "physics_integration": true, + "state_machine_update_rate": 30, + "curve_editor_points": 100, + "physics_update_rate": 60 + }, + "optimization": { + "compression": true, + "lod": true, + "cache": true, + "compression_threshold": 0.001, + "cache_size": 500, + "lod_distance_thresholds": [10.0, 25.0, 50.0], + "lod_frame_rates": [1.0, 0.5, 0.25, 0.1] + }, + "network": { + "enabled": true, + "sync_interval": 0.1, + "interpolation_mode": "linear", + "enable_compression": true, + "max_buffer_size": 100 + } + }, + "performance": { + "target_fps": 60, + "lod_threshold": 30, + "max_buffer_size": 100 + } +} +``` + +## 性能优化建议 + +1. **使用LOD系统** - 根据模型距离摄像机的远近自动调整变形质量 +2. **启用动画压缩** - 减少内存占用,提高加载速度 +3. **合理使用缓存** - 避免重复计算相同变形 +4. **优化网络同步** - 根据网络状况调整同步频率 +5. **及时注销模型** - 不需要的模型及时调用unregister_model释放 +6. **使用状态机** - 管理复杂动画状态,避免手动控制的复杂性 + +## 故障排除 + +常见问题及解决方案: + +1. **模型注册失败**: 检查模型节点是否有效 +2. **Morph目标创建失败**: 确认顶点偏移数据与模型顶点数量匹配 +3. **变形效果不明显**: 检查权重值是否正确设置 +4. **性能问题**: 减少同时变形的模型数量,启用LOD和压缩 +5. **网络同步问题**: 确认网络模式设置正确,检查防火墙设置 +6. **状态机问题**: 检查状态和转换定义是否正确 + +## 扩展开发 + +插件采用模块化设计,可以轻松扩展: + +1. 继承现有类添加新功能 +2. 实现自定义的压缩算法 +3. 添加新的变形控制方式 +4. 扩展网络同步协议 +5. 集成其他物理引擎 +6. 开发新的工具模块 + +## 示例代码 + +查看 `examples.py` 文件获取详细的使用示例。 + +每个模块都有完整的使用示例,可以直接运行测试。 + +## 技术支持 + +如遇到问题,请查看以下资源: + +1. 官方文档和API参考 +2. 示例代码和使用说明 +3. 社区论坛和技术支持 +4. GitHub Issues页面 + +## 版本历史 + +### v1.0.0 (当前版本) +- 初始版本发布 +- 实现完整的Morphing和变形动画系统 +- 包含所有核心功能和高级特性 +- 提供完整的文档和示例 + +## 许可证 + +本插件遵循EG项目的许可证条款。 + +## 联系方式 + 如有任何问题或建议,请联系开发团队。 \ No newline at end of file diff --git a/plugins/user/morphing_animation/__init__.py b/plugins/user/morphing_animation/__init__.py index fa9c7a99..68ce2727 100644 --- a/plugins/user/morphing_animation/__init__.py +++ b/plugins/user/morphing_animation/__init__.py @@ -1,4 +1,4 @@ -""" -Morphing和变形动画插件 -提供完整的顶点变形、morphing目标、网格变形等高级动画功能 +""" +Morphing和变形动画插件 +提供完整的顶点变形、morphing目标、网格变形等高级动画功能 """ \ No newline at end of file diff --git a/plugins/user/morphing_animation/advanced.py b/plugins/user/morphing_animation/advanced.py index 95e260fb..d0e0a221 100644 --- a/plugins/user/morphing_animation/advanced.py +++ b/plugins/user/morphing_animation/advanced.py @@ -1,980 +1,980 @@ -""" -Morphing和变形动画插件 - 高级功能模块 -提供动画状态机、曲线编辑器、物理集成等高级功能 -""" - -import math -import numpy as np -from typing import Dict, List, Tuple, Optional, Any, Callable, Union -from dataclasses import dataclass, field -from enum import Enum - -from panda3d.core import * -from direct.actor.Actor import Actor -from direct.showbase.DirectObject import DirectObject -from direct.interval.IntervalGlobal import * - -# 导入核心模块 -from .core import MorphingCore, InterpolationMode - -# 定义常量 -ADVANCED_SETTINGS = { - 'state_machine_update_rate': 30, - 'curve_editor_points': 100, - 'physics_update_rate': 60 -} - -# 枚举定义 -class AnimationStateType(Enum): - """动画状态类型枚举""" - IDLE = 0 - PLAYING = 1 - TRANSITION = 2 - BLENDING = 3 - PAUSED = 4 - -class TransitionType(Enum): - """转换类型枚举""" - INSTANT = 0 # 瞬间转换 - SMOOTH = 1 # 平滑转换 - BLEND = 2 # 混合转换 - CONDITIONAL = 3 # 条件转换 - -class CurveType(Enum): - """曲线类型枚举""" - LINEAR = 0 # 线性 - BEZIER = 1 # 贝塞尔 - SPLINE = 2 # 样条 - STEP = 3 # 阶梯 - NOISE = 4 # 噪声 - -@dataclass -class AnimationState: - """动画状态数据类""" - name: str - morph_targets: Dict[str, float] # {target_name: weight} - duration: float = 0.0 - loop: bool = False - play_rate: float = 1.0 - active: bool = True - -@dataclass -class StateTransition: - """状态转换数据类""" - from_state: str - to_state: str - transition_type: TransitionType - condition: Optional[Callable] = None # 转换条件函数 - transition_time: float = 0.3 # 转换时间 - blend_mode: InterpolationMode = InterpolationMode.LINEAR - -@dataclass -class AnimationCurve: - """动画曲线数据类""" - name: str - curve_type: CurveType - control_points: List[Tuple[float, float]] # 控制点 [(time, value)] - interpolation: InterpolationMode = InterpolationMode.LINEAR - active: bool = True - -@dataclass -class PhysicsConstraint: - """物理约束数据类""" - name: str - constraint_type: str # 'spring', 'distance', 'angle' - target_bone: str - stiffness: float = 1.0 - damping: float = 0.1 - rest_value: float = 0.0 - active: bool = True - -class AnimationStateMachine: - """ - 动画状态机 - 管理复杂的动画状态转换和混合 - """ - - def __init__(self, morphing_core: MorphingCore, model_id: str): - """ - 初始化动画状态机 - :param morphing_core: Morphing核心系统 - :param model_id: 模型ID - """ - self.morphing_core = morphing_core - self.model_id = model_id - self.states: Dict[str, AnimationState] = {} - self.transitions: List[StateTransition] = [] - self.current_state: Optional[str] = None - self.previous_state: Optional[str] = None - self.transition_progress: float = 0.0 - self.is_transitioning: bool = False - self.is_updating: bool = False - self.update_rate = ADVANCED_SETTINGS['state_machine_update_rate'] - self.last_update_time: float = 0.0 - - print(f"动画状态机初始化完成 (模型: {model_id})") - - def add_state(self, state_name: str, morph_targets: Dict[str, float], - duration: float = 0.0, loop: bool = False, play_rate: float = 1.0) -> bool: - """ - 添加动画状态 - :param state_name: 状态名称 - :param morph_targets: Morph目标权重字典 {target_name: weight} - :param duration: 持续时间 - :param loop: 是否循环 - :param play_rate: 播放速度 - :return: 是否成功添加 - """ - # 验证Morph目标是否存在 - for target_name in morph_targets.keys(): - if (self.model_id not in self.morphing_core.morph_targets or - target_name not in self.morphing_core.morph_targets[self.model_id]): - print(f"错误: Morph目标 {target_name} 不存在于模型 {self.model_id} 中") - return False - - # 创建状态 - state = AnimationState( - name=state_name, - morph_targets=morph_targets, - duration=duration, - loop=loop, - play_rate=play_rate, - active=True - ) - - # 存储状态 - self.states[state_name] = state - print(f"动画状态已添加: {state_name} (模型: {self.model_id})") - return True - - def remove_state(self, state_name: str) -> bool: - """ - 移除动画状态 - :param state_name: 状态名称 - :return: 是否成功移除 - """ - if state_name not in self.states: - print(f"错误: 动画状态 {state_name} 不存在") - return False - - del self.states[state_name] - - # 移除相关的转换 - self.transitions = [t for t in self.transitions - if t.from_state != state_name and t.to_state != state_name] - - print(f"动画状态已移除: {state_name} (模型: {self.model_id})") - return True - - def set_transition(self, from_state: str, to_state: str, - transition_type: TransitionType = TransitionType.SMOOTH, - condition: Optional[Callable] = None, - transition_time: float = 0.3, - blend_mode: InterpolationMode = InterpolationMode.LINEAR) -> bool: - """ - 设置状态转换 - :param from_state: 起始状态 - :param to_state: 目标状态 - :param transition_type: 转换类型 - :param condition: 转换条件函数 - :param transition_time: 转换时间 - :param blend_mode: 混合模式 - :return: 是否成功设置 - """ - # 验证状态是否存在 - if from_state not in self.states: - print(f"错误: 起始状态 {from_state} 不存在") - return False - - if to_state not in self.states: - print(f"错误: 目标状态 {to_state} 不存在") - return False - - # 创建转换 - transition = StateTransition( - from_state=from_state, - to_state=to_state, - transition_type=transition_type, - condition=condition, - transition_time=transition_time, - blend_mode=blend_mode - ) - - # 存储转换 - self.transitions.append(transition) - print(f"状态转换已设置: {from_state} -> {to_state} (模型: {self.model_id})") - return True - - def enter_state(self, state_name: str) -> bool: - """ - 进入指定状态 - :param state_name: 状态名称 - :return: 是否成功进入 - """ - if state_name not in self.states: - print(f"错误: 动画状态 {state_name} 不存在") - return False - - self.previous_state = self.current_state - self.current_state = state_name - self.is_transitioning = False - - # 应用状态的Morph目标权重 - state = self.states[state_name] - for target_name, weight in state.morph_targets.items(): - self.morphing_core.set_morph_target_weight(self.model_id, target_name, weight) - - print(f"进入动画状态: {state_name} (模型: {self.model_id})") - return True - - def trigger_transition(self, to_state: str) -> bool: - """ - 触发状态转换 - :param to_state: 目标状态 - :return: 是否成功触发 - """ - if not self.current_state: - print(f"错误: 当前没有活动状态") - return False - - if to_state not in self.states: - print(f"错误: 目标状态 {to_state} 不存在") - return False - - # 查找转换 - transition = None - for t in self.transitions: - if t.from_state == self.current_state and t.to_state == to_state: - transition = t - break - - if not transition: - print(f"错误: 从 {self.current_state} 到 {to_state} 的转换不存在") - return False - - # 开始转换 - self.is_transitioning = True - self.transition_progress = 0.0 - self.previous_state = self.current_state - self.current_state = to_state - - print(f"触发状态转换: {self.previous_state} -> {to_state} (模型: {self.model_id})") - return True - - def update(self, dt: float) -> None: - """ - 更新状态机 - :param dt: 时间增量 - """ - if not self.is_updating: - return - - current_time = globalClock.getRealTime() - - # 限制更新频率 - if current_time - self.last_update_time < 1.0 / self.update_rate: - return - - self.last_update_time = current_time - - # 更新状态转换 - if self.is_transitioning: - self._update_transition(dt) - - # 检查条件转换 - self._check_conditional_transitions() - - def _update_transition(self, dt: float) -> None: - """ - 更新状态转换 - :param dt: 时间增量 - """ - if not self.is_transitioning or not self.previous_state or not self.current_state: - return - - # 更新转换进度 - transition = None - for t in self.transitions: - if t.from_state == self.previous_state and t.to_state == self.current_state: - transition = t - break - - if not transition: - self.is_transitioning = False - return - - self.transition_progress += dt / transition.transition_time - - # 检查转换是否完成 - if self.transition_progress >= 1.0: - self.is_transitioning = False - self.transition_progress = 1.0 - print(f"状态转换完成: {self.previous_state} -> {self.current_state} (模型: {self.model_id})") - return - - # 计算插值因子 - t = self.transition_progress - if transition.blend_mode == InterpolationMode.EASE_IN: - t = t * t - elif transition.blend_mode == InterpolationMode.EASE_OUT: - t = 1.0 - (1.0 - t) * (1.0 - t) - elif transition.blend_mode == InterpolationMode.EASE_IN_OUT: - t = t * t * (3.0 - 2.0 * t) - - # 混合两个状态的权重 - from_state = self.states[self.previous_state] - to_state = self.states[self.current_state] - - # 获取所有涉及的Morph目标 - all_targets = set(from_state.morph_targets.keys()) | set(to_state.morph_targets.keys()) - - for target_name in all_targets: - from_weight = from_state.morph_targets.get(target_name, 0.0) - to_weight = to_state.morph_targets.get(target_name, 0.0) - - # 计算混合权重 - blended_weight = from_weight * (1.0 - t) + to_weight * t - - # 应用权重 - self.morphing_core.set_morph_target_weight(self.model_id, target_name, blended_weight) - - def _check_conditional_transitions(self) -> None: - """ - 检查条件转换 - """ - if not self.current_state: - return - - # 检查从当前状态出发的条件转换 - for transition in self.transitions: - if (transition.from_state == self.current_state and - transition.transition_type == TransitionType.CONDITIONAL and - transition.condition): - - try: - # 检查条件是否满足 - if transition.condition(): - self.trigger_transition(transition.to_state) - break - except Exception as e: - print(f"检查转换条件时出错: {e}") - - def get_state_info(self, state_name: str) -> Optional[Dict[str, Any]]: - """ - 获取状态信息 - :param state_name: 状态名称 - :return: 状态信息字典 - """ - if state_name not in self.states: - print(f"错误: 动画状态 {state_name} 不存在") - return None - - state = self.states[state_name] - - return { - 'name': state.name, - 'morph_targets': state.morph_targets, - 'duration': state.duration, - 'loop': state.loop, - 'play_rate': state.play_rate, - 'active': state.active - } - - def get_current_state(self) -> Optional[str]: - """ - 获取当前状态 - :return: 当前状态名称 - """ - return self.current_state - - def start_updating(self) -> None: - """ - 开始更新状态机 - """ - if not self.is_updating: - self.is_updating = True - print(f"动画状态机更新已启动 (模型: {self.model_id})") - - def stop_updating(self) -> None: - """ - 停止更新状态机 - """ - if self.is_updating: - self.is_updating = False - print(f"动画状态机更新已停止 (模型: {self.model_id})") - -class AnimationCurveEditor: - """ - 动画曲线编辑器 - 支持对动画属性进行曲线编辑和自定义 - """ - - def __init__(self, morphing_core: MorphingCore): - """ - 初始化动画曲线编辑器 - :param morphing_core: Morphing核心系统 - """ - self.morphing_core = morphing_core - self.curves: Dict[str, AnimationCurve] = {} - self.curve_values: Dict[str, List[Tuple[float, float]]] = {} # {curve_name: [(time, value)]} - self.is_updating = False - self.update_rate = ADVANCED_SETTINGS['curve_editor_points'] - self.last_update_time: float = 0.0 - - print("动画曲线编辑器初始化完成") - - def create_curve(self, curve_name: str, curve_type: CurveType, - control_points: List[Tuple[float, float]], - interpolation: InterpolationMode = InterpolationMode.LINEAR) -> bool: - """ - 创建动画曲线 - :param curve_name: 曲线名称 - :param curve_type: 曲线类型 - :param control_points: 控制点列表 [(time, value)] - :param interpolation: 插值模式 - :return: 是否成功创建 - """ - # 验证控制点 - if not control_points: - print(f"错误: 控制点列表为空") - return False - - # 按时间排序控制点 - sorted_points = sorted(control_points, key=lambda p: p[0]) - - # 创建曲线 - curve = AnimationCurve( - name=curve_name, - curve_type=curve_type, - control_points=sorted_points, - interpolation=interpolation, - active=True - ) - - # 存储曲线 - self.curves[curve_name] = curve - - # 预计算曲线值 - self._precompute_curve_values(curve_name) - - print(f"动画曲线已创建: {curve_name}") - return True - - def remove_curve(self, curve_name: str) -> bool: - """ - 移除动画曲线 - :param curve_name: 曲线名称 - :return: 是否成功移除 - """ - if curve_name not in self.curves: - print(f"错误: 动画曲线 {curve_name} 不存在") - return False - - del self.curves[curve_name] - - if curve_name in self.curve_values: - del self.curve_values[curve_name] - - print(f"动画曲线已移除: {curve_name}") - return True - - def _precompute_curve_values(self, curve_name: str) -> None: - """ - 预计算曲线值 - :param curve_name: 曲线名称 - """ - if curve_name not in self.curves: - return - - curve = self.curves[curve_name] - values = [] - - # 根据曲线类型生成值 - if curve.curve_type == CurveType.LINEAR: - values = self._compute_linear_curve(curve.control_points) - elif curve.curve_type == CurveType.BEZIER: - values = self._compute_bezier_curve(curve.control_points) - elif curve.curve_type == CurveType.SPLINE: - values = self._compute_spline_curve(curve.control_points) - elif curve.curve_type == CurveType.STEP: - values = self._compute_step_curve(curve.control_points) - elif curve.curve_type == CurveType.NOISE: - values = self._compute_noise_curve(curve.control_points) - - self.curve_values[curve_name] = values - - def _compute_linear_curve(self, control_points: List[Tuple[float, float]]) -> List[Tuple[float, float]]: - """ - 计算线性曲线值 - :param control_points: 控制点列表 - :return: 曲线值列表 - """ - if len(control_points) < 2: - return control_points - - values = [] - num_points = ADVANCED_SETTINGS['curve_editor_points'] - - for i in range(len(control_points) - 1): - start_point = control_points[i] - end_point = control_points[i + 1] - - # 计算这一段的点数 - segment_duration = end_point[0] - start_point[0] - total_duration = control_points[-1][0] - control_points[0][0] - segment_points = int(num_points * (segment_duration / total_duration)) if total_duration > 0 else num_points - - for j in range(segment_points): - t = j / max(1, segment_points - 1) - time = start_point[0] + (end_point[0] - start_point[0]) * t - value = start_point[1] + (end_point[1] - start_point[1]) * t - values.append((time, value)) - - return values - - def _compute_bezier_curve(self, control_points: List[Tuple[float, float]]) -> List[Tuple[float, float]]: - """ - 计算贝塞尔曲线值 - :param control_points: 控制点列表 - :return: 曲线值列表 - """ - if len(control_points) < 2: - return control_points - - values = [] - num_points = ADVANCED_SETTINGS['curve_editor_points'] - - # 对于贝塞尔曲线,我们使用相邻的4个点 - for i in range(max(0, len(control_points) - 3)): - if i + 3 >= len(control_points): - break - - p0 = control_points[i] - p1 = control_points[i + 1] - p2 = control_points[i + 2] - p3 = control_points[i + 3] - - # 计算这一段的点数 - segment_points = num_points // max(1, len(control_points) - 3) - - for j in range(segment_points): - t = j / max(1, segment_points - 1) - - # 贝塞尔曲线公式 - x = ((1 - t) ** 3) * p0[0] + 3 * ((1 - t) ** 2) * t * p1[0] + 3 * (1 - t) * (t ** 2) * p2[0] + (t ** 3) * p3[0] - y = ((1 - t) ** 3) * p0[1] + 3 * ((1 - t) ** 2) * t * p1[1] + 3 * (1 - t) * (t ** 2) * p2[1] + (t ** 3) * p3[1] - - values.append((x, y)) - - return values - - def _compute_spline_curve(self, control_points: List[Tuple[float, float]]) -> List[Tuple[float, float]]: - """ - 计算样条曲线值 - :param control_points: 控制点列表 - :return: 曲线值列表 - """ - # 简化的样条实现,使用Catmull-Rom样条 - if len(control_points) < 2: - return control_points - - values = [] - num_points = ADVANCED_SETTINGS['curve_editor_points'] - - for i in range(len(control_points) - 1): - # 获取四个控制点(处理边界情况) - p0 = control_points[max(0, i - 1)] - p1 = control_points[i] - p2 = control_points[i + 1] - p3 = control_points[min(len(control_points) - 1, i + 2)] - - # 计算这一段的点数 - segment_points = num_points // max(1, len(control_points) - 1) - - for j in range(segment_points): - t = j / max(1, segment_points - 1) - - # Catmull-Rom样条公式 - t2 = t * t - t3 = t2 * t - - x = 0.5 * ( - (2 * p1[0]) + - (-p0[0] + p2[0]) * t + - (2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 + - (-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3 - ) - - y = 0.5 * ( - (2 * p1[1]) + - (-p0[1] + p2[1]) * t + - (2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 + - (-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3 - ) - - values.append((x, y)) - - return values - - def _compute_step_curve(self, control_points: List[Tuple[float, float]]) -> List[Tuple[float, float]]: - """ - 计算阶梯曲线值 - :param control_points: 控制点列表 - :return: 曲线值列表 - """ - if not control_points: - return [] - - values = [] - num_points = ADVANCED_SETTINGS['curve_editor_points'] - - # 在每个控制点之间创建阶梯 - for i in range(len(control_points)): - point = control_points[i] - next_point = control_points[i + 1] if i + 1 < len(control_points) else point - - # 计算这一段的点数 - segment_points = num_points // max(1, len(control_points)) - - for j in range(segment_points): - # 阶梯曲线保持前一个点的值直到下一个点 - values.append((point[0] + (next_point[0] - point[0]) * j / max(1, segment_points - 1), point[1])) - - return values - - def _compute_noise_curve(self, control_points: List[Tuple[float, float]]) -> List[Tuple[float, float]]: - """ - 计算噪声曲线值 - :param control_points: 控制点列表 - :return: 曲线值列表 - """ - if not control_points: - return [] - - values = [] - num_points = ADVANCED_SETTINGS['curve_editor_points'] - - # 基于控制点生成噪声曲线 - for i in range(num_points): - t = i / max(1, num_points - 1) - - # 计算基础值(线性插值) - base_value = 0.0 - for j in range(len(control_points) - 1): - if control_points[j][0] <= t <= control_points[j + 1][0]: - t_segment = (t - control_points[j][0]) / (control_points[j + 1][0] - control_points[j][0]) - base_value = control_points[j][1] + (control_points[j + 1][1] - control_points[j][1]) * t_segment - break - - # 添加噪声 - noise = (np.random.random() - 0.5) * 0.2 # ±10%的噪声 - values.append((t, base_value + noise)) - - return values - - def get_curve_value(self, curve_name: str, time: float) -> Optional[float]: - """ - 获取曲线在指定时间的值 - :param curve_name: 曲线名称 - :param time: 时间 - :return: 曲线值 - """ - if curve_name not in self.curve_values: - return None - - values = self.curve_values[curve_name] - if not values: - return None - - # 找到最接近的时间点 - closest_value = values[0][1] - min_diff = abs(values[0][0] - time) - - for t, v in values: - diff = abs(t - time) - if diff < min_diff: - min_diff = diff - closest_value = v - - return closest_value - - def apply_curve_to_morph_target(self, curve_name: str, model_id: str, - target_name: str, time: float, - base_weight: float = 1.0) -> bool: - """ - 将曲线应用到Morph目标 - :param curve_name: 曲线名称 - :param model_id: 模型ID - :param target_name: Morph目标名称 - :param time: 时间 - :param base_weight: 基础权重 - :return: 是否成功应用 - """ - # 获取曲线值 - curve_value = self.get_curve_value(curve_name, time) - if curve_value is None: - return False - - # 计算最终权重 - final_weight = base_weight * curve_value - - # 应用到Morph目标 - return self.morphing_core.set_morph_target_weight(model_id, target_name, final_weight) - - def get_curve_info(self, curve_name: str) -> Optional[Dict[str, Any]]: - """ - 获取曲线信息 - :param curve_name: 曲线名称 - :return: 曲线信息字典 - """ - if curve_name not in self.curves: - print(f"错误: 动画曲线 {curve_name} 不存在") - return None - - curve = self.curves[curve_name] - - return { - 'name': curve.name, - 'curve_type': curve.curve_type.name, - 'control_points': curve.control_points, - 'interpolation': curve.interpolation.name, - 'active': curve.active, - 'precomputed_points': len(self.curve_values.get(curve_name, [])) - } - - def update(self, dt: float) -> None: - """ - 更新曲线编辑器 - :param dt: 时间增量 - """ - if not self.is_updating: - return - - current_time = globalClock.getRealTime() - - # 限制更新频率 - if current_time - self.last_update_time < 1.0 / self.update_rate: - return - - self.last_update_time = current_time - - # 这里可以添加实时曲线编辑的更新逻辑 - pass - - def start_updating(self) -> None: - """ - 开始更新曲线编辑器 - """ - if not self.is_updating: - self.is_updating = True - print("动画曲线编辑器更新已启动") - - def stop_updating(self) -> None: - """ - 停止更新曲线编辑器 - """ - if self.is_updating: - self.is_updating = False - print("动画曲线编辑器更新已停止") - -class PhysicsAnimationIntegration: - """ - 物理动画集成系统 - 结合物理模拟与骨骼动画 - """ - - def __init__(self, morphing_core: MorphingCore): - """ - 初始化物理动画集成系统 - :param morphing_core: Morphing核心系统 - """ - self.morphing_core = morphing_core - self.constraints: Dict[str, Dict[str, PhysicsConstraint]] = {} # {model_id: {constraint_name: PhysicsConstraint}} - self.is_updating = False - self.update_rate = ADVANCED_SETTINGS['physics_update_rate'] - self.last_update_time: float = 0.0 - - print("物理动画集成系统初始化完成") - - def add_constraint(self, model_id: str, constraint_name: str, constraint_type: str, - target_bone: str, stiffness: float = 1.0, damping: float = 0.1, - rest_value: float = 0.0) -> bool: - """ - 添加物理约束 - :param model_id: 模型ID - :param constraint_name: 约束名称 - :param constraint_type: 约束类型 ('spring', 'distance', 'angle') - :param target_bone: 目标骨骼 - :param stiffness: 刚度 - :param damping: 阻尼 - :param rest_value: 静止值 - :return: 是否成功添加 - """ - if model_id not in self.morphing_core.model_data: - print(f"错误: 模型 {model_id} 未注册") - return False - - # 创建约束 - constraint = PhysicsConstraint( - name=constraint_name, - constraint_type=constraint_type, - target_bone=target_bone, - stiffness=stiffness, - damping=damping, - rest_value=rest_value, - active=True - ) - - # 存储约束 - if model_id not in self.constraints: - self.constraints[model_id] = {} - self.constraints[model_id][constraint_name] = constraint - - print(f"物理约束已添加: {constraint_name} (模型: {model_id})") - return True - - def remove_constraint(self, model_id: str, constraint_name: str) -> bool: - """ - 移除物理约束 - :param model_id: 模型ID - :param constraint_name: 约束名称 - :return: 是否成功移除 - """ - if model_id not in self.constraints or constraint_name not in self.constraints[model_id]: - print(f"错误: 物理约束 {constraint_name} 不存在于模型 {model_id} 中") - return False - - del self.constraints[model_id][constraint_name] - print(f"物理约束已移除: {constraint_name} (模型: {model_id})") - return True - - def update(self, dt: float) -> None: - """ - 更新物理动画集成系统 - :param dt: 时间增量 - """ - if not self.is_updating: - return - - current_time = globalClock.getRealTime() - - # 限制更新频率 - if current_time - self.last_update_time < 1.0 / self.update_rate: - return - - self.last_update_time = current_time - - # 更新每个模型的物理约束 - for model_id, constraints in self.constraints.items(): - self._update_model_constraints(model_id, constraints, dt) - - def _update_model_constraints(self, model_id: str, constraints: Dict[str, PhysicsConstraint], dt: float) -> None: - """ - 更新模型的物理约束 - :param model_id: 模型ID - :param constraints: 约束字典 - :param dt: 时间增量 - """ - # 这里实现简化的弹簧物理系统 - for constraint_name, constraint in constraints.items(): - if not constraint.active: - continue - - # 根据约束类型应用不同的物理效果 - if constraint.constraint_type == 'spring': - self._apply_spring_constraint(model_id, constraint, dt) - elif constraint.constraint_type == 'distance': - self._apply_distance_constraint(model_id, constraint, dt) - elif constraint.constraint_type == 'angle': - self._apply_angle_constraint(model_id, constraint, dt) - - def _apply_spring_constraint(self, model_id: str, constraint: PhysicsConstraint, dt: float) -> None: - """ - 应用弹簧约束 - :param model_id: 模型ID - :param constraint: 约束对象 - :param dt: 时间增量 - """ - # 简化的弹簧物理实现 - # 在实际项目中,这里会涉及更复杂的物理计算 - - # 模拟弹簧运动 - # F = -k * x - d * v (胡克定律与阻尼) - - # 获取当前值(这里简化处理) - current_value = 0.0 # 在实际实现中,需要获取当前的morph目标权重或其他值 - - # 计算位移 - displacement = current_value - constraint.rest_value - - # 计算力 - force = -constraint.stiffness * displacement # 弹簧力 - # 这里简化处理,省略速度和阻尼计算 - - # 计算加速度 (F = ma, 假设质量为1) - acceleration = force - - # 更新值(简化积分) - new_value = current_value + acceleration * dt * dt - - # 应用到目标(示例:应用到morph目标) - # self.morphing_core.set_morph_target_weight(model_id, constraint.target_bone, new_value) - - def _apply_distance_constraint(self, model_id: str, constraint: PhysicsConstraint, dt: float) -> None: - """ - 应用距离约束 - :param model_id: 模型ID - :param constraint: 约束对象 - :param dt: 时间增量 - """ - # 距离约束实现 - pass # 简化处理,实际项目中需要具体实现 - - def _apply_angle_constraint(self, model_id: str, constraint: PhysicsConstraint, dt: float) -> None: - """ - 应用角度约束 - :param model_id: 模型ID - :param constraint: 约束对象 - :param dt: 时间增量 - """ - # 角度约束实现 - pass # 简化处理,实际项目中需要具体实现 - - def get_constraint_info(self, model_id: str, constraint_name: str) -> Optional[Dict[str, Any]]: - """ - 获取约束信息 - :param model_id: 模型ID - :param constraint_name: 约束名称 - :return: 约束信息字典 - """ - if model_id not in self.constraints or constraint_name not in self.constraints[model_id]: - print(f"错误: 物理约束 {constraint_name} 不存在于模型 {model_id} 中") - return None - - constraint = self.constraints[model_id][constraint_name] - - return { - 'name': constraint.name, - 'constraint_type': constraint.constraint_type, - 'target_bone': constraint.target_bone, - 'stiffness': constraint.stiffness, - 'damping': constraint.damping, - 'rest_value': constraint.rest_value, - 'active': constraint.active - } - - def start_updating(self) -> None: - """ - 开始更新物理动画集成系统 - """ - if not self.is_updating: - self.is_updating = True - print("物理动画集成系统更新已启动") - - def stop_updating(self) -> None: - """ - 停止更新物理动画集成系统 - """ - if self.is_updating: - self.is_updating = False +""" +Morphing和变形动画插件 - 高级功能模块 +提供动画状态机、曲线编辑器、物理集成等高级功能 +""" + +import math +import numpy as np +from typing import Dict, List, Tuple, Optional, Any, Callable, Union +from dataclasses import dataclass, field +from enum import Enum + +from panda3d.core import * +from direct.actor.Actor import Actor +from direct.showbase.DirectObject import DirectObject +from direct.interval.IntervalGlobal import * + +# 导入核心模块 +from .core import MorphingCore, InterpolationMode + +# 定义常量 +ADVANCED_SETTINGS = { + 'state_machine_update_rate': 30, + 'curve_editor_points': 100, + 'physics_update_rate': 60 +} + +# 枚举定义 +class AnimationStateType(Enum): + """动画状态类型枚举""" + IDLE = 0 + PLAYING = 1 + TRANSITION = 2 + BLENDING = 3 + PAUSED = 4 + +class TransitionType(Enum): + """转换类型枚举""" + INSTANT = 0 # 瞬间转换 + SMOOTH = 1 # 平滑转换 + BLEND = 2 # 混合转换 + CONDITIONAL = 3 # 条件转换 + +class CurveType(Enum): + """曲线类型枚举""" + LINEAR = 0 # 线性 + BEZIER = 1 # 贝塞尔 + SPLINE = 2 # 样条 + STEP = 3 # 阶梯 + NOISE = 4 # 噪声 + +@dataclass +class AnimationState: + """动画状态数据类""" + name: str + morph_targets: Dict[str, float] # {target_name: weight} + duration: float = 0.0 + loop: bool = False + play_rate: float = 1.0 + active: bool = True + +@dataclass +class StateTransition: + """状态转换数据类""" + from_state: str + to_state: str + transition_type: TransitionType + condition: Optional[Callable] = None # 转换条件函数 + transition_time: float = 0.3 # 转换时间 + blend_mode: InterpolationMode = InterpolationMode.LINEAR + +@dataclass +class AnimationCurve: + """动画曲线数据类""" + name: str + curve_type: CurveType + control_points: List[Tuple[float, float]] # 控制点 [(time, value)] + interpolation: InterpolationMode = InterpolationMode.LINEAR + active: bool = True + +@dataclass +class PhysicsConstraint: + """物理约束数据类""" + name: str + constraint_type: str # 'spring', 'distance', 'angle' + target_bone: str + stiffness: float = 1.0 + damping: float = 0.1 + rest_value: float = 0.0 + active: bool = True + +class AnimationStateMachine: + """ + 动画状态机 + 管理复杂的动画状态转换和混合 + """ + + def __init__(self, morphing_core: MorphingCore, model_id: str): + """ + 初始化动画状态机 + :param morphing_core: Morphing核心系统 + :param model_id: 模型ID + """ + self.morphing_core = morphing_core + self.model_id = model_id + self.states: Dict[str, AnimationState] = {} + self.transitions: List[StateTransition] = [] + self.current_state: Optional[str] = None + self.previous_state: Optional[str] = None + self.transition_progress: float = 0.0 + self.is_transitioning: bool = False + self.is_updating: bool = False + self.update_rate = ADVANCED_SETTINGS['state_machine_update_rate'] + self.last_update_time: float = 0.0 + + print(f"动画状态机初始化完成 (模型: {model_id})") + + def add_state(self, state_name: str, morph_targets: Dict[str, float], + duration: float = 0.0, loop: bool = False, play_rate: float = 1.0) -> bool: + """ + 添加动画状态 + :param state_name: 状态名称 + :param morph_targets: Morph目标权重字典 {target_name: weight} + :param duration: 持续时间 + :param loop: 是否循环 + :param play_rate: 播放速度 + :return: 是否成功添加 + """ + # 验证Morph目标是否存在 + for target_name in morph_targets.keys(): + if (self.model_id not in self.morphing_core.morph_targets or + target_name not in self.morphing_core.morph_targets[self.model_id]): + print(f"错误: Morph目标 {target_name} 不存在于模型 {self.model_id} 中") + return False + + # 创建状态 + state = AnimationState( + name=state_name, + morph_targets=morph_targets, + duration=duration, + loop=loop, + play_rate=play_rate, + active=True + ) + + # 存储状态 + self.states[state_name] = state + print(f"动画状态已添加: {state_name} (模型: {self.model_id})") + return True + + def remove_state(self, state_name: str) -> bool: + """ + 移除动画状态 + :param state_name: 状态名称 + :return: 是否成功移除 + """ + if state_name not in self.states: + print(f"错误: 动画状态 {state_name} 不存在") + return False + + del self.states[state_name] + + # 移除相关的转换 + self.transitions = [t for t in self.transitions + if t.from_state != state_name and t.to_state != state_name] + + print(f"动画状态已移除: {state_name} (模型: {self.model_id})") + return True + + def set_transition(self, from_state: str, to_state: str, + transition_type: TransitionType = TransitionType.SMOOTH, + condition: Optional[Callable] = None, + transition_time: float = 0.3, + blend_mode: InterpolationMode = InterpolationMode.LINEAR) -> bool: + """ + 设置状态转换 + :param from_state: 起始状态 + :param to_state: 目标状态 + :param transition_type: 转换类型 + :param condition: 转换条件函数 + :param transition_time: 转换时间 + :param blend_mode: 混合模式 + :return: 是否成功设置 + """ + # 验证状态是否存在 + if from_state not in self.states: + print(f"错误: 起始状态 {from_state} 不存在") + return False + + if to_state not in self.states: + print(f"错误: 目标状态 {to_state} 不存在") + return False + + # 创建转换 + transition = StateTransition( + from_state=from_state, + to_state=to_state, + transition_type=transition_type, + condition=condition, + transition_time=transition_time, + blend_mode=blend_mode + ) + + # 存储转换 + self.transitions.append(transition) + print(f"状态转换已设置: {from_state} -> {to_state} (模型: {self.model_id})") + return True + + def enter_state(self, state_name: str) -> bool: + """ + 进入指定状态 + :param state_name: 状态名称 + :return: 是否成功进入 + """ + if state_name not in self.states: + print(f"错误: 动画状态 {state_name} 不存在") + return False + + self.previous_state = self.current_state + self.current_state = state_name + self.is_transitioning = False + + # 应用状态的Morph目标权重 + state = self.states[state_name] + for target_name, weight in state.morph_targets.items(): + self.morphing_core.set_morph_target_weight(self.model_id, target_name, weight) + + print(f"进入动画状态: {state_name} (模型: {self.model_id})") + return True + + def trigger_transition(self, to_state: str) -> bool: + """ + 触发状态转换 + :param to_state: 目标状态 + :return: 是否成功触发 + """ + if not self.current_state: + print(f"错误: 当前没有活动状态") + return False + + if to_state not in self.states: + print(f"错误: 目标状态 {to_state} 不存在") + return False + + # 查找转换 + transition = None + for t in self.transitions: + if t.from_state == self.current_state and t.to_state == to_state: + transition = t + break + + if not transition: + print(f"错误: 从 {self.current_state} 到 {to_state} 的转换不存在") + return False + + # 开始转换 + self.is_transitioning = True + self.transition_progress = 0.0 + self.previous_state = self.current_state + self.current_state = to_state + + print(f"触发状态转换: {self.previous_state} -> {to_state} (模型: {self.model_id})") + return True + + def update(self, dt: float) -> None: + """ + 更新状态机 + :param dt: 时间增量 + """ + if not self.is_updating: + return + + current_time = globalClock.getRealTime() + + # 限制更新频率 + if current_time - self.last_update_time < 1.0 / self.update_rate: + return + + self.last_update_time = current_time + + # 更新状态转换 + if self.is_transitioning: + self._update_transition(dt) + + # 检查条件转换 + self._check_conditional_transitions() + + def _update_transition(self, dt: float) -> None: + """ + 更新状态转换 + :param dt: 时间增量 + """ + if not self.is_transitioning or not self.previous_state or not self.current_state: + return + + # 更新转换进度 + transition = None + for t in self.transitions: + if t.from_state == self.previous_state and t.to_state == self.current_state: + transition = t + break + + if not transition: + self.is_transitioning = False + return + + self.transition_progress += dt / transition.transition_time + + # 检查转换是否完成 + if self.transition_progress >= 1.0: + self.is_transitioning = False + self.transition_progress = 1.0 + print(f"状态转换完成: {self.previous_state} -> {self.current_state} (模型: {self.model_id})") + return + + # 计算插值因子 + t = self.transition_progress + if transition.blend_mode == InterpolationMode.EASE_IN: + t = t * t + elif transition.blend_mode == InterpolationMode.EASE_OUT: + t = 1.0 - (1.0 - t) * (1.0 - t) + elif transition.blend_mode == InterpolationMode.EASE_IN_OUT: + t = t * t * (3.0 - 2.0 * t) + + # 混合两个状态的权重 + from_state = self.states[self.previous_state] + to_state = self.states[self.current_state] + + # 获取所有涉及的Morph目标 + all_targets = set(from_state.morph_targets.keys()) | set(to_state.morph_targets.keys()) + + for target_name in all_targets: + from_weight = from_state.morph_targets.get(target_name, 0.0) + to_weight = to_state.morph_targets.get(target_name, 0.0) + + # 计算混合权重 + blended_weight = from_weight * (1.0 - t) + to_weight * t + + # 应用权重 + self.morphing_core.set_morph_target_weight(self.model_id, target_name, blended_weight) + + def _check_conditional_transitions(self) -> None: + """ + 检查条件转换 + """ + if not self.current_state: + return + + # 检查从当前状态出发的条件转换 + for transition in self.transitions: + if (transition.from_state == self.current_state and + transition.transition_type == TransitionType.CONDITIONAL and + transition.condition): + + try: + # 检查条件是否满足 + if transition.condition(): + self.trigger_transition(transition.to_state) + break + except Exception as e: + print(f"检查转换条件时出错: {e}") + + def get_state_info(self, state_name: str) -> Optional[Dict[str, Any]]: + """ + 获取状态信息 + :param state_name: 状态名称 + :return: 状态信息字典 + """ + if state_name not in self.states: + print(f"错误: 动画状态 {state_name} 不存在") + return None + + state = self.states[state_name] + + return { + 'name': state.name, + 'morph_targets': state.morph_targets, + 'duration': state.duration, + 'loop': state.loop, + 'play_rate': state.play_rate, + 'active': state.active + } + + def get_current_state(self) -> Optional[str]: + """ + 获取当前状态 + :return: 当前状态名称 + """ + return self.current_state + + def start_updating(self) -> None: + """ + 开始更新状态机 + """ + if not self.is_updating: + self.is_updating = True + print(f"动画状态机更新已启动 (模型: {self.model_id})") + + def stop_updating(self) -> None: + """ + 停止更新状态机 + """ + if self.is_updating: + self.is_updating = False + print(f"动画状态机更新已停止 (模型: {self.model_id})") + +class AnimationCurveEditor: + """ + 动画曲线编辑器 + 支持对动画属性进行曲线编辑和自定义 + """ + + def __init__(self, morphing_core: MorphingCore): + """ + 初始化动画曲线编辑器 + :param morphing_core: Morphing核心系统 + """ + self.morphing_core = morphing_core + self.curves: Dict[str, AnimationCurve] = {} + self.curve_values: Dict[str, List[Tuple[float, float]]] = {} # {curve_name: [(time, value)]} + self.is_updating = False + self.update_rate = ADVANCED_SETTINGS['curve_editor_points'] + self.last_update_time: float = 0.0 + + print("动画曲线编辑器初始化完成") + + def create_curve(self, curve_name: str, curve_type: CurveType, + control_points: List[Tuple[float, float]], + interpolation: InterpolationMode = InterpolationMode.LINEAR) -> bool: + """ + 创建动画曲线 + :param curve_name: 曲线名称 + :param curve_type: 曲线类型 + :param control_points: 控制点列表 [(time, value)] + :param interpolation: 插值模式 + :return: 是否成功创建 + """ + # 验证控制点 + if not control_points: + print(f"错误: 控制点列表为空") + return False + + # 按时间排序控制点 + sorted_points = sorted(control_points, key=lambda p: p[0]) + + # 创建曲线 + curve = AnimationCurve( + name=curve_name, + curve_type=curve_type, + control_points=sorted_points, + interpolation=interpolation, + active=True + ) + + # 存储曲线 + self.curves[curve_name] = curve + + # 预计算曲线值 + self._precompute_curve_values(curve_name) + + print(f"动画曲线已创建: {curve_name}") + return True + + def remove_curve(self, curve_name: str) -> bool: + """ + 移除动画曲线 + :param curve_name: 曲线名称 + :return: 是否成功移除 + """ + if curve_name not in self.curves: + print(f"错误: 动画曲线 {curve_name} 不存在") + return False + + del self.curves[curve_name] + + if curve_name in self.curve_values: + del self.curve_values[curve_name] + + print(f"动画曲线已移除: {curve_name}") + return True + + def _precompute_curve_values(self, curve_name: str) -> None: + """ + 预计算曲线值 + :param curve_name: 曲线名称 + """ + if curve_name not in self.curves: + return + + curve = self.curves[curve_name] + values = [] + + # 根据曲线类型生成值 + if curve.curve_type == CurveType.LINEAR: + values = self._compute_linear_curve(curve.control_points) + elif curve.curve_type == CurveType.BEZIER: + values = self._compute_bezier_curve(curve.control_points) + elif curve.curve_type == CurveType.SPLINE: + values = self._compute_spline_curve(curve.control_points) + elif curve.curve_type == CurveType.STEP: + values = self._compute_step_curve(curve.control_points) + elif curve.curve_type == CurveType.NOISE: + values = self._compute_noise_curve(curve.control_points) + + self.curve_values[curve_name] = values + + def _compute_linear_curve(self, control_points: List[Tuple[float, float]]) -> List[Tuple[float, float]]: + """ + 计算线性曲线值 + :param control_points: 控制点列表 + :return: 曲线值列表 + """ + if len(control_points) < 2: + return control_points + + values = [] + num_points = ADVANCED_SETTINGS['curve_editor_points'] + + for i in range(len(control_points) - 1): + start_point = control_points[i] + end_point = control_points[i + 1] + + # 计算这一段的点数 + segment_duration = end_point[0] - start_point[0] + total_duration = control_points[-1][0] - control_points[0][0] + segment_points = int(num_points * (segment_duration / total_duration)) if total_duration > 0 else num_points + + for j in range(segment_points): + t = j / max(1, segment_points - 1) + time = start_point[0] + (end_point[0] - start_point[0]) * t + value = start_point[1] + (end_point[1] - start_point[1]) * t + values.append((time, value)) + + return values + + def _compute_bezier_curve(self, control_points: List[Tuple[float, float]]) -> List[Tuple[float, float]]: + """ + 计算贝塞尔曲线值 + :param control_points: 控制点列表 + :return: 曲线值列表 + """ + if len(control_points) < 2: + return control_points + + values = [] + num_points = ADVANCED_SETTINGS['curve_editor_points'] + + # 对于贝塞尔曲线,我们使用相邻的4个点 + for i in range(max(0, len(control_points) - 3)): + if i + 3 >= len(control_points): + break + + p0 = control_points[i] + p1 = control_points[i + 1] + p2 = control_points[i + 2] + p3 = control_points[i + 3] + + # 计算这一段的点数 + segment_points = num_points // max(1, len(control_points) - 3) + + for j in range(segment_points): + t = j / max(1, segment_points - 1) + + # 贝塞尔曲线公式 + x = ((1 - t) ** 3) * p0[0] + 3 * ((1 - t) ** 2) * t * p1[0] + 3 * (1 - t) * (t ** 2) * p2[0] + (t ** 3) * p3[0] + y = ((1 - t) ** 3) * p0[1] + 3 * ((1 - t) ** 2) * t * p1[1] + 3 * (1 - t) * (t ** 2) * p2[1] + (t ** 3) * p3[1] + + values.append((x, y)) + + return values + + def _compute_spline_curve(self, control_points: List[Tuple[float, float]]) -> List[Tuple[float, float]]: + """ + 计算样条曲线值 + :param control_points: 控制点列表 + :return: 曲线值列表 + """ + # 简化的样条实现,使用Catmull-Rom样条 + if len(control_points) < 2: + return control_points + + values = [] + num_points = ADVANCED_SETTINGS['curve_editor_points'] + + for i in range(len(control_points) - 1): + # 获取四个控制点(处理边界情况) + p0 = control_points[max(0, i - 1)] + p1 = control_points[i] + p2 = control_points[i + 1] + p3 = control_points[min(len(control_points) - 1, i + 2)] + + # 计算这一段的点数 + segment_points = num_points // max(1, len(control_points) - 1) + + for j in range(segment_points): + t = j / max(1, segment_points - 1) + + # Catmull-Rom样条公式 + t2 = t * t + t3 = t2 * t + + x = 0.5 * ( + (2 * p1[0]) + + (-p0[0] + p2[0]) * t + + (2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 + + (-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3 + ) + + y = 0.5 * ( + (2 * p1[1]) + + (-p0[1] + p2[1]) * t + + (2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 + + (-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3 + ) + + values.append((x, y)) + + return values + + def _compute_step_curve(self, control_points: List[Tuple[float, float]]) -> List[Tuple[float, float]]: + """ + 计算阶梯曲线值 + :param control_points: 控制点列表 + :return: 曲线值列表 + """ + if not control_points: + return [] + + values = [] + num_points = ADVANCED_SETTINGS['curve_editor_points'] + + # 在每个控制点之间创建阶梯 + for i in range(len(control_points)): + point = control_points[i] + next_point = control_points[i + 1] if i + 1 < len(control_points) else point + + # 计算这一段的点数 + segment_points = num_points // max(1, len(control_points)) + + for j in range(segment_points): + # 阶梯曲线保持前一个点的值直到下一个点 + values.append((point[0] + (next_point[0] - point[0]) * j / max(1, segment_points - 1), point[1])) + + return values + + def _compute_noise_curve(self, control_points: List[Tuple[float, float]]) -> List[Tuple[float, float]]: + """ + 计算噪声曲线值 + :param control_points: 控制点列表 + :return: 曲线值列表 + """ + if not control_points: + return [] + + values = [] + num_points = ADVANCED_SETTINGS['curve_editor_points'] + + # 基于控制点生成噪声曲线 + for i in range(num_points): + t = i / max(1, num_points - 1) + + # 计算基础值(线性插值) + base_value = 0.0 + for j in range(len(control_points) - 1): + if control_points[j][0] <= t <= control_points[j + 1][0]: + t_segment = (t - control_points[j][0]) / (control_points[j + 1][0] - control_points[j][0]) + base_value = control_points[j][1] + (control_points[j + 1][1] - control_points[j][1]) * t_segment + break + + # 添加噪声 + noise = (np.random.random() - 0.5) * 0.2 # ±10%的噪声 + values.append((t, base_value + noise)) + + return values + + def get_curve_value(self, curve_name: str, time: float) -> Optional[float]: + """ + 获取曲线在指定时间的值 + :param curve_name: 曲线名称 + :param time: 时间 + :return: 曲线值 + """ + if curve_name not in self.curve_values: + return None + + values = self.curve_values[curve_name] + if not values: + return None + + # 找到最接近的时间点 + closest_value = values[0][1] + min_diff = abs(values[0][0] - time) + + for t, v in values: + diff = abs(t - time) + if diff < min_diff: + min_diff = diff + closest_value = v + + return closest_value + + def apply_curve_to_morph_target(self, curve_name: str, model_id: str, + target_name: str, time: float, + base_weight: float = 1.0) -> bool: + """ + 将曲线应用到Morph目标 + :param curve_name: 曲线名称 + :param model_id: 模型ID + :param target_name: Morph目标名称 + :param time: 时间 + :param base_weight: 基础权重 + :return: 是否成功应用 + """ + # 获取曲线值 + curve_value = self.get_curve_value(curve_name, time) + if curve_value is None: + return False + + # 计算最终权重 + final_weight = base_weight * curve_value + + # 应用到Morph目标 + return self.morphing_core.set_morph_target_weight(model_id, target_name, final_weight) + + def get_curve_info(self, curve_name: str) -> Optional[Dict[str, Any]]: + """ + 获取曲线信息 + :param curve_name: 曲线名称 + :return: 曲线信息字典 + """ + if curve_name not in self.curves: + print(f"错误: 动画曲线 {curve_name} 不存在") + return None + + curve = self.curves[curve_name] + + return { + 'name': curve.name, + 'curve_type': curve.curve_type.name, + 'control_points': curve.control_points, + 'interpolation': curve.interpolation.name, + 'active': curve.active, + 'precomputed_points': len(self.curve_values.get(curve_name, [])) + } + + def update(self, dt: float) -> None: + """ + 更新曲线编辑器 + :param dt: 时间增量 + """ + if not self.is_updating: + return + + current_time = globalClock.getRealTime() + + # 限制更新频率 + if current_time - self.last_update_time < 1.0 / self.update_rate: + return + + self.last_update_time = current_time + + # 这里可以添加实时曲线编辑的更新逻辑 + pass + + def start_updating(self) -> None: + """ + 开始更新曲线编辑器 + """ + if not self.is_updating: + self.is_updating = True + print("动画曲线编辑器更新已启动") + + def stop_updating(self) -> None: + """ + 停止更新曲线编辑器 + """ + if self.is_updating: + self.is_updating = False + print("动画曲线编辑器更新已停止") + +class PhysicsAnimationIntegration: + """ + 物理动画集成系统 + 结合物理模拟与骨骼动画 + """ + + def __init__(self, morphing_core: MorphingCore): + """ + 初始化物理动画集成系统 + :param morphing_core: Morphing核心系统 + """ + self.morphing_core = morphing_core + self.constraints: Dict[str, Dict[str, PhysicsConstraint]] = {} # {model_id: {constraint_name: PhysicsConstraint}} + self.is_updating = False + self.update_rate = ADVANCED_SETTINGS['physics_update_rate'] + self.last_update_time: float = 0.0 + + print("物理动画集成系统初始化完成") + + def add_constraint(self, model_id: str, constraint_name: str, constraint_type: str, + target_bone: str, stiffness: float = 1.0, damping: float = 0.1, + rest_value: float = 0.0) -> bool: + """ + 添加物理约束 + :param model_id: 模型ID + :param constraint_name: 约束名称 + :param constraint_type: 约束类型 ('spring', 'distance', 'angle') + :param target_bone: 目标骨骼 + :param stiffness: 刚度 + :param damping: 阻尼 + :param rest_value: 静止值 + :return: 是否成功添加 + """ + if model_id not in self.morphing_core.model_data: + print(f"错误: 模型 {model_id} 未注册") + return False + + # 创建约束 + constraint = PhysicsConstraint( + name=constraint_name, + constraint_type=constraint_type, + target_bone=target_bone, + stiffness=stiffness, + damping=damping, + rest_value=rest_value, + active=True + ) + + # 存储约束 + if model_id not in self.constraints: + self.constraints[model_id] = {} + self.constraints[model_id][constraint_name] = constraint + + print(f"物理约束已添加: {constraint_name} (模型: {model_id})") + return True + + def remove_constraint(self, model_id: str, constraint_name: str) -> bool: + """ + 移除物理约束 + :param model_id: 模型ID + :param constraint_name: 约束名称 + :return: 是否成功移除 + """ + if model_id not in self.constraints or constraint_name not in self.constraints[model_id]: + print(f"错误: 物理约束 {constraint_name} 不存在于模型 {model_id} 中") + return False + + del self.constraints[model_id][constraint_name] + print(f"物理约束已移除: {constraint_name} (模型: {model_id})") + return True + + def update(self, dt: float) -> None: + """ + 更新物理动画集成系统 + :param dt: 时间增量 + """ + if not self.is_updating: + return + + current_time = globalClock.getRealTime() + + # 限制更新频率 + if current_time - self.last_update_time < 1.0 / self.update_rate: + return + + self.last_update_time = current_time + + # 更新每个模型的物理约束 + for model_id, constraints in self.constraints.items(): + self._update_model_constraints(model_id, constraints, dt) + + def _update_model_constraints(self, model_id: str, constraints: Dict[str, PhysicsConstraint], dt: float) -> None: + """ + 更新模型的物理约束 + :param model_id: 模型ID + :param constraints: 约束字典 + :param dt: 时间增量 + """ + # 这里实现简化的弹簧物理系统 + for constraint_name, constraint in constraints.items(): + if not constraint.active: + continue + + # 根据约束类型应用不同的物理效果 + if constraint.constraint_type == 'spring': + self._apply_spring_constraint(model_id, constraint, dt) + elif constraint.constraint_type == 'distance': + self._apply_distance_constraint(model_id, constraint, dt) + elif constraint.constraint_type == 'angle': + self._apply_angle_constraint(model_id, constraint, dt) + + def _apply_spring_constraint(self, model_id: str, constraint: PhysicsConstraint, dt: float) -> None: + """ + 应用弹簧约束 + :param model_id: 模型ID + :param constraint: 约束对象 + :param dt: 时间增量 + """ + # 简化的弹簧物理实现 + # 在实际项目中,这里会涉及更复杂的物理计算 + + # 模拟弹簧运动 + # F = -k * x - d * v (胡克定律与阻尼) + + # 获取当前值(这里简化处理) + current_value = 0.0 # 在实际实现中,需要获取当前的morph目标权重或其他值 + + # 计算位移 + displacement = current_value - constraint.rest_value + + # 计算力 + force = -constraint.stiffness * displacement # 弹簧力 + # 这里简化处理,省略速度和阻尼计算 + + # 计算加速度 (F = ma, 假设质量为1) + acceleration = force + + # 更新值(简化积分) + new_value = current_value + acceleration * dt * dt + + # 应用到目标(示例:应用到morph目标) + # self.morphing_core.set_morph_target_weight(model_id, constraint.target_bone, new_value) + + def _apply_distance_constraint(self, model_id: str, constraint: PhysicsConstraint, dt: float) -> None: + """ + 应用距离约束 + :param model_id: 模型ID + :param constraint: 约束对象 + :param dt: 时间增量 + """ + # 距离约束实现 + pass # 简化处理,实际项目中需要具体实现 + + def _apply_angle_constraint(self, model_id: str, constraint: PhysicsConstraint, dt: float) -> None: + """ + 应用角度约束 + :param model_id: 模型ID + :param constraint: 约束对象 + :param dt: 时间增量 + """ + # 角度约束实现 + pass # 简化处理,实际项目中需要具体实现 + + def get_constraint_info(self, model_id: str, constraint_name: str) -> Optional[Dict[str, Any]]: + """ + 获取约束信息 + :param model_id: 模型ID + :param constraint_name: 约束名称 + :return: 约束信息字典 + """ + if model_id not in self.constraints or constraint_name not in self.constraints[model_id]: + print(f"错误: 物理约束 {constraint_name} 不存在于模型 {model_id} 中") + return None + + constraint = self.constraints[model_id][constraint_name] + + return { + 'name': constraint.name, + 'constraint_type': constraint.constraint_type, + 'target_bone': constraint.target_bone, + 'stiffness': constraint.stiffness, + 'damping': constraint.damping, + 'rest_value': constraint.rest_value, + 'active': constraint.active + } + + def start_updating(self) -> None: + """ + 开始更新物理动画集成系统 + """ + if not self.is_updating: + self.is_updating = True + print("物理动画集成系统更新已启动") + + def stop_updating(self) -> None: + """ + 停止更新物理动画集成系统 + """ + if self.is_updating: + self.is_updating = False print("物理动画集成系统更新已停止") \ No newline at end of file diff --git a/plugins/user/morphing_animation/config.json b/plugins/user/morphing_animation/config.json index e3bfefb4..ef2f290d 100644 --- a/plugins/user/morphing_animation/config.json +++ b/plugins/user/morphing_animation/config.json @@ -1,53 +1,53 @@ -{ - "name": "morphing_animation", - "version": "1.0.0", - "author": "Morphing Animation Team", - "description": "完整的顶点变形、morphing目标、网格变形解决方案,支持模型动画加载、播放控制、混合、物理集成、压缩、网络同步等高级功能", - "dependencies": [ - "core" - ], - "enabled": true, - "autoload": false, - "modules": { - "core": { - "enabled": true, - "max_morph_targets": 100, - "default_interpolation": "linear", - "update_rate": 60, - "cache_size": 200 - }, - "advanced": { - "state_machine": true, - "curve_editor": true, - "physics_integration": true, - "state_machine_update_rate": 30, - "curve_editor_points": 100, - "physics_update_rate": 60 - }, - "optimization": { - "compression": true, - "lod": true, - "cache": true, - "compression_threshold": 0.001, - "cache_size": 500, - "lod_distance_thresholds": [10.0, 25.0, 50.0], - "lod_frame_rates": [1.0, 0.5, 0.25, 0.1] - }, - "network": { - "enabled": true, - "sync_interval": 0.1, - "interpolation_mode": "linear", - "enable_compression": true, - "max_buffer_size": 100 - } - }, - "supported_formats": [ - ".egg", - ".bam" - ], - "performance": { - "target_fps": 60, - "lod_threshold": 30, - "max_buffer_size": 100 - } +{ + "name": "morphing_animation", + "version": "1.0.0", + "author": "Morphing Animation Team", + "description": "完整的顶点变形、morphing目标、网格变形解决方案,支持模型动画加载、播放控制、混合、物理集成、压缩、网络同步等高级功能", + "dependencies": [ + "core" + ], + "enabled": true, + "autoload": false, + "modules": { + "core": { + "enabled": true, + "max_morph_targets": 100, + "default_interpolation": "linear", + "update_rate": 60, + "cache_size": 200 + }, + "advanced": { + "state_machine": true, + "curve_editor": true, + "physics_integration": true, + "state_machine_update_rate": 30, + "curve_editor_points": 100, + "physics_update_rate": 60 + }, + "optimization": { + "compression": true, + "lod": true, + "cache": true, + "compression_threshold": 0.001, + "cache_size": 500, + "lod_distance_thresholds": [10.0, 25.0, 50.0], + "lod_frame_rates": [1.0, 0.5, 0.25, 0.1] + }, + "network": { + "enabled": true, + "sync_interval": 0.1, + "interpolation_mode": "linear", + "enable_compression": true, + "max_buffer_size": 100 + } + }, + "supported_formats": [ + ".egg", + ".bam" + ], + "performance": { + "target_fps": 60, + "lod_threshold": 30, + "max_buffer_size": 100 + } } \ No newline at end of file diff --git a/plugins/user/morphing_animation/core.py b/plugins/user/morphing_animation/core.py index 8095bdf9..b752516a 100644 --- a/plugins/user/morphing_animation/core.py +++ b/plugins/user/morphing_animation/core.py @@ -1,992 +1,992 @@ -""" -Morphing和变形动画插件 - 核心模块 -提供顶点变形、morphing目标、网格变形等核心功能 -""" - -import math -import numpy as np -from typing import Dict, List, Tuple, Optional, Any, Callable -from dataclasses import dataclass, field -from enum import Enum - -from panda3d.core import * -from direct.actor.Actor import Actor -from direct.showbase.DirectObject import DirectObject - -# 定义常量 -MORPHING_SETTINGS = { - 'max_morph_targets': 100, - 'default_interpolation': 'linear', - 'update_rate': 60, - 'cache_size': 200 -} - -# 枚举定义 -class MorphingMode(Enum): - """Morphing模式枚举""" - VERTEX_BLEND = 0 # 顶点混合 - NORMAL_BLEND = 1 # 法线混合 - TANGENT_BLEND = 2 # 切线混合 - FULL_BLEND = 3 # 完全混合 - -class InterpolationMode(Enum): - """插值模式枚举""" - LINEAR = 0 # 线性插值 - EASE_IN = 1 # 缓入 - EASE_OUT = 2 # 缓出 - EASE_IN_OUT = 3 # 缓入缓出 - SPLINE = 4 # 样条插值 - -class DeformationType(Enum): - """变形类型枚举""" - MORPH_TARGET = 0 # Morph目标 - VERTEX_ANIMATION = 1 # 顶点动画 - SKELETAL_DEFORM = 2 # 骨骼变形 - PROCEDURAL = 3 # 程序化变形 - -@dataclass -class MorphTarget: - """Morph目标数据类""" - name: str - vertices: List[Tuple[float, float, float]] # 顶点位置偏移 - normals: Optional[List[Tuple[float, float, float]]] = None # 法线偏移 - tangents: Optional[List[Tuple[float, float, float]]] = None # 切线偏移 - weights: List[float] = field(default_factory=list) # 权重列表 - active: bool = True - -@dataclass -class MorphingAnimation: - """Morphing动画数据类""" - name: str - target_weights: Dict[str, float] # 目标权重 {target_name: weight} - duration: float # 持续时间 - interpolation: InterpolationMode = InterpolationMode.LINEAR - loop: bool = False - start_time: float = 0.0 - current_time: float = 0.0 - playing: bool = False - paused: bool = False - -@dataclass -class VertexDeformation: - """顶点变形数据类""" - name: str - vertex_indices: List[int] # 顶点索引 - target_positions: List[Tuple[float, float, float]] # 目标位置 - duration: float # 持续时间 - start_time: float = 0.0 - current_time: float = 0.0 - active: bool = True - interpolation: InterpolationMode = InterpolationMode.LINEAR - -@dataclass -class ProceduralDeformation: - """程序化变形数据类""" - name: str - deformation_function: Callable # 变形函数 - parameters: Dict[str, Any] # 参数 - update_rate: float = 1.0 # 更新频率 - last_update: float = 0.0 - active: bool = True - -class MorphingCache: - """Morphing缓存系统""" - - def __init__(self, max_size: int = 200): - self.max_size = max_size - self.cache: Dict[str, Any] = {} - self.access_order: List[str] = [] - - def get(self, key: str) -> Optional[Any]: - """获取缓存项""" - if key in self.cache: - # 更新访问顺序 - if key in self.access_order: - self.access_order.remove(key) - self.access_order.append(key) - return self.cache[key] - return None - - def put(self, key: str, value: Any) -> None: - """放入缓存项""" - # 如果缓存已满,移除最久未使用的项 - if len(self.cache) >= self.max_size: - if self.access_order: - oldest_key = self.access_order.pop(0) - del self.cache[oldest_key] - - self.cache[key] = value - self.access_order.append(key) - - def invalidate(self, key: str) -> None: - """使缓存项失效""" - if key in self.cache: - del self.cache[key] - if key in self.access_order: - self.access_order.remove(key) - - def clear(self) -> None: - """清空缓存""" - self.cache.clear() - self.access_order.clear() - -class MorphingCore(DirectObject): - """ - Morphing核心系统 - 提供完整的顶点变形、morphing目标、网格变形等功能 - """ - - def __init__(self, world): - """ - 初始化Morphing核心系统 - :param world: 世界对象 - """ - super().__init__() - self.world = world - self.morph_targets: Dict[str, Dict[str, MorphTarget]] = {} # {model_id: {target_name: MorphTarget}} - self.morphing_animations: Dict[str, Dict[str, MorphingAnimation]] = {} # {model_id: {anim_name: MorphingAnimation}} - self.vertex_deformations: Dict[str, List[VertexDeformation]] = {} # {model_id: [VertexDeformation]} - self.procedural_deformations: Dict[str, List[ProceduralDeformation]] = {} # {model_id: [ProceduralDeformation]} - self.model_data: Dict[str, Dict[str, Any]] = {} # {model_id: {原始数据}} - self.cache = MorphingCache(MORPHING_SETTINGS['cache_size']) - self.is_updating = False - self.update_task = None - self.update_rate = MORPHING_SETTINGS['update_rate'] - - # 性能统计 - self.stats = { - 'models_processed': 0, - 'morph_targets_created': 0, - 'animations_played': 0, - 'deformations_applied': 0, - 'cache_hits': 0, - 'cache_misses': 0 - } - - print("Morphing核心系统初始化完成") - - def register_model(self, model_node: NodePath, model_id: Optional[str] = None) -> str: - """ - 注册模型以支持morphing - :param model_node: 模型节点 - :param model_id: 模型ID,如果为None则自动生成 - :return: 模型ID - """ - if model_id is None: - model_id = f"model_{len(self.model_data)}" - - # 检查模型是否已注册 - if model_id in self.model_data: - print(f"警告: 模型 {model_id} 已注册,将被替换") - self.unregister_model(model_id) - - # 提取模型数据 - model_data = self._extract_model_data(model_node) - if not model_data: - print(f"错误: 无法提取模型 {model_id} 的数据") - return "" - - # 存储模型数据 - self.model_data[model_id] = model_data - self.morph_targets[model_id] = {} - self.morphing_animations[model_id] = {} - self.vertex_deformations[model_id] = [] - self.procedural_deformations[model_id] = [] - - self.stats['models_processed'] += 1 - print(f"模型已注册: {model_id}") - return model_id - - def unregister_model(self, model_id: str) -> bool: - """ - 注销模型 - :param model_id: 模型ID - :return: 是否成功注销 - """ - if model_id not in self.model_data: - print(f"错误: 模型 {model_id} 未注册") - return False - - # 清理相关数据 - if model_id in self.morph_targets: - del self.morph_targets[model_id] - if model_id in self.morphing_animations: - del self.morphing_animations[model_id] - if model_id in self.vertex_deformations: - del self.vertex_deformations[model_id] - if model_id in self.procedural_deformations: - del self.procedural_deformations[model_id] - if model_id in self.model_data: - del self.model_data[model_id] - - # 使相关缓存失效 - cache_keys_to_invalidate = [key for key in self.cache.cache.keys() if key.startswith(f"{model_id}_")] - for key in cache_keys_to_invalidate: - self.cache.invalidate(key) - - print(f"模型已注销: {model_id}") - return True - - def _extract_model_data(self, model_node: NodePath) -> Optional[Dict[str, Any]]: - """ - 提取模型数据 - :param model_node: 模型节点 - :return: 模型数据字典 - """ - try: - # 查找GeomNode - geom_nodes = model_node.findAllMatches("**/+GeomNode") - if not geom_nodes: - return None - - model_data = { - 'node_path': model_node, - 'geom_nodes': [], - 'vertex_data': [], - 'original_vertices': [], - 'original_normals': [], - 'original_tangents': [] - } - - # 提取每个GeomNode的数据 - for geom_node in geom_nodes: - geom_node_obj = geom_node.node() - for i in range(geom_node_obj.getNumGeoms()): - geom = geom_node_obj.getGeom(i) - vertex_data = geom.getVertexData() - - model_data['geom_nodes'].append(geom_node) - model_data['vertex_data'].append(vertex_data) - - # 提取原始顶点数据 - vertex_reader = GeomVertexReader(vertex_data, 'vertex') - vertices = [] - while not vertex_reader.isAtEnd(): - vertex = vertex_reader.getData3f() - vertices.append((vertex.x, vertex.y, vertex.z)) - model_data['original_vertices'].append(vertices) - - # 提取原始法线数据(如果存在) - if vertex_data.hasColumn('normal'): - normal_reader = GeomVertexReader(vertex_data, 'normal') - normals = [] - while not normal_reader.isAtEnd(): - normal = normal_reader.getData3f() - normals.append((normal.x, normal.y, normal.z)) - model_data['original_normals'].append(normals) - - # 提取原始切线数据(如果存在) - if vertex_data.hasColumn('tangent'): - tangent_reader = GeomVertexReader(vertex_data, 'tangent') - tangents = [] - while not tangent_reader.isAtEnd(): - tangent = tangent_reader.getData3f() - tangents.append((tangent.x, tangent.y, tangent.z)) - model_data['original_tangents'].append(tangents) - - return model_data - - except Exception as e: - print(f"提取模型数据时出错: {e}") - import traceback - traceback.print_exc() - return None - - def create_morph_target(self, model_id: str, target_name: str, - vertex_offsets: List[Tuple[float, float, float]], - normal_offsets: Optional[List[Tuple[float, float, float]]] = None, - tangent_offsets: Optional[List[Tuple[float, float, float]]] = None) -> bool: - """ - 创建Morph目标 - :param model_id: 模型ID - :param target_name: 目标名称 - :param vertex_offsets: 顶点偏移列表 - :param normal_offsets: 法线偏移列表(可选) - :param tangent_offsets: 切线偏移列表(可选) - :return: 是否成功创建 - """ - if model_id not in self.model_data: - print(f"错误: 模型 {model_id} 未注册") - return False - - # 检查模型是否有足够的顶点 - total_vertices = sum(len(vertices) for vertices in self.model_data[model_id]['original_vertices']) - if len(vertex_offsets) != total_vertices: - print(f"错误: 顶点偏移数量 ({len(vertex_offsets)}) 与模型顶点数量 ({total_vertices}) 不匹配") - return False - - # 创建Morph目标 - morph_target = MorphTarget( - name=target_name, - vertices=vertex_offsets, - normals=normal_offsets, - tangents=tangent_offsets, - weights=[0.0] * len(vertex_offsets) - ) - - # 存储Morph目标 - self.morph_targets[model_id][target_name] = morph_target - self.stats['morph_targets_created'] += 1 - - print(f"Morph目标已创建: {target_name} (模型: {model_id})") - return True - - def remove_morph_target(self, model_id: str, target_name: str) -> bool: - """ - 移除Morph目标 - :param model_id: 模型ID - :param target_name: 目标名称 - :return: 是否成功移除 - """ - if model_id not in self.morph_targets or target_name not in self.morph_targets[model_id]: - print(f"错误: Morph目标 {target_name} 不存在于模型 {model_id} 中") - return False - - del self.morph_targets[model_id][target_name] - print(f"Morph目标已移除: {target_name} (模型: {model_id})") - return True - - def set_morph_target_weight(self, model_id: str, target_name: str, weight: float) -> bool: - """ - 设置Morph目标权重 - :param model_id: 模型ID - :param target_name: 目标名称 - :param weight: 权重值 (0.0-1.0) - :return: 是否成功设置 - """ - if model_id not in self.morph_targets or target_name not in self.morph_targets[model_id]: - print(f"错误: Morph目标 {target_name} 不存在于模型 {model_id} 中") - return False - - # 限制权重范围 - weight = max(0.0, min(1.0, weight)) - - # 更新权重 - morph_target = self.morph_targets[model_id][target_name] - morph_target.weights = [weight] * len(morph_target.weights) - - # 应用变形 - self._apply_morph_target(model_id, target_name, weight) - - return True - - def _apply_morph_target(self, model_id: str, target_name: str, weight: float) -> None: - """ - 应用Morph目标 - :param model_id: 模型ID - :param target_name: 目标名称 - :param weight: 权重值 - """ - if model_id not in self.morph_targets or target_name not in self.morph_targets[model_id]: - return - - morph_target = self.morph_targets[model_id][target_name] - model_data = self.model_data[model_id] - - # 应用到每个GeomNode - vertex_offset_index = 0 - for i, (geom_node, vertex_data) in enumerate(zip(model_data['geom_nodes'], model_data['vertex_data'])): - # 创建可写的顶点数据 - writable_vertex_data = vertex_data.copy() - - # 更新顶点位置 - vertex_writer = GeomVertexWriter(writable_vertex_data, 'vertex') - original_vertices = model_data['original_vertices'][i] - - for j, original_vertex in enumerate(original_vertices): - if vertex_offset_index + j < len(morph_target.vertices): - offset = morph_target.vertices[vertex_offset_index + j] - new_vertex = ( - original_vertex[0] + offset[0] * weight, - original_vertex[1] + offset[1] * weight, - original_vertex[2] + offset[2] * weight - ) - vertex_writer.setRow(j) - vertex_writer.setData3f(new_vertex[0], new_vertex[1], new_vertex[2]) - - # 更新法线(如果存在) - if morph_target.normals and writable_vertex_data.hasColumn('normal'): - normal_writer = GeomVertexWriter(writable_vertex_data, 'normal') - original_normals = model_data['original_normals'][i] if i < len(model_data['original_normals']) else [] - - for j, original_normal in enumerate(original_normals): - if vertex_offset_index + j < len(morph_target.normals): - offset = morph_target.normals[vertex_offset_index + j] - new_normal = ( - original_normal[0] + offset[0] * weight, - original_normal[1] + offset[1] * weight, - original_normal[2] + offset[2] * weight - ) - normal_writer.setRow(j) - normal_writer.setData3f(new_normal[0], new_normal[1], new_normal[2]) - - # 更新切线(如果存在) - if morph_target.tangents and writable_vertex_data.hasColumn('tangent'): - tangent_writer = GeomVertexWriter(writable_vertex_data, 'tangent') - original_tangents = model_data['original_tangents'][i] if i < len(model_data['original_tangents']) else [] - - for j, original_tangent in enumerate(original_tangents): - if vertex_offset_index + j < len(morph_target.tangents): - offset = morph_target.tangents[vertex_offset_index + j] - new_tangent = ( - original_tangent[0] + offset[0] * weight, - original_tangent[1] + offset[1] * weight, - original_tangent[2] + offset[2] * weight - ) - tangent_writer.setRow(j) - tangent_writer.setData3f(new_tangent[0], new_tangent[1], new_tangent[2]) - - # 更新GeomNode的顶点数据 - geom_node.node().modifyGeom(0).setVertexData(writable_vertex_data) - - # 更新顶点偏移索引 - vertex_offset_index += len(original_vertices) - - self.stats['deformations_applied'] += 1 - - def create_morphing_animation(self, model_id: str, anim_name: str, - target_weights: Dict[str, float], - duration: float, - interpolation: InterpolationMode = InterpolationMode.LINEAR, - loop: bool = False) -> bool: - """ - 创建Morphing动画 - :param model_id: 模型ID - :param anim_name: 动画名称 - :param target_weights: 目标权重字典 {target_name: weight} - :param duration: 持续时间 - :param interpolation: 插值模式 - :param loop: 是否循环 - :return: 是否成功创建 - """ - if model_id not in self.model_data: - print(f"错误: 模型 {model_id} 未注册") - return False - - # 验证Morph目标是否存在 - for target_name in target_weights.keys(): - if target_name not in self.morph_targets[model_id]: - print(f"错误: Morph目标 {target_name} 不存在于模型 {model_id} 中") - return False - - # 创建动画 - animation = MorphingAnimation( - name=anim_name, - target_weights=target_weights, - duration=duration, - interpolation=interpolation, - loop=loop, - start_time=globalClock.getRealTime(), - current_time=0.0, - playing=False, - paused=False - ) - - # 存储动画 - self.morphing_animations[model_id][anim_name] = animation - print(f"Morphing动画已创建: {anim_name} (模型: {model_id})") - return True - - def play_morphing_animation(self, model_id: str, anim_name: str) -> bool: - """ - 播放Morphing动画 - :param model_id: 模型ID - :param anim_name: 动画名称 - :return: 是否成功播放 - """ - if (model_id not in self.morphing_animations or - anim_name not in self.morphing_animations[model_id]): - print(f"错误: Morphing动画 {anim_name} 不存在于模型 {model_id} 中") - return False - - animation = self.morphing_animations[model_id][anim_name] - animation.playing = True - animation.paused = False - animation.start_time = globalClock.getRealTime() - animation.current_time = 0.0 - - self.stats['animations_played'] += 1 - print(f"Morphing动画开始播放: {anim_name} (模型: {model_id})") - return True - - def stop_morphing_animation(self, model_id: str, anim_name: Optional[str] = None) -> bool: - """ - 停止Morphing动画 - :param model_id: 模型ID - :param anim_name: 动画名称,如果为None则停止所有动画 - :return: 是否成功停止 - """ - if model_id not in self.morphing_animations: - print(f"错误: 模型 {model_id} 未注册") - return False - - if anim_name: - # 停止单个动画 - if anim_name not in self.morphing_animations[model_id]: - print(f"错误: Morphing动画 {anim_name} 不存在于模型 {model_id} 中") - return False - - animation = self.morphing_animations[model_id][anim_name] - animation.playing = False - animation.paused = False - print(f"Morphing动画已停止: {anim_name} (模型: {model_id})") - else: - # 停止所有动画 - for animation in self.morphing_animations[model_id].values(): - animation.playing = False - animation.paused = False - print(f"所有Morphing动画已停止 (模型: {model_id})") - - return True - - def pause_morphing_animation(self, model_id: str, anim_name: str) -> bool: - """ - 暂停Morphing动画 - :param model_id: 模型ID - :param anim_name: 动画名称 - :return: 是否成功暂停 - """ - if (model_id not in self.morphing_animations or - anim_name not in self.morphing_animations[model_id]): - print(f"错误: Morphing动画 {anim_name} 不存在于模型 {model_id} 中") - return False - - animation = self.morphing_animations[model_id][anim_name] - if animation.playing and not animation.paused: - animation.paused = True - print(f"Morphing动画已暂停: {anim_name} (模型: {model_id})") - return True - - return False - - def resume_morphing_animation(self, model_id: str, anim_name: str) -> bool: - """ - 恢复Morphing动画 - :param model_id: 模型ID - :param anim_name: 动画名称 - :return: 是否成功恢复 - """ - if (model_id not in self.morphing_animations or - anim_name not in self.morphing_animations[model_id]): - print(f"错误: Morphing动画 {anim_name} 不存在于模型 {model_id} 中") - return False - - animation = self.morphing_animations[model_id][anim_name] - if animation.playing and animation.paused: - animation.paused = False - # 调整开始时间以补偿暂停时间 - animation.start_time = globalClock.getRealTime() - animation.current_time - print(f"Morphing动画已恢复: {anim_name} (模型: {model_id})") - return True - - return False - - def create_vertex_deformation(self, model_id: str, deformation_name: str, - vertex_indices: List[int], - target_positions: List[Tuple[float, float, float]], - duration: float, - interpolation: InterpolationMode = InterpolationMode.LINEAR) -> bool: - """ - 创建顶点变形 - :param model_id: 模型ID - :param deformation_name: 变形名称 - :param vertex_indices: 顶点索引列表 - :param target_positions: 目标位置列表 - :param duration: 持续时间 - :param interpolation: 插值模式 - :return: 是否成功创建 - """ - if model_id not in self.model_data: - print(f"错误: 模型 {model_id} 未注册") - return False - - if len(vertex_indices) != len(target_positions): - print(f"错误: 顶点索引数量 ({len(vertex_indices)}) 与目标位置数量 ({len(target_positions)}) 不匹配") - return False - - # 创建顶点变形 - deformation = VertexDeformation( - name=deformation_name, - vertex_indices=vertex_indices, - target_positions=target_positions, - duration=duration, - interpolation=interpolation, - start_time=globalClock.getRealTime(), - current_time=0.0, - active=True - ) - - # 存储变形 - self.vertex_deformations[model_id].append(deformation) - print(f"顶点变形已创建: {deformation_name} (模型: {model_id})") - return True - - def remove_vertex_deformation(self, model_id: str, deformation_name: str) -> bool: - """ - 移除顶点变形 - :param model_id: 模型ID - :param deformation_name: 变形名称 - :return: 是否成功移除 - """ - if model_id not in self.vertex_deformations: - print(f"错误: 模型 {model_id} 未注册") - return False - - # 查找并移除变形 - deformations = self.vertex_deformations[model_id] - for i, deformation in enumerate(deformations): - if deformation.name == deformation_name: - deformations.pop(i) - print(f"顶点变形已移除: {deformation_name} (模型: {model_id})") - return True - - print(f"错误: 顶点变形 {deformation_name} 不存在于模型 {model_id} 中") - return False - - def add_procedural_deformation(self, model_id: str, deformation_name: str, - deformation_function: Callable, - parameters: Dict[str, Any], - update_rate: float = 1.0) -> bool: - """ - 添加程序化变形 - :param model_id: 模型ID - :param deformation_name: 变形名称 - :param deformation_function: 变形函数 - :param parameters: 参数字典 - :param update_rate: 更新频率 - :return: 是否成功添加 - """ - if model_id not in self.model_data: - print(f"错误: 模型 {model_id} 未注册") - return False - - # 创建程序化变形 - deformation = ProceduralDeformation( - name=deformation_name, - deformation_function=deformation_function, - parameters=parameters, - update_rate=update_rate, - last_update=globalClock.getRealTime(), - active=True - ) - - # 存储变形 - self.procedural_deformations[model_id].append(deformation) - print(f"程序化变形已添加: {deformation_name} (模型: {model_id})") - return True - - def remove_procedural_deformation(self, model_id: str, deformation_name: str) -> bool: - """ - 移除程序化变形 - :param model_id: 模型ID - :param deformation_name: 变形名称 - :return: 是否成功移除 - """ - if model_id not in self.procedural_deformations: - print(f"错误: 模型 {model_id} 未注册") - return False - - # 查找并移除变形 - deformations = self.procedural_deformations[model_id] - for i, deformation in enumerate(deformations): - if deformation.name == deformation_name: - deformations.pop(i) - print(f"程序化变形已移除: {deformation_name} (模型: {model_id})") - return True - - print(f"错误: 程序化变形 {deformation_name} 不存在于模型 {model_id} 中") - return False - - def update(self, dt: float) -> None: - """ - 更新Morphing系统 - :param dt: 时间增量 - """ - if not self.is_updating: - return - - current_time = globalClock.getRealTime() - - # 更新每个模型 - for model_id in self.model_data.keys(): - # 更新Morphing动画 - self._update_morphing_animations(model_id, current_time, dt) - - # 更新顶点变形 - self._update_vertex_deformations(model_id, current_time, dt) - - # 更新程序化变形 - self._update_procedural_deformations(model_id, current_time, dt) - - def _update_morphing_animations(self, model_id: str, current_time: float, dt: float) -> None: - """ - 更新Morphing动画 - :param model_id: 模型ID - :param current_time: 当前时间 - :param dt: 时间增量 - """ - if model_id not in self.morphing_animations: - return - - # 更新每个动画 - animations_to_remove = [] - for anim_name, animation in self.morphing_animations[model_id].items(): - if not animation.playing or animation.paused: - continue - - # 更新动画时间 - animation.current_time = current_time - animation.start_time - - # 检查动画是否完成 - if animation.current_time >= animation.duration: - if animation.loop: - # 循环播放 - animation.start_time = current_time - animation.current_time = 0.0 - else: - # 停止动画 - animation.playing = False - continue - - # 计算插值因子 - t = animation.current_time / animation.duration - if animation.interpolation == InterpolationMode.EASE_IN: - t = t * t - elif animation.interpolation == InterpolationMode.EASE_OUT: - t = 1.0 - (1.0 - t) * (1.0 - t) - elif animation.interpolation == InterpolationMode.EASE_IN_OUT: - t = t * t * (3.0 - 2.0 * t) - elif animation.interpolation == InterpolationMode.SPLINE: - # 简化的样条插值 - t = t * t * (3.0 - 2.0 * t) - - # 应用每个目标的权重 - for target_name, target_weight in animation.target_weights.items(): - self.set_morph_target_weight(model_id, target_name, target_weight * t) - - def _update_vertex_deformations(self, model_id: str, current_time: float, dt: float) -> None: - """ - 更新顶点变形 - :param model_id: 模型ID - :param current_time: 当前时间 - :param dt: 时间增量 - """ - if model_id not in self.vertex_deformations: - return - - # 更新每个变形 - deformations = self.vertex_deformations[model_id] - active_deformations = [d for d in deformations if d.active] - - if not active_deformations: - return - - model_data = self.model_data[model_id] - - # 对每个GeomNode应用变形 - for i, (geom_node, vertex_data) in enumerate(zip(model_data['geom_nodes'], model_data['vertex_data'])): - # 创建可写的顶点数据 - writable_vertex_data = vertex_data.copy() - vertex_writer = GeomVertexWriter(writable_vertex_data, 'vertex') - original_vertices = model_data['original_vertices'][i] - - # 应用每个活动变形 - for deformation in active_deformations: - # 更新变形时间 - deformation.current_time = current_time - deformation.start_time - - # 检查变形是否完成 - if deformation.current_time >= deformation.duration: - deformation.active = False - continue - - # 计算插值因子 - t = deformation.current_time / deformation.duration - if deformation.interpolation == InterpolationMode.EASE_IN: - t = t * t - elif deformation.interpolation == InterpolationMode.EASE_OUT: - t = 1.0 - (1.0 - t) * (1.0 - t) - elif deformation.interpolation == InterpolationMode.EASE_IN_OUT: - t = t * t * (3.0 - 2.0 * t) - elif deformation.interpolation == InterpolationMode.SPLINE: - t = t * t * (3.0 - 2.0 * t) - - # 应用变形到指定顶点 - for j, vertex_index in enumerate(deformation.vertex_indices): - if vertex_index < len(original_vertices): - original_vertex = original_vertices[vertex_index] - target_position = deformation.target_positions[j] - - # 计算插值位置 - interpolated_position = ( - original_vertex[0] + (target_position[0] - original_vertex[0]) * t, - original_vertex[1] + (target_position[1] - original_vertex[1]) * t, - original_vertex[2] + (target_position[2] - original_vertex[2]) * t - ) - - # 更新顶点位置 - vertex_writer.setRow(vertex_index) - vertex_writer.setData3f( - interpolated_position[0], - interpolated_position[1], - interpolated_position[2] - ) - - # 更新GeomNode的顶点数据 - geom_node.node().modifyGeom(0).setVertexData(writable_vertex_data) - - # 移除已完成的变形 - self.vertex_deformations[model_id] = [d for d in deformations if d.active] - - def _update_procedural_deformations(self, model_id: str, current_time: float, dt: float) -> None: - """ - 更新程序化变形 - :param model_id: 模型ID - :param current_time: 当前时间 - :param dt: 时间增量 - """ - if model_id not in self.procedural_deformations: - return - - # 更新每个变形 - deformations = self.procedural_deformations[model_id] - active_deformations = [d for d in deformations if d.active] - - for deformation in active_deformations: - # 检查更新时间 - if current_time - deformation.last_update < 1.0 / deformation.update_rate: - continue - - deformation.last_update = current_time - - try: - # 调用变形函数 - deformation.deformation_function( - self.model_data[model_id]['node_path'], - **deformation.parameters - ) - except Exception as e: - print(f"执行程序化变形函数时出错: {e}") - - # 移除非活动变形 - self.procedural_deformations[model_id] = [d for d in deformations if d.active] - - def get_model_info(self, model_id: str) -> Optional[Dict[str, Any]]: - """ - 获取模型信息 - :param model_id: 模型ID - :return: 模型信息字典 - """ - if model_id not in self.model_data: - print(f"错误: 模型 {model_id} 未注册") - return None - - model_data = self.model_data[model_id] - - return { - 'model_id': model_id, - 'morph_targets': list(self.morph_targets[model_id].keys()), - 'morphing_animations': list(self.morphing_animations[model_id].keys()), - 'vertex_deformations': [d.name for d in self.vertex_deformations[model_id]], - 'procedural_deformations': [d.name for d in self.procedural_deformations[model_id]], - 'vertex_count': sum(len(vertices) for vertices in model_data['original_vertices']), - 'geom_node_count': len(model_data['geom_nodes']) - } - - def get_morph_target_info(self, model_id: str, target_name: str) -> Optional[Dict[str, Any]]: - """ - 获取Morph目标信息 - :param model_id: 模型ID - :param target_name: 目标名称 - :return: Morph目标信息字典 - """ - if (model_id not in self.morph_targets or - target_name not in self.morph_targets[model_id]): - print(f"错误: Morph目标 {target_name} 不存在于模型 {model_id} 中") - return None - - morph_target = self.morph_targets[model_id][target_name] - - return { - 'name': morph_target.name, - 'vertex_count': len(morph_target.vertices), - 'has_normals': morph_target.normals is not None, - 'has_tangents': morph_target.tangents is not None, - 'active': morph_target.active - } - - def get_animation_info(self, model_id: str, anim_name: str) -> Optional[Dict[str, Any]]: - """ - 获取动画信息 - :param model_id: 模型ID - :param anim_name: 动画名称 - :return: 动画信息字典 - """ - if (model_id not in self.morphing_animations or - anim_name not in self.morphing_animations[model_id]): - print(f"错误: Morphing动画 {anim_name} 不存在于模型 {model_id} 中") - return None - - animation = self.morphing_animations[model_id][anim_name] - - return { - 'name': animation.name, - 'target_weights': animation.target_weights, - 'duration': animation.duration, - 'interpolation': animation.interpolation.name, - 'loop': animation.loop, - 'current_time': animation.current_time, - 'playing': animation.playing, - 'paused': animation.paused - } - - def start_updating(self) -> None: - """ - 开始更新Morphing系统 - """ - if not self.is_updating: - self.is_updating = True - print("Morphing系统更新已启动") - - def stop_updating(self) -> None: - """ - 停止更新Morphing系统 - """ - if self.is_updating: - self.is_updating = False - print("Morphing系统更新已停止") - - def get_system_stats(self) -> Dict[str, Any]: - """ - 获取系统统计信息 - :return: 统计信息字典 - """ - cache_stats = { - 'size': len(self.cache.cache), - 'max_size': self.cache.max_size - } - - return { - 'models_processed': self.stats['models_processed'], - 'morph_targets_created': self.stats['morph_targets_created'], - 'animations_played': self.stats['animations_played'], - 'deformations_applied': self.stats['deformations_applied'], - 'cache_stats': cache_stats, - 'registered_models': len(self.model_data), - 'total_morph_targets': sum(len(targets) for targets in self.morph_targets.values()), - 'total_animations': sum(len(anims) for anims in self.morphing_animations.values()) - } - - def reset_stats(self) -> None: - """ - 重置统计信息 - """ - self.stats = { - 'models_processed': 0, - 'morph_targets_created': 0, - 'animations_played': 0, - 'deformations_applied': 0, - 'cache_hits': 0, - 'cache_misses': 0 - } +""" +Morphing和变形动画插件 - 核心模块 +提供顶点变形、morphing目标、网格变形等核心功能 +""" + +import math +import numpy as np +from typing import Dict, List, Tuple, Optional, Any, Callable +from dataclasses import dataclass, field +from enum import Enum + +from panda3d.core import * +from direct.actor.Actor import Actor +from direct.showbase.DirectObject import DirectObject + +# 定义常量 +MORPHING_SETTINGS = { + 'max_morph_targets': 100, + 'default_interpolation': 'linear', + 'update_rate': 60, + 'cache_size': 200 +} + +# 枚举定义 +class MorphingMode(Enum): + """Morphing模式枚举""" + VERTEX_BLEND = 0 # 顶点混合 + NORMAL_BLEND = 1 # 法线混合 + TANGENT_BLEND = 2 # 切线混合 + FULL_BLEND = 3 # 完全混合 + +class InterpolationMode(Enum): + """插值模式枚举""" + LINEAR = 0 # 线性插值 + EASE_IN = 1 # 缓入 + EASE_OUT = 2 # 缓出 + EASE_IN_OUT = 3 # 缓入缓出 + SPLINE = 4 # 样条插值 + +class DeformationType(Enum): + """变形类型枚举""" + MORPH_TARGET = 0 # Morph目标 + VERTEX_ANIMATION = 1 # 顶点动画 + SKELETAL_DEFORM = 2 # 骨骼变形 + PROCEDURAL = 3 # 程序化变形 + +@dataclass +class MorphTarget: + """Morph目标数据类""" + name: str + vertices: List[Tuple[float, float, float]] # 顶点位置偏移 + normals: Optional[List[Tuple[float, float, float]]] = None # 法线偏移 + tangents: Optional[List[Tuple[float, float, float]]] = None # 切线偏移 + weights: List[float] = field(default_factory=list) # 权重列表 + active: bool = True + +@dataclass +class MorphingAnimation: + """Morphing动画数据类""" + name: str + target_weights: Dict[str, float] # 目标权重 {target_name: weight} + duration: float # 持续时间 + interpolation: InterpolationMode = InterpolationMode.LINEAR + loop: bool = False + start_time: float = 0.0 + current_time: float = 0.0 + playing: bool = False + paused: bool = False + +@dataclass +class VertexDeformation: + """顶点变形数据类""" + name: str + vertex_indices: List[int] # 顶点索引 + target_positions: List[Tuple[float, float, float]] # 目标位置 + duration: float # 持续时间 + start_time: float = 0.0 + current_time: float = 0.0 + active: bool = True + interpolation: InterpolationMode = InterpolationMode.LINEAR + +@dataclass +class ProceduralDeformation: + """程序化变形数据类""" + name: str + deformation_function: Callable # 变形函数 + parameters: Dict[str, Any] # 参数 + update_rate: float = 1.0 # 更新频率 + last_update: float = 0.0 + active: bool = True + +class MorphingCache: + """Morphing缓存系统""" + + def __init__(self, max_size: int = 200): + self.max_size = max_size + self.cache: Dict[str, Any] = {} + self.access_order: List[str] = [] + + def get(self, key: str) -> Optional[Any]: + """获取缓存项""" + if key in self.cache: + # 更新访问顺序 + if key in self.access_order: + self.access_order.remove(key) + self.access_order.append(key) + return self.cache[key] + return None + + def put(self, key: str, value: Any) -> None: + """放入缓存项""" + # 如果缓存已满,移除最久未使用的项 + if len(self.cache) >= self.max_size: + if self.access_order: + oldest_key = self.access_order.pop(0) + del self.cache[oldest_key] + + self.cache[key] = value + self.access_order.append(key) + + def invalidate(self, key: str) -> None: + """使缓存项失效""" + if key in self.cache: + del self.cache[key] + if key in self.access_order: + self.access_order.remove(key) + + def clear(self) -> None: + """清空缓存""" + self.cache.clear() + self.access_order.clear() + +class MorphingCore(DirectObject): + """ + Morphing核心系统 + 提供完整的顶点变形、morphing目标、网格变形等功能 + """ + + def __init__(self, world): + """ + 初始化Morphing核心系统 + :param world: 世界对象 + """ + super().__init__() + self.world = world + self.morph_targets: Dict[str, Dict[str, MorphTarget]] = {} # {model_id: {target_name: MorphTarget}} + self.morphing_animations: Dict[str, Dict[str, MorphingAnimation]] = {} # {model_id: {anim_name: MorphingAnimation}} + self.vertex_deformations: Dict[str, List[VertexDeformation]] = {} # {model_id: [VertexDeformation]} + self.procedural_deformations: Dict[str, List[ProceduralDeformation]] = {} # {model_id: [ProceduralDeformation]} + self.model_data: Dict[str, Dict[str, Any]] = {} # {model_id: {原始数据}} + self.cache = MorphingCache(MORPHING_SETTINGS['cache_size']) + self.is_updating = False + self.update_task = None + self.update_rate = MORPHING_SETTINGS['update_rate'] + + # 性能统计 + self.stats = { + 'models_processed': 0, + 'morph_targets_created': 0, + 'animations_played': 0, + 'deformations_applied': 0, + 'cache_hits': 0, + 'cache_misses': 0 + } + + print("Morphing核心系统初始化完成") + + def register_model(self, model_node: NodePath, model_id: Optional[str] = None) -> str: + """ + 注册模型以支持morphing + :param model_node: 模型节点 + :param model_id: 模型ID,如果为None则自动生成 + :return: 模型ID + """ + if model_id is None: + model_id = f"model_{len(self.model_data)}" + + # 检查模型是否已注册 + if model_id in self.model_data: + print(f"警告: 模型 {model_id} 已注册,将被替换") + self.unregister_model(model_id) + + # 提取模型数据 + model_data = self._extract_model_data(model_node) + if not model_data: + print(f"错误: 无法提取模型 {model_id} 的数据") + return "" + + # 存储模型数据 + self.model_data[model_id] = model_data + self.morph_targets[model_id] = {} + self.morphing_animations[model_id] = {} + self.vertex_deformations[model_id] = [] + self.procedural_deformations[model_id] = [] + + self.stats['models_processed'] += 1 + print(f"模型已注册: {model_id}") + return model_id + + def unregister_model(self, model_id: str) -> bool: + """ + 注销模型 + :param model_id: 模型ID + :return: 是否成功注销 + """ + if model_id not in self.model_data: + print(f"错误: 模型 {model_id} 未注册") + return False + + # 清理相关数据 + if model_id in self.morph_targets: + del self.morph_targets[model_id] + if model_id in self.morphing_animations: + del self.morphing_animations[model_id] + if model_id in self.vertex_deformations: + del self.vertex_deformations[model_id] + if model_id in self.procedural_deformations: + del self.procedural_deformations[model_id] + if model_id in self.model_data: + del self.model_data[model_id] + + # 使相关缓存失效 + cache_keys_to_invalidate = [key for key in self.cache.cache.keys() if key.startswith(f"{model_id}_")] + for key in cache_keys_to_invalidate: + self.cache.invalidate(key) + + print(f"模型已注销: {model_id}") + return True + + def _extract_model_data(self, model_node: NodePath) -> Optional[Dict[str, Any]]: + """ + 提取模型数据 + :param model_node: 模型节点 + :return: 模型数据字典 + """ + try: + # 查找GeomNode + geom_nodes = model_node.findAllMatches("**/+GeomNode") + if not geom_nodes: + return None + + model_data = { + 'node_path': model_node, + 'geom_nodes': [], + 'vertex_data': [], + 'original_vertices': [], + 'original_normals': [], + 'original_tangents': [] + } + + # 提取每个GeomNode的数据 + for geom_node in geom_nodes: + geom_node_obj = geom_node.node() + for i in range(geom_node_obj.getNumGeoms()): + geom = geom_node_obj.getGeom(i) + vertex_data = geom.getVertexData() + + model_data['geom_nodes'].append(geom_node) + model_data['vertex_data'].append(vertex_data) + + # 提取原始顶点数据 + vertex_reader = GeomVertexReader(vertex_data, 'vertex') + vertices = [] + while not vertex_reader.isAtEnd(): + vertex = vertex_reader.getData3f() + vertices.append((vertex.x, vertex.y, vertex.z)) + model_data['original_vertices'].append(vertices) + + # 提取原始法线数据(如果存在) + if vertex_data.hasColumn('normal'): + normal_reader = GeomVertexReader(vertex_data, 'normal') + normals = [] + while not normal_reader.isAtEnd(): + normal = normal_reader.getData3f() + normals.append((normal.x, normal.y, normal.z)) + model_data['original_normals'].append(normals) + + # 提取原始切线数据(如果存在) + if vertex_data.hasColumn('tangent'): + tangent_reader = GeomVertexReader(vertex_data, 'tangent') + tangents = [] + while not tangent_reader.isAtEnd(): + tangent = tangent_reader.getData3f() + tangents.append((tangent.x, tangent.y, tangent.z)) + model_data['original_tangents'].append(tangents) + + return model_data + + except Exception as e: + print(f"提取模型数据时出错: {e}") + import traceback + traceback.print_exc() + return None + + def create_morph_target(self, model_id: str, target_name: str, + vertex_offsets: List[Tuple[float, float, float]], + normal_offsets: Optional[List[Tuple[float, float, float]]] = None, + tangent_offsets: Optional[List[Tuple[float, float, float]]] = None) -> bool: + """ + 创建Morph目标 + :param model_id: 模型ID + :param target_name: 目标名称 + :param vertex_offsets: 顶点偏移列表 + :param normal_offsets: 法线偏移列表(可选) + :param tangent_offsets: 切线偏移列表(可选) + :return: 是否成功创建 + """ + if model_id not in self.model_data: + print(f"错误: 模型 {model_id} 未注册") + return False + + # 检查模型是否有足够的顶点 + total_vertices = sum(len(vertices) for vertices in self.model_data[model_id]['original_vertices']) + if len(vertex_offsets) != total_vertices: + print(f"错误: 顶点偏移数量 ({len(vertex_offsets)}) 与模型顶点数量 ({total_vertices}) 不匹配") + return False + + # 创建Morph目标 + morph_target = MorphTarget( + name=target_name, + vertices=vertex_offsets, + normals=normal_offsets, + tangents=tangent_offsets, + weights=[0.0] * len(vertex_offsets) + ) + + # 存储Morph目标 + self.morph_targets[model_id][target_name] = morph_target + self.stats['morph_targets_created'] += 1 + + print(f"Morph目标已创建: {target_name} (模型: {model_id})") + return True + + def remove_morph_target(self, model_id: str, target_name: str) -> bool: + """ + 移除Morph目标 + :param model_id: 模型ID + :param target_name: 目标名称 + :return: 是否成功移除 + """ + if model_id not in self.morph_targets or target_name not in self.morph_targets[model_id]: + print(f"错误: Morph目标 {target_name} 不存在于模型 {model_id} 中") + return False + + del self.morph_targets[model_id][target_name] + print(f"Morph目标已移除: {target_name} (模型: {model_id})") + return True + + def set_morph_target_weight(self, model_id: str, target_name: str, weight: float) -> bool: + """ + 设置Morph目标权重 + :param model_id: 模型ID + :param target_name: 目标名称 + :param weight: 权重值 (0.0-1.0) + :return: 是否成功设置 + """ + if model_id not in self.morph_targets or target_name not in self.morph_targets[model_id]: + print(f"错误: Morph目标 {target_name} 不存在于模型 {model_id} 中") + return False + + # 限制权重范围 + weight = max(0.0, min(1.0, weight)) + + # 更新权重 + morph_target = self.morph_targets[model_id][target_name] + morph_target.weights = [weight] * len(morph_target.weights) + + # 应用变形 + self._apply_morph_target(model_id, target_name, weight) + + return True + + def _apply_morph_target(self, model_id: str, target_name: str, weight: float) -> None: + """ + 应用Morph目标 + :param model_id: 模型ID + :param target_name: 目标名称 + :param weight: 权重值 + """ + if model_id not in self.morph_targets or target_name not in self.morph_targets[model_id]: + return + + morph_target = self.morph_targets[model_id][target_name] + model_data = self.model_data[model_id] + + # 应用到每个GeomNode + vertex_offset_index = 0 + for i, (geom_node, vertex_data) in enumerate(zip(model_data['geom_nodes'], model_data['vertex_data'])): + # 创建可写的顶点数据 + writable_vertex_data = vertex_data.copy() + + # 更新顶点位置 + vertex_writer = GeomVertexWriter(writable_vertex_data, 'vertex') + original_vertices = model_data['original_vertices'][i] + + for j, original_vertex in enumerate(original_vertices): + if vertex_offset_index + j < len(morph_target.vertices): + offset = morph_target.vertices[vertex_offset_index + j] + new_vertex = ( + original_vertex[0] + offset[0] * weight, + original_vertex[1] + offset[1] * weight, + original_vertex[2] + offset[2] * weight + ) + vertex_writer.setRow(j) + vertex_writer.setData3f(new_vertex[0], new_vertex[1], new_vertex[2]) + + # 更新法线(如果存在) + if morph_target.normals and writable_vertex_data.hasColumn('normal'): + normal_writer = GeomVertexWriter(writable_vertex_data, 'normal') + original_normals = model_data['original_normals'][i] if i < len(model_data['original_normals']) else [] + + for j, original_normal in enumerate(original_normals): + if vertex_offset_index + j < len(morph_target.normals): + offset = morph_target.normals[vertex_offset_index + j] + new_normal = ( + original_normal[0] + offset[0] * weight, + original_normal[1] + offset[1] * weight, + original_normal[2] + offset[2] * weight + ) + normal_writer.setRow(j) + normal_writer.setData3f(new_normal[0], new_normal[1], new_normal[2]) + + # 更新切线(如果存在) + if morph_target.tangents and writable_vertex_data.hasColumn('tangent'): + tangent_writer = GeomVertexWriter(writable_vertex_data, 'tangent') + original_tangents = model_data['original_tangents'][i] if i < len(model_data['original_tangents']) else [] + + for j, original_tangent in enumerate(original_tangents): + if vertex_offset_index + j < len(morph_target.tangents): + offset = morph_target.tangents[vertex_offset_index + j] + new_tangent = ( + original_tangent[0] + offset[0] * weight, + original_tangent[1] + offset[1] * weight, + original_tangent[2] + offset[2] * weight + ) + tangent_writer.setRow(j) + tangent_writer.setData3f(new_tangent[0], new_tangent[1], new_tangent[2]) + + # 更新GeomNode的顶点数据 + geom_node.node().modifyGeom(0).setVertexData(writable_vertex_data) + + # 更新顶点偏移索引 + vertex_offset_index += len(original_vertices) + + self.stats['deformations_applied'] += 1 + + def create_morphing_animation(self, model_id: str, anim_name: str, + target_weights: Dict[str, float], + duration: float, + interpolation: InterpolationMode = InterpolationMode.LINEAR, + loop: bool = False) -> bool: + """ + 创建Morphing动画 + :param model_id: 模型ID + :param anim_name: 动画名称 + :param target_weights: 目标权重字典 {target_name: weight} + :param duration: 持续时间 + :param interpolation: 插值模式 + :param loop: 是否循环 + :return: 是否成功创建 + """ + if model_id not in self.model_data: + print(f"错误: 模型 {model_id} 未注册") + return False + + # 验证Morph目标是否存在 + for target_name in target_weights.keys(): + if target_name not in self.morph_targets[model_id]: + print(f"错误: Morph目标 {target_name} 不存在于模型 {model_id} 中") + return False + + # 创建动画 + animation = MorphingAnimation( + name=anim_name, + target_weights=target_weights, + duration=duration, + interpolation=interpolation, + loop=loop, + start_time=globalClock.getRealTime(), + current_time=0.0, + playing=False, + paused=False + ) + + # 存储动画 + self.morphing_animations[model_id][anim_name] = animation + print(f"Morphing动画已创建: {anim_name} (模型: {model_id})") + return True + + def play_morphing_animation(self, model_id: str, anim_name: str) -> bool: + """ + 播放Morphing动画 + :param model_id: 模型ID + :param anim_name: 动画名称 + :return: 是否成功播放 + """ + if (model_id not in self.morphing_animations or + anim_name not in self.morphing_animations[model_id]): + print(f"错误: Morphing动画 {anim_name} 不存在于模型 {model_id} 中") + return False + + animation = self.morphing_animations[model_id][anim_name] + animation.playing = True + animation.paused = False + animation.start_time = globalClock.getRealTime() + animation.current_time = 0.0 + + self.stats['animations_played'] += 1 + print(f"Morphing动画开始播放: {anim_name} (模型: {model_id})") + return True + + def stop_morphing_animation(self, model_id: str, anim_name: Optional[str] = None) -> bool: + """ + 停止Morphing动画 + :param model_id: 模型ID + :param anim_name: 动画名称,如果为None则停止所有动画 + :return: 是否成功停止 + """ + if model_id not in self.morphing_animations: + print(f"错误: 模型 {model_id} 未注册") + return False + + if anim_name: + # 停止单个动画 + if anim_name not in self.morphing_animations[model_id]: + print(f"错误: Morphing动画 {anim_name} 不存在于模型 {model_id} 中") + return False + + animation = self.morphing_animations[model_id][anim_name] + animation.playing = False + animation.paused = False + print(f"Morphing动画已停止: {anim_name} (模型: {model_id})") + else: + # 停止所有动画 + for animation in self.morphing_animations[model_id].values(): + animation.playing = False + animation.paused = False + print(f"所有Morphing动画已停止 (模型: {model_id})") + + return True + + def pause_morphing_animation(self, model_id: str, anim_name: str) -> bool: + """ + 暂停Morphing动画 + :param model_id: 模型ID + :param anim_name: 动画名称 + :return: 是否成功暂停 + """ + if (model_id not in self.morphing_animations or + anim_name not in self.morphing_animations[model_id]): + print(f"错误: Morphing动画 {anim_name} 不存在于模型 {model_id} 中") + return False + + animation = self.morphing_animations[model_id][anim_name] + if animation.playing and not animation.paused: + animation.paused = True + print(f"Morphing动画已暂停: {anim_name} (模型: {model_id})") + return True + + return False + + def resume_morphing_animation(self, model_id: str, anim_name: str) -> bool: + """ + 恢复Morphing动画 + :param model_id: 模型ID + :param anim_name: 动画名称 + :return: 是否成功恢复 + """ + if (model_id not in self.morphing_animations or + anim_name not in self.morphing_animations[model_id]): + print(f"错误: Morphing动画 {anim_name} 不存在于模型 {model_id} 中") + return False + + animation = self.morphing_animations[model_id][anim_name] + if animation.playing and animation.paused: + animation.paused = False + # 调整开始时间以补偿暂停时间 + animation.start_time = globalClock.getRealTime() - animation.current_time + print(f"Morphing动画已恢复: {anim_name} (模型: {model_id})") + return True + + return False + + def create_vertex_deformation(self, model_id: str, deformation_name: str, + vertex_indices: List[int], + target_positions: List[Tuple[float, float, float]], + duration: float, + interpolation: InterpolationMode = InterpolationMode.LINEAR) -> bool: + """ + 创建顶点变形 + :param model_id: 模型ID + :param deformation_name: 变形名称 + :param vertex_indices: 顶点索引列表 + :param target_positions: 目标位置列表 + :param duration: 持续时间 + :param interpolation: 插值模式 + :return: 是否成功创建 + """ + if model_id not in self.model_data: + print(f"错误: 模型 {model_id} 未注册") + return False + + if len(vertex_indices) != len(target_positions): + print(f"错误: 顶点索引数量 ({len(vertex_indices)}) 与目标位置数量 ({len(target_positions)}) 不匹配") + return False + + # 创建顶点变形 + deformation = VertexDeformation( + name=deformation_name, + vertex_indices=vertex_indices, + target_positions=target_positions, + duration=duration, + interpolation=interpolation, + start_time=globalClock.getRealTime(), + current_time=0.0, + active=True + ) + + # 存储变形 + self.vertex_deformations[model_id].append(deformation) + print(f"顶点变形已创建: {deformation_name} (模型: {model_id})") + return True + + def remove_vertex_deformation(self, model_id: str, deformation_name: str) -> bool: + """ + 移除顶点变形 + :param model_id: 模型ID + :param deformation_name: 变形名称 + :return: 是否成功移除 + """ + if model_id not in self.vertex_deformations: + print(f"错误: 模型 {model_id} 未注册") + return False + + # 查找并移除变形 + deformations = self.vertex_deformations[model_id] + for i, deformation in enumerate(deformations): + if deformation.name == deformation_name: + deformations.pop(i) + print(f"顶点变形已移除: {deformation_name} (模型: {model_id})") + return True + + print(f"错误: 顶点变形 {deformation_name} 不存在于模型 {model_id} 中") + return False + + def add_procedural_deformation(self, model_id: str, deformation_name: str, + deformation_function: Callable, + parameters: Dict[str, Any], + update_rate: float = 1.0) -> bool: + """ + 添加程序化变形 + :param model_id: 模型ID + :param deformation_name: 变形名称 + :param deformation_function: 变形函数 + :param parameters: 参数字典 + :param update_rate: 更新频率 + :return: 是否成功添加 + """ + if model_id not in self.model_data: + print(f"错误: 模型 {model_id} 未注册") + return False + + # 创建程序化变形 + deformation = ProceduralDeformation( + name=deformation_name, + deformation_function=deformation_function, + parameters=parameters, + update_rate=update_rate, + last_update=globalClock.getRealTime(), + active=True + ) + + # 存储变形 + self.procedural_deformations[model_id].append(deformation) + print(f"程序化变形已添加: {deformation_name} (模型: {model_id})") + return True + + def remove_procedural_deformation(self, model_id: str, deformation_name: str) -> bool: + """ + 移除程序化变形 + :param model_id: 模型ID + :param deformation_name: 变形名称 + :return: 是否成功移除 + """ + if model_id not in self.procedural_deformations: + print(f"错误: 模型 {model_id} 未注册") + return False + + # 查找并移除变形 + deformations = self.procedural_deformations[model_id] + for i, deformation in enumerate(deformations): + if deformation.name == deformation_name: + deformations.pop(i) + print(f"程序化变形已移除: {deformation_name} (模型: {model_id})") + return True + + print(f"错误: 程序化变形 {deformation_name} 不存在于模型 {model_id} 中") + return False + + def update(self, dt: float) -> None: + """ + 更新Morphing系统 + :param dt: 时间增量 + """ + if not self.is_updating: + return + + current_time = globalClock.getRealTime() + + # 更新每个模型 + for model_id in self.model_data.keys(): + # 更新Morphing动画 + self._update_morphing_animations(model_id, current_time, dt) + + # 更新顶点变形 + self._update_vertex_deformations(model_id, current_time, dt) + + # 更新程序化变形 + self._update_procedural_deformations(model_id, current_time, dt) + + def _update_morphing_animations(self, model_id: str, current_time: float, dt: float) -> None: + """ + 更新Morphing动画 + :param model_id: 模型ID + :param current_time: 当前时间 + :param dt: 时间增量 + """ + if model_id not in self.morphing_animations: + return + + # 更新每个动画 + animations_to_remove = [] + for anim_name, animation in self.morphing_animations[model_id].items(): + if not animation.playing or animation.paused: + continue + + # 更新动画时间 + animation.current_time = current_time - animation.start_time + + # 检查动画是否完成 + if animation.current_time >= animation.duration: + if animation.loop: + # 循环播放 + animation.start_time = current_time + animation.current_time = 0.0 + else: + # 停止动画 + animation.playing = False + continue + + # 计算插值因子 + t = animation.current_time / animation.duration + if animation.interpolation == InterpolationMode.EASE_IN: + t = t * t + elif animation.interpolation == InterpolationMode.EASE_OUT: + t = 1.0 - (1.0 - t) * (1.0 - t) + elif animation.interpolation == InterpolationMode.EASE_IN_OUT: + t = t * t * (3.0 - 2.0 * t) + elif animation.interpolation == InterpolationMode.SPLINE: + # 简化的样条插值 + t = t * t * (3.0 - 2.0 * t) + + # 应用每个目标的权重 + for target_name, target_weight in animation.target_weights.items(): + self.set_morph_target_weight(model_id, target_name, target_weight * t) + + def _update_vertex_deformations(self, model_id: str, current_time: float, dt: float) -> None: + """ + 更新顶点变形 + :param model_id: 模型ID + :param current_time: 当前时间 + :param dt: 时间增量 + """ + if model_id not in self.vertex_deformations: + return + + # 更新每个变形 + deformations = self.vertex_deformations[model_id] + active_deformations = [d for d in deformations if d.active] + + if not active_deformations: + return + + model_data = self.model_data[model_id] + + # 对每个GeomNode应用变形 + for i, (geom_node, vertex_data) in enumerate(zip(model_data['geom_nodes'], model_data['vertex_data'])): + # 创建可写的顶点数据 + writable_vertex_data = vertex_data.copy() + vertex_writer = GeomVertexWriter(writable_vertex_data, 'vertex') + original_vertices = model_data['original_vertices'][i] + + # 应用每个活动变形 + for deformation in active_deformations: + # 更新变形时间 + deformation.current_time = current_time - deformation.start_time + + # 检查变形是否完成 + if deformation.current_time >= deformation.duration: + deformation.active = False + continue + + # 计算插值因子 + t = deformation.current_time / deformation.duration + if deformation.interpolation == InterpolationMode.EASE_IN: + t = t * t + elif deformation.interpolation == InterpolationMode.EASE_OUT: + t = 1.0 - (1.0 - t) * (1.0 - t) + elif deformation.interpolation == InterpolationMode.EASE_IN_OUT: + t = t * t * (3.0 - 2.0 * t) + elif deformation.interpolation == InterpolationMode.SPLINE: + t = t * t * (3.0 - 2.0 * t) + + # 应用变形到指定顶点 + for j, vertex_index in enumerate(deformation.vertex_indices): + if vertex_index < len(original_vertices): + original_vertex = original_vertices[vertex_index] + target_position = deformation.target_positions[j] + + # 计算插值位置 + interpolated_position = ( + original_vertex[0] + (target_position[0] - original_vertex[0]) * t, + original_vertex[1] + (target_position[1] - original_vertex[1]) * t, + original_vertex[2] + (target_position[2] - original_vertex[2]) * t + ) + + # 更新顶点位置 + vertex_writer.setRow(vertex_index) + vertex_writer.setData3f( + interpolated_position[0], + interpolated_position[1], + interpolated_position[2] + ) + + # 更新GeomNode的顶点数据 + geom_node.node().modifyGeom(0).setVertexData(writable_vertex_data) + + # 移除已完成的变形 + self.vertex_deformations[model_id] = [d for d in deformations if d.active] + + def _update_procedural_deformations(self, model_id: str, current_time: float, dt: float) -> None: + """ + 更新程序化变形 + :param model_id: 模型ID + :param current_time: 当前时间 + :param dt: 时间增量 + """ + if model_id not in self.procedural_deformations: + return + + # 更新每个变形 + deformations = self.procedural_deformations[model_id] + active_deformations = [d for d in deformations if d.active] + + for deformation in active_deformations: + # 检查更新时间 + if current_time - deformation.last_update < 1.0 / deformation.update_rate: + continue + + deformation.last_update = current_time + + try: + # 调用变形函数 + deformation.deformation_function( + self.model_data[model_id]['node_path'], + **deformation.parameters + ) + except Exception as e: + print(f"执行程序化变形函数时出错: {e}") + + # 移除非活动变形 + self.procedural_deformations[model_id] = [d for d in deformations if d.active] + + def get_model_info(self, model_id: str) -> Optional[Dict[str, Any]]: + """ + 获取模型信息 + :param model_id: 模型ID + :return: 模型信息字典 + """ + if model_id not in self.model_data: + print(f"错误: 模型 {model_id} 未注册") + return None + + model_data = self.model_data[model_id] + + return { + 'model_id': model_id, + 'morph_targets': list(self.morph_targets[model_id].keys()), + 'morphing_animations': list(self.morphing_animations[model_id].keys()), + 'vertex_deformations': [d.name for d in self.vertex_deformations[model_id]], + 'procedural_deformations': [d.name for d in self.procedural_deformations[model_id]], + 'vertex_count': sum(len(vertices) for vertices in model_data['original_vertices']), + 'geom_node_count': len(model_data['geom_nodes']) + } + + def get_morph_target_info(self, model_id: str, target_name: str) -> Optional[Dict[str, Any]]: + """ + 获取Morph目标信息 + :param model_id: 模型ID + :param target_name: 目标名称 + :return: Morph目标信息字典 + """ + if (model_id not in self.morph_targets or + target_name not in self.morph_targets[model_id]): + print(f"错误: Morph目标 {target_name} 不存在于模型 {model_id} 中") + return None + + morph_target = self.morph_targets[model_id][target_name] + + return { + 'name': morph_target.name, + 'vertex_count': len(morph_target.vertices), + 'has_normals': morph_target.normals is not None, + 'has_tangents': morph_target.tangents is not None, + 'active': morph_target.active + } + + def get_animation_info(self, model_id: str, anim_name: str) -> Optional[Dict[str, Any]]: + """ + 获取动画信息 + :param model_id: 模型ID + :param anim_name: 动画名称 + :return: 动画信息字典 + """ + if (model_id not in self.morphing_animations or + anim_name not in self.morphing_animations[model_id]): + print(f"错误: Morphing动画 {anim_name} 不存在于模型 {model_id} 中") + return None + + animation = self.morphing_animations[model_id][anim_name] + + return { + 'name': animation.name, + 'target_weights': animation.target_weights, + 'duration': animation.duration, + 'interpolation': animation.interpolation.name, + 'loop': animation.loop, + 'current_time': animation.current_time, + 'playing': animation.playing, + 'paused': animation.paused + } + + def start_updating(self) -> None: + """ + 开始更新Morphing系统 + """ + if not self.is_updating: + self.is_updating = True + print("Morphing系统更新已启动") + + def stop_updating(self) -> None: + """ + 停止更新Morphing系统 + """ + if self.is_updating: + self.is_updating = False + print("Morphing系统更新已停止") + + def get_system_stats(self) -> Dict[str, Any]: + """ + 获取系统统计信息 + :return: 统计信息字典 + """ + cache_stats = { + 'size': len(self.cache.cache), + 'max_size': self.cache.max_size + } + + return { + 'models_processed': self.stats['models_processed'], + 'morph_targets_created': self.stats['morph_targets_created'], + 'animations_played': self.stats['animations_played'], + 'deformations_applied': self.stats['deformations_applied'], + 'cache_stats': cache_stats, + 'registered_models': len(self.model_data), + 'total_morph_targets': sum(len(targets) for targets in self.morph_targets.values()), + 'total_animations': sum(len(anims) for anims in self.morphing_animations.values()) + } + + def reset_stats(self) -> None: + """ + 重置统计信息 + """ + self.stats = { + 'models_processed': 0, + 'morph_targets_created': 0, + 'animations_played': 0, + 'deformations_applied': 0, + 'cache_hits': 0, + 'cache_misses': 0 + } print("统计信息已重置") \ No newline at end of file diff --git a/plugins/user/morphing_animation/examples.py b/plugins/user/morphing_animation/examples.py index 72e5c7db..9f375824 100644 --- a/plugins/user/morphing_animation/examples.py +++ b/plugins/user/morphing_animation/examples.py @@ -1,283 +1,283 @@ -""" -Morphing和变形动画插件使用示例 -提供各种使用场景的详细示例 -""" - -from panda3d.core import * -from direct.showbase.ShowBase import ShowBase -from direct.task import Task - -# 导入插件模块 -from .core import MorphingCore, InterpolationMode -from .advanced import AnimationStateMachine, AnimationCurveEditor, PhysicsAnimationIntegration, AnimationStateType, TransitionType, CurveType -from .optimization import AnimationCompressor, AnimationLODSystem, AnimationCacheManager, CompressionMethod -from .network import AnimationNetworkSync, NetworkMode, SyncMode - -def example_basic_morphing(): - """ - 基础Morphing使用示例 - """ - print("=== 基础Morphing使用示例 ===") - - # 假设我们有一个world对象和模型 - # world = ShowBase() - # model = world.loader.loadModel("path/to/model") - # model.reparentTo(world.render) - - # 创建Morphing核心系统 - # morphing_core = MorphingCore(world) - - # 注册模型 - # model_id = morphing_core.register_model(model, "my_model") - - # 创建Morph目标(示例数据) - # vertex_offsets = [(0.1, 0.0, 0.0)] * 100 # 假设有100个顶点 - # morphing_core.create_morph_target(model_id, "smile", vertex_offsets) - - # 设置Morph目标权重 - # morphing_core.set_morph_target_weight(model_id, "smile", 0.5) - - print("基础Morphing示例:") - print(" 1. 创建MorphingCore实例") - print(" 2. 注册模型") - print(" 3. 创建Morph目标") - print(" 4. 设置Morph目标权重") - print("=== 基础Morphing使用示例结束 ===\n") - -def example_animation_state_machine(): - """ - 动画状态机使用示例 - """ - print("=== 动画状态机使用示例 ===") - - # 假设已有morphing_core和model_id - # state_machine = AnimationStateMachine(morphing_core, model_id) - - # 添加状态 - # state_machine.add_state("idle", {"neutral": 1.0}, duration=0.0, loop=True) - # state_machine.add_state("happy", {"smile": 1.0}, duration=2.0) - # state_machine.add_state("sad", {"frown": 1.0}, duration=2.0) - - # 设置状态转换 - # state_machine.set_transition("idle", "happy", TransitionType.SMOOTH, transition_time=0.5) - # state_machine.set_transition("happy", "sad", TransitionType.SMOOTH, transition_time=0.5) - # state_machine.set_transition("sad", "idle", TransitionType.SMOOTH, transition_time=0.5) - - # 进入初始状态 - # state_machine.enter_state("idle") - - # 启动更新 - # state_machine.start_updating() - - print("动画状态机示例:") - print(" 1. 创建AnimationStateMachine实例") - print(" 2. 添加动画状态") - print(" 3. 设置状态转换") - print(" 4. 进入初始状态") - print(" 5. 启动更新") - print("=== 动画状态机使用示例结束 ===\n") - -def example_curve_editor(): - """ - 曲线编辑器使用示例 - """ - print("=== 曲线编辑器使用示例 ===") - - # 假设已有morphing_core - # curve_editor = AnimationCurveEditor(morphing_core) - - # 创建曲线 - # control_points = [(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)] - # curve_editor.create_curve("bounce", CurveType.BEZIER, control_points) - - # 应用曲线到Morph目标 - # curve_editor.apply_curve_to_morph_target("bounce", model_id, "smile", 0.5) - - print("曲线编辑器示例:") - print(" 1. 创建AnimationCurveEditor实例") - print(" 2. 创建动画曲线") - print(" 3. 应用曲线到Morph目标") - print("=== 曲线编辑器使用示例结束 ===\n") - -def example_physics_integration(): - """ - 物理集成使用示例 - """ - print("=== 物理集成使用示例 ===") - - # 假设已有morphing_core - # physics_system = PhysicsAnimationIntegration(morphing_core) - - # 添加物理约束 - # physics_system.add_constraint(model_id, "spring_smile", "spring", "smile", stiffness=2.0, damping=0.5) - - # 启动更新 - # physics_system.start_updating() - - print("物理集成示例:") - print(" 1. 创建PhysicsAnimationIntegration实例") - print(" 2. 添加物理约束") - print(" 3. 启动更新") - print("=== 物理集成使用示例结束 ===\n") - -def example_compression(): - """ - 动画压缩使用示例 - """ - print("=== 动画压缩使用示例 ===") - - # 创建压缩器 - # compressor = AnimationCompressor() - - # 创建测试数据 - # morph_data = [(0.0, 0.0, 0.0), (0.1, 0.05, 0.02), (0.2, 0.1, 0.04)] - - # 压缩数据 - # compressed = compressor.compress_morph_data(morph_data, CompressionMethod.KEYFRAME_REDUCTION) - - # 解压缩数据 - # if compressed: - # decompressed = compressor.decompress_morph_data(compressed) - - print("动画压缩示例:") - print(" 1. 创建AnimationCompressor实例") - print(" 2. 准备Morph数据") - print(" 3. 压缩数据") - print(" 4. 解压缩数据") - print("=== 动画压缩使用示例结束 ===\n") - -def example_lod_system(): - """ - LOD系统使用示例 - """ - print("=== LOD系统使用示例 ===") - - # 假设已有world对象 - # lod_system = AnimationLODSystem(world) - - # 设置相机 - # lod_system.set_camera(world.camera) - - # 更新LOD级别(在渲染循环中) - # lod_system.update_lod_levels(models) - - print("LOD系统示例:") - print(" 1. 创建AnimationLODSystem实例") - print(" 2. 设置相机") - print(" 3. 更新LOD级别") - print("=== LOD系统使用示例结束 ===\n") - -def example_network_synchronization(): - """ - 网络同步使用示例 - """ - print("=== 网络同步使用示例 ===") - - # 假设已有world和morphing_core - # network_sync = AnimationNetworkSync(world, morphing_core) - - # 设置网络模式 - # network_sync.set_network_mode(NetworkMode.SERVER) - # network_sync.set_sync_mode(SyncMode.STATE_SYNC) - - # 注册模型进行同步 - # network_id = network_sync.register_model_for_sync(model_id) - - # 启动同步任务 - # network_sync.start_sync_task() - - print("网络同步示例:") - print(" 1. 创建AnimationNetworkSync实例") - print(" 2. 设置网络模式") - print(" 3. 注册模型进行同步") - print(" 4. 启动同步任务") - print("=== 网络同步使用示例结束 ===\n") - -def example_complete_workflow(): - """ - 完整工作流程示例 - """ - print("=== 完整工作流程示例 ===") - - print("完整Morphing动画工作流程:") - print(" 1. 初始化系统") - print(" - 创建MorphingCore") - print(" - 创建高级功能模块") - print(" - 创建优化模块") - print(" - 创建网络模块") - - print(" 2. 模型设置") - print(" - 加载模型") - print(" - 注册到MorphingCore") - print(" - 创建Morph目标") - - print(" 3. 动画创建") - print(" - 使用状态机管理复杂动画") - print(" - 使用曲线编辑器创建自定义曲线") - print(" - 集成物理效果") - - print(" 4. 性能优化") - print(" - 应用动画压缩") - print(" - 设置LOD系统") - print(" - 使用缓存管理") - - print(" 5. 网络同步") - print(" - 配置网络同步") - print(" - 注册模型同步") - print(" - 启动同步任务") - - print(" 6. 运行时更新") - print(" - 在主循环中更新各模块") - print(" - 处理用户输入") - print(" - 监控性能") - - print("=== 完整工作流程示例结束 ===\n") - -def example_plugin_usage(): - """ - 插件使用示例 - """ - print("=== 插件使用示例 ===") - - print("在主程序中使用Morphing动画插件:") - print(" # 启用插件系统") - print(" world.plugin_manager.enable_plugin_system()") - print("") - print(" # 启用Morphing动画插件") - print(" world.plugin_manager.enable_plugin('morphing_animation')") - print("") - print(" # 获取插件实例") - print(" plugin = world.plugin_manager.get_plugin('morphing_animation')") - print("") - print(" # 获取核心系统") - print(" core_system = plugin.get_core_system()") - print("") - print(" # 使用核心系统功能") - print(" model_id = core_system.register_model(model_node)") - print(" core_system.create_morph_target(model_id, 'target_name', vertex_offsets)") - print(" core_system.set_morph_target_weight(model_id, 'target_name', 0.5)") - - print("=== 插件使用示例结束 ===\n") - -# 主函数用于测试示例 -def main(): - """ - 主函数 - 运行所有示例 - """ - print("Morphing和变形动画插件使用示例") - print("=" * 50) - - example_basic_morphing() - example_animation_state_machine() - example_curve_editor() - example_physics_integration() - example_compression() - example_lod_system() - example_network_synchronization() - example_complete_workflow() - example_plugin_usage() - - print("所有示例运行完毕") - -if __name__ == "__main__": +""" +Morphing和变形动画插件使用示例 +提供各种使用场景的详细示例 +""" + +from panda3d.core import * +from direct.showbase.ShowBase import ShowBase +from direct.task import Task + +# 导入插件模块 +from .core import MorphingCore, InterpolationMode +from .advanced import AnimationStateMachine, AnimationCurveEditor, PhysicsAnimationIntegration, AnimationStateType, TransitionType, CurveType +from .optimization import AnimationCompressor, AnimationLODSystem, AnimationCacheManager, CompressionMethod +from .network import AnimationNetworkSync, NetworkMode, SyncMode + +def example_basic_morphing(): + """ + 基础Morphing使用示例 + """ + print("=== 基础Morphing使用示例 ===") + + # 假设我们有一个world对象和模型 + # world = ShowBase() + # model = world.loader.loadModel("path/to/model") + # model.reparentTo(world.render) + + # 创建Morphing核心系统 + # morphing_core = MorphingCore(world) + + # 注册模型 + # model_id = morphing_core.register_model(model, "my_model") + + # 创建Morph目标(示例数据) + # vertex_offsets = [(0.1, 0.0, 0.0)] * 100 # 假设有100个顶点 + # morphing_core.create_morph_target(model_id, "smile", vertex_offsets) + + # 设置Morph目标权重 + # morphing_core.set_morph_target_weight(model_id, "smile", 0.5) + + print("基础Morphing示例:") + print(" 1. 创建MorphingCore实例") + print(" 2. 注册模型") + print(" 3. 创建Morph目标") + print(" 4. 设置Morph目标权重") + print("=== 基础Morphing使用示例结束 ===\n") + +def example_animation_state_machine(): + """ + 动画状态机使用示例 + """ + print("=== 动画状态机使用示例 ===") + + # 假设已有morphing_core和model_id + # state_machine = AnimationStateMachine(morphing_core, model_id) + + # 添加状态 + # state_machine.add_state("idle", {"neutral": 1.0}, duration=0.0, loop=True) + # state_machine.add_state("happy", {"smile": 1.0}, duration=2.0) + # state_machine.add_state("sad", {"frown": 1.0}, duration=2.0) + + # 设置状态转换 + # state_machine.set_transition("idle", "happy", TransitionType.SMOOTH, transition_time=0.5) + # state_machine.set_transition("happy", "sad", TransitionType.SMOOTH, transition_time=0.5) + # state_machine.set_transition("sad", "idle", TransitionType.SMOOTH, transition_time=0.5) + + # 进入初始状态 + # state_machine.enter_state("idle") + + # 启动更新 + # state_machine.start_updating() + + print("动画状态机示例:") + print(" 1. 创建AnimationStateMachine实例") + print(" 2. 添加动画状态") + print(" 3. 设置状态转换") + print(" 4. 进入初始状态") + print(" 5. 启动更新") + print("=== 动画状态机使用示例结束 ===\n") + +def example_curve_editor(): + """ + 曲线编辑器使用示例 + """ + print("=== 曲线编辑器使用示例 ===") + + # 假设已有morphing_core + # curve_editor = AnimationCurveEditor(morphing_core) + + # 创建曲线 + # control_points = [(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)] + # curve_editor.create_curve("bounce", CurveType.BEZIER, control_points) + + # 应用曲线到Morph目标 + # curve_editor.apply_curve_to_morph_target("bounce", model_id, "smile", 0.5) + + print("曲线编辑器示例:") + print(" 1. 创建AnimationCurveEditor实例") + print(" 2. 创建动画曲线") + print(" 3. 应用曲线到Morph目标") + print("=== 曲线编辑器使用示例结束 ===\n") + +def example_physics_integration(): + """ + 物理集成使用示例 + """ + print("=== 物理集成使用示例 ===") + + # 假设已有morphing_core + # physics_system = PhysicsAnimationIntegration(morphing_core) + + # 添加物理约束 + # physics_system.add_constraint(model_id, "spring_smile", "spring", "smile", stiffness=2.0, damping=0.5) + + # 启动更新 + # physics_system.start_updating() + + print("物理集成示例:") + print(" 1. 创建PhysicsAnimationIntegration实例") + print(" 2. 添加物理约束") + print(" 3. 启动更新") + print("=== 物理集成使用示例结束 ===\n") + +def example_compression(): + """ + 动画压缩使用示例 + """ + print("=== 动画压缩使用示例 ===") + + # 创建压缩器 + # compressor = AnimationCompressor() + + # 创建测试数据 + # morph_data = [(0.0, 0.0, 0.0), (0.1, 0.05, 0.02), (0.2, 0.1, 0.04)] + + # 压缩数据 + # compressed = compressor.compress_morph_data(morph_data, CompressionMethod.KEYFRAME_REDUCTION) + + # 解压缩数据 + # if compressed: + # decompressed = compressor.decompress_morph_data(compressed) + + print("动画压缩示例:") + print(" 1. 创建AnimationCompressor实例") + print(" 2. 准备Morph数据") + print(" 3. 压缩数据") + print(" 4. 解压缩数据") + print("=== 动画压缩使用示例结束 ===\n") + +def example_lod_system(): + """ + LOD系统使用示例 + """ + print("=== LOD系统使用示例 ===") + + # 假设已有world对象 + # lod_system = AnimationLODSystem(world) + + # 设置相机 + # lod_system.set_camera(world.camera) + + # 更新LOD级别(在渲染循环中) + # lod_system.update_lod_levels(models) + + print("LOD系统示例:") + print(" 1. 创建AnimationLODSystem实例") + print(" 2. 设置相机") + print(" 3. 更新LOD级别") + print("=== LOD系统使用示例结束 ===\n") + +def example_network_synchronization(): + """ + 网络同步使用示例 + """ + print("=== 网络同步使用示例 ===") + + # 假设已有world和morphing_core + # network_sync = AnimationNetworkSync(world, morphing_core) + + # 设置网络模式 + # network_sync.set_network_mode(NetworkMode.SERVER) + # network_sync.set_sync_mode(SyncMode.STATE_SYNC) + + # 注册模型进行同步 + # network_id = network_sync.register_model_for_sync(model_id) + + # 启动同步任务 + # network_sync.start_sync_task() + + print("网络同步示例:") + print(" 1. 创建AnimationNetworkSync实例") + print(" 2. 设置网络模式") + print(" 3. 注册模型进行同步") + print(" 4. 启动同步任务") + print("=== 网络同步使用示例结束 ===\n") + +def example_complete_workflow(): + """ + 完整工作流程示例 + """ + print("=== 完整工作流程示例 ===") + + print("完整Morphing动画工作流程:") + print(" 1. 初始化系统") + print(" - 创建MorphingCore") + print(" - 创建高级功能模块") + print(" - 创建优化模块") + print(" - 创建网络模块") + + print(" 2. 模型设置") + print(" - 加载模型") + print(" - 注册到MorphingCore") + print(" - 创建Morph目标") + + print(" 3. 动画创建") + print(" - 使用状态机管理复杂动画") + print(" - 使用曲线编辑器创建自定义曲线") + print(" - 集成物理效果") + + print(" 4. 性能优化") + print(" - 应用动画压缩") + print(" - 设置LOD系统") + print(" - 使用缓存管理") + + print(" 5. 网络同步") + print(" - 配置网络同步") + print(" - 注册模型同步") + print(" - 启动同步任务") + + print(" 6. 运行时更新") + print(" - 在主循环中更新各模块") + print(" - 处理用户输入") + print(" - 监控性能") + + print("=== 完整工作流程示例结束 ===\n") + +def example_plugin_usage(): + """ + 插件使用示例 + """ + print("=== 插件使用示例 ===") + + print("在主程序中使用Morphing动画插件:") + print(" # 启用插件系统") + print(" world.plugin_manager.enable_plugin_system()") + print("") + print(" # 启用Morphing动画插件") + print(" world.plugin_manager.enable_plugin('morphing_animation')") + print("") + print(" # 获取插件实例") + print(" plugin = world.plugin_manager.get_plugin('morphing_animation')") + print("") + print(" # 获取核心系统") + print(" core_system = plugin.get_core_system()") + print("") + print(" # 使用核心系统功能") + print(" model_id = core_system.register_model(model_node)") + print(" core_system.create_morph_target(model_id, 'target_name', vertex_offsets)") + print(" core_system.set_morph_target_weight(model_id, 'target_name', 0.5)") + + print("=== 插件使用示例结束 ===\n") + +# 主函数用于测试示例 +def main(): + """ + 主函数 - 运行所有示例 + """ + print("Morphing和变形动画插件使用示例") + print("=" * 50) + + example_basic_morphing() + example_animation_state_machine() + example_curve_editor() + example_physics_integration() + example_compression() + example_lod_system() + example_network_synchronization() + example_complete_workflow() + example_plugin_usage() + + print("所有示例运行完毕") + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/plugins/user/morphing_animation/network.py b/plugins/user/morphing_animation/network.py index 2312e393..a96d8fe0 100644 --- a/plugins/user/morphing_animation/network.py +++ b/plugins/user/morphing_animation/network.py @@ -1,625 +1,625 @@ -""" -Morphing和变形动画插件 - 网络同步模块 -提供多人环境中动画状态的同步功能 -""" - -import json -import time -from typing import Dict, List, Tuple, Optional, Any, Callable -from dataclasses import dataclass, field -from enum import Enum - -from panda3d.core import * -from direct.distributed.ClockDelta import globalClockDelta -from direct.showbase.DirectObject import DirectObject - -# 导入核心模块 -from .core import MorphingCore - -# 定义常量 -NETWORK_SETTINGS = { - 'sync_interval': 0.1, - 'interpolation_buffer': 0.1, - 'max_buffer_size': 100, - 'enable_compression': True -} - -# 枚举定义 -class NetworkMode(Enum): - """网络模式枚举""" - DISABLED = 0 # 禁用 - SERVER = 1 # 服务器模式 - CLIENT = 2 # 客户端模式 - PEER_TO_PEER = 3 # 点对点模式 - -class SyncMode(Enum): - """同步模式枚举""" - STATE_SYNC = 0 # 状态同步 - INPUT_SYNC = 1 # 输入同步 - INTERPOLATION = 2 # 插值同步 - -class InterpolationMode(Enum): - """插值模式枚举""" - LINEAR = 0 # 线性插值 - SPLINE = 1 # 样条插值 - HERMITE = 2 # 埃尔米特插值 - -@dataclass -class NetworkState: - """网络状态数据类""" - model_id: str - morph_weights: Dict[str, float] # {target_name: weight} - timestamp: float - sequence_number: int - -@dataclass -class StateBufferEntry: - """状态缓冲区条目""" - state: NetworkState - received_time: float - -@dataclass -class NetworkConfig: - """网络配置数据类""" - mode: NetworkMode = NetworkMode.DISABLED - sync_mode: SyncMode = SyncMode.STATE_SYNC - sync_interval: float = NETWORK_SETTINGS['sync_interval'] - interpolation_mode: InterpolationMode = InterpolationMode.LINEAR - enable_compression: bool = NETWORK_SETTINGS['enable_compression'] - max_buffer_size: int = NETWORK_SETTINGS['max_buffer_size'] - -class AnimationNetworkSync(DirectObject): - """ - 动画网络同步系统 - 处理网络环境中的动画同步 - """ - - def __init__(self, world, morphing_core: MorphingCore): - """ - 初始化动画网络同步系统 - :param world: 世界对象 - :param morphing_core: Morphing核心系统 - """ - super().__init__() - self.world = world - self.morphing_core = morphing_core - self.config = NetworkConfig() - self.network_id_map: Dict[str, str] = {} # {local_model_id: network_id} - self.local_id_map: Dict[str, str] = {} # {network_id: local_model_id} - self.state_buffers: Dict[str, List[StateBufferEntry]] = {} # {network_id: [StateBufferEntry]} - self.sequence_numbers: Dict[str, int] = {} # {network_id: sequence_number} - self.is_syncing = False - self.sync_task = None - self.last_sync_time = 0.0 - self.network_interface = None # 网络接口(需要根据具体网络库实现) - - print("动画网络同步系统初始化完成") - - def set_network_mode(self, mode: NetworkMode) -> None: - """ - 设置网络模式 - :param mode: 网络模式 - """ - self.config.mode = mode - print(f"网络模式设置为: {mode.name}") - - def set_sync_mode(self, mode: SyncMode) -> None: - """ - 设置同步模式 - :param mode: 同步模式 - """ - self.config.sync_mode = mode - print(f"同步模式设置为: {mode.name}") - - def set_sync_interval(self, interval: float) -> None: - """ - 设置同步间隔 - :param interval: 同步间隔(秒) - """ - self.config.sync_interval = interval - print(f"同步间隔设置为: {interval}秒") - - def set_interpolation_mode(self, mode: InterpolationMode) -> None: - """ - 设置插值模式 - :param mode: 插值模式 - """ - self.config.interpolation_mode = mode - print(f"插值模式设置为: {mode.name}") - - def enable_compression(self, enabled: bool = True) -> None: - """ - 启用/禁用压缩 - :param enabled: 是否启用 - """ - self.config.enable_compression = enabled - print(f"压缩已{'启用' if enabled else '禁用'}") - - def register_model_for_sync(self, model_id: str, network_id: Optional[str] = None) -> str: - """ - 注册模型以进行网络同步 - :param model_id: 本地模型ID - :param network_id: 网络ID,如果为None则自动生成 - :return: 网络ID - """ - if self.config.mode == NetworkMode.DISABLED: - print("错误: 网络同步已禁用") - return "" - - if model_id not in self.morphing_core.model_data: - print(f"错误: 模型 {model_id} 未注册到Morphing核心系统") - return "" - - if network_id is None: - network_id = f"net_model_{len(self.network_id_map)}" - - # 检查ID是否已存在 - if network_id in self.local_id_map: - print(f"警告: 网络ID {network_id} 已存在,将被替换") - old_local_id = self.local_id_map[network_id] - if old_local_id in self.network_id_map: - del self.network_id_map[old_local_id] - - # 建立映射关系 - self.network_id_map[model_id] = network_id - self.local_id_map[network_id] = model_id - - # 初始化状态缓冲区 - self.state_buffers[network_id] = [] - self.sequence_numbers[network_id] = 0 - - print(f"模型已注册网络同步: {model_id} -> {network_id}") - return network_id - - def unregister_model_from_sync(self, model_id: str) -> bool: - """ - 从网络同步中注销模型 - :param model_id: 本地模型ID - :return: 是否成功注销 - """ - if model_id not in self.network_id_map: - print(f"错误: 模型 {model_id} 未注册网络同步") - return False - - network_id = self.network_id_map[model_id] - - # 清理相关数据 - del self.network_id_map[model_id] - if network_id in self.local_id_map: - del self.local_id_map[network_id] - if network_id in self.state_buffers: - del self.state_buffers[network_id] - if network_id in self.sequence_numbers: - del self.sequence_numbers[network_id] - - print(f"模型已注销网络同步: {model_id} -> {network_id}") - return True - - def send_model_state(self, model_id: str) -> bool: - """ - 发送模型状态 - :param model_id: 本地模型ID - :return: 是否成功发送 - """ - if self.config.mode not in [NetworkMode.SERVER, NetworkMode.PEER_TO_PEER]: - print("错误: 当前网络模式不支持发送状态") - return False - - if model_id not in self.network_id_map: - print(f"错误: 模型 {model_id} 未注册网络同步") - return False - - network_id = self.network_id_map[model_id] - - # 获取模型的当前状态 - morph_weights = {} - if model_id in self.morphing_core.morph_targets: - for target_name, morph_target in self.morphing_core.morph_targets[model_id].items(): - # 使用平均权重作为代表值 - if morph_target.weights: - morph_weights[target_name] = sum(morph_target.weights) / len(morph_target.weights) - - # 创建网络状态 - state = NetworkState( - model_id=network_id, - morph_weights=morph_weights, - timestamp=globalClockDelta.getRealNetworkTime(bits=32), - sequence_number=self.sequence_numbers.get(network_id, 0) - ) - - # 更新序列号 - self.sequence_numbers[network_id] = self.sequence_numbers.get(network_id, 0) + 1 - - # 压缩状态数据(如果启用) - state_data = self._serialize_state(state) - if self.config.enable_compression: - state_data = self._compress_state_data(state_data) - - # 发送状态数据(需要根据具体网络库实现) - if self.network_interface: - try: - self.network_interface.send_state(network_id, state_data) - print(f"模型状态已发送: {model_id} -> {network_id}") - return True - except Exception as e: - print(f"发送模型状态时出错: {e}") - return False - else: - # 模拟发送(用于测试) - print(f"模拟发送模型状态: {model_id} -> {network_id}") - print(f" 状态数据: {state_data}") - return True - - def receive_model_state(self, network_id: str, state_data: bytes) -> bool: - """ - 接收模型状态 - :param network_id: 网络ID - :param state_data: 状态数据 - :return: 是否成功接收 - """ - if self.config.mode not in [NetworkMode.CLIENT, NetworkMode.PEER_TO_PEER]: - print("错误: 当前网络模式不支持接收状态") - return False - - if network_id not in self.local_id_map: - print(f"警告: 未知的网络ID {network_id},忽略状态数据") - return False - - local_id = self.local_id_map[network_id] - - # 解压缩状态数据(如果启用) - if self.config.enable_compression: - state_data = self._decompress_state_data(state_data) - - # 反序列化状态 - state = self._deserialize_state(state_data) - if not state: - print(f"错误: 无法反序列化状态数据") - return False - - # 验证序列号 - last_sequence = self.sequence_numbers.get(network_id, -1) - if state.sequence_number <= last_sequence: - print(f"警告: 过期的状态数据 (序列号: {state.sequence_number} <= {last_sequence})") - return False - - # 更新序列号 - self.sequence_numbers[network_id] = state.sequence_number - - # 添加到状态缓冲区 - buffer_entry = StateBufferEntry( - state=state, - received_time=globalClock.getRealTime() - ) - - if network_id not in self.state_buffers: - self.state_buffers[network_id] = [] - - self.state_buffers[network_id].append(buffer_entry) - - # 限制缓冲区大小 - if len(self.state_buffers[network_id]) > self.config.max_buffer_size: - self.state_buffers[network_id].pop(0) - - # 应用状态(根据同步模式) - if self.config.sync_mode == SyncMode.STATE_SYNC: - self._apply_state_immediately(local_id, state) - elif self.config.sync_mode == SyncMode.INTERPOLATION: - # 插值模式下暂不立即应用,等待插值更新 - pass - - print(f"模型状态已接收: {network_id} -> {local_id}") - return True - - def _serialize_state(self, state: NetworkState) -> bytes: - """ - 序列化网络状态 - :param state: 网络状态 - :return: 序列化后的数据 - """ - state_dict = { - 'model_id': state.model_id, - 'morph_weights': state.morph_weights, - 'timestamp': state.timestamp, - 'sequence_number': state.sequence_number - } - - return json.dumps(state_dict).encode('utf-8') - - def _deserialize_state(self, state_data: bytes) -> Optional[NetworkState]: - """ - 反序列化网络状态 - :param state_data: 状态数据 - :return: 网络状态对象 - """ - try: - state_dict = json.loads(state_data.decode('utf-8')) - - return NetworkState( - model_id=state_dict['model_id'], - morph_weights=state_dict['morph_weights'], - timestamp=state_dict['timestamp'], - sequence_number=state_dict['sequence_number'] - ) - except Exception as e: - print(f"反序列化状态数据时出错: {e}") - return None - - def _compress_state_data(self, state_data: bytes) -> bytes: - """ - 压缩状态数据 - :param state_data: 状态数据 - :return: 压缩后的数据 - """ - # 简化实现,实际项目中可以使用zlib等压缩库 - return state_data - - def _decompress_state_data(self, state_data: bytes) -> bytes: - """ - 解压缩状态数据 - :param state_data: 压缩的状态数据 - :return: 解压后的数据 - """ - # 简化实现,实际项目中需要对应解压 - return state_data - - def _apply_state_immediately(self, model_id: str, state: NetworkState) -> None: - """ - 立即应用网络状态 - :param model_id: 本地模型ID - :param state: 网络状态 - """ - # 应用Morph目标权重 - for target_name, weight in state.morph_weights.items(): - self.morphing_core.set_morph_target_weight(model_id, target_name, weight) - - def _interpolate_states(self, network_id: str) -> None: - """ - 插值状态 - :param network_id: 网络ID - """ - if network_id not in self.state_buffers or len(self.state_buffers[network_id]) < 2: - return - - local_id = self.local_id_map[network_id] - buffer = self.state_buffers[network_id] - - # 获取当前时间 - current_time = globalClock.getRealTime() - interpolation_time = current_time - NETWORK_SETTINGS['interpolation_buffer'] - - # 找到要插值的两个状态 - prev_state = None - next_state = None - - for entry in buffer: - if entry.received_time <= interpolation_time: - prev_state = entry.state - else: - next_state = entry.state - break - - if not prev_state or not next_state: - return - - # 计算插值因子 - time_diff = next_state.timestamp - prev_state.timestamp - if time_diff <= 0: - return - - t = (interpolation_time - prev_state.timestamp) / time_diff - t = max(0.0, min(1.0, t)) # 限制在0-1范围内 - - # 根据插值模式调整因子 - if self.config.interpolation_mode == InterpolationMode.SPLINE: - t = t * t * (3.0 - 2.0 * t) - elif self.config.interpolation_mode == InterpolationMode.HERMITE: - t = t * t * t * (t * (t * 6.0 - 15.0) + 10.0) - - # 插值Morph目标权重 - all_targets = set(prev_state.morph_weights.keys()) | set(next_state.morph_weights.keys()) - - for target_name in all_targets: - prev_weight = prev_state.morph_weights.get(target_name, 0.0) - next_weight = next_state.morph_weights.get(target_name, 0.0) - - # 计算插值权重 - interpolated_weight = prev_weight * (1.0 - t) + next_weight * t - - # 应用权重 - self.morphing_core.set_morph_target_weight(local_id, target_name, interpolated_weight) - - def start_sync_task(self) -> None: - """ - 启动同步任务 - """ - if self.is_syncing: - return - - self.is_syncing = True - - # 启动同步任务 - self.sync_task = self.world.taskMgr.add( - self._sync_task, - "morphing_network_sync", - sort=40 - ) - - print("动画网络同步任务已启动") - - def stop_sync_task(self) -> None: - """ - 停止同步任务 - """ - if not self.is_syncing: - return - - self.is_syncing = False - - # 停止同步任务 - if self.sync_task: - self.world.taskMgr.remove(self.sync_task) - self.sync_task = None - - print("动画网络同步任务已停止") - - def _sync_task(self, task) -> int: - """ - 网络同步任务 - :param task: 任务对象 - :return: 任务状态 - """ - current_time = globalClock.getRealTime() - - # 检查同步间隔 - if current_time - self.last_sync_time < self.config.sync_interval: - return task.cont - - self.last_sync_time = current_time - - # 根据网络模式执行相应操作 - if self.config.mode == NetworkMode.SERVER: - # 服务器模式:发送所有注册模型的状态 - for model_id in self.network_id_map.keys(): - self.send_model_state(model_id) - - elif self.config.mode == NetworkMode.CLIENT: - # 客户端模式:接收状态并应用插值(如果启用) - if self.config.sync_mode == SyncMode.INTERPOLATION: - for network_id in self.state_buffers.keys(): - self._interpolate_states(network_id) - - elif self.config.mode == NetworkMode.PEER_TO_PEER: - # 点对点模式:发送和接收状态 - for model_id in self.network_id_map.keys(): - self.send_model_state(model_id) - - if self.config.sync_mode == SyncMode.INTERPOLATION: - for network_id in self.state_buffers.keys(): - self._interpolate_states(network_id) - - return task.cont - - def get_network_info(self) -> Dict[str, Any]: - """ - 获取网络同步信息 - :return: 网络信息字典 - """ - return { - 'mode': self.config.mode.name, - 'sync_mode': self.config.sync_mode.name, - 'sync_interval': self.config.sync_interval, - 'interpolation_mode': self.config.interpolation_mode.name, - 'compression_enabled': self.config.enable_compression, - 'registered_models': len(self.network_id_map), - 'network_ids': list(self.network_id_map.values()), - 'sync_status': 'active' if self.is_syncing else 'inactive' - } - - def get_model_network_info(self, model_id: str) -> Optional[Dict[str, Any]]: - """ - 获取模型网络信息 - :param model_id: 本地模型ID - :return: 模型网络信息字典 - """ - if model_id not in self.network_id_map: - print(f"错误: 模型 {model_id} 未注册网络同步") - return None - - network_id = self.network_id_map[model_id] - - return { - 'local_id': model_id, - 'network_id': network_id, - 'sequence_number': self.sequence_numbers.get(network_id, 0), - 'buffer_size': len(self.state_buffers.get(network_id, [])), - 'last_state': self.state_buffers.get(network_id, [])[-1].state if self.state_buffers.get(network_id) else None - } - - def set_network_interface(self, network_interface) -> None: - """ - 设置网络接口 - :param network_interface: 网络接口对象 - """ - self.network_interface = network_interface - print("网络接口已设置") - -# 网络接口基类(需要根据具体网络库实现) -class NetworkInterface: - """ - 网络接口基类 - 需要根据具体使用的网络库(如PyNetGames、Panda3D的PyClientRepository等)进行实现 - """ - - def __init__(self): - """初始化网络接口""" - pass - - def send_state(self, network_id: str, state_data: bytes) -> bool: - """ - 发送状态数据 - :param network_id: 网络ID - :param state_data: 状态数据 - :return: 是否成功发送 - """ - # 需要根据具体网络库实现 - raise NotImplementedError("需要在子类中实现send_state方法") - - def receive_state(self, network_id: str) -> Optional[bytes]: - """ - 接收状态数据 - :param network_id: 网络ID - :return: 状态数据 - """ - # 需要根据具体网络库实现 - raise NotImplementedError("需要在子类中实现receive_state方法") - - def connect(self, address: str, port: int) -> bool: - """ - 连接到服务器 - :param address: 服务器地址 - :param port: 服务器端口 - :return: 是否成功连接 - """ - # 需要根据具体网络库实现 - raise NotImplementedError("需要在子类中实现connect方法") - - def disconnect(self) -> None: - """ - 断开连接 - """ - # 需要根据具体网络库实现 - raise NotImplementedError("需要在子类中实现disconnect方法") - -# 使用示例和测试代码 -def example_network_usage(world, morphing_core): - """ - 动画网络同步系统使用示例 - """ - print("=== 动画网络同步系统使用示例 ===") - - # 创建网络同步系统 - network_sync = AnimationNetworkSync(world, morphing_core) - - # 设置网络模式 - network_sync.set_network_mode(NetworkMode.SERVER) - - # 设置同步模式 - network_sync.set_sync_mode(SyncMode.STATE_SYNC) - - # 设置同步间隔 - network_sync.set_sync_interval(0.05) # 50ms - - # 启用压缩 - network_sync.enable_compression(True) - - # 启动同步任务 - network_sync.start_sync_task() - - print("网络同步系统配置完成") - print("=== 动画网络同步系统示例结束 ===\n") - - return network_sync - -if __name__ == "__main__": +""" +Morphing和变形动画插件 - 网络同步模块 +提供多人环境中动画状态的同步功能 +""" + +import json +import time +from typing import Dict, List, Tuple, Optional, Any, Callable +from dataclasses import dataclass, field +from enum import Enum + +from panda3d.core import * +from direct.distributed.ClockDelta import globalClockDelta +from direct.showbase.DirectObject import DirectObject + +# 导入核心模块 +from .core import MorphingCore + +# 定义常量 +NETWORK_SETTINGS = { + 'sync_interval': 0.1, + 'interpolation_buffer': 0.1, + 'max_buffer_size': 100, + 'enable_compression': True +} + +# 枚举定义 +class NetworkMode(Enum): + """网络模式枚举""" + DISABLED = 0 # 禁用 + SERVER = 1 # 服务器模式 + CLIENT = 2 # 客户端模式 + PEER_TO_PEER = 3 # 点对点模式 + +class SyncMode(Enum): + """同步模式枚举""" + STATE_SYNC = 0 # 状态同步 + INPUT_SYNC = 1 # 输入同步 + INTERPOLATION = 2 # 插值同步 + +class InterpolationMode(Enum): + """插值模式枚举""" + LINEAR = 0 # 线性插值 + SPLINE = 1 # 样条插值 + HERMITE = 2 # 埃尔米特插值 + +@dataclass +class NetworkState: + """网络状态数据类""" + model_id: str + morph_weights: Dict[str, float] # {target_name: weight} + timestamp: float + sequence_number: int + +@dataclass +class StateBufferEntry: + """状态缓冲区条目""" + state: NetworkState + received_time: float + +@dataclass +class NetworkConfig: + """网络配置数据类""" + mode: NetworkMode = NetworkMode.DISABLED + sync_mode: SyncMode = SyncMode.STATE_SYNC + sync_interval: float = NETWORK_SETTINGS['sync_interval'] + interpolation_mode: InterpolationMode = InterpolationMode.LINEAR + enable_compression: bool = NETWORK_SETTINGS['enable_compression'] + max_buffer_size: int = NETWORK_SETTINGS['max_buffer_size'] + +class AnimationNetworkSync(DirectObject): + """ + 动画网络同步系统 + 处理网络环境中的动画同步 + """ + + def __init__(self, world, morphing_core: MorphingCore): + """ + 初始化动画网络同步系统 + :param world: 世界对象 + :param morphing_core: Morphing核心系统 + """ + super().__init__() + self.world = world + self.morphing_core = morphing_core + self.config = NetworkConfig() + self.network_id_map: Dict[str, str] = {} # {local_model_id: network_id} + self.local_id_map: Dict[str, str] = {} # {network_id: local_model_id} + self.state_buffers: Dict[str, List[StateBufferEntry]] = {} # {network_id: [StateBufferEntry]} + self.sequence_numbers: Dict[str, int] = {} # {network_id: sequence_number} + self.is_syncing = False + self.sync_task = None + self.last_sync_time = 0.0 + self.network_interface = None # 网络接口(需要根据具体网络库实现) + + print("动画网络同步系统初始化完成") + + def set_network_mode(self, mode: NetworkMode) -> None: + """ + 设置网络模式 + :param mode: 网络模式 + """ + self.config.mode = mode + print(f"网络模式设置为: {mode.name}") + + def set_sync_mode(self, mode: SyncMode) -> None: + """ + 设置同步模式 + :param mode: 同步模式 + """ + self.config.sync_mode = mode + print(f"同步模式设置为: {mode.name}") + + def set_sync_interval(self, interval: float) -> None: + """ + 设置同步间隔 + :param interval: 同步间隔(秒) + """ + self.config.sync_interval = interval + print(f"同步间隔设置为: {interval}秒") + + def set_interpolation_mode(self, mode: InterpolationMode) -> None: + """ + 设置插值模式 + :param mode: 插值模式 + """ + self.config.interpolation_mode = mode + print(f"插值模式设置为: {mode.name}") + + def enable_compression(self, enabled: bool = True) -> None: + """ + 启用/禁用压缩 + :param enabled: 是否启用 + """ + self.config.enable_compression = enabled + print(f"压缩已{'启用' if enabled else '禁用'}") + + def register_model_for_sync(self, model_id: str, network_id: Optional[str] = None) -> str: + """ + 注册模型以进行网络同步 + :param model_id: 本地模型ID + :param network_id: 网络ID,如果为None则自动生成 + :return: 网络ID + """ + if self.config.mode == NetworkMode.DISABLED: + print("错误: 网络同步已禁用") + return "" + + if model_id not in self.morphing_core.model_data: + print(f"错误: 模型 {model_id} 未注册到Morphing核心系统") + return "" + + if network_id is None: + network_id = f"net_model_{len(self.network_id_map)}" + + # 检查ID是否已存在 + if network_id in self.local_id_map: + print(f"警告: 网络ID {network_id} 已存在,将被替换") + old_local_id = self.local_id_map[network_id] + if old_local_id in self.network_id_map: + del self.network_id_map[old_local_id] + + # 建立映射关系 + self.network_id_map[model_id] = network_id + self.local_id_map[network_id] = model_id + + # 初始化状态缓冲区 + self.state_buffers[network_id] = [] + self.sequence_numbers[network_id] = 0 + + print(f"模型已注册网络同步: {model_id} -> {network_id}") + return network_id + + def unregister_model_from_sync(self, model_id: str) -> bool: + """ + 从网络同步中注销模型 + :param model_id: 本地模型ID + :return: 是否成功注销 + """ + if model_id not in self.network_id_map: + print(f"错误: 模型 {model_id} 未注册网络同步") + return False + + network_id = self.network_id_map[model_id] + + # 清理相关数据 + del self.network_id_map[model_id] + if network_id in self.local_id_map: + del self.local_id_map[network_id] + if network_id in self.state_buffers: + del self.state_buffers[network_id] + if network_id in self.sequence_numbers: + del self.sequence_numbers[network_id] + + print(f"模型已注销网络同步: {model_id} -> {network_id}") + return True + + def send_model_state(self, model_id: str) -> bool: + """ + 发送模型状态 + :param model_id: 本地模型ID + :return: 是否成功发送 + """ + if self.config.mode not in [NetworkMode.SERVER, NetworkMode.PEER_TO_PEER]: + print("错误: 当前网络模式不支持发送状态") + return False + + if model_id not in self.network_id_map: + print(f"错误: 模型 {model_id} 未注册网络同步") + return False + + network_id = self.network_id_map[model_id] + + # 获取模型的当前状态 + morph_weights = {} + if model_id in self.morphing_core.morph_targets: + for target_name, morph_target in self.morphing_core.morph_targets[model_id].items(): + # 使用平均权重作为代表值 + if morph_target.weights: + morph_weights[target_name] = sum(morph_target.weights) / len(morph_target.weights) + + # 创建网络状态 + state = NetworkState( + model_id=network_id, + morph_weights=morph_weights, + timestamp=globalClockDelta.getRealNetworkTime(bits=32), + sequence_number=self.sequence_numbers.get(network_id, 0) + ) + + # 更新序列号 + self.sequence_numbers[network_id] = self.sequence_numbers.get(network_id, 0) + 1 + + # 压缩状态数据(如果启用) + state_data = self._serialize_state(state) + if self.config.enable_compression: + state_data = self._compress_state_data(state_data) + + # 发送状态数据(需要根据具体网络库实现) + if self.network_interface: + try: + self.network_interface.send_state(network_id, state_data) + print(f"模型状态已发送: {model_id} -> {network_id}") + return True + except Exception as e: + print(f"发送模型状态时出错: {e}") + return False + else: + # 模拟发送(用于测试) + print(f"模拟发送模型状态: {model_id} -> {network_id}") + print(f" 状态数据: {state_data}") + return True + + def receive_model_state(self, network_id: str, state_data: bytes) -> bool: + """ + 接收模型状态 + :param network_id: 网络ID + :param state_data: 状态数据 + :return: 是否成功接收 + """ + if self.config.mode not in [NetworkMode.CLIENT, NetworkMode.PEER_TO_PEER]: + print("错误: 当前网络模式不支持接收状态") + return False + + if network_id not in self.local_id_map: + print(f"警告: 未知的网络ID {network_id},忽略状态数据") + return False + + local_id = self.local_id_map[network_id] + + # 解压缩状态数据(如果启用) + if self.config.enable_compression: + state_data = self._decompress_state_data(state_data) + + # 反序列化状态 + state = self._deserialize_state(state_data) + if not state: + print(f"错误: 无法反序列化状态数据") + return False + + # 验证序列号 + last_sequence = self.sequence_numbers.get(network_id, -1) + if state.sequence_number <= last_sequence: + print(f"警告: 过期的状态数据 (序列号: {state.sequence_number} <= {last_sequence})") + return False + + # 更新序列号 + self.sequence_numbers[network_id] = state.sequence_number + + # 添加到状态缓冲区 + buffer_entry = StateBufferEntry( + state=state, + received_time=globalClock.getRealTime() + ) + + if network_id not in self.state_buffers: + self.state_buffers[network_id] = [] + + self.state_buffers[network_id].append(buffer_entry) + + # 限制缓冲区大小 + if len(self.state_buffers[network_id]) > self.config.max_buffer_size: + self.state_buffers[network_id].pop(0) + + # 应用状态(根据同步模式) + if self.config.sync_mode == SyncMode.STATE_SYNC: + self._apply_state_immediately(local_id, state) + elif self.config.sync_mode == SyncMode.INTERPOLATION: + # 插值模式下暂不立即应用,等待插值更新 + pass + + print(f"模型状态已接收: {network_id} -> {local_id}") + return True + + def _serialize_state(self, state: NetworkState) -> bytes: + """ + 序列化网络状态 + :param state: 网络状态 + :return: 序列化后的数据 + """ + state_dict = { + 'model_id': state.model_id, + 'morph_weights': state.morph_weights, + 'timestamp': state.timestamp, + 'sequence_number': state.sequence_number + } + + return json.dumps(state_dict).encode('utf-8') + + def _deserialize_state(self, state_data: bytes) -> Optional[NetworkState]: + """ + 反序列化网络状态 + :param state_data: 状态数据 + :return: 网络状态对象 + """ + try: + state_dict = json.loads(state_data.decode('utf-8')) + + return NetworkState( + model_id=state_dict['model_id'], + morph_weights=state_dict['morph_weights'], + timestamp=state_dict['timestamp'], + sequence_number=state_dict['sequence_number'] + ) + except Exception as e: + print(f"反序列化状态数据时出错: {e}") + return None + + def _compress_state_data(self, state_data: bytes) -> bytes: + """ + 压缩状态数据 + :param state_data: 状态数据 + :return: 压缩后的数据 + """ + # 简化实现,实际项目中可以使用zlib等压缩库 + return state_data + + def _decompress_state_data(self, state_data: bytes) -> bytes: + """ + 解压缩状态数据 + :param state_data: 压缩的状态数据 + :return: 解压后的数据 + """ + # 简化实现,实际项目中需要对应解压 + return state_data + + def _apply_state_immediately(self, model_id: str, state: NetworkState) -> None: + """ + 立即应用网络状态 + :param model_id: 本地模型ID + :param state: 网络状态 + """ + # 应用Morph目标权重 + for target_name, weight in state.morph_weights.items(): + self.morphing_core.set_morph_target_weight(model_id, target_name, weight) + + def _interpolate_states(self, network_id: str) -> None: + """ + 插值状态 + :param network_id: 网络ID + """ + if network_id not in self.state_buffers or len(self.state_buffers[network_id]) < 2: + return + + local_id = self.local_id_map[network_id] + buffer = self.state_buffers[network_id] + + # 获取当前时间 + current_time = globalClock.getRealTime() + interpolation_time = current_time - NETWORK_SETTINGS['interpolation_buffer'] + + # 找到要插值的两个状态 + prev_state = None + next_state = None + + for entry in buffer: + if entry.received_time <= interpolation_time: + prev_state = entry.state + else: + next_state = entry.state + break + + if not prev_state or not next_state: + return + + # 计算插值因子 + time_diff = next_state.timestamp - prev_state.timestamp + if time_diff <= 0: + return + + t = (interpolation_time - prev_state.timestamp) / time_diff + t = max(0.0, min(1.0, t)) # 限制在0-1范围内 + + # 根据插值模式调整因子 + if self.config.interpolation_mode == InterpolationMode.SPLINE: + t = t * t * (3.0 - 2.0 * t) + elif self.config.interpolation_mode == InterpolationMode.HERMITE: + t = t * t * t * (t * (t * 6.0 - 15.0) + 10.0) + + # 插值Morph目标权重 + all_targets = set(prev_state.morph_weights.keys()) | set(next_state.morph_weights.keys()) + + for target_name in all_targets: + prev_weight = prev_state.morph_weights.get(target_name, 0.0) + next_weight = next_state.morph_weights.get(target_name, 0.0) + + # 计算插值权重 + interpolated_weight = prev_weight * (1.0 - t) + next_weight * t + + # 应用权重 + self.morphing_core.set_morph_target_weight(local_id, target_name, interpolated_weight) + + def start_sync_task(self) -> None: + """ + 启动同步任务 + """ + if self.is_syncing: + return + + self.is_syncing = True + + # 启动同步任务 + self.sync_task = self.world.taskMgr.add( + self._sync_task, + "morphing_network_sync", + sort=40 + ) + + print("动画网络同步任务已启动") + + def stop_sync_task(self) -> None: + """ + 停止同步任务 + """ + if not self.is_syncing: + return + + self.is_syncing = False + + # 停止同步任务 + if self.sync_task: + self.world.taskMgr.remove(self.sync_task) + self.sync_task = None + + print("动画网络同步任务已停止") + + def _sync_task(self, task) -> int: + """ + 网络同步任务 + :param task: 任务对象 + :return: 任务状态 + """ + current_time = globalClock.getRealTime() + + # 检查同步间隔 + if current_time - self.last_sync_time < self.config.sync_interval: + return task.cont + + self.last_sync_time = current_time + + # 根据网络模式执行相应操作 + if self.config.mode == NetworkMode.SERVER: + # 服务器模式:发送所有注册模型的状态 + for model_id in self.network_id_map.keys(): + self.send_model_state(model_id) + + elif self.config.mode == NetworkMode.CLIENT: + # 客户端模式:接收状态并应用插值(如果启用) + if self.config.sync_mode == SyncMode.INTERPOLATION: + for network_id in self.state_buffers.keys(): + self._interpolate_states(network_id) + + elif self.config.mode == NetworkMode.PEER_TO_PEER: + # 点对点模式:发送和接收状态 + for model_id in self.network_id_map.keys(): + self.send_model_state(model_id) + + if self.config.sync_mode == SyncMode.INTERPOLATION: + for network_id in self.state_buffers.keys(): + self._interpolate_states(network_id) + + return task.cont + + def get_network_info(self) -> Dict[str, Any]: + """ + 获取网络同步信息 + :return: 网络信息字典 + """ + return { + 'mode': self.config.mode.name, + 'sync_mode': self.config.sync_mode.name, + 'sync_interval': self.config.sync_interval, + 'interpolation_mode': self.config.interpolation_mode.name, + 'compression_enabled': self.config.enable_compression, + 'registered_models': len(self.network_id_map), + 'network_ids': list(self.network_id_map.values()), + 'sync_status': 'active' if self.is_syncing else 'inactive' + } + + def get_model_network_info(self, model_id: str) -> Optional[Dict[str, Any]]: + """ + 获取模型网络信息 + :param model_id: 本地模型ID + :return: 模型网络信息字典 + """ + if model_id not in self.network_id_map: + print(f"错误: 模型 {model_id} 未注册网络同步") + return None + + network_id = self.network_id_map[model_id] + + return { + 'local_id': model_id, + 'network_id': network_id, + 'sequence_number': self.sequence_numbers.get(network_id, 0), + 'buffer_size': len(self.state_buffers.get(network_id, [])), + 'last_state': self.state_buffers.get(network_id, [])[-1].state if self.state_buffers.get(network_id) else None + } + + def set_network_interface(self, network_interface) -> None: + """ + 设置网络接口 + :param network_interface: 网络接口对象 + """ + self.network_interface = network_interface + print("网络接口已设置") + +# 网络接口基类(需要根据具体网络库实现) +class NetworkInterface: + """ + 网络接口基类 + 需要根据具体使用的网络库(如PyNetGames、Panda3D的PyClientRepository等)进行实现 + """ + + def __init__(self): + """初始化网络接口""" + pass + + def send_state(self, network_id: str, state_data: bytes) -> bool: + """ + 发送状态数据 + :param network_id: 网络ID + :param state_data: 状态数据 + :return: 是否成功发送 + """ + # 需要根据具体网络库实现 + raise NotImplementedError("需要在子类中实现send_state方法") + + def receive_state(self, network_id: str) -> Optional[bytes]: + """ + 接收状态数据 + :param network_id: 网络ID + :return: 状态数据 + """ + # 需要根据具体网络库实现 + raise NotImplementedError("需要在子类中实现receive_state方法") + + def connect(self, address: str, port: int) -> bool: + """ + 连接到服务器 + :param address: 服务器地址 + :param port: 服务器端口 + :return: 是否成功连接 + """ + # 需要根据具体网络库实现 + raise NotImplementedError("需要在子类中实现connect方法") + + def disconnect(self) -> None: + """ + 断开连接 + """ + # 需要根据具体网络库实现 + raise NotImplementedError("需要在子类中实现disconnect方法") + +# 使用示例和测试代码 +def example_network_usage(world, morphing_core): + """ + 动画网络同步系统使用示例 + """ + print("=== 动画网络同步系统使用示例 ===") + + # 创建网络同步系统 + network_sync = AnimationNetworkSync(world, morphing_core) + + # 设置网络模式 + network_sync.set_network_mode(NetworkMode.SERVER) + + # 设置同步模式 + network_sync.set_sync_mode(SyncMode.STATE_SYNC) + + # 设置同步间隔 + network_sync.set_sync_interval(0.05) # 50ms + + # 启用压缩 + network_sync.enable_compression(True) + + # 启动同步任务 + network_sync.start_sync_task() + + print("网络同步系统配置完成") + print("=== 动画网络同步系统示例结束 ===\n") + + return network_sync + +if __name__ == "__main__": print("动画网络同步模块加载完成") \ No newline at end of file diff --git a/plugins/user/morphing_animation/optimization.py b/plugins/user/morphing_animation/optimization.py index d6db369f..d60b44aa 100644 --- a/plugins/user/morphing_animation/optimization.py +++ b/plugins/user/morphing_animation/optimization.py @@ -1,800 +1,800 @@ -""" -Morphing和变形动画插件 - 优化模块 -提供动画压缩、LOD系统、缓存管理等优化功能 -""" - -import math -import json -import numpy as np -from typing import Dict, List, Tuple, Optional, Any, Union -from dataclasses import dataclass, field -from enum import Enum - -from panda3d.core import * -from direct.actor.Actor import Actor - -# 定义常量 -OPTIMIZATION_SETTINGS = { - 'compression_threshold': 0.001, - 'lod_distance_thresholds': [10.0, 25.0, 50.0], - 'lod_frame_rates': [1.0, 0.5, 0.25, 0.1], - 'cache_size': 500, - 'max_buffer_size': 100 -} - -# 枚举定义 -class CompressionMethod(Enum): - """压缩方法枚举""" - KEYFRAME_REDUCTION = 0 # 关键帧简化 - QUANTIZATION = 1 # 量化压缩 - DELTA_COMPRESSION = 2 # 差分压缩 - RLE = 3 # 游程编码 - -class LODStrategy(Enum): - """LOD策略枚举""" - DISTANCE_BASED = 0 # 基于距离 - PERFORMANCE_BASED = 1 # 基于性能 - HYBRID = 2 # 混合策略 - -@dataclass -class CompressedMorphData: - """压缩的Morph数据类""" - method: CompressionMethod - original_size: int - compressed_size: int - compression_ratio: float - data: Any # 压缩后的数据 - -@dataclass -class LODLevel: - """LOD级别数据类""" - level: int - distance_threshold: float - frame_rate: float - quality_factor: float - active: bool = True - -@dataclass -class CacheEntry: - """缓存条目数据类""" - key: str - data: Any - size: int - access_time: float - access_count: int = 1 - -class AnimationCompressor: - """ - 动画压缩系统 - 支持多种压缩算法减少动画数据大小 - """ - - def __init__(self): - """初始化动画压缩系统""" - self.compression_threshold = OPTIMIZATION_SETTINGS['compression_threshold'] - self.stats = { - 'animations_compressed': 0, - 'data_saved': 0, - 'compression_time': 0.0 - } - - print("动画压缩系统初始化完成") - - def compress_morph_data(self, morph_data: List[Tuple[float, float, float]], - method: CompressionMethod = CompressionMethod.KEYFRAME_REDUCTION) -> Optional[CompressedMorphData]: - """ - 压缩Morph数据 - :param morph_data: Morph数据列表 - :param method: 压缩方法 - :return: 压缩后的数据 - """ - import time - start_time = time.time() - - original_size = len(morph_data) * 3 * 4 # 3个float,每个4字节 - - try: - if method == CompressionMethod.KEYFRAME_REDUCTION: - compressed_data = self._compress_keyframe_reduction(morph_data) - elif method == CompressionMethod.QUANTIZATION: - compressed_data = self._compress_quantization(morph_data) - elif method == CompressionMethod.DELTA_COMPRESSION: - compressed_data = self._compress_delta(morph_data) - elif method == CompressionMethod.RLE: - compressed_data = self._compress_rle(morph_data) - else: - compressed_data = morph_data - - compressed_size = len(str(compressed_data)) - compression_ratio = original_size / max(1, compressed_size) - - # 创建压缩数据对象 - result = CompressedMorphData( - method=method, - original_size=original_size, - compressed_size=compressed_size, - compression_ratio=compression_ratio, - data=compressed_data - ) - - # 更新统计信息 - self.stats['animations_compressed'] += 1 - self.stats['data_saved'] += original_size - compressed_size - self.stats['compression_time'] += time.time() - start_time - - print(f"Morph数据压缩完成: 原始大小={original_size}字节, 压缩后={compressed_size}字节, 压缩比={compression_ratio:.2f}") - return result - - except Exception as e: - print(f"压缩Morph数据时出错: {e}") - import traceback - traceback.print_exc() - return None - - def _compress_keyframe_reduction(self, morph_data: List[Tuple[float, float, float]]) -> List[Tuple[float, float, float]]: - """ - 关键帧简化压缩 - :param morph_data: Morph数据列表 - :return: 压缩后的数据 - """ - if len(morph_data) <= 2: - return morph_data - - compressed = [morph_data[0]] # 总是保留第一帧 - last_frame = morph_data[0] - - for i in range(1, len(morph_data) - 1): - current_frame = morph_data[i] - next_frame = morph_data[i + 1] - - # 检查当前帧是否可以被简化 - if not self._is_frame_necessary(last_frame, current_frame, next_frame): - continue - - compressed.append(current_frame) - last_frame = current_frame - - compressed.append(morph_data[-1]) # 总是保留最后一帧 - return compressed - - def _is_frame_necessary(self, prev_frame: Tuple[float, float, float], - current_frame: Tuple[float, float, float], - next_frame: Tuple[float, float, float]) -> bool: - """ - 判断帧是否必要 - :param prev_frame: 前一帧 - :param current_frame: 当前帧 - :param next_frame: 后一帧 - :return: 是否必要 - """ - # 计算线性插值 - t = 0.5 # 简化处理,实际应该基于时间计算 - interpolated = ( - prev_frame[0] + t * (next_frame[0] - prev_frame[0]), - prev_frame[1] + t * (next_frame[1] - prev_frame[1]), - prev_frame[2] + t * (next_frame[2] - prev_frame[2]) - ) - - # 计算与实际值的差异 - diff = math.sqrt( - (current_frame[0] - interpolated[0]) ** 2 + - (current_frame[1] - interpolated[1]) ** 2 + - (current_frame[2] - interpolated[2]) ** 2 - ) - - return diff > self.compression_threshold - - def _compress_quantization(self, morph_data: List[Tuple[float, float, float]]) -> List[Tuple[int, int, int]]: - """ - 量化压缩 - :param morph_data: Morph数据列表 - :return: 压缩后的数据 - """ - # 找到数据的范围 - min_vals = [float('inf')] * 3 - max_vals = [float('-inf')] * 3 - - for frame in morph_data: - for i in range(3): - min_vals[i] = min(min_vals[i], frame[i]) - max_vals[i] = max(max_vals[i], frame[i]) - - # 量化到16位整数 - quantized_data = [] - for frame in morph_data: - quantized_frame = [] - for i in range(3): - # 将浮点数映射到0-65535范围 - if max_vals[i] != min_vals[i]: - quantized_value = int((frame[i] - min_vals[i]) / (max_vals[i] - min_vals[i]) * 65535) - else: - quantized_value = 0 - quantized_frame.append(quantized_value) - quantized_data.append(tuple(quantized_frame)) - - return quantized_data - - def _compress_delta(self, morph_data: List[Tuple[float, float, float]]) -> List[Tuple[float, float, float]]: - """ - 差分压缩 - :param morph_data: Morph数据列表 - :return: 压缩后的数据 - """ - if not morph_data: - return morph_data - - delta_data = [morph_data[0]] # 第一帧保持原样 - - for i in range(1, len(morph_data)): - delta = ( - morph_data[i][0] - morph_data[i-1][0], - morph_data[i][1] - morph_data[i-1][1], - morph_data[i][2] - morph_data[i-1][2] - ) - delta_data.append(delta) - - return delta_data - - def _compress_rle(self, morph_data: List[Tuple[float, float, float]]) -> List[Tuple[int, Tuple[float, float, float]]]: - """ - 游程编码压缩 - :param morph_data: Morph数据列表 - :return: 压缩后的数据 - """ - if not morph_data: - return [] - - rle_data = [] - current_value = morph_data[0] - count = 1 - - for i in range(1, len(morph_data)): - if morph_data[i] == current_value: - count += 1 - else: - rle_data.append((count, current_value)) - current_value = morph_data[i] - count = 1 - - rle_data.append((count, current_value)) - return rle_data - - def decompress_morph_data(self, compressed_data: CompressedMorphData) -> Optional[List[Tuple[float, float, float]]]: - """ - 解压缩Morph数据 - :param compressed_data: 压缩的数据 - :return: 解压后的数据 - """ - try: - method = compressed_data.method - data = compressed_data.data - - if method == CompressionMethod.KEYFRAME_REDUCTION: - # 关键帧简化数据不需要特殊解压 - return data - elif method == CompressionMethod.QUANTIZATION: - return self._decompress_quantization(data) - elif method == CompressionMethod.DELTA_COMPRESSION: - return self._decompress_delta(data) - elif method == CompressionMethod.RLE: - return self._decompress_rle(data) - else: - return data - - except Exception as e: - print(f"解压缩Morph数据时出错: {e}") - import traceback - traceback.print_exc() - return None - - def _decompress_quantization(self, quantized_data: List[Tuple[int, int, int]]) -> List[Tuple[float, float, float]]: - """ - 解压量化数据 - :param quantized_data: 量化数据 - :return: 解压后的数据 - """ - # 这里需要存储原始的min_vals和max_vals来进行解压 - # 由于简化实现,我们返回原始数据 - return [(float(x), float(y), float(z)) for x, y, z in quantized_data] - - def _decompress_delta(self, delta_data: List[Tuple[float, float, float]]) -> List[Tuple[float, float, float]]: - """ - 解压差分数据 - :param delta_data: 差分数据 - :return: 解压后的数据 - """ - if not delta_data: - return [] - - original_data = [delta_data[0]] # 第一帧保持原样 - - for i in range(1, len(delta_data)): - original_frame = ( - original_data[i-1][0] + delta_data[i][0], - original_data[i-1][1] + delta_data[i][1], - original_data[i-1][2] + delta_data[i][2] - ) - original_data.append(original_frame) - - return original_data - - def _decompress_rle(self, rle_data: List[Tuple[int, Tuple[float, float, float]]]) -> List[Tuple[float, float, float]]: - """ - 解压RLE数据 - :param rle_data: RLE数据 - :return: 解压后的数据 - """ - original_data = [] - - for count, value in rle_data: - original_data.extend([value] * count) - - return original_data - - def get_compression_stats(self, compressed_data: Optional[CompressedMorphData] = None) -> Dict[str, Any]: - """ - 获取压缩统计信息 - :param compressed_data: 压缩数据(可选) - :return: 统计信息字典 - """ - stats = self.stats.copy() - - if compressed_data: - stats['current_compression_ratio'] = compressed_data.compression_ratio - stats['current_original_size'] = compressed_data.original_size - stats['current_compressed_size'] = compressed_data.compressed_size - - return stats - -class AnimationLODSystem: - """ - 动画LOD系统 - 根据距离和性能自动调整动画质量 - """ - - def __init__(self, world): - """ - 初始化动画LOD系统 - :param world: 世界对象 - """ - self.world = world - self.strategy = LODStrategy.DISTANCE_BASED - self.distance_thresholds = OPTIMIZATION_SETTINGS['lod_distance_thresholds'] - self.frame_rates = OPTIMIZATION_SETTINGS['lod_frame_rates'] - self.lod_levels: List[LODLevel] = [] - self.model_lod_levels: Dict[str, int] = {} # {model_id: lod_level} - self.camera_node = None - - # 创建LOD级别 - self._create_lod_levels() - - print("动画LOD系统初始化完成") - - def _create_lod_levels(self) -> None: - """ - 创建LOD级别 - """ - self.lod_levels = [] - - for i, (distance, frame_rate) in enumerate(zip(self.distance_thresholds, self.frame_rates)): - level = LODLevel( - level=i, - distance_threshold=distance, - frame_rate=frame_rate, - quality_factor=1.0 - (i / len(self.distance_thresholds)) - ) - self.lod_levels.append(level) - - # 添加最高LOD级别(最近距离,最高质量) - highest_level = LODLevel( - level=len(self.lod_levels), - distance_threshold=0.0, - frame_rate=1.0, - quality_factor=1.0 - ) - self.lod_levels.append(highest_level) - - def set_lod_strategy(self, strategy: LODStrategy) -> None: - """ - 设置LOD策略 - :param strategy: LOD策略 - """ - self.strategy = strategy - print(f"LOD策略设置为: {strategy.name}") - - def set_distance_thresholds(self, thresholds: List[float]) -> None: - """ - 设置距离阈值 - :param thresholds: 距离阈值列表 - """ - self.distance_thresholds = thresholds - self._create_lod_levels() - print(f"距离阈值已更新: {thresholds}") - - def set_frame_rates(self, frame_rates: List[float]) -> None: - """ - 设置帧率级别 - :param frame_rates: 帧率列表 - """ - self.frame_rates = frame_rates - self._create_lod_levels() - print(f"帧率级别已更新: {frame_rates}") - - def set_camera(self, camera_node: NodePath) -> None: - """ - 设置相机节点 - :param camera_node: 相机节点 - """ - self.camera_node = camera_node - - def update_lod_levels(self, models: Dict[str, Any]) -> None: - """ - 更新LOD级别 - :param models: 模型字典 {model_id: model_data} - """ - if not self.camera_node: - return - - camera_pos = self.camera_node.getPos() - - # 根据策略更新每个模型的LOD级别 - for model_id, model_data in models.items(): - if self.strategy == LODStrategy.DISTANCE_BASED: - self._update_distance_based_lod(model_id, model_data, camera_pos) - elif self.strategy == LODStrategy.PERFORMANCE_BASED: - self._update_performance_based_lod(model_id, model_data) - elif self.strategy == LODStrategy.HYBRID: - self._update_hybrid_lod(model_id, model_data, camera_pos) - - def _update_distance_based_lod(self, model_id: str, model_data: Any, camera_pos: Vec3) -> None: - """ - 更新基于距离的LOD - :param model_id: 模型ID - :param model_data: 模型数据 - :param camera_pos: 相机位置 - """ - try: - model_pos = model_data['node_path'].getPos() - distance = (camera_pos - model_pos).length() - - # 找到合适的LOD级别 - lod_level = len(self.lod_levels) - 1 # 默认最高质量 - for i, level in enumerate(self.lod_levels): - if distance <= level.distance_threshold: - lod_level = i - break - - self.model_lod_levels[model_id] = lod_level - - except Exception as e: - print(f"更新模型 {model_id} 的距离LOD时出错: {e}") - - def _update_performance_based_lod(self, model_id: str, model_data: Any) -> None: - """ - 更新基于性能的LOD - :param model_id: 模型ID - :param model_data: 模型数据 - """ - # 基于FPS或其他性能指标调整LOD - # 这里简化处理,实际项目中需要获取性能数据 - current_fps = 60.0 # 假设值 - - # 根据FPS调整LOD级别 - if current_fps < 30: - lod_level = min(2, len(self.lod_levels) - 1) - elif current_fps < 45: - lod_level = min(1, len(self.lod_levels) - 1) - else: - lod_level = 0 - - self.model_lod_levels[model_id] = lod_level - - def _update_hybrid_lod(self, model_id: str, model_data: Any, camera_pos: Vec3) -> None: - """ - 更新混合LOD - :param model_id: 模型ID - :param model_data: 模型数据 - :param camera_pos: 相机位置 - """ - # 结合距离和性能的混合策略 - self._update_distance_based_lod(model_id, model_data, camera_pos) - # 可以进一步根据性能调整 - - def get_model_lod_level(self, model_id: str) -> Optional[int]: - """ - 获取模型的LOD级别 - :param model_id: 模型ID - :return: LOD级别 - """ - return self.model_lod_levels.get(model_id) - - def get_lod_info(self, model_id: str) -> Optional[Dict[str, Any]]: - """ - 获取模型的LOD信息 - :param model_id: 模型ID - :return: LOD信息字典 - """ - lod_level = self.model_lod_levels.get(model_id) - if lod_level is None or lod_level >= len(self.lod_levels): - return None - - level_info = self.lod_levels[lod_level] - - return { - 'model_id': model_id, - 'lod_level': lod_level, - 'distance_threshold': level_info.distance_threshold, - 'frame_rate': level_info.frame_rate, - 'quality_factor': level_info.quality_factor - } - - def apply_lod_to_model(self, model_id: str, morphing_core) -> None: - """ - 应用LOD到模型 - :param model_id: 模型ID - :param morphing_core: Morphing核心系统 - """ - lod_level = self.model_lod_levels.get(model_id) - if lod_level is None or lod_level >= len(self.lod_levels): - return - - level_info = self.lod_levels[lod_level] - - # 这里可以应用LOD调整,例如: - # 1. 降低更新频率 - # 2. 减少变形精度 - # 3. 简化morph目标 - # 4. 降低渲染质量 - - # 示例:根据质量因子调整morph目标权重 - if model_id in morphing_core.morph_targets: - for target_name, morph_target in morphing_core.morph_targets[model_id].items(): - # 简化处理,实际项目中需要更复杂的逻辑 - adjusted_weights = [w * level_info.quality_factor for w in morph_target.weights] - morph_target.weights = adjusted_weights - -class AnimationCacheManager: - """ - 动画缓存管理器 - 提供高效的动画数据缓存机制 - """ - - def __init__(self): - """初始化动画缓存管理器""" - self.max_size = OPTIMIZATION_SETTINGS['cache_size'] - self.cache: Dict[str, CacheEntry] = {} - self.access_order: List[str] = [] # 最近最少使用顺序 - self.stats = { - 'hits': 0, - 'misses': 0, - 'evictions': 0 - } - - print("动画缓存管理器初始化完成") - - def get(self, key: str) -> Optional[Any]: - """ - 获取缓存项 - :param key: 缓存键 - :return: 缓存数据 - """ - if key in self.cache: - # 缓存命中 - self.stats['hits'] += 1 - - # 更新访问时间 - entry = self.cache[key] - entry.access_time = globalClock.getRealTime() - entry.access_count += 1 - - # 更新访问顺序 - if key in self.access_order: - self.access_order.remove(key) - self.access_order.append(key) - - return entry.data - else: - # 缓存未命中 - self.stats['misses'] += 1 - return None - - def put(self, key: str, data: Any, size: int = 1) -> None: - """ - 放入缓存项 - :param key: 缓存键 - :param data: 缓存数据 - :param size: 数据大小 - """ - # 如果缓存已满,移除最久未使用的项 - while len(self.cache) >= self.max_size and self.cache: - # 找到最久未使用的项 - if self.access_order: - oldest_key = self.access_order.pop(0) - del self.cache[oldest_key] - self.stats['evictions'] += 1 - else: - # 如果访问顺序列表为空,移除任意项 - oldest_key = next(iter(self.cache)) - del self.cache[oldest_key] - self.stats['evictions'] += 1 - - # 创建缓存条目 - entry = CacheEntry( - key=key, - data=data, - size=size, - access_time=globalClock.getRealTime(), - access_count=1 - ) - - # 存储缓存条目 - self.cache[key] = entry - self.access_order.append(key) - - def invalidate(self, key: str) -> None: - """ - 使缓存项失效 - :param key: 缓存键 - """ - if key in self.cache: - del self.cache[key] - if key in self.access_order: - self.access_order.remove(key) - - def clear(self) -> None: - """ - 清空缓存 - """ - self.cache.clear() - self.access_order.clear() - - def get_stats(self) -> Dict[str, Any]: - """ - 获取缓存统计信息 - :return: 统计信息字典 - """ - total_requests = self.stats['hits'] + self.stats['misses'] - hit_rate = self.stats['hits'] / max(1, total_requests) - - return { - 'size': len(self.cache), - 'max_size': self.max_size, - 'hits': self.stats['hits'], - 'misses': self.stats['misses'], - 'evictions': self.stats['evictions'], - 'hit_rate': hit_rate, - 'total_requests': total_requests - } - - def get_cache_info(self, key: str) -> Optional[Dict[str, Any]]: - """ - 获取缓存项信息 - :param key: 缓存键 - :return: 缓存项信息字典 - """ - if key not in self.cache: - return None - - entry = self.cache[key] - - return { - 'key': entry.key, - 'size': entry.size, - 'access_time': entry.access_time, - 'access_count': entry.access_count - } - -# 使用示例和测试代码 -def example_compressor_usage(): - """ - 动画压缩系统使用示例 - """ - print("=== 动画压缩系统使用示例 ===") - - # 创建压缩器 - compressor = AnimationCompressor() - - # 创建测试数据 - test_data = [ - (0.0, 0.0, 0.0), - (0.1, 0.05, 0.02), - (0.2, 0.1, 0.04), - (0.3, 0.15, 0.06), - (0.4, 0.2, 0.08), - (0.5, 0.25, 0.1), - (0.6, 0.3, 0.12), - (0.7, 0.35, 0.14), - (0.8, 0.4, 0.16), - (0.9, 0.45, 0.18), - (1.0, 0.5, 0.2) - ] - - # 测试不同压缩方法 - methods = [ - CompressionMethod.KEYFRAME_REDUCTION, - CompressionMethod.QUANTIZATION, - CompressionMethod.DELTA_COMPRESSION, - CompressionMethod.RLE - ] - - for method in methods: - print(f"\n测试 {method.name} 压缩:") - compressed = compressor.compress_morph_data(test_data, method) - if compressed: - print(f" 原始大小: {compressed.original_size} 字节") - print(f" 压缩后大小: {compressed.compressed_size} 字节") - print(f" 压缩比: {compressed.compression_ratio:.2f}") - - # 测试解压缩 - decompressed = compressor.decompress_morph_data(compressed) - if decompressed: - print(f" 解压数据点数: {len(decompressed)}") - - # 获取统计信息 - stats = compressor.get_compression_stats() - print(f"\n压缩统计: {stats}") - - print("=== 动画压缩系统示例结束 ===\n") - -def example_lod_usage(world): - """ - 动画LOD系统使用示例 - """ - print("=== 动画LOD系统使用示例 ===") - - # 创建LOD系统 - lod_system = AnimationLODSystem(world) - - # 设置LOD策略 - lod_system.set_lod_strategy(LODStrategy.DISTANCE_BASED) - - # 设置距离阈值 - lod_system.set_distance_thresholds([5.0, 15.0, 30.0]) - - # 设置帧率 - lod_system.set_frame_rates([1.0, 0.7, 0.4, 0.1]) - - print("LOD系统配置完成") - print("=== 动画LOD系统示例结束 ===\n") - - return lod_system - -def example_cache_usage(): - """ - 动画缓存管理器使用示例 - """ - print("=== 动画缓存管理器使用示例 ===") - - # 创建缓存管理器 - cache_manager = AnimationCacheManager() - - # 添加缓存项 - cache_manager.put("test_key_1", "test_data_1", 10) - cache_manager.put("test_key_2", "test_data_2", 20) - cache_manager.put("test_key_3", "test_data_3", 30) - - # 获取缓存项 - data1 = cache_manager.get("test_key_1") - print(f"获取缓存项1: {data1}") - - data2 = cache_manager.get("test_key_2") - print(f"获取缓存项2: {data2}") - - # 测试缓存未命中 - data4 = cache_manager.get("test_key_4") - print(f"获取不存在的缓存项: {data4}") - - # 获取统计信息 - stats = cache_manager.get_stats() - print(f"缓存统计: {stats}") - - # 获取缓存项信息 - info = cache_manager.get_cache_info("test_key_1") - print(f"缓存项信息: {info}") - - print("=== 动画缓存管理器示例结束 ===\n") - -if __name__ == "__main__": - example_compressor_usage() - # example_lod_usage(None) # 需要world对象 +""" +Morphing和变形动画插件 - 优化模块 +提供动画压缩、LOD系统、缓存管理等优化功能 +""" + +import math +import json +import numpy as np +from typing import Dict, List, Tuple, Optional, Any, Union +from dataclasses import dataclass, field +from enum import Enum + +from panda3d.core import * +from direct.actor.Actor import Actor + +# 定义常量 +OPTIMIZATION_SETTINGS = { + 'compression_threshold': 0.001, + 'lod_distance_thresholds': [10.0, 25.0, 50.0], + 'lod_frame_rates': [1.0, 0.5, 0.25, 0.1], + 'cache_size': 500, + 'max_buffer_size': 100 +} + +# 枚举定义 +class CompressionMethod(Enum): + """压缩方法枚举""" + KEYFRAME_REDUCTION = 0 # 关键帧简化 + QUANTIZATION = 1 # 量化压缩 + DELTA_COMPRESSION = 2 # 差分压缩 + RLE = 3 # 游程编码 + +class LODStrategy(Enum): + """LOD策略枚举""" + DISTANCE_BASED = 0 # 基于距离 + PERFORMANCE_BASED = 1 # 基于性能 + HYBRID = 2 # 混合策略 + +@dataclass +class CompressedMorphData: + """压缩的Morph数据类""" + method: CompressionMethod + original_size: int + compressed_size: int + compression_ratio: float + data: Any # 压缩后的数据 + +@dataclass +class LODLevel: + """LOD级别数据类""" + level: int + distance_threshold: float + frame_rate: float + quality_factor: float + active: bool = True + +@dataclass +class CacheEntry: + """缓存条目数据类""" + key: str + data: Any + size: int + access_time: float + access_count: int = 1 + +class AnimationCompressor: + """ + 动画压缩系统 + 支持多种压缩算法减少动画数据大小 + """ + + def __init__(self): + """初始化动画压缩系统""" + self.compression_threshold = OPTIMIZATION_SETTINGS['compression_threshold'] + self.stats = { + 'animations_compressed': 0, + 'data_saved': 0, + 'compression_time': 0.0 + } + + print("动画压缩系统初始化完成") + + def compress_morph_data(self, morph_data: List[Tuple[float, float, float]], + method: CompressionMethod = CompressionMethod.KEYFRAME_REDUCTION) -> Optional[CompressedMorphData]: + """ + 压缩Morph数据 + :param morph_data: Morph数据列表 + :param method: 压缩方法 + :return: 压缩后的数据 + """ + import time + start_time = time.time() + + original_size = len(morph_data) * 3 * 4 # 3个float,每个4字节 + + try: + if method == CompressionMethod.KEYFRAME_REDUCTION: + compressed_data = self._compress_keyframe_reduction(morph_data) + elif method == CompressionMethod.QUANTIZATION: + compressed_data = self._compress_quantization(morph_data) + elif method == CompressionMethod.DELTA_COMPRESSION: + compressed_data = self._compress_delta(morph_data) + elif method == CompressionMethod.RLE: + compressed_data = self._compress_rle(morph_data) + else: + compressed_data = morph_data + + compressed_size = len(str(compressed_data)) + compression_ratio = original_size / max(1, compressed_size) + + # 创建压缩数据对象 + result = CompressedMorphData( + method=method, + original_size=original_size, + compressed_size=compressed_size, + compression_ratio=compression_ratio, + data=compressed_data + ) + + # 更新统计信息 + self.stats['animations_compressed'] += 1 + self.stats['data_saved'] += original_size - compressed_size + self.stats['compression_time'] += time.time() - start_time + + print(f"Morph数据压缩完成: 原始大小={original_size}字节, 压缩后={compressed_size}字节, 压缩比={compression_ratio:.2f}") + return result + + except Exception as e: + print(f"压缩Morph数据时出错: {e}") + import traceback + traceback.print_exc() + return None + + def _compress_keyframe_reduction(self, morph_data: List[Tuple[float, float, float]]) -> List[Tuple[float, float, float]]: + """ + 关键帧简化压缩 + :param morph_data: Morph数据列表 + :return: 压缩后的数据 + """ + if len(morph_data) <= 2: + return morph_data + + compressed = [morph_data[0]] # 总是保留第一帧 + last_frame = morph_data[0] + + for i in range(1, len(morph_data) - 1): + current_frame = morph_data[i] + next_frame = morph_data[i + 1] + + # 检查当前帧是否可以被简化 + if not self._is_frame_necessary(last_frame, current_frame, next_frame): + continue + + compressed.append(current_frame) + last_frame = current_frame + + compressed.append(morph_data[-1]) # 总是保留最后一帧 + return compressed + + def _is_frame_necessary(self, prev_frame: Tuple[float, float, float], + current_frame: Tuple[float, float, float], + next_frame: Tuple[float, float, float]) -> bool: + """ + 判断帧是否必要 + :param prev_frame: 前一帧 + :param current_frame: 当前帧 + :param next_frame: 后一帧 + :return: 是否必要 + """ + # 计算线性插值 + t = 0.5 # 简化处理,实际应该基于时间计算 + interpolated = ( + prev_frame[0] + t * (next_frame[0] - prev_frame[0]), + prev_frame[1] + t * (next_frame[1] - prev_frame[1]), + prev_frame[2] + t * (next_frame[2] - prev_frame[2]) + ) + + # 计算与实际值的差异 + diff = math.sqrt( + (current_frame[0] - interpolated[0]) ** 2 + + (current_frame[1] - interpolated[1]) ** 2 + + (current_frame[2] - interpolated[2]) ** 2 + ) + + return diff > self.compression_threshold + + def _compress_quantization(self, morph_data: List[Tuple[float, float, float]]) -> List[Tuple[int, int, int]]: + """ + 量化压缩 + :param morph_data: Morph数据列表 + :return: 压缩后的数据 + """ + # 找到数据的范围 + min_vals = [float('inf')] * 3 + max_vals = [float('-inf')] * 3 + + for frame in morph_data: + for i in range(3): + min_vals[i] = min(min_vals[i], frame[i]) + max_vals[i] = max(max_vals[i], frame[i]) + + # 量化到16位整数 + quantized_data = [] + for frame in morph_data: + quantized_frame = [] + for i in range(3): + # 将浮点数映射到0-65535范围 + if max_vals[i] != min_vals[i]: + quantized_value = int((frame[i] - min_vals[i]) / (max_vals[i] - min_vals[i]) * 65535) + else: + quantized_value = 0 + quantized_frame.append(quantized_value) + quantized_data.append(tuple(quantized_frame)) + + return quantized_data + + def _compress_delta(self, morph_data: List[Tuple[float, float, float]]) -> List[Tuple[float, float, float]]: + """ + 差分压缩 + :param morph_data: Morph数据列表 + :return: 压缩后的数据 + """ + if not morph_data: + return morph_data + + delta_data = [morph_data[0]] # 第一帧保持原样 + + for i in range(1, len(morph_data)): + delta = ( + morph_data[i][0] - morph_data[i-1][0], + morph_data[i][1] - morph_data[i-1][1], + morph_data[i][2] - morph_data[i-1][2] + ) + delta_data.append(delta) + + return delta_data + + def _compress_rle(self, morph_data: List[Tuple[float, float, float]]) -> List[Tuple[int, Tuple[float, float, float]]]: + """ + 游程编码压缩 + :param morph_data: Morph数据列表 + :return: 压缩后的数据 + """ + if not morph_data: + return [] + + rle_data = [] + current_value = morph_data[0] + count = 1 + + for i in range(1, len(morph_data)): + if morph_data[i] == current_value: + count += 1 + else: + rle_data.append((count, current_value)) + current_value = morph_data[i] + count = 1 + + rle_data.append((count, current_value)) + return rle_data + + def decompress_morph_data(self, compressed_data: CompressedMorphData) -> Optional[List[Tuple[float, float, float]]]: + """ + 解压缩Morph数据 + :param compressed_data: 压缩的数据 + :return: 解压后的数据 + """ + try: + method = compressed_data.method + data = compressed_data.data + + if method == CompressionMethod.KEYFRAME_REDUCTION: + # 关键帧简化数据不需要特殊解压 + return data + elif method == CompressionMethod.QUANTIZATION: + return self._decompress_quantization(data) + elif method == CompressionMethod.DELTA_COMPRESSION: + return self._decompress_delta(data) + elif method == CompressionMethod.RLE: + return self._decompress_rle(data) + else: + return data + + except Exception as e: + print(f"解压缩Morph数据时出错: {e}") + import traceback + traceback.print_exc() + return None + + def _decompress_quantization(self, quantized_data: List[Tuple[int, int, int]]) -> List[Tuple[float, float, float]]: + """ + 解压量化数据 + :param quantized_data: 量化数据 + :return: 解压后的数据 + """ + # 这里需要存储原始的min_vals和max_vals来进行解压 + # 由于简化实现,我们返回原始数据 + return [(float(x), float(y), float(z)) for x, y, z in quantized_data] + + def _decompress_delta(self, delta_data: List[Tuple[float, float, float]]) -> List[Tuple[float, float, float]]: + """ + 解压差分数据 + :param delta_data: 差分数据 + :return: 解压后的数据 + """ + if not delta_data: + return [] + + original_data = [delta_data[0]] # 第一帧保持原样 + + for i in range(1, len(delta_data)): + original_frame = ( + original_data[i-1][0] + delta_data[i][0], + original_data[i-1][1] + delta_data[i][1], + original_data[i-1][2] + delta_data[i][2] + ) + original_data.append(original_frame) + + return original_data + + def _decompress_rle(self, rle_data: List[Tuple[int, Tuple[float, float, float]]]) -> List[Tuple[float, float, float]]: + """ + 解压RLE数据 + :param rle_data: RLE数据 + :return: 解压后的数据 + """ + original_data = [] + + for count, value in rle_data: + original_data.extend([value] * count) + + return original_data + + def get_compression_stats(self, compressed_data: Optional[CompressedMorphData] = None) -> Dict[str, Any]: + """ + 获取压缩统计信息 + :param compressed_data: 压缩数据(可选) + :return: 统计信息字典 + """ + stats = self.stats.copy() + + if compressed_data: + stats['current_compression_ratio'] = compressed_data.compression_ratio + stats['current_original_size'] = compressed_data.original_size + stats['current_compressed_size'] = compressed_data.compressed_size + + return stats + +class AnimationLODSystem: + """ + 动画LOD系统 + 根据距离和性能自动调整动画质量 + """ + + def __init__(self, world): + """ + 初始化动画LOD系统 + :param world: 世界对象 + """ + self.world = world + self.strategy = LODStrategy.DISTANCE_BASED + self.distance_thresholds = OPTIMIZATION_SETTINGS['lod_distance_thresholds'] + self.frame_rates = OPTIMIZATION_SETTINGS['lod_frame_rates'] + self.lod_levels: List[LODLevel] = [] + self.model_lod_levels: Dict[str, int] = {} # {model_id: lod_level} + self.camera_node = None + + # 创建LOD级别 + self._create_lod_levels() + + print("动画LOD系统初始化完成") + + def _create_lod_levels(self) -> None: + """ + 创建LOD级别 + """ + self.lod_levels = [] + + for i, (distance, frame_rate) in enumerate(zip(self.distance_thresholds, self.frame_rates)): + level = LODLevel( + level=i, + distance_threshold=distance, + frame_rate=frame_rate, + quality_factor=1.0 - (i / len(self.distance_thresholds)) + ) + self.lod_levels.append(level) + + # 添加最高LOD级别(最近距离,最高质量) + highest_level = LODLevel( + level=len(self.lod_levels), + distance_threshold=0.0, + frame_rate=1.0, + quality_factor=1.0 + ) + self.lod_levels.append(highest_level) + + def set_lod_strategy(self, strategy: LODStrategy) -> None: + """ + 设置LOD策略 + :param strategy: LOD策略 + """ + self.strategy = strategy + print(f"LOD策略设置为: {strategy.name}") + + def set_distance_thresholds(self, thresholds: List[float]) -> None: + """ + 设置距离阈值 + :param thresholds: 距离阈值列表 + """ + self.distance_thresholds = thresholds + self._create_lod_levels() + print(f"距离阈值已更新: {thresholds}") + + def set_frame_rates(self, frame_rates: List[float]) -> None: + """ + 设置帧率级别 + :param frame_rates: 帧率列表 + """ + self.frame_rates = frame_rates + self._create_lod_levels() + print(f"帧率级别已更新: {frame_rates}") + + def set_camera(self, camera_node: NodePath) -> None: + """ + 设置相机节点 + :param camera_node: 相机节点 + """ + self.camera_node = camera_node + + def update_lod_levels(self, models: Dict[str, Any]) -> None: + """ + 更新LOD级别 + :param models: 模型字典 {model_id: model_data} + """ + if not self.camera_node: + return + + camera_pos = self.camera_node.getPos() + + # 根据策略更新每个模型的LOD级别 + for model_id, model_data in models.items(): + if self.strategy == LODStrategy.DISTANCE_BASED: + self._update_distance_based_lod(model_id, model_data, camera_pos) + elif self.strategy == LODStrategy.PERFORMANCE_BASED: + self._update_performance_based_lod(model_id, model_data) + elif self.strategy == LODStrategy.HYBRID: + self._update_hybrid_lod(model_id, model_data, camera_pos) + + def _update_distance_based_lod(self, model_id: str, model_data: Any, camera_pos: Vec3) -> None: + """ + 更新基于距离的LOD + :param model_id: 模型ID + :param model_data: 模型数据 + :param camera_pos: 相机位置 + """ + try: + model_pos = model_data['node_path'].getPos() + distance = (camera_pos - model_pos).length() + + # 找到合适的LOD级别 + lod_level = len(self.lod_levels) - 1 # 默认最高质量 + for i, level in enumerate(self.lod_levels): + if distance <= level.distance_threshold: + lod_level = i + break + + self.model_lod_levels[model_id] = lod_level + + except Exception as e: + print(f"更新模型 {model_id} 的距离LOD时出错: {e}") + + def _update_performance_based_lod(self, model_id: str, model_data: Any) -> None: + """ + 更新基于性能的LOD + :param model_id: 模型ID + :param model_data: 模型数据 + """ + # 基于FPS或其他性能指标调整LOD + # 这里简化处理,实际项目中需要获取性能数据 + current_fps = 60.0 # 假设值 + + # 根据FPS调整LOD级别 + if current_fps < 30: + lod_level = min(2, len(self.lod_levels) - 1) + elif current_fps < 45: + lod_level = min(1, len(self.lod_levels) - 1) + else: + lod_level = 0 + + self.model_lod_levels[model_id] = lod_level + + def _update_hybrid_lod(self, model_id: str, model_data: Any, camera_pos: Vec3) -> None: + """ + 更新混合LOD + :param model_id: 模型ID + :param model_data: 模型数据 + :param camera_pos: 相机位置 + """ + # 结合距离和性能的混合策略 + self._update_distance_based_lod(model_id, model_data, camera_pos) + # 可以进一步根据性能调整 + + def get_model_lod_level(self, model_id: str) -> Optional[int]: + """ + 获取模型的LOD级别 + :param model_id: 模型ID + :return: LOD级别 + """ + return self.model_lod_levels.get(model_id) + + def get_lod_info(self, model_id: str) -> Optional[Dict[str, Any]]: + """ + 获取模型的LOD信息 + :param model_id: 模型ID + :return: LOD信息字典 + """ + lod_level = self.model_lod_levels.get(model_id) + if lod_level is None or lod_level >= len(self.lod_levels): + return None + + level_info = self.lod_levels[lod_level] + + return { + 'model_id': model_id, + 'lod_level': lod_level, + 'distance_threshold': level_info.distance_threshold, + 'frame_rate': level_info.frame_rate, + 'quality_factor': level_info.quality_factor + } + + def apply_lod_to_model(self, model_id: str, morphing_core) -> None: + """ + 应用LOD到模型 + :param model_id: 模型ID + :param morphing_core: Morphing核心系统 + """ + lod_level = self.model_lod_levels.get(model_id) + if lod_level is None or lod_level >= len(self.lod_levels): + return + + level_info = self.lod_levels[lod_level] + + # 这里可以应用LOD调整,例如: + # 1. 降低更新频率 + # 2. 减少变形精度 + # 3. 简化morph目标 + # 4. 降低渲染质量 + + # 示例:根据质量因子调整morph目标权重 + if model_id in morphing_core.morph_targets: + for target_name, morph_target in morphing_core.morph_targets[model_id].items(): + # 简化处理,实际项目中需要更复杂的逻辑 + adjusted_weights = [w * level_info.quality_factor for w in morph_target.weights] + morph_target.weights = adjusted_weights + +class AnimationCacheManager: + """ + 动画缓存管理器 + 提供高效的动画数据缓存机制 + """ + + def __init__(self): + """初始化动画缓存管理器""" + self.max_size = OPTIMIZATION_SETTINGS['cache_size'] + self.cache: Dict[str, CacheEntry] = {} + self.access_order: List[str] = [] # 最近最少使用顺序 + self.stats = { + 'hits': 0, + 'misses': 0, + 'evictions': 0 + } + + print("动画缓存管理器初始化完成") + + def get(self, key: str) -> Optional[Any]: + """ + 获取缓存项 + :param key: 缓存键 + :return: 缓存数据 + """ + if key in self.cache: + # 缓存命中 + self.stats['hits'] += 1 + + # 更新访问时间 + entry = self.cache[key] + entry.access_time = globalClock.getRealTime() + entry.access_count += 1 + + # 更新访问顺序 + if key in self.access_order: + self.access_order.remove(key) + self.access_order.append(key) + + return entry.data + else: + # 缓存未命中 + self.stats['misses'] += 1 + return None + + def put(self, key: str, data: Any, size: int = 1) -> None: + """ + 放入缓存项 + :param key: 缓存键 + :param data: 缓存数据 + :param size: 数据大小 + """ + # 如果缓存已满,移除最久未使用的项 + while len(self.cache) >= self.max_size and self.cache: + # 找到最久未使用的项 + if self.access_order: + oldest_key = self.access_order.pop(0) + del self.cache[oldest_key] + self.stats['evictions'] += 1 + else: + # 如果访问顺序列表为空,移除任意项 + oldest_key = next(iter(self.cache)) + del self.cache[oldest_key] + self.stats['evictions'] += 1 + + # 创建缓存条目 + entry = CacheEntry( + key=key, + data=data, + size=size, + access_time=globalClock.getRealTime(), + access_count=1 + ) + + # 存储缓存条目 + self.cache[key] = entry + self.access_order.append(key) + + def invalidate(self, key: str) -> None: + """ + 使缓存项失效 + :param key: 缓存键 + """ + if key in self.cache: + del self.cache[key] + if key in self.access_order: + self.access_order.remove(key) + + def clear(self) -> None: + """ + 清空缓存 + """ + self.cache.clear() + self.access_order.clear() + + def get_stats(self) -> Dict[str, Any]: + """ + 获取缓存统计信息 + :return: 统计信息字典 + """ + total_requests = self.stats['hits'] + self.stats['misses'] + hit_rate = self.stats['hits'] / max(1, total_requests) + + return { + 'size': len(self.cache), + 'max_size': self.max_size, + 'hits': self.stats['hits'], + 'misses': self.stats['misses'], + 'evictions': self.stats['evictions'], + 'hit_rate': hit_rate, + 'total_requests': total_requests + } + + def get_cache_info(self, key: str) -> Optional[Dict[str, Any]]: + """ + 获取缓存项信息 + :param key: 缓存键 + :return: 缓存项信息字典 + """ + if key not in self.cache: + return None + + entry = self.cache[key] + + return { + 'key': entry.key, + 'size': entry.size, + 'access_time': entry.access_time, + 'access_count': entry.access_count + } + +# 使用示例和测试代码 +def example_compressor_usage(): + """ + 动画压缩系统使用示例 + """ + print("=== 动画压缩系统使用示例 ===") + + # 创建压缩器 + compressor = AnimationCompressor() + + # 创建测试数据 + test_data = [ + (0.0, 0.0, 0.0), + (0.1, 0.05, 0.02), + (0.2, 0.1, 0.04), + (0.3, 0.15, 0.06), + (0.4, 0.2, 0.08), + (0.5, 0.25, 0.1), + (0.6, 0.3, 0.12), + (0.7, 0.35, 0.14), + (0.8, 0.4, 0.16), + (0.9, 0.45, 0.18), + (1.0, 0.5, 0.2) + ] + + # 测试不同压缩方法 + methods = [ + CompressionMethod.KEYFRAME_REDUCTION, + CompressionMethod.QUANTIZATION, + CompressionMethod.DELTA_COMPRESSION, + CompressionMethod.RLE + ] + + for method in methods: + print(f"\n测试 {method.name} 压缩:") + compressed = compressor.compress_morph_data(test_data, method) + if compressed: + print(f" 原始大小: {compressed.original_size} 字节") + print(f" 压缩后大小: {compressed.compressed_size} 字节") + print(f" 压缩比: {compressed.compression_ratio:.2f}") + + # 测试解压缩 + decompressed = compressor.decompress_morph_data(compressed) + if decompressed: + print(f" 解压数据点数: {len(decompressed)}") + + # 获取统计信息 + stats = compressor.get_compression_stats() + print(f"\n压缩统计: {stats}") + + print("=== 动画压缩系统示例结束 ===\n") + +def example_lod_usage(world): + """ + 动画LOD系统使用示例 + """ + print("=== 动画LOD系统使用示例 ===") + + # 创建LOD系统 + lod_system = AnimationLODSystem(world) + + # 设置LOD策略 + lod_system.set_lod_strategy(LODStrategy.DISTANCE_BASED) + + # 设置距离阈值 + lod_system.set_distance_thresholds([5.0, 15.0, 30.0]) + + # 设置帧率 + lod_system.set_frame_rates([1.0, 0.7, 0.4, 0.1]) + + print("LOD系统配置完成") + print("=== 动画LOD系统示例结束 ===\n") + + return lod_system + +def example_cache_usage(): + """ + 动画缓存管理器使用示例 + """ + print("=== 动画缓存管理器使用示例 ===") + + # 创建缓存管理器 + cache_manager = AnimationCacheManager() + + # 添加缓存项 + cache_manager.put("test_key_1", "test_data_1", 10) + cache_manager.put("test_key_2", "test_data_2", 20) + cache_manager.put("test_key_3", "test_data_3", 30) + + # 获取缓存项 + data1 = cache_manager.get("test_key_1") + print(f"获取缓存项1: {data1}") + + data2 = cache_manager.get("test_key_2") + print(f"获取缓存项2: {data2}") + + # 测试缓存未命中 + data4 = cache_manager.get("test_key_4") + print(f"获取不存在的缓存项: {data4}") + + # 获取统计信息 + stats = cache_manager.get_stats() + print(f"缓存统计: {stats}") + + # 获取缓存项信息 + info = cache_manager.get_cache_info("test_key_1") + print(f"缓存项信息: {info}") + + print("=== 动画缓存管理器示例结束 ===\n") + +if __name__ == "__main__": + example_compressor_usage() + # example_lod_usage(None) # 需要world对象 example_cache_usage() \ No newline at end of file diff --git a/plugins/user/morphing_animation/plugin.py b/plugins/user/morphing_animation/plugin.py index bf5b0c9e..9eeb5341 100644 --- a/plugins/user/morphing_animation/plugin.py +++ b/plugins/user/morphing_animation/plugin.py @@ -1,383 +1,383 @@ -""" -Morphing和变形动画插件 -完整的顶点变形、morphing目标、网格变形解决方案 -""" - -from plugins.plugin_manager import BasePlugin -from direct.showbase.DirectObject import DirectObject - -# 导入核心模块 -from .core import MorphingCore -from .advanced import AnimationStateMachine, AnimationCurveEditor, PhysicsAnimationIntegration -from .optimization import AnimationCompressor, AnimationLODSystem, AnimationCacheManager -from .network import AnimationNetworkSync -from .tools import AnimationAnalyzer, AnimationEditor, DebugVisualizer, AnimationProfiler - -class MorphingAnimationPlugin(BasePlugin): - """ - Morphing和变形动画插件 - 提供完整的顶点变形、morphing目标、网格变形等高级动画功能 - """ - - def __init__(self, plugin_manager, name): - """ - 初始化Morphing动画插件 - :param plugin_manager: 插件管理器 - :param name: 插件名称 - """ - super().__init__(plugin_manager, name) - self.config = { - "version": "1.0.0", - "author": "Morphing Animation Team", - "description": "完整的顶点变形、morphing目标、网格变形解决方案" - } - - # 核心系统组件 - self.core_system = None - self.state_machine_system = None - self.curve_editor_system = None - self.physics_integration_system = None - self.compressor_system = None - self.lod_system = None - self.cache_manager_system = None - self.network_system = None - - # 工具系统组件 - self.analyzer_system = None - self.editor_system = None - self.visualizer_system = None - self.profiler_system = None - - self.is_enabled = False - self.update_task = None - - print(f"Morphing动画插件初始化: {self.name}") - - def initialize(self) -> bool: - """ - 初始化插件 - :return: 是否初始化成功 - """ - try: - world = self.plugin_manager.world - - # 创建核心系统 - self.core_system = MorphingCore(world) - - # 创建高级功能系统 - self.curve_editor_system = AnimationCurveEditor(self.core_system) - self.physics_integration_system = PhysicsAnimationIntegration(self.core_system) - - # 创建优化系统 - self.compressor_system = AnimationCompressor() - self.lod_system = AnimationLODSystem(world) - self.cache_manager_system = AnimationCacheManager() - - # 创建网络同步系统 - self.network_system = AnimationNetworkSync(world, self.core_system) - - # 创建工具系统 - self.analyzer_system = AnimationAnalyzer(self.core_system) - self.editor_system = AnimationEditor(self.core_system, self.curve_editor_system) - self.visualizer_system = DebugVisualizer(world, self.core_system) - self.profiler_system = AnimationProfiler(world, self.core_system) - - print(f"Morphing动画插件初始化成功: {self.name}") - return True - - except Exception as e: - print(f"Morphing动画插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - :return: 是否启用成功 - """ - if not super().enable(): - return False - - if self.is_enabled: - print(f"Morphing动画插件已启用: {self.name}") - return True - - try: - # 启动核心系统更新 - self.core_system.start_updating() - - # 启动曲线编辑器更新 - self.curve_editor_system.start_updating() - - # 启动物理集成系统更新 - self.physics_integration_system.start_updating() - - # 启动网络同步 - self.network_system.start_sync_task() - - # 启动工具系统更新 - self.visualizer_system.is_visualizing = True - self.profiler_system.is_profiling = True - - # 启动更新任务 - self.update_task = self.plugin_manager.world.taskMgr.add( - self._update_task, - f"morphing_animation_update_{self.name}", - sort=35 - ) - - # 标记为已启用 - self.is_enabled = True - - print(f"Morphing动画插件启用成功: {self.name}") - return True - - except Exception as e: - print(f"Morphing动画插件启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self) -> bool: - """ - 禁用插件 - :return: 是否禁用成功 - """ - if not super().disable(): - return False - - if self.is_enabled: - print(f"Morphing动画插件已禁用: {self.name}") - return True - - try: - # 停止更新任务 - if self.update_task: - self.plugin_manager.world.taskMgr.remove(self.update_task) - self.update_task = None - - # 停止核心系统更新 - self.core_system.stop_updating() - - # 停止曲线编辑器更新 - self.curve_editor_system.stop_updating() - - # 停止物理集成系统更新 - self.physics_integration_system.stop_updating() - - # 停止网络同步 - self.network_system.stop_sync_task() - - # 停止工具系统更新 - self.visualizer_system.is_visualizing = False - self.profiler_system.is_profiling = False - - # 标记为已禁用 - self.is_enabled = False - - print(f"Morphing动画插件禁用成功: {self.name}") - return True - - except Exception as e: - print(f"Morphing动画插件禁用失败: {e}") - import traceback - traceback.print_exc() - return False - - def finalize(self) -> None: - """ - 清理插件资源 - """ - print(f"清理Morphing动画插件资源: {self.name}") - - try: - # 停止更新任务 - if self.update_task: - self.plugin_manager.world.taskMgr.remove(self.update_task) - self.update_task = None - - # 停止所有系统 - if self.core_system: - self.core_system.stop_updating() - if self.curve_editor_system: - self.curve_editor_system.stop_updating() - if self.physics_integration_system: - self.physics_integration_system.stop_updating() - if self.network_system: - self.network_system.stop_sync_task() - if self.visualizer_system: - self.visualizer_system.is_visualizing = False - if self.profiler_system: - self.profiler_system.is_profiling = False - - # 清理资源 - self.core_system = None - self.state_machine_system = None - self.curve_editor_system = None - self.physics_integration_system = None - self.compressor_system = None - self.lod_system = None - self.cache_manager_system = None - self.network_system = None - self.analyzer_system = None - self.editor_system = None - self.visualizer_system = None - self.profiler_system = None - - print(f"Morphing动画插件资源清理完成: {self.name}") - - except Exception as e: - print(f"清理Morphing动画插件资源失败: {e}") - import traceback - traceback.print_exc() - - def _update_task(self, task) -> int: - """ - 插件更新任务 - :param task: 任务对象 - :return: 任务状态 - """ - try: - dt = globalClock.getDt() - - # 更新核心系统 - if self.core_system: - self.core_system.update(dt) - - # 更新高级功能系统 - if self.curve_editor_system: - self.curve_editor_system.update(dt) - - # 更新物理集成系统 - if self.physics_integration_system: - self.physics_integration_system.update(dt) - - # 更新LOD系统 - if self.lod_system and self.core_system: - self.lod_system.update_lod_levels(self.core_system.model_data) - - # 更新工具系统 - if self.visualizer_system: - self.visualizer_system.update(dt) - - if self.profiler_system: - self.profiler_system.sample_performance(dt) - - except Exception as e: - print(f"插件更新任务出错: {e}") - - return task.cont - - # 核心系统访问方法 - def get_core_system(self): - """ - 获取核心动画系统 - :return: MorphingCore实例 - """ - return self.core_system - - def get_state_machine_system(self, model_id: str): - """ - 获取动画状态机系统 - :param model_id: 模型ID - :return: AnimationStateMachine实例 - """ - if self.core_system: - if not self.state_machine_system: - self.state_machine_system = {} - if model_id not in self.state_machine_system: - self.state_machine_system[model_id] = AnimationStateMachine(self.core_system, model_id) - return self.state_machine_system[model_id] - return None - - def get_curve_editor_system(self): - """ - 获取曲线编辑器系统 - :return: AnimationCurveEditor实例 - """ - return self.curve_editor_system - - def get_physics_integration_system(self): - """ - 获取物理集成系统 - :return: PhysicsAnimationIntegration实例 - """ - return self.physics_integration_system - - def get_compressor_system(self): - """ - 获取压缩系统 - :return: AnimationCompressor实例 - """ - return self.compressor_system - - def get_lod_system(self): - """ - 获取LOD系统 - :return: AnimationLODSystem实例 - """ - return self.lod_system - - def get_cache_manager_system(self): - """ - 获取缓存管理系统 - :return: AnimationCacheManager实例 - """ - return self.cache_manager_system - - def get_network_system(self): - """ - 获取网络同步系统 - :return: AnimationNetworkSync实例 - """ - return self.network_system - - # 工具系统访问方法 - def get_analyzer_system(self): - """ - 获取动画分析器系统 - :return: AnimationAnalyzer实例 - """ - return self.analyzer_system - - def get_editor_system(self): - """ - 获取动画编辑器系统 - :return: AnimationEditor实例 - """ - return self.editor_system - - def get_visualizer_system(self): - """ - 获取调试可视化工具系统 - :return: DebugVisualizer实例 - """ - return self.visualizer_system - - def get_profiler_system(self): - """ - 获取动画性能分析器系统 - :return: AnimationProfiler实例 - """ - return self.profiler_system - -# 兼容性接口 -Plugin = MorphingAnimationPlugin - -__all__ = [ - 'MorphingAnimationPlugin', - 'Plugin', - 'MorphingCore', - 'AnimationStateMachine', - 'AnimationCurveEditor', - 'PhysicsAnimationIntegration', - 'AnimationCompressor', - 'AnimationLODSystem', - 'AnimationCacheManager', - 'AnimationNetworkSync', - 'AnimationAnalyzer', - 'AnimationEditor', - 'DebugVisualizer', - 'AnimationProfiler' +""" +Morphing和变形动画插件 +完整的顶点变形、morphing目标、网格变形解决方案 +""" + +from plugins.plugin_manager import BasePlugin +from direct.showbase.DirectObject import DirectObject + +# 导入核心模块 +from .core import MorphingCore +from .advanced import AnimationStateMachine, AnimationCurveEditor, PhysicsAnimationIntegration +from .optimization import AnimationCompressor, AnimationLODSystem, AnimationCacheManager +from .network import AnimationNetworkSync +from .tools import AnimationAnalyzer, AnimationEditor, DebugVisualizer, AnimationProfiler + +class MorphingAnimationPlugin(BasePlugin): + """ + Morphing和变形动画插件 + 提供完整的顶点变形、morphing目标、网格变形等高级动画功能 + """ + + def __init__(self, plugin_manager, name): + """ + 初始化Morphing动画插件 + :param plugin_manager: 插件管理器 + :param name: 插件名称 + """ + super().__init__(plugin_manager, name) + self.config = { + "version": "1.0.0", + "author": "Morphing Animation Team", + "description": "完整的顶点变形、morphing目标、网格变形解决方案" + } + + # 核心系统组件 + self.core_system = None + self.state_machine_system = None + self.curve_editor_system = None + self.physics_integration_system = None + self.compressor_system = None + self.lod_system = None + self.cache_manager_system = None + self.network_system = None + + # 工具系统组件 + self.analyzer_system = None + self.editor_system = None + self.visualizer_system = None + self.profiler_system = None + + self.is_enabled = False + self.update_task = None + + print(f"Morphing动画插件初始化: {self.name}") + + def initialize(self) -> bool: + """ + 初始化插件 + :return: 是否初始化成功 + """ + try: + world = self.plugin_manager.world + + # 创建核心系统 + self.core_system = MorphingCore(world) + + # 创建高级功能系统 + self.curve_editor_system = AnimationCurveEditor(self.core_system) + self.physics_integration_system = PhysicsAnimationIntegration(self.core_system) + + # 创建优化系统 + self.compressor_system = AnimationCompressor() + self.lod_system = AnimationLODSystem(world) + self.cache_manager_system = AnimationCacheManager() + + # 创建网络同步系统 + self.network_system = AnimationNetworkSync(world, self.core_system) + + # 创建工具系统 + self.analyzer_system = AnimationAnalyzer(self.core_system) + self.editor_system = AnimationEditor(self.core_system, self.curve_editor_system) + self.visualizer_system = DebugVisualizer(world, self.core_system) + self.profiler_system = AnimationProfiler(world, self.core_system) + + print(f"Morphing动画插件初始化成功: {self.name}") + return True + + except Exception as e: + print(f"Morphing动画插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + :return: 是否启用成功 + """ + if not super().enable(): + return False + + if self.is_enabled: + print(f"Morphing动画插件已启用: {self.name}") + return True + + try: + # 启动核心系统更新 + self.core_system.start_updating() + + # 启动曲线编辑器更新 + self.curve_editor_system.start_updating() + + # 启动物理集成系统更新 + self.physics_integration_system.start_updating() + + # 启动网络同步 + self.network_system.start_sync_task() + + # 启动工具系统更新 + self.visualizer_system.is_visualizing = True + self.profiler_system.is_profiling = True + + # 启动更新任务 + self.update_task = self.plugin_manager.world.taskMgr.add( + self._update_task, + f"morphing_animation_update_{self.name}", + sort=35 + ) + + # 标记为已启用 + self.is_enabled = True + + print(f"Morphing动画插件启用成功: {self.name}") + return True + + except Exception as e: + print(f"Morphing动画插件启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self) -> bool: + """ + 禁用插件 + :return: 是否禁用成功 + """ + if not super().disable(): + return False + + if self.is_enabled: + print(f"Morphing动画插件已禁用: {self.name}") + return True + + try: + # 停止更新任务 + if self.update_task: + self.plugin_manager.world.taskMgr.remove(self.update_task) + self.update_task = None + + # 停止核心系统更新 + self.core_system.stop_updating() + + # 停止曲线编辑器更新 + self.curve_editor_system.stop_updating() + + # 停止物理集成系统更新 + self.physics_integration_system.stop_updating() + + # 停止网络同步 + self.network_system.stop_sync_task() + + # 停止工具系统更新 + self.visualizer_system.is_visualizing = False + self.profiler_system.is_profiling = False + + # 标记为已禁用 + self.is_enabled = False + + print(f"Morphing动画插件禁用成功: {self.name}") + return True + + except Exception as e: + print(f"Morphing动画插件禁用失败: {e}") + import traceback + traceback.print_exc() + return False + + def finalize(self) -> None: + """ + 清理插件资源 + """ + print(f"清理Morphing动画插件资源: {self.name}") + + try: + # 停止更新任务 + if self.update_task: + self.plugin_manager.world.taskMgr.remove(self.update_task) + self.update_task = None + + # 停止所有系统 + if self.core_system: + self.core_system.stop_updating() + if self.curve_editor_system: + self.curve_editor_system.stop_updating() + if self.physics_integration_system: + self.physics_integration_system.stop_updating() + if self.network_system: + self.network_system.stop_sync_task() + if self.visualizer_system: + self.visualizer_system.is_visualizing = False + if self.profiler_system: + self.profiler_system.is_profiling = False + + # 清理资源 + self.core_system = None + self.state_machine_system = None + self.curve_editor_system = None + self.physics_integration_system = None + self.compressor_system = None + self.lod_system = None + self.cache_manager_system = None + self.network_system = None + self.analyzer_system = None + self.editor_system = None + self.visualizer_system = None + self.profiler_system = None + + print(f"Morphing动画插件资源清理完成: {self.name}") + + except Exception as e: + print(f"清理Morphing动画插件资源失败: {e}") + import traceback + traceback.print_exc() + + def _update_task(self, task) -> int: + """ + 插件更新任务 + :param task: 任务对象 + :return: 任务状态 + """ + try: + dt = globalClock.getDt() + + # 更新核心系统 + if self.core_system: + self.core_system.update(dt) + + # 更新高级功能系统 + if self.curve_editor_system: + self.curve_editor_system.update(dt) + + # 更新物理集成系统 + if self.physics_integration_system: + self.physics_integration_system.update(dt) + + # 更新LOD系统 + if self.lod_system and self.core_system: + self.lod_system.update_lod_levels(self.core_system.model_data) + + # 更新工具系统 + if self.visualizer_system: + self.visualizer_system.update(dt) + + if self.profiler_system: + self.profiler_system.sample_performance(dt) + + except Exception as e: + print(f"插件更新任务出错: {e}") + + return task.cont + + # 核心系统访问方法 + def get_core_system(self): + """ + 获取核心动画系统 + :return: MorphingCore实例 + """ + return self.core_system + + def get_state_machine_system(self, model_id: str): + """ + 获取动画状态机系统 + :param model_id: 模型ID + :return: AnimationStateMachine实例 + """ + if self.core_system: + if not self.state_machine_system: + self.state_machine_system = {} + if model_id not in self.state_machine_system: + self.state_machine_system[model_id] = AnimationStateMachine(self.core_system, model_id) + return self.state_machine_system[model_id] + return None + + def get_curve_editor_system(self): + """ + 获取曲线编辑器系统 + :return: AnimationCurveEditor实例 + """ + return self.curve_editor_system + + def get_physics_integration_system(self): + """ + 获取物理集成系统 + :return: PhysicsAnimationIntegration实例 + """ + return self.physics_integration_system + + def get_compressor_system(self): + """ + 获取压缩系统 + :return: AnimationCompressor实例 + """ + return self.compressor_system + + def get_lod_system(self): + """ + 获取LOD系统 + :return: AnimationLODSystem实例 + """ + return self.lod_system + + def get_cache_manager_system(self): + """ + 获取缓存管理系统 + :return: AnimationCacheManager实例 + """ + return self.cache_manager_system + + def get_network_system(self): + """ + 获取网络同步系统 + :return: AnimationNetworkSync实例 + """ + return self.network_system + + # 工具系统访问方法 + def get_analyzer_system(self): + """ + 获取动画分析器系统 + :return: AnimationAnalyzer实例 + """ + return self.analyzer_system + + def get_editor_system(self): + """ + 获取动画编辑器系统 + :return: AnimationEditor实例 + """ + return self.editor_system + + def get_visualizer_system(self): + """ + 获取调试可视化工具系统 + :return: DebugVisualizer实例 + """ + return self.visualizer_system + + def get_profiler_system(self): + """ + 获取动画性能分析器系统 + :return: AnimationProfiler实例 + """ + return self.profiler_system + +# 兼容性接口 +Plugin = MorphingAnimationPlugin + +__all__ = [ + 'MorphingAnimationPlugin', + 'Plugin', + 'MorphingCore', + 'AnimationStateMachine', + 'AnimationCurveEditor', + 'PhysicsAnimationIntegration', + 'AnimationCompressor', + 'AnimationLODSystem', + 'AnimationCacheManager', + 'AnimationNetworkSync', + 'AnimationAnalyzer', + 'AnimationEditor', + 'DebugVisualizer', + 'AnimationProfiler' ] \ No newline at end of file diff --git a/plugins/user/morphing_animation/tools.py b/plugins/user/morphing_animation/tools.py index 0c142c5a..f6707d27 100644 --- a/plugins/user/morphing_animation/tools.py +++ b/plugins/user/morphing_animation/tools.py @@ -1,1150 +1,1150 @@ -""" -Morphing和变形动画插件 - 工具模块 -提供开发和调试工具,包括分析器、编辑器、可视化工具等 -""" - -import json -import time -import math -from typing import Dict, List, Tuple, Optional, Any, Callable -from dataclasses import dataclass, field -from collections import defaultdict, deque - -from panda3d.core import * -from direct.showbase.DirectObject import DirectObject - -# 导入核心模块 -from .core import MorphingCore -from .advanced import AnimationCurveEditor -from .optimization import AnimationCompressor - -# 定义常量 -TOOLS_SETTINGS = { - 'analyzer_sample_count': 100, - 'profiler_sample_rate': 60, - 'visualizer_update_rate': 30 -} - -@dataclass -class PerformanceSample: - """性能采样数据""" - timestamp: float - fps: float - frame_time: float - memory_usage: float - morph_targets_active: int - vertices_processed: int - -@dataclass -class AnalysisResult: - """分析结果数据""" - name: str - value: Any - severity: str # 'info', 'warning', 'error' - recommendation: Optional[str] = None - -class AnimationAnalyzer(DirectObject): - """ - 动画分析器 - 分析动画性能、质量和内存使用情况 - """ - - def __init__(self, morphing_core: MorphingCore): - """ - 初始化动画分析器 - :param morphing_core: Morphing核心系统 - """ - super().__init__() - self.morphing_core = morphing_core - self.analysis_history: List[AnalysisResult] = [] - self.is_analyzing = False - - print("动画分析器初始化完成") - - def analyze_performance(self, duration: float = 5.0) -> List[AnalysisResult]: - """ - 分析动画性能 - :param duration: 分析持续时间(秒) - :return: 分析结果列表 - """ - results = [] - - # 获取系统统计信息 - stats = self.morphing_core.get_system_stats() - - # 检查模型数量 - if stats['registered_models'] > 50: - results.append(AnalysisResult( - name="模型数量", - value=stats['registered_models'], - severity="warning", - recommendation="模型数量较多,考虑使用LOD或减少同时变形的模型数量" - )) - else: - results.append(AnalysisResult( - name="模型数量", - value=stats['registered_models'], - severity="info" - )) - - # 检查Morph目标数量 - if stats['total_morph_targets'] > 500: - results.append(AnalysisResult( - name="Morph目标总数", - value=stats['total_morph_targets'], - severity="warning", - recommendation="Morph目标数量较多,考虑合并相似目标或使用动画压缩" - )) - else: - results.append(AnalysisResult( - name="Morph目标总数", - value=stats['total_morph_targets'], - severity="info" - )) - - # 检查动画数量 - if stats['total_animations'] > 200: - results.append(AnalysisResult( - name="动画总数", - value=stats['total_animations'], - severity="warning", - recommendation="动画数量较多,考虑优化动画管理策略" - )) - else: - results.append(AnalysisResult( - name="动画总数", - value=stats['total_animations'], - severity="info" - )) - - # 检查缓存命中率 - cache_stats = stats['cache_stats'] - if cache_stats['hit_rate'] < 0.7: - results.append(AnalysisResult( - name="缓存命中率", - value=f"{cache_stats['hit_rate']:.2%}", - severity="warning", - recommendation="缓存命中率较低,考虑调整缓存策略或增加缓存大小" - )) - else: - results.append(AnalysisResult( - name="缓存命中率", - value=f"{cache_stats['hit_rate']:.2%}", - severity="info" - )) - - self.analysis_history.extend(results) - return results - - def analyze_quality(self) -> List[AnalysisResult]: - """ - 分析动画质量 - :return: 分析结果列表 - """ - results = [] - - # 检查每个模型的Morph目标 - for model_id, targets in self.morphing_core.morph_targets.items(): - model_info = self.morphing_core.get_model_info(model_id) - if not model_info: - continue - - vertex_count = model_info['vertex_count'] - - # 检查Morph目标密度 - target_count = len(targets) - if target_count > 0: - density = target_count / vertex_count - if density > 0.1: # 每10个顶点超过1个Morph目标 - results.append(AnalysisResult( - name=f"模型 {model_id} Morph目标密度", - value=f"{density:.2f}", - severity="warning", - recommendation="Morph目标密度过高,可能影响性能" - )) - - # 检查Morph目标完整性 - for target_name, target in targets.items(): - target_info = self.morphing_core.get_morph_target_info(model_id, target_name) - if target_info: - if not target_info['has_normals']: - results.append(AnalysisResult( - name=f"Morph目标 {target_name} 法线信息", - value="缺失", - severity="info", - recommendation="建议添加法线信息以获得更好的光照效果" - )) - if not target_info['has_tangents']: - results.append(AnalysisResult( - name=f"Morph目标 {target_name} 切线信息", - value="缺失", - severity="info", - recommendation="建议添加切线信息以支持法线贴图" - )) - - self.analysis_history.extend(results) - return results - - def analyze_memory(self) -> List[AnalysisResult]: - """ - 分析内存使用情况 - :return: 分析结果列表 - """ - results = [] - - total_memory = 0 - model_count = len(self.morphing_core.model_data) - - # 估算每个模型的内存使用 - for model_id, model_data in self.morphing_core.model_data.items(): - # 估算顶点数据内存 - vertex_count = sum(len(vertices) for vertices in model_data['original_vertices']) - # 位置数据 (3 floats * 4 bytes) + 法线数据 + 切线数据 - vertex_memory = vertex_count * 3 * 4 # 位置 - if model_data['original_normals']: - vertex_memory += vertex_count * 3 * 4 # 法线 - if model_data['original_tangents']: - vertex_memory += vertex_count * 3 * 4 # 切线 - - # 估算Morph目标内存 - morph_memory = 0 - if model_id in self.morphing_core.morph_targets: - for target in self.morphing_core.morph_targets[model_id].values(): - morph_memory += len(target.vertices) * 3 * 4 # 顶点偏移 - if target.normals: - morph_memory += len(target.normals) * 3 * 4 # 法线偏移 - if target.tangents: - morph_memory += len(target.tangents) * 3 * 4 # 切线偏移 - - model_memory = vertex_memory + morph_memory - total_memory += model_memory - - results.append(AnalysisResult( - name=f"模型 {model_id} 内存使用", - value=f"{model_memory / 1024 / 1024:.2f} MB", - severity="info" - )) - - results.append(AnalysisResult( - name="总内存使用", - value=f"{total_memory / 1024 / 1024:.2f} MB", - severity="info" - )) - - # 根据内存使用给出建议 - if total_memory > 500 * 1024 * 1024: # 500MB - results.append(AnalysisResult( - name="内存使用警告", - value="高", - severity="warning", - recommendation="内存使用较高,考虑使用动画压缩或减少模型复杂度" - )) - - self.analysis_history.extend(results) - return results - - def analyze_compression_potential(self, compressor: AnimationCompressor) -> List[AnalysisResult]: - """ - 分析压缩潜力 - :param compressor: 动画压缩器 - :return: 分析结果列表 - """ - results = [] - - total_savings = 0 - compressible_targets = 0 - - # 分析每个Morph目标的压缩潜力 - for model_id, targets in self.morphing_core.morph_targets.items(): - for target_name, target in targets.items(): - # 创建测试数据 - test_data = target.vertices - - # 测试不同压缩方法 - for method_name, method in [ - ("关键帧简化", compressor._compress_keyframe_reduction), - ("量化", compressor._compress_quantization), - ("差分", compressor._compress_delta) - ]: - try: - compressed = method(test_data) - original_size = len(test_data) * 3 * 4 - compressed_size = len(str(compressed)) - savings = original_size - compressed_size - ratio = original_size / max(1, compressed_size) - - if savings > 1024: # 节省超过1KB才值得压缩 - compressible_targets += 1 - total_savings += savings - - results.append(AnalysisResult( - name=f"Morph目标 {target_name} {method_name}压缩潜力", - value=f"节省 {savings / 1024:.1f} KB (比率: {ratio:.2f})", - severity="info" - )) - except Exception as e: - results.append(AnalysisResult( - name=f"Morph目标 {target_name} {method_name}压缩错误", - value=str(e), - severity="error" - )) - - if compressible_targets > 0: - results.append(AnalysisResult( - name="总压缩潜力", - value=f"可压缩目标: {compressible_targets}, 预计节省: {total_savings / 1024 / 1024:.2f} MB", - severity="info", - recommendation="建议启用动画压缩以节省内存" - )) - else: - results.append(AnalysisResult( - name="压缩潜力", - value="较低", - severity="info" - )) - - self.analysis_history.extend(results) - return results - - def get_analysis_report(self) -> Dict[str, Any]: - """ - 获取完整分析报告 - :return: 分析报告字典 - """ - report = { - 'timestamp': time.time(), - 'performance': self.analyze_performance(), - 'quality': self.analyze_quality(), - 'memory': self.analyze_memory(), - 'history': [vars(result) for result in self.analysis_history[-50:]] # 最近50条记录 - } - - return report - - def clear_analysis_history(self) -> None: - """ - 清除分析历史 - """ - self.analysis_history.clear() - print("分析历史已清除") - -class AnimationEditor(DirectObject): - """ - 动画编辑器 - 提供Morph目标和动画的编辑功能 - """ - - def __init__(self, morphing_core: MorphingCore, curve_editor: AnimationCurveEditor): - """ - 初始化动画编辑器 - :param morphing_core: Morphing核心系统 - :param curve_editor: 动画曲线编辑器 - """ - super().__init__() - self.morphing_core = morphing_core - self.curve_editor = curve_editor - self.edit_mode = "view" # "view", "keyframe", "curve", "vertex" - self.selected_model = None - self.selected_target = None - self.selected_vertex = None - self.edit_callbacks: List[Callable] = [] - - print("动画编辑器初始化完成") - - def set_edit_mode(self, mode: str) -> bool: - """ - 设置编辑模式 - :param mode: 编辑模式 ("view", "keyframe", "curve", "vertex") - :return: 是否成功设置 - """ - valid_modes = ["view", "keyframe", "curve", "vertex"] - if mode not in valid_modes: - print(f"错误: 无效的编辑模式 {mode},有效模式: {valid_modes}") - return False - - self.edit_mode = mode - print(f"编辑模式设置为: {mode}") - return True - - def select_model(self, model_id: str) -> bool: - """ - 选择模型 - :param model_id: 模型ID - :return: 是否成功选择 - """ - if model_id not in self.morphing_core.model_data: - print(f"错误: 模型 {model_id} 未注册") - return False - - self.selected_model = model_id - print(f"选择模型: {model_id}") - self._trigger_edit_callbacks("model_selected", model_id) - return True - - def select_morph_target(self, target_name: str) -> bool: - """ - 选择Morph目标 - :param target_name: Morph目标名称 - :return: 是否成功选择 - """ - if not self.selected_model: - print("错误: 请先选择模型") - return False - - if (self.selected_model not in self.morphing_core.morph_targets or - target_name not in self.morphing_core.morph_targets[self.selected_model]): - print(f"错误: Morph目标 {target_name} 不存在于模型 {self.selected_model} 中") - return False - - self.selected_target = target_name - print(f"选择Morph目标: {target_name}") - self._trigger_edit_callbacks("target_selected", target_name) - return True - - def select_vertex(self, vertex_index: int) -> bool: - """ - 选择顶点 - :param vertex_index: 顶点索引 - :return: 是否成功选择 - """ - if not self.selected_model: - print("错误: 请先选择模型") - return False - - model_data = self.morphing_core.model_data[self.selected_model] - total_vertices = sum(len(vertices) for vertices in model_data['original_vertices']) - - if vertex_index >= total_vertices: - print(f"错误: 顶点索引 {vertex_index} 超出范围 (0-{total_vertices-1})") - return False - - self.selected_vertex = vertex_index - print(f"选择顶点: {vertex_index}") - self._trigger_edit_callbacks("vertex_selected", vertex_index) - return True - - def create_morph_target(self, target_name: str, - vertex_offsets: List[Tuple[float, float, float]], - normal_offsets: Optional[List[Tuple[float, float, float]]] = None, - tangent_offsets: Optional[List[Tuple[float, float, float]]] = None) -> bool: - """ - 创建Morph目标 - :param target_name: 目标名称 - :param vertex_offsets: 顶点偏移列表 - :param normal_offsets: 法线偏移列表(可选) - :param tangent_offsets: 切线偏移列表(可选) - :return: 是否成功创建 - """ - if not self.selected_model: - print("错误: 请先选择模型") - return False - - success = self.morphing_core.create_morph_target( - self.selected_model, target_name, vertex_offsets, normal_offsets, tangent_offsets - ) - - if success: - print(f"Morph目标创建成功: {target_name}") - self._trigger_edit_callbacks("target_created", target_name) - else: - print(f"Morph目标创建失败: {target_name}") - - return success - - def modify_morph_target(self, target_name: str, vertex_index: int, - new_offset: Tuple[float, float, float]) -> bool: - """ - 修改Morph目标 - :param target_name: 目标名称 - :param vertex_index: 顶点索引 - :param new_offset: 新的偏移值 - :return: 是否成功修改 - """ - if not self.selected_model: - print("错误: 请先选择模型") - return False - - if (self.selected_model not in self.morphing_core.morph_targets or - target_name not in self.morphing_core.morph_targets[self.selected_model]): - print(f"错误: Morph目标 {target_name} 不存在") - return False - - # 获取Morph目标 - morph_target = self.morphing_core.morph_targets[self.selected_model][target_name] - - if vertex_index >= len(morph_target.vertices): - print(f"错误: 顶点索引 {vertex_index} 超出范围") - return False - - # 修改偏移值 - morph_target.vertices[vertex_index] = new_offset - - # 使相关缓存失效 - if self.selected_model in self.morphing_core.cache.cache: - cache_key = f"{self.selected_model}_{target_name}" - self.morphing_core.cache.invalidate(cache_key) - - print(f"修改Morph目标 {target_name} 的顶点 {vertex_index}") - self._trigger_edit_callbacks("target_modified", { - 'target_name': target_name, - 'vertex_index': vertex_index, - 'new_offset': new_offset - }) - return True - - def delete_morph_target(self, target_name: str) -> bool: - """ - 删除Morph目标 - :param target_name: 目标名称 - :return: 是否成功删除 - """ - if not self.selected_model: - print("错误: 请先选择模型") - return False - - success = self.morphing_core.remove_morph_target(self.selected_model, target_name) - - if success: - print(f"Morph目标删除成功: {target_name}") - self._trigger_edit_callbacks("target_deleted", target_name) - else: - print(f"Morph目标删除失败: {target_name}") - - return success - - def create_animation_curve(self, curve_name: str, control_points: List[Tuple[float, float]]) -> bool: - """ - 创建动画曲线 - :param curve_name: 曲线名称 - :param control_points: 控制点列表 - :return: 是否成功创建 - """ - from .advanced import CurveType - from .core import InterpolationMode - - success = self.curve_editor.create_curve( - curve_name, CurveType.BEZIER, control_points, InterpolationMode.LINEAR - ) - - if success: - print(f"动画曲线创建成功: {curve_name}") - self._trigger_edit_callbacks("curve_created", curve_name) - else: - print(f"动画曲线创建失败: {curve_name}") - - return success - - def modify_animation_curve(self, curve_name: str, control_points: List[Tuple[float, float]]) -> bool: - """ - 修改动画曲线 - :param curve_name: 曲线名称 - :param control_points: 新的控制点列表 - :return: 是否成功修改 - """ - # 删除旧曲线 - self.curve_editor.remove_curve(curve_name) - - # 创建新曲线 - return self.create_animation_curve(curve_name, control_points) - - def delete_animation_curve(self, curve_name: str) -> bool: - """ - 删除动画曲线 - :param curve_name: 曲线名称 - :return: 是否成功删除 - """ - success = self.curve_editor.remove_curve(curve_name) - - if success: - print(f"动画曲线删除成功: {curve_name}") - self._trigger_edit_callbacks("curve_deleted", curve_name) - else: - print(f"动画曲线删除失败: {curve_name}") - - return success - - def add_edit_callback(self, callback: Callable) -> None: - """ - 添加编辑回调函数 - :param callback: 回调函数 - """ - self.edit_callbacks.append(callback) - - def remove_edit_callback(self, callback: Callable) -> None: - """ - 移除编辑回调函数 - :param callback: 回调函数 - """ - if callback in self.edit_callbacks: - self.edit_callbacks.remove(callback) - - def _trigger_edit_callbacks(self, event_type: str, data: Any) -> None: - """ - 触发编辑回调函数 - :param event_type: 事件类型 - :param data: 事件数据 - """ - for callback in self.edit_callbacks: - try: - callback(event_type, data) - except Exception as e: - print(f"编辑回调函数执行出错: {e}") - - def get_editor_state(self) -> Dict[str, Any]: - """ - 获取编辑器状态 - :return: 状态字典 - """ - return { - 'edit_mode': self.edit_mode, - 'selected_model': self.selected_model, - 'selected_target': self.selected_target, - 'selected_vertex': self.selected_vertex, - 'available_models': list(self.morphing_core.model_data.keys()), - 'available_targets': list(self.morphing_core.morph_targets.get(self.selected_model, {}).keys()) if self.selected_model else [] - } - -class DebugVisualizer(DirectObject): - """ - 调试可视化工具 - 提供骨骼、边界框等可视化功能 - """ - - def __init__(self, world, morphing_core: MorphingCore): - """ - 初始化调试可视化工具 - :param world: 世界对象 - :param morphing_core: Morphing核心系统 - """ - super().__init__() - self.world = world - self.morphing_core = morphing_core - self.visualizations: Dict[str, NodePath] = {} - self.is_visualizing = False - self.update_rate = TOOLS_SETTINGS['visualizer_update_rate'] - self.last_update_time = 0.0 - - print("调试可视化工具初始化完成") - - def visualize_model_skeleton(self, model_id: str, show_vertices: bool = True, - show_normals: bool = False) -> bool: - """ - 可视化模型骨架 - :param model_id: 模型ID - :param show_vertices: 是否显示顶点 - :param show_normals: 是否显示法线 - :return: 是否成功可视化 - """ - if model_id not in self.morphing_core.model_data: - print(f"错误: 模型 {model_id} 未注册") - return False - - model_data = self.morphing_core.model_data[model_id] - model_node = model_data['node_path'] - - # 创建可视化节点 - vis_node_name = f"skeleton_vis_{model_id}" - if vis_node_name in self.visualizations: - self.visualizations[vis_node_name].removeNode() - - vis_node = self.world.render.attachNewNode(vis_node_name) - self.visualizations[vis_node_name] = vis_node - - # 可视化顶点 - if show_vertices: - self._visualize_vertices(model_id, vis_node) - - # 可视化法线 - if show_normals: - self._visualize_normals(model_id, vis_node) - - # 可视化Morph目标影响 - self._visualize_morph_influences(model_id, vis_node) - - print(f"模型骨架可视化完成: {model_id}") - return True - - def _visualize_vertices(self, model_id: str, parent_node: NodePath) -> None: - """ - 可视化顶点 - :param model_id: 模型ID - :param parent_node: 父节点 - """ - model_data = self.morphing_core.model_data[model_id] - - # 创建顶点可视化点 - vertex_points = PointNode("vertex_points") - vertex_points.setColor(1, 0, 0, 1) # 红色 - vertex_points.setPointSize(3) - - vertex_geom = GeomVertexWriter(parent_node.node().getGeom(0).getVertexData(), 'vertex') - # 注意:这里简化处理,实际项目中需要更复杂的实现 - - def _visualize_normals(self, model_id: str, parent_node: NodePath) -> None: - """ - 可视化法线 - :param model_id: 模型ID - :param parent_node: 父节点 - """ - # 法线可视化实现 - pass # 简化处理 - - def _visualize_morph_influences(self, model_id: str, parent_node: NodePath) -> None: - """ - 可视化Morph影响 - :param model_id: 模型ID - :param parent_node: 父节点 - """ - # Morph影响可视化实现 - pass # 简化处理 - - def visualize_bounding_boxes(self, model_ids: Optional[List[str]] = None) -> bool: - """ - 可视化边界框 - :param model_ids: 模型ID列表,如果为None则可视化所有模型 - :return: 是否成功可视化 - """ - if model_ids is None: - model_ids = list(self.morphing_core.model_data.keys()) - - for model_id in model_ids: - if model_id not in self.morphing_core.model_data: - print(f"警告: 模型 {model_id} 未注册,跳过") - continue - - model_data = self.morphing_core.model_data[model_id] - model_node = model_data['node_path'] - - # 创建边界框可视化 - bbox_node_name = f"bbox_vis_{model_id}" - if bbox_node_name in self.visualizations: - self.visualizations[bbox_node_name].removeNode() - - # 获取模型边界框 - bounds = model_node.getBounds() - if not bounds.isEmpty(): - # 创建线框立方体表示边界框 - bbox_node = self.world.render.attachNewNode(bbox_node_name) - bbox_node.setColor(0, 1, 0, 1) # 绿色 - - # 创建边界框几何体(简化实现) - # 实际项目中需要根据bounds创建对应的线框几何体 - - self.visualizations[bbox_node_name] = bbox_node - print(f"边界框可视化完成: {model_id}") - - return True - - def visualize_morph_weights(self, model_id: str) -> bool: - """ - 可视化Morph权重 - :param model_id: 模型ID - :return: 是否成功可视化 - """ - if model_id not in self.morphing_core.morph_targets: - print(f"错误: 模型 {model_id} 没有Morph目标") - return False - - # 创建权重可视化节点 - weight_node_name = f"weights_vis_{model_id}" - if weight_node_name in self.visualizations: - self.visualizations[weight_node_name].removeNode() - - weight_node = self.world.render.attachNewNode(weight_node_name) - self.visualizations[weight_node_name] = weight_node - - # 可视化每个Morph目标的权重(简化实现) - targets = self.morphing_core.morph_targets[model_id] - for i, (target_name, target) in enumerate(targets.items()): - # 计算平均权重 - if target.weights: - avg_weight = sum(target.weights) / len(target.weights) - - # 创建文本显示权重(简化实现) - # 实际项目中需要使用Panda3D的文本系统 - - print(f"Morph权重可视化完成: {model_id}") - return True - - def set_visualization_mode(self, mode: str, enabled: bool = True) -> None: - """ - 设置可视化模式 - :param mode: 可视化模式 ('skeleton', 'bounds', 'normals', 'wireframe', 'collision') - :param enabled: 是否启用 - """ - valid_modes = ['skeleton', 'bounds', 'normals', 'wireframe', 'collision'] - if mode not in valid_modes: - print(f"错误: 无效的可视化模式 {mode},有效模式: {valid_modes}") - return - - # 根据模式启用/禁用相应的可视化 - if mode == 'wireframe': - if enabled: - self.world.render.setRenderModeWireframe() - else: - self.world.render.setRenderModeFilled() - else: - # 其他模式的处理 - print(f"可视化模式 {mode} {'启用' if enabled else '禁用'}") - - def hide_all_visualizations(self) -> None: - """ - 隐藏所有可视化 - """ - for node in self.visualizations.values(): - node.hide() - print("所有可视化已隐藏") - - def show_all_visualizations(self) -> None: - """ - 显示所有可视化 - """ - for node in self.visualizations.values(): - node.show() - print("所有可视化已显示") - - def clear_visualizations(self) -> None: - """ - 清除所有可视化 - """ - for node in self.visualizations.values(): - node.removeNode() - self.visualizations.clear() - print("所有可视化已清除") - - def update(self, dt: float) -> None: - """ - 更新可视化 - :param dt: 时间增量 - """ - if not self.is_visualizing: - return - - current_time = globalClock.getRealTime() - if current_time - self.last_update_time < 1.0 / self.update_rate: - return - - self.last_update_time = current_time - - # 更新动态可视化内容 - # 例如:实时更新权重显示、动画状态等 - pass - -class AnimationProfiler(DirectObject): - """ - 动画性能分析器 - 实时监控和分析动画性能 - """ - - def __init__(self, world, morphing_core: MorphingCore): - """ - 初始化动画性能分析器 - :param world: 世界对象 - :param morphing_core: Morphing核心系统 - """ - super().__init__() - self.world = world - self.morphing_core = morphing_core - self.sample_rate = TOOLS_SETTINGS['profiler_sample_rate'] - self.sample_buffer = deque(maxlen=TOOLS_SETTINGS['analyzer_sample_count']) - self.is_profiling = False - self.last_sample_time = 0.0 - self.profiling_stats = { - 'total_samples': 0, - 'avg_fps': 0.0, - 'min_fps': float('inf'), - 'max_fps': 0.0, - 'avg_frame_time': 0.0, - 'min_frame_time': float('inf'), - 'max_frame_time': 0.0 - } - - print("动画性能分析器初始化完成") - - def start_profiling(self) -> None: - """ - 开始性能分析 - """ - if self.is_profiling: - return - - self.is_profiling = True - self.sample_buffer.clear() - self.profiling_stats = { - 'total_samples': 0, - 'avg_fps': 0.0, - 'min_fps': float('inf'), - 'max_fps': 0.0, - 'avg_frame_time': 0.0, - 'min_frame_time': float('inf'), - 'max_frame_time': 0.0 - } - - print("动画性能分析已启动") - - def stop_profiling(self) -> None: - """ - 停止性能分析 - """ - if not self.is_profiling: - return - - self.is_profiling = False - print("动画性能分析已停止") - - def sample_performance(self, dt: float) -> None: - """ - 采样性能数据 - :param dt: 时间增量 - """ - if not self.is_profiling: - return - - current_time = globalClock.getRealTime() - if current_time - self.last_sample_time < 1.0 / self.sample_rate: - return - - self.last_sample_time = current_time - - # 获取性能数据 - fps = 1.0 / max(0.001, dt) - frame_time = dt * 1000 # 转换为毫秒 - - # 获取内存使用(简化实现) - memory_usage = 0.0 # 实际项目中需要获取真实内存使用 - - # 获取活跃的Morph目标数量 - active_targets = 0 - for model_targets in self.morphing_core.morph_targets.values(): - active_targets += len([t for t in model_targets.values() if t.active]) - - # 获取处理的顶点数量 - vertices_processed = 0 - for model_data in self.morphing_core.model_data.values(): - vertices_processed += sum(len(vertices) for vertices in model_data['original_vertices']) - - # 创建采样数据 - sample = PerformanceSample( - timestamp=current_time, - fps=fps, - frame_time=frame_time, - memory_usage=memory_usage, - morph_targets_active=active_targets, - vertices_processed=vertices_processed - ) - - # 添加到缓冲区 - self.sample_buffer.append(sample) - - # 更新统计信息 - self.profiling_stats['total_samples'] += 1 - self.profiling_stats['avg_fps'] = ( - (self.profiling_stats['avg_fps'] * (self.profiling_stats['total_samples'] - 1) + fps) - / self.profiling_stats['total_samples'] - ) - self.profiling_stats['min_fps'] = min(self.profiling_stats['min_fps'], fps) - self.profiling_stats['max_fps'] = max(self.profiling_stats['max_fps'], fps) - - self.profiling_stats['avg_frame_time'] = ( - (self.profiling_stats['avg_frame_time'] * (self.profiling_stats['total_samples'] - 1) + frame_time) - / self.profiling_stats['total_samples'] - ) - self.profiling_stats['min_frame_time'] = min(self.profiling_stats['min_frame_time'], frame_time) - self.profiling_stats['max_frame_time'] = max(self.profiling_stats['max_frame_time'], frame_time) - - def get_current_stats(self) -> Dict[str, Any]: - """ - 获取当前性能统计 - :return: 统计信息字典 - """ - return { - 'current_fps': 1.0 / max(0.001, globalClock.getDt()), - 'current_frame_time': globalClock.getDt() * 1000, - 'samples_collected': len(self.sample_buffer), - 'profiling_active': self.is_profiling - } - - def get_profiling_report(self) -> Dict[str, Any]: - """ - 获取性能分析报告 - :return: 分析报告字典 - """ - if not self.sample_buffer: - return { - 'error': '没有采样数据', - 'stats': self.profiling_stats - } - - # 计算详细统计信息 - fps_values = [s.fps for s in self.sample_buffer] - frame_time_values = [s.frame_time for s in self.sample_buffer] - - # 计算标准差 - fps_variance = sum((x - self.profiling_stats['avg_fps']) ** 2 for x in fps_values) / len(fps_values) - fps_std_dev = math.sqrt(fps_variance) - - frame_time_variance = sum((x - self.profiling_stats['avg_frame_time']) ** 2 for x in frame_time_values) / len(frame_time_values) - frame_time_std_dev = math.sqrt(frame_time_variance) - - report = { - 'period': { - 'start': self.sample_buffer[0].timestamp if self.sample_buffer else 0, - 'end': self.sample_buffer[-1].timestamp if self.sample_buffer else 0, - 'duration': (self.sample_buffer[-1].timestamp - self.sample_buffer[0].timestamp) if len(self.sample_buffer) > 1 else 0 - }, - 'fps': { - 'average': self.profiling_stats['avg_fps'], - 'minimum': self.profiling_stats['min_fps'], - 'maximum': self.profiling_stats['max_fps'], - 'std_deviation': fps_std_dev - }, - 'frame_time': { - 'average': self.profiling_stats['avg_frame_time'], - 'minimum': self.profiling_stats['min_frame_time'], - 'maximum': self.profiling_stats['max_frame_time'], - 'std_deviation': frame_time_std_dev - }, - 'morphing_stats': { - 'average_active_targets': sum(s.morph_targets_active for s in self.sample_buffer) / len(self.sample_buffer), - 'average_vertices_processed': sum(s.vertices_processed for s in self.sample_buffer) / len(self.sample_buffer) - }, - 'samples': len(self.sample_buffer), - 'recommendations': self._generate_recommendations() - } - - return report - - def _generate_recommendations(self) -> List[str]: - """ - 生成性能优化建议 - :return: 建议列表 - """ - recommendations = [] - - # FPS相关建议 - if self.profiling_stats['avg_fps'] < 30: - recommendations.append("平均FPS低于30,建议优化模型复杂度或启用LOD") - elif self.profiling_stats['avg_fps'] < 60: - recommendations.append("平均FPS低于60,可以考虑进一步优化") - - # FPS稳定性建议 - fps_values = [s.fps for s in self.sample_buffer] - if fps_values: - fps_variance = sum((x - self.profiling_stats['avg_fps']) ** 2 for x in fps_values) / len(fps_values) - fps_std_dev = math.sqrt(fps_variance) - if fps_std_dev > 10: # FPS波动较大 - recommendations.append("FPS波动较大,检查是否有性能瓶颈") - - # Morph目标相关建议 - avg_targets = sum(s.morph_targets_active for s in self.sample_buffer) / len(self.sample_buffer) - if avg_targets > 100: - recommendations.append(f"平均活跃Morph目标数较高({avg_targets:.1f}),考虑减少同时活跃的目标数") - - return recommendations - - def reset_profiling(self) -> None: - """ - 重置性能分析 - """ - self.sample_buffer.clear() - self.profiling_stats = { - 'total_samples': 0, - 'avg_fps': 0.0, - 'min_fps': float('inf'), - 'max_fps': 0.0, - 'avg_frame_time': 0.0, - 'min_frame_time': float('inf'), - 'max_frame_time': 0.0 - } - print("性能分析已重置") - -# 使用示例和测试代码 -def example_analyzer_usage(morphing_core): - """ - 动画分析器使用示例 - """ - print("=== 动画分析器使用示例 ===") - - # 创建分析器 - analyzer = AnimationAnalyzer(morphing_core) - - # 执行各种分析 - performance_results = analyzer.analyze_performance() - print(f"性能分析结果数量: {len(performance_results)}") - - quality_results = analyzer.analyze_quality() - print(f"质量分析结果数量: {len(quality_results)}") - - memory_results = analyzer.analyze_memory() - print(f"内存分析结果数量: {len(memory_results)}") - - # 获取完整报告 - report = analyzer.get_analysis_report() - print(f"分析报告生成完成,包含 {len(report['history'])} 条历史记录") - - print("=== 动画分析器示例结束 ===\n") - -def example_editor_usage(morphing_core, curve_editor): - """ - 动画编辑器使用示例 - """ - print("=== 动画编辑器使用示例 ===") - - # 创建编辑器 - editor = AnimationEditor(morphing_core, curve_editor) - - # 设置编辑模式 - editor.set_edit_mode("keyframe") - - # 添加编辑回调 - def edit_callback(event_type, data): - print(f"编辑事件: {event_type}, 数据: {data}") - - editor.add_edit_callback(edit_callback) - - # 获取编辑器状态 - state = editor.get_editor_state() - print(f"编辑器状态: {state}") - - print("=== 动画编辑器示例结束 ===\n") - -def example_visualizer_usage(world, morphing_core): - """ - 调试可视化工具使用示例 - """ - print("=== 调试可视化工具使用示例 ===") - - # 创建可视化工具 - visualizer = DebugVisualizer(world, morphing_core) - - # 设置可视化模式 - visualizer.set_visualization_mode("wireframe", True) - - print("=== 调试可视化工具示例结束 ===\n") - -def example_profiler_usage(world, morphing_core): - """ - 动画性能分析器使用示例 - """ - print("=== 动画性能分析器使用示例 ===") - - # 创建性能分析器 - profiler = AnimationProfiler(world, morphing_core) - - # 开始分析 - profiler.start_profiling() - - # 采样性能数据 - profiler.sample_performance(globalClock.getDt()) - - # 获取当前统计 - stats = profiler.get_current_stats() - print(f"当前统计: {stats}") - - # 停止分析 - profiler.stop_profiling() - - print("=== 动画性能分析器示例结束 ===\n") - -if __name__ == "__main__": +""" +Morphing和变形动画插件 - 工具模块 +提供开发和调试工具,包括分析器、编辑器、可视化工具等 +""" + +import json +import time +import math +from typing import Dict, List, Tuple, Optional, Any, Callable +from dataclasses import dataclass, field +from collections import defaultdict, deque + +from panda3d.core import * +from direct.showbase.DirectObject import DirectObject + +# 导入核心模块 +from .core import MorphingCore +from .advanced import AnimationCurveEditor +from .optimization import AnimationCompressor + +# 定义常量 +TOOLS_SETTINGS = { + 'analyzer_sample_count': 100, + 'profiler_sample_rate': 60, + 'visualizer_update_rate': 30 +} + +@dataclass +class PerformanceSample: + """性能采样数据""" + timestamp: float + fps: float + frame_time: float + memory_usage: float + morph_targets_active: int + vertices_processed: int + +@dataclass +class AnalysisResult: + """分析结果数据""" + name: str + value: Any + severity: str # 'info', 'warning', 'error' + recommendation: Optional[str] = None + +class AnimationAnalyzer(DirectObject): + """ + 动画分析器 + 分析动画性能、质量和内存使用情况 + """ + + def __init__(self, morphing_core: MorphingCore): + """ + 初始化动画分析器 + :param morphing_core: Morphing核心系统 + """ + super().__init__() + self.morphing_core = morphing_core + self.analysis_history: List[AnalysisResult] = [] + self.is_analyzing = False + + print("动画分析器初始化完成") + + def analyze_performance(self, duration: float = 5.0) -> List[AnalysisResult]: + """ + 分析动画性能 + :param duration: 分析持续时间(秒) + :return: 分析结果列表 + """ + results = [] + + # 获取系统统计信息 + stats = self.morphing_core.get_system_stats() + + # 检查模型数量 + if stats['registered_models'] > 50: + results.append(AnalysisResult( + name="模型数量", + value=stats['registered_models'], + severity="warning", + recommendation="模型数量较多,考虑使用LOD或减少同时变形的模型数量" + )) + else: + results.append(AnalysisResult( + name="模型数量", + value=stats['registered_models'], + severity="info" + )) + + # 检查Morph目标数量 + if stats['total_morph_targets'] > 500: + results.append(AnalysisResult( + name="Morph目标总数", + value=stats['total_morph_targets'], + severity="warning", + recommendation="Morph目标数量较多,考虑合并相似目标或使用动画压缩" + )) + else: + results.append(AnalysisResult( + name="Morph目标总数", + value=stats['total_morph_targets'], + severity="info" + )) + + # 检查动画数量 + if stats['total_animations'] > 200: + results.append(AnalysisResult( + name="动画总数", + value=stats['total_animations'], + severity="warning", + recommendation="动画数量较多,考虑优化动画管理策略" + )) + else: + results.append(AnalysisResult( + name="动画总数", + value=stats['total_animations'], + severity="info" + )) + + # 检查缓存命中率 + cache_stats = stats['cache_stats'] + if cache_stats['hit_rate'] < 0.7: + results.append(AnalysisResult( + name="缓存命中率", + value=f"{cache_stats['hit_rate']:.2%}", + severity="warning", + recommendation="缓存命中率较低,考虑调整缓存策略或增加缓存大小" + )) + else: + results.append(AnalysisResult( + name="缓存命中率", + value=f"{cache_stats['hit_rate']:.2%}", + severity="info" + )) + + self.analysis_history.extend(results) + return results + + def analyze_quality(self) -> List[AnalysisResult]: + """ + 分析动画质量 + :return: 分析结果列表 + """ + results = [] + + # 检查每个模型的Morph目标 + for model_id, targets in self.morphing_core.morph_targets.items(): + model_info = self.morphing_core.get_model_info(model_id) + if not model_info: + continue + + vertex_count = model_info['vertex_count'] + + # 检查Morph目标密度 + target_count = len(targets) + if target_count > 0: + density = target_count / vertex_count + if density > 0.1: # 每10个顶点超过1个Morph目标 + results.append(AnalysisResult( + name=f"模型 {model_id} Morph目标密度", + value=f"{density:.2f}", + severity="warning", + recommendation="Morph目标密度过高,可能影响性能" + )) + + # 检查Morph目标完整性 + for target_name, target in targets.items(): + target_info = self.morphing_core.get_morph_target_info(model_id, target_name) + if target_info: + if not target_info['has_normals']: + results.append(AnalysisResult( + name=f"Morph目标 {target_name} 法线信息", + value="缺失", + severity="info", + recommendation="建议添加法线信息以获得更好的光照效果" + )) + if not target_info['has_tangents']: + results.append(AnalysisResult( + name=f"Morph目标 {target_name} 切线信息", + value="缺失", + severity="info", + recommendation="建议添加切线信息以支持法线贴图" + )) + + self.analysis_history.extend(results) + return results + + def analyze_memory(self) -> List[AnalysisResult]: + """ + 分析内存使用情况 + :return: 分析结果列表 + """ + results = [] + + total_memory = 0 + model_count = len(self.morphing_core.model_data) + + # 估算每个模型的内存使用 + for model_id, model_data in self.morphing_core.model_data.items(): + # 估算顶点数据内存 + vertex_count = sum(len(vertices) for vertices in model_data['original_vertices']) + # 位置数据 (3 floats * 4 bytes) + 法线数据 + 切线数据 + vertex_memory = vertex_count * 3 * 4 # 位置 + if model_data['original_normals']: + vertex_memory += vertex_count * 3 * 4 # 法线 + if model_data['original_tangents']: + vertex_memory += vertex_count * 3 * 4 # 切线 + + # 估算Morph目标内存 + morph_memory = 0 + if model_id in self.morphing_core.morph_targets: + for target in self.morphing_core.morph_targets[model_id].values(): + morph_memory += len(target.vertices) * 3 * 4 # 顶点偏移 + if target.normals: + morph_memory += len(target.normals) * 3 * 4 # 法线偏移 + if target.tangents: + morph_memory += len(target.tangents) * 3 * 4 # 切线偏移 + + model_memory = vertex_memory + morph_memory + total_memory += model_memory + + results.append(AnalysisResult( + name=f"模型 {model_id} 内存使用", + value=f"{model_memory / 1024 / 1024:.2f} MB", + severity="info" + )) + + results.append(AnalysisResult( + name="总内存使用", + value=f"{total_memory / 1024 / 1024:.2f} MB", + severity="info" + )) + + # 根据内存使用给出建议 + if total_memory > 500 * 1024 * 1024: # 500MB + results.append(AnalysisResult( + name="内存使用警告", + value="高", + severity="warning", + recommendation="内存使用较高,考虑使用动画压缩或减少模型复杂度" + )) + + self.analysis_history.extend(results) + return results + + def analyze_compression_potential(self, compressor: AnimationCompressor) -> List[AnalysisResult]: + """ + 分析压缩潜力 + :param compressor: 动画压缩器 + :return: 分析结果列表 + """ + results = [] + + total_savings = 0 + compressible_targets = 0 + + # 分析每个Morph目标的压缩潜力 + for model_id, targets in self.morphing_core.morph_targets.items(): + for target_name, target in targets.items(): + # 创建测试数据 + test_data = target.vertices + + # 测试不同压缩方法 + for method_name, method in [ + ("关键帧简化", compressor._compress_keyframe_reduction), + ("量化", compressor._compress_quantization), + ("差分", compressor._compress_delta) + ]: + try: + compressed = method(test_data) + original_size = len(test_data) * 3 * 4 + compressed_size = len(str(compressed)) + savings = original_size - compressed_size + ratio = original_size / max(1, compressed_size) + + if savings > 1024: # 节省超过1KB才值得压缩 + compressible_targets += 1 + total_savings += savings + + results.append(AnalysisResult( + name=f"Morph目标 {target_name} {method_name}压缩潜力", + value=f"节省 {savings / 1024:.1f} KB (比率: {ratio:.2f})", + severity="info" + )) + except Exception as e: + results.append(AnalysisResult( + name=f"Morph目标 {target_name} {method_name}压缩错误", + value=str(e), + severity="error" + )) + + if compressible_targets > 0: + results.append(AnalysisResult( + name="总压缩潜力", + value=f"可压缩目标: {compressible_targets}, 预计节省: {total_savings / 1024 / 1024:.2f} MB", + severity="info", + recommendation="建议启用动画压缩以节省内存" + )) + else: + results.append(AnalysisResult( + name="压缩潜力", + value="较低", + severity="info" + )) + + self.analysis_history.extend(results) + return results + + def get_analysis_report(self) -> Dict[str, Any]: + """ + 获取完整分析报告 + :return: 分析报告字典 + """ + report = { + 'timestamp': time.time(), + 'performance': self.analyze_performance(), + 'quality': self.analyze_quality(), + 'memory': self.analyze_memory(), + 'history': [vars(result) for result in self.analysis_history[-50:]] # 最近50条记录 + } + + return report + + def clear_analysis_history(self) -> None: + """ + 清除分析历史 + """ + self.analysis_history.clear() + print("分析历史已清除") + +class AnimationEditor(DirectObject): + """ + 动画编辑器 + 提供Morph目标和动画的编辑功能 + """ + + def __init__(self, morphing_core: MorphingCore, curve_editor: AnimationCurveEditor): + """ + 初始化动画编辑器 + :param morphing_core: Morphing核心系统 + :param curve_editor: 动画曲线编辑器 + """ + super().__init__() + self.morphing_core = morphing_core + self.curve_editor = curve_editor + self.edit_mode = "view" # "view", "keyframe", "curve", "vertex" + self.selected_model = None + self.selected_target = None + self.selected_vertex = None + self.edit_callbacks: List[Callable] = [] + + print("动画编辑器初始化完成") + + def set_edit_mode(self, mode: str) -> bool: + """ + 设置编辑模式 + :param mode: 编辑模式 ("view", "keyframe", "curve", "vertex") + :return: 是否成功设置 + """ + valid_modes = ["view", "keyframe", "curve", "vertex"] + if mode not in valid_modes: + print(f"错误: 无效的编辑模式 {mode},有效模式: {valid_modes}") + return False + + self.edit_mode = mode + print(f"编辑模式设置为: {mode}") + return True + + def select_model(self, model_id: str) -> bool: + """ + 选择模型 + :param model_id: 模型ID + :return: 是否成功选择 + """ + if model_id not in self.morphing_core.model_data: + print(f"错误: 模型 {model_id} 未注册") + return False + + self.selected_model = model_id + print(f"选择模型: {model_id}") + self._trigger_edit_callbacks("model_selected", model_id) + return True + + def select_morph_target(self, target_name: str) -> bool: + """ + 选择Morph目标 + :param target_name: Morph目标名称 + :return: 是否成功选择 + """ + if not self.selected_model: + print("错误: 请先选择模型") + return False + + if (self.selected_model not in self.morphing_core.morph_targets or + target_name not in self.morphing_core.morph_targets[self.selected_model]): + print(f"错误: Morph目标 {target_name} 不存在于模型 {self.selected_model} 中") + return False + + self.selected_target = target_name + print(f"选择Morph目标: {target_name}") + self._trigger_edit_callbacks("target_selected", target_name) + return True + + def select_vertex(self, vertex_index: int) -> bool: + """ + 选择顶点 + :param vertex_index: 顶点索引 + :return: 是否成功选择 + """ + if not self.selected_model: + print("错误: 请先选择模型") + return False + + model_data = self.morphing_core.model_data[self.selected_model] + total_vertices = sum(len(vertices) for vertices in model_data['original_vertices']) + + if vertex_index >= total_vertices: + print(f"错误: 顶点索引 {vertex_index} 超出范围 (0-{total_vertices-1})") + return False + + self.selected_vertex = vertex_index + print(f"选择顶点: {vertex_index}") + self._trigger_edit_callbacks("vertex_selected", vertex_index) + return True + + def create_morph_target(self, target_name: str, + vertex_offsets: List[Tuple[float, float, float]], + normal_offsets: Optional[List[Tuple[float, float, float]]] = None, + tangent_offsets: Optional[List[Tuple[float, float, float]]] = None) -> bool: + """ + 创建Morph目标 + :param target_name: 目标名称 + :param vertex_offsets: 顶点偏移列表 + :param normal_offsets: 法线偏移列表(可选) + :param tangent_offsets: 切线偏移列表(可选) + :return: 是否成功创建 + """ + if not self.selected_model: + print("错误: 请先选择模型") + return False + + success = self.morphing_core.create_morph_target( + self.selected_model, target_name, vertex_offsets, normal_offsets, tangent_offsets + ) + + if success: + print(f"Morph目标创建成功: {target_name}") + self._trigger_edit_callbacks("target_created", target_name) + else: + print(f"Morph目标创建失败: {target_name}") + + return success + + def modify_morph_target(self, target_name: str, vertex_index: int, + new_offset: Tuple[float, float, float]) -> bool: + """ + 修改Morph目标 + :param target_name: 目标名称 + :param vertex_index: 顶点索引 + :param new_offset: 新的偏移值 + :return: 是否成功修改 + """ + if not self.selected_model: + print("错误: 请先选择模型") + return False + + if (self.selected_model not in self.morphing_core.morph_targets or + target_name not in self.morphing_core.morph_targets[self.selected_model]): + print(f"错误: Morph目标 {target_name} 不存在") + return False + + # 获取Morph目标 + morph_target = self.morphing_core.morph_targets[self.selected_model][target_name] + + if vertex_index >= len(morph_target.vertices): + print(f"错误: 顶点索引 {vertex_index} 超出范围") + return False + + # 修改偏移值 + morph_target.vertices[vertex_index] = new_offset + + # 使相关缓存失效 + if self.selected_model in self.morphing_core.cache.cache: + cache_key = f"{self.selected_model}_{target_name}" + self.morphing_core.cache.invalidate(cache_key) + + print(f"修改Morph目标 {target_name} 的顶点 {vertex_index}") + self._trigger_edit_callbacks("target_modified", { + 'target_name': target_name, + 'vertex_index': vertex_index, + 'new_offset': new_offset + }) + return True + + def delete_morph_target(self, target_name: str) -> bool: + """ + 删除Morph目标 + :param target_name: 目标名称 + :return: 是否成功删除 + """ + if not self.selected_model: + print("错误: 请先选择模型") + return False + + success = self.morphing_core.remove_morph_target(self.selected_model, target_name) + + if success: + print(f"Morph目标删除成功: {target_name}") + self._trigger_edit_callbacks("target_deleted", target_name) + else: + print(f"Morph目标删除失败: {target_name}") + + return success + + def create_animation_curve(self, curve_name: str, control_points: List[Tuple[float, float]]) -> bool: + """ + 创建动画曲线 + :param curve_name: 曲线名称 + :param control_points: 控制点列表 + :return: 是否成功创建 + """ + from .advanced import CurveType + from .core import InterpolationMode + + success = self.curve_editor.create_curve( + curve_name, CurveType.BEZIER, control_points, InterpolationMode.LINEAR + ) + + if success: + print(f"动画曲线创建成功: {curve_name}") + self._trigger_edit_callbacks("curve_created", curve_name) + else: + print(f"动画曲线创建失败: {curve_name}") + + return success + + def modify_animation_curve(self, curve_name: str, control_points: List[Tuple[float, float]]) -> bool: + """ + 修改动画曲线 + :param curve_name: 曲线名称 + :param control_points: 新的控制点列表 + :return: 是否成功修改 + """ + # 删除旧曲线 + self.curve_editor.remove_curve(curve_name) + + # 创建新曲线 + return self.create_animation_curve(curve_name, control_points) + + def delete_animation_curve(self, curve_name: str) -> bool: + """ + 删除动画曲线 + :param curve_name: 曲线名称 + :return: 是否成功删除 + """ + success = self.curve_editor.remove_curve(curve_name) + + if success: + print(f"动画曲线删除成功: {curve_name}") + self._trigger_edit_callbacks("curve_deleted", curve_name) + else: + print(f"动画曲线删除失败: {curve_name}") + + return success + + def add_edit_callback(self, callback: Callable) -> None: + """ + 添加编辑回调函数 + :param callback: 回调函数 + """ + self.edit_callbacks.append(callback) + + def remove_edit_callback(self, callback: Callable) -> None: + """ + 移除编辑回调函数 + :param callback: 回调函数 + """ + if callback in self.edit_callbacks: + self.edit_callbacks.remove(callback) + + def _trigger_edit_callbacks(self, event_type: str, data: Any) -> None: + """ + 触发编辑回调函数 + :param event_type: 事件类型 + :param data: 事件数据 + """ + for callback in self.edit_callbacks: + try: + callback(event_type, data) + except Exception as e: + print(f"编辑回调函数执行出错: {e}") + + def get_editor_state(self) -> Dict[str, Any]: + """ + 获取编辑器状态 + :return: 状态字典 + """ + return { + 'edit_mode': self.edit_mode, + 'selected_model': self.selected_model, + 'selected_target': self.selected_target, + 'selected_vertex': self.selected_vertex, + 'available_models': list(self.morphing_core.model_data.keys()), + 'available_targets': list(self.morphing_core.morph_targets.get(self.selected_model, {}).keys()) if self.selected_model else [] + } + +class DebugVisualizer(DirectObject): + """ + 调试可视化工具 + 提供骨骼、边界框等可视化功能 + """ + + def __init__(self, world, morphing_core: MorphingCore): + """ + 初始化调试可视化工具 + :param world: 世界对象 + :param morphing_core: Morphing核心系统 + """ + super().__init__() + self.world = world + self.morphing_core = morphing_core + self.visualizations: Dict[str, NodePath] = {} + self.is_visualizing = False + self.update_rate = TOOLS_SETTINGS['visualizer_update_rate'] + self.last_update_time = 0.0 + + print("调试可视化工具初始化完成") + + def visualize_model_skeleton(self, model_id: str, show_vertices: bool = True, + show_normals: bool = False) -> bool: + """ + 可视化模型骨架 + :param model_id: 模型ID + :param show_vertices: 是否显示顶点 + :param show_normals: 是否显示法线 + :return: 是否成功可视化 + """ + if model_id not in self.morphing_core.model_data: + print(f"错误: 模型 {model_id} 未注册") + return False + + model_data = self.morphing_core.model_data[model_id] + model_node = model_data['node_path'] + + # 创建可视化节点 + vis_node_name = f"skeleton_vis_{model_id}" + if vis_node_name in self.visualizations: + self.visualizations[vis_node_name].removeNode() + + vis_node = self.world.render.attachNewNode(vis_node_name) + self.visualizations[vis_node_name] = vis_node + + # 可视化顶点 + if show_vertices: + self._visualize_vertices(model_id, vis_node) + + # 可视化法线 + if show_normals: + self._visualize_normals(model_id, vis_node) + + # 可视化Morph目标影响 + self._visualize_morph_influences(model_id, vis_node) + + print(f"模型骨架可视化完成: {model_id}") + return True + + def _visualize_vertices(self, model_id: str, parent_node: NodePath) -> None: + """ + 可视化顶点 + :param model_id: 模型ID + :param parent_node: 父节点 + """ + model_data = self.morphing_core.model_data[model_id] + + # 创建顶点可视化点 + vertex_points = PointNode("vertex_points") + vertex_points.setColor(1, 0, 0, 1) # 红色 + vertex_points.setPointSize(3) + + vertex_geom = GeomVertexWriter(parent_node.node().getGeom(0).getVertexData(), 'vertex') + # 注意:这里简化处理,实际项目中需要更复杂的实现 + + def _visualize_normals(self, model_id: str, parent_node: NodePath) -> None: + """ + 可视化法线 + :param model_id: 模型ID + :param parent_node: 父节点 + """ + # 法线可视化实现 + pass # 简化处理 + + def _visualize_morph_influences(self, model_id: str, parent_node: NodePath) -> None: + """ + 可视化Morph影响 + :param model_id: 模型ID + :param parent_node: 父节点 + """ + # Morph影响可视化实现 + pass # 简化处理 + + def visualize_bounding_boxes(self, model_ids: Optional[List[str]] = None) -> bool: + """ + 可视化边界框 + :param model_ids: 模型ID列表,如果为None则可视化所有模型 + :return: 是否成功可视化 + """ + if model_ids is None: + model_ids = list(self.morphing_core.model_data.keys()) + + for model_id in model_ids: + if model_id not in self.morphing_core.model_data: + print(f"警告: 模型 {model_id} 未注册,跳过") + continue + + model_data = self.morphing_core.model_data[model_id] + model_node = model_data['node_path'] + + # 创建边界框可视化 + bbox_node_name = f"bbox_vis_{model_id}" + if bbox_node_name in self.visualizations: + self.visualizations[bbox_node_name].removeNode() + + # 获取模型边界框 + bounds = model_node.getBounds() + if not bounds.isEmpty(): + # 创建线框立方体表示边界框 + bbox_node = self.world.render.attachNewNode(bbox_node_name) + bbox_node.setColor(0, 1, 0, 1) # 绿色 + + # 创建边界框几何体(简化实现) + # 实际项目中需要根据bounds创建对应的线框几何体 + + self.visualizations[bbox_node_name] = bbox_node + print(f"边界框可视化完成: {model_id}") + + return True + + def visualize_morph_weights(self, model_id: str) -> bool: + """ + 可视化Morph权重 + :param model_id: 模型ID + :return: 是否成功可视化 + """ + if model_id not in self.morphing_core.morph_targets: + print(f"错误: 模型 {model_id} 没有Morph目标") + return False + + # 创建权重可视化节点 + weight_node_name = f"weights_vis_{model_id}" + if weight_node_name in self.visualizations: + self.visualizations[weight_node_name].removeNode() + + weight_node = self.world.render.attachNewNode(weight_node_name) + self.visualizations[weight_node_name] = weight_node + + # 可视化每个Morph目标的权重(简化实现) + targets = self.morphing_core.morph_targets[model_id] + for i, (target_name, target) in enumerate(targets.items()): + # 计算平均权重 + if target.weights: + avg_weight = sum(target.weights) / len(target.weights) + + # 创建文本显示权重(简化实现) + # 实际项目中需要使用Panda3D的文本系统 + + print(f"Morph权重可视化完成: {model_id}") + return True + + def set_visualization_mode(self, mode: str, enabled: bool = True) -> None: + """ + 设置可视化模式 + :param mode: 可视化模式 ('skeleton', 'bounds', 'normals', 'wireframe', 'collision') + :param enabled: 是否启用 + """ + valid_modes = ['skeleton', 'bounds', 'normals', 'wireframe', 'collision'] + if mode not in valid_modes: + print(f"错误: 无效的可视化模式 {mode},有效模式: {valid_modes}") + return + + # 根据模式启用/禁用相应的可视化 + if mode == 'wireframe': + if enabled: + self.world.render.setRenderModeWireframe() + else: + self.world.render.setRenderModeFilled() + else: + # 其他模式的处理 + print(f"可视化模式 {mode} {'启用' if enabled else '禁用'}") + + def hide_all_visualizations(self) -> None: + """ + 隐藏所有可视化 + """ + for node in self.visualizations.values(): + node.hide() + print("所有可视化已隐藏") + + def show_all_visualizations(self) -> None: + """ + 显示所有可视化 + """ + for node in self.visualizations.values(): + node.show() + print("所有可视化已显示") + + def clear_visualizations(self) -> None: + """ + 清除所有可视化 + """ + for node in self.visualizations.values(): + node.removeNode() + self.visualizations.clear() + print("所有可视化已清除") + + def update(self, dt: float) -> None: + """ + 更新可视化 + :param dt: 时间增量 + """ + if not self.is_visualizing: + return + + current_time = globalClock.getRealTime() + if current_time - self.last_update_time < 1.0 / self.update_rate: + return + + self.last_update_time = current_time + + # 更新动态可视化内容 + # 例如:实时更新权重显示、动画状态等 + pass + +class AnimationProfiler(DirectObject): + """ + 动画性能分析器 + 实时监控和分析动画性能 + """ + + def __init__(self, world, morphing_core: MorphingCore): + """ + 初始化动画性能分析器 + :param world: 世界对象 + :param morphing_core: Morphing核心系统 + """ + super().__init__() + self.world = world + self.morphing_core = morphing_core + self.sample_rate = TOOLS_SETTINGS['profiler_sample_rate'] + self.sample_buffer = deque(maxlen=TOOLS_SETTINGS['analyzer_sample_count']) + self.is_profiling = False + self.last_sample_time = 0.0 + self.profiling_stats = { + 'total_samples': 0, + 'avg_fps': 0.0, + 'min_fps': float('inf'), + 'max_fps': 0.0, + 'avg_frame_time': 0.0, + 'min_frame_time': float('inf'), + 'max_frame_time': 0.0 + } + + print("动画性能分析器初始化完成") + + def start_profiling(self) -> None: + """ + 开始性能分析 + """ + if self.is_profiling: + return + + self.is_profiling = True + self.sample_buffer.clear() + self.profiling_stats = { + 'total_samples': 0, + 'avg_fps': 0.0, + 'min_fps': float('inf'), + 'max_fps': 0.0, + 'avg_frame_time': 0.0, + 'min_frame_time': float('inf'), + 'max_frame_time': 0.0 + } + + print("动画性能分析已启动") + + def stop_profiling(self) -> None: + """ + 停止性能分析 + """ + if not self.is_profiling: + return + + self.is_profiling = False + print("动画性能分析已停止") + + def sample_performance(self, dt: float) -> None: + """ + 采样性能数据 + :param dt: 时间增量 + """ + if not self.is_profiling: + return + + current_time = globalClock.getRealTime() + if current_time - self.last_sample_time < 1.0 / self.sample_rate: + return + + self.last_sample_time = current_time + + # 获取性能数据 + fps = 1.0 / max(0.001, dt) + frame_time = dt * 1000 # 转换为毫秒 + + # 获取内存使用(简化实现) + memory_usage = 0.0 # 实际项目中需要获取真实内存使用 + + # 获取活跃的Morph目标数量 + active_targets = 0 + for model_targets in self.morphing_core.morph_targets.values(): + active_targets += len([t for t in model_targets.values() if t.active]) + + # 获取处理的顶点数量 + vertices_processed = 0 + for model_data in self.morphing_core.model_data.values(): + vertices_processed += sum(len(vertices) for vertices in model_data['original_vertices']) + + # 创建采样数据 + sample = PerformanceSample( + timestamp=current_time, + fps=fps, + frame_time=frame_time, + memory_usage=memory_usage, + morph_targets_active=active_targets, + vertices_processed=vertices_processed + ) + + # 添加到缓冲区 + self.sample_buffer.append(sample) + + # 更新统计信息 + self.profiling_stats['total_samples'] += 1 + self.profiling_stats['avg_fps'] = ( + (self.profiling_stats['avg_fps'] * (self.profiling_stats['total_samples'] - 1) + fps) + / self.profiling_stats['total_samples'] + ) + self.profiling_stats['min_fps'] = min(self.profiling_stats['min_fps'], fps) + self.profiling_stats['max_fps'] = max(self.profiling_stats['max_fps'], fps) + + self.profiling_stats['avg_frame_time'] = ( + (self.profiling_stats['avg_frame_time'] * (self.profiling_stats['total_samples'] - 1) + frame_time) + / self.profiling_stats['total_samples'] + ) + self.profiling_stats['min_frame_time'] = min(self.profiling_stats['min_frame_time'], frame_time) + self.profiling_stats['max_frame_time'] = max(self.profiling_stats['max_frame_time'], frame_time) + + def get_current_stats(self) -> Dict[str, Any]: + """ + 获取当前性能统计 + :return: 统计信息字典 + """ + return { + 'current_fps': 1.0 / max(0.001, globalClock.getDt()), + 'current_frame_time': globalClock.getDt() * 1000, + 'samples_collected': len(self.sample_buffer), + 'profiling_active': self.is_profiling + } + + def get_profiling_report(self) -> Dict[str, Any]: + """ + 获取性能分析报告 + :return: 分析报告字典 + """ + if not self.sample_buffer: + return { + 'error': '没有采样数据', + 'stats': self.profiling_stats + } + + # 计算详细统计信息 + fps_values = [s.fps for s in self.sample_buffer] + frame_time_values = [s.frame_time for s in self.sample_buffer] + + # 计算标准差 + fps_variance = sum((x - self.profiling_stats['avg_fps']) ** 2 for x in fps_values) / len(fps_values) + fps_std_dev = math.sqrt(fps_variance) + + frame_time_variance = sum((x - self.profiling_stats['avg_frame_time']) ** 2 for x in frame_time_values) / len(frame_time_values) + frame_time_std_dev = math.sqrt(frame_time_variance) + + report = { + 'period': { + 'start': self.sample_buffer[0].timestamp if self.sample_buffer else 0, + 'end': self.sample_buffer[-1].timestamp if self.sample_buffer else 0, + 'duration': (self.sample_buffer[-1].timestamp - self.sample_buffer[0].timestamp) if len(self.sample_buffer) > 1 else 0 + }, + 'fps': { + 'average': self.profiling_stats['avg_fps'], + 'minimum': self.profiling_stats['min_fps'], + 'maximum': self.profiling_stats['max_fps'], + 'std_deviation': fps_std_dev + }, + 'frame_time': { + 'average': self.profiling_stats['avg_frame_time'], + 'minimum': self.profiling_stats['min_frame_time'], + 'maximum': self.profiling_stats['max_frame_time'], + 'std_deviation': frame_time_std_dev + }, + 'morphing_stats': { + 'average_active_targets': sum(s.morph_targets_active for s in self.sample_buffer) / len(self.sample_buffer), + 'average_vertices_processed': sum(s.vertices_processed for s in self.sample_buffer) / len(self.sample_buffer) + }, + 'samples': len(self.sample_buffer), + 'recommendations': self._generate_recommendations() + } + + return report + + def _generate_recommendations(self) -> List[str]: + """ + 生成性能优化建议 + :return: 建议列表 + """ + recommendations = [] + + # FPS相关建议 + if self.profiling_stats['avg_fps'] < 30: + recommendations.append("平均FPS低于30,建议优化模型复杂度或启用LOD") + elif self.profiling_stats['avg_fps'] < 60: + recommendations.append("平均FPS低于60,可以考虑进一步优化") + + # FPS稳定性建议 + fps_values = [s.fps for s in self.sample_buffer] + if fps_values: + fps_variance = sum((x - self.profiling_stats['avg_fps']) ** 2 for x in fps_values) / len(fps_values) + fps_std_dev = math.sqrt(fps_variance) + if fps_std_dev > 10: # FPS波动较大 + recommendations.append("FPS波动较大,检查是否有性能瓶颈") + + # Morph目标相关建议 + avg_targets = sum(s.morph_targets_active for s in self.sample_buffer) / len(self.sample_buffer) + if avg_targets > 100: + recommendations.append(f"平均活跃Morph目标数较高({avg_targets:.1f}),考虑减少同时活跃的目标数") + + return recommendations + + def reset_profiling(self) -> None: + """ + 重置性能分析 + """ + self.sample_buffer.clear() + self.profiling_stats = { + 'total_samples': 0, + 'avg_fps': 0.0, + 'min_fps': float('inf'), + 'max_fps': 0.0, + 'avg_frame_time': 0.0, + 'min_frame_time': float('inf'), + 'max_frame_time': 0.0 + } + print("性能分析已重置") + +# 使用示例和测试代码 +def example_analyzer_usage(morphing_core): + """ + 动画分析器使用示例 + """ + print("=== 动画分析器使用示例 ===") + + # 创建分析器 + analyzer = AnimationAnalyzer(morphing_core) + + # 执行各种分析 + performance_results = analyzer.analyze_performance() + print(f"性能分析结果数量: {len(performance_results)}") + + quality_results = analyzer.analyze_quality() + print(f"质量分析结果数量: {len(quality_results)}") + + memory_results = analyzer.analyze_memory() + print(f"内存分析结果数量: {len(memory_results)}") + + # 获取完整报告 + report = analyzer.get_analysis_report() + print(f"分析报告生成完成,包含 {len(report['history'])} 条历史记录") + + print("=== 动画分析器示例结束 ===\n") + +def example_editor_usage(morphing_core, curve_editor): + """ + 动画编辑器使用示例 + """ + print("=== 动画编辑器使用示例 ===") + + # 创建编辑器 + editor = AnimationEditor(morphing_core, curve_editor) + + # 设置编辑模式 + editor.set_edit_mode("keyframe") + + # 添加编辑回调 + def edit_callback(event_type, data): + print(f"编辑事件: {event_type}, 数据: {data}") + + editor.add_edit_callback(edit_callback) + + # 获取编辑器状态 + state = editor.get_editor_state() + print(f"编辑器状态: {state}") + + print("=== 动画编辑器示例结束 ===\n") + +def example_visualizer_usage(world, morphing_core): + """ + 调试可视化工具使用示例 + """ + print("=== 调试可视化工具使用示例 ===") + + # 创建可视化工具 + visualizer = DebugVisualizer(world, morphing_core) + + # 设置可视化模式 + visualizer.set_visualization_mode("wireframe", True) + + print("=== 调试可视化工具示例结束 ===\n") + +def example_profiler_usage(world, morphing_core): + """ + 动画性能分析器使用示例 + """ + print("=== 动画性能分析器使用示例 ===") + + # 创建性能分析器 + profiler = AnimationProfiler(world, morphing_core) + + # 开始分析 + profiler.start_profiling() + + # 采样性能数据 + profiler.sample_performance(globalClock.getDt()) + + # 获取当前统计 + stats = profiler.get_current_stats() + print(f"当前统计: {stats}") + + # 停止分析 + profiler.stop_profiling() + + print("=== 动画性能分析器示例结束 ===\n") + +if __name__ == "__main__": print("Morphing动画工具模块加载完成") \ No newline at end of file diff --git a/plugins/user/multiplatform_input/README.md b/plugins/user/multiplatform_input/README.md index e4b4a7b8..4b8f8397 100644 --- a/plugins/user/multiplatform_input/README.md +++ b/plugins/user/multiplatform_input/README.md @@ -1,264 +1,264 @@ -# 多平台输入支持插件 - -## 概述 - -多平台输入支持插件为EG引擎提供统一的多平台输入设备支持,包括键盘、鼠标、触摸屏、游戏手柄、VR控制器等各种输入设备的抽象和管理。该插件支持跨平台兼容性,能够在Windows、Linux和macOS系统上运行。 - -## 功能特性 - -### 1. 设备管理 -- 自动发现和管理各种输入设备 -- 支持热插拔设备检测 -- 设备状态监控和统计 -- 跨平台设备兼容性适配 - -### 2. 输入映射 -- 灵活的输入映射系统 -- 支持按键、轴、手势等多种输入类型 -- 可配置的动作映射 -- 组合键和序列键支持 - -### 3. 手势识别 -- 触摸手势识别(点击、滑动、捏合、旋转等) -- 鼠标手势识别 -- 自定义手势支持 -- 手势参数可配置 - -### 4. 触觉反馈 -- 多种触觉效果类型 -- 设备特定的触觉支持 -- 效果参数可调节 -- 实时触觉播放控制 - -### 5. 事件系统 -- 高性能事件处理机制 -- 事件过滤和合并 -- 事件历史记录 -- 多线程事件处理 - -### 6. 配置管理 -- 完整的配置系统 -- 用户偏好设置 -- 配置导入导出 -- 自动备份和恢复 - -### 7. 编辑器支持 -- 可视化设备配置界面 -- 实时输入监控 -- 映射编辑器 -- 触觉效果设计器 - -## 系统架构 - -``` -multiplatform_input/ -├── plugin.py # 插件主入口 -├── core/ -│ └── input_manager.py # 输入管理器 -├── devices/ -│ └── device_manager.py # 设备管理器 -├── events/ -│ └── event_system.py # 事件系统 -├── mapping/ -│ └── input_mapper.py # 输入映射器 -├── gestures/ -│ └── gesture_recognizer.py # 手势识别器 -├── haptics/ -│ └── haptic_manager.py # 触觉管理器 -├── config/ -│ └── config_manager.py # 配置管理器 -├── editor/ -│ └── input_editor.py # 输入编辑器 -├── utils/ -│ └── input_utils.py # 工具类 -└── platforms/ - └── platform_adapter.py # 平台适配器 -``` - -## 安装和使用 - -### 安装 - -将插件目录复制到EG引擎的plugins/user目录下: - -```bash -cp -r multiplatform_input /path/to/eg/plugins/user/ -``` - -### 启用插件 - -在EG引擎中启用插件: - -```python -# 在插件管理器中加载 -plugin_manager.load_plugin('multiplatform_input') -``` - -### 基本使用 - -```python -# 获取插件实例 -input_plugin = plugin_manager.get_plugin('multiplatform_input') - -# 注册输入动作回调 -input_plugin.input_mapper.register_action_callback('jump', on_jump) - -# 处理输入事件 -def on_input_event(event_data): - print(f"Input event: {event_data}") - -input_plugin.input_manager.register_event_listener('input_pressed', on_input_event) -``` - -## 配置说明 - -### 输入配置 -```json -{ - "polling_rate": 120, - "deadzone": 0.1, - "double_click_time": 0.3, - "long_press_time": 0.5 -} -``` - -### 手势配置 -```json -{ - "tap_max_duration": 0.3, - "swipe_min_distance": 50.0, - "pinch_threshold": 20.0 -} -``` - -### 触觉配置 -```json -{ - "default_intensity": 0.5, - "default_duration": 0.2, - "max_intensity": 1.0 -} -``` - -## API参考 - -### 主要类和方法 - -#### InputManager -- `initialize()` - 初始化输入管理器 -- `enable()` - 启用输入管理器 -- `add_input_device()` - 添加输入设备 -- `submit_input_event()` - 提交输入事件 - -#### DeviceManager -- `register_device()` - 注册设备 -- `connect_device()` - 连接设备 -- `scan_for_devices()` - 扫描设备 - -#### InputMapper -- `create_mapping()` - 创建输入映射 -- `handle_input_event()` - 处理输入事件 -- `register_action_callback()` - 注册动作回调 - -#### GestureRecognizer -- `start_touch()` - 开始触摸 -- `update_touch()` - 更新触摸 -- `end_touch()` - 结束触摸 - -#### HapticManager -- `create_haptic_effect()` - 创建触觉效果 -- `play_haptic_effect()` - 播放触觉效果 -- `stop_haptic_effect()` - 停止触觉效果 - -## 平台支持 - -### Windows -- 支持DirectInput和XInput -- 支持原始输入API -- 完整的触觉反馈支持 - -### Linux -- 支持evdev事件接口 -- 支持udev设备管理 -- X11输入支持 - -### macOS -- 支持IOKit框架 -- 支持CoreGraphics事件 -- HID设备支持 - -## 性能优化 - -### 事件处理 -- 多线程事件处理机制 -- 事件合并和过滤 -- 内存池管理 - -### 设备管理 -- 延迟设备初始化 -- 智能轮询策略 -- 缓存优化 - -## 扩展开发 - -### 添加新设备类型 -1. 在DeviceManager中注册新设备类型 -2. 实现设备特定的数据处理逻辑 -3. 在PlatformAdapter中添加平台适配代码 - -### 添加新手势 -1. 在GestureRecognizer中定义新手势类型 -2. 实现手势识别算法 -3. 注册手势回调处理函数 - -### 添加新触觉效果 -1. 在HapticManager中定义新效果类型 -2. 实现效果播放逻辑 -3. 添加设备特定的支持代码 - -## 故障排除 - -### 常见问题 - -1. **设备无法识别** - - 检查设备连接状态 - - 确认平台API支持 - - 查看系统权限设置 - -2. **输入无响应** - - 检查输入映射配置 - - 确认设备已正确连接 - - 查看事件处理日志 - -3. **触觉效果不工作** - - 确认设备支持触觉反馈 - - 检查触觉效果配置 - - 验证驱动程序版本 - -### 日志和调试 -```python -# 启用详细日志 -input_plugin.utils.set_utils_config({'enable_logging': True, 'log_level': 'DEBUG'}) - -# 查看设备状态 -devices = input_plugin.device_manager.get_all_input_devices() -for device_id, device_info in devices.items(): - print(f"Device {device_id}: {device_info}") -``` - -## 许可证 - -本插件遵循EG引擎的许可证协议。 - -## 版本历史 - -### 1.0.0 -- 初始版本发布 -- 支持基本输入设备管理 -- 实现输入映射系统 -- 添加手势识别功能 -- 支持触觉反馈系统 - -## 联系方式 - +# 多平台输入支持插件 + +## 概述 + +多平台输入支持插件为EG引擎提供统一的多平台输入设备支持,包括键盘、鼠标、触摸屏、游戏手柄、VR控制器等各种输入设备的抽象和管理。该插件支持跨平台兼容性,能够在Windows、Linux和macOS系统上运行。 + +## 功能特性 + +### 1. 设备管理 +- 自动发现和管理各种输入设备 +- 支持热插拔设备检测 +- 设备状态监控和统计 +- 跨平台设备兼容性适配 + +### 2. 输入映射 +- 灵活的输入映射系统 +- 支持按键、轴、手势等多种输入类型 +- 可配置的动作映射 +- 组合键和序列键支持 + +### 3. 手势识别 +- 触摸手势识别(点击、滑动、捏合、旋转等) +- 鼠标手势识别 +- 自定义手势支持 +- 手势参数可配置 + +### 4. 触觉反馈 +- 多种触觉效果类型 +- 设备特定的触觉支持 +- 效果参数可调节 +- 实时触觉播放控制 + +### 5. 事件系统 +- 高性能事件处理机制 +- 事件过滤和合并 +- 事件历史记录 +- 多线程事件处理 + +### 6. 配置管理 +- 完整的配置系统 +- 用户偏好设置 +- 配置导入导出 +- 自动备份和恢复 + +### 7. 编辑器支持 +- 可视化设备配置界面 +- 实时输入监控 +- 映射编辑器 +- 触觉效果设计器 + +## 系统架构 + +``` +multiplatform_input/ +├── plugin.py # 插件主入口 +├── core/ +│ └── input_manager.py # 输入管理器 +├── devices/ +│ └── device_manager.py # 设备管理器 +├── events/ +│ └── event_system.py # 事件系统 +├── mapping/ +│ └── input_mapper.py # 输入映射器 +├── gestures/ +│ └── gesture_recognizer.py # 手势识别器 +├── haptics/ +│ └── haptic_manager.py # 触觉管理器 +├── config/ +│ └── config_manager.py # 配置管理器 +├── editor/ +│ └── input_editor.py # 输入编辑器 +├── utils/ +│ └── input_utils.py # 工具类 +└── platforms/ + └── platform_adapter.py # 平台适配器 +``` + +## 安装和使用 + +### 安装 + +将插件目录复制到EG引擎的plugins/user目录下: + +```bash +cp -r multiplatform_input /path/to/eg/plugins/user/ +``` + +### 启用插件 + +在EG引擎中启用插件: + +```python +# 在插件管理器中加载 +plugin_manager.load_plugin('multiplatform_input') +``` + +### 基本使用 + +```python +# 获取插件实例 +input_plugin = plugin_manager.get_plugin('multiplatform_input') + +# 注册输入动作回调 +input_plugin.input_mapper.register_action_callback('jump', on_jump) + +# 处理输入事件 +def on_input_event(event_data): + print(f"Input event: {event_data}") + +input_plugin.input_manager.register_event_listener('input_pressed', on_input_event) +``` + +## 配置说明 + +### 输入配置 +```json +{ + "polling_rate": 120, + "deadzone": 0.1, + "double_click_time": 0.3, + "long_press_time": 0.5 +} +``` + +### 手势配置 +```json +{ + "tap_max_duration": 0.3, + "swipe_min_distance": 50.0, + "pinch_threshold": 20.0 +} +``` + +### 触觉配置 +```json +{ + "default_intensity": 0.5, + "default_duration": 0.2, + "max_intensity": 1.0 +} +``` + +## API参考 + +### 主要类和方法 + +#### InputManager +- `initialize()` - 初始化输入管理器 +- `enable()` - 启用输入管理器 +- `add_input_device()` - 添加输入设备 +- `submit_input_event()` - 提交输入事件 + +#### DeviceManager +- `register_device()` - 注册设备 +- `connect_device()` - 连接设备 +- `scan_for_devices()` - 扫描设备 + +#### InputMapper +- `create_mapping()` - 创建输入映射 +- `handle_input_event()` - 处理输入事件 +- `register_action_callback()` - 注册动作回调 + +#### GestureRecognizer +- `start_touch()` - 开始触摸 +- `update_touch()` - 更新触摸 +- `end_touch()` - 结束触摸 + +#### HapticManager +- `create_haptic_effect()` - 创建触觉效果 +- `play_haptic_effect()` - 播放触觉效果 +- `stop_haptic_effect()` - 停止触觉效果 + +## 平台支持 + +### Windows +- 支持DirectInput和XInput +- 支持原始输入API +- 完整的触觉反馈支持 + +### Linux +- 支持evdev事件接口 +- 支持udev设备管理 +- X11输入支持 + +### macOS +- 支持IOKit框架 +- 支持CoreGraphics事件 +- HID设备支持 + +## 性能优化 + +### 事件处理 +- 多线程事件处理机制 +- 事件合并和过滤 +- 内存池管理 + +### 设备管理 +- 延迟设备初始化 +- 智能轮询策略 +- 缓存优化 + +## 扩展开发 + +### 添加新设备类型 +1. 在DeviceManager中注册新设备类型 +2. 实现设备特定的数据处理逻辑 +3. 在PlatformAdapter中添加平台适配代码 + +### 添加新手势 +1. 在GestureRecognizer中定义新手势类型 +2. 实现手势识别算法 +3. 注册手势回调处理函数 + +### 添加新触觉效果 +1. 在HapticManager中定义新效果类型 +2. 实现效果播放逻辑 +3. 添加设备特定的支持代码 + +## 故障排除 + +### 常见问题 + +1. **设备无法识别** + - 检查设备连接状态 + - 确认平台API支持 + - 查看系统权限设置 + +2. **输入无响应** + - 检查输入映射配置 + - 确认设备已正确连接 + - 查看事件处理日志 + +3. **触觉效果不工作** + - 确认设备支持触觉反馈 + - 检查触觉效果配置 + - 验证驱动程序版本 + +### 日志和调试 +```python +# 启用详细日志 +input_plugin.utils.set_utils_config({'enable_logging': True, 'log_level': 'DEBUG'}) + +# 查看设备状态 +devices = input_plugin.device_manager.get_all_input_devices() +for device_id, device_info in devices.items(): + print(f"Device {device_id}: {device_info}") +``` + +## 许可证 + +本插件遵循EG引擎的许可证协议。 + +## 版本历史 + +### 1.0.0 +- 初始版本发布 +- 支持基本输入设备管理 +- 实现输入映射系统 +- 添加手势识别功能 +- 支持触觉反馈系统 + +## 联系方式 + 如有问题或建议,请联系插件维护团队。 \ No newline at end of file diff --git a/plugins/user/multiplatform_input/config/config_manager.py b/plugins/user/multiplatform_input/config/config_manager.py index 082b4bf2..2e7def22 100644 --- a/plugins/user/multiplatform_input/config/config_manager.py +++ b/plugins/user/multiplatform_input/config/config_manager.py @@ -1,730 +1,730 @@ -""" -配置管理器模块 -负责管理多平台输入插件的配置和用户偏好设置 -""" - -import json -import os -from typing import Dict, Any, List, Optional -from enum import Enum -import time - -class ConfigType(Enum): - """配置类型枚举""" - INPUT = "input" - DEVICE = "device" - GESTURE = "gesture" - HAPTIC = "haptic" - MAPPING = "mapping" - USER_PREFERENCES = "user_preferences" - -class ConfigManager: - """ - 配置管理器 - 负责管理多平台输入插件的配置和用户偏好设置 - """ - - def __init__(self, plugin): - """ - 初始化配置管理器 - - Args: - plugin: 多平台输入支持插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 配置路径 - self.config_paths = { - 'base_path': './config/multiplatform_input', - 'input_config': './config/multiplatform_input/input_config.json', - 'device_config': './config/multiplatform_input/device_config.json', - 'gesture_config': './config/multiplatform_input/gesture_config.json', - 'haptic_config': './config/multiplatform_input/haptic_config.json', - 'mapping_config': './config/multiplatform_input/mapping_config.json', - 'user_preferences': './config/multiplatform_input/user_preferences.json' - } - - # 配置数据 - self.config_data = { - ConfigType.INPUT: {}, - ConfigType.DEVICE: {}, - ConfigType.GESTURE: {}, - ConfigType.HAPTIC: {}, - ConfigType.MAPPING: {}, - ConfigType.USER_PREFERENCES: {} - } - - # 默认配置 - self.default_configs = { - ConfigType.INPUT: { - 'polling_rate': 120, - 'deadzone': 0.1, - 'double_click_time': 0.3, - 'long_press_time': 0.5, - 'swipe_threshold': 50, - 'gesture_timeout': 1.0 - }, - ConfigType.DEVICE: { - 'auto_discovery': True, - 'discovery_interval': 5.0, - 'supported_platforms': ['Windows', 'Linux', 'Darwin'] - }, - ConfigType.GESTURE: { - 'tap_max_duration': 0.3, - 'tap_max_distance': 20.0, - 'double_tap_timeout': 0.3, - 'long_press_duration': 0.5, - 'swipe_min_distance': 50.0, - 'swipe_max_duration': 0.5, - 'pinch_threshold': 20.0, - 'rotate_threshold': 10.0 - }, - ConfigType.HAPTIC: { - 'default_intensity': 0.5, - 'default_duration': 0.2, - 'max_intensity': 1.0, - 'min_duration': 0.01, - 'max_duration': 5.0, - 'update_rate': 60.0 - }, - ConfigType.MAPPING: { - 'enable_mapping': True, - 'enable_combinations': True, - 'enable_sequences': True, - 'combination_timeout': 0.5, - 'sequence_timeout': 1.0 - }, - ConfigType.USER_PREFERENCES: { - 'language': 'zh-CN', - 'theme': 'dark', - 'enable_notifications': True, - 'auto_save_config': True, - 'config_backup_count': 5 - } - } - - # 配置版本 - self.config_version = "1.0.0" - - # 配置变更监听器 - self.config_listeners = {} - - # 统计信息 - self.config_stats = { - 'configs_loaded': 0, - 'configs_saved': 0, - 'configs_changed': 0, - 'backup_created': 0, - 'load_errors': 0, - 'save_errors': 0 - } - - # 时间戳记录 - self.last_config_change = {} - self.config_load_time = {} - - print("✓ 配置管理器已创建") - - def initialize(self) -> bool: - """ - 初始化配置管理器 - - Returns: - 是否初始化成功 - """ - try: - # 创建配置目录 - self._create_config_directories() - - # 加载所有配置 - self._load_all_configs() - - # 应用默认配置(如果配置不存在) - self._apply_default_configs() - - self.initialized = True - print("✓ 配置管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 配置管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用配置管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 配置管理器未初始化") - return False - - self.enabled = True - print("✓ 配置管理器已启用") - return True - - except Exception as e: - print(f"✗ 配置管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用配置管理器""" - try: - self.enabled = False - - # 自动保存配置(如果启用) - if self.get_config(ConfigType.USER_PREFERENCES, 'auto_save_config', True): - self.save_all_configs() - - print("✓ 配置管理器已禁用") - - except Exception as e: - print(f"✗ 配置管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理配置管理器资源""" - try: - self.disable() - self.config_data.clear() - self.default_configs.clear() - self.config_listeners.clear() - self.last_config_change.clear() - self.config_load_time.clear() - self.initialized = False - print("✓ 配置管理器资源已清理") - - except Exception as e: - print(f"✗ 配置管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新配置管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 定期自动保存配置(如果启用) - # 这里可以实现定期保存逻辑 - pass - - except Exception as e: - print(f"✗ 配置管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _create_config_directories(self): - """创建配置目录""" - try: - base_path = self.config_paths['base_path'] - if not os.path.exists(base_path): - os.makedirs(base_path) - print(f"✓ 配置目录已创建: {base_path}") - except Exception as e: - print(f"✗ 配置目录创建失败: {e}") - - def _load_all_configs(self): - """加载所有配置""" - try: - for config_type in ConfigType: - self._load_config(config_type) - - print("✓ 所有配置加载完成") - except Exception as e: - print(f"✗ 配置加载失败: {e}") - - def _load_config(self, config_type: ConfigType): - """ - 加载指定类型的配置 - - Args: - config_type: 配置类型 - """ - try: - config_file = self.config_paths[config_type.value + '_config'] - - if os.path.exists(config_file): - with open(config_file, 'r', encoding='utf-8') as f: - config_data = json.load(f) - - self.config_data[config_type] = config_data - self.config_load_time[config_type.value] = time.time() - self.config_stats['configs_loaded'] += 1 - - print(f"✓ 配置已加载: {config_type.value}") - else: - print(f"⚠ 配置文件不存在: {config_file}") - - except Exception as e: - print(f"✗ 配置加载失败 {config_type.value}: {e}") - self.config_stats['load_errors'] += 1 - - def _apply_default_configs(self): - """应用默认配置""" - try: - for config_type, default_config in self.default_configs.items(): - if not self.config_data[config_type]: - self.config_data[config_type] = default_config.copy() - print(f"✓ 默认配置已应用: {config_type.value}") - - except Exception as e: - print(f"✗ 默认配置应用失败: {e}") - - def save_all_configs(self) -> bool: - """ - 保存所有配置 - - Returns: - 是否保存成功 - """ - try: - success = True - for config_type in ConfigType: - if not self._save_config(config_type): - success = False - - if success: - print("✓ 所有配置保存完成") - return success - - except Exception as e: - print(f"✗ 配置保存失败: {e}") - self.config_stats['save_errors'] += 1 - return False - - def _save_config(self, config_type: ConfigType) -> bool: - """ - 保存指定类型的配置 - - Args: - config_type: 配置类型 - - Returns: - 是否保存成功 - """ - try: - config_file = self.config_paths[config_type.value + '_config'] - - # 创建备份(如果启用) - if self.get_config(ConfigType.USER_PREFERENCES, 'auto_backup_config', False): - self._create_backup(config_file) - - # 保存配置 - with open(config_file, 'w', encoding='utf-8') as f: - json.dump(self.config_data[config_type], f, ensure_ascii=False, indent=2) - - self.config_stats['configs_saved'] += 1 - print(f"✓ 配置已保存: {config_type.value}") - return True - - except Exception as e: - print(f"✗ 配置保存失败 {config_type.value}: {e}") - self.config_stats['save_errors'] += 1 - return False - - def _create_backup(self, config_file: str): - """ - 创建配置备份 - - Args: - config_file: 配置文件路径 - """ - try: - if not os.path.exists(config_file): - return - - backup_count = self.get_config(ConfigType.USER_PREFERENCES, 'config_backup_count', 5) - - # 创建备份文件名 - base_name = os.path.splitext(config_file)[0] - backup_file = f"{base_name}.backup.{int(time.time())}.json" - - # 复制文件 - with open(config_file, 'r', encoding='utf-8') as src: - with open(backup_file, 'w', encoding='utf-8') as dst: - dst.write(src.read()) - - self.config_stats['backup_created'] += 1 - print(f"✓ 配置备份已创建: {backup_file}") - - # 清理旧备份 - self._cleanup_old_backups(config_file, backup_count) - - except Exception as e: - print(f"✗ 配置备份创建失败: {e}") - - def _cleanup_old_backups(self, config_file: str, max_backups: int): - """ - 清理旧备份 - - Args: - config_file: 配置文件路径 - max_backups: 最大备份数量 - """ - try: - base_name = os.path.splitext(config_file)[0] - backup_pattern = f"{base_name}.backup." - - # 查找备份文件 - backup_files = [] - config_dir = os.path.dirname(config_file) - if os.path.exists(config_dir): - for file in os.listdir(config_dir): - if file.startswith(backup_pattern) and file.endswith('.json'): - file_path = os.path.join(config_dir, file) - backup_files.append((file_path, os.path.getmtime(file_path))) - - # 按时间排序 - backup_files.sort(key=lambda x: x[1], reverse=True) - - # 删除多余的备份 - for file_path, _ in backup_files[max_backups:]: - try: - os.remove(file_path) - print(f"✓ 旧备份已删除: {file_path}") - except Exception as e: - print(f"✗ 旧备份删除失败: {file_path} - {e}") - - except Exception as e: - print(f"✗ 旧备份清理失败: {e}") - - def get_config(self, config_type: ConfigType, key: str, default_value: Any = None) -> Any: - """ - 获取配置值 - - Args: - config_type: 配置类型 - key: 配置键 - default_value: 默认值 - - Returns: - 配置值 - """ - try: - config_section = self.config_data.get(config_type, {}) - return config_section.get(key, default_value) - except Exception as e: - print(f"✗ 配置获取失败: {config_type.value}.{key} - {e}") - return default_value - - def set_config(self, config_type: ConfigType, key: str, value: Any) -> bool: - """ - 设置配置值 - - Args: - config_type: 配置类型 - key: 配置键 - value: 配置值 - - Returns: - 是否设置成功 - """ - try: - if config_type not in self.config_data: - self.config_data[config_type] = {} - - old_value = self.config_data[config_type].get(key) - self.config_data[config_type][key] = value - self.last_config_change[config_type.value] = time.time() - self.config_stats['configs_changed'] += 1 - - # 触发配置变更监听器 - self._trigger_config_listener(config_type, key, old_value, value) - - print(f"✓ 配置已设置: {config_type.value}.{key} = {value}") - return True - - except Exception as e: - print(f"✗ 配置设置失败: {config_type.value}.{key} - {e}") - return False - - def get_config_section(self, config_type: ConfigType) -> Dict[str, Any]: - """ - 获取配置节 - - Args: - config_type: 配置类型 - - Returns: - 配置节字典 - """ - return self.config_data.get(config_type, {}).copy() - - def set_config_section(self, config_type: ConfigType, config_section: Dict[str, Any]) -> bool: - """ - 设置配置节 - - Args: - config_type: 配置类型 - config_section: 配置节字典 - - Returns: - 是否设置成功 - """ - try: - self.config_data[config_type] = config_section.copy() - self.last_config_change[config_type.value] = time.time() - self.config_stats['configs_changed'] += 1 - - print(f"✓ 配置节已设置: {config_type.value}") - return True - - except Exception as e: - print(f"✗ 配置节设置失败: {config_type.value} - {e}") - return False - - def reset_config(self, config_type: ConfigType) -> bool: - """ - 重置配置为默认值 - - Args: - config_type: 配置类型 - - Returns: - 是否重置成功 - """ - try: - if config_type in self.default_configs: - self.config_data[config_type] = self.default_configs[config_type].copy() - self.last_config_change[config_type.value] = time.time() - self.config_stats['configs_changed'] += 1 - - print(f"✓ 配置已重置: {config_type.value}") - return True - else: - print(f"✗ 无默认配置: {config_type.value}") - return False - - except Exception as e: - print(f"✗ 配置重置失败: {config_type.value} - {e}") - return False - - def register_config_listener(self, config_type: ConfigType, key: str, listener: callable): - """ - 注册配置变更监听器 - - Args: - config_type: 配置类型 - key: 配置键 - listener: 监听器回调函数 - """ - try: - listener_key = f"{config_type.value}.{key}" - if listener_key not in self.config_listeners: - self.config_listeners[listener_key] = [] - self.config_listeners[listener_key].append(listener) - print(f"✓ 配置监听器已注册: {listener_key}") - except Exception as e: - print(f"✗ 配置监听器注册失败: {e}") - - def unregister_config_listener(self, config_type: ConfigType, key: str, listener: callable): - """ - 注销配置变更监听器 - - Args: - config_type: 配置类型 - key: 配置键 - listener: 监听器回调函数 - """ - try: - listener_key = f"{config_type.value}.{key}" - if listener_key in self.config_listeners: - if listener in self.config_listeners[listener_key]: - self.config_listeners[listener_key].remove(listener) - print(f"✓ 配置监听器已注销: {listener_key}") - except Exception as e: - print(f"✗ 配置监听器注销失败: {e}") - - def _trigger_config_listener(self, config_type: ConfigType, key: str, old_value: Any, new_value: Any): - """ - 触发配置变更监听器 - - Args: - config_type: 配置类型 - key: 配置键 - old_value: 旧值 - new_value: 新值 - """ - try: - listener_key = f"{config_type.value}.{key}" - if listener_key in self.config_listeners: - for listener in self.config_listeners[listener_key]: - try: - listener(key, old_value, new_value) - except Exception as e: - print(f"✗ 配置监听器执行失败: {e}") - except Exception as e: - print(f"✗ 配置监听器触发失败: {e}") - - def export_config(self, config_type: ConfigType, file_path: str) -> bool: - """ - 导出配置 - - Args: - config_type: 配置类型 - file_path: 导出文件路径 - - Returns: - 是否导出成功 - """ - try: - config_data = self.config_data.get(config_type, {}) - - # 添加元数据 - export_data = { - 'config_type': config_type.value, - 'version': self.config_version, - 'export_time': time.time(), - 'data': config_data - } - - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(export_data, f, ensure_ascii=False, indent=2) - - print(f"✓ 配置已导出: {file_path}") - return True - - except Exception as e: - print(f"✗ 配置导出失败: {e}") - return False - - def import_config(self, file_path: str) -> bool: - """ - 导入配置 - - Args: - file_path: 导入文件路径 - - Returns: - 是否导入成功 - """ - try: - with open(file_path, 'r', encoding='utf-8') as f: - import_data = json.load(f) - - config_type = ConfigType(import_data['config_type']) - config_data = import_data['data'] - - self.config_data[config_type] = config_data - self.last_config_change[config_type.value] = time.time() - self.config_stats['configs_changed'] += 1 - - print(f"✓ 配置已导入: {file_path}") - return True - - except Exception as e: - print(f"✗ 配置导入失败: {e}") - return False - - def get_config_stats(self) -> Dict[str, Any]: - """ - 获取配置统计信息 - - Returns: - 配置统计字典 - """ - return self.config_stats.copy() - - def reset_config_stats(self): - """重置配置统计信息""" - try: - self.config_stats = { - 'configs_loaded': sum(1 for ct in ConfigType if self.config_data[ct]), - 'configs_saved': 0, - 'configs_changed': 0, - 'backup_created': 0, - 'load_errors': 0, - 'save_errors': 0 - } - print("✓ 配置统计信息已重置") - except Exception as e: - print(f"✗ 配置统计信息重置失败: {e}") - - def get_config_load_time(self, config_type: ConfigType) -> float: - """ - 获取配置加载时间 - - Args: - config_type: 配置类型 - - Returns: - 加载时间戳 - """ - return self.config_load_time.get(config_type.value, 0.0) - - def get_last_config_change(self, config_type: ConfigType) -> float: - """ - 获取最后配置变更时间 - - Args: - config_type: 配置类型 - - Returns: - 变更时间戳 - """ - return self.last_config_change.get(config_type.value, 0.0) - - def validate_config(self, config_type: ConfigType) -> bool: - """ - 验证配置有效性 - - Args: - config_type: 配置类型 - - Returns: - 是否有效 - """ - try: - # 这里可以实现配置验证逻辑 - # 例如:检查必需字段、值范围等 - config_data = self.config_data.get(config_type, {}) - - # 基本验证:确保是字典类型 - if not isinstance(config_data, dict): - print(f"✗ 配置验证失败: {config_type.value} 不是字典类型") - return False - - print(f"✓ 配置验证通过: {config_type.value}") - return True - - except Exception as e: - print(f"✗ 配置验证失败: {config_type.value} - {e}") - return False - - def migrate_config(self, old_version: str, new_version: str) -> bool: - """ - 迁移配置版本 - - Args: - old_version: 旧版本 - new_version: 新版本 - - Returns: - 是否迁移成功 - """ - try: - # 这里可以实现配置版本迁移逻辑 - # 根据版本差异调整配置结构 - print(f"✓ 配置版本迁移: {old_version} -> {new_version}") - return True - - except Exception as e: - print(f"✗ 配置版本迁移失败: {e}") - return False - +""" +配置管理器模块 +负责管理多平台输入插件的配置和用户偏好设置 +""" + +import json +import os +from typing import Dict, Any, List, Optional +from enum import Enum +import time + +class ConfigType(Enum): + """配置类型枚举""" + INPUT = "input" + DEVICE = "device" + GESTURE = "gesture" + HAPTIC = "haptic" + MAPPING = "mapping" + USER_PREFERENCES = "user_preferences" + +class ConfigManager: + """ + 配置管理器 + 负责管理多平台输入插件的配置和用户偏好设置 + """ + + def __init__(self, plugin): + """ + 初始化配置管理器 + + Args: + plugin: 多平台输入支持插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 配置路径 + self.config_paths = { + 'base_path': './config/multiplatform_input', + 'input_config': './config/multiplatform_input/input_config.json', + 'device_config': './config/multiplatform_input/device_config.json', + 'gesture_config': './config/multiplatform_input/gesture_config.json', + 'haptic_config': './config/multiplatform_input/haptic_config.json', + 'mapping_config': './config/multiplatform_input/mapping_config.json', + 'user_preferences': './config/multiplatform_input/user_preferences.json' + } + + # 配置数据 + self.config_data = { + ConfigType.INPUT: {}, + ConfigType.DEVICE: {}, + ConfigType.GESTURE: {}, + ConfigType.HAPTIC: {}, + ConfigType.MAPPING: {}, + ConfigType.USER_PREFERENCES: {} + } + + # 默认配置 + self.default_configs = { + ConfigType.INPUT: { + 'polling_rate': 120, + 'deadzone': 0.1, + 'double_click_time': 0.3, + 'long_press_time': 0.5, + 'swipe_threshold': 50, + 'gesture_timeout': 1.0 + }, + ConfigType.DEVICE: { + 'auto_discovery': True, + 'discovery_interval': 5.0, + 'supported_platforms': ['Windows', 'Linux', 'Darwin'] + }, + ConfigType.GESTURE: { + 'tap_max_duration': 0.3, + 'tap_max_distance': 20.0, + 'double_tap_timeout': 0.3, + 'long_press_duration': 0.5, + 'swipe_min_distance': 50.0, + 'swipe_max_duration': 0.5, + 'pinch_threshold': 20.0, + 'rotate_threshold': 10.0 + }, + ConfigType.HAPTIC: { + 'default_intensity': 0.5, + 'default_duration': 0.2, + 'max_intensity': 1.0, + 'min_duration': 0.01, + 'max_duration': 5.0, + 'update_rate': 60.0 + }, + ConfigType.MAPPING: { + 'enable_mapping': True, + 'enable_combinations': True, + 'enable_sequences': True, + 'combination_timeout': 0.5, + 'sequence_timeout': 1.0 + }, + ConfigType.USER_PREFERENCES: { + 'language': 'zh-CN', + 'theme': 'dark', + 'enable_notifications': True, + 'auto_save_config': True, + 'config_backup_count': 5 + } + } + + # 配置版本 + self.config_version = "1.0.0" + + # 配置变更监听器 + self.config_listeners = {} + + # 统计信息 + self.config_stats = { + 'configs_loaded': 0, + 'configs_saved': 0, + 'configs_changed': 0, + 'backup_created': 0, + 'load_errors': 0, + 'save_errors': 0 + } + + # 时间戳记录 + self.last_config_change = {} + self.config_load_time = {} + + print("✓ 配置管理器已创建") + + def initialize(self) -> bool: + """ + 初始化配置管理器 + + Returns: + 是否初始化成功 + """ + try: + # 创建配置目录 + self._create_config_directories() + + # 加载所有配置 + self._load_all_configs() + + # 应用默认配置(如果配置不存在) + self._apply_default_configs() + + self.initialized = True + print("✓ 配置管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 配置管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用配置管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 配置管理器未初始化") + return False + + self.enabled = True + print("✓ 配置管理器已启用") + return True + + except Exception as e: + print(f"✗ 配置管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用配置管理器""" + try: + self.enabled = False + + # 自动保存配置(如果启用) + if self.get_config(ConfigType.USER_PREFERENCES, 'auto_save_config', True): + self.save_all_configs() + + print("✓ 配置管理器已禁用") + + except Exception as e: + print(f"✗ 配置管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理配置管理器资源""" + try: + self.disable() + self.config_data.clear() + self.default_configs.clear() + self.config_listeners.clear() + self.last_config_change.clear() + self.config_load_time.clear() + self.initialized = False + print("✓ 配置管理器资源已清理") + + except Exception as e: + print(f"✗ 配置管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新配置管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 定期自动保存配置(如果启用) + # 这里可以实现定期保存逻辑 + pass + + except Exception as e: + print(f"✗ 配置管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _create_config_directories(self): + """创建配置目录""" + try: + base_path = self.config_paths['base_path'] + if not os.path.exists(base_path): + os.makedirs(base_path) + print(f"✓ 配置目录已创建: {base_path}") + except Exception as e: + print(f"✗ 配置目录创建失败: {e}") + + def _load_all_configs(self): + """加载所有配置""" + try: + for config_type in ConfigType: + self._load_config(config_type) + + print("✓ 所有配置加载完成") + except Exception as e: + print(f"✗ 配置加载失败: {e}") + + def _load_config(self, config_type: ConfigType): + """ + 加载指定类型的配置 + + Args: + config_type: 配置类型 + """ + try: + config_file = self.config_paths[config_type.value + '_config'] + + if os.path.exists(config_file): + with open(config_file, 'r', encoding='utf-8') as f: + config_data = json.load(f) + + self.config_data[config_type] = config_data + self.config_load_time[config_type.value] = time.time() + self.config_stats['configs_loaded'] += 1 + + print(f"✓ 配置已加载: {config_type.value}") + else: + print(f"⚠ 配置文件不存在: {config_file}") + + except Exception as e: + print(f"✗ 配置加载失败 {config_type.value}: {e}") + self.config_stats['load_errors'] += 1 + + def _apply_default_configs(self): + """应用默认配置""" + try: + for config_type, default_config in self.default_configs.items(): + if not self.config_data[config_type]: + self.config_data[config_type] = default_config.copy() + print(f"✓ 默认配置已应用: {config_type.value}") + + except Exception as e: + print(f"✗ 默认配置应用失败: {e}") + + def save_all_configs(self) -> bool: + """ + 保存所有配置 + + Returns: + 是否保存成功 + """ + try: + success = True + for config_type in ConfigType: + if not self._save_config(config_type): + success = False + + if success: + print("✓ 所有配置保存完成") + return success + + except Exception as e: + print(f"✗ 配置保存失败: {e}") + self.config_stats['save_errors'] += 1 + return False + + def _save_config(self, config_type: ConfigType) -> bool: + """ + 保存指定类型的配置 + + Args: + config_type: 配置类型 + + Returns: + 是否保存成功 + """ + try: + config_file = self.config_paths[config_type.value + '_config'] + + # 创建备份(如果启用) + if self.get_config(ConfigType.USER_PREFERENCES, 'auto_backup_config', False): + self._create_backup(config_file) + + # 保存配置 + with open(config_file, 'w', encoding='utf-8') as f: + json.dump(self.config_data[config_type], f, ensure_ascii=False, indent=2) + + self.config_stats['configs_saved'] += 1 + print(f"✓ 配置已保存: {config_type.value}") + return True + + except Exception as e: + print(f"✗ 配置保存失败 {config_type.value}: {e}") + self.config_stats['save_errors'] += 1 + return False + + def _create_backup(self, config_file: str): + """ + 创建配置备份 + + Args: + config_file: 配置文件路径 + """ + try: + if not os.path.exists(config_file): + return + + backup_count = self.get_config(ConfigType.USER_PREFERENCES, 'config_backup_count', 5) + + # 创建备份文件名 + base_name = os.path.splitext(config_file)[0] + backup_file = f"{base_name}.backup.{int(time.time())}.json" + + # 复制文件 + with open(config_file, 'r', encoding='utf-8') as src: + with open(backup_file, 'w', encoding='utf-8') as dst: + dst.write(src.read()) + + self.config_stats['backup_created'] += 1 + print(f"✓ 配置备份已创建: {backup_file}") + + # 清理旧备份 + self._cleanup_old_backups(config_file, backup_count) + + except Exception as e: + print(f"✗ 配置备份创建失败: {e}") + + def _cleanup_old_backups(self, config_file: str, max_backups: int): + """ + 清理旧备份 + + Args: + config_file: 配置文件路径 + max_backups: 最大备份数量 + """ + try: + base_name = os.path.splitext(config_file)[0] + backup_pattern = f"{base_name}.backup." + + # 查找备份文件 + backup_files = [] + config_dir = os.path.dirname(config_file) + if os.path.exists(config_dir): + for file in os.listdir(config_dir): + if file.startswith(backup_pattern) and file.endswith('.json'): + file_path = os.path.join(config_dir, file) + backup_files.append((file_path, os.path.getmtime(file_path))) + + # 按时间排序 + backup_files.sort(key=lambda x: x[1], reverse=True) + + # 删除多余的备份 + for file_path, _ in backup_files[max_backups:]: + try: + os.remove(file_path) + print(f"✓ 旧备份已删除: {file_path}") + except Exception as e: + print(f"✗ 旧备份删除失败: {file_path} - {e}") + + except Exception as e: + print(f"✗ 旧备份清理失败: {e}") + + def get_config(self, config_type: ConfigType, key: str, default_value: Any = None) -> Any: + """ + 获取配置值 + + Args: + config_type: 配置类型 + key: 配置键 + default_value: 默认值 + + Returns: + 配置值 + """ + try: + config_section = self.config_data.get(config_type, {}) + return config_section.get(key, default_value) + except Exception as e: + print(f"✗ 配置获取失败: {config_type.value}.{key} - {e}") + return default_value + + def set_config(self, config_type: ConfigType, key: str, value: Any) -> bool: + """ + 设置配置值 + + Args: + config_type: 配置类型 + key: 配置键 + value: 配置值 + + Returns: + 是否设置成功 + """ + try: + if config_type not in self.config_data: + self.config_data[config_type] = {} + + old_value = self.config_data[config_type].get(key) + self.config_data[config_type][key] = value + self.last_config_change[config_type.value] = time.time() + self.config_stats['configs_changed'] += 1 + + # 触发配置变更监听器 + self._trigger_config_listener(config_type, key, old_value, value) + + print(f"✓ 配置已设置: {config_type.value}.{key} = {value}") + return True + + except Exception as e: + print(f"✗ 配置设置失败: {config_type.value}.{key} - {e}") + return False + + def get_config_section(self, config_type: ConfigType) -> Dict[str, Any]: + """ + 获取配置节 + + Args: + config_type: 配置类型 + + Returns: + 配置节字典 + """ + return self.config_data.get(config_type, {}).copy() + + def set_config_section(self, config_type: ConfigType, config_section: Dict[str, Any]) -> bool: + """ + 设置配置节 + + Args: + config_type: 配置类型 + config_section: 配置节字典 + + Returns: + 是否设置成功 + """ + try: + self.config_data[config_type] = config_section.copy() + self.last_config_change[config_type.value] = time.time() + self.config_stats['configs_changed'] += 1 + + print(f"✓ 配置节已设置: {config_type.value}") + return True + + except Exception as e: + print(f"✗ 配置节设置失败: {config_type.value} - {e}") + return False + + def reset_config(self, config_type: ConfigType) -> bool: + """ + 重置配置为默认值 + + Args: + config_type: 配置类型 + + Returns: + 是否重置成功 + """ + try: + if config_type in self.default_configs: + self.config_data[config_type] = self.default_configs[config_type].copy() + self.last_config_change[config_type.value] = time.time() + self.config_stats['configs_changed'] += 1 + + print(f"✓ 配置已重置: {config_type.value}") + return True + else: + print(f"✗ 无默认配置: {config_type.value}") + return False + + except Exception as e: + print(f"✗ 配置重置失败: {config_type.value} - {e}") + return False + + def register_config_listener(self, config_type: ConfigType, key: str, listener: callable): + """ + 注册配置变更监听器 + + Args: + config_type: 配置类型 + key: 配置键 + listener: 监听器回调函数 + """ + try: + listener_key = f"{config_type.value}.{key}" + if listener_key not in self.config_listeners: + self.config_listeners[listener_key] = [] + self.config_listeners[listener_key].append(listener) + print(f"✓ 配置监听器已注册: {listener_key}") + except Exception as e: + print(f"✗ 配置监听器注册失败: {e}") + + def unregister_config_listener(self, config_type: ConfigType, key: str, listener: callable): + """ + 注销配置变更监听器 + + Args: + config_type: 配置类型 + key: 配置键 + listener: 监听器回调函数 + """ + try: + listener_key = f"{config_type.value}.{key}" + if listener_key in self.config_listeners: + if listener in self.config_listeners[listener_key]: + self.config_listeners[listener_key].remove(listener) + print(f"✓ 配置监听器已注销: {listener_key}") + except Exception as e: + print(f"✗ 配置监听器注销失败: {e}") + + def _trigger_config_listener(self, config_type: ConfigType, key: str, old_value: Any, new_value: Any): + """ + 触发配置变更监听器 + + Args: + config_type: 配置类型 + key: 配置键 + old_value: 旧值 + new_value: 新值 + """ + try: + listener_key = f"{config_type.value}.{key}" + if listener_key in self.config_listeners: + for listener in self.config_listeners[listener_key]: + try: + listener(key, old_value, new_value) + except Exception as e: + print(f"✗ 配置监听器执行失败: {e}") + except Exception as e: + print(f"✗ 配置监听器触发失败: {e}") + + def export_config(self, config_type: ConfigType, file_path: str) -> bool: + """ + 导出配置 + + Args: + config_type: 配置类型 + file_path: 导出文件路径 + + Returns: + 是否导出成功 + """ + try: + config_data = self.config_data.get(config_type, {}) + + # 添加元数据 + export_data = { + 'config_type': config_type.value, + 'version': self.config_version, + 'export_time': time.time(), + 'data': config_data + } + + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(export_data, f, ensure_ascii=False, indent=2) + + print(f"✓ 配置已导出: {file_path}") + return True + + except Exception as e: + print(f"✗ 配置导出失败: {e}") + return False + + def import_config(self, file_path: str) -> bool: + """ + 导入配置 + + Args: + file_path: 导入文件路径 + + Returns: + 是否导入成功 + """ + try: + with open(file_path, 'r', encoding='utf-8') as f: + import_data = json.load(f) + + config_type = ConfigType(import_data['config_type']) + config_data = import_data['data'] + + self.config_data[config_type] = config_data + self.last_config_change[config_type.value] = time.time() + self.config_stats['configs_changed'] += 1 + + print(f"✓ 配置已导入: {file_path}") + return True + + except Exception as e: + print(f"✗ 配置导入失败: {e}") + return False + + def get_config_stats(self) -> Dict[str, Any]: + """ + 获取配置统计信息 + + Returns: + 配置统计字典 + """ + return self.config_stats.copy() + + def reset_config_stats(self): + """重置配置统计信息""" + try: + self.config_stats = { + 'configs_loaded': sum(1 for ct in ConfigType if self.config_data[ct]), + 'configs_saved': 0, + 'configs_changed': 0, + 'backup_created': 0, + 'load_errors': 0, + 'save_errors': 0 + } + print("✓ 配置统计信息已重置") + except Exception as e: + print(f"✗ 配置统计信息重置失败: {e}") + + def get_config_load_time(self, config_type: ConfigType) -> float: + """ + 获取配置加载时间 + + Args: + config_type: 配置类型 + + Returns: + 加载时间戳 + """ + return self.config_load_time.get(config_type.value, 0.0) + + def get_last_config_change(self, config_type: ConfigType) -> float: + """ + 获取最后配置变更时间 + + Args: + config_type: 配置类型 + + Returns: + 变更时间戳 + """ + return self.last_config_change.get(config_type.value, 0.0) + + def validate_config(self, config_type: ConfigType) -> bool: + """ + 验证配置有效性 + + Args: + config_type: 配置类型 + + Returns: + 是否有效 + """ + try: + # 这里可以实现配置验证逻辑 + # 例如:检查必需字段、值范围等 + config_data = self.config_data.get(config_type, {}) + + # 基本验证:确保是字典类型 + if not isinstance(config_data, dict): + print(f"✗ 配置验证失败: {config_type.value} 不是字典类型") + return False + + print(f"✓ 配置验证通过: {config_type.value}") + return True + + except Exception as e: + print(f"✗ 配置验证失败: {config_type.value} - {e}") + return False + + def migrate_config(self, old_version: str, new_version: str) -> bool: + """ + 迁移配置版本 + + Args: + old_version: 旧版本 + new_version: 新版本 + + Returns: + 是否迁移成功 + """ + try: + # 这里可以实现配置版本迁移逻辑 + # 根据版本差异调整配置结构 + print(f"✓ 配置版本迁移: {old_version} -> {new_version}") + return True + + except Exception as e: + print(f"✗ 配置版本迁移失败: {e}") + return False + diff --git a/plugins/user/multiplatform_input/core/advanced_input_processor.py b/plugins/user/multiplatform_input/core/advanced_input_processor.py index a65f3bf7..c408e352 100644 --- a/plugins/user/multiplatform_input/core/advanced_input_processor.py +++ b/plugins/user/multiplatform_input/core/advanced_input_processor.py @@ -1,892 +1,892 @@ -""" -高级输入处理器模块 -用于处理复杂的输入组合和上下文感知输入 -""" - -import time -from typing import Dict, Any, List, Optional, Callable, Set -import threading -from enum import Enum - -class InputContext(Enum): - """输入上下文枚举""" - GLOBAL = "global" - MENU = "menu" - GAMEPLAY = "gameplay" - CUTSCENE = "cutscene" - PAUSE = "pause" - INVENTORY = "inventory" - DIALOG = "dialog" - CUSTOM = "custom" - -class AdvancedInputProcessor: - """ - 高级输入处理器 - 用于处理复杂的输入组合和上下文感知输入 - """ - - def __init__(self, plugin): - """ - 初始化高级输入处理器 - - Args: - plugin: 多平台输入支持插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 上下文管理 - self.input_contexts = { - InputContext.GLOBAL: {'priority': 0, 'active': True}, - InputContext.MENU: {'priority': 10, 'active': False}, - InputContext.GAMEPLAY: {'priority': 5, 'active': False}, - InputContext.CUTSCENE: {'priority': 15, 'active': False}, - InputContext.PAUSE: {'priority': 20, 'active': False}, - InputContext.INVENTORY: {'priority': 8, 'active': False}, - InputContext.DIALOG: {'priority': 12, 'active': False} - } - - self.current_context = InputContext.GLOBAL - self.context_stack = [InputContext.GLOBAL] - - # 复杂输入模式 - self.input_patterns = {} - self.pattern_counter = 0 - - # 输入序列 - self.input_sequences = {} - self.sequence_counter = 0 - - # 输入修饰符 - self.active_modifiers = set() - self.modifier_config = { - 'shift': ['shift', 'left_shift', 'right_shift'], - 'ctrl': ['ctrl', 'left_ctrl', 'right_ctrl', 'control'], - 'alt': ['alt', 'left_alt', 'right_alt'], - 'meta': ['meta', 'windows', 'cmd', 'super'] - } - - # 时间相关输入 - self.timed_inputs = {} - self.chord_inputs = {} - self.chord_counter = 0 - - # 回调函数 - self.advanced_callbacks = { - 'context_changed': [], - 'pattern_matched': [], - 'sequence_completed': [], - 'modifier_changed': [], - 'timed_input': [] - } - - # 统计信息 - self.advanced_stats = { - 'contexts_switched': 0, - 'patterns_matched': 0, - 'sequences_completed': 0, - 'modifiers_activated': 0, - 'timed_inputs_processed': 0 - } - - # 配置 - self.advanced_config = { - 'pattern_timeout': 2.0, # 模式匹配超时时间(秒) - 'sequence_timeout': 5.0, # 序列超时时间(秒) - 'chord_timeout': 0.5, # 和弦超时时间(秒) - 'double_press_time': 0.3, # 双击时间阈值(秒) - 'long_press_time': 0.5 # 长按时间阈值(秒) - } - - # 时间戳记录 - self.last_input_time = {} - self.pattern_start_time = {} - self.sequence_start_time = {} - - print("✓ 高级输入处理器已创建") - - def initialize(self) -> bool: - """ - 初始化高级输入处理器 - - Returns: - 是否初始化成功 - """ - try: - # 创建默认输入模式 - self._create_default_patterns() - - # 创建默认输入序列 - self._create_default_sequences() - - self.initialized = True - print("✓ 高级输入处理器初始化完成") - return True - - except Exception as e: - print(f"✗ 高级输入处理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用高级输入处理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 高级输入处理器未初始化") - return False - - self.enabled = True - print("✓ 高级输入处理器已启用") - return True - - except Exception as e: - print(f"✗ 高级输入处理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用高级输入处理器""" - try: - self.enabled = False - print("✓ 高级输入处理器已禁用") - - except Exception as e: - print(f"✗ 高级输入处理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理高级输入处理器资源""" - try: - self.disable() - self.input_patterns.clear() - self.input_sequences.clear() - self.timed_inputs.clear() - self.chord_inputs.clear() - self.advanced_callbacks.clear() - self.initialized = False - print("✓ 高级输入处理器资源已清理") - - except Exception as e: - print(f"✗ 高级输入处理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新高级输入处理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 检查超时的模式和序列 - self._check_pattern_timeouts() - self._check_sequence_timeouts() - self._check_chord_timeouts() - - except Exception as e: - print(f"✗ 高级输入处理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _create_default_patterns(self): - """创建默认输入模式""" - try: - # Konami Code 示例 - self.create_input_pattern( - name="konami_code", - inputs=['up', 'up', 'down', 'down', 'left', 'right', 'left', 'right', 'b', 'a'], - context=InputContext.GLOBAL, - description="Konami Code (↑↑↓↓←→←→BA)" - ) - - # 快速三连击示例 - self.create_input_pattern( - name="quick_triple", - inputs=['action', 'action', 'action'], - context=InputContext.GAMEPLAY, - description="快速三连击", - time_limit=0.5 - ) - - print("✓ 默认输入模式创建完成") - except Exception as e: - print(f"✗ 默认输入模式创建失败: {e}") - - def _create_default_sequences(self): - """创建默认输入序列""" - try: - # 游戏秘籍示例 - self.create_input_sequence( - name="unlimited_ammo", - sequence=['o', 'n', 'e', 'u', 'p'], - context=InputContext.GAMEPLAY, - description="无限弹药秘籍" - ) - - print("✓ 默认输入序列创建完成") - except Exception as e: - print(f"✗ 默认输入序列创建失败: {e}") - - def _check_pattern_timeouts(self): - """检查模式超时""" - try: - current_time = time.time() - timed_out_patterns = [] - - for pattern_id, pattern_data in self.pattern_start_time.items(): - if current_time - pattern_data > self.advanced_config['pattern_timeout']: - timed_out_patterns.append(pattern_id) - - for pattern_id in timed_out_patterns: - if pattern_id in self.pattern_start_time: - del self.pattern_start_time[pattern_id] - - except Exception as e: - print(f"✗ 模式超时检查失败: {e}") - - def _check_sequence_timeouts(self): - """检查序列超时""" - try: - current_time = time.time() - timed_out_sequences = [] - - for sequence_id, sequence_data in self.sequence_start_time.items(): - if current_time - sequence_data > self.advanced_config['sequence_timeout']: - timed_out_sequences.append(sequence_id) - - for sequence_id in timed_out_sequences: - if sequence_id in self.sequence_start_time: - del self.sequence_start_time[sequence_id] - - except Exception as e: - print(f"✗ 序列超时检查失败: {e}") - - def _check_chord_timeouts(self): - """检查和弦超时""" - try: - current_time = time.time() - timed_out_chords = [] - - for chord_id, chord_data in self.chord_inputs.items(): - if current_time - chord_data['start_time'] > self.advanced_config['chord_timeout']: - timed_out_chords.append(chord_id) - - for chord_id in timed_out_chords: - if chord_id in self.chord_inputs: - del self.chord_inputs[chord_id] - - except Exception as e: - print(f"✗ 和弦超时检查失败: {e}") - - def set_input_context(self, context: InputContext) -> bool: - """ - 设置输入上下文 - - Args: - context: 输入上下文 - - Returns: - 是否设置成功 - """ - try: - if context not in self.input_contexts: - print(f"✗ 无效的输入上下文: {context.value}") - return False - - old_context = self.current_context - self.current_context = context - - # 更新上下文激活状态 - for ctx in self.input_contexts: - self.input_contexts[ctx]['active'] = (ctx == context) - - # 更新上下文堆栈 - if context in self.context_stack: - self.context_stack.remove(context) - self.context_stack.append(context) - - # 触发上下文变更回调 - self._trigger_advanced_callback('context_changed', { - 'old_context': old_context.value, - 'new_context': context.value - }) - - self.advanced_stats['contexts_switched'] += 1 - print(f"✓ 输入上下文已设置: {context.value}") - return True - - except Exception as e: - print(f"✗ 输入上下文设置失败: {e}") - return False - - def push_input_context(self, context: InputContext) -> bool: - """ - 推入输入上下文(保存当前上下文) - - Args: - context: 输入上下文 - - Returns: - 是否推入成功 - """ - try: - if context not in self.input_contexts: - print(f"✗ 无效的输入上下文: {context.value}") - return False - - # 保存当前上下文 - self.context_stack.append(context) - self.current_context = context - - # 更新上下文激活状态 - for ctx in self.input_contexts: - self.input_contexts[ctx]['active'] = (ctx == context) - - # 触发上下文变更回调 - self._trigger_advanced_callback('context_changed', { - 'old_context': self.context_stack[-2].value if len(self.context_stack) > 1 else None, - 'new_context': context.value - }) - - self.advanced_stats['contexts_switched'] += 1 - print(f"✓ 输入上下文已推入: {context.value}") - return True - - except Exception as e: - print(f"✗ 输入上下文推入失败: {e}") - return False - - def pop_input_context(self) -> bool: - """ - 弹出输入上下文(恢复上一个上下文) - - Returns: - 是否弹出成功 - """ - try: - if len(self.context_stack) <= 1: - print("✗ 无法弹出最后一个上下文") - return False - - # 移除当前上下文 - self.context_stack.pop() - - # 恢复上一个上下文 - previous_context = self.context_stack[-1] - self.current_context = previous_context - - # 更新上下文激活状态 - for ctx in self.input_contexts: - self.input_contexts[ctx]['active'] = (ctx == previous_context) - - # 触发上下文变更回调 - self._trigger_advanced_callback('context_changed', { - 'old_context': self.context_stack[-2].value if len(self.context_stack) > 1 else None, - 'new_context': previous_context.value - }) - - self.advanced_stats['contexts_switched'] += 1 - print(f"✓ 输入上下文已弹出,恢复到: {previous_context.value}") - return True - - except Exception as e: - print(f"✗ 输入上下文弹出失败: {e}") - return False - - def create_input_pattern(self, name: str, inputs: List[str], context: InputContext, - description: str = "", time_limit: float = None) -> int: - """ - 创建输入模式 - - Args: - name: 模式名称 - inputs: 输入序列 - context: 上下文 - description: 描述 - time_limit: 时间限制(秒) - - Returns: - 模式ID - """ - try: - pattern_id = self.pattern_counter - self.pattern_counter += 1 - - pattern = { - 'id': pattern_id, - 'name': name, - 'inputs': inputs, - 'context': context, - 'description': description, - 'time_limit': time_limit or self.advanced_config['pattern_timeout'], - 'created_time': time.time(), - 'match_count': 0 - } - - self.input_patterns[pattern_id] = pattern - - print(f"✓ 输入模式已创建: {name} (ID: {pattern_id})") - return pattern_id - - except Exception as e: - print(f"✗ 输入模式创建失败: {e}") - return -1 - - def remove_input_pattern(self, pattern_id: int) -> bool: - """ - 移除输入模式 - - Args: - pattern_id: 模式ID - - Returns: - 是否移除成功 - """ - try: - if pattern_id in self.input_patterns: - pattern_name = self.input_patterns[pattern_id]['name'] - del self.input_patterns[pattern_id] - print(f"✓ 输入模式已移除: {pattern_name} (ID: {pattern_id})") - return True - else: - print(f"✗ 输入模式不存在: ID {pattern_id}") - return False - - except Exception as e: - print(f"✗ 输入模式移除失败: {e}") - return False - - def process_input_pattern(self, input_name: str) -> bool: - """ - 处理输入模式匹配 - - Args: - input_name: 输入名称 - - Returns: - 是否匹配成功 - """ - try: - current_time = time.time() - matched_patterns = [] - - for pattern_id, pattern in self.input_patterns.items(): - # 检查上下文是否匹配 - if pattern['context'] != self.current_context and pattern['context'] != InputContext.GLOBAL: - continue - - # 初始化模式跟踪 - if pattern_id not in self.pattern_start_time: - self.pattern_start_time[pattern_id] = current_time - - # 检查时间限制 - if current_time - self.pattern_start_time[pattern_id] > pattern['time_limit']: - continue - - # 这里应该实现实际的模式匹配逻辑 - # 简化处理:只检查最后一个输入 - if pattern['inputs'] and pattern['inputs'][-1] == input_name: - matched_patterns.append(pattern_id) - - # 处理匹配的模式 - for pattern_id in matched_patterns: - pattern = self.input_patterns[pattern_id] - pattern['match_count'] += 1 - self.advanced_stats['patterns_matched'] += 1 - - # 触发模式匹配回调 - self._trigger_advanced_callback('pattern_matched', { - 'pattern_id': pattern_id, - 'pattern_name': pattern['name'], - 'input': input_name - }) - - # 重置模式时间 - if pattern_id in self.pattern_start_time: - del self.pattern_start_time[pattern_id] - - print(f"✓ 输入模式匹配: {pattern['name']}") - - return len(matched_patterns) > 0 - - except Exception as e: - print(f"✗ 输入模式处理失败: {e}") - return False - - def create_input_sequence(self, name: str, sequence: List[str], context: InputContext, - description: str = "") -> int: - """ - 创建输入序列 - - Args: - name: 序列名称 - sequence: 输入序列 - context: 上下文 - description: 描述 - - Returns: - 序列ID - """ - try: - sequence_id = self.sequence_counter - self.sequence_counter += 1 - - seq = { - 'id': sequence_id, - 'name': name, - 'sequence': sequence, - 'context': context, - 'description': description, - 'created_time': time.time(), - 'current_position': 0, - 'completion_count': 0 - } - - self.input_sequences[sequence_id] = seq - - print(f"✓ 输入序列已创建: {name} (ID: {sequence_id})") - return sequence_id - - except Exception as e: - print(f"✗ 输入序列创建失败: {e}") - return -1 - - def remove_input_sequence(self, sequence_id: int) -> bool: - """ - 移除输入序列 - - Args: - sequence_id: 序列ID - - Returns: - 是否移除成功 - """ - try: - if sequence_id in self.input_sequences: - sequence_name = self.input_sequences[sequence_id]['name'] - del self.input_sequences[sequence_id] - print(f"✓ 输入序列已移除: {sequence_name} (ID: {sequence_id})") - return True - else: - print(f"✗ 输入序列不存在: ID {sequence_id}") - return False - - except Exception as e: - print(f"✗ 输入序列移除失败: {e}") - return False - - def process_input_sequence(self, input_name: str) -> bool: - """ - 处理输入序列 - - Args: - input_name: 输入名称 - - Returns: - 是否完成序列 - """ - try: - current_time = time.time() - completed_sequences = [] - - for sequence_id, sequence in self.input_sequences.items(): - # 检查上下文是否匹配 - if sequence['context'] != self.current_context and sequence['context'] != InputContext.GLOBAL: - continue - - # 初始化序列时间 - if sequence_id not in self.sequence_start_time: - self.sequence_start_time[sequence_id] = current_time - sequence['current_position'] = 0 - - # 检查时间限制 - if current_time - self.sequence_start_time[sequence_id] > self.advanced_config['sequence_timeout']: - sequence['current_position'] = 0 - self.sequence_start_time[sequence_id] = current_time - - # 检查输入匹配 - if sequence['current_position'] < len(sequence['sequence']): - expected_input = sequence['sequence'][sequence['current_position']] - if input_name == expected_input: - sequence['current_position'] += 1 - - # 检查序列是否完成 - if sequence['current_position'] >= len(sequence['sequence']): - completed_sequences.append(sequence_id) - - # 处理完成的序列 - for sequence_id in completed_sequences: - sequence = self.input_sequences[sequence_id] - sequence['completion_count'] += 1 - sequence['current_position'] = 0 - self.advanced_stats['sequences_completed'] += 1 - - # 触发序列完成回调 - self._trigger_advanced_callback('sequence_completed', { - 'sequence_id': sequence_id, - 'sequence_name': sequence['name'] - }) - - # 重置序列时间 - if sequence_id in self.sequence_start_time: - del self.sequence_start_time[sequence_id] - - print(f"✓ 输入序列完成: {sequence['name']}") - - return len(completed_sequences) > 0 - - except Exception as e: - print(f"✗ 输入序列处理失败: {e}") - return False - - def update_modifiers(self, modifier: str, pressed: bool): - """ - 更新修饰符状态 - - Args: - modifier: 修饰符名称 - pressed: 是否按下 - """ - try: - if pressed: - self.active_modifiers.add(modifier) - self.advanced_stats['modifiers_activated'] += 1 - - # 触发修饰符变更回调 - self._trigger_advanced_callback('modifier_changed', { - 'modifier': modifier, - 'state': 'pressed' - }) - else: - if modifier in self.active_modifiers: - self.active_modifiers.remove(modifier) - - # 触发修饰符变更回调 - self._trigger_advanced_callback('modifier_changed', { - 'modifier': modifier, - 'state': 'released' - }) - - except Exception as e: - print(f"✗ 修饰符更新失败: {e}") - - def is_modifier_active(self, modifier: str) -> bool: - """ - 检查修饰符是否激活 - - Args: - modifier: 修饰符名称 - - Returns: - 是否激活 - """ - return modifier in self.active_modifiers - - def get_active_modifiers(self) -> Set[str]: - """ - 获取激活的修饰符列表 - - Returns: - 激活的修饰符集合 - """ - return self.active_modifiers.copy() - - def process_timed_input(self, input_name: str, duration: float): - """ - 处理时间相关输入 - - Args: - input_name: 输入名称 - duration: 持续时间(秒) - """ - try: - # 检查是否为双击 - if duration < self.advanced_config['double_press_time']: - last_time = self.last_input_time.get(input_name, 0) - current_time = time.time() - if current_time - last_time < self.advanced_config['double_press_time']: - # 双击事件 - self._trigger_advanced_callback('timed_input', { - 'input': input_name, - 'type': 'double_click', - 'duration': duration - }) - self.advanced_stats['timed_inputs_processed'] += 1 - - # 检查是否为长按 - elif duration >= self.advanced_config['long_press_time']: - # 长按事件 - self._trigger_advanced_callback('timed_input', { - 'input': input_name, - 'type': 'long_press', - 'duration': duration - }) - self.advanced_stats['timed_inputs_processed'] += 1 - - # 更新最后输入时间 - self.last_input_time[input_name] = time.time() - - except Exception as e: - print(f"✗ 时间相关输入处理失败: {e}") - - def _trigger_advanced_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发高级回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.advanced_callbacks: - for callback in self.advanced_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 高级回调执行失败: {e}") - except Exception as e: - print(f"✗ 高级回调触发失败: {e}") - - def register_advanced_callback(self, callback_type: str, callback: Callable): - """ - 注册高级回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.advanced_callbacks: - self.advanced_callbacks[callback_type].append(callback) - print(f"✓ 高级回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 高级回调注册失败: {e}") - - def unregister_advanced_callback(self, callback_type: str, callback: Callable): - """ - 注销高级回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.advanced_callbacks: - if callback in self.advanced_callbacks[callback_type]: - self.advanced_callbacks[callback_type].remove(callback) - print(f"✓ 高级回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 高级回调注销失败: {e}") - - def get_current_context(self) -> InputContext: - """ - 获取当前输入上下文 - - Returns: - 当前输入上下文 - """ - return self.current_context - - def get_context_stack(self) -> List[InputContext]: - """ - 获取上下文堆栈 - - Returns: - 上下文堆栈列表 - """ - return self.context_stack.copy() - - def get_input_patterns(self) -> Dict[int, Dict[str, Any]]: - """ - 获取所有输入模式 - - Returns: - 输入模式字典 - """ - return self.input_patterns.copy() - - def get_input_sequences(self) -> Dict[int, Dict[str, Any]]: - """ - 获取所有输入序列 - - Returns: - 输入序列字典 - """ - return self.input_sequences.copy() - - def set_advanced_config(self, config: Dict[str, Any]) -> bool: - """ - 设置高级配置 - - Args: - config: 高级配置字典 - - Returns: - 是否设置成功 - """ - try: - self.advanced_config.update(config) - print(f"✓ 高级配置已更新: {self.advanced_config}") - return True - except Exception as e: - print(f"✗ 高级配置设置失败: {e}") - return False - - def get_advanced_config(self) -> Dict[str, Any]: - """ - 获取高级配置 - - Returns: - 高级配置字典 - """ - return self.advanced_config.copy() - - def get_advanced_stats(self) -> Dict[str, int]: - """ - 获取高级统计信息 - - Returns: - 高级统计字典 - """ - return self.advanced_stats.copy() - - def reset_advanced_stats(self): - """重置高级统计信息""" - try: - self.advanced_stats = { - 'contexts_switched': 0, - 'patterns_matched': 0, - 'sequences_completed': 0, - 'modifiers_activated': 0, - 'timed_inputs_processed': 0 - } - print("✓ 高级统计信息已重置") - except Exception as e: - print(f"✗ 高级统计信息重置失败: {e}") - - def get_context_info(self, context: InputContext) -> Optional[Dict[str, Any]]: - """ - 获取上下文信息 - - Args: - context: 输入上下文 - - Returns: - 上下文信息字典或None - """ +""" +高级输入处理器模块 +用于处理复杂的输入组合和上下文感知输入 +""" + +import time +from typing import Dict, Any, List, Optional, Callable, Set +import threading +from enum import Enum + +class InputContext(Enum): + """输入上下文枚举""" + GLOBAL = "global" + MENU = "menu" + GAMEPLAY = "gameplay" + CUTSCENE = "cutscene" + PAUSE = "pause" + INVENTORY = "inventory" + DIALOG = "dialog" + CUSTOM = "custom" + +class AdvancedInputProcessor: + """ + 高级输入处理器 + 用于处理复杂的输入组合和上下文感知输入 + """ + + def __init__(self, plugin): + """ + 初始化高级输入处理器 + + Args: + plugin: 多平台输入支持插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 上下文管理 + self.input_contexts = { + InputContext.GLOBAL: {'priority': 0, 'active': True}, + InputContext.MENU: {'priority': 10, 'active': False}, + InputContext.GAMEPLAY: {'priority': 5, 'active': False}, + InputContext.CUTSCENE: {'priority': 15, 'active': False}, + InputContext.PAUSE: {'priority': 20, 'active': False}, + InputContext.INVENTORY: {'priority': 8, 'active': False}, + InputContext.DIALOG: {'priority': 12, 'active': False} + } + + self.current_context = InputContext.GLOBAL + self.context_stack = [InputContext.GLOBAL] + + # 复杂输入模式 + self.input_patterns = {} + self.pattern_counter = 0 + + # 输入序列 + self.input_sequences = {} + self.sequence_counter = 0 + + # 输入修饰符 + self.active_modifiers = set() + self.modifier_config = { + 'shift': ['shift', 'left_shift', 'right_shift'], + 'ctrl': ['ctrl', 'left_ctrl', 'right_ctrl', 'control'], + 'alt': ['alt', 'left_alt', 'right_alt'], + 'meta': ['meta', 'windows', 'cmd', 'super'] + } + + # 时间相关输入 + self.timed_inputs = {} + self.chord_inputs = {} + self.chord_counter = 0 + + # 回调函数 + self.advanced_callbacks = { + 'context_changed': [], + 'pattern_matched': [], + 'sequence_completed': [], + 'modifier_changed': [], + 'timed_input': [] + } + + # 统计信息 + self.advanced_stats = { + 'contexts_switched': 0, + 'patterns_matched': 0, + 'sequences_completed': 0, + 'modifiers_activated': 0, + 'timed_inputs_processed': 0 + } + + # 配置 + self.advanced_config = { + 'pattern_timeout': 2.0, # 模式匹配超时时间(秒) + 'sequence_timeout': 5.0, # 序列超时时间(秒) + 'chord_timeout': 0.5, # 和弦超时时间(秒) + 'double_press_time': 0.3, # 双击时间阈值(秒) + 'long_press_time': 0.5 # 长按时间阈值(秒) + } + + # 时间戳记录 + self.last_input_time = {} + self.pattern_start_time = {} + self.sequence_start_time = {} + + print("✓ 高级输入处理器已创建") + + def initialize(self) -> bool: + """ + 初始化高级输入处理器 + + Returns: + 是否初始化成功 + """ + try: + # 创建默认输入模式 + self._create_default_patterns() + + # 创建默认输入序列 + self._create_default_sequences() + + self.initialized = True + print("✓ 高级输入处理器初始化完成") + return True + + except Exception as e: + print(f"✗ 高级输入处理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用高级输入处理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 高级输入处理器未初始化") + return False + + self.enabled = True + print("✓ 高级输入处理器已启用") + return True + + except Exception as e: + print(f"✗ 高级输入处理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用高级输入处理器""" + try: + self.enabled = False + print("✓ 高级输入处理器已禁用") + + except Exception as e: + print(f"✗ 高级输入处理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理高级输入处理器资源""" + try: + self.disable() + self.input_patterns.clear() + self.input_sequences.clear() + self.timed_inputs.clear() + self.chord_inputs.clear() + self.advanced_callbacks.clear() + self.initialized = False + print("✓ 高级输入处理器资源已清理") + + except Exception as e: + print(f"✗ 高级输入处理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新高级输入处理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 检查超时的模式和序列 + self._check_pattern_timeouts() + self._check_sequence_timeouts() + self._check_chord_timeouts() + + except Exception as e: + print(f"✗ 高级输入处理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _create_default_patterns(self): + """创建默认输入模式""" + try: + # Konami Code 示例 + self.create_input_pattern( + name="konami_code", + inputs=['up', 'up', 'down', 'down', 'left', 'right', 'left', 'right', 'b', 'a'], + context=InputContext.GLOBAL, + description="Konami Code (↑↑↓↓←→←→BA)" + ) + + # 快速三连击示例 + self.create_input_pattern( + name="quick_triple", + inputs=['action', 'action', 'action'], + context=InputContext.GAMEPLAY, + description="快速三连击", + time_limit=0.5 + ) + + print("✓ 默认输入模式创建完成") + except Exception as e: + print(f"✗ 默认输入模式创建失败: {e}") + + def _create_default_sequences(self): + """创建默认输入序列""" + try: + # 游戏秘籍示例 + self.create_input_sequence( + name="unlimited_ammo", + sequence=['o', 'n', 'e', 'u', 'p'], + context=InputContext.GAMEPLAY, + description="无限弹药秘籍" + ) + + print("✓ 默认输入序列创建完成") + except Exception as e: + print(f"✗ 默认输入序列创建失败: {e}") + + def _check_pattern_timeouts(self): + """检查模式超时""" + try: + current_time = time.time() + timed_out_patterns = [] + + for pattern_id, pattern_data in self.pattern_start_time.items(): + if current_time - pattern_data > self.advanced_config['pattern_timeout']: + timed_out_patterns.append(pattern_id) + + for pattern_id in timed_out_patterns: + if pattern_id in self.pattern_start_time: + del self.pattern_start_time[pattern_id] + + except Exception as e: + print(f"✗ 模式超时检查失败: {e}") + + def _check_sequence_timeouts(self): + """检查序列超时""" + try: + current_time = time.time() + timed_out_sequences = [] + + for sequence_id, sequence_data in self.sequence_start_time.items(): + if current_time - sequence_data > self.advanced_config['sequence_timeout']: + timed_out_sequences.append(sequence_id) + + for sequence_id in timed_out_sequences: + if sequence_id in self.sequence_start_time: + del self.sequence_start_time[sequence_id] + + except Exception as e: + print(f"✗ 序列超时检查失败: {e}") + + def _check_chord_timeouts(self): + """检查和弦超时""" + try: + current_time = time.time() + timed_out_chords = [] + + for chord_id, chord_data in self.chord_inputs.items(): + if current_time - chord_data['start_time'] > self.advanced_config['chord_timeout']: + timed_out_chords.append(chord_id) + + for chord_id in timed_out_chords: + if chord_id in self.chord_inputs: + del self.chord_inputs[chord_id] + + except Exception as e: + print(f"✗ 和弦超时检查失败: {e}") + + def set_input_context(self, context: InputContext) -> bool: + """ + 设置输入上下文 + + Args: + context: 输入上下文 + + Returns: + 是否设置成功 + """ + try: + if context not in self.input_contexts: + print(f"✗ 无效的输入上下文: {context.value}") + return False + + old_context = self.current_context + self.current_context = context + + # 更新上下文激活状态 + for ctx in self.input_contexts: + self.input_contexts[ctx]['active'] = (ctx == context) + + # 更新上下文堆栈 + if context in self.context_stack: + self.context_stack.remove(context) + self.context_stack.append(context) + + # 触发上下文变更回调 + self._trigger_advanced_callback('context_changed', { + 'old_context': old_context.value, + 'new_context': context.value + }) + + self.advanced_stats['contexts_switched'] += 1 + print(f"✓ 输入上下文已设置: {context.value}") + return True + + except Exception as e: + print(f"✗ 输入上下文设置失败: {e}") + return False + + def push_input_context(self, context: InputContext) -> bool: + """ + 推入输入上下文(保存当前上下文) + + Args: + context: 输入上下文 + + Returns: + 是否推入成功 + """ + try: + if context not in self.input_contexts: + print(f"✗ 无效的输入上下文: {context.value}") + return False + + # 保存当前上下文 + self.context_stack.append(context) + self.current_context = context + + # 更新上下文激活状态 + for ctx in self.input_contexts: + self.input_contexts[ctx]['active'] = (ctx == context) + + # 触发上下文变更回调 + self._trigger_advanced_callback('context_changed', { + 'old_context': self.context_stack[-2].value if len(self.context_stack) > 1 else None, + 'new_context': context.value + }) + + self.advanced_stats['contexts_switched'] += 1 + print(f"✓ 输入上下文已推入: {context.value}") + return True + + except Exception as e: + print(f"✗ 输入上下文推入失败: {e}") + return False + + def pop_input_context(self) -> bool: + """ + 弹出输入上下文(恢复上一个上下文) + + Returns: + 是否弹出成功 + """ + try: + if len(self.context_stack) <= 1: + print("✗ 无法弹出最后一个上下文") + return False + + # 移除当前上下文 + self.context_stack.pop() + + # 恢复上一个上下文 + previous_context = self.context_stack[-1] + self.current_context = previous_context + + # 更新上下文激活状态 + for ctx in self.input_contexts: + self.input_contexts[ctx]['active'] = (ctx == previous_context) + + # 触发上下文变更回调 + self._trigger_advanced_callback('context_changed', { + 'old_context': self.context_stack[-2].value if len(self.context_stack) > 1 else None, + 'new_context': previous_context.value + }) + + self.advanced_stats['contexts_switched'] += 1 + print(f"✓ 输入上下文已弹出,恢复到: {previous_context.value}") + return True + + except Exception as e: + print(f"✗ 输入上下文弹出失败: {e}") + return False + + def create_input_pattern(self, name: str, inputs: List[str], context: InputContext, + description: str = "", time_limit: float = None) -> int: + """ + 创建输入模式 + + Args: + name: 模式名称 + inputs: 输入序列 + context: 上下文 + description: 描述 + time_limit: 时间限制(秒) + + Returns: + 模式ID + """ + try: + pattern_id = self.pattern_counter + self.pattern_counter += 1 + + pattern = { + 'id': pattern_id, + 'name': name, + 'inputs': inputs, + 'context': context, + 'description': description, + 'time_limit': time_limit or self.advanced_config['pattern_timeout'], + 'created_time': time.time(), + 'match_count': 0 + } + + self.input_patterns[pattern_id] = pattern + + print(f"✓ 输入模式已创建: {name} (ID: {pattern_id})") + return pattern_id + + except Exception as e: + print(f"✗ 输入模式创建失败: {e}") + return -1 + + def remove_input_pattern(self, pattern_id: int) -> bool: + """ + 移除输入模式 + + Args: + pattern_id: 模式ID + + Returns: + 是否移除成功 + """ + try: + if pattern_id in self.input_patterns: + pattern_name = self.input_patterns[pattern_id]['name'] + del self.input_patterns[pattern_id] + print(f"✓ 输入模式已移除: {pattern_name} (ID: {pattern_id})") + return True + else: + print(f"✗ 输入模式不存在: ID {pattern_id}") + return False + + except Exception as e: + print(f"✗ 输入模式移除失败: {e}") + return False + + def process_input_pattern(self, input_name: str) -> bool: + """ + 处理输入模式匹配 + + Args: + input_name: 输入名称 + + Returns: + 是否匹配成功 + """ + try: + current_time = time.time() + matched_patterns = [] + + for pattern_id, pattern in self.input_patterns.items(): + # 检查上下文是否匹配 + if pattern['context'] != self.current_context and pattern['context'] != InputContext.GLOBAL: + continue + + # 初始化模式跟踪 + if pattern_id not in self.pattern_start_time: + self.pattern_start_time[pattern_id] = current_time + + # 检查时间限制 + if current_time - self.pattern_start_time[pattern_id] > pattern['time_limit']: + continue + + # 这里应该实现实际的模式匹配逻辑 + # 简化处理:只检查最后一个输入 + if pattern['inputs'] and pattern['inputs'][-1] == input_name: + matched_patterns.append(pattern_id) + + # 处理匹配的模式 + for pattern_id in matched_patterns: + pattern = self.input_patterns[pattern_id] + pattern['match_count'] += 1 + self.advanced_stats['patterns_matched'] += 1 + + # 触发模式匹配回调 + self._trigger_advanced_callback('pattern_matched', { + 'pattern_id': pattern_id, + 'pattern_name': pattern['name'], + 'input': input_name + }) + + # 重置模式时间 + if pattern_id in self.pattern_start_time: + del self.pattern_start_time[pattern_id] + + print(f"✓ 输入模式匹配: {pattern['name']}") + + return len(matched_patterns) > 0 + + except Exception as e: + print(f"✗ 输入模式处理失败: {e}") + return False + + def create_input_sequence(self, name: str, sequence: List[str], context: InputContext, + description: str = "") -> int: + """ + 创建输入序列 + + Args: + name: 序列名称 + sequence: 输入序列 + context: 上下文 + description: 描述 + + Returns: + 序列ID + """ + try: + sequence_id = self.sequence_counter + self.sequence_counter += 1 + + seq = { + 'id': sequence_id, + 'name': name, + 'sequence': sequence, + 'context': context, + 'description': description, + 'created_time': time.time(), + 'current_position': 0, + 'completion_count': 0 + } + + self.input_sequences[sequence_id] = seq + + print(f"✓ 输入序列已创建: {name} (ID: {sequence_id})") + return sequence_id + + except Exception as e: + print(f"✗ 输入序列创建失败: {e}") + return -1 + + def remove_input_sequence(self, sequence_id: int) -> bool: + """ + 移除输入序列 + + Args: + sequence_id: 序列ID + + Returns: + 是否移除成功 + """ + try: + if sequence_id in self.input_sequences: + sequence_name = self.input_sequences[sequence_id]['name'] + del self.input_sequences[sequence_id] + print(f"✓ 输入序列已移除: {sequence_name} (ID: {sequence_id})") + return True + else: + print(f"✗ 输入序列不存在: ID {sequence_id}") + return False + + except Exception as e: + print(f"✗ 输入序列移除失败: {e}") + return False + + def process_input_sequence(self, input_name: str) -> bool: + """ + 处理输入序列 + + Args: + input_name: 输入名称 + + Returns: + 是否完成序列 + """ + try: + current_time = time.time() + completed_sequences = [] + + for sequence_id, sequence in self.input_sequences.items(): + # 检查上下文是否匹配 + if sequence['context'] != self.current_context and sequence['context'] != InputContext.GLOBAL: + continue + + # 初始化序列时间 + if sequence_id not in self.sequence_start_time: + self.sequence_start_time[sequence_id] = current_time + sequence['current_position'] = 0 + + # 检查时间限制 + if current_time - self.sequence_start_time[sequence_id] > self.advanced_config['sequence_timeout']: + sequence['current_position'] = 0 + self.sequence_start_time[sequence_id] = current_time + + # 检查输入匹配 + if sequence['current_position'] < len(sequence['sequence']): + expected_input = sequence['sequence'][sequence['current_position']] + if input_name == expected_input: + sequence['current_position'] += 1 + + # 检查序列是否完成 + if sequence['current_position'] >= len(sequence['sequence']): + completed_sequences.append(sequence_id) + + # 处理完成的序列 + for sequence_id in completed_sequences: + sequence = self.input_sequences[sequence_id] + sequence['completion_count'] += 1 + sequence['current_position'] = 0 + self.advanced_stats['sequences_completed'] += 1 + + # 触发序列完成回调 + self._trigger_advanced_callback('sequence_completed', { + 'sequence_id': sequence_id, + 'sequence_name': sequence['name'] + }) + + # 重置序列时间 + if sequence_id in self.sequence_start_time: + del self.sequence_start_time[sequence_id] + + print(f"✓ 输入序列完成: {sequence['name']}") + + return len(completed_sequences) > 0 + + except Exception as e: + print(f"✗ 输入序列处理失败: {e}") + return False + + def update_modifiers(self, modifier: str, pressed: bool): + """ + 更新修饰符状态 + + Args: + modifier: 修饰符名称 + pressed: 是否按下 + """ + try: + if pressed: + self.active_modifiers.add(modifier) + self.advanced_stats['modifiers_activated'] += 1 + + # 触发修饰符变更回调 + self._trigger_advanced_callback('modifier_changed', { + 'modifier': modifier, + 'state': 'pressed' + }) + else: + if modifier in self.active_modifiers: + self.active_modifiers.remove(modifier) + + # 触发修饰符变更回调 + self._trigger_advanced_callback('modifier_changed', { + 'modifier': modifier, + 'state': 'released' + }) + + except Exception as e: + print(f"✗ 修饰符更新失败: {e}") + + def is_modifier_active(self, modifier: str) -> bool: + """ + 检查修饰符是否激活 + + Args: + modifier: 修饰符名称 + + Returns: + 是否激活 + """ + return modifier in self.active_modifiers + + def get_active_modifiers(self) -> Set[str]: + """ + 获取激活的修饰符列表 + + Returns: + 激活的修饰符集合 + """ + return self.active_modifiers.copy() + + def process_timed_input(self, input_name: str, duration: float): + """ + 处理时间相关输入 + + Args: + input_name: 输入名称 + duration: 持续时间(秒) + """ + try: + # 检查是否为双击 + if duration < self.advanced_config['double_press_time']: + last_time = self.last_input_time.get(input_name, 0) + current_time = time.time() + if current_time - last_time < self.advanced_config['double_press_time']: + # 双击事件 + self._trigger_advanced_callback('timed_input', { + 'input': input_name, + 'type': 'double_click', + 'duration': duration + }) + self.advanced_stats['timed_inputs_processed'] += 1 + + # 检查是否为长按 + elif duration >= self.advanced_config['long_press_time']: + # 长按事件 + self._trigger_advanced_callback('timed_input', { + 'input': input_name, + 'type': 'long_press', + 'duration': duration + }) + self.advanced_stats['timed_inputs_processed'] += 1 + + # 更新最后输入时间 + self.last_input_time[input_name] = time.time() + + except Exception as e: + print(f"✗ 时间相关输入处理失败: {e}") + + def _trigger_advanced_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发高级回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.advanced_callbacks: + for callback in self.advanced_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 高级回调执行失败: {e}") + except Exception as e: + print(f"✗ 高级回调触发失败: {e}") + + def register_advanced_callback(self, callback_type: str, callback: Callable): + """ + 注册高级回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.advanced_callbacks: + self.advanced_callbacks[callback_type].append(callback) + print(f"✓ 高级回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 高级回调注册失败: {e}") + + def unregister_advanced_callback(self, callback_type: str, callback: Callable): + """ + 注销高级回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.advanced_callbacks: + if callback in self.advanced_callbacks[callback_type]: + self.advanced_callbacks[callback_type].remove(callback) + print(f"✓ 高级回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 高级回调注销失败: {e}") + + def get_current_context(self) -> InputContext: + """ + 获取当前输入上下文 + + Returns: + 当前输入上下文 + """ + return self.current_context + + def get_context_stack(self) -> List[InputContext]: + """ + 获取上下文堆栈 + + Returns: + 上下文堆栈列表 + """ + return self.context_stack.copy() + + def get_input_patterns(self) -> Dict[int, Dict[str, Any]]: + """ + 获取所有输入模式 + + Returns: + 输入模式字典 + """ + return self.input_patterns.copy() + + def get_input_sequences(self) -> Dict[int, Dict[str, Any]]: + """ + 获取所有输入序列 + + Returns: + 输入序列字典 + """ + return self.input_sequences.copy() + + def set_advanced_config(self, config: Dict[str, Any]) -> bool: + """ + 设置高级配置 + + Args: + config: 高级配置字典 + + Returns: + 是否设置成功 + """ + try: + self.advanced_config.update(config) + print(f"✓ 高级配置已更新: {self.advanced_config}") + return True + except Exception as e: + print(f"✗ 高级配置设置失败: {e}") + return False + + def get_advanced_config(self) -> Dict[str, Any]: + """ + 获取高级配置 + + Returns: + 高级配置字典 + """ + return self.advanced_config.copy() + + def get_advanced_stats(self) -> Dict[str, int]: + """ + 获取高级统计信息 + + Returns: + 高级统计字典 + """ + return self.advanced_stats.copy() + + def reset_advanced_stats(self): + """重置高级统计信息""" + try: + self.advanced_stats = { + 'contexts_switched': 0, + 'patterns_matched': 0, + 'sequences_completed': 0, + 'modifiers_activated': 0, + 'timed_inputs_processed': 0 + } + print("✓ 高级统计信息已重置") + except Exception as e: + print(f"✗ 高级统计信息重置失败: {e}") + + def get_context_info(self, context: InputContext) -> Optional[Dict[str, Any]]: + """ + 获取上下文信息 + + Args: + context: 输入上下文 + + Returns: + 上下文信息字典或None + """ return self.input_contexts.get(context) \ No newline at end of file diff --git a/plugins/user/multiplatform_input/core/input_manager.py b/plugins/user/multiplatform_input/core/input_manager.py index 2d455528..5633f056 100644 --- a/plugins/user/multiplatform_input/core/input_manager.py +++ b/plugins/user/multiplatform_input/core/input_manager.py @@ -1,600 +1,600 @@ -""" -输入管理器模块 -负责管理所有输入设备和事件处理 -""" - -import time -from typing import Dict, Any, List, Optional, Callable -import threading -from enum import Enum - -class InputType(Enum): - """输入类型枚举""" - KEYBOARD = "keyboard" - MOUSE = "mouse" - TOUCH = "touch" - GAMEPAD = "gamepad" - VR_CONTROLLER = "vr_controller" - MOTION = "motion" - CUSTOM = "custom" - -class InputState(Enum): - """输入状态枚举""" - IDLE = "idle" - PRESSED = "pressed" - HELD = "held" - RELEASED = "released" - -class InputManager: - """ - 输入管理器 - 负责管理所有输入设备、处理输入事件和维护输入状态 - """ - - def __init__(self, plugin): - """ - 初始化输入管理器 - - Args: - plugin: 多平台输入支持插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 输入设备字典 - self.input_devices = {} - self.device_counter = 0 - - # 输入状态映射 - self.input_states = {} - - # 事件监听器 - self.event_listeners = {} - - # 输入配置 - self.input_config = { - 'polling_rate': 120, # 轮询频率(Hz) - 'deadzone': 0.1, # 死区阈值 - 'double_click_time': 0.3, # 双击时间阈值(秒) - 'long_press_time': 0.5, # 长按时间阈值(秒) - 'swipe_threshold': 50, # 滑动阈值(像素) - 'gesture_timeout': 1.0 # 手势超时时间(秒) - } - - # 输入缓冲区 - self.input_buffer = [] - self.buffer_lock = threading.Lock() - - # 时间戳记录 - self.last_input_time = {} - self.press_start_time = {} - - # 统计信息 - self.stats = { - 'total_inputs': 0, - 'active_devices': 0, - 'events_processed': 0, - 'buffer_size': 0 - } - - # 回调函数 - self.input_callbacks = {} - self.action_callbacks = {} - - # 线程管理 - self.update_thread = None - self.running = False - - print("✓ 输入管理器已创建") - - def initialize(self) -> bool: - """ - 初始化输入管理器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 输入管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 输入管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用输入管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 输入管理器未初始化") - return False - - self.enabled = True - self.running = True - - # 启动更新线程 - self.update_thread = threading.Thread(target=self._update_loop, daemon=True) - self.update_thread.start() - - print("✓ 输入管理器已启用") - return True - - except Exception as e: - print(f"✗ 输入管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用输入管理器""" - try: - self.enabled = False - self.running = False - - # 等待更新线程结束 - if self.update_thread and self.update_thread.is_alive(): - self.update_thread.join(timeout=1.0) - - print("✓ 输入管理器已禁用") - - except Exception as e: - print(f"✗ 输入管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理输入管理器资源""" - try: - self.disable() - self.input_devices.clear() - self.input_states.clear() - self.event_listeners.clear() - self.input_buffer.clear() - self.input_callbacks.clear() - self.action_callbacks.clear() - self.initialized = False - print("✓ 输入管理器资源已清理") - - except Exception as e: - print(f"✗ 输入管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新输入管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 处理输入缓冲区 - self._process_input_buffer() - - # 更新统计信息 - with self.buffer_lock: - self.stats['buffer_size'] = len(self.input_buffer) - - except Exception as e: - print(f"✗ 输入管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_loop(self): - """更新循环线程""" - try: - polling_interval = 1.0 / self.input_config['polling_rate'] - - while self.running: - if self.enabled: - # 检查长按事件 - self._check_long_press_events() - - # 检查手势超时 - self._check_gesture_timeouts() - - time.sleep(polling_interval) - - except Exception as e: - print(f"✗ 输入管理器更新循环失败: {e}") - - def _process_input_buffer(self): - """处理输入缓冲区""" - try: - with self.buffer_lock: - events_to_process = self.input_buffer[:] - self.input_buffer.clear() - - for event in events_to_process: - self._handle_input_event(event) - - except Exception as e: - print(f"✗ 输入缓冲区处理失败: {e}") - - def _handle_input_event(self, event: Dict[str, Any]): - """ - 处理输入事件 - - Args: - event: 输入事件字典 - """ - try: - event_type = event.get('type') - device_id = event.get('device_id') - input_id = event.get('input_id') - value = event.get('value') - timestamp = event.get('timestamp', time.time()) - - # 更新输入状态 - state_key = f"{device_id}:{input_id}" - old_value = self.input_states.get(state_key, 0.0) - self.input_states[state_key] = value - - # 更新时间戳 - self.last_input_time[state_key] = timestamp - - # 确定输入状态变化 - if abs(value) > self.input_config['deadzone']: - if abs(old_value) <= self.input_config['deadzone']: - # 按下事件 - self.press_start_time[state_key] = timestamp - self._trigger_event('input_pressed', { - 'device_id': device_id, - 'input_id': input_id, - 'value': value, - 'timestamp': timestamp - }) - else: - # 持续按住事件 - self._trigger_event('input_held', { - 'device_id': device_id, - 'input_id': input_id, - 'value': value, - 'timestamp': timestamp - }) - else: - if abs(old_value) > self.input_config['deadzone']: - # 释放事件 - press_duration = timestamp - self.press_start_time.get(state_key, timestamp) - self._trigger_event('input_released', { - 'device_id': device_id, - 'input_id': input_id, - 'value': value, - 'duration': press_duration, - 'timestamp': timestamp - }) - - # 检查是否为点击事件 - if press_duration < self.input_config['long_press_time']: - self._trigger_event('input_clicked', { - 'device_id': device_id, - 'input_id': input_id, - 'timestamp': timestamp - }) - - # 检查是否为双击事件 - last_click_time = self.last_input_time.get(f"click:{state_key}", 0) - if timestamp - last_click_time < self.input_config['double_click_time']: - self._trigger_event('input_double_clicked', { - 'device_id': device_id, - 'input_id': input_id, - 'timestamp': timestamp - }) - self.last_input_time[f"click:{state_key}"] = timestamp - - # 检查是否为长按事件 - elif press_duration >= self.input_config['long_press_time']: - self._trigger_event('input_long_pressed', { - 'device_id': device_id, - 'input_id': input_id, - 'duration': press_duration, - 'timestamp': timestamp - }) - - # 更新统计信息 - self.stats['events_processed'] += 1 - - except Exception as e: - print(f"✗ 输入事件处理失败: {e}") - - def _check_long_press_events(self): - """检查长按事件""" - try: - current_time = time.time() - for state_key, start_time in self.press_start_time.items(): - if state_key in self.input_states: - value = self.input_states[state_key] - if abs(value) > self.input_config['deadzone']: - duration = current_time - start_time - if duration >= self.input_config['long_press_time']: - # 检查是否已经触发过长按事件 - last_long_press = self.last_input_time.get(f"long_press:{state_key}", 0) - if current_time - last_long_press > self.input_config['long_press_time']: - device_id, input_id = state_key.split(':', 1) - self._trigger_event('input_long_pressing', { - 'device_id': device_id, - 'input_id': input_id, - 'value': value, - 'duration': duration, - 'timestamp': current_time - }) - self.last_input_time[f"long_press:{state_key}"] = current_time - - except Exception as e: - print(f"✗ 长按事件检查失败: {e}") - - def _check_gesture_timeouts(self): - """检查手势超时""" - try: - # 这里可以实现手势超时检查逻辑 - pass - except Exception as e: - print(f"✗ 手势超时检查失败: {e}") - - def add_input_device(self, device_type: InputType, device_name: str, - capabilities: List[str]) -> int: - """ - 添加输入设备 - - Args: - device_type: 设备类型 - device_name: 设备名称 - capabilities: 设备功能列表 - - Returns: - 设备ID - """ - try: - device_id = self.device_counter - self.device_counter += 1 - - device_info = { - 'id': device_id, - 'type': device_type, - 'name': device_name, - 'capabilities': capabilities, - 'connected': True, - 'last_seen': time.time(), - 'input_states': {} - } - - self.input_devices[device_id] = device_info - self.stats['active_devices'] += 1 - - print(f"✓ 输入设备已添加: {device_name} (ID: {device_id})") - return device_id - - except Exception as e: - print(f"✗ 输入设备添加失败: {e}") - return -1 - - def remove_input_device(self, device_id: int) -> bool: - """ - 移除输入设备 - - Args: - device_id: 设备ID - - Returns: - 是否移除成功 - """ - try: - if device_id in self.input_devices: - device_name = self.input_devices[device_id]['name'] - del self.input_devices[device_id] - self.stats['active_devices'] -= 1 - print(f"✓ 输入设备已移除: {device_name} (ID: {device_id})") - return True - else: - print(f"✗ 输入设备不存在: ID {device_id}") - return False - - except Exception as e: - print(f"✗ 输入设备移除失败: {e}") - return False - - def get_input_device(self, device_id: int) -> Optional[Dict[str, Any]]: - """ - 获取输入设备信息 - - Args: - device_id: 设备ID - - Returns: - 设备信息字典或None - """ - return self.input_devices.get(device_id) - - def get_all_input_devices(self) -> Dict[int, Dict[str, Any]]: - """ - 获取所有输入设备 - - Returns: - 所有设备信息字典 - """ - return self.input_devices.copy() - - def submit_input_event(self, event: Dict[str, Any]): - """ - 提交输入事件到缓冲区 - - Args: - event: 输入事件字典 - """ - try: - with self.buffer_lock: - self.input_buffer.append(event) - self.stats['total_inputs'] += 1 - except Exception as e: - print(f"✗ 输入事件提交失败: {e}") - - def register_event_listener(self, event_type: str, callback: Callable): - """ - 注册事件监听器 - - Args: - event_type: 事件类型 - callback: 回调函数 - """ - try: - if event_type not in self.event_listeners: - self.event_listeners[event_type] = [] - self.event_listeners[event_type].append(callback) - print(f"✓ 事件监听器已注册: {event_type}") - except Exception as e: - print(f"✗ 事件监听器注册失败: {e}") - - def unregister_event_listener(self, event_type: str, callback: Callable): - """ - 注销事件监听器 - - Args: - event_type: 事件类型 - callback: 回调函数 - """ - try: - if event_type in self.event_listeners: - if callback in self.event_listeners[event_type]: - self.event_listeners[event_type].remove(callback) - print(f"✓ 事件监听器已注销: {event_type}") - except Exception as e: - print(f"✗ 事件监听器注销失败: {e}") - - def _trigger_event(self, event_type: str, event_data: Dict[str, Any]): - """ - 触发事件 - - Args: - event_type: 事件类型 - event_data: 事件数据 - """ - try: - if event_type in self.event_listeners: - for callback in self.event_listeners[event_type]: - try: - callback(event_data) - except Exception as e: - print(f"✗ 事件回调执行失败: {e}") - except Exception as e: - print(f"✗ 事件触发失败: {e}") - - def get_input_state(self, device_id: int, input_id: str) -> float: - """ - 获取输入状态 - - Args: - device_id: 设备ID - input_id: 输入ID - - Returns: - 输入值 - """ - state_key = f"{device_id}:{input_id}" - return self.input_states.get(state_key, 0.0) - - def is_input_pressed(self, device_id: int, input_id: str) -> bool: - """ - 检查输入是否被按下 - - Args: - device_id: 设备ID - input_id: 输入ID - - Returns: - 是否被按下 - """ - value = self.get_input_state(device_id, input_id) - return abs(value) > self.input_config['deadzone'] - - def get_input_config(self) -> Dict[str, Any]: - """ - 获取输入配置 - - Returns: - 输入配置字典 - """ - return self.input_config.copy() - - def set_input_config(self, config: Dict[str, Any]): - """ - 设置输入配置 - - Args: - config: 输入配置字典 - """ - try: - self.input_config.update(config) - print(f"✓ 输入配置已更新: {self.input_config}") - except Exception as e: - print(f"✗ 输入配置设置失败: {e}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - try: - self.stats = { - 'total_inputs': 0, - 'active_devices': 0, - 'events_processed': 0, - 'buffer_size': 0 - } - print("✓ 输入管理器统计信息已重置") - except Exception as e: - print(f"✗ 输入管理器统计信息重置失败: {e}") - - def register_action_callback(self, action_name: str, callback: Callable): - """ - 注册动作回调 - - Args: - action_name: 动作名称 - callback: 回调函数 - """ - try: - self.action_callbacks[action_name] = callback - print(f"✓ 动作回调已注册: {action_name}") - except Exception as e: - print(f"✗ 动作回调注册失败: {e}") - - def trigger_action(self, action_name: str, action_data: Dict[str, Any] = None): - """ - 触发动作 - - Args: - action_name: 动作名称 - action_data: 动作数据 - """ - try: - if action_name in self.action_callbacks: - callback = self.action_callbacks[action_name] - try: - if action_data: - callback(action_data) - else: - callback() - except Exception as e: - print(f"✗ 动作回调执行失败: {e}") - else: - print(f"✗ 未找到动作回调: {action_name}") - except Exception as e: +""" +输入管理器模块 +负责管理所有输入设备和事件处理 +""" + +import time +from typing import Dict, Any, List, Optional, Callable +import threading +from enum import Enum + +class InputType(Enum): + """输入类型枚举""" + KEYBOARD = "keyboard" + MOUSE = "mouse" + TOUCH = "touch" + GAMEPAD = "gamepad" + VR_CONTROLLER = "vr_controller" + MOTION = "motion" + CUSTOM = "custom" + +class InputState(Enum): + """输入状态枚举""" + IDLE = "idle" + PRESSED = "pressed" + HELD = "held" + RELEASED = "released" + +class InputManager: + """ + 输入管理器 + 负责管理所有输入设备、处理输入事件和维护输入状态 + """ + + def __init__(self, plugin): + """ + 初始化输入管理器 + + Args: + plugin: 多平台输入支持插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 输入设备字典 + self.input_devices = {} + self.device_counter = 0 + + # 输入状态映射 + self.input_states = {} + + # 事件监听器 + self.event_listeners = {} + + # 输入配置 + self.input_config = { + 'polling_rate': 120, # 轮询频率(Hz) + 'deadzone': 0.1, # 死区阈值 + 'double_click_time': 0.3, # 双击时间阈值(秒) + 'long_press_time': 0.5, # 长按时间阈值(秒) + 'swipe_threshold': 50, # 滑动阈值(像素) + 'gesture_timeout': 1.0 # 手势超时时间(秒) + } + + # 输入缓冲区 + self.input_buffer = [] + self.buffer_lock = threading.Lock() + + # 时间戳记录 + self.last_input_time = {} + self.press_start_time = {} + + # 统计信息 + self.stats = { + 'total_inputs': 0, + 'active_devices': 0, + 'events_processed': 0, + 'buffer_size': 0 + } + + # 回调函数 + self.input_callbacks = {} + self.action_callbacks = {} + + # 线程管理 + self.update_thread = None + self.running = False + + print("✓ 输入管理器已创建") + + def initialize(self) -> bool: + """ + 初始化输入管理器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 输入管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 输入管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用输入管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 输入管理器未初始化") + return False + + self.enabled = True + self.running = True + + # 启动更新线程 + self.update_thread = threading.Thread(target=self._update_loop, daemon=True) + self.update_thread.start() + + print("✓ 输入管理器已启用") + return True + + except Exception as e: + print(f"✗ 输入管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用输入管理器""" + try: + self.enabled = False + self.running = False + + # 等待更新线程结束 + if self.update_thread and self.update_thread.is_alive(): + self.update_thread.join(timeout=1.0) + + print("✓ 输入管理器已禁用") + + except Exception as e: + print(f"✗ 输入管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理输入管理器资源""" + try: + self.disable() + self.input_devices.clear() + self.input_states.clear() + self.event_listeners.clear() + self.input_buffer.clear() + self.input_callbacks.clear() + self.action_callbacks.clear() + self.initialized = False + print("✓ 输入管理器资源已清理") + + except Exception as e: + print(f"✗ 输入管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新输入管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 处理输入缓冲区 + self._process_input_buffer() + + # 更新统计信息 + with self.buffer_lock: + self.stats['buffer_size'] = len(self.input_buffer) + + except Exception as e: + print(f"✗ 输入管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_loop(self): + """更新循环线程""" + try: + polling_interval = 1.0 / self.input_config['polling_rate'] + + while self.running: + if self.enabled: + # 检查长按事件 + self._check_long_press_events() + + # 检查手势超时 + self._check_gesture_timeouts() + + time.sleep(polling_interval) + + except Exception as e: + print(f"✗ 输入管理器更新循环失败: {e}") + + def _process_input_buffer(self): + """处理输入缓冲区""" + try: + with self.buffer_lock: + events_to_process = self.input_buffer[:] + self.input_buffer.clear() + + for event in events_to_process: + self._handle_input_event(event) + + except Exception as e: + print(f"✗ 输入缓冲区处理失败: {e}") + + def _handle_input_event(self, event: Dict[str, Any]): + """ + 处理输入事件 + + Args: + event: 输入事件字典 + """ + try: + event_type = event.get('type') + device_id = event.get('device_id') + input_id = event.get('input_id') + value = event.get('value') + timestamp = event.get('timestamp', time.time()) + + # 更新输入状态 + state_key = f"{device_id}:{input_id}" + old_value = self.input_states.get(state_key, 0.0) + self.input_states[state_key] = value + + # 更新时间戳 + self.last_input_time[state_key] = timestamp + + # 确定输入状态变化 + if abs(value) > self.input_config['deadzone']: + if abs(old_value) <= self.input_config['deadzone']: + # 按下事件 + self.press_start_time[state_key] = timestamp + self._trigger_event('input_pressed', { + 'device_id': device_id, + 'input_id': input_id, + 'value': value, + 'timestamp': timestamp + }) + else: + # 持续按住事件 + self._trigger_event('input_held', { + 'device_id': device_id, + 'input_id': input_id, + 'value': value, + 'timestamp': timestamp + }) + else: + if abs(old_value) > self.input_config['deadzone']: + # 释放事件 + press_duration = timestamp - self.press_start_time.get(state_key, timestamp) + self._trigger_event('input_released', { + 'device_id': device_id, + 'input_id': input_id, + 'value': value, + 'duration': press_duration, + 'timestamp': timestamp + }) + + # 检查是否为点击事件 + if press_duration < self.input_config['long_press_time']: + self._trigger_event('input_clicked', { + 'device_id': device_id, + 'input_id': input_id, + 'timestamp': timestamp + }) + + # 检查是否为双击事件 + last_click_time = self.last_input_time.get(f"click:{state_key}", 0) + if timestamp - last_click_time < self.input_config['double_click_time']: + self._trigger_event('input_double_clicked', { + 'device_id': device_id, + 'input_id': input_id, + 'timestamp': timestamp + }) + self.last_input_time[f"click:{state_key}"] = timestamp + + # 检查是否为长按事件 + elif press_duration >= self.input_config['long_press_time']: + self._trigger_event('input_long_pressed', { + 'device_id': device_id, + 'input_id': input_id, + 'duration': press_duration, + 'timestamp': timestamp + }) + + # 更新统计信息 + self.stats['events_processed'] += 1 + + except Exception as e: + print(f"✗ 输入事件处理失败: {e}") + + def _check_long_press_events(self): + """检查长按事件""" + try: + current_time = time.time() + for state_key, start_time in self.press_start_time.items(): + if state_key in self.input_states: + value = self.input_states[state_key] + if abs(value) > self.input_config['deadzone']: + duration = current_time - start_time + if duration >= self.input_config['long_press_time']: + # 检查是否已经触发过长按事件 + last_long_press = self.last_input_time.get(f"long_press:{state_key}", 0) + if current_time - last_long_press > self.input_config['long_press_time']: + device_id, input_id = state_key.split(':', 1) + self._trigger_event('input_long_pressing', { + 'device_id': device_id, + 'input_id': input_id, + 'value': value, + 'duration': duration, + 'timestamp': current_time + }) + self.last_input_time[f"long_press:{state_key}"] = current_time + + except Exception as e: + print(f"✗ 长按事件检查失败: {e}") + + def _check_gesture_timeouts(self): + """检查手势超时""" + try: + # 这里可以实现手势超时检查逻辑 + pass + except Exception as e: + print(f"✗ 手势超时检查失败: {e}") + + def add_input_device(self, device_type: InputType, device_name: str, + capabilities: List[str]) -> int: + """ + 添加输入设备 + + Args: + device_type: 设备类型 + device_name: 设备名称 + capabilities: 设备功能列表 + + Returns: + 设备ID + """ + try: + device_id = self.device_counter + self.device_counter += 1 + + device_info = { + 'id': device_id, + 'type': device_type, + 'name': device_name, + 'capabilities': capabilities, + 'connected': True, + 'last_seen': time.time(), + 'input_states': {} + } + + self.input_devices[device_id] = device_info + self.stats['active_devices'] += 1 + + print(f"✓ 输入设备已添加: {device_name} (ID: {device_id})") + return device_id + + except Exception as e: + print(f"✗ 输入设备添加失败: {e}") + return -1 + + def remove_input_device(self, device_id: int) -> bool: + """ + 移除输入设备 + + Args: + device_id: 设备ID + + Returns: + 是否移除成功 + """ + try: + if device_id in self.input_devices: + device_name = self.input_devices[device_id]['name'] + del self.input_devices[device_id] + self.stats['active_devices'] -= 1 + print(f"✓ 输入设备已移除: {device_name} (ID: {device_id})") + return True + else: + print(f"✗ 输入设备不存在: ID {device_id}") + return False + + except Exception as e: + print(f"✗ 输入设备移除失败: {e}") + return False + + def get_input_device(self, device_id: int) -> Optional[Dict[str, Any]]: + """ + 获取输入设备信息 + + Args: + device_id: 设备ID + + Returns: + 设备信息字典或None + """ + return self.input_devices.get(device_id) + + def get_all_input_devices(self) -> Dict[int, Dict[str, Any]]: + """ + 获取所有输入设备 + + Returns: + 所有设备信息字典 + """ + return self.input_devices.copy() + + def submit_input_event(self, event: Dict[str, Any]): + """ + 提交输入事件到缓冲区 + + Args: + event: 输入事件字典 + """ + try: + with self.buffer_lock: + self.input_buffer.append(event) + self.stats['total_inputs'] += 1 + except Exception as e: + print(f"✗ 输入事件提交失败: {e}") + + def register_event_listener(self, event_type: str, callback: Callable): + """ + 注册事件监听器 + + Args: + event_type: 事件类型 + callback: 回调函数 + """ + try: + if event_type not in self.event_listeners: + self.event_listeners[event_type] = [] + self.event_listeners[event_type].append(callback) + print(f"✓ 事件监听器已注册: {event_type}") + except Exception as e: + print(f"✗ 事件监听器注册失败: {e}") + + def unregister_event_listener(self, event_type: str, callback: Callable): + """ + 注销事件监听器 + + Args: + event_type: 事件类型 + callback: 回调函数 + """ + try: + if event_type in self.event_listeners: + if callback in self.event_listeners[event_type]: + self.event_listeners[event_type].remove(callback) + print(f"✓ 事件监听器已注销: {event_type}") + except Exception as e: + print(f"✗ 事件监听器注销失败: {e}") + + def _trigger_event(self, event_type: str, event_data: Dict[str, Any]): + """ + 触发事件 + + Args: + event_type: 事件类型 + event_data: 事件数据 + """ + try: + if event_type in self.event_listeners: + for callback in self.event_listeners[event_type]: + try: + callback(event_data) + except Exception as e: + print(f"✗ 事件回调执行失败: {e}") + except Exception as e: + print(f"✗ 事件触发失败: {e}") + + def get_input_state(self, device_id: int, input_id: str) -> float: + """ + 获取输入状态 + + Args: + device_id: 设备ID + input_id: 输入ID + + Returns: + 输入值 + """ + state_key = f"{device_id}:{input_id}" + return self.input_states.get(state_key, 0.0) + + def is_input_pressed(self, device_id: int, input_id: str) -> bool: + """ + 检查输入是否被按下 + + Args: + device_id: 设备ID + input_id: 输入ID + + Returns: + 是否被按下 + """ + value = self.get_input_state(device_id, input_id) + return abs(value) > self.input_config['deadzone'] + + def get_input_config(self) -> Dict[str, Any]: + """ + 获取输入配置 + + Returns: + 输入配置字典 + """ + return self.input_config.copy() + + def set_input_config(self, config: Dict[str, Any]): + """ + 设置输入配置 + + Args: + config: 输入配置字典 + """ + try: + self.input_config.update(config) + print(f"✓ 输入配置已更新: {self.input_config}") + except Exception as e: + print(f"✗ 输入配置设置失败: {e}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + try: + self.stats = { + 'total_inputs': 0, + 'active_devices': 0, + 'events_processed': 0, + 'buffer_size': 0 + } + print("✓ 输入管理器统计信息已重置") + except Exception as e: + print(f"✗ 输入管理器统计信息重置失败: {e}") + + def register_action_callback(self, action_name: str, callback: Callable): + """ + 注册动作回调 + + Args: + action_name: 动作名称 + callback: 回调函数 + """ + try: + self.action_callbacks[action_name] = callback + print(f"✓ 动作回调已注册: {action_name}") + except Exception as e: + print(f"✗ 动作回调注册失败: {e}") + + def trigger_action(self, action_name: str, action_data: Dict[str, Any] = None): + """ + 触发动作 + + Args: + action_name: 动作名称 + action_data: 动作数据 + """ + try: + if action_name in self.action_callbacks: + callback = self.action_callbacks[action_name] + try: + if action_data: + callback(action_data) + else: + callback() + except Exception as e: + print(f"✗ 动作回调执行失败: {e}") + else: + print(f"✗ 未找到动作回调: {action_name}") + except Exception as e: print(f"✗ 动作触发失败: {e}") \ No newline at end of file diff --git a/plugins/user/multiplatform_input/devices/device_calibrator.py b/plugins/user/multiplatform_input/devices/device_calibrator.py index b50c1ea0..0db911c1 100644 --- a/plugins/user/multiplatform_input/devices/device_calibrator.py +++ b/plugins/user/multiplatform_input/devices/device_calibrator.py @@ -1,782 +1,782 @@ -""" -设备校准模块 -负责处理各种输入设备的校准和配置 -""" - -import time -from typing import Dict, Any, List, Optional, Callable -import threading -import math - -class DeviceCalibrator: - """ - 设备校准器 - 负责处理各种输入设备的校准和配置 - """ - - def __init__(self, plugin): - """ - 初始化设备校准器 - - Args: - plugin: 多平台输入支持插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 校准配置 - self.calibration_config = { - 'auto_calibration': True, - 'calibration_interval': 300.0, # 5分钟 - 'sensitivity_threshold': 0.01, - 'deadzone_calibration': True, - 'range_calibration': True, - 'center_calibration': True, - 'drift_correction': True - } - - # 设备校准数据 - self.calibration_data = {} - - # 校准状态 - self.calibration_state = { - 'is_calibrating': False, - 'current_device': None, - 'calibration_step': 0, - 'total_steps': 0, - 'progress': 0.0 - } - - # 校准历史 - self.calibration_history = {} - - # 回调函数 - self.calibration_callbacks = { - 'calibration_started': [], - 'calibration_progress': [], - 'calibration_completed': [], - 'calibration_failed': [] - } - - # 统计信息 - self.calibration_stats = { - 'calibrations_performed': 0, - 'calibrations_successful': 0, - 'calibrations_failed': 0, - 'auto_calibrations': 0, - 'manual_calibrations': 0 - } - - # 线程管理 - self.calibration_thread = None - self.running = False - - # 时间戳记录 - self.last_auto_calibration = {} - self.calibration_start_time = 0.0 - - print("✓ 设备校准器已创建") - - def initialize(self) -> bool: - """ - 初始化设备校准器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 设备校准器初始化完成") - return True - - except Exception as e: - print(f"✗ 设备校准器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用设备校准器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 设备校准器未初始化") - return False - - self.enabled = True - self.running = True - - print("✓ 设备校准器已启用") - return True - - except Exception as e: - print(f"✗ 设备校准器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用设备校准器""" - try: - self.enabled = False - self.running = False - - # 停止正在进行的校准 - if self.calibration_state['is_calibrating']: - self.cancel_calibration() - - print("✓ 设备校准器已禁用") - - except Exception as e: - print(f"✗ 设备校准器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理设备校准器资源""" - try: - self.disable() - self.calibration_data.clear() - self.calibration_history.clear() - self.calibration_callbacks.clear() - self.initialized = False - print("✓ 设备校准器资源已清理") - - except Exception as e: - print(f"✗ 设备校准器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新设备校准器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 检查自动校准 - if self.calibration_config['auto_calibration']: - self._check_auto_calibration() - - except Exception as e: - print(f"✗ 设备校准器更新失败: {e}") - import traceback - traceback.print_exc() - - def _check_auto_calibration(self): - """检查自动校准""" - try: - current_time = time.time() - - # 检查是否需要自动校准 - if self.plugin.device_manager: - devices = self.plugin.device_manager.get_connected_devices() - for device_id, device_info in devices.items(): - last_calibration = self.last_auto_calibration.get(device_id, 0.0) - if current_time - last_calibration >= self.calibration_config['calibration_interval']: - # 执行自动校准 - self.start_auto_calibration(device_id) - self.calibration_stats['auto_calibrations'] += 1 - - except Exception as e: - print(f"✗ 自动校准检查失败: {e}") - - def start_calibration(self, device_id: int, calibration_type: str = "full") -> bool: - """ - 开始设备校准 - - Args: - device_id: 设备ID - calibration_type: 校准类型 ("full", "deadzone", "range", "center") - - Returns: - 是否开始成功 - """ - try: - if not self.enabled: - print("✗ 设备校准器未启用") - return False - - if self.calibration_state['is_calibrating']: - print("✗ 校准已在进行中") - return False - - # 检查设备是否存在 - if not self.plugin.device_manager or not self.plugin.device_manager.get_device(device_id): - print(f"✗ 设备不存在: ID {device_id}") - return False - - # 设置校准状态 - self.calibration_state['is_calibrating'] = True - self.calibration_state['current_device'] = device_id - self.calibration_state['calibration_step'] = 0 - - # 根据校准类型设置步骤数 - if calibration_type == "full": - self.calibration_state['total_steps'] = 4 - elif calibration_type in ["deadzone", "range", "center"]: - self.calibration_state['total_steps'] = 1 - else: - self.calibration_state['total_steps'] = 1 - - self.calibration_start_time = time.time() - - # 初始化校准数据 - if device_id not in self.calibration_data: - self.calibration_data[device_id] = {} - - self.calibration_data[device_id]['type'] = calibration_type - self.calibration_data[device_id]['start_time'] = self.calibration_start_time - self.calibration_data[device_id]['samples'] = [] - - # 触发开始回调 - self._trigger_calibration_callback('calibration_started', { - 'device_id': device_id, - 'calibration_type': calibration_type, - 'total_steps': self.calibration_state['total_steps'] - }) - - print(f"✓ 设备校准已开始: ID {device_id}, 类型 {calibration_type}") - self.calibration_stats['manual_calibrations'] += 1 - return True - - except Exception as e: - print(f"✗ 设备校准开始失败: {e}") - import traceback - traceback.print_exc() - return False - - def start_auto_calibration(self, device_id: int) -> bool: - """ - 开始自动校准 - - Args: - device_id: 设备ID - - Returns: - 是否开始成功 - """ - try: - result = self.start_calibration(device_id, "full") - if result: - print(f"✓ 自动校准已开始: ID {device_id}") - return result - except Exception as e: - print(f"✗ 自动校准开始失败: {e}") - return False - - def update_calibration(self, sample_data: Dict[str, Any]) -> bool: - """ - 更新校准数据 - - Args: - sample_data: 采样数据 - - Returns: - 是否更新成功 - """ - try: - if not self.calibration_state['is_calibrating']: - print("✗ 没有正在进行的校准") - return False - - device_id = self.calibration_state['current_device'] - if device_id not in self.calibration_data: - print("✗ 校准数据不存在") - return False - - # 添加采样数据 - self.calibration_data[device_id]['samples'].append(sample_data) - - # 更新进度 - self.calibration_state['calibration_step'] += 1 - progress = self.calibration_state['calibration_step'] / self.calibration_state['total_steps'] - self.calibration_state['progress'] = min(1.0, progress) - - # 触进进度回调 - self._trigger_calibration_callback('calibration_progress', { - 'device_id': device_id, - 'step': self.calibration_state['calibration_step'], - 'total_steps': self.calibration_state['total_steps'], - 'progress': self.calibration_state['progress'], - 'sample_data': sample_data - }) - - # 检查是否完成 - if self.calibration_state['calibration_step'] >= self.calibration_state['total_steps']: - self._complete_calibration() - - return True - - except Exception as e: - print(f"✗ 校准数据更新失败: {e}") - return False - - def _complete_calibration(self): - """完成校准""" - try: - device_id = self.calibration_state['current_device'] - calibration_data = self.calibration_data[device_id] - - # 分析采样数据 - calibration_result = self._analyze_calibration_samples(calibration_data) - - # 应用校准结果 - self._apply_calibration_result(device_id, calibration_result) - - # 记录校准历史 - self._record_calibration_history(device_id, calibration_result) - - # 更新时间戳 - self.last_auto_calibration[device_id] = time.time() - - # 重置校准状态 - self.calibration_state['is_calibrating'] = False - self.calibration_state['current_device'] = None - self.calibration_state['calibration_step'] = 0 - self.calibration_state['total_steps'] = 0 - self.calibration_state['progress'] = 0.0 - - # 更新统计信息 - self.calibration_stats['calibrations_performed'] += 1 - self.calibration_stats['calibrations_successful'] += 1 - - # 触发完成回调 - self._trigger_calibration_callback('calibration_completed', { - 'device_id': device_id, - 'result': calibration_result, - 'duration': time.time() - self.calibration_start_time - }) - - print(f"✓ 设备校准已完成: ID {device_id}") - - except Exception as e: - print(f"✗ 校准完成处理失败: {e}") - self._handle_calibration_failure() - - def _analyze_calibration_samples(self, calibration_data: Dict[str, Any]) -> Dict[str, Any]: - """ - 分析校准采样数据 - - Args: - calibration_data: 校准数据 - - Returns: - 分析结果 - """ - try: - samples = calibration_data.get('samples', []) - if not samples: - return {} - - result = { - 'sample_count': len(samples), - 'analysis_time': time.time() - } - - # 分析不同类型的输入数据 - axis_data = {} - button_data = {} - - for sample in samples: - for key, value in sample.items(): - if isinstance(value, (int, float)): - # 轴数据 - if key not in axis_data: - axis_data[key] = [] - axis_data[key].append(value) - elif isinstance(value, bool): - # 按钮数据 - if key not in button_data: - button_data[key] = [] - button_data[key].append(value) - - # 计算轴统计数据 - axis_stats = {} - for axis_name, values in axis_data.items(): - if values: - axis_stats[axis_name] = { - 'min': min(values), - 'max': max(values), - 'mean': sum(values) / len(values), - 'median': sorted(values)[len(values) // 2], - 'std_dev': math.sqrt(sum((x - sum(values) / len(values)) ** 2 for x in values) / len(values)) - } - - result['axis_stats'] = axis_stats - result['button_stats'] = button_data - - # 计算死区、范围和中心点 - deadzones = {} - ranges = {} - centers = {} - - for axis_name, stats in axis_stats.items(): - # 死区计算(基于标准差) - deadzones[axis_name] = max(self.calibration_config['sensitivity_threshold'], - stats['std_dev'] * 2) - - # 范围计算 - ranges[axis_name] = { - 'min': stats['min'], - 'max': stats['max'] - } - - # 中心点计算 - centers[axis_name] = stats['mean'] - - result['deadzones'] = deadzones - result['ranges'] = ranges - result['centers'] = centers - - return result - - except Exception as e: - print(f"✗ 校准数据分析失败: {e}") - return {} - - def _apply_calibration_result(self, device_id: int, calibration_result: Dict[str, Any]): - """ - 应用校准结果 - - Args: - device_id: 设备ID - calibration_result: 校准结果 - """ - try: - # 应用死区设置 - if 'deadzones' in calibration_result and self.calibration_config['deadzone_calibration']: - deadzones = calibration_result['deadzones'] - for axis_name, deadzone in deadzones.items(): - # 在实际实现中,这里会更新设备的死区设置 - print(f" 应用死区设置 {axis_name}: {deadzone}") - - # 应用范围设置 - if 'ranges' in calibration_result and self.calibration_config['range_calibration']: - ranges = calibration_result['ranges'] - for axis_name, range_data in ranges.items(): - # 在实际实现中,这里会更新设备的范围设置 - print(f" 应用范围设置 {axis_name}: {range_data}") - - # 应用中心点设置 - if 'centers' in calibration_result and self.calibration_config['center_calibration']: - centers = calibration_result['centers'] - for axis_name, center in centers.items(): - # 在实际实现中,这里会更新设备的中心点设置 - print(f" 应用中心点设置 {axis_name}: {center}") - - # 保存校准配置 - self._save_calibration_config(device_id, calibration_result) - - except Exception as e: - print(f"✗ 校准结果应用失败: {e}") - - def _save_calibration_config(self, device_id: int, calibration_result: Dict[str, Any]): - """ - 保存校准配置 - - Args: - device_id: 设备ID - calibration_result: 校准结果 - """ - try: - # 在实际实现中,这里会将校准结果保存到配置文件或数据库 - print(f" 校准配置已保存: 设备 {device_id}") - except Exception as e: - print(f"✗ 校准配置保存失败: {e}") - - def _record_calibration_history(self, device_id: int, calibration_result: Dict[str, Any]): - """ - 记录校准历史 - - Args: - device_id: 设备ID - calibration_result: 校准结果 - """ - try: - if device_id not in self.calibration_history: - self.calibration_history[device_id] = [] - - history_entry = { - 'timestamp': time.time(), - 'result': calibration_result, - 'duration': time.time() - self.calibration_start_time - } - - self.calibration_history[device_id].append(history_entry) - - # 保持历史记录大小 - if len(self.calibration_history[device_id]) > 50: - self.calibration_history[device_id].pop(0) - - except Exception as e: - print(f"✗ 校准历史记录失败: {e}") - - def _handle_calibration_failure(self): - """处理校准失败""" - try: - device_id = self.calibration_state['current_device'] - - # 重置校准状态 - self.calibration_state['is_calibrating'] = False - self.calibration_state['current_device'] = None - self.calibration_state['calibration_step'] = 0 - self.calibration_state['total_steps'] = 0 - self.calibration_state['progress'] = 0.0 - - # 更新统计信息 - self.calibration_stats['calibrations_performed'] += 1 - self.calibration_stats['calibrations_failed'] += 1 - - # 触发失败回调 - self._trigger_calibration_callback('calibration_failed', { - 'device_id': device_id, - 'error': 'Calibration process failed' - }) - - print(f"✗ 设备校准失败: ID {device_id}") - - except Exception as e: - print(f"✗ 校准失败处理失败: {e}") - - def cancel_calibration(self) -> bool: - """ - 取消校准 - - Returns: - 是否取消成功 - """ - try: - if not self.calibration_state['is_calibrating']: - print("✗ 没有正在进行的校准") - return False - - device_id = self.calibration_state['current_device'] - - # 重置校准状态 - self.calibration_state['is_calibrating'] = False - self.calibration_state['current_device'] = None - self.calibration_state['calibration_step'] = 0 - self.calibration_state['total_steps'] = 0 - self.calibration_state['progress'] = 0.0 - - print(f"✓ 设备校准已取消: ID {device_id}") - return True - - except Exception as e: - print(f"✗ 校准取消失败: {e}") - return False - - def get_calibration_status(self) -> Dict[str, Any]: - """ - 获取校准状态 - - Returns: - 校准状态字典 - """ - return self.calibration_state.copy() - - def get_calibration_data(self, device_id: int) -> Optional[Dict[str, Any]]: - """ - 获取设备校准数据 - - Args: - device_id: 设备ID - - Returns: - 校准数据字典或None - """ - return self.calibration_data.get(device_id) - - def get_calibration_history(self, device_id: int, limit: int = None) -> List[Dict[str, Any]]: - """ - 获取设备校准历史 - - Args: - device_id: 设备ID - limit: 返回记录数量限制 - - Returns: - 校准历史列表 - """ - try: - history = self.calibration_history.get(device_id, []).copy() - if limit: - history = history[-limit:] - return history - except Exception as e: - print(f"✗ 校准历史获取失败: {e}") - return [] - - def clear_calibration_history(self, device_id: int = None): - """ - 清空校准历史 - - Args: - device_id: 设备ID(None表示清空所有) - """ - try: - if device_id is None: - self.calibration_history.clear() - print("✓ 所有校准历史已清空") - else: - if device_id in self.calibration_history: - del self.calibration_history[device_id] - print(f"✓ 设备校准历史已清空: ID {device_id}") - except Exception as e: - print(f"✗ 校准历史清空失败: {e}") - - def _trigger_calibration_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发校准回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.calibration_callbacks: - for callback in self.calibration_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 校准回调执行失败: {e}") - except Exception as e: - print(f"✗ 校准回调触发失败: {e}") - - def register_calibration_callback(self, callback_type: str, callback: Callable): - """ - 注册校准回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.calibration_callbacks: - self.calibration_callbacks[callback_type].append(callback) - print(f"✓ 校准回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 校准回调注册失败: {e}") - - def unregister_calibration_callback(self, callback_type: str, callback: Callable): - """ - 注销校准回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.calibration_callbacks: - if callback in self.calibration_callbacks[callback_type]: - self.calibration_callbacks[callback_type].remove(callback) - print(f"✓ 校准回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 校准回调注销失败: {e}") - - def set_calibration_config(self, config: Dict[str, Any]) -> bool: - """ - 设置校准配置 - - Args: - config: 校准配置字典 - - Returns: - 是否设置成功 - """ - try: - self.calibration_config.update(config) - print(f"✓ 校准配置已更新: {self.calibration_config}") - return True - except Exception as e: - print(f"✗ 校准配置设置失败: {e}") - return False - - def get_calibration_config(self) -> Dict[str, Any]: - """ - 获取校准配置 - - Returns: - 校准配置字典 - """ - return self.calibration_config.copy() - - def get_calibration_stats(self) -> Dict[str, int]: - """ - 获取校准统计信息 - - Returns: - 校准统计字典 - """ - return self.calibration_stats.copy() - - def reset_calibration_stats(self): - """重置校准统计信息""" - try: - self.calibration_stats = { - 'calibrations_performed': 0, - 'calibrations_successful': 0, - 'calibrations_failed': 0, - 'auto_calibrations': 0, - 'manual_calibrations': 0 - } - print("✓ 校准统计信息已重置") - except Exception as e: - print(f"✗ 校准统计信息重置失败: {e}") - - def is_device_calibrated(self, device_id: int) -> bool: - """ - 检查设备是否已校准 - - Args: - device_id: 设备ID - - Returns: - 是否已校准 - """ - try: - return device_id in self.calibration_data - except Exception as e: - print(f"✗ 设备校准状态检查失败: {e}") - return False - - def get_calibration_quality(self, device_id: int) -> float: - """ - 获取设备校准质量 - - Args: - device_id: 设备ID - - Returns: - 校准质量评分 (0.0-1.0) - """ - try: - if device_id not in self.calibration_data: - return 0.0 - - # 基于校准历史和统计数据计算质量评分 - history = self.calibration_history.get(device_id, []) - if not history: - return 0.5 # 默认评分 - - # 简化的质量评分计算 - recent_calibrations = min(5, len(history)) - quality_score = min(1.0, recent_calibrations / 5.0) - - return quality_score - - except Exception as e: - print(f"✗ 校准质量评估失败: {e}") +""" +设备校准模块 +负责处理各种输入设备的校准和配置 +""" + +import time +from typing import Dict, Any, List, Optional, Callable +import threading +import math + +class DeviceCalibrator: + """ + 设备校准器 + 负责处理各种输入设备的校准和配置 + """ + + def __init__(self, plugin): + """ + 初始化设备校准器 + + Args: + plugin: 多平台输入支持插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 校准配置 + self.calibration_config = { + 'auto_calibration': True, + 'calibration_interval': 300.0, # 5分钟 + 'sensitivity_threshold': 0.01, + 'deadzone_calibration': True, + 'range_calibration': True, + 'center_calibration': True, + 'drift_correction': True + } + + # 设备校准数据 + self.calibration_data = {} + + # 校准状态 + self.calibration_state = { + 'is_calibrating': False, + 'current_device': None, + 'calibration_step': 0, + 'total_steps': 0, + 'progress': 0.0 + } + + # 校准历史 + self.calibration_history = {} + + # 回调函数 + self.calibration_callbacks = { + 'calibration_started': [], + 'calibration_progress': [], + 'calibration_completed': [], + 'calibration_failed': [] + } + + # 统计信息 + self.calibration_stats = { + 'calibrations_performed': 0, + 'calibrations_successful': 0, + 'calibrations_failed': 0, + 'auto_calibrations': 0, + 'manual_calibrations': 0 + } + + # 线程管理 + self.calibration_thread = None + self.running = False + + # 时间戳记录 + self.last_auto_calibration = {} + self.calibration_start_time = 0.0 + + print("✓ 设备校准器已创建") + + def initialize(self) -> bool: + """ + 初始化设备校准器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 设备校准器初始化完成") + return True + + except Exception as e: + print(f"✗ 设备校准器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用设备校准器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 设备校准器未初始化") + return False + + self.enabled = True + self.running = True + + print("✓ 设备校准器已启用") + return True + + except Exception as e: + print(f"✗ 设备校准器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用设备校准器""" + try: + self.enabled = False + self.running = False + + # 停止正在进行的校准 + if self.calibration_state['is_calibrating']: + self.cancel_calibration() + + print("✓ 设备校准器已禁用") + + except Exception as e: + print(f"✗ 设备校准器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理设备校准器资源""" + try: + self.disable() + self.calibration_data.clear() + self.calibration_history.clear() + self.calibration_callbacks.clear() + self.initialized = False + print("✓ 设备校准器资源已清理") + + except Exception as e: + print(f"✗ 设备校准器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新设备校准器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 检查自动校准 + if self.calibration_config['auto_calibration']: + self._check_auto_calibration() + + except Exception as e: + print(f"✗ 设备校准器更新失败: {e}") + import traceback + traceback.print_exc() + + def _check_auto_calibration(self): + """检查自动校准""" + try: + current_time = time.time() + + # 检查是否需要自动校准 + if self.plugin.device_manager: + devices = self.plugin.device_manager.get_connected_devices() + for device_id, device_info in devices.items(): + last_calibration = self.last_auto_calibration.get(device_id, 0.0) + if current_time - last_calibration >= self.calibration_config['calibration_interval']: + # 执行自动校准 + self.start_auto_calibration(device_id) + self.calibration_stats['auto_calibrations'] += 1 + + except Exception as e: + print(f"✗ 自动校准检查失败: {e}") + + def start_calibration(self, device_id: int, calibration_type: str = "full") -> bool: + """ + 开始设备校准 + + Args: + device_id: 设备ID + calibration_type: 校准类型 ("full", "deadzone", "range", "center") + + Returns: + 是否开始成功 + """ + try: + if not self.enabled: + print("✗ 设备校准器未启用") + return False + + if self.calibration_state['is_calibrating']: + print("✗ 校准已在进行中") + return False + + # 检查设备是否存在 + if not self.plugin.device_manager or not self.plugin.device_manager.get_device(device_id): + print(f"✗ 设备不存在: ID {device_id}") + return False + + # 设置校准状态 + self.calibration_state['is_calibrating'] = True + self.calibration_state['current_device'] = device_id + self.calibration_state['calibration_step'] = 0 + + # 根据校准类型设置步骤数 + if calibration_type == "full": + self.calibration_state['total_steps'] = 4 + elif calibration_type in ["deadzone", "range", "center"]: + self.calibration_state['total_steps'] = 1 + else: + self.calibration_state['total_steps'] = 1 + + self.calibration_start_time = time.time() + + # 初始化校准数据 + if device_id not in self.calibration_data: + self.calibration_data[device_id] = {} + + self.calibration_data[device_id]['type'] = calibration_type + self.calibration_data[device_id]['start_time'] = self.calibration_start_time + self.calibration_data[device_id]['samples'] = [] + + # 触发开始回调 + self._trigger_calibration_callback('calibration_started', { + 'device_id': device_id, + 'calibration_type': calibration_type, + 'total_steps': self.calibration_state['total_steps'] + }) + + print(f"✓ 设备校准已开始: ID {device_id}, 类型 {calibration_type}") + self.calibration_stats['manual_calibrations'] += 1 + return True + + except Exception as e: + print(f"✗ 设备校准开始失败: {e}") + import traceback + traceback.print_exc() + return False + + def start_auto_calibration(self, device_id: int) -> bool: + """ + 开始自动校准 + + Args: + device_id: 设备ID + + Returns: + 是否开始成功 + """ + try: + result = self.start_calibration(device_id, "full") + if result: + print(f"✓ 自动校准已开始: ID {device_id}") + return result + except Exception as e: + print(f"✗ 自动校准开始失败: {e}") + return False + + def update_calibration(self, sample_data: Dict[str, Any]) -> bool: + """ + 更新校准数据 + + Args: + sample_data: 采样数据 + + Returns: + 是否更新成功 + """ + try: + if not self.calibration_state['is_calibrating']: + print("✗ 没有正在进行的校准") + return False + + device_id = self.calibration_state['current_device'] + if device_id not in self.calibration_data: + print("✗ 校准数据不存在") + return False + + # 添加采样数据 + self.calibration_data[device_id]['samples'].append(sample_data) + + # 更新进度 + self.calibration_state['calibration_step'] += 1 + progress = self.calibration_state['calibration_step'] / self.calibration_state['total_steps'] + self.calibration_state['progress'] = min(1.0, progress) + + # 触进进度回调 + self._trigger_calibration_callback('calibration_progress', { + 'device_id': device_id, + 'step': self.calibration_state['calibration_step'], + 'total_steps': self.calibration_state['total_steps'], + 'progress': self.calibration_state['progress'], + 'sample_data': sample_data + }) + + # 检查是否完成 + if self.calibration_state['calibration_step'] >= self.calibration_state['total_steps']: + self._complete_calibration() + + return True + + except Exception as e: + print(f"✗ 校准数据更新失败: {e}") + return False + + def _complete_calibration(self): + """完成校准""" + try: + device_id = self.calibration_state['current_device'] + calibration_data = self.calibration_data[device_id] + + # 分析采样数据 + calibration_result = self._analyze_calibration_samples(calibration_data) + + # 应用校准结果 + self._apply_calibration_result(device_id, calibration_result) + + # 记录校准历史 + self._record_calibration_history(device_id, calibration_result) + + # 更新时间戳 + self.last_auto_calibration[device_id] = time.time() + + # 重置校准状态 + self.calibration_state['is_calibrating'] = False + self.calibration_state['current_device'] = None + self.calibration_state['calibration_step'] = 0 + self.calibration_state['total_steps'] = 0 + self.calibration_state['progress'] = 0.0 + + # 更新统计信息 + self.calibration_stats['calibrations_performed'] += 1 + self.calibration_stats['calibrations_successful'] += 1 + + # 触发完成回调 + self._trigger_calibration_callback('calibration_completed', { + 'device_id': device_id, + 'result': calibration_result, + 'duration': time.time() - self.calibration_start_time + }) + + print(f"✓ 设备校准已完成: ID {device_id}") + + except Exception as e: + print(f"✗ 校准完成处理失败: {e}") + self._handle_calibration_failure() + + def _analyze_calibration_samples(self, calibration_data: Dict[str, Any]) -> Dict[str, Any]: + """ + 分析校准采样数据 + + Args: + calibration_data: 校准数据 + + Returns: + 分析结果 + """ + try: + samples = calibration_data.get('samples', []) + if not samples: + return {} + + result = { + 'sample_count': len(samples), + 'analysis_time': time.time() + } + + # 分析不同类型的输入数据 + axis_data = {} + button_data = {} + + for sample in samples: + for key, value in sample.items(): + if isinstance(value, (int, float)): + # 轴数据 + if key not in axis_data: + axis_data[key] = [] + axis_data[key].append(value) + elif isinstance(value, bool): + # 按钮数据 + if key not in button_data: + button_data[key] = [] + button_data[key].append(value) + + # 计算轴统计数据 + axis_stats = {} + for axis_name, values in axis_data.items(): + if values: + axis_stats[axis_name] = { + 'min': min(values), + 'max': max(values), + 'mean': sum(values) / len(values), + 'median': sorted(values)[len(values) // 2], + 'std_dev': math.sqrt(sum((x - sum(values) / len(values)) ** 2 for x in values) / len(values)) + } + + result['axis_stats'] = axis_stats + result['button_stats'] = button_data + + # 计算死区、范围和中心点 + deadzones = {} + ranges = {} + centers = {} + + for axis_name, stats in axis_stats.items(): + # 死区计算(基于标准差) + deadzones[axis_name] = max(self.calibration_config['sensitivity_threshold'], + stats['std_dev'] * 2) + + # 范围计算 + ranges[axis_name] = { + 'min': stats['min'], + 'max': stats['max'] + } + + # 中心点计算 + centers[axis_name] = stats['mean'] + + result['deadzones'] = deadzones + result['ranges'] = ranges + result['centers'] = centers + + return result + + except Exception as e: + print(f"✗ 校准数据分析失败: {e}") + return {} + + def _apply_calibration_result(self, device_id: int, calibration_result: Dict[str, Any]): + """ + 应用校准结果 + + Args: + device_id: 设备ID + calibration_result: 校准结果 + """ + try: + # 应用死区设置 + if 'deadzones' in calibration_result and self.calibration_config['deadzone_calibration']: + deadzones = calibration_result['deadzones'] + for axis_name, deadzone in deadzones.items(): + # 在实际实现中,这里会更新设备的死区设置 + print(f" 应用死区设置 {axis_name}: {deadzone}") + + # 应用范围设置 + if 'ranges' in calibration_result and self.calibration_config['range_calibration']: + ranges = calibration_result['ranges'] + for axis_name, range_data in ranges.items(): + # 在实际实现中,这里会更新设备的范围设置 + print(f" 应用范围设置 {axis_name}: {range_data}") + + # 应用中心点设置 + if 'centers' in calibration_result and self.calibration_config['center_calibration']: + centers = calibration_result['centers'] + for axis_name, center in centers.items(): + # 在实际实现中,这里会更新设备的中心点设置 + print(f" 应用中心点设置 {axis_name}: {center}") + + # 保存校准配置 + self._save_calibration_config(device_id, calibration_result) + + except Exception as e: + print(f"✗ 校准结果应用失败: {e}") + + def _save_calibration_config(self, device_id: int, calibration_result: Dict[str, Any]): + """ + 保存校准配置 + + Args: + device_id: 设备ID + calibration_result: 校准结果 + """ + try: + # 在实际实现中,这里会将校准结果保存到配置文件或数据库 + print(f" 校准配置已保存: 设备 {device_id}") + except Exception as e: + print(f"✗ 校准配置保存失败: {e}") + + def _record_calibration_history(self, device_id: int, calibration_result: Dict[str, Any]): + """ + 记录校准历史 + + Args: + device_id: 设备ID + calibration_result: 校准结果 + """ + try: + if device_id not in self.calibration_history: + self.calibration_history[device_id] = [] + + history_entry = { + 'timestamp': time.time(), + 'result': calibration_result, + 'duration': time.time() - self.calibration_start_time + } + + self.calibration_history[device_id].append(history_entry) + + # 保持历史记录大小 + if len(self.calibration_history[device_id]) > 50: + self.calibration_history[device_id].pop(0) + + except Exception as e: + print(f"✗ 校准历史记录失败: {e}") + + def _handle_calibration_failure(self): + """处理校准失败""" + try: + device_id = self.calibration_state['current_device'] + + # 重置校准状态 + self.calibration_state['is_calibrating'] = False + self.calibration_state['current_device'] = None + self.calibration_state['calibration_step'] = 0 + self.calibration_state['total_steps'] = 0 + self.calibration_state['progress'] = 0.0 + + # 更新统计信息 + self.calibration_stats['calibrations_performed'] += 1 + self.calibration_stats['calibrations_failed'] += 1 + + # 触发失败回调 + self._trigger_calibration_callback('calibration_failed', { + 'device_id': device_id, + 'error': 'Calibration process failed' + }) + + print(f"✗ 设备校准失败: ID {device_id}") + + except Exception as e: + print(f"✗ 校准失败处理失败: {e}") + + def cancel_calibration(self) -> bool: + """ + 取消校准 + + Returns: + 是否取消成功 + """ + try: + if not self.calibration_state['is_calibrating']: + print("✗ 没有正在进行的校准") + return False + + device_id = self.calibration_state['current_device'] + + # 重置校准状态 + self.calibration_state['is_calibrating'] = False + self.calibration_state['current_device'] = None + self.calibration_state['calibration_step'] = 0 + self.calibration_state['total_steps'] = 0 + self.calibration_state['progress'] = 0.0 + + print(f"✓ 设备校准已取消: ID {device_id}") + return True + + except Exception as e: + print(f"✗ 校准取消失败: {e}") + return False + + def get_calibration_status(self) -> Dict[str, Any]: + """ + 获取校准状态 + + Returns: + 校准状态字典 + """ + return self.calibration_state.copy() + + def get_calibration_data(self, device_id: int) -> Optional[Dict[str, Any]]: + """ + 获取设备校准数据 + + Args: + device_id: 设备ID + + Returns: + 校准数据字典或None + """ + return self.calibration_data.get(device_id) + + def get_calibration_history(self, device_id: int, limit: int = None) -> List[Dict[str, Any]]: + """ + 获取设备校准历史 + + Args: + device_id: 设备ID + limit: 返回记录数量限制 + + Returns: + 校准历史列表 + """ + try: + history = self.calibration_history.get(device_id, []).copy() + if limit: + history = history[-limit:] + return history + except Exception as e: + print(f"✗ 校准历史获取失败: {e}") + return [] + + def clear_calibration_history(self, device_id: int = None): + """ + 清空校准历史 + + Args: + device_id: 设备ID(None表示清空所有) + """ + try: + if device_id is None: + self.calibration_history.clear() + print("✓ 所有校准历史已清空") + else: + if device_id in self.calibration_history: + del self.calibration_history[device_id] + print(f"✓ 设备校准历史已清空: ID {device_id}") + except Exception as e: + print(f"✗ 校准历史清空失败: {e}") + + def _trigger_calibration_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发校准回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.calibration_callbacks: + for callback in self.calibration_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 校准回调执行失败: {e}") + except Exception as e: + print(f"✗ 校准回调触发失败: {e}") + + def register_calibration_callback(self, callback_type: str, callback: Callable): + """ + 注册校准回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.calibration_callbacks: + self.calibration_callbacks[callback_type].append(callback) + print(f"✓ 校准回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 校准回调注册失败: {e}") + + def unregister_calibration_callback(self, callback_type: str, callback: Callable): + """ + 注销校准回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.calibration_callbacks: + if callback in self.calibration_callbacks[callback_type]: + self.calibration_callbacks[callback_type].remove(callback) + print(f"✓ 校准回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 校准回调注销失败: {e}") + + def set_calibration_config(self, config: Dict[str, Any]) -> bool: + """ + 设置校准配置 + + Args: + config: 校准配置字典 + + Returns: + 是否设置成功 + """ + try: + self.calibration_config.update(config) + print(f"✓ 校准配置已更新: {self.calibration_config}") + return True + except Exception as e: + print(f"✗ 校准配置设置失败: {e}") + return False + + def get_calibration_config(self) -> Dict[str, Any]: + """ + 获取校准配置 + + Returns: + 校准配置字典 + """ + return self.calibration_config.copy() + + def get_calibration_stats(self) -> Dict[str, int]: + """ + 获取校准统计信息 + + Returns: + 校准统计字典 + """ + return self.calibration_stats.copy() + + def reset_calibration_stats(self): + """重置校准统计信息""" + try: + self.calibration_stats = { + 'calibrations_performed': 0, + 'calibrations_successful': 0, + 'calibrations_failed': 0, + 'auto_calibrations': 0, + 'manual_calibrations': 0 + } + print("✓ 校准统计信息已重置") + except Exception as e: + print(f"✗ 校准统计信息重置失败: {e}") + + def is_device_calibrated(self, device_id: int) -> bool: + """ + 检查设备是否已校准 + + Args: + device_id: 设备ID + + Returns: + 是否已校准 + """ + try: + return device_id in self.calibration_data + except Exception as e: + print(f"✗ 设备校准状态检查失败: {e}") + return False + + def get_calibration_quality(self, device_id: int) -> float: + """ + 获取设备校准质量 + + Args: + device_id: 设备ID + + Returns: + 校准质量评分 (0.0-1.0) + """ + try: + if device_id not in self.calibration_data: + return 0.0 + + # 基于校准历史和统计数据计算质量评分 + history = self.calibration_history.get(device_id, []) + if not history: + return 0.5 # 默认评分 + + # 简化的质量评分计算 + recent_calibrations = min(5, len(history)) + quality_score = min(1.0, recent_calibrations / 5.0) + + return quality_score + + except Exception as e: + print(f"✗ 校准质量评估失败: {e}") return 0.0 \ No newline at end of file diff --git a/plugins/user/multiplatform_input/devices/device_manager.py b/plugins/user/multiplatform_input/devices/device_manager.py index fc5285d7..39afdfda 100644 --- a/plugins/user/multiplatform_input/devices/device_manager.py +++ b/plugins/user/multiplatform_input/devices/device_manager.py @@ -1,734 +1,734 @@ -""" -设备管理器模块 -负责发现、管理和配置各种输入设备 -""" - -import time -from typing import Dict, Any, List, Optional, Union -import platform -import threading - -class DeviceType: - """设备类型常量""" - KEYBOARD = "keyboard" - MOUSE = "mouse" - TOUCHSCREEN = "touchscreen" - GAMEPAD = "gamepad" - VR_CONTROLLER = "vr_controller" - MOTION_SENSOR = "motion_sensor" - CUSTOM = "custom" - -class ConnectionType: - """连接类型常量""" - USB = "usb" - BLUETOOTH = "bluetooth" - WIRELESS = "wireless" - BUILT_IN = "built_in" - NETWORK = "network" - -class DeviceManager: - """ - 设备管理器 - 负责发现、管理和配置各种输入设备 - """ - - def __init__(self, plugin): - """ - 初始化设备管理器 - - Args: - plugin: 多平台输入支持插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 设备字典 - self.devices = {} - self.device_counter = 0 - - # 设备发现配置 - self.discovery_config = { - 'auto_discovery': True, - 'discovery_interval': 5.0, # 设备发现间隔(秒) - 'supported_platforms': ['Windows', 'Linux', 'Darwin'], - 'supported_devices': [ - DeviceType.KEYBOARD, - DeviceType.MOUSE, - DeviceType.TOUCHSCREEN, - DeviceType.GAMEPAD, - DeviceType.VR_CONTROLLER, - DeviceType.MOTION_SENSOR - ] - } - - # 平台特定配置 - self.platform_config = { - 'Windows': { - 'api': 'directinput', - 'libraries': ['pydirectinput', 'pynput'], - 'features': ['xinput', 'directinput'] - }, - 'Linux': { - 'api': 'evdev', - 'libraries': ['evdev', 'pyxhook'], - 'features': ['udev', 'x11'] - }, - 'Darwin': { # macOS - 'api': 'iokit', - 'libraries': ['pyobjc', 'pynput'], - 'features': ['coregraphics', 'iokit'] - } - } - - # 设备功能映射 - self.device_capabilities = { - DeviceType.KEYBOARD: [ - 'key_press', 'key_release', 'key_repeat', - 'modifier_keys', 'function_keys' - ], - DeviceType.MOUSE: [ - 'button_press', 'button_release', 'movement', - 'wheel_scroll', 'double_click', 'drag' - ], - DeviceType.TOUCHSCREEN: [ - 'touch_start', 'touch_move', 'touch_end', - 'multi_touch', 'gestures', 'pressure_sensitivity' - ], - DeviceType.GAMEPAD: [ - 'button_press', 'button_release', 'analog_stick', - 'trigger', 'rumble', 'gyro' - ], - DeviceType.VR_CONTROLLER: [ - 'button_press', 'button_release', 'analog_stick', - 'trigger', 'haptic_feedback', 'position_tracking', - 'rotation_tracking', 'gesture_recognition' - ], - DeviceType.MOTION_SENSOR: [ - 'accelerometer', 'gyroscope', 'magnetometer', - 'orientation', 'motion_detection' - ] - } - - # 已连接设备 - self.connected_devices = {} - - # 设备状态 - self.device_states = { - 'scanning': False, - 'last_scan': 0.0, - 'devices_found': 0 - } - - # 统计信息 - self.stats = { - 'devices_registered': 0, - 'devices_connected': 0, - 'devices_disconnected': 0, - 'scan_operations': 0, - 'connection_errors': 0 - } - - # 线程管理 - self.discovery_thread = None - self.running = False - - # 回调函数 - self.device_callbacks = { - 'device_connected': [], - 'device_disconnected': [], - 'device_updated': [] - } - - print("✓ 设备管理器已创建") - - def initialize(self) -> bool: - """ - 初始化设备管理器 - - Returns: - 是否初始化成功 - """ - try: - # 检查平台支持 - current_platform = platform.system() - if current_platform not in self.discovery_config['supported_platforms']: - print(f"⚠ 平台未完全支持: {current_platform}") - - # 初始化平台特定功能 - self._initialize_platform_features() - - # 创建初始设备 - self._create_default_devices() - - self.initialized = True - print("✓ 设备管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 设备管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用设备管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 设备管理器未初始化") - return False - - self.enabled = True - self.running = True - - # 启动设备发现线程 - if self.discovery_config['auto_discovery']: - self.discovery_thread = threading.Thread(target=self._discovery_loop, daemon=True) - self.discovery_thread.start() - - print("✓ 设备管理器已启用") - return True - - except Exception as e: - print(f"✗ 设备管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用设备管理器""" - try: - self.enabled = False - self.running = False - - # 等待发现线程结束 - if self.discovery_thread and self.discovery_thread.is_alive(): - self.discovery_thread.join(timeout=2.0) - - print("✓ 设备管理器已禁用") - - except Exception as e: - print(f"✗ 设备管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理设备管理器资源""" - try: - self.disable() - self.devices.clear() - self.connected_devices.clear() - self.device_states.clear() - self.device_callbacks.clear() - self.initialized = False - print("✓ 设备管理器资源已清理") - - except Exception as e: - print(f"✗ 设备管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新设备管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新设备状态 - self._update_device_states() - - # 更新统计信息 - self.stats['devices_connected'] = len(self.connected_devices) - - except Exception as e: - print(f"✗ 设备管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _initialize_platform_features(self): - """初始化平台特定功能""" - try: - current_platform = platform.system() - platform_info = self.platform_config.get(current_platform, {}) - - print(f"✓ 平台初始化: {current_platform}") - print(f" API: {platform_info.get('api', 'unknown')}") - print(f" 特性: {', '.join(platform_info.get('features', []))}") - - except Exception as e: - print(f"✗ 平台功能初始化失败: {e}") - - def _create_default_devices(self): - """创建默认设备""" - try: - current_platform = platform.system() - - # 创建默认键盘 - keyboard_id = self.register_device( - device_type=DeviceType.KEYBOARD, - name=f"系统键盘 ({current_platform})", - connection_type=ConnectionType.BUILT_IN, - vendor_id="system", - product_id="keyboard" - ) - - # 创建默认鼠标 - mouse_id = self.register_device( - device_type=DeviceType.MOUSE, - name=f"系统鼠标 ({current_platform})", - connection_type=ConnectionType.BUILT_IN, - vendor_id="system", - product_id="mouse" - ) - - # 标记为已连接 - if keyboard_id >= 0: - self.connect_device(keyboard_id) - if mouse_id >= 0: - self.connect_device(mouse_id) - - print("✓ 默认设备创建完成") - - except Exception as e: - print(f"✗ 默认设备创建失败: {e}") - - def _discovery_loop(self): - """设备发现循环""" - try: - discovery_interval = self.discovery_config['discovery_interval'] - - while self.running: - if self.enabled and self.discovery_config['auto_discovery']: - self.scan_for_devices() - - time.sleep(discovery_interval) - - except Exception as e: - print(f"✗ 设备发现循环失败: {e}") - - def _update_device_states(self): - """更新设备状态""" - try: - current_time = time.time() - - # 检查已连接设备的状态 - devices_to_disconnect = [] - for device_id, device_info in self.connected_devices.items(): - # 检查设备是否仍然连接 - if not self._is_device_connected(device_id): - devices_to_disconnect.append(device_id) - - # 断开已丢失的设备 - for device_id in devices_to_disconnect: - self.disconnect_device(device_id) - - except Exception as e: - print(f"✗ 设备状态更新失败: {e}") - - def _is_device_connected(self, device_id: int) -> bool: - """ - 检查设备是否仍然连接 - - Args: - device_id: 设备ID - - Returns: - 是否连接 - """ - try: - # 这里应该实现实际的设备连接检查逻辑 - # 简化处理,假设设备一直连接 - return True - except Exception as e: - print(f"✗ 设备连接检查失败: {e}") - return True - - def register_device(self, device_type: str, name: str, connection_type: str, - vendor_id: str, product_id: str, - capabilities: List[str] = None) -> int: - """ - 注册设备 - - Args: - device_type: 设备类型 - name: 设备名称 - connection_type: 连接类型 - vendor_id: 厂商ID - product_id: 产品ID - capabilities: 设备功能列表 - - Returns: - 设备ID - """ - try: - device_id = self.device_counter - self.device_counter += 1 - - # 如果未提供功能列表,使用默认功能 - if capabilities is None: - capabilities = self.device_capabilities.get(device_type, []) - - device_info = { - 'id': device_id, - 'type': device_type, - 'name': name, - 'connection_type': connection_type, - 'vendor_id': vendor_id, - 'product_id': product_id, - 'capabilities': capabilities, - 'registered_time': time.time(), - 'connected': False, - 'last_seen': 0.0, - 'battery_level': 100.0, # 电池电量百分比 - 'signal_strength': 100.0, # 信号强度百分比 - 'firmware_version': '1.0.0', - 'hardware_version': '1.0.0' - } - - self.devices[device_id] = device_info - self.stats['devices_registered'] += 1 - - print(f"✓ 设备已注册: {name} (ID: {device_id})") - return device_id - - except Exception as e: - print(f"✗ 设备注册失败: {e}") - return -1 - - def unregister_device(self, device_id: int) -> bool: - """ - 注销设备 - - Args: - device_id: 设备ID - - Returns: - 是否注销成功 - """ - try: - if device_id in self.devices: - device_info = self.devices[device_id] - device_name = device_info['name'] - - # 如果设备已连接,先断开连接 - if device_id in self.connected_devices: - self.disconnect_device(device_id) - - del self.devices[device_id] - print(f"✓ 设备已注销: {device_name} (ID: {device_id})") - return True - else: - print(f"✗ 设备不存在: ID {device_id}") - return False - - except Exception as e: - print(f"✗ 设备注销失败: {e}") - return False - - def connect_device(self, device_id: int) -> bool: - """ - 连接设备 - - Args: - device_id: 设备ID - - Returns: - 是否连接成功 - """ - try: - if device_id not in self.devices: - print(f"✗ 设备不存在: ID {device_id}") - return False - - device_info = self.devices[device_id] - if device_info['connected']: - print(f"⚠ 设备已连接: {device_info['name']} (ID: {device_id})") - return True - - # 模拟设备连接过程 - device_info['connected'] = True - device_info['last_seen'] = time.time() - self.connected_devices[device_id] = device_info - - # 触发连接回调 - self._trigger_device_callback('device_connected', { - 'device_id': device_id, - 'device_info': device_info - }) - - self.stats['devices_connected'] += 1 - print(f"✓ 设备已连接: {device_info['name']} (ID: {device_id})") - return True - - except Exception as e: - print(f"✗ 设备连接失败: {e}") - self.stats['connection_errors'] += 1 - return False - - def disconnect_device(self, device_id: int) -> bool: - """ - 断开设备连接 - - Args: - device_id: 设备ID - - Returns: - 是否断开成功 - """ - try: - if device_id not in self.devices: - print(f"✗ 设备不存在: ID {device_id}") - return False - - device_info = self.devices[device_id] - if not device_info['connected']: - print(f"⚠ 设备未连接: {device_info['name']} (ID: {device_id})") - return True - - # 断开设备连接 - device_info['connected'] = False - if device_id in self.connected_devices: - del self.connected_devices[device_id] - - # 触发断开回调 - self._trigger_device_callback('device_disconnected', { - 'device_id': device_id, - 'device_info': device_info - }) - - self.stats['devices_disconnected'] += 1 - print(f"✓ 设备已断开: {device_info['name']} (ID: {device_id})") - return True - - except Exception as e: - print(f"✗ 设备断开失败: {e}") - return False - - def scan_for_devices(self) -> List[int]: - """ - 扫描设备 - - Returns: - 发现的设备ID列表 - """ - try: - self.device_states['scanning'] = True - self.device_states['last_scan'] = time.time() - - discovered_devices = [] - - # 模拟设备发现过程 - # 在实际实现中,这里会根据平台调用相应的API来发现设备 - - # 检查现有设备的状态 - for device_id, device_info in self.devices.items(): - # 模拟设备发现 - if not device_info['connected']: - # 有一定概率"发现"设备 - import random - if random.random() < 0.1: # 10%概率 - discovered_devices.append(device_id) - - self.device_states['devices_found'] = len(discovered_devices) - self.stats['scan_operations'] += 1 - - if discovered_devices: - print(f"✓ 发现 {len(discovered_devices)} 个设备") - - return discovered_devices - - except Exception as e: - print(f"✗ 设备扫描失败: {e}") - return [] - finally: - self.device_states['scanning'] = False - - def get_device(self, device_id: int) -> Optional[Dict[str, Any]]: - """ - 获取设备信息 - - Args: - device_id: 设备ID - - Returns: - 设备信息字典或None - """ - return self.devices.get(device_id) - - def get_connected_devices(self) -> Dict[int, Dict[str, Any]]: - """ - 获取所有已连接的设备 - - Returns: - 已连接设备字典 - """ - return self.connected_devices.copy() - - def get_devices_by_type(self, device_type: str) -> List[Dict[str, Any]]: - """ - 根据类型获取设备列表 - - Args: - device_type: 设备类型 - - Returns: - 设备信息列表 - """ - try: - devices = [] - for device_info in self.devices.values(): - if device_info['type'] == device_type: - devices.append(device_info) - return devices - except Exception as e: - print(f"✗ 按类型获取设备失败: {e}") - return [] - - def update_device_battery(self, device_id: int, battery_level: float): - """ - 更新设备电池电量 - - Args: - device_id: 设备ID - battery_level: 电池电量百分比(0-100) - """ - try: - if device_id in self.devices: - self.devices[device_id]['battery_level'] = max(0.0, min(100.0, battery_level)) - except Exception as e: - print(f"✗ 设备电池电量更新失败: {e}") - - def update_device_signal(self, device_id: int, signal_strength: float): - """ - 更新设备信号强度 - - Args: - device_id: 设备ID - signal_strength: 信号强度百分比(0-100) - """ - try: - if device_id in self.devices: - self.devices[device_id]['signal_strength'] = max(0.0, min(100.0, signal_strength)) - except Exception as e: - print(f"✗ 设备信号强度更新失败: {e}") - - def register_device_callback(self, callback_type: str, callback: callable): - """ - 注册设备回调 - - Args: - callback_type: 回调类型('device_connected', 'device_disconnected', 'device_updated') - callback: 回调函数 - """ - try: - if callback_type in self.device_callbacks: - self.device_callbacks[callback_type].append(callback) - print(f"✓ 设备回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 设备回调注册失败: {e}") - - def _trigger_device_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发设备回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.device_callbacks: - for callback in self.device_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 设备回调执行失败: {e}") - except Exception as e: - print(f"✗ 设备回调触发失败: {e}") - - def get_device_capabilities(self, device_type: str) -> List[str]: - """ - 获取设备类型的功能列表 - - Args: - device_type: 设备类型 - - Returns: - 功能列表 - """ - return self.device_capabilities.get(device_type, []) - - def get_platform_config(self) -> Dict[str, Any]: - """ - 获取平台配置 - - Returns: - 平台配置字典 - """ - current_platform = platform.system() - return self.platform_config.get(current_platform, {}) - - def set_discovery_config(self, config: Dict[str, Any]): - """ - 设置设备发现配置 - - Args: - config: 发现配置字典 - """ - try: - self.discovery_config.update(config) - print(f"✓ 设备发现配置已更新: {self.discovery_config}") - except Exception as e: - print(f"✗ 设备发现配置设置失败: {e}") - - def get_discovery_config(self) -> Dict[str, Any]: - """ - 获取设备发现配置 - - Returns: - 发现配置字典 - """ - return self.discovery_config.copy() - - def get_device_stats(self) -> Dict[str, Any]: - """ - 获取设备统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_device_stats(self): - """重置设备统计信息""" - try: - self.stats = { - 'devices_registered': 0, - 'devices_connected': 0, - 'devices_disconnected': 0, - 'scan_operations': 0, - 'connection_errors': 0 - } - print("✓ 设备统计信息已重置") - except Exception as e: - print(f"✗ 设备统计信息重置失败: {e}") - - def get_device_states(self) -> Dict[str, Any]: - """ - 获取设备状态 - - Returns: - 设备状态字典 - """ +""" +设备管理器模块 +负责发现、管理和配置各种输入设备 +""" + +import time +from typing import Dict, Any, List, Optional, Union +import platform +import threading + +class DeviceType: + """设备类型常量""" + KEYBOARD = "keyboard" + MOUSE = "mouse" + TOUCHSCREEN = "touchscreen" + GAMEPAD = "gamepad" + VR_CONTROLLER = "vr_controller" + MOTION_SENSOR = "motion_sensor" + CUSTOM = "custom" + +class ConnectionType: + """连接类型常量""" + USB = "usb" + BLUETOOTH = "bluetooth" + WIRELESS = "wireless" + BUILT_IN = "built_in" + NETWORK = "network" + +class DeviceManager: + """ + 设备管理器 + 负责发现、管理和配置各种输入设备 + """ + + def __init__(self, plugin): + """ + 初始化设备管理器 + + Args: + plugin: 多平台输入支持插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 设备字典 + self.devices = {} + self.device_counter = 0 + + # 设备发现配置 + self.discovery_config = { + 'auto_discovery': True, + 'discovery_interval': 5.0, # 设备发现间隔(秒) + 'supported_platforms': ['Windows', 'Linux', 'Darwin'], + 'supported_devices': [ + DeviceType.KEYBOARD, + DeviceType.MOUSE, + DeviceType.TOUCHSCREEN, + DeviceType.GAMEPAD, + DeviceType.VR_CONTROLLER, + DeviceType.MOTION_SENSOR + ] + } + + # 平台特定配置 + self.platform_config = { + 'Windows': { + 'api': 'directinput', + 'libraries': ['pydirectinput', 'pynput'], + 'features': ['xinput', 'directinput'] + }, + 'Linux': { + 'api': 'evdev', + 'libraries': ['evdev', 'pyxhook'], + 'features': ['udev', 'x11'] + }, + 'Darwin': { # macOS + 'api': 'iokit', + 'libraries': ['pyobjc', 'pynput'], + 'features': ['coregraphics', 'iokit'] + } + } + + # 设备功能映射 + self.device_capabilities = { + DeviceType.KEYBOARD: [ + 'key_press', 'key_release', 'key_repeat', + 'modifier_keys', 'function_keys' + ], + DeviceType.MOUSE: [ + 'button_press', 'button_release', 'movement', + 'wheel_scroll', 'double_click', 'drag' + ], + DeviceType.TOUCHSCREEN: [ + 'touch_start', 'touch_move', 'touch_end', + 'multi_touch', 'gestures', 'pressure_sensitivity' + ], + DeviceType.GAMEPAD: [ + 'button_press', 'button_release', 'analog_stick', + 'trigger', 'rumble', 'gyro' + ], + DeviceType.VR_CONTROLLER: [ + 'button_press', 'button_release', 'analog_stick', + 'trigger', 'haptic_feedback', 'position_tracking', + 'rotation_tracking', 'gesture_recognition' + ], + DeviceType.MOTION_SENSOR: [ + 'accelerometer', 'gyroscope', 'magnetometer', + 'orientation', 'motion_detection' + ] + } + + # 已连接设备 + self.connected_devices = {} + + # 设备状态 + self.device_states = { + 'scanning': False, + 'last_scan': 0.0, + 'devices_found': 0 + } + + # 统计信息 + self.stats = { + 'devices_registered': 0, + 'devices_connected': 0, + 'devices_disconnected': 0, + 'scan_operations': 0, + 'connection_errors': 0 + } + + # 线程管理 + self.discovery_thread = None + self.running = False + + # 回调函数 + self.device_callbacks = { + 'device_connected': [], + 'device_disconnected': [], + 'device_updated': [] + } + + print("✓ 设备管理器已创建") + + def initialize(self) -> bool: + """ + 初始化设备管理器 + + Returns: + 是否初始化成功 + """ + try: + # 检查平台支持 + current_platform = platform.system() + if current_platform not in self.discovery_config['supported_platforms']: + print(f"⚠ 平台未完全支持: {current_platform}") + + # 初始化平台特定功能 + self._initialize_platform_features() + + # 创建初始设备 + self._create_default_devices() + + self.initialized = True + print("✓ 设备管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 设备管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用设备管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 设备管理器未初始化") + return False + + self.enabled = True + self.running = True + + # 启动设备发现线程 + if self.discovery_config['auto_discovery']: + self.discovery_thread = threading.Thread(target=self._discovery_loop, daemon=True) + self.discovery_thread.start() + + print("✓ 设备管理器已启用") + return True + + except Exception as e: + print(f"✗ 设备管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用设备管理器""" + try: + self.enabled = False + self.running = False + + # 等待发现线程结束 + if self.discovery_thread and self.discovery_thread.is_alive(): + self.discovery_thread.join(timeout=2.0) + + print("✓ 设备管理器已禁用") + + except Exception as e: + print(f"✗ 设备管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理设备管理器资源""" + try: + self.disable() + self.devices.clear() + self.connected_devices.clear() + self.device_states.clear() + self.device_callbacks.clear() + self.initialized = False + print("✓ 设备管理器资源已清理") + + except Exception as e: + print(f"✗ 设备管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新设备管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新设备状态 + self._update_device_states() + + # 更新统计信息 + self.stats['devices_connected'] = len(self.connected_devices) + + except Exception as e: + print(f"✗ 设备管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _initialize_platform_features(self): + """初始化平台特定功能""" + try: + current_platform = platform.system() + platform_info = self.platform_config.get(current_platform, {}) + + print(f"✓ 平台初始化: {current_platform}") + print(f" API: {platform_info.get('api', 'unknown')}") + print(f" 特性: {', '.join(platform_info.get('features', []))}") + + except Exception as e: + print(f"✗ 平台功能初始化失败: {e}") + + def _create_default_devices(self): + """创建默认设备""" + try: + current_platform = platform.system() + + # 创建默认键盘 + keyboard_id = self.register_device( + device_type=DeviceType.KEYBOARD, + name=f"系统键盘 ({current_platform})", + connection_type=ConnectionType.BUILT_IN, + vendor_id="system", + product_id="keyboard" + ) + + # 创建默认鼠标 + mouse_id = self.register_device( + device_type=DeviceType.MOUSE, + name=f"系统鼠标 ({current_platform})", + connection_type=ConnectionType.BUILT_IN, + vendor_id="system", + product_id="mouse" + ) + + # 标记为已连接 + if keyboard_id >= 0: + self.connect_device(keyboard_id) + if mouse_id >= 0: + self.connect_device(mouse_id) + + print("✓ 默认设备创建完成") + + except Exception as e: + print(f"✗ 默认设备创建失败: {e}") + + def _discovery_loop(self): + """设备发现循环""" + try: + discovery_interval = self.discovery_config['discovery_interval'] + + while self.running: + if self.enabled and self.discovery_config['auto_discovery']: + self.scan_for_devices() + + time.sleep(discovery_interval) + + except Exception as e: + print(f"✗ 设备发现循环失败: {e}") + + def _update_device_states(self): + """更新设备状态""" + try: + current_time = time.time() + + # 检查已连接设备的状态 + devices_to_disconnect = [] + for device_id, device_info in self.connected_devices.items(): + # 检查设备是否仍然连接 + if not self._is_device_connected(device_id): + devices_to_disconnect.append(device_id) + + # 断开已丢失的设备 + for device_id in devices_to_disconnect: + self.disconnect_device(device_id) + + except Exception as e: + print(f"✗ 设备状态更新失败: {e}") + + def _is_device_connected(self, device_id: int) -> bool: + """ + 检查设备是否仍然连接 + + Args: + device_id: 设备ID + + Returns: + 是否连接 + """ + try: + # 这里应该实现实际的设备连接检查逻辑 + # 简化处理,假设设备一直连接 + return True + except Exception as e: + print(f"✗ 设备连接检查失败: {e}") + return True + + def register_device(self, device_type: str, name: str, connection_type: str, + vendor_id: str, product_id: str, + capabilities: List[str] = None) -> int: + """ + 注册设备 + + Args: + device_type: 设备类型 + name: 设备名称 + connection_type: 连接类型 + vendor_id: 厂商ID + product_id: 产品ID + capabilities: 设备功能列表 + + Returns: + 设备ID + """ + try: + device_id = self.device_counter + self.device_counter += 1 + + # 如果未提供功能列表,使用默认功能 + if capabilities is None: + capabilities = self.device_capabilities.get(device_type, []) + + device_info = { + 'id': device_id, + 'type': device_type, + 'name': name, + 'connection_type': connection_type, + 'vendor_id': vendor_id, + 'product_id': product_id, + 'capabilities': capabilities, + 'registered_time': time.time(), + 'connected': False, + 'last_seen': 0.0, + 'battery_level': 100.0, # 电池电量百分比 + 'signal_strength': 100.0, # 信号强度百分比 + 'firmware_version': '1.0.0', + 'hardware_version': '1.0.0' + } + + self.devices[device_id] = device_info + self.stats['devices_registered'] += 1 + + print(f"✓ 设备已注册: {name} (ID: {device_id})") + return device_id + + except Exception as e: + print(f"✗ 设备注册失败: {e}") + return -1 + + def unregister_device(self, device_id: int) -> bool: + """ + 注销设备 + + Args: + device_id: 设备ID + + Returns: + 是否注销成功 + """ + try: + if device_id in self.devices: + device_info = self.devices[device_id] + device_name = device_info['name'] + + # 如果设备已连接,先断开连接 + if device_id in self.connected_devices: + self.disconnect_device(device_id) + + del self.devices[device_id] + print(f"✓ 设备已注销: {device_name} (ID: {device_id})") + return True + else: + print(f"✗ 设备不存在: ID {device_id}") + return False + + except Exception as e: + print(f"✗ 设备注销失败: {e}") + return False + + def connect_device(self, device_id: int) -> bool: + """ + 连接设备 + + Args: + device_id: 设备ID + + Returns: + 是否连接成功 + """ + try: + if device_id not in self.devices: + print(f"✗ 设备不存在: ID {device_id}") + return False + + device_info = self.devices[device_id] + if device_info['connected']: + print(f"⚠ 设备已连接: {device_info['name']} (ID: {device_id})") + return True + + # 模拟设备连接过程 + device_info['connected'] = True + device_info['last_seen'] = time.time() + self.connected_devices[device_id] = device_info + + # 触发连接回调 + self._trigger_device_callback('device_connected', { + 'device_id': device_id, + 'device_info': device_info + }) + + self.stats['devices_connected'] += 1 + print(f"✓ 设备已连接: {device_info['name']} (ID: {device_id})") + return True + + except Exception as e: + print(f"✗ 设备连接失败: {e}") + self.stats['connection_errors'] += 1 + return False + + def disconnect_device(self, device_id: int) -> bool: + """ + 断开设备连接 + + Args: + device_id: 设备ID + + Returns: + 是否断开成功 + """ + try: + if device_id not in self.devices: + print(f"✗ 设备不存在: ID {device_id}") + return False + + device_info = self.devices[device_id] + if not device_info['connected']: + print(f"⚠ 设备未连接: {device_info['name']} (ID: {device_id})") + return True + + # 断开设备连接 + device_info['connected'] = False + if device_id in self.connected_devices: + del self.connected_devices[device_id] + + # 触发断开回调 + self._trigger_device_callback('device_disconnected', { + 'device_id': device_id, + 'device_info': device_info + }) + + self.stats['devices_disconnected'] += 1 + print(f"✓ 设备已断开: {device_info['name']} (ID: {device_id})") + return True + + except Exception as e: + print(f"✗ 设备断开失败: {e}") + return False + + def scan_for_devices(self) -> List[int]: + """ + 扫描设备 + + Returns: + 发现的设备ID列表 + """ + try: + self.device_states['scanning'] = True + self.device_states['last_scan'] = time.time() + + discovered_devices = [] + + # 模拟设备发现过程 + # 在实际实现中,这里会根据平台调用相应的API来发现设备 + + # 检查现有设备的状态 + for device_id, device_info in self.devices.items(): + # 模拟设备发现 + if not device_info['connected']: + # 有一定概率"发现"设备 + import random + if random.random() < 0.1: # 10%概率 + discovered_devices.append(device_id) + + self.device_states['devices_found'] = len(discovered_devices) + self.stats['scan_operations'] += 1 + + if discovered_devices: + print(f"✓ 发现 {len(discovered_devices)} 个设备") + + return discovered_devices + + except Exception as e: + print(f"✗ 设备扫描失败: {e}") + return [] + finally: + self.device_states['scanning'] = False + + def get_device(self, device_id: int) -> Optional[Dict[str, Any]]: + """ + 获取设备信息 + + Args: + device_id: 设备ID + + Returns: + 设备信息字典或None + """ + return self.devices.get(device_id) + + def get_connected_devices(self) -> Dict[int, Dict[str, Any]]: + """ + 获取所有已连接的设备 + + Returns: + 已连接设备字典 + """ + return self.connected_devices.copy() + + def get_devices_by_type(self, device_type: str) -> List[Dict[str, Any]]: + """ + 根据类型获取设备列表 + + Args: + device_type: 设备类型 + + Returns: + 设备信息列表 + """ + try: + devices = [] + for device_info in self.devices.values(): + if device_info['type'] == device_type: + devices.append(device_info) + return devices + except Exception as e: + print(f"✗ 按类型获取设备失败: {e}") + return [] + + def update_device_battery(self, device_id: int, battery_level: float): + """ + 更新设备电池电量 + + Args: + device_id: 设备ID + battery_level: 电池电量百分比(0-100) + """ + try: + if device_id in self.devices: + self.devices[device_id]['battery_level'] = max(0.0, min(100.0, battery_level)) + except Exception as e: + print(f"✗ 设备电池电量更新失败: {e}") + + def update_device_signal(self, device_id: int, signal_strength: float): + """ + 更新设备信号强度 + + Args: + device_id: 设备ID + signal_strength: 信号强度百分比(0-100) + """ + try: + if device_id in self.devices: + self.devices[device_id]['signal_strength'] = max(0.0, min(100.0, signal_strength)) + except Exception as e: + print(f"✗ 设备信号强度更新失败: {e}") + + def register_device_callback(self, callback_type: str, callback: callable): + """ + 注册设备回调 + + Args: + callback_type: 回调类型('device_connected', 'device_disconnected', 'device_updated') + callback: 回调函数 + """ + try: + if callback_type in self.device_callbacks: + self.device_callbacks[callback_type].append(callback) + print(f"✓ 设备回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 设备回调注册失败: {e}") + + def _trigger_device_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发设备回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.device_callbacks: + for callback in self.device_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 设备回调执行失败: {e}") + except Exception as e: + print(f"✗ 设备回调触发失败: {e}") + + def get_device_capabilities(self, device_type: str) -> List[str]: + """ + 获取设备类型的功能列表 + + Args: + device_type: 设备类型 + + Returns: + 功能列表 + """ + return self.device_capabilities.get(device_type, []) + + def get_platform_config(self) -> Dict[str, Any]: + """ + 获取平台配置 + + Returns: + 平台配置字典 + """ + current_platform = platform.system() + return self.platform_config.get(current_platform, {}) + + def set_discovery_config(self, config: Dict[str, Any]): + """ + 设置设备发现配置 + + Args: + config: 发现配置字典 + """ + try: + self.discovery_config.update(config) + print(f"✓ 设备发现配置已更新: {self.discovery_config}") + except Exception as e: + print(f"✗ 设备发现配置设置失败: {e}") + + def get_discovery_config(self) -> Dict[str, Any]: + """ + 获取设备发现配置 + + Returns: + 发现配置字典 + """ + return self.discovery_config.copy() + + def get_device_stats(self) -> Dict[str, Any]: + """ + 获取设备统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_device_stats(self): + """重置设备统计信息""" + try: + self.stats = { + 'devices_registered': 0, + 'devices_connected': 0, + 'devices_disconnected': 0, + 'scan_operations': 0, + 'connection_errors': 0 + } + print("✓ 设备统计信息已重置") + except Exception as e: + print(f"✗ 设备统计信息重置失败: {e}") + + def get_device_states(self) -> Dict[str, Any]: + """ + 获取设备状态 + + Returns: + 设备状态字典 + """ return self.device_states.copy() \ No newline at end of file diff --git a/plugins/user/multiplatform_input/editor/input_editor.py b/plugins/user/multiplatform_input/editor/input_editor.py index 005780c0..cfd7fbc0 100644 --- a/plugins/user/multiplatform_input/editor/input_editor.py +++ b/plugins/user/multiplatform_input/editor/input_editor.py @@ -1,1154 +1,1154 @@ -""" -输入编辑器模块 -提供可视化界面用于配置和测试输入设备及映射 -""" - -import time -from typing import Dict, Any, List, Optional -from enum import Enum -import threading - -class EditorMode(Enum): - """编辑器模式枚举""" - DEVICE_SETUP = "device_setup" - MAPPING_EDITOR = "mapping_editor" - GESTURE_TESTER = "gesture_tester" - HAPTIC_DESIGNER = "haptic_designer" - CONFIG_MANAGER = "config_manager" - LIVE_MONITOR = "live_monitor" - DEVICE_CALIBRATOR = "device_calibrator" # 新增设备校准模式 - ADVANCED_INPUT = "advanced_input" # 新增高级输入模式 - -class InputEditor: - """ - 输入编辑器 - 提供可视化界面用于配置和测试输入设备及映射 - """ - - def __init__(self, plugin): - """ - 初始化输入编辑器 - - Args: - plugin: 多平台输入支持插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 编辑器配置 - self.editor_config = { - 'window_width': 1024, - 'window_height': 768, - 'refresh_rate': 30.0, - 'enable_preview': True, - 'enable_realtime_update': True, - 'show_debug_info': False, - 'theme': 'dark' - } - - # 当前编辑器状态 - self.editor_state = { - 'current_mode': EditorMode.DEVICE_SETUP, - 'selected_device': None, - 'selected_mapping': None, - 'selected_gesture': None, - 'selected_effect': None, - 'is_recording': False, - 'is_testing': False, - 'preview_active': False - } - - # UI组件 - self.ui_components = { - 'main_window': None, - 'toolbar': None, - 'property_panel': None, - 'preview_panel': None, - 'status_bar': None, - 'device_list': None, - 'mapping_editor': None, - 'gesture_visualizer': None, - 'haptic_designer': None, - 'calibration_wizard': None, # 新增校准向导 - 'advanced_input_panel': None # 新增高级输入面板 - } - - # 编辑器数据 - self.editor_data = { - 'device_cache': {}, - 'mapping_cache': {}, - 'gesture_cache': {}, - 'haptic_cache': {}, - 'live_data': {}, - 'calibration_data': {}, # 新增校准数据 - 'advanced_patterns': {} # 新增高级模式数据 - } - - # 编辑器操作历史 - self.operation_history = [] - self.history_pointer = -1 - self.max_history_size = 100 - - # 实时监控数据 - self.monitor_data = { - 'input_events': [], - 'gesture_events': [], - 'haptic_events': [], - 'performance_metrics': {}, - 'calibration_progress': {}, # 新增校准进度数据 - 'advanced_input_events': [] # 新增高级输入事件数据 - } - - # 回调函数 - self.editor_callbacks = { - 'device_selected': [], - 'mapping_selected': [], - 'gesture_selected': [], - 'effect_selected': [], - 'mode_changed': [], - 'data_updated': [], - 'calibration_progress': [], # 新增校准进度回调 - 'pattern_matched': [] # 新增模式匹配回调 - } - - # 统计信息 - self.editor_stats = { - 'windows_opened': 0, - 'operations_performed': 0, - 'undo_operations': 0, - 'redo_operations': 0, - 'data_refreshed': 0, - 'preview_updates': 0 - } - - # 线程管理 - self.editor_thread = None - self.monitor_thread = None - self.running = False - - # 时间戳记录 - self.last_refresh_time = 0.0 - self.last_preview_update = 0.0 - - print("✓ 输入编辑器已创建") - - def initialize(self) -> bool: - """ - 初始化输入编辑器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化UI组件(模拟) - self._initialize_ui_components() - - # 加载缓存数据 - self._load_editor_cache() - - # 注册设备校准回调 - if self.plugin.device_calibrator: - self.plugin.device_calibrator.register_calibration_callback( - 'calibration_progress', self._on_calibration_progress) - self.plugin.device_calibrator.register_calibration_callback( - 'calibration_completed', self._on_calibration_completed) - - # 注册高级输入回调 - if self.plugin.advanced_input_processor: - self.plugin.advanced_input_processor.register_advanced_callback( - 'pattern_matched', self._on_pattern_matched) - self.plugin.advanced_input_processor.register_advanced_callback( - 'sequence_completed', self._on_sequence_completed) - - self.initialized = True - print("✓ 输入编辑器初始化完成") - return True - - except Exception as e: - print(f"✗ 输入编辑器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用输入编辑器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 输入编辑器未初始化") - return False - - self.enabled = True - self.running = True - - # 启动编辑器线程 - self.editor_thread = threading.Thread(target=self._editor_update_loop, daemon=True) - self.editor_thread.start() - - # 启动监控线程 - if self.editor_config['enable_realtime_update']: - self.monitor_thread = threading.Thread(target=self._monitor_update_loop, daemon=True) - self.monitor_thread.start() - - print("✓ 输入编辑器已启用") - return True - - except Exception as e: - print(f"✗ 输入编辑器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用输入编辑器""" - try: - self.enabled = False - self.running = False - - # 等待线程结束 - if self.editor_thread and self.editor_thread.is_alive(): - self.editor_thread.join(timeout=2.0) - - if self.monitor_thread and self.monitor_thread.is_alive(): - self.monitor_thread.join(timeout=2.0) - - # 关闭UI组件 - self._close_ui_components() - - print("✓ 输入编辑器已禁用") - - except Exception as e: - print(f"✗ 输入编辑器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理输入编辑器资源""" - try: - self.disable() - self.ui_components.clear() - self.editor_data.clear() - self.operation_history.clear() - self.monitor_data.clear() - self.editor_callbacks.clear() - self.initialized = False - print("✓ 输入编辑器资源已清理") - - except Exception as e: - print(f"✗ 输入编辑器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新输入编辑器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新统计信息 - pass - - except Exception as e: - print(f"✗ 输入编辑器更新失败: {e}") - import traceback - traceback.print_exc() - - def _editor_update_loop(self): - """编辑器更新循环""" - try: - refresh_interval = 1.0 / self.editor_config['refresh_rate'] - - while self.running: - if self.enabled: - self._update_editor_display() - - time.sleep(refresh_interval) - - except Exception as e: - print(f"✗ 编辑器更新循环失败: {e}") - - def _monitor_update_loop(self): - """监控更新循环""" - try: - monitor_interval = 0.1 # 100ms - - while self.running: - if self.enabled and self.editor_config['enable_realtime_update']: - self._update_monitor_data() - - time.sleep(monitor_interval) - - except Exception as e: - print(f"✗ 监控更新循环失败: {e}") - - def _initialize_ui_components(self): - """初始化UI组件""" - try: - # 在实际实现中,这里会创建实际的UI组件 - # 由于这是模拟实现,我们只创建占位符 - self.ui_components['main_window'] = "MainWindow" - self.ui_components['toolbar'] = "Toolbar" - self.ui_components['property_panel'] = "PropertyPanel" - self.ui_components['preview_panel'] = "PreviewPanel" - self.ui_components['status_bar'] = "StatusBar" - self.ui_components['device_list'] = "DeviceList" - self.ui_components['mapping_editor'] = "MappingEditor" - self.ui_components['gesture_visualizer'] = "GestureVisualizer" - self.ui_components['haptic_designer'] = "HapticDesigner" - self.ui_components['calibration_wizard'] = "CalibrationWizard" - self.ui_components['advanced_input_panel'] = "AdvancedInputPanel" - - print("✓ UI组件初始化完成") - except Exception as e: - print(f"✗ UI组件初始化失败: {e}") - - def _close_ui_components(self): - """关闭UI组件""" - try: - # 在实际实现中,这里会关闭实际的UI组件 - self.ui_components.clear() - print("✓ UI组件已关闭") - except Exception as e: - print(f"✗ UI组件关闭失败: {e}") - - def _load_editor_cache(self): - """加载编辑器缓存""" - try: - # 模拟加载缓存数据 - if self.plugin.device_manager: - self.editor_data['device_cache'] = self.plugin.device_manager.get_all_input_devices() - - if self.plugin.input_mapper: - self.editor_data['mapping_cache'] = self.plugin.input_mapper.get_all_mappings() - - if self.plugin.gesture_recognizer: - self.editor_data['gesture_cache'] = self.plugin.gesture_recognizer.get_gesture_history() - - if self.plugin.haptic_manager: - self.editor_data['haptic_cache'] = self.plugin.haptic_manager.get_all_haptic_effects() - - # 加载校准数据 - if self.plugin.device_calibrator: - self.editor_data['calibration_data'] = self.plugin.device_calibrator.calibration_data - - # 加载高级输入模式数据 - if self.plugin.advanced_input_processor: - self.editor_data['advanced_patterns'] = self.plugin.advanced_input_processor.get_input_patterns() - - print("✓ 编辑器缓存加载完成") - except Exception as e: - print(f"✗ 编辑器缓存加载失败: {e}") - - def _update_editor_display(self): - """更新编辑器显示""" - try: - current_time = time.time() - - # 检查是否需要刷新 - if current_time - self.last_refresh_time >= 1.0 / self.editor_config['refresh_rate']: - # 更新设备列表 - self._refresh_device_list() - - # 更新属性面板 - self._refresh_property_panel() - - # 更新预览面板 - if self.editor_config['enable_preview']: - self._update_preview_panel() - - # 更新状态栏 - self._update_status_bar() - - self.last_refresh_time = current_time - self.editor_stats['data_refreshed'] += 1 - - except Exception as e: - print(f"✗ 编辑器显示更新失败: {e}") - - def _update_monitor_data(self): - """更新监控数据""" - try: - # 收集实时数据 - if self.plugin.input_manager: - live_inputs = self.plugin.input_manager.get_stats() - self.monitor_data['input_events'].append({ - 'timestamp': time.time(), - 'data': live_inputs - }) - - if self.plugin.gesture_recognizer: - gestures = self.plugin.gesture_recognizer.get_gesture_stats() - self.monitor_data['gesture_events'].append({ - 'timestamp': time.time(), - 'data': gestures - }) - - if self.plugin.haptic_manager: - haptics = self.plugin.haptic_manager.get_haptic_stats() - self.monitor_data['haptic_events'].append({ - 'timestamp': time.time(), - 'data': haptics - }) - - # 保持历史记录大小 - max_history = 100 - for event_type in ['input_events', 'gesture_events', 'haptic_events', 'advanced_input_events']: - if len(self.monitor_data[event_type]) > max_history: - self.monitor_data[event_type] = self.monitor_data[event_type][-max_history:] - - except Exception as e: - print(f"✗ 监控数据更新失败: {e}") - - def _refresh_device_list(self): - """刷新设备列表""" - try: - if self.plugin.device_manager: - devices = self.plugin.device_manager.get_all_input_devices() - self.editor_data['device_cache'] = devices - # 在实际实现中,这里会更新UI组件 - except Exception as e: - print(f"✗ 设备列表刷新失败: {e}") - - def _refresh_property_panel(self): - """刷新属性面板""" - try: - # 根据当前选择更新属性面板 - selected_device = self.editor_state['selected_device'] - selected_mapping = self.editor_state['selected_mapping'] - - # 在实际实现中,这里会更新属性面板的显示内容 - pass - except Exception as e: - print(f"✗ 属性面板刷新失败: {e}") - - def _update_preview_panel(self): - """更新预览面板""" - try: - current_time = time.time() - if current_time - self.last_preview_update >= 0.1: # 100ms - # 更新预览数据 - self.editor_data['live_data'] = self._collect_live_data() - - # 在实际实现中,这里会更新预览面板的显示 - self.editor_stats['preview_updates'] += 1 - self.last_preview_update = current_time - - except Exception as e: - print(f"✗ 预览面板更新失败: {e}") - - def _collect_live_data(self) -> Dict[str, Any]: - """ - 收集实时数据 - - Returns: - 实时数据字典 - """ - try: - live_data = { - 'timestamp': time.time(), - 'devices': {}, - 'inputs': {}, - 'gestures': {}, - 'haptics': {} - } - - # 收集设备数据 - if self.plugin.device_manager: - live_data['devices'] = self.plugin.device_manager.get_device_stats() - - # 收集输入数据 - if self.plugin.input_manager: - live_data['inputs'] = self.plugin.input_manager.get_stats() - - # 收集手势数据 - if self.plugin.gesture_recognizer: - live_data['gestures'] = self.plugin.gesture_recognizer.get_gesture_stats() - - # 收集触觉数据 - if self.plugin.haptic_manager: - live_data['haptics'] = self.plugin.haptic_manager.get_haptic_stats() - - return live_data - - except Exception as e: - print(f"✗ 实时数据收集失败: {e}") - return {} - - def _update_status_bar(self): - """更新状态栏""" - try: - # 在实际实现中,这里会更新状态栏的显示内容 - pass - except Exception as e: - print(f"✗ 状态栏更新失败: {e}") - - def set_editor_mode(self, mode: EditorMode) -> bool: - """ - 设置编辑器模式 - - Args: - mode: 编辑器模式 - - Returns: - 是否设置成功 - """ - try: - old_mode = self.editor_state['current_mode'] - self.editor_state['current_mode'] = mode - - # 触发模式变更回调 - self._trigger_editor_callback('mode_changed', { - 'old_mode': old_mode.value, - 'new_mode': mode.value - }) - - print(f"✓ 编辑器模式已设置: {mode.value}") - return True - - except Exception as e: - print(f"✗ 编辑器模式设置失败: {e}") - return False - - def select_device(self, device_id: int) -> bool: - """ - 选择设备 - - Args: - device_id: 设备ID - - Returns: - 是否选择成功 - """ - try: - self.editor_state['selected_device'] = device_id - - # 触发设备选择回调 - self._trigger_editor_callback('device_selected', { - 'device_id': device_id - }) - - print(f"✓ 设备已选择: ID {device_id}") - return True - - except Exception as e: - print(f"✗ 设备选择失败: {e}") - return False - - def select_mapping(self, mapping_id: int) -> bool: - """ - 选择映射 - - Args: - mapping_id: 映射ID - - Returns: - 是否选择成功 - """ - try: - self.editor_state['selected_mapping'] = mapping_id - - # 触发映射选择回调 - self._trigger_editor_callback('mapping_selected', { - 'mapping_id': mapping_id - }) - - print(f"✓ 映射已选择: ID {mapping_id}") - return True - - except Exception as e: - print(f"✗ 映射选择失败: {e}") - return False - - def select_gesture(self, gesture_id: int) -> bool: - """ - 选择手势 - - Args: - gesture_id: 手势ID - - Returns: - 是否选择成功 - """ - try: - self.editor_state['selected_gesture'] = gesture_id - - # 触发手势选择回调 - self._trigger_editor_callback('gesture_selected', { - 'gesture_id': gesture_id - }) - - print(f"✓ 手势已选择: ID {gesture_id}") - return True - - except Exception as e: - print(f"✗ 手势选择失败: {e}") - return False - - def select_effect(self, effect_id: int) -> bool: - """ - 选择触觉效果 - - Args: - effect_id: 效果ID - - Returns: - 是否选择成功 - """ - try: - self.editor_state['selected_effect'] = effect_id - - # 触发效果选择回调 - self._trigger_editor_callback('effect_selected', { - 'effect_id': effect_id - }) - - print(f"✓ 触觉效果已选择: ID {effect_id}") - return True - - except Exception as e: - print(f"✗ 触觉效果选择失败: {e}") - return False - - def start_recording(self) -> bool: - """ - 开始录制 - - Returns: - 是否开始成功 - """ - try: - self.editor_state['is_recording'] = True - print("✓ 录制已开始") - return True - - except Exception as e: - print(f"✗ 录制开始失败: {e}") - return False - - def stop_recording(self) -> bool: - """ - 停止录制 - - Returns: - 是否停止成功 - """ - try: - self.editor_state['is_recording'] = False - print("✓ 录制已停止") - return True - - except Exception as e: - print(f"✗ 录制停止失败: {e}") - return False - - def start_testing(self) -> bool: - """ - 开始测试 - - Returns: - 是否开始成功 - """ - try: - self.editor_state['is_testing'] = True - print("✓ 测试已开始") - return True - - except Exception as e: - print(f"✗ 测试开始失败: {e}") - return False - - def stop_testing(self) -> bool: - """ - 停止测试 - - Returns: - 是否停止成功 - """ - try: - self.editor_state['is_testing'] = False - print("✓ 测试已停止") - return True - - except Exception as e: - print(f"✗ 测试停止失败: {e}") - return False - - def toggle_preview(self, enable: bool = None) -> bool: - """ - 切换预览 - - Args: - enable: 是否启用预览(None表示切换) - - Returns: - 是否切换成功 - """ - try: - if enable is None: - enable = not self.editor_state['preview_active'] - - self.editor_state['preview_active'] = enable - self.editor_config['enable_preview'] = enable - - status = "启用" if enable else "禁用" - print(f"✓ 预览已{status}") - return True - - except Exception as e: - print(f"✗ 预览切换失败: {e}") - return False - - def undo_operation(self) -> bool: - """ - 撤销操作 - - Returns: - 是否撤销成功 - """ - try: - if self.history_pointer >= 0 and self.operation_history: - # 执行撤销操作 - operation = self.operation_history[self.history_pointer] - self.history_pointer -= 1 - self.editor_stats['undo_operations'] += 1 - - print("✓ 操作已撤销") - return True - else: - print("⚠ 没有可撤销的操作") - return False - - except Exception as e: - print(f"✗ 操作撤销失败: {e}") - return False - - def redo_operation(self) -> bool: - """ - 重做操作 - - Returns: - 是否重做成功 - """ - try: - if self.history_pointer < len(self.operation_history) - 1: - # 执行重做操作 - self.history_pointer += 1 - operation = self.operation_history[self.history_pointer] - self.editor_stats['redo_operations'] += 1 - - print("✓ 操作已重做") - return True - else: - print("⚠ 没有可重做的操作") - return False - - except Exception as e: - print(f"✗ 操作重做失败: {e}") - return False - - def add_operation_to_history(self, operation: Dict[str, Any]): - """ - 添加操作到历史 - - Args: - operation: 操作数据 - """ - try: - # 添加到历史记录 - self.operation_history.append(operation) - self.history_pointer = len(self.operation_history) - 1 - - # 保持历史记录大小 - if len(self.operation_history) > self.max_history_size: - remove_count = len(self.operation_history) - self.max_history_size - self.operation_history = self.operation_history[remove_count:] - self.history_pointer -= remove_count - self.history_pointer = max(0, self.history_pointer) - - self.editor_stats['operations_performed'] += 1 - - except Exception as e: - print(f"✗ 操作添加到历史失败: {e}") - - def _trigger_editor_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发编辑器回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.editor_callbacks: - for callback in self.editor_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 编辑器回调执行失败: {e}") - except Exception as e: - print(f"✗ 编辑器回调触发失败: {e}") - - def register_editor_callback(self, callback_type: str, callback: callable): - """ - 注册编辑器回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.editor_callbacks: - self.editor_callbacks[callback_type].append(callback) - print(f"✓ 编辑器回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 编辑器回调注册失败: {e}") - - def unregister_editor_callback(self, callback_type: str, callback: callable): - """ - 注销编辑器回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.editor_callbacks: - if callback in self.editor_callbacks[callback_type]: - self.editor_callbacks[callback_type].remove(callback) - print(f"✓ 编辑器回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 编辑器回调注销失败: {e}") - - def get_editor_state(self) -> Dict[str, Any]: - """ - 获取编辑器状态 - - Returns: - 编辑器状态字典 - """ - return self.editor_state.copy() - - def get_editor_config(self) -> Dict[str, Any]: - """ - 获取编辑器配置 - - Returns: - 编辑器配置字典 - """ - return self.editor_config.copy() - - def set_editor_config(self, config: Dict[str, Any]) -> bool: - """ - 设置编辑器配置 - - Args: - config: 编辑器配置字典 - - Returns: - 是否设置成功 - """ - try: - self.editor_config.update(config) - print(f"✓ 编辑器配置已更新: {self.editor_config}") - return True - except Exception as e: - print(f"✗ 编辑器配置设置失败: {e}") - return False - - def get_editor_data(self, data_type: str) -> Optional[Dict[str, Any]]: - """ - 获取编辑器数据 - - Args: - data_type: 数据类型 - - Returns: - 数据字典或None - """ - return self.editor_data.get(data_type) - - def get_monitor_data(self) -> Dict[str, Any]: - """ - 获取监控数据 - - Returns: - 监控数据字典 - """ - return self.monitor_data.copy() - - def get_editor_stats(self) -> Dict[str, int]: - """ - 获取编辑器统计信息 - - Returns: - 编辑器统计字典 - """ - return self.editor_stats.copy() - - def reset_editor_stats(self): - """重置编辑器统计信息""" - try: - self.editor_stats = { - 'windows_opened': 0, - 'operations_performed': 0, - 'undo_operations': 0, - 'redo_operations': 0, - 'data_refreshed': 0, - 'preview_updates': 0 - } - print("✓ 编辑器统计信息已重置") - except Exception as e: - print(f"✗ 编辑器统计信息重置失败: {e}") - - def refresh_editor_data(self): - """刷新编辑器数据""" - try: - self._load_editor_cache() - self._update_editor_display() - print("✓ 编辑器数据已刷新") - except Exception as e: - print(f"✗ 编辑器数据刷新失败: {e}") - - def show_editor_window(self) -> bool: - """ - 显示编辑器窗口 - - Returns: - 是否显示成功 - """ - try: - # 在实际实现中,这里会显示实际的编辑器窗口 - self.editor_stats['windows_opened'] += 1 - print("✓ 编辑器窗口已显示") - return True - except Exception as e: - print(f"✗ 编辑器窗口显示失败: {e}") - return False - - def hide_editor_window(self) -> bool: - """ - 隐藏编辑器窗口 - - Returns: - 是否隐藏成功 - """ - try: - # 在实际实现中,这里会隐藏实际的编辑器窗口 - print("✓ 编辑器窗口已隐藏") - return True - except Exception as e: - print(f"✗ 编辑器窗口隐藏失败: {e}") - return False - - def export_editor_data(self, file_path: str) -> bool: - """ - 导出编辑器数据 - - Args: - file_path: 导出文件路径 - - Returns: - 是否导出成功 - """ - try: - # 在实际实现中,这里会导出编辑器数据到文件 - print(f"✓ 编辑器数据已导出: {file_path}") - return True - except Exception as e: - print(f"✗ 编辑器数据导出失败: {e}") - return False - - def import_editor_data(self, file_path: str) -> bool: - """ - 导入编辑器数据 - - Args: - file_path: 导入文件路径 - - Returns: - 是否导入成功 - """ - try: - # 在实际实现中,这里会从文件导入编辑器数据 - print(f"✓ 编辑器数据已导入: {file_path}") - return True - except Exception as e: - print(f"✗ 编辑器数据导入失败: {e}") - return False - - # 新增的设备校准相关方法 - def start_device_calibration(self, device_id: int) -> bool: - """ - 开始设备校准 - - Args: - device_id: 设备ID - - Returns: - 是否开始成功 - """ - try: - if self.plugin.device_calibrator: - result = self.plugin.device_calibrator.start_calibration(device_id) - if result: - print(f"✓ 设备校准已开始: ID {device_id}") - return result - else: - print("✗ 设备校准器未初始化") - return False - except Exception as e: - print(f"✗ 设备校准启动失败: {e}") - return False - - def cancel_device_calibration(self) -> bool: - """ - 取消设备校准 - - Returns: - 是否取消成功 - """ - try: - if self.plugin.device_calibrator: - result = self.plugin.device_calibrator.cancel_calibration() - if result: - print("✓ 设备校准已取消") - return result - else: - print("✗ 设备校准器未初始化") - return False - except Exception as e: - print(f"✗ 设备校准取消失败: {e}") - return False - - def get_calibration_status(self) -> Dict[str, Any]: - """ - 获取校准状态 - - Returns: - 校准状态字典 - """ - try: - if self.plugin.device_calibrator: - return self.plugin.device_calibrator.get_calibration_status() - else: - return {} - except Exception as e: - print(f"✗ 校准状态获取失败: {e}") - return {} - - def _on_calibration_progress(self, data: Dict[str, Any]): - """ - 校准进度回调 - - Args: - data: 进度数据 - """ - try: - # 更新监控数据 - self.monitor_data['calibration_progress'] = data - - # 触发编辑器回调 - self._trigger_editor_callback('calibration_progress', data) - - print(f"✓ 校准进度更新: {data}") - except Exception as e: - print(f"✗ 校准进度处理失败: {e}") - - def _on_calibration_completed(self, data: Dict[str, Any]): - """ - 校准完成回调 - - Args: - data: 完成数据 - """ - try: - print(f"✓ 设备校准完成: {data}") - - # 刷新设备列表以显示校准状态 - self._refresh_device_list() - except Exception as e: - print(f"✗ 校准完成处理失败: {e}") - - # 新增的高级输入处理相关方法 - def create_input_pattern(self, name: str, inputs: List[str], context: str, - description: str = "") -> int: - """ - 创建输入模式 - - Args: - name: 模式名称 - inputs: 输入序列 - context: 上下文 - description: 描述 - - Returns: - 模式ID - """ - try: - if self.plugin.advanced_input_processor: - from .core.advanced_input_processor import InputContext - context_enum = getattr(InputContext, context.upper(), InputContext.GLOBAL) - pattern_id = self.plugin.advanced_input_processor.create_input_pattern( - name, inputs, context_enum, description) - if pattern_id >= 0: - print(f"✓ 输入模式已创建: {name} (ID: {pattern_id})") - return pattern_id - else: - print("✗ 高级输入处理器未初始化") - return -1 - except Exception as e: - print(f"✗ 输入模式创建失败: {e}") - return -1 - - def _on_pattern_matched(self, data: Dict[str, Any]): - """ - 模式匹配回调 - - Args: - data: 匹配数据 - """ - try: - # 添加到监控数据 - self.monitor_data['advanced_input_events'].append({ - 'type': 'pattern_matched', - 'data': data, - 'timestamp': time.time() - }) - - # 触发编辑器回调 - self._trigger_editor_callback('pattern_matched', data) - - print(f"✓ 输入模式匹配: {data}") - except Exception as e: - print(f"✗ 模式匹配处理失败: {e}") - - def _on_sequence_completed(self, data: Dict[str, Any]): - """ - 序列完成回调 - - Args: - data: 完成数据 - """ - try: - # 添加到监控数据 - self.monitor_data['advanced_input_events'].append({ - 'type': 'sequence_completed', - 'data': data, - 'timestamp': time.time() - }) - - print(f"✓ 输入序列完成: {data}") - except Exception as e: +""" +输入编辑器模块 +提供可视化界面用于配置和测试输入设备及映射 +""" + +import time +from typing import Dict, Any, List, Optional +from enum import Enum +import threading + +class EditorMode(Enum): + """编辑器模式枚举""" + DEVICE_SETUP = "device_setup" + MAPPING_EDITOR = "mapping_editor" + GESTURE_TESTER = "gesture_tester" + HAPTIC_DESIGNER = "haptic_designer" + CONFIG_MANAGER = "config_manager" + LIVE_MONITOR = "live_monitor" + DEVICE_CALIBRATOR = "device_calibrator" # 新增设备校准模式 + ADVANCED_INPUT = "advanced_input" # 新增高级输入模式 + +class InputEditor: + """ + 输入编辑器 + 提供可视化界面用于配置和测试输入设备及映射 + """ + + def __init__(self, plugin): + """ + 初始化输入编辑器 + + Args: + plugin: 多平台输入支持插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 编辑器配置 + self.editor_config = { + 'window_width': 1024, + 'window_height': 768, + 'refresh_rate': 30.0, + 'enable_preview': True, + 'enable_realtime_update': True, + 'show_debug_info': False, + 'theme': 'dark' + } + + # 当前编辑器状态 + self.editor_state = { + 'current_mode': EditorMode.DEVICE_SETUP, + 'selected_device': None, + 'selected_mapping': None, + 'selected_gesture': None, + 'selected_effect': None, + 'is_recording': False, + 'is_testing': False, + 'preview_active': False + } + + # UI组件 + self.ui_components = { + 'main_window': None, + 'toolbar': None, + 'property_panel': None, + 'preview_panel': None, + 'status_bar': None, + 'device_list': None, + 'mapping_editor': None, + 'gesture_visualizer': None, + 'haptic_designer': None, + 'calibration_wizard': None, # 新增校准向导 + 'advanced_input_panel': None # 新增高级输入面板 + } + + # 编辑器数据 + self.editor_data = { + 'device_cache': {}, + 'mapping_cache': {}, + 'gesture_cache': {}, + 'haptic_cache': {}, + 'live_data': {}, + 'calibration_data': {}, # 新增校准数据 + 'advanced_patterns': {} # 新增高级模式数据 + } + + # 编辑器操作历史 + self.operation_history = [] + self.history_pointer = -1 + self.max_history_size = 100 + + # 实时监控数据 + self.monitor_data = { + 'input_events': [], + 'gesture_events': [], + 'haptic_events': [], + 'performance_metrics': {}, + 'calibration_progress': {}, # 新增校准进度数据 + 'advanced_input_events': [] # 新增高级输入事件数据 + } + + # 回调函数 + self.editor_callbacks = { + 'device_selected': [], + 'mapping_selected': [], + 'gesture_selected': [], + 'effect_selected': [], + 'mode_changed': [], + 'data_updated': [], + 'calibration_progress': [], # 新增校准进度回调 + 'pattern_matched': [] # 新增模式匹配回调 + } + + # 统计信息 + self.editor_stats = { + 'windows_opened': 0, + 'operations_performed': 0, + 'undo_operations': 0, + 'redo_operations': 0, + 'data_refreshed': 0, + 'preview_updates': 0 + } + + # 线程管理 + self.editor_thread = None + self.monitor_thread = None + self.running = False + + # 时间戳记录 + self.last_refresh_time = 0.0 + self.last_preview_update = 0.0 + + print("✓ 输入编辑器已创建") + + def initialize(self) -> bool: + """ + 初始化输入编辑器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化UI组件(模拟) + self._initialize_ui_components() + + # 加载缓存数据 + self._load_editor_cache() + + # 注册设备校准回调 + if self.plugin.device_calibrator: + self.plugin.device_calibrator.register_calibration_callback( + 'calibration_progress', self._on_calibration_progress) + self.plugin.device_calibrator.register_calibration_callback( + 'calibration_completed', self._on_calibration_completed) + + # 注册高级输入回调 + if self.plugin.advanced_input_processor: + self.plugin.advanced_input_processor.register_advanced_callback( + 'pattern_matched', self._on_pattern_matched) + self.plugin.advanced_input_processor.register_advanced_callback( + 'sequence_completed', self._on_sequence_completed) + + self.initialized = True + print("✓ 输入编辑器初始化完成") + return True + + except Exception as e: + print(f"✗ 输入编辑器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用输入编辑器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 输入编辑器未初始化") + return False + + self.enabled = True + self.running = True + + # 启动编辑器线程 + self.editor_thread = threading.Thread(target=self._editor_update_loop, daemon=True) + self.editor_thread.start() + + # 启动监控线程 + if self.editor_config['enable_realtime_update']: + self.monitor_thread = threading.Thread(target=self._monitor_update_loop, daemon=True) + self.monitor_thread.start() + + print("✓ 输入编辑器已启用") + return True + + except Exception as e: + print(f"✗ 输入编辑器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用输入编辑器""" + try: + self.enabled = False + self.running = False + + # 等待线程结束 + if self.editor_thread and self.editor_thread.is_alive(): + self.editor_thread.join(timeout=2.0) + + if self.monitor_thread and self.monitor_thread.is_alive(): + self.monitor_thread.join(timeout=2.0) + + # 关闭UI组件 + self._close_ui_components() + + print("✓ 输入编辑器已禁用") + + except Exception as e: + print(f"✗ 输入编辑器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理输入编辑器资源""" + try: + self.disable() + self.ui_components.clear() + self.editor_data.clear() + self.operation_history.clear() + self.monitor_data.clear() + self.editor_callbacks.clear() + self.initialized = False + print("✓ 输入编辑器资源已清理") + + except Exception as e: + print(f"✗ 输入编辑器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新输入编辑器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新统计信息 + pass + + except Exception as e: + print(f"✗ 输入编辑器更新失败: {e}") + import traceback + traceback.print_exc() + + def _editor_update_loop(self): + """编辑器更新循环""" + try: + refresh_interval = 1.0 / self.editor_config['refresh_rate'] + + while self.running: + if self.enabled: + self._update_editor_display() + + time.sleep(refresh_interval) + + except Exception as e: + print(f"✗ 编辑器更新循环失败: {e}") + + def _monitor_update_loop(self): + """监控更新循环""" + try: + monitor_interval = 0.1 # 100ms + + while self.running: + if self.enabled and self.editor_config['enable_realtime_update']: + self._update_monitor_data() + + time.sleep(monitor_interval) + + except Exception as e: + print(f"✗ 监控更新循环失败: {e}") + + def _initialize_ui_components(self): + """初始化UI组件""" + try: + # 在实际实现中,这里会创建实际的UI组件 + # 由于这是模拟实现,我们只创建占位符 + self.ui_components['main_window'] = "MainWindow" + self.ui_components['toolbar'] = "Toolbar" + self.ui_components['property_panel'] = "PropertyPanel" + self.ui_components['preview_panel'] = "PreviewPanel" + self.ui_components['status_bar'] = "StatusBar" + self.ui_components['device_list'] = "DeviceList" + self.ui_components['mapping_editor'] = "MappingEditor" + self.ui_components['gesture_visualizer'] = "GestureVisualizer" + self.ui_components['haptic_designer'] = "HapticDesigner" + self.ui_components['calibration_wizard'] = "CalibrationWizard" + self.ui_components['advanced_input_panel'] = "AdvancedInputPanel" + + print("✓ UI组件初始化完成") + except Exception as e: + print(f"✗ UI组件初始化失败: {e}") + + def _close_ui_components(self): + """关闭UI组件""" + try: + # 在实际实现中,这里会关闭实际的UI组件 + self.ui_components.clear() + print("✓ UI组件已关闭") + except Exception as e: + print(f"✗ UI组件关闭失败: {e}") + + def _load_editor_cache(self): + """加载编辑器缓存""" + try: + # 模拟加载缓存数据 + if self.plugin.device_manager: + self.editor_data['device_cache'] = self.plugin.device_manager.get_all_input_devices() + + if self.plugin.input_mapper: + self.editor_data['mapping_cache'] = self.plugin.input_mapper.get_all_mappings() + + if self.plugin.gesture_recognizer: + self.editor_data['gesture_cache'] = self.plugin.gesture_recognizer.get_gesture_history() + + if self.plugin.haptic_manager: + self.editor_data['haptic_cache'] = self.plugin.haptic_manager.get_all_haptic_effects() + + # 加载校准数据 + if self.plugin.device_calibrator: + self.editor_data['calibration_data'] = self.plugin.device_calibrator.calibration_data + + # 加载高级输入模式数据 + if self.plugin.advanced_input_processor: + self.editor_data['advanced_patterns'] = self.plugin.advanced_input_processor.get_input_patterns() + + print("✓ 编辑器缓存加载完成") + except Exception as e: + print(f"✗ 编辑器缓存加载失败: {e}") + + def _update_editor_display(self): + """更新编辑器显示""" + try: + current_time = time.time() + + # 检查是否需要刷新 + if current_time - self.last_refresh_time >= 1.0 / self.editor_config['refresh_rate']: + # 更新设备列表 + self._refresh_device_list() + + # 更新属性面板 + self._refresh_property_panel() + + # 更新预览面板 + if self.editor_config['enable_preview']: + self._update_preview_panel() + + # 更新状态栏 + self._update_status_bar() + + self.last_refresh_time = current_time + self.editor_stats['data_refreshed'] += 1 + + except Exception as e: + print(f"✗ 编辑器显示更新失败: {e}") + + def _update_monitor_data(self): + """更新监控数据""" + try: + # 收集实时数据 + if self.plugin.input_manager: + live_inputs = self.plugin.input_manager.get_stats() + self.monitor_data['input_events'].append({ + 'timestamp': time.time(), + 'data': live_inputs + }) + + if self.plugin.gesture_recognizer: + gestures = self.plugin.gesture_recognizer.get_gesture_stats() + self.monitor_data['gesture_events'].append({ + 'timestamp': time.time(), + 'data': gestures + }) + + if self.plugin.haptic_manager: + haptics = self.plugin.haptic_manager.get_haptic_stats() + self.monitor_data['haptic_events'].append({ + 'timestamp': time.time(), + 'data': haptics + }) + + # 保持历史记录大小 + max_history = 100 + for event_type in ['input_events', 'gesture_events', 'haptic_events', 'advanced_input_events']: + if len(self.monitor_data[event_type]) > max_history: + self.monitor_data[event_type] = self.monitor_data[event_type][-max_history:] + + except Exception as e: + print(f"✗ 监控数据更新失败: {e}") + + def _refresh_device_list(self): + """刷新设备列表""" + try: + if self.plugin.device_manager: + devices = self.plugin.device_manager.get_all_input_devices() + self.editor_data['device_cache'] = devices + # 在实际实现中,这里会更新UI组件 + except Exception as e: + print(f"✗ 设备列表刷新失败: {e}") + + def _refresh_property_panel(self): + """刷新属性面板""" + try: + # 根据当前选择更新属性面板 + selected_device = self.editor_state['selected_device'] + selected_mapping = self.editor_state['selected_mapping'] + + # 在实际实现中,这里会更新属性面板的显示内容 + pass + except Exception as e: + print(f"✗ 属性面板刷新失败: {e}") + + def _update_preview_panel(self): + """更新预览面板""" + try: + current_time = time.time() + if current_time - self.last_preview_update >= 0.1: # 100ms + # 更新预览数据 + self.editor_data['live_data'] = self._collect_live_data() + + # 在实际实现中,这里会更新预览面板的显示 + self.editor_stats['preview_updates'] += 1 + self.last_preview_update = current_time + + except Exception as e: + print(f"✗ 预览面板更新失败: {e}") + + def _collect_live_data(self) -> Dict[str, Any]: + """ + 收集实时数据 + + Returns: + 实时数据字典 + """ + try: + live_data = { + 'timestamp': time.time(), + 'devices': {}, + 'inputs': {}, + 'gestures': {}, + 'haptics': {} + } + + # 收集设备数据 + if self.plugin.device_manager: + live_data['devices'] = self.plugin.device_manager.get_device_stats() + + # 收集输入数据 + if self.plugin.input_manager: + live_data['inputs'] = self.plugin.input_manager.get_stats() + + # 收集手势数据 + if self.plugin.gesture_recognizer: + live_data['gestures'] = self.plugin.gesture_recognizer.get_gesture_stats() + + # 收集触觉数据 + if self.plugin.haptic_manager: + live_data['haptics'] = self.plugin.haptic_manager.get_haptic_stats() + + return live_data + + except Exception as e: + print(f"✗ 实时数据收集失败: {e}") + return {} + + def _update_status_bar(self): + """更新状态栏""" + try: + # 在实际实现中,这里会更新状态栏的显示内容 + pass + except Exception as e: + print(f"✗ 状态栏更新失败: {e}") + + def set_editor_mode(self, mode: EditorMode) -> bool: + """ + 设置编辑器模式 + + Args: + mode: 编辑器模式 + + Returns: + 是否设置成功 + """ + try: + old_mode = self.editor_state['current_mode'] + self.editor_state['current_mode'] = mode + + # 触发模式变更回调 + self._trigger_editor_callback('mode_changed', { + 'old_mode': old_mode.value, + 'new_mode': mode.value + }) + + print(f"✓ 编辑器模式已设置: {mode.value}") + return True + + except Exception as e: + print(f"✗ 编辑器模式设置失败: {e}") + return False + + def select_device(self, device_id: int) -> bool: + """ + 选择设备 + + Args: + device_id: 设备ID + + Returns: + 是否选择成功 + """ + try: + self.editor_state['selected_device'] = device_id + + # 触发设备选择回调 + self._trigger_editor_callback('device_selected', { + 'device_id': device_id + }) + + print(f"✓ 设备已选择: ID {device_id}") + return True + + except Exception as e: + print(f"✗ 设备选择失败: {e}") + return False + + def select_mapping(self, mapping_id: int) -> bool: + """ + 选择映射 + + Args: + mapping_id: 映射ID + + Returns: + 是否选择成功 + """ + try: + self.editor_state['selected_mapping'] = mapping_id + + # 触发映射选择回调 + self._trigger_editor_callback('mapping_selected', { + 'mapping_id': mapping_id + }) + + print(f"✓ 映射已选择: ID {mapping_id}") + return True + + except Exception as e: + print(f"✗ 映射选择失败: {e}") + return False + + def select_gesture(self, gesture_id: int) -> bool: + """ + 选择手势 + + Args: + gesture_id: 手势ID + + Returns: + 是否选择成功 + """ + try: + self.editor_state['selected_gesture'] = gesture_id + + # 触发手势选择回调 + self._trigger_editor_callback('gesture_selected', { + 'gesture_id': gesture_id + }) + + print(f"✓ 手势已选择: ID {gesture_id}") + return True + + except Exception as e: + print(f"✗ 手势选择失败: {e}") + return False + + def select_effect(self, effect_id: int) -> bool: + """ + 选择触觉效果 + + Args: + effect_id: 效果ID + + Returns: + 是否选择成功 + """ + try: + self.editor_state['selected_effect'] = effect_id + + # 触发效果选择回调 + self._trigger_editor_callback('effect_selected', { + 'effect_id': effect_id + }) + + print(f"✓ 触觉效果已选择: ID {effect_id}") + return True + + except Exception as e: + print(f"✗ 触觉效果选择失败: {e}") + return False + + def start_recording(self) -> bool: + """ + 开始录制 + + Returns: + 是否开始成功 + """ + try: + self.editor_state['is_recording'] = True + print("✓ 录制已开始") + return True + + except Exception as e: + print(f"✗ 录制开始失败: {e}") + return False + + def stop_recording(self) -> bool: + """ + 停止录制 + + Returns: + 是否停止成功 + """ + try: + self.editor_state['is_recording'] = False + print("✓ 录制已停止") + return True + + except Exception as e: + print(f"✗ 录制停止失败: {e}") + return False + + def start_testing(self) -> bool: + """ + 开始测试 + + Returns: + 是否开始成功 + """ + try: + self.editor_state['is_testing'] = True + print("✓ 测试已开始") + return True + + except Exception as e: + print(f"✗ 测试开始失败: {e}") + return False + + def stop_testing(self) -> bool: + """ + 停止测试 + + Returns: + 是否停止成功 + """ + try: + self.editor_state['is_testing'] = False + print("✓ 测试已停止") + return True + + except Exception as e: + print(f"✗ 测试停止失败: {e}") + return False + + def toggle_preview(self, enable: bool = None) -> bool: + """ + 切换预览 + + Args: + enable: 是否启用预览(None表示切换) + + Returns: + 是否切换成功 + """ + try: + if enable is None: + enable = not self.editor_state['preview_active'] + + self.editor_state['preview_active'] = enable + self.editor_config['enable_preview'] = enable + + status = "启用" if enable else "禁用" + print(f"✓ 预览已{status}") + return True + + except Exception as e: + print(f"✗ 预览切换失败: {e}") + return False + + def undo_operation(self) -> bool: + """ + 撤销操作 + + Returns: + 是否撤销成功 + """ + try: + if self.history_pointer >= 0 and self.operation_history: + # 执行撤销操作 + operation = self.operation_history[self.history_pointer] + self.history_pointer -= 1 + self.editor_stats['undo_operations'] += 1 + + print("✓ 操作已撤销") + return True + else: + print("⚠ 没有可撤销的操作") + return False + + except Exception as e: + print(f"✗ 操作撤销失败: {e}") + return False + + def redo_operation(self) -> bool: + """ + 重做操作 + + Returns: + 是否重做成功 + """ + try: + if self.history_pointer < len(self.operation_history) - 1: + # 执行重做操作 + self.history_pointer += 1 + operation = self.operation_history[self.history_pointer] + self.editor_stats['redo_operations'] += 1 + + print("✓ 操作已重做") + return True + else: + print("⚠ 没有可重做的操作") + return False + + except Exception as e: + print(f"✗ 操作重做失败: {e}") + return False + + def add_operation_to_history(self, operation: Dict[str, Any]): + """ + 添加操作到历史 + + Args: + operation: 操作数据 + """ + try: + # 添加到历史记录 + self.operation_history.append(operation) + self.history_pointer = len(self.operation_history) - 1 + + # 保持历史记录大小 + if len(self.operation_history) > self.max_history_size: + remove_count = len(self.operation_history) - self.max_history_size + self.operation_history = self.operation_history[remove_count:] + self.history_pointer -= remove_count + self.history_pointer = max(0, self.history_pointer) + + self.editor_stats['operations_performed'] += 1 + + except Exception as e: + print(f"✗ 操作添加到历史失败: {e}") + + def _trigger_editor_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发编辑器回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.editor_callbacks: + for callback in self.editor_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 编辑器回调执行失败: {e}") + except Exception as e: + print(f"✗ 编辑器回调触发失败: {e}") + + def register_editor_callback(self, callback_type: str, callback: callable): + """ + 注册编辑器回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.editor_callbacks: + self.editor_callbacks[callback_type].append(callback) + print(f"✓ 编辑器回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 编辑器回调注册失败: {e}") + + def unregister_editor_callback(self, callback_type: str, callback: callable): + """ + 注销编辑器回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.editor_callbacks: + if callback in self.editor_callbacks[callback_type]: + self.editor_callbacks[callback_type].remove(callback) + print(f"✓ 编辑器回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 编辑器回调注销失败: {e}") + + def get_editor_state(self) -> Dict[str, Any]: + """ + 获取编辑器状态 + + Returns: + 编辑器状态字典 + """ + return self.editor_state.copy() + + def get_editor_config(self) -> Dict[str, Any]: + """ + 获取编辑器配置 + + Returns: + 编辑器配置字典 + """ + return self.editor_config.copy() + + def set_editor_config(self, config: Dict[str, Any]) -> bool: + """ + 设置编辑器配置 + + Args: + config: 编辑器配置字典 + + Returns: + 是否设置成功 + """ + try: + self.editor_config.update(config) + print(f"✓ 编辑器配置已更新: {self.editor_config}") + return True + except Exception as e: + print(f"✗ 编辑器配置设置失败: {e}") + return False + + def get_editor_data(self, data_type: str) -> Optional[Dict[str, Any]]: + """ + 获取编辑器数据 + + Args: + data_type: 数据类型 + + Returns: + 数据字典或None + """ + return self.editor_data.get(data_type) + + def get_monitor_data(self) -> Dict[str, Any]: + """ + 获取监控数据 + + Returns: + 监控数据字典 + """ + return self.monitor_data.copy() + + def get_editor_stats(self) -> Dict[str, int]: + """ + 获取编辑器统计信息 + + Returns: + 编辑器统计字典 + """ + return self.editor_stats.copy() + + def reset_editor_stats(self): + """重置编辑器统计信息""" + try: + self.editor_stats = { + 'windows_opened': 0, + 'operations_performed': 0, + 'undo_operations': 0, + 'redo_operations': 0, + 'data_refreshed': 0, + 'preview_updates': 0 + } + print("✓ 编辑器统计信息已重置") + except Exception as e: + print(f"✗ 编辑器统计信息重置失败: {e}") + + def refresh_editor_data(self): + """刷新编辑器数据""" + try: + self._load_editor_cache() + self._update_editor_display() + print("✓ 编辑器数据已刷新") + except Exception as e: + print(f"✗ 编辑器数据刷新失败: {e}") + + def show_editor_window(self) -> bool: + """ + 显示编辑器窗口 + + Returns: + 是否显示成功 + """ + try: + # 在实际实现中,这里会显示实际的编辑器窗口 + self.editor_stats['windows_opened'] += 1 + print("✓ 编辑器窗口已显示") + return True + except Exception as e: + print(f"✗ 编辑器窗口显示失败: {e}") + return False + + def hide_editor_window(self) -> bool: + """ + 隐藏编辑器窗口 + + Returns: + 是否隐藏成功 + """ + try: + # 在实际实现中,这里会隐藏实际的编辑器窗口 + print("✓ 编辑器窗口已隐藏") + return True + except Exception as e: + print(f"✗ 编辑器窗口隐藏失败: {e}") + return False + + def export_editor_data(self, file_path: str) -> bool: + """ + 导出编辑器数据 + + Args: + file_path: 导出文件路径 + + Returns: + 是否导出成功 + """ + try: + # 在实际实现中,这里会导出编辑器数据到文件 + print(f"✓ 编辑器数据已导出: {file_path}") + return True + except Exception as e: + print(f"✗ 编辑器数据导出失败: {e}") + return False + + def import_editor_data(self, file_path: str) -> bool: + """ + 导入编辑器数据 + + Args: + file_path: 导入文件路径 + + Returns: + 是否导入成功 + """ + try: + # 在实际实现中,这里会从文件导入编辑器数据 + print(f"✓ 编辑器数据已导入: {file_path}") + return True + except Exception as e: + print(f"✗ 编辑器数据导入失败: {e}") + return False + + # 新增的设备校准相关方法 + def start_device_calibration(self, device_id: int) -> bool: + """ + 开始设备校准 + + Args: + device_id: 设备ID + + Returns: + 是否开始成功 + """ + try: + if self.plugin.device_calibrator: + result = self.plugin.device_calibrator.start_calibration(device_id) + if result: + print(f"✓ 设备校准已开始: ID {device_id}") + return result + else: + print("✗ 设备校准器未初始化") + return False + except Exception as e: + print(f"✗ 设备校准启动失败: {e}") + return False + + def cancel_device_calibration(self) -> bool: + """ + 取消设备校准 + + Returns: + 是否取消成功 + """ + try: + if self.plugin.device_calibrator: + result = self.plugin.device_calibrator.cancel_calibration() + if result: + print("✓ 设备校准已取消") + return result + else: + print("✗ 设备校准器未初始化") + return False + except Exception as e: + print(f"✗ 设备校准取消失败: {e}") + return False + + def get_calibration_status(self) -> Dict[str, Any]: + """ + 获取校准状态 + + Returns: + 校准状态字典 + """ + try: + if self.plugin.device_calibrator: + return self.plugin.device_calibrator.get_calibration_status() + else: + return {} + except Exception as e: + print(f"✗ 校准状态获取失败: {e}") + return {} + + def _on_calibration_progress(self, data: Dict[str, Any]): + """ + 校准进度回调 + + Args: + data: 进度数据 + """ + try: + # 更新监控数据 + self.monitor_data['calibration_progress'] = data + + # 触发编辑器回调 + self._trigger_editor_callback('calibration_progress', data) + + print(f"✓ 校准进度更新: {data}") + except Exception as e: + print(f"✗ 校准进度处理失败: {e}") + + def _on_calibration_completed(self, data: Dict[str, Any]): + """ + 校准完成回调 + + Args: + data: 完成数据 + """ + try: + print(f"✓ 设备校准完成: {data}") + + # 刷新设备列表以显示校准状态 + self._refresh_device_list() + except Exception as e: + print(f"✗ 校准完成处理失败: {e}") + + # 新增的高级输入处理相关方法 + def create_input_pattern(self, name: str, inputs: List[str], context: str, + description: str = "") -> int: + """ + 创建输入模式 + + Args: + name: 模式名称 + inputs: 输入序列 + context: 上下文 + description: 描述 + + Returns: + 模式ID + """ + try: + if self.plugin.advanced_input_processor: + from .core.advanced_input_processor import InputContext + context_enum = getattr(InputContext, context.upper(), InputContext.GLOBAL) + pattern_id = self.plugin.advanced_input_processor.create_input_pattern( + name, inputs, context_enum, description) + if pattern_id >= 0: + print(f"✓ 输入模式已创建: {name} (ID: {pattern_id})") + return pattern_id + else: + print("✗ 高级输入处理器未初始化") + return -1 + except Exception as e: + print(f"✗ 输入模式创建失败: {e}") + return -1 + + def _on_pattern_matched(self, data: Dict[str, Any]): + """ + 模式匹配回调 + + Args: + data: 匹配数据 + """ + try: + # 添加到监控数据 + self.monitor_data['advanced_input_events'].append({ + 'type': 'pattern_matched', + 'data': data, + 'timestamp': time.time() + }) + + # 触发编辑器回调 + self._trigger_editor_callback('pattern_matched', data) + + print(f"✓ 输入模式匹配: {data}") + except Exception as e: + print(f"✗ 模式匹配处理失败: {e}") + + def _on_sequence_completed(self, data: Dict[str, Any]): + """ + 序列完成回调 + + Args: + data: 完成数据 + """ + try: + # 添加到监控数据 + self.monitor_data['advanced_input_events'].append({ + 'type': 'sequence_completed', + 'data': data, + 'timestamp': time.time() + }) + + print(f"✓ 输入序列完成: {data}") + except Exception as e: print(f"✗ 序列完成处理失败: {e}") \ No newline at end of file diff --git a/plugins/user/multiplatform_input/events/event_system.py b/plugins/user/multiplatform_input/events/event_system.py index 4dca8bed..22c58345 100644 --- a/plugins/user/multiplatform_input/events/event_system.py +++ b/plugins/user/multiplatform_input/events/event_system.py @@ -1,685 +1,685 @@ -""" -事件系统模块 -负责处理和分发各种输入事件 -""" - -import time -from typing import Dict, Any, List, Optional, Callable -import threading -import queue -from enum import Enum -from dataclasses import dataclass -from collections import defaultdict - -class EventType(Enum): - """事件类型枚举""" - INPUT_PRESSED = "input_pressed" - INPUT_RELEASED = "input_released" - INPUT_HELD = "input_held" - INPUT_CLICKED = "input_clicked" - INPUT_DOUBLE_CLICKED = "input_double_clicked" - INPUT_LONG_PRESSED = "input_long_pressed" - INPUT_LONG_PRESSING = "input_long_pressing" - INPUT_SWIPE = "input_swipe" - INPUT_DRAG = "input_drag" - INPUT_PINCH = "input_pinch" - INPUT_ROTATE = "input_rotate" - INPUT_SHAKE = "input_shake" - DEVICE_CONNECTED = "device_connected" - DEVICE_DISCONNECTED = "device_disconnected" - GESTURE_RECOGNIZED = "gesture_recognized" - HAPTIC_FEEDBACK = "haptic_feedback" - CUSTOM = "custom" - -@dataclass -class InputEvent: - """输入事件数据类""" - event_type: EventType - device_id: int - input_id: str - value: Any = None - timestamp: float = 0.0 - position: tuple = None # (x, y) 对于触摸和鼠标事件 - velocity: tuple = None # (vx, vy) 对于移动事件 - duration: float = 0.0 # 事件持续时间 - metadata: Dict[str, Any] = None - -class EventSystem: - """ - 事件系统 - 负责处理和分发各种输入事件 - """ - - def __init__(self, plugin): - """ - 初始化事件系统 - - Args: - plugin: 多平台输入支持插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 事件队列 - self.event_queue = queue.Queue() - self.processed_events = queue.Queue() - - # 事件监听器 - self.event_listeners = defaultdict(list) - - # 事件过滤器 - self.event_filters = [] - - # 事件配置 - self.event_config = { - 'queue_max_size': 1000, - 'process_interval': 0.016, # ~60 FPS - 'batch_process_size': 10, - 'enable_event_coalescing': True, - 'enable_event_filtering': True, - 'duplicate_suppression_time': 0.01 # 10ms内重复事件抑制 - } - - # 事件历史记录 - self.event_history = [] - self.history_config = { - 'max_history_size': 100, - 'record_all_events': False, - 'recorded_event_types': [ - EventType.INPUT_PRESSED, - EventType.INPUT_RELEASED, - EventType.GESTURE_RECOGNIZED - ] - } - - # 事件统计 - self.event_stats = { - 'events_queued': 0, - 'events_processed': 0, - 'events_dropped': 0, - 'listeners_called': 0, - 'filters_applied': 0, - 'events_coalesced': 0 - } - - # 时间戳记录(用于重复事件抑制) - self.last_event_timestamps = {} - - # 线程管理 - self.event_thread = None - self.running = False - - # 锁 - self.event_lock = threading.Lock() - self.history_lock = threading.Lock() - - # 回调函数 - self.global_event_callback = None - - print("✓ 事件系统已创建") - - def initialize(self) -> bool: - """ - 初始化事件系统 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 事件系统初始化完成") - return True - - except Exception as e: - print(f"✗ 事件系统初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用事件系统 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 事件系统未初始化") - return False - - self.enabled = True - self.running = True - - # 启动事件处理线程 - self.event_thread = threading.Thread(target=self._event_processing_loop, daemon=True) - self.event_thread.start() - - print("✓ 事件系统已启用") - return True - - except Exception as e: - print(f"✗ 事件系统启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用事件系统""" - try: - self.enabled = False - self.running = False - - # 等待事件处理线程结束 - if self.event_thread and self.event_thread.is_alive(): - self.event_thread.join(timeout=2.0) - - # 清理队列 - while not self.event_queue.empty(): - try: - self.event_queue.get_nowait() - except queue.Empty: - break - - while not self.processed_events.empty(): - try: - self.processed_events.get_nowait() - except queue.Empty: - break - - print("✓ 事件系统已禁用") - - except Exception as e: - print(f"✗ 事件系统禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理事件系统资源""" - try: - self.disable() - self.event_listeners.clear() - self.event_filters.clear() - self.event_history.clear() - self.last_event_timestamps.clear() - self.initialized = False - print("✓ 事件系统资源已清理") - - except Exception as e: - print(f"✗ 事件系统资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新事件系统状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 处理已完成的事件 - self._process_completed_events() - - # 更新统计信息 - pass # 统计信息在其他地方更新 - - except Exception as e: - print(f"✗ 事件系统更新失败: {e}") - import traceback - traceback.print_exc() - - def _event_processing_loop(self): - """事件处理循环""" - try: - process_interval = self.event_config['process_interval'] - - while self.running: - if self.enabled: - self._process_events() - - time.sleep(process_interval) - - except Exception as e: - print(f"✗ 事件处理循环失败: {e}") - - def _process_events(self): - """处理事件队列""" - try: - processed_count = 0 - batch_size = self.event_config['batch_process_size'] - - while processed_count < batch_size and not self.event_queue.empty(): - try: - event = self.event_queue.get_nowait() - - # 应用事件过滤器 - if self.event_config['enable_event_filtering']: - if not self._apply_filters(event): - self.event_stats['events_dropped'] += 1 - continue - - # 事件合并 - if self.event_config['enable_event_coalescing']: - if self._coalesce_event(event): - self.event_stats['events_coalesced'] += 1 - continue - - # 重复事件抑制 - if self._suppress_duplicate_event(event): - self.event_stats['events_dropped'] += 1 - continue - - # 分发事件 - self._dispatch_event(event) - - # 记录事件历史 - self._record_event_history(event) - - self.event_stats['events_processed'] += 1 - processed_count += 1 - - except queue.Empty: - break - except Exception as e: - print(f"✗ 事件处理失败: {e}") - self.event_stats['events_dropped'] += 1 - - except Exception as e: - print(f"✗ 事件队列处理失败: {e}") - - def _process_completed_events(self): - """处理已完成的事件""" - try: - # 这里可以处理需要在主线程执行的事件 - pass - except Exception as e: - print(f"✗ 已完成事件处理失败: {e}") - - def _apply_filters(self, event: InputEvent) -> bool: - """ - 应用事件过滤器 - - Args: - event: 输入事件 - - Returns: - 是否通过过滤器 - """ - try: - self.event_stats['filters_applied'] += len(self.event_filters) - - for filter_func in self.event_filters: - if not filter_func(event): - return False - return True - except Exception as e: - print(f"✗ 事件过滤器应用失败: {e}") - return True # 默认通过 - - def _coalesce_event(self, event: InputEvent) -> bool: - """ - 合并事件(减少重复事件) - - Args: - event: 输入事件 - - Returns: - 是否合并了事件 - """ - try: - # 简化的事件合并逻辑 - # 在实际实现中,可以基于事件类型和设备进行更复杂的合并 - return False - except Exception as e: - print(f"✗ 事件合并失败: {e}") - return False - - def _suppress_duplicate_event(self, event: InputEvent) -> bool: - """ - 抑制重复事件 - - Args: - event: 输入事件 - - Returns: - 是否抑制了事件 - """ - try: - # 创建事件标识符 - event_key = f"{event.event_type.value}:{event.device_id}:{event.input_id}" - - current_time = time.time() - last_time = self.last_event_timestamps.get(event_key, 0.0) - - if current_time - last_time < self.event_config['duplicate_suppression_time']: - return True - - self.last_event_timestamps[event_key] = current_time - return False - except Exception as e: - print(f"✗ 重复事件抑制失败: {e}") - return False - - def _dispatch_event(self, event: InputEvent): - """ - 分发事件 - - Args: - event: 输入事件 - """ - try: - # 调用全局回调 - if self.global_event_callback: - try: - self.global_event_callback(event) - except Exception as e: - print(f"✗ 全局事件回调执行失败: {e}") - - # 调用特定类型监听器 - listeners = self.event_listeners[event.event_type] - for listener in listeners: - try: - listener(event) - self.event_stats['listeners_called'] += 1 - except Exception as e: - print(f"✗ 事件监听器执行失败: {e}") - - # 将事件放入已完成队列(如果需要主线程处理) - self.processed_events.put(event) - - except Exception as e: - print(f"✗ 事件分发失败: {e}") - - def _record_event_history(self, event: InputEvent): - """ - 记录事件历史 - - Args: - event: 输入事件 - """ - try: - # 检查是否需要记录此事件类型 - should_record = self.history_config['record_all_events'] or \ - event.event_type in self.history_config['recorded_event_types'] - - if not should_record: - return - - with self.history_lock: - self.event_history.append(event) - - # 保持历史记录大小 - if len(self.event_history) > self.history_config['max_history_size']: - self.event_history.pop(0) - - except Exception as e: - print(f"✗ 事件历史记录失败: {e}") - - def queue_event(self, event: InputEvent): - """ - 将事件加入队列 - - Args: - event: 输入事件 - """ - try: - if not self.enabled: - return - - # 设置时间戳 - if event.timestamp == 0.0: - event.timestamp = time.time() - - # 检查队列大小 - if self.event_queue.qsize() >= self.event_config['queue_max_size']: - # 丢弃最旧的事件 - try: - self.event_queue.get_nowait() - self.event_stats['events_dropped'] += 1 - except queue.Empty: - pass - - self.event_queue.put(event) - self.event_stats['events_queued'] += 1 - - except Exception as e: - print(f"✗ 事件入队失败: {e}") - - def create_event(self, event_type: EventType, device_id: int, input_id: str, - value: Any = None, position: tuple = None, velocity: tuple = None, - duration: float = 0.0, metadata: Dict[str, Any] = None) -> InputEvent: - """ - 创建输入事件 - - Args: - event_type: 事件类型 - device_id: 设备ID - input_id: 输入ID - value: 输入值 - position: 位置坐标 - velocity: 速度向量 - duration: 持续时间 - metadata: 元数据 - - Returns: - 输入事件 - """ - try: - event = InputEvent( - event_type=event_type, - device_id=device_id, - input_id=input_id, - value=value, - timestamp=time.time(), - position=position, - velocity=velocity, - duration=duration, - metadata=metadata or {} - ) - return event - except Exception as e: - print(f"✗ 事件创建失败: {e}") - return None - - def register_event_listener(self, event_type: EventType, listener: Callable): - """ - 注册事件监听器 - - Args: - event_type: 事件类型 - listener: 监听器回调函数 - """ - try: - self.event_listeners[event_type].append(listener) - print(f"✓ 事件监听器已注册: {event_type.value}") - except Exception as e: - print(f"✗ 事件监听器注册失败: {e}") - - def unregister_event_listener(self, event_type: EventType, listener: Callable): - """ - 注销事件监听器 - - Args: - event_type: 事件类型 - listener: 监听器回调函数 - """ - try: - if event_type in self.event_listeners: - if listener in self.event_listeners[event_type]: - self.event_listeners[event_type].remove(listener) - print(f"✓ 事件监听器已注销: {event_type.value}") - except Exception as e: - print(f"✗ 事件监听器注销失败: {e}") - - def add_event_filter(self, filter_func: Callable[[InputEvent], bool]): - """ - 添加事件过滤器 - - Args: - filter_func: 过滤器函数,返回True表示通过,False表示过滤 - """ - try: - self.event_filters.append(filter_func) - print("✓ 事件过滤器已添加") - except Exception as e: - print(f"✗ 事件过滤器添加失败: {e}") - - def remove_event_filter(self, filter_func: Callable[[InputEvent], bool]): - """ - 移除事件过滤器 - - Args: - filter_func: 过滤器函数 - """ - try: - if filter_func in self.event_filters: - self.event_filters.remove(filter_func) - print("✓ 事件过滤器已移除") - except Exception as e: - print(f"✗ 事件过滤器移除失败: {e}") - - def set_global_event_callback(self, callback: Callable[[InputEvent], None]): - """ - 设置全局事件回调 - - Args: - callback: 全局回调函数 - """ - try: - self.global_event_callback = callback - print("✓ 全局事件回调已设置") - except Exception as e: - print(f"✗ 全局事件回调设置失败: {e}") - - def get_event_history(self, event_type: EventType = None, limit: int = None) -> List[InputEvent]: - """ - 获取事件历史记录 - - Args: - event_type: 事件类型过滤器 - limit: 返回事件数量限制 - - Returns: - 事件历史列表 - """ - try: - with self.history_lock: - history = self.event_history.copy() - - if event_type: - history = [event for event in history if event.event_type == event_type] - - if limit: - history = history[-limit:] - - return history - except Exception as e: - print(f"✗ 事件历史获取失败: {e}") - return [] - - def clear_event_history(self): - """清空事件历史""" - try: - with self.history_lock: - self.event_history.clear() - print("✓ 事件历史已清空") - except Exception as e: - print(f"✗ 事件历史清空失败: {e}") - - def set_event_config(self, config: Dict[str, Any]): - """ - 设置事件配置 - - Args: - config: 事件配置字典 - """ - try: - self.event_config.update(config) - print(f"✓ 事件配置已更新: {self.event_config}") - except Exception as e: - print(f"✗ 事件配置设置失败: {e}") - - def get_event_config(self) -> Dict[str, Any]: - """ - 获取事件配置 - - Returns: - 事件配置字典 - """ - return self.event_config.copy() - - def set_history_config(self, config: Dict[str, Any]): - """ - 设置历史记录配置 - - Args: - config: 历史记录配置字典 - """ - try: - self.history_config.update(config) - print(f"✓ 历史记录配置已更新: {self.history_config}") - except Exception as e: - print(f"✗ 历史记录配置设置失败: {e}") - - def get_history_config(self) -> Dict[str, Any]: - """ - 获取历史记录配置 - - Returns: - 历史记录配置字典 - """ - return self.history_config.copy() - - def get_event_stats(self) -> Dict[str, int]: - """ - 获取事件统计信息 - - Returns: - 事件统计字典 - """ - return self.event_stats.copy() - - def reset_event_stats(self): - """重置事件统计信息""" - try: - self.event_stats = { - 'events_queued': 0, - 'events_processed': 0, - 'events_dropped': 0, - 'listeners_called': 0, - 'filters_applied': 0, - 'events_coalesced': 0 - } - print("✓ 事件统计信息已重置") - except Exception as e: - print(f"✗ 事件统计信息重置失败: {e}") - - def get_pending_events_count(self) -> int: - """ - 获取待处理事件数量 - - Returns: - 待处理事件数量 - """ - try: - return self.event_queue.qsize() - except Exception as e: - print(f"✗ 待处理事件数量获取失败: {e}") - return 0 - - def flush_events(self): - """清空事件队列""" - try: - while not self.event_queue.empty(): - try: - self.event_queue.get_nowait() - except queue.Empty: - break - print("✓ 事件队列已清空") - except Exception as e: +""" +事件系统模块 +负责处理和分发各种输入事件 +""" + +import time +from typing import Dict, Any, List, Optional, Callable +import threading +import queue +from enum import Enum +from dataclasses import dataclass +from collections import defaultdict + +class EventType(Enum): + """事件类型枚举""" + INPUT_PRESSED = "input_pressed" + INPUT_RELEASED = "input_released" + INPUT_HELD = "input_held" + INPUT_CLICKED = "input_clicked" + INPUT_DOUBLE_CLICKED = "input_double_clicked" + INPUT_LONG_PRESSED = "input_long_pressed" + INPUT_LONG_PRESSING = "input_long_pressing" + INPUT_SWIPE = "input_swipe" + INPUT_DRAG = "input_drag" + INPUT_PINCH = "input_pinch" + INPUT_ROTATE = "input_rotate" + INPUT_SHAKE = "input_shake" + DEVICE_CONNECTED = "device_connected" + DEVICE_DISCONNECTED = "device_disconnected" + GESTURE_RECOGNIZED = "gesture_recognized" + HAPTIC_FEEDBACK = "haptic_feedback" + CUSTOM = "custom" + +@dataclass +class InputEvent: + """输入事件数据类""" + event_type: EventType + device_id: int + input_id: str + value: Any = None + timestamp: float = 0.0 + position: tuple = None # (x, y) 对于触摸和鼠标事件 + velocity: tuple = None # (vx, vy) 对于移动事件 + duration: float = 0.0 # 事件持续时间 + metadata: Dict[str, Any] = None + +class EventSystem: + """ + 事件系统 + 负责处理和分发各种输入事件 + """ + + def __init__(self, plugin): + """ + 初始化事件系统 + + Args: + plugin: 多平台输入支持插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 事件队列 + self.event_queue = queue.Queue() + self.processed_events = queue.Queue() + + # 事件监听器 + self.event_listeners = defaultdict(list) + + # 事件过滤器 + self.event_filters = [] + + # 事件配置 + self.event_config = { + 'queue_max_size': 1000, + 'process_interval': 0.016, # ~60 FPS + 'batch_process_size': 10, + 'enable_event_coalescing': True, + 'enable_event_filtering': True, + 'duplicate_suppression_time': 0.01 # 10ms内重复事件抑制 + } + + # 事件历史记录 + self.event_history = [] + self.history_config = { + 'max_history_size': 100, + 'record_all_events': False, + 'recorded_event_types': [ + EventType.INPUT_PRESSED, + EventType.INPUT_RELEASED, + EventType.GESTURE_RECOGNIZED + ] + } + + # 事件统计 + self.event_stats = { + 'events_queued': 0, + 'events_processed': 0, + 'events_dropped': 0, + 'listeners_called': 0, + 'filters_applied': 0, + 'events_coalesced': 0 + } + + # 时间戳记录(用于重复事件抑制) + self.last_event_timestamps = {} + + # 线程管理 + self.event_thread = None + self.running = False + + # 锁 + self.event_lock = threading.Lock() + self.history_lock = threading.Lock() + + # 回调函数 + self.global_event_callback = None + + print("✓ 事件系统已创建") + + def initialize(self) -> bool: + """ + 初始化事件系统 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 事件系统初始化完成") + return True + + except Exception as e: + print(f"✗ 事件系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用事件系统 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 事件系统未初始化") + return False + + self.enabled = True + self.running = True + + # 启动事件处理线程 + self.event_thread = threading.Thread(target=self._event_processing_loop, daemon=True) + self.event_thread.start() + + print("✓ 事件系统已启用") + return True + + except Exception as e: + print(f"✗ 事件系统启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用事件系统""" + try: + self.enabled = False + self.running = False + + # 等待事件处理线程结束 + if self.event_thread and self.event_thread.is_alive(): + self.event_thread.join(timeout=2.0) + + # 清理队列 + while not self.event_queue.empty(): + try: + self.event_queue.get_nowait() + except queue.Empty: + break + + while not self.processed_events.empty(): + try: + self.processed_events.get_nowait() + except queue.Empty: + break + + print("✓ 事件系统已禁用") + + except Exception as e: + print(f"✗ 事件系统禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理事件系统资源""" + try: + self.disable() + self.event_listeners.clear() + self.event_filters.clear() + self.event_history.clear() + self.last_event_timestamps.clear() + self.initialized = False + print("✓ 事件系统资源已清理") + + except Exception as e: + print(f"✗ 事件系统资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新事件系统状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 处理已完成的事件 + self._process_completed_events() + + # 更新统计信息 + pass # 统计信息在其他地方更新 + + except Exception as e: + print(f"✗ 事件系统更新失败: {e}") + import traceback + traceback.print_exc() + + def _event_processing_loop(self): + """事件处理循环""" + try: + process_interval = self.event_config['process_interval'] + + while self.running: + if self.enabled: + self._process_events() + + time.sleep(process_interval) + + except Exception as e: + print(f"✗ 事件处理循环失败: {e}") + + def _process_events(self): + """处理事件队列""" + try: + processed_count = 0 + batch_size = self.event_config['batch_process_size'] + + while processed_count < batch_size and not self.event_queue.empty(): + try: + event = self.event_queue.get_nowait() + + # 应用事件过滤器 + if self.event_config['enable_event_filtering']: + if not self._apply_filters(event): + self.event_stats['events_dropped'] += 1 + continue + + # 事件合并 + if self.event_config['enable_event_coalescing']: + if self._coalesce_event(event): + self.event_stats['events_coalesced'] += 1 + continue + + # 重复事件抑制 + if self._suppress_duplicate_event(event): + self.event_stats['events_dropped'] += 1 + continue + + # 分发事件 + self._dispatch_event(event) + + # 记录事件历史 + self._record_event_history(event) + + self.event_stats['events_processed'] += 1 + processed_count += 1 + + except queue.Empty: + break + except Exception as e: + print(f"✗ 事件处理失败: {e}") + self.event_stats['events_dropped'] += 1 + + except Exception as e: + print(f"✗ 事件队列处理失败: {e}") + + def _process_completed_events(self): + """处理已完成的事件""" + try: + # 这里可以处理需要在主线程执行的事件 + pass + except Exception as e: + print(f"✗ 已完成事件处理失败: {e}") + + def _apply_filters(self, event: InputEvent) -> bool: + """ + 应用事件过滤器 + + Args: + event: 输入事件 + + Returns: + 是否通过过滤器 + """ + try: + self.event_stats['filters_applied'] += len(self.event_filters) + + for filter_func in self.event_filters: + if not filter_func(event): + return False + return True + except Exception as e: + print(f"✗ 事件过滤器应用失败: {e}") + return True # 默认通过 + + def _coalesce_event(self, event: InputEvent) -> bool: + """ + 合并事件(减少重复事件) + + Args: + event: 输入事件 + + Returns: + 是否合并了事件 + """ + try: + # 简化的事件合并逻辑 + # 在实际实现中,可以基于事件类型和设备进行更复杂的合并 + return False + except Exception as e: + print(f"✗ 事件合并失败: {e}") + return False + + def _suppress_duplicate_event(self, event: InputEvent) -> bool: + """ + 抑制重复事件 + + Args: + event: 输入事件 + + Returns: + 是否抑制了事件 + """ + try: + # 创建事件标识符 + event_key = f"{event.event_type.value}:{event.device_id}:{event.input_id}" + + current_time = time.time() + last_time = self.last_event_timestamps.get(event_key, 0.0) + + if current_time - last_time < self.event_config['duplicate_suppression_time']: + return True + + self.last_event_timestamps[event_key] = current_time + return False + except Exception as e: + print(f"✗ 重复事件抑制失败: {e}") + return False + + def _dispatch_event(self, event: InputEvent): + """ + 分发事件 + + Args: + event: 输入事件 + """ + try: + # 调用全局回调 + if self.global_event_callback: + try: + self.global_event_callback(event) + except Exception as e: + print(f"✗ 全局事件回调执行失败: {e}") + + # 调用特定类型监听器 + listeners = self.event_listeners[event.event_type] + for listener in listeners: + try: + listener(event) + self.event_stats['listeners_called'] += 1 + except Exception as e: + print(f"✗ 事件监听器执行失败: {e}") + + # 将事件放入已完成队列(如果需要主线程处理) + self.processed_events.put(event) + + except Exception as e: + print(f"✗ 事件分发失败: {e}") + + def _record_event_history(self, event: InputEvent): + """ + 记录事件历史 + + Args: + event: 输入事件 + """ + try: + # 检查是否需要记录此事件类型 + should_record = self.history_config['record_all_events'] or \ + event.event_type in self.history_config['recorded_event_types'] + + if not should_record: + return + + with self.history_lock: + self.event_history.append(event) + + # 保持历史记录大小 + if len(self.event_history) > self.history_config['max_history_size']: + self.event_history.pop(0) + + except Exception as e: + print(f"✗ 事件历史记录失败: {e}") + + def queue_event(self, event: InputEvent): + """ + 将事件加入队列 + + Args: + event: 输入事件 + """ + try: + if not self.enabled: + return + + # 设置时间戳 + if event.timestamp == 0.0: + event.timestamp = time.time() + + # 检查队列大小 + if self.event_queue.qsize() >= self.event_config['queue_max_size']: + # 丢弃最旧的事件 + try: + self.event_queue.get_nowait() + self.event_stats['events_dropped'] += 1 + except queue.Empty: + pass + + self.event_queue.put(event) + self.event_stats['events_queued'] += 1 + + except Exception as e: + print(f"✗ 事件入队失败: {e}") + + def create_event(self, event_type: EventType, device_id: int, input_id: str, + value: Any = None, position: tuple = None, velocity: tuple = None, + duration: float = 0.0, metadata: Dict[str, Any] = None) -> InputEvent: + """ + 创建输入事件 + + Args: + event_type: 事件类型 + device_id: 设备ID + input_id: 输入ID + value: 输入值 + position: 位置坐标 + velocity: 速度向量 + duration: 持续时间 + metadata: 元数据 + + Returns: + 输入事件 + """ + try: + event = InputEvent( + event_type=event_type, + device_id=device_id, + input_id=input_id, + value=value, + timestamp=time.time(), + position=position, + velocity=velocity, + duration=duration, + metadata=metadata or {} + ) + return event + except Exception as e: + print(f"✗ 事件创建失败: {e}") + return None + + def register_event_listener(self, event_type: EventType, listener: Callable): + """ + 注册事件监听器 + + Args: + event_type: 事件类型 + listener: 监听器回调函数 + """ + try: + self.event_listeners[event_type].append(listener) + print(f"✓ 事件监听器已注册: {event_type.value}") + except Exception as e: + print(f"✗ 事件监听器注册失败: {e}") + + def unregister_event_listener(self, event_type: EventType, listener: Callable): + """ + 注销事件监听器 + + Args: + event_type: 事件类型 + listener: 监听器回调函数 + """ + try: + if event_type in self.event_listeners: + if listener in self.event_listeners[event_type]: + self.event_listeners[event_type].remove(listener) + print(f"✓ 事件监听器已注销: {event_type.value}") + except Exception as e: + print(f"✗ 事件监听器注销失败: {e}") + + def add_event_filter(self, filter_func: Callable[[InputEvent], bool]): + """ + 添加事件过滤器 + + Args: + filter_func: 过滤器函数,返回True表示通过,False表示过滤 + """ + try: + self.event_filters.append(filter_func) + print("✓ 事件过滤器已添加") + except Exception as e: + print(f"✗ 事件过滤器添加失败: {e}") + + def remove_event_filter(self, filter_func: Callable[[InputEvent], bool]): + """ + 移除事件过滤器 + + Args: + filter_func: 过滤器函数 + """ + try: + if filter_func in self.event_filters: + self.event_filters.remove(filter_func) + print("✓ 事件过滤器已移除") + except Exception as e: + print(f"✗ 事件过滤器移除失败: {e}") + + def set_global_event_callback(self, callback: Callable[[InputEvent], None]): + """ + 设置全局事件回调 + + Args: + callback: 全局回调函数 + """ + try: + self.global_event_callback = callback + print("✓ 全局事件回调已设置") + except Exception as e: + print(f"✗ 全局事件回调设置失败: {e}") + + def get_event_history(self, event_type: EventType = None, limit: int = None) -> List[InputEvent]: + """ + 获取事件历史记录 + + Args: + event_type: 事件类型过滤器 + limit: 返回事件数量限制 + + Returns: + 事件历史列表 + """ + try: + with self.history_lock: + history = self.event_history.copy() + + if event_type: + history = [event for event in history if event.event_type == event_type] + + if limit: + history = history[-limit:] + + return history + except Exception as e: + print(f"✗ 事件历史获取失败: {e}") + return [] + + def clear_event_history(self): + """清空事件历史""" + try: + with self.history_lock: + self.event_history.clear() + print("✓ 事件历史已清空") + except Exception as e: + print(f"✗ 事件历史清空失败: {e}") + + def set_event_config(self, config: Dict[str, Any]): + """ + 设置事件配置 + + Args: + config: 事件配置字典 + """ + try: + self.event_config.update(config) + print(f"✓ 事件配置已更新: {self.event_config}") + except Exception as e: + print(f"✗ 事件配置设置失败: {e}") + + def get_event_config(self) -> Dict[str, Any]: + """ + 获取事件配置 + + Returns: + 事件配置字典 + """ + return self.event_config.copy() + + def set_history_config(self, config: Dict[str, Any]): + """ + 设置历史记录配置 + + Args: + config: 历史记录配置字典 + """ + try: + self.history_config.update(config) + print(f"✓ 历史记录配置已更新: {self.history_config}") + except Exception as e: + print(f"✗ 历史记录配置设置失败: {e}") + + def get_history_config(self) -> Dict[str, Any]: + """ + 获取历史记录配置 + + Returns: + 历史记录配置字典 + """ + return self.history_config.copy() + + def get_event_stats(self) -> Dict[str, int]: + """ + 获取事件统计信息 + + Returns: + 事件统计字典 + """ + return self.event_stats.copy() + + def reset_event_stats(self): + """重置事件统计信息""" + try: + self.event_stats = { + 'events_queued': 0, + 'events_processed': 0, + 'events_dropped': 0, + 'listeners_called': 0, + 'filters_applied': 0, + 'events_coalesced': 0 + } + print("✓ 事件统计信息已重置") + except Exception as e: + print(f"✗ 事件统计信息重置失败: {e}") + + def get_pending_events_count(self) -> int: + """ + 获取待处理事件数量 + + Returns: + 待处理事件数量 + """ + try: + return self.event_queue.qsize() + except Exception as e: + print(f"✗ 待处理事件数量获取失败: {e}") + return 0 + + def flush_events(self): + """清空事件队列""" + try: + while not self.event_queue.empty(): + try: + self.event_queue.get_nowait() + except queue.Empty: + break + print("✓ 事件队列已清空") + except Exception as e: print(f"✗ 事件队列清空失败: {e}") \ No newline at end of file diff --git a/plugins/user/multiplatform_input/gestures/gesture_recognizer.py b/plugins/user/multiplatform_input/gestures/gesture_recognizer.py index f9809f85..ad828e27 100644 --- a/plugins/user/multiplatform_input/gestures/gesture_recognizer.py +++ b/plugins/user/multiplatform_input/gestures/gesture_recognizer.py @@ -1,821 +1,821 @@ -""" -手势识别器模块 -负责识别和处理各种手势输入 -""" - -import time -from typing import Dict, Any, List, Optional, Callable, Tuple -from enum import Enum -import math - -class GestureType(Enum): - """手势类型枚举""" - TAP = "tap" - DOUBLE_TAP = "double_tap" - LONG_PRESS = "long_press" - SWIPE = "swipe" - PINCH = "pinch" - ROTATE = "rotate" - DRAG = "drag" - FLICK = "flick" - CIRCLE = "circle" - SHAKE = "shake" - CUSTOM = "custom" - -class GestureState(Enum): - """手势状态枚举""" - STARTED = "started" - UPDATED = "updated" - ENDED = "ended" - CANCELLED = "cancelled" - RECOGNIZED = "recognized" - -class GestureRecognizer: - """ - 手势识别器 - 负责识别和处理各种手势输入 - """ - - def __init__(self, plugin): - """ - 初始化手势识别器 - - Args: - plugin: 多平台输入支持插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 手势配置 - self.gesture_config = { - 'tap_max_duration': 0.3, # 点击最大持续时间(秒) - 'tap_max_distance': 20.0, # 点击最大移动距离(像素) - 'double_tap_timeout': 0.3, # 双击超时时间(秒) - 'long_press_duration': 0.5, # 长按持续时间(秒) - 'swipe_min_distance': 50.0, # 滑动最小距离(像素) - 'swipe_max_duration': 0.5, # 滑动最大持续时间(秒) - 'pinch_threshold': 20.0, # 捏合阈值(像素) - 'rotate_threshold': 10.0, # 旋转阈值(度) - 'flick_min_velocity': 100.0, # 轻拂最小速度(像素/秒) - 'circle_min_radius': 30.0, # 圆形最小半径(像素) - 'circle_tolerance': 0.3, # 圆形容差 - 'shake_threshold': 20.0, # 摇晃阈值(像素) - 'shake_count': 3, # 摇晃次数阈值 - 'update_interval': 0.016 # 更新间隔(秒) - } - - # 当前手势状态 - self.active_gestures = {} - self.gesture_counter = 0 - - # 触摸点跟踪 - self.touch_points = {} - self.touch_counter = 0 - - # 手势历史 - self.gesture_history = [] - - # 回调函数 - self.gesture_callbacks = {} - - # 统计信息 - self.gesture_stats = { - 'gestures_started': 0, - 'gestures_recognized': 0, - 'gestures_cancelled': 0, - 'gestures_failed': 0, - 'active_gestures': 0 - } - - # 时间戳记录 - self.last_gesture_time = {} - self.gesture_start_time = {} - - print("✓ 手势识别器已创建") - - def initialize(self) -> bool: - """ - 初始化手势识别器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 手势识别器初始化完成") - return True - - except Exception as e: - print(f"✗ 手势识别器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用手势识别器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 手势识别器未初始化") - return False - - self.enabled = True - print("✓ 手势识别器已启用") - return True - - except Exception as e: - print(f"✗ 手势识别器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用手势识别器""" - try: - self.enabled = False - print("✓ 手势识别器已禁用") - - except Exception as e: - print(f"✗ 手势识别器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理手势识别器资源""" - try: - self.disable() - self.active_gestures.clear() - self.touch_points.clear() - self.gesture_history.clear() - self.gesture_callbacks.clear() - self.last_gesture_time.clear() - self.gesture_start_time.clear() - self.initialized = False - print("✓ 手势识别器资源已清理") - - except Exception as e: - print(f"✗ 手势识别器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新手势识别器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新活动手势 - self._update_active_gestures(dt) - - except Exception as e: - print(f"✗ 手势识别器更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_active_gestures(self, dt: float): - """更新活动手势""" - try: - current_time = time.time() - - # 检查超时的手势 - timed_out_gestures = [] - for gesture_id, gesture_data in self.active_gestures.items(): - start_time = self.gesture_start_time.get(gesture_id, current_time) - if current_time - start_time > 5.0: # 5秒超时 - timed_out_gestures.append(gesture_id) - - # 取消超时手势 - for gesture_id in timed_out_gestures: - self._cancel_gesture(gesture_id) - - except Exception as e: - print(f"✗ 活动手势更新失败: {e}") - - def start_touch(self, touch_id: int, x: float, y: float, timestamp: float = None): - """ - 开始触摸 - - Args: - touch_id: 触摸点ID - x: X坐标 - y: Y坐标 - timestamp: 时间戳 - """ - try: - if not self.enabled: - return - - if timestamp is None: - timestamp = time.time() - - # 记录触摸点 - self.touch_points[touch_id] = { - 'id': touch_id, - 'start_x': x, - 'start_y': y, - 'current_x': x, - 'current_y': y, - 'start_time': timestamp, - 'last_update': timestamp, - 'path': [(x, y, timestamp)] - } - - # 如果是第一个触摸点,开始新的手势 - if len(self.touch_points) == 1: - self._start_new_gesture(touch_id, x, y, timestamp) - - except Exception as e: - print(f"✗ 触摸开始处理失败: {e}") - - def update_touch(self, touch_id: int, x: float, y: float, timestamp: float = None): - """ - 更新触摸 - - Args: - touch_id: 触摸点ID - x: X坐标 - y: Y坐标 - timestamp: 时间戳 - """ - try: - if not self.enabled: - return - - if timestamp is None: - timestamp = time.time() - - # 更新触摸点 - if touch_id in self.touch_points: - touch_data = self.touch_points[touch_id] - touch_data['current_x'] = x - touch_data['current_y'] = y - touch_data['last_update'] = timestamp - touch_data['path'].append((x, y, timestamp)) - - # 保持路径大小 - if len(touch_data['path']) > 50: - touch_data['path'].pop(0) - - # 更新活动手势 - self._update_gesture(touch_id, x, y, timestamp) - - except Exception as e: - print(f"✗ 触摸更新处理失败: {e}") - - def end_touch(self, touch_id: int, timestamp: float = None): - """ - 结束触摸 - - Args: - touch_id: 触摸点ID - timestamp: 时间戳 - """ - try: - if not self.enabled: - return - - if timestamp is None: - timestamp = time.time() - - # 更新触摸点 - if touch_id in self.touch_points: - touch_data = self.touch_points[touch_id] - touch_data['last_update'] = timestamp - - # 结束手势 - self._end_gesture(touch_id, timestamp) - - # 移除触摸点 - del self.touch_points[touch_id] - - except Exception as e: - print(f"✗ 触摸结束处理失败: {e}") - - def cancel_touch(self, touch_id: int, timestamp: float = None): - """ - 取消触摸 - - Args: - touch_id: 触摸点ID - timestamp: 时间戳 - """ - try: - if not self.enabled: - return - - if timestamp is None: - timestamp = time.time() - - # 取消手势 - if touch_id in self.touch_points: - self._cancel_gesture(touch_id) - del self.touch_points[touch_id] - - except Exception as e: - print(f"✗ 触摸取消处理失败: {e}") - - def _start_new_gesture(self, touch_id: int, x: float, y: float, timestamp: float): - """ - 开始新手势 - - Args: - touch_id: 触摸点ID - x: X坐标 - y: Y坐标 - timestamp: 时间戳 - """ - try: - gesture_id = self.gesture_counter - self.gesture_counter += 1 - - gesture_data = { - 'id': gesture_id, - 'type': None, - 'state': GestureState.STARTED, - 'start_x': x, - 'start_y': y, - 'current_x': x, - 'current_y': y, - 'start_time': timestamp, - 'last_update': timestamp, - 'touch_points': [touch_id], - 'parameters': {} - } - - self.active_gestures[gesture_id] = gesture_data - self.gesture_start_time[gesture_id] = timestamp - self.gesture_stats['gestures_started'] += 1 - - # 触发手势开始事件 - self._trigger_gesture_event(gesture_id, GestureState.STARTED, gesture_data) - - except Exception as e: - print(f"✗ 新手势开始失败: {e}") - - def _update_gesture(self, touch_id: int, x: float, y: float, timestamp: float): - """ - 更新手势 - - Args: - touch_id: 触摸点ID - x: X坐标 - y: Y坐标 - timestamp: 时间戳 - """ - try: - # 更新所有包含此触摸点的活动手势 - for gesture_id, gesture_data in self.active_gestures.items(): - if touch_id in gesture_data['touch_points']: - gesture_data['current_x'] = x - gesture_data['current_y'] = y - gesture_data['last_update'] = timestamp - - # 尝试识别手势 - self._recognize_gesture(gesture_id, gesture_data, timestamp) - - # 触发手势更新事件 - self._trigger_gesture_event(gesture_id, GestureState.UPDATED, gesture_data) - - except Exception as e: - print(f"✗ 手势更新失败: {e}") - - def _end_gesture(self, touch_id: int, timestamp: float): - """ - 结束手势 - - Args: - touch_id: 触摸点ID - timestamp: 时间戳 - """ - try: - # 结束所有包含此触摸点的活动手势 - finished_gestures = [] - for gesture_id, gesture_data in self.active_gestures.items(): - if touch_id in gesture_data['touch_points']: - # 从触摸点列表中移除 - gesture_data['touch_points'].remove(touch_id) - - # 如果没有触摸点了,结束手势 - if not gesture_data['touch_points']: - finished_gestures.append(gesture_id) - - # 如果手势已被识别,触发结束事件 - if gesture_data['state'] == GestureState.RECOGNIZED: - self._trigger_gesture_event(gesture_id, GestureState.ENDED, gesture_data) - else: - # 尝试最后一次识别 - self._recognize_gesture(gesture_id, gesture_data, timestamp) - if gesture_data['state'] == GestureState.RECOGNIZED: - self._trigger_gesture_event(gesture_id, GestureState.ENDED, gesture_data) - else: - self._cancel_gesture(gesture_id) - - # 移除已完成的手势 - for gesture_id in finished_gestures: - if gesture_id in self.active_gestures: - del self.active_gestures[gesture_id] - - except Exception as e: - print(f"✗ 手势结束失败: {e}") - - def _cancel_gesture(self, gesture_id: int): - """ - 取消手势 - - Args: - gesture_id: 手势ID - """ - try: - if gesture_id in self.active_gestures: - gesture_data = self.active_gestures[gesture_id] - gesture_data['state'] = GestureState.CANCELLED - - self._trigger_gesture_event(gesture_id, GestureState.CANCELLED, gesture_data) - self.gesture_stats['gestures_cancelled'] += 1 - - del self.active_gestures[gesture_id] - - except Exception as e: - print(f"✗ 手势取消失败: {e}") - - def _recognize_gesture(self, gesture_id: int, gesture_data: Dict[str, Any], timestamp: float): - """ - 识别手势 - - Args: - gesture_id: 手势ID - gesture_data: 手势数据 - timestamp: 时间戳 - """ - try: - # 根据触摸点数量选择识别器 - touch_count = len(gesture_data['touch_points']) - - if touch_count == 1: - self._recognize_single_touch_gesture(gesture_id, gesture_data, timestamp) - elif touch_count == 2: - self._recognize_two_touch_gesture(gesture_id, gesture_data, timestamp) - else: - self._recognize_multi_touch_gesture(gesture_id, gesture_data, timestamp) - - except Exception as e: - print(f"✗ 手势识别失败: {e}") - - def _recognize_single_touch_gesture(self, gesture_id: int, gesture_data: Dict[str, Any], timestamp: float): - """ - 识别单点触摸手势 - - Args: - gesture_id: 手势ID - gesture_data: 手势数据 - timestamp: 时间戳 - """ - try: - start_x = gesture_data['start_x'] - start_y = gesture_data['start_y'] - current_x = gesture_data['current_x'] - current_y = gesture_data['current_y'] - start_time = gesture_data['start_time'] - - # 计算距离和持续时间 - distance = math.sqrt((current_x - start_x)**2 + (current_y - start_y)**2) - duration = timestamp - start_time - - # 检查是否为点击 - if duration <= self.gesture_config['tap_max_duration'] and \ - distance <= self.gesture_config['tap_max_distance']: - - # 检查是否为双击 - last_tap_time = self.last_gesture_time.get('tap', 0) - if timestamp - last_tap_time <= self.gesture_config['double_tap_timeout']: - # 双击 - gesture_data['type'] = GestureType.DOUBLE_TAP - gesture_data['state'] = GestureState.RECOGNIZED - gesture_data['parameters'] = { - 'position': (current_x, current_y), - 'duration': duration - } - self.gesture_stats['gestures_recognized'] += 1 - self.last_gesture_time['tap'] = timestamp - return - - # 单击 - gesture_data['type'] = GestureType.TAP - gesture_data['state'] = GestureState.RECOGNIZED - gesture_data['parameters'] = { - 'position': (current_x, current_y), - 'duration': duration - } - self.gesture_stats['gestures_recognized'] += 1 - self.last_gesture_time['tap'] = timestamp - return - - # 检查是否为长按 - if duration >= self.gesture_config['long_press_duration'] and \ - distance <= self.gesture_config['tap_max_distance']: - gesture_data['type'] = GestureType.LONG_PRESS - gesture_data['state'] = GestureState.RECOGNIZED - gesture_data['parameters'] = { - 'position': (current_x, current_y), - 'duration': duration - } - self.gesture_stats['gestures_recognized'] += 1 - return - - # 检查是否为滑动 - if duration <= self.gesture_config['swipe_max_duration'] and \ - distance >= self.gesture_config['swipe_min_distance']: - # 计算滑动方向 - dx = current_x - start_x - dy = current_y - start_y - angle = math.degrees(math.atan2(dy, dx)) - - gesture_data['type'] = GestureType.SWIPE - gesture_data['state'] = GestureState.RECOGNIZED - gesture_data['parameters'] = { - 'start_position': (start_x, start_y), - 'end_position': (current_x, current_y), - 'distance': distance, - 'angle': angle, - 'duration': duration - } - self.gesture_stats['gestures_recognized'] += 1 - return - - # 检查是否为拖拽 - if duration > self.gesture_config['tap_max_duration'] and \ - distance > self.gesture_config['tap_max_distance']: - gesture_data['type'] = GestureType.DRAG - gesture_data['state'] = GestureState.RECOGNIZED - gesture_data['parameters'] = { - 'start_position': (start_x, start_y), - 'end_position': (current_x, current_y), - 'distance': distance, - 'duration': duration - } - self.gesture_stats['gestures_recognized'] += 1 - return - - except Exception as e: - print(f"✗ 单点手势识别失败: {e}") - - def _recognize_two_touch_gesture(self, gesture_id: int, gesture_data: Dict[str, Any], timestamp: float): - """ - 识别两点触摸手势 - - Args: - gesture_id: 手势ID - gesture_data: 手势数据 - timestamp: 时间戳 - """ - try: - # 获取两个触摸点 - if len(gesture_data['touch_points']) >= 2: - touch_ids = list(gesture_data['touch_points'])[:2] - touch1 = self.touch_points.get(touch_ids[0]) - touch2 = self.touch_points.get(touch_ids[1]) - - if touch1 and touch2: - # 计算初始和当前的两点距离 - start_distance = math.sqrt( - (touch1['start_x'] - touch2['start_x'])**2 + - (touch1['start_y'] - touch2['start_y'])**2 - ) - - current_distance = math.sqrt( - (touch1['current_x'] - touch2['current_x'])**2 + - (touch1['current_y'] - touch2['current_y'])**2 - ) - - # 计算初始和当前的角度 - start_angle = math.degrees(math.atan2( - touch2['start_y'] - touch1['start_y'], - touch2['start_x'] - touch1['start_x'] - )) - - current_angle = math.degrees(math.atan2( - touch2['current_y'] - touch1['current_y'], - touch2['current_x'] - touch1['current_x'] - )) - - # 检查是否为捏合 - distance_change = abs(current_distance - start_distance) - if distance_change >= self.gesture_config['pinch_threshold']: - gesture_data['type'] = GestureType.PINCH - gesture_data['state'] = GestureState.RECOGNIZED - gesture_data['parameters'] = { - 'start_distance': start_distance, - 'current_distance': current_distance, - 'scale': current_distance / start_distance if start_distance > 0 else 1.0, - 'center': ( - (touch1['current_x'] + touch2['current_x']) / 2, - (touch1['current_y'] + touch2['current_y']) / 2 - ) - } - self.gesture_stats['gestures_recognized'] += 1 - return - - # 检查是否为旋转 - angle_change = abs(current_angle - start_angle) - if angle_change >= self.gesture_config['rotate_threshold']: - gesture_data['type'] = GestureType.ROTATE - gesture_data['state'] = GestureState.RECOGNIZED - gesture_data['parameters'] = { - 'start_angle': start_angle, - 'current_angle': current_angle, - 'rotation': current_angle - start_angle, - 'center': ( - (touch1['current_x'] + touch2['current_x']) / 2, - (touch1['current_y'] + touch2['current_y']) / 2 - ) - } - self.gesture_stats['gestures_recognized'] += 1 - return - - except Exception as e: - print(f"✗ 两点手势识别失败: {e}") - - def _recognize_multi_touch_gesture(self, gesture_id: int, gesture_data: Dict[str, Any], timestamp: float): - """ - 识别多点触摸手势 - - Args: - gesture_id: 手势ID - gesture_data: 手势数据 - timestamp: 时间戳 - """ - try: - # 多点手势可以在这里扩展实现 - # 例如:三指滑动、四指捏合等 - pass - except Exception as e: - print(f"✗ 多点手势识别失败: {e}") - - def _trigger_gesture_event(self, gesture_id: int, state: GestureState, gesture_data: Dict[str, Any]): - """ - 触发手势事件 - - Args: - gesture_id: 手势ID - state: 手势状态 - gesture_data: 手势数据 - """ - try: - # 记录手势历史 - if state == GestureState.RECOGNIZED: - self.gesture_history.append({ - 'id': gesture_id, - 'type': gesture_data['type'].value if gesture_data['type'] else 'unknown', - 'state': state.value, - 'parameters': gesture_data['parameters'], - 'timestamp': time.time() - }) - - # 保持历史记录大小 - if len(self.gesture_history) > 100: - self.gesture_history.pop(0) - - # 调用手势回调 - gesture_type = gesture_data.get('type') - if gesture_type and gesture_type in self.gesture_callbacks: - callback = self.gesture_callbacks[gesture_type] - try: - callback({ - 'gesture_id': gesture_id, - 'type': gesture_type.value, - 'state': state.value, - 'parameters': gesture_data['parameters'] - }) - except Exception as e: - print(f"✗ 手势回调执行失败: {e}") - - except Exception as e: - print(f"✗ 手势事件触发失败: {e}") - - def register_gesture_callback(self, gesture_type: GestureType, callback: Callable): - """ - 注册手势回调 - - Args: - gesture_type: 手势类型 - callback: 回调函数 - """ - try: - self.gesture_callbacks[gesture_type] = callback - print(f"✓ 手势回调已注册: {gesture_type.value}") - except Exception as e: - print(f"✗ 手势回调注册失败: {e}") - - def unregister_gesture_callback(self, gesture_type: GestureType): - """ - 注销手势回调 - - Args: - gesture_type: 手势类型 - """ - try: - if gesture_type in self.gesture_callbacks: - del self.gesture_callbacks[gesture_type] - print(f"✓ 手势回调已注销: {gesture_type.value}") - except Exception as e: - print(f"✗ 手势回调注销失败: {e}") - - def get_gesture_history(self, limit: int = None) -> List[Dict[str, Any]]: - """ - 获取手势历史记录 - - Args: - limit: 返回记录数量限制 - - Returns: - 手势历史列表 - """ - try: - history = self.gesture_history.copy() - if limit: - history = history[-limit:] - return history - except Exception as e: - print(f"✗ 手势历史获取失败: {e}") - return [] - - def clear_gesture_history(self): - """清空手势历史""" - try: - self.gesture_history.clear() - print("✓ 手势历史已清空") - except Exception as e: - print(f"✗ 手势历史清空失败: {e}") - - def set_gesture_config(self, config: Dict[str, Any]): - """ - 设置手势配置 - - Args: - config: 手势配置字典 - """ - try: - self.gesture_config.update(config) - print(f"✓ 手势配置已更新: {self.gesture_config}") - except Exception as e: - print(f"✗ 手势配置设置失败: {e}") - - def get_gesture_config(self) -> Dict[str, Any]: - """ - 获取手势配置 - - Returns: - 手势配置字典 - """ - return self.gesture_config.copy() - - def get_gesture_stats(self) -> Dict[str, int]: - """ - 获取手势统计信息 - - Returns: - 手势统计字典 - """ - return self.gesture_stats.copy() - - def reset_gesture_stats(self): - """重置手势统计信息""" - try: - self.gesture_stats = { - 'gestures_started': 0, - 'gestures_recognized': 0, - 'gestures_cancelled': 0, - 'gestures_failed': 0, - 'active_gestures': 0 - } - print("✓ 手势统计信息已重置") - except Exception as e: - print(f"✗ 手势统计信息重置失败: {e}") - - def get_active_gestures(self) -> Dict[int, Dict[str, Any]]: - """ - 获取活动手势 - - Returns: - 活动手势字典 - """ - return self.active_gestures.copy() - - def get_touch_points(self) -> Dict[int, Dict[str, Any]]: - """ - 获取触摸点信息 - - Returns: - 触摸点字典 - """ +""" +手势识别器模块 +负责识别和处理各种手势输入 +""" + +import time +from typing import Dict, Any, List, Optional, Callable, Tuple +from enum import Enum +import math + +class GestureType(Enum): + """手势类型枚举""" + TAP = "tap" + DOUBLE_TAP = "double_tap" + LONG_PRESS = "long_press" + SWIPE = "swipe" + PINCH = "pinch" + ROTATE = "rotate" + DRAG = "drag" + FLICK = "flick" + CIRCLE = "circle" + SHAKE = "shake" + CUSTOM = "custom" + +class GestureState(Enum): + """手势状态枚举""" + STARTED = "started" + UPDATED = "updated" + ENDED = "ended" + CANCELLED = "cancelled" + RECOGNIZED = "recognized" + +class GestureRecognizer: + """ + 手势识别器 + 负责识别和处理各种手势输入 + """ + + def __init__(self, plugin): + """ + 初始化手势识别器 + + Args: + plugin: 多平台输入支持插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 手势配置 + self.gesture_config = { + 'tap_max_duration': 0.3, # 点击最大持续时间(秒) + 'tap_max_distance': 20.0, # 点击最大移动距离(像素) + 'double_tap_timeout': 0.3, # 双击超时时间(秒) + 'long_press_duration': 0.5, # 长按持续时间(秒) + 'swipe_min_distance': 50.0, # 滑动最小距离(像素) + 'swipe_max_duration': 0.5, # 滑动最大持续时间(秒) + 'pinch_threshold': 20.0, # 捏合阈值(像素) + 'rotate_threshold': 10.0, # 旋转阈值(度) + 'flick_min_velocity': 100.0, # 轻拂最小速度(像素/秒) + 'circle_min_radius': 30.0, # 圆形最小半径(像素) + 'circle_tolerance': 0.3, # 圆形容差 + 'shake_threshold': 20.0, # 摇晃阈值(像素) + 'shake_count': 3, # 摇晃次数阈值 + 'update_interval': 0.016 # 更新间隔(秒) + } + + # 当前手势状态 + self.active_gestures = {} + self.gesture_counter = 0 + + # 触摸点跟踪 + self.touch_points = {} + self.touch_counter = 0 + + # 手势历史 + self.gesture_history = [] + + # 回调函数 + self.gesture_callbacks = {} + + # 统计信息 + self.gesture_stats = { + 'gestures_started': 0, + 'gestures_recognized': 0, + 'gestures_cancelled': 0, + 'gestures_failed': 0, + 'active_gestures': 0 + } + + # 时间戳记录 + self.last_gesture_time = {} + self.gesture_start_time = {} + + print("✓ 手势识别器已创建") + + def initialize(self) -> bool: + """ + 初始化手势识别器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 手势识别器初始化完成") + return True + + except Exception as e: + print(f"✗ 手势识别器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用手势识别器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 手势识别器未初始化") + return False + + self.enabled = True + print("✓ 手势识别器已启用") + return True + + except Exception as e: + print(f"✗ 手势识别器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用手势识别器""" + try: + self.enabled = False + print("✓ 手势识别器已禁用") + + except Exception as e: + print(f"✗ 手势识别器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理手势识别器资源""" + try: + self.disable() + self.active_gestures.clear() + self.touch_points.clear() + self.gesture_history.clear() + self.gesture_callbacks.clear() + self.last_gesture_time.clear() + self.gesture_start_time.clear() + self.initialized = False + print("✓ 手势识别器资源已清理") + + except Exception as e: + print(f"✗ 手势识别器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新手势识别器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新活动手势 + self._update_active_gestures(dt) + + except Exception as e: + print(f"✗ 手势识别器更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_active_gestures(self, dt: float): + """更新活动手势""" + try: + current_time = time.time() + + # 检查超时的手势 + timed_out_gestures = [] + for gesture_id, gesture_data in self.active_gestures.items(): + start_time = self.gesture_start_time.get(gesture_id, current_time) + if current_time - start_time > 5.0: # 5秒超时 + timed_out_gestures.append(gesture_id) + + # 取消超时手势 + for gesture_id in timed_out_gestures: + self._cancel_gesture(gesture_id) + + except Exception as e: + print(f"✗ 活动手势更新失败: {e}") + + def start_touch(self, touch_id: int, x: float, y: float, timestamp: float = None): + """ + 开始触摸 + + Args: + touch_id: 触摸点ID + x: X坐标 + y: Y坐标 + timestamp: 时间戳 + """ + try: + if not self.enabled: + return + + if timestamp is None: + timestamp = time.time() + + # 记录触摸点 + self.touch_points[touch_id] = { + 'id': touch_id, + 'start_x': x, + 'start_y': y, + 'current_x': x, + 'current_y': y, + 'start_time': timestamp, + 'last_update': timestamp, + 'path': [(x, y, timestamp)] + } + + # 如果是第一个触摸点,开始新的手势 + if len(self.touch_points) == 1: + self._start_new_gesture(touch_id, x, y, timestamp) + + except Exception as e: + print(f"✗ 触摸开始处理失败: {e}") + + def update_touch(self, touch_id: int, x: float, y: float, timestamp: float = None): + """ + 更新触摸 + + Args: + touch_id: 触摸点ID + x: X坐标 + y: Y坐标 + timestamp: 时间戳 + """ + try: + if not self.enabled: + return + + if timestamp is None: + timestamp = time.time() + + # 更新触摸点 + if touch_id in self.touch_points: + touch_data = self.touch_points[touch_id] + touch_data['current_x'] = x + touch_data['current_y'] = y + touch_data['last_update'] = timestamp + touch_data['path'].append((x, y, timestamp)) + + # 保持路径大小 + if len(touch_data['path']) > 50: + touch_data['path'].pop(0) + + # 更新活动手势 + self._update_gesture(touch_id, x, y, timestamp) + + except Exception as e: + print(f"✗ 触摸更新处理失败: {e}") + + def end_touch(self, touch_id: int, timestamp: float = None): + """ + 结束触摸 + + Args: + touch_id: 触摸点ID + timestamp: 时间戳 + """ + try: + if not self.enabled: + return + + if timestamp is None: + timestamp = time.time() + + # 更新触摸点 + if touch_id in self.touch_points: + touch_data = self.touch_points[touch_id] + touch_data['last_update'] = timestamp + + # 结束手势 + self._end_gesture(touch_id, timestamp) + + # 移除触摸点 + del self.touch_points[touch_id] + + except Exception as e: + print(f"✗ 触摸结束处理失败: {e}") + + def cancel_touch(self, touch_id: int, timestamp: float = None): + """ + 取消触摸 + + Args: + touch_id: 触摸点ID + timestamp: 时间戳 + """ + try: + if not self.enabled: + return + + if timestamp is None: + timestamp = time.time() + + # 取消手势 + if touch_id in self.touch_points: + self._cancel_gesture(touch_id) + del self.touch_points[touch_id] + + except Exception as e: + print(f"✗ 触摸取消处理失败: {e}") + + def _start_new_gesture(self, touch_id: int, x: float, y: float, timestamp: float): + """ + 开始新手势 + + Args: + touch_id: 触摸点ID + x: X坐标 + y: Y坐标 + timestamp: 时间戳 + """ + try: + gesture_id = self.gesture_counter + self.gesture_counter += 1 + + gesture_data = { + 'id': gesture_id, + 'type': None, + 'state': GestureState.STARTED, + 'start_x': x, + 'start_y': y, + 'current_x': x, + 'current_y': y, + 'start_time': timestamp, + 'last_update': timestamp, + 'touch_points': [touch_id], + 'parameters': {} + } + + self.active_gestures[gesture_id] = gesture_data + self.gesture_start_time[gesture_id] = timestamp + self.gesture_stats['gestures_started'] += 1 + + # 触发手势开始事件 + self._trigger_gesture_event(gesture_id, GestureState.STARTED, gesture_data) + + except Exception as e: + print(f"✗ 新手势开始失败: {e}") + + def _update_gesture(self, touch_id: int, x: float, y: float, timestamp: float): + """ + 更新手势 + + Args: + touch_id: 触摸点ID + x: X坐标 + y: Y坐标 + timestamp: 时间戳 + """ + try: + # 更新所有包含此触摸点的活动手势 + for gesture_id, gesture_data in self.active_gestures.items(): + if touch_id in gesture_data['touch_points']: + gesture_data['current_x'] = x + gesture_data['current_y'] = y + gesture_data['last_update'] = timestamp + + # 尝试识别手势 + self._recognize_gesture(gesture_id, gesture_data, timestamp) + + # 触发手势更新事件 + self._trigger_gesture_event(gesture_id, GestureState.UPDATED, gesture_data) + + except Exception as e: + print(f"✗ 手势更新失败: {e}") + + def _end_gesture(self, touch_id: int, timestamp: float): + """ + 结束手势 + + Args: + touch_id: 触摸点ID + timestamp: 时间戳 + """ + try: + # 结束所有包含此触摸点的活动手势 + finished_gestures = [] + for gesture_id, gesture_data in self.active_gestures.items(): + if touch_id in gesture_data['touch_points']: + # 从触摸点列表中移除 + gesture_data['touch_points'].remove(touch_id) + + # 如果没有触摸点了,结束手势 + if not gesture_data['touch_points']: + finished_gestures.append(gesture_id) + + # 如果手势已被识别,触发结束事件 + if gesture_data['state'] == GestureState.RECOGNIZED: + self._trigger_gesture_event(gesture_id, GestureState.ENDED, gesture_data) + else: + # 尝试最后一次识别 + self._recognize_gesture(gesture_id, gesture_data, timestamp) + if gesture_data['state'] == GestureState.RECOGNIZED: + self._trigger_gesture_event(gesture_id, GestureState.ENDED, gesture_data) + else: + self._cancel_gesture(gesture_id) + + # 移除已完成的手势 + for gesture_id in finished_gestures: + if gesture_id in self.active_gestures: + del self.active_gestures[gesture_id] + + except Exception as e: + print(f"✗ 手势结束失败: {e}") + + def _cancel_gesture(self, gesture_id: int): + """ + 取消手势 + + Args: + gesture_id: 手势ID + """ + try: + if gesture_id in self.active_gestures: + gesture_data = self.active_gestures[gesture_id] + gesture_data['state'] = GestureState.CANCELLED + + self._trigger_gesture_event(gesture_id, GestureState.CANCELLED, gesture_data) + self.gesture_stats['gestures_cancelled'] += 1 + + del self.active_gestures[gesture_id] + + except Exception as e: + print(f"✗ 手势取消失败: {e}") + + def _recognize_gesture(self, gesture_id: int, gesture_data: Dict[str, Any], timestamp: float): + """ + 识别手势 + + Args: + gesture_id: 手势ID + gesture_data: 手势数据 + timestamp: 时间戳 + """ + try: + # 根据触摸点数量选择识别器 + touch_count = len(gesture_data['touch_points']) + + if touch_count == 1: + self._recognize_single_touch_gesture(gesture_id, gesture_data, timestamp) + elif touch_count == 2: + self._recognize_two_touch_gesture(gesture_id, gesture_data, timestamp) + else: + self._recognize_multi_touch_gesture(gesture_id, gesture_data, timestamp) + + except Exception as e: + print(f"✗ 手势识别失败: {e}") + + def _recognize_single_touch_gesture(self, gesture_id: int, gesture_data: Dict[str, Any], timestamp: float): + """ + 识别单点触摸手势 + + Args: + gesture_id: 手势ID + gesture_data: 手势数据 + timestamp: 时间戳 + """ + try: + start_x = gesture_data['start_x'] + start_y = gesture_data['start_y'] + current_x = gesture_data['current_x'] + current_y = gesture_data['current_y'] + start_time = gesture_data['start_time'] + + # 计算距离和持续时间 + distance = math.sqrt((current_x - start_x)**2 + (current_y - start_y)**2) + duration = timestamp - start_time + + # 检查是否为点击 + if duration <= self.gesture_config['tap_max_duration'] and \ + distance <= self.gesture_config['tap_max_distance']: + + # 检查是否为双击 + last_tap_time = self.last_gesture_time.get('tap', 0) + if timestamp - last_tap_time <= self.gesture_config['double_tap_timeout']: + # 双击 + gesture_data['type'] = GestureType.DOUBLE_TAP + gesture_data['state'] = GestureState.RECOGNIZED + gesture_data['parameters'] = { + 'position': (current_x, current_y), + 'duration': duration + } + self.gesture_stats['gestures_recognized'] += 1 + self.last_gesture_time['tap'] = timestamp + return + + # 单击 + gesture_data['type'] = GestureType.TAP + gesture_data['state'] = GestureState.RECOGNIZED + gesture_data['parameters'] = { + 'position': (current_x, current_y), + 'duration': duration + } + self.gesture_stats['gestures_recognized'] += 1 + self.last_gesture_time['tap'] = timestamp + return + + # 检查是否为长按 + if duration >= self.gesture_config['long_press_duration'] and \ + distance <= self.gesture_config['tap_max_distance']: + gesture_data['type'] = GestureType.LONG_PRESS + gesture_data['state'] = GestureState.RECOGNIZED + gesture_data['parameters'] = { + 'position': (current_x, current_y), + 'duration': duration + } + self.gesture_stats['gestures_recognized'] += 1 + return + + # 检查是否为滑动 + if duration <= self.gesture_config['swipe_max_duration'] and \ + distance >= self.gesture_config['swipe_min_distance']: + # 计算滑动方向 + dx = current_x - start_x + dy = current_y - start_y + angle = math.degrees(math.atan2(dy, dx)) + + gesture_data['type'] = GestureType.SWIPE + gesture_data['state'] = GestureState.RECOGNIZED + gesture_data['parameters'] = { + 'start_position': (start_x, start_y), + 'end_position': (current_x, current_y), + 'distance': distance, + 'angle': angle, + 'duration': duration + } + self.gesture_stats['gestures_recognized'] += 1 + return + + # 检查是否为拖拽 + if duration > self.gesture_config['tap_max_duration'] and \ + distance > self.gesture_config['tap_max_distance']: + gesture_data['type'] = GestureType.DRAG + gesture_data['state'] = GestureState.RECOGNIZED + gesture_data['parameters'] = { + 'start_position': (start_x, start_y), + 'end_position': (current_x, current_y), + 'distance': distance, + 'duration': duration + } + self.gesture_stats['gestures_recognized'] += 1 + return + + except Exception as e: + print(f"✗ 单点手势识别失败: {e}") + + def _recognize_two_touch_gesture(self, gesture_id: int, gesture_data: Dict[str, Any], timestamp: float): + """ + 识别两点触摸手势 + + Args: + gesture_id: 手势ID + gesture_data: 手势数据 + timestamp: 时间戳 + """ + try: + # 获取两个触摸点 + if len(gesture_data['touch_points']) >= 2: + touch_ids = list(gesture_data['touch_points'])[:2] + touch1 = self.touch_points.get(touch_ids[0]) + touch2 = self.touch_points.get(touch_ids[1]) + + if touch1 and touch2: + # 计算初始和当前的两点距离 + start_distance = math.sqrt( + (touch1['start_x'] - touch2['start_x'])**2 + + (touch1['start_y'] - touch2['start_y'])**2 + ) + + current_distance = math.sqrt( + (touch1['current_x'] - touch2['current_x'])**2 + + (touch1['current_y'] - touch2['current_y'])**2 + ) + + # 计算初始和当前的角度 + start_angle = math.degrees(math.atan2( + touch2['start_y'] - touch1['start_y'], + touch2['start_x'] - touch1['start_x'] + )) + + current_angle = math.degrees(math.atan2( + touch2['current_y'] - touch1['current_y'], + touch2['current_x'] - touch1['current_x'] + )) + + # 检查是否为捏合 + distance_change = abs(current_distance - start_distance) + if distance_change >= self.gesture_config['pinch_threshold']: + gesture_data['type'] = GestureType.PINCH + gesture_data['state'] = GestureState.RECOGNIZED + gesture_data['parameters'] = { + 'start_distance': start_distance, + 'current_distance': current_distance, + 'scale': current_distance / start_distance if start_distance > 0 else 1.0, + 'center': ( + (touch1['current_x'] + touch2['current_x']) / 2, + (touch1['current_y'] + touch2['current_y']) / 2 + ) + } + self.gesture_stats['gestures_recognized'] += 1 + return + + # 检查是否为旋转 + angle_change = abs(current_angle - start_angle) + if angle_change >= self.gesture_config['rotate_threshold']: + gesture_data['type'] = GestureType.ROTATE + gesture_data['state'] = GestureState.RECOGNIZED + gesture_data['parameters'] = { + 'start_angle': start_angle, + 'current_angle': current_angle, + 'rotation': current_angle - start_angle, + 'center': ( + (touch1['current_x'] + touch2['current_x']) / 2, + (touch1['current_y'] + touch2['current_y']) / 2 + ) + } + self.gesture_stats['gestures_recognized'] += 1 + return + + except Exception as e: + print(f"✗ 两点手势识别失败: {e}") + + def _recognize_multi_touch_gesture(self, gesture_id: int, gesture_data: Dict[str, Any], timestamp: float): + """ + 识别多点触摸手势 + + Args: + gesture_id: 手势ID + gesture_data: 手势数据 + timestamp: 时间戳 + """ + try: + # 多点手势可以在这里扩展实现 + # 例如:三指滑动、四指捏合等 + pass + except Exception as e: + print(f"✗ 多点手势识别失败: {e}") + + def _trigger_gesture_event(self, gesture_id: int, state: GestureState, gesture_data: Dict[str, Any]): + """ + 触发手势事件 + + Args: + gesture_id: 手势ID + state: 手势状态 + gesture_data: 手势数据 + """ + try: + # 记录手势历史 + if state == GestureState.RECOGNIZED: + self.gesture_history.append({ + 'id': gesture_id, + 'type': gesture_data['type'].value if gesture_data['type'] else 'unknown', + 'state': state.value, + 'parameters': gesture_data['parameters'], + 'timestamp': time.time() + }) + + # 保持历史记录大小 + if len(self.gesture_history) > 100: + self.gesture_history.pop(0) + + # 调用手势回调 + gesture_type = gesture_data.get('type') + if gesture_type and gesture_type in self.gesture_callbacks: + callback = self.gesture_callbacks[gesture_type] + try: + callback({ + 'gesture_id': gesture_id, + 'type': gesture_type.value, + 'state': state.value, + 'parameters': gesture_data['parameters'] + }) + except Exception as e: + print(f"✗ 手势回调执行失败: {e}") + + except Exception as e: + print(f"✗ 手势事件触发失败: {e}") + + def register_gesture_callback(self, gesture_type: GestureType, callback: Callable): + """ + 注册手势回调 + + Args: + gesture_type: 手势类型 + callback: 回调函数 + """ + try: + self.gesture_callbacks[gesture_type] = callback + print(f"✓ 手势回调已注册: {gesture_type.value}") + except Exception as e: + print(f"✗ 手势回调注册失败: {e}") + + def unregister_gesture_callback(self, gesture_type: GestureType): + """ + 注销手势回调 + + Args: + gesture_type: 手势类型 + """ + try: + if gesture_type in self.gesture_callbacks: + del self.gesture_callbacks[gesture_type] + print(f"✓ 手势回调已注销: {gesture_type.value}") + except Exception as e: + print(f"✗ 手势回调注销失败: {e}") + + def get_gesture_history(self, limit: int = None) -> List[Dict[str, Any]]: + """ + 获取手势历史记录 + + Args: + limit: 返回记录数量限制 + + Returns: + 手势历史列表 + """ + try: + history = self.gesture_history.copy() + if limit: + history = history[-limit:] + return history + except Exception as e: + print(f"✗ 手势历史获取失败: {e}") + return [] + + def clear_gesture_history(self): + """清空手势历史""" + try: + self.gesture_history.clear() + print("✓ 手势历史已清空") + except Exception as e: + print(f"✗ 手势历史清空失败: {e}") + + def set_gesture_config(self, config: Dict[str, Any]): + """ + 设置手势配置 + + Args: + config: 手势配置字典 + """ + try: + self.gesture_config.update(config) + print(f"✓ 手势配置已更新: {self.gesture_config}") + except Exception as e: + print(f"✗ 手势配置设置失败: {e}") + + def get_gesture_config(self) -> Dict[str, Any]: + """ + 获取手势配置 + + Returns: + 手势配置字典 + """ + return self.gesture_config.copy() + + def get_gesture_stats(self) -> Dict[str, int]: + """ + 获取手势统计信息 + + Returns: + 手势统计字典 + """ + return self.gesture_stats.copy() + + def reset_gesture_stats(self): + """重置手势统计信息""" + try: + self.gesture_stats = { + 'gestures_started': 0, + 'gestures_recognized': 0, + 'gestures_cancelled': 0, + 'gestures_failed': 0, + 'active_gestures': 0 + } + print("✓ 手势统计信息已重置") + except Exception as e: + print(f"✗ 手势统计信息重置失败: {e}") + + def get_active_gestures(self) -> Dict[int, Dict[str, Any]]: + """ + 获取活动手势 + + Returns: + 活动手势字典 + """ + return self.active_gestures.copy() + + def get_touch_points(self) -> Dict[int, Dict[str, Any]]: + """ + 获取触摸点信息 + + Returns: + 触摸点字典 + """ return self.touch_points.copy() \ No newline at end of file diff --git a/plugins/user/multiplatform_input/haptics/haptic_manager.py b/plugins/user/multiplatform_input/haptics/haptic_manager.py index af80d2c5..ac441514 100644 --- a/plugins/user/multiplatform_input/haptics/haptic_manager.py +++ b/plugins/user/multiplatform_input/haptics/haptic_manager.py @@ -1,887 +1,887 @@ -""" -触觉反馈管理器模块 -负责管理和播放各种触觉反馈效果 -""" - -import time -from typing import Dict, Any, List, Optional, Callable, Union -from enum import Enum -import threading -import math - -class HapticEffectType(Enum): - """触觉效果类型枚举""" - SIMPLE = "simple" - PATTERN = "pattern" - CURVE = "curve" - CUSTOM = "custom" - -class HapticDeviceType(Enum): - """触觉设备类型枚举""" - GAMEPAD = "gamepad" - VR_CONTROLLER = "vr_controller" - TOUCH_DEVICE = "touch_device" - CUSTOM = "custom" - -class HapticManager: - """ - 触觉反馈管理器 - 负责管理和播放各种触觉反馈效果 - """ - - def __init__(self, plugin): - """ - 初始化触觉反馈管理器 - - Args: - plugin: 多平台输入支持插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 触觉效果配置 - self.haptic_config = { - 'default_intensity': 0.5, # 默认强度 (0.0-1.0) - 'default_duration': 0.2, # 默认持续时间(秒) - 'max_intensity': 1.0, # 最大强度 - 'min_duration': 0.01, # 最小持续时间(秒) - 'max_duration': 5.0, # 最大持续时间(秒) - 'update_rate': 60.0, # 更新频率(Hz) - 'enable_fading': True, # 启用淡入淡出 - 'fade_duration': 0.05 # 淡入淡出持续时间(秒) - } - - # 触觉效果定义 - self.haptic_effects = {} - self.effect_counter = 0 - - # 活动效果 - self.active_effects = {} - self.effect_instance_counter = 0 - - # 设备支持 - self.supported_devices = {} - self.device_counter = 0 - - # 回调函数 - self.haptic_callbacks = { - 'effect_started': [], - 'effect_updated': [], - 'effect_finished': [], - 'effect_cancelled': [] - } - - # 统计信息 - self.haptic_stats = { - 'effects_registered': 0, - 'effects_played': 0, - 'effects_cancelled': 0, - 'active_effects': 0, - 'total_haptic_time': 0.0 - } - - # 时间戳记录 - self.last_effect_time = {} - - # 线程管理 - self.haptic_thread = None - self.running = False - self.haptic_lock = threading.Lock() - - print("✓ 触觉反馈管理器已创建") - - def initialize(self) -> bool: - """ - 初始化触觉反馈管理器 - - Returns: - 是否初始化成功 - """ - try: - # 创建默认触觉效果 - self._create_default_effects() - - # 初始化支持的设备 - self._initialize_supported_devices() - - self.initialized = True - print("✓ 触觉反馈管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 触觉反馈管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用触觉反馈管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 触觉反馈管理器未初始化") - return False - - self.enabled = True - self.running = True - - # 启动触觉更新线程 - self.haptic_thread = threading.Thread(target=self._haptic_update_loop, daemon=True) - self.haptic_thread.start() - - print("✓ 触觉反馈管理器已启用") - return True - - except Exception as e: - print(f"✗ 触觉反馈管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用触觉反馈管理器""" - try: - self.enabled = False - self.running = False - - # 等待触觉线程结束 - if self.haptic_thread and self.haptic_thread.is_alive(): - self.haptic_thread.join(timeout=2.0) - - # 停止所有活动效果 - self.stop_all_effects() - - print("✓ 触觉反馈管理器已禁用") - - except Exception as e: - print(f"✗ 触觉反馈管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理触觉反馈管理器资源""" - try: - self.disable() - self.haptic_effects.clear() - self.active_effects.clear() - self.supported_devices.clear() - self.haptic_callbacks.clear() - self.last_effect_time.clear() - self.initialized = False - print("✓ 触觉反馈管理器资源已清理") - - except Exception as e: - print(f"✗ 触觉反馈管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新触觉反馈管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新统计信息 - self.haptic_stats['active_effects'] = len(self.active_effects) - - except Exception as e: - print(f"✗ 触觉反馈管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _haptic_update_loop(self): - """触觉更新循环""" - try: - update_interval = 1.0 / self.haptic_config['update_rate'] - - while self.running: - if self.enabled: - self._update_active_effects() - - time.sleep(update_interval) - - except Exception as e: - print(f"✗ 触觉更新循环失败: {e}") - - def _update_active_effects(self): - """更新活动效果""" - try: - current_time = time.time() - finished_effects = [] - - with self.haptic_lock: - for effect_id, effect_data in self.active_effects.items(): - start_time = effect_data['start_time'] - duration = effect_data['duration'] - elapsed_time = current_time - start_time - - # 检查效果是否完成 - if elapsed_time >= duration: - finished_effects.append(effect_id) - continue - - # 更新效果强度 - intensity = self._calculate_effect_intensity(effect_data, elapsed_time) - effect_data['current_intensity'] = intensity - - # 应用效果到设备 - self._apply_haptic_effect(effect_data['device_id'], intensity) - - # 触发更新回调 - self._trigger_haptic_callback('effect_updated', { - 'effect_id': effect_id, - 'device_id': effect_data['device_id'], - 'intensity': intensity, - 'elapsed_time': elapsed_time - }) - - # 处理完成的效果 - for effect_id in finished_effects: - self._finish_effect(effect_id) - - except Exception as e: - print(f"✗ 活动效果更新失败: {e}") - - def _calculate_effect_intensity(self, effect_data: Dict[str, Any], elapsed_time: float) -> float: - """ - 计算效果强度 - - Args: - effect_data: 效果数据 - elapsed_time: 已经过的时间 - - Returns: - 效果强度 - """ - try: - duration = effect_data['duration'] - base_intensity = effect_data['intensity'] - - # 检查是否启用淡入淡出 - if self.haptic_config['enable_fading']: - fade_duration = self.haptic_config['fade_duration'] - - # 淡入阶段 - if elapsed_time < fade_duration: - fade_ratio = elapsed_time / fade_duration - return base_intensity * fade_ratio - - # 淡出阶段 - elif elapsed_time > duration - fade_duration: - fade_ratio = (duration - elapsed_time) / fade_duration - return base_intensity * fade_ratio - - # 正常播放阶段 - return base_intensity - - except Exception as e: - print(f"✗ 效果强度计算失败: {e}") - return 0.0 - - def _apply_haptic_effect(self, device_id: int, intensity: float): - """ - 应用触觉效果到设备 - - Args: - device_id: 设备ID - intensity: 强度 - """ - try: - # 这里应该实现实际的设备控制逻辑 - # 在实际实现中,会根据设备类型调用相应的API - pass - except Exception as e: - print(f"✗ 触觉效果应用失败: {e}") - - def _finish_effect(self, effect_id: int): - """ - 完成效果 - - Args: - effect_id: 效果ID - """ - try: - with self.haptic_lock: - if effect_id in self.active_effects: - effect_data = self.active_effects[effect_id] - - # 停止设备上的效果 - self._stop_device_effect(effect_data['device_id']) - - # 更新统计信息 - self.haptic_stats['effects_played'] += 1 - self.haptic_stats['total_haptic_time'] += effect_data['duration'] - - # 触发完成回调 - self._trigger_haptic_callback('effect_finished', { - 'effect_id': effect_id, - 'device_id': effect_data['device_id'], - 'duration': effect_data['duration'] - }) - - # 移除活动效果 - del self.active_effects[effect_id] - - except Exception as e: - print(f"✗ 效果完成处理失败: {e}") - - def _stop_device_effect(self, device_id: int): - """ - 停止设备上的效果 - - Args: - device_id: 设备ID - """ - try: - # 这里应该实现实际的设备停止逻辑 - pass - except Exception as e: - print(f"✗ 设备效果停止失败: {e}") - - def _create_default_effects(self): - """创建默认触觉效果""" - try: - # 简单点击效果 - self.create_haptic_effect( - name="click", - effect_type=HapticEffectType.SIMPLE, - parameters={ - 'intensity': 0.6, - 'duration': 0.1, - 'attack_time': 0.02, - 'decay_time': 0.02 - } - ) - - # 重击效果 - self.create_haptic_effect( - name="heavy_click", - effect_type=HapticEffectType.SIMPLE, - parameters={ - 'intensity': 0.9, - 'duration': 0.2, - 'attack_time': 0.05, - 'decay_time': 0.05 - } - ) - - # 软击效果 - self.create_haptic_effect( - name="soft_click", - effect_type=HapticEffectType.SIMPLE, - parameters={ - 'intensity': 0.3, - 'duration': 0.05, - 'attack_time': 0.01, - 'decay_time': 0.01 - } - ) - - # 震动效果 - self.create_haptic_effect( - name="vibration", - effect_type=HapticEffectType.PATTERN, - parameters={ - 'pattern': [0.5, 0.2, 0.5, 0.2], # 强度, 持续时间, 强度, 持续时间 - 'loop': True - } - ) - - # 脉冲效果 - self.create_haptic_effect( - name="pulse", - effect_type=HapticEffectType.CURVE, - parameters={ - 'curve': 'sine', # sine, square, triangle, sawtooth - 'frequency': 50.0, # Hz - 'amplitude': 0.7, - 'duration': 1.0 - } - ) - - print("✓ 默认触觉效果创建完成") - - except Exception as e: - print(f"✗ 默认触觉效果创建失败: {e}") - - def _initialize_supported_devices(self): - """初始化支持的设备""" - try: - # 在实际实现中,这里会检测和初始化连接的设备 - print("✓ 支持的设备初始化完成") - except Exception as e: - print(f"✗ 支持的设备初始化失败: {e}") - - def create_haptic_effect(self, name: str, effect_type: HapticEffectType, - parameters: Dict[str, Any]) -> int: - """ - 创建触觉效果 - - Args: - name: 效果名称 - effect_type: 效果类型 - parameters: 参数字典 - - Returns: - 效果ID - """ - try: - effect_id = self.effect_counter - self.effect_counter += 1 - - effect = { - 'id': effect_id, - 'name': name, - 'type': effect_type, - 'parameters': parameters, - 'created_time': time.time() - } - - self.haptic_effects[effect_id] = effect - self.haptic_stats['effects_registered'] += 1 - - print(f"✓ 触觉效果已创建: {name} (ID: {effect_id})") - return effect_id - - except Exception as e: - print(f"✗ 触觉效果创建失败: {e}") - return -1 - - def remove_haptic_effect(self, effect_id: int) -> bool: - """ - 移除触觉效果 - - Args: - effect_id: 效果ID - - Returns: - 是否移除成功 - """ - try: - if effect_id in self.haptic_effects: - effect_name = self.haptic_effects[effect_id]['name'] - - # 停止所有使用此效果的活动实例 - self._stop_effect_instances(effect_id) - - del self.haptic_effects[effect_id] - print(f"✓ 触觉效果已移除: {effect_name} (ID: {effect_id})") - return True - else: - print(f"✗ 触觉效果不存在: ID {effect_id}") - return False - - except Exception as e: - print(f"✗ 触觉效果移除失败: {e}") - return False - - def _stop_effect_instances(self, effect_id: int): - """ - 停止效果的所有实例 - - Args: - effect_id: 效果ID - """ - try: - with self.haptic_lock: - instances_to_stop = [ - instance_id for instance_id, instance_data in self.active_effects.items() - if instance_data['effect_id'] == effect_id - ] - - for instance_id in instances_to_stop: - self.stop_haptic_effect(instance_id) - - except Exception as e: - print(f"✗ 效果实例停止失败: {e}") - - def play_haptic_effect(self, effect_id: int, device_id: int, - intensity: float = None, duration: float = None) -> int: - """ - 播放触觉效果 - - Args: - effect_id: 效果ID - device_id: 设备ID - intensity: 强度(可选,覆盖效果默认值) - duration: 持续时间(可选,覆盖效果默认值) - - Returns: - 效果实例ID - """ - try: - if not self.enabled: - return -1 - - if effect_id not in self.haptic_effects: - print(f"✗ 触觉效果不存在: ID {effect_id}") - return -1 - - # 获取效果定义 - effect = self.haptic_effects[effect_id] - - # 使用默认值或传入值 - final_intensity = intensity if intensity is not None else effect['parameters'].get('intensity', self.haptic_config['default_intensity']) - final_duration = duration if duration is not None else effect['parameters'].get('duration', self.haptic_config['default_duration']) - - # 限制值范围 - final_intensity = max(0.0, min(self.haptic_config['max_intensity'], final_intensity)) - final_duration = max(self.haptic_config['min_duration'], min(self.haptic_config['max_duration'], final_duration)) - - # 创建效果实例 - instance_id = self.effect_instance_counter - self.effect_instance_counter += 1 - - effect_instance = { - 'instance_id': instance_id, - 'effect_id': effect_id, - 'device_id': device_id, - 'intensity': final_intensity, - 'duration': final_duration, - 'start_time': time.time(), - 'current_intensity': 0.0 - } - - with self.haptic_lock: - self.active_effects[instance_id] = effect_instance - - # 触发开始回调 - self._trigger_haptic_callback('effect_started', { - 'effect_id': instance_id, - 'device_id': device_id, - 'effect_name': effect['name'], - 'intensity': final_intensity, - 'duration': final_duration - }) - - print(f"✓ 触觉效果已播放: {effect['name']} (实例ID: {instance_id})") - return instance_id - - except Exception as e: - print(f"✗ 触觉效果播放失败: {e}") - return -1 - - def stop_haptic_effect(self, instance_id: int) -> bool: - """ - 停止触觉效果实例 - - Args: - instance_id: 效果实例ID - - Returns: - 是否停止成功 - """ - try: - with self.haptic_lock: - if instance_id in self.active_effects: - effect_data = self.active_effects[instance_id] - - # 停止设备上的效果 - self._stop_device_effect(effect_data['device_id']) - - # 更新统计信息 - self.haptic_stats['effects_cancelled'] += 1 - - # 触发取消回调 - self._trigger_haptic_callback('effect_cancelled', { - 'effect_id': instance_id, - 'device_id': effect_data['device_id'] - }) - - # 移除活动效果 - del self.active_effects[instance_id] - - print(f"✓ 触觉效果已停止: 实例ID {instance_id}") - return True - else: - print(f"✗ 触觉效果实例不存在: ID {instance_id}") - return False - - except Exception as e: - print(f"✗ 触觉效果停止失败: {e}") - return False - - def stop_all_effects(self): - """停止所有触觉效果""" - try: - with self.haptic_lock: - active_instances = list(self.active_effects.keys()) - - for instance_id in active_instances: - self.stop_haptic_effect(instance_id) - - print("✓ 所有触觉效果已停止") - - except Exception as e: - print(f"✗ 停止所有触觉效果失败: {e}") - - def play_simple_effect(self, device_id: int, intensity: float = None, - duration: float = None) -> int: - """ - 播放简单触觉效果 - - Args: - device_id: 设备ID - intensity: 强度 - duration: 持续时间 - - Returns: - 效果实例ID - """ - try: - # 创建临时简单效果 - temp_effect_id = self.create_haptic_effect( - name=f"temp_simple_{int(time.time()*1000)}", - effect_type=HapticEffectType.SIMPLE, - parameters={ - 'intensity': intensity or self.haptic_config['default_intensity'], - 'duration': duration or self.haptic_config['default_duration'] - } - ) - - # 播放效果 - instance_id = self.play_haptic_effect(temp_effect_id, device_id) - - # 移除临时效果定义 - if temp_effect_id >= 0: - # 延迟移除,确保效果播放完成 - threading.Timer( - duration or self.haptic_config['default_duration'] + 0.1, - lambda: self.remove_haptic_effect(temp_effect_id) - ).start() - - return instance_id - - except Exception as e: - print(f"✗ 简单触觉效果播放失败: {e}") - return -1 - - def register_haptic_callback(self, callback_type: str, callback: Callable): - """ - 注册触觉回调 - - Args: - callback_type: 回调类型('effect_started', 'effect_updated', 'effect_finished', 'effect_cancelled') - callback: 回调函数 - """ - try: - if callback_type in self.haptic_callbacks: - self.haptic_callbacks[callback_type].append(callback) - print(f"✓ 触觉回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 触觉回调注册失败: {e}") - - def unregister_haptic_callback(self, callback_type: str, callback: Callable): - """ - 注销触觉回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.haptic_callbacks: - if callback in self.haptic_callbacks[callback_type]: - self.haptic_callbacks[callback_type].remove(callback) - print(f"✓ 触觉回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 触觉回调注销失败: {e}") - - def _trigger_haptic_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发触觉回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.haptic_callbacks: - for callback in self.haptic_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 触觉回调执行失败: {e}") - except Exception as e: - print(f"✗ 触觉回调触发失败: {e}") - - def add_supported_device(self, device_type: HapticDeviceType, device_name: str, - capabilities: List[str]) -> int: - """ - 添加支持的设备 - - Args: - device_type: 设备类型 - device_name: 设备名称 - capabilities: 设备功能列表 - - Returns: - 设备ID - """ - try: - device_id = self.device_counter - self.device_counter += 1 - - device_info = { - 'id': device_id, - 'type': device_type, - 'name': device_name, - 'capabilities': capabilities, - 'connected': True, - 'max_intensity': 1.0, - 'supported_effects': [] - } - - self.supported_devices[device_id] = device_info - - print(f"✓ 支持的设备已添加: {device_name} (ID: {device_id})") - return device_id - - except Exception as e: - print(f"✗ 支持的设备添加失败: {e}") - return -1 - - def remove_supported_device(self, device_id: int) -> bool: - """ - 移除支持的设备 - - Args: - device_id: 设备ID - - Returns: - 是否移除成功 - """ - try: - if device_id in self.supported_devices: - device_name = self.supported_devices[device_id]['name'] - - # 停止设备上的所有效果 - self._stop_device_effects(device_id) - - del self.supported_devices[device_id] - print(f"✓ 支持的设备已移除: {device_name} (ID: {device_id})") - return True - else: - print(f"✗ 支持的设备不存在: ID {device_id}") - return False - - except Exception as e: - print(f"✗ 支持的设备移除失败: {e}") - return False - - def _stop_device_effects(self, device_id: int): - """ - 停止设备上的所有效果 - - Args: - device_id: 设备ID - """ - try: - with self.haptic_lock: - effects_to_stop = [ - instance_id for instance_id, instance_data in self.active_effects.items() - if instance_data['device_id'] == device_id - ] - - for instance_id in effects_to_stop: - self.stop_haptic_effect(instance_id) - - except Exception as e: - print(f"✗ 设备效果停止失败: {e}") - - def get_haptic_effect(self, effect_id: int) -> Optional[Dict[str, Any]]: - """ - 获取触觉效果信息 - - Args: - effect_id: 效果ID - - Returns: - 效果信息字典或None - """ - return self.haptic_effects.get(effect_id) - - def get_all_haptic_effects(self) -> Dict[int, Dict[str, Any]]: - """ - 获取所有触觉效果 - - Returns: - 所有触觉效果字典 - """ - return self.haptic_effects.copy() - - def get_supported_devices(self) -> Dict[int, Dict[str, Any]]: - """ - 获取支持的设备 - - Returns: - 支持的设备字典 - """ - return self.supported_devices.copy() - - def set_haptic_config(self, config: Dict[str, Any]): - """ - 设置触觉配置 - - Args: - config: 触觉配置字典 - """ - try: - self.haptic_config.update(config) - print(f"✓ 触觉配置已更新: {self.haptic_config}") - except Exception as e: - print(f"✗ 触觉配置设置失败: {e}") - - def get_haptic_config(self) -> Dict[str, Any]: - """ - 获取触觉配置 - - Returns: - 触觉配置字典 - """ - return self.haptic_config.copy() - - def get_haptic_stats(self) -> Dict[str, Any]: - """ - 获取触觉统计信息 - - Returns: - 触觉统计字典 - """ - return self.haptic_stats.copy() - - def reset_haptic_stats(self): - """重置触觉统计信息""" - try: - self.haptic_stats = { - 'effects_registered': len(self.haptic_effects), - 'effects_played': 0, - 'effects_cancelled': 0, - 'active_effects': 0, - 'total_haptic_time': 0.0 - } - print("✓ 触觉统计信息已重置") - except Exception as e: - print(f"✗ 触觉统计信息重置失败: {e}") - - def get_active_effects(self) -> Dict[int, Dict[str, Any]]: - """ - 获取活动效果 - - Returns: - 活动效果字典 - """ - with self.haptic_lock: +""" +触觉反馈管理器模块 +负责管理和播放各种触觉反馈效果 +""" + +import time +from typing import Dict, Any, List, Optional, Callable, Union +from enum import Enum +import threading +import math + +class HapticEffectType(Enum): + """触觉效果类型枚举""" + SIMPLE = "simple" + PATTERN = "pattern" + CURVE = "curve" + CUSTOM = "custom" + +class HapticDeviceType(Enum): + """触觉设备类型枚举""" + GAMEPAD = "gamepad" + VR_CONTROLLER = "vr_controller" + TOUCH_DEVICE = "touch_device" + CUSTOM = "custom" + +class HapticManager: + """ + 触觉反馈管理器 + 负责管理和播放各种触觉反馈效果 + """ + + def __init__(self, plugin): + """ + 初始化触觉反馈管理器 + + Args: + plugin: 多平台输入支持插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 触觉效果配置 + self.haptic_config = { + 'default_intensity': 0.5, # 默认强度 (0.0-1.0) + 'default_duration': 0.2, # 默认持续时间(秒) + 'max_intensity': 1.0, # 最大强度 + 'min_duration': 0.01, # 最小持续时间(秒) + 'max_duration': 5.0, # 最大持续时间(秒) + 'update_rate': 60.0, # 更新频率(Hz) + 'enable_fading': True, # 启用淡入淡出 + 'fade_duration': 0.05 # 淡入淡出持续时间(秒) + } + + # 触觉效果定义 + self.haptic_effects = {} + self.effect_counter = 0 + + # 活动效果 + self.active_effects = {} + self.effect_instance_counter = 0 + + # 设备支持 + self.supported_devices = {} + self.device_counter = 0 + + # 回调函数 + self.haptic_callbacks = { + 'effect_started': [], + 'effect_updated': [], + 'effect_finished': [], + 'effect_cancelled': [] + } + + # 统计信息 + self.haptic_stats = { + 'effects_registered': 0, + 'effects_played': 0, + 'effects_cancelled': 0, + 'active_effects': 0, + 'total_haptic_time': 0.0 + } + + # 时间戳记录 + self.last_effect_time = {} + + # 线程管理 + self.haptic_thread = None + self.running = False + self.haptic_lock = threading.Lock() + + print("✓ 触觉反馈管理器已创建") + + def initialize(self) -> bool: + """ + 初始化触觉反馈管理器 + + Returns: + 是否初始化成功 + """ + try: + # 创建默认触觉效果 + self._create_default_effects() + + # 初始化支持的设备 + self._initialize_supported_devices() + + self.initialized = True + print("✓ 触觉反馈管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 触觉反馈管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用触觉反馈管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 触觉反馈管理器未初始化") + return False + + self.enabled = True + self.running = True + + # 启动触觉更新线程 + self.haptic_thread = threading.Thread(target=self._haptic_update_loop, daemon=True) + self.haptic_thread.start() + + print("✓ 触觉反馈管理器已启用") + return True + + except Exception as e: + print(f"✗ 触觉反馈管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用触觉反馈管理器""" + try: + self.enabled = False + self.running = False + + # 等待触觉线程结束 + if self.haptic_thread and self.haptic_thread.is_alive(): + self.haptic_thread.join(timeout=2.0) + + # 停止所有活动效果 + self.stop_all_effects() + + print("✓ 触觉反馈管理器已禁用") + + except Exception as e: + print(f"✗ 触觉反馈管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理触觉反馈管理器资源""" + try: + self.disable() + self.haptic_effects.clear() + self.active_effects.clear() + self.supported_devices.clear() + self.haptic_callbacks.clear() + self.last_effect_time.clear() + self.initialized = False + print("✓ 触觉反馈管理器资源已清理") + + except Exception as e: + print(f"✗ 触觉反馈管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新触觉反馈管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新统计信息 + self.haptic_stats['active_effects'] = len(self.active_effects) + + except Exception as e: + print(f"✗ 触觉反馈管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _haptic_update_loop(self): + """触觉更新循环""" + try: + update_interval = 1.0 / self.haptic_config['update_rate'] + + while self.running: + if self.enabled: + self._update_active_effects() + + time.sleep(update_interval) + + except Exception as e: + print(f"✗ 触觉更新循环失败: {e}") + + def _update_active_effects(self): + """更新活动效果""" + try: + current_time = time.time() + finished_effects = [] + + with self.haptic_lock: + for effect_id, effect_data in self.active_effects.items(): + start_time = effect_data['start_time'] + duration = effect_data['duration'] + elapsed_time = current_time - start_time + + # 检查效果是否完成 + if elapsed_time >= duration: + finished_effects.append(effect_id) + continue + + # 更新效果强度 + intensity = self._calculate_effect_intensity(effect_data, elapsed_time) + effect_data['current_intensity'] = intensity + + # 应用效果到设备 + self._apply_haptic_effect(effect_data['device_id'], intensity) + + # 触发更新回调 + self._trigger_haptic_callback('effect_updated', { + 'effect_id': effect_id, + 'device_id': effect_data['device_id'], + 'intensity': intensity, + 'elapsed_time': elapsed_time + }) + + # 处理完成的效果 + for effect_id in finished_effects: + self._finish_effect(effect_id) + + except Exception as e: + print(f"✗ 活动效果更新失败: {e}") + + def _calculate_effect_intensity(self, effect_data: Dict[str, Any], elapsed_time: float) -> float: + """ + 计算效果强度 + + Args: + effect_data: 效果数据 + elapsed_time: 已经过的时间 + + Returns: + 效果强度 + """ + try: + duration = effect_data['duration'] + base_intensity = effect_data['intensity'] + + # 检查是否启用淡入淡出 + if self.haptic_config['enable_fading']: + fade_duration = self.haptic_config['fade_duration'] + + # 淡入阶段 + if elapsed_time < fade_duration: + fade_ratio = elapsed_time / fade_duration + return base_intensity * fade_ratio + + # 淡出阶段 + elif elapsed_time > duration - fade_duration: + fade_ratio = (duration - elapsed_time) / fade_duration + return base_intensity * fade_ratio + + # 正常播放阶段 + return base_intensity + + except Exception as e: + print(f"✗ 效果强度计算失败: {e}") + return 0.0 + + def _apply_haptic_effect(self, device_id: int, intensity: float): + """ + 应用触觉效果到设备 + + Args: + device_id: 设备ID + intensity: 强度 + """ + try: + # 这里应该实现实际的设备控制逻辑 + # 在实际实现中,会根据设备类型调用相应的API + pass + except Exception as e: + print(f"✗ 触觉效果应用失败: {e}") + + def _finish_effect(self, effect_id: int): + """ + 完成效果 + + Args: + effect_id: 效果ID + """ + try: + with self.haptic_lock: + if effect_id in self.active_effects: + effect_data = self.active_effects[effect_id] + + # 停止设备上的效果 + self._stop_device_effect(effect_data['device_id']) + + # 更新统计信息 + self.haptic_stats['effects_played'] += 1 + self.haptic_stats['total_haptic_time'] += effect_data['duration'] + + # 触发完成回调 + self._trigger_haptic_callback('effect_finished', { + 'effect_id': effect_id, + 'device_id': effect_data['device_id'], + 'duration': effect_data['duration'] + }) + + # 移除活动效果 + del self.active_effects[effect_id] + + except Exception as e: + print(f"✗ 效果完成处理失败: {e}") + + def _stop_device_effect(self, device_id: int): + """ + 停止设备上的效果 + + Args: + device_id: 设备ID + """ + try: + # 这里应该实现实际的设备停止逻辑 + pass + except Exception as e: + print(f"✗ 设备效果停止失败: {e}") + + def _create_default_effects(self): + """创建默认触觉效果""" + try: + # 简单点击效果 + self.create_haptic_effect( + name="click", + effect_type=HapticEffectType.SIMPLE, + parameters={ + 'intensity': 0.6, + 'duration': 0.1, + 'attack_time': 0.02, + 'decay_time': 0.02 + } + ) + + # 重击效果 + self.create_haptic_effect( + name="heavy_click", + effect_type=HapticEffectType.SIMPLE, + parameters={ + 'intensity': 0.9, + 'duration': 0.2, + 'attack_time': 0.05, + 'decay_time': 0.05 + } + ) + + # 软击效果 + self.create_haptic_effect( + name="soft_click", + effect_type=HapticEffectType.SIMPLE, + parameters={ + 'intensity': 0.3, + 'duration': 0.05, + 'attack_time': 0.01, + 'decay_time': 0.01 + } + ) + + # 震动效果 + self.create_haptic_effect( + name="vibration", + effect_type=HapticEffectType.PATTERN, + parameters={ + 'pattern': [0.5, 0.2, 0.5, 0.2], # 强度, 持续时间, 强度, 持续时间 + 'loop': True + } + ) + + # 脉冲效果 + self.create_haptic_effect( + name="pulse", + effect_type=HapticEffectType.CURVE, + parameters={ + 'curve': 'sine', # sine, square, triangle, sawtooth + 'frequency': 50.0, # Hz + 'amplitude': 0.7, + 'duration': 1.0 + } + ) + + print("✓ 默认触觉效果创建完成") + + except Exception as e: + print(f"✗ 默认触觉效果创建失败: {e}") + + def _initialize_supported_devices(self): + """初始化支持的设备""" + try: + # 在实际实现中,这里会检测和初始化连接的设备 + print("✓ 支持的设备初始化完成") + except Exception as e: + print(f"✗ 支持的设备初始化失败: {e}") + + def create_haptic_effect(self, name: str, effect_type: HapticEffectType, + parameters: Dict[str, Any]) -> int: + """ + 创建触觉效果 + + Args: + name: 效果名称 + effect_type: 效果类型 + parameters: 参数字典 + + Returns: + 效果ID + """ + try: + effect_id = self.effect_counter + self.effect_counter += 1 + + effect = { + 'id': effect_id, + 'name': name, + 'type': effect_type, + 'parameters': parameters, + 'created_time': time.time() + } + + self.haptic_effects[effect_id] = effect + self.haptic_stats['effects_registered'] += 1 + + print(f"✓ 触觉效果已创建: {name} (ID: {effect_id})") + return effect_id + + except Exception as e: + print(f"✗ 触觉效果创建失败: {e}") + return -1 + + def remove_haptic_effect(self, effect_id: int) -> bool: + """ + 移除触觉效果 + + Args: + effect_id: 效果ID + + Returns: + 是否移除成功 + """ + try: + if effect_id in self.haptic_effects: + effect_name = self.haptic_effects[effect_id]['name'] + + # 停止所有使用此效果的活动实例 + self._stop_effect_instances(effect_id) + + del self.haptic_effects[effect_id] + print(f"✓ 触觉效果已移除: {effect_name} (ID: {effect_id})") + return True + else: + print(f"✗ 触觉效果不存在: ID {effect_id}") + return False + + except Exception as e: + print(f"✗ 触觉效果移除失败: {e}") + return False + + def _stop_effect_instances(self, effect_id: int): + """ + 停止效果的所有实例 + + Args: + effect_id: 效果ID + """ + try: + with self.haptic_lock: + instances_to_stop = [ + instance_id for instance_id, instance_data in self.active_effects.items() + if instance_data['effect_id'] == effect_id + ] + + for instance_id in instances_to_stop: + self.stop_haptic_effect(instance_id) + + except Exception as e: + print(f"✗ 效果实例停止失败: {e}") + + def play_haptic_effect(self, effect_id: int, device_id: int, + intensity: float = None, duration: float = None) -> int: + """ + 播放触觉效果 + + Args: + effect_id: 效果ID + device_id: 设备ID + intensity: 强度(可选,覆盖效果默认值) + duration: 持续时间(可选,覆盖效果默认值) + + Returns: + 效果实例ID + """ + try: + if not self.enabled: + return -1 + + if effect_id not in self.haptic_effects: + print(f"✗ 触觉效果不存在: ID {effect_id}") + return -1 + + # 获取效果定义 + effect = self.haptic_effects[effect_id] + + # 使用默认值或传入值 + final_intensity = intensity if intensity is not None else effect['parameters'].get('intensity', self.haptic_config['default_intensity']) + final_duration = duration if duration is not None else effect['parameters'].get('duration', self.haptic_config['default_duration']) + + # 限制值范围 + final_intensity = max(0.0, min(self.haptic_config['max_intensity'], final_intensity)) + final_duration = max(self.haptic_config['min_duration'], min(self.haptic_config['max_duration'], final_duration)) + + # 创建效果实例 + instance_id = self.effect_instance_counter + self.effect_instance_counter += 1 + + effect_instance = { + 'instance_id': instance_id, + 'effect_id': effect_id, + 'device_id': device_id, + 'intensity': final_intensity, + 'duration': final_duration, + 'start_time': time.time(), + 'current_intensity': 0.0 + } + + with self.haptic_lock: + self.active_effects[instance_id] = effect_instance + + # 触发开始回调 + self._trigger_haptic_callback('effect_started', { + 'effect_id': instance_id, + 'device_id': device_id, + 'effect_name': effect['name'], + 'intensity': final_intensity, + 'duration': final_duration + }) + + print(f"✓ 触觉效果已播放: {effect['name']} (实例ID: {instance_id})") + return instance_id + + except Exception as e: + print(f"✗ 触觉效果播放失败: {e}") + return -1 + + def stop_haptic_effect(self, instance_id: int) -> bool: + """ + 停止触觉效果实例 + + Args: + instance_id: 效果实例ID + + Returns: + 是否停止成功 + """ + try: + with self.haptic_lock: + if instance_id in self.active_effects: + effect_data = self.active_effects[instance_id] + + # 停止设备上的效果 + self._stop_device_effect(effect_data['device_id']) + + # 更新统计信息 + self.haptic_stats['effects_cancelled'] += 1 + + # 触发取消回调 + self._trigger_haptic_callback('effect_cancelled', { + 'effect_id': instance_id, + 'device_id': effect_data['device_id'] + }) + + # 移除活动效果 + del self.active_effects[instance_id] + + print(f"✓ 触觉效果已停止: 实例ID {instance_id}") + return True + else: + print(f"✗ 触觉效果实例不存在: ID {instance_id}") + return False + + except Exception as e: + print(f"✗ 触觉效果停止失败: {e}") + return False + + def stop_all_effects(self): + """停止所有触觉效果""" + try: + with self.haptic_lock: + active_instances = list(self.active_effects.keys()) + + for instance_id in active_instances: + self.stop_haptic_effect(instance_id) + + print("✓ 所有触觉效果已停止") + + except Exception as e: + print(f"✗ 停止所有触觉效果失败: {e}") + + def play_simple_effect(self, device_id: int, intensity: float = None, + duration: float = None) -> int: + """ + 播放简单触觉效果 + + Args: + device_id: 设备ID + intensity: 强度 + duration: 持续时间 + + Returns: + 效果实例ID + """ + try: + # 创建临时简单效果 + temp_effect_id = self.create_haptic_effect( + name=f"temp_simple_{int(time.time()*1000)}", + effect_type=HapticEffectType.SIMPLE, + parameters={ + 'intensity': intensity or self.haptic_config['default_intensity'], + 'duration': duration or self.haptic_config['default_duration'] + } + ) + + # 播放效果 + instance_id = self.play_haptic_effect(temp_effect_id, device_id) + + # 移除临时效果定义 + if temp_effect_id >= 0: + # 延迟移除,确保效果播放完成 + threading.Timer( + duration or self.haptic_config['default_duration'] + 0.1, + lambda: self.remove_haptic_effect(temp_effect_id) + ).start() + + return instance_id + + except Exception as e: + print(f"✗ 简单触觉效果播放失败: {e}") + return -1 + + def register_haptic_callback(self, callback_type: str, callback: Callable): + """ + 注册触觉回调 + + Args: + callback_type: 回调类型('effect_started', 'effect_updated', 'effect_finished', 'effect_cancelled') + callback: 回调函数 + """ + try: + if callback_type in self.haptic_callbacks: + self.haptic_callbacks[callback_type].append(callback) + print(f"✓ 触觉回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 触觉回调注册失败: {e}") + + def unregister_haptic_callback(self, callback_type: str, callback: Callable): + """ + 注销触觉回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.haptic_callbacks: + if callback in self.haptic_callbacks[callback_type]: + self.haptic_callbacks[callback_type].remove(callback) + print(f"✓ 触觉回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 触觉回调注销失败: {e}") + + def _trigger_haptic_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发触觉回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.haptic_callbacks: + for callback in self.haptic_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 触觉回调执行失败: {e}") + except Exception as e: + print(f"✗ 触觉回调触发失败: {e}") + + def add_supported_device(self, device_type: HapticDeviceType, device_name: str, + capabilities: List[str]) -> int: + """ + 添加支持的设备 + + Args: + device_type: 设备类型 + device_name: 设备名称 + capabilities: 设备功能列表 + + Returns: + 设备ID + """ + try: + device_id = self.device_counter + self.device_counter += 1 + + device_info = { + 'id': device_id, + 'type': device_type, + 'name': device_name, + 'capabilities': capabilities, + 'connected': True, + 'max_intensity': 1.0, + 'supported_effects': [] + } + + self.supported_devices[device_id] = device_info + + print(f"✓ 支持的设备已添加: {device_name} (ID: {device_id})") + return device_id + + except Exception as e: + print(f"✗ 支持的设备添加失败: {e}") + return -1 + + def remove_supported_device(self, device_id: int) -> bool: + """ + 移除支持的设备 + + Args: + device_id: 设备ID + + Returns: + 是否移除成功 + """ + try: + if device_id in self.supported_devices: + device_name = self.supported_devices[device_id]['name'] + + # 停止设备上的所有效果 + self._stop_device_effects(device_id) + + del self.supported_devices[device_id] + print(f"✓ 支持的设备已移除: {device_name} (ID: {device_id})") + return True + else: + print(f"✗ 支持的设备不存在: ID {device_id}") + return False + + except Exception as e: + print(f"✗ 支持的设备移除失败: {e}") + return False + + def _stop_device_effects(self, device_id: int): + """ + 停止设备上的所有效果 + + Args: + device_id: 设备ID + """ + try: + with self.haptic_lock: + effects_to_stop = [ + instance_id for instance_id, instance_data in self.active_effects.items() + if instance_data['device_id'] == device_id + ] + + for instance_id in effects_to_stop: + self.stop_haptic_effect(instance_id) + + except Exception as e: + print(f"✗ 设备效果停止失败: {e}") + + def get_haptic_effect(self, effect_id: int) -> Optional[Dict[str, Any]]: + """ + 获取触觉效果信息 + + Args: + effect_id: 效果ID + + Returns: + 效果信息字典或None + """ + return self.haptic_effects.get(effect_id) + + def get_all_haptic_effects(self) -> Dict[int, Dict[str, Any]]: + """ + 获取所有触觉效果 + + Returns: + 所有触觉效果字典 + """ + return self.haptic_effects.copy() + + def get_supported_devices(self) -> Dict[int, Dict[str, Any]]: + """ + 获取支持的设备 + + Returns: + 支持的设备字典 + """ + return self.supported_devices.copy() + + def set_haptic_config(self, config: Dict[str, Any]): + """ + 设置触觉配置 + + Args: + config: 触觉配置字典 + """ + try: + self.haptic_config.update(config) + print(f"✓ 触觉配置已更新: {self.haptic_config}") + except Exception as e: + print(f"✗ 触觉配置设置失败: {e}") + + def get_haptic_config(self) -> Dict[str, Any]: + """ + 获取触觉配置 + + Returns: + 触觉配置字典 + """ + return self.haptic_config.copy() + + def get_haptic_stats(self) -> Dict[str, Any]: + """ + 获取触觉统计信息 + + Returns: + 触觉统计字典 + """ + return self.haptic_stats.copy() + + def reset_haptic_stats(self): + """重置触觉统计信息""" + try: + self.haptic_stats = { + 'effects_registered': len(self.haptic_effects), + 'effects_played': 0, + 'effects_cancelled': 0, + 'active_effects': 0, + 'total_haptic_time': 0.0 + } + print("✓ 触觉统计信息已重置") + except Exception as e: + print(f"✗ 触觉统计信息重置失败: {e}") + + def get_active_effects(self) -> Dict[int, Dict[str, Any]]: + """ + 获取活动效果 + + Returns: + 活动效果字典 + """ + with self.haptic_lock: return self.active_effects.copy() \ No newline at end of file diff --git a/plugins/user/multiplatform_input/mapping/input_mapper.py b/plugins/user/multiplatform_input/mapping/input_mapper.py index 39b1dd13..788a6f37 100644 --- a/plugins/user/multiplatform_input/mapping/input_mapper.py +++ b/plugins/user/multiplatform_input/mapping/input_mapper.py @@ -1,877 +1,877 @@ -""" -输入映射器模块 -负责将物理输入映射到逻辑动作 -""" - -import time -from typing import Dict, Any, List, Optional, Callable, Union -from enum import Enum -import json - -class MappingType(Enum): - """映射类型枚举""" - BUTTON = "button" - AXIS = "axis" - GESTURE = "gesture" - COMBINATION = "combination" - SEQUENCE = "sequence" - -class ActionType(Enum): - """动作类型枚举""" - SIMPLE = "simple" - CONTINUOUS = "continuous" - TOGGLE = "toggle" - HOLD = "hold" - -class InputMapper: - """ - 输入映射器 - 负责将物理输入映射到逻辑动作 - """ - - def __init__(self, plugin): - """ - 初始化输入映射器 - - Args: - plugin: 多平台输入支持插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 映射配置 - self.mappings = {} - self.mapping_counter = 0 - - # 动作定义 - self.actions = {} - self.action_counter = 0 - - # 映射配置 - self.mapping_config = { - 'enable_mapping': True, - 'enable_combinations': True, - 'enable_sequences': True, - 'combination_timeout': 0.5, # 组合键超时时间(秒) - 'sequence_timeout': 1.0, # 序列键超时时间(秒) - 'double_tap_timeout': 0.3, # 双击超时时间(秒) - 'hold_threshold': 0.5 # 按住阈值时间(秒) - } - - # 当前激活的输入 - self.active_inputs = {} - self.active_combinations = {} - self.active_sequences = {} - - # 输入历史(用于序列检测) - self.input_history = [] - - # 动作状态 - self.action_states = {} - - # 回调函数 - self.action_callbacks = {} - - # 统计信息 - self.mapping_stats = { - 'mappings_loaded': 0, - 'actions_triggered': 0, - 'combinations_detected': 0, - 'sequences_detected': 0, - 'mappings_updated': 0 - } - - # 时间戳记录 - self.last_input_time = {} - self.action_start_time = {} - - print("✓ 输入映射器已创建") - - def initialize(self) -> bool: - """ - 初始化输入映射器 - - Returns: - 是否初始化成功 - """ - try: - # 创建默认映射 - self._create_default_mappings() - - self.initialized = True - print("✓ 输入映射器初始化完成") - return True - - except Exception as e: - print(f"✗ 输入映射器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用输入映射器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 输入映射器未初始化") - return False - - self.enabled = True - print("✓ 输入映射器已启用") - return True - - except Exception as e: - print(f"✗ 输入映射器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用输入映射器""" - try: - self.enabled = False - print("✓ 输入映射器已禁用") - - except Exception as e: - print(f"✗ 输入映射器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理输入映射器资源""" - try: - self.disable() - self.mappings.clear() - self.actions.clear() - self.active_inputs.clear() - self.active_combinations.clear() - self.active_sequences.clear() - self.input_history.clear() - self.action_states.clear() - self.action_callbacks.clear() - self.initialized = False - print("✓ 输入映射器资源已清理") - - except Exception as e: - print(f"✗ 输入映射器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新输入映射器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 检查按住动作 - self._check_hold_actions() - - # 检查组合键超时 - self._check_combination_timeouts() - - # 检查序列超时 - self._check_sequence_timeouts() - - except Exception as e: - print(f"✗ 输入映射器更新失败: {e}") - import traceback - traceback.print_exc() - - def _create_default_mappings(self): - """创建默认映射""" - try: - # 键盘默认映射 - self.create_mapping( - name="前进", - device_type="keyboard", - input_id="w", - action_name="move_forward", - mapping_type=MappingType.BUTTON, - action_type=ActionType.CONTINUOUS - ) - - self.create_mapping( - name="后退", - device_type="keyboard", - input_id="s", - action_name="move_backward", - mapping_type=MappingType.BUTTON, - action_type=ActionType.CONTINUOUS - ) - - self.create_mapping( - name="左转", - device_type="keyboard", - input_id="a", - action_name="turn_left", - mapping_type=MappingType.BUTTON, - action_type=ActionType.CONTINUOUS - ) - - self.create_mapping( - name="右转", - device_type="keyboard", - input_id="d", - action_name="turn_right", - mapping_type=MappingType.BUTTON, - action_type=ActionType.CONTINUOUS - ) - - self.create_mapping( - name="跳跃", - device_type="keyboard", - input_id="space", - action_name="jump", - mapping_type=MappingType.BUTTON, - action_type=ActionType.SIMPLE - ) - - self.create_mapping( - name="蹲下", - device_type="keyboard", - input_id="c", - action_name="crouch", - mapping_type=MappingType.BUTTON, - action_type=ActionType.TOGGLE - ) - - # 鼠标默认映射 - self.create_mapping( - name="查看", - device_type="mouse", - input_id="movement", - action_name="look", - mapping_type=MappingType.AXIS, - action_type=ActionType.CONTINUOUS - ) - - self.create_mapping( - name="射击", - device_type="mouse", - input_id="button_1", # 左键 - action_name="shoot", - mapping_type=MappingType.BUTTON, - action_type=ActionType.SIMPLE - ) - - # 手柄默认映射 - self.create_mapping( - name="移动", - device_type="gamepad", - input_id="left_stick", - action_name="move", - mapping_type=MappingType.AXIS, - action_type=ActionType.CONTINUOUS - ) - - self.create_mapping( - name="视角", - device_type="gamepad", - input_id="right_stick", - action_name="look", - mapping_type=MappingType.AXIS, - action_type=ActionType.CONTINUOUS - ) - - print("✓ 默认映射创建完成") - - except Exception as e: - print(f"✗ 默认映射创建失败: {e}") - - def _check_hold_actions(self): - """检查按住动作""" - try: - current_time = time.time() - - for action_name, state in self.action_states.items(): - if state.get('active', False) and state.get('type') == ActionType.HOLD: - start_time = self.action_start_time.get(action_name, current_time) - hold_duration = current_time - start_time - - if hold_duration >= self.mapping_config['hold_threshold']: - # 触发按住动作 - self._trigger_action(action_name, { - 'type': 'hold', - 'duration': hold_duration - }) - - except Exception as e: - print(f"✗ 按住动作检查失败: {e}") - - def _check_combination_timeouts(self): - """检查组合键超时""" - try: - current_time = time.time() - - expired_combinations = [] - for combo_key, combo_data in self.active_combinations.items(): - if current_time - combo_data['timestamp'] > self.mapping_config['combination_timeout']: - expired_combinations.append(combo_key) - - for combo_key in expired_combinations: - del self.active_combinations[combo_key] - - except Exception as e: - print(f"✗ 组合键超时检查失败: {e}") - - def _check_sequence_timeouts(self): - """检查序列超时""" - try: - current_time = time.time() - - if self.input_history: - last_input_time = self.input_history[-1]['timestamp'] - if current_time - last_input_time > self.mapping_config['sequence_timeout']: - self.input_history.clear() - - except Exception as e: - print(f"✗ 序列超时检查失败: {e}") - - def create_mapping(self, name: str, device_type: str, input_id: str, - action_name: str, mapping_type: MappingType, - action_type: ActionType, modifiers: List[str] = None, - parameters: Dict[str, Any] = None) -> int: - """ - 创建输入映射 - - Args: - name: 映射名称 - device_type: 设备类型 - input_id: 输入ID - action_name: 动作名称 - mapping_type: 映射类型 - action_type: 动作类型 - modifiers: 修饰键列表 - parameters: 参数字典 - - Returns: - 映射ID - """ - try: - mapping_id = self.mapping_counter - self.mapping_counter += 1 - - mapping = { - 'id': mapping_id, - 'name': name, - 'device_type': device_type, - 'input_id': input_id, - 'action_name': action_name, - 'mapping_type': mapping_type, - 'action_type': action_type, - 'modifiers': modifiers or [], - 'parameters': parameters or {}, - 'enabled': True, - 'created_time': time.time() - } - - self.mappings[mapping_id] = mapping - self.mapping_stats['mappings_loaded'] += 1 - - print(f"✓ 输入映射已创建: {name} (ID: {mapping_id})") - return mapping_id - - except Exception as e: - print(f"✗ 输入映射创建失败: {e}") - return -1 - - def remove_mapping(self, mapping_id: int) -> bool: - """ - 移除输入映射 - - Args: - mapping_id: 映射ID - - Returns: - 是否移除成功 - """ - try: - if mapping_id in self.mappings: - mapping_name = self.mappings[mapping_id]['name'] - del self.mappings[mapping_id] - print(f"✓ 输入映射已移除: {mapping_name} (ID: {mapping_id})") - return True - else: - print(f"✗ 输入映射不存在: ID {mapping_id}") - return False - - except Exception as e: - print(f"✗ 输入映射移除失败: {e}") - return False - - def create_action(self, name: str, description: str = "", - parameters: Dict[str, Any] = None) -> int: - """ - 创建动作定义 - - Args: - name: 动作名称 - description: 动作描述 - parameters: 参数字典 - - Returns: - 动作ID - """ - try: - action_id = self.action_counter - self.action_counter += 1 - - action = { - 'id': action_id, - 'name': name, - 'description': description, - 'parameters': parameters or {}, - 'created_time': time.time() - } - - self.actions[action_id] = action - - print(f"✓ 动作定义已创建: {name} (ID: {action_id})") - return action_id - - except Exception as e: - print(f"✗ 动作定义创建失败: {e}") - return -1 - - def remove_action(self, action_id: int) -> bool: - """ - 移除动作定义 - - Args: - action_id: 动作ID - - Returns: - 是否移除成功 - """ - try: - if action_id in self.actions: - action_name = self.actions[action_id]['name'] - del self.actions[action_id] - print(f"✓ 动作定义已移除: {action_name} (ID: {action_id})") - return True - else: - print(f"✗ 动作定义不存在: ID {action_id}") - return False - - except Exception as e: - print(f"✗ 动作定义移除失败: {e}") - return False - - def handle_input_event(self, device_id: int, device_type: str, input_id: str, - value: Any, timestamp: float = None): - """ - 处理输入事件 - - Args: - device_id: 设备ID - device_type: 设备类型 - input_id: 输入ID - value: 输入值 - timestamp: 时间戳 - """ - try: - if not self.enabled or not self.mapping_config['enable_mapping']: - return - - if timestamp is None: - timestamp = time.time() - - # 记录输入历史 - self.input_history.append({ - 'device_id': device_id, - 'device_type': device_type, - 'input_id': input_id, - 'value': value, - 'timestamp': timestamp - }) - - # 保持历史记录大小 - if len(self.input_history) > 10: - self.input_history.pop(0) - - # 查找匹配的映射 - matched_mappings = self._find_matching_mappings(device_type, input_id, value) - - # 处理匹配的映射 - for mapping in matched_mappings: - self._process_mapping(mapping, device_id, value, timestamp) - - except Exception as e: - print(f"✗ 输入事件处理失败: {e}") - - def _find_matching_mappings(self, device_type: str, input_id: str, value: Any) -> List[Dict[str, Any]]: - """ - 查找匹配的映射 - - Args: - device_type: 设备类型 - input_id: 输入ID - value: 输入值 - - Returns: - 匹配的映射列表 - """ - try: - matched = [] - - for mapping in self.mappings.values(): - if not mapping['enabled']: - continue - - if (mapping['device_type'] == device_type and - mapping['input_id'] == input_id): - matched.append(mapping) - - return matched - except Exception as e: - print(f"✗ 匹配映射查找失败: {e}") - return [] - - def _process_mapping(self, mapping: Dict[str, Any], device_id: int, - value: Any, timestamp: float): - """ - 处理映射 - - Args: - mapping: 映射配置 - device_id: 设备ID - value: 输入值 - timestamp: 时间戳 - """ - try: - action_name = mapping['action_name'] - action_type = mapping['action_type'] - - # 根据动作类型处理 - if action_type == ActionType.SIMPLE: - # 简单动作:按下时触发 - if abs(value) > 0.1: # 超过死区 - self._trigger_action(action_name, { - 'device_id': device_id, - 'value': value, - 'timestamp': timestamp - }) - - elif action_type == ActionType.CONTINUOUS: - # 连续动作:根据值持续触发 - self._trigger_action(action_name, { - 'device_id': device_id, - 'value': value, - 'timestamp': timestamp - }) - - elif action_type == ActionType.TOGGLE: - # 切换动作:按下时切换状态 - if abs(value) > 0.1: - current_state = self.action_states.get(action_name, {}).get('active', False) - new_state = not current_state - self.action_states[action_name] = { - 'active': new_state, - 'type': ActionType.TOGGLE - } - - self._trigger_action(action_name, { - 'device_id': device_id, - 'value': new_state, - 'timestamp': timestamp - }) - - elif action_type == ActionType.HOLD: - # 按住动作:需要按住一定时间 - if abs(value) > 0.1: - # 按下 - self.action_states[action_name] = { - 'active': True, - 'type': ActionType.HOLD - } - self.action_start_time[action_name] = timestamp - else: - # 释放 - if action_name in self.action_states: - del self.action_states[action_name] - if action_name in self.action_start_time: - del self.action_start_time[action_name] - - except Exception as e: - print(f"✗ 映射处理失败: {e}") - - def _trigger_action(self, action_name: str, action_data: Dict[str, Any]): - """ - 触发动作 - - Args: - action_name: 动作名称 - action_data: 动作数据 - """ - try: - # 调用动作回调 - if action_name in self.action_callbacks: - callback = self.action_callbacks[action_name] - try: - callback(action_data) - self.mapping_stats['actions_triggered'] += 1 - except Exception as e: - print(f"✗ 动作回调执行失败: {e}") - else: - print(f"✗ 未找到动作回调: {action_name}") - - except Exception as e: - print(f"✗ 动作触发失败: {e}") - - def register_action_callback(self, action_name: str, callback: Callable): - """ - 注册动作回调 - - Args: - action_name: 动作名称 - callback: 回调函数 - """ - try: - self.action_callbacks[action_name] = callback - print(f"✓ 动作回调已注册: {action_name}") - except Exception as e: - print(f"✗ 动作回调注册失败: {e}") - - def unregister_action_callback(self, action_name: str): - """ - 注销动作回调 - - Args: - action_name: 动作名称 - """ - try: - if action_name in self.action_callbacks: - del self.action_callbacks[action_name] - print(f"✓ 动作回调已注销: {action_name}") - except Exception as e: - print(f"✗ 动作回调注销失败: {e}") - - def get_mapping(self, mapping_id: int) -> Optional[Dict[str, Any]]: - """ - 获取映射配置 - - Args: - mapping_id: 映射ID - - Returns: - 映射配置字典或None - """ - return self.mappings.get(mapping_id) - - def get_all_mappings(self) -> Dict[int, Dict[str, Any]]: - """ - 获取所有映射配置 - - Returns: - 所有映射配置字典 - """ - return self.mappings.copy() - - def get_action(self, action_id: int) -> Optional[Dict[str, Any]]: - """ - 获取动作定义 - - Args: - action_id: 动作ID - - Returns: - 动作定义字典或None - """ - return self.actions.get(action_id) - - def get_all_actions(self) -> Dict[int, Dict[str, Any]]: - """ - 获取所有动作定义 - - Returns: - 所有动作定义字典 - """ - return self.actions.copy() - - def update_mapping(self, mapping_id: int, updates: Dict[str, Any]) -> bool: - """ - 更新映射配置 - - Args: - mapping_id: 映射ID - updates: 更新字典 - - Returns: - 是否更新成功 - """ - try: - if mapping_id in self.mappings: - self.mappings[mapping_id].update(updates) - self.mapping_stats['mappings_updated'] += 1 - print(f"✓ 映射已更新: ID {mapping_id}") - return True - else: - print(f"✗ 映射不存在: ID {mapping_id}") - return False - - except Exception as e: - print(f"✗ 映射更新失败: {e}") - return False - - def enable_mapping(self, mapping_id: int, enabled: bool = True) -> bool: - """ - 启用/禁用映射 - - Args: - mapping_id: 映射ID - enabled: 是否启用 - - Returns: - 是否操作成功 - """ - try: - if mapping_id in self.mappings: - self.mappings[mapping_id]['enabled'] = enabled - status = "启用" if enabled else "禁用" - print(f"✓ 映射已{status}: ID {mapping_id}") - return True - else: - print(f"✗ 映射不存在: ID {mapping_id}") - return False - - except Exception as e: - print(f"✗ 映射启用/禁用失败: {e}") - return False - - def load_mappings_from_file(self, file_path: str) -> bool: - """ - 从文件加载映射配置 - - Args: - file_path: 文件路径 - - Returns: - 是否加载成功 - """ - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - - mappings = data.get('mappings', []) - actions = data.get('actions', []) - - # 加载动作定义 - for action_data in actions: - self.create_action( - name=action_data['name'], - description=action_data.get('description', ''), - parameters=action_data.get('parameters', {}) - ) - - # 加载映射配置 - for mapping_data in mappings: - self.create_mapping( - name=mapping_data['name'], - device_type=mapping_data['device_type'], - input_id=mapping_data['input_id'], - action_name=mapping_data['action_name'], - mapping_type=MappingType(mapping_data['mapping_type']), - action_type=ActionType(mapping_data['action_type']), - modifiers=mapping_data.get('modifiers', []), - parameters=mapping_data.get('parameters', {}) - ) - - print(f"✓ 映射配置已从文件加载: {file_path}") - return True - - except Exception as e: - print(f"✗ 映射配置加载失败: {e}") - return False - - def save_mappings_to_file(self, file_path: str) -> bool: - """ - 保存映射配置到文件 - - Args: - file_path: 文件路径 - - Returns: - 是否保存成功 - """ - try: - data = { - 'mappings': list(self.mappings.values()), - 'actions': list(self.actions.values()) - } - - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(data, f, ensure_ascii=False, indent=2) - - print(f"✓ 映射配置已保存到文件: {file_path}") - return True - - except Exception as e: - print(f"✗ 映射配置保存失败: {e}") - return False - - def set_mapping_config(self, config: Dict[str, Any]): - """ - 设置映射配置 - - Args: - config: 映射配置字典 - """ - try: - self.mapping_config.update(config) - print(f"✓ 映射配置已更新: {self.mapping_config}") - except Exception as e: - print(f"✗ 映射配置设置失败: {e}") - - def get_mapping_config(self) -> Dict[str, Any]: - """ - 获取映射配置 - - Returns: - 映射配置字典 - """ - return self.mapping_config.copy() - - def get_mapping_stats(self) -> Dict[str, int]: - """ - 获取映射统计信息 - - Returns: - 映射统计字典 - """ - return self.mapping_stats.copy() - - def reset_mapping_stats(self): - """重置映射统计信息""" - try: - self.mapping_stats = { - 'mappings_loaded': len(self.mappings), - 'actions_triggered': 0, - 'combinations_detected': 0, - 'sequences_detected': 0, - 'mappings_updated': 0 - } - print("✓ 映射统计信息已重置") - except Exception as e: - print(f"✗ 映射统计信息重置失败: {e}") - - def get_action_state(self, action_name: str) -> Dict[str, Any]: - """ - 获取动作状态 - - Args: - action_name: 动作名称 - - Returns: - 动作状态字典 - """ +""" +输入映射器模块 +负责将物理输入映射到逻辑动作 +""" + +import time +from typing import Dict, Any, List, Optional, Callable, Union +from enum import Enum +import json + +class MappingType(Enum): + """映射类型枚举""" + BUTTON = "button" + AXIS = "axis" + GESTURE = "gesture" + COMBINATION = "combination" + SEQUENCE = "sequence" + +class ActionType(Enum): + """动作类型枚举""" + SIMPLE = "simple" + CONTINUOUS = "continuous" + TOGGLE = "toggle" + HOLD = "hold" + +class InputMapper: + """ + 输入映射器 + 负责将物理输入映射到逻辑动作 + """ + + def __init__(self, plugin): + """ + 初始化输入映射器 + + Args: + plugin: 多平台输入支持插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 映射配置 + self.mappings = {} + self.mapping_counter = 0 + + # 动作定义 + self.actions = {} + self.action_counter = 0 + + # 映射配置 + self.mapping_config = { + 'enable_mapping': True, + 'enable_combinations': True, + 'enable_sequences': True, + 'combination_timeout': 0.5, # 组合键超时时间(秒) + 'sequence_timeout': 1.0, # 序列键超时时间(秒) + 'double_tap_timeout': 0.3, # 双击超时时间(秒) + 'hold_threshold': 0.5 # 按住阈值时间(秒) + } + + # 当前激活的输入 + self.active_inputs = {} + self.active_combinations = {} + self.active_sequences = {} + + # 输入历史(用于序列检测) + self.input_history = [] + + # 动作状态 + self.action_states = {} + + # 回调函数 + self.action_callbacks = {} + + # 统计信息 + self.mapping_stats = { + 'mappings_loaded': 0, + 'actions_triggered': 0, + 'combinations_detected': 0, + 'sequences_detected': 0, + 'mappings_updated': 0 + } + + # 时间戳记录 + self.last_input_time = {} + self.action_start_time = {} + + print("✓ 输入映射器已创建") + + def initialize(self) -> bool: + """ + 初始化输入映射器 + + Returns: + 是否初始化成功 + """ + try: + # 创建默认映射 + self._create_default_mappings() + + self.initialized = True + print("✓ 输入映射器初始化完成") + return True + + except Exception as e: + print(f"✗ 输入映射器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用输入映射器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 输入映射器未初始化") + return False + + self.enabled = True + print("✓ 输入映射器已启用") + return True + + except Exception as e: + print(f"✗ 输入映射器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用输入映射器""" + try: + self.enabled = False + print("✓ 输入映射器已禁用") + + except Exception as e: + print(f"✗ 输入映射器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理输入映射器资源""" + try: + self.disable() + self.mappings.clear() + self.actions.clear() + self.active_inputs.clear() + self.active_combinations.clear() + self.active_sequences.clear() + self.input_history.clear() + self.action_states.clear() + self.action_callbacks.clear() + self.initialized = False + print("✓ 输入映射器资源已清理") + + except Exception as e: + print(f"✗ 输入映射器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新输入映射器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 检查按住动作 + self._check_hold_actions() + + # 检查组合键超时 + self._check_combination_timeouts() + + # 检查序列超时 + self._check_sequence_timeouts() + + except Exception as e: + print(f"✗ 输入映射器更新失败: {e}") + import traceback + traceback.print_exc() + + def _create_default_mappings(self): + """创建默认映射""" + try: + # 键盘默认映射 + self.create_mapping( + name="前进", + device_type="keyboard", + input_id="w", + action_name="move_forward", + mapping_type=MappingType.BUTTON, + action_type=ActionType.CONTINUOUS + ) + + self.create_mapping( + name="后退", + device_type="keyboard", + input_id="s", + action_name="move_backward", + mapping_type=MappingType.BUTTON, + action_type=ActionType.CONTINUOUS + ) + + self.create_mapping( + name="左转", + device_type="keyboard", + input_id="a", + action_name="turn_left", + mapping_type=MappingType.BUTTON, + action_type=ActionType.CONTINUOUS + ) + + self.create_mapping( + name="右转", + device_type="keyboard", + input_id="d", + action_name="turn_right", + mapping_type=MappingType.BUTTON, + action_type=ActionType.CONTINUOUS + ) + + self.create_mapping( + name="跳跃", + device_type="keyboard", + input_id="space", + action_name="jump", + mapping_type=MappingType.BUTTON, + action_type=ActionType.SIMPLE + ) + + self.create_mapping( + name="蹲下", + device_type="keyboard", + input_id="c", + action_name="crouch", + mapping_type=MappingType.BUTTON, + action_type=ActionType.TOGGLE + ) + + # 鼠标默认映射 + self.create_mapping( + name="查看", + device_type="mouse", + input_id="movement", + action_name="look", + mapping_type=MappingType.AXIS, + action_type=ActionType.CONTINUOUS + ) + + self.create_mapping( + name="射击", + device_type="mouse", + input_id="button_1", # 左键 + action_name="shoot", + mapping_type=MappingType.BUTTON, + action_type=ActionType.SIMPLE + ) + + # 手柄默认映射 + self.create_mapping( + name="移动", + device_type="gamepad", + input_id="left_stick", + action_name="move", + mapping_type=MappingType.AXIS, + action_type=ActionType.CONTINUOUS + ) + + self.create_mapping( + name="视角", + device_type="gamepad", + input_id="right_stick", + action_name="look", + mapping_type=MappingType.AXIS, + action_type=ActionType.CONTINUOUS + ) + + print("✓ 默认映射创建完成") + + except Exception as e: + print(f"✗ 默认映射创建失败: {e}") + + def _check_hold_actions(self): + """检查按住动作""" + try: + current_time = time.time() + + for action_name, state in self.action_states.items(): + if state.get('active', False) and state.get('type') == ActionType.HOLD: + start_time = self.action_start_time.get(action_name, current_time) + hold_duration = current_time - start_time + + if hold_duration >= self.mapping_config['hold_threshold']: + # 触发按住动作 + self._trigger_action(action_name, { + 'type': 'hold', + 'duration': hold_duration + }) + + except Exception as e: + print(f"✗ 按住动作检查失败: {e}") + + def _check_combination_timeouts(self): + """检查组合键超时""" + try: + current_time = time.time() + + expired_combinations = [] + for combo_key, combo_data in self.active_combinations.items(): + if current_time - combo_data['timestamp'] > self.mapping_config['combination_timeout']: + expired_combinations.append(combo_key) + + for combo_key in expired_combinations: + del self.active_combinations[combo_key] + + except Exception as e: + print(f"✗ 组合键超时检查失败: {e}") + + def _check_sequence_timeouts(self): + """检查序列超时""" + try: + current_time = time.time() + + if self.input_history: + last_input_time = self.input_history[-1]['timestamp'] + if current_time - last_input_time > self.mapping_config['sequence_timeout']: + self.input_history.clear() + + except Exception as e: + print(f"✗ 序列超时检查失败: {e}") + + def create_mapping(self, name: str, device_type: str, input_id: str, + action_name: str, mapping_type: MappingType, + action_type: ActionType, modifiers: List[str] = None, + parameters: Dict[str, Any] = None) -> int: + """ + 创建输入映射 + + Args: + name: 映射名称 + device_type: 设备类型 + input_id: 输入ID + action_name: 动作名称 + mapping_type: 映射类型 + action_type: 动作类型 + modifiers: 修饰键列表 + parameters: 参数字典 + + Returns: + 映射ID + """ + try: + mapping_id = self.mapping_counter + self.mapping_counter += 1 + + mapping = { + 'id': mapping_id, + 'name': name, + 'device_type': device_type, + 'input_id': input_id, + 'action_name': action_name, + 'mapping_type': mapping_type, + 'action_type': action_type, + 'modifiers': modifiers or [], + 'parameters': parameters or {}, + 'enabled': True, + 'created_time': time.time() + } + + self.mappings[mapping_id] = mapping + self.mapping_stats['mappings_loaded'] += 1 + + print(f"✓ 输入映射已创建: {name} (ID: {mapping_id})") + return mapping_id + + except Exception as e: + print(f"✗ 输入映射创建失败: {e}") + return -1 + + def remove_mapping(self, mapping_id: int) -> bool: + """ + 移除输入映射 + + Args: + mapping_id: 映射ID + + Returns: + 是否移除成功 + """ + try: + if mapping_id in self.mappings: + mapping_name = self.mappings[mapping_id]['name'] + del self.mappings[mapping_id] + print(f"✓ 输入映射已移除: {mapping_name} (ID: {mapping_id})") + return True + else: + print(f"✗ 输入映射不存在: ID {mapping_id}") + return False + + except Exception as e: + print(f"✗ 输入映射移除失败: {e}") + return False + + def create_action(self, name: str, description: str = "", + parameters: Dict[str, Any] = None) -> int: + """ + 创建动作定义 + + Args: + name: 动作名称 + description: 动作描述 + parameters: 参数字典 + + Returns: + 动作ID + """ + try: + action_id = self.action_counter + self.action_counter += 1 + + action = { + 'id': action_id, + 'name': name, + 'description': description, + 'parameters': parameters or {}, + 'created_time': time.time() + } + + self.actions[action_id] = action + + print(f"✓ 动作定义已创建: {name} (ID: {action_id})") + return action_id + + except Exception as e: + print(f"✗ 动作定义创建失败: {e}") + return -1 + + def remove_action(self, action_id: int) -> bool: + """ + 移除动作定义 + + Args: + action_id: 动作ID + + Returns: + 是否移除成功 + """ + try: + if action_id in self.actions: + action_name = self.actions[action_id]['name'] + del self.actions[action_id] + print(f"✓ 动作定义已移除: {action_name} (ID: {action_id})") + return True + else: + print(f"✗ 动作定义不存在: ID {action_id}") + return False + + except Exception as e: + print(f"✗ 动作定义移除失败: {e}") + return False + + def handle_input_event(self, device_id: int, device_type: str, input_id: str, + value: Any, timestamp: float = None): + """ + 处理输入事件 + + Args: + device_id: 设备ID + device_type: 设备类型 + input_id: 输入ID + value: 输入值 + timestamp: 时间戳 + """ + try: + if not self.enabled or not self.mapping_config['enable_mapping']: + return + + if timestamp is None: + timestamp = time.time() + + # 记录输入历史 + self.input_history.append({ + 'device_id': device_id, + 'device_type': device_type, + 'input_id': input_id, + 'value': value, + 'timestamp': timestamp + }) + + # 保持历史记录大小 + if len(self.input_history) > 10: + self.input_history.pop(0) + + # 查找匹配的映射 + matched_mappings = self._find_matching_mappings(device_type, input_id, value) + + # 处理匹配的映射 + for mapping in matched_mappings: + self._process_mapping(mapping, device_id, value, timestamp) + + except Exception as e: + print(f"✗ 输入事件处理失败: {e}") + + def _find_matching_mappings(self, device_type: str, input_id: str, value: Any) -> List[Dict[str, Any]]: + """ + 查找匹配的映射 + + Args: + device_type: 设备类型 + input_id: 输入ID + value: 输入值 + + Returns: + 匹配的映射列表 + """ + try: + matched = [] + + for mapping in self.mappings.values(): + if not mapping['enabled']: + continue + + if (mapping['device_type'] == device_type and + mapping['input_id'] == input_id): + matched.append(mapping) + + return matched + except Exception as e: + print(f"✗ 匹配映射查找失败: {e}") + return [] + + def _process_mapping(self, mapping: Dict[str, Any], device_id: int, + value: Any, timestamp: float): + """ + 处理映射 + + Args: + mapping: 映射配置 + device_id: 设备ID + value: 输入值 + timestamp: 时间戳 + """ + try: + action_name = mapping['action_name'] + action_type = mapping['action_type'] + + # 根据动作类型处理 + if action_type == ActionType.SIMPLE: + # 简单动作:按下时触发 + if abs(value) > 0.1: # 超过死区 + self._trigger_action(action_name, { + 'device_id': device_id, + 'value': value, + 'timestamp': timestamp + }) + + elif action_type == ActionType.CONTINUOUS: + # 连续动作:根据值持续触发 + self._trigger_action(action_name, { + 'device_id': device_id, + 'value': value, + 'timestamp': timestamp + }) + + elif action_type == ActionType.TOGGLE: + # 切换动作:按下时切换状态 + if abs(value) > 0.1: + current_state = self.action_states.get(action_name, {}).get('active', False) + new_state = not current_state + self.action_states[action_name] = { + 'active': new_state, + 'type': ActionType.TOGGLE + } + + self._trigger_action(action_name, { + 'device_id': device_id, + 'value': new_state, + 'timestamp': timestamp + }) + + elif action_type == ActionType.HOLD: + # 按住动作:需要按住一定时间 + if abs(value) > 0.1: + # 按下 + self.action_states[action_name] = { + 'active': True, + 'type': ActionType.HOLD + } + self.action_start_time[action_name] = timestamp + else: + # 释放 + if action_name in self.action_states: + del self.action_states[action_name] + if action_name in self.action_start_time: + del self.action_start_time[action_name] + + except Exception as e: + print(f"✗ 映射处理失败: {e}") + + def _trigger_action(self, action_name: str, action_data: Dict[str, Any]): + """ + 触发动作 + + Args: + action_name: 动作名称 + action_data: 动作数据 + """ + try: + # 调用动作回调 + if action_name in self.action_callbacks: + callback = self.action_callbacks[action_name] + try: + callback(action_data) + self.mapping_stats['actions_triggered'] += 1 + except Exception as e: + print(f"✗ 动作回调执行失败: {e}") + else: + print(f"✗ 未找到动作回调: {action_name}") + + except Exception as e: + print(f"✗ 动作触发失败: {e}") + + def register_action_callback(self, action_name: str, callback: Callable): + """ + 注册动作回调 + + Args: + action_name: 动作名称 + callback: 回调函数 + """ + try: + self.action_callbacks[action_name] = callback + print(f"✓ 动作回调已注册: {action_name}") + except Exception as e: + print(f"✗ 动作回调注册失败: {e}") + + def unregister_action_callback(self, action_name: str): + """ + 注销动作回调 + + Args: + action_name: 动作名称 + """ + try: + if action_name in self.action_callbacks: + del self.action_callbacks[action_name] + print(f"✓ 动作回调已注销: {action_name}") + except Exception as e: + print(f"✗ 动作回调注销失败: {e}") + + def get_mapping(self, mapping_id: int) -> Optional[Dict[str, Any]]: + """ + 获取映射配置 + + Args: + mapping_id: 映射ID + + Returns: + 映射配置字典或None + """ + return self.mappings.get(mapping_id) + + def get_all_mappings(self) -> Dict[int, Dict[str, Any]]: + """ + 获取所有映射配置 + + Returns: + 所有映射配置字典 + """ + return self.mappings.copy() + + def get_action(self, action_id: int) -> Optional[Dict[str, Any]]: + """ + 获取动作定义 + + Args: + action_id: 动作ID + + Returns: + 动作定义字典或None + """ + return self.actions.get(action_id) + + def get_all_actions(self) -> Dict[int, Dict[str, Any]]: + """ + 获取所有动作定义 + + Returns: + 所有动作定义字典 + """ + return self.actions.copy() + + def update_mapping(self, mapping_id: int, updates: Dict[str, Any]) -> bool: + """ + 更新映射配置 + + Args: + mapping_id: 映射ID + updates: 更新字典 + + Returns: + 是否更新成功 + """ + try: + if mapping_id in self.mappings: + self.mappings[mapping_id].update(updates) + self.mapping_stats['mappings_updated'] += 1 + print(f"✓ 映射已更新: ID {mapping_id}") + return True + else: + print(f"✗ 映射不存在: ID {mapping_id}") + return False + + except Exception as e: + print(f"✗ 映射更新失败: {e}") + return False + + def enable_mapping(self, mapping_id: int, enabled: bool = True) -> bool: + """ + 启用/禁用映射 + + Args: + mapping_id: 映射ID + enabled: 是否启用 + + Returns: + 是否操作成功 + """ + try: + if mapping_id in self.mappings: + self.mappings[mapping_id]['enabled'] = enabled + status = "启用" if enabled else "禁用" + print(f"✓ 映射已{status}: ID {mapping_id}") + return True + else: + print(f"✗ 映射不存在: ID {mapping_id}") + return False + + except Exception as e: + print(f"✗ 映射启用/禁用失败: {e}") + return False + + def load_mappings_from_file(self, file_path: str) -> bool: + """ + 从文件加载映射配置 + + Args: + file_path: 文件路径 + + Returns: + 是否加载成功 + """ + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + mappings = data.get('mappings', []) + actions = data.get('actions', []) + + # 加载动作定义 + for action_data in actions: + self.create_action( + name=action_data['name'], + description=action_data.get('description', ''), + parameters=action_data.get('parameters', {}) + ) + + # 加载映射配置 + for mapping_data in mappings: + self.create_mapping( + name=mapping_data['name'], + device_type=mapping_data['device_type'], + input_id=mapping_data['input_id'], + action_name=mapping_data['action_name'], + mapping_type=MappingType(mapping_data['mapping_type']), + action_type=ActionType(mapping_data['action_type']), + modifiers=mapping_data.get('modifiers', []), + parameters=mapping_data.get('parameters', {}) + ) + + print(f"✓ 映射配置已从文件加载: {file_path}") + return True + + except Exception as e: + print(f"✗ 映射配置加载失败: {e}") + return False + + def save_mappings_to_file(self, file_path: str) -> bool: + """ + 保存映射配置到文件 + + Args: + file_path: 文件路径 + + Returns: + 是否保存成功 + """ + try: + data = { + 'mappings': list(self.mappings.values()), + 'actions': list(self.actions.values()) + } + + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + print(f"✓ 映射配置已保存到文件: {file_path}") + return True + + except Exception as e: + print(f"✗ 映射配置保存失败: {e}") + return False + + def set_mapping_config(self, config: Dict[str, Any]): + """ + 设置映射配置 + + Args: + config: 映射配置字典 + """ + try: + self.mapping_config.update(config) + print(f"✓ 映射配置已更新: {self.mapping_config}") + except Exception as e: + print(f"✗ 映射配置设置失败: {e}") + + def get_mapping_config(self) -> Dict[str, Any]: + """ + 获取映射配置 + + Returns: + 映射配置字典 + """ + return self.mapping_config.copy() + + def get_mapping_stats(self) -> Dict[str, int]: + """ + 获取映射统计信息 + + Returns: + 映射统计字典 + """ + return self.mapping_stats.copy() + + def reset_mapping_stats(self): + """重置映射统计信息""" + try: + self.mapping_stats = { + 'mappings_loaded': len(self.mappings), + 'actions_triggered': 0, + 'combinations_detected': 0, + 'sequences_detected': 0, + 'mappings_updated': 0 + } + print("✓ 映射统计信息已重置") + except Exception as e: + print(f"✗ 映射统计信息重置失败: {e}") + + def get_action_state(self, action_name: str) -> Dict[str, Any]: + """ + 获取动作状态 + + Args: + action_name: 动作名称 + + Returns: + 动作状态字典 + """ return self.action_states.get(action_name, {}).copy() \ No newline at end of file diff --git a/plugins/user/multiplatform_input/platforms/platform_adapter.py b/plugins/user/multiplatform_input/platforms/platform_adapter.py index b3f34d90..81b7724e 100644 --- a/plugins/user/multiplatform_input/platforms/platform_adapter.py +++ b/plugins/user/multiplatform_input/platforms/platform_adapter.py @@ -1,639 +1,639 @@ -""" -平台适配器模块 -负责处理不同平台的输入设备兼容性 -""" - -import platform -import time -from typing import Dict, Any, List, Optional -import threading - -class PlatformAdapter: - """ - 平台适配器 - 负责处理不同平台的输入设备兼容性 - """ - - def __init__(self, plugin): - """ - 初始化平台适配器 - - Args: - plugin: 多平台输入支持插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 平台信息 - self.platform_info = { - 'system': platform.system(), - 'release': platform.release(), - 'version': platform.version(), - 'machine': platform.machine(), - 'processor': platform.processor(), - 'architecture': platform.architecture()[0] - } - - # 平台配置 - self.platform_config = { - 'Windows': { - 'api_priority': ['directinput', 'xinput', 'rawinput'], - 'default_deadzone': 0.1, - 'max_devices': 16, - 'polling_rate': 120, - 'supported_features': ['xinput', 'directinput', 'hid'] - }, - 'Linux': { - 'api_priority': ['evdev', 'x11', 'udev'], - 'default_deadzone': 0.15, - 'max_devices': 32, - 'polling_rate': 100, - 'supported_features': ['evdev', 'udev', 'x11'] - }, - 'Darwin': { # macOS - 'api_priority': ['iokit', 'coregraphics', 'hid'], - 'default_deadzone': 0.1, - 'max_devices': 16, - 'polling_rate': 120, - 'supported_features': ['iokit', 'coregraphics', 'hid'] - } - } - - # 当前平台配置 - self.current_platform_config = self.platform_config.get( - self.platform_info['system'], - { - 'api_priority': ['generic'], - 'default_deadzone': 0.1, - 'max_devices': 8, - 'polling_rate': 60, - 'supported_features': ['generic'] - } - ) - - # 设备适配器 - self.device_adapters = {} - - # 平台特定功能 - self.platform_features = { - 'has_vibration': False, - 'has_motion': False, - 'has_touch': False, - 'has_gesture': False, - 'max_touch_points': 0 - } - - # API支持状态 - self.api_support = {} - - # 统计信息 - self.adapter_stats = { - 'devices_adapted': 0, - 'api_loaded': 0, - 'api_errors': 0, - 'compatibility_fixes': 0, - 'data_translated': 0 - } - - # 线程管理 - self.adapter_thread = None - self.running = False - - # 时间戳记录 - self.last_api_check = 0.0 - self.last_compatibility_check = 0.0 - - print(f"✓ 平台适配器已创建 ({self.platform_info['system']})") - - def initialize(self) -> bool: - """ - 初始化平台适配器 - - Returns: - 是否初始化成功 - """ - try: - # 检测平台特定功能 - self._detect_platform_features() - - # 加载平台API - self._load_platform_apis() - - # 创建设备适配器 - self._create_device_adapters() - - self.initialized = True - print("✓ 平台适配器初始化完成") - return True - - except Exception as e: - print(f"✗ 平台适配器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用平台适配器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 平台适配器未初始化") - return False - - self.enabled = True - self.running = True - - # 启动适配器线程 - self.adapter_thread = threading.Thread(target=self._adapter_update_loop, daemon=True) - self.adapter_thread.start() - - print("✓ 平台适配器已启用") - return True - - except Exception as e: - print(f"✗ 平台适配器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用平台适配器""" - try: - self.enabled = False - self.running = False - - # 等待适配器线程结束 - if self.adapter_thread and self.adapter_thread.is_alive(): - self.adapter_thread.join(timeout=2.0) - - # 清理平台API - self._cleanup_platform_apis() - - print("✓ 平台适配器已禁用") - - except Exception as e: - print(f"✗ 平台适配器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理平台适配器资源""" - try: - self.disable() - self.device_adapters.clear() - self.api_support.clear() - self.initialized = False - print("✓ 平台适配器资源已清理") - - except Exception as e: - print(f"✗ 平台适配器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新平台适配器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 定期检查API支持状态 - self._check_api_support() - - except Exception as e: - print(f"✗ 平台适配器更新失败: {e}") - import traceback - traceback.print_exc() - - def _adapter_update_loop(self): - """适配器更新循环""" - try: - # 更新间隔基于平台轮询率 - update_interval = 1.0 / self.current_platform_config['polling_rate'] - - while self.running: - if self.enabled: - self._update_device_adapters() - - time.sleep(update_interval) - - except Exception as e: - print(f"✗ 适配器更新循环失败: {e}") - - def _detect_platform_features(self): - """检测平台特定功能""" - try: - system = self.platform_info['system'] - - if system == 'Windows': - self.platform_features['has_vibration'] = True - self.platform_features['has_motion'] = True - self.platform_features['has_touch'] = True - self.platform_features['max_touch_points'] = 10 - - elif system == 'Linux': - self.platform_features['has_vibration'] = True - self.platform_features['has_motion'] = True - self.platform_features['has_touch'] = True - self.platform_features['max_touch_points'] = 10 - - elif system == 'Darwin': # macOS - self.platform_features['has_vibration'] = True - self.platform_features['has_motion'] = True - self.platform_features['has_touch'] = True - self.platform_features['max_touch_points'] = 5 - - print(f"✓ 平台功能检测完成: {self.platform_features}") - - except Exception as e: - print(f"✗ 平台功能检测失败: {e}") - - def _load_platform_apis(self): - """加载平台API""" - try: - system = self.platform_info['system'] - api_priority = self.current_platform_config['api_priority'] - - for api_name in api_priority: - try: - if self._load_api(api_name): - self.api_support[api_name] = True - self.adapter_stats['api_loaded'] += 1 - print(f"✓ 平台API已加载: {api_name}") - else: - self.api_support[api_name] = False - except Exception as e: - self.api_support[api_name] = False - self.adapter_stats['api_errors'] += 1 - print(f"✗ 平台API加载失败: {api_name} - {e}") - - except Exception as e: - print(f"✗ 平台API加载失败: {e}") - - def _load_api(self, api_name: str) -> bool: - """ - 加载特定API - - Args: - api_name: API名称 - - Returns: - 是否加载成功 - """ - try: - system = self.platform_info['system'] - - # 这里应该实现实际的API加载逻辑 - # 由于是模拟实现,我们只是记录支持状态 - - if system == 'Windows' and api_name in ['directinput', 'xinput', 'rawinput']: - return True - elif system == 'Linux' and api_name in ['evdev', 'x11', 'udev']: - return True - elif system == 'Darwin' and api_name in ['iokit', 'coregraphics', 'hid']: - return True - else: - return False - - except Exception as e: - print(f"✗ API加载失败: {api_name} - {e}") - return False - - def _cleanup_platform_apis(self): - """清理平台API""" - try: - # 这里应该实现实际的API清理逻辑 - self.api_support.clear() - print("✓ 平台API已清理") - except Exception as e: - print(f"✗ 平台API清理失败: {e}") - - def _create_device_adapters(self): - """创建设备适配器""" - try: - # 这里应该为不同类型的设备创建适配器 - # 由于是模拟实现,我们只是初始化适配器字典 - self.device_adapters = { - 'keyboard': None, - 'mouse': None, - 'gamepad': None, - 'touch': None, - 'vr_controller': None, - 'motion_sensor': None - } - - print("✓ 设备适配器创建完成") - except Exception as e: - print(f"✗ 设备适配器创建失败: {e}") - - def _update_device_adapters(self): - """更新设备适配器""" - try: - # 这里应该实现设备适配器的更新逻辑 - pass - except Exception as e: - print(f"✗ 设备适配器更新失败: {e}") - - def _check_api_support(self): - """检查API支持状态""" - try: - current_time = time.time() - - # 每30秒检查一次API支持 - if current_time - self.last_api_check >= 30.0: - for api_name in list(self.api_support.keys()): - # 重新检查API支持状态 - try: - # 这里应该实现实际的API支持检查逻辑 - pass - except Exception: - pass - - self.last_api_check = current_time - - except Exception as e: - print(f"✗ API支持检查失败: {e}") - - def adapt_device_data(self, device_type: str, raw_data: Dict[str, Any]) -> Dict[str, Any]: - """ - 适配设备数据 - - Args: - device_type: 设备类型 - raw_data: 原始设备数据 - - Returns: - 适配后的设备数据 - """ - try: - # 根据设备类型和平台进行数据适配 - adapted_data = raw_data.copy() - - # 应用死区处理 - deadzone = self.current_platform_config['default_deadzone'] - if 'axes' in adapted_data: - for axis_name, axis_value in adapted_data['axes'].items(): - if abs(axis_value) < deadzone: - adapted_data['axes'][axis_name] = 0.0 - - # 应用平台特定的转换 - system = self.platform_info['system'] - if system == 'Windows': - adapted_data = self._adapt_windows_data(device_type, adapted_data) - elif system == 'Linux': - adapted_data = self._adapt_linux_data(device_type, adapted_data) - elif system == 'Darwin': - adapted_data = self._adapt_macos_data(device_type, adapted_data) - - self.adapter_stats['data_translated'] += 1 - return adapted_data - - except Exception as e: - print(f"✗ 设备数据适配失败: {e}") - return raw_data - - def _adapt_windows_data(self, device_type: str, data: Dict[str, Any]) -> Dict[str, Any]: - """ - 适配Windows平台数据 - - Args: - device_type: 设备类型 - data: 设备数据 - - Returns: - 适配后的数据 - """ - try: - # Windows平台特定的数据适配 - adapted_data = data.copy() - - # 例如:调整坐标系、按键映射等 - if device_type == 'mouse': - # Windows鼠标坐标可能需要调整 - pass - elif device_type == 'gamepad': - # XInput控制器数据适配 - pass - - return adapted_data - except Exception as e: - print(f"✗ Windows数据适配失败: {e}") - return data - - def _adapt_linux_data(self, device_type: str, data: Dict[str, Any]) -> Dict[str, Any]: - """ - 适配Linux平台数据 - - Args: - device_type: 设备类型 - data: 设备数据 - - Returns: - 适配后的数据 - """ - try: - # Linux平台特定的数据适配 - adapted_data = data.copy() - - # 例如:evdev事件处理、设备路径解析等 - return adapted_data - except Exception as e: - print(f"✗ Linux数据适配失败: {e}") - return data - - def _adapt_macos_data(self, device_type: str, data: Dict[str, Any]) -> Dict[str, Any]: - """ - 适配macOS平台数据 - - Args: - device_type: 设备类型 - data: 设备数据 - - Returns: - 适配后的数据 - """ - try: - # macOS平台特定的数据适配 - adapted_data = data.copy() - - # 例如:CoreGraphics事件处理、HID设备解析等 - return adapted_data - except Exception as e: - print(f"✗ macOS数据适配失败: {e}") - return data - - def get_platform_info(self) -> Dict[str, Any]: - """ - 获取平台信息 - - Returns: - 平台信息字典 - """ - return self.platform_info.copy() - - def get_platform_config(self) -> Dict[str, Any]: - """ - 获取当前平台配置 - - Returns: - 平台配置字典 - """ - return self.current_platform_config.copy() - - def get_platform_features(self) -> Dict[str, Any]: - """ - 获取平台功能 - - Returns: - 平台功能字典 - """ - return self.platform_features.copy() - - def is_api_supported(self, api_name: str) -> bool: - """ - 检查API是否支持 - - Args: - api_name: API名称 - - Returns: - 是否支持 - """ - return self.api_support.get(api_name, False) - - def get_supported_apis(self) -> List[str]: - """ - 获取支持的API列表 - - Returns: - 支持的API列表 - """ - return [api for api, supported in self.api_support.items() if supported] - - def apply_compatibility_fix(self, device_id: int, issue_type: str) -> bool: - """ - 应用兼容性修复 - - Args: - device_id: 设备ID - issue_type: 问题类型 - - Returns: - 是否应用成功 - """ - try: - # 这里应该实现具体的兼容性修复逻辑 - self.adapter_stats['compatibility_fixes'] += 1 - print(f"✓ 兼容性修复已应用: 设备 {device_id}, 问题 {issue_type}") - return True - except Exception as e: - print(f"✗ 兼容性修复应用失败: {e}") - return False - - def get_adapter_stats(self) -> Dict[str, int]: - """ - 获取适配器统计信息 - - Returns: - 适配器统计字典 - """ - return self.adapter_stats.copy() - - def reset_adapter_stats(self): - """重置适配器统计信息""" - try: - self.adapter_stats = { - 'devices_adapted': 0, - 'api_loaded': 0, - 'api_errors': 0, - 'compatibility_fixes': 0, - 'data_translated': 0 - } - print("✓ 适配器统计信息已重置") - except Exception as e: - print(f"✗ 适配器统计信息重置失败: {e}") - - def set_platform_config(self, config: Dict[str, Any]) -> bool: - """ - 设置平台配置 - - Args: - config: 平台配置字典 - - Returns: - 是否设置成功 - """ - try: - self.current_platform_config.update(config) - print(f"✓ 平台配置已更新: {self.current_platform_config}") - return True - except Exception as e: - print(f"✗ 平台配置设置失败: {e}") - return False - - def get_device_adapter(self, device_type: str) -> Optional[Any]: - """ - 获取设备适配器 - - Args: - device_type: 设备类型 - - Returns: - 设备适配器或None - """ - return self.device_adapters.get(device_type) - - def register_device_adapter(self, device_type: str, adapter: Any) -> bool: - """ - 注册设备适配器 - - Args: - device_type: 设备类型 - adapter: 设备适配器 - - Returns: - 是否注册成功 - """ - try: - self.device_adapters[device_type] = adapter - print(f"✓ 设备适配器已注册: {device_type}") - return True - except Exception as e: - print(f"✗ 设备适配器注册失败: {e}") - return False - - def get_system_architecture(self) -> str: - """ - 获取系统架构 - - Returns: - 系统架构字符串 - """ - return self.platform_info['architecture'] - - def is_64bit_system(self) -> bool: - """ - 检查是否为64位系统 - - Returns: - 是否为64位系统 - """ - return '64' in self.platform_info['architecture'] - - def get_system_processor(self) -> str: - """ - 获取系统处理器信息 - - Returns: - 处理器信息字符串 - """ +""" +平台适配器模块 +负责处理不同平台的输入设备兼容性 +""" + +import platform +import time +from typing import Dict, Any, List, Optional +import threading + +class PlatformAdapter: + """ + 平台适配器 + 负责处理不同平台的输入设备兼容性 + """ + + def __init__(self, plugin): + """ + 初始化平台适配器 + + Args: + plugin: 多平台输入支持插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 平台信息 + self.platform_info = { + 'system': platform.system(), + 'release': platform.release(), + 'version': platform.version(), + 'machine': platform.machine(), + 'processor': platform.processor(), + 'architecture': platform.architecture()[0] + } + + # 平台配置 + self.platform_config = { + 'Windows': { + 'api_priority': ['directinput', 'xinput', 'rawinput'], + 'default_deadzone': 0.1, + 'max_devices': 16, + 'polling_rate': 120, + 'supported_features': ['xinput', 'directinput', 'hid'] + }, + 'Linux': { + 'api_priority': ['evdev', 'x11', 'udev'], + 'default_deadzone': 0.15, + 'max_devices': 32, + 'polling_rate': 100, + 'supported_features': ['evdev', 'udev', 'x11'] + }, + 'Darwin': { # macOS + 'api_priority': ['iokit', 'coregraphics', 'hid'], + 'default_deadzone': 0.1, + 'max_devices': 16, + 'polling_rate': 120, + 'supported_features': ['iokit', 'coregraphics', 'hid'] + } + } + + # 当前平台配置 + self.current_platform_config = self.platform_config.get( + self.platform_info['system'], + { + 'api_priority': ['generic'], + 'default_deadzone': 0.1, + 'max_devices': 8, + 'polling_rate': 60, + 'supported_features': ['generic'] + } + ) + + # 设备适配器 + self.device_adapters = {} + + # 平台特定功能 + self.platform_features = { + 'has_vibration': False, + 'has_motion': False, + 'has_touch': False, + 'has_gesture': False, + 'max_touch_points': 0 + } + + # API支持状态 + self.api_support = {} + + # 统计信息 + self.adapter_stats = { + 'devices_adapted': 0, + 'api_loaded': 0, + 'api_errors': 0, + 'compatibility_fixes': 0, + 'data_translated': 0 + } + + # 线程管理 + self.adapter_thread = None + self.running = False + + # 时间戳记录 + self.last_api_check = 0.0 + self.last_compatibility_check = 0.0 + + print(f"✓ 平台适配器已创建 ({self.platform_info['system']})") + + def initialize(self) -> bool: + """ + 初始化平台适配器 + + Returns: + 是否初始化成功 + """ + try: + # 检测平台特定功能 + self._detect_platform_features() + + # 加载平台API + self._load_platform_apis() + + # 创建设备适配器 + self._create_device_adapters() + + self.initialized = True + print("✓ 平台适配器初始化完成") + return True + + except Exception as e: + print(f"✗ 平台适配器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用平台适配器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 平台适配器未初始化") + return False + + self.enabled = True + self.running = True + + # 启动适配器线程 + self.adapter_thread = threading.Thread(target=self._adapter_update_loop, daemon=True) + self.adapter_thread.start() + + print("✓ 平台适配器已启用") + return True + + except Exception as e: + print(f"✗ 平台适配器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用平台适配器""" + try: + self.enabled = False + self.running = False + + # 等待适配器线程结束 + if self.adapter_thread and self.adapter_thread.is_alive(): + self.adapter_thread.join(timeout=2.0) + + # 清理平台API + self._cleanup_platform_apis() + + print("✓ 平台适配器已禁用") + + except Exception as e: + print(f"✗ 平台适配器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理平台适配器资源""" + try: + self.disable() + self.device_adapters.clear() + self.api_support.clear() + self.initialized = False + print("✓ 平台适配器资源已清理") + + except Exception as e: + print(f"✗ 平台适配器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新平台适配器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 定期检查API支持状态 + self._check_api_support() + + except Exception as e: + print(f"✗ 平台适配器更新失败: {e}") + import traceback + traceback.print_exc() + + def _adapter_update_loop(self): + """适配器更新循环""" + try: + # 更新间隔基于平台轮询率 + update_interval = 1.0 / self.current_platform_config['polling_rate'] + + while self.running: + if self.enabled: + self._update_device_adapters() + + time.sleep(update_interval) + + except Exception as e: + print(f"✗ 适配器更新循环失败: {e}") + + def _detect_platform_features(self): + """检测平台特定功能""" + try: + system = self.platform_info['system'] + + if system == 'Windows': + self.platform_features['has_vibration'] = True + self.platform_features['has_motion'] = True + self.platform_features['has_touch'] = True + self.platform_features['max_touch_points'] = 10 + + elif system == 'Linux': + self.platform_features['has_vibration'] = True + self.platform_features['has_motion'] = True + self.platform_features['has_touch'] = True + self.platform_features['max_touch_points'] = 10 + + elif system == 'Darwin': # macOS + self.platform_features['has_vibration'] = True + self.platform_features['has_motion'] = True + self.platform_features['has_touch'] = True + self.platform_features['max_touch_points'] = 5 + + print(f"✓ 平台功能检测完成: {self.platform_features}") + + except Exception as e: + print(f"✗ 平台功能检测失败: {e}") + + def _load_platform_apis(self): + """加载平台API""" + try: + system = self.platform_info['system'] + api_priority = self.current_platform_config['api_priority'] + + for api_name in api_priority: + try: + if self._load_api(api_name): + self.api_support[api_name] = True + self.adapter_stats['api_loaded'] += 1 + print(f"✓ 平台API已加载: {api_name}") + else: + self.api_support[api_name] = False + except Exception as e: + self.api_support[api_name] = False + self.adapter_stats['api_errors'] += 1 + print(f"✗ 平台API加载失败: {api_name} - {e}") + + except Exception as e: + print(f"✗ 平台API加载失败: {e}") + + def _load_api(self, api_name: str) -> bool: + """ + 加载特定API + + Args: + api_name: API名称 + + Returns: + 是否加载成功 + """ + try: + system = self.platform_info['system'] + + # 这里应该实现实际的API加载逻辑 + # 由于是模拟实现,我们只是记录支持状态 + + if system == 'Windows' and api_name in ['directinput', 'xinput', 'rawinput']: + return True + elif system == 'Linux' and api_name in ['evdev', 'x11', 'udev']: + return True + elif system == 'Darwin' and api_name in ['iokit', 'coregraphics', 'hid']: + return True + else: + return False + + except Exception as e: + print(f"✗ API加载失败: {api_name} - {e}") + return False + + def _cleanup_platform_apis(self): + """清理平台API""" + try: + # 这里应该实现实际的API清理逻辑 + self.api_support.clear() + print("✓ 平台API已清理") + except Exception as e: + print(f"✗ 平台API清理失败: {e}") + + def _create_device_adapters(self): + """创建设备适配器""" + try: + # 这里应该为不同类型的设备创建适配器 + # 由于是模拟实现,我们只是初始化适配器字典 + self.device_adapters = { + 'keyboard': None, + 'mouse': None, + 'gamepad': None, + 'touch': None, + 'vr_controller': None, + 'motion_sensor': None + } + + print("✓ 设备适配器创建完成") + except Exception as e: + print(f"✗ 设备适配器创建失败: {e}") + + def _update_device_adapters(self): + """更新设备适配器""" + try: + # 这里应该实现设备适配器的更新逻辑 + pass + except Exception as e: + print(f"✗ 设备适配器更新失败: {e}") + + def _check_api_support(self): + """检查API支持状态""" + try: + current_time = time.time() + + # 每30秒检查一次API支持 + if current_time - self.last_api_check >= 30.0: + for api_name in list(self.api_support.keys()): + # 重新检查API支持状态 + try: + # 这里应该实现实际的API支持检查逻辑 + pass + except Exception: + pass + + self.last_api_check = current_time + + except Exception as e: + print(f"✗ API支持检查失败: {e}") + + def adapt_device_data(self, device_type: str, raw_data: Dict[str, Any]) -> Dict[str, Any]: + """ + 适配设备数据 + + Args: + device_type: 设备类型 + raw_data: 原始设备数据 + + Returns: + 适配后的设备数据 + """ + try: + # 根据设备类型和平台进行数据适配 + adapted_data = raw_data.copy() + + # 应用死区处理 + deadzone = self.current_platform_config['default_deadzone'] + if 'axes' in adapted_data: + for axis_name, axis_value in adapted_data['axes'].items(): + if abs(axis_value) < deadzone: + adapted_data['axes'][axis_name] = 0.0 + + # 应用平台特定的转换 + system = self.platform_info['system'] + if system == 'Windows': + adapted_data = self._adapt_windows_data(device_type, adapted_data) + elif system == 'Linux': + adapted_data = self._adapt_linux_data(device_type, adapted_data) + elif system == 'Darwin': + adapted_data = self._adapt_macos_data(device_type, adapted_data) + + self.adapter_stats['data_translated'] += 1 + return adapted_data + + except Exception as e: + print(f"✗ 设备数据适配失败: {e}") + return raw_data + + def _adapt_windows_data(self, device_type: str, data: Dict[str, Any]) -> Dict[str, Any]: + """ + 适配Windows平台数据 + + Args: + device_type: 设备类型 + data: 设备数据 + + Returns: + 适配后的数据 + """ + try: + # Windows平台特定的数据适配 + adapted_data = data.copy() + + # 例如:调整坐标系、按键映射等 + if device_type == 'mouse': + # Windows鼠标坐标可能需要调整 + pass + elif device_type == 'gamepad': + # XInput控制器数据适配 + pass + + return adapted_data + except Exception as e: + print(f"✗ Windows数据适配失败: {e}") + return data + + def _adapt_linux_data(self, device_type: str, data: Dict[str, Any]) -> Dict[str, Any]: + """ + 适配Linux平台数据 + + Args: + device_type: 设备类型 + data: 设备数据 + + Returns: + 适配后的数据 + """ + try: + # Linux平台特定的数据适配 + adapted_data = data.copy() + + # 例如:evdev事件处理、设备路径解析等 + return adapted_data + except Exception as e: + print(f"✗ Linux数据适配失败: {e}") + return data + + def _adapt_macos_data(self, device_type: str, data: Dict[str, Any]) -> Dict[str, Any]: + """ + 适配macOS平台数据 + + Args: + device_type: 设备类型 + data: 设备数据 + + Returns: + 适配后的数据 + """ + try: + # macOS平台特定的数据适配 + adapted_data = data.copy() + + # 例如:CoreGraphics事件处理、HID设备解析等 + return adapted_data + except Exception as e: + print(f"✗ macOS数据适配失败: {e}") + return data + + def get_platform_info(self) -> Dict[str, Any]: + """ + 获取平台信息 + + Returns: + 平台信息字典 + """ + return self.platform_info.copy() + + def get_platform_config(self) -> Dict[str, Any]: + """ + 获取当前平台配置 + + Returns: + 平台配置字典 + """ + return self.current_platform_config.copy() + + def get_platform_features(self) -> Dict[str, Any]: + """ + 获取平台功能 + + Returns: + 平台功能字典 + """ + return self.platform_features.copy() + + def is_api_supported(self, api_name: str) -> bool: + """ + 检查API是否支持 + + Args: + api_name: API名称 + + Returns: + 是否支持 + """ + return self.api_support.get(api_name, False) + + def get_supported_apis(self) -> List[str]: + """ + 获取支持的API列表 + + Returns: + 支持的API列表 + """ + return [api for api, supported in self.api_support.items() if supported] + + def apply_compatibility_fix(self, device_id: int, issue_type: str) -> bool: + """ + 应用兼容性修复 + + Args: + device_id: 设备ID + issue_type: 问题类型 + + Returns: + 是否应用成功 + """ + try: + # 这里应该实现具体的兼容性修复逻辑 + self.adapter_stats['compatibility_fixes'] += 1 + print(f"✓ 兼容性修复已应用: 设备 {device_id}, 问题 {issue_type}") + return True + except Exception as e: + print(f"✗ 兼容性修复应用失败: {e}") + return False + + def get_adapter_stats(self) -> Dict[str, int]: + """ + 获取适配器统计信息 + + Returns: + 适配器统计字典 + """ + return self.adapter_stats.copy() + + def reset_adapter_stats(self): + """重置适配器统计信息""" + try: + self.adapter_stats = { + 'devices_adapted': 0, + 'api_loaded': 0, + 'api_errors': 0, + 'compatibility_fixes': 0, + 'data_translated': 0 + } + print("✓ 适配器统计信息已重置") + except Exception as e: + print(f"✗ 适配器统计信息重置失败: {e}") + + def set_platform_config(self, config: Dict[str, Any]) -> bool: + """ + 设置平台配置 + + Args: + config: 平台配置字典 + + Returns: + 是否设置成功 + """ + try: + self.current_platform_config.update(config) + print(f"✓ 平台配置已更新: {self.current_platform_config}") + return True + except Exception as e: + print(f"✗ 平台配置设置失败: {e}") + return False + + def get_device_adapter(self, device_type: str) -> Optional[Any]: + """ + 获取设备适配器 + + Args: + device_type: 设备类型 + + Returns: + 设备适配器或None + """ + return self.device_adapters.get(device_type) + + def register_device_adapter(self, device_type: str, adapter: Any) -> bool: + """ + 注册设备适配器 + + Args: + device_type: 设备类型 + adapter: 设备适配器 + + Returns: + 是否注册成功 + """ + try: + self.device_adapters[device_type] = adapter + print(f"✓ 设备适配器已注册: {device_type}") + return True + except Exception as e: + print(f"✗ 设备适配器注册失败: {e}") + return False + + def get_system_architecture(self) -> str: + """ + 获取系统架构 + + Returns: + 系统架构字符串 + """ + return self.platform_info['architecture'] + + def is_64bit_system(self) -> bool: + """ + 检查是否为64位系统 + + Returns: + 是否为64位系统 + """ + return '64' in self.platform_info['architecture'] + + def get_system_processor(self) -> str: + """ + 获取系统处理器信息 + + Returns: + 处理器信息字符串 + """ return self.platform_info['processor'] \ No newline at end of file diff --git a/plugins/user/multiplatform_input/plugin.py b/plugins/user/multiplatform_input/plugin.py index ba3da6b5..3d62ab89 100644 --- a/plugins/user/multiplatform_input/plugin.py +++ b/plugins/user/multiplatform_input/plugin.py @@ -1,292 +1,292 @@ -""" -多平台输入支持插件 -为EG引擎提供统一的多平台输入设备支持 -""" - -from .core.input_manager import InputManager -from .core.advanced_input_processor import AdvancedInputProcessor -from .devices.device_manager import DeviceManager -from .devices.device_calibrator import DeviceCalibrator -from .events.event_system import EventSystem -from .mapping.input_mapper import InputMapper -from .gestures.gesture_recognizer import GestureRecognizer -from .haptics.haptic_manager import HapticManager -from .config.config_manager import ConfigManager -from .editor.input_editor import InputEditor -from .utils.input_utils import InputUtils -from .platforms.platform_adapter import PlatformAdapter - -class MultiPlatformInputPlugin: - """ - 多平台输入支持插件主类 - 整合所有输入设备支持功能模块 - """ - - def __init__(self, engine): - """ - 初始化多平台输入支持插件 - - Args: - engine: EG引擎实例 - """ - self.engine = engine - self.name = "MultiPlatformInput" - self.version = "1.0.0" - self.author = "EG Plugin System" - - # 功能模块 - self.input_manager = None - self.advanced_input_processor = None - self.device_manager = None - self.device_calibrator = None - self.event_system = None - self.input_mapper = None - self.gesture_recognizer = None - self.haptic_manager = None - self.config_manager = None - self.input_editor = None - self.input_utils = None - self.platform_adapter = None - - # 插件状态 - self.enabled = False - self.initialized = False - - # 性能统计 - self.stats = { - 'devices_connected': 0, - 'events_processed': 0, - 'gestures_recognized': 0, - 'haptic_effects': 0, - 'input_mappings': 0 - } - - print(f"✓ 多平台输入支持插件 v{self.version} 已创建") - - def initialize(self) -> bool: - """ - 初始化插件 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化多平台输入支持插件...") - - # 初始化核心组件 - self.input_manager = InputManager(self) - self.advanced_input_processor = AdvancedInputProcessor(self) - self.device_manager = DeviceManager(self) - self.device_calibrator = DeviceCalibrator(self) - self.event_system = EventSystem(self) - self.input_mapper = InputMapper(self) - self.gesture_recognizer = GestureRecognizer(self) - self.haptic_manager = HapticManager(self) - self.config_manager = ConfigManager(self) - self.input_editor = InputEditor(self) - self.input_utils = InputUtils(self) - self.platform_adapter = PlatformAdapter(self) - - # 初始化各个模块 - modules = [ - self.input_manager, - self.advanced_input_processor, - self.device_manager, - self.device_calibrator, - self.event_system, - self.input_mapper, - self.gesture_recognizer, - self.haptic_manager, - self.config_manager, - self.input_editor, - self.input_utils, - self.platform_adapter - ] - - for module in modules: - if module and not module.initialize(): - print(f"✗ 模块初始化失败: {module.__class__.__name__}") - return False - - self.initialized = True - print("✓ 多平台输入支持插件初始化完成") - return True - - except Exception as e: - print(f"✗ 多平台输入支持插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 插件未初始化") - return False - - print("正在启用多平台输入支持插件...") - - # 启用各个模块 - modules = [ - self.input_manager, - self.advanced_input_processor, - self.device_manager, - self.device_calibrator, - self.event_system, - self.input_mapper, - self.gesture_recognizer, - self.haptic_manager, - self.config_manager, - self.input_editor, - self.input_utils, - self.platform_adapter - ] - - for module in modules: - if module and not module.enable(): - print(f"✗ 模块启用失败: {module.__class__.__name__}") - return False - - self.enabled = True - print("✓ 多平台输入支持插件已启用") - return True - - except Exception as e: - print(f"✗ 多平台输入支持插件启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用插件""" - try: - if not self.enabled: - return - - print("正在禁用多平台输入支持插件...") - - # 禁用各个模块 - modules = [ - self.input_manager, - self.advanced_input_processor, - self.device_manager, - self.device_calibrator, - self.event_system, - self.input_mapper, - self.gesture_recognizer, - self.haptic_manager, - self.config_manager, - self.input_editor, - self.input_utils, - self.platform_adapter - ] - - for module in modules: - if module: - module.disable() - - self.enabled = False - print("✓ 多平台输入支持插件已禁用") - - except Exception as e: - print(f"✗ 多平台输入支持插件禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理插件资源""" - try: - print("正在清理多平台输入支持插件资源...") - - # 清理各个模块 - modules = [ - self.input_manager, - self.advanced_input_processor, - self.device_manager, - self.device_calibrator, - self.event_system, - self.input_mapper, - self.gesture_recognizer, - self.haptic_manager, - self.config_manager, - self.input_editor, - self.input_utils, - self.platform_adapter - ] - - for module in modules: - if module: - module.finalize() - - self.initialized = False - print("✓ 多平台输入支持插件资源已清理") - - except Exception as e: - print(f"✗ 多平台输入支持插件资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新插件状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - # 更新各个模块 - modules = [ - self.input_manager, - self.advanced_input_processor, - self.device_manager, - self.device_calibrator, - self.event_system, - self.input_mapper, - self.gesture_recognizer, - self.haptic_manager, - self.config_manager, - self.input_editor, - self.input_utils, - self.platform_adapter - ] - - for module in modules: - if module: - module.update(dt) - - # 更新统计信息 - self.stats['events_processed'] += 1 - - except Exception as e: - print(f"✗ 多平台输入支持插件更新失败: {e}") - import traceback - traceback.print_exc() - - def get_stats(self) -> dict: - """ - 获取插件统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'devices_connected': 0, - 'events_processed': 0, - 'gestures_recognized': 0, - 'haptic_effects': 0, - 'input_mappings': 0 - } +""" +多平台输入支持插件 +为EG引擎提供统一的多平台输入设备支持 +""" + +from .core.input_manager import InputManager +from .core.advanced_input_processor import AdvancedInputProcessor +from .devices.device_manager import DeviceManager +from .devices.device_calibrator import DeviceCalibrator +from .events.event_system import EventSystem +from .mapping.input_mapper import InputMapper +from .gestures.gesture_recognizer import GestureRecognizer +from .haptics.haptic_manager import HapticManager +from .config.config_manager import ConfigManager +from .editor.input_editor import InputEditor +from .utils.input_utils import InputUtils +from .platforms.platform_adapter import PlatformAdapter + +class MultiPlatformInputPlugin: + """ + 多平台输入支持插件主类 + 整合所有输入设备支持功能模块 + """ + + def __init__(self, engine): + """ + 初始化多平台输入支持插件 + + Args: + engine: EG引擎实例 + """ + self.engine = engine + self.name = "MultiPlatformInput" + self.version = "1.0.0" + self.author = "EG Plugin System" + + # 功能模块 + self.input_manager = None + self.advanced_input_processor = None + self.device_manager = None + self.device_calibrator = None + self.event_system = None + self.input_mapper = None + self.gesture_recognizer = None + self.haptic_manager = None + self.config_manager = None + self.input_editor = None + self.input_utils = None + self.platform_adapter = None + + # 插件状态 + self.enabled = False + self.initialized = False + + # 性能统计 + self.stats = { + 'devices_connected': 0, + 'events_processed': 0, + 'gestures_recognized': 0, + 'haptic_effects': 0, + 'input_mappings': 0 + } + + print(f"✓ 多平台输入支持插件 v{self.version} 已创建") + + def initialize(self) -> bool: + """ + 初始化插件 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化多平台输入支持插件...") + + # 初始化核心组件 + self.input_manager = InputManager(self) + self.advanced_input_processor = AdvancedInputProcessor(self) + self.device_manager = DeviceManager(self) + self.device_calibrator = DeviceCalibrator(self) + self.event_system = EventSystem(self) + self.input_mapper = InputMapper(self) + self.gesture_recognizer = GestureRecognizer(self) + self.haptic_manager = HapticManager(self) + self.config_manager = ConfigManager(self) + self.input_editor = InputEditor(self) + self.input_utils = InputUtils(self) + self.platform_adapter = PlatformAdapter(self) + + # 初始化各个模块 + modules = [ + self.input_manager, + self.advanced_input_processor, + self.device_manager, + self.device_calibrator, + self.event_system, + self.input_mapper, + self.gesture_recognizer, + self.haptic_manager, + self.config_manager, + self.input_editor, + self.input_utils, + self.platform_adapter + ] + + for module in modules: + if module and not module.initialize(): + print(f"✗ 模块初始化失败: {module.__class__.__name__}") + return False + + self.initialized = True + print("✓ 多平台输入支持插件初始化完成") + return True + + except Exception as e: + print(f"✗ 多平台输入支持插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 插件未初始化") + return False + + print("正在启用多平台输入支持插件...") + + # 启用各个模块 + modules = [ + self.input_manager, + self.advanced_input_processor, + self.device_manager, + self.device_calibrator, + self.event_system, + self.input_mapper, + self.gesture_recognizer, + self.haptic_manager, + self.config_manager, + self.input_editor, + self.input_utils, + self.platform_adapter + ] + + for module in modules: + if module and not module.enable(): + print(f"✗ 模块启用失败: {module.__class__.__name__}") + return False + + self.enabled = True + print("✓ 多平台输入支持插件已启用") + return True + + except Exception as e: + print(f"✗ 多平台输入支持插件启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用插件""" + try: + if not self.enabled: + return + + print("正在禁用多平台输入支持插件...") + + # 禁用各个模块 + modules = [ + self.input_manager, + self.advanced_input_processor, + self.device_manager, + self.device_calibrator, + self.event_system, + self.input_mapper, + self.gesture_recognizer, + self.haptic_manager, + self.config_manager, + self.input_editor, + self.input_utils, + self.platform_adapter + ] + + for module in modules: + if module: + module.disable() + + self.enabled = False + print("✓ 多平台输入支持插件已禁用") + + except Exception as e: + print(f"✗ 多平台输入支持插件禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理插件资源""" + try: + print("正在清理多平台输入支持插件资源...") + + # 清理各个模块 + modules = [ + self.input_manager, + self.advanced_input_processor, + self.device_manager, + self.device_calibrator, + self.event_system, + self.input_mapper, + self.gesture_recognizer, + self.haptic_manager, + self.config_manager, + self.input_editor, + self.input_utils, + self.platform_adapter + ] + + for module in modules: + if module: + module.finalize() + + self.initialized = False + print("✓ 多平台输入支持插件资源已清理") + + except Exception as e: + print(f"✗ 多平台输入支持插件资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新插件状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + # 更新各个模块 + modules = [ + self.input_manager, + self.advanced_input_processor, + self.device_manager, + self.device_calibrator, + self.event_system, + self.input_mapper, + self.gesture_recognizer, + self.haptic_manager, + self.config_manager, + self.input_editor, + self.input_utils, + self.platform_adapter + ] + + for module in modules: + if module: + module.update(dt) + + # 更新统计信息 + self.stats['events_processed'] += 1 + + except Exception as e: + print(f"✗ 多平台输入支持插件更新失败: {e}") + import traceback + traceback.print_exc() + + def get_stats(self) -> dict: + """ + 获取插件统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'devices_connected': 0, + 'events_processed': 0, + 'gestures_recognized': 0, + 'haptic_effects': 0, + 'input_mappings': 0 + } print("✓ 多平台输入支持统计信息已重置") \ No newline at end of file diff --git a/plugins/user/multiplatform_input/utils/input_utils.py b/plugins/user/multiplatform_input/utils/input_utils.py index 7bd0aa69..fb717625 100644 --- a/plugins/user/multiplatform_input/utils/input_utils.py +++ b/plugins/user/multiplatform_input/utils/input_utils.py @@ -1,770 +1,770 @@ -""" -工具类模块 -提供各种实用工具函数和辅助类 -""" - -import time -import math -from typing import Dict, Any, List, Optional, Tuple, Union -from enum import Enum -import json -import hashlib - -class InputUtils: - """ - 输入工具类 - 提供各种实用工具函数和辅助类 - """ - - def __init__(self, plugin): - """ - 初始化输入工具类 - - Args: - plugin: 多平台输入支持插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 工具配置 - self.utils_config = { - 'enable_logging': True, - 'log_level': 'INFO', - 'max_log_entries': 1000, - 'enable_performance_monitoring': True, - 'performance_sample_rate': 10.0, # Hz - 'cache_size': 100, - 'enable_data_compression': False - } - - # 日志系统 - self.log_entries = [] - self.log_counter = 0 - - # 性能监控 - self.performance_metrics = { - 'update_times': [], - 'memory_usage': 0, - 'cpu_usage': 0, - 'frame_rate': 0.0, - 'last_sample_time': 0.0 - } - - # 缓存系统 - self.cache = {} - self.cache_timestamps = {} - - # 数据转换配置 - self.conversion_config = { - 'angle_unit': 'radians', # 'radians' or 'degrees' - 'distance_unit': 'pixels', # 'pixels', 'meters', 'units' - 'time_unit': 'seconds' # 'seconds', 'milliseconds' - } - - # 统计信息 - self.utils_stats = { - 'functions_called': 0, - 'cache_hits': 0, - 'cache_misses': 0, - 'data_converted': 0, - 'logs_written': 0, - 'errors_handled': 0 - } - - # 时间戳记录 - self.last_performance_sample = 0.0 - self.startup_time = time.time() - - print("✓ 输入工具类已创建") - - def initialize(self) -> bool: - """ - 初始化输入工具类 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 输入工具类初始化完成") - return True - - except Exception as e: - print(f"✗ 输入工具类初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用输入工具类 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 输入工具类未初始化") - return False - - self.enabled = True - print("✓ 输入工具类已启用") - return True - - except Exception as e: - print(f"✗ 输入工具类启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用输入工具类""" - try: - self.enabled = False - print("✓ 输入工具类已禁用") - - except Exception as e: - print(f"✗ 输入工具类禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理输入工具类资源""" - try: - self.disable() - self.log_entries.clear() - self.performance_metrics.clear() - self.cache.clear() - self.cache_timestamps.clear() - self.initialized = False - print("✓ 输入工具类资源已清理") - - except Exception as e: - print(f"✗ 输入工具类资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新输入工具类状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新性能监控 - if self.utils_config['enable_performance_monitoring']: - self._update_performance_metrics(dt) - - except Exception as e: - print(f"✗ 输入工具类更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_performance_metrics(self, dt: float): - """ - 更新性能监控指标 - - Args: - dt: 时间增量 - """ - try: - current_time = time.time() - - # 采样间隔控制 - if current_time - self.last_performance_sample >= 1.0 / self.utils_config['performance_sample_rate']: - # 记录更新时间 - self.performance_metrics['update_times'].append(dt) - - # 保持采样数量 - if len(self.performance_metrics['update_times']) > 100: - self.performance_metrics['update_times'].pop(0) - - # 计算帧率 - if dt > 0: - self.performance_metrics['frame_rate'] = 1.0 / dt - - self.last_performance_sample = current_time - - except Exception as e: - print(f"✗ 性能监控更新失败: {e}") - - # 日志工具函数 - def log_message(self, level: str, message: str, data: Any = None): - """ - 记录日志消息 - - Args: - level: 日志级别 - message: 日志消息 - data: 附加数据 - """ - try: - if not self.utils_config['enable_logging']: - return - - log_entry = { - 'id': self.log_counter, - 'timestamp': time.time(), - 'level': level, - 'message': message, - 'data': data - } - - self.log_counter += 1 - self.log_entries.append(log_entry) - self.utils_stats['logs_written'] += 1 - - # 保持日志数量 - if len(self.log_entries) > self.utils_config['max_log_entries']: - self.log_entries.pop(0) - - # 打印到控制台(根据日志级别) - if self._should_print_log(level): - print(f"[{level}] {message}") - if data is not None: - print(f" Data: {data}") - - except Exception as e: - print(f"✗ 日志记录失败: {e}") - - def _should_print_log(self, level: str) -> bool: - """ - 判断是否应该打印日志 - - Args: - level: 日志级别 - - Returns: - 是否应该打印 - """ - try: - level_order = {'DEBUG': 0, 'INFO': 1, 'WARNING': 2, 'ERROR': 3, 'CRITICAL': 4} - current_level = self.utils_config['log_level'] - - return level_order.get(level, 0) >= level_order.get(current_level, 1) - except Exception: - return True - - def get_log_entries(self, level: str = None, limit: int = None) -> List[Dict[str, Any]]: - """ - 获取日志条目 - - Args: - level: 日志级别过滤器 - limit: 返回条目数量限制 - - Returns: - 日志条目列表 - """ - try: - entries = self.log_entries.copy() - - if level: - entries = [entry for entry in entries if entry['level'] == level] - - if limit: - entries = entries[-limit:] - - return entries - except Exception as e: - print(f"✗ 日志条目获取失败: {e}") - return [] - - def clear_log_entries(self): - """清空日志条目""" - try: - self.log_entries.clear() - print("✓ 日志条目已清空") - except Exception as e: - print(f"✗ 日志条目清空失败: {e}") - - # 缓存工具函数 - def cache_set(self, key: str, value: Any, ttl: float = 300.0) -> bool: - """ - 设置缓存值 - - Args: - key: 缓存键 - value: 缓存值 - ttl: 生存时间(秒) - - Returns: - 是否设置成功 - """ - try: - self.cache[key] = value - self.cache_timestamps[key] = time.time() + ttl - - # 保持缓存大小 - if len(self.cache) > self.utils_config['cache_size']: - oldest_key = min(self.cache_timestamps.keys(), key=lambda k: self.cache_timestamps[k]) - del self.cache[oldest_key] - del self.cache_timestamps[oldest_key] - - return True - except Exception as e: - print(f"✗ 缓存设置失败: {e}") - return False - - def cache_get(self, key: str, default: Any = None) -> Any: - """ - 获取缓存值 - - Args: - key: 缓存键 - default: 默认值 - - Returns: - 缓存值或默认值 - """ - try: - if key in self.cache: - # 检查是否过期 - if time.time() < self.cache_timestamps.get(key, float('inf')): - self.utils_stats['cache_hits'] += 1 - return self.cache[key] - else: - # 过期,删除 - del self.cache[key] - del self.cache_timestamps[key] - - self.utils_stats['cache_misses'] += 1 - return default - except Exception as e: - print(f"✗ 缓存获取失败: {e}") - self.utils_stats['cache_misses'] += 1 - return default - - def cache_clear(self): - """清空缓存""" - try: - self.cache.clear() - self.cache_timestamps.clear() - print("✓ 缓存已清空") - except Exception as e: - print(f"✗ 缓存清空失败: {e}") - - # 数据转换工具函数 - def convert_angle(self, angle: float, from_unit: str = None, to_unit: str = None) -> float: - """ - 转换角度单位 - - Args: - angle: 角度值 - from_unit: 源单位(默认使用配置) - to_unit: 目标单位(默认使用配置) - - Returns: - 转换后的角度值 - """ - try: - if from_unit is None: - from_unit = self.conversion_config['angle_unit'] - if to_unit is None: - to_unit = self.conversion_config['angle_unit'] - - # 如果单位相同,直接返回 - if from_unit == to_unit: - return angle - - # 转换到弧度 - if from_unit == 'degrees': - radians = math.radians(angle) - else: - radians = angle - - # 从弧度转换到目标单位 - if to_unit == 'degrees': - result = math.degrees(radians) - else: - result = radians - - self.utils_stats['data_converted'] += 1 - return result - - except Exception as e: - print(f"✗ 角度转换失败: {e}") - return angle - - def convert_distance(self, distance: float, from_unit: str = None, to_unit: str = None) -> float: - """ - 转换距离单位 - - Args: - distance: 距离值 - from_unit: 源单位(默认使用配置) - to_unit: 目标单位(默认使用配置) - - Returns: - 转换后的距离值 - """ - try: - # 简化的距离转换(实际应用中可能需要更复杂的转换) - # 这里假设像素和单位是相同的,米需要缩放因子 - return distance # 简化处理 - - except Exception as e: - print(f"✗ 距离转换失败: {e}") - return distance - - def convert_time(self, time_value: float, from_unit: str = None, to_unit: str = None) -> float: - """ - 转换时间单位 - - Args: - time_value: 时间值 - from_unit: 源单位(默认使用配置) - to_unit: 目标单位(默认使用配置) - - Returns: - 转换后的时间值 - """ - try: - if from_unit is None: - from_unit = self.conversion_config['time_unit'] - if to_unit is None: - to_unit = self.conversion_config['time_unit'] - - # 如果单位相同,直接返回 - if from_unit == to_unit: - return time_value - - # 转换到秒 - if from_unit == 'milliseconds': - seconds = time_value / 1000.0 - else: - seconds = time_value - - # 从秒转换到目标单位 - if to_unit == 'milliseconds': - result = seconds * 1000.0 - else: - result = seconds - - self.utils_stats['data_converted'] += 1 - return result - - except Exception as e: - print(f"✗ 时间转换失败: {e}") - return time_value - - # 数学工具函数 - def calculate_distance(self, point1: Tuple[float, float], point2: Tuple[float, float]) -> float: - """ - 计算两点间距离 - - Args: - point1: 第一个点 (x, y) - point2: 第二个点 (x, y) - - Returns: - 距离值 - """ - try: - dx = point2[0] - point1[0] - dy = point2[1] - point1[1] - return math.sqrt(dx*dx + dy*dy) - except Exception as e: - print(f"✗ 距离计算失败: {e}") - return 0.0 - - def calculate_angle(self, point1: Tuple[float, float], point2: Tuple[float, float]) -> float: - """ - 计算两点间角度 - - Args: - point1: 第一个点 (x, y) - point2: 第二个点 (x, y) - - Returns: - 角度值(弧度) - """ - try: - dx = point2[0] - point1[0] - dy = point2[1] - point1[1] - return math.atan2(dy, dx) - except Exception as e: - print(f"✗ 角度计算失败: {e}") - return 0.0 - - def normalize_vector(self, vector: Tuple[float, float]) -> Tuple[float, float]: - """ - 归一化向量 - - Args: - vector: 向量 (x, y) - - Returns: - 归一化后的向量 - """ - try: - length = math.sqrt(vector[0]*vector[0] + vector[1]*vector[1]) - if length > 0: - return (vector[0]/length, vector[1]/length) - else: - return (0.0, 0.0) - except Exception as e: - print(f"✗ 向量归一化失败: {e}") - return (0.0, 0.0) - - def clamp_value(self, value: float, min_value: float, max_value: float) -> float: - """ - 限制值范围 - - Args: - value: 值 - min_value: 最小值 - max_value: 最大值 - - Returns: - 限制后的值 - """ - try: - return max(min_value, min(max_value, value)) - except Exception as e: - print(f"✗ 值限制失败: {e}") - return value - - # JSON工具函数 - def safe_json_load(self, json_string: str, default_value: Any = None) -> Any: - """ - 安全地加载JSON字符串 - - Args: - json_string: JSON字符串 - default_value: 默认值 - - Returns: - 解析后的数据或默认值 - """ - try: - return json.loads(json_string) - except Exception as e: - print(f"✗ JSON加载失败: {e}") - self.utils_stats['errors_handled'] += 1 - return default_value - - def safe_json_dump(self, data: Any, **kwargs) -> str: - """ - 安全地导出JSON数据 - - Args: - data: 要导出的数据 - **kwargs: json.dumps的其他参数 - - Returns: - JSON字符串 - """ - try: - return json.dumps(data, **kwargs) - except Exception as e: - print(f"✗ JSON导出失败: {e}") - self.utils_stats['errors_handled'] += 1 - return "{}" - - # 哈希工具函数 - def calculate_hash(self, data: Union[str, bytes], algorithm: str = 'md5') -> str: - """ - 计算数据哈希值 - - Args: - data: 要计算哈希的数据 - algorithm: 哈希算法 ('md5', 'sha1', 'sha256') - - Returns: - 哈希值十六进制字符串 - """ - try: - if isinstance(data, str): - data = data.encode('utf-8') - - if algorithm == 'md5': - hash_obj = hashlib.md5() - elif algorithm == 'sha1': - hash_obj = hashlib.sha1() - elif algorithm == 'sha256': - hash_obj = hashlib.sha256() - else: - hash_obj = hashlib.md5() - - hash_obj.update(data) - return hash_obj.hexdigest() - - except Exception as e: - print(f"✗ 哈希计算失败: {e}") - return "" - - # 性能工具函数 - def get_performance_metrics(self) -> Dict[str, Any]: - """ - 获取性能指标 - - Returns: - 性能指标字典 - """ - try: - metrics = self.performance_metrics.copy() - - # 计算平均更新时间 - if self.performance_metrics['update_times']: - metrics['average_update_time'] = sum(self.performance_metrics['update_times']) / len(self.performance_metrics['update_times']) - else: - metrics['average_update_time'] = 0.0 - - # 计算运行时间 - metrics['uptime'] = time.time() - self.startup_time - - return metrics - except Exception as e: - print(f"✗ 性能指标获取失败: {e}") - return {} - - def reset_performance_metrics(self): - """重置性能指标""" - try: - self.performance_metrics = { - 'update_times': [], - 'memory_usage': 0, - 'cpu_usage': 0, - 'frame_rate': 0.0, - 'last_sample_time': 0.0 - } - print("✓ 性能指标已重置") - except Exception as e: - print(f"✗ 性能指标重置失败: {e}") - - # 配置工具函数 - def get_utils_config(self) -> Dict[str, Any]: - """ - 获取工具配置 - - Returns: - 工具配置字典 - """ - return self.utils_config.copy() - - def set_utils_config(self, config: Dict[str, Any]) -> bool: - """ - 设置工具配置 - - Args: - config: 工具配置字典 - - Returns: - 是否设置成功 - """ - try: - self.utils_config.update(config) - print(f"✓ 工具配置已更新: {self.utils_config}") - return True - except Exception as e: - print(f"✗ 工具配置设置失败: {e}") - return False - - def get_conversion_config(self) -> Dict[str, str]: - """ - 获取转换配置 - - Returns: - 转换配置字典 - """ - return self.conversion_config.copy() - - def set_conversion_config(self, config: Dict[str, str]) -> bool: - """ - 设置转换配置 - - Args: - config: 转换配置字典 - - Returns: - 是否设置成功 - """ - try: - self.conversion_config.update(config) - print(f"✓ 转换配置已更新: {self.conversion_config}") - return True - except Exception as e: - print(f"✗ 转换配置设置失败: {e}") - return False - - # 统计工具函数 - def get_utils_stats(self) -> Dict[str, int]: - """ - 获取工具统计信息 - - Returns: - 工具统计字典 - """ - return self.utils_stats.copy() - - def reset_utils_stats(self): - """重置工具统计信息""" - try: - self.utils_stats = { - 'functions_called': 0, - 'cache_hits': 0, - 'cache_misses': 0, - 'data_converted': 0, - 'logs_written': 0, - 'errors_handled': 0 - } - print("✓ 工具统计信息已重置") - except Exception as e: - print(f"✗ 工具统计信息重置失败: {e}") - - # 调试工具函数 - def dump_object_info(self, obj: Any, name: str = "Object") -> str: - """ - 转储对象信息 - - Args: - obj: 要转储的对象 - name: 对象名称 - - Returns: - 对象信息字符串 - """ - try: - info = f"=== {name} Info ===\n" - info += f"Type: {type(obj)}\n" - info += f"String representation: {str(obj)}\n" - - if hasattr(obj, '__dict__'): - info += f"Attributes: {obj.__dict__}\n" - - if hasattr(obj, '__slots__'): - info += f"Slots: {obj.__slots__}\n" - - return info - except Exception as e: - print(f"✗ 对象信息转储失败: {e}") - return f"Error dumping {name}: {e}" - - def measure_execution_time(self, func: callable, *args, **kwargs) -> Tuple[Any, float]: - """ - 测量函数执行时间 - - Args: - func: 要测量的函数 - *args: 函数参数 - **kwargs: 函数关键字参数 - - Returns: - (函数返回值, 执行时间) - """ - try: - start_time = time.time() - result = func(*args, **kwargs) - end_time = time.time() - - execution_time = end_time - start_time - return result, execution_time - except Exception as e: - print(f"✗ 执行时间测量失败: {e}") +""" +工具类模块 +提供各种实用工具函数和辅助类 +""" + +import time +import math +from typing import Dict, Any, List, Optional, Tuple, Union +from enum import Enum +import json +import hashlib + +class InputUtils: + """ + 输入工具类 + 提供各种实用工具函数和辅助类 + """ + + def __init__(self, plugin): + """ + 初始化输入工具类 + + Args: + plugin: 多平台输入支持插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 工具配置 + self.utils_config = { + 'enable_logging': True, + 'log_level': 'INFO', + 'max_log_entries': 1000, + 'enable_performance_monitoring': True, + 'performance_sample_rate': 10.0, # Hz + 'cache_size': 100, + 'enable_data_compression': False + } + + # 日志系统 + self.log_entries = [] + self.log_counter = 0 + + # 性能监控 + self.performance_metrics = { + 'update_times': [], + 'memory_usage': 0, + 'cpu_usage': 0, + 'frame_rate': 0.0, + 'last_sample_time': 0.0 + } + + # 缓存系统 + self.cache = {} + self.cache_timestamps = {} + + # 数据转换配置 + self.conversion_config = { + 'angle_unit': 'radians', # 'radians' or 'degrees' + 'distance_unit': 'pixels', # 'pixels', 'meters', 'units' + 'time_unit': 'seconds' # 'seconds', 'milliseconds' + } + + # 统计信息 + self.utils_stats = { + 'functions_called': 0, + 'cache_hits': 0, + 'cache_misses': 0, + 'data_converted': 0, + 'logs_written': 0, + 'errors_handled': 0 + } + + # 时间戳记录 + self.last_performance_sample = 0.0 + self.startup_time = time.time() + + print("✓ 输入工具类已创建") + + def initialize(self) -> bool: + """ + 初始化输入工具类 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 输入工具类初始化完成") + return True + + except Exception as e: + print(f"✗ 输入工具类初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用输入工具类 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 输入工具类未初始化") + return False + + self.enabled = True + print("✓ 输入工具类已启用") + return True + + except Exception as e: + print(f"✗ 输入工具类启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用输入工具类""" + try: + self.enabled = False + print("✓ 输入工具类已禁用") + + except Exception as e: + print(f"✗ 输入工具类禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理输入工具类资源""" + try: + self.disable() + self.log_entries.clear() + self.performance_metrics.clear() + self.cache.clear() + self.cache_timestamps.clear() + self.initialized = False + print("✓ 输入工具类资源已清理") + + except Exception as e: + print(f"✗ 输入工具类资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新输入工具类状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新性能监控 + if self.utils_config['enable_performance_monitoring']: + self._update_performance_metrics(dt) + + except Exception as e: + print(f"✗ 输入工具类更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_performance_metrics(self, dt: float): + """ + 更新性能监控指标 + + Args: + dt: 时间增量 + """ + try: + current_time = time.time() + + # 采样间隔控制 + if current_time - self.last_performance_sample >= 1.0 / self.utils_config['performance_sample_rate']: + # 记录更新时间 + self.performance_metrics['update_times'].append(dt) + + # 保持采样数量 + if len(self.performance_metrics['update_times']) > 100: + self.performance_metrics['update_times'].pop(0) + + # 计算帧率 + if dt > 0: + self.performance_metrics['frame_rate'] = 1.0 / dt + + self.last_performance_sample = current_time + + except Exception as e: + print(f"✗ 性能监控更新失败: {e}") + + # 日志工具函数 + def log_message(self, level: str, message: str, data: Any = None): + """ + 记录日志消息 + + Args: + level: 日志级别 + message: 日志消息 + data: 附加数据 + """ + try: + if not self.utils_config['enable_logging']: + return + + log_entry = { + 'id': self.log_counter, + 'timestamp': time.time(), + 'level': level, + 'message': message, + 'data': data + } + + self.log_counter += 1 + self.log_entries.append(log_entry) + self.utils_stats['logs_written'] += 1 + + # 保持日志数量 + if len(self.log_entries) > self.utils_config['max_log_entries']: + self.log_entries.pop(0) + + # 打印到控制台(根据日志级别) + if self._should_print_log(level): + print(f"[{level}] {message}") + if data is not None: + print(f" Data: {data}") + + except Exception as e: + print(f"✗ 日志记录失败: {e}") + + def _should_print_log(self, level: str) -> bool: + """ + 判断是否应该打印日志 + + Args: + level: 日志级别 + + Returns: + 是否应该打印 + """ + try: + level_order = {'DEBUG': 0, 'INFO': 1, 'WARNING': 2, 'ERROR': 3, 'CRITICAL': 4} + current_level = self.utils_config['log_level'] + + return level_order.get(level, 0) >= level_order.get(current_level, 1) + except Exception: + return True + + def get_log_entries(self, level: str = None, limit: int = None) -> List[Dict[str, Any]]: + """ + 获取日志条目 + + Args: + level: 日志级别过滤器 + limit: 返回条目数量限制 + + Returns: + 日志条目列表 + """ + try: + entries = self.log_entries.copy() + + if level: + entries = [entry for entry in entries if entry['level'] == level] + + if limit: + entries = entries[-limit:] + + return entries + except Exception as e: + print(f"✗ 日志条目获取失败: {e}") + return [] + + def clear_log_entries(self): + """清空日志条目""" + try: + self.log_entries.clear() + print("✓ 日志条目已清空") + except Exception as e: + print(f"✗ 日志条目清空失败: {e}") + + # 缓存工具函数 + def cache_set(self, key: str, value: Any, ttl: float = 300.0) -> bool: + """ + 设置缓存值 + + Args: + key: 缓存键 + value: 缓存值 + ttl: 生存时间(秒) + + Returns: + 是否设置成功 + """ + try: + self.cache[key] = value + self.cache_timestamps[key] = time.time() + ttl + + # 保持缓存大小 + if len(self.cache) > self.utils_config['cache_size']: + oldest_key = min(self.cache_timestamps.keys(), key=lambda k: self.cache_timestamps[k]) + del self.cache[oldest_key] + del self.cache_timestamps[oldest_key] + + return True + except Exception as e: + print(f"✗ 缓存设置失败: {e}") + return False + + def cache_get(self, key: str, default: Any = None) -> Any: + """ + 获取缓存值 + + Args: + key: 缓存键 + default: 默认值 + + Returns: + 缓存值或默认值 + """ + try: + if key in self.cache: + # 检查是否过期 + if time.time() < self.cache_timestamps.get(key, float('inf')): + self.utils_stats['cache_hits'] += 1 + return self.cache[key] + else: + # 过期,删除 + del self.cache[key] + del self.cache_timestamps[key] + + self.utils_stats['cache_misses'] += 1 + return default + except Exception as e: + print(f"✗ 缓存获取失败: {e}") + self.utils_stats['cache_misses'] += 1 + return default + + def cache_clear(self): + """清空缓存""" + try: + self.cache.clear() + self.cache_timestamps.clear() + print("✓ 缓存已清空") + except Exception as e: + print(f"✗ 缓存清空失败: {e}") + + # 数据转换工具函数 + def convert_angle(self, angle: float, from_unit: str = None, to_unit: str = None) -> float: + """ + 转换角度单位 + + Args: + angle: 角度值 + from_unit: 源单位(默认使用配置) + to_unit: 目标单位(默认使用配置) + + Returns: + 转换后的角度值 + """ + try: + if from_unit is None: + from_unit = self.conversion_config['angle_unit'] + if to_unit is None: + to_unit = self.conversion_config['angle_unit'] + + # 如果单位相同,直接返回 + if from_unit == to_unit: + return angle + + # 转换到弧度 + if from_unit == 'degrees': + radians = math.radians(angle) + else: + radians = angle + + # 从弧度转换到目标单位 + if to_unit == 'degrees': + result = math.degrees(radians) + else: + result = radians + + self.utils_stats['data_converted'] += 1 + return result + + except Exception as e: + print(f"✗ 角度转换失败: {e}") + return angle + + def convert_distance(self, distance: float, from_unit: str = None, to_unit: str = None) -> float: + """ + 转换距离单位 + + Args: + distance: 距离值 + from_unit: 源单位(默认使用配置) + to_unit: 目标单位(默认使用配置) + + Returns: + 转换后的距离值 + """ + try: + # 简化的距离转换(实际应用中可能需要更复杂的转换) + # 这里假设像素和单位是相同的,米需要缩放因子 + return distance # 简化处理 + + except Exception as e: + print(f"✗ 距离转换失败: {e}") + return distance + + def convert_time(self, time_value: float, from_unit: str = None, to_unit: str = None) -> float: + """ + 转换时间单位 + + Args: + time_value: 时间值 + from_unit: 源单位(默认使用配置) + to_unit: 目标单位(默认使用配置) + + Returns: + 转换后的时间值 + """ + try: + if from_unit is None: + from_unit = self.conversion_config['time_unit'] + if to_unit is None: + to_unit = self.conversion_config['time_unit'] + + # 如果单位相同,直接返回 + if from_unit == to_unit: + return time_value + + # 转换到秒 + if from_unit == 'milliseconds': + seconds = time_value / 1000.0 + else: + seconds = time_value + + # 从秒转换到目标单位 + if to_unit == 'milliseconds': + result = seconds * 1000.0 + else: + result = seconds + + self.utils_stats['data_converted'] += 1 + return result + + except Exception as e: + print(f"✗ 时间转换失败: {e}") + return time_value + + # 数学工具函数 + def calculate_distance(self, point1: Tuple[float, float], point2: Tuple[float, float]) -> float: + """ + 计算两点间距离 + + Args: + point1: 第一个点 (x, y) + point2: 第二个点 (x, y) + + Returns: + 距离值 + """ + try: + dx = point2[0] - point1[0] + dy = point2[1] - point1[1] + return math.sqrt(dx*dx + dy*dy) + except Exception as e: + print(f"✗ 距离计算失败: {e}") + return 0.0 + + def calculate_angle(self, point1: Tuple[float, float], point2: Tuple[float, float]) -> float: + """ + 计算两点间角度 + + Args: + point1: 第一个点 (x, y) + point2: 第二个点 (x, y) + + Returns: + 角度值(弧度) + """ + try: + dx = point2[0] - point1[0] + dy = point2[1] - point1[1] + return math.atan2(dy, dx) + except Exception as e: + print(f"✗ 角度计算失败: {e}") + return 0.0 + + def normalize_vector(self, vector: Tuple[float, float]) -> Tuple[float, float]: + """ + 归一化向量 + + Args: + vector: 向量 (x, y) + + Returns: + 归一化后的向量 + """ + try: + length = math.sqrt(vector[0]*vector[0] + vector[1]*vector[1]) + if length > 0: + return (vector[0]/length, vector[1]/length) + else: + return (0.0, 0.0) + except Exception as e: + print(f"✗ 向量归一化失败: {e}") + return (0.0, 0.0) + + def clamp_value(self, value: float, min_value: float, max_value: float) -> float: + """ + 限制值范围 + + Args: + value: 值 + min_value: 最小值 + max_value: 最大值 + + Returns: + 限制后的值 + """ + try: + return max(min_value, min(max_value, value)) + except Exception as e: + print(f"✗ 值限制失败: {e}") + return value + + # JSON工具函数 + def safe_json_load(self, json_string: str, default_value: Any = None) -> Any: + """ + 安全地加载JSON字符串 + + Args: + json_string: JSON字符串 + default_value: 默认值 + + Returns: + 解析后的数据或默认值 + """ + try: + return json.loads(json_string) + except Exception as e: + print(f"✗ JSON加载失败: {e}") + self.utils_stats['errors_handled'] += 1 + return default_value + + def safe_json_dump(self, data: Any, **kwargs) -> str: + """ + 安全地导出JSON数据 + + Args: + data: 要导出的数据 + **kwargs: json.dumps的其他参数 + + Returns: + JSON字符串 + """ + try: + return json.dumps(data, **kwargs) + except Exception as e: + print(f"✗ JSON导出失败: {e}") + self.utils_stats['errors_handled'] += 1 + return "{}" + + # 哈希工具函数 + def calculate_hash(self, data: Union[str, bytes], algorithm: str = 'md5') -> str: + """ + 计算数据哈希值 + + Args: + data: 要计算哈希的数据 + algorithm: 哈希算法 ('md5', 'sha1', 'sha256') + + Returns: + 哈希值十六进制字符串 + """ + try: + if isinstance(data, str): + data = data.encode('utf-8') + + if algorithm == 'md5': + hash_obj = hashlib.md5() + elif algorithm == 'sha1': + hash_obj = hashlib.sha1() + elif algorithm == 'sha256': + hash_obj = hashlib.sha256() + else: + hash_obj = hashlib.md5() + + hash_obj.update(data) + return hash_obj.hexdigest() + + except Exception as e: + print(f"✗ 哈希计算失败: {e}") + return "" + + # 性能工具函数 + def get_performance_metrics(self) -> Dict[str, Any]: + """ + 获取性能指标 + + Returns: + 性能指标字典 + """ + try: + metrics = self.performance_metrics.copy() + + # 计算平均更新时间 + if self.performance_metrics['update_times']: + metrics['average_update_time'] = sum(self.performance_metrics['update_times']) / len(self.performance_metrics['update_times']) + else: + metrics['average_update_time'] = 0.0 + + # 计算运行时间 + metrics['uptime'] = time.time() - self.startup_time + + return metrics + except Exception as e: + print(f"✗ 性能指标获取失败: {e}") + return {} + + def reset_performance_metrics(self): + """重置性能指标""" + try: + self.performance_metrics = { + 'update_times': [], + 'memory_usage': 0, + 'cpu_usage': 0, + 'frame_rate': 0.0, + 'last_sample_time': 0.0 + } + print("✓ 性能指标已重置") + except Exception as e: + print(f"✗ 性能指标重置失败: {e}") + + # 配置工具函数 + def get_utils_config(self) -> Dict[str, Any]: + """ + 获取工具配置 + + Returns: + 工具配置字典 + """ + return self.utils_config.copy() + + def set_utils_config(self, config: Dict[str, Any]) -> bool: + """ + 设置工具配置 + + Args: + config: 工具配置字典 + + Returns: + 是否设置成功 + """ + try: + self.utils_config.update(config) + print(f"✓ 工具配置已更新: {self.utils_config}") + return True + except Exception as e: + print(f"✗ 工具配置设置失败: {e}") + return False + + def get_conversion_config(self) -> Dict[str, str]: + """ + 获取转换配置 + + Returns: + 转换配置字典 + """ + return self.conversion_config.copy() + + def set_conversion_config(self, config: Dict[str, str]) -> bool: + """ + 设置转换配置 + + Args: + config: 转换配置字典 + + Returns: + 是否设置成功 + """ + try: + self.conversion_config.update(config) + print(f"✓ 转换配置已更新: {self.conversion_config}") + return True + except Exception as e: + print(f"✗ 转换配置设置失败: {e}") + return False + + # 统计工具函数 + def get_utils_stats(self) -> Dict[str, int]: + """ + 获取工具统计信息 + + Returns: + 工具统计字典 + """ + return self.utils_stats.copy() + + def reset_utils_stats(self): + """重置工具统计信息""" + try: + self.utils_stats = { + 'functions_called': 0, + 'cache_hits': 0, + 'cache_misses': 0, + 'data_converted': 0, + 'logs_written': 0, + 'errors_handled': 0 + } + print("✓ 工具统计信息已重置") + except Exception as e: + print(f"✗ 工具统计信息重置失败: {e}") + + # 调试工具函数 + def dump_object_info(self, obj: Any, name: str = "Object") -> str: + """ + 转储对象信息 + + Args: + obj: 要转储的对象 + name: 对象名称 + + Returns: + 对象信息字符串 + """ + try: + info = f"=== {name} Info ===\n" + info += f"Type: {type(obj)}\n" + info += f"String representation: {str(obj)}\n" + + if hasattr(obj, '__dict__'): + info += f"Attributes: {obj.__dict__}\n" + + if hasattr(obj, '__slots__'): + info += f"Slots: {obj.__slots__}\n" + + return info + except Exception as e: + print(f"✗ 对象信息转储失败: {e}") + return f"Error dumping {name}: {e}" + + def measure_execution_time(self, func: callable, *args, **kwargs) -> Tuple[Any, float]: + """ + 测量函数执行时间 + + Args: + func: 要测量的函数 + *args: 函数参数 + **kwargs: 函数关键字参数 + + Returns: + (函数返回值, 执行时间) + """ + try: + start_time = time.time() + result = func(*args, **kwargs) + end_time = time.time() + + execution_time = end_time - start_time + return result, execution_time + except Exception as e: + print(f"✗ 执行时间测量失败: {e}") return None, 0.0 \ No newline at end of file diff --git a/plugins/user/navigation_mesh/README.md b/plugins/user/navigation_mesh/README.md index 469b5779..c6854d81 100644 --- a/plugins/user/navigation_mesh/README.md +++ b/plugins/user/navigation_mesh/README.md @@ -1,494 +1,494 @@ -# 导航网格插件 - -一个功能完整的3D导航网格系统插件,为EG引擎提供基于导航网格的寻路和路径规划功能。 - -## 功能特性 - -### 核心功能 -- **导航网格生成**: 支持从3D场景自动生成导航网格 -- **路径查找**: 基于A*算法的高效寻路 -- **可视化编辑**: 直观的导航网格编辑界面 -- **多边形管理**: 导航网格多边形的增删改查 -- **路径可视化**: 实时显示寻路结果 - -### 寻路算法 -- **A*算法**: 经典的启发式搜索算法 -- **Dijkstra算法**: 保证最短路径的搜索算法 -- **多种启发式函数**: 欀曼哈顿距离、欧几里得距离等 -- **路径平滑**: 漏斗算法和简单平滑算法 -- **多路点路径**: 支持经过多个路点的复杂路径规划 -- **动态障碍物**: 运行时添加和移除障碍物 -- **路径缓存**: 提高重复路径查找性能 - -### 编辑功能 -- **可视化编辑**: 图形化界面编辑导航网格 -- **多种工具**: 选择、添加、删除、绘制工具 -- **属性调节**: 网格大小、代理尺寸等参数调节 -- **撤销重做**: 编辑历史管理和操作回退 -- **网格对齐**: 网格对齐功能提高编辑精度 -- **自动连接**: 自动计算多边形连接关系 - -### 性能优化 -- **空间哈希**: 加速多边形查找 -- **对象复用**: 减少内存分配 -- **批量处理**: 提高大量数据处理效率 -- **LOD支持**: 多层次细节支持 - -### 高级特性 -- **分层寻路**: 大场景高效寻路 -- **路径复杂度分析**: 路径质量评估 -- **备选路径**: 多条路径选择 -- **路径验证**: 路径有效性检查 -- **统计信息**: 详细的性能和使用统计 -- **网格分析**: 质量分析和优化建议 -- **网格优化**: 自动优化和简化功能 -- **格式支持**: 多种文件格式导入导出 - -## 使用方法 - -### 快捷键操作 -- **F1**: 切换导航网格编辑器显示 -- **F2**: 切换导航网格可见性 -- **F3**: 查找随机路径进行测试 -- **F4**: 清除所有路径可视化 -- **F5**: 从场景生成导航网格 -- **F6**: 验证导航网格完整性 -- **F7**: 优化导航网格 -- **F8**: 显示统计信息 -- **F9**: 分析导航网格质量 -- **F10**: 导出分析报告 -- **F11**: 简化导航网格 -- **F12**: 检测瓶颈区域 - -### GUI界面操作 -插件会在屏幕左上角创建控制面板,可以通过按钮直接操作导航网格功能。 - -### 编程接口 -```python -# 获取插件实例 -navmesh_plugin = world.plugin_manager.get_plugin("navigation_mesh") - -# 生成导航网格 -navmesh_plugin.generate_navmesh_from_scene() - -# 从文件加载导航网格 -navmesh_plugin.load_navmesh_from_file("path/to/navmesh.json") - -# 保存导航网格到文件 -navmesh_plugin.save_navmesh_to_file("path/to/navmesh.json") - -# 查找路径 -start_point = Point3(0, 0, 0) -end_point = Point3(10, 0, 10) -path = navmesh_plugin.find_path(start_point, end_point) - -# 检查点是否可行走 -is_walkable = navmesh_plugin.is_point_walkable(Point3(5, 0, 5)) - -# 获取最近的可行走点 -closest_point = navmesh_plugin.get_closest_point(Point3(3, 2, 4)) - -# 添加动态障碍物 -obstacle_id = navmesh_plugin.add_dynamic_obstacle(Point3(5, 0, 5), 1.0, 2.0) - -# 移除动态障碍物 -navmesh_plugin.remove_dynamic_obstacle(obstacle_id) - -# 设置导航网格参数 -navmesh_plugin.set_navmesh_settings({ - 'cell_size': 0.3, - 'agent_height': 1.8, - 'agent_radius': 0.4 -}) - -# 设置寻路算法 -navmesh_plugin.set_pathfinder_algorithm('astar') # 或 'dijkstra' - -# 查找多路点路径 -waypoints = [Point3(0, 0, 0), Point3(5, 0, 5), Point3(10, 0, 10)] -path = navmesh_plugin.find_path_with_waypoints(waypoints) - -# 估算路径代价 -cost = navmesh_plugin.estimate_path_cost(Point3(0, 0, 0), Point3(10, 0, 10)) - -# 验证路径有效性 -is_valid = navmesh_plugin.validate_path(path) - -# 分析路径复杂度 -complexity = navmesh_plugin.get_path_complexity(path) - -# 查找备选路径 -alternative_paths = navmesh_plugin.find_alternative_paths(Point3(0, 0, 0), Point3(10, 0, 10), 3) - -# 清空路径缓存 -navmesh_plugin.clear_path_cache() - -# 重新构建导航网格 -navmesh_plugin.rebuild_navmesh() - -# 启用空间哈希优化 -navmesh_plugin.enable_spatial_hashing(True) - -# 设置空间哈希网格大小 -navmesh_plugin.set_spatial_hash_cell_size(5.0) - -# 导出统计信息 -navmesh_plugin.export_statistics("stats.json") - -# 导入/导出设置 -navmesh_plugin.import_settings("settings.json") -navmesh_plugin.export_settings("settings.json") - -# 高级功能 -# 分析网格质量 -quality_report = navmesh_plugin.analyze_mesh_quality() - -# 获取优化建议 -suggestions = navmesh_plugin.suggest_optimizations() - -# 检测瓶颈区域 -bottlenecks = navmesh_plugin.detect_navigation_bottlenecks() - -# 生成完整分析报告 -report = navmesh_plugin.generate_analysis_report() - -# 导出分析报告 -navmesh_plugin.export_analysis_report("analysis_report.json") - -# 获取网格复杂度评分 -complexity_score = navmesh_plugin.get_mesh_complexity_score() - -# 推荐网格简化策略 -simplification_recommendation = navmesh_plugin.recommend_mesh_simplification() - -# 合并相邻多边形 -merged_count = navmesh_plugin.merge_adjacent_polygons() - -# 简化多边形 -simplified_count = navmesh_plugin.simplify_mesh_polygons() - -# 移除小多边形 -removed_count = navmesh_plugin.remove_small_mesh_polygons() - -# 执行完整网格优化 -optimization_result = navmesh_plugin.optimize_navigation_mesh('medium') - -# 设置优化参数 -navmesh_plugin.set_optimization_settings({ - 'merge_threshold': 0.5, - 'simplify_threshold': 0.1, - 'min_polygon_area': 0.01 -}) - -# 获取优化参数 -optimization_settings = navmesh_plugin.get_optimization_settings() - -# 填补网格空隙 -filled_gaps = navmesh_plugin.fill_mesh_gaps() - -# 平滑边界 -smoothed_edges = navmesh_plugin.smooth_mesh_boundaries() - -# 平衡多边形大小 -balanced_polygons = navmesh_plugin.balance_polygon_sizes() - -# 序列化导航网格 -serialized_data = navmesh_plugin.serialize_navigation_mesh() - -# 反序列化导航网格 -success = navmesh_plugin.deserialize_navigation_mesh(serialized_data) - -# 保存导航网格(高级选项) -navmesh_plugin.save_navmesh_to_file_advanced("navmesh.json", compress=True, pretty_print=True) - -# 加载导航网格(高级选项) -navmesh_plugin.load_navmesh_from_file_advanced("navmesh.json") - -# 导出为标准格式 -navmesh_plugin.export_to_standard_format("navmesh.obj", "obj") - -# 从标准格式导入 -navmesh_plugin.import_from_standard_format("navmesh.obj", "obj") - -# 获取文件信息 -file_info = navmesh_plugin.get_navmesh_file_info("navmesh.json") - -# 批量分析多个网格文件 -analysis_results = navmesh_plugin.batch_analyze_navmeshes(["navmesh1.json", "navmesh2.json"]) - -# 比较两个导航网格 -comparison_result = navmesh_plugin.compare_navigation_meshes("navmesh1.json", "navmesh2.json") - -# 验证网格文件 -validation_result = navmesh_plugin.validate_navmesh_file("navmesh.json") - -# 优化并保存网格 -optimization_result = navmesh_plugin.optimize_and_save_navmesh("input.json", "output.json", "medium") - -# 创建优化流水线 -pipeline_result = navmesh_plugin.create_optimization_pipeline(['merge', 'simplify', 'remove_small']) -``` - -## 核心组件 - -### 导航网格管理器 -负责导航网格的加载、生成、管理和查询。 - -主要功能: -- 多边形数据管理(添加、删除、修改) -- 点位置查询(最近点、可行走性检查) -- 空间查询优化(空间哈希) -- 可视化显示控制 -- 动态障碍物管理 -- 网格验证和优化 - -### 路径查找器 -实现基于导航网格的多种寻路算法。 - -主要功能: -- A*、Dijkstra等多种寻路算法 -- 路径平滑和优化 -- 动态障碍物避让 -- 路径缓存机制 -- 性能统计和分析 -- 备选路径生成 - -### 导航网格编辑器 -提供可视化编辑导航网格的功能。 - -主要功能: -- 多种编辑工具(选择、添加、删除、绘制) -- 实时可视化反馈 -- 编辑历史管理(撤销/重做) -- 参数调节界面 -- 快捷键支持 -- 网格对齐和辅助功能 - -### 导航网格工具集 -提供导航网格的高级分析和优化功能。 - -主要功能: -- 网格质量分析 -- 性能评估 -- 优化建议 -- 自动优化 -- 格式转换 -- 批量处理 - -## 数据结构 - -### 导航网格多边形 -表示导航网格中的一个可行走区域,包含: -- 多边形顶点(任意数量) -- 相邻多边形信息 -- 门户点(与邻居的连接点) -- 中心点计算 -- 面积和边界框计算 -- 点在多边形内判断 -- 最近点计算 - -### 导航网格节点 -用于寻路算法中的节点表示,包含: -- 多边形ID -- 位置信息 -- 代价信息(g_cost, h_cost, f_cost) -- 父节点引用 -- 门户点入口信息 - -### 寻路路径 -表示计算出的路径,包含: -- 路径点列表 -- 路径长度和复杂度 -- 路径有效性验证 - -## 性能优化 - -### 算法优化 -- **高效数据结构**: 使用优先队列优化寻路算法 -- **启发式函数**: 智能的启发式代价计算 -- **节点剪枝**: 避免重复节点计算 -- **路径缓存**: 缓存常用路径提高查找速度 -- **空间哈希**: 加速多边形空间查询 - -### 内存管理 -- **对象池**: 减少对象创建和销毁开销 -- **数据压缩**: 优化导航网格数据存储 -- **批量处理**: 提高大量数据处理效率 - -### 渲染优化 -- **LOD支持**: 根据距离调整显示细节 -- **视锥剔除**: 只渲染可见区域 -- **批处理**: 合并相似的渲染调用 - -## 扩展开发 - -### 添加新的寻路算法 -1. 在[pathfinder.py](file:///home/hello/EG/plugins/user/navigation_mesh/tools/pathfinder.py)中实现新的算法类 -2. 继承现有寻路器或创建新的寻路器类 -3. 在插件主文件中集成新的算法 - -### 添加新的网格生成方法 -1. 在[navmesh_manager.py](file:///home/hello/EG/plugins/user/navigation_mesh/core/navmesh_manager.py)中实现新的生成方法 -2. 添加对应的GUI控制元素 -3. 在编辑器中添加对应的生成选项 - -### 添加新的编辑工具 -1. 在[navmesh_editor.py](file:///home/hello/EG/plugins/user/navigation_mesh/editor/navmesh_editor.py)中创建新的工具类 -2. 继承NavMeshEditorTool基类 -3. 实现工具的交互逻辑 -4. 在编辑器中注册新工具 - -### 添加新的分析工具 -1. 在[navmesh_analyzer.py](file:///home/hello/EG/plugins/user/navigation_mesh/tools/navmesh_analyzer.py)中实现新的分析功能 -2. 在[navmesh_tools.py](file:///home/hello/EG/plugins/user/navigation_mesh/tools/navmesh_tools.py)中集成新工具 -3. 在插件主文件中暴露新功能接口 - -## 故障排除 - -### 路径查找失败 -1. 检查导航网格是否正确生成和加载 -2. 确认起点和终点在可行走区域内 -3. 检查多边形连接是否正确 -4. 验证导航网格完整性(使用F6快捷键) - -### 性能问题 -1. 使用寻路统计功能分析性能瓶颈 -2. 调整启发式权重优化搜索效率 -3. 简化导航网格结构 -4. 启用空间哈希优化 -5. 调整最大搜索节点数限制 - -### 编辑器无法使用 -1. 确认GUI系统是否正常工作 -2. 检查快捷键是否被其他系统占用 -3. 重新启用插件尝试 -4. 检查是否有Python异常(查看控制台输出) - -### 网格生成问题 -1. 检查场景中是否有合适的几何体用于生成 -2. 调整网格生成参数(网格大小、代理尺寸等) -3. 使用测试网格功能验证基本功能 - -### 文件操作问题 -1. 检查文件路径是否正确 -2. 确认文件权限是否足够 -3. 验证文件格式是否支持 -4. 检查磁盘空间是否充足 - -## API参考 - -### Plugin类主要方法 - -#### 初始化和控制 -- `initialize()` - 初始化插件 -- `enable()` - 启用插件 -- `disable()` - 禁用插件 -- `finalize()` - 清理插件资源 - -#### 导航网格操作 -- `load_navmesh_from_file(filepath)` - 从文件加载导航网格 -- `save_navmesh_to_file(filepath)` - 保存导航网格到文件 -- `generate_navmesh_from_scene()` - 从场景生成导航网格 -- `rebuild_navmesh()` - 重新构建导航网格 - -#### 路径查找 -- `find_path(start_point, end_point)` - 查找两点间路径 -- `find_path_with_waypoints(waypoints)` - 查找经过多个路点的路径 -- `find_alternative_paths(start_point, end_point, count)` - 查找备选路径 - -#### 点查询 -- `is_point_walkable(point)` - 检查点是否可行走 -- `get_closest_point(point)` - 获取最近的可行走点 - -#### 动态障碍物 -- `add_dynamic_obstacle(position, radius, height)` - 添加动态障碍物 -- `remove_dynamic_obstacle(obstacle_id)` - 移除动态障碍物 - -#### 设置和配置 -- `set_navmesh_settings(settings)` - 设置导航网格参数 -- `get_navmesh_settings()` - 获取导航网格参数 -- `set_pathfinder_algorithm(algorithm)` - 设置寻路算法 -- `get_available_algorithms()` - 获取可用算法列表 -- `set_pathfinder_settings(settings)` - 设置寻路参数 -- `get_pathfinder_settings()` - 获取寻路参数 - -#### 性能和统计 -- `show_stats()` - 显示统计信息 -- `export_statistics(filepath)` - 导出统计信息 -- `clear_path_cache()` - 清空路径缓存 - -#### 优化和验证 -- `validate_navmesh()` - 验证导航网格 -- `optimize_navmesh()` - 优化导航网格 -- `enable_spatial_hashing(enabled)` - 启用/禁用空间哈希 -- `set_spatial_hash_cell_size(size)` - 设置空间哈希网格大小 - -#### 高级分析功能 -- `analyze_mesh_quality()` - 分析网格质量 -- `suggest_optimizations()` - 提供优化建议 -- `analyze_pathfinding_performance(sample_count)` - 分析寻路性能 -- `detect_navigation_bottlenecks()` - 检测瓶颈区域 -- `analyze_coverage()` - 分析覆盖情况 -- `generate_analysis_report()` - 生成分析报告 -- `export_analysis_report(filepath)` - 导出分析报告 -- `get_mesh_complexity_score()` - 获取网格复杂度评分 -- `recommend_mesh_simplification()` - 推荐网格简化策略 - -#### 网格优化功能 -- `merge_adjacent_polygons()` - 合并相邻多边形 -- `simplify_mesh_polygons()` - 简化多边形 -- `remove_small_mesh_polygons()` - 移除小多边形 -- `optimize_navigation_mesh(optimization_level)` - 优化导航网格 -- `set_optimization_settings(settings)` - 设置优化参数 -- `get_optimization_settings()` - 获取优化参数 -- `fill_mesh_gaps()` - 填补网格空隙 -- `smooth_mesh_boundaries()` - 平滑边界 -- `balance_polygon_sizes()` - 平衡多边形大小 - -#### 序列化和文件操作 -- `serialize_navigation_mesh()` - 序列化导航网格 -- `deserialize_navigation_mesh(data_str)` - 反序列化导航网格 -- `save_navmesh_to_file_advanced(filepath, compress, pretty_print)` - 保存网格(高级选项) -- `load_navmesh_from_file_advanced(filepath)` - 加载网格(高级选项) -- `export_to_standard_format(filepath, format_type)` - 导出为标准格式 -- `import_from_standard_format(filepath, format_type)` - 从标准格式导入 -- `get_navmesh_file_info(filepath)` - 获取文件信息 -- `batch_analyze_navmeshes(filepaths)` - 批量分析网格 -- `compare_navigation_meshes(navmesh1_path, navmesh2_path)` - 比较网格 -- `validate_navmesh_file(filepath)` - 验证网格文件 -- `optimize_and_save_navmesh(input_filepath, output_filepath, optimization_level)` - 优化并保存 -- `create_optimization_pipeline(steps)` - 创建优化流水线 - -## 性能调优建议 - -### 寻路性能 -1. 合理设置启发式权重(通常1.0为最佳) -2. 限制最大搜索节点数避免长时间搜索 -3. 启用路径缓存减少重复计算 -4. 使用适当的空间查询优化 - -### 内存使用 -1. 及时清理不用的路径缓存 -2. 控制导航网格复杂度 -3. 合理设置空间哈希网格大小 - -### 渲染性能 -1. 只在需要时显示导航网格 -2. 使用LOD减少远处细节 -3. 批量处理渲染调用 - -### 网格优化 -1. 定期分析网格质量 -2. 根据建议进行优化 -3. 平衡网格精度和性能 -4. 移除不必要的细节 - -## 版本信息 - -- 版本: 1.0.0 -- 作者: EG Team -- 发布日期: 2024年 - -## 许可证 - +# 导航网格插件 + +一个功能完整的3D导航网格系统插件,为EG引擎提供基于导航网格的寻路和路径规划功能。 + +## 功能特性 + +### 核心功能 +- **导航网格生成**: 支持从3D场景自动生成导航网格 +- **路径查找**: 基于A*算法的高效寻路 +- **可视化编辑**: 直观的导航网格编辑界面 +- **多边形管理**: 导航网格多边形的增删改查 +- **路径可视化**: 实时显示寻路结果 + +### 寻路算法 +- **A*算法**: 经典的启发式搜索算法 +- **Dijkstra算法**: 保证最短路径的搜索算法 +- **多种启发式函数**: 欀曼哈顿距离、欧几里得距离等 +- **路径平滑**: 漏斗算法和简单平滑算法 +- **多路点路径**: 支持经过多个路点的复杂路径规划 +- **动态障碍物**: 运行时添加和移除障碍物 +- **路径缓存**: 提高重复路径查找性能 + +### 编辑功能 +- **可视化编辑**: 图形化界面编辑导航网格 +- **多种工具**: 选择、添加、删除、绘制工具 +- **属性调节**: 网格大小、代理尺寸等参数调节 +- **撤销重做**: 编辑历史管理和操作回退 +- **网格对齐**: 网格对齐功能提高编辑精度 +- **自动连接**: 自动计算多边形连接关系 + +### 性能优化 +- **空间哈希**: 加速多边形查找 +- **对象复用**: 减少内存分配 +- **批量处理**: 提高大量数据处理效率 +- **LOD支持**: 多层次细节支持 + +### 高级特性 +- **分层寻路**: 大场景高效寻路 +- **路径复杂度分析**: 路径质量评估 +- **备选路径**: 多条路径选择 +- **路径验证**: 路径有效性检查 +- **统计信息**: 详细的性能和使用统计 +- **网格分析**: 质量分析和优化建议 +- **网格优化**: 自动优化和简化功能 +- **格式支持**: 多种文件格式导入导出 + +## 使用方法 + +### 快捷键操作 +- **F1**: 切换导航网格编辑器显示 +- **F2**: 切换导航网格可见性 +- **F3**: 查找随机路径进行测试 +- **F4**: 清除所有路径可视化 +- **F5**: 从场景生成导航网格 +- **F6**: 验证导航网格完整性 +- **F7**: 优化导航网格 +- **F8**: 显示统计信息 +- **F9**: 分析导航网格质量 +- **F10**: 导出分析报告 +- **F11**: 简化导航网格 +- **F12**: 检测瓶颈区域 + +### GUI界面操作 +插件会在屏幕左上角创建控制面板,可以通过按钮直接操作导航网格功能。 + +### 编程接口 +```python +# 获取插件实例 +navmesh_plugin = world.plugin_manager.get_plugin("navigation_mesh") + +# 生成导航网格 +navmesh_plugin.generate_navmesh_from_scene() + +# 从文件加载导航网格 +navmesh_plugin.load_navmesh_from_file("path/to/navmesh.json") + +# 保存导航网格到文件 +navmesh_plugin.save_navmesh_to_file("path/to/navmesh.json") + +# 查找路径 +start_point = Point3(0, 0, 0) +end_point = Point3(10, 0, 10) +path = navmesh_plugin.find_path(start_point, end_point) + +# 检查点是否可行走 +is_walkable = navmesh_plugin.is_point_walkable(Point3(5, 0, 5)) + +# 获取最近的可行走点 +closest_point = navmesh_plugin.get_closest_point(Point3(3, 2, 4)) + +# 添加动态障碍物 +obstacle_id = navmesh_plugin.add_dynamic_obstacle(Point3(5, 0, 5), 1.0, 2.0) + +# 移除动态障碍物 +navmesh_plugin.remove_dynamic_obstacle(obstacle_id) + +# 设置导航网格参数 +navmesh_plugin.set_navmesh_settings({ + 'cell_size': 0.3, + 'agent_height': 1.8, + 'agent_radius': 0.4 +}) + +# 设置寻路算法 +navmesh_plugin.set_pathfinder_algorithm('astar') # 或 'dijkstra' + +# 查找多路点路径 +waypoints = [Point3(0, 0, 0), Point3(5, 0, 5), Point3(10, 0, 10)] +path = navmesh_plugin.find_path_with_waypoints(waypoints) + +# 估算路径代价 +cost = navmesh_plugin.estimate_path_cost(Point3(0, 0, 0), Point3(10, 0, 10)) + +# 验证路径有效性 +is_valid = navmesh_plugin.validate_path(path) + +# 分析路径复杂度 +complexity = navmesh_plugin.get_path_complexity(path) + +# 查找备选路径 +alternative_paths = navmesh_plugin.find_alternative_paths(Point3(0, 0, 0), Point3(10, 0, 10), 3) + +# 清空路径缓存 +navmesh_plugin.clear_path_cache() + +# 重新构建导航网格 +navmesh_plugin.rebuild_navmesh() + +# 启用空间哈希优化 +navmesh_plugin.enable_spatial_hashing(True) + +# 设置空间哈希网格大小 +navmesh_plugin.set_spatial_hash_cell_size(5.0) + +# 导出统计信息 +navmesh_plugin.export_statistics("stats.json") + +# 导入/导出设置 +navmesh_plugin.import_settings("settings.json") +navmesh_plugin.export_settings("settings.json") + +# 高级功能 +# 分析网格质量 +quality_report = navmesh_plugin.analyze_mesh_quality() + +# 获取优化建议 +suggestions = navmesh_plugin.suggest_optimizations() + +# 检测瓶颈区域 +bottlenecks = navmesh_plugin.detect_navigation_bottlenecks() + +# 生成完整分析报告 +report = navmesh_plugin.generate_analysis_report() + +# 导出分析报告 +navmesh_plugin.export_analysis_report("analysis_report.json") + +# 获取网格复杂度评分 +complexity_score = navmesh_plugin.get_mesh_complexity_score() + +# 推荐网格简化策略 +simplification_recommendation = navmesh_plugin.recommend_mesh_simplification() + +# 合并相邻多边形 +merged_count = navmesh_plugin.merge_adjacent_polygons() + +# 简化多边形 +simplified_count = navmesh_plugin.simplify_mesh_polygons() + +# 移除小多边形 +removed_count = navmesh_plugin.remove_small_mesh_polygons() + +# 执行完整网格优化 +optimization_result = navmesh_plugin.optimize_navigation_mesh('medium') + +# 设置优化参数 +navmesh_plugin.set_optimization_settings({ + 'merge_threshold': 0.5, + 'simplify_threshold': 0.1, + 'min_polygon_area': 0.01 +}) + +# 获取优化参数 +optimization_settings = navmesh_plugin.get_optimization_settings() + +# 填补网格空隙 +filled_gaps = navmesh_plugin.fill_mesh_gaps() + +# 平滑边界 +smoothed_edges = navmesh_plugin.smooth_mesh_boundaries() + +# 平衡多边形大小 +balanced_polygons = navmesh_plugin.balance_polygon_sizes() + +# 序列化导航网格 +serialized_data = navmesh_plugin.serialize_navigation_mesh() + +# 反序列化导航网格 +success = navmesh_plugin.deserialize_navigation_mesh(serialized_data) + +# 保存导航网格(高级选项) +navmesh_plugin.save_navmesh_to_file_advanced("navmesh.json", compress=True, pretty_print=True) + +# 加载导航网格(高级选项) +navmesh_plugin.load_navmesh_from_file_advanced("navmesh.json") + +# 导出为标准格式 +navmesh_plugin.export_to_standard_format("navmesh.obj", "obj") + +# 从标准格式导入 +navmesh_plugin.import_from_standard_format("navmesh.obj", "obj") + +# 获取文件信息 +file_info = navmesh_plugin.get_navmesh_file_info("navmesh.json") + +# 批量分析多个网格文件 +analysis_results = navmesh_plugin.batch_analyze_navmeshes(["navmesh1.json", "navmesh2.json"]) + +# 比较两个导航网格 +comparison_result = navmesh_plugin.compare_navigation_meshes("navmesh1.json", "navmesh2.json") + +# 验证网格文件 +validation_result = navmesh_plugin.validate_navmesh_file("navmesh.json") + +# 优化并保存网格 +optimization_result = navmesh_plugin.optimize_and_save_navmesh("input.json", "output.json", "medium") + +# 创建优化流水线 +pipeline_result = navmesh_plugin.create_optimization_pipeline(['merge', 'simplify', 'remove_small']) +``` + +## 核心组件 + +### 导航网格管理器 +负责导航网格的加载、生成、管理和查询。 + +主要功能: +- 多边形数据管理(添加、删除、修改) +- 点位置查询(最近点、可行走性检查) +- 空间查询优化(空间哈希) +- 可视化显示控制 +- 动态障碍物管理 +- 网格验证和优化 + +### 路径查找器 +实现基于导航网格的多种寻路算法。 + +主要功能: +- A*、Dijkstra等多种寻路算法 +- 路径平滑和优化 +- 动态障碍物避让 +- 路径缓存机制 +- 性能统计和分析 +- 备选路径生成 + +### 导航网格编辑器 +提供可视化编辑导航网格的功能。 + +主要功能: +- 多种编辑工具(选择、添加、删除、绘制) +- 实时可视化反馈 +- 编辑历史管理(撤销/重做) +- 参数调节界面 +- 快捷键支持 +- 网格对齐和辅助功能 + +### 导航网格工具集 +提供导航网格的高级分析和优化功能。 + +主要功能: +- 网格质量分析 +- 性能评估 +- 优化建议 +- 自动优化 +- 格式转换 +- 批量处理 + +## 数据结构 + +### 导航网格多边形 +表示导航网格中的一个可行走区域,包含: +- 多边形顶点(任意数量) +- 相邻多边形信息 +- 门户点(与邻居的连接点) +- 中心点计算 +- 面积和边界框计算 +- 点在多边形内判断 +- 最近点计算 + +### 导航网格节点 +用于寻路算法中的节点表示,包含: +- 多边形ID +- 位置信息 +- 代价信息(g_cost, h_cost, f_cost) +- 父节点引用 +- 门户点入口信息 + +### 寻路路径 +表示计算出的路径,包含: +- 路径点列表 +- 路径长度和复杂度 +- 路径有效性验证 + +## 性能优化 + +### 算法优化 +- **高效数据结构**: 使用优先队列优化寻路算法 +- **启发式函数**: 智能的启发式代价计算 +- **节点剪枝**: 避免重复节点计算 +- **路径缓存**: 缓存常用路径提高查找速度 +- **空间哈希**: 加速多边形空间查询 + +### 内存管理 +- **对象池**: 减少对象创建和销毁开销 +- **数据压缩**: 优化导航网格数据存储 +- **批量处理**: 提高大量数据处理效率 + +### 渲染优化 +- **LOD支持**: 根据距离调整显示细节 +- **视锥剔除**: 只渲染可见区域 +- **批处理**: 合并相似的渲染调用 + +## 扩展开发 + +### 添加新的寻路算法 +1. 在[pathfinder.py](file:///home/hello/EG/plugins/user/navigation_mesh/tools/pathfinder.py)中实现新的算法类 +2. 继承现有寻路器或创建新的寻路器类 +3. 在插件主文件中集成新的算法 + +### 添加新的网格生成方法 +1. 在[navmesh_manager.py](file:///home/hello/EG/plugins/user/navigation_mesh/core/navmesh_manager.py)中实现新的生成方法 +2. 添加对应的GUI控制元素 +3. 在编辑器中添加对应的生成选项 + +### 添加新的编辑工具 +1. 在[navmesh_editor.py](file:///home/hello/EG/plugins/user/navigation_mesh/editor/navmesh_editor.py)中创建新的工具类 +2. 继承NavMeshEditorTool基类 +3. 实现工具的交互逻辑 +4. 在编辑器中注册新工具 + +### 添加新的分析工具 +1. 在[navmesh_analyzer.py](file:///home/hello/EG/plugins/user/navigation_mesh/tools/navmesh_analyzer.py)中实现新的分析功能 +2. 在[navmesh_tools.py](file:///home/hello/EG/plugins/user/navigation_mesh/tools/navmesh_tools.py)中集成新工具 +3. 在插件主文件中暴露新功能接口 + +## 故障排除 + +### 路径查找失败 +1. 检查导航网格是否正确生成和加载 +2. 确认起点和终点在可行走区域内 +3. 检查多边形连接是否正确 +4. 验证导航网格完整性(使用F6快捷键) + +### 性能问题 +1. 使用寻路统计功能分析性能瓶颈 +2. 调整启发式权重优化搜索效率 +3. 简化导航网格结构 +4. 启用空间哈希优化 +5. 调整最大搜索节点数限制 + +### 编辑器无法使用 +1. 确认GUI系统是否正常工作 +2. 检查快捷键是否被其他系统占用 +3. 重新启用插件尝试 +4. 检查是否有Python异常(查看控制台输出) + +### 网格生成问题 +1. 检查场景中是否有合适的几何体用于生成 +2. 调整网格生成参数(网格大小、代理尺寸等) +3. 使用测试网格功能验证基本功能 + +### 文件操作问题 +1. 检查文件路径是否正确 +2. 确认文件权限是否足够 +3. 验证文件格式是否支持 +4. 检查磁盘空间是否充足 + +## API参考 + +### Plugin类主要方法 + +#### 初始化和控制 +- `initialize()` - 初始化插件 +- `enable()` - 启用插件 +- `disable()` - 禁用插件 +- `finalize()` - 清理插件资源 + +#### 导航网格操作 +- `load_navmesh_from_file(filepath)` - 从文件加载导航网格 +- `save_navmesh_to_file(filepath)` - 保存导航网格到文件 +- `generate_navmesh_from_scene()` - 从场景生成导航网格 +- `rebuild_navmesh()` - 重新构建导航网格 + +#### 路径查找 +- `find_path(start_point, end_point)` - 查找两点间路径 +- `find_path_with_waypoints(waypoints)` - 查找经过多个路点的路径 +- `find_alternative_paths(start_point, end_point, count)` - 查找备选路径 + +#### 点查询 +- `is_point_walkable(point)` - 检查点是否可行走 +- `get_closest_point(point)` - 获取最近的可行走点 + +#### 动态障碍物 +- `add_dynamic_obstacle(position, radius, height)` - 添加动态障碍物 +- `remove_dynamic_obstacle(obstacle_id)` - 移除动态障碍物 + +#### 设置和配置 +- `set_navmesh_settings(settings)` - 设置导航网格参数 +- `get_navmesh_settings()` - 获取导航网格参数 +- `set_pathfinder_algorithm(algorithm)` - 设置寻路算法 +- `get_available_algorithms()` - 获取可用算法列表 +- `set_pathfinder_settings(settings)` - 设置寻路参数 +- `get_pathfinder_settings()` - 获取寻路参数 + +#### 性能和统计 +- `show_stats()` - 显示统计信息 +- `export_statistics(filepath)` - 导出统计信息 +- `clear_path_cache()` - 清空路径缓存 + +#### 优化和验证 +- `validate_navmesh()` - 验证导航网格 +- `optimize_navmesh()` - 优化导航网格 +- `enable_spatial_hashing(enabled)` - 启用/禁用空间哈希 +- `set_spatial_hash_cell_size(size)` - 设置空间哈希网格大小 + +#### 高级分析功能 +- `analyze_mesh_quality()` - 分析网格质量 +- `suggest_optimizations()` - 提供优化建议 +- `analyze_pathfinding_performance(sample_count)` - 分析寻路性能 +- `detect_navigation_bottlenecks()` - 检测瓶颈区域 +- `analyze_coverage()` - 分析覆盖情况 +- `generate_analysis_report()` - 生成分析报告 +- `export_analysis_report(filepath)` - 导出分析报告 +- `get_mesh_complexity_score()` - 获取网格复杂度评分 +- `recommend_mesh_simplification()` - 推荐网格简化策略 + +#### 网格优化功能 +- `merge_adjacent_polygons()` - 合并相邻多边形 +- `simplify_mesh_polygons()` - 简化多边形 +- `remove_small_mesh_polygons()` - 移除小多边形 +- `optimize_navigation_mesh(optimization_level)` - 优化导航网格 +- `set_optimization_settings(settings)` - 设置优化参数 +- `get_optimization_settings()` - 获取优化参数 +- `fill_mesh_gaps()` - 填补网格空隙 +- `smooth_mesh_boundaries()` - 平滑边界 +- `balance_polygon_sizes()` - 平衡多边形大小 + +#### 序列化和文件操作 +- `serialize_navigation_mesh()` - 序列化导航网格 +- `deserialize_navigation_mesh(data_str)` - 反序列化导航网格 +- `save_navmesh_to_file_advanced(filepath, compress, pretty_print)` - 保存网格(高级选项) +- `load_navmesh_from_file_advanced(filepath)` - 加载网格(高级选项) +- `export_to_standard_format(filepath, format_type)` - 导出为标准格式 +- `import_from_standard_format(filepath, format_type)` - 从标准格式导入 +- `get_navmesh_file_info(filepath)` - 获取文件信息 +- `batch_analyze_navmeshes(filepaths)` - 批量分析网格 +- `compare_navigation_meshes(navmesh1_path, navmesh2_path)` - 比较网格 +- `validate_navmesh_file(filepath)` - 验证网格文件 +- `optimize_and_save_navmesh(input_filepath, output_filepath, optimization_level)` - 优化并保存 +- `create_optimization_pipeline(steps)` - 创建优化流水线 + +## 性能调优建议 + +### 寻路性能 +1. 合理设置启发式权重(通常1.0为最佳) +2. 限制最大搜索节点数避免长时间搜索 +3. 启用路径缓存减少重复计算 +4. 使用适当的空间查询优化 + +### 内存使用 +1. 及时清理不用的路径缓存 +2. 控制导航网格复杂度 +3. 合理设置空间哈希网格大小 + +### 渲染性能 +1. 只在需要时显示导航网格 +2. 使用LOD减少远处细节 +3. 批量处理渲染调用 + +### 网格优化 +1. 定期分析网格质量 +2. 根据建议进行优化 +3. 平衡网格精度和性能 +4. 移除不必要的细节 + +## 版本信息 + +- 版本: 1.0.0 +- 作者: EG Team +- 发布日期: 2024年 + +## 许可证 + 本插件遵循EG引擎许可证协议。 \ No newline at end of file diff --git a/plugins/user/navigation_mesh/core/navmesh_manager.py b/plugins/user/navigation_mesh/core/navmesh_manager.py index db1540ce..48b33fcf 100644 --- a/plugins/user/navigation_mesh/core/navmesh_manager.py +++ b/plugins/user/navigation_mesh/core/navmesh_manager.py @@ -1,1639 +1,1639 @@ -""" -导航网格管理器 -负责导航网格的加载、生成、管理和查询 -""" - -import math -import json -import random -from typing import List, Tuple, Optional, Dict, Any, Set -from panda3d.core import Vec3, Point3, NodePath, GeomNode, Geom, GeomVertexFormat -from panda3d.core import GeomVertexData, GeomVertexWriter, GeomTriangles, GeomLines -from panda3d.core import LColor, Material, BitMask32 -from direct.actor.Actor import Actor - -class NavMeshPolygon: - """ - 导航网格多边形 - 表示导航网格中的一个可行走区域 - """ - - def __init__(self, polygon_id: int, vertices: List[Point3]): - self.polygon_id = polygon_id - self.vertices = vertices # 多边形顶点列表 - self.neighbors: List[int] = [] # 相邻多边形ID列表 - self.center = self._calculate_center() # 多边形中心点 - self.walkable = True # 是否可行走 - self.portal_points: Dict[int, Tuple[Point3, Point3]] = {} # 与其他多边形的门户点 - self.area = self._calculate_area() # 多边形面积 - self.bounding_box = self._calculate_bounding_box() # 边界框 - self.flags = 0 # 标志位,用于存储额外信息 - - def _calculate_center(self) -> Point3: - """计算多边形中心点""" - if not self.vertices: - return Point3(0, 0, 0) - - center = Point3(0, 0, 0) - for vertex in self.vertices: - center += vertex - center /= len(self.vertices) - return center - - def _calculate_area(self) -> float: - """计算多边形面积(使用鞋带公式)""" - if len(self.vertices) < 3: - return 0.0 - - area = 0.0 - n = len(self.vertices) - for i in range(n): - j = (i + 1) % n - area += self.vertices[i].x * self.vertices[j].z - area -= self.vertices[j].x * self.vertices[i].z - return abs(area) / 2.0 - - def _calculate_bounding_box(self) -> Tuple[Point3, Point3]: - """计算边界框(最小点和最大点)""" - if not self.vertices: - return Point3(0, 0, 0), Point3(0, 0, 0) - - min_x = min(v.x for v in self.vertices) - min_y = min(v.y for v in self.vertices) - min_z = min(v.z for v in self.vertices) - max_x = max(v.x for v in self.vertices) - max_y = max(v.y for v in self.vertices) - max_z = max(v.z for v in self.vertices) - - return Point3(min_x, min_y, min_z), Point3(max_x, max_y, max_z) - - def add_neighbor(self, neighbor_id: int): - """添加相邻多边形""" - if neighbor_id not in self.neighbors: - self.neighbors.append(neighbor_id) - - def remove_neighbor(self, neighbor_id: int): - """移除相邻多边形""" - if neighbor_id in self.neighbors: - self.neighbors.remove(neighbor_id) - - def set_portal_points(self, neighbor_id: int, left_point: Point3, right_point: Point3): - """设置与邻居多边形的门户点""" - self.portal_points[neighbor_id] = (left_point, right_point) - - def get_portal_points(self, neighbor_id: int) -> Optional[Tuple[Point3, Point3]]: - """获取与邻居多边形的门户点""" - return self.portal_points.get(neighbor_id) - - def is_point_inside(self, point: Point3) -> bool: - """检查点是否在多边形内(射线投射算法)""" - # 使用射线投射算法判断点是否在多边形内 - x, z = point.x, point.z - n = len(self.vertices) - inside = False - - p1x, p1z = self.vertices[0].x, self.vertices[0].z - for i in range(1, n + 1): - p2x, p2z = self.vertices[i % n].x, self.vertices[i % n].z - if z > min(p1z, p2z): - if z <= max(p1z, p2z): - if x <= max(p1x, p2x): - if p1z != p2z: - xinters = (z - p1z) * (p2x - p1x) / (p2z - p1z) + p1x - if p1x == p2x or x <= xinters: - inside = not inside - p1x, p1z = p2x, p2z - - return inside - - def is_point_in_bounding_box(self, point: Point3) -> bool: - """快速检查点是否在边界框内""" - min_point, max_point = self.bounding_box - return (min_point.x <= point.x <= max_point.x and - min_point.y <= point.y <= max_point.y and - min_point.z <= point.z <= max_point.z) - - def get_distance_to_point(self, point: Point3) -> float: - """计算到点的距离""" - return (self.center - point).length() - - def get_closest_point_on_boundary(self, point: Point3) -> Point3: - """获取多边形边界上距离给定点最近的点""" - if not self.vertices: - return point - - closest_point = None - min_distance = float('inf') - - # 检查每条边 - for i in range(len(self.vertices)): - start = self.vertices[i] - end = self.vertices[(i + 1) % len(self.vertices)] - - # 计算点到线段的最近点 - segment_vec = end - start - point_vec = point - start - segment_len_sq = segment_vec.lengthSquared() - - if segment_len_sq == 0: - # 线段退化为点 - closest = start - else: - t = point_vec.dot(segment_vec) / segment_len_sq - t = max(0, min(1, t)) # 限制在[0,1]范围内 - closest = start + segment_vec * t - - distance = (point - closest).length() - if distance < min_distance: - min_distance = distance - closest_point = closest - - return closest_point if closest_point else self.center - - def get_closest_point(self, point: Point3) -> Point3: - """获取多边形上距离给定点最近的点""" - # 如果点在多边形内,返回该点 - if self.is_point_inside(point): - return point - # 否则返回边界上的最近点 - return self.get_closest_point_on_boundary(point) - - def intersects_with_ray(self, ray_origin: Point3, ray_direction: Vec3) -> Optional[Point3]: - """检查与射线的交点""" - # 简化实现:只检查与多边形平面的交点 - if len(self.vertices) < 3: - return None - - # 计算法平面的法向量 - edge1 = self.vertices[1] - self.vertices[0] - edge2 = self.vertices[2] - self.vertices[0] - normal = edge1.cross(edge2) - normal.normalize() - - # 计算射线与平面的交点 - denom = normal.dot(ray_direction) - if abs(denom) < 1e-6: # 射线与平面平行 - return None - - t = (normal.dot(Vec3(self.vertices[0] - ray_origin))) / denom - if t < 0: # 交点在射线起点后面 - return None - - intersection = ray_origin + ray_direction * t - - # 检查交点是否在多边形内 - if self.is_point_inside(intersection): - return intersection - - return None - - def get_vertices(self) -> List[Point3]: - """获取多边形顶点""" - return self.vertices.copy() - - def get_neighbors(self) -> List[int]: - """获取邻居列表""" - return self.neighbors.copy() - - def get_portal_point_to_neighbor(self, neighbor_id: int) -> Optional[Tuple[Point3, Point3]]: - """获取到指定邻居的门户点""" - return self.portal_points.get(neighbor_id) - -class NavMeshManager: - """ - 导航网格管理器 - 负责整个导航网格系统的管理 - """ - - def __init__(self, world): - """ - 初始化导航网格管理器 - - Args: - world: 3D世界对象 - """ - self.world = world - self.enabled = False - - # 导航网格数据 - self.polygons: Dict[int, NavMeshPolygon] = {} # 多边形字典 - self.polygon_counter = 0 - self.navmesh_node = None # 可视化节点 - self.path_visualization_node = None # 路径可视化节点 - self.obstacles_node = None # 障碍物可视化节点 - - # 导航网格设置 - self.cell_size = 0.5 # 网格单元大小 - self.cell_height = 0.2 # 网格高度 - self.agent_height = 1.8 # 代理高度 - self.agent_radius = 0.3 # 代理半径 - self.max_slope = 45.0 # 最大坡度(度) - self.max_climb = 0.2 # 最大可爬升高度 - self.min_region_area = 3 # 最小区域面积 - self.merge_region_area = 20 # 合并区域面积阈值 - self.edge_max_len = 12.0 # 边最大长度 - self.edge_max_error = 1.3 # 边最大误差 - self.verts_per_poly = 6 # 每个多边形最大顶点数 - self.walkable = True # 是否启用 - - # 可视化设置 - self.visualization_enabled = True - self.navmesh_visible = False - self.path_visualization_enabled = True - self.show_portals = False # 显示门户点 - self.show_normals = False # 显示法向量 - - # 性能优化 - self.spatial_hash_grid = {} # 空间哈希网格,用于快速查找 - self.spatial_hash_cell_size = 5.0 # 空间哈希网格单元大小 - self.use_spatial_hashing = True # 是否使用空间哈希 - - # 导航网格统计 - self.stats = { - 'polygons': 0, - 'vertices': 0, - 'edges': 0, - 'portals': 0, - 'obstacles': 0, - 'last_generation_time': 0.0 - } - - # 障碍物管理 - self.obstacles: Dict[int, Dict[str, Any]] = {} # 动态障碍物 - self.obstacle_counter = 0 - - # 导航网格版本和元数据 - self.version = "1.0.0" - self.generator = "EG Navigation Mesh System" - self.created_time = "" - self.modified_time = "" - - print("✓ 导航网格管理器初始化完成") - - def enable(self): - """启用导航网格管理器""" - if self.enabled: - return - - self.enabled = True - - # 创建可视化节点 - self._create_visualization_nodes() - - print("✓ 导航网格管理器已启用") - - def disable(self): - """禁用导航网格管理器""" - if not self.enabled: - return - - self.enabled = False - - # 清理可视化节点 - self._cleanup_visualization_nodes() - - print("✓ 导航网格管理器已禁用") - - def cleanup(self): - """清理资源""" - self.disable() - self.polygons.clear() - self.obstacles.clear() - self.spatial_hash_grid.clear() - - def _create_visualization_nodes(self): - """创建可视化节点""" - if not self.world or not self.world.render: - return - - # 导航网格可视化节点 - self.navmesh_node = self.world.render.attachNewNode("navmesh_visualization") - self.navmesh_node.setTransparency(True) - self.navmesh_node.setColor(LColor(0.0, 0.8, 0.0, 0.3)) # 半透明绿色 - - # 路径可视化节点 - self.path_visualization_node = self.world.render.attachNewNode("path_visualization") - self.path_visualization_node.setTransparency(True) - - # 障碍物可视化节点 - self.obstacles_node = self.world.render.attachNewNode("obstacles_visualization") - self.obstacles_node.setTransparency(True) - self.obstacles_node.setColor(LColor(1.0, 0.0, 0.0, 0.5)) # 半透明红色 - - # 默认隐藏 - self.navmesh_node.hide() - self.path_visualization_node.hide() - self.obstacles_node.hide() - - def _cleanup_visualization_nodes(self): - """清理可视化节点""" - if self.navmesh_node: - self.navmesh_node.removeNode() - self.navmesh_node = None - - if self.path_visualization_node: - self.path_visualization_node.removeNode() - self.path_visualization_node = None - - if self.obstacles_node: - self.obstacles_node.removeNode() - self.obstacles_node = None - - def toggle_visibility(self): - """切换导航网格可见性""" - if not self.navmesh_node: - return - - if self.navmesh_visible: - self.navmesh_node.hide() - self.navmesh_visible = False - print("✓ 导航网格已隐藏") - else: - self.navmesh_node.show() - self.navmesh_visible = True - print("✓ 导航网格已显示") - - # 更新可视化 - self._update_navmesh_visualization() - - def _update_spatial_hash(self): - """更新空间哈希网格""" - if not self.use_spatial_hashing: - return - - # 清空现有哈希网格 - self.spatial_hash_grid.clear() - - # 为每个多边形计算哈希键 - for polygon_id, polygon in self.polygons.items(): - if not polygon.vertices: - continue - - # 计算多边形的边界框 - min_point, max_point = polygon.bounding_box - - # 计算覆盖的哈希网格单元 - min_x = int(min_point.x / self.spatial_hash_cell_size) - min_z = int(min_point.z / self.spatial_hash_cell_size) - max_x = int(max_point.x / self.spatial_hash_cell_size) - max_z = int(max_point.z / self.spatial_hash_cell_size) - - # 将多边形添加到所有覆盖的单元格中 - for x in range(min_x, max_x + 1): - for z in range(min_z, max_z + 1): - key = (x, z) - if key not in self.spatial_hash_grid: - self.spatial_hash_grid[key] = [] - self.spatial_hash_grid[key].append(polygon_id) - - def _get_polygons_in_radius(self, center: Point3, radius: float) -> List[int]: - """获取指定半径内的多边形""" - if self.use_spatial_hashing: - # 使用空间哈希加速查找 - result = set() - - # 计算覆盖的哈希网格单元 - min_x = int((center.x - radius) / self.spatial_hash_cell_size) - min_z = int((center.z - radius) / self.spatial_hash_cell_size) - max_x = int((center.x + radius) / self.spatial_hash_cell_size) - max_z = int((center.z + radius) / self.spatial_hash_cell_size) - - # 检查所有覆盖的单元格 - for x in range(min_x, max_x + 1): - for z in range(min_z, max_z + 1): - key = (x, z) - if key in self.spatial_hash_grid: - for polygon_id in self.spatial_hash_grid[key]: - polygon = self.polygons[polygon_id] - # 精确检查距离 - if polygon.get_distance_to_point(center) <= radius: - result.add(polygon_id) - - return list(result) - else: - # 暴力查找 - result = [] - radius_sq = radius * radius - for polygon_id, polygon in self.polygons.items(): - dist_sq = (polygon.center - center).lengthSquared() - if dist_sq <= radius_sq: - result.append(polygon_id) - return result - - def _update_navmesh_visualization(self): - """更新导航网格可视化""" - if not self.navmesh_node or not self.navmesh_visible: - return - - # 清除现有几何体 - self.navmesh_node.node().removeAllGeoms() - - if not self.polygons: - return - - # 创建新的几何体 - self._create_navmesh_geometry() - - # 如果需要,创建门户点可视化 - if self.show_portals: - self._create_portals_geometry() - - # 如果需要,创建法向量可视化 - if self.show_normals: - self._create_normals_geometry() - - def _create_navmesh_geometry(self): - """创建导航网格几何体""" - if not self.navmesh_node or not self.polygons: - return - - # 创建顶点格式 - vertex_format = GeomVertexFormat.getV3n3c4() - vertex_data = GeomVertexData("navmesh", vertex_format, Geom.UHStatic) - - # 计算总顶点数(每个三角形3个顶点) - total_vertices = 0 - for polygon in self.polygons.values(): - if len(polygon.vertices) >= 3: - # 简单三角剖分(假设多边形是凸的) - total_vertices += (len(polygon.vertices) - 2) * 3 - - if total_vertices == 0: - return - - vertex_data.setNumRows(total_vertices) - - # 创建顶点写入器 - vertex_writer = GeomVertexWriter(vertex_data, "vertex") - normal_writer = GeomVertexWriter(vertex_data, "normal") - color_writer = GeomVertexWriter(vertex_data, "color") - - # 创建几何体 - geom = Geom(vertex_data) - triangles = GeomTriangles(Geom.UHStatic) - - vertex_index = 0 - for polygon in self.polygons.values(): - if len(polygon.vertices) < 3: - continue - - # 计算法平面的法向量 - if len(polygon.vertices) >= 3: - edge1 = polygon.vertices[1] - polygon.vertices[0] - edge2 = polygon.vertices[2] - polygon.vertices[0] - normal = edge1.cross(edge2) - if normal.length() > 0: - normal.normalize() - else: - normal = Vec3(0, 1, 0) - else: - normal = Vec3(0, 1, 0) - - # 简单三角剖分(扇形剖分) - for i in range(1, len(polygon.vertices) - 1): - # 三角形顶点索引 - indices = [0, i, i + 1] - - # 写入顶点数据 - for idx in indices: - vertex = polygon.vertices[idx] - vertex_writer.addData3f(vertex.x, vertex.y, vertex.z) - normal_writer.addData3f(normal.x, normal.y, normal.z) - color_writer.addData4f(0.0, 0.8, 0.0, 0.3) # 绿色半透明 - - # 添加三角形索引 - triangles.addVertex(vertex_index) - triangles.addVertex(vertex_index + 1) - triangles.addVertex(vertex_index + 2) - triangles.closePrimitive() - - vertex_index += 3 - - geom.addPrimitive(triangles) - self.navmesh_node.node().addGeom(geom) - - def _create_portals_geometry(self): - """创建门户点几何体""" - if not self.navmesh_node or not self.polygons: - return - - # 创建顶点格式 - vertex_format = GeomVertexFormat.getV3c4() - vertex_data = GeomVertexData("portals", vertex_format, Geom.UHStatic) - - # 计算总顶点数 - total_vertices = 0 - for polygon in self.polygons.values(): - total_vertices += len(polygon.portal_points) * 2 # 每个门户2个点 - - if total_vertices == 0: - return - - vertex_data.setNumRows(total_vertices) - - # 创建顶点写入器 - vertex_writer = GeomVertexWriter(vertex_data, "vertex") - color_writer = GeomVertexWriter(vertex_data, "color") - - # 写入门户点数据 - vertex_index = 0 - for polygon in self.polygons.values(): - for neighbor_id, (left_point, right_point) in polygon.portal_points.items(): - # 左点 - vertex_writer.addData3f(left_point.x, left_point.y + 0.1, left_point.z) - color_writer.addData4f(1.0, 1.0, 0.0, 1.0) # 黄色 - - # 右点 - vertex_writer.addData3f(right_point.x, right_point.y + 0.1, right_point.z) - color_writer.addData4f(1.0, 1.0, 0.0, 1.0) # 黄色 - - vertex_index += 2 - - # 创建几何体 - geom = Geom(vertex_data) - lines = GeomLines(Geom.UHStatic) - - # 添加线段 - vertex_index = 0 - for polygon in self.polygons.values(): - for _ in polygon.portal_points: - lines.addVertex(vertex_index) - lines.addVertex(vertex_index + 1) - lines.closePrimitive() - vertex_index += 2 - - geom.addPrimitive(lines) - self.navmesh_node.node().addGeom(geom) - - # 设置线宽 - portal_node = self.navmesh_node.attachNewNode("portals") - portal_node.node().addGeom(geom) - portal_node.setRenderModeThickness(2.0) - - def _create_normals_geometry(self): - """创建法向量几何体""" - if not self.navmesh_node or not self.polygons: - return - - # 创建顶点格式 - vertex_format = GeomVertexFormat.getV3c4() - vertex_data = GeomVertexData("normals", vertex_format, Geom.UHStatic) - - # 计算总顶点数 - total_vertices = 0 - for polygon in self.polygons.values(): - if polygon.vertices: - total_vertices += 2 # 每个多边形中心点和法向量终点 - - if total_vertices == 0: - return - - vertex_data.setNumRows(total_vertices) - - # 创建顶点写入器 - vertex_writer = GeomVertexWriter(vertex_data, "vertex") - color_writer = GeomVertexWriter(vertex_data, "color") - - # 写入法向量数据 - vertex_index = 0 - normal_length = 0.5 - for polygon in self.polygons.values(): - if not polygon.vertices or len(polygon.vertices) < 3: - continue - - # 计算法向量 - edge1 = polygon.vertices[1] - polygon.vertices[0] - edge2 = polygon.vertices[2] - polygon.vertices[0] - normal = edge1.cross(edge2) - if normal.length() > 0: - normal.normalize() - normal *= normal_length - - # 起点(中心点) - vertex_writer.addData3f(polygon.center.x, polygon.center.y, polygon.center.z) - color_writer.addData4f(0.0, 0.0, 1.0, 1.0) # 蓝色 - - # 终点(法向量方向) - end_point = polygon.center + normal - vertex_writer.addData3f(end_point.x, end_point.y, end_point.z) - color_writer.addData4f(0.0, 0.0, 1.0, 1.0) # 蓝色 - - vertex_index += 2 - - # 创建几何体 - geom = Geom(vertex_data) - lines = GeomLines(Geom.UHStatic) - - # 添加线段 - for i in range(0, total_vertices, 2): - lines.addVertex(i) - lines.addVertex(i + 1) - lines.closePrimitive() - - geom.addPrimitive(lines) - self.navmesh_node.node().addGeom(geom) - - # 设置线宽 - normals_node = self.navmesh_node.attachNewNode("normals") - normals_node.node().addGeom(geom) - normals_node.setRenderModeThickness(1.5) - - def load_from_file(self, filepath: str) -> bool: - """ - 从文件加载导航网格 - - Args: - filepath: 导航网格文件路径 - - Returns: - 是否加载成功 - """ - try: - with open(filepath, 'r', encoding='utf-8') as f: - data = json.load(f) - - # 解析导航网格数据 - self._parse_navmesh_data(data) - - # 更新可视化 - self._update_navmesh_visualization() - - print(f"✓ 导航网格加载成功: {filepath}") - return True - - except Exception as e: - print(f"✗ 导航网格加载失败: {e}") - return False - - def _parse_navmesh_data(self, data: Dict[str, Any]): - """解析导航网格数据""" - # 清除现有数据 - self.polygons.clear() - self.obstacles.clear() - - # 解析元数据 - if 'metadata' in data: - metadata = data['metadata'] - self.version = metadata.get('version', self.version) - self.generator = metadata.get('generator', self.generator) - self.created_time = metadata.get('created', '') - self.modified_time = metadata.get('modified', '') - - # 解析设置 - if 'settings' in data: - settings = data['settings'] - self.cell_size = settings.get('cell_size', self.cell_size) - self.cell_height = settings.get('cell_height', self.cell_height) - self.agent_height = settings.get('agent_height', self.agent_height) - self.agent_radius = settings.get('agent_radius', self.agent_radius) - self.max_slope = settings.get('max_slope', self.max_slope) - self.max_climb = settings.get('max_climb', self.max_climb) - self.min_region_area = settings.get('min_region_area', self.min_region_area) - self.merge_region_area = settings.get('merge_region_area', self.merge_region_area) - self.edge_max_len = settings.get('edge_max_len', self.edge_max_len) - self.edge_max_error = settings.get('edge_max_error', self.edge_max_error) - self.verts_per_poly = settings.get('verts_per_poly', self.verts_per_poly) - - # 解析多边形 - if 'polygons' in data: - for poly_data in data['polygons']: - polygon_id = poly_data['id'] - vertices = [Point3(v[0], v[1], v[2]) for v in poly_data['vertices']] - - polygon = NavMeshPolygon(polygon_id, vertices) - - # 添加邻居信息 - if 'neighbors' in poly_data: - for neighbor_id in poly_data['neighbors']: - polygon.add_neighbor(neighbor_id) - - # 添加门户点信息 - if 'portal_points' in poly_data: - for neighbor_id, portal_data in poly_data['portal_points'].items(): - left_point = Point3(portal_data[0][0], portal_data[0][1], portal_data[0][2]) - right_point = Point3(portal_data[1][0], portal_data[1][1], portal_data[1][2]) - polygon.set_portal_points(int(neighbor_id), left_point, right_point) - - # 设置标志位 - if 'flags' in poly_data: - polygon.flags = poly_data['flags'] - - self.polygons[polygon_id] = polygon - - # 解析障碍物 - if 'obstacles' in data: - for obs_data in data['obstacles']: - obstacle_id = obs_data['id'] - self.obstacles[obstacle_id] = { - 'position': Point3(obs_data['position'][0], obs_data['position'][1], obs_data['position'][2]), - 'radius': obs_data['radius'], - 'height': obs_data['height'], - 'type': obs_data.get('type', 'cylinder') - } - - # 更新统计信息 - self._update_stats() - - # 更新空间哈希 - self._update_spatial_hash() - - print(f"✓ 解析了 {len(self.polygons)} 个多边形和 {len(self.obstacles)} 个障碍物") - - def save_to_file(self, filepath: str) -> bool: - """ - 保存导航网格到文件 - - Args: - filepath: 保存文件路径 - - Returns: - 是否保存成功 - """ - try: - import time - current_time = time.strftime("%Y-%m-%d %H:%M:%S") - - # 构建导航网格数据 - data = { - "metadata": { - "version": self.version, - "generator": self.generator, - "created": self.created_time if self.created_time else current_time, - "modified": current_time - }, - "settings": { - "cell_size": self.cell_size, - "cell_height": self.cell_height, - "agent_height": self.agent_height, - "agent_radius": self.agent_radius, - "max_slope": self.max_slope, - "max_climb": self.max_climb, - "min_region_area": self.min_region_area, - "merge_region_area": self.merge_region_area, - "edge_max_len": self.edge_max_len, - "edge_max_error": self.edge_max_error, - "verts_per_poly": self.verts_per_poly - }, - "polygons": [], - "obstacles": [] - } - - # 添加多边形数据 - for polygon_id, polygon in self.polygons.items(): - poly_data = { - "id": polygon_id, - "vertices": [[v.x, v.y, v.z] for v in polygon.vertices], - "neighbors": polygon.neighbors, - "portal_points": {}, - "flags": polygon.flags - } - - # 添加门户点数据 - for neighbor_id, (left_point, right_point) in polygon.portal_points.items(): - poly_data['portal_points'][str(neighbor_id)] = [ - [left_point.x, left_point.y, left_point.z], - [right_point.x, right_point.y, right_point.z] - ] - - data["polygons"].append(poly_data) - - # 添加障碍物数据 - for obstacle_id, obstacle in self.obstacles.items(): - obs_data = { - "id": obstacle_id, - "position": [obstacle['position'].x, obstacle['position'].y, obstacle['position'].z], - "radius": obstacle['radius'], - "height": obstacle['height'], - "type": obstacle['type'] - } - data["obstacles"].append(obs_data) - - # 保存到文件 - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(data, f, indent=2, ensure_ascii=False) - - print(f"✓ 导航网格保存成功: {filepath}") - return True - - except Exception as e: - print(f"✗ 导航网格保存失败: {e}") - return False - - def generate_from_scene(self) -> bool: - """ - 从当前场景生成导航网格 - - Returns: - 是否生成成功 - """ - try: - import time - start_time = time.time() - - print("✓ 开始从场景生成导航网格...") - - # 这里应该实现实际的导航网格生成算法 - # 为演示目的,我们创建一个更复杂的测试网格 - self._create_complex_test_navmesh() - - # 计算多边形之间的连接关系 - self._calculate_polygon_connections() - - # 计算门户点 - self._calculate_portal_points() - - # 更新可视化 - self._update_navmesh_visualization() - - # 更新统计信息 - self._update_stats() - - # 更新空间哈希 - self._update_spatial_hash() - - end_time = time.time() - self.stats['last_generation_time'] = end_time - start_time - - print(f"✓ 导航网格生成完成,耗时 {end_time - start_time:.2f} 秒") - return True - - except Exception as e: - print(f"✗ 导航网格生成失败: {e}") - return False - - def _create_complex_test_navmesh(self): - """创建复杂测试导航网格""" - # 清除现有数据 - self.polygons.clear() - - # 创建一个更复杂的测试网格(10x10的区域,包含一些障碍物) - size = 20.0 - step = 2.0 - obstacle_positions = [ - Point3(5, 0, 5), - Point3(10, 0, 10), - Point3(15, 0, 5), - Point3(5, 0, 15) - ] - - # 创建网格多边形 - polygon_id = 0 - for x in range(0, int(size/step)): - for z in range(0, int(size/step)): - # 检查是否与障碍物重叠 - x_pos = x * step - size/2 - z_pos = z * step - size/2 - center = Point3(x_pos + step/2, 0, z_pos + step/2) - - # 检查是否与障碍物重叠 - overlap = False - for obstacle_pos in obstacle_positions: - distance = (center - obstacle_pos).length() - if distance < step * 1.5: # 避开障碍物 - overlap = True - break - - if overlap: - continue - - # 创建正方形多边形 - vertices = [ - Point3(x_pos, 0, z_pos), - Point3(x_pos + step, 0, z_pos), - Point3(x_pos + step, 0, z_pos + step), - Point3(x_pos, 0, z_pos + step) - ] - - polygon = NavMeshPolygon(polygon_id, vertices) - self.polygons[polygon_id] = polygon - polygon_id += 1 - - # 创建一些不规则多边形 - irregular_polygons = [ - # L形多边形 - [ - Point3(-8, 0, -8), - Point3(-6, 0, -8), - Point3(-6, 0, -6), - Point3(-4, 0, -6), - Point3(-4, 0, -4), - Point3(-8, 0, -4) - ], - # 三角形 - [ - Point3(8, 0, -8), - Point3(10, 0, -6), - Point3(6, 0, -6) - ], - # 六边形 - [ - Point3(-8, 0, 8), - Point3(-7, 0, 9), - Point3(-5, 0, 9), - Point3(-4, 0, 8), - Point3(-5, 0, 7), - Point3(-7, 0, 7) - ] - ] - - for vertices in irregular_polygons: - polygon = NavMeshPolygon(polygon_id, vertices) - self.polygons[polygon_id] = polygon - polygon_id += 1 - - print(f"✓ 创建了 {len(self.polygons)} 个复杂测试多边形") - - def _calculate_polygon_connections(self): - """计算多边形之间的连接关系""" - # 为每个多边形找到相邻的多边形 - polygon_list = list(self.polygons.values()) - - for i, polygon in enumerate(polygon_list): - polygon.neighbors.clear() # 清除现有邻居 - - # 检查与其他多边形的共享边 - for j, other_polygon in enumerate(polygon_list): - if i == j: - continue - - # 检查是否有共享边 - if self._polygons_share_edge(polygon, other_polygon): - polygon.add_neighbor(other_polygon.polygon_id) - - def _polygons_share_edge(self, poly1: NavMeshPolygon, poly2: NavMeshPolygon) -> bool: - """检查两个多边形是否共享边""" - # 简化实现:检查是否有两个顶点非常接近 - threshold = 0.1 # 距离阈值 - - for i, v1 in enumerate(poly1.vertices): - next_i = (i + 1) % len(poly1.vertices) - edge1_start = v1 - edge1_end = poly1.vertices[next_i] - - for j, v2 in enumerate(poly2.vertices): - next_j = (j + 1) % len(poly2.vertices) - edge2_start = v2 - edge2_end = poly2.vertices[next_j] - - # 检查边是否重合(方向相同或相反) - if (self._points_equal(edge1_start, edge2_end, threshold) and - self._points_equal(edge1_end, edge2_start, threshold)): - return True # 方向相反的共享边 - - if (self._points_equal(edge1_start, edge2_start, threshold) and - self._points_equal(edge1_end, edge2_end, threshold)): - return True # 方向相同的共享边 - - return False - - def _points_equal(self, p1: Point3, p2: Point3, threshold: float) -> bool: - """检查两点是否相等(在阈值范围内)""" - return (p1 - p2).length() < threshold - - def _calculate_portal_points(self): - """计算多边形之间的门户点""" - for polygon in self.polygons.values(): - polygon.portal_points.clear() # 清除现有门户点 - - for neighbor_id in polygon.neighbors: - if neighbor_id in self.polygons: - neighbor_polygon = self.polygons[neighbor_id] - portal_points = self._calculate_portal_between_polygons(polygon, neighbor_polygon) - if portal_points: - left_point, right_point = portal_points - polygon.set_portal_points(neighbor_id, left_point, right_point) - # 同时在邻居多边形上设置反向门户点 - neighbor_polygon.set_portal_points(polygon.polygon_id, right_point, left_point) - - def _calculate_portal_between_polygons(self, poly1: NavMeshPolygon, poly2: NavMeshPolygon) -> Optional[Tuple[Point3, Point3]]: - """计算两个多边形之间的门户点""" - # 简化实现:找到两个多边形共享边的两个端点 - threshold = 0.1 - - for i, v1 in enumerate(poly1.vertices): - next_i = (i + 1) % len(poly1.vertices) - edge1_start = v1 - edge1_end = poly1.vertices[next_i] - - for j, v2 in enumerate(poly2.vertices): - next_j = (j + 1) % len(poly2.vertices) - edge2_start = v2 - edge2_end = poly2.vertices[next_j] - - # 检查边是否重合(方向相同) - if (self._points_equal(edge1_start, edge2_start, threshold) and - self._points_equal(edge1_end, edge2_end, threshold)): - return (edge1_start, edge1_end) - - # 检查边是否重合(方向相反) - if (self._points_equal(edge1_start, edge2_end, threshold) and - self._points_equal(edge1_end, edge2_start, threshold)): - return (edge1_end, edge1_start) # 保持poly1到poly2的方向 - - return None - - def is_point_walkable(self, point: Point3) -> bool: - """ - 检查点是否可行走 - - Args: - point: 检查的点 - - Returns: - 是否可行走 - """ - if not self.polygons: - return False - - # 首先检查是否有动态障碍物阻挡 - for obstacle in self.obstacles.values(): - obstacle_pos = obstacle['position'] - distance = (Point3(point.x, 0, point.z) - Point3(obstacle_pos.x, 0, obstacle_pos.z)).length() - if distance < obstacle['radius']: - # 检查高度 - if obstacle_pos.y <= point.y <= obstacle_pos.y + obstacle['height']: - return False # 被障碍物阻挡 - - # 检查点是否在任何多边形内 - if self.use_spatial_hashing: - # 使用空间哈希加速查找 - x_key = int(point.x / self.spatial_hash_cell_size) - z_key = int(point.z / self.spatial_hash_cell_size) - key = (x_key, z_key) - - if key in self.spatial_hash_grid: - for polygon_id in self.spatial_hash_grid[key]: - if polygon_id in self.polygons: - polygon = self.polygons[polygon_id] - if polygon.is_point_in_bounding_box(point) and polygon.is_point_inside(point): - return polygon.walkable - else: - # 暴力查找 - for polygon in self.polygons.values(): - if polygon.is_point_in_bounding_box(point) and polygon.is_point_inside(point): - return polygon.walkable - - return False - - def get_closest_point(self, point: Point3) -> Optional[Point3]: - """ - 获取导航网格上最近的点 - - Args: - point: 目标点 - - Returns: - 最近的可行走点,如果找不到则返回None - """ - if not self.polygons: - return None - - closest_point = None - min_distance = float('inf') - - # 使用空间哈希加速查找附近的多边形 - search_radius = 5.0 # 初始搜索半径 - polygons_to_check = self._get_polygons_in_radius(point, search_radius) - - # 如果没找到,扩大搜索范围 - if not polygons_to_check: - search_radius = 20.0 - polygons_to_check = self._get_polygons_in_radius(point, search_radius) - - # 如果仍然没找到,检查所有多边形 - if not polygons_to_check: - polygons_to_check = list(self.polygons.keys()) - - # 遍历相关多边形找到最近的点 - for polygon_id in polygons_to_check: - if polygon_id in self.polygons: - polygon = self.polygons[polygon_id] - poly_point = polygon.get_closest_point(point) - distance = (poly_point - point).length() - - if distance < min_distance: - min_distance = distance - closest_point = poly_point - - return closest_point - - def get_random_point(self) -> Optional[Point3]: - """ - 获取导航网格上的随机点 - - Returns: - 随机可行走点,如果导航网格为空则返回None - """ - if not self.polygons: - return None - - # 随机选择一个多边形 - polygon = random.choice(list(self.polygons.values())) - - # 在多边形内生成随机点 - if len(polygon.vertices) >= 3: - # 使用重心坐标生成多边形内的随机点 - # 选择三个随机顶点 - indices = random.sample(range(len(polygon.vertices)), - min(3, len(polygon.vertices))) - - if len(indices) == 3: - v1 = polygon.vertices[indices[0]] - v2 = polygon.vertices[indices[1]] - v3 = polygon.vertices[indices[2]] - - # 生成重心坐标 - r1 = random.random() - r2 = random.random() - if r1 + r2 > 1: - r1 = 1 - r1 - r2 = 1 - r2 - - r3 = 1 - r1 - r2 - - # 计算随机点 - random_point = v1 * r1 + v2 * r2 + v3 * r3 - return random_point - - # 如果无法生成重心坐标,返回中心点 - return polygon.center - - def get_polygon_containing_point(self, point: Point3) -> Optional[NavMeshPolygon]: - """ - 获取包含指定点的多边形 - - Args: - point: 目标点 - - Returns: - 包含点的多边形,如果找不到则返回None - """ - # 使用空间哈希加速查找 - if self.use_spatial_hashing: - x_key = int(point.x / self.spatial_hash_cell_size) - z_key = int(point.z / self.spatial_hash_cell_size) - key = (x_key, z_key) - - if key in self.spatial_hash_grid: - for polygon_id in self.spatial_hash_grid[key]: - if polygon_id in self.polygons: - polygon = self.polygons[polygon_id] - if polygon.is_point_in_bounding_box(point) and polygon.is_point_inside(point): - return polygon - else: - # 暴力查找 - for polygon in self.polygons.values(): - if polygon.is_point_in_bounding_box(point) and polygon.is_point_inside(point): - return polygon - - return None - - def get_adjacent_polygons(self, polygon_id: int) -> List[NavMeshPolygon]: - """ - 获取相邻多边形列表 - - Args: - polygon_id: 多边形ID - - Returns: - 相邻多边形列表 - """ - if polygon_id not in self.polygons: - return [] - - polygon = self.polygons[polygon_id] - adjacent_polygons = [] - - for neighbor_id in polygon.neighbors: - if neighbor_id in self.polygons: - adjacent_polygons.append(self.polygons[neighbor_id]) - - return adjacent_polygons - - def visualize_path(self, path: List[Point3]): - """ - 可视化路径 - - Args: - path: 路径点列表 - """ - if not self.path_visualization_node or not path: - return - - # 清除现有路径可视化 - self.path_visualization_node.node().removeAllGeoms() - - if len(path) < 2: - return - - # 创建路径几何体 - self._create_path_geometry(path) - - # 显示路径 - self.path_visualization_node.show() - - def _create_path_geometry(self, path: List[Point3]): - """创建路径几何体""" - if not self.path_visualization_node or len(path) < 2: - return - - # 创建顶点格式 - vertex_format = GeomVertexFormat.getV3c4() - vertex_data = GeomVertexData("path", vertex_format, Geom.UHStatic) - vertex_data.setNumRows(len(path)) - - # 创建顶点写入器 - vertex_writer = GeomVertexWriter(vertex_data, "vertex") - color_writer = GeomVertexWriter(vertex_data, "color") - - # 写入路径点 - for point in path: - vertex_writer.addData3f(point.x, point.y + 0.1, point.z) # 稍微抬高避免与地面重叠 - color_writer.addData4f(1.0, 0.0, 0.0, 1.0) # 红色 - - # 创建几何体 - geom = Geom(vertex_data) - lines = GeomLines(Geom.UHStatic) - - # 添加线段 - for i in range(len(path) - 1): - lines.addVertex(i) - lines.addVertex(i + 1) - lines.closePrimitive() - - geom.addPrimitive(lines) - self.path_visualization_node.node().addGeom(geom) - - # 设置线宽 - self.path_visualization_node.setRenderModeThickness(3.0) - - # 添加路径点标记 - self._create_path_point_markers(path) - - def _create_path_point_markers(self, path: List[Point3]): - """创建路径点标记""" - if not self.path_visualization_node or not path: - return - - marker_node = self.path_visualization_node.attachNewNode("path_markers") - - for i, point in enumerate(path): - # 创建标记球体 - marker = self.world.render.attachNewNode(f"path_marker_{i}") - marker.setPos(point.x, point.y + 0.2, point.z) - - # 可以在这里添加可视化标记(比如一个小球) - # 为简化,我们只添加位置信息 - - def clear_path_visualization(self): - """清除路径可视化""" - if self.path_visualization_node: - self.path_visualization_node.node().removeAllGeoms() - self.path_visualization_node.hide() - - def is_navmesh_loaded(self) -> bool: - """检查导航网格是否已加载""" - return len(self.polygons) > 0 - - def get_polygon_count(self) -> int: - """获取多边形数量""" - return len(self.polygons) - - def add_polygon(self, vertices: List[Point3]) -> int: - """ - 添加多边形 - - Args: - vertices: 多边形顶点列表 - - Returns: - 新多边形的ID - """ - polygon_id = self.polygon_counter - self.polygon_counter += 1 - - polygon = NavMeshPolygon(polygon_id, vertices) - self.polygons[polygon_id] = polygon - - # 更新空间哈希 - self._update_spatial_hash() - - # 更新可视化 - self._update_navmesh_visualization() - - # 更新统计信息 - self._update_stats() - - return polygon_id - - def remove_polygon(self, polygon_id: int) -> bool: - """ - 移除多边形 - - Args: - polygon_id: 多边形ID - - Returns: - 是否移除成功 - """ - if polygon_id in self.polygons: - # 从邻居多边形中移除这个多边形的引用 - polygon = self.polygons[polygon_id] - for neighbor_id in polygon.neighbors: - if neighbor_id in self.polygons: - self.polygons[neighbor_id].remove_neighbor(polygon_id) - - # 移除多边形 - del self.polygons[polygon_id] - - # 更新空间哈希 - self._update_spatial_hash() - - # 更新可视化 - self._update_navmesh_visualization() - - # 更新统计信息 - self._update_stats() - - return True - return False - - def add_obstacle(self, position: Point3, radius: float, height: float, obstacle_type: str = "cylinder") -> int: - """ - 添加动态障碍物 - - Args: - position: 障碍物位置 - radius: 障碍物半径 - height: 障碍物高度 - obstacle_type: 障碍物类型 - - Returns: - 新障碍物的ID - """ - obstacle_id = self.obstacle_counter - self.obstacle_counter += 1 - - self.obstacles[obstacle_id] = { - 'position': position, - 'radius': radius, - 'height': height, - 'type': obstacle_type - } - - # 更新统计信息 - self._update_stats() - - # 更新可视化 - self._update_obstacles_visualization() - - return obstacle_id - - def remove_obstacle(self, obstacle_id: int) -> bool: - """ - 移除动态障碍物 - - Args: - obstacle_id: 障碍物ID - - Returns: - 是否移除成功 - """ - if obstacle_id in self.obstacles: - del self.obstacles[obstacle_id] - - # 更新统计信息 - self._update_stats() - - # 更新可视化 - self._update_obstacles_visualization() - - return True - return False - - def _update_obstacles_visualization(self): - """更新障碍物可视化""" - if not self.obstacles_node: - return - - # 清除现有障碍物可视化 - self.obstacles_node.node().removeAllGeoms() - - if not self.obstacles or not self.navmesh_visible: - return - - # 创建障碍物几何体 - self._create_obstacles_geometry() - - # 显示障碍物 - self.obstacles_node.show() - - def _create_obstacles_geometry(self): - """创建障碍物几何体""" - if not self.obstacles_node or not self.obstacles: - return - - # 为每个障碍物创建可视化表示 - for obstacle_id, obstacle in self.obstacles.items(): - # 创建圆柱体表示障碍物 - # 这里简化处理,实际应用中可能需要更复杂的几何体 - - # 创建顶点格式 - vertex_format = GeomVertexFormat.getV3c4() - vertex_data = GeomVertexData(f"obstacle_{obstacle_id}", vertex_format, Geom.UHStatic) - - # 简化:创建一个正方形表示障碍物 - pos = obstacle['position'] - radius = obstacle['radius'] - - vertices = [ - Point3(pos.x - radius, pos.y, pos.z - radius), - Point3(pos.x + radius, pos.y, pos.z - radius), - Point3(pos.x + radius, pos.y, pos.z + radius), - Point3(pos.x - radius, pos.y, pos.z + radius), - Point3(pos.x - radius, pos.y + obstacle['height'], pos.z - radius), - Point3(pos.x + radius, pos.y + obstacle['height'], pos.z - radius), - Point3(pos.x + radius, pos.y + obstacle['height'], pos.z + radius), - Point3(pos.x - radius, pos.y + obstacle['height'], pos.z + radius) - ] - - vertex_data.setNumRows(len(vertices)) - - # 创建顶点写入器 - vertex_writer = GeomVertexWriter(vertex_data, "vertex") - color_writer = GeomVertexWriter(vertex_data, "color") - - # 写入顶点 - for vertex in vertices: - vertex_writer.addData3f(vertex.x, vertex.y, vertex.z) - color_writer.addData4f(1.0, 0.0, 0.0, 0.5) # 红色半透明 - - # 创建几何体 - geom = Geom(vertex_data) - - # 创建面(简化为立方体) - # 底面 - triangles = GeomTriangles(Geom.UHStatic) - triangles.addVertices(0, 1, 2) - triangles.addVertices(0, 2, 3) - triangles.closePrimitive() - geom.addPrimitive(triangles) - - # 顶面 - triangles = GeomTriangles(Geom.UHStatic) - triangles.addVertices(4, 6, 5) - triangles.addVertices(4, 7, 6) - triangles.closePrimitive() - geom.addPrimitive(triangles) - - # 前面 - triangles = GeomTriangles(Geom.UHStatic) - triangles.addVertices(0, 4, 5) - triangles.addVertices(0, 5, 1) - triangles.closePrimitive() - geom.addPrimitive(triangles) - - # 后面 - triangles = GeomTriangles(Geom.UHStatic) - triangles.addVertices(2, 6, 7) - triangles.addVertices(2, 7, 3) - triangles.closePrimitive() - geom.addPrimitive(triangles) - - # 左面 - triangles = GeomTriangles(Geom.UHStatic) - triangles.addVertices(0, 3, 7) - triangles.addVertices(0, 7, 4) - triangles.closePrimitive() - geom.addPrimitive(triangles) - - # 右面 - triangles = GeomTriangles(Geom.UHStatic) - triangles.addVertices(1, 5, 6) - triangles.addVertices(1, 6, 2) - triangles.closePrimitive() - geom.addPrimitive(triangles) - - obstacle_node = self.obstacles_node.attachNewNode(f"obstacle_geom_{obstacle_id}") - obstacle_node.node().addGeom(geom) - - def _update_stats(self): - """更新统计信息""" - total_vertices = sum(len(polygon.vertices) for polygon in self.polygons.values()) - total_edges = sum(len(polygon.vertices) for polygon in self.polygons.values()) # 简化处理 - total_portals = sum(len(polygon.portal_points) for polygon in self.polygons.values()) - - self.stats.update({ - 'polygons': len(self.polygons), - 'vertices': total_vertices, - 'edges': total_edges, - 'portals': total_portals, - 'obstacles': len(self.obstacles) - }) - - def get_stats(self) -> Dict[str, Any]: - """获取统计信息""" - return self.stats.copy() - - def set_cell_size(self, size: float): - """设置网格单元大小""" - self.cell_size = max(0.1, size) - - def set_agent_height(self, height: float): - """设置代理高度""" - self.agent_height = max(0.1, height) - - def set_agent_radius(self, radius: float): - """设置代理半径""" - self.agent_radius = max(0.1, radius) - - def set_max_slope(self, slope: float): - """设置最大坡度""" - self.max_slope = max(0.0, min(89.0, slope)) - - def set_max_climb(self, climb: float): - """设置最大可爬升高度""" - self.max_climb = max(0.0, climb) - - def set_min_region_area(self, area: float): - """设置最小区域面积""" - self.min_region_area = max(1, area) - - def set_merge_region_area(self, area: float): - """设置合并区域面积阈值""" - self.merge_region_area = max(1, area) - - def set_edge_max_len(self, length: float): - """设置边最大长度""" - self.edge_max_len = max(0.1, length) - - def set_edge_max_error(self, error: float): - """设置边最大误差""" - self.edge_max_error = max(0.1, error) - - def set_verts_per_poly(self, count: int): - """设置每个多边形最大顶点数""" - self.verts_per_poly = max(3, count) - - def enable_spatial_hashing(self, enabled: bool): - """启用/禁用空间哈希""" - self.use_spatial_hashing = enabled - if enabled: - self._update_spatial_hash() - - def set_spatial_hash_cell_size(self, size: float): - """设置空间哈希网格单元大小""" - self.spatial_hash_cell_size = max(0.1, size) - if self.use_spatial_hashing: - self._update_spatial_hash() - - def rebuild_navmesh(self): - """重新构建导航网格""" - print("✓ 重新构建导航网格...") - return self.generate_from_scene() - - def optimize_navmesh(self): - """优化导航网格""" - print("✓ 优化导航网格...") - # 这里可以实现多边形合并、简化等优化算法 - # 为简化,我们只更新连接关系 - self._calculate_polygon_connections() - self._calculate_portal_points() - self._update_navmesh_visualization() - print("✓ 导航网格优化完成") - - def validate_navmesh(self) -> bool: - """验证导航网格的完整性""" - print("✓ 验证导航网格完整性...") - - issues = [] - - # 检查多边形数据 - for polygon_id, polygon in self.polygons.items(): - if len(polygon.vertices) < 3: - issues.append(f"多边形 {polygon_id} 顶点数不足") - - # 检查邻居关系一致性 - for neighbor_id in polygon.neighbors: - if neighbor_id not in self.polygons: - issues.append(f"多边形 {polygon_id} 的邻居 {neighbor_id} 不存在") - else: - # 检查双向连接 - neighbor = self.polygons[neighbor_id] - if polygon_id not in neighbor.neighbors: - issues.append(f"多边形 {polygon_id} 和 {neighbor_id} 的连接关系不一致") - - # 检查门户点数据 - for polygon_id, polygon in self.polygons.items(): - for neighbor_id, (left_point, right_point) in polygon.portal_points.items(): - if neighbor_id not in polygon.neighbors: - issues.append(f"多边形 {polygon_id} 存在未连接邻居 {neighbor_id} 的门户点") - - if issues: - print(f"✗ 发现 {len(issues)} 个问题:") - for issue in issues: - print(f" - {issue}") - return False - else: - print("✓ 导航网格验证通过") +""" +导航网格管理器 +负责导航网格的加载、生成、管理和查询 +""" + +import math +import json +import random +from typing import List, Tuple, Optional, Dict, Any, Set +from panda3d.core import Vec3, Point3, NodePath, GeomNode, Geom, GeomVertexFormat +from panda3d.core import GeomVertexData, GeomVertexWriter, GeomTriangles, GeomLines +from panda3d.core import LColor, Material, BitMask32 +from direct.actor.Actor import Actor + +class NavMeshPolygon: + """ + 导航网格多边形 + 表示导航网格中的一个可行走区域 + """ + + def __init__(self, polygon_id: int, vertices: List[Point3]): + self.polygon_id = polygon_id + self.vertices = vertices # 多边形顶点列表 + self.neighbors: List[int] = [] # 相邻多边形ID列表 + self.center = self._calculate_center() # 多边形中心点 + self.walkable = True # 是否可行走 + self.portal_points: Dict[int, Tuple[Point3, Point3]] = {} # 与其他多边形的门户点 + self.area = self._calculate_area() # 多边形面积 + self.bounding_box = self._calculate_bounding_box() # 边界框 + self.flags = 0 # 标志位,用于存储额外信息 + + def _calculate_center(self) -> Point3: + """计算多边形中心点""" + if not self.vertices: + return Point3(0, 0, 0) + + center = Point3(0, 0, 0) + for vertex in self.vertices: + center += vertex + center /= len(self.vertices) + return center + + def _calculate_area(self) -> float: + """计算多边形面积(使用鞋带公式)""" + if len(self.vertices) < 3: + return 0.0 + + area = 0.0 + n = len(self.vertices) + for i in range(n): + j = (i + 1) % n + area += self.vertices[i].x * self.vertices[j].z + area -= self.vertices[j].x * self.vertices[i].z + return abs(area) / 2.0 + + def _calculate_bounding_box(self) -> Tuple[Point3, Point3]: + """计算边界框(最小点和最大点)""" + if not self.vertices: + return Point3(0, 0, 0), Point3(0, 0, 0) + + min_x = min(v.x for v in self.vertices) + min_y = min(v.y for v in self.vertices) + min_z = min(v.z for v in self.vertices) + max_x = max(v.x for v in self.vertices) + max_y = max(v.y for v in self.vertices) + max_z = max(v.z for v in self.vertices) + + return Point3(min_x, min_y, min_z), Point3(max_x, max_y, max_z) + + def add_neighbor(self, neighbor_id: int): + """添加相邻多边形""" + if neighbor_id not in self.neighbors: + self.neighbors.append(neighbor_id) + + def remove_neighbor(self, neighbor_id: int): + """移除相邻多边形""" + if neighbor_id in self.neighbors: + self.neighbors.remove(neighbor_id) + + def set_portal_points(self, neighbor_id: int, left_point: Point3, right_point: Point3): + """设置与邻居多边形的门户点""" + self.portal_points[neighbor_id] = (left_point, right_point) + + def get_portal_points(self, neighbor_id: int) -> Optional[Tuple[Point3, Point3]]: + """获取与邻居多边形的门户点""" + return self.portal_points.get(neighbor_id) + + def is_point_inside(self, point: Point3) -> bool: + """检查点是否在多边形内(射线投射算法)""" + # 使用射线投射算法判断点是否在多边形内 + x, z = point.x, point.z + n = len(self.vertices) + inside = False + + p1x, p1z = self.vertices[0].x, self.vertices[0].z + for i in range(1, n + 1): + p2x, p2z = self.vertices[i % n].x, self.vertices[i % n].z + if z > min(p1z, p2z): + if z <= max(p1z, p2z): + if x <= max(p1x, p2x): + if p1z != p2z: + xinters = (z - p1z) * (p2x - p1x) / (p2z - p1z) + p1x + if p1x == p2x or x <= xinters: + inside = not inside + p1x, p1z = p2x, p2z + + return inside + + def is_point_in_bounding_box(self, point: Point3) -> bool: + """快速检查点是否在边界框内""" + min_point, max_point = self.bounding_box + return (min_point.x <= point.x <= max_point.x and + min_point.y <= point.y <= max_point.y and + min_point.z <= point.z <= max_point.z) + + def get_distance_to_point(self, point: Point3) -> float: + """计算到点的距离""" + return (self.center - point).length() + + def get_closest_point_on_boundary(self, point: Point3) -> Point3: + """获取多边形边界上距离给定点最近的点""" + if not self.vertices: + return point + + closest_point = None + min_distance = float('inf') + + # 检查每条边 + for i in range(len(self.vertices)): + start = self.vertices[i] + end = self.vertices[(i + 1) % len(self.vertices)] + + # 计算点到线段的最近点 + segment_vec = end - start + point_vec = point - start + segment_len_sq = segment_vec.lengthSquared() + + if segment_len_sq == 0: + # 线段退化为点 + closest = start + else: + t = point_vec.dot(segment_vec) / segment_len_sq + t = max(0, min(1, t)) # 限制在[0,1]范围内 + closest = start + segment_vec * t + + distance = (point - closest).length() + if distance < min_distance: + min_distance = distance + closest_point = closest + + return closest_point if closest_point else self.center + + def get_closest_point(self, point: Point3) -> Point3: + """获取多边形上距离给定点最近的点""" + # 如果点在多边形内,返回该点 + if self.is_point_inside(point): + return point + # 否则返回边界上的最近点 + return self.get_closest_point_on_boundary(point) + + def intersects_with_ray(self, ray_origin: Point3, ray_direction: Vec3) -> Optional[Point3]: + """检查与射线的交点""" + # 简化实现:只检查与多边形平面的交点 + if len(self.vertices) < 3: + return None + + # 计算法平面的法向量 + edge1 = self.vertices[1] - self.vertices[0] + edge2 = self.vertices[2] - self.vertices[0] + normal = edge1.cross(edge2) + normal.normalize() + + # 计算射线与平面的交点 + denom = normal.dot(ray_direction) + if abs(denom) < 1e-6: # 射线与平面平行 + return None + + t = (normal.dot(Vec3(self.vertices[0] - ray_origin))) / denom + if t < 0: # 交点在射线起点后面 + return None + + intersection = ray_origin + ray_direction * t + + # 检查交点是否在多边形内 + if self.is_point_inside(intersection): + return intersection + + return None + + def get_vertices(self) -> List[Point3]: + """获取多边形顶点""" + return self.vertices.copy() + + def get_neighbors(self) -> List[int]: + """获取邻居列表""" + return self.neighbors.copy() + + def get_portal_point_to_neighbor(self, neighbor_id: int) -> Optional[Tuple[Point3, Point3]]: + """获取到指定邻居的门户点""" + return self.portal_points.get(neighbor_id) + +class NavMeshManager: + """ + 导航网格管理器 + 负责整个导航网格系统的管理 + """ + + def __init__(self, world): + """ + 初始化导航网格管理器 + + Args: + world: 3D世界对象 + """ + self.world = world + self.enabled = False + + # 导航网格数据 + self.polygons: Dict[int, NavMeshPolygon] = {} # 多边形字典 + self.polygon_counter = 0 + self.navmesh_node = None # 可视化节点 + self.path_visualization_node = None # 路径可视化节点 + self.obstacles_node = None # 障碍物可视化节点 + + # 导航网格设置 + self.cell_size = 0.5 # 网格单元大小 + self.cell_height = 0.2 # 网格高度 + self.agent_height = 1.8 # 代理高度 + self.agent_radius = 0.3 # 代理半径 + self.max_slope = 45.0 # 最大坡度(度) + self.max_climb = 0.2 # 最大可爬升高度 + self.min_region_area = 3 # 最小区域面积 + self.merge_region_area = 20 # 合并区域面积阈值 + self.edge_max_len = 12.0 # 边最大长度 + self.edge_max_error = 1.3 # 边最大误差 + self.verts_per_poly = 6 # 每个多边形最大顶点数 + self.walkable = True # 是否启用 + + # 可视化设置 + self.visualization_enabled = True + self.navmesh_visible = False + self.path_visualization_enabled = True + self.show_portals = False # 显示门户点 + self.show_normals = False # 显示法向量 + + # 性能优化 + self.spatial_hash_grid = {} # 空间哈希网格,用于快速查找 + self.spatial_hash_cell_size = 5.0 # 空间哈希网格单元大小 + self.use_spatial_hashing = True # 是否使用空间哈希 + + # 导航网格统计 + self.stats = { + 'polygons': 0, + 'vertices': 0, + 'edges': 0, + 'portals': 0, + 'obstacles': 0, + 'last_generation_time': 0.0 + } + + # 障碍物管理 + self.obstacles: Dict[int, Dict[str, Any]] = {} # 动态障碍物 + self.obstacle_counter = 0 + + # 导航网格版本和元数据 + self.version = "1.0.0" + self.generator = "EG Navigation Mesh System" + self.created_time = "" + self.modified_time = "" + + print("✓ 导航网格管理器初始化完成") + + def enable(self): + """启用导航网格管理器""" + if self.enabled: + return + + self.enabled = True + + # 创建可视化节点 + self._create_visualization_nodes() + + print("✓ 导航网格管理器已启用") + + def disable(self): + """禁用导航网格管理器""" + if not self.enabled: + return + + self.enabled = False + + # 清理可视化节点 + self._cleanup_visualization_nodes() + + print("✓ 导航网格管理器已禁用") + + def cleanup(self): + """清理资源""" + self.disable() + self.polygons.clear() + self.obstacles.clear() + self.spatial_hash_grid.clear() + + def _create_visualization_nodes(self): + """创建可视化节点""" + if not self.world or not self.world.render: + return + + # 导航网格可视化节点 + self.navmesh_node = self.world.render.attachNewNode("navmesh_visualization") + self.navmesh_node.setTransparency(True) + self.navmesh_node.setColor(LColor(0.0, 0.8, 0.0, 0.3)) # 半透明绿色 + + # 路径可视化节点 + self.path_visualization_node = self.world.render.attachNewNode("path_visualization") + self.path_visualization_node.setTransparency(True) + + # 障碍物可视化节点 + self.obstacles_node = self.world.render.attachNewNode("obstacles_visualization") + self.obstacles_node.setTransparency(True) + self.obstacles_node.setColor(LColor(1.0, 0.0, 0.0, 0.5)) # 半透明红色 + + # 默认隐藏 + self.navmesh_node.hide() + self.path_visualization_node.hide() + self.obstacles_node.hide() + + def _cleanup_visualization_nodes(self): + """清理可视化节点""" + if self.navmesh_node: + self.navmesh_node.removeNode() + self.navmesh_node = None + + if self.path_visualization_node: + self.path_visualization_node.removeNode() + self.path_visualization_node = None + + if self.obstacles_node: + self.obstacles_node.removeNode() + self.obstacles_node = None + + def toggle_visibility(self): + """切换导航网格可见性""" + if not self.navmesh_node: + return + + if self.navmesh_visible: + self.navmesh_node.hide() + self.navmesh_visible = False + print("✓ 导航网格已隐藏") + else: + self.navmesh_node.show() + self.navmesh_visible = True + print("✓ 导航网格已显示") + + # 更新可视化 + self._update_navmesh_visualization() + + def _update_spatial_hash(self): + """更新空间哈希网格""" + if not self.use_spatial_hashing: + return + + # 清空现有哈希网格 + self.spatial_hash_grid.clear() + + # 为每个多边形计算哈希键 + for polygon_id, polygon in self.polygons.items(): + if not polygon.vertices: + continue + + # 计算多边形的边界框 + min_point, max_point = polygon.bounding_box + + # 计算覆盖的哈希网格单元 + min_x = int(min_point.x / self.spatial_hash_cell_size) + min_z = int(min_point.z / self.spatial_hash_cell_size) + max_x = int(max_point.x / self.spatial_hash_cell_size) + max_z = int(max_point.z / self.spatial_hash_cell_size) + + # 将多边形添加到所有覆盖的单元格中 + for x in range(min_x, max_x + 1): + for z in range(min_z, max_z + 1): + key = (x, z) + if key not in self.spatial_hash_grid: + self.spatial_hash_grid[key] = [] + self.spatial_hash_grid[key].append(polygon_id) + + def _get_polygons_in_radius(self, center: Point3, radius: float) -> List[int]: + """获取指定半径内的多边形""" + if self.use_spatial_hashing: + # 使用空间哈希加速查找 + result = set() + + # 计算覆盖的哈希网格单元 + min_x = int((center.x - radius) / self.spatial_hash_cell_size) + min_z = int((center.z - radius) / self.spatial_hash_cell_size) + max_x = int((center.x + radius) / self.spatial_hash_cell_size) + max_z = int((center.z + radius) / self.spatial_hash_cell_size) + + # 检查所有覆盖的单元格 + for x in range(min_x, max_x + 1): + for z in range(min_z, max_z + 1): + key = (x, z) + if key in self.spatial_hash_grid: + for polygon_id in self.spatial_hash_grid[key]: + polygon = self.polygons[polygon_id] + # 精确检查距离 + if polygon.get_distance_to_point(center) <= radius: + result.add(polygon_id) + + return list(result) + else: + # 暴力查找 + result = [] + radius_sq = radius * radius + for polygon_id, polygon in self.polygons.items(): + dist_sq = (polygon.center - center).lengthSquared() + if dist_sq <= radius_sq: + result.append(polygon_id) + return result + + def _update_navmesh_visualization(self): + """更新导航网格可视化""" + if not self.navmesh_node or not self.navmesh_visible: + return + + # 清除现有几何体 + self.navmesh_node.node().removeAllGeoms() + + if not self.polygons: + return + + # 创建新的几何体 + self._create_navmesh_geometry() + + # 如果需要,创建门户点可视化 + if self.show_portals: + self._create_portals_geometry() + + # 如果需要,创建法向量可视化 + if self.show_normals: + self._create_normals_geometry() + + def _create_navmesh_geometry(self): + """创建导航网格几何体""" + if not self.navmesh_node or not self.polygons: + return + + # 创建顶点格式 + vertex_format = GeomVertexFormat.getV3n3c4() + vertex_data = GeomVertexData("navmesh", vertex_format, Geom.UHStatic) + + # 计算总顶点数(每个三角形3个顶点) + total_vertices = 0 + for polygon in self.polygons.values(): + if len(polygon.vertices) >= 3: + # 简单三角剖分(假设多边形是凸的) + total_vertices += (len(polygon.vertices) - 2) * 3 + + if total_vertices == 0: + return + + vertex_data.setNumRows(total_vertices) + + # 创建顶点写入器 + vertex_writer = GeomVertexWriter(vertex_data, "vertex") + normal_writer = GeomVertexWriter(vertex_data, "normal") + color_writer = GeomVertexWriter(vertex_data, "color") + + # 创建几何体 + geom = Geom(vertex_data) + triangles = GeomTriangles(Geom.UHStatic) + + vertex_index = 0 + for polygon in self.polygons.values(): + if len(polygon.vertices) < 3: + continue + + # 计算法平面的法向量 + if len(polygon.vertices) >= 3: + edge1 = polygon.vertices[1] - polygon.vertices[0] + edge2 = polygon.vertices[2] - polygon.vertices[0] + normal = edge1.cross(edge2) + if normal.length() > 0: + normal.normalize() + else: + normal = Vec3(0, 1, 0) + else: + normal = Vec3(0, 1, 0) + + # 简单三角剖分(扇形剖分) + for i in range(1, len(polygon.vertices) - 1): + # 三角形顶点索引 + indices = [0, i, i + 1] + + # 写入顶点数据 + for idx in indices: + vertex = polygon.vertices[idx] + vertex_writer.addData3f(vertex.x, vertex.y, vertex.z) + normal_writer.addData3f(normal.x, normal.y, normal.z) + color_writer.addData4f(0.0, 0.8, 0.0, 0.3) # 绿色半透明 + + # 添加三角形索引 + triangles.addVertex(vertex_index) + triangles.addVertex(vertex_index + 1) + triangles.addVertex(vertex_index + 2) + triangles.closePrimitive() + + vertex_index += 3 + + geom.addPrimitive(triangles) + self.navmesh_node.node().addGeom(geom) + + def _create_portals_geometry(self): + """创建门户点几何体""" + if not self.navmesh_node or not self.polygons: + return + + # 创建顶点格式 + vertex_format = GeomVertexFormat.getV3c4() + vertex_data = GeomVertexData("portals", vertex_format, Geom.UHStatic) + + # 计算总顶点数 + total_vertices = 0 + for polygon in self.polygons.values(): + total_vertices += len(polygon.portal_points) * 2 # 每个门户2个点 + + if total_vertices == 0: + return + + vertex_data.setNumRows(total_vertices) + + # 创建顶点写入器 + vertex_writer = GeomVertexWriter(vertex_data, "vertex") + color_writer = GeomVertexWriter(vertex_data, "color") + + # 写入门户点数据 + vertex_index = 0 + for polygon in self.polygons.values(): + for neighbor_id, (left_point, right_point) in polygon.portal_points.items(): + # 左点 + vertex_writer.addData3f(left_point.x, left_point.y + 0.1, left_point.z) + color_writer.addData4f(1.0, 1.0, 0.0, 1.0) # 黄色 + + # 右点 + vertex_writer.addData3f(right_point.x, right_point.y + 0.1, right_point.z) + color_writer.addData4f(1.0, 1.0, 0.0, 1.0) # 黄色 + + vertex_index += 2 + + # 创建几何体 + geom = Geom(vertex_data) + lines = GeomLines(Geom.UHStatic) + + # 添加线段 + vertex_index = 0 + for polygon in self.polygons.values(): + for _ in polygon.portal_points: + lines.addVertex(vertex_index) + lines.addVertex(vertex_index + 1) + lines.closePrimitive() + vertex_index += 2 + + geom.addPrimitive(lines) + self.navmesh_node.node().addGeom(geom) + + # 设置线宽 + portal_node = self.navmesh_node.attachNewNode("portals") + portal_node.node().addGeom(geom) + portal_node.setRenderModeThickness(2.0) + + def _create_normals_geometry(self): + """创建法向量几何体""" + if not self.navmesh_node or not self.polygons: + return + + # 创建顶点格式 + vertex_format = GeomVertexFormat.getV3c4() + vertex_data = GeomVertexData("normals", vertex_format, Geom.UHStatic) + + # 计算总顶点数 + total_vertices = 0 + for polygon in self.polygons.values(): + if polygon.vertices: + total_vertices += 2 # 每个多边形中心点和法向量终点 + + if total_vertices == 0: + return + + vertex_data.setNumRows(total_vertices) + + # 创建顶点写入器 + vertex_writer = GeomVertexWriter(vertex_data, "vertex") + color_writer = GeomVertexWriter(vertex_data, "color") + + # 写入法向量数据 + vertex_index = 0 + normal_length = 0.5 + for polygon in self.polygons.values(): + if not polygon.vertices or len(polygon.vertices) < 3: + continue + + # 计算法向量 + edge1 = polygon.vertices[1] - polygon.vertices[0] + edge2 = polygon.vertices[2] - polygon.vertices[0] + normal = edge1.cross(edge2) + if normal.length() > 0: + normal.normalize() + normal *= normal_length + + # 起点(中心点) + vertex_writer.addData3f(polygon.center.x, polygon.center.y, polygon.center.z) + color_writer.addData4f(0.0, 0.0, 1.0, 1.0) # 蓝色 + + # 终点(法向量方向) + end_point = polygon.center + normal + vertex_writer.addData3f(end_point.x, end_point.y, end_point.z) + color_writer.addData4f(0.0, 0.0, 1.0, 1.0) # 蓝色 + + vertex_index += 2 + + # 创建几何体 + geom = Geom(vertex_data) + lines = GeomLines(Geom.UHStatic) + + # 添加线段 + for i in range(0, total_vertices, 2): + lines.addVertex(i) + lines.addVertex(i + 1) + lines.closePrimitive() + + geom.addPrimitive(lines) + self.navmesh_node.node().addGeom(geom) + + # 设置线宽 + normals_node = self.navmesh_node.attachNewNode("normals") + normals_node.node().addGeom(geom) + normals_node.setRenderModeThickness(1.5) + + def load_from_file(self, filepath: str) -> bool: + """ + 从文件加载导航网格 + + Args: + filepath: 导航网格文件路径 + + Returns: + 是否加载成功 + """ + try: + with open(filepath, 'r', encoding='utf-8') as f: + data = json.load(f) + + # 解析导航网格数据 + self._parse_navmesh_data(data) + + # 更新可视化 + self._update_navmesh_visualization() + + print(f"✓ 导航网格加载成功: {filepath}") + return True + + except Exception as e: + print(f"✗ 导航网格加载失败: {e}") + return False + + def _parse_navmesh_data(self, data: Dict[str, Any]): + """解析导航网格数据""" + # 清除现有数据 + self.polygons.clear() + self.obstacles.clear() + + # 解析元数据 + if 'metadata' in data: + metadata = data['metadata'] + self.version = metadata.get('version', self.version) + self.generator = metadata.get('generator', self.generator) + self.created_time = metadata.get('created', '') + self.modified_time = metadata.get('modified', '') + + # 解析设置 + if 'settings' in data: + settings = data['settings'] + self.cell_size = settings.get('cell_size', self.cell_size) + self.cell_height = settings.get('cell_height', self.cell_height) + self.agent_height = settings.get('agent_height', self.agent_height) + self.agent_radius = settings.get('agent_radius', self.agent_radius) + self.max_slope = settings.get('max_slope', self.max_slope) + self.max_climb = settings.get('max_climb', self.max_climb) + self.min_region_area = settings.get('min_region_area', self.min_region_area) + self.merge_region_area = settings.get('merge_region_area', self.merge_region_area) + self.edge_max_len = settings.get('edge_max_len', self.edge_max_len) + self.edge_max_error = settings.get('edge_max_error', self.edge_max_error) + self.verts_per_poly = settings.get('verts_per_poly', self.verts_per_poly) + + # 解析多边形 + if 'polygons' in data: + for poly_data in data['polygons']: + polygon_id = poly_data['id'] + vertices = [Point3(v[0], v[1], v[2]) for v in poly_data['vertices']] + + polygon = NavMeshPolygon(polygon_id, vertices) + + # 添加邻居信息 + if 'neighbors' in poly_data: + for neighbor_id in poly_data['neighbors']: + polygon.add_neighbor(neighbor_id) + + # 添加门户点信息 + if 'portal_points' in poly_data: + for neighbor_id, portal_data in poly_data['portal_points'].items(): + left_point = Point3(portal_data[0][0], portal_data[0][1], portal_data[0][2]) + right_point = Point3(portal_data[1][0], portal_data[1][1], portal_data[1][2]) + polygon.set_portal_points(int(neighbor_id), left_point, right_point) + + # 设置标志位 + if 'flags' in poly_data: + polygon.flags = poly_data['flags'] + + self.polygons[polygon_id] = polygon + + # 解析障碍物 + if 'obstacles' in data: + for obs_data in data['obstacles']: + obstacle_id = obs_data['id'] + self.obstacles[obstacle_id] = { + 'position': Point3(obs_data['position'][0], obs_data['position'][1], obs_data['position'][2]), + 'radius': obs_data['radius'], + 'height': obs_data['height'], + 'type': obs_data.get('type', 'cylinder') + } + + # 更新统计信息 + self._update_stats() + + # 更新空间哈希 + self._update_spatial_hash() + + print(f"✓ 解析了 {len(self.polygons)} 个多边形和 {len(self.obstacles)} 个障碍物") + + def save_to_file(self, filepath: str) -> bool: + """ + 保存导航网格到文件 + + Args: + filepath: 保存文件路径 + + Returns: + 是否保存成功 + """ + try: + import time + current_time = time.strftime("%Y-%m-%d %H:%M:%S") + + # 构建导航网格数据 + data = { + "metadata": { + "version": self.version, + "generator": self.generator, + "created": self.created_time if self.created_time else current_time, + "modified": current_time + }, + "settings": { + "cell_size": self.cell_size, + "cell_height": self.cell_height, + "agent_height": self.agent_height, + "agent_radius": self.agent_radius, + "max_slope": self.max_slope, + "max_climb": self.max_climb, + "min_region_area": self.min_region_area, + "merge_region_area": self.merge_region_area, + "edge_max_len": self.edge_max_len, + "edge_max_error": self.edge_max_error, + "verts_per_poly": self.verts_per_poly + }, + "polygons": [], + "obstacles": [] + } + + # 添加多边形数据 + for polygon_id, polygon in self.polygons.items(): + poly_data = { + "id": polygon_id, + "vertices": [[v.x, v.y, v.z] for v in polygon.vertices], + "neighbors": polygon.neighbors, + "portal_points": {}, + "flags": polygon.flags + } + + # 添加门户点数据 + for neighbor_id, (left_point, right_point) in polygon.portal_points.items(): + poly_data['portal_points'][str(neighbor_id)] = [ + [left_point.x, left_point.y, left_point.z], + [right_point.x, right_point.y, right_point.z] + ] + + data["polygons"].append(poly_data) + + # 添加障碍物数据 + for obstacle_id, obstacle in self.obstacles.items(): + obs_data = { + "id": obstacle_id, + "position": [obstacle['position'].x, obstacle['position'].y, obstacle['position'].z], + "radius": obstacle['radius'], + "height": obstacle['height'], + "type": obstacle['type'] + } + data["obstacles"].append(obs_data) + + # 保存到文件 + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + + print(f"✓ 导航网格保存成功: {filepath}") + return True + + except Exception as e: + print(f"✗ 导航网格保存失败: {e}") + return False + + def generate_from_scene(self) -> bool: + """ + 从当前场景生成导航网格 + + Returns: + 是否生成成功 + """ + try: + import time + start_time = time.time() + + print("✓ 开始从场景生成导航网格...") + + # 这里应该实现实际的导航网格生成算法 + # 为演示目的,我们创建一个更复杂的测试网格 + self._create_complex_test_navmesh() + + # 计算多边形之间的连接关系 + self._calculate_polygon_connections() + + # 计算门户点 + self._calculate_portal_points() + + # 更新可视化 + self._update_navmesh_visualization() + + # 更新统计信息 + self._update_stats() + + # 更新空间哈希 + self._update_spatial_hash() + + end_time = time.time() + self.stats['last_generation_time'] = end_time - start_time + + print(f"✓ 导航网格生成完成,耗时 {end_time - start_time:.2f} 秒") + return True + + except Exception as e: + print(f"✗ 导航网格生成失败: {e}") + return False + + def _create_complex_test_navmesh(self): + """创建复杂测试导航网格""" + # 清除现有数据 + self.polygons.clear() + + # 创建一个更复杂的测试网格(10x10的区域,包含一些障碍物) + size = 20.0 + step = 2.0 + obstacle_positions = [ + Point3(5, 0, 5), + Point3(10, 0, 10), + Point3(15, 0, 5), + Point3(5, 0, 15) + ] + + # 创建网格多边形 + polygon_id = 0 + for x in range(0, int(size/step)): + for z in range(0, int(size/step)): + # 检查是否与障碍物重叠 + x_pos = x * step - size/2 + z_pos = z * step - size/2 + center = Point3(x_pos + step/2, 0, z_pos + step/2) + + # 检查是否与障碍物重叠 + overlap = False + for obstacle_pos in obstacle_positions: + distance = (center - obstacle_pos).length() + if distance < step * 1.5: # 避开障碍物 + overlap = True + break + + if overlap: + continue + + # 创建正方形多边形 + vertices = [ + Point3(x_pos, 0, z_pos), + Point3(x_pos + step, 0, z_pos), + Point3(x_pos + step, 0, z_pos + step), + Point3(x_pos, 0, z_pos + step) + ] + + polygon = NavMeshPolygon(polygon_id, vertices) + self.polygons[polygon_id] = polygon + polygon_id += 1 + + # 创建一些不规则多边形 + irregular_polygons = [ + # L形多边形 + [ + Point3(-8, 0, -8), + Point3(-6, 0, -8), + Point3(-6, 0, -6), + Point3(-4, 0, -6), + Point3(-4, 0, -4), + Point3(-8, 0, -4) + ], + # 三角形 + [ + Point3(8, 0, -8), + Point3(10, 0, -6), + Point3(6, 0, -6) + ], + # 六边形 + [ + Point3(-8, 0, 8), + Point3(-7, 0, 9), + Point3(-5, 0, 9), + Point3(-4, 0, 8), + Point3(-5, 0, 7), + Point3(-7, 0, 7) + ] + ] + + for vertices in irregular_polygons: + polygon = NavMeshPolygon(polygon_id, vertices) + self.polygons[polygon_id] = polygon + polygon_id += 1 + + print(f"✓ 创建了 {len(self.polygons)} 个复杂测试多边形") + + def _calculate_polygon_connections(self): + """计算多边形之间的连接关系""" + # 为每个多边形找到相邻的多边形 + polygon_list = list(self.polygons.values()) + + for i, polygon in enumerate(polygon_list): + polygon.neighbors.clear() # 清除现有邻居 + + # 检查与其他多边形的共享边 + for j, other_polygon in enumerate(polygon_list): + if i == j: + continue + + # 检查是否有共享边 + if self._polygons_share_edge(polygon, other_polygon): + polygon.add_neighbor(other_polygon.polygon_id) + + def _polygons_share_edge(self, poly1: NavMeshPolygon, poly2: NavMeshPolygon) -> bool: + """检查两个多边形是否共享边""" + # 简化实现:检查是否有两个顶点非常接近 + threshold = 0.1 # 距离阈值 + + for i, v1 in enumerate(poly1.vertices): + next_i = (i + 1) % len(poly1.vertices) + edge1_start = v1 + edge1_end = poly1.vertices[next_i] + + for j, v2 in enumerate(poly2.vertices): + next_j = (j + 1) % len(poly2.vertices) + edge2_start = v2 + edge2_end = poly2.vertices[next_j] + + # 检查边是否重合(方向相同或相反) + if (self._points_equal(edge1_start, edge2_end, threshold) and + self._points_equal(edge1_end, edge2_start, threshold)): + return True # 方向相反的共享边 + + if (self._points_equal(edge1_start, edge2_start, threshold) and + self._points_equal(edge1_end, edge2_end, threshold)): + return True # 方向相同的共享边 + + return False + + def _points_equal(self, p1: Point3, p2: Point3, threshold: float) -> bool: + """检查两点是否相等(在阈值范围内)""" + return (p1 - p2).length() < threshold + + def _calculate_portal_points(self): + """计算多边形之间的门户点""" + for polygon in self.polygons.values(): + polygon.portal_points.clear() # 清除现有门户点 + + for neighbor_id in polygon.neighbors: + if neighbor_id in self.polygons: + neighbor_polygon = self.polygons[neighbor_id] + portal_points = self._calculate_portal_between_polygons(polygon, neighbor_polygon) + if portal_points: + left_point, right_point = portal_points + polygon.set_portal_points(neighbor_id, left_point, right_point) + # 同时在邻居多边形上设置反向门户点 + neighbor_polygon.set_portal_points(polygon.polygon_id, right_point, left_point) + + def _calculate_portal_between_polygons(self, poly1: NavMeshPolygon, poly2: NavMeshPolygon) -> Optional[Tuple[Point3, Point3]]: + """计算两个多边形之间的门户点""" + # 简化实现:找到两个多边形共享边的两个端点 + threshold = 0.1 + + for i, v1 in enumerate(poly1.vertices): + next_i = (i + 1) % len(poly1.vertices) + edge1_start = v1 + edge1_end = poly1.vertices[next_i] + + for j, v2 in enumerate(poly2.vertices): + next_j = (j + 1) % len(poly2.vertices) + edge2_start = v2 + edge2_end = poly2.vertices[next_j] + + # 检查边是否重合(方向相同) + if (self._points_equal(edge1_start, edge2_start, threshold) and + self._points_equal(edge1_end, edge2_end, threshold)): + return (edge1_start, edge1_end) + + # 检查边是否重合(方向相反) + if (self._points_equal(edge1_start, edge2_end, threshold) and + self._points_equal(edge1_end, edge2_start, threshold)): + return (edge1_end, edge1_start) # 保持poly1到poly2的方向 + + return None + + def is_point_walkable(self, point: Point3) -> bool: + """ + 检查点是否可行走 + + Args: + point: 检查的点 + + Returns: + 是否可行走 + """ + if not self.polygons: + return False + + # 首先检查是否有动态障碍物阻挡 + for obstacle in self.obstacles.values(): + obstacle_pos = obstacle['position'] + distance = (Point3(point.x, 0, point.z) - Point3(obstacle_pos.x, 0, obstacle_pos.z)).length() + if distance < obstacle['radius']: + # 检查高度 + if obstacle_pos.y <= point.y <= obstacle_pos.y + obstacle['height']: + return False # 被障碍物阻挡 + + # 检查点是否在任何多边形内 + if self.use_spatial_hashing: + # 使用空间哈希加速查找 + x_key = int(point.x / self.spatial_hash_cell_size) + z_key = int(point.z / self.spatial_hash_cell_size) + key = (x_key, z_key) + + if key in self.spatial_hash_grid: + for polygon_id in self.spatial_hash_grid[key]: + if polygon_id in self.polygons: + polygon = self.polygons[polygon_id] + if polygon.is_point_in_bounding_box(point) and polygon.is_point_inside(point): + return polygon.walkable + else: + # 暴力查找 + for polygon in self.polygons.values(): + if polygon.is_point_in_bounding_box(point) and polygon.is_point_inside(point): + return polygon.walkable + + return False + + def get_closest_point(self, point: Point3) -> Optional[Point3]: + """ + 获取导航网格上最近的点 + + Args: + point: 目标点 + + Returns: + 最近的可行走点,如果找不到则返回None + """ + if not self.polygons: + return None + + closest_point = None + min_distance = float('inf') + + # 使用空间哈希加速查找附近的多边形 + search_radius = 5.0 # 初始搜索半径 + polygons_to_check = self._get_polygons_in_radius(point, search_radius) + + # 如果没找到,扩大搜索范围 + if not polygons_to_check: + search_radius = 20.0 + polygons_to_check = self._get_polygons_in_radius(point, search_radius) + + # 如果仍然没找到,检查所有多边形 + if not polygons_to_check: + polygons_to_check = list(self.polygons.keys()) + + # 遍历相关多边形找到最近的点 + for polygon_id in polygons_to_check: + if polygon_id in self.polygons: + polygon = self.polygons[polygon_id] + poly_point = polygon.get_closest_point(point) + distance = (poly_point - point).length() + + if distance < min_distance: + min_distance = distance + closest_point = poly_point + + return closest_point + + def get_random_point(self) -> Optional[Point3]: + """ + 获取导航网格上的随机点 + + Returns: + 随机可行走点,如果导航网格为空则返回None + """ + if not self.polygons: + return None + + # 随机选择一个多边形 + polygon = random.choice(list(self.polygons.values())) + + # 在多边形内生成随机点 + if len(polygon.vertices) >= 3: + # 使用重心坐标生成多边形内的随机点 + # 选择三个随机顶点 + indices = random.sample(range(len(polygon.vertices)), + min(3, len(polygon.vertices))) + + if len(indices) == 3: + v1 = polygon.vertices[indices[0]] + v2 = polygon.vertices[indices[1]] + v3 = polygon.vertices[indices[2]] + + # 生成重心坐标 + r1 = random.random() + r2 = random.random() + if r1 + r2 > 1: + r1 = 1 - r1 + r2 = 1 - r2 + + r3 = 1 - r1 - r2 + + # 计算随机点 + random_point = v1 * r1 + v2 * r2 + v3 * r3 + return random_point + + # 如果无法生成重心坐标,返回中心点 + return polygon.center + + def get_polygon_containing_point(self, point: Point3) -> Optional[NavMeshPolygon]: + """ + 获取包含指定点的多边形 + + Args: + point: 目标点 + + Returns: + 包含点的多边形,如果找不到则返回None + """ + # 使用空间哈希加速查找 + if self.use_spatial_hashing: + x_key = int(point.x / self.spatial_hash_cell_size) + z_key = int(point.z / self.spatial_hash_cell_size) + key = (x_key, z_key) + + if key in self.spatial_hash_grid: + for polygon_id in self.spatial_hash_grid[key]: + if polygon_id in self.polygons: + polygon = self.polygons[polygon_id] + if polygon.is_point_in_bounding_box(point) and polygon.is_point_inside(point): + return polygon + else: + # 暴力查找 + for polygon in self.polygons.values(): + if polygon.is_point_in_bounding_box(point) and polygon.is_point_inside(point): + return polygon + + return None + + def get_adjacent_polygons(self, polygon_id: int) -> List[NavMeshPolygon]: + """ + 获取相邻多边形列表 + + Args: + polygon_id: 多边形ID + + Returns: + 相邻多边形列表 + """ + if polygon_id not in self.polygons: + return [] + + polygon = self.polygons[polygon_id] + adjacent_polygons = [] + + for neighbor_id in polygon.neighbors: + if neighbor_id in self.polygons: + adjacent_polygons.append(self.polygons[neighbor_id]) + + return adjacent_polygons + + def visualize_path(self, path: List[Point3]): + """ + 可视化路径 + + Args: + path: 路径点列表 + """ + if not self.path_visualization_node or not path: + return + + # 清除现有路径可视化 + self.path_visualization_node.node().removeAllGeoms() + + if len(path) < 2: + return + + # 创建路径几何体 + self._create_path_geometry(path) + + # 显示路径 + self.path_visualization_node.show() + + def _create_path_geometry(self, path: List[Point3]): + """创建路径几何体""" + if not self.path_visualization_node or len(path) < 2: + return + + # 创建顶点格式 + vertex_format = GeomVertexFormat.getV3c4() + vertex_data = GeomVertexData("path", vertex_format, Geom.UHStatic) + vertex_data.setNumRows(len(path)) + + # 创建顶点写入器 + vertex_writer = GeomVertexWriter(vertex_data, "vertex") + color_writer = GeomVertexWriter(vertex_data, "color") + + # 写入路径点 + for point in path: + vertex_writer.addData3f(point.x, point.y + 0.1, point.z) # 稍微抬高避免与地面重叠 + color_writer.addData4f(1.0, 0.0, 0.0, 1.0) # 红色 + + # 创建几何体 + geom = Geom(vertex_data) + lines = GeomLines(Geom.UHStatic) + + # 添加线段 + for i in range(len(path) - 1): + lines.addVertex(i) + lines.addVertex(i + 1) + lines.closePrimitive() + + geom.addPrimitive(lines) + self.path_visualization_node.node().addGeom(geom) + + # 设置线宽 + self.path_visualization_node.setRenderModeThickness(3.0) + + # 添加路径点标记 + self._create_path_point_markers(path) + + def _create_path_point_markers(self, path: List[Point3]): + """创建路径点标记""" + if not self.path_visualization_node or not path: + return + + marker_node = self.path_visualization_node.attachNewNode("path_markers") + + for i, point in enumerate(path): + # 创建标记球体 + marker = self.world.render.attachNewNode(f"path_marker_{i}") + marker.setPos(point.x, point.y + 0.2, point.z) + + # 可以在这里添加可视化标记(比如一个小球) + # 为简化,我们只添加位置信息 + + def clear_path_visualization(self): + """清除路径可视化""" + if self.path_visualization_node: + self.path_visualization_node.node().removeAllGeoms() + self.path_visualization_node.hide() + + def is_navmesh_loaded(self) -> bool: + """检查导航网格是否已加载""" + return len(self.polygons) > 0 + + def get_polygon_count(self) -> int: + """获取多边形数量""" + return len(self.polygons) + + def add_polygon(self, vertices: List[Point3]) -> int: + """ + 添加多边形 + + Args: + vertices: 多边形顶点列表 + + Returns: + 新多边形的ID + """ + polygon_id = self.polygon_counter + self.polygon_counter += 1 + + polygon = NavMeshPolygon(polygon_id, vertices) + self.polygons[polygon_id] = polygon + + # 更新空间哈希 + self._update_spatial_hash() + + # 更新可视化 + self._update_navmesh_visualization() + + # 更新统计信息 + self._update_stats() + + return polygon_id + + def remove_polygon(self, polygon_id: int) -> bool: + """ + 移除多边形 + + Args: + polygon_id: 多边形ID + + Returns: + 是否移除成功 + """ + if polygon_id in self.polygons: + # 从邻居多边形中移除这个多边形的引用 + polygon = self.polygons[polygon_id] + for neighbor_id in polygon.neighbors: + if neighbor_id in self.polygons: + self.polygons[neighbor_id].remove_neighbor(polygon_id) + + # 移除多边形 + del self.polygons[polygon_id] + + # 更新空间哈希 + self._update_spatial_hash() + + # 更新可视化 + self._update_navmesh_visualization() + + # 更新统计信息 + self._update_stats() + + return True + return False + + def add_obstacle(self, position: Point3, radius: float, height: float, obstacle_type: str = "cylinder") -> int: + """ + 添加动态障碍物 + + Args: + position: 障碍物位置 + radius: 障碍物半径 + height: 障碍物高度 + obstacle_type: 障碍物类型 + + Returns: + 新障碍物的ID + """ + obstacle_id = self.obstacle_counter + self.obstacle_counter += 1 + + self.obstacles[obstacle_id] = { + 'position': position, + 'radius': radius, + 'height': height, + 'type': obstacle_type + } + + # 更新统计信息 + self._update_stats() + + # 更新可视化 + self._update_obstacles_visualization() + + return obstacle_id + + def remove_obstacle(self, obstacle_id: int) -> bool: + """ + 移除动态障碍物 + + Args: + obstacle_id: 障碍物ID + + Returns: + 是否移除成功 + """ + if obstacle_id in self.obstacles: + del self.obstacles[obstacle_id] + + # 更新统计信息 + self._update_stats() + + # 更新可视化 + self._update_obstacles_visualization() + + return True + return False + + def _update_obstacles_visualization(self): + """更新障碍物可视化""" + if not self.obstacles_node: + return + + # 清除现有障碍物可视化 + self.obstacles_node.node().removeAllGeoms() + + if not self.obstacles or not self.navmesh_visible: + return + + # 创建障碍物几何体 + self._create_obstacles_geometry() + + # 显示障碍物 + self.obstacles_node.show() + + def _create_obstacles_geometry(self): + """创建障碍物几何体""" + if not self.obstacles_node or not self.obstacles: + return + + # 为每个障碍物创建可视化表示 + for obstacle_id, obstacle in self.obstacles.items(): + # 创建圆柱体表示障碍物 + # 这里简化处理,实际应用中可能需要更复杂的几何体 + + # 创建顶点格式 + vertex_format = GeomVertexFormat.getV3c4() + vertex_data = GeomVertexData(f"obstacle_{obstacle_id}", vertex_format, Geom.UHStatic) + + # 简化:创建一个正方形表示障碍物 + pos = obstacle['position'] + radius = obstacle['radius'] + + vertices = [ + Point3(pos.x - radius, pos.y, pos.z - radius), + Point3(pos.x + radius, pos.y, pos.z - radius), + Point3(pos.x + radius, pos.y, pos.z + radius), + Point3(pos.x - radius, pos.y, pos.z + radius), + Point3(pos.x - radius, pos.y + obstacle['height'], pos.z - radius), + Point3(pos.x + radius, pos.y + obstacle['height'], pos.z - radius), + Point3(pos.x + radius, pos.y + obstacle['height'], pos.z + radius), + Point3(pos.x - radius, pos.y + obstacle['height'], pos.z + radius) + ] + + vertex_data.setNumRows(len(vertices)) + + # 创建顶点写入器 + vertex_writer = GeomVertexWriter(vertex_data, "vertex") + color_writer = GeomVertexWriter(vertex_data, "color") + + # 写入顶点 + for vertex in vertices: + vertex_writer.addData3f(vertex.x, vertex.y, vertex.z) + color_writer.addData4f(1.0, 0.0, 0.0, 0.5) # 红色半透明 + + # 创建几何体 + geom = Geom(vertex_data) + + # 创建面(简化为立方体) + # 底面 + triangles = GeomTriangles(Geom.UHStatic) + triangles.addVertices(0, 1, 2) + triangles.addVertices(0, 2, 3) + triangles.closePrimitive() + geom.addPrimitive(triangles) + + # 顶面 + triangles = GeomTriangles(Geom.UHStatic) + triangles.addVertices(4, 6, 5) + triangles.addVertices(4, 7, 6) + triangles.closePrimitive() + geom.addPrimitive(triangles) + + # 前面 + triangles = GeomTriangles(Geom.UHStatic) + triangles.addVertices(0, 4, 5) + triangles.addVertices(0, 5, 1) + triangles.closePrimitive() + geom.addPrimitive(triangles) + + # 后面 + triangles = GeomTriangles(Geom.UHStatic) + triangles.addVertices(2, 6, 7) + triangles.addVertices(2, 7, 3) + triangles.closePrimitive() + geom.addPrimitive(triangles) + + # 左面 + triangles = GeomTriangles(Geom.UHStatic) + triangles.addVertices(0, 3, 7) + triangles.addVertices(0, 7, 4) + triangles.closePrimitive() + geom.addPrimitive(triangles) + + # 右面 + triangles = GeomTriangles(Geom.UHStatic) + triangles.addVertices(1, 5, 6) + triangles.addVertices(1, 6, 2) + triangles.closePrimitive() + geom.addPrimitive(triangles) + + obstacle_node = self.obstacles_node.attachNewNode(f"obstacle_geom_{obstacle_id}") + obstacle_node.node().addGeom(geom) + + def _update_stats(self): + """更新统计信息""" + total_vertices = sum(len(polygon.vertices) for polygon in self.polygons.values()) + total_edges = sum(len(polygon.vertices) for polygon in self.polygons.values()) # 简化处理 + total_portals = sum(len(polygon.portal_points) for polygon in self.polygons.values()) + + self.stats.update({ + 'polygons': len(self.polygons), + 'vertices': total_vertices, + 'edges': total_edges, + 'portals': total_portals, + 'obstacles': len(self.obstacles) + }) + + def get_stats(self) -> Dict[str, Any]: + """获取统计信息""" + return self.stats.copy() + + def set_cell_size(self, size: float): + """设置网格单元大小""" + self.cell_size = max(0.1, size) + + def set_agent_height(self, height: float): + """设置代理高度""" + self.agent_height = max(0.1, height) + + def set_agent_radius(self, radius: float): + """设置代理半径""" + self.agent_radius = max(0.1, radius) + + def set_max_slope(self, slope: float): + """设置最大坡度""" + self.max_slope = max(0.0, min(89.0, slope)) + + def set_max_climb(self, climb: float): + """设置最大可爬升高度""" + self.max_climb = max(0.0, climb) + + def set_min_region_area(self, area: float): + """设置最小区域面积""" + self.min_region_area = max(1, area) + + def set_merge_region_area(self, area: float): + """设置合并区域面积阈值""" + self.merge_region_area = max(1, area) + + def set_edge_max_len(self, length: float): + """设置边最大长度""" + self.edge_max_len = max(0.1, length) + + def set_edge_max_error(self, error: float): + """设置边最大误差""" + self.edge_max_error = max(0.1, error) + + def set_verts_per_poly(self, count: int): + """设置每个多边形最大顶点数""" + self.verts_per_poly = max(3, count) + + def enable_spatial_hashing(self, enabled: bool): + """启用/禁用空间哈希""" + self.use_spatial_hashing = enabled + if enabled: + self._update_spatial_hash() + + def set_spatial_hash_cell_size(self, size: float): + """设置空间哈希网格单元大小""" + self.spatial_hash_cell_size = max(0.1, size) + if self.use_spatial_hashing: + self._update_spatial_hash() + + def rebuild_navmesh(self): + """重新构建导航网格""" + print("✓ 重新构建导航网格...") + return self.generate_from_scene() + + def optimize_navmesh(self): + """优化导航网格""" + print("✓ 优化导航网格...") + # 这里可以实现多边形合并、简化等优化算法 + # 为简化,我们只更新连接关系 + self._calculate_polygon_connections() + self._calculate_portal_points() + self._update_navmesh_visualization() + print("✓ 导航网格优化完成") + + def validate_navmesh(self) -> bool: + """验证导航网格的完整性""" + print("✓ 验证导航网格完整性...") + + issues = [] + + # 检查多边形数据 + for polygon_id, polygon in self.polygons.items(): + if len(polygon.vertices) < 3: + issues.append(f"多边形 {polygon_id} 顶点数不足") + + # 检查邻居关系一致性 + for neighbor_id in polygon.neighbors: + if neighbor_id not in self.polygons: + issues.append(f"多边形 {polygon_id} 的邻居 {neighbor_id} 不存在") + else: + # 检查双向连接 + neighbor = self.polygons[neighbor_id] + if polygon_id not in neighbor.neighbors: + issues.append(f"多边形 {polygon_id} 和 {neighbor_id} 的连接关系不一致") + + # 检查门户点数据 + for polygon_id, polygon in self.polygons.items(): + for neighbor_id, (left_point, right_point) in polygon.portal_points.items(): + if neighbor_id not in polygon.neighbors: + issues.append(f"多边形 {polygon_id} 存在未连接邻居 {neighbor_id} 的门户点") + + if issues: + print(f"✗ 发现 {len(issues)} 个问题:") + for issue in issues: + print(f" - {issue}") + return False + else: + print("✓ 导航网格验证通过") return True \ No newline at end of file diff --git a/plugins/user/navigation_mesh/editor/navmesh_editor.py b/plugins/user/navigation_mesh/editor/navmesh_editor.py index 612bfb19..404828c4 100644 --- a/plugins/user/navigation_mesh/editor/navmesh_editor.py +++ b/plugins/user/navigation_mesh/editor/navmesh_editor.py @@ -1,478 +1,478 @@ -""" -导航网格编辑器 -提供可视化编辑导航网格的功能 -""" - -from typing import List, Optional, Dict, Any -from panda3d.core import Vec3, Point3, KeyboardButton, MouseWatcher - -class NavMeshEditor: - """ - 导航网格编辑器 - 提供可视化编辑导航网格的功能 - """ - - def __init__(self, world, navmesh_manager): - """ - 初始化导航网格编辑器 - - Args: - world: 3D世界对象 - navmesh_manager: 导航网格管理器 - """ - self.world = world - self.navmesh_manager = navmesh_manager - self.visible = False - - # 编辑器状态 - self.edit_mode = False - self.selected_polygon = None - self.edit_tool = "select" # select, add, delete, paint - self.brush_size = 1.0 - - # 编辑器GUI元素 - self.gui_elements = [] - self.tool_buttons = {} - self.property_controls = {} - - # 鼠标交互 - self.mouse_watcher = None - self.last_mouse_pos = None - - # 编辑历史(用于撤销/重做) - self.edit_history = [] - self.history_index = -1 - self.max_history_size = 50 - - print("✓ 导航网格编辑器初始化完成") - - def toggle_visibility(self): - """切换编辑器可见性""" - if self.visible: - self.hide() - else: - self.show() - - def show(self): - """显示编辑器""" - if self.visible: - return - - self.visible = True - self._create_editor_gui() - print("✓ 导航网格编辑器已显示") - - def hide(self): - """隐藏编辑器""" - if not self.visible: - return - - self.visible = False - self._cleanup_editor_gui() - print("✓ 导航网格编辑器已隐藏") - - def _create_editor_gui(self): - """创建编辑器GUI""" - try: - if not hasattr(self.world, 'gui_manager') or not self.world.gui_manager: - print("⚠ GUI管理器不可用,无法创建编辑器界面") - return - - gui_manager = self.world.gui_manager - - # 创建编辑器面板标题 - panel = gui_manager.createGUIButton( - pos=(0.7, 0, 0.95), - text="导航网格编辑器", - size=0.06 - ) - self.gui_elements.append(panel) - - # 创建工具按钮 - tools = [ - ("选择", "select"), - ("添加", "add"), - ("删除", "delete"), - ("绘制", "paint") - ] - - for i, (name, tool) in enumerate(tools): - button = gui_manager.createGUIButton( - pos=(0.7, 0, 0.88 - i * 0.06), - text=name, - size=0.04 - ) - self.gui_elements.append(button) - self.tool_buttons[tool] = button - self.world.accept(f"navmesh_tool_{tool}", - lambda t=tool: self.set_edit_tool(t)) - - # 创建编辑功能按钮 - functions = [ - ("生成网格", self.generate_navmesh), - ("加载网格", self.load_navmesh), - ("保存网格", self.save_navmesh), - ("清除网格", self.clear_navmesh), - ("撤销", self.undo), - ("重做", self.redo) - ] - - for i, (name, callback) in enumerate(functions): - button = gui_manager.createGUIButton( - pos=(0.82, 0, 0.88 - i * 0.06), - text=name, - size=0.04 - ) - self.gui_elements.append(button) - self.world.accept(f"navmesh_func_{name}", callback) - - # 创建属性控制 - self._create_property_controls() - - # 显示网格 - self.navmesh_manager.toggle_visibility() - - except Exception as e: - print(f"⚠ 编辑器GUI创建失败: {e}") - - def _create_property_controls(self): - """创建属性控制面板""" - if not hasattr(self.world, 'gui_manager') or not self.world.gui_manager: - return - - gui_manager = self.world.gui_manager - - # 网格属性 - properties = [ - ("网格大小", "cell_size", 0.1, 2.0, 0.1), - ("网格高度", "cell_height", 0.1, 1.0, 0.1), - ("代理高度", "agent_height", 0.5, 3.0, 0.1), - ("代理半径", "agent_radius", 0.1, 2.0, 0.1), - ("最大坡度", "max_slope", 0.0, 89.0, 1.0), - ("笔刷大小", "brush_size", 0.1, 5.0, 0.1) - ] - - for i, (name, prop, min_val, max_val, step) in enumerate(properties): - y_pos = 0.5 - i * 0.055 - - # 属性标签 - label = gui_manager.createGUIButton( - pos=(0.75, 0, y_pos), - text=f"{name}:", - size=0.03 - ) - self.gui_elements.append(label) - - # 属性值显示 - value_label = gui_manager.createGUIButton( - pos=(0.88, 0, y_pos), - text=f"{getattr(self.navmesh_manager, prop, 0.0):.2f}", - size=0.03 - ) - self.gui_elements.append(value_label) - - # 减少按钮 - dec_button = gui_manager.createGUIButton( - pos=(0.83, 0, y_pos), - text="-", - size=0.03 - ) - self.gui_elements.append(dec_button) - - # 增加按钮 - inc_button = gui_manager.createGUIButton( - pos=(0.93, 0, y_pos), - text="+", - size=0.03 - ) - self.gui_elements.append(inc_button) - - # 注册事件 - self.world.accept(f"dec_{prop}", lambda p=prop, s=step: self._decrease_property(p, s)) - self.world.accept(f"inc_{prop}", lambda p=prop, s=step: self._increase_property(p, s)) - - # 存储控制信息 - self.property_controls[prop] = { - 'name': name, - 'min': min_val, - 'max': max_val, - 'step': step, - 'value_label': value_label, - 'dec_button': dec_button, - 'inc_button': inc_button - } - - def _cleanup_editor_gui(self): - """清理编辑器GUI""" - try: - if hasattr(self.world, 'gui_manager') and self.world.gui_manager: - gui_manager = self.world.gui_manager - - for element in self.gui_elements: - gui_manager.deleteGUIElement(element) - - # 清理事件监听 - tools = ["select", "add", "delete", "paint"] - for tool in tools: - self.world.ignore(f"navmesh_tool_{tool}") - - functions = ["生成网格", "加载网格", "保存网格", "清除网格", "撤销", "重做"] - for func in functions: - self.world.ignore(f"navmesh_func_{func}") - - # 清理属性控制事件 - properties = ["cell_size", "cell_height", "agent_height", "agent_radius", - "max_slope", "brush_size"] - for prop in properties: - self.world.ignore(f"dec_{prop}") - self.world.ignore(f"inc_{prop}") - - self.gui_elements.clear() - self.tool_buttons.clear() - self.property_controls.clear() - - except Exception as e: - print(f"⚠ 编辑器GUI清理失败: {e}") - - def _decrease_property(self, prop_name: str, step: float): - """减少属性值""" - if hasattr(self.navmesh_manager, prop_name): - current_value = getattr(self.navmesh_manager, prop_name) - control = self.property_controls[prop_name] - new_value = max(control['min'], current_value - step) - setattr(self.navmesh_manager, prop_name, new_value) - - # 更新显示 - if 'value_label' in control: - control['value_label'].setText(f"{new_value:.2f}") - - def _increase_property(self, prop_name: str, step: float): - """增加属性值""" - if hasattr(self.navmesh_manager, prop_name): - current_value = getattr(self.navmesh_manager, prop_name) - control = self.property_controls[prop_name] - new_value = min(control['max'], current_value + step) - setattr(self.navmesh_manager, prop_name, new_value) - - # 更新显示 - if 'value_label' in control: - control['value_label'].setText(f"{new_value:.2f}") - - def set_edit_tool(self, tool: str): - """设置编辑工具""" - self.edit_tool = tool - print(f"✓ 切换到 {tool} 工具") - - # 更新按钮状态 - for tool_name, button in self.tool_buttons.items(): - if tool_name == tool: - # 高亮选中工具 - button.setColor(0.5, 1.0, 0.5, 1.0) # 绿色 - else: - # 恢复默认颜色 - button.setColor(1.0, 1.0, 1.0, 1.0) # 白色 - - def generate_navmesh(self): - """生成导航网格""" - if self.navmesh_manager: - success = self.navmesh_manager.generate_from_scene() - if success: - print("✓ 导航网格生成完成") - else: - print("✗ 导航网格生成失败") - - def load_navmesh(self): - """加载导航网格""" - # 简化实现:加载测试网格 - if self.navmesh_manager: - self.navmesh_manager._create_test_navmesh() - print("✓ 导航网格已加载(测试数据)") - - def save_navmesh(self): - """保存导航网格""" - print("⚠ 保存功能需要实现文件对话框") - - def clear_navmesh(self): - """清除导航网格""" - if self.navmesh_manager: - self.navmesh_manager.polygons.clear() - self.navmesh_manager._update_navmesh_visualization() - print("✓ 导航网格已清除") - - def undo(self): - """撤销操作""" - if self.history_index >= 0: - self.history_index -= 1 - print("✓ 撤销操作") - else: - print("⚠ 没有可撤销的操作") - - def redo(self): - """重做操作""" - if self.history_index < len(self.edit_history) - 1: - self.history_index += 1 - print("✓ 重做操作") - else: - print("⚠ 没有可重做的操作") - - def _add_to_history(self, operation: Dict[str, Any]): - """添加操作到历史记录""" - # 移除历史记录中超过当前索引的部分 - if self.history_index < len(self.edit_history) - 1: - self.edit_history = self.edit_history[:self.history_index + 1] - - # 添加新操作 - self.edit_history.append(operation) - - # 限制历史记录大小 - if len(self.edit_history) > self.max_history_size: - self.edit_history.pop(0) - else: - self.history_index += 1 - - def select_polygon(self, polygon_id: int): - """选择多边形""" - self.selected_polygon = polygon_id - print(f"✓ 选择多边形: {polygon_id}") - - def add_polygon(self, vertices: List[Point3]): - """添加多边形""" - if self.navmesh_manager: - polygon_id = self.navmesh_manager.add_polygon(vertices) - print(f"✓ 添加多边形: {polygon_id}") - - # 添加到历史记录 - operation = { - 'type': 'add_polygon', - 'polygon_id': polygon_id - } - self._add_to_history(operation) - - def delete_polygon(self, polygon_id: int): - """删除多边形""" - if self.navmesh_manager: - success = self.navmesh_manager.remove_polygon(polygon_id) - if success: - print(f"✓ 删除多边形: {polygon_id}") - - # 添加到历史记录 - operation = { - 'type': 'delete_polygon', - 'polygon_id': polygon_id - } - self._add_to_history(operation) - else: - print(f"⚠ 无法删除多边形: {polygon_id}") - - def paint_walkable_area(self, center: Point3): - """绘制可行走区域""" - print(f"✓ 在 {center} 附近绘制可行走区域") - - def toggle_edit_mode(self): - """切换编辑模式""" - self.edit_mode = not self.edit_mode - if self.edit_mode: - print("✓ 进入编辑模式") - else: - print("✓ 退出编辑模式") - - def set_brush_size(self, size: float): - """设置笔刷大小""" - self.brush_size = max(0.1, size) - print(f"✓ 笔刷大小设置为: {self.brush_size:.2f}") - - def get_editor_state(self) -> Dict[str, Any]: - """获取编辑器状态""" - return { - 'visible': self.visible, - 'edit_mode': self.edit_mode, - 'selected_tool': self.edit_tool, - 'selected_polygon': self.selected_polygon, - 'brush_size': self.brush_size, - 'polygon_count': len(self.navmesh_manager.polygons) if self.navmesh_manager else 0 - } - - def set_editor_state(self, state: Dict[str, Any]): - """设置编辑器状态""" - try: - if 'edit_mode' in state: - self.edit_mode = state['edit_mode'] - - if 'selected_tool' in state: - self.set_edit_tool(state['selected_tool']) - - if 'brush_size' in state: - self.set_brush_size(state['brush_size']) - - print("✓ 编辑器状态已恢复") - - except Exception as e: - print(f"⚠ 恢复编辑器状态失败: {e}") - - def handle_mouse_click(self, mouse_x: float, mouse_y: float): - """处理鼠标点击事件""" - if not self.edit_mode: - return - - # 将屏幕坐标转换为世界坐标(简化实现) - world_x = mouse_x * 20 # 假设场景大小为20x20 - world_z = mouse_y * 20 - click_point = Point3(world_x, 0, world_z) - - if self.edit_tool == "add": - # 添加多边形(简化为正方形) - half_size = 0.5 - vertices = [ - Point3(world_x - half_size, 0, world_z - half_size), - Point3(world_x + half_size, 0, world_z - half_size), - Point3(world_x + half_size, 0, world_z + half_size), - Point3(world_x - half_size, 0, world_z + half_size) - ] - self.add_polygon(vertices) - - elif self.edit_tool == "delete": - # 删除多边形 - if self.navmesh_manager: - polygon = self.navmesh_manager.get_polygon_containing_point(click_point) - if polygon: - self.delete_polygon(polygon.polygon_id) - - elif self.edit_tool == "paint": - # 绘制可行走区域 - self.paint_walkable_area(click_point) - - elif self.edit_tool == "select": - # 选择多边形 - if self.navmesh_manager: - polygon = self.navmesh_manager.get_polygon_containing_point(click_point) - if polygon: - self.select_polygon(polygon.polygon_id) - - def update(self, dt: float): - """更新编辑器状态""" - # 处理键盘输入 - if self.world.mouseWatcherNode.hasMouse(): - mouse_pos = self.world.mouseWatcherNode.getMouse() - - # 检查鼠标按键 - if self.world.mouseWatcherNode.isButtonDown(MouseWatcher.getGlobalPointer()): - if self.last_mouse_pos is None or \ - (mouse_pos - self.last_mouse_pos).length() > 0.01: - self.handle_mouse_click(mouse_pos.x, mouse_pos.y) - self.last_mouse_pos = mouse_pos - - # 检查键盘快捷键 - if self.world.mouseWatcherNode.isButtonDown(KeyboardButton.asciiKey('e')): - self.toggle_edit_mode() - - def cleanup(self): - """清理编辑器资源""" - self.hide() - - # 清理历史记录 - self.edit_history.clear() - self.history_index = -1 - - # 清理状态 - self.selected_polygon = None +""" +导航网格编辑器 +提供可视化编辑导航网格的功能 +""" + +from typing import List, Optional, Dict, Any +from panda3d.core import Vec3, Point3, KeyboardButton, MouseWatcher + +class NavMeshEditor: + """ + 导航网格编辑器 + 提供可视化编辑导航网格的功能 + """ + + def __init__(self, world, navmesh_manager): + """ + 初始化导航网格编辑器 + + Args: + world: 3D世界对象 + navmesh_manager: 导航网格管理器 + """ + self.world = world + self.navmesh_manager = navmesh_manager + self.visible = False + + # 编辑器状态 + self.edit_mode = False + self.selected_polygon = None + self.edit_tool = "select" # select, add, delete, paint + self.brush_size = 1.0 + + # 编辑器GUI元素 + self.gui_elements = [] + self.tool_buttons = {} + self.property_controls = {} + + # 鼠标交互 + self.mouse_watcher = None + self.last_mouse_pos = None + + # 编辑历史(用于撤销/重做) + self.edit_history = [] + self.history_index = -1 + self.max_history_size = 50 + + print("✓ 导航网格编辑器初始化完成") + + def toggle_visibility(self): + """切换编辑器可见性""" + if self.visible: + self.hide() + else: + self.show() + + def show(self): + """显示编辑器""" + if self.visible: + return + + self.visible = True + self._create_editor_gui() + print("✓ 导航网格编辑器已显示") + + def hide(self): + """隐藏编辑器""" + if not self.visible: + return + + self.visible = False + self._cleanup_editor_gui() + print("✓ 导航网格编辑器已隐藏") + + def _create_editor_gui(self): + """创建编辑器GUI""" + try: + if not hasattr(self.world, 'gui_manager') or not self.world.gui_manager: + print("⚠ GUI管理器不可用,无法创建编辑器界面") + return + + gui_manager = self.world.gui_manager + + # 创建编辑器面板标题 + panel = gui_manager.createGUIButton( + pos=(0.7, 0, 0.95), + text="导航网格编辑器", + size=0.06 + ) + self.gui_elements.append(panel) + + # 创建工具按钮 + tools = [ + ("选择", "select"), + ("添加", "add"), + ("删除", "delete"), + ("绘制", "paint") + ] + + for i, (name, tool) in enumerate(tools): + button = gui_manager.createGUIButton( + pos=(0.7, 0, 0.88 - i * 0.06), + text=name, + size=0.04 + ) + self.gui_elements.append(button) + self.tool_buttons[tool] = button + self.world.accept(f"navmesh_tool_{tool}", + lambda t=tool: self.set_edit_tool(t)) + + # 创建编辑功能按钮 + functions = [ + ("生成网格", self.generate_navmesh), + ("加载网格", self.load_navmesh), + ("保存网格", self.save_navmesh), + ("清除网格", self.clear_navmesh), + ("撤销", self.undo), + ("重做", self.redo) + ] + + for i, (name, callback) in enumerate(functions): + button = gui_manager.createGUIButton( + pos=(0.82, 0, 0.88 - i * 0.06), + text=name, + size=0.04 + ) + self.gui_elements.append(button) + self.world.accept(f"navmesh_func_{name}", callback) + + # 创建属性控制 + self._create_property_controls() + + # 显示网格 + self.navmesh_manager.toggle_visibility() + + except Exception as e: + print(f"⚠ 编辑器GUI创建失败: {e}") + + def _create_property_controls(self): + """创建属性控制面板""" + if not hasattr(self.world, 'gui_manager') or not self.world.gui_manager: + return + + gui_manager = self.world.gui_manager + + # 网格属性 + properties = [ + ("网格大小", "cell_size", 0.1, 2.0, 0.1), + ("网格高度", "cell_height", 0.1, 1.0, 0.1), + ("代理高度", "agent_height", 0.5, 3.0, 0.1), + ("代理半径", "agent_radius", 0.1, 2.0, 0.1), + ("最大坡度", "max_slope", 0.0, 89.0, 1.0), + ("笔刷大小", "brush_size", 0.1, 5.0, 0.1) + ] + + for i, (name, prop, min_val, max_val, step) in enumerate(properties): + y_pos = 0.5 - i * 0.055 + + # 属性标签 + label = gui_manager.createGUIButton( + pos=(0.75, 0, y_pos), + text=f"{name}:", + size=0.03 + ) + self.gui_elements.append(label) + + # 属性值显示 + value_label = gui_manager.createGUIButton( + pos=(0.88, 0, y_pos), + text=f"{getattr(self.navmesh_manager, prop, 0.0):.2f}", + size=0.03 + ) + self.gui_elements.append(value_label) + + # 减少按钮 + dec_button = gui_manager.createGUIButton( + pos=(0.83, 0, y_pos), + text="-", + size=0.03 + ) + self.gui_elements.append(dec_button) + + # 增加按钮 + inc_button = gui_manager.createGUIButton( + pos=(0.93, 0, y_pos), + text="+", + size=0.03 + ) + self.gui_elements.append(inc_button) + + # 注册事件 + self.world.accept(f"dec_{prop}", lambda p=prop, s=step: self._decrease_property(p, s)) + self.world.accept(f"inc_{prop}", lambda p=prop, s=step: self._increase_property(p, s)) + + # 存储控制信息 + self.property_controls[prop] = { + 'name': name, + 'min': min_val, + 'max': max_val, + 'step': step, + 'value_label': value_label, + 'dec_button': dec_button, + 'inc_button': inc_button + } + + def _cleanup_editor_gui(self): + """清理编辑器GUI""" + try: + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + gui_manager = self.world.gui_manager + + for element in self.gui_elements: + gui_manager.deleteGUIElement(element) + + # 清理事件监听 + tools = ["select", "add", "delete", "paint"] + for tool in tools: + self.world.ignore(f"navmesh_tool_{tool}") + + functions = ["生成网格", "加载网格", "保存网格", "清除网格", "撤销", "重做"] + for func in functions: + self.world.ignore(f"navmesh_func_{func}") + + # 清理属性控制事件 + properties = ["cell_size", "cell_height", "agent_height", "agent_radius", + "max_slope", "brush_size"] + for prop in properties: + self.world.ignore(f"dec_{prop}") + self.world.ignore(f"inc_{prop}") + + self.gui_elements.clear() + self.tool_buttons.clear() + self.property_controls.clear() + + except Exception as e: + print(f"⚠ 编辑器GUI清理失败: {e}") + + def _decrease_property(self, prop_name: str, step: float): + """减少属性值""" + if hasattr(self.navmesh_manager, prop_name): + current_value = getattr(self.navmesh_manager, prop_name) + control = self.property_controls[prop_name] + new_value = max(control['min'], current_value - step) + setattr(self.navmesh_manager, prop_name, new_value) + + # 更新显示 + if 'value_label' in control: + control['value_label'].setText(f"{new_value:.2f}") + + def _increase_property(self, prop_name: str, step: float): + """增加属性值""" + if hasattr(self.navmesh_manager, prop_name): + current_value = getattr(self.navmesh_manager, prop_name) + control = self.property_controls[prop_name] + new_value = min(control['max'], current_value + step) + setattr(self.navmesh_manager, prop_name, new_value) + + # 更新显示 + if 'value_label' in control: + control['value_label'].setText(f"{new_value:.2f}") + + def set_edit_tool(self, tool: str): + """设置编辑工具""" + self.edit_tool = tool + print(f"✓ 切换到 {tool} 工具") + + # 更新按钮状态 + for tool_name, button in self.tool_buttons.items(): + if tool_name == tool: + # 高亮选中工具 + button.setColor(0.5, 1.0, 0.5, 1.0) # 绿色 + else: + # 恢复默认颜色 + button.setColor(1.0, 1.0, 1.0, 1.0) # 白色 + + def generate_navmesh(self): + """生成导航网格""" + if self.navmesh_manager: + success = self.navmesh_manager.generate_from_scene() + if success: + print("✓ 导航网格生成完成") + else: + print("✗ 导航网格生成失败") + + def load_navmesh(self): + """加载导航网格""" + # 简化实现:加载测试网格 + if self.navmesh_manager: + self.navmesh_manager._create_test_navmesh() + print("✓ 导航网格已加载(测试数据)") + + def save_navmesh(self): + """保存导航网格""" + print("⚠ 保存功能需要实现文件对话框") + + def clear_navmesh(self): + """清除导航网格""" + if self.navmesh_manager: + self.navmesh_manager.polygons.clear() + self.navmesh_manager._update_navmesh_visualization() + print("✓ 导航网格已清除") + + def undo(self): + """撤销操作""" + if self.history_index >= 0: + self.history_index -= 1 + print("✓ 撤销操作") + else: + print("⚠ 没有可撤销的操作") + + def redo(self): + """重做操作""" + if self.history_index < len(self.edit_history) - 1: + self.history_index += 1 + print("✓ 重做操作") + else: + print("⚠ 没有可重做的操作") + + def _add_to_history(self, operation: Dict[str, Any]): + """添加操作到历史记录""" + # 移除历史记录中超过当前索引的部分 + if self.history_index < len(self.edit_history) - 1: + self.edit_history = self.edit_history[:self.history_index + 1] + + # 添加新操作 + self.edit_history.append(operation) + + # 限制历史记录大小 + if len(self.edit_history) > self.max_history_size: + self.edit_history.pop(0) + else: + self.history_index += 1 + + def select_polygon(self, polygon_id: int): + """选择多边形""" + self.selected_polygon = polygon_id + print(f"✓ 选择多边形: {polygon_id}") + + def add_polygon(self, vertices: List[Point3]): + """添加多边形""" + if self.navmesh_manager: + polygon_id = self.navmesh_manager.add_polygon(vertices) + print(f"✓ 添加多边形: {polygon_id}") + + # 添加到历史记录 + operation = { + 'type': 'add_polygon', + 'polygon_id': polygon_id + } + self._add_to_history(operation) + + def delete_polygon(self, polygon_id: int): + """删除多边形""" + if self.navmesh_manager: + success = self.navmesh_manager.remove_polygon(polygon_id) + if success: + print(f"✓ 删除多边形: {polygon_id}") + + # 添加到历史记录 + operation = { + 'type': 'delete_polygon', + 'polygon_id': polygon_id + } + self._add_to_history(operation) + else: + print(f"⚠ 无法删除多边形: {polygon_id}") + + def paint_walkable_area(self, center: Point3): + """绘制可行走区域""" + print(f"✓ 在 {center} 附近绘制可行走区域") + + def toggle_edit_mode(self): + """切换编辑模式""" + self.edit_mode = not self.edit_mode + if self.edit_mode: + print("✓ 进入编辑模式") + else: + print("✓ 退出编辑模式") + + def set_brush_size(self, size: float): + """设置笔刷大小""" + self.brush_size = max(0.1, size) + print(f"✓ 笔刷大小设置为: {self.brush_size:.2f}") + + def get_editor_state(self) -> Dict[str, Any]: + """获取编辑器状态""" + return { + 'visible': self.visible, + 'edit_mode': self.edit_mode, + 'selected_tool': self.edit_tool, + 'selected_polygon': self.selected_polygon, + 'brush_size': self.brush_size, + 'polygon_count': len(self.navmesh_manager.polygons) if self.navmesh_manager else 0 + } + + def set_editor_state(self, state: Dict[str, Any]): + """设置编辑器状态""" + try: + if 'edit_mode' in state: + self.edit_mode = state['edit_mode'] + + if 'selected_tool' in state: + self.set_edit_tool(state['selected_tool']) + + if 'brush_size' in state: + self.set_brush_size(state['brush_size']) + + print("✓ 编辑器状态已恢复") + + except Exception as e: + print(f"⚠ 恢复编辑器状态失败: {e}") + + def handle_mouse_click(self, mouse_x: float, mouse_y: float): + """处理鼠标点击事件""" + if not self.edit_mode: + return + + # 将屏幕坐标转换为世界坐标(简化实现) + world_x = mouse_x * 20 # 假设场景大小为20x20 + world_z = mouse_y * 20 + click_point = Point3(world_x, 0, world_z) + + if self.edit_tool == "add": + # 添加多边形(简化为正方形) + half_size = 0.5 + vertices = [ + Point3(world_x - half_size, 0, world_z - half_size), + Point3(world_x + half_size, 0, world_z - half_size), + Point3(world_x + half_size, 0, world_z + half_size), + Point3(world_x - half_size, 0, world_z + half_size) + ] + self.add_polygon(vertices) + + elif self.edit_tool == "delete": + # 删除多边形 + if self.navmesh_manager: + polygon = self.navmesh_manager.get_polygon_containing_point(click_point) + if polygon: + self.delete_polygon(polygon.polygon_id) + + elif self.edit_tool == "paint": + # 绘制可行走区域 + self.paint_walkable_area(click_point) + + elif self.edit_tool == "select": + # 选择多边形 + if self.navmesh_manager: + polygon = self.navmesh_manager.get_polygon_containing_point(click_point) + if polygon: + self.select_polygon(polygon.polygon_id) + + def update(self, dt: float): + """更新编辑器状态""" + # 处理键盘输入 + if self.world.mouseWatcherNode.hasMouse(): + mouse_pos = self.world.mouseWatcherNode.getMouse() + + # 检查鼠标按键 + if self.world.mouseWatcherNode.isButtonDown(MouseWatcher.getGlobalPointer()): + if self.last_mouse_pos is None or \ + (mouse_pos - self.last_mouse_pos).length() > 0.01: + self.handle_mouse_click(mouse_pos.x, mouse_pos.y) + self.last_mouse_pos = mouse_pos + + # 检查键盘快捷键 + if self.world.mouseWatcherNode.isButtonDown(KeyboardButton.asciiKey('e')): + self.toggle_edit_mode() + + def cleanup(self): + """清理编辑器资源""" + self.hide() + + # 清理历史记录 + self.edit_history.clear() + self.history_index = -1 + + # 清理状态 + self.selected_polygon = None self.edit_mode = False \ No newline at end of file diff --git a/plugins/user/navigation_mesh/plugin.py b/plugins/user/navigation_mesh/plugin.py index 8af4df8c..a7471658 100644 --- a/plugins/user/navigation_mesh/plugin.py +++ b/plugins/user/navigation_mesh/plugin.py @@ -1,868 +1,868 @@ -""" -导航网格插件主文件 -""" - -import time -import json -from plugins.plugin_manager import BasePlugin -from .core.navmesh_manager import NavMeshManager -from .editor.navmesh_editor import NavMeshEditor -from .tools.pathfinder import Pathfinder -from .tools.navmesh_tools import NavMeshTools - -class Plugin(BasePlugin): - """ - 导航网格插件 - 提供基于导航网格的寻路和路径规划功能 - """ - - def __init__(self, plugin_manager, name): - super().__init__(plugin_manager, name) - self.config = { - "version": "1.0.0", - "author": "EG Team", - "description": "基于导航网格的寻路系统,支持复杂地形的路径规划" - } - - # 核心组件 - self.navmesh_manager = None - self.navmesh_editor = None - self.pathfinder = None - self.navmesh_tools = None - - # GUI元素 - self.gui_elements = [] - - # 事件处理器 - self.event_handlers = {} - - # 插件状态 - self.is_initialized = False - self.is_enabled = False - - # 性能监控 - self.performance_stats = { - 'initialization_time': 0.0, - 'enable_time': 0.0, - 'last_update_time': 0.0, - 'total_updates': 0 - } - - def initialize(self) -> bool: - """ - 初始化插件 - """ - try: - if self.is_initialized: - return True - - init_start_time = time.time() - print(f"初始化导航网格插件: {self.name}") - - # 初始化导航网格管理器 - self.navmesh_manager = NavMeshManager(self.plugin_manager.world) - - # 初始化路径查找器 - self.pathfinder = Pathfinder(self.navmesh_manager) - - # 初始化导航网格编辑器 - self.navmesh_editor = NavMeshEditor( - self.plugin_manager.world, - self.navmesh_manager - ) - - # 初始化导航网格工具集 - self.navmesh_tools = NavMeshTools(self) - - self.is_initialized = True - self.performance_stats['initialization_time'] = time.time() - init_start_time - print("✓ 导航网格插件初始化完成") - return True - - except Exception as e: - print(f"✗ 导航网格插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - """ - if not super().enable(): - return False - - if self.is_enabled: - return True - - try: - enable_start_time = time.time() - print(f"启用导航网格插件: {self.name}") - - # 启用导航网格管理器 - if self.navmesh_manager: - self.navmesh_manager.enable() - - # 注册事件处理器 - self._register_event_handlers() - - # 创建GUI界面 - self._create_gui() - - self.is_enabled = True - self.performance_stats['enable_time'] = time.time() - enable_start_time - print("✓ 导航网格插件启用成功") - return True - - except Exception as e: - print(f"✗ 导航网格插件启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self) -> bool: - """ - 禁用插件 - """ - if not super().disable(): - return False - - if not self.is_enabled: - return True - - try: - print(f"禁用导航网格插件: {self.name}") - - # 清理GUI界面 - self._cleanup_gui() - - # 移除事件处理器 - self._unregister_event_handlers() - - # 禁用导航网格管理器 - if self.navmesh_manager: - self.navmesh_manager.disable() - - self.is_enabled = False - print("✓ 导航网格插件禁用成功") - return True - - except Exception as e: - print(f"✗ 导航网格插件禁用失败: {e}") - import traceback - traceback.print_exc() - return False - - def finalize(self): - """ - 清理插件资源 - """ - print(f"清理导航网格插件资源: {self.name}") - - # 清理所有组件 - if self.navmesh_editor: - self.navmesh_editor.cleanup() - self.navmesh_editor = None - - if self.pathfinder: - self.pathfinder = None - - if self.navmesh_tools: - self.navmesh_tools = None - - if self.navmesh_manager: - self.navmesh_manager.cleanup() - self.navmesh_manager = None - - self.is_initialized = False - self.is_enabled = False - - def _register_event_handlers(self): - """ - 注册事件处理器 - """ - world = self.plugin_manager.world - - # 导航网格系统控制快捷键 - self.event_handlers['f1'] = self.toggle_navmesh_editor - self.event_handlers['f2'] = self.toggle_navmesh_visibility - self.event_handlers['f3'] = self.find_random_path - self.event_handlers['f4'] = self.clear_paths - self.event_handlers['f5'] = self.generate_navmesh - self.event_handlers['f6'] = self.validate_navmesh - self.event_handlers['f7'] = self.optimize_navmesh - self.event_handlers['f8'] = self.show_stats - self.event_handlers['f9'] = self.analyze_navmesh - self.event_handlers['f10'] = self.export_report - self.event_handlers['f11'] = self.simplify_navmesh - self.event_handlers['f12'] = self.detect_bottlenecks - - # 注册事件 - for event, handler in self.event_handlers.items(): - world.accept(event, handler) - world.accept(event.upper(), handler) # 同时注册大写事件 - - def _unregister_event_handlers(self): - """ - 移除事件处理器 - """ - world = self.plugin_manager.world - - for event in self.event_handlers.keys(): - world.ignore(event) - world.ignore(event.upper()) - - self.event_handlers.clear() - - def _create_gui(self): - """ - 创建GUI界面 - """ - try: - world = self.plugin_manager.world - - # 检查是否有GUI管理器 - if not hasattr(world, 'gui_manager') or not world.gui_manager: - print("⚠ GUI管理器不可用,跳过GUI创建") - return - - gui_manager = world.gui_manager - - # 创建导航网格控制面板 - panel_button = gui_manager.createGUIButton( - pos=(0.02, 0, 0.75), - text="导航网格", - size=0.06 - ) - self.gui_elements.append(panel_button) - - # 创建功能按钮 - functions = [ - ("编辑器 [F1]", self.toggle_navmesh_editor), - ("显示/隐藏 [F2]", self.toggle_navmesh_visibility), - ("随机路径 [F3]", self.find_random_path), - ("清除路径 [F4]", self.clear_paths), - ("生成网格 [F5]", self.generate_navmesh), - ("验证网格 [F6]", self.validate_navmesh), - ("优化网格 [F7]", self.optimize_navmesh), - ("显示统计 [F8]", self.show_stats), - ("分析网格 [F9]", self.analyze_navmesh), - ("导出报告 [F10]", self.export_report), - ("简化网格 [F11]", self.simplify_navmesh), - ("检测瓶颈 [F12]", self.detect_bottlenecks) - ] - - for i, (name, callback) in enumerate(functions): - button = gui_manager.createGUIButton( - pos=(0.02, 0, 0.68 - i * 0.055), - text=name, - size=0.045 - ) - self.gui_elements.append(button) - world.accept(f"navmesh_function_{name}", callback) - - except Exception as e: - print(f"⚠ GUI创建失败: {e}") - - def _cleanup_gui(self): - """ - 清理GUI界面 - """ - try: - world = self.plugin_manager.world - - if hasattr(world, 'gui_manager') and world.gui_manager: - gui_manager = world.gui_manager - - # 删除所有GUI元素 - for element in self.gui_elements: - gui_manager.deleteGUIElement(element) - - self.gui_elements.clear() - - except Exception as e: - print(f"⚠ GUI清理失败: {e}") - - # 事件处理方法 - def toggle_navmesh_editor(self): - """切换导航网格编辑器显示""" - if self.navmesh_editor: - self.navmesh_editor.toggle_visibility() - - def toggle_navmesh_visibility(self): - """切换导航网格可见性""" - if self.navmesh_manager: - self.navmesh_manager.toggle_visibility() - - def find_random_path(self): - """查找随机路径""" - if self.pathfinder and self.navmesh_manager: - # 获取随机起点和终点 - start = self.navmesh_manager.get_random_point() - end = self.navmesh_manager.get_random_point() - - if start and end: - path = self.pathfinder.find_path(start, end) - if path: - print(f"✓ 找到路径,包含 {len(path)} 个节点") - # 可视化路径 - self.navmesh_manager.visualize_path(path) - else: - print("⚠ 未找到路径") - - def clear_paths(self): - """清除所有路径可视化""" - if self.navmesh_manager: - self.navmesh_manager.clear_path_visualization() - - def generate_navmesh(self): - """生成导航网格""" - if self.navmesh_manager: - success = self.navmesh_manager.generate_from_scene() - if success: - print("✓ 导航网格生成完成") - else: - print("✗ 导航网格生成失败") - - def validate_navmesh(self): - """验证导航网格""" - if self.navmesh_manager: - is_valid = self.navmesh_manager.validate_navmesh() - if is_valid: - print("✓ 导航网格验证通过") - else: - print("✗ 导航网格验证失败") - - def optimize_navmesh(self): - """优化导航网格""" - if self.navmesh_manager and self.navmesh_tools: - print("✓ 开始优化导航网格...") - optimization_result = self.navmesh_tools.optimize_mesh('medium') - print(f"✓ 导航网格优化完成: {optimization_result}") - - def show_stats(self): - """显示统计信息""" - if self.navmesh_manager: - stats = self.navmesh_manager.get_stats() - print("=== 导航网格统计信息 ===") - for key, value in stats.items(): - print(f" {key}: {value}") - - if self.pathfinder: - path_stats = self.pathfinder.get_pathfinding_stats() - print("=== 寻路统计信息 ===") - for key, value in path_stats.items(): - print(f" {key}: {value}") - - if self.navmesh_editor: - editor_stats = self.navmesh_editor.get_stats() - print("=== 编辑器统计信息 ===") - for key, value in editor_stats.items(): - print(f" {key}: {value}") - - def analyze_navmesh(self): - """分析导航网格""" - if self.navmesh_tools: - print("✓ 开始分析导航网格...") - quality_report = self.navmesh_tools.analyze_mesh_quality() - suggestions = self.navmesh_tools.suggest_optimizations() - - print("=== 导航网格质量分析 ===") - print(f" 多边形数量: {quality_report.get('polygon_count', 0)}") - print(f" 顶点数量: {quality_report.get('vertex_count', 0)}") - print(f" 孤立多边形: {quality_report.get('isolated_polygons', 0)}") - print(f" 小多边形: {quality_report.get('small_polygons', 0)}") - print(f" 不规则多边形: {quality_report.get('irregular_polygons', 0)}") - - print("=== 优化建议 ===") - for i, suggestion in enumerate(suggestions[:5]): # 显示前5个建议 - print(f" {i+1}. {suggestion.get('description', '无描述')}") - print(f" 建议: {suggestion.get('recommendation', '无建议')}") - - def export_report(self): - """导出分析报告""" - if self.navmesh_tools: - print("✓ 开始导出分析报告...") - timestamp = int(time.time()) - filename = f"navmesh_analysis_report_{timestamp}.json" - success = self.navmesh_tools.export_report(filename) - if success: - print(f"✓ 分析报告已导出到: {filename}") - else: - print("✗ 导出分析报告失败") - - def simplify_navmesh(self): - """简化导航网格""" - if self.navmesh_tools: - print("✓ 开始简化导航网格...") - simplification_result = self.navmesh_tools.optimize_mesh('light') - print(f"✓ 导航网格简化完成: {simplification_result}") - - def detect_bottlenecks(self): - """检测瓶颈区域""" - if self.navmesh_tools: - print("✓ 开始检测瓶颈区域...") - bottlenecks = self.navmesh_tools.detect_bottlenecks() - - print("=== 瓶颈区域检测结果 ===") - if bottlenecks: - for i, bottleneck in enumerate(bottlenecks[:5]): # 显示前5个瓶颈 - position = bottleneck.get('position', '未知') - compactness = bottleneck.get('compactness', 0) - print(f" {i+1}. 位置: {position}, 紧凑度: {compactness:.4f}") - else: - print(" 未检测到明显的瓶颈区域") - - def get_info(self) -> dict: - """ - 获取插件信息 - """ - info = super().get_info() - info.update({ - "version": self.config.get("version", "1.0.0"), - "author": self.config.get("author", "EG Team"), - "description": self.config.get("description", "导航网格插件"), - "navmesh_loaded": self.navmesh_manager.is_navmesh_loaded() if self.navmesh_manager else False, - "navmesh_polygons": self.navmesh_manager.get_polygon_count() if self.navmesh_manager else 0, - "is_enabled": self.is_enabled, - "is_initialized": self.is_initialized - }) - return info - - # 公共接口方法 - def load_navmesh_from_file(self, filepath: str) -> bool: - """从文件加载导航网格""" - if self.navmesh_tools: - return self.navmesh_tools.load_navmesh_from_file(filepath) - return False - - def save_navmesh_to_file(self, filepath: str) -> bool: - """保存导航网格到文件""" - if self.navmesh_tools: - return self.navmesh_tools.save_navmesh_to_file(filepath) - return False - - def generate_navmesh_from_scene(self) -> bool: - """从当前场景生成导航网格""" - if self.navmesh_manager: - return self.navmesh_manager.generate_from_scene() - return False - - def find_path(self, start_point, end_point): - """查找两点之间的路径""" - if self.pathfinder: - return self.pathfinder.find_path(start_point, end_point) - return None - - def is_point_walkable(self, point) -> bool: - """检查点是否可行走""" - if self.navmesh_manager: - return self.navmesh_manager.is_point_walkable(point) - return False - - def get_closest_point(self, point): - """获取导航网格上最近的点""" - if self.navmesh_manager: - return self.navmesh_manager.get_closest_point(point) - return None - - def add_dynamic_obstacle(self, position, radius, height): - """添加动态障碍物""" - if self.navmesh_manager: - obstacle_id = self.navmesh_manager.add_obstacle(position, radius, height) - # 通知寻路器 - if self.pathfinder: - self.pathfinder.add_dynamic_obstacle(obstacle_id, []) - return obstacle_id - return -1 - - def remove_dynamic_obstacle(self, obstacle_id): - """移除动态障碍物""" - if self.navmesh_manager: - success = self.navmesh_manager.remove_obstacle(obstacle_id) - # 通知寻路器 - if self.pathfinder and success: - self.pathfinder.remove_dynamic_obstacle(obstacle_id) - return success - return False - - def set_navmesh_settings(self, settings: dict): - """设置导航网格参数""" - if self.navmesh_manager: - if 'cell_size' in settings: - self.navmesh_manager.set_cell_size(settings['cell_size']) - if 'agent_height' in settings: - self.navmesh_manager.set_agent_height(settings['agent_height']) - if 'agent_radius' in settings: - self.navmesh_manager.set_agent_radius(settings['agent_radius']) - if 'max_slope' in settings: - self.navmesh_manager.set_max_slope(settings['max_slope']) - if 'max_climb' in settings: - self.navmesh_manager.set_max_climb(settings['max_climb']) - if 'min_region_area' in settings: - self.navmesh_manager.set_min_region_area(settings['min_region_area']) - if 'merge_region_area' in settings: - self.navmesh_manager.set_merge_region_area(settings['merge_region_area']) - if 'edge_max_len' in settings: - self.navmesh_manager.set_edge_max_len(settings['edge_max_len']) - if 'edge_max_error' in settings: - self.navmesh_manager.set_edge_max_error(settings['edge_max_error']) - if 'verts_per_poly' in settings: - self.navmesh_manager.set_verts_per_poly(settings['verts_per_poly']) - - def get_navmesh_settings(self) -> dict: - """获取导航网格参数""" - if self.navmesh_manager: - return { - 'cell_size': self.navmesh_manager.cell_size, - 'agent_height': self.navmesh_manager.agent_height, - 'agent_radius': self.navmesh_manager.agent_radius, - 'max_slope': self.navmesh_manager.max_slope, - 'max_climb': self.navmesh_manager.max_climb, - 'min_region_area': self.navmesh_manager.min_region_area, - 'merge_region_area': self.navmesh_manager.merge_region_area, - 'edge_max_len': self.navmesh_manager.edge_max_len, - 'edge_max_error': self.navmesh_manager.edge_max_error, - 'verts_per_poly': self.navmesh_manager.verts_per_poly - } - return {} - - def set_pathfinder_algorithm(self, algorithm: str): - """设置寻路算法""" - if self.pathfinder: - self.pathfinder.set_algorithm(algorithm) - - def get_available_algorithms(self) -> list: - """获取可用的寻路算法""" - if self.pathfinder: - return self.pathfinder.get_available_algorithms() - return [] - - def set_pathfinder_settings(self, settings: dict): - """设置寻路参数""" - if self.pathfinder: - if 'heuristic_weight' in settings: - self.pathfinder.set_heuristic_weight(settings['heuristic_weight']) - if 'max_path_nodes' in settings: - self.pathfinder.set_max_path_nodes(settings['max_path_nodes']) - if 'use_funnel_pruning' in settings: - self.pathfinder.enable_funnel_pruning(settings['use_funnel_pruning']) - if 'smooth_paths' in settings: - self.pathfinder.enable_path_smoothing(settings['smooth_paths']) - - def get_pathfinder_settings(self) -> dict: - """获取寻路参数""" - if self.pathfinder: - return { - 'heuristic_weight': self.pathfinder.heuristic_weight, - 'max_path_nodes': self.pathfinder.max_path_nodes, - 'use_funnel_pruning': self.pathfinder.use_funnel_pruning, - 'smooth_paths': self.pathfinder.smooth_paths, - 'current_algorithm': self.pathfinder.current_algorithm - } - return {} - - def find_path_with_waypoints(self, waypoints: list): - """查找经过多个路点的路径""" - if self.pathfinder: - return self.pathfinder.find_path_with_multiple_waypoints(waypoints) - return None - - def estimate_path_cost(self, start_point, end_point): - """估算路径代价""" - if self.pathfinder: - return self.pathfinder.estimate_path_cost(start_point, end_point) - return None - - def validate_path(self, path: list) -> bool: - """验证路径有效性""" - if self.pathfinder: - return self.pathfinder.validate_path(path) - return False - - def get_path_complexity(self, path: list) -> dict: - """分析路径复杂度""" - if self.pathfinder: - return self.pathfinder.get_path_complexity(path) - return {} - - def find_alternative_paths(self, start_point, end_point, count: int = 3): - """查找备选路径""" - if self.pathfinder: - return self.pathfinder.find_alternative_paths(start_point, end_point, count) - return [] - - def clear_path_cache(self): - """清空路径缓存""" - if self.pathfinder: - self.pathfinder.clear_cache() - - def rebuild_navmesh(self): - """重新构建导航网格""" - if self.navmesh_manager: - return self.navmesh_manager.rebuild_navmesh() - return False - - def enable_spatial_hashing(self, enabled: bool): - """启用/禁用空间哈希""" - if self.navmesh_manager: - self.navmesh_manager.enable_spatial_hashing(enabled) - - def set_spatial_hash_cell_size(self, size: float): - """设置空间哈希网格单元大小""" - if self.navmesh_manager: - self.navmesh_manager.set_spatial_hash_cell_size(size) - - def export_statistics(self, filepath: str) -> bool: - """导出统计信息""" - try: - stats_data = { - 'plugin_info': self.get_info(), - 'navmesh_settings': self.get_navmesh_settings(), - 'pathfinder_settings': self.get_pathfinder_settings(), - 'performance_stats': self.performance_stats, - 'timestamp': time.strftime("%Y-%m-%d %H:%M:%S") - } - - if self.navmesh_manager: - stats_data['navmesh_stats'] = self.navmesh_manager.get_stats() - - if self.pathfinder: - stats_data['pathfinder_stats'] = self.pathfinder.get_pathfinding_stats() - - if self.navmesh_editor: - stats_data['editor_stats'] = self.navmesh_editor.get_stats() - - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(stats_data, f, indent=2, ensure_ascii=False) - - print(f"✓ 统计信息已导出到: {filepath}") - return True - - except Exception as e: - print(f"✗ 导出统计信息失败: {e}") - return False - - def import_settings(self, filepath: str) -> bool: - """导入设置""" - try: - with open(filepath, 'r', encoding='utf-8') as f: - settings = json.load(f) - - # 应用导航网格设置 - if 'navmesh_settings' in settings: - self.set_navmesh_settings(settings['navmesh_settings']) - - # 应用寻路设置 - if 'pathfinder_settings' in settings: - pf_settings = settings['pathfinder_settings'] - self.set_pathfinder_settings(pf_settings) - - print(f"✓ 设置已从 {filepath} 导入") - return True - - except Exception as e: - print(f"✗ 导入设置失败: {e}") - return False - - def export_settings(self, filepath: str) -> bool: - """导出设置""" - try: - settings_data = { - 'navmesh_settings': self.get_navmesh_settings(), - 'pathfinder_settings': self.get_pathfinder_settings() - } - - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(settings_data, f, indent=2, ensure_ascii=False) - - print(f"✓ 设置已导出到: {filepath}") - return True - - except Exception as e: - print(f"✗ 导出设置失败: {e}") - return False - - # 新增的工具方法 - def analyze_mesh_quality(self) -> dict: - """分析导航网格质量""" - if self.navmesh_tools: - return self.navmesh_tools.analyze_mesh_quality() - return {} - - def suggest_optimizations(self) -> list: - """提供优化建议""" - if self.navmesh_tools: - return self.navmesh_tools.suggest_optimizations() - return [] - - def analyze_pathfinding_performance(self, sample_count: int = 100) -> dict: - """分析寻路性能""" - if self.navmesh_tools: - return self.navmesh_tools.analyze_pathfinding_performance(sample_count) - return {} - - def detect_navigation_bottlenecks(self) -> list: - """检测导航网格中的瓶颈区域""" - if self.navmesh_tools: - return self.navmesh_tools.detect_bottlenecks() - return [] - - def analyze_coverage(self) -> dict: - """分析导航网格覆盖情况""" - if self.navmesh_tools: - return self.navmesh_tools.analyze_coverage() - return {} - - def generate_analysis_report(self) -> dict: - """生成完整的分析报告""" - if self.navmesh_tools: - return self.navmesh_tools.generate_report() - return {} - - def export_analysis_report(self, filepath: str) -> bool: - """导出分析报告到文件""" - if self.navmesh_tools: - return self.navmesh_tools.export_report(filepath) - return False - - def get_mesh_complexity_score(self) -> float: - """计算导航网格复杂度评分""" - if self.navmesh_tools: - return self.navmesh_tools.get_complexity_score() - return 0.0 - - def recommend_mesh_simplification(self) -> dict: - """推荐网格简化策略""" - if self.navmesh_tools: - return self.navmesh_tools.recommend_simplification() - return {} - - def merge_adjacent_polygons(self) -> int: - """合并相邻的多边形""" - if self.navmesh_tools: - return self.navmesh_tools.merge_adjacent_polygons() - return 0 - - def simplify_mesh_polygons(self) -> int: - """简化多边形(移除不必要的顶点)""" - if self.navmesh_tools: - return self.navmesh_tools.simplify_polygons() - return 0 - - def remove_small_mesh_polygons(self) -> int: - """移除极小的多边形""" - if self.navmesh_tools: - return self.navmesh_tools.remove_small_polygons() - return 0 - - def optimize_navigation_mesh(self, optimization_level: str = 'medium') -> dict: - """执行完整的网格优化""" - if self.navmesh_tools: - return self.navmesh_tools.optimize_mesh(optimization_level) - return {} - - def set_optimization_settings(self, settings: dict): - """设置优化参数""" - if self.navmesh_tools: - self.navmesh_tools.set_optimization_settings(settings) - - def get_optimization_settings(self) -> dict: - """获取优化参数""" - if self.navmesh_tools: - return self.navmesh_tools.get_optimization_settings() - return {} - - def fill_mesh_gaps(self) -> int: - """填补导航网格中的空隙""" - if self.navmesh_tools: - return self.navmesh_tools.fill_gaps() - return 0 - - def smooth_mesh_boundaries(self) -> int: - """平滑多边形边界""" - if self.navmesh_tools: - return self.navmesh_tools.smooth_boundaries() - return 0 - - def balance_polygon_sizes(self) -> int: - """平衡多边形大小""" - if self.navmesh_tools: - return self.navmesh_tools.balance_polygon_sizes() - return 0 - - def serialize_navigation_mesh(self) -> str: - """序列化导航网格为字符串""" - if self.navmesh_tools: - return self.navmesh_tools.serialize_navmesh() - return "" - - def deserialize_navigation_mesh(self, data_str: str) -> bool: - """从字符串反序列化导航网格""" - if self.navmesh_tools: - return self.navmesh_tools.deserialize_navmesh(data_str) - return False - - def save_navmesh_to_file_advanced(self, filepath: str, - compress: bool = False, pretty_print: bool = False) -> bool: - """保存导航网格到文件(高级选项)""" - if self.navmesh_tools: - return self.navmesh_tools.save_navmesh_to_file(filepath, compress, pretty_print) - return False - - def load_navmesh_from_file_advanced(self, filepath: str) -> bool: - """从文件加载导航网格(高级选项)""" - if self.navmesh_tools: - return self.navmesh_tools.load_navmesh_from_file(filepath) - return False - - def export_to_standard_format(self, filepath: str, - format_type: str = 'obj') -> bool: - """导出为标准格式""" - if self.navmesh_tools: - return self.navmesh_tools.export_to_standard_format(filepath, format_type) - return False - - def import_from_standard_format(self, filepath: str, - format_type: str = 'obj') -> bool: - """从标准格式导入""" - if self.navmesh_tools: - return self.navmesh_tools.import_from_standard_format(filepath, format_type) - return False - - def get_navmesh_file_info(self, filepath: str) -> dict: - """获取导航网格文件信息""" - if self.navmesh_tools: - return self.navmesh_tools.get_file_info(filepath) - return {} - - def batch_analyze_navmeshes(self, filepaths: list) -> dict: - """批量分析多个导航网格文件""" - if self.navmesh_tools: - return self.navmesh_tools.batch_analyze(filepaths) - return {} - - def compare_navigation_meshes(self, navmesh1_path: str, navmesh2_path: str) -> dict: - """比较两个导航网格""" - if self.navmesh_tools: - return self.navmesh_tools.compare_navmeshes(navmesh1_path, navmesh2_path) - return {} - - def validate_navmesh_file(self, filepath: str) -> dict: - """验证导航网格文件的有效性""" - if self.navmesh_tools: - return self.navmesh_tools.validate_navmesh_file(filepath) - return {} - - def optimize_and_save_navmesh(self, input_filepath: str, output_filepath: str, - optimization_level: str = 'medium') -> dict: - """优化并保存导航网格""" - if self.navmesh_tools: - return self.navmesh_tools.optimize_and_save(input_filepath, output_filepath, optimization_level) - return {} - - def create_optimization_pipeline(self, steps: list) -> dict: - """创建优化流水线""" - if self.navmesh_tools: - return self.navmesh_tools.create_optimization_pipeline(steps) +""" +导航网格插件主文件 +""" + +import time +import json +from plugins.plugin_manager import BasePlugin +from .core.navmesh_manager import NavMeshManager +from .editor.navmesh_editor import NavMeshEditor +from .tools.pathfinder import Pathfinder +from .tools.navmesh_tools import NavMeshTools + +class Plugin(BasePlugin): + """ + 导航网格插件 + 提供基于导航网格的寻路和路径规划功能 + """ + + def __init__(self, plugin_manager, name): + super().__init__(plugin_manager, name) + self.config = { + "version": "1.0.0", + "author": "EG Team", + "description": "基于导航网格的寻路系统,支持复杂地形的路径规划" + } + + # 核心组件 + self.navmesh_manager = None + self.navmesh_editor = None + self.pathfinder = None + self.navmesh_tools = None + + # GUI元素 + self.gui_elements = [] + + # 事件处理器 + self.event_handlers = {} + + # 插件状态 + self.is_initialized = False + self.is_enabled = False + + # 性能监控 + self.performance_stats = { + 'initialization_time': 0.0, + 'enable_time': 0.0, + 'last_update_time': 0.0, + 'total_updates': 0 + } + + def initialize(self) -> bool: + """ + 初始化插件 + """ + try: + if self.is_initialized: + return True + + init_start_time = time.time() + print(f"初始化导航网格插件: {self.name}") + + # 初始化导航网格管理器 + self.navmesh_manager = NavMeshManager(self.plugin_manager.world) + + # 初始化路径查找器 + self.pathfinder = Pathfinder(self.navmesh_manager) + + # 初始化导航网格编辑器 + self.navmesh_editor = NavMeshEditor( + self.plugin_manager.world, + self.navmesh_manager + ) + + # 初始化导航网格工具集 + self.navmesh_tools = NavMeshTools(self) + + self.is_initialized = True + self.performance_stats['initialization_time'] = time.time() - init_start_time + print("✓ 导航网格插件初始化完成") + return True + + except Exception as e: + print(f"✗ 导航网格插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + """ + if not super().enable(): + return False + + if self.is_enabled: + return True + + try: + enable_start_time = time.time() + print(f"启用导航网格插件: {self.name}") + + # 启用导航网格管理器 + if self.navmesh_manager: + self.navmesh_manager.enable() + + # 注册事件处理器 + self._register_event_handlers() + + # 创建GUI界面 + self._create_gui() + + self.is_enabled = True + self.performance_stats['enable_time'] = time.time() - enable_start_time + print("✓ 导航网格插件启用成功") + return True + + except Exception as e: + print(f"✗ 导航网格插件启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self) -> bool: + """ + 禁用插件 + """ + if not super().disable(): + return False + + if not self.is_enabled: + return True + + try: + print(f"禁用导航网格插件: {self.name}") + + # 清理GUI界面 + self._cleanup_gui() + + # 移除事件处理器 + self._unregister_event_handlers() + + # 禁用导航网格管理器 + if self.navmesh_manager: + self.navmesh_manager.disable() + + self.is_enabled = False + print("✓ 导航网格插件禁用成功") + return True + + except Exception as e: + print(f"✗ 导航网格插件禁用失败: {e}") + import traceback + traceback.print_exc() + return False + + def finalize(self): + """ + 清理插件资源 + """ + print(f"清理导航网格插件资源: {self.name}") + + # 清理所有组件 + if self.navmesh_editor: + self.navmesh_editor.cleanup() + self.navmesh_editor = None + + if self.pathfinder: + self.pathfinder = None + + if self.navmesh_tools: + self.navmesh_tools = None + + if self.navmesh_manager: + self.navmesh_manager.cleanup() + self.navmesh_manager = None + + self.is_initialized = False + self.is_enabled = False + + def _register_event_handlers(self): + """ + 注册事件处理器 + """ + world = self.plugin_manager.world + + # 导航网格系统控制快捷键 + self.event_handlers['f1'] = self.toggle_navmesh_editor + self.event_handlers['f2'] = self.toggle_navmesh_visibility + self.event_handlers['f3'] = self.find_random_path + self.event_handlers['f4'] = self.clear_paths + self.event_handlers['f5'] = self.generate_navmesh + self.event_handlers['f6'] = self.validate_navmesh + self.event_handlers['f7'] = self.optimize_navmesh + self.event_handlers['f8'] = self.show_stats + self.event_handlers['f9'] = self.analyze_navmesh + self.event_handlers['f10'] = self.export_report + self.event_handlers['f11'] = self.simplify_navmesh + self.event_handlers['f12'] = self.detect_bottlenecks + + # 注册事件 + for event, handler in self.event_handlers.items(): + world.accept(event, handler) + world.accept(event.upper(), handler) # 同时注册大写事件 + + def _unregister_event_handlers(self): + """ + 移除事件处理器 + """ + world = self.plugin_manager.world + + for event in self.event_handlers.keys(): + world.ignore(event) + world.ignore(event.upper()) + + self.event_handlers.clear() + + def _create_gui(self): + """ + 创建GUI界面 + """ + try: + world = self.plugin_manager.world + + # 检查是否有GUI管理器 + if not hasattr(world, 'gui_manager') or not world.gui_manager: + print("⚠ GUI管理器不可用,跳过GUI创建") + return + + gui_manager = world.gui_manager + + # 创建导航网格控制面板 + panel_button = gui_manager.createGUIButton( + pos=(0.02, 0, 0.75), + text="导航网格", + size=0.06 + ) + self.gui_elements.append(panel_button) + + # 创建功能按钮 + functions = [ + ("编辑器 [F1]", self.toggle_navmesh_editor), + ("显示/隐藏 [F2]", self.toggle_navmesh_visibility), + ("随机路径 [F3]", self.find_random_path), + ("清除路径 [F4]", self.clear_paths), + ("生成网格 [F5]", self.generate_navmesh), + ("验证网格 [F6]", self.validate_navmesh), + ("优化网格 [F7]", self.optimize_navmesh), + ("显示统计 [F8]", self.show_stats), + ("分析网格 [F9]", self.analyze_navmesh), + ("导出报告 [F10]", self.export_report), + ("简化网格 [F11]", self.simplify_navmesh), + ("检测瓶颈 [F12]", self.detect_bottlenecks) + ] + + for i, (name, callback) in enumerate(functions): + button = gui_manager.createGUIButton( + pos=(0.02, 0, 0.68 - i * 0.055), + text=name, + size=0.045 + ) + self.gui_elements.append(button) + world.accept(f"navmesh_function_{name}", callback) + + except Exception as e: + print(f"⚠ GUI创建失败: {e}") + + def _cleanup_gui(self): + """ + 清理GUI界面 + """ + try: + world = self.plugin_manager.world + + if hasattr(world, 'gui_manager') and world.gui_manager: + gui_manager = world.gui_manager + + # 删除所有GUI元素 + for element in self.gui_elements: + gui_manager.deleteGUIElement(element) + + self.gui_elements.clear() + + except Exception as e: + print(f"⚠ GUI清理失败: {e}") + + # 事件处理方法 + def toggle_navmesh_editor(self): + """切换导航网格编辑器显示""" + if self.navmesh_editor: + self.navmesh_editor.toggle_visibility() + + def toggle_navmesh_visibility(self): + """切换导航网格可见性""" + if self.navmesh_manager: + self.navmesh_manager.toggle_visibility() + + def find_random_path(self): + """查找随机路径""" + if self.pathfinder and self.navmesh_manager: + # 获取随机起点和终点 + start = self.navmesh_manager.get_random_point() + end = self.navmesh_manager.get_random_point() + + if start and end: + path = self.pathfinder.find_path(start, end) + if path: + print(f"✓ 找到路径,包含 {len(path)} 个节点") + # 可视化路径 + self.navmesh_manager.visualize_path(path) + else: + print("⚠ 未找到路径") + + def clear_paths(self): + """清除所有路径可视化""" + if self.navmesh_manager: + self.navmesh_manager.clear_path_visualization() + + def generate_navmesh(self): + """生成导航网格""" + if self.navmesh_manager: + success = self.navmesh_manager.generate_from_scene() + if success: + print("✓ 导航网格生成完成") + else: + print("✗ 导航网格生成失败") + + def validate_navmesh(self): + """验证导航网格""" + if self.navmesh_manager: + is_valid = self.navmesh_manager.validate_navmesh() + if is_valid: + print("✓ 导航网格验证通过") + else: + print("✗ 导航网格验证失败") + + def optimize_navmesh(self): + """优化导航网格""" + if self.navmesh_manager and self.navmesh_tools: + print("✓ 开始优化导航网格...") + optimization_result = self.navmesh_tools.optimize_mesh('medium') + print(f"✓ 导航网格优化完成: {optimization_result}") + + def show_stats(self): + """显示统计信息""" + if self.navmesh_manager: + stats = self.navmesh_manager.get_stats() + print("=== 导航网格统计信息 ===") + for key, value in stats.items(): + print(f" {key}: {value}") + + if self.pathfinder: + path_stats = self.pathfinder.get_pathfinding_stats() + print("=== 寻路统计信息 ===") + for key, value in path_stats.items(): + print(f" {key}: {value}") + + if self.navmesh_editor: + editor_stats = self.navmesh_editor.get_stats() + print("=== 编辑器统计信息 ===") + for key, value in editor_stats.items(): + print(f" {key}: {value}") + + def analyze_navmesh(self): + """分析导航网格""" + if self.navmesh_tools: + print("✓ 开始分析导航网格...") + quality_report = self.navmesh_tools.analyze_mesh_quality() + suggestions = self.navmesh_tools.suggest_optimizations() + + print("=== 导航网格质量分析 ===") + print(f" 多边形数量: {quality_report.get('polygon_count', 0)}") + print(f" 顶点数量: {quality_report.get('vertex_count', 0)}") + print(f" 孤立多边形: {quality_report.get('isolated_polygons', 0)}") + print(f" 小多边形: {quality_report.get('small_polygons', 0)}") + print(f" 不规则多边形: {quality_report.get('irregular_polygons', 0)}") + + print("=== 优化建议 ===") + for i, suggestion in enumerate(suggestions[:5]): # 显示前5个建议 + print(f" {i+1}. {suggestion.get('description', '无描述')}") + print(f" 建议: {suggestion.get('recommendation', '无建议')}") + + def export_report(self): + """导出分析报告""" + if self.navmesh_tools: + print("✓ 开始导出分析报告...") + timestamp = int(time.time()) + filename = f"navmesh_analysis_report_{timestamp}.json" + success = self.navmesh_tools.export_report(filename) + if success: + print(f"✓ 分析报告已导出到: {filename}") + else: + print("✗ 导出分析报告失败") + + def simplify_navmesh(self): + """简化导航网格""" + if self.navmesh_tools: + print("✓ 开始简化导航网格...") + simplification_result = self.navmesh_tools.optimize_mesh('light') + print(f"✓ 导航网格简化完成: {simplification_result}") + + def detect_bottlenecks(self): + """检测瓶颈区域""" + if self.navmesh_tools: + print("✓ 开始检测瓶颈区域...") + bottlenecks = self.navmesh_tools.detect_bottlenecks() + + print("=== 瓶颈区域检测结果 ===") + if bottlenecks: + for i, bottleneck in enumerate(bottlenecks[:5]): # 显示前5个瓶颈 + position = bottleneck.get('position', '未知') + compactness = bottleneck.get('compactness', 0) + print(f" {i+1}. 位置: {position}, 紧凑度: {compactness:.4f}") + else: + print(" 未检测到明显的瓶颈区域") + + def get_info(self) -> dict: + """ + 获取插件信息 + """ + info = super().get_info() + info.update({ + "version": self.config.get("version", "1.0.0"), + "author": self.config.get("author", "EG Team"), + "description": self.config.get("description", "导航网格插件"), + "navmesh_loaded": self.navmesh_manager.is_navmesh_loaded() if self.navmesh_manager else False, + "navmesh_polygons": self.navmesh_manager.get_polygon_count() if self.navmesh_manager else 0, + "is_enabled": self.is_enabled, + "is_initialized": self.is_initialized + }) + return info + + # 公共接口方法 + def load_navmesh_from_file(self, filepath: str) -> bool: + """从文件加载导航网格""" + if self.navmesh_tools: + return self.navmesh_tools.load_navmesh_from_file(filepath) + return False + + def save_navmesh_to_file(self, filepath: str) -> bool: + """保存导航网格到文件""" + if self.navmesh_tools: + return self.navmesh_tools.save_navmesh_to_file(filepath) + return False + + def generate_navmesh_from_scene(self) -> bool: + """从当前场景生成导航网格""" + if self.navmesh_manager: + return self.navmesh_manager.generate_from_scene() + return False + + def find_path(self, start_point, end_point): + """查找两点之间的路径""" + if self.pathfinder: + return self.pathfinder.find_path(start_point, end_point) + return None + + def is_point_walkable(self, point) -> bool: + """检查点是否可行走""" + if self.navmesh_manager: + return self.navmesh_manager.is_point_walkable(point) + return False + + def get_closest_point(self, point): + """获取导航网格上最近的点""" + if self.navmesh_manager: + return self.navmesh_manager.get_closest_point(point) + return None + + def add_dynamic_obstacle(self, position, radius, height): + """添加动态障碍物""" + if self.navmesh_manager: + obstacle_id = self.navmesh_manager.add_obstacle(position, radius, height) + # 通知寻路器 + if self.pathfinder: + self.pathfinder.add_dynamic_obstacle(obstacle_id, []) + return obstacle_id + return -1 + + def remove_dynamic_obstacle(self, obstacle_id): + """移除动态障碍物""" + if self.navmesh_manager: + success = self.navmesh_manager.remove_obstacle(obstacle_id) + # 通知寻路器 + if self.pathfinder and success: + self.pathfinder.remove_dynamic_obstacle(obstacle_id) + return success + return False + + def set_navmesh_settings(self, settings: dict): + """设置导航网格参数""" + if self.navmesh_manager: + if 'cell_size' in settings: + self.navmesh_manager.set_cell_size(settings['cell_size']) + if 'agent_height' in settings: + self.navmesh_manager.set_agent_height(settings['agent_height']) + if 'agent_radius' in settings: + self.navmesh_manager.set_agent_radius(settings['agent_radius']) + if 'max_slope' in settings: + self.navmesh_manager.set_max_slope(settings['max_slope']) + if 'max_climb' in settings: + self.navmesh_manager.set_max_climb(settings['max_climb']) + if 'min_region_area' in settings: + self.navmesh_manager.set_min_region_area(settings['min_region_area']) + if 'merge_region_area' in settings: + self.navmesh_manager.set_merge_region_area(settings['merge_region_area']) + if 'edge_max_len' in settings: + self.navmesh_manager.set_edge_max_len(settings['edge_max_len']) + if 'edge_max_error' in settings: + self.navmesh_manager.set_edge_max_error(settings['edge_max_error']) + if 'verts_per_poly' in settings: + self.navmesh_manager.set_verts_per_poly(settings['verts_per_poly']) + + def get_navmesh_settings(self) -> dict: + """获取导航网格参数""" + if self.navmesh_manager: + return { + 'cell_size': self.navmesh_manager.cell_size, + 'agent_height': self.navmesh_manager.agent_height, + 'agent_radius': self.navmesh_manager.agent_radius, + 'max_slope': self.navmesh_manager.max_slope, + 'max_climb': self.navmesh_manager.max_climb, + 'min_region_area': self.navmesh_manager.min_region_area, + 'merge_region_area': self.navmesh_manager.merge_region_area, + 'edge_max_len': self.navmesh_manager.edge_max_len, + 'edge_max_error': self.navmesh_manager.edge_max_error, + 'verts_per_poly': self.navmesh_manager.verts_per_poly + } + return {} + + def set_pathfinder_algorithm(self, algorithm: str): + """设置寻路算法""" + if self.pathfinder: + self.pathfinder.set_algorithm(algorithm) + + def get_available_algorithms(self) -> list: + """获取可用的寻路算法""" + if self.pathfinder: + return self.pathfinder.get_available_algorithms() + return [] + + def set_pathfinder_settings(self, settings: dict): + """设置寻路参数""" + if self.pathfinder: + if 'heuristic_weight' in settings: + self.pathfinder.set_heuristic_weight(settings['heuristic_weight']) + if 'max_path_nodes' in settings: + self.pathfinder.set_max_path_nodes(settings['max_path_nodes']) + if 'use_funnel_pruning' in settings: + self.pathfinder.enable_funnel_pruning(settings['use_funnel_pruning']) + if 'smooth_paths' in settings: + self.pathfinder.enable_path_smoothing(settings['smooth_paths']) + + def get_pathfinder_settings(self) -> dict: + """获取寻路参数""" + if self.pathfinder: + return { + 'heuristic_weight': self.pathfinder.heuristic_weight, + 'max_path_nodes': self.pathfinder.max_path_nodes, + 'use_funnel_pruning': self.pathfinder.use_funnel_pruning, + 'smooth_paths': self.pathfinder.smooth_paths, + 'current_algorithm': self.pathfinder.current_algorithm + } + return {} + + def find_path_with_waypoints(self, waypoints: list): + """查找经过多个路点的路径""" + if self.pathfinder: + return self.pathfinder.find_path_with_multiple_waypoints(waypoints) + return None + + def estimate_path_cost(self, start_point, end_point): + """估算路径代价""" + if self.pathfinder: + return self.pathfinder.estimate_path_cost(start_point, end_point) + return None + + def validate_path(self, path: list) -> bool: + """验证路径有效性""" + if self.pathfinder: + return self.pathfinder.validate_path(path) + return False + + def get_path_complexity(self, path: list) -> dict: + """分析路径复杂度""" + if self.pathfinder: + return self.pathfinder.get_path_complexity(path) + return {} + + def find_alternative_paths(self, start_point, end_point, count: int = 3): + """查找备选路径""" + if self.pathfinder: + return self.pathfinder.find_alternative_paths(start_point, end_point, count) + return [] + + def clear_path_cache(self): + """清空路径缓存""" + if self.pathfinder: + self.pathfinder.clear_cache() + + def rebuild_navmesh(self): + """重新构建导航网格""" + if self.navmesh_manager: + return self.navmesh_manager.rebuild_navmesh() + return False + + def enable_spatial_hashing(self, enabled: bool): + """启用/禁用空间哈希""" + if self.navmesh_manager: + self.navmesh_manager.enable_spatial_hashing(enabled) + + def set_spatial_hash_cell_size(self, size: float): + """设置空间哈希网格单元大小""" + if self.navmesh_manager: + self.navmesh_manager.set_spatial_hash_cell_size(size) + + def export_statistics(self, filepath: str) -> bool: + """导出统计信息""" + try: + stats_data = { + 'plugin_info': self.get_info(), + 'navmesh_settings': self.get_navmesh_settings(), + 'pathfinder_settings': self.get_pathfinder_settings(), + 'performance_stats': self.performance_stats, + 'timestamp': time.strftime("%Y-%m-%d %H:%M:%S") + } + + if self.navmesh_manager: + stats_data['navmesh_stats'] = self.navmesh_manager.get_stats() + + if self.pathfinder: + stats_data['pathfinder_stats'] = self.pathfinder.get_pathfinding_stats() + + if self.navmesh_editor: + stats_data['editor_stats'] = self.navmesh_editor.get_stats() + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(stats_data, f, indent=2, ensure_ascii=False) + + print(f"✓ 统计信息已导出到: {filepath}") + return True + + except Exception as e: + print(f"✗ 导出统计信息失败: {e}") + return False + + def import_settings(self, filepath: str) -> bool: + """导入设置""" + try: + with open(filepath, 'r', encoding='utf-8') as f: + settings = json.load(f) + + # 应用导航网格设置 + if 'navmesh_settings' in settings: + self.set_navmesh_settings(settings['navmesh_settings']) + + # 应用寻路设置 + if 'pathfinder_settings' in settings: + pf_settings = settings['pathfinder_settings'] + self.set_pathfinder_settings(pf_settings) + + print(f"✓ 设置已从 {filepath} 导入") + return True + + except Exception as e: + print(f"✗ 导入设置失败: {e}") + return False + + def export_settings(self, filepath: str) -> bool: + """导出设置""" + try: + settings_data = { + 'navmesh_settings': self.get_navmesh_settings(), + 'pathfinder_settings': self.get_pathfinder_settings() + } + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(settings_data, f, indent=2, ensure_ascii=False) + + print(f"✓ 设置已导出到: {filepath}") + return True + + except Exception as e: + print(f"✗ 导出设置失败: {e}") + return False + + # 新增的工具方法 + def analyze_mesh_quality(self) -> dict: + """分析导航网格质量""" + if self.navmesh_tools: + return self.navmesh_tools.analyze_mesh_quality() + return {} + + def suggest_optimizations(self) -> list: + """提供优化建议""" + if self.navmesh_tools: + return self.navmesh_tools.suggest_optimizations() + return [] + + def analyze_pathfinding_performance(self, sample_count: int = 100) -> dict: + """分析寻路性能""" + if self.navmesh_tools: + return self.navmesh_tools.analyze_pathfinding_performance(sample_count) + return {} + + def detect_navigation_bottlenecks(self) -> list: + """检测导航网格中的瓶颈区域""" + if self.navmesh_tools: + return self.navmesh_tools.detect_bottlenecks() + return [] + + def analyze_coverage(self) -> dict: + """分析导航网格覆盖情况""" + if self.navmesh_tools: + return self.navmesh_tools.analyze_coverage() + return {} + + def generate_analysis_report(self) -> dict: + """生成完整的分析报告""" + if self.navmesh_tools: + return self.navmesh_tools.generate_report() + return {} + + def export_analysis_report(self, filepath: str) -> bool: + """导出分析报告到文件""" + if self.navmesh_tools: + return self.navmesh_tools.export_report(filepath) + return False + + def get_mesh_complexity_score(self) -> float: + """计算导航网格复杂度评分""" + if self.navmesh_tools: + return self.navmesh_tools.get_complexity_score() + return 0.0 + + def recommend_mesh_simplification(self) -> dict: + """推荐网格简化策略""" + if self.navmesh_tools: + return self.navmesh_tools.recommend_simplification() + return {} + + def merge_adjacent_polygons(self) -> int: + """合并相邻的多边形""" + if self.navmesh_tools: + return self.navmesh_tools.merge_adjacent_polygons() + return 0 + + def simplify_mesh_polygons(self) -> int: + """简化多边形(移除不必要的顶点)""" + if self.navmesh_tools: + return self.navmesh_tools.simplify_polygons() + return 0 + + def remove_small_mesh_polygons(self) -> int: + """移除极小的多边形""" + if self.navmesh_tools: + return self.navmesh_tools.remove_small_polygons() + return 0 + + def optimize_navigation_mesh(self, optimization_level: str = 'medium') -> dict: + """执行完整的网格优化""" + if self.navmesh_tools: + return self.navmesh_tools.optimize_mesh(optimization_level) + return {} + + def set_optimization_settings(self, settings: dict): + """设置优化参数""" + if self.navmesh_tools: + self.navmesh_tools.set_optimization_settings(settings) + + def get_optimization_settings(self) -> dict: + """获取优化参数""" + if self.navmesh_tools: + return self.navmesh_tools.get_optimization_settings() + return {} + + def fill_mesh_gaps(self) -> int: + """填补导航网格中的空隙""" + if self.navmesh_tools: + return self.navmesh_tools.fill_gaps() + return 0 + + def smooth_mesh_boundaries(self) -> int: + """平滑多边形边界""" + if self.navmesh_tools: + return self.navmesh_tools.smooth_boundaries() + return 0 + + def balance_polygon_sizes(self) -> int: + """平衡多边形大小""" + if self.navmesh_tools: + return self.navmesh_tools.balance_polygon_sizes() + return 0 + + def serialize_navigation_mesh(self) -> str: + """序列化导航网格为字符串""" + if self.navmesh_tools: + return self.navmesh_tools.serialize_navmesh() + return "" + + def deserialize_navigation_mesh(self, data_str: str) -> bool: + """从字符串反序列化导航网格""" + if self.navmesh_tools: + return self.navmesh_tools.deserialize_navmesh(data_str) + return False + + def save_navmesh_to_file_advanced(self, filepath: str, + compress: bool = False, pretty_print: bool = False) -> bool: + """保存导航网格到文件(高级选项)""" + if self.navmesh_tools: + return self.navmesh_tools.save_navmesh_to_file(filepath, compress, pretty_print) + return False + + def load_navmesh_from_file_advanced(self, filepath: str) -> bool: + """从文件加载导航网格(高级选项)""" + if self.navmesh_tools: + return self.navmesh_tools.load_navmesh_from_file(filepath) + return False + + def export_to_standard_format(self, filepath: str, + format_type: str = 'obj') -> bool: + """导出为标准格式""" + if self.navmesh_tools: + return self.navmesh_tools.export_to_standard_format(filepath, format_type) + return False + + def import_from_standard_format(self, filepath: str, + format_type: str = 'obj') -> bool: + """从标准格式导入""" + if self.navmesh_tools: + return self.navmesh_tools.import_from_standard_format(filepath, format_type) + return False + + def get_navmesh_file_info(self, filepath: str) -> dict: + """获取导航网格文件信息""" + if self.navmesh_tools: + return self.navmesh_tools.get_file_info(filepath) + return {} + + def batch_analyze_navmeshes(self, filepaths: list) -> dict: + """批量分析多个导航网格文件""" + if self.navmesh_tools: + return self.navmesh_tools.batch_analyze(filepaths) + return {} + + def compare_navigation_meshes(self, navmesh1_path: str, navmesh2_path: str) -> dict: + """比较两个导航网格""" + if self.navmesh_tools: + return self.navmesh_tools.compare_navmeshes(navmesh1_path, navmesh2_path) + return {} + + def validate_navmesh_file(self, filepath: str) -> dict: + """验证导航网格文件的有效性""" + if self.navmesh_tools: + return self.navmesh_tools.validate_navmesh_file(filepath) + return {} + + def optimize_and_save_navmesh(self, input_filepath: str, output_filepath: str, + optimization_level: str = 'medium') -> dict: + """优化并保存导航网格""" + if self.navmesh_tools: + return self.navmesh_tools.optimize_and_save(input_filepath, output_filepath, optimization_level) + return {} + + def create_optimization_pipeline(self, steps: list) -> dict: + """创建优化流水线""" + if self.navmesh_tools: + return self.navmesh_tools.create_optimization_pipeline(steps) return {} \ No newline at end of file diff --git a/plugins/user/navigation_mesh/tools/navmesh_analyzer.py b/plugins/user/navigation_mesh/tools/navmesh_analyzer.py index 73fbd3a6..8cb780eb 100644 --- a/plugins/user/navigation_mesh/tools/navmesh_analyzer.py +++ b/plugins/user/navigation_mesh/tools/navmesh_analyzer.py @@ -1,492 +1,492 @@ -""" -导航网格分析工具 -提供导航网格质量分析、优化建议和性能评估功能 -""" - -import math -import time -from typing import List, Dict, Tuple, Optional, Set -from collections import defaultdict, deque -import json -from panda3d.core import Point3 - -class NavMeshAnalyzer: - """ - 导航网格分析器 - 提供导航网格质量分析、优化建议和性能评估功能 - """ - - def __init__(self, navmesh_manager): - """ - 初始化导航网格分析器 - - Args: - navmesh_manager: 导航网格管理器实例 - """ - self.navmesh_manager = navmesh_manager - self.analysis_results = {} - self.optimization_suggestions = [] - - def analyze_mesh_quality(self) -> Dict: - """ - 分析导航网格质量 - - Returns: - 质量分析结果 - """ - if not self.navmesh_manager or not self.navmesh_manager.polygons: - return {} - - results = { - 'polygon_count': len(self.navmesh_manager.polygons), - 'vertex_count': 0, - 'edge_count': 0, - 'isolated_polygons': 0, - 'overlapping_polygons': 0, - 'small_polygons': 0, - 'irregular_polygons': 0, - 'connectivity_issues': 0, - 'portal_issues': 0, - 'area_statistics': {}, - 'vertex_statistics': {}, - 'performance_metrics': {} - } - - # 统计顶点和边 - total_vertices = 0 - total_edges = 0 - polygon_areas = [] - vertex_counts = [] - isolated_count = 0 - small_polygon_count = 0 - irregular_polygon_count = 0 - overlapping_count = 0 - connectivity_issues = 0 - portal_issues = 0 - - # 分析每个多边形 - for polygon_id, polygon in self.navmesh_manager.polygons.items(): - vertex_count = len(polygon.vertices) - total_vertices += vertex_count - vertex_counts.append(vertex_count) - - # 边数等于顶点数 - total_edges += vertex_count - - # 面积统计 - area = polygon.area - polygon_areas.append(area) - - # 检查小多边形 - if area < 0.1: # 面积极小阈值 - small_polygon_count += 1 - - # 检查不规则多边形(顶点数过多) - if vertex_count > 8: # 顶点数过多阈值 - irregular_polygon_count += 1 - - # 检查孤立多边形(没有邻居) - if not polygon.neighbors: - isolated_count += 1 - - # 检查连接性问题 - for neighbor_id in polygon.neighbors: - if neighbor_id not in self.navmesh_manager.polygons: - connectivity_issues += 1 - - # 检查门户点问题 - for neighbor_id, portal_points in polygon.portal_points.items(): - if neighbor_id not in polygon.neighbors: - portal_issues += 1 - - # 计算面积统计 - if polygon_areas: - results['area_statistics'] = { - 'total_area': sum(polygon_areas), - 'average_area': sum(polygon_areas) / len(polygon_areas), - 'min_area': min(polygon_areas), - 'max_area': max(polygon_areas), - 'area_variance': self._calculate_variance(polygon_areas) - } - - # 计算顶点统计 - if vertex_counts: - results['vertex_statistics'] = { - 'total_vertices': total_vertices, - 'average_vertices': sum(vertex_counts) / len(vertex_counts), - 'min_vertices': min(vertex_counts), - 'max_vertices': max(vertex_counts), - 'vertex_variance': self._calculate_variance(vertex_counts) - } - - # 设置结果 - results['vertex_count'] = total_vertices - results['edge_count'] = total_edges - results['isolated_polygons'] = isolated_count - results['small_polygons'] = small_polygon_count - results['irregular_polygons'] = irregular_polygon_count - results['overlapping_polygons'] = overlapping_count - results['connectivity_issues'] = connectivity_issues - results['portal_issues'] = portal_issues - - self.analysis_results = results - return results - - def _calculate_variance(self, values: List[float]) -> float: - """计算方差""" - if not values: - return 0.0 - - mean = sum(values) / len(values) - variance = sum((x - mean) ** 2 for x in values) / len(values) - return variance - - def suggest_optimizations(self) -> List[Dict]: - """ - 提供优化建议 - - Returns: - 优化建议列表 - """ - suggestions = [] - - if not self.analysis_results: - self.analyze_mesh_quality() - - results = self.analysis_results - - # 检查多边形数量 - if results.get('polygon_count', 0) > 10000: - suggestions.append({ - 'type': 'performance', - 'severity': 'high', - 'description': '导航网格包含过多的多边形', - 'recommendation': '考虑简化网格或使用分层寻路' - }) - - # 检查孤立多边形 - if results.get('isolated_polygons', 0) > 0: - suggestions.append({ - 'type': 'connectivity', - 'severity': 'medium', - 'description': '存在孤立的多边形', - 'recommendation': '检查并连接孤立的多边形或移除它们' - }) - - # 检查小多边形 - if results.get('small_polygons', 0) > results.get('polygon_count', 0) * 0.1: - suggestions.append({ - 'type': 'quality', - 'severity': 'medium', - 'description': '存在过多极小的多边形', - 'recommendation': '合并相邻的小多边形或移除它们' - }) - - # 检查不规则多边形 - if results.get('irregular_polygons', 0) > results.get('polygon_count', 0) * 0.2: - suggestions.append({ - 'type': 'quality', - 'severity': 'low', - 'description': '存在过多顶点的不规则多边形', - 'recommendation': '简化复杂多边形以提高性能' - }) - - # 检查连接性问题 - if results.get('connectivity_issues', 0) > 0: - suggestions.append({ - 'type': 'correctness', - 'severity': 'high', - 'description': '存在连接性问题', - 'recommendation': '修复多边形间的连接关系' - }) - - # 检查门户点问题 - if results.get('portal_issues', 0) > 0: - suggestions.append({ - 'type': 'correctness', - 'severity': 'high', - 'description': '存在门户点问题', - 'recommendation': '重新计算多边形间的门户点' - }) - - self.optimization_suggestions = suggestions - return suggestions - - def analyze_pathfinding_performance(self, sample_count: int = 100) -> Dict: - """ - 分析寻路性能 - - Args: - sample_count: 采样次数 - - Returns: - 性能分析结果 - """ - if not self.navmesh_manager or not hasattr(self.navmesh_manager, 'world'): - return {} - - from .pathfinder import Pathfinder - pathfinder = Pathfinder(self.navmesh_manager) - - performance_results = { - 'total_time': 0.0, - 'average_time': 0.0, - 'min_time': float('inf'), - 'max_time': 0.0, - 'success_count': 0, - 'failure_count': 0, - 'average_path_length': 0.0, - 'path_lengths': [] - } - - total_length = 0.0 - path_lengths = [] - - # 执行多次寻路测试 - for _ in range(sample_count): - start_point = self.navmesh_manager.get_random_point() - end_point = self.navmesh_manager.get_random_point() - - if start_point and end_point: - start_time = time.time() - path = pathfinder.find_path(start_point, end_point) - end_time = time.time() - - search_time = end_time - start_time - performance_results['total_time'] += search_time - performance_results['min_time'] = min(performance_results['min_time'], search_time) - performance_results['max_time'] = max(performance_results['max_time'], search_time) - - if path: - performance_results['success_count'] += 1 - path_length = sum((path[i+1] - path[i]).length() for i in range(len(path)-1)) - total_length += path_length - path_lengths.append(path_length) - else: - performance_results['failure_count'] += 1 - - # 计算平均值 - if sample_count > 0: - performance_results['average_time'] = performance_results['total_time'] / sample_count - - if path_lengths: - performance_results['average_path_length'] = total_length / len(path_lengths) - performance_results['path_lengths'] = path_lengths - - return performance_results - - def detect_bottlenecks(self) -> List[Dict]: - """ - 检测导航网格中的瓶颈区域 - - Returns: - 瓶颈区域列表 - """ - bottlenecks = [] - - if not self.navmesh_manager or not self.navmesh_manager.polygons: - return bottlenecks - - # 计算每个多边形的"狭窄度" - for polygon_id, polygon in self.navmesh_manager.polygons.items(): - # 狭窄度基于面积和周长的比值 - if len(polygon.vertices) >= 3: - perimeter = self._calculate_perimeter(polygon.vertices) - if perimeter > 0: - compactness = polygon.area / (perimeter ** 2) - - # 如果紧凑度很低,可能是狭窄区域 - if compactness < 0.01: # 阈值需要根据实际情况调整 - bottlenecks.append({ - 'polygon_id': polygon_id, - 'position': polygon.center, - 'compactness': compactness, - 'area': polygon.area, - 'perimeter': perimeter - }) - - # 按紧凑度排序 - bottlenecks.sort(key=lambda x: x['compactness']) - - return bottlenecks[:10] # 返回最狭窄的10个区域 - - def _calculate_perimeter(self, vertices: List[Point3]) -> float: - """计算多边形周长""" - if len(vertices) < 2: - return 0.0 - - perimeter = 0.0 - for i in range(len(vertices)): - p1 = vertices[i] - p2 = vertices[(i + 1) % len(vertices)] - perimeter += (p2 - p1).length() - - return perimeter - - def analyze_coverage(self) -> Dict: - """ - 分析导航网格覆盖情况 - - Returns: - 覆盖情况分析结果 - """ - coverage_results = { - 'covered_area': 0.0, - 'total_area': 0.0, - 'coverage_ratio': 0.0, - 'uncovered_regions': [] - } - - if not self.navmesh_manager or not self.navmesh_manager.polygons: - return coverage_results - - # 计算总覆盖面积(简单近似) - total_area = 0.0 - for polygon in self.navmesh_manager.polygons.values(): - total_area += polygon.area - - coverage_results['covered_area'] = total_area - coverage_results['total_area'] = total_area # 简化处理 - coverage_results['coverage_ratio'] = 1.0 if total_area > 0 else 0.0 - - return coverage_results - - def generate_report(self) -> Dict: - """ - 生成完整的分析报告 - - Returns: - 分析报告 - """ - report = { - 'timestamp': time.time(), - 'mesh_quality': self.analyze_mesh_quality(), - 'optimization_suggestions': self.suggest_optimizations(), - 'coverage_analysis': self.analyze_coverage(), - 'bottlenecks': self.detect_bottlenecks() - } - - return report - - def export_report(self, filepath: str) -> bool: - """ - 导出分析报告到文件 - - Args: - filepath: 导出文件路径 - - Returns: - 是否导出成功 - """ - try: - report = self.generate_report() - - # 转换不可序列化的对象 - serializable_report = self._make_serializable(report) - - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(serializable_report, f, indent=2, ensure_ascii=False) - - return True - except Exception as e: - print(f"导出报告失败: {e}") - return False - - def _make_serializable(self, obj): - """将对象转换为可序列化的格式""" - if isinstance(obj, dict): - return {key: self._make_serializable(value) for key, value in obj.items()} - elif isinstance(obj, list): - return [self._make_serializable(item) for item in obj] - elif isinstance(obj, Point3): - return [obj.x, obj.y, obj.z] - elif isinstance(obj, float) and (math.isnan(obj) or math.isinf(obj)): - return str(obj) - else: - return obj - - def get_complexity_score(self) -> float: - """ - 计算导航网格复杂度评分 - - Returns: - 复杂度评分 (0-100) - """ - if not self.analysis_results: - self.analyze_mesh_quality() - - results = self.analysis_results - score = 50.0 # 基础分数 - - # 根据多边形数量调整 - polygon_count = results.get('polygon_count', 0) - if polygon_count > 5000: - score += min(30, (polygon_count - 5000) / 1000) - elif polygon_count < 1000: - score -= min(20, (1000 - polygon_count) / 100) - - # 根据问题数量调整 - issues = (results.get('isolated_polygons', 0) + - results.get('connectivity_issues', 0) + - results.get('portal_issues', 0)) - score += min(20, issues / 10) - - # 根据不规则多边形调整 - irregular = results.get('irregular_polygons', 0) - score += min(10, irregular / 50) - - return max(0, min(100, score)) - - def recommend_simplification(self) -> Dict: - """ - 推荐网格简化策略 - - Returns: - 简化策略建议 - """ - if not self.analysis_results: - self.analyze_mesh_quality() - - results = self.analysis_results - recommendations = { - 'should_simplify': False, - 'reasons': [], - 'simplification_level': 'none', # none, light, medium, heavy - 'target_polygon_count': results.get('polygon_count', 0) - } - - polygon_count = results.get('polygon_count', 0) - isolated_count = results.get('isolated_polygons', 0) - small_count = results.get('small_polygons', 0) - irregular_count = results.get('irregular_polygons', 0) - - # 检查是否需要简化 - if polygon_count > 5000: - recommendations['should_simplify'] = True - recommendations['reasons'].append('多边形数量过多') - - if isolated_count > polygon_count * 0.05: - recommendations['should_simplify'] = True - recommendations['reasons'].append('存在大量孤立多边形') - - if small_count > polygon_count * 0.1: - recommendations['should_simplify'] = True - recommendations['reasons'].append('存在大量极小多边形') - - if irregular_count > polygon_count * 0.2: - recommendations['should_simplify'] = True - recommendations['reasons'].append('存在大量不规则多边形') - - # 确定简化级别 - if recommendations['should_simplify']: - if polygon_count > 10000: - recommendations['simplification_level'] = 'heavy' - recommendations['target_polygon_count'] = int(polygon_count * 0.5) - elif polygon_count > 5000: - recommendations['simplification_level'] = 'medium' - recommendations['target_polygon_count'] = int(polygon_count * 0.7) - else: - recommendations['simplification_level'] = 'light' - recommendations['target_polygon_count'] = int(polygon_count * 0.8) - +""" +导航网格分析工具 +提供导航网格质量分析、优化建议和性能评估功能 +""" + +import math +import time +from typing import List, Dict, Tuple, Optional, Set +from collections import defaultdict, deque +import json +from panda3d.core import Point3 + +class NavMeshAnalyzer: + """ + 导航网格分析器 + 提供导航网格质量分析、优化建议和性能评估功能 + """ + + def __init__(self, navmesh_manager): + """ + 初始化导航网格分析器 + + Args: + navmesh_manager: 导航网格管理器实例 + """ + self.navmesh_manager = navmesh_manager + self.analysis_results = {} + self.optimization_suggestions = [] + + def analyze_mesh_quality(self) -> Dict: + """ + 分析导航网格质量 + + Returns: + 质量分析结果 + """ + if not self.navmesh_manager or not self.navmesh_manager.polygons: + return {} + + results = { + 'polygon_count': len(self.navmesh_manager.polygons), + 'vertex_count': 0, + 'edge_count': 0, + 'isolated_polygons': 0, + 'overlapping_polygons': 0, + 'small_polygons': 0, + 'irregular_polygons': 0, + 'connectivity_issues': 0, + 'portal_issues': 0, + 'area_statistics': {}, + 'vertex_statistics': {}, + 'performance_metrics': {} + } + + # 统计顶点和边 + total_vertices = 0 + total_edges = 0 + polygon_areas = [] + vertex_counts = [] + isolated_count = 0 + small_polygon_count = 0 + irregular_polygon_count = 0 + overlapping_count = 0 + connectivity_issues = 0 + portal_issues = 0 + + # 分析每个多边形 + for polygon_id, polygon in self.navmesh_manager.polygons.items(): + vertex_count = len(polygon.vertices) + total_vertices += vertex_count + vertex_counts.append(vertex_count) + + # 边数等于顶点数 + total_edges += vertex_count + + # 面积统计 + area = polygon.area + polygon_areas.append(area) + + # 检查小多边形 + if area < 0.1: # 面积极小阈值 + small_polygon_count += 1 + + # 检查不规则多边形(顶点数过多) + if vertex_count > 8: # 顶点数过多阈值 + irregular_polygon_count += 1 + + # 检查孤立多边形(没有邻居) + if not polygon.neighbors: + isolated_count += 1 + + # 检查连接性问题 + for neighbor_id in polygon.neighbors: + if neighbor_id not in self.navmesh_manager.polygons: + connectivity_issues += 1 + + # 检查门户点问题 + for neighbor_id, portal_points in polygon.portal_points.items(): + if neighbor_id not in polygon.neighbors: + portal_issues += 1 + + # 计算面积统计 + if polygon_areas: + results['area_statistics'] = { + 'total_area': sum(polygon_areas), + 'average_area': sum(polygon_areas) / len(polygon_areas), + 'min_area': min(polygon_areas), + 'max_area': max(polygon_areas), + 'area_variance': self._calculate_variance(polygon_areas) + } + + # 计算顶点统计 + if vertex_counts: + results['vertex_statistics'] = { + 'total_vertices': total_vertices, + 'average_vertices': sum(vertex_counts) / len(vertex_counts), + 'min_vertices': min(vertex_counts), + 'max_vertices': max(vertex_counts), + 'vertex_variance': self._calculate_variance(vertex_counts) + } + + # 设置结果 + results['vertex_count'] = total_vertices + results['edge_count'] = total_edges + results['isolated_polygons'] = isolated_count + results['small_polygons'] = small_polygon_count + results['irregular_polygons'] = irregular_polygon_count + results['overlapping_polygons'] = overlapping_count + results['connectivity_issues'] = connectivity_issues + results['portal_issues'] = portal_issues + + self.analysis_results = results + return results + + def _calculate_variance(self, values: List[float]) -> float: + """计算方差""" + if not values: + return 0.0 + + mean = sum(values) / len(values) + variance = sum((x - mean) ** 2 for x in values) / len(values) + return variance + + def suggest_optimizations(self) -> List[Dict]: + """ + 提供优化建议 + + Returns: + 优化建议列表 + """ + suggestions = [] + + if not self.analysis_results: + self.analyze_mesh_quality() + + results = self.analysis_results + + # 检查多边形数量 + if results.get('polygon_count', 0) > 10000: + suggestions.append({ + 'type': 'performance', + 'severity': 'high', + 'description': '导航网格包含过多的多边形', + 'recommendation': '考虑简化网格或使用分层寻路' + }) + + # 检查孤立多边形 + if results.get('isolated_polygons', 0) > 0: + suggestions.append({ + 'type': 'connectivity', + 'severity': 'medium', + 'description': '存在孤立的多边形', + 'recommendation': '检查并连接孤立的多边形或移除它们' + }) + + # 检查小多边形 + if results.get('small_polygons', 0) > results.get('polygon_count', 0) * 0.1: + suggestions.append({ + 'type': 'quality', + 'severity': 'medium', + 'description': '存在过多极小的多边形', + 'recommendation': '合并相邻的小多边形或移除它们' + }) + + # 检查不规则多边形 + if results.get('irregular_polygons', 0) > results.get('polygon_count', 0) * 0.2: + suggestions.append({ + 'type': 'quality', + 'severity': 'low', + 'description': '存在过多顶点的不规则多边形', + 'recommendation': '简化复杂多边形以提高性能' + }) + + # 检查连接性问题 + if results.get('connectivity_issues', 0) > 0: + suggestions.append({ + 'type': 'correctness', + 'severity': 'high', + 'description': '存在连接性问题', + 'recommendation': '修复多边形间的连接关系' + }) + + # 检查门户点问题 + if results.get('portal_issues', 0) > 0: + suggestions.append({ + 'type': 'correctness', + 'severity': 'high', + 'description': '存在门户点问题', + 'recommendation': '重新计算多边形间的门户点' + }) + + self.optimization_suggestions = suggestions + return suggestions + + def analyze_pathfinding_performance(self, sample_count: int = 100) -> Dict: + """ + 分析寻路性能 + + Args: + sample_count: 采样次数 + + Returns: + 性能分析结果 + """ + if not self.navmesh_manager or not hasattr(self.navmesh_manager, 'world'): + return {} + + from .pathfinder import Pathfinder + pathfinder = Pathfinder(self.navmesh_manager) + + performance_results = { + 'total_time': 0.0, + 'average_time': 0.0, + 'min_time': float('inf'), + 'max_time': 0.0, + 'success_count': 0, + 'failure_count': 0, + 'average_path_length': 0.0, + 'path_lengths': [] + } + + total_length = 0.0 + path_lengths = [] + + # 执行多次寻路测试 + for _ in range(sample_count): + start_point = self.navmesh_manager.get_random_point() + end_point = self.navmesh_manager.get_random_point() + + if start_point and end_point: + start_time = time.time() + path = pathfinder.find_path(start_point, end_point) + end_time = time.time() + + search_time = end_time - start_time + performance_results['total_time'] += search_time + performance_results['min_time'] = min(performance_results['min_time'], search_time) + performance_results['max_time'] = max(performance_results['max_time'], search_time) + + if path: + performance_results['success_count'] += 1 + path_length = sum((path[i+1] - path[i]).length() for i in range(len(path)-1)) + total_length += path_length + path_lengths.append(path_length) + else: + performance_results['failure_count'] += 1 + + # 计算平均值 + if sample_count > 0: + performance_results['average_time'] = performance_results['total_time'] / sample_count + + if path_lengths: + performance_results['average_path_length'] = total_length / len(path_lengths) + performance_results['path_lengths'] = path_lengths + + return performance_results + + def detect_bottlenecks(self) -> List[Dict]: + """ + 检测导航网格中的瓶颈区域 + + Returns: + 瓶颈区域列表 + """ + bottlenecks = [] + + if not self.navmesh_manager or not self.navmesh_manager.polygons: + return bottlenecks + + # 计算每个多边形的"狭窄度" + for polygon_id, polygon in self.navmesh_manager.polygons.items(): + # 狭窄度基于面积和周长的比值 + if len(polygon.vertices) >= 3: + perimeter = self._calculate_perimeter(polygon.vertices) + if perimeter > 0: + compactness = polygon.area / (perimeter ** 2) + + # 如果紧凑度很低,可能是狭窄区域 + if compactness < 0.01: # 阈值需要根据实际情况调整 + bottlenecks.append({ + 'polygon_id': polygon_id, + 'position': polygon.center, + 'compactness': compactness, + 'area': polygon.area, + 'perimeter': perimeter + }) + + # 按紧凑度排序 + bottlenecks.sort(key=lambda x: x['compactness']) + + return bottlenecks[:10] # 返回最狭窄的10个区域 + + def _calculate_perimeter(self, vertices: List[Point3]) -> float: + """计算多边形周长""" + if len(vertices) < 2: + return 0.0 + + perimeter = 0.0 + for i in range(len(vertices)): + p1 = vertices[i] + p2 = vertices[(i + 1) % len(vertices)] + perimeter += (p2 - p1).length() + + return perimeter + + def analyze_coverage(self) -> Dict: + """ + 分析导航网格覆盖情况 + + Returns: + 覆盖情况分析结果 + """ + coverage_results = { + 'covered_area': 0.0, + 'total_area': 0.0, + 'coverage_ratio': 0.0, + 'uncovered_regions': [] + } + + if not self.navmesh_manager or not self.navmesh_manager.polygons: + return coverage_results + + # 计算总覆盖面积(简单近似) + total_area = 0.0 + for polygon in self.navmesh_manager.polygons.values(): + total_area += polygon.area + + coverage_results['covered_area'] = total_area + coverage_results['total_area'] = total_area # 简化处理 + coverage_results['coverage_ratio'] = 1.0 if total_area > 0 else 0.0 + + return coverage_results + + def generate_report(self) -> Dict: + """ + 生成完整的分析报告 + + Returns: + 分析报告 + """ + report = { + 'timestamp': time.time(), + 'mesh_quality': self.analyze_mesh_quality(), + 'optimization_suggestions': self.suggest_optimizations(), + 'coverage_analysis': self.analyze_coverage(), + 'bottlenecks': self.detect_bottlenecks() + } + + return report + + def export_report(self, filepath: str) -> bool: + """ + 导出分析报告到文件 + + Args: + filepath: 导出文件路径 + + Returns: + 是否导出成功 + """ + try: + report = self.generate_report() + + # 转换不可序列化的对象 + serializable_report = self._make_serializable(report) + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(serializable_report, f, indent=2, ensure_ascii=False) + + return True + except Exception as e: + print(f"导出报告失败: {e}") + return False + + def _make_serializable(self, obj): + """将对象转换为可序列化的格式""" + if isinstance(obj, dict): + return {key: self._make_serializable(value) for key, value in obj.items()} + elif isinstance(obj, list): + return [self._make_serializable(item) for item in obj] + elif isinstance(obj, Point3): + return [obj.x, obj.y, obj.z] + elif isinstance(obj, float) and (math.isnan(obj) or math.isinf(obj)): + return str(obj) + else: + return obj + + def get_complexity_score(self) -> float: + """ + 计算导航网格复杂度评分 + + Returns: + 复杂度评分 (0-100) + """ + if not self.analysis_results: + self.analyze_mesh_quality() + + results = self.analysis_results + score = 50.0 # 基础分数 + + # 根据多边形数量调整 + polygon_count = results.get('polygon_count', 0) + if polygon_count > 5000: + score += min(30, (polygon_count - 5000) / 1000) + elif polygon_count < 1000: + score -= min(20, (1000 - polygon_count) / 100) + + # 根据问题数量调整 + issues = (results.get('isolated_polygons', 0) + + results.get('connectivity_issues', 0) + + results.get('portal_issues', 0)) + score += min(20, issues / 10) + + # 根据不规则多边形调整 + irregular = results.get('irregular_polygons', 0) + score += min(10, irregular / 50) + + return max(0, min(100, score)) + + def recommend_simplification(self) -> Dict: + """ + 推荐网格简化策略 + + Returns: + 简化策略建议 + """ + if not self.analysis_results: + self.analyze_mesh_quality() + + results = self.analysis_results + recommendations = { + 'should_simplify': False, + 'reasons': [], + 'simplification_level': 'none', # none, light, medium, heavy + 'target_polygon_count': results.get('polygon_count', 0) + } + + polygon_count = results.get('polygon_count', 0) + isolated_count = results.get('isolated_polygons', 0) + small_count = results.get('small_polygons', 0) + irregular_count = results.get('irregular_polygons', 0) + + # 检查是否需要简化 + if polygon_count > 5000: + recommendations['should_simplify'] = True + recommendations['reasons'].append('多边形数量过多') + + if isolated_count > polygon_count * 0.05: + recommendations['should_simplify'] = True + recommendations['reasons'].append('存在大量孤立多边形') + + if small_count > polygon_count * 0.1: + recommendations['should_simplify'] = True + recommendations['reasons'].append('存在大量极小多边形') + + if irregular_count > polygon_count * 0.2: + recommendations['should_simplify'] = True + recommendations['reasons'].append('存在大量不规则多边形') + + # 确定简化级别 + if recommendations['should_simplify']: + if polygon_count > 10000: + recommendations['simplification_level'] = 'heavy' + recommendations['target_polygon_count'] = int(polygon_count * 0.5) + elif polygon_count > 5000: + recommendations['simplification_level'] = 'medium' + recommendations['target_polygon_count'] = int(polygon_count * 0.7) + else: + recommendations['simplification_level'] = 'light' + recommendations['target_polygon_count'] = int(polygon_count * 0.8) + return recommendations \ No newline at end of file diff --git a/plugins/user/navigation_mesh/tools/navmesh_optimizer.py b/plugins/user/navigation_mesh/tools/navmesh_optimizer.py index 47a1df28..9d57b6f9 100644 --- a/plugins/user/navigation_mesh/tools/navmesh_optimizer.py +++ b/plugins/user/navigation_mesh/tools/navmesh_optimizer.py @@ -1,456 +1,456 @@ -""" -导航网格优化器 -提供自动优化导航网格的功能,包括多边形合并、简化和重构 -""" - -import math -from typing import List, Dict, Set, Tuple, Optional -from panda3d.core import Point3 - -class NavMeshOptimizer: - """ - 导航网格优化器 - 提供自动优化导航网格的功能 - """ - - def __init__(self, navmesh_manager): - """ - 初始化导航网格优化器 - - Args: - navmesh_manager: 导航网格管理器实例 - """ - self.navmesh_manager = navmesh_manager - self.optimization_settings = { - 'merge_threshold': 0.5, # 合并阈值 - 'simplify_threshold': 0.1, # 简化阈值 - 'min_polygon_area': 0.01, # 最小多边形面积 - 'max_vertices_per_polygon': 8, # 每个多边形最大顶点数 - 'preserve_boundaries': True, # 是否保留边界 - 'preserve_portals': True # 是否保留门户点 - } - - def set_optimization_settings(self, settings: Dict): - """ - 设置优化参数 - - Args: - settings: 优化参数字典 - """ - self.optimization_settings.update(settings) - - def get_optimization_settings(self) -> Dict: - """ - 获取优化参数 - - Returns: - 优化参数字典 - """ - return self.optimization_settings.copy() - - def merge_adjacent_polygons(self) -> int: - """ - 合并相邻的多边形 - - Returns: - 合并的多边形数量 - """ - if not self.navmesh_manager or not self.navmesh_manager.polygons: - return 0 - - merged_count = 0 - processed_polygons = set() - - # 创建多边形ID列表的副本,避免在迭代时修改字典 - polygon_ids = list(self.navmesh_manager.polygons.keys()) - - for polygon_id in polygon_ids: - if polygon_id in processed_polygons: - continue - - if polygon_id not in self.navmesh_manager.polygons: - continue - - polygon = self.navmesh_manager.polygons[polygon_id] - - # 检查是否可以与其他多边形合并 - for neighbor_id in polygon.neighbors[:]: # 使用副本避免修改时的问题 - if neighbor_id in processed_polygons: - continue - - if neighbor_id not in self.navmesh_manager.polygons: - continue - - neighbor = self.navmesh_manager.polygons[neighbor_id] - - # 检查合并条件 - if self._can_merge_polygons(polygon, neighbor): - # 执行合并 - if self._merge_two_polygons(polygon_id, neighbor_id): - merged_count += 1 - processed_polygons.add(polygon_id) - processed_polygons.add(neighbor_id) - break - - # 重新计算连接关系 - if merged_count > 0: - self.navmesh_manager._calculate_polygon_connections() - self.navmesh_manager._calculate_portal_points() - self.navmesh_manager._update_navmesh_visualization() - - return merged_count - - def _can_merge_polygons(self, polygon1, polygon2) -> bool: - """ - 检查两个多边形是否可以合并 - - Args: - polygon1: 第一个多边形 - polygon2: 第二个多边形 - - Returns: - 是否可以合并 - """ - # 检查是否有共享边 - if not self._polygons_share_full_edge(polygon1, polygon2): - return False - - # 检查合并后的多边形是否过于复杂 - merged_vertices = self._get_merged_vertices(polygon1, polygon2) - if len(merged_vertices) > self.optimization_settings['max_vertices_per_polygon']: - return False - - # 检查面积 - merged_area = self._calculate_polygon_area(merged_vertices) - if merged_area < self.optimization_settings['min_polygon_area']: - return False - - return True - - def _polygons_share_full_edge(self, polygon1, polygon2) -> bool: - """ - 检查两个多边形是否共享完整的一条边 - - Args: - polygon1: 第一个多边形 - polygon2: 第二个多边形 - - Returns: - 是否共享完整边 - """ - threshold = 0.01 # 点重合阈值 - - # 检查polygon1的每条边 - for i in range(len(polygon1.vertices)): - v1_start = polygon1.vertices[i] - v1_end = polygon1.vertices[(i + 1) % len(polygon1.vertices)] - - # 检查polygon2的每条边 - for j in range(len(polygon2.vertices)): - v2_start = polygon2.vertices[j] - v2_end = polygon2.vertices[(j + 1) % len(polygon2.vertices)] - - # 检查边是否重合(方向相同) - if (self._points_equal(v1_start, v2_start, threshold) and - self._points_equal(v1_end, v2_end, threshold)): - return True - - # 检查边是否重合(方向相反) - if (self._points_equal(v1_start, v2_end, threshold) and - self._points_equal(v1_end, v2_start, threshold)): - return True - - return False - - def _points_equal(self, p1: Point3, p2: Point3, threshold: float) -> bool: - """检查两点是否相等(在阈值范围内)""" - return (p1 - p2).length() < threshold - - def _get_merged_vertices(self, polygon1, polygon2) -> List[Point3]: - """ - 获取两个多边形合并后的顶点 - - Args: - polygon1: 第一个多边形 - polygon2: 第二个多边形 - - Returns: - 合并后的顶点列表 - """ - # 这是一个简化的实现 - # 实际应用中需要正确处理多边形合并的几何运算 - merged_vertices = [] - merged_vertices.extend(polygon1.vertices) - merged_vertices.extend(polygon2.vertices) - return merged_vertices - - def _calculate_polygon_area(self, vertices: List[Point3]) -> float: - """计算多边形面积(使用鞋带公式)""" - if len(vertices) < 3: - return 0.0 - - area = 0.0 - n = len(vertices) - for i in range(n): - j = (i + 1) % n - area += vertices[i].x * vertices[j].z - area -= vertices[j].x * vertices[i].z - return abs(area) / 2.0 - - def _merge_two_polygons(self, polygon1_id: int, polygon2_id: int) -> bool: - """ - 合并两个多边形 - - Args: - polygon1_id: 第一个多边形ID - polygon2_id: 第二个多边形ID - - Returns: - 是否合并成功 - """ - try: - if (polygon1_id not in self.navmesh_manager.polygons or - polygon2_id not in self.navmesh_manager.polygons): - return False - - polygon1 = self.navmesh_manager.polygons[polygon1_id] - polygon2 = self.navmesh_manager.polygons[polygon2_id] - - # 创建合并后的新多边形 - merged_vertices = self._merge_polygon_vertices(polygon1, polygon2) - if not merged_vertices: - return False - - # 移除旧的多边形 - self.navmesh_manager.remove_polygon(polygon2_id) - - # 更新第一个多边形 - polygon1.vertices = merged_vertices - polygon1.center = polygon1._calculate_center() - polygon1.area = polygon1._calculate_area() - polygon1.bounding_box = polygon1._calculate_bounding_box() - - return True - - except Exception as e: - print(f"合并多边形失败: {e}") - return False - - def _merge_polygon_vertices(self, polygon1, polygon2) -> List[Point3]: - """ - 合并两个多边形的顶点 - - Args: - polygon1: 第一个多边形 - polygon2: 第二个多边形 - - Returns: - 合并后的顶点列表 - """ - # 这是一个简化的实现 - # 实际应用中需要正确处理多边形合并的几何运算 - # 包括移除共享边,正确排序顶点等 - - # 简单合并所有顶点 - merged_vertices = [] - merged_vertices.extend(polygon1.vertices) - merged_vertices.extend(polygon2.vertices) - - # 去除重复顶点(简化处理) - unique_vertices = [] - for vertex in merged_vertices: - is_duplicate = False - for existing_vertex in unique_vertices: - if self._points_equal(vertex, existing_vertex, 0.01): - is_duplicate = True - break - if not is_duplicate: - unique_vertices.append(vertex) - - return unique_vertices[:self.optimization_settings['max_vertices_per_polygon']] - - def simplify_polygons(self) -> int: - """ - 简化多边形(移除不必要的顶点) - - Returns: - 简化的顶点数量 - """ - if not self.navmesh_manager or not self.navmesh_manager.polygons: - return 0 - - simplified_count = 0 - - for polygon in self.navmesh_manager.polygons.values(): - original_vertex_count = len(polygon.vertices) - simplified_vertices = self._simplify_polygon(polygon.vertices) - - if len(simplified_vertices) < original_vertex_count: - polygon.vertices = simplified_vertices - polygon.center = polygon._calculate_center() - polygon.area = polygon._calculate_area() - polygon.bounding_box = polygon._calculate_bounding_box() - simplified_count += original_vertex_count - len(simplified_vertices) - - # 重新计算连接关系 - if simplified_count > 0: - self.navmesh_manager._calculate_polygon_connections() - self.navmesh_manager._calculate_portal_points() - self.navmesh_manager._update_navmesh_visualization() - - return simplified_count - - def _simplify_polygon(self, vertices: List[Point3]) -> List[Point3]: - """ - 简化单个多边形 - - Args: - vertices: 多边形顶点列表 - - Returns: - 简化后的顶点列表 - """ - if len(vertices) <= 3: - return vertices - - simplified = [vertices[0]] # 保留第一个顶点 - i = 0 - - while i < len(vertices): - # 找到下一个需要保留的顶点 - next_i = self._find_next_significant_vertex(vertices, i) - if next_i >= len(vertices): - break - - simplified.append(vertices[next_i]) - i = next_i - - # 确保至少有3个顶点 - if len(simplified) < 3: - return vertices[:3] - - return simplified - - def _find_next_significant_vertex(self, vertices: List[Point3], start_index: int) -> int: - """ - 找到下一个重要的顶点(需要保留的顶点) - - Args: - vertices: 顶点列表 - start_index: 起始索引 - - Returns: - 下一个重要顶点的索引 - """ - threshold = self.optimization_settings['simplify_threshold'] - - # 简化处理:每隔几个顶点保留一个 - step = max(1, int(len(vertices) / 8)) # 保持最多8个顶点 - next_index = min(start_index + step, len(vertices) - 1) - - return next_index - - def remove_small_polygons(self) -> int: - """ - 移除极小的多边形 - - Returns: - 移除的多边形数量 - """ - if not self.navmesh_manager or not self.navmesh_manager.polygons: - return 0 - - min_area = self.optimization_settings['min_polygon_area'] - removed_count = 0 - - # 创建需要移除的多边形ID列表 - to_remove = [] - for polygon_id, polygon in self.navmesh_manager.polygons.items(): - if polygon.area < min_area: - to_remove.append(polygon_id) - - # 移除多边形 - for polygon_id in to_remove: - self.navmesh_manager.remove_polygon(polygon_id) - removed_count += 1 - - # 更新连接关系 - if removed_count > 0: - self.navmesh_manager._calculate_polygon_connections() - self.navmesh_manager._calculate_portal_points() - self.navmesh_manager._update_navmesh_visualization() - - return removed_count - - def optimize_mesh(self, optimization_level: str = 'medium') -> Dict: - """ - 执行完整的网格优化 - - Args: - optimization_level: 优化级别 ('light', 'medium', 'heavy') - - Returns: - 优化结果统计 - """ - results = { - 'merged_polygons': 0, - 'simplified_vertices': 0, - 'removed_polygons': 0, - 'total_optimizations': 0 - } - - # 根据优化级别设置参数 - if optimization_level == 'heavy': - self.optimization_settings['merge_threshold'] = 0.3 - self.optimization_settings['simplify_threshold'] = 0.2 - self.optimization_settings['min_polygon_area'] = 0.05 - elif optimization_level == 'light': - self.optimization_settings['merge_threshold'] = 0.7 - self.optimization_settings['simplify_threshold'] = 0.05 - self.optimization_settings['min_polygon_area'] = 0.005 - - # 执行优化步骤 - if optimization_level in ['medium', 'heavy']: - results['merged_polygons'] = self.merge_adjacent_polygons() - - results['simplified_vertices'] = self.simplify_polygons() - results['removed_polygons'] = self.remove_small_polygons() - - results['total_optimizations'] = (results['merged_polygons'] + - results['simplified_vertices'] + - results['removed_polygons']) - - return results - - def fill_gaps(self) -> int: - """ - 填补导航网格中的空隙 - - Returns: - 填补的多边形数量 - """ - # 这是一个复杂的功能,需要分析未覆盖的区域并生成新的多边形 - # 简化实现:返回0 - return 0 - - def smooth_boundaries(self) -> int: - """ - 平滑多边形边界 - - Returns: - 平滑处理的边数 - """ - # 这需要复杂的几何运算来平滑多边形边界 - # 简化实现:返回0 - return 0 - - def balance_polygon_sizes(self) -> int: - """ - 平衡多边形大小 - - Returns: - 调整的多边形数量 - """ - # 这需要分析多边形大小分布并调整过大或过小的多边形 - # 简化实现:返回0 +""" +导航网格优化器 +提供自动优化导航网格的功能,包括多边形合并、简化和重构 +""" + +import math +from typing import List, Dict, Set, Tuple, Optional +from panda3d.core import Point3 + +class NavMeshOptimizer: + """ + 导航网格优化器 + 提供自动优化导航网格的功能 + """ + + def __init__(self, navmesh_manager): + """ + 初始化导航网格优化器 + + Args: + navmesh_manager: 导航网格管理器实例 + """ + self.navmesh_manager = navmesh_manager + self.optimization_settings = { + 'merge_threshold': 0.5, # 合并阈值 + 'simplify_threshold': 0.1, # 简化阈值 + 'min_polygon_area': 0.01, # 最小多边形面积 + 'max_vertices_per_polygon': 8, # 每个多边形最大顶点数 + 'preserve_boundaries': True, # 是否保留边界 + 'preserve_portals': True # 是否保留门户点 + } + + def set_optimization_settings(self, settings: Dict): + """ + 设置优化参数 + + Args: + settings: 优化参数字典 + """ + self.optimization_settings.update(settings) + + def get_optimization_settings(self) -> Dict: + """ + 获取优化参数 + + Returns: + 优化参数字典 + """ + return self.optimization_settings.copy() + + def merge_adjacent_polygons(self) -> int: + """ + 合并相邻的多边形 + + Returns: + 合并的多边形数量 + """ + if not self.navmesh_manager or not self.navmesh_manager.polygons: + return 0 + + merged_count = 0 + processed_polygons = set() + + # 创建多边形ID列表的副本,避免在迭代时修改字典 + polygon_ids = list(self.navmesh_manager.polygons.keys()) + + for polygon_id in polygon_ids: + if polygon_id in processed_polygons: + continue + + if polygon_id not in self.navmesh_manager.polygons: + continue + + polygon = self.navmesh_manager.polygons[polygon_id] + + # 检查是否可以与其他多边形合并 + for neighbor_id in polygon.neighbors[:]: # 使用副本避免修改时的问题 + if neighbor_id in processed_polygons: + continue + + if neighbor_id not in self.navmesh_manager.polygons: + continue + + neighbor = self.navmesh_manager.polygons[neighbor_id] + + # 检查合并条件 + if self._can_merge_polygons(polygon, neighbor): + # 执行合并 + if self._merge_two_polygons(polygon_id, neighbor_id): + merged_count += 1 + processed_polygons.add(polygon_id) + processed_polygons.add(neighbor_id) + break + + # 重新计算连接关系 + if merged_count > 0: + self.navmesh_manager._calculate_polygon_connections() + self.navmesh_manager._calculate_portal_points() + self.navmesh_manager._update_navmesh_visualization() + + return merged_count + + def _can_merge_polygons(self, polygon1, polygon2) -> bool: + """ + 检查两个多边形是否可以合并 + + Args: + polygon1: 第一个多边形 + polygon2: 第二个多边形 + + Returns: + 是否可以合并 + """ + # 检查是否有共享边 + if not self._polygons_share_full_edge(polygon1, polygon2): + return False + + # 检查合并后的多边形是否过于复杂 + merged_vertices = self._get_merged_vertices(polygon1, polygon2) + if len(merged_vertices) > self.optimization_settings['max_vertices_per_polygon']: + return False + + # 检查面积 + merged_area = self._calculate_polygon_area(merged_vertices) + if merged_area < self.optimization_settings['min_polygon_area']: + return False + + return True + + def _polygons_share_full_edge(self, polygon1, polygon2) -> bool: + """ + 检查两个多边形是否共享完整的一条边 + + Args: + polygon1: 第一个多边形 + polygon2: 第二个多边形 + + Returns: + 是否共享完整边 + """ + threshold = 0.01 # 点重合阈值 + + # 检查polygon1的每条边 + for i in range(len(polygon1.vertices)): + v1_start = polygon1.vertices[i] + v1_end = polygon1.vertices[(i + 1) % len(polygon1.vertices)] + + # 检查polygon2的每条边 + for j in range(len(polygon2.vertices)): + v2_start = polygon2.vertices[j] + v2_end = polygon2.vertices[(j + 1) % len(polygon2.vertices)] + + # 检查边是否重合(方向相同) + if (self._points_equal(v1_start, v2_start, threshold) and + self._points_equal(v1_end, v2_end, threshold)): + return True + + # 检查边是否重合(方向相反) + if (self._points_equal(v1_start, v2_end, threshold) and + self._points_equal(v1_end, v2_start, threshold)): + return True + + return False + + def _points_equal(self, p1: Point3, p2: Point3, threshold: float) -> bool: + """检查两点是否相等(在阈值范围内)""" + return (p1 - p2).length() < threshold + + def _get_merged_vertices(self, polygon1, polygon2) -> List[Point3]: + """ + 获取两个多边形合并后的顶点 + + Args: + polygon1: 第一个多边形 + polygon2: 第二个多边形 + + Returns: + 合并后的顶点列表 + """ + # 这是一个简化的实现 + # 实际应用中需要正确处理多边形合并的几何运算 + merged_vertices = [] + merged_vertices.extend(polygon1.vertices) + merged_vertices.extend(polygon2.vertices) + return merged_vertices + + def _calculate_polygon_area(self, vertices: List[Point3]) -> float: + """计算多边形面积(使用鞋带公式)""" + if len(vertices) < 3: + return 0.0 + + area = 0.0 + n = len(vertices) + for i in range(n): + j = (i + 1) % n + area += vertices[i].x * vertices[j].z + area -= vertices[j].x * vertices[i].z + return abs(area) / 2.0 + + def _merge_two_polygons(self, polygon1_id: int, polygon2_id: int) -> bool: + """ + 合并两个多边形 + + Args: + polygon1_id: 第一个多边形ID + polygon2_id: 第二个多边形ID + + Returns: + 是否合并成功 + """ + try: + if (polygon1_id not in self.navmesh_manager.polygons or + polygon2_id not in self.navmesh_manager.polygons): + return False + + polygon1 = self.navmesh_manager.polygons[polygon1_id] + polygon2 = self.navmesh_manager.polygons[polygon2_id] + + # 创建合并后的新多边形 + merged_vertices = self._merge_polygon_vertices(polygon1, polygon2) + if not merged_vertices: + return False + + # 移除旧的多边形 + self.navmesh_manager.remove_polygon(polygon2_id) + + # 更新第一个多边形 + polygon1.vertices = merged_vertices + polygon1.center = polygon1._calculate_center() + polygon1.area = polygon1._calculate_area() + polygon1.bounding_box = polygon1._calculate_bounding_box() + + return True + + except Exception as e: + print(f"合并多边形失败: {e}") + return False + + def _merge_polygon_vertices(self, polygon1, polygon2) -> List[Point3]: + """ + 合并两个多边形的顶点 + + Args: + polygon1: 第一个多边形 + polygon2: 第二个多边形 + + Returns: + 合并后的顶点列表 + """ + # 这是一个简化的实现 + # 实际应用中需要正确处理多边形合并的几何运算 + # 包括移除共享边,正确排序顶点等 + + # 简单合并所有顶点 + merged_vertices = [] + merged_vertices.extend(polygon1.vertices) + merged_vertices.extend(polygon2.vertices) + + # 去除重复顶点(简化处理) + unique_vertices = [] + for vertex in merged_vertices: + is_duplicate = False + for existing_vertex in unique_vertices: + if self._points_equal(vertex, existing_vertex, 0.01): + is_duplicate = True + break + if not is_duplicate: + unique_vertices.append(vertex) + + return unique_vertices[:self.optimization_settings['max_vertices_per_polygon']] + + def simplify_polygons(self) -> int: + """ + 简化多边形(移除不必要的顶点) + + Returns: + 简化的顶点数量 + """ + if not self.navmesh_manager or not self.navmesh_manager.polygons: + return 0 + + simplified_count = 0 + + for polygon in self.navmesh_manager.polygons.values(): + original_vertex_count = len(polygon.vertices) + simplified_vertices = self._simplify_polygon(polygon.vertices) + + if len(simplified_vertices) < original_vertex_count: + polygon.vertices = simplified_vertices + polygon.center = polygon._calculate_center() + polygon.area = polygon._calculate_area() + polygon.bounding_box = polygon._calculate_bounding_box() + simplified_count += original_vertex_count - len(simplified_vertices) + + # 重新计算连接关系 + if simplified_count > 0: + self.navmesh_manager._calculate_polygon_connections() + self.navmesh_manager._calculate_portal_points() + self.navmesh_manager._update_navmesh_visualization() + + return simplified_count + + def _simplify_polygon(self, vertices: List[Point3]) -> List[Point3]: + """ + 简化单个多边形 + + Args: + vertices: 多边形顶点列表 + + Returns: + 简化后的顶点列表 + """ + if len(vertices) <= 3: + return vertices + + simplified = [vertices[0]] # 保留第一个顶点 + i = 0 + + while i < len(vertices): + # 找到下一个需要保留的顶点 + next_i = self._find_next_significant_vertex(vertices, i) + if next_i >= len(vertices): + break + + simplified.append(vertices[next_i]) + i = next_i + + # 确保至少有3个顶点 + if len(simplified) < 3: + return vertices[:3] + + return simplified + + def _find_next_significant_vertex(self, vertices: List[Point3], start_index: int) -> int: + """ + 找到下一个重要的顶点(需要保留的顶点) + + Args: + vertices: 顶点列表 + start_index: 起始索引 + + Returns: + 下一个重要顶点的索引 + """ + threshold = self.optimization_settings['simplify_threshold'] + + # 简化处理:每隔几个顶点保留一个 + step = max(1, int(len(vertices) / 8)) # 保持最多8个顶点 + next_index = min(start_index + step, len(vertices) - 1) + + return next_index + + def remove_small_polygons(self) -> int: + """ + 移除极小的多边形 + + Returns: + 移除的多边形数量 + """ + if not self.navmesh_manager or not self.navmesh_manager.polygons: + return 0 + + min_area = self.optimization_settings['min_polygon_area'] + removed_count = 0 + + # 创建需要移除的多边形ID列表 + to_remove = [] + for polygon_id, polygon in self.navmesh_manager.polygons.items(): + if polygon.area < min_area: + to_remove.append(polygon_id) + + # 移除多边形 + for polygon_id in to_remove: + self.navmesh_manager.remove_polygon(polygon_id) + removed_count += 1 + + # 更新连接关系 + if removed_count > 0: + self.navmesh_manager._calculate_polygon_connections() + self.navmesh_manager._calculate_portal_points() + self.navmesh_manager._update_navmesh_visualization() + + return removed_count + + def optimize_mesh(self, optimization_level: str = 'medium') -> Dict: + """ + 执行完整的网格优化 + + Args: + optimization_level: 优化级别 ('light', 'medium', 'heavy') + + Returns: + 优化结果统计 + """ + results = { + 'merged_polygons': 0, + 'simplified_vertices': 0, + 'removed_polygons': 0, + 'total_optimizations': 0 + } + + # 根据优化级别设置参数 + if optimization_level == 'heavy': + self.optimization_settings['merge_threshold'] = 0.3 + self.optimization_settings['simplify_threshold'] = 0.2 + self.optimization_settings['min_polygon_area'] = 0.05 + elif optimization_level == 'light': + self.optimization_settings['merge_threshold'] = 0.7 + self.optimization_settings['simplify_threshold'] = 0.05 + self.optimization_settings['min_polygon_area'] = 0.005 + + # 执行优化步骤 + if optimization_level in ['medium', 'heavy']: + results['merged_polygons'] = self.merge_adjacent_polygons() + + results['simplified_vertices'] = self.simplify_polygons() + results['removed_polygons'] = self.remove_small_polygons() + + results['total_optimizations'] = (results['merged_polygons'] + + results['simplified_vertices'] + + results['removed_polygons']) + + return results + + def fill_gaps(self) -> int: + """ + 填补导航网格中的空隙 + + Returns: + 填补的多边形数量 + """ + # 这是一个复杂的功能,需要分析未覆盖的区域并生成新的多边形 + # 简化实现:返回0 + return 0 + + def smooth_boundaries(self) -> int: + """ + 平滑多边形边界 + + Returns: + 平滑处理的边数 + """ + # 这需要复杂的几何运算来平滑多边形边界 + # 简化实现:返回0 + return 0 + + def balance_polygon_sizes(self) -> int: + """ + 平衡多边形大小 + + Returns: + 调整的多边形数量 + """ + # 这需要分析多边形大小分布并调整过大或过小的多边形 + # 简化实现:返回0 return 0 \ No newline at end of file diff --git a/plugins/user/navigation_mesh/tools/navmesh_tools.py b/plugins/user/navigation_mesh/tools/navmesh_tools.py index c01a39ff..c3ede0de 100644 --- a/plugins/user/navigation_mesh/tools/navmesh_tools.py +++ b/plugins/user/navigation_mesh/tools/navmesh_tools.py @@ -1,531 +1,531 @@ -""" -导航网格工具集 -整合导航网格的各种工具功能 -""" - -from typing import Dict, Any, List, Optional -from panda3d.core import Point3 - -class NavMeshTools: - """ - 导航网格工具集 - 整合导航网格的各种工具功能 - """ - - def __init__(self, plugin): - """ - 初始化工具集 - - Args: - plugin: 导航网格插件实例 - """ - self.plugin = plugin - self.tools = {} - - # 初始化各个工具 - self._initialize_tools() - - def _initialize_tools(self): - """初始化各个工具""" - try: - from .navmesh_analyzer import NavMeshAnalyzer - from .navmesh_optimizer import NavMeshOptimizer - from ..utils.navmesh_serializer import NavMeshSerializer - - # 初始化分析器 - self.tools['analyzer'] = NavMeshAnalyzer(self.plugin.navmesh_manager) - - # 初始化优化器 - self.tools['optimizer'] = NavMeshOptimizer(self.plugin.navmesh_manager) - - # 初始化序列化器 - self.tools['serializer'] = NavMeshSerializer() - - except Exception as e: - print(f"初始化工具失败: {e}") - - def analyze_mesh_quality(self) -> Dict: - """ - 分析导航网格质量 - - Returns: - 质量分析结果 - """ - if 'analyzer' in self.tools: - return self.tools['analyzer'].analyze_mesh_quality() - return {} - - def suggest_optimizations(self) -> List[Dict]: - """ - 提供优化建议 - - Returns: - 优化建议列表 - """ - if 'analyzer' in self.tools: - return self.tools['analyzer'].suggest_optimizations() - return [] - - def analyze_pathfinding_performance(self, sample_count: int = 100) -> Dict: - """ - 分析寻路性能 - - Args: - sample_count: 采样次数 - - Returns: - 性能分析结果 - """ - if 'analyzer' in self.tools: - return self.tools['analyzer'].analyze_pathfinding_performance(sample_count) - return {} - - def detect_bottlenecks(self) -> List[Dict]: - """ - 检测导航网格中的瓶颈区域 - - Returns: - 瓶颈区域列表 - """ - if 'analyzer' in self.tools: - return self.tools['analyzer'].detect_bottlenecks() - return [] - - def analyze_coverage(self) -> Dict: - """ - 分析导航网格覆盖情况 - - Returns: - 覆盖情况分析结果 - """ - if 'analyzer' in self.tools: - return self.tools['analyzer'].analyze_coverage() - return {} - - def generate_report(self) -> Dict: - """ - 生成完整的分析报告 - - Returns: - 分析报告 - """ - if 'analyzer' in self.tools: - return self.tools['analyzer'].generate_report() - return {} - - def export_report(self, filepath: str) -> bool: - """ - 导出分析报告到文件 - - Args: - filepath: 导出文件路径 - - Returns: - 是否导出成功 - """ - if 'analyzer' in self.tools: - return self.tools['analyzer'].export_report(filepath) - return False - - def get_complexity_score(self) -> float: - """ - 计算导航网格复杂度评分 - - Returns: - 复杂度评分 (0-100) - """ - if 'analyzer' in self.tools: - return self.tools['analyzer'].get_complexity_score() - return 0.0 - - def recommend_simplification(self) -> Dict: - """ - 推荐网格简化策略 - - Returns: - 简化策略建议 - """ - if 'analyzer' in self.tools: - return self.tools['analyzer'].recommend_simplification() - return {} - - def merge_adjacent_polygons(self) -> int: - """ - 合并相邻的多边形 - - Returns: - 合并的多边形数量 - """ - if 'optimizer' in self.tools: - return self.tools['optimizer'].merge_adjacent_polygons() - return 0 - - def simplify_polygons(self) -> int: - """ - 简化多边形(移除不必要的顶点) - - Returns: - 简化的顶点数量 - """ - if 'optimizer' in self.tools: - return self.tools['optimizer'].simplify_polygons() - return 0 - - def remove_small_polygons(self) -> int: - """ - 移除极小的多边形 - - Returns: - 移除的多边形数量 - """ - if 'optimizer' in self.tools: - return self.tools['optimizer'].remove_small_polygons() - return 0 - - def optimize_mesh(self, optimization_level: str = 'medium') -> Dict: - """ - 执行完整的网格优化 - - Args: - optimization_level: 优化级别 ('light', 'medium', 'heavy') - - Returns: - 优化结果统计 - """ - if 'optimizer' in self.tools: - return self.tools['optimizer'].optimize_mesh(optimization_level) - return {} - - def set_optimization_settings(self, settings: Dict): - """ - 设置优化参数 - - Args: - settings: 优化参数字典 - """ - if 'optimizer' in self.tools: - self.tools['optimizer'].set_optimization_settings(settings) - - def get_optimization_settings(self) -> Dict: - """ - 获取优化参数 - - Returns: - 优化参数字典 - """ - if 'optimizer' in self.tools: - return self.tools['optimizer'].get_optimization_settings() - return {} - - def fill_gaps(self) -> int: - """ - 填补导航网格中的空隙 - - Returns: - 填补的多边形数量 - """ - if 'optimizer' in self.tools: - return self.tools['optimizer'].fill_gaps() - return 0 - - def smooth_boundaries(self) -> int: - """ - 平滑多边形边界 - - Returns: - 平滑处理的边数 - """ - if 'optimizer' in self.tools: - return self.tools['optimizer'].smooth_boundaries() - return 0 - - def balance_polygon_sizes(self) -> int: - """ - 平衡多边形大小 - - Returns: - 调整的多边形数量 - """ - if 'optimizer' in self.tools: - return self.tools['optimizer'].balance_polygon_sizes() - return 0 - - def serialize_navmesh(self) -> str: - """ - 序列化导航网格为字符串 - - Returns: - 序列化后的字符串 - """ - if 'serializer' in self.tools and self.plugin.navmesh_manager: - return self.tools['serializer'].serialize_navmesh(self.plugin.navmesh_manager) - return "" - - def deserialize_navmesh(self, data_str: str) -> bool: - """ - 从字符串反序列化导航网格 - - Args: - data_str: 序列化数据字符串 - - Returns: - 是否反序列化成功 - """ - if 'serializer' in self.tools and self.plugin.navmesh_manager: - return self.tools['serializer'].deserialize_navmesh(data_str, self.plugin.navmesh_manager) - return False - - def save_navmesh_to_file(self, filepath: str, - compress: bool = False, pretty_print: bool = False) -> bool: - """ - 保存导航网格到文件 - - Args: - filepath: 文件路径 - compress: 是否压缩 - pretty_print: 是否美化输出 - - Returns: - 是否保存成功 - """ - if 'serializer' in self.tools and self.plugin.navmesh_manager: - return self.tools['serializer'].save_navmesh_to_file( - self.plugin.navmesh_manager, filepath, compress, pretty_print) - return False - - def load_navmesh_from_file(self, filepath: str) -> bool: - """ - 从文件加载导航网格 - - Args: - filepath: 文件路径 - - Returns: - 是否加载成功 - """ - if 'serializer' in self.tools and self.plugin.navmesh_manager: - return self.tools['serializer'].load_navmesh_from_file(filepath, self.plugin.navmesh_manager) - return False - - def export_to_standard_format(self, filepath: str, - format_type: str = 'obj') -> bool: - """ - 导出为标准格式 - - Args: - filepath: 导出文件路径 - format_type: 导出格式 ('obj', 'ply') - - Returns: - 是否导出成功 - """ - if 'serializer' in self.tools and self.plugin.navmesh_manager: - return self.tools['serializer'].export_to_standard_format( - self.plugin.navmesh_manager, filepath, format_type) - return False - - def import_from_standard_format(self, filepath: str, - format_type: str = 'obj') -> bool: - """ - 从标准格式导入 - - Args: - filepath: 导入文件路径 - format_type: 导入格式 ('obj', 'ply') - - Returns: - 是否导入成功 - """ - if 'serializer' in self.tools and self.plugin.navmesh_manager: - return self.tools['serializer'].import_from_standard_format( - filepath, self.plugin.navmesh_manager, format_type) - return False - - def get_file_info(self, filepath: str) -> Dict[str, Any]: - """ - 获取导航网格文件信息 - - Args: - filepath: 文件路径 - - Returns: - 文件信息字典 - """ - if 'serializer' in self.tools: - return self.tools['serializer'].get_file_info(filepath) - return {} - - def batch_analyze(self, filepaths: List[str]) -> Dict[str, Dict]: - """ - 批量分析多个导航网格文件 - - Args: - filepaths: 文件路径列表 - - Returns: - 各文件的分析结果 - """ - results = {} - - for filepath in filepaths: - try: - info = self.get_file_info(filepath) - results[filepath] = info - except Exception as e: - results[filepath] = {'error': str(e)} - - return results - - def compare_navmeshes(self, navmesh1_path: str, navmesh2_path: str) -> Dict: - """ - 比较两个导航网格 - - Args: - navmesh1_path: 第一个导航网格文件路径 - navmesh2_path: 第二个导航网格文件路径 - - Returns: - 比较结果 - """ - try: - info1 = self.get_file_info(navmesh1_path) - info2 = self.get_file_info(navmesh2_path) - - comparison = { - 'file1': info1, - 'file2': info2, - 'differences': {} - } - - # 比较关键指标 - for key in ['polygon_count', 'obstacle_count', 'file_size']: - val1 = info1.get(key, 0) - val2 = info2.get(key, 0) - if val1 != val2: - comparison['differences'][key] = { - 'file1': val1, - 'file2': val2, - 'difference': val2 - val1, - 'percentage': ((val2 - val1) / val1 * 100) if val1 != 0 else float('inf') - } - - return comparison - - except Exception as e: - return {'error': str(e)} - - def validate_navmesh_file(self, filepath: str) -> Dict: - """ - 验证导航网格文件的有效性 - - Args: - filepath: 文件路径 - - Returns: - 验证结果 - """ - try: - info = self.get_file_info(filepath) - - validation = { - 'filepath': filepath, - 'valid': True, - 'issues': [], - 'info': info - } - - # 检查基本信息 - if info.get('file_size', 0) == 0: - validation['valid'] = False - validation['issues'].append('文件为空') - - if info.get('version') == 'unknown': - validation['issues'].append('无法识别文件版本') - - # 检查多边形数量 - polygon_count = info.get('polygon_count', 0) - if polygon_count == 0: - validation['issues'].append('导航网格不包含多边形') - elif polygon_count > 100000: - validation['issues'].append('多边形数量过多,可能影响性能') - - return validation - - except Exception as e: - return { - 'filepath': filepath, - 'valid': False, - 'issues': [str(e)], - 'error': True - } - - def optimize_and_save(self, input_filepath: str, output_filepath: str, - optimization_level: str = 'medium') -> Dict: - """ - 优化并保存导航网格 - - Args: - input_filepath: 输入文件路径 - output_filepath: 输出文件路径 - optimization_level: 优化级别 - - Returns: - 操作结果 - """ - try: - # 加载导航网格 - if not self.load_navmesh_from_file(input_filepath): - return {'success': False, 'error': '无法加载输入文件'} - - # 执行优化 - optimization_result = self.optimize_mesh(optimization_level) - - # 保存优化后的网格 - if not self.save_navmesh_to_file(output_filepath): - return {'success': False, 'error': '无法保存优化后的文件'} - - return { - 'success': True, - 'optimization_result': optimization_result, - 'input_file': input_filepath, - 'output_file': output_filepath - } - - except Exception as e: - return {'success': False, 'error': str(e)} - - def create_optimization_pipeline(self, steps: List[str]) -> Dict: - """ - 创建优化流水线 - - Args: - steps: 优化步骤列表 - - Returns: - 流水线执行结果 - """ - results = { - 'steps': steps, - 'results': [], - 'total_improvement': 0 - } - - for step in steps: - try: - if step == 'merge': - result = {'step': 'merge', 'polygons_merged': self.merge_adjacent_polygons()} - elif step == 'simplify': - result = {'step': 'simplify', 'vertices_simplified': self.simplify_polygons()} - elif step == 'remove_small': - result = {'step': 'remove_small', 'polygons_removed': self.remove_small_polygons()} - elif step == 'optimize': - result = {'step': 'optimize', 'optimization_result': self.optimize_mesh()} - else: - result = {'step': step, 'error': '未知的优化步骤'} - - results['results'].append(result) - - except Exception as e: - results['results'].append({'step': step, 'error': str(e)}) - +""" +导航网格工具集 +整合导航网格的各种工具功能 +""" + +from typing import Dict, Any, List, Optional +from panda3d.core import Point3 + +class NavMeshTools: + """ + 导航网格工具集 + 整合导航网格的各种工具功能 + """ + + def __init__(self, plugin): + """ + 初始化工具集 + + Args: + plugin: 导航网格插件实例 + """ + self.plugin = plugin + self.tools = {} + + # 初始化各个工具 + self._initialize_tools() + + def _initialize_tools(self): + """初始化各个工具""" + try: + from .navmesh_analyzer import NavMeshAnalyzer + from .navmesh_optimizer import NavMeshOptimizer + from ..utils.navmesh_serializer import NavMeshSerializer + + # 初始化分析器 + self.tools['analyzer'] = NavMeshAnalyzer(self.plugin.navmesh_manager) + + # 初始化优化器 + self.tools['optimizer'] = NavMeshOptimizer(self.plugin.navmesh_manager) + + # 初始化序列化器 + self.tools['serializer'] = NavMeshSerializer() + + except Exception as e: + print(f"初始化工具失败: {e}") + + def analyze_mesh_quality(self) -> Dict: + """ + 分析导航网格质量 + + Returns: + 质量分析结果 + """ + if 'analyzer' in self.tools: + return self.tools['analyzer'].analyze_mesh_quality() + return {} + + def suggest_optimizations(self) -> List[Dict]: + """ + 提供优化建议 + + Returns: + 优化建议列表 + """ + if 'analyzer' in self.tools: + return self.tools['analyzer'].suggest_optimizations() + return [] + + def analyze_pathfinding_performance(self, sample_count: int = 100) -> Dict: + """ + 分析寻路性能 + + Args: + sample_count: 采样次数 + + Returns: + 性能分析结果 + """ + if 'analyzer' in self.tools: + return self.tools['analyzer'].analyze_pathfinding_performance(sample_count) + return {} + + def detect_bottlenecks(self) -> List[Dict]: + """ + 检测导航网格中的瓶颈区域 + + Returns: + 瓶颈区域列表 + """ + if 'analyzer' in self.tools: + return self.tools['analyzer'].detect_bottlenecks() + return [] + + def analyze_coverage(self) -> Dict: + """ + 分析导航网格覆盖情况 + + Returns: + 覆盖情况分析结果 + """ + if 'analyzer' in self.tools: + return self.tools['analyzer'].analyze_coverage() + return {} + + def generate_report(self) -> Dict: + """ + 生成完整的分析报告 + + Returns: + 分析报告 + """ + if 'analyzer' in self.tools: + return self.tools['analyzer'].generate_report() + return {} + + def export_report(self, filepath: str) -> bool: + """ + 导出分析报告到文件 + + Args: + filepath: 导出文件路径 + + Returns: + 是否导出成功 + """ + if 'analyzer' in self.tools: + return self.tools['analyzer'].export_report(filepath) + return False + + def get_complexity_score(self) -> float: + """ + 计算导航网格复杂度评分 + + Returns: + 复杂度评分 (0-100) + """ + if 'analyzer' in self.tools: + return self.tools['analyzer'].get_complexity_score() + return 0.0 + + def recommend_simplification(self) -> Dict: + """ + 推荐网格简化策略 + + Returns: + 简化策略建议 + """ + if 'analyzer' in self.tools: + return self.tools['analyzer'].recommend_simplification() + return {} + + def merge_adjacent_polygons(self) -> int: + """ + 合并相邻的多边形 + + Returns: + 合并的多边形数量 + """ + if 'optimizer' in self.tools: + return self.tools['optimizer'].merge_adjacent_polygons() + return 0 + + def simplify_polygons(self) -> int: + """ + 简化多边形(移除不必要的顶点) + + Returns: + 简化的顶点数量 + """ + if 'optimizer' in self.tools: + return self.tools['optimizer'].simplify_polygons() + return 0 + + def remove_small_polygons(self) -> int: + """ + 移除极小的多边形 + + Returns: + 移除的多边形数量 + """ + if 'optimizer' in self.tools: + return self.tools['optimizer'].remove_small_polygons() + return 0 + + def optimize_mesh(self, optimization_level: str = 'medium') -> Dict: + """ + 执行完整的网格优化 + + Args: + optimization_level: 优化级别 ('light', 'medium', 'heavy') + + Returns: + 优化结果统计 + """ + if 'optimizer' in self.tools: + return self.tools['optimizer'].optimize_mesh(optimization_level) + return {} + + def set_optimization_settings(self, settings: Dict): + """ + 设置优化参数 + + Args: + settings: 优化参数字典 + """ + if 'optimizer' in self.tools: + self.tools['optimizer'].set_optimization_settings(settings) + + def get_optimization_settings(self) -> Dict: + """ + 获取优化参数 + + Returns: + 优化参数字典 + """ + if 'optimizer' in self.tools: + return self.tools['optimizer'].get_optimization_settings() + return {} + + def fill_gaps(self) -> int: + """ + 填补导航网格中的空隙 + + Returns: + 填补的多边形数量 + """ + if 'optimizer' in self.tools: + return self.tools['optimizer'].fill_gaps() + return 0 + + def smooth_boundaries(self) -> int: + """ + 平滑多边形边界 + + Returns: + 平滑处理的边数 + """ + if 'optimizer' in self.tools: + return self.tools['optimizer'].smooth_boundaries() + return 0 + + def balance_polygon_sizes(self) -> int: + """ + 平衡多边形大小 + + Returns: + 调整的多边形数量 + """ + if 'optimizer' in self.tools: + return self.tools['optimizer'].balance_polygon_sizes() + return 0 + + def serialize_navmesh(self) -> str: + """ + 序列化导航网格为字符串 + + Returns: + 序列化后的字符串 + """ + if 'serializer' in self.tools and self.plugin.navmesh_manager: + return self.tools['serializer'].serialize_navmesh(self.plugin.navmesh_manager) + return "" + + def deserialize_navmesh(self, data_str: str) -> bool: + """ + 从字符串反序列化导航网格 + + Args: + data_str: 序列化数据字符串 + + Returns: + 是否反序列化成功 + """ + if 'serializer' in self.tools and self.plugin.navmesh_manager: + return self.tools['serializer'].deserialize_navmesh(data_str, self.plugin.navmesh_manager) + return False + + def save_navmesh_to_file(self, filepath: str, + compress: bool = False, pretty_print: bool = False) -> bool: + """ + 保存导航网格到文件 + + Args: + filepath: 文件路径 + compress: 是否压缩 + pretty_print: 是否美化输出 + + Returns: + 是否保存成功 + """ + if 'serializer' in self.tools and self.plugin.navmesh_manager: + return self.tools['serializer'].save_navmesh_to_file( + self.plugin.navmesh_manager, filepath, compress, pretty_print) + return False + + def load_navmesh_from_file(self, filepath: str) -> bool: + """ + 从文件加载导航网格 + + Args: + filepath: 文件路径 + + Returns: + 是否加载成功 + """ + if 'serializer' in self.tools and self.plugin.navmesh_manager: + return self.tools['serializer'].load_navmesh_from_file(filepath, self.plugin.navmesh_manager) + return False + + def export_to_standard_format(self, filepath: str, + format_type: str = 'obj') -> bool: + """ + 导出为标准格式 + + Args: + filepath: 导出文件路径 + format_type: 导出格式 ('obj', 'ply') + + Returns: + 是否导出成功 + """ + if 'serializer' in self.tools and self.plugin.navmesh_manager: + return self.tools['serializer'].export_to_standard_format( + self.plugin.navmesh_manager, filepath, format_type) + return False + + def import_from_standard_format(self, filepath: str, + format_type: str = 'obj') -> bool: + """ + 从标准格式导入 + + Args: + filepath: 导入文件路径 + format_type: 导入格式 ('obj', 'ply') + + Returns: + 是否导入成功 + """ + if 'serializer' in self.tools and self.plugin.navmesh_manager: + return self.tools['serializer'].import_from_standard_format( + filepath, self.plugin.navmesh_manager, format_type) + return False + + def get_file_info(self, filepath: str) -> Dict[str, Any]: + """ + 获取导航网格文件信息 + + Args: + filepath: 文件路径 + + Returns: + 文件信息字典 + """ + if 'serializer' in self.tools: + return self.tools['serializer'].get_file_info(filepath) + return {} + + def batch_analyze(self, filepaths: List[str]) -> Dict[str, Dict]: + """ + 批量分析多个导航网格文件 + + Args: + filepaths: 文件路径列表 + + Returns: + 各文件的分析结果 + """ + results = {} + + for filepath in filepaths: + try: + info = self.get_file_info(filepath) + results[filepath] = info + except Exception as e: + results[filepath] = {'error': str(e)} + + return results + + def compare_navmeshes(self, navmesh1_path: str, navmesh2_path: str) -> Dict: + """ + 比较两个导航网格 + + Args: + navmesh1_path: 第一个导航网格文件路径 + navmesh2_path: 第二个导航网格文件路径 + + Returns: + 比较结果 + """ + try: + info1 = self.get_file_info(navmesh1_path) + info2 = self.get_file_info(navmesh2_path) + + comparison = { + 'file1': info1, + 'file2': info2, + 'differences': {} + } + + # 比较关键指标 + for key in ['polygon_count', 'obstacle_count', 'file_size']: + val1 = info1.get(key, 0) + val2 = info2.get(key, 0) + if val1 != val2: + comparison['differences'][key] = { + 'file1': val1, + 'file2': val2, + 'difference': val2 - val1, + 'percentage': ((val2 - val1) / val1 * 100) if val1 != 0 else float('inf') + } + + return comparison + + except Exception as e: + return {'error': str(e)} + + def validate_navmesh_file(self, filepath: str) -> Dict: + """ + 验证导航网格文件的有效性 + + Args: + filepath: 文件路径 + + Returns: + 验证结果 + """ + try: + info = self.get_file_info(filepath) + + validation = { + 'filepath': filepath, + 'valid': True, + 'issues': [], + 'info': info + } + + # 检查基本信息 + if info.get('file_size', 0) == 0: + validation['valid'] = False + validation['issues'].append('文件为空') + + if info.get('version') == 'unknown': + validation['issues'].append('无法识别文件版本') + + # 检查多边形数量 + polygon_count = info.get('polygon_count', 0) + if polygon_count == 0: + validation['issues'].append('导航网格不包含多边形') + elif polygon_count > 100000: + validation['issues'].append('多边形数量过多,可能影响性能') + + return validation + + except Exception as e: + return { + 'filepath': filepath, + 'valid': False, + 'issues': [str(e)], + 'error': True + } + + def optimize_and_save(self, input_filepath: str, output_filepath: str, + optimization_level: str = 'medium') -> Dict: + """ + 优化并保存导航网格 + + Args: + input_filepath: 输入文件路径 + output_filepath: 输出文件路径 + optimization_level: 优化级别 + + Returns: + 操作结果 + """ + try: + # 加载导航网格 + if not self.load_navmesh_from_file(input_filepath): + return {'success': False, 'error': '无法加载输入文件'} + + # 执行优化 + optimization_result = self.optimize_mesh(optimization_level) + + # 保存优化后的网格 + if not self.save_navmesh_to_file(output_filepath): + return {'success': False, 'error': '无法保存优化后的文件'} + + return { + 'success': True, + 'optimization_result': optimization_result, + 'input_file': input_filepath, + 'output_file': output_filepath + } + + except Exception as e: + return {'success': False, 'error': str(e)} + + def create_optimization_pipeline(self, steps: List[str]) -> Dict: + """ + 创建优化流水线 + + Args: + steps: 优化步骤列表 + + Returns: + 流水线执行结果 + """ + results = { + 'steps': steps, + 'results': [], + 'total_improvement': 0 + } + + for step in steps: + try: + if step == 'merge': + result = {'step': 'merge', 'polygons_merged': self.merge_adjacent_polygons()} + elif step == 'simplify': + result = {'step': 'simplify', 'vertices_simplified': self.simplify_polygons()} + elif step == 'remove_small': + result = {'step': 'remove_small', 'polygons_removed': self.remove_small_polygons()} + elif step == 'optimize': + result = {'step': 'optimize', 'optimization_result': self.optimize_mesh()} + else: + result = {'step': step, 'error': '未知的优化步骤'} + + results['results'].append(result) + + except Exception as e: + results['results'].append({'step': step, 'error': str(e)}) + return results \ No newline at end of file diff --git a/plugins/user/navigation_mesh/tools/pathfinder.py b/plugins/user/navigation_mesh/tools/pathfinder.py index 3aba8382..134d103b 100644 --- a/plugins/user/navigation_mesh/tools/pathfinder.py +++ b/plugins/user/navigation_mesh/tools/pathfinder.py @@ -1,419 +1,419 @@ -""" -路径查找器 -实现基于导航网格的寻路算法 -""" - -import heapq -import math -from typing import List, Optional, Dict, Tuple -from panda3d.core import Point3 - -class PathfinderNode: - """ - 寻路节点 - 用于A*算法中的节点表示 - """ - - def __init__(self, polygon_id: int, position: Point3): - self.polygon_id = polygon_id - self.position = position - self.g_cost = 0.0 # 从起点到当前节点的实际代价 - self.h_cost = 0.0 # 启发式代价(到终点的估计代价) - self.f_cost = 0.0 # 总代价 (g_cost + h_cost) - self.parent = None # 父节点 - - def __lt__(self, other): - """用于优先队列比较""" - return self.f_cost < other.f_cost - - def __eq__(self, other): - """用于节点比较""" - return self.polygon_id == other.polygon_id - -class Pathfinder: - """ - 路径查找器 - 实现基于导航网格的A*寻路算法 - """ - - def __init__(self, navmesh_manager): - """ - 初始化路径查找器 - - Args: - navmesh_manager: 导航网格管理器 - """ - self.navmesh_manager = navmesh_manager - - # 寻路设置 - self.heuristic_weight = 1.0 # 启发式权重 - self.max_path_nodes = 10000 # 最大路径节点数 - - # 性能统计 - self.stats = { - 'paths_found': 0, - 'paths_not_found': 0, - 'total_search_time': 0.0, - 'total_nodes_searched': 0 - } - - print("✓ 路径查找器初始化完成") - - def find_path(self, start_point: Point3, end_point: Point3) -> Optional[List[Point3]]: - """ - 查找从起点到终点的路径 - - Args: - start_point: 起点 - end_point: 终点 - - Returns: - 路径点列表,如果找不到路径则返回None - """ - try: - # 检查导航网格是否可用 - if not self.navmesh_manager or not self.navmesh_manager.polygons: - print("⚠ 导航网格不可用") - return None - - # 获取起点和终点所在的多边形 - start_polygon = self.navmesh_manager.get_polygon_containing_point(start_point) - end_polygon = self.navmesh_manager.get_polygon_containing_point(end_point) - - # 如果起点或终点不在导航网格上,使用最近的点 - if not start_polygon: - closest_start = self.navmesh_manager.get_closest_point(start_point) - if not closest_start: - print("⚠ 无法找到起点附近的可行走区域") - return None - start_point = closest_start - start_polygon = self.navmesh_manager.get_polygon_containing_point(start_point) - - if not end_polygon: - closest_end = self.navmesh_manager.get_closest_point(end_point) - if not closest_end: - print("⚠ 无法找到终点附近的可行走区域") - return None - end_point = closest_end - end_polygon = self.navmesh_manager.get_polygon_containing_point(end_point) - - # 如果起点和终点在同一个多边形内,直接连接 - if start_polygon.polygon_id == end_polygon.polygon_id: - return [start_point, end_point] - - # 使用A*算法查找路径 - path = self._a_star_search(start_polygon.polygon_id, end_polygon.polygon_id, - start_point, end_point) - - if path: - self.stats['paths_found'] += 1 - print(f"✓ 找到路径,包含 {len(path)} 个节点") - return path - else: - self.stats['paths_not_found'] += 1 - print("⚠ 未找到路径") - return None - - except Exception as e: - print(f"✗ 路径查找失败: {e}") - return None - - def _a_star_search(self, start_polygon_id: int, end_polygon_id: int, - start_point: Point3, end_point: Point3) -> Optional[List[Point3]]: - """ - A*寻路算法实现 - - Args: - start_polygon_id: 起始多边形ID - end_polygon_id: 目标多边形ID - start_point: 起点 - end_point: 终点 - - Returns: - 路径点列表,如果找不到路径则返回None - """ - import time - start_time = time.time() - - # 开放列表(优先队列) - open_list = [] - - # 关闭列表(已访问的节点) - closed_list = set() - - # 创建起始节点 - start_node = PathfinderNode(start_polygon_id, start_point) - start_node.g_cost = 0 - start_node.h_cost = self._calculate_heuristic(start_point, end_point) - start_node.f_cost = start_node.g_cost + start_node.h_cost - - # 将起始节点加入开放列表 - heapq.heappush(open_list, start_node) - - # 节点搜索计数 - nodes_searched = 0 - - while open_list and nodes_searched < self.max_path_nodes: - # 取出f_cost最小的节点 - current_node = heapq.heappop(open_list) - nodes_searched += 1 - - # 如果到达目标节点 - if current_node.polygon_id == end_polygon_id: - # 重构路径 - path = self._reconstruct_path(current_node) - # 添加终点 - path.append(end_point) - self.stats['total_search_time'] += time.time() - start_time - self.stats['total_nodes_searched'] += nodes_searched - return path - - # 将当前节点加入关闭列表 - closed_list.add(current_node.polygon_id) - - # 获取当前多边形 - if current_node.polygon_id not in self.navmesh_manager.polygons: - continue - - current_polygon = self.navmesh_manager.polygons[current_node.polygon_id] - - # 检查所有相邻多边形 - for neighbor_id in current_polygon.neighbors: - # 如果已在关闭列表,跳过 - if neighbor_id in closed_list: - continue - - # 获取邻居多边形 - if neighbor_id not in self.navmesh_manager.polygons: - continue - - neighbor_polygon = self.navmesh_manager.polygons[neighbor_id] - - # 计算到邻居节点的代价 - neighbor_position = neighbor_polygon.center - tentative_g_cost = current_node.g_cost + \ - self._calculate_distance(current_node.position, neighbor_position) - - # 检查是否需要更新邻居节点 - existing_node = self._find_node_in_open_list(open_list, neighbor_id) - if existing_node is None: - # 创建新节点 - neighbor_node = PathfinderNode(neighbor_id, neighbor_position) - neighbor_node.g_cost = tentative_g_cost - neighbor_node.h_cost = self._calculate_heuristic(neighbor_position, end_point) - neighbor_node.f_cost = neighbor_node.g_cost + neighbor_node.h_cost - neighbor_node.parent = current_node - - heapq.heappush(open_list, neighbor_node) - - elif tentative_g_cost < existing_node.g_cost: - # 更新现有节点 - existing_node.g_cost = tentative_g_cost - existing_node.f_cost = existing_node.g_cost + existing_node.h_cost - existing_node.parent = current_node - - # 未找到路径 - self.stats['total_search_time'] += time.time() - start_time - self.stats['total_nodes_searched'] += nodes_searched - return None - - def _calculate_heuristic(self, point1: Point3, point2: Point3) -> float: - """ - 计算启发式代价(使用欧几里得距离) - - Args: - point1: 起点 - point2: 终点 - - Returns: - 启发式代价 - """ - return (point2 - point1).length() * self.heuristic_weight - - def _calculate_distance(self, point1: Point3, point2: Point3) -> float: - """ - 计算两点间距离 - - Args: - point1: 第一个点 - point2: 第二个点 - - Returns: - 两点间距离 - """ - return (point2 - point1).length() - - def _find_node_in_open_list(self, open_list: List[PathfinderNode], polygon_id: int) -> Optional[PathfinderNode]: - """ - 在开放列表中查找指定多边形ID的节点 - - Args: - open_list: 开放列表 - polygon_id: 多边形ID - - Returns: - 找到的节点,如果未找到则返回None - """ - for node in open_list: - if node.polygon_id == polygon_id: - return node - return None - - def _reconstruct_path(self, end_node: PathfinderNode) -> List[Point3]: - """ - 重构路径 - - Args: - end_node: 终点节点 - - Returns: - 路径点列表 - """ - path = [] - current = end_node - - # 从终点回溯到起点 - while current is not None: - path.append(current.position) - current = current.parent - - # 反转路径(从起点到终点) - path.reverse() - return path - - def smooth_path(self, path: List[Point3]) -> List[Point3]: - """ - 平滑路径(可选优化) - - Args: - path: 原始路径 - - Returns: - 平滑后的路径 - """ - if len(path) <= 2: - return path - - smoothed_path = [path[0]] - - # 简单的路径平滑算法 - i = 0 - while i < len(path) - 1: - # 尝试直接连接当前点和后面的点 - j = len(path) - 1 - while j > i: - # 检查两点间是否可以直接通行 - if self._is_line_walkable(path[i], path[j]): - smoothed_path.append(path[j]) - i = j - break - j -= 1 - else: - # 如果不能直接连接,添加下一个点 - i += 1 - if i < len(path): - smoothed_path.append(path[i]) - - return smoothed_path - - def _is_line_walkable(self, start: Point3, end: Point3) -> bool: - """ - 检查两点间直线是否可行走 - - Args: - start: 起点 - end: 终点 - - Returns: - 是否可行走 - """ - # 简化实现:检查线段上的几个点是否都在可行走区域 - steps = 10 - for i in range(steps + 1): - t = i / steps - point = start + (end - start) * t - if not self.navmesh_manager.is_point_walkable(point): - return False - return True - - def get_pathfinding_stats(self) -> Dict: - """ - 获取寻路统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'paths_found': 0, - 'paths_not_found': 0, - 'total_search_time': 0.0, - 'total_nodes_searched': 0 - } - - def set_heuristic_weight(self, weight: float): - """ - 设置启发式权重 - - Args: - weight: 权重值 - """ - self.heuristic_weight = max(0.0, weight) - - def set_max_path_nodes(self, max_nodes: int): - """ - 设置最大路径节点数 - - Args: - max_nodes: 最大节点数 - """ - self.max_path_nodes = max(1, max_nodes) - - def find_path_with_multiple_waypoints(self, waypoints: List[Point3]) -> Optional[List[Point3]]: - """ - 查找经过多个路点的路径 - - Args: - waypoints: 路点列表 - - Returns: - 完整路径,如果任何一段找不到则返回None - """ - if len(waypoints) < 2: - return None - - # 查找每段路径并连接 - full_path = [] - - for i in range(len(waypoints) - 1): - segment_path = self.find_path(waypoints[i], waypoints[i + 1]) - if not segment_path: - return None # 如果任何一段找不到路径,则整个路径失败 - - # 避免重复点 - if full_path: - full_path.pop() # 移除上一段的终点(与这一段的起点重复) - full_path.extend(segment_path) - - return full_path - - def find_random_path(self) -> Optional[List[Point3]]: - """ - 查找随机路径(用于测试) - - Returns: - 随机路径 - """ - if not self.navmesh_manager: - return None - - start_point = self.navmesh_manager.get_random_point() - end_point = self.navmesh_manager.get_random_point() - - if start_point and end_point: - return self.find_path(start_point, end_point) - +""" +路径查找器 +实现基于导航网格的寻路算法 +""" + +import heapq +import math +from typing import List, Optional, Dict, Tuple +from panda3d.core import Point3 + +class PathfinderNode: + """ + 寻路节点 + 用于A*算法中的节点表示 + """ + + def __init__(self, polygon_id: int, position: Point3): + self.polygon_id = polygon_id + self.position = position + self.g_cost = 0.0 # 从起点到当前节点的实际代价 + self.h_cost = 0.0 # 启发式代价(到终点的估计代价) + self.f_cost = 0.0 # 总代价 (g_cost + h_cost) + self.parent = None # 父节点 + + def __lt__(self, other): + """用于优先队列比较""" + return self.f_cost < other.f_cost + + def __eq__(self, other): + """用于节点比较""" + return self.polygon_id == other.polygon_id + +class Pathfinder: + """ + 路径查找器 + 实现基于导航网格的A*寻路算法 + """ + + def __init__(self, navmesh_manager): + """ + 初始化路径查找器 + + Args: + navmesh_manager: 导航网格管理器 + """ + self.navmesh_manager = navmesh_manager + + # 寻路设置 + self.heuristic_weight = 1.0 # 启发式权重 + self.max_path_nodes = 10000 # 最大路径节点数 + + # 性能统计 + self.stats = { + 'paths_found': 0, + 'paths_not_found': 0, + 'total_search_time': 0.0, + 'total_nodes_searched': 0 + } + + print("✓ 路径查找器初始化完成") + + def find_path(self, start_point: Point3, end_point: Point3) -> Optional[List[Point3]]: + """ + 查找从起点到终点的路径 + + Args: + start_point: 起点 + end_point: 终点 + + Returns: + 路径点列表,如果找不到路径则返回None + """ + try: + # 检查导航网格是否可用 + if not self.navmesh_manager or not self.navmesh_manager.polygons: + print("⚠ 导航网格不可用") + return None + + # 获取起点和终点所在的多边形 + start_polygon = self.navmesh_manager.get_polygon_containing_point(start_point) + end_polygon = self.navmesh_manager.get_polygon_containing_point(end_point) + + # 如果起点或终点不在导航网格上,使用最近的点 + if not start_polygon: + closest_start = self.navmesh_manager.get_closest_point(start_point) + if not closest_start: + print("⚠ 无法找到起点附近的可行走区域") + return None + start_point = closest_start + start_polygon = self.navmesh_manager.get_polygon_containing_point(start_point) + + if not end_polygon: + closest_end = self.navmesh_manager.get_closest_point(end_point) + if not closest_end: + print("⚠ 无法找到终点附近的可行走区域") + return None + end_point = closest_end + end_polygon = self.navmesh_manager.get_polygon_containing_point(end_point) + + # 如果起点和终点在同一个多边形内,直接连接 + if start_polygon.polygon_id == end_polygon.polygon_id: + return [start_point, end_point] + + # 使用A*算法查找路径 + path = self._a_star_search(start_polygon.polygon_id, end_polygon.polygon_id, + start_point, end_point) + + if path: + self.stats['paths_found'] += 1 + print(f"✓ 找到路径,包含 {len(path)} 个节点") + return path + else: + self.stats['paths_not_found'] += 1 + print("⚠ 未找到路径") + return None + + except Exception as e: + print(f"✗ 路径查找失败: {e}") + return None + + def _a_star_search(self, start_polygon_id: int, end_polygon_id: int, + start_point: Point3, end_point: Point3) -> Optional[List[Point3]]: + """ + A*寻路算法实现 + + Args: + start_polygon_id: 起始多边形ID + end_polygon_id: 目标多边形ID + start_point: 起点 + end_point: 终点 + + Returns: + 路径点列表,如果找不到路径则返回None + """ + import time + start_time = time.time() + + # 开放列表(优先队列) + open_list = [] + + # 关闭列表(已访问的节点) + closed_list = set() + + # 创建起始节点 + start_node = PathfinderNode(start_polygon_id, start_point) + start_node.g_cost = 0 + start_node.h_cost = self._calculate_heuristic(start_point, end_point) + start_node.f_cost = start_node.g_cost + start_node.h_cost + + # 将起始节点加入开放列表 + heapq.heappush(open_list, start_node) + + # 节点搜索计数 + nodes_searched = 0 + + while open_list and nodes_searched < self.max_path_nodes: + # 取出f_cost最小的节点 + current_node = heapq.heappop(open_list) + nodes_searched += 1 + + # 如果到达目标节点 + if current_node.polygon_id == end_polygon_id: + # 重构路径 + path = self._reconstruct_path(current_node) + # 添加终点 + path.append(end_point) + self.stats['total_search_time'] += time.time() - start_time + self.stats['total_nodes_searched'] += nodes_searched + return path + + # 将当前节点加入关闭列表 + closed_list.add(current_node.polygon_id) + + # 获取当前多边形 + if current_node.polygon_id not in self.navmesh_manager.polygons: + continue + + current_polygon = self.navmesh_manager.polygons[current_node.polygon_id] + + # 检查所有相邻多边形 + for neighbor_id in current_polygon.neighbors: + # 如果已在关闭列表,跳过 + if neighbor_id in closed_list: + continue + + # 获取邻居多边形 + if neighbor_id not in self.navmesh_manager.polygons: + continue + + neighbor_polygon = self.navmesh_manager.polygons[neighbor_id] + + # 计算到邻居节点的代价 + neighbor_position = neighbor_polygon.center + tentative_g_cost = current_node.g_cost + \ + self._calculate_distance(current_node.position, neighbor_position) + + # 检查是否需要更新邻居节点 + existing_node = self._find_node_in_open_list(open_list, neighbor_id) + if existing_node is None: + # 创建新节点 + neighbor_node = PathfinderNode(neighbor_id, neighbor_position) + neighbor_node.g_cost = tentative_g_cost + neighbor_node.h_cost = self._calculate_heuristic(neighbor_position, end_point) + neighbor_node.f_cost = neighbor_node.g_cost + neighbor_node.h_cost + neighbor_node.parent = current_node + + heapq.heappush(open_list, neighbor_node) + + elif tentative_g_cost < existing_node.g_cost: + # 更新现有节点 + existing_node.g_cost = tentative_g_cost + existing_node.f_cost = existing_node.g_cost + existing_node.h_cost + existing_node.parent = current_node + + # 未找到路径 + self.stats['total_search_time'] += time.time() - start_time + self.stats['total_nodes_searched'] += nodes_searched + return None + + def _calculate_heuristic(self, point1: Point3, point2: Point3) -> float: + """ + 计算启发式代价(使用欧几里得距离) + + Args: + point1: 起点 + point2: 终点 + + Returns: + 启发式代价 + """ + return (point2 - point1).length() * self.heuristic_weight + + def _calculate_distance(self, point1: Point3, point2: Point3) -> float: + """ + 计算两点间距离 + + Args: + point1: 第一个点 + point2: 第二个点 + + Returns: + 两点间距离 + """ + return (point2 - point1).length() + + def _find_node_in_open_list(self, open_list: List[PathfinderNode], polygon_id: int) -> Optional[PathfinderNode]: + """ + 在开放列表中查找指定多边形ID的节点 + + Args: + open_list: 开放列表 + polygon_id: 多边形ID + + Returns: + 找到的节点,如果未找到则返回None + """ + for node in open_list: + if node.polygon_id == polygon_id: + return node + return None + + def _reconstruct_path(self, end_node: PathfinderNode) -> List[Point3]: + """ + 重构路径 + + Args: + end_node: 终点节点 + + Returns: + 路径点列表 + """ + path = [] + current = end_node + + # 从终点回溯到起点 + while current is not None: + path.append(current.position) + current = current.parent + + # 反转路径(从起点到终点) + path.reverse() + return path + + def smooth_path(self, path: List[Point3]) -> List[Point3]: + """ + 平滑路径(可选优化) + + Args: + path: 原始路径 + + Returns: + 平滑后的路径 + """ + if len(path) <= 2: + return path + + smoothed_path = [path[0]] + + # 简单的路径平滑算法 + i = 0 + while i < len(path) - 1: + # 尝试直接连接当前点和后面的点 + j = len(path) - 1 + while j > i: + # 检查两点间是否可以直接通行 + if self._is_line_walkable(path[i], path[j]): + smoothed_path.append(path[j]) + i = j + break + j -= 1 + else: + # 如果不能直接连接,添加下一个点 + i += 1 + if i < len(path): + smoothed_path.append(path[i]) + + return smoothed_path + + def _is_line_walkable(self, start: Point3, end: Point3) -> bool: + """ + 检查两点间直线是否可行走 + + Args: + start: 起点 + end: 终点 + + Returns: + 是否可行走 + """ + # 简化实现:检查线段上的几个点是否都在可行走区域 + steps = 10 + for i in range(steps + 1): + t = i / steps + point = start + (end - start) * t + if not self.navmesh_manager.is_point_walkable(point): + return False + return True + + def get_pathfinding_stats(self) -> Dict: + """ + 获取寻路统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'paths_found': 0, + 'paths_not_found': 0, + 'total_search_time': 0.0, + 'total_nodes_searched': 0 + } + + def set_heuristic_weight(self, weight: float): + """ + 设置启发式权重 + + Args: + weight: 权重值 + """ + self.heuristic_weight = max(0.0, weight) + + def set_max_path_nodes(self, max_nodes: int): + """ + 设置最大路径节点数 + + Args: + max_nodes: 最大节点数 + """ + self.max_path_nodes = max(1, max_nodes) + + def find_path_with_multiple_waypoints(self, waypoints: List[Point3]) -> Optional[List[Point3]]: + """ + 查找经过多个路点的路径 + + Args: + waypoints: 路点列表 + + Returns: + 完整路径,如果任何一段找不到则返回None + """ + if len(waypoints) < 2: + return None + + # 查找每段路径并连接 + full_path = [] + + for i in range(len(waypoints) - 1): + segment_path = self.find_path(waypoints[i], waypoints[i + 1]) + if not segment_path: + return None # 如果任何一段找不到路径,则整个路径失败 + + # 避免重复点 + if full_path: + full_path.pop() # 移除上一段的终点(与这一段的起点重复) + full_path.extend(segment_path) + + return full_path + + def find_random_path(self) -> Optional[List[Point3]]: + """ + 查找随机路径(用于测试) + + Returns: + 随机路径 + """ + if not self.navmesh_manager: + return None + + start_point = self.navmesh_manager.get_random_point() + end_point = self.navmesh_manager.get_random_point() + + if start_point and end_point: + return self.find_path(start_point, end_point) + return None \ No newline at end of file diff --git a/plugins/user/navigation_mesh/utils/navmesh_serializer.py b/plugins/user/navigation_mesh/utils/navmesh_serializer.py index e5e6a683..4bb42276 100644 --- a/plugins/user/navigation_mesh/utils/navmesh_serializer.py +++ b/plugins/user/navigation_mesh/utils/navmesh_serializer.py @@ -1,728 +1,728 @@ -""" -导航网格序列化器 -处理导航网格的导入导出和版本兼容性 -""" - -import json -import zlib -import base64 -from typing import Dict, Any, List, Optional -from panda3d.core import Point3 -import time - -class NavMeshSerializer: - """ - 导航网格序列化器 - 处理导航网格的导入导出和版本兼容性 - """ - - def __init__(self): - """初始化序列化器""" - self.current_version = "2.0" - self.supported_versions = ["1.0", "1.1", "2.0"] - - def serialize_navmesh(self, navmesh_manager) -> str: - """ - 序列化导航网格为字符串 - - Args: - navmesh_manager: 导航网格管理器 - - Returns: - 序列化后的字符串 - """ - try: - # 构建导航网格数据 - data = self._build_navmesh_data(navmesh_manager) - - # 转换为JSON字符串 - json_str = json.dumps(data, ensure_ascii=False, separators=(',', ':')) - - return json_str - - except Exception as e: - print(f"序列化导航网格失败: {e}") - return "" - - def deserialize_navmesh(self, data_str: str, navmesh_manager) -> bool: - """ - 从字符串反序列化导航网格 - - Args: - data_str: 序列化数据字符串 - navmesh_manager: 导航网格管理器 - - Returns: - 是否反序列化成功 - """ - try: - # 解析JSON数据 - data = json.loads(data_str) - - # 处理版本兼容性 - data = self._handle_version_compatibility(data) - - # 应用数据到导航网格管理器 - self._apply_navmesh_data(data, navmesh_manager) - - return True - - except Exception as e: - print(f"反序列化导航网格失败: {e}") - return False - - def save_navmesh_to_file(self, navmesh_manager, filepath: str, - compress: bool = False, pretty_print: bool = False) -> bool: - """ - 保存导航网格到文件 - - Args: - navmesh_manager: 导航网格管理器 - filepath: 文件路径 - compress: 是否压缩 - pretty_print: 是否美化输出 - - Returns: - 是否保存成功 - """ - try: - # 构建导航网格数据 - data = self._build_navmesh_data(navmesh_manager) - - # 添加保存时间戳 - data['metadata']['saved_at'] = time.strftime("%Y-%m-%d %H:%M:%S") - - # 转换为JSON字符串 - if pretty_print: - json_str = json.dumps(data, ensure_ascii=False, indent=2) - else: - json_str = json.dumps(data, ensure_ascii=False, separators=(',', ':')) - - # 处理压缩 - if compress: - # 压缩数据 - compressed_data = zlib.compress(json_str.encode('utf-8')) - encoded_data = base64.b64encode(compressed_data).decode('ascii') - - # 保存压缩数据 - save_data = { - 'version': self.current_version, - 'compressed': True, - 'data': encoded_data - } - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(save_data, f, ensure_ascii=False, indent=2) - else: - # 直接保存JSON数据 - with open(filepath, 'w', encoding='utf-8') as f: - f.write(json_str) - - return True - - except Exception as e: - print(f"保存导航网格失败: {e}") - return False - - def load_navmesh_from_file(self, filepath: str, navmesh_manager) -> bool: - """ - 从文件加载导航网格 - - Args: - filepath: 文件路径 - navmesh_manager: 导航网格管理器 - - Returns: - 是否加载成功 - """ - try: - # 读取文件内容 - with open(filepath, 'r', encoding='utf-8') as f: - content = f.read() - - # 尝试解析为JSON - try: - # 首先尝试解析为包装格式(可能包含压缩信息) - wrapper_data = json.loads(content) - if isinstance(wrapper_data, dict) and 'compressed' in wrapper_data: - if wrapper_data.get('compressed', False): - # 解压缩数据 - encoded_data = wrapper_data['data'] - compressed_data = base64.b64decode(encoded_data.encode('ascii')) - json_str = zlib.decompress(compressed_data).decode('utf-8') - data = json.loads(json_str) - else: - data = wrapper_data - else: - # 直接使用内容 - data = wrapper_data - except json.JSONDecodeError: - # 如果不是JSON格式,假设是直接的导航网格数据 - data = json.loads(content) - - # 处理版本兼容性 - data = self._handle_version_compatibility(data) - - # 应用数据到导航网格管理器 - self._apply_navmesh_data(data, navmesh_manager) - - return True - - except Exception as e: - print(f"加载导航网格失败: {e}") - return False - - def _build_navmesh_data(self, navmesh_manager) -> Dict[str, Any]: - """ - 构建导航网格数据 - - Args: - navmesh_manager: 导航网格管理器 - - Returns: - 导航网格数据字典 - """ - # 构建元数据 - metadata = { - 'version': self.current_version, - 'generator': 'EG Navigation Mesh System', - 'created': time.strftime("%Y-%m-%d %H:%M:%S"), - 'polygon_count': len(navmesh_manager.polygons) if navmesh_manager.polygons else 0, - 'obstacle_count': len(navmesh_manager.obstacles) if navmesh_manager.obstacles else 0 - } - - # 添加设置信息 - settings = { - 'cell_size': getattr(navmesh_manager, 'cell_size', 0.5), - 'cell_height': getattr(navmesh_manager, 'cell_height', 0.2), - 'agent_height': getattr(navmesh_manager, 'agent_height', 1.8), - 'agent_radius': getattr(navmesh_manager, 'agent_radius', 0.3), - 'max_slope': getattr(navmesh_manager, 'max_slope', 45.0), - 'max_climb': getattr(navmesh_manager, 'max_climb', 0.2), - 'min_region_area': getattr(navmesh_manager, 'min_region_area', 3), - 'merge_region_area': getattr(navmesh_manager, 'merge_region_area', 20), - 'edge_max_len': getattr(navmesh_manager, 'edge_max_len', 12.0), - 'edge_max_error': getattr(navmesh_manager, 'edge_max_error', 1.3), - 'verts_per_poly': getattr(navmesh_manager, 'verts_per_poly', 6) - } - - # 构建多边形数据 - polygons = [] - if navmesh_manager.polygons: - for polygon_id, polygon in navmesh_manager.polygons.items(): - poly_data = { - 'id': polygon_id, - 'vertices': [[v.x, v.y, v.z] for v in polygon.vertices], - 'neighbors': polygon.neighbors, - 'portal_points': {}, - 'flags': getattr(polygon, 'flags', 0), - 'area': getattr(polygon, 'area', 0.0) - } - - # 添加门户点数据 - for neighbor_id, (left_point, right_point) in polygon.portal_points.items(): - poly_data['portal_points'][str(neighbor_id)] = [ - [left_point.x, left_point.y, left_point.z], - [right_point.x, right_point.y, right_point.z] - ] - - polygons.append(poly_data) - - # 构建障碍物数据 - obstacles = [] - if navmesh_manager.obstacles: - for obstacle_id, obstacle in navmesh_manager.obstacles.items(): - obs_data = { - 'id': obstacle_id, - 'position': [ - obstacle['position'].x, - obstacle['position'].y, - obstacle['position'].z - ], - 'radius': obstacle['radius'], - 'height': obstacle['height'], - 'type': obstacle.get('type', 'cylinder') - } - obstacles.append(obs_data) - - # 构建完整数据结构 - data = { - 'metadata': metadata, - 'settings': settings, - 'polygons': polygons, - 'obstacles': obstacles - } - - return data - - def _handle_version_compatibility(self, data: Dict[str, Any]) -> Dict[str, Any]: - """ - 处理版本兼容性 - - Args: - data: 导航网格数据 - - Returns: - 处理后的数据 - """ - if 'metadata' not in data or 'version' not in data['metadata']: - # 假设是旧版本数据 - data = self._upgrade_from_version_1_0(data) - - version = data['metadata']['version'] - - # 根据版本进行升级 - if version == "1.0": - data = self._upgrade_from_version_1_0(data) - elif version == "1.1": - data = self._upgrade_from_version_1_1(data) - # 版本2.0不需要升级 - - return data - - def _upgrade_from_version_1_0(self, data: Dict[str, Any]) -> Dict[str, Any]: - """ - 从版本1.0升级 - - Args: - data: 版本1.0的数据 - - Returns: - 升级后的数据 - """ - # 添加缺失的字段 - if 'metadata' not in data: - data['metadata'] = {} - - data['metadata']['version'] = self.current_version - - # 确保所有必需字段存在 - if 'settings' not in data: - data['settings'] = {} - - # 设置默认值 - default_settings = { - 'cell_size': 0.5, - 'cell_height': 0.2, - 'agent_height': 1.8, - 'agent_radius': 0.3, - 'max_slope': 45.0, - 'max_climb': 0.2, - 'min_region_area': 3, - 'merge_region_area': 20, - 'edge_max_len': 12.0, - 'edge_max_error': 1.3, - 'verts_per_poly': 6 - } - - for key, value in default_settings.items(): - if key not in data['settings']: - data['settings'][key] = value - - return data - - def _upgrade_from_version_1_1(self, data: Dict[str, Any]) -> Dict[str, Any]: - """ - 从版本1.1升级 - - Args: - data: 版本1.1的数据 - - Returns: - 升级后的数据 - """ - # 版本1.1到2.0的升级 - data['metadata']['version'] = self.current_version - return data - - def _apply_navmesh_data(self, data: Dict[str, Any], navmesh_manager): - """ - 应用导航网格数据到管理器 - - Args: - data: 导航网格数据 - navmesh_manager: 导航网格管理器 - """ - from ..core.navmesh_manager import NavMeshPolygon - - # 清除现有数据 - navmesh_manager.polygons.clear() - navmesh_manager.obstacles.clear() - - # 应用设置 - if 'settings' in data: - settings = data['settings'] - for key, value in settings.items(): - if hasattr(navmesh_manager, key): - setattr(navmesh_manager, key, value) - - # 解析多边形 - if 'polygons' in data: - for poly_data in data['polygons']: - polygon_id = poly_data['id'] - vertices = [Point3(v[0], v[1], v[2]) for v in poly_data['vertices']] - - polygon = NavMeshPolygon(polygon_id, vertices) - - # 添加邻居信息 - if 'neighbors' in poly_data: - for neighbor_id in poly_data['neighbors']: - polygon.add_neighbor(neighbor_id) - - # 添加门户点信息 - if 'portal_points' in poly_data: - for neighbor_id, portal_data in poly_data['portal_points'].items(): - left_point = Point3(portal_data[0][0], portal_data[0][1], portal_data[0][2]) - right_point = Point3(portal_data[1][0], portal_data[1][1], portal_data[1][2]) - polygon.set_portal_points(int(neighbor_id), left_point, right_point) - - # 设置标志位 - if 'flags' in poly_data: - polygon.flags = poly_data['flags'] - - # 设置面积 - if 'area' in poly_data: - polygon.area = poly_data['area'] - - navmesh_manager.polygons[polygon_id] = polygon - - # 解析障碍物 - if 'obstacles' in data: - for obs_data in data['obstacles']: - obstacle_id = obs_data['id'] - navmesh_manager.obstacles[obstacle_id] = { - 'position': Point3(obs_data['position'][0], obs_data['position'][1], obs_data['position'][2]), - 'radius': obs_data['radius'], - 'height': obs_data['height'], - 'type': obs_data.get('type', 'cylinder') - } - - # 更新统计信息 - navmesh_manager._update_stats() - - # 更新空间哈希 - navmesh_manager._update_spatial_hash() - - def export_to_standard_format(self, navmesh_manager, filepath: str, - format_type: str = 'obj') -> bool: - """ - 导出为标准格式 - - Args: - navmesh_manager: 导航网格管理器 - filepath: 导出文件路径 - format_type: 导出格式 ('obj', 'ply') - - Returns: - 是否导出成功 - """ - try: - if format_type.lower() == 'obj': - return self._export_to_obj(navmesh_manager, filepath) - elif format_type.lower() == 'ply': - return self._export_to_ply(navmesh_manager, filepath) - else: - print(f"不支持的导出格式: {format_type}") - return False - - except Exception as e: - print(f"导出为标准格式失败: {e}") - return False - - def _export_to_obj(self, navmesh_manager, filepath: str) -> bool: - """ - 导出为OBJ格式 - - Args: - navmesh_manager: 导航网格管理器 - filepath: 导出文件路径 - - Returns: - 是否导出成功 - """ - try: - with open(filepath, 'w', encoding='utf-8') as f: - f.write("# Navigation Mesh Export\n") - f.write(f"# Generated by EG Navigation Mesh System\n") - f.write(f"# Polygons: {len(navmesh_manager.polygons)}\n\n") - - # 写入顶点 - vertex_index = 1 - vertex_mapping = {} # 旧顶点索引到新顶点索引的映射 - - for polygon_id, polygon in navmesh_manager.polygons.items(): - for vertex in polygon.vertices: - vertex_key = (vertex.x, vertex.y, vertex.z) - if vertex_key not in vertex_mapping: - f.write(f"v {vertex.x} {vertex.y} {vertex.z}\n") - vertex_mapping[vertex_key] = vertex_index - vertex_index += 1 - - f.write("\n") - - # 写入面 - for polygon_id, polygon in navmesh_manager.polygons.items(): - f.write(f"g polygon_{polygon_id}\n") - face_vertices = [] - for vertex in polygon.vertices: - vertex_key = (vertex.x, vertex.y, vertex.z) - face_vertices.append(str(vertex_mapping[vertex_key])) - f.write(f"f {' '.join(face_vertices)}\n\n") - - return True - - except Exception as e: - print(f"导出为OBJ格式失败: {e}") - return False - - def _export_to_ply(self, navmesh_manager, filepath: str) -> bool: - """ - 导出为PLY格式 - - Args: - navmesh_manager: 导航网格管理器 - filepath: 导出文件路径 - - Returns: - 是否导出成功 - """ - try: - # 收集所有唯一顶点 - vertices = [] - vertex_mapping = {} - faces = [] - - for polygon_id, polygon in navmesh_manager.polygons.items(): - face_indices = [] - for vertex in polygon.vertices: - vertex_key = (vertex.x, vertex.y, vertex.z) - if vertex_key not in vertex_mapping: - vertex_mapping[vertex_key] = len(vertices) - vertices.append(vertex) - face_indices.append(vertex_mapping[vertex_key]) - faces.append(face_indices) - - # 写入PLY文件 - with open(filepath, 'w', encoding='utf-8') as f: - # PLY头部 - f.write("ply\n") - f.write("format ascii 1.0\n") - f.write(f"element vertex {len(vertices)}\n") - f.write("property float x\n") - f.write("property float y\n") - f.write("property float z\n") - f.write(f"element face {len(faces)}\n") - f.write("property list uchar int vertex_indices\n") - f.write("end_header\n") - - # 写入顶点 - for vertex in vertices: - f.write(f"{vertex.x} {vertex.y} {vertex.z}\n") - - # 写入面 - for face in faces: - f.write(f"{len(face)} {' '.join(map(str, face))}\n") - - return True - - except Exception as e: - print(f"导出为PLY格式失败: {e}") - return False - - def import_from_standard_format(self, filepath: str, navmesh_manager, - format_type: str = 'obj') -> bool: - """ - 从标准格式导入 - - Args: - filepath: 导入文件路径 - navmesh_manager: 导航网格管理器 - format_type: 导入格式 ('obj', 'ply') - - Returns: - 是否导入成功 - """ - try: - if format_type.lower() == 'obj': - return self._import_from_obj(filepath, navmesh_manager) - elif format_type.lower() == 'ply': - return self._import_from_ply(filepath, navmesh_manager) - else: - print(f"不支持的导入格式: {format_type}") - return False - - except Exception as e: - print(f"从标准格式导入失败: {e}") - return False - - def _import_from_obj(self, filepath: str, navmesh_manager) -> bool: - """ - 从OBJ格式导入 - - Args: - filepath: 导入文件路径 - navmesh_manager: 导航网格管理器 - - Returns: - 是否导入成功 - """ - try: - vertices = [] - faces = [] - - with open(filepath, 'r', encoding='utf-8') as f: - for line in f: - line = line.strip() - if not line or line.startswith('#'): - continue - - parts = line.split() - if not parts: - continue - - if parts[0] == 'v': - # 顶点 - if len(parts) >= 4: - x, y, z = float(parts[1]), float(parts[2]), float(parts[3]) - vertices.append(Point3(x, y, z)) - elif parts[0] == 'f': - # 面 - face_indices = [] - for part in parts[1:]: - # 处理索引(OBJ格式索引从1开始) - index = int(part.split('/')[0]) - 1 - face_indices.append(index) - faces.append(face_indices) - - # 创建多边形 - navmesh_manager.polygons.clear() - for i, face_indices in enumerate(faces): - polygon_vertices = [vertices[idx] for idx in face_indices if 0 <= idx < len(vertices)] - if len(polygon_vertices) >= 3: - navmesh_manager.add_polygon(polygon_vertices) - - return True - - except Exception as e: - print(f"从OBJ格式导入失败: {e}") - return False - - def _import_from_ply(self, filepath: str, navmesh_manager) -> bool: - """ - 从PLY格式导入 - - Args: - filepath: 导入文件路径 - navmesh_manager: 导航网格管理器 - - Returns: - 是否导入成功 - """ - try: - vertices = [] - faces = [] - - with open(filepath, 'r', encoding='utf-8') as f: - lines = f.readlines() - - # 解析PLY头部 - i = 0 - vertex_count = 0 - face_count = 0 - - while i < len(lines): - line = lines[i].strip() - if not line: - i += 1 - continue - - if line == "end_header": - i += 1 - break - - parts = line.split() - if len(parts) >= 3 and parts[0] == "element": - if parts[1] == "vertex": - vertex_count = int(parts[2]) - elif parts[1] == "face": - face_count = int(parts[2]) - - i += 1 - - # 读取顶点 - for j in range(vertex_count): - if i + j < len(lines): - parts = lines[i + j].strip().split() - if len(parts) >= 3: - x, y, z = float(parts[0]), float(parts[1]), float(parts[2]) - vertices.append(Point3(x, y, z)) - - i += vertex_count - - # 读取面 - for j in range(face_count): - if i + j < len(lines): - parts = lines[i + j].strip().split() - if len(parts) >= 2: - count = int(parts[0]) - face_indices = [int(idx) for idx in parts[1:1+count]] - faces.append(face_indices) - - # 创建多边形 - navmesh_manager.polygons.clear() - for face_indices in faces: - polygon_vertices = [vertices[idx] for idx in face_indices if 0 <= idx < len(vertices)] - if len(polygon_vertices) >= 3: - navmesh_manager.add_polygon(polygon_vertices) - - return True - - except Exception as e: - print(f"从PLY格式导入失败: {e}") - return False - - def get_file_info(self, filepath: str) -> Dict[str, Any]: - """ - 获取导航网格文件信息 - - Args: - filepath: 文件路径 - - Returns: - 文件信息字典 - """ - try: - info = { - 'filepath': filepath, - 'file_size': 0, - 'version': 'unknown', - 'polygon_count': 0, - 'obstacle_count': 0, - 'compressed': False - } - - import os - info['file_size'] = os.path.getsize(filepath) - - # 读取文件内容 - with open(filepath, 'r', encoding='utf-8') as f: - content = f.read(1024) # 只读取前1024个字符 - - # 尝试解析 - try: - data = json.loads(content) - if isinstance(data, dict): - if 'compressed' in data: - info['compressed'] = data['compressed'] - info['version'] = data.get('version', 'unknown') - elif 'metadata' in data: - info['version'] = data['metadata'].get('version', 'unknown') - info['polygon_count'] = data['metadata'].get('polygon_count', 0) - info['obstacle_count'] = data['metadata'].get('obstacle_count', 0) - except json.JSONDecodeError: - pass - - return info - - except Exception as e: - print(f"获取文件信息失败: {e}") +""" +导航网格序列化器 +处理导航网格的导入导出和版本兼容性 +""" + +import json +import zlib +import base64 +from typing import Dict, Any, List, Optional +from panda3d.core import Point3 +import time + +class NavMeshSerializer: + """ + 导航网格序列化器 + 处理导航网格的导入导出和版本兼容性 + """ + + def __init__(self): + """初始化序列化器""" + self.current_version = "2.0" + self.supported_versions = ["1.0", "1.1", "2.0"] + + def serialize_navmesh(self, navmesh_manager) -> str: + """ + 序列化导航网格为字符串 + + Args: + navmesh_manager: 导航网格管理器 + + Returns: + 序列化后的字符串 + """ + try: + # 构建导航网格数据 + data = self._build_navmesh_data(navmesh_manager) + + # 转换为JSON字符串 + json_str = json.dumps(data, ensure_ascii=False, separators=(',', ':')) + + return json_str + + except Exception as e: + print(f"序列化导航网格失败: {e}") + return "" + + def deserialize_navmesh(self, data_str: str, navmesh_manager) -> bool: + """ + 从字符串反序列化导航网格 + + Args: + data_str: 序列化数据字符串 + navmesh_manager: 导航网格管理器 + + Returns: + 是否反序列化成功 + """ + try: + # 解析JSON数据 + data = json.loads(data_str) + + # 处理版本兼容性 + data = self._handle_version_compatibility(data) + + # 应用数据到导航网格管理器 + self._apply_navmesh_data(data, navmesh_manager) + + return True + + except Exception as e: + print(f"反序列化导航网格失败: {e}") + return False + + def save_navmesh_to_file(self, navmesh_manager, filepath: str, + compress: bool = False, pretty_print: bool = False) -> bool: + """ + 保存导航网格到文件 + + Args: + navmesh_manager: 导航网格管理器 + filepath: 文件路径 + compress: 是否压缩 + pretty_print: 是否美化输出 + + Returns: + 是否保存成功 + """ + try: + # 构建导航网格数据 + data = self._build_navmesh_data(navmesh_manager) + + # 添加保存时间戳 + data['metadata']['saved_at'] = time.strftime("%Y-%m-%d %H:%M:%S") + + # 转换为JSON字符串 + if pretty_print: + json_str = json.dumps(data, ensure_ascii=False, indent=2) + else: + json_str = json.dumps(data, ensure_ascii=False, separators=(',', ':')) + + # 处理压缩 + if compress: + # 压缩数据 + compressed_data = zlib.compress(json_str.encode('utf-8')) + encoded_data = base64.b64encode(compressed_data).decode('ascii') + + # 保存压缩数据 + save_data = { + 'version': self.current_version, + 'compressed': True, + 'data': encoded_data + } + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(save_data, f, ensure_ascii=False, indent=2) + else: + # 直接保存JSON数据 + with open(filepath, 'w', encoding='utf-8') as f: + f.write(json_str) + + return True + + except Exception as e: + print(f"保存导航网格失败: {e}") + return False + + def load_navmesh_from_file(self, filepath: str, navmesh_manager) -> bool: + """ + 从文件加载导航网格 + + Args: + filepath: 文件路径 + navmesh_manager: 导航网格管理器 + + Returns: + 是否加载成功 + """ + try: + # 读取文件内容 + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + + # 尝试解析为JSON + try: + # 首先尝试解析为包装格式(可能包含压缩信息) + wrapper_data = json.loads(content) + if isinstance(wrapper_data, dict) and 'compressed' in wrapper_data: + if wrapper_data.get('compressed', False): + # 解压缩数据 + encoded_data = wrapper_data['data'] + compressed_data = base64.b64decode(encoded_data.encode('ascii')) + json_str = zlib.decompress(compressed_data).decode('utf-8') + data = json.loads(json_str) + else: + data = wrapper_data + else: + # 直接使用内容 + data = wrapper_data + except json.JSONDecodeError: + # 如果不是JSON格式,假设是直接的导航网格数据 + data = json.loads(content) + + # 处理版本兼容性 + data = self._handle_version_compatibility(data) + + # 应用数据到导航网格管理器 + self._apply_navmesh_data(data, navmesh_manager) + + return True + + except Exception as e: + print(f"加载导航网格失败: {e}") + return False + + def _build_navmesh_data(self, navmesh_manager) -> Dict[str, Any]: + """ + 构建导航网格数据 + + Args: + navmesh_manager: 导航网格管理器 + + Returns: + 导航网格数据字典 + """ + # 构建元数据 + metadata = { + 'version': self.current_version, + 'generator': 'EG Navigation Mesh System', + 'created': time.strftime("%Y-%m-%d %H:%M:%S"), + 'polygon_count': len(navmesh_manager.polygons) if navmesh_manager.polygons else 0, + 'obstacle_count': len(navmesh_manager.obstacles) if navmesh_manager.obstacles else 0 + } + + # 添加设置信息 + settings = { + 'cell_size': getattr(navmesh_manager, 'cell_size', 0.5), + 'cell_height': getattr(navmesh_manager, 'cell_height', 0.2), + 'agent_height': getattr(navmesh_manager, 'agent_height', 1.8), + 'agent_radius': getattr(navmesh_manager, 'agent_radius', 0.3), + 'max_slope': getattr(navmesh_manager, 'max_slope', 45.0), + 'max_climb': getattr(navmesh_manager, 'max_climb', 0.2), + 'min_region_area': getattr(navmesh_manager, 'min_region_area', 3), + 'merge_region_area': getattr(navmesh_manager, 'merge_region_area', 20), + 'edge_max_len': getattr(navmesh_manager, 'edge_max_len', 12.0), + 'edge_max_error': getattr(navmesh_manager, 'edge_max_error', 1.3), + 'verts_per_poly': getattr(navmesh_manager, 'verts_per_poly', 6) + } + + # 构建多边形数据 + polygons = [] + if navmesh_manager.polygons: + for polygon_id, polygon in navmesh_manager.polygons.items(): + poly_data = { + 'id': polygon_id, + 'vertices': [[v.x, v.y, v.z] for v in polygon.vertices], + 'neighbors': polygon.neighbors, + 'portal_points': {}, + 'flags': getattr(polygon, 'flags', 0), + 'area': getattr(polygon, 'area', 0.0) + } + + # 添加门户点数据 + for neighbor_id, (left_point, right_point) in polygon.portal_points.items(): + poly_data['portal_points'][str(neighbor_id)] = [ + [left_point.x, left_point.y, left_point.z], + [right_point.x, right_point.y, right_point.z] + ] + + polygons.append(poly_data) + + # 构建障碍物数据 + obstacles = [] + if navmesh_manager.obstacles: + for obstacle_id, obstacle in navmesh_manager.obstacles.items(): + obs_data = { + 'id': obstacle_id, + 'position': [ + obstacle['position'].x, + obstacle['position'].y, + obstacle['position'].z + ], + 'radius': obstacle['radius'], + 'height': obstacle['height'], + 'type': obstacle.get('type', 'cylinder') + } + obstacles.append(obs_data) + + # 构建完整数据结构 + data = { + 'metadata': metadata, + 'settings': settings, + 'polygons': polygons, + 'obstacles': obstacles + } + + return data + + def _handle_version_compatibility(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + 处理版本兼容性 + + Args: + data: 导航网格数据 + + Returns: + 处理后的数据 + """ + if 'metadata' not in data or 'version' not in data['metadata']: + # 假设是旧版本数据 + data = self._upgrade_from_version_1_0(data) + + version = data['metadata']['version'] + + # 根据版本进行升级 + if version == "1.0": + data = self._upgrade_from_version_1_0(data) + elif version == "1.1": + data = self._upgrade_from_version_1_1(data) + # 版本2.0不需要升级 + + return data + + def _upgrade_from_version_1_0(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + 从版本1.0升级 + + Args: + data: 版本1.0的数据 + + Returns: + 升级后的数据 + """ + # 添加缺失的字段 + if 'metadata' not in data: + data['metadata'] = {} + + data['metadata']['version'] = self.current_version + + # 确保所有必需字段存在 + if 'settings' not in data: + data['settings'] = {} + + # 设置默认值 + default_settings = { + 'cell_size': 0.5, + 'cell_height': 0.2, + 'agent_height': 1.8, + 'agent_radius': 0.3, + 'max_slope': 45.0, + 'max_climb': 0.2, + 'min_region_area': 3, + 'merge_region_area': 20, + 'edge_max_len': 12.0, + 'edge_max_error': 1.3, + 'verts_per_poly': 6 + } + + for key, value in default_settings.items(): + if key not in data['settings']: + data['settings'][key] = value + + return data + + def _upgrade_from_version_1_1(self, data: Dict[str, Any]) -> Dict[str, Any]: + """ + 从版本1.1升级 + + Args: + data: 版本1.1的数据 + + Returns: + 升级后的数据 + """ + # 版本1.1到2.0的升级 + data['metadata']['version'] = self.current_version + return data + + def _apply_navmesh_data(self, data: Dict[str, Any], navmesh_manager): + """ + 应用导航网格数据到管理器 + + Args: + data: 导航网格数据 + navmesh_manager: 导航网格管理器 + """ + from ..core.navmesh_manager import NavMeshPolygon + + # 清除现有数据 + navmesh_manager.polygons.clear() + navmesh_manager.obstacles.clear() + + # 应用设置 + if 'settings' in data: + settings = data['settings'] + for key, value in settings.items(): + if hasattr(navmesh_manager, key): + setattr(navmesh_manager, key, value) + + # 解析多边形 + if 'polygons' in data: + for poly_data in data['polygons']: + polygon_id = poly_data['id'] + vertices = [Point3(v[0], v[1], v[2]) for v in poly_data['vertices']] + + polygon = NavMeshPolygon(polygon_id, vertices) + + # 添加邻居信息 + if 'neighbors' in poly_data: + for neighbor_id in poly_data['neighbors']: + polygon.add_neighbor(neighbor_id) + + # 添加门户点信息 + if 'portal_points' in poly_data: + for neighbor_id, portal_data in poly_data['portal_points'].items(): + left_point = Point3(portal_data[0][0], portal_data[0][1], portal_data[0][2]) + right_point = Point3(portal_data[1][0], portal_data[1][1], portal_data[1][2]) + polygon.set_portal_points(int(neighbor_id), left_point, right_point) + + # 设置标志位 + if 'flags' in poly_data: + polygon.flags = poly_data['flags'] + + # 设置面积 + if 'area' in poly_data: + polygon.area = poly_data['area'] + + navmesh_manager.polygons[polygon_id] = polygon + + # 解析障碍物 + if 'obstacles' in data: + for obs_data in data['obstacles']: + obstacle_id = obs_data['id'] + navmesh_manager.obstacles[obstacle_id] = { + 'position': Point3(obs_data['position'][0], obs_data['position'][1], obs_data['position'][2]), + 'radius': obs_data['radius'], + 'height': obs_data['height'], + 'type': obs_data.get('type', 'cylinder') + } + + # 更新统计信息 + navmesh_manager._update_stats() + + # 更新空间哈希 + navmesh_manager._update_spatial_hash() + + def export_to_standard_format(self, navmesh_manager, filepath: str, + format_type: str = 'obj') -> bool: + """ + 导出为标准格式 + + Args: + navmesh_manager: 导航网格管理器 + filepath: 导出文件路径 + format_type: 导出格式 ('obj', 'ply') + + Returns: + 是否导出成功 + """ + try: + if format_type.lower() == 'obj': + return self._export_to_obj(navmesh_manager, filepath) + elif format_type.lower() == 'ply': + return self._export_to_ply(navmesh_manager, filepath) + else: + print(f"不支持的导出格式: {format_type}") + return False + + except Exception as e: + print(f"导出为标准格式失败: {e}") + return False + + def _export_to_obj(self, navmesh_manager, filepath: str) -> bool: + """ + 导出为OBJ格式 + + Args: + navmesh_manager: 导航网格管理器 + filepath: 导出文件路径 + + Returns: + 是否导出成功 + """ + try: + with open(filepath, 'w', encoding='utf-8') as f: + f.write("# Navigation Mesh Export\n") + f.write(f"# Generated by EG Navigation Mesh System\n") + f.write(f"# Polygons: {len(navmesh_manager.polygons)}\n\n") + + # 写入顶点 + vertex_index = 1 + vertex_mapping = {} # 旧顶点索引到新顶点索引的映射 + + for polygon_id, polygon in navmesh_manager.polygons.items(): + for vertex in polygon.vertices: + vertex_key = (vertex.x, vertex.y, vertex.z) + if vertex_key not in vertex_mapping: + f.write(f"v {vertex.x} {vertex.y} {vertex.z}\n") + vertex_mapping[vertex_key] = vertex_index + vertex_index += 1 + + f.write("\n") + + # 写入面 + for polygon_id, polygon in navmesh_manager.polygons.items(): + f.write(f"g polygon_{polygon_id}\n") + face_vertices = [] + for vertex in polygon.vertices: + vertex_key = (vertex.x, vertex.y, vertex.z) + face_vertices.append(str(vertex_mapping[vertex_key])) + f.write(f"f {' '.join(face_vertices)}\n\n") + + return True + + except Exception as e: + print(f"导出为OBJ格式失败: {e}") + return False + + def _export_to_ply(self, navmesh_manager, filepath: str) -> bool: + """ + 导出为PLY格式 + + Args: + navmesh_manager: 导航网格管理器 + filepath: 导出文件路径 + + Returns: + 是否导出成功 + """ + try: + # 收集所有唯一顶点 + vertices = [] + vertex_mapping = {} + faces = [] + + for polygon_id, polygon in navmesh_manager.polygons.items(): + face_indices = [] + for vertex in polygon.vertices: + vertex_key = (vertex.x, vertex.y, vertex.z) + if vertex_key not in vertex_mapping: + vertex_mapping[vertex_key] = len(vertices) + vertices.append(vertex) + face_indices.append(vertex_mapping[vertex_key]) + faces.append(face_indices) + + # 写入PLY文件 + with open(filepath, 'w', encoding='utf-8') as f: + # PLY头部 + f.write("ply\n") + f.write("format ascii 1.0\n") + f.write(f"element vertex {len(vertices)}\n") + f.write("property float x\n") + f.write("property float y\n") + f.write("property float z\n") + f.write(f"element face {len(faces)}\n") + f.write("property list uchar int vertex_indices\n") + f.write("end_header\n") + + # 写入顶点 + for vertex in vertices: + f.write(f"{vertex.x} {vertex.y} {vertex.z}\n") + + # 写入面 + for face in faces: + f.write(f"{len(face)} {' '.join(map(str, face))}\n") + + return True + + except Exception as e: + print(f"导出为PLY格式失败: {e}") + return False + + def import_from_standard_format(self, filepath: str, navmesh_manager, + format_type: str = 'obj') -> bool: + """ + 从标准格式导入 + + Args: + filepath: 导入文件路径 + navmesh_manager: 导航网格管理器 + format_type: 导入格式 ('obj', 'ply') + + Returns: + 是否导入成功 + """ + try: + if format_type.lower() == 'obj': + return self._import_from_obj(filepath, navmesh_manager) + elif format_type.lower() == 'ply': + return self._import_from_ply(filepath, navmesh_manager) + else: + print(f"不支持的导入格式: {format_type}") + return False + + except Exception as e: + print(f"从标准格式导入失败: {e}") + return False + + def _import_from_obj(self, filepath: str, navmesh_manager) -> bool: + """ + 从OBJ格式导入 + + Args: + filepath: 导入文件路径 + navmesh_manager: 导航网格管理器 + + Returns: + 是否导入成功 + """ + try: + vertices = [] + faces = [] + + with open(filepath, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if not line or line.startswith('#'): + continue + + parts = line.split() + if not parts: + continue + + if parts[0] == 'v': + # 顶点 + if len(parts) >= 4: + x, y, z = float(parts[1]), float(parts[2]), float(parts[3]) + vertices.append(Point3(x, y, z)) + elif parts[0] == 'f': + # 面 + face_indices = [] + for part in parts[1:]: + # 处理索引(OBJ格式索引从1开始) + index = int(part.split('/')[0]) - 1 + face_indices.append(index) + faces.append(face_indices) + + # 创建多边形 + navmesh_manager.polygons.clear() + for i, face_indices in enumerate(faces): + polygon_vertices = [vertices[idx] for idx in face_indices if 0 <= idx < len(vertices)] + if len(polygon_vertices) >= 3: + navmesh_manager.add_polygon(polygon_vertices) + + return True + + except Exception as e: + print(f"从OBJ格式导入失败: {e}") + return False + + def _import_from_ply(self, filepath: str, navmesh_manager) -> bool: + """ + 从PLY格式导入 + + Args: + filepath: 导入文件路径 + navmesh_manager: 导航网格管理器 + + Returns: + 是否导入成功 + """ + try: + vertices = [] + faces = [] + + with open(filepath, 'r', encoding='utf-8') as f: + lines = f.readlines() + + # 解析PLY头部 + i = 0 + vertex_count = 0 + face_count = 0 + + while i < len(lines): + line = lines[i].strip() + if not line: + i += 1 + continue + + if line == "end_header": + i += 1 + break + + parts = line.split() + if len(parts) >= 3 and parts[0] == "element": + if parts[1] == "vertex": + vertex_count = int(parts[2]) + elif parts[1] == "face": + face_count = int(parts[2]) + + i += 1 + + # 读取顶点 + for j in range(vertex_count): + if i + j < len(lines): + parts = lines[i + j].strip().split() + if len(parts) >= 3: + x, y, z = float(parts[0]), float(parts[1]), float(parts[2]) + vertices.append(Point3(x, y, z)) + + i += vertex_count + + # 读取面 + for j in range(face_count): + if i + j < len(lines): + parts = lines[i + j].strip().split() + if len(parts) >= 2: + count = int(parts[0]) + face_indices = [int(idx) for idx in parts[1:1+count]] + faces.append(face_indices) + + # 创建多边形 + navmesh_manager.polygons.clear() + for face_indices in faces: + polygon_vertices = [vertices[idx] for idx in face_indices if 0 <= idx < len(vertices)] + if len(polygon_vertices) >= 3: + navmesh_manager.add_polygon(polygon_vertices) + + return True + + except Exception as e: + print(f"从PLY格式导入失败: {e}") + return False + + def get_file_info(self, filepath: str) -> Dict[str, Any]: + """ + 获取导航网格文件信息 + + Args: + filepath: 文件路径 + + Returns: + 文件信息字典 + """ + try: + info = { + 'filepath': filepath, + 'file_size': 0, + 'version': 'unknown', + 'polygon_count': 0, + 'obstacle_count': 0, + 'compressed': False + } + + import os + info['file_size'] = os.path.getsize(filepath) + + # 读取文件内容 + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read(1024) # 只读取前1024个字符 + + # 尝试解析 + try: + data = json.loads(content) + if isinstance(data, dict): + if 'compressed' in data: + info['compressed'] = data['compressed'] + info['version'] = data.get('version', 'unknown') + elif 'metadata' in data: + info['version'] = data['metadata'].get('version', 'unknown') + info['polygon_count'] = data['metadata'].get('polygon_count', 0) + info['obstacle_count'] = data['metadata'].get('obstacle_count', 0) + except json.JSONDecodeError: + pass + + return info + + except Exception as e: + print(f"获取文件信息失败: {e}") return {} \ No newline at end of file diff --git a/plugins/user/network_sync/README.md b/plugins/user/network_sync/README.md index 91948153..863deda3 100644 --- a/plugins/user/network_sync/README.md +++ b/plugins/user/network_sync/README.md @@ -1,258 +1,258 @@ -# 网络同步插件 - -## 概述 - -网络同步插件为EG引擎提供了完整的网络同步功能,支持客户端-服务器架构、实时对象同步、时钟同步、预测与插值等高级功能。该插件采用模块化设计,具有高性能、可扩展和易配置的特点。 - -## 功能特性 - -### 1. 核心功能 -- **网络通信**: 完整的TCP/IP网络通信实现 -- **对象同步**: 实时游戏对象状态同步 -- **协议处理**: 自定义网络协议支持 -- **连接管理**: 客户端和服务器连接管理 -- **数据序列化**: 高效的数据序列化和反序列化 -- **时钟同步**: 精确的客户端-服务器时钟同步 -- **预测管理**: 网络延迟下的对象状态预测 -- **配置管理**: 灵活的配置系统 -- **事件处理**: 完整的事件系统 -- **加密支持**: 数据加密和安全传输 -- **可视化编辑器**: 图形化配置和监控界面 - -### 2. 高级功能 -- **插值与外推**: 平滑的对象运动效果 -- **增量压缩**: 减少网络带宽使用 -- **心跳检测**: 连接状态监控 -- **断线重连**: 自动连接恢复 -- **数据缓存**: 提高处理效率 -- **性能监控**: 实时性能数据展示 -- **错误处理**: 完善的异常处理机制 - -## 模块架构 - -``` -network_sync/ -├── plugin.py # 插件主入口 -├── core/ -│ └── sync_manager.py # 同步管理器 -├── network/ -│ └── network_manager.py # 网络管理器 -├── protocol/ -│ └── protocol_handler.py # 协议处理器 -├── sync/ -│ └── object_sync.py # 对象同步器 -├── connection/ -│ └── connection_manager.py # 连接管理器 -├── serialization/ -│ └── data_serializer.py # 数据序列化器 -├── clock/ -│ └── clock_sync.py # 时钟同步器 -├── prediction/ -│ └── prediction_manager.py # 预测管理器 -├── config/ -│ └── sync_config.py # 配置管理器 -├── editor/ -│ └── sync_editor.py # 可视化编辑器 -├── events/ -│ └── event_handler.py # 事件处理器 -├── encryption/ -│ └── encryption_manager.py # 加密管理器 -├── utils/ -│ └── network_utils.py # 网络工具类 -└── README.md # 说明文档 -``` - -## 安装与配置 - -### 安装依赖 - -```bash -pip install cryptography psutil -``` - -### 基本配置 - -在插件配置文件中设置基本参数: - -```python -# 网络配置 -network_config = { - "server_host": "localhost", - "server_port": 7777, - "max_connections": 32, - "connection_timeout": 30.0 -} - -# 同步配置 -sync_config = { - "sync_rate": 20, # 每秒20次同步 - "interpolation_rate": 60, - "enable_delta_compression": True -} -``` - -## 使用方法 - -### 1. 初始化插件 - -```python -from plugins.user.network_sync.plugin import NetworkSyncPlugin - -# 创建插件实例 -plugin = NetworkSyncPlugin(engine) - -# 初始化插件 -if plugin.initialize(): - print("插件初始化成功") - -# 启用插件 -if plugin.enable(): - print("插件已启用") -``` - -### 2. 启动服务器 - -```python -# 启动服务器 -if plugin.start_server(port=7777): - print("服务器启动成功") -``` - -### 3. 连接服务器 - -```python -# 连接服务器 -if plugin.connect_to_server("localhost", 7777): - print("连接服务器成功") -``` - -### 4. 同步对象 - -```python -# 注册本地对象 -object_data = { - "position": [0.0, 0.0, 0.0], - "rotation": [0.0, 0.0, 0.0], - "scale": [1.0, 1.0, 1.0] -} - -plugin.sync_manager.register_local_object("player_1", object_data) - -# 同步对象状态 -plugin.sync_object("player_1", object_data) -``` - -## API参考 - -### 主要类和方法 - -#### NetworkSyncPlugin -- `initialize()`: 初始化插件 -- `enable()`: 启用插件 -- `disable()`: 禁用插件 -- `start_server(port)`: 启动服务器 -- `connect_to_server(host, port)`: 连接服务器 -- `sync_object(obj_id, obj_data)`: 同步对象 - -#### SyncManager -- `register_local_object(obj_id, obj_data)`: 注册本地对象 -- `register_remote_object(obj_id, obj_data)`: 注册远程对象 -- `update_object_data(obj_id, obj_data)`: 更新对象数据 -- `get_object_data(obj_id)`: 获取对象数据 - -#### NetworkManager -- `send_sync_data(data, target_client_id)`: 发送同步数据 -- `get_connection_state()`: 获取连接状态 -- `get_network_stats()`: 获取网络统计 - -#### ObjectSync -- `sync_object(obj_id, obj_data)`: 同步对象 -- `receive_sync_data(obj_id, obj_data)`: 接收同步数据 -- `get_local_objects()`: 获取本地对象 -- `get_remote_objects()`: 获取远程对象 - -## 性能优化 - -### 1. 带宽优化 -- 启用增量压缩减少数据传输 -- 使用数据缓存避免重复传输 -- 实现智能同步频率调节 - -### 2. CPU优化 -- 多线程处理网络通信 -- 对象池减少内存分配 -- 智能更新检测避免无用计算 - -### 3. 内存优化 -- 及时清理过期数据 -- 使用高效的数据结构 -- 实现LRU缓存策略 - -## 安全特性 - -### 1. 数据加密 -- 支持AES加密算法 -- 会话密钥管理 -- 密钥轮换机制 - -### 2. 身份验证 -- 用户名/密码验证 -- 令牌认证 -- 连接授权 - -### 3. 数据完整性 -- 消息认证码(MAC) -- 数据校验和 -- 防篡改检测 - -## 故障排除 - -### 常见问题 - -1. **连接失败** - - 检查服务器地址和端口 - - 确认防火墙设置 - - 查看网络连接状态 - -2. **同步延迟** - - 检查网络带宽 - - 调整同步频率 - - 优化数据传输 - -3. **性能问题** - - 监控CPU和内存使用 - - 减少同步对象数量 - - 调整插值参数 - -### 日志查看 - -```python -# 获取插件统计信息 -stats = plugin.get_stats() -print(f"发送字节: {stats['bytes_sent']}") -print(f"接收字节: {stats['bytes_received']}") -``` - -## 扩展开发 - -### 添加新功能模块 - -1. 创建新的模块文件 -2. 实现标准接口方法 -3. 在主插件中注册模块 -4. 添加配置选项 - -### 自定义协议 - -1. 扩展ProtocolHandler类 -2. 添加新的消息类型 -3. 实现消息处理器 -4. 更新协议版本 - -## 许可证 - -本插件采用MIT许可证,详情请查看LICENSE文件。 - -## 联系方式 - +# 网络同步插件 + +## 概述 + +网络同步插件为EG引擎提供了完整的网络同步功能,支持客户端-服务器架构、实时对象同步、时钟同步、预测与插值等高级功能。该插件采用模块化设计,具有高性能、可扩展和易配置的特点。 + +## 功能特性 + +### 1. 核心功能 +- **网络通信**: 完整的TCP/IP网络通信实现 +- **对象同步**: 实时游戏对象状态同步 +- **协议处理**: 自定义网络协议支持 +- **连接管理**: 客户端和服务器连接管理 +- **数据序列化**: 高效的数据序列化和反序列化 +- **时钟同步**: 精确的客户端-服务器时钟同步 +- **预测管理**: 网络延迟下的对象状态预测 +- **配置管理**: 灵活的配置系统 +- **事件处理**: 完整的事件系统 +- **加密支持**: 数据加密和安全传输 +- **可视化编辑器**: 图形化配置和监控界面 + +### 2. 高级功能 +- **插值与外推**: 平滑的对象运动效果 +- **增量压缩**: 减少网络带宽使用 +- **心跳检测**: 连接状态监控 +- **断线重连**: 自动连接恢复 +- **数据缓存**: 提高处理效率 +- **性能监控**: 实时性能数据展示 +- **错误处理**: 完善的异常处理机制 + +## 模块架构 + +``` +network_sync/ +├── plugin.py # 插件主入口 +├── core/ +│ └── sync_manager.py # 同步管理器 +├── network/ +│ └── network_manager.py # 网络管理器 +├── protocol/ +│ └── protocol_handler.py # 协议处理器 +├── sync/ +│ └── object_sync.py # 对象同步器 +├── connection/ +│ └── connection_manager.py # 连接管理器 +├── serialization/ +│ └── data_serializer.py # 数据序列化器 +├── clock/ +│ └── clock_sync.py # 时钟同步器 +├── prediction/ +│ └── prediction_manager.py # 预测管理器 +├── config/ +│ └── sync_config.py # 配置管理器 +├── editor/ +│ └── sync_editor.py # 可视化编辑器 +├── events/ +│ └── event_handler.py # 事件处理器 +├── encryption/ +│ └── encryption_manager.py # 加密管理器 +├── utils/ +│ └── network_utils.py # 网络工具类 +└── README.md # 说明文档 +``` + +## 安装与配置 + +### 安装依赖 + +```bash +pip install cryptography psutil +``` + +### 基本配置 + +在插件配置文件中设置基本参数: + +```python +# 网络配置 +network_config = { + "server_host": "localhost", + "server_port": 7777, + "max_connections": 32, + "connection_timeout": 30.0 +} + +# 同步配置 +sync_config = { + "sync_rate": 20, # 每秒20次同步 + "interpolation_rate": 60, + "enable_delta_compression": True +} +``` + +## 使用方法 + +### 1. 初始化插件 + +```python +from plugins.user.network_sync.plugin import NetworkSyncPlugin + +# 创建插件实例 +plugin = NetworkSyncPlugin(engine) + +# 初始化插件 +if plugin.initialize(): + print("插件初始化成功") + +# 启用插件 +if plugin.enable(): + print("插件已启用") +``` + +### 2. 启动服务器 + +```python +# 启动服务器 +if plugin.start_server(port=7777): + print("服务器启动成功") +``` + +### 3. 连接服务器 + +```python +# 连接服务器 +if plugin.connect_to_server("localhost", 7777): + print("连接服务器成功") +``` + +### 4. 同步对象 + +```python +# 注册本地对象 +object_data = { + "position": [0.0, 0.0, 0.0], + "rotation": [0.0, 0.0, 0.0], + "scale": [1.0, 1.0, 1.0] +} + +plugin.sync_manager.register_local_object("player_1", object_data) + +# 同步对象状态 +plugin.sync_object("player_1", object_data) +``` + +## API参考 + +### 主要类和方法 + +#### NetworkSyncPlugin +- `initialize()`: 初始化插件 +- `enable()`: 启用插件 +- `disable()`: 禁用插件 +- `start_server(port)`: 启动服务器 +- `connect_to_server(host, port)`: 连接服务器 +- `sync_object(obj_id, obj_data)`: 同步对象 + +#### SyncManager +- `register_local_object(obj_id, obj_data)`: 注册本地对象 +- `register_remote_object(obj_id, obj_data)`: 注册远程对象 +- `update_object_data(obj_id, obj_data)`: 更新对象数据 +- `get_object_data(obj_id)`: 获取对象数据 + +#### NetworkManager +- `send_sync_data(data, target_client_id)`: 发送同步数据 +- `get_connection_state()`: 获取连接状态 +- `get_network_stats()`: 获取网络统计 + +#### ObjectSync +- `sync_object(obj_id, obj_data)`: 同步对象 +- `receive_sync_data(obj_id, obj_data)`: 接收同步数据 +- `get_local_objects()`: 获取本地对象 +- `get_remote_objects()`: 获取远程对象 + +## 性能优化 + +### 1. 带宽优化 +- 启用增量压缩减少数据传输 +- 使用数据缓存避免重复传输 +- 实现智能同步频率调节 + +### 2. CPU优化 +- 多线程处理网络通信 +- 对象池减少内存分配 +- 智能更新检测避免无用计算 + +### 3. 内存优化 +- 及时清理过期数据 +- 使用高效的数据结构 +- 实现LRU缓存策略 + +## 安全特性 + +### 1. 数据加密 +- 支持AES加密算法 +- 会话密钥管理 +- 密钥轮换机制 + +### 2. 身份验证 +- 用户名/密码验证 +- 令牌认证 +- 连接授权 + +### 3. 数据完整性 +- 消息认证码(MAC) +- 数据校验和 +- 防篡改检测 + +## 故障排除 + +### 常见问题 + +1. **连接失败** + - 检查服务器地址和端口 + - 确认防火墙设置 + - 查看网络连接状态 + +2. **同步延迟** + - 检查网络带宽 + - 调整同步频率 + - 优化数据传输 + +3. **性能问题** + - 监控CPU和内存使用 + - 减少同步对象数量 + - 调整插值参数 + +### 日志查看 + +```python +# 获取插件统计信息 +stats = plugin.get_stats() +print(f"发送字节: {stats['bytes_sent']}") +print(f"接收字节: {stats['bytes_received']}") +``` + +## 扩展开发 + +### 添加新功能模块 + +1. 创建新的模块文件 +2. 实现标准接口方法 +3. 在主插件中注册模块 +4. 添加配置选项 + +### 自定义协议 + +1. 扩展ProtocolHandler类 +2. 添加新的消息类型 +3. 实现消息处理器 +4. 更新协议版本 + +## 许可证 + +本插件采用MIT许可证,详情请查看LICENSE文件。 + +## 联系方式 + 如有问题或建议,请联系开发团队。 \ No newline at end of file diff --git a/plugins/user/network_sync/clock/clock_sync.py b/plugins/user/network_sync/clock/clock_sync.py index aa84fcbc..2061ed91 100644 --- a/plugins/user/network_sync/clock/clock_sync.py +++ b/plugins/user/network_sync/clock/clock_sync.py @@ -1,621 +1,621 @@ -""" -时钟同步模块 -负责客户端与服务器之间的时间同步 -""" - -import time -from typing import Dict, Any, List, Optional -import threading - -class ClockSync: - """ - 时钟同步器 - 负责客户端与服务器之间的时间同步 - """ - - def __init__(self, plugin): - """ - 初始化时钟同步器 - - Args: - plugin: 网络同步插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 时钟同步配置 - self.clock_config = { - 'sync_interval': 5.0, # 同步间隔(秒) - 'samples_per_sync': 10, # 每次同步的样本数 - 'max_offset_samples': 100, # 最大偏移样本数 - 'enable_interpolation': True, # 启用时间插值 - 'interpolation_rate': 60.0, # 插值更新频率 - 'max_drift_correction': 0.1, # 最大漂移校正(秒/秒) - 'min_sync_quality': 0.8, # 最小同步质量 - 'timeout': 10.0 # 同步超时时间 - } - - # 时钟状态 - self.clock_state = { - 'is_synchronized': False, - 'time_offset': 0.0, # 本地时间与服务器时间的偏移 - 'time_drift': 0.0, # 时间漂移率 - 'last_sync_time': 0.0, # 最后同步时间 - 'sync_quality': 0.0, # 同步质量 - 'round_trip_time': 0.0, # 往返时间 - 'sync_samples': [] # 同步样本 - } - - # 插值状态 - self.interpolation_state = { - 'interpolated_time': 0.0, - 'target_time': 0.0, - 'interpolation_start': 0.0, - 'interpolation_duration': 0.0 - } - - # 时钟统计 - self.clock_stats = { - 'sync_attempts': 0, - 'successful_syncs': 0, - 'failed_syncs': 0, - 'average_rtt': 0.0, - 'min_rtt': float('inf'), - 'max_rtt': 0.0, - 'sync_errors': 0 - } - - # 同步请求队列 - self.sync_requests = [] - self.sync_lock = threading.Lock() - - # 回调函数 - self.clock_callbacks = { - 'sync_started': [], - 'sync_completed': [], - 'sync_failed': [], - 'time_updated': [], - 'sync_quality_changed': [] - } - - # 时间戳记录 - self.last_sync_attempt = 0.0 - self.last_time_update = 0.0 - self.last_interpolation_update = 0.0 - - print("✓ 时钟同步器已创建") - - def initialize(self) -> bool: - """ - 初始化时钟同步器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 时钟同步器初始化完成") - return True - - except Exception as e: - print(f"✗ 时钟同步器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用时钟同步器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 时钟同步器未初始化") - return False - - self.enabled = True - print("✓ 时钟同步器已启用") - return True - - except Exception as e: - print(f"✗ 时钟同步器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用时钟同步器""" - try: - self.enabled = False - print("✓ 时钟同步器已禁用") - - except Exception as e: - print(f"✗ 时钟同步器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理时钟同步器资源""" - try: - self.disable() - self.sync_requests.clear() - self.clock_callbacks.clear() - self.initialized = False - print("✓ 时钟同步器资源已清理") - - except Exception as e: - print(f"✗ 时钟同步器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新时钟同步器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 定期发起时钟同步 - if current_time - self.last_sync_attempt >= self.clock_config['sync_interval']: - self._request_time_sync() - self.last_sync_attempt = current_time - - # 更新时间插值 - if self.clock_config['enable_interpolation']: - self._update_time_interpolation(current_time) - - self.last_time_update = current_time - - except Exception as e: - print(f"✗ 时钟同步器更新失败: {e}") - import traceback - traceback.print_exc() - - def _request_time_sync(self): - """请求时间同步""" - try: - if not self.plugin.network_manager or not self.plugin.protocol_handler: - return - - # 增加同步尝试计数 - self.clock_stats['sync_attempts'] += 1 - - # 触发同步开始回调 - self._trigger_clock_callback('sync_started', { - 'timestamp': time.time() - }) - - # 发送时间同步请求 - sync_data = { - 'client_time': time.time(), - 'request_id': f"sync_{int(time.time() * 1000000)}" - } - - success = self.plugin.protocol_handler.send_message('SYNC_REQUEST', sync_data) - if success: - # 记录同步请求 - with self.sync_lock: - self.sync_requests.append({ - 'request_id': sync_data['request_id'], - 'send_time': time.time(), - 'completed': False - }) - else: - self.clock_stats['failed_syncs'] += 1 - self._trigger_clock_callback('sync_failed', { - 'reason': 'send_failed', - 'timestamp': time.time() - }) - - except Exception as e: - print(f"✗ 时间同步请求失败: {e}") - self.clock_stats['sync_errors'] += 1 - - def handle_sync_request(self, data: Dict[str, Any], client_id: str = None): - """ - 处理时间同步请求 - - Args: - data: 同步请求数据 - client_id: 客户端ID - """ - try: - # 构造同步响应 - response_data = { - 'client_time': data.get('client_time', 0), - 'server_time': time.time(), - 'request_id': data.get('request_id', ''), - 'client_id': client_id - } - - # 发送同步响应 - if self.plugin.protocol_handler: - self.plugin.protocol_handler.send_message('SYNC_RESPONSE', response_data, client_id) - - except Exception as e: - print(f"✗ 时间同步请求处理失败: {e}") - - def handle_sync_response(self, data: Dict[str, Any]): - """ - 处理时间同步响应 - - Args: - data: 同步响应数据 - """ - try: - client_send_time = data.get('client_time', 0) - server_time = data.get('server_time', 0) - request_id = data.get('request_id', '') - receive_time = time.time() - - # 计算往返时间 - rtt = receive_time - client_send_time - - # 计算时间偏移(假设网络延迟对称) - time_offset = server_time - (client_send_time + rtt / 2) - - # 更新统计信息 - self.clock_stats['successful_syncs'] += 1 - self.clock_stats['average_rtt'] = ( - (self.clock_stats['average_rtt'] * (self.clock_stats['successful_syncs'] - 1) + rtt) / - self.clock_stats['successful_syncs'] - ) - self.clock_stats['min_rtt'] = min(self.clock_stats['min_rtt'], rtt) - self.clock_stats['max_rtt'] = max(self.clock_stats['max_rtt'], rtt) - - # 添加同步样本 - sample = { - 'offset': time_offset, - 'rtt': rtt, - 'timestamp': receive_time - } - - with self.sync_lock: - self.clock_state['sync_samples'].append(sample) - - # 保持样本数量在限制范围内 - if len(self.clock_state['sync_samples']) > self.clock_config['max_offset_samples']: - self.clock_state['sync_samples'].pop(0) - - # 更新时钟状态 - self._update_clock_state() - - # 标记请求完成 - with self.sync_lock: - for request in self.sync_requests: - if request['request_id'] == request_id: - request['completed'] = True - break - - # 触发同步完成回调 - self._trigger_clock_callback('sync_completed', { - 'offset': time_offset, - 'rtt': rtt, - 'quality': self.clock_state['sync_quality'], - 'timestamp': receive_time - }) - - self.clock_state['last_sync_time'] = receive_time - - except Exception as e: - print(f"✗ 时间同步响应处理失败: {e}") - self.clock_stats['sync_errors'] += 1 - self._trigger_clock_callback('sync_failed', { - 'reason': 'process_failed', - 'error': str(e), - 'timestamp': time.time() - }) - - def _update_clock_state(self): - """更新时钟状态""" - try: - with self.sync_lock: - if not self.clock_state['sync_samples']: - return - - # 计算平均时间偏移 - offsets = [sample['offset'] for sample in self.clock_state['sync_samples']] - avg_offset = sum(offsets) / len(offsets) - - # 计算时间偏移的标准差(用于质量评估) - if len(offsets) > 1: - variance = sum((offset - avg_offset) ** 2 for offset in offsets) / (len(offsets) - 1) - std_dev = variance ** 0.5 - else: - std_dev = 0.0 - - # 计算同步质量(基于标准差和RTT) - if std_dev < 0.001: # 小于1毫秒的标准差 - quality = 1.0 - else: - # 质量与标准差成反比 - quality = max(0.0, 1.0 - (std_dev * 1000)) # 转换为毫秒 - - # 计算平均RTT - rtts = [sample['rtt'] for sample in self.clock_state['sync_samples']] - avg_rtt = sum(rtts) / len(rtts) - - # 更新时钟状态 - self.clock_state['time_offset'] = avg_offset - self.clock_state['round_trip_time'] = avg_rtt - self.clock_state['sync_quality'] = quality - self.clock_state['is_synchronized'] = quality >= self.clock_config['min_sync_quality'] - - # 触发同步质量变化回调 - self._trigger_clock_callback('sync_quality_changed', { - 'quality': quality, - 'offset': avg_offset, - 'rtt': avg_rtt - }) - - except Exception as e: - print(f"✗ 时钟状态更新失败: {e}") - - def _update_time_interpolation(self, current_time: float): - """ - 更新时间插值 - - Args: - current_time: 当前时间 - """ - try: - if not self.clock_config['enable_interpolation']: - return - - # 计算插值时间 - server_time = self.get_server_time() - - if self.interpolation_state['interpolated_time'] == 0.0: - # 初始化插值 - self.interpolation_state['interpolated_time'] = server_time - self.interpolation_state['target_time'] = server_time - else: - # 更新目标时间 - self.interpolation_state['target_time'] = server_time - - # 执行插值 - if self.interpolation_state['interpolated_time'] != self.interpolation_state['target_time']: - interpolation_speed = self.clock_config['interpolation_rate'] - time_diff = self.interpolation_state['target_time'] - self.interpolation_state['interpolated_time'] - max_step = interpolation_speed * (1.0 / 60.0) # 假设60FPS - - if abs(time_diff) > max_step: - # 逐步接近目标时间 - step = max_step if time_diff > 0 else -max_step - self.interpolation_state['interpolated_time'] += step - else: - # 直接设置为目标时间 - self.interpolation_state['interpolated_time'] = self.interpolation_state['target_time'] - - # 触发时间更新回调 - self._trigger_clock_callback('time_updated', { - 'server_time': server_time, - 'interpolated_time': self.interpolation_state['interpolated_time'], - 'local_time': current_time - }) - - self.last_interpolation_update = current_time - - except Exception as e: - print(f"✗ 时间插值更新失败: {e}") - - def get_server_time(self) -> float: - """ - 获取服务器时间 - - Returns: - 服务器时间戳 - """ - try: - local_time = time.time() - return local_time + self.clock_state['time_offset'] - except Exception as e: - print(f"✗ 服务器时间获取失败: {e}") - return time.time() - - def get_interpolated_time(self) -> float: - """ - 获取插值时间 - - Returns: - 插值时间戳 - """ - try: - if self.clock_config['enable_interpolation'] and self.interpolation_state['interpolated_time'] != 0.0: - return self.interpolation_state['interpolated_time'] - else: - return self.get_server_time() - except Exception as e: - print(f"✗ 插值时间获取失败: {e}") - return time.time() - - def get_time_offset(self) -> float: - """ - 获取时间偏移 - - Returns: - 时间偏移值 - """ - return self.clock_state['time_offset'] - - def get_sync_quality(self) -> float: - """ - 获取同步质量 - - Returns: - 同步质量值 (0.0-1.0) - """ - return self.clock_state['sync_quality'] - - def is_synchronized(self) -> bool: - """ - 检查是否已同步 - - Returns: - 是否已同步 - """ - return self.clock_state['is_synchronized'] - - def get_round_trip_time(self) -> float: - """ - 获取往返时间 - - Returns: - 往返时间(秒) - """ - return self.clock_state['round_trip_time'] - - def get_clock_stats(self) -> Dict[str, Any]: - """ - 获取时钟统计信息 - - Returns: - 时钟统计字典 - """ - return self.clock_stats.copy() - - def reset_clock_stats(self): - """重置时钟统计信息""" - try: - self.clock_stats = { - 'sync_attempts': 0, - 'successful_syncs': 0, - 'failed_syncs': 0, - 'average_rtt': 0.0, - 'min_rtt': float('inf'), - 'max_rtt': 0.0, - 'sync_errors': 0 - } - print("✓ 时钟统计信息已重置") - except Exception as e: - print(f"✗ 时钟统计信息重置失败: {e}") - - def force_sync(self) -> bool: - """ - 强制进行时钟同步 - - Returns: - 是否成功发起同步 - """ - try: - self._request_time_sync() - self.last_sync_attempt = time.time() - print("✓ 强制时钟同步已发起") - return True - except Exception as e: - print(f"✗ 强制时钟同步失败: {e}") - return False - - def reset_sync(self): - """重置时钟同步状态""" - try: - with self.sync_lock: - self.clock_state = { - 'is_synchronized': False, - 'time_offset': 0.0, - 'time_drift': 0.0, - 'last_sync_time': 0.0, - 'sync_quality': 0.0, - 'round_trip_time': 0.0, - 'sync_samples': [] - } - - self.interpolation_state = { - 'interpolated_time': 0.0, - 'target_time': 0.0, - 'interpolation_start': 0.0, - 'interpolation_duration': 0.0 - } - - self.sync_requests.clear() - - print("✓ 时钟同步状态已重置") - except Exception as e: - print(f"✗ 时钟同步状态重置失败: {e}") - - def set_clock_config(self, config: Dict[str, Any]) -> bool: - """ - 设置时钟配置 - - Args: - config: 时钟配置字典 - - Returns: - 是否设置成功 - """ - try: - self.clock_config.update(config) - print(f"✓ 时钟配置已更新: {self.clock_config}") - return True - except Exception as e: - print(f"✗ 时钟配置设置失败: {e}") - return False - - def get_clock_config(self) -> Dict[str, Any]: - """ - 获取时钟配置 - - Returns: - 时钟配置字典 - """ - return self.clock_config.copy() - - def _trigger_clock_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发时钟回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.clock_callbacks: - for callback in self.clock_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 时钟回调执行失败: {e}") - except Exception as e: - print(f"✗ 时钟回调触发失败: {e}") - - def register_clock_callback(self, callback_type: str, callback: callable): - """ - 注册时钟回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.clock_callbacks: - self.clock_callbacks[callback_type].append(callback) - print(f"✓ 时钟回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 时钟回调注册失败: {e}") - - def unregister_clock_callback(self, callback_type: str, callback: callable): - """ - 注销时钟回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.clock_callbacks: - if callback in self.clock_callbacks[callback_type]: - self.clock_callbacks[callback_type].remove(callback) - print(f"✓ 时钟回调已注销: {callback_type}") - except Exception as e: +""" +时钟同步模块 +负责客户端与服务器之间的时间同步 +""" + +import time +from typing import Dict, Any, List, Optional +import threading + +class ClockSync: + """ + 时钟同步器 + 负责客户端与服务器之间的时间同步 + """ + + def __init__(self, plugin): + """ + 初始化时钟同步器 + + Args: + plugin: 网络同步插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 时钟同步配置 + self.clock_config = { + 'sync_interval': 5.0, # 同步间隔(秒) + 'samples_per_sync': 10, # 每次同步的样本数 + 'max_offset_samples': 100, # 最大偏移样本数 + 'enable_interpolation': True, # 启用时间插值 + 'interpolation_rate': 60.0, # 插值更新频率 + 'max_drift_correction': 0.1, # 最大漂移校正(秒/秒) + 'min_sync_quality': 0.8, # 最小同步质量 + 'timeout': 10.0 # 同步超时时间 + } + + # 时钟状态 + self.clock_state = { + 'is_synchronized': False, + 'time_offset': 0.0, # 本地时间与服务器时间的偏移 + 'time_drift': 0.0, # 时间漂移率 + 'last_sync_time': 0.0, # 最后同步时间 + 'sync_quality': 0.0, # 同步质量 + 'round_trip_time': 0.0, # 往返时间 + 'sync_samples': [] # 同步样本 + } + + # 插值状态 + self.interpolation_state = { + 'interpolated_time': 0.0, + 'target_time': 0.0, + 'interpolation_start': 0.0, + 'interpolation_duration': 0.0 + } + + # 时钟统计 + self.clock_stats = { + 'sync_attempts': 0, + 'successful_syncs': 0, + 'failed_syncs': 0, + 'average_rtt': 0.0, + 'min_rtt': float('inf'), + 'max_rtt': 0.0, + 'sync_errors': 0 + } + + # 同步请求队列 + self.sync_requests = [] + self.sync_lock = threading.Lock() + + # 回调函数 + self.clock_callbacks = { + 'sync_started': [], + 'sync_completed': [], + 'sync_failed': [], + 'time_updated': [], + 'sync_quality_changed': [] + } + + # 时间戳记录 + self.last_sync_attempt = 0.0 + self.last_time_update = 0.0 + self.last_interpolation_update = 0.0 + + print("✓ 时钟同步器已创建") + + def initialize(self) -> bool: + """ + 初始化时钟同步器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 时钟同步器初始化完成") + return True + + except Exception as e: + print(f"✗ 时钟同步器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用时钟同步器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 时钟同步器未初始化") + return False + + self.enabled = True + print("✓ 时钟同步器已启用") + return True + + except Exception as e: + print(f"✗ 时钟同步器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用时钟同步器""" + try: + self.enabled = False + print("✓ 时钟同步器已禁用") + + except Exception as e: + print(f"✗ 时钟同步器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理时钟同步器资源""" + try: + self.disable() + self.sync_requests.clear() + self.clock_callbacks.clear() + self.initialized = False + print("✓ 时钟同步器资源已清理") + + except Exception as e: + print(f"✗ 时钟同步器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新时钟同步器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 定期发起时钟同步 + if current_time - self.last_sync_attempt >= self.clock_config['sync_interval']: + self._request_time_sync() + self.last_sync_attempt = current_time + + # 更新时间插值 + if self.clock_config['enable_interpolation']: + self._update_time_interpolation(current_time) + + self.last_time_update = current_time + + except Exception as e: + print(f"✗ 时钟同步器更新失败: {e}") + import traceback + traceback.print_exc() + + def _request_time_sync(self): + """请求时间同步""" + try: + if not self.plugin.network_manager or not self.plugin.protocol_handler: + return + + # 增加同步尝试计数 + self.clock_stats['sync_attempts'] += 1 + + # 触发同步开始回调 + self._trigger_clock_callback('sync_started', { + 'timestamp': time.time() + }) + + # 发送时间同步请求 + sync_data = { + 'client_time': time.time(), + 'request_id': f"sync_{int(time.time() * 1000000)}" + } + + success = self.plugin.protocol_handler.send_message('SYNC_REQUEST', sync_data) + if success: + # 记录同步请求 + with self.sync_lock: + self.sync_requests.append({ + 'request_id': sync_data['request_id'], + 'send_time': time.time(), + 'completed': False + }) + else: + self.clock_stats['failed_syncs'] += 1 + self._trigger_clock_callback('sync_failed', { + 'reason': 'send_failed', + 'timestamp': time.time() + }) + + except Exception as e: + print(f"✗ 时间同步请求失败: {e}") + self.clock_stats['sync_errors'] += 1 + + def handle_sync_request(self, data: Dict[str, Any], client_id: str = None): + """ + 处理时间同步请求 + + Args: + data: 同步请求数据 + client_id: 客户端ID + """ + try: + # 构造同步响应 + response_data = { + 'client_time': data.get('client_time', 0), + 'server_time': time.time(), + 'request_id': data.get('request_id', ''), + 'client_id': client_id + } + + # 发送同步响应 + if self.plugin.protocol_handler: + self.plugin.protocol_handler.send_message('SYNC_RESPONSE', response_data, client_id) + + except Exception as e: + print(f"✗ 时间同步请求处理失败: {e}") + + def handle_sync_response(self, data: Dict[str, Any]): + """ + 处理时间同步响应 + + Args: + data: 同步响应数据 + """ + try: + client_send_time = data.get('client_time', 0) + server_time = data.get('server_time', 0) + request_id = data.get('request_id', '') + receive_time = time.time() + + # 计算往返时间 + rtt = receive_time - client_send_time + + # 计算时间偏移(假设网络延迟对称) + time_offset = server_time - (client_send_time + rtt / 2) + + # 更新统计信息 + self.clock_stats['successful_syncs'] += 1 + self.clock_stats['average_rtt'] = ( + (self.clock_stats['average_rtt'] * (self.clock_stats['successful_syncs'] - 1) + rtt) / + self.clock_stats['successful_syncs'] + ) + self.clock_stats['min_rtt'] = min(self.clock_stats['min_rtt'], rtt) + self.clock_stats['max_rtt'] = max(self.clock_stats['max_rtt'], rtt) + + # 添加同步样本 + sample = { + 'offset': time_offset, + 'rtt': rtt, + 'timestamp': receive_time + } + + with self.sync_lock: + self.clock_state['sync_samples'].append(sample) + + # 保持样本数量在限制范围内 + if len(self.clock_state['sync_samples']) > self.clock_config['max_offset_samples']: + self.clock_state['sync_samples'].pop(0) + + # 更新时钟状态 + self._update_clock_state() + + # 标记请求完成 + with self.sync_lock: + for request in self.sync_requests: + if request['request_id'] == request_id: + request['completed'] = True + break + + # 触发同步完成回调 + self._trigger_clock_callback('sync_completed', { + 'offset': time_offset, + 'rtt': rtt, + 'quality': self.clock_state['sync_quality'], + 'timestamp': receive_time + }) + + self.clock_state['last_sync_time'] = receive_time + + except Exception as e: + print(f"✗ 时间同步响应处理失败: {e}") + self.clock_stats['sync_errors'] += 1 + self._trigger_clock_callback('sync_failed', { + 'reason': 'process_failed', + 'error': str(e), + 'timestamp': time.time() + }) + + def _update_clock_state(self): + """更新时钟状态""" + try: + with self.sync_lock: + if not self.clock_state['sync_samples']: + return + + # 计算平均时间偏移 + offsets = [sample['offset'] for sample in self.clock_state['sync_samples']] + avg_offset = sum(offsets) / len(offsets) + + # 计算时间偏移的标准差(用于质量评估) + if len(offsets) > 1: + variance = sum((offset - avg_offset) ** 2 for offset in offsets) / (len(offsets) - 1) + std_dev = variance ** 0.5 + else: + std_dev = 0.0 + + # 计算同步质量(基于标准差和RTT) + if std_dev < 0.001: # 小于1毫秒的标准差 + quality = 1.0 + else: + # 质量与标准差成反比 + quality = max(0.0, 1.0 - (std_dev * 1000)) # 转换为毫秒 + + # 计算平均RTT + rtts = [sample['rtt'] for sample in self.clock_state['sync_samples']] + avg_rtt = sum(rtts) / len(rtts) + + # 更新时钟状态 + self.clock_state['time_offset'] = avg_offset + self.clock_state['round_trip_time'] = avg_rtt + self.clock_state['sync_quality'] = quality + self.clock_state['is_synchronized'] = quality >= self.clock_config['min_sync_quality'] + + # 触发同步质量变化回调 + self._trigger_clock_callback('sync_quality_changed', { + 'quality': quality, + 'offset': avg_offset, + 'rtt': avg_rtt + }) + + except Exception as e: + print(f"✗ 时钟状态更新失败: {e}") + + def _update_time_interpolation(self, current_time: float): + """ + 更新时间插值 + + Args: + current_time: 当前时间 + """ + try: + if not self.clock_config['enable_interpolation']: + return + + # 计算插值时间 + server_time = self.get_server_time() + + if self.interpolation_state['interpolated_time'] == 0.0: + # 初始化插值 + self.interpolation_state['interpolated_time'] = server_time + self.interpolation_state['target_time'] = server_time + else: + # 更新目标时间 + self.interpolation_state['target_time'] = server_time + + # 执行插值 + if self.interpolation_state['interpolated_time'] != self.interpolation_state['target_time']: + interpolation_speed = self.clock_config['interpolation_rate'] + time_diff = self.interpolation_state['target_time'] - self.interpolation_state['interpolated_time'] + max_step = interpolation_speed * (1.0 / 60.0) # 假设60FPS + + if abs(time_diff) > max_step: + # 逐步接近目标时间 + step = max_step if time_diff > 0 else -max_step + self.interpolation_state['interpolated_time'] += step + else: + # 直接设置为目标时间 + self.interpolation_state['interpolated_time'] = self.interpolation_state['target_time'] + + # 触发时间更新回调 + self._trigger_clock_callback('time_updated', { + 'server_time': server_time, + 'interpolated_time': self.interpolation_state['interpolated_time'], + 'local_time': current_time + }) + + self.last_interpolation_update = current_time + + except Exception as e: + print(f"✗ 时间插值更新失败: {e}") + + def get_server_time(self) -> float: + """ + 获取服务器时间 + + Returns: + 服务器时间戳 + """ + try: + local_time = time.time() + return local_time + self.clock_state['time_offset'] + except Exception as e: + print(f"✗ 服务器时间获取失败: {e}") + return time.time() + + def get_interpolated_time(self) -> float: + """ + 获取插值时间 + + Returns: + 插值时间戳 + """ + try: + if self.clock_config['enable_interpolation'] and self.interpolation_state['interpolated_time'] != 0.0: + return self.interpolation_state['interpolated_time'] + else: + return self.get_server_time() + except Exception as e: + print(f"✗ 插值时间获取失败: {e}") + return time.time() + + def get_time_offset(self) -> float: + """ + 获取时间偏移 + + Returns: + 时间偏移值 + """ + return self.clock_state['time_offset'] + + def get_sync_quality(self) -> float: + """ + 获取同步质量 + + Returns: + 同步质量值 (0.0-1.0) + """ + return self.clock_state['sync_quality'] + + def is_synchronized(self) -> bool: + """ + 检查是否已同步 + + Returns: + 是否已同步 + """ + return self.clock_state['is_synchronized'] + + def get_round_trip_time(self) -> float: + """ + 获取往返时间 + + Returns: + 往返时间(秒) + """ + return self.clock_state['round_trip_time'] + + def get_clock_stats(self) -> Dict[str, Any]: + """ + 获取时钟统计信息 + + Returns: + 时钟统计字典 + """ + return self.clock_stats.copy() + + def reset_clock_stats(self): + """重置时钟统计信息""" + try: + self.clock_stats = { + 'sync_attempts': 0, + 'successful_syncs': 0, + 'failed_syncs': 0, + 'average_rtt': 0.0, + 'min_rtt': float('inf'), + 'max_rtt': 0.0, + 'sync_errors': 0 + } + print("✓ 时钟统计信息已重置") + except Exception as e: + print(f"✗ 时钟统计信息重置失败: {e}") + + def force_sync(self) -> bool: + """ + 强制进行时钟同步 + + Returns: + 是否成功发起同步 + """ + try: + self._request_time_sync() + self.last_sync_attempt = time.time() + print("✓ 强制时钟同步已发起") + return True + except Exception as e: + print(f"✗ 强制时钟同步失败: {e}") + return False + + def reset_sync(self): + """重置时钟同步状态""" + try: + with self.sync_lock: + self.clock_state = { + 'is_synchronized': False, + 'time_offset': 0.0, + 'time_drift': 0.0, + 'last_sync_time': 0.0, + 'sync_quality': 0.0, + 'round_trip_time': 0.0, + 'sync_samples': [] + } + + self.interpolation_state = { + 'interpolated_time': 0.0, + 'target_time': 0.0, + 'interpolation_start': 0.0, + 'interpolation_duration': 0.0 + } + + self.sync_requests.clear() + + print("✓ 时钟同步状态已重置") + except Exception as e: + print(f"✗ 时钟同步状态重置失败: {e}") + + def set_clock_config(self, config: Dict[str, Any]) -> bool: + """ + 设置时钟配置 + + Args: + config: 时钟配置字典 + + Returns: + 是否设置成功 + """ + try: + self.clock_config.update(config) + print(f"✓ 时钟配置已更新: {self.clock_config}") + return True + except Exception as e: + print(f"✗ 时钟配置设置失败: {e}") + return False + + def get_clock_config(self) -> Dict[str, Any]: + """ + 获取时钟配置 + + Returns: + 时钟配置字典 + """ + return self.clock_config.copy() + + def _trigger_clock_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发时钟回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.clock_callbacks: + for callback in self.clock_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 时钟回调执行失败: {e}") + except Exception as e: + print(f"✗ 时钟回调触发失败: {e}") + + def register_clock_callback(self, callback_type: str, callback: callable): + """ + 注册时钟回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.clock_callbacks: + self.clock_callbacks[callback_type].append(callback) + print(f"✓ 时钟回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 时钟回调注册失败: {e}") + + def unregister_clock_callback(self, callback_type: str, callback: callable): + """ + 注销时钟回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.clock_callbacks: + if callback in self.clock_callbacks[callback_type]: + self.clock_callbacks[callback_type].remove(callback) + print(f"✓ 时钟回调已注销: {callback_type}") + except Exception as e: print(f"✗ 时钟回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/network_sync/config/sync_config.py b/plugins/user/network_sync/config/sync_config.py index 408bf5ee..16e04b28 100644 --- a/plugins/user/network_sync/config/sync_config.py +++ b/plugins/user/network_sync/config/sync_config.py @@ -1,867 +1,867 @@ -""" -配置管理模块 -负责管理网络同步插件的配置 -""" - -import time -import json -import os -from typing import Dict, Any, List, Optional -import threading - -class SyncConfig: - """ - 同步配置管理器 - 负责管理网络同步插件的配置 - """ - - def __init__(self, plugin): - """ - 初始化同步配置管理器 - - Args: - plugin: 网络同步插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 配置文件路径 - self.config_path = "/home/hello/EG/plugins/user/network_sync/config/network_sync_config.json" - - # 默认配置 - self.default_config = { - "general": { - "plugin_version": "1.0.0", - "enable_network_sync": True, - "log_level": "INFO", - "max_log_size": 10485760, # 10MB - "enable_debug_logging": False - }, - "network": { - "server_host": "localhost", - "server_port": 7777, - "max_connections": 32, - "connection_timeout": 30.0, - "heartbeat_interval": 5.0, - "buffer_size": 4096, - "enable_compression": True, - "enable_encryption": False, - "protocol_version": "1.0" - }, - "sync": { - "sync_rate": 20, - "interpolation_rate": 60, - "extrapolation_limit": 0.1, - "sync_distance_threshold": 0.01, - "sync_rotation_threshold": 0.1, - "sync_scale_threshold": 0.01, - "enable_delta_compression": True, - "enable_state_filtering": True, - "max_sync_objects": 1000, - "sync_area_radius": 100.0 - }, - "prediction": { - "enable_prediction": True, - "enable_interpolation": True, - "enable_extrapolation": True, - "max_prediction_time": 0.5, - "prediction_smoothing": 0.1, - "correction_speed": 5.0, - "velocity_damping": 0.95, - "position_threshold": 0.01, - "rotation_threshold": 0.1, - "enable_snap_correction": True, - "snap_threshold": 1.0 - }, - "clock": { - "sync_interval": 5.0, - "samples_per_sync": 10, - "max_offset_samples": 100, - "enable_interpolation": True, - "interpolation_rate": 60.0, - "max_drift_correction": 0.1, - "min_sync_quality": 0.8, - "timeout": 10.0 - }, - "security": { - "enable_authentication": False, - "auth_timeout": 30.0, - "max_auth_attempts": 3, - "enable_rate_limiting": True, - "max_requests_per_second": 100, - "enable_blacklist": True - }, - "performance": { - "max_packet_size": 65536, - "enable_fragmentation": True, - "fragment_size": 1024, - "enable_caching": True, - "cache_size": 1000, - "thread_pool_size": 4 - } - } - - # 当前配置 - self.current_config = {} - - # 配置变更监听器 - self.config_listeners = {} - - # 配置备份 - self.config_backup = {} - - # 配置统计 - self.config_stats = { - "loads": 0, - "saves": 0, - "changes": 0, - "errors": 0, - "backups": 0 - } - - # 线程锁 - self.config_lock = threading.RLock() - - # 回调函数 - self.config_callbacks = { - "config_loaded": [], - "config_saved": [], - "config_changed": [], - "config_error": [] - } - - # 时间戳记录 - self.last_load_time = 0.0 - self.last_save_time = 0.0 - self.last_change_time = 0.0 - - print("✓ 同步配置管理器已创建") - - def initialize(self) -> bool: - """ - 初始化同步配置管理器 - - Returns: - 是否初始化成功 - """ - try: - # 创建配置目录 - config_dir = os.path.dirname(self.config_path) - if not os.path.exists(config_dir): - os.makedirs(config_dir) - - # 加载配置 - self.load_config() - - # 如果配置为空,使用默认配置 - if not self.current_config: - self.current_config = self.default_config.copy() - self.save_config() - - self.initialized = True - print("✓ 同步配置管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 同步配置管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用同步配置管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 配置管理器未初始化") - return False - - self.enabled = True - print("✓ 同步配置管理器已启用") - return True - - except Exception as e: - print(f"✗ 同步配置管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用同步配置管理器""" - try: - self.enabled = False - print("✓ 同步配置管理器已禁用") - - except Exception as e: - print(f"✗ 同步配置管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理同步配置管理器资源""" - try: - self.disable() - self.config_listeners.clear() - self.config_callbacks.clear() - self.initialized = False - print("✓ 同步配置管理器资源已清理") - - except Exception as e: - print(f"✗ 同步配置管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新同步配置管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 配置管理器不需要频繁更新 - pass - - except Exception as e: - print(f"✗ 同步配置管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def load_config(self) -> bool: - """ - 加载配置 - - Returns: - 是否加载成功 - """ - try: - with self.config_lock: - if os.path.exists(self.config_path): - with open(self.config_path, 'r', encoding='utf-8') as f: - self.current_config = json.load(f) - else: - # 使用默认配置 - self.current_config = self.default_config.copy() - - self.config_stats["loads"] += 1 - self.last_load_time = time.time() - - # 触发配置加载回调 - self._trigger_config_callback("config_loaded", { - "config": self.current_config, - "timestamp": self.last_load_time - }) - - print("✓ 配置加载完成") - return True - - except Exception as e: - print(f"✗ 配置加载失败: {e}") - self.config_stats["errors"] += 1 - - # 触发配置错误回调 - self._trigger_config_callback("config_error", { - "error": str(e), - "operation": "load" - }) - - return False - - def save_config(self) -> bool: - """ - 保存配置 - - Returns: - 是否保存成功 - """ - try: - with self.config_lock: - # 创建配置目录 - config_dir = os.path.dirname(self.config_path) - if not os.path.exists(config_dir): - os.makedirs(config_dir) - - # 保存配置 - with open(self.config_path, 'w', encoding='utf-8') as f: - json.dump(self.current_config, f, ensure_ascii=False, indent=2) - - self.config_stats["saves"] += 1 - self.last_save_time = time.time() - - # 触发配置保存回调 - self._trigger_config_callback("config_saved", { - "config": self.current_config, - "timestamp": self.last_save_time - }) - - print("✓ 配置保存完成") - return True - - except Exception as e: - print(f"✗ 配置保存失败: {e}") - self.config_stats["errors"] += 1 - - # 触发配置错误回调 - self._trigger_config_callback("config_error", { - "error": str(e), - "operation": "save" - }) - - return False - - def get_config(self, section: str = None, key: str = None, default: Any = None) -> Any: - """ - 获取配置值 - - Args: - section: 配置节名称 - key: 配置键名称 - default: 默认值 - - Returns: - 配置值 - """ - try: - with self.config_lock: - if section is None: - return self.current_config.copy() - elif key is None: - return self.current_config.get(section, {}).copy() - else: - return self.current_config.get(section, {}).get(key, default) - - except Exception as e: - print(f"✗ 配置获取失败: {e}") - return default - - def set_config(self, section: str, key: str = None, value: Any = None, save: bool = True) -> bool: - """ - 设置配置值 - - Args: - section: 配置节名称 - key: 配置键名称 - value: 配置值 - save: 是否保存到文件 - - Returns: - 是否设置成功 - """ - try: - with self.config_lock: - # 备份当前配置 - self._backup_config() - - if key is None: - # 设置整个节 - if isinstance(value, dict): - self.current_config[section] = value - else: - print(f"✗ 配置值必须是字典类型: {section}") - return False - else: - # 设置特定键值 - if section not in self.current_config: - self.current_config[section] = {} - - old_value = self.current_config[section].get(key) - self.current_config[section][key] = value - - # 触发配置变更回调 - self._trigger_config_callback("config_changed", { - "section": section, - "key": key, - "old_value": old_value, - "new_value": value, - "timestamp": time.time() - }) - - self.config_stats["changes"] += 1 - self.last_change_time = time.time() - - # 保存配置 - if save: - return self.save_config() - else: - return True - - except Exception as e: - print(f"✗ 配置设置失败: {e}") - self.config_stats["errors"] += 1 - - # 触发配置错误回调 - self._trigger_config_callback("config_error", { - "error": str(e), - "operation": "set" - }) - - return False - - def update_config(self, updates: Dict[str, Any], save: bool = True) -> bool: - """ - 批量更新配置 - - Args: - updates: 更新字典 - save: 是否保存到文件 - - Returns: - 是否更新成功 - """ - try: - with self.config_lock: - # 备份当前配置 - self._backup_config() - - old_values = {} - - # 应用更新 - for section, section_data in updates.items(): - if isinstance(section_data, dict): - if section not in self.current_config: - self.current_config[section] = {} - - for key, value in section_data.items(): - old_values[f"{section}.{key}"] = self.current_config[section].get(key) - self.current_config[section][key] = value - else: - old_values[section] = self.current_config.get(section) - self.current_config[section] = section_data - - self.config_stats["changes"] += 1 - self.last_change_time = time.time() - - # 触发配置变更回调 - self._trigger_config_callback("config_changed", { - "updates": updates, - "old_values": old_values, - "timestamp": self.last_change_time - }) - - # 保存配置 - if save: - return self.save_config() - else: - return True - - except Exception as e: - print(f"✗ 配置批量更新失败: {e}") - self.config_stats["errors"] += 1 - - # 触发配置错误回调 - self._trigger_config_callback("config_error", { - "error": str(e), - "operation": "update" - }) - - return False - - def reset_config(self, section: str = None) -> bool: - """ - 重置配置 - - Args: - section: 要重置的配置节,None表示重置所有配置 - - Returns: - 是否重置成功 - """ - try: - with self.config_lock: - # 备份当前配置 - self._backup_config() - - if section is None: - # 重置所有配置 - self.current_config = self.default_config.copy() - elif section in self.default_config: - # 重置特定节 - self.current_config[section] = self.default_config[section].copy() - else: - print(f"✗ 无效的配置节: {section}") - return False - - self.config_stats["changes"] += 1 - self.last_change_time = time.time() - - # 触发配置变更回调 - self._trigger_config_callback("config_changed", { - "operation": "reset", - "section": section, - "timestamp": self.last_change_time - }) - - # 保存配置 - return self.save_config() - - except Exception as e: - print(f"✗ 配置重置失败: {e}") - self.config_stats["errors"] += 1 - - # 触发配置错误回调 - self._trigger_config_callback("config_error", { - "error": str(e), - "operation": "reset" - }) - - return False - - def _backup_config(self): - """备份当前配置""" - try: - self.config_backup = json.loads(json.dumps(self.current_config)) - self.config_stats["backups"] += 1 - print("✓ 配置已备份") - except Exception as e: - print(f"✗ 配置备份失败: {e}") - - def restore_config(self) -> bool: - """ - 恢复配置备份 - - Returns: - 是否恢复成功 - """ - try: - with self.config_lock: - if self.config_backup: - self.current_config = json.loads(json.dumps(self.config_backup)) - self.config_stats["changes"] += 1 - self.last_change_time = time.time() - - # 触发配置变更回调 - self._trigger_config_callback("config_changed", { - "operation": "restore", - "timestamp": self.last_change_time - }) - - # 保存配置 - return self.save_config() - else: - print("✗ 没有可用的配置备份") - return False - - except Exception as e: - print(f"✗ 配置恢复失败: {e}") - self.config_stats["errors"] += 1 - - # 触发配置错误回调 - self._trigger_config_callback("config_error", { - "error": str(e), - "operation": "restore" - }) - - return False - - def validate_config(self) -> bool: - """ - 验证配置 - - Returns: - 配置是否有效 - """ - try: - # 这里可以添加配置验证逻辑 - # 例如:检查必需的配置项是否存在,值是否在有效范围内等 - - # 检查基本结构 - required_sections = ["general", "network", "sync", "prediction", "clock"] - for section in required_sections: - if section not in self.current_config: - print(f"✗ 缺少必需的配置节: {section}") - return False - - # 检查关键配置值范围 - network = self.current_config.get("network", {}) - if "server_port" in network and not (1 <= network["server_port"] <= 65535): - print("✗ 无效的服务器端口") - return False - - sync = self.current_config.get("sync", {}) - if "sync_rate" in sync and sync["sync_rate"] <= 0: - print("✗ 同步频率必须大于0") - return False - - print("✓ 配置验证通过") - return True - - except Exception as e: - print(f"✗ 配置验证失败: {e}") - return False - - def export_config(self, file_path: str) -> bool: - """ - 导出配置到文件 - - Args: - file_path: 导出文件路径 - - Returns: - 是否导出成功 - """ - try: - with self.config_lock: - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(self.current_config, f, ensure_ascii=False, indent=2) - - print(f"✓ 配置已导出到: {file_path}") - return True - - except Exception as e: - print(f"✗ 配置导出失败: {e}") - return False - - def import_config(self, file_path: str) -> bool: - """ - 从文件导入配置 - - Args: - file_path: 导入文件路径 - - Returns: - 是否导入成功 - """ - try: - with self.config_lock: - # 备份当前配置 - self._backup_config() - - with open(file_path, 'r', encoding='utf-8') as f: - imported_config = json.load(f) - - # 验证导入的配置 - temp_manager = SyncConfig(self.plugin) - temp_manager.current_config = imported_config - if not temp_manager.validate_config(): - print("✗ 导入的配置无效") - return False - - # 应用导入的配置 - self.current_config = imported_config - self.config_stats["changes"] += 1 - self.last_change_time = time.time() - - # 触发配置变更回调 - self._trigger_config_callback("config_changed", { - "operation": "import", - "source": file_path, - "timestamp": self.last_change_time - }) - - # 保存配置 - return self.save_config() - - except Exception as e: - print(f"✗ 配置导入失败: {e}") - self.config_stats["errors"] += 1 - - # 触发配置错误回调 - self._trigger_config_callback("config_error", { - "error": str(e), - "operation": "import" - }) - - return False - - def get_default_config(self) -> Dict[str, Any]: - """ - 获取默认配置 - - Returns: - 默认配置字典 - """ - return self.default_config.copy() - - def get_config_stats(self) -> Dict[str, int]: - """ - 获取配置统计信息 - - Returns: - 统计信息字典 - """ - return self.config_stats.copy() - - def reset_config_stats(self): - """重置配置统计信息""" - try: - self.config_stats = { - "loads": 0, - "saves": 0, - "changes": 0, - "errors": 0, - "backups": 0 - } - print("✓ 配置统计信息已重置") - except Exception as e: - print(f"✗ 配置统计信息重置失败: {e}") - - def register_config_listener(self, section: str, key: str, listener: callable) -> bool: - """ - 注册配置监听器 - - Args: - section: 配置节名称 - key: 配置键名称 - listener: 监听器函数 - - Returns: - 是否注册成功 - """ - try: - listener_key = f"{section}.{key}" - if listener_key not in self.config_listeners: - self.config_listeners[listener_key] = [] - - self.config_listeners[listener_key].append(listener) - print(f"✓ 配置监听器已注册: {listener_key}") - return True - - except Exception as e: - print(f"✗ 配置监听器注册失败: {e}") - return False - - def unregister_config_listener(self, section: str, key: str, listener: callable) -> bool: - """ - 注销配置监听器 - - Args: - section: 配置节名称 - key: 配置键名称 - listener: 监听器函数 - - Returns: - 是否注销成功 - """ - try: - listener_key = f"{section}.{key}" - if listener_key in self.config_listeners: - if listener in self.config_listeners[listener_key]: - self.config_listeners[listener_key].remove(listener) - print(f"✓ 配置监听器已注销: {listener_key}") - return True - - print(f"✗ 配置监听器不存在: {listener_key}") - return False - - except Exception as e: - print(f"✗ 配置监听器注销失败: {e}") - return False - - def _trigger_config_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发配置回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.config_callbacks: - for callback in self.config_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 配置回调执行失败: {e}") - except Exception as e: - print(f"✗ 配置回调触发失败: {e}") - - def register_config_callback(self, callback_type: str, callback: callable): - """ - 注册配置回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.config_callbacks: - self.config_callbacks[callback_type].append(callback) - print(f"✓ 配置回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 配置回调注册失败: {e}") - - def unregister_config_callback(self, callback_type: str, callback: callable): - """ - 注销配置回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.config_callbacks: - if callback in self.config_callbacks[callback_type]: - self.config_callbacks[callback_type].remove(callback) - print(f"✓ 配置回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 配置回调注销失败: {e}") - - def apply_config_to_modules(self): - """ - 将配置应用到各个模块 - """ - try: - if not self.enabled: - return - - # 应用配置到同步管理器 - if self.plugin.sync_manager: - sync_config = self.get_config("sync") - self.plugin.sync_manager.set_sync_config(sync_config) - - # 应用配置到网络管理器 - if self.plugin.network_manager: - network_config = self.get_config("network") - self.plugin.network_manager.set_network_config(network_config) - - # 应用配置到协议处理器 - if self.plugin.protocol_handler: - # 协议配置可能需要从多个节获取 - protocol_config = {} - protocol_config.update(self.get_config("network", {})) - protocol_config.update(self.get_config("performance", {})) - self.plugin.protocol_handler.set_protocol_config(protocol_config) - - # 应用配置到对象同步器 - if self.plugin.object_sync: - sync_config = self.get_config("sync") - self.plugin.object_sync.set_sync_config(sync_config) - - # 应用配置到连接管理器 - if self.plugin.connection_manager: - connection_config = self.get_config("network") - security_config = self.get_config("security") - combined_config = {**connection_config, **security_config} - self.plugin.connection_manager.set_connection_config(combined_config) - - # 应用配置到数据序列化器 - if self.plugin.data_serializer: - serialization_config = self.get_config("performance") - self.plugin.data_serializer.set_serialization_config(serialization_config) - - # 应用配置到时钟同步器 - if self.plugin.clock_sync: - clock_config = self.get_config("clock") - self.plugin.clock_sync.set_clock_config(clock_config) - - # 应用配置到预测管理器 - if self.plugin.prediction_manager: - prediction_config = self.get_config("prediction") - self.plugin.prediction_manager.set_prediction_config(prediction_config) - - print("✓ 配置已应用到所有模块") - - except Exception as e: - print(f"✗ 配置应用失败: {e}") - import traceback +""" +配置管理模块 +负责管理网络同步插件的配置 +""" + +import time +import json +import os +from typing import Dict, Any, List, Optional +import threading + +class SyncConfig: + """ + 同步配置管理器 + 负责管理网络同步插件的配置 + """ + + def __init__(self, plugin): + """ + 初始化同步配置管理器 + + Args: + plugin: 网络同步插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 配置文件路径 + self.config_path = "/home/hello/EG/plugins/user/network_sync/config/network_sync_config.json" + + # 默认配置 + self.default_config = { + "general": { + "plugin_version": "1.0.0", + "enable_network_sync": True, + "log_level": "INFO", + "max_log_size": 10485760, # 10MB + "enable_debug_logging": False + }, + "network": { + "server_host": "localhost", + "server_port": 7777, + "max_connections": 32, + "connection_timeout": 30.0, + "heartbeat_interval": 5.0, + "buffer_size": 4096, + "enable_compression": True, + "enable_encryption": False, + "protocol_version": "1.0" + }, + "sync": { + "sync_rate": 20, + "interpolation_rate": 60, + "extrapolation_limit": 0.1, + "sync_distance_threshold": 0.01, + "sync_rotation_threshold": 0.1, + "sync_scale_threshold": 0.01, + "enable_delta_compression": True, + "enable_state_filtering": True, + "max_sync_objects": 1000, + "sync_area_radius": 100.0 + }, + "prediction": { + "enable_prediction": True, + "enable_interpolation": True, + "enable_extrapolation": True, + "max_prediction_time": 0.5, + "prediction_smoothing": 0.1, + "correction_speed": 5.0, + "velocity_damping": 0.95, + "position_threshold": 0.01, + "rotation_threshold": 0.1, + "enable_snap_correction": True, + "snap_threshold": 1.0 + }, + "clock": { + "sync_interval": 5.0, + "samples_per_sync": 10, + "max_offset_samples": 100, + "enable_interpolation": True, + "interpolation_rate": 60.0, + "max_drift_correction": 0.1, + "min_sync_quality": 0.8, + "timeout": 10.0 + }, + "security": { + "enable_authentication": False, + "auth_timeout": 30.0, + "max_auth_attempts": 3, + "enable_rate_limiting": True, + "max_requests_per_second": 100, + "enable_blacklist": True + }, + "performance": { + "max_packet_size": 65536, + "enable_fragmentation": True, + "fragment_size": 1024, + "enable_caching": True, + "cache_size": 1000, + "thread_pool_size": 4 + } + } + + # 当前配置 + self.current_config = {} + + # 配置变更监听器 + self.config_listeners = {} + + # 配置备份 + self.config_backup = {} + + # 配置统计 + self.config_stats = { + "loads": 0, + "saves": 0, + "changes": 0, + "errors": 0, + "backups": 0 + } + + # 线程锁 + self.config_lock = threading.RLock() + + # 回调函数 + self.config_callbacks = { + "config_loaded": [], + "config_saved": [], + "config_changed": [], + "config_error": [] + } + + # 时间戳记录 + self.last_load_time = 0.0 + self.last_save_time = 0.0 + self.last_change_time = 0.0 + + print("✓ 同步配置管理器已创建") + + def initialize(self) -> bool: + """ + 初始化同步配置管理器 + + Returns: + 是否初始化成功 + """ + try: + # 创建配置目录 + config_dir = os.path.dirname(self.config_path) + if not os.path.exists(config_dir): + os.makedirs(config_dir) + + # 加载配置 + self.load_config() + + # 如果配置为空,使用默认配置 + if not self.current_config: + self.current_config = self.default_config.copy() + self.save_config() + + self.initialized = True + print("✓ 同步配置管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 同步配置管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用同步配置管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 配置管理器未初始化") + return False + + self.enabled = True + print("✓ 同步配置管理器已启用") + return True + + except Exception as e: + print(f"✗ 同步配置管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用同步配置管理器""" + try: + self.enabled = False + print("✓ 同步配置管理器已禁用") + + except Exception as e: + print(f"✗ 同步配置管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理同步配置管理器资源""" + try: + self.disable() + self.config_listeners.clear() + self.config_callbacks.clear() + self.initialized = False + print("✓ 同步配置管理器资源已清理") + + except Exception as e: + print(f"✗ 同步配置管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新同步配置管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 配置管理器不需要频繁更新 + pass + + except Exception as e: + print(f"✗ 同步配置管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def load_config(self) -> bool: + """ + 加载配置 + + Returns: + 是否加载成功 + """ + try: + with self.config_lock: + if os.path.exists(self.config_path): + with open(self.config_path, 'r', encoding='utf-8') as f: + self.current_config = json.load(f) + else: + # 使用默认配置 + self.current_config = self.default_config.copy() + + self.config_stats["loads"] += 1 + self.last_load_time = time.time() + + # 触发配置加载回调 + self._trigger_config_callback("config_loaded", { + "config": self.current_config, + "timestamp": self.last_load_time + }) + + print("✓ 配置加载完成") + return True + + except Exception as e: + print(f"✗ 配置加载失败: {e}") + self.config_stats["errors"] += 1 + + # 触发配置错误回调 + self._trigger_config_callback("config_error", { + "error": str(e), + "operation": "load" + }) + + return False + + def save_config(self) -> bool: + """ + 保存配置 + + Returns: + 是否保存成功 + """ + try: + with self.config_lock: + # 创建配置目录 + config_dir = os.path.dirname(self.config_path) + if not os.path.exists(config_dir): + os.makedirs(config_dir) + + # 保存配置 + with open(self.config_path, 'w', encoding='utf-8') as f: + json.dump(self.current_config, f, ensure_ascii=False, indent=2) + + self.config_stats["saves"] += 1 + self.last_save_time = time.time() + + # 触发配置保存回调 + self._trigger_config_callback("config_saved", { + "config": self.current_config, + "timestamp": self.last_save_time + }) + + print("✓ 配置保存完成") + return True + + except Exception as e: + print(f"✗ 配置保存失败: {e}") + self.config_stats["errors"] += 1 + + # 触发配置错误回调 + self._trigger_config_callback("config_error", { + "error": str(e), + "operation": "save" + }) + + return False + + def get_config(self, section: str = None, key: str = None, default: Any = None) -> Any: + """ + 获取配置值 + + Args: + section: 配置节名称 + key: 配置键名称 + default: 默认值 + + Returns: + 配置值 + """ + try: + with self.config_lock: + if section is None: + return self.current_config.copy() + elif key is None: + return self.current_config.get(section, {}).copy() + else: + return self.current_config.get(section, {}).get(key, default) + + except Exception as e: + print(f"✗ 配置获取失败: {e}") + return default + + def set_config(self, section: str, key: str = None, value: Any = None, save: bool = True) -> bool: + """ + 设置配置值 + + Args: + section: 配置节名称 + key: 配置键名称 + value: 配置值 + save: 是否保存到文件 + + Returns: + 是否设置成功 + """ + try: + with self.config_lock: + # 备份当前配置 + self._backup_config() + + if key is None: + # 设置整个节 + if isinstance(value, dict): + self.current_config[section] = value + else: + print(f"✗ 配置值必须是字典类型: {section}") + return False + else: + # 设置特定键值 + if section not in self.current_config: + self.current_config[section] = {} + + old_value = self.current_config[section].get(key) + self.current_config[section][key] = value + + # 触发配置变更回调 + self._trigger_config_callback("config_changed", { + "section": section, + "key": key, + "old_value": old_value, + "new_value": value, + "timestamp": time.time() + }) + + self.config_stats["changes"] += 1 + self.last_change_time = time.time() + + # 保存配置 + if save: + return self.save_config() + else: + return True + + except Exception as e: + print(f"✗ 配置设置失败: {e}") + self.config_stats["errors"] += 1 + + # 触发配置错误回调 + self._trigger_config_callback("config_error", { + "error": str(e), + "operation": "set" + }) + + return False + + def update_config(self, updates: Dict[str, Any], save: bool = True) -> bool: + """ + 批量更新配置 + + Args: + updates: 更新字典 + save: 是否保存到文件 + + Returns: + 是否更新成功 + """ + try: + with self.config_lock: + # 备份当前配置 + self._backup_config() + + old_values = {} + + # 应用更新 + for section, section_data in updates.items(): + if isinstance(section_data, dict): + if section not in self.current_config: + self.current_config[section] = {} + + for key, value in section_data.items(): + old_values[f"{section}.{key}"] = self.current_config[section].get(key) + self.current_config[section][key] = value + else: + old_values[section] = self.current_config.get(section) + self.current_config[section] = section_data + + self.config_stats["changes"] += 1 + self.last_change_time = time.time() + + # 触发配置变更回调 + self._trigger_config_callback("config_changed", { + "updates": updates, + "old_values": old_values, + "timestamp": self.last_change_time + }) + + # 保存配置 + if save: + return self.save_config() + else: + return True + + except Exception as e: + print(f"✗ 配置批量更新失败: {e}") + self.config_stats["errors"] += 1 + + # 触发配置错误回调 + self._trigger_config_callback("config_error", { + "error": str(e), + "operation": "update" + }) + + return False + + def reset_config(self, section: str = None) -> bool: + """ + 重置配置 + + Args: + section: 要重置的配置节,None表示重置所有配置 + + Returns: + 是否重置成功 + """ + try: + with self.config_lock: + # 备份当前配置 + self._backup_config() + + if section is None: + # 重置所有配置 + self.current_config = self.default_config.copy() + elif section in self.default_config: + # 重置特定节 + self.current_config[section] = self.default_config[section].copy() + else: + print(f"✗ 无效的配置节: {section}") + return False + + self.config_stats["changes"] += 1 + self.last_change_time = time.time() + + # 触发配置变更回调 + self._trigger_config_callback("config_changed", { + "operation": "reset", + "section": section, + "timestamp": self.last_change_time + }) + + # 保存配置 + return self.save_config() + + except Exception as e: + print(f"✗ 配置重置失败: {e}") + self.config_stats["errors"] += 1 + + # 触发配置错误回调 + self._trigger_config_callback("config_error", { + "error": str(e), + "operation": "reset" + }) + + return False + + def _backup_config(self): + """备份当前配置""" + try: + self.config_backup = json.loads(json.dumps(self.current_config)) + self.config_stats["backups"] += 1 + print("✓ 配置已备份") + except Exception as e: + print(f"✗ 配置备份失败: {e}") + + def restore_config(self) -> bool: + """ + 恢复配置备份 + + Returns: + 是否恢复成功 + """ + try: + with self.config_lock: + if self.config_backup: + self.current_config = json.loads(json.dumps(self.config_backup)) + self.config_stats["changes"] += 1 + self.last_change_time = time.time() + + # 触发配置变更回调 + self._trigger_config_callback("config_changed", { + "operation": "restore", + "timestamp": self.last_change_time + }) + + # 保存配置 + return self.save_config() + else: + print("✗ 没有可用的配置备份") + return False + + except Exception as e: + print(f"✗ 配置恢复失败: {e}") + self.config_stats["errors"] += 1 + + # 触发配置错误回调 + self._trigger_config_callback("config_error", { + "error": str(e), + "operation": "restore" + }) + + return False + + def validate_config(self) -> bool: + """ + 验证配置 + + Returns: + 配置是否有效 + """ + try: + # 这里可以添加配置验证逻辑 + # 例如:检查必需的配置项是否存在,值是否在有效范围内等 + + # 检查基本结构 + required_sections = ["general", "network", "sync", "prediction", "clock"] + for section in required_sections: + if section not in self.current_config: + print(f"✗ 缺少必需的配置节: {section}") + return False + + # 检查关键配置值范围 + network = self.current_config.get("network", {}) + if "server_port" in network and not (1 <= network["server_port"] <= 65535): + print("✗ 无效的服务器端口") + return False + + sync = self.current_config.get("sync", {}) + if "sync_rate" in sync and sync["sync_rate"] <= 0: + print("✗ 同步频率必须大于0") + return False + + print("✓ 配置验证通过") + return True + + except Exception as e: + print(f"✗ 配置验证失败: {e}") + return False + + def export_config(self, file_path: str) -> bool: + """ + 导出配置到文件 + + Args: + file_path: 导出文件路径 + + Returns: + 是否导出成功 + """ + try: + with self.config_lock: + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(self.current_config, f, ensure_ascii=False, indent=2) + + print(f"✓ 配置已导出到: {file_path}") + return True + + except Exception as e: + print(f"✗ 配置导出失败: {e}") + return False + + def import_config(self, file_path: str) -> bool: + """ + 从文件导入配置 + + Args: + file_path: 导入文件路径 + + Returns: + 是否导入成功 + """ + try: + with self.config_lock: + # 备份当前配置 + self._backup_config() + + with open(file_path, 'r', encoding='utf-8') as f: + imported_config = json.load(f) + + # 验证导入的配置 + temp_manager = SyncConfig(self.plugin) + temp_manager.current_config = imported_config + if not temp_manager.validate_config(): + print("✗ 导入的配置无效") + return False + + # 应用导入的配置 + self.current_config = imported_config + self.config_stats["changes"] += 1 + self.last_change_time = time.time() + + # 触发配置变更回调 + self._trigger_config_callback("config_changed", { + "operation": "import", + "source": file_path, + "timestamp": self.last_change_time + }) + + # 保存配置 + return self.save_config() + + except Exception as e: + print(f"✗ 配置导入失败: {e}") + self.config_stats["errors"] += 1 + + # 触发配置错误回调 + self._trigger_config_callback("config_error", { + "error": str(e), + "operation": "import" + }) + + return False + + def get_default_config(self) -> Dict[str, Any]: + """ + 获取默认配置 + + Returns: + 默认配置字典 + """ + return self.default_config.copy() + + def get_config_stats(self) -> Dict[str, int]: + """ + 获取配置统计信息 + + Returns: + 统计信息字典 + """ + return self.config_stats.copy() + + def reset_config_stats(self): + """重置配置统计信息""" + try: + self.config_stats = { + "loads": 0, + "saves": 0, + "changes": 0, + "errors": 0, + "backups": 0 + } + print("✓ 配置统计信息已重置") + except Exception as e: + print(f"✗ 配置统计信息重置失败: {e}") + + def register_config_listener(self, section: str, key: str, listener: callable) -> bool: + """ + 注册配置监听器 + + Args: + section: 配置节名称 + key: 配置键名称 + listener: 监听器函数 + + Returns: + 是否注册成功 + """ + try: + listener_key = f"{section}.{key}" + if listener_key not in self.config_listeners: + self.config_listeners[listener_key] = [] + + self.config_listeners[listener_key].append(listener) + print(f"✓ 配置监听器已注册: {listener_key}") + return True + + except Exception as e: + print(f"✗ 配置监听器注册失败: {e}") + return False + + def unregister_config_listener(self, section: str, key: str, listener: callable) -> bool: + """ + 注销配置监听器 + + Args: + section: 配置节名称 + key: 配置键名称 + listener: 监听器函数 + + Returns: + 是否注销成功 + """ + try: + listener_key = f"{section}.{key}" + if listener_key in self.config_listeners: + if listener in self.config_listeners[listener_key]: + self.config_listeners[listener_key].remove(listener) + print(f"✓ 配置监听器已注销: {listener_key}") + return True + + print(f"✗ 配置监听器不存在: {listener_key}") + return False + + except Exception as e: + print(f"✗ 配置监听器注销失败: {e}") + return False + + def _trigger_config_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发配置回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.config_callbacks: + for callback in self.config_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 配置回调执行失败: {e}") + except Exception as e: + print(f"✗ 配置回调触发失败: {e}") + + def register_config_callback(self, callback_type: str, callback: callable): + """ + 注册配置回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.config_callbacks: + self.config_callbacks[callback_type].append(callback) + print(f"✓ 配置回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 配置回调注册失败: {e}") + + def unregister_config_callback(self, callback_type: str, callback: callable): + """ + 注销配置回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.config_callbacks: + if callback in self.config_callbacks[callback_type]: + self.config_callbacks[callback_type].remove(callback) + print(f"✓ 配置回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 配置回调注销失败: {e}") + + def apply_config_to_modules(self): + """ + 将配置应用到各个模块 + """ + try: + if not self.enabled: + return + + # 应用配置到同步管理器 + if self.plugin.sync_manager: + sync_config = self.get_config("sync") + self.plugin.sync_manager.set_sync_config(sync_config) + + # 应用配置到网络管理器 + if self.plugin.network_manager: + network_config = self.get_config("network") + self.plugin.network_manager.set_network_config(network_config) + + # 应用配置到协议处理器 + if self.plugin.protocol_handler: + # 协议配置可能需要从多个节获取 + protocol_config = {} + protocol_config.update(self.get_config("network", {})) + protocol_config.update(self.get_config("performance", {})) + self.plugin.protocol_handler.set_protocol_config(protocol_config) + + # 应用配置到对象同步器 + if self.plugin.object_sync: + sync_config = self.get_config("sync") + self.plugin.object_sync.set_sync_config(sync_config) + + # 应用配置到连接管理器 + if self.plugin.connection_manager: + connection_config = self.get_config("network") + security_config = self.get_config("security") + combined_config = {**connection_config, **security_config} + self.plugin.connection_manager.set_connection_config(combined_config) + + # 应用配置到数据序列化器 + if self.plugin.data_serializer: + serialization_config = self.get_config("performance") + self.plugin.data_serializer.set_serialization_config(serialization_config) + + # 应用配置到时钟同步器 + if self.plugin.clock_sync: + clock_config = self.get_config("clock") + self.plugin.clock_sync.set_clock_config(clock_config) + + # 应用配置到预测管理器 + if self.plugin.prediction_manager: + prediction_config = self.get_config("prediction") + self.plugin.prediction_manager.set_prediction_config(prediction_config) + + print("✓ 配置已应用到所有模块") + + except Exception as e: + print(f"✗ 配置应用失败: {e}") + import traceback traceback.print_exc() \ No newline at end of file diff --git a/plugins/user/network_sync/connection/connection_manager.py b/plugins/user/network_sync/connection/connection_manager.py index df5440eb..3ec9fabd 100644 --- a/plugins/user/network_sync/connection/connection_manager.py +++ b/plugins/user/network_sync/connection/connection_manager.py @@ -1,685 +1,685 @@ -""" -连接管理器模块 -负责管理网络连接和客户端状态 -""" - -import time -from typing import Dict, Any, List, Optional -import threading -import socket - -class ConnectionManager: - """ - 连接管理器 - 负责管理网络连接和客户端状态 - """ - - def __init__(self, plugin): - """ - 初始化连接管理器 - - Args: - plugin: 网络同步插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 连接配置 - self.connection_config = { - 'max_connections': 32, - 'connection_timeout': 30.0, - 'reconnect_attempts': 3, - 'reconnect_interval': 5.0, - 'heartbeat_interval': 5.0, - 'enable_auto_reconnect': True, - 'enable_connection_limits': True, - 'max_connections_per_ip': 5 - } - - # 客户端管理 - self.clients = {} # 已连接的客户端 - self.pending_connections = {} # 待处理的连接 - self.banned_ips = set() # 被禁止的IP地址 - - # 服务器状态 - self.server_state = { - 'is_running': False, - 'port': 7777, - 'clients_connected': 0, - 'peak_connections': 0, - 'bytes_sent': 0, - 'bytes_received': 0 - } - - # 客户端状态 - self.client_state = { - 'is_connected': False, - 'server_address': '', - 'server_port': 7777, - 'connection_time': 0.0, - 'last_heartbeat': 0.0, - 'reconnect_attempts': 0 - } - - # 连接统计 - self.connection_stats = { - 'connections_accepted': 0, - 'connections_rejected': 0, - 'connections_lost': 0, - 'connections_timed_out': 0, - 'reconnections': 0, - 'banned_connections': 0 - } - - # 线程管理 - self.connection_thread = None - self.heartbeat_thread = None - self.reconnect_thread = None - self.running = False - - # 回调函数 - self.connection_callbacks = { - 'client_connected': [], - 'client_disconnected': [], - 'connection_timeout': [], - 'reconnect_attempt': [], - 'connection_banned': [] - } - - # 时间戳记录 - self.last_connection_check = 0.0 - self.last_heartbeat_check = 0.0 - self.last_reconnect_attempt = 0.0 - - print("✓ 连接管理器已创建") - - def initialize(self) -> bool: - """ - 初始化连接管理器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 连接管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 连接管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用连接管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 连接管理器未初始化") - return False - - self.enabled = True - self.running = True - - print("✓ 连接管理器已启用") - return True - - except Exception as e: - print(f"✗ 连接管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用连接管理器""" - try: - self.enabled = False - self.running = False - - # 断开所有连接 - self._disconnect_all_clients() - self._disconnect_from_server() - - print("✓ 连接管理器已禁用") - - except Exception as e: - print(f"✗ 连接管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理连接管理器资源""" - try: - self.disable() - self.clients.clear() - self.pending_connections.clear() - self.banned_ips.clear() - self.connection_callbacks.clear() - self.initialized = False - print("✓ 连接管理器资源已清理") - - except Exception as e: - print(f"✗ 连接管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新连接管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 检查连接超时 - if current_time - self.last_connection_check >= 1.0: # 每秒检查一次 - self._check_connection_timeouts(current_time) - self.last_connection_check = current_time - - # 检查心跳超时 - if current_time - self.last_heartbeat_check >= self.connection_config['heartbeat_interval']: - self._check_heartbeat_timeouts(current_time) - self.last_heartbeat_check = current_time - - except Exception as e: - print(f"✗ 连接管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _check_connection_timeouts(self, current_time: float): - """ - 检查连接超时 - - Args: - current_time: 当前时间 - """ - try: - timeout_clients = [] - for client_id, client_data in self.clients.items(): - connect_time = client_data.get('connect_time', 0) - if current_time - connect_time > self.connection_config['connection_timeout']: - timeout_clients.append(client_id) - - for client_id in timeout_clients: - self._disconnect_client(client_id, "timeout") - self.connection_stats['connections_timed_out'] += 1 - - # 触发连接超时回调 - self._trigger_connection_callback('connection_timeout', { - 'client_id': client_id, - 'reason': 'timeout' - }) - - except Exception as e: - print(f"✗ 连接超时检查失败: {e}") - - def _check_heartbeat_timeouts(self, current_time: float): - """ - 检查心跳超时 - - Args: - current_time: 当前时间 - """ - try: - timeout_clients = [] - for client_id, client_data in self.clients.items(): - last_heartbeat = client_data.get('last_heartbeat', 0) - if current_time - last_heartbeat > self.connection_config['heartbeat_interval'] * 3: - timeout_clients.append(client_id) - - for client_id in timeout_clients: - self._disconnect_client(client_id, "heartbeat_timeout") - - except Exception as e: - print(f"✗ 心跳超时检查失败: {e}") - - def connect_to_server(self, host: str, port: int = None) -> bool: - """ - 连接到服务器 - - Args: - host: 服务器地址 - port: 服务器端口 - - Returns: - 是否连接成功 - """ - try: - if self.client_state['is_connected']: - print("✗ 客户端已连接到服务器") - return False - - if port is None: - port = self.connection_config['server_port'] - - # 更新客户端状态 - self.client_state['server_address'] = host - self.client_state['server_port'] = port - self.client_state['connection_time'] = time.time() - self.client_state['reconnect_attempts'] = 0 - - # 通过网络管理器连接 - if self.plugin.network_manager: - success = self.plugin.network_manager.connect_to_server(host, port) - if success: - self.client_state['is_connected'] = True - print(f"✓ 已连接到服务器: {host}:{port}") - return True - else: - print("✗ 连接服务器失败") - return False - else: - print("✗ 网络管理器未初始化") - return False - - except Exception as e: - print(f"✗ 连接服务器失败: {e}") - import traceback - traceback.print_exc() - return False - - def disconnect_from_server(self): - """断开与服务器的连接""" - try: - self._disconnect_from_server() - print("✓ 已断开与服务器的连接") - except Exception as e: - print(f"✗ 断开服务器连接失败: {e}") - - def _disconnect_from_server(self): - """内部方法:断开与服务器的连接""" - try: - if self.plugin.network_manager: - self.plugin.network_manager.disconnect_client() - - self.client_state['is_connected'] = False - self.client_state['server_address'] = '' - self.client_state['connection_time'] = 0.0 - - except Exception as e: - print(f"✗ 服务器断开连接失败: {e}") - - def add_client(self, client_id: str, client_socket: socket.socket, - client_address: tuple) -> bool: - """ - 添加客户端连接 - - Args: - client_id: 客户端ID - client_socket: 客户端套接字 - client_address: 客户端地址 - - Returns: - 是否添加成功 - """ - try: - client_ip = client_address[0] - - # 检查IP是否被禁止 - if client_ip in self.banned_ips: - self.connection_stats['banned_connections'] += 1 - client_socket.close() - - # 触发连接禁止回调 - self._trigger_connection_callback('connection_banned', { - 'client_id': client_id, - 'ip': client_ip, - 'address': client_address - }) - - return False - - # 检查连接限制 - if self.connection_config['enable_connection_limits']: - ip_connections = sum(1 for client in self.clients.values() - if client.get('address', ('', 0))[0] == client_ip) - if ip_connections >= self.connection_config['max_connections_per_ip']: - self.connection_stats['connections_rejected'] += 1 - client_socket.close() - return False - - # 检查最大连接数 - if (self.connection_config['enable_connection_limits'] and - len(self.clients) >= self.connection_config['max_connections']): - self.connection_stats['connections_rejected'] += 1 - client_socket.close() - return False - - # 添加客户端 - self.clients[client_id] = { - 'socket': client_socket, - 'address': client_address, - 'connect_time': time.time(), - 'last_heartbeat': time.time(), - 'bytes_sent': 0, - 'bytes_received': 0, - 'ping': 0.0 - } - - # 更新服务器状态 - self.server_state['clients_connected'] = len(self.clients) - if len(self.clients) > self.server_state['peak_connections']: - self.server_state['peak_connections'] = len(self.clients) - - self.connection_stats['connections_accepted'] += 1 - - # 触发客户端连接回调 - self._trigger_connection_callback('client_connected', { - 'client_id': client_id, - 'address': client_address - }) - - print(f"✓ 客户端已连接: {client_id} ({client_address})") - return True - - except Exception as e: - print(f"✗ 添加客户端失败: {e}") - self.connection_stats['connections_rejected'] += 1 - return False - - def remove_client(self, client_id: str, reason: str = "unknown"): - """ - 移除客户端连接 - - Args: - client_id: 客户端ID - reason: 断开原因 - """ - try: - if client_id in self.clients: - client_data = self.clients[client_id] - - # 关闭套接字 - try: - client_data['socket'].close() - except Exception as e: - pass - - # 移除客户端 - del self.clients[client_id] - - # 更新服务器状态 - self.server_state['clients_connected'] = len(self.clients) - - self.connection_stats['connections_lost'] += 1 - - # 触发客户端断开连接回调 - self._trigger_connection_callback('client_disconnected', { - 'client_id': client_id, - 'reason': reason - }) - - print(f"✓ 客户端已断开: {client_id} (原因: {reason})") - - except Exception as e: - print(f"✗ 移除客户端失败: {e}") - - def _disconnect_client(self, client_id: str, reason: str = "unknown"): - """ - 断开客户端连接 - - Args: - client_id: 客户端ID - reason: 断开原因 - """ - self.remove_client(client_id, reason) - - def _disconnect_all_clients(self): - """断开所有客户端连接""" - try: - client_ids = list(self.clients.keys()) - for client_id in client_ids: - self._disconnect_client(client_id, "server_shutdown") - except Exception as e: - print(f"✗ 断开所有客户端连接失败: {e}") - - def send_heartbeat(self, client_id: str = None): - """ - 发送心跳包 - - Args: - client_id: 客户端ID(None表示服务器发送给所有客户端) - """ - try: - heartbeat_data = { - 'timestamp': time.time() - } - - if client_id: - # 发送给特定客户端 - if self.plugin.protocol_handler: - self.plugin.protocol_handler.send_message('HEARTBEAT', heartbeat_data, client_id) - else: - # 发送给所有客户端 - if self.plugin.protocol_handler: - for cid in self.clients.keys(): - self.plugin.protocol_handler.send_message('HEARTBEAT', heartbeat_data, cid) - - except Exception as e: - print(f"✗ 心跳包发送失败: {e}") - - def handle_heartbeat(self, client_id: str, data: Dict[str, Any]): - """ - 处理心跳包 - - Args: - client_id: 客户端ID - data: 心跳数据 - """ - try: - if client_id in self.clients: - self.clients[client_id]['last_heartbeat'] = time.time() - self.clients[client_id]['ping'] = time.time() - data.get('timestamp', time.time()) - except Exception as e: - print(f"✗ 心跳包处理失败: {e}") - - def ban_ip(self, ip_address: str): - """ - 禁止IP地址 - - Args: - ip_address: IP地址 - """ - try: - self.banned_ips.add(ip_address) - print(f"✓ IP地址已被禁止: {ip_address}") - - # 断开该IP的所有连接 - clients_to_disconnect = [ - client_id for client_id, client_data in self.clients.items() - if client_data.get('address', ('', 0))[0] == ip_address - ] - - for client_id in clients_to_disconnect: - self._disconnect_client(client_id, "banned") - - except Exception as e: - print(f"✗ IP禁止失败: {e}") - - def unban_ip(self, ip_address: str): - """ - 解除IP地址禁止 - - Args: - ip_address: IP地址 - """ - try: - if ip_address in self.banned_ips: - self.banned_ips.remove(ip_address) - print(f"✓ IP地址禁止已解除: {ip_address}") - except Exception as e: - print(f"✗ IP解除禁止失败: {e}") - - def get_clients(self) -> Dict[str, Any]: - """ - 获取所有客户端信息 - - Returns: - 客户端信息字典 - """ - try: - return {k: v.copy() for k, v in self.clients.items()} - except Exception as e: - print(f"✗ 客户端信息获取失败: {e}") - return {} - - def get_client_count(self) -> int: - """ - 获取客户端数量 - - Returns: - 客户端数量 - """ - return len(self.clients) - - def get_banned_ips(self) -> List[str]: - """ - 获取被禁止的IP地址列表 - - Returns: - 被禁止的IP地址列表 - """ - return list(self.banned_ips) - - def is_client_connected(self, client_id: str) -> bool: - """ - 检查客户端是否连接 - - Args: - client_id: 客户端ID - - Returns: - 客户端是否连接 - """ - return client_id in self.clients - - def get_server_state(self) -> Dict[str, Any]: - """ - 获取服务器状态 - - Returns: - 服务器状态字典 - """ - return self.server_state.copy() - - def get_client_state(self) -> Dict[str, Any]: - """ - 获取客户端状态 - - Returns: - 客户端状态字典 - """ - return self.client_state.copy() - - def get_connection_stats(self) -> Dict[str, Any]: - """ - 获取连接统计信息 - - Returns: - 连接统计字典 - """ - return self.connection_stats.copy() - - def reset_connection_stats(self): - """重置连接统计信息""" - try: - self.connection_stats = { - 'connections_accepted': 0, - 'connections_rejected': 0, - 'connections_lost': 0, - 'connections_timed_out': 0, - 'reconnections': 0, - 'banned_connections': 0 - } - print("✓ 连接统计信息已重置") - except Exception as e: - print(f"✗ 连接统计信息重置失败: {e}") - - def set_connection_config(self, config: Dict[str, Any]) -> bool: - """ - 设置连接配置 - - Args: - config: 连接配置字典 - - Returns: - 是否设置成功 - """ - try: - self.connection_config.update(config) - print(f"✓ 连接配置已更新: {self.connection_config}") - return True - except Exception as e: - print(f"✗ 连接配置设置失败: {e}") - return False - - def get_connection_config(self) -> Dict[str, Any]: - """ - 获取连接配置 - - Returns: - 连接配置字典 - """ - return self.connection_config.copy() - - def _trigger_connection_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发连接回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.connection_callbacks: - for callback in self.connection_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 连接回调执行失败: {e}") - except Exception as e: - print(f"✗ 连接回调触发失败: {e}") - - def register_connection_callback(self, callback_type: str, callback: callable): - """ - 注册连接回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.connection_callbacks: - self.connection_callbacks[callback_type].append(callback) - print(f"✓ 连接回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 连接回调注册失败: {e}") - - def unregister_connection_callback(self, callback_type: str, callback: callable): - """ - 注销连接回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.connection_callbacks: - if callback in self.connection_callbacks[callback_type]: - self.connection_callbacks[callback_type].remove(callback) - print(f"✓ 连接回调已注销: {callback_type}") - except Exception as e: +""" +连接管理器模块 +负责管理网络连接和客户端状态 +""" + +import time +from typing import Dict, Any, List, Optional +import threading +import socket + +class ConnectionManager: + """ + 连接管理器 + 负责管理网络连接和客户端状态 + """ + + def __init__(self, plugin): + """ + 初始化连接管理器 + + Args: + plugin: 网络同步插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 连接配置 + self.connection_config = { + 'max_connections': 32, + 'connection_timeout': 30.0, + 'reconnect_attempts': 3, + 'reconnect_interval': 5.0, + 'heartbeat_interval': 5.0, + 'enable_auto_reconnect': True, + 'enable_connection_limits': True, + 'max_connections_per_ip': 5 + } + + # 客户端管理 + self.clients = {} # 已连接的客户端 + self.pending_connections = {} # 待处理的连接 + self.banned_ips = set() # 被禁止的IP地址 + + # 服务器状态 + self.server_state = { + 'is_running': False, + 'port': 7777, + 'clients_connected': 0, + 'peak_connections': 0, + 'bytes_sent': 0, + 'bytes_received': 0 + } + + # 客户端状态 + self.client_state = { + 'is_connected': False, + 'server_address': '', + 'server_port': 7777, + 'connection_time': 0.0, + 'last_heartbeat': 0.0, + 'reconnect_attempts': 0 + } + + # 连接统计 + self.connection_stats = { + 'connections_accepted': 0, + 'connections_rejected': 0, + 'connections_lost': 0, + 'connections_timed_out': 0, + 'reconnections': 0, + 'banned_connections': 0 + } + + # 线程管理 + self.connection_thread = None + self.heartbeat_thread = None + self.reconnect_thread = None + self.running = False + + # 回调函数 + self.connection_callbacks = { + 'client_connected': [], + 'client_disconnected': [], + 'connection_timeout': [], + 'reconnect_attempt': [], + 'connection_banned': [] + } + + # 时间戳记录 + self.last_connection_check = 0.0 + self.last_heartbeat_check = 0.0 + self.last_reconnect_attempt = 0.0 + + print("✓ 连接管理器已创建") + + def initialize(self) -> bool: + """ + 初始化连接管理器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 连接管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 连接管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用连接管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 连接管理器未初始化") + return False + + self.enabled = True + self.running = True + + print("✓ 连接管理器已启用") + return True + + except Exception as e: + print(f"✗ 连接管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用连接管理器""" + try: + self.enabled = False + self.running = False + + # 断开所有连接 + self._disconnect_all_clients() + self._disconnect_from_server() + + print("✓ 连接管理器已禁用") + + except Exception as e: + print(f"✗ 连接管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理连接管理器资源""" + try: + self.disable() + self.clients.clear() + self.pending_connections.clear() + self.banned_ips.clear() + self.connection_callbacks.clear() + self.initialized = False + print("✓ 连接管理器资源已清理") + + except Exception as e: + print(f"✗ 连接管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新连接管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 检查连接超时 + if current_time - self.last_connection_check >= 1.0: # 每秒检查一次 + self._check_connection_timeouts(current_time) + self.last_connection_check = current_time + + # 检查心跳超时 + if current_time - self.last_heartbeat_check >= self.connection_config['heartbeat_interval']: + self._check_heartbeat_timeouts(current_time) + self.last_heartbeat_check = current_time + + except Exception as e: + print(f"✗ 连接管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _check_connection_timeouts(self, current_time: float): + """ + 检查连接超时 + + Args: + current_time: 当前时间 + """ + try: + timeout_clients = [] + for client_id, client_data in self.clients.items(): + connect_time = client_data.get('connect_time', 0) + if current_time - connect_time > self.connection_config['connection_timeout']: + timeout_clients.append(client_id) + + for client_id in timeout_clients: + self._disconnect_client(client_id, "timeout") + self.connection_stats['connections_timed_out'] += 1 + + # 触发连接超时回调 + self._trigger_connection_callback('connection_timeout', { + 'client_id': client_id, + 'reason': 'timeout' + }) + + except Exception as e: + print(f"✗ 连接超时检查失败: {e}") + + def _check_heartbeat_timeouts(self, current_time: float): + """ + 检查心跳超时 + + Args: + current_time: 当前时间 + """ + try: + timeout_clients = [] + for client_id, client_data in self.clients.items(): + last_heartbeat = client_data.get('last_heartbeat', 0) + if current_time - last_heartbeat > self.connection_config['heartbeat_interval'] * 3: + timeout_clients.append(client_id) + + for client_id in timeout_clients: + self._disconnect_client(client_id, "heartbeat_timeout") + + except Exception as e: + print(f"✗ 心跳超时检查失败: {e}") + + def connect_to_server(self, host: str, port: int = None) -> bool: + """ + 连接到服务器 + + Args: + host: 服务器地址 + port: 服务器端口 + + Returns: + 是否连接成功 + """ + try: + if self.client_state['is_connected']: + print("✗ 客户端已连接到服务器") + return False + + if port is None: + port = self.connection_config['server_port'] + + # 更新客户端状态 + self.client_state['server_address'] = host + self.client_state['server_port'] = port + self.client_state['connection_time'] = time.time() + self.client_state['reconnect_attempts'] = 0 + + # 通过网络管理器连接 + if self.plugin.network_manager: + success = self.plugin.network_manager.connect_to_server(host, port) + if success: + self.client_state['is_connected'] = True + print(f"✓ 已连接到服务器: {host}:{port}") + return True + else: + print("✗ 连接服务器失败") + return False + else: + print("✗ 网络管理器未初始化") + return False + + except Exception as e: + print(f"✗ 连接服务器失败: {e}") + import traceback + traceback.print_exc() + return False + + def disconnect_from_server(self): + """断开与服务器的连接""" + try: + self._disconnect_from_server() + print("✓ 已断开与服务器的连接") + except Exception as e: + print(f"✗ 断开服务器连接失败: {e}") + + def _disconnect_from_server(self): + """内部方法:断开与服务器的连接""" + try: + if self.plugin.network_manager: + self.plugin.network_manager.disconnect_client() + + self.client_state['is_connected'] = False + self.client_state['server_address'] = '' + self.client_state['connection_time'] = 0.0 + + except Exception as e: + print(f"✗ 服务器断开连接失败: {e}") + + def add_client(self, client_id: str, client_socket: socket.socket, + client_address: tuple) -> bool: + """ + 添加客户端连接 + + Args: + client_id: 客户端ID + client_socket: 客户端套接字 + client_address: 客户端地址 + + Returns: + 是否添加成功 + """ + try: + client_ip = client_address[0] + + # 检查IP是否被禁止 + if client_ip in self.banned_ips: + self.connection_stats['banned_connections'] += 1 + client_socket.close() + + # 触发连接禁止回调 + self._trigger_connection_callback('connection_banned', { + 'client_id': client_id, + 'ip': client_ip, + 'address': client_address + }) + + return False + + # 检查连接限制 + if self.connection_config['enable_connection_limits']: + ip_connections = sum(1 for client in self.clients.values() + if client.get('address', ('', 0))[0] == client_ip) + if ip_connections >= self.connection_config['max_connections_per_ip']: + self.connection_stats['connections_rejected'] += 1 + client_socket.close() + return False + + # 检查最大连接数 + if (self.connection_config['enable_connection_limits'] and + len(self.clients) >= self.connection_config['max_connections']): + self.connection_stats['connections_rejected'] += 1 + client_socket.close() + return False + + # 添加客户端 + self.clients[client_id] = { + 'socket': client_socket, + 'address': client_address, + 'connect_time': time.time(), + 'last_heartbeat': time.time(), + 'bytes_sent': 0, + 'bytes_received': 0, + 'ping': 0.0 + } + + # 更新服务器状态 + self.server_state['clients_connected'] = len(self.clients) + if len(self.clients) > self.server_state['peak_connections']: + self.server_state['peak_connections'] = len(self.clients) + + self.connection_stats['connections_accepted'] += 1 + + # 触发客户端连接回调 + self._trigger_connection_callback('client_connected', { + 'client_id': client_id, + 'address': client_address + }) + + print(f"✓ 客户端已连接: {client_id} ({client_address})") + return True + + except Exception as e: + print(f"✗ 添加客户端失败: {e}") + self.connection_stats['connections_rejected'] += 1 + return False + + def remove_client(self, client_id: str, reason: str = "unknown"): + """ + 移除客户端连接 + + Args: + client_id: 客户端ID + reason: 断开原因 + """ + try: + if client_id in self.clients: + client_data = self.clients[client_id] + + # 关闭套接字 + try: + client_data['socket'].close() + except Exception as e: + pass + + # 移除客户端 + del self.clients[client_id] + + # 更新服务器状态 + self.server_state['clients_connected'] = len(self.clients) + + self.connection_stats['connections_lost'] += 1 + + # 触发客户端断开连接回调 + self._trigger_connection_callback('client_disconnected', { + 'client_id': client_id, + 'reason': reason + }) + + print(f"✓ 客户端已断开: {client_id} (原因: {reason})") + + except Exception as e: + print(f"✗ 移除客户端失败: {e}") + + def _disconnect_client(self, client_id: str, reason: str = "unknown"): + """ + 断开客户端连接 + + Args: + client_id: 客户端ID + reason: 断开原因 + """ + self.remove_client(client_id, reason) + + def _disconnect_all_clients(self): + """断开所有客户端连接""" + try: + client_ids = list(self.clients.keys()) + for client_id in client_ids: + self._disconnect_client(client_id, "server_shutdown") + except Exception as e: + print(f"✗ 断开所有客户端连接失败: {e}") + + def send_heartbeat(self, client_id: str = None): + """ + 发送心跳包 + + Args: + client_id: 客户端ID(None表示服务器发送给所有客户端) + """ + try: + heartbeat_data = { + 'timestamp': time.time() + } + + if client_id: + # 发送给特定客户端 + if self.plugin.protocol_handler: + self.plugin.protocol_handler.send_message('HEARTBEAT', heartbeat_data, client_id) + else: + # 发送给所有客户端 + if self.plugin.protocol_handler: + for cid in self.clients.keys(): + self.plugin.protocol_handler.send_message('HEARTBEAT', heartbeat_data, cid) + + except Exception as e: + print(f"✗ 心跳包发送失败: {e}") + + def handle_heartbeat(self, client_id: str, data: Dict[str, Any]): + """ + 处理心跳包 + + Args: + client_id: 客户端ID + data: 心跳数据 + """ + try: + if client_id in self.clients: + self.clients[client_id]['last_heartbeat'] = time.time() + self.clients[client_id]['ping'] = time.time() - data.get('timestamp', time.time()) + except Exception as e: + print(f"✗ 心跳包处理失败: {e}") + + def ban_ip(self, ip_address: str): + """ + 禁止IP地址 + + Args: + ip_address: IP地址 + """ + try: + self.banned_ips.add(ip_address) + print(f"✓ IP地址已被禁止: {ip_address}") + + # 断开该IP的所有连接 + clients_to_disconnect = [ + client_id for client_id, client_data in self.clients.items() + if client_data.get('address', ('', 0))[0] == ip_address + ] + + for client_id in clients_to_disconnect: + self._disconnect_client(client_id, "banned") + + except Exception as e: + print(f"✗ IP禁止失败: {e}") + + def unban_ip(self, ip_address: str): + """ + 解除IP地址禁止 + + Args: + ip_address: IP地址 + """ + try: + if ip_address in self.banned_ips: + self.banned_ips.remove(ip_address) + print(f"✓ IP地址禁止已解除: {ip_address}") + except Exception as e: + print(f"✗ IP解除禁止失败: {e}") + + def get_clients(self) -> Dict[str, Any]: + """ + 获取所有客户端信息 + + Returns: + 客户端信息字典 + """ + try: + return {k: v.copy() for k, v in self.clients.items()} + except Exception as e: + print(f"✗ 客户端信息获取失败: {e}") + return {} + + def get_client_count(self) -> int: + """ + 获取客户端数量 + + Returns: + 客户端数量 + """ + return len(self.clients) + + def get_banned_ips(self) -> List[str]: + """ + 获取被禁止的IP地址列表 + + Returns: + 被禁止的IP地址列表 + """ + return list(self.banned_ips) + + def is_client_connected(self, client_id: str) -> bool: + """ + 检查客户端是否连接 + + Args: + client_id: 客户端ID + + Returns: + 客户端是否连接 + """ + return client_id in self.clients + + def get_server_state(self) -> Dict[str, Any]: + """ + 获取服务器状态 + + Returns: + 服务器状态字典 + """ + return self.server_state.copy() + + def get_client_state(self) -> Dict[str, Any]: + """ + 获取客户端状态 + + Returns: + 客户端状态字典 + """ + return self.client_state.copy() + + def get_connection_stats(self) -> Dict[str, Any]: + """ + 获取连接统计信息 + + Returns: + 连接统计字典 + """ + return self.connection_stats.copy() + + def reset_connection_stats(self): + """重置连接统计信息""" + try: + self.connection_stats = { + 'connections_accepted': 0, + 'connections_rejected': 0, + 'connections_lost': 0, + 'connections_timed_out': 0, + 'reconnections': 0, + 'banned_connections': 0 + } + print("✓ 连接统计信息已重置") + except Exception as e: + print(f"✗ 连接统计信息重置失败: {e}") + + def set_connection_config(self, config: Dict[str, Any]) -> bool: + """ + 设置连接配置 + + Args: + config: 连接配置字典 + + Returns: + 是否设置成功 + """ + try: + self.connection_config.update(config) + print(f"✓ 连接配置已更新: {self.connection_config}") + return True + except Exception as e: + print(f"✗ 连接配置设置失败: {e}") + return False + + def get_connection_config(self) -> Dict[str, Any]: + """ + 获取连接配置 + + Returns: + 连接配置字典 + """ + return self.connection_config.copy() + + def _trigger_connection_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发连接回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.connection_callbacks: + for callback in self.connection_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 连接回调执行失败: {e}") + except Exception as e: + print(f"✗ 连接回调触发失败: {e}") + + def register_connection_callback(self, callback_type: str, callback: callable): + """ + 注册连接回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.connection_callbacks: + self.connection_callbacks[callback_type].append(callback) + print(f"✓ 连接回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 连接回调注册失败: {e}") + + def unregister_connection_callback(self, callback_type: str, callback: callable): + """ + 注销连接回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.connection_callbacks: + if callback in self.connection_callbacks[callback_type]: + self.connection_callbacks[callback_type].remove(callback) + print(f"✓ 连接回调已注销: {callback_type}") + except Exception as e: print(f"✗ 连接回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/network_sync/core/sync_manager.py b/plugins/user/network_sync/core/sync_manager.py index 4354ef09..b7eba638 100644 --- a/plugins/user/network_sync/core/sync_manager.py +++ b/plugins/user/network_sync/core/sync_manager.py @@ -1,777 +1,777 @@ -""" -同步管理器模块 -负责管理网络同步的核心功能 -""" - -import time -from typing import Dict, Any, List, Optional -import threading - -class SyncManager: - """ - 同步管理器 - 负责管理网络同步的核心功能 - """ - - def __init__(self, plugin): - """ - 初始化同步管理器 - - Args: - plugin: 网络同步插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 同步配置 - self.sync_config = { - 'sync_rate': 20, # 每秒同步频率 - 'interpolation_rate': 60, # 插值更新频率 - 'extrapolation_limit': 0.1, # 外推限制(秒) - 'sync_distance_threshold': 0.01, # 同步距离阈值 - 'sync_rotation_threshold': 0.1, # 同步旋转阈值 - 'sync_scale_threshold': 0.01, # 同步缩放阈值 - 'enable_delta_compression': True, # 启用增量压缩 - 'enable_state_filtering': True, # 启用状态过滤 - 'max_sync_objects': 1000, # 最大同步对象数 - 'sync_area_radius': 100.0 # 同步区域半径 - } - - # 同步对象管理 - self.sync_objects = {} # 所有同步对象 - self.local_objects = {} # 本地对象 - self.remote_objects = {} # 远程对象 - self.sync_groups = {} # 同步组 - - # 同步状态 - self.sync_state = { - 'last_sync_time': 0.0, - 'last_interpolation_time': 0.0, - 'sync_objects_count': 0, - 'local_objects_count': 0, - 'remote_objects_count': 0, - 'sync_groups_count': 0 - } - - # 插值管理 - self.interpolation_data = {} - - # 外推管理 - self.extrapolation_data = {} - - # 同步统计 - self.sync_stats = { - 'objects_synced': 0, - 'sync_operations': 0, - 'interpolations': 0, - 'extrapolations': 0, - 'sync_errors': 0, - 'delta_compressions': 0 - } - - # 线程管理 - self.sync_thread = None - self.interpolation_thread = None - self.running = False - - # 回调函数 - self.sync_callbacks = { - 'object_synced': [], - 'sync_error': [], - 'interpolation_updated': [], - 'extrapolation_updated': [] - } - - # 时间戳记录 - self.last_update_time = 0.0 - self.last_sync_check = 0.0 - - print("✓ 同步管理器已创建") - - def initialize(self) -> bool: - """ - 初始化同步管理器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 同步管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 同步管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用同步管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 同步管理器未初始化") - return False - - self.enabled = True - self.running = True - - # 启动同步线程 - self.sync_thread = threading.Thread(target=self._sync_loop, daemon=True) - self.sync_thread.start() - - # 启动插值线程 - self.interpolation_thread = threading.Thread(target=self._interpolation_loop, daemon=True) - self.interpolation_thread.start() - - print("✓ 同步管理器已启用") - return True - - except Exception as e: - print(f"✗ 同步管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用同步管理器""" - try: - self.enabled = False - self.running = False - - # 等待线程结束 - if self.sync_thread and self.sync_thread.is_alive(): - self.sync_thread.join(timeout=1.0) - - if self.interpolation_thread and self.interpolation_thread.is_alive(): - self.interpolation_thread.join(timeout=1.0) - - print("✓ 同步管理器已禁用") - - except Exception as e: - print(f"✗ 同步管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理同步管理器资源""" - try: - self.disable() - self.sync_objects.clear() - self.local_objects.clear() - self.remote_objects.clear() - self.sync_groups.clear() - self.interpolation_data.clear() - self.extrapolation_data.clear() - self.sync_callbacks.clear() - self.initialized = False - print("✓ 同步管理器资源已清理") - - except Exception as e: - print(f"✗ 同步管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新同步管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.last_update_time = current_time - - except Exception as e: - print(f"✗ 同步管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _sync_loop(self): - """同步循环线程""" - try: - sync_interval = 1.0 / self.sync_config['sync_rate'] - - while self.running: - if self.enabled: - current_time = time.time() - if current_time - self.sync_state['last_sync_time'] >= sync_interval: - self._perform_sync() - self.sync_state['last_sync_time'] = current_time - - time.sleep(0.001) # 1ms - - except Exception as e: - print(f"✗ 同步循环失败: {e}") - - def _interpolation_loop(self): - """插值循环线程""" - try: - interpolation_interval = 1.0 / self.sync_config['interpolation_rate'] - - while self.running: - if self.enabled: - current_time = time.time() - if current_time - self.sync_state['last_interpolation_time'] >= interpolation_interval: - self._perform_interpolation() - self.sync_state['last_interpolation_time'] = current_time - - time.sleep(0.001) # 1ms - - except Exception as e: - print(f"✗ 插值循环失败: {e}") - - def _perform_sync(self): - """执行同步操作""" - try: - # 收集需要同步的对象 - objects_to_sync = self._collect_sync_objects() - - # 执行同步 - for obj_id, obj_data in objects_to_sync.items(): - if self.plugin.object_sync: - success = self.plugin.object_sync.sync_object(obj_id, obj_data) - if success: - self.sync_stats['objects_synced'] += 1 - self._trigger_sync_callback('object_synced', { - 'object_id': obj_id, - 'object_data': obj_data - }) - else: - self.sync_stats['sync_errors'] += 1 - self._trigger_sync_callback('sync_error', { - 'object_id': obj_id, - 'error': 'Sync failed' - }) - - self.sync_stats['sync_operations'] += 1 - - except Exception as e: - print(f"✗ 同步操作失败: {e}") - self.sync_stats['sync_errors'] += 1 - - def _perform_interpolation(self): - """执行插值操作""" - try: - current_time = time.time() - - # 更新插值数据 - for obj_id, interp_data in self.interpolation_data.items(): - if self._update_interpolation(obj_id, interp_data, current_time): - self.sync_stats['interpolations'] += 1 - self._trigger_sync_callback('interpolation_updated', { - 'object_id': obj_id, - 'interpolation_data': interp_data - }) - - # 更新外推数据 - for obj_id, extrap_data in self.extrapolation_data.items(): - if self._update_extrapolation(obj_id, extrap_data, current_time): - self.sync_stats['extrapolations'] += 1 - self._trigger_sync_callback('extrapolation_updated', { - 'object_id': obj_id, - 'extrapolation_data': extrap_data - }) - - except Exception as e: - print(f"✗ 插值操作失败: {e}") - - def _collect_sync_objects(self) -> Dict[str, Any]: - """ - 收集需要同步的对象 - - Returns: - 需要同步的对象字典 - """ - try: - objects_to_sync = {} - - # 收集本地对象中需要同步的 - for obj_id, obj_data in self.local_objects.items(): - # 检查对象是否需要同步(基于变化阈值) - if self._should_sync_object(obj_id, obj_data): - objects_to_sync[obj_id] = obj_data - - return objects_to_sync - - except Exception as e: - print(f"✗ 同步对象收集失败: {e}") - return {} - - def _should_sync_object(self, obj_id: str, obj_data: Dict[str, Any]) -> bool: - """ - 检查对象是否需要同步 - - Args: - obj_id: 对象ID - obj_data: 对象数据 - - Returns: - 是否需要同步 - """ - try: - # 检查是否超出同步区域 - if not self._is_in_sync_area(obj_data): - return False - - # 检查是否有显著变化 - if obj_id in self.sync_objects: - old_data = self.sync_objects[obj_id] - # 检查位置变化 - if 'position' in obj_data and 'position' in old_data: - pos_diff = self._calculate_distance(obj_data['position'], old_data['position']) - if pos_diff > self.sync_config['sync_distance_threshold']: - return True - - # 检查旋转变化 - if 'rotation' in obj_data and 'rotation' in old_data: - rot_diff = self._calculate_rotation_difference(obj_data['rotation'], old_data['rotation']) - if rot_diff > self.sync_config['sync_rotation_threshold']: - return True - - # 检查缩放变化 - if 'scale' in obj_data and 'scale' in old_data: - scale_diff = self._calculate_scale_difference(obj_data['scale'], old_data['scale']) - if scale_diff > self.sync_config['sync_scale_threshold']: - return True - - else: - # 新对象总是需要同步 - return True - - return False - - except Exception as e: - print(f"✗ 对象同步检查失败: {e}") - return True # 出错时默认同步 - - def _is_in_sync_area(self, obj_data: Dict[str, Any]) -> bool: - """ - 检查对象是否在同步区域内 - - Args: - obj_data: 对象数据 - - Returns: - 是否在同步区域内 - """ - try: - # 这里应该根据具体的游戏逻辑实现 - # 简单实现:假设所有对象都在同步区域内 - return True - except Exception as e: - print(f"✗ 同步区域检查失败: {e}") - return True - - def _calculate_distance(self, pos1: List[float], pos2: List[float]) -> float: - """ - 计算两点间距离 - - Args: - pos1: 位置1 - pos2: 位置2 - - Returns: - 距离 - """ - try: - if len(pos1) >= 3 and len(pos2) >= 3: - dx = pos1[0] - pos2[0] - dy = pos1[1] - pos2[1] - dz = pos1[2] - pos2[2] - return (dx*dx + dy*dy + dz*dz) ** 0.5 - elif len(pos1) >= 2 and len(pos2) >= 2: - dx = pos1[0] - pos2[0] - dy = pos1[1] - pos2[1] - return (dx*dx + dy*dy) ** 0.5 - else: - return 0.0 - except Exception as e: - print(f"✗ 距离计算失败: {e}") - return 0.0 - - def _calculate_rotation_difference(self, rot1: List[float], rot2: List[float]) -> float: - """ - 计算旋转差异 - - Args: - rot1: 旋转1 - rot2: 旋转2 - - Returns: - 旋转差异 - """ - try: - # 简单实现:计算欧拉角差异 - diff = 0.0 - for i in range(min(len(rot1), len(rot2))): - diff += abs(rot1[i] - rot2[i]) - return diff - except Exception as e: - print(f"✗ 旋转差异计算失败: {e}") - return 0.0 - - def _calculate_scale_difference(self, scale1: List[float], scale2: List[float]) -> float: - """ - 计算缩放差异 - - Args: - scale1: 缩放1 - scale2: 缩放2 - - Returns: - 缩放差异 - """ - try: - # 简单实现:计算缩放差异 - diff = 0.0 - for i in range(min(len(scale1), len(scale2))): - diff += abs(scale1[i] - scale2[i]) - return diff - except Exception as e: - print(f"✗ 缩放差异计算失败: {e}") - return 0.0 - - def _update_interpolation(self, obj_id: str, interp_data: Dict[str, Any], current_time: float) -> bool: - """ - 更新插值数据 - - Args: - obj_id: 对象ID - interp_data: 插值数据 - current_time: 当前时间 - - Returns: - 是否更新成功 - """ - try: - # 简单实现:线性插值 - start_time = interp_data.get('start_time', current_time) - end_time = interp_data.get('end_time', current_time) - duration = end_time - start_time - - if duration > 0: - progress = (current_time - start_time) / duration - progress = max(0.0, min(1.0, progress)) # 限制在0-1之间 - - # 更新对象状态 - if obj_id in self.remote_objects: - obj = self.remote_objects[obj_id] - # 这里应该实际更新对象的位置、旋转等属性 - # 由于是模拟实现,只打印信息 - pass - - return True - else: - return False - - except Exception as e: - print(f"✗ 插值更新失败: {e}") - return False - - def _update_extrapolation(self, obj_id: str, extrap_data: Dict[str, Any], current_time: float) -> bool: - """ - 更新外推数据 - - Args: - obj_id: 对象ID - extrap_data: 外推数据 - current_time: 当前时间 - - Returns: - 是否更新成功 - """ - try: - # 简单实现:基于速度的外推 - last_update_time = extrap_data.get('last_update_time', current_time) - time_diff = current_time - last_update_time - - if time_diff <= self.sync_config['extrapolation_limit']: - # 在外推限制内,可以进行外推 - # 这里应该实际更新对象的状态 - # 由于是模拟实现,只打印信息 - pass - return True - else: - return False - - except Exception as e: - print(f"✗ 外推更新失败: {e}") - return False - - def register_local_object(self, obj_id: str, obj_data: Dict[str, Any]) -> bool: - """ - 注册本地对象 - - Args: - obj_id: 对象ID - obj_data: 对象数据 - - Returns: - 是否注册成功 - """ - try: - self.local_objects[obj_id] = obj_data - self.sync_objects[obj_id] = obj_data - self.sync_state['local_objects_count'] = len(self.local_objects) - self.sync_state['sync_objects_count'] = len(self.sync_objects) - - print(f"✓ 本地对象已注册: {obj_id}") - return True - - except Exception as e: - print(f"✗ 本地对象注册失败: {e}") - return False - - def unregister_local_object(self, obj_id: str) -> bool: - """ - 注销本地对象 - - Args: - obj_id: 对象ID - - Returns: - 是否注销成功 - """ - try: - if obj_id in self.local_objects: - del self.local_objects[obj_id] - - if obj_id in self.sync_objects: - del self.sync_objects[obj_id] - - self.sync_state['local_objects_count'] = len(self.local_objects) - self.sync_state['sync_objects_count'] = len(self.sync_objects) - - print(f"✓ 本地对象已注销: {obj_id}") - return True - - except Exception as e: - print(f"✗ 本地对象注销失败: {e}") - return False - - def register_remote_object(self, obj_id: str, obj_data: Dict[str, Any]) -> bool: - """ - 注册远程对象 - - Args: - obj_id: 对象ID - obj_data: 对象数据 - - Returns: - 是否注册成功 - """ - try: - self.remote_objects[obj_id] = obj_data - self.sync_objects[obj_id] = obj_data - self.sync_state['remote_objects_count'] = len(self.remote_objects) - self.sync_state['sync_objects_count'] = len(self.sync_objects) - - print(f"✓ 远程对象已注册: {obj_id}") - return True - - except Exception as e: - print(f"✗ 远程对象注册失败: {e}") - return False - - def unregister_remote_object(self, obj_id: str) -> bool: - """ - 注销远程对象 - - Args: - obj_id: 对象ID - - Returns: - 是否注销成功 - """ - try: - if obj_id in self.remote_objects: - del self.remote_objects[obj_id] - - if obj_id in self.sync_objects: - del self.sync_objects[obj_id] - - self.sync_state['remote_objects_count'] = len(self.remote_objects) - self.sync_state['sync_objects_count'] = len(self.sync_objects) - - print(f"✓ 远程对象已注销: {obj_id}") - return True - - except Exception as e: - print(f"✗ 远程对象注销失败: {e}") - return False - - def update_object_data(self, obj_id: str, obj_data: Dict[str, Any]) -> bool: - """ - 更新对象数据 - - Args: - obj_id: 对象ID - obj_data: 对象数据 - - Returns: - 是否更新成功 - """ - try: - if obj_id in self.local_objects: - self.local_objects[obj_id].update(obj_data) - - if obj_id in self.sync_objects: - self.sync_objects[obj_id].update(obj_data) - - return True - - except Exception as e: - print(f"✗ 对象数据更新失败: {e}") - return False - - def get_object_data(self, obj_id: str) -> Optional[Dict[str, Any]]: - """ - 获取对象数据 - - Args: - obj_id: 对象ID - - Returns: - 对象数据或None - """ - return self.sync_objects.get(obj_id) - - def get_sync_objects(self) -> Dict[str, Any]: - """ - 获取所有同步对象 - - Returns: - 同步对象字典 - """ - return self.sync_objects.copy() - - def get_local_objects(self) -> Dict[str, Any]: - """ - 获取所有本地对象 - - Returns: - 本地对象字典 - """ - return self.local_objects.copy() - - def get_remote_objects(self) -> Dict[str, Any]: - """ - 获取所有远程对象 - - Returns: - 远程对象字典 - """ - return self.remote_objects.copy() - - def set_sync_config(self, config: Dict[str, Any]) -> bool: - """ - 设置同步配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.sync_config.update(config) - print(f"✓ 同步配置已更新: {self.sync_config}") - return True - except Exception as e: - print(f"✗ 同步配置设置失败: {e}") - return False - - def get_sync_config(self) -> Dict[str, Any]: - """ - 获取同步配置 - - Returns: - 配置字典 - """ - return self.sync_config.copy() - - def get_sync_stats(self) -> Dict[str, Any]: - """ - 获取同步统计信息 - - Returns: - 统计信息字典 - """ - return self.sync_stats.copy() - - def reset_sync_stats(self): - """重置同步统计信息""" - try: - self.sync_stats = { - 'objects_synced': 0, - 'sync_operations': 0, - 'interpolations': 0, - 'extrapolations': 0, - 'sync_errors': 0, - 'delta_compressions': 0 - } - print("✓ 同步统计信息已重置") - except Exception as e: - print(f"✗ 同步统计信息重置失败: {e}") - - def _trigger_sync_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发同步回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.sync_callbacks: - for callback in self.sync_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 同步回调执行失败: {e}") - except Exception as e: - print(f"✗ 同步回调触发失败: {e}") - - def register_sync_callback(self, callback_type: str, callback: callable): - """ - 注册同步回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.sync_callbacks: - self.sync_callbacks[callback_type].append(callback) - print(f"✓ 同步回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 同步回调注册失败: {e}") - - def unregister_sync_callback(self, callback_type: str, callback: callable): - """ - 注销同步回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.sync_callbacks: - if callback in self.sync_callbacks[callback_type]: - self.sync_callbacks[callback_type].remove(callback) - print(f"✓ 同步回调已注销: {callback_type}") - except Exception as e: +""" +同步管理器模块 +负责管理网络同步的核心功能 +""" + +import time +from typing import Dict, Any, List, Optional +import threading + +class SyncManager: + """ + 同步管理器 + 负责管理网络同步的核心功能 + """ + + def __init__(self, plugin): + """ + 初始化同步管理器 + + Args: + plugin: 网络同步插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 同步配置 + self.sync_config = { + 'sync_rate': 20, # 每秒同步频率 + 'interpolation_rate': 60, # 插值更新频率 + 'extrapolation_limit': 0.1, # 外推限制(秒) + 'sync_distance_threshold': 0.01, # 同步距离阈值 + 'sync_rotation_threshold': 0.1, # 同步旋转阈值 + 'sync_scale_threshold': 0.01, # 同步缩放阈值 + 'enable_delta_compression': True, # 启用增量压缩 + 'enable_state_filtering': True, # 启用状态过滤 + 'max_sync_objects': 1000, # 最大同步对象数 + 'sync_area_radius': 100.0 # 同步区域半径 + } + + # 同步对象管理 + self.sync_objects = {} # 所有同步对象 + self.local_objects = {} # 本地对象 + self.remote_objects = {} # 远程对象 + self.sync_groups = {} # 同步组 + + # 同步状态 + self.sync_state = { + 'last_sync_time': 0.0, + 'last_interpolation_time': 0.0, + 'sync_objects_count': 0, + 'local_objects_count': 0, + 'remote_objects_count': 0, + 'sync_groups_count': 0 + } + + # 插值管理 + self.interpolation_data = {} + + # 外推管理 + self.extrapolation_data = {} + + # 同步统计 + self.sync_stats = { + 'objects_synced': 0, + 'sync_operations': 0, + 'interpolations': 0, + 'extrapolations': 0, + 'sync_errors': 0, + 'delta_compressions': 0 + } + + # 线程管理 + self.sync_thread = None + self.interpolation_thread = None + self.running = False + + # 回调函数 + self.sync_callbacks = { + 'object_synced': [], + 'sync_error': [], + 'interpolation_updated': [], + 'extrapolation_updated': [] + } + + # 时间戳记录 + self.last_update_time = 0.0 + self.last_sync_check = 0.0 + + print("✓ 同步管理器已创建") + + def initialize(self) -> bool: + """ + 初始化同步管理器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 同步管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 同步管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用同步管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 同步管理器未初始化") + return False + + self.enabled = True + self.running = True + + # 启动同步线程 + self.sync_thread = threading.Thread(target=self._sync_loop, daemon=True) + self.sync_thread.start() + + # 启动插值线程 + self.interpolation_thread = threading.Thread(target=self._interpolation_loop, daemon=True) + self.interpolation_thread.start() + + print("✓ 同步管理器已启用") + return True + + except Exception as e: + print(f"✗ 同步管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用同步管理器""" + try: + self.enabled = False + self.running = False + + # 等待线程结束 + if self.sync_thread and self.sync_thread.is_alive(): + self.sync_thread.join(timeout=1.0) + + if self.interpolation_thread and self.interpolation_thread.is_alive(): + self.interpolation_thread.join(timeout=1.0) + + print("✓ 同步管理器已禁用") + + except Exception as e: + print(f"✗ 同步管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理同步管理器资源""" + try: + self.disable() + self.sync_objects.clear() + self.local_objects.clear() + self.remote_objects.clear() + self.sync_groups.clear() + self.interpolation_data.clear() + self.extrapolation_data.clear() + self.sync_callbacks.clear() + self.initialized = False + print("✓ 同步管理器资源已清理") + + except Exception as e: + print(f"✗ 同步管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新同步管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.last_update_time = current_time + + except Exception as e: + print(f"✗ 同步管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _sync_loop(self): + """同步循环线程""" + try: + sync_interval = 1.0 / self.sync_config['sync_rate'] + + while self.running: + if self.enabled: + current_time = time.time() + if current_time - self.sync_state['last_sync_time'] >= sync_interval: + self._perform_sync() + self.sync_state['last_sync_time'] = current_time + + time.sleep(0.001) # 1ms + + except Exception as e: + print(f"✗ 同步循环失败: {e}") + + def _interpolation_loop(self): + """插值循环线程""" + try: + interpolation_interval = 1.0 / self.sync_config['interpolation_rate'] + + while self.running: + if self.enabled: + current_time = time.time() + if current_time - self.sync_state['last_interpolation_time'] >= interpolation_interval: + self._perform_interpolation() + self.sync_state['last_interpolation_time'] = current_time + + time.sleep(0.001) # 1ms + + except Exception as e: + print(f"✗ 插值循环失败: {e}") + + def _perform_sync(self): + """执行同步操作""" + try: + # 收集需要同步的对象 + objects_to_sync = self._collect_sync_objects() + + # 执行同步 + for obj_id, obj_data in objects_to_sync.items(): + if self.plugin.object_sync: + success = self.plugin.object_sync.sync_object(obj_id, obj_data) + if success: + self.sync_stats['objects_synced'] += 1 + self._trigger_sync_callback('object_synced', { + 'object_id': obj_id, + 'object_data': obj_data + }) + else: + self.sync_stats['sync_errors'] += 1 + self._trigger_sync_callback('sync_error', { + 'object_id': obj_id, + 'error': 'Sync failed' + }) + + self.sync_stats['sync_operations'] += 1 + + except Exception as e: + print(f"✗ 同步操作失败: {e}") + self.sync_stats['sync_errors'] += 1 + + def _perform_interpolation(self): + """执行插值操作""" + try: + current_time = time.time() + + # 更新插值数据 + for obj_id, interp_data in self.interpolation_data.items(): + if self._update_interpolation(obj_id, interp_data, current_time): + self.sync_stats['interpolations'] += 1 + self._trigger_sync_callback('interpolation_updated', { + 'object_id': obj_id, + 'interpolation_data': interp_data + }) + + # 更新外推数据 + for obj_id, extrap_data in self.extrapolation_data.items(): + if self._update_extrapolation(obj_id, extrap_data, current_time): + self.sync_stats['extrapolations'] += 1 + self._trigger_sync_callback('extrapolation_updated', { + 'object_id': obj_id, + 'extrapolation_data': extrap_data + }) + + except Exception as e: + print(f"✗ 插值操作失败: {e}") + + def _collect_sync_objects(self) -> Dict[str, Any]: + """ + 收集需要同步的对象 + + Returns: + 需要同步的对象字典 + """ + try: + objects_to_sync = {} + + # 收集本地对象中需要同步的 + for obj_id, obj_data in self.local_objects.items(): + # 检查对象是否需要同步(基于变化阈值) + if self._should_sync_object(obj_id, obj_data): + objects_to_sync[obj_id] = obj_data + + return objects_to_sync + + except Exception as e: + print(f"✗ 同步对象收集失败: {e}") + return {} + + def _should_sync_object(self, obj_id: str, obj_data: Dict[str, Any]) -> bool: + """ + 检查对象是否需要同步 + + Args: + obj_id: 对象ID + obj_data: 对象数据 + + Returns: + 是否需要同步 + """ + try: + # 检查是否超出同步区域 + if not self._is_in_sync_area(obj_data): + return False + + # 检查是否有显著变化 + if obj_id in self.sync_objects: + old_data = self.sync_objects[obj_id] + # 检查位置变化 + if 'position' in obj_data and 'position' in old_data: + pos_diff = self._calculate_distance(obj_data['position'], old_data['position']) + if pos_diff > self.sync_config['sync_distance_threshold']: + return True + + # 检查旋转变化 + if 'rotation' in obj_data and 'rotation' in old_data: + rot_diff = self._calculate_rotation_difference(obj_data['rotation'], old_data['rotation']) + if rot_diff > self.sync_config['sync_rotation_threshold']: + return True + + # 检查缩放变化 + if 'scale' in obj_data and 'scale' in old_data: + scale_diff = self._calculate_scale_difference(obj_data['scale'], old_data['scale']) + if scale_diff > self.sync_config['sync_scale_threshold']: + return True + + else: + # 新对象总是需要同步 + return True + + return False + + except Exception as e: + print(f"✗ 对象同步检查失败: {e}") + return True # 出错时默认同步 + + def _is_in_sync_area(self, obj_data: Dict[str, Any]) -> bool: + """ + 检查对象是否在同步区域内 + + Args: + obj_data: 对象数据 + + Returns: + 是否在同步区域内 + """ + try: + # 这里应该根据具体的游戏逻辑实现 + # 简单实现:假设所有对象都在同步区域内 + return True + except Exception as e: + print(f"✗ 同步区域检查失败: {e}") + return True + + def _calculate_distance(self, pos1: List[float], pos2: List[float]) -> float: + """ + 计算两点间距离 + + Args: + pos1: 位置1 + pos2: 位置2 + + Returns: + 距离 + """ + try: + if len(pos1) >= 3 and len(pos2) >= 3: + dx = pos1[0] - pos2[0] + dy = pos1[1] - pos2[1] + dz = pos1[2] - pos2[2] + return (dx*dx + dy*dy + dz*dz) ** 0.5 + elif len(pos1) >= 2 and len(pos2) >= 2: + dx = pos1[0] - pos2[0] + dy = pos1[1] - pos2[1] + return (dx*dx + dy*dy) ** 0.5 + else: + return 0.0 + except Exception as e: + print(f"✗ 距离计算失败: {e}") + return 0.0 + + def _calculate_rotation_difference(self, rot1: List[float], rot2: List[float]) -> float: + """ + 计算旋转差异 + + Args: + rot1: 旋转1 + rot2: 旋转2 + + Returns: + 旋转差异 + """ + try: + # 简单实现:计算欧拉角差异 + diff = 0.0 + for i in range(min(len(rot1), len(rot2))): + diff += abs(rot1[i] - rot2[i]) + return diff + except Exception as e: + print(f"✗ 旋转差异计算失败: {e}") + return 0.0 + + def _calculate_scale_difference(self, scale1: List[float], scale2: List[float]) -> float: + """ + 计算缩放差异 + + Args: + scale1: 缩放1 + scale2: 缩放2 + + Returns: + 缩放差异 + """ + try: + # 简单实现:计算缩放差异 + diff = 0.0 + for i in range(min(len(scale1), len(scale2))): + diff += abs(scale1[i] - scale2[i]) + return diff + except Exception as e: + print(f"✗ 缩放差异计算失败: {e}") + return 0.0 + + def _update_interpolation(self, obj_id: str, interp_data: Dict[str, Any], current_time: float) -> bool: + """ + 更新插值数据 + + Args: + obj_id: 对象ID + interp_data: 插值数据 + current_time: 当前时间 + + Returns: + 是否更新成功 + """ + try: + # 简单实现:线性插值 + start_time = interp_data.get('start_time', current_time) + end_time = interp_data.get('end_time', current_time) + duration = end_time - start_time + + if duration > 0: + progress = (current_time - start_time) / duration + progress = max(0.0, min(1.0, progress)) # 限制在0-1之间 + + # 更新对象状态 + if obj_id in self.remote_objects: + obj = self.remote_objects[obj_id] + # 这里应该实际更新对象的位置、旋转等属性 + # 由于是模拟实现,只打印信息 + pass + + return True + else: + return False + + except Exception as e: + print(f"✗ 插值更新失败: {e}") + return False + + def _update_extrapolation(self, obj_id: str, extrap_data: Dict[str, Any], current_time: float) -> bool: + """ + 更新外推数据 + + Args: + obj_id: 对象ID + extrap_data: 外推数据 + current_time: 当前时间 + + Returns: + 是否更新成功 + """ + try: + # 简单实现:基于速度的外推 + last_update_time = extrap_data.get('last_update_time', current_time) + time_diff = current_time - last_update_time + + if time_diff <= self.sync_config['extrapolation_limit']: + # 在外推限制内,可以进行外推 + # 这里应该实际更新对象的状态 + # 由于是模拟实现,只打印信息 + pass + return True + else: + return False + + except Exception as e: + print(f"✗ 外推更新失败: {e}") + return False + + def register_local_object(self, obj_id: str, obj_data: Dict[str, Any]) -> bool: + """ + 注册本地对象 + + Args: + obj_id: 对象ID + obj_data: 对象数据 + + Returns: + 是否注册成功 + """ + try: + self.local_objects[obj_id] = obj_data + self.sync_objects[obj_id] = obj_data + self.sync_state['local_objects_count'] = len(self.local_objects) + self.sync_state['sync_objects_count'] = len(self.sync_objects) + + print(f"✓ 本地对象已注册: {obj_id}") + return True + + except Exception as e: + print(f"✗ 本地对象注册失败: {e}") + return False + + def unregister_local_object(self, obj_id: str) -> bool: + """ + 注销本地对象 + + Args: + obj_id: 对象ID + + Returns: + 是否注销成功 + """ + try: + if obj_id in self.local_objects: + del self.local_objects[obj_id] + + if obj_id in self.sync_objects: + del self.sync_objects[obj_id] + + self.sync_state['local_objects_count'] = len(self.local_objects) + self.sync_state['sync_objects_count'] = len(self.sync_objects) + + print(f"✓ 本地对象已注销: {obj_id}") + return True + + except Exception as e: + print(f"✗ 本地对象注销失败: {e}") + return False + + def register_remote_object(self, obj_id: str, obj_data: Dict[str, Any]) -> bool: + """ + 注册远程对象 + + Args: + obj_id: 对象ID + obj_data: 对象数据 + + Returns: + 是否注册成功 + """ + try: + self.remote_objects[obj_id] = obj_data + self.sync_objects[obj_id] = obj_data + self.sync_state['remote_objects_count'] = len(self.remote_objects) + self.sync_state['sync_objects_count'] = len(self.sync_objects) + + print(f"✓ 远程对象已注册: {obj_id}") + return True + + except Exception as e: + print(f"✗ 远程对象注册失败: {e}") + return False + + def unregister_remote_object(self, obj_id: str) -> bool: + """ + 注销远程对象 + + Args: + obj_id: 对象ID + + Returns: + 是否注销成功 + """ + try: + if obj_id in self.remote_objects: + del self.remote_objects[obj_id] + + if obj_id in self.sync_objects: + del self.sync_objects[obj_id] + + self.sync_state['remote_objects_count'] = len(self.remote_objects) + self.sync_state['sync_objects_count'] = len(self.sync_objects) + + print(f"✓ 远程对象已注销: {obj_id}") + return True + + except Exception as e: + print(f"✗ 远程对象注销失败: {e}") + return False + + def update_object_data(self, obj_id: str, obj_data: Dict[str, Any]) -> bool: + """ + 更新对象数据 + + Args: + obj_id: 对象ID + obj_data: 对象数据 + + Returns: + 是否更新成功 + """ + try: + if obj_id in self.local_objects: + self.local_objects[obj_id].update(obj_data) + + if obj_id in self.sync_objects: + self.sync_objects[obj_id].update(obj_data) + + return True + + except Exception as e: + print(f"✗ 对象数据更新失败: {e}") + return False + + def get_object_data(self, obj_id: str) -> Optional[Dict[str, Any]]: + """ + 获取对象数据 + + Args: + obj_id: 对象ID + + Returns: + 对象数据或None + """ + return self.sync_objects.get(obj_id) + + def get_sync_objects(self) -> Dict[str, Any]: + """ + 获取所有同步对象 + + Returns: + 同步对象字典 + """ + return self.sync_objects.copy() + + def get_local_objects(self) -> Dict[str, Any]: + """ + 获取所有本地对象 + + Returns: + 本地对象字典 + """ + return self.local_objects.copy() + + def get_remote_objects(self) -> Dict[str, Any]: + """ + 获取所有远程对象 + + Returns: + 远程对象字典 + """ + return self.remote_objects.copy() + + def set_sync_config(self, config: Dict[str, Any]) -> bool: + """ + 设置同步配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.sync_config.update(config) + print(f"✓ 同步配置已更新: {self.sync_config}") + return True + except Exception as e: + print(f"✗ 同步配置设置失败: {e}") + return False + + def get_sync_config(self) -> Dict[str, Any]: + """ + 获取同步配置 + + Returns: + 配置字典 + """ + return self.sync_config.copy() + + def get_sync_stats(self) -> Dict[str, Any]: + """ + 获取同步统计信息 + + Returns: + 统计信息字典 + """ + return self.sync_stats.copy() + + def reset_sync_stats(self): + """重置同步统计信息""" + try: + self.sync_stats = { + 'objects_synced': 0, + 'sync_operations': 0, + 'interpolations': 0, + 'extrapolations': 0, + 'sync_errors': 0, + 'delta_compressions': 0 + } + print("✓ 同步统计信息已重置") + except Exception as e: + print(f"✗ 同步统计信息重置失败: {e}") + + def _trigger_sync_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发同步回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.sync_callbacks: + for callback in self.sync_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 同步回调执行失败: {e}") + except Exception as e: + print(f"✗ 同步回调触发失败: {e}") + + def register_sync_callback(self, callback_type: str, callback: callable): + """ + 注册同步回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.sync_callbacks: + self.sync_callbacks[callback_type].append(callback) + print(f"✓ 同步回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 同步回调注册失败: {e}") + + def unregister_sync_callback(self, callback_type: str, callback: callable): + """ + 注销同步回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.sync_callbacks: + if callback in self.sync_callbacks[callback_type]: + self.sync_callbacks[callback_type].remove(callback) + print(f"✓ 同步回调已注销: {callback_type}") + except Exception as e: print(f"✗ 同步回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/network_sync/editor/sync_editor.py b/plugins/user/network_sync/editor/sync_editor.py index bf98977f..0df08f14 100644 --- a/plugins/user/network_sync/editor/sync_editor.py +++ b/plugins/user/network_sync/editor/sync_editor.py @@ -1,1238 +1,1238 @@ -""" -网络同步编辑器模块 -提供可视化界面用于配置和监控网络同步功能 -""" - -import time -from typing import Dict, Any, List, Optional -import threading - -class EditorMode: - """编辑器模式枚举""" - CONFIGURATION = "configuration" - MONITORING = "monitoring" - TESTING = "testing" - DEBUGGING = "debugging" - PERFORMANCE = "performance" - -class SyncEditor: - """ - 网络同步编辑器 - 提供可视化界面用于配置和监控网络同步功能 - """ - - def __init__(self, plugin): - """ - 初始化网络同步编辑器 - - Args: - plugin: 网络同步插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 编辑器配置 - self.editor_config = { - "window_width": 1024, - "window_height": 768, - "refresh_rate": 30.0, - "enable_preview": True, - "enable_realtime_update": True, - "show_debug_info": False, - "theme": "dark", - "enable_auto_refresh": True, - "auto_refresh_interval": 1.0 - } - - # 当前编辑器状态 - self.editor_state = { - "current_mode": EditorMode.CONFIGURATION, - "selected_tab": "general", - "is_testing": False, - "preview_active": False, - "monitoring_active": True, - "debug_active": False - } - - # UI组件 - self.ui_components = { - "main_window": None, - "toolbar": None, - "property_panel": None, - "preview_panel": None, - "status_bar": None, - "config_view": None, - "monitor_view": None, - "test_view": None, - "debug_view": None, - "performance_view": None - } - - # 编辑器数据 - self.editor_data = { - "config_cache": {}, - "monitor_cache": {}, - "test_cache": {}, - "debug_cache": {}, - "performance_cache": {}, - "live_data": {} - } - - # 编辑器操作历史 - self.operation_history = [] - self.history_pointer = -1 - self.max_history_size = 100 - - # 实时监控数据 - self.monitor_data = { - "network_stats": [], - "sync_stats": [], - "connection_stats": [], - "performance_metrics": [], - "error_logs": [] - } - - # 测试数据 - self.test_data = { - "test_cases": [], - "test_results": [], - "current_test": None, - "test_history": [] - } - - # 调试数据 - self.debug_data = { - "debug_messages": [], - "object_states": {}, - "network_messages": [], - "timing_info": {} - } - - # 性能数据 - self.performance_data = { - "fps_history": [], - "latency_history": [], - "bandwidth_usage": [], - "cpu_usage": [], - "memory_usage": [] - } - - # 回调函数 - self.editor_callbacks = { - "mode_changed": [], - "tab_changed": [], - "data_updated": [], - "test_started": [], - "test_completed": [], - "debug_message": [] - } - - # 统计信息 - self.editor_stats = { - "windows_opened": 0, - "operations_performed": 0, - "undo_operations": 0, - "redo_operations": 0, - "data_refreshed": 0, - "preview_updates": 0, - "tests_run": 0 - } - - # 线程管理 - self.editor_thread = None - self.monitor_thread = None - self.running = False - - # 时间戳记录 - self.last_refresh_time = 0.0 - self.last_preview_update = 0.0 - self.last_monitor_update = 0.0 - self.last_performance_update = 0.0 - - print("✓ 网络同步编辑器已创建") - - def initialize(self) -> bool: - """ - 初始化网络同步编辑器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化UI组件(模拟) - self._initialize_ui_components() - - # 加载缓存数据 - self._load_editor_cache() - - self.initialized = True - print("✓ 网络同步编辑器初始化完成") - return True - - except Exception as e: - print(f"✗ 网络同步编辑器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用网络同步编辑器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 网络同步编辑器未初始化") - return False - - self.enabled = True - self.running = True - - # 启动编辑器线程 - self.editor_thread = threading.Thread(target=self._editor_update_loop, daemon=True) - self.editor_thread.start() - - # 启动监控线程 - if self.editor_config["enable_realtime_update"]: - self.monitor_thread = threading.Thread(target=self._monitor_update_loop, daemon=True) - self.monitor_thread.start() - - print("✓ 网络同步编辑器已启用") - return True - - except Exception as e: - print(f"✗ 网络同步编辑器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用网络同步编辑器""" - try: - self.enabled = False - self.running = False - - # 等待线程结束 - if self.editor_thread and self.editor_thread.is_alive(): - self.editor_thread.join(timeout=2.0) - - if self.monitor_thread and self.monitor_thread.is_alive(): - self.monitor_thread.join(timeout=2.0) - - # 关闭UI组件 - self._close_ui_components() - - print("✓ 网络同步编辑器已禁用") - - except Exception as e: - print(f"✗ 网络同步编辑器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理网络同步编辑器资源""" - try: - self.disable() - self.ui_components.clear() - self.editor_data.clear() - self.operation_history.clear() - self.monitor_data.clear() - self.test_data.clear() - self.debug_data.clear() - self.performance_data.clear() - self.editor_callbacks.clear() - self.initialized = False - print("✓ 网络同步编辑器资源已清理") - - except Exception as e: - print(f"✗ 网络同步编辑器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新网络同步编辑器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新统计信息 - pass - - except Exception as e: - print(f"✗ 网络同步编辑器更新失败: {e}") - import traceback - traceback.print_exc() - - def _editor_update_loop(self): - """编辑器更新循环""" - try: - refresh_interval = 1.0 / self.editor_config["refresh_rate"] - - while self.running: - if self.enabled: - self._update_editor_display() - - time.sleep(refresh_interval) - - except Exception as e: - print(f"✗ 编辑器更新循环失败: {e}") - - def _monitor_update_loop(self): - """监控更新循环""" - try: - monitor_interval = 0.1 # 100ms - - while self.running: - if self.enabled and self.editor_config["enable_realtime_update"]: - self._update_monitor_data() - self._update_performance_data() - - time.sleep(monitor_interval) - - except Exception as e: - print(f"✗ 监控更新循环失败: {e}") - - def _initialize_ui_components(self): - """初始化UI组件""" - try: - # 在实际实现中,这里会创建实际的UI组件 - # 由于这是模拟实现,我们只创建占位符 - self.ui_components["main_window"] = "MainWindow" - self.ui_components["toolbar"] = "Toolbar" - self.ui_components["property_panel"] = "PropertyPanel" - self.ui_components["preview_panel"] = "PreviewPanel" - self.ui_components["status_bar"] = "StatusBar" - self.ui_components["config_view"] = "ConfigView" - self.ui_components["monitor_view"] = "MonitorView" - self.ui_components["test_view"] = "TestView" - self.ui_components["debug_view"] = "DebugView" - self.ui_components["performance_view"] = "PerformanceView" - - print("✓ UI组件初始化完成") - except Exception as e: - print(f"✗ UI组件初始化失败: {e}") - - def _close_ui_components(self): - """关闭UI组件""" - try: - # 在实际实现中,这里会关闭实际的UI组件 - self.ui_components.clear() - print("✓ UI组件已关闭") - except Exception as e: - print(f"✗ UI组件关闭失败: {e}") - - def _load_editor_cache(self): - """加载编辑器缓存""" - try: - # 模拟加载缓存数据 - if self.plugin.config_manager: - self.editor_data["config_cache"] = self.plugin.config_manager.get_config() - - print("✓ 编辑器缓存加载完成") - except Exception as e: - print(f"✗ 编辑器缓存加载失败: {e}") - - def _update_editor_display(self): - """更新编辑器显示""" - try: - current_time = time.time() - - # 检查是否需要刷新 - if current_time - self.last_refresh_time >= 1.0 / self.editor_config["refresh_rate"]: - # 更新配置视图 - self._refresh_config_view() - - # 更新属性面板 - self._refresh_property_panel() - - # 更新预览面板 - if self.editor_config["enable_preview"]: - self._update_preview_panel() - - # 更新状态栏 - self._update_status_bar() - - self.last_refresh_time = current_time - self.editor_stats["data_refreshed"] += 1 - - except Exception as e: - print(f"✗ 编辑器显示更新失败: {e}") - - def _update_monitor_data(self): - """更新监控数据""" - try: - current_time = time.time() - - # 收集网络统计 - if self.plugin.network_manager: - network_stats = self.plugin.network_manager.get_network_stats() - network_stats["timestamp"] = current_time - self.monitor_data["network_stats"].append(network_stats) - - # 保持数据大小 - if len(self.monitor_data["network_stats"]) > 100: - self.monitor_data["network_stats"] = self.monitor_data["network_stats"][-100:] - - # 收集同步统计 - if self.plugin.object_sync: - sync_stats = self.plugin.object_sync.get_sync_stats() - sync_stats["timestamp"] = current_time - self.monitor_data["sync_stats"].append(sync_stats) - - # 保持数据大小 - if len(self.monitor_data["sync_stats"]) > 100: - self.monitor_data["sync_stats"] = self.monitor_data["sync_stats"][-100:] - - # 收集连接统计 - if self.plugin.connection_manager: - connection_stats = self.plugin.connection_manager.get_connection_stats() - connection_stats["timestamp"] = current_time - self.monitor_data["connection_stats"].append(connection_stats) - - # 保持数据大小 - if len(self.monitor_data["connection_stats"]) > 100: - self.monitor_data["connection_stats"] = self.monitor_data["connection_stats"][-100:] - - # 收集性能指标 - self._collect_performance_metrics() - - self.last_monitor_update = current_time - - except Exception as e: - print(f"✗ 监控数据更新失败: {e}") - - def _update_performance_data(self): - """更新性能数据""" - try: - current_time = time.time() - - # 收集FPS历史 - if self.plugin.clock_sync: - # 模拟FPS数据 - fps = 60.0 # 假设固定FPS - self.performance_data["fps_history"].append({ - "fps": fps, - "timestamp": current_time - }) - - # 保持数据大小 - if len(self.performance_data["fps_history"]) > 100: - self.performance_data["fps_history"] = self.performance_data["fps_history"][-100:] - - # 收集延迟历史 - if self.plugin.network_manager: - connection_state = self.plugin.network_manager.get_connection_state() - latency = connection_state.get("latency", 0.0) - self.performance_data["latency_history"].append({ - "latency": latency, - "timestamp": current_time - }) - - # 保持数据大小 - if len(self.performance_data["latency_history"]) > 100: - self.performance_data["latency_history"] = self.performance_data["latency_history"][-100:] - - self.last_performance_update = current_time - - except Exception as e: - print(f"✗ 性能数据更新失败: {e}") - - def _collect_performance_metrics(self): - """收集性能指标""" - try: - metrics = {} - - # 收集各模块统计信息 - if self.plugin.sync_manager: - metrics["sync_manager"] = self.plugin.sync_manager.get_sync_stats() - - if self.plugin.network_manager: - metrics["network_manager"] = self.plugin.network_manager.get_network_stats() - - if self.plugin.protocol_handler: - metrics["protocol_handler"] = self.plugin.protocol_handler.get_protocol_stats() - - if self.plugin.object_sync: - metrics["object_sync"] = self.plugin.object_sync.get_sync_stats() - - if self.plugin.connection_manager: - metrics["connection_manager"] = self.plugin.connection_manager.get_connection_stats() - - if self.plugin.data_serializer: - metrics["data_serializer"] = self.plugin.data_serializer.get_serialization_stats() - - if self.plugin.clock_sync: - metrics["clock_sync"] = self.plugin.clock_sync.get_clock_stats() - - if self.plugin.prediction_manager: - metrics["prediction_manager"] = self.plugin.prediction_manager.get_prediction_stats() - - if self.plugin.config_manager: - metrics["config_manager"] = self.plugin.config_manager.get_config_stats() - - metrics["timestamp"] = time.time() - self.monitor_data["performance_metrics"].append(metrics) - - # 保持数据大小 - if len(self.monitor_data["performance_metrics"]) > 100: - self.monitor_data["performance_metrics"] = self.monitor_data["performance_metrics"][-100:] - - except Exception as e: - print(f"✗ 性能指标收集失败: {e}") - - def _refresh_config_view(self): - """刷新配置视图""" - try: - # 在实际实现中,这里会更新配置视图的显示内容 - pass - except Exception as e: - print(f"✗ 配置视图刷新失败: {e}") - - def _refresh_property_panel(self): - """刷新属性面板""" - try: - # 根据当前模式更新属性面板 - current_mode = self.editor_state["current_mode"] - - # 在实际实现中,这里会更新属性面板的显示内容 - pass - except Exception as e: - print(f"✗ 属性面板刷新失败: {e}") - - def _update_preview_panel(self): - """更新预览面板""" - try: - current_time = time.time() - if current_time - self.last_preview_update >= 0.1: # 100ms - # 更新预览数据 - self.editor_data["live_data"] = self._collect_live_data() - - # 在实际实现中,这里会更新预览面板的显示 - self.editor_stats["preview_updates"] += 1 - self.last_preview_update = current_time - - except Exception as e: - print(f"✗ 预览面板更新失败: {e}") - - def _collect_live_data(self) -> Dict[str, Any]: - """ - 收集实时数据 - - Returns: - 实时数据字典 - """ - try: - live_data = { - "timestamp": time.time(), - "network": {}, - "sync": {}, - "connections": {}, - "performance": {} - } - - # 收集网络数据 - if self.plugin.network_manager: - live_data["network"] = { - "bytes_sent": self.plugin.network_manager.connection_state["bytes_sent"], - "bytes_received": self.plugin.network_manager.connection_state["bytes_received"], - "packets_sent": self.plugin.network_manager.connection_state["packets_sent"], - "packets_received": self.plugin.network_manager.connection_state["packets_received"] - } - - # 收集同步数据 - if self.plugin.object_sync: - live_data["sync"] = self.plugin.object_sync.get_sync_stats() - - # 收集连接数据 - if self.plugin.connection_manager: - live_data["connections"] = self.plugin.connection_manager.get_connection_stats() - - # 收集性能数据 - live_data["performance"] = self._collect_performance_metrics() - - return live_data - - except Exception as e: - print(f"✗ 实时数据收集失败: {e}") - return {} - - def _update_status_bar(self): - """更新状态栏""" - try: - # 在实际实现中,这里会更新状态栏的显示内容 - pass - except Exception as e: - print(f"✗ 状态栏更新失败: {e}") - - def set_editor_mode(self, mode: str) -> bool: - """ - 设置编辑器模式 - - Args: - mode: 编辑器模式 - - Returns: - 是否设置成功 - """ - try: - old_mode = self.editor_state["current_mode"] - self.editor_state["current_mode"] = mode - - # 触发模式变更回调 - self._trigger_editor_callback("mode_changed", { - "old_mode": old_mode, - "new_mode": mode - }) - - print(f"✓ 编辑器模式已设置: {mode}") - return True - - except Exception as e: - print(f"✗ 编辑器模式设置失败: {e}") - return False - - def set_selected_tab(self, tab: str) -> bool: - """ - 设置选中标签页 - - Args: - tab: 标签页名称 - - Returns: - 是否设置成功 - """ - try: - old_tab = self.editor_state["selected_tab"] - self.editor_state["selected_tab"] = tab - - # 触发标签页变更回调 - self._trigger_editor_callback("tab_changed", { - "old_tab": old_tab, - "new_tab": tab - }) - - print(f"✓ 选中标签页已设置: {tab}") - return True - - except Exception as e: - print(f"✗ 选中标签页设置失败: {e}") - return False - - def start_testing(self, test_case: Dict[str, Any] = None) -> bool: - """ - 开始测试 - - Args: - test_case: 测试用例 - - Returns: - 是否开始成功 - """ - try: - self.editor_state["is_testing"] = True - - # 添加到测试历史 - if test_case: - self.test_data["current_test"] = test_case - self.test_data["test_cases"].append(test_case) - - # 触发测试开始回调 - self._trigger_editor_callback("test_started", { - "test_case": test_case, - "timestamp": time.time() - }) - - self.editor_stats["tests_run"] += 1 - print("✓ 测试已开始") - return True - - except Exception as e: - print(f"✗ 测试开始失败: {e}") - return False - - def stop_testing(self) -> bool: - """ - 停止测试 - - Returns: - 是否停止成功 - """ - try: - self.editor_state["is_testing"] = False - - # 记录测试结果 - if self.test_data["current_test"]: - test_result = { - "test_case": self.test_data["current_test"], - "end_time": time.time(), - "status": "completed" - } - self.test_data["test_results"].append(test_result) - self.test_data["current_test"] = None - - # 触发测试完成回调 - self._trigger_editor_callback("test_completed", { - "timestamp": time.time() - }) - - print("✓ 测试已停止") - return True - - except Exception as e: - print(f"✗ 测试停止失败: {e}") - return False - - def test_network_connection(self, host: str, port: int) -> bool: - """ - 测试网络连接 - - Args: - host: 主机地址 - port: 端口号 - - Returns: - 是否测试成功 - """ - try: - print(f"✓ 网络连接测试已开始: {host}:{port}") - - # 在实际实现中,这里会执行网络连接测试 - # 可能包括连接测试、延迟测试、带宽测试等 - - # 添加到测试历史 - test_entry = { - "type": "network_connection", - "host": host, - "port": port, - "start_time": time.time(), - "parameters": { - "timeout": self.plugin.config_manager.get_config("network", "connection_timeout", 30.0) - if self.plugin.config_manager else 30.0 - } - } - - self.editor_data["test_history"].append(test_entry) - - return True - - except Exception as e: - print(f"✗ 网络连接测试失败: {e}") - return False - - def test_sync_performance(self) -> bool: - """ - 测试同步性能 - - Returns: - 是否测试成功 - """ - try: - print("✓ 同步性能测试已开始") - - # 在实际实现中,这里会执行同步性能测试 - # 可能包括对象同步测试、延迟测试、带宽测试等 - - # 添加到测试历史 - test_entry = { - "type": "sync_performance", - "start_time": time.time(), - "parameters": { - "sync_rate": self.plugin.config_manager.get_config("sync", "sync_rate", 20) - if self.plugin.config_manager else 20 - } - } - - self.editor_data["test_history"].append(test_entry) - - return True - - except Exception as e: - print(f"✗ 同步性能测试失败: {e}") - return False - - def test_protocol_compatibility(self) -> bool: - """ - 测试协议兼容性 - - Returns: - 是否测试成功 - """ - try: - print("✓ 协议兼容性测试已开始") - - # 在实际实现中,这里会执行协议兼容性测试 - - # 添加到测试历史 - test_entry = { - "type": "protocol_compatibility", - "start_time": time.time(), - "parameters": { - "protocol_version": self.plugin.config_manager.get_config("network", "protocol_version", "1.0") - if self.plugin.config_manager else "1.0" - } - } - - self.editor_data["test_history"].append(test_entry) - - return True - - except Exception as e: - print(f"✗ 协议兼容性测试失败: {e}") - return False - - def toggle_preview(self, enable: bool = None) -> bool: - """ - 切换预览 - - Args: - enable: 是否启用预览(None表示切换) - - Returns: - 是否切换成功 - """ - try: - if enable is None: - enable = not self.editor_state["preview_active"] - - self.editor_state["preview_active"] = enable - self.editor_config["enable_preview"] = enable - - status = "启用" if enable else "禁用" - print(f"✓ 预览已{status}") - return True - - except Exception as e: - print(f"✗ 预览切换失败: {e}") - return False - - def toggle_monitoring(self, enable: bool = None) -> bool: - """ - 切换监控 - - Args: - enable: 是否启用监控(None表示切换) - - Returns: - 是否切换成功 - """ - try: - if enable is None: - enable = not self.editor_state["monitoring_active"] - - self.editor_state["monitoring_active"] = enable - - status = "启用" if enable else "禁用" - print(f"✓ 监控已{status}") - return True - - except Exception as e: - print(f"✗ 监控切换失败: {e}") - return False - - def toggle_debug_mode(self, enable: bool = None) -> bool: - """ - 切换调试模式 - - Args: - enable: 是否启用调试模式(None表示切换) - - Returns: - 是否切换成功 - """ - try: - if enable is None: - enable = not self.editor_state["debug_active"] - - self.editor_state["debug_active"] = enable - self.editor_config["show_debug_info"] = enable - - status = "启用" if enable else "禁用" - print(f"✓ 调试模式已{status}") - return True - - except Exception as e: - print(f"✗ 调试模式切换失败: {e}") - return False - - def undo_operation(self) -> bool: - """ - 撤销操作 - - Returns: - 是否撤销成功 - """ - try: - if self.history_pointer >= 0 and self.operation_history: - # 执行撤销操作 - operation = self.operation_history[self.history_pointer] - self.history_pointer -= 1 - self.editor_stats["undo_operations"] += 1 - - print("✓ 操作已撤销") - return True - else: - print("⚠ 没有可撤销的操作") - return False - - except Exception as e: - print(f"✗ 操作撤销失败: {e}") - return False - - def redo_operation(self) -> bool: - """ - 重做操作 - - Returns: - 是否重做成功 - """ - try: - if self.history_pointer < len(self.operation_history) - 1: - # 执行重做操作 - self.history_pointer += 1 - operation = self.operation_history[self.history_pointer] - self.editor_stats["redo_operations"] += 1 - - print("✓ 操作已重做") - return True - else: - print("⚠ 没有可重做的操作") - return False - - except Exception as e: - print(f"✗ 操作重做失败: {e}") - return False - - def add_operation_to_history(self, operation: Dict[str, Any]): - """ - 添加操作到历史 - - Args: - operation: 操作数据 - """ - try: - # 添加到历史记录 - self.operation_history.append(operation) - self.history_pointer = len(self.operation_history) - 1 - - # 保持历史记录大小 - if len(self.operation_history) > self.max_history_size: - remove_count = len(self.operation_history) - self.max_history_size - self.operation_history = self.operation_history[remove_count:] - self.history_pointer -= remove_count - self.history_pointer = max(0, self.history_pointer) - - self.editor_stats["operations_performed"] += 1 - - except Exception as e: - print(f"✗ 操作添加到历史失败: {e}") - - def _trigger_editor_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发编辑器回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.editor_callbacks: - for callback in self.editor_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 编辑器回调执行失败: {e}") - except Exception as e: - print(f"✗ 编辑器回调触发失败: {e}") - - def register_editor_callback(self, callback_type: str, callback: callable): - """ - 注册编辑器回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.editor_callbacks: - self.editor_callbacks[callback_type].append(callback) - print(f"✓ 编辑器回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 编辑器回调注册失败: {e}") - - def unregister_editor_callback(self, callback_type: str, callback: callable): - """ - 注销编辑器回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.editor_callbacks: - if callback in self.editor_callbacks[callback_type]: - self.editor_callbacks[callback_type].remove(callback) - print(f"✓ 编辑器回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 编辑器回调注销失败: {e}") - - def get_editor_state(self) -> Dict[str, Any]: - """ - 获取编辑器状态 - - Returns: - 编辑器状态字典 - """ - return self.editor_state.copy() - - def get_editor_config(self) -> Dict[str, Any]: - """ - 获取编辑器配置 - - Returns: - 编辑器配置字典 - """ - return self.editor_config.copy() - - def set_editor_config(self, config: Dict[str, Any]) -> bool: - """ - 设置编辑器配置 - - Args: - config: 编辑器配置字典 - - Returns: - 是否设置成功 - """ - try: - self.editor_config.update(config) - print(f"✓ 编辑器配置已更新: {self.editor_config}") - return True - except Exception as e: - print(f"✗ 编辑器配置设置失败: {e}") - return False - - def get_editor_data(self, data_type: str) -> Optional[Dict[str, Any]]: - """ - 获取编辑器数据 - - Args: - data_type: 数据类型 - - Returns: - 数据字典或None - """ - return self.editor_data.get(data_type) - - def get_monitor_data(self) -> Dict[str, Any]: - """ - 获取监控数据 - - Returns: - 监控数据字典 - """ - return self.monitor_data.copy() - - def get_test_data(self) -> Dict[str, Any]: - """ - 获取测试数据 - - Returns: - 测试数据字典 - """ - return self.test_data.copy() - - def get_debug_data(self) -> Dict[str, Any]: - """ - 获取调试数据 - - Returns: - 调试数据字典 - """ - return self.debug_data.copy() - - def get_performance_data(self) -> Dict[str, Any]: - """ - 获取性能数据 - - Returns: - 性能数据字典 - """ - return self.performance_data.copy() - - def get_editor_stats(self) -> Dict[str, int]: - """ - 获取编辑器统计信息 - - Returns: - 编辑器统计字典 - """ - return self.editor_stats.copy() - - def reset_editor_stats(self): - """重置编辑器统计信息""" - try: - self.editor_stats = { - "windows_opened": 0, - "operations_performed": 0, - "undo_operations": 0, - "redo_operations": 0, - "data_refreshed": 0, - "preview_updates": 0, - "tests_run": 0 - } - print("✓ 编辑器统计信息已重置") - except Exception as e: - print(f"✗ 编辑器统计信息重置失败: {e}") - - def refresh_editor_data(self): - """刷新编辑器数据""" - try: - self._load_editor_cache() - self._update_editor_display() - print("✓ 编辑器数据已刷新") - except Exception as e: - print(f"✗ 编辑器数据刷新失败: {e}") - - def show_editor_window(self) -> bool: - """ - 显示编辑器窗口 - - Returns: - 是否显示成功 - """ - try: - # 在实际实现中,这里会显示实际的编辑器窗口 - self.editor_stats["windows_opened"] += 1 - print("✓ 编辑器窗口已显示") - return True - except Exception as e: - print(f"✗ 编辑器窗口显示失败: {e}") - return False - - def hide_editor_window(self) -> bool: - """ - 隐藏编辑器窗口 - - Returns: - 是否隐藏成功 - """ - try: - # 在实际实现中,这里会隐藏实际的编辑器窗口 - print("✓ 编辑器窗口已隐藏") - return True - except Exception as e: - print(f"✗ 编辑器窗口隐藏失败: {e}") - return False - - def export_editor_data(self, file_path: str) -> bool: - """ - 导出编辑器数据 - - Args: - file_path: 导出文件路径 - - Returns: - 是否导出成功 - """ - try: - # 在实际实现中,这里会导出编辑器数据到文件 - print(f"✓ 编辑器数据已导出: {file_path}") - return True - except Exception as e: - print(f"✗ 编辑器数据导出失败: {e}") - return False - - def import_editor_data(self, file_path: str) -> bool: - """ - 导入编辑器数据 - - Args: - file_path: 导入文件路径 - - Returns: - 是否导入成功 - """ - try: - # 在实际实现中,这里会从文件导入编辑器数据 - print(f"✓ 编辑器数据已导入: {file_path}") - return True - except Exception as e: - print(f"✗ 编辑器数据导入失败: {e}") - return False - - def run_comprehensive_test(self) -> Dict[str, Any]: - """ - 运行综合测试 - - Returns: - 测试结果字典 - """ - try: - print("✓ 开始综合测试...") - - test_results = { - "timestamp": time.time(), - "tests": {}, - "summary": { - "total_tests": 0, - "passed_tests": 0, - "failed_tests": 0, - "success_rate": 0.0 - } - } - - # 测试网络连接 - test_results["tests"]["network_connection"] = { - "status": "passed" if self.test_network_connection("localhost", 7777) else "failed", - "details": "网络连接功能测试" - } - - # 测试同步性能 - test_results["tests"]["sync_performance"] = { - "status": "passed" if self.test_sync_performance() else "failed", - "details": "同步性能功能测试" - } - - # 测试协议兼容性 - test_results["tests"]["protocol_compatibility"] = { - "status": "passed" if self.test_protocol_compatibility() else "failed", - "details": "协议兼容性功能测试" - } - - # 统计结果 - total_tests = len(test_results["tests"]) - passed_tests = sum(1 for test in test_results["tests"].values() if test["status"] == "passed") - - test_results["summary"]["total_tests"] = total_tests - test_results["summary"]["passed_tests"] = passed_tests - test_results["summary"]["failed_tests"] = total_tests - passed_tests - test_results["summary"]["success_rate"] = passed_tests / total_tests if total_tests > 0 else 0.0 - - print(f"✓ 综合测试完成: {passed_tests}/{total_tests} 通过") - return test_results - - except Exception as e: - print(f"✗ 综合测试失败: {e}") - import traceback - traceback.print_exc() - return { - "timestamp": time.time(), - "tests": {}, - "summary": { - "total_tests": 0, - "passed_tests": 0, - "failed_tests": 0, - "success_rate": 0.0, - "error": str(e) - } +""" +网络同步编辑器模块 +提供可视化界面用于配置和监控网络同步功能 +""" + +import time +from typing import Dict, Any, List, Optional +import threading + +class EditorMode: + """编辑器模式枚举""" + CONFIGURATION = "configuration" + MONITORING = "monitoring" + TESTING = "testing" + DEBUGGING = "debugging" + PERFORMANCE = "performance" + +class SyncEditor: + """ + 网络同步编辑器 + 提供可视化界面用于配置和监控网络同步功能 + """ + + def __init__(self, plugin): + """ + 初始化网络同步编辑器 + + Args: + plugin: 网络同步插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 编辑器配置 + self.editor_config = { + "window_width": 1024, + "window_height": 768, + "refresh_rate": 30.0, + "enable_preview": True, + "enable_realtime_update": True, + "show_debug_info": False, + "theme": "dark", + "enable_auto_refresh": True, + "auto_refresh_interval": 1.0 + } + + # 当前编辑器状态 + self.editor_state = { + "current_mode": EditorMode.CONFIGURATION, + "selected_tab": "general", + "is_testing": False, + "preview_active": False, + "monitoring_active": True, + "debug_active": False + } + + # UI组件 + self.ui_components = { + "main_window": None, + "toolbar": None, + "property_panel": None, + "preview_panel": None, + "status_bar": None, + "config_view": None, + "monitor_view": None, + "test_view": None, + "debug_view": None, + "performance_view": None + } + + # 编辑器数据 + self.editor_data = { + "config_cache": {}, + "monitor_cache": {}, + "test_cache": {}, + "debug_cache": {}, + "performance_cache": {}, + "live_data": {} + } + + # 编辑器操作历史 + self.operation_history = [] + self.history_pointer = -1 + self.max_history_size = 100 + + # 实时监控数据 + self.monitor_data = { + "network_stats": [], + "sync_stats": [], + "connection_stats": [], + "performance_metrics": [], + "error_logs": [] + } + + # 测试数据 + self.test_data = { + "test_cases": [], + "test_results": [], + "current_test": None, + "test_history": [] + } + + # 调试数据 + self.debug_data = { + "debug_messages": [], + "object_states": {}, + "network_messages": [], + "timing_info": {} + } + + # 性能数据 + self.performance_data = { + "fps_history": [], + "latency_history": [], + "bandwidth_usage": [], + "cpu_usage": [], + "memory_usage": [] + } + + # 回调函数 + self.editor_callbacks = { + "mode_changed": [], + "tab_changed": [], + "data_updated": [], + "test_started": [], + "test_completed": [], + "debug_message": [] + } + + # 统计信息 + self.editor_stats = { + "windows_opened": 0, + "operations_performed": 0, + "undo_operations": 0, + "redo_operations": 0, + "data_refreshed": 0, + "preview_updates": 0, + "tests_run": 0 + } + + # 线程管理 + self.editor_thread = None + self.monitor_thread = None + self.running = False + + # 时间戳记录 + self.last_refresh_time = 0.0 + self.last_preview_update = 0.0 + self.last_monitor_update = 0.0 + self.last_performance_update = 0.0 + + print("✓ 网络同步编辑器已创建") + + def initialize(self) -> bool: + """ + 初始化网络同步编辑器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化UI组件(模拟) + self._initialize_ui_components() + + # 加载缓存数据 + self._load_editor_cache() + + self.initialized = True + print("✓ 网络同步编辑器初始化完成") + return True + + except Exception as e: + print(f"✗ 网络同步编辑器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用网络同步编辑器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 网络同步编辑器未初始化") + return False + + self.enabled = True + self.running = True + + # 启动编辑器线程 + self.editor_thread = threading.Thread(target=self._editor_update_loop, daemon=True) + self.editor_thread.start() + + # 启动监控线程 + if self.editor_config["enable_realtime_update"]: + self.monitor_thread = threading.Thread(target=self._monitor_update_loop, daemon=True) + self.monitor_thread.start() + + print("✓ 网络同步编辑器已启用") + return True + + except Exception as e: + print(f"✗ 网络同步编辑器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用网络同步编辑器""" + try: + self.enabled = False + self.running = False + + # 等待线程结束 + if self.editor_thread and self.editor_thread.is_alive(): + self.editor_thread.join(timeout=2.0) + + if self.monitor_thread and self.monitor_thread.is_alive(): + self.monitor_thread.join(timeout=2.0) + + # 关闭UI组件 + self._close_ui_components() + + print("✓ 网络同步编辑器已禁用") + + except Exception as e: + print(f"✗ 网络同步编辑器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理网络同步编辑器资源""" + try: + self.disable() + self.ui_components.clear() + self.editor_data.clear() + self.operation_history.clear() + self.monitor_data.clear() + self.test_data.clear() + self.debug_data.clear() + self.performance_data.clear() + self.editor_callbacks.clear() + self.initialized = False + print("✓ 网络同步编辑器资源已清理") + + except Exception as e: + print(f"✗ 网络同步编辑器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新网络同步编辑器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新统计信息 + pass + + except Exception as e: + print(f"✗ 网络同步编辑器更新失败: {e}") + import traceback + traceback.print_exc() + + def _editor_update_loop(self): + """编辑器更新循环""" + try: + refresh_interval = 1.0 / self.editor_config["refresh_rate"] + + while self.running: + if self.enabled: + self._update_editor_display() + + time.sleep(refresh_interval) + + except Exception as e: + print(f"✗ 编辑器更新循环失败: {e}") + + def _monitor_update_loop(self): + """监控更新循环""" + try: + monitor_interval = 0.1 # 100ms + + while self.running: + if self.enabled and self.editor_config["enable_realtime_update"]: + self._update_monitor_data() + self._update_performance_data() + + time.sleep(monitor_interval) + + except Exception as e: + print(f"✗ 监控更新循环失败: {e}") + + def _initialize_ui_components(self): + """初始化UI组件""" + try: + # 在实际实现中,这里会创建实际的UI组件 + # 由于这是模拟实现,我们只创建占位符 + self.ui_components["main_window"] = "MainWindow" + self.ui_components["toolbar"] = "Toolbar" + self.ui_components["property_panel"] = "PropertyPanel" + self.ui_components["preview_panel"] = "PreviewPanel" + self.ui_components["status_bar"] = "StatusBar" + self.ui_components["config_view"] = "ConfigView" + self.ui_components["monitor_view"] = "MonitorView" + self.ui_components["test_view"] = "TestView" + self.ui_components["debug_view"] = "DebugView" + self.ui_components["performance_view"] = "PerformanceView" + + print("✓ UI组件初始化完成") + except Exception as e: + print(f"✗ UI组件初始化失败: {e}") + + def _close_ui_components(self): + """关闭UI组件""" + try: + # 在实际实现中,这里会关闭实际的UI组件 + self.ui_components.clear() + print("✓ UI组件已关闭") + except Exception as e: + print(f"✗ UI组件关闭失败: {e}") + + def _load_editor_cache(self): + """加载编辑器缓存""" + try: + # 模拟加载缓存数据 + if self.plugin.config_manager: + self.editor_data["config_cache"] = self.plugin.config_manager.get_config() + + print("✓ 编辑器缓存加载完成") + except Exception as e: + print(f"✗ 编辑器缓存加载失败: {e}") + + def _update_editor_display(self): + """更新编辑器显示""" + try: + current_time = time.time() + + # 检查是否需要刷新 + if current_time - self.last_refresh_time >= 1.0 / self.editor_config["refresh_rate"]: + # 更新配置视图 + self._refresh_config_view() + + # 更新属性面板 + self._refresh_property_panel() + + # 更新预览面板 + if self.editor_config["enable_preview"]: + self._update_preview_panel() + + # 更新状态栏 + self._update_status_bar() + + self.last_refresh_time = current_time + self.editor_stats["data_refreshed"] += 1 + + except Exception as e: + print(f"✗ 编辑器显示更新失败: {e}") + + def _update_monitor_data(self): + """更新监控数据""" + try: + current_time = time.time() + + # 收集网络统计 + if self.plugin.network_manager: + network_stats = self.plugin.network_manager.get_network_stats() + network_stats["timestamp"] = current_time + self.monitor_data["network_stats"].append(network_stats) + + # 保持数据大小 + if len(self.monitor_data["network_stats"]) > 100: + self.monitor_data["network_stats"] = self.monitor_data["network_stats"][-100:] + + # 收集同步统计 + if self.plugin.object_sync: + sync_stats = self.plugin.object_sync.get_sync_stats() + sync_stats["timestamp"] = current_time + self.monitor_data["sync_stats"].append(sync_stats) + + # 保持数据大小 + if len(self.monitor_data["sync_stats"]) > 100: + self.monitor_data["sync_stats"] = self.monitor_data["sync_stats"][-100:] + + # 收集连接统计 + if self.plugin.connection_manager: + connection_stats = self.plugin.connection_manager.get_connection_stats() + connection_stats["timestamp"] = current_time + self.monitor_data["connection_stats"].append(connection_stats) + + # 保持数据大小 + if len(self.monitor_data["connection_stats"]) > 100: + self.monitor_data["connection_stats"] = self.monitor_data["connection_stats"][-100:] + + # 收集性能指标 + self._collect_performance_metrics() + + self.last_monitor_update = current_time + + except Exception as e: + print(f"✗ 监控数据更新失败: {e}") + + def _update_performance_data(self): + """更新性能数据""" + try: + current_time = time.time() + + # 收集FPS历史 + if self.plugin.clock_sync: + # 模拟FPS数据 + fps = 60.0 # 假设固定FPS + self.performance_data["fps_history"].append({ + "fps": fps, + "timestamp": current_time + }) + + # 保持数据大小 + if len(self.performance_data["fps_history"]) > 100: + self.performance_data["fps_history"] = self.performance_data["fps_history"][-100:] + + # 收集延迟历史 + if self.plugin.network_manager: + connection_state = self.plugin.network_manager.get_connection_state() + latency = connection_state.get("latency", 0.0) + self.performance_data["latency_history"].append({ + "latency": latency, + "timestamp": current_time + }) + + # 保持数据大小 + if len(self.performance_data["latency_history"]) > 100: + self.performance_data["latency_history"] = self.performance_data["latency_history"][-100:] + + self.last_performance_update = current_time + + except Exception as e: + print(f"✗ 性能数据更新失败: {e}") + + def _collect_performance_metrics(self): + """收集性能指标""" + try: + metrics = {} + + # 收集各模块统计信息 + if self.plugin.sync_manager: + metrics["sync_manager"] = self.plugin.sync_manager.get_sync_stats() + + if self.plugin.network_manager: + metrics["network_manager"] = self.plugin.network_manager.get_network_stats() + + if self.plugin.protocol_handler: + metrics["protocol_handler"] = self.plugin.protocol_handler.get_protocol_stats() + + if self.plugin.object_sync: + metrics["object_sync"] = self.plugin.object_sync.get_sync_stats() + + if self.plugin.connection_manager: + metrics["connection_manager"] = self.plugin.connection_manager.get_connection_stats() + + if self.plugin.data_serializer: + metrics["data_serializer"] = self.plugin.data_serializer.get_serialization_stats() + + if self.plugin.clock_sync: + metrics["clock_sync"] = self.plugin.clock_sync.get_clock_stats() + + if self.plugin.prediction_manager: + metrics["prediction_manager"] = self.plugin.prediction_manager.get_prediction_stats() + + if self.plugin.config_manager: + metrics["config_manager"] = self.plugin.config_manager.get_config_stats() + + metrics["timestamp"] = time.time() + self.monitor_data["performance_metrics"].append(metrics) + + # 保持数据大小 + if len(self.monitor_data["performance_metrics"]) > 100: + self.monitor_data["performance_metrics"] = self.monitor_data["performance_metrics"][-100:] + + except Exception as e: + print(f"✗ 性能指标收集失败: {e}") + + def _refresh_config_view(self): + """刷新配置视图""" + try: + # 在实际实现中,这里会更新配置视图的显示内容 + pass + except Exception as e: + print(f"✗ 配置视图刷新失败: {e}") + + def _refresh_property_panel(self): + """刷新属性面板""" + try: + # 根据当前模式更新属性面板 + current_mode = self.editor_state["current_mode"] + + # 在实际实现中,这里会更新属性面板的显示内容 + pass + except Exception as e: + print(f"✗ 属性面板刷新失败: {e}") + + def _update_preview_panel(self): + """更新预览面板""" + try: + current_time = time.time() + if current_time - self.last_preview_update >= 0.1: # 100ms + # 更新预览数据 + self.editor_data["live_data"] = self._collect_live_data() + + # 在实际实现中,这里会更新预览面板的显示 + self.editor_stats["preview_updates"] += 1 + self.last_preview_update = current_time + + except Exception as e: + print(f"✗ 预览面板更新失败: {e}") + + def _collect_live_data(self) -> Dict[str, Any]: + """ + 收集实时数据 + + Returns: + 实时数据字典 + """ + try: + live_data = { + "timestamp": time.time(), + "network": {}, + "sync": {}, + "connections": {}, + "performance": {} + } + + # 收集网络数据 + if self.plugin.network_manager: + live_data["network"] = { + "bytes_sent": self.plugin.network_manager.connection_state["bytes_sent"], + "bytes_received": self.plugin.network_manager.connection_state["bytes_received"], + "packets_sent": self.plugin.network_manager.connection_state["packets_sent"], + "packets_received": self.plugin.network_manager.connection_state["packets_received"] + } + + # 收集同步数据 + if self.plugin.object_sync: + live_data["sync"] = self.plugin.object_sync.get_sync_stats() + + # 收集连接数据 + if self.plugin.connection_manager: + live_data["connections"] = self.plugin.connection_manager.get_connection_stats() + + # 收集性能数据 + live_data["performance"] = self._collect_performance_metrics() + + return live_data + + except Exception as e: + print(f"✗ 实时数据收集失败: {e}") + return {} + + def _update_status_bar(self): + """更新状态栏""" + try: + # 在实际实现中,这里会更新状态栏的显示内容 + pass + except Exception as e: + print(f"✗ 状态栏更新失败: {e}") + + def set_editor_mode(self, mode: str) -> bool: + """ + 设置编辑器模式 + + Args: + mode: 编辑器模式 + + Returns: + 是否设置成功 + """ + try: + old_mode = self.editor_state["current_mode"] + self.editor_state["current_mode"] = mode + + # 触发模式变更回调 + self._trigger_editor_callback("mode_changed", { + "old_mode": old_mode, + "new_mode": mode + }) + + print(f"✓ 编辑器模式已设置: {mode}") + return True + + except Exception as e: + print(f"✗ 编辑器模式设置失败: {e}") + return False + + def set_selected_tab(self, tab: str) -> bool: + """ + 设置选中标签页 + + Args: + tab: 标签页名称 + + Returns: + 是否设置成功 + """ + try: + old_tab = self.editor_state["selected_tab"] + self.editor_state["selected_tab"] = tab + + # 触发标签页变更回调 + self._trigger_editor_callback("tab_changed", { + "old_tab": old_tab, + "new_tab": tab + }) + + print(f"✓ 选中标签页已设置: {tab}") + return True + + except Exception as e: + print(f"✗ 选中标签页设置失败: {e}") + return False + + def start_testing(self, test_case: Dict[str, Any] = None) -> bool: + """ + 开始测试 + + Args: + test_case: 测试用例 + + Returns: + 是否开始成功 + """ + try: + self.editor_state["is_testing"] = True + + # 添加到测试历史 + if test_case: + self.test_data["current_test"] = test_case + self.test_data["test_cases"].append(test_case) + + # 触发测试开始回调 + self._trigger_editor_callback("test_started", { + "test_case": test_case, + "timestamp": time.time() + }) + + self.editor_stats["tests_run"] += 1 + print("✓ 测试已开始") + return True + + except Exception as e: + print(f"✗ 测试开始失败: {e}") + return False + + def stop_testing(self) -> bool: + """ + 停止测试 + + Returns: + 是否停止成功 + """ + try: + self.editor_state["is_testing"] = False + + # 记录测试结果 + if self.test_data["current_test"]: + test_result = { + "test_case": self.test_data["current_test"], + "end_time": time.time(), + "status": "completed" + } + self.test_data["test_results"].append(test_result) + self.test_data["current_test"] = None + + # 触发测试完成回调 + self._trigger_editor_callback("test_completed", { + "timestamp": time.time() + }) + + print("✓ 测试已停止") + return True + + except Exception as e: + print(f"✗ 测试停止失败: {e}") + return False + + def test_network_connection(self, host: str, port: int) -> bool: + """ + 测试网络连接 + + Args: + host: 主机地址 + port: 端口号 + + Returns: + 是否测试成功 + """ + try: + print(f"✓ 网络连接测试已开始: {host}:{port}") + + # 在实际实现中,这里会执行网络连接测试 + # 可能包括连接测试、延迟测试、带宽测试等 + + # 添加到测试历史 + test_entry = { + "type": "network_connection", + "host": host, + "port": port, + "start_time": time.time(), + "parameters": { + "timeout": self.plugin.config_manager.get_config("network", "connection_timeout", 30.0) + if self.plugin.config_manager else 30.0 + } + } + + self.editor_data["test_history"].append(test_entry) + + return True + + except Exception as e: + print(f"✗ 网络连接测试失败: {e}") + return False + + def test_sync_performance(self) -> bool: + """ + 测试同步性能 + + Returns: + 是否测试成功 + """ + try: + print("✓ 同步性能测试已开始") + + # 在实际实现中,这里会执行同步性能测试 + # 可能包括对象同步测试、延迟测试、带宽测试等 + + # 添加到测试历史 + test_entry = { + "type": "sync_performance", + "start_time": time.time(), + "parameters": { + "sync_rate": self.plugin.config_manager.get_config("sync", "sync_rate", 20) + if self.plugin.config_manager else 20 + } + } + + self.editor_data["test_history"].append(test_entry) + + return True + + except Exception as e: + print(f"✗ 同步性能测试失败: {e}") + return False + + def test_protocol_compatibility(self) -> bool: + """ + 测试协议兼容性 + + Returns: + 是否测试成功 + """ + try: + print("✓ 协议兼容性测试已开始") + + # 在实际实现中,这里会执行协议兼容性测试 + + # 添加到测试历史 + test_entry = { + "type": "protocol_compatibility", + "start_time": time.time(), + "parameters": { + "protocol_version": self.plugin.config_manager.get_config("network", "protocol_version", "1.0") + if self.plugin.config_manager else "1.0" + } + } + + self.editor_data["test_history"].append(test_entry) + + return True + + except Exception as e: + print(f"✗ 协议兼容性测试失败: {e}") + return False + + def toggle_preview(self, enable: bool = None) -> bool: + """ + 切换预览 + + Args: + enable: 是否启用预览(None表示切换) + + Returns: + 是否切换成功 + """ + try: + if enable is None: + enable = not self.editor_state["preview_active"] + + self.editor_state["preview_active"] = enable + self.editor_config["enable_preview"] = enable + + status = "启用" if enable else "禁用" + print(f"✓ 预览已{status}") + return True + + except Exception as e: + print(f"✗ 预览切换失败: {e}") + return False + + def toggle_monitoring(self, enable: bool = None) -> bool: + """ + 切换监控 + + Args: + enable: 是否启用监控(None表示切换) + + Returns: + 是否切换成功 + """ + try: + if enable is None: + enable = not self.editor_state["monitoring_active"] + + self.editor_state["monitoring_active"] = enable + + status = "启用" if enable else "禁用" + print(f"✓ 监控已{status}") + return True + + except Exception as e: + print(f"✗ 监控切换失败: {e}") + return False + + def toggle_debug_mode(self, enable: bool = None) -> bool: + """ + 切换调试模式 + + Args: + enable: 是否启用调试模式(None表示切换) + + Returns: + 是否切换成功 + """ + try: + if enable is None: + enable = not self.editor_state["debug_active"] + + self.editor_state["debug_active"] = enable + self.editor_config["show_debug_info"] = enable + + status = "启用" if enable else "禁用" + print(f"✓ 调试模式已{status}") + return True + + except Exception as e: + print(f"✗ 调试模式切换失败: {e}") + return False + + def undo_operation(self) -> bool: + """ + 撤销操作 + + Returns: + 是否撤销成功 + """ + try: + if self.history_pointer >= 0 and self.operation_history: + # 执行撤销操作 + operation = self.operation_history[self.history_pointer] + self.history_pointer -= 1 + self.editor_stats["undo_operations"] += 1 + + print("✓ 操作已撤销") + return True + else: + print("⚠ 没有可撤销的操作") + return False + + except Exception as e: + print(f"✗ 操作撤销失败: {e}") + return False + + def redo_operation(self) -> bool: + """ + 重做操作 + + Returns: + 是否重做成功 + """ + try: + if self.history_pointer < len(self.operation_history) - 1: + # 执行重做操作 + self.history_pointer += 1 + operation = self.operation_history[self.history_pointer] + self.editor_stats["redo_operations"] += 1 + + print("✓ 操作已重做") + return True + else: + print("⚠ 没有可重做的操作") + return False + + except Exception as e: + print(f"✗ 操作重做失败: {e}") + return False + + def add_operation_to_history(self, operation: Dict[str, Any]): + """ + 添加操作到历史 + + Args: + operation: 操作数据 + """ + try: + # 添加到历史记录 + self.operation_history.append(operation) + self.history_pointer = len(self.operation_history) - 1 + + # 保持历史记录大小 + if len(self.operation_history) > self.max_history_size: + remove_count = len(self.operation_history) - self.max_history_size + self.operation_history = self.operation_history[remove_count:] + self.history_pointer -= remove_count + self.history_pointer = max(0, self.history_pointer) + + self.editor_stats["operations_performed"] += 1 + + except Exception as e: + print(f"✗ 操作添加到历史失败: {e}") + + def _trigger_editor_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发编辑器回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.editor_callbacks: + for callback in self.editor_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 编辑器回调执行失败: {e}") + except Exception as e: + print(f"✗ 编辑器回调触发失败: {e}") + + def register_editor_callback(self, callback_type: str, callback: callable): + """ + 注册编辑器回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.editor_callbacks: + self.editor_callbacks[callback_type].append(callback) + print(f"✓ 编辑器回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 编辑器回调注册失败: {e}") + + def unregister_editor_callback(self, callback_type: str, callback: callable): + """ + 注销编辑器回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.editor_callbacks: + if callback in self.editor_callbacks[callback_type]: + self.editor_callbacks[callback_type].remove(callback) + print(f"✓ 编辑器回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 编辑器回调注销失败: {e}") + + def get_editor_state(self) -> Dict[str, Any]: + """ + 获取编辑器状态 + + Returns: + 编辑器状态字典 + """ + return self.editor_state.copy() + + def get_editor_config(self) -> Dict[str, Any]: + """ + 获取编辑器配置 + + Returns: + 编辑器配置字典 + """ + return self.editor_config.copy() + + def set_editor_config(self, config: Dict[str, Any]) -> bool: + """ + 设置编辑器配置 + + Args: + config: 编辑器配置字典 + + Returns: + 是否设置成功 + """ + try: + self.editor_config.update(config) + print(f"✓ 编辑器配置已更新: {self.editor_config}") + return True + except Exception as e: + print(f"✗ 编辑器配置设置失败: {e}") + return False + + def get_editor_data(self, data_type: str) -> Optional[Dict[str, Any]]: + """ + 获取编辑器数据 + + Args: + data_type: 数据类型 + + Returns: + 数据字典或None + """ + return self.editor_data.get(data_type) + + def get_monitor_data(self) -> Dict[str, Any]: + """ + 获取监控数据 + + Returns: + 监控数据字典 + """ + return self.monitor_data.copy() + + def get_test_data(self) -> Dict[str, Any]: + """ + 获取测试数据 + + Returns: + 测试数据字典 + """ + return self.test_data.copy() + + def get_debug_data(self) -> Dict[str, Any]: + """ + 获取调试数据 + + Returns: + 调试数据字典 + """ + return self.debug_data.copy() + + def get_performance_data(self) -> Dict[str, Any]: + """ + 获取性能数据 + + Returns: + 性能数据字典 + """ + return self.performance_data.copy() + + def get_editor_stats(self) -> Dict[str, int]: + """ + 获取编辑器统计信息 + + Returns: + 编辑器统计字典 + """ + return self.editor_stats.copy() + + def reset_editor_stats(self): + """重置编辑器统计信息""" + try: + self.editor_stats = { + "windows_opened": 0, + "operations_performed": 0, + "undo_operations": 0, + "redo_operations": 0, + "data_refreshed": 0, + "preview_updates": 0, + "tests_run": 0 + } + print("✓ 编辑器统计信息已重置") + except Exception as e: + print(f"✗ 编辑器统计信息重置失败: {e}") + + def refresh_editor_data(self): + """刷新编辑器数据""" + try: + self._load_editor_cache() + self._update_editor_display() + print("✓ 编辑器数据已刷新") + except Exception as e: + print(f"✗ 编辑器数据刷新失败: {e}") + + def show_editor_window(self) -> bool: + """ + 显示编辑器窗口 + + Returns: + 是否显示成功 + """ + try: + # 在实际实现中,这里会显示实际的编辑器窗口 + self.editor_stats["windows_opened"] += 1 + print("✓ 编辑器窗口已显示") + return True + except Exception as e: + print(f"✗ 编辑器窗口显示失败: {e}") + return False + + def hide_editor_window(self) -> bool: + """ + 隐藏编辑器窗口 + + Returns: + 是否隐藏成功 + """ + try: + # 在实际实现中,这里会隐藏实际的编辑器窗口 + print("✓ 编辑器窗口已隐藏") + return True + except Exception as e: + print(f"✗ 编辑器窗口隐藏失败: {e}") + return False + + def export_editor_data(self, file_path: str) -> bool: + """ + 导出编辑器数据 + + Args: + file_path: 导出文件路径 + + Returns: + 是否导出成功 + """ + try: + # 在实际实现中,这里会导出编辑器数据到文件 + print(f"✓ 编辑器数据已导出: {file_path}") + return True + except Exception as e: + print(f"✗ 编辑器数据导出失败: {e}") + return False + + def import_editor_data(self, file_path: str) -> bool: + """ + 导入编辑器数据 + + Args: + file_path: 导入文件路径 + + Returns: + 是否导入成功 + """ + try: + # 在实际实现中,这里会从文件导入编辑器数据 + print(f"✓ 编辑器数据已导入: {file_path}") + return True + except Exception as e: + print(f"✗ 编辑器数据导入失败: {e}") + return False + + def run_comprehensive_test(self) -> Dict[str, Any]: + """ + 运行综合测试 + + Returns: + 测试结果字典 + """ + try: + print("✓ 开始综合测试...") + + test_results = { + "timestamp": time.time(), + "tests": {}, + "summary": { + "total_tests": 0, + "passed_tests": 0, + "failed_tests": 0, + "success_rate": 0.0 + } + } + + # 测试网络连接 + test_results["tests"]["network_connection"] = { + "status": "passed" if self.test_network_connection("localhost", 7777) else "failed", + "details": "网络连接功能测试" + } + + # 测试同步性能 + test_results["tests"]["sync_performance"] = { + "status": "passed" if self.test_sync_performance() else "failed", + "details": "同步性能功能测试" + } + + # 测试协议兼容性 + test_results["tests"]["protocol_compatibility"] = { + "status": "passed" if self.test_protocol_compatibility() else "failed", + "details": "协议兼容性功能测试" + } + + # 统计结果 + total_tests = len(test_results["tests"]) + passed_tests = sum(1 for test in test_results["tests"].values() if test["status"] == "passed") + + test_results["summary"]["total_tests"] = total_tests + test_results["summary"]["passed_tests"] = passed_tests + test_results["summary"]["failed_tests"] = total_tests - passed_tests + test_results["summary"]["success_rate"] = passed_tests / total_tests if total_tests > 0 else 0.0 + + print(f"✓ 综合测试完成: {passed_tests}/{total_tests} 通过") + return test_results + + except Exception as e: + print(f"✗ 综合测试失败: {e}") + import traceback + traceback.print_exc() + return { + "timestamp": time.time(), + "tests": {}, + "summary": { + "total_tests": 0, + "passed_tests": 0, + "failed_tests": 0, + "success_rate": 0.0, + "error": str(e) + } } \ No newline at end of file diff --git a/plugins/user/network_sync/encryption/encryption_manager.py b/plugins/user/network_sync/encryption/encryption_manager.py index 776ee939..dbc6d8d7 100644 --- a/plugins/user/network_sync/encryption/encryption_manager.py +++ b/plugins/user/network_sync/encryption/encryption_manager.py @@ -1,648 +1,648 @@ -""" -加密管理模块 -负责处理网络数据的加密和解密 -""" - -import time -from typing import Dict, Any, Optional -import hashlib -import hmac -import secrets -from cryptography.fernet import Fernet -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC -import base64 - -class EncryptionManager: - """ - 加密管理器 - 负责处理网络数据的加密和解密 - """ - - def __init__(self, plugin): - """ - 初始化加密管理器 - - Args: - plugin: 网络同步插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 加密配置 - self.encryption_config = { - "enable_encryption": False, - "encryption_algorithm": "AES", # Fernet使用AES 128加密 - "key_derivation_function": "PBKDF2", - "salt_length": 16, - "iterations": 100000, - "enable_message_authentication": True, - "enable_session_keys": True, - "session_key_lifetime": 3600, # 1小时 - "enable_key_rotation": True, - "key_rotation_interval": 86400 # 24小时 - } - - # 加密密钥 - self.master_key = None - self.session_key = None - self.session_key_timestamp = 0.0 - self.previous_key = None - - # 加密器实例 - self.cipher = None - self.previous_cipher = None - - # 加密统计 - self.encryption_stats = { - "messages_encrypted": 0, - "messages_decrypted": 0, - "encryption_errors": 0, - "decryption_errors": 0, - "key_rotations": 0, - "auth_failures": 0, - "bytes_encrypted": 0, - "bytes_decrypted": 0 - } - - # 回调函数 - self.encryption_callbacks = { - "encryption_enabled": [], - "key_rotated": [], - "auth_failed": [], - "encryption_error": [] - } - - # 时间戳记录 - self.last_key_rotation = 0.0 - self.last_encryption_operation = 0.0 - - print("✓ 加密管理器已创建") - - def initialize(self) -> bool: - """ - 初始化加密管理器 - - Returns: - 是否初始化成功 - """ - try: - # 生成主密钥 - self._generate_master_key() - - self.initialized = True - print("✓ 加密管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 加密管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用加密管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 加密管理器未初始化") - return False - - self.enabled = True - - # 生成会话密钥 - if self.encryption_config["enable_session_keys"]: - self._generate_session_key() - - # 触发加密启用回调 - self._trigger_encryption_callback("encryption_enabled", { - "timestamp": time.time() - }) - - print("✓ 加密管理器已启用") - return True - - except Exception as e: - print(f"✗ 加密管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用加密管理器""" - try: - self.enabled = False - self.cipher = None - self.session_key = None - print("✓ 加密管理器已禁用") - - except Exception as e: - print(f"✗ 加密管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理加密管理器资源""" - try: - self.disable() - self.encryption_callbacks.clear() - self.master_key = None - self.previous_key = None - self.initialized = False - print("✓ 加密管理器资源已清理") - - except Exception as e: - print(f"✗ 加密管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新加密管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.last_encryption_operation = current_time - - # 检查密钥轮换 - if (self.encryption_config["enable_key_rotation"] and - current_time - self.last_key_rotation > self.encryption_config["key_rotation_interval"]): - self._rotate_keys() - - # 检查会话密钥过期 - if (self.encryption_config["enable_session_keys"] and - current_time - self.session_key_timestamp > self.encryption_config["session_key_lifetime"]): - self._generate_session_key() - - except Exception as e: - print(f"✗ 加密管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _generate_master_key(self): - """生成主密钥""" - try: - # 生成随机主密钥 - self.master_key = Fernet.generate_key() - print("✓ 主密钥已生成") - except Exception as e: - print(f"✗ 主密钥生成失败: {e}") - - def _generate_session_key(self): - """生成会话密钥""" - try: - if not self.master_key: - self._generate_master_key() - - # 生成新的会话密钥 - self.session_key = Fernet.generate_key() - self.session_key_timestamp = time.time() - self.cipher = Fernet(self.session_key) - - print("✓ 会话密钥已生成") - except Exception as e: - print(f"✗ 会话密钥生成失败: {e}") - - def _rotate_keys(self): - """轮换密钥""" - try: - # 保存当前密钥作为前一个密钥 - self.previous_key = self.session_key - self.previous_cipher = self.cipher - - # 生成新密钥 - self._generate_session_key() - - self.last_key_rotation = time.time() - self.encryption_stats["key_rotations"] += 1 - - # 触发密钥轮换回调 - self._trigger_encryption_callback("key_rotated", { - "timestamp": self.last_key_rotation - }) - - print("✓ 密钥已轮换") - except Exception as e: - print(f"✗ 密钥轮换失败: {e}") - - def encrypt_data(self, data: bytes, add_auth: bool = None) -> Optional[bytes]: - """ - 加密数据 - - Args: - data: 要加密的数据 - add_auth: 是否添加认证(None表示使用配置) - - Returns: - 加密后的数据或None - """ - try: - if not self.enabled: - print("✗ 加密管理器未启用") - return data # 不加密直接返回 - - if add_auth is None: - add_auth = self.encryption_config["enable_message_authentication"] - - self.encryption_stats["messages_encrypted"] += 1 - self.encryption_stats["bytes_encrypted"] += len(data) - - # 使用Fernet加密 - if self.cipher: - encrypted_data = self.cipher.encrypt(data) - else: - # 如果没有会话密钥,使用主密钥 - cipher = Fernet(self.master_key) - encrypted_data = cipher.encrypt(data) - - # 添加认证信息 - if add_auth: - # 使用HMAC添加消息认证码 - salt = secrets.token_bytes(self.encryption_config["salt_length"]) - mac = hmac.new(self.session_key or self.master_key, - encrypted_data + salt, - hashlib.sha256).digest() - encrypted_data = salt + mac + encrypted_data - - return encrypted_data - - except Exception as e: - print(f"✗ 数据加密失败: {e}") - self.encryption_stats["encryption_errors"] += 1 - - # 触发加密错误回调 - self._trigger_encryption_callback("encryption_error", { - "error": str(e), - "data_size": len(data) if data else 0 - }) - - return None - - def decrypt_data(self, encrypted_data: bytes, verify_auth: bool = None) -> Optional[bytes]: - """ - 解密数据 - - Args: - encrypted_data: 要解密的数据 - verify_auth: 是否验证认证(None表示使用配置) - - Returns: - 解密后的数据或None - """ - try: - if not self.enabled: - print("✗ 加密管理器未启用") - return encrypted_data # 不解密直接返回 - - if verify_auth is None: - verify_auth = self.encryption_config["enable_message_authentication"] - - original_size = len(encrypted_data) - self.encryption_stats["messages_decrypted"] += 1 - - # 验证认证信息 - data_to_decrypt = encrypted_data - if verify_auth: - try: - salt_length = self.encryption_config["salt_length"] - mac_length = 32 # SHA256 HMAC长度 - - if len(encrypted_data) < salt_length + mac_length: - raise ValueError("数据太短,无法包含认证信息") - - salt = encrypted_data[:salt_length] - expected_mac = encrypted_data[salt_length:salt_length + mac_length] - data_to_decrypt = encrypted_data[salt_length + mac_length:] - - # 验证MAC - actual_mac = hmac.new(self.session_key or self.master_key, - data_to_decrypt + salt, - hashlib.sha256).digest() - - if not hmac.compare_digest(expected_mac, actual_mac): - raise ValueError("消息认证失败") - - except Exception as e: - print(f"✗ 消息认证失败: {e}") - self.encryption_stats["auth_failures"] += 1 - - # 触发认证失败回调 - self._trigger_encryption_callback("auth_failed", { - "error": str(e) - }) - - return None - - # 使用Fernet解密 - try: - if self.cipher: - decrypted_data = self.cipher.decrypt(data_to_decrypt) - else: - # 尝试使用主密钥解密 - cipher = Fernet(self.master_key) - decrypted_data = cipher.decrypt(data_to_decrypt) - - self.encryption_stats["bytes_decrypted"] += len(decrypted_data) - return decrypted_data - - except Exception as e: - # 如果当前密钥失败,尝试使用前一个密钥(密钥轮换期间) - if self.previous_cipher and self.encryption_config["enable_key_rotation"]: - try: - decrypted_data = self.previous_cipher.decrypt(data_to_decrypt) - self.encryption_stats["bytes_decrypted"] += len(decrypted_data) - return decrypted_data - except: - pass - - raise e # 重新抛出原始异常 - - except Exception as e: - print(f"✗ 数据解密失败: {e}") - self.encryption_stats["decryption_errors"] += 1 - - # 触发加密错误回调 - self._trigger_encryption_callback("encryption_error", { - "error": str(e), - "data_size": original_size - }) - - return None - - def hash_password(self, password: str, salt: bytes = None) -> Dict[str, Any]: - """ - 哈希密码 - - Args: - password: 密码 - salt: 盐值(None表示自动生成) - - Returns: - 包含哈希值和盐值的字典 - """ - try: - if salt is None: - salt = secrets.token_bytes(16) - - # 使用PBKDF2进行密码哈希 - kdf = PBKDF2HMAC( - algorithm=hashes.SHA256(), - length=32, - salt=salt, - iterations=self.encryption_config["iterations"] - ) - - password_bytes = password.encode('utf-8') - hashed_password = base64.urlsafe_b64encode(kdf.derive(password_bytes)) - - return { - "hash": hashed_password.decode('utf-8'), - "salt": base64.urlsafe_b64encode(salt).decode('utf-8') - } - - except Exception as e: - print(f"✗ 密码哈希失败: {e}") - return {} - - def verify_password(self, password: str, hashed_password: str, salt: str) -> bool: - """ - 验证密码 - - Args: - password: 密码 - hashed_password: 哈希值 - salt: 盐值 - - Returns: - 密码是否正确 - """ - try: - # 解码盐值和哈希值 - salt_bytes = base64.urlsafe_b64decode(salt.encode('utf-8')) - stored_hash = base64.urlsafe_b64decode(hashed_password.encode('utf-8')) - - # 使用相同的参数进行哈希 - kdf = PBKDF2HMAC( - algorithm=hashes.SHA256(), - length=32, - salt=salt_bytes, - iterations=self.encryption_config["iterations"] - ) - - password_bytes = password.encode('utf-8') - - # 验证密码 - try: - kdf.verify(password_bytes, stored_hash) - return True - except Exception: - return False - - except Exception as e: - print(f"✗ 密码验证失败: {e}") - return False - - def generate_token(self, data: str, lifetime: int = 3600) -> str: - """ - 生成令牌 - - Args: - data: 令牌数据 - lifetime: 生存时间(秒) - - Returns: - 生成的令牌 - """ - try: - # 创建包含数据和过期时间的令牌 - token_data = { - "data": data, - "expires": time.time() + lifetime - } - - # 序列化数据 - token_string = str(token_data) - - # 加密令牌 - token_bytes = token_string.encode('utf-8') - encrypted_token = self.encrypt_data(token_bytes) - - if encrypted_token: - # Base64编码以便传输 - return base64.urlsafe_b64encode(encrypted_token).decode('utf-8') - else: - return "" - - except Exception as e: - print(f"✗ 令牌生成失败: {e}") - return "" - - def verify_token(self, token: str) -> Optional[str]: - """ - 验证令牌 - - Args: - token: 令牌 - - Returns: - 令牌数据或None - """ - try: - # Base64解码 - token_bytes = base64.urlsafe_b64decode(token.encode('utf-8')) - - # 解密令牌 - decrypted_data = self.decrypt_data(token_bytes) - - if decrypted_data: - # 反序列化数据 - token_string = decrypted_data.decode('utf-8') - token_data = eval(token_string) # 注意:实际应用中应使用安全的反序列化方法 - - # 检查过期时间 - if token_data.get("expires", 0) > time.time(): - return token_data.get("data") - else: - print("✗ 令牌已过期") - return None - else: - return None - - except Exception as e: - print(f"✗ 令牌验证失败: {e}") - return None - - def get_encryption_stats(self) -> Dict[str, Any]: - """ - 获取加密统计信息 - - Returns: - 加密统计字典 - """ - return self.encryption_stats.copy() - - def reset_encryption_stats(self): - """重置加密统计信息""" - try: - self.encryption_stats = { - "messages_encrypted": 0, - "messages_decrypted": 0, - "encryption_errors": 0, - "decryption_errors": 0, - "key_rotations": 0, - "auth_failures": 0, - "bytes_encrypted": 0, - "bytes_decrypted": 0 - } - print("✓ 加密统计信息已重置") - except Exception as e: - print(f"✗ 加密统计信息重置失败: {e}") - - def set_encryption_config(self, config: Dict[str, Any]) -> bool: - """ - 设置加密配置 - - Args: - config: 加密配置字典 - - Returns: - 是否设置成功 - """ - try: - old_enabled = self.encryption_config.get("enable_encryption", False) - new_enabled = config.get("enable_encryption", False) - - self.encryption_config.update(config) - print(f"✓ 加密配置已更新: {self.encryption_config}") - - # 如果启用了加密,确保密钥已生成 - if new_enabled and not old_enabled: - if not self.master_key: - self._generate_master_key() - if self.encryption_config["enable_session_keys"]: - self._generate_session_key() - - return True - except Exception as e: - print(f"✗ 加密配置设置失败: {e}") - return False - - def get_encryption_config(self) -> Dict[str, Any]: - """ - 获取加密配置 - - Returns: - 加密配置字典 - """ - return self.encryption_config.copy() - - def is_encryption_enabled(self) -> bool: - """ - 检查加密是否启用 - - Returns: - 加密是否启用 - """ - return self.enabled and self.encryption_config.get("enable_encryption", False) - - def _trigger_encryption_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发加密回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.encryption_callbacks: - for callback in self.encryption_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 加密回调执行失败: {e}") - except Exception as e: - print(f"✗ 加密回调触发失败: {e}") - - def register_encryption_callback(self, callback_type: str, callback: callable): - """ - 注册加密回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.encryption_callbacks: - self.encryption_callbacks[callback_type].append(callback) - print(f"✓ 加密回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 加密回调注册失败: {e}") - - def unregister_encryption_callback(self, callback_type: str, callback: callable): - """ - 注销加密回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.encryption_callbacks: - if callback in self.encryption_callbacks[callback_type]: - self.encryption_callbacks[callback_type].remove(callback) - print(f"✓ 加密回调已注销: {callback_type}") - except Exception as e: +""" +加密管理模块 +负责处理网络数据的加密和解密 +""" + +import time +from typing import Dict, Any, Optional +import hashlib +import hmac +import secrets +from cryptography.fernet import Fernet +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +import base64 + +class EncryptionManager: + """ + 加密管理器 + 负责处理网络数据的加密和解密 + """ + + def __init__(self, plugin): + """ + 初始化加密管理器 + + Args: + plugin: 网络同步插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 加密配置 + self.encryption_config = { + "enable_encryption": False, + "encryption_algorithm": "AES", # Fernet使用AES 128加密 + "key_derivation_function": "PBKDF2", + "salt_length": 16, + "iterations": 100000, + "enable_message_authentication": True, + "enable_session_keys": True, + "session_key_lifetime": 3600, # 1小时 + "enable_key_rotation": True, + "key_rotation_interval": 86400 # 24小时 + } + + # 加密密钥 + self.master_key = None + self.session_key = None + self.session_key_timestamp = 0.0 + self.previous_key = None + + # 加密器实例 + self.cipher = None + self.previous_cipher = None + + # 加密统计 + self.encryption_stats = { + "messages_encrypted": 0, + "messages_decrypted": 0, + "encryption_errors": 0, + "decryption_errors": 0, + "key_rotations": 0, + "auth_failures": 0, + "bytes_encrypted": 0, + "bytes_decrypted": 0 + } + + # 回调函数 + self.encryption_callbacks = { + "encryption_enabled": [], + "key_rotated": [], + "auth_failed": [], + "encryption_error": [] + } + + # 时间戳记录 + self.last_key_rotation = 0.0 + self.last_encryption_operation = 0.0 + + print("✓ 加密管理器已创建") + + def initialize(self) -> bool: + """ + 初始化加密管理器 + + Returns: + 是否初始化成功 + """ + try: + # 生成主密钥 + self._generate_master_key() + + self.initialized = True + print("✓ 加密管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 加密管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用加密管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 加密管理器未初始化") + return False + + self.enabled = True + + # 生成会话密钥 + if self.encryption_config["enable_session_keys"]: + self._generate_session_key() + + # 触发加密启用回调 + self._trigger_encryption_callback("encryption_enabled", { + "timestamp": time.time() + }) + + print("✓ 加密管理器已启用") + return True + + except Exception as e: + print(f"✗ 加密管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用加密管理器""" + try: + self.enabled = False + self.cipher = None + self.session_key = None + print("✓ 加密管理器已禁用") + + except Exception as e: + print(f"✗ 加密管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理加密管理器资源""" + try: + self.disable() + self.encryption_callbacks.clear() + self.master_key = None + self.previous_key = None + self.initialized = False + print("✓ 加密管理器资源已清理") + + except Exception as e: + print(f"✗ 加密管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新加密管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.last_encryption_operation = current_time + + # 检查密钥轮换 + if (self.encryption_config["enable_key_rotation"] and + current_time - self.last_key_rotation > self.encryption_config["key_rotation_interval"]): + self._rotate_keys() + + # 检查会话密钥过期 + if (self.encryption_config["enable_session_keys"] and + current_time - self.session_key_timestamp > self.encryption_config["session_key_lifetime"]): + self._generate_session_key() + + except Exception as e: + print(f"✗ 加密管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _generate_master_key(self): + """生成主密钥""" + try: + # 生成随机主密钥 + self.master_key = Fernet.generate_key() + print("✓ 主密钥已生成") + except Exception as e: + print(f"✗ 主密钥生成失败: {e}") + + def _generate_session_key(self): + """生成会话密钥""" + try: + if not self.master_key: + self._generate_master_key() + + # 生成新的会话密钥 + self.session_key = Fernet.generate_key() + self.session_key_timestamp = time.time() + self.cipher = Fernet(self.session_key) + + print("✓ 会话密钥已生成") + except Exception as e: + print(f"✗ 会话密钥生成失败: {e}") + + def _rotate_keys(self): + """轮换密钥""" + try: + # 保存当前密钥作为前一个密钥 + self.previous_key = self.session_key + self.previous_cipher = self.cipher + + # 生成新密钥 + self._generate_session_key() + + self.last_key_rotation = time.time() + self.encryption_stats["key_rotations"] += 1 + + # 触发密钥轮换回调 + self._trigger_encryption_callback("key_rotated", { + "timestamp": self.last_key_rotation + }) + + print("✓ 密钥已轮换") + except Exception as e: + print(f"✗ 密钥轮换失败: {e}") + + def encrypt_data(self, data: bytes, add_auth: bool = None) -> Optional[bytes]: + """ + 加密数据 + + Args: + data: 要加密的数据 + add_auth: 是否添加认证(None表示使用配置) + + Returns: + 加密后的数据或None + """ + try: + if not self.enabled: + print("✗ 加密管理器未启用") + return data # 不加密直接返回 + + if add_auth is None: + add_auth = self.encryption_config["enable_message_authentication"] + + self.encryption_stats["messages_encrypted"] += 1 + self.encryption_stats["bytes_encrypted"] += len(data) + + # 使用Fernet加密 + if self.cipher: + encrypted_data = self.cipher.encrypt(data) + else: + # 如果没有会话密钥,使用主密钥 + cipher = Fernet(self.master_key) + encrypted_data = cipher.encrypt(data) + + # 添加认证信息 + if add_auth: + # 使用HMAC添加消息认证码 + salt = secrets.token_bytes(self.encryption_config["salt_length"]) + mac = hmac.new(self.session_key or self.master_key, + encrypted_data + salt, + hashlib.sha256).digest() + encrypted_data = salt + mac + encrypted_data + + return encrypted_data + + except Exception as e: + print(f"✗ 数据加密失败: {e}") + self.encryption_stats["encryption_errors"] += 1 + + # 触发加密错误回调 + self._trigger_encryption_callback("encryption_error", { + "error": str(e), + "data_size": len(data) if data else 0 + }) + + return None + + def decrypt_data(self, encrypted_data: bytes, verify_auth: bool = None) -> Optional[bytes]: + """ + 解密数据 + + Args: + encrypted_data: 要解密的数据 + verify_auth: 是否验证认证(None表示使用配置) + + Returns: + 解密后的数据或None + """ + try: + if not self.enabled: + print("✗ 加密管理器未启用") + return encrypted_data # 不解密直接返回 + + if verify_auth is None: + verify_auth = self.encryption_config["enable_message_authentication"] + + original_size = len(encrypted_data) + self.encryption_stats["messages_decrypted"] += 1 + + # 验证认证信息 + data_to_decrypt = encrypted_data + if verify_auth: + try: + salt_length = self.encryption_config["salt_length"] + mac_length = 32 # SHA256 HMAC长度 + + if len(encrypted_data) < salt_length + mac_length: + raise ValueError("数据太短,无法包含认证信息") + + salt = encrypted_data[:salt_length] + expected_mac = encrypted_data[salt_length:salt_length + mac_length] + data_to_decrypt = encrypted_data[salt_length + mac_length:] + + # 验证MAC + actual_mac = hmac.new(self.session_key or self.master_key, + data_to_decrypt + salt, + hashlib.sha256).digest() + + if not hmac.compare_digest(expected_mac, actual_mac): + raise ValueError("消息认证失败") + + except Exception as e: + print(f"✗ 消息认证失败: {e}") + self.encryption_stats["auth_failures"] += 1 + + # 触发认证失败回调 + self._trigger_encryption_callback("auth_failed", { + "error": str(e) + }) + + return None + + # 使用Fernet解密 + try: + if self.cipher: + decrypted_data = self.cipher.decrypt(data_to_decrypt) + else: + # 尝试使用主密钥解密 + cipher = Fernet(self.master_key) + decrypted_data = cipher.decrypt(data_to_decrypt) + + self.encryption_stats["bytes_decrypted"] += len(decrypted_data) + return decrypted_data + + except Exception as e: + # 如果当前密钥失败,尝试使用前一个密钥(密钥轮换期间) + if self.previous_cipher and self.encryption_config["enable_key_rotation"]: + try: + decrypted_data = self.previous_cipher.decrypt(data_to_decrypt) + self.encryption_stats["bytes_decrypted"] += len(decrypted_data) + return decrypted_data + except: + pass + + raise e # 重新抛出原始异常 + + except Exception as e: + print(f"✗ 数据解密失败: {e}") + self.encryption_stats["decryption_errors"] += 1 + + # 触发加密错误回调 + self._trigger_encryption_callback("encryption_error", { + "error": str(e), + "data_size": original_size + }) + + return None + + def hash_password(self, password: str, salt: bytes = None) -> Dict[str, Any]: + """ + 哈希密码 + + Args: + password: 密码 + salt: 盐值(None表示自动生成) + + Returns: + 包含哈希值和盐值的字典 + """ + try: + if salt is None: + salt = secrets.token_bytes(16) + + # 使用PBKDF2进行密码哈希 + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt, + iterations=self.encryption_config["iterations"] + ) + + password_bytes = password.encode('utf-8') + hashed_password = base64.urlsafe_b64encode(kdf.derive(password_bytes)) + + return { + "hash": hashed_password.decode('utf-8'), + "salt": base64.urlsafe_b64encode(salt).decode('utf-8') + } + + except Exception as e: + print(f"✗ 密码哈希失败: {e}") + return {} + + def verify_password(self, password: str, hashed_password: str, salt: str) -> bool: + """ + 验证密码 + + Args: + password: 密码 + hashed_password: 哈希值 + salt: 盐值 + + Returns: + 密码是否正确 + """ + try: + # 解码盐值和哈希值 + salt_bytes = base64.urlsafe_b64decode(salt.encode('utf-8')) + stored_hash = base64.urlsafe_b64decode(hashed_password.encode('utf-8')) + + # 使用相同的参数进行哈希 + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt_bytes, + iterations=self.encryption_config["iterations"] + ) + + password_bytes = password.encode('utf-8') + + # 验证密码 + try: + kdf.verify(password_bytes, stored_hash) + return True + except Exception: + return False + + except Exception as e: + print(f"✗ 密码验证失败: {e}") + return False + + def generate_token(self, data: str, lifetime: int = 3600) -> str: + """ + 生成令牌 + + Args: + data: 令牌数据 + lifetime: 生存时间(秒) + + Returns: + 生成的令牌 + """ + try: + # 创建包含数据和过期时间的令牌 + token_data = { + "data": data, + "expires": time.time() + lifetime + } + + # 序列化数据 + token_string = str(token_data) + + # 加密令牌 + token_bytes = token_string.encode('utf-8') + encrypted_token = self.encrypt_data(token_bytes) + + if encrypted_token: + # Base64编码以便传输 + return base64.urlsafe_b64encode(encrypted_token).decode('utf-8') + else: + return "" + + except Exception as e: + print(f"✗ 令牌生成失败: {e}") + return "" + + def verify_token(self, token: str) -> Optional[str]: + """ + 验证令牌 + + Args: + token: 令牌 + + Returns: + 令牌数据或None + """ + try: + # Base64解码 + token_bytes = base64.urlsafe_b64decode(token.encode('utf-8')) + + # 解密令牌 + decrypted_data = self.decrypt_data(token_bytes) + + if decrypted_data: + # 反序列化数据 + token_string = decrypted_data.decode('utf-8') + token_data = eval(token_string) # 注意:实际应用中应使用安全的反序列化方法 + + # 检查过期时间 + if token_data.get("expires", 0) > time.time(): + return token_data.get("data") + else: + print("✗ 令牌已过期") + return None + else: + return None + + except Exception as e: + print(f"✗ 令牌验证失败: {e}") + return None + + def get_encryption_stats(self) -> Dict[str, Any]: + """ + 获取加密统计信息 + + Returns: + 加密统计字典 + """ + return self.encryption_stats.copy() + + def reset_encryption_stats(self): + """重置加密统计信息""" + try: + self.encryption_stats = { + "messages_encrypted": 0, + "messages_decrypted": 0, + "encryption_errors": 0, + "decryption_errors": 0, + "key_rotations": 0, + "auth_failures": 0, + "bytes_encrypted": 0, + "bytes_decrypted": 0 + } + print("✓ 加密统计信息已重置") + except Exception as e: + print(f"✗ 加密统计信息重置失败: {e}") + + def set_encryption_config(self, config: Dict[str, Any]) -> bool: + """ + 设置加密配置 + + Args: + config: 加密配置字典 + + Returns: + 是否设置成功 + """ + try: + old_enabled = self.encryption_config.get("enable_encryption", False) + new_enabled = config.get("enable_encryption", False) + + self.encryption_config.update(config) + print(f"✓ 加密配置已更新: {self.encryption_config}") + + # 如果启用了加密,确保密钥已生成 + if new_enabled and not old_enabled: + if not self.master_key: + self._generate_master_key() + if self.encryption_config["enable_session_keys"]: + self._generate_session_key() + + return True + except Exception as e: + print(f"✗ 加密配置设置失败: {e}") + return False + + def get_encryption_config(self) -> Dict[str, Any]: + """ + 获取加密配置 + + Returns: + 加密配置字典 + """ + return self.encryption_config.copy() + + def is_encryption_enabled(self) -> bool: + """ + 检查加密是否启用 + + Returns: + 加密是否启用 + """ + return self.enabled and self.encryption_config.get("enable_encryption", False) + + def _trigger_encryption_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发加密回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.encryption_callbacks: + for callback in self.encryption_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 加密回调执行失败: {e}") + except Exception as e: + print(f"✗ 加密回调触发失败: {e}") + + def register_encryption_callback(self, callback_type: str, callback: callable): + """ + 注册加密回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.encryption_callbacks: + self.encryption_callbacks[callback_type].append(callback) + print(f"✓ 加密回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 加密回调注册失败: {e}") + + def unregister_encryption_callback(self, callback_type: str, callback: callable): + """ + 注销加密回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.encryption_callbacks: + if callback in self.encryption_callbacks[callback_type]: + self.encryption_callbacks[callback_type].remove(callback) + print(f"✓ 加密回调已注销: {callback_type}") + except Exception as e: print(f"✗ 加密回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/network_sync/events/event_handler.py b/plugins/user/network_sync/events/event_handler.py index 03f14db8..0b9d6c2d 100644 --- a/plugins/user/network_sync/events/event_handler.py +++ b/plugins/user/network_sync/events/event_handler.py @@ -1,1158 +1,1158 @@ -""" -事件处理器模块 -负责处理网络同步相关的事件 -""" - -import time -from typing import Dict, Any, List, Optional, Callable -import threading -import queue - -class NetworkEvent: - """网络事件类""" - def __init__(self, event_type: str, data: Dict[str, Any], timestamp: float = None): - self.event_type = event_type - self.data = data - self.timestamp = timestamp or time.time() - self.handled = False - self.processed = False - -class EventHandler: - """ - 事件处理器 - 负责处理网络同步相关的事件 - """ - - def __init__(self, plugin): - """ - 初始化事件处理器 - - Args: - plugin: 网络同步插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 事件配置 - self.event_config = { - "enable_event_queue": True, - "max_queue_size": 1000, - "enable_async_processing": True, - "max_processing_threads": 4, - "event_timeout": 30.0, - "enable_event_filtering": True, - "enable_event_prioritization": True, - "enable_event_coalescing": True, - "coalesce_time_window": 0.1, # 100ms - "enable_event_logging": True, - "log_level": "INFO" - } - - # 事件队列 - self.event_queue = queue.Queue(maxsize=self.event_config["max_queue_size"]) - self.priority_queue = queue.PriorityQueue() - - # 事件处理器映射 - self.event_handlers = {} - - # 事件监听器 - self.event_listeners = {} - - # 事件统计 - self.event_stats = { - "events_received": 0, - "events_processed": 0, - "events_dropped": 0, - "events_timed_out": 0, - "handler_errors": 0, - "async_tasks": 0, - "coalesced_events": 0 - } - - # 事件合并缓存 - self.coalesce_cache = {} - - # 异步处理线程 - self.processing_threads = [] - self.running = False - - # 回调函数 - self.event_callbacks = { - "event_received": [], - "event_processed": [], - "event_dropped": [], - "event_error": [] - } - - # 时间戳记录 - self.last_event_time = 0.0 - self.last_processing_time = 0.0 - - print("✓ 事件处理器已创建") - - def initialize(self) -> bool: - """ - 初始化事件处理器 - - Returns: - 是否初始化成功 - """ - try: - # 注册默认事件处理器 - self._register_default_handlers() - - self.initialized = True - print("✓ 事件处理器初始化完成") - return True - - except Exception as e: - print(f"✗ 事件处理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用事件处理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 事件处理器未初始化") - return False - - self.enabled = True - self.running = True - - # 启动事件处理线程 - if self.event_config["enable_async_processing"]: - thread_count = self.event_config["max_processing_threads"] - for i in range(thread_count): - thread = threading.Thread(target=self._event_processing_loop, daemon=True, - name=f"EventProcessor-{i}") - self.processing_threads.append(thread) - thread.start() - - print("✓ 事件处理器已启用") - return True - - except Exception as e: - print(f"✗ 事件处理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用事件处理器""" - try: - self.enabled = False - self.running = False - - # 等待处理线程结束 - for thread in self.processing_threads: - if thread.is_alive(): - thread.join(timeout=2.0) - - self.processing_threads.clear() - print("✓ 事件处理器已禁用") - - except Exception as e: - print(f"✗ 事件处理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理事件处理器资源""" - try: - self.disable() - self.event_handlers.clear() - self.event_listeners.clear() - self.event_callbacks.clear() - self.coalesce_cache.clear() - - # 清空队列 - while not self.event_queue.empty(): - try: - self.event_queue.get_nowait() - except queue.Empty: - break - - while not self.priority_queue.empty(): - try: - self.priority_queue.get_nowait() - except queue.Empty: - break - - self.initialized = False - print("✓ 事件处理器资源已清理") - - except Exception as e: - print(f"✗ 事件处理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新事件处理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.last_processing_time = current_time - - # 清理过期的合并缓存 - self._cleanup_coalesce_cache(current_time) - - except Exception as e: - print(f"✗ 事件处理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _register_default_handlers(self): - """注册默认事件处理器""" - try: - # 网络相关事件 - self.register_event_handler("network_connected", self._handle_network_connected) - self.register_event_handler("network_disconnected", self._handle_network_disconnected) - self.register_event_handler("network_error", self._handle_network_error) - self.register_event_handler("network_data_received", self._handle_network_data_received) - self.register_event_handler("network_data_sent", self._handle_network_data_sent) - - # 同步相关事件 - self.register_event_handler("object_synced", self._handle_object_synced) - self.register_event_handler("sync_error", self._handle_sync_error) - self.register_event_handler("interpolation_updated", self._handle_interpolation_updated) - self.register_event_handler("extrapolation_updated", self._handle_extrapolation_updated) - - # 连接相关事件 - self.register_event_handler("client_connected", self._handle_client_connected) - self.register_event_handler("client_disconnected", self._handle_client_disconnected) - self.register_event_handler("connection_timeout", self._handle_connection_timeout) - - # 协议相关事件 - self.register_event_handler("message_received", self._handle_message_received) - self.register_event_handler("message_sent", self._handle_message_sent) - self.register_event_handler("protocol_error", self._handle_protocol_error) - - # 时钟相关事件 - self.register_event_handler("sync_started", self._handle_sync_started) - self.register_event_handler("sync_completed", self._handle_sync_completed) - self.register_event_handler("sync_failed", self._handle_sync_failed) - self.register_event_handler("time_updated", self._handle_time_updated) - self.register_event_handler("sync_quality_changed", self._handle_sync_quality_changed) - - # 配置相关事件 - self.register_event_handler("config_loaded", self._handle_config_loaded) - self.register_event_handler("config_saved", self._handle_config_saved) - self.register_event_handler("config_changed", self._handle_config_changed) - self.register_event_handler("config_error", self._handle_config_error) - - # 序列化相关事件 - self.register_event_handler("data_serialized", self._handle_data_serialized) - self.register_event_handler("data_deserialized", self._handle_data_deserialized) - self.register_event_handler("serialization_error", self._handle_serialization_error) - self.register_event_handler("deserialization_error", self._handle_deserialization_error) - - # 预测相关事件 - self.register_event_handler("prediction_updated", self._handle_prediction_updated) - self.register_event_handler("correction_applied", self._handle_correction_applied) - self.register_event_handler("snap_correction", self._handle_snap_correction) - self.register_event_handler("prediction_error", self._handle_prediction_error) - - print("✓ 默认事件处理器已注册") - except Exception as e: - print(f"✗ 默认事件处理器注册失败: {e}") - - def _event_processing_loop(self): - """事件处理循环""" - try: - while self.running: - try: - # 优先处理优先级队列中的事件 - try: - priority, event = self.priority_queue.get(timeout=0.01) - self._process_event(event) - self.priority_queue.task_done() - except queue.Empty: - # 处理普通队列中的事件 - try: - event = self.event_queue.get(timeout=0.01) - self._process_event(event) - self.event_queue.task_done() - except queue.Empty: - # 没有事件,短暂休眠 - time.sleep(0.001) - - except Exception as e: - print(f"✗ 事件处理循环错误: {e}") - time.sleep(0.1) - - except Exception as e: - print(f"✗ 事件处理循环失败: {e}") - - def _process_event(self, event: NetworkEvent): - """ - 处理事件 - - Args: - event: 网络事件 - """ - try: - current_time = time.time() - - # 检查事件是否超时 - if current_time - event.timestamp > self.event_config["event_timeout"]: - self.event_stats["events_timed_out"] += 1 - self._trigger_event_callback("event_dropped", { - "event": event, - "reason": "timeout" - }) - return - - # 查找事件处理器 - handler = self.event_handlers.get(event.event_type) - if handler: - try: - # 调用事件处理器 - result = handler(event.data) - event.processed = True - - # 更新统计信息 - self.event_stats["events_processed"] += 1 - - # 触发事件处理完成回调 - self._trigger_event_callback("event_processed", { - "event": event, - "result": result, - "processing_time": time.time() - event.timestamp - }) - - except Exception as e: - print(f"✗ 事件处理器执行失败: {event.event_type} - {e}") - self.event_stats["handler_errors"] += 1 - - # 触发事件错误回调 - self._trigger_event_callback("event_error", { - "event": event, - "error": str(e) - }) - else: - # 没有找到处理器,但仍标记为已处理 - event.processed = True - self.event_stats["events_processed"] += 1 - - # 触发事件处理完成回调 - self._trigger_event_callback("event_processed", { - "event": event, - "result": None, - "processing_time": time.time() - event.timestamp - }) - - except Exception as e: - print(f"✗ 事件处理失败: {e}") - - def post_event(self, event_type: str, data: Dict[str, Any] = None, - priority: int = 0, timestamp: float = None) -> bool: - """ - 发布事件 - - Args: - event_type: 事件类型 - data: 事件数据 - priority: 事件优先级(数字越小优先级越高) - timestamp: 事件时间戳 - - Returns: - 是否发布成功 - """ - try: - if not self.enabled: - print("✗ 事件处理器未启用") - return False - - # 创建事件对象 - event = NetworkEvent(event_type, data or {}, timestamp) - - # 触发事件接收回调 - self._trigger_event_callback("event_received", { - "event": event - }) - - # 检查事件合并 - if self.event_config["enable_event_coalescing"]: - if self._coalesce_event(event): - # 事件已合并,不需要添加到队列 - return True - - # 添加到相应队列 - if priority > 0 and self.event_config["enable_event_prioritization"]: - # 添加到优先级队列 - try: - self.priority_queue.put((priority, event), timeout=0.1) - except queue.Full: - self.event_stats["events_dropped"] += 1 - self._trigger_event_callback("event_dropped", { - "event": event, - "reason": "priority_queue_full" - }) - return False - else: - # 添加到普通队列 - try: - self.event_queue.put(event, timeout=0.1) - except queue.Full: - self.event_stats["events_dropped"] += 1 - self._trigger_event_callback("event_dropped", { - "event": event, - "reason": "queue_full" - }) - return False - - self.event_stats["events_received"] += 1 - self.last_event_time = event.timestamp - - return True - - except Exception as e: - print(f"✗ 事件发布失败: {e}") - return False - - def _coalesce_event(self, event: NetworkEvent) -> bool: - """ - 合并事件 - - Args: - event: 网络事件 - - Returns: - 是否合并成功 - """ - try: - if not self.event_config["enable_event_coalescing"]: - return False - - current_time = time.time() - coalesce_key = f"{event.event_type}_{hash(str(event.data))}" - - # 检查是否在合并时间窗口内 - if coalesce_key in self.coalesce_cache: - cached_event, cache_time = self.coalesce_cache[coalesce_key] - if current_time - cache_time < self.event_config["coalesce_time_window"]: - # 合并事件 - self.event_stats["coalesced_events"] += 1 - return True - - # 缓存事件 - self.coalesce_cache[coalesce_key] = (event, current_time) - return False - - except Exception as e: - print(f"✗ 事件合并失败: {e}") - return False - - def _cleanup_coalesce_cache(self, current_time: float): - """ - 清理合并缓存 - - Args: - current_time: 当前时间 - """ - try: - expired_keys = [ - key for key, (_, cache_time) in self.coalesce_cache.items() - if current_time - cache_time > self.event_config["coalesce_time_window"] * 2 - ] - - for key in expired_keys: - del self.coalesce_cache[key] - - except Exception as e: - print(f"✗ 合并缓存清理失败: {e}") - - def register_event_handler(self, event_type: str, handler: Callable) -> bool: - """ - 注册事件处理器 - - Args: - event_type: 事件类型 - handler: 处理器函数 - - Returns: - 是否注册成功 - """ - try: - self.event_handlers[event_type] = handler - print(f"✓ 事件处理器已注册: {event_type}") - return True - except Exception as e: - print(f"✗ 事件处理器注册失败: {e}") - return False - - def unregister_event_handler(self, event_type: str) -> bool: - """ - 注销事件处理器 - - Args: - event_type: 事件类型 - - Returns: - 是否注销成功 - """ - try: - if event_type in self.event_handlers: - del self.event_handlers[event_type] - print(f"✓ 事件处理器已注销: {event_type}") - return True - else: - print(f"✗ 事件处理器不存在: {event_type}") - return False - except Exception as e: - print(f"✗ 事件处理器注销失败: {e}") - return False - - def register_event_listener(self, event_type: str, listener: Callable) -> bool: - """ - 注册事件监听器 - - Args: - event_type: 事件类型 - listener: 监听器函数 - - Returns: - 是否注册成功 - """ - try: - if event_type not in self.event_listeners: - self.event_listeners[event_type] = [] - - self.event_listeners[event_type].append(listener) - print(f"✓ 事件监听器已注册: {event_type}") - return True - except Exception as e: - print(f"✗ 事件监听器注册失败: {e}") - return False - - def unregister_event_listener(self, event_type: str, listener: Callable) -> bool: - """ - 注销事件监听器 - - Args: - event_type: 事件类型 - listener: 监听器函数 - - Returns: - 是否注销成功 - """ - try: - if event_type in self.event_listeners: - if listener in self.event_listeners[event_type]: - self.event_listeners[event_type].remove(listener) - print(f"✓ 事件监听器已注销: {event_type}") - return True - - print(f"✗ 事件监听器不存在: {event_type}") - return False - except Exception as e: - print(f"✗ 事件监听器注销失败: {e}") - return False - - # 默认事件处理器 - def _handle_network_connected(self, data: Dict[str, Any]) -> bool: - """处理网络连接事件""" - try: - print(f"✓ 网络已连接: {data}") - - # 更新连接状态 - if self.plugin.connection_manager: - self.plugin.connection_manager.client_state["is_connected"] = True - self.plugin.connection_manager.client_state["connection_time"] = time.time() - - return True - except Exception as e: - print(f"✗ 网络连接事件处理失败: {e}") - return False - - def _handle_network_disconnected(self, data: Dict[str, Any]) -> bool: - """处理网络断开事件""" - try: - print(f"✓ 网络已断开: {data}") - - # 更新连接状态 - if self.plugin.connection_manager: - self.plugin.connection_manager.client_state["is_connected"] = False - self.plugin.connection_manager.client_state["connection_time"] = 0.0 - - return True - except Exception as e: - print(f"✗ 网络断开事件处理失败: {e}") - return False - - def _handle_network_error(self, data: Dict[str, Any]) -> bool: - """处理网络错误事件""" - try: - print(f"✗ 网络错误: {data}") - - # 记录错误统计 - self.event_stats["handler_errors"] += 1 - - return True - except Exception as e: - print(f"✗ 网络错误事件处理失败: {e}") - return False - - def _handle_network_data_received(self, data: Dict[str, Any]) -> bool: - """处理网络数据接收事件""" - try: - # 将数据传递给协议处理器 - if self.plugin.protocol_handler: - self.plugin.protocol_handler.receive_message(data.get("message_data", b"")) - - return True - except Exception as e: - print(f"✗ 网络数据接收事件处理失败: {e}") - return False - - def _handle_network_data_sent(self, data: Dict[str, Any]) -> bool: - """处理网络数据发送事件""" - try: - # 可以在这里添加发送确认逻辑 - return True - except Exception as e: - print(f"✗ 网络数据发送事件处理失败: {e}") - return False - - def _handle_object_synced(self, data: Dict[str, Any]) -> bool: - """处理对象同步事件""" - try: - object_id = data.get("object_id") - object_data = data.get("object_data") - - # 更新对象状态 - if self.plugin.object_sync and object_id and object_data: - self.plugin.object_sync.update_object(object_id, object_data) - - return True - except Exception as e: - print(f"✗ 对象同步事件处理失败: {e}") - return False - - def _handle_sync_error(self, data: Dict[str, Any]) -> bool: - """处理同步错误事件""" - try: - print(f"✗ 同步错误: {data}") - - # 记录错误统计 - self.event_stats["handler_errors"] += 1 - - return True - except Exception as e: - print(f"✗ 同步错误事件处理失败: {e}") - return False - - def _handle_interpolation_updated(self, data: Dict[str, Any]) -> bool: - """处理插值更新事件""" - try: - # 可以在这里添加插值更新后的处理逻辑 - return True - except Exception as e: - print(f"✗ 插值更新事件处理失败: {e}") - return False - - def _handle_extrapolation_updated(self, data: Dict[str, Any]) -> bool: - """处理外推更新事件""" - try: - # 可以在这里添加外推更新后的处理逻辑 - return True - except Exception as e: - print(f"✗ 外推更新事件处理失败: {e}") - return False - - def _handle_client_connected(self, data: Dict[str, Any]) -> bool: - """处理客户端连接事件""" - try: - client_id = data.get("client_id") - address = data.get("address") - - print(f"✓ 客户端已连接: {client_id} ({address})") - - # 添加到连接管理器 - if self.plugin.connection_manager and client_id and address: - # 这里需要访问socket对象,简化处理 - pass - - return True - except Exception as e: - print(f"✗ 客户端连接事件处理失败: {e}") - return False - - def _handle_client_disconnected(self, data: Dict[str, Any]) -> bool: - """处理客户端断开事件""" - try: - client_id = data.get("client_id") - reason = data.get("reason", "unknown") - - print(f"✓ 客户端已断开: {client_id} (原因: {reason})") - - # 从连接管理器移除 - if self.plugin.connection_manager and client_id: - self.plugin.connection_manager.remove_client(client_id, reason) - - return True - except Exception as e: - print(f"✗ 客户端断开事件处理失败: {e}") - return False - - def _handle_connection_timeout(self, data: Dict[str, Any]) -> bool: - """处理连接超时事件""" - try: - client_id = data.get("client_id") - - print(f"✓ 连接超时: {client_id}") - - # 断开超时连接 - if self.plugin.connection_manager and client_id: - self.plugin.connection_manager.remove_client(client_id, "timeout") - - return True - except Exception as e: - print(f"✗ 连接超时事件处理失败: {e}") - return False - - def _handle_message_received(self, data: Dict[str, Any]) -> bool: - """处理消息接收事件""" - try: - message_type = data.get("type") - message_data = data.get("data") - - print(f"✓ 消息已接收: {message_type}") - - # 根据消息类型处理 - if message_type == "sync_data": - # 处理同步数据 - if self.plugin.object_sync and message_data: - for obj_id, obj_data in message_data.items(): - self.plugin.object_sync.receive_sync_data(obj_id, obj_data) - - elif message_type == "heartbeat": - # 处理心跳包 - if self.plugin.connection_manager: - # 需要client_id来处理心跳 - pass - - return True - except Exception as e: - print(f"✗ 消息接收事件处理失败: {e}") - return False - - def _handle_message_sent(self, data: Dict[str, Any]) -> bool: - """处理消息发送事件""" - try: - message_type = data.get("type") - fragmented = data.get("fragmented", False) - - print(f"✓ 消息已发送: {message_type} ({'分片' if fragmented else '完整'})") - - return True - except Exception as e: - print(f"✗ 消息发送事件处理失败: {e}") - return False - - def _handle_protocol_error(self, data: Dict[str, Any]) -> bool: - """处理协议错误事件""" - try: - error = data.get("error", "Unknown protocol error") - print(f"✗ 协议错误: {error}") - - # 记录错误统计 - self.event_stats["handler_errors"] += 1 - - return True - except Exception as e: - print(f"✗ 协议错误事件处理失败: {e}") - return False - - def _handle_sync_started(self, data: Dict[str, Any]) -> bool: - """处理同步开始事件""" - try: - print("✓ 时钟同步已开始") - return True - except Exception as e: - print(f"✗ 同步开始事件处理失败: {e}") - return False - - def _handle_sync_completed(self, data: Dict[str, Any]) -> bool: - """处理同步完成事件""" - try: - offset = data.get("offset", 0.0) - rtt = data.get("rtt", 0.0) - quality = data.get("quality", 0.0) - - print(f"✓ 时钟同步已完成 - 偏移: {offset:.3f}s, RTT: {rtt:.3f}s, 质量: {quality:.2f}") - - return True - except Exception as e: - print(f"✗ 同步完成事件处理失败: {e}") - return False - - def _handle_sync_failed(self, data: Dict[str, Any]) -> bool: - """处理同步失败事件""" - try: - reason = data.get("reason", "unknown") - error = data.get("error", "") - - print(f"✗ 时钟同步失败 - 原因: {reason}, 错误: {error}") - - # 记录错误统计 - self.event_stats["handler_errors"] += 1 - - return True - except Exception as e: - print(f"✗ 同步失败事件处理失败: {e}") - return False - - def _handle_time_updated(self, data: Dict[str, Any]) -> bool: - """处理时间更新事件""" - try: - server_time = data.get("server_time", 0.0) - interpolated_time = data.get("interpolated_time", 0.0) - - # 可以在这里添加时间更新后的处理逻辑 - return True - except Exception as e: - print(f"✗ 时间更新事件处理失败: {e}") - return False - - def _handle_sync_quality_changed(self, data: Dict[str, Any]) -> bool: - """处理同步质量变化事件""" - try: - quality = data.get("quality", 0.0) - offset = data.get("offset", 0.0) - rtt = data.get("rtt", 0.0) - - print(f"✓ 同步质量变化 - 质量: {quality:.2f}, 偏移: {offset:.3f}s, RTT: {rtt:.3f}s") - - return True - except Exception as e: - print(f"✗ 同步质量变化事件处理失败: {e}") - return False - - def _handle_config_loaded(self, data: Dict[str, Any]) -> bool: - """处理配置加载事件""" - try: - config = data.get("config", {}) - timestamp = data.get("timestamp", 0.0) - - print(f"✓ 配置已加载 - 时间: {timestamp}") - - # 应用配置到各个模块 - if self.plugin.config_manager: - self.plugin.config_manager.apply_config_to_modules() - - return True - except Exception as e: - print(f"✗ 配置加载事件处理失败: {e}") - return False - - def _handle_config_saved(self, data: Dict[str, Any]) -> bool: - """处理配置保存事件""" - try: - config = data.get("config", {}) - timestamp = data.get("timestamp", 0.0) - - print(f"✓ 配置已保存 - 时间: {timestamp}") - - return True - except Exception as e: - print(f"✗ 配置保存事件处理失败: {e}") - return False - - def _handle_config_changed(self, data: Dict[str, Any]) -> bool: - """处理配置变更事件""" - try: - section = data.get("section") - key = data.get("key") - old_value = data.get("old_value") - new_value = data.get("new_value") - - print(f"✓ 配置已变更 - {section}.{key}: {old_value} -> {new_value}") - - # 应用配置变更到相应模块 - if self.plugin.config_manager: - self.plugin.config_manager.apply_config_to_modules() - - return True - except Exception as e: - print(f"✗ 配置变更事件处理失败: {e}") - return False - - def _handle_config_error(self, data: Dict[str, Any]) -> bool: - """处理配置错误事件""" - try: - error = data.get("error", "Unknown config error") - operation = data.get("operation", "unknown") - - print(f"✗ 配置错误 - 操作: {operation}, 错误: {error}") - - # 记录错误统计 - self.event_stats["handler_errors"] += 1 - - return True - except Exception as e: - print(f"✗ 配置错误事件处理失败: {e}") - return False - - def _handle_data_serialized(self, data: Dict[str, Any]) -> bool: - """处理数据序列化事件""" - try: - format = data.get("format", "unknown") - size = data.get("size", 0) - cached = data.get("cached", False) - - print(f"✓ 数据已序列化 - 格式: {format}, 大小: {size}字节 ({'缓存' if cached else '新'})") - - return True - except Exception as e: - print(f"✗ 数据序列化事件处理失败: {e}") - return False - - def _handle_data_deserialized(self, data: Dict[str, Any]) -> bool: - """处理数据反序列化事件""" - try: - format = data.get("format", "unknown") - size = data.get("size", 0) - - print(f"✓ 数据已反序列化 - 格式: {format}, 大小: {size}字节") - - return True - except Exception as e: - print(f"✗ 数据反序列化事件处理失败: {e}") - return False - - def _handle_serialization_error(self, data: Dict[str, Any]) -> bool: - """处理序列化错误事件""" - try: - error = data.get("error", "Unknown serialization error") - - print(f"✗ 序列化错误: {error}") - - # 记录错误统计 - self.event_stats["handler_errors"] += 1 - - return True - except Exception as e: - print(f"✗ 序列化错误事件处理失败: {e}") - return False - - def _handle_deserialization_error(self, data: Dict[str, Any]) -> bool: - """处理反序列化错误事件""" - try: - error = data.get("error", "Unknown deserialization error") - - print(f"✗ 反序列化错误: {error}") - - # 记录错误统计 - self.event_stats["handler_errors"] += 1 - - return True - except Exception as e: - print(f"✗ 反序列化错误事件处理失败: {e}") - return False - - def _handle_prediction_updated(self, data: Dict[str, Any]) -> bool: - """处理预测更新事件""" - try: - object_id = data.get("object_id") - prediction_time = data.get("prediction_time", 0.0) - - print(f"✓ 预测已更新 - 对象: {object_id}, 时间: {prediction_time:.3f}") - - return True - except Exception as e: - print(f"✗ 预测更新事件处理失败: {e}") - return False - - def _handle_correction_applied(self, data: Dict[str, Any]) -> bool: - """处理校正应用事件""" - try: - object_id = data.get("object_id") - error = data.get("error", 0.0) - - print(f"✓ 校正已应用 - 对象: {object_id}, 误差: {error:.3f}") - - return True - except Exception as e: - print(f"✗ 校正应用事件处理失败: {e}") - return False - - def _handle_snap_correction(self, data: Dict[str, Any]) -> bool: - """处理快照校正事件""" - try: - object_id = data.get("object_id") - error = data.get("error", 0.0) - - print(f"✓ 快照校正 - 对象: {object_id}, 误差: {error:.3f}") - - return True - except Exception as e: - print(f"✗ 快照校正事件处理失败: {e}") - return False - - def _handle_prediction_error(self, data: Dict[str, Any]) -> bool: - """处理预测错误事件""" - try: - object_id = data.get("object_id") - error = data.get("error", "") - - print(f"✗ 预测错误 - 对象: {object_id}, 错误: {error}") - - # 记录错误统计 - self.event_stats["handler_errors"] += 1 - - return True - except Exception as e: - print(f"✗ 预测错误事件处理失败: {e}") - return False - - def get_event_stats(self) -> Dict[str, int]: - """ - 获取事件统计信息 - - Returns: - 事件统计字典 - """ - return self.event_stats.copy() - - def reset_event_stats(self): - """重置事件统计信息""" - try: - self.event_stats = { - "events_received": 0, - "events_processed": 0, - "events_dropped": 0, - "events_timed_out": 0, - "handler_errors": 0, - "async_tasks": 0, - "coalesced_events": 0 - } - print("✓ 事件统计信息已重置") - except Exception as e: - print(f"✗ 事件统计信息重置失败: {e}") - - def set_event_config(self, config: Dict[str, Any]) -> bool: - """ - 设置事件配置 - - Args: - config: 事件配置字典 - - Returns: - 是否设置成功 - """ - try: - self.event_config.update(config) - print(f"✓ 事件配置已更新: {self.event_config}") - return True - except Exception as e: - print(f"✗ 事件配置设置失败: {e}") - return False - - def get_event_config(self) -> Dict[str, Any]: - """ - 获取事件配置 - - Returns: - 事件配置字典 - """ - return self.event_config.copy() - - def _trigger_event_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发事件回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.event_callbacks: - for callback in self.event_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 事件回调执行失败: {e}") - except Exception as e: - print(f"✗ 事件回调触发失败: {e}") - - def register_event_callback(self, callback_type: str, callback: Callable): - """ - 注册事件回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.event_callbacks: - self.event_callbacks[callback_type].append(callback) - print(f"✓ 事件回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 事件回调注册失败: {e}") - - def unregister_event_callback(self, callback_type: str, callback: Callable): - """ - 注销事件回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.event_callbacks: - if callback in self.event_callbacks[callback_type]: - self.event_callbacks[callback_type].remove(callback) - print(f"✓ 事件回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 事件回调注销失败: {e}") - - def clear_event_queue(self): - """清空事件队列""" - try: - # 清空普通队列 - while not self.event_queue.empty(): - try: - self.event_queue.get_nowait() - except queue.Empty: - break - - # 清空优先级队列 - while not self.priority_queue.empty(): - try: - self.priority_queue.get_nowait() - except queue.Empty: - break - - print("✓ 事件队列已清空") - except Exception as e: - print(f"✗ 事件队列清空失败: {e}") - - def get_queue_size(self) -> Dict[str, int]: - """ - 获取队列大小 - - Returns: - 队列大小字典 - """ - return { - "event_queue": self.event_queue.qsize(), - "priority_queue": self.priority_queue.qsize() +""" +事件处理器模块 +负责处理网络同步相关的事件 +""" + +import time +from typing import Dict, Any, List, Optional, Callable +import threading +import queue + +class NetworkEvent: + """网络事件类""" + def __init__(self, event_type: str, data: Dict[str, Any], timestamp: float = None): + self.event_type = event_type + self.data = data + self.timestamp = timestamp or time.time() + self.handled = False + self.processed = False + +class EventHandler: + """ + 事件处理器 + 负责处理网络同步相关的事件 + """ + + def __init__(self, plugin): + """ + 初始化事件处理器 + + Args: + plugin: 网络同步插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 事件配置 + self.event_config = { + "enable_event_queue": True, + "max_queue_size": 1000, + "enable_async_processing": True, + "max_processing_threads": 4, + "event_timeout": 30.0, + "enable_event_filtering": True, + "enable_event_prioritization": True, + "enable_event_coalescing": True, + "coalesce_time_window": 0.1, # 100ms + "enable_event_logging": True, + "log_level": "INFO" + } + + # 事件队列 + self.event_queue = queue.Queue(maxsize=self.event_config["max_queue_size"]) + self.priority_queue = queue.PriorityQueue() + + # 事件处理器映射 + self.event_handlers = {} + + # 事件监听器 + self.event_listeners = {} + + # 事件统计 + self.event_stats = { + "events_received": 0, + "events_processed": 0, + "events_dropped": 0, + "events_timed_out": 0, + "handler_errors": 0, + "async_tasks": 0, + "coalesced_events": 0 + } + + # 事件合并缓存 + self.coalesce_cache = {} + + # 异步处理线程 + self.processing_threads = [] + self.running = False + + # 回调函数 + self.event_callbacks = { + "event_received": [], + "event_processed": [], + "event_dropped": [], + "event_error": [] + } + + # 时间戳记录 + self.last_event_time = 0.0 + self.last_processing_time = 0.0 + + print("✓ 事件处理器已创建") + + def initialize(self) -> bool: + """ + 初始化事件处理器 + + Returns: + 是否初始化成功 + """ + try: + # 注册默认事件处理器 + self._register_default_handlers() + + self.initialized = True + print("✓ 事件处理器初始化完成") + return True + + except Exception as e: + print(f"✗ 事件处理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用事件处理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 事件处理器未初始化") + return False + + self.enabled = True + self.running = True + + # 启动事件处理线程 + if self.event_config["enable_async_processing"]: + thread_count = self.event_config["max_processing_threads"] + for i in range(thread_count): + thread = threading.Thread(target=self._event_processing_loop, daemon=True, + name=f"EventProcessor-{i}") + self.processing_threads.append(thread) + thread.start() + + print("✓ 事件处理器已启用") + return True + + except Exception as e: + print(f"✗ 事件处理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用事件处理器""" + try: + self.enabled = False + self.running = False + + # 等待处理线程结束 + for thread in self.processing_threads: + if thread.is_alive(): + thread.join(timeout=2.0) + + self.processing_threads.clear() + print("✓ 事件处理器已禁用") + + except Exception as e: + print(f"✗ 事件处理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理事件处理器资源""" + try: + self.disable() + self.event_handlers.clear() + self.event_listeners.clear() + self.event_callbacks.clear() + self.coalesce_cache.clear() + + # 清空队列 + while not self.event_queue.empty(): + try: + self.event_queue.get_nowait() + except queue.Empty: + break + + while not self.priority_queue.empty(): + try: + self.priority_queue.get_nowait() + except queue.Empty: + break + + self.initialized = False + print("✓ 事件处理器资源已清理") + + except Exception as e: + print(f"✗ 事件处理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新事件处理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.last_processing_time = current_time + + # 清理过期的合并缓存 + self._cleanup_coalesce_cache(current_time) + + except Exception as e: + print(f"✗ 事件处理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _register_default_handlers(self): + """注册默认事件处理器""" + try: + # 网络相关事件 + self.register_event_handler("network_connected", self._handle_network_connected) + self.register_event_handler("network_disconnected", self._handle_network_disconnected) + self.register_event_handler("network_error", self._handle_network_error) + self.register_event_handler("network_data_received", self._handle_network_data_received) + self.register_event_handler("network_data_sent", self._handle_network_data_sent) + + # 同步相关事件 + self.register_event_handler("object_synced", self._handle_object_synced) + self.register_event_handler("sync_error", self._handle_sync_error) + self.register_event_handler("interpolation_updated", self._handle_interpolation_updated) + self.register_event_handler("extrapolation_updated", self._handle_extrapolation_updated) + + # 连接相关事件 + self.register_event_handler("client_connected", self._handle_client_connected) + self.register_event_handler("client_disconnected", self._handle_client_disconnected) + self.register_event_handler("connection_timeout", self._handle_connection_timeout) + + # 协议相关事件 + self.register_event_handler("message_received", self._handle_message_received) + self.register_event_handler("message_sent", self._handle_message_sent) + self.register_event_handler("protocol_error", self._handle_protocol_error) + + # 时钟相关事件 + self.register_event_handler("sync_started", self._handle_sync_started) + self.register_event_handler("sync_completed", self._handle_sync_completed) + self.register_event_handler("sync_failed", self._handle_sync_failed) + self.register_event_handler("time_updated", self._handle_time_updated) + self.register_event_handler("sync_quality_changed", self._handle_sync_quality_changed) + + # 配置相关事件 + self.register_event_handler("config_loaded", self._handle_config_loaded) + self.register_event_handler("config_saved", self._handle_config_saved) + self.register_event_handler("config_changed", self._handle_config_changed) + self.register_event_handler("config_error", self._handle_config_error) + + # 序列化相关事件 + self.register_event_handler("data_serialized", self._handle_data_serialized) + self.register_event_handler("data_deserialized", self._handle_data_deserialized) + self.register_event_handler("serialization_error", self._handle_serialization_error) + self.register_event_handler("deserialization_error", self._handle_deserialization_error) + + # 预测相关事件 + self.register_event_handler("prediction_updated", self._handle_prediction_updated) + self.register_event_handler("correction_applied", self._handle_correction_applied) + self.register_event_handler("snap_correction", self._handle_snap_correction) + self.register_event_handler("prediction_error", self._handle_prediction_error) + + print("✓ 默认事件处理器已注册") + except Exception as e: + print(f"✗ 默认事件处理器注册失败: {e}") + + def _event_processing_loop(self): + """事件处理循环""" + try: + while self.running: + try: + # 优先处理优先级队列中的事件 + try: + priority, event = self.priority_queue.get(timeout=0.01) + self._process_event(event) + self.priority_queue.task_done() + except queue.Empty: + # 处理普通队列中的事件 + try: + event = self.event_queue.get(timeout=0.01) + self._process_event(event) + self.event_queue.task_done() + except queue.Empty: + # 没有事件,短暂休眠 + time.sleep(0.001) + + except Exception as e: + print(f"✗ 事件处理循环错误: {e}") + time.sleep(0.1) + + except Exception as e: + print(f"✗ 事件处理循环失败: {e}") + + def _process_event(self, event: NetworkEvent): + """ + 处理事件 + + Args: + event: 网络事件 + """ + try: + current_time = time.time() + + # 检查事件是否超时 + if current_time - event.timestamp > self.event_config["event_timeout"]: + self.event_stats["events_timed_out"] += 1 + self._trigger_event_callback("event_dropped", { + "event": event, + "reason": "timeout" + }) + return + + # 查找事件处理器 + handler = self.event_handlers.get(event.event_type) + if handler: + try: + # 调用事件处理器 + result = handler(event.data) + event.processed = True + + # 更新统计信息 + self.event_stats["events_processed"] += 1 + + # 触发事件处理完成回调 + self._trigger_event_callback("event_processed", { + "event": event, + "result": result, + "processing_time": time.time() - event.timestamp + }) + + except Exception as e: + print(f"✗ 事件处理器执行失败: {event.event_type} - {e}") + self.event_stats["handler_errors"] += 1 + + # 触发事件错误回调 + self._trigger_event_callback("event_error", { + "event": event, + "error": str(e) + }) + else: + # 没有找到处理器,但仍标记为已处理 + event.processed = True + self.event_stats["events_processed"] += 1 + + # 触发事件处理完成回调 + self._trigger_event_callback("event_processed", { + "event": event, + "result": None, + "processing_time": time.time() - event.timestamp + }) + + except Exception as e: + print(f"✗ 事件处理失败: {e}") + + def post_event(self, event_type: str, data: Dict[str, Any] = None, + priority: int = 0, timestamp: float = None) -> bool: + """ + 发布事件 + + Args: + event_type: 事件类型 + data: 事件数据 + priority: 事件优先级(数字越小优先级越高) + timestamp: 事件时间戳 + + Returns: + 是否发布成功 + """ + try: + if not self.enabled: + print("✗ 事件处理器未启用") + return False + + # 创建事件对象 + event = NetworkEvent(event_type, data or {}, timestamp) + + # 触发事件接收回调 + self._trigger_event_callback("event_received", { + "event": event + }) + + # 检查事件合并 + if self.event_config["enable_event_coalescing"]: + if self._coalesce_event(event): + # 事件已合并,不需要添加到队列 + return True + + # 添加到相应队列 + if priority > 0 and self.event_config["enable_event_prioritization"]: + # 添加到优先级队列 + try: + self.priority_queue.put((priority, event), timeout=0.1) + except queue.Full: + self.event_stats["events_dropped"] += 1 + self._trigger_event_callback("event_dropped", { + "event": event, + "reason": "priority_queue_full" + }) + return False + else: + # 添加到普通队列 + try: + self.event_queue.put(event, timeout=0.1) + except queue.Full: + self.event_stats["events_dropped"] += 1 + self._trigger_event_callback("event_dropped", { + "event": event, + "reason": "queue_full" + }) + return False + + self.event_stats["events_received"] += 1 + self.last_event_time = event.timestamp + + return True + + except Exception as e: + print(f"✗ 事件发布失败: {e}") + return False + + def _coalesce_event(self, event: NetworkEvent) -> bool: + """ + 合并事件 + + Args: + event: 网络事件 + + Returns: + 是否合并成功 + """ + try: + if not self.event_config["enable_event_coalescing"]: + return False + + current_time = time.time() + coalesce_key = f"{event.event_type}_{hash(str(event.data))}" + + # 检查是否在合并时间窗口内 + if coalesce_key in self.coalesce_cache: + cached_event, cache_time = self.coalesce_cache[coalesce_key] + if current_time - cache_time < self.event_config["coalesce_time_window"]: + # 合并事件 + self.event_stats["coalesced_events"] += 1 + return True + + # 缓存事件 + self.coalesce_cache[coalesce_key] = (event, current_time) + return False + + except Exception as e: + print(f"✗ 事件合并失败: {e}") + return False + + def _cleanup_coalesce_cache(self, current_time: float): + """ + 清理合并缓存 + + Args: + current_time: 当前时间 + """ + try: + expired_keys = [ + key for key, (_, cache_time) in self.coalesce_cache.items() + if current_time - cache_time > self.event_config["coalesce_time_window"] * 2 + ] + + for key in expired_keys: + del self.coalesce_cache[key] + + except Exception as e: + print(f"✗ 合并缓存清理失败: {e}") + + def register_event_handler(self, event_type: str, handler: Callable) -> bool: + """ + 注册事件处理器 + + Args: + event_type: 事件类型 + handler: 处理器函数 + + Returns: + 是否注册成功 + """ + try: + self.event_handlers[event_type] = handler + print(f"✓ 事件处理器已注册: {event_type}") + return True + except Exception as e: + print(f"✗ 事件处理器注册失败: {e}") + return False + + def unregister_event_handler(self, event_type: str) -> bool: + """ + 注销事件处理器 + + Args: + event_type: 事件类型 + + Returns: + 是否注销成功 + """ + try: + if event_type in self.event_handlers: + del self.event_handlers[event_type] + print(f"✓ 事件处理器已注销: {event_type}") + return True + else: + print(f"✗ 事件处理器不存在: {event_type}") + return False + except Exception as e: + print(f"✗ 事件处理器注销失败: {e}") + return False + + def register_event_listener(self, event_type: str, listener: Callable) -> bool: + """ + 注册事件监听器 + + Args: + event_type: 事件类型 + listener: 监听器函数 + + Returns: + 是否注册成功 + """ + try: + if event_type not in self.event_listeners: + self.event_listeners[event_type] = [] + + self.event_listeners[event_type].append(listener) + print(f"✓ 事件监听器已注册: {event_type}") + return True + except Exception as e: + print(f"✗ 事件监听器注册失败: {e}") + return False + + def unregister_event_listener(self, event_type: str, listener: Callable) -> bool: + """ + 注销事件监听器 + + Args: + event_type: 事件类型 + listener: 监听器函数 + + Returns: + 是否注销成功 + """ + try: + if event_type in self.event_listeners: + if listener in self.event_listeners[event_type]: + self.event_listeners[event_type].remove(listener) + print(f"✓ 事件监听器已注销: {event_type}") + return True + + print(f"✗ 事件监听器不存在: {event_type}") + return False + except Exception as e: + print(f"✗ 事件监听器注销失败: {e}") + return False + + # 默认事件处理器 + def _handle_network_connected(self, data: Dict[str, Any]) -> bool: + """处理网络连接事件""" + try: + print(f"✓ 网络已连接: {data}") + + # 更新连接状态 + if self.plugin.connection_manager: + self.plugin.connection_manager.client_state["is_connected"] = True + self.plugin.connection_manager.client_state["connection_time"] = time.time() + + return True + except Exception as e: + print(f"✗ 网络连接事件处理失败: {e}") + return False + + def _handle_network_disconnected(self, data: Dict[str, Any]) -> bool: + """处理网络断开事件""" + try: + print(f"✓ 网络已断开: {data}") + + # 更新连接状态 + if self.plugin.connection_manager: + self.plugin.connection_manager.client_state["is_connected"] = False + self.plugin.connection_manager.client_state["connection_time"] = 0.0 + + return True + except Exception as e: + print(f"✗ 网络断开事件处理失败: {e}") + return False + + def _handle_network_error(self, data: Dict[str, Any]) -> bool: + """处理网络错误事件""" + try: + print(f"✗ 网络错误: {data}") + + # 记录错误统计 + self.event_stats["handler_errors"] += 1 + + return True + except Exception as e: + print(f"✗ 网络错误事件处理失败: {e}") + return False + + def _handle_network_data_received(self, data: Dict[str, Any]) -> bool: + """处理网络数据接收事件""" + try: + # 将数据传递给协议处理器 + if self.plugin.protocol_handler: + self.plugin.protocol_handler.receive_message(data.get("message_data", b"")) + + return True + except Exception as e: + print(f"✗ 网络数据接收事件处理失败: {e}") + return False + + def _handle_network_data_sent(self, data: Dict[str, Any]) -> bool: + """处理网络数据发送事件""" + try: + # 可以在这里添加发送确认逻辑 + return True + except Exception as e: + print(f"✗ 网络数据发送事件处理失败: {e}") + return False + + def _handle_object_synced(self, data: Dict[str, Any]) -> bool: + """处理对象同步事件""" + try: + object_id = data.get("object_id") + object_data = data.get("object_data") + + # 更新对象状态 + if self.plugin.object_sync and object_id and object_data: + self.plugin.object_sync.update_object(object_id, object_data) + + return True + except Exception as e: + print(f"✗ 对象同步事件处理失败: {e}") + return False + + def _handle_sync_error(self, data: Dict[str, Any]) -> bool: + """处理同步错误事件""" + try: + print(f"✗ 同步错误: {data}") + + # 记录错误统计 + self.event_stats["handler_errors"] += 1 + + return True + except Exception as e: + print(f"✗ 同步错误事件处理失败: {e}") + return False + + def _handle_interpolation_updated(self, data: Dict[str, Any]) -> bool: + """处理插值更新事件""" + try: + # 可以在这里添加插值更新后的处理逻辑 + return True + except Exception as e: + print(f"✗ 插值更新事件处理失败: {e}") + return False + + def _handle_extrapolation_updated(self, data: Dict[str, Any]) -> bool: + """处理外推更新事件""" + try: + # 可以在这里添加外推更新后的处理逻辑 + return True + except Exception as e: + print(f"✗ 外推更新事件处理失败: {e}") + return False + + def _handle_client_connected(self, data: Dict[str, Any]) -> bool: + """处理客户端连接事件""" + try: + client_id = data.get("client_id") + address = data.get("address") + + print(f"✓ 客户端已连接: {client_id} ({address})") + + # 添加到连接管理器 + if self.plugin.connection_manager and client_id and address: + # 这里需要访问socket对象,简化处理 + pass + + return True + except Exception as e: + print(f"✗ 客户端连接事件处理失败: {e}") + return False + + def _handle_client_disconnected(self, data: Dict[str, Any]) -> bool: + """处理客户端断开事件""" + try: + client_id = data.get("client_id") + reason = data.get("reason", "unknown") + + print(f"✓ 客户端已断开: {client_id} (原因: {reason})") + + # 从连接管理器移除 + if self.plugin.connection_manager and client_id: + self.plugin.connection_manager.remove_client(client_id, reason) + + return True + except Exception as e: + print(f"✗ 客户端断开事件处理失败: {e}") + return False + + def _handle_connection_timeout(self, data: Dict[str, Any]) -> bool: + """处理连接超时事件""" + try: + client_id = data.get("client_id") + + print(f"✓ 连接超时: {client_id}") + + # 断开超时连接 + if self.plugin.connection_manager and client_id: + self.plugin.connection_manager.remove_client(client_id, "timeout") + + return True + except Exception as e: + print(f"✗ 连接超时事件处理失败: {e}") + return False + + def _handle_message_received(self, data: Dict[str, Any]) -> bool: + """处理消息接收事件""" + try: + message_type = data.get("type") + message_data = data.get("data") + + print(f"✓ 消息已接收: {message_type}") + + # 根据消息类型处理 + if message_type == "sync_data": + # 处理同步数据 + if self.plugin.object_sync and message_data: + for obj_id, obj_data in message_data.items(): + self.plugin.object_sync.receive_sync_data(obj_id, obj_data) + + elif message_type == "heartbeat": + # 处理心跳包 + if self.plugin.connection_manager: + # 需要client_id来处理心跳 + pass + + return True + except Exception as e: + print(f"✗ 消息接收事件处理失败: {e}") + return False + + def _handle_message_sent(self, data: Dict[str, Any]) -> bool: + """处理消息发送事件""" + try: + message_type = data.get("type") + fragmented = data.get("fragmented", False) + + print(f"✓ 消息已发送: {message_type} ({'分片' if fragmented else '完整'})") + + return True + except Exception as e: + print(f"✗ 消息发送事件处理失败: {e}") + return False + + def _handle_protocol_error(self, data: Dict[str, Any]) -> bool: + """处理协议错误事件""" + try: + error = data.get("error", "Unknown protocol error") + print(f"✗ 协议错误: {error}") + + # 记录错误统计 + self.event_stats["handler_errors"] += 1 + + return True + except Exception as e: + print(f"✗ 协议错误事件处理失败: {e}") + return False + + def _handle_sync_started(self, data: Dict[str, Any]) -> bool: + """处理同步开始事件""" + try: + print("✓ 时钟同步已开始") + return True + except Exception as e: + print(f"✗ 同步开始事件处理失败: {e}") + return False + + def _handle_sync_completed(self, data: Dict[str, Any]) -> bool: + """处理同步完成事件""" + try: + offset = data.get("offset", 0.0) + rtt = data.get("rtt", 0.0) + quality = data.get("quality", 0.0) + + print(f"✓ 时钟同步已完成 - 偏移: {offset:.3f}s, RTT: {rtt:.3f}s, 质量: {quality:.2f}") + + return True + except Exception as e: + print(f"✗ 同步完成事件处理失败: {e}") + return False + + def _handle_sync_failed(self, data: Dict[str, Any]) -> bool: + """处理同步失败事件""" + try: + reason = data.get("reason", "unknown") + error = data.get("error", "") + + print(f"✗ 时钟同步失败 - 原因: {reason}, 错误: {error}") + + # 记录错误统计 + self.event_stats["handler_errors"] += 1 + + return True + except Exception as e: + print(f"✗ 同步失败事件处理失败: {e}") + return False + + def _handle_time_updated(self, data: Dict[str, Any]) -> bool: + """处理时间更新事件""" + try: + server_time = data.get("server_time", 0.0) + interpolated_time = data.get("interpolated_time", 0.0) + + # 可以在这里添加时间更新后的处理逻辑 + return True + except Exception as e: + print(f"✗ 时间更新事件处理失败: {e}") + return False + + def _handle_sync_quality_changed(self, data: Dict[str, Any]) -> bool: + """处理同步质量变化事件""" + try: + quality = data.get("quality", 0.0) + offset = data.get("offset", 0.0) + rtt = data.get("rtt", 0.0) + + print(f"✓ 同步质量变化 - 质量: {quality:.2f}, 偏移: {offset:.3f}s, RTT: {rtt:.3f}s") + + return True + except Exception as e: + print(f"✗ 同步质量变化事件处理失败: {e}") + return False + + def _handle_config_loaded(self, data: Dict[str, Any]) -> bool: + """处理配置加载事件""" + try: + config = data.get("config", {}) + timestamp = data.get("timestamp", 0.0) + + print(f"✓ 配置已加载 - 时间: {timestamp}") + + # 应用配置到各个模块 + if self.plugin.config_manager: + self.plugin.config_manager.apply_config_to_modules() + + return True + except Exception as e: + print(f"✗ 配置加载事件处理失败: {e}") + return False + + def _handle_config_saved(self, data: Dict[str, Any]) -> bool: + """处理配置保存事件""" + try: + config = data.get("config", {}) + timestamp = data.get("timestamp", 0.0) + + print(f"✓ 配置已保存 - 时间: {timestamp}") + + return True + except Exception as e: + print(f"✗ 配置保存事件处理失败: {e}") + return False + + def _handle_config_changed(self, data: Dict[str, Any]) -> bool: + """处理配置变更事件""" + try: + section = data.get("section") + key = data.get("key") + old_value = data.get("old_value") + new_value = data.get("new_value") + + print(f"✓ 配置已变更 - {section}.{key}: {old_value} -> {new_value}") + + # 应用配置变更到相应模块 + if self.plugin.config_manager: + self.plugin.config_manager.apply_config_to_modules() + + return True + except Exception as e: + print(f"✗ 配置变更事件处理失败: {e}") + return False + + def _handle_config_error(self, data: Dict[str, Any]) -> bool: + """处理配置错误事件""" + try: + error = data.get("error", "Unknown config error") + operation = data.get("operation", "unknown") + + print(f"✗ 配置错误 - 操作: {operation}, 错误: {error}") + + # 记录错误统计 + self.event_stats["handler_errors"] += 1 + + return True + except Exception as e: + print(f"✗ 配置错误事件处理失败: {e}") + return False + + def _handle_data_serialized(self, data: Dict[str, Any]) -> bool: + """处理数据序列化事件""" + try: + format = data.get("format", "unknown") + size = data.get("size", 0) + cached = data.get("cached", False) + + print(f"✓ 数据已序列化 - 格式: {format}, 大小: {size}字节 ({'缓存' if cached else '新'})") + + return True + except Exception as e: + print(f"✗ 数据序列化事件处理失败: {e}") + return False + + def _handle_data_deserialized(self, data: Dict[str, Any]) -> bool: + """处理数据反序列化事件""" + try: + format = data.get("format", "unknown") + size = data.get("size", 0) + + print(f"✓ 数据已反序列化 - 格式: {format}, 大小: {size}字节") + + return True + except Exception as e: + print(f"✗ 数据反序列化事件处理失败: {e}") + return False + + def _handle_serialization_error(self, data: Dict[str, Any]) -> bool: + """处理序列化错误事件""" + try: + error = data.get("error", "Unknown serialization error") + + print(f"✗ 序列化错误: {error}") + + # 记录错误统计 + self.event_stats["handler_errors"] += 1 + + return True + except Exception as e: + print(f"✗ 序列化错误事件处理失败: {e}") + return False + + def _handle_deserialization_error(self, data: Dict[str, Any]) -> bool: + """处理反序列化错误事件""" + try: + error = data.get("error", "Unknown deserialization error") + + print(f"✗ 反序列化错误: {error}") + + # 记录错误统计 + self.event_stats["handler_errors"] += 1 + + return True + except Exception as e: + print(f"✗ 反序列化错误事件处理失败: {e}") + return False + + def _handle_prediction_updated(self, data: Dict[str, Any]) -> bool: + """处理预测更新事件""" + try: + object_id = data.get("object_id") + prediction_time = data.get("prediction_time", 0.0) + + print(f"✓ 预测已更新 - 对象: {object_id}, 时间: {prediction_time:.3f}") + + return True + except Exception as e: + print(f"✗ 预测更新事件处理失败: {e}") + return False + + def _handle_correction_applied(self, data: Dict[str, Any]) -> bool: + """处理校正应用事件""" + try: + object_id = data.get("object_id") + error = data.get("error", 0.0) + + print(f"✓ 校正已应用 - 对象: {object_id}, 误差: {error:.3f}") + + return True + except Exception as e: + print(f"✗ 校正应用事件处理失败: {e}") + return False + + def _handle_snap_correction(self, data: Dict[str, Any]) -> bool: + """处理快照校正事件""" + try: + object_id = data.get("object_id") + error = data.get("error", 0.0) + + print(f"✓ 快照校正 - 对象: {object_id}, 误差: {error:.3f}") + + return True + except Exception as e: + print(f"✗ 快照校正事件处理失败: {e}") + return False + + def _handle_prediction_error(self, data: Dict[str, Any]) -> bool: + """处理预测错误事件""" + try: + object_id = data.get("object_id") + error = data.get("error", "") + + print(f"✗ 预测错误 - 对象: {object_id}, 错误: {error}") + + # 记录错误统计 + self.event_stats["handler_errors"] += 1 + + return True + except Exception as e: + print(f"✗ 预测错误事件处理失败: {e}") + return False + + def get_event_stats(self) -> Dict[str, int]: + """ + 获取事件统计信息 + + Returns: + 事件统计字典 + """ + return self.event_stats.copy() + + def reset_event_stats(self): + """重置事件统计信息""" + try: + self.event_stats = { + "events_received": 0, + "events_processed": 0, + "events_dropped": 0, + "events_timed_out": 0, + "handler_errors": 0, + "async_tasks": 0, + "coalesced_events": 0 + } + print("✓ 事件统计信息已重置") + except Exception as e: + print(f"✗ 事件统计信息重置失败: {e}") + + def set_event_config(self, config: Dict[str, Any]) -> bool: + """ + 设置事件配置 + + Args: + config: 事件配置字典 + + Returns: + 是否设置成功 + """ + try: + self.event_config.update(config) + print(f"✓ 事件配置已更新: {self.event_config}") + return True + except Exception as e: + print(f"✗ 事件配置设置失败: {e}") + return False + + def get_event_config(self) -> Dict[str, Any]: + """ + 获取事件配置 + + Returns: + 事件配置字典 + """ + return self.event_config.copy() + + def _trigger_event_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发事件回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.event_callbacks: + for callback in self.event_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 事件回调执行失败: {e}") + except Exception as e: + print(f"✗ 事件回调触发失败: {e}") + + def register_event_callback(self, callback_type: str, callback: Callable): + """ + 注册事件回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.event_callbacks: + self.event_callbacks[callback_type].append(callback) + print(f"✓ 事件回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 事件回调注册失败: {e}") + + def unregister_event_callback(self, callback_type: str, callback: Callable): + """ + 注销事件回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.event_callbacks: + if callback in self.event_callbacks[callback_type]: + self.event_callbacks[callback_type].remove(callback) + print(f"✓ 事件回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 事件回调注销失败: {e}") + + def clear_event_queue(self): + """清空事件队列""" + try: + # 清空普通队列 + while not self.event_queue.empty(): + try: + self.event_queue.get_nowait() + except queue.Empty: + break + + # 清空优先级队列 + while not self.priority_queue.empty(): + try: + self.priority_queue.get_nowait() + except queue.Empty: + break + + print("✓ 事件队列已清空") + except Exception as e: + print(f"✗ 事件队列清空失败: {e}") + + def get_queue_size(self) -> Dict[str, int]: + """ + 获取队列大小 + + Returns: + 队列大小字典 + """ + return { + "event_queue": self.event_queue.qsize(), + "priority_queue": self.priority_queue.qsize() } \ No newline at end of file diff --git a/plugins/user/network_sync/network/network_manager.py b/plugins/user/network_sync/network/network_manager.py index 9e714ee3..d4624eed 100644 --- a/plugins/user/network_sync/network/network_manager.py +++ b/plugins/user/network_sync/network/network_manager.py @@ -1,942 +1,942 @@ -""" -网络管理器模块 -负责处理网络连接和数据传输 -""" - -import time -import socket -import threading -from typing import Dict, Any, List, Optional -import json -import struct - -class NetworkManager: - """ - 网络管理器 - 负责处理网络连接和数据传输 - """ - - def __init__(self, plugin): - """ - 初始化网络管理器 - - Args: - plugin: 网络同步插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 网络配置 - self.network_config = { - 'server_host': 'localhost', - 'server_port': 7777, - 'max_connections': 32, - 'buffer_size': 4096, - 'timeout': 30.0, - 'heartbeat_interval': 5.0, - 'enable_compression': True, - 'enable_encryption': False, - 'protocol_version': '1.0' - } - - # 服务器相关 - self.server_socket = None - self.server_thread = None - self.is_server_running = False - self.server_clients = {} # 客户端连接 - - # 客户端相关 - self.client_socket = None - self.client_thread = None - self.is_client_connected = False - self.client_id = None - - # 连接状态 - self.connection_state = { - 'is_connected': False, - 'connection_time': 0.0, - 'last_heartbeat': 0.0, - 'bytes_sent': 0, - 'bytes_received': 0, - 'packets_sent': 0, - 'packets_received': 0 - } - - # 网络统计 - self.network_stats = { - 'connections_accepted': 0, - 'connections_rejected': 0, - 'connections_lost': 0, - 'data_sent': 0, - 'data_received': 0, - 'packets_sent': 0, - 'packets_received': 0, - 'network_errors': 0 - } - - # 消息队列 - self.message_queue = [] - self.message_lock = threading.Lock() - - # 回调函数 - self.network_callbacks = { - 'client_connected': [], - 'client_disconnected': [], - 'data_received': [], - 'network_error': [], - 'server_started': [], - 'server_stopped': [] - } - - # 时间戳记录 - self.last_heartbeat_time = 0.0 - self.last_network_update = 0.0 - - print("✓ 网络管理器已创建") - - def initialize(self) -> bool: - """ - 初始化网络管理器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 网络管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 网络管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用网络管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 网络管理器未初始化") - return False - - self.enabled = True - print("✓ 网络管理器已启用") - return True - - except Exception as e: - print(f"✗ 网络管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用网络管理器""" - try: - self.enabled = False - - # 停止服务器 - if self.is_server_running: - self.stop_server() - - # 断开客户端 - if self.is_client_connected: - self.disconnect_client() - - print("✓ 网络管理器已禁用") - - except Exception as e: - print(f"✗ 网络管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理网络管理器资源""" - try: - self.disable() - self.network_callbacks.clear() - self.server_clients.clear() - self.initialized = False - print("✓ 网络管理器资源已清理") - - except Exception as e: - print(f"✗ 网络管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新网络管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.last_network_update = current_time - - # 处理消息队列 - self._process_message_queue() - - # 发送心跳包 - if current_time - self.last_heartbeat_time >= self.network_config['heartbeat_interval']: - self._send_heartbeat() - self.last_heartbeat_time = current_time - - except Exception as e: - print(f"✗ 网络管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _process_message_queue(self): - """处理消息队列""" - try: - with self.message_lock: - while self.message_queue: - message = self.message_queue.pop(0) - self._handle_message(message) - except Exception as e: - print(f"✗ 消息队列处理失败: {e}") - - def _handle_message(self, message: Dict[str, Any]): - """ - 处理消息 - - Args: - message: 消息数据 - """ - try: - message_type = message.get('type') - message_data = message.get('data') - client_id = message.get('client_id') - - # 触发数据接收回调 - self._trigger_network_callback('data_received', { - 'type': message_type, - 'data': message_data, - 'client_id': client_id - }) - - # 根据消息类型处理 - if message_type == 'sync_data': - self._handle_sync_data(message_data, client_id) - elif message_type == 'heartbeat': - self._handle_heartbeat(message_data, client_id) - elif message_type == 'connect': - self._handle_connect(message_data, client_id) - elif message_type == 'disconnect': - self._handle_disconnect(message_data, client_id) - - except Exception as e: - print(f"✗ 消息处理失败: {e}") - self.network_stats['network_errors'] += 1 - - def _handle_sync_data(self, data: Dict[str, Any], client_id: str): - """ - 处理同步数据 - - Args: - data: 同步数据 - client_id: 客户端ID - """ - try: - # 将同步数据传递给对象同步模块 - if self.plugin.object_sync: - for obj_id, obj_data in data.items(): - self.plugin.object_sync.receive_sync_data(obj_id, obj_data, client_id) - except Exception as e: - print(f"✗ 同步数据处理失败: {e}") - - def _handle_heartbeat(self, data: Dict[str, Any], client_id: str): - """ - 处理心跳包 - - Args: - data: 心跳数据 - client_id: 客户端ID - """ - try: - # 更新客户端最后心跳时间 - if client_id in self.server_clients: - self.server_clients[client_id]['last_heartbeat'] = time.time() - except Exception as e: - print(f"✗ 心跳包处理失败: {e}") - - def _handle_connect(self, data: Dict[str, Any], client_id: str): - """ - 处理连接消息 - - Args: - data: 连接数据 - client_id: 客户端ID - """ - try: - # 触发客户端连接回调 - self._trigger_network_callback('client_connected', { - 'client_id': client_id, - 'data': data - }) - except Exception as e: - print(f"✗ 连接消息处理失败: {e}") - - def _handle_disconnect(self, data: Dict[str, Any], client_id: str): - """ - 处理断开连接消息 - - Args: - data: 断开连接数据 - client_id: 客户端ID - """ - try: - # 从客户端列表中移除 - if client_id in self.server_clients: - del self.server_clients[client_id] - - # 触发客户端断开连接回调 - self._trigger_network_callback('client_disconnected', { - 'client_id': client_id, - 'data': data - }) - except Exception as e: - print(f"✗ 断开连接消息处理失败: {e}") - - def start_server(self, port: int = None) -> bool: - """ - 启动服务器 - - Args: - port: 服务器端口 - - Returns: - 是否启动成功 - """ - try: - if self.is_server_running: - print("✗ 服务器已在运行") - return False - - if port is None: - port = self.network_config['server_port'] - - # 创建服务器套接字 - self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.server_socket.bind((self.network_config['server_host'], port)) - self.server_socket.listen(self.network_config['max_connections']) - self.server_socket.settimeout(1.0) # 设置超时以便可以检查运行状态 - - self.is_server_running = True - self.network_config['server_port'] = port - - # 启动服务器线程 - self.server_thread = threading.Thread(target=self._server_loop, daemon=True) - self.server_thread.start() - - # 触发服务器启动回调 - self._trigger_network_callback('server_started', { - 'host': self.network_config['server_host'], - 'port': port - }) - - print(f"✓ 服务器已启动: {self.network_config['server_host']}:{port}") - return True - - except Exception as e: - print(f"✗ 服务器启动失败: {e}") - import traceback - traceback.print_exc() - return False - - def stop_server(self): - """停止服务器""" - try: - self.is_server_running = False - - # 关闭所有客户端连接 - for client_id, client_data in list(self.server_clients.items()): - try: - client_data['socket'].close() - except Exception as e: - pass - - self.server_clients.clear() - - # 关闭服务器套接字 - if self.server_socket: - self.server_socket.close() - self.server_socket = None - - # 等待服务器线程结束 - if self.server_thread and self.server_thread.is_alive(): - self.server_thread.join(timeout=2.0) - - # 触发服务器停止回调 - self._trigger_network_callback('server_stopped', {}) - - print("✓ 服务器已停止") - - except Exception as e: - print(f"✗ 服务器停止失败: {e}") - - def _server_loop(self): - """服务器主循环""" - try: - print("✓ 服务器主循环已启动") - - while self.is_server_running: - try: - # 接受客户端连接 - client_socket, client_address = self.server_socket.accept() - - # 创建客户端线程 - client_thread = threading.Thread( - target=self._handle_client, - args=(client_socket, client_address), - daemon=True - ) - client_thread.start() - - # 分配客户端ID - client_id = f"client_{int(time.time() * 1000)}" - - # 添加到客户端列表 - self.server_clients[client_id] = { - 'socket': client_socket, - 'address': client_address, - 'thread': client_thread, - 'connect_time': time.time(), - 'last_heartbeat': time.time(), - 'bytes_sent': 0, - 'bytes_received': 0 - } - - self.network_stats['connections_accepted'] += 1 - print(f"✓ 客户端已连接: {client_address} ({client_id})") - - except socket.timeout: - # 正常超时,继续循环 - continue - except Exception as e: - if self.is_server_running: - print(f"✗ 接受客户端连接失败: {e}") - self.network_stats['network_errors'] += 1 - - except Exception as e: - print(f"✗ 服务器主循环失败: {e}") - finally: - print("✓ 服务器主循环已结束") - - def _handle_client(self, client_socket: socket.socket, client_address: tuple): - """ - 处理客户端连接 - - Args: - client_socket: 客户端套接字 - client_address: 客户端地址 - """ - client_id = None - try: - # 为客户端分配ID - client_id = f"client_{hash(client_address)}_{int(time.time() * 1000)}" - - print(f"✓ 开始处理客户端: {client_address} ({client_id})") - - # 发送连接确认消息 - connect_message = { - 'type': 'connect_ack', - 'data': { - 'client_id': client_id, - 'server_time': time.time(), - 'protocol_version': self.network_config['protocol_version'] - } - } - self._send_message(client_socket, connect_message) - - # 主接收循环 - while self.is_server_running: - try: - # 接收消息 - message = self._receive_message(client_socket) - if message is None: - # 客户端断开连接 - break - - # 添加客户端ID到消息 - message['client_id'] = client_id - - # 添加到消息队列 - with self.message_lock: - self.message_queue.append(message) - - # 更新统计信息 - self.network_stats['packets_received'] += 1 - self.network_stats['data_received'] += len(str(message).encode('utf-8')) - - except socket.timeout: - # 检查心跳超时 - if client_id in self.server_clients: - last_heartbeat = self.server_clients[client_id]['last_heartbeat'] - if time.time() - last_heartbeat > self.network_config['timeout']: - print(f"✗ 客户端心跳超时: {client_address} ({client_id})") - break - continue - except Exception as e: - print(f"✗ 接收客户端消息失败: {e}") - break - - except Exception as e: - print(f"✗ 处理客户端连接失败: {e}") - finally: - # 清理客户端连接 - if client_id and client_id in self.server_clients: - del self.server_clients[client_id] - - try: - client_socket.close() - except Exception as e: - pass - - self.network_stats['connections_lost'] += 1 - print(f"✓ 客户端连接已断开: {client_address} ({client_id})") - - # 触发客户端断开连接回调 - self._trigger_network_callback('client_disconnected', { - 'client_id': client_id, - 'address': client_address - }) - - def connect_to_server(self, host: str = None, port: int = None) -> bool: - """ - 连接到服务器 - - Args: - host: 服务器地址 - port: 服务器端口 - - Returns: - 是否连接成功 - """ - try: - if self.is_client_connected: - print("✗ 客户端已连接到服务器") - return False - - if host is None: - host = self.network_config['server_host'] - - if port is None: - port = self.network_config['server_port'] - - # 创建客户端套接字 - self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.client_socket.settimeout(self.network_config['timeout']) - - # 连接服务器 - self.client_socket.connect((host, port)) - - # 启动客户端接收线程 - self.client_thread = threading.Thread(target=self._client_receive_loop, daemon=True) - self.client_thread.start() - - # 等待连接确认 - connect_ack = self._receive_message(self.client_socket) - if connect_ack and connect_ack.get('type') == 'connect_ack': - self.client_id = connect_ack['data']['client_id'] - self.is_client_connected = True - self.connection_state['is_connected'] = True - self.connection_state['connection_time'] = time.time() - - print(f"✓ 已连接到服务器: {host}:{port} (Client ID: {self.client_id})") - return True - else: - print("✗ 服务器连接确认失败") - self.disconnect_client() - return False - - except Exception as e: - print(f"✗ 连接服务器失败: {e}") - import traceback - traceback.print_exc() - self.disconnect_client() - return False - - def disconnect_client(self): - """断开客户端连接""" - try: - self.is_client_connected = False - self.connection_state['is_connected'] = False - - if self.client_socket: - try: - # 发送断开连接消息 - disconnect_message = { - 'type': 'disconnect', - 'data': {} - } - self._send_message(self.client_socket, disconnect_message) - except Exception as e: - pass - - self.client_socket.close() - self.client_socket = None - - # 等待客户端线程结束 - if self.client_thread and self.client_thread.is_alive(): - self.client_thread.join(timeout=2.0) - - self.client_id = None - print("✓ 客户端已断开连接") - - except Exception as e: - print(f"✗ 客户端断开连接失败: {e}") - - def _client_receive_loop(self): - """客户端接收循环""" - try: - while self.is_client_connected and self.client_socket: - try: - # 接收消息 - message = self._receive_message(self.client_socket) - if message is None: - # 服务器断开连接 - break - - # 添加到消息队列 - with self.message_lock: - self.message_queue.append(message) - - # 更新统计信息 - self.network_stats['packets_received'] += 1 - self.network_stats['data_received'] += len(str(message).encode('utf-8')) - - except socket.timeout: - continue - except Exception as e: - if self.is_client_connected: - print(f"✗ 接收服务器消息失败: {e}") - break - - except Exception as e: - print(f"✗ 客户端接收循环失败: {e}") - finally: - if self.is_client_connected: - self.disconnect_client() - - def send_sync_data(self, data: Dict[str, Any], target_client_id: str = None) -> bool: - """ - 发送同步数据 - - Args: - data: 同步数据 - target_client_id: 目标客户端ID(None表示广播给所有客户端) - - Returns: - 是否发送成功 - """ - try: - if not self.enabled: - print("✗ 网络管理器未启用") - return False - - message = { - 'type': 'sync_data', - 'data': data - } - - # 服务器模式:广播给客户端 - if self.is_server_running: - if target_client_id: - # 发送给特定客户端 - if target_client_id in self.server_clients: - client_socket = self.server_clients[target_client_id]['socket'] - return self._send_message(client_socket, message) - else: - print(f"✗ 目标客户端不存在: {target_client_id}") - return False - else: - # 广播给所有客户端 - success_count = 0 - for client_id, client_data in self.server_clients.items(): - try: - if self._send_message(client_data['socket'], message): - success_count += 1 - except Exception as e: - print(f"✗ 向客户端 {client_id} 发送数据失败: {e}") - - return success_count > 0 - - # 客户端模式:发送给服务器 - elif self.is_client_connected: - return self._send_message(self.client_socket, message) - else: - print("✗ 未连接到服务器") - return False - - except Exception as e: - print(f"✗ 同步数据发送失败: {e}") - self.network_stats['network_errors'] += 1 - return False - - def _send_message(self, sock: socket.socket, message: Dict[str, Any]) -> bool: - """ - 发送消息 - - Args: - sock: 套接字 - message: 消息数据 - - Returns: - 是否发送成功 - """ - try: - # 序列化消息 - message_json = json.dumps(message, ensure_ascii=False) - message_bytes = message_json.encode('utf-8') - - # 添加长度前缀 - message_length = len(message_bytes) - length_prefix = struct.pack('!I', message_length) - - # 发送数据 - sock.sendall(length_prefix + message_bytes) - - # 更新统计信息 - self.network_stats['packets_sent'] += 1 - self.network_stats['data_sent'] += message_length - - if self.is_server_running: - # 更新服务器统计 - pass - elif self.is_client_connected: - self.connection_state['bytes_sent'] += message_length - self.connection_state['packets_sent'] += 1 - - return True - - except Exception as e: - print(f"✗ 消息发送失败: {e}") - self.network_stats['network_errors'] += 1 - return False - - def _receive_message(self, sock: socket.socket) -> Optional[Dict[str, Any]]: - """ - 接收消息 - - Args: - sock: 套接字 - - Returns: - 消息数据或None - """ - try: - # 接收长度前缀 - length_prefix = self._receive_all(sock, 4) - if not length_prefix: - return None - - message_length = struct.unpack('!I', length_prefix)[0] - - # 接收消息数据 - message_bytes = self._receive_all(sock, message_length) - if not message_bytes: - return None - - # 反序列化消息 - message_json = message_bytes.decode('utf-8') - message = json.loads(message_json) - - # 更新统计信息 - self.network_stats['packets_received'] += 1 - self.network_stats['data_received'] += message_length - - if self.is_server_running: - # 更新服务器统计 - pass - elif self.is_client_connected: - self.connection_state['bytes_received'] += message_length - self.connection_state['packets_received'] += 1 - - return message - - except Exception as e: - print(f"✗ 消息接收失败: {e}") - self.network_stats['network_errors'] += 1 - return None - - def _receive_all(self, sock: socket.socket, length: int) -> Optional[bytes]: - """ - 接收指定长度的数据 - - Args: - sock: 套接字 - length: 数据长度 - - Returns: - 接收到的数据或None - """ - try: - data = b'' - while len(data) < length: - packet = sock.recv(length - len(data)) - if not packet: - return None - data += packet - return data - except Exception as e: - print(f"✗ 数据接收失败: {e}") - return None - - def _send_heartbeat(self): - """发送心跳包""" - try: - heartbeat_message = { - 'type': 'heartbeat', - 'data': { - 'timestamp': time.time() - } - } - - # 服务器模式:广播心跳包 - if self.is_server_running: - for client_id, client_data in self.server_clients.items(): - try: - self._send_message(client_data['socket'], heartbeat_message) - except Exception as e: - print(f"✗ 向客户端 {client_id} 发送心跳包失败: {e}") - - # 客户端模式:发送心跳包到服务器 - elif self.is_client_connected: - self._send_message(self.client_socket, heartbeat_message) - - except Exception as e: - print(f"✗ 心跳包发送失败: {e}") - - def get_server_clients(self) -> Dict[str, Any]: - """ - 获取服务器客户端列表 - - Returns: - 客户端列表 - """ - return self.server_clients.copy() - - def get_connection_state(self) -> Dict[str, Any]: - """ - 获取连接状态 - - Returns: - 连接状态字典 - """ - return self.connection_state.copy() - - def set_network_config(self, config: Dict[str, Any]) -> bool: - """ - 设置网络配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.network_config.update(config) - print(f"✓ 网络配置已更新: {self.network_config}") - return True - except Exception as e: - print(f"✗ 网络配置设置失败: {e}") - return False - - def get_network_config(self) -> Dict[str, Any]: - """ - 获取网络配置 - - Returns: - 配置字典 - """ - return self.network_config.copy() - - def get_network_stats(self) -> Dict[str, Any]: - """ - 获取网络统计信息 - - Returns: - 统计信息字典 - """ - return self.network_stats.copy() - - def reset_network_stats(self): - """重置网络统计信息""" - try: - self.network_stats = { - 'connections_accepted': 0, - 'connections_rejected': 0, - 'connections_lost': 0, - 'data_sent': 0, - 'data_received': 0, - 'packets_sent': 0, - 'packets_received': 0, - 'network_errors': 0 - } - print("✓ 网络统计信息已重置") - except Exception as e: - print(f"✗ 网络统计信息重置失败: {e}") - - def _trigger_network_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发网络回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.network_callbacks: - for callback in self.network_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 网络回调执行失败: {e}") - except Exception as e: - print(f"✗ 网络回调触发失败: {e}") - - def register_network_callback(self, callback_type: str, callback: callable): - """ - 注册网络回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.network_callbacks: - self.network_callbacks[callback_type].append(callback) - print(f"✓ 网络回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 网络回调注册失败: {e}") - - def unregister_network_callback(self, callback_type: str, callback: callable): - """ - 注销网络回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.network_callbacks: - if callback in self.network_callbacks[callback_type]: - self.network_callbacks[callback_type].remove(callback) - print(f"✓ 网络回调已注销: {callback_type}") - except Exception as e: +""" +网络管理器模块 +负责处理网络连接和数据传输 +""" + +import time +import socket +import threading +from typing import Dict, Any, List, Optional +import json +import struct + +class NetworkManager: + """ + 网络管理器 + 负责处理网络连接和数据传输 + """ + + def __init__(self, plugin): + """ + 初始化网络管理器 + + Args: + plugin: 网络同步插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 网络配置 + self.network_config = { + 'server_host': 'localhost', + 'server_port': 7777, + 'max_connections': 32, + 'buffer_size': 4096, + 'timeout': 30.0, + 'heartbeat_interval': 5.0, + 'enable_compression': True, + 'enable_encryption': False, + 'protocol_version': '1.0' + } + + # 服务器相关 + self.server_socket = None + self.server_thread = None + self.is_server_running = False + self.server_clients = {} # 客户端连接 + + # 客户端相关 + self.client_socket = None + self.client_thread = None + self.is_client_connected = False + self.client_id = None + + # 连接状态 + self.connection_state = { + 'is_connected': False, + 'connection_time': 0.0, + 'last_heartbeat': 0.0, + 'bytes_sent': 0, + 'bytes_received': 0, + 'packets_sent': 0, + 'packets_received': 0 + } + + # 网络统计 + self.network_stats = { + 'connections_accepted': 0, + 'connections_rejected': 0, + 'connections_lost': 0, + 'data_sent': 0, + 'data_received': 0, + 'packets_sent': 0, + 'packets_received': 0, + 'network_errors': 0 + } + + # 消息队列 + self.message_queue = [] + self.message_lock = threading.Lock() + + # 回调函数 + self.network_callbacks = { + 'client_connected': [], + 'client_disconnected': [], + 'data_received': [], + 'network_error': [], + 'server_started': [], + 'server_stopped': [] + } + + # 时间戳记录 + self.last_heartbeat_time = 0.0 + self.last_network_update = 0.0 + + print("✓ 网络管理器已创建") + + def initialize(self) -> bool: + """ + 初始化网络管理器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 网络管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 网络管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用网络管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 网络管理器未初始化") + return False + + self.enabled = True + print("✓ 网络管理器已启用") + return True + + except Exception as e: + print(f"✗ 网络管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用网络管理器""" + try: + self.enabled = False + + # 停止服务器 + if self.is_server_running: + self.stop_server() + + # 断开客户端 + if self.is_client_connected: + self.disconnect_client() + + print("✓ 网络管理器已禁用") + + except Exception as e: + print(f"✗ 网络管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理网络管理器资源""" + try: + self.disable() + self.network_callbacks.clear() + self.server_clients.clear() + self.initialized = False + print("✓ 网络管理器资源已清理") + + except Exception as e: + print(f"✗ 网络管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新网络管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.last_network_update = current_time + + # 处理消息队列 + self._process_message_queue() + + # 发送心跳包 + if current_time - self.last_heartbeat_time >= self.network_config['heartbeat_interval']: + self._send_heartbeat() + self.last_heartbeat_time = current_time + + except Exception as e: + print(f"✗ 网络管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _process_message_queue(self): + """处理消息队列""" + try: + with self.message_lock: + while self.message_queue: + message = self.message_queue.pop(0) + self._handle_message(message) + except Exception as e: + print(f"✗ 消息队列处理失败: {e}") + + def _handle_message(self, message: Dict[str, Any]): + """ + 处理消息 + + Args: + message: 消息数据 + """ + try: + message_type = message.get('type') + message_data = message.get('data') + client_id = message.get('client_id') + + # 触发数据接收回调 + self._trigger_network_callback('data_received', { + 'type': message_type, + 'data': message_data, + 'client_id': client_id + }) + + # 根据消息类型处理 + if message_type == 'sync_data': + self._handle_sync_data(message_data, client_id) + elif message_type == 'heartbeat': + self._handle_heartbeat(message_data, client_id) + elif message_type == 'connect': + self._handle_connect(message_data, client_id) + elif message_type == 'disconnect': + self._handle_disconnect(message_data, client_id) + + except Exception as e: + print(f"✗ 消息处理失败: {e}") + self.network_stats['network_errors'] += 1 + + def _handle_sync_data(self, data: Dict[str, Any], client_id: str): + """ + 处理同步数据 + + Args: + data: 同步数据 + client_id: 客户端ID + """ + try: + # 将同步数据传递给对象同步模块 + if self.plugin.object_sync: + for obj_id, obj_data in data.items(): + self.plugin.object_sync.receive_sync_data(obj_id, obj_data, client_id) + except Exception as e: + print(f"✗ 同步数据处理失败: {e}") + + def _handle_heartbeat(self, data: Dict[str, Any], client_id: str): + """ + 处理心跳包 + + Args: + data: 心跳数据 + client_id: 客户端ID + """ + try: + # 更新客户端最后心跳时间 + if client_id in self.server_clients: + self.server_clients[client_id]['last_heartbeat'] = time.time() + except Exception as e: + print(f"✗ 心跳包处理失败: {e}") + + def _handle_connect(self, data: Dict[str, Any], client_id: str): + """ + 处理连接消息 + + Args: + data: 连接数据 + client_id: 客户端ID + """ + try: + # 触发客户端连接回调 + self._trigger_network_callback('client_connected', { + 'client_id': client_id, + 'data': data + }) + except Exception as e: + print(f"✗ 连接消息处理失败: {e}") + + def _handle_disconnect(self, data: Dict[str, Any], client_id: str): + """ + 处理断开连接消息 + + Args: + data: 断开连接数据 + client_id: 客户端ID + """ + try: + # 从客户端列表中移除 + if client_id in self.server_clients: + del self.server_clients[client_id] + + # 触发客户端断开连接回调 + self._trigger_network_callback('client_disconnected', { + 'client_id': client_id, + 'data': data + }) + except Exception as e: + print(f"✗ 断开连接消息处理失败: {e}") + + def start_server(self, port: int = None) -> bool: + """ + 启动服务器 + + Args: + port: 服务器端口 + + Returns: + 是否启动成功 + """ + try: + if self.is_server_running: + print("✗ 服务器已在运行") + return False + + if port is None: + port = self.network_config['server_port'] + + # 创建服务器套接字 + self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.server_socket.bind((self.network_config['server_host'], port)) + self.server_socket.listen(self.network_config['max_connections']) + self.server_socket.settimeout(1.0) # 设置超时以便可以检查运行状态 + + self.is_server_running = True + self.network_config['server_port'] = port + + # 启动服务器线程 + self.server_thread = threading.Thread(target=self._server_loop, daemon=True) + self.server_thread.start() + + # 触发服务器启动回调 + self._trigger_network_callback('server_started', { + 'host': self.network_config['server_host'], + 'port': port + }) + + print(f"✓ 服务器已启动: {self.network_config['server_host']}:{port}") + return True + + except Exception as e: + print(f"✗ 服务器启动失败: {e}") + import traceback + traceback.print_exc() + return False + + def stop_server(self): + """停止服务器""" + try: + self.is_server_running = False + + # 关闭所有客户端连接 + for client_id, client_data in list(self.server_clients.items()): + try: + client_data['socket'].close() + except Exception as e: + pass + + self.server_clients.clear() + + # 关闭服务器套接字 + if self.server_socket: + self.server_socket.close() + self.server_socket = None + + # 等待服务器线程结束 + if self.server_thread and self.server_thread.is_alive(): + self.server_thread.join(timeout=2.0) + + # 触发服务器停止回调 + self._trigger_network_callback('server_stopped', {}) + + print("✓ 服务器已停止") + + except Exception as e: + print(f"✗ 服务器停止失败: {e}") + + def _server_loop(self): + """服务器主循环""" + try: + print("✓ 服务器主循环已启动") + + while self.is_server_running: + try: + # 接受客户端连接 + client_socket, client_address = self.server_socket.accept() + + # 创建客户端线程 + client_thread = threading.Thread( + target=self._handle_client, + args=(client_socket, client_address), + daemon=True + ) + client_thread.start() + + # 分配客户端ID + client_id = f"client_{int(time.time() * 1000)}" + + # 添加到客户端列表 + self.server_clients[client_id] = { + 'socket': client_socket, + 'address': client_address, + 'thread': client_thread, + 'connect_time': time.time(), + 'last_heartbeat': time.time(), + 'bytes_sent': 0, + 'bytes_received': 0 + } + + self.network_stats['connections_accepted'] += 1 + print(f"✓ 客户端已连接: {client_address} ({client_id})") + + except socket.timeout: + # 正常超时,继续循环 + continue + except Exception as e: + if self.is_server_running: + print(f"✗ 接受客户端连接失败: {e}") + self.network_stats['network_errors'] += 1 + + except Exception as e: + print(f"✗ 服务器主循环失败: {e}") + finally: + print("✓ 服务器主循环已结束") + + def _handle_client(self, client_socket: socket.socket, client_address: tuple): + """ + 处理客户端连接 + + Args: + client_socket: 客户端套接字 + client_address: 客户端地址 + """ + client_id = None + try: + # 为客户端分配ID + client_id = f"client_{hash(client_address)}_{int(time.time() * 1000)}" + + print(f"✓ 开始处理客户端: {client_address} ({client_id})") + + # 发送连接确认消息 + connect_message = { + 'type': 'connect_ack', + 'data': { + 'client_id': client_id, + 'server_time': time.time(), + 'protocol_version': self.network_config['protocol_version'] + } + } + self._send_message(client_socket, connect_message) + + # 主接收循环 + while self.is_server_running: + try: + # 接收消息 + message = self._receive_message(client_socket) + if message is None: + # 客户端断开连接 + break + + # 添加客户端ID到消息 + message['client_id'] = client_id + + # 添加到消息队列 + with self.message_lock: + self.message_queue.append(message) + + # 更新统计信息 + self.network_stats['packets_received'] += 1 + self.network_stats['data_received'] += len(str(message).encode('utf-8')) + + except socket.timeout: + # 检查心跳超时 + if client_id in self.server_clients: + last_heartbeat = self.server_clients[client_id]['last_heartbeat'] + if time.time() - last_heartbeat > self.network_config['timeout']: + print(f"✗ 客户端心跳超时: {client_address} ({client_id})") + break + continue + except Exception as e: + print(f"✗ 接收客户端消息失败: {e}") + break + + except Exception as e: + print(f"✗ 处理客户端连接失败: {e}") + finally: + # 清理客户端连接 + if client_id and client_id in self.server_clients: + del self.server_clients[client_id] + + try: + client_socket.close() + except Exception as e: + pass + + self.network_stats['connections_lost'] += 1 + print(f"✓ 客户端连接已断开: {client_address} ({client_id})") + + # 触发客户端断开连接回调 + self._trigger_network_callback('client_disconnected', { + 'client_id': client_id, + 'address': client_address + }) + + def connect_to_server(self, host: str = None, port: int = None) -> bool: + """ + 连接到服务器 + + Args: + host: 服务器地址 + port: 服务器端口 + + Returns: + 是否连接成功 + """ + try: + if self.is_client_connected: + print("✗ 客户端已连接到服务器") + return False + + if host is None: + host = self.network_config['server_host'] + + if port is None: + port = self.network_config['server_port'] + + # 创建客户端套接字 + self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.client_socket.settimeout(self.network_config['timeout']) + + # 连接服务器 + self.client_socket.connect((host, port)) + + # 启动客户端接收线程 + self.client_thread = threading.Thread(target=self._client_receive_loop, daemon=True) + self.client_thread.start() + + # 等待连接确认 + connect_ack = self._receive_message(self.client_socket) + if connect_ack and connect_ack.get('type') == 'connect_ack': + self.client_id = connect_ack['data']['client_id'] + self.is_client_connected = True + self.connection_state['is_connected'] = True + self.connection_state['connection_time'] = time.time() + + print(f"✓ 已连接到服务器: {host}:{port} (Client ID: {self.client_id})") + return True + else: + print("✗ 服务器连接确认失败") + self.disconnect_client() + return False + + except Exception as e: + print(f"✗ 连接服务器失败: {e}") + import traceback + traceback.print_exc() + self.disconnect_client() + return False + + def disconnect_client(self): + """断开客户端连接""" + try: + self.is_client_connected = False + self.connection_state['is_connected'] = False + + if self.client_socket: + try: + # 发送断开连接消息 + disconnect_message = { + 'type': 'disconnect', + 'data': {} + } + self._send_message(self.client_socket, disconnect_message) + except Exception as e: + pass + + self.client_socket.close() + self.client_socket = None + + # 等待客户端线程结束 + if self.client_thread and self.client_thread.is_alive(): + self.client_thread.join(timeout=2.0) + + self.client_id = None + print("✓ 客户端已断开连接") + + except Exception as e: + print(f"✗ 客户端断开连接失败: {e}") + + def _client_receive_loop(self): + """客户端接收循环""" + try: + while self.is_client_connected and self.client_socket: + try: + # 接收消息 + message = self._receive_message(self.client_socket) + if message is None: + # 服务器断开连接 + break + + # 添加到消息队列 + with self.message_lock: + self.message_queue.append(message) + + # 更新统计信息 + self.network_stats['packets_received'] += 1 + self.network_stats['data_received'] += len(str(message).encode('utf-8')) + + except socket.timeout: + continue + except Exception as e: + if self.is_client_connected: + print(f"✗ 接收服务器消息失败: {e}") + break + + except Exception as e: + print(f"✗ 客户端接收循环失败: {e}") + finally: + if self.is_client_connected: + self.disconnect_client() + + def send_sync_data(self, data: Dict[str, Any], target_client_id: str = None) -> bool: + """ + 发送同步数据 + + Args: + data: 同步数据 + target_client_id: 目标客户端ID(None表示广播给所有客户端) + + Returns: + 是否发送成功 + """ + try: + if not self.enabled: + print("✗ 网络管理器未启用") + return False + + message = { + 'type': 'sync_data', + 'data': data + } + + # 服务器模式:广播给客户端 + if self.is_server_running: + if target_client_id: + # 发送给特定客户端 + if target_client_id in self.server_clients: + client_socket = self.server_clients[target_client_id]['socket'] + return self._send_message(client_socket, message) + else: + print(f"✗ 目标客户端不存在: {target_client_id}") + return False + else: + # 广播给所有客户端 + success_count = 0 + for client_id, client_data in self.server_clients.items(): + try: + if self._send_message(client_data['socket'], message): + success_count += 1 + except Exception as e: + print(f"✗ 向客户端 {client_id} 发送数据失败: {e}") + + return success_count > 0 + + # 客户端模式:发送给服务器 + elif self.is_client_connected: + return self._send_message(self.client_socket, message) + else: + print("✗ 未连接到服务器") + return False + + except Exception as e: + print(f"✗ 同步数据发送失败: {e}") + self.network_stats['network_errors'] += 1 + return False + + def _send_message(self, sock: socket.socket, message: Dict[str, Any]) -> bool: + """ + 发送消息 + + Args: + sock: 套接字 + message: 消息数据 + + Returns: + 是否发送成功 + """ + try: + # 序列化消息 + message_json = json.dumps(message, ensure_ascii=False) + message_bytes = message_json.encode('utf-8') + + # 添加长度前缀 + message_length = len(message_bytes) + length_prefix = struct.pack('!I', message_length) + + # 发送数据 + sock.sendall(length_prefix + message_bytes) + + # 更新统计信息 + self.network_stats['packets_sent'] += 1 + self.network_stats['data_sent'] += message_length + + if self.is_server_running: + # 更新服务器统计 + pass + elif self.is_client_connected: + self.connection_state['bytes_sent'] += message_length + self.connection_state['packets_sent'] += 1 + + return True + + except Exception as e: + print(f"✗ 消息发送失败: {e}") + self.network_stats['network_errors'] += 1 + return False + + def _receive_message(self, sock: socket.socket) -> Optional[Dict[str, Any]]: + """ + 接收消息 + + Args: + sock: 套接字 + + Returns: + 消息数据或None + """ + try: + # 接收长度前缀 + length_prefix = self._receive_all(sock, 4) + if not length_prefix: + return None + + message_length = struct.unpack('!I', length_prefix)[0] + + # 接收消息数据 + message_bytes = self._receive_all(sock, message_length) + if not message_bytes: + return None + + # 反序列化消息 + message_json = message_bytes.decode('utf-8') + message = json.loads(message_json) + + # 更新统计信息 + self.network_stats['packets_received'] += 1 + self.network_stats['data_received'] += message_length + + if self.is_server_running: + # 更新服务器统计 + pass + elif self.is_client_connected: + self.connection_state['bytes_received'] += message_length + self.connection_state['packets_received'] += 1 + + return message + + except Exception as e: + print(f"✗ 消息接收失败: {e}") + self.network_stats['network_errors'] += 1 + return None + + def _receive_all(self, sock: socket.socket, length: int) -> Optional[bytes]: + """ + 接收指定长度的数据 + + Args: + sock: 套接字 + length: 数据长度 + + Returns: + 接收到的数据或None + """ + try: + data = b'' + while len(data) < length: + packet = sock.recv(length - len(data)) + if not packet: + return None + data += packet + return data + except Exception as e: + print(f"✗ 数据接收失败: {e}") + return None + + def _send_heartbeat(self): + """发送心跳包""" + try: + heartbeat_message = { + 'type': 'heartbeat', + 'data': { + 'timestamp': time.time() + } + } + + # 服务器模式:广播心跳包 + if self.is_server_running: + for client_id, client_data in self.server_clients.items(): + try: + self._send_message(client_data['socket'], heartbeat_message) + except Exception as e: + print(f"✗ 向客户端 {client_id} 发送心跳包失败: {e}") + + # 客户端模式:发送心跳包到服务器 + elif self.is_client_connected: + self._send_message(self.client_socket, heartbeat_message) + + except Exception as e: + print(f"✗ 心跳包发送失败: {e}") + + def get_server_clients(self) -> Dict[str, Any]: + """ + 获取服务器客户端列表 + + Returns: + 客户端列表 + """ + return self.server_clients.copy() + + def get_connection_state(self) -> Dict[str, Any]: + """ + 获取连接状态 + + Returns: + 连接状态字典 + """ + return self.connection_state.copy() + + def set_network_config(self, config: Dict[str, Any]) -> bool: + """ + 设置网络配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.network_config.update(config) + print(f"✓ 网络配置已更新: {self.network_config}") + return True + except Exception as e: + print(f"✗ 网络配置设置失败: {e}") + return False + + def get_network_config(self) -> Dict[str, Any]: + """ + 获取网络配置 + + Returns: + 配置字典 + """ + return self.network_config.copy() + + def get_network_stats(self) -> Dict[str, Any]: + """ + 获取网络统计信息 + + Returns: + 统计信息字典 + """ + return self.network_stats.copy() + + def reset_network_stats(self): + """重置网络统计信息""" + try: + self.network_stats = { + 'connections_accepted': 0, + 'connections_rejected': 0, + 'connections_lost': 0, + 'data_sent': 0, + 'data_received': 0, + 'packets_sent': 0, + 'packets_received': 0, + 'network_errors': 0 + } + print("✓ 网络统计信息已重置") + except Exception as e: + print(f"✗ 网络统计信息重置失败: {e}") + + def _trigger_network_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发网络回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.network_callbacks: + for callback in self.network_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 网络回调执行失败: {e}") + except Exception as e: + print(f"✗ 网络回调触发失败: {e}") + + def register_network_callback(self, callback_type: str, callback: callable): + """ + 注册网络回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.network_callbacks: + self.network_callbacks[callback_type].append(callback) + print(f"✓ 网络回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 网络回调注册失败: {e}") + + def unregister_network_callback(self, callback_type: str, callback: callable): + """ + 注销网络回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.network_callbacks: + if callback in self.network_callbacks[callback_type]: + self.network_callbacks[callback_type].remove(callback) + print(f"✓ 网络回调已注销: {callback_type}") + except Exception as e: print(f"✗ 网络回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/network_sync/plugin.py b/plugins/user/network_sync/plugin.py index 481935e6..c4eaa53a 100644 --- a/plugins/user/network_sync/plugin.py +++ b/plugins/user/network_sync/plugin.py @@ -1,397 +1,397 @@ -""" -网络同步插件 -为EG引擎提供完整的网络同步功能 -""" - -from .core.sync_manager import SyncManager -from .network.network_manager import NetworkManager -from .protocol.protocol_handler import ProtocolHandler -from .sync.object_sync import ObjectSync -from .connection.connection_manager import ConnectionManager -from .serialization.data_serializer import DataSerializer -from .clock.clock_sync import ClockSync -from .prediction.prediction_manager import PredictionManager -from .config.sync_config import SyncConfig -from .editor.sync_editor import SyncEditor -from .events.event_handler import NetworkEventHandler -from .utils.network_utils import NetworkUtils -from .encryption.encryption_manager import EncryptionManager - -class NetworkSyncPlugin: - """ - 网络同步插件主类 - 整合所有网络同步功能模块 - """ - - def __init__(self, engine): - """ - 初始化网络同步插件 - - Args: - engine: EG引擎实例 - """ - self.engine = engine - self.name = "NetworkSync" - self.version = "1.0.0" - self.author = "EG Plugin System" - - # 功能模块 - self.sync_manager = None - self.network_manager = None - self.protocol_handler = None - self.object_sync = None - self.connection_manager = None - self.data_serializer = None - self.clock_sync = None - self.prediction_manager = None - self.sync_config = None - self.sync_editor = None - self.event_handler = None - self.network_utils = None - self.encryption_manager = None - - # 插件状态 - self.enabled = False - self.initialized = False - self.is_server = False - self.is_client = False - - # 性能统计 - self.stats = { - 'bytes_sent': 0, - 'bytes_received': 0, - 'packets_sent': 0, - 'packets_received': 0, - 'sync_objects': 0, - 'connections': 0, - 'sync_errors': 0, - 'latency': 0.0 - } - - print(f"✓ 网络同步插件 v{self.version} 已创建") - - def initialize(self) -> bool: - """ - 初始化插件 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化网络同步插件...") - - # 初始化核心组件 - self.sync_manager = SyncManager(self) - self.network_manager = NetworkManager(self) - self.protocol_handler = ProtocolHandler(self) - self.object_sync = ObjectSync(self) - self.connection_manager = ConnectionManager(self) - self.data_serializer = DataSerializer(self) - self.clock_sync = ClockSync(self) - self.prediction_manager = PredictionManager(self) - self.sync_config = SyncConfig(self) - self.sync_editor = SyncEditor(self) - self.event_handler = NetworkEventHandler(self) - self.network_utils = NetworkUtils(self) - self.encryption_manager = EncryptionManager(self) - - # 初始化各个模块 - modules = [ - self.sync_manager, - self.network_manager, - self.protocol_handler, - self.object_sync, - self.connection_manager, - self.data_serializer, - self.clock_sync, - self.prediction_manager, - self.sync_config, - self.sync_editor, - self.event_handler, - self.network_utils, - self.encryption_manager - ] - - for module in modules: - if module and not module.initialize(): - print(f"✗ 模块初始化失败: {module.__class__.__name__}") - return False - - self.initialized = True - print("✓ 网络同步插件初始化完成") - return True - - except Exception as e: - print(f"✗ 网络同步插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 插件未初始化") - return False - - print("正在启用网络同步插件...") - - # 启用各个模块 - modules = [ - self.sync_manager, - self.network_manager, - self.protocol_handler, - self.object_sync, - self.connection_manager, - self.data_serializer, - self.clock_sync, - self.prediction_manager, - self.sync_config, - self.sync_editor, - self.event_handler, - self.network_utils, - self.encryption_manager - ] - - for module in modules: - if module and not module.enable(): - print(f"✗ 模块启用失败: {module.__class__.__name__}") - return False - - self.enabled = True - print("✓ 网络同步插件已启用") - return True - - except Exception as e: - print(f"✗ 网络同步插件启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用插件""" - try: - if not self.enabled: - return - - print("正在禁用网络同步插件...") - - # 禁用各个模块 - modules = [ - self.sync_manager, - self.network_manager, - self.protocol_handler, - self.object_sync, - self.connection_manager, - self.data_serializer, - self.clock_sync, - self.prediction_manager, - self.sync_config, - self.sync_editor, - self.event_handler, - self.network_utils, - self.encryption_manager - ] - - for module in modules: - if module: - module.disable() - - self.enabled = False - print("✓ 网络同步插件已禁用") - - except Exception as e: - print(f"✗ 网络同步插件禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理插件资源""" - try: - print("正在清理网络同步插件资源...") - - # 清理各个模块 - modules = [ - self.sync_manager, - self.network_manager, - self.protocol_handler, - self.object_sync, - self.connection_manager, - self.data_serializer, - self.clock_sync, - self.prediction_manager, - self.sync_config, - self.sync_editor, - self.event_handler, - self.network_utils, - self.encryption_manager - ] - - for module in modules: - if module: - module.finalize() - - self.initialized = False - print("✓ 网络同步插件资源已清理") - - except Exception as e: - print(f"✗ 网络同步插件资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新插件状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - # 更新各个模块 - modules = [ - self.sync_manager, - self.network_manager, - self.protocol_handler, - self.object_sync, - self.connection_manager, - self.data_serializer, - self.clock_sync, - self.prediction_manager, - self.sync_config, - self.sync_editor, - self.event_handler, - self.network_utils, - self.encryption_manager - ] - - for module in modules: - if module: - module.update(dt) - - except Exception as e: - print(f"✗ 网络同步插件更新失败: {e}") - import traceback - traceback.print_exc() - - def start_server(self, port: int = 7777) -> bool: - """ - 启动服务器 - - Args: - port: 服务器端口 - - Returns: - 是否启动成功 - """ - try: - if not self.enabled: - print("✗ 插件未启用") - return False - - if self.network_manager.start_server(port): - self.is_server = True - self.is_client = False - print(f"✓ 服务器已启动,端口: {port}") - return True - else: - print("✗ 服务器启动失败") - return False - - except Exception as e: - print(f"✗ 服务器启动异常: {e}") - return False - - def connect_to_server(self, host: str, port: int = 7777) -> bool: - """ - 连接到服务器 - - Args: - host: 服务器地址 - port: 服务器端口 - - Returns: - 是否连接成功 - """ - try: - if not self.enabled: - print("✗ 插件未启用") - return False - - if self.connection_manager.connect_to_server(host, port): - self.is_client = True - self.is_server = False - print(f"✓ 已连接到服务器 {host}:{port}") - return True - else: - print("✗ 连接服务器失败") - return False - - except Exception as e: - print(f"✗ 连接服务器异常: {e}") - return False - - def disconnect(self): - """断开连接""" - try: - if self.is_server: - self.network_manager.stop_server() - self.is_server = False - elif self.is_client: - self.connection_manager.disconnect() - self.is_client = False - - print("✓ 已断开连接") - except Exception as e: - print(f"✗ 断开连接失败: {e}") - - def sync_object(self, obj_id: str, obj_data: dict) -> bool: - """ - 同步对象 - - Args: - obj_id: 对象ID - obj_data: 对象数据 - - Returns: - 是否同步成功 - """ - try: - if not self.enabled: - print("✗ 插件未启用") - return False - - return self.object_sync.sync_object(obj_id, obj_data) - except Exception as e: - print(f"✗ 对象同步失败: {e}") - return False - - def get_stats(self) -> dict: - """ - 获取插件统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'bytes_sent': 0, - 'bytes_received': 0, - 'packets_sent': 0, - 'packets_received': 0, - 'sync_objects': 0, - 'connections': 0, - 'sync_errors': 0, - 'latency': 0.0 - } +""" +网络同步插件 +为EG引擎提供完整的网络同步功能 +""" + +from .core.sync_manager import SyncManager +from .network.network_manager import NetworkManager +from .protocol.protocol_handler import ProtocolHandler +from .sync.object_sync import ObjectSync +from .connection.connection_manager import ConnectionManager +from .serialization.data_serializer import DataSerializer +from .clock.clock_sync import ClockSync +from .prediction.prediction_manager import PredictionManager +from .config.sync_config import SyncConfig +from .editor.sync_editor import SyncEditor +from .events.event_handler import NetworkEventHandler +from .utils.network_utils import NetworkUtils +from .encryption.encryption_manager import EncryptionManager + +class NetworkSyncPlugin: + """ + 网络同步插件主类 + 整合所有网络同步功能模块 + """ + + def __init__(self, engine): + """ + 初始化网络同步插件 + + Args: + engine: EG引擎实例 + """ + self.engine = engine + self.name = "NetworkSync" + self.version = "1.0.0" + self.author = "EG Plugin System" + + # 功能模块 + self.sync_manager = None + self.network_manager = None + self.protocol_handler = None + self.object_sync = None + self.connection_manager = None + self.data_serializer = None + self.clock_sync = None + self.prediction_manager = None + self.sync_config = None + self.sync_editor = None + self.event_handler = None + self.network_utils = None + self.encryption_manager = None + + # 插件状态 + self.enabled = False + self.initialized = False + self.is_server = False + self.is_client = False + + # 性能统计 + self.stats = { + 'bytes_sent': 0, + 'bytes_received': 0, + 'packets_sent': 0, + 'packets_received': 0, + 'sync_objects': 0, + 'connections': 0, + 'sync_errors': 0, + 'latency': 0.0 + } + + print(f"✓ 网络同步插件 v{self.version} 已创建") + + def initialize(self) -> bool: + """ + 初始化插件 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化网络同步插件...") + + # 初始化核心组件 + self.sync_manager = SyncManager(self) + self.network_manager = NetworkManager(self) + self.protocol_handler = ProtocolHandler(self) + self.object_sync = ObjectSync(self) + self.connection_manager = ConnectionManager(self) + self.data_serializer = DataSerializer(self) + self.clock_sync = ClockSync(self) + self.prediction_manager = PredictionManager(self) + self.sync_config = SyncConfig(self) + self.sync_editor = SyncEditor(self) + self.event_handler = NetworkEventHandler(self) + self.network_utils = NetworkUtils(self) + self.encryption_manager = EncryptionManager(self) + + # 初始化各个模块 + modules = [ + self.sync_manager, + self.network_manager, + self.protocol_handler, + self.object_sync, + self.connection_manager, + self.data_serializer, + self.clock_sync, + self.prediction_manager, + self.sync_config, + self.sync_editor, + self.event_handler, + self.network_utils, + self.encryption_manager + ] + + for module in modules: + if module and not module.initialize(): + print(f"✗ 模块初始化失败: {module.__class__.__name__}") + return False + + self.initialized = True + print("✓ 网络同步插件初始化完成") + return True + + except Exception as e: + print(f"✗ 网络同步插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 插件未初始化") + return False + + print("正在启用网络同步插件...") + + # 启用各个模块 + modules = [ + self.sync_manager, + self.network_manager, + self.protocol_handler, + self.object_sync, + self.connection_manager, + self.data_serializer, + self.clock_sync, + self.prediction_manager, + self.sync_config, + self.sync_editor, + self.event_handler, + self.network_utils, + self.encryption_manager + ] + + for module in modules: + if module and not module.enable(): + print(f"✗ 模块启用失败: {module.__class__.__name__}") + return False + + self.enabled = True + print("✓ 网络同步插件已启用") + return True + + except Exception as e: + print(f"✗ 网络同步插件启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用插件""" + try: + if not self.enabled: + return + + print("正在禁用网络同步插件...") + + # 禁用各个模块 + modules = [ + self.sync_manager, + self.network_manager, + self.protocol_handler, + self.object_sync, + self.connection_manager, + self.data_serializer, + self.clock_sync, + self.prediction_manager, + self.sync_config, + self.sync_editor, + self.event_handler, + self.network_utils, + self.encryption_manager + ] + + for module in modules: + if module: + module.disable() + + self.enabled = False + print("✓ 网络同步插件已禁用") + + except Exception as e: + print(f"✗ 网络同步插件禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理插件资源""" + try: + print("正在清理网络同步插件资源...") + + # 清理各个模块 + modules = [ + self.sync_manager, + self.network_manager, + self.protocol_handler, + self.object_sync, + self.connection_manager, + self.data_serializer, + self.clock_sync, + self.prediction_manager, + self.sync_config, + self.sync_editor, + self.event_handler, + self.network_utils, + self.encryption_manager + ] + + for module in modules: + if module: + module.finalize() + + self.initialized = False + print("✓ 网络同步插件资源已清理") + + except Exception as e: + print(f"✗ 网络同步插件资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新插件状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + # 更新各个模块 + modules = [ + self.sync_manager, + self.network_manager, + self.protocol_handler, + self.object_sync, + self.connection_manager, + self.data_serializer, + self.clock_sync, + self.prediction_manager, + self.sync_config, + self.sync_editor, + self.event_handler, + self.network_utils, + self.encryption_manager + ] + + for module in modules: + if module: + module.update(dt) + + except Exception as e: + print(f"✗ 网络同步插件更新失败: {e}") + import traceback + traceback.print_exc() + + def start_server(self, port: int = 7777) -> bool: + """ + 启动服务器 + + Args: + port: 服务器端口 + + Returns: + 是否启动成功 + """ + try: + if not self.enabled: + print("✗ 插件未启用") + return False + + if self.network_manager.start_server(port): + self.is_server = True + self.is_client = False + print(f"✓ 服务器已启动,端口: {port}") + return True + else: + print("✗ 服务器启动失败") + return False + + except Exception as e: + print(f"✗ 服务器启动异常: {e}") + return False + + def connect_to_server(self, host: str, port: int = 7777) -> bool: + """ + 连接到服务器 + + Args: + host: 服务器地址 + port: 服务器端口 + + Returns: + 是否连接成功 + """ + try: + if not self.enabled: + print("✗ 插件未启用") + return False + + if self.connection_manager.connect_to_server(host, port): + self.is_client = True + self.is_server = False + print(f"✓ 已连接到服务器 {host}:{port}") + return True + else: + print("✗ 连接服务器失败") + return False + + except Exception as e: + print(f"✗ 连接服务器异常: {e}") + return False + + def disconnect(self): + """断开连接""" + try: + if self.is_server: + self.network_manager.stop_server() + self.is_server = False + elif self.is_client: + self.connection_manager.disconnect() + self.is_client = False + + print("✓ 已断开连接") + except Exception as e: + print(f"✗ 断开连接失败: {e}") + + def sync_object(self, obj_id: str, obj_data: dict) -> bool: + """ + 同步对象 + + Args: + obj_id: 对象ID + obj_data: 对象数据 + + Returns: + 是否同步成功 + """ + try: + if not self.enabled: + print("✗ 插件未启用") + return False + + return self.object_sync.sync_object(obj_id, obj_data) + except Exception as e: + print(f"✗ 对象同步失败: {e}") + return False + + def get_stats(self) -> dict: + """ + 获取插件统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'bytes_sent': 0, + 'bytes_received': 0, + 'packets_sent': 0, + 'packets_received': 0, + 'sync_objects': 0, + 'connections': 0, + 'sync_errors': 0, + 'latency': 0.0 + } print("✓ 网络同步统计信息已重置") \ No newline at end of file diff --git a/plugins/user/network_sync/prediction/prediction_manager.py b/plugins/user/network_sync/prediction/prediction_manager.py index af907ef9..90991813 100644 --- a/plugins/user/network_sync/prediction/prediction_manager.py +++ b/plugins/user/network_sync/prediction/prediction_manager.py @@ -1,791 +1,791 @@ -""" -预测管理模块 -负责处理网络延迟下的对象状态预测 -""" - -import time -from typing import Dict, Any, List, Optional -import threading -import copy - -class PredictionManager: - """ - 预测管理器 - 负责处理网络延迟下的对象状态预测 - """ - - def __init__(self, plugin): - """ - 初始化预测管理器 - - Args: - plugin: 网络同步插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 预测配置 - self.prediction_config = { - 'enable_prediction': True, - 'enable_interpolation': True, - 'enable_extrapolation': True, - 'max_prediction_time': 0.5, # 最大预测时间(秒) - 'prediction_smoothing': 0.1, # 预测平滑系数 - 'correction_speed': 5.0, # 状态校正速度 - 'velocity_damping': 0.95, # 速度阻尼 - 'position_threshold': 0.01, # 位置校正阈值 - 'rotation_threshold': 0.1, # 旋转校正阈值 - 'enable_snap_correction': True, # 启用快照校正 - 'snap_threshold': 1.0 # 快照阈值 - } - - # 预测对象管理 - self.predicted_objects = {} # 预测对象状态 - self.object_history = {} # 对象历史状态 - self.object_velocities = {} # 对象速度状态 - - # 预测状态 - self.prediction_state = { - 'last_prediction_time': 0.0, - 'predicted_objects_count': 0, - 'corrections_applied': 0, - 'snap_corrections': 0 - } - - # 预测统计 - self.prediction_stats = { - 'predictions_made': 0, - 'corrections_applied': 0, - 'snap_corrections': 0, - 'prediction_errors': 0, - 'average_error': 0.0, - 'max_error': 0.0 - } - - # 线程锁 - self.prediction_lock = threading.RLock() - - # 回调函数 - self.prediction_callbacks = { - 'prediction_updated': [], - 'correction_applied': [], - 'snap_correction': [], - 'prediction_error': [] - } - - # 时间戳记录 - self.last_prediction_update = 0.0 - self.last_correction_apply = 0.0 - - print("✓ 预测管理器已创建") - - def initialize(self) -> bool: - """ - 初始化预测管理器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 预测管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 预测管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用预测管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 预测管理器未初始化") - return False - - self.enabled = True - print("✓ 预测管理器已启用") - return True - - except Exception as e: - print(f"✗ 预测管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用预测管理器""" - try: - self.enabled = False - print("✓ 预测管理器已禁用") - - except Exception as e: - print(f"✗ 预测管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理预测管理器资源""" - try: - self.disable() - self.predicted_objects.clear() - self.object_history.clear() - self.object_velocities.clear() - self.prediction_callbacks.clear() - self.initialized = False - print("✓ 预测管理器资源已清理") - - except Exception as e: - print(f"✗ 预测管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新预测管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.last_prediction_update = current_time - - # 更新预测 - if self.prediction_config['enable_prediction']: - self._update_predictions(current_time, dt) - - except Exception as e: - print(f"✗ 预测管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_predictions(self, current_time: float, dt: float): - """ - 更新预测状态 - - Args: - current_time: 当前时间 - dt: 时间增量 - """ - try: - with self.prediction_lock: - for obj_id, pred_data in self.predicted_objects.items(): - # 获取服务器时间 - server_time = self.plugin.clock_sync.get_server_time() if self.plugin.clock_sync else current_time - - # 计算预测时间 - prediction_time = server_time + self.plugin.network_manager.get_connection_state().get('latency', 0.0) \ - if self.plugin.network_manager else server_time - - # 限制最大预测时间 - max_prediction_time = server_time + self.prediction_config['max_prediction_time'] - prediction_time = min(prediction_time, max_prediction_time) - - # 执行预测 - if self.prediction_config['enable_extrapolation']: - self._extrapolate_object(obj_id, pred_data, prediction_time, current_time, dt) - - # 更新统计信息 - self.prediction_stats['predictions_made'] += 1 - - # 更新预测对象计数 - self.prediction_state['predicted_objects_count'] = len(self.predicted_objects) - self.prediction_state['last_prediction_time'] = current_time - - except Exception as e: - print(f"✗ 预测更新失败: {e}") - self.prediction_stats['prediction_errors'] += 1 - - def _extrapolate_object(self, obj_id: str, pred_data: Dict[str, Any], - prediction_time: float, current_time: float, dt: float): - """ - 外推对象状态 - - Args: - obj_id: 对象ID - pred_data: 预测数据 - prediction_time: 预测时间 - current_time: 当前时间 - dt: 时间增量 - """ - try: - # 获取对象当前状态 - if obj_id not in self.object_history or not self.object_history[obj_id]: - return - - current_state = self.object_history[obj_id][-1] - current_timestamp = current_state.get('timestamp', current_time) - - # 计算时间差 - time_diff = prediction_time - current_timestamp - - # 如果时间差太大,不进行外推 - if abs(time_diff) > self.prediction_config['max_prediction_time']: - return - - # 获取对象速度 - velocity = self.object_velocities.get(obj_id, { - 'position': [0.0, 0.0, 0.0], - 'rotation': [0.0, 0.0, 0.0], - 'scale': [0.0, 0.0, 0.0] - }) - - # 外推位置 - if 'position' in current_state: - current_pos = current_state['position'] - velocity_pos = velocity['position'] - - predicted_pos = [ - current_pos[i] + velocity_pos[i] * time_diff - for i in range(min(len(current_pos), len(velocity_pos))) - ] - - pred_data['position'] = predicted_pos - - # 外推旋转 - if 'rotation' in current_state: - current_rot = current_state['rotation'] - velocity_rot = velocity['rotation'] - - predicted_rot = [ - current_rot[i] + velocity_rot[i] * time_diff - for i in range(min(len(current_rot), len(velocity_rot))) - ] - - pred_data['rotation'] = predicted_rot - - # 外推缩放 - if 'scale' in current_state: - current_scale = current_state['scale'] - velocity_scale = velocity['scale'] - - predicted_scale = [ - current_scale[i] + velocity_scale[i] * time_diff - for i in range(min(len(current_scale), len(velocity_scale))) - ] - - pred_data['scale'] = predicted_scale - - # 应用阻尼 - self._apply_velocity_damping(obj_id, dt) - - # 触发预测更新回调 - self._trigger_prediction_callback('prediction_updated', { - 'object_id': obj_id, - 'predicted_state': pred_data.copy(), - 'prediction_time': prediction_time, - 'time_diff': time_diff - }) - - except Exception as e: - print(f"✗ 对象外推失败: {e}") - self.prediction_stats['prediction_errors'] += 1 - - def _apply_velocity_damping(self, obj_id: str, dt: float): - """ - 应用速度阻尼 - - Args: - obj_id: 对象ID - dt: 时间增量 - """ - try: - if obj_id in self.object_velocities: - damping = self.prediction_config['velocity_damping'] - velocity = self.object_velocities[obj_id] - - for key in ['position', 'rotation', 'scale']: - if key in velocity: - velocity[key] = [ - v * (damping ** dt) - for v in velocity[key] - ] - - except Exception as e: - print(f"✗ 速度阻尼应用失败: {e}") - - def register_object(self, obj_id: str, initial_state: Dict[str, Any]) -> bool: - """ - 注册预测对象 - - Args: - obj_id: 对象ID - initial_state: 初始状态 - - Returns: - 是否注册成功 - """ - try: - with self.prediction_lock: - # 初始化预测数据 - self.predicted_objects[obj_id] = initial_state.copy() - - # 初始化历史记录 - self.object_history[obj_id] = [{ - 'state': initial_state.copy(), - 'timestamp': time.time() - }] - - # 初始化速度数据 - self.object_velocities[obj_id] = { - 'position': [0.0, 0.0, 0.0], - 'rotation': [0.0, 0.0, 0.0], - 'scale': [0.0, 0.0, 0.0] - } - - print(f"✓ 预测对象已注册: {obj_id}") - return True - - except Exception as e: - print(f"✗ 预测对象注册失败: {e}") - return False - - def unregister_object(self, obj_id: str) -> bool: - """ - 注销预测对象 - - Args: - obj_id: 对象ID - - Returns: - 是否注销成功 - """ - try: - with self.prediction_lock: - if obj_id in self.predicted_objects: - del self.predicted_objects[obj_id] - - if obj_id in self.object_history: - del self.object_history[obj_id] - - if obj_id in self.object_velocities: - del self.object_velocities[obj_id] - - print(f"✓ 预测对象已注销: {obj_id}") - return True - - except Exception as e: - print(f"✗ 预测对象注销失败: {e}") - return False - - def update_object_state(self, obj_id: str, new_state: Dict[str, Any], timestamp: float = None): - """ - 更新对象状态 - - Args: - obj_id: 对象ID - new_state: 新状态 - timestamp: 时间戳 - """ - try: - if timestamp is None: - timestamp = time.time() - - with self.prediction_lock: - # 添加到历史记录 - if obj_id not in self.object_history: - self.object_history[obj_id] = [] - - self.object_history[obj_id].append({ - 'state': new_state.copy(), - 'timestamp': timestamp - }) - - # 保持历史记录大小 - if len(self.object_history[obj_id]) > 20: # 保持最近20个状态 - self.object_history[obj_id].pop(0) - - # 计算速度(如果可能) - self._calculate_object_velocity(obj_id, new_state, timestamp) - - # 更新预测对象状态 - if obj_id in self.predicted_objects: - self.predicted_objects[obj_id].update(new_state) - - except Exception as e: - print(f"✗ 对象状态更新失败: {e}") - - def _calculate_object_velocity(self, obj_id: str, new_state: Dict[str, Any], timestamp: float): - """ - 计算对象速度 - - Args: - obj_id: 对象ID - new_state: 新状态 - timestamp: 时间戳 - """ - try: - # 需要至少两个历史状态来计算速度 - if obj_id not in self.object_history or len(self.object_history[obj_id]) < 2: - return - - history = self.object_history[obj_id] - current_entry = history[-1] - previous_entry = history[-2] - - current_state = current_entry['state'] - previous_state = previous_entry['state'] - current_time = current_entry['timestamp'] - previous_time = previous_entry['timestamp'] - - time_diff = current_time - previous_time - if time_diff <= 0: - return - - # 计算位置速度 - if 'position' in new_state and 'position' in previous_state: - current_pos = current_state['position'] - previous_pos = previous_state['position'] - - velocity_pos = [ - (current_pos[i] - previous_pos[i]) / time_diff - for i in range(min(len(current_pos), len(previous_pos))) - ] - - self.object_velocities[obj_id]['position'] = velocity_pos - - # 计算旋转速度 - if 'rotation' in new_state and 'rotation' in previous_state: - current_rot = current_state['rotation'] - previous_rot = previous_state['rotation'] - - velocity_rot = [ - (current_rot[i] - previous_rot[i]) / time_diff - for i in range(min(len(current_rot), len(previous_rot))) - ] - - self.object_velocities[obj_id]['rotation'] = velocity_rot - - # 计算缩放速度 - if 'scale' in new_state and 'scale' in previous_state: - current_scale = current_state['scale'] - previous_scale = previous_state['scale'] - - velocity_scale = [ - (current_scale[i] - previous_scale[i]) / time_diff - for i in range(min(len(current_scale), len(previous_scale))) - ] - - self.object_velocities[obj_id]['scale'] = velocity_scale - - except Exception as e: - print(f"✗ 对象速度计算失败: {e}") - - def apply_correction(self, obj_id: str, server_state: Dict[str, Any], server_time: float): - """ - 应用服务器校正 - - Args: - obj_id: 对象ID - server_state: 服务器状态 - server_time: 服务器时间 - """ - try: - if not self.prediction_config['enable_prediction']: - return - - with self.prediction_lock: - # 检查对象是否存在 - if obj_id not in self.predicted_objects: - return - - predicted_state = self.predicted_objects[obj_id] - - # 计算位置差异 - position_error = 0.0 - if 'position' in server_state and 'position' in predicted_state: - pos1 = server_state['position'] - pos2 = predicted_state['position'] - position_error = self._calculate_distance(pos1, pos2) - - # 计算旋转差异 - rotation_error = 0.0 - if 'rotation' in server_state and 'rotation' in predicted_state: - rot1 = server_state['rotation'] - rot2 = predicted_state['rotation'] - rotation_error = self._calculate_rotation_difference(rot1, rot2) - - # 更新平均误差统计 - total_error = position_error + rotation_error - if self.prediction_stats['predictions_made'] > 0: - self.prediction_stats['average_error'] = ( - (self.prediction_stats['average_error'] * (self.prediction_stats['predictions_made'] - 1) + total_error) / - self.prediction_stats['predictions_made'] - ) - else: - self.prediction_stats['average_error'] = total_error - - self.prediction_stats['max_error'] = max(self.prediction_stats['max_error'], total_error) - - # 检查是否需要快照校正 - if (self.prediction_config['enable_snap_correction'] and - total_error > self.prediction_config['snap_threshold']): - # 快照校正 - self.predicted_objects[obj_id].update(server_state) - self.prediction_stats['snap_corrections'] += 1 - self.prediction_state['snap_corrections'] += 1 - - # 触发快照校正回调 - self._trigger_prediction_callback('snap_correction', { - 'object_id': obj_id, - 'server_state': server_state, - 'predicted_state': predicted_state, - 'error': total_error - }) - else: - # 平滑校正 - self._smooth_correct_object(obj_id, server_state, server_time) - self.prediction_stats['corrections_applied'] += 1 - self.prediction_state['corrections_applied'] += 1 - - # 触发校正应用回调 - self._trigger_prediction_callback('correction_applied', { - 'object_id': obj_id, - 'server_state': server_state, - 'predicted_state': predicted_state, - 'error': total_error - }) - - # 更新对象历史 - self.update_object_state(obj_id, server_state, server_time) - - except Exception as e: - print(f"✗ 校正应用失败: {e}") - self.prediction_stats['prediction_errors'] += 1 - self._trigger_prediction_callback('prediction_error', { - 'object_id': obj_id, - 'error': str(e) - }) - - def _smooth_correct_object(self, obj_id: str, server_state: Dict[str, Any], server_time: float): - """ - 平滑校正对象 - - Args: - obj_id: 对象ID - server_state: 服务器状态 - server_time: 服务器时间 - """ - try: - if obj_id not in self.predicted_objects: - return - - predicted_state = self.predicted_objects[obj_id] - correction_speed = self.prediction_config['correction_speed'] - - # 平滑校正位置 - if 'position' in server_state and 'position' in predicted_state: - server_pos = server_state['position'] - predicted_pos = predicted_state['position'] - - corrected_pos = [ - predicted_pos[i] + (server_pos[i] - predicted_pos[i]) * correction_speed * 0.016 # 假设60FPS - for i in range(min(len(server_pos), len(predicted_pos))) - ] - - self.predicted_objects[obj_id]['position'] = corrected_pos - - # 平滑校正旋转 - if 'rotation' in server_state and 'rotation' in predicted_state: - server_rot = server_state['rotation'] - predicted_rot = predicted_state['rotation'] - - corrected_rot = [ - predicted_rot[i] + (server_rot[i] - predicted_rot[i]) * correction_speed * 0.016 - for i in range(min(len(server_rot), len(predicted_rot))) - ] - - self.predicted_objects[obj_id]['rotation'] = corrected_rot - - # 平滑校正缩放 - if 'scale' in server_state and 'scale' in predicted_state: - server_scale = server_state['scale'] - predicted_scale = predicted_state['scale'] - - corrected_scale = [ - predicted_scale[i] + (server_scale[i] - predicted_scale[i]) * correction_speed * 0.016 - for i in range(min(len(server_scale), len(predicted_scale))) - ] - - self.predicted_objects[obj_id]['scale'] = corrected_scale - - except Exception as e: - print(f"✗ 平滑校正失败: {e}") - - def _calculate_distance(self, pos1: List[float], pos2: List[float]) -> float: - """ - 计算两点间距离 - - Args: - pos1: 位置1 - pos2: 位置2 - - Returns: - 距离 - """ - try: - if len(pos1) >= 3 and len(pos2) >= 3: - dx = pos1[0] - pos2[0] - dy = pos1[1] - pos2[1] - dz = pos1[2] - pos2[2] - return (dx*dx + dy*dy + dz*dz) ** 0.5 - elif len(pos1) >= 2 and len(pos2) >= 2: - dx = pos1[0] - pos2[0] - dy = pos1[1] - pos2[1] - return (dx*dx + dy*dy) ** 0.5 - else: - return 0.0 - except Exception as e: - print(f"✗ 距离计算失败: {e}") - return 0.0 - - def _calculate_rotation_difference(self, rot1: List[float], rot2: List[float]) -> float: - """ - 计算旋转差异 - - Args: - rot1: 旋转1 - rot2: 旋转2 - - Returns: - 旋转差异 - """ - try: - # 简单实现:计算欧拉角差异 - diff = 0.0 - for i in range(min(len(rot1), len(rot2))): - diff += abs(rot1[i] - rot2[i]) - return diff - except Exception as e: - print(f"✗ 旋转差异计算失败: {e}") - return 0.0 - - def get_predicted_state(self, obj_id: str) -> Optional[Dict[str, Any]]: - """ - 获取预测状态 - - Args: - obj_id: 对象ID - - Returns: - 预测状态或None - """ - try: - with self.prediction_lock: - return self.predicted_objects.get(obj_id, {}).copy() - except Exception as e: - print(f"✗ 预测状态获取失败: {e}") - return None - - def get_prediction_stats(self) -> Dict[str, Any]: - """ - 获取预测统计信息 - - Returns: - 预测统计字典 - """ - return self.prediction_stats.copy() - - def reset_prediction_stats(self): - """重置预测统计信息""" - try: - self.prediction_stats = { - 'predictions_made': 0, - 'corrections_applied': 0, - 'snap_corrections': 0, - 'prediction_errors': 0, - 'average_error': 0.0, - 'max_error': 0.0 - } - print("✓ 预测统计信息已重置") - except Exception as e: - print(f"✗ 预测统计信息重置失败: {e}") - - def set_prediction_config(self, config: Dict[str, Any]) -> bool: - """ - 设置预测配置 - - Args: - config: 预测配置字典 - - Returns: - 是否设置成功 - """ - try: - self.prediction_config.update(config) - print(f"✓ 预测配置已更新: {self.prediction_config}") - return True - except Exception as e: - print(f"✗ 预测配置设置失败: {e}") - return False - - def get_prediction_config(self) -> Dict[str, Any]: - """ - 获取预测配置 - - Returns: - 预测配置字典 - """ - return self.prediction_config.copy() - - def _trigger_prediction_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发预测回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.prediction_callbacks: - for callback in self.prediction_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 预测回调执行失败: {e}") - except Exception as e: - print(f"✗ 预测回调触发失败: {e}") - - def register_prediction_callback(self, callback_type: str, callback: callable): - """ - 注册预测回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.prediction_callbacks: - self.prediction_callbacks[callback_type].append(callback) - print(f"✓ 预测回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 预测回调注册失败: {e}") - - def unregister_prediction_callback(self, callback_type: str, callback: callable): - """ - 注销预测回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.prediction_callbacks: - if callback in self.prediction_callbacks[callback_type]: - self.prediction_callbacks[callback_type].remove(callback) - print(f"✓ 预测回调已注销: {callback_type}") - except Exception as e: +""" +预测管理模块 +负责处理网络延迟下的对象状态预测 +""" + +import time +from typing import Dict, Any, List, Optional +import threading +import copy + +class PredictionManager: + """ + 预测管理器 + 负责处理网络延迟下的对象状态预测 + """ + + def __init__(self, plugin): + """ + 初始化预测管理器 + + Args: + plugin: 网络同步插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 预测配置 + self.prediction_config = { + 'enable_prediction': True, + 'enable_interpolation': True, + 'enable_extrapolation': True, + 'max_prediction_time': 0.5, # 最大预测时间(秒) + 'prediction_smoothing': 0.1, # 预测平滑系数 + 'correction_speed': 5.0, # 状态校正速度 + 'velocity_damping': 0.95, # 速度阻尼 + 'position_threshold': 0.01, # 位置校正阈值 + 'rotation_threshold': 0.1, # 旋转校正阈值 + 'enable_snap_correction': True, # 启用快照校正 + 'snap_threshold': 1.0 # 快照阈值 + } + + # 预测对象管理 + self.predicted_objects = {} # 预测对象状态 + self.object_history = {} # 对象历史状态 + self.object_velocities = {} # 对象速度状态 + + # 预测状态 + self.prediction_state = { + 'last_prediction_time': 0.0, + 'predicted_objects_count': 0, + 'corrections_applied': 0, + 'snap_corrections': 0 + } + + # 预测统计 + self.prediction_stats = { + 'predictions_made': 0, + 'corrections_applied': 0, + 'snap_corrections': 0, + 'prediction_errors': 0, + 'average_error': 0.0, + 'max_error': 0.0 + } + + # 线程锁 + self.prediction_lock = threading.RLock() + + # 回调函数 + self.prediction_callbacks = { + 'prediction_updated': [], + 'correction_applied': [], + 'snap_correction': [], + 'prediction_error': [] + } + + # 时间戳记录 + self.last_prediction_update = 0.0 + self.last_correction_apply = 0.0 + + print("✓ 预测管理器已创建") + + def initialize(self) -> bool: + """ + 初始化预测管理器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 预测管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 预测管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用预测管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 预测管理器未初始化") + return False + + self.enabled = True + print("✓ 预测管理器已启用") + return True + + except Exception as e: + print(f"✗ 预测管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用预测管理器""" + try: + self.enabled = False + print("✓ 预测管理器已禁用") + + except Exception as e: + print(f"✗ 预测管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理预测管理器资源""" + try: + self.disable() + self.predicted_objects.clear() + self.object_history.clear() + self.object_velocities.clear() + self.prediction_callbacks.clear() + self.initialized = False + print("✓ 预测管理器资源已清理") + + except Exception as e: + print(f"✗ 预测管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新预测管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.last_prediction_update = current_time + + # 更新预测 + if self.prediction_config['enable_prediction']: + self._update_predictions(current_time, dt) + + except Exception as e: + print(f"✗ 预测管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_predictions(self, current_time: float, dt: float): + """ + 更新预测状态 + + Args: + current_time: 当前时间 + dt: 时间增量 + """ + try: + with self.prediction_lock: + for obj_id, pred_data in self.predicted_objects.items(): + # 获取服务器时间 + server_time = self.plugin.clock_sync.get_server_time() if self.plugin.clock_sync else current_time + + # 计算预测时间 + prediction_time = server_time + self.plugin.network_manager.get_connection_state().get('latency', 0.0) \ + if self.plugin.network_manager else server_time + + # 限制最大预测时间 + max_prediction_time = server_time + self.prediction_config['max_prediction_time'] + prediction_time = min(prediction_time, max_prediction_time) + + # 执行预测 + if self.prediction_config['enable_extrapolation']: + self._extrapolate_object(obj_id, pred_data, prediction_time, current_time, dt) + + # 更新统计信息 + self.prediction_stats['predictions_made'] += 1 + + # 更新预测对象计数 + self.prediction_state['predicted_objects_count'] = len(self.predicted_objects) + self.prediction_state['last_prediction_time'] = current_time + + except Exception as e: + print(f"✗ 预测更新失败: {e}") + self.prediction_stats['prediction_errors'] += 1 + + def _extrapolate_object(self, obj_id: str, pred_data: Dict[str, Any], + prediction_time: float, current_time: float, dt: float): + """ + 外推对象状态 + + Args: + obj_id: 对象ID + pred_data: 预测数据 + prediction_time: 预测时间 + current_time: 当前时间 + dt: 时间增量 + """ + try: + # 获取对象当前状态 + if obj_id not in self.object_history or not self.object_history[obj_id]: + return + + current_state = self.object_history[obj_id][-1] + current_timestamp = current_state.get('timestamp', current_time) + + # 计算时间差 + time_diff = prediction_time - current_timestamp + + # 如果时间差太大,不进行外推 + if abs(time_diff) > self.prediction_config['max_prediction_time']: + return + + # 获取对象速度 + velocity = self.object_velocities.get(obj_id, { + 'position': [0.0, 0.0, 0.0], + 'rotation': [0.0, 0.0, 0.0], + 'scale': [0.0, 0.0, 0.0] + }) + + # 外推位置 + if 'position' in current_state: + current_pos = current_state['position'] + velocity_pos = velocity['position'] + + predicted_pos = [ + current_pos[i] + velocity_pos[i] * time_diff + for i in range(min(len(current_pos), len(velocity_pos))) + ] + + pred_data['position'] = predicted_pos + + # 外推旋转 + if 'rotation' in current_state: + current_rot = current_state['rotation'] + velocity_rot = velocity['rotation'] + + predicted_rot = [ + current_rot[i] + velocity_rot[i] * time_diff + for i in range(min(len(current_rot), len(velocity_rot))) + ] + + pred_data['rotation'] = predicted_rot + + # 外推缩放 + if 'scale' in current_state: + current_scale = current_state['scale'] + velocity_scale = velocity['scale'] + + predicted_scale = [ + current_scale[i] + velocity_scale[i] * time_diff + for i in range(min(len(current_scale), len(velocity_scale))) + ] + + pred_data['scale'] = predicted_scale + + # 应用阻尼 + self._apply_velocity_damping(obj_id, dt) + + # 触发预测更新回调 + self._trigger_prediction_callback('prediction_updated', { + 'object_id': obj_id, + 'predicted_state': pred_data.copy(), + 'prediction_time': prediction_time, + 'time_diff': time_diff + }) + + except Exception as e: + print(f"✗ 对象外推失败: {e}") + self.prediction_stats['prediction_errors'] += 1 + + def _apply_velocity_damping(self, obj_id: str, dt: float): + """ + 应用速度阻尼 + + Args: + obj_id: 对象ID + dt: 时间增量 + """ + try: + if obj_id in self.object_velocities: + damping = self.prediction_config['velocity_damping'] + velocity = self.object_velocities[obj_id] + + for key in ['position', 'rotation', 'scale']: + if key in velocity: + velocity[key] = [ + v * (damping ** dt) + for v in velocity[key] + ] + + except Exception as e: + print(f"✗ 速度阻尼应用失败: {e}") + + def register_object(self, obj_id: str, initial_state: Dict[str, Any]) -> bool: + """ + 注册预测对象 + + Args: + obj_id: 对象ID + initial_state: 初始状态 + + Returns: + 是否注册成功 + """ + try: + with self.prediction_lock: + # 初始化预测数据 + self.predicted_objects[obj_id] = initial_state.copy() + + # 初始化历史记录 + self.object_history[obj_id] = [{ + 'state': initial_state.copy(), + 'timestamp': time.time() + }] + + # 初始化速度数据 + self.object_velocities[obj_id] = { + 'position': [0.0, 0.0, 0.0], + 'rotation': [0.0, 0.0, 0.0], + 'scale': [0.0, 0.0, 0.0] + } + + print(f"✓ 预测对象已注册: {obj_id}") + return True + + except Exception as e: + print(f"✗ 预测对象注册失败: {e}") + return False + + def unregister_object(self, obj_id: str) -> bool: + """ + 注销预测对象 + + Args: + obj_id: 对象ID + + Returns: + 是否注销成功 + """ + try: + with self.prediction_lock: + if obj_id in self.predicted_objects: + del self.predicted_objects[obj_id] + + if obj_id in self.object_history: + del self.object_history[obj_id] + + if obj_id in self.object_velocities: + del self.object_velocities[obj_id] + + print(f"✓ 预测对象已注销: {obj_id}") + return True + + except Exception as e: + print(f"✗ 预测对象注销失败: {e}") + return False + + def update_object_state(self, obj_id: str, new_state: Dict[str, Any], timestamp: float = None): + """ + 更新对象状态 + + Args: + obj_id: 对象ID + new_state: 新状态 + timestamp: 时间戳 + """ + try: + if timestamp is None: + timestamp = time.time() + + with self.prediction_lock: + # 添加到历史记录 + if obj_id not in self.object_history: + self.object_history[obj_id] = [] + + self.object_history[obj_id].append({ + 'state': new_state.copy(), + 'timestamp': timestamp + }) + + # 保持历史记录大小 + if len(self.object_history[obj_id]) > 20: # 保持最近20个状态 + self.object_history[obj_id].pop(0) + + # 计算速度(如果可能) + self._calculate_object_velocity(obj_id, new_state, timestamp) + + # 更新预测对象状态 + if obj_id in self.predicted_objects: + self.predicted_objects[obj_id].update(new_state) + + except Exception as e: + print(f"✗ 对象状态更新失败: {e}") + + def _calculate_object_velocity(self, obj_id: str, new_state: Dict[str, Any], timestamp: float): + """ + 计算对象速度 + + Args: + obj_id: 对象ID + new_state: 新状态 + timestamp: 时间戳 + """ + try: + # 需要至少两个历史状态来计算速度 + if obj_id not in self.object_history or len(self.object_history[obj_id]) < 2: + return + + history = self.object_history[obj_id] + current_entry = history[-1] + previous_entry = history[-2] + + current_state = current_entry['state'] + previous_state = previous_entry['state'] + current_time = current_entry['timestamp'] + previous_time = previous_entry['timestamp'] + + time_diff = current_time - previous_time + if time_diff <= 0: + return + + # 计算位置速度 + if 'position' in new_state and 'position' in previous_state: + current_pos = current_state['position'] + previous_pos = previous_state['position'] + + velocity_pos = [ + (current_pos[i] - previous_pos[i]) / time_diff + for i in range(min(len(current_pos), len(previous_pos))) + ] + + self.object_velocities[obj_id]['position'] = velocity_pos + + # 计算旋转速度 + if 'rotation' in new_state and 'rotation' in previous_state: + current_rot = current_state['rotation'] + previous_rot = previous_state['rotation'] + + velocity_rot = [ + (current_rot[i] - previous_rot[i]) / time_diff + for i in range(min(len(current_rot), len(previous_rot))) + ] + + self.object_velocities[obj_id]['rotation'] = velocity_rot + + # 计算缩放速度 + if 'scale' in new_state and 'scale' in previous_state: + current_scale = current_state['scale'] + previous_scale = previous_state['scale'] + + velocity_scale = [ + (current_scale[i] - previous_scale[i]) / time_diff + for i in range(min(len(current_scale), len(previous_scale))) + ] + + self.object_velocities[obj_id]['scale'] = velocity_scale + + except Exception as e: + print(f"✗ 对象速度计算失败: {e}") + + def apply_correction(self, obj_id: str, server_state: Dict[str, Any], server_time: float): + """ + 应用服务器校正 + + Args: + obj_id: 对象ID + server_state: 服务器状态 + server_time: 服务器时间 + """ + try: + if not self.prediction_config['enable_prediction']: + return + + with self.prediction_lock: + # 检查对象是否存在 + if obj_id not in self.predicted_objects: + return + + predicted_state = self.predicted_objects[obj_id] + + # 计算位置差异 + position_error = 0.0 + if 'position' in server_state and 'position' in predicted_state: + pos1 = server_state['position'] + pos2 = predicted_state['position'] + position_error = self._calculate_distance(pos1, pos2) + + # 计算旋转差异 + rotation_error = 0.0 + if 'rotation' in server_state and 'rotation' in predicted_state: + rot1 = server_state['rotation'] + rot2 = predicted_state['rotation'] + rotation_error = self._calculate_rotation_difference(rot1, rot2) + + # 更新平均误差统计 + total_error = position_error + rotation_error + if self.prediction_stats['predictions_made'] > 0: + self.prediction_stats['average_error'] = ( + (self.prediction_stats['average_error'] * (self.prediction_stats['predictions_made'] - 1) + total_error) / + self.prediction_stats['predictions_made'] + ) + else: + self.prediction_stats['average_error'] = total_error + + self.prediction_stats['max_error'] = max(self.prediction_stats['max_error'], total_error) + + # 检查是否需要快照校正 + if (self.prediction_config['enable_snap_correction'] and + total_error > self.prediction_config['snap_threshold']): + # 快照校正 + self.predicted_objects[obj_id].update(server_state) + self.prediction_stats['snap_corrections'] += 1 + self.prediction_state['snap_corrections'] += 1 + + # 触发快照校正回调 + self._trigger_prediction_callback('snap_correction', { + 'object_id': obj_id, + 'server_state': server_state, + 'predicted_state': predicted_state, + 'error': total_error + }) + else: + # 平滑校正 + self._smooth_correct_object(obj_id, server_state, server_time) + self.prediction_stats['corrections_applied'] += 1 + self.prediction_state['corrections_applied'] += 1 + + # 触发校正应用回调 + self._trigger_prediction_callback('correction_applied', { + 'object_id': obj_id, + 'server_state': server_state, + 'predicted_state': predicted_state, + 'error': total_error + }) + + # 更新对象历史 + self.update_object_state(obj_id, server_state, server_time) + + except Exception as e: + print(f"✗ 校正应用失败: {e}") + self.prediction_stats['prediction_errors'] += 1 + self._trigger_prediction_callback('prediction_error', { + 'object_id': obj_id, + 'error': str(e) + }) + + def _smooth_correct_object(self, obj_id: str, server_state: Dict[str, Any], server_time: float): + """ + 平滑校正对象 + + Args: + obj_id: 对象ID + server_state: 服务器状态 + server_time: 服务器时间 + """ + try: + if obj_id not in self.predicted_objects: + return + + predicted_state = self.predicted_objects[obj_id] + correction_speed = self.prediction_config['correction_speed'] + + # 平滑校正位置 + if 'position' in server_state and 'position' in predicted_state: + server_pos = server_state['position'] + predicted_pos = predicted_state['position'] + + corrected_pos = [ + predicted_pos[i] + (server_pos[i] - predicted_pos[i]) * correction_speed * 0.016 # 假设60FPS + for i in range(min(len(server_pos), len(predicted_pos))) + ] + + self.predicted_objects[obj_id]['position'] = corrected_pos + + # 平滑校正旋转 + if 'rotation' in server_state and 'rotation' in predicted_state: + server_rot = server_state['rotation'] + predicted_rot = predicted_state['rotation'] + + corrected_rot = [ + predicted_rot[i] + (server_rot[i] - predicted_rot[i]) * correction_speed * 0.016 + for i in range(min(len(server_rot), len(predicted_rot))) + ] + + self.predicted_objects[obj_id]['rotation'] = corrected_rot + + # 平滑校正缩放 + if 'scale' in server_state and 'scale' in predicted_state: + server_scale = server_state['scale'] + predicted_scale = predicted_state['scale'] + + corrected_scale = [ + predicted_scale[i] + (server_scale[i] - predicted_scale[i]) * correction_speed * 0.016 + for i in range(min(len(server_scale), len(predicted_scale))) + ] + + self.predicted_objects[obj_id]['scale'] = corrected_scale + + except Exception as e: + print(f"✗ 平滑校正失败: {e}") + + def _calculate_distance(self, pos1: List[float], pos2: List[float]) -> float: + """ + 计算两点间距离 + + Args: + pos1: 位置1 + pos2: 位置2 + + Returns: + 距离 + """ + try: + if len(pos1) >= 3 and len(pos2) >= 3: + dx = pos1[0] - pos2[0] + dy = pos1[1] - pos2[1] + dz = pos1[2] - pos2[2] + return (dx*dx + dy*dy + dz*dz) ** 0.5 + elif len(pos1) >= 2 and len(pos2) >= 2: + dx = pos1[0] - pos2[0] + dy = pos1[1] - pos2[1] + return (dx*dx + dy*dy) ** 0.5 + else: + return 0.0 + except Exception as e: + print(f"✗ 距离计算失败: {e}") + return 0.0 + + def _calculate_rotation_difference(self, rot1: List[float], rot2: List[float]) -> float: + """ + 计算旋转差异 + + Args: + rot1: 旋转1 + rot2: 旋转2 + + Returns: + 旋转差异 + """ + try: + # 简单实现:计算欧拉角差异 + diff = 0.0 + for i in range(min(len(rot1), len(rot2))): + diff += abs(rot1[i] - rot2[i]) + return diff + except Exception as e: + print(f"✗ 旋转差异计算失败: {e}") + return 0.0 + + def get_predicted_state(self, obj_id: str) -> Optional[Dict[str, Any]]: + """ + 获取预测状态 + + Args: + obj_id: 对象ID + + Returns: + 预测状态或None + """ + try: + with self.prediction_lock: + return self.predicted_objects.get(obj_id, {}).copy() + except Exception as e: + print(f"✗ 预测状态获取失败: {e}") + return None + + def get_prediction_stats(self) -> Dict[str, Any]: + """ + 获取预测统计信息 + + Returns: + 预测统计字典 + """ + return self.prediction_stats.copy() + + def reset_prediction_stats(self): + """重置预测统计信息""" + try: + self.prediction_stats = { + 'predictions_made': 0, + 'corrections_applied': 0, + 'snap_corrections': 0, + 'prediction_errors': 0, + 'average_error': 0.0, + 'max_error': 0.0 + } + print("✓ 预测统计信息已重置") + except Exception as e: + print(f"✗ 预测统计信息重置失败: {e}") + + def set_prediction_config(self, config: Dict[str, Any]) -> bool: + """ + 设置预测配置 + + Args: + config: 预测配置字典 + + Returns: + 是否设置成功 + """ + try: + self.prediction_config.update(config) + print(f"✓ 预测配置已更新: {self.prediction_config}") + return True + except Exception as e: + print(f"✗ 预测配置设置失败: {e}") + return False + + def get_prediction_config(self) -> Dict[str, Any]: + """ + 获取预测配置 + + Returns: + 预测配置字典 + """ + return self.prediction_config.copy() + + def _trigger_prediction_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发预测回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.prediction_callbacks: + for callback in self.prediction_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 预测回调执行失败: {e}") + except Exception as e: + print(f"✗ 预测回调触发失败: {e}") + + def register_prediction_callback(self, callback_type: str, callback: callable): + """ + 注册预测回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.prediction_callbacks: + self.prediction_callbacks[callback_type].append(callback) + print(f"✓ 预测回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 预测回调注册失败: {e}") + + def unregister_prediction_callback(self, callback_type: str, callback: callable): + """ + 注销预测回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.prediction_callbacks: + if callback in self.prediction_callbacks[callback_type]: + self.prediction_callbacks[callback_type].remove(callback) + print(f"✓ 预测回调已注销: {callback_type}") + except Exception as e: print(f"✗ 预测回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/network_sync/protocol/protocol_handler.py b/plugins/user/network_sync/protocol/protocol_handler.py index 68213e70..5cc3ab66 100644 --- a/plugins/user/network_sync/protocol/protocol_handler.py +++ b/plugins/user/network_sync/protocol/protocol_handler.py @@ -1,883 +1,883 @@ -""" -协议处理器模块 -负责处理各种网络协议和消息格式 -""" - -import time -import json -import struct -from typing import Dict, Any, List, Optional -import hashlib -import zlib - -class ProtocolHandler: - """ - 协议处理器 - 负责处理各种网络协议和消息格式 - """ - - def __init__(self, plugin): - """ - 初始化协议处理器 - - Args: - plugin: 网络同步插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 协议配置 - self.protocol_config = { - 'version': '1.0', - 'enable_compression': True, - 'compression_level': 6, - 'enable_checksum': True, - 'max_message_size': 65536, # 64KB - 'enable_fragmentation': True, - 'fragment_size': 1024, - 'enable_encryption': False, - 'supported_protocols': ['sync_v1', 'sync_v2'] - } - - # 消息类型定义 - self.message_types = { - 'CONNECT': 1, - 'CONNECT_ACK': 2, - 'DISCONNECT': 3, - 'HEARTBEAT': 4, - 'SYNC_DATA': 5, - 'SYNC_REQUEST': 6, - 'SYNC_RESPONSE': 7, - 'RPC_CALL': 8, - 'RPC_RESPONSE': 9, - 'EVENT': 10, - 'CHAT': 11, - 'ERROR': 12 - } - - # 消息处理器 - self.message_handlers = {} - - # 协议状态 - self.protocol_state = { - 'current_protocol': 'sync_v1', - 'bytes_processed': 0, - 'messages_processed': 0, - 'compression_savings': 0, - 'fragmented_messages': 0 - } - - # 消息片段缓存 - self.message_fragments = {} - - # 协议统计 - self.protocol_stats = { - 'messages_sent': 0, - 'messages_received': 0, - 'bytes_sent': 0, - 'bytes_received': 0, - 'compressed_messages': 0, - 'checksum_errors': 0, - 'protocol_errors': 0, - 'fragmented_packets': 0 - } - - # 回调函数 - self.protocol_callbacks = { - 'message_received': [], - 'message_sent': [], - 'protocol_error': [], - 'message_fragmented': [] - } - - # 时间戳记录 - self.last_protocol_update = 0.0 - self.last_compression_savings = 0.0 - - print("✓ 协议处理器已创建") - - def initialize(self) -> bool: - """ - 初始化协议处理器 - - Returns: - 是否初始化成功 - """ - try: - # 注册默认消息处理器 - self._register_default_handlers() - - self.initialized = True - print("✓ 协议处理器初始化完成") - return True - - except Exception as e: - print(f"✗ 协议处理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用协议处理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 协议处理器未初始化") - return False - - self.enabled = True - print("✓ 协议处理器已启用") - return True - - except Exception as e: - print(f"✗ 协议处理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用协议处理器""" - try: - self.enabled = False - print("✓ 协议处理器已禁用") - - except Exception as e: - print(f"✗ 协议处理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理协议处理器资源""" - try: - self.disable() - self.message_handlers.clear() - self.message_fragments.clear() - self.protocol_callbacks.clear() - self.initialized = False - print("✓ 协议处理器资源已清理") - - except Exception as e: - print(f"✗ 协议处理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新协议处理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.last_protocol_update = current_time - - # 清理过期的消息片段 - self._cleanup_fragments(current_time) - - except Exception as e: - print(f"✗ 协议处理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _register_default_handlers(self): - """注册默认消息处理器""" - try: - self.message_handlers = { - self.message_types['CONNECT']: self._handle_connect, - self.message_types['CONNECT_ACK']: self._handle_connect_ack, - self.message_types['DISCONNECT']: self._handle_disconnect, - self.message_types['HEARTBEAT']: self._handle_heartbeat, - self.message_types['SYNC_DATA']: self._handle_sync_data, - self.message_types['SYNC_REQUEST']: self._handle_sync_request, - self.message_types['SYNC_RESPONSE']: self._handle_sync_response, - self.message_types['RPC_CALL']: self._handle_rpc_call, - self.message_types['RPC_RESPONSE']: self._handle_rpc_response, - self.message_types['EVENT']: self._handle_event, - self.message_types['CHAT']: self._handle_chat, - self.message_types['ERROR']: self._handle_error - } - - print("✓ 默认消息处理器已注册") - except Exception as e: - print(f"✗ 默认消息处理器注册失败: {e}") - - def _cleanup_fragments(self, current_time: float): - """ - 清理过期的消息片段 - - Args: - current_time: 当前时间 - """ - try: - expired_fragments = [ - msg_id for msg_id, fragment_data in self.message_fragments.items() - if current_time - fragment_data.get('timestamp', 0) > 30.0 # 30秒超时 - ] - - for msg_id in expired_fragments: - del self.message_fragments[msg_id] - - except Exception as e: - print(f"✗ 消息片段清理失败: {e}") - - def encode_message(self, message_type: str, data: Dict[str, Any], - compress: bool = None, add_checksum: bool = None) -> Optional[bytes]: - """ - 编码消息 - - Args: - message_type: 消息类型 - data: 消息数据 - compress: 是否压缩 - add_checksum: 是否添加校验和 - - Returns: - 编码后的消息数据或None - """ - try: - # 获取消息类型ID - type_id = self.message_types.get(message_type.upper()) - if type_id is None: - print(f"✗ 未知的消息类型: {message_type}") - return None - - # 序列化数据 - data_json = json.dumps(data, ensure_ascii=False) - data_bytes = data_json.encode('utf-8') - original_size = len(data_bytes) - - # 压缩数据 - compressed = False - if compress is None: - compress = self.protocol_config['enable_compression'] - - if compress and original_size > 256: # 只压缩大于256字节的数据 - compressed_data = zlib.compress(data_bytes, self.protocol_config['compression_level']) - if len(compressed_data) < original_size: - data_bytes = compressed_data - compressed = True - self.protocol_stats['compressed_messages'] += 1 - self.protocol_state['compression_savings'] += original_size - len(compressed_data) - - # 添加校验和 - checksum = b'' - if add_checksum is None: - add_checksum = self.protocol_config['enable_checksum'] - - if add_checksum: - checksum = hashlib.md5(data_bytes).digest() - - # 构造消息头 - timestamp = int(time.time() * 1000) & 0xFFFFFFFF # 32位时间戳 - header = struct.pack('!BBI', type_id, compressed, timestamp) - - # 构造完整消息 - message = header + checksum + data_bytes - - # 更新统计信息 - self.protocol_stats['messages_sent'] += 1 - self.protocol_stats['bytes_sent'] += len(message) - self.protocol_state['bytes_processed'] += len(message) - self.protocol_state['messages_processed'] += 1 - - return message - - except Exception as e: - print(f"✗ 消息编码失败: {e}") - self.protocol_stats['protocol_errors'] += 1 - return None - - def decode_message(self, message_data: bytes) -> Optional[Dict[str, Any]]: - """ - 解码消息 - - Args: - message_data: 消息数据 - - Returns: - 解码后的消息字典或None - """ - try: - if len(message_data) < 6: # 最小消息长度(1字节类型+1字节压缩标志+4字节时间戳) - print("✗ 消息数据太短") - self.protocol_stats['protocol_errors'] += 1 - return None - - # 解析消息头 - header_size = 6 - type_id, compressed_flag, timestamp = struct.unpack('!BBI', message_data[:header_size]) - - # 获取消息类型名称 - message_type = None - for name, id_val in self.message_types.items(): - if id_val == type_id: - message_type = name - break - - if message_type is None: - print(f"✗ 未知的消息类型ID: {type_id}") - self.protocol_stats['protocol_errors'] += 1 - return None - - # 计算校验和偏移 - checksum_offset = header_size - if self.protocol_config['enable_checksum']: - checksum_offset += 16 # MD5校验和长度 - - # 获取数据部分 - data_bytes = message_data[checksum_offset:] - - # 验证校验和 - if self.protocol_config['enable_checksum'] and len(message_data) >= checksum_offset: - expected_checksum = message_data[header_size:checksum_offset] - actual_checksum = hashlib.md5(data_bytes).digest() - if expected_checksum != actual_checksum: - print("✗ 消息校验和错误") - self.protocol_stats['checksum_errors'] += 1 - return None - - # 解压缩数据 - if compressed_flag: - try: - data_bytes = zlib.decompress(data_bytes) - except Exception as e: - print(f"✗ 消息解压缩失败: {e}") - self.protocol_stats['protocol_errors'] += 1 - return None - - # 反序列化数据 - data_json = data_bytes.decode('utf-8') - data = json.loads(data_json) - - # 构造解码结果 - result = { - 'type': message_type, - 'type_id': type_id, - 'compressed': bool(compressed_flag), - 'timestamp': timestamp, - 'data': data - } - - # 更新统计信息 - self.protocol_stats['messages_received'] += 1 - self.protocol_stats['bytes_received'] += len(message_data) - self.protocol_state['bytes_processed'] += len(message_data) - self.protocol_state['messages_processed'] += 1 - - # 触发消息接收回调 - self._trigger_protocol_callback('message_received', result) - - return result - - except Exception as e: - print(f"✗ 消息解码失败: {e}") - self.protocol_stats['protocol_errors'] += 1 - return None - - def fragment_message(self, message_data: bytes, fragment_size: int = None) -> List[bytes]: - """ - 分片消息 - - Args: - message_data: 消息数据 - fragment_size: 分片大小 - - Returns: - 分片后的消息列表 - """ - try: - if fragment_size is None: - fragment_size = self.protocol_config['fragment_size'] - - if len(message_data) <= fragment_size: - return [message_data] - - fragments = [] - message_id = hashlib.md5(message_data).hexdigest()[:8] - total_fragments = (len(message_data) + fragment_size - 1) // fragment_size - - for i in range(total_fragments): - start_pos = i * fragment_size - end_pos = min((i + 1) * fragment_size, len(message_data)) - fragment_data = message_data[start_pos:end_pos] - - # 构造分片头 - fragment_header = struct.pack('!8sHH', - message_id.encode('ascii'), - i, - total_fragments) - - fragments.append(fragment_header + fragment_data) - - self.protocol_stats['fragmented_packets'] += len(fragments) - self.protocol_state['fragmented_messages'] += 1 - - # 触发消息分片回调 - self._trigger_protocol_callback('message_fragmented', { - 'message_id': message_id, - 'total_fragments': total_fragments, - 'fragment_size': fragment_size - }) - - return fragments - - except Exception as e: - print(f"✗ 消息分片失败: {e}") - self.protocol_stats['protocol_errors'] += 1 - return [message_data] - - def reassemble_message(self, fragment_data: bytes) -> Optional[bytes]: - """ - 重组消息 - - Args: - fragment_data: 分片数据 - - Returns: - 重组后的消息数据或None - """ - try: - if len(fragment_data) < 12: # 分片头最小长度 - return None - - # 解析分片头 - message_id_bytes, fragment_index, total_fragments = struct.unpack('!8sHH', fragment_data[:12]) - message_id = message_id_bytes.decode('ascii').rstrip('\x00') - fragment_payload = fragment_data[12:] - - # 初始化分片缓存 - if message_id not in self.message_fragments: - self.message_fragments[message_id] = { - 'fragments': {}, - 'total_fragments': total_fragments, - 'timestamp': time.time() - } - - # 存储分片 - self.message_fragments[message_id]['fragments'][fragment_index] = fragment_payload - - # 检查是否所有分片都已到达 - fragment_cache = self.message_fragments[message_id] - if len(fragment_cache['fragments']) == fragment_cache['total_fragments']: - # 重组消息 - message_parts = [] - for i in range(fragment_cache['total_fragments']): - if i in fragment_cache['fragments']: - message_parts.append(fragment_cache['fragments'][i]) - else: - # 缺少分片 - return None - - # 清理缓存 - del self.message_fragments[message_id] - - # 返回重组后的消息 - return b''.join(message_parts) - - return None - - except Exception as e: - print(f"✗ 消息重组失败: {e}") - self.protocol_stats['protocol_errors'] += 1 - return None - - def send_message(self, message_type: str, data: Dict[str, Any], - target_client_id: str = None) -> bool: - """ - 发送消息 - - Args: - message_type: 消息类型 - data: 消息数据 - target_client_id: 目标客户端ID - - Returns: - 是否发送成功 - """ - try: - if not self.enabled: - print("✗ 协议处理器未启用") - return False - - # 编码消息 - message_bytes = self.encode_message(message_type, data) - if message_bytes is None: - print("✗ 消息编码失败") - return False - - # 检查是否需要分片 - if (self.protocol_config['enable_fragmentation'] and - len(message_bytes) > self.protocol_config['fragment_size']): - fragments = self.fragment_message(message_bytes) - success = True - - # 发送所有分片 - for fragment in fragments: - if not self._send_raw_message(fragment, target_client_id): - success = False - - if success: - # 触发消息发送回调 - self._trigger_protocol_callback('message_sent', { - 'type': message_type, - 'data': data, - 'fragmented': True, - 'fragment_count': len(fragments) - }) - - return success - else: - # 发送完整消息 - if self._send_raw_message(message_bytes, target_client_id): - # 触发消息发送回调 - self._trigger_protocol_callback('message_sent', { - 'type': message_type, - 'data': data, - 'fragmented': False - }) - return True - else: - return False - - except Exception as e: - print(f"✗ 消息发送失败: {e}") - self.protocol_stats['protocol_errors'] += 1 - return False - - def _send_raw_message(self, message_bytes: bytes, target_client_id: str = None) -> bool: - """ - 发送原始消息数据 - - Args: - message_bytes: 消息字节数据 - target_client_id: 目标客户端ID - - Returns: - 是否发送成功 - """ - try: - # 通过网络管理器发送消息 - if self.plugin.network_manager: - # 这里应该调用网络管理器的发送方法 - # 由于是模拟实现,我们只返回True - return True - else: - print("✗ 网络管理器未初始化") - return False - - except Exception as e: - print(f"✗ 原始消息发送失败: {e}") - return False - - def receive_message(self, message_bytes: bytes) -> bool: - """ - 接收消息 - - Args: - message_bytes: 消息字节数据 - - Returns: - 是否处理成功 - """ - try: - if not self.enabled: - print("✗ 协议处理器未启用") - return False - - # 检查是否为分片消息 - if len(message_bytes) >= 12: - # 尝试解析分片头 - try: - message_id_bytes, fragment_index, total_fragments = struct.unpack('!8sHH', message_bytes[:12]) - # 这是一个分片消息 - reassembled_message = self.reassemble_message(message_bytes) - if reassembled_message is not None: - # 分片重组完成,处理完整消息 - return self.receive_message(reassembled_message) - else: - # 等待更多分片 - return True - except struct.error: - # 不是分片消息,继续处理 - pass - - # 解码消息 - decoded_message = self.decode_message(message_bytes) - if decoded_message is None: - print("✗ 消息解码失败") - return False - - # 处理消息 - return self._process_message(decoded_message) - - except Exception as e: - print(f"✗ 消息接收失败: {e}") - self.protocol_stats['protocol_errors'] += 1 - return False - - def _process_message(self, message: Dict[str, Any]) -> bool: - """ - 处理消息 - - Args: - message: 解码后的消息 - - Returns: - 是否处理成功 - """ - try: - message_type_id = message.get('type_id') - message_data = message.get('data', {}) - - # 查找对应的消息处理器 - if message_type_id in self.message_handlers: - handler = self.message_handlers[message_type_id] - return handler(message_data) - else: - print(f"✗ 未找到消息处理器: {message_type_id}") - return False - - except Exception as e: - print(f"✗ 消息处理失败: {e}") - return False - - # 默认消息处理器 - def _handle_connect(self, data: Dict[str, Any]) -> bool: - """处理连接消息""" - try: - print(f"✓ 处理连接消息: {data}") - return True - except Exception as e: - print(f"✗ 连接消息处理失败: {e}") - return False - - def _handle_connect_ack(self, data: Dict[str, Any]) -> bool: - """处理连接确认消息""" - try: - print(f"✓ 处理连接确认消息: {data}") - return True - except Exception as e: - print(f"✗ 连接确认消息处理失败: {e}") - return False - - def _handle_disconnect(self, data: Dict[str, Any]) -> bool: - """处理断开连接消息""" - try: - print(f"✓ 处理断开连接消息: {data}") - return True - except Exception as e: - print(f"✗ 断开连接消息处理失败: {e}") - return False - - def _handle_heartbeat(self, data: Dict[str, Any]) -> bool: - """处理心跳消息""" - try: - print(f"✓ 处理心跳消息: {data}") - return True - except Exception as e: - print(f"✗ 心跳消息处理失败: {e}") - return False - - def _handle_sync_data(self, data: Dict[str, Any]) -> bool: - """处理同步数据消息""" - try: - print(f"✓ 处理同步数据消息: {len(data)}个对象") - - # 传递给对象同步模块 - if self.plugin.object_sync: - for obj_id, obj_data in data.items(): - self.plugin.object_sync.receive_sync_data(obj_id, obj_data) - - return True - except Exception as e: - print(f"✗ 同步数据消息处理失败: {e}") - return False - - def _handle_sync_request(self, data: Dict[str, Any]) -> bool: - """处理同步请求消息""" - try: - print(f"✓ 处理同步请求消息: {data}") - return True - except Exception as e: - print(f"✗ 同步请求消息处理失败: {e}") - return False - - def _handle_sync_response(self, data: Dict[str, Any]) -> bool: - """处理同步响应消息""" - try: - print(f"✓ 处理同步响应消息: {data}") - return True - except Exception as e: - print(f"✗ 同步响应消息处理失败: {e}") - return False - - def _handle_rpc_call(self, data: Dict[str, Any]) -> bool: - """处理RPC调用消息""" - try: - print(f"✓ 处理RPC调用消息: {data}") - return True - except Exception as e: - print(f"✗ RPC调用消息处理失败: {e}") - return False - - def _handle_rpc_response(self, data: Dict[str, Any]) -> bool: - """处理RPC响应消息""" - try: - print(f"✓ 处理RPC响应消息: {data}") - return True - except Exception as e: - print(f"✗ RPC响应消息处理失败: {e}") - return False - - def _handle_event(self, data: Dict[str, Any]) -> bool: - """处理事件消息""" - try: - print(f"✓ 处理事件消息: {data}") - return True - except Exception as e: - print(f"✗ 事件消息处理失败: {e}") - return False - - def _handle_chat(self, data: Dict[str, Any]) -> bool: - """处理聊天消息""" - try: - print(f"✓ 处理聊天消息: {data}") - return True - except Exception as e: - print(f"✗ 聊天消息处理失败: {e}") - return False - - def _handle_error(self, data: Dict[str, Any]) -> bool: - """处理错误消息""" - try: - print(f"✗ 处理错误消息: {data}") - - # 触发协议错误回调 - self._trigger_protocol_callback('protocol_error', data) - - return True - except Exception as e: - print(f"✗ 错误消息处理失败: {e}") - return False - - def get_protocol_config(self) -> Dict[str, Any]: - """ - 获取协议配置 - - Returns: - 协议配置字典 - """ - return self.protocol_config.copy() - - def set_protocol_config(self, config: Dict[str, Any]) -> bool: - """ - 设置协议配置 - - Args: - config: 协议配置字典 - - Returns: - 是否设置成功 - """ - try: - self.protocol_config.update(config) - print(f"✓ 协议配置已更新: {self.protocol_config}") - return True - except Exception as e: - print(f"✗ 协议配置设置失败: {e}") - return False - - def get_protocol_stats(self) -> Dict[str, Any]: - """ - 获取协议统计信息 - - Returns: - 协议统计字典 - """ - return self.protocol_stats.copy() - - def reset_protocol_stats(self): - """重置协议统计信息""" - try: - self.protocol_stats = { - 'messages_sent': 0, - 'messages_received': 0, - 'bytes_sent': 0, - 'bytes_received': 0, - 'compressed_messages': 0, - 'checksum_errors': 0, - 'protocol_errors': 0, - 'fragmented_packets': 0 - } - print("✓ 协议统计信息已重置") - except Exception as e: - print(f"✗ 协议统计信息重置失败: {e}") - - def get_protocol_state(self) -> Dict[str, Any]: - """ - 获取协议状态 - - Returns: - 协议状态字典 - """ - return self.protocol_state.copy() - - def _trigger_protocol_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发协议回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.protocol_callbacks: - for callback in self.protocol_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 协议回调执行失败: {e}") - except Exception as e: - print(f"✗ 协议回调触发失败: {e}") - - def register_protocol_callback(self, callback_type: str, callback: callable): - """ - 注册协议回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.protocol_callbacks: - self.protocol_callbacks[callback_type].append(callback) - print(f"✓ 协议回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 协议回调注册失败: {e}") - - def unregister_protocol_callback(self, callback_type: str, callback: callable): - """ - 注销协议回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.protocol_callbacks: - if callback in self.protocol_callbacks[callback_type]: - self.protocol_callbacks[callback_type].remove(callback) - print(f"✓ 协议回调已注销: {callback_type}") - except Exception as e: +""" +协议处理器模块 +负责处理各种网络协议和消息格式 +""" + +import time +import json +import struct +from typing import Dict, Any, List, Optional +import hashlib +import zlib + +class ProtocolHandler: + """ + 协议处理器 + 负责处理各种网络协议和消息格式 + """ + + def __init__(self, plugin): + """ + 初始化协议处理器 + + Args: + plugin: 网络同步插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 协议配置 + self.protocol_config = { + 'version': '1.0', + 'enable_compression': True, + 'compression_level': 6, + 'enable_checksum': True, + 'max_message_size': 65536, # 64KB + 'enable_fragmentation': True, + 'fragment_size': 1024, + 'enable_encryption': False, + 'supported_protocols': ['sync_v1', 'sync_v2'] + } + + # 消息类型定义 + self.message_types = { + 'CONNECT': 1, + 'CONNECT_ACK': 2, + 'DISCONNECT': 3, + 'HEARTBEAT': 4, + 'SYNC_DATA': 5, + 'SYNC_REQUEST': 6, + 'SYNC_RESPONSE': 7, + 'RPC_CALL': 8, + 'RPC_RESPONSE': 9, + 'EVENT': 10, + 'CHAT': 11, + 'ERROR': 12 + } + + # 消息处理器 + self.message_handlers = {} + + # 协议状态 + self.protocol_state = { + 'current_protocol': 'sync_v1', + 'bytes_processed': 0, + 'messages_processed': 0, + 'compression_savings': 0, + 'fragmented_messages': 0 + } + + # 消息片段缓存 + self.message_fragments = {} + + # 协议统计 + self.protocol_stats = { + 'messages_sent': 0, + 'messages_received': 0, + 'bytes_sent': 0, + 'bytes_received': 0, + 'compressed_messages': 0, + 'checksum_errors': 0, + 'protocol_errors': 0, + 'fragmented_packets': 0 + } + + # 回调函数 + self.protocol_callbacks = { + 'message_received': [], + 'message_sent': [], + 'protocol_error': [], + 'message_fragmented': [] + } + + # 时间戳记录 + self.last_protocol_update = 0.0 + self.last_compression_savings = 0.0 + + print("✓ 协议处理器已创建") + + def initialize(self) -> bool: + """ + 初始化协议处理器 + + Returns: + 是否初始化成功 + """ + try: + # 注册默认消息处理器 + self._register_default_handlers() + + self.initialized = True + print("✓ 协议处理器初始化完成") + return True + + except Exception as e: + print(f"✗ 协议处理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用协议处理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 协议处理器未初始化") + return False + + self.enabled = True + print("✓ 协议处理器已启用") + return True + + except Exception as e: + print(f"✗ 协议处理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用协议处理器""" + try: + self.enabled = False + print("✓ 协议处理器已禁用") + + except Exception as e: + print(f"✗ 协议处理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理协议处理器资源""" + try: + self.disable() + self.message_handlers.clear() + self.message_fragments.clear() + self.protocol_callbacks.clear() + self.initialized = False + print("✓ 协议处理器资源已清理") + + except Exception as e: + print(f"✗ 协议处理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新协议处理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.last_protocol_update = current_time + + # 清理过期的消息片段 + self._cleanup_fragments(current_time) + + except Exception as e: + print(f"✗ 协议处理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _register_default_handlers(self): + """注册默认消息处理器""" + try: + self.message_handlers = { + self.message_types['CONNECT']: self._handle_connect, + self.message_types['CONNECT_ACK']: self._handle_connect_ack, + self.message_types['DISCONNECT']: self._handle_disconnect, + self.message_types['HEARTBEAT']: self._handle_heartbeat, + self.message_types['SYNC_DATA']: self._handle_sync_data, + self.message_types['SYNC_REQUEST']: self._handle_sync_request, + self.message_types['SYNC_RESPONSE']: self._handle_sync_response, + self.message_types['RPC_CALL']: self._handle_rpc_call, + self.message_types['RPC_RESPONSE']: self._handle_rpc_response, + self.message_types['EVENT']: self._handle_event, + self.message_types['CHAT']: self._handle_chat, + self.message_types['ERROR']: self._handle_error + } + + print("✓ 默认消息处理器已注册") + except Exception as e: + print(f"✗ 默认消息处理器注册失败: {e}") + + def _cleanup_fragments(self, current_time: float): + """ + 清理过期的消息片段 + + Args: + current_time: 当前时间 + """ + try: + expired_fragments = [ + msg_id for msg_id, fragment_data in self.message_fragments.items() + if current_time - fragment_data.get('timestamp', 0) > 30.0 # 30秒超时 + ] + + for msg_id in expired_fragments: + del self.message_fragments[msg_id] + + except Exception as e: + print(f"✗ 消息片段清理失败: {e}") + + def encode_message(self, message_type: str, data: Dict[str, Any], + compress: bool = None, add_checksum: bool = None) -> Optional[bytes]: + """ + 编码消息 + + Args: + message_type: 消息类型 + data: 消息数据 + compress: 是否压缩 + add_checksum: 是否添加校验和 + + Returns: + 编码后的消息数据或None + """ + try: + # 获取消息类型ID + type_id = self.message_types.get(message_type.upper()) + if type_id is None: + print(f"✗ 未知的消息类型: {message_type}") + return None + + # 序列化数据 + data_json = json.dumps(data, ensure_ascii=False) + data_bytes = data_json.encode('utf-8') + original_size = len(data_bytes) + + # 压缩数据 + compressed = False + if compress is None: + compress = self.protocol_config['enable_compression'] + + if compress and original_size > 256: # 只压缩大于256字节的数据 + compressed_data = zlib.compress(data_bytes, self.protocol_config['compression_level']) + if len(compressed_data) < original_size: + data_bytes = compressed_data + compressed = True + self.protocol_stats['compressed_messages'] += 1 + self.protocol_state['compression_savings'] += original_size - len(compressed_data) + + # 添加校验和 + checksum = b'' + if add_checksum is None: + add_checksum = self.protocol_config['enable_checksum'] + + if add_checksum: + checksum = hashlib.md5(data_bytes).digest() + + # 构造消息头 + timestamp = int(time.time() * 1000) & 0xFFFFFFFF # 32位时间戳 + header = struct.pack('!BBI', type_id, compressed, timestamp) + + # 构造完整消息 + message = header + checksum + data_bytes + + # 更新统计信息 + self.protocol_stats['messages_sent'] += 1 + self.protocol_stats['bytes_sent'] += len(message) + self.protocol_state['bytes_processed'] += len(message) + self.protocol_state['messages_processed'] += 1 + + return message + + except Exception as e: + print(f"✗ 消息编码失败: {e}") + self.protocol_stats['protocol_errors'] += 1 + return None + + def decode_message(self, message_data: bytes) -> Optional[Dict[str, Any]]: + """ + 解码消息 + + Args: + message_data: 消息数据 + + Returns: + 解码后的消息字典或None + """ + try: + if len(message_data) < 6: # 最小消息长度(1字节类型+1字节压缩标志+4字节时间戳) + print("✗ 消息数据太短") + self.protocol_stats['protocol_errors'] += 1 + return None + + # 解析消息头 + header_size = 6 + type_id, compressed_flag, timestamp = struct.unpack('!BBI', message_data[:header_size]) + + # 获取消息类型名称 + message_type = None + for name, id_val in self.message_types.items(): + if id_val == type_id: + message_type = name + break + + if message_type is None: + print(f"✗ 未知的消息类型ID: {type_id}") + self.protocol_stats['protocol_errors'] += 1 + return None + + # 计算校验和偏移 + checksum_offset = header_size + if self.protocol_config['enable_checksum']: + checksum_offset += 16 # MD5校验和长度 + + # 获取数据部分 + data_bytes = message_data[checksum_offset:] + + # 验证校验和 + if self.protocol_config['enable_checksum'] and len(message_data) >= checksum_offset: + expected_checksum = message_data[header_size:checksum_offset] + actual_checksum = hashlib.md5(data_bytes).digest() + if expected_checksum != actual_checksum: + print("✗ 消息校验和错误") + self.protocol_stats['checksum_errors'] += 1 + return None + + # 解压缩数据 + if compressed_flag: + try: + data_bytes = zlib.decompress(data_bytes) + except Exception as e: + print(f"✗ 消息解压缩失败: {e}") + self.protocol_stats['protocol_errors'] += 1 + return None + + # 反序列化数据 + data_json = data_bytes.decode('utf-8') + data = json.loads(data_json) + + # 构造解码结果 + result = { + 'type': message_type, + 'type_id': type_id, + 'compressed': bool(compressed_flag), + 'timestamp': timestamp, + 'data': data + } + + # 更新统计信息 + self.protocol_stats['messages_received'] += 1 + self.protocol_stats['bytes_received'] += len(message_data) + self.protocol_state['bytes_processed'] += len(message_data) + self.protocol_state['messages_processed'] += 1 + + # 触发消息接收回调 + self._trigger_protocol_callback('message_received', result) + + return result + + except Exception as e: + print(f"✗ 消息解码失败: {e}") + self.protocol_stats['protocol_errors'] += 1 + return None + + def fragment_message(self, message_data: bytes, fragment_size: int = None) -> List[bytes]: + """ + 分片消息 + + Args: + message_data: 消息数据 + fragment_size: 分片大小 + + Returns: + 分片后的消息列表 + """ + try: + if fragment_size is None: + fragment_size = self.protocol_config['fragment_size'] + + if len(message_data) <= fragment_size: + return [message_data] + + fragments = [] + message_id = hashlib.md5(message_data).hexdigest()[:8] + total_fragments = (len(message_data) + fragment_size - 1) // fragment_size + + for i in range(total_fragments): + start_pos = i * fragment_size + end_pos = min((i + 1) * fragment_size, len(message_data)) + fragment_data = message_data[start_pos:end_pos] + + # 构造分片头 + fragment_header = struct.pack('!8sHH', + message_id.encode('ascii'), + i, + total_fragments) + + fragments.append(fragment_header + fragment_data) + + self.protocol_stats['fragmented_packets'] += len(fragments) + self.protocol_state['fragmented_messages'] += 1 + + # 触发消息分片回调 + self._trigger_protocol_callback('message_fragmented', { + 'message_id': message_id, + 'total_fragments': total_fragments, + 'fragment_size': fragment_size + }) + + return fragments + + except Exception as e: + print(f"✗ 消息分片失败: {e}") + self.protocol_stats['protocol_errors'] += 1 + return [message_data] + + def reassemble_message(self, fragment_data: bytes) -> Optional[bytes]: + """ + 重组消息 + + Args: + fragment_data: 分片数据 + + Returns: + 重组后的消息数据或None + """ + try: + if len(fragment_data) < 12: # 分片头最小长度 + return None + + # 解析分片头 + message_id_bytes, fragment_index, total_fragments = struct.unpack('!8sHH', fragment_data[:12]) + message_id = message_id_bytes.decode('ascii').rstrip('\x00') + fragment_payload = fragment_data[12:] + + # 初始化分片缓存 + if message_id not in self.message_fragments: + self.message_fragments[message_id] = { + 'fragments': {}, + 'total_fragments': total_fragments, + 'timestamp': time.time() + } + + # 存储分片 + self.message_fragments[message_id]['fragments'][fragment_index] = fragment_payload + + # 检查是否所有分片都已到达 + fragment_cache = self.message_fragments[message_id] + if len(fragment_cache['fragments']) == fragment_cache['total_fragments']: + # 重组消息 + message_parts = [] + for i in range(fragment_cache['total_fragments']): + if i in fragment_cache['fragments']: + message_parts.append(fragment_cache['fragments'][i]) + else: + # 缺少分片 + return None + + # 清理缓存 + del self.message_fragments[message_id] + + # 返回重组后的消息 + return b''.join(message_parts) + + return None + + except Exception as e: + print(f"✗ 消息重组失败: {e}") + self.protocol_stats['protocol_errors'] += 1 + return None + + def send_message(self, message_type: str, data: Dict[str, Any], + target_client_id: str = None) -> bool: + """ + 发送消息 + + Args: + message_type: 消息类型 + data: 消息数据 + target_client_id: 目标客户端ID + + Returns: + 是否发送成功 + """ + try: + if not self.enabled: + print("✗ 协议处理器未启用") + return False + + # 编码消息 + message_bytes = self.encode_message(message_type, data) + if message_bytes is None: + print("✗ 消息编码失败") + return False + + # 检查是否需要分片 + if (self.protocol_config['enable_fragmentation'] and + len(message_bytes) > self.protocol_config['fragment_size']): + fragments = self.fragment_message(message_bytes) + success = True + + # 发送所有分片 + for fragment in fragments: + if not self._send_raw_message(fragment, target_client_id): + success = False + + if success: + # 触发消息发送回调 + self._trigger_protocol_callback('message_sent', { + 'type': message_type, + 'data': data, + 'fragmented': True, + 'fragment_count': len(fragments) + }) + + return success + else: + # 发送完整消息 + if self._send_raw_message(message_bytes, target_client_id): + # 触发消息发送回调 + self._trigger_protocol_callback('message_sent', { + 'type': message_type, + 'data': data, + 'fragmented': False + }) + return True + else: + return False + + except Exception as e: + print(f"✗ 消息发送失败: {e}") + self.protocol_stats['protocol_errors'] += 1 + return False + + def _send_raw_message(self, message_bytes: bytes, target_client_id: str = None) -> bool: + """ + 发送原始消息数据 + + Args: + message_bytes: 消息字节数据 + target_client_id: 目标客户端ID + + Returns: + 是否发送成功 + """ + try: + # 通过网络管理器发送消息 + if self.plugin.network_manager: + # 这里应该调用网络管理器的发送方法 + # 由于是模拟实现,我们只返回True + return True + else: + print("✗ 网络管理器未初始化") + return False + + except Exception as e: + print(f"✗ 原始消息发送失败: {e}") + return False + + def receive_message(self, message_bytes: bytes) -> bool: + """ + 接收消息 + + Args: + message_bytes: 消息字节数据 + + Returns: + 是否处理成功 + """ + try: + if not self.enabled: + print("✗ 协议处理器未启用") + return False + + # 检查是否为分片消息 + if len(message_bytes) >= 12: + # 尝试解析分片头 + try: + message_id_bytes, fragment_index, total_fragments = struct.unpack('!8sHH', message_bytes[:12]) + # 这是一个分片消息 + reassembled_message = self.reassemble_message(message_bytes) + if reassembled_message is not None: + # 分片重组完成,处理完整消息 + return self.receive_message(reassembled_message) + else: + # 等待更多分片 + return True + except struct.error: + # 不是分片消息,继续处理 + pass + + # 解码消息 + decoded_message = self.decode_message(message_bytes) + if decoded_message is None: + print("✗ 消息解码失败") + return False + + # 处理消息 + return self._process_message(decoded_message) + + except Exception as e: + print(f"✗ 消息接收失败: {e}") + self.protocol_stats['protocol_errors'] += 1 + return False + + def _process_message(self, message: Dict[str, Any]) -> bool: + """ + 处理消息 + + Args: + message: 解码后的消息 + + Returns: + 是否处理成功 + """ + try: + message_type_id = message.get('type_id') + message_data = message.get('data', {}) + + # 查找对应的消息处理器 + if message_type_id in self.message_handlers: + handler = self.message_handlers[message_type_id] + return handler(message_data) + else: + print(f"✗ 未找到消息处理器: {message_type_id}") + return False + + except Exception as e: + print(f"✗ 消息处理失败: {e}") + return False + + # 默认消息处理器 + def _handle_connect(self, data: Dict[str, Any]) -> bool: + """处理连接消息""" + try: + print(f"✓ 处理连接消息: {data}") + return True + except Exception as e: + print(f"✗ 连接消息处理失败: {e}") + return False + + def _handle_connect_ack(self, data: Dict[str, Any]) -> bool: + """处理连接确认消息""" + try: + print(f"✓ 处理连接确认消息: {data}") + return True + except Exception as e: + print(f"✗ 连接确认消息处理失败: {e}") + return False + + def _handle_disconnect(self, data: Dict[str, Any]) -> bool: + """处理断开连接消息""" + try: + print(f"✓ 处理断开连接消息: {data}") + return True + except Exception as e: + print(f"✗ 断开连接消息处理失败: {e}") + return False + + def _handle_heartbeat(self, data: Dict[str, Any]) -> bool: + """处理心跳消息""" + try: + print(f"✓ 处理心跳消息: {data}") + return True + except Exception as e: + print(f"✗ 心跳消息处理失败: {e}") + return False + + def _handle_sync_data(self, data: Dict[str, Any]) -> bool: + """处理同步数据消息""" + try: + print(f"✓ 处理同步数据消息: {len(data)}个对象") + + # 传递给对象同步模块 + if self.plugin.object_sync: + for obj_id, obj_data in data.items(): + self.plugin.object_sync.receive_sync_data(obj_id, obj_data) + + return True + except Exception as e: + print(f"✗ 同步数据消息处理失败: {e}") + return False + + def _handle_sync_request(self, data: Dict[str, Any]) -> bool: + """处理同步请求消息""" + try: + print(f"✓ 处理同步请求消息: {data}") + return True + except Exception as e: + print(f"✗ 同步请求消息处理失败: {e}") + return False + + def _handle_sync_response(self, data: Dict[str, Any]) -> bool: + """处理同步响应消息""" + try: + print(f"✓ 处理同步响应消息: {data}") + return True + except Exception as e: + print(f"✗ 同步响应消息处理失败: {e}") + return False + + def _handle_rpc_call(self, data: Dict[str, Any]) -> bool: + """处理RPC调用消息""" + try: + print(f"✓ 处理RPC调用消息: {data}") + return True + except Exception as e: + print(f"✗ RPC调用消息处理失败: {e}") + return False + + def _handle_rpc_response(self, data: Dict[str, Any]) -> bool: + """处理RPC响应消息""" + try: + print(f"✓ 处理RPC响应消息: {data}") + return True + except Exception as e: + print(f"✗ RPC响应消息处理失败: {e}") + return False + + def _handle_event(self, data: Dict[str, Any]) -> bool: + """处理事件消息""" + try: + print(f"✓ 处理事件消息: {data}") + return True + except Exception as e: + print(f"✗ 事件消息处理失败: {e}") + return False + + def _handle_chat(self, data: Dict[str, Any]) -> bool: + """处理聊天消息""" + try: + print(f"✓ 处理聊天消息: {data}") + return True + except Exception as e: + print(f"✗ 聊天消息处理失败: {e}") + return False + + def _handle_error(self, data: Dict[str, Any]) -> bool: + """处理错误消息""" + try: + print(f"✗ 处理错误消息: {data}") + + # 触发协议错误回调 + self._trigger_protocol_callback('protocol_error', data) + + return True + except Exception as e: + print(f"✗ 错误消息处理失败: {e}") + return False + + def get_protocol_config(self) -> Dict[str, Any]: + """ + 获取协议配置 + + Returns: + 协议配置字典 + """ + return self.protocol_config.copy() + + def set_protocol_config(self, config: Dict[str, Any]) -> bool: + """ + 设置协议配置 + + Args: + config: 协议配置字典 + + Returns: + 是否设置成功 + """ + try: + self.protocol_config.update(config) + print(f"✓ 协议配置已更新: {self.protocol_config}") + return True + except Exception as e: + print(f"✗ 协议配置设置失败: {e}") + return False + + def get_protocol_stats(self) -> Dict[str, Any]: + """ + 获取协议统计信息 + + Returns: + 协议统计字典 + """ + return self.protocol_stats.copy() + + def reset_protocol_stats(self): + """重置协议统计信息""" + try: + self.protocol_stats = { + 'messages_sent': 0, + 'messages_received': 0, + 'bytes_sent': 0, + 'bytes_received': 0, + 'compressed_messages': 0, + 'checksum_errors': 0, + 'protocol_errors': 0, + 'fragmented_packets': 0 + } + print("✓ 协议统计信息已重置") + except Exception as e: + print(f"✗ 协议统计信息重置失败: {e}") + + def get_protocol_state(self) -> Dict[str, Any]: + """ + 获取协议状态 + + Returns: + 协议状态字典 + """ + return self.protocol_state.copy() + + def _trigger_protocol_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发协议回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.protocol_callbacks: + for callback in self.protocol_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 协议回调执行失败: {e}") + except Exception as e: + print(f"✗ 协议回调触发失败: {e}") + + def register_protocol_callback(self, callback_type: str, callback: callable): + """ + 注册协议回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.protocol_callbacks: + self.protocol_callbacks[callback_type].append(callback) + print(f"✓ 协议回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 协议回调注册失败: {e}") + + def unregister_protocol_callback(self, callback_type: str, callback: callable): + """ + 注销协议回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.protocol_callbacks: + if callback in self.protocol_callbacks[callback_type]: + self.protocol_callbacks[callback_type].remove(callback) + print(f"✓ 协议回调已注销: {callback_type}") + except Exception as e: print(f"✗ 协议回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/network_sync/serialization/data_serializer.py b/plugins/user/network_sync/serialization/data_serializer.py index 0a7a90e7..40341403 100644 --- a/plugins/user/network_sync/serialization/data_serializer.py +++ b/plugins/user/network_sync/serialization/data_serializer.py @@ -1,837 +1,837 @@ -""" -数据序列化模块 -负责数据的序列化和反序列化 -""" - -import json -import pickle -import struct -from typing import Dict, Any, List, Optional, Union -import zlib -import base64 - -class DataSerializer: - """ - 数据序列化器 - 负责数据的序列化和反序列化 - """ - - def __init__(self, plugin): - """ - 初始化数据序列化器 - - Args: - plugin: 网络同步插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 序列化配置 - self.serialization_config = { - 'default_format': 'json', # json, pickle, binary - 'enable_compression': True, - 'compression_level': 6, - 'enable_base64_encoding': False, # 用于文本传输 - 'enable_delta_encoding': True, # 启用增量编码 - 'max_serialization_depth': 10, - 'enable_type_preservation': True - } - - # 序列化格式支持 - self.supported_formats = { - 'json': self._serialize_json, - 'pickle': self._serialize_pickle, - 'binary': self._serialize_binary - } - - self.deserialization_methods = { - 'json': self._deserialize_json, - 'pickle': self._deserialize_pickle, - 'binary': self._deserialize_binary - } - - # 序列化统计 - self.serialization_stats = { - 'objects_serialized': 0, - 'objects_deserialized': 0, - 'bytes_serialized': 0, - 'bytes_deserialized': 0, - 'compression_savings': 0, - 'serialization_errors': 0, - 'deserialization_errors': 0 - } - - # 缓存管理 - self.serialization_cache = {} - self.cache_max_size = 1000 - self.cache_cleanup_threshold = 0.8 - - # 回调函数 - self.serialization_callbacks = { - 'data_serialized': [], - 'data_deserialized': [], - 'serialization_error': [], - 'deserialization_error': [] - } - - # 时间戳记录 - self.last_serialization = 0.0 - self.last_deserialization = 0.0 - - print("✓ 数据序列化器已创建") - - def initialize(self) -> bool: - """ - 初始化数据序列化器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 数据序列化器初始化完成") - return True - - except Exception as e: - print(f"✗ 数据序列化器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用数据序列化器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 数据序列化器未初始化") - return False - - self.enabled = True - print("✓ 数据序列化器已启用") - return True - - except Exception as e: - print(f"✗ 数据序列化器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用数据序列化器""" - try: - self.enabled = False - print("✓ 数据序列化器已禁用") - - except Exception as e: - print(f"✗ 数据序列化器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理数据序列化器资源""" - try: - self.disable() - self.serialization_cache.clear() - self.serialization_callbacks.clear() - self.initialized = False - print("✓ 数据序列化器资源已清理") - - except Exception as e: - print(f"✗ 数据序列化器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新数据序列化器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 定期清理缓存 - if len(self.serialization_cache) > self.cache_max_size * self.cache_cleanup_threshold: - self._cleanup_cache() - - except Exception as e: - print(f"✗ 数据序列化器更新失败: {e}") - import traceback - traceback.print_exc() - - def _cleanup_cache(self): - """清理序列化缓存""" - try: - # 简单的LRU缓存清理:移除一半最旧的条目 - cache_items = list(self.serialization_cache.items()) - cache_items.sort(key=lambda x: x[1]['timestamp']) - - remove_count = len(cache_items) // 2 - for i in range(remove_count): - del self.serialization_cache[cache_items[i][0]] - - except Exception as e: - print(f"✗ 缓存清理失败: {e}") - - def serialize(self, data: Any, format: str = None, compress: bool = None) -> Optional[bytes]: - """ - 序列化数据 - - Args: - data: 要序列化的数据 - format: 序列化格式 - compress: 是否压缩 - - Returns: - 序列化后的字节数据或None - """ - try: - if not self.enabled: - print("✗ 数据序列化器未启用") - return None - - # 使用默认格式 - if format is None: - format = self.serialization_config['default_format'] - - # 检查是否支持该格式 - if format not in self.supported_formats: - print(f"✗ 不支持的序列化格式: {format}") - self.serialization_stats['serialization_errors'] += 1 - return None - - # 检查缓存 - cache_key = self._generate_cache_key(data, format, compress) - if cache_key in self.serialization_cache: - cached_data = self.serialization_cache[cache_key] - self.serialization_stats['objects_serialized'] += 1 - self.last_serialization = time.time() - - # 触发序列化回调 - self._trigger_serialization_callback('data_serialized', { - 'data': data, - 'format': format, - 'cached': True, - 'size': len(cached_data['data']) - }) - - return cached_data['data'] - - # 执行序列化 - serializer = self.supported_formats[format] - serialized_data = serializer(data) - - if serialized_data is None: - self.serialization_stats['serialization_errors'] += 1 - return None - - # 压缩数据 - if compress is None: - compress = self.serialization_config['enable_compression'] - - if compress: - original_size = len(serialized_data) - serialized_data = zlib.compress(serialized_data, self.serialization_config['compression_level']) - compression_savings = original_size - len(serialized_data) - self.serialization_stats['compression_savings'] += compression_savings - - # Base64编码(如果需要) - if self.serialization_config['enable_base64_encoding']: - serialized_data = base64.b64encode(serialized_data) - - # 缓存结果 - self.serialization_cache[cache_key] = { - 'data': serialized_data, - 'timestamp': time.time(), - 'format': format - } - - # 保持缓存大小 - if len(self.serialization_cache) > self.cache_max_size: - self._cleanup_cache() - - # 更新统计信息 - self.serialization_stats['objects_serialized'] += 1 - self.serialization_stats['bytes_serialized'] += len(serialized_data) - self.last_serialization = time.time() - - # 触发序列化回调 - self._trigger_serialization_callback('data_serialized', { - 'data': data, - 'format': format, - 'cached': False, - 'size': len(serialized_data) - }) - - return serialized_data - - except Exception as e: - print(f"✗ 数据序列化失败: {e}") - self.serialization_stats['serialization_errors'] += 1 - - # 触发序列化错误回调 - self._trigger_serialization_callback('serialization_error', { - 'data': data, - 'error': str(e) - }) - - return None - - def deserialize(self, data: bytes, format: str = None) -> Optional[Any]: - """ - 反序列化数据 - - Args: - data: 要反序列化的字节数据 - format: 序列化格式 - - Returns: - 反序列化后的数据或None - """ - try: - if not self.enabled: - print("✗ 数据序列化器未启用") - return None - - # Base64解码(如果需要) - if self.serialization_config['enable_base64_encoding']: - data = base64.b64decode(data) - - # 使用默认格式 - if format is None: - format = self.serialization_config['default_format'] - - # 检查是否支持该格式 - if format not in self.deserialization_methods: - print(f"✗ 不支持的反序列化格式: {format}") - self.serialization_stats['deserialization_errors'] += 1 - return None - - # 解压缩数据 - try: - decompressed_data = zlib.decompress(data) - data = decompressed_data - except zlib.error: - # 数据未压缩,直接使用 - pass - - # 执行反序列化 - deserializer = self.deserialization_methods[format] - deserialized_data = deserializer(data) - - if deserialized_data is None: - self.serialization_stats['deserialization_errors'] += 1 - return None - - # 更新统计信息 - self.serialization_stats['objects_deserialized'] += 1 - self.serialization_stats['bytes_deserialized'] += len(data) - self.last_deserialization = time.time() - - # 触发反序列化回调 - self._trigger_serialization_callback('data_deserialized', { - 'format': format, - 'size': len(data) - }) - - return deserialized_data - - except Exception as e: - print(f"✗ 数据反序列化失败: {e}") - self.serialization_stats['deserialization_errors'] += 1 - - # 触发反序列化错误回调 - self._trigger_serialization_callback('deserialization_error', { - 'data': data, - 'error': str(e) - }) - - return None - - def _serialize_json(self, data: Any) -> Optional[bytes]: - """ - JSON序列化 - - Args: - data: 要序列化的数据 - - Returns: - 序列化后的字节数据或None - """ - try: - json_str = json.dumps(data, ensure_ascii=False, separators=(',', ':')) - return json_str.encode('utf-8') - except Exception as e: - print(f"✗ JSON序列化失败: {e}") - return None - - def _deserialize_json(self, data: bytes) -> Optional[Any]: - """ - JSON反序列化 - - Args: - data: 要反序列化的字节数据 - - Returns: - 反序列化后的数据或None - """ - try: - json_str = data.decode('utf-8') - return json.loads(json_str) - except Exception as e: - print(f"✗ JSON反序列化失败: {e}") - return None - - def _serialize_pickle(self, data: Any) -> Optional[bytes]: - """ - Pickle序列化 - - Args: - data: 要序列化的数据 - - Returns: - 序列化后的字节数据或None - """ - try: - return pickle.dumps(data) - except Exception as e: - print(f"✗ Pickle序列化失败: {e}") - return None - - def _deserialize_pickle(self, data: bytes) -> Optional[Any]: - """ - Pickle反序列化 - - Args: - data: 要反序列化的字节数据 - - Returns: - 反序列化后的数据或None - """ - try: - return pickle.loads(data) - except Exception as e: - print(f"✗ Pickle反序列化失败: {e}") - return None - - def _serialize_binary(self, data: Any) -> Optional[bytes]: - """ - 二进制序列化 - - Args: - data: 要序列化的数据 - - Returns: - 序列化后的字节数据或None - """ - try: - # 简化的二进制序列化实现 - # 实际应用中可能需要更复杂的实现 - if isinstance(data, dict): - return self._serialize_dict_binary(data) - elif isinstance(data, list): - return self._serialize_list_binary(data) - elif isinstance(data, (int, float, str, bool)): - return self._serialize_primitive_binary(data) - else: - # 对于不支持的类型,回退到pickle - return pickle.dumps(data) - except Exception as e: - print(f"✗ 二进制序列化失败: {e}") - return None - - def _deserialize_binary(self, data: bytes) -> Optional[Any]: - """ - 二进制反序列化 - - Args: - data: 要反序列化的字节数据 - - Returns: - 反序列化后的数据或None - """ - try: - # 简化的二进制反序列化实现 - # 实际应用中可能需要更复杂的实现 - if len(data) < 4: - return None - - # 读取类型标识 - type_id = struct.unpack('!I', data[:4])[0] - - if type_id == 1: # 字典 - return self._deserialize_dict_binary(data) - elif type_id == 2: # 列表 - return self._deserialize_list_binary(data) - elif type_id == 3: # 原始类型 - return self._deserialize_primitive_binary(data) - else: - # 对于不支持的类型,回退到pickle - return pickle.loads(data) - except Exception as e: - print(f"✗ 二进制反序列化失败: {e}") - return None - - def _serialize_dict_binary(self, data: Dict[str, Any]) -> bytes: - """序列化字典为二进制格式""" - try: - # 类型标识 - result = struct.pack('!I', 1) # 字典类型 - - # 键值对数量 - result += struct.pack('!I', len(data)) - - # 序列化每个键值对 - for key, value in data.items(): - # 序列化键 - key_bytes = str(key).encode('utf-8') - result += struct.pack('!I', len(key_bytes)) - result += key_bytes - - # 序列化值 - if isinstance(value, (int, float, str, bool)): - value_bytes = self._serialize_primitive_binary(value) - else: - value_bytes = pickle.dumps(value) - - result += struct.pack('!I', len(value_bytes)) - result += value_bytes - - return result - except Exception as e: - print(f"✗ 字典二进制序列化失败: {e}") - raise - - def _deserialize_dict_binary(self, data: bytes) -> Dict[str, Any]: - """从二进制格式反序列化字典""" - try: - offset = 4 # 跳过类型标识 - - # 读取键值对数量 - count = struct.unpack('!I', data[offset:offset+4])[0] - offset += 4 - - result = {} - for _ in range(count): - # 读取键 - key_len = struct.unpack('!I', data[offset:offset+4])[0] - offset += 4 - key = data[offset:offset+key_len].decode('utf-8') - offset += key_len - - # 读取值 - value_len = struct.unpack('!I', data[offset:offset+4])[0] - offset += 4 - value_bytes = data[offset:offset+value_len] - offset += value_len - - # 反序列化值 - try: - value = self._deserialize_primitive_binary(value_bytes) - except: - value = pickle.loads(value_bytes) - - result[key] = value - - return result - except Exception as e: - print(f"✗ 字典二进制反序列化失败: {e}") - raise - - def _serialize_list_binary(self, data: List[Any]) -> bytes: - """序列化列表为二进制格式""" - try: - # 类型标识 - result = struct.pack('!I', 2) # 列表类型 - - # 元素数量 - result += struct.pack('!I', len(data)) - - # 序列化每个元素 - for item in data: - if isinstance(item, (int, float, str, bool)): - item_bytes = self._serialize_primitive_binary(item) - else: - item_bytes = pickle.dumps(item) - - result += struct.pack('!I', len(item_bytes)) - result += item_bytes - - return result - except Exception as e: - print(f"✗ 列表二进制序列化失败: {e}") - raise - - def _deserialize_list_binary(self, data: bytes) -> List[Any]: - """从二进制格式反序列化列表""" - try: - offset = 4 # 跳过类型标识 - - # 读取元素数量 - count = struct.unpack('!I', data[offset:offset+4])[0] - offset += 4 - - result = [] - for _ in range(count): - # 读取元素 - item_len = struct.unpack('!I', data[offset:offset+4])[0] - offset += 4 - item_bytes = data[offset:offset+item_len] - offset += item_len - - # 反序列化元素 - try: - item = self._deserialize_primitive_binary(item_bytes) - except: - item = pickle.loads(item_bytes) - - result.append(item) - - return result - except Exception as e: - print(f"✗ 列表二进制反序列化失败: {e}") - raise - - def _serialize_primitive_binary(self, data: Union[int, float, str, bool]) -> bytes: - """序列化原始类型为二进制格式""" - try: - # 类型标识 - result = struct.pack('!I', 3) # 原始类型 - - if isinstance(data, bool): - result += struct.pack('!B', 1 if data else 0) - elif isinstance(data, int): - result += struct.pack('!B', 1) # int类型 - result += struct.pack('!q', data) - elif isinstance(data, float): - result += struct.pack('!B', 2) # float类型 - result += struct.pack('!d', data) - elif isinstance(data, str): - result += struct.pack('!B', 3) # string类型 - data_bytes = data.encode('utf-8') - result += struct.pack('!I', len(data_bytes)) - result += data_bytes - else: - raise ValueError(f"Unsupported primitive type: {type(data)}") - - return result - except Exception as e: - print(f"✗ 原始类型二进制序列化失败: {e}") - raise - - def _deserialize_primitive_binary(self, data: bytes) -> Union[int, float, str, bool]: - """从二进制格式反序列化原始类型""" - try: - offset = 4 # 跳过类型标识 - - type_code = struct.unpack('!B', data[offset:offset+1])[0] - offset += 1 - - if type_code == 0: # bool - return struct.unpack('!B', data[offset:offset+1])[0] != 0 - elif type_code == 1: # int - return struct.unpack('!q', data[offset:offset+8])[0] - elif type_code == 2: # float - return struct.unpack('!d', data[offset:offset+8])[0] - elif type_code == 3: # string - str_len = struct.unpack('!I', data[offset:offset+4])[0] - offset += 4 - return data[offset:offset+str_len].decode('utf-8') - else: - raise ValueError(f"Unknown primitive type code: {type_code}") - except Exception as e: - print(f"✗ 原始类型二进制反序列化失败: {e}") - raise - - def _generate_cache_key(self, data: Any, format: str, compress: bool) -> str: - """ - 生成缓存键 - - Args: - data: 数据 - format: 格式 - compress: 是否压缩 - - Returns: - 缓存键 - """ - try: - # 简单的缓存键生成方法 - import hashlib - data_hash = hashlib.md5(str(data).encode('utf-8')).hexdigest() - return f"{data_hash}_{format}_{compress}" - except Exception as e: - print(f"✗ 缓存键生成失败: {e}") - return str(id(data)) - - def delta_encode(self, current_data: Dict[str, Any], - previous_data: Dict[str, Any]) -> Dict[str, Any]: - """ - 增量编码 - - Args: - current_data: 当前数据 - previous_data: 之前的数据 - - Returns: - 增量编码后的数据 - """ - try: - if not self.serialization_config['enable_delta_encoding']: - return current_data - - delta_data = {} - for key, value in current_data.items(): - if key not in previous_data or previous_data[key] != value: - delta_data[key] = value - - return delta_data - - except Exception as e: - print(f"✗ 增量编码失败: {e}") - return current_data - - def delta_decode(self, delta_data: Dict[str, Any], - previous_data: Dict[str, Any]) -> Dict[str, Any]: - """ - 增量解码 - - Args: - delta_data: 增量数据 - previous_data: 之前的数据 - - Returns: - 解码后的完整数据 - """ - try: - if not self.serialization_config['enable_delta_encoding']: - return delta_data - - full_data = previous_data.copy() - full_data.update(delta_data) - return full_data - - except Exception as e: - print(f"✗ 增量解码失败: {e}") - return delta_data - - def get_serialization_stats(self) -> Dict[str, Any]: - """ - 获取序列化统计信息 - - Returns: - 序列化统计字典 - """ - return self.serialization_stats.copy() - - def reset_serialization_stats(self): - """重置序列化统计信息""" - try: - self.serialization_stats = { - 'objects_serialized': 0, - 'objects_deserialized': 0, - 'bytes_serialized': 0, - 'bytes_deserialized': 0, - 'compression_savings': 0, - 'serialization_errors': 0, - 'deserialization_errors': 0 - } - print("✓ 序列化统计信息已重置") - except Exception as e: - print(f"✗ 序列化统计信息重置失败: {e}") - - def set_serialization_config(self, config: Dict[str, Any]) -> bool: - """ - 设置序列化配置 - - Args: - config: 序列化配置字典 - - Returns: - 是否设置成功 - """ - try: - self.serialization_config.update(config) - print(f"✓ 序列化配置已更新: {self.serialization_config}") - return True - except Exception as e: - print(f"✗ 序列化配置设置失败: {e}") - return False - - def get_serialization_config(self) -> Dict[str, Any]: - """ - 获取序列化配置 - - Returns: - 序列化配置字典 - """ - return self.serialization_config.copy() - - def clear_cache(self): - """清空序列化缓存""" - try: - self.serialization_cache.clear() - print("✓ 序列化缓存已清空") - except Exception as e: - print(f"✗ 序列化缓存清空失败: {e}") - - def _trigger_serialization_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发序列化回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.serialization_callbacks: - for callback in self.serialization_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 序列化回调执行失败: {e}") - except Exception as e: - print(f"✗ 序列化回调触发失败: {e}") - - def register_serialization_callback(self, callback_type: str, callback: callable): - """ - 注册序列化回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.serialization_callbacks: - self.serialization_callbacks[callback_type].append(callback) - print(f"✓ 序列化回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 序列化回调注册失败: {e}") - - def unregister_serialization_callback(self, callback_type: str, callback: callable): - """ - 注销序列化回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.serialization_callbacks: - if callback in self.serialization_callbacks[callback_type]: - self.serialization_callbacks[callback_type].remove(callback) - print(f"✓ 序列化回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 序列化回调注销失败: {e}") - -# 导入time模块 +""" +数据序列化模块 +负责数据的序列化和反序列化 +""" + +import json +import pickle +import struct +from typing import Dict, Any, List, Optional, Union +import zlib +import base64 + +class DataSerializer: + """ + 数据序列化器 + 负责数据的序列化和反序列化 + """ + + def __init__(self, plugin): + """ + 初始化数据序列化器 + + Args: + plugin: 网络同步插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 序列化配置 + self.serialization_config = { + 'default_format': 'json', # json, pickle, binary + 'enable_compression': True, + 'compression_level': 6, + 'enable_base64_encoding': False, # 用于文本传输 + 'enable_delta_encoding': True, # 启用增量编码 + 'max_serialization_depth': 10, + 'enable_type_preservation': True + } + + # 序列化格式支持 + self.supported_formats = { + 'json': self._serialize_json, + 'pickle': self._serialize_pickle, + 'binary': self._serialize_binary + } + + self.deserialization_methods = { + 'json': self._deserialize_json, + 'pickle': self._deserialize_pickle, + 'binary': self._deserialize_binary + } + + # 序列化统计 + self.serialization_stats = { + 'objects_serialized': 0, + 'objects_deserialized': 0, + 'bytes_serialized': 0, + 'bytes_deserialized': 0, + 'compression_savings': 0, + 'serialization_errors': 0, + 'deserialization_errors': 0 + } + + # 缓存管理 + self.serialization_cache = {} + self.cache_max_size = 1000 + self.cache_cleanup_threshold = 0.8 + + # 回调函数 + self.serialization_callbacks = { + 'data_serialized': [], + 'data_deserialized': [], + 'serialization_error': [], + 'deserialization_error': [] + } + + # 时间戳记录 + self.last_serialization = 0.0 + self.last_deserialization = 0.0 + + print("✓ 数据序列化器已创建") + + def initialize(self) -> bool: + """ + 初始化数据序列化器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 数据序列化器初始化完成") + return True + + except Exception as e: + print(f"✗ 数据序列化器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用数据序列化器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 数据序列化器未初始化") + return False + + self.enabled = True + print("✓ 数据序列化器已启用") + return True + + except Exception as e: + print(f"✗ 数据序列化器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用数据序列化器""" + try: + self.enabled = False + print("✓ 数据序列化器已禁用") + + except Exception as e: + print(f"✗ 数据序列化器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理数据序列化器资源""" + try: + self.disable() + self.serialization_cache.clear() + self.serialization_callbacks.clear() + self.initialized = False + print("✓ 数据序列化器资源已清理") + + except Exception as e: + print(f"✗ 数据序列化器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新数据序列化器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 定期清理缓存 + if len(self.serialization_cache) > self.cache_max_size * self.cache_cleanup_threshold: + self._cleanup_cache() + + except Exception as e: + print(f"✗ 数据序列化器更新失败: {e}") + import traceback + traceback.print_exc() + + def _cleanup_cache(self): + """清理序列化缓存""" + try: + # 简单的LRU缓存清理:移除一半最旧的条目 + cache_items = list(self.serialization_cache.items()) + cache_items.sort(key=lambda x: x[1]['timestamp']) + + remove_count = len(cache_items) // 2 + for i in range(remove_count): + del self.serialization_cache[cache_items[i][0]] + + except Exception as e: + print(f"✗ 缓存清理失败: {e}") + + def serialize(self, data: Any, format: str = None, compress: bool = None) -> Optional[bytes]: + """ + 序列化数据 + + Args: + data: 要序列化的数据 + format: 序列化格式 + compress: 是否压缩 + + Returns: + 序列化后的字节数据或None + """ + try: + if not self.enabled: + print("✗ 数据序列化器未启用") + return None + + # 使用默认格式 + if format is None: + format = self.serialization_config['default_format'] + + # 检查是否支持该格式 + if format not in self.supported_formats: + print(f"✗ 不支持的序列化格式: {format}") + self.serialization_stats['serialization_errors'] += 1 + return None + + # 检查缓存 + cache_key = self._generate_cache_key(data, format, compress) + if cache_key in self.serialization_cache: + cached_data = self.serialization_cache[cache_key] + self.serialization_stats['objects_serialized'] += 1 + self.last_serialization = time.time() + + # 触发序列化回调 + self._trigger_serialization_callback('data_serialized', { + 'data': data, + 'format': format, + 'cached': True, + 'size': len(cached_data['data']) + }) + + return cached_data['data'] + + # 执行序列化 + serializer = self.supported_formats[format] + serialized_data = serializer(data) + + if serialized_data is None: + self.serialization_stats['serialization_errors'] += 1 + return None + + # 压缩数据 + if compress is None: + compress = self.serialization_config['enable_compression'] + + if compress: + original_size = len(serialized_data) + serialized_data = zlib.compress(serialized_data, self.serialization_config['compression_level']) + compression_savings = original_size - len(serialized_data) + self.serialization_stats['compression_savings'] += compression_savings + + # Base64编码(如果需要) + if self.serialization_config['enable_base64_encoding']: + serialized_data = base64.b64encode(serialized_data) + + # 缓存结果 + self.serialization_cache[cache_key] = { + 'data': serialized_data, + 'timestamp': time.time(), + 'format': format + } + + # 保持缓存大小 + if len(self.serialization_cache) > self.cache_max_size: + self._cleanup_cache() + + # 更新统计信息 + self.serialization_stats['objects_serialized'] += 1 + self.serialization_stats['bytes_serialized'] += len(serialized_data) + self.last_serialization = time.time() + + # 触发序列化回调 + self._trigger_serialization_callback('data_serialized', { + 'data': data, + 'format': format, + 'cached': False, + 'size': len(serialized_data) + }) + + return serialized_data + + except Exception as e: + print(f"✗ 数据序列化失败: {e}") + self.serialization_stats['serialization_errors'] += 1 + + # 触发序列化错误回调 + self._trigger_serialization_callback('serialization_error', { + 'data': data, + 'error': str(e) + }) + + return None + + def deserialize(self, data: bytes, format: str = None) -> Optional[Any]: + """ + 反序列化数据 + + Args: + data: 要反序列化的字节数据 + format: 序列化格式 + + Returns: + 反序列化后的数据或None + """ + try: + if not self.enabled: + print("✗ 数据序列化器未启用") + return None + + # Base64解码(如果需要) + if self.serialization_config['enable_base64_encoding']: + data = base64.b64decode(data) + + # 使用默认格式 + if format is None: + format = self.serialization_config['default_format'] + + # 检查是否支持该格式 + if format not in self.deserialization_methods: + print(f"✗ 不支持的反序列化格式: {format}") + self.serialization_stats['deserialization_errors'] += 1 + return None + + # 解压缩数据 + try: + decompressed_data = zlib.decompress(data) + data = decompressed_data + except zlib.error: + # 数据未压缩,直接使用 + pass + + # 执行反序列化 + deserializer = self.deserialization_methods[format] + deserialized_data = deserializer(data) + + if deserialized_data is None: + self.serialization_stats['deserialization_errors'] += 1 + return None + + # 更新统计信息 + self.serialization_stats['objects_deserialized'] += 1 + self.serialization_stats['bytes_deserialized'] += len(data) + self.last_deserialization = time.time() + + # 触发反序列化回调 + self._trigger_serialization_callback('data_deserialized', { + 'format': format, + 'size': len(data) + }) + + return deserialized_data + + except Exception as e: + print(f"✗ 数据反序列化失败: {e}") + self.serialization_stats['deserialization_errors'] += 1 + + # 触发反序列化错误回调 + self._trigger_serialization_callback('deserialization_error', { + 'data': data, + 'error': str(e) + }) + + return None + + def _serialize_json(self, data: Any) -> Optional[bytes]: + """ + JSON序列化 + + Args: + data: 要序列化的数据 + + Returns: + 序列化后的字节数据或None + """ + try: + json_str = json.dumps(data, ensure_ascii=False, separators=(',', ':')) + return json_str.encode('utf-8') + except Exception as e: + print(f"✗ JSON序列化失败: {e}") + return None + + def _deserialize_json(self, data: bytes) -> Optional[Any]: + """ + JSON反序列化 + + Args: + data: 要反序列化的字节数据 + + Returns: + 反序列化后的数据或None + """ + try: + json_str = data.decode('utf-8') + return json.loads(json_str) + except Exception as e: + print(f"✗ JSON反序列化失败: {e}") + return None + + def _serialize_pickle(self, data: Any) -> Optional[bytes]: + """ + Pickle序列化 + + Args: + data: 要序列化的数据 + + Returns: + 序列化后的字节数据或None + """ + try: + return pickle.dumps(data) + except Exception as e: + print(f"✗ Pickle序列化失败: {e}") + return None + + def _deserialize_pickle(self, data: bytes) -> Optional[Any]: + """ + Pickle反序列化 + + Args: + data: 要反序列化的字节数据 + + Returns: + 反序列化后的数据或None + """ + try: + return pickle.loads(data) + except Exception as e: + print(f"✗ Pickle反序列化失败: {e}") + return None + + def _serialize_binary(self, data: Any) -> Optional[bytes]: + """ + 二进制序列化 + + Args: + data: 要序列化的数据 + + Returns: + 序列化后的字节数据或None + """ + try: + # 简化的二进制序列化实现 + # 实际应用中可能需要更复杂的实现 + if isinstance(data, dict): + return self._serialize_dict_binary(data) + elif isinstance(data, list): + return self._serialize_list_binary(data) + elif isinstance(data, (int, float, str, bool)): + return self._serialize_primitive_binary(data) + else: + # 对于不支持的类型,回退到pickle + return pickle.dumps(data) + except Exception as e: + print(f"✗ 二进制序列化失败: {e}") + return None + + def _deserialize_binary(self, data: bytes) -> Optional[Any]: + """ + 二进制反序列化 + + Args: + data: 要反序列化的字节数据 + + Returns: + 反序列化后的数据或None + """ + try: + # 简化的二进制反序列化实现 + # 实际应用中可能需要更复杂的实现 + if len(data) < 4: + return None + + # 读取类型标识 + type_id = struct.unpack('!I', data[:4])[0] + + if type_id == 1: # 字典 + return self._deserialize_dict_binary(data) + elif type_id == 2: # 列表 + return self._deserialize_list_binary(data) + elif type_id == 3: # 原始类型 + return self._deserialize_primitive_binary(data) + else: + # 对于不支持的类型,回退到pickle + return pickle.loads(data) + except Exception as e: + print(f"✗ 二进制反序列化失败: {e}") + return None + + def _serialize_dict_binary(self, data: Dict[str, Any]) -> bytes: + """序列化字典为二进制格式""" + try: + # 类型标识 + result = struct.pack('!I', 1) # 字典类型 + + # 键值对数量 + result += struct.pack('!I', len(data)) + + # 序列化每个键值对 + for key, value in data.items(): + # 序列化键 + key_bytes = str(key).encode('utf-8') + result += struct.pack('!I', len(key_bytes)) + result += key_bytes + + # 序列化值 + if isinstance(value, (int, float, str, bool)): + value_bytes = self._serialize_primitive_binary(value) + else: + value_bytes = pickle.dumps(value) + + result += struct.pack('!I', len(value_bytes)) + result += value_bytes + + return result + except Exception as e: + print(f"✗ 字典二进制序列化失败: {e}") + raise + + def _deserialize_dict_binary(self, data: bytes) -> Dict[str, Any]: + """从二进制格式反序列化字典""" + try: + offset = 4 # 跳过类型标识 + + # 读取键值对数量 + count = struct.unpack('!I', data[offset:offset+4])[0] + offset += 4 + + result = {} + for _ in range(count): + # 读取键 + key_len = struct.unpack('!I', data[offset:offset+4])[0] + offset += 4 + key = data[offset:offset+key_len].decode('utf-8') + offset += key_len + + # 读取值 + value_len = struct.unpack('!I', data[offset:offset+4])[0] + offset += 4 + value_bytes = data[offset:offset+value_len] + offset += value_len + + # 反序列化值 + try: + value = self._deserialize_primitive_binary(value_bytes) + except: + value = pickle.loads(value_bytes) + + result[key] = value + + return result + except Exception as e: + print(f"✗ 字典二进制反序列化失败: {e}") + raise + + def _serialize_list_binary(self, data: List[Any]) -> bytes: + """序列化列表为二进制格式""" + try: + # 类型标识 + result = struct.pack('!I', 2) # 列表类型 + + # 元素数量 + result += struct.pack('!I', len(data)) + + # 序列化每个元素 + for item in data: + if isinstance(item, (int, float, str, bool)): + item_bytes = self._serialize_primitive_binary(item) + else: + item_bytes = pickle.dumps(item) + + result += struct.pack('!I', len(item_bytes)) + result += item_bytes + + return result + except Exception as e: + print(f"✗ 列表二进制序列化失败: {e}") + raise + + def _deserialize_list_binary(self, data: bytes) -> List[Any]: + """从二进制格式反序列化列表""" + try: + offset = 4 # 跳过类型标识 + + # 读取元素数量 + count = struct.unpack('!I', data[offset:offset+4])[0] + offset += 4 + + result = [] + for _ in range(count): + # 读取元素 + item_len = struct.unpack('!I', data[offset:offset+4])[0] + offset += 4 + item_bytes = data[offset:offset+item_len] + offset += item_len + + # 反序列化元素 + try: + item = self._deserialize_primitive_binary(item_bytes) + except: + item = pickle.loads(item_bytes) + + result.append(item) + + return result + except Exception as e: + print(f"✗ 列表二进制反序列化失败: {e}") + raise + + def _serialize_primitive_binary(self, data: Union[int, float, str, bool]) -> bytes: + """序列化原始类型为二进制格式""" + try: + # 类型标识 + result = struct.pack('!I', 3) # 原始类型 + + if isinstance(data, bool): + result += struct.pack('!B', 1 if data else 0) + elif isinstance(data, int): + result += struct.pack('!B', 1) # int类型 + result += struct.pack('!q', data) + elif isinstance(data, float): + result += struct.pack('!B', 2) # float类型 + result += struct.pack('!d', data) + elif isinstance(data, str): + result += struct.pack('!B', 3) # string类型 + data_bytes = data.encode('utf-8') + result += struct.pack('!I', len(data_bytes)) + result += data_bytes + else: + raise ValueError(f"Unsupported primitive type: {type(data)}") + + return result + except Exception as e: + print(f"✗ 原始类型二进制序列化失败: {e}") + raise + + def _deserialize_primitive_binary(self, data: bytes) -> Union[int, float, str, bool]: + """从二进制格式反序列化原始类型""" + try: + offset = 4 # 跳过类型标识 + + type_code = struct.unpack('!B', data[offset:offset+1])[0] + offset += 1 + + if type_code == 0: # bool + return struct.unpack('!B', data[offset:offset+1])[0] != 0 + elif type_code == 1: # int + return struct.unpack('!q', data[offset:offset+8])[0] + elif type_code == 2: # float + return struct.unpack('!d', data[offset:offset+8])[0] + elif type_code == 3: # string + str_len = struct.unpack('!I', data[offset:offset+4])[0] + offset += 4 + return data[offset:offset+str_len].decode('utf-8') + else: + raise ValueError(f"Unknown primitive type code: {type_code}") + except Exception as e: + print(f"✗ 原始类型二进制反序列化失败: {e}") + raise + + def _generate_cache_key(self, data: Any, format: str, compress: bool) -> str: + """ + 生成缓存键 + + Args: + data: 数据 + format: 格式 + compress: 是否压缩 + + Returns: + 缓存键 + """ + try: + # 简单的缓存键生成方法 + import hashlib + data_hash = hashlib.md5(str(data).encode('utf-8')).hexdigest() + return f"{data_hash}_{format}_{compress}" + except Exception as e: + print(f"✗ 缓存键生成失败: {e}") + return str(id(data)) + + def delta_encode(self, current_data: Dict[str, Any], + previous_data: Dict[str, Any]) -> Dict[str, Any]: + """ + 增量编码 + + Args: + current_data: 当前数据 + previous_data: 之前的数据 + + Returns: + 增量编码后的数据 + """ + try: + if not self.serialization_config['enable_delta_encoding']: + return current_data + + delta_data = {} + for key, value in current_data.items(): + if key not in previous_data or previous_data[key] != value: + delta_data[key] = value + + return delta_data + + except Exception as e: + print(f"✗ 增量编码失败: {e}") + return current_data + + def delta_decode(self, delta_data: Dict[str, Any], + previous_data: Dict[str, Any]) -> Dict[str, Any]: + """ + 增量解码 + + Args: + delta_data: 增量数据 + previous_data: 之前的数据 + + Returns: + 解码后的完整数据 + """ + try: + if not self.serialization_config['enable_delta_encoding']: + return delta_data + + full_data = previous_data.copy() + full_data.update(delta_data) + return full_data + + except Exception as e: + print(f"✗ 增量解码失败: {e}") + return delta_data + + def get_serialization_stats(self) -> Dict[str, Any]: + """ + 获取序列化统计信息 + + Returns: + 序列化统计字典 + """ + return self.serialization_stats.copy() + + def reset_serialization_stats(self): + """重置序列化统计信息""" + try: + self.serialization_stats = { + 'objects_serialized': 0, + 'objects_deserialized': 0, + 'bytes_serialized': 0, + 'bytes_deserialized': 0, + 'compression_savings': 0, + 'serialization_errors': 0, + 'deserialization_errors': 0 + } + print("✓ 序列化统计信息已重置") + except Exception as e: + print(f"✗ 序列化统计信息重置失败: {e}") + + def set_serialization_config(self, config: Dict[str, Any]) -> bool: + """ + 设置序列化配置 + + Args: + config: 序列化配置字典 + + Returns: + 是否设置成功 + """ + try: + self.serialization_config.update(config) + print(f"✓ 序列化配置已更新: {self.serialization_config}") + return True + except Exception as e: + print(f"✗ 序列化配置设置失败: {e}") + return False + + def get_serialization_config(self) -> Dict[str, Any]: + """ + 获取序列化配置 + + Returns: + 序列化配置字典 + """ + return self.serialization_config.copy() + + def clear_cache(self): + """清空序列化缓存""" + try: + self.serialization_cache.clear() + print("✓ 序列化缓存已清空") + except Exception as e: + print(f"✗ 序列化缓存清空失败: {e}") + + def _trigger_serialization_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发序列化回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.serialization_callbacks: + for callback in self.serialization_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 序列化回调执行失败: {e}") + except Exception as e: + print(f"✗ 序列化回调触发失败: {e}") + + def register_serialization_callback(self, callback_type: str, callback: callable): + """ + 注册序列化回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.serialization_callbacks: + self.serialization_callbacks[callback_type].append(callback) + print(f"✓ 序列化回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 序列化回调注册失败: {e}") + + def unregister_serialization_callback(self, callback_type: str, callback: callable): + """ + 注销序列化回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.serialization_callbacks: + if callback in self.serialization_callbacks[callback_type]: + self.serialization_callbacks[callback_type].remove(callback) + print(f"✓ 序列化回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 序列化回调注销失败: {e}") + +# 导入time模块 import time \ No newline at end of file diff --git a/plugins/user/network_sync/sync/object_sync.py b/plugins/user/network_sync/sync/object_sync.py index 02ad842b..40ec77c4 100644 --- a/plugins/user/network_sync/sync/object_sync.py +++ b/plugins/user/network_sync/sync/object_sync.py @@ -1,905 +1,905 @@ -""" -对象同步模块 -负责同步游戏对象的状态 -""" - -import time -from typing import Dict, Any, List, Optional -import threading -import copy - -class ObjectSync: - """ - 对象同步器 - 负责同步游戏对象的状态 - """ - - def __init__(self, plugin): - """ - 初始化对象同步器 - - Args: - plugin: 网络同步插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 同步配置 - self.sync_config = { - 'sync_interval': 0.05, # 50ms同步间隔 - 'interpolation_time': 0.1, # 100ms插值时间 - 'extrapolation_limit': 0.2, # 200ms外推限制 - 'position_threshold': 0.01, # 位置变化阈值 - 'rotation_threshold': 0.1, # 旋转变化阈值 - 'scale_threshold': 0.01, # 缩放变化阈值 - 'enable_delta_compression': True, # 启用增量压缩 - 'enable_interpolation': True, # 启用插值 - 'enable_extrapolation': True, # 启用外推 - 'max_sync_distance': 100.0, # 最大同步距离 - 'sync_area_radius': 50.0 # 同步区域半径 - } - - # 同步对象管理 - self.sync_objects = {} # 所有同步对象 - self.local_objects = {} # 本地对象 - self.remote_objects = {} # 远程对象 - - # 同步状态 - self.sync_state = { - 'last_sync_time': 0.0, - 'objects_synced': 0, - 'sync_operations': 0, - 'interpolations': 0, - 'extrapolations': 0 - } - - # 插值数据 - self.interpolation_data = {} - - # 外推数据 - self.extrapolation_data = {} - - # 对象历史状态(用于插值) - self.object_history = {} - - # 同步统计 - self.sync_stats = { - 'objects_registered': 0, - 'objects_synced': 0, - 'sync_data_sent': 0, - 'sync_data_received': 0, - 'interpolations': 0, - 'extrapolations': 0, - 'sync_errors': 0, - 'delta_compressions': 0 - } - - # 线程锁 - self.sync_lock = threading.RLock() - - # 回调函数 - self.sync_callbacks = { - 'object_registered': [], - 'object_unregistered': [], - 'sync_data_received': [], - 'object_updated': [], - 'sync_error': [] - } - - # 时间戳记录 - self.last_sync_update = 0.0 - self.last_interpolation_update = 0.0 - - print("✓ 对象同步器已创建") - - def initialize(self) -> bool: - """ - 初始化对象同步器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 对象同步器初始化完成") - return True - - except Exception as e: - print(f"✗ 对象同步器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用对象同步器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 对象同步器未初始化") - return False - - self.enabled = True - print("✓ 对象同步器已启用") - return True - - except Exception as e: - print(f"✗ 对象同步器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用对象同步器""" - try: - self.enabled = False - print("✓ 对象同步器已禁用") - - except Exception as e: - print(f"✗ 对象同步器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理对象同步器资源""" - try: - self.disable() - self.sync_objects.clear() - self.local_objects.clear() - self.remote_objects.clear() - self.interpolation_data.clear() - self.extrapolation_data.clear() - self.object_history.clear() - self.sync_callbacks.clear() - self.initialized = False - print("✓ 对象同步器资源已清理") - - except Exception as e: - print(f"✗ 对象同步器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新对象同步器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.last_sync_update = current_time - - # 执行同步操作 - if current_time - self.sync_state['last_sync_time'] >= self.sync_config['sync_interval']: - self._perform_sync() - self.sync_state['last_sync_time'] = current_time - - # 更新插值和外推 - self._update_interpolation(current_time) - self._update_extrapolation(current_time) - - except Exception as e: - print(f"✗ 对象同步器更新失败: {e}") - import traceback - traceback.print_exc() - - def _perform_sync(self): - """执行同步操作""" - try: - # 收集需要同步的本地对象数据 - sync_data = {} - - with self.sync_lock: - for obj_id, obj_data in self.local_objects.items(): - # 检查对象是否需要同步 - if self._should_sync_object(obj_id, obj_data): - sync_data[obj_id] = obj_data.copy() - - # 发送同步数据 - if sync_data and self.plugin.network_manager: - success = self.plugin.network_manager.send_sync_data(sync_data) - if success: - self.sync_stats['sync_data_sent'] += 1 - self.sync_stats['objects_synced'] += len(sync_data) - self.sync_state['sync_operations'] += 1 - else: - self.sync_stats['sync_errors'] += 1 - - except Exception as e: - print(f"✗ 对象同步操作失败: {e}") - self.sync_stats['sync_errors'] += 1 - - def _should_sync_object(self, obj_id: str, obj_data: Dict[str, Any]) -> bool: - """ - 检查对象是否需要同步 - - Args: - obj_id: 对象ID - obj_data: 对象数据 - - Returns: - 是否需要同步 - """ - try: - # 检查是否超出同步距离 - if not self._is_in_sync_range(obj_data): - return False - - # 检查是否有显著变化 - if obj_id in self.sync_objects: - old_data = self.sync_objects[obj_id] - - # 检查位置变化 - if 'position' in obj_data and 'position' in old_data: - pos_diff = self._calculate_distance(obj_data['position'], old_data['position']) - if pos_diff > self.sync_config['position_threshold']: - return True - - # 检查旋转变化 - if 'rotation' in obj_data and 'rotation' in old_data: - rot_diff = self._calculate_rotation_difference(obj_data['rotation'], old_data['rotation']) - if rot_diff > self.sync_config['rotation_threshold']: - return True - - # 检查缩放变化 - if 'scale' in obj_data and 'scale' in old_data: - scale_diff = self._calculate_scale_difference(obj_data['scale'], old_data['scale']) - if scale_diff > self.sync_config['scale_threshold']: - return True - - else: - # 新对象总是需要同步 - return True - - return False - - except Exception as e: - print(f"✗ 对象同步检查失败: {e}") - return True # 出错时默认同步 - - def _is_in_sync_range(self, obj_data: Dict[str, Any]) -> bool: - """ - 检查对象是否在同步范围内 - - Args: - obj_data: 对象数据 - - Returns: - 是否在同步范围内 - """ - try: - # 这里应该根据具体的游戏场景实现 - # 简单实现:假设所有对象都在同步范围内 - return True - except Exception as e: - print(f"✗ 同步范围检查失败: {e}") - return True - - def _calculate_distance(self, pos1: List[float], pos2: List[float]) -> float: - """ - 计算两点间距离 - - Args: - pos1: 位置1 - pos2: 位置2 - - Returns: - 距离 - """ - try: - if len(pos1) >= 3 and len(pos2) >= 3: - dx = pos1[0] - pos2[0] - dy = pos1[1] - pos2[1] - dz = pos1[2] - pos2[2] - return (dx*dx + dy*dy + dz*dz) ** 0.5 - elif len(pos1) >= 2 and len(pos2) >= 2: - dx = pos1[0] - pos2[0] - dy = pos1[1] - pos2[1] - return (dx*dx + dy*dy) ** 0.5 - else: - return 0.0 - except Exception as e: - print(f"✗ 距离计算失败: {e}") - return 0.0 - - def _calculate_rotation_difference(self, rot1: List[float], rot2: List[float]) -> float: - """ - 计算旋转差异 - - Args: - rot1: 旋转1 - rot2: 旋转2 - - Returns: - 旋转差异 - """ - try: - # 简单实现:计算欧拉角差异 - diff = 0.0 - for i in range(min(len(rot1), len(rot2))): - diff += abs(rot1[i] - rot2[i]) - return diff - except Exception as e: - print(f"✗ 旋转差异计算失败: {e}") - return 0.0 - - def _calculate_scale_difference(self, scale1: List[float], scale2: List[float]) -> float: - """ - 计算缩放差异 - - Args: - scale1: 缩放1 - scale2: 缩放2 - - Returns: - 缩放差异 - """ - try: - # 简单实现:计算缩放差异 - diff = 0.0 - for i in range(min(len(scale1), len(scale2))): - diff += abs(scale1[i] - scale2[i]) - return diff - except Exception as e: - print(f"✗ 缩放差异计算失败: {e}") - return 0.0 - - def _update_interpolation(self, current_time: float): - """ - 更新插值 - - Args: - current_time: 当前时间 - """ - try: - if not self.sync_config['enable_interpolation']: - return - - updated_count = 0 - with self.sync_lock: - for obj_id, interp_data in self.interpolation_data.items(): - if self._perform_object_interpolation(obj_id, interp_data, current_time): - updated_count += 1 - - self.sync_stats['interpolations'] += updated_count - - except Exception as e: - print(f"✗ 插值更新失败: {e}") - - def _perform_object_interpolation(self, obj_id: str, interp_data: Dict[str, Any], current_time: float) -> bool: - """ - 执行对象插值 - - Args: - obj_id: 对象ID - interp_data: 插值数据 - current_time: 当前时间 - - Returns: - 是否插值成功 - """ - try: - start_time = interp_data.get('start_time', current_time) - end_time = interp_data.get('end_time', current_time) - duration = end_time - start_time - - if duration > 0: - progress = (current_time - start_time) / duration - progress = max(0.0, min(1.0, progress)) # 限制在0-1之间 - - # 线性插值位置 - if 'start_position' in interp_data and 'end_position' in interp_data: - start_pos = interp_data['start_position'] - end_pos = interp_data['end_position'] - current_pos = [ - start_pos[i] + (end_pos[i] - start_pos[i]) * progress - for i in range(min(len(start_pos), len(end_pos))) - ] - - # 更新对象位置 - if obj_id in self.remote_objects: - self.remote_objects[obj_id]['position'] = current_pos - - # 球面插值旋转(简化为线性插值) - if 'start_rotation' in interp_data and 'end_rotation' in interp_data: - start_rot = interp_data['start_rotation'] - end_rot = interp_data['end_rotation'] - current_rot = [ - start_rot[i] + (end_rot[i] - start_rot[i]) * progress - for i in range(min(len(start_rot), len(end_rot))) - ] - - # 更新对象旋转 - if obj_id in self.remote_objects: - self.remote_objects[obj_id]['rotation'] = current_rot - - # 线性插值缩放 - if 'start_scale' in interp_data and 'end_scale' in interp_data: - start_scale = interp_data['start_scale'] - end_scale = interp_data['end_scale'] - current_scale = [ - start_scale[i] + (end_scale[i] - start_scale[i]) * progress - for i in range(min(len(start_scale), len(end_scale))) - ] - - # 更新对象缩放 - if obj_id in self.remote_objects: - self.remote_objects[obj_id]['scale'] = current_scale - - return True - else: - return False - - except Exception as e: - print(f"✗ 对象插值失败: {e}") - return False - - def _update_extrapolation(self, current_time: float): - """ - 更新外推 - - Args: - current_time: 当前时间 - """ - try: - if not self.sync_config['enable_extrapolation']: - return - - updated_count = 0 - with self.sync_lock: - for obj_id, extrap_data in self.extrapolation_data.items(): - if self._perform_object_extrapolation(obj_id, extrap_data, current_time): - updated_count += 1 - - self.sync_stats['extrapolations'] += updated_count - - except Exception as e: - print(f"✗ 外推更新失败: {e}") - - def _perform_object_extrapolation(self, obj_id: str, extrap_data: Dict[str, Any], current_time: float) -> bool: - """ - 执行对象外推 - - Args: - obj_id: 对象ID - extrap_data: 外推数据 - current_time: 当前时间 - - Returns: - 是否外推成功 - """ - try: - last_update_time = extrap_data.get('last_update_time', current_time) - time_diff = current_time - last_update_time - - # 检查是否超出外推限制 - if time_diff > self.sync_config['extrapolation_limit']: - return False - - # 基于速度的外推 - if 'position' in extrap_data and 'velocity' in extrap_data: - position = extrap_data['position'] - velocity = extrap_data['velocity'] - - # 计算外推位置 - extrapolated_pos = [ - position[i] + velocity[i] * time_diff - for i in range(min(len(position), len(velocity))) - ] - - # 更新对象位置 - if obj_id in self.remote_objects: - self.remote_objects[obj_id]['position'] = extrapolated_pos - - return True - - except Exception as e: - print(f"✗ 对象外推失败: {e}") - return False - - def register_object(self, obj_id: str, obj_data: Dict[str, Any], is_local: bool = True) -> bool: - """ - 注册同步对象 - - Args: - obj_id: 对象ID - obj_data: 对象数据 - is_local: 是否为本地对象 - - Returns: - 是否注册成功 - """ - try: - with self.sync_lock: - # 存储对象数据 - self.sync_objects[obj_id] = obj_data.copy() - - if is_local: - self.local_objects[obj_id] = obj_data.copy() - else: - self.remote_objects[obj_id] = obj_data.copy() - - # 初始化对象历史 - self.object_history[obj_id] = { - 'states': [obj_data.copy()], - 'timestamps': [time.time()] - } - - # 更新统计信息 - self.sync_stats['objects_registered'] += 1 - - # 触发对象注册回调 - self._trigger_sync_callback('object_registered', { - 'object_id': obj_id, - 'is_local': is_local, - 'data': obj_data - }) - - print(f"✓ 对象已注册: {obj_id} ({'本地' if is_local else '远程'})") - return True - - except Exception as e: - print(f"✗ 对象注册失败: {e}") - self.sync_stats['sync_errors'] += 1 - return False - - def unregister_object(self, obj_id: str) -> bool: - """ - 注销同步对象 - - Args: - obj_id: 对象ID - - Returns: - 是否注销成功 - """ - try: - with self.sync_lock: - # 从各个字典中移除对象 - if obj_id in self.sync_objects: - del self.sync_objects[obj_id] - - if obj_id in self.local_objects: - del self.local_objects[obj_id] - - if obj_id in self.remote_objects: - del self.remote_objects[obj_id] - - if obj_id in self.interpolation_data: - del self.interpolation_data[obj_id] - - if obj_id in self.extrapolation_data: - del self.extrapolation_data[obj_id] - - if obj_id in self.object_history: - del self.object_history[obj_id] - - # 触发对象注销回调 - self._trigger_sync_callback('object_unregistered', { - 'object_id': obj_id - }) - - print(f"✓ 对象已注销: {obj_id}") - return True - - except Exception as e: - print(f"✗ 对象注销失败: {e}") - self.sync_stats['sync_errors'] += 1 - return False - - def update_object(self, obj_id: str, obj_data: Dict[str, Any]) -> bool: - """ - 更新对象数据 - - Args: - obj_id: 对象ID - obj_data: 对象数据 - - Returns: - 是否更新成功 - """ - try: - with self.sync_lock: - # 更新同步对象 - if obj_id in self.sync_objects: - self.sync_objects[obj_id].update(obj_data) - - # 更新本地对象 - if obj_id in self.local_objects: - self.local_objects[obj_id].update(obj_data) - - # 添加到对象历史(用于插值) - if obj_id in self.object_history: - history = self.object_history[obj_id] - history['states'].append(obj_data.copy()) - history['timestamps'].append(time.time()) - - # 保持历史记录大小 - if len(history['states']) > 10: # 保持最近10个状态 - history['states'].pop(0) - history['timestamps'].pop(0) - - # 触发对象更新回调 - self._trigger_sync_callback('object_updated', { - 'object_id': obj_id, - 'data': obj_data - }) - - return True - - except Exception as e: - print(f"✗ 对象更新失败: {e}") - self.sync_stats['sync_errors'] += 1 - return False - - def sync_object(self, obj_id: str, obj_data: Dict[str, Any]) -> bool: - """ - 同步对象 - - Args: - obj_id: 对象ID - obj_data: 对象数据 - - Returns: - 是否同步成功 - """ - try: - if not self.enabled: - print("✗ 对象同步器未启用") - return False - - # 更新对象数据 - success = self.update_object(obj_id, obj_data) - - if success: - self.sync_stats['objects_synced'] += 1 - return True - else: - self.sync_stats['sync_errors'] += 1 - return False - - except Exception as e: - print(f"✗ 对象同步失败: {e}") - self.sync_stats['sync_errors'] += 1 - return False - - def receive_sync_data(self, obj_id: str, obj_data: Dict[str, Any], client_id: str = None) -> bool: - """ - 接收同步数据 - - Args: - obj_id: 对象ID - obj_data: 对象数据 - client_id: 客户端ID(可选) - - Returns: - 是否接收成功 - """ - try: - with self.sync_lock: - # 存储远程对象数据 - self.remote_objects[obj_id] = obj_data.copy() - self.sync_objects[obj_id] = obj_data.copy() - - # 设置插值数据 - current_time = time.time() - if obj_id in self.interpolation_data: - # 更新现有插值数据 - interp_data = self.interpolation_data[obj_id] - interp_data['start_time'] = current_time - interp_data['end_time'] = current_time + self.sync_config['interpolation_time'] - interp_data['start_position'] = interp_data.get('end_position', obj_data.get('position', [0, 0, 0])) - interp_data['end_position'] = obj_data.get('position', [0, 0, 0]) - interp_data['start_rotation'] = interp_data.get('end_rotation', obj_data.get('rotation', [0, 0, 0])) - interp_data['end_rotation'] = obj_data.get('rotation', [0, 0, 0]) - interp_data['start_scale'] = interp_data.get('end_scale', obj_data.get('scale', [1, 1, 1])) - interp_data['end_scale'] = obj_data.get('scale', [1, 1, 1]) - else: - # 创建新的插值数据 - self.interpolation_data[obj_id] = { - 'start_time': current_time, - 'end_time': current_time + self.sync_config['interpolation_time'], - 'start_position': obj_data.get('position', [0, 0, 0]), - 'end_position': obj_data.get('position', [0, 0, 0]), - 'start_rotation': obj_data.get('rotation', [0, 0, 0]), - 'end_rotation': obj_data.get('rotation', [0, 0, 0]), - 'start_scale': obj_data.get('scale', [1, 1, 1]), - 'end_scale': obj_data.get('scale', [1, 1, 1]) - } - - # 设置外推数据 - self.extrapolation_data[obj_id] = { - 'position': obj_data.get('position', [0, 0, 0]), - 'velocity': obj_data.get('velocity', [0, 0, 0]), - 'last_update_time': current_time - } - - # 更新对象历史 - if obj_id in self.object_history: - history = self.object_history[obj_id] - history['states'].append(obj_data.copy()) - history['timestamps'].append(current_time) - - # 保持历史记录大小 - if len(history['states']) > 10: - history['states'].pop(0) - history['timestamps'].pop(0) - else: - self.object_history[obj_id] = { - 'states': [obj_data.copy()], - 'timestamps': [current_time] - } - - # 更新统计信息 - self.sync_stats['sync_data_received'] += 1 - - # 触发同步数据接收回调 - self._trigger_sync_callback('sync_data_received', { - 'object_id': obj_id, - 'data': obj_data, - 'client_id': client_id - }) - - return True - - except Exception as e: - print(f"✗ 同步数据接收失败: {e}") - self.sync_stats['sync_errors'] += 1 - return False - - def get_object_data(self, obj_id: str) -> Optional[Dict[str, Any]]: - """ - 获取对象数据 - - Args: - obj_id: 对象ID - - Returns: - 对象数据或None - """ - try: - with self.sync_lock: - return self.sync_objects.get(obj_id, {}).copy() - except Exception as e: - print(f"✗ 对象数据获取失败: {e}") - return None - - def get_local_objects(self) -> Dict[str, Dict[str, Any]]: - """ - 获取所有本地对象 - - Returns: - 本地对象字典 - """ - try: - with self.sync_lock: - return {k: v.copy() for k, v in self.local_objects.items()} - except Exception as e: - print(f"✗ 本地对象获取失败: {e}") - return {} - - def get_remote_objects(self) -> Dict[str, Dict[str, Any]]: - """ - 获取所有远程对象 - - Returns: - 远程对象字典 - """ - try: - with self.sync_lock: - return {k: v.copy() for k, v in self.remote_objects.items()} - except Exception as e: - print(f"✗ 远程对象获取失败: {e}") - return {} - - def get_sync_stats(self) -> Dict[str, Any]: - """ - 获取同步统计信息 - - Returns: - 同步统计字典 - """ - return self.sync_stats.copy() - - def reset_sync_stats(self): - """重置同步统计信息""" - try: - self.sync_stats = { - 'objects_registered': 0, - 'objects_synced': 0, - 'sync_data_sent': 0, - 'sync_data_received': 0, - 'interpolations': 0, - 'extrapolations': 0, - 'sync_errors': 0, - 'delta_compressions': 0 - } - print("✓ 对象同步统计信息已重置") - except Exception as e: - print(f"✗ 对象同步统计信息重置失败: {e}") - - def set_sync_config(self, config: Dict[str, Any]) -> bool: - """ - 设置同步配置 - - Args: - config: 同步配置字典 - - Returns: - 是否设置成功 - """ - try: - self.sync_config.update(config) - print(f"✓ 同步配置已更新: {self.sync_config}") - return True - except Exception as e: - print(f"✗ 同步配置设置失败: {e}") - return False - - def get_sync_config(self) -> Dict[str, Any]: - """ - 获取同步配置 - - Returns: - 同步配置字典 - """ - return self.sync_config.copy() - - def _trigger_sync_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发同步回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.sync_callbacks: - for callback in self.sync_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 同步回调执行失败: {e}") - except Exception as e: - print(f"✗ 同步回调触发失败: {e}") - - def register_sync_callback(self, callback_type: str, callback: callable): - """ - 注册同步回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.sync_callbacks: - self.sync_callbacks[callback_type].append(callback) - print(f"✓ 同步回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 同步回调注册失败: {e}") - - def unregister_sync_callback(self, callback_type: str, callback: callable): - """ - 注销同步回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.sync_callbacks: - if callback in self.sync_callbacks[callback_type]: - self.sync_callbacks[callback_type].remove(callback) - print(f"✓ 同步回调已注销: {callback_type}") - except Exception as e: +""" +对象同步模块 +负责同步游戏对象的状态 +""" + +import time +from typing import Dict, Any, List, Optional +import threading +import copy + +class ObjectSync: + """ + 对象同步器 + 负责同步游戏对象的状态 + """ + + def __init__(self, plugin): + """ + 初始化对象同步器 + + Args: + plugin: 网络同步插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 同步配置 + self.sync_config = { + 'sync_interval': 0.05, # 50ms同步间隔 + 'interpolation_time': 0.1, # 100ms插值时间 + 'extrapolation_limit': 0.2, # 200ms外推限制 + 'position_threshold': 0.01, # 位置变化阈值 + 'rotation_threshold': 0.1, # 旋转变化阈值 + 'scale_threshold': 0.01, # 缩放变化阈值 + 'enable_delta_compression': True, # 启用增量压缩 + 'enable_interpolation': True, # 启用插值 + 'enable_extrapolation': True, # 启用外推 + 'max_sync_distance': 100.0, # 最大同步距离 + 'sync_area_radius': 50.0 # 同步区域半径 + } + + # 同步对象管理 + self.sync_objects = {} # 所有同步对象 + self.local_objects = {} # 本地对象 + self.remote_objects = {} # 远程对象 + + # 同步状态 + self.sync_state = { + 'last_sync_time': 0.0, + 'objects_synced': 0, + 'sync_operations': 0, + 'interpolations': 0, + 'extrapolations': 0 + } + + # 插值数据 + self.interpolation_data = {} + + # 外推数据 + self.extrapolation_data = {} + + # 对象历史状态(用于插值) + self.object_history = {} + + # 同步统计 + self.sync_stats = { + 'objects_registered': 0, + 'objects_synced': 0, + 'sync_data_sent': 0, + 'sync_data_received': 0, + 'interpolations': 0, + 'extrapolations': 0, + 'sync_errors': 0, + 'delta_compressions': 0 + } + + # 线程锁 + self.sync_lock = threading.RLock() + + # 回调函数 + self.sync_callbacks = { + 'object_registered': [], + 'object_unregistered': [], + 'sync_data_received': [], + 'object_updated': [], + 'sync_error': [] + } + + # 时间戳记录 + self.last_sync_update = 0.0 + self.last_interpolation_update = 0.0 + + print("✓ 对象同步器已创建") + + def initialize(self) -> bool: + """ + 初始化对象同步器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 对象同步器初始化完成") + return True + + except Exception as e: + print(f"✗ 对象同步器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用对象同步器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 对象同步器未初始化") + return False + + self.enabled = True + print("✓ 对象同步器已启用") + return True + + except Exception as e: + print(f"✗ 对象同步器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用对象同步器""" + try: + self.enabled = False + print("✓ 对象同步器已禁用") + + except Exception as e: + print(f"✗ 对象同步器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理对象同步器资源""" + try: + self.disable() + self.sync_objects.clear() + self.local_objects.clear() + self.remote_objects.clear() + self.interpolation_data.clear() + self.extrapolation_data.clear() + self.object_history.clear() + self.sync_callbacks.clear() + self.initialized = False + print("✓ 对象同步器资源已清理") + + except Exception as e: + print(f"✗ 对象同步器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新对象同步器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.last_sync_update = current_time + + # 执行同步操作 + if current_time - self.sync_state['last_sync_time'] >= self.sync_config['sync_interval']: + self._perform_sync() + self.sync_state['last_sync_time'] = current_time + + # 更新插值和外推 + self._update_interpolation(current_time) + self._update_extrapolation(current_time) + + except Exception as e: + print(f"✗ 对象同步器更新失败: {e}") + import traceback + traceback.print_exc() + + def _perform_sync(self): + """执行同步操作""" + try: + # 收集需要同步的本地对象数据 + sync_data = {} + + with self.sync_lock: + for obj_id, obj_data in self.local_objects.items(): + # 检查对象是否需要同步 + if self._should_sync_object(obj_id, obj_data): + sync_data[obj_id] = obj_data.copy() + + # 发送同步数据 + if sync_data and self.plugin.network_manager: + success = self.plugin.network_manager.send_sync_data(sync_data) + if success: + self.sync_stats['sync_data_sent'] += 1 + self.sync_stats['objects_synced'] += len(sync_data) + self.sync_state['sync_operations'] += 1 + else: + self.sync_stats['sync_errors'] += 1 + + except Exception as e: + print(f"✗ 对象同步操作失败: {e}") + self.sync_stats['sync_errors'] += 1 + + def _should_sync_object(self, obj_id: str, obj_data: Dict[str, Any]) -> bool: + """ + 检查对象是否需要同步 + + Args: + obj_id: 对象ID + obj_data: 对象数据 + + Returns: + 是否需要同步 + """ + try: + # 检查是否超出同步距离 + if not self._is_in_sync_range(obj_data): + return False + + # 检查是否有显著变化 + if obj_id in self.sync_objects: + old_data = self.sync_objects[obj_id] + + # 检查位置变化 + if 'position' in obj_data and 'position' in old_data: + pos_diff = self._calculate_distance(obj_data['position'], old_data['position']) + if pos_diff > self.sync_config['position_threshold']: + return True + + # 检查旋转变化 + if 'rotation' in obj_data and 'rotation' in old_data: + rot_diff = self._calculate_rotation_difference(obj_data['rotation'], old_data['rotation']) + if rot_diff > self.sync_config['rotation_threshold']: + return True + + # 检查缩放变化 + if 'scale' in obj_data and 'scale' in old_data: + scale_diff = self._calculate_scale_difference(obj_data['scale'], old_data['scale']) + if scale_diff > self.sync_config['scale_threshold']: + return True + + else: + # 新对象总是需要同步 + return True + + return False + + except Exception as e: + print(f"✗ 对象同步检查失败: {e}") + return True # 出错时默认同步 + + def _is_in_sync_range(self, obj_data: Dict[str, Any]) -> bool: + """ + 检查对象是否在同步范围内 + + Args: + obj_data: 对象数据 + + Returns: + 是否在同步范围内 + """ + try: + # 这里应该根据具体的游戏场景实现 + # 简单实现:假设所有对象都在同步范围内 + return True + except Exception as e: + print(f"✗ 同步范围检查失败: {e}") + return True + + def _calculate_distance(self, pos1: List[float], pos2: List[float]) -> float: + """ + 计算两点间距离 + + Args: + pos1: 位置1 + pos2: 位置2 + + Returns: + 距离 + """ + try: + if len(pos1) >= 3 and len(pos2) >= 3: + dx = pos1[0] - pos2[0] + dy = pos1[1] - pos2[1] + dz = pos1[2] - pos2[2] + return (dx*dx + dy*dy + dz*dz) ** 0.5 + elif len(pos1) >= 2 and len(pos2) >= 2: + dx = pos1[0] - pos2[0] + dy = pos1[1] - pos2[1] + return (dx*dx + dy*dy) ** 0.5 + else: + return 0.0 + except Exception as e: + print(f"✗ 距离计算失败: {e}") + return 0.0 + + def _calculate_rotation_difference(self, rot1: List[float], rot2: List[float]) -> float: + """ + 计算旋转差异 + + Args: + rot1: 旋转1 + rot2: 旋转2 + + Returns: + 旋转差异 + """ + try: + # 简单实现:计算欧拉角差异 + diff = 0.0 + for i in range(min(len(rot1), len(rot2))): + diff += abs(rot1[i] - rot2[i]) + return diff + except Exception as e: + print(f"✗ 旋转差异计算失败: {e}") + return 0.0 + + def _calculate_scale_difference(self, scale1: List[float], scale2: List[float]) -> float: + """ + 计算缩放差异 + + Args: + scale1: 缩放1 + scale2: 缩放2 + + Returns: + 缩放差异 + """ + try: + # 简单实现:计算缩放差异 + diff = 0.0 + for i in range(min(len(scale1), len(scale2))): + diff += abs(scale1[i] - scale2[i]) + return diff + except Exception as e: + print(f"✗ 缩放差异计算失败: {e}") + return 0.0 + + def _update_interpolation(self, current_time: float): + """ + 更新插值 + + Args: + current_time: 当前时间 + """ + try: + if not self.sync_config['enable_interpolation']: + return + + updated_count = 0 + with self.sync_lock: + for obj_id, interp_data in self.interpolation_data.items(): + if self._perform_object_interpolation(obj_id, interp_data, current_time): + updated_count += 1 + + self.sync_stats['interpolations'] += updated_count + + except Exception as e: + print(f"✗ 插值更新失败: {e}") + + def _perform_object_interpolation(self, obj_id: str, interp_data: Dict[str, Any], current_time: float) -> bool: + """ + 执行对象插值 + + Args: + obj_id: 对象ID + interp_data: 插值数据 + current_time: 当前时间 + + Returns: + 是否插值成功 + """ + try: + start_time = interp_data.get('start_time', current_time) + end_time = interp_data.get('end_time', current_time) + duration = end_time - start_time + + if duration > 0: + progress = (current_time - start_time) / duration + progress = max(0.0, min(1.0, progress)) # 限制在0-1之间 + + # 线性插值位置 + if 'start_position' in interp_data and 'end_position' in interp_data: + start_pos = interp_data['start_position'] + end_pos = interp_data['end_position'] + current_pos = [ + start_pos[i] + (end_pos[i] - start_pos[i]) * progress + for i in range(min(len(start_pos), len(end_pos))) + ] + + # 更新对象位置 + if obj_id in self.remote_objects: + self.remote_objects[obj_id]['position'] = current_pos + + # 球面插值旋转(简化为线性插值) + if 'start_rotation' in interp_data and 'end_rotation' in interp_data: + start_rot = interp_data['start_rotation'] + end_rot = interp_data['end_rotation'] + current_rot = [ + start_rot[i] + (end_rot[i] - start_rot[i]) * progress + for i in range(min(len(start_rot), len(end_rot))) + ] + + # 更新对象旋转 + if obj_id in self.remote_objects: + self.remote_objects[obj_id]['rotation'] = current_rot + + # 线性插值缩放 + if 'start_scale' in interp_data and 'end_scale' in interp_data: + start_scale = interp_data['start_scale'] + end_scale = interp_data['end_scale'] + current_scale = [ + start_scale[i] + (end_scale[i] - start_scale[i]) * progress + for i in range(min(len(start_scale), len(end_scale))) + ] + + # 更新对象缩放 + if obj_id in self.remote_objects: + self.remote_objects[obj_id]['scale'] = current_scale + + return True + else: + return False + + except Exception as e: + print(f"✗ 对象插值失败: {e}") + return False + + def _update_extrapolation(self, current_time: float): + """ + 更新外推 + + Args: + current_time: 当前时间 + """ + try: + if not self.sync_config['enable_extrapolation']: + return + + updated_count = 0 + with self.sync_lock: + for obj_id, extrap_data in self.extrapolation_data.items(): + if self._perform_object_extrapolation(obj_id, extrap_data, current_time): + updated_count += 1 + + self.sync_stats['extrapolations'] += updated_count + + except Exception as e: + print(f"✗ 外推更新失败: {e}") + + def _perform_object_extrapolation(self, obj_id: str, extrap_data: Dict[str, Any], current_time: float) -> bool: + """ + 执行对象外推 + + Args: + obj_id: 对象ID + extrap_data: 外推数据 + current_time: 当前时间 + + Returns: + 是否外推成功 + """ + try: + last_update_time = extrap_data.get('last_update_time', current_time) + time_diff = current_time - last_update_time + + # 检查是否超出外推限制 + if time_diff > self.sync_config['extrapolation_limit']: + return False + + # 基于速度的外推 + if 'position' in extrap_data and 'velocity' in extrap_data: + position = extrap_data['position'] + velocity = extrap_data['velocity'] + + # 计算外推位置 + extrapolated_pos = [ + position[i] + velocity[i] * time_diff + for i in range(min(len(position), len(velocity))) + ] + + # 更新对象位置 + if obj_id in self.remote_objects: + self.remote_objects[obj_id]['position'] = extrapolated_pos + + return True + + except Exception as e: + print(f"✗ 对象外推失败: {e}") + return False + + def register_object(self, obj_id: str, obj_data: Dict[str, Any], is_local: bool = True) -> bool: + """ + 注册同步对象 + + Args: + obj_id: 对象ID + obj_data: 对象数据 + is_local: 是否为本地对象 + + Returns: + 是否注册成功 + """ + try: + with self.sync_lock: + # 存储对象数据 + self.sync_objects[obj_id] = obj_data.copy() + + if is_local: + self.local_objects[obj_id] = obj_data.copy() + else: + self.remote_objects[obj_id] = obj_data.copy() + + # 初始化对象历史 + self.object_history[obj_id] = { + 'states': [obj_data.copy()], + 'timestamps': [time.time()] + } + + # 更新统计信息 + self.sync_stats['objects_registered'] += 1 + + # 触发对象注册回调 + self._trigger_sync_callback('object_registered', { + 'object_id': obj_id, + 'is_local': is_local, + 'data': obj_data + }) + + print(f"✓ 对象已注册: {obj_id} ({'本地' if is_local else '远程'})") + return True + + except Exception as e: + print(f"✗ 对象注册失败: {e}") + self.sync_stats['sync_errors'] += 1 + return False + + def unregister_object(self, obj_id: str) -> bool: + """ + 注销同步对象 + + Args: + obj_id: 对象ID + + Returns: + 是否注销成功 + """ + try: + with self.sync_lock: + # 从各个字典中移除对象 + if obj_id in self.sync_objects: + del self.sync_objects[obj_id] + + if obj_id in self.local_objects: + del self.local_objects[obj_id] + + if obj_id in self.remote_objects: + del self.remote_objects[obj_id] + + if obj_id in self.interpolation_data: + del self.interpolation_data[obj_id] + + if obj_id in self.extrapolation_data: + del self.extrapolation_data[obj_id] + + if obj_id in self.object_history: + del self.object_history[obj_id] + + # 触发对象注销回调 + self._trigger_sync_callback('object_unregistered', { + 'object_id': obj_id + }) + + print(f"✓ 对象已注销: {obj_id}") + return True + + except Exception as e: + print(f"✗ 对象注销失败: {e}") + self.sync_stats['sync_errors'] += 1 + return False + + def update_object(self, obj_id: str, obj_data: Dict[str, Any]) -> bool: + """ + 更新对象数据 + + Args: + obj_id: 对象ID + obj_data: 对象数据 + + Returns: + 是否更新成功 + """ + try: + with self.sync_lock: + # 更新同步对象 + if obj_id in self.sync_objects: + self.sync_objects[obj_id].update(obj_data) + + # 更新本地对象 + if obj_id in self.local_objects: + self.local_objects[obj_id].update(obj_data) + + # 添加到对象历史(用于插值) + if obj_id in self.object_history: + history = self.object_history[obj_id] + history['states'].append(obj_data.copy()) + history['timestamps'].append(time.time()) + + # 保持历史记录大小 + if len(history['states']) > 10: # 保持最近10个状态 + history['states'].pop(0) + history['timestamps'].pop(0) + + # 触发对象更新回调 + self._trigger_sync_callback('object_updated', { + 'object_id': obj_id, + 'data': obj_data + }) + + return True + + except Exception as e: + print(f"✗ 对象更新失败: {e}") + self.sync_stats['sync_errors'] += 1 + return False + + def sync_object(self, obj_id: str, obj_data: Dict[str, Any]) -> bool: + """ + 同步对象 + + Args: + obj_id: 对象ID + obj_data: 对象数据 + + Returns: + 是否同步成功 + """ + try: + if not self.enabled: + print("✗ 对象同步器未启用") + return False + + # 更新对象数据 + success = self.update_object(obj_id, obj_data) + + if success: + self.sync_stats['objects_synced'] += 1 + return True + else: + self.sync_stats['sync_errors'] += 1 + return False + + except Exception as e: + print(f"✗ 对象同步失败: {e}") + self.sync_stats['sync_errors'] += 1 + return False + + def receive_sync_data(self, obj_id: str, obj_data: Dict[str, Any], client_id: str = None) -> bool: + """ + 接收同步数据 + + Args: + obj_id: 对象ID + obj_data: 对象数据 + client_id: 客户端ID(可选) + + Returns: + 是否接收成功 + """ + try: + with self.sync_lock: + # 存储远程对象数据 + self.remote_objects[obj_id] = obj_data.copy() + self.sync_objects[obj_id] = obj_data.copy() + + # 设置插值数据 + current_time = time.time() + if obj_id in self.interpolation_data: + # 更新现有插值数据 + interp_data = self.interpolation_data[obj_id] + interp_data['start_time'] = current_time + interp_data['end_time'] = current_time + self.sync_config['interpolation_time'] + interp_data['start_position'] = interp_data.get('end_position', obj_data.get('position', [0, 0, 0])) + interp_data['end_position'] = obj_data.get('position', [0, 0, 0]) + interp_data['start_rotation'] = interp_data.get('end_rotation', obj_data.get('rotation', [0, 0, 0])) + interp_data['end_rotation'] = obj_data.get('rotation', [0, 0, 0]) + interp_data['start_scale'] = interp_data.get('end_scale', obj_data.get('scale', [1, 1, 1])) + interp_data['end_scale'] = obj_data.get('scale', [1, 1, 1]) + else: + # 创建新的插值数据 + self.interpolation_data[obj_id] = { + 'start_time': current_time, + 'end_time': current_time + self.sync_config['interpolation_time'], + 'start_position': obj_data.get('position', [0, 0, 0]), + 'end_position': obj_data.get('position', [0, 0, 0]), + 'start_rotation': obj_data.get('rotation', [0, 0, 0]), + 'end_rotation': obj_data.get('rotation', [0, 0, 0]), + 'start_scale': obj_data.get('scale', [1, 1, 1]), + 'end_scale': obj_data.get('scale', [1, 1, 1]) + } + + # 设置外推数据 + self.extrapolation_data[obj_id] = { + 'position': obj_data.get('position', [0, 0, 0]), + 'velocity': obj_data.get('velocity', [0, 0, 0]), + 'last_update_time': current_time + } + + # 更新对象历史 + if obj_id in self.object_history: + history = self.object_history[obj_id] + history['states'].append(obj_data.copy()) + history['timestamps'].append(current_time) + + # 保持历史记录大小 + if len(history['states']) > 10: + history['states'].pop(0) + history['timestamps'].pop(0) + else: + self.object_history[obj_id] = { + 'states': [obj_data.copy()], + 'timestamps': [current_time] + } + + # 更新统计信息 + self.sync_stats['sync_data_received'] += 1 + + # 触发同步数据接收回调 + self._trigger_sync_callback('sync_data_received', { + 'object_id': obj_id, + 'data': obj_data, + 'client_id': client_id + }) + + return True + + except Exception as e: + print(f"✗ 同步数据接收失败: {e}") + self.sync_stats['sync_errors'] += 1 + return False + + def get_object_data(self, obj_id: str) -> Optional[Dict[str, Any]]: + """ + 获取对象数据 + + Args: + obj_id: 对象ID + + Returns: + 对象数据或None + """ + try: + with self.sync_lock: + return self.sync_objects.get(obj_id, {}).copy() + except Exception as e: + print(f"✗ 对象数据获取失败: {e}") + return None + + def get_local_objects(self) -> Dict[str, Dict[str, Any]]: + """ + 获取所有本地对象 + + Returns: + 本地对象字典 + """ + try: + with self.sync_lock: + return {k: v.copy() for k, v in self.local_objects.items()} + except Exception as e: + print(f"✗ 本地对象获取失败: {e}") + return {} + + def get_remote_objects(self) -> Dict[str, Dict[str, Any]]: + """ + 获取所有远程对象 + + Returns: + 远程对象字典 + """ + try: + with self.sync_lock: + return {k: v.copy() for k, v in self.remote_objects.items()} + except Exception as e: + print(f"✗ 远程对象获取失败: {e}") + return {} + + def get_sync_stats(self) -> Dict[str, Any]: + """ + 获取同步统计信息 + + Returns: + 同步统计字典 + """ + return self.sync_stats.copy() + + def reset_sync_stats(self): + """重置同步统计信息""" + try: + self.sync_stats = { + 'objects_registered': 0, + 'objects_synced': 0, + 'sync_data_sent': 0, + 'sync_data_received': 0, + 'interpolations': 0, + 'extrapolations': 0, + 'sync_errors': 0, + 'delta_compressions': 0 + } + print("✓ 对象同步统计信息已重置") + except Exception as e: + print(f"✗ 对象同步统计信息重置失败: {e}") + + def set_sync_config(self, config: Dict[str, Any]) -> bool: + """ + 设置同步配置 + + Args: + config: 同步配置字典 + + Returns: + 是否设置成功 + """ + try: + self.sync_config.update(config) + print(f"✓ 同步配置已更新: {self.sync_config}") + return True + except Exception as e: + print(f"✗ 同步配置设置失败: {e}") + return False + + def get_sync_config(self) -> Dict[str, Any]: + """ + 获取同步配置 + + Returns: + 同步配置字典 + """ + return self.sync_config.copy() + + def _trigger_sync_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发同步回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.sync_callbacks: + for callback in self.sync_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 同步回调执行失败: {e}") + except Exception as e: + print(f"✗ 同步回调触发失败: {e}") + + def register_sync_callback(self, callback_type: str, callback: callable): + """ + 注册同步回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.sync_callbacks: + self.sync_callbacks[callback_type].append(callback) + print(f"✓ 同步回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 同步回调注册失败: {e}") + + def unregister_sync_callback(self, callback_type: str, callback: callable): + """ + 注销同步回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.sync_callbacks: + if callback in self.sync_callbacks[callback_type]: + self.sync_callbacks[callback_type].remove(callback) + print(f"✓ 同步回调已注销: {callback_type}") + except Exception as e: print(f"✗ 同步回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/network_sync/utils/network_utils.py b/plugins/user/network_sync/utils/network_utils.py index 06081706..fd4d5158 100644 --- a/plugins/user/network_sync/utils/network_utils.py +++ b/plugins/user/network_sync/utils/network_utils.py @@ -1,730 +1,730 @@ -""" -网络工具类模块 -提供网络同步插件所需的工具函数 -""" - -import time -import hashlib -import zlib -import base64 -from typing import Dict, Any, List, Optional, Union -import json -import struct -import threading - -class NetworkUtils: - """ - 网络工具类 - 提供网络同步插件所需的工具函数 - """ - - def __init__(self, plugin): - """ - 初始化网络工具类 - - Args: - plugin: 网络同步插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 工具配置 - self.utils_config = { - "enable_caching": True, - "cache_size": 1000, - "enable_compression": True, - "compression_level": 6, - "enable_checksum": True, - "max_retry_attempts": 3, - "retry_delay": 1.0 - } - - # 缓存管理 - self.cache = {} - self.cache_lock = threading.RLock() - - # 性能统计 - self.utils_stats = { - "cache_hits": 0, - "cache_misses": 0, - "compressions": 0, - "decompressions": 0, - "checksums_calculated": 0, - "data_processed": 0, - "functions_called": 0 - } - - # 回调函数 - self.utils_callbacks = { - "cache_updated": [], - "performance_alert": [] - } - - # 时间戳记录 - self.last_cache_cleanup = 0.0 - self.last_utility_call = 0.0 - - print("✓ 网络工具类已创建") - - def initialize(self) -> bool: - """ - 初始化网络工具类 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 网络工具类初始化完成") - return True - - except Exception as e: - print(f"✗ 网络工具类初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用网络工具类 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 网络工具类未初始化") - return False - - self.enabled = True - print("✓ 网络工具类已启用") - return True - - except Exception as e: - print(f"✗ 网络工具类启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用网络工具类""" - try: - self.enabled = False - print("✓ 网络工具类已禁用") - - except Exception as e: - print(f"✗ 网络工具类禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理网络工具类资源""" - try: - self.disable() - self.cache.clear() - self.utils_callbacks.clear() - self.initialized = False - print("✓ 网络工具类资源已清理") - - except Exception as e: - print(f"✗ 网络工具类资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新网络工具类状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.last_utility_call = current_time - - # 定期清理缓存 - if current_time - self.last_cache_cleanup > 60.0: # 每分钟清理一次 - self._cleanup_cache() - self.last_cache_cleanup = current_time - - except Exception as e: - print(f"✗ 网络工具类更新失败: {e}") - import traceback - traceback.print_exc() - - def _cleanup_cache(self): - """清理缓存""" - try: - with self.cache_lock: - if len(self.cache) > self.utils_config["cache_size"]: - # 移除最旧的一半缓存项 - items_to_remove = len(self.cache) - (self.utils_config["cache_size"] // 2) - keys_to_remove = list(self.cache.keys())[:items_to_remove] - - for key in keys_to_remove: - del self.cache[key] - - print(f"✓ 缓存已清理,移除了 {items_to_remove} 项") - - except Exception as e: - print(f"✗ 缓存清理失败: {e}") - - def calculate_checksum(self, data: bytes) -> str: - """ - 计算数据校验和 - - Args: - data: 要计算校验和的数据 - - Returns: - 校验和字符串 - """ - try: - self.utils_stats["functions_called"] += 1 - self.utils_stats["checksums_calculated"] += 1 - - checksum = hashlib.md5(data).hexdigest() - return checksum - - except Exception as e: - print(f"✗ 校验和计算失败: {e}") - return "" - - def compress_data(self, data: bytes) -> Optional[bytes]: - """ - 压缩数据 - - Args: - data: 要压缩的数据 - - Returns: - 压缩后的数据或None - """ - try: - if not self.utils_config["enable_compression"]: - return data - - self.utils_stats["functions_called"] += 1 - self.utils_stats["compressions"] += 1 - - compressed_data = zlib.compress(data, self.utils_config["compression_level"]) - self.utils_stats["data_processed"] += len(data) - len(compressed_data) - - return compressed_data - - except Exception as e: - print(f"✗ 数据压缩失败: {e}") - return None - - def decompress_data(self, data: bytes) -> Optional[bytes]: - """ - 解压缩数据 - - Args: - data: 要解压缩的数据 - - Returns: - 解压缩后的数据或None - """ - try: - self.utils_stats["functions_called"] += 1 - self.utils_stats["decompressions"] += 1 - - decompressed_data = zlib.decompress(data) - return decompressed_data - - except Exception as e: - print(f"✗ 数据解压缩失败: {e}") - return None - - def encode_base64(self, data: bytes) -> str: - """ - Base64编码 - - Args: - data: 要编码的数据 - - Returns: - 编码后的字符串 - """ - try: - self.utils_stats["functions_called"] += 1 - - encoded_data = base64.b64encode(data).decode('utf-8') - return encoded_data - - except Exception as e: - print(f"✗ Base64编码失败: {e}") - return "" - - def decode_base64(self, data: str) -> Optional[bytes]: - """ - Base64解码 - - Args: - data: 要解码的字符串 - - Returns: - 解码后的数据或None - """ - try: - self.utils_stats["functions_called"] += 1 - - decoded_data = base64.b64decode(data.encode('utf-8')) - return decoded_data - - except Exception as e: - print(f"✗ Base64解码失败: {e}") - return None - - def generate_cache_key(self, data: Any) -> str: - """ - 生成缓存键 - - Args: - data: 要生成键的数据 - - Returns: - 缓存键字符串 - """ - try: - self.utils_stats["functions_called"] += 1 - - # 生成数据的哈希值作为缓存键 - data_str = str(data) - cache_key = hashlib.sha256(data_str.encode('utf-8')).hexdigest() - return cache_key - - except Exception as e: - print(f"✗ 缓存键生成失败: {e}") - return str(hash(data)) - - def cache_get(self, key: str) -> Optional[Any]: - """ - 从缓存获取数据 - - Args: - key: 缓存键 - - Returns: - 缓存数据或None - """ - try: - if not self.utils_config["enable_caching"]: - return None - - self.utils_stats["functions_called"] += 1 - - with self.cache_lock: - if key in self.cache: - self.utils_stats["cache_hits"] += 1 - return self.cache[key]["data"] - else: - self.utils_stats["cache_misses"] += 1 - return None - - except Exception as e: - print(f"✗ 缓存获取失败: {e}") - return None - - def cache_set(self, key: str, data: Any, ttl: float = 300.0) -> bool: - """ - 设置缓存数据 - - Args: - key: 缓存键 - data: 要缓存的数据 - ttl: 生存时间(秒) - - Returns: - 是否设置成功 - """ - try: - if not self.utils_config["enable_caching"]: - return False - - self.utils_stats["functions_called"] += 1 - - with self.cache_lock: - self.cache[key] = { - "data": data, - "timestamp": time.time(), - "ttl": ttl - } - - # 触发缓存更新回调 - self._trigger_utils_callback("cache_updated", { - "key": key, - "operation": "set" - }) - - return True - - except Exception as e: - print(f"✗ 缓存设置失败: {e}") - return False - - def cache_delete(self, key: str) -> bool: - """ - 删除缓存数据 - - Args: - key: 缓存键 - - Returns: - 是否删除成功 - """ - try: - self.utils_stats["functions_called"] += 1 - - with self.cache_lock: - if key in self.cache: - del self.cache[key] - - # 触发缓存更新回调 - self._trigger_utils_callback("cache_updated", { - "key": key, - "operation": "delete" - }) - - return True - else: - return False - - except Exception as e: - print(f"✗ 缓存删除失败: {e}") - return False - - def retry_operation(self, operation: callable, *args, **kwargs) -> Optional[Any]: - """ - 重试操作 - - Args: - operation: 要重试的操作 - *args: 操作参数 - **kwargs: 操作关键字参数 - - Returns: - 操作结果或None - """ - try: - self.utils_stats["functions_called"] += 1 - - max_attempts = self.utils_config["max_retry_attempts"] - delay = self.utils_config["retry_delay"] - - for attempt in range(max_attempts): - try: - result = operation(*args, **kwargs) - return result - except Exception as e: - if attempt < max_attempts - 1: - print(f"✗ 操作失败,{delay}秒后重试 ({attempt + 1}/{max_attempts}): {e}") - time.sleep(delay) - else: - print(f"✗ 操作最终失败: {e}") - return None - - except Exception as e: - print(f"✗ 重试操作失败: {e}") - return None - - def format_bytes(self, bytes_count: int) -> str: - """ - 格式化字节数 - - Args: - bytes_count: 字节数 - - Returns: - 格式化后的字符串 - """ - try: - self.utils_stats["functions_called"] += 1 - - if bytes_count < 1024: - return f"{bytes_count} B" - elif bytes_count < 1024 * 1024: - return f"{bytes_count / 1024:.2f} KB" - elif bytes_count < 1024 * 1024 * 1024: - return f"{bytes_count / (1024 * 1024):.2f} MB" - else: - return f"{bytes_count / (1024 * 1024 * 1024):.2f} GB" - - except Exception as e: - print(f"✗ 字节格式化失败: {e}") - return f"{bytes_count} B" - - def calculate_bandwidth(self, bytes_count: int, duration: float) -> float: - """ - 计算带宽 - - Args: - bytes_count: 字节数 - duration: 持续时间(秒) - - Returns: - 带宽(B/s) - """ - try: - self.utils_stats["functions_called"] += 1 - - if duration <= 0: - return 0.0 - - bandwidth = bytes_count / duration - return bandwidth - - except Exception as e: - print(f"✗ 带宽计算失败: {e}") - return 0.0 - - def get_local_ip(self) -> Optional[str]: - """ - 获取本地IP地址 - - Returns: - 本地IP地址或None - """ - try: - self.utils_stats["functions_called"] += 1 - - import socket - # 获取本地IP地址 - with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: - # 连接到一个远程地址(不需要真实连接) - s.connect(("8.8.8.8", 80)) - local_ip = s.getsockname()[0] - - return local_ip - - except Exception as e: - print(f"✗ 本地IP获取失败: {e}") - return None - - def is_valid_port(self, port: int) -> bool: - """ - 检查端口是否有效 - - Args: - port: 端口号 - - Returns: - 端口是否有效 - """ - try: - self.utils_stats["functions_called"] += 1 - - return 1 <= port <= 65535 - - except Exception as e: - print(f"✗ 端口检查失败: {e}") - return False - - def get_system_info(self) -> Dict[str, Any]: - """ - 获取系统信息 - - Returns: - 系统信息字典 - """ - try: - self.utils_stats["functions_called"] += 1 - - import platform - import psutil - - system_info = { - "platform": platform.system(), - "platform_version": platform.version(), - "architecture": platform.architecture()[0], - "processor": platform.processor(), - "cpu_count": psutil.cpu_count(), - "memory_total": psutil.virtual_memory().total, - "memory_available": psutil.virtual_memory().available - } - - return system_info - - except Exception as e: - print(f"✗ 系统信息获取失败: {e}") - return {} - - def get_network_interfaces(self) -> List[Dict[str, Any]]: - """ - 获取网络接口信息 - - Returns: - 网络接口信息列表 - """ - try: - self.utils_stats["functions_called"] += 1 - - import psutil - - interfaces = [] - net_if_addrs = psutil.net_if_addrs() - - for interface_name, interface_addresses in net_if_addrs.items(): - interface_info = { - "name": interface_name, - "addresses": [] - } - - for address in interface_addresses: - interface_info["addresses"].append({ - "family": str(address.family), - "address": address.address, - "netmask": address.netmask, - "broadcast": address.broadcast - }) - - interfaces.append(interface_info) - - return interfaces - - except Exception as e: - print(f"✗ 网络接口信息获取失败: {e}") - return [] - - def measure_latency(self, host: str, port: int, timeout: float = 5.0) -> Optional[float]: - """ - 测量网络延迟 - - Args: - host: 主机地址 - port: 端口号 - timeout: 超时时间 - - Returns: - 延迟时间(秒)或None - """ - try: - self.utils_stats["functions_called"] += 1 - - import socket - import time - - start_time = time.time() - - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: - sock.settimeout(timeout) - result = sock.connect_ex((host, port)) - - if result == 0: - end_time = time.time() - latency = end_time - start_time - return latency - else: - return None - - except Exception as e: - print(f"✗ 延迟测量失败: {e}") - return None - - def get_utils_stats(self) -> Dict[str, Any]: - """ - 获取工具统计信息 - - Returns: - 工具统计字典 - """ - return self.utils_stats.copy() - - def reset_utils_stats(self): - """重置工具统计信息""" - try: - self.utils_stats = { - "cache_hits": 0, - "cache_misses": 0, - "compressions": 0, - "decompressions": 0, - "checksums_calculated": 0, - "data_processed": 0, - "functions_called": 0 - } - print("✓ 工具统计信息已重置") - except Exception as e: - print(f"✗ 工具统计信息重置失败: {e}") - - def set_utils_config(self, config: Dict[str, Any]) -> bool: - """ - 设置工具配置 - - Args: - config: 工具配置字典 - - Returns: - 是否设置成功 - """ - try: - self.utils_config.update(config) - print(f"✓ 工具配置已更新: {self.utils_config}") - return True - except Exception as e: - print(f"✗ 工具配置设置失败: {e}") - return False - - def get_utils_config(self) -> Dict[str, Any]: - """ - 获取工具配置 - - Returns: - 工具配置字典 - """ - return self.utils_config.copy() - - def _trigger_utils_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发工具回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.utils_callbacks: - for callback in self.utils_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 工具回调执行失败: {e}") - except Exception as e: - print(f"✗ 工具回调触发失败: {e}") - - def register_utils_callback(self, callback_type: str, callback: callable): - """ - 注册工具回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.utils_callbacks: - self.utils_callbacks[callback_type].append(callback) - print(f"✓ 工具回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 工具回调注册失败: {e}") - - def unregister_utils_callback(self, callback_type: str, callback: callable): - """ - 注销工具回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.utils_callbacks: - if callback in self.utils_callbacks[callback_type]: - self.utils_callbacks[callback_type].remove(callback) - print(f"✓ 工具回调已注销: {callback_type}") - except Exception as e: +""" +网络工具类模块 +提供网络同步插件所需的工具函数 +""" + +import time +import hashlib +import zlib +import base64 +from typing import Dict, Any, List, Optional, Union +import json +import struct +import threading + +class NetworkUtils: + """ + 网络工具类 + 提供网络同步插件所需的工具函数 + """ + + def __init__(self, plugin): + """ + 初始化网络工具类 + + Args: + plugin: 网络同步插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 工具配置 + self.utils_config = { + "enable_caching": True, + "cache_size": 1000, + "enable_compression": True, + "compression_level": 6, + "enable_checksum": True, + "max_retry_attempts": 3, + "retry_delay": 1.0 + } + + # 缓存管理 + self.cache = {} + self.cache_lock = threading.RLock() + + # 性能统计 + self.utils_stats = { + "cache_hits": 0, + "cache_misses": 0, + "compressions": 0, + "decompressions": 0, + "checksums_calculated": 0, + "data_processed": 0, + "functions_called": 0 + } + + # 回调函数 + self.utils_callbacks = { + "cache_updated": [], + "performance_alert": [] + } + + # 时间戳记录 + self.last_cache_cleanup = 0.0 + self.last_utility_call = 0.0 + + print("✓ 网络工具类已创建") + + def initialize(self) -> bool: + """ + 初始化网络工具类 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 网络工具类初始化完成") + return True + + except Exception as e: + print(f"✗ 网络工具类初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用网络工具类 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 网络工具类未初始化") + return False + + self.enabled = True + print("✓ 网络工具类已启用") + return True + + except Exception as e: + print(f"✗ 网络工具类启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用网络工具类""" + try: + self.enabled = False + print("✓ 网络工具类已禁用") + + except Exception as e: + print(f"✗ 网络工具类禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理网络工具类资源""" + try: + self.disable() + self.cache.clear() + self.utils_callbacks.clear() + self.initialized = False + print("✓ 网络工具类资源已清理") + + except Exception as e: + print(f"✗ 网络工具类资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新网络工具类状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.last_utility_call = current_time + + # 定期清理缓存 + if current_time - self.last_cache_cleanup > 60.0: # 每分钟清理一次 + self._cleanup_cache() + self.last_cache_cleanup = current_time + + except Exception as e: + print(f"✗ 网络工具类更新失败: {e}") + import traceback + traceback.print_exc() + + def _cleanup_cache(self): + """清理缓存""" + try: + with self.cache_lock: + if len(self.cache) > self.utils_config["cache_size"]: + # 移除最旧的一半缓存项 + items_to_remove = len(self.cache) - (self.utils_config["cache_size"] // 2) + keys_to_remove = list(self.cache.keys())[:items_to_remove] + + for key in keys_to_remove: + del self.cache[key] + + print(f"✓ 缓存已清理,移除了 {items_to_remove} 项") + + except Exception as e: + print(f"✗ 缓存清理失败: {e}") + + def calculate_checksum(self, data: bytes) -> str: + """ + 计算数据校验和 + + Args: + data: 要计算校验和的数据 + + Returns: + 校验和字符串 + """ + try: + self.utils_stats["functions_called"] += 1 + self.utils_stats["checksums_calculated"] += 1 + + checksum = hashlib.md5(data).hexdigest() + return checksum + + except Exception as e: + print(f"✗ 校验和计算失败: {e}") + return "" + + def compress_data(self, data: bytes) -> Optional[bytes]: + """ + 压缩数据 + + Args: + data: 要压缩的数据 + + Returns: + 压缩后的数据或None + """ + try: + if not self.utils_config["enable_compression"]: + return data + + self.utils_stats["functions_called"] += 1 + self.utils_stats["compressions"] += 1 + + compressed_data = zlib.compress(data, self.utils_config["compression_level"]) + self.utils_stats["data_processed"] += len(data) - len(compressed_data) + + return compressed_data + + except Exception as e: + print(f"✗ 数据压缩失败: {e}") + return None + + def decompress_data(self, data: bytes) -> Optional[bytes]: + """ + 解压缩数据 + + Args: + data: 要解压缩的数据 + + Returns: + 解压缩后的数据或None + """ + try: + self.utils_stats["functions_called"] += 1 + self.utils_stats["decompressions"] += 1 + + decompressed_data = zlib.decompress(data) + return decompressed_data + + except Exception as e: + print(f"✗ 数据解压缩失败: {e}") + return None + + def encode_base64(self, data: bytes) -> str: + """ + Base64编码 + + Args: + data: 要编码的数据 + + Returns: + 编码后的字符串 + """ + try: + self.utils_stats["functions_called"] += 1 + + encoded_data = base64.b64encode(data).decode('utf-8') + return encoded_data + + except Exception as e: + print(f"✗ Base64编码失败: {e}") + return "" + + def decode_base64(self, data: str) -> Optional[bytes]: + """ + Base64解码 + + Args: + data: 要解码的字符串 + + Returns: + 解码后的数据或None + """ + try: + self.utils_stats["functions_called"] += 1 + + decoded_data = base64.b64decode(data.encode('utf-8')) + return decoded_data + + except Exception as e: + print(f"✗ Base64解码失败: {e}") + return None + + def generate_cache_key(self, data: Any) -> str: + """ + 生成缓存键 + + Args: + data: 要生成键的数据 + + Returns: + 缓存键字符串 + """ + try: + self.utils_stats["functions_called"] += 1 + + # 生成数据的哈希值作为缓存键 + data_str = str(data) + cache_key = hashlib.sha256(data_str.encode('utf-8')).hexdigest() + return cache_key + + except Exception as e: + print(f"✗ 缓存键生成失败: {e}") + return str(hash(data)) + + def cache_get(self, key: str) -> Optional[Any]: + """ + 从缓存获取数据 + + Args: + key: 缓存键 + + Returns: + 缓存数据或None + """ + try: + if not self.utils_config["enable_caching"]: + return None + + self.utils_stats["functions_called"] += 1 + + with self.cache_lock: + if key in self.cache: + self.utils_stats["cache_hits"] += 1 + return self.cache[key]["data"] + else: + self.utils_stats["cache_misses"] += 1 + return None + + except Exception as e: + print(f"✗ 缓存获取失败: {e}") + return None + + def cache_set(self, key: str, data: Any, ttl: float = 300.0) -> bool: + """ + 设置缓存数据 + + Args: + key: 缓存键 + data: 要缓存的数据 + ttl: 生存时间(秒) + + Returns: + 是否设置成功 + """ + try: + if not self.utils_config["enable_caching"]: + return False + + self.utils_stats["functions_called"] += 1 + + with self.cache_lock: + self.cache[key] = { + "data": data, + "timestamp": time.time(), + "ttl": ttl + } + + # 触发缓存更新回调 + self._trigger_utils_callback("cache_updated", { + "key": key, + "operation": "set" + }) + + return True + + except Exception as e: + print(f"✗ 缓存设置失败: {e}") + return False + + def cache_delete(self, key: str) -> bool: + """ + 删除缓存数据 + + Args: + key: 缓存键 + + Returns: + 是否删除成功 + """ + try: + self.utils_stats["functions_called"] += 1 + + with self.cache_lock: + if key in self.cache: + del self.cache[key] + + # 触发缓存更新回调 + self._trigger_utils_callback("cache_updated", { + "key": key, + "operation": "delete" + }) + + return True + else: + return False + + except Exception as e: + print(f"✗ 缓存删除失败: {e}") + return False + + def retry_operation(self, operation: callable, *args, **kwargs) -> Optional[Any]: + """ + 重试操作 + + Args: + operation: 要重试的操作 + *args: 操作参数 + **kwargs: 操作关键字参数 + + Returns: + 操作结果或None + """ + try: + self.utils_stats["functions_called"] += 1 + + max_attempts = self.utils_config["max_retry_attempts"] + delay = self.utils_config["retry_delay"] + + for attempt in range(max_attempts): + try: + result = operation(*args, **kwargs) + return result + except Exception as e: + if attempt < max_attempts - 1: + print(f"✗ 操作失败,{delay}秒后重试 ({attempt + 1}/{max_attempts}): {e}") + time.sleep(delay) + else: + print(f"✗ 操作最终失败: {e}") + return None + + except Exception as e: + print(f"✗ 重试操作失败: {e}") + return None + + def format_bytes(self, bytes_count: int) -> str: + """ + 格式化字节数 + + Args: + bytes_count: 字节数 + + Returns: + 格式化后的字符串 + """ + try: + self.utils_stats["functions_called"] += 1 + + if bytes_count < 1024: + return f"{bytes_count} B" + elif bytes_count < 1024 * 1024: + return f"{bytes_count / 1024:.2f} KB" + elif bytes_count < 1024 * 1024 * 1024: + return f"{bytes_count / (1024 * 1024):.2f} MB" + else: + return f"{bytes_count / (1024 * 1024 * 1024):.2f} GB" + + except Exception as e: + print(f"✗ 字节格式化失败: {e}") + return f"{bytes_count} B" + + def calculate_bandwidth(self, bytes_count: int, duration: float) -> float: + """ + 计算带宽 + + Args: + bytes_count: 字节数 + duration: 持续时间(秒) + + Returns: + 带宽(B/s) + """ + try: + self.utils_stats["functions_called"] += 1 + + if duration <= 0: + return 0.0 + + bandwidth = bytes_count / duration + return bandwidth + + except Exception as e: + print(f"✗ 带宽计算失败: {e}") + return 0.0 + + def get_local_ip(self) -> Optional[str]: + """ + 获取本地IP地址 + + Returns: + 本地IP地址或None + """ + try: + self.utils_stats["functions_called"] += 1 + + import socket + # 获取本地IP地址 + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + # 连接到一个远程地址(不需要真实连接) + s.connect(("8.8.8.8", 80)) + local_ip = s.getsockname()[0] + + return local_ip + + except Exception as e: + print(f"✗ 本地IP获取失败: {e}") + return None + + def is_valid_port(self, port: int) -> bool: + """ + 检查端口是否有效 + + Args: + port: 端口号 + + Returns: + 端口是否有效 + """ + try: + self.utils_stats["functions_called"] += 1 + + return 1 <= port <= 65535 + + except Exception as e: + print(f"✗ 端口检查失败: {e}") + return False + + def get_system_info(self) -> Dict[str, Any]: + """ + 获取系统信息 + + Returns: + 系统信息字典 + """ + try: + self.utils_stats["functions_called"] += 1 + + import platform + import psutil + + system_info = { + "platform": platform.system(), + "platform_version": platform.version(), + "architecture": platform.architecture()[0], + "processor": platform.processor(), + "cpu_count": psutil.cpu_count(), + "memory_total": psutil.virtual_memory().total, + "memory_available": psutil.virtual_memory().available + } + + return system_info + + except Exception as e: + print(f"✗ 系统信息获取失败: {e}") + return {} + + def get_network_interfaces(self) -> List[Dict[str, Any]]: + """ + 获取网络接口信息 + + Returns: + 网络接口信息列表 + """ + try: + self.utils_stats["functions_called"] += 1 + + import psutil + + interfaces = [] + net_if_addrs = psutil.net_if_addrs() + + for interface_name, interface_addresses in net_if_addrs.items(): + interface_info = { + "name": interface_name, + "addresses": [] + } + + for address in interface_addresses: + interface_info["addresses"].append({ + "family": str(address.family), + "address": address.address, + "netmask": address.netmask, + "broadcast": address.broadcast + }) + + interfaces.append(interface_info) + + return interfaces + + except Exception as e: + print(f"✗ 网络接口信息获取失败: {e}") + return [] + + def measure_latency(self, host: str, port: int, timeout: float = 5.0) -> Optional[float]: + """ + 测量网络延迟 + + Args: + host: 主机地址 + port: 端口号 + timeout: 超时时间 + + Returns: + 延迟时间(秒)或None + """ + try: + self.utils_stats["functions_called"] += 1 + + import socket + import time + + start_time = time.time() + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: + sock.settimeout(timeout) + result = sock.connect_ex((host, port)) + + if result == 0: + end_time = time.time() + latency = end_time - start_time + return latency + else: + return None + + except Exception as e: + print(f"✗ 延迟测量失败: {e}") + return None + + def get_utils_stats(self) -> Dict[str, Any]: + """ + 获取工具统计信息 + + Returns: + 工具统计字典 + """ + return self.utils_stats.copy() + + def reset_utils_stats(self): + """重置工具统计信息""" + try: + self.utils_stats = { + "cache_hits": 0, + "cache_misses": 0, + "compressions": 0, + "decompressions": 0, + "checksums_calculated": 0, + "data_processed": 0, + "functions_called": 0 + } + print("✓ 工具统计信息已重置") + except Exception as e: + print(f"✗ 工具统计信息重置失败: {e}") + + def set_utils_config(self, config: Dict[str, Any]) -> bool: + """ + 设置工具配置 + + Args: + config: 工具配置字典 + + Returns: + 是否设置成功 + """ + try: + self.utils_config.update(config) + print(f"✓ 工具配置已更新: {self.utils_config}") + return True + except Exception as e: + print(f"✗ 工具配置设置失败: {e}") + return False + + def get_utils_config(self) -> Dict[str, Any]: + """ + 获取工具配置 + + Returns: + 工具配置字典 + """ + return self.utils_config.copy() + + def _trigger_utils_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发工具回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.utils_callbacks: + for callback in self.utils_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 工具回调执行失败: {e}") + except Exception as e: + print(f"✗ 工具回调触发失败: {e}") + + def register_utils_callback(self, callback_type: str, callback: callable): + """ + 注册工具回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.utils_callbacks: + self.utils_callbacks[callback_type].append(callback) + print(f"✓ 工具回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 工具回调注册失败: {e}") + + def unregister_utils_callback(self, callback_type: str, callback: callable): + """ + 注销工具回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.utils_callbacks: + if callback in self.utils_callbacks[callback_type]: + self.utils_callbacks[callback_type].remove(callback) + print(f"✓ 工具回调已注销: {callback_type}") + except Exception as e: print(f"✗ 工具回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/particle_system/README.md b/plugins/user/particle_system/README.md index 3429feb3..7c3c87ad 100644 --- a/plugins/user/particle_system/README.md +++ b/plugins/user/particle_system/README.md @@ -1,159 +1,159 @@ -# 粒子系统插件 - -一个功能完整的3D粒子系统插件,为EG引擎提供高性能的粒子特效功能。 - -## 功能特性 - -### 核心功能 -- **多种粒子效果**: 支持火焰、烟雾、爆炸、水花、魔法、雪花等多种预设效果 -- **实时物理模拟**: 重力、风力、碰撞检测、力场等物理效果 -- **高性能渲染**: 支持广告牌和点精灵渲染模式,优化的批量渲染 -- **可视化编辑**: 直观的参数调节界面,实时预览效果 -- **性能监控**: 实时监控FPS、粒子数量、渲染时间等性能指标 -- **预设管理**: 内置预设和自定义预设的保存、加载功能 - -### 粒子属性控制 -- 生命周期和颜色渐变 -- 尺寸变化和旋转效果 -- 速度和发射形状控制 -- 重力、空气阻力和反弹系数 -- 多种发射器形状(点、圆形、球形、盒形) - -### 物理系统 -- 重力和风力模拟 -- 地面和边界碰撞检测 -- 自定义力场(引力、斥力、涡流等) -- 与Bullet物理引擎集成 - -### 编辑功能 -- 实时参数调节 -- 效果类型切换 -- 预设保存和加载 -- 可视化性能监控 - -## 使用方法 - -### 快捷键操作 -- **F5**: 切换粒子编辑器显示 -- **F6**: 切换性能监控器显示 -- **F7**: 创建测试粒子效果 -- **F8**: 清除所有粒子 -- **F9**: 创建火焰效果 -- **F10**: 创建烟雾效果 -- **F11**: 创建爆炸效果 -- **F12**: 创建水花效果 - -### GUI界面操作 -插件会在屏幕左上角创建控制面板,可以通过按钮直接创建各种粒子效果。 - -### 编程接口 -```python -# 获取插件实例 -particle_plugin = world.plugin_manager.get_plugin("particle_system") - -# 创建预设效果 -particle_plugin.create_fire_effect() # 火焰效果 -particle_plugin.create_smoke_effect() # 烟雾效果 -particle_plugin.create_explosion_effect() # 爆炸效果 -particle_plugin.create_water_effect() # 水花效果 -particle_plugin.create_magic_effect() # 魔法效果 -particle_plugin.create_snow_effect() # 雪花效果 - -# 创建自定义效果 -config = { - 'emission_rate': 100, - 'particle_lifetime': 3.0, - 'start_color': Vec3(1.0, 0.5, 0.0), - 'end_color': Vec3(1.0, 0.0, 0.0), - 'start_size': 0.3, - 'end_size': 0.6, - 'velocity': Vec3(0, 0, 2), - 'gravity_scale': -0.5 -} -particle_plugin.particle_manager.create_emitter("custom", position, config) - -# 性能控制 -particle_plugin.set_max_particles(5000) # 设置最大粒子数 -particle_plugin.set_global_time_scale(0.5) # 设置时间缩放 -particle_plugin.set_emit_rate_multiplier(2.0) # 设置发射速率倍增 -particle_plugin.set_particle_size_multiplier(1.5) # 设置粒子尺寸倍增 - -# 预设管理 -particle_plugin.list_presets() # 列出所有预设 -particle_plugin.load_preset("经典火焰") # 加载预设 -``` - -## 性能优化 - -### 对象池技术 -使用对象池管理粒子对象,避免频繁创建和销毁对象的开销。 - -### 批量渲染 -支持批量渲染大量粒子,减少绘制调用次数。 - -### 自适应更新 -根据粒子数量和复杂度动态调整更新频率。 - -## 预设效果 - -### 内置预设 -- 经典火焰: 典型的上升火焰效果 -- 浓密烟雾: 缓慢上升的烟雾效果 -- 强力爆炸: 瞬间爆发的爆炸效果 -- 清澈水花: 下落并反弹的水花效果 -- 魔法光球: 悬浮旋转的魔法粒子效果 -- 飘落雪花: 从上至下飘落的雪花效果 - -### 自定义预设 -支持创建、保存和加载自定义预设,满足特定项目需求。 - -## 扩展开发 - -### 添加新的粒子效果类型 -1. 在[particle_manager.py](file:///home/hello/EG/plugins/user/particle_system/core/particle_manager.py)中添加新的创建方法 -2. 在[particle_emitter.py](file:///home/hello/EG/plugins/user/particle_system/core/particle_emitter.py)中支持新的参数配置 -3. 在[particle_renderer.py](file:///home/hello/EG/plugins/user/particle_system/core/particle_renderer.py)中添加渲染支持 -4. 在编辑器中添加对应的界面元素 - -### 添加新的物理效果 -1. 在[particle_physics.py](file:///home/hello/EG/plugins/user/particle_system/core/particle_physics.py)中实现新的物理计算 -2. 在粒子属性中添加对应的参数 -3. 在编辑器中添加参数调节控件 - -## 性能监控 - -插件内置性能监控功能,可以实时显示: -- FPS和平均FPS -- 当前粒子数量和活跃发射器数量 -- 渲染时间和物理计算时间 -- 内存使用情况 -- 绘制调用次数和顶点数量 - -通过性能监控可以及时发现和解决性能瓶颈问题。 - -## 故障排除 - -### 粒子效果不显示 -1. 检查插件是否已正确启用 -2. 确认粒子数量限制设置 -3. 检查摄像机位置和粒子发射位置 - -### 性能问题 -1. 使用性能监控器查看瓶颈所在 -2. 降低粒子数量或简化粒子效果 -3. 调整渲染设置(如使用点精灵代替广告牌) - -### 编辑器无法使用 -1. 确认GUI系统是否正常工作 -2. 检查快捷键是否被其他系统占用 -3. 重新启用插件尝试 - -## 版本信息 - -- 版本: 1.0.0 -- 作者: EG Team -- 发布日期: 2024年 - -## 许可证 - +# 粒子系统插件 + +一个功能完整的3D粒子系统插件,为EG引擎提供高性能的粒子特效功能。 + +## 功能特性 + +### 核心功能 +- **多种粒子效果**: 支持火焰、烟雾、爆炸、水花、魔法、雪花等多种预设效果 +- **实时物理模拟**: 重力、风力、碰撞检测、力场等物理效果 +- **高性能渲染**: 支持广告牌和点精灵渲染模式,优化的批量渲染 +- **可视化编辑**: 直观的参数调节界面,实时预览效果 +- **性能监控**: 实时监控FPS、粒子数量、渲染时间等性能指标 +- **预设管理**: 内置预设和自定义预设的保存、加载功能 + +### 粒子属性控制 +- 生命周期和颜色渐变 +- 尺寸变化和旋转效果 +- 速度和发射形状控制 +- 重力、空气阻力和反弹系数 +- 多种发射器形状(点、圆形、球形、盒形) + +### 物理系统 +- 重力和风力模拟 +- 地面和边界碰撞检测 +- 自定义力场(引力、斥力、涡流等) +- 与Bullet物理引擎集成 + +### 编辑功能 +- 实时参数调节 +- 效果类型切换 +- 预设保存和加载 +- 可视化性能监控 + +## 使用方法 + +### 快捷键操作 +- **F5**: 切换粒子编辑器显示 +- **F6**: 切换性能监控器显示 +- **F7**: 创建测试粒子效果 +- **F8**: 清除所有粒子 +- **F9**: 创建火焰效果 +- **F10**: 创建烟雾效果 +- **F11**: 创建爆炸效果 +- **F12**: 创建水花效果 + +### GUI界面操作 +插件会在屏幕左上角创建控制面板,可以通过按钮直接创建各种粒子效果。 + +### 编程接口 +```python +# 获取插件实例 +particle_plugin = world.plugin_manager.get_plugin("particle_system") + +# 创建预设效果 +particle_plugin.create_fire_effect() # 火焰效果 +particle_plugin.create_smoke_effect() # 烟雾效果 +particle_plugin.create_explosion_effect() # 爆炸效果 +particle_plugin.create_water_effect() # 水花效果 +particle_plugin.create_magic_effect() # 魔法效果 +particle_plugin.create_snow_effect() # 雪花效果 + +# 创建自定义效果 +config = { + 'emission_rate': 100, + 'particle_lifetime': 3.0, + 'start_color': Vec3(1.0, 0.5, 0.0), + 'end_color': Vec3(1.0, 0.0, 0.0), + 'start_size': 0.3, + 'end_size': 0.6, + 'velocity': Vec3(0, 0, 2), + 'gravity_scale': -0.5 +} +particle_plugin.particle_manager.create_emitter("custom", position, config) + +# 性能控制 +particle_plugin.set_max_particles(5000) # 设置最大粒子数 +particle_plugin.set_global_time_scale(0.5) # 设置时间缩放 +particle_plugin.set_emit_rate_multiplier(2.0) # 设置发射速率倍增 +particle_plugin.set_particle_size_multiplier(1.5) # 设置粒子尺寸倍增 + +# 预设管理 +particle_plugin.list_presets() # 列出所有预设 +particle_plugin.load_preset("经典火焰") # 加载预设 +``` + +## 性能优化 + +### 对象池技术 +使用对象池管理粒子对象,避免频繁创建和销毁对象的开销。 + +### 批量渲染 +支持批量渲染大量粒子,减少绘制调用次数。 + +### 自适应更新 +根据粒子数量和复杂度动态调整更新频率。 + +## 预设效果 + +### 内置预设 +- 经典火焰: 典型的上升火焰效果 +- 浓密烟雾: 缓慢上升的烟雾效果 +- 强力爆炸: 瞬间爆发的爆炸效果 +- 清澈水花: 下落并反弹的水花效果 +- 魔法光球: 悬浮旋转的魔法粒子效果 +- 飘落雪花: 从上至下飘落的雪花效果 + +### 自定义预设 +支持创建、保存和加载自定义预设,满足特定项目需求。 + +## 扩展开发 + +### 添加新的粒子效果类型 +1. 在[particle_manager.py](file:///home/hello/EG/plugins/user/particle_system/core/particle_manager.py)中添加新的创建方法 +2. 在[particle_emitter.py](file:///home/hello/EG/plugins/user/particle_system/core/particle_emitter.py)中支持新的参数配置 +3. 在[particle_renderer.py](file:///home/hello/EG/plugins/user/particle_system/core/particle_renderer.py)中添加渲染支持 +4. 在编辑器中添加对应的界面元素 + +### 添加新的物理效果 +1. 在[particle_physics.py](file:///home/hello/EG/plugins/user/particle_system/core/particle_physics.py)中实现新的物理计算 +2. 在粒子属性中添加对应的参数 +3. 在编辑器中添加参数调节控件 + +## 性能监控 + +插件内置性能监控功能,可以实时显示: +- FPS和平均FPS +- 当前粒子数量和活跃发射器数量 +- 渲染时间和物理计算时间 +- 内存使用情况 +- 绘制调用次数和顶点数量 + +通过性能监控可以及时发现和解决性能瓶颈问题。 + +## 故障排除 + +### 粒子效果不显示 +1. 检查插件是否已正确启用 +2. 确认粒子数量限制设置 +3. 检查摄像机位置和粒子发射位置 + +### 性能问题 +1. 使用性能监控器查看瓶颈所在 +2. 降低粒子数量或简化粒子效果 +3. 调整渲染设置(如使用点精灵代替广告牌) + +### 编辑器无法使用 +1. 确认GUI系统是否正常工作 +2. 检查快捷键是否被其他系统占用 +3. 重新启用插件尝试 + +## 版本信息 + +- 版本: 1.0.0 +- 作者: EG Team +- 发布日期: 2024年 + +## 许可证 + 本插件遵循EG引擎许可证协议。 \ No newline at end of file diff --git a/plugins/user/particle_system/__init__.py b/plugins/user/particle_system/__init__.py index 9330df9a..c544d6c9 100644 --- a/plugins/user/particle_system/__init__.py +++ b/plugins/user/particle_system/__init__.py @@ -1,12 +1,12 @@ -""" -粒子系统插件 -提供强大的3D粒子特效功能 -""" - -from .plugin import Plugin - -__version__ = "1.0.0" -__author__ = "EG Team" -__description__ = "高性能3D粒子系统插件,支持多种特效和实时编辑" - +""" +粒子系统插件 +提供强大的3D粒子特效功能 +""" + +from .plugin import Plugin + +__version__ = "1.0.0" +__author__ = "EG Team" +__description__ = "高性能3D粒子系统插件,支持多种特效和实时编辑" + __all__ = ['Plugin'] \ No newline at end of file diff --git a/plugins/user/particle_system/core/__init__.py b/plugins/user/particle_system/core/__init__.py index daf860c8..236a21ff 100644 --- a/plugins/user/particle_system/core/__init__.py +++ b/plugins/user/particle_system/core/__init__.py @@ -1,15 +1,15 @@ -""" -粒子系统核心模块 -""" - -from .particle_manager import ParticleManager -from .particle_emitter import ParticleEmitter -from .particle_renderer import ParticleRenderer -from .particle_physics import ParticlePhysics - -__all__ = [ - 'ParticleManager', - 'ParticleEmitter', - 'ParticleRenderer', - 'ParticlePhysics' +""" +粒子系统核心模块 +""" + +from .particle_manager import ParticleManager +from .particle_emitter import ParticleEmitter +from .particle_renderer import ParticleRenderer +from .particle_physics import ParticlePhysics + +__all__ = [ + 'ParticleManager', + 'ParticleEmitter', + 'ParticleRenderer', + 'ParticlePhysics' ] \ No newline at end of file diff --git a/plugins/user/particle_system/core/particle_emitter.py b/plugins/user/particle_system/core/particle_emitter.py index 2d9e046e..89c3bb58 100644 --- a/plugins/user/particle_system/core/particle_emitter.py +++ b/plugins/user/particle_system/core/particle_emitter.py @@ -1,403 +1,403 @@ -""" -粒子发射器 -负责粒子的生成和发射逻辑 -""" - -import time -import random -import math -from typing import List, Dict, Optional -from panda3d.core import Vec3, Point3, LVector3f -from collections import deque - -class Particle: - """单个粒子类""" - - def __init__(self): - self.position = Point3(0, 0, 0) - self.velocity = Vec3(0, 0, 0) - self.acceleration = Vec3(0, 0, 0) - self.color = Vec3(1, 1, 1) - self.size = 1.0 - self.rotation = 0.0 - self.angular_velocity = 0.0 - self.lifetime = 1.0 - self.age = 0.0 - self.mass = 1.0 - self.active = True - - # 生命周期属性 - self.start_color = Vec3(1, 1, 1) - self.end_color = Vec3(1, 1, 1) - self.start_size = 1.0 - self.end_size = 1.0 - - # 物理属性 - self.gravity_scale = 1.0 - self.air_resistance = 0.01 - self.bounce_factor = 0.5 - - def update(self, dt: float): - """更新粒子状态""" - if not self.active: - return - - # 更新年龄 - self.age += dt - - # 检查生命周期 - if self.age >= self.lifetime: - self.active = False - return - - # 计算生命周期进度 - life_progress = self.age / self.lifetime - - # 更新颜色(线性插值) - self.color = self.start_color + (self.end_color - self.start_color) * life_progress - - # 更新尺寸(线性插值) - self.size = self.start_size + (self.end_size - self.start_size) * life_progress - - # 更新旋转 - self.rotation += self.angular_velocity * dt - - # 应用空气阻力 - drag = self.velocity * self.air_resistance - self.velocity -= drag * dt - - def is_alive(self) -> bool: - """检查粒子是否存活""" - return self.active and self.age < self.lifetime - - def reset(self): - """重置粒子状态(用于对象池)""" - self.position = Point3(0, 0, 0) - self.velocity = Vec3(0, 0, 0) - self.acceleration = Vec3(0, 0, 0) - self.color = Vec3(1, 1, 1) - self.size = 1.0 - self.rotation = 0.0 - self.angular_velocity = 0.0 - self.age = 0.0 - self.active = True - -class ParticleEmitter: - """ - 粒子发射器 - 负责粒子的生成、管理和更新 - """ - - def __init__(self, emitter_id: str, emitter_type: str, position: Point3, world, config: Dict): - """ - 初始化粒子发射器 - - Args: - emitter_id: 发射器唯一ID - emitter_type: 发射器类型 - position: 发射器位置 - world: 3D世界对象 - config: 发射器配置 - """ - self.emitter_id = emitter_id - self.emitter_type = emitter_type - self.world = world - self.position = position - - # 粒子容器 - self.particles: List[Particle] = [] - self.particle_pool = deque() # 对象池 - self.max_particles = config.get('max_particles', 1000) - - # 发射器状态 - self.active = True - self.enabled = True - self.age = 0.0 - self.duration = config.get('duration', -1) # -1表示无限持续 - - # 发射参数 - self.emission_rate = config.get('emission_rate', 10) # 每秒发射粒子数 - self.emission_accumulator = 0.0 - self.burst_count = config.get('burst_count', 0) # 爆发粒子数 - self.burst_emitted = False - - # 粒子属性 - self.particle_lifetime = config.get('particle_lifetime', 2.0) - self.particle_lifetime_variation = config.get('particle_lifetime_variation', 0.2) - - # 颜色属性 - self.start_color = config.get('start_color', Vec3(1, 1, 1)) - self.end_color = config.get('end_color', Vec3(1, 1, 1)) - self.color_variation = config.get('color_variation', 0.1) - - # 尺寸属性 - self.start_size = config.get('start_size', 1.0) - self.end_size = config.get('end_size', 1.0) - self.size_variation = config.get('size_variation', 0.2) - - # 速度属性 - self.velocity = config.get('velocity', Vec3(0, 0, 1)) - self.velocity_variation = config.get('velocity_variation', 0.3) - - # 物理属性 - self.gravity_scale = config.get('gravity_scale', 1.0) - self.air_resistance = config.get('air_resistance', 0.01) - self.bounce_factor = config.get('bounce_factor', 0.5) - self.mass = config.get('mass', 1.0) - - # 发射器形状 - self.emitter_shape = config.get('emitter_shape', 'point') # point, circle, sphere, box - self.emitter_radius = config.get('emitter_radius', 0.0) - self.emitter_dimensions = config.get('emitter_dimensions', Vec3(1, 1, 1)) - - # 角度和方向 - self.emission_angle = config.get('emission_angle', 0.0) # 发射角度(弧度) - self.emission_cone = config.get('emission_cone', math.pi / 6) # 发射锥角 - self.emission_direction = config.get('emission_direction', Vec3(0, 0, 1)) - - # 旋转属性 - self.rotation_enabled = config.get('rotation_enabled', False) - self.rotation_speed = config.get('rotation_speed', 0.0) - self.rotation_variation = config.get('rotation_variation', 0.5) - - # 随机种子支持 - self.random_seed = config.get('random_seed', None) - if self.random_seed is not None: - random.seed(self.random_seed) - - print(f"✓ 粒子发射器创建: {emitter_id} ({emitter_type})") - - def update(self, dt: float): - """更新发射器""" - if not self.enabled: - return - - self.age += dt - - # 检查持续时间 - if self.duration > 0 and self.age >= self.duration: - self.active = False - - # 发射新粒子 - if self.active: - self._emit_particles(dt) - - # 更新现有粒子 - self._update_particles(dt) - - # 清理死亡粒子 - self._cleanup_dead_particles() - - def _emit_particles(self, dt: float): - """发射新粒子""" - # 爆发发射 - if self.burst_count > 0 and not self.burst_emitted: - for _ in range(self.burst_count): - if len(self.particles) < self.max_particles: - self._create_particle() - self.burst_emitted = True - - # 持续发射 - if self.emission_rate > 0: - self.emission_accumulator += self.emission_rate * dt - - while self.emission_accumulator >= 1.0 and len(self.particles) < self.max_particles: - self._create_particle() - self.emission_accumulator -= 1.0 - - def _create_particle(self): - """创建新粒子""" - # 从对象池获取或创建新粒子 - if self.particle_pool: - particle = self.particle_pool.popleft() - particle.reset() - else: - particle = Particle() - - # 设置粒子基本属性 - particle.mass = self.mass - - # 设置粒子位置 - particle.position = self._get_emission_position() - - # 设置粒子速度 - particle.velocity = self._get_emission_velocity() - - # 设置粒子生命周期 - lifetime_var = random.uniform(-self.particle_lifetime_variation, self.particle_lifetime_variation) - particle.lifetime = max(0.1, self.particle_lifetime + lifetime_var) - - # 设置颜色 - particle.start_color = self._vary_color(self.start_color) - particle.end_color = self._vary_color(self.end_color) - particle.color = particle.start_color - - # 设置尺寸 - size_var = random.uniform(-self.size_variation, self.size_variation) - particle.start_size = max(0.01, self.start_size + size_var) - particle.end_size = max(0.01, self.end_size + size_var) - particle.size = particle.start_size - - # 设置物理属性 - particle.gravity_scale = self.gravity_scale - particle.air_resistance = self.air_resistance - particle.bounce_factor = self.bounce_factor - - # 设置旋转 - if self.rotation_enabled: - particle.angular_velocity = self.rotation_speed + random.uniform( - -self.rotation_variation, self.rotation_variation - ) - - # 添加到粒子列表 - self.particles.append(particle) - - def _get_emission_position(self) -> Point3: - """获取发射位置""" - if self.emitter_shape == 'point': - return Point3(self.position) - - elif self.emitter_shape == 'circle': - angle = random.uniform(0, 2 * math.pi) - radius = random.uniform(0, self.emitter_radius) - offset = Vec3( - radius * math.cos(angle), - radius * math.sin(angle), - 0 - ) - return self.position + offset - - elif self.emitter_shape == 'sphere': - # 球形发射器 - phi = random.uniform(0, 2 * math.pi) - costheta = random.uniform(-1, 1) - theta = math.acos(costheta) - radius = random.uniform(0, self.emitter_radius) - - offset = Vec3( - radius * math.sin(theta) * math.cos(phi), - radius * math.sin(theta) * math.sin(phi), - radius * math.cos(theta) - ) - return self.position + offset - - elif self.emitter_shape == 'box': - # 盒形发射器 - offset = Vec3( - random.uniform(-self.emitter_dimensions.x/2, self.emitter_dimensions.x/2), - random.uniform(-self.emitter_dimensions.y/2, self.emitter_dimensions.y/2), - random.uniform(-self.emitter_dimensions.z/2, self.emitter_dimensions.z/2) - ) - return self.position + offset - - return Point3(self.position) - - def _get_emission_velocity(self) -> Vec3: - """获取发射速度""" - base_velocity = Vec3(self.velocity) - - # 添加随机变化 - if self.velocity_variation > 0: - variation = Vec3( - random.uniform(-self.velocity_variation, self.velocity_variation), - random.uniform(-self.velocity_variation, self.velocity_variation), - random.uniform(-self.velocity_variation, self.velocity_variation) - ) - base_velocity += variation - - # 应用发射锥角 - if self.emission_cone > 0: - # 在锥形范围内随机方向 - cone_angle = random.uniform(0, self.emission_cone) - azimuth = random.uniform(0, 2 * math.pi) - - # 计算锥形方向 - cone_dir = Vec3( - math.sin(cone_angle) * math.cos(azimuth), - math.sin(cone_angle) * math.sin(azimuth), - math.cos(cone_angle) - ) - - # 将锥形方向转换到发射方向 - # 这里简化处理,实际应该做完整的旋转变换 - base_velocity = base_velocity.length() * cone_dir - - return base_velocity - - def _vary_color(self, base_color: Vec3) -> Vec3: - """添加颜色变化""" - if self.color_variation <= 0: - return Vec3(base_color) - - variation = Vec3( - random.uniform(-self.color_variation, self.color_variation), - random.uniform(-self.color_variation, self.color_variation), - random.uniform(-self.color_variation, self.color_variation) - ) - - result = base_color + variation - - # 确保颜色值在有效范围内 - result.x = max(0.0, min(1.0, result.x)) - result.y = max(0.0, min(1.0, result.y)) - result.z = max(0.0, min(1.0, result.z)) - - return result - - def _update_particles(self, dt: float): - """更新所有粒子""" - for particle in self.particles: - if particle.is_alive(): - particle.update(dt) - - def _cleanup_dead_particles(self): - """清理死亡粒子""" - alive_particles = [] - - for particle in self.particles: - if particle.is_alive(): - alive_particles.append(particle) - else: - # 回收到对象池 - self.particle_pool.append(particle) - - self.particles = alive_particles - - def get_particles(self) -> List[Particle]: - """获取所有活跃粒子""" - return [p for p in self.particles if p.is_alive()] - - def get_particle_count(self) -> int: - """获取粒子数量""" - return len([p for p in self.particles if p.is_alive()]) - - def is_active(self) -> bool: - """检查发射器是否活跃""" - return self.active or len(self.particles) > 0 - - def should_remove(self) -> bool: - """检查是否应该移除发射器""" - return not self.active and len(self.particles) == 0 - - def stop_emission(self): - """停止发射新粒子""" - self.active = False - - def resume_emission(self): - """恢复发射粒子""" - self.active = True - - def set_position(self, position: Point3): - """设置发射器位置""" - self.position = position - - def set_enabled(self, enabled: bool): - """设置发射器启用状态""" - self.enabled = enabled - - def cleanup(self): - """清理发射器资源""" - self.particles.clear() - self.particle_pool.clear() - self.active = False +""" +粒子发射器 +负责粒子的生成和发射逻辑 +""" + +import time +import random +import math +from typing import List, Dict, Optional +from panda3d.core import Vec3, Point3, LVector3f +from collections import deque + +class Particle: + """单个粒子类""" + + def __init__(self): + self.position = Point3(0, 0, 0) + self.velocity = Vec3(0, 0, 0) + self.acceleration = Vec3(0, 0, 0) + self.color = Vec3(1, 1, 1) + self.size = 1.0 + self.rotation = 0.0 + self.angular_velocity = 0.0 + self.lifetime = 1.0 + self.age = 0.0 + self.mass = 1.0 + self.active = True + + # 生命周期属性 + self.start_color = Vec3(1, 1, 1) + self.end_color = Vec3(1, 1, 1) + self.start_size = 1.0 + self.end_size = 1.0 + + # 物理属性 + self.gravity_scale = 1.0 + self.air_resistance = 0.01 + self.bounce_factor = 0.5 + + def update(self, dt: float): + """更新粒子状态""" + if not self.active: + return + + # 更新年龄 + self.age += dt + + # 检查生命周期 + if self.age >= self.lifetime: + self.active = False + return + + # 计算生命周期进度 + life_progress = self.age / self.lifetime + + # 更新颜色(线性插值) + self.color = self.start_color + (self.end_color - self.start_color) * life_progress + + # 更新尺寸(线性插值) + self.size = self.start_size + (self.end_size - self.start_size) * life_progress + + # 更新旋转 + self.rotation += self.angular_velocity * dt + + # 应用空气阻力 + drag = self.velocity * self.air_resistance + self.velocity -= drag * dt + + def is_alive(self) -> bool: + """检查粒子是否存活""" + return self.active and self.age < self.lifetime + + def reset(self): + """重置粒子状态(用于对象池)""" + self.position = Point3(0, 0, 0) + self.velocity = Vec3(0, 0, 0) + self.acceleration = Vec3(0, 0, 0) + self.color = Vec3(1, 1, 1) + self.size = 1.0 + self.rotation = 0.0 + self.angular_velocity = 0.0 + self.age = 0.0 + self.active = True + +class ParticleEmitter: + """ + 粒子发射器 + 负责粒子的生成、管理和更新 + """ + + def __init__(self, emitter_id: str, emitter_type: str, position: Point3, world, config: Dict): + """ + 初始化粒子发射器 + + Args: + emitter_id: 发射器唯一ID + emitter_type: 发射器类型 + position: 发射器位置 + world: 3D世界对象 + config: 发射器配置 + """ + self.emitter_id = emitter_id + self.emitter_type = emitter_type + self.world = world + self.position = position + + # 粒子容器 + self.particles: List[Particle] = [] + self.particle_pool = deque() # 对象池 + self.max_particles = config.get('max_particles', 1000) + + # 发射器状态 + self.active = True + self.enabled = True + self.age = 0.0 + self.duration = config.get('duration', -1) # -1表示无限持续 + + # 发射参数 + self.emission_rate = config.get('emission_rate', 10) # 每秒发射粒子数 + self.emission_accumulator = 0.0 + self.burst_count = config.get('burst_count', 0) # 爆发粒子数 + self.burst_emitted = False + + # 粒子属性 + self.particle_lifetime = config.get('particle_lifetime', 2.0) + self.particle_lifetime_variation = config.get('particle_lifetime_variation', 0.2) + + # 颜色属性 + self.start_color = config.get('start_color', Vec3(1, 1, 1)) + self.end_color = config.get('end_color', Vec3(1, 1, 1)) + self.color_variation = config.get('color_variation', 0.1) + + # 尺寸属性 + self.start_size = config.get('start_size', 1.0) + self.end_size = config.get('end_size', 1.0) + self.size_variation = config.get('size_variation', 0.2) + + # 速度属性 + self.velocity = config.get('velocity', Vec3(0, 0, 1)) + self.velocity_variation = config.get('velocity_variation', 0.3) + + # 物理属性 + self.gravity_scale = config.get('gravity_scale', 1.0) + self.air_resistance = config.get('air_resistance', 0.01) + self.bounce_factor = config.get('bounce_factor', 0.5) + self.mass = config.get('mass', 1.0) + + # 发射器形状 + self.emitter_shape = config.get('emitter_shape', 'point') # point, circle, sphere, box + self.emitter_radius = config.get('emitter_radius', 0.0) + self.emitter_dimensions = config.get('emitter_dimensions', Vec3(1, 1, 1)) + + # 角度和方向 + self.emission_angle = config.get('emission_angle', 0.0) # 发射角度(弧度) + self.emission_cone = config.get('emission_cone', math.pi / 6) # 发射锥角 + self.emission_direction = config.get('emission_direction', Vec3(0, 0, 1)) + + # 旋转属性 + self.rotation_enabled = config.get('rotation_enabled', False) + self.rotation_speed = config.get('rotation_speed', 0.0) + self.rotation_variation = config.get('rotation_variation', 0.5) + + # 随机种子支持 + self.random_seed = config.get('random_seed', None) + if self.random_seed is not None: + random.seed(self.random_seed) + + print(f"✓ 粒子发射器创建: {emitter_id} ({emitter_type})") + + def update(self, dt: float): + """更新发射器""" + if not self.enabled: + return + + self.age += dt + + # 检查持续时间 + if self.duration > 0 and self.age >= self.duration: + self.active = False + + # 发射新粒子 + if self.active: + self._emit_particles(dt) + + # 更新现有粒子 + self._update_particles(dt) + + # 清理死亡粒子 + self._cleanup_dead_particles() + + def _emit_particles(self, dt: float): + """发射新粒子""" + # 爆发发射 + if self.burst_count > 0 and not self.burst_emitted: + for _ in range(self.burst_count): + if len(self.particles) < self.max_particles: + self._create_particle() + self.burst_emitted = True + + # 持续发射 + if self.emission_rate > 0: + self.emission_accumulator += self.emission_rate * dt + + while self.emission_accumulator >= 1.0 and len(self.particles) < self.max_particles: + self._create_particle() + self.emission_accumulator -= 1.0 + + def _create_particle(self): + """创建新粒子""" + # 从对象池获取或创建新粒子 + if self.particle_pool: + particle = self.particle_pool.popleft() + particle.reset() + else: + particle = Particle() + + # 设置粒子基本属性 + particle.mass = self.mass + + # 设置粒子位置 + particle.position = self._get_emission_position() + + # 设置粒子速度 + particle.velocity = self._get_emission_velocity() + + # 设置粒子生命周期 + lifetime_var = random.uniform(-self.particle_lifetime_variation, self.particle_lifetime_variation) + particle.lifetime = max(0.1, self.particle_lifetime + lifetime_var) + + # 设置颜色 + particle.start_color = self._vary_color(self.start_color) + particle.end_color = self._vary_color(self.end_color) + particle.color = particle.start_color + + # 设置尺寸 + size_var = random.uniform(-self.size_variation, self.size_variation) + particle.start_size = max(0.01, self.start_size + size_var) + particle.end_size = max(0.01, self.end_size + size_var) + particle.size = particle.start_size + + # 设置物理属性 + particle.gravity_scale = self.gravity_scale + particle.air_resistance = self.air_resistance + particle.bounce_factor = self.bounce_factor + + # 设置旋转 + if self.rotation_enabled: + particle.angular_velocity = self.rotation_speed + random.uniform( + -self.rotation_variation, self.rotation_variation + ) + + # 添加到粒子列表 + self.particles.append(particle) + + def _get_emission_position(self) -> Point3: + """获取发射位置""" + if self.emitter_shape == 'point': + return Point3(self.position) + + elif self.emitter_shape == 'circle': + angle = random.uniform(0, 2 * math.pi) + radius = random.uniform(0, self.emitter_radius) + offset = Vec3( + radius * math.cos(angle), + radius * math.sin(angle), + 0 + ) + return self.position + offset + + elif self.emitter_shape == 'sphere': + # 球形发射器 + phi = random.uniform(0, 2 * math.pi) + costheta = random.uniform(-1, 1) + theta = math.acos(costheta) + radius = random.uniform(0, self.emitter_radius) + + offset = Vec3( + radius * math.sin(theta) * math.cos(phi), + radius * math.sin(theta) * math.sin(phi), + radius * math.cos(theta) + ) + return self.position + offset + + elif self.emitter_shape == 'box': + # 盒形发射器 + offset = Vec3( + random.uniform(-self.emitter_dimensions.x/2, self.emitter_dimensions.x/2), + random.uniform(-self.emitter_dimensions.y/2, self.emitter_dimensions.y/2), + random.uniform(-self.emitter_dimensions.z/2, self.emitter_dimensions.z/2) + ) + return self.position + offset + + return Point3(self.position) + + def _get_emission_velocity(self) -> Vec3: + """获取发射速度""" + base_velocity = Vec3(self.velocity) + + # 添加随机变化 + if self.velocity_variation > 0: + variation = Vec3( + random.uniform(-self.velocity_variation, self.velocity_variation), + random.uniform(-self.velocity_variation, self.velocity_variation), + random.uniform(-self.velocity_variation, self.velocity_variation) + ) + base_velocity += variation + + # 应用发射锥角 + if self.emission_cone > 0: + # 在锥形范围内随机方向 + cone_angle = random.uniform(0, self.emission_cone) + azimuth = random.uniform(0, 2 * math.pi) + + # 计算锥形方向 + cone_dir = Vec3( + math.sin(cone_angle) * math.cos(azimuth), + math.sin(cone_angle) * math.sin(azimuth), + math.cos(cone_angle) + ) + + # 将锥形方向转换到发射方向 + # 这里简化处理,实际应该做完整的旋转变换 + base_velocity = base_velocity.length() * cone_dir + + return base_velocity + + def _vary_color(self, base_color: Vec3) -> Vec3: + """添加颜色变化""" + if self.color_variation <= 0: + return Vec3(base_color) + + variation = Vec3( + random.uniform(-self.color_variation, self.color_variation), + random.uniform(-self.color_variation, self.color_variation), + random.uniform(-self.color_variation, self.color_variation) + ) + + result = base_color + variation + + # 确保颜色值在有效范围内 + result.x = max(0.0, min(1.0, result.x)) + result.y = max(0.0, min(1.0, result.y)) + result.z = max(0.0, min(1.0, result.z)) + + return result + + def _update_particles(self, dt: float): + """更新所有粒子""" + for particle in self.particles: + if particle.is_alive(): + particle.update(dt) + + def _cleanup_dead_particles(self): + """清理死亡粒子""" + alive_particles = [] + + for particle in self.particles: + if particle.is_alive(): + alive_particles.append(particle) + else: + # 回收到对象池 + self.particle_pool.append(particle) + + self.particles = alive_particles + + def get_particles(self) -> List[Particle]: + """获取所有活跃粒子""" + return [p for p in self.particles if p.is_alive()] + + def get_particle_count(self) -> int: + """获取粒子数量""" + return len([p for p in self.particles if p.is_alive()]) + + def is_active(self) -> bool: + """检查发射器是否活跃""" + return self.active or len(self.particles) > 0 + + def should_remove(self) -> bool: + """检查是否应该移除发射器""" + return not self.active and len(self.particles) == 0 + + def stop_emission(self): + """停止发射新粒子""" + self.active = False + + def resume_emission(self): + """恢复发射粒子""" + self.active = True + + def set_position(self, position: Point3): + """设置发射器位置""" + self.position = position + + def set_enabled(self, enabled: bool): + """设置发射器启用状态""" + self.enabled = enabled + + def cleanup(self): + """清理发射器资源""" + self.particles.clear() + self.particle_pool.clear() + self.active = False self.enabled = False \ No newline at end of file diff --git a/plugins/user/particle_system/core/particle_manager.py b/plugins/user/particle_system/core/particle_manager.py index a0eda501..b8ed83b4 100644 --- a/plugins/user/particle_system/core/particle_manager.py +++ b/plugins/user/particle_system/core/particle_manager.py @@ -1,455 +1,455 @@ -""" -粒子管理器 -负责粒子系统的整体管理和协调 -""" - -import time -import random -from typing import List, Dict, Optional -from panda3d.core import Vec3, Point3, NodePath -from direct.task import Task - -from .particle_emitter import ParticleEmitter -from .particle_renderer import ParticleRenderer -from .particle_physics import ParticlePhysics - -class ParticleManager: - """ - 粒子管理器 - 统一管理所有粒子发射器、渲染器和物理系统 - """ - - def __init__(self, world): - """ - 初始化粒子管理器 - - Args: - world: 3D世界对象 - """ - self.world = world - self.enabled = False - - # 核心组件 - self.renderer = ParticleRenderer(world) - self.physics = ParticlePhysics(world) - - # 粒子发射器管理 - self.emitters: Dict[str, ParticleEmitter] = {} - self.emitter_counter = 0 - - # 全局粒子设置 - self.global_time_scale = 1.0 - self.max_particles = 10000 - self.current_particle_count = 0 - - # 性能统计 - self.stats = { - 'total_particles': 0, - 'active_emitters': 0, - 'render_time': 0.0, - 'physics_time': 0.0, - 'update_time': 0.0 - } - - # 粒子系统设置 - self.auto_cleanup = True # 自动清理已完成的发射器 - self.emit_rate_multiplier = 1.0 # 发射速率倍增器 - self.particle_size_multiplier = 1.0 # 粒子尺寸倍增器 - - # 任务管理 - self.update_task = None - - print("✓ 粒子管理器初始化完成") - - def enable(self): - """启用粒子管理器""" - if self.enabled: - return - - self.enabled = True - - # 启动更新任务 - self.update_task = self.world.taskMgr.add( - self._update_task, - "particle_manager_update" - ) - - # 启用渲染器和物理系统 - self.renderer.enable() - self.physics.enable() - - print("✓ 粒子管理器已启用") - - def disable(self): - """禁用粒子管理器""" - if not self.enabled: - return - - self.enabled = False - - # 停止更新任务 - if self.update_task: - self.world.taskMgr.remove(self.update_task) - self.update_task = None - - # 禁用渲染器和物理系统 - self.renderer.disable() - self.physics.disable() - - # 清除所有粒子 - self.clear_all_particles() - - print("✓ 粒子管理器已禁用") - - def cleanup(self): - """清理资源""" - self.disable() - - # 清理组件 - if self.renderer: - self.renderer.cleanup() - self.renderer = None - - if self.physics: - self.physics.cleanup() - self.physics = None - - self.emitters.clear() - - def _update_task(self, task): - """粒子系统更新任务""" - if not self.enabled: - return task.done - - dt = globalClock.getDt() * self.global_time_scale - start_time = time.time() - - try: - # 更新所有发射器 - active_emitters = 0 - total_particles = 0 - - # 创建一个发射器列表的副本,以防在迭代过程中有发射器被删除 - emitters_copy = list(self.emitters.items()) - - for emitter_id, emitter in emitters_copy: - if emitter.is_active(): - emitter.update(dt) - active_emitters += 1 - total_particles += emitter.get_particle_count() - elif emitter.should_remove() and self.auto_cleanup: - # 移除已完成的发射器 - self.remove_emitter(emitter_id) - - # 更新物理系统 - physics_start = time.time() - self.physics.update(dt) - self.stats['physics_time'] = time.time() - physics_start - - # 更新渲染系统 - render_start = time.time() - self.renderer.update(dt) - self.stats['render_time'] = time.time() - render_start - - # 更新统计信息 - self.stats['active_emitters'] = active_emitters - self.stats['total_particles'] = total_particles - self.current_particle_count = total_particles - - except Exception as e: - print(f"⚠ 粒子系统更新错误: {e}") - - self.stats['update_time'] = time.time() - start_time - return task.cont - - def create_emitter(self, emitter_type: str = "default", - position: Point3 = None, - config: Dict = None) -> str: - """ - 创建粒子发射器 - - Args: - emitter_type: 发射器类型 - position: 发射器位置 - config: 发射器配置 - - Returns: - 发射器ID - """ - if position is None: - position = Point3(0, 0, 0) - - if config is None: - config = {} - - # 应用全局设置 - if 'emission_rate' in config: - config['emission_rate'] *= self.emit_rate_multiplier - - if 'start_size' in config: - config['start_size'] *= self.particle_size_multiplier - - if 'end_size' in config: - config['end_size'] *= self.particle_size_multiplier - - # 生成唯一ID - emitter_id = f"emitter_{self.emitter_counter}" - self.emitter_counter += 1 - - # 创建发射器 - emitter = ParticleEmitter( - emitter_id, - emitter_type, - position, - self.world, - config - ) - - # 注册到渲染器和物理系统 - self.renderer.register_emitter(emitter) - self.physics.register_emitter(emitter) - - # 添加到管理器 - self.emitters[emitter_id] = emitter - - print(f"✓ 创建粒子发射器: {emitter_id} ({emitter_type})") - return emitter_id - - def remove_emitter(self, emitter_id: str): - """ - 移除粒子发射器 - - Args: - emitter_id: 发射器ID - """ - if emitter_id not in self.emitters: - return - - emitter = self.emitters[emitter_id] - - # 从渲染器和物理系统注销 - self.renderer.unregister_emitter(emitter) - self.physics.unregister_emitter(emitter) - - # 清理发射器 - emitter.cleanup() - - # 从管理器移除 - del self.emitters[emitter_id] - - print(f"✓ 移除粒子发射器: {emitter_id}") - - def get_emitter(self, emitter_id: str) -> Optional[ParticleEmitter]: - """获取粒子发射器""" - return self.emitters.get(emitter_id) - - def clear_all_particles(self): - """清除所有粒子""" - # 创建一个发射器ID列表的副本,以防在迭代过程中字典被修改 - emitter_ids = list(self.emitters.keys()) - - for emitter_id in emitter_ids: - self.remove_emitter(emitter_id) - - if self.renderer: - self.renderer.clear_all() - - if self.physics: - self.physics.clear_all() - - print("✓ 已清除所有粒子") - - def set_global_time_scale(self, scale: float): - """设置全局时间缩放""" - self.global_time_scale = max(0.0, scale) - - def get_particle_count(self) -> int: - """获取当前粒子总数""" - return self.current_particle_count - - def get_emitter_count(self) -> int: - """获取活跃发射器数量""" - return len([e for e in self.emitters.values() if e.is_active()]) - - def get_stats(self) -> Dict: - """获取性能统计信息""" - return self.stats.copy() - - def set_max_particles(self, max_count: int): - """设置最大粒子数""" - self.max_particles = max(1, max_count) - - def set_auto_cleanup(self, enabled: bool): - """设置自动清理""" - self.auto_cleanup = enabled - - def set_emit_rate_multiplier(self, multiplier: float): - """设置发射速率倍增器""" - self.emit_rate_multiplier = max(0.0, multiplier) - - def set_particle_size_multiplier(self, multiplier: float): - """设置粒子尺寸倍增器""" - self.particle_size_multiplier = max(0.0, multiplier) - - # 预设效果创建方法 - def create_fire_effect(self, position: Point3 = None) -> str: - """创建火焰效果""" - if position is None: - position = Point3(0, 0, 0) - - config = { - 'particle_type': 'fire', - 'emission_rate': 50, - 'particle_lifetime': 2.0, - 'start_color': Vec3(1.0, 0.3, 0.0), - 'end_color': Vec3(1.0, 0.0, 0.0), - 'start_size': 0.2, - 'end_size': 0.8, - 'velocity': Vec3(0, 0, 2), - 'velocity_variation': 0.5, - 'gravity_scale': -0.2, - 'air_resistance': 0.02, - 'emitter_shape': 'circle', - 'emitter_radius': 0.3 - } - - return self.create_emitter("fire", position, config) - - def create_smoke_effect(self, position: Point3 = None) -> str: - """创建烟雾效果""" - if position is None: - position = Point3(0, 0, 0) - - config = { - 'particle_type': 'smoke', - 'emission_rate': 30, - 'particle_lifetime': 4.0, - 'start_color': Vec3(0.8, 0.8, 0.8), - 'end_color': Vec3(0.3, 0.3, 0.3), - 'start_size': 0.3, - 'end_size': 1.5, - 'velocity': Vec3(0, 0, 1), - 'velocity_variation': 0.3, - 'gravity_scale': -0.1, - 'air_resistance': 0.05, - 'emitter_shape': 'circle', - 'emitter_radius': 0.5 - } - - return self.create_emitter("smoke", position, config) - - def create_explosion_effect(self, position: Point3 = None) -> str: - """创建爆炸效果""" - if position is None: - position = Point3(0, 0, 0) - - config = { - 'particle_type': 'explosion', - 'emission_rate': 200, - 'burst_count': 100, - 'particle_lifetime': 1.5, - 'start_color': Vec3(1.0, 0.8, 0.0), - 'end_color': Vec3(1.0, 0.2, 0.0), - 'start_size': 0.1, - 'end_size': 0.5, - 'velocity': Vec3(0, 0, 0), - 'velocity_variation': 2.0, - 'gravity_scale': 0.5, - 'duration': 0.2, - 'emitter_shape': 'sphere', - 'emitter_radius': 0.1 - } - - return self.create_emitter("explosion", position, config) - - def create_water_effect(self, position: Point3 = None) -> str: - """创建水花效果""" - if position is None: - position = Point3(0, 0, 0) - - config = { - 'particle_type': 'water', - 'emission_rate': 80, - 'particle_lifetime': 3.0, - 'start_color': Vec3(0.2, 0.6, 1.0), - 'end_color': Vec3(0.1, 0.3, 0.8), - 'start_size': 0.1, - 'end_size': 0.05, - 'velocity': Vec3(0, 0, 3), - 'velocity_variation': 1.0, - 'gravity_scale': 2.0, - 'bounce_factor': 0.3, - 'air_resistance': 0.01, - 'emitter_shape': 'circle', - 'emitter_radius': 0.2 - } - - return self.create_emitter("water", position, config) - - def create_magic_effect(self, position: Point3 = None) -> str: - """创建魔法效果""" - if position is None: - position = Point3(0, 0, 0) - - config = { - 'particle_type': 'magic', - 'emission_rate': 40, - 'particle_lifetime': 3.0, - 'start_color': Vec3(0.5, 0.2, 1.0), - 'end_color': Vec3(0.8, 0.6, 1.0), - 'start_size': 0.15, - 'end_size': 0.05, - 'velocity': Vec3(0, 0, 0.5), - 'velocity_variation': 0.8, - 'gravity_scale': -0.5, - 'rotation_enabled': True, - 'rotation_speed': 2.0, - 'emitter_shape': 'sphere', - 'emitter_radius': 0.8 - } - - return self.create_emitter("magic", position, config) - - def create_snow_effect(self, position: Point3 = None) -> str: - """创建雪花效果""" - if position is None: - position = Point3(0, 0, 10) # 从高处开始 - - config = { - 'particle_type': 'snow', - 'emission_rate': 60, - 'particle_lifetime': 8.0, - 'start_color': Vec3(0.9, 0.9, 1.0), - 'end_color': Vec3(0.9, 0.9, 1.0), - 'start_size': 0.05, - 'end_size': 0.05, - 'velocity': Vec3(0, 0, -1), - 'velocity_variation': 0.5, - 'gravity_scale': 0.8, - 'air_resistance': 0.1, - 'bounce_factor': 0.2, - 'emitter_shape': 'box', - 'emitter_dimensions': Vec3(20, 20, 1) - } - - return self.create_emitter("snow", position, config) - - def create_test_effect(self, position: Point3 = None) -> str: - """创建测试效果""" - if position is None: - # 在摄像机前方创建 - camera_pos = self.world.camera.getPos() - camera_forward = self.world.camera.getNetTransform().getMat().getRow3(1) - position = camera_pos + camera_forward * 5 - - # 随机选择一种效果 - effects = [ - self.create_fire_effect, - self.create_smoke_effect, - self.create_explosion_effect, - self.create_water_effect, - self.create_magic_effect - ] - - effect_func = random.choice(effects) +""" +粒子管理器 +负责粒子系统的整体管理和协调 +""" + +import time +import random +from typing import List, Dict, Optional +from panda3d.core import Vec3, Point3, NodePath +from direct.task import Task + +from .particle_emitter import ParticleEmitter +from .particle_renderer import ParticleRenderer +from .particle_physics import ParticlePhysics + +class ParticleManager: + """ + 粒子管理器 + 统一管理所有粒子发射器、渲染器和物理系统 + """ + + def __init__(self, world): + """ + 初始化粒子管理器 + + Args: + world: 3D世界对象 + """ + self.world = world + self.enabled = False + + # 核心组件 + self.renderer = ParticleRenderer(world) + self.physics = ParticlePhysics(world) + + # 粒子发射器管理 + self.emitters: Dict[str, ParticleEmitter] = {} + self.emitter_counter = 0 + + # 全局粒子设置 + self.global_time_scale = 1.0 + self.max_particles = 10000 + self.current_particle_count = 0 + + # 性能统计 + self.stats = { + 'total_particles': 0, + 'active_emitters': 0, + 'render_time': 0.0, + 'physics_time': 0.0, + 'update_time': 0.0 + } + + # 粒子系统设置 + self.auto_cleanup = True # 自动清理已完成的发射器 + self.emit_rate_multiplier = 1.0 # 发射速率倍增器 + self.particle_size_multiplier = 1.0 # 粒子尺寸倍增器 + + # 任务管理 + self.update_task = None + + print("✓ 粒子管理器初始化完成") + + def enable(self): + """启用粒子管理器""" + if self.enabled: + return + + self.enabled = True + + # 启动更新任务 + self.update_task = self.world.taskMgr.add( + self._update_task, + "particle_manager_update" + ) + + # 启用渲染器和物理系统 + self.renderer.enable() + self.physics.enable() + + print("✓ 粒子管理器已启用") + + def disable(self): + """禁用粒子管理器""" + if not self.enabled: + return + + self.enabled = False + + # 停止更新任务 + if self.update_task: + self.world.taskMgr.remove(self.update_task) + self.update_task = None + + # 禁用渲染器和物理系统 + self.renderer.disable() + self.physics.disable() + + # 清除所有粒子 + self.clear_all_particles() + + print("✓ 粒子管理器已禁用") + + def cleanup(self): + """清理资源""" + self.disable() + + # 清理组件 + if self.renderer: + self.renderer.cleanup() + self.renderer = None + + if self.physics: + self.physics.cleanup() + self.physics = None + + self.emitters.clear() + + def _update_task(self, task): + """粒子系统更新任务""" + if not self.enabled: + return task.done + + dt = globalClock.getDt() * self.global_time_scale + start_time = time.time() + + try: + # 更新所有发射器 + active_emitters = 0 + total_particles = 0 + + # 创建一个发射器列表的副本,以防在迭代过程中有发射器被删除 + emitters_copy = list(self.emitters.items()) + + for emitter_id, emitter in emitters_copy: + if emitter.is_active(): + emitter.update(dt) + active_emitters += 1 + total_particles += emitter.get_particle_count() + elif emitter.should_remove() and self.auto_cleanup: + # 移除已完成的发射器 + self.remove_emitter(emitter_id) + + # 更新物理系统 + physics_start = time.time() + self.physics.update(dt) + self.stats['physics_time'] = time.time() - physics_start + + # 更新渲染系统 + render_start = time.time() + self.renderer.update(dt) + self.stats['render_time'] = time.time() - render_start + + # 更新统计信息 + self.stats['active_emitters'] = active_emitters + self.stats['total_particles'] = total_particles + self.current_particle_count = total_particles + + except Exception as e: + print(f"⚠ 粒子系统更新错误: {e}") + + self.stats['update_time'] = time.time() - start_time + return task.cont + + def create_emitter(self, emitter_type: str = "default", + position: Point3 = None, + config: Dict = None) -> str: + """ + 创建粒子发射器 + + Args: + emitter_type: 发射器类型 + position: 发射器位置 + config: 发射器配置 + + Returns: + 发射器ID + """ + if position is None: + position = Point3(0, 0, 0) + + if config is None: + config = {} + + # 应用全局设置 + if 'emission_rate' in config: + config['emission_rate'] *= self.emit_rate_multiplier + + if 'start_size' in config: + config['start_size'] *= self.particle_size_multiplier + + if 'end_size' in config: + config['end_size'] *= self.particle_size_multiplier + + # 生成唯一ID + emitter_id = f"emitter_{self.emitter_counter}" + self.emitter_counter += 1 + + # 创建发射器 + emitter = ParticleEmitter( + emitter_id, + emitter_type, + position, + self.world, + config + ) + + # 注册到渲染器和物理系统 + self.renderer.register_emitter(emitter) + self.physics.register_emitter(emitter) + + # 添加到管理器 + self.emitters[emitter_id] = emitter + + print(f"✓ 创建粒子发射器: {emitter_id} ({emitter_type})") + return emitter_id + + def remove_emitter(self, emitter_id: str): + """ + 移除粒子发射器 + + Args: + emitter_id: 发射器ID + """ + if emitter_id not in self.emitters: + return + + emitter = self.emitters[emitter_id] + + # 从渲染器和物理系统注销 + self.renderer.unregister_emitter(emitter) + self.physics.unregister_emitter(emitter) + + # 清理发射器 + emitter.cleanup() + + # 从管理器移除 + del self.emitters[emitter_id] + + print(f"✓ 移除粒子发射器: {emitter_id}") + + def get_emitter(self, emitter_id: str) -> Optional[ParticleEmitter]: + """获取粒子发射器""" + return self.emitters.get(emitter_id) + + def clear_all_particles(self): + """清除所有粒子""" + # 创建一个发射器ID列表的副本,以防在迭代过程中字典被修改 + emitter_ids = list(self.emitters.keys()) + + for emitter_id in emitter_ids: + self.remove_emitter(emitter_id) + + if self.renderer: + self.renderer.clear_all() + + if self.physics: + self.physics.clear_all() + + print("✓ 已清除所有粒子") + + def set_global_time_scale(self, scale: float): + """设置全局时间缩放""" + self.global_time_scale = max(0.0, scale) + + def get_particle_count(self) -> int: + """获取当前粒子总数""" + return self.current_particle_count + + def get_emitter_count(self) -> int: + """获取活跃发射器数量""" + return len([e for e in self.emitters.values() if e.is_active()]) + + def get_stats(self) -> Dict: + """获取性能统计信息""" + return self.stats.copy() + + def set_max_particles(self, max_count: int): + """设置最大粒子数""" + self.max_particles = max(1, max_count) + + def set_auto_cleanup(self, enabled: bool): + """设置自动清理""" + self.auto_cleanup = enabled + + def set_emit_rate_multiplier(self, multiplier: float): + """设置发射速率倍增器""" + self.emit_rate_multiplier = max(0.0, multiplier) + + def set_particle_size_multiplier(self, multiplier: float): + """设置粒子尺寸倍增器""" + self.particle_size_multiplier = max(0.0, multiplier) + + # 预设效果创建方法 + def create_fire_effect(self, position: Point3 = None) -> str: + """创建火焰效果""" + if position is None: + position = Point3(0, 0, 0) + + config = { + 'particle_type': 'fire', + 'emission_rate': 50, + 'particle_lifetime': 2.0, + 'start_color': Vec3(1.0, 0.3, 0.0), + 'end_color': Vec3(1.0, 0.0, 0.0), + 'start_size': 0.2, + 'end_size': 0.8, + 'velocity': Vec3(0, 0, 2), + 'velocity_variation': 0.5, + 'gravity_scale': -0.2, + 'air_resistance': 0.02, + 'emitter_shape': 'circle', + 'emitter_radius': 0.3 + } + + return self.create_emitter("fire", position, config) + + def create_smoke_effect(self, position: Point3 = None) -> str: + """创建烟雾效果""" + if position is None: + position = Point3(0, 0, 0) + + config = { + 'particle_type': 'smoke', + 'emission_rate': 30, + 'particle_lifetime': 4.0, + 'start_color': Vec3(0.8, 0.8, 0.8), + 'end_color': Vec3(0.3, 0.3, 0.3), + 'start_size': 0.3, + 'end_size': 1.5, + 'velocity': Vec3(0, 0, 1), + 'velocity_variation': 0.3, + 'gravity_scale': -0.1, + 'air_resistance': 0.05, + 'emitter_shape': 'circle', + 'emitter_radius': 0.5 + } + + return self.create_emitter("smoke", position, config) + + def create_explosion_effect(self, position: Point3 = None) -> str: + """创建爆炸效果""" + if position is None: + position = Point3(0, 0, 0) + + config = { + 'particle_type': 'explosion', + 'emission_rate': 200, + 'burst_count': 100, + 'particle_lifetime': 1.5, + 'start_color': Vec3(1.0, 0.8, 0.0), + 'end_color': Vec3(1.0, 0.2, 0.0), + 'start_size': 0.1, + 'end_size': 0.5, + 'velocity': Vec3(0, 0, 0), + 'velocity_variation': 2.0, + 'gravity_scale': 0.5, + 'duration': 0.2, + 'emitter_shape': 'sphere', + 'emitter_radius': 0.1 + } + + return self.create_emitter("explosion", position, config) + + def create_water_effect(self, position: Point3 = None) -> str: + """创建水花效果""" + if position is None: + position = Point3(0, 0, 0) + + config = { + 'particle_type': 'water', + 'emission_rate': 80, + 'particle_lifetime': 3.0, + 'start_color': Vec3(0.2, 0.6, 1.0), + 'end_color': Vec3(0.1, 0.3, 0.8), + 'start_size': 0.1, + 'end_size': 0.05, + 'velocity': Vec3(0, 0, 3), + 'velocity_variation': 1.0, + 'gravity_scale': 2.0, + 'bounce_factor': 0.3, + 'air_resistance': 0.01, + 'emitter_shape': 'circle', + 'emitter_radius': 0.2 + } + + return self.create_emitter("water", position, config) + + def create_magic_effect(self, position: Point3 = None) -> str: + """创建魔法效果""" + if position is None: + position = Point3(0, 0, 0) + + config = { + 'particle_type': 'magic', + 'emission_rate': 40, + 'particle_lifetime': 3.0, + 'start_color': Vec3(0.5, 0.2, 1.0), + 'end_color': Vec3(0.8, 0.6, 1.0), + 'start_size': 0.15, + 'end_size': 0.05, + 'velocity': Vec3(0, 0, 0.5), + 'velocity_variation': 0.8, + 'gravity_scale': -0.5, + 'rotation_enabled': True, + 'rotation_speed': 2.0, + 'emitter_shape': 'sphere', + 'emitter_radius': 0.8 + } + + return self.create_emitter("magic", position, config) + + def create_snow_effect(self, position: Point3 = None) -> str: + """创建雪花效果""" + if position is None: + position = Point3(0, 0, 10) # 从高处开始 + + config = { + 'particle_type': 'snow', + 'emission_rate': 60, + 'particle_lifetime': 8.0, + 'start_color': Vec3(0.9, 0.9, 1.0), + 'end_color': Vec3(0.9, 0.9, 1.0), + 'start_size': 0.05, + 'end_size': 0.05, + 'velocity': Vec3(0, 0, -1), + 'velocity_variation': 0.5, + 'gravity_scale': 0.8, + 'air_resistance': 0.1, + 'bounce_factor': 0.2, + 'emitter_shape': 'box', + 'emitter_dimensions': Vec3(20, 20, 1) + } + + return self.create_emitter("snow", position, config) + + def create_test_effect(self, position: Point3 = None) -> str: + """创建测试效果""" + if position is None: + # 在摄像机前方创建 + camera_pos = self.world.camera.getPos() + camera_forward = self.world.camera.getNetTransform().getMat().getRow3(1) + position = camera_pos + camera_forward * 5 + + # 随机选择一种效果 + effects = [ + self.create_fire_effect, + self.create_smoke_effect, + self.create_explosion_effect, + self.create_water_effect, + self.create_magic_effect + ] + + effect_func = random.choice(effects) return effect_func(position) \ No newline at end of file diff --git a/plugins/user/particle_system/core/particle_physics.py b/plugins/user/particle_system/core/particle_physics.py index e129f284..43885188 100644 --- a/plugins/user/particle_system/core/particle_physics.py +++ b/plugins/user/particle_system/core/particle_physics.py @@ -1,384 +1,384 @@ -""" -粒子物理系统 -负责粒子的物理模拟和环境交互 -""" - -import math -import random -from typing import List, Dict, Optional -from panda3d.core import Vec3, Point3, BitMask32 -from panda3d.bullet import BulletWorld, BulletRigidBodyNode, BulletSphereShape - -from .particle_emitter import ParticleEmitter, Particle - -class ParticlePhysics: - """ - 粒子物理系统 - 处理粒子的物理模拟,包括重力、碰撞、力场等 - """ - - def __init__(self, world): - """ - 初始化粒子物理系统 - - Args: - world: 3D世界对象 - """ - self.world = world - self.enabled = False - - # 物理世界(如果可用) - self.bullet_world = None - self._init_bullet_world() - - # 全局物理参数 - self.gravity = Vec3(0, 0, -9.81) - self.air_density = 1.225 # kg/m³ - self.wind_velocity = Vec3(0, 0, 0) - self.wind_turbulence = 0.0 - - # 环境力场 - self.force_fields = [] - - # 碰撞检测 - self.collision_enabled = True - self.ground_level = 0.0 - self.bounce_damping = 0.8 - - # 注册的发射器 - self.registered_emitters: Dict[str, ParticleEmitter] = {} - - # 物理统计 - self.physics_stats = { - 'collision_checks': 0, - 'collisions_detected': 0, - 'force_calculations': 0, - 'physics_time': 0.0 - } - - print("✓ 粒子物理系统初始化完成") - - def _init_bullet_world(self): - """初始化Bullet物理世界""" - try: - # 尝试获取现有的物理世界 - if hasattr(self.world, 'physics_world') and self.world.physics_world: - self.bullet_world = self.world.physics_world.bullet_world - print("✓ 使用现有Bullet物理世界") - else: - print("⚠ 未找到Bullet物理世界,使用简化物理模拟") - except Exception as e: - print(f"⚠ Bullet物理世界初始化失败: {e}") - - def enable(self): - """启用物理系统""" - if self.enabled: - return - - self.enabled = True - print("✓ 粒子物理系统已启用") - - def disable(self): - """禁用物理系统""" - if not self.enabled: - return - - self.enabled = False - print("✓ 粒子物理系统已禁用") - - def cleanup(self): - """清理资源""" - self.disable() - self.registered_emitters.clear() - self.force_fields.clear() - - def register_emitter(self, emitter: ParticleEmitter): - """注册粒子发射器""" - self.registered_emitters[emitter.emitter_id] = emitter - - def unregister_emitter(self, emitter: ParticleEmitter): - """注销粒子发射器""" - emitter_id = emitter.emitter_id - if emitter_id in self.registered_emitters: - del self.registered_emitters[emitter_id] - - def update(self, dt: float): - """更新物理模拟""" - if not self.enabled: - return - - import time - start_time = time.time() - - # 重置统计信息 - self.physics_stats['collision_checks'] = 0 - self.physics_stats['collisions_detected'] = 0 - self.physics_stats['force_calculations'] = 0 - - # 更新每个发射器的粒子物理 - for emitter in self.registered_emitters.values(): - self._update_emitter_physics(emitter, dt) - - self.physics_stats['physics_time'] = time.time() - start_time - - def _update_emitter_physics(self, emitter: ParticleEmitter, dt: float): - """更新发射器的粒子物理""" - particles = emitter.get_particles() - - for particle in particles: - if not particle.is_alive(): - continue - - # 计算作用在粒子上的所有力 - total_force = self._calculate_particle_forces(particle) - - # 应用力到粒子 - if particle.mass > 0: - acceleration = total_force / particle.mass - particle.acceleration = acceleration - else: - particle.acceleration = Vec3(0, 0, 0) - - # 更新速度和位置(使用欧拉积分) - particle.velocity += particle.acceleration * dt - particle.position += particle.velocity * dt - - # 碰撞检测 - if self.collision_enabled: - self._handle_particle_collision(particle, dt) - - self.physics_stats['force_calculations'] += 1 - - def _calculate_particle_forces(self, particle: Particle) -> Vec3: - """计算作用在粒子上的所有力""" - total_force = Vec3(0, 0, 0) - - # 重力 - gravity_force = self.gravity * particle.mass * particle.gravity_scale - total_force += gravity_force - - # 空气阻力 - drag_force = self._calculate_drag_force(particle) - total_force += drag_force - - # 风力 - wind_force = self._calculate_wind_force(particle) - total_force += wind_force - - # 力场 - for force_field in self.force_fields: - field_force = self._calculate_force_field(particle, force_field) - total_force += field_force - - return total_force - - def _calculate_drag_force(self, particle: Particle) -> Vec3: - """计算空气阻力""" - if particle.velocity.lengthSquared() == 0: - return Vec3(0, 0, 0) - - # 简化的阻力模型: F = -k * v * |v| - velocity_magnitude = particle.velocity.length() - drag_coefficient = particle.air_resistance - - drag_force = -particle.velocity * drag_coefficient * velocity_magnitude - return drag_force - - def _calculate_wind_force(self, particle: Particle) -> Vec3: - """计算风力""" - if self.wind_velocity.lengthSquared() == 0 and self.wind_turbulence == 0: - return Vec3(0, 0, 0) - - # 相对风速 - relative_wind = Vec3(self.wind_velocity) - - # 添加湍流 - if self.wind_turbulence > 0: - turbulence = Vec3( - random.uniform(-self.wind_turbulence, self.wind_turbulence), - random.uniform(-self.wind_turbulence, self.wind_turbulence), - random.uniform(-self.wind_turbulence, self.wind_turbulence) - ) - relative_wind += turbulence - - # 风力 = 空气密度 * 阻力系数 * 面积 * 相对风速² - # 简化为: F = k * relative_wind * |relative_wind| - wind_magnitude = relative_wind.length() - if wind_magnitude > 0: - wind_coefficient = 0.1 # 可调参数 - wind_force = relative_wind * wind_coefficient * wind_magnitude - return wind_force - - return Vec3(0, 0, 0) - - def _calculate_force_field(self, particle: Particle, force_field: Dict) -> Vec3: - """计算力场作用力""" - field_type = force_field.get('type', 'point') - field_position = force_field.get('position', Point3(0, 0, 0)) - field_strength = force_field.get('strength', 1.0) - field_radius = force_field.get('radius', 10.0) - - # 计算到力场中心的距离 - to_field = field_position - particle.position - distance = to_field.length() - - if distance > field_radius or distance == 0: - return Vec3(0, 0, 0) - - if field_type == 'point': - # 点力场(引力/斥力) - direction = to_field / distance - # 力的大小与距离平方成反比 - force_magnitude = field_strength / (distance * distance + 0.01) # 添加小值避免除零 - return direction * force_magnitude - - elif field_type == 'vortex': - # 涡流力场 - axis = force_field.get('axis', Vec3(0, 0, 1)) - axis.normalize() - - # 计算切向力 - radial_vector = to_field - to_field.dot(axis) * axis - if radial_vector.length() > 0: - radial_vector.normalize() - tangent = axis.cross(radial_vector) - force_magnitude = field_strength / (distance + 0.01) - return tangent * force_magnitude - - elif field_type == 'directional': - # 方向力场(如风) - direction = force_field.get('direction', Vec3(0, 0, 1)) - direction.normalize() - falloff = max(0, 1.0 - distance / field_radius) - return direction * field_strength * falloff - - return Vec3(0, 0, 0) - - def _handle_particle_collision(self, particle: Particle, dt: float): - """处理粒子碰撞""" - self.physics_stats['collision_checks'] += 1 - - # 简单的地面碰撞检测 - if particle.position.z <= self.ground_level and particle.velocity.z < 0: - # 反弹 - particle.velocity.z = -particle.velocity.z * particle.bounce_factor * self.bounce_damping - particle.position.z = self.ground_level + 0.01 # 稍微抬高避免穿透 - - # 应用摩擦力 - friction = 0.9 - particle.velocity.x *= friction - particle.velocity.y *= friction - - self.physics_stats['collisions_detected'] += 1 - - # 墙壁碰撞检测 - # 假设有一个边界盒子 [-50, 50] x [-50, 50] x [0, 100] - boundary_min = Point3(-50, -50, 0) - boundary_max = Point3(50, 50, 100) - - # X轴边界 - if particle.position.x <= boundary_min.x and particle.velocity.x < 0: - particle.velocity.x = -particle.velocity.x * particle.bounce_factor - particle.position.x = boundary_min.x + 0.01 - self.physics_stats['collisions_detected'] += 1 - elif particle.position.x >= boundary_max.x and particle.velocity.x > 0: - particle.velocity.x = -particle.velocity.x * particle.bounce_factor - particle.position.x = boundary_max.x - 0.01 - self.physics_stats['collisions_detected'] += 1 - - # Y轴边界 - if particle.position.y <= boundary_min.y and particle.velocity.y < 0: - particle.velocity.y = -particle.velocity.y * particle.bounce_factor - particle.position.y = boundary_min.y + 0.01 - self.physics_stats['collisions_detected'] += 1 - elif particle.position.y >= boundary_max.y and particle.velocity.y > 0: - particle.velocity.y = -particle.velocity.y * particle.bounce_factor - particle.position.y = boundary_max.y - 0.01 - self.physics_stats['collisions_detected'] += 1 - - # Z轴上边界 - if particle.position.z >= boundary_max.z and particle.velocity.z > 0: - particle.velocity.z = -particle.velocity.z * particle.bounce_factor - particle.position.z = boundary_max.z - 0.01 - self.physics_stats['collisions_detected'] += 1 - - # 如果有Bullet物理世界,可以进行更复杂的碰撞检测 - if self.bullet_world: - self._handle_bullet_collision(particle, dt) - - def _handle_bullet_collision(self, particle: Particle, dt: float): - """使用Bullet进行碰撞检测""" - # 这里可以实现更复杂的碰撞检测 - # 由于粒子数量可能很大,通常只对重要粒子进行精确碰撞检测 - pass - - def add_force_field(self, field_type: str, position: Point3, - strength: float, radius: float, **kwargs): - """添加力场""" - force_field = { - 'type': field_type, - 'position': position, - 'strength': strength, - 'radius': radius, - **kwargs - } - self.force_fields.append(force_field) - print(f"✓ 添加力场: {field_type} at {position}") - - def remove_force_field(self, index: int): - """移除力场""" - if 0 <= index < len(self.force_fields): - del self.force_fields[index] - print(f"✓ 移除力场: {index}") - - def clear_force_fields(self): - """清除所有力场""" - self.force_fields.clear() - print("✓ 清除所有力场") - - def set_gravity(self, gravity: Vec3): - """设置重力""" - self.gravity = gravity - - def set_wind(self, velocity: Vec3, turbulence: float = 0.0): - """设置风力""" - self.wind_velocity = velocity - self.wind_turbulence = turbulence - - def set_ground_level(self, level: float): - """设置地面高度""" - self.ground_level = level - - def set_collision_enabled(self, enabled: bool): - """设置碰撞检测""" - self.collision_enabled = enabled - - def get_physics_stats(self) -> Dict: - """获取物理统计信息""" - return self.physics_stats.copy() - - def clear_all(self): - """清除所有物理状态""" - self.force_fields.clear() - - # 预设力场创建方法 - def create_gravity_well(self, position: Point3, strength: float = 10.0, radius: float = 5.0): - """创建重力井""" - self.add_force_field('point', position, strength, radius) - - def create_repulsion_field(self, position: Point3, strength: float = -10.0, radius: float = 3.0): - """创建排斥力场""" - self.add_force_field('point', position, strength, radius) - - def create_vortex(self, position: Point3, strength: float = 5.0, - radius: float = 4.0, axis: Vec3 = None): - """创建涡流""" - if axis is None: - axis = Vec3(0, 0, 1) - self.add_force_field('vortex', position, strength, radius, axis=axis) - - def create_wind_zone(self, position: Point3, direction: Vec3, - strength: float = 2.0, radius: float = 8.0): - """创建风区""" - direction.normalize() +""" +粒子物理系统 +负责粒子的物理模拟和环境交互 +""" + +import math +import random +from typing import List, Dict, Optional +from panda3d.core import Vec3, Point3, BitMask32 +from panda3d.bullet import BulletWorld, BulletRigidBodyNode, BulletSphereShape + +from .particle_emitter import ParticleEmitter, Particle + +class ParticlePhysics: + """ + 粒子物理系统 + 处理粒子的物理模拟,包括重力、碰撞、力场等 + """ + + def __init__(self, world): + """ + 初始化粒子物理系统 + + Args: + world: 3D世界对象 + """ + self.world = world + self.enabled = False + + # 物理世界(如果可用) + self.bullet_world = None + self._init_bullet_world() + + # 全局物理参数 + self.gravity = Vec3(0, 0, -9.81) + self.air_density = 1.225 # kg/m³ + self.wind_velocity = Vec3(0, 0, 0) + self.wind_turbulence = 0.0 + + # 环境力场 + self.force_fields = [] + + # 碰撞检测 + self.collision_enabled = True + self.ground_level = 0.0 + self.bounce_damping = 0.8 + + # 注册的发射器 + self.registered_emitters: Dict[str, ParticleEmitter] = {} + + # 物理统计 + self.physics_stats = { + 'collision_checks': 0, + 'collisions_detected': 0, + 'force_calculations': 0, + 'physics_time': 0.0 + } + + print("✓ 粒子物理系统初始化完成") + + def _init_bullet_world(self): + """初始化Bullet物理世界""" + try: + # 尝试获取现有的物理世界 + if hasattr(self.world, 'physics_world') and self.world.physics_world: + self.bullet_world = self.world.physics_world.bullet_world + print("✓ 使用现有Bullet物理世界") + else: + print("⚠ 未找到Bullet物理世界,使用简化物理模拟") + except Exception as e: + print(f"⚠ Bullet物理世界初始化失败: {e}") + + def enable(self): + """启用物理系统""" + if self.enabled: + return + + self.enabled = True + print("✓ 粒子物理系统已启用") + + def disable(self): + """禁用物理系统""" + if not self.enabled: + return + + self.enabled = False + print("✓ 粒子物理系统已禁用") + + def cleanup(self): + """清理资源""" + self.disable() + self.registered_emitters.clear() + self.force_fields.clear() + + def register_emitter(self, emitter: ParticleEmitter): + """注册粒子发射器""" + self.registered_emitters[emitter.emitter_id] = emitter + + def unregister_emitter(self, emitter: ParticleEmitter): + """注销粒子发射器""" + emitter_id = emitter.emitter_id + if emitter_id in self.registered_emitters: + del self.registered_emitters[emitter_id] + + def update(self, dt: float): + """更新物理模拟""" + if not self.enabled: + return + + import time + start_time = time.time() + + # 重置统计信息 + self.physics_stats['collision_checks'] = 0 + self.physics_stats['collisions_detected'] = 0 + self.physics_stats['force_calculations'] = 0 + + # 更新每个发射器的粒子物理 + for emitter in self.registered_emitters.values(): + self._update_emitter_physics(emitter, dt) + + self.physics_stats['physics_time'] = time.time() - start_time + + def _update_emitter_physics(self, emitter: ParticleEmitter, dt: float): + """更新发射器的粒子物理""" + particles = emitter.get_particles() + + for particle in particles: + if not particle.is_alive(): + continue + + # 计算作用在粒子上的所有力 + total_force = self._calculate_particle_forces(particle) + + # 应用力到粒子 + if particle.mass > 0: + acceleration = total_force / particle.mass + particle.acceleration = acceleration + else: + particle.acceleration = Vec3(0, 0, 0) + + # 更新速度和位置(使用欧拉积分) + particle.velocity += particle.acceleration * dt + particle.position += particle.velocity * dt + + # 碰撞检测 + if self.collision_enabled: + self._handle_particle_collision(particle, dt) + + self.physics_stats['force_calculations'] += 1 + + def _calculate_particle_forces(self, particle: Particle) -> Vec3: + """计算作用在粒子上的所有力""" + total_force = Vec3(0, 0, 0) + + # 重力 + gravity_force = self.gravity * particle.mass * particle.gravity_scale + total_force += gravity_force + + # 空气阻力 + drag_force = self._calculate_drag_force(particle) + total_force += drag_force + + # 风力 + wind_force = self._calculate_wind_force(particle) + total_force += wind_force + + # 力场 + for force_field in self.force_fields: + field_force = self._calculate_force_field(particle, force_field) + total_force += field_force + + return total_force + + def _calculate_drag_force(self, particle: Particle) -> Vec3: + """计算空气阻力""" + if particle.velocity.lengthSquared() == 0: + return Vec3(0, 0, 0) + + # 简化的阻力模型: F = -k * v * |v| + velocity_magnitude = particle.velocity.length() + drag_coefficient = particle.air_resistance + + drag_force = -particle.velocity * drag_coefficient * velocity_magnitude + return drag_force + + def _calculate_wind_force(self, particle: Particle) -> Vec3: + """计算风力""" + if self.wind_velocity.lengthSquared() == 0 and self.wind_turbulence == 0: + return Vec3(0, 0, 0) + + # 相对风速 + relative_wind = Vec3(self.wind_velocity) + + # 添加湍流 + if self.wind_turbulence > 0: + turbulence = Vec3( + random.uniform(-self.wind_turbulence, self.wind_turbulence), + random.uniform(-self.wind_turbulence, self.wind_turbulence), + random.uniform(-self.wind_turbulence, self.wind_turbulence) + ) + relative_wind += turbulence + + # 风力 = 空气密度 * 阻力系数 * 面积 * 相对风速² + # 简化为: F = k * relative_wind * |relative_wind| + wind_magnitude = relative_wind.length() + if wind_magnitude > 0: + wind_coefficient = 0.1 # 可调参数 + wind_force = relative_wind * wind_coefficient * wind_magnitude + return wind_force + + return Vec3(0, 0, 0) + + def _calculate_force_field(self, particle: Particle, force_field: Dict) -> Vec3: + """计算力场作用力""" + field_type = force_field.get('type', 'point') + field_position = force_field.get('position', Point3(0, 0, 0)) + field_strength = force_field.get('strength', 1.0) + field_radius = force_field.get('radius', 10.0) + + # 计算到力场中心的距离 + to_field = field_position - particle.position + distance = to_field.length() + + if distance > field_radius or distance == 0: + return Vec3(0, 0, 0) + + if field_type == 'point': + # 点力场(引力/斥力) + direction = to_field / distance + # 力的大小与距离平方成反比 + force_magnitude = field_strength / (distance * distance + 0.01) # 添加小值避免除零 + return direction * force_magnitude + + elif field_type == 'vortex': + # 涡流力场 + axis = force_field.get('axis', Vec3(0, 0, 1)) + axis.normalize() + + # 计算切向力 + radial_vector = to_field - to_field.dot(axis) * axis + if radial_vector.length() > 0: + radial_vector.normalize() + tangent = axis.cross(radial_vector) + force_magnitude = field_strength / (distance + 0.01) + return tangent * force_magnitude + + elif field_type == 'directional': + # 方向力场(如风) + direction = force_field.get('direction', Vec3(0, 0, 1)) + direction.normalize() + falloff = max(0, 1.0 - distance / field_radius) + return direction * field_strength * falloff + + return Vec3(0, 0, 0) + + def _handle_particle_collision(self, particle: Particle, dt: float): + """处理粒子碰撞""" + self.physics_stats['collision_checks'] += 1 + + # 简单的地面碰撞检测 + if particle.position.z <= self.ground_level and particle.velocity.z < 0: + # 反弹 + particle.velocity.z = -particle.velocity.z * particle.bounce_factor * self.bounce_damping + particle.position.z = self.ground_level + 0.01 # 稍微抬高避免穿透 + + # 应用摩擦力 + friction = 0.9 + particle.velocity.x *= friction + particle.velocity.y *= friction + + self.physics_stats['collisions_detected'] += 1 + + # 墙壁碰撞检测 + # 假设有一个边界盒子 [-50, 50] x [-50, 50] x [0, 100] + boundary_min = Point3(-50, -50, 0) + boundary_max = Point3(50, 50, 100) + + # X轴边界 + if particle.position.x <= boundary_min.x and particle.velocity.x < 0: + particle.velocity.x = -particle.velocity.x * particle.bounce_factor + particle.position.x = boundary_min.x + 0.01 + self.physics_stats['collisions_detected'] += 1 + elif particle.position.x >= boundary_max.x and particle.velocity.x > 0: + particle.velocity.x = -particle.velocity.x * particle.bounce_factor + particle.position.x = boundary_max.x - 0.01 + self.physics_stats['collisions_detected'] += 1 + + # Y轴边界 + if particle.position.y <= boundary_min.y and particle.velocity.y < 0: + particle.velocity.y = -particle.velocity.y * particle.bounce_factor + particle.position.y = boundary_min.y + 0.01 + self.physics_stats['collisions_detected'] += 1 + elif particle.position.y >= boundary_max.y and particle.velocity.y > 0: + particle.velocity.y = -particle.velocity.y * particle.bounce_factor + particle.position.y = boundary_max.y - 0.01 + self.physics_stats['collisions_detected'] += 1 + + # Z轴上边界 + if particle.position.z >= boundary_max.z and particle.velocity.z > 0: + particle.velocity.z = -particle.velocity.z * particle.bounce_factor + particle.position.z = boundary_max.z - 0.01 + self.physics_stats['collisions_detected'] += 1 + + # 如果有Bullet物理世界,可以进行更复杂的碰撞检测 + if self.bullet_world: + self._handle_bullet_collision(particle, dt) + + def _handle_bullet_collision(self, particle: Particle, dt: float): + """使用Bullet进行碰撞检测""" + # 这里可以实现更复杂的碰撞检测 + # 由于粒子数量可能很大,通常只对重要粒子进行精确碰撞检测 + pass + + def add_force_field(self, field_type: str, position: Point3, + strength: float, radius: float, **kwargs): + """添加力场""" + force_field = { + 'type': field_type, + 'position': position, + 'strength': strength, + 'radius': radius, + **kwargs + } + self.force_fields.append(force_field) + print(f"✓ 添加力场: {field_type} at {position}") + + def remove_force_field(self, index: int): + """移除力场""" + if 0 <= index < len(self.force_fields): + del self.force_fields[index] + print(f"✓ 移除力场: {index}") + + def clear_force_fields(self): + """清除所有力场""" + self.force_fields.clear() + print("✓ 清除所有力场") + + def set_gravity(self, gravity: Vec3): + """设置重力""" + self.gravity = gravity + + def set_wind(self, velocity: Vec3, turbulence: float = 0.0): + """设置风力""" + self.wind_velocity = velocity + self.wind_turbulence = turbulence + + def set_ground_level(self, level: float): + """设置地面高度""" + self.ground_level = level + + def set_collision_enabled(self, enabled: bool): + """设置碰撞检测""" + self.collision_enabled = enabled + + def get_physics_stats(self) -> Dict: + """获取物理统计信息""" + return self.physics_stats.copy() + + def clear_all(self): + """清除所有物理状态""" + self.force_fields.clear() + + # 预设力场创建方法 + def create_gravity_well(self, position: Point3, strength: float = 10.0, radius: float = 5.0): + """创建重力井""" + self.add_force_field('point', position, strength, radius) + + def create_repulsion_field(self, position: Point3, strength: float = -10.0, radius: float = 3.0): + """创建排斥力场""" + self.add_force_field('point', position, strength, radius) + + def create_vortex(self, position: Point3, strength: float = 5.0, + radius: float = 4.0, axis: Vec3 = None): + """创建涡流""" + if axis is None: + axis = Vec3(0, 0, 1) + self.add_force_field('vortex', position, strength, radius, axis=axis) + + def create_wind_zone(self, position: Point3, direction: Vec3, + strength: float = 2.0, radius: float = 8.0): + """创建风区""" + direction.normalize() self.add_force_field('directional', position, strength, radius, direction=direction) \ No newline at end of file diff --git a/plugins/user/particle_system/core/particle_renderer.py b/plugins/user/particle_system/core/particle_renderer.py index 1d0b567d..23349266 100644 --- a/plugins/user/particle_system/core/particle_renderer.py +++ b/plugins/user/particle_system/core/particle_renderer.py @@ -1,473 +1,473 @@ -""" -粒子渲染器 -负责粒子的可视化渲染 -""" - -import math -import numpy as np -from typing import List, Dict -from panda3d.core import ( - GeomNode, Geom, GeomVertexFormat, GeomVertexData, GeomVertexWriter, - GeomPoints, GeomTriangles, RenderState, ColorBlendAttrib, - TransparencyAttrib, DepthWriteAttrib, Vec3, Vec4, Point3, - CardMaker, Texture, TextureStage, Material, LColor, NodePath -) -from direct.showbase.ShowBaseGlobal import globalClock - -from .particle_emitter import ParticleEmitter, Particle - -class ParticleRenderer: - """ - 粒子渲染器 - 使用Panda3D的几何体系统高效渲染大量粒子 - """ - - def __init__(self, world): - """ - 初始化粒子渲染器 - - Args: - world: 3D世界对象 - """ - self.world = world - self.enabled = False - - # 渲染节点 - self.render_root = None - self.particle_nodes: Dict[str, GeomNode] = {} - - # 渲染设置 - self.use_billboards = True # 使用广告牌渲染 - self.use_point_sprites = False # 使用点精灵 - self.batch_rendering = True # 批量渲染 - self.max_particles_per_batch = 1000 # 每批次最大粒子数 - - # 材质和纹理 - self.default_material = None - self.particle_textures = {} - self.custom_textures = {} # 用户自定义纹理 - - # 渲染统计 - self.render_stats = { - 'draw_calls': 0, - 'vertices_rendered': 0, - 'triangles_rendered': 0, - 'render_time': 0.0 - } - - # 注册的发射器 - self.registered_emitters: Dict[str, ParticleEmitter] = {} - - # 渲染优化 - self.use_vertex_colors = True - self.use_alpha_fading = True - - print("✓ 粒子渲染器初始化完成") - - def enable(self): - """启用渲染器""" - if self.enabled: - return - - self.enabled = True - - # 创建渲染根节点 - self.render_root = self.world.render.attachNewNode("particle_system") - - # 设置渲染状态 - self._setup_render_states() - - # 创建默认材质 - self._create_default_material() - - print("✓ 粒子渲染器已启用") - - def disable(self): - """禁用渲染器""" - if not self.enabled: - return - - self.enabled = False - - # 清理渲染节点 - if self.render_root: - self.render_root.removeNode() - self.render_root = None - - self.particle_nodes.clear() - - print("✓ 粒子渲染器已禁用") - - def cleanup(self): - """清理资源""" - self.disable() - self.registered_emitters.clear() - self.particle_textures.clear() - self.custom_textures.clear() - - def _setup_render_states(self): - """设置渲染状态""" - if not self.render_root: - return - - # 启用透明度 - self.render_root.setTransparency(TransparencyAttrib.MAlpha) - - # 设置混合模式(加法混合用于发光效果) - self.render_root.setAttrib(ColorBlendAttrib.make( - ColorBlendAttrib.MAdd, - ColorBlendAttrib.OIncomingAlpha, - ColorBlendAttrib.OOne - )) - - # 禁用深度写入(但保持深度测试) - self.render_root.setAttrib(DepthWriteAttrib.make(DepthWriteAttrib.MOff)) - - # 设置渲染顺序(在透明物体之后渲染) - self.render_root.setBin("fixed", 100) - - def _create_default_material(self): - """创建默认材质""" - self.default_material = Material() - self.default_material.setEmission(LColor(1, 1, 1, 1)) - self.default_material.setAmbient(LColor(0.2, 0.2, 0.2, 1)) - self.default_material.setDiffuse(LColor(0.8, 0.8, 0.8, 1)) - self.default_material.setSpecular(LColor(0, 0, 0, 1)) - self.default_material.setShininess(1.0) - - def register_emitter(self, emitter: ParticleEmitter): - """注册粒子发射器""" - self.registered_emitters[emitter.emitter_id] = emitter - - # 为发射器创建渲染节点 - self._create_emitter_render_node(emitter) - - def unregister_emitter(self, emitter: ParticleEmitter): - """注销粒子发射器""" - emitter_id = emitter.emitter_id - - if emitter_id in self.registered_emitters: - del self.registered_emitters[emitter_id] - - # 移除渲染节点 - if emitter_id in self.particle_nodes: - node = self.particle_nodes[emitter_id] - if node.getParent(0): - node.getParent(0).removeNode() - del self.particle_nodes[emitter_id] - - def _create_emitter_render_node(self, emitter: ParticleEmitter): - """为发射器创建渲染节点""" - if not self.enabled or not self.render_root: - return - - emitter_id = emitter.emitter_id - - # 创建几何节点 - geom_node = GeomNode(f"particles_{emitter_id}") - node_path = self.render_root.attachNewNode(geom_node) - - # 应用材质 - if self.default_material: - node_path.setMaterial(self.default_material) - - # 根据粒子类型设置纹理 - self._apply_particle_texture(node_path, emitter.emitter_type) - - self.particle_nodes[emitter_id] = geom_node - - def _apply_particle_texture(self, node_path, particle_type: str): - """应用粒子纹理""" - # 这里可以根据粒子类型加载不同的纹理 - # 目前使用程序生成的简单纹理 - - if particle_type == 'fire': - # 火焰效果:橙红色渐变 - node_path.setColor(1.0, 0.5, 0.1, 0.8) - elif particle_type == 'smoke': - # 烟雾效果:灰色 - node_path.setColor(0.6, 0.6, 0.6, 0.5) - elif particle_type == 'explosion': - # 爆炸效果:亮黄色 - node_path.setColor(1.0, 0.8, 0.2, 0.9) - elif particle_type == 'water': - # 水花效果:蓝色 - node_path.setColor(0.2, 0.6, 1.0, 0.7) - else: - # 默认:白色 - node_path.setColor(1.0, 1.0, 1.0, 0.8) - - def update(self, dt: float): - """更新渲染""" - if not self.enabled: - return - - import time - start_time = time.time() - - # 重置统计信息 - self.render_stats['draw_calls'] = 0 - self.render_stats['vertices_rendered'] = 0 - self.render_stats['triangles_rendered'] = 0 - - # 更新每个发射器的渲染 - for emitter_id, emitter in self.registered_emitters.items(): - if emitter_id in self.particle_nodes: - self._update_emitter_rendering(emitter) - - self.render_stats['render_time'] = time.time() - start_time - - def _update_emitter_rendering(self, emitter: ParticleEmitter): - """更新发射器的渲染""" - emitter_id = emitter.emitter_id - geom_node = self.particle_nodes.get(emitter_id) - - if not geom_node: - return - - # 获取活跃粒子 - particles = emitter.get_particles() - - if not particles: - # 清空几何体 - geom_node.removeAllGeoms() - return - - # 分批渲染大量粒子 - if len(particles) > self.max_particles_per_batch: - # 分批处理 - for i in range(0, len(particles), self.max_particles_per_batch): - batch_particles = particles[i:i + self.max_particles_per_batch] - if self.use_billboards: - self._create_billboard_geometry(geom_node, batch_particles) - else: - self._create_point_geometry(geom_node, batch_particles) - self.render_stats['draw_calls'] += 1 - else: - # 单次渲染 - if self.use_billboards: - self._create_billboard_geometry(geom_node, particles) - else: - self._create_point_geometry(geom_node, particles) - self.render_stats['draw_calls'] += 1 - - def _create_billboard_geometry(self, geom_node: GeomNode, particles: List[Particle]): - """创建广告牌几何体""" - if not particles: - return - - # 清除现有几何体 - geom_node.removeAllGeoms() - - # 创建顶点格式 - if self.use_vertex_colors: - vertex_format = GeomVertexFormat.getV3n3c4t2() - else: - vertex_format = GeomVertexFormat.getV3n3t2() - - vertex_data = GeomVertexData("particles", vertex_format, Geom.UHStatic) - vertex_data.setNumRows(len(particles) * 4) # 每个粒子4个顶点 - - # 创建顶点写入器 - vertex_writer = GeomVertexWriter(vertex_data, "vertex") - normal_writer = GeomVertexWriter(vertex_data, "normal") - if self.use_vertex_colors: - color_writer = GeomVertexWriter(vertex_data, "color") - texcoord_writer = GeomVertexWriter(vertex_data, "texcoord") - - # 获取摄像机位置(用于广告牌对齐) - camera_pos = self.world.camera.getPos() - - # 为每个粒子创建四边形 - for i, particle in enumerate(particles): - if not particle.is_alive(): - continue - - # 计算广告牌方向 - to_camera = camera_pos - particle.position - to_camera.normalize() - - # 创建局部坐标系(简化版本) - up = Vec3(0, 0, 1) - right = to_camera.cross(up) - if right.length() < 0.001: # 避免与向上向量平行 - right = Vec3(1, 0, 0) - else: - right.normalize() - up = right.cross(to_camera) - up.normalize() - - # 计算四个顶点位置 - half_size = particle.size * 0.5 - positions = [ - particle.position + (-right - up) * half_size, # 左下 - particle.position + (right - up) * half_size, # 右下 - particle.position + (right + up) * half_size, # 右上 - particle.position + (-right + up) * half_size # 左上 - ] - - # 纹理坐标 - texcoords = [ - (0, 0), (1, 0), (1, 1), (0, 1) - ] - - # 计算透明度 - alpha = 1.0 - if self.use_alpha_fading: - alpha = 1.0 - particle.age / particle.lifetime # 透明度随生命周期变化 - - # 写入顶点数据 - for j, (pos, texcoord) in enumerate(zip(positions, texcoords)): - vertex_writer.addData3f(pos.x, pos.y, pos.z) - normal_writer.addData3f(to_camera.x, to_camera.y, to_camera.z) - if self.use_vertex_colors: - color_writer.addData4f( - particle.color.x, - particle.color.y, - particle.color.z, - alpha - ) - texcoord_writer.addData2f(texcoord[0], texcoord[1]) - - # 创建几何体 - geom = Geom(vertex_data) - - # 创建三角形索引 - triangles = GeomTriangles(Geom.UHStatic) - - for i in range(len(particles)): - base_index = i * 4 - - # 第一个三角形 - triangles.addVertex(base_index) - triangles.addVertex(base_index + 1) - triangles.addVertex(base_index + 2) - - # 第二个三角形 - triangles.addVertex(base_index) - triangles.addVertex(base_index + 2) - triangles.addVertex(base_index + 3) - - triangles.closePrimitive() - geom.addPrimitive(triangles) - - # 添加到节点 - geom_node.addGeom(geom) - - # 更新统计信息 - self.render_stats['vertices_rendered'] += len(particles) * 4 - self.render_stats['triangles_rendered'] += len(particles) * 2 - - def _create_point_geometry(self, geom_node: GeomNode, particles: List[Particle]): - """创建点几何体""" - if not particles: - return - - # 清除现有几何体 - geom_node.removeAllGeoms() - - # 创建顶点格式 - if self.use_vertex_colors: - vertex_format = GeomVertexFormat.getV3c4() - else: - vertex_format = GeomVertexFormat.getV3() - - vertex_data = GeomVertexData("particles", vertex_format, Geom.UHStatic) - vertex_data.setNumRows(len(particles)) - - # 创建顶点写入器 - vertex_writer = GeomVertexWriter(vertex_data, "vertex") - if self.use_vertex_colors: - color_writer = GeomVertexWriter(vertex_data, "color") - - # 写入粒子数据 - for particle in particles: - if not particle.is_alive(): - continue - - vertex_writer.addData3f( - particle.position.x, - particle.position.y, - particle.position.z - ) - - if self.use_vertex_colors: - # 计算透明度 - alpha = 1.0 - if self.use_alpha_fading: - alpha = 1.0 - particle.age / particle.lifetime - - color_writer.addData4f( - particle.color.x, - particle.color.y, - particle.color.z, - alpha - ) - - # 创建几何体 - geom = Geom(vertex_data) - - # 创建点图元 - points = GeomPoints(Geom.UHStatic) - for i in range(len(particles)): - points.addVertex(i) - points.closePrimitive() - - geom.addPrimitive(points) - geom_node.addGeom(geom) - - # 更新统计信息 - self.render_stats['vertices_rendered'] += len(particles) - - def clear_all(self): - """清除所有渲染内容""" - for geom_node in self.particle_nodes.values(): - geom_node.removeAllGeoms() - - def get_render_stats(self) -> Dict: - """获取渲染统计信息""" - return self.render_stats.copy() - - def set_billboard_mode(self, enabled: bool): - """设置广告牌模式""" - self.use_billboards = enabled - - def set_point_sprite_mode(self, enabled: bool): - """设置点精灵模式""" - self.use_point_sprites = enabled - - def set_batch_rendering(self, enabled: bool): - """设置批量渲染""" - self.batch_rendering = enabled - - def set_max_particles_per_batch(self, count: int): - """设置每批次最大粒子数""" - self.max_particles_per_batch = max(1, count) - - def load_custom_texture(self, texture_name: str, texture_path: str): - """加载自定义纹理""" - try: - texture = self.world.loader.loadTexture(texture_path) - if texture: - self.custom_textures[texture_name] = texture - print(f"✓ 加载自定义纹理: {texture_name}") - return True - except Exception as e: - print(f"✗ 纹理加载失败 {texture_name}: {e}") - - return False - - def apply_custom_texture(self, emitter_id: str, texture_name: str): - """应用自定义纹理到发射器""" - if texture_name not in self.custom_textures: - print(f"⚠ 纹理未找到: {texture_name}") - return False - - if emitter_id in self.particle_nodes: - node_path = NodePath(self.particle_nodes[emitter_id]) - texture = self.custom_textures[texture_name] - node_path.setTexture(texture, 1) - print(f"✓ 应用纹理 {texture_name} 到发射器 {emitter_id}") - return True - +""" +粒子渲染器 +负责粒子的可视化渲染 +""" + +import math +import numpy as np +from typing import List, Dict +from panda3d.core import ( + GeomNode, Geom, GeomVertexFormat, GeomVertexData, GeomVertexWriter, + GeomPoints, GeomTriangles, RenderState, ColorBlendAttrib, + TransparencyAttrib, DepthWriteAttrib, Vec3, Vec4, Point3, + CardMaker, Texture, TextureStage, Material, LColor, NodePath +) +from direct.showbase.ShowBaseGlobal import globalClock + +from .particle_emitter import ParticleEmitter, Particle + +class ParticleRenderer: + """ + 粒子渲染器 + 使用Panda3D的几何体系统高效渲染大量粒子 + """ + + def __init__(self, world): + """ + 初始化粒子渲染器 + + Args: + world: 3D世界对象 + """ + self.world = world + self.enabled = False + + # 渲染节点 + self.render_root = None + self.particle_nodes: Dict[str, GeomNode] = {} + + # 渲染设置 + self.use_billboards = True # 使用广告牌渲染 + self.use_point_sprites = False # 使用点精灵 + self.batch_rendering = True # 批量渲染 + self.max_particles_per_batch = 1000 # 每批次最大粒子数 + + # 材质和纹理 + self.default_material = None + self.particle_textures = {} + self.custom_textures = {} # 用户自定义纹理 + + # 渲染统计 + self.render_stats = { + 'draw_calls': 0, + 'vertices_rendered': 0, + 'triangles_rendered': 0, + 'render_time': 0.0 + } + + # 注册的发射器 + self.registered_emitters: Dict[str, ParticleEmitter] = {} + + # 渲染优化 + self.use_vertex_colors = True + self.use_alpha_fading = True + + print("✓ 粒子渲染器初始化完成") + + def enable(self): + """启用渲染器""" + if self.enabled: + return + + self.enabled = True + + # 创建渲染根节点 + self.render_root = self.world.render.attachNewNode("particle_system") + + # 设置渲染状态 + self._setup_render_states() + + # 创建默认材质 + self._create_default_material() + + print("✓ 粒子渲染器已启用") + + def disable(self): + """禁用渲染器""" + if not self.enabled: + return + + self.enabled = False + + # 清理渲染节点 + if self.render_root: + self.render_root.removeNode() + self.render_root = None + + self.particle_nodes.clear() + + print("✓ 粒子渲染器已禁用") + + def cleanup(self): + """清理资源""" + self.disable() + self.registered_emitters.clear() + self.particle_textures.clear() + self.custom_textures.clear() + + def _setup_render_states(self): + """设置渲染状态""" + if not self.render_root: + return + + # 启用透明度 + self.render_root.setTransparency(TransparencyAttrib.MAlpha) + + # 设置混合模式(加法混合用于发光效果) + self.render_root.setAttrib(ColorBlendAttrib.make( + ColorBlendAttrib.MAdd, + ColorBlendAttrib.OIncomingAlpha, + ColorBlendAttrib.OOne + )) + + # 禁用深度写入(但保持深度测试) + self.render_root.setAttrib(DepthWriteAttrib.make(DepthWriteAttrib.MOff)) + + # 设置渲染顺序(在透明物体之后渲染) + self.render_root.setBin("fixed", 100) + + def _create_default_material(self): + """创建默认材质""" + self.default_material = Material() + self.default_material.setEmission(LColor(1, 1, 1, 1)) + self.default_material.setAmbient(LColor(0.2, 0.2, 0.2, 1)) + self.default_material.setDiffuse(LColor(0.8, 0.8, 0.8, 1)) + self.default_material.setSpecular(LColor(0, 0, 0, 1)) + self.default_material.setShininess(1.0) + + def register_emitter(self, emitter: ParticleEmitter): + """注册粒子发射器""" + self.registered_emitters[emitter.emitter_id] = emitter + + # 为发射器创建渲染节点 + self._create_emitter_render_node(emitter) + + def unregister_emitter(self, emitter: ParticleEmitter): + """注销粒子发射器""" + emitter_id = emitter.emitter_id + + if emitter_id in self.registered_emitters: + del self.registered_emitters[emitter_id] + + # 移除渲染节点 + if emitter_id in self.particle_nodes: + node = self.particle_nodes[emitter_id] + if node.getParent(0): + node.getParent(0).removeNode() + del self.particle_nodes[emitter_id] + + def _create_emitter_render_node(self, emitter: ParticleEmitter): + """为发射器创建渲染节点""" + if not self.enabled or not self.render_root: + return + + emitter_id = emitter.emitter_id + + # 创建几何节点 + geom_node = GeomNode(f"particles_{emitter_id}") + node_path = self.render_root.attachNewNode(geom_node) + + # 应用材质 + if self.default_material: + node_path.setMaterial(self.default_material) + + # 根据粒子类型设置纹理 + self._apply_particle_texture(node_path, emitter.emitter_type) + + self.particle_nodes[emitter_id] = geom_node + + def _apply_particle_texture(self, node_path, particle_type: str): + """应用粒子纹理""" + # 这里可以根据粒子类型加载不同的纹理 + # 目前使用程序生成的简单纹理 + + if particle_type == 'fire': + # 火焰效果:橙红色渐变 + node_path.setColor(1.0, 0.5, 0.1, 0.8) + elif particle_type == 'smoke': + # 烟雾效果:灰色 + node_path.setColor(0.6, 0.6, 0.6, 0.5) + elif particle_type == 'explosion': + # 爆炸效果:亮黄色 + node_path.setColor(1.0, 0.8, 0.2, 0.9) + elif particle_type == 'water': + # 水花效果:蓝色 + node_path.setColor(0.2, 0.6, 1.0, 0.7) + else: + # 默认:白色 + node_path.setColor(1.0, 1.0, 1.0, 0.8) + + def update(self, dt: float): + """更新渲染""" + if not self.enabled: + return + + import time + start_time = time.time() + + # 重置统计信息 + self.render_stats['draw_calls'] = 0 + self.render_stats['vertices_rendered'] = 0 + self.render_stats['triangles_rendered'] = 0 + + # 更新每个发射器的渲染 + for emitter_id, emitter in self.registered_emitters.items(): + if emitter_id in self.particle_nodes: + self._update_emitter_rendering(emitter) + + self.render_stats['render_time'] = time.time() - start_time + + def _update_emitter_rendering(self, emitter: ParticleEmitter): + """更新发射器的渲染""" + emitter_id = emitter.emitter_id + geom_node = self.particle_nodes.get(emitter_id) + + if not geom_node: + return + + # 获取活跃粒子 + particles = emitter.get_particles() + + if not particles: + # 清空几何体 + geom_node.removeAllGeoms() + return + + # 分批渲染大量粒子 + if len(particles) > self.max_particles_per_batch: + # 分批处理 + for i in range(0, len(particles), self.max_particles_per_batch): + batch_particles = particles[i:i + self.max_particles_per_batch] + if self.use_billboards: + self._create_billboard_geometry(geom_node, batch_particles) + else: + self._create_point_geometry(geom_node, batch_particles) + self.render_stats['draw_calls'] += 1 + else: + # 单次渲染 + if self.use_billboards: + self._create_billboard_geometry(geom_node, particles) + else: + self._create_point_geometry(geom_node, particles) + self.render_stats['draw_calls'] += 1 + + def _create_billboard_geometry(self, geom_node: GeomNode, particles: List[Particle]): + """创建广告牌几何体""" + if not particles: + return + + # 清除现有几何体 + geom_node.removeAllGeoms() + + # 创建顶点格式 + if self.use_vertex_colors: + vertex_format = GeomVertexFormat.getV3n3c4t2() + else: + vertex_format = GeomVertexFormat.getV3n3t2() + + vertex_data = GeomVertexData("particles", vertex_format, Geom.UHStatic) + vertex_data.setNumRows(len(particles) * 4) # 每个粒子4个顶点 + + # 创建顶点写入器 + vertex_writer = GeomVertexWriter(vertex_data, "vertex") + normal_writer = GeomVertexWriter(vertex_data, "normal") + if self.use_vertex_colors: + color_writer = GeomVertexWriter(vertex_data, "color") + texcoord_writer = GeomVertexWriter(vertex_data, "texcoord") + + # 获取摄像机位置(用于广告牌对齐) + camera_pos = self.world.camera.getPos() + + # 为每个粒子创建四边形 + for i, particle in enumerate(particles): + if not particle.is_alive(): + continue + + # 计算广告牌方向 + to_camera = camera_pos - particle.position + to_camera.normalize() + + # 创建局部坐标系(简化版本) + up = Vec3(0, 0, 1) + right = to_camera.cross(up) + if right.length() < 0.001: # 避免与向上向量平行 + right = Vec3(1, 0, 0) + else: + right.normalize() + up = right.cross(to_camera) + up.normalize() + + # 计算四个顶点位置 + half_size = particle.size * 0.5 + positions = [ + particle.position + (-right - up) * half_size, # 左下 + particle.position + (right - up) * half_size, # 右下 + particle.position + (right + up) * half_size, # 右上 + particle.position + (-right + up) * half_size # 左上 + ] + + # 纹理坐标 + texcoords = [ + (0, 0), (1, 0), (1, 1), (0, 1) + ] + + # 计算透明度 + alpha = 1.0 + if self.use_alpha_fading: + alpha = 1.0 - particle.age / particle.lifetime # 透明度随生命周期变化 + + # 写入顶点数据 + for j, (pos, texcoord) in enumerate(zip(positions, texcoords)): + vertex_writer.addData3f(pos.x, pos.y, pos.z) + normal_writer.addData3f(to_camera.x, to_camera.y, to_camera.z) + if self.use_vertex_colors: + color_writer.addData4f( + particle.color.x, + particle.color.y, + particle.color.z, + alpha + ) + texcoord_writer.addData2f(texcoord[0], texcoord[1]) + + # 创建几何体 + geom = Geom(vertex_data) + + # 创建三角形索引 + triangles = GeomTriangles(Geom.UHStatic) + + for i in range(len(particles)): + base_index = i * 4 + + # 第一个三角形 + triangles.addVertex(base_index) + triangles.addVertex(base_index + 1) + triangles.addVertex(base_index + 2) + + # 第二个三角形 + triangles.addVertex(base_index) + triangles.addVertex(base_index + 2) + triangles.addVertex(base_index + 3) + + triangles.closePrimitive() + geom.addPrimitive(triangles) + + # 添加到节点 + geom_node.addGeom(geom) + + # 更新统计信息 + self.render_stats['vertices_rendered'] += len(particles) * 4 + self.render_stats['triangles_rendered'] += len(particles) * 2 + + def _create_point_geometry(self, geom_node: GeomNode, particles: List[Particle]): + """创建点几何体""" + if not particles: + return + + # 清除现有几何体 + geom_node.removeAllGeoms() + + # 创建顶点格式 + if self.use_vertex_colors: + vertex_format = GeomVertexFormat.getV3c4() + else: + vertex_format = GeomVertexFormat.getV3() + + vertex_data = GeomVertexData("particles", vertex_format, Geom.UHStatic) + vertex_data.setNumRows(len(particles)) + + # 创建顶点写入器 + vertex_writer = GeomVertexWriter(vertex_data, "vertex") + if self.use_vertex_colors: + color_writer = GeomVertexWriter(vertex_data, "color") + + # 写入粒子数据 + for particle in particles: + if not particle.is_alive(): + continue + + vertex_writer.addData3f( + particle.position.x, + particle.position.y, + particle.position.z + ) + + if self.use_vertex_colors: + # 计算透明度 + alpha = 1.0 + if self.use_alpha_fading: + alpha = 1.0 - particle.age / particle.lifetime + + color_writer.addData4f( + particle.color.x, + particle.color.y, + particle.color.z, + alpha + ) + + # 创建几何体 + geom = Geom(vertex_data) + + # 创建点图元 + points = GeomPoints(Geom.UHStatic) + for i in range(len(particles)): + points.addVertex(i) + points.closePrimitive() + + geom.addPrimitive(points) + geom_node.addGeom(geom) + + # 更新统计信息 + self.render_stats['vertices_rendered'] += len(particles) + + def clear_all(self): + """清除所有渲染内容""" + for geom_node in self.particle_nodes.values(): + geom_node.removeAllGeoms() + + def get_render_stats(self) -> Dict: + """获取渲染统计信息""" + return self.render_stats.copy() + + def set_billboard_mode(self, enabled: bool): + """设置广告牌模式""" + self.use_billboards = enabled + + def set_point_sprite_mode(self, enabled: bool): + """设置点精灵模式""" + self.use_point_sprites = enabled + + def set_batch_rendering(self, enabled: bool): + """设置批量渲染""" + self.batch_rendering = enabled + + def set_max_particles_per_batch(self, count: int): + """设置每批次最大粒子数""" + self.max_particles_per_batch = max(1, count) + + def load_custom_texture(self, texture_name: str, texture_path: str): + """加载自定义纹理""" + try: + texture = self.world.loader.loadTexture(texture_path) + if texture: + self.custom_textures[texture_name] = texture + print(f"✓ 加载自定义纹理: {texture_name}") + return True + except Exception as e: + print(f"✗ 纹理加载失败 {texture_name}: {e}") + + return False + + def apply_custom_texture(self, emitter_id: str, texture_name: str): + """应用自定义纹理到发射器""" + if texture_name not in self.custom_textures: + print(f"⚠ 纹理未找到: {texture_name}") + return False + + if emitter_id in self.particle_nodes: + node_path = NodePath(self.particle_nodes[emitter_id]) + texture = self.custom_textures[texture_name] + node_path.setTexture(texture, 1) + print(f"✓ 应用纹理 {texture_name} 到发射器 {emitter_id}") + return True + return False \ No newline at end of file diff --git a/plugins/user/particle_system/editor/__init__.py b/plugins/user/particle_system/editor/__init__.py index ea021e47..6d2f25df 100644 --- a/plugins/user/particle_system/editor/__init__.py +++ b/plugins/user/particle_system/editor/__init__.py @@ -1,11 +1,11 @@ -""" -粒子系统编辑器模块 -""" - -from .particle_editor import ParticleEditor -from .preset_manager import PresetManager - -__all__ = [ - 'ParticleEditor', - 'PresetManager' +""" +粒子系统编辑器模块 +""" + +from .particle_editor import ParticleEditor +from .preset_manager import PresetManager + +__all__ = [ + 'ParticleEditor', + 'PresetManager' ] \ No newline at end of file diff --git a/plugins/user/particle_system/editor/particle_editor.py b/plugins/user/particle_system/editor/particle_editor.py index 07d4aaa7..0e52a4dd 100644 --- a/plugins/user/particle_system/editor/particle_editor.py +++ b/plugins/user/particle_system/editor/particle_editor.py @@ -1,692 +1,692 @@ -""" -粒子编辑器 -提供可视化的粒子效果编辑功能 -""" - -import json -from typing import Dict, Optional, Any -from panda3d.core import Vec3, Point3 - -class ParticleEditor: - """ - 粒子编辑器 - 提供实时编辑粒子效果的功能 - """ - - def __init__(self, world, particle_manager): - """ - 初始化粒子编辑器 - - Args: - world: 3D世界对象 - particle_manager: 粒子管理器 - """ - self.world = world - self.particle_manager = particle_manager - self.visible = False - - # 当前编辑的发射器 - self.current_emitter_id = None - self.current_emitter = None - - # 编辑器GUI元素 - self.gui_elements = [] - self.parameter_controls = {} - self.effect_type_buttons = {} - - # 预览模式 - self.preview_mode = True - self.auto_update = True - - # 编辑器状态 - self.editor_position = Point3(0, 0, 0) - self.editor_enabled = False - - # 编辑参数 - self.edit_parameters = { - 'emitter_type': 'fire', - 'emission_rate': 50, - 'particle_lifetime': 2.0, - 'start_color': Vec3(1.0, 0.3, 0.0), - 'end_color': Vec3(1.0, 0.0, 0.0), - 'start_size': 0.2, - 'end_size': 0.8, - 'velocity': Vec3(0, 0, 2), - 'velocity_variation': 0.5, - 'gravity_scale': -0.2, - 'air_resistance': 0.02, - 'bounce_factor': 0.5, - 'emitter_shape': 'circle', - 'emitter_radius': 0.3, - 'rotation_enabled': False, - 'rotation_speed': 0.0 - } - - print("✓ 粒子编辑器初始化完成") - - def toggle_visibility(self): - """切换编辑器可见性""" - if self.visible: - self.hide() - else: - self.show() - - def show(self): - """显示编辑器""" - if self.visible: - return - - self.visible = True - self._create_editor_gui() - - # 如果没有当前发射器,创建一个默认的 - if not self.current_emitter_id: - self.create_new_effect() - - print("✓ 粒子编辑器已显示") - - def hide(self): - """隐藏编辑器""" - if not self.visible: - return - - self.visible = False - self._cleanup_editor_gui() - - print("✓ 粒子编辑器已隐藏") - - def _create_editor_gui(self): - """创建编辑器GUI""" - try: - if not hasattr(self.world, 'gui_manager') or not self.world.gui_manager: - print("⚠ GUI管理器不可用,无法创建编辑器界面") - return - - gui_manager = self.world.gui_manager - - # 创建编辑器面板标题 - panel = gui_manager.createGUIButton( - pos=(0.7, 0, 0.95), - text="粒子编辑器", - size=0.06 - ) - self.gui_elements.append(panel) - - # 创建效果类型选择按钮 - effect_types = ["火焰", "烟雾", "爆炸", "水花", "魔法", "雪花", "自定义"] - for i, effect_type in enumerate(effect_types): - button = gui_manager.createGUIButton( - pos=(0.7, 0, 0.88 - i * 0.06), - text=effect_type, - size=0.04 - ) - self.gui_elements.append(button) - self.effect_type_buttons[effect_type] = button - self.world.accept(f"editor_effect_{effect_type}", - lambda t=effect_type: self.set_effect_type(t)) - - # 创建参数控制面板 - self._create_parameter_controls() - - # 创建操作按钮 - self._create_action_buttons() - - except Exception as e: - print(f"⚠ 编辑器GUI创建失败: {e}") - - def _create_parameter_controls(self): - """创建参数控制界面""" - if not hasattr(self.world, 'gui_manager') or not self.world.gui_manager: - return - - gui_manager = self.world.gui_manager - - # 基础参数控制 - parameters = [ - ("发射率", "emission_rate", 0, 500, 1), - ("生命周期", "particle_lifetime", 0.1, 10.0, 0.1), - ("起始尺寸", "start_size", 0.01, 5.0, 0.01), - ("结束尺寸", "end_size", 0.01, 5.0, 0.01), - ("速度变化", "velocity_variation", 0.0, 5.0, 0.1), - ("重力缩放", "gravity_scale", -5.0, 5.0, 0.1), - ("空气阻力", "air_resistance", 0.0, 1.0, 0.01), - ("反弹系数", "bounce_factor", 0.0, 1.0, 0.1), - ("发射器半径", "emitter_radius", 0.0, 5.0, 0.1), - ("旋转速度", "rotation_speed", -10.0, 10.0, 0.1) - ] - - for i, (name, param, min_val, max_val, step) in enumerate(parameters): - y_pos = 0.8 - i * 0.055 - - # 参数标签 - label = gui_manager.createGUIButton( - pos=(0.75, 0, y_pos), - text=f"{name}:", - size=0.03 - ) - self.gui_elements.append(label) - - # 参数值显示 - value_label = gui_manager.createGUIButton( - pos=(0.88, 0, y_pos), - text=f"{self.edit_parameters[param]:.2f}", - size=0.03 - ) - self.gui_elements.append(value_label) - - # 减少按钮 - dec_button = gui_manager.createGUIButton( - pos=(0.83, 0, y_pos), - text="-", - size=0.03 - ) - self.gui_elements.append(dec_button) - - # 增加按钮 - inc_button = gui_manager.createGUIButton( - pos=(0.93, 0, y_pos), - text="+", - size=0.03 - ) - self.gui_elements.append(inc_button) - - # 注册事件 - self.world.accept(f"dec_{param}", lambda p=param, s=step: self._decrease_parameter(p, s)) - self.world.accept(f"inc_{param}", lambda p=param, s=step: self._increase_parameter(p, s)) - - # 存储控制信息 - self.parameter_controls[param] = { - 'name': name, - 'min': min_val, - 'max': max_val, - 'step': step, - 'value_label': value_label, - 'dec_button': dec_button, - 'inc_button': inc_button - } - - # 布尔型参数 - bool_params = [ - ("启用旋转", "rotation_enabled"), - ("自动更新", "auto_update"), - ("预览模式", "preview_mode") - ] - - for i, (name, param) in enumerate(bool_params): - y_pos = 0.25 - i * 0.055 - - # 参数标签 - label = gui_manager.createGUIButton( - pos=(0.75, 0, y_pos), - text=f"{name}:", - size=0.03 - ) - self.gui_elements.append(label) - - # 开关按钮 - switch_button = gui_manager.createGUIButton( - pos=(0.88, 0, y_pos), - text="开" if self.edit_parameters.get(param, False) else "关", - size=0.03 - ) - self.gui_elements.append(switch_button) - - # 注册事件 - self.world.accept(f"toggle_{param}", lambda p=param: self._toggle_boolean_parameter(p)) - - self.parameter_controls[param] = { - 'name': name, - 'value_label': switch_button, - 'type': 'boolean' - } - - def _create_action_buttons(self): - """创建操作按钮""" - if not hasattr(self.world, 'gui_manager') or not self.world.gui_manager: - return - - gui_manager = self.world.gui_manager - - actions = [ - ("应用更改", 0.15, self.apply_changes), - ("重置参数", 0.09, self.reset_parameters), - ("创建新效果", 0.03, self.create_new_effect), - ("删除当前", -0.03, self.delete_current_effect), - ("保存预设", -0.09, self.save_preset), - ("加载预设", -0.15, self.load_preset) - ] - - for name, y_offset, callback in actions: - button = gui_manager.createGUIButton( - pos=(0.82, 0, y_offset), - text=name, - size=0.035 - ) - self.gui_elements.append(button) - self.world.accept(f"action_{name}", callback) - - def _cleanup_editor_gui(self): - """清理编辑器GUI""" - try: - if hasattr(self.world, 'gui_manager') and self.world.gui_manager: - gui_manager = self.world.gui_manager - - for element in self.gui_elements: - gui_manager.deleteGUIElement(element) - - # 清理事件监听 - for param in self.parameter_controls: - self.world.ignore(f"dec_{param}") - self.world.ignore(f"inc_{param}") - - for param in ['rotation_enabled', 'auto_update', 'preview_mode']: - self.world.ignore(f"toggle_{param}") - - self.world.ignore("action_应用更改") - self.world.ignore("action_重置参数") - self.world.ignore("action_创建新效果") - self.world.ignore("action_删除当前") - self.world.ignore("action_保存预设") - self.world.ignore("action_加载预设") - - self.gui_elements.clear() - self.parameter_controls.clear() - self.effect_type_buttons.clear() - - except Exception as e: - print(f"⚠ 编辑器GUI清理失败: {e}") - - def _decrease_parameter(self, param_name: str, step: float): - """减少参数值""" - if param_name in self.edit_parameters: - control = self.parameter_controls[param_name] - current_value = self.edit_parameters[param_name] - new_value = max(control['min'], current_value - step) - self.edit_parameters[param_name] = new_value - - # 更新显示 - if 'value_label' in control: - control['value_label'].setText(f"{new_value:.2f}") - - # 自动应用更改 - if self.auto_update: - self.apply_changes() - - def _increase_parameter(self, param_name: str, step: float): - """增加参数值""" - if param_name in self.edit_parameters: - control = self.parameter_controls[param_name] - current_value = self.edit_parameters[param_name] - new_value = min(control['max'], current_value + step) - self.edit_parameters[param_name] = new_value - - # 更新显示 - if 'value_label' in control: - control['value_label'].setText(f"{new_value:.2f}") - - # 自动应用更改 - if self.auto_update: - self.apply_changes() - - def _toggle_boolean_parameter(self, param_name: str): - """切换布尔参数""" - if param_name in self.edit_parameters: - current_value = self.edit_parameters[param_name] - new_value = not current_value - self.edit_parameters[param_name] = new_value - - # 更新显示 - control = self.parameter_controls[param_name] - if 'value_label' in control: - control['value_label'].setText("开" if new_value else "关") - - # 特殊处理某些参数 - if param_name == 'auto_update': - self.auto_update = new_value - elif param_name == 'preview_mode': - self.preview_mode = new_value - - # 自动应用更改 - if self.auto_update: - self.apply_changes() - - def create_new_effect(self, effect_type: str = "火焰"): - """创建新的粒子效果""" - # 删除当前效果 - if self.current_emitter_id: - self.particle_manager.remove_emitter(self.current_emitter_id) - - # 在摄像机前方创建 - camera_pos = self.world.camera.getPos() - camera_forward = self.world.camera.getNetTransform().getMat().getRow3(1) - position = camera_pos + camera_forward * 5 - - # 根据类型设置参数 - self.set_effect_type(effect_type) - - # 创建发射器 - config = self.edit_parameters.copy() - config['emitter_type'] = effect_type.lower() - - self.current_emitter_id = self.particle_manager.create_emitter( - effect_type.lower(), - position, - config - ) - - self.current_emitter = self.particle_manager.get_emitter(self.current_emitter_id) - self.editor_position = position - - print(f"✓ 创建新粒子效果: {effect_type}") - - def set_effect_type(self, effect_type: str): - """设置效果类型""" - self.edit_parameters['emitter_type'] = effect_type.lower() - - # 根据类型预设参数 - if effect_type == "火焰": - self.edit_parameters.update({ - 'emission_rate': 50, - 'particle_lifetime': 2.0, - 'start_color': Vec3(1.0, 0.3, 0.0), - 'end_color': Vec3(1.0, 0.0, 0.0), - 'start_size': 0.2, - 'end_size': 0.8, - 'velocity': Vec3(0, 0, 2), - 'velocity_variation': 0.5, - 'gravity_scale': -0.2, - 'emitter_shape': 'circle', - 'emitter_radius': 0.3 - }) - elif effect_type == "烟雾": - self.edit_parameters.update({ - 'emission_rate': 30, - 'particle_lifetime': 4.0, - 'start_color': Vec3(0.8, 0.8, 0.8), - 'end_color': Vec3(0.3, 0.3, 0.3), - 'start_size': 0.3, - 'end_size': 1.5, - 'velocity': Vec3(0, 0, 1), - 'velocity_variation': 0.3, - 'gravity_scale': -0.1, - 'emitter_shape': 'circle', - 'emitter_radius': 0.5 - }) - elif effect_type == "爆炸": - self.edit_parameters.update({ - 'emission_rate': 200, - 'burst_count': 100, - 'particle_lifetime': 1.5, - 'start_color': Vec3(1.0, 0.8, 0.0), - 'end_color': Vec3(1.0, 0.2, 0.0), - 'start_size': 0.1, - 'end_size': 0.5, - 'velocity': Vec3(0, 0, 0), - 'velocity_variation': 2.0, - 'gravity_scale': 0.5, - 'duration': 0.2, - 'emitter_shape': 'sphere', - 'emitter_radius': 0.1 - }) - elif effect_type == "水花": - self.edit_parameters.update({ - 'emission_rate': 80, - 'particle_lifetime': 3.0, - 'start_color': Vec3(0.2, 0.6, 1.0), - 'end_color': Vec3(0.1, 0.3, 0.8), - 'start_size': 0.1, - 'end_size': 0.05, - 'velocity': Vec3(0, 0, 3), - 'velocity_variation': 1.0, - 'gravity_scale': 2.0, - 'bounce_factor': 0.3, - 'emitter_shape': 'circle', - 'emitter_radius': 0.2 - }) - elif effect_type == "魔法": - self.edit_parameters.update({ - 'emission_rate': 40, - 'particle_lifetime': 3.0, - 'start_color': Vec3(0.5, 0.2, 1.0), - 'end_color': Vec3(0.8, 0.6, 1.0), - 'start_size': 0.15, - 'end_size': 0.05, - 'velocity': Vec3(0, 0, 0.5), - 'velocity_variation': 0.8, - 'gravity_scale': -0.5, - 'rotation_enabled': True, - 'rotation_speed': 2.0, - 'emitter_shape': 'sphere', - 'emitter_radius': 0.8 - }) - elif effect_type == "雪花": - self.edit_parameters.update({ - 'emission_rate': 60, - 'particle_lifetime': 8.0, - 'start_color': Vec3(0.9, 0.9, 1.0), - 'end_color': Vec3(0.9, 0.9, 1.0), - 'start_size': 0.05, - 'end_size': 0.05, - 'velocity': Vec3(0, 0, -1), - 'velocity_variation': 0.5, - 'gravity_scale': 0.8, - 'air_resistance': 0.1, - 'bounce_factor': 0.2, - 'emitter_shape': 'box', - 'emitter_dimensions': Vec3(20, 20, 1) - }) - - # 更新GUI显示 - self._update_parameter_display() - - def _update_parameter_display(self): - """更新参数显示""" - for param_name, control in self.parameter_controls.items(): - if param_name in self.edit_parameters: - value = self.edit_parameters[param_name] - if control.get('type') == 'boolean': - if 'value_label' in control: - control['value_label'].setText("开" if value else "关") - else: - if 'value_label' in control: - if isinstance(value, (int, float)): - control['value_label'].setText(f"{value:.2f}") - else: - control['value_label'].setText(str(value)) - - def apply_changes(self): - """应用更改到当前效果""" - if not self.current_emitter_id or not self.current_emitter: - print("⚠ 没有选中的效果") - return - - # 更新发射器参数 - try: - # 更新发射器配置 - for param_name, value in self.edit_parameters.items(): - if hasattr(self.current_emitter, param_name): - setattr(self.current_emitter, param_name, value) - - print("✓ 参数已应用") - - except Exception as e: - print(f"⚠ 参数应用失败: {e}") - - def reset_parameters(self): - """重置参数为默认值""" - # 重新设置当前类型参数 - current_type = self.edit_parameters.get('emitter_type', 'fire') - self.set_effect_type(current_type.capitalize()) - print("✓ 参数已重置") - - def delete_current_effect(self): - """删除当前效果""" - if self.current_emitter_id: - self.particle_manager.remove_emitter(self.current_emitter_id) - self.current_emitter_id = None - self.current_emitter = None - print("✓ 当前效果已删除") - - def update_parameter(self, param_name: str, value: float): - """更新参数值""" - if not self.current_emitter or param_name not in self.parameter_controls: - return - - # 更新控制信息 - control = self.parameter_controls[param_name] - - # 更新GUI显示 - if 'value_label' in control: - try: - control['value_label'].setText(f"{value:.2f}") - except: - pass - - # 应用到发射器 - self._apply_parameter_to_emitter(param_name, value) - - if self.auto_update: - print(f"✓ 更新参数 {param_name}: {value}") - - def _apply_parameter_to_emitter(self, param_name: str, value: float): - """将参数应用到发射器""" - if not self.current_emitter: - return - - try: - if param_name == "emission_rate": - self.current_emitter.emission_rate = value - elif param_name == "particle_lifetime": - self.current_emitter.particle_lifetime = value - elif param_name == "start_size": - self.current_emitter.start_size = value - elif param_name == "end_size": - self.current_emitter.end_size = value - elif param_name == "velocity_variation": - self.current_emitter.velocity_variation = value - - except Exception as e: - print(f"⚠ 参数应用失败 {param_name}: {e}") - - def set_emitter_position(self, position: Point3): - """设置发射器位置""" - self.editor_position = position - if self.current_emitter: - self.current_emitter.set_position(position) - - def get_current_config(self) -> Dict[str, Any]: - """获取当前配置""" - config = self.edit_parameters.copy() - config['emitter_type'] = self.edit_parameters.get('emitter_type', 'fire') - config['position'] = [ - self.editor_position.x, - self.editor_position.y, - self.editor_position.z - ] - return config - - def load_config(self, config: Dict[str, Any]): - """加载配置""" - try: - # 设置位置 - if 'position' in config: - pos = config['position'] - position = Point3(pos[0], pos[1], pos[2]) - self.set_emitter_position(position) - - # 更新编辑参数 - for param_name, value in config.items(): - if param_name in self.edit_parameters: - self.edit_parameters[param_name] = value - - # 更新GUI显示 - self._update_parameter_display() - - # 应用到当前效果 - if self.auto_update: - self.apply_changes() - - print("✓ 配置加载完成") - - except Exception as e: - print(f"⚠ 配置加载失败: {e}") - - def save_preset(self, name: str = "自定义预设") -> bool: - """保存预设""" - try: - config = self.get_current_config() - - # 这里应该调用预设管理器保存 - print(f"✓ 保存预设 '{name}':") - print(json.dumps(config, indent=2, default=str)) - - return True - - except Exception as e: - print(f"⚠ 预设保存失败: {e}") - return False - - def load_preset(self, name: str = "经典火焰") -> bool: - """加载预设""" - try: - # 这里应该调用预设管理器加载 - # 简化实现:使用默认配置 - default_configs = { - "经典火焰": { - 'emitter_type': 'fire', - 'emission_rate': 50, - 'particle_lifetime': 2.0, - 'start_color': [1.0, 0.3, 0.0], - 'end_color': [1.0, 0.0, 0.0], - 'start_size': 0.2, - 'end_size': 0.8, - 'velocity': [0, 0, 2], - 'velocity_variation': 0.5, - 'gravity_scale': -0.2, - 'emitter_shape': 'circle', - 'emitter_radius': 0.3 - }, - "浓密烟雾": { - 'emitter_type': 'smoke', - 'emission_rate': 30, - 'particle_lifetime': 4.0, - 'start_color': [0.8, 0.8, 0.8], - 'end_color': [0.3, 0.3, 0.3], - 'start_size': 0.3, - 'end_size': 1.5, - 'velocity': [0, 0, 1], - 'velocity_variation': 0.3, - 'gravity_scale': -0.1, - 'emitter_shape': 'circle', - 'emitter_radius': 0.5 - } - } - - if name in default_configs: - self.load_config(default_configs[name]) - print(f"✓ 加载预设 '{name}'") - return True - else: - print(f"⚠ 预设 '{name}' 不存在") - return False - - except Exception as e: - print(f"⚠ 预设加载失败: {e}") - return False - - def reset_to_defaults(self): - """重置为默认值""" - if self.current_emitter: - # 重新创建当前类型的发射器 - effect_type = self.current_emitter.emitter_type - self.create_new_effect(effect_type.capitalize()) - - def cleanup(self): - """清理编辑器资源""" - self.hide() - - if self.current_emitter_id: - self.particle_manager.remove_emitter(self.current_emitter_id) - - self.current_emitter_id = None +""" +粒子编辑器 +提供可视化的粒子效果编辑功能 +""" + +import json +from typing import Dict, Optional, Any +from panda3d.core import Vec3, Point3 + +class ParticleEditor: + """ + 粒子编辑器 + 提供实时编辑粒子效果的功能 + """ + + def __init__(self, world, particle_manager): + """ + 初始化粒子编辑器 + + Args: + world: 3D世界对象 + particle_manager: 粒子管理器 + """ + self.world = world + self.particle_manager = particle_manager + self.visible = False + + # 当前编辑的发射器 + self.current_emitter_id = None + self.current_emitter = None + + # 编辑器GUI元素 + self.gui_elements = [] + self.parameter_controls = {} + self.effect_type_buttons = {} + + # 预览模式 + self.preview_mode = True + self.auto_update = True + + # 编辑器状态 + self.editor_position = Point3(0, 0, 0) + self.editor_enabled = False + + # 编辑参数 + self.edit_parameters = { + 'emitter_type': 'fire', + 'emission_rate': 50, + 'particle_lifetime': 2.0, + 'start_color': Vec3(1.0, 0.3, 0.0), + 'end_color': Vec3(1.0, 0.0, 0.0), + 'start_size': 0.2, + 'end_size': 0.8, + 'velocity': Vec3(0, 0, 2), + 'velocity_variation': 0.5, + 'gravity_scale': -0.2, + 'air_resistance': 0.02, + 'bounce_factor': 0.5, + 'emitter_shape': 'circle', + 'emitter_radius': 0.3, + 'rotation_enabled': False, + 'rotation_speed': 0.0 + } + + print("✓ 粒子编辑器初始化完成") + + def toggle_visibility(self): + """切换编辑器可见性""" + if self.visible: + self.hide() + else: + self.show() + + def show(self): + """显示编辑器""" + if self.visible: + return + + self.visible = True + self._create_editor_gui() + + # 如果没有当前发射器,创建一个默认的 + if not self.current_emitter_id: + self.create_new_effect() + + print("✓ 粒子编辑器已显示") + + def hide(self): + """隐藏编辑器""" + if not self.visible: + return + + self.visible = False + self._cleanup_editor_gui() + + print("✓ 粒子编辑器已隐藏") + + def _create_editor_gui(self): + """创建编辑器GUI""" + try: + if not hasattr(self.world, 'gui_manager') or not self.world.gui_manager: + print("⚠ GUI管理器不可用,无法创建编辑器界面") + return + + gui_manager = self.world.gui_manager + + # 创建编辑器面板标题 + panel = gui_manager.createGUIButton( + pos=(0.7, 0, 0.95), + text="粒子编辑器", + size=0.06 + ) + self.gui_elements.append(panel) + + # 创建效果类型选择按钮 + effect_types = ["火焰", "烟雾", "爆炸", "水花", "魔法", "雪花", "自定义"] + for i, effect_type in enumerate(effect_types): + button = gui_manager.createGUIButton( + pos=(0.7, 0, 0.88 - i * 0.06), + text=effect_type, + size=0.04 + ) + self.gui_elements.append(button) + self.effect_type_buttons[effect_type] = button + self.world.accept(f"editor_effect_{effect_type}", + lambda t=effect_type: self.set_effect_type(t)) + + # 创建参数控制面板 + self._create_parameter_controls() + + # 创建操作按钮 + self._create_action_buttons() + + except Exception as e: + print(f"⚠ 编辑器GUI创建失败: {e}") + + def _create_parameter_controls(self): + """创建参数控制界面""" + if not hasattr(self.world, 'gui_manager') or not self.world.gui_manager: + return + + gui_manager = self.world.gui_manager + + # 基础参数控制 + parameters = [ + ("发射率", "emission_rate", 0, 500, 1), + ("生命周期", "particle_lifetime", 0.1, 10.0, 0.1), + ("起始尺寸", "start_size", 0.01, 5.0, 0.01), + ("结束尺寸", "end_size", 0.01, 5.0, 0.01), + ("速度变化", "velocity_variation", 0.0, 5.0, 0.1), + ("重力缩放", "gravity_scale", -5.0, 5.0, 0.1), + ("空气阻力", "air_resistance", 0.0, 1.0, 0.01), + ("反弹系数", "bounce_factor", 0.0, 1.0, 0.1), + ("发射器半径", "emitter_radius", 0.0, 5.0, 0.1), + ("旋转速度", "rotation_speed", -10.0, 10.0, 0.1) + ] + + for i, (name, param, min_val, max_val, step) in enumerate(parameters): + y_pos = 0.8 - i * 0.055 + + # 参数标签 + label = gui_manager.createGUIButton( + pos=(0.75, 0, y_pos), + text=f"{name}:", + size=0.03 + ) + self.gui_elements.append(label) + + # 参数值显示 + value_label = gui_manager.createGUIButton( + pos=(0.88, 0, y_pos), + text=f"{self.edit_parameters[param]:.2f}", + size=0.03 + ) + self.gui_elements.append(value_label) + + # 减少按钮 + dec_button = gui_manager.createGUIButton( + pos=(0.83, 0, y_pos), + text="-", + size=0.03 + ) + self.gui_elements.append(dec_button) + + # 增加按钮 + inc_button = gui_manager.createGUIButton( + pos=(0.93, 0, y_pos), + text="+", + size=0.03 + ) + self.gui_elements.append(inc_button) + + # 注册事件 + self.world.accept(f"dec_{param}", lambda p=param, s=step: self._decrease_parameter(p, s)) + self.world.accept(f"inc_{param}", lambda p=param, s=step: self._increase_parameter(p, s)) + + # 存储控制信息 + self.parameter_controls[param] = { + 'name': name, + 'min': min_val, + 'max': max_val, + 'step': step, + 'value_label': value_label, + 'dec_button': dec_button, + 'inc_button': inc_button + } + + # 布尔型参数 + bool_params = [ + ("启用旋转", "rotation_enabled"), + ("自动更新", "auto_update"), + ("预览模式", "preview_mode") + ] + + for i, (name, param) in enumerate(bool_params): + y_pos = 0.25 - i * 0.055 + + # 参数标签 + label = gui_manager.createGUIButton( + pos=(0.75, 0, y_pos), + text=f"{name}:", + size=0.03 + ) + self.gui_elements.append(label) + + # 开关按钮 + switch_button = gui_manager.createGUIButton( + pos=(0.88, 0, y_pos), + text="开" if self.edit_parameters.get(param, False) else "关", + size=0.03 + ) + self.gui_elements.append(switch_button) + + # 注册事件 + self.world.accept(f"toggle_{param}", lambda p=param: self._toggle_boolean_parameter(p)) + + self.parameter_controls[param] = { + 'name': name, + 'value_label': switch_button, + 'type': 'boolean' + } + + def _create_action_buttons(self): + """创建操作按钮""" + if not hasattr(self.world, 'gui_manager') or not self.world.gui_manager: + return + + gui_manager = self.world.gui_manager + + actions = [ + ("应用更改", 0.15, self.apply_changes), + ("重置参数", 0.09, self.reset_parameters), + ("创建新效果", 0.03, self.create_new_effect), + ("删除当前", -0.03, self.delete_current_effect), + ("保存预设", -0.09, self.save_preset), + ("加载预设", -0.15, self.load_preset) + ] + + for name, y_offset, callback in actions: + button = gui_manager.createGUIButton( + pos=(0.82, 0, y_offset), + text=name, + size=0.035 + ) + self.gui_elements.append(button) + self.world.accept(f"action_{name}", callback) + + def _cleanup_editor_gui(self): + """清理编辑器GUI""" + try: + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + gui_manager = self.world.gui_manager + + for element in self.gui_elements: + gui_manager.deleteGUIElement(element) + + # 清理事件监听 + for param in self.parameter_controls: + self.world.ignore(f"dec_{param}") + self.world.ignore(f"inc_{param}") + + for param in ['rotation_enabled', 'auto_update', 'preview_mode']: + self.world.ignore(f"toggle_{param}") + + self.world.ignore("action_应用更改") + self.world.ignore("action_重置参数") + self.world.ignore("action_创建新效果") + self.world.ignore("action_删除当前") + self.world.ignore("action_保存预设") + self.world.ignore("action_加载预设") + + self.gui_elements.clear() + self.parameter_controls.clear() + self.effect_type_buttons.clear() + + except Exception as e: + print(f"⚠ 编辑器GUI清理失败: {e}") + + def _decrease_parameter(self, param_name: str, step: float): + """减少参数值""" + if param_name in self.edit_parameters: + control = self.parameter_controls[param_name] + current_value = self.edit_parameters[param_name] + new_value = max(control['min'], current_value - step) + self.edit_parameters[param_name] = new_value + + # 更新显示 + if 'value_label' in control: + control['value_label'].setText(f"{new_value:.2f}") + + # 自动应用更改 + if self.auto_update: + self.apply_changes() + + def _increase_parameter(self, param_name: str, step: float): + """增加参数值""" + if param_name in self.edit_parameters: + control = self.parameter_controls[param_name] + current_value = self.edit_parameters[param_name] + new_value = min(control['max'], current_value + step) + self.edit_parameters[param_name] = new_value + + # 更新显示 + if 'value_label' in control: + control['value_label'].setText(f"{new_value:.2f}") + + # 自动应用更改 + if self.auto_update: + self.apply_changes() + + def _toggle_boolean_parameter(self, param_name: str): + """切换布尔参数""" + if param_name in self.edit_parameters: + current_value = self.edit_parameters[param_name] + new_value = not current_value + self.edit_parameters[param_name] = new_value + + # 更新显示 + control = self.parameter_controls[param_name] + if 'value_label' in control: + control['value_label'].setText("开" if new_value else "关") + + # 特殊处理某些参数 + if param_name == 'auto_update': + self.auto_update = new_value + elif param_name == 'preview_mode': + self.preview_mode = new_value + + # 自动应用更改 + if self.auto_update: + self.apply_changes() + + def create_new_effect(self, effect_type: str = "火焰"): + """创建新的粒子效果""" + # 删除当前效果 + if self.current_emitter_id: + self.particle_manager.remove_emitter(self.current_emitter_id) + + # 在摄像机前方创建 + camera_pos = self.world.camera.getPos() + camera_forward = self.world.camera.getNetTransform().getMat().getRow3(1) + position = camera_pos + camera_forward * 5 + + # 根据类型设置参数 + self.set_effect_type(effect_type) + + # 创建发射器 + config = self.edit_parameters.copy() + config['emitter_type'] = effect_type.lower() + + self.current_emitter_id = self.particle_manager.create_emitter( + effect_type.lower(), + position, + config + ) + + self.current_emitter = self.particle_manager.get_emitter(self.current_emitter_id) + self.editor_position = position + + print(f"✓ 创建新粒子效果: {effect_type}") + + def set_effect_type(self, effect_type: str): + """设置效果类型""" + self.edit_parameters['emitter_type'] = effect_type.lower() + + # 根据类型预设参数 + if effect_type == "火焰": + self.edit_parameters.update({ + 'emission_rate': 50, + 'particle_lifetime': 2.0, + 'start_color': Vec3(1.0, 0.3, 0.0), + 'end_color': Vec3(1.0, 0.0, 0.0), + 'start_size': 0.2, + 'end_size': 0.8, + 'velocity': Vec3(0, 0, 2), + 'velocity_variation': 0.5, + 'gravity_scale': -0.2, + 'emitter_shape': 'circle', + 'emitter_radius': 0.3 + }) + elif effect_type == "烟雾": + self.edit_parameters.update({ + 'emission_rate': 30, + 'particle_lifetime': 4.0, + 'start_color': Vec3(0.8, 0.8, 0.8), + 'end_color': Vec3(0.3, 0.3, 0.3), + 'start_size': 0.3, + 'end_size': 1.5, + 'velocity': Vec3(0, 0, 1), + 'velocity_variation': 0.3, + 'gravity_scale': -0.1, + 'emitter_shape': 'circle', + 'emitter_radius': 0.5 + }) + elif effect_type == "爆炸": + self.edit_parameters.update({ + 'emission_rate': 200, + 'burst_count': 100, + 'particle_lifetime': 1.5, + 'start_color': Vec3(1.0, 0.8, 0.0), + 'end_color': Vec3(1.0, 0.2, 0.0), + 'start_size': 0.1, + 'end_size': 0.5, + 'velocity': Vec3(0, 0, 0), + 'velocity_variation': 2.0, + 'gravity_scale': 0.5, + 'duration': 0.2, + 'emitter_shape': 'sphere', + 'emitter_radius': 0.1 + }) + elif effect_type == "水花": + self.edit_parameters.update({ + 'emission_rate': 80, + 'particle_lifetime': 3.0, + 'start_color': Vec3(0.2, 0.6, 1.0), + 'end_color': Vec3(0.1, 0.3, 0.8), + 'start_size': 0.1, + 'end_size': 0.05, + 'velocity': Vec3(0, 0, 3), + 'velocity_variation': 1.0, + 'gravity_scale': 2.0, + 'bounce_factor': 0.3, + 'emitter_shape': 'circle', + 'emitter_radius': 0.2 + }) + elif effect_type == "魔法": + self.edit_parameters.update({ + 'emission_rate': 40, + 'particle_lifetime': 3.0, + 'start_color': Vec3(0.5, 0.2, 1.0), + 'end_color': Vec3(0.8, 0.6, 1.0), + 'start_size': 0.15, + 'end_size': 0.05, + 'velocity': Vec3(0, 0, 0.5), + 'velocity_variation': 0.8, + 'gravity_scale': -0.5, + 'rotation_enabled': True, + 'rotation_speed': 2.0, + 'emitter_shape': 'sphere', + 'emitter_radius': 0.8 + }) + elif effect_type == "雪花": + self.edit_parameters.update({ + 'emission_rate': 60, + 'particle_lifetime': 8.0, + 'start_color': Vec3(0.9, 0.9, 1.0), + 'end_color': Vec3(0.9, 0.9, 1.0), + 'start_size': 0.05, + 'end_size': 0.05, + 'velocity': Vec3(0, 0, -1), + 'velocity_variation': 0.5, + 'gravity_scale': 0.8, + 'air_resistance': 0.1, + 'bounce_factor': 0.2, + 'emitter_shape': 'box', + 'emitter_dimensions': Vec3(20, 20, 1) + }) + + # 更新GUI显示 + self._update_parameter_display() + + def _update_parameter_display(self): + """更新参数显示""" + for param_name, control in self.parameter_controls.items(): + if param_name in self.edit_parameters: + value = self.edit_parameters[param_name] + if control.get('type') == 'boolean': + if 'value_label' in control: + control['value_label'].setText("开" if value else "关") + else: + if 'value_label' in control: + if isinstance(value, (int, float)): + control['value_label'].setText(f"{value:.2f}") + else: + control['value_label'].setText(str(value)) + + def apply_changes(self): + """应用更改到当前效果""" + if not self.current_emitter_id or not self.current_emitter: + print("⚠ 没有选中的效果") + return + + # 更新发射器参数 + try: + # 更新发射器配置 + for param_name, value in self.edit_parameters.items(): + if hasattr(self.current_emitter, param_name): + setattr(self.current_emitter, param_name, value) + + print("✓ 参数已应用") + + except Exception as e: + print(f"⚠ 参数应用失败: {e}") + + def reset_parameters(self): + """重置参数为默认值""" + # 重新设置当前类型参数 + current_type = self.edit_parameters.get('emitter_type', 'fire') + self.set_effect_type(current_type.capitalize()) + print("✓ 参数已重置") + + def delete_current_effect(self): + """删除当前效果""" + if self.current_emitter_id: + self.particle_manager.remove_emitter(self.current_emitter_id) + self.current_emitter_id = None + self.current_emitter = None + print("✓ 当前效果已删除") + + def update_parameter(self, param_name: str, value: float): + """更新参数值""" + if not self.current_emitter or param_name not in self.parameter_controls: + return + + # 更新控制信息 + control = self.parameter_controls[param_name] + + # 更新GUI显示 + if 'value_label' in control: + try: + control['value_label'].setText(f"{value:.2f}") + except: + pass + + # 应用到发射器 + self._apply_parameter_to_emitter(param_name, value) + + if self.auto_update: + print(f"✓ 更新参数 {param_name}: {value}") + + def _apply_parameter_to_emitter(self, param_name: str, value: float): + """将参数应用到发射器""" + if not self.current_emitter: + return + + try: + if param_name == "emission_rate": + self.current_emitter.emission_rate = value + elif param_name == "particle_lifetime": + self.current_emitter.particle_lifetime = value + elif param_name == "start_size": + self.current_emitter.start_size = value + elif param_name == "end_size": + self.current_emitter.end_size = value + elif param_name == "velocity_variation": + self.current_emitter.velocity_variation = value + + except Exception as e: + print(f"⚠ 参数应用失败 {param_name}: {e}") + + def set_emitter_position(self, position: Point3): + """设置发射器位置""" + self.editor_position = position + if self.current_emitter: + self.current_emitter.set_position(position) + + def get_current_config(self) -> Dict[str, Any]: + """获取当前配置""" + config = self.edit_parameters.copy() + config['emitter_type'] = self.edit_parameters.get('emitter_type', 'fire') + config['position'] = [ + self.editor_position.x, + self.editor_position.y, + self.editor_position.z + ] + return config + + def load_config(self, config: Dict[str, Any]): + """加载配置""" + try: + # 设置位置 + if 'position' in config: + pos = config['position'] + position = Point3(pos[0], pos[1], pos[2]) + self.set_emitter_position(position) + + # 更新编辑参数 + for param_name, value in config.items(): + if param_name in self.edit_parameters: + self.edit_parameters[param_name] = value + + # 更新GUI显示 + self._update_parameter_display() + + # 应用到当前效果 + if self.auto_update: + self.apply_changes() + + print("✓ 配置加载完成") + + except Exception as e: + print(f"⚠ 配置加载失败: {e}") + + def save_preset(self, name: str = "自定义预设") -> bool: + """保存预设""" + try: + config = self.get_current_config() + + # 这里应该调用预设管理器保存 + print(f"✓ 保存预设 '{name}':") + print(json.dumps(config, indent=2, default=str)) + + return True + + except Exception as e: + print(f"⚠ 预设保存失败: {e}") + return False + + def load_preset(self, name: str = "经典火焰") -> bool: + """加载预设""" + try: + # 这里应该调用预设管理器加载 + # 简化实现:使用默认配置 + default_configs = { + "经典火焰": { + 'emitter_type': 'fire', + 'emission_rate': 50, + 'particle_lifetime': 2.0, + 'start_color': [1.0, 0.3, 0.0], + 'end_color': [1.0, 0.0, 0.0], + 'start_size': 0.2, + 'end_size': 0.8, + 'velocity': [0, 0, 2], + 'velocity_variation': 0.5, + 'gravity_scale': -0.2, + 'emitter_shape': 'circle', + 'emitter_radius': 0.3 + }, + "浓密烟雾": { + 'emitter_type': 'smoke', + 'emission_rate': 30, + 'particle_lifetime': 4.0, + 'start_color': [0.8, 0.8, 0.8], + 'end_color': [0.3, 0.3, 0.3], + 'start_size': 0.3, + 'end_size': 1.5, + 'velocity': [0, 0, 1], + 'velocity_variation': 0.3, + 'gravity_scale': -0.1, + 'emitter_shape': 'circle', + 'emitter_radius': 0.5 + } + } + + if name in default_configs: + self.load_config(default_configs[name]) + print(f"✓ 加载预设 '{name}'") + return True + else: + print(f"⚠ 预设 '{name}' 不存在") + return False + + except Exception as e: + print(f"⚠ 预设加载失败: {e}") + return False + + def reset_to_defaults(self): + """重置为默认值""" + if self.current_emitter: + # 重新创建当前类型的发射器 + effect_type = self.current_emitter.emitter_type + self.create_new_effect(effect_type.capitalize()) + + def cleanup(self): + """清理编辑器资源""" + self.hide() + + if self.current_emitter_id: + self.particle_manager.remove_emitter(self.current_emitter_id) + + self.current_emitter_id = None self.current_emitter = None \ No newline at end of file diff --git a/plugins/user/particle_system/editor/preset_manager.py b/plugins/user/particle_system/editor/preset_manager.py index 476e38bd..bda2475a 100644 --- a/plugins/user/particle_system/editor/preset_manager.py +++ b/plugins/user/particle_system/editor/preset_manager.py @@ -1,605 +1,605 @@ -""" -预设管理器 -负责粒子效果预设的保存、加载和管理 -""" - -import os -import json -from typing import Dict, List, Optional, Any -from datetime import datetime -from panda3d.core import Vec3 - -class PresetManager: - """ - 预设管理器 - 管理粒子效果的预设配置 - """ - - def __init__(self, plugin_dir: str = None): - """ - 初始化预设管理器 - - Args: - plugin_dir: 插件目录路径 - """ - if plugin_dir is None: - plugin_dir = os.path.dirname(os.path.dirname(__file__)) - - self.plugin_dir = plugin_dir - self.presets_dir = os.path.join(plugin_dir, "presets") - - # 确保预设目录存在 - os.makedirs(self.presets_dir, exist_ok=True) - - # 预设缓存 - self.presets_cache: Dict[str, Dict] = {} - self.cache_dirty = True - - # 内置预设 - self.builtin_presets = self._create_builtin_presets() - - print(f"✓ 预设管理器初始化完成,预设目录: {self.presets_dir}") - - def _create_builtin_presets(self) -> Dict[str, Dict]: - """创建内置预设""" - return { - "经典火焰": { - "metadata": { - "name": "经典火焰", - "description": "经典的火焰效果", - "author": "EG Team", - "version": "1.0", - "created": "2024-10-28", - "builtin": True - }, - "config": { - "emitter_type": "fire", - "emission_rate": 50, - "particle_lifetime": 2.0, - "start_color": [1.0, 0.3, 0.0], - "end_color": [1.0, 0.0, 0.0], - "start_size": 0.2, - "end_size": 0.8, - "velocity": [0, 0, 2], - "velocity_variation": 0.5, - "gravity_scale": -0.2, - "air_resistance": 0.02, - "emitter_shape": "circle", - "emitter_radius": 0.3 - } - }, - - "浓密烟雾": { - "metadata": { - "name": "浓密烟雾", - "description": "浓密的烟雾效果", - "author": "EG Team", - "version": "1.0", - "created": "2024-10-28", - "builtin": True - }, - "config": { - "emitter_type": "smoke", - "emission_rate": 30, - "particle_lifetime": 4.0, - "start_color": [0.8, 0.8, 0.8], - "end_color": [0.3, 0.3, 0.3], - "start_size": 0.3, - "end_size": 1.5, - "velocity": [0, 0, 1], - "velocity_variation": 0.3, - "gravity_scale": -0.1, - "air_resistance": 0.05, - "emitter_shape": "circle", - "emitter_radius": 0.5 - } - }, - - "强力爆炸": { - "metadata": { - "name": "强力爆炸", - "description": "强力的爆炸效果", - "author": "EG Team", - "version": "1.0", - "created": "2024-10-28", - "builtin": True - }, - "config": { - "emitter_type": "explosion", - "emission_rate": 200, - "burst_count": 100, - "particle_lifetime": 1.5, - "start_color": [1.0, 0.8, 0.0], - "end_color": [1.0, 0.2, 0.0], - "start_size": 0.1, - "end_size": 0.5, - "velocity": [0, 0, 0], - "velocity_variation": 2.0, - "gravity_scale": 0.5, - "duration": 0.2, - "emitter_shape": "sphere", - "emitter_radius": 0.1 - } - }, - - "清澈水花": { - "metadata": { - "name": "清澈水花", - "description": "清澈的水花效果", - "author": "EG Team", - "version": "1.0", - "created": "2024-10-28", - "builtin": True - }, - "config": { - "emitter_type": "water", - "emission_rate": 80, - "particle_lifetime": 3.0, - "start_color": [0.2, 0.6, 1.0], - "end_color": [0.1, 0.3, 0.8], - "start_size": 0.1, - "end_size": 0.05, - "velocity": [0, 0, 3], - "velocity_variation": 1.0, - "gravity_scale": 2.0, - "bounce_factor": 0.3, - "air_resistance": 0.01, - "emitter_shape": "circle", - "emitter_radius": 0.2 - } - }, - - "魔法光球": { - "metadata": { - "name": "魔法光球", - "description": "神秘的魔法光球效果", - "author": "EG Team", - "version": "1.0", - "created": "2024-10-28", - "builtin": True - }, - "config": { - "emitter_type": "magic", - "emission_rate": 40, - "particle_lifetime": 3.0, - "start_color": [0.5, 0.2, 1.0], - "end_color": [0.8, 0.6, 1.0], - "start_size": 0.15, - "end_size": 0.05, - "velocity": [0, 0, 0.5], - "velocity_variation": 0.8, - "gravity_scale": -0.5, - "rotation_enabled": True, - "rotation_speed": 2.0, - "emitter_shape": "sphere", - "emitter_radius": 0.8 - } - }, - - "飘落雪花": { - "metadata": { - "name": "飘落雪花", - "description": "冬季飘落的雪花效果", - "author": "EG Team", - "version": "1.0", - "created": "2024-10-28", - "builtin": True - }, - "config": { - "emitter_type": "snow", - "emission_rate": 60, - "particle_lifetime": 8.0, - "start_color": [0.9, 0.9, 1.0], - "end_color": [0.9, 0.9, 1.0], - "start_size": 0.05, - "end_size": 0.05, - "velocity": [0, 0, -1], - "velocity_variation": 0.5, - "gravity_scale": 0.8, - "air_resistance": 0.1, - "bounce_factor": 0.2, - "emitter_shape": "box", - "emitter_dimensions": [20, 20, 1] - } - } - } - - def save_preset(self, name: str, config: Dict[str, Any], - description: str = "", author: str = "User") -> bool: - """ - 保存预设 - - Args: - name: 预设名称 - config: 预设配置 - description: 预设描述 - author: 作者 - - Returns: - 是否保存成功 - """ - try: - # 创建预设数据 - preset_data = { - "metadata": { - "name": name, - "description": description, - "author": author, - "version": "1.0", - "created": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "builtin": False - }, - "config": config - } - - # 保存到文件 - filename = self._sanitize_filename(name) + ".json" - filepath = os.path.join(self.presets_dir, filename) - - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(preset_data, f, indent=2, ensure_ascii=False, default=self._json_serializer) - - # 更新缓存 - self.presets_cache[name] = preset_data - - print(f"✓ 预设已保存: {name} -> {filepath}") - return True - - except Exception as e: - print(f"⚠ 预设保存失败 '{name}': {e}") - return False - - def _json_serializer(self, obj): - """JSON序列化器,处理特殊对象""" - if isinstance(obj, Vec3): - return [obj.x, obj.y, obj.z] - raise TypeError(f"Object of type {type(obj)} is not JSON serializable") - - def load_preset(self, name: str) -> Optional[Dict[str, Any]]: - """ - 加载预设 - - Args: - name: 预设名称 - - Returns: - 预设数据,如果不存在返回None - """ - try: - # 检查内置预设 - if name in self.builtin_presets: - return self.builtin_presets[name] - - # 检查缓存 - if name in self.presets_cache and not self.cache_dirty: - return self.presets_cache[name] - - # 从文件加载 - filename = self._sanitize_filename(name) + ".json" - filepath = os.path.join(self.presets_dir, filename) - - if os.path.exists(filepath): - with open(filepath, 'r', encoding='utf-8') as f: - preset_data = json.load(f) - - # 更新缓存 - self.presets_cache[name] = preset_data - self.cache_dirty = False - - print(f"✓ 预设已加载: {name}") - return preset_data - else: - print(f"⚠ 预设文件不存在: {filepath}") - return None - - except Exception as e: - print(f"⚠ 预设加载失败 '{name}': {e}") - return None - - def delete_preset(self, name: str) -> bool: - """ - 删除预设 - - Args: - name: 预设名称 - - Returns: - 是否删除成功 - """ - try: - # 不能删除内置预设 - if name in self.builtin_presets: - print(f"⚠ 不能删除内置预设: {name}") - return False - - # 删除文件 - filename = self._sanitize_filename(name) + ".json" - filepath = os.path.join(self.presets_dir, filename) - - if os.path.exists(filepath): - os.remove(filepath) - - # 从缓存移除 - if name in self.presets_cache: - del self.presets_cache[name] - - print(f"✓ 预设已删除: {name}") - return True - - except Exception as e: - print(f"⚠ 预设删除失败 '{name}': {e}") - return False - - def list_presets(self) -> List[str]: - """ - 获取所有预设名称列表 - - Returns: - 预设名称列表 - """ - presets = [] - - # 添加内置预设 - presets.extend(self.builtin_presets.keys()) - - # 添加用户预设 - try: - if os.path.exists(self.presets_dir): - for filename in os.listdir(self.presets_dir): - if filename.endswith('.json'): - name = filename[:-5] # 移除.json扩展名 - # 尝试从文件名恢复原始名称 - original_name = self._restore_filename(name) - if original_name not in presets: - presets.append(original_name) - - except Exception as e: - print(f"⚠ 列出预设失败: {e}") - - return sorted(presets) - - def get_preset_info(self, name: str) -> Optional[Dict[str, Any]]: - """ - 获取预设信息 - - Args: - name: 预设名称 - - Returns: - 预设元数据 - """ - preset_data = self.load_preset(name) - if preset_data and 'metadata' in preset_data: - return preset_data['metadata'] - return None - - def get_preset_config(self, name: str) -> Optional[Dict[str, Any]]: - """ - 获取预设配置 - - Args: - name: 预设名称 - - Returns: - 预设配置 - """ - preset_data = self.load_preset(name) - if preset_data and 'config' in preset_data: - return preset_data['config'] - return None - - def export_preset(self, name: str, filepath: str) -> bool: - """ - 导出预设到文件 - - Args: - name: 预设名称 - filepath: 导出文件路径 - - Returns: - 是否导出成功 - """ - try: - preset_data = self.load_preset(name) - if not preset_data: - print(f"⚠ 预设不存在: {name}") - return False - - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(preset_data, f, indent=2, ensure_ascii=False, default=self._json_serializer) - - print(f"✓ 预设已导出: {name} -> {filepath}") - return True - - except Exception as e: - print(f"⚠ 预设导出失败 '{name}': {e}") - return False - - def import_preset(self, filepath: str) -> bool: - """ - 从文件导入预设 - - Args: - filepath: 预设文件路径 - - Returns: - 是否导入成功 - """ - try: - if not os.path.exists(filepath): - print(f"⚠ 预设文件不存在: {filepath}") - return False - - with open(filepath, 'r', encoding='utf-8') as f: - preset_data = json.load(f) - - # 验证预设数据格式 - if not self._validate_preset_data(preset_data): - print(f"⚠ 预设文件格式无效: {filepath}") - return False - - # 获取预设名称 - name = preset_data['metadata']['name'] - - # 保存预设 - return self.save_preset( - name, - preset_data['config'], - preset_data['metadata'].get('description', ''), - preset_data['metadata'].get('author', 'Unknown') - ) - - except Exception as e: - print(f"⚠ 预设导入失败: {e}") - return False - - def _sanitize_filename(self, name: str) -> str: - """清理文件名,移除非法字符""" - import re - # 移除或替换非法字符 - sanitized = re.sub(r'[<>:"/\\|?*]', '_', name) - # 限制长度 - if len(sanitized) > 100: - sanitized = sanitized[:100] - return sanitized - - def _restore_filename(self, filename: str) -> str: - """尝试恢复原始文件名""" - # 简单实现:直接返回文件名 - # 实际应用中可能需要更复杂的映射 - return filename - - def _validate_preset_data(self, data: Dict) -> bool: - """验证预设数据格式""" - try: - # 检查必需字段 - if 'metadata' not in data or 'config' not in data: - return False - - metadata = data['metadata'] - if 'name' not in metadata: - return False - - # 基本验证通过 - return True - - except: - return False - - def clear_cache(self): - """清空预设缓存""" - self.presets_cache.clear() - self.cache_dirty = True - print("✓ 预设缓存已清空") - - def get_cache_stats(self) -> Dict[str, Any]: - """获取缓存统计信息""" - return { - 'cached_presets': len(self.presets_cache), - 'builtin_presets': len(self.builtin_presets), - 'cache_dirty': self.cache_dirty - } - - def refresh_presets(self): - """刷新预设列表""" - self.cache_dirty = True - print("✓ 预设列表已刷新") - - def create_preset_from_config(self, name: str, config: Dict[str, Any], - description: str = "", author: str = "User") -> bool: - """ - 从配置创建预设 - - Args: - name: 预设名称 - config: 预设配置 - description: 预设描述 - author: 作者 - - Returns: - 是否创建成功 - """ - return self.save_preset(name, config, description, author) - - def duplicate_preset(self, source_name: str, new_name: str) -> bool: - """ - 复制预设 - - Args: - source_name: 源预设名称 - new_name: 新预设名称 - - Returns: - 是否复制成功 - """ - preset_data = self.load_preset(source_name) - if not preset_data: - print(f"⚠ 源预设不存在: {source_name}") - return False - - # 更新元数据 - preset_data['metadata']['name'] = new_name - preset_data['metadata']['created'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - preset_data['metadata']['builtin'] = False - - return self.save_preset( - new_name, - preset_data['config'], - preset_data['metadata'].get('description', ''), - preset_data['metadata'].get('author', 'User') - ) - - def update_preset_description(self, name: str, description: str) -> bool: - """ - 更新预设描述 - - Args: - name: 预设名称 - description: 新描述 - - Returns: - 是否更新成功 - """ - preset_data = self.load_preset(name) - if not preset_data: - print(f"⚠ 预设不存在: {name}") - return False - - if preset_data['metadata'].get('builtin', False): - print(f"⚠ 不能修改内置预设: {name}") - return False - - preset_data['metadata']['description'] = description - - return self.save_preset( - name, - preset_data['config'], - description, - preset_data['metadata'].get('author', 'User') - ) - - def search_presets(self, keyword: str) -> List[str]: - """ - 搜索预设 - - Args: - keyword: 搜索关键词 - - Returns: - 匹配的预设名称列表 - """ - all_presets = self.list_presets() - keyword = keyword.lower() - - matching_presets = [] - for preset_name in all_presets: - # 检查名称和描述 - if keyword in preset_name.lower(): - matching_presets.append(preset_name) - else: - info = self.get_preset_info(preset_name) - if info and keyword in info.get('description', '').lower(): - matching_presets.append(preset_name) - - return matching_presets - +""" +预设管理器 +负责粒子效果预设的保存、加载和管理 +""" + +import os +import json +from typing import Dict, List, Optional, Any +from datetime import datetime +from panda3d.core import Vec3 + +class PresetManager: + """ + 预设管理器 + 管理粒子效果的预设配置 + """ + + def __init__(self, plugin_dir: str = None): + """ + 初始化预设管理器 + + Args: + plugin_dir: 插件目录路径 + """ + if plugin_dir is None: + plugin_dir = os.path.dirname(os.path.dirname(__file__)) + + self.plugin_dir = plugin_dir + self.presets_dir = os.path.join(plugin_dir, "presets") + + # 确保预设目录存在 + os.makedirs(self.presets_dir, exist_ok=True) + + # 预设缓存 + self.presets_cache: Dict[str, Dict] = {} + self.cache_dirty = True + + # 内置预设 + self.builtin_presets = self._create_builtin_presets() + + print(f"✓ 预设管理器初始化完成,预设目录: {self.presets_dir}") + + def _create_builtin_presets(self) -> Dict[str, Dict]: + """创建内置预设""" + return { + "经典火焰": { + "metadata": { + "name": "经典火焰", + "description": "经典的火焰效果", + "author": "EG Team", + "version": "1.0", + "created": "2024-10-28", + "builtin": True + }, + "config": { + "emitter_type": "fire", + "emission_rate": 50, + "particle_lifetime": 2.0, + "start_color": [1.0, 0.3, 0.0], + "end_color": [1.0, 0.0, 0.0], + "start_size": 0.2, + "end_size": 0.8, + "velocity": [0, 0, 2], + "velocity_variation": 0.5, + "gravity_scale": -0.2, + "air_resistance": 0.02, + "emitter_shape": "circle", + "emitter_radius": 0.3 + } + }, + + "浓密烟雾": { + "metadata": { + "name": "浓密烟雾", + "description": "浓密的烟雾效果", + "author": "EG Team", + "version": "1.0", + "created": "2024-10-28", + "builtin": True + }, + "config": { + "emitter_type": "smoke", + "emission_rate": 30, + "particle_lifetime": 4.0, + "start_color": [0.8, 0.8, 0.8], + "end_color": [0.3, 0.3, 0.3], + "start_size": 0.3, + "end_size": 1.5, + "velocity": [0, 0, 1], + "velocity_variation": 0.3, + "gravity_scale": -0.1, + "air_resistance": 0.05, + "emitter_shape": "circle", + "emitter_radius": 0.5 + } + }, + + "强力爆炸": { + "metadata": { + "name": "强力爆炸", + "description": "强力的爆炸效果", + "author": "EG Team", + "version": "1.0", + "created": "2024-10-28", + "builtin": True + }, + "config": { + "emitter_type": "explosion", + "emission_rate": 200, + "burst_count": 100, + "particle_lifetime": 1.5, + "start_color": [1.0, 0.8, 0.0], + "end_color": [1.0, 0.2, 0.0], + "start_size": 0.1, + "end_size": 0.5, + "velocity": [0, 0, 0], + "velocity_variation": 2.0, + "gravity_scale": 0.5, + "duration": 0.2, + "emitter_shape": "sphere", + "emitter_radius": 0.1 + } + }, + + "清澈水花": { + "metadata": { + "name": "清澈水花", + "description": "清澈的水花效果", + "author": "EG Team", + "version": "1.0", + "created": "2024-10-28", + "builtin": True + }, + "config": { + "emitter_type": "water", + "emission_rate": 80, + "particle_lifetime": 3.0, + "start_color": [0.2, 0.6, 1.0], + "end_color": [0.1, 0.3, 0.8], + "start_size": 0.1, + "end_size": 0.05, + "velocity": [0, 0, 3], + "velocity_variation": 1.0, + "gravity_scale": 2.0, + "bounce_factor": 0.3, + "air_resistance": 0.01, + "emitter_shape": "circle", + "emitter_radius": 0.2 + } + }, + + "魔法光球": { + "metadata": { + "name": "魔法光球", + "description": "神秘的魔法光球效果", + "author": "EG Team", + "version": "1.0", + "created": "2024-10-28", + "builtin": True + }, + "config": { + "emitter_type": "magic", + "emission_rate": 40, + "particle_lifetime": 3.0, + "start_color": [0.5, 0.2, 1.0], + "end_color": [0.8, 0.6, 1.0], + "start_size": 0.15, + "end_size": 0.05, + "velocity": [0, 0, 0.5], + "velocity_variation": 0.8, + "gravity_scale": -0.5, + "rotation_enabled": True, + "rotation_speed": 2.0, + "emitter_shape": "sphere", + "emitter_radius": 0.8 + } + }, + + "飘落雪花": { + "metadata": { + "name": "飘落雪花", + "description": "冬季飘落的雪花效果", + "author": "EG Team", + "version": "1.0", + "created": "2024-10-28", + "builtin": True + }, + "config": { + "emitter_type": "snow", + "emission_rate": 60, + "particle_lifetime": 8.0, + "start_color": [0.9, 0.9, 1.0], + "end_color": [0.9, 0.9, 1.0], + "start_size": 0.05, + "end_size": 0.05, + "velocity": [0, 0, -1], + "velocity_variation": 0.5, + "gravity_scale": 0.8, + "air_resistance": 0.1, + "bounce_factor": 0.2, + "emitter_shape": "box", + "emitter_dimensions": [20, 20, 1] + } + } + } + + def save_preset(self, name: str, config: Dict[str, Any], + description: str = "", author: str = "User") -> bool: + """ + 保存预设 + + Args: + name: 预设名称 + config: 预设配置 + description: 预设描述 + author: 作者 + + Returns: + 是否保存成功 + """ + try: + # 创建预设数据 + preset_data = { + "metadata": { + "name": name, + "description": description, + "author": author, + "version": "1.0", + "created": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "builtin": False + }, + "config": config + } + + # 保存到文件 + filename = self._sanitize_filename(name) + ".json" + filepath = os.path.join(self.presets_dir, filename) + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(preset_data, f, indent=2, ensure_ascii=False, default=self._json_serializer) + + # 更新缓存 + self.presets_cache[name] = preset_data + + print(f"✓ 预设已保存: {name} -> {filepath}") + return True + + except Exception as e: + print(f"⚠ 预设保存失败 '{name}': {e}") + return False + + def _json_serializer(self, obj): + """JSON序列化器,处理特殊对象""" + if isinstance(obj, Vec3): + return [obj.x, obj.y, obj.z] + raise TypeError(f"Object of type {type(obj)} is not JSON serializable") + + def load_preset(self, name: str) -> Optional[Dict[str, Any]]: + """ + 加载预设 + + Args: + name: 预设名称 + + Returns: + 预设数据,如果不存在返回None + """ + try: + # 检查内置预设 + if name in self.builtin_presets: + return self.builtin_presets[name] + + # 检查缓存 + if name in self.presets_cache and not self.cache_dirty: + return self.presets_cache[name] + + # 从文件加载 + filename = self._sanitize_filename(name) + ".json" + filepath = os.path.join(self.presets_dir, filename) + + if os.path.exists(filepath): + with open(filepath, 'r', encoding='utf-8') as f: + preset_data = json.load(f) + + # 更新缓存 + self.presets_cache[name] = preset_data + self.cache_dirty = False + + print(f"✓ 预设已加载: {name}") + return preset_data + else: + print(f"⚠ 预设文件不存在: {filepath}") + return None + + except Exception as e: + print(f"⚠ 预设加载失败 '{name}': {e}") + return None + + def delete_preset(self, name: str) -> bool: + """ + 删除预设 + + Args: + name: 预设名称 + + Returns: + 是否删除成功 + """ + try: + # 不能删除内置预设 + if name in self.builtin_presets: + print(f"⚠ 不能删除内置预设: {name}") + return False + + # 删除文件 + filename = self._sanitize_filename(name) + ".json" + filepath = os.path.join(self.presets_dir, filename) + + if os.path.exists(filepath): + os.remove(filepath) + + # 从缓存移除 + if name in self.presets_cache: + del self.presets_cache[name] + + print(f"✓ 预设已删除: {name}") + return True + + except Exception as e: + print(f"⚠ 预设删除失败 '{name}': {e}") + return False + + def list_presets(self) -> List[str]: + """ + 获取所有预设名称列表 + + Returns: + 预设名称列表 + """ + presets = [] + + # 添加内置预设 + presets.extend(self.builtin_presets.keys()) + + # 添加用户预设 + try: + if os.path.exists(self.presets_dir): + for filename in os.listdir(self.presets_dir): + if filename.endswith('.json'): + name = filename[:-5] # 移除.json扩展名 + # 尝试从文件名恢复原始名称 + original_name = self._restore_filename(name) + if original_name not in presets: + presets.append(original_name) + + except Exception as e: + print(f"⚠ 列出预设失败: {e}") + + return sorted(presets) + + def get_preset_info(self, name: str) -> Optional[Dict[str, Any]]: + """ + 获取预设信息 + + Args: + name: 预设名称 + + Returns: + 预设元数据 + """ + preset_data = self.load_preset(name) + if preset_data and 'metadata' in preset_data: + return preset_data['metadata'] + return None + + def get_preset_config(self, name: str) -> Optional[Dict[str, Any]]: + """ + 获取预设配置 + + Args: + name: 预设名称 + + Returns: + 预设配置 + """ + preset_data = self.load_preset(name) + if preset_data and 'config' in preset_data: + return preset_data['config'] + return None + + def export_preset(self, name: str, filepath: str) -> bool: + """ + 导出预设到文件 + + Args: + name: 预设名称 + filepath: 导出文件路径 + + Returns: + 是否导出成功 + """ + try: + preset_data = self.load_preset(name) + if not preset_data: + print(f"⚠ 预设不存在: {name}") + return False + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(preset_data, f, indent=2, ensure_ascii=False, default=self._json_serializer) + + print(f"✓ 预设已导出: {name} -> {filepath}") + return True + + except Exception as e: + print(f"⚠ 预设导出失败 '{name}': {e}") + return False + + def import_preset(self, filepath: str) -> bool: + """ + 从文件导入预设 + + Args: + filepath: 预设文件路径 + + Returns: + 是否导入成功 + """ + try: + if not os.path.exists(filepath): + print(f"⚠ 预设文件不存在: {filepath}") + return False + + with open(filepath, 'r', encoding='utf-8') as f: + preset_data = json.load(f) + + # 验证预设数据格式 + if not self._validate_preset_data(preset_data): + print(f"⚠ 预设文件格式无效: {filepath}") + return False + + # 获取预设名称 + name = preset_data['metadata']['name'] + + # 保存预设 + return self.save_preset( + name, + preset_data['config'], + preset_data['metadata'].get('description', ''), + preset_data['metadata'].get('author', 'Unknown') + ) + + except Exception as e: + print(f"⚠ 预设导入失败: {e}") + return False + + def _sanitize_filename(self, name: str) -> str: + """清理文件名,移除非法字符""" + import re + # 移除或替换非法字符 + sanitized = re.sub(r'[<>:"/\\|?*]', '_', name) + # 限制长度 + if len(sanitized) > 100: + sanitized = sanitized[:100] + return sanitized + + def _restore_filename(self, filename: str) -> str: + """尝试恢复原始文件名""" + # 简单实现:直接返回文件名 + # 实际应用中可能需要更复杂的映射 + return filename + + def _validate_preset_data(self, data: Dict) -> bool: + """验证预设数据格式""" + try: + # 检查必需字段 + if 'metadata' not in data or 'config' not in data: + return False + + metadata = data['metadata'] + if 'name' not in metadata: + return False + + # 基本验证通过 + return True + + except: + return False + + def clear_cache(self): + """清空预设缓存""" + self.presets_cache.clear() + self.cache_dirty = True + print("✓ 预设缓存已清空") + + def get_cache_stats(self) -> Dict[str, Any]: + """获取缓存统计信息""" + return { + 'cached_presets': len(self.presets_cache), + 'builtin_presets': len(self.builtin_presets), + 'cache_dirty': self.cache_dirty + } + + def refresh_presets(self): + """刷新预设列表""" + self.cache_dirty = True + print("✓ 预设列表已刷新") + + def create_preset_from_config(self, name: str, config: Dict[str, Any], + description: str = "", author: str = "User") -> bool: + """ + 从配置创建预设 + + Args: + name: 预设名称 + config: 预设配置 + description: 预设描述 + author: 作者 + + Returns: + 是否创建成功 + """ + return self.save_preset(name, config, description, author) + + def duplicate_preset(self, source_name: str, new_name: str) -> bool: + """ + 复制预设 + + Args: + source_name: 源预设名称 + new_name: 新预设名称 + + Returns: + 是否复制成功 + """ + preset_data = self.load_preset(source_name) + if not preset_data: + print(f"⚠ 源预设不存在: {source_name}") + return False + + # 更新元数据 + preset_data['metadata']['name'] = new_name + preset_data['metadata']['created'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + preset_data['metadata']['builtin'] = False + + return self.save_preset( + new_name, + preset_data['config'], + preset_data['metadata'].get('description', ''), + preset_data['metadata'].get('author', 'User') + ) + + def update_preset_description(self, name: str, description: str) -> bool: + """ + 更新预设描述 + + Args: + name: 预设名称 + description: 新描述 + + Returns: + 是否更新成功 + """ + preset_data = self.load_preset(name) + if not preset_data: + print(f"⚠ 预设不存在: {name}") + return False + + if preset_data['metadata'].get('builtin', False): + print(f"⚠ 不能修改内置预设: {name}") + return False + + preset_data['metadata']['description'] = description + + return self.save_preset( + name, + preset_data['config'], + description, + preset_data['metadata'].get('author', 'User') + ) + + def search_presets(self, keyword: str) -> List[str]: + """ + 搜索预设 + + Args: + keyword: 搜索关键词 + + Returns: + 匹配的预设名称列表 + """ + all_presets = self.list_presets() + keyword = keyword.lower() + + matching_presets = [] + for preset_name in all_presets: + # 检查名称和描述 + if keyword in preset_name.lower(): + matching_presets.append(preset_name) + else: + info = self.get_preset_info(preset_name) + if info and keyword in info.get('description', '').lower(): + matching_presets.append(preset_name) + + return matching_presets + diff --git a/plugins/user/particle_system/effects/__init__.py b/plugins/user/particle_system/effects/__init__.py index 7acf1793..9fcf67c3 100644 --- a/plugins/user/particle_system/effects/__init__.py +++ b/plugins/user/particle_system/effects/__init__.py @@ -1,15 +1,15 @@ -""" -粒子效果库 -""" - -from .fire_effect import FireEffect -from .smoke_effect import SmokeEffect -from .water_effect import WaterEffect -from .explosion_effect import ExplosionEffect - -__all__ = [ - 'FireEffect', - 'SmokeEffect', - 'WaterEffect', - 'ExplosionEffect' +""" +粒子效果库 +""" + +from .fire_effect import FireEffect +from .smoke_effect import SmokeEffect +from .water_effect import WaterEffect +from .explosion_effect import ExplosionEffect + +__all__ = [ + 'FireEffect', + 'SmokeEffect', + 'WaterEffect', + 'ExplosionEffect' ] \ No newline at end of file diff --git a/plugins/user/particle_system/effects/explosion_effect.py b/plugins/user/particle_system/effects/explosion_effect.py index a7c5dabb..1bde9d48 100644 --- a/plugins/user/particle_system/effects/explosion_effect.py +++ b/plugins/user/particle_system/effects/explosion_effect.py @@ -1,102 +1,102 @@ -""" -爆炸效果 -""" - -from panda3d.core import Vec3, Point3 -from ..core.particle_emitter import ParticleEmitter - -class ExplosionEffect: - """ - 爆炸效果 - 模拟爆炸效果 - """ - - @staticmethod - def create_effect(world, position: Point3 = None, config: Dict = None) -> str: - """ - 创建爆炸效果 - - Args: - world: 3D世界对象 - position: 发射器位置 - config: 自定义配置 - - Returns: - 发射器ID - """ - if position is None: - position = Point3(0, 0, 0) - - if config is None: - config = {} - - # 默认配置 - default_config = { - 'particle_type': 'explosion', - 'emission_rate': 200, - 'burst_count': 100, - 'particle_lifetime': 1.5, - 'start_color': Vec3(1.0, 0.8, 0.0), - 'end_color': Vec3(1.0, 0.2, 0.0), - 'start_size': 0.1, - 'end_size': 0.5, - 'velocity': Vec3(0, 0, 0), - 'velocity_variation': 2.0, - 'gravity_scale': 0.5, - 'duration': 0.2, - 'emitter_shape': 'sphere', - 'emitter_radius': 0.1 - } - - # 合并自定义配置 - effect_config = {**default_config, **config} - - # 创建发射器 - emitter = ParticleEmitter( - "explosion_emitter", - "explosion", - position, - world, - effect_config - ) - - return emitter.emitter_id - - @staticmethod - def update_effect(emitter: ParticleEmitter, config: Dict): - """ - 更新爆炸效果 - - Args: - emitter: 粒子发射器 - config: 更新配置 - """ - # 更新发射器参数 - for key, value in config.items(): - if hasattr(emitter, key): - setattr(emitter, key, value) - - @staticmethod - def get_default_config() -> Dict: - """ - 获取默认配置 - - Returns: - 默认配置字典 - """ - return { - 'particle_type': 'explosion', - 'emission_rate': 200, - 'burst_count': 100, - 'particle_lifetime': 1.5, - 'start_color': Vec3(1.0, 0.8, 0.0), - 'end_color': Vec3(1.0, 0.2, 0.0), - 'start_size': 0.1, - 'end_size': 0.5, - 'velocity': Vec3(0, 0, 0), - 'velocity_variation': 2.0, - 'gravity_scale': 0.5, - 'duration': 0.2, - 'emitter_shape': 'sphere', - 'emitter_radius': 0.1 +""" +爆炸效果 +""" + +from panda3d.core import Vec3, Point3 +from ..core.particle_emitter import ParticleEmitter + +class ExplosionEffect: + """ + 爆炸效果 + 模拟爆炸效果 + """ + + @staticmethod + def create_effect(world, position: Point3 = None, config: Dict = None) -> str: + """ + 创建爆炸效果 + + Args: + world: 3D世界对象 + position: 发射器位置 + config: 自定义配置 + + Returns: + 发射器ID + """ + if position is None: + position = Point3(0, 0, 0) + + if config is None: + config = {} + + # 默认配置 + default_config = { + 'particle_type': 'explosion', + 'emission_rate': 200, + 'burst_count': 100, + 'particle_lifetime': 1.5, + 'start_color': Vec3(1.0, 0.8, 0.0), + 'end_color': Vec3(1.0, 0.2, 0.0), + 'start_size': 0.1, + 'end_size': 0.5, + 'velocity': Vec3(0, 0, 0), + 'velocity_variation': 2.0, + 'gravity_scale': 0.5, + 'duration': 0.2, + 'emitter_shape': 'sphere', + 'emitter_radius': 0.1 + } + + # 合并自定义配置 + effect_config = {**default_config, **config} + + # 创建发射器 + emitter = ParticleEmitter( + "explosion_emitter", + "explosion", + position, + world, + effect_config + ) + + return emitter.emitter_id + + @staticmethod + def update_effect(emitter: ParticleEmitter, config: Dict): + """ + 更新爆炸效果 + + Args: + emitter: 粒子发射器 + config: 更新配置 + """ + # 更新发射器参数 + for key, value in config.items(): + if hasattr(emitter, key): + setattr(emitter, key, value) + + @staticmethod + def get_default_config() -> Dict: + """ + 获取默认配置 + + Returns: + 默认配置字典 + """ + return { + 'particle_type': 'explosion', + 'emission_rate': 200, + 'burst_count': 100, + 'particle_lifetime': 1.5, + 'start_color': Vec3(1.0, 0.8, 0.0), + 'end_color': Vec3(1.0, 0.2, 0.0), + 'start_size': 0.1, + 'end_size': 0.5, + 'velocity': Vec3(0, 0, 0), + 'velocity_variation': 2.0, + 'gravity_scale': 0.5, + 'duration': 0.2, + 'emitter_shape': 'sphere', + 'emitter_radius': 0.1 } \ No newline at end of file diff --git a/plugins/user/particle_system/effects/fire_effect.py b/plugins/user/particle_system/effects/fire_effect.py index 7a672f8c..76c459e7 100644 --- a/plugins/user/particle_system/effects/fire_effect.py +++ b/plugins/user/particle_system/effects/fire_effect.py @@ -1,98 +1,98 @@ -""" -火焰效果 -""" - -from panda3d.core import Vec3, Point3 -from ..core.particle_emitter import ParticleEmitter - -class FireEffect: - """ - 火焰效果 - 模拟火焰燃烧效果 - """ - - @staticmethod - def create_effect(world, position: Point3 = None, config: Dict = None) -> str: - """ - 创建火焰效果 - - Args: - world: 3D世界对象 - position: 发射器位置 - config: 自定义配置 - - Returns: - 发射器ID - """ - if position is None: - position = Point3(0, 0, 0) - - if config is None: - config = {} - - # 默认配置 - default_config = { - 'particle_type': 'fire', - 'emission_rate': 50, - 'particle_lifetime': 2.0, - 'start_color': Vec3(1.0, 0.3, 0.0), - 'end_color': Vec3(1.0, 0.0, 0.0), - 'start_size': 0.2, - 'end_size': 0.8, - 'velocity': Vec3(0, 0, 2), - 'velocity_variation': 0.5, - 'gravity_scale': -0.2, - 'emitter_shape': 'circle', - 'emitter_radius': 0.3 - } - - # 合并自定义配置 - effect_config = {**default_config, **config} - - # 创建发射器 - emitter = ParticleEmitter( - "fire_emitter", - "fire", - position, - world, - effect_config - ) - - return emitter.emitter_id - - @staticmethod - def update_effect(emitter: ParticleEmitter, config: Dict): - """ - 更新火焰效果 - - Args: - emitter: 粒子发射器 - config: 更新配置 - """ - # 更新发射器参数 - for key, value in config.items(): - if hasattr(emitter, key): - setattr(emitter, key, value) - - @staticmethod - def get_default_config() -> Dict: - """ - 获取默认配置 - - Returns: - 默认配置字典 - """ - return { - 'particle_type': 'fire', - 'emission_rate': 50, - 'particle_lifetime': 2.0, - 'start_color': Vec3(1.0, 0.3, 0.0), - 'end_color': Vec3(1.0, 0.0, 0.0), - 'start_size': 0.2, - 'end_size': 0.8, - 'velocity': Vec3(0, 0, 2), - 'velocity_variation': 0.5, - 'gravity_scale': -0.2, - 'emitter_shape': 'circle', - 'emitter_radius': 0.3 +""" +火焰效果 +""" + +from panda3d.core import Vec3, Point3 +from ..core.particle_emitter import ParticleEmitter + +class FireEffect: + """ + 火焰效果 + 模拟火焰燃烧效果 + """ + + @staticmethod + def create_effect(world, position: Point3 = None, config: Dict = None) -> str: + """ + 创建火焰效果 + + Args: + world: 3D世界对象 + position: 发射器位置 + config: 自定义配置 + + Returns: + 发射器ID + """ + if position is None: + position = Point3(0, 0, 0) + + if config is None: + config = {} + + # 默认配置 + default_config = { + 'particle_type': 'fire', + 'emission_rate': 50, + 'particle_lifetime': 2.0, + 'start_color': Vec3(1.0, 0.3, 0.0), + 'end_color': Vec3(1.0, 0.0, 0.0), + 'start_size': 0.2, + 'end_size': 0.8, + 'velocity': Vec3(0, 0, 2), + 'velocity_variation': 0.5, + 'gravity_scale': -0.2, + 'emitter_shape': 'circle', + 'emitter_radius': 0.3 + } + + # 合并自定义配置 + effect_config = {**default_config, **config} + + # 创建发射器 + emitter = ParticleEmitter( + "fire_emitter", + "fire", + position, + world, + effect_config + ) + + return emitter.emitter_id + + @staticmethod + def update_effect(emitter: ParticleEmitter, config: Dict): + """ + 更新火焰效果 + + Args: + emitter: 粒子发射器 + config: 更新配置 + """ + # 更新发射器参数 + for key, value in config.items(): + if hasattr(emitter, key): + setattr(emitter, key, value) + + @staticmethod + def get_default_config() -> Dict: + """ + 获取默认配置 + + Returns: + 默认配置字典 + """ + return { + 'particle_type': 'fire', + 'emission_rate': 50, + 'particle_lifetime': 2.0, + 'start_color': Vec3(1.0, 0.3, 0.0), + 'end_color': Vec3(1.0, 0.0, 0.0), + 'start_size': 0.2, + 'end_size': 0.8, + 'velocity': Vec3(0, 0, 2), + 'velocity_variation': 0.5, + 'gravity_scale': -0.2, + 'emitter_shape': 'circle', + 'emitter_radius': 0.3 } \ No newline at end of file diff --git a/plugins/user/particle_system/effects/smoke_effect.py b/plugins/user/particle_system/effects/smoke_effect.py index 128d6ae6..1ee8c321 100644 --- a/plugins/user/particle_system/effects/smoke_effect.py +++ b/plugins/user/particle_system/effects/smoke_effect.py @@ -1,98 +1,98 @@ -""" -烟雾效果 -""" - -from panda3d.core import Vec3, Point3 -from ..core.particle_emitter import ParticleEmitter - -class SmokeEffect: - """ - 烟雾效果 - 模拟烟雾扩散效果 - """ - - @staticmethod - def create_effect(world, position: Point3 = None, config: Dict = None) -> str: - """ - 创建烟雾效果 - - Args: - world: 3D世界对象 - position: 发射器位置 - config: 自定义配置 - - Returns: - 发射器ID - """ - if position is None: - position = Point3(0, 0, 0) - - if config is None: - config = {} - - # 默认配置 - default_config = { - 'particle_type': 'smoke', - 'emission_rate': 30, - 'particle_lifetime': 4.0, - 'start_color': Vec3(0.8, 0.8, 0.8), - 'end_color': Vec3(0.3, 0.3, 0.3), - 'start_size': 0.3, - 'end_size': 1.5, - 'velocity': Vec3(0, 0, 1), - 'velocity_variation': 0.3, - 'gravity_scale': -0.1, - 'emitter_shape': 'circle', - 'emitter_radius': 0.5 - } - - # 合并自定义配置 - effect_config = {**default_config, **config} - - # 创建发射器 - emitter = ParticleEmitter( - "smoke_emitter", - "smoke", - position, - world, - effect_config - ) - - return emitter.emitter_id - - @staticmethod - def update_effect(emitter: ParticleEmitter, config: Dict): - """ - 更新烟雾效果 - - Args: - emitter: 粒子发射器 - config: 更新配置 - """ - # 更新发射器参数 - for key, value in config.items(): - if hasattr(emitter, key): - setattr(emitter, key, value) - - @staticmethod - def get_default_config() -> Dict: - """ - 获取默认配置 - - Returns: - 默认配置字典 - """ - return { - 'particle_type': 'smoke', - 'emission_rate': 30, - 'particle_lifetime': 4.0, - 'start_color': Vec3(0.8, 0.8, 0.8), - 'end_color': Vec3(0.3, 0.3, 0.3), - 'start_size': 0.3, - 'end_size': 1.5, - 'velocity': Vec3(0, 0, 1), - 'velocity_variation': 0.3, - 'gravity_scale': -0.1, - 'emitter_shape': 'circle', - 'emitter_radius': 0.5 +""" +烟雾效果 +""" + +from panda3d.core import Vec3, Point3 +from ..core.particle_emitter import ParticleEmitter + +class SmokeEffect: + """ + 烟雾效果 + 模拟烟雾扩散效果 + """ + + @staticmethod + def create_effect(world, position: Point3 = None, config: Dict = None) -> str: + """ + 创建烟雾效果 + + Args: + world: 3D世界对象 + position: 发射器位置 + config: 自定义配置 + + Returns: + 发射器ID + """ + if position is None: + position = Point3(0, 0, 0) + + if config is None: + config = {} + + # 默认配置 + default_config = { + 'particle_type': 'smoke', + 'emission_rate': 30, + 'particle_lifetime': 4.0, + 'start_color': Vec3(0.8, 0.8, 0.8), + 'end_color': Vec3(0.3, 0.3, 0.3), + 'start_size': 0.3, + 'end_size': 1.5, + 'velocity': Vec3(0, 0, 1), + 'velocity_variation': 0.3, + 'gravity_scale': -0.1, + 'emitter_shape': 'circle', + 'emitter_radius': 0.5 + } + + # 合并自定义配置 + effect_config = {**default_config, **config} + + # 创建发射器 + emitter = ParticleEmitter( + "smoke_emitter", + "smoke", + position, + world, + effect_config + ) + + return emitter.emitter_id + + @staticmethod + def update_effect(emitter: ParticleEmitter, config: Dict): + """ + 更新烟雾效果 + + Args: + emitter: 粒子发射器 + config: 更新配置 + """ + # 更新发射器参数 + for key, value in config.items(): + if hasattr(emitter, key): + setattr(emitter, key, value) + + @staticmethod + def get_default_config() -> Dict: + """ + 获取默认配置 + + Returns: + 默认配置字典 + """ + return { + 'particle_type': 'smoke', + 'emission_rate': 30, + 'particle_lifetime': 4.0, + 'start_color': Vec3(0.8, 0.8, 0.8), + 'end_color': Vec3(0.3, 0.3, 0.3), + 'start_size': 0.3, + 'end_size': 1.5, + 'velocity': Vec3(0, 0, 1), + 'velocity_variation': 0.3, + 'gravity_scale': -0.1, + 'emitter_shape': 'circle', + 'emitter_radius': 0.5 } \ No newline at end of file diff --git a/plugins/user/particle_system/effects/water_effect.py b/plugins/user/particle_system/effects/water_effect.py index baed53ba..62f24c4c 100644 --- a/plugins/user/particle_system/effects/water_effect.py +++ b/plugins/user/particle_system/effects/water_effect.py @@ -1,100 +1,100 @@ -""" -水花效果 -""" - -from panda3d.core import Vec3, Point3 -from ..core.particle_emitter import ParticleEmitter - -class WaterEffect: - """ - 水花效果 - 模拟水花飞溅效果 - """ - - @staticmethod - def create_effect(world, position: Point3 = None, config: Dict = None) -> str: - """ - 创建水花效果 - - Args: - world: 3D世界对象 - position: 发射器位置 - config: 自定义配置 - - Returns: - 发射器ID - """ - if position is None: - position = Point3(0, 0, 0) - - if config is None: - config = {} - - # 默认配置 - default_config = { - 'particle_type': 'water', - 'emission_rate': 80, - 'particle_lifetime': 3.0, - 'start_color': Vec3(0.2, 0.6, 1.0), - 'end_color': Vec3(0.1, 0.3, 0.8), - 'start_size': 0.1, - 'end_size': 0.05, - 'velocity': Vec3(0, 0, 3), - 'velocity_variation': 1.0, - 'gravity_scale': 2.0, - 'bounce_factor': 0.3, - 'emitter_shape': 'circle', - 'emitter_radius': 0.2 - } - - # 合并自定义配置 - effect_config = {**default_config, **config} - - # 创建发射器 - emitter = ParticleEmitter( - "water_emitter", - "water", - position, - world, - effect_config - ) - - return emitter.emitter_id - - @staticmethod - def update_effect(emitter: ParticleEmitter, config: Dict): - """ - 更新水花效果 - - Args: - emitter: 粒子发射器 - config: 更新配置 - """ - # 更新发射器参数 - for key, value in config.items(): - if hasattr(emitter, key): - setattr(emitter, key, value) - - @staticmethod - def get_default_config() -> Dict: - """ - 获取默认配置 - - Returns: - 默认配置字典 - """ - return { - 'particle_type': 'water', - 'emission_rate': 80, - 'particle_lifetime': 3.0, - 'start_color': Vec3(0.2, 0.6, 1.0), - 'end_color': Vec3(0.1, 0.3, 0.8), - 'start_size': 0.1, - 'end_size': 0.05, - 'velocity': Vec3(0, 0, 3), - 'velocity_variation': 1.0, - 'gravity_scale': 2.0, - 'bounce_factor': 0.3, - 'emitter_shape': 'circle', - 'emitter_radius': 0.2 +""" +水花效果 +""" + +from panda3d.core import Vec3, Point3 +from ..core.particle_emitter import ParticleEmitter + +class WaterEffect: + """ + 水花效果 + 模拟水花飞溅效果 + """ + + @staticmethod + def create_effect(world, position: Point3 = None, config: Dict = None) -> str: + """ + 创建水花效果 + + Args: + world: 3D世界对象 + position: 发射器位置 + config: 自定义配置 + + Returns: + 发射器ID + """ + if position is None: + position = Point3(0, 0, 0) + + if config is None: + config = {} + + # 默认配置 + default_config = { + 'particle_type': 'water', + 'emission_rate': 80, + 'particle_lifetime': 3.0, + 'start_color': Vec3(0.2, 0.6, 1.0), + 'end_color': Vec3(0.1, 0.3, 0.8), + 'start_size': 0.1, + 'end_size': 0.05, + 'velocity': Vec3(0, 0, 3), + 'velocity_variation': 1.0, + 'gravity_scale': 2.0, + 'bounce_factor': 0.3, + 'emitter_shape': 'circle', + 'emitter_radius': 0.2 + } + + # 合并自定义配置 + effect_config = {**default_config, **config} + + # 创建发射器 + emitter = ParticleEmitter( + "water_emitter", + "water", + position, + world, + effect_config + ) + + return emitter.emitter_id + + @staticmethod + def update_effect(emitter: ParticleEmitter, config: Dict): + """ + 更新水花效果 + + Args: + emitter: 粒子发射器 + config: 更新配置 + """ + # 更新发射器参数 + for key, value in config.items(): + if hasattr(emitter, key): + setattr(emitter, key, value) + + @staticmethod + def get_default_config() -> Dict: + """ + 获取默认配置 + + Returns: + 默认配置字典 + """ + return { + 'particle_type': 'water', + 'emission_rate': 80, + 'particle_lifetime': 3.0, + 'start_color': Vec3(0.2, 0.6, 1.0), + 'end_color': Vec3(0.1, 0.3, 0.8), + 'start_size': 0.1, + 'end_size': 0.05, + 'velocity': Vec3(0, 0, 3), + 'velocity_variation': 1.0, + 'gravity_scale': 2.0, + 'bounce_factor': 0.3, + 'emitter_shape': 'circle', + 'emitter_radius': 0.2 } \ No newline at end of file diff --git a/plugins/user/particle_system/plugin.py b/plugins/user/particle_system/plugin.py index ff6106c4..2dfcfe8f 100644 --- a/plugins/user/particle_system/plugin.py +++ b/plugins/user/particle_system/plugin.py @@ -1,411 +1,411 @@ -""" -粒子系统插件主文件 -""" - -from plugins.plugin_manager import BasePlugin -from .core.particle_manager import ParticleManager -from .editor.particle_editor import ParticleEditor -from .tools.performance_monitor import PerformanceMonitor -from .editor.preset_manager import PresetManager - -class Plugin(BasePlugin): - """ - 粒子系统插件 - 提供完整的3D粒子特效功能 - """ - - def __init__(self, plugin_manager, name): - super().__init__(plugin_manager, name) - self.config = { - "version": "1.0.0", - "author": "EG Team", - "description": "高性能3D粒子系统插件,支持火焰、烟雾、爆炸等多种特效" - } - - # 核心组件 - self.particle_manager = None - self.particle_editor = None - self.performance_monitor = None - self.preset_manager = None - - # GUI元素 - self.gui_elements = [] - - # 事件处理器 - self.event_handlers = {} - - # 插件状态 - self.is_initialized = False - self.is_enabled = False - - def initialize(self) -> bool: - """ - 初始化插件 - """ - try: - if self.is_initialized: - return True - - print(f"初始化粒子系统插件: {self.name}") - - # 初始化粒子管理器 - self.particle_manager = ParticleManager(self.plugin_manager.world) - - # 初始化性能监控器 - self.performance_monitor = PerformanceMonitor(self.particle_manager) - - # 初始化粒子编辑器 - self.particle_editor = ParticleEditor( - self.plugin_manager.world, - self.particle_manager - ) - - # 初始化预设管理器 - self.preset_manager = PresetManager() - - self.is_initialized = True - print("✓ 粒子系统插件初始化完成") - return True - - except Exception as e: - print(f"✗ 粒子系统插件初始化失败: {e}") - return False - - def enable(self) -> bool: - """ - 启用插件 - """ - if not super().enable(): - return False - - if self.is_enabled: - return True - - try: - print(f"启用粒子系统插件: {self.name}") - - # 启用粒子管理器 - if self.particle_manager: - self.particle_manager.enable() - - # 注册事件处理器 - self._register_event_handlers() - - # 创建GUI界面 - self._create_gui() - - # 启动性能监控 - if self.performance_monitor: - self.performance_monitor.start() - - self.is_enabled = True - print("✓ 粒子系统插件启用成功") - return True - - except Exception as e: - print(f"✗ 粒子系统插件启用失败: {e}") - return False - - def disable(self) -> bool: - """ - 禁用插件 - """ - if not super().disable(): - return False - - if not self.is_enabled: - return True - - try: - print(f"禁用粒子系统插件: {self.name}") - - # 停止性能监控 - if self.performance_monitor: - self.performance_monitor.stop() - - # 清理GUI界面 - self._cleanup_gui() - - # 移除事件处理器 - self._unregister_event_handlers() - - # 禁用粒子管理器 - if self.particle_manager: - self.particle_manager.disable() - - self.is_enabled = False - print("✓ 粒子系统插件禁用成功") - return True - - except Exception as e: - print(f"✗ 粒子系统插件禁用失败: {e}") - return False - - def finalize(self): - """ - 清理插件资源 - """ - print(f"清理粒子系统插件资源: {self.name}") - - # 清理所有组件 - if self.particle_editor: - self.particle_editor.cleanup() - self.particle_editor = None - - if self.performance_monitor: - self.performance_monitor.cleanup() - self.performance_monitor = None - - if self.particle_manager: - self.particle_manager.cleanup() - self.particle_manager = None - - if self.preset_manager: - self.preset_manager = None - - self.is_initialized = False - self.is_enabled = False - - def _register_event_handlers(self): - """ - 注册事件处理器 - """ - world = self.plugin_manager.world - - # 粒子系统控制快捷键 - self.event_handlers['f5'] = self.toggle_particle_editor - self.event_handlers['f6'] = self.toggle_performance_monitor - self.event_handlers['f7'] = self.create_test_particles - self.event_handlers['f8'] = self.clear_all_particles - self.event_handlers['f9'] = self.create_fire_effect - self.event_handlers['f10'] = self.create_smoke_effect - self.event_handlers['f11'] = self.create_explosion_effect - self.event_handlers['f12'] = self.create_water_effect - - # 注册事件 - for event, handler in self.event_handlers.items(): - world.accept(event, handler) - world.accept(event.upper(), handler) # 同时注册大写事件 - - def _unregister_event_handlers(self): - """ - 移除事件处理器 - """ - world = self.plugin_manager.world - - for event in self.event_handlers.keys(): - world.ignore(event) - world.ignore(event.upper()) - - self.event_handlers.clear() - - def _create_gui(self): - """ - 创建GUI界面 - """ - try: - world = self.plugin_manager.world - - # 检查是否有GUI管理器 - if not hasattr(world, 'gui_manager') or not world.gui_manager: - print("⚠ GUI管理器不可用,跳过GUI创建") - return - - gui_manager = world.gui_manager - - # 创建粒子系统控制面板 - panel_button = gui_manager.createGUIButton( - pos=(0.02, 0, 0.95), - text="粒子系统", - size=0.06 - ) - self.gui_elements.append(panel_button) - - # 创建快速效果按钮 - effects = [ - ("火焰 [F9]", self.create_fire_effect), - ("烟雾 [F10]", self.create_smoke_effect), - ("爆炸 [F11]", self.create_explosion_effect), - ("水花 [F12]", self.create_water_effect), - ("测试 [F7]", self.create_test_particles), - ("清除 [F8]", self.clear_all_particles) - ] - - for i, (name, callback) in enumerate(effects): - button = gui_manager.createGUIButton( - pos=(0.02, 0, 0.88 - i * 0.07), - text=name, - size=0.05 - ) - self.gui_elements.append(button) - world.accept(f"particle_effect_{name}", callback) - - # 创建工具按钮 - tools = [ - ("编辑器 [F5]", self.toggle_particle_editor), - ("性能监控 [F6]", self.toggle_performance_monitor) - ] - - for i, (name, callback) in enumerate(tools): - button = gui_manager.createGUIButton( - pos=(0.02, 0, 0.40 - i * 0.07), - text=name, - size=0.05 - ) - self.gui_elements.append(button) - - except Exception as e: - print(f"⚠ GUI创建失败: {e}") - - def _cleanup_gui(self): - """ - 清理GUI界面 - """ - try: - world = self.plugin_manager.world - - if hasattr(world, 'gui_manager') and world.gui_manager: - gui_manager = world.gui_manager - - # 删除所有GUI元素 - for element in self.gui_elements: - gui_manager.deleteGUIElement(element) - - self.gui_elements.clear() - - except Exception as e: - print(f"⚠ GUI清理失败: {e}") - - # 事件处理方法 - def toggle_particle_editor(self): - """切换粒子编辑器显示""" - if self.particle_editor: - self.particle_editor.toggle_visibility() - - def toggle_performance_monitor(self): - """切换性能监控器显示""" - if self.performance_monitor: - self.performance_monitor.toggle_display() - - def create_test_particles(self): - """创建测试粒子""" - if self.particle_manager: - self.particle_manager.create_test_effect() - - def clear_all_particles(self): - """清除所有粒子""" - if self.particle_manager: - self.particle_manager.clear_all_particles() - - def create_fire_effect(self): - """创建火焰效果""" - if self.particle_manager: - self.particle_manager.create_fire_effect() - - def create_smoke_effect(self): - """创建烟雾效果""" - if self.particle_manager: - self.particle_manager.create_smoke_effect() - - def create_explosion_effect(self): - """创建爆炸效果""" - if self.particle_manager: - self.particle_manager.create_explosion_effect() - - def create_water_effect(self): - """创建水花效果""" - if self.particle_manager: - self.particle_manager.create_water_effect() - - def create_magic_effect(self): - """创建魔法效果""" - if self.particle_manager: - self.particle_manager.create_magic_effect() - - def create_snow_effect(self): - """创建雪花效果""" - if self.particle_manager: - self.particle_manager.create_snow_effect() - - def get_info(self) -> dict: - """ - 获取插件信息 - """ - info = super().get_info() - info.update({ - "version": self.config.get("version", "1.0.0"), - "author": self.config.get("author", "EG Team"), - "description": self.config.get("description", "粒子系统插件"), - "particle_count": self.particle_manager.get_particle_count() if self.particle_manager else 0, - "active_emitters": self.particle_manager.get_emitter_count() if self.particle_manager else 0, - "is_enabled": self.is_enabled, - "is_initialized": self.is_initialized - }) - return info - - # 高级功能接口 - def load_preset(self, preset_name: str): - """加载预设效果""" - if self.preset_manager and self.particle_manager: - config = self.preset_manager.get_preset_config(preset_name) - if config: - # 在摄像机前方创建效果 - camera_pos = self.plugin_manager.world.camera.getPos() - camera_forward = self.plugin_manager.world.camera.getNetTransform().getMat().getRow3(1) - position = camera_pos + camera_forward * 5 - - self.particle_manager.create_emitter( - config.get('emitter_type', 'default'), - position, - config - ) - print(f"✓ 加载预设: {preset_name}") - else: - print(f"✗ 预设未找到: {preset_name}") - - def save_current_as_preset(self, preset_name: str, description: str = ""): - """保存当前效果为预设""" - if self.preset_manager and self.particle_editor: - config = self.particle_editor.get_current_config() - if config: - success = self.preset_manager.save_preset( - preset_name, - config, - description - ) - if success: - print(f"✓ 保存预设: {preset_name}") - else: - print(f"✗ 保存预设失败: {preset_name}") - else: - print("✗ 没有可保存的配置") - - def list_presets(self): - """列出所有预设""" - if self.preset_manager: - presets = self.preset_manager.list_presets() - print("可用预设:") - for preset in presets: - print(f" - {preset}") - return presets - return [] - - def set_global_time_scale(self, scale: float): - """设置全局时间缩放""" - if self.particle_manager: - self.particle_manager.set_global_time_scale(scale) - - def set_max_particles(self, max_count: int): - """设置最大粒子数""" - if self.particle_manager: - self.particle_manager.set_max_particles(max_count) - - def set_emit_rate_multiplier(self, multiplier: float): - """设置发射速率倍增器""" - if self.particle_manager: - self.particle_manager.set_emit_rate_multiplier(multiplier) - - def set_particle_size_multiplier(self, multiplier: float): - """设置粒子尺寸倍增器""" - if self.particle_manager: +""" +粒子系统插件主文件 +""" + +from plugins.plugin_manager import BasePlugin +from .core.particle_manager import ParticleManager +from .editor.particle_editor import ParticleEditor +from .tools.performance_monitor import PerformanceMonitor +from .editor.preset_manager import PresetManager + +class Plugin(BasePlugin): + """ + 粒子系统插件 + 提供完整的3D粒子特效功能 + """ + + def __init__(self, plugin_manager, name): + super().__init__(plugin_manager, name) + self.config = { + "version": "1.0.0", + "author": "EG Team", + "description": "高性能3D粒子系统插件,支持火焰、烟雾、爆炸等多种特效" + } + + # 核心组件 + self.particle_manager = None + self.particle_editor = None + self.performance_monitor = None + self.preset_manager = None + + # GUI元素 + self.gui_elements = [] + + # 事件处理器 + self.event_handlers = {} + + # 插件状态 + self.is_initialized = False + self.is_enabled = False + + def initialize(self) -> bool: + """ + 初始化插件 + """ + try: + if self.is_initialized: + return True + + print(f"初始化粒子系统插件: {self.name}") + + # 初始化粒子管理器 + self.particle_manager = ParticleManager(self.plugin_manager.world) + + # 初始化性能监控器 + self.performance_monitor = PerformanceMonitor(self.particle_manager) + + # 初始化粒子编辑器 + self.particle_editor = ParticleEditor( + self.plugin_manager.world, + self.particle_manager + ) + + # 初始化预设管理器 + self.preset_manager = PresetManager() + + self.is_initialized = True + print("✓ 粒子系统插件初始化完成") + return True + + except Exception as e: + print(f"✗ 粒子系统插件初始化失败: {e}") + return False + + def enable(self) -> bool: + """ + 启用插件 + """ + if not super().enable(): + return False + + if self.is_enabled: + return True + + try: + print(f"启用粒子系统插件: {self.name}") + + # 启用粒子管理器 + if self.particle_manager: + self.particle_manager.enable() + + # 注册事件处理器 + self._register_event_handlers() + + # 创建GUI界面 + self._create_gui() + + # 启动性能监控 + if self.performance_monitor: + self.performance_monitor.start() + + self.is_enabled = True + print("✓ 粒子系统插件启用成功") + return True + + except Exception as e: + print(f"✗ 粒子系统插件启用失败: {e}") + return False + + def disable(self) -> bool: + """ + 禁用插件 + """ + if not super().disable(): + return False + + if not self.is_enabled: + return True + + try: + print(f"禁用粒子系统插件: {self.name}") + + # 停止性能监控 + if self.performance_monitor: + self.performance_monitor.stop() + + # 清理GUI界面 + self._cleanup_gui() + + # 移除事件处理器 + self._unregister_event_handlers() + + # 禁用粒子管理器 + if self.particle_manager: + self.particle_manager.disable() + + self.is_enabled = False + print("✓ 粒子系统插件禁用成功") + return True + + except Exception as e: + print(f"✗ 粒子系统插件禁用失败: {e}") + return False + + def finalize(self): + """ + 清理插件资源 + """ + print(f"清理粒子系统插件资源: {self.name}") + + # 清理所有组件 + if self.particle_editor: + self.particle_editor.cleanup() + self.particle_editor = None + + if self.performance_monitor: + self.performance_monitor.cleanup() + self.performance_monitor = None + + if self.particle_manager: + self.particle_manager.cleanup() + self.particle_manager = None + + if self.preset_manager: + self.preset_manager = None + + self.is_initialized = False + self.is_enabled = False + + def _register_event_handlers(self): + """ + 注册事件处理器 + """ + world = self.plugin_manager.world + + # 粒子系统控制快捷键 + self.event_handlers['f5'] = self.toggle_particle_editor + self.event_handlers['f6'] = self.toggle_performance_monitor + self.event_handlers['f7'] = self.create_test_particles + self.event_handlers['f8'] = self.clear_all_particles + self.event_handlers['f9'] = self.create_fire_effect + self.event_handlers['f10'] = self.create_smoke_effect + self.event_handlers['f11'] = self.create_explosion_effect + self.event_handlers['f12'] = self.create_water_effect + + # 注册事件 + for event, handler in self.event_handlers.items(): + world.accept(event, handler) + world.accept(event.upper(), handler) # 同时注册大写事件 + + def _unregister_event_handlers(self): + """ + 移除事件处理器 + """ + world = self.plugin_manager.world + + for event in self.event_handlers.keys(): + world.ignore(event) + world.ignore(event.upper()) + + self.event_handlers.clear() + + def _create_gui(self): + """ + 创建GUI界面 + """ + try: + world = self.plugin_manager.world + + # 检查是否有GUI管理器 + if not hasattr(world, 'gui_manager') or not world.gui_manager: + print("⚠ GUI管理器不可用,跳过GUI创建") + return + + gui_manager = world.gui_manager + + # 创建粒子系统控制面板 + panel_button = gui_manager.createGUIButton( + pos=(0.02, 0, 0.95), + text="粒子系统", + size=0.06 + ) + self.gui_elements.append(panel_button) + + # 创建快速效果按钮 + effects = [ + ("火焰 [F9]", self.create_fire_effect), + ("烟雾 [F10]", self.create_smoke_effect), + ("爆炸 [F11]", self.create_explosion_effect), + ("水花 [F12]", self.create_water_effect), + ("测试 [F7]", self.create_test_particles), + ("清除 [F8]", self.clear_all_particles) + ] + + for i, (name, callback) in enumerate(effects): + button = gui_manager.createGUIButton( + pos=(0.02, 0, 0.88 - i * 0.07), + text=name, + size=0.05 + ) + self.gui_elements.append(button) + world.accept(f"particle_effect_{name}", callback) + + # 创建工具按钮 + tools = [ + ("编辑器 [F5]", self.toggle_particle_editor), + ("性能监控 [F6]", self.toggle_performance_monitor) + ] + + for i, (name, callback) in enumerate(tools): + button = gui_manager.createGUIButton( + pos=(0.02, 0, 0.40 - i * 0.07), + text=name, + size=0.05 + ) + self.gui_elements.append(button) + + except Exception as e: + print(f"⚠ GUI创建失败: {e}") + + def _cleanup_gui(self): + """ + 清理GUI界面 + """ + try: + world = self.plugin_manager.world + + if hasattr(world, 'gui_manager') and world.gui_manager: + gui_manager = world.gui_manager + + # 删除所有GUI元素 + for element in self.gui_elements: + gui_manager.deleteGUIElement(element) + + self.gui_elements.clear() + + except Exception as e: + print(f"⚠ GUI清理失败: {e}") + + # 事件处理方法 + def toggle_particle_editor(self): + """切换粒子编辑器显示""" + if self.particle_editor: + self.particle_editor.toggle_visibility() + + def toggle_performance_monitor(self): + """切换性能监控器显示""" + if self.performance_monitor: + self.performance_monitor.toggle_display() + + def create_test_particles(self): + """创建测试粒子""" + if self.particle_manager: + self.particle_manager.create_test_effect() + + def clear_all_particles(self): + """清除所有粒子""" + if self.particle_manager: + self.particle_manager.clear_all_particles() + + def create_fire_effect(self): + """创建火焰效果""" + if self.particle_manager: + self.particle_manager.create_fire_effect() + + def create_smoke_effect(self): + """创建烟雾效果""" + if self.particle_manager: + self.particle_manager.create_smoke_effect() + + def create_explosion_effect(self): + """创建爆炸效果""" + if self.particle_manager: + self.particle_manager.create_explosion_effect() + + def create_water_effect(self): + """创建水花效果""" + if self.particle_manager: + self.particle_manager.create_water_effect() + + def create_magic_effect(self): + """创建魔法效果""" + if self.particle_manager: + self.particle_manager.create_magic_effect() + + def create_snow_effect(self): + """创建雪花效果""" + if self.particle_manager: + self.particle_manager.create_snow_effect() + + def get_info(self) -> dict: + """ + 获取插件信息 + """ + info = super().get_info() + info.update({ + "version": self.config.get("version", "1.0.0"), + "author": self.config.get("author", "EG Team"), + "description": self.config.get("description", "粒子系统插件"), + "particle_count": self.particle_manager.get_particle_count() if self.particle_manager else 0, + "active_emitters": self.particle_manager.get_emitter_count() if self.particle_manager else 0, + "is_enabled": self.is_enabled, + "is_initialized": self.is_initialized + }) + return info + + # 高级功能接口 + def load_preset(self, preset_name: str): + """加载预设效果""" + if self.preset_manager and self.particle_manager: + config = self.preset_manager.get_preset_config(preset_name) + if config: + # 在摄像机前方创建效果 + camera_pos = self.plugin_manager.world.camera.getPos() + camera_forward = self.plugin_manager.world.camera.getNetTransform().getMat().getRow3(1) + position = camera_pos + camera_forward * 5 + + self.particle_manager.create_emitter( + config.get('emitter_type', 'default'), + position, + config + ) + print(f"✓ 加载预设: {preset_name}") + else: + print(f"✗ 预设未找到: {preset_name}") + + def save_current_as_preset(self, preset_name: str, description: str = ""): + """保存当前效果为预设""" + if self.preset_manager and self.particle_editor: + config = self.particle_editor.get_current_config() + if config: + success = self.preset_manager.save_preset( + preset_name, + config, + description + ) + if success: + print(f"✓ 保存预设: {preset_name}") + else: + print(f"✗ 保存预设失败: {preset_name}") + else: + print("✗ 没有可保存的配置") + + def list_presets(self): + """列出所有预设""" + if self.preset_manager: + presets = self.preset_manager.list_presets() + print("可用预设:") + for preset in presets: + print(f" - {preset}") + return presets + return [] + + def set_global_time_scale(self, scale: float): + """设置全局时间缩放""" + if self.particle_manager: + self.particle_manager.set_global_time_scale(scale) + + def set_max_particles(self, max_count: int): + """设置最大粒子数""" + if self.particle_manager: + self.particle_manager.set_max_particles(max_count) + + def set_emit_rate_multiplier(self, multiplier: float): + """设置发射速率倍增器""" + if self.particle_manager: + self.particle_manager.set_emit_rate_multiplier(multiplier) + + def set_particle_size_multiplier(self, multiplier: float): + """设置粒子尺寸倍增器""" + if self.particle_manager: self.particle_manager.set_particle_size_multiplier(multiplier) \ No newline at end of file diff --git a/plugins/user/particle_system/tools/__init__.py b/plugins/user/particle_system/tools/__init__.py index 7fc71f28..62ac75c4 100644 --- a/plugins/user/particle_system/tools/__init__.py +++ b/plugins/user/particle_system/tools/__init__.py @@ -1,11 +1,11 @@ -""" -粒子系统工具模块 -""" - -from .performance_monitor import PerformanceMonitor -from .particle_utils import ParticleUtils - -__all__ = [ - 'PerformanceMonitor', - 'ParticleUtils' +""" +粒子系统工具模块 +""" + +from .performance_monitor import PerformanceMonitor +from .particle_utils import ParticleUtils + +__all__ = [ + 'PerformanceMonitor', + 'ParticleUtils' ] \ No newline at end of file diff --git a/plugins/user/particle_system/tools/particle_utils.py b/plugins/user/particle_system/tools/particle_utils.py index b184cab3..751310a4 100644 --- a/plugins/user/particle_system/tools/particle_utils.py +++ b/plugins/user/particle_system/tools/particle_utils.py @@ -1,459 +1,459 @@ -""" -粒子工具函数 -提供粒子系统相关的实用工具函数 -""" - -import math -import random -from typing import List, Tuple, Optional -from panda3d.core import Vec3, Point3, LVector3f, Quat - -class ParticleUtils: - """ - 粒子系统工具类 - 提供各种实用的粒子计算和生成函数 - """ - - @staticmethod - def lerp(a: float, b: float, t: float) -> float: - """ - 线性插值 - - Args: - a: 起始值 - b: 结束值 - t: 插值参数 (0-1) - - Returns: - 插值结果 - """ - return a + (b - a) * t - - @staticmethod - def lerp_vec3(a: Vec3, b: Vec3, t: float) -> Vec3: - """ - Vec3线性插值 - - Args: - a: 起始向量 - b: 结束向量 - t: 插值参数 (0-1) - - Returns: - 插值结果向量 - """ - return Vec3( - ParticleUtils.lerp(a.x, b.x, t), - ParticleUtils.lerp(a.y, b.y, t), - ParticleUtils.lerp(a.z, b.z, t) - ) - - @staticmethod - def smooth_step(t: float) -> float: - """ - 平滑步进函数 - - Args: - t: 输入值 (0-1) - - Returns: - 平滑后的值 - """ - t = max(0.0, min(1.0, t)) - return t * t * (3.0 - 2.0 * t) - - @staticmethod - def ease_in_out(t: float) -> float: - """ - 缓入缓出函数 - - Args: - t: 输入值 (0-1) - - Returns: - 缓动后的值 - """ - if t < 0.5: - return 2.0 * t * t - else: - return -1.0 + (4.0 - 2.0 * t) * t - - @staticmethod - def random_in_sphere(radius: float = 1.0) -> Vec3: - """ - 在球体内生成随机点 - - Args: - radius: 球体半径 - - Returns: - 随机点坐标 - """ - # 使用拒绝采样法 - while True: - x = random.uniform(-1, 1) - y = random.uniform(-1, 1) - z = random.uniform(-1, 1) - - if x*x + y*y + z*z <= 1.0: - return Vec3(x, y, z) * radius - - @staticmethod - def random_on_sphere(radius: float = 1.0) -> Vec3: - """ - 在球面上生成随机点 - - Args: - radius: 球体半径 - - Returns: - 随机点坐标 - """ - # 使用正态分布方法 - x = random.gauss(0, 1) - y = random.gauss(0, 1) - z = random.gauss(0, 1) - - length = math.sqrt(x*x + y*y + z*z) - if length > 0: - return Vec3(x/length, y/length, z/length) * radius - else: - return Vec3(0, 0, radius) - - @staticmethod - def random_in_circle(radius: float = 1.0) -> Vec3: - """ - 在圆形内生成随机点 (Z=0) - - Args: - radius: 圆形半径 - - Returns: - 随机点坐标 - """ - angle = random.uniform(0, 2 * math.pi) - r = random.uniform(0, radius) - return Vec3(r * math.cos(angle), r * math.sin(angle), 0) - - @staticmethod - def random_on_circle(radius: float = 1.0) -> Vec3: - """ - 在圆周上生成随机点 (Z=0) - - Args: - radius: 圆形半径 - - Returns: - 随机点坐标 - """ - angle = random.uniform(0, 2 * math.pi) - return Vec3(radius * math.cos(angle), radius * math.sin(angle), 0) - - @staticmethod - def random_in_box(dimensions: Vec3) -> Vec3: - """ - 在盒子内生成随机点 - - Args: - dimensions: 盒子尺寸 - - Returns: - 随机点坐标 - """ - return Vec3( - random.uniform(-dimensions.x/2, dimensions.x/2), - random.uniform(-dimensions.y/2, dimensions.y/2), - random.uniform(-dimensions.z/2, dimensions.z/2) - ) - - @staticmethod - def random_cone_direction(direction: Vec3, cone_angle: float) -> Vec3: - """ - 在锥形范围内生成随机方向 - - Args: - direction: 锥形中心方向 - cone_angle: 锥角 (弧度) - - Returns: - 随机方向向量 - """ - # 生成锥形内的随机方向 - phi = random.uniform(0, 2 * math.pi) - cos_theta = random.uniform(math.cos(cone_angle), 1.0) - sin_theta = math.sqrt(1.0 - cos_theta * cos_theta) - - # 局部坐标系中的方向 - local_dir = Vec3( - sin_theta * math.cos(phi), - sin_theta * math.sin(phi), - cos_theta - ) - - # 转换到世界坐标系 - # 简化实现:假设direction是(0,0,1) - if abs(direction.z - 1.0) < 0.001: - return local_dir - elif abs(direction.z + 1.0) < 0.001: - return Vec3(local_dir.x, -local_dir.y, -local_dir.z) - else: - # 需要完整的旋转变换 - # 这里使用简化版本 - return local_dir - - @staticmethod - def calculate_billboard_rotation(position: Point3, camera_pos: Point3) -> Quat: - """ - 计算广告牌旋转 - - Args: - position: 广告牌位置 - camera_pos: 摄像机位置 - - Returns: - 旋转四元数 - """ - to_camera = camera_pos - position - to_camera.normalize() - - # 计算旋转四元数 - up = Vec3(0, 0, 1) - right = to_camera.cross(up) - - if right.lengthSquared() < 0.001: - # 摄像机在正上方或正下方 - right = Vec3(1, 0, 0) - - right.normalize() - up = right.cross(to_camera) - up.normalize() - - # 构建旋转矩阵并转换为四元数 - # 简化实现 - return Quat() - - @staticmethod - def apply_noise(value: float, noise_strength: float, time: float = 0.0) -> float: - """ - 应用噪声到数值 - - Args: - value: 原始值 - noise_strength: 噪声强度 - time: 时间参数 - - Returns: - 添加噪声后的值 - """ - # 简单的伪随机噪声 - noise = math.sin(value * 12.9898 + time * 78.233) * 43758.5453 - noise = noise - math.floor(noise) # 取小数部分 - noise = (noise - 0.5) * 2.0 # 转换到 -1 到 1 - - return value + noise * noise_strength - - @staticmethod - def calculate_drag_force(velocity: Vec3, drag_coefficient: float, - air_density: float = 1.225) -> Vec3: - """ - 计算阻力 - - Args: - velocity: 速度向量 - drag_coefficient: 阻力系数 - air_density: 空气密度 - - Returns: - 阻力向量 - """ - speed_squared = velocity.lengthSquared() - if speed_squared < 0.001: - return Vec3(0, 0, 0) - - speed = math.sqrt(speed_squared) - drag_magnitude = 0.5 * air_density * drag_coefficient * speed_squared - - # 阻力方向与速度相反 - drag_direction = -velocity / speed - - return drag_direction * drag_magnitude - - @staticmethod - def calculate_buoyancy_force(volume: float, fluid_density: float = 1000.0, - gravity: float = 9.81) -> float: - """ - 计算浮力 - - Args: - volume: 物体体积 - fluid_density: 流体密度 - gravity: 重力加速度 - - Returns: - 浮力大小 - """ - return volume * fluid_density * gravity - - @staticmethod - def distance_squared(a: Point3, b: Point3) -> float: - """ - 计算两点间距离的平方 - - Args: - a: 点A - b: 点B - - Returns: - 距离的平方 - """ - dx = a.x - b.x - dy = a.y - b.y - dz = a.z - b.z - return dx*dx + dy*dy + dz*dz - - @staticmethod - def clamp(value: float, min_val: float, max_val: float) -> float: - """ - 限制数值范围 - - Args: - value: 输入值 - min_val: 最小值 - max_val: 最大值 - - Returns: - 限制后的值 - """ - return max(min_val, min(max_val, value)) - - @staticmethod - def wrap_angle(angle: float) -> float: - """ - 将角度限制在 -π 到 π 范围内 - - Args: - angle: 输入角度 (弧度) - - Returns: - 限制后的角度 - """ - while angle > math.pi: - angle -= 2 * math.pi - while angle < -math.pi: - angle += 2 * math.pi - return angle - - @staticmethod - def create_color_gradient(colors: List[Vec3], positions: List[float], t: float) -> Vec3: - """ - 创建颜色渐变 - - Args: - colors: 颜色列表 - positions: 位置列表 (0-1) - t: 插值参数 (0-1) - - Returns: - 插值后的颜色 - """ - if not colors or not positions or len(colors) != len(positions): - return Vec3(1, 1, 1) - - if len(colors) == 1: - return colors[0] - - # 找到插值区间 - for i in range(len(positions) - 1): - if t <= positions[i + 1]: - # 在区间 [i, i+1] 内插值 - local_t = (t - positions[i]) / (positions[i + 1] - positions[i]) - return ParticleUtils.lerp_vec3(colors[i], colors[i + 1], local_t) - - # 超出范围,返回最后一个颜色 - return colors[-1] - - @staticmethod - def create_size_curve(sizes: List[float], positions: List[float], t: float) -> float: - """ - 创建尺寸曲线 - - Args: - sizes: 尺寸列表 - positions: 位置列表 (0-1) - t: 插值参数 (0-1) - - Returns: - 插值后的尺寸 - """ - if not sizes or not positions or len(sizes) != len(positions): - return 1.0 - - if len(sizes) == 1: - return sizes[0] - - # 找到插值区间 - for i in range(len(positions) - 1): - if t <= positions[i + 1]: - # 在区间 [i, i+1] 内插值 - local_t = (t - positions[i]) / (positions[i + 1] - positions[i]) - return ParticleUtils.lerp(sizes[i], sizes[i + 1], local_t) - - # 超出范围,返回最后一个尺寸 - return sizes[-1] - - @staticmethod - def generate_spiral_positions(count: int, radius: float, height: float, - turns: float = 2.0) -> List[Point3]: - """ - 生成螺旋形位置 - - Args: - count: 点的数量 - radius: 螺旋半径 - height: 螺旋高度 - turns: 螺旋圈数 - - Returns: - 位置列表 - """ - positions = [] - - for i in range(count): - t = i / max(1, count - 1) - angle = t * turns * 2 * math.pi - z = t * height - - x = radius * math.cos(angle) - y = radius * math.sin(angle) - - positions.append(Point3(x, y, z)) - - return positions - - @staticmethod - def calculate_particle_bounds(positions: List[Point3]) -> Tuple[Point3, Point3]: - """ - 计算粒子边界框 - - Args: - positions: 粒子位置列表 - - Returns: - (最小点, 最大点) - """ - if not positions: - return Point3(0, 0, 0), Point3(0, 0, 0) - - min_pos = Point3(positions[0]) - max_pos = Point3(positions[0]) - - for pos in positions[1:]: - min_pos.x = min(min_pos.x, pos.x) - min_pos.y = min(min_pos.y, pos.y) - min_pos.z = min(min_pos.z, pos.z) - - max_pos.x = max(max_pos.x, pos.x) - max_pos.y = max(max_pos.y, pos.y) - max_pos.z = max(max_pos.z, pos.z) - +""" +粒子工具函数 +提供粒子系统相关的实用工具函数 +""" + +import math +import random +from typing import List, Tuple, Optional +from panda3d.core import Vec3, Point3, LVector3f, Quat + +class ParticleUtils: + """ + 粒子系统工具类 + 提供各种实用的粒子计算和生成函数 + """ + + @staticmethod + def lerp(a: float, b: float, t: float) -> float: + """ + 线性插值 + + Args: + a: 起始值 + b: 结束值 + t: 插值参数 (0-1) + + Returns: + 插值结果 + """ + return a + (b - a) * t + + @staticmethod + def lerp_vec3(a: Vec3, b: Vec3, t: float) -> Vec3: + """ + Vec3线性插值 + + Args: + a: 起始向量 + b: 结束向量 + t: 插值参数 (0-1) + + Returns: + 插值结果向量 + """ + return Vec3( + ParticleUtils.lerp(a.x, b.x, t), + ParticleUtils.lerp(a.y, b.y, t), + ParticleUtils.lerp(a.z, b.z, t) + ) + + @staticmethod + def smooth_step(t: float) -> float: + """ + 平滑步进函数 + + Args: + t: 输入值 (0-1) + + Returns: + 平滑后的值 + """ + t = max(0.0, min(1.0, t)) + return t * t * (3.0 - 2.0 * t) + + @staticmethod + def ease_in_out(t: float) -> float: + """ + 缓入缓出函数 + + Args: + t: 输入值 (0-1) + + Returns: + 缓动后的值 + """ + if t < 0.5: + return 2.0 * t * t + else: + return -1.0 + (4.0 - 2.0 * t) * t + + @staticmethod + def random_in_sphere(radius: float = 1.0) -> Vec3: + """ + 在球体内生成随机点 + + Args: + radius: 球体半径 + + Returns: + 随机点坐标 + """ + # 使用拒绝采样法 + while True: + x = random.uniform(-1, 1) + y = random.uniform(-1, 1) + z = random.uniform(-1, 1) + + if x*x + y*y + z*z <= 1.0: + return Vec3(x, y, z) * radius + + @staticmethod + def random_on_sphere(radius: float = 1.0) -> Vec3: + """ + 在球面上生成随机点 + + Args: + radius: 球体半径 + + Returns: + 随机点坐标 + """ + # 使用正态分布方法 + x = random.gauss(0, 1) + y = random.gauss(0, 1) + z = random.gauss(0, 1) + + length = math.sqrt(x*x + y*y + z*z) + if length > 0: + return Vec3(x/length, y/length, z/length) * radius + else: + return Vec3(0, 0, radius) + + @staticmethod + def random_in_circle(radius: float = 1.0) -> Vec3: + """ + 在圆形内生成随机点 (Z=0) + + Args: + radius: 圆形半径 + + Returns: + 随机点坐标 + """ + angle = random.uniform(0, 2 * math.pi) + r = random.uniform(0, radius) + return Vec3(r * math.cos(angle), r * math.sin(angle), 0) + + @staticmethod + def random_on_circle(radius: float = 1.0) -> Vec3: + """ + 在圆周上生成随机点 (Z=0) + + Args: + radius: 圆形半径 + + Returns: + 随机点坐标 + """ + angle = random.uniform(0, 2 * math.pi) + return Vec3(radius * math.cos(angle), radius * math.sin(angle), 0) + + @staticmethod + def random_in_box(dimensions: Vec3) -> Vec3: + """ + 在盒子内生成随机点 + + Args: + dimensions: 盒子尺寸 + + Returns: + 随机点坐标 + """ + return Vec3( + random.uniform(-dimensions.x/2, dimensions.x/2), + random.uniform(-dimensions.y/2, dimensions.y/2), + random.uniform(-dimensions.z/2, dimensions.z/2) + ) + + @staticmethod + def random_cone_direction(direction: Vec3, cone_angle: float) -> Vec3: + """ + 在锥形范围内生成随机方向 + + Args: + direction: 锥形中心方向 + cone_angle: 锥角 (弧度) + + Returns: + 随机方向向量 + """ + # 生成锥形内的随机方向 + phi = random.uniform(0, 2 * math.pi) + cos_theta = random.uniform(math.cos(cone_angle), 1.0) + sin_theta = math.sqrt(1.0 - cos_theta * cos_theta) + + # 局部坐标系中的方向 + local_dir = Vec3( + sin_theta * math.cos(phi), + sin_theta * math.sin(phi), + cos_theta + ) + + # 转换到世界坐标系 + # 简化实现:假设direction是(0,0,1) + if abs(direction.z - 1.0) < 0.001: + return local_dir + elif abs(direction.z + 1.0) < 0.001: + return Vec3(local_dir.x, -local_dir.y, -local_dir.z) + else: + # 需要完整的旋转变换 + # 这里使用简化版本 + return local_dir + + @staticmethod + def calculate_billboard_rotation(position: Point3, camera_pos: Point3) -> Quat: + """ + 计算广告牌旋转 + + Args: + position: 广告牌位置 + camera_pos: 摄像机位置 + + Returns: + 旋转四元数 + """ + to_camera = camera_pos - position + to_camera.normalize() + + # 计算旋转四元数 + up = Vec3(0, 0, 1) + right = to_camera.cross(up) + + if right.lengthSquared() < 0.001: + # 摄像机在正上方或正下方 + right = Vec3(1, 0, 0) + + right.normalize() + up = right.cross(to_camera) + up.normalize() + + # 构建旋转矩阵并转换为四元数 + # 简化实现 + return Quat() + + @staticmethod + def apply_noise(value: float, noise_strength: float, time: float = 0.0) -> float: + """ + 应用噪声到数值 + + Args: + value: 原始值 + noise_strength: 噪声强度 + time: 时间参数 + + Returns: + 添加噪声后的值 + """ + # 简单的伪随机噪声 + noise = math.sin(value * 12.9898 + time * 78.233) * 43758.5453 + noise = noise - math.floor(noise) # 取小数部分 + noise = (noise - 0.5) * 2.0 # 转换到 -1 到 1 + + return value + noise * noise_strength + + @staticmethod + def calculate_drag_force(velocity: Vec3, drag_coefficient: float, + air_density: float = 1.225) -> Vec3: + """ + 计算阻力 + + Args: + velocity: 速度向量 + drag_coefficient: 阻力系数 + air_density: 空气密度 + + Returns: + 阻力向量 + """ + speed_squared = velocity.lengthSquared() + if speed_squared < 0.001: + return Vec3(0, 0, 0) + + speed = math.sqrt(speed_squared) + drag_magnitude = 0.5 * air_density * drag_coefficient * speed_squared + + # 阻力方向与速度相反 + drag_direction = -velocity / speed + + return drag_direction * drag_magnitude + + @staticmethod + def calculate_buoyancy_force(volume: float, fluid_density: float = 1000.0, + gravity: float = 9.81) -> float: + """ + 计算浮力 + + Args: + volume: 物体体积 + fluid_density: 流体密度 + gravity: 重力加速度 + + Returns: + 浮力大小 + """ + return volume * fluid_density * gravity + + @staticmethod + def distance_squared(a: Point3, b: Point3) -> float: + """ + 计算两点间距离的平方 + + Args: + a: 点A + b: 点B + + Returns: + 距离的平方 + """ + dx = a.x - b.x + dy = a.y - b.y + dz = a.z - b.z + return dx*dx + dy*dy + dz*dz + + @staticmethod + def clamp(value: float, min_val: float, max_val: float) -> float: + """ + 限制数值范围 + + Args: + value: 输入值 + min_val: 最小值 + max_val: 最大值 + + Returns: + 限制后的值 + """ + return max(min_val, min(max_val, value)) + + @staticmethod + def wrap_angle(angle: float) -> float: + """ + 将角度限制在 -π 到 π 范围内 + + Args: + angle: 输入角度 (弧度) + + Returns: + 限制后的角度 + """ + while angle > math.pi: + angle -= 2 * math.pi + while angle < -math.pi: + angle += 2 * math.pi + return angle + + @staticmethod + def create_color_gradient(colors: List[Vec3], positions: List[float], t: float) -> Vec3: + """ + 创建颜色渐变 + + Args: + colors: 颜色列表 + positions: 位置列表 (0-1) + t: 插值参数 (0-1) + + Returns: + 插值后的颜色 + """ + if not colors or not positions or len(colors) != len(positions): + return Vec3(1, 1, 1) + + if len(colors) == 1: + return colors[0] + + # 找到插值区间 + for i in range(len(positions) - 1): + if t <= positions[i + 1]: + # 在区间 [i, i+1] 内插值 + local_t = (t - positions[i]) / (positions[i + 1] - positions[i]) + return ParticleUtils.lerp_vec3(colors[i], colors[i + 1], local_t) + + # 超出范围,返回最后一个颜色 + return colors[-1] + + @staticmethod + def create_size_curve(sizes: List[float], positions: List[float], t: float) -> float: + """ + 创建尺寸曲线 + + Args: + sizes: 尺寸列表 + positions: 位置列表 (0-1) + t: 插值参数 (0-1) + + Returns: + 插值后的尺寸 + """ + if not sizes or not positions or len(sizes) != len(positions): + return 1.0 + + if len(sizes) == 1: + return sizes[0] + + # 找到插值区间 + for i in range(len(positions) - 1): + if t <= positions[i + 1]: + # 在区间 [i, i+1] 内插值 + local_t = (t - positions[i]) / (positions[i + 1] - positions[i]) + return ParticleUtils.lerp(sizes[i], sizes[i + 1], local_t) + + # 超出范围,返回最后一个尺寸 + return sizes[-1] + + @staticmethod + def generate_spiral_positions(count: int, radius: float, height: float, + turns: float = 2.0) -> List[Point3]: + """ + 生成螺旋形位置 + + Args: + count: 点的数量 + radius: 螺旋半径 + height: 螺旋高度 + turns: 螺旋圈数 + + Returns: + 位置列表 + """ + positions = [] + + for i in range(count): + t = i / max(1, count - 1) + angle = t * turns * 2 * math.pi + z = t * height + + x = radius * math.cos(angle) + y = radius * math.sin(angle) + + positions.append(Point3(x, y, z)) + + return positions + + @staticmethod + def calculate_particle_bounds(positions: List[Point3]) -> Tuple[Point3, Point3]: + """ + 计算粒子边界框 + + Args: + positions: 粒子位置列表 + + Returns: + (最小点, 最大点) + """ + if not positions: + return Point3(0, 0, 0), Point3(0, 0, 0) + + min_pos = Point3(positions[0]) + max_pos = Point3(positions[0]) + + for pos in positions[1:]: + min_pos.x = min(min_pos.x, pos.x) + min_pos.y = min(min_pos.y, pos.y) + min_pos.z = min(min_pos.z, pos.z) + + max_pos.x = max(max_pos.x, pos.x) + max_pos.y = max(max_pos.y, pos.y) + max_pos.z = max(max_pos.z, pos.z) + return min_pos, max_pos \ No newline at end of file diff --git a/plugins/user/particle_system/tools/performance_monitor.py b/plugins/user/particle_system/tools/performance_monitor.py index c6191c4d..33530139 100644 --- a/plugins/user/particle_system/tools/performance_monitor.py +++ b/plugins/user/particle_system/tools/performance_monitor.py @@ -1,484 +1,484 @@ -""" -性能监控器 -监控粒子系统的性能指标 -""" - -import time -from typing import Dict, List, Optional -from collections import deque -from direct.task import Task - -class PerformanceMonitor: - """ - 粒子系统性能监控器 - 实时监控和显示性能指标 - """ - - def __init__(self, particle_manager): - """ - 初始化性能监控器 - - Args: - particle_manager: 粒子管理器 - """ - self.particle_manager = particle_manager - self.enabled = False - self.display_visible = False - - # 性能数据收集 - self.frame_times = deque(maxlen=60) # 保存60帧的数据 - self.particle_counts = deque(maxlen=60) - self.render_times = deque(maxlen=60) - self.physics_times = deque(maxlen=60) - self.update_times = deque(maxlen=60) - - # 统计数据 - self.stats = { - 'fps': 0.0, - 'avg_fps': 0.0, - 'min_fps': 0.0, - 'max_fps': 0.0, - 'particle_count': 0, - 'avg_particle_count': 0.0, - 'max_particle_count': 0, - 'render_time': 0.0, - 'physics_time': 0.0, - 'update_time': 0.0, - 'memory_usage': 0.0, - 'draw_calls': 0, - 'vertices_rendered': 0, - 'active_emitters': 0 - } - - # 性能阈值 - self.performance_thresholds = { - 'low_fps_warning': 30.0, - 'high_particle_warning': 5000, - 'high_render_time_warning': 16.0, # ms - 'high_update_time_warning': 5.0 # ms - } - - # 警告状态 - self.warnings = { - 'low_fps': False, - 'high_particles': False, - 'high_render_time': False, - 'high_update_time': False - } - - # GUI元素 - self.gui_elements = [] - self.info_labels = {} - - # 监控任务 - self.monitor_task = None - - # 时间记录 - self.last_update_time = time.time() - self.update_interval = 0.1 # 100ms更新一次 - - # 历史数据(用于图表显示) - self.history_data = { - 'fps_history': deque(maxlen=100), - 'particle_count_history': deque(maxlen=100), - 'render_time_history': deque(maxlen=100) - } - - print("✓ 性能监控器初始化完成") - - def start(self): - """启动性能监控""" - if self.enabled: - return - - self.enabled = True - - # 启动监控任务 - if hasattr(self.particle_manager, 'world') and self.particle_manager.world: - self.monitor_task = self.particle_manager.world.taskMgr.add( - self._monitor_task, - "particle_performance_monitor" - ) - - print("✓ 性能监控器已启动") - - def stop(self): - """停止性能监控""" - if not self.enabled: - return - - self.enabled = False - - # 停止监控任务 - if self.monitor_task and hasattr(self.particle_manager, 'world'): - self.particle_manager.world.taskMgr.remove(self.monitor_task) - self.monitor_task = None - - # 隐藏显示 - self.hide_display() - - print("✓ 性能监控器已停止") - - def toggle_display(self): - """切换显示状态""" - if self.display_visible: - self.hide_display() - else: - self.show_display() - - def show_display(self): - """显示性能信息""" - if self.display_visible: - return - - self.display_visible = True - self._create_display_gui() - - print("✓ 性能监控显示已开启") - - def hide_display(self): - """隐藏性能信息""" - if not self.display_visible: - return - - self.display_visible = False - self._cleanup_display_gui() - - print("✓ 性能监控显示已关闭") - - def _monitor_task(self, task): - """监控任务""" - if not self.enabled: - return task.done - - current_time = time.time() - - # 检查更新间隔 - if current_time - self.last_update_time < self.update_interval: - return task.cont - - dt = current_time - self.last_update_time - self.last_update_time = current_time - - # 收集性能数据 - self._collect_performance_data(dt) - - # 更新统计信息 - self._update_statistics() - - # 检查性能警告 - self._check_performance_warnings() - - # 更新显示 - if self.display_visible: - self._update_display() - - return task.cont - - def _collect_performance_data(self, dt: float): - """收集性能数据""" - try: - # FPS计算 - fps = 1.0 / dt if dt > 0 else 0.0 - self.frame_times.append(dt) - self.history_data['fps_history'].append(fps) - - # 获取粒子管理器统计信息 - if hasattr(self.particle_manager, 'get_stats'): - manager_stats = self.particle_manager.get_stats() - - # 粒子数量 - particle_count = self.particle_manager.get_particle_count() - self.particle_counts.append(particle_count) - self.history_data['particle_count_history'].append(particle_count) - - # 渲染时间 - render_time = manager_stats.get('render_time', 0.0) - self.render_times.append(render_time) - self.history_data['render_time_history'].append(render_time * 1000) # 转换为毫秒 - - # 物理时间 - physics_time = manager_stats.get('physics_time', 0.0) - self.physics_times.append(physics_time) - - # 更新时间 - update_time = manager_stats.get('update_time', 0.0) - self.update_times.append(update_time) - - # 渲染统计 - if hasattr(self.particle_manager, 'renderer'): - render_stats = self.particle_manager.renderer.get_render_stats() - self.stats['draw_calls'] = render_stats.get('draw_calls', 0) - self.stats['vertices_rendered'] = render_stats.get('vertices_rendered', 0) - - # 活跃发射器 - self.stats['active_emitters'] = self.particle_manager.get_emitter_count() - - # 更新当前值 - self.stats['fps'] = fps - self.stats['particle_count'] = self.particle_manager.get_particle_count() if self.particle_manager else 0 - - except Exception as e: - print(f"⚠ 性能数据收集失败: {e}") - - def _update_statistics(self): - """更新统计信息""" - try: - # FPS统计 - if self.frame_times: - fps_values = [1.0/dt if dt > 0 else 0.0 for dt in self.frame_times] - self.stats['avg_fps'] = sum(fps_values) / len(fps_values) if fps_values else 0.0 - self.stats['min_fps'] = min(fps_values) if fps_values else 0.0 - self.stats['max_fps'] = max(fps_values) if fps_values else 0.0 - - # 粒子数量统计 - if self.particle_counts: - self.stats['avg_particle_count'] = sum(self.particle_counts) / len(self.particle_counts) - self.stats['max_particle_count'] = max(self.particle_counts) - - # 渲染时间统计 - if self.render_times: - self.stats['render_time'] = sum(self.render_times[-10:]) / min(10, len(self.render_times)) - - # 物理时间统计 - if self.physics_times: - self.stats['physics_time'] = sum(self.physics_times[-10:]) / min(10, len(self.physics_times)) - - # 更新时间统计 - if self.update_times: - self.stats['update_time'] = sum(self.update_times[-10:]) / min(10, len(self.update_times)) - - # 内存使用(简化估算) - self.stats['memory_usage'] = self.stats['particle_count'] * 0.1 # KB per particle - - except Exception as e: - print(f"⚠ 统计信息更新失败: {e}") - - def _check_performance_warnings(self): - """检查性能警告""" - try: - # 低FPS警告 - if self.stats['fps'] < self.performance_thresholds['low_fps_warning']: - if not self.warnings['low_fps']: - self.warnings['low_fps'] = True - print(f"⚠ 性能警告: FPS过低 ({self.stats['fps']:.1f})") - else: - self.warnings['low_fps'] = False - - # 高粒子数警告 - if self.stats['particle_count'] > self.performance_thresholds['high_particle_warning']: - if not self.warnings['high_particles']: - self.warnings['high_particles'] = True - print(f"⚠ 性能警告: 粒子数过多 ({self.stats['particle_count']})") - else: - self.warnings['high_particles'] = False - - # 高渲染时间警告 - render_time_ms = self.stats['render_time'] * 1000 - if render_time_ms > self.performance_thresholds['high_render_time_warning']: - if not self.warnings['high_render_time']: - self.warnings['high_render_time'] = True - print(f"⚠ 性能警告: 渲染时间过长 ({render_time_ms:.1f}ms)") - else: - self.warnings['high_render_time'] = False - - # 高更新时间警告 - update_time_ms = self.stats['update_time'] * 1000 - if update_time_ms > self.performance_thresholds['high_update_time_warning']: - if not self.warnings['high_update_time']: - self.warnings['high_update_time'] = True - print(f"⚠ 性能警告: 更新时间过长 ({update_time_ms:.1f}ms)") - else: - self.warnings['high_update_time'] = False - - except Exception as e: - print(f"⚠ 性能警告检查失败: {e}") - - def _create_display_gui(self): - """创建显示GUI""" - try: - world = self.particle_manager.world - if not hasattr(world, 'gui_manager') or not world.gui_manager: - print("⚠ GUI管理器不可用,无法显示性能信息") - return - - gui_manager = world.gui_manager - - # 创建性能信息面板 - panel = gui_manager.createGUIButton( - pos=(0.02, 0, 0.5), - text="性能监控", - size=0.04 - ) - self.gui_elements.append(panel) - - # 创建信息标签 - info_items = [ - ("FPS", "fps"), - ("平均FPS", "avg_fps"), - ("粒子数", "particle_count"), - ("活跃发射器", "active_emitters"), - ("渲染时间", "render_time"), - ("物理时间", "physics_time"), - ("更新时间", "update_time"), - ("内存使用", "memory_usage"), - ("绘制调用", "draw_calls"), - ("顶点数", "vertices_rendered") - ] - - for i, (label, key) in enumerate(info_items): - # 标签 - label_gui = gui_manager.createGUIButton( - pos=(0.02, 0, 0.45 - i * 0.04), - text=f"{label}:", - size=0.03 - ) - self.gui_elements.append(label_gui) - - # 值 - value_gui = gui_manager.createGUIButton( - pos=(0.12, 0, 0.45 - i * 0.04), - text="0", - size=0.03 - ) - self.gui_elements.append(value_gui) - self.info_labels[key] = value_gui - - except Exception as e: - print(f"⚠ 性能显示GUI创建失败: {e}") - - def _cleanup_display_gui(self): - """清理显示GUI""" - try: - world = self.particle_manager.world - if hasattr(world, 'gui_manager') and world.gui_manager: - gui_manager = world.gui_manager - - for element in self.gui_elements: - gui_manager.deleteGUIElement(element) - - self.gui_elements.clear() - self.info_labels.clear() - - except Exception as e: - print(f"⚠ 性能显示GUI清理失败: {e}") - - def _update_display(self): - """更新显示内容""" - try: - # 更新各项数值显示 - for key, label_gui in self.info_labels.items(): - value = self.stats.get(key, 0) - - if key in ['fps', 'avg_fps', 'min_fps', 'max_fps']: - text = f"{value:.1f}" - elif key in ['particle_count', 'max_particle_count', 'draw_calls', 'vertices_rendered', 'active_emitters']: - text = f"{int(value)}" - elif key in ['render_time', 'physics_time', 'update_time']: - text = f"{value*1000:.1f}ms" - elif key == 'memory_usage': - if value > 1024: - text = f"{value/1024:.1f}MB" - else: - text = f"{value:.1f}KB" - else: - text = f"{value:.2f}" - - label_gui.setText(text) - - # 根据警告状态设置颜色 - color_set = False - if key == 'fps' and self.warnings['low_fps']: - label_gui.setColor(1, 0.3, 0.3, 1) # 红色 - color_set = True - elif key == 'particle_count' and self.warnings['high_particles']: - label_gui.setColor(1, 0.8, 0.3, 1) # 橙色 - color_set = True - elif key == 'render_time' and self.warnings['high_render_time']: - label_gui.setColor(1, 0.5, 0.3, 1) # 橙红色 - color_set = True - elif key == 'update_time' and self.warnings['high_update_time']: - label_gui.setColor(1, 0.4, 0.4, 1) # 红色 - color_set = True - - if not color_set: - label_gui.setColor(1, 1, 1, 1) # 白色 - - except Exception as e: - print(f"⚠ 性能显示更新失败: {e}") - - def get_performance_report(self) -> Dict: - """获取性能报告""" - return { - 'current_stats': self.stats.copy(), - 'warnings': self.warnings.copy(), - 'thresholds': self.performance_thresholds.copy(), - 'data_points': { - 'frame_times': len(self.frame_times), - 'particle_counts': len(self.particle_counts), - 'render_times': len(self.render_times), - 'physics_times': len(self.physics_times), - 'update_times': len(self.update_times) - }, - 'history': { - 'fps_history': list(self.history_data['fps_history']), - 'particle_count_history': list(self.history_data['particle_count_history']), - 'render_time_history': list(self.history_data['render_time_history']) - } - } - - def reset_statistics(self): - """重置统计数据""" - self.frame_times.clear() - self.particle_counts.clear() - self.render_times.clear() - self.physics_times.clear() - self.update_times.clear() - - # 清空历史数据 - for history_list in self.history_data.values(): - history_list.clear() - - # 重置统计值 - for key in self.stats: - self.stats[key] = 0.0 - - # 重置警告状态 - for key in self.warnings: - self.warnings[key] = False - - print("✓ 性能统计数据已重置") - - def set_performance_thresholds(self, **thresholds): - """设置性能阈值""" - for key, value in thresholds.items(): - if key in self.performance_thresholds: - self.performance_thresholds[key] = value - print(f"✓ 性能阈值已更新 {key}: {value}") - - def cleanup(self): - """清理监控器资源""" - self.stop() - self.reset_statistics() - - def get_history_data(self, data_type: str) -> List: - """获取历史数据""" - if data_type in self.history_data: - return list(self.history_data[data_type]) - return [] - - def export_performance_data(self, filepath: str) -> bool: - """导出性能数据""" - try: - import json - report = self.get_performance_report() - - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(report, f, indent=2, ensure_ascii=False) - - print(f"✓ 性能数据已导出到: {filepath}") - return True - - except Exception as e: - print(f"✗ 性能数据导出失败: {e}") +""" +性能监控器 +监控粒子系统的性能指标 +""" + +import time +from typing import Dict, List, Optional +from collections import deque +from direct.task import Task + +class PerformanceMonitor: + """ + 粒子系统性能监控器 + 实时监控和显示性能指标 + """ + + def __init__(self, particle_manager): + """ + 初始化性能监控器 + + Args: + particle_manager: 粒子管理器 + """ + self.particle_manager = particle_manager + self.enabled = False + self.display_visible = False + + # 性能数据收集 + self.frame_times = deque(maxlen=60) # 保存60帧的数据 + self.particle_counts = deque(maxlen=60) + self.render_times = deque(maxlen=60) + self.physics_times = deque(maxlen=60) + self.update_times = deque(maxlen=60) + + # 统计数据 + self.stats = { + 'fps': 0.0, + 'avg_fps': 0.0, + 'min_fps': 0.0, + 'max_fps': 0.0, + 'particle_count': 0, + 'avg_particle_count': 0.0, + 'max_particle_count': 0, + 'render_time': 0.0, + 'physics_time': 0.0, + 'update_time': 0.0, + 'memory_usage': 0.0, + 'draw_calls': 0, + 'vertices_rendered': 0, + 'active_emitters': 0 + } + + # 性能阈值 + self.performance_thresholds = { + 'low_fps_warning': 30.0, + 'high_particle_warning': 5000, + 'high_render_time_warning': 16.0, # ms + 'high_update_time_warning': 5.0 # ms + } + + # 警告状态 + self.warnings = { + 'low_fps': False, + 'high_particles': False, + 'high_render_time': False, + 'high_update_time': False + } + + # GUI元素 + self.gui_elements = [] + self.info_labels = {} + + # 监控任务 + self.monitor_task = None + + # 时间记录 + self.last_update_time = time.time() + self.update_interval = 0.1 # 100ms更新一次 + + # 历史数据(用于图表显示) + self.history_data = { + 'fps_history': deque(maxlen=100), + 'particle_count_history': deque(maxlen=100), + 'render_time_history': deque(maxlen=100) + } + + print("✓ 性能监控器初始化完成") + + def start(self): + """启动性能监控""" + if self.enabled: + return + + self.enabled = True + + # 启动监控任务 + if hasattr(self.particle_manager, 'world') and self.particle_manager.world: + self.monitor_task = self.particle_manager.world.taskMgr.add( + self._monitor_task, + "particle_performance_monitor" + ) + + print("✓ 性能监控器已启动") + + def stop(self): + """停止性能监控""" + if not self.enabled: + return + + self.enabled = False + + # 停止监控任务 + if self.monitor_task and hasattr(self.particle_manager, 'world'): + self.particle_manager.world.taskMgr.remove(self.monitor_task) + self.monitor_task = None + + # 隐藏显示 + self.hide_display() + + print("✓ 性能监控器已停止") + + def toggle_display(self): + """切换显示状态""" + if self.display_visible: + self.hide_display() + else: + self.show_display() + + def show_display(self): + """显示性能信息""" + if self.display_visible: + return + + self.display_visible = True + self._create_display_gui() + + print("✓ 性能监控显示已开启") + + def hide_display(self): + """隐藏性能信息""" + if not self.display_visible: + return + + self.display_visible = False + self._cleanup_display_gui() + + print("✓ 性能监控显示已关闭") + + def _monitor_task(self, task): + """监控任务""" + if not self.enabled: + return task.done + + current_time = time.time() + + # 检查更新间隔 + if current_time - self.last_update_time < self.update_interval: + return task.cont + + dt = current_time - self.last_update_time + self.last_update_time = current_time + + # 收集性能数据 + self._collect_performance_data(dt) + + # 更新统计信息 + self._update_statistics() + + # 检查性能警告 + self._check_performance_warnings() + + # 更新显示 + if self.display_visible: + self._update_display() + + return task.cont + + def _collect_performance_data(self, dt: float): + """收集性能数据""" + try: + # FPS计算 + fps = 1.0 / dt if dt > 0 else 0.0 + self.frame_times.append(dt) + self.history_data['fps_history'].append(fps) + + # 获取粒子管理器统计信息 + if hasattr(self.particle_manager, 'get_stats'): + manager_stats = self.particle_manager.get_stats() + + # 粒子数量 + particle_count = self.particle_manager.get_particle_count() + self.particle_counts.append(particle_count) + self.history_data['particle_count_history'].append(particle_count) + + # 渲染时间 + render_time = manager_stats.get('render_time', 0.0) + self.render_times.append(render_time) + self.history_data['render_time_history'].append(render_time * 1000) # 转换为毫秒 + + # 物理时间 + physics_time = manager_stats.get('physics_time', 0.0) + self.physics_times.append(physics_time) + + # 更新时间 + update_time = manager_stats.get('update_time', 0.0) + self.update_times.append(update_time) + + # 渲染统计 + if hasattr(self.particle_manager, 'renderer'): + render_stats = self.particle_manager.renderer.get_render_stats() + self.stats['draw_calls'] = render_stats.get('draw_calls', 0) + self.stats['vertices_rendered'] = render_stats.get('vertices_rendered', 0) + + # 活跃发射器 + self.stats['active_emitters'] = self.particle_manager.get_emitter_count() + + # 更新当前值 + self.stats['fps'] = fps + self.stats['particle_count'] = self.particle_manager.get_particle_count() if self.particle_manager else 0 + + except Exception as e: + print(f"⚠ 性能数据收集失败: {e}") + + def _update_statistics(self): + """更新统计信息""" + try: + # FPS统计 + if self.frame_times: + fps_values = [1.0/dt if dt > 0 else 0.0 for dt in self.frame_times] + self.stats['avg_fps'] = sum(fps_values) / len(fps_values) if fps_values else 0.0 + self.stats['min_fps'] = min(fps_values) if fps_values else 0.0 + self.stats['max_fps'] = max(fps_values) if fps_values else 0.0 + + # 粒子数量统计 + if self.particle_counts: + self.stats['avg_particle_count'] = sum(self.particle_counts) / len(self.particle_counts) + self.stats['max_particle_count'] = max(self.particle_counts) + + # 渲染时间统计 + if self.render_times: + self.stats['render_time'] = sum(self.render_times[-10:]) / min(10, len(self.render_times)) + + # 物理时间统计 + if self.physics_times: + self.stats['physics_time'] = sum(self.physics_times[-10:]) / min(10, len(self.physics_times)) + + # 更新时间统计 + if self.update_times: + self.stats['update_time'] = sum(self.update_times[-10:]) / min(10, len(self.update_times)) + + # 内存使用(简化估算) + self.stats['memory_usage'] = self.stats['particle_count'] * 0.1 # KB per particle + + except Exception as e: + print(f"⚠ 统计信息更新失败: {e}") + + def _check_performance_warnings(self): + """检查性能警告""" + try: + # 低FPS警告 + if self.stats['fps'] < self.performance_thresholds['low_fps_warning']: + if not self.warnings['low_fps']: + self.warnings['low_fps'] = True + print(f"⚠ 性能警告: FPS过低 ({self.stats['fps']:.1f})") + else: + self.warnings['low_fps'] = False + + # 高粒子数警告 + if self.stats['particle_count'] > self.performance_thresholds['high_particle_warning']: + if not self.warnings['high_particles']: + self.warnings['high_particles'] = True + print(f"⚠ 性能警告: 粒子数过多 ({self.stats['particle_count']})") + else: + self.warnings['high_particles'] = False + + # 高渲染时间警告 + render_time_ms = self.stats['render_time'] * 1000 + if render_time_ms > self.performance_thresholds['high_render_time_warning']: + if not self.warnings['high_render_time']: + self.warnings['high_render_time'] = True + print(f"⚠ 性能警告: 渲染时间过长 ({render_time_ms:.1f}ms)") + else: + self.warnings['high_render_time'] = False + + # 高更新时间警告 + update_time_ms = self.stats['update_time'] * 1000 + if update_time_ms > self.performance_thresholds['high_update_time_warning']: + if not self.warnings['high_update_time']: + self.warnings['high_update_time'] = True + print(f"⚠ 性能警告: 更新时间过长 ({update_time_ms:.1f}ms)") + else: + self.warnings['high_update_time'] = False + + except Exception as e: + print(f"⚠ 性能警告检查失败: {e}") + + def _create_display_gui(self): + """创建显示GUI""" + try: + world = self.particle_manager.world + if not hasattr(world, 'gui_manager') or not world.gui_manager: + print("⚠ GUI管理器不可用,无法显示性能信息") + return + + gui_manager = world.gui_manager + + # 创建性能信息面板 + panel = gui_manager.createGUIButton( + pos=(0.02, 0, 0.5), + text="性能监控", + size=0.04 + ) + self.gui_elements.append(panel) + + # 创建信息标签 + info_items = [ + ("FPS", "fps"), + ("平均FPS", "avg_fps"), + ("粒子数", "particle_count"), + ("活跃发射器", "active_emitters"), + ("渲染时间", "render_time"), + ("物理时间", "physics_time"), + ("更新时间", "update_time"), + ("内存使用", "memory_usage"), + ("绘制调用", "draw_calls"), + ("顶点数", "vertices_rendered") + ] + + for i, (label, key) in enumerate(info_items): + # 标签 + label_gui = gui_manager.createGUIButton( + pos=(0.02, 0, 0.45 - i * 0.04), + text=f"{label}:", + size=0.03 + ) + self.gui_elements.append(label_gui) + + # 值 + value_gui = gui_manager.createGUIButton( + pos=(0.12, 0, 0.45 - i * 0.04), + text="0", + size=0.03 + ) + self.gui_elements.append(value_gui) + self.info_labels[key] = value_gui + + except Exception as e: + print(f"⚠ 性能显示GUI创建失败: {e}") + + def _cleanup_display_gui(self): + """清理显示GUI""" + try: + world = self.particle_manager.world + if hasattr(world, 'gui_manager') and world.gui_manager: + gui_manager = world.gui_manager + + for element in self.gui_elements: + gui_manager.deleteGUIElement(element) + + self.gui_elements.clear() + self.info_labels.clear() + + except Exception as e: + print(f"⚠ 性能显示GUI清理失败: {e}") + + def _update_display(self): + """更新显示内容""" + try: + # 更新各项数值显示 + for key, label_gui in self.info_labels.items(): + value = self.stats.get(key, 0) + + if key in ['fps', 'avg_fps', 'min_fps', 'max_fps']: + text = f"{value:.1f}" + elif key in ['particle_count', 'max_particle_count', 'draw_calls', 'vertices_rendered', 'active_emitters']: + text = f"{int(value)}" + elif key in ['render_time', 'physics_time', 'update_time']: + text = f"{value*1000:.1f}ms" + elif key == 'memory_usage': + if value > 1024: + text = f"{value/1024:.1f}MB" + else: + text = f"{value:.1f}KB" + else: + text = f"{value:.2f}" + + label_gui.setText(text) + + # 根据警告状态设置颜色 + color_set = False + if key == 'fps' and self.warnings['low_fps']: + label_gui.setColor(1, 0.3, 0.3, 1) # 红色 + color_set = True + elif key == 'particle_count' and self.warnings['high_particles']: + label_gui.setColor(1, 0.8, 0.3, 1) # 橙色 + color_set = True + elif key == 'render_time' and self.warnings['high_render_time']: + label_gui.setColor(1, 0.5, 0.3, 1) # 橙红色 + color_set = True + elif key == 'update_time' and self.warnings['high_update_time']: + label_gui.setColor(1, 0.4, 0.4, 1) # 红色 + color_set = True + + if not color_set: + label_gui.setColor(1, 1, 1, 1) # 白色 + + except Exception as e: + print(f"⚠ 性能显示更新失败: {e}") + + def get_performance_report(self) -> Dict: + """获取性能报告""" + return { + 'current_stats': self.stats.copy(), + 'warnings': self.warnings.copy(), + 'thresholds': self.performance_thresholds.copy(), + 'data_points': { + 'frame_times': len(self.frame_times), + 'particle_counts': len(self.particle_counts), + 'render_times': len(self.render_times), + 'physics_times': len(self.physics_times), + 'update_times': len(self.update_times) + }, + 'history': { + 'fps_history': list(self.history_data['fps_history']), + 'particle_count_history': list(self.history_data['particle_count_history']), + 'render_time_history': list(self.history_data['render_time_history']) + } + } + + def reset_statistics(self): + """重置统计数据""" + self.frame_times.clear() + self.particle_counts.clear() + self.render_times.clear() + self.physics_times.clear() + self.update_times.clear() + + # 清空历史数据 + for history_list in self.history_data.values(): + history_list.clear() + + # 重置统计值 + for key in self.stats: + self.stats[key] = 0.0 + + # 重置警告状态 + for key in self.warnings: + self.warnings[key] = False + + print("✓ 性能统计数据已重置") + + def set_performance_thresholds(self, **thresholds): + """设置性能阈值""" + for key, value in thresholds.items(): + if key in self.performance_thresholds: + self.performance_thresholds[key] = value + print(f"✓ 性能阈值已更新 {key}: {value}") + + def cleanup(self): + """清理监控器资源""" + self.stop() + self.reset_statistics() + + def get_history_data(self, data_type: str) -> List: + """获取历史数据""" + if data_type in self.history_data: + return list(self.history_data[data_type]) + return [] + + def export_performance_data(self, filepath: str) -> bool: + """导出性能数据""" + try: + import json + report = self.get_performance_report() + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(report, f, indent=2, ensure_ascii=False) + + print(f"✓ 性能数据已导出到: {filepath}") + return True + + except Exception as e: + print(f"✗ 性能数据导出失败: {e}") return False \ No newline at end of file diff --git a/plugins/user/pathfinding_algorithms/README.md b/plugins/user/pathfinding_algorithms/README.md index b08babb3..b9fd62db 100644 --- a/plugins/user/pathfinding_algorithms/README.md +++ b/plugins/user/pathfinding_algorithms/README.md @@ -1,402 +1,402 @@ -# 路径规划算法插件 - -一个功能完整的路径规划算法系统插件,为EG引擎提供多种经典和现代路径规划算法的实现。 - -## 功能特性 - -### 核心算法 -- **A*算法**: 经典的启发式搜索算法,支持多种启发式函数和优化选项 -- **Dijkstra算法**: 保证最短路径的搜索算法 -- **BFS算法**: 广度优先搜索,适用于无权图 -- **DFS算法**: 深度优先搜索,可能更快找到任意路径 -- **JPS算法**: Jump Point Search,针对网格地图优化的A*算法,性能比传统A*快数倍 - -### 算法管理 -- **统一接口**: 所有算法通过统一接口访问 -- **参数配置**: 支持算法参数配置和调整 -- **性能统计**: 自动收集和分析算法性能数据 -- **批量测试**: 支持批量测试多个算法和场景 -- **算法推荐**: 根据场景特点自动推荐最适合的算法 -- **基准测试**: 对算法进行性能基准测试 - -### 可视化功能 -- **网格显示**: 可视化显示网格地图,区分可通行区域和障碍物 -- **路径绘制**: 实时绘制计算出的路径,支持动画效果 -- **交互编辑**: 通过可视化界面编辑地图 -- **场景切换**: 支持多种预设场景 -- **节点可视化**: 可视化已访问节点和开放列表节点 -- **动画支持**: 支持路径查找过程的动画演示 - -### 编辑器功能 -- **可视化编辑**: 图形化界面编辑路径规划场景 -- **多种工具**: 选择、添加障碍、移除障碍、设置起点终点等工具 -- **场景生成**: 自动生成多种类型的测试场景 -- **算法比较**: 直接在编辑器中比较不同算法性能 - -### 高级特性 -- **性能优化**: 多种算法优化技术,包括启发式权重调整、Tie Breaker等 -- **复杂度分析**: 提供各算法的时间和空间复杂度信息 -- **数据验证**: 自动验证输入数据的有效性 -- **报告生成**: 生成详细的算法性能报告 -- **缓存机制**: 缓存性能数据以供后续分析 - -## 使用方法 - -### 快捷键操作 -- **F1**: 切换路径规划编辑器显示 -- **F2**: 切换可视化显示 -- **F3**: 运行演示 -- **F4**: 清除可视化 -- **F5**: 比较算法性能 -- **F6**: 显示统计信息 -- **F7**: 生成算法报告 -- **F8**: 优化路径 -- **F9**: 生成随机场景 -- **F10**: 切换动画 -- **F11**: 显示算法信息 -- **F12**: 导出详细报告 - -### GUI界面操作 -插件会在屏幕左上角创建控制面板,右上角创建编辑器面板,可以通过按钮直接操作路径规划功能。 - -### 编程接口 -```python -# 获取插件实例 -pathfinding_plugin = world.plugin_manager.get_plugin("pathfinding_algorithms") - -# 创建网格 -grid = [[0, 0, 0, 1, 0], - [0, 1, 0, 1, 0], - [0, 1, 0, 0, 0], - [0, 0, 0, 1, 0], - [0, 0, 0, 0, 0]] - -# 设置起点和终点 -start = (0, 0) -goal = (4, 4) - -# 使用A*算法查找路径 -path, stats = pathfinding_plugin.find_path("astar", grid, start, goal) - -if path: - print(f"找到路径,长度: {len(path)}") - print(f"访问节点数: {stats['nodes_visited']}") - print(f"耗时: {stats['time_taken']}秒") -else: - print("未找到路径") - -# 可视化路径 -pathfinding_plugin.visualize_path(path, grid) - -# 获取可用算法列表 -algorithms = pathfinding_plugin.get_available_algorithms() -print("可用算法:", algorithms) - -# 设置算法参数 -pathfinding_plugin.set_algorithm_parameter("astar", "heuristic", "euclidean") - -# 获取算法参数 -parameters = pathfinding_plugin.get_algorithm_parameters("astar") -print("A*算法参数:", parameters) - -# 添加自定义算法 -class CustomAlgorithm: - def find_path(self, grid, start, goal): - # 实现自定义算法 - pass - -pathfinding_plugin.add_algorithm("custom", CustomAlgorithm, "自定义算法描述") - -# 比较算法性能 -pathfinding_plugin.compare_algorithms() - -# 生成报告 -report = pathfinding_plugin.generate_report() -print("算法报告:", report) - -# 设置可视化参数 -pathfinding_plugin.set_visualization_settings({ - 'grid_color': LColor(0.7, 0.7, 0.7, 1.0), - 'path_color': LColor(1.0, 0.5, 0.0, 1.0) -}) - -# 获取可视化参数 -settings = pathfinding_plugin.get_visualization_settings() -print("可视化设置:", settings) - -# 启用动画 -pathfinding_plugin.enable_visualization_animation(True) - -# 批量测试算法 -test_cases = [ - {'grid': grid, 'start': (0, 0), 'goal': (4, 4), 'name': 'test1'}, - {'grid': grid, 'start': (1, 1), 'goal': (3, 3), 'name': 'test2'} -] -results = pathfinding_plugin.batch_test_algorithms(['astar', 'bfs'], test_cases) - -# 详细比较算法 -detailed_results = pathfinding_plugin.compare_algorithms_detailed( - ['astar', 'jps'], grid, start, goal) - -# 获取缓存的性能数据 -performance_data = pathfinding_plugin.get_cached_performance_data('astar') - -# 清除性能缓存 -pathfinding_plugin.clear_performance_cache() - -# 重置统计信息 -pathfinding_plugin.reset_statistics() - -# 验证网格数据 -validation_result = pathfinding_plugin.validate_grid_data(grid) -print("网格验证结果:", validation_result) - -# 获取算法复杂度信息 -complexity = pathfinding_plugin.get_algorithm_complexity('astar') -print("A*算法复杂度:", complexity) - -# 基准测试算法 -benchmark_results = pathfinding_plugin.benchmark_algorithm('astar', grid, start, goal, 1000) -print("基准测试结果:", benchmark_results) - -# 为特定网格优化算法 -pathfinding_plugin.optimize_algorithm_for_grid('jps', grid) -``` - -## 核心组件 - -### 算法管理器 -负责各种路径规划算法的注册、配置和执行。 - -主要功能: -- 算法注册和管理 -- 参数配置和调整 -- 统一接口访问 -- 性能统计和分析 -- 批量测试支持 -- 算法推荐系统 - -### 可视化管理器 -负责路径规划算法的可视化显示。 - -主要功能: -- 网格地图可视化 -- 路径绘制和显示 -- 起点终点标记 -- 可视化参数配置 -- 节点可视化(已访问节点、开放列表) -- 动画支持 - -### 路径规划编辑器 -提供可视化编辑路径规划场景的功能。 - -主要功能: -- 图形化界面编辑 -- 多种编辑工具 -- 场景生成和管理 -- 算法性能比较 - -## 算法说明 - -### A*算法 -A*是一种启发式搜索算法,结合了Dijkstra算法的准确性和贪心最佳优先搜索的效率。 - -特点: -- 通过启发式函数指导搜索方向 -- 保证找到最优解(在启发式函数可接受的情况下) -- 适用于静态环境 -- 支持多种启发式函数(曼哈顿距离、欧几里得距离、切比雪夫距离等) -- 支持对角线移动和角落切割 -- 支持Tie Breaker优化 - -### Dijkstra算法 -Dijkstra算法是一种用于计算单源最短路径的经典算法。 - -特点: -- 保证找到最短路径 -- 适用于权重图 -- 时间复杂度较高 -- 不需要启发式函数 - -### BFS算法 -广度优先搜索算法,保证找到最短路径(在无权图中)。 - -特点: -- 保证找到最短路径 -- 适用于无权图 -- 内存使用较多 -- 实现简单 - -### DFS算法 -深度优先搜索算法,不保证找到最短路径,但可能更快找到任意路径。 - -特点: -- 可能更快找到路径 -- 不保证最优解 -- 内存使用较少 -- 支持深度限制 - -### JPS算法 -Jump Point Search是一种优化的A*算法,专门用于网格地图。 - -特点: -- 针对网格地图优化 -- 比传统A*更快 -- 保持A*的最优性 -- 利用网格特性跳过大量不必要的节点 -- 适用于大型网格地图 - -## 场景生成 - -### 简单场景 -空的网格地图,适用于基本测试。 - -### 迷宫场景 -使用深度优先搜索算法生成的迷宫。 - -### 房间场景 -包含多个房间和走廊的复杂场景。 - -### 随机场景 -随机生成障碍物的场景。 - -### 隧道场景 -多条曲折隧道的场景。 - -## 性能优化 - -### 算法优化 -- **启发式函数**: 多种启发式函数可选,适应不同场景 -- **启发式权重**: 可调整权重平衡搜索速度和最优性 -- **对角线移动**: 支持对角线移动提高效率 -- **Tie Breaker**: 使用tie breaker优化打破平局 -- **JPS优化**: 利用跳点搜索跳过大量节点 - -### 内存管理 -- **对象复用**: 减少对象创建和销毁开销 -- **批量处理**: 提高大量数据处理效率 -- **缓存机制**: 缓存性能数据供后续分析 - -### 渲染优化 -- **LOD支持**: 根据距离调整显示细节 -- **视锥剔除**: 只渲染可见区域 -- **批处理**: 合并相似的渲染调用 -- **透明度优化**: 优化透明节点的渲染 - -## 扩展开发 - -### 添加新的路径规划算法 -1. 在[algorithms](file:///home/hello/EG/plugins/user/pathfinding_algorithms/algorithms)目录中创建新的算法文件 -2. 实现算法类,确保有find_path方法 -3. 在[algorithm_manager.py](file:///home/hello/EG/plugins/user/pathfinding_algorithms/core/algorithm_manager.py)中注册新算法 - -### 添加新的场景生成器 -1. 在[utils/demo_scenarios.py](file:///home/hello/EG/plugins/user/pathfinding_algorithms/utils/demo_scenarios.py)中实现新的场景生成函数 -2. 在编辑器中添加对应的生成选项 - -### 添加新的可视化效果 -1. 在[visualization_manager.py](file:///home/hello/EG/plugins/user/pathfinding_algorithms/visualization/visualization_manager.py)中实现新的可视化方法 -2. 在编辑器中添加对应的控制选项 - -## 故障排除 - -### 路径查找失败 -1. 检查起点和终点是否在可通行区域内 -2. 确认网格数据格式是否正确 -3. 检查算法参数设置 -4. 使用数据验证功能检查网格有效性 - -### 性能问题 -1. 使用算法比较功能分析性能瓶颈 -2. 调整启发式权重优化搜索效率 -3. 简化网格结构 -4. 对于大型网格考虑使用JPS算法 - -### 可视化问题 -1. 确认GUI系统是否正常工作 -2. 检查可视化参数设置 -3. 重新启用插件尝试 - -## API参考 - -### Plugin类主要方法 - -#### 初始化和控制 -- `initialize()` - 初始化插件 -- `enable()` - 启用插件 -- `disable()` - 禁用插件 -- `finalize()` - 清理插件资源 -- `update(dt)` - 更新插件状态 - -#### 路径查找 -- `find_path(algorithm, grid, start, goal)` - 查找路径 -- `add_algorithm(name, algorithm_class, description)` - 添加新算法 -- `remove_algorithm(name)` - 移除算法 -- `get_available_algorithms()` - 获取可用算法列表 -- `get_algorithm_description(algorithm)` - 获取算法描述 -- `set_algorithm_parameter(algorithm, parameter, value)` - 设置算法参数 -- `get_algorithm_parameters(algorithm)` - 获取算法参数 - -#### 可视化 -- `visualize_path(path, grid)` - 可视化路径 -- `visualize_grid(grid)` - 可视化网格 -- `visualize_visited_nodes(visited_nodes)` - 可视化已访问节点 -- `visualize_open_list(open_list)` - 可视化开放列表 -- `clear_path_visualization()` - 清除路径可视化 -- `set_visualization_settings(settings)` - 设置可视化参数 -- `get_visualization_settings()` - 获取可视化参数 -- `enable_visualization_animation(enable)` - 启用可视化动画 - -#### 性能和统计 -- `compare_algorithms()` - 比较算法性能 -- `show_stats()` - 显示统计信息 -- `generate_report()` - 生成报告 -- `batch_test_algorithms(algorithms, test_cases)` - 批量测试算法 -- `compare_algorithms_detailed(algorithms, grid, start, goal)` - 详细比较算法 -- `get_cached_performance_data(algorithm)` - 获取缓存的性能数据 -- `clear_performance_cache()` - 清除性能缓存 -- `reset_statistics()` - 重置统计信息 -- `validate_grid_data(grid)` - 验证网格数据 -- `get_algorithm_complexity(algorithm)` - 获取算法复杂度信息 -- `benchmark_algorithm(algorithm, grid, start, goal, iterations)` - 基准测试算法 -- `optimize_algorithm_for_grid(algorithm, grid)` - 为特定网格优化算法 - -#### 编辑器功能 -- `toggle_editor()` - 切换编辑器显示 -- `toggle_visualization()` - 切换可视化显示 -- `run_demo()` - 运行演示 -- `clear_visualization()` - 清除可视化 -- `generate_random_scenario()` - 生成随机场景 -- `toggle_animation()` - 切换动画 -- `show_algorithm_info()` - 显示算法信息 -- `export_detailed_report()` - 导出详细报告 - -## 性能调优建议 - -### 算法选择 -1. 对于需要最优解的场景,使用A*或Dijkstra算法 -2. 对于只需要找到任意路径的场景,使用BFS或DFS算法 -3. 对于大型网格地图,使用JPS算法 -4. 根据场景特点使用算法推荐功能 - -### 参数调整 -1. 合理设置启发式权重(通常1.0为最佳) -2. 根据场景特点选择合适的启发式函数 -3. 对于DFS算法,合理设置深度限制 -4. 对于A*算法,考虑启用Tie Breaker优化 - -### 场景优化 -1. 简化不必要的障碍物 -2. 减小网格尺寸 -3. 使用分层路径规划处理大场景 -4. 对于静态环境,考虑预计算路径 - -## 版本信息 - -- 版本: 1.0.0 -- 作者: EG Team -- 发布日期: 2024年 - -## 许可证 - +# 路径规划算法插件 + +一个功能完整的路径规划算法系统插件,为EG引擎提供多种经典和现代路径规划算法的实现。 + +## 功能特性 + +### 核心算法 +- **A*算法**: 经典的启发式搜索算法,支持多种启发式函数和优化选项 +- **Dijkstra算法**: 保证最短路径的搜索算法 +- **BFS算法**: 广度优先搜索,适用于无权图 +- **DFS算法**: 深度优先搜索,可能更快找到任意路径 +- **JPS算法**: Jump Point Search,针对网格地图优化的A*算法,性能比传统A*快数倍 + +### 算法管理 +- **统一接口**: 所有算法通过统一接口访问 +- **参数配置**: 支持算法参数配置和调整 +- **性能统计**: 自动收集和分析算法性能数据 +- **批量测试**: 支持批量测试多个算法和场景 +- **算法推荐**: 根据场景特点自动推荐最适合的算法 +- **基准测试**: 对算法进行性能基准测试 + +### 可视化功能 +- **网格显示**: 可视化显示网格地图,区分可通行区域和障碍物 +- **路径绘制**: 实时绘制计算出的路径,支持动画效果 +- **交互编辑**: 通过可视化界面编辑地图 +- **场景切换**: 支持多种预设场景 +- **节点可视化**: 可视化已访问节点和开放列表节点 +- **动画支持**: 支持路径查找过程的动画演示 + +### 编辑器功能 +- **可视化编辑**: 图形化界面编辑路径规划场景 +- **多种工具**: 选择、添加障碍、移除障碍、设置起点终点等工具 +- **场景生成**: 自动生成多种类型的测试场景 +- **算法比较**: 直接在编辑器中比较不同算法性能 + +### 高级特性 +- **性能优化**: 多种算法优化技术,包括启发式权重调整、Tie Breaker等 +- **复杂度分析**: 提供各算法的时间和空间复杂度信息 +- **数据验证**: 自动验证输入数据的有效性 +- **报告生成**: 生成详细的算法性能报告 +- **缓存机制**: 缓存性能数据以供后续分析 + +## 使用方法 + +### 快捷键操作 +- **F1**: 切换路径规划编辑器显示 +- **F2**: 切换可视化显示 +- **F3**: 运行演示 +- **F4**: 清除可视化 +- **F5**: 比较算法性能 +- **F6**: 显示统计信息 +- **F7**: 生成算法报告 +- **F8**: 优化路径 +- **F9**: 生成随机场景 +- **F10**: 切换动画 +- **F11**: 显示算法信息 +- **F12**: 导出详细报告 + +### GUI界面操作 +插件会在屏幕左上角创建控制面板,右上角创建编辑器面板,可以通过按钮直接操作路径规划功能。 + +### 编程接口 +```python +# 获取插件实例 +pathfinding_plugin = world.plugin_manager.get_plugin("pathfinding_algorithms") + +# 创建网格 +grid = [[0, 0, 0, 1, 0], + [0, 1, 0, 1, 0], + [0, 1, 0, 0, 0], + [0, 0, 0, 1, 0], + [0, 0, 0, 0, 0]] + +# 设置起点和终点 +start = (0, 0) +goal = (4, 4) + +# 使用A*算法查找路径 +path, stats = pathfinding_plugin.find_path("astar", grid, start, goal) + +if path: + print(f"找到路径,长度: {len(path)}") + print(f"访问节点数: {stats['nodes_visited']}") + print(f"耗时: {stats['time_taken']}秒") +else: + print("未找到路径") + +# 可视化路径 +pathfinding_plugin.visualize_path(path, grid) + +# 获取可用算法列表 +algorithms = pathfinding_plugin.get_available_algorithms() +print("可用算法:", algorithms) + +# 设置算法参数 +pathfinding_plugin.set_algorithm_parameter("astar", "heuristic", "euclidean") + +# 获取算法参数 +parameters = pathfinding_plugin.get_algorithm_parameters("astar") +print("A*算法参数:", parameters) + +# 添加自定义算法 +class CustomAlgorithm: + def find_path(self, grid, start, goal): + # 实现自定义算法 + pass + +pathfinding_plugin.add_algorithm("custom", CustomAlgorithm, "自定义算法描述") + +# 比较算法性能 +pathfinding_plugin.compare_algorithms() + +# 生成报告 +report = pathfinding_plugin.generate_report() +print("算法报告:", report) + +# 设置可视化参数 +pathfinding_plugin.set_visualization_settings({ + 'grid_color': LColor(0.7, 0.7, 0.7, 1.0), + 'path_color': LColor(1.0, 0.5, 0.0, 1.0) +}) + +# 获取可视化参数 +settings = pathfinding_plugin.get_visualization_settings() +print("可视化设置:", settings) + +# 启用动画 +pathfinding_plugin.enable_visualization_animation(True) + +# 批量测试算法 +test_cases = [ + {'grid': grid, 'start': (0, 0), 'goal': (4, 4), 'name': 'test1'}, + {'grid': grid, 'start': (1, 1), 'goal': (3, 3), 'name': 'test2'} +] +results = pathfinding_plugin.batch_test_algorithms(['astar', 'bfs'], test_cases) + +# 详细比较算法 +detailed_results = pathfinding_plugin.compare_algorithms_detailed( + ['astar', 'jps'], grid, start, goal) + +# 获取缓存的性能数据 +performance_data = pathfinding_plugin.get_cached_performance_data('astar') + +# 清除性能缓存 +pathfinding_plugin.clear_performance_cache() + +# 重置统计信息 +pathfinding_plugin.reset_statistics() + +# 验证网格数据 +validation_result = pathfinding_plugin.validate_grid_data(grid) +print("网格验证结果:", validation_result) + +# 获取算法复杂度信息 +complexity = pathfinding_plugin.get_algorithm_complexity('astar') +print("A*算法复杂度:", complexity) + +# 基准测试算法 +benchmark_results = pathfinding_plugin.benchmark_algorithm('astar', grid, start, goal, 1000) +print("基准测试结果:", benchmark_results) + +# 为特定网格优化算法 +pathfinding_plugin.optimize_algorithm_for_grid('jps', grid) +``` + +## 核心组件 + +### 算法管理器 +负责各种路径规划算法的注册、配置和执行。 + +主要功能: +- 算法注册和管理 +- 参数配置和调整 +- 统一接口访问 +- 性能统计和分析 +- 批量测试支持 +- 算法推荐系统 + +### 可视化管理器 +负责路径规划算法的可视化显示。 + +主要功能: +- 网格地图可视化 +- 路径绘制和显示 +- 起点终点标记 +- 可视化参数配置 +- 节点可视化(已访问节点、开放列表) +- 动画支持 + +### 路径规划编辑器 +提供可视化编辑路径规划场景的功能。 + +主要功能: +- 图形化界面编辑 +- 多种编辑工具 +- 场景生成和管理 +- 算法性能比较 + +## 算法说明 + +### A*算法 +A*是一种启发式搜索算法,结合了Dijkstra算法的准确性和贪心最佳优先搜索的效率。 + +特点: +- 通过启发式函数指导搜索方向 +- 保证找到最优解(在启发式函数可接受的情况下) +- 适用于静态环境 +- 支持多种启发式函数(曼哈顿距离、欧几里得距离、切比雪夫距离等) +- 支持对角线移动和角落切割 +- 支持Tie Breaker优化 + +### Dijkstra算法 +Dijkstra算法是一种用于计算单源最短路径的经典算法。 + +特点: +- 保证找到最短路径 +- 适用于权重图 +- 时间复杂度较高 +- 不需要启发式函数 + +### BFS算法 +广度优先搜索算法,保证找到最短路径(在无权图中)。 + +特点: +- 保证找到最短路径 +- 适用于无权图 +- 内存使用较多 +- 实现简单 + +### DFS算法 +深度优先搜索算法,不保证找到最短路径,但可能更快找到任意路径。 + +特点: +- 可能更快找到路径 +- 不保证最优解 +- 内存使用较少 +- 支持深度限制 + +### JPS算法 +Jump Point Search是一种优化的A*算法,专门用于网格地图。 + +特点: +- 针对网格地图优化 +- 比传统A*更快 +- 保持A*的最优性 +- 利用网格特性跳过大量不必要的节点 +- 适用于大型网格地图 + +## 场景生成 + +### 简单场景 +空的网格地图,适用于基本测试。 + +### 迷宫场景 +使用深度优先搜索算法生成的迷宫。 + +### 房间场景 +包含多个房间和走廊的复杂场景。 + +### 随机场景 +随机生成障碍物的场景。 + +### 隧道场景 +多条曲折隧道的场景。 + +## 性能优化 + +### 算法优化 +- **启发式函数**: 多种启发式函数可选,适应不同场景 +- **启发式权重**: 可调整权重平衡搜索速度和最优性 +- **对角线移动**: 支持对角线移动提高效率 +- **Tie Breaker**: 使用tie breaker优化打破平局 +- **JPS优化**: 利用跳点搜索跳过大量节点 + +### 内存管理 +- **对象复用**: 减少对象创建和销毁开销 +- **批量处理**: 提高大量数据处理效率 +- **缓存机制**: 缓存性能数据供后续分析 + +### 渲染优化 +- **LOD支持**: 根据距离调整显示细节 +- **视锥剔除**: 只渲染可见区域 +- **批处理**: 合并相似的渲染调用 +- **透明度优化**: 优化透明节点的渲染 + +## 扩展开发 + +### 添加新的路径规划算法 +1. 在[algorithms](file:///home/hello/EG/plugins/user/pathfinding_algorithms/algorithms)目录中创建新的算法文件 +2. 实现算法类,确保有find_path方法 +3. 在[algorithm_manager.py](file:///home/hello/EG/plugins/user/pathfinding_algorithms/core/algorithm_manager.py)中注册新算法 + +### 添加新的场景生成器 +1. 在[utils/demo_scenarios.py](file:///home/hello/EG/plugins/user/pathfinding_algorithms/utils/demo_scenarios.py)中实现新的场景生成函数 +2. 在编辑器中添加对应的生成选项 + +### 添加新的可视化效果 +1. 在[visualization_manager.py](file:///home/hello/EG/plugins/user/pathfinding_algorithms/visualization/visualization_manager.py)中实现新的可视化方法 +2. 在编辑器中添加对应的控制选项 + +## 故障排除 + +### 路径查找失败 +1. 检查起点和终点是否在可通行区域内 +2. 确认网格数据格式是否正确 +3. 检查算法参数设置 +4. 使用数据验证功能检查网格有效性 + +### 性能问题 +1. 使用算法比较功能分析性能瓶颈 +2. 调整启发式权重优化搜索效率 +3. 简化网格结构 +4. 对于大型网格考虑使用JPS算法 + +### 可视化问题 +1. 确认GUI系统是否正常工作 +2. 检查可视化参数设置 +3. 重新启用插件尝试 + +## API参考 + +### Plugin类主要方法 + +#### 初始化和控制 +- `initialize()` - 初始化插件 +- `enable()` - 启用插件 +- `disable()` - 禁用插件 +- `finalize()` - 清理插件资源 +- `update(dt)` - 更新插件状态 + +#### 路径查找 +- `find_path(algorithm, grid, start, goal)` - 查找路径 +- `add_algorithm(name, algorithm_class, description)` - 添加新算法 +- `remove_algorithm(name)` - 移除算法 +- `get_available_algorithms()` - 获取可用算法列表 +- `get_algorithm_description(algorithm)` - 获取算法描述 +- `set_algorithm_parameter(algorithm, parameter, value)` - 设置算法参数 +- `get_algorithm_parameters(algorithm)` - 获取算法参数 + +#### 可视化 +- `visualize_path(path, grid)` - 可视化路径 +- `visualize_grid(grid)` - 可视化网格 +- `visualize_visited_nodes(visited_nodes)` - 可视化已访问节点 +- `visualize_open_list(open_list)` - 可视化开放列表 +- `clear_path_visualization()` - 清除路径可视化 +- `set_visualization_settings(settings)` - 设置可视化参数 +- `get_visualization_settings()` - 获取可视化参数 +- `enable_visualization_animation(enable)` - 启用可视化动画 + +#### 性能和统计 +- `compare_algorithms()` - 比较算法性能 +- `show_stats()` - 显示统计信息 +- `generate_report()` - 生成报告 +- `batch_test_algorithms(algorithms, test_cases)` - 批量测试算法 +- `compare_algorithms_detailed(algorithms, grid, start, goal)` - 详细比较算法 +- `get_cached_performance_data(algorithm)` - 获取缓存的性能数据 +- `clear_performance_cache()` - 清除性能缓存 +- `reset_statistics()` - 重置统计信息 +- `validate_grid_data(grid)` - 验证网格数据 +- `get_algorithm_complexity(algorithm)` - 获取算法复杂度信息 +- `benchmark_algorithm(algorithm, grid, start, goal, iterations)` - 基准测试算法 +- `optimize_algorithm_for_grid(algorithm, grid)` - 为特定网格优化算法 + +#### 编辑器功能 +- `toggle_editor()` - 切换编辑器显示 +- `toggle_visualization()` - 切换可视化显示 +- `run_demo()` - 运行演示 +- `clear_visualization()` - 清除可视化 +- `generate_random_scenario()` - 生成随机场景 +- `toggle_animation()` - 切换动画 +- `show_algorithm_info()` - 显示算法信息 +- `export_detailed_report()` - 导出详细报告 + +## 性能调优建议 + +### 算法选择 +1. 对于需要最优解的场景,使用A*或Dijkstra算法 +2. 对于只需要找到任意路径的场景,使用BFS或DFS算法 +3. 对于大型网格地图,使用JPS算法 +4. 根据场景特点使用算法推荐功能 + +### 参数调整 +1. 合理设置启发式权重(通常1.0为最佳) +2. 根据场景特点选择合适的启发式函数 +3. 对于DFS算法,合理设置深度限制 +4. 对于A*算法,考虑启用Tie Breaker优化 + +### 场景优化 +1. 简化不必要的障碍物 +2. 减小网格尺寸 +3. 使用分层路径规划处理大场景 +4. 对于静态环境,考虑预计算路径 + +## 版本信息 + +- 版本: 1.0.0 +- 作者: EG Team +- 发布日期: 2024年 + +## 许可证 + 本插件遵循EG引擎许可证协议。 \ No newline at end of file diff --git a/plugins/user/pathfinding_algorithms/algorithms/astar.py b/plugins/user/pathfinding_algorithms/algorithms/astar.py index fc0b5ce1..f40fe16b 100644 --- a/plugins/user/pathfinding_algorithms/algorithms/astar.py +++ b/plugins/user/pathfinding_algorithms/algorithms/astar.py @@ -1,185 +1,185 @@ -""" -A*算法实现 -A*是一种启发式搜索算法,广泛用于路径规划 -""" - -import heapq -import math -from typing import List, Tuple, Optional, Dict, Set - -class AStar: - """ - A*算法实现 - A*是一种启发式搜索算法,广泛用于路径规划 - """ - - def __init__(self): - """初始化A*算法""" - self.heuristic = 'manhattan' # 默认启发式函数 - self.stats = { - 'nodes_visited': 0, - 'max_queue_size': 0 - } - - def find_path(self, grid: List[List[int]], start: Tuple[int, int], goal: Tuple[int, int]) -> Optional[List[Tuple[int, int]]]: - """ - 使用A*算法查找路径 - - Args: - grid: 网格地图,0表示可通行,1表示障碍物 - start: 起点坐标 (row, col) - goal: 终点坐标 (row, col) - - Returns: - 路径坐标列表,如果找不到路径则返回None - """ - # 重置统计信息 - self.stats = { - 'nodes_visited': 0, - 'max_queue_size': 0 - } - - # 获取网格尺寸 - rows = len(grid) - cols = len(grid[0]) if rows > 0 else 0 - - # 检查起点和终点是否有效 - if not (0 <= start[0] < rows and 0 <= start[1] < cols): - return None - if not (0 <= goal[0] < rows and 0 <= goal[1] < cols): - return None - if grid[start[0]][start[1]] == 1 or grid[goal[0]][goal[1]] == 1: - return None - - # 初始化开放列表(优先队列) - open_list = [] - heapq.heappush(open_list, (0, start)) - - # 初始化关闭列表 - closed_list: Set[Tuple[int, int]] = set() - - # 初始化距离和父节点字典 - g_score: Dict[Tuple[int, int], float] = {start: 0} - f_score: Dict[Tuple[int, int], float] = {start: self._calculate_heuristic(start, goal)} - parent: Dict[Tuple[int, int], Tuple[int, int]] = {} - - # 8方向移动(包括对角线) - directions = [ - (-1, 0), (1, 0), (0, -1), (0, 1), # 上下左右 - (-1, -1), (-1, 1), (1, -1), (1, 1) # 对角线 - ] - - while open_list: - # 更新统计信息 - self.stats['nodes_visited'] += 1 - self.stats['max_queue_size'] = max(self.stats['max_queue_size'], len(open_list)) - - # 取出f_score最小的节点 - current = heapq.heappop(open_list)[1] - - # 如果到达目标节点 - if current == goal: - # 重构路径 - path = self._reconstruct_path(parent, current) - return path - - # 将当前节点加入关闭列表 - closed_list.add(current) - - # 检查所有邻居节点 - for dr, dc in directions: - neighbor = (current[0] + dr, current[1] + dc) - - # 检查邻居节点是否在网格范围内 - if not (0 <= neighbor[0] < rows and 0 <= neighbor[1] < cols): - continue - - # 检查邻居节点是否为障碍物 - if grid[neighbor[0]][neighbor[1]] == 1: - continue - - # 检查邻居节点是否已在关闭列表中 - if neighbor in closed_list: - continue - - # 计算移动代价 - if abs(dr) + abs(dc) == 2: # 对角线移动 - move_cost = 1.414 # √2 - else: # 直线移动 - move_cost = 1.0 - - # 计算到邻居节点的 tentative_g_score - tentative_g_score = g_score[current] + move_cost - - # 如果找到更优路径或首次访问该节点 - if neighbor not in g_score or tentative_g_score < g_score[neighbor]: - # 更新父节点和得分 - parent[neighbor] = current - g_score[neighbor] = tentative_g_score - f_score[neighbor] = g_score[neighbor] + self._calculate_heuristic(neighbor, goal) - - # 将邻居节点加入开放列表 - heapq.heappush(open_list, (f_score[neighbor], neighbor)) - - # 未找到路径 - return None - - def _calculate_heuristic(self, point1: Tuple[int, int], point2: Tuple[int, int]) -> float: - """ - 计算启发式函数值 - - Args: - point1: 第一个点 - point2: 第二个点 - - Returns: - 启发式函数值 - """ - if self.heuristic == 'manhattan': - # 曼哈顿距离 - return abs(point1[0] - point2[0]) + abs(point1[1] - point2[1]) - elif self.heuristic == 'euclidean': - # 欧几里得距离 - return math.sqrt((point1[0] - point2[0])**2 + (point1[1] - point2[1])**2) - elif self.heuristic == 'chebyshev': - # 切比雪夫距离 - return max(abs(point1[0] - point2[0]), abs(point1[1] - point2[1])) - else: - # 默认使用曼哈顿距离 - return abs(point1[0] - point2[0]) + abs(point1[1] - point2[1]) - - def _reconstruct_path(self, parent: Dict[Tuple[int, int], Tuple[int, int]], current: Tuple[int, int]) -> List[Tuple[int, int]]: - """ - 重构路径 - - Args: - parent: 父节点字典 - current: 当前节点 - - Returns: - 路径坐标列表 - """ - path = [current] - while current in parent: - current = parent[current] - path.append(current) - path.reverse() - return path - - def get_stats(self) -> Dict[str, int]: - """ - 获取算法统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def set_heuristic(self, heuristic: str): - """ - 设置启发式函数 - - Args: - heuristic: 启发式函数名称 ('manhattan', 'euclidean', 'chebyshev') - """ +""" +A*算法实现 +A*是一种启发式搜索算法,广泛用于路径规划 +""" + +import heapq +import math +from typing import List, Tuple, Optional, Dict, Set + +class AStar: + """ + A*算法实现 + A*是一种启发式搜索算法,广泛用于路径规划 + """ + + def __init__(self): + """初始化A*算法""" + self.heuristic = 'manhattan' # 默认启发式函数 + self.stats = { + 'nodes_visited': 0, + 'max_queue_size': 0 + } + + def find_path(self, grid: List[List[int]], start: Tuple[int, int], goal: Tuple[int, int]) -> Optional[List[Tuple[int, int]]]: + """ + 使用A*算法查找路径 + + Args: + grid: 网格地图,0表示可通行,1表示障碍物 + start: 起点坐标 (row, col) + goal: 终点坐标 (row, col) + + Returns: + 路径坐标列表,如果找不到路径则返回None + """ + # 重置统计信息 + self.stats = { + 'nodes_visited': 0, + 'max_queue_size': 0 + } + + # 获取网格尺寸 + rows = len(grid) + cols = len(grid[0]) if rows > 0 else 0 + + # 检查起点和终点是否有效 + if not (0 <= start[0] < rows and 0 <= start[1] < cols): + return None + if not (0 <= goal[0] < rows and 0 <= goal[1] < cols): + return None + if grid[start[0]][start[1]] == 1 or grid[goal[0]][goal[1]] == 1: + return None + + # 初始化开放列表(优先队列) + open_list = [] + heapq.heappush(open_list, (0, start)) + + # 初始化关闭列表 + closed_list: Set[Tuple[int, int]] = set() + + # 初始化距离和父节点字典 + g_score: Dict[Tuple[int, int], float] = {start: 0} + f_score: Dict[Tuple[int, int], float] = {start: self._calculate_heuristic(start, goal)} + parent: Dict[Tuple[int, int], Tuple[int, int]] = {} + + # 8方向移动(包括对角线) + directions = [ + (-1, 0), (1, 0), (0, -1), (0, 1), # 上下左右 + (-1, -1), (-1, 1), (1, -1), (1, 1) # 对角线 + ] + + while open_list: + # 更新统计信息 + self.stats['nodes_visited'] += 1 + self.stats['max_queue_size'] = max(self.stats['max_queue_size'], len(open_list)) + + # 取出f_score最小的节点 + current = heapq.heappop(open_list)[1] + + # 如果到达目标节点 + if current == goal: + # 重构路径 + path = self._reconstruct_path(parent, current) + return path + + # 将当前节点加入关闭列表 + closed_list.add(current) + + # 检查所有邻居节点 + for dr, dc in directions: + neighbor = (current[0] + dr, current[1] + dc) + + # 检查邻居节点是否在网格范围内 + if not (0 <= neighbor[0] < rows and 0 <= neighbor[1] < cols): + continue + + # 检查邻居节点是否为障碍物 + if grid[neighbor[0]][neighbor[1]] == 1: + continue + + # 检查邻居节点是否已在关闭列表中 + if neighbor in closed_list: + continue + + # 计算移动代价 + if abs(dr) + abs(dc) == 2: # 对角线移动 + move_cost = 1.414 # √2 + else: # 直线移动 + move_cost = 1.0 + + # 计算到邻居节点的 tentative_g_score + tentative_g_score = g_score[current] + move_cost + + # 如果找到更优路径或首次访问该节点 + if neighbor not in g_score or tentative_g_score < g_score[neighbor]: + # 更新父节点和得分 + parent[neighbor] = current + g_score[neighbor] = tentative_g_score + f_score[neighbor] = g_score[neighbor] + self._calculate_heuristic(neighbor, goal) + + # 将邻居节点加入开放列表 + heapq.heappush(open_list, (f_score[neighbor], neighbor)) + + # 未找到路径 + return None + + def _calculate_heuristic(self, point1: Tuple[int, int], point2: Tuple[int, int]) -> float: + """ + 计算启发式函数值 + + Args: + point1: 第一个点 + point2: 第二个点 + + Returns: + 启发式函数值 + """ + if self.heuristic == 'manhattan': + # 曼哈顿距离 + return abs(point1[0] - point2[0]) + abs(point1[1] - point2[1]) + elif self.heuristic == 'euclidean': + # 欧几里得距离 + return math.sqrt((point1[0] - point2[0])**2 + (point1[1] - point2[1])**2) + elif self.heuristic == 'chebyshev': + # 切比雪夫距离 + return max(abs(point1[0] - point2[0]), abs(point1[1] - point2[1])) + else: + # 默认使用曼哈顿距离 + return abs(point1[0] - point2[0]) + abs(point1[1] - point2[1]) + + def _reconstruct_path(self, parent: Dict[Tuple[int, int], Tuple[int, int]], current: Tuple[int, int]) -> List[Tuple[int, int]]: + """ + 重构路径 + + Args: + parent: 父节点字典 + current: 当前节点 + + Returns: + 路径坐标列表 + """ + path = [current] + while current in parent: + current = parent[current] + path.append(current) + path.reverse() + return path + + def get_stats(self) -> Dict[str, int]: + """ + 获取算法统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def set_heuristic(self, heuristic: str): + """ + 设置启发式函数 + + Args: + heuristic: 启发式函数名称 ('manhattan', 'euclidean', 'chebyshev') + """ self.heuristic = heuristic \ No newline at end of file diff --git a/plugins/user/pathfinding_algorithms/algorithms/bfs.py b/plugins/user/pathfinding_algorithms/algorithms/bfs.py index d17860f4..cf5fe5e6 100644 --- a/plugins/user/pathfinding_algorithms/algorithms/bfs.py +++ b/plugins/user/pathfinding_algorithms/algorithms/bfs.py @@ -1,125 +1,125 @@ -""" -BFS算法实现 -广度优先搜索算法,保证找到最短路径(在无权图中) -""" - -from collections import deque -from typing import List, Tuple, Optional, Dict, Set - -class BFS: - """ - BFS算法实现 - 广度优先搜索算法,保证找到最短路径(在无权图中) - """ - - def __init__(self): - """初始化BFS算法""" - self.stats = { - 'nodes_visited': 0, - 'max_queue_size': 0 - } - - def find_path(self, grid: List[List[int]], start: Tuple[int, int], goal: Tuple[int, int]) -> Optional[List[Tuple[int, int]]]: - """ - 使用BFS算法查找路径 - - Args: - grid: 网格地图,0表示可通行,1表示障碍物 - start: 起点坐标 (row, col) - goal: 终点坐标 (row, col) - - Returns: - 路径坐标列表,如果找不到路径则返回None - """ - # 重置统计信息 - self.stats = { - 'nodes_visited': 0, - 'max_queue_size': 0 - } - - # 获取网格尺寸 - rows = len(grid) - cols = len(grid[0]) if rows > 0 else 0 - - # 检查起点和终点是否有效 - if not (0 <= start[0] < rows and 0 <= start[1] < cols): - return None - if not (0 <= goal[0] < rows and 0 <= goal[1] < cols): - return None - if grid[start[0]][start[1]] == 1 or grid[goal[0]][goal[1]] == 1: - return None - - # 初始化队列 - queue = deque([start]) - - # 初始化父节点字典和访问集合 - parent: Dict[Tuple[int, int], Tuple[int, int]] = {} - visited: Set[Tuple[int, int]] = {start} - - # 4方向移动(上下左右) - directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] - - while queue: - # 更新统计信息 - self.stats['nodes_visited'] += 1 - self.stats['max_queue_size'] = max(self.stats['max_queue_size'], len(queue)) - - # 取出队列中的第一个节点 - current = queue.popleft() - - # 如果到达目标节点 - if current == goal: - # 重构路径 - path = self._reconstruct_path(parent, current) - return path - - # 检查所有邻居节点 - for dr, dc in directions: - neighbor = (current[0] + dr, current[1] + dc) - - # 检查邻居节点是否在网格范围内 - if not (0 <= neighbor[0] < rows and 0 <= neighbor[1] < cols): - continue - - # 检查邻居节点是否为障碍物 - if grid[neighbor[0]][neighbor[1]] == 1: - continue - - # 检查邻居节点是否已访问 - if neighbor in visited: - continue - - # 标记为已访问并加入队列 - visited.add(neighbor) - parent[neighbor] = current - queue.append(neighbor) - - # 未找到路径 - return None - - def _reconstruct_path(self, parent: Dict[Tuple[int, int], Tuple[int, int]], current: Tuple[int, int]) -> List[Tuple[int, int]]: - """ - 重构路径 - - Args: - parent: 父节点字典 - current: 当前节点 - - Returns: - 路径坐标列表 - """ - path = [current] - while current in parent: - current = parent[current] - path.append(current) - path.reverse() - return path - - def get_stats(self) -> Dict[str, int]: - """ - 获取算法统计信息 - - Returns: - 统计信息字典 - """ +""" +BFS算法实现 +广度优先搜索算法,保证找到最短路径(在无权图中) +""" + +from collections import deque +from typing import List, Tuple, Optional, Dict, Set + +class BFS: + """ + BFS算法实现 + 广度优先搜索算法,保证找到最短路径(在无权图中) + """ + + def __init__(self): + """初始化BFS算法""" + self.stats = { + 'nodes_visited': 0, + 'max_queue_size': 0 + } + + def find_path(self, grid: List[List[int]], start: Tuple[int, int], goal: Tuple[int, int]) -> Optional[List[Tuple[int, int]]]: + """ + 使用BFS算法查找路径 + + Args: + grid: 网格地图,0表示可通行,1表示障碍物 + start: 起点坐标 (row, col) + goal: 终点坐标 (row, col) + + Returns: + 路径坐标列表,如果找不到路径则返回None + """ + # 重置统计信息 + self.stats = { + 'nodes_visited': 0, + 'max_queue_size': 0 + } + + # 获取网格尺寸 + rows = len(grid) + cols = len(grid[0]) if rows > 0 else 0 + + # 检查起点和终点是否有效 + if not (0 <= start[0] < rows and 0 <= start[1] < cols): + return None + if not (0 <= goal[0] < rows and 0 <= goal[1] < cols): + return None + if grid[start[0]][start[1]] == 1 or grid[goal[0]][goal[1]] == 1: + return None + + # 初始化队列 + queue = deque([start]) + + # 初始化父节点字典和访问集合 + parent: Dict[Tuple[int, int], Tuple[int, int]] = {} + visited: Set[Tuple[int, int]] = {start} + + # 4方向移动(上下左右) + directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] + + while queue: + # 更新统计信息 + self.stats['nodes_visited'] += 1 + self.stats['max_queue_size'] = max(self.stats['max_queue_size'], len(queue)) + + # 取出队列中的第一个节点 + current = queue.popleft() + + # 如果到达目标节点 + if current == goal: + # 重构路径 + path = self._reconstruct_path(parent, current) + return path + + # 检查所有邻居节点 + for dr, dc in directions: + neighbor = (current[0] + dr, current[1] + dc) + + # 检查邻居节点是否在网格范围内 + if not (0 <= neighbor[0] < rows and 0 <= neighbor[1] < cols): + continue + + # 检查邻居节点是否为障碍物 + if grid[neighbor[0]][neighbor[1]] == 1: + continue + + # 检查邻居节点是否已访问 + if neighbor in visited: + continue + + # 标记为已访问并加入队列 + visited.add(neighbor) + parent[neighbor] = current + queue.append(neighbor) + + # 未找到路径 + return None + + def _reconstruct_path(self, parent: Dict[Tuple[int, int], Tuple[int, int]], current: Tuple[int, int]) -> List[Tuple[int, int]]: + """ + 重构路径 + + Args: + parent: 父节点字典 + current: 当前节点 + + Returns: + 路径坐标列表 + """ + path = [current] + while current in parent: + current = parent[current] + path.append(current) + path.reverse() + return path + + def get_stats(self) -> Dict[str, int]: + """ + 获取算法统计信息 + + Returns: + 统计信息字典 + """ return self.stats.copy() \ No newline at end of file diff --git a/plugins/user/pathfinding_algorithms/algorithms/dfs.py b/plugins/user/pathfinding_algorithms/algorithms/dfs.py index 58160ae8..16dc4651 100644 --- a/plugins/user/pathfinding_algorithms/algorithms/dfs.py +++ b/plugins/user/pathfinding_algorithms/algorithms/dfs.py @@ -1,139 +1,139 @@ -""" -DFS算法实现 -深度优先搜索算法,不保证找到最短路径,但可能更快找到任意路径 -""" - -from collections import deque -from typing import List, Tuple, Optional, Dict, Set - -class DFS: - """ - DFS算法实现 - 深度优先搜索算法,不保证找到最短路径,但可能更快找到任意路径 - """ - - def __init__(self): - """初始化DFS算法""" - self.depth_limit = 1000 # 深度限制,防止无限递归 - self.stats = { - 'nodes_visited': 0, - 'max_depth': 0 - } - - def find_path(self, grid: List[List[int]], start: Tuple[int, int], goal: Tuple[int, int]) -> Optional[List[Tuple[int, int]]]: - """ - 使用DFS算法查找路径 - - Args: - grid: 网格地图,0表示可通行,1表示障碍物 - start: 起点坐标 (row, col) - goal: 终点坐标 (row, col) - - Returns: - 路径坐标列表,如果找不到路径则返回None - """ - # 重置统计信息 - self.stats = { - 'nodes_visited': 0, - 'max_depth': 0 - } - - # 获取网格尺寸 - rows = len(grid) - cols = len(grid[0]) if rows > 0 else 0 - - # 检查起点和终点是否有效 - if not (0 <= start[0] < rows and 0 <= start[1] < cols): - return None - if not (0 <= goal[0] < rows and 0 <= goal[1] < cols): - return None - if grid[start[0]][start[1]] == 1 or grid[goal[0]][goal[1]] == 1: - return None - - # 初始化栈 - stack = [(start, 0)] # (节点, 深度) - - # 初始化父节点字典和访问集合 - parent: Dict[Tuple[int, int], Tuple[int, int]] = {} - visited: Set[Tuple[int, int]] = {start} - - # 4方向移动(上下左右) - directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] - - while stack: - # 更新统计信息 - self.stats['nodes_visited'] += 1 - - # 取出栈顶节点 - current, depth = stack.pop() - self.stats['max_depth'] = max(self.stats['max_depth'], depth) - - # 如果超过深度限制,跳过 - if depth >= self.depth_limit: - continue - - # 如果到达目标节点 - if current == goal: - # 重构路径 - path = self._reconstruct_path(parent, current) - return path - - # 检查所有邻居节点 - for dr, dc in directions: - neighbor = (current[0] + dr, current[1] + dc) - - # 检查邻居节点是否在网格范围内 - if not (0 <= neighbor[0] < rows and 0 <= neighbor[1] < cols): - continue - - # 检查邻居节点是否为障碍物 - if grid[neighbor[0]][neighbor[1]] == 1: - continue - - # 检查邻居节点是否已访问 - if neighbor in visited: - continue - - # 标记为已访问并加入栈 - visited.add(neighbor) - parent[neighbor] = current - stack.append((neighbor, depth + 1)) - - # 未找到路径 - return None - - def _reconstruct_path(self, parent: Dict[Tuple[int, int], Tuple[int, int]], current: Tuple[int, int]) -> List[Tuple[int, int]]: - """ - 重构路径 - - Args: - parent: 父节点字典 - current: 当前节点 - - Returns: - 路径坐标列表 - """ - path = [current] - while current in parent: - current = parent[current] - path.append(current) - path.reverse() - return path - - def get_stats(self) -> Dict[str, int]: - """ - 获取算法统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def set_depth_limit(self, limit: int): - """ - 设置深度限制 - - Args: - limit: 深度限制 - """ +""" +DFS算法实现 +深度优先搜索算法,不保证找到最短路径,但可能更快找到任意路径 +""" + +from collections import deque +from typing import List, Tuple, Optional, Dict, Set + +class DFS: + """ + DFS算法实现 + 深度优先搜索算法,不保证找到最短路径,但可能更快找到任意路径 + """ + + def __init__(self): + """初始化DFS算法""" + self.depth_limit = 1000 # 深度限制,防止无限递归 + self.stats = { + 'nodes_visited': 0, + 'max_depth': 0 + } + + def find_path(self, grid: List[List[int]], start: Tuple[int, int], goal: Tuple[int, int]) -> Optional[List[Tuple[int, int]]]: + """ + 使用DFS算法查找路径 + + Args: + grid: 网格地图,0表示可通行,1表示障碍物 + start: 起点坐标 (row, col) + goal: 终点坐标 (row, col) + + Returns: + 路径坐标列表,如果找不到路径则返回None + """ + # 重置统计信息 + self.stats = { + 'nodes_visited': 0, + 'max_depth': 0 + } + + # 获取网格尺寸 + rows = len(grid) + cols = len(grid[0]) if rows > 0 else 0 + + # 检查起点和终点是否有效 + if not (0 <= start[0] < rows and 0 <= start[1] < cols): + return None + if not (0 <= goal[0] < rows and 0 <= goal[1] < cols): + return None + if grid[start[0]][start[1]] == 1 or grid[goal[0]][goal[1]] == 1: + return None + + # 初始化栈 + stack = [(start, 0)] # (节点, 深度) + + # 初始化父节点字典和访问集合 + parent: Dict[Tuple[int, int], Tuple[int, int]] = {} + visited: Set[Tuple[int, int]] = {start} + + # 4方向移动(上下左右) + directions = [(-1, 0), (1, 0), (0, -1), (0, 1)] + + while stack: + # 更新统计信息 + self.stats['nodes_visited'] += 1 + + # 取出栈顶节点 + current, depth = stack.pop() + self.stats['max_depth'] = max(self.stats['max_depth'], depth) + + # 如果超过深度限制,跳过 + if depth >= self.depth_limit: + continue + + # 如果到达目标节点 + if current == goal: + # 重构路径 + path = self._reconstruct_path(parent, current) + return path + + # 检查所有邻居节点 + for dr, dc in directions: + neighbor = (current[0] + dr, current[1] + dc) + + # 检查邻居节点是否在网格范围内 + if not (0 <= neighbor[0] < rows and 0 <= neighbor[1] < cols): + continue + + # 检查邻居节点是否为障碍物 + if grid[neighbor[0]][neighbor[1]] == 1: + continue + + # 检查邻居节点是否已访问 + if neighbor in visited: + continue + + # 标记为已访问并加入栈 + visited.add(neighbor) + parent[neighbor] = current + stack.append((neighbor, depth + 1)) + + # 未找到路径 + return None + + def _reconstruct_path(self, parent: Dict[Tuple[int, int], Tuple[int, int]], current: Tuple[int, int]) -> List[Tuple[int, int]]: + """ + 重构路径 + + Args: + parent: 父节点字典 + current: 当前节点 + + Returns: + 路径坐标列表 + """ + path = [current] + while current in parent: + current = parent[current] + path.append(current) + path.reverse() + return path + + def get_stats(self) -> Dict[str, int]: + """ + 获取算法统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def set_depth_limit(self, limit: int): + """ + 设置深度限制 + + Args: + limit: 深度限制 + """ self.depth_limit = limit \ No newline at end of file diff --git a/plugins/user/pathfinding_algorithms/algorithms/dijkstra.py b/plugins/user/pathfinding_algorithms/algorithms/dijkstra.py index d5ffb49d..4fe22121 100644 --- a/plugins/user/pathfinding_algorithms/algorithms/dijkstra.py +++ b/plugins/user/pathfinding_algorithms/algorithms/dijkstra.py @@ -1,148 +1,148 @@ -""" -Dijkstra算法实现 -Dijkstra算法是一种用于计算单源最短路径的经典算法 -""" - -import heapq -from typing import List, Tuple, Optional, Dict, Set - -class Dijkstra: - """ - Dijkstra算法实现 - Dijkstra算法是一种用于计算单源最短路径的经典算法 - """ - - def __init__(self): - """初始化Dijkstra算法""" - self.stats = { - 'nodes_visited': 0, - 'max_queue_size': 0 - } - - def find_path(self, grid: List[List[int]], start: Tuple[int, int], goal: Tuple[int, int]) -> Optional[List[Tuple[int, int]]]: - """ - 使用Dijkstra算法查找路径 - - Args: - grid: 网格地图,0表示可通行,1表示障碍物 - start: 起点坐标 (row, col) - goal: 终点坐标 (row, col) - - Returns: - 路径坐标列表,如果找不到路径则返回None - """ - # 重置统计信息 - self.stats = { - 'nodes_visited': 0, - 'max_queue_size': 0 - } - - # 获取网格尺寸 - rows = len(grid) - cols = len(grid[0]) if rows > 0 else 0 - - # 检查起点和终点是否有效 - if not (0 <= start[0] < rows and 0 <= start[1] < cols): - return None - if not (0 <= goal[0] < rows and 0 <= goal[1] < cols): - return None - if grid[start[0]][start[1]] == 1 or grid[goal[0]][goal[1]] == 1: - return None - - # 初始化优先队列 - priority_queue = [] - heapq.heappush(priority_queue, (0, start)) - - # 初始化距离和父节点字典 - distances: Dict[Tuple[int, int], float] = {start: 0} - parent: Dict[Tuple[int, int], Tuple[int, int]] = {} - visited: Set[Tuple[int, int]] = set() - - # 8方向移动(包括对角线) - directions = [ - (-1, 0), (1, 0), (0, -1), (0, 1), # 上下左右 - (-1, -1), (-1, 1), (1, -1), (1, 1) # 对角线 - ] - - while priority_queue: - # 更新统计信息 - self.stats['nodes_visited'] += 1 - self.stats['max_queue_size'] = max(self.stats['max_queue_size'], len(priority_queue)) - - # 取出距离最小的节点 - current_distance, current = heapq.heappop(priority_queue) - - # 如果已经访问过,跳过 - if current in visited: - continue - - # 标记为已访问 - visited.add(current) - - # 如果到达目标节点 - if current == goal: - # 重构路径 - path = self._reconstruct_path(parent, current) - return path - - # 检查所有邻居节点 - for dr, dc in directions: - neighbor = (current[0] + dr, current[1] + dc) - - # 检查邻居节点是否在网格范围内 - if not (0 <= neighbor[0] < rows and 0 <= neighbor[1] < cols): - continue - - # 检查邻居节点是否为障碍物 - if grid[neighbor[0]][neighbor[1]] == 1: - continue - - # 检查邻居节点是否已访问 - if neighbor in visited: - continue - - # 计算移动代价 - if abs(dr) + abs(dc) == 2: # 对角线移动 - move_cost = 1.414 # √2 - else: # 直线移动 - move_cost = 1.0 - - # 计算到邻居节点的距离 - distance = current_distance + move_cost - - # 如果找到更短路径 - if neighbor not in distances or distance < distances[neighbor]: - # 更新距离和父节点 - distances[neighbor] = distance - parent[neighbor] = current - heapq.heappush(priority_queue, (distance, neighbor)) - - # 未找到路径 - return None - - def _reconstruct_path(self, parent: Dict[Tuple[int, int], Tuple[int, int]], current: Tuple[int, int]) -> List[Tuple[int, int]]: - """ - 重构路径 - - Args: - parent: 父节点字典 - current: 当前节点 - - Returns: - 路径坐标列表 - """ - path = [current] - while current in parent: - current = parent[current] - path.append(current) - path.reverse() - return path - - def get_stats(self) -> Dict[str, int]: - """ - 获取算法统计信息 - - Returns: - 统计信息字典 - """ +""" +Dijkstra算法实现 +Dijkstra算法是一种用于计算单源最短路径的经典算法 +""" + +import heapq +from typing import List, Tuple, Optional, Dict, Set + +class Dijkstra: + """ + Dijkstra算法实现 + Dijkstra算法是一种用于计算单源最短路径的经典算法 + """ + + def __init__(self): + """初始化Dijkstra算法""" + self.stats = { + 'nodes_visited': 0, + 'max_queue_size': 0 + } + + def find_path(self, grid: List[List[int]], start: Tuple[int, int], goal: Tuple[int, int]) -> Optional[List[Tuple[int, int]]]: + """ + 使用Dijkstra算法查找路径 + + Args: + grid: 网格地图,0表示可通行,1表示障碍物 + start: 起点坐标 (row, col) + goal: 终点坐标 (row, col) + + Returns: + 路径坐标列表,如果找不到路径则返回None + """ + # 重置统计信息 + self.stats = { + 'nodes_visited': 0, + 'max_queue_size': 0 + } + + # 获取网格尺寸 + rows = len(grid) + cols = len(grid[0]) if rows > 0 else 0 + + # 检查起点和终点是否有效 + if not (0 <= start[0] < rows and 0 <= start[1] < cols): + return None + if not (0 <= goal[0] < rows and 0 <= goal[1] < cols): + return None + if grid[start[0]][start[1]] == 1 or grid[goal[0]][goal[1]] == 1: + return None + + # 初始化优先队列 + priority_queue = [] + heapq.heappush(priority_queue, (0, start)) + + # 初始化距离和父节点字典 + distances: Dict[Tuple[int, int], float] = {start: 0} + parent: Dict[Tuple[int, int], Tuple[int, int]] = {} + visited: Set[Tuple[int, int]] = set() + + # 8方向移动(包括对角线) + directions = [ + (-1, 0), (1, 0), (0, -1), (0, 1), # 上下左右 + (-1, -1), (-1, 1), (1, -1), (1, 1) # 对角线 + ] + + while priority_queue: + # 更新统计信息 + self.stats['nodes_visited'] += 1 + self.stats['max_queue_size'] = max(self.stats['max_queue_size'], len(priority_queue)) + + # 取出距离最小的节点 + current_distance, current = heapq.heappop(priority_queue) + + # 如果已经访问过,跳过 + if current in visited: + continue + + # 标记为已访问 + visited.add(current) + + # 如果到达目标节点 + if current == goal: + # 重构路径 + path = self._reconstruct_path(parent, current) + return path + + # 检查所有邻居节点 + for dr, dc in directions: + neighbor = (current[0] + dr, current[1] + dc) + + # 检查邻居节点是否在网格范围内 + if not (0 <= neighbor[0] < rows and 0 <= neighbor[1] < cols): + continue + + # 检查邻居节点是否为障碍物 + if grid[neighbor[0]][neighbor[1]] == 1: + continue + + # 检查邻居节点是否已访问 + if neighbor in visited: + continue + + # 计算移动代价 + if abs(dr) + abs(dc) == 2: # 对角线移动 + move_cost = 1.414 # √2 + else: # 直线移动 + move_cost = 1.0 + + # 计算到邻居节点的距离 + distance = current_distance + move_cost + + # 如果找到更短路径 + if neighbor not in distances or distance < distances[neighbor]: + # 更新距离和父节点 + distances[neighbor] = distance + parent[neighbor] = current + heapq.heappush(priority_queue, (distance, neighbor)) + + # 未找到路径 + return None + + def _reconstruct_path(self, parent: Dict[Tuple[int, int], Tuple[int, int]], current: Tuple[int, int]) -> List[Tuple[int, int]]: + """ + 重构路径 + + Args: + parent: 父节点字典 + current: 当前节点 + + Returns: + 路径坐标列表 + """ + path = [current] + while current in parent: + current = parent[current] + path.append(current) + path.reverse() + return path + + def get_stats(self) -> Dict[str, int]: + """ + 获取算法统计信息 + + Returns: + 统计信息字典 + """ return self.stats.copy() \ No newline at end of file diff --git a/plugins/user/pathfinding_algorithms/algorithms/jps.py b/plugins/user/pathfinding_algorithms/algorithms/jps.py index 69c96b37..cb07ad96 100644 --- a/plugins/user/pathfinding_algorithms/algorithms/jps.py +++ b/plugins/user/pathfinding_algorithms/algorithms/jps.py @@ -1,290 +1,290 @@ -""" -JPS算法实现 -Jump Point Search (JPS) 是一种优化的A*算法,专门用于网格地图 -""" - -import heapq -from typing import List, Tuple, Optional, Dict, Set - -class JPS: - """ - JPS算法实现 - Jump Point Search (JPS) 是一种优化的A*算法,专门用于网格地图 - """ - - def __init__(self): - """初始化JPS算法""" - self.stats = { - 'nodes_visited': 0, - 'max_queue_size': 0 - } - - def find_path(self, grid: List[List[int]], start: Tuple[int, int], goal: Tuple[int, int]) -> Optional[List[Tuple[int, int]]]: - """ - 使用JPS算法查找路径 - - Args: - grid: 网格地图,0表示可通行,1表示障碍物 - start: 起点坐标 (row, col) - goal: 终点坐标 (row, col) - - Returns: - 路径坐标列表,如果找不到路径则返回None - """ - # 重置统计信息 - self.stats = { - 'nodes_visited': 0, - 'max_queue_size': 0 - } - - # 获取网格尺寸 - self.rows = len(grid) - self.cols = len(grid[0]) if self.rows > 0 else 0 - self.grid = grid - - # 检查起点和终点是否有效 - if not (0 <= start[0] < self.rows and 0 <= start[1] < self.cols): - return None - if not (0 <= goal[0] < self.rows and 0 <= goal[1] < self.cols): - return None - if grid[start[0]][start[1]] == 1 or grid[goal[0]][goal[1]] == 1: - return None - - # 初始化开放列表(优先队列) - open_list = [] - heapq.heappush(open_list, (0, start)) - - # 初始化关闭列表 - closed_list: Set[Tuple[int, int]] = set() - - # 初始化距离和父节点字典 - g_score: Dict[Tuple[int, int], float] = {start: 0} - f_score: Dict[Tuple[int, int], float] = {start: self._manhattan_distance(start, goal)} - parent: Dict[Tuple[int, int], Tuple[int, int]] = {} - - while open_list: - # 更新统计信息 - self.stats['nodes_visited'] += 1 - self.stats['max_queue_size'] = max(self.stats['max_queue_size'], len(open_list)) - - # 取出f_score最小的节点 - current = heapq.heappop(open_list)[1] - - # 如果到达目标节点 - if current == goal: - # 重构路径 - path = self._reconstruct_path(parent, current) - return path - - # 将当前节点加入关闭列表 - closed_list.add(current) - - # 获取当前节点的跳点邻居 - successors = self._get_jump_point_successors(current, goal, g_score[current], parent) - - # 处理所有跳点邻居 - for successor, move_cost in successors: - if successor in closed_list: - continue - - tentative_g_score = g_score[current] + move_cost - - if successor not in g_score or tentative_g_score < g_score[successor]: - parent[successor] = current - g_score[successor] = tentative_g_score - f_score[successor] = g_score[successor] + self._manhattan_distance(successor, goal) - heapq.heappush(open_list, (f_score[successor], successor)) - - # 未找到路径 - return None - - def _manhattan_distance(self, point1: Tuple[int, int], point2: Tuple[int, int]) -> float: - """计算曼哈顿距离""" - return abs(point1[0] - point2[0]) + abs(point1[1] - point2[1]) - - def _get_jump_point_successors(self, current: Tuple[int, int], goal: Tuple[int, int], - current_g: float, parent: Dict[Tuple[int, int], Tuple[int, int]]) -> List[Tuple[Tuple[int, int], float]]: - """ - 获取跳点邻居 - - Args: - current: 当前节点 - goal: 目标节点 - current_g: 当前节点的g值 - parent: 父节点字典 - - Returns: - 跳点邻居列表 [(节点, 移动代价), ...] - """ - successors = [] - - # 如果当前节点有父节点,则沿着父节点方向寻找跳点 - if current in parent: - parent_node = parent[current] - dx = current[0] - parent_node[0] - dy = current[1] - parent_node[1] - - # 标准化方向 - dx = 1 if dx > 0 else (-1 if dx < 0 else 0) - dy = 1 if dy > 0 else (-1 if dy < 0 else 0) - - # 沿着该方向寻找跳点 - jump_point = self._jump(current[0], current[1], dx, dy, goal) - if jump_point: - move_cost = self._calculate_move_cost(current, jump_point) - successors.append((jump_point, move_cost)) - else: - # 当前节点是起点,检查所有方向 - directions = [ - (-1, 0), (1, 0), (0, -1), (0, 1), # 上下左右 - (-1, -1), (-1, 1), (1, -1), (1, 1) # 对角线 - ] - - for dx, dy in directions: - jump_point = self._jump(current[0], current[1], dx, dy, goal) - if jump_point: - move_cost = self._calculate_move_cost(current, jump_point) - successors.append((jump_point, move_cost)) - - return successors - - def _jump(self, x: int, y: int, dx: int, dy: int, goal: Tuple[int, int]) -> Optional[Tuple[int, int]]: - """ - 沿着给定方向跳跃寻找跳点 - - Args: - x, y: 当前位置 - dx, dy: 跳跃方向 - goal: 目标位置 - - Returns: - 跳点坐标,如果没有找到则返回None - """ - # 计算下一个位置 - next_x = x + dx - next_y = y + dy - - # 检查边界 - if not (0 <= next_x < self.rows and 0 <= next_y < self.cols): - return None - - # 检查障碍物 - if self.grid[next_x][next_y] == 1: - return None - - # 如果到达目标点 - if (next_x, next_y) == goal: - return (next_x, next_y) - - # 检查是否为跳点(对于对角线移动) - if dx != 0 and dy != 0: - # 检查水平和垂直方向是否有强制邻居 - if (self._has_forced_neighbor(next_x, next_y, -dx, dy) or - self._has_forced_neighbor(next_x, next_y, dx, -dy)): - return (next_x, next_y) - # 检查是否为跳点(对于直线移动) - else: - # 检查是否有强制邻居 - if self._has_forced_neighbor(next_x, next_y, dx, dy): - return (next_x, next_y) - - # 如果是直线移动,继续递归跳跃 - if dx == 0 or dy == 0: - return self._jump(next_x, next_y, dx, dy, goal) - # 如果是对角线移动,检查水平和垂直分量 - else: - # 检查对角线方向的跳点 - if (self._jump(next_x, next_y, dx, 0, goal) is not None or - self._jump(next_x, next_y, 0, dy, goal) is not None): - return (next_x, next_y) - # 继续对角线跳跃 - return self._jump(next_x, next_y, dx, dy, goal) - - def _has_forced_neighbor(self, x: int, y: int, dx: int, dy: int) -> bool: - """ - 检查是否存在强制邻居 - - Args: - x, y: 当前位置 - dx, dy: 检查方向 - - Returns: - 是否存在强制邻居 - """ - # 检查对角线方向 - if dx != 0 and dy != 0: - # 检查对角线方向的强制邻居 - if (self._is_blocked(x - dx, y) and not self._is_blocked(x - dx, y + dy)) or \ - (self._is_blocked(x, y - dy) and not self._is_blocked(x + dx, y - dy)): - return True - # 检查直线方向 - else: - # 水平移动 - if dx == 0: - if (self._is_blocked(x - 1, y) and not self._is_blocked(x - 1, y + dy)) or \ - (self._is_blocked(x + 1, y) and not self._is_blocked(x + 1, y + dy)): - return True - # 垂直移动 - else: - if (self._is_blocked(x, y - 1) and not self._is_blocked(x + dx, y - 1)) or \ - (self._is_blocked(x, y + 1) and not self._is_blocked(x + dx, y + 1)): - return True - return False - - def _is_blocked(self, x: int, y: int) -> bool: - """ - 检查位置是否被阻挡 - - Args: - x, y: 位置坐标 - - Returns: - 是否被阻挡 - """ - return not (0 <= x < self.rows and 0 <= y < self.cols) or self.grid[x][y] == 1 - - def _calculate_move_cost(self, point1: Tuple[int, int], point2: Tuple[int, int]) -> float: - """ - 计算移动代价 - - Args: - point1: 起点 - point2: 终点 - - Returns: - 移动代价 - """ - dx = abs(point1[0] - point2[0]) - dy = abs(point1[1] - point2[1]) - - if dx != 0 and dy != 0: # 对角线移动 - return max(dx, dy) * 1.414 - else: # 直线移动 - return max(dx, dy) * 1.0 - - def _reconstruct_path(self, parent: Dict[Tuple[int, int], Tuple[int, int]], current: Tuple[int, int]) -> List[Tuple[int, int]]: - """ - 重构路径 - - Args: - parent: 父节点字典 - current: 当前节点 - - Returns: - 路径坐标列表 - """ - path = [current] - while current in parent: - current = parent[current] - path.append(current) - path.reverse() - return path - - def get_stats(self) -> Dict[str, int]: - """ - 获取算法统计信息 - - Returns: - 统计信息字典 - """ +""" +JPS算法实现 +Jump Point Search (JPS) 是一种优化的A*算法,专门用于网格地图 +""" + +import heapq +from typing import List, Tuple, Optional, Dict, Set + +class JPS: + """ + JPS算法实现 + Jump Point Search (JPS) 是一种优化的A*算法,专门用于网格地图 + """ + + def __init__(self): + """初始化JPS算法""" + self.stats = { + 'nodes_visited': 0, + 'max_queue_size': 0 + } + + def find_path(self, grid: List[List[int]], start: Tuple[int, int], goal: Tuple[int, int]) -> Optional[List[Tuple[int, int]]]: + """ + 使用JPS算法查找路径 + + Args: + grid: 网格地图,0表示可通行,1表示障碍物 + start: 起点坐标 (row, col) + goal: 终点坐标 (row, col) + + Returns: + 路径坐标列表,如果找不到路径则返回None + """ + # 重置统计信息 + self.stats = { + 'nodes_visited': 0, + 'max_queue_size': 0 + } + + # 获取网格尺寸 + self.rows = len(grid) + self.cols = len(grid[0]) if self.rows > 0 else 0 + self.grid = grid + + # 检查起点和终点是否有效 + if not (0 <= start[0] < self.rows and 0 <= start[1] < self.cols): + return None + if not (0 <= goal[0] < self.rows and 0 <= goal[1] < self.cols): + return None + if grid[start[0]][start[1]] == 1 or grid[goal[0]][goal[1]] == 1: + return None + + # 初始化开放列表(优先队列) + open_list = [] + heapq.heappush(open_list, (0, start)) + + # 初始化关闭列表 + closed_list: Set[Tuple[int, int]] = set() + + # 初始化距离和父节点字典 + g_score: Dict[Tuple[int, int], float] = {start: 0} + f_score: Dict[Tuple[int, int], float] = {start: self._manhattan_distance(start, goal)} + parent: Dict[Tuple[int, int], Tuple[int, int]] = {} + + while open_list: + # 更新统计信息 + self.stats['nodes_visited'] += 1 + self.stats['max_queue_size'] = max(self.stats['max_queue_size'], len(open_list)) + + # 取出f_score最小的节点 + current = heapq.heappop(open_list)[1] + + # 如果到达目标节点 + if current == goal: + # 重构路径 + path = self._reconstruct_path(parent, current) + return path + + # 将当前节点加入关闭列表 + closed_list.add(current) + + # 获取当前节点的跳点邻居 + successors = self._get_jump_point_successors(current, goal, g_score[current], parent) + + # 处理所有跳点邻居 + for successor, move_cost in successors: + if successor in closed_list: + continue + + tentative_g_score = g_score[current] + move_cost + + if successor not in g_score or tentative_g_score < g_score[successor]: + parent[successor] = current + g_score[successor] = tentative_g_score + f_score[successor] = g_score[successor] + self._manhattan_distance(successor, goal) + heapq.heappush(open_list, (f_score[successor], successor)) + + # 未找到路径 + return None + + def _manhattan_distance(self, point1: Tuple[int, int], point2: Tuple[int, int]) -> float: + """计算曼哈顿距离""" + return abs(point1[0] - point2[0]) + abs(point1[1] - point2[1]) + + def _get_jump_point_successors(self, current: Tuple[int, int], goal: Tuple[int, int], + current_g: float, parent: Dict[Tuple[int, int], Tuple[int, int]]) -> List[Tuple[Tuple[int, int], float]]: + """ + 获取跳点邻居 + + Args: + current: 当前节点 + goal: 目标节点 + current_g: 当前节点的g值 + parent: 父节点字典 + + Returns: + 跳点邻居列表 [(节点, 移动代价), ...] + """ + successors = [] + + # 如果当前节点有父节点,则沿着父节点方向寻找跳点 + if current in parent: + parent_node = parent[current] + dx = current[0] - parent_node[0] + dy = current[1] - parent_node[1] + + # 标准化方向 + dx = 1 if dx > 0 else (-1 if dx < 0 else 0) + dy = 1 if dy > 0 else (-1 if dy < 0 else 0) + + # 沿着该方向寻找跳点 + jump_point = self._jump(current[0], current[1], dx, dy, goal) + if jump_point: + move_cost = self._calculate_move_cost(current, jump_point) + successors.append((jump_point, move_cost)) + else: + # 当前节点是起点,检查所有方向 + directions = [ + (-1, 0), (1, 0), (0, -1), (0, 1), # 上下左右 + (-1, -1), (-1, 1), (1, -1), (1, 1) # 对角线 + ] + + for dx, dy in directions: + jump_point = self._jump(current[0], current[1], dx, dy, goal) + if jump_point: + move_cost = self._calculate_move_cost(current, jump_point) + successors.append((jump_point, move_cost)) + + return successors + + def _jump(self, x: int, y: int, dx: int, dy: int, goal: Tuple[int, int]) -> Optional[Tuple[int, int]]: + """ + 沿着给定方向跳跃寻找跳点 + + Args: + x, y: 当前位置 + dx, dy: 跳跃方向 + goal: 目标位置 + + Returns: + 跳点坐标,如果没有找到则返回None + """ + # 计算下一个位置 + next_x = x + dx + next_y = y + dy + + # 检查边界 + if not (0 <= next_x < self.rows and 0 <= next_y < self.cols): + return None + + # 检查障碍物 + if self.grid[next_x][next_y] == 1: + return None + + # 如果到达目标点 + if (next_x, next_y) == goal: + return (next_x, next_y) + + # 检查是否为跳点(对于对角线移动) + if dx != 0 and dy != 0: + # 检查水平和垂直方向是否有强制邻居 + if (self._has_forced_neighbor(next_x, next_y, -dx, dy) or + self._has_forced_neighbor(next_x, next_y, dx, -dy)): + return (next_x, next_y) + # 检查是否为跳点(对于直线移动) + else: + # 检查是否有强制邻居 + if self._has_forced_neighbor(next_x, next_y, dx, dy): + return (next_x, next_y) + + # 如果是直线移动,继续递归跳跃 + if dx == 0 or dy == 0: + return self._jump(next_x, next_y, dx, dy, goal) + # 如果是对角线移动,检查水平和垂直分量 + else: + # 检查对角线方向的跳点 + if (self._jump(next_x, next_y, dx, 0, goal) is not None or + self._jump(next_x, next_y, 0, dy, goal) is not None): + return (next_x, next_y) + # 继续对角线跳跃 + return self._jump(next_x, next_y, dx, dy, goal) + + def _has_forced_neighbor(self, x: int, y: int, dx: int, dy: int) -> bool: + """ + 检查是否存在强制邻居 + + Args: + x, y: 当前位置 + dx, dy: 检查方向 + + Returns: + 是否存在强制邻居 + """ + # 检查对角线方向 + if dx != 0 and dy != 0: + # 检查对角线方向的强制邻居 + if (self._is_blocked(x - dx, y) and not self._is_blocked(x - dx, y + dy)) or \ + (self._is_blocked(x, y - dy) and not self._is_blocked(x + dx, y - dy)): + return True + # 检查直线方向 + else: + # 水平移动 + if dx == 0: + if (self._is_blocked(x - 1, y) and not self._is_blocked(x - 1, y + dy)) or \ + (self._is_blocked(x + 1, y) and not self._is_blocked(x + 1, y + dy)): + return True + # 垂直移动 + else: + if (self._is_blocked(x, y - 1) and not self._is_blocked(x + dx, y - 1)) or \ + (self._is_blocked(x, y + 1) and not self._is_blocked(x + dx, y + 1)): + return True + return False + + def _is_blocked(self, x: int, y: int) -> bool: + """ + 检查位置是否被阻挡 + + Args: + x, y: 位置坐标 + + Returns: + 是否被阻挡 + """ + return not (0 <= x < self.rows and 0 <= y < self.cols) or self.grid[x][y] == 1 + + def _calculate_move_cost(self, point1: Tuple[int, int], point2: Tuple[int, int]) -> float: + """ + 计算移动代价 + + Args: + point1: 起点 + point2: 终点 + + Returns: + 移动代价 + """ + dx = abs(point1[0] - point2[0]) + dy = abs(point1[1] - point2[1]) + + if dx != 0 and dy != 0: # 对角线移动 + return max(dx, dy) * 1.414 + else: # 直线移动 + return max(dx, dy) * 1.0 + + def _reconstruct_path(self, parent: Dict[Tuple[int, int], Tuple[int, int]], current: Tuple[int, int]) -> List[Tuple[int, int]]: + """ + 重构路径 + + Args: + parent: 父节点字典 + current: 当前节点 + + Returns: + 路径坐标列表 + """ + path = [current] + while current in parent: + current = parent[current] + path.append(current) + path.reverse() + return path + + def get_stats(self) -> Dict[str, int]: + """ + 获取算法统计信息 + + Returns: + 统计信息字典 + """ return self.stats.copy() \ No newline at end of file diff --git a/plugins/user/pathfinding_algorithms/core/algorithm_manager.py b/plugins/user/pathfinding_algorithms/core/algorithm_manager.py index 29cd5c14..01966c51 100644 --- a/plugins/user/pathfinding_algorithms/core/algorithm_manager.py +++ b/plugins/user/pathfinding_algorithms/core/algorithm_manager.py @@ -1,500 +1,500 @@ -""" -算法管理器 -管理各种路径规划算法的注册、配置、执行和性能分析 -""" - -import time -import json -from typing import Dict, List, Tuple, Any, Callable, Optional -from collections import defaultdict - -class AlgorithmManager: - """ - 算法管理器 - 管理各种路径规划算法的注册、配置、执行和性能分析 - """ - - def __init__(self): - """初始化算法管理器""" - self.algorithms: Dict[str, Callable] = {} - self.algorithm_parameters: Dict[str, Dict[str, Any]] = {} - self.algorithm_descriptions: Dict[str, str] = {} - self.stats: Dict[str, Any] = { - 'total_tests': 0, - 'successful_tests': 0, - 'failed_tests': 0, - 'total_time': 0.0, - 'algorithm_usage': defaultdict(int) - } - - # 算法性能缓存 - self.performance_cache: Dict[str, List[Dict[str, Any]]] = defaultdict(list) - - # 注册内置算法 - self._register_builtin_algorithms() - - def _register_builtin_algorithms(self): - """注册内置算法""" - from ..algorithms import astar, dijkstra, bfs, dfs, jps - self.algorithms['astar'] = astar.AStar - self.algorithms['dijkstra'] = dijkstra.Dijkstra - self.algorithms['bfs'] = bfs.BFS - self.algorithms['dfs'] = dfs.DFS - self.algorithms['jps'] = jps.JPS - - # 设置算法描述 - self.algorithm_descriptions['astar'] = "A*算法是一种启发式搜索算法,结合了Dijkstra算法的准确性和贪心最佳优先搜索的效率,保证找到最优解" - self.algorithm_descriptions['dijkstra'] = "Dijkstra算法是一种用于计算单源最短路径的经典算法,保证找到最短路径" - self.algorithm_descriptions['bfs'] = "广度优先搜索算法,保证找到最短路径(在无权图中)" - self.algorithm_descriptions['dfs'] = "深度优先搜索算法,不保证找到最短路径,但可能更快找到任意路径" - self.algorithm_descriptions['jps'] = "Jump Point Search是一种优化的A*算法,专门用于网格地图,比传统A*更快" - - # 设置默认参数 - self.algorithm_parameters['astar'] = { - 'heuristic': 'manhattan', - 'weight': 1.0 - } - self.algorithm_parameters['dijkstra'] = {} - self.algorithm_parameters['bfs'] = {} - self.algorithm_parameters['dfs'] = { - 'depth_limit': 1000 - } - self.algorithm_parameters['jps'] = { - 'heuristic': 'manhattan' - } - - def add_algorithm(self, name: str, algorithm_class: Callable, description: str = ""): - """ - 添加新算法 - - Args: - name: 算法名称 - algorithm_class: 算法类 - description: 算法描述 - """ - self.algorithms[name] = algorithm_class - self.algorithm_parameters[name] = {} - self.algorithm_descriptions[name] = description - print(f"✓ 算法 '{name}' 已添加") - - def remove_algorithm(self, name: str): - """ - 移除算法 - - Args: - name: 算法名称 - """ - if name in self.algorithms: - del self.algorithms[name] - if name in self.algorithm_parameters: - del self.algorithm_parameters[name] - if name in self.algorithm_descriptions: - del self.algorithm_descriptions[name] - if name in self.stats['algorithm_usage']: - del self.stats['algorithm_usage'][name] - print(f"✓ 算法 '{name}' 已移除") - - def get_available_algorithms(self) -> List[str]: - """ - 获取可用算法列表 - - Returns: - 算法名称列表 - """ - return list(self.algorithms.keys()) - - def get_algorithm_description(self, algorithm: str) -> str: - """ - 获取算法描述 - - Args: - algorithm: 算法名称 - - Returns: - 算法描述 - """ - return self.algorithm_descriptions.get(algorithm, "无描述") - - def set_algorithm_parameter(self, algorithm: str, parameter: str, value: Any): - """ - 设置算法参数 - - Args: - algorithm: 算法名称 - parameter: 参数名称 - value: 参数值 - """ - if algorithm in self.algorithm_parameters: - self.algorithm_parameters[algorithm][parameter] = value - print(f"✓ 算法 '{algorithm}' 参数 '{parameter}' 已设置为 {value}") - - def get_algorithm_parameters(self, algorithm: str) -> Dict[str, Any]: - """ - 获取算法参数 - - Args: - algorithm: 算法名称 - - Returns: - 算法参数字典 - """ - return self.algorithm_parameters.get(algorithm, {}).copy() - - def find_path(self, algorithm: str, grid: List[List[int]], start: Tuple[int, int], goal: Tuple[int, int]) -> Tuple[Optional[List[Tuple[int, int]]], Dict[str, Any]]: - """ - 查找路径 - - Args: - algorithm: 算法名称 - grid: 网格地图 - start: 起点 - goal: 终点 - - Returns: - (路径, 统计信息) 元组 - """ - if algorithm not in self.algorithms: - return None, {'error': f'算法 {algorithm} 未找到'} - - try: - # 更新统计信息 - self.stats['total_tests'] += 1 - self.stats['algorithm_usage'][algorithm] += 1 - - # 记录开始时间 - start_time = time.time() - - # 创建算法实例 - algo_class = self.algorithms[algorithm] - algo_instance = algo_class() - - # 设置参数 - if algorithm in self.algorithm_parameters: - for param, value in self.algorithm_parameters[algorithm].items(): - if hasattr(algo_instance, param): - setattr(algo_instance, param, value) - - # 执行路径查找 - path = algo_instance.find_path(grid, start, goal) - - # 记录结束时间 - end_time = time.time() - elapsed_time = end_time - start_time - - # 更新统计信息 - self.stats['total_time'] += elapsed_time - if path: - self.stats['successful_tests'] += 1 - else: - self.stats['failed_tests'] += 1 - - # 收集算法统计信息 - algorithm_stats = {} - if hasattr(algo_instance, 'get_stats'): - algorithm_stats = algo_instance.get_stats() - - # 合并统计信息 - stats = { - 'time_taken': elapsed_time, - 'success': path is not None, - 'path_length': len(path) if path else 0, - 'algorithm': algorithm - } - stats.update(algorithm_stats) - - # 缓存性能数据 - self._cache_performance_data(algorithm, stats) - - return path, stats - - except Exception as e: - self.stats['failed_tests'] += 1 - error_stats = { - 'error': str(e), - 'time_taken': 0, - 'success': False, - 'path_length': 0, - 'algorithm': algorithm - } - self._cache_performance_data(algorithm, error_stats) - return None, error_stats - - def _cache_performance_data(self, algorithm: str, stats: Dict[str, Any]): - """ - 缓存性能数据 - - Args: - algorithm: 算法名称 - stats: 统计信息 - """ - self.performance_cache[algorithm].append(stats) - # 限制缓存大小 - if len(self.performance_cache[algorithm]) > 1000: - self.performance_cache[algorithm] = self.performance_cache[algorithm][-1000:] - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - stats = self.stats.copy() - if self.stats['total_tests'] > 0: - stats['average_time'] = self.stats['total_time'] / self.stats['total_tests'] - stats['success_rate'] = self.stats['successful_tests'] / self.stats['total_tests'] - else: - stats['average_time'] = 0 - stats['success_rate'] = 0 - return stats - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'total_tests': 0, - 'successful_tests': 0, - 'failed_tests': 0, - 'total_time': 0.0, - 'algorithm_usage': defaultdict(int) - } - self.performance_cache.clear() - print("✓ 统计信息已重置") - - def generate_report(self) -> Dict[str, Any]: - """ - 生成报告 - - Returns: - 报告字典 - """ - stats = self.get_stats() - report = { - 'timestamp': time.time(), - 'algorithms': self.get_available_algorithms(), - 'algorithm_descriptions': {algo: self.get_algorithm_description(algo) - for algo in self.get_available_algorithms()}, - 'stats': stats, - 'total_tests': stats.get('total_tests', 0), - 'success_rate': stats.get('success_rate', 0), - 'algorithm_usage': dict(self.stats['algorithm_usage']), - 'performance_summary': self._generate_performance_summary() - } - return report - - def _generate_performance_summary(self) -> Dict[str, Dict[str, float]]: - """ - 生成性能摘要 - - Returns: - 性能摘要字典 - """ - summary = {} - for algorithm in self.algorithms.keys(): - if algorithm in self.performance_cache and self.performance_cache[algorithm]: - performances = self.performance_cache[algorithm] - avg_time = sum(p.get('time_taken', 0) for p in performances) / len(performances) - avg_path_length = sum(p.get('path_length', 0) for p in performances) / len(performances) - success_count = sum(1 for p in performances if p.get('success', False)) - success_rate = success_count / len(performances) if performances else 0 - - summary[algorithm] = { - 'average_time': avg_time, - 'average_path_length': avg_path_length, - 'success_rate': success_rate, - 'test_count': len(performances) - } - return summary - - def export_report(self, filepath: str) -> bool: - """ - 导出报告到文件 - - Args: - filepath: 文件路径 - - Returns: - 是否导出成功 - """ - try: - report = self.generate_report() - - # 转换不可序列化的对象 - serializable_report = self._make_serializable(report) - - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(serializable_report, f, indent=2, ensure_ascii=False) - - print(f"✓ 报告已导出到: {filepath}") - return True - except Exception as e: - print(f"✗ 导出报告失败: {e}") - return False - - def _make_serializable(self, obj): - """将对象转换为可序列化的格式""" - if isinstance(obj, dict): - return {key: self._make_serializable(value) for key, value in obj.items()} - elif isinstance(obj, (list, tuple)): - return [self._make_serializable(item) for item in obj] - elif isinstance(obj, (int, float, str, bool)) or obj is None: - return obj - else: - return str(obj) - - def batch_test(self, algorithms: List[str], test_cases: List[Dict]) -> Dict[str, List[Dict]]: - """ - 批量测试 - - Args: - algorithms: 算法列表 - test_cases: 测试用例列表 - - Returns: - 测试结果字典 - """ - print(f"✓ 开始批量测试 {len(algorithms)} 个算法,{len(test_cases)} 个测试用例") - results = {algorithm: [] for algorithm in algorithms} - - for i, test_case in enumerate(test_cases): - print(f" 测试进度: {i+1}/{len(test_cases)}") - grid = test_case['grid'] - start = test_case['start'] - goal = test_case['goal'] - - for algorithm in algorithms: - path, stats = self.find_path(algorithm, grid, start, goal) - results[algorithm].append({ - 'test_case': test_case.get('name', f'unnamed_{i}'), - 'path': path, - 'stats': stats - }) - - print("✓ 批量测试完成") - return results - - def compare_algorithms(self, algorithms: List[str], grid: List[List[int]], - start: Tuple[int, int], goal: Tuple[int, int]) -> Dict[str, Dict[str, Any]]: - """ - 比较算法性能 - - Args: - algorithms: 算法列表 - grid: 网格地图 - start: 起点 - goal: 终点 - - Returns: - 比较结果字典 - """ - print(f"✓ 比较 {len(algorithms)} 个算法") - results = {} - - for algorithm in algorithms: - if algorithm in self.algorithms: - path, stats = self.find_path(algorithm, grid, start, goal) - results[algorithm] = { - 'path': path, - 'stats': stats, - 'description': self.get_algorithm_description(algorithm) - } - - return results - - def get_algorithm_complexity(self, algorithm: str) -> str: - """ - 获取算法复杂度信息 - - Args: - algorithm: 算法名称 - - Returns: - 复杂度信息 - """ - complexity_info = { - 'astar': '时间复杂度: O(b^d), 空间复杂度: O(b^d) (b为分支因子, d为搜索深度)', - 'dijkstra': '时间复杂度: O(V^2) 或 O(E + V log V) (使用优先队列), 空间复杂度: O(V)', - 'bfs': '时间复杂度: O(V + E), 空间复杂度: O(V)', - 'dfs': '时间复杂度: O(V + E), 空间复杂度: O(V)', - 'jps': '时间复杂度: 比A*快数倍, 具体取决于地图结构, 空间复杂度: O(b^d)' - } - return complexity_info.get(algorithm, '未知复杂度') - - def get_cached_performance_data(self, algorithm: str) -> List[Dict[str, Any]]: - """ - 获取缓存的性能数据 - - Args: - algorithm: 算法名称 - - Returns: - 性能数据列表 - """ - return self.performance_cache.get(algorithm, []).copy() - - def clear_performance_cache(self): - """清除性能缓存""" - self.performance_cache.clear() - print("✓ 性能缓存已清除") - - def get_algorithm_recommendation(self, grid_size: Tuple[int, int], - obstacle_density: float, - optimality_required: bool = True) -> str: - """ - 根据场景特点推荐算法 - - Args: - grid_size: 网格尺寸 (行数, 列数) - obstacle_density: 障碍物密度 (0-1) - optimality_required: 是否要求最优解 - - Returns: - 推荐的算法名称 - """ - rows, cols = grid_size - total_cells = rows * cols - - # 根据场景特点推荐算法 - if total_cells > 10000: # 大型地图 - if optimality_required: - return 'jps' # JPS在大型地图上性能更好 - else: - return 'dfs' # 如果不要求最优解,DFS可能更快 - elif obstacle_density > 0.5: # 高障碍物密度 - if optimality_required: - return 'astar' # A*在复杂环境中表现稳定 - else: - return 'bfs' # BFS在高障碍物环境中可能更快 - else: # 小型或中等地图,低障碍物密度 - if optimality_required: - return 'astar' # A*是通用的最佳选择 - else: - return 'bfs' # BFS简单高效 - - def validate_grid(self, grid: List[List[int]]) -> Dict[str, Any]: - """ - 验证网格数据 - - Args: - grid: 网格数据 - - Returns: - 验证结果 - """ - if not grid: - return {'valid': False, 'error': '网格为空'} - - rows = len(grid) - cols = len(grid[0]) if rows > 0 else 0 - - if rows == 0 or cols == 0: - return {'valid': False, 'error': '网格尺寸为0'} - - # 检查数据类型 - for r in range(rows): - if len(grid[r]) != cols: - return {'valid': False, 'error': f'第{r}行列数不一致'} - for c in range(cols): - if not isinstance(grid[r][c], int) or grid[r][c] not in [0, 1]: - return {'valid': False, 'error': f'位置({r},{c})的数据无效'} - +""" +算法管理器 +管理各种路径规划算法的注册、配置、执行和性能分析 +""" + +import time +import json +from typing import Dict, List, Tuple, Any, Callable, Optional +from collections import defaultdict + +class AlgorithmManager: + """ + 算法管理器 + 管理各种路径规划算法的注册、配置、执行和性能分析 + """ + + def __init__(self): + """初始化算法管理器""" + self.algorithms: Dict[str, Callable] = {} + self.algorithm_parameters: Dict[str, Dict[str, Any]] = {} + self.algorithm_descriptions: Dict[str, str] = {} + self.stats: Dict[str, Any] = { + 'total_tests': 0, + 'successful_tests': 0, + 'failed_tests': 0, + 'total_time': 0.0, + 'algorithm_usage': defaultdict(int) + } + + # 算法性能缓存 + self.performance_cache: Dict[str, List[Dict[str, Any]]] = defaultdict(list) + + # 注册内置算法 + self._register_builtin_algorithms() + + def _register_builtin_algorithms(self): + """注册内置算法""" + from ..algorithms import astar, dijkstra, bfs, dfs, jps + self.algorithms['astar'] = astar.AStar + self.algorithms['dijkstra'] = dijkstra.Dijkstra + self.algorithms['bfs'] = bfs.BFS + self.algorithms['dfs'] = dfs.DFS + self.algorithms['jps'] = jps.JPS + + # 设置算法描述 + self.algorithm_descriptions['astar'] = "A*算法是一种启发式搜索算法,结合了Dijkstra算法的准确性和贪心最佳优先搜索的效率,保证找到最优解" + self.algorithm_descriptions['dijkstra'] = "Dijkstra算法是一种用于计算单源最短路径的经典算法,保证找到最短路径" + self.algorithm_descriptions['bfs'] = "广度优先搜索算法,保证找到最短路径(在无权图中)" + self.algorithm_descriptions['dfs'] = "深度优先搜索算法,不保证找到最短路径,但可能更快找到任意路径" + self.algorithm_descriptions['jps'] = "Jump Point Search是一种优化的A*算法,专门用于网格地图,比传统A*更快" + + # 设置默认参数 + self.algorithm_parameters['astar'] = { + 'heuristic': 'manhattan', + 'weight': 1.0 + } + self.algorithm_parameters['dijkstra'] = {} + self.algorithm_parameters['bfs'] = {} + self.algorithm_parameters['dfs'] = { + 'depth_limit': 1000 + } + self.algorithm_parameters['jps'] = { + 'heuristic': 'manhattan' + } + + def add_algorithm(self, name: str, algorithm_class: Callable, description: str = ""): + """ + 添加新算法 + + Args: + name: 算法名称 + algorithm_class: 算法类 + description: 算法描述 + """ + self.algorithms[name] = algorithm_class + self.algorithm_parameters[name] = {} + self.algorithm_descriptions[name] = description + print(f"✓ 算法 '{name}' 已添加") + + def remove_algorithm(self, name: str): + """ + 移除算法 + + Args: + name: 算法名称 + """ + if name in self.algorithms: + del self.algorithms[name] + if name in self.algorithm_parameters: + del self.algorithm_parameters[name] + if name in self.algorithm_descriptions: + del self.algorithm_descriptions[name] + if name in self.stats['algorithm_usage']: + del self.stats['algorithm_usage'][name] + print(f"✓ 算法 '{name}' 已移除") + + def get_available_algorithms(self) -> List[str]: + """ + 获取可用算法列表 + + Returns: + 算法名称列表 + """ + return list(self.algorithms.keys()) + + def get_algorithm_description(self, algorithm: str) -> str: + """ + 获取算法描述 + + Args: + algorithm: 算法名称 + + Returns: + 算法描述 + """ + return self.algorithm_descriptions.get(algorithm, "无描述") + + def set_algorithm_parameter(self, algorithm: str, parameter: str, value: Any): + """ + 设置算法参数 + + Args: + algorithm: 算法名称 + parameter: 参数名称 + value: 参数值 + """ + if algorithm in self.algorithm_parameters: + self.algorithm_parameters[algorithm][parameter] = value + print(f"✓ 算法 '{algorithm}' 参数 '{parameter}' 已设置为 {value}") + + def get_algorithm_parameters(self, algorithm: str) -> Dict[str, Any]: + """ + 获取算法参数 + + Args: + algorithm: 算法名称 + + Returns: + 算法参数字典 + """ + return self.algorithm_parameters.get(algorithm, {}).copy() + + def find_path(self, algorithm: str, grid: List[List[int]], start: Tuple[int, int], goal: Tuple[int, int]) -> Tuple[Optional[List[Tuple[int, int]]], Dict[str, Any]]: + """ + 查找路径 + + Args: + algorithm: 算法名称 + grid: 网格地图 + start: 起点 + goal: 终点 + + Returns: + (路径, 统计信息) 元组 + """ + if algorithm not in self.algorithms: + return None, {'error': f'算法 {algorithm} 未找到'} + + try: + # 更新统计信息 + self.stats['total_tests'] += 1 + self.stats['algorithm_usage'][algorithm] += 1 + + # 记录开始时间 + start_time = time.time() + + # 创建算法实例 + algo_class = self.algorithms[algorithm] + algo_instance = algo_class() + + # 设置参数 + if algorithm in self.algorithm_parameters: + for param, value in self.algorithm_parameters[algorithm].items(): + if hasattr(algo_instance, param): + setattr(algo_instance, param, value) + + # 执行路径查找 + path = algo_instance.find_path(grid, start, goal) + + # 记录结束时间 + end_time = time.time() + elapsed_time = end_time - start_time + + # 更新统计信息 + self.stats['total_time'] += elapsed_time + if path: + self.stats['successful_tests'] += 1 + else: + self.stats['failed_tests'] += 1 + + # 收集算法统计信息 + algorithm_stats = {} + if hasattr(algo_instance, 'get_stats'): + algorithm_stats = algo_instance.get_stats() + + # 合并统计信息 + stats = { + 'time_taken': elapsed_time, + 'success': path is not None, + 'path_length': len(path) if path else 0, + 'algorithm': algorithm + } + stats.update(algorithm_stats) + + # 缓存性能数据 + self._cache_performance_data(algorithm, stats) + + return path, stats + + except Exception as e: + self.stats['failed_tests'] += 1 + error_stats = { + 'error': str(e), + 'time_taken': 0, + 'success': False, + 'path_length': 0, + 'algorithm': algorithm + } + self._cache_performance_data(algorithm, error_stats) + return None, error_stats + + def _cache_performance_data(self, algorithm: str, stats: Dict[str, Any]): + """ + 缓存性能数据 + + Args: + algorithm: 算法名称 + stats: 统计信息 + """ + self.performance_cache[algorithm].append(stats) + # 限制缓存大小 + if len(self.performance_cache[algorithm]) > 1000: + self.performance_cache[algorithm] = self.performance_cache[algorithm][-1000:] + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + stats = self.stats.copy() + if self.stats['total_tests'] > 0: + stats['average_time'] = self.stats['total_time'] / self.stats['total_tests'] + stats['success_rate'] = self.stats['successful_tests'] / self.stats['total_tests'] + else: + stats['average_time'] = 0 + stats['success_rate'] = 0 + return stats + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'total_tests': 0, + 'successful_tests': 0, + 'failed_tests': 0, + 'total_time': 0.0, + 'algorithm_usage': defaultdict(int) + } + self.performance_cache.clear() + print("✓ 统计信息已重置") + + def generate_report(self) -> Dict[str, Any]: + """ + 生成报告 + + Returns: + 报告字典 + """ + stats = self.get_stats() + report = { + 'timestamp': time.time(), + 'algorithms': self.get_available_algorithms(), + 'algorithm_descriptions': {algo: self.get_algorithm_description(algo) + for algo in self.get_available_algorithms()}, + 'stats': stats, + 'total_tests': stats.get('total_tests', 0), + 'success_rate': stats.get('success_rate', 0), + 'algorithm_usage': dict(self.stats['algorithm_usage']), + 'performance_summary': self._generate_performance_summary() + } + return report + + def _generate_performance_summary(self) -> Dict[str, Dict[str, float]]: + """ + 生成性能摘要 + + Returns: + 性能摘要字典 + """ + summary = {} + for algorithm in self.algorithms.keys(): + if algorithm in self.performance_cache and self.performance_cache[algorithm]: + performances = self.performance_cache[algorithm] + avg_time = sum(p.get('time_taken', 0) for p in performances) / len(performances) + avg_path_length = sum(p.get('path_length', 0) for p in performances) / len(performances) + success_count = sum(1 for p in performances if p.get('success', False)) + success_rate = success_count / len(performances) if performances else 0 + + summary[algorithm] = { + 'average_time': avg_time, + 'average_path_length': avg_path_length, + 'success_rate': success_rate, + 'test_count': len(performances) + } + return summary + + def export_report(self, filepath: str) -> bool: + """ + 导出报告到文件 + + Args: + filepath: 文件路径 + + Returns: + 是否导出成功 + """ + try: + report = self.generate_report() + + # 转换不可序列化的对象 + serializable_report = self._make_serializable(report) + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(serializable_report, f, indent=2, ensure_ascii=False) + + print(f"✓ 报告已导出到: {filepath}") + return True + except Exception as e: + print(f"✗ 导出报告失败: {e}") + return False + + def _make_serializable(self, obj): + """将对象转换为可序列化的格式""" + if isinstance(obj, dict): + return {key: self._make_serializable(value) for key, value in obj.items()} + elif isinstance(obj, (list, tuple)): + return [self._make_serializable(item) for item in obj] + elif isinstance(obj, (int, float, str, bool)) or obj is None: + return obj + else: + return str(obj) + + def batch_test(self, algorithms: List[str], test_cases: List[Dict]) -> Dict[str, List[Dict]]: + """ + 批量测试 + + Args: + algorithms: 算法列表 + test_cases: 测试用例列表 + + Returns: + 测试结果字典 + """ + print(f"✓ 开始批量测试 {len(algorithms)} 个算法,{len(test_cases)} 个测试用例") + results = {algorithm: [] for algorithm in algorithms} + + for i, test_case in enumerate(test_cases): + print(f" 测试进度: {i+1}/{len(test_cases)}") + grid = test_case['grid'] + start = test_case['start'] + goal = test_case['goal'] + + for algorithm in algorithms: + path, stats = self.find_path(algorithm, grid, start, goal) + results[algorithm].append({ + 'test_case': test_case.get('name', f'unnamed_{i}'), + 'path': path, + 'stats': stats + }) + + print("✓ 批量测试完成") + return results + + def compare_algorithms(self, algorithms: List[str], grid: List[List[int]], + start: Tuple[int, int], goal: Tuple[int, int]) -> Dict[str, Dict[str, Any]]: + """ + 比较算法性能 + + Args: + algorithms: 算法列表 + grid: 网格地图 + start: 起点 + goal: 终点 + + Returns: + 比较结果字典 + """ + print(f"✓ 比较 {len(algorithms)} 个算法") + results = {} + + for algorithm in algorithms: + if algorithm in self.algorithms: + path, stats = self.find_path(algorithm, grid, start, goal) + results[algorithm] = { + 'path': path, + 'stats': stats, + 'description': self.get_algorithm_description(algorithm) + } + + return results + + def get_algorithm_complexity(self, algorithm: str) -> str: + """ + 获取算法复杂度信息 + + Args: + algorithm: 算法名称 + + Returns: + 复杂度信息 + """ + complexity_info = { + 'astar': '时间复杂度: O(b^d), 空间复杂度: O(b^d) (b为分支因子, d为搜索深度)', + 'dijkstra': '时间复杂度: O(V^2) 或 O(E + V log V) (使用优先队列), 空间复杂度: O(V)', + 'bfs': '时间复杂度: O(V + E), 空间复杂度: O(V)', + 'dfs': '时间复杂度: O(V + E), 空间复杂度: O(V)', + 'jps': '时间复杂度: 比A*快数倍, 具体取决于地图结构, 空间复杂度: O(b^d)' + } + return complexity_info.get(algorithm, '未知复杂度') + + def get_cached_performance_data(self, algorithm: str) -> List[Dict[str, Any]]: + """ + 获取缓存的性能数据 + + Args: + algorithm: 算法名称 + + Returns: + 性能数据列表 + """ + return self.performance_cache.get(algorithm, []).copy() + + def clear_performance_cache(self): + """清除性能缓存""" + self.performance_cache.clear() + print("✓ 性能缓存已清除") + + def get_algorithm_recommendation(self, grid_size: Tuple[int, int], + obstacle_density: float, + optimality_required: bool = True) -> str: + """ + 根据场景特点推荐算法 + + Args: + grid_size: 网格尺寸 (行数, 列数) + obstacle_density: 障碍物密度 (0-1) + optimality_required: 是否要求最优解 + + Returns: + 推荐的算法名称 + """ + rows, cols = grid_size + total_cells = rows * cols + + # 根据场景特点推荐算法 + if total_cells > 10000: # 大型地图 + if optimality_required: + return 'jps' # JPS在大型地图上性能更好 + else: + return 'dfs' # 如果不要求最优解,DFS可能更快 + elif obstacle_density > 0.5: # 高障碍物密度 + if optimality_required: + return 'astar' # A*在复杂环境中表现稳定 + else: + return 'bfs' # BFS在高障碍物环境中可能更快 + else: # 小型或中等地图,低障碍物密度 + if optimality_required: + return 'astar' # A*是通用的最佳选择 + else: + return 'bfs' # BFS简单高效 + + def validate_grid(self, grid: List[List[int]]) -> Dict[str, Any]: + """ + 验证网格数据 + + Args: + grid: 网格数据 + + Returns: + 验证结果 + """ + if not grid: + return {'valid': False, 'error': '网格为空'} + + rows = len(grid) + cols = len(grid[0]) if rows > 0 else 0 + + if rows == 0 or cols == 0: + return {'valid': False, 'error': '网格尺寸为0'} + + # 检查数据类型 + for r in range(rows): + if len(grid[r]) != cols: + return {'valid': False, 'error': f'第{r}行列数不一致'} + for c in range(cols): + if not isinstance(grid[r][c], int) or grid[r][c] not in [0, 1]: + return {'valid': False, 'error': f'位置({r},{c})的数据无效'} + return {'valid': True, 'rows': rows, 'cols': cols} \ No newline at end of file diff --git a/plugins/user/pathfinding_algorithms/editor/pathfinding_editor.py b/plugins/user/pathfinding_algorithms/editor/pathfinding_editor.py index fda0e2bd..68d37a81 100644 --- a/plugins/user/pathfinding_algorithms/editor/pathfinding_editor.py +++ b/plugins/user/pathfinding_algorithms/editor/pathfinding_editor.py @@ -1,380 +1,380 @@ -""" -路径规划编辑器 -提供可视化编辑路径规划场景的功能 -""" - -from typing import List, Tuple, Optional, Dict, Any -from panda3d.core import Point3, KeyboardButton, MouseWatcher - -class PathfindingEditor: - """ - 路径规划编辑器 - 提供可视化编辑路径规划场景的功能 - """ - - def __init__(self, world, algorithm_manager, visualization_manager): - """ - 初始化路径规划编辑器 - - Args: - world: 3D世界对象 - algorithm_manager: 算法管理器 - visualization_manager: 可视化管理器 - """ - self.world = world - self.algorithm_manager = algorithm_manager - self.visualization_manager = visualization_manager - self.visible = False - - # 编辑器状态 - self.edit_mode = False - self.selected_tool = "select" # select, add_obstacle, remove_obstacle, set_start, set_goal - self.grid_size = (20, 20) - self.current_grid = None - - # 当前场景元素 - self.start_point = (0, 0) - self.goal_point = (19, 19) - - # 编辑器GUI元素 - self.gui_elements = [] - self.tool_buttons = {} - - # 鼠标交互 - self.last_mouse_pos = None - - # 初始化网格 - self._initialize_grid() - - def _initialize_grid(self): - """初始化网格""" - rows, cols = self.grid_size - self.current_grid = [[0 for _ in range(cols)] for _ in range(rows)] - - def toggle_visibility(self): - """切换编辑器可见性""" - if self.visible: - self.hide() - else: - self.show() - - def show(self): - """显示编辑器""" - if self.visible: - return - - self.visible = True - self._create_editor_gui() - print("✓ 路径规划编辑器已显示") - - def hide(self): - """隐藏编辑器""" - if not self.visible: - return - - self.visible = False - self._cleanup_editor_gui() - print("✓ 路径规划编辑器已隐藏") - - def _create_editor_gui(self): - """创建编辑器GUI""" - try: - if not hasattr(self.world, 'gui_manager') or not self.world.gui_manager: - print("⚠ GUI管理器不可用,无法创建编辑器界面") - return - - gui_manager = self.world.gui_manager - - # 创建编辑器面板标题 - panel = gui_manager.createGUIButton( - pos=(0.7, 0, 0.95), - text="路径规划编辑器", - size=0.06 - ) - self.gui_elements.append(panel) - - # 创建工具按钮 - tools = [ - ("选择", "select"), - ("添加障碍", "add_obstacle"), - ("移除障碍", "remove_obstacle"), - ("设置起点", "set_start"), - ("设置终点", "set_goal") - ] - - for i, (name, tool) in enumerate(tools): - button = gui_manager.createGUIButton( - pos=(0.7, 0, 0.88 - i * 0.06), - text=name, - size=0.04 - ) - self.gui_elements.append(button) - self.tool_buttons[tool] = button - self.world.accept(f"pathfinding_tool_{tool}", - lambda t=tool: self.set_edit_tool(t)) - - # 创建编辑功能按钮 - functions = [ - ("生成随机障碍", self.generate_random_obstacles), - ("清除障碍", self.clear_obstacles), - ("运行A*", lambda: self.run_algorithm("astar")), - ("运行Dijkstra", lambda: self.run_algorithm("dijkstra")), - ("运行BFS", lambda: self.run_algorithm("bfs")), - ("运行DFS", lambda: self.run_algorithm("dfs")), - ("比较算法", self.compare_algorithms) - ] - - for i, (name, callback) in enumerate(functions): - button = gui_manager.createGUIButton( - pos=(0.82, 0, 0.88 - i * 0.06), - text=name, - size=0.04 - ) - self.gui_elements.append(button) - self.world.accept(f"pathfinding_func_{name}", callback) - - # 显示网格 - self.visualization_manager.visualize_grid(self.current_grid) - - except Exception as e: - print(f"⚠ 编辑器GUI创建失败: {e}") - - def _cleanup_editor_gui(self): - """清理编辑器GUI""" - try: - if hasattr(self.world, 'gui_manager') and self.world.gui_manager: - gui_manager = self.world.gui_manager - - for element in self.gui_elements: - gui_manager.deleteGUIElement(element) - - # 清理事件监听 - tools = ["select", "add_obstacle", "remove_obstacle", "set_start", "set_goal"] - for tool in tools: - self.world.ignore(f"pathfinding_tool_{tool}") - - functions = [ - "生成随机障碍", "清除障碍", "运行A*", "运行Dijkstra", - "运行BFS", "运行DFS", "比较算法" - ] - for func in functions: - self.world.ignore(f"pathfinding_func_{func}") - - self.gui_elements.clear() - self.tool_buttons.clear() - - except Exception as e: - print(f"⚠ 编辑器GUI清理失败: {e}") - - def set_edit_tool(self, tool: str): - """设置编辑工具""" - self.selected_tool = tool - print(f"✓ 切换到 {tool} 工具") - - # 更新按钮状态 - for tool_name, button in self.tool_buttons.items(): - if tool_name == tool: - # 高亮选中工具 - button.setColor(0.5, 1.0, 0.5, 1.0) # 绿色 - else: - # 恢复默认颜色 - button.setColor(1.0, 1.0, 1.0, 1.0) # 白色 - - def generate_random_obstacles(self): - """生成随机障碍物""" - import random - - if not self.current_grid: - return - - rows, cols = len(self.current_grid), len(self.current_grid[0]) - obstacle_count = rows * cols // 4 # 25%的格子设为障碍物 - - # 清除现有障碍物 - self.clear_obstacles() - - # 生成随机障碍物 - for _ in range(obstacle_count): - row = random.randint(0, rows - 1) - col = random.randint(0, cols - 1) - - # 不要在起点和终点生成障碍物 - if (row, col) != self.start_point and (row, col) != self.goal_point: - self.current_grid[row][col] = 1 - - # 更新可视化 - self.visualization_manager.visualize_grid(self.current_grid) - print(f"✓ 生成了 {obstacle_count} 个随机障碍物") - - def clear_obstacles(self): - """清除障碍物""" - if not self.current_grid: - return - - rows, cols = len(self.current_grid), len(self.current_grid[0]) - for row in range(rows): - for col in range(cols): - # 不清除起点和终点 - if (row, col) != self.start_point and (row, col) != self.goal_point: - self.current_grid[row][col] = 0 - - # 更新可视化 - self.visualization_manager.visualize_grid(self.current_grid) - print("✓ 障碍物已清除") - - def run_algorithm(self, algorithm: str): - """运行指定算法""" - if not self.algorithm_manager or not self.visualization_manager: - return - - print(f"✓ 运行 {algorithm} 算法...") - - # 查找路径 - path, stats = self.algorithm_manager.find_path( - algorithm, self.current_grid, self.start_point, self.goal_point) - - if path: - print(f"✓ 找到路径,长度: {len(path)}") - print(f" 访问节点数: {stats.get('nodes_visited', 0)}") - print(f" 耗时: {stats.get('time_taken', 0):.6f}秒") - - # 可视化路径 - self.visualization_manager.visualize_path(path, self.current_grid) - else: - print("✗ 未找到路径") - print(f" 耗时: {stats.get('time_taken', 0):.6f}秒") - - def compare_algorithms(self): - """比较算法性能""" - if not self.algorithm_manager: - return - - print("✓ 比较路径规划算法...") - - algorithms = ["astar", "dijkstra", "bfs", "dfs"] - results = {} - - for algorithm in algorithms: - path, stats = self.algorithm_manager.find_path( - algorithm, self.current_grid, self.start_point, self.goal_point) - results[algorithm] = { - "path_length": len(path) if path else 0, - "nodes_visited": stats.get("nodes_visited", 0), - "time_taken": stats.get("time_taken", 0) - } - - print("=== 算法性能比较 ===") - for algorithm, result in results.items(): - print(f"{algorithm}:") - print(f" 路径长度: {result['path_length']}") - print(f" 访问节点数: {result['nodes_visited']}") - print(f" 耗时: {result['time_taken']:.6f}秒") - - def handle_mouse_click(self, mouse_x: float, mouse_y: float): - """处理鼠标点击事件""" - if not self.edit_mode: - return - - # 将屏幕坐标转换为网格坐标 - grid_pos = self._screen_to_grid(mouse_x, mouse_y) - if not grid_pos: - return - - row, col = grid_pos - - # 检查坐标是否在网格范围内 - rows, cols = len(self.current_grid), len(self.current_grid[0]) if self.current_grid else (0, 0) - if not (0 <= row < rows and 0 <= col < cols): - return - - # 根据当前工具执行操作 - if self.selected_tool == "add_obstacle": - # 不要在起点和终点添加障碍物 - if (row, col) != self.start_point and (row, col) != self.goal_point: - self.current_grid[row][col] = 1 - self.visualization_manager.visualize_grid(self.current_grid) - - elif self.selected_tool == "remove_obstacle": - # 不要移除起点和终点 - if (row, col) != self.start_point and (row, col) != self.goal_point: - self.current_grid[row][col] = 0 - self.visualization_manager.visualize_grid(self.current_grid) - - elif self.selected_tool == "set_start": - # 移除旧起点的障碍物 - old_row, old_col = self.start_point - if self.current_grid[old_row][old_col] == 1: - self.current_grid[old_row][old_col] = 0 - - self.start_point = (row, col) - print(f"✓ 起点设置为: {row}, {col}") - # 更新可视化 - self.visualization_manager.visualize_grid(self.current_grid) - - elif self.selected_tool == "set_goal": - # 移除旧终点的障碍物 - old_row, old_col = self.goal_point - if self.current_grid[old_row][old_col] == 1: - self.current_grid[old_row][old_col] = 0 - - self.goal_point = (row, col) - print(f"✓ 终点设置为: {row}, {col}") - # 更新可视化 - self.visualization_manager.visualize_grid(self.current_grid) - - def _screen_to_grid(self, screen_x: float, screen_y: float) -> Optional[Tuple[int, int]]: - """ - 将屏幕坐标转换为网格坐标 - - Args: - screen_x: 屏幕X坐标 (-1 到 1) - screen_y: 屏幕Y坐标 (-1 到 1) - - Returns: - 网格坐标 (row, col),如果转换失败则返回None - """ - # 这是一个简化的实现 - # 实际应用中需要根据摄像机位置和方向进行更精确的转换 - - if not self.current_grid: - return None - - rows, cols = len(self.current_grid), len(self.current_grid[0]) - - # 将屏幕坐标映射到网格坐标 - col = int((screen_x + 1) * cols / 2) - row = int((1 - screen_y) * rows / 2) - - return (row, col) - - def toggle_edit_mode(self): - """切换编辑模式""" - self.edit_mode = not self.edit_mode - if self.edit_mode: - print("✓ 进入编辑模式") - else: - print("✓ 退出编辑模式") - - def update(self, dt: float): - """更新编辑器状态""" - # 处理键盘输入 - if self.world.mouseWatcherNode.hasMouse(): - mouse_pos = self.world.mouseWatcherNode.getMouse() - - # 检查鼠标按键 - if self.world.mouseWatcherNode.isButtonDown(MouseWatcher.getGlobalPointer()): - if self.last_mouse_pos is None or \ - (mouse_pos - self.last_mouse_pos).length() > 0.01: - self.handle_mouse_click(mouse_pos.x, mouse_pos.y) - self.last_mouse_pos = mouse_pos - - # 检查键盘快捷键 - if self.world.mouseWatcherNode.isButtonDown(KeyboardButton.asciiKey('e')): - self.toggle_edit_mode() - - def cleanup(self): - """清理编辑器资源""" - self.hide() - self.current_grid = None - self.start_point = None +""" +路径规划编辑器 +提供可视化编辑路径规划场景的功能 +""" + +from typing import List, Tuple, Optional, Dict, Any +from panda3d.core import Point3, KeyboardButton, MouseWatcher + +class PathfindingEditor: + """ + 路径规划编辑器 + 提供可视化编辑路径规划场景的功能 + """ + + def __init__(self, world, algorithm_manager, visualization_manager): + """ + 初始化路径规划编辑器 + + Args: + world: 3D世界对象 + algorithm_manager: 算法管理器 + visualization_manager: 可视化管理器 + """ + self.world = world + self.algorithm_manager = algorithm_manager + self.visualization_manager = visualization_manager + self.visible = False + + # 编辑器状态 + self.edit_mode = False + self.selected_tool = "select" # select, add_obstacle, remove_obstacle, set_start, set_goal + self.grid_size = (20, 20) + self.current_grid = None + + # 当前场景元素 + self.start_point = (0, 0) + self.goal_point = (19, 19) + + # 编辑器GUI元素 + self.gui_elements = [] + self.tool_buttons = {} + + # 鼠标交互 + self.last_mouse_pos = None + + # 初始化网格 + self._initialize_grid() + + def _initialize_grid(self): + """初始化网格""" + rows, cols = self.grid_size + self.current_grid = [[0 for _ in range(cols)] for _ in range(rows)] + + def toggle_visibility(self): + """切换编辑器可见性""" + if self.visible: + self.hide() + else: + self.show() + + def show(self): + """显示编辑器""" + if self.visible: + return + + self.visible = True + self._create_editor_gui() + print("✓ 路径规划编辑器已显示") + + def hide(self): + """隐藏编辑器""" + if not self.visible: + return + + self.visible = False + self._cleanup_editor_gui() + print("✓ 路径规划编辑器已隐藏") + + def _create_editor_gui(self): + """创建编辑器GUI""" + try: + if not hasattr(self.world, 'gui_manager') or not self.world.gui_manager: + print("⚠ GUI管理器不可用,无法创建编辑器界面") + return + + gui_manager = self.world.gui_manager + + # 创建编辑器面板标题 + panel = gui_manager.createGUIButton( + pos=(0.7, 0, 0.95), + text="路径规划编辑器", + size=0.06 + ) + self.gui_elements.append(panel) + + # 创建工具按钮 + tools = [ + ("选择", "select"), + ("添加障碍", "add_obstacle"), + ("移除障碍", "remove_obstacle"), + ("设置起点", "set_start"), + ("设置终点", "set_goal") + ] + + for i, (name, tool) in enumerate(tools): + button = gui_manager.createGUIButton( + pos=(0.7, 0, 0.88 - i * 0.06), + text=name, + size=0.04 + ) + self.gui_elements.append(button) + self.tool_buttons[tool] = button + self.world.accept(f"pathfinding_tool_{tool}", + lambda t=tool: self.set_edit_tool(t)) + + # 创建编辑功能按钮 + functions = [ + ("生成随机障碍", self.generate_random_obstacles), + ("清除障碍", self.clear_obstacles), + ("运行A*", lambda: self.run_algorithm("astar")), + ("运行Dijkstra", lambda: self.run_algorithm("dijkstra")), + ("运行BFS", lambda: self.run_algorithm("bfs")), + ("运行DFS", lambda: self.run_algorithm("dfs")), + ("比较算法", self.compare_algorithms) + ] + + for i, (name, callback) in enumerate(functions): + button = gui_manager.createGUIButton( + pos=(0.82, 0, 0.88 - i * 0.06), + text=name, + size=0.04 + ) + self.gui_elements.append(button) + self.world.accept(f"pathfinding_func_{name}", callback) + + # 显示网格 + self.visualization_manager.visualize_grid(self.current_grid) + + except Exception as e: + print(f"⚠ 编辑器GUI创建失败: {e}") + + def _cleanup_editor_gui(self): + """清理编辑器GUI""" + try: + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + gui_manager = self.world.gui_manager + + for element in self.gui_elements: + gui_manager.deleteGUIElement(element) + + # 清理事件监听 + tools = ["select", "add_obstacle", "remove_obstacle", "set_start", "set_goal"] + for tool in tools: + self.world.ignore(f"pathfinding_tool_{tool}") + + functions = [ + "生成随机障碍", "清除障碍", "运行A*", "运行Dijkstra", + "运行BFS", "运行DFS", "比较算法" + ] + for func in functions: + self.world.ignore(f"pathfinding_func_{func}") + + self.gui_elements.clear() + self.tool_buttons.clear() + + except Exception as e: + print(f"⚠ 编辑器GUI清理失败: {e}") + + def set_edit_tool(self, tool: str): + """设置编辑工具""" + self.selected_tool = tool + print(f"✓ 切换到 {tool} 工具") + + # 更新按钮状态 + for tool_name, button in self.tool_buttons.items(): + if tool_name == tool: + # 高亮选中工具 + button.setColor(0.5, 1.0, 0.5, 1.0) # 绿色 + else: + # 恢复默认颜色 + button.setColor(1.0, 1.0, 1.0, 1.0) # 白色 + + def generate_random_obstacles(self): + """生成随机障碍物""" + import random + + if not self.current_grid: + return + + rows, cols = len(self.current_grid), len(self.current_grid[0]) + obstacle_count = rows * cols // 4 # 25%的格子设为障碍物 + + # 清除现有障碍物 + self.clear_obstacles() + + # 生成随机障碍物 + for _ in range(obstacle_count): + row = random.randint(0, rows - 1) + col = random.randint(0, cols - 1) + + # 不要在起点和终点生成障碍物 + if (row, col) != self.start_point and (row, col) != self.goal_point: + self.current_grid[row][col] = 1 + + # 更新可视化 + self.visualization_manager.visualize_grid(self.current_grid) + print(f"✓ 生成了 {obstacle_count} 个随机障碍物") + + def clear_obstacles(self): + """清除障碍物""" + if not self.current_grid: + return + + rows, cols = len(self.current_grid), len(self.current_grid[0]) + for row in range(rows): + for col in range(cols): + # 不清除起点和终点 + if (row, col) != self.start_point and (row, col) != self.goal_point: + self.current_grid[row][col] = 0 + + # 更新可视化 + self.visualization_manager.visualize_grid(self.current_grid) + print("✓ 障碍物已清除") + + def run_algorithm(self, algorithm: str): + """运行指定算法""" + if not self.algorithm_manager or not self.visualization_manager: + return + + print(f"✓ 运行 {algorithm} 算法...") + + # 查找路径 + path, stats = self.algorithm_manager.find_path( + algorithm, self.current_grid, self.start_point, self.goal_point) + + if path: + print(f"✓ 找到路径,长度: {len(path)}") + print(f" 访问节点数: {stats.get('nodes_visited', 0)}") + print(f" 耗时: {stats.get('time_taken', 0):.6f}秒") + + # 可视化路径 + self.visualization_manager.visualize_path(path, self.current_grid) + else: + print("✗ 未找到路径") + print(f" 耗时: {stats.get('time_taken', 0):.6f}秒") + + def compare_algorithms(self): + """比较算法性能""" + if not self.algorithm_manager: + return + + print("✓ 比较路径规划算法...") + + algorithms = ["astar", "dijkstra", "bfs", "dfs"] + results = {} + + for algorithm in algorithms: + path, stats = self.algorithm_manager.find_path( + algorithm, self.current_grid, self.start_point, self.goal_point) + results[algorithm] = { + "path_length": len(path) if path else 0, + "nodes_visited": stats.get("nodes_visited", 0), + "time_taken": stats.get("time_taken", 0) + } + + print("=== 算法性能比较 ===") + for algorithm, result in results.items(): + print(f"{algorithm}:") + print(f" 路径长度: {result['path_length']}") + print(f" 访问节点数: {result['nodes_visited']}") + print(f" 耗时: {result['time_taken']:.6f}秒") + + def handle_mouse_click(self, mouse_x: float, mouse_y: float): + """处理鼠标点击事件""" + if not self.edit_mode: + return + + # 将屏幕坐标转换为网格坐标 + grid_pos = self._screen_to_grid(mouse_x, mouse_y) + if not grid_pos: + return + + row, col = grid_pos + + # 检查坐标是否在网格范围内 + rows, cols = len(self.current_grid), len(self.current_grid[0]) if self.current_grid else (0, 0) + if not (0 <= row < rows and 0 <= col < cols): + return + + # 根据当前工具执行操作 + if self.selected_tool == "add_obstacle": + # 不要在起点和终点添加障碍物 + if (row, col) != self.start_point and (row, col) != self.goal_point: + self.current_grid[row][col] = 1 + self.visualization_manager.visualize_grid(self.current_grid) + + elif self.selected_tool == "remove_obstacle": + # 不要移除起点和终点 + if (row, col) != self.start_point and (row, col) != self.goal_point: + self.current_grid[row][col] = 0 + self.visualization_manager.visualize_grid(self.current_grid) + + elif self.selected_tool == "set_start": + # 移除旧起点的障碍物 + old_row, old_col = self.start_point + if self.current_grid[old_row][old_col] == 1: + self.current_grid[old_row][old_col] = 0 + + self.start_point = (row, col) + print(f"✓ 起点设置为: {row}, {col}") + # 更新可视化 + self.visualization_manager.visualize_grid(self.current_grid) + + elif self.selected_tool == "set_goal": + # 移除旧终点的障碍物 + old_row, old_col = self.goal_point + if self.current_grid[old_row][old_col] == 1: + self.current_grid[old_row][old_col] = 0 + + self.goal_point = (row, col) + print(f"✓ 终点设置为: {row}, {col}") + # 更新可视化 + self.visualization_manager.visualize_grid(self.current_grid) + + def _screen_to_grid(self, screen_x: float, screen_y: float) -> Optional[Tuple[int, int]]: + """ + 将屏幕坐标转换为网格坐标 + + Args: + screen_x: 屏幕X坐标 (-1 到 1) + screen_y: 屏幕Y坐标 (-1 到 1) + + Returns: + 网格坐标 (row, col),如果转换失败则返回None + """ + # 这是一个简化的实现 + # 实际应用中需要根据摄像机位置和方向进行更精确的转换 + + if not self.current_grid: + return None + + rows, cols = len(self.current_grid), len(self.current_grid[0]) + + # 将屏幕坐标映射到网格坐标 + col = int((screen_x + 1) * cols / 2) + row = int((1 - screen_y) * rows / 2) + + return (row, col) + + def toggle_edit_mode(self): + """切换编辑模式""" + self.edit_mode = not self.edit_mode + if self.edit_mode: + print("✓ 进入编辑模式") + else: + print("✓ 退出编辑模式") + + def update(self, dt: float): + """更新编辑器状态""" + # 处理键盘输入 + if self.world.mouseWatcherNode.hasMouse(): + mouse_pos = self.world.mouseWatcherNode.getMouse() + + # 检查鼠标按键 + if self.world.mouseWatcherNode.isButtonDown(MouseWatcher.getGlobalPointer()): + if self.last_mouse_pos is None or \ + (mouse_pos - self.last_mouse_pos).length() > 0.01: + self.handle_mouse_click(mouse_pos.x, mouse_pos.y) + self.last_mouse_pos = mouse_pos + + # 检查键盘快捷键 + if self.world.mouseWatcherNode.isButtonDown(KeyboardButton.asciiKey('e')): + self.toggle_edit_mode() + + def cleanup(self): + """清理编辑器资源""" + self.hide() + self.current_grid = None + self.start_point = None self.goal_point = None \ No newline at end of file diff --git a/plugins/user/pathfinding_algorithms/plugin.py b/plugins/user/pathfinding_algorithms/plugin.py index d5591852..68d805e7 100644 --- a/plugins/user/pathfinding_algorithms/plugin.py +++ b/plugins/user/pathfinding_algorithms/plugin.py @@ -1,616 +1,616 @@ -""" -路径规划算法插件主文件 -提供多种路径规划算法的实现和可视化工具 -""" - -import time -import json -from plugins.plugin_manager import BasePlugin -from .core.algorithm_manager import AlgorithmManager -from .visualization.visualization_manager import VisualizationManager -from .editor.pathfinding_editor import PathfindingEditor - -class Plugin(BasePlugin): - """ - 路径规划算法插件 - 提供多种经典和现代路径规划算法的实现 - """ - - def __init__(self, plugin_manager, name): - super().__init__(plugin_manager, name) - self.config = { - "version": "1.0.0", - "author": "EG Team", - "description": "路径规划算法插件,包含多种经典和现代路径规划算法实现" - } - - # 核心组件 - self.algorithm_manager = None - self.visualization_manager = None - self.editor = None - - # GUI元素 - self.gui_elements = [] - - # 事件处理器 - self.event_handlers = {} - - # 插件状态 - self.is_initialized = False - self.is_enabled = False - - # 性能监控 - self.performance_stats = { - 'initialization_time': 0.0, - 'enable_time': 0.0, - 'last_update_time': 0.0, - 'total_updates': 0 - } - - # 当前场景数据 - self.current_grid = None - self.current_start = (0, 0) - self.current_goal = (9, 9) - - def initialize(self) -> bool: - """ - 初始化插件 - """ - try: - if self.is_initialized: - return True - - init_start_time = time.time() - print(f"初始化路径规划算法插件: {self.name}") - - # 初始化算法管理器 - self.algorithm_manager = AlgorithmManager() - - # 初始化可视化管理器 - self.visualization_manager = VisualizationManager(self.plugin_manager.world) - - # 初始化编辑器 - self.editor = PathfindingEditor( - self.plugin_manager.world, - self.algorithm_manager, - self.visualization_manager - ) - - self.is_initialized = True - self.performance_stats['initialization_time'] = time.time() - init_start_time - print("✓ 路径规划算法插件初始化完成") - return True - - except Exception as e: - print(f"✗ 路径规划算法插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - """ - if not super().enable(): - return False - - if self.is_enabled: - return True - - try: - enable_start_time = time.time() - print(f"启用路径规划算法插件: {self.name}") - - # 启用可视化管理器 - if self.visualization_manager: - self.visualization_manager.enable() - - # 注册事件处理器 - self._register_event_handlers() - - # 创建GUI界面 - self._create_gui() - - self.is_enabled = True - self.performance_stats['enable_time'] = time.time() - enable_start_time - print("✓ 路径规划算法插件启用成功") - return True - - except Exception as e: - print(f"✗ 路径规划算法插件启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self) -> bool: - """ - 禁用插件 - """ - if not super().disable(): - return False - - if not self.is_enabled: - return True - - try: - print(f"禁用路径规划算法插件: {self.name}") - - # 清理GUI界面 - self._cleanup_gui() - - # 移除事件处理器 - self._unregister_event_handlers() - - # 禁用可视化管理器 - if self.visualization_manager: - self.visualization_manager.disable() - - self.is_enabled = False - print("✓ 路径规划算法插件禁用成功") - return True - - except Exception as e: - print(f"✗ 路径规划算法插件禁用失败: {e}") - import traceback - traceback.print_exc() - return False - - def finalize(self): - """ - 清理插件资源 - """ - print(f"清理路径规划算法插件资源: {self.name}") - - # 清理所有组件 - if self.editor: - self.editor.cleanup() - self.editor = None - - if self.visualization_manager: - self.visualization_manager.cleanup() - self.visualization_manager = None - - if self.algorithm_manager: - self.algorithm_manager = None - - self.is_initialized = False - self.is_enabled = False - self.current_grid = None - - def _register_event_handlers(self): - """ - 注册事件处理器 - """ - world = self.plugin_manager.world - - # 路径规划系统控制快捷键 - self.event_handlers['f1'] = self.toggle_editor - self.event_handlers['f2'] = self.toggle_visualization - self.event_handlers['f3'] = self.run_demo - self.event_handlers['f4'] = self.clear_visualization - self.event_handlers['f5'] = self.compare_algorithms - self.event_handlers['f6'] = self.show_stats - self.event_handlers['f7'] = self.generate_report - self.event_handlers['f8'] = self.optimize_paths - self.event_handlers['f9'] = self.generate_random_scenario - self.event_handlers['f10'] = self.toggle_animation - self.event_handlers['f11'] = self.show_algorithm_info - self.event_handlers['f12'] = self.export_detailed_report - - # 注册事件 - for event, handler in self.event_handlers.items(): - world.accept(event, handler) - world.accept(event.upper(), handler) # 同时注册大写事件 - - def _unregister_event_handlers(self): - """ - 移除事件处理器 - """ - world = self.plugin_manager.world - - for event in self.event_handlers.keys(): - world.ignore(event) - world.ignore(event.upper()) - - self.event_handlers.clear() - - def _create_gui(self): - """ - 创建GUI界面 - """ - try: - world = self.plugin_manager.world - - # 检查是否有GUI管理器 - if not hasattr(world, 'gui_manager') or not world.gui_manager: - print("⚠ GUI管理器不可用,跳过GUI创建") - return - - gui_manager = world.gui_manager - - # 创建路径规划控制面板 - panel_button = gui_manager.createGUIButton( - pos=(0.02, 0, 0.75), - text="路径规划", - size=0.06 - ) - self.gui_elements.append(panel_button) - - # 创建功能按钮 - functions = [ - ("编辑器 [F1]", self.toggle_editor), - ("可视化 [F2]", self.toggle_visualization), - ("运行演示 [F3]", self.run_demo), - ("清除视图 [F4]", self.clear_visualization), - ("算法比较 [F5]", self.compare_algorithms), - ("显示统计 [F6]", self.show_stats), - ("生成报告 [F7]", self.generate_report), - ("优化路径 [F8]", self.optimize_paths), - ("随机场景 [F9]", self.generate_random_scenario), - ("动画切换 [F10]", self.toggle_animation), - ("算法信息 [F11]", self.show_algorithm_info), - ("导出报告 [F12]", self.export_detailed_report) - ] - - for i, (name, callback) in enumerate(functions): - button = gui_manager.createGUIButton( - pos=(0.02, 0, 0.68 - i * 0.055), - text=name, - size=0.045 - ) - self.gui_elements.append(button) - world.accept(f"pathfinding_function_{i}", callback) - - except Exception as e: - print(f"⚠ GUI创建失败: {e}") - - def _cleanup_gui(self): - """ - 清理GUI界面 - """ - try: - world = self.plugin_manager.world - - if hasattr(world, 'gui_manager') and world.gui_manager: - gui_manager = world.gui_manager - - # 删除所有GUI元素 - for element in self.gui_elements: - gui_manager.deleteGUIElement(element) - - self.gui_elements.clear() - - except Exception as e: - print(f"⚠ GUI清理失败: {e}") - - # 事件处理方法 - def toggle_editor(self): - """切换编辑器显示""" - if self.editor: - self.editor.toggle_visibility() - print("✓ 编辑器显示已切换") - - def toggle_visualization(self): - """切换可视化显示""" - if self.visualization_manager: - self.visualization_manager.toggle_visibility() - print("✓ 可视化显示已切换") - - def run_demo(self): - """运行演示""" - if self.algorithm_manager and self.visualization_manager: - print("✓ 运行路径规划演示...") - # 运行一个简单的演示 - from .utils.demo_scenarios import create_simple_grid - self.current_grid = create_simple_grid(10, 10) - self.current_start = (0, 0) - self.current_goal = (9, 9) - - # 使用A*算法查找路径 - path, stats = self.algorithm_manager.find_path("astar", self.current_grid, self.current_start, self.current_goal) - - if path: - print(f"✓ 找到路径,长度: {len(path)}") - print(f" 访问节点数: {stats.get('nodes_visited', 0)}") - print(f" 耗时: {stats.get('time_taken', 0):.6f}秒") - # 可视化路径 - self.visualization_manager.visualize_path(path, self.current_grid) - self.visualization_manager.visualize_grid(self.current_grid) - else: - print("✗ 未找到路径") - - def clear_visualization(self): - """清除可视化""" - if self.visualization_manager: - self.visualization_manager.clear_visualization() - print("✓ 可视化已清除") - - def compare_algorithms(self): - """比较算法性能""" - if self.algorithm_manager: - print("✓ 比较路径规划算法...") - from .utils.demo_scenarios import create_simple_grid - self.current_grid = create_simple_grid(20, 20) - self.current_start = (0, 0) - self.current_goal = (19, 19) - - algorithms = ["astar", "dijkstra", "bfs", "dfs"] - results = {} - - for algorithm in algorithms: - path, stats = self.algorithm_manager.find_path(algorithm, self.current_grid, self.current_start, self.current_goal) - results[algorithm] = { - "path_length": len(path) if path else 0, - "nodes_visited": stats.get("nodes_visited", 0), - "time_taken": stats.get("time_taken", 0), - "success": stats.get("success", False) - } - - print("=== 算法性能比较 ===") - for algorithm, result in results.items(): - print(f"{algorithm}:") - print(f" 路径长度: {result['path_length']}") - print(f" 访问节点数: {result['nodes_visited']}") - print(f" 耗时: {result['time_taken']:.6f}秒") - print(f" 成功: {'是' if result['success'] else '否'}") - - def show_stats(self): - """显示统计信息""" - if self.algorithm_manager: - stats = self.algorithm_manager.get_stats() - print("=== 路径规划算法统计 ===") - for key, value in stats.items(): - if key != 'algorithm_usage': - print(f" {key}: {value}") - print(" 算法使用统计:") - for algo, count in stats.get('algorithm_usage', {}).items(): - print(f" {algo}: {count}次") - - def generate_report(self): - """生成报告""" - if self.algorithm_manager: - print("✓ 生成路径规划算法报告...") - report = self.algorithm_manager.generate_report() - # 简化输出报告摘要 - print(f"报告生成时间: {time.ctime(report.get('timestamp', 0))}") - print(f"总算法数: {len(report.get('algorithms', []))}") - print(f"总测试数: {report.get('total_tests', 0)}") - print(f"成功率: {report.get('success_rate', 0):.2%}") - - def optimize_paths(self): - """优化路径""" - print("✓ 路径优化功能...") - if self.current_grid and self.algorithm_manager: - # 根据网格特点推荐算法 - rows = len(self.current_grid) - cols = len(self.current_grid[0]) if rows > 0 else 0 - obstacle_count = sum(sum(row) for row in self.current_grid) - obstacle_density = obstacle_count / (rows * cols) if rows * cols > 0 else 0 - - recommended_algo = self.algorithm_manager.get_algorithm_recommendation( - (rows, cols), obstacle_density, True) - print(f" 根据当前场景特点推荐算法: {recommended_algo}") - - def generate_random_scenario(self): - """生成随机场景""" - if self.visualization_manager: - print("✓ 生成随机路径规划场景...") - from .utils.demo_scenarios import create_random_grid - self.current_grid = create_random_grid(15, 15, 0.25) # 25%障碍物 - self.current_start = (0, 0) - self.current_goal = (14, 14) - - self.visualization_manager.visualize_grid(self.current_grid) - print("✓ 随机场景已生成") - - def toggle_animation(self): - """切换动画""" - if self.visualization_manager: - # 这里可以实现动画切换逻辑 - print("✓ 动画切换功能待实现...") - - def show_algorithm_info(self): - """显示算法信息""" - if self.algorithm_manager: - algorithms = self.algorithm_manager.get_available_algorithms() - print("=== 可用算法信息 ===") - for algo in algorithms: - description = self.algorithm_manager.get_algorithm_description(algo) - complexity = self.algorithm_manager.get_algorithm_complexity(algo) - print(f"{algo}:") - print(f" 描述: {description}") - print(f" 复杂度: {complexity}") - print() - - def export_detailed_report(self): - """导出详细报告""" - if self.algorithm_manager: - print("✓ 导出详细报告...") - timestamp = int(time.time()) - filename = f"pathfinding_report_{timestamp}.json" - success = self.algorithm_manager.export_report(filename) - if success: - print(f"✓ 详细报告已导出到: {filename}") - else: - print("✗ 导出详细报告失败") - - def get_info(self) -> dict: - """ - 获取插件信息 - """ - info = super().get_info() - info.update({ - "version": self.config.get("version", "1.0.0"), - "author": self.config.get("author", "EG Team"), - "description": self.config.get("description", "路径规划算法插件"), - "is_enabled": self.is_enabled, - "is_initialized": self.is_initialized, - "available_algorithms": self.algorithm_manager.get_available_algorithms() if self.algorithm_manager else [] - }) - return info - - # 公共接口方法 - def find_path(self, algorithm, grid, start, goal): - """查找路径""" - if self.algorithm_manager: - return self.algorithm_manager.find_path(algorithm, grid, start, goal) - return None, {} - - def add_algorithm(self, name, algorithm_class, description=""): - """添加新算法""" - if self.algorithm_manager: - self.algorithm_manager.add_algorithm(name, algorithm_class, description) - - def remove_algorithm(self, name): - """移除算法""" - if self.algorithm_manager: - self.algorithm_manager.remove_algorithm(name) - - def get_available_algorithms(self): - """获取可用算法列表""" - if self.algorithm_manager: - return self.algorithm_manager.get_available_algorithms() - return [] - - def get_algorithm_description(self, algorithm): - """获取算法描述""" - if self.algorithm_manager: - return self.algorithm_manager.get_algorithm_description(algorithm) - return "" - - def set_algorithm_parameter(self, algorithm, parameter, value): - """设置算法参数""" - if self.algorithm_manager: - self.algorithm_manager.set_algorithm_parameter(algorithm, parameter, value) - - def get_algorithm_parameters(self, algorithm): - """获取算法参数""" - if self.algorithm_manager: - return self.algorithm_manager.get_algorithm_parameters(algorithm) - return {} - - def visualize_path(self, path, grid=None): - """可视化路径""" - if self.visualization_manager: - self.visualization_manager.visualize_path(path, grid) - - def visualize_grid(self, grid): - """可视化网格""" - if self.visualization_manager: - self.visualization_manager.visualize_grid(grid) - - def visualize_visited_nodes(self, visited_nodes): - """可视化已访问节点""" - if self.visualization_manager: - self.visualization_manager.visualize_visited_nodes(visited_nodes) - - def visualize_open_list(self, open_list): - """可视化开放列表""" - if self.visualization_manager: - self.visualization_manager.visualize_open_list(open_list) - - def clear_path_visualization(self): - """清除路径可视化""" - if self.visualization_manager: - self.visualization_manager.clear_path_visualization() - - def set_visualization_settings(self, settings): - """设置可视化参数""" - if self.visualization_manager: - self.visualization_manager.set_settings(settings) - - def get_visualization_settings(self): - """获取可视化参数""" - if self.visualization_manager: - return self.visualization_manager.get_settings() - return {} - - def enable_visualization_animation(self, enable=True): - """启用可视化动画""" - if self.visualization_manager: - self.visualization_manager.enable_animation(enable) - - def update(self, dt: float): - """更新插件状态""" - self.performance_stats['total_updates'] += 1 - self.performance_stats['last_update_time'] = time.time() - - # 更新编辑器 - if self.editor: - self.editor.update(dt) - - # 更新可视化动画 - if self.visualization_manager: - self.visualization_manager.update_animation(dt) - - def batch_test_algorithms(self, algorithms, test_cases): - """批量测试算法""" - if self.algorithm_manager: - return self.algorithm_manager.batch_test(algorithms, test_cases) - return {} - - def compare_algorithms_detailed(self, algorithms, grid, start, goal): - """详细比较算法""" - if self.algorithm_manager: - return self.algorithm_manager.compare_algorithms(algorithms, grid, start, goal) - return {} - - def get_cached_performance_data(self, algorithm): - """获取缓存的性能数据""" - if self.algorithm_manager: - return self.algorithm_manager.get_cached_performance_data(algorithm) - return [] - - def clear_performance_cache(self): - """清除性能缓存""" - if self.algorithm_manager: - self.algorithm_manager.clear_performance_cache() - - def reset_statistics(self): - """重置统计信息""" - if self.algorithm_manager: - self.algorithm_manager.reset_stats() - print("✓ 统计信息已重置") - - def validate_grid_data(self, grid): - """验证网格数据""" - if self.algorithm_manager: - return self.algorithm_manager.validate_grid(grid) - return {'valid': False, 'error': '算法管理器未初始化'} - - def get_algorithm_complexity(self, algorithm): - """获取算法复杂度信息""" - if self.algorithm_manager: - return self.algorithm_manager.get_algorithm_complexity(algorithm) - return "未知" - - def benchmark_algorithm(self, algorithm, grid, start, goal, iterations=100): - """对算法进行基准测试""" - if self.algorithm_manager and algorithm in self.algorithm_manager.algorithms: - # 创建算法实例 - algo_class = self.algorithm_manager.algorithms[algorithm] - algo_instance = algo_class() - - # 设置参数 - if algorithm in self.algorithm_manager.algorithm_parameters: - for param, value in self.algorithm_manager.algorithm_parameters[algorithm].items(): - if hasattr(algo_instance, param): - setattr(algo_instance, param, value) - - # 执行基准测试 - if hasattr(algo_instance, 'benchmark'): - return algo_instance.benchmark(grid, start, goal, iterations) - else: - print(f"算法 {algorithm} 不支持基准测试") - return {} - return {} - - def optimize_algorithm_for_grid(self, algorithm, grid): - """为特定网格优化算法""" - if algorithm == 'jps' and self.algorithm_manager: - from .algorithms.jps import JPS - jps_instance = JPS() - jps_instance.optimize_for_map(grid) - print(f"✓ JPS算法已为当前网格优化") - else: +""" +路径规划算法插件主文件 +提供多种路径规划算法的实现和可视化工具 +""" + +import time +import json +from plugins.plugin_manager import BasePlugin +from .core.algorithm_manager import AlgorithmManager +from .visualization.visualization_manager import VisualizationManager +from .editor.pathfinding_editor import PathfindingEditor + +class Plugin(BasePlugin): + """ + 路径规划算法插件 + 提供多种经典和现代路径规划算法的实现 + """ + + def __init__(self, plugin_manager, name): + super().__init__(plugin_manager, name) + self.config = { + "version": "1.0.0", + "author": "EG Team", + "description": "路径规划算法插件,包含多种经典和现代路径规划算法实现" + } + + # 核心组件 + self.algorithm_manager = None + self.visualization_manager = None + self.editor = None + + # GUI元素 + self.gui_elements = [] + + # 事件处理器 + self.event_handlers = {} + + # 插件状态 + self.is_initialized = False + self.is_enabled = False + + # 性能监控 + self.performance_stats = { + 'initialization_time': 0.0, + 'enable_time': 0.0, + 'last_update_time': 0.0, + 'total_updates': 0 + } + + # 当前场景数据 + self.current_grid = None + self.current_start = (0, 0) + self.current_goal = (9, 9) + + def initialize(self) -> bool: + """ + 初始化插件 + """ + try: + if self.is_initialized: + return True + + init_start_time = time.time() + print(f"初始化路径规划算法插件: {self.name}") + + # 初始化算法管理器 + self.algorithm_manager = AlgorithmManager() + + # 初始化可视化管理器 + self.visualization_manager = VisualizationManager(self.plugin_manager.world) + + # 初始化编辑器 + self.editor = PathfindingEditor( + self.plugin_manager.world, + self.algorithm_manager, + self.visualization_manager + ) + + self.is_initialized = True + self.performance_stats['initialization_time'] = time.time() - init_start_time + print("✓ 路径规划算法插件初始化完成") + return True + + except Exception as e: + print(f"✗ 路径规划算法插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + """ + if not super().enable(): + return False + + if self.is_enabled: + return True + + try: + enable_start_time = time.time() + print(f"启用路径规划算法插件: {self.name}") + + # 启用可视化管理器 + if self.visualization_manager: + self.visualization_manager.enable() + + # 注册事件处理器 + self._register_event_handlers() + + # 创建GUI界面 + self._create_gui() + + self.is_enabled = True + self.performance_stats['enable_time'] = time.time() - enable_start_time + print("✓ 路径规划算法插件启用成功") + return True + + except Exception as e: + print(f"✗ 路径规划算法插件启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self) -> bool: + """ + 禁用插件 + """ + if not super().disable(): + return False + + if not self.is_enabled: + return True + + try: + print(f"禁用路径规划算法插件: {self.name}") + + # 清理GUI界面 + self._cleanup_gui() + + # 移除事件处理器 + self._unregister_event_handlers() + + # 禁用可视化管理器 + if self.visualization_manager: + self.visualization_manager.disable() + + self.is_enabled = False + print("✓ 路径规划算法插件禁用成功") + return True + + except Exception as e: + print(f"✗ 路径规划算法插件禁用失败: {e}") + import traceback + traceback.print_exc() + return False + + def finalize(self): + """ + 清理插件资源 + """ + print(f"清理路径规划算法插件资源: {self.name}") + + # 清理所有组件 + if self.editor: + self.editor.cleanup() + self.editor = None + + if self.visualization_manager: + self.visualization_manager.cleanup() + self.visualization_manager = None + + if self.algorithm_manager: + self.algorithm_manager = None + + self.is_initialized = False + self.is_enabled = False + self.current_grid = None + + def _register_event_handlers(self): + """ + 注册事件处理器 + """ + world = self.plugin_manager.world + + # 路径规划系统控制快捷键 + self.event_handlers['f1'] = self.toggle_editor + self.event_handlers['f2'] = self.toggle_visualization + self.event_handlers['f3'] = self.run_demo + self.event_handlers['f4'] = self.clear_visualization + self.event_handlers['f5'] = self.compare_algorithms + self.event_handlers['f6'] = self.show_stats + self.event_handlers['f7'] = self.generate_report + self.event_handlers['f8'] = self.optimize_paths + self.event_handlers['f9'] = self.generate_random_scenario + self.event_handlers['f10'] = self.toggle_animation + self.event_handlers['f11'] = self.show_algorithm_info + self.event_handlers['f12'] = self.export_detailed_report + + # 注册事件 + for event, handler in self.event_handlers.items(): + world.accept(event, handler) + world.accept(event.upper(), handler) # 同时注册大写事件 + + def _unregister_event_handlers(self): + """ + 移除事件处理器 + """ + world = self.plugin_manager.world + + for event in self.event_handlers.keys(): + world.ignore(event) + world.ignore(event.upper()) + + self.event_handlers.clear() + + def _create_gui(self): + """ + 创建GUI界面 + """ + try: + world = self.plugin_manager.world + + # 检查是否有GUI管理器 + if not hasattr(world, 'gui_manager') or not world.gui_manager: + print("⚠ GUI管理器不可用,跳过GUI创建") + return + + gui_manager = world.gui_manager + + # 创建路径规划控制面板 + panel_button = gui_manager.createGUIButton( + pos=(0.02, 0, 0.75), + text="路径规划", + size=0.06 + ) + self.gui_elements.append(panel_button) + + # 创建功能按钮 + functions = [ + ("编辑器 [F1]", self.toggle_editor), + ("可视化 [F2]", self.toggle_visualization), + ("运行演示 [F3]", self.run_demo), + ("清除视图 [F4]", self.clear_visualization), + ("算法比较 [F5]", self.compare_algorithms), + ("显示统计 [F6]", self.show_stats), + ("生成报告 [F7]", self.generate_report), + ("优化路径 [F8]", self.optimize_paths), + ("随机场景 [F9]", self.generate_random_scenario), + ("动画切换 [F10]", self.toggle_animation), + ("算法信息 [F11]", self.show_algorithm_info), + ("导出报告 [F12]", self.export_detailed_report) + ] + + for i, (name, callback) in enumerate(functions): + button = gui_manager.createGUIButton( + pos=(0.02, 0, 0.68 - i * 0.055), + text=name, + size=0.045 + ) + self.gui_elements.append(button) + world.accept(f"pathfinding_function_{i}", callback) + + except Exception as e: + print(f"⚠ GUI创建失败: {e}") + + def _cleanup_gui(self): + """ + 清理GUI界面 + """ + try: + world = self.plugin_manager.world + + if hasattr(world, 'gui_manager') and world.gui_manager: + gui_manager = world.gui_manager + + # 删除所有GUI元素 + for element in self.gui_elements: + gui_manager.deleteGUIElement(element) + + self.gui_elements.clear() + + except Exception as e: + print(f"⚠ GUI清理失败: {e}") + + # 事件处理方法 + def toggle_editor(self): + """切换编辑器显示""" + if self.editor: + self.editor.toggle_visibility() + print("✓ 编辑器显示已切换") + + def toggle_visualization(self): + """切换可视化显示""" + if self.visualization_manager: + self.visualization_manager.toggle_visibility() + print("✓ 可视化显示已切换") + + def run_demo(self): + """运行演示""" + if self.algorithm_manager and self.visualization_manager: + print("✓ 运行路径规划演示...") + # 运行一个简单的演示 + from .utils.demo_scenarios import create_simple_grid + self.current_grid = create_simple_grid(10, 10) + self.current_start = (0, 0) + self.current_goal = (9, 9) + + # 使用A*算法查找路径 + path, stats = self.algorithm_manager.find_path("astar", self.current_grid, self.current_start, self.current_goal) + + if path: + print(f"✓ 找到路径,长度: {len(path)}") + print(f" 访问节点数: {stats.get('nodes_visited', 0)}") + print(f" 耗时: {stats.get('time_taken', 0):.6f}秒") + # 可视化路径 + self.visualization_manager.visualize_path(path, self.current_grid) + self.visualization_manager.visualize_grid(self.current_grid) + else: + print("✗ 未找到路径") + + def clear_visualization(self): + """清除可视化""" + if self.visualization_manager: + self.visualization_manager.clear_visualization() + print("✓ 可视化已清除") + + def compare_algorithms(self): + """比较算法性能""" + if self.algorithm_manager: + print("✓ 比较路径规划算法...") + from .utils.demo_scenarios import create_simple_grid + self.current_grid = create_simple_grid(20, 20) + self.current_start = (0, 0) + self.current_goal = (19, 19) + + algorithms = ["astar", "dijkstra", "bfs", "dfs"] + results = {} + + for algorithm in algorithms: + path, stats = self.algorithm_manager.find_path(algorithm, self.current_grid, self.current_start, self.current_goal) + results[algorithm] = { + "path_length": len(path) if path else 0, + "nodes_visited": stats.get("nodes_visited", 0), + "time_taken": stats.get("time_taken", 0), + "success": stats.get("success", False) + } + + print("=== 算法性能比较 ===") + for algorithm, result in results.items(): + print(f"{algorithm}:") + print(f" 路径长度: {result['path_length']}") + print(f" 访问节点数: {result['nodes_visited']}") + print(f" 耗时: {result['time_taken']:.6f}秒") + print(f" 成功: {'是' if result['success'] else '否'}") + + def show_stats(self): + """显示统计信息""" + if self.algorithm_manager: + stats = self.algorithm_manager.get_stats() + print("=== 路径规划算法统计 ===") + for key, value in stats.items(): + if key != 'algorithm_usage': + print(f" {key}: {value}") + print(" 算法使用统计:") + for algo, count in stats.get('algorithm_usage', {}).items(): + print(f" {algo}: {count}次") + + def generate_report(self): + """生成报告""" + if self.algorithm_manager: + print("✓ 生成路径规划算法报告...") + report = self.algorithm_manager.generate_report() + # 简化输出报告摘要 + print(f"报告生成时间: {time.ctime(report.get('timestamp', 0))}") + print(f"总算法数: {len(report.get('algorithms', []))}") + print(f"总测试数: {report.get('total_tests', 0)}") + print(f"成功率: {report.get('success_rate', 0):.2%}") + + def optimize_paths(self): + """优化路径""" + print("✓ 路径优化功能...") + if self.current_grid and self.algorithm_manager: + # 根据网格特点推荐算法 + rows = len(self.current_grid) + cols = len(self.current_grid[0]) if rows > 0 else 0 + obstacle_count = sum(sum(row) for row in self.current_grid) + obstacle_density = obstacle_count / (rows * cols) if rows * cols > 0 else 0 + + recommended_algo = self.algorithm_manager.get_algorithm_recommendation( + (rows, cols), obstacle_density, True) + print(f" 根据当前场景特点推荐算法: {recommended_algo}") + + def generate_random_scenario(self): + """生成随机场景""" + if self.visualization_manager: + print("✓ 生成随机路径规划场景...") + from .utils.demo_scenarios import create_random_grid + self.current_grid = create_random_grid(15, 15, 0.25) # 25%障碍物 + self.current_start = (0, 0) + self.current_goal = (14, 14) + + self.visualization_manager.visualize_grid(self.current_grid) + print("✓ 随机场景已生成") + + def toggle_animation(self): + """切换动画""" + if self.visualization_manager: + # 这里可以实现动画切换逻辑 + print("✓ 动画切换功能待实现...") + + def show_algorithm_info(self): + """显示算法信息""" + if self.algorithm_manager: + algorithms = self.algorithm_manager.get_available_algorithms() + print("=== 可用算法信息 ===") + for algo in algorithms: + description = self.algorithm_manager.get_algorithm_description(algo) + complexity = self.algorithm_manager.get_algorithm_complexity(algo) + print(f"{algo}:") + print(f" 描述: {description}") + print(f" 复杂度: {complexity}") + print() + + def export_detailed_report(self): + """导出详细报告""" + if self.algorithm_manager: + print("✓ 导出详细报告...") + timestamp = int(time.time()) + filename = f"pathfinding_report_{timestamp}.json" + success = self.algorithm_manager.export_report(filename) + if success: + print(f"✓ 详细报告已导出到: {filename}") + else: + print("✗ 导出详细报告失败") + + def get_info(self) -> dict: + """ + 获取插件信息 + """ + info = super().get_info() + info.update({ + "version": self.config.get("version", "1.0.0"), + "author": self.config.get("author", "EG Team"), + "description": self.config.get("description", "路径规划算法插件"), + "is_enabled": self.is_enabled, + "is_initialized": self.is_initialized, + "available_algorithms": self.algorithm_manager.get_available_algorithms() if self.algorithm_manager else [] + }) + return info + + # 公共接口方法 + def find_path(self, algorithm, grid, start, goal): + """查找路径""" + if self.algorithm_manager: + return self.algorithm_manager.find_path(algorithm, grid, start, goal) + return None, {} + + def add_algorithm(self, name, algorithm_class, description=""): + """添加新算法""" + if self.algorithm_manager: + self.algorithm_manager.add_algorithm(name, algorithm_class, description) + + def remove_algorithm(self, name): + """移除算法""" + if self.algorithm_manager: + self.algorithm_manager.remove_algorithm(name) + + def get_available_algorithms(self): + """获取可用算法列表""" + if self.algorithm_manager: + return self.algorithm_manager.get_available_algorithms() + return [] + + def get_algorithm_description(self, algorithm): + """获取算法描述""" + if self.algorithm_manager: + return self.algorithm_manager.get_algorithm_description(algorithm) + return "" + + def set_algorithm_parameter(self, algorithm, parameter, value): + """设置算法参数""" + if self.algorithm_manager: + self.algorithm_manager.set_algorithm_parameter(algorithm, parameter, value) + + def get_algorithm_parameters(self, algorithm): + """获取算法参数""" + if self.algorithm_manager: + return self.algorithm_manager.get_algorithm_parameters(algorithm) + return {} + + def visualize_path(self, path, grid=None): + """可视化路径""" + if self.visualization_manager: + self.visualization_manager.visualize_path(path, grid) + + def visualize_grid(self, grid): + """可视化网格""" + if self.visualization_manager: + self.visualization_manager.visualize_grid(grid) + + def visualize_visited_nodes(self, visited_nodes): + """可视化已访问节点""" + if self.visualization_manager: + self.visualization_manager.visualize_visited_nodes(visited_nodes) + + def visualize_open_list(self, open_list): + """可视化开放列表""" + if self.visualization_manager: + self.visualization_manager.visualize_open_list(open_list) + + def clear_path_visualization(self): + """清除路径可视化""" + if self.visualization_manager: + self.visualization_manager.clear_path_visualization() + + def set_visualization_settings(self, settings): + """设置可视化参数""" + if self.visualization_manager: + self.visualization_manager.set_settings(settings) + + def get_visualization_settings(self): + """获取可视化参数""" + if self.visualization_manager: + return self.visualization_manager.get_settings() + return {} + + def enable_visualization_animation(self, enable=True): + """启用可视化动画""" + if self.visualization_manager: + self.visualization_manager.enable_animation(enable) + + def update(self, dt: float): + """更新插件状态""" + self.performance_stats['total_updates'] += 1 + self.performance_stats['last_update_time'] = time.time() + + # 更新编辑器 + if self.editor: + self.editor.update(dt) + + # 更新可视化动画 + if self.visualization_manager: + self.visualization_manager.update_animation(dt) + + def batch_test_algorithms(self, algorithms, test_cases): + """批量测试算法""" + if self.algorithm_manager: + return self.algorithm_manager.batch_test(algorithms, test_cases) + return {} + + def compare_algorithms_detailed(self, algorithms, grid, start, goal): + """详细比较算法""" + if self.algorithm_manager: + return self.algorithm_manager.compare_algorithms(algorithms, grid, start, goal) + return {} + + def get_cached_performance_data(self, algorithm): + """获取缓存的性能数据""" + if self.algorithm_manager: + return self.algorithm_manager.get_cached_performance_data(algorithm) + return [] + + def clear_performance_cache(self): + """清除性能缓存""" + if self.algorithm_manager: + self.algorithm_manager.clear_performance_cache() + + def reset_statistics(self): + """重置统计信息""" + if self.algorithm_manager: + self.algorithm_manager.reset_stats() + print("✓ 统计信息已重置") + + def validate_grid_data(self, grid): + """验证网格数据""" + if self.algorithm_manager: + return self.algorithm_manager.validate_grid(grid) + return {'valid': False, 'error': '算法管理器未初始化'} + + def get_algorithm_complexity(self, algorithm): + """获取算法复杂度信息""" + if self.algorithm_manager: + return self.algorithm_manager.get_algorithm_complexity(algorithm) + return "未知" + + def benchmark_algorithm(self, algorithm, grid, start, goal, iterations=100): + """对算法进行基准测试""" + if self.algorithm_manager and algorithm in self.algorithm_manager.algorithms: + # 创建算法实例 + algo_class = self.algorithm_manager.algorithms[algorithm] + algo_instance = algo_class() + + # 设置参数 + if algorithm in self.algorithm_manager.algorithm_parameters: + for param, value in self.algorithm_manager.algorithm_parameters[algorithm].items(): + if hasattr(algo_instance, param): + setattr(algo_instance, param, value) + + # 执行基准测试 + if hasattr(algo_instance, 'benchmark'): + return algo_instance.benchmark(grid, start, goal, iterations) + else: + print(f"算法 {algorithm} 不支持基准测试") + return {} + return {} + + def optimize_algorithm_for_grid(self, algorithm, grid): + """为特定网格优化算法""" + if algorithm == 'jps' and self.algorithm_manager: + from .algorithms.jps import JPS + jps_instance = JPS() + jps_instance.optimize_for_map(grid) + print(f"✓ JPS算法已为当前网格优化") + else: print(f"算法 {algorithm} 不支持自动优化") \ No newline at end of file diff --git a/plugins/user/pathfinding_algorithms/utils/demo_scenarios.py b/plugins/user/pathfinding_algorithms/utils/demo_scenarios.py index ddcd6f89..89845049 100644 --- a/plugins/user/pathfinding_algorithms/utils/demo_scenarios.py +++ b/plugins/user/pathfinding_algorithms/utils/demo_scenarios.py @@ -1,257 +1,257 @@ -""" -演示场景工具 -提供各种路径规划演示场景的创建功能 -""" - -from typing import List, Tuple -import random - -def create_simple_grid(rows: int, cols: int) -> List[List[int]]: - """ - 创建简单的空网格 - - Args: - rows: 行数 - cols: 列数 - - Returns: - 网格数据 - """ - return [[0 for _ in range(cols)] for _ in range(rows)] - -def create_maze_grid(rows: int, cols: int) -> List[List[int]]: - """ - 创建迷宫网格(使用深度优先搜索算法生成) - - Args: - rows: 行数 - cols: 列数 - - Returns: - 迷宫网格数据 - """ - # 确保行列数为奇数 - if rows % 2 == 0: - rows += 1 - if cols % 2 == 0: - cols += 1 - - # 初始化网格(全部设为墙) - grid = [[1 for _ in range(cols)] for _ in range(rows)] - - # 使用深度优先搜索生成迷宫 - stack = [] - - # 起点 - start_row, start_col = 1, 1 - grid[start_row][start_col] = 0 - stack.append((start_row, start_col)) - - # 方向:上、右、下、左 - directions = [(-2, 0), (0, 2), (2, 0), (0, -2)] - - while stack: - current_row, current_col = stack[-1] - - # 获取未访问的邻居 - neighbors = [] - for dr, dc in directions: - new_row, new_col = current_row + dr, current_col + dc - if 1 <= new_row < rows - 1 and 1 <= new_col < cols - 1: - if grid[new_row][new_col] == 1: - neighbors.append((new_row, new_col, dr // 2, dc // 2)) - - if neighbors: - # 随机选择一个邻居 - new_row, new_col, wall_row, wall_col = random.choice(neighbors) - - # 打通当前单元格和选中邻居之间的墙 - grid[new_row][new_col] = 0 - grid[current_row + wall_row][current_col + wall_col] = 0 - - # 将选中的邻居加入栈 - stack.append((new_row, new_col)) - else: - # 回溯 - stack.pop() - - return grid - -def create_room_grid(rows: int, cols: int) -> List[List[int]]: - """ - 创建房间网格(包含多个房间和走廊) - - Args: - rows: 行数 - cols: 列数 - - Returns: - 房间网格数据 - """ - grid = [[0 for _ in range(cols)] for _ in range(rows)] - - # 添加一些房间(用障碍物围起来的空心矩形) - rooms = [ - (2, 2, 5, 5), # (row, col, height, width) - (2, 10, 5, 5), - (10, 2, 5, 5), - (10, 10, 5, 5) - ] - - for room_row, room_col, room_height, room_width in rooms: - # 创建房间的墙 - for r in range(room_row, min(room_row + room_height, rows)): - for c in range(room_col, min(room_col + room_width, cols)): - if (r == room_row or r == room_row + room_height - 1 or - c == room_col or c == room_col + room_width - 1): - if 0 <= r < rows and 0 <= c < cols: - grid[r][c] = 1 - - # 添加走廊连接房间 - # 水平走廊 - for c in range(7, 10): - if 4 < rows: - grid[4][c] = 0 - - # 垂直走廊 - for r in range(7, 10): - if 4 < cols: - grid[r][4] = 0 - if 12 < cols: - grid[r][12] = 0 - - return grid - -def create_random_grid(rows: int, cols: int, obstacle_probability: float = 0.3) -> List[List[int]]: - """ - 创建随机网格 - - Args: - rows: 行数 - cols: 列数 - obstacle_probability: 障碍物概率 - - Returns: - 随机网格数据 - """ - grid = [] - for r in range(rows): - row = [] - for c in range(cols): - if random.random() < obstacle_probability: - row.append(1) # 障碍物 - else: - row.append(0) # 空地 - grid.append(row) - return grid - -def create_tunnel_grid(rows: int, cols: int) -> List[List[int]]: - """ - 创建隧道网格(多条曲折的隧道) - - Args: - rows: 行数 - cols: 列数 - - Returns: - 隧道网格数据 - """ - grid = [[1 for _ in range(cols)] for _ in range(rows)] # 全部初始化为墙 - - # 创建主要隧道 - # 水平隧道 - tunnel_row = rows // 2 - for c in range(1, cols - 1): - grid[tunnel_row][c] = 0 - - # 垂直隧道 - tunnel_col = cols // 2 - for r in range(1, rows - 1): - grid[r][tunnel_col] = 0 - - # 添加一些分支隧道 - branch_positions = [(tunnel_row, tunnel_col // 4), - (tunnel_row, 3 * tunnel_col // 4), - (tunnel_row // 4, tunnel_col), - (3 * tunnel_row // 4, tunnel_col)] - - for br, bc in branch_positions: - # 水平分支 - for c in range(max(1, bc - 3), min(cols - 1, bc + 4)): - grid[br][c] = 0 - # 垂直分支 - for r in range(max(1, br - 3), min(rows - 1, br + 4)): - grid[r][bc] = 0 - - return grid - -def get_scenario_names() -> List[str]: - """ - 获取可用场景名称 - - Returns: - 场景名称列表 - """ - return ["simple", "maze", "room", "random", "tunnel"] - -def create_scenario(scenario_name: str, rows: int = 20, cols: int = 20) -> List[List[int]]: - """ - 创建指定场景 - - Args: - scenario_name: 场景名称 - rows: 行数 - cols: 列数 - - Returns: - 网格数据 - """ - if scenario_name == "simple": - return create_simple_grid(rows, cols) - elif scenario_name == "maze": - return create_maze_grid(rows, cols) - elif scenario_name == "room": - return create_room_grid(rows, cols) - elif scenario_name == "random": - return create_random_grid(rows, cols) - elif scenario_name == "tunnel": - return create_tunnel_grid(rows, cols) - else: - return create_simple_grid(rows, cols) - -def get_suggested_start_goal(scenario_name: str, grid: List[List[int]]) -> Tuple[Tuple[int, int], Tuple[int, int]]: - """ - 获取场景建议的起点和终点 - - Args: - scenario_name: 场景名称 - grid: 网格数据 - - Returns: - (起点, 终点) 元组 - """ - rows = len(grid) - cols = len(grid[0]) if rows > 0 else 0 - - if scenario_name == "simple": - return (0, 0), (rows - 1, cols - 1) - elif scenario_name == "maze": - return (1, 1), (rows - 2, cols - 2) - elif scenario_name == "room": - return (0, 0), (rows - 1, cols - 1) - elif scenario_name == "random": - # 寻找空地作为起点和终点 - start = None - goal = None - for r in range(rows): - for c in range(cols): - if grid[r][c] == 0: - if start is None: - start = (r, c) - else: - goal = (r, c) - return start or (0, 0), goal or (rows - 1, cols - 1) - elif scenario_name == "tunnel": - return (1, 1), (rows - 2, cols - 2) - else: +""" +演示场景工具 +提供各种路径规划演示场景的创建功能 +""" + +from typing import List, Tuple +import random + +def create_simple_grid(rows: int, cols: int) -> List[List[int]]: + """ + 创建简单的空网格 + + Args: + rows: 行数 + cols: 列数 + + Returns: + 网格数据 + """ + return [[0 for _ in range(cols)] for _ in range(rows)] + +def create_maze_grid(rows: int, cols: int) -> List[List[int]]: + """ + 创建迷宫网格(使用深度优先搜索算法生成) + + Args: + rows: 行数 + cols: 列数 + + Returns: + 迷宫网格数据 + """ + # 确保行列数为奇数 + if rows % 2 == 0: + rows += 1 + if cols % 2 == 0: + cols += 1 + + # 初始化网格(全部设为墙) + grid = [[1 for _ in range(cols)] for _ in range(rows)] + + # 使用深度优先搜索生成迷宫 + stack = [] + + # 起点 + start_row, start_col = 1, 1 + grid[start_row][start_col] = 0 + stack.append((start_row, start_col)) + + # 方向:上、右、下、左 + directions = [(-2, 0), (0, 2), (2, 0), (0, -2)] + + while stack: + current_row, current_col = stack[-1] + + # 获取未访问的邻居 + neighbors = [] + for dr, dc in directions: + new_row, new_col = current_row + dr, current_col + dc + if 1 <= new_row < rows - 1 and 1 <= new_col < cols - 1: + if grid[new_row][new_col] == 1: + neighbors.append((new_row, new_col, dr // 2, dc // 2)) + + if neighbors: + # 随机选择一个邻居 + new_row, new_col, wall_row, wall_col = random.choice(neighbors) + + # 打通当前单元格和选中邻居之间的墙 + grid[new_row][new_col] = 0 + grid[current_row + wall_row][current_col + wall_col] = 0 + + # 将选中的邻居加入栈 + stack.append((new_row, new_col)) + else: + # 回溯 + stack.pop() + + return grid + +def create_room_grid(rows: int, cols: int) -> List[List[int]]: + """ + 创建房间网格(包含多个房间和走廊) + + Args: + rows: 行数 + cols: 列数 + + Returns: + 房间网格数据 + """ + grid = [[0 for _ in range(cols)] for _ in range(rows)] + + # 添加一些房间(用障碍物围起来的空心矩形) + rooms = [ + (2, 2, 5, 5), # (row, col, height, width) + (2, 10, 5, 5), + (10, 2, 5, 5), + (10, 10, 5, 5) + ] + + for room_row, room_col, room_height, room_width in rooms: + # 创建房间的墙 + for r in range(room_row, min(room_row + room_height, rows)): + for c in range(room_col, min(room_col + room_width, cols)): + if (r == room_row or r == room_row + room_height - 1 or + c == room_col or c == room_col + room_width - 1): + if 0 <= r < rows and 0 <= c < cols: + grid[r][c] = 1 + + # 添加走廊连接房间 + # 水平走廊 + for c in range(7, 10): + if 4 < rows: + grid[4][c] = 0 + + # 垂直走廊 + for r in range(7, 10): + if 4 < cols: + grid[r][4] = 0 + if 12 < cols: + grid[r][12] = 0 + + return grid + +def create_random_grid(rows: int, cols: int, obstacle_probability: float = 0.3) -> List[List[int]]: + """ + 创建随机网格 + + Args: + rows: 行数 + cols: 列数 + obstacle_probability: 障碍物概率 + + Returns: + 随机网格数据 + """ + grid = [] + for r in range(rows): + row = [] + for c in range(cols): + if random.random() < obstacle_probability: + row.append(1) # 障碍物 + else: + row.append(0) # 空地 + grid.append(row) + return grid + +def create_tunnel_grid(rows: int, cols: int) -> List[List[int]]: + """ + 创建隧道网格(多条曲折的隧道) + + Args: + rows: 行数 + cols: 列数 + + Returns: + 隧道网格数据 + """ + grid = [[1 for _ in range(cols)] for _ in range(rows)] # 全部初始化为墙 + + # 创建主要隧道 + # 水平隧道 + tunnel_row = rows // 2 + for c in range(1, cols - 1): + grid[tunnel_row][c] = 0 + + # 垂直隧道 + tunnel_col = cols // 2 + for r in range(1, rows - 1): + grid[r][tunnel_col] = 0 + + # 添加一些分支隧道 + branch_positions = [(tunnel_row, tunnel_col // 4), + (tunnel_row, 3 * tunnel_col // 4), + (tunnel_row // 4, tunnel_col), + (3 * tunnel_row // 4, tunnel_col)] + + for br, bc in branch_positions: + # 水平分支 + for c in range(max(1, bc - 3), min(cols - 1, bc + 4)): + grid[br][c] = 0 + # 垂直分支 + for r in range(max(1, br - 3), min(rows - 1, br + 4)): + grid[r][bc] = 0 + + return grid + +def get_scenario_names() -> List[str]: + """ + 获取可用场景名称 + + Returns: + 场景名称列表 + """ + return ["simple", "maze", "room", "random", "tunnel"] + +def create_scenario(scenario_name: str, rows: int = 20, cols: int = 20) -> List[List[int]]: + """ + 创建指定场景 + + Args: + scenario_name: 场景名称 + rows: 行数 + cols: 列数 + + Returns: + 网格数据 + """ + if scenario_name == "simple": + return create_simple_grid(rows, cols) + elif scenario_name == "maze": + return create_maze_grid(rows, cols) + elif scenario_name == "room": + return create_room_grid(rows, cols) + elif scenario_name == "random": + return create_random_grid(rows, cols) + elif scenario_name == "tunnel": + return create_tunnel_grid(rows, cols) + else: + return create_simple_grid(rows, cols) + +def get_suggested_start_goal(scenario_name: str, grid: List[List[int]]) -> Tuple[Tuple[int, int], Tuple[int, int]]: + """ + 获取场景建议的起点和终点 + + Args: + scenario_name: 场景名称 + grid: 网格数据 + + Returns: + (起点, 终点) 元组 + """ + rows = len(grid) + cols = len(grid[0]) if rows > 0 else 0 + + if scenario_name == "simple": + return (0, 0), (rows - 1, cols - 1) + elif scenario_name == "maze": + return (1, 1), (rows - 2, cols - 2) + elif scenario_name == "room": + return (0, 0), (rows - 1, cols - 1) + elif scenario_name == "random": + # 寻找空地作为起点和终点 + start = None + goal = None + for r in range(rows): + for c in range(cols): + if grid[r][c] == 0: + if start is None: + start = (r, c) + else: + goal = (r, c) + return start or (0, 0), goal or (rows - 1, cols - 1) + elif scenario_name == "tunnel": + return (1, 1), (rows - 2, cols - 2) + else: return (0, 0), (rows - 1, cols - 1) \ No newline at end of file diff --git a/plugins/user/pathfinding_algorithms/visualization/visualization_manager.py b/plugins/user/pathfinding_algorithms/visualization/visualization_manager.py index 26d81f7d..33517bc4 100644 --- a/plugins/user/pathfinding_algorithms/visualization/visualization_manager.py +++ b/plugins/user/pathfinding_algorithms/visualization/visualization_manager.py @@ -1,679 +1,679 @@ -""" -可视化管理器 -负责路径规划算法的可视化显示 -""" - -from typing import List, Tuple, Optional, Dict, Any -from panda3d.core import NodePath, GeomNode, GeomVertexFormat, GeomVertexData -from panda3d.core import GeomVertexWriter, GeomTriangles, GeomLines, GeomPoints -from panda3d.core import LColor, Point3, Material, TransparencyAttrib -import math - -class VisualizationManager: - """ - 可视化管理器 - 负责路径规划算法的可视化显示 - """ - - def __init__(self, world): - """ - 初始化可视化管理器 - - Args: - world: 3D世界对象 - """ - self.world = world - self.enabled = False - - # 可视化节点 - self.grid_node = None - self.path_node = None - self.start_node = None - self.goal_node = None - self.visited_nodes_node = None - self.open_list_node = None - - # 可视化设置 - self.settings = { - 'grid_visible': True, - 'path_visible': True, - 'start_visible': True, - 'goal_visible': True, - 'visited_nodes_visible': False, - 'open_list_visible': False, - 'grid_color': LColor(0.5, 0.5, 0.5, 1.0), - 'path_color': LColor(1.0, 0.0, 0.0, 1.0), - 'start_color': LColor(0.0, 1.0, 0.0, 1.0), - 'goal_color': LColor(0.0, 0.0, 1.0, 1.0), - 'visited_nodes_color': LColor(0.0, 0.5, 1.0, 0.7), - 'open_list_color': LColor(1.0, 1.0, 0.0, 0.7), - 'obstacle_color': LColor(0.2, 0.2, 0.2, 1.0), - 'cell_size': 1.0, - 'cell_height': 0.1, - 'path_thickness': 4.0, - 'show_coordinates': False, - 'animation_speed': 1.0 - } - - # 当前显示的数据 - self.current_grid = None - self.current_path = None - self.current_start = None - self.current_goal = None - self.visited_nodes = [] - self.open_list = [] - - # 动画相关 - self.animation_enabled = False - self.animation_progress = 0.0 - self.animation_path = [] - self.animation_timer = 0.0 - - # 材质 - self.grid_material = None - self.path_material = None - self.start_material = None - self.goal_material = None - - def enable(self): - """启用可视化管理器""" - if self.enabled: - return - - self.enabled = True - self._create_visualization_nodes() - self._create_materials() - print("✓ 可视化管理器已启用") - - def disable(self): - """禁用可视化管理器""" - if not self.enabled: - return - - self.enabled = False - self._cleanup_visualization_nodes() - print("✓ 可视化管理器已禁用") - - def cleanup(self): - """清理资源""" - self.disable() - self.current_grid = None - self.current_path = None - self.current_start = None - self.current_goal = None - self.visited_nodes = [] - self.open_list = [] - self.animation_path = [] - - def _create_visualization_nodes(self): - """创建可视化节点""" - if not self.world or not self.world.render: - return - - # 创建网格可视化节点 - self.grid_node = self.world.render.attachNewNode("pathfinding_grid") - - # 创建路径可视化节点 - self.path_node = self.world.render.attachNewNode("pathfinding_path") - - # 创建起点可视化节点 - self.start_node = self.world.render.attachNewNode("pathfinding_start") - - # 创建终点可视化节点 - self.goal_node = self.world.render.attachNewNode("pathfinding_goal") - - # 创建已访问节点可视化节点 - self.visited_nodes_node = self.world.render.attachNewNode("pathfinding_visited_nodes") - - # 创建开放列表可视化节点 - self.open_list_node = self.world.render.attachNewNode("pathfinding_open_list") - - def _cleanup_visualization_nodes(self): - """清理可视化节点""" - nodes = [ - self.grid_node, self.path_node, self.start_node, self.goal_node, - self.visited_nodes_node, self.open_list_node - ] - - for node in nodes: - if node: - node.removeNode() - - self.grid_node = None - self.path_node = None - self.start_node = None - self.goal_node = None - self.visited_nodes_node = None - self.open_list_node = None - - def _create_materials(self): - """创建材质""" - # 网格材质 - self.grid_material = Material() - self.grid_material.setAmbient(self.settings['grid_color']) - self.grid_material.setDiffuse(self.settings['grid_color']) - - # 路径材质 - self.path_material = Material() - self.path_material.setAmbient(self.settings['path_color']) - self.path_material.setDiffuse(self.settings['path_color']) - - # 起点材质 - self.start_material = Material() - self.start_material.setAmbient(self.settings['start_color']) - self.start_material.setDiffuse(self.settings['start_color']) - - # 终点材质 - self.goal_material = Material() - self.goal_material.setAmbient(self.settings['goal_color']) - self.goal_material.setDiffuse(self.settings['goal_color']) - - def toggle_visibility(self): - """切换可视化可见性""" - if not self.enabled: - return - - self.settings['grid_visible'] = not self.settings['grid_visible'] - self.settings['path_visible'] = not self.settings['path_visible'] - self.settings['start_visible'] = not self.settings['start_visible'] - self.settings['goal_visible'] = not self.settings['goal_visible'] - self.settings['visited_nodes_visible'] = not self.settings['visited_nodes_visible'] - self.settings['open_list_visible'] = not self.settings['open_list_visible'] - - self._update_visibility() - print("✓ 可视化可见性已切换") - - def _update_visibility(self): - """更新可视化元素的可见性""" - visibility_map = { - self.grid_node: self.settings['grid_visible'], - self.path_node: self.settings['path_visible'], - self.start_node: self.settings['start_visible'], - self.goal_node: self.settings['goal_visible'], - self.visited_nodes_node: self.settings['visited_nodes_visible'], - self.open_list_node: self.settings['open_list_visible'] - } - - for node, visible in visibility_map.items(): - if node: - if visible: - node.show() - else: - node.hide() - - def visualize_grid(self, grid: List[List[int]]): - """ - 可视化网格 - - Args: - grid: 网格数据 - """ - if not self.enabled or not self.grid_node: - return - - self.current_grid = grid - self._create_grid_geometry() - print(f"✓ 网格已可视化 ({len(grid)}x{len(grid[0]) if grid else 0})") - - def _create_grid_geometry(self): - """创建网格几何体""" - if not self.current_grid or not self.grid_node: - return - - # 清除现有几何体 - self.grid_node.node().removeAllGeoms() - - rows = len(self.current_grid) - cols = len(self.current_grid[0]) if rows > 0 else 0 - if rows == 0 or cols == 0: - return - - cell_size = self.settings['cell_size'] - cell_height = self.settings['cell_height'] - grid_color = self.settings['grid_color'] - obstacle_color = self.settings['obstacle_color'] - - # 创建顶点格式 - vertex_format = GeomVertexFormat.getV3c4() - vertex_data = GeomVertexData("grid", vertex_format, Geom.UHStatic) - - # 计算总顶点数(每个格子4个顶点) - total_vertices = rows * cols * 4 - vertex_data.setNumRows(total_vertices) - - # 创建顶点写入器 - vertex_writer = GeomVertexWriter(vertex_data, "vertex") - color_writer = GeomVertexWriter(vertex_data, "color") - - # 创建几何体 - geom = Geom(vertex_data) - triangles = GeomTriangles(Geom.UHStatic) - - vertex_index = 0 - for row in range(rows): - for col in range(cols): - # 计算格子位置 - x = col * cell_size - (cols * cell_size) / 2 - z = row * cell_size - (rows * cell_size) / 2 - y = 0 if self.current_grid[row][col] == 0 else cell_height - - # 选择颜色(可通行区域或障碍物) - color = grid_color if self.current_grid[row][col] == 0 else obstacle_color - - # 创建格子的四个顶点 - vertices = [ - Point3(x, y, z), - Point3(x + cell_size, y, z), - Point3(x + cell_size, y, z + cell_size), - Point3(x, y, z + cell_size) - ] - - # 写入顶点数据 - for vertex in vertices: - vertex_writer.addData3f(vertex.x, vertex.y, vertex.z) - color_writer.addData4f(color) - - # 添加两个三角形 - triangles.addVertex(vertex_index) - triangles.addVertex(vertex_index + 1) - triangles.addVertex(vertex_index + 2) - triangles.closePrimitive() - - triangles.addVertex(vertex_index) - triangles.addVertex(vertex_index + 2) - triangles.addVertex(vertex_index + 3) - triangles.closePrimitive() - - vertex_index += 4 - - geom.addPrimitive(triangles) - self.grid_node.node().addGeom(geom) - - # 应用材质 - if self.grid_material: - self.grid_node.setMaterial(self.grid_material, 1) - - # 添加坐标文本(如果启用) - if self.settings['show_coordinates']: - self._add_coordinate_labels(rows, cols, cell_size) - - def _add_coordinate_labels(self, rows: int, cols: int, cell_size: float): - """ - 添加坐标标签 - - Args: - rows: 行数 - cols: 列数 - cell_size: 单元格大小 - """ - # 这里可以添加坐标标签的实现 - # 为了简化,我们只打印信息 - print(f" 坐标标签已启用 ({rows}x{cols})") - - def visualize_path(self, path: List[Tuple[int, int]], grid: Optional[List[List[int]]] = None): - """ - 可视化路径 - - Args: - path: 路径坐标列表 - grid: 网格数据(可选) - """ - if not self.enabled: - return - - self.current_path = path - if grid is not None: - self.current_grid = grid - - self._create_path_geometry() - self._update_visibility() - print(f"✓ 路径已可视化 (长度: {len(path) if path else 0})") - - def _create_path_geometry(self): - """创建路径几何体""" - if not self.current_path or not self.path_node: - return - - # 清除现有几何体 - self.path_node.node().removeAllGeoms() - - if len(self.current_path) < 2: - return - - cell_size = self.settings['cell_size'] - rows = len(self.current_grid) if self.current_grid else 0 - cols = len(self.current_grid[0]) if self.current_grid and rows > 0 else 0 - - # 创建顶点格式 - vertex_format = GeomVertexFormat.getV3c4() - vertex_data = GeomVertexData("path", vertex_format, Geom.UHStatic) - vertex_data.setNumRows(len(self.current_path)) - - # 创建顶点写入器 - vertex_writer = GeomVertexWriter(vertex_data, "vertex") - color_writer = GeomVertexWriter(vertex_data, "color") - - # 写入路径点 - for i, (row, col) in enumerate(self.current_path): - x = col * cell_size - (cols * cell_size) / 2 + cell_size / 2 - z = row * cell_size - (rows * cell_size) / 2 + cell_size / 2 - y = 0.2 # 稍微抬高以避免与网格重叠 - - vertex_writer.addData3f(x, y, z) - color_writer.addData4f(self.settings['path_color']) - - # 创建几何体 - geom = Geom(vertex_data) - lines = GeomLines(Geom.UHStatic) - - # 添加线段 - for i in range(len(self.current_path) - 1): - lines.addVertex(i) - lines.addVertex(i + 1) - lines.closePrimitive() - - geom.addPrimitive(lines) - self.path_node.node().addGeom(geom) - - # 设置线宽和材质 - self.path_node.setRenderModeThickness(self.settings['path_thickness']) - if self.path_material: - self.path_node.setMaterial(self.path_material, 1) - - def visualize_visited_nodes(self, visited_nodes: List[Tuple[int, int]]): - """ - 可视化已访问节点 - - Args: - visited_nodes: 已访问节点列表 - """ - if not self.enabled: - return - - self.visited_nodes = visited_nodes - self._create_visited_nodes_geometry() - print(f"✓ 已访问节点已可视化 (数量: {len(visited_nodes)})") - - def _create_visited_nodes_geometry(self): - """创建已访问节点几何体""" - if not self.visited_nodes or not self.visited_nodes_node: - return - - # 清除现有几何体 - self.visited_nodes_node.node().removeAllGeoms() - - cell_size = self.settings['cell_size'] - rows = len(self.current_grid) if self.current_grid else 0 - cols = len(self.current_grid[0]) if self.current_grid and rows > 0 else 0 - - # 创建顶点格式 - vertex_format = GeomVertexFormat.getV3c4() - vertex_data = GeomVertexData("visited_nodes", vertex_format, Geom.UHStatic) - vertex_data.setNumRows(len(self.visited_nodes)) - - # 创建顶点写入器 - vertex_writer = GeomVertexWriter(vertex_data, "vertex") - color_writer = GeomVertexWriter(vertex_data, "color") - - # 写入节点点 - for i, (row, col) in enumerate(self.visited_nodes): - x = col * cell_size - (cols * cell_size) / 2 + cell_size / 2 - z = row * cell_size - (rows * cell_size) / 2 + cell_size / 2 - y = 0.15 # 稍微抬高以避免与网格重叠 - - vertex_writer.addData3f(x, y, z) - color_writer.addData4f(self.settings['visited_nodes_color']) - - # 创建几何体 - geom = Geom(vertex_data) - points = GeomPoints(Geom.UHStatic) - points.addNextVertices(len(self.visited_nodes)) - geom.addPrimitive(points) - self.visited_nodes_node.node().addGeom(geom) - - # 设置点大小和透明度 - self.visited_nodes_node.setRenderModeThickness(3.0) - self.visited_nodes_node.setTransparency(TransparencyAttrib.MAlpha) - - def visualize_open_list(self, open_list: List[Tuple[int, int]]): - """ - 可视化开放列表节点 - - Args: - open_list: 开放列表节点列表 - """ - if not self.enabled: - return - - self.open_list = open_list - self._create_open_list_geometry() - print(f"✓ 开放列表节点已可视化 (数量: {len(open_list)})") - - def _create_open_list_geometry(self): - """创建开放列表节点几何体""" - if not self.open_list or not self.open_list_node: - return - - # 清除现有几何体 - self.open_list_node.node().removeAllGeoms() - - cell_size = self.settings['cell_size'] - rows = len(self.current_grid) if self.current_grid else 0 - cols = len(self.current_grid[0]) if self.current_grid and rows > 0 else 0 - - # 创建顶点格式 - vertex_format = GeomVertexFormat.getV3c4() - vertex_data = GeomVertexData("open_list", vertex_format, Geom.UHStatic) - vertex_data.setNumRows(len(self.open_list)) - - # 创建顶点写入器 - vertex_writer = GeomVertexWriter(vertex_data, "vertex") - color_writer = GeomVertexWriter(vertex_data, "color") - - # 写入节点点 - for i, (row, col) in enumerate(self.open_list): - x = col * cell_size - (cols * cell_size) / 2 + cell_size / 2 - z = row * cell_size - (rows * cell_size) / 2 + cell_size / 2 - y = 0.18 # 稍微抬高以避免与网格重叠 - - vertex_writer.addData3f(x, y, z) - color_writer.addData4f(self.settings['open_list_color']) - - # 创建几何体 - geom = Geom(vertex_data) - points = GeomPoints(Geom.UHStatic) - points.addNextVertices(len(self.open_list)) - geom.addPrimitive(points) - self.open_list_node.node().addGeom(geom) - - # 设置点大小和透明度 - self.open_list_node.setRenderModeThickness(2.0) - self.open_list_node.setTransparency(TransparencyAttrib.MAlpha) - - def clear_visualization(self): - """清除所有可视化""" - self.clear_grid_visualization() - self.clear_path_visualization() - self.clear_start_visualization() - self.clear_goal_visualization() - self.clear_visited_nodes_visualization() - self.clear_open_list_visualization() - print("✓ 所有可视化已清除") - - def clear_grid_visualization(self): - """清除网格可视化""" - if self.grid_node: - self.grid_node.node().removeAllGeoms() - self.current_grid = None - - def clear_path_visualization(self): - """清除路径可视化""" - if self.path_node: - self.path_node.node().removeAllGeoms() - self.current_path = None - - def clear_start_visualization(self): - """清除起点可视化""" - if self.start_node: - self.start_node.node().removeAllGeoms() - self.current_start = None - - def clear_goal_visualization(self): - """清除终点可视化""" - if self.goal_node: - self.goal_node.node().removeAllGeoms() - self.current_goal = None - - def clear_visited_nodes_visualization(self): - """清除已访问节点可视化""" - if self.visited_nodes_node: - self.visited_nodes_node.node().removeAllGeoms() - self.visited_nodes = [] - - def clear_open_list_visualization(self): - """清除开放列表可视化""" - if self.open_list_node: - self.open_list_node.node().removeAllGeoms() - self.open_list = [] - - def set_settings(self, settings: Dict[str, Any]): - """ - 设置可视化参数 - - Args: - settings: 参数字典 - """ - old_settings = self.settings.copy() - self.settings.update(settings) - self._update_visualization() - - # 如果颜色设置发生变化,更新材质 - color_keys = ['grid_color', 'path_color', 'start_color', 'goal_color'] - if any(key in settings for key in color_keys): - self._create_materials() - - print("✓ 可视化设置已更新") - - def get_settings(self) -> Dict[str, Any]: - """ - 获取可视化参数 - - Returns: - 参数字典 - """ - return self.settings.copy() - - def _update_visualization(self): - """更新可视化显示""" - if self.current_grid: - self._create_grid_geometry() - if self.current_path: - self._create_path_geometry() - if self.visited_nodes: - self._create_visited_nodes_geometry() - if self.open_list: - self._create_open_list_geometry() - self._update_visibility() - - def enable_animation(self, enable: bool): - """ - 启用/禁用路径动画 - - Args: - enable: 是否启用动画 - """ - self.animation_enabled = enable - if enable: - self.animation_progress = 0.0 - self.animation_timer = 0.0 - self.animation_path = self.current_path.copy() if self.current_path else [] - print("✓ 路径动画已启用") - else: - print("✓ 路径动画已禁用") - - def update_animation(self, dt: float): - """ - 更新动画 - - Args: - dt: 时间增量 - """ - if not self.animation_enabled or not self.animation_path: - return - - self.animation_timer += dt * self.settings['animation_speed'] - self.animation_progress = min(1.0, self.animation_timer / 2.0) # 2秒完成动画 - - # 更新路径显示 - path_length = len(self.animation_path) - visible_length = int(path_length * self.animation_progress) - - if visible_length > 1: - visible_path = self.animation_path[:visible_length] - self._update_animated_path(visible_path) - - def _update_animated_path(self, visible_path: List[Tuple[int, int]]): - """ - 更新动画路径显示 - - Args: - visible_path: 可见路径部分 - """ - if not self.path_node: - return - - # 清除现有几何体 - self.path_node.node().removeAllGeoms() - - if len(visible_path) < 2: - return - - cell_size = self.settings['cell_size'] - rows = len(self.current_grid) if self.current_grid else 0 - cols = len(self.current_grid[0]) if self.current_grid and rows > 0 else 0 - - # 创建顶点格式 - vertex_format = GeomVertexFormat.getV3c4() - vertex_data = GeomVertexData("path", vertex_format, Geom.UHStatic) - vertex_data.setNumRows(len(visible_path)) - - # 创建顶点写入器 - vertex_writer = GeomVertexWriter(vertex_data, "vertex") - color_writer = GeomVertexWriter(vertex_data, "color") - - # 写入路径点 - for i, (row, col) in enumerate(visible_path): - x = col * cell_size - (cols * cell_size) / 2 + cell_size / 2 - z = row * cell_size - (rows * cell_size) / 2 + cell_size / 2 - y = 0.2 # 稍微抬高以避免与网格重叠 - - # 根据进度调整颜色 - progress = i / len(visible_path) - base_color = self.settings['path_color'] - color = LColor( - base_color.x * (1 - progress) + 1.0 * progress, # 逐渐变为白色 - base_color.y * (1 - progress), - base_color.z * (1 - progress), - base_color.w - ) - - vertex_writer.addData3f(x, y, z) - color_writer.addData4f(color) - - # 创建几何体 - geom = Geom(vertex_data) - lines = GeomLines(Geom.UHStatic) - - # 添加线段 - for i in range(len(visible_path) - 1): - lines.addVertex(i) - lines.addVertex(i + 1) - lines.closePrimitive() - - geom.addPrimitive(lines) - self.path_node.node().addGeom(geom) - - # 设置线宽和材质 - self.path_node.setRenderModeThickness(self.settings['path_thickness']) - if self.path_material: +""" +可视化管理器 +负责路径规划算法的可视化显示 +""" + +from typing import List, Tuple, Optional, Dict, Any +from panda3d.core import NodePath, GeomNode, GeomVertexFormat, GeomVertexData +from panda3d.core import GeomVertexWriter, GeomTriangles, GeomLines, GeomPoints +from panda3d.core import LColor, Point3, Material, TransparencyAttrib +import math + +class VisualizationManager: + """ + 可视化管理器 + 负责路径规划算法的可视化显示 + """ + + def __init__(self, world): + """ + 初始化可视化管理器 + + Args: + world: 3D世界对象 + """ + self.world = world + self.enabled = False + + # 可视化节点 + self.grid_node = None + self.path_node = None + self.start_node = None + self.goal_node = None + self.visited_nodes_node = None + self.open_list_node = None + + # 可视化设置 + self.settings = { + 'grid_visible': True, + 'path_visible': True, + 'start_visible': True, + 'goal_visible': True, + 'visited_nodes_visible': False, + 'open_list_visible': False, + 'grid_color': LColor(0.5, 0.5, 0.5, 1.0), + 'path_color': LColor(1.0, 0.0, 0.0, 1.0), + 'start_color': LColor(0.0, 1.0, 0.0, 1.0), + 'goal_color': LColor(0.0, 0.0, 1.0, 1.0), + 'visited_nodes_color': LColor(0.0, 0.5, 1.0, 0.7), + 'open_list_color': LColor(1.0, 1.0, 0.0, 0.7), + 'obstacle_color': LColor(0.2, 0.2, 0.2, 1.0), + 'cell_size': 1.0, + 'cell_height': 0.1, + 'path_thickness': 4.0, + 'show_coordinates': False, + 'animation_speed': 1.0 + } + + # 当前显示的数据 + self.current_grid = None + self.current_path = None + self.current_start = None + self.current_goal = None + self.visited_nodes = [] + self.open_list = [] + + # 动画相关 + self.animation_enabled = False + self.animation_progress = 0.0 + self.animation_path = [] + self.animation_timer = 0.0 + + # 材质 + self.grid_material = None + self.path_material = None + self.start_material = None + self.goal_material = None + + def enable(self): + """启用可视化管理器""" + if self.enabled: + return + + self.enabled = True + self._create_visualization_nodes() + self._create_materials() + print("✓ 可视化管理器已启用") + + def disable(self): + """禁用可视化管理器""" + if not self.enabled: + return + + self.enabled = False + self._cleanup_visualization_nodes() + print("✓ 可视化管理器已禁用") + + def cleanup(self): + """清理资源""" + self.disable() + self.current_grid = None + self.current_path = None + self.current_start = None + self.current_goal = None + self.visited_nodes = [] + self.open_list = [] + self.animation_path = [] + + def _create_visualization_nodes(self): + """创建可视化节点""" + if not self.world or not self.world.render: + return + + # 创建网格可视化节点 + self.grid_node = self.world.render.attachNewNode("pathfinding_grid") + + # 创建路径可视化节点 + self.path_node = self.world.render.attachNewNode("pathfinding_path") + + # 创建起点可视化节点 + self.start_node = self.world.render.attachNewNode("pathfinding_start") + + # 创建终点可视化节点 + self.goal_node = self.world.render.attachNewNode("pathfinding_goal") + + # 创建已访问节点可视化节点 + self.visited_nodes_node = self.world.render.attachNewNode("pathfinding_visited_nodes") + + # 创建开放列表可视化节点 + self.open_list_node = self.world.render.attachNewNode("pathfinding_open_list") + + def _cleanup_visualization_nodes(self): + """清理可视化节点""" + nodes = [ + self.grid_node, self.path_node, self.start_node, self.goal_node, + self.visited_nodes_node, self.open_list_node + ] + + for node in nodes: + if node: + node.removeNode() + + self.grid_node = None + self.path_node = None + self.start_node = None + self.goal_node = None + self.visited_nodes_node = None + self.open_list_node = None + + def _create_materials(self): + """创建材质""" + # 网格材质 + self.grid_material = Material() + self.grid_material.setAmbient(self.settings['grid_color']) + self.grid_material.setDiffuse(self.settings['grid_color']) + + # 路径材质 + self.path_material = Material() + self.path_material.setAmbient(self.settings['path_color']) + self.path_material.setDiffuse(self.settings['path_color']) + + # 起点材质 + self.start_material = Material() + self.start_material.setAmbient(self.settings['start_color']) + self.start_material.setDiffuse(self.settings['start_color']) + + # 终点材质 + self.goal_material = Material() + self.goal_material.setAmbient(self.settings['goal_color']) + self.goal_material.setDiffuse(self.settings['goal_color']) + + def toggle_visibility(self): + """切换可视化可见性""" + if not self.enabled: + return + + self.settings['grid_visible'] = not self.settings['grid_visible'] + self.settings['path_visible'] = not self.settings['path_visible'] + self.settings['start_visible'] = not self.settings['start_visible'] + self.settings['goal_visible'] = not self.settings['goal_visible'] + self.settings['visited_nodes_visible'] = not self.settings['visited_nodes_visible'] + self.settings['open_list_visible'] = not self.settings['open_list_visible'] + + self._update_visibility() + print("✓ 可视化可见性已切换") + + def _update_visibility(self): + """更新可视化元素的可见性""" + visibility_map = { + self.grid_node: self.settings['grid_visible'], + self.path_node: self.settings['path_visible'], + self.start_node: self.settings['start_visible'], + self.goal_node: self.settings['goal_visible'], + self.visited_nodes_node: self.settings['visited_nodes_visible'], + self.open_list_node: self.settings['open_list_visible'] + } + + for node, visible in visibility_map.items(): + if node: + if visible: + node.show() + else: + node.hide() + + def visualize_grid(self, grid: List[List[int]]): + """ + 可视化网格 + + Args: + grid: 网格数据 + """ + if not self.enabled or not self.grid_node: + return + + self.current_grid = grid + self._create_grid_geometry() + print(f"✓ 网格已可视化 ({len(grid)}x{len(grid[0]) if grid else 0})") + + def _create_grid_geometry(self): + """创建网格几何体""" + if not self.current_grid or not self.grid_node: + return + + # 清除现有几何体 + self.grid_node.node().removeAllGeoms() + + rows = len(self.current_grid) + cols = len(self.current_grid[0]) if rows > 0 else 0 + if rows == 0 or cols == 0: + return + + cell_size = self.settings['cell_size'] + cell_height = self.settings['cell_height'] + grid_color = self.settings['grid_color'] + obstacle_color = self.settings['obstacle_color'] + + # 创建顶点格式 + vertex_format = GeomVertexFormat.getV3c4() + vertex_data = GeomVertexData("grid", vertex_format, Geom.UHStatic) + + # 计算总顶点数(每个格子4个顶点) + total_vertices = rows * cols * 4 + vertex_data.setNumRows(total_vertices) + + # 创建顶点写入器 + vertex_writer = GeomVertexWriter(vertex_data, "vertex") + color_writer = GeomVertexWriter(vertex_data, "color") + + # 创建几何体 + geom = Geom(vertex_data) + triangles = GeomTriangles(Geom.UHStatic) + + vertex_index = 0 + for row in range(rows): + for col in range(cols): + # 计算格子位置 + x = col * cell_size - (cols * cell_size) / 2 + z = row * cell_size - (rows * cell_size) / 2 + y = 0 if self.current_grid[row][col] == 0 else cell_height + + # 选择颜色(可通行区域或障碍物) + color = grid_color if self.current_grid[row][col] == 0 else obstacle_color + + # 创建格子的四个顶点 + vertices = [ + Point3(x, y, z), + Point3(x + cell_size, y, z), + Point3(x + cell_size, y, z + cell_size), + Point3(x, y, z + cell_size) + ] + + # 写入顶点数据 + for vertex in vertices: + vertex_writer.addData3f(vertex.x, vertex.y, vertex.z) + color_writer.addData4f(color) + + # 添加两个三角形 + triangles.addVertex(vertex_index) + triangles.addVertex(vertex_index + 1) + triangles.addVertex(vertex_index + 2) + triangles.closePrimitive() + + triangles.addVertex(vertex_index) + triangles.addVertex(vertex_index + 2) + triangles.addVertex(vertex_index + 3) + triangles.closePrimitive() + + vertex_index += 4 + + geom.addPrimitive(triangles) + self.grid_node.node().addGeom(geom) + + # 应用材质 + if self.grid_material: + self.grid_node.setMaterial(self.grid_material, 1) + + # 添加坐标文本(如果启用) + if self.settings['show_coordinates']: + self._add_coordinate_labels(rows, cols, cell_size) + + def _add_coordinate_labels(self, rows: int, cols: int, cell_size: float): + """ + 添加坐标标签 + + Args: + rows: 行数 + cols: 列数 + cell_size: 单元格大小 + """ + # 这里可以添加坐标标签的实现 + # 为了简化,我们只打印信息 + print(f" 坐标标签已启用 ({rows}x{cols})") + + def visualize_path(self, path: List[Tuple[int, int]], grid: Optional[List[List[int]]] = None): + """ + 可视化路径 + + Args: + path: 路径坐标列表 + grid: 网格数据(可选) + """ + if not self.enabled: + return + + self.current_path = path + if grid is not None: + self.current_grid = grid + + self._create_path_geometry() + self._update_visibility() + print(f"✓ 路径已可视化 (长度: {len(path) if path else 0})") + + def _create_path_geometry(self): + """创建路径几何体""" + if not self.current_path or not self.path_node: + return + + # 清除现有几何体 + self.path_node.node().removeAllGeoms() + + if len(self.current_path) < 2: + return + + cell_size = self.settings['cell_size'] + rows = len(self.current_grid) if self.current_grid else 0 + cols = len(self.current_grid[0]) if self.current_grid and rows > 0 else 0 + + # 创建顶点格式 + vertex_format = GeomVertexFormat.getV3c4() + vertex_data = GeomVertexData("path", vertex_format, Geom.UHStatic) + vertex_data.setNumRows(len(self.current_path)) + + # 创建顶点写入器 + vertex_writer = GeomVertexWriter(vertex_data, "vertex") + color_writer = GeomVertexWriter(vertex_data, "color") + + # 写入路径点 + for i, (row, col) in enumerate(self.current_path): + x = col * cell_size - (cols * cell_size) / 2 + cell_size / 2 + z = row * cell_size - (rows * cell_size) / 2 + cell_size / 2 + y = 0.2 # 稍微抬高以避免与网格重叠 + + vertex_writer.addData3f(x, y, z) + color_writer.addData4f(self.settings['path_color']) + + # 创建几何体 + geom = Geom(vertex_data) + lines = GeomLines(Geom.UHStatic) + + # 添加线段 + for i in range(len(self.current_path) - 1): + lines.addVertex(i) + lines.addVertex(i + 1) + lines.closePrimitive() + + geom.addPrimitive(lines) + self.path_node.node().addGeom(geom) + + # 设置线宽和材质 + self.path_node.setRenderModeThickness(self.settings['path_thickness']) + if self.path_material: + self.path_node.setMaterial(self.path_material, 1) + + def visualize_visited_nodes(self, visited_nodes: List[Tuple[int, int]]): + """ + 可视化已访问节点 + + Args: + visited_nodes: 已访问节点列表 + """ + if not self.enabled: + return + + self.visited_nodes = visited_nodes + self._create_visited_nodes_geometry() + print(f"✓ 已访问节点已可视化 (数量: {len(visited_nodes)})") + + def _create_visited_nodes_geometry(self): + """创建已访问节点几何体""" + if not self.visited_nodes or not self.visited_nodes_node: + return + + # 清除现有几何体 + self.visited_nodes_node.node().removeAllGeoms() + + cell_size = self.settings['cell_size'] + rows = len(self.current_grid) if self.current_grid else 0 + cols = len(self.current_grid[0]) if self.current_grid and rows > 0 else 0 + + # 创建顶点格式 + vertex_format = GeomVertexFormat.getV3c4() + vertex_data = GeomVertexData("visited_nodes", vertex_format, Geom.UHStatic) + vertex_data.setNumRows(len(self.visited_nodes)) + + # 创建顶点写入器 + vertex_writer = GeomVertexWriter(vertex_data, "vertex") + color_writer = GeomVertexWriter(vertex_data, "color") + + # 写入节点点 + for i, (row, col) in enumerate(self.visited_nodes): + x = col * cell_size - (cols * cell_size) / 2 + cell_size / 2 + z = row * cell_size - (rows * cell_size) / 2 + cell_size / 2 + y = 0.15 # 稍微抬高以避免与网格重叠 + + vertex_writer.addData3f(x, y, z) + color_writer.addData4f(self.settings['visited_nodes_color']) + + # 创建几何体 + geom = Geom(vertex_data) + points = GeomPoints(Geom.UHStatic) + points.addNextVertices(len(self.visited_nodes)) + geom.addPrimitive(points) + self.visited_nodes_node.node().addGeom(geom) + + # 设置点大小和透明度 + self.visited_nodes_node.setRenderModeThickness(3.0) + self.visited_nodes_node.setTransparency(TransparencyAttrib.MAlpha) + + def visualize_open_list(self, open_list: List[Tuple[int, int]]): + """ + 可视化开放列表节点 + + Args: + open_list: 开放列表节点列表 + """ + if not self.enabled: + return + + self.open_list = open_list + self._create_open_list_geometry() + print(f"✓ 开放列表节点已可视化 (数量: {len(open_list)})") + + def _create_open_list_geometry(self): + """创建开放列表节点几何体""" + if not self.open_list or not self.open_list_node: + return + + # 清除现有几何体 + self.open_list_node.node().removeAllGeoms() + + cell_size = self.settings['cell_size'] + rows = len(self.current_grid) if self.current_grid else 0 + cols = len(self.current_grid[0]) if self.current_grid and rows > 0 else 0 + + # 创建顶点格式 + vertex_format = GeomVertexFormat.getV3c4() + vertex_data = GeomVertexData("open_list", vertex_format, Geom.UHStatic) + vertex_data.setNumRows(len(self.open_list)) + + # 创建顶点写入器 + vertex_writer = GeomVertexWriter(vertex_data, "vertex") + color_writer = GeomVertexWriter(vertex_data, "color") + + # 写入节点点 + for i, (row, col) in enumerate(self.open_list): + x = col * cell_size - (cols * cell_size) / 2 + cell_size / 2 + z = row * cell_size - (rows * cell_size) / 2 + cell_size / 2 + y = 0.18 # 稍微抬高以避免与网格重叠 + + vertex_writer.addData3f(x, y, z) + color_writer.addData4f(self.settings['open_list_color']) + + # 创建几何体 + geom = Geom(vertex_data) + points = GeomPoints(Geom.UHStatic) + points.addNextVertices(len(self.open_list)) + geom.addPrimitive(points) + self.open_list_node.node().addGeom(geom) + + # 设置点大小和透明度 + self.open_list_node.setRenderModeThickness(2.0) + self.open_list_node.setTransparency(TransparencyAttrib.MAlpha) + + def clear_visualization(self): + """清除所有可视化""" + self.clear_grid_visualization() + self.clear_path_visualization() + self.clear_start_visualization() + self.clear_goal_visualization() + self.clear_visited_nodes_visualization() + self.clear_open_list_visualization() + print("✓ 所有可视化已清除") + + def clear_grid_visualization(self): + """清除网格可视化""" + if self.grid_node: + self.grid_node.node().removeAllGeoms() + self.current_grid = None + + def clear_path_visualization(self): + """清除路径可视化""" + if self.path_node: + self.path_node.node().removeAllGeoms() + self.current_path = None + + def clear_start_visualization(self): + """清除起点可视化""" + if self.start_node: + self.start_node.node().removeAllGeoms() + self.current_start = None + + def clear_goal_visualization(self): + """清除终点可视化""" + if self.goal_node: + self.goal_node.node().removeAllGeoms() + self.current_goal = None + + def clear_visited_nodes_visualization(self): + """清除已访问节点可视化""" + if self.visited_nodes_node: + self.visited_nodes_node.node().removeAllGeoms() + self.visited_nodes = [] + + def clear_open_list_visualization(self): + """清除开放列表可视化""" + if self.open_list_node: + self.open_list_node.node().removeAllGeoms() + self.open_list = [] + + def set_settings(self, settings: Dict[str, Any]): + """ + 设置可视化参数 + + Args: + settings: 参数字典 + """ + old_settings = self.settings.copy() + self.settings.update(settings) + self._update_visualization() + + # 如果颜色设置发生变化,更新材质 + color_keys = ['grid_color', 'path_color', 'start_color', 'goal_color'] + if any(key in settings for key in color_keys): + self._create_materials() + + print("✓ 可视化设置已更新") + + def get_settings(self) -> Dict[str, Any]: + """ + 获取可视化参数 + + Returns: + 参数字典 + """ + return self.settings.copy() + + def _update_visualization(self): + """更新可视化显示""" + if self.current_grid: + self._create_grid_geometry() + if self.current_path: + self._create_path_geometry() + if self.visited_nodes: + self._create_visited_nodes_geometry() + if self.open_list: + self._create_open_list_geometry() + self._update_visibility() + + def enable_animation(self, enable: bool): + """ + 启用/禁用路径动画 + + Args: + enable: 是否启用动画 + """ + self.animation_enabled = enable + if enable: + self.animation_progress = 0.0 + self.animation_timer = 0.0 + self.animation_path = self.current_path.copy() if self.current_path else [] + print("✓ 路径动画已启用") + else: + print("✓ 路径动画已禁用") + + def update_animation(self, dt: float): + """ + 更新动画 + + Args: + dt: 时间增量 + """ + if not self.animation_enabled or not self.animation_path: + return + + self.animation_timer += dt * self.settings['animation_speed'] + self.animation_progress = min(1.0, self.animation_timer / 2.0) # 2秒完成动画 + + # 更新路径显示 + path_length = len(self.animation_path) + visible_length = int(path_length * self.animation_progress) + + if visible_length > 1: + visible_path = self.animation_path[:visible_length] + self._update_animated_path(visible_path) + + def _update_animated_path(self, visible_path: List[Tuple[int, int]]): + """ + 更新动画路径显示 + + Args: + visible_path: 可见路径部分 + """ + if not self.path_node: + return + + # 清除现有几何体 + self.path_node.node().removeAllGeoms() + + if len(visible_path) < 2: + return + + cell_size = self.settings['cell_size'] + rows = len(self.current_grid) if self.current_grid else 0 + cols = len(self.current_grid[0]) if self.current_grid and rows > 0 else 0 + + # 创建顶点格式 + vertex_format = GeomVertexFormat.getV3c4() + vertex_data = GeomVertexData("path", vertex_format, Geom.UHStatic) + vertex_data.setNumRows(len(visible_path)) + + # 创建顶点写入器 + vertex_writer = GeomVertexWriter(vertex_data, "vertex") + color_writer = GeomVertexWriter(vertex_data, "color") + + # 写入路径点 + for i, (row, col) in enumerate(visible_path): + x = col * cell_size - (cols * cell_size) / 2 + cell_size / 2 + z = row * cell_size - (rows * cell_size) / 2 + cell_size / 2 + y = 0.2 # 稍微抬高以避免与网格重叠 + + # 根据进度调整颜色 + progress = i / len(visible_path) + base_color = self.settings['path_color'] + color = LColor( + base_color.x * (1 - progress) + 1.0 * progress, # 逐渐变为白色 + base_color.y * (1 - progress), + base_color.z * (1 - progress), + base_color.w + ) + + vertex_writer.addData3f(x, y, z) + color_writer.addData4f(color) + + # 创建几何体 + geom = Geom(vertex_data) + lines = GeomLines(Geom.UHStatic) + + # 添加线段 + for i in range(len(visible_path) - 1): + lines.addVertex(i) + lines.addVertex(i + 1) + lines.closePrimitive() + + geom.addPrimitive(lines) + self.path_node.node().addGeom(geom) + + # 设置线宽和材质 + self.path_node.setRenderModeThickness(self.settings['path_thickness']) + if self.path_material: self.path_node.setMaterial(self.path_material, 1) \ No newline at end of file diff --git a/plugins/user/procedural_terrain_generation/README.md b/plugins/user/procedural_terrain_generation/README.md index a6d50d9c..253bc63f 100644 --- a/plugins/user/procedural_terrain_generation/README.md +++ b/plugins/user/procedural_terrain_generation/README.md @@ -1,277 +1,277 @@ -# 程序化地形生成插件 (Procedural Terrain Generation Plugin) - -![地形生成示例](https://example.com/terrain_example.png) - -为EG引擎提供高性能的程序化地形生成功能。 - -## 功能特性 - -### 🌍 核心功能 -- **高度图生成**: 基于多种噪声算法生成逼真的地形高度图 -- **生物群落分布**: 自动根据气候条件生成不同的生态环境 -- **物理侵蚀模拟**: 模拟水力、风力、热力等自然侵蚀过程 -- **植被分布系统**: 根据环境条件自动生成植被覆盖 -- **实时渲染支持**: 支持LOD、视锥体剔除等优化技术 - -### 🎨 高级特性 -- **多噪声算法**: 支持Perlin、Simplex、分形噪声等多种算法 -- **气候系统**: 湿度、温度等环境因素影响地形生成 -- **地形分析工具**: 坡度、曲率、粗糙度等分析功能 -- **数据序列化**: 支持多种格式的地形数据保存和加载 -- **性能优化**: 多线程处理、内存优化等技术 - -### 🛠️ 技术规格 -- **地形尺寸**: 支持最大8192x8192分辨率 -- **LOD级别**: 可配置的细节层次系统 -- **渲染优化**: 支持视锥体剔除、遮挡剔除 -- **扩展性**: 模块化设计,易于扩展新功能 - -## 安装与使用 - -### 安装方法 -```python -# 在EG引擎中加载插件 -import procedural_terrain_generation -terrain_plugin = procedural_terrain_generation.create_plugin(world) -terrain_plugin.initialize() -terrain_plugin.enable() -``` - -### 基本使用 -```python -# 生成地形 -terrain_plugin.generate_terrain(width=512, height=512, seed=12345) - -# 获取地形数据 -terrain_data = terrain_plugin.get_terrain_data() - -# 保存地形 -terrain_plugin.save_terrain("my_terrain.json") - -# 加载地形 -terrain_plugin.load_terrain("my_terrain.json") -``` - -## 核心组件 - -### 1. 地形管理器 (TerrainManager) -负责地形数据的整体管理,包括生成、存储和访问。 - -### 2. 高度图生成器 (HeightmapGenerator) -使用多种噪声算法生成地形高度图。 - -### 3. 噪声生成器 (NoiseGenerator) -提供多种程序化噪声生成算法。 - -### 4. 生物群落生成器 (BiomeGenerator) -根据气候和地理条件生成不同的生态环境。 - -### 5. 侵蚀处理器 (ErosionProcessor) -模拟自然侵蚀过程,增加地形的真实感。 - -### 6. 植被生成器 (VegetationGenerator) -根据环境条件自动生成植被分布。 - -### 7. 地形渲染器 (TerrainRenderer) -负责地形的实时渲染和可视化。 - -### 8. 地形工具类 (TerrainUtils) -提供各种地形分析和处理工具。 - -### 9. 地形序列化器 (TerrainSerializer) -处理地形数据的保存和加载。 - -## API参考 - -### 主要类和方法 - -#### ProceduralTerrainGenerationPlugin -- `initialize()` - 初始化插件 -- `enable()` - 启用插件 -- `generate_terrain(width, height, seed)` - 生成地形 -- `save_terrain(filename)` - 保存地形 -- `load_terrain(filename)` - 加载地形 - -#### TerrainManager -- `generate_terrain(width, height, seed)` - 生成地形数据 -- `get_terrain_data()` - 获取地形数据 -- `get_height_at(x, y)` - 获取指定点高度 -- `get_normal_at(x, y)` - 获取指定点法线 - -#### HeightmapGenerator -- `generate_heightmap(width, height, seed)` - 生成高度图 -- `generate_island_heightmap(width, height, seed)` - 生成岛屿地形 -- `generate_mountain_heightmap(width, height, seed)` - 生成山脉地形 - -#### NoiseGenerator -- `generate_noise(x, y, seed)` - 生成噪声值 -- `generate_fractal_noise(x, y, seed)` - 生成分形噪声 -- `generate_simplex_noise(x, y, seed)` - 生成Simplex噪声 - -#### BiomeGenerator -- `generate_biomes(heightmap, seed)` - 生成生物群落分布 -- `get_biome_info(biome_id)` - 获取生物群落信息 - -#### ErosionProcessor -- `apply_erosion(heightmap, seed)` - 应用侵蚀效果 -- `set_erosion_parameters(erosion_type, params)` - 设置侵蚀参数 - -#### VegetationGenerator -- `generate_vegetation(heightmap, biome_map, seed)` - 生成植被分布 -- `get_vegetation_info(vegetation_id)` - 获取植被信息 - -#### TerrainRenderer -- `render_terrain(camera_position, view_matrix, projection_matrix)` - 渲染地形 -- `generate_terrain_mesh(heightmap)` - 生成地形网格 -- `generate_terrain_texture(heightmap, biome_map)` - 生成地形纹理 - -#### TerrainUtils -- `analyze_terrain_slope(heightmap)` - 分析地形坡度 -- `smooth_terrain(heightmap, iterations)` - 平滑地形 -- `calculate_terrain_statistics(heightmap)` - 计算地形统计信息 - -#### TerrainSerializer -- `save_terrain(filename, terrain_data)` - 保存地形数据 -- `load_terrain(filename)` - 加载地形数据 -- `export_terrain_to_image(heightmap, filename)` - 导出地形为图像 - -## 配置参数 - -### 插件配置 -```python -config = { - 'terrain_size': 512, # 地形尺寸 - 'terrain_scale': 1.0, # 地形缩放 - 'height_scale': 100.0, # 高度缩放 - 'seed': 12345, # 随机种子 - 'lod_levels': 5, # LOD级别数 - 'chunk_size': 32 # 区块大小 -} -``` - -### 噪声参数 -```python -noise_params = { - 'scale': 50.0, # 噪声缩放 - 'octaves': 6, # 八度数 - 'persistence': 0.5, # 持久性 - 'lacunarity': 2.0 # 间隙性 -} -``` - -### 侵蚀参数 -```python -erosion_params = { - 'water_erosion': { - 'rate': 0.1, # 侵蚀速率 - 'solubility': 0.8, # 溶解度 - 'evaporation': 0.01 # 蒸发率 - }, - 'thermal_erosion': { - 'rate': 0.05, # 侵蚀速率 - 'angle_of_repose': 30 # 休止角 - } -} -``` - -## 性能优化 - -### 渲染优化 -- **LOD系统**: 根据距离自动调整细节级别 -- **视锥体剔除**: 只渲染可见区域 -- **遮挡剔除**: 避免渲染被遮挡的区域 -- **区块管理**: 按需加载和卸载地形区块 - -### 内存优化 -- **数据压缩**: 使用zlib压缩保存的地形数据 -- **纹理压缩**: 支持多种纹理压缩格式 -- **内存池**: 复用内存对象减少分配开销 - -### 计算优化 -- **多线程处理**: 利用多核CPU并行计算 -- **缓存机制**: 缓存计算结果避免重复计算 -- **增量更新**: 只更新变化的部分 - -## 扩展开发 - -### 添加新的噪声算法 -```python -# 在NoiseGenerator中添加新方法 -def generate_custom_noise(self, x, y, seed): - # 实现自定义噪声算法 - pass -``` - -### 添加新的生物群落类型 -```python -# 在BiomeGenerator中添加新类型 -biome_types = { - 12: {'name': 'volcano', 'color': (128, 0, 0), 'humidity': 0.2, 'temperature': 0.9} -} -``` - -### 添加新的植被类型 -```python -# 在VegetationGenerator中添加新类型 -vegetation_types = { - 16: {'name': 'lava_plant', 'density': 0.05, 'color': (255, 69, 0)} -} -``` - -## 故障排除 - -### 常见问题 - -#### 地形生成缓慢 -- 检查地形尺寸是否过大 -- 降低LOD级别数 -- 减少噪声八度数 - -#### 内存占用过高 -- 启用数据压缩 -- 减小区块大小 -- 及时清理不需要的地形数据 - -#### 渲染异常 -- 检查显卡驱动 -- 降低渲染质量设置 -- 确认着色器文件是否存在 - -### 错误代码 -- `PTG-001`: 地形生成失败 -- `PTG-002`: 文件保存失败 -- `PTG-003`: 文件加载失败 -- `PTG-004`: 内存不足 - -## 更新日志 - -### v1.0.0 (2024-01-15) -- 初始版本发布 -- 实现基本地形生成功能 -- 支持多种噪声算法 -- 添加生物群落和植被系统 - -### v1.1.0 (2024-02-20) -- 添加侵蚀模拟功能 -- 优化渲染性能 -- 改进地形分析工具 - -### v1.2.0 (2024-03-25) -- 添加数据序列化功能 -- 支持多种文件格式 -- 增强地形编辑工具 - -## 许可证 - -本插件采用MIT许可证,详情请见[LICENSE](LICENSE)文件。 - -## 联系方式 - -- 作者: EG Team -- 邮箱: support@eg-engine.com -- 官网: https://www.eg-engine.com -- GitHub: https://github.com/eg-team/procedural-terrain-generation - ---- - +# 程序化地形生成插件 (Procedural Terrain Generation Plugin) + +![地形生成示例](https://example.com/terrain_example.png) + +为EG引擎提供高性能的程序化地形生成功能。 + +## 功能特性 + +### 🌍 核心功能 +- **高度图生成**: 基于多种噪声算法生成逼真的地形高度图 +- **生物群落分布**: 自动根据气候条件生成不同的生态环境 +- **物理侵蚀模拟**: 模拟水力、风力、热力等自然侵蚀过程 +- **植被分布系统**: 根据环境条件自动生成植被覆盖 +- **实时渲染支持**: 支持LOD、视锥体剔除等优化技术 + +### 🎨 高级特性 +- **多噪声算法**: 支持Perlin、Simplex、分形噪声等多种算法 +- **气候系统**: 湿度、温度等环境因素影响地形生成 +- **地形分析工具**: 坡度、曲率、粗糙度等分析功能 +- **数据序列化**: 支持多种格式的地形数据保存和加载 +- **性能优化**: 多线程处理、内存优化等技术 + +### 🛠️ 技术规格 +- **地形尺寸**: 支持最大8192x8192分辨率 +- **LOD级别**: 可配置的细节层次系统 +- **渲染优化**: 支持视锥体剔除、遮挡剔除 +- **扩展性**: 模块化设计,易于扩展新功能 + +## 安装与使用 + +### 安装方法 +```python +# 在EG引擎中加载插件 +import procedural_terrain_generation +terrain_plugin = procedural_terrain_generation.create_plugin(world) +terrain_plugin.initialize() +terrain_plugin.enable() +``` + +### 基本使用 +```python +# 生成地形 +terrain_plugin.generate_terrain(width=512, height=512, seed=12345) + +# 获取地形数据 +terrain_data = terrain_plugin.get_terrain_data() + +# 保存地形 +terrain_plugin.save_terrain("my_terrain.json") + +# 加载地形 +terrain_plugin.load_terrain("my_terrain.json") +``` + +## 核心组件 + +### 1. 地形管理器 (TerrainManager) +负责地形数据的整体管理,包括生成、存储和访问。 + +### 2. 高度图生成器 (HeightmapGenerator) +使用多种噪声算法生成地形高度图。 + +### 3. 噪声生成器 (NoiseGenerator) +提供多种程序化噪声生成算法。 + +### 4. 生物群落生成器 (BiomeGenerator) +根据气候和地理条件生成不同的生态环境。 + +### 5. 侵蚀处理器 (ErosionProcessor) +模拟自然侵蚀过程,增加地形的真实感。 + +### 6. 植被生成器 (VegetationGenerator) +根据环境条件自动生成植被分布。 + +### 7. 地形渲染器 (TerrainRenderer) +负责地形的实时渲染和可视化。 + +### 8. 地形工具类 (TerrainUtils) +提供各种地形分析和处理工具。 + +### 9. 地形序列化器 (TerrainSerializer) +处理地形数据的保存和加载。 + +## API参考 + +### 主要类和方法 + +#### ProceduralTerrainGenerationPlugin +- `initialize()` - 初始化插件 +- `enable()` - 启用插件 +- `generate_terrain(width, height, seed)` - 生成地形 +- `save_terrain(filename)` - 保存地形 +- `load_terrain(filename)` - 加载地形 + +#### TerrainManager +- `generate_terrain(width, height, seed)` - 生成地形数据 +- `get_terrain_data()` - 获取地形数据 +- `get_height_at(x, y)` - 获取指定点高度 +- `get_normal_at(x, y)` - 获取指定点法线 + +#### HeightmapGenerator +- `generate_heightmap(width, height, seed)` - 生成高度图 +- `generate_island_heightmap(width, height, seed)` - 生成岛屿地形 +- `generate_mountain_heightmap(width, height, seed)` - 生成山脉地形 + +#### NoiseGenerator +- `generate_noise(x, y, seed)` - 生成噪声值 +- `generate_fractal_noise(x, y, seed)` - 生成分形噪声 +- `generate_simplex_noise(x, y, seed)` - 生成Simplex噪声 + +#### BiomeGenerator +- `generate_biomes(heightmap, seed)` - 生成生物群落分布 +- `get_biome_info(biome_id)` - 获取生物群落信息 + +#### ErosionProcessor +- `apply_erosion(heightmap, seed)` - 应用侵蚀效果 +- `set_erosion_parameters(erosion_type, params)` - 设置侵蚀参数 + +#### VegetationGenerator +- `generate_vegetation(heightmap, biome_map, seed)` - 生成植被分布 +- `get_vegetation_info(vegetation_id)` - 获取植被信息 + +#### TerrainRenderer +- `render_terrain(camera_position, view_matrix, projection_matrix)` - 渲染地形 +- `generate_terrain_mesh(heightmap)` - 生成地形网格 +- `generate_terrain_texture(heightmap, biome_map)` - 生成地形纹理 + +#### TerrainUtils +- `analyze_terrain_slope(heightmap)` - 分析地形坡度 +- `smooth_terrain(heightmap, iterations)` - 平滑地形 +- `calculate_terrain_statistics(heightmap)` - 计算地形统计信息 + +#### TerrainSerializer +- `save_terrain(filename, terrain_data)` - 保存地形数据 +- `load_terrain(filename)` - 加载地形数据 +- `export_terrain_to_image(heightmap, filename)` - 导出地形为图像 + +## 配置参数 + +### 插件配置 +```python +config = { + 'terrain_size': 512, # 地形尺寸 + 'terrain_scale': 1.0, # 地形缩放 + 'height_scale': 100.0, # 高度缩放 + 'seed': 12345, # 随机种子 + 'lod_levels': 5, # LOD级别数 + 'chunk_size': 32 # 区块大小 +} +``` + +### 噪声参数 +```python +noise_params = { + 'scale': 50.0, # 噪声缩放 + 'octaves': 6, # 八度数 + 'persistence': 0.5, # 持久性 + 'lacunarity': 2.0 # 间隙性 +} +``` + +### 侵蚀参数 +```python +erosion_params = { + 'water_erosion': { + 'rate': 0.1, # 侵蚀速率 + 'solubility': 0.8, # 溶解度 + 'evaporation': 0.01 # 蒸发率 + }, + 'thermal_erosion': { + 'rate': 0.05, # 侵蚀速率 + 'angle_of_repose': 30 # 休止角 + } +} +``` + +## 性能优化 + +### 渲染优化 +- **LOD系统**: 根据距离自动调整细节级别 +- **视锥体剔除**: 只渲染可见区域 +- **遮挡剔除**: 避免渲染被遮挡的区域 +- **区块管理**: 按需加载和卸载地形区块 + +### 内存优化 +- **数据压缩**: 使用zlib压缩保存的地形数据 +- **纹理压缩**: 支持多种纹理压缩格式 +- **内存池**: 复用内存对象减少分配开销 + +### 计算优化 +- **多线程处理**: 利用多核CPU并行计算 +- **缓存机制**: 缓存计算结果避免重复计算 +- **增量更新**: 只更新变化的部分 + +## 扩展开发 + +### 添加新的噪声算法 +```python +# 在NoiseGenerator中添加新方法 +def generate_custom_noise(self, x, y, seed): + # 实现自定义噪声算法 + pass +``` + +### 添加新的生物群落类型 +```python +# 在BiomeGenerator中添加新类型 +biome_types = { + 12: {'name': 'volcano', 'color': (128, 0, 0), 'humidity': 0.2, 'temperature': 0.9} +} +``` + +### 添加新的植被类型 +```python +# 在VegetationGenerator中添加新类型 +vegetation_types = { + 16: {'name': 'lava_plant', 'density': 0.05, 'color': (255, 69, 0)} +} +``` + +## 故障排除 + +### 常见问题 + +#### 地形生成缓慢 +- 检查地形尺寸是否过大 +- 降低LOD级别数 +- 减少噪声八度数 + +#### 内存占用过高 +- 启用数据压缩 +- 减小区块大小 +- 及时清理不需要的地形数据 + +#### 渲染异常 +- 检查显卡驱动 +- 降低渲染质量设置 +- 确认着色器文件是否存在 + +### 错误代码 +- `PTG-001`: 地形生成失败 +- `PTG-002`: 文件保存失败 +- `PTG-003`: 文件加载失败 +- `PTG-004`: 内存不足 + +## 更新日志 + +### v1.0.0 (2024-01-15) +- 初始版本发布 +- 实现基本地形生成功能 +- 支持多种噪声算法 +- 添加生物群落和植被系统 + +### v1.1.0 (2024-02-20) +- 添加侵蚀模拟功能 +- 优化渲染性能 +- 改进地形分析工具 + +### v1.2.0 (2024-03-25) +- 添加数据序列化功能 +- 支持多种文件格式 +- 增强地形编辑工具 + +## 许可证 + +本插件采用MIT许可证,详情请见[LICENSE](LICENSE)文件。 + +## 联系方式 + +- 作者: EG Team +- 邮箱: support@eg-engine.com +- 官网: https://www.eg-engine.com +- GitHub: https://github.com/eg-team/procedural-terrain-generation + +--- + **注意**: 本插件需要EG引擎v2.0或更高版本才能正常运行。 \ No newline at end of file diff --git a/plugins/user/procedural_terrain_generation/biomes/biome_generator.py b/plugins/user/procedural_terrain_generation/biomes/biome_generator.py index 474eb36c..5494c088 100644 --- a/plugins/user/procedural_terrain_generation/biomes/biome_generator.py +++ b/plugins/user/procedural_terrain_generation/biomes/biome_generator.py @@ -1,1275 +1,1275 @@ -""" -生物群落生成器 -负责生成程序化地形的生物群落分布 -""" - -import numpy as np -import math -from typing import Dict, Any, List, Tuple - -class BiomeGenerator: - """ - 生物群落生成器 - 负责生成程序化地形的生物群落分布,基于高度、湿度、温度等因素 - """ - - def __init__(self, plugin): - """ - 初始化生物群落生成器 - - Args: - plugin: 程序化地形生成插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 生物群落配置 - self.seed = plugin.config.get('seed', 12345) - - # 生物群落类型定义 - self.biome_types = { - 0: {'name': 'ocean', 'color': (0, 0, 128), 'humidity': 0.8, 'temperature': 0.5}, - 1: {'name': 'beach', 'color': (255, 255, 0), 'humidity': 0.6, 'temperature': 0.7}, - 2: {'name': 'plains', 'color': (144, 238, 144), 'humidity': 0.5, 'temperature': 0.6}, - 3: {'name': 'forest', 'color': (34, 139, 34), 'humidity': 0.7, 'temperature': 0.6}, - 4: {'name': 'jungle', 'color': (0, 100, 0), 'humidity': 0.9, 'temperature': 0.8}, - 5: {'name': 'desert', 'color': (255, 165, 0), 'humidity': 0.2, 'temperature': 0.9}, - 6: {'name': 'mountain', 'color': (139, 137, 137), 'humidity': 0.4, 'temperature': 0.4}, - 7: {'name': 'snow', 'color': (255, 255, 255), 'humidity': 0.3, 'temperature': 0.2}, - 8: {'name': 'taiga', 'color': (0, 128, 128), 'humidity': 0.6, 'temperature': 0.3}, - 9: {'name': 'tundra', 'color': (173, 216, 230), 'humidity': 0.4, 'temperature': 0.2}, - 10: {'name': 'swamp', 'color': (128, 128, 0), 'humidity': 0.9, 'temperature': 0.7}, - 11: {'name': 'savanna', 'color': (255, 215, 0), 'humidity': 0.4, 'temperature': 0.8} - } - - # 生物群落分布参数 - self.distribution_params = { - 'humidity_scale': 20.0, - 'temperature_scale': 20.0, - 'height_influence': 0.3, - 'humidity_influence': 0.4, - 'temperature_influence': 0.3, - 'edge_blending': 0.1 - } - - # 气候参数 - self.climate_params = { - 'equator_temperature': 1.0, - 'pole_temperature': 0.0, - 'ocean_temperature_modifier': 0.1, - 'mountain_temperature_drop': 0.3, - 'humidity_persistence': 0.6, - 'humidity_lacunarity': 2.0 - } - - # 噪声生成器引用 - self.noise_generator = None - - # 统计信息 - self.stats = { - 'biomes_generated': 0, - 'total_generation_time': 0.0, - 'average_generation_time': 0.0, - 'biome_distribution': {} - } - - print("✓ 生物群落生成器已创建") - - def initialize(self) -> bool: - """ - 初始化生物群落生成器 - - Returns: - 是否初始化成功 - """ - try: - # 获取噪声生成器引用 - self.noise_generator = self.plugin.noise_generator - - self.initialized = True - print("✓ 生物群落生成器初始化完成") - return True - - except Exception as e: - print(f"✗ 生物群落生成器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用生物群落生成器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 生物群落生成器未初始化") - return False - - self.enabled = True - print("✓ 生物群落生成器已启用") - return True - - except Exception as e: - print(f"✗ 生物群落生成器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用生物群落生成器""" - try: - self.enabled = False - print("✓ 生物群落生成器已禁用") - - except Exception as e: - print(f"✗ 生物群落生成器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理生物群落生成器资源""" - try: - self.disable() - self.initialized = False - print("✓ 生物群落生成器资源已清理") - - except Exception as e: - print(f"✗ 生物群落生成器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新生物群落生成器状态 - - Args: - dt: 时间增量 - """ - # 处理更新逻辑 - pass - - def generate_biomes(self, heightmap: np.ndarray, seed: int = None) -> Dict[str, np.ndarray]: - """ - 生成生物群落分布 - - Args: - heightmap: 高度图数据 - seed: 随机种子 - - Returns: - 包含生物群落图、湿度图和温度图的字典 - """ - try: - import time - generation_start_time = time.time() - - if not self.enabled: - print("✗ 生物群落生成器未启用") - return { - 'biome_map': np.zeros_like(heightmap, dtype=np.int32), - 'moisture_map': np.zeros_like(heightmap, dtype=np.float32), - 'temperature_map': np.zeros_like(heightmap, dtype=np.float32) - } - - # 更新种子 - if seed is not None: - self.seed = seed - - print("✓ 开始生成生物群落分布...") - - # 生成气候数据 - print(" → 生成气候数据...") - climate_data = self._generate_climate_data(heightmap) - moisture_map = climate_data['moisture'] - temperature_map = climate_data['temperature'] - - # 生成生物群落图 - print(" → 生成生物群落图...") - biome_map = self._generate_biome_map(heightmap, moisture_map, temperature_map) - - # 更新统计信息 - generation_time = time.time() - generation_start_time - self.stats['biomes_generated'] += 1 - self.stats['total_generation_time'] += generation_time - self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['biomes_generated'] - - # 更新生物群落分布统计 - self._update_biome_distribution_stats(biome_map) - - print(f"✓ 生物群落生成完成,耗时: {generation_time:.2f}秒") - return { - 'biome_map': biome_map, - 'moisture_map': moisture_map, - 'temperature_map': temperature_map - } - - except Exception as e: - print(f"✗ 生物群落生成失败: {e}") - import traceback - traceback.print_exc() - return { - 'biome_map': np.zeros_like(heightmap, dtype=np.int32), - 'moisture_map': np.zeros_like(heightmap, dtype=np.float32), - 'temperature_map': np.zeros_like(heightmap, dtype=np.float32) - } - - def _generate_climate_data(self, heightmap: np.ndarray) -> Dict[str, np.ndarray]: - """ - 生成气候数据(湿度和温度) - - Args: - heightmap: 高度图数据 - - Returns: - 包含湿度图和温度图的字典 - """ - try: - height, width = heightmap.shape - - # 创建气候图 - moisture_map = np.zeros((height, width), dtype=np.float32) - temperature_map = np.zeros((height, width), dtype=np.float32) - - # 计算中心点(用于纬度计算) - center_y = height / 2.0 - - # 生成基础噪声 - if self.noise_generator: - # 生成湿度噪声 - for y in range(height): - for x in range(width): - moisture_noise = self.noise_generator.generate_fractal_noise( - x / self.distribution_params['humidity_scale'], - y / self.distribution_params['humidity_scale'], - self.seed, - octaves=4, - persistence=self.climate_params['humidity_persistence'], - lacunarity=self.climate_params['humidity_lacunarity'] - ) - moisture_map[y, x] = (moisture_noise + 1.0) / 2.0 # 转换到0-1范围 - - # 生成温度噪声 - for y in range(height): - for x in range(width): - temperature_noise = self.noise_generator.generate_noise( - x / self.distribution_params['temperature_scale'], - y / self.distribution_params['temperature_scale'], - self.seed + 10000 - ) - temperature_map[y, x] = (temperature_noise + 1.0) / 2.0 - - else: - # 如果没有噪声生成器,使用简单的梯度 - moisture_map = np.random.rand(height, width).astype(np.float32) - temperature_map = np.random.rand(height, width).astype(np.float32) - - # 应用纬度影响(赤道热,两极冷) - for y in range(height): - # 计算纬度因子(赤道=1,两极=0) - latitude_factor = 1.0 - abs(y - center_y) / center_y - base_temperature = self.climate_params['pole_temperature'] + \ - (self.climate_params['equator_temperature'] - self.climate_params['pole_temperature']) * latitude_factor - - for x in range(width): - # 应用基础温度 - temperature_map[y, x] = base_temperature + (temperature_map[y, x] - 0.5) * 0.2 - - # 根据高度调整温度(越高越冷) - height_factor = heightmap[y, x] - temperature_map[y, x] -= height_factor * self.climate_params['mountain_temperature_drop'] - - # 海洋温度调整 - if heightmap[y, x] < 0.2: # 假设低于0.2为海洋 - temperature_map[y, x] += self.climate_params['ocean_temperature_modifier'] - - # 确保值在0-1范围内 - moisture_map = np.clip(moisture_map, 0.0, 1.0) - temperature_map = np.clip(temperature_map, 0.0, 1.0) - - return { - 'moisture': moisture_map, - 'temperature': temperature_map - } - - except Exception as e: - print(f"✗ 气候数据生成失败: {e}") - height, width = heightmap.shape - return { - 'moisture': np.random.rand(height, width).astype(np.float32), - 'temperature': np.random.rand(height, width).astype(np.float32) - } - - def _generate_biome_map(self, heightmap: np.ndarray, moisture_map: np.ndarray, - temperature_map: np.ndarray) -> np.ndarray: - """ - 生成生物群落图 - - Args: - heightmap: 高度图数据 - moisture_map: 湿度图数据 - temperature_map: 温度图数据 - - Returns: - 生物群落图 - """ - try: - height, width = heightmap.shape - biome_map = np.zeros((height, width), dtype=np.int32) - - # 为每个像素确定生物群落类型 - for y in range(height): - for x in range(width): - # 获取当前像素的属性 - height_value = heightmap[y, x] - moisture_value = moisture_map[y, x] - temperature_value = temperature_map[y, x] - - # 根据属性确定生物群落 - biome_type = self._determine_biome(height_value, moisture_value, temperature_value) - biome_map[y, x] = biome_type - - # 应用边缘混合以减少生物群落之间的硬边界 - biome_map = self._apply_edge_blending(biome_map, heightmap, moisture_map, temperature_map) - - return biome_map - - except Exception as e: - print(f"✗ 生物群落图生成失败: {e}") - return np.zeros_like(heightmap, dtype=np.int32) - - def _determine_biome(self, height: float, moisture: float, temperature: float) -> int: - """ - 根据属性确定生物群落类型 - - Args: - height: 高度值 (0-1) - moisture: 湿度值 (0-1) - temperature: 温度值 (0-1) - - Returns: - 生物群落类型ID - """ - try: - # 海洋 - if height < 0.2: - return 0 # ocean - - # 海滩 - if height < 0.25: - return 1 # beach - - # 根据湿度和温度组合确定生物群落 - if temperature > 0.7: # 热 - if moisture > 0.7: # 湿 - return 4 # jungle - elif moisture > 0.3: # 中等湿度 - return 11 # savanna - else: # 干燥 - return 5 # desert - elif temperature > 0.4: # 温暖 - if moisture > 0.7: # 湿 - if height > 0.7: # 高海拔 - return 3 # forest (山地森林) - else: - return 3 # forest - elif moisture > 0.4: # 中等湿度 - return 2 # plains - else: # 干燥 - return 2 # plains - else: # 冷 - if moisture > 0.6: # 湿 - if height > 0.8: # 高海拔 - return 7 # snow - else: - return 8 # taiga - elif moisture > 0.3: # 中等湿度 - if height > 0.6: # 高海拔 - return 7 # snow - else: - return 9 # tundra - else: # 干燥 - if height > 0.5: # 高海拔 - return 7 # snow - else: - return 9 # tundra - - except Exception as e: - print(f"✗ 生物群落确定失败: {e}") - return 2 # 默认为平原 - - def _apply_edge_blending(self, biome_map: np.ndarray, heightmap: np.ndarray, - moisture_map: np.ndarray, temperature_map: np.ndarray) -> np.ndarray: - """ - 应用边缘混合以减少生物群落之间的硬边界 - - Args: - biome_map: 原始生物群落图 - heightmap: 高度图数据 - moisture_map: 湿度图数据 - temperature_map: 温度图数据 - - Returns: - 边缘混合后的生物群落图 - """ - try: - height, width = biome_map.shape - blended_map = biome_map.copy() - - # 混合边缘宽度 - blend_width = max(1, int(min(height, width) * self.distribution_params['edge_blending'])) - - # 简化的边缘混合(只在生物群落边界应用) - for y in range(height): - for x in range(width): - current_biome = biome_map[y, x] - - # 检查周围像素是否属于不同生物群落 - different_neighbors = 0 - total_neighbors = 0 - - for dy in range(-blend_width, blend_width + 1): - for dx in range(-blend_width, blend_width + 1): - if dy == 0 and dx == 0: - continue - - ny, nx = y + dy, x + dx - if 0 <= ny < height and 0 <= nx < width: - total_neighbors += 1 - if biome_map[ny, nx] != current_biome: - different_neighbors += 1 - - # 如果有不同邻居,则有一定概率混合 - if total_neighbors > 0 and different_neighbors > 0: - blend_probability = different_neighbors / total_neighbors - if np.random.random() < blend_probability * 0.5: # 50%的混合概率 - # 随机选择一个邻居的生物群落 - neighbor_biomes = [] - for dy in range(-1, 2): - for dx in range(-1, 2): - if dy == 0 and dx == 0: - continue - ny, nx = y + dy, x + dx - if 0 <= ny < height and 0 <= nx < width: - neighbor_biomes.append(biome_map[ny, nx]) - - if neighbor_biomes: - blended_map[y, x] = np.random.choice(neighbor_biomes) - - return blended_map - - except Exception as e: - print(f"✗ 边缘混合应用失败: {e}") - return biome_map - - def _update_biome_distribution_stats(self, biome_map: np.ndarray): - """更新生物群落分布统计""" - try: - unique, counts = np.unique(biome_map, return_counts=True) - for biome_id, count in zip(unique, counts): - biome_name = self.biome_types.get(biome_id, {}).get('name', f'biome_{biome_id}') - self.stats['biome_distribution'][biome_name] = self.stats['biome_distribution'].get(biome_name, 0) + count - - except Exception as e: - print(f"✗ 生物群落分布统计更新失败: {e}") - - def set_seed(self, seed: int): - """ - 设置随机种子 - - Args: - seed: 随机种子 - """ - self.seed = seed - print(f"✓ 生物群落生成器随机种子设置为: {seed}") - - def set_distribution_parameters(self, params: Dict[str, float]): - """ - 设置分布参数 - - Args: - params: 分布参数字典 - """ - self.distribution_params.update(params) - print(f"✓ 生物群落分布参数已更新: {self.distribution_params}") - - def set_climate_parameters(self, params: Dict[str, float]): - """ - 设置气候参数 - - Args: - params: 气候参数字典 - """ - self.climate_params.update(params) - print(f"✓ 气候参数已更新: {self.climate_params}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - # 更新平均生成时间 - if self.stats['biomes_generated'] > 0: - self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['biomes_generated'] - return self.stats.copy() - - def get_biome_info(self, biome_id: int) -> Dict[str, Any]: - """ - 获取生物群落信息 - - Args: - biome_id: 生物群落ID - - Returns: - 生物群落信息字典 - """ - return self.biome_types.get(biome_id, { - 'name': f'unknown_{biome_id}', - 'color': (128, 128, 128), - 'humidity': 0.5, - 'temperature': 0.5 - }) - - def get_available_biomes(self) -> List[Dict[str, Any]]: - """ - 获取可用的生物群落列表 - - Returns: - 生物群落信息列表 - """ - return [self.get_biome_info(biome_id) for biome_id in sorted(self.biome_types.keys())] - - def generate_biome_with_zones(self, heightmap: np.ndarray, zones: Dict[str, Any], - seed: int = None) -> Dict[str, np.ndarray]: - """ - 根据区域定义生成生物群落 - - Args: - heightmap: 高度图数据 - zones: 区域定义字典 - seed: 随机种子 - - Returns: - 包含生物群落图、湿度图和温度图的字典 - """ - try: - import time - generation_start_time = time.time() - - if not self.enabled: - print("✗ 生物群落生成器未启用") - return { - 'biome_map': np.zeros_like(heightmap, dtype=np.int32), - 'moisture_map': np.zeros_like(heightmap, dtype=np.float32), - 'temperature_map': np.zeros_like(heightmap, dtype=np.float32) - } - - # 更新种子 - if seed is not None: - self.seed = seed - - print("✓ 开始生成区域化生物群落分布...") - - # 生成基础气候数据 - climate_data = self._generate_climate_data(heightmap) - moisture_map = climate_data['moisture'] - temperature_map = climate_data['temperature'] - - # 创建生物群落图 - height, width = heightmap.shape - biome_map = np.zeros((height, width), dtype=np.int32) - - # 首先生成基础生物群落 - print(" → 生成基础生物群落...") - for y in range(height): - for x in range(width): - height_value = heightmap[y, x] - moisture_value = moisture_map[y, x] - temperature_value = temperature_map[y, x] - biome_map[y, x] = self._determine_biome(height_value, moisture_value, temperature_value) - - # 然后应用区域定义 - print(" → 应用区域定义...") - for zone_name, zone_params in zones.items(): - zone_biome = zone_params.get('biome', 2) # 默认为平原 - zone_position = zone_params.get('position', (0, 0)) - zone_size = zone_params.get('size', (width, height)) - - x_start, y_start = zone_position - zone_width, zone_height = zone_size - - # 确保区域在地图范围内 - x_start = max(0, min(x_start, width - 1)) - y_start = max(0, min(y_start, height - 1)) - x_end = min(width, x_start + zone_width) - y_end = min(height, y_start + zone_height) - - # 在区域中应用指定的生物群落 - for y in range(y_start, y_end): - for x in range(x_start, x_end): - biome_map[y, x] = zone_biome - - # 应用边缘混合 - biome_map = self._apply_edge_blending(biome_map, heightmap, moisture_map, temperature_map) - - # 更新统计信息 - generation_time = time.time() - generation_start_time - self.stats['biomes_generated'] += 1 - self.stats['total_generation_time'] += generation_time - self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['biomes_generated'] - - # 更新生物群落分布统计 - self._update_biome_distribution_stats(biome_map) - - print(f"✓ 区域化生物群落生成完成,耗时: {generation_time:.2f}秒") - return { - 'biome_map': biome_map, - 'moisture_map': moisture_map, - 'temperature_map': temperature_map - } - - except Exception as e: - print(f"✗ 区域化生物群落生成失败: {e}") - import traceback - traceback.print_exc() - height, width = heightmap.shape - return { - 'biome_map': np.zeros((height, width), dtype=np.int32), - 'moisture_map': np.zeros((height, width), dtype=np.float32), - 'temperature_map': np.zeros((height, width), dtype=np.float32) - } - - def generate_biome_from_rules(self, heightmap: np.ndarray, rules: List[Dict[str, Any]], - seed: int = None) -> Dict[str, np.ndarray]: - """ - 根据规则生成生物群落 - - Args: - heightmap: 高度图数据 - rules: 生物群落生成规则列表 - seed: 随机种子 - - Returns: - 包含生物群落图、湿度图和温度图的字典 - """ - try: - import time - generation_start_time = time.time() - - if not self.enabled: - print("✗ 生物群落生成器未启用") - return { - 'biome_map': np.zeros_like(heightmap, dtype=np.int32), - 'moisture_map': np.zeros_like(heightmap, dtype=np.float32), - 'temperature_map': np.zeros_like(heightmap, dtype=np.float32) - } - - # 更新种子 - if seed is not None: - self.seed = seed - - print("✓ 开始基于规则生成生物群落...") - - # 生成基础气候数据 - climate_data = self._generate_climate_data(heightmap) - moisture_map = climate_data['moisture'] - temperature_map = climate_data['temperature'] - - # 创建生物群落图 - height, width = heightmap.shape - biome_map = np.zeros((height, width), dtype=np.int32) - - # 根据规则生成生物群落 - for y in range(height): - for x in range(width): - height_value = heightmap[y, x] - moisture_value = moisture_map[y, x] - temperature_value = temperature_map[y, x] - - # 应用规则 - biome_type = self._apply_biome_rules( - height_value, moisture_value, temperature_value, rules - ) - biome_map[y, x] = biome_type - - # 应用边缘混合 - biome_map = self._apply_edge_blending(biome_map, heightmap, moisture_map, temperature_map) - - # 更新统计信息 - generation_time = time.time() - generation_start_time - self.stats['biomes_generated'] += 1 - self.stats['total_generation_time'] += generation_time - self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['biomes_generated'] - - # 更新生物群落分布统计 - self._update_biome_distribution_stats(biome_map) - - print(f"✓ 基于规则的生物群落生成完成,耗时: {generation_time:.2f}秒") - return { - 'biome_map': biome_map, - 'moisture_map': moisture_map, - 'temperature_map': temperature_map - } - - except Exception as e: - print(f"✗ 基于规则的生物群落生成失败: {e}") - import traceback - traceback.print_exc() - height, width = heightmap.shape - return { - 'biome_map': np.zeros((height, width), dtype=np.int32), - 'moisture_map': np.zeros((height, width), dtype=np.float32), - 'temperature_map': np.zeros((height, width), dtype=np.float32) - } - - def _apply_biome_rules(self, height: float, moisture: float, temperature: float, - rules: List[Dict[str, Any]]) -> int: - """ - 根据规则确定生物群落类型 - - Args: - height: 高度值 - moisture: 湿度值 - temperature: 温度值 - rules: 规则列表 - - Returns: - 生物群落类型ID - """ - try: - # 遍历规则 - for rule in rules: - # 检查条件是否满足 - conditions = rule.get('conditions', {}) - match = True - - # 检查高度条件 - if 'height' in conditions: - height_range = conditions['height'] - if not (height_range[0] <= height <= height_range[1]): - match = False - - # 检查湿度条件 - if 'moisture' in conditions: - moisture_range = conditions['moisture'] - if not (moisture_range[0] <= moisture <= moisture_range[1]): - match = False - - # 检查温度条件 - if 'temperature' in conditions: - temperature_range = conditions['temperature'] - if not (temperature_range[0] <= temperature <= temperature_range[1]): - match = False - - # 如果所有条件都满足,应用该规则 - if match: - return rule.get('biome', 2) # 默认为平原 - - # 如果没有规则匹配,使用默认方法 - return self._determine_biome(height, moisture, temperature) - - except Exception as e: - print(f"✗ 生物群落规则应用失败: {e}") - return self._determine_biome(height, moisture, temperature) - - def generate_biome_with_influence_maps(self, heightmap: np.ndarray, - influence_maps: Dict[str, np.ndarray], - seed: int = None) -> Dict[str, np.ndarray]: - """ - 使用影响力图生成生物群落 - - Args: - heightmap: 高度图数据 - influence_maps: 影响力图字典(如'forest_influence', 'desert_influence'等) - seed: 随机种子 - - Returns: - 包含生物群落图、湿度图和温度图的字典 - """ - try: - import time - generation_start_time = time.time() - - if not self.enabled: - print("✗ 生物群落生成器未启用") - return { - 'biome_map': np.zeros_like(heightmap, dtype=np.int32), - 'moisture_map': np.zeros_like(heightmap, dtype=np.float32), - 'temperature_map': np.zeros_like(heightmap, dtype=np.float32) - } - - # 更新种子 - if seed is not None: - self.seed = seed - - print("✓ 开始使用影响力图生成生物群落...") - - # 生成基础气候数据 - climate_data = self._generate_climate_data(heightmap) - moisture_map = climate_data['moisture'] - temperature_map = climate_data['temperature'] - - # 创建生物群落图 - height, width = heightmap.shape - biome_map = np.zeros((height, width), dtype=np.int32) - - # 为每个像素根据影响力图确定生物群落 - for y in range(height): - for x in range(width): - height_value = heightmap[y, x] - moisture_value = moisture_map[y, x] - temperature_value = temperature_map[y, x] - - # 获取影响力值 - influences = {} - for biome_name, influence_map in influence_maps.items(): - if 0 <= y < influence_map.shape[0] and 0 <= x < influence_map.shape[1]: - influences[biome_name] = influence_map[y, x] - else: - influences[biome_name] = 0.0 - - # 根据影响力确定生物群落 - biome_type = self._determine_biome_by_influence( - height_value, moisture_value, temperature_value, influences - ) - biome_map[y, x] = biome_type - - # 应用边缘混合 - biome_map = self._apply_edge_blending(biome_map, heightmap, moisture_map, temperature_map) - - # 更新统计信息 - generation_time = time.time() - generation_start_time - self.stats['biomes_generated'] += 1 - self.stats['total_generation_time'] += generation_time - self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['biomes_generated'] - - # 更新生物群落分布统计 - self._update_biome_distribution_stats(biome_map) - - print(f"✓ 基于影响力图的生物群落生成完成,耗时: {generation_time:.2f}秒") - return { - 'biome_map': biome_map, - 'moisture_map': moisture_map, - 'temperature_map': temperature_map - } - - except Exception as e: - print(f"✗ 基于影响力图的生物群落生成失败: {e}") - import traceback - traceback.print_exc() - height, width = heightmap.shape - return { - 'biome_map': np.zeros((height, width), dtype=np.int32), - 'moisture_map': np.zeros((height, width), dtype=np.float32), - 'temperature_map': np.zeros((height, width), dtype=np.float32) - } - - def _determine_biome_by_influence(self, height: float, moisture: float, temperature: float, - influences: Dict[str, float]) -> int: - """ - 根据影响力确定生物群落类型 - - Args: - height: 高度值 - moisture: 湿度值 - temperature: 温度值 - influences: 各生物群落的影响力 - - Returns: - 生物群落类型ID - """ - try: - # 如果有任何影响力大于阈值,选择影响力最大的生物群落 - max_influence = 0.0 - selected_biome = None - - for biome_name, influence in influences.items(): - if influence > max_influence and influence > 0.3: # 阈值 - max_influence = influence - # 将生物群落名称映射到ID - for biome_id, biome_info in self.biome_types.items(): - if biome_info['name'] == biome_name: - selected_biome = biome_id - break - - # 如果有选定的生物群落,返回它 - if selected_biome is not None: - return selected_biome - - # 否则使用默认方法 - return self._determine_biome(height, moisture, temperature) - - except Exception as e: - print(f"✗ 基于影响力的生物群落确定失败: {e}") - return self._determine_biome(height, moisture, temperature) - - def generate_biome_with_erosion_influence(self, heightmap: np.ndarray, - erosion_map: np.ndarray, - sediment_map: np.ndarray, - seed: int = None) -> Dict[str, np.ndarray]: - """ - 考虑侵蚀和沉积影响生成生物群落 - - Args: - heightmap: 高度图数据 - erosion_map: 侵蚀图数据 - sediment_map: 沉积图数据 - seed: 随机种子 - - Returns: - 包含生物群落图、湿度图和温度图的字典 - """ - try: - import time - generation_start_time = time.time() - - if not self.enabled: - print("✗ 生物群落生成器未启用") - return { - 'biome_map': np.zeros_like(heightmap, dtype=np.int32), - 'moisture_map': np.zeros_like(heightmap, dtype=np.float32), - 'temperature_map': np.zeros_like(heightmap, dtype=np.float32) - } - - # 更新种子 - if seed is not None: - self.seed = seed - - print("✓ 开始生成受侵蚀影响的生物群落...") - - # 生成基础气候数据 - climate_data = self._generate_climate_data(heightmap) - moisture_map = climate_data['moisture'] - temperature_map = climate_data['temperature'] - - # 创建生物群落图 - height, width = heightmap.shape - biome_map = np.zeros((height, width), dtype=np.int32) - - # 考虑侵蚀和沉积影响生成生物群落 - for y in range(height): - for x in range(width): - height_value = heightmap[y, x] - moisture_value = moisture_map[y, x] - temperature_value = temperature_map[y, x] - erosion_value = erosion_map[y, x] if y < erosion_map.shape[0] and x < erosion_map.shape[1] else 0 - sediment_value = sediment_map[y, x] if y < sediment_map.shape[0] and x < sediment_map.shape[1] else 0 - - # 调整湿度和温度以考虑侵蚀影响 - adjusted_moisture = moisture_value + sediment_value * 0.2 - erosion_value * 0.1 - adjusted_temperature = temperature_value - erosion_value * 0.1 - - # 确保调整后的值在有效范围内 - adjusted_moisture = max(0.0, min(1.0, adjusted_moisture)) - adjusted_temperature = max(0.0, min(1.0, adjusted_temperature)) - - # 确定生物群落 - biome_map[y, x] = self._determine_biome(height_value, adjusted_moisture, adjusted_temperature) - - # 应用边缘混合 - biome_map = self._apply_edge_blending(biome_map, heightmap, moisture_map, temperature_map) - - # 更新统计信息 - generation_time = time.time() - generation_start_time - self.stats['biomes_generated'] += 1 - self.stats['total_generation_time'] += generation_time - self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['biomes_generated'] - - # 更新生物群落分布统计 - self._update_biome_distribution_stats(biome_map) - - print(f"✓ 受侵蚀影响的生物群落生成完成,耗时: {generation_time:.2f}秒") - return { - 'biome_map': biome_map, - 'moisture_map': moisture_map, - 'temperature_map': temperature_map - } - - except Exception as e: - print(f"✗ 受侵蚀影响的生物群落生成失败: {e}") - import traceback - traceback.print_exc() - height, width = heightmap.shape - return { - 'biome_map': np.zeros((height, width), dtype=np.int32), - 'moisture_map': np.zeros((height, width), dtype=np.float32), - 'temperature_map': np.zeros((height, width), dtype=np.float32) - } - - def blend_biome_maps(self, biome_maps: List[np.ndarray], weights: List[float] = None) -> np.ndarray: - """ - 混合多个生物群落图 - - Args: - biome_maps: 生物群落图列表 - weights: 权重列表(如果为None,则平均分配权重) - - Returns: - 混合后的生物群落图 - """ - try: - if not biome_maps: - return np.array([]) - - if len(biome_maps) == 1: - return biome_maps[0].copy() - - # 确保所有生物群落图尺寸相同 - base_shape = biome_maps[0].shape - for bm in biome_maps: - if bm.shape != base_shape: - raise ValueError("所有生物群落图必须具有相同的尺寸") - - # 处理权重 - if weights is None: - weights = [1.0 / len(biome_maps)] * len(biome_maps) - elif len(weights) != len(biome_maps): - raise ValueError("权重数量必须与生物群落图数量相同") - - # 归一化权重 - total_weight = sum(weights) - if total_weight > 0: - weights = [w / total_weight for w in weights] - - # 混合生物群落图 - height, width = base_shape - blended = np.zeros(base_shape, dtype=np.int32) - - # 为每个像素计算加权生物群落类型 - for y in range(height): - for x in range(width): - # 计算每个生物群落类型的加权投票 - biome_votes = {} - for i, (bm, weight) in enumerate(zip(biome_maps, weights)): - biome_type = bm[y, x] - biome_votes[biome_type] = biome_votes.get(biome_type, 0.0) + weight - - # 选择得票最多的生物群落类型 - blended[y, x] = max(biome_votes, key=biome_votes.get) - - return blended - - except Exception as e: - print(f"✗ 生物群落图混合失败: {e}") - import traceback - traceback.print_exc() - # 返回第一个生物群落图的副本 - return biome_maps[0].copy() if biome_maps else np.array([]) - - def smooth_biome_map(self, biome_map: np.ndarray, iterations: int = 1) -> np.ndarray: - """ - 平滑生物群落图 - - Args: - biome_map: 原始生物群落图 - iterations: 平滑迭代次数 - - Returns: - 平滑后的生物群落图 - """ - try: - smoothed = biome_map.copy() - height, width = smoothed.shape - - for _ in range(iterations): - new_map = smoothed.copy() - - # 对每个像素进行平滑处理 - for y in range(height): - for x in range(width): - # 统计邻居的生物群落类型 - neighbor_types = [] - - # 检查8个邻居 - for dy in [-1, 0, 1]: - for dx in [-1, 0, 1]: - if dy == 0 and dx == 0: - continue - - ny, nx = y + dy, x + dx - if 0 <= ny < height and 0 <= nx < width: - neighbor_types.append(smoothed[ny, nx]) - - # 如果大多数邻居是相同类型,则改变当前像素 - if neighbor_types: - unique, counts = np.unique(neighbor_types, return_counts=True) - most_common_type = unique[np.argmax(counts)] - most_common_count = np.max(counts) - - # 如果超过一半的邻居是相同类型,则采用该类型 - if most_common_count > len(neighbor_types) / 2: - new_map[y, x] = most_common_type - - smoothed = new_map - - return smoothed - - except Exception as e: - print(f"✗ 生物群落图平滑失败: {e}") - return biome_map - - def generate_biome_colors(self, biome_map: np.ndarray) -> np.ndarray: - """ - 为生物群落图生成颜色表示 - - Args: - biome_map: 生物群落图 - - Returns: - 颜色图 (height, width, 3) - """ - try: - height, width = biome_map.shape - color_map = np.zeros((height, width, 3), dtype=np.uint8) - - # 为每个生物群落类型分配颜色 - for y in range(height): - for x in range(width): - biome_id = biome_map[y, x] - biome_info = self.biome_types.get(biome_id, {'color': (128, 128, 128)}) - color_map[y, x] = biome_info['color'] - - return color_map - - except Exception as e: - print(f"✗ 生物群落颜色图生成失败: {e}") - height, width = biome_map.shape - return np.zeros((height, width, 3), dtype=np.uint8) - - def export_biome_legend(self, filename: str) -> bool: - """ - 导出生物群落图例 - - Args: - filename: 文件名 - - Returns: - 是否导出成功 - """ - try: - import json - - legend_data = {} - for biome_id, biome_info in self.biome_types.items(): - legend_data[biome_id] = { - 'name': biome_info['name'], - 'color': biome_info['color'], - 'humidity': biome_info['humidity'], - 'temperature': biome_info['temperature'] - } - - with open(filename, 'w', encoding='utf-8') as f: - json.dump(legend_data, f, ensure_ascii=False, indent=2) - - print(f"✓ 生物群落图例已导出到: {filename}") - return True - - except Exception as e: - print(f"✗ 生物群落图例导出失败: {e}") - return False - - def import_biome_legend(self, filename: str) -> bool: - """ - 导入生物群落图例 - - Args: - filename: 文件名 - - Returns: - 是否导入成功 - """ - try: - import json - - with open(filename, 'r', encoding='utf-8') as f: - legend_data = json.load(f) - - # 更新生物群落类型 - self.biome_types = {} - for biome_id, biome_info in legend_data.items(): - self.biome_types[int(biome_id)] = { - 'name': biome_info['name'], - 'color': tuple(biome_info['color']), - 'humidity': biome_info['humidity'], - 'temperature': biome_info['temperature'] - } - - print(f"✓ 生物群落图例已从 {filename} 导入") - return True - - except Exception as e: - print(f"✗ 生物群落图例导入失败: {e}") - return False - - def get_biome_distribution_stats(self) -> Dict[str, int]: - """ - 获取生物群落分布统计 - - Returns: - 生物群落分布统计字典 - """ - return self.stats['biome_distribution'].copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'biomes_generated': 0, - 'total_generation_time': 0.0, - 'average_generation_time': 0.0, - 'biome_distribution': {} - } - print("✓ 生物群落生成器统计信息已重置") - - def set_biome_types(self, biome_types: Dict[int, Dict[str, Any]]): - """ - 设置生物群落类型 - - Args: - biome_types: 生物群落类型字典 - """ - self.biome_types = biome_types - print(f"✓ 生物群落类型已更新,共 {len(biome_types)} 种生物群落") - - def add_biome_type(self, biome_id: int, biome_info: Dict[str, Any]): - """ - 添加生物群落类型 - - Args: - biome_id: 生物群落ID - biome_info: 生物群落信息 - """ - self.biome_types[biome_id] = biome_info - print(f"✓ 生物群落类型 {biome_info['name']} (ID: {biome_id}) 已添加") - - def remove_biome_type(self, biome_id: int): - """ - 移除生物群落类型 - - Args: - biome_id: 生物群落ID - """ - if biome_id in self.biome_types: - biome_name = self.biome_types[biome_id]['name'] - del self.biome_types[biome_id] - print(f"✓ 生物群落类型 {biome_name} (ID: {biome_id}) 已移除") - else: - print(f"✗ 无效的生物群落ID: {biome_id}") - - def modify_biome_type(self, biome_id: int, modifications: Dict[str, Any]): - """ - 修改生物群落类型 - - Args: - biome_id: 生物群落ID - modifications: 修改内容 - """ - if biome_id in self.biome_types: - self.biome_types[biome_id].update(modifications) - print(f"✓ 生物群落类型 (ID: {biome_id}) 已修改") - else: +""" +生物群落生成器 +负责生成程序化地形的生物群落分布 +""" + +import numpy as np +import math +from typing import Dict, Any, List, Tuple + +class BiomeGenerator: + """ + 生物群落生成器 + 负责生成程序化地形的生物群落分布,基于高度、湿度、温度等因素 + """ + + def __init__(self, plugin): + """ + 初始化生物群落生成器 + + Args: + plugin: 程序化地形生成插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 生物群落配置 + self.seed = plugin.config.get('seed', 12345) + + # 生物群落类型定义 + self.biome_types = { + 0: {'name': 'ocean', 'color': (0, 0, 128), 'humidity': 0.8, 'temperature': 0.5}, + 1: {'name': 'beach', 'color': (255, 255, 0), 'humidity': 0.6, 'temperature': 0.7}, + 2: {'name': 'plains', 'color': (144, 238, 144), 'humidity': 0.5, 'temperature': 0.6}, + 3: {'name': 'forest', 'color': (34, 139, 34), 'humidity': 0.7, 'temperature': 0.6}, + 4: {'name': 'jungle', 'color': (0, 100, 0), 'humidity': 0.9, 'temperature': 0.8}, + 5: {'name': 'desert', 'color': (255, 165, 0), 'humidity': 0.2, 'temperature': 0.9}, + 6: {'name': 'mountain', 'color': (139, 137, 137), 'humidity': 0.4, 'temperature': 0.4}, + 7: {'name': 'snow', 'color': (255, 255, 255), 'humidity': 0.3, 'temperature': 0.2}, + 8: {'name': 'taiga', 'color': (0, 128, 128), 'humidity': 0.6, 'temperature': 0.3}, + 9: {'name': 'tundra', 'color': (173, 216, 230), 'humidity': 0.4, 'temperature': 0.2}, + 10: {'name': 'swamp', 'color': (128, 128, 0), 'humidity': 0.9, 'temperature': 0.7}, + 11: {'name': 'savanna', 'color': (255, 215, 0), 'humidity': 0.4, 'temperature': 0.8} + } + + # 生物群落分布参数 + self.distribution_params = { + 'humidity_scale': 20.0, + 'temperature_scale': 20.0, + 'height_influence': 0.3, + 'humidity_influence': 0.4, + 'temperature_influence': 0.3, + 'edge_blending': 0.1 + } + + # 气候参数 + self.climate_params = { + 'equator_temperature': 1.0, + 'pole_temperature': 0.0, + 'ocean_temperature_modifier': 0.1, + 'mountain_temperature_drop': 0.3, + 'humidity_persistence': 0.6, + 'humidity_lacunarity': 2.0 + } + + # 噪声生成器引用 + self.noise_generator = None + + # 统计信息 + self.stats = { + 'biomes_generated': 0, + 'total_generation_time': 0.0, + 'average_generation_time': 0.0, + 'biome_distribution': {} + } + + print("✓ 生物群落生成器已创建") + + def initialize(self) -> bool: + """ + 初始化生物群落生成器 + + Returns: + 是否初始化成功 + """ + try: + # 获取噪声生成器引用 + self.noise_generator = self.plugin.noise_generator + + self.initialized = True + print("✓ 生物群落生成器初始化完成") + return True + + except Exception as e: + print(f"✗ 生物群落生成器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用生物群落生成器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 生物群落生成器未初始化") + return False + + self.enabled = True + print("✓ 生物群落生成器已启用") + return True + + except Exception as e: + print(f"✗ 生物群落生成器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用生物群落生成器""" + try: + self.enabled = False + print("✓ 生物群落生成器已禁用") + + except Exception as e: + print(f"✗ 生物群落生成器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理生物群落生成器资源""" + try: + self.disable() + self.initialized = False + print("✓ 生物群落生成器资源已清理") + + except Exception as e: + print(f"✗ 生物群落生成器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新生物群落生成器状态 + + Args: + dt: 时间增量 + """ + # 处理更新逻辑 + pass + + def generate_biomes(self, heightmap: np.ndarray, seed: int = None) -> Dict[str, np.ndarray]: + """ + 生成生物群落分布 + + Args: + heightmap: 高度图数据 + seed: 随机种子 + + Returns: + 包含生物群落图、湿度图和温度图的字典 + """ + try: + import time + generation_start_time = time.time() + + if not self.enabled: + print("✗ 生物群落生成器未启用") + return { + 'biome_map': np.zeros_like(heightmap, dtype=np.int32), + 'moisture_map': np.zeros_like(heightmap, dtype=np.float32), + 'temperature_map': np.zeros_like(heightmap, dtype=np.float32) + } + + # 更新种子 + if seed is not None: + self.seed = seed + + print("✓ 开始生成生物群落分布...") + + # 生成气候数据 + print(" → 生成气候数据...") + climate_data = self._generate_climate_data(heightmap) + moisture_map = climate_data['moisture'] + temperature_map = climate_data['temperature'] + + # 生成生物群落图 + print(" → 生成生物群落图...") + biome_map = self._generate_biome_map(heightmap, moisture_map, temperature_map) + + # 更新统计信息 + generation_time = time.time() - generation_start_time + self.stats['biomes_generated'] += 1 + self.stats['total_generation_time'] += generation_time + self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['biomes_generated'] + + # 更新生物群落分布统计 + self._update_biome_distribution_stats(biome_map) + + print(f"✓ 生物群落生成完成,耗时: {generation_time:.2f}秒") + return { + 'biome_map': biome_map, + 'moisture_map': moisture_map, + 'temperature_map': temperature_map + } + + except Exception as e: + print(f"✗ 生物群落生成失败: {e}") + import traceback + traceback.print_exc() + return { + 'biome_map': np.zeros_like(heightmap, dtype=np.int32), + 'moisture_map': np.zeros_like(heightmap, dtype=np.float32), + 'temperature_map': np.zeros_like(heightmap, dtype=np.float32) + } + + def _generate_climate_data(self, heightmap: np.ndarray) -> Dict[str, np.ndarray]: + """ + 生成气候数据(湿度和温度) + + Args: + heightmap: 高度图数据 + + Returns: + 包含湿度图和温度图的字典 + """ + try: + height, width = heightmap.shape + + # 创建气候图 + moisture_map = np.zeros((height, width), dtype=np.float32) + temperature_map = np.zeros((height, width), dtype=np.float32) + + # 计算中心点(用于纬度计算) + center_y = height / 2.0 + + # 生成基础噪声 + if self.noise_generator: + # 生成湿度噪声 + for y in range(height): + for x in range(width): + moisture_noise = self.noise_generator.generate_fractal_noise( + x / self.distribution_params['humidity_scale'], + y / self.distribution_params['humidity_scale'], + self.seed, + octaves=4, + persistence=self.climate_params['humidity_persistence'], + lacunarity=self.climate_params['humidity_lacunarity'] + ) + moisture_map[y, x] = (moisture_noise + 1.0) / 2.0 # 转换到0-1范围 + + # 生成温度噪声 + for y in range(height): + for x in range(width): + temperature_noise = self.noise_generator.generate_noise( + x / self.distribution_params['temperature_scale'], + y / self.distribution_params['temperature_scale'], + self.seed + 10000 + ) + temperature_map[y, x] = (temperature_noise + 1.0) / 2.0 + + else: + # 如果没有噪声生成器,使用简单的梯度 + moisture_map = np.random.rand(height, width).astype(np.float32) + temperature_map = np.random.rand(height, width).astype(np.float32) + + # 应用纬度影响(赤道热,两极冷) + for y in range(height): + # 计算纬度因子(赤道=1,两极=0) + latitude_factor = 1.0 - abs(y - center_y) / center_y + base_temperature = self.climate_params['pole_temperature'] + \ + (self.climate_params['equator_temperature'] - self.climate_params['pole_temperature']) * latitude_factor + + for x in range(width): + # 应用基础温度 + temperature_map[y, x] = base_temperature + (temperature_map[y, x] - 0.5) * 0.2 + + # 根据高度调整温度(越高越冷) + height_factor = heightmap[y, x] + temperature_map[y, x] -= height_factor * self.climate_params['mountain_temperature_drop'] + + # 海洋温度调整 + if heightmap[y, x] < 0.2: # 假设低于0.2为海洋 + temperature_map[y, x] += self.climate_params['ocean_temperature_modifier'] + + # 确保值在0-1范围内 + moisture_map = np.clip(moisture_map, 0.0, 1.0) + temperature_map = np.clip(temperature_map, 0.0, 1.0) + + return { + 'moisture': moisture_map, + 'temperature': temperature_map + } + + except Exception as e: + print(f"✗ 气候数据生成失败: {e}") + height, width = heightmap.shape + return { + 'moisture': np.random.rand(height, width).astype(np.float32), + 'temperature': np.random.rand(height, width).astype(np.float32) + } + + def _generate_biome_map(self, heightmap: np.ndarray, moisture_map: np.ndarray, + temperature_map: np.ndarray) -> np.ndarray: + """ + 生成生物群落图 + + Args: + heightmap: 高度图数据 + moisture_map: 湿度图数据 + temperature_map: 温度图数据 + + Returns: + 生物群落图 + """ + try: + height, width = heightmap.shape + biome_map = np.zeros((height, width), dtype=np.int32) + + # 为每个像素确定生物群落类型 + for y in range(height): + for x in range(width): + # 获取当前像素的属性 + height_value = heightmap[y, x] + moisture_value = moisture_map[y, x] + temperature_value = temperature_map[y, x] + + # 根据属性确定生物群落 + biome_type = self._determine_biome(height_value, moisture_value, temperature_value) + biome_map[y, x] = biome_type + + # 应用边缘混合以减少生物群落之间的硬边界 + biome_map = self._apply_edge_blending(biome_map, heightmap, moisture_map, temperature_map) + + return biome_map + + except Exception as e: + print(f"✗ 生物群落图生成失败: {e}") + return np.zeros_like(heightmap, dtype=np.int32) + + def _determine_biome(self, height: float, moisture: float, temperature: float) -> int: + """ + 根据属性确定生物群落类型 + + Args: + height: 高度值 (0-1) + moisture: 湿度值 (0-1) + temperature: 温度值 (0-1) + + Returns: + 生物群落类型ID + """ + try: + # 海洋 + if height < 0.2: + return 0 # ocean + + # 海滩 + if height < 0.25: + return 1 # beach + + # 根据湿度和温度组合确定生物群落 + if temperature > 0.7: # 热 + if moisture > 0.7: # 湿 + return 4 # jungle + elif moisture > 0.3: # 中等湿度 + return 11 # savanna + else: # 干燥 + return 5 # desert + elif temperature > 0.4: # 温暖 + if moisture > 0.7: # 湿 + if height > 0.7: # 高海拔 + return 3 # forest (山地森林) + else: + return 3 # forest + elif moisture > 0.4: # 中等湿度 + return 2 # plains + else: # 干燥 + return 2 # plains + else: # 冷 + if moisture > 0.6: # 湿 + if height > 0.8: # 高海拔 + return 7 # snow + else: + return 8 # taiga + elif moisture > 0.3: # 中等湿度 + if height > 0.6: # 高海拔 + return 7 # snow + else: + return 9 # tundra + else: # 干燥 + if height > 0.5: # 高海拔 + return 7 # snow + else: + return 9 # tundra + + except Exception as e: + print(f"✗ 生物群落确定失败: {e}") + return 2 # 默认为平原 + + def _apply_edge_blending(self, biome_map: np.ndarray, heightmap: np.ndarray, + moisture_map: np.ndarray, temperature_map: np.ndarray) -> np.ndarray: + """ + 应用边缘混合以减少生物群落之间的硬边界 + + Args: + biome_map: 原始生物群落图 + heightmap: 高度图数据 + moisture_map: 湿度图数据 + temperature_map: 温度图数据 + + Returns: + 边缘混合后的生物群落图 + """ + try: + height, width = biome_map.shape + blended_map = biome_map.copy() + + # 混合边缘宽度 + blend_width = max(1, int(min(height, width) * self.distribution_params['edge_blending'])) + + # 简化的边缘混合(只在生物群落边界应用) + for y in range(height): + for x in range(width): + current_biome = biome_map[y, x] + + # 检查周围像素是否属于不同生物群落 + different_neighbors = 0 + total_neighbors = 0 + + for dy in range(-blend_width, blend_width + 1): + for dx in range(-blend_width, blend_width + 1): + if dy == 0 and dx == 0: + continue + + ny, nx = y + dy, x + dx + if 0 <= ny < height and 0 <= nx < width: + total_neighbors += 1 + if biome_map[ny, nx] != current_biome: + different_neighbors += 1 + + # 如果有不同邻居,则有一定概率混合 + if total_neighbors > 0 and different_neighbors > 0: + blend_probability = different_neighbors / total_neighbors + if np.random.random() < blend_probability * 0.5: # 50%的混合概率 + # 随机选择一个邻居的生物群落 + neighbor_biomes = [] + for dy in range(-1, 2): + for dx in range(-1, 2): + if dy == 0 and dx == 0: + continue + ny, nx = y + dy, x + dx + if 0 <= ny < height and 0 <= nx < width: + neighbor_biomes.append(biome_map[ny, nx]) + + if neighbor_biomes: + blended_map[y, x] = np.random.choice(neighbor_biomes) + + return blended_map + + except Exception as e: + print(f"✗ 边缘混合应用失败: {e}") + return biome_map + + def _update_biome_distribution_stats(self, biome_map: np.ndarray): + """更新生物群落分布统计""" + try: + unique, counts = np.unique(biome_map, return_counts=True) + for biome_id, count in zip(unique, counts): + biome_name = self.biome_types.get(biome_id, {}).get('name', f'biome_{biome_id}') + self.stats['biome_distribution'][biome_name] = self.stats['biome_distribution'].get(biome_name, 0) + count + + except Exception as e: + print(f"✗ 生物群落分布统计更新失败: {e}") + + def set_seed(self, seed: int): + """ + 设置随机种子 + + Args: + seed: 随机种子 + """ + self.seed = seed + print(f"✓ 生物群落生成器随机种子设置为: {seed}") + + def set_distribution_parameters(self, params: Dict[str, float]): + """ + 设置分布参数 + + Args: + params: 分布参数字典 + """ + self.distribution_params.update(params) + print(f"✓ 生物群落分布参数已更新: {self.distribution_params}") + + def set_climate_parameters(self, params: Dict[str, float]): + """ + 设置气候参数 + + Args: + params: 气候参数字典 + """ + self.climate_params.update(params) + print(f"✓ 气候参数已更新: {self.climate_params}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + # 更新平均生成时间 + if self.stats['biomes_generated'] > 0: + self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['biomes_generated'] + return self.stats.copy() + + def get_biome_info(self, biome_id: int) -> Dict[str, Any]: + """ + 获取生物群落信息 + + Args: + biome_id: 生物群落ID + + Returns: + 生物群落信息字典 + """ + return self.biome_types.get(biome_id, { + 'name': f'unknown_{biome_id}', + 'color': (128, 128, 128), + 'humidity': 0.5, + 'temperature': 0.5 + }) + + def get_available_biomes(self) -> List[Dict[str, Any]]: + """ + 获取可用的生物群落列表 + + Returns: + 生物群落信息列表 + """ + return [self.get_biome_info(biome_id) for biome_id in sorted(self.biome_types.keys())] + + def generate_biome_with_zones(self, heightmap: np.ndarray, zones: Dict[str, Any], + seed: int = None) -> Dict[str, np.ndarray]: + """ + 根据区域定义生成生物群落 + + Args: + heightmap: 高度图数据 + zones: 区域定义字典 + seed: 随机种子 + + Returns: + 包含生物群落图、湿度图和温度图的字典 + """ + try: + import time + generation_start_time = time.time() + + if not self.enabled: + print("✗ 生物群落生成器未启用") + return { + 'biome_map': np.zeros_like(heightmap, dtype=np.int32), + 'moisture_map': np.zeros_like(heightmap, dtype=np.float32), + 'temperature_map': np.zeros_like(heightmap, dtype=np.float32) + } + + # 更新种子 + if seed is not None: + self.seed = seed + + print("✓ 开始生成区域化生物群落分布...") + + # 生成基础气候数据 + climate_data = self._generate_climate_data(heightmap) + moisture_map = climate_data['moisture'] + temperature_map = climate_data['temperature'] + + # 创建生物群落图 + height, width = heightmap.shape + biome_map = np.zeros((height, width), dtype=np.int32) + + # 首先生成基础生物群落 + print(" → 生成基础生物群落...") + for y in range(height): + for x in range(width): + height_value = heightmap[y, x] + moisture_value = moisture_map[y, x] + temperature_value = temperature_map[y, x] + biome_map[y, x] = self._determine_biome(height_value, moisture_value, temperature_value) + + # 然后应用区域定义 + print(" → 应用区域定义...") + for zone_name, zone_params in zones.items(): + zone_biome = zone_params.get('biome', 2) # 默认为平原 + zone_position = zone_params.get('position', (0, 0)) + zone_size = zone_params.get('size', (width, height)) + + x_start, y_start = zone_position + zone_width, zone_height = zone_size + + # 确保区域在地图范围内 + x_start = max(0, min(x_start, width - 1)) + y_start = max(0, min(y_start, height - 1)) + x_end = min(width, x_start + zone_width) + y_end = min(height, y_start + zone_height) + + # 在区域中应用指定的生物群落 + for y in range(y_start, y_end): + for x in range(x_start, x_end): + biome_map[y, x] = zone_biome + + # 应用边缘混合 + biome_map = self._apply_edge_blending(biome_map, heightmap, moisture_map, temperature_map) + + # 更新统计信息 + generation_time = time.time() - generation_start_time + self.stats['biomes_generated'] += 1 + self.stats['total_generation_time'] += generation_time + self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['biomes_generated'] + + # 更新生物群落分布统计 + self._update_biome_distribution_stats(biome_map) + + print(f"✓ 区域化生物群落生成完成,耗时: {generation_time:.2f}秒") + return { + 'biome_map': biome_map, + 'moisture_map': moisture_map, + 'temperature_map': temperature_map + } + + except Exception as e: + print(f"✗ 区域化生物群落生成失败: {e}") + import traceback + traceback.print_exc() + height, width = heightmap.shape + return { + 'biome_map': np.zeros((height, width), dtype=np.int32), + 'moisture_map': np.zeros((height, width), dtype=np.float32), + 'temperature_map': np.zeros((height, width), dtype=np.float32) + } + + def generate_biome_from_rules(self, heightmap: np.ndarray, rules: List[Dict[str, Any]], + seed: int = None) -> Dict[str, np.ndarray]: + """ + 根据规则生成生物群落 + + Args: + heightmap: 高度图数据 + rules: 生物群落生成规则列表 + seed: 随机种子 + + Returns: + 包含生物群落图、湿度图和温度图的字典 + """ + try: + import time + generation_start_time = time.time() + + if not self.enabled: + print("✗ 生物群落生成器未启用") + return { + 'biome_map': np.zeros_like(heightmap, dtype=np.int32), + 'moisture_map': np.zeros_like(heightmap, dtype=np.float32), + 'temperature_map': np.zeros_like(heightmap, dtype=np.float32) + } + + # 更新种子 + if seed is not None: + self.seed = seed + + print("✓ 开始基于规则生成生物群落...") + + # 生成基础气候数据 + climate_data = self._generate_climate_data(heightmap) + moisture_map = climate_data['moisture'] + temperature_map = climate_data['temperature'] + + # 创建生物群落图 + height, width = heightmap.shape + biome_map = np.zeros((height, width), dtype=np.int32) + + # 根据规则生成生物群落 + for y in range(height): + for x in range(width): + height_value = heightmap[y, x] + moisture_value = moisture_map[y, x] + temperature_value = temperature_map[y, x] + + # 应用规则 + biome_type = self._apply_biome_rules( + height_value, moisture_value, temperature_value, rules + ) + biome_map[y, x] = biome_type + + # 应用边缘混合 + biome_map = self._apply_edge_blending(biome_map, heightmap, moisture_map, temperature_map) + + # 更新统计信息 + generation_time = time.time() - generation_start_time + self.stats['biomes_generated'] += 1 + self.stats['total_generation_time'] += generation_time + self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['biomes_generated'] + + # 更新生物群落分布统计 + self._update_biome_distribution_stats(biome_map) + + print(f"✓ 基于规则的生物群落生成完成,耗时: {generation_time:.2f}秒") + return { + 'biome_map': biome_map, + 'moisture_map': moisture_map, + 'temperature_map': temperature_map + } + + except Exception as e: + print(f"✗ 基于规则的生物群落生成失败: {e}") + import traceback + traceback.print_exc() + height, width = heightmap.shape + return { + 'biome_map': np.zeros((height, width), dtype=np.int32), + 'moisture_map': np.zeros((height, width), dtype=np.float32), + 'temperature_map': np.zeros((height, width), dtype=np.float32) + } + + def _apply_biome_rules(self, height: float, moisture: float, temperature: float, + rules: List[Dict[str, Any]]) -> int: + """ + 根据规则确定生物群落类型 + + Args: + height: 高度值 + moisture: 湿度值 + temperature: 温度值 + rules: 规则列表 + + Returns: + 生物群落类型ID + """ + try: + # 遍历规则 + for rule in rules: + # 检查条件是否满足 + conditions = rule.get('conditions', {}) + match = True + + # 检查高度条件 + if 'height' in conditions: + height_range = conditions['height'] + if not (height_range[0] <= height <= height_range[1]): + match = False + + # 检查湿度条件 + if 'moisture' in conditions: + moisture_range = conditions['moisture'] + if not (moisture_range[0] <= moisture <= moisture_range[1]): + match = False + + # 检查温度条件 + if 'temperature' in conditions: + temperature_range = conditions['temperature'] + if not (temperature_range[0] <= temperature <= temperature_range[1]): + match = False + + # 如果所有条件都满足,应用该规则 + if match: + return rule.get('biome', 2) # 默认为平原 + + # 如果没有规则匹配,使用默认方法 + return self._determine_biome(height, moisture, temperature) + + except Exception as e: + print(f"✗ 生物群落规则应用失败: {e}") + return self._determine_biome(height, moisture, temperature) + + def generate_biome_with_influence_maps(self, heightmap: np.ndarray, + influence_maps: Dict[str, np.ndarray], + seed: int = None) -> Dict[str, np.ndarray]: + """ + 使用影响力图生成生物群落 + + Args: + heightmap: 高度图数据 + influence_maps: 影响力图字典(如'forest_influence', 'desert_influence'等) + seed: 随机种子 + + Returns: + 包含生物群落图、湿度图和温度图的字典 + """ + try: + import time + generation_start_time = time.time() + + if not self.enabled: + print("✗ 生物群落生成器未启用") + return { + 'biome_map': np.zeros_like(heightmap, dtype=np.int32), + 'moisture_map': np.zeros_like(heightmap, dtype=np.float32), + 'temperature_map': np.zeros_like(heightmap, dtype=np.float32) + } + + # 更新种子 + if seed is not None: + self.seed = seed + + print("✓ 开始使用影响力图生成生物群落...") + + # 生成基础气候数据 + climate_data = self._generate_climate_data(heightmap) + moisture_map = climate_data['moisture'] + temperature_map = climate_data['temperature'] + + # 创建生物群落图 + height, width = heightmap.shape + biome_map = np.zeros((height, width), dtype=np.int32) + + # 为每个像素根据影响力图确定生物群落 + for y in range(height): + for x in range(width): + height_value = heightmap[y, x] + moisture_value = moisture_map[y, x] + temperature_value = temperature_map[y, x] + + # 获取影响力值 + influences = {} + for biome_name, influence_map in influence_maps.items(): + if 0 <= y < influence_map.shape[0] and 0 <= x < influence_map.shape[1]: + influences[biome_name] = influence_map[y, x] + else: + influences[biome_name] = 0.0 + + # 根据影响力确定生物群落 + biome_type = self._determine_biome_by_influence( + height_value, moisture_value, temperature_value, influences + ) + biome_map[y, x] = biome_type + + # 应用边缘混合 + biome_map = self._apply_edge_blending(biome_map, heightmap, moisture_map, temperature_map) + + # 更新统计信息 + generation_time = time.time() - generation_start_time + self.stats['biomes_generated'] += 1 + self.stats['total_generation_time'] += generation_time + self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['biomes_generated'] + + # 更新生物群落分布统计 + self._update_biome_distribution_stats(biome_map) + + print(f"✓ 基于影响力图的生物群落生成完成,耗时: {generation_time:.2f}秒") + return { + 'biome_map': biome_map, + 'moisture_map': moisture_map, + 'temperature_map': temperature_map + } + + except Exception as e: + print(f"✗ 基于影响力图的生物群落生成失败: {e}") + import traceback + traceback.print_exc() + height, width = heightmap.shape + return { + 'biome_map': np.zeros((height, width), dtype=np.int32), + 'moisture_map': np.zeros((height, width), dtype=np.float32), + 'temperature_map': np.zeros((height, width), dtype=np.float32) + } + + def _determine_biome_by_influence(self, height: float, moisture: float, temperature: float, + influences: Dict[str, float]) -> int: + """ + 根据影响力确定生物群落类型 + + Args: + height: 高度值 + moisture: 湿度值 + temperature: 温度值 + influences: 各生物群落的影响力 + + Returns: + 生物群落类型ID + """ + try: + # 如果有任何影响力大于阈值,选择影响力最大的生物群落 + max_influence = 0.0 + selected_biome = None + + for biome_name, influence in influences.items(): + if influence > max_influence and influence > 0.3: # 阈值 + max_influence = influence + # 将生物群落名称映射到ID + for biome_id, biome_info in self.biome_types.items(): + if biome_info['name'] == biome_name: + selected_biome = biome_id + break + + # 如果有选定的生物群落,返回它 + if selected_biome is not None: + return selected_biome + + # 否则使用默认方法 + return self._determine_biome(height, moisture, temperature) + + except Exception as e: + print(f"✗ 基于影响力的生物群落确定失败: {e}") + return self._determine_biome(height, moisture, temperature) + + def generate_biome_with_erosion_influence(self, heightmap: np.ndarray, + erosion_map: np.ndarray, + sediment_map: np.ndarray, + seed: int = None) -> Dict[str, np.ndarray]: + """ + 考虑侵蚀和沉积影响生成生物群落 + + Args: + heightmap: 高度图数据 + erosion_map: 侵蚀图数据 + sediment_map: 沉积图数据 + seed: 随机种子 + + Returns: + 包含生物群落图、湿度图和温度图的字典 + """ + try: + import time + generation_start_time = time.time() + + if not self.enabled: + print("✗ 生物群落生成器未启用") + return { + 'biome_map': np.zeros_like(heightmap, dtype=np.int32), + 'moisture_map': np.zeros_like(heightmap, dtype=np.float32), + 'temperature_map': np.zeros_like(heightmap, dtype=np.float32) + } + + # 更新种子 + if seed is not None: + self.seed = seed + + print("✓ 开始生成受侵蚀影响的生物群落...") + + # 生成基础气候数据 + climate_data = self._generate_climate_data(heightmap) + moisture_map = climate_data['moisture'] + temperature_map = climate_data['temperature'] + + # 创建生物群落图 + height, width = heightmap.shape + biome_map = np.zeros((height, width), dtype=np.int32) + + # 考虑侵蚀和沉积影响生成生物群落 + for y in range(height): + for x in range(width): + height_value = heightmap[y, x] + moisture_value = moisture_map[y, x] + temperature_value = temperature_map[y, x] + erosion_value = erosion_map[y, x] if y < erosion_map.shape[0] and x < erosion_map.shape[1] else 0 + sediment_value = sediment_map[y, x] if y < sediment_map.shape[0] and x < sediment_map.shape[1] else 0 + + # 调整湿度和温度以考虑侵蚀影响 + adjusted_moisture = moisture_value + sediment_value * 0.2 - erosion_value * 0.1 + adjusted_temperature = temperature_value - erosion_value * 0.1 + + # 确保调整后的值在有效范围内 + adjusted_moisture = max(0.0, min(1.0, adjusted_moisture)) + adjusted_temperature = max(0.0, min(1.0, adjusted_temperature)) + + # 确定生物群落 + biome_map[y, x] = self._determine_biome(height_value, adjusted_moisture, adjusted_temperature) + + # 应用边缘混合 + biome_map = self._apply_edge_blending(biome_map, heightmap, moisture_map, temperature_map) + + # 更新统计信息 + generation_time = time.time() - generation_start_time + self.stats['biomes_generated'] += 1 + self.stats['total_generation_time'] += generation_time + self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['biomes_generated'] + + # 更新生物群落分布统计 + self._update_biome_distribution_stats(biome_map) + + print(f"✓ 受侵蚀影响的生物群落生成完成,耗时: {generation_time:.2f}秒") + return { + 'biome_map': biome_map, + 'moisture_map': moisture_map, + 'temperature_map': temperature_map + } + + except Exception as e: + print(f"✗ 受侵蚀影响的生物群落生成失败: {e}") + import traceback + traceback.print_exc() + height, width = heightmap.shape + return { + 'biome_map': np.zeros((height, width), dtype=np.int32), + 'moisture_map': np.zeros((height, width), dtype=np.float32), + 'temperature_map': np.zeros((height, width), dtype=np.float32) + } + + def blend_biome_maps(self, biome_maps: List[np.ndarray], weights: List[float] = None) -> np.ndarray: + """ + 混合多个生物群落图 + + Args: + biome_maps: 生物群落图列表 + weights: 权重列表(如果为None,则平均分配权重) + + Returns: + 混合后的生物群落图 + """ + try: + if not biome_maps: + return np.array([]) + + if len(biome_maps) == 1: + return biome_maps[0].copy() + + # 确保所有生物群落图尺寸相同 + base_shape = biome_maps[0].shape + for bm in biome_maps: + if bm.shape != base_shape: + raise ValueError("所有生物群落图必须具有相同的尺寸") + + # 处理权重 + if weights is None: + weights = [1.0 / len(biome_maps)] * len(biome_maps) + elif len(weights) != len(biome_maps): + raise ValueError("权重数量必须与生物群落图数量相同") + + # 归一化权重 + total_weight = sum(weights) + if total_weight > 0: + weights = [w / total_weight for w in weights] + + # 混合生物群落图 + height, width = base_shape + blended = np.zeros(base_shape, dtype=np.int32) + + # 为每个像素计算加权生物群落类型 + for y in range(height): + for x in range(width): + # 计算每个生物群落类型的加权投票 + biome_votes = {} + for i, (bm, weight) in enumerate(zip(biome_maps, weights)): + biome_type = bm[y, x] + biome_votes[biome_type] = biome_votes.get(biome_type, 0.0) + weight + + # 选择得票最多的生物群落类型 + blended[y, x] = max(biome_votes, key=biome_votes.get) + + return blended + + except Exception as e: + print(f"✗ 生物群落图混合失败: {e}") + import traceback + traceback.print_exc() + # 返回第一个生物群落图的副本 + return biome_maps[0].copy() if biome_maps else np.array([]) + + def smooth_biome_map(self, biome_map: np.ndarray, iterations: int = 1) -> np.ndarray: + """ + 平滑生物群落图 + + Args: + biome_map: 原始生物群落图 + iterations: 平滑迭代次数 + + Returns: + 平滑后的生物群落图 + """ + try: + smoothed = biome_map.copy() + height, width = smoothed.shape + + for _ in range(iterations): + new_map = smoothed.copy() + + # 对每个像素进行平滑处理 + for y in range(height): + for x in range(width): + # 统计邻居的生物群落类型 + neighbor_types = [] + + # 检查8个邻居 + for dy in [-1, 0, 1]: + for dx in [-1, 0, 1]: + if dy == 0 and dx == 0: + continue + + ny, nx = y + dy, x + dx + if 0 <= ny < height and 0 <= nx < width: + neighbor_types.append(smoothed[ny, nx]) + + # 如果大多数邻居是相同类型,则改变当前像素 + if neighbor_types: + unique, counts = np.unique(neighbor_types, return_counts=True) + most_common_type = unique[np.argmax(counts)] + most_common_count = np.max(counts) + + # 如果超过一半的邻居是相同类型,则采用该类型 + if most_common_count > len(neighbor_types) / 2: + new_map[y, x] = most_common_type + + smoothed = new_map + + return smoothed + + except Exception as e: + print(f"✗ 生物群落图平滑失败: {e}") + return biome_map + + def generate_biome_colors(self, biome_map: np.ndarray) -> np.ndarray: + """ + 为生物群落图生成颜色表示 + + Args: + biome_map: 生物群落图 + + Returns: + 颜色图 (height, width, 3) + """ + try: + height, width = biome_map.shape + color_map = np.zeros((height, width, 3), dtype=np.uint8) + + # 为每个生物群落类型分配颜色 + for y in range(height): + for x in range(width): + biome_id = biome_map[y, x] + biome_info = self.biome_types.get(biome_id, {'color': (128, 128, 128)}) + color_map[y, x] = biome_info['color'] + + return color_map + + except Exception as e: + print(f"✗ 生物群落颜色图生成失败: {e}") + height, width = biome_map.shape + return np.zeros((height, width, 3), dtype=np.uint8) + + def export_biome_legend(self, filename: str) -> bool: + """ + 导出生物群落图例 + + Args: + filename: 文件名 + + Returns: + 是否导出成功 + """ + try: + import json + + legend_data = {} + for biome_id, biome_info in self.biome_types.items(): + legend_data[biome_id] = { + 'name': biome_info['name'], + 'color': biome_info['color'], + 'humidity': biome_info['humidity'], + 'temperature': biome_info['temperature'] + } + + with open(filename, 'w', encoding='utf-8') as f: + json.dump(legend_data, f, ensure_ascii=False, indent=2) + + print(f"✓ 生物群落图例已导出到: {filename}") + return True + + except Exception as e: + print(f"✗ 生物群落图例导出失败: {e}") + return False + + def import_biome_legend(self, filename: str) -> bool: + """ + 导入生物群落图例 + + Args: + filename: 文件名 + + Returns: + 是否导入成功 + """ + try: + import json + + with open(filename, 'r', encoding='utf-8') as f: + legend_data = json.load(f) + + # 更新生物群落类型 + self.biome_types = {} + for biome_id, biome_info in legend_data.items(): + self.biome_types[int(biome_id)] = { + 'name': biome_info['name'], + 'color': tuple(biome_info['color']), + 'humidity': biome_info['humidity'], + 'temperature': biome_info['temperature'] + } + + print(f"✓ 生物群落图例已从 {filename} 导入") + return True + + except Exception as e: + print(f"✗ 生物群落图例导入失败: {e}") + return False + + def get_biome_distribution_stats(self) -> Dict[str, int]: + """ + 获取生物群落分布统计 + + Returns: + 生物群落分布统计字典 + """ + return self.stats['biome_distribution'].copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'biomes_generated': 0, + 'total_generation_time': 0.0, + 'average_generation_time': 0.0, + 'biome_distribution': {} + } + print("✓ 生物群落生成器统计信息已重置") + + def set_biome_types(self, biome_types: Dict[int, Dict[str, Any]]): + """ + 设置生物群落类型 + + Args: + biome_types: 生物群落类型字典 + """ + self.biome_types = biome_types + print(f"✓ 生物群落类型已更新,共 {len(biome_types)} 种生物群落") + + def add_biome_type(self, biome_id: int, biome_info: Dict[str, Any]): + """ + 添加生物群落类型 + + Args: + biome_id: 生物群落ID + biome_info: 生物群落信息 + """ + self.biome_types[biome_id] = biome_info + print(f"✓ 生物群落类型 {biome_info['name']} (ID: {biome_id}) 已添加") + + def remove_biome_type(self, biome_id: int): + """ + 移除生物群落类型 + + Args: + biome_id: 生物群落ID + """ + if biome_id in self.biome_types: + biome_name = self.biome_types[biome_id]['name'] + del self.biome_types[biome_id] + print(f"✓ 生物群落类型 {biome_name} (ID: {biome_id}) 已移除") + else: + print(f"✗ 无效的生物群落ID: {biome_id}") + + def modify_biome_type(self, biome_id: int, modifications: Dict[str, Any]): + """ + 修改生物群落类型 + + Args: + biome_id: 生物群落ID + modifications: 修改内容 + """ + if biome_id in self.biome_types: + self.biome_types[biome_id].update(modifications) + print(f"✓ 生物群落类型 (ID: {biome_id}) 已修改") + else: print(f"✗ 无效的生物群落ID: {biome_id}") \ No newline at end of file diff --git a/plugins/user/procedural_terrain_generation/core/terrain_manager.py b/plugins/user/procedural_terrain_generation/core/terrain_manager.py index 48e8bbfc..47cf2622 100644 --- a/plugins/user/procedural_terrain_generation/core/terrain_manager.py +++ b/plugins/user/procedural_terrain_generation/core/terrain_manager.py @@ -1,866 +1,866 @@ -""" -地形管理器 -负责管理地形的核心功能 -""" - -import numpy as np -from typing import Dict, Any, List, Tuple -import time - -class TerrainManager: - """ - 地形管理器 - 负责管理地形的核心功能,包括地形数据管理、生成控制等 - """ - - def __init__(self, plugin): - """ - 初始化地形管理器 - - Args: - plugin: 程序化地形生成插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 地形配置 - self.terrain_size = plugin.config.get('terrain_size', 512) - self.terrain_scale = plugin.config.get('terrain_scale', 1.0) - self.height_scale = plugin.config.get('height_scale', 100.0) - self.seed = plugin.config.get('seed', 12345) - - # 地形数据 - self.heightmap = None - self.normalmap = None - self.biome_map = None - self.moisture_map = None - self.temperature_map = None - self.erosion_map = None - self.sediment_map = None - self.water_map = None - self.vegetation_map = None - - # 地形统计信息 - self.stats = { - 'generation_time': 0.0, - 'triangles_generated': 0, - 'vertices_generated': 0, - 'chunks_generated': 0, - 'lod_levels': plugin.config.get('lod_levels', 5) - } - - # 地形生成状态 - self.is_generating = False - self.generation_progress = 0.0 - self.last_generation_time = 0.0 - - # 地形分块管理 - self.chunk_size = plugin.config.get('chunk_size', 32) - self.chunks = {} - self.visible_chunks = set() - - print("✓ 地形管理器已创建") - - def initialize(self) -> bool: - """ - 初始化地形管理器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化地形数据结构 - self._initialize_terrain_data() - - self.initialized = True - print("✓ 地形管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 地形管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用地形管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 地形管理器未初始化") - return False - - self.enabled = True - print("✓ 地形管理器已启用") - return True - - except Exception as e: - print(f"✗ 地形管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用地形管理器""" - try: - self.enabled = False - print("✓ 地形管理器已禁用") - - except Exception as e: - print(f"✗ 地形管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理地形管理器资源""" - try: - self.disable() - self.initialized = False - print("✓ 地形管理器资源已清理") - - except Exception as e: - print(f"✗ 地形管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新地形管理器状态 - - Args: - dt: 时间增量 - """ - # 处理地形更新逻辑 - pass - - def _initialize_terrain_data(self): - """初始化地形数据结构""" - try: - size = self.terrain_size - # 初始化高度图 - self.heightmap = np.zeros((size, size), dtype=np.float32) - - # 初始化法线图 - self.normalmap = np.zeros((size, size, 3), dtype=np.float32) - - # 初始化生物群落图 - self.biome_map = np.zeros((size, size), dtype=np.int32) - - # 初始化湿度图 - self.moisture_map = np.zeros((size, size), dtype=np.float32) - - # 初始化温度图 - self.temperature_map = np.zeros((size, size), dtype=np.float32) - - # 初始化侵蚀图 - self.erosion_map = np.zeros((size, size), dtype=np.float32) - - # 初始化沉积物图 - self.sediment_map = np.zeros((size, size), dtype=np.float32) - - # 初始化水图 - self.water_map = np.zeros((size, size), dtype=np.float32) - - # 初始化植被图 - self.vegetation_map = np.zeros((size, size), dtype=np.int32) - - print("✓ 地形数据结构初始化完成") - - except Exception as e: - print(f"✗ 地形数据结构初始化失败: {e}") - import traceback - traceback.print_exc() - - def generate_terrain(self, width: int = None, height: int = None, seed: int = None) -> bool: - """ - 生成地形 - - Args: - width: 地形宽度 - height: 地形高度 - seed: 随机种子 - - Returns: - 是否生成成功 - """ - try: - if not self.enabled: - print("✗ 地形管理器未启用") - return False - - if self.is_generating: - print("⚠ 地形生成已在进行中") - return True - - # 更新参数 - if width is not None: - self.terrain_size = width - - if seed is not None: - self.seed = seed - if self.plugin.noise_generator: - self.plugin.noise_generator.set_seed(seed) - if self.plugin.heightmap_generator: - self.plugin.heightmap_generator.set_seed(seed) - - # 重新初始化地形数据(如果尺寸改变) - if width is not None or height is not None: - self._initialize_terrain_data() - - generation_start_time = time.time() - self.is_generating = True - self.generation_progress = 0.0 - - print("✓ 开始生成地形...") - - # 1. 生成高度图 - print(" → 生成高度图...") - if self.plugin.heightmap_generator: - self.heightmap = self.plugin.heightmap_generator.generate_heightmap( - self.terrain_size, self.terrain_size, self.seed - ) - self.generation_progress = 20.0 - - # 2. 计算法线 - print(" → 计算法线...") - self.normalmap = self._calculate_normals(self.heightmap) - self.generation_progress = 30.0 - - # 3. 生成生物群落 - print(" → 生成生物群落...") - if self.plugin.biome_generator: - biome_data = self.plugin.biome_generator.generate_biomes( - self.heightmap, self.seed - ) - self.biome_map = biome_data.get('biome_map', self.biome_map) - self.moisture_map = biome_data.get('moisture_map', self.moisture_map) - self.temperature_map = biome_data.get('temperature_map', self.temperature_map) - self.generation_progress = 50.0 - - # 4. 应用侵蚀 - print(" → 应用侵蚀模拟...") - if self.plugin.erosion_processor: - erosion_result = self.plugin.erosion_processor.apply_erosion( - self.heightmap, self.seed - ) - self.heightmap = erosion_result.get('heightmap', self.heightmap) - self.erosion_map = erosion_result.get('erosion_map', self.erosion_map) - self.sediment_map = erosion_result.get('sediment_map', self.sediment_map) - self.water_map = erosion_result.get('water_map', self.water_map) - self.generation_progress = 70.0 - - # 5. 生成植被 - print(" → 生成植被...") - if self.plugin.vegetation_generator: - self.vegetation_map = self.plugin.vegetation_generator.generate_vegetation( - self.heightmap, self.biome_map, self.seed - ) - self.generation_progress = 90.0 - - # 6. 更新统计信息 - self.stats['generation_time'] = time.time() - generation_start_time - self.stats['triangles_generated'] = self.terrain_size * self.terrain_size * 2 - self.stats['vertices_generated'] = self.terrain_size * self.terrain_size - self.stats['chunks_generated'] = (self.terrain_size // self.chunk_size) ** 2 - - self.is_generating = False - self.generation_progress = 100.0 - self.last_generation_time = time.time() - - print(f"✓ 地形生成完成,耗时: {self.stats['generation_time']:.2f}秒") - return True - - except Exception as e: - print(f"✗ 地形生成失败: {e}") - import traceback - traceback.print_exc() - self.is_generating = False - return False - - def _calculate_normals(self, heightmap: np.ndarray) -> np.ndarray: - """ - 根据高度图计算法线图 - - Args: - heightmap: 高度图数据 - - Returns: - 法线图数据 - """ - try: - height, width = heightmap.shape - normals = np.zeros((height, width, 3), dtype=np.float32) - - # 计算每个点的法线 - for y in range(height): - for x in range(width): - # 计算相邻点的高度 - h_left = heightmap[y, max(0, x-1)] - h_right = heightmap[y, min(width-1, x+1)] - h_up = heightmap[max(0, y-1), x] - h_down = heightmap[min(height-1, y+1), x] - - # 计算梯度 - dx = (h_right - h_left) * self.height_scale - dy = (h_down - h_up) * self.height_scale - dz = 2.0 * self.terrain_scale - - # 计算法线 - normal = np.array([-dx, -dy, dz], dtype=np.float32) - norm = np.linalg.norm(normal) - if norm > 0: - normal = normal / norm - - normals[y, x] = normal - - return normals - - except Exception as e: - print(f"✗ 法线计算失败: {e}") - return np.zeros((heightmap.shape[0], heightmap.shape[1], 3), dtype=np.float32) - - def get_terrain_data(self) -> Dict[str, Any]: - """ - 获取地形数据 - - Returns: - 地形数据字典 - """ - return { - 'heightmap': self.heightmap, - 'normalmap': self.normalmap, - 'biome_map': self.biome_map, - 'moisture_map': self.moisture_map, - 'temperature_map': self.temperature_map, - 'erosion_map': self.erosion_map, - 'sediment_map': self.sediment_map, - 'water_map': self.water_map, - 'vegetation_map': self.vegetation_map, - 'stats': self.stats.copy(), - 'is_generating': self.is_generating, - 'generation_progress': self.generation_progress - } - - def get_height_at(self, x: int, y: int) -> float: - """ - 获取指定位置的高度值 - - Args: - x: X坐标 - y: Y坐标 - - Returns: - 高度值 - """ - try: - if self.heightmap is not None and 0 <= x < self.heightmap.shape[1] and 0 <= y < self.heightmap.shape[0]: - return self.heightmap[y, x] - return 0.0 - except Exception as e: - print(f"✗ 获取高度值失败: {e}") - return 0.0 - - def get_normal_at(self, x: int, y: int) -> np.ndarray: - """ - 获取指定位置的法线向量 - - Args: - x: X坐标 - y: Y坐标 - - Returns: - 法线向量 - """ - try: - if self.normalmap is not None and 0 <= x < self.normalmap.shape[1] and 0 <= y < self.normalmap.shape[0]: - return self.normalmap[y, x].copy() - return np.array([0.0, 0.0, 1.0], dtype=np.float32) - except Exception as e: - print(f"✗ 获取法线向量失败: {e}") - return np.array([0.0, 0.0, 1.0], dtype=np.float32) - - def get_biome_at(self, x: int, y: int) -> int: - """ - 获取指定位置的生物群落类型 - - Args: - x: X坐标 - y: Y坐标 - - Returns: - 生物群落类型 - """ - try: - if self.biome_map is not None and 0 <= x < self.biome_map.shape[1] and 0 <= y < self.biome_map.shape[0]: - return self.biome_map[y, x] - return 0 - except Exception as e: - print(f"✗ 获取生物群落类型失败: {e}") - return 0 - - def set_terrain_scale(self, scale: float): - """ - 设置地形缩放 - - Args: - scale: 缩放因子 - """ - self.terrain_scale = scale - print(f"✓ 地形缩放设置为: {scale}") - - def set_height_scale(self, scale: float): - """ - 设置高度缩放 - - Args: - scale: 高度缩放因子 - """ - self.height_scale = scale - print(f"✓ 高度缩放设置为: {scale}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def is_terrain_generated(self) -> bool: - """ - 检查地形是否已生成 - - Returns: - 是否已生成地形 - """ - return self.heightmap is not None and self.heightmap.size > 0 - - def clear_terrain(self): - """清空地形数据""" - try: - self.heightmap = None - self.normalmap = None - self.biome_map = None - self.moisture_map = None - self.temperature_map = None - self.erosion_map = None - self.sediment_map = None - self.water_map = None - self.vegetation_map = None - - # 重新初始化数据结构 - self._initialize_terrain_data() - - print("✓ 地形数据已清空") - - except Exception as e: - print(f"✗ 清空地形数据失败: {e}") - import traceback - traceback.print_exc() - - def resize_terrain(self, new_size: int): - """ - 调整地形大小 - - Args: - new_size: 新的地形大小 - """ - try: - old_size = self.terrain_size - self.terrain_size = new_size - - # 重新初始化地形数据 - self._initialize_terrain_data() - - print(f"✓ 地形大小已从 {old_size} 调整为 {new_size}") - - except Exception as e: - print(f"✗ 调整地形大小失败: {e}") - import traceback - traceback.print_exc() - - def get_generation_progress(self) -> float: - """ - 获取生成进度 - - Returns: - 生成进度百分比 (0.0-100.0) - """ - return self.generation_progress - - def is_generating(self) -> bool: - """ - 检查是否正在生成地形 - - Returns: - 是否正在生成 - """ - return self.is_generating - - def get_chunk_info(self, chunk_x: int, chunk_y: int) -> Dict[str, Any]: - """ - 获取指定区块的信息 - - Args: - chunk_x: 区块X坐标 - chunk_y: 区块Y坐标 - - Returns: - 区块信息 - """ - chunk_key = f"{chunk_x},{chunk_y}" - if chunk_key in self.chunks: - return self.chunks[chunk_key].copy() - else: - return { - 'x': chunk_x, - 'y': chunk_y, - 'generated': False, - 'visible': False, - 'lod_level': 0 - } - - def set_chunk_visibility(self, chunk_x: int, chunk_y: int, visible: bool): - """ - 设置区块可见性 - - Args: - chunk_x: 区块X坐标 - chunk_y: 区块Y坐标 - visible: 是否可见 - """ - chunk_key = f"{chunk_x},{chunk_y}" - if chunk_key in self.chunks: - self.chunks[chunk_key]['visible'] = visible - if visible: - self.visible_chunks.add(chunk_key) - elif chunk_key in self.visible_chunks: - self.visible_chunks.remove(chunk_key) - - def get_visible_chunks(self) -> List[str]: - """ - 获取可见区块列表 - - Returns: - 可见区块键列表 - """ - return list(self.visible_chunks) - - def update_chunk_lod(self, chunk_x: int, chunk_y: int, lod_level: int): - """ - 更新区块LOD级别 - - Args: - chunk_x: 区块X坐标 - chunk_y: 区块Y坐标 - lod_level: LOD级别 - """ - chunk_key = f"{chunk_x},{chunk_y}" - if chunk_key in self.chunks: - self.chunks[chunk_key]['lod_level'] = max(0, min(lod_level, self.stats['lod_levels'] - 1)) - - def get_terrain_bounds(self) -> Tuple[int, int]: - """ - 获取地形边界 - - Returns: - (宽度, 高度) - """ - if self.heightmap is not None: - return self.heightmap.shape[1], self.heightmap.shape[0] - return self.terrain_size, self.terrain_size - - def get_terrain_center(self) -> Tuple[float, float]: - """ - 获取地形中心点 - - Returns: - (中心X坐标, 中心Y坐标) - """ - width, height = self.get_terrain_bounds() - return width / 2.0, height / 2.0 - - def get_terrain_area(self) -> float: - """ - 获取地形面积 - - Returns: - 地形面积 - """ - width, height = self.get_terrain_bounds() - return width * height * self.terrain_scale * self.terrain_scale - - def get_height_range(self) -> Tuple[float, float]: - """ - 获取高度范围 - - Returns: - (最小高度, 最大高度) - """ - if self.heightmap is not None and self.heightmap.size > 0: - return float(np.min(self.heightmap)), float(np.max(self.heightmap)) - return 0.0, 0.0 - - def get_average_height(self) -> float: - """ - 获取平均高度 - - Returns: - 平均高度 - """ - if self.heightmap is not None and self.heightmap.size > 0: - return float(np.mean(self.heightmap)) - return 0.0 - - def get_terrain_gradient(self, x: int, y: int) -> float: - """ - 获取指定位置的地形坡度 - - Args: - x: X坐标 - y: Y坐标 - - Returns: - 坡度值 (弧度) - """ - try: - if self.normalmap is not None and 0 <= x < self.normalmap.shape[1] and 0 <= y < self.normalmap.shape[0]: - normal = self.normalmap[y, x] - # 计算与垂直方向的夹角 - vertical = np.array([0.0, 0.0, 1.0]) - dot_product = np.dot(normal, vertical) - angle = np.arccos(np.clip(dot_product, -1.0, 1.0)) - return float(angle) - return 0.0 - except Exception as e: - print(f"✗ 计算地形坡度失败: {e}") - return 0.0 - - def get_terrain_aspect(self, x: int, y: int) -> float: - """ - 获取指定位置的地形坡向 - - Args: - x: X坐标 - y: Y坐标 - - Returns: - 坡向值 (弧度,0为北向) - """ - try: - if self.normalmap is not None and 0 <= x < self.normalmap.shape[1] and 0 <= y < self.normalmap.shape[0]: - normal = self.normalmap[y, x] - # 计算在水平面上的投影 - aspect = np.arctan2(normal[1], normal[0]) - # 转换为0-2π范围 - if aspect < 0: - aspect += 2 * np.pi - return float(aspect) - return 0.0 - except Exception as e: - print(f"✗ 计算地形坡向失败: {e}") - return 0.0 - - def get_terrain_curvature(self, x: int, y: int) -> float: - """ - 获取指定位置的地形曲率 - - Args: - x: X坐标 - y: Y坐标 - - Returns: - 曲率值 - """ - try: - if self.heightmap is None: - return 0.0 - - height, width = self.heightmap.shape - if not (1 <= x < width - 1 and 1 <= y < height - 1): - return 0.0 - - # 使用拉普拉斯算子计算曲率 - center = self.heightmap[y, x] - laplacian = ( - self.heightmap[y-1, x] + self.heightmap[y+1, x] + - self.heightmap[y, x-1] + self.heightmap[y, x+1] - 4 * center - ) - - return float(laplacian) - except Exception as e: - print(f"✗ 计算地形曲率失败: {e}") - return 0.0 - - def get_terrain_roughness(self, x: int, y: int, radius: int = 3) -> float: - """ - 获取指定位置的地形粗糙度 - - Args: - x: X坐标 - y: Y坐标 - radius: 计算半径 - - Returns: - 粗糙度值 - """ - try: - if self.heightmap is None: - return 0.0 - - height, width = self.heightmap.shape - min_x = max(0, x - radius) - max_x = min(width - 1, x + radius) - min_y = max(0, y - radius) - max_y = min(height - 1, y + radius) - - if min_x >= max_x or min_y >= max_y: - return 0.0 - - # 计算局部高度变化 - local_heights = self.heightmap[min_y:max_y+1, min_x:max_x+1] - roughness = float(np.std(local_heights)) - - return roughness - except Exception as e: - print(f"✗ 计算地形粗糙度失败: {e}") - return 0.0 - - def get_water_level(self, x: int, y: int) -> float: - """ - 获取指定位置的水位 - - Args: - x: X坐标 - y: Y坐标 - - Returns: - 水位值 - """ - try: - if self.water_map is not None and 0 <= x < self.water_map.shape[1] and 0 <= y < self.water_map.shape[0]: - return self.water_map[y, x] - return 0.0 - except Exception as e: - print(f"✗ 获取水位值失败: {e}") - return 0.0 - - def is_under_water(self, x: int, y: int) -> bool: - """ - 检查指定位置是否在水下 - - Args: - x: X坐标 - y: Y坐标 - - Returns: - 是否在水下 - """ - try: - if self.heightmap is not None and self.water_map is not None: - if 0 <= x < self.heightmap.shape[1] and 0 <= y < self.heightmap.shape[0]: - return self.water_map[y, x] > self.heightmap[y, x] - return False - except Exception as e: - print(f"✗ 检查水下状态失败: {e}") - return False - - def get_vegetation_density(self, x: int, y: int) -> float: - """ - 获取指定位置的植被密度 - - Args: - x: X坐标 - y: Y坐标 - - Returns: - 植被密度 (0.0-1.0) - """ - try: - if self.vegetation_map is not None and 0 <= x < self.vegetation_map.shape[1] and 0 <= y < self.vegetation_map.shape[0]: - # 简化处理,将植被类型转换为密度值 - vegetation_type = self.vegetation_map[y, x] - return min(1.0, vegetation_type / 10.0) # 假设最多10种植被类型 - return 0.0 - except Exception as e: - print(f"✗ 获取植被密度失败: {e}") - return 0.0 - - def get_biome_properties(self, x: int, y: int) -> Dict[str, Any]: - """ - 获取指定位置的生物群落属性 - - Args: - x: X坐标 - y: Y坐标 - - Returns: - 生物群落属性字典 - """ - try: - biome_type = self.get_biome_at(x, y) - moisture = 0.0 - temperature = 0.0 - - if self.moisture_map is not None and 0 <= x < self.moisture_map.shape[1] and 0 <= y < self.moisture_map.shape[0]: - moisture = self.moisture_map[y, x] - - if self.temperature_map is not None and 0 <= x < self.temperature_map.shape[1] and 0 <= y < self.temperature_map.shape[0]: - temperature = self.temperature_map[y, x] - - return { - 'biome_type': biome_type, - 'moisture': moisture, - 'temperature': temperature - } - except Exception as e: - print(f"✗ 获取生物群落属性失败: {e}") - return { - 'biome_type': 0, - 'moisture': 0.0, - 'temperature': 0.0 - } - - def get_erosion_level(self, x: int, y: int) -> float: - """ - 获取指定位置的侵蚀程度 - - Args: - x: X坐标 - y: Y坐标 - - Returns: - 侵蚀程度 (0.0-1.0) - """ - try: - if self.erosion_map is not None and 0 <= x < self.erosion_map.shape[1] and 0 <= y < self.erosion_map.shape[0]: - return self.erosion_map[y, x] - return 0.0 - except Exception as e: - print(f"✗ 获取侵蚀程度失败: {e}") - return 0.0 - - def get_sediment_level(self, x: int, y: int) -> float: - """ - 获取指定位置的沉积物水平 - - Args: - x: X坐标 - y: Y坐标 - - Returns: - 沉积物水平 (0.0-1.0) - """ - try: - if self.sediment_map is not None and 0 <= x < self.sediment_map.shape[1] and 0 <= y < self.sediment_map.shape[0]: - return self.sediment_map[y, x] - return 0.0 - except Exception as e: - print(f"✗ 获取沉积物水平失败: {e}") +""" +地形管理器 +负责管理地形的核心功能 +""" + +import numpy as np +from typing import Dict, Any, List, Tuple +import time + +class TerrainManager: + """ + 地形管理器 + 负责管理地形的核心功能,包括地形数据管理、生成控制等 + """ + + def __init__(self, plugin): + """ + 初始化地形管理器 + + Args: + plugin: 程序化地形生成插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 地形配置 + self.terrain_size = plugin.config.get('terrain_size', 512) + self.terrain_scale = plugin.config.get('terrain_scale', 1.0) + self.height_scale = plugin.config.get('height_scale', 100.0) + self.seed = plugin.config.get('seed', 12345) + + # 地形数据 + self.heightmap = None + self.normalmap = None + self.biome_map = None + self.moisture_map = None + self.temperature_map = None + self.erosion_map = None + self.sediment_map = None + self.water_map = None + self.vegetation_map = None + + # 地形统计信息 + self.stats = { + 'generation_time': 0.0, + 'triangles_generated': 0, + 'vertices_generated': 0, + 'chunks_generated': 0, + 'lod_levels': plugin.config.get('lod_levels', 5) + } + + # 地形生成状态 + self.is_generating = False + self.generation_progress = 0.0 + self.last_generation_time = 0.0 + + # 地形分块管理 + self.chunk_size = plugin.config.get('chunk_size', 32) + self.chunks = {} + self.visible_chunks = set() + + print("✓ 地形管理器已创建") + + def initialize(self) -> bool: + """ + 初始化地形管理器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化地形数据结构 + self._initialize_terrain_data() + + self.initialized = True + print("✓ 地形管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 地形管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用地形管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 地形管理器未初始化") + return False + + self.enabled = True + print("✓ 地形管理器已启用") + return True + + except Exception as e: + print(f"✗ 地形管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用地形管理器""" + try: + self.enabled = False + print("✓ 地形管理器已禁用") + + except Exception as e: + print(f"✗ 地形管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理地形管理器资源""" + try: + self.disable() + self.initialized = False + print("✓ 地形管理器资源已清理") + + except Exception as e: + print(f"✗ 地形管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新地形管理器状态 + + Args: + dt: 时间增量 + """ + # 处理地形更新逻辑 + pass + + def _initialize_terrain_data(self): + """初始化地形数据结构""" + try: + size = self.terrain_size + # 初始化高度图 + self.heightmap = np.zeros((size, size), dtype=np.float32) + + # 初始化法线图 + self.normalmap = np.zeros((size, size, 3), dtype=np.float32) + + # 初始化生物群落图 + self.biome_map = np.zeros((size, size), dtype=np.int32) + + # 初始化湿度图 + self.moisture_map = np.zeros((size, size), dtype=np.float32) + + # 初始化温度图 + self.temperature_map = np.zeros((size, size), dtype=np.float32) + + # 初始化侵蚀图 + self.erosion_map = np.zeros((size, size), dtype=np.float32) + + # 初始化沉积物图 + self.sediment_map = np.zeros((size, size), dtype=np.float32) + + # 初始化水图 + self.water_map = np.zeros((size, size), dtype=np.float32) + + # 初始化植被图 + self.vegetation_map = np.zeros((size, size), dtype=np.int32) + + print("✓ 地形数据结构初始化完成") + + except Exception as e: + print(f"✗ 地形数据结构初始化失败: {e}") + import traceback + traceback.print_exc() + + def generate_terrain(self, width: int = None, height: int = None, seed: int = None) -> bool: + """ + 生成地形 + + Args: + width: 地形宽度 + height: 地形高度 + seed: 随机种子 + + Returns: + 是否生成成功 + """ + try: + if not self.enabled: + print("✗ 地形管理器未启用") + return False + + if self.is_generating: + print("⚠ 地形生成已在进行中") + return True + + # 更新参数 + if width is not None: + self.terrain_size = width + + if seed is not None: + self.seed = seed + if self.plugin.noise_generator: + self.plugin.noise_generator.set_seed(seed) + if self.plugin.heightmap_generator: + self.plugin.heightmap_generator.set_seed(seed) + + # 重新初始化地形数据(如果尺寸改变) + if width is not None or height is not None: + self._initialize_terrain_data() + + generation_start_time = time.time() + self.is_generating = True + self.generation_progress = 0.0 + + print("✓ 开始生成地形...") + + # 1. 生成高度图 + print(" → 生成高度图...") + if self.plugin.heightmap_generator: + self.heightmap = self.plugin.heightmap_generator.generate_heightmap( + self.terrain_size, self.terrain_size, self.seed + ) + self.generation_progress = 20.0 + + # 2. 计算法线 + print(" → 计算法线...") + self.normalmap = self._calculate_normals(self.heightmap) + self.generation_progress = 30.0 + + # 3. 生成生物群落 + print(" → 生成生物群落...") + if self.plugin.biome_generator: + biome_data = self.plugin.biome_generator.generate_biomes( + self.heightmap, self.seed + ) + self.biome_map = biome_data.get('biome_map', self.biome_map) + self.moisture_map = biome_data.get('moisture_map', self.moisture_map) + self.temperature_map = biome_data.get('temperature_map', self.temperature_map) + self.generation_progress = 50.0 + + # 4. 应用侵蚀 + print(" → 应用侵蚀模拟...") + if self.plugin.erosion_processor: + erosion_result = self.plugin.erosion_processor.apply_erosion( + self.heightmap, self.seed + ) + self.heightmap = erosion_result.get('heightmap', self.heightmap) + self.erosion_map = erosion_result.get('erosion_map', self.erosion_map) + self.sediment_map = erosion_result.get('sediment_map', self.sediment_map) + self.water_map = erosion_result.get('water_map', self.water_map) + self.generation_progress = 70.0 + + # 5. 生成植被 + print(" → 生成植被...") + if self.plugin.vegetation_generator: + self.vegetation_map = self.plugin.vegetation_generator.generate_vegetation( + self.heightmap, self.biome_map, self.seed + ) + self.generation_progress = 90.0 + + # 6. 更新统计信息 + self.stats['generation_time'] = time.time() - generation_start_time + self.stats['triangles_generated'] = self.terrain_size * self.terrain_size * 2 + self.stats['vertices_generated'] = self.terrain_size * self.terrain_size + self.stats['chunks_generated'] = (self.terrain_size // self.chunk_size) ** 2 + + self.is_generating = False + self.generation_progress = 100.0 + self.last_generation_time = time.time() + + print(f"✓ 地形生成完成,耗时: {self.stats['generation_time']:.2f}秒") + return True + + except Exception as e: + print(f"✗ 地形生成失败: {e}") + import traceback + traceback.print_exc() + self.is_generating = False + return False + + def _calculate_normals(self, heightmap: np.ndarray) -> np.ndarray: + """ + 根据高度图计算法线图 + + Args: + heightmap: 高度图数据 + + Returns: + 法线图数据 + """ + try: + height, width = heightmap.shape + normals = np.zeros((height, width, 3), dtype=np.float32) + + # 计算每个点的法线 + for y in range(height): + for x in range(width): + # 计算相邻点的高度 + h_left = heightmap[y, max(0, x-1)] + h_right = heightmap[y, min(width-1, x+1)] + h_up = heightmap[max(0, y-1), x] + h_down = heightmap[min(height-1, y+1), x] + + # 计算梯度 + dx = (h_right - h_left) * self.height_scale + dy = (h_down - h_up) * self.height_scale + dz = 2.0 * self.terrain_scale + + # 计算法线 + normal = np.array([-dx, -dy, dz], dtype=np.float32) + norm = np.linalg.norm(normal) + if norm > 0: + normal = normal / norm + + normals[y, x] = normal + + return normals + + except Exception as e: + print(f"✗ 法线计算失败: {e}") + return np.zeros((heightmap.shape[0], heightmap.shape[1], 3), dtype=np.float32) + + def get_terrain_data(self) -> Dict[str, Any]: + """ + 获取地形数据 + + Returns: + 地形数据字典 + """ + return { + 'heightmap': self.heightmap, + 'normalmap': self.normalmap, + 'biome_map': self.biome_map, + 'moisture_map': self.moisture_map, + 'temperature_map': self.temperature_map, + 'erosion_map': self.erosion_map, + 'sediment_map': self.sediment_map, + 'water_map': self.water_map, + 'vegetation_map': self.vegetation_map, + 'stats': self.stats.copy(), + 'is_generating': self.is_generating, + 'generation_progress': self.generation_progress + } + + def get_height_at(self, x: int, y: int) -> float: + """ + 获取指定位置的高度值 + + Args: + x: X坐标 + y: Y坐标 + + Returns: + 高度值 + """ + try: + if self.heightmap is not None and 0 <= x < self.heightmap.shape[1] and 0 <= y < self.heightmap.shape[0]: + return self.heightmap[y, x] + return 0.0 + except Exception as e: + print(f"✗ 获取高度值失败: {e}") + return 0.0 + + def get_normal_at(self, x: int, y: int) -> np.ndarray: + """ + 获取指定位置的法线向量 + + Args: + x: X坐标 + y: Y坐标 + + Returns: + 法线向量 + """ + try: + if self.normalmap is not None and 0 <= x < self.normalmap.shape[1] and 0 <= y < self.normalmap.shape[0]: + return self.normalmap[y, x].copy() + return np.array([0.0, 0.0, 1.0], dtype=np.float32) + except Exception as e: + print(f"✗ 获取法线向量失败: {e}") + return np.array([0.0, 0.0, 1.0], dtype=np.float32) + + def get_biome_at(self, x: int, y: int) -> int: + """ + 获取指定位置的生物群落类型 + + Args: + x: X坐标 + y: Y坐标 + + Returns: + 生物群落类型 + """ + try: + if self.biome_map is not None and 0 <= x < self.biome_map.shape[1] and 0 <= y < self.biome_map.shape[0]: + return self.biome_map[y, x] + return 0 + except Exception as e: + print(f"✗ 获取生物群落类型失败: {e}") + return 0 + + def set_terrain_scale(self, scale: float): + """ + 设置地形缩放 + + Args: + scale: 缩放因子 + """ + self.terrain_scale = scale + print(f"✓ 地形缩放设置为: {scale}") + + def set_height_scale(self, scale: float): + """ + 设置高度缩放 + + Args: + scale: 高度缩放因子 + """ + self.height_scale = scale + print(f"✓ 高度缩放设置为: {scale}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def is_terrain_generated(self) -> bool: + """ + 检查地形是否已生成 + + Returns: + 是否已生成地形 + """ + return self.heightmap is not None and self.heightmap.size > 0 + + def clear_terrain(self): + """清空地形数据""" + try: + self.heightmap = None + self.normalmap = None + self.biome_map = None + self.moisture_map = None + self.temperature_map = None + self.erosion_map = None + self.sediment_map = None + self.water_map = None + self.vegetation_map = None + + # 重新初始化数据结构 + self._initialize_terrain_data() + + print("✓ 地形数据已清空") + + except Exception as e: + print(f"✗ 清空地形数据失败: {e}") + import traceback + traceback.print_exc() + + def resize_terrain(self, new_size: int): + """ + 调整地形大小 + + Args: + new_size: 新的地形大小 + """ + try: + old_size = self.terrain_size + self.terrain_size = new_size + + # 重新初始化地形数据 + self._initialize_terrain_data() + + print(f"✓ 地形大小已从 {old_size} 调整为 {new_size}") + + except Exception as e: + print(f"✗ 调整地形大小失败: {e}") + import traceback + traceback.print_exc() + + def get_generation_progress(self) -> float: + """ + 获取生成进度 + + Returns: + 生成进度百分比 (0.0-100.0) + """ + return self.generation_progress + + def is_generating(self) -> bool: + """ + 检查是否正在生成地形 + + Returns: + 是否正在生成 + """ + return self.is_generating + + def get_chunk_info(self, chunk_x: int, chunk_y: int) -> Dict[str, Any]: + """ + 获取指定区块的信息 + + Args: + chunk_x: 区块X坐标 + chunk_y: 区块Y坐标 + + Returns: + 区块信息 + """ + chunk_key = f"{chunk_x},{chunk_y}" + if chunk_key in self.chunks: + return self.chunks[chunk_key].copy() + else: + return { + 'x': chunk_x, + 'y': chunk_y, + 'generated': False, + 'visible': False, + 'lod_level': 0 + } + + def set_chunk_visibility(self, chunk_x: int, chunk_y: int, visible: bool): + """ + 设置区块可见性 + + Args: + chunk_x: 区块X坐标 + chunk_y: 区块Y坐标 + visible: 是否可见 + """ + chunk_key = f"{chunk_x},{chunk_y}" + if chunk_key in self.chunks: + self.chunks[chunk_key]['visible'] = visible + if visible: + self.visible_chunks.add(chunk_key) + elif chunk_key in self.visible_chunks: + self.visible_chunks.remove(chunk_key) + + def get_visible_chunks(self) -> List[str]: + """ + 获取可见区块列表 + + Returns: + 可见区块键列表 + """ + return list(self.visible_chunks) + + def update_chunk_lod(self, chunk_x: int, chunk_y: int, lod_level: int): + """ + 更新区块LOD级别 + + Args: + chunk_x: 区块X坐标 + chunk_y: 区块Y坐标 + lod_level: LOD级别 + """ + chunk_key = f"{chunk_x},{chunk_y}" + if chunk_key in self.chunks: + self.chunks[chunk_key]['lod_level'] = max(0, min(lod_level, self.stats['lod_levels'] - 1)) + + def get_terrain_bounds(self) -> Tuple[int, int]: + """ + 获取地形边界 + + Returns: + (宽度, 高度) + """ + if self.heightmap is not None: + return self.heightmap.shape[1], self.heightmap.shape[0] + return self.terrain_size, self.terrain_size + + def get_terrain_center(self) -> Tuple[float, float]: + """ + 获取地形中心点 + + Returns: + (中心X坐标, 中心Y坐标) + """ + width, height = self.get_terrain_bounds() + return width / 2.0, height / 2.0 + + def get_terrain_area(self) -> float: + """ + 获取地形面积 + + Returns: + 地形面积 + """ + width, height = self.get_terrain_bounds() + return width * height * self.terrain_scale * self.terrain_scale + + def get_height_range(self) -> Tuple[float, float]: + """ + 获取高度范围 + + Returns: + (最小高度, 最大高度) + """ + if self.heightmap is not None and self.heightmap.size > 0: + return float(np.min(self.heightmap)), float(np.max(self.heightmap)) + return 0.0, 0.0 + + def get_average_height(self) -> float: + """ + 获取平均高度 + + Returns: + 平均高度 + """ + if self.heightmap is not None and self.heightmap.size > 0: + return float(np.mean(self.heightmap)) + return 0.0 + + def get_terrain_gradient(self, x: int, y: int) -> float: + """ + 获取指定位置的地形坡度 + + Args: + x: X坐标 + y: Y坐标 + + Returns: + 坡度值 (弧度) + """ + try: + if self.normalmap is not None and 0 <= x < self.normalmap.shape[1] and 0 <= y < self.normalmap.shape[0]: + normal = self.normalmap[y, x] + # 计算与垂直方向的夹角 + vertical = np.array([0.0, 0.0, 1.0]) + dot_product = np.dot(normal, vertical) + angle = np.arccos(np.clip(dot_product, -1.0, 1.0)) + return float(angle) + return 0.0 + except Exception as e: + print(f"✗ 计算地形坡度失败: {e}") + return 0.0 + + def get_terrain_aspect(self, x: int, y: int) -> float: + """ + 获取指定位置的地形坡向 + + Args: + x: X坐标 + y: Y坐标 + + Returns: + 坡向值 (弧度,0为北向) + """ + try: + if self.normalmap is not None and 0 <= x < self.normalmap.shape[1] and 0 <= y < self.normalmap.shape[0]: + normal = self.normalmap[y, x] + # 计算在水平面上的投影 + aspect = np.arctan2(normal[1], normal[0]) + # 转换为0-2π范围 + if aspect < 0: + aspect += 2 * np.pi + return float(aspect) + return 0.0 + except Exception as e: + print(f"✗ 计算地形坡向失败: {e}") + return 0.0 + + def get_terrain_curvature(self, x: int, y: int) -> float: + """ + 获取指定位置的地形曲率 + + Args: + x: X坐标 + y: Y坐标 + + Returns: + 曲率值 + """ + try: + if self.heightmap is None: + return 0.0 + + height, width = self.heightmap.shape + if not (1 <= x < width - 1 and 1 <= y < height - 1): + return 0.0 + + # 使用拉普拉斯算子计算曲率 + center = self.heightmap[y, x] + laplacian = ( + self.heightmap[y-1, x] + self.heightmap[y+1, x] + + self.heightmap[y, x-1] + self.heightmap[y, x+1] - 4 * center + ) + + return float(laplacian) + except Exception as e: + print(f"✗ 计算地形曲率失败: {e}") + return 0.0 + + def get_terrain_roughness(self, x: int, y: int, radius: int = 3) -> float: + """ + 获取指定位置的地形粗糙度 + + Args: + x: X坐标 + y: Y坐标 + radius: 计算半径 + + Returns: + 粗糙度值 + """ + try: + if self.heightmap is None: + return 0.0 + + height, width = self.heightmap.shape + min_x = max(0, x - radius) + max_x = min(width - 1, x + radius) + min_y = max(0, y - radius) + max_y = min(height - 1, y + radius) + + if min_x >= max_x or min_y >= max_y: + return 0.0 + + # 计算局部高度变化 + local_heights = self.heightmap[min_y:max_y+1, min_x:max_x+1] + roughness = float(np.std(local_heights)) + + return roughness + except Exception as e: + print(f"✗ 计算地形粗糙度失败: {e}") + return 0.0 + + def get_water_level(self, x: int, y: int) -> float: + """ + 获取指定位置的水位 + + Args: + x: X坐标 + y: Y坐标 + + Returns: + 水位值 + """ + try: + if self.water_map is not None and 0 <= x < self.water_map.shape[1] and 0 <= y < self.water_map.shape[0]: + return self.water_map[y, x] + return 0.0 + except Exception as e: + print(f"✗ 获取水位值失败: {e}") + return 0.0 + + def is_under_water(self, x: int, y: int) -> bool: + """ + 检查指定位置是否在水下 + + Args: + x: X坐标 + y: Y坐标 + + Returns: + 是否在水下 + """ + try: + if self.heightmap is not None and self.water_map is not None: + if 0 <= x < self.heightmap.shape[1] and 0 <= y < self.heightmap.shape[0]: + return self.water_map[y, x] > self.heightmap[y, x] + return False + except Exception as e: + print(f"✗ 检查水下状态失败: {e}") + return False + + def get_vegetation_density(self, x: int, y: int) -> float: + """ + 获取指定位置的植被密度 + + Args: + x: X坐标 + y: Y坐标 + + Returns: + 植被密度 (0.0-1.0) + """ + try: + if self.vegetation_map is not None and 0 <= x < self.vegetation_map.shape[1] and 0 <= y < self.vegetation_map.shape[0]: + # 简化处理,将植被类型转换为密度值 + vegetation_type = self.vegetation_map[y, x] + return min(1.0, vegetation_type / 10.0) # 假设最多10种植被类型 + return 0.0 + except Exception as e: + print(f"✗ 获取植被密度失败: {e}") + return 0.0 + + def get_biome_properties(self, x: int, y: int) -> Dict[str, Any]: + """ + 获取指定位置的生物群落属性 + + Args: + x: X坐标 + y: Y坐标 + + Returns: + 生物群落属性字典 + """ + try: + biome_type = self.get_biome_at(x, y) + moisture = 0.0 + temperature = 0.0 + + if self.moisture_map is not None and 0 <= x < self.moisture_map.shape[1] and 0 <= y < self.moisture_map.shape[0]: + moisture = self.moisture_map[y, x] + + if self.temperature_map is not None and 0 <= x < self.temperature_map.shape[1] and 0 <= y < self.temperature_map.shape[0]: + temperature = self.temperature_map[y, x] + + return { + 'biome_type': biome_type, + 'moisture': moisture, + 'temperature': temperature + } + except Exception as e: + print(f"✗ 获取生物群落属性失败: {e}") + return { + 'biome_type': 0, + 'moisture': 0.0, + 'temperature': 0.0 + } + + def get_erosion_level(self, x: int, y: int) -> float: + """ + 获取指定位置的侵蚀程度 + + Args: + x: X坐标 + y: Y坐标 + + Returns: + 侵蚀程度 (0.0-1.0) + """ + try: + if self.erosion_map is not None and 0 <= x < self.erosion_map.shape[1] and 0 <= y < self.erosion_map.shape[0]: + return self.erosion_map[y, x] + return 0.0 + except Exception as e: + print(f"✗ 获取侵蚀程度失败: {e}") + return 0.0 + + def get_sediment_level(self, x: int, y: int) -> float: + """ + 获取指定位置的沉积物水平 + + Args: + x: X坐标 + y: Y坐标 + + Returns: + 沉积物水平 (0.0-1.0) + """ + try: + if self.sediment_map is not None and 0 <= x < self.sediment_map.shape[1] and 0 <= y < self.sediment_map.shape[0]: + return self.sediment_map[y, x] + return 0.0 + except Exception as e: + print(f"✗ 获取沉积物水平失败: {e}") return 0.0 \ No newline at end of file diff --git a/plugins/user/procedural_terrain_generation/editor/preset_manager.py b/plugins/user/procedural_terrain_generation/editor/preset_manager.py index 73d835cd..7675640a 100644 --- a/plugins/user/procedural_terrain_generation/editor/preset_manager.py +++ b/plugins/user/procedural_terrain_generation/editor/preset_manager.py @@ -1,1110 +1,1110 @@ -""" -预设管理器 -管理地形生成和编辑的预设配置 -""" - -import json -import os -from typing import Dict, Any, List, Optional -import time - -class PresetManager: - """ - 预设管理器 - 管理地形生成和编辑的预设配置,包括创建、保存、加载和应用预设 - """ - - def __init__(self, plugin): - """ - 初始化预设管理器 - - Args: - plugin: 程序化地形生成插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 预设配置 - self.preset_config = { - 'presets_directory': 'presets', - 'default_preset': 'default', - 'auto_save_enabled': True, - 'max_presets': 100 - } - - # 预设数据 - self.presets = {} - self.current_preset = None - - # 预设类别 - self.preset_categories = { - 'terrain': {'name': '地形', 'description': '地形生成预设'}, - 'biome': {'name': '生物群落', 'description': '生物群落分布预设'}, - 'erosion': {'name': '侵蚀', 'description': '侵蚀效果预设'}, - 'vegetation': {'name': '植被', 'description': '植被分布预设'}, - 'editor': {'name': '编辑器', 'description': '编辑器配置预设'} - } - - # 统计信息 - self.stats = { - 'presets_loaded': 0, - 'presets_saved': 0, - 'presets_applied': 0, - 'total_load_time': 0.0, - 'total_save_time': 0.0 - } - - print("✓ 预设管理器已创建") - - def initialize(self) -> bool: - """ - 初始化预设管理器 - - Returns: - 是否初始化成功 - """ - try: - # 创建预设目录 - presets_dir = self.preset_config['presets_directory'] - if not os.path.exists(presets_dir): - os.makedirs(presets_dir) - - # 加载默认预设 - self._load_default_presets() - - # 加载用户预设 - self._load_user_presets() - - self.initialized = True - print("✓ 预设管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 预设管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用预设管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 预设管理器未初始化") - return False - - self.enabled = True - print("✓ 预设管理器已启用") - return True - - except Exception as e: - print(f"✗ 预设管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用预设管理器""" - try: - self.enabled = False - print("✓ 预设管理器已禁用") - - except Exception as e: - print(f"✗ 预设管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理预设管理器资源""" - try: - self.disable() - - # 自动保存当前预设 - if self.preset_config['auto_save_enabled'] and self.current_preset: - self.save_preset(self.current_preset) - - self.initialized = False - print("✓ 预设管理器资源已清理") - - except Exception as e: - print(f"✗ 预设管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新预设管理器状态 - - Args: - dt: 时间增量 - """ - # 处理更新逻辑 - pass - - def _load_default_presets(self): - """加载默认预设""" - try: - # 创建默认地形预设 - self.presets['default_terrain'] = { - 'name': '默认地形', - 'category': 'terrain', - 'description': '默认的地形生成参数', - 'parameters': { - 'terrain_size': 512, - 'height_scale': 100.0, - 'noise_scale': 50.0, - 'octaves': 6, - 'persistence': 0.5, - 'lacunarity': 2.0 - }, - 'timestamp': time.time(), - 'is_default': True - } - - # 创建默认生物群落预设 - self.presets['default_biome'] = { - 'name': '默认生物群落', - 'category': 'biome', - 'description': '默认的生物群落分布参数', - 'parameters': { - 'humidity_scale': 20.0, - 'temperature_scale': 20.0, - 'height_influence': 0.3, - 'humidity_influence': 0.4, - 'temperature_influence': 0.3 - }, - 'timestamp': time.time(), - 'is_default': True - } - - # 创建默认侵蚀预设 - self.presets['default_erosion'] = { - 'name': '默认侵蚀', - 'category': 'erosion', - 'description': '默认的侵蚀效果参数', - 'parameters': { - 'water_erosion_rate': 0.1, - 'thermal_erosion_rate': 0.05, - 'wind_erosion_rate': 0.02 - }, - 'timestamp': time.time(), - 'is_default': True - } - - # 创建默认植被预设 - self.presets['default_vegetation'] = { - 'name': '默认植被', - 'category': 'vegetation', - 'description': '默认的植被分布参数', - 'parameters': { - 'density_scale': 10.0, - 'clumping_factor': 0.7, - 'slope_tolerance': 0.3 - }, - 'timestamp': time.time(), - 'is_default': True - } - - # 创建默认编辑器预设 - self.presets['default_editor'] = { - 'name': '默认编辑器', - 'category': 'editor', - 'description': '默认的编辑器配置', - 'parameters': { - 'brush_size': 10, - 'brush_strength': 0.5, - 'preview_enabled': True, - 'realtime_update': True - }, - 'timestamp': time.time(), - 'is_default': True - } - - print("✓ 默认预设加载完成") - - except Exception as e: - print(f"✗ 默认预设加载失败: {e}") - import traceback - traceback.print_exc() - - def _load_user_presets(self): - """加载用户预设""" - try: - load_start_time = time.time() - - presets_dir = self.preset_config['presets_directory'] - if not os.path.exists(presets_dir): - print("✓ 用户预设目录不存在,跳过加载") - return - - # 遍历预设目录 - for filename in os.listdir(presets_dir): - if filename.endswith('.json'): - try: - filepath = os.path.join(presets_dir, filename) - with open(filepath, 'r', encoding='utf-8') as f: - preset_data = json.load(f) - - # 使用文件名(不含扩展名)作为预设ID - preset_id = os.path.splitext(filename)[0] - self.presets[preset_id] = preset_data - - except Exception as e: - print(f"✗ 预设文件加载失败 {filename}: {e}") - - load_time = time.time() - load_start_time - self.stats['presets_loaded'] = len(self.presets) - self.stats['total_load_time'] = load_time - - print(f"✓ 用户预设加载完成,共加载 {len(self.presets)} 个预设,耗时: {load_time:.3f}秒") - - except Exception as e: - print(f"✗ 用户预设加载失败: {e}") - import traceback - traceback.print_exc() - - def save_preset(self, preset_id: str, preset_data: Dict[str, Any] = None) -> bool: - """ - 保存预设 - - Args: - preset_id: 预设ID - preset_data: 预设数据(如果为None,则保存当前配置) - - Returns: - 是否保存成功 - """ - try: - save_start_time = time.time() - - if not self.enabled: - print("✗ 预设管理器未启用") - return False - - # 如果未提供预设数据,则创建当前配置的快照 - if preset_data is None: - preset_data = self._create_current_preset_snapshot(preset_id) - - # 保存预设数据 - self.presets[preset_id] = preset_data - - # 保存到文件 - presets_dir = self.preset_config['presets_directory'] - filepath = os.path.join(presets_dir, f"{preset_id}.json") - - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(preset_data, f, ensure_ascii=False, indent=2) - - # 更新统计信息 - save_time = time.time() - save_start_time - self.stats['presets_saved'] += 1 - self.stats['total_save_time'] += save_time - - print(f"✓ 预设 '{preset_id}' 保存完成,耗时: {save_time:.3f}秒") - return True - - except Exception as e: - print(f"✗ 预设保存失败: {e}") - import traceback - traceback.print_exc() - return False - - def _create_current_preset_snapshot(self, preset_id: str) -> Dict[str, Any]: - """ - 创建当前配置的预设快照 - - Args: - preset_id: 预设ID - - Returns: - 预设数据字典 - """ - try: - # 获取当前插件配置 - preset_data = { - 'name': preset_id, - 'category': 'terrain', # 默认类别 - 'description': '用户自定义预设', - 'parameters': {}, - 'timestamp': time.time(), - 'is_default': False - } - - # 添加地形管理器配置 - if self.plugin.terrain_manager: - tm = self.plugin.terrain_manager - preset_data['parameters'].update({ - 'terrain_size': tm.terrain_size, - 'terrain_scale': tm.terrain_scale, - 'height_scale': tm.height_scale - }) - - # 添加噪声生成器配置 - if self.plugin.noise_generator: - ng = self.plugin.noise_generator - preset_data['parameters'].update({ - 'noise_seed': ng.seed, - 'noise_scale': ng.fractal_params.get('scale', 50.0), - 'noise_octaves': ng.fractal_params.get('octaves', 6), - 'noise_persistence': ng.fractal_params.get('persistence', 0.5), - 'noise_lacunarity': ng.fractal_params.get('lacunarity', 2.0) - }) - - # 添加高度图生成器配置 - if self.plugin.heightmap_generator: - hg = self.plugin.heightmap_generator - preset_data['parameters'].update({ - 'heightmap_scale': hg.scale, - 'heightmap_octaves': hg.octaves, - 'heightmap_persistence': hg.persistence, - 'heightmap_lacunarity': hg.lacunarity - }) - - # 添加生物群落生成器配置 - if self.plugin.biome_generator: - bg = self.plugin.biome_generator - preset_data['parameters'].update({ - 'biome_seed': bg.seed, - 'humidity_scale': bg.distribution_params.get('humidity_scale', 20.0), - 'temperature_scale': bg.distribution_params.get('temperature_scale', 20.0) - }) - - # 添加侵蚀处理器配置 - if self.plugin.erosion_processor: - ep = self.plugin.erosion_processor - preset_data['parameters'].update({ - 'erosion_seed': ep.seed, - 'water_erosion_rate': ep.erosion_params['water_erosion'].get('rate', 0.1), - 'thermal_erosion_rate': ep.erosion_params['thermal_erosion'].get('rate', 0.05), - 'wind_erosion_rate': ep.erosion_params['wind_erosion'].get('rate', 0.02) - }) - - # 添加植被生成器配置 - if self.plugin.vegetation_generator: - vg = self.plugin.vegetation_generator - preset_data['parameters'].update({ - 'vegetation_seed': vg.seed, - 'density_scale': vg.vegetation_params.get('density_scale', 10.0), - 'clumping_factor': vg.vegetation_params.get('clumping_factor', 0.7) - }) - - return preset_data - - except Exception as e: - print(f"✗ 预设快照创建失败: {e}") - import traceback - traceback.print_exc() - return { - 'name': preset_id, - 'category': 'terrain', - 'description': '默认预设', - 'parameters': {}, - 'timestamp': time.time(), - 'is_default': False - } - - def load_preset(self, preset_id: str) -> Optional[Dict[str, Any]]: - """ - 加载预设 - - Args: - preset_id: 预设ID - - Returns: - 预设数据字典或None - """ - try: - if not self.enabled: - print("✗ 预设管理器未启用") - return None - - if preset_id not in self.presets: - print(f"✗ 预设不存在: {preset_id}") - return None - - preset_data = self.presets[preset_id] - - # 更新统计信息 - self.stats['presets_applied'] += 1 - self.current_preset = preset_id - - print(f"✓ 预设 '{preset_id}' 加载完成") - return preset_data - - except Exception as e: - print(f"✗ 预设加载失败: {e}") - import traceback - traceback.print_exc() - return None - - def apply_preset(self, preset_id: str) -> bool: - """ - 应用预设到当前配置 - - Args: - preset_id: 预设ID - - Returns: - 是否应用成功 - """ - try: - if not self.enabled: - print("✗ 预设管理器未启用") - return False - - preset_data = self.load_preset(preset_id) - if preset_data is None: - return False - - parameters = preset_data.get('parameters', {}) - - # 应用参数到各个组件 - self._apply_parameters_to_components(parameters) - - # 更新统计信息 - self.stats['presets_applied'] += 1 - self.current_preset = preset_id - - print(f"✓ 预设 '{preset_id}' 应用完成") - return True - - except Exception as e: - print(f"✗ 预设应用失败: {e}") - import traceback - traceback.print_exc() - return False - - def _apply_parameters_to_components(self, parameters: Dict[str, Any]): - """ - 应用参数到各个组件 - - Args: - parameters: 参数字典 - """ - try: - # 应用到地形管理器 - if self.plugin.terrain_manager and 'terrain_size' in parameters: - self.plugin.terrain_manager.terrain_size = parameters['terrain_size'] - if self.plugin.terrain_manager and 'terrain_scale' in parameters: - self.plugin.terrain_manager.terrain_scale = parameters['terrain_scale'] - if self.plugin.terrain_manager and 'height_scale' in parameters: - self.plugin.terrain_manager.height_scale = parameters['height_scale'] - - # 应用到噪声生成器 - if self.plugin.noise_generator and 'noise_seed' in parameters: - self.plugin.noise_generator.set_seed(parameters['noise_seed']) - if self.plugin.noise_generator and 'noise_scale' in parameters: - self.plugin.noise_generator.set_fractal_parameters( - scale=parameters['noise_scale'] - ) - - # 应用到高度图生成器 - if self.plugin.heightmap_generator and 'heightmap_scale' in parameters: - self.plugin.heightmap_generator.set_scale(parameters['heightmap_scale']) - if self.plugin.heightmap_generator and 'heightmap_octaves' in parameters: - self.plugin.heightmap_generator.set_octaves(parameters['heightmap_octaves']) - - # 应用到生物群落生成器 - if self.plugin.biome_generator and 'biome_seed' in parameters: - self.plugin.biome_generator.set_seed(parameters['biome_seed']) - - # 应用到侵蚀处理器 - if self.plugin.erosion_processor and 'erosion_seed' in parameters: - self.plugin.erosion_processor.set_seed(parameters['erosion_seed']) - - # 应用到植被生成器 - if self.plugin.vegetation_generator and 'vegetation_seed' in parameters: - self.plugin.vegetation_generator.set_seed(parameters['vegetation_seed']) - - # 应用到地形编辑器 - if (self.plugin.terrain_editor and - hasattr(self.plugin, 'terrain_editor') and - self.plugin.terrain_editor): - editor = self.plugin.terrain_editor - if 'brush_size' in parameters: - editor.set_brush_size(parameters['brush_size']) - if 'brush_strength' in parameters: - editor.set_brush_strength(parameters['brush_strength']) - - except Exception as e: - print(f"✗ 参数应用失败: {e}") - import traceback - traceback.print_exc() - - def delete_preset(self, preset_id: str) -> bool: - """ - 删除预设 - - Args: - preset_id: 预设ID - - Returns: - 是否删除成功 - """ - try: - if not self.enabled: - print("✗ 预设管理器未启用") - return False - - if preset_id not in self.presets: - print(f"✗ 预设不存在: {preset_id}") - return False - - # 检查是否为默认预设 - if self.presets[preset_id].get('is_default', False): - print(f"✗ 无法删除默认预设: {preset_id}") - return False - - # 从内存中删除 - del self.presets[preset_id] - - # 从文件系统中删除 - presets_dir = self.preset_config['presets_directory'] - filepath = os.path.join(presets_dir, f"{preset_id}.json") - if os.path.exists(filepath): - os.remove(filepath) - - # 如果删除的是当前预设,清空当前预设 - if self.current_preset == preset_id: - self.current_preset = None - - print(f"✓ 预设 '{preset_id}' 删除完成") - return True - - except Exception as e: - print(f"✗ 预设删除失败: {e}") - import traceback - traceback.print_exc() - return False - - def list_presets(self, category: str = None) -> List[str]: - """ - 列出预设 - - Args: - category: 预设类别(如果为None,则列出所有预设) - - Returns: - 预设ID列表 - """ - try: - if category is None: - return list(self.presets.keys()) - else: - return [preset_id for preset_id, preset_data in self.presets.items() - if preset_data.get('category') == category] - - except Exception as e: - print(f"✗ 预设列表获取失败: {e}") - import traceback - traceback.print_exc() - return [] - - def get_preset_info(self, preset_id: str) -> Optional[Dict[str, Any]]: - """ - 获取预设信息 - - Args: - preset_id: 预设ID - - Returns: - 预设信息字典或None - """ - try: - if preset_id not in self.presets: - return None - - preset_data = self.presets[preset_id] - return { - 'name': preset_data.get('name', preset_id), - 'category': preset_data.get('category', 'unknown'), - 'description': preset_data.get('description', ''), - 'timestamp': preset_data.get('timestamp', 0), - 'is_default': preset_data.get('is_default', False), - 'parameters_count': len(preset_data.get('parameters', {})) - } - - except Exception as e: - print(f"✗ 预设信息获取失败: {e}") - import traceback - traceback.print_exc() - return None - - def get_preset_categories(self) -> Dict[str, Dict[str, str]]: - """ - 获取预设类别 - - Returns: - 预设类别字典 - """ - return self.preset_categories.copy() - - def create_preset_category(self, category_id: str, name: str, description: str = '') -> bool: - """ - 创建预设类别 - - Args: - category_id: 类别ID - name: 类别名称 - description: 类别描述 - - Returns: - 是否创建成功 - """ - try: - self.preset_categories[category_id] = { - 'name': name, - 'description': description - } - - print(f"✓ 预设类别 '{name}' 创建完成") - return True - - except Exception as e: - print(f"✗ 预设类别创建失败: {e}") - import traceback - traceback.print_exc() - return False - - def export_preset(self, preset_id: str, filepath: str) -> bool: - """ - 导出预设到文件 - - Args: - preset_id: 预设ID - filepath: 导出文件路径 - - Returns: - 是否导出成功 - """ - try: - if preset_id not in self.presets: - print(f"✗ 预设不存在: {preset_id}") - return False - - preset_data = self.presets[preset_id] - - # 保存到指定文件 - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(preset_data, f, ensure_ascii=False, indent=2) - - print(f"✓ 预设 '{preset_id}' 导出到 {filepath}") - return True - - except Exception as e: - print(f"✗ 预设导出失败: {e}") - import traceback - traceback.print_exc() - return False - - def import_preset(self, filepath: str, preset_id: str = None) -> bool: - """ - 从文件导入预设 - - Args: - filepath: 导入文件路径 - preset_id: 预设ID(如果为None,则使用文件名) - - Returns: - 是否导入成功 - """ - try: - # 读取预设文件 - with open(filepath, 'r', encoding='utf-8') as f: - preset_data = json.load(f) - - # 确定预设ID - if preset_id is None: - preset_id = os.path.splitext(os.path.basename(filepath))[0] - - # 保存预设 - self.presets[preset_id] = preset_data - - # 保存到预设目录 - presets_dir = self.preset_config['presets_directory'] - save_filepath = os.path.join(presets_dir, f"{preset_id}.json") - - with open(save_filepath, 'w', encoding='utf-8') as f: - json.dump(preset_data, f, ensure_ascii=False, indent=2) - - print(f"✓ 预设从 {filepath} 导入完成,ID: {preset_id}") - return True - - except Exception as e: - print(f"✗ 预设导入失败: {e}") - import traceback - traceback.print_exc() - return False - - def set_preset_config(self, config: Dict[str, Any]): - """ - 设置预设配置 - - Args: - config: 配置字典 - """ - self.preset_config.update(config) - print(f"✓ 预设配置已更新: {self.preset_config}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'presets_loaded': 0, - 'presets_saved': 0, - 'presets_applied': 0, - 'total_load_time': 0.0, - 'total_save_time': 0.0 - } - print("✓ 预设管理器统计信息已重置") - - def get_current_preset(self) -> Optional[str]: - """ - 获取当前预设 - - Returns: - 当前预设ID或None - """ - return self.current_preset - - def set_current_preset(self, preset_id: str): - """ - 设置当前预设 - - Args: - preset_id: 预设ID - """ - if preset_id in self.presets: - self.current_preset = preset_id - print(f"✓ 当前预设设置为: {preset_id}") - else: - print(f"✗ 预设不存在: {preset_id}") - - def create_terrain_preset_from_current(self, preset_name: str, description: str = '') -> str: - """ - 从当前地形创建预设 - - Args: - preset_name: 预设名称 - description: 预设描述 - - Returns: - 预设ID - """ - try: - # 生成预设ID - preset_id = preset_name.lower().replace(' ', '_') - - # 创建预设数据 - preset_data = { - 'name': preset_name, - 'category': 'terrain', - 'description': description, - 'parameters': {}, - 'timestamp': time.time(), - 'is_default': False - } - - # 获取当前地形参数 - if self.plugin.terrain_manager: - tm = self.plugin.terrain_manager - preset_data['parameters'].update({ - 'terrain_size': tm.terrain_size, - 'terrain_scale': tm.terrain_scale, - 'height_scale': tm.height_scale, - 'seed': getattr(tm, 'seed', 12345) - }) - - # 保存预设 - self.save_preset(preset_id, preset_data) - - print(f"✓ 从当前地形创建预设: {preset_name}") - return preset_id - - except Exception as e: - print(f"✗ 从当前地形创建预设失败: {e}") - import traceback - traceback.print_exc() - return '' - - def batch_apply_presets(self, preset_ids: List[str]) -> bool: - """ - 批量应用预设 - - Args: - preset_ids: 预设ID列表 - - Returns: - 是否应用成功 - """ - try: - if not self.enabled: - print("✗ 预设管理器未启用") - return False - - success_count = 0 - for preset_id in preset_ids: - if self.apply_preset(preset_id): - success_count += 1 - - print(f"✓ 批量应用预设完成: {success_count}/{len(preset_ids)} 个预设应用成功") - return success_count == len(preset_ids) - - except Exception as e: - print(f"✗ 批量应用预设失败: {e}") - import traceback - traceback.print_exc() - return False - - def search_presets(self, keyword: str, category: str = None) -> List[str]: - """ - 搜索预设 - - Args: - keyword: 搜索关键词 - category: 预设类别(可选) - - Returns: - 匹配的预设ID列表 - """ - try: - matching_presets = [] - - for preset_id, preset_data in self.presets.items(): - # 检查类别筛选 - if category and preset_data.get('category') != category: - continue - - # 检查关键词匹配 - if (keyword.lower() in preset_data.get('name', '').lower() or - keyword.lower() in preset_data.get('description', '').lower()): - matching_presets.append(preset_id) - - return matching_presets - - except Exception as e: - print(f"✗ 预设搜索失败: {e}") - import traceback - traceback.print_exc() - return [] - - def duplicate_preset(self, source_preset_id: str, new_preset_id: str, new_name: str = None) -> bool: - """ - 复制预设 - - Args: - source_preset_id: 源预设ID - new_preset_id: 新预设ID - new_name: 新预设名称(如果为None,则使用源预设名称) - - Returns: - 是否复制成功 - """ - try: - if source_preset_id not in self.presets: - print(f"✗ 源预设不存在: {source_preset_id}") - return False - - # 复制预设数据 - preset_data = self.presets[source_preset_id].copy() - - # 更新名称 - if new_name: - preset_data['name'] = new_name - else: - preset_data['name'] = f"{preset_data['name']} (副本)" - - # 更新时间戳 - preset_data['timestamp'] = time.time() - - # 标记为非默认预设 - preset_data['is_default'] = False - - # 保存新预设 - return self.save_preset(new_preset_id, preset_data) - - except Exception as e: - print(f"✗ 预设复制失败: {e}") - import traceback - traceback.print_exc() - return False - - def update_preset_parameters(self, preset_id: str, parameters: Dict[str, Any]) -> bool: - """ - 更新预设参数 - - Args: - preset_id: 预设ID - parameters: 要更新的参数 - - Returns: - 是否更新成功 - """ - try: - if preset_id not in self.presets: - print(f"✗ 预设不存在: {preset_id}") - return False - - # 更新参数 - self.presets[preset_id]['parameters'].update(parameters) - - # 更新时间戳 - self.presets[preset_id]['timestamp'] = time.time() - - # 保存到文件 - presets_dir = self.preset_config['presets_directory'] - filepath = os.path.join(presets_dir, f"{preset_id}.json") - - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(self.presets[preset_id], f, ensure_ascii=False, indent=2) - - print(f"✓ 预设 '{preset_id}' 参数更新完成") - return True - - except Exception as e: - print(f"✗ 预设参数更新失败: {e}") - import traceback - traceback.print_exc() - return False - - def get_preset_parameters(self, preset_id: str) -> Optional[Dict[str, Any]]: - """ - 获取预设参数 - - Args: - preset_id: 预设ID - - Returns: - 参数字典或None - """ - try: - if preset_id not in self.presets: - return None - - return self.presets[preset_id].get('parameters', {}).copy() - - except Exception as e: - print(f"✗ 预设参数获取失败: {e}") - import traceback - traceback.print_exc() - return None - - def validate_preset(self, preset_id: str) -> bool: - """ - 验证预设完整性 - - Args: - preset_id: 预设ID - - Returns: - 预设是否有效 - """ - try: - if preset_id not in self.presets: - return False - - preset_data = self.presets[preset_id] - - # 检查必需字段 - required_fields = ['name', 'category', 'parameters'] - for field in required_fields: - if field not in preset_data: - print(f"✗ 预设 '{preset_id}' 缺少必需字段: {field}") - return False - - # 检查类别有效性 - if preset_data['category'] not in self.preset_categories: - print(f"✗ 预设 '{preset_id}' 包含无效类别: {preset_data['category']}") - return False - - return True - - except Exception as e: - print(f"✗ 预设验证失败: {e}") - import traceback - traceback.print_exc() - return False - - def backup_presets(self, backup_dir: str = 'presets_backup') -> bool: - """ - 备份所有预设 - - Args: - backup_dir: 备份目录 - - Returns: - 是否备份成功 - """ - try: - if not os.path.exists(backup_dir): - os.makedirs(backup_dir) - - backup_count = 0 - for preset_id, preset_data in self.presets.items(): - try: - filepath = os.path.join(backup_dir, f"{preset_id}.json") - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(preset_data, f, ensure_ascii=False, indent=2) - backup_count += 1 - except Exception as e: - print(f"✗ 预设 '{preset_id}' 备份失败: {e}") - - print(f"✓ 预设备份完成: {backup_count}/{len(self.presets)} 个预设已备份到 {backup_dir}") - return True - - except Exception as e: - print(f"✗ 预设备份失败: {e}") - import traceback - traceback.print_exc() - return False - - def restore_presets(self, backup_dir: str = 'presets_backup') -> bool: - """ - 从备份恢复预设 - - Args: - backup_dir: 备份目录 - - Returns: - 是否恢复成功 - """ - try: - if not os.path.exists(backup_dir): - print(f"✗ 备份目录不存在: {backup_dir}") - return False - - restore_count = 0 - for filename in os.listdir(backup_dir): - if filename.endswith('.json'): - try: - filepath = os.path.join(backup_dir, filename) - with open(filepath, 'r', encoding='utf-8') as f: - preset_data = json.load(f) - - preset_id = os.path.splitext(filename)[0] - self.presets[preset_id] = preset_data - restore_count += 1 - except Exception as e: - print(f"✗ 预设文件 '{filename}' 恢复失败: {e}") - - print(f"✓ 预设恢复完成: {restore_count} 个预设已从 {backup_dir} 恢复") - return True - - except Exception as e: - print(f"✗ 预设恢复失败: {e}") - import traceback - traceback.print_exc() +""" +预设管理器 +管理地形生成和编辑的预设配置 +""" + +import json +import os +from typing import Dict, Any, List, Optional +import time + +class PresetManager: + """ + 预设管理器 + 管理地形生成和编辑的预设配置,包括创建、保存、加载和应用预设 + """ + + def __init__(self, plugin): + """ + 初始化预设管理器 + + Args: + plugin: 程序化地形生成插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 预设配置 + self.preset_config = { + 'presets_directory': 'presets', + 'default_preset': 'default', + 'auto_save_enabled': True, + 'max_presets': 100 + } + + # 预设数据 + self.presets = {} + self.current_preset = None + + # 预设类别 + self.preset_categories = { + 'terrain': {'name': '地形', 'description': '地形生成预设'}, + 'biome': {'name': '生物群落', 'description': '生物群落分布预设'}, + 'erosion': {'name': '侵蚀', 'description': '侵蚀效果预设'}, + 'vegetation': {'name': '植被', 'description': '植被分布预设'}, + 'editor': {'name': '编辑器', 'description': '编辑器配置预设'} + } + + # 统计信息 + self.stats = { + 'presets_loaded': 0, + 'presets_saved': 0, + 'presets_applied': 0, + 'total_load_time': 0.0, + 'total_save_time': 0.0 + } + + print("✓ 预设管理器已创建") + + def initialize(self) -> bool: + """ + 初始化预设管理器 + + Returns: + 是否初始化成功 + """ + try: + # 创建预设目录 + presets_dir = self.preset_config['presets_directory'] + if not os.path.exists(presets_dir): + os.makedirs(presets_dir) + + # 加载默认预设 + self._load_default_presets() + + # 加载用户预设 + self._load_user_presets() + + self.initialized = True + print("✓ 预设管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 预设管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用预设管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 预设管理器未初始化") + return False + + self.enabled = True + print("✓ 预设管理器已启用") + return True + + except Exception as e: + print(f"✗ 预设管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用预设管理器""" + try: + self.enabled = False + print("✓ 预设管理器已禁用") + + except Exception as e: + print(f"✗ 预设管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理预设管理器资源""" + try: + self.disable() + + # 自动保存当前预设 + if self.preset_config['auto_save_enabled'] and self.current_preset: + self.save_preset(self.current_preset) + + self.initialized = False + print("✓ 预设管理器资源已清理") + + except Exception as e: + print(f"✗ 预设管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新预设管理器状态 + + Args: + dt: 时间增量 + """ + # 处理更新逻辑 + pass + + def _load_default_presets(self): + """加载默认预设""" + try: + # 创建默认地形预设 + self.presets['default_terrain'] = { + 'name': '默认地形', + 'category': 'terrain', + 'description': '默认的地形生成参数', + 'parameters': { + 'terrain_size': 512, + 'height_scale': 100.0, + 'noise_scale': 50.0, + 'octaves': 6, + 'persistence': 0.5, + 'lacunarity': 2.0 + }, + 'timestamp': time.time(), + 'is_default': True + } + + # 创建默认生物群落预设 + self.presets['default_biome'] = { + 'name': '默认生物群落', + 'category': 'biome', + 'description': '默认的生物群落分布参数', + 'parameters': { + 'humidity_scale': 20.0, + 'temperature_scale': 20.0, + 'height_influence': 0.3, + 'humidity_influence': 0.4, + 'temperature_influence': 0.3 + }, + 'timestamp': time.time(), + 'is_default': True + } + + # 创建默认侵蚀预设 + self.presets['default_erosion'] = { + 'name': '默认侵蚀', + 'category': 'erosion', + 'description': '默认的侵蚀效果参数', + 'parameters': { + 'water_erosion_rate': 0.1, + 'thermal_erosion_rate': 0.05, + 'wind_erosion_rate': 0.02 + }, + 'timestamp': time.time(), + 'is_default': True + } + + # 创建默认植被预设 + self.presets['default_vegetation'] = { + 'name': '默认植被', + 'category': 'vegetation', + 'description': '默认的植被分布参数', + 'parameters': { + 'density_scale': 10.0, + 'clumping_factor': 0.7, + 'slope_tolerance': 0.3 + }, + 'timestamp': time.time(), + 'is_default': True + } + + # 创建默认编辑器预设 + self.presets['default_editor'] = { + 'name': '默认编辑器', + 'category': 'editor', + 'description': '默认的编辑器配置', + 'parameters': { + 'brush_size': 10, + 'brush_strength': 0.5, + 'preview_enabled': True, + 'realtime_update': True + }, + 'timestamp': time.time(), + 'is_default': True + } + + print("✓ 默认预设加载完成") + + except Exception as e: + print(f"✗ 默认预设加载失败: {e}") + import traceback + traceback.print_exc() + + def _load_user_presets(self): + """加载用户预设""" + try: + load_start_time = time.time() + + presets_dir = self.preset_config['presets_directory'] + if not os.path.exists(presets_dir): + print("✓ 用户预设目录不存在,跳过加载") + return + + # 遍历预设目录 + for filename in os.listdir(presets_dir): + if filename.endswith('.json'): + try: + filepath = os.path.join(presets_dir, filename) + with open(filepath, 'r', encoding='utf-8') as f: + preset_data = json.load(f) + + # 使用文件名(不含扩展名)作为预设ID + preset_id = os.path.splitext(filename)[0] + self.presets[preset_id] = preset_data + + except Exception as e: + print(f"✗ 预设文件加载失败 {filename}: {e}") + + load_time = time.time() - load_start_time + self.stats['presets_loaded'] = len(self.presets) + self.stats['total_load_time'] = load_time + + print(f"✓ 用户预设加载完成,共加载 {len(self.presets)} 个预设,耗时: {load_time:.3f}秒") + + except Exception as e: + print(f"✗ 用户预设加载失败: {e}") + import traceback + traceback.print_exc() + + def save_preset(self, preset_id: str, preset_data: Dict[str, Any] = None) -> bool: + """ + 保存预设 + + Args: + preset_id: 预设ID + preset_data: 预设数据(如果为None,则保存当前配置) + + Returns: + 是否保存成功 + """ + try: + save_start_time = time.time() + + if not self.enabled: + print("✗ 预设管理器未启用") + return False + + # 如果未提供预设数据,则创建当前配置的快照 + if preset_data is None: + preset_data = self._create_current_preset_snapshot(preset_id) + + # 保存预设数据 + self.presets[preset_id] = preset_data + + # 保存到文件 + presets_dir = self.preset_config['presets_directory'] + filepath = os.path.join(presets_dir, f"{preset_id}.json") + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(preset_data, f, ensure_ascii=False, indent=2) + + # 更新统计信息 + save_time = time.time() - save_start_time + self.stats['presets_saved'] += 1 + self.stats['total_save_time'] += save_time + + print(f"✓ 预设 '{preset_id}' 保存完成,耗时: {save_time:.3f}秒") + return True + + except Exception as e: + print(f"✗ 预设保存失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_current_preset_snapshot(self, preset_id: str) -> Dict[str, Any]: + """ + 创建当前配置的预设快照 + + Args: + preset_id: 预设ID + + Returns: + 预设数据字典 + """ + try: + # 获取当前插件配置 + preset_data = { + 'name': preset_id, + 'category': 'terrain', # 默认类别 + 'description': '用户自定义预设', + 'parameters': {}, + 'timestamp': time.time(), + 'is_default': False + } + + # 添加地形管理器配置 + if self.plugin.terrain_manager: + tm = self.plugin.terrain_manager + preset_data['parameters'].update({ + 'terrain_size': tm.terrain_size, + 'terrain_scale': tm.terrain_scale, + 'height_scale': tm.height_scale + }) + + # 添加噪声生成器配置 + if self.plugin.noise_generator: + ng = self.plugin.noise_generator + preset_data['parameters'].update({ + 'noise_seed': ng.seed, + 'noise_scale': ng.fractal_params.get('scale', 50.0), + 'noise_octaves': ng.fractal_params.get('octaves', 6), + 'noise_persistence': ng.fractal_params.get('persistence', 0.5), + 'noise_lacunarity': ng.fractal_params.get('lacunarity', 2.0) + }) + + # 添加高度图生成器配置 + if self.plugin.heightmap_generator: + hg = self.plugin.heightmap_generator + preset_data['parameters'].update({ + 'heightmap_scale': hg.scale, + 'heightmap_octaves': hg.octaves, + 'heightmap_persistence': hg.persistence, + 'heightmap_lacunarity': hg.lacunarity + }) + + # 添加生物群落生成器配置 + if self.plugin.biome_generator: + bg = self.plugin.biome_generator + preset_data['parameters'].update({ + 'biome_seed': bg.seed, + 'humidity_scale': bg.distribution_params.get('humidity_scale', 20.0), + 'temperature_scale': bg.distribution_params.get('temperature_scale', 20.0) + }) + + # 添加侵蚀处理器配置 + if self.plugin.erosion_processor: + ep = self.plugin.erosion_processor + preset_data['parameters'].update({ + 'erosion_seed': ep.seed, + 'water_erosion_rate': ep.erosion_params['water_erosion'].get('rate', 0.1), + 'thermal_erosion_rate': ep.erosion_params['thermal_erosion'].get('rate', 0.05), + 'wind_erosion_rate': ep.erosion_params['wind_erosion'].get('rate', 0.02) + }) + + # 添加植被生成器配置 + if self.plugin.vegetation_generator: + vg = self.plugin.vegetation_generator + preset_data['parameters'].update({ + 'vegetation_seed': vg.seed, + 'density_scale': vg.vegetation_params.get('density_scale', 10.0), + 'clumping_factor': vg.vegetation_params.get('clumping_factor', 0.7) + }) + + return preset_data + + except Exception as e: + print(f"✗ 预设快照创建失败: {e}") + import traceback + traceback.print_exc() + return { + 'name': preset_id, + 'category': 'terrain', + 'description': '默认预设', + 'parameters': {}, + 'timestamp': time.time(), + 'is_default': False + } + + def load_preset(self, preset_id: str) -> Optional[Dict[str, Any]]: + """ + 加载预设 + + Args: + preset_id: 预设ID + + Returns: + 预设数据字典或None + """ + try: + if not self.enabled: + print("✗ 预设管理器未启用") + return None + + if preset_id not in self.presets: + print(f"✗ 预设不存在: {preset_id}") + return None + + preset_data = self.presets[preset_id] + + # 更新统计信息 + self.stats['presets_applied'] += 1 + self.current_preset = preset_id + + print(f"✓ 预设 '{preset_id}' 加载完成") + return preset_data + + except Exception as e: + print(f"✗ 预设加载失败: {e}") + import traceback + traceback.print_exc() + return None + + def apply_preset(self, preset_id: str) -> bool: + """ + 应用预设到当前配置 + + Args: + preset_id: 预设ID + + Returns: + 是否应用成功 + """ + try: + if not self.enabled: + print("✗ 预设管理器未启用") + return False + + preset_data = self.load_preset(preset_id) + if preset_data is None: + return False + + parameters = preset_data.get('parameters', {}) + + # 应用参数到各个组件 + self._apply_parameters_to_components(parameters) + + # 更新统计信息 + self.stats['presets_applied'] += 1 + self.current_preset = preset_id + + print(f"✓ 预设 '{preset_id}' 应用完成") + return True + + except Exception as e: + print(f"✗ 预设应用失败: {e}") + import traceback + traceback.print_exc() + return False + + def _apply_parameters_to_components(self, parameters: Dict[str, Any]): + """ + 应用参数到各个组件 + + Args: + parameters: 参数字典 + """ + try: + # 应用到地形管理器 + if self.plugin.terrain_manager and 'terrain_size' in parameters: + self.plugin.terrain_manager.terrain_size = parameters['terrain_size'] + if self.plugin.terrain_manager and 'terrain_scale' in parameters: + self.plugin.terrain_manager.terrain_scale = parameters['terrain_scale'] + if self.plugin.terrain_manager and 'height_scale' in parameters: + self.plugin.terrain_manager.height_scale = parameters['height_scale'] + + # 应用到噪声生成器 + if self.plugin.noise_generator and 'noise_seed' in parameters: + self.plugin.noise_generator.set_seed(parameters['noise_seed']) + if self.plugin.noise_generator and 'noise_scale' in parameters: + self.plugin.noise_generator.set_fractal_parameters( + scale=parameters['noise_scale'] + ) + + # 应用到高度图生成器 + if self.plugin.heightmap_generator and 'heightmap_scale' in parameters: + self.plugin.heightmap_generator.set_scale(parameters['heightmap_scale']) + if self.plugin.heightmap_generator and 'heightmap_octaves' in parameters: + self.plugin.heightmap_generator.set_octaves(parameters['heightmap_octaves']) + + # 应用到生物群落生成器 + if self.plugin.biome_generator and 'biome_seed' in parameters: + self.plugin.biome_generator.set_seed(parameters['biome_seed']) + + # 应用到侵蚀处理器 + if self.plugin.erosion_processor and 'erosion_seed' in parameters: + self.plugin.erosion_processor.set_seed(parameters['erosion_seed']) + + # 应用到植被生成器 + if self.plugin.vegetation_generator and 'vegetation_seed' in parameters: + self.plugin.vegetation_generator.set_seed(parameters['vegetation_seed']) + + # 应用到地形编辑器 + if (self.plugin.terrain_editor and + hasattr(self.plugin, 'terrain_editor') and + self.plugin.terrain_editor): + editor = self.plugin.terrain_editor + if 'brush_size' in parameters: + editor.set_brush_size(parameters['brush_size']) + if 'brush_strength' in parameters: + editor.set_brush_strength(parameters['brush_strength']) + + except Exception as e: + print(f"✗ 参数应用失败: {e}") + import traceback + traceback.print_exc() + + def delete_preset(self, preset_id: str) -> bool: + """ + 删除预设 + + Args: + preset_id: 预设ID + + Returns: + 是否删除成功 + """ + try: + if not self.enabled: + print("✗ 预设管理器未启用") + return False + + if preset_id not in self.presets: + print(f"✗ 预设不存在: {preset_id}") + return False + + # 检查是否为默认预设 + if self.presets[preset_id].get('is_default', False): + print(f"✗ 无法删除默认预设: {preset_id}") + return False + + # 从内存中删除 + del self.presets[preset_id] + + # 从文件系统中删除 + presets_dir = self.preset_config['presets_directory'] + filepath = os.path.join(presets_dir, f"{preset_id}.json") + if os.path.exists(filepath): + os.remove(filepath) + + # 如果删除的是当前预设,清空当前预设 + if self.current_preset == preset_id: + self.current_preset = None + + print(f"✓ 预设 '{preset_id}' 删除完成") + return True + + except Exception as e: + print(f"✗ 预设删除失败: {e}") + import traceback + traceback.print_exc() + return False + + def list_presets(self, category: str = None) -> List[str]: + """ + 列出预设 + + Args: + category: 预设类别(如果为None,则列出所有预设) + + Returns: + 预设ID列表 + """ + try: + if category is None: + return list(self.presets.keys()) + else: + return [preset_id for preset_id, preset_data in self.presets.items() + if preset_data.get('category') == category] + + except Exception as e: + print(f"✗ 预设列表获取失败: {e}") + import traceback + traceback.print_exc() + return [] + + def get_preset_info(self, preset_id: str) -> Optional[Dict[str, Any]]: + """ + 获取预设信息 + + Args: + preset_id: 预设ID + + Returns: + 预设信息字典或None + """ + try: + if preset_id not in self.presets: + return None + + preset_data = self.presets[preset_id] + return { + 'name': preset_data.get('name', preset_id), + 'category': preset_data.get('category', 'unknown'), + 'description': preset_data.get('description', ''), + 'timestamp': preset_data.get('timestamp', 0), + 'is_default': preset_data.get('is_default', False), + 'parameters_count': len(preset_data.get('parameters', {})) + } + + except Exception as e: + print(f"✗ 预设信息获取失败: {e}") + import traceback + traceback.print_exc() + return None + + def get_preset_categories(self) -> Dict[str, Dict[str, str]]: + """ + 获取预设类别 + + Returns: + 预设类别字典 + """ + return self.preset_categories.copy() + + def create_preset_category(self, category_id: str, name: str, description: str = '') -> bool: + """ + 创建预设类别 + + Args: + category_id: 类别ID + name: 类别名称 + description: 类别描述 + + Returns: + 是否创建成功 + """ + try: + self.preset_categories[category_id] = { + 'name': name, + 'description': description + } + + print(f"✓ 预设类别 '{name}' 创建完成") + return True + + except Exception as e: + print(f"✗ 预设类别创建失败: {e}") + import traceback + traceback.print_exc() + return False + + def export_preset(self, preset_id: str, filepath: str) -> bool: + """ + 导出预设到文件 + + Args: + preset_id: 预设ID + filepath: 导出文件路径 + + Returns: + 是否导出成功 + """ + try: + if preset_id not in self.presets: + print(f"✗ 预设不存在: {preset_id}") + return False + + preset_data = self.presets[preset_id] + + # 保存到指定文件 + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(preset_data, f, ensure_ascii=False, indent=2) + + print(f"✓ 预设 '{preset_id}' 导出到 {filepath}") + return True + + except Exception as e: + print(f"✗ 预设导出失败: {e}") + import traceback + traceback.print_exc() + return False + + def import_preset(self, filepath: str, preset_id: str = None) -> bool: + """ + 从文件导入预设 + + Args: + filepath: 导入文件路径 + preset_id: 预设ID(如果为None,则使用文件名) + + Returns: + 是否导入成功 + """ + try: + # 读取预设文件 + with open(filepath, 'r', encoding='utf-8') as f: + preset_data = json.load(f) + + # 确定预设ID + if preset_id is None: + preset_id = os.path.splitext(os.path.basename(filepath))[0] + + # 保存预设 + self.presets[preset_id] = preset_data + + # 保存到预设目录 + presets_dir = self.preset_config['presets_directory'] + save_filepath = os.path.join(presets_dir, f"{preset_id}.json") + + with open(save_filepath, 'w', encoding='utf-8') as f: + json.dump(preset_data, f, ensure_ascii=False, indent=2) + + print(f"✓ 预设从 {filepath} 导入完成,ID: {preset_id}") + return True + + except Exception as e: + print(f"✗ 预设导入失败: {e}") + import traceback + traceback.print_exc() + return False + + def set_preset_config(self, config: Dict[str, Any]): + """ + 设置预设配置 + + Args: + config: 配置字典 + """ + self.preset_config.update(config) + print(f"✓ 预设配置已更新: {self.preset_config}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'presets_loaded': 0, + 'presets_saved': 0, + 'presets_applied': 0, + 'total_load_time': 0.0, + 'total_save_time': 0.0 + } + print("✓ 预设管理器统计信息已重置") + + def get_current_preset(self) -> Optional[str]: + """ + 获取当前预设 + + Returns: + 当前预设ID或None + """ + return self.current_preset + + def set_current_preset(self, preset_id: str): + """ + 设置当前预设 + + Args: + preset_id: 预设ID + """ + if preset_id in self.presets: + self.current_preset = preset_id + print(f"✓ 当前预设设置为: {preset_id}") + else: + print(f"✗ 预设不存在: {preset_id}") + + def create_terrain_preset_from_current(self, preset_name: str, description: str = '') -> str: + """ + 从当前地形创建预设 + + Args: + preset_name: 预设名称 + description: 预设描述 + + Returns: + 预设ID + """ + try: + # 生成预设ID + preset_id = preset_name.lower().replace(' ', '_') + + # 创建预设数据 + preset_data = { + 'name': preset_name, + 'category': 'terrain', + 'description': description, + 'parameters': {}, + 'timestamp': time.time(), + 'is_default': False + } + + # 获取当前地形参数 + if self.plugin.terrain_manager: + tm = self.plugin.terrain_manager + preset_data['parameters'].update({ + 'terrain_size': tm.terrain_size, + 'terrain_scale': tm.terrain_scale, + 'height_scale': tm.height_scale, + 'seed': getattr(tm, 'seed', 12345) + }) + + # 保存预设 + self.save_preset(preset_id, preset_data) + + print(f"✓ 从当前地形创建预设: {preset_name}") + return preset_id + + except Exception as e: + print(f"✗ 从当前地形创建预设失败: {e}") + import traceback + traceback.print_exc() + return '' + + def batch_apply_presets(self, preset_ids: List[str]) -> bool: + """ + 批量应用预设 + + Args: + preset_ids: 预设ID列表 + + Returns: + 是否应用成功 + """ + try: + if not self.enabled: + print("✗ 预设管理器未启用") + return False + + success_count = 0 + for preset_id in preset_ids: + if self.apply_preset(preset_id): + success_count += 1 + + print(f"✓ 批量应用预设完成: {success_count}/{len(preset_ids)} 个预设应用成功") + return success_count == len(preset_ids) + + except Exception as e: + print(f"✗ 批量应用预设失败: {e}") + import traceback + traceback.print_exc() + return False + + def search_presets(self, keyword: str, category: str = None) -> List[str]: + """ + 搜索预设 + + Args: + keyword: 搜索关键词 + category: 预设类别(可选) + + Returns: + 匹配的预设ID列表 + """ + try: + matching_presets = [] + + for preset_id, preset_data in self.presets.items(): + # 检查类别筛选 + if category and preset_data.get('category') != category: + continue + + # 检查关键词匹配 + if (keyword.lower() in preset_data.get('name', '').lower() or + keyword.lower() in preset_data.get('description', '').lower()): + matching_presets.append(preset_id) + + return matching_presets + + except Exception as e: + print(f"✗ 预设搜索失败: {e}") + import traceback + traceback.print_exc() + return [] + + def duplicate_preset(self, source_preset_id: str, new_preset_id: str, new_name: str = None) -> bool: + """ + 复制预设 + + Args: + source_preset_id: 源预设ID + new_preset_id: 新预设ID + new_name: 新预设名称(如果为None,则使用源预设名称) + + Returns: + 是否复制成功 + """ + try: + if source_preset_id not in self.presets: + print(f"✗ 源预设不存在: {source_preset_id}") + return False + + # 复制预设数据 + preset_data = self.presets[source_preset_id].copy() + + # 更新名称 + if new_name: + preset_data['name'] = new_name + else: + preset_data['name'] = f"{preset_data['name']} (副本)" + + # 更新时间戳 + preset_data['timestamp'] = time.time() + + # 标记为非默认预设 + preset_data['is_default'] = False + + # 保存新预设 + return self.save_preset(new_preset_id, preset_data) + + except Exception as e: + print(f"✗ 预设复制失败: {e}") + import traceback + traceback.print_exc() + return False + + def update_preset_parameters(self, preset_id: str, parameters: Dict[str, Any]) -> bool: + """ + 更新预设参数 + + Args: + preset_id: 预设ID + parameters: 要更新的参数 + + Returns: + 是否更新成功 + """ + try: + if preset_id not in self.presets: + print(f"✗ 预设不存在: {preset_id}") + return False + + # 更新参数 + self.presets[preset_id]['parameters'].update(parameters) + + # 更新时间戳 + self.presets[preset_id]['timestamp'] = time.time() + + # 保存到文件 + presets_dir = self.preset_config['presets_directory'] + filepath = os.path.join(presets_dir, f"{preset_id}.json") + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(self.presets[preset_id], f, ensure_ascii=False, indent=2) + + print(f"✓ 预设 '{preset_id}' 参数更新完成") + return True + + except Exception as e: + print(f"✗ 预设参数更新失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_preset_parameters(self, preset_id: str) -> Optional[Dict[str, Any]]: + """ + 获取预设参数 + + Args: + preset_id: 预设ID + + Returns: + 参数字典或None + """ + try: + if preset_id not in self.presets: + return None + + return self.presets[preset_id].get('parameters', {}).copy() + + except Exception as e: + print(f"✗ 预设参数获取失败: {e}") + import traceback + traceback.print_exc() + return None + + def validate_preset(self, preset_id: str) -> bool: + """ + 验证预设完整性 + + Args: + preset_id: 预设ID + + Returns: + 预设是否有效 + """ + try: + if preset_id not in self.presets: + return False + + preset_data = self.presets[preset_id] + + # 检查必需字段 + required_fields = ['name', 'category', 'parameters'] + for field in required_fields: + if field not in preset_data: + print(f"✗ 预设 '{preset_id}' 缺少必需字段: {field}") + return False + + # 检查类别有效性 + if preset_data['category'] not in self.preset_categories: + print(f"✗ 预设 '{preset_id}' 包含无效类别: {preset_data['category']}") + return False + + return True + + except Exception as e: + print(f"✗ 预设验证失败: {e}") + import traceback + traceback.print_exc() + return False + + def backup_presets(self, backup_dir: str = 'presets_backup') -> bool: + """ + 备份所有预设 + + Args: + backup_dir: 备份目录 + + Returns: + 是否备份成功 + """ + try: + if not os.path.exists(backup_dir): + os.makedirs(backup_dir) + + backup_count = 0 + for preset_id, preset_data in self.presets.items(): + try: + filepath = os.path.join(backup_dir, f"{preset_id}.json") + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(preset_data, f, ensure_ascii=False, indent=2) + backup_count += 1 + except Exception as e: + print(f"✗ 预设 '{preset_id}' 备份失败: {e}") + + print(f"✓ 预设备份完成: {backup_count}/{len(self.presets)} 个预设已备份到 {backup_dir}") + return True + + except Exception as e: + print(f"✗ 预设备份失败: {e}") + import traceback + traceback.print_exc() + return False + + def restore_presets(self, backup_dir: str = 'presets_backup') -> bool: + """ + 从备份恢复预设 + + Args: + backup_dir: 备份目录 + + Returns: + 是否恢复成功 + """ + try: + if not os.path.exists(backup_dir): + print(f"✗ 备份目录不存在: {backup_dir}") + return False + + restore_count = 0 + for filename in os.listdir(backup_dir): + if filename.endswith('.json'): + try: + filepath = os.path.join(backup_dir, filename) + with open(filepath, 'r', encoding='utf-8') as f: + preset_data = json.load(f) + + preset_id = os.path.splitext(filename)[0] + self.presets[preset_id] = preset_data + restore_count += 1 + except Exception as e: + print(f"✗ 预设文件 '{filename}' 恢复失败: {e}") + + print(f"✓ 预设恢复完成: {restore_count} 个预设已从 {backup_dir} 恢复") + return True + + except Exception as e: + print(f"✗ 预设恢复失败: {e}") + import traceback + traceback.print_exc() return False \ No newline at end of file diff --git a/plugins/user/procedural_terrain_generation/editor/terrain_editor.py b/plugins/user/procedural_terrain_generation/editor/terrain_editor.py index 2ab63f58..b78e7aca 100644 --- a/plugins/user/procedural_terrain_generation/editor/terrain_editor.py +++ b/plugins/user/procedural_terrain_generation/editor/terrain_editor.py @@ -1,1538 +1,1538 @@ -""" -地形编辑器 -提供可视化界面用于编辑和调整程序化生成的地形 -""" - -import numpy as np -import math -from typing import Dict, Any, List, Tuple, Optional -import json -import time - -class TerrainEditor: - """ - 地形编辑器 - 提供可视化界面用于编辑和调整程序化生成的地形,包括参数调整、实时预览、笔刷工具等功能 - """ - - def __init__(self, plugin): - """ - 初始化地形编辑器 - - Args: - plugin: 程序化地形生成插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 编辑器配置 - self.editor_params = { - 'brush_size': 10, - 'brush_strength': 0.5, - 'brush_type': 'raise', # 'raise', 'lower', 'smooth', 'flatten', 'noise' - 'edit_mode': 'terrain', # 'terrain', 'biome', 'vegetation', 'water' - 'preview_enabled': True, - 'realtime_update': True, - 'history_limit': 50 - } - - # 笔刷类型定义 - self.brush_types = { - 'raise': {'name': '抬高', 'description': '抬高地形'}, - 'lower': {'name': '降低', 'description': '降低地形'}, - 'smooth': {'name': '平滑', 'description': '平滑地形'}, - 'flatten': {'name': '平整', 'description': '将地形平整到指定高度'}, - 'noise': {'name': '噪声', 'description': '添加噪声效果'}, - 'erode': {'name': '侵蚀', 'description': '模拟侵蚀效果'}, - 'paint': {'name': '绘制', 'description': '绘制生物群落或植被'} - } - - # 编辑历史 - self.edit_history = [] - self.history_position = -1 - - # 当前选择 - self.selected_area = None - self.selected_brush_position = None - - # 预览数据 - self.preview_data = None - self.preview_dirty = False - - # 工具状态 - self.is_painting = False - self.last_paint_position = None - - # 统计信息 - self.stats = { - 'edits_made': 0, - 'undos_performed': 0, - 'redos_performed': 0, - 'total_edit_time': 0.0, - 'average_edit_time': 0.0 - } - - # UI组件引用(在实际实现中会连接到GUI) - self.ui_components = { - 'toolbar': None, - 'properties_panel': None, - 'preview_window': None, - 'brush_settings': None, - 'history_panel': None - } - - print("✓ 地形编辑器已创建") - - def initialize(self) -> bool: - """ - 初始化地形编辑器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 地形编辑器初始化完成") - return True - - except Exception as e: - print(f"✗ 地形编辑器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用地形编辑器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 地形编辑器未初始化") - return False - - self.enabled = True - print("✓ 地形编辑器已启用") - return True - - except Exception as e: - print(f"✗ 地形编辑器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用地形编辑器""" - try: - self.enabled = False - print("✓ 地形编辑器已禁用") - - except Exception as e: - print(f"✗ 地形编辑器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理地形编辑器资源""" - try: - self.disable() - self.initialized = False - print("✓ 地形编辑器资源已清理") - - except Exception as e: - print(f"✗ 地形编辑器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新地形编辑器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新预览 - if self.preview_dirty and self.editor_params['preview_enabled']: - self._update_preview() - - # 处理实时更新 - if self.editor_params['realtime_update']: - self._handle_realtime_updates() - - except Exception as e: - print(f"✗ 地形编辑器更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_preview(self): - """更新预览显示""" - try: - # 在实际实现中,这里会更新预览窗口 - self.preview_dirty = False - except Exception as e: - print(f"✗ 预览更新失败: {e}") - - def _handle_realtime_updates(self): - """处理实时更新""" - try: - # 在实际实现中,这里会处理实时编辑更新 - pass - except Exception as e: - print(f"✗ 实时更新处理失败: {e}") - - def set_brush_size(self, size: int): - """ - 设置笔刷大小 - - Args: - size: 笔刷大小(像素) - """ - self.editor_params['brush_size'] = max(1, size) - print(f"✓ 笔刷大小设置为: {size}") - - def set_brush_strength(self, strength: float): - """ - 设置笔刷强度 - - Args: - strength: 笔刷强度 (0.0-1.0) - """ - self.editor_params['brush_strength'] = max(0.0, min(1.0, strength)) - print(f"✓ 笔刷强度设置为: {strength}") - - def set_brush_type(self, brush_type: str): - """ - 设置笔刷类型 - - Args: - brush_type: 笔刷类型 - """ - if brush_type in self.brush_types: - self.editor_params['brush_type'] = brush_type - print(f"✓ 笔刷类型设置为: {self.brush_types[brush_type]['name']}") - else: - print(f"✗ 无效的笔刷类型: {brush_type}") - - def set_edit_mode(self, mode: str): - """ - 设置编辑模式 - - Args: - mode: 编辑模式 ('terrain', 'biome', 'vegetation', 'water') - """ - valid_modes = ['terrain', 'biome', 'vegetation', 'water'] - if mode in valid_modes: - self.editor_params['edit_mode'] = mode - print(f"✓ 编辑模式设置为: {mode}") - else: - print(f"✗ 无效的编辑模式: {mode}") - - def set_preview_enabled(self, enabled: bool): - """ - 设置预览是否启用 - - Args: - enabled: 是否启用预览 - """ - self.editor_params['preview_enabled'] = enabled - print(f"✓ 预览{'已启用' if enabled else '已禁用'}") - - def set_realtime_update(self, enabled: bool): - """ - 设置实时更新是否启用 - - Args: - enabled: 是否启用实时更新 - """ - self.editor_params['realtime_update'] = enabled - print(f"✓ 实时更新{'已启用' if enabled else '已禁用'}") - - def begin_paint(self, x: int, y: int): - """ - 开始绘制操作 - - Args: - x: X坐标 - y: Y坐标 - """ - try: - if not self.enabled: - print("✗ 地形编辑器未启用") - return - - self.is_painting = True - self.last_paint_position = (x, y) - - # 保存当前状态到历史记录 - self._save_state_to_history() - - # 执行绘制操作 - self._apply_brush(x, y) - - except Exception as e: - print(f"✗ 开始绘制失败: {e}") - import traceback - traceback.print_exc() - - def continue_paint(self, x: int, y: int): - """ - 继续绘制操作 - - Args: - x: X坐标 - y: Y坐标 - """ - try: - if not self.enabled or not self.is_painting: - return - - # 如果位置发生变化,执行绘制 - if self.last_paint_position != (x, y): - self._apply_brush_stroke(self.last_paint_position, (x, y)) - self.last_paint_position = (x, y) - - except Exception as e: - print(f"✗ 绘制过程失败: {e}") - import traceback - traceback.print_exc() - - def end_paint(self): - """结束绘制操作""" - try: - self.is_painting = False - self.last_paint_position = None - - # 更新统计信息 - self.stats['edits_made'] += 1 - - except Exception as e: - print(f"✗ 结束绘制失败: {e}") - import traceback - traceback.print_exc() - - def _apply_brush(self, x: int, y: int): - """ - 应用笔刷效果 - - Args: - x: X坐标 - y: Y坐标 - """ - try: - brush_type = self.editor_params['brush_type'] - brush_size = self.editor_params['brush_size'] - brush_strength = self.editor_params['brush_strength'] - edit_mode = self.editor_params['edit_mode'] - - # 获取当前地形数据 - if self.plugin.terrain_manager: - terrain_data = self.plugin.terrain_manager.get_terrain_data() - - if edit_mode == 'terrain': - self._apply_terrain_brush(terrain_data, x, y, brush_type, brush_size, brush_strength) - elif edit_mode == 'biome': - self._apply_biome_brush(terrain_data, x, y, brush_type, brush_size, brush_strength) - elif edit_mode == 'vegetation': - self._apply_vegetation_brush(terrain_data, x, y, brush_type, brush_size, brush_strength) - elif edit_mode == 'water': - self._apply_water_brush(terrain_data, x, y, brush_type, brush_size, brush_strength) - - # 标记预览需要更新 - self.preview_dirty = True - - except Exception as e: - print(f"✗ 笔刷应用失败: {e}") - import traceback - traceback.print_exc() - - def _apply_brush_stroke(self, start_pos: Tuple[int, int], end_pos: Tuple[int, int]): - """ - 应用笔刷笔触(在两点之间绘制) - - Args: - start_pos: 起始位置 (x, y) - end_pos: 结束位置 (x, y) - """ - try: - # 使用直线算法在两点之间插值 - x0, y0 = start_pos - x1, y1 = end_pos - - # 计算距离 - distance = math.sqrt((x1 - x0)**2 + (y1 - y0)**2) - - # 如果距离很小,只应用一次 - if distance <= 1: - self._apply_brush(x1, y1) - return - - # 在两点之间插值 - steps = int(distance) - for i in range(steps + 1): - t = i / steps if steps > 0 else 0 - x = int(x0 + (x1 - x0) * t) - y = int(y0 + (y1 - y0) * t) - self._apply_brush(x, y) - - except Exception as e: - print(f"✗ 笔触应用失败: {e}") - import traceback - traceback.print_exc() - - def _apply_terrain_brush(self, terrain_data: Dict[str, Any], x: int, y: int, - brush_type: str, brush_size: int, brush_strength: float): - """ - 应用地形笔刷 - - Args: - terrain_data: 地形数据 - x: X坐标 - y: Y坐标 - brush_type: 笔刷类型 - brush_size: 笔刷大小 - brush_strength: 笔刷强度 - """ - try: - heightmap = terrain_data.get('heightmap') - if heightmap is None: - return - - height, width = heightmap.shape - - # 计算笔刷影响区域 - min_x = max(0, x - brush_size) - max_x = min(width - 1, x + brush_size) - min_y = max(0, y - brush_size) - max_y = min(height - 1, y + brush_size) - - # 创建笔刷形状(圆形) - brush_radius = brush_size - brush_radius_sq = brush_radius * brush_radius - - # 应用笔刷效果 - for by in range(min_y, max_y + 1): - for bx in range(min_x, max_x + 1): - # 计算到笔刷中心的距离 - dx = bx - x - dy = by - y - distance_sq = dx * dx + dy * dy - - # 如果在笔刷范围内 - if distance_sq <= brush_radius_sq: - # 计算笔刷影响强度(边缘递减) - distance = math.sqrt(distance_sq) - influence = max(0.0, 1.0 - distance / brush_radius) - effective_strength = brush_strength * influence - - # 根据笔刷类型应用不同效果 - if brush_type == 'raise': - heightmap[by, bx] = min(1.0, heightmap[by, bx] + effective_strength * 0.1) - elif brush_type == 'lower': - heightmap[by, bx] = max(0.0, heightmap[by, bx] - effective_strength * 0.1) - elif brush_type == 'smooth': - # 简单的平滑处理 - neighbors = [] - for ny in range(max(0, by - 1), min(height, by + 2)): - for nx in range(max(0, bx - 1), min(width, bx + 2)): - neighbors.append(heightmap[ny, nx]) - if neighbors: - avg_height = sum(neighbors) / len(neighbors) - heightmap[by, bx] = heightmap[by, bx] * (1 - effective_strength) + avg_height * effective_strength - elif brush_type == 'flatten': - # 平整到固定高度(这里使用0.5作为示例) - target_height = 0.5 - heightmap[by, bx] = heightmap[by, bx] * (1 - effective_strength) + target_height * effective_strength - elif brush_type == 'noise': - # 添加噪声 - noise = (np.random.random() - 0.5) * effective_strength * 0.2 - heightmap[by, bx] = max(0.0, min(1.0, heightmap[by, bx] + noise)) - elif brush_type == 'erode': - # 简单的侵蚀效果 - heightmap[by, bx] = max(0.0, heightmap[by, bx] - effective_strength * 0.05) - - except Exception as e: - print(f"✗ 地形笔刷应用失败: {e}") - import traceback - traceback.print_exc() - - def _apply_biome_brush(self, terrain_data: Dict[str, Any], x: int, y: int, - brush_type: str, brush_size: int, brush_strength: float): - """ - 应用生物群落笔刷 - - Args: - terrain_data: 地形数据 - x: X坐标 - y: Y坐标 - brush_type: 笔刷类型 - brush_size: 笔刷大小 - brush_strength: 笔刷强度 - """ - try: - biome_map = terrain_data.get('biome_map') - if biome_map is None: - return - - height, width = biome_map.shape - - # 计算笔刷影响区域 - min_x = max(0, x - brush_size) - max_x = min(width - 1, x + brush_size) - min_y = max(0, y - brush_size) - max_y = min(height - 1, y + brush_size) - - # 创建笔刷形状(圆形) - brush_radius = brush_size - brush_radius_sq = brush_radius * brush_radius - - # 应用笔刷效果 - for by in range(min_y, max_y + 1): - for bx in range(min_x, max_x + 1): - # 计算到笔刷中心的距离 - dx = bx - x - dy = by - y - distance_sq = dx * dx + dy * dy - - # 如果在笔刷范围内 - if distance_sq <= brush_radius_sq: - # 计算笔刷影响强度(边缘递减) - distance = math.sqrt(distance_sq) - influence = max(0.0, 1.0 - distance / brush_radius) - effective_strength = brush_strength * influence - - # 根据笔刷类型应用不同效果 - if brush_type == 'paint': - # 绘制生物群落(这里简单地设置为生物群落类型1) - if effective_strength > 0.5: # 只有强度足够时才改变 - biome_map[by, bx] = 1 # 设置为草原生物群落 - - except Exception as e: - print(f"✗ 生物群落笔刷应用失败: {e}") - import traceback - traceback.print_exc() - - def _apply_vegetation_brush(self, terrain_data: Dict[str, Any], x: int, y: int, - brush_type: str, brush_size: int, brush_strength: float): - """ - 应用植被笔刷 - - Args: - terrain_data: 地形数据 - x: X坐标 - y: Y坐标 - brush_type: 笔刷类型 - brush_size: 笔刷大小 - brush_strength: 笔刷强度 - """ - try: - vegetation_map = terrain_data.get('vegetation_map') - if vegetation_map is None: - return - - height, width = vegetation_map.shape - - # 计算笔刷影响区域 - min_x = max(0, x - brush_size) - max_x = min(width - 1, x + brush_size) - min_y = max(0, y - brush_size) - max_y = min(height - 1, y + brush_size) - - # 创建笔刷形状(圆形) - brush_radius = brush_size - brush_radius_sq = brush_radius * brush_radius - - # 应用笔刷效果 - for by in range(min_y, max_y + 1): - for bx in range(min_x, max_x + 1): - # 计算到笔刷中心的距离 - dx = bx - x - dy = by - y - distance_sq = dx * dx + dy * dy - - # 如果在笔刷范围内 - if distance_sq <= brush_radius_sq: - # 计算笔刷影响强度(边缘递减) - distance = math.sqrt(distance_sq) - influence = max(0.0, 1.0 - distance / brush_radius) - effective_strength = brush_strength * influence - - # 根据笔刷类型应用不同效果 - if brush_type == 'paint': - # 绘制植被(这里简单地设置为植被类型1) - if effective_strength > 0.3: # 只有强度足够时才改变 - vegetation_map[by, bx] = 1 # 设置为草 - - except Exception as e: - print(f"✗ 植被笔刷应用失败: {e}") - import traceback - traceback.print_exc() - - def _apply_water_brush(self, terrain_data: Dict[str, Any], x: int, y: int, - brush_type: str, brush_size: int, brush_strength: float): - """ - 应用水笔刷 - - Args: - terrain_data: 地形数据 - x: X坐标 - y: Y坐标 - brush_type: 笔刷类型 - brush_size: 笔刷大小 - brush_strength: 笔刷强度 - """ - try: - water_map = terrain_data.get('water_map') - if water_map is None: - return - - height, width = water_map.shape - - # 计算笔刷影响区域 - min_x = max(0, x - brush_size) - max_x = min(width - 1, x + brush_size) - min_y = max(0, y - brush_size) - max_y = min(height - 1, y + brush_size) - - # 创建笔刷形状(圆形) - brush_radius = brush_size - brush_radius_sq = brush_radius * brush_radius - - # 应用笔刷效果 - for by in range(min_y, max_y + 1): - for bx in range(min_x, max_x + 1): - # 计算到笔刷中心的距离 - dx = bx - x - dy = by - y - distance_sq = dx * dx + dy * dy - - # 如果在笔刷范围内 - if distance_sq <= brush_radius_sq: - # 计算笔刷影响强度(边缘递减) - distance = math.sqrt(distance_sq) - influence = max(0.0, 1.0 - distance / brush_radius) - effective_strength = brush_strength * influence - - # 根据笔刷类型应用不同效果 - if brush_type == 'raise': - water_map[by, bx] = min(1.0, water_map[by, bx] + effective_strength * 0.1) - elif brush_type == 'lower': - water_map[by, bx] = max(0.0, water_map[by, bx] - effective_strength * 0.1) - elif brush_type == 'paint': - # 设置固定水位 - water_map[by, bx] = 0.3 # 设置为中等水位 - - except Exception as e: - print(f"✗ 水笔刷应用失败: {e}") - import traceback - traceback.print_exc() - - def _save_state_to_history(self): - """保存当前状态到历史记录""" - try: - if self.plugin.terrain_manager: - terrain_data = self.plugin.terrain_manager.get_terrain_data() - - # 创建状态快照 - state_snapshot = { - 'timestamp': time.time(), - 'heightmap': terrain_data.get('heightmap', np.array([])).copy() if terrain_data.get('heightmap') is not None else np.array([]), - 'biome_map': terrain_data.get('biome_map', np.array([])).copy() if terrain_data.get('biome_map') is not None else np.array([]), - 'vegetation_map': terrain_data.get('vegetation_map', np.array([])).copy() if terrain_data.get('vegetation_map') is not None else np.array([]), - 'water_map': terrain_data.get('water_map', np.array([])).copy() if terrain_data.get('water_map') is not None else np.array([]) - } - - # 添加到历史记录 - if self.history_position < len(self.edit_history) - 1: - # 如果当前不在历史记录末尾,截断后续记录 - self.edit_history = self.edit_history[:self.history_position + 1] - - self.edit_history.append(state_snapshot) - self.history_position += 1 - - # 限制历史记录数量 - if len(self.edit_history) > self.editor_params['history_limit']: - self.edit_history.pop(0) - self.history_position -= 1 - - except Exception as e: - print(f"✗ 状态保存失败: {e}") - import traceback - traceback.print_exc() - - def undo(self) -> bool: - """ - 撤销操作 - - Returns: - 是否撤销成功 - """ - try: - if not self.enabled: - print("✗ 地形编辑器未启用") - return False - - if self.history_position <= 0: - print("✗ 没有可撤销的操作") - return False - - # 移动到前一个历史状态 - self.history_position -= 1 - - # 恢复状态 - self._restore_state_from_history() - - # 更新统计信息 - self.stats['undos_performed'] += 1 - - print("✓ 撤销操作完成") - return True - - except Exception as e: - print(f"✗ 撤销操作失败: {e}") - import traceback - traceback.print_exc() - return False - - def redo(self) -> bool: - """ - 重做操作 - - Returns: - 是否重做成功 - """ - try: - if not self.enabled: - print("✗ 地形编辑器未启用") - return False - - if self.history_position >= len(self.edit_history) - 1: - print("✗ 没有可重做的操作") - return False - - # 移动到下一个历史状态 - self.history_position += 1 - - # 恢复状态 - self._restore_state_from_history() - - # 更新统计信息 - self.stats['redos_performed'] += 1 - - print("✓ 重做操作完成") - return True - - except Exception as e: - print(f"✗ 重做操作失败: {e}") - import traceback - traceback.print_exc() - return False - - def _restore_state_from_history(self): - """从历史记录恢复状态""" - try: - if not self.plugin.terrain_manager or self.history_position < 0: - return - - # 获取历史状态 - state = self.edit_history[self.history_position] - - # 恢复地形数据 - terrain_data = self.plugin.terrain_manager.get_terrain_data() - - if 'heightmap' in state and state['heightmap'].size > 0: - terrain_data['heightmap'] = state['heightmap'] - - if 'biome_map' in state and state['biome_map'].size > 0: - terrain_data['biome_map'] = state['biome_map'] - - if 'vegetation_map' in state and state['vegetation_map'].size > 0: - terrain_data['vegetation_map'] = state['vegetation_map'] - - if 'water_map' in state and state['water_map'].size > 0: - terrain_data['water_map'] = state['water_map'] - - # 标记预览需要更新 - self.preview_dirty = True - - except Exception as e: - print(f"✗ 状态恢复失败: {e}") - import traceback - traceback.print_exc() - - def clear_history(self): - """清空历史记录""" - try: - self.edit_history.clear() - self.history_position = -1 - print("✓ 历史记录已清空") - except Exception as e: - print(f"✗ 历史记录清空失败: {e}") - import traceback - traceback.print_exc() - - def select_area(self, x1: int, y1: int, x2: int, y2: int): - """ - 选择区域 - - Args: - x1: 起始X坐标 - y1: 起始Y坐标 - x2: 结束X坐标 - y2: 结束Y坐标 - """ - try: - if not self.enabled: - print("✗ 地形编辑器未启用") - return - - # 标准化坐标 - min_x = min(x1, x2) - max_x = max(x1, x2) - min_y = min(y1, y2) - max_y = max(y1, y2) - - self.selected_area = (min_x, min_y, max_x, max_y) - print(f"✓ 区域已选择: ({min_x}, {min_y}) to ({max_x}, {max_y})") - - except Exception as e: - print(f"✗ 区域选择失败: {e}") - import traceback - traceback.print_exc() - - def clear_selection(self): - """清除选择""" - try: - self.selected_area = None - print("✓ 选择已清除") - except Exception as e: - print(f"✗ 选择清除失败: {e}") - import traceback - traceback.print_exc() - - def fill_selection(self, fill_type: str = 'flatten', fill_value: float = 0.5): - """ - 填充选择区域 - - Args: - fill_type: 填充类型 ('flatten', 'noise', 'smooth') - fill_value: 填充值(用于flatten) - """ - try: - if not self.enabled: - print("✗ 地形编辑器未启用") - return - - if self.selected_area is None: - print("✗ 没有选择区域") - return - - # 保存状态到历史记录 - self._save_state_to_history() - - # 获取选择区域 - min_x, min_y, max_x, max_y = self.selected_area - - # 获取地形数据 - if self.plugin.terrain_manager: - terrain_data = self.plugin.terrain_manager.get_terrain_data() - heightmap = terrain_data.get('heightmap') - - if heightmap is not None: - # 填充区域 - for y in range(min_y, max_y + 1): - for x in range(min_x, max_x + 1): - if fill_type == 'flatten': - heightmap[y, x] = fill_value - elif fill_type == 'noise': - heightmap[y, x] = np.random.random() - elif fill_type == 'smooth': - # 简单平滑处理 - neighbors = [] - h, w = heightmap.shape - for ny in range(max(0, y - 1), min(h, y + 2)): - for nx in range(max(0, x - 1), min(w, x + 2)): - neighbors.append(heightmap[ny, nx]) - if neighbors: - heightmap[y, x] = sum(neighbors) / len(neighbors) - - # 标记预览需要更新 - self.preview_dirty = True - - # 更新统计信息 - self.stats['edits_made'] += 1 - - print(f"✓ 区域填充完成: {fill_type}") - - except Exception as e: - print(f"✗ 区域填充失败: {e}") - import traceback - traceback.print_exc() - - def apply_filter_to_selection(self, filter_type: str = 'smooth', filter_strength: float = 1.0): - """ - 对选择区域应用滤镜 - - Args: - filter_type: 滤镜类型 ('smooth', 'sharpen', 'erode', 'dilate') - filter_strength: 滤镜强度 - """ - try: - if not self.enabled: - print("✗ 地形编辑器未启用") - return - - if self.selected_area is None: - print("✗ 没有选择区域") - return - - # 保存状态到历史记录 - self._save_state_to_history() - - # 获取选择区域 - min_x, min_y, max_x, max_y = self.selected_area - - # 获取地形数据 - if self.plugin.terrain_manager: - terrain_data = self.plugin.terrain_manager.get_terrain_data() - heightmap = terrain_data.get('heightmap') - - if heightmap is not None: - # 创建处理后的副本 - processed_heightmap = heightmap.copy() - - # 应用滤镜 - if filter_type == 'smooth': - self._apply_smoothing_filter(heightmap, processed_heightmap, min_x, min_y, max_x, max_y, filter_strength) - elif filter_type == 'sharpen': - self._apply_sharpening_filter(heightmap, processed_heightmap, min_x, min_y, max_x, max_y, filter_strength) - elif filter_type == 'erode': - self._apply_erosion_filter(heightmap, processed_heightmap, min_x, min_y, max_x, max_y, filter_strength) - elif filter_type == 'dilate': - self._apply_dilation_filter(heightmap, processed_heightmap, min_x, min_y, max_x, max_y, filter_strength) - - # 更新原高度图 - heightmap[min_y:max_y+1, min_x:max_x+1] = processed_heightmap[min_y:max_y+1, min_x:max_x+1] - - # 标记预览需要更新 - self.preview_dirty = True - - # 更新统计信息 - self.stats['edits_made'] += 1 - - print(f"✓ 滤镜应用完成: {filter_type}") - - except Exception as e: - print(f"✗ 滤镜应用失败: {e}") - import traceback - traceback.print_exc() - - def _apply_smoothing_filter(self, original: np.ndarray, processed: np.ndarray, - min_x: int, min_y: int, max_x: int, max_y: int, strength: float): - """应用平滑滤镜""" - try: - height, width = original.shape - - for y in range(min_y, max_y + 1): - for x in range(min_x, max_x + 1): - # 计算邻域平均值 - neighbors = [] - for dy in [-1, 0, 1]: - for dx in [-1, 0, 1]: - ny, nx = y + dy, x + dx - if 0 <= ny < height and 0 <= nx < width: - neighbors.append(original[ny, nx]) - - if neighbors: - avg_value = sum(neighbors) / len(neighbors) - processed[y, x] = original[y, x] * (1 - strength) + avg_value * strength - - except Exception as e: - print(f"✗ 平滑滤镜应用失败: {e}") - - def _apply_sharpening_filter(self, original: np.ndarray, processed: np.ndarray, - min_x: int, min_y: int, max_x: int, max_y: int, strength: float): - """应用锐化滤镜""" - try: - height, width = original.shape - - # 简单的锐化核 - kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]]) - - for y in range(max(min_y, 1), min(max_y + 1, height - 1)): - for x in range(max(min_x, 1), min(max_x + 1, width - 1)): - # 应用卷积 - value = 0.0 - for ky in [-1, 0, 1]: - for kx in [-1, 0, 1]: - value += original[y + ky, x + kx] * kernel[ky + 1, kx + 1] - - # 限制值范围并应用强度 - value = max(0.0, min(1.0, value)) - processed[y, x] = original[y, x] * (1 - strength) + value * strength - - except Exception as e: - print(f"✗ 锐化滤镜应用失败: {e}") - - def _apply_erosion_filter(self, original: np.ndarray, processed: np.ndarray, - min_x: int, min_y: int, max_x: int, max_y: int, strength: float): - """应用侵蚀滤镜""" - try: - height, width = original.shape - - for y in range(min_y, max_y + 1): - for x in range(min_x, max_x + 1): - # 简单的侵蚀:降低高度 - processed[y, x] = max(0.0, original[y, x] - 0.1 * strength) - - except Exception as e: - print(f"✗ 侵蚀滤镜应用失败: {e}") - - def _apply_dilation_filter(self, original: np.ndarray, processed: np.ndarray, - min_x: int, min_y: int, max_x: int, max_y: int, strength: float): - """应用膨胀滤镜""" - try: - height, width = original.shape - - for y in range(min_y, max_y + 1): - for x in range(min_x, max_x + 1): - # 简单的膨胀:增加高度 - processed[y, x] = min(1.0, original[y, x] + 0.1 * strength) - - except Exception as e: - print(f"✗ 膨胀滤镜应用失败: {e}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - # 更新平均编辑时间 - total_edits = self.stats['edits_made'] + self.stats['undos_performed'] + self.stats['redos_performed'] - if total_edits > 0: - self.stats['average_edit_time'] = self.stats['total_edit_time'] / total_edits - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'edits_made': 0, - 'undos_performed': 0, - 'redos_performed': 0, - 'total_edit_time': 0.0, - 'average_edit_time': 0.0 - } - print("✓ 地形编辑器统计信息已重置") - - def set_editor_parameters(self, params: Dict[str, Any]): - """ - 设置编辑器参数 - - Args: - params: 参数字典 - """ - self.editor_params.update(params) - print(f"✓ 编辑器参数已更新: {self.editor_params}") - - def get_brush_types(self) -> Dict[str, Dict[str, str]]: - """ - 获取可用的笔刷类型 - - Returns: - 笔刷类型字典 - """ - return self.brush_types.copy() - - def get_edit_modes(self) -> List[str]: - """ - 获取可用的编辑模式 - - Returns: - 编辑模式列表 - """ - return ['terrain', 'biome', 'vegetation', 'water'] - - def export_brush_preset(self, preset_name: str, filename: str) -> bool: - """ - 导出笔刷预设 - - Args: - preset_name: 预设名称 - filename: 文件名 - - Returns: - 是否导出成功 - """ - try: - # 创建预设数据 - preset_data = { - 'name': preset_name, - 'brush_size': self.editor_params['brush_size'], - 'brush_strength': self.editor_params['brush_strength'], - 'brush_type': self.editor_params['brush_type'], - 'edit_mode': self.editor_params['edit_mode'], - 'timestamp': time.time() - } - - # 保存为JSON文件 - with open(filename, 'w', encoding='utf-8') as f: - json.dump(preset_data, f, ensure_ascii=False, indent=2) - - print(f"✓ 笔刷预设已导出到: {filename}") - return True - - except Exception as e: - print(f"✗ 笔刷预设导出失败: {e}") - import traceback - traceback.print_exc() - return False - - def import_brush_preset(self, filename: str) -> bool: - """ - 导入笔刷预设 - - Args: - filename: 文件名 - - Returns: - 是否导入成功 - """ - try: - # 读取JSON文件 - with open(filename, 'r', encoding='utf-8') as f: - preset_data = json.load(f) - - # 应用预设 - if 'brush_size' in preset_data: - self.set_brush_size(preset_data['brush_size']) - if 'brush_strength' in preset_data: - self.set_brush_strength(preset_data['brush_strength']) - if 'brush_type' in preset_data: - self.set_brush_type(preset_data['brush_type']) - if 'edit_mode' in preset_data: - self.set_edit_mode(preset_data['edit_mode']) - - print(f"✓ 笔刷预设已从 {filename} 导入") - return True - - except Exception as e: - print(f"✗ 笔刷预设导入失败: {e}") - import traceback - traceback.print_exc() - return False - - def create_custom_brush(self, brush_name: str, brush_function: callable) -> bool: - """ - 创建自定义笔刷 - - Args: - brush_name: 笔刷名称 - brush_function: 笔刷函数 - - Returns: - 是否创建成功 - """ - try: - # 在实际实现中,这里会注册自定义笔刷函数 - self.brush_types[brush_name] = { - 'name': brush_name, - 'description': '自定义笔刷', - 'function': brush_function - } - - print(f"✓ 自定义笔刷 '{brush_name}' 已创建") - return True - - except Exception as e: - print(f"✗ 自定义笔刷创建失败: {e}") - return False - - def set_ui_component(self, component_name: str, component: Any): - """ - 设置UI组件引用 - - Args: - component_name: 组件名称 - component: 组件引用 - """ - if component_name in self.ui_components: - self.ui_components[component_name] = component - print(f"✓ UI组件 '{component_name}' 已设置") - else: - print(f"✗ 无效的UI组件名称: {component_name}") - - def refresh_ui(self): - """刷新UI显示""" - try: - # 在实际实现中,这里会更新所有UI组件的显示 - print("✓ UI刷新完成") - except Exception as e: - print(f"✗ UI刷新失败: {e}") - - def show_preview_window(self): - """显示预览窗口""" - try: - # 在实际实现中,这里会创建并显示预览窗口 - print("✓ 预览窗口已显示") - except Exception as e: - print(f"✗ 预览窗口显示失败: {e}") - - def hide_preview_window(self): - """隐藏预览窗口""" - try: - # 在实际实现中,这里会隐藏预览窗口 - print("✓ 预览窗口已隐藏") - except Exception as e: - print(f"✗ 预览窗口隐藏失败: {e}") - - def update_preview_window(self): - """更新预览窗口内容""" - try: - # 在实际实现中,这里会更新预览窗口的内容 - print("✓ 预览窗口已更新") - except Exception as e: - print(f"✗ 预览窗口更新失败: {e}") - - def set_brush_position(self, x: int, y: int): - """ - 设置笔刷位置(用于预览显示) - - Args: - x: X坐标 - y: Y坐标 - """ - try: - self.selected_brush_position = (x, y) - # 在实际实现中,这里会更新UI上的笔刷预览位置 - except Exception as e: - print(f"✗ 笔刷位置设置失败: {e}") - - def clear_brush_position(self): - """清除笔刷位置""" - try: - self.selected_brush_position = None - # 在实际实现中,这里会清除UI上的笔刷预览位置 - except Exception as e: - print(f"✗ 笔刷位置清除失败: {e}") - - def apply_generator_to_selection(self, generator_type: str = 'mountain', generator_params: Dict[str, Any] = None): - """ - 对选择区域应用生成器 - - Args: - generator_type: 生成器类型 ('mountain', 'valley', 'plateau', 'crater', 'island') - generator_params: 生成器参数 - """ - try: - if not self.enabled: - print("✗ 地形编辑器未启用") - return - - if self.selected_area is None: - print("✗ 没有选择区域") - return - - # 保存状态到历史记录 - self._save_state_to_history() - - # 获取选择区域 - min_x, min_y, max_x, max_y = self.selected_area - width = max_x - min_x + 1 - height = max_y - min_y + 1 - - # 获取地形数据 - if self.plugin.terrain_manager and self.plugin.heightmap_generator: - terrain_data = self.plugin.terrain_manager.get_terrain_data() - heightmap = terrain_data.get('heightmap') - - if heightmap is not None: - # 生成新的地形片段 - if generator_type == 'mountain': - new_heightmap = self.plugin.heightmap_generator.generate_mountain_heightmap(width, height) - elif generator_type == 'valley': - new_heightmap = self.plugin.heightmap_generator.generate_valley_heightmap(width, height) - elif generator_type == 'plateau': - new_heightmap = self.plugin.heightmap_generator.generate_plateau_heightmap(width, height) - elif generator_type == 'crater': - new_heightmap = self.plugin.heightmap_generator.generate_crater_heightmap(width, height) - elif generator_type == 'island': - new_heightmap = self.plugin.heightmap_generator.generate_island_heightmap(width, height) - else: - # 默认使用基础噪声生成 - new_heightmap = self.plugin.heightmap_generator.generate_heightmap(width, height) - - # 应用到选择区域 - heightmap[min_y:max_y+1, min_x:max_x+1] = new_heightmap - - # 标记预览需要更新 - self.preview_dirty = True - - # 更新统计信息 - self.stats['edits_made'] += 1 - - print(f"✓ 生成器应用完成: {generator_type}") - - except Exception as e: - print(f"✗ 生成器应用失败: {e}") - import traceback - traceback.print_exc() - - def clone_area(self, source_x1: int, source_y1: int, source_x2: int, source_y2: int, - target_x: int, target_y: int): - """ - 克隆区域 - - Args: - source_x1: 源区域起始X坐标 - source_y1: 源区域起始Y坐标 - source_x2: 源区域结束X坐标 - source_y2: 源区域结束Y坐标 - target_x: 目标位置X坐标 - target_y: 目标位置Y坐标 - """ - try: - if not self.enabled: - print("✗ 地形编辑器未启用") - return - - # 保存状态到历史记录 - self._save_state_to_history() - - # 标准化源区域坐标 - source_min_x = min(source_x1, source_x2) - source_max_x = max(source_x1, source_x2) - source_min_y = min(source_y1, source_y2) - source_max_y = max(source_y1, source_y2) - - # 计算区域尺寸 - region_width = source_max_x - source_min_x + 1 - region_height = source_max_y - source_min_y + 1 - - # 获取地形数据 - if self.plugin.terrain_manager: - terrain_data = self.plugin.terrain_manager.get_terrain_data() - - # 克隆高度图 - heightmap = terrain_data.get('heightmap') - if heightmap is not None: - # 确保源区域有效 - if (0 <= source_min_x < heightmap.shape[1] and - 0 <= source_max_x < heightmap.shape[1] and - 0 <= source_min_y < heightmap.shape[0] and - 0 <= source_max_y < heightmap.shape[0]): - - # 提取源区域 - source_region = heightmap[source_min_y:source_max_y+1, source_min_x:source_max_x+1].copy() - - # 计算目标区域边界 - target_min_x = target_x - target_max_x = target_x + region_width - 1 - target_min_y = target_y - target_max_y = target_y + region_height - 1 - - # 确保目标区域在地图范围内 - if (0 <= target_min_x < heightmap.shape[1] and - 0 <= target_max_x < heightmap.shape[1] and - 0 <= target_min_y < heightmap.shape[0] and - 0 <= target_max_y < heightmap.shape[0]): - - # 应用到目标区域 - heightmap[target_min_y:target_max_y+1, target_min_x:target_max_x+1] = source_region - - # 标记预览需要更新 - self.preview_dirty = True - - # 更新统计信息 - self.stats['edits_made'] += 1 - - print("✓ 区域克隆完成") - return - - print("✗ 无法克隆区域:源区域或目标区域超出边界") - - except Exception as e: - print(f"✗ 区域克隆失败: {e}") - import traceback - traceback.print_exc() - - def flip_area(self, horizontal: bool = True, vertical: bool = False): - """ - 翻转选择区域 - - Args: - horizontal: 是否水平翻转 - vertical: 是否垂直翻转 - """ - try: - if not self.enabled: - print("✗ 地形编辑器未启用") - return - - if self.selected_area is None: - print("✗ 没有选择区域") - return - - # 保存状态到历史记录 - self._save_state_to_history() - - # 获取选择区域 - min_x, min_y, max_x, max_y = self.selected_area - - # 获取地形数据 - if self.plugin.terrain_manager: - terrain_data = self.plugin.terrain_manager.get_terrain_data() - - # 翻转高度图 - heightmap = terrain_data.get('heightmap') - if heightmap is not None: - region = heightmap[min_y:max_y+1, min_x:max_x+1] - if horizontal: - region = np.fliplr(region) - if vertical: - region = np.flipud(region) - heightmap[min_y:max_y+1, min_x:max_x+1] = region - - # 翻转生物群落图 - biome_map = terrain_data.get('biome_map') - if biome_map is not None: - region = biome_map[min_y:max_y+1, min_x:max_x+1] - if horizontal: - region = np.fliplr(region) - if vertical: - region = np.flipud(region) - biome_map[min_y:max_y+1, min_x:max_x+1] = region - - # 翻转植被图 - vegetation_map = terrain_data.get('vegetation_map') - if vegetation_map is not None: - region = vegetation_map[min_y:max_y+1, min_x:max_x+1] - if horizontal: - region = np.fliplr(region) - if vertical: - region = np.flipud(region) - vegetation_map[min_y:max_y+1, min_x:max_x+1] = region - - # 翻转水图 - water_map = terrain_data.get('water_map') - if water_map is not None: - region = water_map[min_y:max_y+1, min_x:max_x+1] - if horizontal: - region = np.fliplr(region) - if vertical: - region = np.flipud(region) - water_map[min_y:max_y+1, min_x:max_x+1] = region - - # 标记预览需要更新 - self.preview_dirty = True - - # 更新统计信息 - self.stats['edits_made'] += 1 - - print("✓ 区域翻转完成") - - except Exception as e: - print(f"✗ 区域翻转失败: {e}") - import traceback - traceback.print_exc() - - def rotate_area(self, angle: int = 90): - """ - 旋转选择区域 - - Args: - angle: 旋转角度 (90, 180, 270) - """ - try: - if not self.enabled: - print("✗ 地形编辑器未启用") - return - - if self.selected_area is None: - print("✗ 没有选择区域") - return - - # 保存状态到历史记录 - self._save_state_to_history() - - # 获取选择区域 - min_x, min_y, max_x, max_y = self.selected_area - region_width = max_x - min_x + 1 - region_height = max_y - min_y + 1 - - # 只支持正方形区域的旋转 - if region_width != region_height: - print("✗ 只支持正方形区域的旋转") - return - - # 获取地形数据 - if self.plugin.terrain_manager: - terrain_data = self.plugin.terrain_manager.get_terrain_data() - - # 旋转高度图 - heightmap = terrain_data.get('heightmap') - if heightmap is not None: - region = heightmap[min_y:max_y+1, min_x:max_x+1] - if angle == 90: - region = np.rot90(region, k=1) - elif angle == 180: - region = np.rot90(region, k=2) - elif angle == 270: - region = np.rot90(region, k=3) - heightmap[min_y:max_y+1, min_x:max_x+1] = region - - # 旋转生物群落图 - biome_map = terrain_data.get('biome_map') - if biome_map is not None: - region = biome_map[min_y:max_y+1, min_x:max_x+1] - if angle == 90: - region = np.rot90(region, k=1) - elif angle == 180: - region = np.rot90(region, k=2) - elif angle == 270: - region = np.rot90(region, k=3) - biome_map[min_y:max_y+1, min_x:max_x+1] = region - - # 旋转植被图 - vegetation_map = terrain_data.get('vegetation_map') - if vegetation_map is not None: - region = vegetation_map[min_y:max_y+1, min_x:max_x+1] - if angle == 90: - region = np.rot90(region, k=1) - elif angle == 180: - region = np.rot90(region, k=2) - elif angle == 270: - region = np.rot90(region, k=3) - vegetation_map[min_y:max_y+1, min_x:max_x+1] = region - - # 旋转水图 - water_map = terrain_data.get('water_map') - if water_map is not None: - region = water_map[min_y:max_y+1, min_x:max_x+1] - if angle == 90: - region = np.rot90(region, k=1) - elif angle == 180: - region = np.rot90(region, k=2) - elif angle == 270: - region = np.rot90(region, k=3) - water_map[min_y:max_y+1, min_x:max_x+1] = region - - # 标记预览需要更新 - self.preview_dirty = True - - # 更新统计信息 - self.stats['edits_made'] += 1 - - print(f"✓ 区域旋转完成 ({angle}度)") - - except Exception as e: - print(f"✗ 区域旋转失败: {e}") - import traceback +""" +地形编辑器 +提供可视化界面用于编辑和调整程序化生成的地形 +""" + +import numpy as np +import math +from typing import Dict, Any, List, Tuple, Optional +import json +import time + +class TerrainEditor: + """ + 地形编辑器 + 提供可视化界面用于编辑和调整程序化生成的地形,包括参数调整、实时预览、笔刷工具等功能 + """ + + def __init__(self, plugin): + """ + 初始化地形编辑器 + + Args: + plugin: 程序化地形生成插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 编辑器配置 + self.editor_params = { + 'brush_size': 10, + 'brush_strength': 0.5, + 'brush_type': 'raise', # 'raise', 'lower', 'smooth', 'flatten', 'noise' + 'edit_mode': 'terrain', # 'terrain', 'biome', 'vegetation', 'water' + 'preview_enabled': True, + 'realtime_update': True, + 'history_limit': 50 + } + + # 笔刷类型定义 + self.brush_types = { + 'raise': {'name': '抬高', 'description': '抬高地形'}, + 'lower': {'name': '降低', 'description': '降低地形'}, + 'smooth': {'name': '平滑', 'description': '平滑地形'}, + 'flatten': {'name': '平整', 'description': '将地形平整到指定高度'}, + 'noise': {'name': '噪声', 'description': '添加噪声效果'}, + 'erode': {'name': '侵蚀', 'description': '模拟侵蚀效果'}, + 'paint': {'name': '绘制', 'description': '绘制生物群落或植被'} + } + + # 编辑历史 + self.edit_history = [] + self.history_position = -1 + + # 当前选择 + self.selected_area = None + self.selected_brush_position = None + + # 预览数据 + self.preview_data = None + self.preview_dirty = False + + # 工具状态 + self.is_painting = False + self.last_paint_position = None + + # 统计信息 + self.stats = { + 'edits_made': 0, + 'undos_performed': 0, + 'redos_performed': 0, + 'total_edit_time': 0.0, + 'average_edit_time': 0.0 + } + + # UI组件引用(在实际实现中会连接到GUI) + self.ui_components = { + 'toolbar': None, + 'properties_panel': None, + 'preview_window': None, + 'brush_settings': None, + 'history_panel': None + } + + print("✓ 地形编辑器已创建") + + def initialize(self) -> bool: + """ + 初始化地形编辑器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 地形编辑器初始化完成") + return True + + except Exception as e: + print(f"✗ 地形编辑器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用地形编辑器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 地形编辑器未初始化") + return False + + self.enabled = True + print("✓ 地形编辑器已启用") + return True + + except Exception as e: + print(f"✗ 地形编辑器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用地形编辑器""" + try: + self.enabled = False + print("✓ 地形编辑器已禁用") + + except Exception as e: + print(f"✗ 地形编辑器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理地形编辑器资源""" + try: + self.disable() + self.initialized = False + print("✓ 地形编辑器资源已清理") + + except Exception as e: + print(f"✗ 地形编辑器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新地形编辑器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新预览 + if self.preview_dirty and self.editor_params['preview_enabled']: + self._update_preview() + + # 处理实时更新 + if self.editor_params['realtime_update']: + self._handle_realtime_updates() + + except Exception as e: + print(f"✗ 地形编辑器更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_preview(self): + """更新预览显示""" + try: + # 在实际实现中,这里会更新预览窗口 + self.preview_dirty = False + except Exception as e: + print(f"✗ 预览更新失败: {e}") + + def _handle_realtime_updates(self): + """处理实时更新""" + try: + # 在实际实现中,这里会处理实时编辑更新 + pass + except Exception as e: + print(f"✗ 实时更新处理失败: {e}") + + def set_brush_size(self, size: int): + """ + 设置笔刷大小 + + Args: + size: 笔刷大小(像素) + """ + self.editor_params['brush_size'] = max(1, size) + print(f"✓ 笔刷大小设置为: {size}") + + def set_brush_strength(self, strength: float): + """ + 设置笔刷强度 + + Args: + strength: 笔刷强度 (0.0-1.0) + """ + self.editor_params['brush_strength'] = max(0.0, min(1.0, strength)) + print(f"✓ 笔刷强度设置为: {strength}") + + def set_brush_type(self, brush_type: str): + """ + 设置笔刷类型 + + Args: + brush_type: 笔刷类型 + """ + if brush_type in self.brush_types: + self.editor_params['brush_type'] = brush_type + print(f"✓ 笔刷类型设置为: {self.brush_types[brush_type]['name']}") + else: + print(f"✗ 无效的笔刷类型: {brush_type}") + + def set_edit_mode(self, mode: str): + """ + 设置编辑模式 + + Args: + mode: 编辑模式 ('terrain', 'biome', 'vegetation', 'water') + """ + valid_modes = ['terrain', 'biome', 'vegetation', 'water'] + if mode in valid_modes: + self.editor_params['edit_mode'] = mode + print(f"✓ 编辑模式设置为: {mode}") + else: + print(f"✗ 无效的编辑模式: {mode}") + + def set_preview_enabled(self, enabled: bool): + """ + 设置预览是否启用 + + Args: + enabled: 是否启用预览 + """ + self.editor_params['preview_enabled'] = enabled + print(f"✓ 预览{'已启用' if enabled else '已禁用'}") + + def set_realtime_update(self, enabled: bool): + """ + 设置实时更新是否启用 + + Args: + enabled: 是否启用实时更新 + """ + self.editor_params['realtime_update'] = enabled + print(f"✓ 实时更新{'已启用' if enabled else '已禁用'}") + + def begin_paint(self, x: int, y: int): + """ + 开始绘制操作 + + Args: + x: X坐标 + y: Y坐标 + """ + try: + if not self.enabled: + print("✗ 地形编辑器未启用") + return + + self.is_painting = True + self.last_paint_position = (x, y) + + # 保存当前状态到历史记录 + self._save_state_to_history() + + # 执行绘制操作 + self._apply_brush(x, y) + + except Exception as e: + print(f"✗ 开始绘制失败: {e}") + import traceback + traceback.print_exc() + + def continue_paint(self, x: int, y: int): + """ + 继续绘制操作 + + Args: + x: X坐标 + y: Y坐标 + """ + try: + if not self.enabled or not self.is_painting: + return + + # 如果位置发生变化,执行绘制 + if self.last_paint_position != (x, y): + self._apply_brush_stroke(self.last_paint_position, (x, y)) + self.last_paint_position = (x, y) + + except Exception as e: + print(f"✗ 绘制过程失败: {e}") + import traceback + traceback.print_exc() + + def end_paint(self): + """结束绘制操作""" + try: + self.is_painting = False + self.last_paint_position = None + + # 更新统计信息 + self.stats['edits_made'] += 1 + + except Exception as e: + print(f"✗ 结束绘制失败: {e}") + import traceback + traceback.print_exc() + + def _apply_brush(self, x: int, y: int): + """ + 应用笔刷效果 + + Args: + x: X坐标 + y: Y坐标 + """ + try: + brush_type = self.editor_params['brush_type'] + brush_size = self.editor_params['brush_size'] + brush_strength = self.editor_params['brush_strength'] + edit_mode = self.editor_params['edit_mode'] + + # 获取当前地形数据 + if self.plugin.terrain_manager: + terrain_data = self.plugin.terrain_manager.get_terrain_data() + + if edit_mode == 'terrain': + self._apply_terrain_brush(terrain_data, x, y, brush_type, brush_size, brush_strength) + elif edit_mode == 'biome': + self._apply_biome_brush(terrain_data, x, y, brush_type, brush_size, brush_strength) + elif edit_mode == 'vegetation': + self._apply_vegetation_brush(terrain_data, x, y, brush_type, brush_size, brush_strength) + elif edit_mode == 'water': + self._apply_water_brush(terrain_data, x, y, brush_type, brush_size, brush_strength) + + # 标记预览需要更新 + self.preview_dirty = True + + except Exception as e: + print(f"✗ 笔刷应用失败: {e}") + import traceback + traceback.print_exc() + + def _apply_brush_stroke(self, start_pos: Tuple[int, int], end_pos: Tuple[int, int]): + """ + 应用笔刷笔触(在两点之间绘制) + + Args: + start_pos: 起始位置 (x, y) + end_pos: 结束位置 (x, y) + """ + try: + # 使用直线算法在两点之间插值 + x0, y0 = start_pos + x1, y1 = end_pos + + # 计算距离 + distance = math.sqrt((x1 - x0)**2 + (y1 - y0)**2) + + # 如果距离很小,只应用一次 + if distance <= 1: + self._apply_brush(x1, y1) + return + + # 在两点之间插值 + steps = int(distance) + for i in range(steps + 1): + t = i / steps if steps > 0 else 0 + x = int(x0 + (x1 - x0) * t) + y = int(y0 + (y1 - y0) * t) + self._apply_brush(x, y) + + except Exception as e: + print(f"✗ 笔触应用失败: {e}") + import traceback + traceback.print_exc() + + def _apply_terrain_brush(self, terrain_data: Dict[str, Any], x: int, y: int, + brush_type: str, brush_size: int, brush_strength: float): + """ + 应用地形笔刷 + + Args: + terrain_data: 地形数据 + x: X坐标 + y: Y坐标 + brush_type: 笔刷类型 + brush_size: 笔刷大小 + brush_strength: 笔刷强度 + """ + try: + heightmap = terrain_data.get('heightmap') + if heightmap is None: + return + + height, width = heightmap.shape + + # 计算笔刷影响区域 + min_x = max(0, x - brush_size) + max_x = min(width - 1, x + brush_size) + min_y = max(0, y - brush_size) + max_y = min(height - 1, y + brush_size) + + # 创建笔刷形状(圆形) + brush_radius = brush_size + brush_radius_sq = brush_radius * brush_radius + + # 应用笔刷效果 + for by in range(min_y, max_y + 1): + for bx in range(min_x, max_x + 1): + # 计算到笔刷中心的距离 + dx = bx - x + dy = by - y + distance_sq = dx * dx + dy * dy + + # 如果在笔刷范围内 + if distance_sq <= brush_radius_sq: + # 计算笔刷影响强度(边缘递减) + distance = math.sqrt(distance_sq) + influence = max(0.0, 1.0 - distance / brush_radius) + effective_strength = brush_strength * influence + + # 根据笔刷类型应用不同效果 + if brush_type == 'raise': + heightmap[by, bx] = min(1.0, heightmap[by, bx] + effective_strength * 0.1) + elif brush_type == 'lower': + heightmap[by, bx] = max(0.0, heightmap[by, bx] - effective_strength * 0.1) + elif brush_type == 'smooth': + # 简单的平滑处理 + neighbors = [] + for ny in range(max(0, by - 1), min(height, by + 2)): + for nx in range(max(0, bx - 1), min(width, bx + 2)): + neighbors.append(heightmap[ny, nx]) + if neighbors: + avg_height = sum(neighbors) / len(neighbors) + heightmap[by, bx] = heightmap[by, bx] * (1 - effective_strength) + avg_height * effective_strength + elif brush_type == 'flatten': + # 平整到固定高度(这里使用0.5作为示例) + target_height = 0.5 + heightmap[by, bx] = heightmap[by, bx] * (1 - effective_strength) + target_height * effective_strength + elif brush_type == 'noise': + # 添加噪声 + noise = (np.random.random() - 0.5) * effective_strength * 0.2 + heightmap[by, bx] = max(0.0, min(1.0, heightmap[by, bx] + noise)) + elif brush_type == 'erode': + # 简单的侵蚀效果 + heightmap[by, bx] = max(0.0, heightmap[by, bx] - effective_strength * 0.05) + + except Exception as e: + print(f"✗ 地形笔刷应用失败: {e}") + import traceback + traceback.print_exc() + + def _apply_biome_brush(self, terrain_data: Dict[str, Any], x: int, y: int, + brush_type: str, brush_size: int, brush_strength: float): + """ + 应用生物群落笔刷 + + Args: + terrain_data: 地形数据 + x: X坐标 + y: Y坐标 + brush_type: 笔刷类型 + brush_size: 笔刷大小 + brush_strength: 笔刷强度 + """ + try: + biome_map = terrain_data.get('biome_map') + if biome_map is None: + return + + height, width = biome_map.shape + + # 计算笔刷影响区域 + min_x = max(0, x - brush_size) + max_x = min(width - 1, x + brush_size) + min_y = max(0, y - brush_size) + max_y = min(height - 1, y + brush_size) + + # 创建笔刷形状(圆形) + brush_radius = brush_size + brush_radius_sq = brush_radius * brush_radius + + # 应用笔刷效果 + for by in range(min_y, max_y + 1): + for bx in range(min_x, max_x + 1): + # 计算到笔刷中心的距离 + dx = bx - x + dy = by - y + distance_sq = dx * dx + dy * dy + + # 如果在笔刷范围内 + if distance_sq <= brush_radius_sq: + # 计算笔刷影响强度(边缘递减) + distance = math.sqrt(distance_sq) + influence = max(0.0, 1.0 - distance / brush_radius) + effective_strength = brush_strength * influence + + # 根据笔刷类型应用不同效果 + if brush_type == 'paint': + # 绘制生物群落(这里简单地设置为生物群落类型1) + if effective_strength > 0.5: # 只有强度足够时才改变 + biome_map[by, bx] = 1 # 设置为草原生物群落 + + except Exception as e: + print(f"✗ 生物群落笔刷应用失败: {e}") + import traceback + traceback.print_exc() + + def _apply_vegetation_brush(self, terrain_data: Dict[str, Any], x: int, y: int, + brush_type: str, brush_size: int, brush_strength: float): + """ + 应用植被笔刷 + + Args: + terrain_data: 地形数据 + x: X坐标 + y: Y坐标 + brush_type: 笔刷类型 + brush_size: 笔刷大小 + brush_strength: 笔刷强度 + """ + try: + vegetation_map = terrain_data.get('vegetation_map') + if vegetation_map is None: + return + + height, width = vegetation_map.shape + + # 计算笔刷影响区域 + min_x = max(0, x - brush_size) + max_x = min(width - 1, x + brush_size) + min_y = max(0, y - brush_size) + max_y = min(height - 1, y + brush_size) + + # 创建笔刷形状(圆形) + brush_radius = brush_size + brush_radius_sq = brush_radius * brush_radius + + # 应用笔刷效果 + for by in range(min_y, max_y + 1): + for bx in range(min_x, max_x + 1): + # 计算到笔刷中心的距离 + dx = bx - x + dy = by - y + distance_sq = dx * dx + dy * dy + + # 如果在笔刷范围内 + if distance_sq <= brush_radius_sq: + # 计算笔刷影响强度(边缘递减) + distance = math.sqrt(distance_sq) + influence = max(0.0, 1.0 - distance / brush_radius) + effective_strength = brush_strength * influence + + # 根据笔刷类型应用不同效果 + if brush_type == 'paint': + # 绘制植被(这里简单地设置为植被类型1) + if effective_strength > 0.3: # 只有强度足够时才改变 + vegetation_map[by, bx] = 1 # 设置为草 + + except Exception as e: + print(f"✗ 植被笔刷应用失败: {e}") + import traceback + traceback.print_exc() + + def _apply_water_brush(self, terrain_data: Dict[str, Any], x: int, y: int, + brush_type: str, brush_size: int, brush_strength: float): + """ + 应用水笔刷 + + Args: + terrain_data: 地形数据 + x: X坐标 + y: Y坐标 + brush_type: 笔刷类型 + brush_size: 笔刷大小 + brush_strength: 笔刷强度 + """ + try: + water_map = terrain_data.get('water_map') + if water_map is None: + return + + height, width = water_map.shape + + # 计算笔刷影响区域 + min_x = max(0, x - brush_size) + max_x = min(width - 1, x + brush_size) + min_y = max(0, y - brush_size) + max_y = min(height - 1, y + brush_size) + + # 创建笔刷形状(圆形) + brush_radius = brush_size + brush_radius_sq = brush_radius * brush_radius + + # 应用笔刷效果 + for by in range(min_y, max_y + 1): + for bx in range(min_x, max_x + 1): + # 计算到笔刷中心的距离 + dx = bx - x + dy = by - y + distance_sq = dx * dx + dy * dy + + # 如果在笔刷范围内 + if distance_sq <= brush_radius_sq: + # 计算笔刷影响强度(边缘递减) + distance = math.sqrt(distance_sq) + influence = max(0.0, 1.0 - distance / brush_radius) + effective_strength = brush_strength * influence + + # 根据笔刷类型应用不同效果 + if brush_type == 'raise': + water_map[by, bx] = min(1.0, water_map[by, bx] + effective_strength * 0.1) + elif brush_type == 'lower': + water_map[by, bx] = max(0.0, water_map[by, bx] - effective_strength * 0.1) + elif brush_type == 'paint': + # 设置固定水位 + water_map[by, bx] = 0.3 # 设置为中等水位 + + except Exception as e: + print(f"✗ 水笔刷应用失败: {e}") + import traceback + traceback.print_exc() + + def _save_state_to_history(self): + """保存当前状态到历史记录""" + try: + if self.plugin.terrain_manager: + terrain_data = self.plugin.terrain_manager.get_terrain_data() + + # 创建状态快照 + state_snapshot = { + 'timestamp': time.time(), + 'heightmap': terrain_data.get('heightmap', np.array([])).copy() if terrain_data.get('heightmap') is not None else np.array([]), + 'biome_map': terrain_data.get('biome_map', np.array([])).copy() if terrain_data.get('biome_map') is not None else np.array([]), + 'vegetation_map': terrain_data.get('vegetation_map', np.array([])).copy() if terrain_data.get('vegetation_map') is not None else np.array([]), + 'water_map': terrain_data.get('water_map', np.array([])).copy() if terrain_data.get('water_map') is not None else np.array([]) + } + + # 添加到历史记录 + if self.history_position < len(self.edit_history) - 1: + # 如果当前不在历史记录末尾,截断后续记录 + self.edit_history = self.edit_history[:self.history_position + 1] + + self.edit_history.append(state_snapshot) + self.history_position += 1 + + # 限制历史记录数量 + if len(self.edit_history) > self.editor_params['history_limit']: + self.edit_history.pop(0) + self.history_position -= 1 + + except Exception as e: + print(f"✗ 状态保存失败: {e}") + import traceback + traceback.print_exc() + + def undo(self) -> bool: + """ + 撤销操作 + + Returns: + 是否撤销成功 + """ + try: + if not self.enabled: + print("✗ 地形编辑器未启用") + return False + + if self.history_position <= 0: + print("✗ 没有可撤销的操作") + return False + + # 移动到前一个历史状态 + self.history_position -= 1 + + # 恢复状态 + self._restore_state_from_history() + + # 更新统计信息 + self.stats['undos_performed'] += 1 + + print("✓ 撤销操作完成") + return True + + except Exception as e: + print(f"✗ 撤销操作失败: {e}") + import traceback + traceback.print_exc() + return False + + def redo(self) -> bool: + """ + 重做操作 + + Returns: + 是否重做成功 + """ + try: + if not self.enabled: + print("✗ 地形编辑器未启用") + return False + + if self.history_position >= len(self.edit_history) - 1: + print("✗ 没有可重做的操作") + return False + + # 移动到下一个历史状态 + self.history_position += 1 + + # 恢复状态 + self._restore_state_from_history() + + # 更新统计信息 + self.stats['redos_performed'] += 1 + + print("✓ 重做操作完成") + return True + + except Exception as e: + print(f"✗ 重做操作失败: {e}") + import traceback + traceback.print_exc() + return False + + def _restore_state_from_history(self): + """从历史记录恢复状态""" + try: + if not self.plugin.terrain_manager or self.history_position < 0: + return + + # 获取历史状态 + state = self.edit_history[self.history_position] + + # 恢复地形数据 + terrain_data = self.plugin.terrain_manager.get_terrain_data() + + if 'heightmap' in state and state['heightmap'].size > 0: + terrain_data['heightmap'] = state['heightmap'] + + if 'biome_map' in state and state['biome_map'].size > 0: + terrain_data['biome_map'] = state['biome_map'] + + if 'vegetation_map' in state and state['vegetation_map'].size > 0: + terrain_data['vegetation_map'] = state['vegetation_map'] + + if 'water_map' in state and state['water_map'].size > 0: + terrain_data['water_map'] = state['water_map'] + + # 标记预览需要更新 + self.preview_dirty = True + + except Exception as e: + print(f"✗ 状态恢复失败: {e}") + import traceback + traceback.print_exc() + + def clear_history(self): + """清空历史记录""" + try: + self.edit_history.clear() + self.history_position = -1 + print("✓ 历史记录已清空") + except Exception as e: + print(f"✗ 历史记录清空失败: {e}") + import traceback + traceback.print_exc() + + def select_area(self, x1: int, y1: int, x2: int, y2: int): + """ + 选择区域 + + Args: + x1: 起始X坐标 + y1: 起始Y坐标 + x2: 结束X坐标 + y2: 结束Y坐标 + """ + try: + if not self.enabled: + print("✗ 地形编辑器未启用") + return + + # 标准化坐标 + min_x = min(x1, x2) + max_x = max(x1, x2) + min_y = min(y1, y2) + max_y = max(y1, y2) + + self.selected_area = (min_x, min_y, max_x, max_y) + print(f"✓ 区域已选择: ({min_x}, {min_y}) to ({max_x}, {max_y})") + + except Exception as e: + print(f"✗ 区域选择失败: {e}") + import traceback + traceback.print_exc() + + def clear_selection(self): + """清除选择""" + try: + self.selected_area = None + print("✓ 选择已清除") + except Exception as e: + print(f"✗ 选择清除失败: {e}") + import traceback + traceback.print_exc() + + def fill_selection(self, fill_type: str = 'flatten', fill_value: float = 0.5): + """ + 填充选择区域 + + Args: + fill_type: 填充类型 ('flatten', 'noise', 'smooth') + fill_value: 填充值(用于flatten) + """ + try: + if not self.enabled: + print("✗ 地形编辑器未启用") + return + + if self.selected_area is None: + print("✗ 没有选择区域") + return + + # 保存状态到历史记录 + self._save_state_to_history() + + # 获取选择区域 + min_x, min_y, max_x, max_y = self.selected_area + + # 获取地形数据 + if self.plugin.terrain_manager: + terrain_data = self.plugin.terrain_manager.get_terrain_data() + heightmap = terrain_data.get('heightmap') + + if heightmap is not None: + # 填充区域 + for y in range(min_y, max_y + 1): + for x in range(min_x, max_x + 1): + if fill_type == 'flatten': + heightmap[y, x] = fill_value + elif fill_type == 'noise': + heightmap[y, x] = np.random.random() + elif fill_type == 'smooth': + # 简单平滑处理 + neighbors = [] + h, w = heightmap.shape + for ny in range(max(0, y - 1), min(h, y + 2)): + for nx in range(max(0, x - 1), min(w, x + 2)): + neighbors.append(heightmap[ny, nx]) + if neighbors: + heightmap[y, x] = sum(neighbors) / len(neighbors) + + # 标记预览需要更新 + self.preview_dirty = True + + # 更新统计信息 + self.stats['edits_made'] += 1 + + print(f"✓ 区域填充完成: {fill_type}") + + except Exception as e: + print(f"✗ 区域填充失败: {e}") + import traceback + traceback.print_exc() + + def apply_filter_to_selection(self, filter_type: str = 'smooth', filter_strength: float = 1.0): + """ + 对选择区域应用滤镜 + + Args: + filter_type: 滤镜类型 ('smooth', 'sharpen', 'erode', 'dilate') + filter_strength: 滤镜强度 + """ + try: + if not self.enabled: + print("✗ 地形编辑器未启用") + return + + if self.selected_area is None: + print("✗ 没有选择区域") + return + + # 保存状态到历史记录 + self._save_state_to_history() + + # 获取选择区域 + min_x, min_y, max_x, max_y = self.selected_area + + # 获取地形数据 + if self.plugin.terrain_manager: + terrain_data = self.plugin.terrain_manager.get_terrain_data() + heightmap = terrain_data.get('heightmap') + + if heightmap is not None: + # 创建处理后的副本 + processed_heightmap = heightmap.copy() + + # 应用滤镜 + if filter_type == 'smooth': + self._apply_smoothing_filter(heightmap, processed_heightmap, min_x, min_y, max_x, max_y, filter_strength) + elif filter_type == 'sharpen': + self._apply_sharpening_filter(heightmap, processed_heightmap, min_x, min_y, max_x, max_y, filter_strength) + elif filter_type == 'erode': + self._apply_erosion_filter(heightmap, processed_heightmap, min_x, min_y, max_x, max_y, filter_strength) + elif filter_type == 'dilate': + self._apply_dilation_filter(heightmap, processed_heightmap, min_x, min_y, max_x, max_y, filter_strength) + + # 更新原高度图 + heightmap[min_y:max_y+1, min_x:max_x+1] = processed_heightmap[min_y:max_y+1, min_x:max_x+1] + + # 标记预览需要更新 + self.preview_dirty = True + + # 更新统计信息 + self.stats['edits_made'] += 1 + + print(f"✓ 滤镜应用完成: {filter_type}") + + except Exception as e: + print(f"✗ 滤镜应用失败: {e}") + import traceback + traceback.print_exc() + + def _apply_smoothing_filter(self, original: np.ndarray, processed: np.ndarray, + min_x: int, min_y: int, max_x: int, max_y: int, strength: float): + """应用平滑滤镜""" + try: + height, width = original.shape + + for y in range(min_y, max_y + 1): + for x in range(min_x, max_x + 1): + # 计算邻域平均值 + neighbors = [] + for dy in [-1, 0, 1]: + for dx in [-1, 0, 1]: + ny, nx = y + dy, x + dx + if 0 <= ny < height and 0 <= nx < width: + neighbors.append(original[ny, nx]) + + if neighbors: + avg_value = sum(neighbors) / len(neighbors) + processed[y, x] = original[y, x] * (1 - strength) + avg_value * strength + + except Exception as e: + print(f"✗ 平滑滤镜应用失败: {e}") + + def _apply_sharpening_filter(self, original: np.ndarray, processed: np.ndarray, + min_x: int, min_y: int, max_x: int, max_y: int, strength: float): + """应用锐化滤镜""" + try: + height, width = original.shape + + # 简单的锐化核 + kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]]) + + for y in range(max(min_y, 1), min(max_y + 1, height - 1)): + for x in range(max(min_x, 1), min(max_x + 1, width - 1)): + # 应用卷积 + value = 0.0 + for ky in [-1, 0, 1]: + for kx in [-1, 0, 1]: + value += original[y + ky, x + kx] * kernel[ky + 1, kx + 1] + + # 限制值范围并应用强度 + value = max(0.0, min(1.0, value)) + processed[y, x] = original[y, x] * (1 - strength) + value * strength + + except Exception as e: + print(f"✗ 锐化滤镜应用失败: {e}") + + def _apply_erosion_filter(self, original: np.ndarray, processed: np.ndarray, + min_x: int, min_y: int, max_x: int, max_y: int, strength: float): + """应用侵蚀滤镜""" + try: + height, width = original.shape + + for y in range(min_y, max_y + 1): + for x in range(min_x, max_x + 1): + # 简单的侵蚀:降低高度 + processed[y, x] = max(0.0, original[y, x] - 0.1 * strength) + + except Exception as e: + print(f"✗ 侵蚀滤镜应用失败: {e}") + + def _apply_dilation_filter(self, original: np.ndarray, processed: np.ndarray, + min_x: int, min_y: int, max_x: int, max_y: int, strength: float): + """应用膨胀滤镜""" + try: + height, width = original.shape + + for y in range(min_y, max_y + 1): + for x in range(min_x, max_x + 1): + # 简单的膨胀:增加高度 + processed[y, x] = min(1.0, original[y, x] + 0.1 * strength) + + except Exception as e: + print(f"✗ 膨胀滤镜应用失败: {e}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + # 更新平均编辑时间 + total_edits = self.stats['edits_made'] + self.stats['undos_performed'] + self.stats['redos_performed'] + if total_edits > 0: + self.stats['average_edit_time'] = self.stats['total_edit_time'] / total_edits + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'edits_made': 0, + 'undos_performed': 0, + 'redos_performed': 0, + 'total_edit_time': 0.0, + 'average_edit_time': 0.0 + } + print("✓ 地形编辑器统计信息已重置") + + def set_editor_parameters(self, params: Dict[str, Any]): + """ + 设置编辑器参数 + + Args: + params: 参数字典 + """ + self.editor_params.update(params) + print(f"✓ 编辑器参数已更新: {self.editor_params}") + + def get_brush_types(self) -> Dict[str, Dict[str, str]]: + """ + 获取可用的笔刷类型 + + Returns: + 笔刷类型字典 + """ + return self.brush_types.copy() + + def get_edit_modes(self) -> List[str]: + """ + 获取可用的编辑模式 + + Returns: + 编辑模式列表 + """ + return ['terrain', 'biome', 'vegetation', 'water'] + + def export_brush_preset(self, preset_name: str, filename: str) -> bool: + """ + 导出笔刷预设 + + Args: + preset_name: 预设名称 + filename: 文件名 + + Returns: + 是否导出成功 + """ + try: + # 创建预设数据 + preset_data = { + 'name': preset_name, + 'brush_size': self.editor_params['brush_size'], + 'brush_strength': self.editor_params['brush_strength'], + 'brush_type': self.editor_params['brush_type'], + 'edit_mode': self.editor_params['edit_mode'], + 'timestamp': time.time() + } + + # 保存为JSON文件 + with open(filename, 'w', encoding='utf-8') as f: + json.dump(preset_data, f, ensure_ascii=False, indent=2) + + print(f"✓ 笔刷预设已导出到: {filename}") + return True + + except Exception as e: + print(f"✗ 笔刷预设导出失败: {e}") + import traceback + traceback.print_exc() + return False + + def import_brush_preset(self, filename: str) -> bool: + """ + 导入笔刷预设 + + Args: + filename: 文件名 + + Returns: + 是否导入成功 + """ + try: + # 读取JSON文件 + with open(filename, 'r', encoding='utf-8') as f: + preset_data = json.load(f) + + # 应用预设 + if 'brush_size' in preset_data: + self.set_brush_size(preset_data['brush_size']) + if 'brush_strength' in preset_data: + self.set_brush_strength(preset_data['brush_strength']) + if 'brush_type' in preset_data: + self.set_brush_type(preset_data['brush_type']) + if 'edit_mode' in preset_data: + self.set_edit_mode(preset_data['edit_mode']) + + print(f"✓ 笔刷预设已从 {filename} 导入") + return True + + except Exception as e: + print(f"✗ 笔刷预设导入失败: {e}") + import traceback + traceback.print_exc() + return False + + def create_custom_brush(self, brush_name: str, brush_function: callable) -> bool: + """ + 创建自定义笔刷 + + Args: + brush_name: 笔刷名称 + brush_function: 笔刷函数 + + Returns: + 是否创建成功 + """ + try: + # 在实际实现中,这里会注册自定义笔刷函数 + self.brush_types[brush_name] = { + 'name': brush_name, + 'description': '自定义笔刷', + 'function': brush_function + } + + print(f"✓ 自定义笔刷 '{brush_name}' 已创建") + return True + + except Exception as e: + print(f"✗ 自定义笔刷创建失败: {e}") + return False + + def set_ui_component(self, component_name: str, component: Any): + """ + 设置UI组件引用 + + Args: + component_name: 组件名称 + component: 组件引用 + """ + if component_name in self.ui_components: + self.ui_components[component_name] = component + print(f"✓ UI组件 '{component_name}' 已设置") + else: + print(f"✗ 无效的UI组件名称: {component_name}") + + def refresh_ui(self): + """刷新UI显示""" + try: + # 在实际实现中,这里会更新所有UI组件的显示 + print("✓ UI刷新完成") + except Exception as e: + print(f"✗ UI刷新失败: {e}") + + def show_preview_window(self): + """显示预览窗口""" + try: + # 在实际实现中,这里会创建并显示预览窗口 + print("✓ 预览窗口已显示") + except Exception as e: + print(f"✗ 预览窗口显示失败: {e}") + + def hide_preview_window(self): + """隐藏预览窗口""" + try: + # 在实际实现中,这里会隐藏预览窗口 + print("✓ 预览窗口已隐藏") + except Exception as e: + print(f"✗ 预览窗口隐藏失败: {e}") + + def update_preview_window(self): + """更新预览窗口内容""" + try: + # 在实际实现中,这里会更新预览窗口的内容 + print("✓ 预览窗口已更新") + except Exception as e: + print(f"✗ 预览窗口更新失败: {e}") + + def set_brush_position(self, x: int, y: int): + """ + 设置笔刷位置(用于预览显示) + + Args: + x: X坐标 + y: Y坐标 + """ + try: + self.selected_brush_position = (x, y) + # 在实际实现中,这里会更新UI上的笔刷预览位置 + except Exception as e: + print(f"✗ 笔刷位置设置失败: {e}") + + def clear_brush_position(self): + """清除笔刷位置""" + try: + self.selected_brush_position = None + # 在实际实现中,这里会清除UI上的笔刷预览位置 + except Exception as e: + print(f"✗ 笔刷位置清除失败: {e}") + + def apply_generator_to_selection(self, generator_type: str = 'mountain', generator_params: Dict[str, Any] = None): + """ + 对选择区域应用生成器 + + Args: + generator_type: 生成器类型 ('mountain', 'valley', 'plateau', 'crater', 'island') + generator_params: 生成器参数 + """ + try: + if not self.enabled: + print("✗ 地形编辑器未启用") + return + + if self.selected_area is None: + print("✗ 没有选择区域") + return + + # 保存状态到历史记录 + self._save_state_to_history() + + # 获取选择区域 + min_x, min_y, max_x, max_y = self.selected_area + width = max_x - min_x + 1 + height = max_y - min_y + 1 + + # 获取地形数据 + if self.plugin.terrain_manager and self.plugin.heightmap_generator: + terrain_data = self.plugin.terrain_manager.get_terrain_data() + heightmap = terrain_data.get('heightmap') + + if heightmap is not None: + # 生成新的地形片段 + if generator_type == 'mountain': + new_heightmap = self.plugin.heightmap_generator.generate_mountain_heightmap(width, height) + elif generator_type == 'valley': + new_heightmap = self.plugin.heightmap_generator.generate_valley_heightmap(width, height) + elif generator_type == 'plateau': + new_heightmap = self.plugin.heightmap_generator.generate_plateau_heightmap(width, height) + elif generator_type == 'crater': + new_heightmap = self.plugin.heightmap_generator.generate_crater_heightmap(width, height) + elif generator_type == 'island': + new_heightmap = self.plugin.heightmap_generator.generate_island_heightmap(width, height) + else: + # 默认使用基础噪声生成 + new_heightmap = self.plugin.heightmap_generator.generate_heightmap(width, height) + + # 应用到选择区域 + heightmap[min_y:max_y+1, min_x:max_x+1] = new_heightmap + + # 标记预览需要更新 + self.preview_dirty = True + + # 更新统计信息 + self.stats['edits_made'] += 1 + + print(f"✓ 生成器应用完成: {generator_type}") + + except Exception as e: + print(f"✗ 生成器应用失败: {e}") + import traceback + traceback.print_exc() + + def clone_area(self, source_x1: int, source_y1: int, source_x2: int, source_y2: int, + target_x: int, target_y: int): + """ + 克隆区域 + + Args: + source_x1: 源区域起始X坐标 + source_y1: 源区域起始Y坐标 + source_x2: 源区域结束X坐标 + source_y2: 源区域结束Y坐标 + target_x: 目标位置X坐标 + target_y: 目标位置Y坐标 + """ + try: + if not self.enabled: + print("✗ 地形编辑器未启用") + return + + # 保存状态到历史记录 + self._save_state_to_history() + + # 标准化源区域坐标 + source_min_x = min(source_x1, source_x2) + source_max_x = max(source_x1, source_x2) + source_min_y = min(source_y1, source_y2) + source_max_y = max(source_y1, source_y2) + + # 计算区域尺寸 + region_width = source_max_x - source_min_x + 1 + region_height = source_max_y - source_min_y + 1 + + # 获取地形数据 + if self.plugin.terrain_manager: + terrain_data = self.plugin.terrain_manager.get_terrain_data() + + # 克隆高度图 + heightmap = terrain_data.get('heightmap') + if heightmap is not None: + # 确保源区域有效 + if (0 <= source_min_x < heightmap.shape[1] and + 0 <= source_max_x < heightmap.shape[1] and + 0 <= source_min_y < heightmap.shape[0] and + 0 <= source_max_y < heightmap.shape[0]): + + # 提取源区域 + source_region = heightmap[source_min_y:source_max_y+1, source_min_x:source_max_x+1].copy() + + # 计算目标区域边界 + target_min_x = target_x + target_max_x = target_x + region_width - 1 + target_min_y = target_y + target_max_y = target_y + region_height - 1 + + # 确保目标区域在地图范围内 + if (0 <= target_min_x < heightmap.shape[1] and + 0 <= target_max_x < heightmap.shape[1] and + 0 <= target_min_y < heightmap.shape[0] and + 0 <= target_max_y < heightmap.shape[0]): + + # 应用到目标区域 + heightmap[target_min_y:target_max_y+1, target_min_x:target_max_x+1] = source_region + + # 标记预览需要更新 + self.preview_dirty = True + + # 更新统计信息 + self.stats['edits_made'] += 1 + + print("✓ 区域克隆完成") + return + + print("✗ 无法克隆区域:源区域或目标区域超出边界") + + except Exception as e: + print(f"✗ 区域克隆失败: {e}") + import traceback + traceback.print_exc() + + def flip_area(self, horizontal: bool = True, vertical: bool = False): + """ + 翻转选择区域 + + Args: + horizontal: 是否水平翻转 + vertical: 是否垂直翻转 + """ + try: + if not self.enabled: + print("✗ 地形编辑器未启用") + return + + if self.selected_area is None: + print("✗ 没有选择区域") + return + + # 保存状态到历史记录 + self._save_state_to_history() + + # 获取选择区域 + min_x, min_y, max_x, max_y = self.selected_area + + # 获取地形数据 + if self.plugin.terrain_manager: + terrain_data = self.plugin.terrain_manager.get_terrain_data() + + # 翻转高度图 + heightmap = terrain_data.get('heightmap') + if heightmap is not None: + region = heightmap[min_y:max_y+1, min_x:max_x+1] + if horizontal: + region = np.fliplr(region) + if vertical: + region = np.flipud(region) + heightmap[min_y:max_y+1, min_x:max_x+1] = region + + # 翻转生物群落图 + biome_map = terrain_data.get('biome_map') + if biome_map is not None: + region = biome_map[min_y:max_y+1, min_x:max_x+1] + if horizontal: + region = np.fliplr(region) + if vertical: + region = np.flipud(region) + biome_map[min_y:max_y+1, min_x:max_x+1] = region + + # 翻转植被图 + vegetation_map = terrain_data.get('vegetation_map') + if vegetation_map is not None: + region = vegetation_map[min_y:max_y+1, min_x:max_x+1] + if horizontal: + region = np.fliplr(region) + if vertical: + region = np.flipud(region) + vegetation_map[min_y:max_y+1, min_x:max_x+1] = region + + # 翻转水图 + water_map = terrain_data.get('water_map') + if water_map is not None: + region = water_map[min_y:max_y+1, min_x:max_x+1] + if horizontal: + region = np.fliplr(region) + if vertical: + region = np.flipud(region) + water_map[min_y:max_y+1, min_x:max_x+1] = region + + # 标记预览需要更新 + self.preview_dirty = True + + # 更新统计信息 + self.stats['edits_made'] += 1 + + print("✓ 区域翻转完成") + + except Exception as e: + print(f"✗ 区域翻转失败: {e}") + import traceback + traceback.print_exc() + + def rotate_area(self, angle: int = 90): + """ + 旋转选择区域 + + Args: + angle: 旋转角度 (90, 180, 270) + """ + try: + if not self.enabled: + print("✗ 地形编辑器未启用") + return + + if self.selected_area is None: + print("✗ 没有选择区域") + return + + # 保存状态到历史记录 + self._save_state_to_history() + + # 获取选择区域 + min_x, min_y, max_x, max_y = self.selected_area + region_width = max_x - min_x + 1 + region_height = max_y - min_y + 1 + + # 只支持正方形区域的旋转 + if region_width != region_height: + print("✗ 只支持正方形区域的旋转") + return + + # 获取地形数据 + if self.plugin.terrain_manager: + terrain_data = self.plugin.terrain_manager.get_terrain_data() + + # 旋转高度图 + heightmap = terrain_data.get('heightmap') + if heightmap is not None: + region = heightmap[min_y:max_y+1, min_x:max_x+1] + if angle == 90: + region = np.rot90(region, k=1) + elif angle == 180: + region = np.rot90(region, k=2) + elif angle == 270: + region = np.rot90(region, k=3) + heightmap[min_y:max_y+1, min_x:max_x+1] = region + + # 旋转生物群落图 + biome_map = terrain_data.get('biome_map') + if biome_map is not None: + region = biome_map[min_y:max_y+1, min_x:max_x+1] + if angle == 90: + region = np.rot90(region, k=1) + elif angle == 180: + region = np.rot90(region, k=2) + elif angle == 270: + region = np.rot90(region, k=3) + biome_map[min_y:max_y+1, min_x:max_x+1] = region + + # 旋转植被图 + vegetation_map = terrain_data.get('vegetation_map') + if vegetation_map is not None: + region = vegetation_map[min_y:max_y+1, min_x:max_x+1] + if angle == 90: + region = np.rot90(region, k=1) + elif angle == 180: + region = np.rot90(region, k=2) + elif angle == 270: + region = np.rot90(region, k=3) + vegetation_map[min_y:max_y+1, min_x:max_x+1] = region + + # 旋转水图 + water_map = terrain_data.get('water_map') + if water_map is not None: + region = water_map[min_y:max_y+1, min_x:max_x+1] + if angle == 90: + region = np.rot90(region, k=1) + elif angle == 180: + region = np.rot90(region, k=2) + elif angle == 270: + region = np.rot90(region, k=3) + water_map[min_y:max_y+1, min_x:max_x+1] = region + + # 标记预览需要更新 + self.preview_dirty = True + + # 更新统计信息 + self.stats['edits_made'] += 1 + + print(f"✓ 区域旋转完成 ({angle}度)") + + except Exception as e: + print(f"✗ 区域旋转失败: {e}") + import traceback traceback.print_exc() \ No newline at end of file diff --git a/plugins/user/procedural_terrain_generation/erosion/erosion_processor.py b/plugins/user/procedural_terrain_generation/erosion/erosion_processor.py index f04021b7..263dc48a 100644 --- a/plugins/user/procedural_terrain_generation/erosion/erosion_processor.py +++ b/plugins/user/procedural_terrain_generation/erosion/erosion_processor.py @@ -1,1201 +1,1201 @@ -""" -侵蚀处理器 -负责模拟地形的侵蚀过程 -""" - -import numpy as np -import math -from typing import Dict, Any -import time - -class ErosionProcessor: - """ - 侵蚀处理器 - 负责模拟地形的侵蚀过程,包括水力侵蚀、热力侵蚀、风力侵蚀等 - """ - - def __init__(self, plugin): - """ - 初始化侵蚀处理器 - - Args: - plugin: 程序化地形生成插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 侵蚀配置 - self.seed = plugin.config.get('seed', 12345) - - # 侵蚀参数 - self.erosion_params = { - 'water_erosion': { - 'enabled': True, - 'rate': 0.1, - 'solubility': 0.8, - 'evaporation': 0.01, - 'iterations': 50, - 'inertia': 0.05, - 'sediment_capacity': 0.5, - 'deposition_rate': 0.1 - }, - 'thermal_erosion': { - 'enabled': True, - 'rate': 0.05, - 'angle_of_repose': 30.0, # 度 - 'iterations': 30 - }, - 'wind_erosion': { - 'enabled': True, - 'rate': 0.02, - 'direction': 0.0, # 弧度 - 'iterations': 20 - }, - 'chemical_erosion': { - 'enabled': False, - 'rate': 0.01, - 'iterations': 10 - }, - 'glacial_erosion': { - 'enabled': False, - 'rate': 0.03, - 'iterations': 15 - } - } - - # 侵蚀图层 - self.erosion_map = None - self.sediment_map = None - self.water_map = None - self.flow_map = None - - # 统计信息 - self.stats = { - 'erosion_processed': 0, - 'total_processing_time': 0.0, - 'average_processing_time': 0.0, - 'total_erosion_amount': 0.0, - 'total_sediment_amount': 0.0 - } - - print("✓ 侵蚀处理器已创建") - - def initialize(self) -> bool: - """ - 初始化侵蚀处理器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 侵蚀处理器初始化完成") - return True - - except Exception as e: - print(f"✗ 侵蚀处理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用侵蚀处理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 侵蚀处理器未初始化") - return False - - self.enabled = True - print("✓ 侵蚀处理器已启用") - return True - - except Exception as e: - print(f"✗ 侵蚀处理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用侵蚀处理器""" - try: - self.enabled = False - print("✓ 侵蚀处理器已禁用") - - except Exception as e: - print(f"✗ 侵蚀处理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理侵蚀处理器资源""" - try: - self.disable() - self.initialized = False - print("✓ 侵蚀处理器资源已清理") - - except Exception as e: - print(f"✗ 侵蚀处理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新侵蚀处理器状态 - - Args: - dt: 时间增量 - """ - # 处理更新逻辑 - pass - - def apply_erosion(self, heightmap: np.ndarray, seed: int = None) -> Dict[str, np.ndarray]: - """ - 应用侵蚀效果到高度图 - - Args: - heightmap: 高度图数据 - seed: 随机种子 - - Returns: - 包含处理后高度图和侵蚀相关数据的字典 - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 侵蚀处理器未启用") - return { - 'heightmap': heightmap, - 'erosion_map': np.zeros_like(heightmap, dtype=np.float32), - 'sediment_map': np.zeros_like(heightmap, dtype=np.float32), - 'water_map': np.zeros_like(heightmap, dtype=np.float32) - } - - # 更新种子 - if seed is not None: - self.seed = seed - - print("✓ 开始应用侵蚀效果...") - - # 初始化侵蚀图层 - self._initialize_erosion_layers(heightmap.shape) - - # 创建高度图副本进行处理 - processed_heightmap = heightmap.copy() - - # 应用各种侵蚀类型 - if self.erosion_params['water_erosion']['enabled']: - print(" → 应用水力侵蚀...") - processed_heightmap = self._apply_water_erosion(processed_heightmap) - - if self.erosion_params['thermal_erosion']['enabled']: - print(" → 应用热力侵蚀...") - processed_heightmap = self._apply_thermal_erosion(processed_heightmap) - - if self.erosion_params['wind_erosion']['enabled']: - print(" → 应用风力侵蚀...") - processed_heightmap = self._apply_wind_erosion(processed_heightmap) - - if self.erosion_params['chemical_erosion']['enabled']: - print(" → 应用化学侵蚀...") - processed_heightmap = self._apply_chemical_erosion(processed_heightmap) - - if self.erosion_params['glacial_erosion']['enabled']: - print(" → 应用冰川侵蚀...") - processed_heightmap = self._apply_glacial_erosion(processed_heightmap) - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self.stats['erosion_processed'] += 1 - self.stats['total_processing_time'] += processing_time - self.stats['average_processing_time'] = self.stats['total_processing_time'] / self.stats['erosion_processed'] - - print(f"✓ 侵蚀处理完成,耗时: {processing_time:.2f}秒") - return { - 'heightmap': processed_heightmap, - 'erosion_map': self.erosion_map, - 'sediment_map': self.sediment_map, - 'water_map': self.water_map - } - - except Exception as e: - print(f"✗ 侵蚀处理失败: {e}") - import traceback - traceback.print_exc() - return { - 'heightmap': heightmap, - 'erosion_map': np.zeros_like(heightmap, dtype=np.float32), - 'sediment_map': np.zeros_like(heightmap, dtype=np.float32), - 'water_map': np.zeros_like(heightmap, dtype=np.float32) - } - - def _initialize_erosion_layers(self, shape: tuple): - """初始化侵蚀相关图层""" - try: - height, width = shape - self.erosion_map = np.zeros((height, width), dtype=np.float32) - self.sediment_map = np.zeros((height, width), dtype=np.float32) - self.water_map = np.zeros((height, width), dtype=np.float32) - self.flow_map = np.zeros((height, width, 2), dtype=np.float32) # x, y 流向 - except Exception as e: - print(f"✗ 侵蚀图层初始化失败: {e}") - - def _apply_water_erosion(self, heightmap: np.ndarray) -> np.ndarray: - """ - 应用水力侵蚀 - - Args: - heightmap: 高度图数据 - - Returns: - 处理后的高度图 - """ - try: - processed = heightmap.copy() - height, width = processed.shape - - # 获取水力侵蚀参数 - params = self.erosion_params['water_erosion'] - iterations = params['iterations'] - erosion_rate = params['rate'] - solubility = params['solubility'] - evaporation = params['evaporation'] - inertia = params['inertia'] - sediment_capacity = params['sediment_capacity'] - deposition_rate = params['deposition_rate'] - - # 模拟多个迭代步骤 - for iteration in range(iterations): - # 计算水流方向 - self._calculate_water_flow(processed) - - # 模拟侵蚀和沉积 - for y in range(height): - for x in range(width): - # 计算当前点的侵蚀量 - erosion_amount = erosion_rate * solubility * self.water_map[y, x] - - # 应用侵蚀 - if erosion_amount > 0: - processed[y, x] -= erosion_amount - self.erosion_map[y, x] += erosion_amount - self.sediment_map[y, x] += erosion_amount - - # 检查沉积(如果水流速度慢且携带沉积物过多) - sediment_ratio = self.sediment_map[y, x] / max(0.001, self.water_map[y, x]) - if sediment_ratio > sediment_capacity: - deposition_amount = (sediment_ratio - sediment_capacity) * deposition_rate - processed[y, x] += deposition_amount - self.sediment_map[y, x] -= deposition_amount - - # 应用蒸发 - self.water_map *= (1.0 - evaporation) - - # 确保高度值合理 - processed = np.clip(processed, 0.0, 1.0) - - # 更新统计信息 - self.stats['total_erosion_amount'] += np.sum(self.erosion_map) - self.stats['total_sediment_amount'] += np.sum(self.sediment_map) - - return processed - - except Exception as e: - print(f"✗ 水力侵蚀应用失败: {e}") - return heightmap - - def _calculate_water_flow(self, heightmap: np.ndarray): - """ - 计算水流方向和累积 - - Args: - heightmap: 高度图数据 - """ - try: - height, width = heightmap.shape - - # 简化的水流计算 - for y in range(height): - for x in range(width): - # 增加降雨量 - self.water_map[y, x] += 0.01 - - # 计算流向(流向最低的邻居) - lowest_height = heightmap[y, x] - flow_x, flow_y = 0, 0 - - # 检查8个邻居 - for dy in [-1, 0, 1]: - for dx in [-1, 0, 1]: - if dy == 0 and dx == 0: - continue - - ny, nx = y + dy, x + dx - if 0 <= ny < height and 0 <= nx < width: - neighbor_height = heightmap[ny, nx] + self.water_map[ny, nx] - if neighbor_height < lowest_height: - lowest_height = neighbor_height - flow_x, flow_y = dx, dy - - # 设置流向 - self.flow_map[y, x, 0] = flow_x - self.flow_map[y, x, 1] = flow_y - - # 将水流移动到下一个位置 - if flow_x != 0 or flow_y != 0: - ny, nx = y + int(flow_y), x + int(flow_x) - if 0 <= ny < height and 0 <= nx < width: - self.water_map[ny, nx] += self.water_map[y, x] * 0.8 - self.water_map[y, x] *= 0.2 - - except Exception as e: - print(f"✗ 水流计算失败: {e}") - - def _apply_thermal_erosion(self, heightmap: np.ndarray) -> np.ndarray: - """ - 应用热力侵蚀(重力侵蚀) - - Args: - heightmap: 高度图数据 - - Returns: - 处理后的高度图 - """ - try: - processed = heightmap.copy() - height, width = processed.shape - - # 获取热力侵蚀参数 - params = self.erosion_params['thermal_erosion'] - iterations = params['iterations'] - erosion_rate = params['rate'] - angle_of_repose = math.radians(params['angle_of_repose']) - tan_repose = math.tan(angle_of_repose) - - # 模拟多个迭代步骤 - for iteration in range(iterations): - new_heightmap = processed.copy() - - # 对每个点应用热力侵蚀 - for y in range(height): - for x in range(width): - current_height = processed[y, x] - - # 检查4个直接邻居 - for dy, dx in [(-1, 0), (1, 0), (0, -1), (0, 1)]: - ny, nx = y + dy, x + dx - if 0 <= ny < height and 0 <= nx < width: - neighbor_height = processed[ny, nx] - - # 计算高度差 - height_diff = current_height - neighbor_height - - # 如果高度差超过角度限制 - if height_diff > 0: - # 计算最大稳定高度差 - max_diff = tan_repose * 1.0 # 假设网格间距为1 - - # 如果超过最大稳定差值,则发生侵蚀 - if height_diff > max_diff: - # 移动材料到较低位置 - move_amount = (height_diff - max_diff) * erosion_rate - new_heightmap[y, x] -= move_amount - new_heightmap[ny, nx] += move_amount - - # 更新侵蚀图 - self.erosion_map[y, x] += move_amount - - processed = new_heightmap - - return processed - - except Exception as e: - print(f"✗ 热力侵蚀应用失败: {e}") - return heightmap - - def _apply_wind_erosion(self, heightmap: np.ndarray) -> np.ndarray: - """ - 应用风力侵蚀 - - Args: - heightmap: 高度图数据 - - Returns: - 处理后的高度图 - """ - try: - processed = heightmap.copy() - height, width = processed.shape - - # 获取风力侵蚀参数 - params = self.erosion_params['wind_erosion'] - iterations = params['iterations'] - erosion_rate = params['rate'] - wind_direction = params['direction'] - - # 计算风向向量 - wind_dx = math.cos(wind_direction) - wind_dy = math.sin(wind_direction) - - # 模拟多个迭代步骤 - for iteration in range(iterations): - new_heightmap = processed.copy() - - # 对每个点应用风力侵蚀 - for y in range(height): - for x in range(width): - # 风从特定方向吹来 - # 计算风的来源位置 - source_x = int(x - wind_dx * 2) # 2个单位距离 - source_y = int(y - wind_dy * 2) - - # 如果来源位置有效 - if 0 <= source_y < height and 0 <= source_x < width: - # 如果当前点比来源点高,则可能发生风蚀 - height_diff = processed[y, x] - processed[source_y, source_x] - if height_diff > 0.01: # 阈值 - # 应用风力侵蚀 - erosion_amount = erosion_rate * height_diff - new_heightmap[y, x] -= erosion_amount - self.erosion_map[y, x] += erosion_amount - - processed = new_heightmap - - return processed - - except Exception as e: - print(f"✗ 风力侵蚀应用失败: {e}") - return heightmap - - def _apply_chemical_erosion(self, heightmap: np.ndarray) -> np.ndarray: - """ - 应用化学侵蚀 - - Args: - heightmap: 高度图数据 - - Returns: - 处理后的高度图 - """ - try: - processed = heightmap.copy() - height, width = processed.shape - - # 获取化学侵蚀参数 - params = self.erosion_params['chemical_erosion'] - iterations = params['iterations'] - erosion_rate = params['rate'] - - # 模拟多个迭代步骤 - for iteration in range(iterations): - # 简化的化学侵蚀:基于高度和随机因素 - for y in range(height): - for x in range(width): - # 化学侵蚀通常影响较低的区域(如河流、湖泊) - if processed[y, x] < 0.3: # 假设低地更容易发生化学侵蚀 - # 添加随机因素 - chemical_factor = np.random.random() * 0.5 + 0.5 - erosion_amount = erosion_rate * chemical_factor * (0.3 - processed[y, x]) - processed[y, x] -= erosion_amount - self.erosion_map[y, x] += erosion_amount - - return processed - - except Exception as e: - print(f"✗ 化学侵蚀应用失败: {e}") - return heightmap - - def _apply_glacial_erosion(self, heightmap: np.ndarray) -> np.ndarray: - """ - 应用冰川侵蚀 - - Args: - heightmap: 高度图数据 - - Returns: - 处理后的高度图 - """ - try: - processed = heightmap.copy() - height, width = processed.shape - - # 获取冰川侵蚀参数 - params = self.erosion_params['glacial_erosion'] - iterations = params['iterations'] - erosion_rate = params['rate'] - - # 模拟多个迭代步骤 - for iteration in range(iterations): - # 冰川侵蚀:平滑地形并创建U型谷 - new_heightmap = processed.copy() - - for y in range(height): - for x in range(width): - # 冰川通常在高海拔地区形成 - if processed[y, x] > 0.7: # 高海拔 - # 平滑周围区域 - total_height = 0.0 - count = 0 - - # 检查3x3邻域 - for dy in [-1, 0, 1]: - for dx in [-1, 0, 1]: - ny, nx = y + dy, x + dx - if 0 <= ny < height and 0 <= nx < width: - total_height += processed[ny, nx] - count += 1 - - if count > 0: - average_height = total_height / count - # 应用冰川侵蚀(平滑化) - glacial_effect = erosion_rate * (processed[y, x] - average_height) - new_heightmap[y, x] -= glacial_effect - self.erosion_map[y, x] += abs(glacial_effect) - - processed = new_heightmap - - return processed - - except Exception as e: - print(f"✗ 冰川侵蚀应用失败: {e}") - return heightmap - - def set_seed(self, seed: int): - """ - 设置随机种子 - - Args: - seed: 随机种子 - """ - self.seed = seed - print(f"✓ 侵蚀处理器随机种子设置为: {seed}") - - def set_erosion_parameters(self, erosion_type: str, params: Dict[str, Any]): - """ - 设置侵蚀参数 - - Args: - erosion_type: 侵蚀类型 - params: 参数字典 - """ - if erosion_type in self.erosion_params: - self.erosion_params[erosion_type].update(params) - print(f"✓ {erosion_type} 侵蚀参数已更新: {self.erosion_params[erosion_type]}") - else: - print(f"✗ 无效的侵蚀类型: {erosion_type}") - - def enable_erosion_type(self, erosion_type: str, enabled: bool): - """ - 启用/禁用侵蚀类型 - - Args: - erosion_type: 侵蚀类型 - enabled: 是否启用 - """ - if erosion_type in self.erosion_params: - self.erosion_params[erosion_type]['enabled'] = enabled - state = "启用" if enabled else "禁用" - print(f"✓ {erosion_type} 侵蚀已{state}") - else: - print(f"✗ 无效的侵蚀类型: {erosion_type}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - # 更新平均处理时间 - if self.stats['erosion_processed'] > 0: - self.stats['average_processing_time'] = self.stats['total_processing_time'] / self.stats['erosion_processed'] - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'erosion_processed': 0, - 'total_processing_time': 0.0, - 'average_processing_time': 0.0, - 'total_erosion_amount': 0.0, - 'total_sediment_amount': 0.0 - } - print("✓ 侵蚀处理器统计信息已重置") - - def get_erosion_map(self) -> np.ndarray: - """ - 获取侵蚀图 - - Returns: - 侵蚀图数据 - """ - return self.erosion_map if self.erosion_map is not None else np.array([]) - - def get_sediment_map(self) -> np.ndarray: - """ - 获取沉积物图 - - Returns: - 沉积物图数据 - """ - return self.sediment_map if self.sediment_map is not None else np.array([]) - - def get_water_map(self) -> np.ndarray: - """ - 获取水图 - - Returns: - 水图数据 - """ - return self.water_map if self.water_map is not None else np.array([]) - - def set_water_erosion_parameters(self, rate: float = None, solubility: float = None, - evaporation: float = None, inertia: float = None, - sediment_capacity: float = None, deposition_rate: float = None): - """ - 设置水力侵蚀参数 - - Args: - rate: 侵蚀速率 - solubility: 溶解度 - evaporation: 蒸发率 - inertia: 惯性 - sediment_capacity: 沉积物容量 - deposition_rate: 沉积率 - """ - params = self.erosion_params['water_erosion'] - if rate is not None: - params['rate'] = max(0.0, rate) - if solubility is not None: - params['solubility'] = max(0.0, min(1.0, solubility)) - if evaporation is not None: - params['evaporation'] = max(0.0, min(1.0, evaporation)) - if inertia is not None: - params['inertia'] = max(0.0, min(1.0, inertia)) - if sediment_capacity is not None: - params['sediment_capacity'] = max(0.0, sediment_capacity) - if deposition_rate is not None: - params['deposition_rate'] = max(0.0, deposition_rate) - - print(f"✓ 水力侵蚀参数已更新: {params}") - - def set_thermal_erosion_parameters(self, rate: float = None, angle_of_repose: float = None): - """ - 设置热力侵蚀参数 - - Args: - rate: 侵蚀速率 - angle_of_repose: 休止角(度) - """ - params = self.erosion_params['thermal_erosion'] - if rate is not None: - params['rate'] = max(0.0, rate) - if angle_of_repose is not None: - params['angle_of_repose'] = max(0.0, angle_of_repose) - - print(f"✓ 热力侵蚀参数已更新: {params}") - - def set_wind_erosion_parameters(self, rate: float = None, direction: float = None): - """ - 设置风力侵蚀参数 - - Args: - rate: 侵蚀速率 - direction: 风向(弧度) - """ - params = self.erosion_params['wind_erosion'] - if rate is not None: - params['rate'] = max(0.0, rate) - if direction is not None: - params['direction'] = direction - - print(f"✓ 风力侵蚀参数已更新: {params}") - - def apply_selective_erosion(self, heightmap: np.ndarray, mask: np.ndarray, - seed: int = None) -> Dict[str, np.ndarray]: - """ - 在指定区域应用侵蚀 - - Args: - heightmap: 高度图数据 - mask: 掩码(0-1范围,1表示完全应用侵蚀) - seed: 随机种子 - - Returns: - 包含处理后高度图和侵蚀相关数据的字典 - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 侵蚀处理器未启用") - return { - 'heightmap': heightmap, - 'erosion_map': np.zeros_like(heightmap, dtype=np.float32), - 'sediment_map': np.zeros_like(heightmap, dtype=np.float32), - 'water_map': np.zeros_like(heightmap, dtype=np.float32) - } - - # 更新种子 - if seed is not None: - self.seed = seed - - print("✓ 开始应用选择性侵蚀...") - - # 确保掩码尺寸匹配 - if mask.shape != heightmap.shape: - print("✗ 掩码尺寸与高度图不匹配") - return { - 'heightmap': heightmap, - 'erosion_map': np.zeros_like(heightmap, dtype=np.float32), - 'sediment_map': np.zeros_like(heightmap, dtype=np.float32), - 'water_map': np.zeros_like(heightmap, dtype=np.float32) - } - - # 初始化侵蚀图层 - self._initialize_erosion_layers(heightmap.shape) - - # 创建高度图副本进行处理 - processed_heightmap = heightmap.copy() - - # 在掩码区域应用侵蚀 - mask_indices = np.where(mask > 0.5) - - # 创建临时高度图只包含掩码区域 - temp_heightmap = heightmap.copy() - - # 应用各种侵蚀类型(只在掩码区域) - if self.erosion_params['water_erosion']['enabled']: - print(" → 应用水力侵蚀(选择性)...") - temp_heightmap = self._apply_water_erosion_selective(temp_heightmap, mask) - - if self.erosion_params['thermal_erosion']['enabled']: - print(" → 应用热力侵蚀(选择性)...") - temp_heightmap = self._apply_thermal_erosion_selective(temp_heightmap, mask) - - if self.erosion_params['wind_erosion']['enabled']: - print(" → 应用风力侵蚀(选择性)...") - temp_heightmap = self._apply_wind_erosion_selective(temp_heightmap, mask) - - # 只更新掩码区域的值 - processed_heightmap[mask_indices] = temp_heightmap[mask_indices] - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self.stats['erosion_processed'] += 1 - self.stats['total_processing_time'] += processing_time - self.stats['average_processing_time'] = self.stats['total_processing_time'] / self.stats['erosion_processed'] - - print(f"✓ 选择性侵蚀处理完成,耗时: {processing_time:.2f}秒") - return { - 'heightmap': processed_heightmap, - 'erosion_map': self.erosion_map, - 'sediment_map': self.sediment_map, - 'water_map': self.water_map - } - - except Exception as e: - print(f"✗ 选择性侵蚀处理失败: {e}") - import traceback - traceback.print_exc() - return { - 'heightmap': heightmap, - 'erosion_map': np.zeros_like(heightmap, dtype=np.float32), - 'sediment_map': np.zeros_like(heightmap, dtype=np.float32), - 'water_map': np.zeros_like(heightmap, dtype=np.float32) - } - - def _apply_water_erosion_selective(self, heightmap: np.ndarray, mask: np.ndarray) -> np.ndarray: - """在选择区域应用水力侵蚀""" - try: - processed = heightmap.copy() - height, width = processed.shape - - # 获取水力侵蚀参数 - params = self.erosion_params['water_erosion'] - iterations = params['iterations'] - erosion_rate = params['rate'] * 0.5 # 降低速率以适应选择性应用 - - # 只在掩码区域应用侵蚀 - mask_indices = np.where(mask > 0.5) - - # 简化的选择性水力侵蚀 - for iteration in range(iterations): - for y, x in zip(mask_indices[0], mask_indices[1]): - # 简单的侵蚀:降低高点,填充低点 - if processed[y, x] > 0.8: # 高点 - erosion_amount = erosion_rate * mask[y, x] - processed[y, x] -= erosion_amount - self.erosion_map[y, x] += erosion_amount - elif processed[y, x] < 0.2: # 低点 - deposition_amount = erosion_rate * 0.5 * mask[y, x] - processed[y, x] += deposition_amount - self.sediment_map[y, x] += deposition_amount - - return processed - - except Exception as e: - print(f"✗ 选择性水力侵蚀应用失败: {e}") - return heightmap - - def _apply_thermal_erosion_selective(self, heightmap: np.ndarray, mask: np.ndarray) -> np.ndarray: - """在选择区域应用热力侵蚀""" - try: - processed = heightmap.copy() - height, width = processed.shape - - # 获取热力侵蚀参数 - params = self.erosion_params['thermal_erosion'] - iterations = params['iterations'] - erosion_rate = params['rate'] * 0.5 # 降低速率以适应选择性应用 - - # 只在掩码区域应用侵蚀 - mask_indices = np.where(mask > 0.5) - - # 简化的选择性热力侵蚀 - for iteration in range(iterations): - for y, x in zip(mask_indices[0], mask_indices[1]): - # 检查直接邻居 - for dy, dx in [(-1, 0), (1, 0), (0, -1), (0, 1)]: - ny, nx = y + dy, x + dx - if 0 <= ny < height and 0 <= nx < width: - height_diff = processed[y, x] - processed[ny, nx] - if height_diff > 0.1: # 显著高度差 - move_amount = erosion_rate * height_diff * mask[y, x] - processed[y, x] -= move_amount - processed[ny, nx] += move_amount - self.erosion_map[y, x] += move_amount - - return processed - - except Exception as e: - print(f"✗ 选择性热力侵蚀应用失败: {e}") - return heightmap - - def _apply_wind_erosion_selective(self, heightmap: np.ndarray, mask: np.ndarray) -> np.ndarray: - """在选择区域应用风力侵蚀""" - try: - processed = heightmap.copy() - height, width = processed.shape - - # 获取风力侵蚀参数 - params = self.erosion_params['wind_erosion'] - erosion_rate = params['rate'] * 0.5 # 降低速率以适应选择性应用 - wind_direction = params['direction'] - - # 计算风向向量 - wind_dx = math.cos(wind_direction) - wind_dy = math.sin(wind_direction) - - # 只在掩码区域应用侵蚀 - mask_indices = np.where(mask > 0.5) - - # 简化的选择性风力侵蚀 - for y, x in zip(mask_indices[0], mask_indices[1]): - # 风从特定方向吹来 - source_x = int(x - wind_dx * 1) # 1个单位距离 - source_y = int(y - wind_dy * 1) - - # 如果来源位置有效且也在掩码区域内 - if (0 <= source_y < height and 0 <= source_x < width and - mask[source_y, source_x] > 0.5): - # 应用风力侵蚀 - height_diff = processed[y, x] - processed[source_y, source_x] - if height_diff > 0.01: - erosion_amount = erosion_rate * height_diff * mask[y, x] - processed[y, x] -= erosion_amount - self.erosion_map[y, x] += erosion_amount - - return processed - - except Exception as e: - print(f"✗ 选择性风力侵蚀应用失败: {e}") - return heightmap - - def blend_erosion_maps(self, erosion_maps: List[np.ndarray], weights: List[float] = None) -> np.ndarray: - """ - 混合多个侵蚀图 - - Args: - erosion_maps: 侵蚀图列表 - weights: 权重列表(如果为None,则平均分配权重) - - Returns: - 混合后的侵蚀图 - """ - try: - if not erosion_maps: - return np.array([]) - - if len(erosion_maps) == 1: - return erosion_maps[0].copy() - - # 确保所有侵蚀图尺寸相同 - base_shape = erosion_maps[0].shape - for em in erosion_maps: - if em.shape != base_shape: - raise ValueError("所有侵蚀图必须具有相同的尺寸") - - # 处理权重 - if weights is None: - weights = [1.0 / len(erosion_maps)] * len(erosion_maps) - elif len(weights) != len(erosion_maps): - raise ValueError("权重数量必须与侵蚀图数量相同") - - # 归一化权重 - total_weight = sum(weights) - if total_weight > 0: - weights = [w / total_weight for w in weights] - - # 混合侵蚀图 - blended = np.zeros(base_shape, dtype=np.float32) - for i, (em, weight) in enumerate(zip(erosion_maps, weights)): - blended += em * weight - - return blended - - except Exception as e: - print(f"✗ 侵蚀图混合失败: {e}") - import traceback - traceback.print_exc() - # 返回第一个侵蚀图的副本 - return erosion_maps[0].copy() if erosion_maps else np.array([]) - - def smooth_erosion_map(self, erosion_map: np.ndarray, kernel_size: int = 3) -> np.ndarray: - """ - 平滑侵蚀图 - - Args: - erosion_map: 原始侵蚀图 - kernel_size: 平滑核大小 - - Returns: - 平滑后的侵蚀图 - """ - try: - if kernel_size < 1: - return erosion_map - - smoothed = erosion_map.copy() - height, width = smoothed.shape - - # 简单的均值滤波 - half_kernel = kernel_size // 2 - - for y in range(height): - for x in range(width): - total = 0.0 - count = 0 - - # 应用平滑核 - for dy in range(-half_kernel, half_kernel + 1): - for dx in range(-half_kernel, half_kernel + 1): - ny, nx = y + dy, x + dx - if 0 <= ny < height and 0 <= nx < width: - total += erosion_map[ny, nx] - count += 1 - - if count > 0: - smoothed[y, x] = total / count - - return smoothed - - except Exception as e: - print(f"✗ 侵蚀图平滑失败: {e}") - return erosion_map - - def apply_erosion_intensity(self, heightmap: np.ndarray, intensity_map: np.ndarray, - seed: int = None) -> Dict[str, np.ndarray]: - """ - 根据强度图应用侵蚀 - - Args: - heightmap: 高度图数据 - intensity_map: 侵蚀强度图(0-1范围) - seed: 随机种子 - - Returns: - 包含处理后高度图和侵蚀相关数据的字典 - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 侵蚀处理器未启用") - return { - 'heightmap': heightmap, - 'erosion_map': np.zeros_like(heightmap, dtype=np.float32), - 'sediment_map': np.zeros_like(heightmap, dtype=np.float32), - 'water_map': np.zeros_like(heightmap, dtype=np.float32) - } - - # 更新种子 - if seed is not None: - self.seed = seed - - # 确保强度图尺寸匹配 - if intensity_map.shape != heightmap.shape: - print("✗ 强度图尺寸与高度图不匹配") - return { - 'heightmap': heightmap, - 'erosion_map': np.zeros_like(heightmap, dtype=np.float32), - 'sediment_map': np.zeros_like(heightmap, dtype=np.float32), - 'water_map': np.zeros_like(heightmap, dtype=np.float32) - } - - print("✓ 开始应用强度控制的侵蚀...") - - # 初始化侵蚀图层 - self._initialize_erosion_layers(heightmap.shape) - - # 创建高度图副本进行处理 - processed_heightmap = heightmap.copy() - - # 根据强度图调整侵蚀参数 - base_water_params = self.erosion_params['water_erosion'].copy() - base_thermal_params = self.erosion_params['thermal_erosion'].copy() - base_wind_params = self.erosion_params['wind_erosion'].copy() - - # 对每个点根据强度应用侵蚀 - height, width = processed_heightmap.shape - for y in range(height): - for x in range(width): - intensity = intensity_map[y, x] - if intensity > 0.1: # 只在强度大于阈值时应用侵蚀 - # 调整侵蚀参数 - self.erosion_params['water_erosion']['rate'] = base_water_params['rate'] * intensity - self.erosion_params['thermal_erosion']['rate'] = base_thermal_params['rate'] * intensity - self.erosion_params['wind_erosion']['rate'] = base_wind_params['rate'] * intensity - - # 应用局部侵蚀(简化版本) - if self.erosion_params['water_erosion']['enabled']: - # 简化的水力侵蚀 - if processed_heightmap[y, x] > 0.8: - erosion_amount = self.erosion_params['water_erosion']['rate'] * 0.1 - processed_heightmap[y, x] -= erosion_amount - self.erosion_map[y, x] += erosion_amount - - # 恢复原始侵蚀参数 - self.erosion_params['water_erosion'] = base_water_params - self.erosion_params['thermal_erosion'] = base_thermal_params - self.erosion_params['wind_erosion'] = base_wind_params - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self.stats['erosion_processed'] += 1 - self.stats['total_processing_time'] += processing_time - self.stats['average_processing_time'] = self.stats['total_processing_time'] / self.stats['erosion_processed'] - - print(f"✓ 强度控制的侵蚀处理完成,耗时: {processing_time:.2f}秒") - return { - 'heightmap': processed_heightmap, - 'erosion_map': self.erosion_map, - 'sediment_map': self.sediment_map, - 'water_map': self.water_map - } - - except Exception as e: - print(f"✗ 强度控制的侵蚀处理失败: {e}") - import traceback - traceback.print_exc() - return { - 'heightmap': heightmap, - 'erosion_map': np.zeros_like(heightmap, dtype=np.float32), - 'sediment_map': np.zeros_like(heightmap, dtype=np.float32), - 'water_map': np.zeros_like(heightmap, dtype=np.float32) - } - - def export_erosion_data(self, filename: str) -> bool: - """ - 导出侵蚀数据 - - Args: - filename: 文件名 - - Returns: - 是否导出成功 - """ - try: - import json - - erosion_data = { - 'parameters': self.erosion_params, - 'stats': self.stats, - 'timestamp': time.time() - } - - with open(filename, 'w', encoding='utf-8') as f: - json.dump(erosion_data, f, ensure_ascii=False, indent=2) - - print(f"✓ 侵蚀数据已导出到: {filename}") - return True - - except Exception as e: - print(f"✗ 侵蚀数据导出失败: {e}") - return False - - def import_erosion_data(self, filename: str) -> bool: - """ - 导入侵蚀数据 - - Args: - filename: 文件名 - - Returns: - 是否导入成功 - """ - try: - import json - - with open(filename, 'r', encoding='utf-8') as f: - erosion_data = json.load(f) - - # 更新参数 - if 'parameters' in erosion_data: - self.erosion_params.update(erosion_data['parameters']) - - # 更新统计信息 - if 'stats' in erosion_data: - self.stats.update(erosion_data['stats']) - - print(f"✓ 侵蚀数据已从 {filename} 导入") - return True - - except Exception as e: - print(f"✗ 侵蚀数据导入失败: {e}") - return False - - def reset_erosion_maps(self): - """重置侵蚀图层""" - self.erosion_map = None - self.sediment_map = None - self.water_map = None - self.flow_map = None - print("✓ 侵蚀图层已重置") - - def get_erosion_types(self) -> List[str]: - """ - 获取可用的侵蚀类型 - - Returns: - 侵蚀类型列表 - """ - return list(self.erosion_params.keys()) - - def get_erosion_parameters(self, erosion_type: str) -> Dict[str, Any]: - """ - 获取指定侵蚀类型的参数 - - Args: - erosion_type: 侵蚀类型 - - Returns: - 参数字典 - """ +""" +侵蚀处理器 +负责模拟地形的侵蚀过程 +""" + +import numpy as np +import math +from typing import Dict, Any +import time + +class ErosionProcessor: + """ + 侵蚀处理器 + 负责模拟地形的侵蚀过程,包括水力侵蚀、热力侵蚀、风力侵蚀等 + """ + + def __init__(self, plugin): + """ + 初始化侵蚀处理器 + + Args: + plugin: 程序化地形生成插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 侵蚀配置 + self.seed = plugin.config.get('seed', 12345) + + # 侵蚀参数 + self.erosion_params = { + 'water_erosion': { + 'enabled': True, + 'rate': 0.1, + 'solubility': 0.8, + 'evaporation': 0.01, + 'iterations': 50, + 'inertia': 0.05, + 'sediment_capacity': 0.5, + 'deposition_rate': 0.1 + }, + 'thermal_erosion': { + 'enabled': True, + 'rate': 0.05, + 'angle_of_repose': 30.0, # 度 + 'iterations': 30 + }, + 'wind_erosion': { + 'enabled': True, + 'rate': 0.02, + 'direction': 0.0, # 弧度 + 'iterations': 20 + }, + 'chemical_erosion': { + 'enabled': False, + 'rate': 0.01, + 'iterations': 10 + }, + 'glacial_erosion': { + 'enabled': False, + 'rate': 0.03, + 'iterations': 15 + } + } + + # 侵蚀图层 + self.erosion_map = None + self.sediment_map = None + self.water_map = None + self.flow_map = None + + # 统计信息 + self.stats = { + 'erosion_processed': 0, + 'total_processing_time': 0.0, + 'average_processing_time': 0.0, + 'total_erosion_amount': 0.0, + 'total_sediment_amount': 0.0 + } + + print("✓ 侵蚀处理器已创建") + + def initialize(self) -> bool: + """ + 初始化侵蚀处理器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 侵蚀处理器初始化完成") + return True + + except Exception as e: + print(f"✗ 侵蚀处理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用侵蚀处理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 侵蚀处理器未初始化") + return False + + self.enabled = True + print("✓ 侵蚀处理器已启用") + return True + + except Exception as e: + print(f"✗ 侵蚀处理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用侵蚀处理器""" + try: + self.enabled = False + print("✓ 侵蚀处理器已禁用") + + except Exception as e: + print(f"✗ 侵蚀处理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理侵蚀处理器资源""" + try: + self.disable() + self.initialized = False + print("✓ 侵蚀处理器资源已清理") + + except Exception as e: + print(f"✗ 侵蚀处理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新侵蚀处理器状态 + + Args: + dt: 时间增量 + """ + # 处理更新逻辑 + pass + + def apply_erosion(self, heightmap: np.ndarray, seed: int = None) -> Dict[str, np.ndarray]: + """ + 应用侵蚀效果到高度图 + + Args: + heightmap: 高度图数据 + seed: 随机种子 + + Returns: + 包含处理后高度图和侵蚀相关数据的字典 + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 侵蚀处理器未启用") + return { + 'heightmap': heightmap, + 'erosion_map': np.zeros_like(heightmap, dtype=np.float32), + 'sediment_map': np.zeros_like(heightmap, dtype=np.float32), + 'water_map': np.zeros_like(heightmap, dtype=np.float32) + } + + # 更新种子 + if seed is not None: + self.seed = seed + + print("✓ 开始应用侵蚀效果...") + + # 初始化侵蚀图层 + self._initialize_erosion_layers(heightmap.shape) + + # 创建高度图副本进行处理 + processed_heightmap = heightmap.copy() + + # 应用各种侵蚀类型 + if self.erosion_params['water_erosion']['enabled']: + print(" → 应用水力侵蚀...") + processed_heightmap = self._apply_water_erosion(processed_heightmap) + + if self.erosion_params['thermal_erosion']['enabled']: + print(" → 应用热力侵蚀...") + processed_heightmap = self._apply_thermal_erosion(processed_heightmap) + + if self.erosion_params['wind_erosion']['enabled']: + print(" → 应用风力侵蚀...") + processed_heightmap = self._apply_wind_erosion(processed_heightmap) + + if self.erosion_params['chemical_erosion']['enabled']: + print(" → 应用化学侵蚀...") + processed_heightmap = self._apply_chemical_erosion(processed_heightmap) + + if self.erosion_params['glacial_erosion']['enabled']: + print(" → 应用冰川侵蚀...") + processed_heightmap = self._apply_glacial_erosion(processed_heightmap) + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self.stats['erosion_processed'] += 1 + self.stats['total_processing_time'] += processing_time + self.stats['average_processing_time'] = self.stats['total_processing_time'] / self.stats['erosion_processed'] + + print(f"✓ 侵蚀处理完成,耗时: {processing_time:.2f}秒") + return { + 'heightmap': processed_heightmap, + 'erosion_map': self.erosion_map, + 'sediment_map': self.sediment_map, + 'water_map': self.water_map + } + + except Exception as e: + print(f"✗ 侵蚀处理失败: {e}") + import traceback + traceback.print_exc() + return { + 'heightmap': heightmap, + 'erosion_map': np.zeros_like(heightmap, dtype=np.float32), + 'sediment_map': np.zeros_like(heightmap, dtype=np.float32), + 'water_map': np.zeros_like(heightmap, dtype=np.float32) + } + + def _initialize_erosion_layers(self, shape: tuple): + """初始化侵蚀相关图层""" + try: + height, width = shape + self.erosion_map = np.zeros((height, width), dtype=np.float32) + self.sediment_map = np.zeros((height, width), dtype=np.float32) + self.water_map = np.zeros((height, width), dtype=np.float32) + self.flow_map = np.zeros((height, width, 2), dtype=np.float32) # x, y 流向 + except Exception as e: + print(f"✗ 侵蚀图层初始化失败: {e}") + + def _apply_water_erosion(self, heightmap: np.ndarray) -> np.ndarray: + """ + 应用水力侵蚀 + + Args: + heightmap: 高度图数据 + + Returns: + 处理后的高度图 + """ + try: + processed = heightmap.copy() + height, width = processed.shape + + # 获取水力侵蚀参数 + params = self.erosion_params['water_erosion'] + iterations = params['iterations'] + erosion_rate = params['rate'] + solubility = params['solubility'] + evaporation = params['evaporation'] + inertia = params['inertia'] + sediment_capacity = params['sediment_capacity'] + deposition_rate = params['deposition_rate'] + + # 模拟多个迭代步骤 + for iteration in range(iterations): + # 计算水流方向 + self._calculate_water_flow(processed) + + # 模拟侵蚀和沉积 + for y in range(height): + for x in range(width): + # 计算当前点的侵蚀量 + erosion_amount = erosion_rate * solubility * self.water_map[y, x] + + # 应用侵蚀 + if erosion_amount > 0: + processed[y, x] -= erosion_amount + self.erosion_map[y, x] += erosion_amount + self.sediment_map[y, x] += erosion_amount + + # 检查沉积(如果水流速度慢且携带沉积物过多) + sediment_ratio = self.sediment_map[y, x] / max(0.001, self.water_map[y, x]) + if sediment_ratio > sediment_capacity: + deposition_amount = (sediment_ratio - sediment_capacity) * deposition_rate + processed[y, x] += deposition_amount + self.sediment_map[y, x] -= deposition_amount + + # 应用蒸发 + self.water_map *= (1.0 - evaporation) + + # 确保高度值合理 + processed = np.clip(processed, 0.0, 1.0) + + # 更新统计信息 + self.stats['total_erosion_amount'] += np.sum(self.erosion_map) + self.stats['total_sediment_amount'] += np.sum(self.sediment_map) + + return processed + + except Exception as e: + print(f"✗ 水力侵蚀应用失败: {e}") + return heightmap + + def _calculate_water_flow(self, heightmap: np.ndarray): + """ + 计算水流方向和累积 + + Args: + heightmap: 高度图数据 + """ + try: + height, width = heightmap.shape + + # 简化的水流计算 + for y in range(height): + for x in range(width): + # 增加降雨量 + self.water_map[y, x] += 0.01 + + # 计算流向(流向最低的邻居) + lowest_height = heightmap[y, x] + flow_x, flow_y = 0, 0 + + # 检查8个邻居 + for dy in [-1, 0, 1]: + for dx in [-1, 0, 1]: + if dy == 0 and dx == 0: + continue + + ny, nx = y + dy, x + dx + if 0 <= ny < height and 0 <= nx < width: + neighbor_height = heightmap[ny, nx] + self.water_map[ny, nx] + if neighbor_height < lowest_height: + lowest_height = neighbor_height + flow_x, flow_y = dx, dy + + # 设置流向 + self.flow_map[y, x, 0] = flow_x + self.flow_map[y, x, 1] = flow_y + + # 将水流移动到下一个位置 + if flow_x != 0 or flow_y != 0: + ny, nx = y + int(flow_y), x + int(flow_x) + if 0 <= ny < height and 0 <= nx < width: + self.water_map[ny, nx] += self.water_map[y, x] * 0.8 + self.water_map[y, x] *= 0.2 + + except Exception as e: + print(f"✗ 水流计算失败: {e}") + + def _apply_thermal_erosion(self, heightmap: np.ndarray) -> np.ndarray: + """ + 应用热力侵蚀(重力侵蚀) + + Args: + heightmap: 高度图数据 + + Returns: + 处理后的高度图 + """ + try: + processed = heightmap.copy() + height, width = processed.shape + + # 获取热力侵蚀参数 + params = self.erosion_params['thermal_erosion'] + iterations = params['iterations'] + erosion_rate = params['rate'] + angle_of_repose = math.radians(params['angle_of_repose']) + tan_repose = math.tan(angle_of_repose) + + # 模拟多个迭代步骤 + for iteration in range(iterations): + new_heightmap = processed.copy() + + # 对每个点应用热力侵蚀 + for y in range(height): + for x in range(width): + current_height = processed[y, x] + + # 检查4个直接邻居 + for dy, dx in [(-1, 0), (1, 0), (0, -1), (0, 1)]: + ny, nx = y + dy, x + dx + if 0 <= ny < height and 0 <= nx < width: + neighbor_height = processed[ny, nx] + + # 计算高度差 + height_diff = current_height - neighbor_height + + # 如果高度差超过角度限制 + if height_diff > 0: + # 计算最大稳定高度差 + max_diff = tan_repose * 1.0 # 假设网格间距为1 + + # 如果超过最大稳定差值,则发生侵蚀 + if height_diff > max_diff: + # 移动材料到较低位置 + move_amount = (height_diff - max_diff) * erosion_rate + new_heightmap[y, x] -= move_amount + new_heightmap[ny, nx] += move_amount + + # 更新侵蚀图 + self.erosion_map[y, x] += move_amount + + processed = new_heightmap + + return processed + + except Exception as e: + print(f"✗ 热力侵蚀应用失败: {e}") + return heightmap + + def _apply_wind_erosion(self, heightmap: np.ndarray) -> np.ndarray: + """ + 应用风力侵蚀 + + Args: + heightmap: 高度图数据 + + Returns: + 处理后的高度图 + """ + try: + processed = heightmap.copy() + height, width = processed.shape + + # 获取风力侵蚀参数 + params = self.erosion_params['wind_erosion'] + iterations = params['iterations'] + erosion_rate = params['rate'] + wind_direction = params['direction'] + + # 计算风向向量 + wind_dx = math.cos(wind_direction) + wind_dy = math.sin(wind_direction) + + # 模拟多个迭代步骤 + for iteration in range(iterations): + new_heightmap = processed.copy() + + # 对每个点应用风力侵蚀 + for y in range(height): + for x in range(width): + # 风从特定方向吹来 + # 计算风的来源位置 + source_x = int(x - wind_dx * 2) # 2个单位距离 + source_y = int(y - wind_dy * 2) + + # 如果来源位置有效 + if 0 <= source_y < height and 0 <= source_x < width: + # 如果当前点比来源点高,则可能发生风蚀 + height_diff = processed[y, x] - processed[source_y, source_x] + if height_diff > 0.01: # 阈值 + # 应用风力侵蚀 + erosion_amount = erosion_rate * height_diff + new_heightmap[y, x] -= erosion_amount + self.erosion_map[y, x] += erosion_amount + + processed = new_heightmap + + return processed + + except Exception as e: + print(f"✗ 风力侵蚀应用失败: {e}") + return heightmap + + def _apply_chemical_erosion(self, heightmap: np.ndarray) -> np.ndarray: + """ + 应用化学侵蚀 + + Args: + heightmap: 高度图数据 + + Returns: + 处理后的高度图 + """ + try: + processed = heightmap.copy() + height, width = processed.shape + + # 获取化学侵蚀参数 + params = self.erosion_params['chemical_erosion'] + iterations = params['iterations'] + erosion_rate = params['rate'] + + # 模拟多个迭代步骤 + for iteration in range(iterations): + # 简化的化学侵蚀:基于高度和随机因素 + for y in range(height): + for x in range(width): + # 化学侵蚀通常影响较低的区域(如河流、湖泊) + if processed[y, x] < 0.3: # 假设低地更容易发生化学侵蚀 + # 添加随机因素 + chemical_factor = np.random.random() * 0.5 + 0.5 + erosion_amount = erosion_rate * chemical_factor * (0.3 - processed[y, x]) + processed[y, x] -= erosion_amount + self.erosion_map[y, x] += erosion_amount + + return processed + + except Exception as e: + print(f"✗ 化学侵蚀应用失败: {e}") + return heightmap + + def _apply_glacial_erosion(self, heightmap: np.ndarray) -> np.ndarray: + """ + 应用冰川侵蚀 + + Args: + heightmap: 高度图数据 + + Returns: + 处理后的高度图 + """ + try: + processed = heightmap.copy() + height, width = processed.shape + + # 获取冰川侵蚀参数 + params = self.erosion_params['glacial_erosion'] + iterations = params['iterations'] + erosion_rate = params['rate'] + + # 模拟多个迭代步骤 + for iteration in range(iterations): + # 冰川侵蚀:平滑地形并创建U型谷 + new_heightmap = processed.copy() + + for y in range(height): + for x in range(width): + # 冰川通常在高海拔地区形成 + if processed[y, x] > 0.7: # 高海拔 + # 平滑周围区域 + total_height = 0.0 + count = 0 + + # 检查3x3邻域 + for dy in [-1, 0, 1]: + for dx in [-1, 0, 1]: + ny, nx = y + dy, x + dx + if 0 <= ny < height and 0 <= nx < width: + total_height += processed[ny, nx] + count += 1 + + if count > 0: + average_height = total_height / count + # 应用冰川侵蚀(平滑化) + glacial_effect = erosion_rate * (processed[y, x] - average_height) + new_heightmap[y, x] -= glacial_effect + self.erosion_map[y, x] += abs(glacial_effect) + + processed = new_heightmap + + return processed + + except Exception as e: + print(f"✗ 冰川侵蚀应用失败: {e}") + return heightmap + + def set_seed(self, seed: int): + """ + 设置随机种子 + + Args: + seed: 随机种子 + """ + self.seed = seed + print(f"✓ 侵蚀处理器随机种子设置为: {seed}") + + def set_erosion_parameters(self, erosion_type: str, params: Dict[str, Any]): + """ + 设置侵蚀参数 + + Args: + erosion_type: 侵蚀类型 + params: 参数字典 + """ + if erosion_type in self.erosion_params: + self.erosion_params[erosion_type].update(params) + print(f"✓ {erosion_type} 侵蚀参数已更新: {self.erosion_params[erosion_type]}") + else: + print(f"✗ 无效的侵蚀类型: {erosion_type}") + + def enable_erosion_type(self, erosion_type: str, enabled: bool): + """ + 启用/禁用侵蚀类型 + + Args: + erosion_type: 侵蚀类型 + enabled: 是否启用 + """ + if erosion_type in self.erosion_params: + self.erosion_params[erosion_type]['enabled'] = enabled + state = "启用" if enabled else "禁用" + print(f"✓ {erosion_type} 侵蚀已{state}") + else: + print(f"✗ 无效的侵蚀类型: {erosion_type}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + # 更新平均处理时间 + if self.stats['erosion_processed'] > 0: + self.stats['average_processing_time'] = self.stats['total_processing_time'] / self.stats['erosion_processed'] + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'erosion_processed': 0, + 'total_processing_time': 0.0, + 'average_processing_time': 0.0, + 'total_erosion_amount': 0.0, + 'total_sediment_amount': 0.0 + } + print("✓ 侵蚀处理器统计信息已重置") + + def get_erosion_map(self) -> np.ndarray: + """ + 获取侵蚀图 + + Returns: + 侵蚀图数据 + """ + return self.erosion_map if self.erosion_map is not None else np.array([]) + + def get_sediment_map(self) -> np.ndarray: + """ + 获取沉积物图 + + Returns: + 沉积物图数据 + """ + return self.sediment_map if self.sediment_map is not None else np.array([]) + + def get_water_map(self) -> np.ndarray: + """ + 获取水图 + + Returns: + 水图数据 + """ + return self.water_map if self.water_map is not None else np.array([]) + + def set_water_erosion_parameters(self, rate: float = None, solubility: float = None, + evaporation: float = None, inertia: float = None, + sediment_capacity: float = None, deposition_rate: float = None): + """ + 设置水力侵蚀参数 + + Args: + rate: 侵蚀速率 + solubility: 溶解度 + evaporation: 蒸发率 + inertia: 惯性 + sediment_capacity: 沉积物容量 + deposition_rate: 沉积率 + """ + params = self.erosion_params['water_erosion'] + if rate is not None: + params['rate'] = max(0.0, rate) + if solubility is not None: + params['solubility'] = max(0.0, min(1.0, solubility)) + if evaporation is not None: + params['evaporation'] = max(0.0, min(1.0, evaporation)) + if inertia is not None: + params['inertia'] = max(0.0, min(1.0, inertia)) + if sediment_capacity is not None: + params['sediment_capacity'] = max(0.0, sediment_capacity) + if deposition_rate is not None: + params['deposition_rate'] = max(0.0, deposition_rate) + + print(f"✓ 水力侵蚀参数已更新: {params}") + + def set_thermal_erosion_parameters(self, rate: float = None, angle_of_repose: float = None): + """ + 设置热力侵蚀参数 + + Args: + rate: 侵蚀速率 + angle_of_repose: 休止角(度) + """ + params = self.erosion_params['thermal_erosion'] + if rate is not None: + params['rate'] = max(0.0, rate) + if angle_of_repose is not None: + params['angle_of_repose'] = max(0.0, angle_of_repose) + + print(f"✓ 热力侵蚀参数已更新: {params}") + + def set_wind_erosion_parameters(self, rate: float = None, direction: float = None): + """ + 设置风力侵蚀参数 + + Args: + rate: 侵蚀速率 + direction: 风向(弧度) + """ + params = self.erosion_params['wind_erosion'] + if rate is not None: + params['rate'] = max(0.0, rate) + if direction is not None: + params['direction'] = direction + + print(f"✓ 风力侵蚀参数已更新: {params}") + + def apply_selective_erosion(self, heightmap: np.ndarray, mask: np.ndarray, + seed: int = None) -> Dict[str, np.ndarray]: + """ + 在指定区域应用侵蚀 + + Args: + heightmap: 高度图数据 + mask: 掩码(0-1范围,1表示完全应用侵蚀) + seed: 随机种子 + + Returns: + 包含处理后高度图和侵蚀相关数据的字典 + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 侵蚀处理器未启用") + return { + 'heightmap': heightmap, + 'erosion_map': np.zeros_like(heightmap, dtype=np.float32), + 'sediment_map': np.zeros_like(heightmap, dtype=np.float32), + 'water_map': np.zeros_like(heightmap, dtype=np.float32) + } + + # 更新种子 + if seed is not None: + self.seed = seed + + print("✓ 开始应用选择性侵蚀...") + + # 确保掩码尺寸匹配 + if mask.shape != heightmap.shape: + print("✗ 掩码尺寸与高度图不匹配") + return { + 'heightmap': heightmap, + 'erosion_map': np.zeros_like(heightmap, dtype=np.float32), + 'sediment_map': np.zeros_like(heightmap, dtype=np.float32), + 'water_map': np.zeros_like(heightmap, dtype=np.float32) + } + + # 初始化侵蚀图层 + self._initialize_erosion_layers(heightmap.shape) + + # 创建高度图副本进行处理 + processed_heightmap = heightmap.copy() + + # 在掩码区域应用侵蚀 + mask_indices = np.where(mask > 0.5) + + # 创建临时高度图只包含掩码区域 + temp_heightmap = heightmap.copy() + + # 应用各种侵蚀类型(只在掩码区域) + if self.erosion_params['water_erosion']['enabled']: + print(" → 应用水力侵蚀(选择性)...") + temp_heightmap = self._apply_water_erosion_selective(temp_heightmap, mask) + + if self.erosion_params['thermal_erosion']['enabled']: + print(" → 应用热力侵蚀(选择性)...") + temp_heightmap = self._apply_thermal_erosion_selective(temp_heightmap, mask) + + if self.erosion_params['wind_erosion']['enabled']: + print(" → 应用风力侵蚀(选择性)...") + temp_heightmap = self._apply_wind_erosion_selective(temp_heightmap, mask) + + # 只更新掩码区域的值 + processed_heightmap[mask_indices] = temp_heightmap[mask_indices] + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self.stats['erosion_processed'] += 1 + self.stats['total_processing_time'] += processing_time + self.stats['average_processing_time'] = self.stats['total_processing_time'] / self.stats['erosion_processed'] + + print(f"✓ 选择性侵蚀处理完成,耗时: {processing_time:.2f}秒") + return { + 'heightmap': processed_heightmap, + 'erosion_map': self.erosion_map, + 'sediment_map': self.sediment_map, + 'water_map': self.water_map + } + + except Exception as e: + print(f"✗ 选择性侵蚀处理失败: {e}") + import traceback + traceback.print_exc() + return { + 'heightmap': heightmap, + 'erosion_map': np.zeros_like(heightmap, dtype=np.float32), + 'sediment_map': np.zeros_like(heightmap, dtype=np.float32), + 'water_map': np.zeros_like(heightmap, dtype=np.float32) + } + + def _apply_water_erosion_selective(self, heightmap: np.ndarray, mask: np.ndarray) -> np.ndarray: + """在选择区域应用水力侵蚀""" + try: + processed = heightmap.copy() + height, width = processed.shape + + # 获取水力侵蚀参数 + params = self.erosion_params['water_erosion'] + iterations = params['iterations'] + erosion_rate = params['rate'] * 0.5 # 降低速率以适应选择性应用 + + # 只在掩码区域应用侵蚀 + mask_indices = np.where(mask > 0.5) + + # 简化的选择性水力侵蚀 + for iteration in range(iterations): + for y, x in zip(mask_indices[0], mask_indices[1]): + # 简单的侵蚀:降低高点,填充低点 + if processed[y, x] > 0.8: # 高点 + erosion_amount = erosion_rate * mask[y, x] + processed[y, x] -= erosion_amount + self.erosion_map[y, x] += erosion_amount + elif processed[y, x] < 0.2: # 低点 + deposition_amount = erosion_rate * 0.5 * mask[y, x] + processed[y, x] += deposition_amount + self.sediment_map[y, x] += deposition_amount + + return processed + + except Exception as e: + print(f"✗ 选择性水力侵蚀应用失败: {e}") + return heightmap + + def _apply_thermal_erosion_selective(self, heightmap: np.ndarray, mask: np.ndarray) -> np.ndarray: + """在选择区域应用热力侵蚀""" + try: + processed = heightmap.copy() + height, width = processed.shape + + # 获取热力侵蚀参数 + params = self.erosion_params['thermal_erosion'] + iterations = params['iterations'] + erosion_rate = params['rate'] * 0.5 # 降低速率以适应选择性应用 + + # 只在掩码区域应用侵蚀 + mask_indices = np.where(mask > 0.5) + + # 简化的选择性热力侵蚀 + for iteration in range(iterations): + for y, x in zip(mask_indices[0], mask_indices[1]): + # 检查直接邻居 + for dy, dx in [(-1, 0), (1, 0), (0, -1), (0, 1)]: + ny, nx = y + dy, x + dx + if 0 <= ny < height and 0 <= nx < width: + height_diff = processed[y, x] - processed[ny, nx] + if height_diff > 0.1: # 显著高度差 + move_amount = erosion_rate * height_diff * mask[y, x] + processed[y, x] -= move_amount + processed[ny, nx] += move_amount + self.erosion_map[y, x] += move_amount + + return processed + + except Exception as e: + print(f"✗ 选择性热力侵蚀应用失败: {e}") + return heightmap + + def _apply_wind_erosion_selective(self, heightmap: np.ndarray, mask: np.ndarray) -> np.ndarray: + """在选择区域应用风力侵蚀""" + try: + processed = heightmap.copy() + height, width = processed.shape + + # 获取风力侵蚀参数 + params = self.erosion_params['wind_erosion'] + erosion_rate = params['rate'] * 0.5 # 降低速率以适应选择性应用 + wind_direction = params['direction'] + + # 计算风向向量 + wind_dx = math.cos(wind_direction) + wind_dy = math.sin(wind_direction) + + # 只在掩码区域应用侵蚀 + mask_indices = np.where(mask > 0.5) + + # 简化的选择性风力侵蚀 + for y, x in zip(mask_indices[0], mask_indices[1]): + # 风从特定方向吹来 + source_x = int(x - wind_dx * 1) # 1个单位距离 + source_y = int(y - wind_dy * 1) + + # 如果来源位置有效且也在掩码区域内 + if (0 <= source_y < height and 0 <= source_x < width and + mask[source_y, source_x] > 0.5): + # 应用风力侵蚀 + height_diff = processed[y, x] - processed[source_y, source_x] + if height_diff > 0.01: + erosion_amount = erosion_rate * height_diff * mask[y, x] + processed[y, x] -= erosion_amount + self.erosion_map[y, x] += erosion_amount + + return processed + + except Exception as e: + print(f"✗ 选择性风力侵蚀应用失败: {e}") + return heightmap + + def blend_erosion_maps(self, erosion_maps: List[np.ndarray], weights: List[float] = None) -> np.ndarray: + """ + 混合多个侵蚀图 + + Args: + erosion_maps: 侵蚀图列表 + weights: 权重列表(如果为None,则平均分配权重) + + Returns: + 混合后的侵蚀图 + """ + try: + if not erosion_maps: + return np.array([]) + + if len(erosion_maps) == 1: + return erosion_maps[0].copy() + + # 确保所有侵蚀图尺寸相同 + base_shape = erosion_maps[0].shape + for em in erosion_maps: + if em.shape != base_shape: + raise ValueError("所有侵蚀图必须具有相同的尺寸") + + # 处理权重 + if weights is None: + weights = [1.0 / len(erosion_maps)] * len(erosion_maps) + elif len(weights) != len(erosion_maps): + raise ValueError("权重数量必须与侵蚀图数量相同") + + # 归一化权重 + total_weight = sum(weights) + if total_weight > 0: + weights = [w / total_weight for w in weights] + + # 混合侵蚀图 + blended = np.zeros(base_shape, dtype=np.float32) + for i, (em, weight) in enumerate(zip(erosion_maps, weights)): + blended += em * weight + + return blended + + except Exception as e: + print(f"✗ 侵蚀图混合失败: {e}") + import traceback + traceback.print_exc() + # 返回第一个侵蚀图的副本 + return erosion_maps[0].copy() if erosion_maps else np.array([]) + + def smooth_erosion_map(self, erosion_map: np.ndarray, kernel_size: int = 3) -> np.ndarray: + """ + 平滑侵蚀图 + + Args: + erosion_map: 原始侵蚀图 + kernel_size: 平滑核大小 + + Returns: + 平滑后的侵蚀图 + """ + try: + if kernel_size < 1: + return erosion_map + + smoothed = erosion_map.copy() + height, width = smoothed.shape + + # 简单的均值滤波 + half_kernel = kernel_size // 2 + + for y in range(height): + for x in range(width): + total = 0.0 + count = 0 + + # 应用平滑核 + for dy in range(-half_kernel, half_kernel + 1): + for dx in range(-half_kernel, half_kernel + 1): + ny, nx = y + dy, x + dx + if 0 <= ny < height and 0 <= nx < width: + total += erosion_map[ny, nx] + count += 1 + + if count > 0: + smoothed[y, x] = total / count + + return smoothed + + except Exception as e: + print(f"✗ 侵蚀图平滑失败: {e}") + return erosion_map + + def apply_erosion_intensity(self, heightmap: np.ndarray, intensity_map: np.ndarray, + seed: int = None) -> Dict[str, np.ndarray]: + """ + 根据强度图应用侵蚀 + + Args: + heightmap: 高度图数据 + intensity_map: 侵蚀强度图(0-1范围) + seed: 随机种子 + + Returns: + 包含处理后高度图和侵蚀相关数据的字典 + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 侵蚀处理器未启用") + return { + 'heightmap': heightmap, + 'erosion_map': np.zeros_like(heightmap, dtype=np.float32), + 'sediment_map': np.zeros_like(heightmap, dtype=np.float32), + 'water_map': np.zeros_like(heightmap, dtype=np.float32) + } + + # 更新种子 + if seed is not None: + self.seed = seed + + # 确保强度图尺寸匹配 + if intensity_map.shape != heightmap.shape: + print("✗ 强度图尺寸与高度图不匹配") + return { + 'heightmap': heightmap, + 'erosion_map': np.zeros_like(heightmap, dtype=np.float32), + 'sediment_map': np.zeros_like(heightmap, dtype=np.float32), + 'water_map': np.zeros_like(heightmap, dtype=np.float32) + } + + print("✓ 开始应用强度控制的侵蚀...") + + # 初始化侵蚀图层 + self._initialize_erosion_layers(heightmap.shape) + + # 创建高度图副本进行处理 + processed_heightmap = heightmap.copy() + + # 根据强度图调整侵蚀参数 + base_water_params = self.erosion_params['water_erosion'].copy() + base_thermal_params = self.erosion_params['thermal_erosion'].copy() + base_wind_params = self.erosion_params['wind_erosion'].copy() + + # 对每个点根据强度应用侵蚀 + height, width = processed_heightmap.shape + for y in range(height): + for x in range(width): + intensity = intensity_map[y, x] + if intensity > 0.1: # 只在强度大于阈值时应用侵蚀 + # 调整侵蚀参数 + self.erosion_params['water_erosion']['rate'] = base_water_params['rate'] * intensity + self.erosion_params['thermal_erosion']['rate'] = base_thermal_params['rate'] * intensity + self.erosion_params['wind_erosion']['rate'] = base_wind_params['rate'] * intensity + + # 应用局部侵蚀(简化版本) + if self.erosion_params['water_erosion']['enabled']: + # 简化的水力侵蚀 + if processed_heightmap[y, x] > 0.8: + erosion_amount = self.erosion_params['water_erosion']['rate'] * 0.1 + processed_heightmap[y, x] -= erosion_amount + self.erosion_map[y, x] += erosion_amount + + # 恢复原始侵蚀参数 + self.erosion_params['water_erosion'] = base_water_params + self.erosion_params['thermal_erosion'] = base_thermal_params + self.erosion_params['wind_erosion'] = base_wind_params + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self.stats['erosion_processed'] += 1 + self.stats['total_processing_time'] += processing_time + self.stats['average_processing_time'] = self.stats['total_processing_time'] / self.stats['erosion_processed'] + + print(f"✓ 强度控制的侵蚀处理完成,耗时: {processing_time:.2f}秒") + return { + 'heightmap': processed_heightmap, + 'erosion_map': self.erosion_map, + 'sediment_map': self.sediment_map, + 'water_map': self.water_map + } + + except Exception as e: + print(f"✗ 强度控制的侵蚀处理失败: {e}") + import traceback + traceback.print_exc() + return { + 'heightmap': heightmap, + 'erosion_map': np.zeros_like(heightmap, dtype=np.float32), + 'sediment_map': np.zeros_like(heightmap, dtype=np.float32), + 'water_map': np.zeros_like(heightmap, dtype=np.float32) + } + + def export_erosion_data(self, filename: str) -> bool: + """ + 导出侵蚀数据 + + Args: + filename: 文件名 + + Returns: + 是否导出成功 + """ + try: + import json + + erosion_data = { + 'parameters': self.erosion_params, + 'stats': self.stats, + 'timestamp': time.time() + } + + with open(filename, 'w', encoding='utf-8') as f: + json.dump(erosion_data, f, ensure_ascii=False, indent=2) + + print(f"✓ 侵蚀数据已导出到: {filename}") + return True + + except Exception as e: + print(f"✗ 侵蚀数据导出失败: {e}") + return False + + def import_erosion_data(self, filename: str) -> bool: + """ + 导入侵蚀数据 + + Args: + filename: 文件名 + + Returns: + 是否导入成功 + """ + try: + import json + + with open(filename, 'r', encoding='utf-8') as f: + erosion_data = json.load(f) + + # 更新参数 + if 'parameters' in erosion_data: + self.erosion_params.update(erosion_data['parameters']) + + # 更新统计信息 + if 'stats' in erosion_data: + self.stats.update(erosion_data['stats']) + + print(f"✓ 侵蚀数据已从 {filename} 导入") + return True + + except Exception as e: + print(f"✗ 侵蚀数据导入失败: {e}") + return False + + def reset_erosion_maps(self): + """重置侵蚀图层""" + self.erosion_map = None + self.sediment_map = None + self.water_map = None + self.flow_map = None + print("✓ 侵蚀图层已重置") + + def get_erosion_types(self) -> List[str]: + """ + 获取可用的侵蚀类型 + + Returns: + 侵蚀类型列表 + """ + return list(self.erosion_params.keys()) + + def get_erosion_parameters(self, erosion_type: str) -> Dict[str, Any]: + """ + 获取指定侵蚀类型的参数 + + Args: + erosion_type: 侵蚀类型 + + Returns: + 参数字典 + """ return self.erosion_params.get(erosion_type, {}).copy() \ No newline at end of file diff --git a/plugins/user/procedural_terrain_generation/generators/heightmap_generator.py b/plugins/user/procedural_terrain_generation/generators/heightmap_generator.py index b328f9b9..dd71b87a 100644 --- a/plugins/user/procedural_terrain_generation/generators/heightmap_generator.py +++ b/plugins/user/procedural_terrain_generation/generators/heightmap_generator.py @@ -1,1123 +1,1123 @@ -""" -高度图生成器 -负责生成程序化地形的高度图数据 -""" - -import numpy as np -from typing import Dict, Any -import math -import random - -class HeightmapGenerator: - """ - 高度图生成器 - 负责生成程序化地形的高度图数据,使用多种噪声算法和地形特征生成技术 - """ - - def __init__(self, plugin): - """ - 初始化高度图生成器 - - Args: - plugin: 程序化地形生成插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 生成器配置 - self.seed = plugin.config.get('seed', 12345) - self.scale = 50.0 - self.octaves = 6 - self.persistence = 0.5 - self.lacunarity = 2.0 - self.height_multiplier = 1.0 - self.water_level = 0.8 - - # 地形特征参数 - self.mountain_scale = 1.0 - self.hill_scale = 0.5 - self.plain_scale = 0.1 - self.valley_scale = 0.3 - - # 地形类型权重 - self.terrain_weights = { - 'mountains': 0.3, - 'hills': 0.4, - 'plains': 0.2, - 'valleys': 0.1 - } - - # 噪声生成器引用 - self.noise_generator = None - - # 统计信息 - self.stats = { - 'generations_count': 0, - 'total_generation_time': 0.0, - 'average_generation_time': 0.0, - 'last_generation_time': 0.0 - } - - print("✓ 高度图生成器已创建") - - def initialize(self) -> bool: - """ - 初始化高度图生成器 - - Returns: - 是否初始化成功 - """ - try: - # 获取噪声生成器引用 - self.noise_generator = self.plugin.noise_generator - - self.initialized = True - print("✓ 高度图生成器初始化完成") - return True - - except Exception as e: - print(f"✗ 高度图生成器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用高度图生成器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 高度图生成器未初始化") - return False - - self.enabled = True - print("✓ 高度图生成器已启用") - return True - - except Exception as e: - print(f"✗ 高度图生成器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用高度图生成器""" - try: - self.enabled = False - print("✓ 高度图生成器已禁用") - - except Exception as e: - print(f"✗ 高度图生成器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理高度图生成器资源""" - try: - self.disable() - self.initialized = False - print("✓ 高度图生成器资源已清理") - - except Exception as e: - print(f"✗ 高度图生成器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新高度图生成器状态 - - Args: - dt: 时间增量 - """ - # 处理更新逻辑 - pass - - def generate_heightmap(self, width: int, height: int, seed: int = None) -> np.ndarray: - """ - 生成高度图 - - Args: - width: 高度图宽度 - height: 高度图高度 - seed: 随机种子 - - Returns: - 高度图数据 - """ - try: - import time - generation_start_time = time.time() - - if not self.enabled: - print("✗ 高度图生成器未启用") - return np.zeros((height, width), dtype=np.float32) - - # 更新种子 - if seed is not None: - self.seed = seed - - # 设置随机种子 - random.seed(self.seed) - np.random.seed(self.seed) - - print(f"✓ 开始生成 {width}x{height} 高度图...") - - # 创建高度图数组 - heightmap = np.zeros((height, width), dtype=np.float32) - - # 生成基础噪声 - print(" → 生成基础噪声...") - base_noise = self._generate_base_noise(width, height) - - # 应用地形特征 - print(" → 应用地形特征...") - terrain_features = self._apply_terrain_features(width, height) - - # 混合噪声和地形特征 - print(" → 混合地形数据...") - heightmap = self._blend_terrain_data(base_noise, terrain_features) - - # 应用后处理 - print(" → 应用后处理...") - heightmap = self._post_process_heightmap(heightmap) - - # 更新统计信息 - generation_time = time.time() - generation_start_time - self.stats['generations_count'] += 1 - self.stats['total_generation_time'] += generation_time - self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['generations_count'] - self.stats['last_generation_time'] = generation_time - - print(f"✓ 高度图生成完成,耗时: {generation_time:.2f}秒") - return heightmap - - except Exception as e: - print(f"✗ 高度图生成失败: {e}") - import traceback - traceback.print_exc() - return np.zeros((height, width), dtype=np.float32) - - def _generate_base_noise(self, width: int, height: int) -> np.ndarray: - """ - 生成基础噪声 - - Args: - width: 噪声图宽度 - height: 噪声图高度 - - Returns: - 基础噪声数据 - """ - try: - if self.noise_generator is None: - # 如果没有噪声生成器,生成简单的随机噪声 - print("⚠ 未找到噪声生成器,使用简化噪声生成") - return np.random.rand(height, width).astype(np.float32) - - # 使用分形噪声生成基础地形 - noise_map = np.zeros((height, width), dtype=np.float32) - - # 生成多层噪声 - amplitude = 1.0 - frequency = 1.0 - max_value = 0.0 - - for octave in range(self.octaves): - # 计算当前层的频率和振幅 - current_frequency = frequency / self.scale - current_amplitude = amplitude - - # 生成噪声层 - for y in range(height): - for x in range(width): - noise_value = self.noise_generator.generate_noise( - x * current_frequency, - y * current_frequency, - self.seed + octave - ) - noise_map[y, x] += noise_value * current_amplitude - - # 更新最大值用于归一化 - max_value += current_amplitude - - # 更新频率和振幅 - amplitude *= self.persistence - frequency *= self.lacunarity - - # 归一化噪声值到0-1范围 - if max_value > 0: - noise_map = (noise_map + 1) / 2 # 将[-1,1]转换为[0,1] - - return noise_map - - except Exception as e: - print(f"✗ 基础噪声生成失败: {e}") - return np.random.rand(height, width).astype(np.float32) - - def _apply_terrain_features(self, width: int, height: int) -> np.ndarray: - """ - 应用地形特征 - - Args: - width: 地形宽度 - height: 地形高度 - - Returns: - 地形特征数据 - """ - try: - features_map = np.zeros((height, width), dtype=np.float32) - - # 计算中心点 - center_x, center_y = width / 2.0, height / 2.0 - max_distance = math.sqrt(center_x**2 + center_y**2) - - # 生成不同的地形特征 - for y in range(height): - for x in range(width): - # 计算到中心的距离 - distance_x = x - center_x - distance_y = y - center_y - distance = math.sqrt(distance_x**2 + distance_y**2) / max_distance - - # 生成径向地形特征 - radial_feature = math.sin(distance * math.pi * 4) * 0.1 - - # 生成角度特征 - angle = math.atan2(distance_y, distance_x) - angular_feature = math.sin(angle * 8) * 0.05 - - # 组合特征 - features_map[y, x] = radial_feature + angular_feature - - return features_map - - except Exception as e: - print(f"✗ 地形特征应用失败: {e}") - return np.zeros((height, width), dtype=np.float32) - - def _blend_terrain_data(self, base_noise: np.ndarray, terrain_features: np.ndarray) -> np.ndarray: - """ - 混合地形数据 - - Args: - base_noise: 基础噪声数据 - terrain_features: 地形特征数据 - - Returns: - 混合后的地形数据 - """ - try: - height, width = base_noise.shape - - # 确保两个数组尺寸相同 - if terrain_features.shape != (height, width): - # 调整地形特征尺寸 - terrain_features = np.resize(terrain_features, (height, width)) - - # 混合数据 - blended_map = np.zeros((height, width), dtype=np.float32) - - # 获取地形权重 - mountain_weight = self.terrain_weights.get('mountains', 0.3) - hill_weight = self.terrain_weights.get('hills', 0.4) - plain_weight = self.terrain_weights.get('plains', 0.2) - valley_weight = self.terrain_weights.get('valleys', 0.1) - - for y in range(height): - for x in range(width): - # 基础噪声值 - noise_value = base_noise[y, x] - - # 地形特征值 - feature_value = terrain_features[y, x] - - # 根据权重混合 - blended_value = ( - noise_value * 0.7 + # 主要使用噪声 - feature_value * 0.3 # 添加部分特征 - ) - - # 应用高度乘数 - blended_value *= self.height_multiplier - - # 确保值在0-1范围内 - blended_value = max(0.0, min(1.0, blended_value)) - - blended_map[y, x] = blended_value - - return blended_map - - except Exception as e: - print(f"✗ 地形数据混合失败: {e}") - return base_noise - - def _post_process_heightmap(self, heightmap: np.ndarray) -> np.ndarray: - """ - 后处理高度图 - - Args: - heightmap: 原始高度图 - - Returns: - 处理后的高度图 - """ - try: - processed_map = heightmap.copy() - height, width = processed_map.shape - - # 应用平滑滤波器 - processed_map = self._apply_smoothing_filter(processed_map) - - # 调整边缘值,使边缘更加平滑 - processed_map = self._smooth_edges(processed_map) - - # 应用高程调整 - processed_map = self._adjust_elevation(processed_map) - - # 确保所有值在0-1范围内 - processed_map = np.clip(processed_map, 0.0, 1.0) - - return processed_map - - except Exception as e: - print(f"✗ 高度图后处理失败: {e}") - return heightmap - - def _apply_smoothing_filter(self, heightmap: np.ndarray) -> np.ndarray: - """ - 应用平滑滤波器 - - Args: - heightmap: 高度图数据 - - Returns: - 平滑后的高度图 - """ - try: - smoothed = heightmap.copy() - height, width = smoothed.shape - - # 简单的3x3平均滤波器 - for y in range(1, height - 1): - for x in range(1, width - 1): - # 计算3x3邻域的平均值 - avg_value = ( - heightmap[y-1, x-1] + heightmap[y-1, x] + heightmap[y-1, x+1] + - heightmap[y, x-1] + heightmap[y, x] + heightmap[y, x+1] + - heightmap[y+1, x-1] + heightmap[y+1, x] + heightmap[y+1, x+1] - ) / 9.0 - - smoothed[y, x] = avg_value - - return smoothed - - except Exception as e: - print(f"✗ 平滑滤波器应用失败: {e}") - return heightmap - - def _smooth_edges(self, heightmap: np.ndarray) -> np.ndarray: - """ - 平滑地形边缘 - - Args: - heightmap: 高度图数据 - - Returns: - 边缘平滑后的高度图 - """ - try: - smoothed = heightmap.copy() - height, width = smoothed.shape - - # 平滑边缘几行/列 - edge_width = min(10, width // 20) # 边缘宽度为地形尺寸的5%或10像素 - - # 平滑顶部和底部边缘 - for i in range(edge_width): - factor = i / edge_width - # 顶部边缘 - smoothed[i, :] *= factor - # 底部边缘 - smoothed[height - 1 - i, :] *= factor - - # 平滑左侧和右侧边缘 - for i in range(edge_width): - factor = i / edge_width - # 左侧边缘 - smoothed[:, i] *= factor - # 右侧边缘 - smoothed[:, width - 1 - i] *= factor - - return smoothed - - except Exception as e: - print(f"✗ 边缘平滑处理失败: {e}") - return heightmap - - def _adjust_elevation(self, heightmap: np.ndarray) -> np.ndarray: - """ - 调整地形高程 - - Args: - heightmap: 高度图数据 - - Returns: - 高程调整后的高度图 - """ - try: - adjusted = heightmap.copy() - - # 应用高程曲线调整,使低地更平缓,高地更陡峭 - adjusted = np.power(adjusted, 1.5) - - # 添加一些随机变化 - noise = np.random.rand(*adjusted.shape).astype(np.float32) * 0.02 - adjusted = adjusted + noise - adjusted = np.clip(adjusted, 0.0, 1.0) - - return adjusted - - except Exception as e: - print(f"✗ 高程调整失败: {e}") - return heightmap - - def set_seed(self, seed: int): - """ - 设置随机种子 - - Args: - seed: 随机种子 - """ - self.seed = seed - print(f"✓ 高度图生成器随机种子设置为: {seed}") - - def set_scale(self, scale: float): - """ - 设置噪声缩放 - - Args: - scale: 噪声缩放因子 - """ - self.scale = max(0.1, scale) - print(f"✓ 噪声缩放设置为: {self.scale}") - - def set_octaves(self, octaves: int): - """ - 设置噪声八度数 - - Args: - octaves: 八度数 - """ - self.octaves = max(1, min(10, octaves)) - print(f"✓ 噪声八度数设置为: {self.octaves}") - - def set_persistence(self, persistence: float): - """ - 设置噪声持久性 - - Args: - persistence: 持久性值 - """ - self.persistence = max(0.0, min(1.0, persistence)) - print(f"✓ 噪声持久性设置为: {self.persistence}") - - def set_lacunarity(self, lacunarity: float): - """ - 设置噪声间隙性 - - Args: - lacunarity: 间隙性值 - """ - self.lacunarity = max(1.0, lacunarity) - print(f"✓ 噪声间隙性设置为: {self.lacunarity}") - - def set_height_multiplier(self, multiplier: float): - """ - 设置高度乘数 - - Args: - multiplier: 高度乘数 - """ - self.height_multiplier = max(0.0, multiplier) - print(f"✓ 高度乘数设置为: {self.height_multiplier}") - - def set_water_level(self, level: float): - """ - 设置水位 - - Args: - level: 水位值 (0.0-1.0) - """ - self.water_level = max(0.0, min(1.0, level)) - print(f"✓ 水位设置为: {self.water_level}") - - def set_terrain_weights(self, weights: Dict[str, float]): - """ - 设置地形类型权重 - - Args: - weights: 地形权重字典 - """ - self.terrain_weights.update(weights) - print(f"✓ 地形权重已更新: {self.terrain_weights}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def generate_island_heightmap(self, width: int, height: int, seed: int = None) -> np.ndarray: - """ - 生成岛屿型高度图 - - Args: - width: 高度图宽度 - height: 高度图高度 - seed: 随机种子 - - Returns: - 岛屿型高度图数据 - """ - try: - if seed is not None: - self.seed = seed - - # 设置随机种子 - random.seed(self.seed) - np.random.seed(self.seed) - - # 创建高度图数组 - heightmap = np.zeros((height, width), dtype=np.float32) - - # 计算中心点 - center_x, center_y = width / 2.0, height / 2.0 - max_distance = math.sqrt(center_x**2 + center_y**2) - - # 生成岛屿形状 - for y in range(height): - for x in range(width): - # 计算到中心的距离 - distance_x = x - center_x - distance_y = y - center_y - distance = math.sqrt(distance_x**2 + distance_y**2) - normalized_distance = distance / max_distance - - # 岛屿衰减函数 (边缘为0,中心为1) - island_factor = max(0.0, 1.0 - normalized_distance**2) - - # 生成基础噪声 - if self.noise_generator: - noise_value = self.noise_generator.generate_noise( - x / self.scale, - y / self.scale, - self.seed - ) - noise_value = (noise_value + 1) / 2 # 转换到0-1范围 - else: - noise_value = random.random() - - # 应用岛屿因子 - heightmap[y, x] = noise_value * island_factor - - # 应用后处理 - heightmap = self._post_process_heightmap(heightmap) - - return heightmap - - except Exception as e: - print(f"✗ 岛屿型高度图生成失败: {e}") - import traceback - traceback.print_exc() - return np.zeros((height, width), dtype=np.float32) - - def generate_mountain_heightmap(self, width: int, height: int, seed: int = None) -> np.ndarray: - """ - 生成山脉型高度图 - - Args: - width: 高度图宽度 - height: 高度图高度 - seed: 随机种子 - - Returns: - 山脉型高度图数据 - """ - try: - if seed is not None: - self.seed = seed - - # 设置随机种子 - random.seed(self.seed) - np.random.seed(self.seed) - - # 创建高度图数组 - heightmap = np.zeros((height, width), dtype=np.float32) - - # 生成多个山峰 - num_peaks = random.randint(3, 8) - peaks = [] - for _ in range(num_peaks): - peak_x = random.randint(0, width - 1) - peak_y = random.randint(0, height - 1) - peak_height = random.uniform(0.5, 1.0) - peak_radius = random.randint(width // 10, width // 4) - peaks.append((peak_x, peak_y, peak_height, peak_radius)) - - # 根据山峰生成地形 - for y in range(height): - for x in range(width): - height_value = 0.0 - for peak_x, peak_y, peak_height, peak_radius in peaks: - # 计算到山峰的距离 - distance = math.sqrt((x - peak_x)**2 + (y - peak_y)**2) - if distance <= peak_radius: - # 使用高斯函数生成山峰形状 - distance_factor = 1.0 - (distance / peak_radius) - height_value += peak_height * math.exp(-distance_factor**2 / 0.1) - - heightmap[y, x] = min(1.0, height_value) - - # 添加细节噪声 - if self.noise_generator: - for y in range(height): - for x in range(width): - noise_value = self.noise_generator.generate_noise( - x / (self.scale / 2), - y / (self.scale / 2), - self.seed + 1000 - ) - noise_value = (noise_value + 1) / 2 # 转换到0-1范围 - heightmap[y, x] = heightmap[y, x] * 0.8 + noise_value * 0.2 - - # 应用后处理 - heightmap = self._post_process_heightmap(heightmap) - - return heightmap - - except Exception as e: - print(f"✗ 山脉型高度图生成失败: {e}") - import traceback - traceback.print_exc() - return np.zeros((height, width), dtype=np.float32) - - def generate_plateau_heightmap(self, width: int, height: int, seed: int = None) -> np.ndarray: - """ - 生成高原型高度图 - - Args: - width: 高度图宽度 - height: 高度图高度 - seed: 随机种子 - - Returns: - 高原型高度图数据 - """ - try: - if seed is not None: - self.seed = seed - - # 设置随机种子 - random.seed(self.seed) - np.random.seed(self.seed) - - # 创建高度图数组 - heightmap = np.zeros((height, width), dtype=np.float32) - - # 生成基础高原高度 - plateau_height = random.uniform(0.4, 0.7) - - # 填充基础高原 - heightmap.fill(plateau_height) - - # 添加边缘斜坡 - slope_width = min(30, width // 10) - for i in range(slope_width): - factor = i / slope_width - # 上边缘 - heightmap[i, :] *= factor - # 下边缘 - heightmap[height - 1 - i, :] *= factor - # 左边缘 - heightmap[:, i] *= factor - # 右边缘 - heightmap[:, width - 1 - i] *= factor - - # 添加一些起伏 - if self.noise_generator: - for y in range(height): - for x in range(width): - noise_value = self.noise_generator.generate_noise( - x / self.scale, - y / self.scale, - self.seed + 2000 - ) - noise_value = (noise_value + 1) / 2 # 转换到0-1范围 - # 减少噪声影响,保持高原特征 - heightmap[y, x] = heightmap[y, x] * 0.9 + noise_value * 0.1 - - # 应用后处理 - heightmap = self._post_process_heightmap(heightmap) - - return heightmap - - except Exception as e: - print(f"✗ 高原型高度图生成失败: {e}") - import traceback - traceback.print_exc() - return np.zeros((height, width), dtype=np.float32) - - def generate_valley_heightmap(self, width: int, height: int, seed: int = None) -> np.ndarray: - """ - 生成峡谷型高度图 - - Args: - width: 高度图宽度 - height: 高度图高度 - seed: 随机种子 - - Returns: - 峡谷型高度图数据 - """ - try: - if seed is not None: - self.seed = seed - - # 设置随机种子 - random.seed(self.seed) - np.random.seed(self.seed) - - # 创建高度图数组 - heightmap = np.zeros((height, width), dtype=np.float32) - - # 选择峡谷方向 (0=水平, 1=垂直, 2=对角线) - valley_direction = random.randint(0, 2) - - # 生成峡谷 - for y in range(height): - for x in range(width): - if valley_direction == 0: # 水平峡谷 - valley_factor = 1.0 - abs(y - height/2) / (height/2) - elif valley_direction == 1: # 垂直峡谷 - valley_factor = 1.0 - abs(x - width/2) / (width/2) - else: # 对角线峡谷 - valley_factor = 1.0 - abs((x - width/2) - (y - height/2)) / max(width, height) - - # 确保峡谷因子在合理范围内 - valley_factor = max(0.0, min(1.0, valley_factor)) - - # 生成周围地形 - if self.noise_generator: - noise_value = self.noise_generator.generate_noise( - x / self.scale, - y / self.scale, - self.seed + 3000 - ) - noise_value = (noise_value + 1) / 2 # 转换到0-1范围 - else: - noise_value = random.random() - - # 应用峡谷因子 (峡谷区域较低) - heightmap[y, x] = noise_value * (0.5 + valley_factor * 0.5) - - # 应用后处理 - heightmap = self._post_process_heightmap(heightmap) - - return heightmap - - except Exception as e: - print(f"✗ 峡谷型高度图生成失败: {e}") - import traceback - traceback.print_exc() - return np.zeros((height, width), dtype=np.float32) - - def generate_crater_heightmap(self, width: int, height: int, seed: int = None) -> np.ndarray: - """ - 生成陨石坑型高度图 - - Args: - width: 高度图宽度 - height: 高度图高度 - seed: 随机种子 - - Returns: - 陨石坑型高度图数据 - """ - try: - if seed is not None: - self.seed = seed - - # 设置随机种子 - random.seed(self.seed) - np.random.seed(self.seed) - - # 创建高度图数组 - heightmap = np.zeros((height, width), dtype=np.float32) - - # 生成多个陨石坑 - num_craters = random.randint(1, 5) - craters = [] - for _ in range(num_craters): - crater_x = random.randint(width // 4, 3 * width // 4) - crater_y = random.randint(height // 4, 3 * height // 4) - crater_radius = random.randint(width // 20, width // 5) - crater_depth = random.uniform(0.2, 0.6) - craters.append((crater_x, crater_y, crater_radius, crater_depth)) - - # 初始化为平坦地形 - heightmap.fill(0.8) - - # 根据陨石坑生成地形 - for y in range(height): - for x in range(width): - for crater_x, crater_y, crater_radius, crater_depth in craters: - # 计算到陨石坑中心的距离 - distance = math.sqrt((x - crater_x)**2 + (y - crater_y)**2) - if distance <= crater_radius: - # 陨石坑形状 (边缘高,中心低) - if distance < crater_radius * 0.8: # 坑内 - # 抛物线形坑底 - normalized_distance = distance / (crater_radius * 0.8) - depth_factor = 1.0 - normalized_distance**2 - heightmap[y, x] = min(heightmap[y, x], 0.8 - crater_depth * depth_factor) - else: # 坑边缘 - # 边缘隆起 - edge_factor = (distance - crater_radius * 0.8) / (crater_radius * 0.2) - heightmap[y, x] = max(heightmap[y, x], 0.8 + crater_depth * 0.3 * (1.0 - edge_factor)) - - # 添加细节噪声 - if self.noise_generator: - for y in range(height): - for x in range(width): - noise_value = self.noise_generator.generate_noise( - x / (self.scale / 3), - y / (self.scale / 3), - self.seed + 4000 - ) - noise_value = (noise_value + 1) / 2 # 转换到0-1范围 - heightmap[y, x] = heightmap[y, x] * 0.9 + noise_value * 0.1 - - # 应用后处理 - heightmap = self._post_process_heightmap(heightmap) - - return heightmap - - except Exception as e: - print(f"✗ 陨石坑型高度图生成失败: {e}") - import traceback - traceback.print_exc() - return np.zeros((height, width), dtype=np.float32) - - def generate_terrain_with_zones(self, width: int, height: int, zones: Dict[str, Any], seed: int = None) -> np.ndarray: - """ - 根据区域定义生成地形 - - Args: - width: 高度图宽度 - height: 高度图高度 - zones: 区域定义字典 - seed: 随机种子 - - Returns: - 区域化高度图数据 - """ - try: - if seed is not None: - self.seed = seed - - # 设置随机种子 - random.seed(self.seed) - np.random.seed(self.seed) - - # 创建高度图数组 - heightmap = np.zeros((height, width), dtype=np.float32) - - # 为每个区域生成地形 - for zone_name, zone_params in zones.items(): - zone_type = zone_params.get('type', 'default') - zone_position = zone_params.get('position', (0, 0)) - zone_size = zone_params.get('size', (width, height)) - zone_height = zone_params.get('height', 0.5) - - # 根据区域类型生成地形 - if zone_type == 'mountain': - # 在指定区域生成山脉 - self._generate_zone_mountains(heightmap, zone_position, zone_size, zone_height) - elif zone_type == 'valley': - # 在指定区域生成峡谷 - self._generate_zone_valley(heightmap, zone_position, zone_size, zone_height) - elif zone_type == 'plateau': - # 在指定区域生成高原 - self._generate_zone_plateau(heightmap, zone_position, zone_size, zone_height) - elif zone_type == 'water': - # 在指定区域生成水域 - self._generate_zone_water(heightmap, zone_position, zone_size, zone_height) - - # 应用后处理 - heightmap = self._post_process_heightmap(heightmap) - - return heightmap - - except Exception as e: - print(f"✗ 区域化地形生成失败: {e}") - import traceback - traceback.print_exc() - return np.zeros((height, width), dtype=np.float32) - - def _generate_zone_mountains(self, heightmap: np.ndarray, position: tuple, size: tuple, base_height: float): - """在指定区域生成山脉""" - try: - x_start, y_start = position - width, height = size - map_height, map_width = heightmap.shape - - # 确保区域在地图范围内 - x_start = max(0, min(x_start, map_width - 1)) - y_start = max(0, min(y_start, map_height - 1)) - x_end = min(map_width, x_start + width) - y_end = min(map_height, y_start + height) - - # 生成山脉 - for y in range(y_start, y_end): - for x in range(x_start, x_end): - # 计算相对于区域的位置 - rel_x = (x - x_start) / width - rel_y = (y - y_start) / height - - # 生成山峰 - peak_influence = math.sin(rel_x * math.pi) * math.sin(rel_y * math.pi) - heightmap[y, x] = max(heightmap[y, x], base_height + peak_influence * 0.3) - - except Exception as e: - print(f"✗ 区域山脉生成失败: {e}") - - def _generate_zone_valley(self, heightmap: np.ndarray, position: tuple, size: tuple, base_height: float): - """在指定区域生成峡谷""" - try: - x_start, y_start = position - width, height = size - map_height, map_width = heightmap.shape - - # 确保区域在地图范围内 - x_start = max(0, min(x_start, map_width - 1)) - y_start = max(0, min(y_start, map_height - 1)) - x_end = min(map_width, x_start + width) - y_end = min(map_height, y_start + height) - - # 生成峡谷 - for y in range(y_start, y_end): - for x in range(x_start, x_end): - # 计算相对于区域的位置 - rel_x = (x - x_start) / width - rel_y = (y - y_start) / height - - # 生成峡谷形状 (中心低,边缘高) - valley_factor = 1.0 - math.sqrt((rel_x - 0.5)**2 + (rel_y - 0.5)**2) * 2 - valley_factor = max(0.0, valley_factor) - heightmap[y, x] = min(heightmap[y, x], base_height - valley_factor * 0.4) - - except Exception as e: - print(f"✗ 区域峡谷生成失败: {e}") - - def _generate_zone_plateau(self, heightmap: np.ndarray, position: tuple, size: tuple, base_height: float): - """在指定区域生成高原""" - try: - x_start, y_start = position - width, height = size - map_height, map_width = heightmap.shape - - # 确保区域在地图范围内 - x_start = max(0, min(x_start, map_width - 1)) - y_start = max(0, min(y_start, map_height - 1)) - x_end = min(map_width, x_start + width) - y_end = min(map_height, y_start + height) - - # 生成高原 - for y in range(y_start, y_end): - for x in range(x_start, x_end): - heightmap[y, x] = max(heightmap[y, x], base_height) - - except Exception as e: - print(f"✗ 区域高原生成失败: {e}") - - def _generate_zone_water(self, heightmap: np.ndarray, position: tuple, size: tuple, water_level: float): - """在指定区域生成水域""" - try: - x_start, y_start = position - width, height = size - map_height, map_width = heightmap.shape - - # 确保区域在地图范围内 - x_start = max(0, min(x_start, map_width - 1)) - y_start = max(0, min(y_start, map_height - 1)) - x_end = min(map_width, x_start + width) - y_end = min(map_height, y_start + height) - - # 生成平坦水域 - for y in range(y_start, y_end): - for x in range(x_start, x_end): - heightmap[y, x] = min(heightmap[y, x], water_level) - - except Exception as e: - print(f"✗ 区域水域生成失败: {e}") - - def blend_heightmaps(self, heightmaps: List[np.ndarray], weights: List[float] = None) -> np.ndarray: - """ - 混合多个高度图 - - Args: - heightmaps: 高度图列表 - weights: 权重列表 (如果为None,则平均分配权重) - - Returns: - 混合后的高度图 - """ - try: - if not heightmaps: - return np.array([]) - - if len(heightmaps) == 1: - return heightmaps[0].copy() - - # 确保所有高度图尺寸相同 - base_shape = heightmaps[0].shape - for hm in heightmaps: - if hm.shape != base_shape: - raise ValueError("所有高度图必须具有相同的尺寸") - - # 处理权重 - if weights is None: - weights = [1.0 / len(heightmaps)] * len(heightmaps) - elif len(weights) != len(heightmaps): - raise ValueError("权重数量必须与高度图数量相同") - - # 归一化权重 - total_weight = sum(weights) - if total_weight > 0: - weights = [w / total_weight for w in weights] - - # 混合高度图 - blended = np.zeros(base_shape, dtype=np.float32) - for i, (hm, weight) in enumerate(zip(heightmaps, weights)): - blended += hm * weight - - # 确保值在0-1范围内 - blended = np.clip(blended, 0.0, 1.0) - - return blended - - except Exception as e: - print(f"✗ 高度图混合失败: {e}") - import traceback - traceback.print_exc() - # 返回第一个高度图的副本 +""" +高度图生成器 +负责生成程序化地形的高度图数据 +""" + +import numpy as np +from typing import Dict, Any +import math +import random + +class HeightmapGenerator: + """ + 高度图生成器 + 负责生成程序化地形的高度图数据,使用多种噪声算法和地形特征生成技术 + """ + + def __init__(self, plugin): + """ + 初始化高度图生成器 + + Args: + plugin: 程序化地形生成插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 生成器配置 + self.seed = plugin.config.get('seed', 12345) + self.scale = 50.0 + self.octaves = 6 + self.persistence = 0.5 + self.lacunarity = 2.0 + self.height_multiplier = 1.0 + self.water_level = 0.8 + + # 地形特征参数 + self.mountain_scale = 1.0 + self.hill_scale = 0.5 + self.plain_scale = 0.1 + self.valley_scale = 0.3 + + # 地形类型权重 + self.terrain_weights = { + 'mountains': 0.3, + 'hills': 0.4, + 'plains': 0.2, + 'valleys': 0.1 + } + + # 噪声生成器引用 + self.noise_generator = None + + # 统计信息 + self.stats = { + 'generations_count': 0, + 'total_generation_time': 0.0, + 'average_generation_time': 0.0, + 'last_generation_time': 0.0 + } + + print("✓ 高度图生成器已创建") + + def initialize(self) -> bool: + """ + 初始化高度图生成器 + + Returns: + 是否初始化成功 + """ + try: + # 获取噪声生成器引用 + self.noise_generator = self.plugin.noise_generator + + self.initialized = True + print("✓ 高度图生成器初始化完成") + return True + + except Exception as e: + print(f"✗ 高度图生成器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用高度图生成器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 高度图生成器未初始化") + return False + + self.enabled = True + print("✓ 高度图生成器已启用") + return True + + except Exception as e: + print(f"✗ 高度图生成器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用高度图生成器""" + try: + self.enabled = False + print("✓ 高度图生成器已禁用") + + except Exception as e: + print(f"✗ 高度图生成器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理高度图生成器资源""" + try: + self.disable() + self.initialized = False + print("✓ 高度图生成器资源已清理") + + except Exception as e: + print(f"✗ 高度图生成器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新高度图生成器状态 + + Args: + dt: 时间增量 + """ + # 处理更新逻辑 + pass + + def generate_heightmap(self, width: int, height: int, seed: int = None) -> np.ndarray: + """ + 生成高度图 + + Args: + width: 高度图宽度 + height: 高度图高度 + seed: 随机种子 + + Returns: + 高度图数据 + """ + try: + import time + generation_start_time = time.time() + + if not self.enabled: + print("✗ 高度图生成器未启用") + return np.zeros((height, width), dtype=np.float32) + + # 更新种子 + if seed is not None: + self.seed = seed + + # 设置随机种子 + random.seed(self.seed) + np.random.seed(self.seed) + + print(f"✓ 开始生成 {width}x{height} 高度图...") + + # 创建高度图数组 + heightmap = np.zeros((height, width), dtype=np.float32) + + # 生成基础噪声 + print(" → 生成基础噪声...") + base_noise = self._generate_base_noise(width, height) + + # 应用地形特征 + print(" → 应用地形特征...") + terrain_features = self._apply_terrain_features(width, height) + + # 混合噪声和地形特征 + print(" → 混合地形数据...") + heightmap = self._blend_terrain_data(base_noise, terrain_features) + + # 应用后处理 + print(" → 应用后处理...") + heightmap = self._post_process_heightmap(heightmap) + + # 更新统计信息 + generation_time = time.time() - generation_start_time + self.stats['generations_count'] += 1 + self.stats['total_generation_time'] += generation_time + self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['generations_count'] + self.stats['last_generation_time'] = generation_time + + print(f"✓ 高度图生成完成,耗时: {generation_time:.2f}秒") + return heightmap + + except Exception as e: + print(f"✗ 高度图生成失败: {e}") + import traceback + traceback.print_exc() + return np.zeros((height, width), dtype=np.float32) + + def _generate_base_noise(self, width: int, height: int) -> np.ndarray: + """ + 生成基础噪声 + + Args: + width: 噪声图宽度 + height: 噪声图高度 + + Returns: + 基础噪声数据 + """ + try: + if self.noise_generator is None: + # 如果没有噪声生成器,生成简单的随机噪声 + print("⚠ 未找到噪声生成器,使用简化噪声生成") + return np.random.rand(height, width).astype(np.float32) + + # 使用分形噪声生成基础地形 + noise_map = np.zeros((height, width), dtype=np.float32) + + # 生成多层噪声 + amplitude = 1.0 + frequency = 1.0 + max_value = 0.0 + + for octave in range(self.octaves): + # 计算当前层的频率和振幅 + current_frequency = frequency / self.scale + current_amplitude = amplitude + + # 生成噪声层 + for y in range(height): + for x in range(width): + noise_value = self.noise_generator.generate_noise( + x * current_frequency, + y * current_frequency, + self.seed + octave + ) + noise_map[y, x] += noise_value * current_amplitude + + # 更新最大值用于归一化 + max_value += current_amplitude + + # 更新频率和振幅 + amplitude *= self.persistence + frequency *= self.lacunarity + + # 归一化噪声值到0-1范围 + if max_value > 0: + noise_map = (noise_map + 1) / 2 # 将[-1,1]转换为[0,1] + + return noise_map + + except Exception as e: + print(f"✗ 基础噪声生成失败: {e}") + return np.random.rand(height, width).astype(np.float32) + + def _apply_terrain_features(self, width: int, height: int) -> np.ndarray: + """ + 应用地形特征 + + Args: + width: 地形宽度 + height: 地形高度 + + Returns: + 地形特征数据 + """ + try: + features_map = np.zeros((height, width), dtype=np.float32) + + # 计算中心点 + center_x, center_y = width / 2.0, height / 2.0 + max_distance = math.sqrt(center_x**2 + center_y**2) + + # 生成不同的地形特征 + for y in range(height): + for x in range(width): + # 计算到中心的距离 + distance_x = x - center_x + distance_y = y - center_y + distance = math.sqrt(distance_x**2 + distance_y**2) / max_distance + + # 生成径向地形特征 + radial_feature = math.sin(distance * math.pi * 4) * 0.1 + + # 生成角度特征 + angle = math.atan2(distance_y, distance_x) + angular_feature = math.sin(angle * 8) * 0.05 + + # 组合特征 + features_map[y, x] = radial_feature + angular_feature + + return features_map + + except Exception as e: + print(f"✗ 地形特征应用失败: {e}") + return np.zeros((height, width), dtype=np.float32) + + def _blend_terrain_data(self, base_noise: np.ndarray, terrain_features: np.ndarray) -> np.ndarray: + """ + 混合地形数据 + + Args: + base_noise: 基础噪声数据 + terrain_features: 地形特征数据 + + Returns: + 混合后的地形数据 + """ + try: + height, width = base_noise.shape + + # 确保两个数组尺寸相同 + if terrain_features.shape != (height, width): + # 调整地形特征尺寸 + terrain_features = np.resize(terrain_features, (height, width)) + + # 混合数据 + blended_map = np.zeros((height, width), dtype=np.float32) + + # 获取地形权重 + mountain_weight = self.terrain_weights.get('mountains', 0.3) + hill_weight = self.terrain_weights.get('hills', 0.4) + plain_weight = self.terrain_weights.get('plains', 0.2) + valley_weight = self.terrain_weights.get('valleys', 0.1) + + for y in range(height): + for x in range(width): + # 基础噪声值 + noise_value = base_noise[y, x] + + # 地形特征值 + feature_value = terrain_features[y, x] + + # 根据权重混合 + blended_value = ( + noise_value * 0.7 + # 主要使用噪声 + feature_value * 0.3 # 添加部分特征 + ) + + # 应用高度乘数 + blended_value *= self.height_multiplier + + # 确保值在0-1范围内 + blended_value = max(0.0, min(1.0, blended_value)) + + blended_map[y, x] = blended_value + + return blended_map + + except Exception as e: + print(f"✗ 地形数据混合失败: {e}") + return base_noise + + def _post_process_heightmap(self, heightmap: np.ndarray) -> np.ndarray: + """ + 后处理高度图 + + Args: + heightmap: 原始高度图 + + Returns: + 处理后的高度图 + """ + try: + processed_map = heightmap.copy() + height, width = processed_map.shape + + # 应用平滑滤波器 + processed_map = self._apply_smoothing_filter(processed_map) + + # 调整边缘值,使边缘更加平滑 + processed_map = self._smooth_edges(processed_map) + + # 应用高程调整 + processed_map = self._adjust_elevation(processed_map) + + # 确保所有值在0-1范围内 + processed_map = np.clip(processed_map, 0.0, 1.0) + + return processed_map + + except Exception as e: + print(f"✗ 高度图后处理失败: {e}") + return heightmap + + def _apply_smoothing_filter(self, heightmap: np.ndarray) -> np.ndarray: + """ + 应用平滑滤波器 + + Args: + heightmap: 高度图数据 + + Returns: + 平滑后的高度图 + """ + try: + smoothed = heightmap.copy() + height, width = smoothed.shape + + # 简单的3x3平均滤波器 + for y in range(1, height - 1): + for x in range(1, width - 1): + # 计算3x3邻域的平均值 + avg_value = ( + heightmap[y-1, x-1] + heightmap[y-1, x] + heightmap[y-1, x+1] + + heightmap[y, x-1] + heightmap[y, x] + heightmap[y, x+1] + + heightmap[y+1, x-1] + heightmap[y+1, x] + heightmap[y+1, x+1] + ) / 9.0 + + smoothed[y, x] = avg_value + + return smoothed + + except Exception as e: + print(f"✗ 平滑滤波器应用失败: {e}") + return heightmap + + def _smooth_edges(self, heightmap: np.ndarray) -> np.ndarray: + """ + 平滑地形边缘 + + Args: + heightmap: 高度图数据 + + Returns: + 边缘平滑后的高度图 + """ + try: + smoothed = heightmap.copy() + height, width = smoothed.shape + + # 平滑边缘几行/列 + edge_width = min(10, width // 20) # 边缘宽度为地形尺寸的5%或10像素 + + # 平滑顶部和底部边缘 + for i in range(edge_width): + factor = i / edge_width + # 顶部边缘 + smoothed[i, :] *= factor + # 底部边缘 + smoothed[height - 1 - i, :] *= factor + + # 平滑左侧和右侧边缘 + for i in range(edge_width): + factor = i / edge_width + # 左侧边缘 + smoothed[:, i] *= factor + # 右侧边缘 + smoothed[:, width - 1 - i] *= factor + + return smoothed + + except Exception as e: + print(f"✗ 边缘平滑处理失败: {e}") + return heightmap + + def _adjust_elevation(self, heightmap: np.ndarray) -> np.ndarray: + """ + 调整地形高程 + + Args: + heightmap: 高度图数据 + + Returns: + 高程调整后的高度图 + """ + try: + adjusted = heightmap.copy() + + # 应用高程曲线调整,使低地更平缓,高地更陡峭 + adjusted = np.power(adjusted, 1.5) + + # 添加一些随机变化 + noise = np.random.rand(*adjusted.shape).astype(np.float32) * 0.02 + adjusted = adjusted + noise + adjusted = np.clip(adjusted, 0.0, 1.0) + + return adjusted + + except Exception as e: + print(f"✗ 高程调整失败: {e}") + return heightmap + + def set_seed(self, seed: int): + """ + 设置随机种子 + + Args: + seed: 随机种子 + """ + self.seed = seed + print(f"✓ 高度图生成器随机种子设置为: {seed}") + + def set_scale(self, scale: float): + """ + 设置噪声缩放 + + Args: + scale: 噪声缩放因子 + """ + self.scale = max(0.1, scale) + print(f"✓ 噪声缩放设置为: {self.scale}") + + def set_octaves(self, octaves: int): + """ + 设置噪声八度数 + + Args: + octaves: 八度数 + """ + self.octaves = max(1, min(10, octaves)) + print(f"✓ 噪声八度数设置为: {self.octaves}") + + def set_persistence(self, persistence: float): + """ + 设置噪声持久性 + + Args: + persistence: 持久性值 + """ + self.persistence = max(0.0, min(1.0, persistence)) + print(f"✓ 噪声持久性设置为: {self.persistence}") + + def set_lacunarity(self, lacunarity: float): + """ + 设置噪声间隙性 + + Args: + lacunarity: 间隙性值 + """ + self.lacunarity = max(1.0, lacunarity) + print(f"✓ 噪声间隙性设置为: {self.lacunarity}") + + def set_height_multiplier(self, multiplier: float): + """ + 设置高度乘数 + + Args: + multiplier: 高度乘数 + """ + self.height_multiplier = max(0.0, multiplier) + print(f"✓ 高度乘数设置为: {self.height_multiplier}") + + def set_water_level(self, level: float): + """ + 设置水位 + + Args: + level: 水位值 (0.0-1.0) + """ + self.water_level = max(0.0, min(1.0, level)) + print(f"✓ 水位设置为: {self.water_level}") + + def set_terrain_weights(self, weights: Dict[str, float]): + """ + 设置地形类型权重 + + Args: + weights: 地形权重字典 + """ + self.terrain_weights.update(weights) + print(f"✓ 地形权重已更新: {self.terrain_weights}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def generate_island_heightmap(self, width: int, height: int, seed: int = None) -> np.ndarray: + """ + 生成岛屿型高度图 + + Args: + width: 高度图宽度 + height: 高度图高度 + seed: 随机种子 + + Returns: + 岛屿型高度图数据 + """ + try: + if seed is not None: + self.seed = seed + + # 设置随机种子 + random.seed(self.seed) + np.random.seed(self.seed) + + # 创建高度图数组 + heightmap = np.zeros((height, width), dtype=np.float32) + + # 计算中心点 + center_x, center_y = width / 2.0, height / 2.0 + max_distance = math.sqrt(center_x**2 + center_y**2) + + # 生成岛屿形状 + for y in range(height): + for x in range(width): + # 计算到中心的距离 + distance_x = x - center_x + distance_y = y - center_y + distance = math.sqrt(distance_x**2 + distance_y**2) + normalized_distance = distance / max_distance + + # 岛屿衰减函数 (边缘为0,中心为1) + island_factor = max(0.0, 1.0 - normalized_distance**2) + + # 生成基础噪声 + if self.noise_generator: + noise_value = self.noise_generator.generate_noise( + x / self.scale, + y / self.scale, + self.seed + ) + noise_value = (noise_value + 1) / 2 # 转换到0-1范围 + else: + noise_value = random.random() + + # 应用岛屿因子 + heightmap[y, x] = noise_value * island_factor + + # 应用后处理 + heightmap = self._post_process_heightmap(heightmap) + + return heightmap + + except Exception as e: + print(f"✗ 岛屿型高度图生成失败: {e}") + import traceback + traceback.print_exc() + return np.zeros((height, width), dtype=np.float32) + + def generate_mountain_heightmap(self, width: int, height: int, seed: int = None) -> np.ndarray: + """ + 生成山脉型高度图 + + Args: + width: 高度图宽度 + height: 高度图高度 + seed: 随机种子 + + Returns: + 山脉型高度图数据 + """ + try: + if seed is not None: + self.seed = seed + + # 设置随机种子 + random.seed(self.seed) + np.random.seed(self.seed) + + # 创建高度图数组 + heightmap = np.zeros((height, width), dtype=np.float32) + + # 生成多个山峰 + num_peaks = random.randint(3, 8) + peaks = [] + for _ in range(num_peaks): + peak_x = random.randint(0, width - 1) + peak_y = random.randint(0, height - 1) + peak_height = random.uniform(0.5, 1.0) + peak_radius = random.randint(width // 10, width // 4) + peaks.append((peak_x, peak_y, peak_height, peak_radius)) + + # 根据山峰生成地形 + for y in range(height): + for x in range(width): + height_value = 0.0 + for peak_x, peak_y, peak_height, peak_radius in peaks: + # 计算到山峰的距离 + distance = math.sqrt((x - peak_x)**2 + (y - peak_y)**2) + if distance <= peak_radius: + # 使用高斯函数生成山峰形状 + distance_factor = 1.0 - (distance / peak_radius) + height_value += peak_height * math.exp(-distance_factor**2 / 0.1) + + heightmap[y, x] = min(1.0, height_value) + + # 添加细节噪声 + if self.noise_generator: + for y in range(height): + for x in range(width): + noise_value = self.noise_generator.generate_noise( + x / (self.scale / 2), + y / (self.scale / 2), + self.seed + 1000 + ) + noise_value = (noise_value + 1) / 2 # 转换到0-1范围 + heightmap[y, x] = heightmap[y, x] * 0.8 + noise_value * 0.2 + + # 应用后处理 + heightmap = self._post_process_heightmap(heightmap) + + return heightmap + + except Exception as e: + print(f"✗ 山脉型高度图生成失败: {e}") + import traceback + traceback.print_exc() + return np.zeros((height, width), dtype=np.float32) + + def generate_plateau_heightmap(self, width: int, height: int, seed: int = None) -> np.ndarray: + """ + 生成高原型高度图 + + Args: + width: 高度图宽度 + height: 高度图高度 + seed: 随机种子 + + Returns: + 高原型高度图数据 + """ + try: + if seed is not None: + self.seed = seed + + # 设置随机种子 + random.seed(self.seed) + np.random.seed(self.seed) + + # 创建高度图数组 + heightmap = np.zeros((height, width), dtype=np.float32) + + # 生成基础高原高度 + plateau_height = random.uniform(0.4, 0.7) + + # 填充基础高原 + heightmap.fill(plateau_height) + + # 添加边缘斜坡 + slope_width = min(30, width // 10) + for i in range(slope_width): + factor = i / slope_width + # 上边缘 + heightmap[i, :] *= factor + # 下边缘 + heightmap[height - 1 - i, :] *= factor + # 左边缘 + heightmap[:, i] *= factor + # 右边缘 + heightmap[:, width - 1 - i] *= factor + + # 添加一些起伏 + if self.noise_generator: + for y in range(height): + for x in range(width): + noise_value = self.noise_generator.generate_noise( + x / self.scale, + y / self.scale, + self.seed + 2000 + ) + noise_value = (noise_value + 1) / 2 # 转换到0-1范围 + # 减少噪声影响,保持高原特征 + heightmap[y, x] = heightmap[y, x] * 0.9 + noise_value * 0.1 + + # 应用后处理 + heightmap = self._post_process_heightmap(heightmap) + + return heightmap + + except Exception as e: + print(f"✗ 高原型高度图生成失败: {e}") + import traceback + traceback.print_exc() + return np.zeros((height, width), dtype=np.float32) + + def generate_valley_heightmap(self, width: int, height: int, seed: int = None) -> np.ndarray: + """ + 生成峡谷型高度图 + + Args: + width: 高度图宽度 + height: 高度图高度 + seed: 随机种子 + + Returns: + 峡谷型高度图数据 + """ + try: + if seed is not None: + self.seed = seed + + # 设置随机种子 + random.seed(self.seed) + np.random.seed(self.seed) + + # 创建高度图数组 + heightmap = np.zeros((height, width), dtype=np.float32) + + # 选择峡谷方向 (0=水平, 1=垂直, 2=对角线) + valley_direction = random.randint(0, 2) + + # 生成峡谷 + for y in range(height): + for x in range(width): + if valley_direction == 0: # 水平峡谷 + valley_factor = 1.0 - abs(y - height/2) / (height/2) + elif valley_direction == 1: # 垂直峡谷 + valley_factor = 1.0 - abs(x - width/2) / (width/2) + else: # 对角线峡谷 + valley_factor = 1.0 - abs((x - width/2) - (y - height/2)) / max(width, height) + + # 确保峡谷因子在合理范围内 + valley_factor = max(0.0, min(1.0, valley_factor)) + + # 生成周围地形 + if self.noise_generator: + noise_value = self.noise_generator.generate_noise( + x / self.scale, + y / self.scale, + self.seed + 3000 + ) + noise_value = (noise_value + 1) / 2 # 转换到0-1范围 + else: + noise_value = random.random() + + # 应用峡谷因子 (峡谷区域较低) + heightmap[y, x] = noise_value * (0.5 + valley_factor * 0.5) + + # 应用后处理 + heightmap = self._post_process_heightmap(heightmap) + + return heightmap + + except Exception as e: + print(f"✗ 峡谷型高度图生成失败: {e}") + import traceback + traceback.print_exc() + return np.zeros((height, width), dtype=np.float32) + + def generate_crater_heightmap(self, width: int, height: int, seed: int = None) -> np.ndarray: + """ + 生成陨石坑型高度图 + + Args: + width: 高度图宽度 + height: 高度图高度 + seed: 随机种子 + + Returns: + 陨石坑型高度图数据 + """ + try: + if seed is not None: + self.seed = seed + + # 设置随机种子 + random.seed(self.seed) + np.random.seed(self.seed) + + # 创建高度图数组 + heightmap = np.zeros((height, width), dtype=np.float32) + + # 生成多个陨石坑 + num_craters = random.randint(1, 5) + craters = [] + for _ in range(num_craters): + crater_x = random.randint(width // 4, 3 * width // 4) + crater_y = random.randint(height // 4, 3 * height // 4) + crater_radius = random.randint(width // 20, width // 5) + crater_depth = random.uniform(0.2, 0.6) + craters.append((crater_x, crater_y, crater_radius, crater_depth)) + + # 初始化为平坦地形 + heightmap.fill(0.8) + + # 根据陨石坑生成地形 + for y in range(height): + for x in range(width): + for crater_x, crater_y, crater_radius, crater_depth in craters: + # 计算到陨石坑中心的距离 + distance = math.sqrt((x - crater_x)**2 + (y - crater_y)**2) + if distance <= crater_radius: + # 陨石坑形状 (边缘高,中心低) + if distance < crater_radius * 0.8: # 坑内 + # 抛物线形坑底 + normalized_distance = distance / (crater_radius * 0.8) + depth_factor = 1.0 - normalized_distance**2 + heightmap[y, x] = min(heightmap[y, x], 0.8 - crater_depth * depth_factor) + else: # 坑边缘 + # 边缘隆起 + edge_factor = (distance - crater_radius * 0.8) / (crater_radius * 0.2) + heightmap[y, x] = max(heightmap[y, x], 0.8 + crater_depth * 0.3 * (1.0 - edge_factor)) + + # 添加细节噪声 + if self.noise_generator: + for y in range(height): + for x in range(width): + noise_value = self.noise_generator.generate_noise( + x / (self.scale / 3), + y / (self.scale / 3), + self.seed + 4000 + ) + noise_value = (noise_value + 1) / 2 # 转换到0-1范围 + heightmap[y, x] = heightmap[y, x] * 0.9 + noise_value * 0.1 + + # 应用后处理 + heightmap = self._post_process_heightmap(heightmap) + + return heightmap + + except Exception as e: + print(f"✗ 陨石坑型高度图生成失败: {e}") + import traceback + traceback.print_exc() + return np.zeros((height, width), dtype=np.float32) + + def generate_terrain_with_zones(self, width: int, height: int, zones: Dict[str, Any], seed: int = None) -> np.ndarray: + """ + 根据区域定义生成地形 + + Args: + width: 高度图宽度 + height: 高度图高度 + zones: 区域定义字典 + seed: 随机种子 + + Returns: + 区域化高度图数据 + """ + try: + if seed is not None: + self.seed = seed + + # 设置随机种子 + random.seed(self.seed) + np.random.seed(self.seed) + + # 创建高度图数组 + heightmap = np.zeros((height, width), dtype=np.float32) + + # 为每个区域生成地形 + for zone_name, zone_params in zones.items(): + zone_type = zone_params.get('type', 'default') + zone_position = zone_params.get('position', (0, 0)) + zone_size = zone_params.get('size', (width, height)) + zone_height = zone_params.get('height', 0.5) + + # 根据区域类型生成地形 + if zone_type == 'mountain': + # 在指定区域生成山脉 + self._generate_zone_mountains(heightmap, zone_position, zone_size, zone_height) + elif zone_type == 'valley': + # 在指定区域生成峡谷 + self._generate_zone_valley(heightmap, zone_position, zone_size, zone_height) + elif zone_type == 'plateau': + # 在指定区域生成高原 + self._generate_zone_plateau(heightmap, zone_position, zone_size, zone_height) + elif zone_type == 'water': + # 在指定区域生成水域 + self._generate_zone_water(heightmap, zone_position, zone_size, zone_height) + + # 应用后处理 + heightmap = self._post_process_heightmap(heightmap) + + return heightmap + + except Exception as e: + print(f"✗ 区域化地形生成失败: {e}") + import traceback + traceback.print_exc() + return np.zeros((height, width), dtype=np.float32) + + def _generate_zone_mountains(self, heightmap: np.ndarray, position: tuple, size: tuple, base_height: float): + """在指定区域生成山脉""" + try: + x_start, y_start = position + width, height = size + map_height, map_width = heightmap.shape + + # 确保区域在地图范围内 + x_start = max(0, min(x_start, map_width - 1)) + y_start = max(0, min(y_start, map_height - 1)) + x_end = min(map_width, x_start + width) + y_end = min(map_height, y_start + height) + + # 生成山脉 + for y in range(y_start, y_end): + for x in range(x_start, x_end): + # 计算相对于区域的位置 + rel_x = (x - x_start) / width + rel_y = (y - y_start) / height + + # 生成山峰 + peak_influence = math.sin(rel_x * math.pi) * math.sin(rel_y * math.pi) + heightmap[y, x] = max(heightmap[y, x], base_height + peak_influence * 0.3) + + except Exception as e: + print(f"✗ 区域山脉生成失败: {e}") + + def _generate_zone_valley(self, heightmap: np.ndarray, position: tuple, size: tuple, base_height: float): + """在指定区域生成峡谷""" + try: + x_start, y_start = position + width, height = size + map_height, map_width = heightmap.shape + + # 确保区域在地图范围内 + x_start = max(0, min(x_start, map_width - 1)) + y_start = max(0, min(y_start, map_height - 1)) + x_end = min(map_width, x_start + width) + y_end = min(map_height, y_start + height) + + # 生成峡谷 + for y in range(y_start, y_end): + for x in range(x_start, x_end): + # 计算相对于区域的位置 + rel_x = (x - x_start) / width + rel_y = (y - y_start) / height + + # 生成峡谷形状 (中心低,边缘高) + valley_factor = 1.0 - math.sqrt((rel_x - 0.5)**2 + (rel_y - 0.5)**2) * 2 + valley_factor = max(0.0, valley_factor) + heightmap[y, x] = min(heightmap[y, x], base_height - valley_factor * 0.4) + + except Exception as e: + print(f"✗ 区域峡谷生成失败: {e}") + + def _generate_zone_plateau(self, heightmap: np.ndarray, position: tuple, size: tuple, base_height: float): + """在指定区域生成高原""" + try: + x_start, y_start = position + width, height = size + map_height, map_width = heightmap.shape + + # 确保区域在地图范围内 + x_start = max(0, min(x_start, map_width - 1)) + y_start = max(0, min(y_start, map_height - 1)) + x_end = min(map_width, x_start + width) + y_end = min(map_height, y_start + height) + + # 生成高原 + for y in range(y_start, y_end): + for x in range(x_start, x_end): + heightmap[y, x] = max(heightmap[y, x], base_height) + + except Exception as e: + print(f"✗ 区域高原生成失败: {e}") + + def _generate_zone_water(self, heightmap: np.ndarray, position: tuple, size: tuple, water_level: float): + """在指定区域生成水域""" + try: + x_start, y_start = position + width, height = size + map_height, map_width = heightmap.shape + + # 确保区域在地图范围内 + x_start = max(0, min(x_start, map_width - 1)) + y_start = max(0, min(y_start, map_height - 1)) + x_end = min(map_width, x_start + width) + y_end = min(map_height, y_start + height) + + # 生成平坦水域 + for y in range(y_start, y_end): + for x in range(x_start, x_end): + heightmap[y, x] = min(heightmap[y, x], water_level) + + except Exception as e: + print(f"✗ 区域水域生成失败: {e}") + + def blend_heightmaps(self, heightmaps: List[np.ndarray], weights: List[float] = None) -> np.ndarray: + """ + 混合多个高度图 + + Args: + heightmaps: 高度图列表 + weights: 权重列表 (如果为None,则平均分配权重) + + Returns: + 混合后的高度图 + """ + try: + if not heightmaps: + return np.array([]) + + if len(heightmaps) == 1: + return heightmaps[0].copy() + + # 确保所有高度图尺寸相同 + base_shape = heightmaps[0].shape + for hm in heightmaps: + if hm.shape != base_shape: + raise ValueError("所有高度图必须具有相同的尺寸") + + # 处理权重 + if weights is None: + weights = [1.0 / len(heightmaps)] * len(heightmaps) + elif len(weights) != len(heightmaps): + raise ValueError("权重数量必须与高度图数量相同") + + # 归一化权重 + total_weight = sum(weights) + if total_weight > 0: + weights = [w / total_weight for w in weights] + + # 混合高度图 + blended = np.zeros(base_shape, dtype=np.float32) + for i, (hm, weight) in enumerate(zip(heightmaps, weights)): + blended += hm * weight + + # 确保值在0-1范围内 + blended = np.clip(blended, 0.0, 1.0) + + return blended + + except Exception as e: + print(f"✗ 高度图混合失败: {e}") + import traceback + traceback.print_exc() + # 返回第一个高度图的副本 return heightmaps[0].copy() if heightmaps else np.array([]) \ No newline at end of file diff --git a/plugins/user/procedural_terrain_generation/noise/noise_generator.py b/plugins/user/procedural_terrain_generation/noise/noise_generator.py index 53d845f1..8e3fd5d5 100644 --- a/plugins/user/procedural_terrain_generation/noise/noise_generator.py +++ b/plugins/user/procedural_terrain_generation/noise/noise_generator.py @@ -1,1319 +1,1319 @@ -""" -噪声生成器 -负责生成各种类型的程序化噪声 -""" - -import numpy as np -import math -from typing import Dict, Any - -class NoiseGenerator: - """ - 噪声生成器 - 负责生成各种类型的程序化噪声,包括Perlin噪声、Simplex噪声、分形噪声等 - """ - - def __init__(self, plugin): - """ - 初始化噪声生成器 - - Args: - plugin: 程序化地形生成插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 噪声配置 - self.seed = plugin.config.get('seed', 12345) - - # 噪声类型参数 - self.noise_types = { - 'perlin': {'enabled': True, 'weight': 1.0}, - 'simplex': {'enabled': True, 'weight': 1.0}, - 'value': {'enabled': True, 'weight': 0.5}, - 'worley': {'enabled': True, 'weight': 0.3}, - 'white': {'enabled': True, 'weight': 0.2} - } - - # 分形噪声参数 - self.fractal_params = { - 'octaves': 6, - 'persistence': 0.5, - 'lacunarity': 2.0, - 'scale': 1.0 - } - - # 随机数生成器 - self.random_generator = None - - # 预计算的梯度向量表 - self.gradient_table = [] - - # 统计信息 - self.stats = { - 'noise_generated': 0, - 'total_generation_time': 0.0, - 'average_generation_time': 0.0 - } - - print("✓ 噪声生成器已创建") - - def initialize(self) -> bool: - """ - 初始化噪声生成器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化随机数生成器 - self.random_generator = np.random.RandomState(self.seed) - - # 初始化梯度表 - self._initialize_gradient_table() - - self.initialized = True - print("✓ 噪声生成器初始化完成") - return True - - except Exception as e: - print(f"✗ 噪声生成器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用噪声生成器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 噪声生成器未初始化") - return False - - self.enabled = True - print("✓ 噪声生成器已启用") - return True - - except Exception as e: - print(f"✗ 噪声生成器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用噪声生成器""" - try: - self.enabled = False - print("✓ 噪声生成器已禁用") - - except Exception as e: - print(f"✗ 噪声生成器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理噪声生成器资源""" - try: - self.disable() - self.initialized = False - print("✓ 噪声生成器资源已清理") - - except Exception as e: - print(f"✗ 噪声生成器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新噪声生成器状态 - - Args: - dt: 时间增量 - """ - # 处理更新逻辑 - pass - - def _initialize_gradient_table(self): - """初始化梯度向量表""" - try: - # 创建2D梯度向量表 (Perlin噪声使用) - self.gradient_table = [] - for i in range(256): - # 生成随机角度 - angle = self.random_generator.uniform(0, 2 * math.pi) - # 计算梯度向量 - gradient = (math.cos(angle), math.sin(angle)) - self.gradient_table.append(gradient) - - # 复制表以避免边界检查 - self.gradient_table = self.gradient_table + self.gradient_table - - except Exception as e: - print(f"✗ 梯度表初始化失败: {e}") - # 创建默认梯度表 - self.gradient_table = [(1, 0), (0, 1), (-1, 0), (0, -1)] * 64 - - def set_seed(self, seed: int): - """ - 设置随机种子 - - Args: - seed: 随机种子 - """ - self.seed = seed - if self.random_generator is not None: - self.random_generator.seed(seed) - # 重新初始化梯度表 - self._initialize_gradient_table() - print(f"✓ 噪声生成器随机种子设置为: {seed}") - - def generate_noise(self, x: float, y: float, seed: int = None) -> float: - """ - 生成噪声值 - - Args: - x: X坐标 - y: Y坐标 - seed: 随机种子(可选) - - Returns: - 噪声值 (-1.0 到 1.0) - """ - try: - if not self.enabled: - return 0.0 - - # 使用指定种子或默认种子 - if seed is not None: - local_seed = seed - else: - local_seed = self.seed - - # 生成Perlin噪声(默认) - noise_value = self._perlin_noise(x, y, local_seed) - - # 更新统计信息 - self.stats['noise_generated'] += 1 - - return noise_value - - except Exception as e: - print(f"✗ 噪声生成失败: {e}") - return 0.0 - - def _perlin_noise(self, x: float, y: float, seed: int) -> float: - """ - 生成Perlin噪声 - - Args: - x: X坐标 - y: Y坐标 - seed: 随机种子 - - Returns: - Perlin噪声值 (-1.0 到 1.0) - """ - try: - # 确保使用正确的种子 - if seed != self.seed and self.random_generator is not None: - self.random_generator.seed(seed) - - # 找到包含点的单元格 - x0 = int(math.floor(x)) & 255 - y0 = int(math.floor(y)) & 255 - x1 = (x0 + 1) & 255 - y1 = (y0 + 1) & 255 - - # 计算单元格内的坐标 - xf = x - math.floor(x) - yf = y - math.floor(y) - - # 计算平滑系数 - u = self._fade(xf) - v = self._fade(yf) - - # 计算梯度点积 - n00 = self._gradient(self.gradient_table[x0 + self.gradient_table[y0][0]], xf, yf) - n01 = self._gradient(self.gradient_table[x0 + self.gradient_table[y1][0]], xf, yf - 1) - n10 = self._gradient(self.gradient_table[x1 + self.gradient_table[y0][0]], xf - 1, yf) - n11 = self._gradient(self.gradient_table[x1 + self.gradient_table[y1][0]], xf - 1, yf - 1) - - # 双线性插值 - x1_result = self._lerp(n00, n10, u) - x2_result = self._lerp(n01, n11, u) - result = self._lerp(x1_result, x2_result, v) - - return result - - except Exception as e: - # 出错时返回简单噪声 - return self._simple_noise(x, y, seed) - - def _fade(self, t: float) -> float: - """ - Fade函数 (6*t^5 - 15*t^4 + 10*t^3) - - Args: - t: 输入值 - - Returns: - 平滑后的值 - """ - return t * t * t * (t * (t * 6 - 15) + 10) - - def _lerp(self, a: float, b: float, t: float) -> float: - """ - 线性插值 - - Args: - a: 起始值 - b: 结束值 - t: 插值参数 (0-1) - - Returns: - 插值结果 - """ - return a + t * (b - a) - - def _gradient(self, hash_val: tuple, x: float, y: float) -> float: - """ - 计算梯度点积 - - Args: - hash_val: 梯度向量 - x: X偏移 - y: Y偏移 - - Returns: - 点积结果 - """ - return hash_val[0] * x + hash_val[1] * y - - def _simple_noise(self, x: float, y: float, seed: int) -> float: - """ - 简单噪声生成(备用方案) - - Args: - x: X坐标 - y: Y坐标 - seed: 随机种子 - - Returns: - 噪声值 - """ - # 使用正弦波生成伪随机噪声 - value = math.sin(x * 12.9898 + y * 78.233 + seed) * 43758.5453 - return (value - math.floor(value)) * 2.0 - 1.0 - - def generate_simplex_noise(self, x: float, y: float, seed: int = None) -> float: - """ - 生成Simplex噪声 - - Args: - x: X坐标 - y: Y坐标 - seed: 随机种子(可选) - - Returns: - Simplex噪声值 (-1.0 到 1.0) - """ - try: - if not self.enabled: - return 0.0 - - # 使用指定种子或默认种子 - if seed is not None: - local_seed = seed - else: - local_seed = self.seed - - # Simplex噪声实现 - # 这里使用简化版本 - value = self._simple_noise(x, y, local_seed) - return value - - except Exception as e: - print(f"✗ Simplex噪声生成失败: {e}") - return 0.0 - - def generate_value_noise(self, x: float, y: float, seed: int = None) -> float: - """ - 生成值噪声 - - Args: - x: X坐标 - y: Y坐标 - seed: 随机种子(可选) - - Returns: - 值噪声值 (-1.0 到 1.0) - """ - try: - if not self.enabled: - return 0.0 - - # 使用指定种子或默认种子 - if seed is not None: - local_seed = seed - else: - local_seed = self.seed - - # 值噪声实现 - # 获取整数坐标 - x_int = int(math.floor(x)) - y_int = int(math.floor(y)) - - # 获取小数部分 - x_frac = x - x_int - y_frac = y - y_int - - # 生成四个角的随机值 - corners = [] - for dy in [0, 1]: - for dx in [0, 1]: - corner_seed = ((x_int + dx) * 374761393 + (y_int + dy) * 668265263 + local_seed) & 0x7fffffff - self.random_generator.seed(corner_seed) - corners.append(self.random_generator.uniform(-1.0, 1.0)) - - # 双线性插值 - u = self._fade(x_frac) - v = self._fade(y_frac) - - top = self._lerp(corners[0], corners[1], u) - bottom = self._lerp(corners[2], corners[3], u) - result = self._lerp(top, bottom, v) - - return result - - except Exception as e: - print(f"✗ 值噪声生成失败: {e}") - return 0.0 - - def generate_worley_noise(self, x: float, y: float, seed: int = None) -> float: - """ - 生成Worley噪声(细胞噪声) - - Args: - x: X坐标 - y: Y坐标 - seed: 随机种子(可选) - - Returns: - Worley噪声值 (0.0 到 1.0) - """ - try: - if not self.enabled: - return 0.0 - - # 使用指定种子或默认种子 - if seed is not None: - local_seed = seed - else: - local_seed = self.seed - - # 找到点所在的网格单元格 - cell_x = int(math.floor(x)) - cell_y = int(math.floor(y)) - - # 初始化最小距离 - min_distance = float('inf') - - # 检查周围的9个单元格 - for dy in [-1, 0, 1]: - for dx in [-1, 0, 1]: - # 计算相邻单元格坐标 - nx = cell_x + dx - ny = cell_y + dy - - # 生成单元格中的特征点 - feature_seed = (nx * 374761393 + ny * 668265263 + local_seed) & 0x7fffffff - self.random_generator.seed(feature_seed) - fx = nx + self.random_generator.uniform(0.0, 1.0) - fy = ny + self.random_generator.uniform(0.0, 1.0) - - # 计算到特征点的距离 - distance = math.sqrt((x - fx)**2 + (y - fy)**2) - min_distance = min(min_distance, distance) - - # 将距离转换为噪声值 (通常限制在0-1范围内) - return min(1.0, min_distance) - - except Exception as e: - print(f"✗ Worley噪声生成失败: {e}") - return 0.0 - - def generate_white_noise(self, x: float, y: float, seed: int = None) -> float: - """ - 生成白噪声 - - Args: - x: X坐标 - y: Y坐标 - seed: 随机种子(可选) - - Returns: - 白噪声值 (-1.0 到 1.0) - """ - try: - if not self.enabled: - return 0.0 - - # 使用指定种子或默认种子 - if seed is not None: - local_seed = seed - else: - local_seed = self.seed - - # 生成伪随机值 - noise_seed = (int(x * 1000) * 374761393 + int(y * 1000) * 668265263 + local_seed) & 0x7fffffff - self.random_generator.seed(noise_seed) - return self.random_generator.uniform(-1.0, 1.0) - - except Exception as e: - print(f"✗ 白噪声生成失败: {e}") - return 0.0 - - def generate_fractal_noise(self, x: float, y: float, seed: int = None, - octaves: int = None, persistence: float = None, - lacunarity: float = None, scale: float = None) -> float: - """ - 生成分形噪声(fBm - 分数布朗运动) - - Args: - x: X坐标 - y: Y坐标 - seed: 随机种子(可选) - octaves: 八度数(可选) - persistence: 持久性(可选) - lacunarity: 间隙性(可选) - scale: 缩放(可选) - - Returns: - 分形噪声值 (-1.0 到 1.0) - """ - try: - if not self.enabled: - return 0.0 - - # 使用指定参数或默认参数 - if seed is not None: - local_seed = seed - else: - local_seed = self.seed - - if octaves is None: - octaves = self.fractal_params['octaves'] - if persistence is None: - persistence = self.fractal_params['persistence'] - if lacunarity is None: - lacunarity = self.fractal_params['lacunarity'] - if scale is None: - scale = self.fractal_params['scale'] - - # 生成分形噪声 - total = 0.0 - frequency = scale - amplitude = 1.0 - max_value = 0.0 # 用于归一化 - - for i in range(octaves): - # 生成当前层噪声 - noise_value = self.generate_noise(x * frequency, y * frequency, local_seed + i) - total += noise_value * amplitude - - # 更新最大值 - max_value += amplitude - - # 更新频率和振幅 - frequency *= lacunarity - amplitude *= persistence - - # 归一化结果 - if max_value > 0: - return total / max_value - else: - return total - - except Exception as e: - print(f"✗ 分形噪声生成失败: {e}") - return 0.0 - - def generate_rigid_noise(self, x: float, y: float, seed: int = None, - octaves: int = None, persistence: float = None, - lacunarity: float = None, scale: float = None) -> float: - """ - 生成刚性噪声(用于生成山脉等地质特征) - - Args: - x: X坐标 - y: Y坐标 - seed: 随机种子(可选) - octaves: 八度数(可选) - persistence: 持久性(可选) - lacunarity: 间隙性(可选) - scale: 缩放(可选) - - Returns: - 刚性噪声值 (0.0 到 1.0) - """ - try: - if not self.enabled: - return 0.0 - - # 使用指定参数或默认参数 - if seed is not None: - local_seed = seed - else: - local_seed = self.seed - - if octaves is None: - octaves = self.fractal_params['octaves'] - if persistence is None: - persistence = self.fractal_params['persistence'] - if lacunarity is None: - lacunarity = self.fractal_params['lacunarity'] - if scale is None: - scale = self.fractal_params['scale'] - - # 生成刚性噪声 - total = 0.0 - frequency = scale - amplitude = 1.0 - - for i in range(octaves): - # 生成当前层噪声并转换为刚性形式 - noise_value = abs(self.generate_noise(x * frequency, y * frequency, local_seed + i)) - total += (1.0 - noise_value) * amplitude - - # 更新频率和振幅 - frequency *= lacunarity - amplitude *= persistence - - return min(1.0, total) - - except Exception as e: - print(f"✗ 刚性噪声生成失败: {e}") - return 0.0 - - def generate_turbulence_noise(self, x: float, y: float, seed: int = None, - octaves: int = None, persistence: float = None, - lacunarity: float = None, scale: float = None) -> float: - """ - 生成湍流噪声 - - Args: - x: X坐标 - y: Y坐标 - seed: 随机种子(可选) - octaves: 八度数(可选) - persistence: 持久性(可选) - lacunarity: 间隙性(可选) - scale: 缩放(可选) - - Returns: - 湍流噪声值 (0.0 到 1.0) - """ - try: - if not self.enabled: - return 0.0 - - # 使用指定参数或默认参数 - if seed is not None: - local_seed = seed - else: - local_seed = self.seed - - if octaves is None: - octaves = self.fractal_params['octaves'] - if persistence is None: - persistence = self.fractal_params['persistence'] - if lacunarity is None: - lacunarity = self.fractal_params['lacunarity'] - if scale is None: - scale = self.fractal_params['scale'] - - # 生成湍流噪声 - total = 0.0 - frequency = scale - amplitude = 1.0 - - for i in range(octaves): - # 生成当前层噪声并取绝对值 - noise_value = abs(self.generate_noise(x * frequency, y * frequency, local_seed + i)) - total += noise_value * amplitude - - # 更新频率和振幅 - frequency *= lacunarity - amplitude *= persistence - - return min(1.0, total) - - except Exception as e: - print(f"✗ 湍流噪声生成失败: {e}") - return 0.0 - - def generate_combined_noise(self, x: float, y: float, seed: int = None) -> float: - """ - 生成组合噪声(混合多种噪声类型) - - Args: - x: X坐标 - y: Y坐标 - seed: 随机种子(可选) - - Returns: - 组合噪声值 (-1.0 到 1.0) - """ - try: - if not self.enabled: - return 0.0 - - # 使用指定种子或默认种子 - if seed is not None: - local_seed = seed - else: - local_seed = self.seed - - total = 0.0 - total_weight = 0.0 - - # 生成Perlin噪声 - if self.noise_types['perlin']['enabled']: - weight = self.noise_types['perlin']['weight'] - noise_value = self.generate_noise(x, y, local_seed) - total += noise_value * weight - total_weight += weight - - # 生成Simplex噪声 - if self.noise_types['simplex']['enabled']: - weight = self.noise_types['simplex']['weight'] - noise_value = self.generate_simplex_noise(x, y, local_seed + 1000) - total += noise_value * weight - total_weight += weight - - # 生成值噪声 - if self.noise_types['value']['enabled']: - weight = self.noise_types['value']['weight'] - noise_value = self.generate_value_noise(x, y, local_seed + 2000) - total += noise_value * weight - total_weight += weight - - # 生成Worley噪声 - if self.noise_types['worley']['enabled']: - weight = self.noise_types['worley']['weight'] - noise_value = self.generate_worley_noise(x, y, local_seed + 3000) - # Worley噪声范围是0-1,需要转换为-1到1 - noise_value = noise_value * 2.0 - 1.0 - total += noise_value * weight - total_weight += weight - - # 生成白噪声 - if self.noise_types['white']['enabled']: - weight = self.noise_types['white']['weight'] - noise_value = self.generate_white_noise(x, y, local_seed + 4000) - total += noise_value * weight - total_weight += weight - - # 归一化结果 - if total_weight > 0: - return total / total_weight - else: - return 0.0 - - except Exception as e: - print(f"✗ 组合噪声生成失败: {e}") - return 0.0 - - def set_noise_type_enabled(self, noise_type: str, enabled: bool): - """ - 设置噪声类型是否启用 - - Args: - noise_type: 噪声类型 - enabled: 是否启用 - """ - if noise_type in self.noise_types: - self.noise_types[noise_type]['enabled'] = enabled - print(f"✓ 噪声类型 '{noise_type}' 已{'启用' if enabled else '禁用'}") - else: - print(f"✗ 无效的噪声类型: {noise_type}") - - def set_noise_type_weight(self, noise_type: str, weight: float): - """ - 设置噪声类型权重 - - Args: - noise_type: 噪声类型 - weight: 权重值 - """ - if noise_type in self.noise_types: - self.noise_types[noise_type]['weight'] = max(0.0, weight) - print(f"✓ 噪声类型 '{noise_type}' 权重设置为: {weight}") - else: - print(f"✗ 无效的噪声类型: {noise_type}") - - def set_fractal_parameters(self, octaves: int = None, persistence: float = None, - lacunarity: float = None, scale: float = None): - """ - 设置分形噪声参数 - - Args: - octaves: 八度数 - persistence: 持久性 - lacunarity: 间隙性 - scale: 缩放 - """ - if octaves is not None: - self.fractal_params['octaves'] = max(1, octaves) - if persistence is not None: - self.fractal_params['persistence'] = max(0.0, min(1.0, persistence)) - if lacunarity is not None: - self.fractal_params['lacunarity'] = max(1.0, lacunarity) - if scale is not None: - self.fractal_params['scale'] = max(0.01, scale) - - print(f"✓ 分形噪声参数已更新: {self.fractal_params}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - # 更新平均生成时间 - if self.stats['noise_generated'] > 0: - self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['noise_generated'] - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'noise_generated': 0, - 'total_generation_time': 0.0, - 'average_generation_time': 0.0 - } - print("✓ 噪声生成器统计信息已重置") - - def generate_anisotropic_noise(self, x: float, y: float, direction: float, - anisotropy: float, seed: int = None) -> float: - """ - 生成各向异性噪声 - - Args: - x: X坐标 - y: Y坐标 - direction: 主要方向(弧度) - anisotropy: 各向异性程度(0-1) - seed: 随机种子(可选) - - Returns: - 各向异性噪声值 (-1.0 到 1.0) - """ - try: - if not self.enabled: - return 0.0 - - # 使用指定种子或默认种子 - if seed is not None: - local_seed = seed - else: - local_seed = self.seed - - # 计算各向异性变换 - cos_dir = math.cos(direction) - sin_dir = math.sin(direction) - - # 应用各向异性变换 - x_aniso = x * (cos_dir * cos_dir + sin_dir * sin_dir * (1 - anisotropy)) - y_aniso = y * (sin_dir * sin_dir + cos_dir * cos_dir * (1 - anisotropy)) - - # 生成噪声 - return self.generate_noise(x_aniso, y_aniso, local_seed) - - except Exception as e: - print(f"✗ 各向异性噪声生成失败: {e}") - return 0.0 - - def generate_billow_noise(self, x: float, y: float, seed: int = None, - octaves: int = None, persistence: float = None, - lacunarity: float = None, scale: float = None) -> float: - """ - 生成云朵状噪声(Billow噪声) - - Args: - x: X坐标 - y: Y坐标 - seed: 随机种子(可选) - octaves: 八度数(可选) - persistence: 持久性(可选) - lacunarity: 间隙性(可选) - scale: 缩放(可选) - - Returns: - Billow噪声值 (0.0 到 1.0) - """ - try: - if not self.enabled: - return 0.0 - - # 使用指定参数或默认参数 - if seed is not None: - local_seed = seed - else: - local_seed = self.seed - - if octaves is None: - octaves = self.fractal_params['octaves'] - if persistence is None: - persistence = self.fractal_params['persistence'] - if lacunarity is None: - lacunarity = self.fractal_params['lacunarity'] - if scale is None: - scale = self.fractal_params['scale'] - - # 生成Billow噪声 - total = 0.0 - frequency = scale - amplitude = 1.0 - - for i in range(octaves): - # 生成当前层噪声并转换为Billow形式 - noise_value = abs(self.generate_noise(x * frequency, y * frequency, local_seed + i)) * 2.0 - 1.0 - noise_value = noise_value * noise_value # 平方以增强对比度 - total += noise_value * amplitude - - # 更新频率和振幅 - frequency *= lacunarity - amplitude *= persistence - - # 转换到0-1范围 - return min(1.0, max(0.0, total * 0.5 + 0.5)) - - except Exception as e: - print(f"✗ Billow噪声生成失败: {e}") - return 0.0 - - def generate_voronoi_noise(self, x: float, y: float, seed: int = None, - distance_metric: str = 'euclidean') -> float: - """ - 生成Voronoi噪声(另一种细胞噪声) - - Args: - x: X坐标 - y: Y坐标 - seed: 随机种子(可选) - distance_metric: 距离度量方式 ('euclidean', 'manhattan', 'chebyshev') - - Returns: - Voronoi噪声值 (0.0 到 1.0) - """ - try: - if not self.enabled: - return 0.0 - - # 使用指定种子或默认种子 - if seed is not None: - local_seed = seed - else: - local_seed = self.seed - - # 找到点所在的网格单元格 - cell_x = int(math.floor(x)) - cell_y = int(math.floor(y)) - - # 初始化最近和次近距离 - closest_distance = float('inf') - second_closest_distance = float('inf') - - # 检查周围的9个单元格 - for dy in [-1, 0, 1]: - for dx in [-1, 0, 1]: - # 计算相邻单元格坐标 - nx = cell_x + dx - ny = cell_y + dy - - # 生成单元格中的特征点 - feature_seed = (nx * 374761393 + ny * 668265263 + local_seed) & 0x7fffffff - self.random_generator.seed(feature_seed) - fx = nx + self.random_generator.uniform(0.0, 1.0) - fy = ny + self.random_generator.uniform(0.0, 1.0) - - # 计算到特征点的距离 - if distance_metric == 'euclidean': - distance = math.sqrt((x - fx)**2 + (y - fy)**2) - elif distance_metric == 'manhattan': - distance = abs(x - fx) + abs(y - fy) - elif distance_metric == 'chebyshev': - distance = max(abs(x - fx), abs(y - fy)) - else: - distance = math.sqrt((x - fx)**2 + (y - fy)**2) - - # 更新最近和次近距离 - if distance < closest_distance: - second_closest_distance = closest_distance - closest_distance = distance - elif distance < second_closest_distance: - second_closest_distance = distance - - # 返回次近和最近距离的差值 - difference = second_closest_distance - closest_distance - return max(0.0, min(1.0, difference)) - - except Exception as e: - print(f"✗ Voronoi噪声生成失败: {e}") - return 0.0 - - def generate_cylindrical_noise(self, x: float, y: float, z: float, seed: int = None) -> float: - """ - 生成圆柱形噪声(3D噪声投影到2D) - - Args: - x: X坐标 - y: Y坐标 - z: Z坐标 - seed: 随机种子(可选) - - Returns: - 圆柱形噪声值 (-1.0 到 1.0) - """ - try: - if not self.enabled: - return 0.0 - - # 使用指定种子或默认种子 - if seed is not None: - local_seed = seed - else: - local_seed = self.seed - - # 将2D坐标转换为圆柱坐标 - angle = math.atan2(y, x) - radius = math.sqrt(x*x + y*y) - - # 生成3D噪声 - return self.generate_noise(angle, radius, local_seed + int(z * 1000)) - - except Exception as e: - print(f"✗ 圆柱形噪声生成失败: {e}") - return 0.0 - - def generate_spherical_noise(self, x: float, y: float, z: float, seed: int = None) -> float: - """ - 生成球形噪声(3D噪声投影到2D) - - Args: - x: X坐标 - y: Y坐标 - z: Z坐标 - seed: 随机种子(可选) - - Returns: - 球形噪声值 (-1.0 到 1.0) - """ - try: - if not self.enabled: - return 0.0 - - # 使用指定种子或默认种子 - if seed is not None: - local_seed = seed - else: - local_seed = self.seed - - # 将笛卡尔坐标转换为球坐标 - radius = math.sqrt(x*x + y*y + z*z) - if radius == 0: - return 0.0 - - theta = math.acos(z / radius) # 极角 - phi = math.atan2(y, x) # 方位角 - - # 生成噪声 - return self.generate_noise(theta, phi, local_seed + int(radius * 1000)) - - except Exception as e: - print(f"✗ 球形噪声生成失败: {e}") - return 0.0 - - def generate_domain_warping_noise(self, x: float, y: float, seed: int = None, - warp_strength: float = 1.0) -> float: - """ - 生成域变形噪声 - - Args: - x: X坐标 - y: Y坐标 - seed: 随机种子(可选) - warp_strength: 变形强度 - - Returns: - 域变形噪声值 (-1.0 到 1.0) - """ - try: - if not self.enabled: - return 0.0 - - # 使用指定种子或默认种子 - if seed is not None: - local_seed = seed - else: - local_seed = self.seed - - # 生成变形向量 - warp_x = self.generate_noise(x, y, local_seed + 10000) * warp_strength - warp_y = self.generate_noise(x, y, local_seed + 20000) * warp_strength - - # 应用变形并生成噪声 - return self.generate_noise(x + warp_x, y + warp_y, local_seed) - - except Exception as e: - print(f"✗ 域变形噪声生成失败: {e}") - return 0.0 - - def generate_swiss_noise(self, x: float, y: float, seed: int = None, - octaves: int = None, persistence: float = None, - lacunarity: float = None, scale: float = None) -> float: - """ - 生成Swiss噪声(用于生成山脉等地质特征) - - Args: - x: X坐标 - y: Y坐标 - seed: 随机种子(可选) - octaves: 八度数(可选) - persistence: 持久性(可选) - lacunarity: 间隙性(可选) - scale: 缩放(可选) - - Returns: - Swiss噪声值 (0.0 到 1.0) - """ - try: - if not self.enabled: - return 0.0 - - # 使用指定参数或默认参数 - if seed is not None: - local_seed = seed - else: - local_seed = self.seed - - if octaves is None: - octaves = self.fractal_params['octaves'] - if persistence is None: - persistence = self.fractal_params['persistence'] - if lacunarity is None: - lacunarity = self.fractal_params['lacunarity'] - if scale is None: - scale = self.fractal_params['scale'] - - # 生成Swiss噪声 - total = 0.0 - frequency = scale - amplitude = 1.0 - - for i in range(octaves): - # 生成当前层噪声 - noise_value = self.generate_noise(x * frequency, y * frequency, local_seed + i) - # Swiss噪声使用噪声值的绝对值并反转 - swiss_value = 1.0 - abs(noise_value) - total += swiss_value * amplitude - - # 更新频率和振幅 - frequency *= lacunarity - amplitude *= persistence * swiss_value # Swiss噪声的关键:振幅根据值调整 - - return min(1.0, max(0.0, total)) - - except Exception as e: - print(f"✗ Swiss噪声生成失败: {e}") - return 0.0 - - def generate_jordan_noise(self, x: float, y: float, seed: int = None, - octaves: int = None, persistence: float = None, - lacunarity: float = None, scale: float = None) -> float: - """ - 生成Jordan噪声(用于生成更自然的地形) - - Args: - x: X坐标 - y: Y坐标 - seed: 随机种子(可选) - octaves: 八度数(可选) - persistence: 持久性(可选) - lacunarity: 间隙性(可选) - scale: 缩放(可选) - - Returns: - Jordan噪声值 (-1.0 到 1.0) - """ - try: - if not self.enabled: - return 0.0 - - # 使用指定参数或默认参数 - if seed is not None: - local_seed = seed - else: - local_seed = self.seed - - if octaves is None: - octaves = self.fractal_params['octaves'] - if persistence is None: - persistence = self.fractal_params['persistence'] - if lacunarity is None: - lacunarity = self.fractal_params['lacunarity'] - if scale is None: - scale = self.fractal_params['scale'] - - # 生成Jordan噪声 - total = 0.0 - frequency = scale - amplitude = 1.0 - gain = 1.0 - - for i in range(octaves): - # 生成当前层噪声 - noise_value = self.generate_noise(x * frequency, y * frequency, local_seed + i) - total += noise_value * amplitude * gain - - # Jordan噪声的关键:增益根据前一层的值调整 - gain = 0.9 * (1.0 - abs(noise_value)) - - # 更新频率和振幅 - frequency *= lacunarity - amplitude *= persistence - - return max(-1.0, min(1.0, total)) - - except Exception as e: - print(f"✗ Jordan噪声生成失败: {e}") - return 0.0 - - def generate_ridged_multifractal_noise(self, x: float, y: float, seed: int = None, - octaves: int = None, persistence: float = None, - lacunarity: float = None, scale: float = None, - offset: float = 1.0, gain: float = 2.0) -> float: - """ - 生成脊状多分形噪声 - - Args: - x: X坐标 - y: Y坐标 - seed: 随机种子(可选) - octaves: 八度数(可选) - persistence: 持久性(可选) - lacunarity: 间隙性(可选) - scale: 缩放(可选) - offset: 偏移值 - gain: 增益值 - - Returns: - 脊状多分形噪声值 (0.0 到 1.0) - """ - try: - if not self.enabled: - return 0.0 - - # 使用指定参数或默认参数 - if seed is not None: - local_seed = seed - else: - local_seed = self.seed - - if octaves is None: - octaves = self.fractal_params['octaves'] - if persistence is None: - persistence = self.fractal_params['persistence'] - if lacunarity is None: - lacunarity = self.fractal_params['lacunarity'] - if scale is None: - scale = self.fractal_params['scale'] - - # 生成脊状多分形噪声 - result = 0.0 - frequency = scale - weight = 1.0 - - for i in range(octaves): - # 生成当前层噪声 - noise_value = self.generate_noise(x * frequency, y * frequency, local_seed + i) - # 转换为脊状 - noise_value = offset - abs(noise_value) - noise_value *= noise_value # 平方以增强脊状效果 - - # 应用权重 - result += noise_value * weight - - # 更新权重 - weight = noise_value * gain - weight = max(0.0, min(1.0, weight)) - - # 更新频率 - frequency *= lacunarity - - return min(1.0, max(0.0, result)) - - except Exception as e: - print(f"✗ 脊状多分形噪声生成失败: {e}") - return 0.0 - - def generate_gabor_noise(self, x: float, y: float, seed: int = None, - frequency: float = 1.0, bandwidth: float = 1.0, - impulse_variance: float = 1.0) -> float: - """ - 生成Gabor噪声(滤波型噪声) - - Args: - x: X坐标 - y: Y坐标 - seed: 随机种子(可选) - frequency: 频率 - bandwidth: 带宽 - impulse_variance: 脉冲方差 - - Returns: - Gabor噪声值 (-1.0 到 1.0) - """ - try: - if not self.enabled: - return 0.0 - - # 使用指定种子或默认种子 - if seed is not None: - local_seed = seed - else: - local_seed = self.seed - - # 简化的Gabor噪声实现 - # 实际实现会更复杂,这里提供一个近似版本 - - # 生成随机相位和方向 - phase_seed = int(x * 1000 + y * 1000 + local_seed) & 0x7fffffff - self.random_generator.seed(phase_seed) - phase = self.random_generator.uniform(0, 2 * math.pi) - orientation = self.random_generator.uniform(0, 2 * math.pi) - - # 计算方向向量 - cos_orient = math.cos(orientation) - sin_orient = math.sin(orientation) - - # 投影到方向向量上 - projected_x = x * cos_orient + y * sin_orient - projected_y = -x * sin_orient + y * cos_orient - - # 生成Gabor函数响应 - envelope = math.exp(-(projected_x**2 + projected_y**2) / (2 * impulse_variance)) - wave = math.cos(2 * math.pi * frequency * projected_x + phase) - - return envelope * wave - - except Exception as e: - print(f"✗ Gabor噪声生成失败: {e}") +""" +噪声生成器 +负责生成各种类型的程序化噪声 +""" + +import numpy as np +import math +from typing import Dict, Any + +class NoiseGenerator: + """ + 噪声生成器 + 负责生成各种类型的程序化噪声,包括Perlin噪声、Simplex噪声、分形噪声等 + """ + + def __init__(self, plugin): + """ + 初始化噪声生成器 + + Args: + plugin: 程序化地形生成插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 噪声配置 + self.seed = plugin.config.get('seed', 12345) + + # 噪声类型参数 + self.noise_types = { + 'perlin': {'enabled': True, 'weight': 1.0}, + 'simplex': {'enabled': True, 'weight': 1.0}, + 'value': {'enabled': True, 'weight': 0.5}, + 'worley': {'enabled': True, 'weight': 0.3}, + 'white': {'enabled': True, 'weight': 0.2} + } + + # 分形噪声参数 + self.fractal_params = { + 'octaves': 6, + 'persistence': 0.5, + 'lacunarity': 2.0, + 'scale': 1.0 + } + + # 随机数生成器 + self.random_generator = None + + # 预计算的梯度向量表 + self.gradient_table = [] + + # 统计信息 + self.stats = { + 'noise_generated': 0, + 'total_generation_time': 0.0, + 'average_generation_time': 0.0 + } + + print("✓ 噪声生成器已创建") + + def initialize(self) -> bool: + """ + 初始化噪声生成器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化随机数生成器 + self.random_generator = np.random.RandomState(self.seed) + + # 初始化梯度表 + self._initialize_gradient_table() + + self.initialized = True + print("✓ 噪声生成器初始化完成") + return True + + except Exception as e: + print(f"✗ 噪声生成器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用噪声生成器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 噪声生成器未初始化") + return False + + self.enabled = True + print("✓ 噪声生成器已启用") + return True + + except Exception as e: + print(f"✗ 噪声生成器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用噪声生成器""" + try: + self.enabled = False + print("✓ 噪声生成器已禁用") + + except Exception as e: + print(f"✗ 噪声生成器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理噪声生成器资源""" + try: + self.disable() + self.initialized = False + print("✓ 噪声生成器资源已清理") + + except Exception as e: + print(f"✗ 噪声生成器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新噪声生成器状态 + + Args: + dt: 时间增量 + """ + # 处理更新逻辑 + pass + + def _initialize_gradient_table(self): + """初始化梯度向量表""" + try: + # 创建2D梯度向量表 (Perlin噪声使用) + self.gradient_table = [] + for i in range(256): + # 生成随机角度 + angle = self.random_generator.uniform(0, 2 * math.pi) + # 计算梯度向量 + gradient = (math.cos(angle), math.sin(angle)) + self.gradient_table.append(gradient) + + # 复制表以避免边界检查 + self.gradient_table = self.gradient_table + self.gradient_table + + except Exception as e: + print(f"✗ 梯度表初始化失败: {e}") + # 创建默认梯度表 + self.gradient_table = [(1, 0), (0, 1), (-1, 0), (0, -1)] * 64 + + def set_seed(self, seed: int): + """ + 设置随机种子 + + Args: + seed: 随机种子 + """ + self.seed = seed + if self.random_generator is not None: + self.random_generator.seed(seed) + # 重新初始化梯度表 + self._initialize_gradient_table() + print(f"✓ 噪声生成器随机种子设置为: {seed}") + + def generate_noise(self, x: float, y: float, seed: int = None) -> float: + """ + 生成噪声值 + + Args: + x: X坐标 + y: Y坐标 + seed: 随机种子(可选) + + Returns: + 噪声值 (-1.0 到 1.0) + """ + try: + if not self.enabled: + return 0.0 + + # 使用指定种子或默认种子 + if seed is not None: + local_seed = seed + else: + local_seed = self.seed + + # 生成Perlin噪声(默认) + noise_value = self._perlin_noise(x, y, local_seed) + + # 更新统计信息 + self.stats['noise_generated'] += 1 + + return noise_value + + except Exception as e: + print(f"✗ 噪声生成失败: {e}") + return 0.0 + + def _perlin_noise(self, x: float, y: float, seed: int) -> float: + """ + 生成Perlin噪声 + + Args: + x: X坐标 + y: Y坐标 + seed: 随机种子 + + Returns: + Perlin噪声值 (-1.0 到 1.0) + """ + try: + # 确保使用正确的种子 + if seed != self.seed and self.random_generator is not None: + self.random_generator.seed(seed) + + # 找到包含点的单元格 + x0 = int(math.floor(x)) & 255 + y0 = int(math.floor(y)) & 255 + x1 = (x0 + 1) & 255 + y1 = (y0 + 1) & 255 + + # 计算单元格内的坐标 + xf = x - math.floor(x) + yf = y - math.floor(y) + + # 计算平滑系数 + u = self._fade(xf) + v = self._fade(yf) + + # 计算梯度点积 + n00 = self._gradient(self.gradient_table[x0 + self.gradient_table[y0][0]], xf, yf) + n01 = self._gradient(self.gradient_table[x0 + self.gradient_table[y1][0]], xf, yf - 1) + n10 = self._gradient(self.gradient_table[x1 + self.gradient_table[y0][0]], xf - 1, yf) + n11 = self._gradient(self.gradient_table[x1 + self.gradient_table[y1][0]], xf - 1, yf - 1) + + # 双线性插值 + x1_result = self._lerp(n00, n10, u) + x2_result = self._lerp(n01, n11, u) + result = self._lerp(x1_result, x2_result, v) + + return result + + except Exception as e: + # 出错时返回简单噪声 + return self._simple_noise(x, y, seed) + + def _fade(self, t: float) -> float: + """ + Fade函数 (6*t^5 - 15*t^4 + 10*t^3) + + Args: + t: 输入值 + + Returns: + 平滑后的值 + """ + return t * t * t * (t * (t * 6 - 15) + 10) + + def _lerp(self, a: float, b: float, t: float) -> float: + """ + 线性插值 + + Args: + a: 起始值 + b: 结束值 + t: 插值参数 (0-1) + + Returns: + 插值结果 + """ + return a + t * (b - a) + + def _gradient(self, hash_val: tuple, x: float, y: float) -> float: + """ + 计算梯度点积 + + Args: + hash_val: 梯度向量 + x: X偏移 + y: Y偏移 + + Returns: + 点积结果 + """ + return hash_val[0] * x + hash_val[1] * y + + def _simple_noise(self, x: float, y: float, seed: int) -> float: + """ + 简单噪声生成(备用方案) + + Args: + x: X坐标 + y: Y坐标 + seed: 随机种子 + + Returns: + 噪声值 + """ + # 使用正弦波生成伪随机噪声 + value = math.sin(x * 12.9898 + y * 78.233 + seed) * 43758.5453 + return (value - math.floor(value)) * 2.0 - 1.0 + + def generate_simplex_noise(self, x: float, y: float, seed: int = None) -> float: + """ + 生成Simplex噪声 + + Args: + x: X坐标 + y: Y坐标 + seed: 随机种子(可选) + + Returns: + Simplex噪声值 (-1.0 到 1.0) + """ + try: + if not self.enabled: + return 0.0 + + # 使用指定种子或默认种子 + if seed is not None: + local_seed = seed + else: + local_seed = self.seed + + # Simplex噪声实现 + # 这里使用简化版本 + value = self._simple_noise(x, y, local_seed) + return value + + except Exception as e: + print(f"✗ Simplex噪声生成失败: {e}") + return 0.0 + + def generate_value_noise(self, x: float, y: float, seed: int = None) -> float: + """ + 生成值噪声 + + Args: + x: X坐标 + y: Y坐标 + seed: 随机种子(可选) + + Returns: + 值噪声值 (-1.0 到 1.0) + """ + try: + if not self.enabled: + return 0.0 + + # 使用指定种子或默认种子 + if seed is not None: + local_seed = seed + else: + local_seed = self.seed + + # 值噪声实现 + # 获取整数坐标 + x_int = int(math.floor(x)) + y_int = int(math.floor(y)) + + # 获取小数部分 + x_frac = x - x_int + y_frac = y - y_int + + # 生成四个角的随机值 + corners = [] + for dy in [0, 1]: + for dx in [0, 1]: + corner_seed = ((x_int + dx) * 374761393 + (y_int + dy) * 668265263 + local_seed) & 0x7fffffff + self.random_generator.seed(corner_seed) + corners.append(self.random_generator.uniform(-1.0, 1.0)) + + # 双线性插值 + u = self._fade(x_frac) + v = self._fade(y_frac) + + top = self._lerp(corners[0], corners[1], u) + bottom = self._lerp(corners[2], corners[3], u) + result = self._lerp(top, bottom, v) + + return result + + except Exception as e: + print(f"✗ 值噪声生成失败: {e}") + return 0.0 + + def generate_worley_noise(self, x: float, y: float, seed: int = None) -> float: + """ + 生成Worley噪声(细胞噪声) + + Args: + x: X坐标 + y: Y坐标 + seed: 随机种子(可选) + + Returns: + Worley噪声值 (0.0 到 1.0) + """ + try: + if not self.enabled: + return 0.0 + + # 使用指定种子或默认种子 + if seed is not None: + local_seed = seed + else: + local_seed = self.seed + + # 找到点所在的网格单元格 + cell_x = int(math.floor(x)) + cell_y = int(math.floor(y)) + + # 初始化最小距离 + min_distance = float('inf') + + # 检查周围的9个单元格 + for dy in [-1, 0, 1]: + for dx in [-1, 0, 1]: + # 计算相邻单元格坐标 + nx = cell_x + dx + ny = cell_y + dy + + # 生成单元格中的特征点 + feature_seed = (nx * 374761393 + ny * 668265263 + local_seed) & 0x7fffffff + self.random_generator.seed(feature_seed) + fx = nx + self.random_generator.uniform(0.0, 1.0) + fy = ny + self.random_generator.uniform(0.0, 1.0) + + # 计算到特征点的距离 + distance = math.sqrt((x - fx)**2 + (y - fy)**2) + min_distance = min(min_distance, distance) + + # 将距离转换为噪声值 (通常限制在0-1范围内) + return min(1.0, min_distance) + + except Exception as e: + print(f"✗ Worley噪声生成失败: {e}") + return 0.0 + + def generate_white_noise(self, x: float, y: float, seed: int = None) -> float: + """ + 生成白噪声 + + Args: + x: X坐标 + y: Y坐标 + seed: 随机种子(可选) + + Returns: + 白噪声值 (-1.0 到 1.0) + """ + try: + if not self.enabled: + return 0.0 + + # 使用指定种子或默认种子 + if seed is not None: + local_seed = seed + else: + local_seed = self.seed + + # 生成伪随机值 + noise_seed = (int(x * 1000) * 374761393 + int(y * 1000) * 668265263 + local_seed) & 0x7fffffff + self.random_generator.seed(noise_seed) + return self.random_generator.uniform(-1.0, 1.0) + + except Exception as e: + print(f"✗ 白噪声生成失败: {e}") + return 0.0 + + def generate_fractal_noise(self, x: float, y: float, seed: int = None, + octaves: int = None, persistence: float = None, + lacunarity: float = None, scale: float = None) -> float: + """ + 生成分形噪声(fBm - 分数布朗运动) + + Args: + x: X坐标 + y: Y坐标 + seed: 随机种子(可选) + octaves: 八度数(可选) + persistence: 持久性(可选) + lacunarity: 间隙性(可选) + scale: 缩放(可选) + + Returns: + 分形噪声值 (-1.0 到 1.0) + """ + try: + if not self.enabled: + return 0.0 + + # 使用指定参数或默认参数 + if seed is not None: + local_seed = seed + else: + local_seed = self.seed + + if octaves is None: + octaves = self.fractal_params['octaves'] + if persistence is None: + persistence = self.fractal_params['persistence'] + if lacunarity is None: + lacunarity = self.fractal_params['lacunarity'] + if scale is None: + scale = self.fractal_params['scale'] + + # 生成分形噪声 + total = 0.0 + frequency = scale + amplitude = 1.0 + max_value = 0.0 # 用于归一化 + + for i in range(octaves): + # 生成当前层噪声 + noise_value = self.generate_noise(x * frequency, y * frequency, local_seed + i) + total += noise_value * amplitude + + # 更新最大值 + max_value += amplitude + + # 更新频率和振幅 + frequency *= lacunarity + amplitude *= persistence + + # 归一化结果 + if max_value > 0: + return total / max_value + else: + return total + + except Exception as e: + print(f"✗ 分形噪声生成失败: {e}") + return 0.0 + + def generate_rigid_noise(self, x: float, y: float, seed: int = None, + octaves: int = None, persistence: float = None, + lacunarity: float = None, scale: float = None) -> float: + """ + 生成刚性噪声(用于生成山脉等地质特征) + + Args: + x: X坐标 + y: Y坐标 + seed: 随机种子(可选) + octaves: 八度数(可选) + persistence: 持久性(可选) + lacunarity: 间隙性(可选) + scale: 缩放(可选) + + Returns: + 刚性噪声值 (0.0 到 1.0) + """ + try: + if not self.enabled: + return 0.0 + + # 使用指定参数或默认参数 + if seed is not None: + local_seed = seed + else: + local_seed = self.seed + + if octaves is None: + octaves = self.fractal_params['octaves'] + if persistence is None: + persistence = self.fractal_params['persistence'] + if lacunarity is None: + lacunarity = self.fractal_params['lacunarity'] + if scale is None: + scale = self.fractal_params['scale'] + + # 生成刚性噪声 + total = 0.0 + frequency = scale + amplitude = 1.0 + + for i in range(octaves): + # 生成当前层噪声并转换为刚性形式 + noise_value = abs(self.generate_noise(x * frequency, y * frequency, local_seed + i)) + total += (1.0 - noise_value) * amplitude + + # 更新频率和振幅 + frequency *= lacunarity + amplitude *= persistence + + return min(1.0, total) + + except Exception as e: + print(f"✗ 刚性噪声生成失败: {e}") + return 0.0 + + def generate_turbulence_noise(self, x: float, y: float, seed: int = None, + octaves: int = None, persistence: float = None, + lacunarity: float = None, scale: float = None) -> float: + """ + 生成湍流噪声 + + Args: + x: X坐标 + y: Y坐标 + seed: 随机种子(可选) + octaves: 八度数(可选) + persistence: 持久性(可选) + lacunarity: 间隙性(可选) + scale: 缩放(可选) + + Returns: + 湍流噪声值 (0.0 到 1.0) + """ + try: + if not self.enabled: + return 0.0 + + # 使用指定参数或默认参数 + if seed is not None: + local_seed = seed + else: + local_seed = self.seed + + if octaves is None: + octaves = self.fractal_params['octaves'] + if persistence is None: + persistence = self.fractal_params['persistence'] + if lacunarity is None: + lacunarity = self.fractal_params['lacunarity'] + if scale is None: + scale = self.fractal_params['scale'] + + # 生成湍流噪声 + total = 0.0 + frequency = scale + amplitude = 1.0 + + for i in range(octaves): + # 生成当前层噪声并取绝对值 + noise_value = abs(self.generate_noise(x * frequency, y * frequency, local_seed + i)) + total += noise_value * amplitude + + # 更新频率和振幅 + frequency *= lacunarity + amplitude *= persistence + + return min(1.0, total) + + except Exception as e: + print(f"✗ 湍流噪声生成失败: {e}") + return 0.0 + + def generate_combined_noise(self, x: float, y: float, seed: int = None) -> float: + """ + 生成组合噪声(混合多种噪声类型) + + Args: + x: X坐标 + y: Y坐标 + seed: 随机种子(可选) + + Returns: + 组合噪声值 (-1.0 到 1.0) + """ + try: + if not self.enabled: + return 0.0 + + # 使用指定种子或默认种子 + if seed is not None: + local_seed = seed + else: + local_seed = self.seed + + total = 0.0 + total_weight = 0.0 + + # 生成Perlin噪声 + if self.noise_types['perlin']['enabled']: + weight = self.noise_types['perlin']['weight'] + noise_value = self.generate_noise(x, y, local_seed) + total += noise_value * weight + total_weight += weight + + # 生成Simplex噪声 + if self.noise_types['simplex']['enabled']: + weight = self.noise_types['simplex']['weight'] + noise_value = self.generate_simplex_noise(x, y, local_seed + 1000) + total += noise_value * weight + total_weight += weight + + # 生成值噪声 + if self.noise_types['value']['enabled']: + weight = self.noise_types['value']['weight'] + noise_value = self.generate_value_noise(x, y, local_seed + 2000) + total += noise_value * weight + total_weight += weight + + # 生成Worley噪声 + if self.noise_types['worley']['enabled']: + weight = self.noise_types['worley']['weight'] + noise_value = self.generate_worley_noise(x, y, local_seed + 3000) + # Worley噪声范围是0-1,需要转换为-1到1 + noise_value = noise_value * 2.0 - 1.0 + total += noise_value * weight + total_weight += weight + + # 生成白噪声 + if self.noise_types['white']['enabled']: + weight = self.noise_types['white']['weight'] + noise_value = self.generate_white_noise(x, y, local_seed + 4000) + total += noise_value * weight + total_weight += weight + + # 归一化结果 + if total_weight > 0: + return total / total_weight + else: + return 0.0 + + except Exception as e: + print(f"✗ 组合噪声生成失败: {e}") + return 0.0 + + def set_noise_type_enabled(self, noise_type: str, enabled: bool): + """ + 设置噪声类型是否启用 + + Args: + noise_type: 噪声类型 + enabled: 是否启用 + """ + if noise_type in self.noise_types: + self.noise_types[noise_type]['enabled'] = enabled + print(f"✓ 噪声类型 '{noise_type}' 已{'启用' if enabled else '禁用'}") + else: + print(f"✗ 无效的噪声类型: {noise_type}") + + def set_noise_type_weight(self, noise_type: str, weight: float): + """ + 设置噪声类型权重 + + Args: + noise_type: 噪声类型 + weight: 权重值 + """ + if noise_type in self.noise_types: + self.noise_types[noise_type]['weight'] = max(0.0, weight) + print(f"✓ 噪声类型 '{noise_type}' 权重设置为: {weight}") + else: + print(f"✗ 无效的噪声类型: {noise_type}") + + def set_fractal_parameters(self, octaves: int = None, persistence: float = None, + lacunarity: float = None, scale: float = None): + """ + 设置分形噪声参数 + + Args: + octaves: 八度数 + persistence: 持久性 + lacunarity: 间隙性 + scale: 缩放 + """ + if octaves is not None: + self.fractal_params['octaves'] = max(1, octaves) + if persistence is not None: + self.fractal_params['persistence'] = max(0.0, min(1.0, persistence)) + if lacunarity is not None: + self.fractal_params['lacunarity'] = max(1.0, lacunarity) + if scale is not None: + self.fractal_params['scale'] = max(0.01, scale) + + print(f"✓ 分形噪声参数已更新: {self.fractal_params}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + # 更新平均生成时间 + if self.stats['noise_generated'] > 0: + self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['noise_generated'] + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'noise_generated': 0, + 'total_generation_time': 0.0, + 'average_generation_time': 0.0 + } + print("✓ 噪声生成器统计信息已重置") + + def generate_anisotropic_noise(self, x: float, y: float, direction: float, + anisotropy: float, seed: int = None) -> float: + """ + 生成各向异性噪声 + + Args: + x: X坐标 + y: Y坐标 + direction: 主要方向(弧度) + anisotropy: 各向异性程度(0-1) + seed: 随机种子(可选) + + Returns: + 各向异性噪声值 (-1.0 到 1.0) + """ + try: + if not self.enabled: + return 0.0 + + # 使用指定种子或默认种子 + if seed is not None: + local_seed = seed + else: + local_seed = self.seed + + # 计算各向异性变换 + cos_dir = math.cos(direction) + sin_dir = math.sin(direction) + + # 应用各向异性变换 + x_aniso = x * (cos_dir * cos_dir + sin_dir * sin_dir * (1 - anisotropy)) + y_aniso = y * (sin_dir * sin_dir + cos_dir * cos_dir * (1 - anisotropy)) + + # 生成噪声 + return self.generate_noise(x_aniso, y_aniso, local_seed) + + except Exception as e: + print(f"✗ 各向异性噪声生成失败: {e}") + return 0.0 + + def generate_billow_noise(self, x: float, y: float, seed: int = None, + octaves: int = None, persistence: float = None, + lacunarity: float = None, scale: float = None) -> float: + """ + 生成云朵状噪声(Billow噪声) + + Args: + x: X坐标 + y: Y坐标 + seed: 随机种子(可选) + octaves: 八度数(可选) + persistence: 持久性(可选) + lacunarity: 间隙性(可选) + scale: 缩放(可选) + + Returns: + Billow噪声值 (0.0 到 1.0) + """ + try: + if not self.enabled: + return 0.0 + + # 使用指定参数或默认参数 + if seed is not None: + local_seed = seed + else: + local_seed = self.seed + + if octaves is None: + octaves = self.fractal_params['octaves'] + if persistence is None: + persistence = self.fractal_params['persistence'] + if lacunarity is None: + lacunarity = self.fractal_params['lacunarity'] + if scale is None: + scale = self.fractal_params['scale'] + + # 生成Billow噪声 + total = 0.0 + frequency = scale + amplitude = 1.0 + + for i in range(octaves): + # 生成当前层噪声并转换为Billow形式 + noise_value = abs(self.generate_noise(x * frequency, y * frequency, local_seed + i)) * 2.0 - 1.0 + noise_value = noise_value * noise_value # 平方以增强对比度 + total += noise_value * amplitude + + # 更新频率和振幅 + frequency *= lacunarity + amplitude *= persistence + + # 转换到0-1范围 + return min(1.0, max(0.0, total * 0.5 + 0.5)) + + except Exception as e: + print(f"✗ Billow噪声生成失败: {e}") + return 0.0 + + def generate_voronoi_noise(self, x: float, y: float, seed: int = None, + distance_metric: str = 'euclidean') -> float: + """ + 生成Voronoi噪声(另一种细胞噪声) + + Args: + x: X坐标 + y: Y坐标 + seed: 随机种子(可选) + distance_metric: 距离度量方式 ('euclidean', 'manhattan', 'chebyshev') + + Returns: + Voronoi噪声值 (0.0 到 1.0) + """ + try: + if not self.enabled: + return 0.0 + + # 使用指定种子或默认种子 + if seed is not None: + local_seed = seed + else: + local_seed = self.seed + + # 找到点所在的网格单元格 + cell_x = int(math.floor(x)) + cell_y = int(math.floor(y)) + + # 初始化最近和次近距离 + closest_distance = float('inf') + second_closest_distance = float('inf') + + # 检查周围的9个单元格 + for dy in [-1, 0, 1]: + for dx in [-1, 0, 1]: + # 计算相邻单元格坐标 + nx = cell_x + dx + ny = cell_y + dy + + # 生成单元格中的特征点 + feature_seed = (nx * 374761393 + ny * 668265263 + local_seed) & 0x7fffffff + self.random_generator.seed(feature_seed) + fx = nx + self.random_generator.uniform(0.0, 1.0) + fy = ny + self.random_generator.uniform(0.0, 1.0) + + # 计算到特征点的距离 + if distance_metric == 'euclidean': + distance = math.sqrt((x - fx)**2 + (y - fy)**2) + elif distance_metric == 'manhattan': + distance = abs(x - fx) + abs(y - fy) + elif distance_metric == 'chebyshev': + distance = max(abs(x - fx), abs(y - fy)) + else: + distance = math.sqrt((x - fx)**2 + (y - fy)**2) + + # 更新最近和次近距离 + if distance < closest_distance: + second_closest_distance = closest_distance + closest_distance = distance + elif distance < second_closest_distance: + second_closest_distance = distance + + # 返回次近和最近距离的差值 + difference = second_closest_distance - closest_distance + return max(0.0, min(1.0, difference)) + + except Exception as e: + print(f"✗ Voronoi噪声生成失败: {e}") + return 0.0 + + def generate_cylindrical_noise(self, x: float, y: float, z: float, seed: int = None) -> float: + """ + 生成圆柱形噪声(3D噪声投影到2D) + + Args: + x: X坐标 + y: Y坐标 + z: Z坐标 + seed: 随机种子(可选) + + Returns: + 圆柱形噪声值 (-1.0 到 1.0) + """ + try: + if not self.enabled: + return 0.0 + + # 使用指定种子或默认种子 + if seed is not None: + local_seed = seed + else: + local_seed = self.seed + + # 将2D坐标转换为圆柱坐标 + angle = math.atan2(y, x) + radius = math.sqrt(x*x + y*y) + + # 生成3D噪声 + return self.generate_noise(angle, radius, local_seed + int(z * 1000)) + + except Exception as e: + print(f"✗ 圆柱形噪声生成失败: {e}") + return 0.0 + + def generate_spherical_noise(self, x: float, y: float, z: float, seed: int = None) -> float: + """ + 生成球形噪声(3D噪声投影到2D) + + Args: + x: X坐标 + y: Y坐标 + z: Z坐标 + seed: 随机种子(可选) + + Returns: + 球形噪声值 (-1.0 到 1.0) + """ + try: + if not self.enabled: + return 0.0 + + # 使用指定种子或默认种子 + if seed is not None: + local_seed = seed + else: + local_seed = self.seed + + # 将笛卡尔坐标转换为球坐标 + radius = math.sqrt(x*x + y*y + z*z) + if radius == 0: + return 0.0 + + theta = math.acos(z / radius) # 极角 + phi = math.atan2(y, x) # 方位角 + + # 生成噪声 + return self.generate_noise(theta, phi, local_seed + int(radius * 1000)) + + except Exception as e: + print(f"✗ 球形噪声生成失败: {e}") + return 0.0 + + def generate_domain_warping_noise(self, x: float, y: float, seed: int = None, + warp_strength: float = 1.0) -> float: + """ + 生成域变形噪声 + + Args: + x: X坐标 + y: Y坐标 + seed: 随机种子(可选) + warp_strength: 变形强度 + + Returns: + 域变形噪声值 (-1.0 到 1.0) + """ + try: + if not self.enabled: + return 0.0 + + # 使用指定种子或默认种子 + if seed is not None: + local_seed = seed + else: + local_seed = self.seed + + # 生成变形向量 + warp_x = self.generate_noise(x, y, local_seed + 10000) * warp_strength + warp_y = self.generate_noise(x, y, local_seed + 20000) * warp_strength + + # 应用变形并生成噪声 + return self.generate_noise(x + warp_x, y + warp_y, local_seed) + + except Exception as e: + print(f"✗ 域变形噪声生成失败: {e}") + return 0.0 + + def generate_swiss_noise(self, x: float, y: float, seed: int = None, + octaves: int = None, persistence: float = None, + lacunarity: float = None, scale: float = None) -> float: + """ + 生成Swiss噪声(用于生成山脉等地质特征) + + Args: + x: X坐标 + y: Y坐标 + seed: 随机种子(可选) + octaves: 八度数(可选) + persistence: 持久性(可选) + lacunarity: 间隙性(可选) + scale: 缩放(可选) + + Returns: + Swiss噪声值 (0.0 到 1.0) + """ + try: + if not self.enabled: + return 0.0 + + # 使用指定参数或默认参数 + if seed is not None: + local_seed = seed + else: + local_seed = self.seed + + if octaves is None: + octaves = self.fractal_params['octaves'] + if persistence is None: + persistence = self.fractal_params['persistence'] + if lacunarity is None: + lacunarity = self.fractal_params['lacunarity'] + if scale is None: + scale = self.fractal_params['scale'] + + # 生成Swiss噪声 + total = 0.0 + frequency = scale + amplitude = 1.0 + + for i in range(octaves): + # 生成当前层噪声 + noise_value = self.generate_noise(x * frequency, y * frequency, local_seed + i) + # Swiss噪声使用噪声值的绝对值并反转 + swiss_value = 1.0 - abs(noise_value) + total += swiss_value * amplitude + + # 更新频率和振幅 + frequency *= lacunarity + amplitude *= persistence * swiss_value # Swiss噪声的关键:振幅根据值调整 + + return min(1.0, max(0.0, total)) + + except Exception as e: + print(f"✗ Swiss噪声生成失败: {e}") + return 0.0 + + def generate_jordan_noise(self, x: float, y: float, seed: int = None, + octaves: int = None, persistence: float = None, + lacunarity: float = None, scale: float = None) -> float: + """ + 生成Jordan噪声(用于生成更自然的地形) + + Args: + x: X坐标 + y: Y坐标 + seed: 随机种子(可选) + octaves: 八度数(可选) + persistence: 持久性(可选) + lacunarity: 间隙性(可选) + scale: 缩放(可选) + + Returns: + Jordan噪声值 (-1.0 到 1.0) + """ + try: + if not self.enabled: + return 0.0 + + # 使用指定参数或默认参数 + if seed is not None: + local_seed = seed + else: + local_seed = self.seed + + if octaves is None: + octaves = self.fractal_params['octaves'] + if persistence is None: + persistence = self.fractal_params['persistence'] + if lacunarity is None: + lacunarity = self.fractal_params['lacunarity'] + if scale is None: + scale = self.fractal_params['scale'] + + # 生成Jordan噪声 + total = 0.0 + frequency = scale + amplitude = 1.0 + gain = 1.0 + + for i in range(octaves): + # 生成当前层噪声 + noise_value = self.generate_noise(x * frequency, y * frequency, local_seed + i) + total += noise_value * amplitude * gain + + # Jordan噪声的关键:增益根据前一层的值调整 + gain = 0.9 * (1.0 - abs(noise_value)) + + # 更新频率和振幅 + frequency *= lacunarity + amplitude *= persistence + + return max(-1.0, min(1.0, total)) + + except Exception as e: + print(f"✗ Jordan噪声生成失败: {e}") + return 0.0 + + def generate_ridged_multifractal_noise(self, x: float, y: float, seed: int = None, + octaves: int = None, persistence: float = None, + lacunarity: float = None, scale: float = None, + offset: float = 1.0, gain: float = 2.0) -> float: + """ + 生成脊状多分形噪声 + + Args: + x: X坐标 + y: Y坐标 + seed: 随机种子(可选) + octaves: 八度数(可选) + persistence: 持久性(可选) + lacunarity: 间隙性(可选) + scale: 缩放(可选) + offset: 偏移值 + gain: 增益值 + + Returns: + 脊状多分形噪声值 (0.0 到 1.0) + """ + try: + if not self.enabled: + return 0.0 + + # 使用指定参数或默认参数 + if seed is not None: + local_seed = seed + else: + local_seed = self.seed + + if octaves is None: + octaves = self.fractal_params['octaves'] + if persistence is None: + persistence = self.fractal_params['persistence'] + if lacunarity is None: + lacunarity = self.fractal_params['lacunarity'] + if scale is None: + scale = self.fractal_params['scale'] + + # 生成脊状多分形噪声 + result = 0.0 + frequency = scale + weight = 1.0 + + for i in range(octaves): + # 生成当前层噪声 + noise_value = self.generate_noise(x * frequency, y * frequency, local_seed + i) + # 转换为脊状 + noise_value = offset - abs(noise_value) + noise_value *= noise_value # 平方以增强脊状效果 + + # 应用权重 + result += noise_value * weight + + # 更新权重 + weight = noise_value * gain + weight = max(0.0, min(1.0, weight)) + + # 更新频率 + frequency *= lacunarity + + return min(1.0, max(0.0, result)) + + except Exception as e: + print(f"✗ 脊状多分形噪声生成失败: {e}") + return 0.0 + + def generate_gabor_noise(self, x: float, y: float, seed: int = None, + frequency: float = 1.0, bandwidth: float = 1.0, + impulse_variance: float = 1.0) -> float: + """ + 生成Gabor噪声(滤波型噪声) + + Args: + x: X坐标 + y: Y坐标 + seed: 随机种子(可选) + frequency: 频率 + bandwidth: 带宽 + impulse_variance: 脉冲方差 + + Returns: + Gabor噪声值 (-1.0 到 1.0) + """ + try: + if not self.enabled: + return 0.0 + + # 使用指定种子或默认种子 + if seed is not None: + local_seed = seed + else: + local_seed = self.seed + + # 简化的Gabor噪声实现 + # 实际实现会更复杂,这里提供一个近似版本 + + # 生成随机相位和方向 + phase_seed = int(x * 1000 + y * 1000 + local_seed) & 0x7fffffff + self.random_generator.seed(phase_seed) + phase = self.random_generator.uniform(0, 2 * math.pi) + orientation = self.random_generator.uniform(0, 2 * math.pi) + + # 计算方向向量 + cos_orient = math.cos(orientation) + sin_orient = math.sin(orientation) + + # 投影到方向向量上 + projected_x = x * cos_orient + y * sin_orient + projected_y = -x * sin_orient + y * cos_orient + + # 生成Gabor函数响应 + envelope = math.exp(-(projected_x**2 + projected_y**2) / (2 * impulse_variance)) + wave = math.cos(2 * math.pi * frequency * projected_x + phase) + + return envelope * wave + + except Exception as e: + print(f"✗ Gabor噪声生成失败: {e}") return 0.0 \ No newline at end of file diff --git a/plugins/user/procedural_terrain_generation/plugin.py b/plugins/user/procedural_terrain_generation/plugin.py index 05bf107e..9f04d416 100644 --- a/plugins/user/procedural_terrain_generation/plugin.py +++ b/plugins/user/procedural_terrain_generation/plugin.py @@ -1,395 +1,395 @@ -""" -程序化地形生成插件 -为EG引擎提供完整的程序化地形生成和编辑功能 -""" - -import sys -import os - -# 添加项目根目录到Python路径 -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -from core.terrain_manager import TerrainManager -from generators.heightmap_generator import HeightmapGenerator -from noise.noise_generator import NoiseGenerator -from biomes.biome_generator import BiomeGenerator -from erosion.erosion_processor import ErosionProcessor -from vegetation.vegetation_generator import VegetationGenerator -from rendering.terrain_renderer import TerrainRenderer -from utils.terrain_utils import TerrainUtils -from serialization.terrain_serializer import TerrainSerializer - -class ProceduralTerrainGenerationPlugin: - """ - 程序化地形生成插件主类 - 整合所有地形生成和编辑功能模块 - """ - - def __init__(self, engine): - """ - 初始化程序化地形生成插件 - - Args: - engine: EG引擎实例 - """ - self.engine = engine - self.name = "ProceduralTerrainGeneration" - self.version = "1.0.0" - self.author = "EG Plugin System" - - # 功能模块 - self.terrain_manager = None - self.heightmap_generator = None - self.noise_generator = None - self.biome_generator = None - self.erosion_processor = None - self.vegetation_generator = None - self.terrain_renderer = None - self.terrain_utils = None - self.terrain_serializer = None - - # 新增的编辑器模块 - self.terrain_editor = None - self.preset_manager = None - - # 插件状态 - self.enabled = False - self.initialized = False - - # 性能统计 - self.stats = { - 'terrains_generated': 0, - 'terrains_rendered': 0, - 'generation_time': 0.0, - 'rendering_time': 0.0 - } - - print(f"✓ 程序化地形生成插件 v{self.version} 已创建") - - def initialize(self) -> bool: - """ - 初始化插件 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化程序化地形生成插件...") - - # 初始化核心组件 - self.terrain_manager = TerrainManager(self) - self.heightmap_generator = HeightmapGenerator(self) - self.noise_generator = NoiseGenerator(self) - self.biome_generator = BiomeGenerator(self) - self.erosion_processor = ErosionProcessor(self) - self.vegetation_generator = VegetationGenerator(self) - self.terrain_renderer = TerrainRenderer(self) - self.terrain_utils = TerrainUtils(self) - self.terrain_serializer = TerrainSerializer(self) - - # 初始化新增的编辑器组件 - from .editor.terrain_editor import TerrainEditor - from .editor.preset_manager import PresetManager - - self.terrain_editor = TerrainEditor(self) - self.preset_manager = PresetManager(self) - - # 初始化各个模块 - modules = [ - self.terrain_manager, - self.heightmap_generator, - self.noise_generator, - self.biome_generator, - self.erosion_processor, - self.vegetation_generator, - self.terrain_renderer, - self.terrain_utils, - self.terrain_serializer, - self.terrain_editor, - self.preset_manager - ] - - for module in modules: - if module and not module.initialize(): - print(f"✗ 模块初始化失败: {module.__class__.__name__}") - return False - - self.initialized = True - print("✓ 程序化地形生成插件初始化完成") - return True - - except Exception as e: - print(f"✗ 程序化地形生成插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 插件未初始化") - return False - - print("正在启用程序化地形生成插件...") - - # 启用各个模块 - modules = [ - self.terrain_manager, - self.heightmap_generator, - self.noise_generator, - self.biome_generator, - self.erosion_processor, - self.vegetation_generator, - self.terrain_renderer, - self.terrain_utils, - self.terrain_serializer, - self.terrain_editor, - self.preset_manager - ] - - for module in modules: - if module and not module.enable(): - print(f"✗ 模块启用失败: {module.__class__.__name__}") - return False - - self.enabled = True - print("✓ 程序化地形生成插件已启用") - return True - - except Exception as e: - print(f"✗ 程序化地形生成插件启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用插件""" - try: - if not self.enabled: - return - - print("正在禁用程序化地形生成插件...") - - # 禁用各个模块 - modules = [ - self.terrain_manager, - self.heightmap_generator, - self.noise_generator, - self.biome_generator, - self.erosion_processor, - self.vegetation_generator, - self.terrain_renderer, - self.terrain_utils, - self.terrain_serializer, - self.terrain_editor, - self.preset_manager - ] - - for module in modules: - if module: - module.disable() - - self.enabled = False - print("✓ 程序化地形生成插件已禁用") - - except Exception as e: - print(f"✗ 程序化地形生成插件禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理插件资源""" - try: - print("正在清理程序化地形生成插件资源...") - - # 清理各个模块 - modules = [ - self.terrain_manager, - self.heightmap_generator, - self.noise_generator, - self.biome_generator, - self.erosion_processor, - self.vegetation_generator, - self.terrain_renderer, - self.terrain_utils, - self.terrain_serializer, - self.terrain_editor, - self.preset_manager - ] - - for module in modules: - if module: - module.finalize() - - self.initialized = False - print("✓ 程序化地形生成插件资源已清理") - - except Exception as e: - print(f"✗ 程序化地形生成插件资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新插件状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - # 更新各个模块 - modules = [ - self.terrain_manager, - self.heightmap_generator, - self.noise_generator, - self.biome_generator, - self.erosion_processor, - self.vegetation_generator, - self.terrain_renderer, - self.terrain_utils, - self.terrain_serializer, - self.terrain_editor, - self.preset_manager - ] - - for module in modules: - if module: - module.update(dt) - - except Exception as e: - print(f"✗ 程序化地形生成插件更新失败: {e}") - import traceback - traceback.print_exc() - - def generate_terrain(self, width: int = None, height: int = None, seed: int = None) -> bool: - """ - 生成地形 - - Args: - width: 地形宽度 - height: 地形高度 - seed: 随机种子 - - Returns: - 是否生成成功 - """ - try: - if not self.enabled: - print("✗ 插件未启用") - return False - - if self.terrain_manager: - return self.terrain_manager.generate_terrain(width, height, seed) - return False - - except Exception as e: - print(f"✗ 地形生成失败: {e}") - import traceback - traceback.print_exc() - return False - - def set_seed(self, seed: int): - """ - 设置随机种子 - - Args: - seed: 随机种子 - """ - self.config['seed'] = seed - if self.noise_generator: - self.noise_generator.set_seed(seed) - if self.heightmap_generator: - self.heightmap_generator.set_seed(seed) - print(f"✓ 随机种子设置为: {seed}") - - def set_terrain_size(self, size: int): - """ - 设置地形大小 - - Args: - size: 地形大小 - """ - self.config['terrain_size'] = size - print(f"✓ 地形大小设置为: {size}") - - def set_height_scale(self, scale: float): - """ - 设置高度缩放 - - Args: - scale: 高度缩放因子 - """ - self.config['height_scale'] = scale - print(f"✓ 高度缩放设置为: {scale}") - - def get_terrain_data(self) -> dict: - """ - 获取地形数据 - - Returns: - 地形数据字典 - """ - if self.terrain_manager: - return self.terrain_manager.get_terrain_data() - return {} - - def save_terrain(self, filename: str) -> bool: - """ - 保存地形到文件 - - Args: - filename: 文件名 - - Returns: - 是否保存成功 - """ - try: - if self.terrain_serializer: - return self.terrain_serializer.save_terrain(filename) - return False - except Exception as e: - print(f"✗ 地形保存失败: {e}") - return False - - def load_terrain(self, filename: str) -> bool: - """ - 从文件加载地形 - - Args: - filename: 文件名 - - Returns: - 是否加载成功 - """ - try: - if self.terrain_serializer: - return self.terrain_serializer.load_terrain(filename) - return False - except Exception as e: - print(f"✗ 地形加载失败: {e}") - return False - -# 插件注册信息 -def get_plugin_info(): - """获取插件信息""" - return { - 'name': 'procedural_terrain_generation', - 'version': '1.0.0', - 'author': 'EG Team', - 'description': '程序化地形生成插件,提供高性能的程序化地形生成功能', - 'dependencies': [] - } - -# 创建插件实例的工厂函数 -def create_plugin(world): - """创建插件实例""" +""" +程序化地形生成插件 +为EG引擎提供完整的程序化地形生成和编辑功能 +""" + +import sys +import os + +# 添加项目根目录到Python路径 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from core.terrain_manager import TerrainManager +from generators.heightmap_generator import HeightmapGenerator +from noise.noise_generator import NoiseGenerator +from biomes.biome_generator import BiomeGenerator +from erosion.erosion_processor import ErosionProcessor +from vegetation.vegetation_generator import VegetationGenerator +from rendering.terrain_renderer import TerrainRenderer +from utils.terrain_utils import TerrainUtils +from serialization.terrain_serializer import TerrainSerializer + +class ProceduralTerrainGenerationPlugin: + """ + 程序化地形生成插件主类 + 整合所有地形生成和编辑功能模块 + """ + + def __init__(self, engine): + """ + 初始化程序化地形生成插件 + + Args: + engine: EG引擎实例 + """ + self.engine = engine + self.name = "ProceduralTerrainGeneration" + self.version = "1.0.0" + self.author = "EG Plugin System" + + # 功能模块 + self.terrain_manager = None + self.heightmap_generator = None + self.noise_generator = None + self.biome_generator = None + self.erosion_processor = None + self.vegetation_generator = None + self.terrain_renderer = None + self.terrain_utils = None + self.terrain_serializer = None + + # 新增的编辑器模块 + self.terrain_editor = None + self.preset_manager = None + + # 插件状态 + self.enabled = False + self.initialized = False + + # 性能统计 + self.stats = { + 'terrains_generated': 0, + 'terrains_rendered': 0, + 'generation_time': 0.0, + 'rendering_time': 0.0 + } + + print(f"✓ 程序化地形生成插件 v{self.version} 已创建") + + def initialize(self) -> bool: + """ + 初始化插件 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化程序化地形生成插件...") + + # 初始化核心组件 + self.terrain_manager = TerrainManager(self) + self.heightmap_generator = HeightmapGenerator(self) + self.noise_generator = NoiseGenerator(self) + self.biome_generator = BiomeGenerator(self) + self.erosion_processor = ErosionProcessor(self) + self.vegetation_generator = VegetationGenerator(self) + self.terrain_renderer = TerrainRenderer(self) + self.terrain_utils = TerrainUtils(self) + self.terrain_serializer = TerrainSerializer(self) + + # 初始化新增的编辑器组件 + from .editor.terrain_editor import TerrainEditor + from .editor.preset_manager import PresetManager + + self.terrain_editor = TerrainEditor(self) + self.preset_manager = PresetManager(self) + + # 初始化各个模块 + modules = [ + self.terrain_manager, + self.heightmap_generator, + self.noise_generator, + self.biome_generator, + self.erosion_processor, + self.vegetation_generator, + self.terrain_renderer, + self.terrain_utils, + self.terrain_serializer, + self.terrain_editor, + self.preset_manager + ] + + for module in modules: + if module and not module.initialize(): + print(f"✗ 模块初始化失败: {module.__class__.__name__}") + return False + + self.initialized = True + print("✓ 程序化地形生成插件初始化完成") + return True + + except Exception as e: + print(f"✗ 程序化地形生成插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 插件未初始化") + return False + + print("正在启用程序化地形生成插件...") + + # 启用各个模块 + modules = [ + self.terrain_manager, + self.heightmap_generator, + self.noise_generator, + self.biome_generator, + self.erosion_processor, + self.vegetation_generator, + self.terrain_renderer, + self.terrain_utils, + self.terrain_serializer, + self.terrain_editor, + self.preset_manager + ] + + for module in modules: + if module and not module.enable(): + print(f"✗ 模块启用失败: {module.__class__.__name__}") + return False + + self.enabled = True + print("✓ 程序化地形生成插件已启用") + return True + + except Exception as e: + print(f"✗ 程序化地形生成插件启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用插件""" + try: + if not self.enabled: + return + + print("正在禁用程序化地形生成插件...") + + # 禁用各个模块 + modules = [ + self.terrain_manager, + self.heightmap_generator, + self.noise_generator, + self.biome_generator, + self.erosion_processor, + self.vegetation_generator, + self.terrain_renderer, + self.terrain_utils, + self.terrain_serializer, + self.terrain_editor, + self.preset_manager + ] + + for module in modules: + if module: + module.disable() + + self.enabled = False + print("✓ 程序化地形生成插件已禁用") + + except Exception as e: + print(f"✗ 程序化地形生成插件禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理插件资源""" + try: + print("正在清理程序化地形生成插件资源...") + + # 清理各个模块 + modules = [ + self.terrain_manager, + self.heightmap_generator, + self.noise_generator, + self.biome_generator, + self.erosion_processor, + self.vegetation_generator, + self.terrain_renderer, + self.terrain_utils, + self.terrain_serializer, + self.terrain_editor, + self.preset_manager + ] + + for module in modules: + if module: + module.finalize() + + self.initialized = False + print("✓ 程序化地形生成插件资源已清理") + + except Exception as e: + print(f"✗ 程序化地形生成插件资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新插件状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + # 更新各个模块 + modules = [ + self.terrain_manager, + self.heightmap_generator, + self.noise_generator, + self.biome_generator, + self.erosion_processor, + self.vegetation_generator, + self.terrain_renderer, + self.terrain_utils, + self.terrain_serializer, + self.terrain_editor, + self.preset_manager + ] + + for module in modules: + if module: + module.update(dt) + + except Exception as e: + print(f"✗ 程序化地形生成插件更新失败: {e}") + import traceback + traceback.print_exc() + + def generate_terrain(self, width: int = None, height: int = None, seed: int = None) -> bool: + """ + 生成地形 + + Args: + width: 地形宽度 + height: 地形高度 + seed: 随机种子 + + Returns: + 是否生成成功 + """ + try: + if not self.enabled: + print("✗ 插件未启用") + return False + + if self.terrain_manager: + return self.terrain_manager.generate_terrain(width, height, seed) + return False + + except Exception as e: + print(f"✗ 地形生成失败: {e}") + import traceback + traceback.print_exc() + return False + + def set_seed(self, seed: int): + """ + 设置随机种子 + + Args: + seed: 随机种子 + """ + self.config['seed'] = seed + if self.noise_generator: + self.noise_generator.set_seed(seed) + if self.heightmap_generator: + self.heightmap_generator.set_seed(seed) + print(f"✓ 随机种子设置为: {seed}") + + def set_terrain_size(self, size: int): + """ + 设置地形大小 + + Args: + size: 地形大小 + """ + self.config['terrain_size'] = size + print(f"✓ 地形大小设置为: {size}") + + def set_height_scale(self, scale: float): + """ + 设置高度缩放 + + Args: + scale: 高度缩放因子 + """ + self.config['height_scale'] = scale + print(f"✓ 高度缩放设置为: {scale}") + + def get_terrain_data(self) -> dict: + """ + 获取地形数据 + + Returns: + 地形数据字典 + """ + if self.terrain_manager: + return self.terrain_manager.get_terrain_data() + return {} + + def save_terrain(self, filename: str) -> bool: + """ + 保存地形到文件 + + Args: + filename: 文件名 + + Returns: + 是否保存成功 + """ + try: + if self.terrain_serializer: + return self.terrain_serializer.save_terrain(filename) + return False + except Exception as e: + print(f"✗ 地形保存失败: {e}") + return False + + def load_terrain(self, filename: str) -> bool: + """ + 从文件加载地形 + + Args: + filename: 文件名 + + Returns: + 是否加载成功 + """ + try: + if self.terrain_serializer: + return self.terrain_serializer.load_terrain(filename) + return False + except Exception as e: + print(f"✗ 地形加载失败: {e}") + return False + +# 插件注册信息 +def get_plugin_info(): + """获取插件信息""" + return { + 'name': 'procedural_terrain_generation', + 'version': '1.0.0', + 'author': 'EG Team', + 'description': '程序化地形生成插件,提供高性能的程序化地形生成功能', + 'dependencies': [] + } + +# 创建插件实例的工厂函数 +def create_plugin(world): + """创建插件实例""" return ProceduralTerrainGenerationPlugin(world) \ No newline at end of file diff --git a/plugins/user/procedural_terrain_generation/rendering/terrain_renderer.py b/plugins/user/procedural_terrain_generation/rendering/terrain_renderer.py index 69565aea..4b5ca4ce 100644 --- a/plugins/user/procedural_terrain_generation/rendering/terrain_renderer.py +++ b/plugins/user/procedural_terrain_generation/rendering/terrain_renderer.py @@ -1,1295 +1,1295 @@ -""" -地形渲染器 -负责渲染程序化生成的地形 -""" - -import numpy as np -import math -from typing import Dict, Any, List, Tuple - -class TerrainRenderer: - """ - 地形渲染器 - 负责渲染程序化生成的地形,包括网格生成、纹理映射、光照计算等 - """ - - def __init__(self, plugin): - """ - 初始化地形渲染器 - - Args: - plugin: 程序化地形生成插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 渲染配置 - self.render_params = { - 'resolution': plugin.config.get('terrain_size', 512), - 'lod_levels': plugin.config.get('lod_levels', 5), - 'chunk_size': plugin.config.get('chunk_size', 32), - 'max_distance': 1000.0, - 'lod_distance_factor': 2.0 - } - - # 光照参数 - self.lighting_params = { - 'ambient_light': 0.3, - 'diffuse_light': 0.7, - 'specular_light': 0.2, - 'light_direction': [0.5, 0.5, 0.5], - 'light_color': [1.0, 1.0, 1.0] - } - - # 材质参数 - self.material_params = { - 'diffuse_texture': True, - 'normal_mapping': True, - 'specular_mapping': False, - 'texture_scale': 1.0, - 'tessellation_factor': 1.0 - } - - # 着色器参数 - self.shader_params = { - 'vertex_shader': 'terrain_vertex.glsl', - 'fragment_shader': 'terrain_fragment.glsl', - 'geometry_shader': None, - 'tessellation_control_shader': None, - 'tessellation_evaluation_shader': None - } - - # 渲染状态 - self.render_buffers = {} - self.vertex_arrays = {} - self.textures = {} - self.shaders = {} - - # LOD管理 - self.lod_chunks = {} - self.visible_chunks = set() - - # 统计信息 - self.stats = { - 'chunks_rendered': 0, - 'vertices_rendered': 0, - 'triangles_rendered': 0, - 'render_time': 0.0, - 'average_render_time': 0.0, - 'lod_switches': 0 - } - - print("✓ 地形渲染器已创建") - - def initialize(self) -> bool: - """ - 初始化地形渲染器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化渲染资源(在实际实现中会初始化GPU资源) - self._initialize_render_resources() - - self.initialized = True - print("✓ 地形渲染器初始化完成") - return True - - except Exception as e: - print(f"✗ 地形渲染器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用地形渲染器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 地形渲染器未初始化") - return False - - self.enabled = True - print("✓ 地形渲染器已启用") - return True - - except Exception as e: - print(f"✗ 地形渲染器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用地形渲染器""" - try: - self.enabled = False - print("✓ 地形渲染器已禁用") - - except Exception as e: - print(f"✗ 地形渲染器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理地形渲染器资源""" - try: - self.disable() - - # 清理渲染资源 - self._cleanup_render_resources() - - self.initialized = False - print("✓ 地形渲染器资源已清理") - - except Exception as e: - print(f"✗ 地形渲染器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新地形渲染器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新LOD - self._update_lod() - - # 更新可见区块 - self._update_visible_chunks() - - except Exception as e: - print(f"✗ 地形渲染器更新失败: {e}") - import traceback - traceback.print_exc() - - def _initialize_render_resources(self): - """初始化渲染资源""" - try: - # 在实际实现中,这里会初始化GPU缓冲区、纹理、着色器等 - print(" → 初始化渲染缓冲区...") - print(" → 初始化着色器...") - print(" → 初始化纹理...") - - # 模拟创建一些基本的渲染资源 - self.render_buffers['terrain_vertices'] = [] - self.render_buffers['terrain_indices'] = [] - self.vertex_arrays['terrain_vao'] = [] - self.textures['terrain_diffuse'] = [] - self.textures['terrain_normal'] = [] - self.shaders['terrain_shader'] = [] - - except Exception as e: - print(f"✗ 渲染资源初始化失败: {e}") - - def _cleanup_render_resources(self): - """清理渲染资源""" - try: - # 在实际实现中,这里会清理GPU资源 - self.render_buffers.clear() - self.vertex_arrays.clear() - self.textures.clear() - self.shaders.clear() - self.lod_chunks.clear() - self.visible_chunks.clear() - - except Exception as e: - print(f"✗ 渲染资源清理失败: {e}") - - def render_terrain(self, camera_position: List[float], view_matrix: np.ndarray, - projection_matrix: np.ndarray) -> bool: - """ - 渲染地形 - - Args: - camera_position: 摄像机位置 - view_matrix: 视图矩阵 - projection_matrix: 投影矩阵 - - Returns: - 是否渲染成功 - """ - try: - if not self.enabled: - print("✗ 地形渲染器未启用") - return False - - import time - render_start_time = time.time() - - print("✓ 开始渲染地形...") - - # 更新摄像机相关数据 - self._update_camera_data(camera_position, view_matrix, projection_matrix) - - # 准备渲染数据 - self._prepare_render_data() - - # 执行渲染 - self._execute_rendering() - - # 更新统计信息 - render_time = time.time() - render_start_time - self.stats['render_time'] = render_time - self.stats['chunks_rendered'] += len(self.visible_chunks) - - if self.stats['chunks_rendered'] > 0: - self.stats['average_render_time'] = ( - self.stats['average_render_time'] * (self.stats['chunks_rendered'] - 1) + render_time - ) / self.stats['chunks_rendered'] - - print(f"✓ 地形渲染完成,耗时: {render_time:.3f}秒") - return True - - except Exception as e: - print(f"✗ 地形渲染失败: {e}") - import traceback - traceback.print_exc() - return False - - def _update_camera_data(self, camera_position: List[float], view_matrix: np.ndarray, - projection_matrix: np.ndarray): - """更新摄像机相关数据""" - try: - # 在实际实现中,这里会更新着色器中的摄像机参数 - pass - except Exception as e: - print(f"✗ 摄像机数据更新失败: {e}") - - def _prepare_render_data(self): - """准备渲染数据""" - try: - # 在实际实现中,这里会准备顶点缓冲区、索引缓冲区等 - pass - except Exception as e: - print(f"✗ 渲染数据准备失败: {e}") - - def _execute_rendering(self): - """执行渲染""" - try: - # 在实际实现中,这里会调用GPU渲染命令 - print(" → 绑定着色器程序...") - print(" → 设置光照参数...") - print(" → 绑定纹理...") - print(" → 渲染可见区块...") - - # 模拟渲染过程 - for chunk_key in self.visible_chunks: - print(f" → 渲染区块 {chunk_key}") - - except Exception as e: - print(f"✗ 渲染执行失败: {e}") - - def _update_lod(self): - """更新LOD级别""" - try: - # 在实际实现中,这里会根据摄像机距离更新每个区块的LOD级别 - pass - except Exception as e: - print(f"✗ LOD更新失败: {e}") - - def _update_visible_chunks(self): - """更新可见区块""" - try: - # 在实际实现中,这里会根据视锥体剔除确定可见区块 - if self.plugin.terrain_manager: - terrain_data = self.plugin.terrain_manager.get_terrain_data() - if terrain_data.get('is_generating', False): - # 如果地形正在生成,暂时不更新可见区块 - return - - # 获取可见区块列表 - visible_chunk_keys = self.plugin.terrain_manager.get_visible_chunks() - self.visible_chunks = set(visible_chunk_keys) - - except Exception as e: - print(f"✗ 可见区块更新失败: {e}") - - def set_render_parameters(self, params: Dict[str, Any]): - """ - 设置渲染参数 - - Args: - params: 渲染参数字典 - """ - self.render_params.update(params) - print(f"✓ 渲染参数已更新: {self.render_params}") - - def set_lighting_parameters(self, params: Dict[str, Any]): - """ - 设置光照参数 - - Args: - params: 光照参数字典 - """ - self.lighting_params.update(params) - print(f"✓ 光照参数已更新: {self.lighting_params}") - - def set_material_parameters(self, params: Dict[str, Any]): - """ - 设置材质参数 - - Args: - params: 材质参数字典 - """ - self.material_params.update(params) - print(f"✓ 材质参数已更新: {self.material_params}") - - def set_shader_parameters(self, params: Dict[str, Any]): - """ - 设置着色器参数 - - Args: - params: 着色器参数字典 - """ - self.shader_params.update(params) - print(f"✓ 着色器参数已更新: {self.shader_params}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'chunks_rendered': 0, - 'vertices_rendered': 0, - 'triangles_rendered': 0, - 'render_time': 0.0, - 'average_render_time': 0.0, - 'lod_switches': 0 - } - print("✓ 地形渲染器统计信息已重置") - - def generate_terrain_mesh(self, heightmap: np.ndarray, resolution: int = None) -> Dict[str, np.ndarray]: - """ - 生成地形网格 - - Args: - heightmap: 高度图数据 - resolution: 网格分辨率 - - Returns: - 包含顶点、法线、纹理坐标和索引的字典 - """ - try: - if resolution is None: - resolution = self.render_params['resolution'] - - print(f"✓ 开始生成 {resolution}x{resolution} 地形网格...") - - # 创建网格数据 - vertices = [] - normals = [] - texcoords = [] - indices = [] - - # 生成顶点数据 - height, width = heightmap.shape - scale_x = 1.0 / (width - 1) - scale_y = 1.0 / (height - 1) - - # 生成顶点 - for y in range(height): - for x in range(width): - # 顶点位置 - vertex_x = (x / (width - 1)) * 2.0 - 1.0 # -1 到 1 - vertex_y = heightmap[y, x] # 高度值 - vertex_z = (y / (height - 1)) * 2.0 - 1.0 # -1 到 1 - vertices.extend([vertex_x, vertex_y, vertex_z]) - - # 纹理坐标 - texcoord_u = x / (width - 1) - texcoord_v = 1.0 - y / (height - 1) # 翻转V坐标 - texcoords.extend([texcoord_u, texcoord_v]) - - # 计算法线 - normals = self._calculate_normals(heightmap) - - # 生成索引(三角形列表) - for y in range(height - 1): - for x in range(width - 1): - # 第一个三角形 - indices.append(y * width + x) - indices.append(y * width + x + 1) - indices.append((y + 1) * width + x) - - # 第二个三角形 - indices.append((y + 1) * width + x) - indices.append(y * width + x + 1) - indices.append((y + 1) * width + x + 1) - - # 转换为numpy数组 - vertices_array = np.array(vertices, dtype=np.float32) - normals_array = np.array(normals, dtype=np.float32) - texcoords_array = np.array(texcoords, dtype=np.float32) - indices_array = np.array(indices, dtype=np.uint32) - - # 更新统计信息 - self.stats['vertices_rendered'] = len(vertices) // 3 - self.stats['triangles_rendered'] = len(indices) // 3 - - print(f"✓ 地形网格生成完成: {len(vertices)//3} 顶点, {len(indices)//3} 三角形") - return { - 'vertices': vertices_array, - 'normals': normals_array, - 'texcoords': texcoords_array, - 'indices': indices_array - } - - except Exception as e: - print(f"✗ 地形网格生成失败: {e}") - import traceback - traceback.print_exc() - return { - 'vertices': np.array([], dtype=np.float32), - 'normals': np.array([], dtype=np.float32), - 'texcoords': np.array([], dtype=np.float32), - 'indices': np.array([], dtype=np.uint32) - } - - def _calculate_normals(self, heightmap: np.ndarray) -> List[float]: - """ - 计算法线 - - Args: - heightmap: 高度图数据 - - Returns: - 法线数据列表 - """ - try: - height, width = heightmap.shape - normals = [] - - for y in range(height): - for x in range(width): - # 计算相邻点的高度 - h_left = heightmap[y, max(0, x-1)] - h_right = heightmap[y, min(width-1, x+1)] - h_up = heightmap[max(0, y-1), x] - h_down = heightmap[min(height-1, y+1), x] - - # 计算梯度 - dx = (h_right - h_left) * width / 2.0 - dy = (h_down - h_up) * height / 2.0 - dz = 2.0 - - # 计算法线 - normal = [-dx, -dy, dz] - norm = math.sqrt(normal[0]*normal[0] + normal[1]*normal[1] + normal[2]*normal[2]) - if norm > 0: - normal = [n/norm for n in normal] - else: - normal = [0.0, 0.0, 1.0] - - normals.extend(normal) - - return normals - - except Exception as e: - print(f"✗ 法线计算失败: {e}") - # 返回默认法线 - height, width = heightmap.shape - return [0.0, 0.0, 1.0] * height * width - - def generate_terrain_texture(self, heightmap: np.ndarray, biome_map: np.ndarray = None, - moisture_map: np.ndarray = None, temperature_map: np.ndarray = None) -> np.ndarray: - """ - 生成地形纹理 - - Args: - heightmap: 高度图数据 - biome_map: 生物群落图数据(可选) - moisture_map: 湿度图数据(可选) - temperature_map: 温度图数据(可选) - - Returns: - 纹理数据 (RGBA格式) - """ - try: - height, width = heightmap.shape - print(f"✓ 开始生成 {width}x{height} 地形纹理...") - - # 创建RGBA纹理数据 - texture_data = np.zeros((height, width, 4), dtype=np.uint8) - - # 如果有生物群落数据,基于生物群落生成纹理 - if biome_map is not None and biome_map.shape == heightmap.shape: - texture_data = self._generate_biome_based_texture(biome_map) - else: - # 基于高度生成基础纹理 - texture_data = self._generate_height_based_texture(heightmap) - - # 如果有湿度和温度数据,添加细节 - if moisture_map is not None and moisture_map.shape == heightmap.shape: - texture_data = self._apply_moisture_effects(texture_data, moisture_map) - - if temperature_map is not None and temperature_map.shape == heightmap.shape: - texture_data = self._apply_temperature_effects(texture_data, temperature_map) - - print("✓ 地形纹理生成完成") - return texture_data - - except Exception as e: - print(f"✗ 地形纹理生成失败: {e}") - import traceback - traceback.print_exc() - # 返回默认纹理(蓝色) - height, width = heightmap.shape - return np.zeros((height, width, 4), dtype=np.uint8) - - def _generate_biome_based_texture(self, biome_map: np.ndarray) -> np.ndarray: - """ - 基于生物群落生成纹理 - - Args: - biome_map: 生物群落图数据 - - Returns: - 纹理数据 - """ - try: - height, width = biome_map.shape - texture_data = np.zeros((height, width, 4), dtype=np.uint8) - - # 生物群落颜色映射 - biome_colors = { - 0: [0, 0, 128, 255], # ocean - 深蓝色 - 1: [255, 255, 0, 255], # beach - 黄色 - 2: [144, 238, 144, 255], # plains - 浅绿色 - 3: [34, 139, 34, 255], # forest - 绿色 - 4: [0, 100, 0, 255], # jungle - 深绿色 - 5: [255, 165, 0, 255], # desert - 橙色 - 6: [139, 137, 137, 255], # mountain - 灰色 - 7: [255, 255, 255, 255], # snow - 白色 - 8: [0, 128, 128, 255], # taiga - 青色 - 9: [173, 216, 230, 255], # tundra - 淡蓝色 - 10: [128, 128, 0, 255], # swamp - 橄榄色 - 11: [255, 215, 0, 255] # savanna - 金色 - } - - # 为每个像素分配颜色 - for y in range(height): - for x in range(width): - biome_id = biome_map[y, x] - color = biome_colors.get(biome_id, [128, 128, 128, 255]) # 默认灰色 - texture_data[y, x] = color - - return texture_data - - except Exception as e: - print(f"✗ 生物群落纹理生成失败: {e}") - height, width = biome_map.shape - return np.zeros((height, width, 4), dtype=np.uint8) - - def _generate_height_based_texture(self, heightmap: np.ndarray) -> np.ndarray: - """ - 基于高度生成纹理 - - Args: - heightmap: 高度图数据 - - Returns: - 纹理数据 - """ - try: - height, width = heightmap.shape - texture_data = np.zeros((height, width, 4), dtype=np.uint8) - - # 为每个像素根据高度分配颜色 - for y in range(height): - for x in range(width): - height_value = heightmap[y, x] - - # 根据高度选择颜色 - if height_value < 0.2: - # 水域 - 蓝色 - r, g, b = 0, 0, int(128 + height_value * 127) - elif height_value < 0.3: - # 浅水/海滩 - 浅蓝色 - r, g, b = int(height_value * 255), int(height_value * 255), 255 - elif height_value < 0.5: - # 平原 - 绿色 - r, g, b = 0, int(100 + height_value * 155), 0 - elif height_value < 0.7: - # 丘陵 - 棕色 - r, g, b = int(139 * height_value), int(69 * height_value), int(19 * height_value) - elif height_value < 0.9: - # 山地 - 灰色 - r, g, b = int(100 + height_value * 155), int(100 + height_value * 155), int(100 + height_value * 155) - else: - # 雪山 - 白色 - r, g, b = 255, 255, 255 - - texture_data[y, x] = [r, g, b, 255] - - return texture_data - - except Exception as e: - print(f"✗ 高度纹理生成失败: {e}") - height, width = heightmap.shape - return np.zeros((height, width, 4), dtype=np.uint8) - - def _apply_moisture_effects(self, texture_data: np.ndarray, moisture_map: np.ndarray) -> np.ndarray: - """ - 应用湿度效果到纹理 - - Args: - texture_data: 原始纹理数据 - moisture_map: 湿度图数据 - - Returns: - 处理后的纹理数据 - """ - try: - processed_texture = texture_data.copy() - height, width = moisture_map.shape - - # 根据湿度调整颜色 - for y in range(height): - for x in range(width): - moisture_value = moisture_map[y, x] - - # 增加蓝色调表示湿润 - if moisture_value > 0.6: - processed_texture[y, x, 2] = min(255, processed_texture[y, x, 2] + int(50 * moisture_value)) - - # 减少红色和绿色通道表示湿润 - if moisture_value > 0.5: - processed_texture[y, x, 0] = max(0, processed_texture[y, x, 0] - int(30 * moisture_value)) - processed_texture[y, x, 1] = max(0, processed_texture[y, x, 1] - int(30 * moisture_value)) - - return processed_texture - - except Exception as e: - print(f"✗ 湿度效果应用失败: {e}") - return texture_data - - def _apply_temperature_effects(self, texture_data: np.ndarray, temperature_map: np.ndarray) -> np.ndarray: - """ - 应用温度效果到纹理 - - Args: - texture_data: 原始纹理数据 - temperature_map: 温度图数据 - - Returns: - 处理后的纹理数据 - """ - try: - processed_texture = texture_data.copy() - height, width = temperature_map.shape - - # 根据温度调整颜色 - for y in range(height): - for x in range(width): - temperature_value = temperature_map[y, x] - - # 增加热色调表示温暖 - if temperature_value > 0.7: - processed_texture[y, x, 0] = min(255, processed_texture[y, x, 0] + int(50 * (temperature_value - 0.7) / 0.3)) - - # 增加冷色调表示寒冷 - if temperature_value < 0.3: - processed_texture[y, x, 2] = min(255, processed_texture[y, x, 2] + int(50 * (0.3 - temperature_value) / 0.3)) - - return processed_texture - - except Exception as e: - print(f"✗ 温度效果应用失败: {e}") - return texture_data - - def generate_normal_map(self, heightmap: np.ndarray, strength: float = 1.0) -> np.ndarray: - """ - 生成法线贴图 - - Args: - heightmap: 高度图数据 - strength: 法线强度 - - Returns: - 法线贴图数据 (RGB格式) - """ - try: - height, width = heightmap.shape - print(f"✓ 开始生成 {width}x{height} 法线贴图...") - - # 创建法线贴图数据 - normal_map = np.zeros((height, width, 3), dtype=np.uint8) - - # 计算每个像素的法线 - for y in range(height): - for x in range(width): - # 计算相邻点的高度 - h_left = heightmap[y, max(0, x-1)] - h_right = heightmap[y, min(width-1, x+1)] - h_up = heightmap[max(0, y-1), x] - h_down = heightmap[min(height-1, y+1), x] - - # 计算梯度 - dx = (h_right - h_left) * strength - dy = (h_down - h_up) * strength - - # 计算法线 (转换到0-1范围再转为0-255) - normal_x = (dx + 1.0) * 0.5 - normal_y = (dy + 1.0) * 0.5 - normal_z = 1.0 # 假设主要朝向摄像机 - normal_z = normal_z / math.sqrt(normal_x*normal_x + normal_y*normal_y + normal_z*normal_z) - normal_z = (normal_z + 1.0) * 0.5 - - # 转换为8位值 - normal_map[y, x] = [ - int(normal_x * 255), - int(normal_y * 255), - int(normal_z * 255) - ] - - print("✓ 法线贴图生成完成") - return normal_map - - except Exception as e: - print(f"✗ 法线贴图生成失败: {e}") - import traceback - traceback.print_exc() - # 返回默认法线贴图(正面法线) - height, width = heightmap.shape - return np.full((height, width, 3), [128, 128, 255], dtype=np.uint8) - - def generate_splat_map(self, biome_map: np.ndarray, num_textures: int = 4) -> np.ndarray: - """ - 生成混合贴图(用于多重纹理混合) - - Args: - biome_map: 生物群落图数据 - num_textures: 纹理数量 - - Returns: - 混合贴图数据 (RGBA格式,每个通道代表一种纹理的权重) - """ - try: - height, width = biome_map.shape - print(f"✓ 开始生成 {width}x{height} 混合贴图...") - - # 创建混合贴图数据 - splat_map = np.zeros((height, width, 4), dtype=np.uint8) - - # 简化的混合贴图生成 - # 在实际实现中,这里会根据生物群落类型和地形特征生成更复杂的混合 - for y in range(height): - for x in range(width): - biome_id = biome_map[y, x] - - # 根据生物群落ID分配到不同的纹理通道 - channel = biome_id % 4 - splat_map[y, x, channel] = 255 - - print("✓ 混合贴图生成完成") - return splat_map - - except Exception as e: - print(f"✗ 混合贴图生成失败: {e}") - import traceback - traceback.print_exc() - # 返回默认混合贴图 - height, width = biome_map.shape - return np.zeros((height, width, 4), dtype=np.uint8) - - def update_chunk_mesh(self, chunk_x: int, chunk_y: int, lod_level: int, - heightmap: np.ndarray) -> bool: - """ - 更新区块网格 - - Args: - chunk_x: 区块X坐标 - chunk_y: 区块Y坐标 - lod_level: LOD级别 - heightmap: 高度图数据 - - Returns: - 是否更新成功 - """ - try: - chunk_key = f"{chunk_x},{chunk_y},{lod_level}" - - # 生成区块网格数据 - mesh_data = self.generate_terrain_mesh(heightmap) - - # 存储到LOD区块中 - if chunk_key not in self.lod_chunks: - self.lod_chunks[chunk_key] = {} - - self.lod_chunks[chunk_key]['mesh_data'] = mesh_data - self.lod_chunks[chunk_key]['last_update'] = time.time() - - print(f"✓ 区块 [{chunk_x}, {chunk_y}] LOD {lod_level} 网格已更新") - return True - - except Exception as e: - print(f"✗ 区块网格更新失败: {e}") - import traceback - traceback.print_exc() - return False - - def get_chunk_mesh(self, chunk_x: int, chunk_y: int, lod_level: int) -> Dict[str, np.ndarray]: - """ - 获取区块网格数据 - - Args: - chunk_x: 区块X坐标 - chunk_y: 区块Y坐标 - lod_level: LOD级别 - - Returns: - 网格数据字典 - """ - try: - chunk_key = f"{chunk_x},{chunk_y},{lod_level}" - if chunk_key in self.lod_chunks: - return self.lod_chunks[chunk_key].get('mesh_data', {}) - else: - return {} - - except Exception as e: - print(f"✗ 区块网格获取失败: {e}") - return {} - - def set_lod_distances(self, distances: List[float]): - """ - 设置LOD距离 - - Args: - distances: LOD距离列表 - """ - self.render_params['lod_distances'] = distances - print(f"✓ LOD距离已设置: {distances}") - - def get_lod_level(self, distance: float) -> int: - """ - 根据距离确定LOD级别 - - Args: - distance: 距离 - - Returns: - LOD级别 - """ - try: - lod_distances = self.render_params.get('lod_distances', []) - if not lod_distances: - # 使用默认距离计算 - base_distance = 100.0 - for i in range(self.render_params['lod_levels']): - if distance < base_distance * (i + 1): - return i - return self.render_params['lod_levels'] - 1 - else: - # 使用自定义距离 - for i, lod_distance in enumerate(lod_distances): - if distance < lod_distance: - return i - return len(lod_distances) - 1 - - except Exception as e: - print(f"✗ LOD级别计算失败: {e}") - return 0 - - def frustum_cull(self, camera_position: List[float], - view_matrix: np.ndarray, projection_matrix: np.ndarray) -> List[Tuple[int, int]]: - """ - 视锥体剔除 - - Args: - camera_position: 摄像机位置 - view_matrix: 视图矩阵 - projection_matrix: 投影矩阵 - - Returns: - 可见区块列表 [(chunk_x, chunk_y), ...] - """ - try: - # 在实际实现中,这里会进行复杂的视锥体剔除计算 - # 简化实现:返回所有区块 - visible_chunks = [] - - if self.plugin.terrain_manager: - terrain_data = self.plugin.terrain_manager.get_terrain_data() - terrain_size = terrain_data.get('stats', {}).get('chunks_generated', 0) - if terrain_size > 0: - # 模拟一些可见区块 - for i in range(min(10, terrain_size)): - visible_chunks.append((i, i)) - - return visible_chunks - - except Exception as e: - print(f"✗ 视锥体剔除失败: {e}") - return [] - - def occlusion_cull(self, visible_chunks: List[Tuple[int, int]]) -> List[Tuple[int, int]]: - """ - 遮挡剔除 - - Args: - visible_chunks: 可见区块列表 - - Returns: - 未被遮挡的区块列表 - """ - try: - # 在实际实现中,这里会进行遮挡剔除计算 - # 简化实现:返回所有可见区块 - return visible_chunks - - except Exception as e: - print(f"✗ 遮挡剔除失败: {e}") - return visible_chunks - - def generate_terrain_materials(self, biome_map: np.ndarray) -> Dict[str, Any]: - """ - 生成地形材质 - - Args: - biome_map: 生物群落图数据 - - Returns: - 材质数据字典 - """ - try: - print("✓ 开始生成地形材质...") - - # 在实际实现中,这里会生成多种材质纹理和属性 - materials = { - 'diffuse_textures': [], - 'normal_textures': [], - 'specular_textures': [], - 'biome_mapping': {} - } - - # 为每种生物群落生成材质ID - unique_biomes = np.unique(biome_map) - for biome_id in unique_biomes: - materials['biome_mapping'][biome_id] = f"material_{biome_id}" - - print("✓ 地形材质生成完成") - return materials - - except Exception as e: - print(f"✗ 地形材质生成失败: {e}") - import traceback - traceback.print_exc() - return {} - - def apply_tessellation(self, vertices: np.ndarray, tessellation_factor: float) -> np.ndarray: - """ - 应用细分曲面 - - Args: - vertices: 顶点数据 - tessellation_factor: 细分因子 - - Returns: - 细分后的顶点数据 - """ - try: - # 在实际实现中,这里会使用GPU细分着色器 - # 简化实现:返回原始顶点 - print(f"✓ 应用细分曲面 (因子: {tessellation_factor})") - return vertices - - except Exception as e: - print(f"✗ 细分曲面应用失败: {e}") - return vertices - - def apply_displacement_mapping(self, heightmap: np.ndarray, displacement_factor: float) -> np.ndarray: - """ - 应用置换贴图 - - Args: - heightmap: 高度图数据 - displacement_factor: 置换因子 - - Returns: - 置换后的顶点数据 - """ - try: - # 在实际实现中,这里会在细分着色器中应用置换贴图 - # 简化实现:返回处理后的高度图 - displaced_heightmap = heightmap * displacement_factor - print(f"✓ 应用置换贴图 (因子: {displacement_factor})") - return displaced_heightmap - - except Exception as e: - print(f"✗ 置换贴图应用失败: {e}") - return heightmap - - def calculate_shadow_map(self, light_direction: List[float], - heightmap: np.ndarray) -> np.ndarray: - """ - 计算阴影贴图 - - Args: - light_direction: 光照方向 - heightmap: 高度图数据 - - Returns: - 阴影贴图数据 - """ - try: - height, width = heightmap.shape - print(f"✓ 开始计算 {width}x{height} 阴影贴图...") - - # 创建阴影贴图数据 - shadow_map = np.ones((height, width), dtype=np.float32) - - # 简化的阴影计算 - light_x, light_y, light_z = light_direction - - # 根据光照方向计算阴影 - for y in range(height): - for x in range(width): - # 简单的高度遮挡计算 - current_height = heightmap[y, x] - shadow_factor = 1.0 - - # 检查在光照方向上是否有更高的地形遮挡 - step_x = int(light_x * 10) - step_y = int(light_y * 10) - - check_x = x + step_x - check_y = y + step_y - - if 0 <= check_x < width and 0 <= check_y < height: - check_height = heightmap[check_y, check_x] - if check_height > current_height: - shadow_factor = 0.5 # 简单的半阴影 - - shadow_map[y, x] = shadow_factor - - print("✓ 阴影贴图计算完成") - return shadow_map - - except Exception as e: - print(f"✗ 阴影贴图计算失败: {e}") - import traceback - traceback.print_exc() - # 返回默认阴影贴图(无阴影) - height, width = heightmap.shape - return np.ones((height, width), dtype=np.float32) - - def apply_atmospheric_effects(self, color_data: np.ndarray, - camera_position: List[float], - sun_direction: List[float]) -> np.ndarray: - """ - 应用大气效果 - - Args: - color_data: 颜色数据 - camera_position: 摄像机位置 - sun_direction: 太阳方向 - - Returns: - 处理后的颜色数据 - """ - try: - processed_colors = color_data.copy() - height, width = processed_colors.shape[:2] - - print("✓ 应用大气效果...") - - # 简化的雾效和光照计算 - fog_density = 0.01 - sun_r, sun_g, sun_b = sun_direction - - for y in range(height): - for x in range(width): - # 计算到摄像机的距离(简化) - distance = math.sqrt((x - camera_position[0])**2 + (y - camera_position[1])**2) - - # 应用雾效 - fog_factor = math.exp(-distance * fog_density) - fog_factor = max(0.0, min(1.0, fog_factor)) - - # 混合雾颜色(白色) - processed_colors[y, x, 0] = int(processed_colors[y, x, 0] * fog_factor + 255 * (1 - fog_factor)) - processed_colors[y, x, 1] = int(processed_colors[y, x, 1] * fog_factor + 255 * (1 - fog_factor)) - processed_colors[y, x, 2] = int(processed_colors[y, x, 2] * fog_factor + 255 * (1 - fog_factor)) - - print("✓ 大气效果应用完成") - return processed_colors - - except Exception as e: - print(f"✗ 大气效果应用失败: {e}") - return color_data - - def optimize_rendering(self, target_fps: float = 60.0): - """ - 优化渲染性能 - - Args: - target_fps: 目标帧率 - """ - try: - print(f"✓ 开始优化渲染性能 (目标FPS: {target_fps})") - - # 根据目标FPS调整渲染参数 - current_fps = 1.0 / max(0.001, self.stats['average_render_time']) - - if current_fps < target_fps * 0.8: # 如果FPS低于目标的80% - # 降低渲染质量以提高性能 - self.render_params['lod_levels'] = max(1, self.render_params['lod_levels'] - 1) - self.render_params['chunk_size'] = min(64, self.render_params['chunk_size'] * 2) - print(" → 降低LOD级别和增加区块大小以提高性能") - - elif current_fps > target_fps * 1.2: # 如果FPS高于目标的120% - # 提高渲染质量 - self.render_params['lod_levels'] = min(10, self.render_params['lod_levels'] + 1) - self.render_params['chunk_size'] = max(8, self.render_params['chunk_size'] // 2) - print(" → 提高LOD级别和减小区块大小以提高质量") - - print("✓ 渲染性能优化完成") - - except Exception as e: - print(f"✗ 渲染性能优化失败: {e}") - - def export_render_settings(self, filename: str) -> bool: - """ - 导出渲染设置 - - Args: - filename: 文件名 - - Returns: - 是否导出成功 - """ - try: - import json - - settings = { - 'render_params': self.render_params, - 'lighting_params': self.lighting_params, - 'material_params': self.material_params, - 'shader_params': self.shader_params, - 'timestamp': time.time() - } - - with open(filename, 'w', encoding='utf-8') as f: - json.dump(settings, f, ensure_ascii=False, indent=2) - - print(f"✓ 渲染设置已导出到: {filename}") - return True - - except Exception as e: - print(f"✗ 渲染设置导出失败: {e}") - return False - - def import_render_settings(self, filename: str) -> bool: - """ - 导入渲染设置 - - Args: - filename: 文件名 - - Returns: - 是否导入成功 - """ - try: - import json - - with open(filename, 'r', encoding='utf-8') as f: - settings = json.load(f) - - # 更新参数 - if 'render_params' in settings: - self.render_params.update(settings['render_params']) - if 'lighting_params' in settings: - self.lighting_params.update(settings['lighting_params']) - if 'material_params' in settings: - self.material_params.update(settings['material_params']) - if 'shader_params' in settings: - self.shader_params.update(settings['shader_params']) - - print(f"✓ 渲染设置已从 {filename} 导入") - return True - - except Exception as e: - print(f"✗ 渲染设置导入失败: {e}") - return False - - def get_render_capabilities(self) -> Dict[str, Any]: - """ - 获取渲染能力信息 - - Returns: - 渲染能力信息字典 - """ - return { - 'max_texture_size': 8192, - 'max_lod_levels': 10, - 'supported_shaders': ['vertex', 'fragment', 'geometry', 'tessellation'], - 'supports_normal_mapping': True, - 'supports_displacement_mapping': True, - 'supports_tessellation': True, - 'supports_shadow_mapping': True, - 'max_anisotropy': 16.0 - } - - def set_render_quality(self, quality_level: str): - """ - 设置渲染质量级别 - - Args: - quality_level: 质量级别 ('low', 'medium', 'high', 'ultra') - """ - quality_settings = { - 'low': { - 'lod_levels': 2, - 'chunk_size': 64, - 'normal_mapping': False, - 'tessellation_factor': 0.5 - }, - 'medium': { - 'lod_levels': 4, - 'chunk_size': 32, - 'normal_mapping': True, - 'tessellation_factor': 1.0 - }, - 'high': { - 'lod_levels': 6, - 'chunk_size': 16, - 'normal_mapping': True, - 'tessellation_factor': 2.0 - }, - 'ultra': { - 'lod_levels': 8, - 'chunk_size': 8, - 'normal_mapping': True, - 'tessellation_factor': 4.0 - } - } - - if quality_level in quality_settings: - settings = quality_settings[quality_level] - self.render_params['lod_levels'] = settings['lod_levels'] - self.render_params['chunk_size'] = settings['chunk_size'] - self.material_params['normal_mapping'] = settings['normal_mapping'] - self.material_params['tessellation_factor'] = settings['tessellation_factor'] - print(f"✓ 渲染质量已设置为: {quality_level}") - else: +""" +地形渲染器 +负责渲染程序化生成的地形 +""" + +import numpy as np +import math +from typing import Dict, Any, List, Tuple + +class TerrainRenderer: + """ + 地形渲染器 + 负责渲染程序化生成的地形,包括网格生成、纹理映射、光照计算等 + """ + + def __init__(self, plugin): + """ + 初始化地形渲染器 + + Args: + plugin: 程序化地形生成插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 渲染配置 + self.render_params = { + 'resolution': plugin.config.get('terrain_size', 512), + 'lod_levels': plugin.config.get('lod_levels', 5), + 'chunk_size': plugin.config.get('chunk_size', 32), + 'max_distance': 1000.0, + 'lod_distance_factor': 2.0 + } + + # 光照参数 + self.lighting_params = { + 'ambient_light': 0.3, + 'diffuse_light': 0.7, + 'specular_light': 0.2, + 'light_direction': [0.5, 0.5, 0.5], + 'light_color': [1.0, 1.0, 1.0] + } + + # 材质参数 + self.material_params = { + 'diffuse_texture': True, + 'normal_mapping': True, + 'specular_mapping': False, + 'texture_scale': 1.0, + 'tessellation_factor': 1.0 + } + + # 着色器参数 + self.shader_params = { + 'vertex_shader': 'terrain_vertex.glsl', + 'fragment_shader': 'terrain_fragment.glsl', + 'geometry_shader': None, + 'tessellation_control_shader': None, + 'tessellation_evaluation_shader': None + } + + # 渲染状态 + self.render_buffers = {} + self.vertex_arrays = {} + self.textures = {} + self.shaders = {} + + # LOD管理 + self.lod_chunks = {} + self.visible_chunks = set() + + # 统计信息 + self.stats = { + 'chunks_rendered': 0, + 'vertices_rendered': 0, + 'triangles_rendered': 0, + 'render_time': 0.0, + 'average_render_time': 0.0, + 'lod_switches': 0 + } + + print("✓ 地形渲染器已创建") + + def initialize(self) -> bool: + """ + 初始化地形渲染器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化渲染资源(在实际实现中会初始化GPU资源) + self._initialize_render_resources() + + self.initialized = True + print("✓ 地形渲染器初始化完成") + return True + + except Exception as e: + print(f"✗ 地形渲染器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用地形渲染器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 地形渲染器未初始化") + return False + + self.enabled = True + print("✓ 地形渲染器已启用") + return True + + except Exception as e: + print(f"✗ 地形渲染器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用地形渲染器""" + try: + self.enabled = False + print("✓ 地形渲染器已禁用") + + except Exception as e: + print(f"✗ 地形渲染器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理地形渲染器资源""" + try: + self.disable() + + # 清理渲染资源 + self._cleanup_render_resources() + + self.initialized = False + print("✓ 地形渲染器资源已清理") + + except Exception as e: + print(f"✗ 地形渲染器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新地形渲染器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新LOD + self._update_lod() + + # 更新可见区块 + self._update_visible_chunks() + + except Exception as e: + print(f"✗ 地形渲染器更新失败: {e}") + import traceback + traceback.print_exc() + + def _initialize_render_resources(self): + """初始化渲染资源""" + try: + # 在实际实现中,这里会初始化GPU缓冲区、纹理、着色器等 + print(" → 初始化渲染缓冲区...") + print(" → 初始化着色器...") + print(" → 初始化纹理...") + + # 模拟创建一些基本的渲染资源 + self.render_buffers['terrain_vertices'] = [] + self.render_buffers['terrain_indices'] = [] + self.vertex_arrays['terrain_vao'] = [] + self.textures['terrain_diffuse'] = [] + self.textures['terrain_normal'] = [] + self.shaders['terrain_shader'] = [] + + except Exception as e: + print(f"✗ 渲染资源初始化失败: {e}") + + def _cleanup_render_resources(self): + """清理渲染资源""" + try: + # 在实际实现中,这里会清理GPU资源 + self.render_buffers.clear() + self.vertex_arrays.clear() + self.textures.clear() + self.shaders.clear() + self.lod_chunks.clear() + self.visible_chunks.clear() + + except Exception as e: + print(f"✗ 渲染资源清理失败: {e}") + + def render_terrain(self, camera_position: List[float], view_matrix: np.ndarray, + projection_matrix: np.ndarray) -> bool: + """ + 渲染地形 + + Args: + camera_position: 摄像机位置 + view_matrix: 视图矩阵 + projection_matrix: 投影矩阵 + + Returns: + 是否渲染成功 + """ + try: + if not self.enabled: + print("✗ 地形渲染器未启用") + return False + + import time + render_start_time = time.time() + + print("✓ 开始渲染地形...") + + # 更新摄像机相关数据 + self._update_camera_data(camera_position, view_matrix, projection_matrix) + + # 准备渲染数据 + self._prepare_render_data() + + # 执行渲染 + self._execute_rendering() + + # 更新统计信息 + render_time = time.time() - render_start_time + self.stats['render_time'] = render_time + self.stats['chunks_rendered'] += len(self.visible_chunks) + + if self.stats['chunks_rendered'] > 0: + self.stats['average_render_time'] = ( + self.stats['average_render_time'] * (self.stats['chunks_rendered'] - 1) + render_time + ) / self.stats['chunks_rendered'] + + print(f"✓ 地形渲染完成,耗时: {render_time:.3f}秒") + return True + + except Exception as e: + print(f"✗ 地形渲染失败: {e}") + import traceback + traceback.print_exc() + return False + + def _update_camera_data(self, camera_position: List[float], view_matrix: np.ndarray, + projection_matrix: np.ndarray): + """更新摄像机相关数据""" + try: + # 在实际实现中,这里会更新着色器中的摄像机参数 + pass + except Exception as e: + print(f"✗ 摄像机数据更新失败: {e}") + + def _prepare_render_data(self): + """准备渲染数据""" + try: + # 在实际实现中,这里会准备顶点缓冲区、索引缓冲区等 + pass + except Exception as e: + print(f"✗ 渲染数据准备失败: {e}") + + def _execute_rendering(self): + """执行渲染""" + try: + # 在实际实现中,这里会调用GPU渲染命令 + print(" → 绑定着色器程序...") + print(" → 设置光照参数...") + print(" → 绑定纹理...") + print(" → 渲染可见区块...") + + # 模拟渲染过程 + for chunk_key in self.visible_chunks: + print(f" → 渲染区块 {chunk_key}") + + except Exception as e: + print(f"✗ 渲染执行失败: {e}") + + def _update_lod(self): + """更新LOD级别""" + try: + # 在实际实现中,这里会根据摄像机距离更新每个区块的LOD级别 + pass + except Exception as e: + print(f"✗ LOD更新失败: {e}") + + def _update_visible_chunks(self): + """更新可见区块""" + try: + # 在实际实现中,这里会根据视锥体剔除确定可见区块 + if self.plugin.terrain_manager: + terrain_data = self.plugin.terrain_manager.get_terrain_data() + if terrain_data.get('is_generating', False): + # 如果地形正在生成,暂时不更新可见区块 + return + + # 获取可见区块列表 + visible_chunk_keys = self.plugin.terrain_manager.get_visible_chunks() + self.visible_chunks = set(visible_chunk_keys) + + except Exception as e: + print(f"✗ 可见区块更新失败: {e}") + + def set_render_parameters(self, params: Dict[str, Any]): + """ + 设置渲染参数 + + Args: + params: 渲染参数字典 + """ + self.render_params.update(params) + print(f"✓ 渲染参数已更新: {self.render_params}") + + def set_lighting_parameters(self, params: Dict[str, Any]): + """ + 设置光照参数 + + Args: + params: 光照参数字典 + """ + self.lighting_params.update(params) + print(f"✓ 光照参数已更新: {self.lighting_params}") + + def set_material_parameters(self, params: Dict[str, Any]): + """ + 设置材质参数 + + Args: + params: 材质参数字典 + """ + self.material_params.update(params) + print(f"✓ 材质参数已更新: {self.material_params}") + + def set_shader_parameters(self, params: Dict[str, Any]): + """ + 设置着色器参数 + + Args: + params: 着色器参数字典 + """ + self.shader_params.update(params) + print(f"✓ 着色器参数已更新: {self.shader_params}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'chunks_rendered': 0, + 'vertices_rendered': 0, + 'triangles_rendered': 0, + 'render_time': 0.0, + 'average_render_time': 0.0, + 'lod_switches': 0 + } + print("✓ 地形渲染器统计信息已重置") + + def generate_terrain_mesh(self, heightmap: np.ndarray, resolution: int = None) -> Dict[str, np.ndarray]: + """ + 生成地形网格 + + Args: + heightmap: 高度图数据 + resolution: 网格分辨率 + + Returns: + 包含顶点、法线、纹理坐标和索引的字典 + """ + try: + if resolution is None: + resolution = self.render_params['resolution'] + + print(f"✓ 开始生成 {resolution}x{resolution} 地形网格...") + + # 创建网格数据 + vertices = [] + normals = [] + texcoords = [] + indices = [] + + # 生成顶点数据 + height, width = heightmap.shape + scale_x = 1.0 / (width - 1) + scale_y = 1.0 / (height - 1) + + # 生成顶点 + for y in range(height): + for x in range(width): + # 顶点位置 + vertex_x = (x / (width - 1)) * 2.0 - 1.0 # -1 到 1 + vertex_y = heightmap[y, x] # 高度值 + vertex_z = (y / (height - 1)) * 2.0 - 1.0 # -1 到 1 + vertices.extend([vertex_x, vertex_y, vertex_z]) + + # 纹理坐标 + texcoord_u = x / (width - 1) + texcoord_v = 1.0 - y / (height - 1) # 翻转V坐标 + texcoords.extend([texcoord_u, texcoord_v]) + + # 计算法线 + normals = self._calculate_normals(heightmap) + + # 生成索引(三角形列表) + for y in range(height - 1): + for x in range(width - 1): + # 第一个三角形 + indices.append(y * width + x) + indices.append(y * width + x + 1) + indices.append((y + 1) * width + x) + + # 第二个三角形 + indices.append((y + 1) * width + x) + indices.append(y * width + x + 1) + indices.append((y + 1) * width + x + 1) + + # 转换为numpy数组 + vertices_array = np.array(vertices, dtype=np.float32) + normals_array = np.array(normals, dtype=np.float32) + texcoords_array = np.array(texcoords, dtype=np.float32) + indices_array = np.array(indices, dtype=np.uint32) + + # 更新统计信息 + self.stats['vertices_rendered'] = len(vertices) // 3 + self.stats['triangles_rendered'] = len(indices) // 3 + + print(f"✓ 地形网格生成完成: {len(vertices)//3} 顶点, {len(indices)//3} 三角形") + return { + 'vertices': vertices_array, + 'normals': normals_array, + 'texcoords': texcoords_array, + 'indices': indices_array + } + + except Exception as e: + print(f"✗ 地形网格生成失败: {e}") + import traceback + traceback.print_exc() + return { + 'vertices': np.array([], dtype=np.float32), + 'normals': np.array([], dtype=np.float32), + 'texcoords': np.array([], dtype=np.float32), + 'indices': np.array([], dtype=np.uint32) + } + + def _calculate_normals(self, heightmap: np.ndarray) -> List[float]: + """ + 计算法线 + + Args: + heightmap: 高度图数据 + + Returns: + 法线数据列表 + """ + try: + height, width = heightmap.shape + normals = [] + + for y in range(height): + for x in range(width): + # 计算相邻点的高度 + h_left = heightmap[y, max(0, x-1)] + h_right = heightmap[y, min(width-1, x+1)] + h_up = heightmap[max(0, y-1), x] + h_down = heightmap[min(height-1, y+1), x] + + # 计算梯度 + dx = (h_right - h_left) * width / 2.0 + dy = (h_down - h_up) * height / 2.0 + dz = 2.0 + + # 计算法线 + normal = [-dx, -dy, dz] + norm = math.sqrt(normal[0]*normal[0] + normal[1]*normal[1] + normal[2]*normal[2]) + if norm > 0: + normal = [n/norm for n in normal] + else: + normal = [0.0, 0.0, 1.0] + + normals.extend(normal) + + return normals + + except Exception as e: + print(f"✗ 法线计算失败: {e}") + # 返回默认法线 + height, width = heightmap.shape + return [0.0, 0.0, 1.0] * height * width + + def generate_terrain_texture(self, heightmap: np.ndarray, biome_map: np.ndarray = None, + moisture_map: np.ndarray = None, temperature_map: np.ndarray = None) -> np.ndarray: + """ + 生成地形纹理 + + Args: + heightmap: 高度图数据 + biome_map: 生物群落图数据(可选) + moisture_map: 湿度图数据(可选) + temperature_map: 温度图数据(可选) + + Returns: + 纹理数据 (RGBA格式) + """ + try: + height, width = heightmap.shape + print(f"✓ 开始生成 {width}x{height} 地形纹理...") + + # 创建RGBA纹理数据 + texture_data = np.zeros((height, width, 4), dtype=np.uint8) + + # 如果有生物群落数据,基于生物群落生成纹理 + if biome_map is not None and biome_map.shape == heightmap.shape: + texture_data = self._generate_biome_based_texture(biome_map) + else: + # 基于高度生成基础纹理 + texture_data = self._generate_height_based_texture(heightmap) + + # 如果有湿度和温度数据,添加细节 + if moisture_map is not None and moisture_map.shape == heightmap.shape: + texture_data = self._apply_moisture_effects(texture_data, moisture_map) + + if temperature_map is not None and temperature_map.shape == heightmap.shape: + texture_data = self._apply_temperature_effects(texture_data, temperature_map) + + print("✓ 地形纹理生成完成") + return texture_data + + except Exception as e: + print(f"✗ 地形纹理生成失败: {e}") + import traceback + traceback.print_exc() + # 返回默认纹理(蓝色) + height, width = heightmap.shape + return np.zeros((height, width, 4), dtype=np.uint8) + + def _generate_biome_based_texture(self, biome_map: np.ndarray) -> np.ndarray: + """ + 基于生物群落生成纹理 + + Args: + biome_map: 生物群落图数据 + + Returns: + 纹理数据 + """ + try: + height, width = biome_map.shape + texture_data = np.zeros((height, width, 4), dtype=np.uint8) + + # 生物群落颜色映射 + biome_colors = { + 0: [0, 0, 128, 255], # ocean - 深蓝色 + 1: [255, 255, 0, 255], # beach - 黄色 + 2: [144, 238, 144, 255], # plains - 浅绿色 + 3: [34, 139, 34, 255], # forest - 绿色 + 4: [0, 100, 0, 255], # jungle - 深绿色 + 5: [255, 165, 0, 255], # desert - 橙色 + 6: [139, 137, 137, 255], # mountain - 灰色 + 7: [255, 255, 255, 255], # snow - 白色 + 8: [0, 128, 128, 255], # taiga - 青色 + 9: [173, 216, 230, 255], # tundra - 淡蓝色 + 10: [128, 128, 0, 255], # swamp - 橄榄色 + 11: [255, 215, 0, 255] # savanna - 金色 + } + + # 为每个像素分配颜色 + for y in range(height): + for x in range(width): + biome_id = biome_map[y, x] + color = biome_colors.get(biome_id, [128, 128, 128, 255]) # 默认灰色 + texture_data[y, x] = color + + return texture_data + + except Exception as e: + print(f"✗ 生物群落纹理生成失败: {e}") + height, width = biome_map.shape + return np.zeros((height, width, 4), dtype=np.uint8) + + def _generate_height_based_texture(self, heightmap: np.ndarray) -> np.ndarray: + """ + 基于高度生成纹理 + + Args: + heightmap: 高度图数据 + + Returns: + 纹理数据 + """ + try: + height, width = heightmap.shape + texture_data = np.zeros((height, width, 4), dtype=np.uint8) + + # 为每个像素根据高度分配颜色 + for y in range(height): + for x in range(width): + height_value = heightmap[y, x] + + # 根据高度选择颜色 + if height_value < 0.2: + # 水域 - 蓝色 + r, g, b = 0, 0, int(128 + height_value * 127) + elif height_value < 0.3: + # 浅水/海滩 - 浅蓝色 + r, g, b = int(height_value * 255), int(height_value * 255), 255 + elif height_value < 0.5: + # 平原 - 绿色 + r, g, b = 0, int(100 + height_value * 155), 0 + elif height_value < 0.7: + # 丘陵 - 棕色 + r, g, b = int(139 * height_value), int(69 * height_value), int(19 * height_value) + elif height_value < 0.9: + # 山地 - 灰色 + r, g, b = int(100 + height_value * 155), int(100 + height_value * 155), int(100 + height_value * 155) + else: + # 雪山 - 白色 + r, g, b = 255, 255, 255 + + texture_data[y, x] = [r, g, b, 255] + + return texture_data + + except Exception as e: + print(f"✗ 高度纹理生成失败: {e}") + height, width = heightmap.shape + return np.zeros((height, width, 4), dtype=np.uint8) + + def _apply_moisture_effects(self, texture_data: np.ndarray, moisture_map: np.ndarray) -> np.ndarray: + """ + 应用湿度效果到纹理 + + Args: + texture_data: 原始纹理数据 + moisture_map: 湿度图数据 + + Returns: + 处理后的纹理数据 + """ + try: + processed_texture = texture_data.copy() + height, width = moisture_map.shape + + # 根据湿度调整颜色 + for y in range(height): + for x in range(width): + moisture_value = moisture_map[y, x] + + # 增加蓝色调表示湿润 + if moisture_value > 0.6: + processed_texture[y, x, 2] = min(255, processed_texture[y, x, 2] + int(50 * moisture_value)) + + # 减少红色和绿色通道表示湿润 + if moisture_value > 0.5: + processed_texture[y, x, 0] = max(0, processed_texture[y, x, 0] - int(30 * moisture_value)) + processed_texture[y, x, 1] = max(0, processed_texture[y, x, 1] - int(30 * moisture_value)) + + return processed_texture + + except Exception as e: + print(f"✗ 湿度效果应用失败: {e}") + return texture_data + + def _apply_temperature_effects(self, texture_data: np.ndarray, temperature_map: np.ndarray) -> np.ndarray: + """ + 应用温度效果到纹理 + + Args: + texture_data: 原始纹理数据 + temperature_map: 温度图数据 + + Returns: + 处理后的纹理数据 + """ + try: + processed_texture = texture_data.copy() + height, width = temperature_map.shape + + # 根据温度调整颜色 + for y in range(height): + for x in range(width): + temperature_value = temperature_map[y, x] + + # 增加热色调表示温暖 + if temperature_value > 0.7: + processed_texture[y, x, 0] = min(255, processed_texture[y, x, 0] + int(50 * (temperature_value - 0.7) / 0.3)) + + # 增加冷色调表示寒冷 + if temperature_value < 0.3: + processed_texture[y, x, 2] = min(255, processed_texture[y, x, 2] + int(50 * (0.3 - temperature_value) / 0.3)) + + return processed_texture + + except Exception as e: + print(f"✗ 温度效果应用失败: {e}") + return texture_data + + def generate_normal_map(self, heightmap: np.ndarray, strength: float = 1.0) -> np.ndarray: + """ + 生成法线贴图 + + Args: + heightmap: 高度图数据 + strength: 法线强度 + + Returns: + 法线贴图数据 (RGB格式) + """ + try: + height, width = heightmap.shape + print(f"✓ 开始生成 {width}x{height} 法线贴图...") + + # 创建法线贴图数据 + normal_map = np.zeros((height, width, 3), dtype=np.uint8) + + # 计算每个像素的法线 + for y in range(height): + for x in range(width): + # 计算相邻点的高度 + h_left = heightmap[y, max(0, x-1)] + h_right = heightmap[y, min(width-1, x+1)] + h_up = heightmap[max(0, y-1), x] + h_down = heightmap[min(height-1, y+1), x] + + # 计算梯度 + dx = (h_right - h_left) * strength + dy = (h_down - h_up) * strength + + # 计算法线 (转换到0-1范围再转为0-255) + normal_x = (dx + 1.0) * 0.5 + normal_y = (dy + 1.0) * 0.5 + normal_z = 1.0 # 假设主要朝向摄像机 + normal_z = normal_z / math.sqrt(normal_x*normal_x + normal_y*normal_y + normal_z*normal_z) + normal_z = (normal_z + 1.0) * 0.5 + + # 转换为8位值 + normal_map[y, x] = [ + int(normal_x * 255), + int(normal_y * 255), + int(normal_z * 255) + ] + + print("✓ 法线贴图生成完成") + return normal_map + + except Exception as e: + print(f"✗ 法线贴图生成失败: {e}") + import traceback + traceback.print_exc() + # 返回默认法线贴图(正面法线) + height, width = heightmap.shape + return np.full((height, width, 3), [128, 128, 255], dtype=np.uint8) + + def generate_splat_map(self, biome_map: np.ndarray, num_textures: int = 4) -> np.ndarray: + """ + 生成混合贴图(用于多重纹理混合) + + Args: + biome_map: 生物群落图数据 + num_textures: 纹理数量 + + Returns: + 混合贴图数据 (RGBA格式,每个通道代表一种纹理的权重) + """ + try: + height, width = biome_map.shape + print(f"✓ 开始生成 {width}x{height} 混合贴图...") + + # 创建混合贴图数据 + splat_map = np.zeros((height, width, 4), dtype=np.uint8) + + # 简化的混合贴图生成 + # 在实际实现中,这里会根据生物群落类型和地形特征生成更复杂的混合 + for y in range(height): + for x in range(width): + biome_id = biome_map[y, x] + + # 根据生物群落ID分配到不同的纹理通道 + channel = biome_id % 4 + splat_map[y, x, channel] = 255 + + print("✓ 混合贴图生成完成") + return splat_map + + except Exception as e: + print(f"✗ 混合贴图生成失败: {e}") + import traceback + traceback.print_exc() + # 返回默认混合贴图 + height, width = biome_map.shape + return np.zeros((height, width, 4), dtype=np.uint8) + + def update_chunk_mesh(self, chunk_x: int, chunk_y: int, lod_level: int, + heightmap: np.ndarray) -> bool: + """ + 更新区块网格 + + Args: + chunk_x: 区块X坐标 + chunk_y: 区块Y坐标 + lod_level: LOD级别 + heightmap: 高度图数据 + + Returns: + 是否更新成功 + """ + try: + chunk_key = f"{chunk_x},{chunk_y},{lod_level}" + + # 生成区块网格数据 + mesh_data = self.generate_terrain_mesh(heightmap) + + # 存储到LOD区块中 + if chunk_key not in self.lod_chunks: + self.lod_chunks[chunk_key] = {} + + self.lod_chunks[chunk_key]['mesh_data'] = mesh_data + self.lod_chunks[chunk_key]['last_update'] = time.time() + + print(f"✓ 区块 [{chunk_x}, {chunk_y}] LOD {lod_level} 网格已更新") + return True + + except Exception as e: + print(f"✗ 区块网格更新失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_chunk_mesh(self, chunk_x: int, chunk_y: int, lod_level: int) -> Dict[str, np.ndarray]: + """ + 获取区块网格数据 + + Args: + chunk_x: 区块X坐标 + chunk_y: 区块Y坐标 + lod_level: LOD级别 + + Returns: + 网格数据字典 + """ + try: + chunk_key = f"{chunk_x},{chunk_y},{lod_level}" + if chunk_key in self.lod_chunks: + return self.lod_chunks[chunk_key].get('mesh_data', {}) + else: + return {} + + except Exception as e: + print(f"✗ 区块网格获取失败: {e}") + return {} + + def set_lod_distances(self, distances: List[float]): + """ + 设置LOD距离 + + Args: + distances: LOD距离列表 + """ + self.render_params['lod_distances'] = distances + print(f"✓ LOD距离已设置: {distances}") + + def get_lod_level(self, distance: float) -> int: + """ + 根据距离确定LOD级别 + + Args: + distance: 距离 + + Returns: + LOD级别 + """ + try: + lod_distances = self.render_params.get('lod_distances', []) + if not lod_distances: + # 使用默认距离计算 + base_distance = 100.0 + for i in range(self.render_params['lod_levels']): + if distance < base_distance * (i + 1): + return i + return self.render_params['lod_levels'] - 1 + else: + # 使用自定义距离 + for i, lod_distance in enumerate(lod_distances): + if distance < lod_distance: + return i + return len(lod_distances) - 1 + + except Exception as e: + print(f"✗ LOD级别计算失败: {e}") + return 0 + + def frustum_cull(self, camera_position: List[float], + view_matrix: np.ndarray, projection_matrix: np.ndarray) -> List[Tuple[int, int]]: + """ + 视锥体剔除 + + Args: + camera_position: 摄像机位置 + view_matrix: 视图矩阵 + projection_matrix: 投影矩阵 + + Returns: + 可见区块列表 [(chunk_x, chunk_y), ...] + """ + try: + # 在实际实现中,这里会进行复杂的视锥体剔除计算 + # 简化实现:返回所有区块 + visible_chunks = [] + + if self.plugin.terrain_manager: + terrain_data = self.plugin.terrain_manager.get_terrain_data() + terrain_size = terrain_data.get('stats', {}).get('chunks_generated', 0) + if terrain_size > 0: + # 模拟一些可见区块 + for i in range(min(10, terrain_size)): + visible_chunks.append((i, i)) + + return visible_chunks + + except Exception as e: + print(f"✗ 视锥体剔除失败: {e}") + return [] + + def occlusion_cull(self, visible_chunks: List[Tuple[int, int]]) -> List[Tuple[int, int]]: + """ + 遮挡剔除 + + Args: + visible_chunks: 可见区块列表 + + Returns: + 未被遮挡的区块列表 + """ + try: + # 在实际实现中,这里会进行遮挡剔除计算 + # 简化实现:返回所有可见区块 + return visible_chunks + + except Exception as e: + print(f"✗ 遮挡剔除失败: {e}") + return visible_chunks + + def generate_terrain_materials(self, biome_map: np.ndarray) -> Dict[str, Any]: + """ + 生成地形材质 + + Args: + biome_map: 生物群落图数据 + + Returns: + 材质数据字典 + """ + try: + print("✓ 开始生成地形材质...") + + # 在实际实现中,这里会生成多种材质纹理和属性 + materials = { + 'diffuse_textures': [], + 'normal_textures': [], + 'specular_textures': [], + 'biome_mapping': {} + } + + # 为每种生物群落生成材质ID + unique_biomes = np.unique(biome_map) + for biome_id in unique_biomes: + materials['biome_mapping'][biome_id] = f"material_{biome_id}" + + print("✓ 地形材质生成完成") + return materials + + except Exception as e: + print(f"✗ 地形材质生成失败: {e}") + import traceback + traceback.print_exc() + return {} + + def apply_tessellation(self, vertices: np.ndarray, tessellation_factor: float) -> np.ndarray: + """ + 应用细分曲面 + + Args: + vertices: 顶点数据 + tessellation_factor: 细分因子 + + Returns: + 细分后的顶点数据 + """ + try: + # 在实际实现中,这里会使用GPU细分着色器 + # 简化实现:返回原始顶点 + print(f"✓ 应用细分曲面 (因子: {tessellation_factor})") + return vertices + + except Exception as e: + print(f"✗ 细分曲面应用失败: {e}") + return vertices + + def apply_displacement_mapping(self, heightmap: np.ndarray, displacement_factor: float) -> np.ndarray: + """ + 应用置换贴图 + + Args: + heightmap: 高度图数据 + displacement_factor: 置换因子 + + Returns: + 置换后的顶点数据 + """ + try: + # 在实际实现中,这里会在细分着色器中应用置换贴图 + # 简化实现:返回处理后的高度图 + displaced_heightmap = heightmap * displacement_factor + print(f"✓ 应用置换贴图 (因子: {displacement_factor})") + return displaced_heightmap + + except Exception as e: + print(f"✗ 置换贴图应用失败: {e}") + return heightmap + + def calculate_shadow_map(self, light_direction: List[float], + heightmap: np.ndarray) -> np.ndarray: + """ + 计算阴影贴图 + + Args: + light_direction: 光照方向 + heightmap: 高度图数据 + + Returns: + 阴影贴图数据 + """ + try: + height, width = heightmap.shape + print(f"✓ 开始计算 {width}x{height} 阴影贴图...") + + # 创建阴影贴图数据 + shadow_map = np.ones((height, width), dtype=np.float32) + + # 简化的阴影计算 + light_x, light_y, light_z = light_direction + + # 根据光照方向计算阴影 + for y in range(height): + for x in range(width): + # 简单的高度遮挡计算 + current_height = heightmap[y, x] + shadow_factor = 1.0 + + # 检查在光照方向上是否有更高的地形遮挡 + step_x = int(light_x * 10) + step_y = int(light_y * 10) + + check_x = x + step_x + check_y = y + step_y + + if 0 <= check_x < width and 0 <= check_y < height: + check_height = heightmap[check_y, check_x] + if check_height > current_height: + shadow_factor = 0.5 # 简单的半阴影 + + shadow_map[y, x] = shadow_factor + + print("✓ 阴影贴图计算完成") + return shadow_map + + except Exception as e: + print(f"✗ 阴影贴图计算失败: {e}") + import traceback + traceback.print_exc() + # 返回默认阴影贴图(无阴影) + height, width = heightmap.shape + return np.ones((height, width), dtype=np.float32) + + def apply_atmospheric_effects(self, color_data: np.ndarray, + camera_position: List[float], + sun_direction: List[float]) -> np.ndarray: + """ + 应用大气效果 + + Args: + color_data: 颜色数据 + camera_position: 摄像机位置 + sun_direction: 太阳方向 + + Returns: + 处理后的颜色数据 + """ + try: + processed_colors = color_data.copy() + height, width = processed_colors.shape[:2] + + print("✓ 应用大气效果...") + + # 简化的雾效和光照计算 + fog_density = 0.01 + sun_r, sun_g, sun_b = sun_direction + + for y in range(height): + for x in range(width): + # 计算到摄像机的距离(简化) + distance = math.sqrt((x - camera_position[0])**2 + (y - camera_position[1])**2) + + # 应用雾效 + fog_factor = math.exp(-distance * fog_density) + fog_factor = max(0.0, min(1.0, fog_factor)) + + # 混合雾颜色(白色) + processed_colors[y, x, 0] = int(processed_colors[y, x, 0] * fog_factor + 255 * (1 - fog_factor)) + processed_colors[y, x, 1] = int(processed_colors[y, x, 1] * fog_factor + 255 * (1 - fog_factor)) + processed_colors[y, x, 2] = int(processed_colors[y, x, 2] * fog_factor + 255 * (1 - fog_factor)) + + print("✓ 大气效果应用完成") + return processed_colors + + except Exception as e: + print(f"✗ 大气效果应用失败: {e}") + return color_data + + def optimize_rendering(self, target_fps: float = 60.0): + """ + 优化渲染性能 + + Args: + target_fps: 目标帧率 + """ + try: + print(f"✓ 开始优化渲染性能 (目标FPS: {target_fps})") + + # 根据目标FPS调整渲染参数 + current_fps = 1.0 / max(0.001, self.stats['average_render_time']) + + if current_fps < target_fps * 0.8: # 如果FPS低于目标的80% + # 降低渲染质量以提高性能 + self.render_params['lod_levels'] = max(1, self.render_params['lod_levels'] - 1) + self.render_params['chunk_size'] = min(64, self.render_params['chunk_size'] * 2) + print(" → 降低LOD级别和增加区块大小以提高性能") + + elif current_fps > target_fps * 1.2: # 如果FPS高于目标的120% + # 提高渲染质量 + self.render_params['lod_levels'] = min(10, self.render_params['lod_levels'] + 1) + self.render_params['chunk_size'] = max(8, self.render_params['chunk_size'] // 2) + print(" → 提高LOD级别和减小区块大小以提高质量") + + print("✓ 渲染性能优化完成") + + except Exception as e: + print(f"✗ 渲染性能优化失败: {e}") + + def export_render_settings(self, filename: str) -> bool: + """ + 导出渲染设置 + + Args: + filename: 文件名 + + Returns: + 是否导出成功 + """ + try: + import json + + settings = { + 'render_params': self.render_params, + 'lighting_params': self.lighting_params, + 'material_params': self.material_params, + 'shader_params': self.shader_params, + 'timestamp': time.time() + } + + with open(filename, 'w', encoding='utf-8') as f: + json.dump(settings, f, ensure_ascii=False, indent=2) + + print(f"✓ 渲染设置已导出到: {filename}") + return True + + except Exception as e: + print(f"✗ 渲染设置导出失败: {e}") + return False + + def import_render_settings(self, filename: str) -> bool: + """ + 导入渲染设置 + + Args: + filename: 文件名 + + Returns: + 是否导入成功 + """ + try: + import json + + with open(filename, 'r', encoding='utf-8') as f: + settings = json.load(f) + + # 更新参数 + if 'render_params' in settings: + self.render_params.update(settings['render_params']) + if 'lighting_params' in settings: + self.lighting_params.update(settings['lighting_params']) + if 'material_params' in settings: + self.material_params.update(settings['material_params']) + if 'shader_params' in settings: + self.shader_params.update(settings['shader_params']) + + print(f"✓ 渲染设置已从 {filename} 导入") + return True + + except Exception as e: + print(f"✗ 渲染设置导入失败: {e}") + return False + + def get_render_capabilities(self) -> Dict[str, Any]: + """ + 获取渲染能力信息 + + Returns: + 渲染能力信息字典 + """ + return { + 'max_texture_size': 8192, + 'max_lod_levels': 10, + 'supported_shaders': ['vertex', 'fragment', 'geometry', 'tessellation'], + 'supports_normal_mapping': True, + 'supports_displacement_mapping': True, + 'supports_tessellation': True, + 'supports_shadow_mapping': True, + 'max_anisotropy': 16.0 + } + + def set_render_quality(self, quality_level: str): + """ + 设置渲染质量级别 + + Args: + quality_level: 质量级别 ('low', 'medium', 'high', 'ultra') + """ + quality_settings = { + 'low': { + 'lod_levels': 2, + 'chunk_size': 64, + 'normal_mapping': False, + 'tessellation_factor': 0.5 + }, + 'medium': { + 'lod_levels': 4, + 'chunk_size': 32, + 'normal_mapping': True, + 'tessellation_factor': 1.0 + }, + 'high': { + 'lod_levels': 6, + 'chunk_size': 16, + 'normal_mapping': True, + 'tessellation_factor': 2.0 + }, + 'ultra': { + 'lod_levels': 8, + 'chunk_size': 8, + 'normal_mapping': True, + 'tessellation_factor': 4.0 + } + } + + if quality_level in quality_settings: + settings = quality_settings[quality_level] + self.render_params['lod_levels'] = settings['lod_levels'] + self.render_params['chunk_size'] = settings['chunk_size'] + self.material_params['normal_mapping'] = settings['normal_mapping'] + self.material_params['tessellation_factor'] = settings['tessellation_factor'] + print(f"✓ 渲染质量已设置为: {quality_level}") + else: print(f"✗ 无效的质量级别: {quality_level}") \ No newline at end of file diff --git a/plugins/user/procedural_terrain_generation/serialization/terrain_serializer.py b/plugins/user/procedural_terrain_generation/serialization/terrain_serializer.py index 717d2db0..fbf7e015 100644 --- a/plugins/user/procedural_terrain_generation/serialization/terrain_serializer.py +++ b/plugins/user/procedural_terrain_generation/serialization/terrain_serializer.py @@ -1,1416 +1,1416 @@ -""" -地形序列化器 -负责地形数据的序列化和反序列化 -""" - -import numpy as np -import json -import pickle -import zlib -import base64 -from typing import Dict, Any, Optional -import time -import os - -class TerrainSerializer: - """ - 地形序列化器 - 负责地形数据的序列化和反序列化,支持多种格式和压缩方式 - """ - - def __init__(self, plugin): - """ - 初始化地形序列化器 - - Args: - plugin: 程序化地形生成插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 序列化配置 - self.serialization_params = { - 'default_format': 'json', - 'compression_enabled': True, - 'compression_level': 6, - 'chunk_size': 1024, - 'include_metadata': True, - 'include_textures': True, - 'include_vegetation': True, - 'include_water': True - } - - # 支持的格式 - self.supported_formats = ['json', 'pickle', 'binary', 'custom'] - - # 统计信息 - self.stats = { - 'terrains_saved': 0, - 'terrains_loaded': 0, - 'total_save_time': 0.0, - 'total_load_time': 0.0, - 'average_save_time': 0.0, - 'average_load_time': 0.0, - 'data_compressed': 0, - 'data_uncompressed': 0 - } - - print("✓ 地形序列化器已创建") - - def initialize(self) -> bool: - """ - 初始化地形序列化器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 地形序列化器初始化完成") - return True - - except Exception as e: - print(f"✗ 地形序列化器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用地形序列化器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 地形序列化器未初始化") - return False - - self.enabled = True - print("✓ 地形序列化器已启用") - return True - - except Exception as e: - print(f"✗ 地形序列化器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用地形序列化器""" - try: - self.enabled = False - print("✓ 地形序列化器已禁用") - - except Exception as e: - print(f"✗ 地形序列化器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理地形序列化器资源""" - try: - self.disable() - self.initialized = False - print("✓ 地形序列化器资源已清理") - - except Exception as e: - print(f"✗ 地形序列化器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新地形序列化器状态 - - Args: - dt: 时间增量 - """ - # 处理更新逻辑 - pass - - def save_terrain(self, filename: str, terrain_data: Dict[str, Any] = None, - format: str = None, compress: bool = None) -> bool: - """ - 保存地形数据 - - Args: - filename: 文件名 - terrain_data: 地形数据字典(如果为None,则从地形管理器获取) - format: 保存格式 - compress: 是否压缩 - - Returns: - 是否保存成功 - """ - try: - save_start_time = time.time() - - if not self.enabled: - print("✗ 地形序列化器未启用") - return False - - # 使用默认参数如果未指定 - if format is None: - format = self.serialization_params['default_format'] - if compress is None: - compress = self.serialization_params['compression_enabled'] - - # 验证格式 - if format not in self.supported_formats: - print(f"✗ 不支持的格式: {format}") - return False - - print(f"✓ 开始保存地形到 {filename} (格式: {format}, 压缩: {compress})...") - - # 如果未提供地形数据,则从地形管理器获取 - if terrain_data is None: - if self.plugin.terrain_manager: - terrain_data = self.plugin.terrain_manager.get_terrain_data() - else: - print("✗ 无法获取地形数据") - return False - - # 准备保存数据 - save_data = self._prepare_terrain_data(terrain_data) - - # 根据格式保存 - if format == 'json': - success = self._save_json(filename, save_data, compress) - elif format == 'pickle': - success = self._save_pickle(filename, save_data, compress) - elif format == 'binary': - success = self._save_binary(filename, save_data, compress) - elif format == 'custom': - success = self._save_custom(filename, save_data, compress) - else: - print(f"✗ 不支持的保存格式: {format}") - return False - - if success: - # 更新统计信息 - save_time = time.time() - save_start_time - self.stats['terrains_saved'] += 1 - self.stats['total_save_time'] += save_time - if self.stats['terrains_saved'] > 0: - self.stats['average_save_time'] = ( - self.stats['total_save_time'] / self.stats['terrains_saved'] - ) - - print(f"✓ 地形保存完成,耗时: {save_time:.3f}秒") - return True - else: - print("✗ 地形保存失败") - return False - - except Exception as e: - print(f"✗ 地形保存失败: {e}") - import traceback - traceback.print_exc() - return False - - def load_terrain(self, filename: str, format: str = None) -> Optional[Dict[str, Any]]: - """ - 加载地形数据 - - Args: - filename: 文件名 - format: 加载格式(如果为None,则自动检测) - - Returns: - 地形数据字典或None - """ - try: - load_start_time = time.time() - - if not self.enabled: - print("✗ 地形序列化器未启用") - return None - - # 检查文件是否存在 - if not os.path.exists(filename): - print(f"✗ 文件不存在: {filename}") - return None - - print(f"✓ 开始加载地形从 {filename}...") - - # 如果未指定格式,则尝试自动检测 - if format is None: - format = self._detect_format(filename) - - # 验证格式 - if format not in self.supported_formats: - print(f"✗ 不支持的格式: {format}") - return None - - # 根据格式加载 - if format == 'json': - terrain_data = self._load_json(filename) - elif format == 'pickle': - terrain_data = self._load_pickle(filename) - elif format == 'binary': - terrain_data = self._load_binary(filename) - elif format == 'custom': - terrain_data = self._load_custom(filename) - else: - print(f"✗ 不支持的加载格式: {format}") - return None - - if terrain_data is not None: - # 处理加载的数据 - processed_data = self._process_loaded_data(terrain_data) - - # 更新统计信息 - load_time = time.time() - load_start_time - self.stats['terrains_loaded'] += 1 - self.stats['total_load_time'] += load_time - if self.stats['terrains_loaded'] > 0: - self.stats['average_load_time'] = ( - self.stats['total_load_time'] / self.stats['terrains_loaded'] - ) - - print(f"✓ 地形加载完成,耗时: {load_time:.3f}秒") - return processed_data - else: - print("✗ 地形加载失败") - return None - - except Exception as e: - print(f"✗ 地形加载失败: {e}") - import traceback - traceback.print_exc() - return None - - def _prepare_terrain_data(self, terrain_data: Dict[str, Any]) -> Dict[str, Any]: - """ - 准备地形数据以供保存 - - Args: - terrain_data: 原始地形数据 - - Returns: - 准备好的地形数据 - """ - try: - # 创建保存数据的副本 - save_data = { - 'version': '1.0', - 'timestamp': time.time(), - 'format': self.serialization_params['default_format'] - } - - # 添加地形数据 - if 'heightmap' in terrain_data and terrain_data['heightmap'] is not None: - heightmap = terrain_data['heightmap'] - save_data['heightmap'] = { - 'data': heightmap.tolist() if isinstance(heightmap, np.ndarray) else heightmap, - 'shape': heightmap.shape if isinstance(heightmap, np.ndarray) else None, - 'dtype': str(heightmap.dtype) if isinstance(heightmap, np.ndarray) else 'unknown' - } - - # 添加法线图 - if 'normalmap' in terrain_data and terrain_data['normalmap'] is not None: - normalmap = terrain_data['normalmap'] - save_data['normalmap'] = { - 'data': normalmap.tolist() if isinstance(normalmap, np.ndarray) else normalmap, - 'shape': normalmap.shape if isinstance(normalmap, np.ndarray) else None, - 'dtype': str(normalmap.dtype) if isinstance(normalmap, np.ndarray) else 'unknown' - } - - # 添加生物群落图 - if self.serialization_params['include_metadata']: - if 'biome_map' in terrain_data and terrain_data['biome_map'] is not None: - biome_map = terrain_data['biome_map'] - save_data['biome_map'] = { - 'data': biome_map.tolist() if isinstance(biome_map, np.ndarray) else biome_map, - 'shape': biome_map.shape if isinstance(biome_map, np.ndarray) else None, - 'dtype': str(biome_map.dtype) if isinstance(biome_map, np.ndarray) else 'unknown' - } - - # 添加湿度图 - if 'moisture_map' in terrain_data and terrain_data['moisture_map'] is not None: - moisture_map = terrain_data['moisture_map'] - save_data['moisture_map'] = { - 'data': moisture_map.tolist() if isinstance(moisture_map, np.ndarray) else moisture_map, - 'shape': moisture_map.shape if isinstance(moisture_map, np.ndarray) else None, - 'dtype': str(moisture_map.dtype) if isinstance(moisture_map, np.ndarray) else 'unknown' - } - - # 添加温度图 - if 'temperature_map' in terrain_data and terrain_data['temperature_map'] is not None: - temperature_map = terrain_data['temperature_map'] - save_data['temperature_map'] = { - 'data': temperature_map.tolist() if isinstance(temperature_map, np.ndarray) else temperature_map, - 'shape': temperature_map.shape if isinstance(temperature_map, np.ndarray) else None, - 'dtype': str(temperature_map.dtype) if isinstance(temperature_map, np.ndarray) else 'unknown' - } - - # 添加侵蚀数据 - if 'erosion_map' in terrain_data and terrain_data['erosion_map'] is not None: - erosion_map = terrain_data['erosion_map'] - save_data['erosion_map'] = { - 'data': erosion_map.tolist() if isinstance(erosion_map, np.ndarray) else erosion_map, - 'shape': erosion_map.shape if isinstance(erosion_map, np.ndarray) else None, - 'dtype': str(erosion_map.dtype) if isinstance(erosion_map, np.ndarray) else 'unknown' - } - - # 添加沉积物数据 - if 'sediment_map' in terrain_data and terrain_data['sediment_map'] is not None: - sediment_map = terrain_data['sediment_map'] - save_data['sediment_map'] = { - 'data': sediment_map.tolist() if isinstance(sediment_map, np.ndarray) else sediment_map, - 'shape': sediment_map.shape if isinstance(sediment_map, np.ndarray) else None, - 'dtype': str(sediment_map.dtype) if isinstance(sediment_map, np.ndarray) else 'unknown' - } - - # 添加水图 - if self.serialization_params['include_water']: - if 'water_map' in terrain_data and terrain_data['water_map'] is not None: - water_map = terrain_data['water_map'] - save_data['water_map'] = { - 'data': water_map.tolist() if isinstance(water_map, np.ndarray) else water_map, - 'shape': water_map.shape if isinstance(water_map, np.ndarray) else None, - 'dtype': str(water_map.dtype) if isinstance(water_map, np.ndarray) else 'unknown' - } - - # 添加植被数据 - if self.serialization_params['include_vegetation']: - if 'vegetation_map' in terrain_data and terrain_data['vegetation_map'] is not None: - vegetation_map = terrain_data['vegetation_map'] - save_data['vegetation_map'] = { - 'data': vegetation_map.tolist() if isinstance(vegetation_map, np.ndarray) else vegetation_map, - 'shape': vegetation_map.shape if isinstance(vegetation_map, np.ndarray) else None, - 'dtype': str(vegetation_map.dtype) if isinstance(vegetation_map, np.ndarray) else 'unknown' - } - - # 添加统计信息 - if 'stats' in terrain_data: - save_data['stats'] = terrain_data['stats'] - - return save_data - - except Exception as e: - print(f"✗ 地形数据准备失败: {e}") - return {} - - def _process_loaded_data(self, loaded_data: Dict[str, Any]) -> Dict[str, Any]: - """ - 处理加载的地形数据 - - Args: - loaded_data: 加载的地形数据 - - Returns: - 处理后的地形数据 - """ - try: - # 创建处理后的数据 - processed_data = {} - - # 处理高度图 - if 'heightmap' in loaded_data: - heightmap_data = loaded_data['heightmap'] - if 'data' in heightmap_data: - processed_data['heightmap'] = np.array(heightmap_data['data'], dtype=np.float32) - - # 处理法线图 - if 'normalmap' in loaded_data: - normalmap_data = loaded_data['normalmap'] - if 'data' in normalmap_data: - processed_data['normalmap'] = np.array(normalmap_data['data'], dtype=np.float32) - - # 处理生物群落图 - if 'biome_map' in loaded_data: - biome_data = loaded_data['biome_map'] - if 'data' in biome_data: - processed_data['biome_map'] = np.array(biome_data['data'], dtype=np.int32) - - # 处理湿度图 - if 'moisture_map' in loaded_data: - moisture_data = loaded_data['moisture_map'] - if 'data' in moisture_data: - processed_data['moisture_map'] = np.array(moisture_data['data'], dtype=np.float32) - - # 处理温度图 - if 'temperature_map' in loaded_data: - temperature_data = loaded_data['temperature_map'] - if 'data' in temperature_data: - processed_data['temperature_map'] = np.array(temperature_data['data'], dtype=np.float32) - - # 处理侵蚀图 - if 'erosion_map' in loaded_data: - erosion_data = loaded_data['erosion_map'] - if 'data' in erosion_data: - processed_data['erosion_map'] = np.array(erosion_data['data'], dtype=np.float32) - - # 处理沉积物图 - if 'sediment_map' in loaded_data: - sediment_data = loaded_data['sediment_map'] - if 'data' in sediment_data: - processed_data['sediment_map'] = np.array(sediment_data['data'], dtype=np.float32) - - # 处理水图 - if 'water_map' in loaded_data: - water_data = loaded_data['water_map'] - if 'data' in water_data: - processed_data['water_map'] = np.array(water_data['data'], dtype=np.float32) - - # 处理植被图 - if 'vegetation_map' in loaded_data: - vegetation_data = loaded_data['vegetation_map'] - if 'data' in vegetation_data: - processed_data['vegetation_map'] = np.array(vegetation_data['data'], dtype=np.int32) - - # 处理统计信息 - if 'stats' in loaded_data: - processed_data['stats'] = loaded_data['stats'] - - return processed_data - - except Exception as e: - print(f"✗ 加载数据处理失败: {e}") - return {} - - def _save_json(self, filename: str, data: Dict[str, Any], compress: bool) -> bool: - """ - 以JSON格式保存地形数据 - - Args: - filename: 文件名 - data: 地形数据 - compress: 是否压缩 - - Returns: - 是否保存成功 - """ - try: - # 序列化为JSON - json_data = json.dumps(data, ensure_ascii=False, indent=2) - - if compress: - # 压缩数据 - compressed_data = zlib.compress(json_data.encode('utf-8'), - self.serialization_params['compression_level']) - with open(filename, 'wb') as f: - f.write(compressed_data) - self.stats['data_compressed'] += len(compressed_data) - self.stats['data_uncompressed'] += len(json_data.encode('utf-8')) - else: - # 不压缩 - with open(filename, 'w', encoding='utf-8') as f: - f.write(json_data) - self.stats['data_uncompressed'] += len(json_data.encode('utf-8')) - - return True - - except Exception as e: - print(f"✗ JSON格式地形保存失败: {e}") - return False - - def _save_pickle(self, filename: str, data: Dict[str, Any], compress: bool) -> bool: - """ - 以Pickle格式保存地形数据 - - Args: - filename: 文件名 - data: 地形数据 - compress: 是否压缩 - - Returns: - 是否保存成功 - """ - try: - # 序列化为Pickle - pickle_data = pickle.dumps(data) - - if compress: - # 压缩数据 - compressed_data = zlib.compress(pickle_data, - self.serialization_params['compression_level']) - with open(filename, 'wb') as f: - f.write(compressed_data) - self.stats['data_compressed'] += len(compressed_data) - self.stats['data_uncompressed'] += len(pickle_data) - else: - # 不压缩 - with open(filename, 'wb') as f: - f.write(pickle_data) - self.stats['data_uncompressed'] += len(pickle_data) - - return True - - except Exception as e: - print(f"✗ Pickle格式地形保存失败: {e}") - return False - - def _save_binary(self, filename: str, data: Dict[str, Any], compress: bool) -> bool: - """ - 以二进制格式保存地形数据 - - Args: - filename: 文件名 - data: 地形数据 - compress: 是否压缩 - - Returns: - 是否保存成功 - """ - try: - # 转换为二进制格式 - binary_data = self._convert_to_binary(data) - - if compress: - # 压缩数据 - compressed_data = zlib.compress(binary_data, - self.serialization_params['compression_level']) - with open(filename, 'wb') as f: - f.write(compressed_data) - self.stats['data_compressed'] += len(compressed_data) - self.stats['data_uncompressed'] += len(binary_data) - else: - # 不压缩 - with open(filename, 'wb') as f: - f.write(binary_data) - self.stats['data_uncompressed'] += len(binary_data) - - return True - - except Exception as e: - print(f"✗ 二进制格式地形保存失败: {e}") - return False - - def _save_custom(self, filename: str, data: Dict[str, Any], compress: bool) -> bool: - """ - 以自定义格式保存地形数据 - - Args: - filename: 文件名 - data: 地形数据 - compress: 是否压缩 - - Returns: - 是否保存成功 - """ - try: - # 自定义格式保存(这里简化为JSON格式) - return self._save_json(filename, data, compress) - except Exception as e: - print(f"✗ 自定义格式地形保存失败: {e}") - return False - - def _load_json(self, filename: str) -> Optional[Dict[str, Any]]: - """ - 从JSON格式加载地形数据 - - Args: - filename: 文件名 - - Returns: - 地形数据字典或None - """ - try: - # 读取文件 - with open(filename, 'rb') as f: - file_data = f.read() - - # 检查是否压缩 - try: - # 尝试解压 - decompressed_data = zlib.decompress(file_data) - json_data = decompressed_data.decode('utf-8') - self.stats['data_compressed'] += len(file_data) - self.stats['data_uncompressed'] += len(decompressed_data) - except zlib.error: - # 未压缩的JSON - json_data = file_data.decode('utf-8') - self.stats['data_uncompressed'] += len(file_data) - - # 解析JSON - data = json.loads(json_data) - return data - - except Exception as e: - print(f"✗ JSON格式地形加载失败: {e}") - return None - - def _load_pickle(self, filename: str) -> Optional[Dict[str, Any]]: - """ - 从Pickle格式加载地形数据 - - Args: - filename: 文件名 - - Returns: - 地形数据字典或None - """ - try: - # 读取文件 - with open(filename, 'rb') as f: - file_data = f.read() - - # 检查是否压缩 - try: - # 尝试解压 - decompressed_data = zlib.decompress(file_data) - pickle_data = decompressed_data - self.stats['data_compressed'] += len(file_data) - self.stats['data_uncompressed'] += len(decompressed_data) - except zlib.error: - # 未压缩的Pickle - pickle_data = file_data - self.stats['data_uncompressed'] += len(file_data) - - # 解析Pickle - data = pickle.loads(pickle_data) - return data - - except Exception as e: - print(f"✗ Pickle格式地形加载失败: {e}") - return None - - def _load_binary(self, filename: str) -> Optional[Dict[str, Any]]: - """ - 从二进制格式加载地形数据 - - Args: - filename: 文件名 - - Returns: - 地形数据字典或None - """ - try: - # 读取文件 - with open(filename, 'rb') as f: - file_data = f.read() - - # 检查是否压缩 - try: - # 尝试解压 - decompressed_data = zlib.decompress(file_data) - binary_data = decompressed_data - self.stats['data_compressed'] += len(file_data) - self.stats['data_uncompressed'] += len(decompressed_data) - except zlib.error: - # 未压缩的二进制数据 - binary_data = file_data - self.stats['data_uncompressed'] += len(file_data) - - # 解析二进制数据 - data = self._convert_from_binary(binary_data) - return data - - except Exception as e: - print(f"✗ 二进制格式地形加载失败: {e}") - return None - - def _load_custom(self, filename: str) -> Optional[Dict[str, Any]]: - """ - 从自定义格式加载地形数据 - - Args: - filename: 文件名 - - Returns: - 地形数据字典或None - """ - try: - # 自定义格式加载(这里简化为JSON格式) - return self._load_json(filename) - except Exception as e: - print(f"✗ 自定义格式地形加载失败: {e}") - return None - - def _convert_to_binary(self, data: Dict[str, Any]) -> bytes: - """ - 将数据转换为二进制格式 - - Args: - data: 数据字典 - - Returns: - 二进制数据 - """ - try: - # 这里简化为使用Pickle作为二进制格式 - return pickle.dumps(data) - except Exception as e: - print(f"✗ 数据转换为二进制失败: {e}") - return b'' - - def _convert_from_binary(self, binary_data: bytes) -> Dict[str, Any]: - """ - 从二进制数据转换为字典 - - Args: - binary_data: 二进制数据 - - Returns: - 数据字典 - """ - try: - # 这里简化为使用Pickle作为二进制格式 - return pickle.loads(binary_data) - except Exception as e: - print(f"✗ 二进制数据转换失败: {e}") - return {} - - def _detect_format(self, filename: str) -> str: - """ - 检测文件格式 - - Args: - filename: 文件名 - - Returns: - 检测到的格式 - """ - try: - # 根据文件扩展名检测格式 - if filename.endswith('.json') or filename.endswith('.json.gz'): - return 'json' - elif filename.endswith('.pkl') or filename.endswith('.pkl.gz'): - return 'pickle' - elif filename.endswith('.bin') or filename.endswith('.bin.gz'): - return 'binary' - else: - # 默认使用JSON格式 - return self.serialization_params['default_format'] - - except Exception as e: - print(f"✗ 格式检测失败: {e}") - return self.serialization_params['default_format'] - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - # 更新平均时间 - if self.stats['terrains_saved'] > 0: - self.stats['average_save_time'] = self.stats['total_save_time'] / self.stats['terrains_saved'] - if self.stats['terrains_loaded'] > 0: - self.stats['average_load_time'] = self.stats['total_load_time'] / self.stats['terrains_loaded'] - - # 计算压缩率 - total_data = self.stats['data_compressed'] + self.stats['data_uncompressed'] - compression_ratio = 0.0 - if total_data > 0: - compression_ratio = self.stats['data_compressed'] / total_data if self.stats['data_compressed'] > 0 else 1.0 - - stats = self.stats.copy() - stats['compression_ratio'] = compression_ratio - return stats - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'terrains_saved': 0, - 'terrains_loaded': 0, - 'total_save_time': 0.0, - 'total_load_time': 0.0, - 'average_save_time': 0.0, - 'average_load_time': 0.0, - 'data_compressed': 0, - 'data_uncompressed': 0 - } - print("✓ 地形序列化器统计信息已重置") - - def set_serialization_parameters(self, params: Dict[str, Any]): - """ - 设置序列化参数 - - Args: - params: 参数字典 - """ - self.serialization_params.update(params) - print(f"✓ 序列化参数已更新: {self.serialization_params}") - - def export_terrain_to_image(self, heightmap: np.ndarray, filename: str) -> bool: - """ - 导出地形为图像文件 - - Args: - heightmap: 高度图数据 - filename: 文件名 - - Returns: - 是否导出成功 - """ - try: - print(f"✓ 开始导出地形到图像 {filename}...") - - # 确保高度图在0-1范围内 - normalized_heightmap = (heightmap - np.min(heightmap)) / (np.max(heightmap) - np.min(heightmap)) - - # 转换为0-255范围的整数 - image_data = (normalized_heightmap * 255).astype(np.uint8) - - # 保存为PNG文件 - try: - from PIL import Image - image = Image.fromarray(image_data, mode='L') - image.save(filename) - print("✓ 地形图像导出完成") - return True - except ImportError: - # 如果没有PIL,保存为原始二进制数据 - with open(filename + '.raw', 'wb') as f: - f.write(image_data.tobytes()) - print("✓ 地形数据导出完成(无PIL,保存为.raw文件)") - return True - - except Exception as e: - print(f"✗ 地形图像导出失败: {e}") - import traceback - traceback.print_exc() - return False - - def import_terrain_from_image(self, filename: str) -> Optional[np.ndarray]: - """ - 从图像文件导入地形 - - Args: - filename: 文件名 - - Returns: - 高度图数据或None - """ - try: - print(f"✓ 开始从图像 {filename} 导入地形...") - - # 读取图像文件 - try: - from PIL import Image - image = Image.open(filename) - # 转换为灰度图 - image = image.convert('L') - # 转换为numpy数组 - image_data = np.array(image, dtype=np.float32) - # 归一化到0-1范围 - heightmap = image_data / 255.0 - print("✓ 地形图像导入完成") - return heightmap - except ImportError: - # 如果没有PIL,尝试读取原始二进制数据 - if filename.endswith('.raw'): - with open(filename, 'rb') as f: - raw_data = f.read() - # 假设是512x512的图像 - image_data = np.frombuffer(raw_data, dtype=np.uint8) - heightmap = image_data.astype(np.float32) / 255.0 - # 重塑为方形 - size = int(np.sqrt(len(heightmap))) - heightmap = heightmap.reshape((size, size)) - print("✓ 地形数据导入完成(从.raw文件)") - return heightmap - else: - print("✗ 无法读取图像文件(需要PIL库)") - return None - - except Exception as e: - print(f"✗ 地形图像导入失败: {e}") - import traceback - traceback.print_exc() - return None - - def backup_terrain(self, filename: str) -> bool: - """ - 备份当前地形 - - Args: - filename: 备份文件名 - - Returns: - 是否备份成功 - """ - try: - if not self.enabled: - print("✗ 地形序列化器未启用") - return False - - # 获取当前地形数据 - if self.plugin.terrain_manager: - terrain_data = self.plugin.terrain_manager.get_terrain_data() - if terrain_data: - # 保存备份 - backup_filename = filename + '.backup' - success = self.save_terrain(backup_filename, terrain_data) - if success: - print(f"✓ 地形已备份到 {backup_filename}") - return True - else: - print("✗ 地形备份失败") - return False - else: - print("✗ 无法获取当前地形数据") - return False - else: - print("✗ 地形管理器不可用") - return False - - except Exception as e: - print(f"✗ 地形备份失败: {e}") - import traceback - traceback.print_exc() - return False - - def restore_terrain(self, filename: str) -> bool: - """ - 恢复地形备份 - - Args: - filename: 备份文件名 - - Returns: - 是否恢复成功 - """ - try: - if not self.enabled: - print("✗ 地形序列化器未启用") - return False - - # 加载备份数据 - backup_filename = filename + '.backup' - terrain_data = self.load_terrain(backup_filename) - - if terrain_data: - # 应用到地形管理器 - if self.plugin.terrain_manager: - # 这里应该调用地形管理器的方法来恢复数据 - # 由于地形管理器可能没有相应的方法,我们只打印信息 - print(f"✓ 地形已从 {backup_filename} 恢复") - print("注意:需要在地形管理器中实现实际的恢复逻辑") - return True - else: - print("✗ 地形管理器不可用") - return False - else: - print("✗ 无法加载备份数据") - return False - - except Exception as e: - print(f"✗ 地形恢复失败: {e}") - import traceback - traceback.print_exc() - return False - - def get_file_info(self, filename: str) -> Dict[str, Any]: - """ - 获取文件信息 - - Args: - filename: 文件名 - - Returns: - 文件信息字典 - """ - try: - if not os.path.exists(filename): - return {'error': '文件不存在'} - - # 获取文件统计信息 - stat = os.stat(filename) - - file_info = { - 'filename': filename, - 'size': stat.st_size, - 'created': time.ctime(stat.st_ctime), - 'modified': time.ctime(stat.st_mtime), - 'accessed': time.ctime(stat.st_atime) - } - - # 尝试加载文件以获取地形信息 - try: - terrain_data = self.load_terrain(filename) - if terrain_data: - file_info['terrain_info'] = { - 'has_heightmap': 'heightmap' in terrain_data and terrain_data['heightmap'] is not None, - 'has_biome_map': 'biome_map' in terrain_data and terrain_data['biome_map'] is not None, - 'has_vegetation_map': 'vegetation_map' in terrain_data and terrain_data['vegetation_map'] is not None, - 'has_water_map': 'water_map' in terrain_data and terrain_data['water_map'] is not None - } - - if 'stats' in terrain_data: - file_info['terrain_stats'] = terrain_data['stats'] - - except Exception as e: - file_info['load_error'] = str(e) - - return file_info - - except Exception as e: - print(f"✗ 文件信息获取失败: {e}") - return {'error': str(e)} - - def list_terrain_files(self, directory: str = '.') -> List[str]: - """ - 列出目录中的地形文件 - - Args: - directory: 目录路径 - - Returns: - 地形文件列表 - """ - try: - terrain_files = [] - - # 支持的文件扩展名 - supported_extensions = ['.json', '.pkl', '.bin', '.raw'] - - # 遍历目录 - for filename in os.listdir(directory): - file_path = os.path.join(directory, filename) - if os.path.isfile(file_path): - # 检查文件扩展名 - _, ext = os.path.splitext(filename) - if ext in supported_extensions: - terrain_files.append(filename) - - return terrain_files - - except Exception as e: - print(f"✗ 地形文件列表获取失败: {e}") - return [] - - def delete_terrain_file(self, filename: str) -> bool: - """ - 删除地形文件 - - Args: - filename: 文件名 - - Returns: - 是否删除成功 - """ - try: - if os.path.exists(filename): - os.remove(filename) - print(f"✓ 地形文件 {filename} 已删除") - return True - else: - print(f"✗ 地形文件不存在: {filename}") - return False - - except Exception as e: - print(f"✗ 地形文件删除失败: {e}") - return False - - def compress_terrain_file(self, filename: str, output_filename: str = None) -> bool: - """ - 压缩地形文件 - - Args: - filename: 输入文件名 - output_filename: 输出文件名(如果为None,则在原文件名后添加.gz) - - Returns: - 是否压缩成功 - """ - try: - if not os.path.exists(filename): - print(f"✗ 文件不存在: {filename}") - return False - - if output_filename is None: - output_filename = filename + '.gz' - - # 读取原始文件 - with open(filename, 'rb') as f: - data = f.read() - - # 压缩数据 - compressed_data = zlib.compress(data, self.serialization_params['compression_level']) - - # 写入压缩文件 - with open(output_filename, 'wb') as f: - f.write(compressed_data) - - print(f"✓ 地形文件已压缩: {filename} -> {output_filename}") - return True - - except Exception as e: - print(f"✗ 地形文件压缩失败: {e}") - return False - - def decompress_terrain_file(self, filename: str, output_filename: str = None) -> bool: - """ - 解压地形文件 - - Args: - filename: 输入文件名(应该以.gz结尾) - output_filename: 输出文件名(如果为None,则移除.gz后缀) - - Returns: - 是否解压成功 - """ - try: - if not os.path.exists(filename): - print(f"✗ 文件不存在: {filename}") - return False - - if output_filename is None: - if filename.endswith('.gz'): - output_filename = filename[:-3] - else: - output_filename = filename + '.decompressed' - - # 读取压缩文件 - with open(filename, 'rb') as f: - compressed_data = f.read() - - # 解压数据 - try: - data = zlib.decompress(compressed_data) - except zlib.error as e: - print(f"✗ 文件解压失败: {e}") - return False - - # 写入解压文件 - with open(output_filename, 'wb') as f: - f.write(data) - - print(f"✓ 地形文件已解压: {filename} -> {output_filename}") - return True - - except Exception as e: - print(f"✗ 地形文件解压失败: {e}") - return False - - def validate_terrain_file(self, filename: str) -> bool: - """ - 验证地形文件完整性 - - Args: - filename: 文件名 - - Returns: - 文件是否完整 - """ - try: - if not os.path.exists(filename): - print(f"✗ 文件不存在: {filename}") - return False - - # 尝试加载文件 - terrain_data = self.load_terrain(filename) - - if terrain_data is None: - print(f"✗ 地形文件无效: {filename}") - return False - - # 检查必要数据是否存在 - required_keys = ['heightmap'] - missing_keys = [key for key in required_keys if key not in terrain_data or terrain_data[key] is None] - - if missing_keys: - print(f"✗ 地形文件缺少必要数据: {missing_keys}") - return False - - print(f"✓ 地形文件验证通过: {filename}") - return True - - except Exception as e: - print(f"✗ 地形文件验证失败: {e}") - return False - - def compare_terrain_files(self, filename1: str, filename2: str) -> Dict[str, Any]: - """ - 比较两个地形文件 - - Args: - filename1: 第一个文件名 - filename2: 第二个文件名 - - Returns: - 比较结果字典 - """ - try: - # 加载两个文件 - terrain1 = self.load_terrain(filename1) - terrain2 = self.load_terrain(filename2) - - if terrain1 is None or terrain2 is None: - return {'error': '无法加载一个或两个地形文件'} - - comparison = { - 'file1': filename1, - 'file2': filename2, - 'identical': False, - 'differences': [] - } - - # 比较高度图 - if ('heightmap' in terrain1 and terrain1['heightmap'] is not None and - 'heightmap' in terrain2 and terrain2['heightmap'] is not None): - heightmap1 = np.array(terrain1['heightmap']) - heightmap2 = np.array(terrain2['heightmap']) - - if heightmap1.shape == heightmap2.shape: - # 计算差异 - diff = np.abs(heightmap1 - heightmap2) - max_diff = np.max(diff) - mean_diff = np.mean(diff) - - comparison['heightmap_comparison'] = { - 'shapes_match': True, - 'max_difference': float(max_diff), - 'mean_difference': float(mean_diff), - 'identical': max_diff < 1e-6 - } - - if max_diff >= 1e-6: - comparison['differences'].append('heightmap') - else: - comparison['heightmap_comparison'] = { - 'shapes_match': False - } - comparison['differences'].append('heightmap_shape') - else: - comparison['heightmap_comparison'] = { - 'missing_data': True - } - - # 比较其他数据 - maps_to_compare = ['biome_map', 'vegetation_map', 'water_map'] - for map_name in maps_to_compare: - if (map_name in terrain1 and terrain1[map_name] is not None and - map_name in terrain2 and terrain2[map_name] is not None): - map1 = np.array(terrain1[map_name]) - map2 = np.array(terrain2[map_name]) - - if map1.shape == map2.shape: - if not np.array_equal(map1, map2): - comparison['differences'].append(map_name) - else: - comparison['differences'].append(f'{map_name}_shape') - - # 检查是否完全相同 - comparison['identical'] = len(comparison['differences']) == 0 - - return comparison - - except Exception as e: - print(f"✗ 地形文件比较失败: {e}") - return {'error': str(e)} - - def merge_terrain_files(self, filenames: List[str], output_filename: str) -> bool: - """ - 合并多个地形文件 - - Args: - filenames: 文件名列表 - output_filename: 输出文件名 - - Returns: - 是否合并成功 - """ - try: - if len(filenames) < 2: - print("✗ 至少需要两个文件才能合并") - return False - - print(f"✓ 开始合并 {len(filenames)} 个地形文件...") - - # 加载所有文件 - terrains = [] - for filename in filenames: - terrain = self.load_terrain(filename) - if terrain is None: - print(f"✗ 无法加载文件: {filename}") - return False - terrains.append(terrain) - - # 创建合并后的地形数据 - merged_terrain = {} - - # 合并高度图(取平均值) - heightmaps = [t.get('heightmap') for t in terrains if t.get('heightmap') is not None] - if heightmaps: - # 转换为numpy数组 - np_heightmaps = [np.array(h) for h in heightmaps] - # 确保所有高度图尺寸相同 - if all(h.shape == np_heightmaps[0].shape for h in np_heightmaps): - # 计算平均值 - merged_heightmap = np.mean(np_heightmaps, axis=0) - merged_terrain['heightmap'] = merged_heightmap - else: - print("✗ 高度图尺寸不匹配,无法合并") - return False - - # 合并其他数据(这里简化为取第一个文件的数据) - if terrains: - first_terrain = terrains[0] - for key in ['biome_map', 'vegetation_map', 'water_map', 'stats']: - if key in first_terrain and first_terrain[key] is not None: - merged_terrain[key] = first_terrain[key] - - # 保存合并后的地形 - success = self.save_terrain(output_filename, merged_terrain) - - if success: - print(f"✓ 地形文件合并完成: {output_filename}") - return True - else: - print("✗ 地形文件合并失败") - return False - - except Exception as e: - print(f"✗ 地形文件合并失败: {e}") - import traceback - traceback.print_exc() - return False - - def convert_terrain_format(self, input_filename: str, output_filename: str, - output_format: str) -> bool: - """ - 转换地形文件格式 - - Args: - input_filename: 输入文件名 - output_filename: 输出文件名 - output_format: 输出格式 - - Returns: - 是否转换成功 - """ - try: - # 验证输出格式 - if output_format not in self.supported_formats: - print(f"✗ 不支持的输出格式: {output_format}") - return False - - print(f"✓ 开始转换地形格式: {input_filename} -> {output_filename} ({output_format})") - - # 加载输入文件 - terrain_data = self.load_terrain(input_filename) - - if terrain_data is None: - print("✗ 无法加载输入文件") - return False - - # 保存为新格式 - original_format = self.serialization_params['default_format'] - self.serialization_params['default_format'] = output_format - - success = self.save_terrain(output_filename, terrain_data, format=output_format) - - # 恢复原始格式设置 - self.serialization_params['default_format'] = original_format - - if success: - print("✓ 地形格式转换完成") - return True - else: - print("✗ 地形格式转换失败") - return False - - except Exception as e: - print(f"✗ 地形格式转换失败: {e}") - import traceback - traceback.print_exc() +""" +地形序列化器 +负责地形数据的序列化和反序列化 +""" + +import numpy as np +import json +import pickle +import zlib +import base64 +from typing import Dict, Any, Optional +import time +import os + +class TerrainSerializer: + """ + 地形序列化器 + 负责地形数据的序列化和反序列化,支持多种格式和压缩方式 + """ + + def __init__(self, plugin): + """ + 初始化地形序列化器 + + Args: + plugin: 程序化地形生成插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 序列化配置 + self.serialization_params = { + 'default_format': 'json', + 'compression_enabled': True, + 'compression_level': 6, + 'chunk_size': 1024, + 'include_metadata': True, + 'include_textures': True, + 'include_vegetation': True, + 'include_water': True + } + + # 支持的格式 + self.supported_formats = ['json', 'pickle', 'binary', 'custom'] + + # 统计信息 + self.stats = { + 'terrains_saved': 0, + 'terrains_loaded': 0, + 'total_save_time': 0.0, + 'total_load_time': 0.0, + 'average_save_time': 0.0, + 'average_load_time': 0.0, + 'data_compressed': 0, + 'data_uncompressed': 0 + } + + print("✓ 地形序列化器已创建") + + def initialize(self) -> bool: + """ + 初始化地形序列化器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 地形序列化器初始化完成") + return True + + except Exception as e: + print(f"✗ 地形序列化器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用地形序列化器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 地形序列化器未初始化") + return False + + self.enabled = True + print("✓ 地形序列化器已启用") + return True + + except Exception as e: + print(f"✗ 地形序列化器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用地形序列化器""" + try: + self.enabled = False + print("✓ 地形序列化器已禁用") + + except Exception as e: + print(f"✗ 地形序列化器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理地形序列化器资源""" + try: + self.disable() + self.initialized = False + print("✓ 地形序列化器资源已清理") + + except Exception as e: + print(f"✗ 地形序列化器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新地形序列化器状态 + + Args: + dt: 时间增量 + """ + # 处理更新逻辑 + pass + + def save_terrain(self, filename: str, terrain_data: Dict[str, Any] = None, + format: str = None, compress: bool = None) -> bool: + """ + 保存地形数据 + + Args: + filename: 文件名 + terrain_data: 地形数据字典(如果为None,则从地形管理器获取) + format: 保存格式 + compress: 是否压缩 + + Returns: + 是否保存成功 + """ + try: + save_start_time = time.time() + + if not self.enabled: + print("✗ 地形序列化器未启用") + return False + + # 使用默认参数如果未指定 + if format is None: + format = self.serialization_params['default_format'] + if compress is None: + compress = self.serialization_params['compression_enabled'] + + # 验证格式 + if format not in self.supported_formats: + print(f"✗ 不支持的格式: {format}") + return False + + print(f"✓ 开始保存地形到 {filename} (格式: {format}, 压缩: {compress})...") + + # 如果未提供地形数据,则从地形管理器获取 + if terrain_data is None: + if self.plugin.terrain_manager: + terrain_data = self.plugin.terrain_manager.get_terrain_data() + else: + print("✗ 无法获取地形数据") + return False + + # 准备保存数据 + save_data = self._prepare_terrain_data(terrain_data) + + # 根据格式保存 + if format == 'json': + success = self._save_json(filename, save_data, compress) + elif format == 'pickle': + success = self._save_pickle(filename, save_data, compress) + elif format == 'binary': + success = self._save_binary(filename, save_data, compress) + elif format == 'custom': + success = self._save_custom(filename, save_data, compress) + else: + print(f"✗ 不支持的保存格式: {format}") + return False + + if success: + # 更新统计信息 + save_time = time.time() - save_start_time + self.stats['terrains_saved'] += 1 + self.stats['total_save_time'] += save_time + if self.stats['terrains_saved'] > 0: + self.stats['average_save_time'] = ( + self.stats['total_save_time'] / self.stats['terrains_saved'] + ) + + print(f"✓ 地形保存完成,耗时: {save_time:.3f}秒") + return True + else: + print("✗ 地形保存失败") + return False + + except Exception as e: + print(f"✗ 地形保存失败: {e}") + import traceback + traceback.print_exc() + return False + + def load_terrain(self, filename: str, format: str = None) -> Optional[Dict[str, Any]]: + """ + 加载地形数据 + + Args: + filename: 文件名 + format: 加载格式(如果为None,则自动检测) + + Returns: + 地形数据字典或None + """ + try: + load_start_time = time.time() + + if not self.enabled: + print("✗ 地形序列化器未启用") + return None + + # 检查文件是否存在 + if not os.path.exists(filename): + print(f"✗ 文件不存在: {filename}") + return None + + print(f"✓ 开始加载地形从 {filename}...") + + # 如果未指定格式,则尝试自动检测 + if format is None: + format = self._detect_format(filename) + + # 验证格式 + if format not in self.supported_formats: + print(f"✗ 不支持的格式: {format}") + return None + + # 根据格式加载 + if format == 'json': + terrain_data = self._load_json(filename) + elif format == 'pickle': + terrain_data = self._load_pickle(filename) + elif format == 'binary': + terrain_data = self._load_binary(filename) + elif format == 'custom': + terrain_data = self._load_custom(filename) + else: + print(f"✗ 不支持的加载格式: {format}") + return None + + if terrain_data is not None: + # 处理加载的数据 + processed_data = self._process_loaded_data(terrain_data) + + # 更新统计信息 + load_time = time.time() - load_start_time + self.stats['terrains_loaded'] += 1 + self.stats['total_load_time'] += load_time + if self.stats['terrains_loaded'] > 0: + self.stats['average_load_time'] = ( + self.stats['total_load_time'] / self.stats['terrains_loaded'] + ) + + print(f"✓ 地形加载完成,耗时: {load_time:.3f}秒") + return processed_data + else: + print("✗ 地形加载失败") + return None + + except Exception as e: + print(f"✗ 地形加载失败: {e}") + import traceback + traceback.print_exc() + return None + + def _prepare_terrain_data(self, terrain_data: Dict[str, Any]) -> Dict[str, Any]: + """ + 准备地形数据以供保存 + + Args: + terrain_data: 原始地形数据 + + Returns: + 准备好的地形数据 + """ + try: + # 创建保存数据的副本 + save_data = { + 'version': '1.0', + 'timestamp': time.time(), + 'format': self.serialization_params['default_format'] + } + + # 添加地形数据 + if 'heightmap' in terrain_data and terrain_data['heightmap'] is not None: + heightmap = terrain_data['heightmap'] + save_data['heightmap'] = { + 'data': heightmap.tolist() if isinstance(heightmap, np.ndarray) else heightmap, + 'shape': heightmap.shape if isinstance(heightmap, np.ndarray) else None, + 'dtype': str(heightmap.dtype) if isinstance(heightmap, np.ndarray) else 'unknown' + } + + # 添加法线图 + if 'normalmap' in terrain_data and terrain_data['normalmap'] is not None: + normalmap = terrain_data['normalmap'] + save_data['normalmap'] = { + 'data': normalmap.tolist() if isinstance(normalmap, np.ndarray) else normalmap, + 'shape': normalmap.shape if isinstance(normalmap, np.ndarray) else None, + 'dtype': str(normalmap.dtype) if isinstance(normalmap, np.ndarray) else 'unknown' + } + + # 添加生物群落图 + if self.serialization_params['include_metadata']: + if 'biome_map' in terrain_data and terrain_data['biome_map'] is not None: + biome_map = terrain_data['biome_map'] + save_data['biome_map'] = { + 'data': biome_map.tolist() if isinstance(biome_map, np.ndarray) else biome_map, + 'shape': biome_map.shape if isinstance(biome_map, np.ndarray) else None, + 'dtype': str(biome_map.dtype) if isinstance(biome_map, np.ndarray) else 'unknown' + } + + # 添加湿度图 + if 'moisture_map' in terrain_data and terrain_data['moisture_map'] is not None: + moisture_map = terrain_data['moisture_map'] + save_data['moisture_map'] = { + 'data': moisture_map.tolist() if isinstance(moisture_map, np.ndarray) else moisture_map, + 'shape': moisture_map.shape if isinstance(moisture_map, np.ndarray) else None, + 'dtype': str(moisture_map.dtype) if isinstance(moisture_map, np.ndarray) else 'unknown' + } + + # 添加温度图 + if 'temperature_map' in terrain_data and terrain_data['temperature_map'] is not None: + temperature_map = terrain_data['temperature_map'] + save_data['temperature_map'] = { + 'data': temperature_map.tolist() if isinstance(temperature_map, np.ndarray) else temperature_map, + 'shape': temperature_map.shape if isinstance(temperature_map, np.ndarray) else None, + 'dtype': str(temperature_map.dtype) if isinstance(temperature_map, np.ndarray) else 'unknown' + } + + # 添加侵蚀数据 + if 'erosion_map' in terrain_data and terrain_data['erosion_map'] is not None: + erosion_map = terrain_data['erosion_map'] + save_data['erosion_map'] = { + 'data': erosion_map.tolist() if isinstance(erosion_map, np.ndarray) else erosion_map, + 'shape': erosion_map.shape if isinstance(erosion_map, np.ndarray) else None, + 'dtype': str(erosion_map.dtype) if isinstance(erosion_map, np.ndarray) else 'unknown' + } + + # 添加沉积物数据 + if 'sediment_map' in terrain_data and terrain_data['sediment_map'] is not None: + sediment_map = terrain_data['sediment_map'] + save_data['sediment_map'] = { + 'data': sediment_map.tolist() if isinstance(sediment_map, np.ndarray) else sediment_map, + 'shape': sediment_map.shape if isinstance(sediment_map, np.ndarray) else None, + 'dtype': str(sediment_map.dtype) if isinstance(sediment_map, np.ndarray) else 'unknown' + } + + # 添加水图 + if self.serialization_params['include_water']: + if 'water_map' in terrain_data and terrain_data['water_map'] is not None: + water_map = terrain_data['water_map'] + save_data['water_map'] = { + 'data': water_map.tolist() if isinstance(water_map, np.ndarray) else water_map, + 'shape': water_map.shape if isinstance(water_map, np.ndarray) else None, + 'dtype': str(water_map.dtype) if isinstance(water_map, np.ndarray) else 'unknown' + } + + # 添加植被数据 + if self.serialization_params['include_vegetation']: + if 'vegetation_map' in terrain_data and terrain_data['vegetation_map'] is not None: + vegetation_map = terrain_data['vegetation_map'] + save_data['vegetation_map'] = { + 'data': vegetation_map.tolist() if isinstance(vegetation_map, np.ndarray) else vegetation_map, + 'shape': vegetation_map.shape if isinstance(vegetation_map, np.ndarray) else None, + 'dtype': str(vegetation_map.dtype) if isinstance(vegetation_map, np.ndarray) else 'unknown' + } + + # 添加统计信息 + if 'stats' in terrain_data: + save_data['stats'] = terrain_data['stats'] + + return save_data + + except Exception as e: + print(f"✗ 地形数据准备失败: {e}") + return {} + + def _process_loaded_data(self, loaded_data: Dict[str, Any]) -> Dict[str, Any]: + """ + 处理加载的地形数据 + + Args: + loaded_data: 加载的地形数据 + + Returns: + 处理后的地形数据 + """ + try: + # 创建处理后的数据 + processed_data = {} + + # 处理高度图 + if 'heightmap' in loaded_data: + heightmap_data = loaded_data['heightmap'] + if 'data' in heightmap_data: + processed_data['heightmap'] = np.array(heightmap_data['data'], dtype=np.float32) + + # 处理法线图 + if 'normalmap' in loaded_data: + normalmap_data = loaded_data['normalmap'] + if 'data' in normalmap_data: + processed_data['normalmap'] = np.array(normalmap_data['data'], dtype=np.float32) + + # 处理生物群落图 + if 'biome_map' in loaded_data: + biome_data = loaded_data['biome_map'] + if 'data' in biome_data: + processed_data['biome_map'] = np.array(biome_data['data'], dtype=np.int32) + + # 处理湿度图 + if 'moisture_map' in loaded_data: + moisture_data = loaded_data['moisture_map'] + if 'data' in moisture_data: + processed_data['moisture_map'] = np.array(moisture_data['data'], dtype=np.float32) + + # 处理温度图 + if 'temperature_map' in loaded_data: + temperature_data = loaded_data['temperature_map'] + if 'data' in temperature_data: + processed_data['temperature_map'] = np.array(temperature_data['data'], dtype=np.float32) + + # 处理侵蚀图 + if 'erosion_map' in loaded_data: + erosion_data = loaded_data['erosion_map'] + if 'data' in erosion_data: + processed_data['erosion_map'] = np.array(erosion_data['data'], dtype=np.float32) + + # 处理沉积物图 + if 'sediment_map' in loaded_data: + sediment_data = loaded_data['sediment_map'] + if 'data' in sediment_data: + processed_data['sediment_map'] = np.array(sediment_data['data'], dtype=np.float32) + + # 处理水图 + if 'water_map' in loaded_data: + water_data = loaded_data['water_map'] + if 'data' in water_data: + processed_data['water_map'] = np.array(water_data['data'], dtype=np.float32) + + # 处理植被图 + if 'vegetation_map' in loaded_data: + vegetation_data = loaded_data['vegetation_map'] + if 'data' in vegetation_data: + processed_data['vegetation_map'] = np.array(vegetation_data['data'], dtype=np.int32) + + # 处理统计信息 + if 'stats' in loaded_data: + processed_data['stats'] = loaded_data['stats'] + + return processed_data + + except Exception as e: + print(f"✗ 加载数据处理失败: {e}") + return {} + + def _save_json(self, filename: str, data: Dict[str, Any], compress: bool) -> bool: + """ + 以JSON格式保存地形数据 + + Args: + filename: 文件名 + data: 地形数据 + compress: 是否压缩 + + Returns: + 是否保存成功 + """ + try: + # 序列化为JSON + json_data = json.dumps(data, ensure_ascii=False, indent=2) + + if compress: + # 压缩数据 + compressed_data = zlib.compress(json_data.encode('utf-8'), + self.serialization_params['compression_level']) + with open(filename, 'wb') as f: + f.write(compressed_data) + self.stats['data_compressed'] += len(compressed_data) + self.stats['data_uncompressed'] += len(json_data.encode('utf-8')) + else: + # 不压缩 + with open(filename, 'w', encoding='utf-8') as f: + f.write(json_data) + self.stats['data_uncompressed'] += len(json_data.encode('utf-8')) + + return True + + except Exception as e: + print(f"✗ JSON格式地形保存失败: {e}") + return False + + def _save_pickle(self, filename: str, data: Dict[str, Any], compress: bool) -> bool: + """ + 以Pickle格式保存地形数据 + + Args: + filename: 文件名 + data: 地形数据 + compress: 是否压缩 + + Returns: + 是否保存成功 + """ + try: + # 序列化为Pickle + pickle_data = pickle.dumps(data) + + if compress: + # 压缩数据 + compressed_data = zlib.compress(pickle_data, + self.serialization_params['compression_level']) + with open(filename, 'wb') as f: + f.write(compressed_data) + self.stats['data_compressed'] += len(compressed_data) + self.stats['data_uncompressed'] += len(pickle_data) + else: + # 不压缩 + with open(filename, 'wb') as f: + f.write(pickle_data) + self.stats['data_uncompressed'] += len(pickle_data) + + return True + + except Exception as e: + print(f"✗ Pickle格式地形保存失败: {e}") + return False + + def _save_binary(self, filename: str, data: Dict[str, Any], compress: bool) -> bool: + """ + 以二进制格式保存地形数据 + + Args: + filename: 文件名 + data: 地形数据 + compress: 是否压缩 + + Returns: + 是否保存成功 + """ + try: + # 转换为二进制格式 + binary_data = self._convert_to_binary(data) + + if compress: + # 压缩数据 + compressed_data = zlib.compress(binary_data, + self.serialization_params['compression_level']) + with open(filename, 'wb') as f: + f.write(compressed_data) + self.stats['data_compressed'] += len(compressed_data) + self.stats['data_uncompressed'] += len(binary_data) + else: + # 不压缩 + with open(filename, 'wb') as f: + f.write(binary_data) + self.stats['data_uncompressed'] += len(binary_data) + + return True + + except Exception as e: + print(f"✗ 二进制格式地形保存失败: {e}") + return False + + def _save_custom(self, filename: str, data: Dict[str, Any], compress: bool) -> bool: + """ + 以自定义格式保存地形数据 + + Args: + filename: 文件名 + data: 地形数据 + compress: 是否压缩 + + Returns: + 是否保存成功 + """ + try: + # 自定义格式保存(这里简化为JSON格式) + return self._save_json(filename, data, compress) + except Exception as e: + print(f"✗ 自定义格式地形保存失败: {e}") + return False + + def _load_json(self, filename: str) -> Optional[Dict[str, Any]]: + """ + 从JSON格式加载地形数据 + + Args: + filename: 文件名 + + Returns: + 地形数据字典或None + """ + try: + # 读取文件 + with open(filename, 'rb') as f: + file_data = f.read() + + # 检查是否压缩 + try: + # 尝试解压 + decompressed_data = zlib.decompress(file_data) + json_data = decompressed_data.decode('utf-8') + self.stats['data_compressed'] += len(file_data) + self.stats['data_uncompressed'] += len(decompressed_data) + except zlib.error: + # 未压缩的JSON + json_data = file_data.decode('utf-8') + self.stats['data_uncompressed'] += len(file_data) + + # 解析JSON + data = json.loads(json_data) + return data + + except Exception as e: + print(f"✗ JSON格式地形加载失败: {e}") + return None + + def _load_pickle(self, filename: str) -> Optional[Dict[str, Any]]: + """ + 从Pickle格式加载地形数据 + + Args: + filename: 文件名 + + Returns: + 地形数据字典或None + """ + try: + # 读取文件 + with open(filename, 'rb') as f: + file_data = f.read() + + # 检查是否压缩 + try: + # 尝试解压 + decompressed_data = zlib.decompress(file_data) + pickle_data = decompressed_data + self.stats['data_compressed'] += len(file_data) + self.stats['data_uncompressed'] += len(decompressed_data) + except zlib.error: + # 未压缩的Pickle + pickle_data = file_data + self.stats['data_uncompressed'] += len(file_data) + + # 解析Pickle + data = pickle.loads(pickle_data) + return data + + except Exception as e: + print(f"✗ Pickle格式地形加载失败: {e}") + return None + + def _load_binary(self, filename: str) -> Optional[Dict[str, Any]]: + """ + 从二进制格式加载地形数据 + + Args: + filename: 文件名 + + Returns: + 地形数据字典或None + """ + try: + # 读取文件 + with open(filename, 'rb') as f: + file_data = f.read() + + # 检查是否压缩 + try: + # 尝试解压 + decompressed_data = zlib.decompress(file_data) + binary_data = decompressed_data + self.stats['data_compressed'] += len(file_data) + self.stats['data_uncompressed'] += len(decompressed_data) + except zlib.error: + # 未压缩的二进制数据 + binary_data = file_data + self.stats['data_uncompressed'] += len(file_data) + + # 解析二进制数据 + data = self._convert_from_binary(binary_data) + return data + + except Exception as e: + print(f"✗ 二进制格式地形加载失败: {e}") + return None + + def _load_custom(self, filename: str) -> Optional[Dict[str, Any]]: + """ + 从自定义格式加载地形数据 + + Args: + filename: 文件名 + + Returns: + 地形数据字典或None + """ + try: + # 自定义格式加载(这里简化为JSON格式) + return self._load_json(filename) + except Exception as e: + print(f"✗ 自定义格式地形加载失败: {e}") + return None + + def _convert_to_binary(self, data: Dict[str, Any]) -> bytes: + """ + 将数据转换为二进制格式 + + Args: + data: 数据字典 + + Returns: + 二进制数据 + """ + try: + # 这里简化为使用Pickle作为二进制格式 + return pickle.dumps(data) + except Exception as e: + print(f"✗ 数据转换为二进制失败: {e}") + return b'' + + def _convert_from_binary(self, binary_data: bytes) -> Dict[str, Any]: + """ + 从二进制数据转换为字典 + + Args: + binary_data: 二进制数据 + + Returns: + 数据字典 + """ + try: + # 这里简化为使用Pickle作为二进制格式 + return pickle.loads(binary_data) + except Exception as e: + print(f"✗ 二进制数据转换失败: {e}") + return {} + + def _detect_format(self, filename: str) -> str: + """ + 检测文件格式 + + Args: + filename: 文件名 + + Returns: + 检测到的格式 + """ + try: + # 根据文件扩展名检测格式 + if filename.endswith('.json') or filename.endswith('.json.gz'): + return 'json' + elif filename.endswith('.pkl') or filename.endswith('.pkl.gz'): + return 'pickle' + elif filename.endswith('.bin') or filename.endswith('.bin.gz'): + return 'binary' + else: + # 默认使用JSON格式 + return self.serialization_params['default_format'] + + except Exception as e: + print(f"✗ 格式检测失败: {e}") + return self.serialization_params['default_format'] + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + # 更新平均时间 + if self.stats['terrains_saved'] > 0: + self.stats['average_save_time'] = self.stats['total_save_time'] / self.stats['terrains_saved'] + if self.stats['terrains_loaded'] > 0: + self.stats['average_load_time'] = self.stats['total_load_time'] / self.stats['terrains_loaded'] + + # 计算压缩率 + total_data = self.stats['data_compressed'] + self.stats['data_uncompressed'] + compression_ratio = 0.0 + if total_data > 0: + compression_ratio = self.stats['data_compressed'] / total_data if self.stats['data_compressed'] > 0 else 1.0 + + stats = self.stats.copy() + stats['compression_ratio'] = compression_ratio + return stats + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'terrains_saved': 0, + 'terrains_loaded': 0, + 'total_save_time': 0.0, + 'total_load_time': 0.0, + 'average_save_time': 0.0, + 'average_load_time': 0.0, + 'data_compressed': 0, + 'data_uncompressed': 0 + } + print("✓ 地形序列化器统计信息已重置") + + def set_serialization_parameters(self, params: Dict[str, Any]): + """ + 设置序列化参数 + + Args: + params: 参数字典 + """ + self.serialization_params.update(params) + print(f"✓ 序列化参数已更新: {self.serialization_params}") + + def export_terrain_to_image(self, heightmap: np.ndarray, filename: str) -> bool: + """ + 导出地形为图像文件 + + Args: + heightmap: 高度图数据 + filename: 文件名 + + Returns: + 是否导出成功 + """ + try: + print(f"✓ 开始导出地形到图像 {filename}...") + + # 确保高度图在0-1范围内 + normalized_heightmap = (heightmap - np.min(heightmap)) / (np.max(heightmap) - np.min(heightmap)) + + # 转换为0-255范围的整数 + image_data = (normalized_heightmap * 255).astype(np.uint8) + + # 保存为PNG文件 + try: + from PIL import Image + image = Image.fromarray(image_data, mode='L') + image.save(filename) + print("✓ 地形图像导出完成") + return True + except ImportError: + # 如果没有PIL,保存为原始二进制数据 + with open(filename + '.raw', 'wb') as f: + f.write(image_data.tobytes()) + print("✓ 地形数据导出完成(无PIL,保存为.raw文件)") + return True + + except Exception as e: + print(f"✗ 地形图像导出失败: {e}") + import traceback + traceback.print_exc() + return False + + def import_terrain_from_image(self, filename: str) -> Optional[np.ndarray]: + """ + 从图像文件导入地形 + + Args: + filename: 文件名 + + Returns: + 高度图数据或None + """ + try: + print(f"✓ 开始从图像 {filename} 导入地形...") + + # 读取图像文件 + try: + from PIL import Image + image = Image.open(filename) + # 转换为灰度图 + image = image.convert('L') + # 转换为numpy数组 + image_data = np.array(image, dtype=np.float32) + # 归一化到0-1范围 + heightmap = image_data / 255.0 + print("✓ 地形图像导入完成") + return heightmap + except ImportError: + # 如果没有PIL,尝试读取原始二进制数据 + if filename.endswith('.raw'): + with open(filename, 'rb') as f: + raw_data = f.read() + # 假设是512x512的图像 + image_data = np.frombuffer(raw_data, dtype=np.uint8) + heightmap = image_data.astype(np.float32) / 255.0 + # 重塑为方形 + size = int(np.sqrt(len(heightmap))) + heightmap = heightmap.reshape((size, size)) + print("✓ 地形数据导入完成(从.raw文件)") + return heightmap + else: + print("✗ 无法读取图像文件(需要PIL库)") + return None + + except Exception as e: + print(f"✗ 地形图像导入失败: {e}") + import traceback + traceback.print_exc() + return None + + def backup_terrain(self, filename: str) -> bool: + """ + 备份当前地形 + + Args: + filename: 备份文件名 + + Returns: + 是否备份成功 + """ + try: + if not self.enabled: + print("✗ 地形序列化器未启用") + return False + + # 获取当前地形数据 + if self.plugin.terrain_manager: + terrain_data = self.plugin.terrain_manager.get_terrain_data() + if terrain_data: + # 保存备份 + backup_filename = filename + '.backup' + success = self.save_terrain(backup_filename, terrain_data) + if success: + print(f"✓ 地形已备份到 {backup_filename}") + return True + else: + print("✗ 地形备份失败") + return False + else: + print("✗ 无法获取当前地形数据") + return False + else: + print("✗ 地形管理器不可用") + return False + + except Exception as e: + print(f"✗ 地形备份失败: {e}") + import traceback + traceback.print_exc() + return False + + def restore_terrain(self, filename: str) -> bool: + """ + 恢复地形备份 + + Args: + filename: 备份文件名 + + Returns: + 是否恢复成功 + """ + try: + if not self.enabled: + print("✗ 地形序列化器未启用") + return False + + # 加载备份数据 + backup_filename = filename + '.backup' + terrain_data = self.load_terrain(backup_filename) + + if terrain_data: + # 应用到地形管理器 + if self.plugin.terrain_manager: + # 这里应该调用地形管理器的方法来恢复数据 + # 由于地形管理器可能没有相应的方法,我们只打印信息 + print(f"✓ 地形已从 {backup_filename} 恢复") + print("注意:需要在地形管理器中实现实际的恢复逻辑") + return True + else: + print("✗ 地形管理器不可用") + return False + else: + print("✗ 无法加载备份数据") + return False + + except Exception as e: + print(f"✗ 地形恢复失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_file_info(self, filename: str) -> Dict[str, Any]: + """ + 获取文件信息 + + Args: + filename: 文件名 + + Returns: + 文件信息字典 + """ + try: + if not os.path.exists(filename): + return {'error': '文件不存在'} + + # 获取文件统计信息 + stat = os.stat(filename) + + file_info = { + 'filename': filename, + 'size': stat.st_size, + 'created': time.ctime(stat.st_ctime), + 'modified': time.ctime(stat.st_mtime), + 'accessed': time.ctime(stat.st_atime) + } + + # 尝试加载文件以获取地形信息 + try: + terrain_data = self.load_terrain(filename) + if terrain_data: + file_info['terrain_info'] = { + 'has_heightmap': 'heightmap' in terrain_data and terrain_data['heightmap'] is not None, + 'has_biome_map': 'biome_map' in terrain_data and terrain_data['biome_map'] is not None, + 'has_vegetation_map': 'vegetation_map' in terrain_data and terrain_data['vegetation_map'] is not None, + 'has_water_map': 'water_map' in terrain_data and terrain_data['water_map'] is not None + } + + if 'stats' in terrain_data: + file_info['terrain_stats'] = terrain_data['stats'] + + except Exception as e: + file_info['load_error'] = str(e) + + return file_info + + except Exception as e: + print(f"✗ 文件信息获取失败: {e}") + return {'error': str(e)} + + def list_terrain_files(self, directory: str = '.') -> List[str]: + """ + 列出目录中的地形文件 + + Args: + directory: 目录路径 + + Returns: + 地形文件列表 + """ + try: + terrain_files = [] + + # 支持的文件扩展名 + supported_extensions = ['.json', '.pkl', '.bin', '.raw'] + + # 遍历目录 + for filename in os.listdir(directory): + file_path = os.path.join(directory, filename) + if os.path.isfile(file_path): + # 检查文件扩展名 + _, ext = os.path.splitext(filename) + if ext in supported_extensions: + terrain_files.append(filename) + + return terrain_files + + except Exception as e: + print(f"✗ 地形文件列表获取失败: {e}") + return [] + + def delete_terrain_file(self, filename: str) -> bool: + """ + 删除地形文件 + + Args: + filename: 文件名 + + Returns: + 是否删除成功 + """ + try: + if os.path.exists(filename): + os.remove(filename) + print(f"✓ 地形文件 {filename} 已删除") + return True + else: + print(f"✗ 地形文件不存在: {filename}") + return False + + except Exception as e: + print(f"✗ 地形文件删除失败: {e}") + return False + + def compress_terrain_file(self, filename: str, output_filename: str = None) -> bool: + """ + 压缩地形文件 + + Args: + filename: 输入文件名 + output_filename: 输出文件名(如果为None,则在原文件名后添加.gz) + + Returns: + 是否压缩成功 + """ + try: + if not os.path.exists(filename): + print(f"✗ 文件不存在: {filename}") + return False + + if output_filename is None: + output_filename = filename + '.gz' + + # 读取原始文件 + with open(filename, 'rb') as f: + data = f.read() + + # 压缩数据 + compressed_data = zlib.compress(data, self.serialization_params['compression_level']) + + # 写入压缩文件 + with open(output_filename, 'wb') as f: + f.write(compressed_data) + + print(f"✓ 地形文件已压缩: {filename} -> {output_filename}") + return True + + except Exception as e: + print(f"✗ 地形文件压缩失败: {e}") + return False + + def decompress_terrain_file(self, filename: str, output_filename: str = None) -> bool: + """ + 解压地形文件 + + Args: + filename: 输入文件名(应该以.gz结尾) + output_filename: 输出文件名(如果为None,则移除.gz后缀) + + Returns: + 是否解压成功 + """ + try: + if not os.path.exists(filename): + print(f"✗ 文件不存在: {filename}") + return False + + if output_filename is None: + if filename.endswith('.gz'): + output_filename = filename[:-3] + else: + output_filename = filename + '.decompressed' + + # 读取压缩文件 + with open(filename, 'rb') as f: + compressed_data = f.read() + + # 解压数据 + try: + data = zlib.decompress(compressed_data) + except zlib.error as e: + print(f"✗ 文件解压失败: {e}") + return False + + # 写入解压文件 + with open(output_filename, 'wb') as f: + f.write(data) + + print(f"✓ 地形文件已解压: {filename} -> {output_filename}") + return True + + except Exception as e: + print(f"✗ 地形文件解压失败: {e}") + return False + + def validate_terrain_file(self, filename: str) -> bool: + """ + 验证地形文件完整性 + + Args: + filename: 文件名 + + Returns: + 文件是否完整 + """ + try: + if not os.path.exists(filename): + print(f"✗ 文件不存在: {filename}") + return False + + # 尝试加载文件 + terrain_data = self.load_terrain(filename) + + if terrain_data is None: + print(f"✗ 地形文件无效: {filename}") + return False + + # 检查必要数据是否存在 + required_keys = ['heightmap'] + missing_keys = [key for key in required_keys if key not in terrain_data or terrain_data[key] is None] + + if missing_keys: + print(f"✗ 地形文件缺少必要数据: {missing_keys}") + return False + + print(f"✓ 地形文件验证通过: {filename}") + return True + + except Exception as e: + print(f"✗ 地形文件验证失败: {e}") + return False + + def compare_terrain_files(self, filename1: str, filename2: str) -> Dict[str, Any]: + """ + 比较两个地形文件 + + Args: + filename1: 第一个文件名 + filename2: 第二个文件名 + + Returns: + 比较结果字典 + """ + try: + # 加载两个文件 + terrain1 = self.load_terrain(filename1) + terrain2 = self.load_terrain(filename2) + + if terrain1 is None or terrain2 is None: + return {'error': '无法加载一个或两个地形文件'} + + comparison = { + 'file1': filename1, + 'file2': filename2, + 'identical': False, + 'differences': [] + } + + # 比较高度图 + if ('heightmap' in terrain1 and terrain1['heightmap'] is not None and + 'heightmap' in terrain2 and terrain2['heightmap'] is not None): + heightmap1 = np.array(terrain1['heightmap']) + heightmap2 = np.array(terrain2['heightmap']) + + if heightmap1.shape == heightmap2.shape: + # 计算差异 + diff = np.abs(heightmap1 - heightmap2) + max_diff = np.max(diff) + mean_diff = np.mean(diff) + + comparison['heightmap_comparison'] = { + 'shapes_match': True, + 'max_difference': float(max_diff), + 'mean_difference': float(mean_diff), + 'identical': max_diff < 1e-6 + } + + if max_diff >= 1e-6: + comparison['differences'].append('heightmap') + else: + comparison['heightmap_comparison'] = { + 'shapes_match': False + } + comparison['differences'].append('heightmap_shape') + else: + comparison['heightmap_comparison'] = { + 'missing_data': True + } + + # 比较其他数据 + maps_to_compare = ['biome_map', 'vegetation_map', 'water_map'] + for map_name in maps_to_compare: + if (map_name in terrain1 and terrain1[map_name] is not None and + map_name in terrain2 and terrain2[map_name] is not None): + map1 = np.array(terrain1[map_name]) + map2 = np.array(terrain2[map_name]) + + if map1.shape == map2.shape: + if not np.array_equal(map1, map2): + comparison['differences'].append(map_name) + else: + comparison['differences'].append(f'{map_name}_shape') + + # 检查是否完全相同 + comparison['identical'] = len(comparison['differences']) == 0 + + return comparison + + except Exception as e: + print(f"✗ 地形文件比较失败: {e}") + return {'error': str(e)} + + def merge_terrain_files(self, filenames: List[str], output_filename: str) -> bool: + """ + 合并多个地形文件 + + Args: + filenames: 文件名列表 + output_filename: 输出文件名 + + Returns: + 是否合并成功 + """ + try: + if len(filenames) < 2: + print("✗ 至少需要两个文件才能合并") + return False + + print(f"✓ 开始合并 {len(filenames)} 个地形文件...") + + # 加载所有文件 + terrains = [] + for filename in filenames: + terrain = self.load_terrain(filename) + if terrain is None: + print(f"✗ 无法加载文件: {filename}") + return False + terrains.append(terrain) + + # 创建合并后的地形数据 + merged_terrain = {} + + # 合并高度图(取平均值) + heightmaps = [t.get('heightmap') for t in terrains if t.get('heightmap') is not None] + if heightmaps: + # 转换为numpy数组 + np_heightmaps = [np.array(h) for h in heightmaps] + # 确保所有高度图尺寸相同 + if all(h.shape == np_heightmaps[0].shape for h in np_heightmaps): + # 计算平均值 + merged_heightmap = np.mean(np_heightmaps, axis=0) + merged_terrain['heightmap'] = merged_heightmap + else: + print("✗ 高度图尺寸不匹配,无法合并") + return False + + # 合并其他数据(这里简化为取第一个文件的数据) + if terrains: + first_terrain = terrains[0] + for key in ['biome_map', 'vegetation_map', 'water_map', 'stats']: + if key in first_terrain and first_terrain[key] is not None: + merged_terrain[key] = first_terrain[key] + + # 保存合并后的地形 + success = self.save_terrain(output_filename, merged_terrain) + + if success: + print(f"✓ 地形文件合并完成: {output_filename}") + return True + else: + print("✗ 地形文件合并失败") + return False + + except Exception as e: + print(f"✗ 地形文件合并失败: {e}") + import traceback + traceback.print_exc() + return False + + def convert_terrain_format(self, input_filename: str, output_filename: str, + output_format: str) -> bool: + """ + 转换地形文件格式 + + Args: + input_filename: 输入文件名 + output_filename: 输出文件名 + output_format: 输出格式 + + Returns: + 是否转换成功 + """ + try: + # 验证输出格式 + if output_format not in self.supported_formats: + print(f"✗ 不支持的输出格式: {output_format}") + return False + + print(f"✓ 开始转换地形格式: {input_filename} -> {output_filename} ({output_format})") + + # 加载输入文件 + terrain_data = self.load_terrain(input_filename) + + if terrain_data is None: + print("✗ 无法加载输入文件") + return False + + # 保存为新格式 + original_format = self.serialization_params['default_format'] + self.serialization_params['default_format'] = output_format + + success = self.save_terrain(output_filename, terrain_data, format=output_format) + + # 恢复原始格式设置 + self.serialization_params['default_format'] = original_format + + if success: + print("✓ 地形格式转换完成") + return True + else: + print("✗ 地形格式转换失败") + return False + + except Exception as e: + print(f"✗ 地形格式转换失败: {e}") + import traceback + traceback.print_exc() return False \ No newline at end of file diff --git a/plugins/user/procedural_terrain_generation/utils/terrain_utils.py b/plugins/user/procedural_terrain_generation/utils/terrain_utils.py index 9e667403..9f556816 100644 --- a/plugins/user/procedural_terrain_generation/utils/terrain_utils.py +++ b/plugins/user/procedural_terrain_generation/utils/terrain_utils.py @@ -1,1651 +1,1651 @@ -""" -地形工具类 -提供各种地形处理和分析工具 -""" - -import numpy as np -import math -from typing import Dict, Any, List, Tuple, Optional -import json -import time - -class TerrainUtils: - """ - 地形工具类 - 提供各种地形处理和分析工具,包括地形分析、过滤、转换等功能 - """ - - def __init__(self, plugin): - """ - 初始化地形工具类 - - Args: - plugin: 程序化地形生成插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 工具配置 - self.tool_params = { - 'analysis_resolution': 64, - 'filter_radius': 3, - 'erosion_iterations': 10, - 'smoothing_iterations': 5, - 'slope_threshold': 0.3, - 'curvature_threshold': 0.1 - } - - # 分析结果缓存 - self.analysis_cache = {} - - # 统计信息 - self.stats = { - 'tools_used': 0, - 'total_processing_time': 0.0, - 'average_processing_time': 0.0, - 'cache_hits': 0, - 'cache_misses': 0 - } - - print("✓ 地形工具类已创建") - - def initialize(self) -> bool: - """ - 初始化地形工具类 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 地形工具类初始化完成") - return True - - except Exception as e: - print(f"✗ 地形工具类初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用地形工具类 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 地形工具类未初始化") - return False - - self.enabled = True - print("✓ 地形工具类已启用") - return True - - except Exception as e: - print(f"✗ 地形工具类启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用地形工具类""" - try: - self.enabled = False - print("✓ 地形工具类已禁用") - - except Exception as e: - print(f"✗ 地形工具类禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理地形工具类资源""" - try: - self.disable() - self.initialized = False - print("✓ 地形工具类资源已清理") - - except Exception as e: - print(f"✗ 地形工具类资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新地形工具类状态 - - Args: - dt: 时间增量 - """ - # 处理更新逻辑 - pass - - def analyze_terrain_slope(self, heightmap: np.ndarray) -> np.ndarray: - """ - 分析地形坡度 - - Args: - heightmap: 高度图数据 - - Returns: - 坡度图数据 (弧度) - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return np.zeros_like(heightmap, dtype=np.float32) - - print("✓ 开始分析地形坡度...") - - height, width = heightmap.shape - slope_map = np.zeros((height, width), dtype=np.float32) - - # 计算每个点的坡度 - for y in range(height): - for x in range(width): - # 计算梯度 - slope = self._calculate_slope_at_point(heightmap, x, y) - slope_map[y, x] = slope - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 地形坡度分析完成,耗时: {processing_time:.3f}秒") - return slope_map - - except Exception as e: - print(f"✗ 地形坡度分析失败: {e}") - import traceback - traceback.print_exc() - return np.zeros_like(heightmap, dtype=np.float32) - - def _calculate_slope_at_point(self, heightmap: np.ndarray, x: int, y: int) -> float: - """ - 计算指定点的坡度 - - Args: - heightmap: 高度图数据 - x: X坐标 - y: Y坐标 - - Returns: - 坡度值 (弧度) - """ - try: - height, width = heightmap.shape - - # 计算梯度 - dx = 0.0 - dy = 0.0 - - # X方向梯度 - if x > 0 and x < width - 1: - dx = (heightmap[y, x + 1] - heightmap[y, x - 1]) / 2.0 - elif x == 0: - dx = heightmap[y, x + 1] - heightmap[y, x] - else: - dx = heightmap[y, x] - heightmap[y, x - 1] - - # Y方向梯度 - if y > 0 and y < height - 1: - dy = (heightmap[y + 1, x] - heightmap[y - 1, x]) / 2.0 - elif y == 0: - dy = heightmap[y + 1, x] - heightmap[y, x] - else: - dy = heightmap[y, x] - heightmap[y - 1, x] - - # 计算坡度(梯度的模) - gradient_magnitude = math.sqrt(dx * dx + dy * dy) - slope = math.atan(gradient_magnitude) - - return slope - - except Exception as e: - print(f"✗ 点坡度计算失败: {e}") - return 0.0 - - def analyze_terrain_curvature(self, heightmap: np.ndarray) -> np.ndarray: - """ - 分析地形曲率 - - Args: - heightmap: 高度图数据 - - Returns: - 曲率图数据 - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return np.zeros_like(heightmap, dtype=np.float32) - - print("✓ 开始分析地形曲率...") - - height, width = heightmap.shape - curvature_map = np.zeros((height, width), dtype=np.float32) - - # 计算每个点的曲率 - for y in range(1, height - 1): - for x in range(1, width - 1): - # 使用拉普拉斯算子计算曲率 - center = heightmap[y, x] - laplacian = ( - heightmap[y-1, x] + heightmap[y+1, x] + - heightmap[y, x-1] + heightmap[y, x+1] - 4 * center - ) - curvature_map[y, x] = laplacian - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 地形曲率分析完成,耗时: {processing_time:.3f}秒") - return curvature_map - - except Exception as e: - print(f"✗ 地形曲率分析失败: {e}") - import traceback - traceback.print_exc() - return np.zeros_like(heightmap, dtype=np.float32) - - def analyze_terrain_aspect(self, heightmap: np.ndarray) -> np.ndarray: - """ - 分析地形坡向 - - Args: - heightmap: 高度图数据 - - Returns: - 坡向图数据 (弧度) - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return np.zeros_like(heightmap, dtype=np.float32) - - print("✓ 开始分析地形坡向...") - - height, width = heightmap.shape - aspect_map = np.zeros((height, width), dtype=np.float32) - - # 计算每个点的坡向 - for y in range(1, height - 1): - for x in range(1, width - 1): - # 计算梯度 - dx = (heightmap[y, x + 1] - heightmap[y, x - 1]) / 2.0 - dy = (heightmap[y + 1, x] - heightmap[y - 1, x]) / 2.0 - - # 计算坡向 - if dx == 0 and dy == 0: - aspect = 0.0 - else: - aspect = math.atan2(dy, dx) - # 转换为0-2π范围 - if aspect < 0: - aspect += 2 * math.pi - - aspect_map[y, x] = aspect - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 地形坡向分析完成,耗时: {processing_time:.3f}秒") - return aspect_map - - except Exception as e: - print(f"✗ 地形坡向分析失败: {e}") - import traceback - traceback.print_exc() - return np.zeros_like(heightmap, dtype=np.float32) - - def analyze_terrain_rugosity(self, heightmap: np.ndarray, window_size: int = 3) -> np.ndarray: - """ - 分析地形粗糙度 - - Args: - heightmap: 高度图数据 - window_size: 窗口大小 - - Returns: - 粗糙度图数据 - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return np.zeros_like(heightmap, dtype=np.float32) - - print("✓ 开始分析地形粗糙度...") - - height, width = heightmap.shape - rugosity_map = np.zeros((height, width), dtype=np.float32) - - half_window = window_size // 2 - - # 计算每个点的粗糙度 - for y in range(half_window, height - half_window): - for x in range(half_window, width - half_window): - # 计算局部窗口内的标准差 - window = heightmap[y-half_window:y+half_window+1, x-half_window:x+half_window+1] - rugosity = np.std(window) - rugosity_map[y, x] = rugosity - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 地形粗糙度分析完成,耗时: {processing_time:.3f}秒") - return rugosity_map - - except Exception as e: - print(f"✗ 地形粗糙度分析失败: {e}") - import traceback - traceback.print_exc() - return np.zeros_like(heightmap, dtype=np.float32) - - def smooth_terrain(self, heightmap: np.ndarray, iterations: int = None) -> np.ndarray: - """ - 平滑地形 - - Args: - heightmap: 高度图数据 - iterations: 平滑迭代次数 - - Returns: - 平滑后的高度图数据 - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return heightmap - - if iterations is None: - iterations = self.tool_params['smoothing_iterations'] - - print(f"✓ 开始平滑地形 ({iterations} 次迭代)...") - - smoothed = heightmap.copy() - height, width = smoothed.shape - - # 多次平滑迭代 - for iteration in range(iterations): - new_heightmap = smoothed.copy() - - # 应用3x3平均滤波器 - for y in range(1, height - 1): - for x in range(1, width - 1): - # 计算3x3邻域的平均值 - avg_value = ( - smoothed[y-1, x-1] + smoothed[y-1, x] + smoothed[y-1, x+1] + - smoothed[y, x-1] + smoothed[y, x] + smoothed[y, x+1] + - smoothed[y+1, x-1] + smoothed[y+1, x] + smoothed[y+1, x+1] - ) / 9.0 - - new_heightmap[y, x] = avg_value - - smoothed = new_heightmap - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 地形平滑完成,耗时: {processing_time:.3f}秒") - return smoothed - - except Exception as e: - print(f"✗ 地形平滑失败: {e}") - import traceback - traceback.print_exc() - return heightmap - - def filter_terrain(self, heightmap: np.ndarray, filter_type: str = 'gaussian', - radius: int = None) -> np.ndarray: - """ - 过滤地形 - - Args: - heightmap: 高度图数据 - filter_type: 滤波器类型 ('gaussian', 'median', 'bilateral') - radius: 滤波器半径 - - Returns: - 过滤后的高度图数据 - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return heightmap - - if radius is None: - radius = self.tool_params['filter_radius'] - - print(f"✓ 开始{filter_type}滤波地形 (半径: {radius})...") - - filtered = heightmap.copy() - height, width = filtered.shape - kernel_size = radius * 2 + 1 - - if filter_type == 'gaussian': - filtered = self._apply_gaussian_filter(filtered, radius) - elif filter_type == 'median': - filtered = self._apply_median_filter(filtered, radius) - elif filter_type == 'bilateral': - filtered = self._apply_bilateral_filter(filtered, radius) - else: - print(f"✗ 不支持的滤波器类型: {filter_type}") - return heightmap - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 地形{filter_type}滤波完成,耗时: {processing_time:.3f}秒") - return filtered - - except Exception as e: - print(f"✗ 地形滤波失败: {e}") - import traceback - traceback.print_exc() - return heightmap - - def _apply_gaussian_filter(self, heightmap: np.ndarray, radius: int) -> np.ndarray: - """应用高斯滤波""" - try: - filtered = heightmap.copy() - height, width = filtered.shape - kernel_size = radius * 2 + 1 - - # 创建高斯核 - kernel = np.zeros((kernel_size, kernel_size), dtype=np.float32) - sigma = radius / 3.0 - for y in range(kernel_size): - for x in range(kernel_size): - dx = x - radius - dy = y - radius - kernel[y, x] = math.exp(-(dx*dx + dy*dy) / (2 * sigma * sigma)) - kernel = kernel / np.sum(kernel) - - # 应用滤波 - for y in range(radius, height - radius): - for x in range(radius, width - radius): - value = 0.0 - for ky in range(kernel_size): - for kx in range(kernel_size): - ny = y + ky - radius - nx = x + kx - radius - value += heightmap[ny, nx] * kernel[ky, kx] - filtered[y, x] = value - - return filtered - - except Exception as e: - print(f"✗ 高斯滤波应用失败: {e}") - return heightmap - - def _apply_median_filter(self, heightmap: np.ndarray, radius: int) -> np.ndarray: - """应用中值滤波""" - try: - filtered = heightmap.copy() - height, width = filtered.shape - kernel_size = radius * 2 + 1 - - # 应用滤波 - for y in range(radius, height - radius): - for x in range(radius, width - radius): - values = [] - for ky in range(kernel_size): - for kx in range(kernel_size): - ny = y + ky - radius - nx = x + kx - radius - values.append(heightmap[ny, nx]) - filtered[y, x] = np.median(values) - - return filtered - - except Exception as e: - print(f"✗ 中值滤波应用失败: {e}") - return heightmap - - def _apply_bilateral_filter(self, heightmap: np.ndarray, radius: int) -> np.ndarray: - """应用双边滤波""" - try: - filtered = heightmap.copy() - height, width = filtered.shape - kernel_size = radius * 2 + 1 - - # 双边滤波参数 - spatial_sigma = radius / 3.0 - intensity_sigma = 0.1 - - # 应用滤波 - for y in range(radius, height - radius): - for x in range(radius, width - radius): - weighted_sum = 0.0 - weight_sum = 0.0 - center_value = heightmap[y, x] - - for ky in range(kernel_size): - for kx in range(kernel_size): - ny = y + ky - radius - nx = x + kx - radius - neighbor_value = heightmap[ny, nx] - - # 空间权重 - dx = kx - radius - dy = ky - radius - spatial_weight = math.exp(-(dx*dx + dy*dy) / (2 * spatial_sigma * spatial_sigma)) - - # 强度权重 - intensity_diff = abs(neighbor_value - center_value) - intensity_weight = math.exp(-(intensity_diff * intensity_diff) / (2 * intensity_sigma * intensity_sigma)) - - # 总权重 - weight = spatial_weight * intensity_weight - weighted_sum += neighbor_value * weight - weight_sum += weight - - if weight_sum > 0: - filtered[y, x] = weighted_sum / weight_sum - - return filtered - - except Exception as e: - print(f"✗ 双边滤波应用失败: {e}") - return heightmap - - def normalize_heightmap(self, heightmap: np.ndarray, min_value: float = 0.0, - max_value: float = 1.0) -> np.ndarray: - """ - 归一化高度图 - - Args: - heightmap: 高度图数据 - min_value: 最小值 - max_value: 最大值 - - Returns: - 归一化后的高度图数据 - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return heightmap - - print("✓ 开始归一化高度图...") - - # 计算当前范围 - current_min = np.min(heightmap) - current_max = np.max(heightmap) - - if current_max == current_min: - # 如果所有值相同,返回常量图 - normalized = np.full_like(heightmap, (min_value + max_value) / 2.0) - else: - # 归一化到0-1范围 - normalized_01 = (heightmap - current_min) / (current_max - current_min) - # 缩放到指定范围 - normalized = normalized_01 * (max_value - min_value) + min_value - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 高度图归一化完成,耗时: {processing_time:.3f}秒") - return normalized - - except Exception as e: - print(f"✗ 高度图归一化失败: {e}") - import traceback - traceback.print_exc() - return heightmap - - def invert_heightmap(self, heightmap: np.ndarray) -> np.ndarray: - """ - 反转高度图 - - Args: - heightmap: 高度图数据 - - Returns: - 反转后的高度图数据 - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return heightmap - - print("✓ 开始反转高度图...") - - # 反转高度图 - inverted = 1.0 - heightmap - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 高度图反转完成,耗时: {processing_time:.3f}秒") - return inverted - - except Exception as e: - print(f"✗ 高度图反转失败: {e}") - import traceback - traceback.print_exc() - return heightmap - - def blend_heightmaps(self, heightmaps: List[np.ndarray], weights: List[float] = None) -> np.ndarray: - """ - 混合多个高度图 - - Args: - heightmaps: 高度图列表 - weights: 权重列表(如果为None,则平均分配权重) - - Returns: - 混合后的高度图数据 - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return np.array([]) - - if not heightmaps: - print("✗ 高度图列表为空") - return np.array([]) - - if len(heightmaps) == 1: - return heightmaps[0].copy() - - print(f"✓ 开始混合 {len(heightmaps)} 个高度图...") - - # 确保所有高度图尺寸相同 - base_shape = heightmaps[0].shape - for hm in heightmaps: - if hm.shape != base_shape: - print("✗ 所有高度图必须具有相同的尺寸") - return heightmaps[0].copy() - - # 处理权重 - if weights is None: - weights = [1.0 / len(heightmaps)] * len(heightmaps) - elif len(weights) != len(heightmaps): - print("✗ 权重数量必须与高度图数量相同") - return heightmaps[0].copy() - - # 归一化权重 - total_weight = sum(weights) - if total_weight > 0: - weights = [w / total_weight for w in weights] - - # 混合高度图 - blended = np.zeros(base_shape, dtype=np.float32) - for i, (hm, weight) in enumerate(zip(heightmaps, weights)): - blended += hm * weight - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 高度图混合完成,耗时: {processing_time:.3f}秒") - return blended - - except Exception as e: - print(f"✗ 高度图混合失败: {e}") - import traceback - traceback.print_exc() - # 返回第一个高度图的副本 - return heightmaps[0].copy() if heightmaps else np.array([]) - - def generate_terrain_mask(self, heightmap: np.ndarray, condition: str, - threshold: float = 0.5) -> np.ndarray: - """ - 生成地形掩码 - - Args: - heightmap: 高度图数据 - condition: 条件 ('above', 'below', 'equal', 'between') - threshold: 阈值 - - Returns: - 掩码数据 (0-1范围) - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return np.zeros_like(heightmap, dtype=np.float32) - - print(f"✓ 开始生成地形掩码 (条件: {condition}, 阈值: {threshold})...") - - mask = np.zeros_like(heightmap, dtype=np.float32) - - if condition == 'above': - mask = np.where(heightmap > threshold, 1.0, 0.0) - elif condition == 'below': - mask = np.where(heightmap < threshold, 1.0, 0.0) - elif condition == 'equal': - mask = np.where(np.abs(heightmap - threshold) < 0.01, 1.0, 0.0) - elif condition == 'between': - # 假设阈值是一个范围 [min, max] - if isinstance(threshold, (list, tuple)) and len(threshold) == 2: - min_val, max_val = threshold - mask = np.where((heightmap >= min_val) & (heightmap <= max_val), 1.0, 0.0) - else: - mask = np.where(np.abs(heightmap - threshold) < 0.1, 1.0, 0.0) - else: - print(f"✗ 不支持的条件: {condition}") - return mask - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 地形掩码生成完成,耗时: {processing_time:.3f}秒") - return mask - - except Exception as e: - print(f"✗ 地形掩码生成失败: {e}") - import traceback - traceback.print_exc() - return np.zeros_like(heightmap, dtype=np.float32) - - def apply_terrain_mask(self, heightmap: np.ndarray, mask: np.ndarray, - operation: str = 'multiply') -> np.ndarray: - """ - 应用地形掩码 - - Args: - heightmap: 高度图数据 - mask: 掩码数据 - operation: 操作 ('multiply', 'add', 'subtract', 'replace') - - Returns: - 处理后的高度图数据 - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return heightmap - - # 确保掩码尺寸匹配 - if mask.shape != heightmap.shape: - print("✗ 掩码尺寸与高度图不匹配") - return heightmap - - print(f"✓ 开始应用地形掩码 (操作: {operation})...") - - result = heightmap.copy() - - if operation == 'multiply': - result = heightmap * mask - elif operation == 'add': - result = heightmap + mask - elif operation == 'subtract': - result = heightmap - mask - elif operation == 'replace': - # 只在掩码为1的地方替换 - result = np.where(mask > 0.5, mask, heightmap) - else: - print(f"✗ 不支持的操作: {operation}") - return heightmap - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 地形掩码应用完成,耗时: {processing_time:.3f}秒") - return result - - except Exception as e: - print(f"✗ 地形掩码应用失败: {e}") - import traceback - traceback.print_exc() - return heightmap - - def calculate_terrain_statistics(self, heightmap: np.ndarray) -> Dict[str, float]: - """ - 计算地形统计信息 - - Args: - heightmap: 高度图数据 - - Returns: - 统计信息字典 - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return {} - - print("✓ 开始计算地形统计信息...") - - # 计算基本统计信息 - stats = { - 'min_height': float(np.min(heightmap)), - 'max_height': float(np.max(heightmap)), - 'mean_height': float(np.mean(heightmap)), - 'std_height': float(np.std(heightmap)), - 'median_height': float(np.median(heightmap)), - 'total_area': float(heightmap.size), - 'width': heightmap.shape[1], - 'height': heightmap.shape[0] - } - - # 计算坡度统计 - slope_map = self.analyze_terrain_slope(heightmap) - stats['mean_slope'] = float(np.mean(slope_map)) - stats['max_slope'] = float(np.max(slope_map)) - stats['std_slope'] = float(np.std(slope_map)) - - # 计算曲率统计 - curvature_map = self.analyze_terrain_curvature(heightmap) - stats['mean_curvature'] = float(np.mean(curvature_map)) - stats['max_curvature'] = float(np.max(curvature_map)) - stats['std_curvature'] = float(np.std(curvature_map)) - - # 计算粗糙度统计 - rugosity_map = self.analyze_terrain_rugosity(heightmap) - stats['mean_rugosity'] = float(np.mean(rugosity_map)) - stats['max_rugosity'] = float(np.max(rugosity_map)) - stats['std_rugosity'] = float(np.std(rugosity_map)) - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 地形统计信息计算完成,耗时: {processing_time:.3f}秒") - return stats - - except Exception as e: - print(f"✗ 地形统计信息计算失败: {e}") - import traceback - traceback.print_exc() - return {} - - def _update_stats(self, processing_time: float): - """更新统计信息""" - self.stats['tools_used'] += 1 - self.stats['total_processing_time'] += processing_time - if self.stats['tools_used'] > 0: - self.stats['average_processing_time'] = ( - self.stats['total_processing_time'] / self.stats['tools_used'] - ) - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'tools_used': 0, - 'total_processing_time': 0.0, - 'average_processing_time': 0.0, - 'cache_hits': 0, - 'cache_misses': 0 - } - print("✓ 地形工具类统计信息已重置") - - def set_tool_parameters(self, params: Dict[str, Any]): - """ - 设置工具参数 - - Args: - params: 参数字典 - """ - self.tool_params.update(params) - print(f"✓ 地形工具参数已更新: {self.tool_params}") - - def export_terrain_data(self, heightmap: np.ndarray, filename: str, - metadata: Dict[str, Any] = None) -> bool: - """ - 导出地形数据 - - Args: - heightmap: 高度图数据 - filename: 文件名 - metadata: 元数据 - - Returns: - 是否导出成功 - """ - try: - print(f"✓ 开始导出地形数据到: {filename}") - - # 创建导出数据 - export_data = { - 'heightmap': heightmap.tolist(), - 'shape': heightmap.shape, - 'dtype': str(heightmap.dtype), - 'metadata': metadata or {}, - 'export_time': time.time() - } - - # 保存为JSON文件 - with open(filename, 'w', encoding='utf-8') as f: - json.dump(export_data, f, ensure_ascii=False, indent=2) - - print(f"✓ 地形数据导出完成") - return True - - except Exception as e: - print(f"✗ 地形数据导出失败: {e}") - import traceback - traceback.print_exc() - return False - - def import_terrain_data(self, filename: str) -> Optional[np.ndarray]: - """ - 导入地形数据 - - Args: - filename: 文件名 - - Returns: - 高度图数据或None - """ - try: - print(f"✓ 开始导入地形数据从: {filename}") - - # 读取JSON文件 - with open(filename, 'r', encoding='utf-8') as f: - import_data = json.load(f) - - # 恢复高度图数据 - heightmap_data = import_data['heightmap'] - heightmap = np.array(heightmap_data, dtype=np.float32) - - print(f"✓ 地形数据导入完成,尺寸: {heightmap.shape}") - return heightmap - - except Exception as e: - print(f"✗ 地形数据导入失败: {e}") - import traceback - traceback.print_exc() - return None - - def resample_heightmap(self, heightmap: np.ndarray, new_width: int, new_height: int) -> np.ndarray: - """ - 重采样高度图 - - Args: - heightmap: 原始高度图数据 - new_width: 新宽度 - new_height: 新高度 - - Returns: - 重采样后的高度图数据 - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return heightmap - - print(f"✓ 开始重采样高度图到 {new_width}x{new_height}...") - - old_height, old_width = heightmap.shape - - # 创建新的高度图 - resampled = np.zeros((new_height, new_width), dtype=np.float32) - - # 双线性插值重采样 - for y in range(new_height): - for x in range(new_width): - # 计算在原图中的对应位置 - old_x = x * (old_width - 1) / (new_width - 1) - old_y = y * (old_height - 1) / (new_height - 1) - - # 双线性插值 - resampled[y, x] = self._bilinear_interpolate(heightmap, old_x, old_y) - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 高度图重采样完成,耗时: {processing_time:.3f}秒") - return resampled - - except Exception as e: - print(f"✗ 高度图重采样失败: {e}") - import traceback - traceback.print_exc() - return heightmap - - def _bilinear_interpolate(self, heightmap: np.ndarray, x: float, y: float) -> float: - """ - 双线性插值 - - Args: - heightmap: 高度图数据 - x: X坐标 - y: Y坐标 - - Returns: - 插值结果 - """ - try: - height, width = heightmap.shape - - # 获取整数部分 - x0 = int(math.floor(x)) - y0 = int(math.floor(y)) - x1 = min(x0 + 1, width - 1) - y1 = min(y0 + 1, height - 1) - - # 获取小数部分 - fx = x - x0 - fy = y - y0 - - # 双线性插值 - top = heightmap[y0, x0] * (1 - fx) + heightmap[y0, x1] * fx - bottom = heightmap[y1, x0] * (1 - fx) + heightmap[y1, x1] * fx - result = top * (1 - fy) + bottom * fy - - return result - - except Exception as e: - print(f"✗ 双线性插值失败: {e}") - return 0.0 - - def generate_terrain_preview(self, heightmap: np.ndarray, width: int = 256, height: int = 256) -> np.ndarray: - """ - 生成地形预览图 - - Args: - heightmap: 高度图数据 - width: 预览图宽度 - height: 预览图高度 - - Returns: - 预览图数据 (RGB格式) - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return np.zeros((height, width, 3), dtype=np.uint8) - - print("✓ 开始生成地形预览图...") - - # 重采样到预览尺寸 - preview_heightmap = self.resample_heightmap(heightmap, width, height) - - # 创建RGB预览图 - preview = np.zeros((height, width, 3), dtype=np.uint8) - - # 根据高度生成颜色 - for y in range(height): - for x in range(width): - height_value = preview_heightmap[y, x] - - # 根据高度选择颜色 - if height_value < 0.2: - # 水域 - 蓝色 - r, g, b = 0, 0, int(128 + height_value * 127) - elif height_value < 0.3: - # 浅水/海滩 - 浅蓝色 - r, g, b = int(height_value * 255), int(height_value * 255), 255 - elif height_value < 0.5: - # 平原 - 绿色 - r, g, b = 0, int(100 + height_value * 155), 0 - elif height_value < 0.7: - # 丘陵 - 棕色 - r, g, b = int(139 * height_value), int(69 * height_value), int(19 * height_value) - elif height_value < 0.9: - # 山地 - 灰色 - r, g, b = int(100 + height_value * 155), int(100 + height_value * 155), int(100 + height_value * 155) - else: - # 雪山 - 白色 - r, g, b = 255, 255, 255 - - preview[y, x] = [r, g, b] - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 地形预览图生成完成,耗时: {processing_time:.3f}秒") - return preview - - except Exception as e: - print(f"✗ 地形预览图生成失败: {e}") - import traceback - traceback.print_exc() - return np.zeros((height, width, 3), dtype=np.uint8) - - def calculate_terrain_volume(self, heightmap: np.ndarray, cell_size: float = 1.0) -> float: - """ - 计算地形体积 - - Args: - heightmap: 高度图数据 - cell_size: 网格单元大小 - - Returns: - 体积值 - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return 0.0 - - print("✓ 开始计算地形体积...") - - # 计算总体积(假设每个网格单元是柱体) - volume = np.sum(heightmap) * cell_size * cell_size - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 地形体积计算完成,耗时: {processing_time:.3f}秒") - return float(volume) - - except Exception as e: - print(f"✗ 地形体积计算失败: {e}") - import traceback - traceback.print_exc() - return 0.0 - - def calculate_terrain_area(self, heightmap: np.ndarray, cell_size: float = 1.0) -> float: - """ - 计算地形表面积 - - Args: - heightmap: 高度图数据 - cell_size: 网格单元大小 - - Returns: - 表面积值 - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return 0.0 - - print("✓ 开始计算地形表面积...") - - height, width = heightmap.shape - total_area = 0.0 - - # 计算每个网格单元的表面积 - for y in range(height - 1): - for x in range(width - 1): - # 获取四个角点的高度 - h00 = heightmap[y, x] - h01 = heightmap[y, x + 1] - h10 = heightmap[y + 1, x] - h11 = heightmap[y + 1, x + 1] - - # 计算两个三角形的面积 - # 三角形1: (0,0,h00), (0,1,h01), (1,0,h10) - dx1 = cell_size - dy1 = 0.0 - dz1 = h01 - h00 - dv1 = np.array([dx1, dy1, dz1]) - - dx2 = 0.0 - dy2 = cell_size - dz2 = h10 - h00 - dv2 = np.array([dx2, dy2, dz2]) - - cross1 = np.cross(dv1, dv2) - area1 = 0.5 * np.linalg.norm(cross1) - - # 三角形2: (1,1,h11), (0,1,h01), (1,0,h10) - dx3 = 0.0 - dy3 = -cell_size - dz3 = h11 - h01 - dv3 = np.array([dx3, dy3, dz3]) - - dx4 = -cell_size - dy4 = 0.0 - dz4 = h11 - h10 - dv4 = np.array([dx4, dy4, dz4]) - - cross2 = np.cross(dv3, dv4) - area2 = 0.5 * np.linalg.norm(cross2) - - total_area += area1 + area2 - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 地形表面积计算完成,耗时: {processing_time:.3f}秒") - return total_area - - except Exception as e: - print(f"✗ 地形表面积计算失败: {e}") - import traceback - traceback.print_exc() - return 0.0 - - def detect_terrain_features(self, heightmap: np.ndarray) -> Dict[str, np.ndarray]: - """ - 检测地形特征 - - Args: - heightmap: 高度图数据 - - Returns: - 特征检测结果字典 - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return {} - - print("✓ 开始检测地形特征...") - - # 检测山峰 - peaks = self._detect_peaks(heightmap) - - # 检测山谷 - valleys = self._detect_valleys(heightmap) - - # 检测山脊 - ridges = self._detect_ridges(heightmap) - - # 检测河道 - rivers = self._detect_rivers(heightmap) - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 地形特征检测完成,耗时: {processing_time:.3f}秒") - return { - 'peaks': peaks, - 'valleys': valleys, - 'ridges': ridges, - 'rivers': rivers - } - - except Exception as e: - print(f"✗ 地形特征检测失败: {e}") - import traceback - traceback.print_exc() - return {} - - def _detect_peaks(self, heightmap: np.ndarray) -> np.ndarray: - """检测山峰""" - try: - height, width = heightmap.shape - peaks = np.zeros_like(heightmap, dtype=np.float32) - - # 简化的山峰检测 - for y in range(1, height - 1): - for x in range(1, width - 1): - center = heightmap[y, x] - is_peak = True - - # 检查是否比所有邻居都高 - for dy in [-1, 0, 1]: - for dx in [-1, 0, 1]: - if dy == 0 and dx == 0: - continue - if heightmap[y + dy, x + dx] >= center: - is_peak = False - break - if not is_peak: - break - - if is_peak and center > 0.7: # 高度阈值 - peaks[y, x] = center - - return peaks - - except Exception as e: - print(f"✗ 山峰检测失败: {e}") - return np.zeros_like(heightmap, dtype=np.float32) - - def _detect_valleys(self, heightmap: np.ndarray) -> np.ndarray: - """检测山谷""" - try: - height, width = heightmap.shape - valleys = np.zeros_like(heightmap, dtype=np.float32) - - # 简化的山谷检测 - for y in range(1, height - 1): - for x in range(1, width - 1): - center = heightmap[y, x] - is_valley = True - - # 检查是否比所有邻居都低 - for dy in [-1, 0, 1]: - for dx in [-1, 0, 1]: - if dy == 0 and dx == 0: - continue - if heightmap[y + dy, x + dx] <= center: - is_valley = False - break - if not is_valley: - break - - if is_valley and center < 0.3: # 高度阈值 - valleys[y, x] = 1.0 - center - - return valleys - - except Exception as e: - print(f"✗ 山谷检测失败: {e}") - return np.zeros_like(heightmap, dtype=np.float32) - - def _detect_ridges(self, heightmap: np.ndarray) -> np.ndarray: - """检测山脊""" - try: - # 简化的山脊检测 - slope_map = self.analyze_terrain_slope(heightmap) - ridge_map = np.zeros_like(heightmap, dtype=np.float32) - - # 山脊通常是坡度变化较大的地方 - ridge_map = np.where(slope_map > self.tool_params['slope_threshold'], slope_map, 0) - - return ridge_map - - except Exception as e: - print(f"✗ 山脊检测失败: {e}") - return np.zeros_like(heightmap, dtype=np.float32) - - def _detect_rivers(self, heightmap: np.ndarray) -> np.ndarray: - """检测河道""" - try: - # 简化的河道检测 - curvature_map = self.analyze_terrain_curvature(heightmap) - river_map = np.zeros_like(heightmap, dtype=np.float32) - - # 河道通常在负曲率区域(凹陷处) - river_map = np.where(curvature_map < -self.tool_params['curvature_threshold'], - np.abs(curvature_map), 0) - - return river_map - - except Exception as e: - print(f"✗ 河道检测失败: {e}") - return np.zeros_like(heightmap, dtype=np.float32) - - def fill_terrain_holes(self, heightmap: np.ndarray, hole_threshold: float = 0.01) -> np.ndarray: - """ - 填补地形空洞 - - Args: - heightmap: 高度图数据 - hole_threshold: 空洞阈值 - - Returns: - 处理后的高度图数据 - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return heightmap - - print("✓ 开始填补地形空洞...") - - filled = heightmap.copy() - height, width = filled.shape - - # 简化的空洞填补算法 - for y in range(1, height - 1): - for x in range(1, width - 1): - center = filled[y, x] - - # 如果中心点是空洞 - if center < hole_threshold: - # 计算邻居的平均值 - neighbors = [] - for dy in [-1, 0, 1]: - for dx in [-1, 0, 1]: - if dy == 0 and dx == 0: - continue - neighbor_value = filled[y + dy, x + dx] - if neighbor_value >= hole_threshold: # 只考虑非空洞邻居 - neighbors.append(neighbor_value) - - if neighbors: - filled[y, x] = np.mean(neighbors) - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 地形空洞填补完成,耗时: {processing_time:.3f}秒") - return filled - - except Exception as e: - print(f"✗ 地形空洞填补失败: {e}") - import traceback - traceback.print_exc() - return heightmap - - def clamp_heightmap(self, heightmap: np.ndarray, min_value: float = 0.0, - max_value: float = 1.0) -> np.ndarray: - """ - 限制高度图范围 - - Args: - heightmap: 高度图数据 - min_value: 最小值 - max_value: 最大值 - - Returns: - 处理后的高度图数据 - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return heightmap - - print("✓ 开始限制高度图范围...") - - # 限制范围 - clamped = np.clip(heightmap, min_value, max_value) - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 高度图范围限制完成,耗时: {processing_time:.3f}秒") - return clamped - - except Exception as e: - print(f"✗ 高度图范围限制失败: {e}") - import traceback - traceback.print_exc() - return heightmap - - def add_terrain_noise(self, heightmap: np.ndarray, noise_amount: float = 0.01, - seed: int = None) -> np.ndarray: - """ - 添加噪声到地形 - - Args: - heightmap: 高度图数据 - noise_amount: 噪声强度 - seed: 随机种子 - - Returns: - 处理后的高度图数据 - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return heightmap - - print("✓ 开始添加噪声到地形...") - - # 设置随机种子 - if seed is not None: - np.random.seed(seed) - - # 生成噪声 - noise = np.random.normal(0, noise_amount, heightmap.shape).astype(np.float32) - - # 添加噪声 - noisy = heightmap + noise - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 地形噪声添加完成,耗时: {processing_time:.3f}秒") - return noisy - - except Exception as e: - print(f"✗ 地形噪声添加失败: {e}") - import traceback - traceback.print_exc() - return heightmap - - def create_terrain_from_function(self, width: int, height: int, - function: callable) -> np.ndarray: - """ - 根据函数创建地形 - - Args: - width: 宽度 - height: 高度 - function: 生成函数 (x, y) -> height - - Returns: - 生成的高度图数据 - """ - try: - processing_start_time = time.time() - - if not self.enabled: - print("✗ 地形工具类未启用") - return np.zeros((height, width), dtype=np.float32) - - print("✓ 开始根据函数创建地形...") - - # 创建高度图 - heightmap = np.zeros((height, width), dtype=np.float32) - - # 应用函数 - for y in range(height): - for x in range(width): - # 将坐标标准化到[-1, 1]范围 - norm_x = (x / (width - 1)) * 2.0 - 1.0 - norm_y = (y / (height - 1)) * 2.0 - 1.0 - heightmap[y, x] = function(norm_x, norm_y) - - # 更新统计信息 - processing_time = time.time() - processing_start_time - self._update_stats(processing_time) - - print(f"✓ 函数地形创建完成,耗时: {processing_time:.3f}秒") - return heightmap - - except Exception as e: - print(f"✗ 函数地形创建失败: {e}") - import traceback - traceback.print_exc() - return np.zeros((height, width), dtype=np.float32) - - def cache_analysis_result(self, key: str, result: Any): - """ - 缓存分析结果 - - Args: - key: 缓存键 - result: 分析结果 - """ - try: - self.analysis_cache[key] = { - 'result': result, - 'timestamp': time.time() - } - print(f"✓ 分析结果已缓存: {key}") - except Exception as e: - print(f"✗ 分析结果缓存失败: {e}") - - def get_cached_analysis_result(self, key: str) -> Optional[Any]: - """ - 获取缓存的分析结果 - - Args: - key: 缓存键 - - Returns: - 缓存的分析结果或None - """ - try: - if key in self.analysis_cache: - self.stats['cache_hits'] += 1 - return self.analysis_cache[key]['result'] - else: - self.stats['cache_misses'] += 1 - return None - except Exception as e: - print(f"✗ 缓存分析结果获取失败: {e}") - return None - - def clear_analysis_cache(self): - """清空分析缓存""" - try: - self.analysis_cache.clear() - print("✓ 分析缓存已清空") - except Exception as e: - print(f"✗ 分析缓存清空失败: {e}") - - def get_cache_stats(self) -> Dict[str, int]: - """ - 获取缓存统计信息 - - Returns: - 缓存统计信息 - """ - return { - 'cache_size': len(self.analysis_cache), - 'cache_hits': self.stats['cache_hits'], - 'cache_misses': self.stats['cache_misses'], - 'hit_rate': self.stats['cache_hits'] / max(1, self.stats['cache_hits'] + self.stats['cache_misses']) +""" +地形工具类 +提供各种地形处理和分析工具 +""" + +import numpy as np +import math +from typing import Dict, Any, List, Tuple, Optional +import json +import time + +class TerrainUtils: + """ + 地形工具类 + 提供各种地形处理和分析工具,包括地形分析、过滤、转换等功能 + """ + + def __init__(self, plugin): + """ + 初始化地形工具类 + + Args: + plugin: 程序化地形生成插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 工具配置 + self.tool_params = { + 'analysis_resolution': 64, + 'filter_radius': 3, + 'erosion_iterations': 10, + 'smoothing_iterations': 5, + 'slope_threshold': 0.3, + 'curvature_threshold': 0.1 + } + + # 分析结果缓存 + self.analysis_cache = {} + + # 统计信息 + self.stats = { + 'tools_used': 0, + 'total_processing_time': 0.0, + 'average_processing_time': 0.0, + 'cache_hits': 0, + 'cache_misses': 0 + } + + print("✓ 地形工具类已创建") + + def initialize(self) -> bool: + """ + 初始化地形工具类 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 地形工具类初始化完成") + return True + + except Exception as e: + print(f"✗ 地形工具类初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用地形工具类 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 地形工具类未初始化") + return False + + self.enabled = True + print("✓ 地形工具类已启用") + return True + + except Exception as e: + print(f"✗ 地形工具类启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用地形工具类""" + try: + self.enabled = False + print("✓ 地形工具类已禁用") + + except Exception as e: + print(f"✗ 地形工具类禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理地形工具类资源""" + try: + self.disable() + self.initialized = False + print("✓ 地形工具类资源已清理") + + except Exception as e: + print(f"✗ 地形工具类资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新地形工具类状态 + + Args: + dt: 时间增量 + """ + # 处理更新逻辑 + pass + + def analyze_terrain_slope(self, heightmap: np.ndarray) -> np.ndarray: + """ + 分析地形坡度 + + Args: + heightmap: 高度图数据 + + Returns: + 坡度图数据 (弧度) + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return np.zeros_like(heightmap, dtype=np.float32) + + print("✓ 开始分析地形坡度...") + + height, width = heightmap.shape + slope_map = np.zeros((height, width), dtype=np.float32) + + # 计算每个点的坡度 + for y in range(height): + for x in range(width): + # 计算梯度 + slope = self._calculate_slope_at_point(heightmap, x, y) + slope_map[y, x] = slope + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 地形坡度分析完成,耗时: {processing_time:.3f}秒") + return slope_map + + except Exception as e: + print(f"✗ 地形坡度分析失败: {e}") + import traceback + traceback.print_exc() + return np.zeros_like(heightmap, dtype=np.float32) + + def _calculate_slope_at_point(self, heightmap: np.ndarray, x: int, y: int) -> float: + """ + 计算指定点的坡度 + + Args: + heightmap: 高度图数据 + x: X坐标 + y: Y坐标 + + Returns: + 坡度值 (弧度) + """ + try: + height, width = heightmap.shape + + # 计算梯度 + dx = 0.0 + dy = 0.0 + + # X方向梯度 + if x > 0 and x < width - 1: + dx = (heightmap[y, x + 1] - heightmap[y, x - 1]) / 2.0 + elif x == 0: + dx = heightmap[y, x + 1] - heightmap[y, x] + else: + dx = heightmap[y, x] - heightmap[y, x - 1] + + # Y方向梯度 + if y > 0 and y < height - 1: + dy = (heightmap[y + 1, x] - heightmap[y - 1, x]) / 2.0 + elif y == 0: + dy = heightmap[y + 1, x] - heightmap[y, x] + else: + dy = heightmap[y, x] - heightmap[y - 1, x] + + # 计算坡度(梯度的模) + gradient_magnitude = math.sqrt(dx * dx + dy * dy) + slope = math.atan(gradient_magnitude) + + return slope + + except Exception as e: + print(f"✗ 点坡度计算失败: {e}") + return 0.0 + + def analyze_terrain_curvature(self, heightmap: np.ndarray) -> np.ndarray: + """ + 分析地形曲率 + + Args: + heightmap: 高度图数据 + + Returns: + 曲率图数据 + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return np.zeros_like(heightmap, dtype=np.float32) + + print("✓ 开始分析地形曲率...") + + height, width = heightmap.shape + curvature_map = np.zeros((height, width), dtype=np.float32) + + # 计算每个点的曲率 + for y in range(1, height - 1): + for x in range(1, width - 1): + # 使用拉普拉斯算子计算曲率 + center = heightmap[y, x] + laplacian = ( + heightmap[y-1, x] + heightmap[y+1, x] + + heightmap[y, x-1] + heightmap[y, x+1] - 4 * center + ) + curvature_map[y, x] = laplacian + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 地形曲率分析完成,耗时: {processing_time:.3f}秒") + return curvature_map + + except Exception as e: + print(f"✗ 地形曲率分析失败: {e}") + import traceback + traceback.print_exc() + return np.zeros_like(heightmap, dtype=np.float32) + + def analyze_terrain_aspect(self, heightmap: np.ndarray) -> np.ndarray: + """ + 分析地形坡向 + + Args: + heightmap: 高度图数据 + + Returns: + 坡向图数据 (弧度) + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return np.zeros_like(heightmap, dtype=np.float32) + + print("✓ 开始分析地形坡向...") + + height, width = heightmap.shape + aspect_map = np.zeros((height, width), dtype=np.float32) + + # 计算每个点的坡向 + for y in range(1, height - 1): + for x in range(1, width - 1): + # 计算梯度 + dx = (heightmap[y, x + 1] - heightmap[y, x - 1]) / 2.0 + dy = (heightmap[y + 1, x] - heightmap[y - 1, x]) / 2.0 + + # 计算坡向 + if dx == 0 and dy == 0: + aspect = 0.0 + else: + aspect = math.atan2(dy, dx) + # 转换为0-2π范围 + if aspect < 0: + aspect += 2 * math.pi + + aspect_map[y, x] = aspect + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 地形坡向分析完成,耗时: {processing_time:.3f}秒") + return aspect_map + + except Exception as e: + print(f"✗ 地形坡向分析失败: {e}") + import traceback + traceback.print_exc() + return np.zeros_like(heightmap, dtype=np.float32) + + def analyze_terrain_rugosity(self, heightmap: np.ndarray, window_size: int = 3) -> np.ndarray: + """ + 分析地形粗糙度 + + Args: + heightmap: 高度图数据 + window_size: 窗口大小 + + Returns: + 粗糙度图数据 + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return np.zeros_like(heightmap, dtype=np.float32) + + print("✓ 开始分析地形粗糙度...") + + height, width = heightmap.shape + rugosity_map = np.zeros((height, width), dtype=np.float32) + + half_window = window_size // 2 + + # 计算每个点的粗糙度 + for y in range(half_window, height - half_window): + for x in range(half_window, width - half_window): + # 计算局部窗口内的标准差 + window = heightmap[y-half_window:y+half_window+1, x-half_window:x+half_window+1] + rugosity = np.std(window) + rugosity_map[y, x] = rugosity + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 地形粗糙度分析完成,耗时: {processing_time:.3f}秒") + return rugosity_map + + except Exception as e: + print(f"✗ 地形粗糙度分析失败: {e}") + import traceback + traceback.print_exc() + return np.zeros_like(heightmap, dtype=np.float32) + + def smooth_terrain(self, heightmap: np.ndarray, iterations: int = None) -> np.ndarray: + """ + 平滑地形 + + Args: + heightmap: 高度图数据 + iterations: 平滑迭代次数 + + Returns: + 平滑后的高度图数据 + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return heightmap + + if iterations is None: + iterations = self.tool_params['smoothing_iterations'] + + print(f"✓ 开始平滑地形 ({iterations} 次迭代)...") + + smoothed = heightmap.copy() + height, width = smoothed.shape + + # 多次平滑迭代 + for iteration in range(iterations): + new_heightmap = smoothed.copy() + + # 应用3x3平均滤波器 + for y in range(1, height - 1): + for x in range(1, width - 1): + # 计算3x3邻域的平均值 + avg_value = ( + smoothed[y-1, x-1] + smoothed[y-1, x] + smoothed[y-1, x+1] + + smoothed[y, x-1] + smoothed[y, x] + smoothed[y, x+1] + + smoothed[y+1, x-1] + smoothed[y+1, x] + smoothed[y+1, x+1] + ) / 9.0 + + new_heightmap[y, x] = avg_value + + smoothed = new_heightmap + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 地形平滑完成,耗时: {processing_time:.3f}秒") + return smoothed + + except Exception as e: + print(f"✗ 地形平滑失败: {e}") + import traceback + traceback.print_exc() + return heightmap + + def filter_terrain(self, heightmap: np.ndarray, filter_type: str = 'gaussian', + radius: int = None) -> np.ndarray: + """ + 过滤地形 + + Args: + heightmap: 高度图数据 + filter_type: 滤波器类型 ('gaussian', 'median', 'bilateral') + radius: 滤波器半径 + + Returns: + 过滤后的高度图数据 + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return heightmap + + if radius is None: + radius = self.tool_params['filter_radius'] + + print(f"✓ 开始{filter_type}滤波地形 (半径: {radius})...") + + filtered = heightmap.copy() + height, width = filtered.shape + kernel_size = radius * 2 + 1 + + if filter_type == 'gaussian': + filtered = self._apply_gaussian_filter(filtered, radius) + elif filter_type == 'median': + filtered = self._apply_median_filter(filtered, radius) + elif filter_type == 'bilateral': + filtered = self._apply_bilateral_filter(filtered, radius) + else: + print(f"✗ 不支持的滤波器类型: {filter_type}") + return heightmap + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 地形{filter_type}滤波完成,耗时: {processing_time:.3f}秒") + return filtered + + except Exception as e: + print(f"✗ 地形滤波失败: {e}") + import traceback + traceback.print_exc() + return heightmap + + def _apply_gaussian_filter(self, heightmap: np.ndarray, radius: int) -> np.ndarray: + """应用高斯滤波""" + try: + filtered = heightmap.copy() + height, width = filtered.shape + kernel_size = radius * 2 + 1 + + # 创建高斯核 + kernel = np.zeros((kernel_size, kernel_size), dtype=np.float32) + sigma = radius / 3.0 + for y in range(kernel_size): + for x in range(kernel_size): + dx = x - radius + dy = y - radius + kernel[y, x] = math.exp(-(dx*dx + dy*dy) / (2 * sigma * sigma)) + kernel = kernel / np.sum(kernel) + + # 应用滤波 + for y in range(radius, height - radius): + for x in range(radius, width - radius): + value = 0.0 + for ky in range(kernel_size): + for kx in range(kernel_size): + ny = y + ky - radius + nx = x + kx - radius + value += heightmap[ny, nx] * kernel[ky, kx] + filtered[y, x] = value + + return filtered + + except Exception as e: + print(f"✗ 高斯滤波应用失败: {e}") + return heightmap + + def _apply_median_filter(self, heightmap: np.ndarray, radius: int) -> np.ndarray: + """应用中值滤波""" + try: + filtered = heightmap.copy() + height, width = filtered.shape + kernel_size = radius * 2 + 1 + + # 应用滤波 + for y in range(radius, height - radius): + for x in range(radius, width - radius): + values = [] + for ky in range(kernel_size): + for kx in range(kernel_size): + ny = y + ky - radius + nx = x + kx - radius + values.append(heightmap[ny, nx]) + filtered[y, x] = np.median(values) + + return filtered + + except Exception as e: + print(f"✗ 中值滤波应用失败: {e}") + return heightmap + + def _apply_bilateral_filter(self, heightmap: np.ndarray, radius: int) -> np.ndarray: + """应用双边滤波""" + try: + filtered = heightmap.copy() + height, width = filtered.shape + kernel_size = radius * 2 + 1 + + # 双边滤波参数 + spatial_sigma = radius / 3.0 + intensity_sigma = 0.1 + + # 应用滤波 + for y in range(radius, height - radius): + for x in range(radius, width - radius): + weighted_sum = 0.0 + weight_sum = 0.0 + center_value = heightmap[y, x] + + for ky in range(kernel_size): + for kx in range(kernel_size): + ny = y + ky - radius + nx = x + kx - radius + neighbor_value = heightmap[ny, nx] + + # 空间权重 + dx = kx - radius + dy = ky - radius + spatial_weight = math.exp(-(dx*dx + dy*dy) / (2 * spatial_sigma * spatial_sigma)) + + # 强度权重 + intensity_diff = abs(neighbor_value - center_value) + intensity_weight = math.exp(-(intensity_diff * intensity_diff) / (2 * intensity_sigma * intensity_sigma)) + + # 总权重 + weight = spatial_weight * intensity_weight + weighted_sum += neighbor_value * weight + weight_sum += weight + + if weight_sum > 0: + filtered[y, x] = weighted_sum / weight_sum + + return filtered + + except Exception as e: + print(f"✗ 双边滤波应用失败: {e}") + return heightmap + + def normalize_heightmap(self, heightmap: np.ndarray, min_value: float = 0.0, + max_value: float = 1.0) -> np.ndarray: + """ + 归一化高度图 + + Args: + heightmap: 高度图数据 + min_value: 最小值 + max_value: 最大值 + + Returns: + 归一化后的高度图数据 + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return heightmap + + print("✓ 开始归一化高度图...") + + # 计算当前范围 + current_min = np.min(heightmap) + current_max = np.max(heightmap) + + if current_max == current_min: + # 如果所有值相同,返回常量图 + normalized = np.full_like(heightmap, (min_value + max_value) / 2.0) + else: + # 归一化到0-1范围 + normalized_01 = (heightmap - current_min) / (current_max - current_min) + # 缩放到指定范围 + normalized = normalized_01 * (max_value - min_value) + min_value + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 高度图归一化完成,耗时: {processing_time:.3f}秒") + return normalized + + except Exception as e: + print(f"✗ 高度图归一化失败: {e}") + import traceback + traceback.print_exc() + return heightmap + + def invert_heightmap(self, heightmap: np.ndarray) -> np.ndarray: + """ + 反转高度图 + + Args: + heightmap: 高度图数据 + + Returns: + 反转后的高度图数据 + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return heightmap + + print("✓ 开始反转高度图...") + + # 反转高度图 + inverted = 1.0 - heightmap + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 高度图反转完成,耗时: {processing_time:.3f}秒") + return inverted + + except Exception as e: + print(f"✗ 高度图反转失败: {e}") + import traceback + traceback.print_exc() + return heightmap + + def blend_heightmaps(self, heightmaps: List[np.ndarray], weights: List[float] = None) -> np.ndarray: + """ + 混合多个高度图 + + Args: + heightmaps: 高度图列表 + weights: 权重列表(如果为None,则平均分配权重) + + Returns: + 混合后的高度图数据 + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return np.array([]) + + if not heightmaps: + print("✗ 高度图列表为空") + return np.array([]) + + if len(heightmaps) == 1: + return heightmaps[0].copy() + + print(f"✓ 开始混合 {len(heightmaps)} 个高度图...") + + # 确保所有高度图尺寸相同 + base_shape = heightmaps[0].shape + for hm in heightmaps: + if hm.shape != base_shape: + print("✗ 所有高度图必须具有相同的尺寸") + return heightmaps[0].copy() + + # 处理权重 + if weights is None: + weights = [1.0 / len(heightmaps)] * len(heightmaps) + elif len(weights) != len(heightmaps): + print("✗ 权重数量必须与高度图数量相同") + return heightmaps[0].copy() + + # 归一化权重 + total_weight = sum(weights) + if total_weight > 0: + weights = [w / total_weight for w in weights] + + # 混合高度图 + blended = np.zeros(base_shape, dtype=np.float32) + for i, (hm, weight) in enumerate(zip(heightmaps, weights)): + blended += hm * weight + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 高度图混合完成,耗时: {processing_time:.3f}秒") + return blended + + except Exception as e: + print(f"✗ 高度图混合失败: {e}") + import traceback + traceback.print_exc() + # 返回第一个高度图的副本 + return heightmaps[0].copy() if heightmaps else np.array([]) + + def generate_terrain_mask(self, heightmap: np.ndarray, condition: str, + threshold: float = 0.5) -> np.ndarray: + """ + 生成地形掩码 + + Args: + heightmap: 高度图数据 + condition: 条件 ('above', 'below', 'equal', 'between') + threshold: 阈值 + + Returns: + 掩码数据 (0-1范围) + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return np.zeros_like(heightmap, dtype=np.float32) + + print(f"✓ 开始生成地形掩码 (条件: {condition}, 阈值: {threshold})...") + + mask = np.zeros_like(heightmap, dtype=np.float32) + + if condition == 'above': + mask = np.where(heightmap > threshold, 1.0, 0.0) + elif condition == 'below': + mask = np.where(heightmap < threshold, 1.0, 0.0) + elif condition == 'equal': + mask = np.where(np.abs(heightmap - threshold) < 0.01, 1.0, 0.0) + elif condition == 'between': + # 假设阈值是一个范围 [min, max] + if isinstance(threshold, (list, tuple)) and len(threshold) == 2: + min_val, max_val = threshold + mask = np.where((heightmap >= min_val) & (heightmap <= max_val), 1.0, 0.0) + else: + mask = np.where(np.abs(heightmap - threshold) < 0.1, 1.0, 0.0) + else: + print(f"✗ 不支持的条件: {condition}") + return mask + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 地形掩码生成完成,耗时: {processing_time:.3f}秒") + return mask + + except Exception as e: + print(f"✗ 地形掩码生成失败: {e}") + import traceback + traceback.print_exc() + return np.zeros_like(heightmap, dtype=np.float32) + + def apply_terrain_mask(self, heightmap: np.ndarray, mask: np.ndarray, + operation: str = 'multiply') -> np.ndarray: + """ + 应用地形掩码 + + Args: + heightmap: 高度图数据 + mask: 掩码数据 + operation: 操作 ('multiply', 'add', 'subtract', 'replace') + + Returns: + 处理后的高度图数据 + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return heightmap + + # 确保掩码尺寸匹配 + if mask.shape != heightmap.shape: + print("✗ 掩码尺寸与高度图不匹配") + return heightmap + + print(f"✓ 开始应用地形掩码 (操作: {operation})...") + + result = heightmap.copy() + + if operation == 'multiply': + result = heightmap * mask + elif operation == 'add': + result = heightmap + mask + elif operation == 'subtract': + result = heightmap - mask + elif operation == 'replace': + # 只在掩码为1的地方替换 + result = np.where(mask > 0.5, mask, heightmap) + else: + print(f"✗ 不支持的操作: {operation}") + return heightmap + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 地形掩码应用完成,耗时: {processing_time:.3f}秒") + return result + + except Exception as e: + print(f"✗ 地形掩码应用失败: {e}") + import traceback + traceback.print_exc() + return heightmap + + def calculate_terrain_statistics(self, heightmap: np.ndarray) -> Dict[str, float]: + """ + 计算地形统计信息 + + Args: + heightmap: 高度图数据 + + Returns: + 统计信息字典 + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return {} + + print("✓ 开始计算地形统计信息...") + + # 计算基本统计信息 + stats = { + 'min_height': float(np.min(heightmap)), + 'max_height': float(np.max(heightmap)), + 'mean_height': float(np.mean(heightmap)), + 'std_height': float(np.std(heightmap)), + 'median_height': float(np.median(heightmap)), + 'total_area': float(heightmap.size), + 'width': heightmap.shape[1], + 'height': heightmap.shape[0] + } + + # 计算坡度统计 + slope_map = self.analyze_terrain_slope(heightmap) + stats['mean_slope'] = float(np.mean(slope_map)) + stats['max_slope'] = float(np.max(slope_map)) + stats['std_slope'] = float(np.std(slope_map)) + + # 计算曲率统计 + curvature_map = self.analyze_terrain_curvature(heightmap) + stats['mean_curvature'] = float(np.mean(curvature_map)) + stats['max_curvature'] = float(np.max(curvature_map)) + stats['std_curvature'] = float(np.std(curvature_map)) + + # 计算粗糙度统计 + rugosity_map = self.analyze_terrain_rugosity(heightmap) + stats['mean_rugosity'] = float(np.mean(rugosity_map)) + stats['max_rugosity'] = float(np.max(rugosity_map)) + stats['std_rugosity'] = float(np.std(rugosity_map)) + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 地形统计信息计算完成,耗时: {processing_time:.3f}秒") + return stats + + except Exception as e: + print(f"✗ 地形统计信息计算失败: {e}") + import traceback + traceback.print_exc() + return {} + + def _update_stats(self, processing_time: float): + """更新统计信息""" + self.stats['tools_used'] += 1 + self.stats['total_processing_time'] += processing_time + if self.stats['tools_used'] > 0: + self.stats['average_processing_time'] = ( + self.stats['total_processing_time'] / self.stats['tools_used'] + ) + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'tools_used': 0, + 'total_processing_time': 0.0, + 'average_processing_time': 0.0, + 'cache_hits': 0, + 'cache_misses': 0 + } + print("✓ 地形工具类统计信息已重置") + + def set_tool_parameters(self, params: Dict[str, Any]): + """ + 设置工具参数 + + Args: + params: 参数字典 + """ + self.tool_params.update(params) + print(f"✓ 地形工具参数已更新: {self.tool_params}") + + def export_terrain_data(self, heightmap: np.ndarray, filename: str, + metadata: Dict[str, Any] = None) -> bool: + """ + 导出地形数据 + + Args: + heightmap: 高度图数据 + filename: 文件名 + metadata: 元数据 + + Returns: + 是否导出成功 + """ + try: + print(f"✓ 开始导出地形数据到: {filename}") + + # 创建导出数据 + export_data = { + 'heightmap': heightmap.tolist(), + 'shape': heightmap.shape, + 'dtype': str(heightmap.dtype), + 'metadata': metadata or {}, + 'export_time': time.time() + } + + # 保存为JSON文件 + with open(filename, 'w', encoding='utf-8') as f: + json.dump(export_data, f, ensure_ascii=False, indent=2) + + print(f"✓ 地形数据导出完成") + return True + + except Exception as e: + print(f"✗ 地形数据导出失败: {e}") + import traceback + traceback.print_exc() + return False + + def import_terrain_data(self, filename: str) -> Optional[np.ndarray]: + """ + 导入地形数据 + + Args: + filename: 文件名 + + Returns: + 高度图数据或None + """ + try: + print(f"✓ 开始导入地形数据从: {filename}") + + # 读取JSON文件 + with open(filename, 'r', encoding='utf-8') as f: + import_data = json.load(f) + + # 恢复高度图数据 + heightmap_data = import_data['heightmap'] + heightmap = np.array(heightmap_data, dtype=np.float32) + + print(f"✓ 地形数据导入完成,尺寸: {heightmap.shape}") + return heightmap + + except Exception as e: + print(f"✗ 地形数据导入失败: {e}") + import traceback + traceback.print_exc() + return None + + def resample_heightmap(self, heightmap: np.ndarray, new_width: int, new_height: int) -> np.ndarray: + """ + 重采样高度图 + + Args: + heightmap: 原始高度图数据 + new_width: 新宽度 + new_height: 新高度 + + Returns: + 重采样后的高度图数据 + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return heightmap + + print(f"✓ 开始重采样高度图到 {new_width}x{new_height}...") + + old_height, old_width = heightmap.shape + + # 创建新的高度图 + resampled = np.zeros((new_height, new_width), dtype=np.float32) + + # 双线性插值重采样 + for y in range(new_height): + for x in range(new_width): + # 计算在原图中的对应位置 + old_x = x * (old_width - 1) / (new_width - 1) + old_y = y * (old_height - 1) / (new_height - 1) + + # 双线性插值 + resampled[y, x] = self._bilinear_interpolate(heightmap, old_x, old_y) + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 高度图重采样完成,耗时: {processing_time:.3f}秒") + return resampled + + except Exception as e: + print(f"✗ 高度图重采样失败: {e}") + import traceback + traceback.print_exc() + return heightmap + + def _bilinear_interpolate(self, heightmap: np.ndarray, x: float, y: float) -> float: + """ + 双线性插值 + + Args: + heightmap: 高度图数据 + x: X坐标 + y: Y坐标 + + Returns: + 插值结果 + """ + try: + height, width = heightmap.shape + + # 获取整数部分 + x0 = int(math.floor(x)) + y0 = int(math.floor(y)) + x1 = min(x0 + 1, width - 1) + y1 = min(y0 + 1, height - 1) + + # 获取小数部分 + fx = x - x0 + fy = y - y0 + + # 双线性插值 + top = heightmap[y0, x0] * (1 - fx) + heightmap[y0, x1] * fx + bottom = heightmap[y1, x0] * (1 - fx) + heightmap[y1, x1] * fx + result = top * (1 - fy) + bottom * fy + + return result + + except Exception as e: + print(f"✗ 双线性插值失败: {e}") + return 0.0 + + def generate_terrain_preview(self, heightmap: np.ndarray, width: int = 256, height: int = 256) -> np.ndarray: + """ + 生成地形预览图 + + Args: + heightmap: 高度图数据 + width: 预览图宽度 + height: 预览图高度 + + Returns: + 预览图数据 (RGB格式) + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return np.zeros((height, width, 3), dtype=np.uint8) + + print("✓ 开始生成地形预览图...") + + # 重采样到预览尺寸 + preview_heightmap = self.resample_heightmap(heightmap, width, height) + + # 创建RGB预览图 + preview = np.zeros((height, width, 3), dtype=np.uint8) + + # 根据高度生成颜色 + for y in range(height): + for x in range(width): + height_value = preview_heightmap[y, x] + + # 根据高度选择颜色 + if height_value < 0.2: + # 水域 - 蓝色 + r, g, b = 0, 0, int(128 + height_value * 127) + elif height_value < 0.3: + # 浅水/海滩 - 浅蓝色 + r, g, b = int(height_value * 255), int(height_value * 255), 255 + elif height_value < 0.5: + # 平原 - 绿色 + r, g, b = 0, int(100 + height_value * 155), 0 + elif height_value < 0.7: + # 丘陵 - 棕色 + r, g, b = int(139 * height_value), int(69 * height_value), int(19 * height_value) + elif height_value < 0.9: + # 山地 - 灰色 + r, g, b = int(100 + height_value * 155), int(100 + height_value * 155), int(100 + height_value * 155) + else: + # 雪山 - 白色 + r, g, b = 255, 255, 255 + + preview[y, x] = [r, g, b] + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 地形预览图生成完成,耗时: {processing_time:.3f}秒") + return preview + + except Exception as e: + print(f"✗ 地形预览图生成失败: {e}") + import traceback + traceback.print_exc() + return np.zeros((height, width, 3), dtype=np.uint8) + + def calculate_terrain_volume(self, heightmap: np.ndarray, cell_size: float = 1.0) -> float: + """ + 计算地形体积 + + Args: + heightmap: 高度图数据 + cell_size: 网格单元大小 + + Returns: + 体积值 + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return 0.0 + + print("✓ 开始计算地形体积...") + + # 计算总体积(假设每个网格单元是柱体) + volume = np.sum(heightmap) * cell_size * cell_size + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 地形体积计算完成,耗时: {processing_time:.3f}秒") + return float(volume) + + except Exception as e: + print(f"✗ 地形体积计算失败: {e}") + import traceback + traceback.print_exc() + return 0.0 + + def calculate_terrain_area(self, heightmap: np.ndarray, cell_size: float = 1.0) -> float: + """ + 计算地形表面积 + + Args: + heightmap: 高度图数据 + cell_size: 网格单元大小 + + Returns: + 表面积值 + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return 0.0 + + print("✓ 开始计算地形表面积...") + + height, width = heightmap.shape + total_area = 0.0 + + # 计算每个网格单元的表面积 + for y in range(height - 1): + for x in range(width - 1): + # 获取四个角点的高度 + h00 = heightmap[y, x] + h01 = heightmap[y, x + 1] + h10 = heightmap[y + 1, x] + h11 = heightmap[y + 1, x + 1] + + # 计算两个三角形的面积 + # 三角形1: (0,0,h00), (0,1,h01), (1,0,h10) + dx1 = cell_size + dy1 = 0.0 + dz1 = h01 - h00 + dv1 = np.array([dx1, dy1, dz1]) + + dx2 = 0.0 + dy2 = cell_size + dz2 = h10 - h00 + dv2 = np.array([dx2, dy2, dz2]) + + cross1 = np.cross(dv1, dv2) + area1 = 0.5 * np.linalg.norm(cross1) + + # 三角形2: (1,1,h11), (0,1,h01), (1,0,h10) + dx3 = 0.0 + dy3 = -cell_size + dz3 = h11 - h01 + dv3 = np.array([dx3, dy3, dz3]) + + dx4 = -cell_size + dy4 = 0.0 + dz4 = h11 - h10 + dv4 = np.array([dx4, dy4, dz4]) + + cross2 = np.cross(dv3, dv4) + area2 = 0.5 * np.linalg.norm(cross2) + + total_area += area1 + area2 + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 地形表面积计算完成,耗时: {processing_time:.3f}秒") + return total_area + + except Exception as e: + print(f"✗ 地形表面积计算失败: {e}") + import traceback + traceback.print_exc() + return 0.0 + + def detect_terrain_features(self, heightmap: np.ndarray) -> Dict[str, np.ndarray]: + """ + 检测地形特征 + + Args: + heightmap: 高度图数据 + + Returns: + 特征检测结果字典 + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return {} + + print("✓ 开始检测地形特征...") + + # 检测山峰 + peaks = self._detect_peaks(heightmap) + + # 检测山谷 + valleys = self._detect_valleys(heightmap) + + # 检测山脊 + ridges = self._detect_ridges(heightmap) + + # 检测河道 + rivers = self._detect_rivers(heightmap) + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 地形特征检测完成,耗时: {processing_time:.3f}秒") + return { + 'peaks': peaks, + 'valleys': valleys, + 'ridges': ridges, + 'rivers': rivers + } + + except Exception as e: + print(f"✗ 地形特征检测失败: {e}") + import traceback + traceback.print_exc() + return {} + + def _detect_peaks(self, heightmap: np.ndarray) -> np.ndarray: + """检测山峰""" + try: + height, width = heightmap.shape + peaks = np.zeros_like(heightmap, dtype=np.float32) + + # 简化的山峰检测 + for y in range(1, height - 1): + for x in range(1, width - 1): + center = heightmap[y, x] + is_peak = True + + # 检查是否比所有邻居都高 + for dy in [-1, 0, 1]: + for dx in [-1, 0, 1]: + if dy == 0 and dx == 0: + continue + if heightmap[y + dy, x + dx] >= center: + is_peak = False + break + if not is_peak: + break + + if is_peak and center > 0.7: # 高度阈值 + peaks[y, x] = center + + return peaks + + except Exception as e: + print(f"✗ 山峰检测失败: {e}") + return np.zeros_like(heightmap, dtype=np.float32) + + def _detect_valleys(self, heightmap: np.ndarray) -> np.ndarray: + """检测山谷""" + try: + height, width = heightmap.shape + valleys = np.zeros_like(heightmap, dtype=np.float32) + + # 简化的山谷检测 + for y in range(1, height - 1): + for x in range(1, width - 1): + center = heightmap[y, x] + is_valley = True + + # 检查是否比所有邻居都低 + for dy in [-1, 0, 1]: + for dx in [-1, 0, 1]: + if dy == 0 and dx == 0: + continue + if heightmap[y + dy, x + dx] <= center: + is_valley = False + break + if not is_valley: + break + + if is_valley and center < 0.3: # 高度阈值 + valleys[y, x] = 1.0 - center + + return valleys + + except Exception as e: + print(f"✗ 山谷检测失败: {e}") + return np.zeros_like(heightmap, dtype=np.float32) + + def _detect_ridges(self, heightmap: np.ndarray) -> np.ndarray: + """检测山脊""" + try: + # 简化的山脊检测 + slope_map = self.analyze_terrain_slope(heightmap) + ridge_map = np.zeros_like(heightmap, dtype=np.float32) + + # 山脊通常是坡度变化较大的地方 + ridge_map = np.where(slope_map > self.tool_params['slope_threshold'], slope_map, 0) + + return ridge_map + + except Exception as e: + print(f"✗ 山脊检测失败: {e}") + return np.zeros_like(heightmap, dtype=np.float32) + + def _detect_rivers(self, heightmap: np.ndarray) -> np.ndarray: + """检测河道""" + try: + # 简化的河道检测 + curvature_map = self.analyze_terrain_curvature(heightmap) + river_map = np.zeros_like(heightmap, dtype=np.float32) + + # 河道通常在负曲率区域(凹陷处) + river_map = np.where(curvature_map < -self.tool_params['curvature_threshold'], + np.abs(curvature_map), 0) + + return river_map + + except Exception as e: + print(f"✗ 河道检测失败: {e}") + return np.zeros_like(heightmap, dtype=np.float32) + + def fill_terrain_holes(self, heightmap: np.ndarray, hole_threshold: float = 0.01) -> np.ndarray: + """ + 填补地形空洞 + + Args: + heightmap: 高度图数据 + hole_threshold: 空洞阈值 + + Returns: + 处理后的高度图数据 + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return heightmap + + print("✓ 开始填补地形空洞...") + + filled = heightmap.copy() + height, width = filled.shape + + # 简化的空洞填补算法 + for y in range(1, height - 1): + for x in range(1, width - 1): + center = filled[y, x] + + # 如果中心点是空洞 + if center < hole_threshold: + # 计算邻居的平均值 + neighbors = [] + for dy in [-1, 0, 1]: + for dx in [-1, 0, 1]: + if dy == 0 and dx == 0: + continue + neighbor_value = filled[y + dy, x + dx] + if neighbor_value >= hole_threshold: # 只考虑非空洞邻居 + neighbors.append(neighbor_value) + + if neighbors: + filled[y, x] = np.mean(neighbors) + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 地形空洞填补完成,耗时: {processing_time:.3f}秒") + return filled + + except Exception as e: + print(f"✗ 地形空洞填补失败: {e}") + import traceback + traceback.print_exc() + return heightmap + + def clamp_heightmap(self, heightmap: np.ndarray, min_value: float = 0.0, + max_value: float = 1.0) -> np.ndarray: + """ + 限制高度图范围 + + Args: + heightmap: 高度图数据 + min_value: 最小值 + max_value: 最大值 + + Returns: + 处理后的高度图数据 + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return heightmap + + print("✓ 开始限制高度图范围...") + + # 限制范围 + clamped = np.clip(heightmap, min_value, max_value) + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 高度图范围限制完成,耗时: {processing_time:.3f}秒") + return clamped + + except Exception as e: + print(f"✗ 高度图范围限制失败: {e}") + import traceback + traceback.print_exc() + return heightmap + + def add_terrain_noise(self, heightmap: np.ndarray, noise_amount: float = 0.01, + seed: int = None) -> np.ndarray: + """ + 添加噪声到地形 + + Args: + heightmap: 高度图数据 + noise_amount: 噪声强度 + seed: 随机种子 + + Returns: + 处理后的高度图数据 + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return heightmap + + print("✓ 开始添加噪声到地形...") + + # 设置随机种子 + if seed is not None: + np.random.seed(seed) + + # 生成噪声 + noise = np.random.normal(0, noise_amount, heightmap.shape).astype(np.float32) + + # 添加噪声 + noisy = heightmap + noise + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 地形噪声添加完成,耗时: {processing_time:.3f}秒") + return noisy + + except Exception as e: + print(f"✗ 地形噪声添加失败: {e}") + import traceback + traceback.print_exc() + return heightmap + + def create_terrain_from_function(self, width: int, height: int, + function: callable) -> np.ndarray: + """ + 根据函数创建地形 + + Args: + width: 宽度 + height: 高度 + function: 生成函数 (x, y) -> height + + Returns: + 生成的高度图数据 + """ + try: + processing_start_time = time.time() + + if not self.enabled: + print("✗ 地形工具类未启用") + return np.zeros((height, width), dtype=np.float32) + + print("✓ 开始根据函数创建地形...") + + # 创建高度图 + heightmap = np.zeros((height, width), dtype=np.float32) + + # 应用函数 + for y in range(height): + for x in range(width): + # 将坐标标准化到[-1, 1]范围 + norm_x = (x / (width - 1)) * 2.0 - 1.0 + norm_y = (y / (height - 1)) * 2.0 - 1.0 + heightmap[y, x] = function(norm_x, norm_y) + + # 更新统计信息 + processing_time = time.time() - processing_start_time + self._update_stats(processing_time) + + print(f"✓ 函数地形创建完成,耗时: {processing_time:.3f}秒") + return heightmap + + except Exception as e: + print(f"✗ 函数地形创建失败: {e}") + import traceback + traceback.print_exc() + return np.zeros((height, width), dtype=np.float32) + + def cache_analysis_result(self, key: str, result: Any): + """ + 缓存分析结果 + + Args: + key: 缓存键 + result: 分析结果 + """ + try: + self.analysis_cache[key] = { + 'result': result, + 'timestamp': time.time() + } + print(f"✓ 分析结果已缓存: {key}") + except Exception as e: + print(f"✗ 分析结果缓存失败: {e}") + + def get_cached_analysis_result(self, key: str) -> Optional[Any]: + """ + 获取缓存的分析结果 + + Args: + key: 缓存键 + + Returns: + 缓存的分析结果或None + """ + try: + if key in self.analysis_cache: + self.stats['cache_hits'] += 1 + return self.analysis_cache[key]['result'] + else: + self.stats['cache_misses'] += 1 + return None + except Exception as e: + print(f"✗ 缓存分析结果获取失败: {e}") + return None + + def clear_analysis_cache(self): + """清空分析缓存""" + try: + self.analysis_cache.clear() + print("✓ 分析缓存已清空") + except Exception as e: + print(f"✗ 分析缓存清空失败: {e}") + + def get_cache_stats(self) -> Dict[str, int]: + """ + 获取缓存统计信息 + + Returns: + 缓存统计信息 + """ + return { + 'cache_size': len(self.analysis_cache), + 'cache_hits': self.stats['cache_hits'], + 'cache_misses': self.stats['cache_misses'], + 'hit_rate': self.stats['cache_hits'] / max(1, self.stats['cache_hits'] + self.stats['cache_misses']) } \ No newline at end of file diff --git a/plugins/user/procedural_terrain_generation/vegetation/vegetation_generator.py b/plugins/user/procedural_terrain_generation/vegetation/vegetation_generator.py index fa4cabc2..971d9e91 100644 --- a/plugins/user/procedural_terrain_generation/vegetation/vegetation_generator.py +++ b/plugins/user/procedural_terrain_generation/vegetation/vegetation_generator.py @@ -1,1370 +1,1370 @@ -""" -植被生成器 -负责生成程序化地形的植被分布 -""" - -import numpy as np -import math -from typing import Dict, Any, List, Tuple -import random - -class VegetationGenerator: - """ - 植被生成器 - 负责生成程序化地形的植被分布,基于生物群落、湿度、温度等因素 - """ - - def __init__(self, plugin): - """ - 初始化植被生成器 - - Args: - plugin: 程序化地形生成插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 植被配置 - self.seed = plugin.config.get('seed', 12345) - - # 植被类型定义 - self.vegetation_types = { - 0: {'name': 'none', 'density': 0.0, 'color': (0, 0, 0)}, - 1: {'name': 'grass', 'density': 0.3, 'color': (144, 238, 144)}, - 2: {'name': 'shrub', 'density': 0.2, 'color': (107, 142, 35)}, - 3: {'name': 'flower', 'density': 0.1, 'color': (255, 105, 180)}, - 4: {'name': 'tree', 'density': 0.15, 'color': (34, 139, 34)}, - 5: {'name': 'pine_tree', 'density': 0.12, 'color': (0, 128, 0)}, - 6: {'name': 'palm_tree', 'density': 0.08, 'color': (0, 100, 0)}, - 7: {'name': 'cactus', 'density': 0.05, 'color': (0, 100, 0)}, - 8: {'name': 'bush', 'density': 0.25, 'color': (107, 142, 35)}, - 9: {'name': 'fern', 'density': 0.2, 'color': (34, 139, 34)}, - 10: {'name': 'moss', 'density': 0.15, 'color': (144, 238, 144)}, - 11: {'name': 'vine', 'density': 0.1, 'color': (0, 128, 0)}, - 12: {'name': 'seaweed', 'density': 0.4, 'color': (0, 128, 0)}, - 13: {'name': 'algae', 'density': 0.35, 'color': (0, 100, 0)}, - 14: {'name': 'kelp', 'density': 0.3, 'color': (0, 128, 0)}, - 15: {'name': 'coral', 'density': 0.25, 'color': (255, 99, 71)} - } - - # 生物群落植被分布 - self.biome_vegetation = { - 0: [0, 12, 13, 14, 15], # ocean - 1: [0, 1, 2], # beach - 2: [0, 1, 2, 8], # plains - 3: [0, 1, 2, 4, 9], # forest - 4: [0, 1, 2, 4, 6, 9, 11], # jungle - 5: [0, 1, 2, 7], # desert - 6: [0, 1, 2, 5], # mountain - 7: [0, 1, 2], # snow - 8: [0, 1, 2, 5, 10], # taiga - 9: [0, 1, 2], # tundra - 10: [0, 1, 2, 3, 9, 10], # swamp - 11: [0, 1, 2, 8] # savanna - } - - # 植被生成参数 - self.vegetation_params = { - 'density_scale': 10.0, - 'clumping_factor': 0.7, - 'slope_tolerance': 0.3, - 'height_min': 0.0, - 'height_max': 1.0, - 'humidity_min': 0.0, - 'humidity_max': 1.0, - 'temperature_min': 0.0, - 'temperature_max': 1.0, - 'edge_blending': 0.05 - } - - # 噪声生成器引用 - self.noise_generator = None - - # 植被分布图 - self.vegetation_map = None - - # 统计信息 - self.stats = { - 'vegetation_generated': 0, - 'total_generation_time': 0.0, - 'average_generation_time': 0.0, - 'vegetation_distribution': {} - } - - print("✓ 植被生成器已创建") - - def initialize(self) -> bool: - """ - 初始化植被生成器 - - Returns: - 是否初始化成功 - """ - try: - # 获取噪声生成器引用 - self.noise_generator = self.plugin.noise_generator - - self.initialized = True - print("✓ 植被生成器初始化完成") - return True - - except Exception as e: - print(f"✗ 植被生成器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用植被生成器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 植被生成器未初始化") - return False - - self.enabled = True - print("✓ 植被生成器已启用") - return True - - except Exception as e: - print(f"✗ 植被生成器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用植被生成器""" - try: - self.enabled = False - print("✓ 植被生成器已禁用") - - except Exception as e: - print(f"✗ 植被生成器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理植被生成器资源""" - try: - self.disable() - self.initialized = False - print("✓ 植被生成器资源已清理") - - except Exception as e: - print(f"✗ 植被生成器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新植被生成器状态 - - Args: - dt: 时间增量 - """ - # 处理更新逻辑 - pass - - def generate_vegetation(self, heightmap: np.ndarray, biome_map: np.ndarray, seed: int = None) -> np.ndarray: - """ - 生成植被分布 - - Args: - heightmap: 高度图数据 - biome_map: 生物群落图数据 - seed: 随机种子 - - Returns: - 植被分布图 - """ - try: - import time - generation_start_time = time.time() - - if not self.enabled: - print("✗ 植被生成器未启用") - return np.zeros_like(heightmap, dtype=np.int32) - - # 更新种子 - if seed is not None: - self.seed = seed - - print("✓ 开始生成植被分布...") - - # 初始化随机数生成器 - random.seed(self.seed) - np.random.seed(self.seed) - - # 创建植被图 - height, width = heightmap.shape - self.vegetation_map = np.zeros((height, width), dtype=np.int32) - - # 生成基础植被噪声 - vegetation_noise = self._generate_vegetation_noise(width, height) - - # 为每个像素生成植被 - print(" → 生成植被类型...") - for y in range(height): - for x in range(width): - # 获取当前位置的属性 - height_value = heightmap[y, x] - biome_type = biome_map[y, x] if y < biome_map.shape[0] and x < biome_map.shape[1] else 0 - - # 生成植被 - vegetation_type = self._generate_vegetation_at_position( - x, y, height_value, biome_type, vegetation_noise[y, x] - ) - self.vegetation_map[y, x] = vegetation_type - - # 应用后处理 - print(" → 应用后处理...") - self.vegetation_map = self._post_process_vegetation(self.vegetation_map, heightmap, biome_map) - - # 更新统计信息 - generation_time = time.time() - generation_start_time - self.stats['vegetation_generated'] += 1 - self.stats['total_generation_time'] += generation_time - self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['vegetation_generated'] - - # 更新植被分布统计 - self._update_vegetation_distribution_stats(self.vegetation_map) - - print(f"✓ 植被生成完成,耗时: {generation_time:.2f}秒") - return self.vegetation_map - - except Exception as e: - print(f"✗ 植被生成失败: {e}") - import traceback - traceback.print_exc() - return np.zeros_like(heightmap, dtype=np.int32) - - def _generate_vegetation_noise(self, width: int, height: int) -> np.ndarray: - """ - 生成植被噪声图 - - Args: - width: 宽度 - height: 高度 - - Returns: - 植被噪声图 - """ - try: - noise_map = np.zeros((height, width), dtype=np.float32) - - if self.noise_generator: - # 生成分形噪声 - for y in range(height): - for x in range(width): - noise_value = self.noise_generator.generate_fractal_noise( - x / self.vegetation_params['density_scale'], - y / self.vegetation_params['density_scale'], - self.seed, - octaves=3, - persistence=0.7, - lacunarity=2.0 - ) - noise_map[y, x] = (noise_value + 1.0) / 2.0 # 转换到0-1范围 - else: - # 如果没有噪声生成器,使用随机噪声 - noise_map = np.random.rand(height, width).astype(np.float32) - - return noise_map - - except Exception as e: - print(f"✗ 植被噪声生成失败: {e}") - return np.random.rand(height, width).astype(np.float32) - - def _generate_vegetation_at_position(self, x: int, y: int, height: float, biome: int, noise: float) -> int: - """ - 在指定位置生成植被 - - Args: - x: X坐标 - y: Y坐标 - height: 高度值 - biome: 生物群落类型 - noise: 噪声值 - - Returns: - 植被类型ID - """ - try: - # 检查高度限制 - if height < self.vegetation_params['height_min'] or height > self.vegetation_params['height_max']: - return 0 # 无植被 - - # 获取生物群落的植被类型 - biome_veg_types = self.biome_vegetation.get(biome, [0]) - - # 根据噪声值选择植被类型 - if noise > 0.7 and len(biome_veg_types) > 1: - # 高噪声值,选择较稀有的植被 - weights = [self.vegetation_types.get(v, {'density': 0.1})['density'] for v in biome_veg_types] - # 反转权重以使稀有植被更容易被选中 - weights = [1.0 / (w + 0.01) for w in weights] - vegetation_type = np.random.choice(biome_veg_types, p=np.array(weights) / sum(weights)) - elif noise > 0.4 and len(biome_veg_types) > 1: - # 中等噪声值,随机选择 - vegetation_type = np.random.choice(biome_veg_types) - elif len(biome_veg_types) > 0: - # 低噪声值,选择常见的植被 - weights = [self.vegetation_types.get(v, {'density': 0.1})['density'] for v in biome_veg_types] - vegetation_type = np.random.choice(biome_veg_types, p=np.array(weights) / sum(weights)) - else: - vegetation_type = 0 # 无植被 - - return vegetation_type - - except Exception as e: - print(f"✗ 位置植被生成失败: {e}") - return 0 - - def _post_process_vegetation(self, vegetation_map: np.ndarray, heightmap: np.ndarray, biome_map: np.ndarray) -> np.ndarray: - """ - 后处理植被图 - - Args: - vegetation_map: 原始植被图 - heightmap: 高度图 - biome_map: 生物群落图 - - Returns: - 处理后的植被图 - """ - try: - processed = vegetation_map.copy() - height, width = processed.shape - - # 应用聚类效果 - processed = self._apply_clumping(processed, heightmap) - - # 应用坡度限制 - processed = self._apply_slope_limit(processed, heightmap) - - # 应用边缘混合 - processed = self._apply_edge_blending(processed) - - return processed - - except Exception as e: - print(f"✗ 植被后处理失败: {e}") - return vegetation_map - - def _apply_clumping(self, vegetation_map: np.ndarray, heightmap: np.ndarray) -> np.ndarray: - """ - 应用植被聚类效果 - - Args: - vegetation_map: 植被图 - heightmap: 高度图 - - Returns: - 处理后的植被图 - """ - try: - processed = vegetation_map.copy() - height, width = processed.shape - clumping_factor = self.vegetation_params['clumping_factor'] - - # 对每个植被类型应用聚类 - for y in range(height): - for x in range(width): - if vegetation_map[y, x] != 0: # 如果有植被 - # 检查邻居是否有相同类型的植被 - same_type_neighbors = 0 - total_neighbors = 0 - - for dy in [-1, 0, 1]: - for dx in [-1, 0, 1]: - if dy == 0 and dx == 0: - continue - - ny, nx = y + dy, x + dx - if 0 <= ny < height and 0 <= nx < width: - total_neighbors += 1 - if vegetation_map[ny, nx] == vegetation_map[y, x]: - same_type_neighbors += 1 - - # 根据邻居情况调整植被类型 - if total_neighbors > 0: - similarity = same_type_neighbors / total_neighbors - if similarity < clumping_factor and np.random.random() > 0.5: - # 减少孤立的植被 - processed[y, x] = 0 - - return processed - - except Exception as e: - print(f"✗ 植被聚类应用失败: {e}") - return vegetation_map - - def _apply_slope_limit(self, vegetation_map: np.ndarray, heightmap: np.ndarray) -> np.ndarray: - """ - 应用坡度限制 - - Args: - vegetation_map: 植被图 - heightmap: 高度图 - - Returns: - 处理后的植被图 - """ - try: - processed = vegetation_map.copy() - height, width = processed.shape - slope_tolerance = self.vegetation_params['slope_tolerance'] - - # 计算坡度并应用限制 - for y in range(height): - for x in range(width): - if vegetation_map[y, x] != 0: # 如果有植被 - # 计算当前位置的坡度 - slope = self._calculate_slope(heightmap, x, y) - - # 如果坡度太大,移除植被 - if slope > slope_tolerance: - processed[y, x] = 0 - - return processed - - except Exception as e: - print(f"✗ 坡度限制应用失败: {e}") - return vegetation_map - - def _calculate_slope(self, heightmap: np.ndarray, x: int, y: int) -> float: - """ - 计算指定位置的坡度 - - Args: - heightmap: 高度图 - x: X坐标 - y: Y坐标 - - Returns: - 坡度值 - """ - try: - height, width = heightmap.shape - - # 计算梯度 - dx = 0.0 - dy = 0.0 - - # X方向梯度 - if x > 0 and x < width - 1: - dx = (heightmap[y, x + 1] - heightmap[y, x - 1]) / 2.0 - elif x == 0: - dx = heightmap[y, x + 1] - heightmap[y, x] - else: - dx = heightmap[y, x] - heightmap[y, x - 1] - - # Y方向梯度 - if y > 0 and y < height - 1: - dy = (heightmap[y + 1, x] - heightmap[y - 1, x]) / 2.0 - elif y == 0: - dy = heightmap[y + 1, x] - heightmap[y, x] - else: - dy = heightmap[y, x] - heightmap[y - 1, x] - - # 计算坡度(梯度的模) - slope = math.sqrt(dx * dx + dy * dy) - return slope - - except Exception as e: - print(f"✗ 坡度计算失败: {e}") - return 0.0 - - def _apply_edge_blending(self, vegetation_map: np.ndarray) -> np.ndarray: - """ - 应用边缘混合 - - Args: - vegetation_map: 植被图 - - Returns: - 处理后的植被图 - """ - try: - processed = vegetation_map.copy() - height, width = processed.shape - blend_width = max(1, int(min(height, width) * self.vegetation_params['edge_blending'])) - - # 简化的边缘混合 - for y in range(height): - for x in range(width): - # 在边缘区域随机减少植被 - if (x < blend_width or x > width - blend_width or - y < blend_width or y > height - blend_width): - if np.random.random() < 0.3: # 30%概率移除边缘植被 - processed[y, x] = 0 - - return processed - - except Exception as e: - print(f"✗ 边缘混合应用失败: {e}") - return vegetation_map - - def _update_vegetation_distribution_stats(self, vegetation_map: np.ndarray): - """更新植被分布统计""" - try: - unique, counts = np.unique(vegetation_map, return_counts=True) - for veg_id, count in zip(unique, counts): - veg_name = self.vegetation_types.get(veg_id, {}).get('name', f'vegetation_{veg_id}') - self.stats['vegetation_distribution'][veg_name] = self.stats['vegetation_distribution'].get(veg_name, 0) + count - - except Exception as e: - print(f"✗ 植被分布统计更新失败: {e}") - - def set_seed(self, seed: int): - """ - 设置随机种子 - - Args: - seed: 随机种子 - """ - self.seed = seed - print(f"✓ 植被生成器随机种子设置为: {seed}") - - def set_vegetation_parameters(self, params: Dict[str, Any]): - """ - 设置植被生成参数 - - Args: - params: 参数字典 - """ - self.vegetation_params.update(params) - print(f"✓ 植被生成参数已更新: {self.vegetation_params}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - # 更新平均生成时间 - if self.stats['vegetation_generated'] > 0: - self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['vegetation_generated'] - return self.stats.copy() - - def get_vegetation_info(self, vegetation_id: int) -> Dict[str, Any]: - """ - 获取植被信息 - - Args: - vegetation_id: 植被ID - - Returns: - 植被信息字典 - """ - return self.vegetation_types.get(vegetation_id, { - 'name': f'unknown_{vegetation_id}', - 'density': 0.1, - 'color': (128, 128, 128) - }) - - def get_available_vegetation(self) -> List[Dict[str, Any]]: - """ - 获取可用的植被类型列表 - - Returns: - 植被信息列表 - """ - return [self.get_vegetation_info(veg_id) for veg_id in sorted(self.vegetation_types.keys())] - - def generate_vegetation_with_density(self, heightmap: np.ndarray, biome_map: np.ndarray, - density_map: np.ndarray, seed: int = None) -> np.ndarray: - """ - 根据密度图生成植被 - - Args: - heightmap: 高度图数据 - biome_map: 生物群落图数据 - density_map: 植被密度图数据 - seed: 随机种子 - - Returns: - 植被分布图 - """ - try: - import time - generation_start_time = time.time() - - if not self.enabled: - print("✗ 植被生成器未启用") - return np.zeros_like(heightmap, dtype=np.int32) - - # 更新种子 - if seed is not None: - self.seed = seed - - print("✓ 开始生成基于密度的植被分布...") - - # 初始化随机数生成器 - random.seed(self.seed) - np.random.seed(self.seed) - - # 确保密度图尺寸匹配 - if density_map.shape != heightmap.shape: - print("✗ 密度图尺寸与高度图不匹配") - return np.zeros_like(heightmap, dtype=np.int32) - - # 创建植被图 - height, width = heightmap.shape - self.vegetation_map = np.zeros((height, width), dtype=np.int32) - - # 为每个像素生成植被 - print(" → 生成密度控制的植被...") - for y in range(height): - for x in range(width): - # 获取当前位置的属性 - height_value = heightmap[y, x] - biome_type = biome_map[y, x] if y < biome_map.shape[0] and x < biome_map.shape[1] else 0 - density_value = density_map[y, x] - - # 根据密度值决定是否生成植被 - if np.random.random() < density_value: - # 生成植被 - vegetation_type = self._generate_vegetation_at_position( - x, y, height_value, biome_type, np.random.random() - ) - self.vegetation_map[y, x] = vegetation_type - - # 应用后处理 - print(" → 应用后处理...") - self.vegetation_map = self._post_process_vegetation(self.vegetation_map, heightmap, biome_map) - - # 更新统计信息 - generation_time = time.time() - generation_start_time - self.stats['vegetation_generated'] += 1 - self.stats['total_generation_time'] += generation_time - self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['vegetation_generated'] - - # 更新植被分布统计 - self._update_vegetation_distribution_stats(self.vegetation_map) - - print(f"✓ 基于密度的植被生成完成,耗时: {generation_time:.2f}秒") - return self.vegetation_map - - except Exception as e: - print(f"✗ 基于密度的植被生成失败: {e}") - import traceback - traceback.print_exc() - return np.zeros_like(heightmap, dtype=np.int32) - - def generate_forest_vegetation(self, heightmap: np.ndarray, biome_map: np.ndarray, - tree_density: float = 0.15, seed: int = None) -> np.ndarray: - """ - 生成森林植被(专门用于森林区域) - - Args: - heightmap: 高度图数据 - biome_map: 生物群落图数据 - tree_density: 树木密度 - seed: 随机种子 - - Returns: - 植被分布图 - """ - try: - import time - generation_start_time = time.time() - - if not self.enabled: - print("✗ 植被生成器未启用") - return np.zeros_like(heightmap, dtype=np.int32) - - # 更新种子 - if seed is not None: - self.seed = seed - - print("✓ 开始生成森林植被...") - - # 初始化随机数生成器 - random.seed(self.seed) - np.random.seed(self.seed) - - # 创建植被图 - height, width = heightmap.shape - self.vegetation_map = np.zeros((height, width), dtype=np.int32) - - # 专门处理森林生物群落(ID 3 和 4) - forest_biomes = [3, 4] # forest 和 jungle - - # 为每个像素生成森林植被 - print(" → 生成森林植被...") - for y in range(height): - for x in range(width): - height_value = heightmap[y, x] - biome_type = biome_map[y, x] if y < biome_map.shape[0] and x < biome_map.shape[1] else 0 - - # 只在森林生物群落中生成植被 - if biome_type in forest_biomes: - # 根据树木密度决定是否生成树 - if np.random.random() < tree_density: - # 选择合适的树种 - if biome_type == 4: # jungle - tree_type = 6 if np.random.random() < 0.3 else 4 # 30%概率生成棕榈树 - else: # forest - tree_type = 5 if height_value > 0.8 else 4 # 高海拔生成松树 - - self.vegetation_map[y, x] = tree_type - else: - # 生成地面植被 - ground_vegetation = [1, 2, 8, 9] # grass, shrub, bush, fern - self.vegetation_map[y, x] = np.random.choice(ground_vegetation) - - # 应用后处理 - print(" → 应用后处理...") - self.vegetation_map = self._post_process_vegetation(self.vegetation_map, heightmap, biome_map) - - # 更新统计信息 - generation_time = time.time() - generation_start_time - self.stats['vegetation_generated'] += 1 - self.stats['total_generation_time'] += generation_time - self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['vegetation_generated'] - - # 更新植被分布统计 - self._update_vegetation_distribution_stats(self.vegetation_map) - - print(f"✓ 森林植被生成完成,耗时: {generation_time:.2f}秒") - return self.vegetation_map - - except Exception as e: - print(f"✗ 森林植被生成失败: {e}") - import traceback - traceback.print_exc() - return np.zeros_like(heightmap, dtype=np.int32) - - def generate_desert_vegetation(self, heightmap: np.ndarray, biome_map: np.ndarray, - cactus_density: float = 0.05, seed: int = None) -> np.ndarray: - """ - 生成沙漠植被(专门用于沙漠区域) - - Args: - heightmap: 高度图数据 - biome_map: 生物群落图数据 - cactus_density: 仙人掌密度 - seed: 随机种子 - - Returns: - 植被分布图 - """ - try: - import time - generation_start_time = time.time() - - if not self.enabled: - print("✗ 植被生成器未启用") - return np.zeros_like(heightmap, dtype=np.int32) - - # 更新种子 - if seed is not None: - self.seed = seed - - print("✓ 开始生成沙漠植被...") - - # 初始化随机数生成器 - random.seed(self.seed) - np.random.seed(self.seed) - - # 创建植被图 - height, width = heightmap.shape - self.vegetation_map = np.zeros((height, width), dtype=np.int32) - - # 专门处理沙漠生物群落(ID 5) - desert_biome = 5 - - # 为每个像素生成沙漠植被 - print(" → 生成沙漠植被...") - for y in range(height): - for x in range(width): - biome_type = biome_map[y, x] if y < biome_map.shape[0] and x < biome_map.shape[1] else 0 - - # 只在沙漠生物群落中生成植被 - if biome_type == desert_biome: - # 根据仙人掌密度决定是否生成仙人掌 - if np.random.random() < cactus_density: - self.vegetation_map[y, x] = 7 # cactus - else: - # 生成稀疏的地面植被 - if np.random.random() < 0.1: # 10%概率 - ground_vegetation = [1, 2, 8] # grass, shrub, bush - self.vegetation_map[y, x] = np.random.choice(ground_vegetation) - - # 应用后处理 - print(" → 应用后处理...") - self.vegetation_map = self._post_process_vegetation(self.vegetation_map, heightmap, biome_map) - - # 更新统计信息 - generation_time = time.time() - generation_start_time - self.stats['vegetation_generated'] += 1 - self.stats['total_generation_time'] += generation_time - self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['vegetation_generated'] - - # 更新植被分布统计 - self._update_vegetation_distribution_stats(self.vegetation_map) - - print(f"✓ 沙漠植被生成完成,耗时: {generation_time:.2f}秒") - return self.vegetation_map - - except Exception as e: - print(f"✗ 沙漠植被生成失败: {e}") - import traceback - traceback.print_exc() - return np.zeros_like(heightmap, dtype=np.int32) - - def generate_water_vegetation(self, heightmap: np.ndarray, biome_map: np.ndarray, - water_level: float = 0.2, seed: int = None) -> np.ndarray: - """ - 生成水下植被(专门用于水域) - - Args: - heightmap: 高度图数据 - biome_map: 生物群落图数据 - water_level: 水位 - seed: 随机种子 - - Returns: - 植被分布图 - """ - try: - import time - generation_start_time = time.time() - - if not self.enabled: - print("✗ 植被生成器未启用") - return np.zeros_like(heightmap, dtype=np.int32) - - # 更新种子 - if seed is not None: - self.seed = seed - - print("✓ 开始生成水下植被...") - - # 初始化随机数生成器 - random.seed(self.seed) - np.random.seed(self.seed) - - # 创建植被图 - height, width = heightmap.shape - self.vegetation_map = np.zeros((height, width), dtype=np.int32) - - # 专门处理水域生物群落(ID 0) - water_biome = 0 - - # 为每个像素生成水下植被 - print(" → 生成水下植被...") - for y in range(height): - for x in range(width): - height_value = heightmap[y, x] - biome_type = biome_map[y, x] if y < biome_map.shape[0] and x < biome_map.shape[1] else 0 - - # 只在水域中且低于水位的地方生成植被 - if biome_type == water_biome and height_value < water_level: - # 生成水下植被 - water_vegetation = [12, 13, 14, 15] # seaweed, algae, kelp, coral - self.vegetation_map[y, x] = np.random.choice(water_vegetation) - - # 应用后处理 - print(" → 应用后处理...") - self.vegetation_map = self._post_process_vegetation(self.vegetation_map, heightmap, biome_map) - - # 更新统计信息 - generation_time = time.time() - generation_start_time - self.stats['vegetation_generated'] += 1 - self.stats['total_generation_time'] += generation_time - self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['vegetation_generated'] - - # 更新植被分布统计 - self._update_vegetation_distribution_stats(self.vegetation_map) - - print(f"✓ 水下植被生成完成,耗时: {generation_time:.2f}秒") - return self.vegetation_map - - except Exception as e: - print(f"✗ 水下植被生成失败: {e}") - import traceback - traceback.print_exc() - return np.zeros_like(heightmap, dtype=np.int32) - - def blend_vegetation_maps(self, vegetation_maps: List[np.ndarray], weights: List[float] = None) -> np.ndarray: - """ - 混合多个植被图 - - Args: - vegetation_maps: 植被图列表 - weights: 权重列表(如果为None,则平均分配权重) - - Returns: - 混合后的植被图 - """ - try: - if not vegetation_maps: - return np.array([]) - - if len(vegetation_maps) == 1: - return vegetation_maps[0].copy() - - # 确保所有植被图尺寸相同 - base_shape = vegetation_maps[0].shape - for vm in vegetation_maps: - if vm.shape != base_shape: - raise ValueError("所有植被图必须具有相同的尺寸") - - # 处理权重 - if weights is None: - weights = [1.0 / len(vegetation_maps)] * len(vegetation_maps) - elif len(weights) != len(vegetation_maps): - raise ValueError("权重数量必须与植被图数量相同") - - # 归一化权重 - total_weight = sum(weights) - if total_weight > 0: - weights = [w / total_weight for w in weights] - - # 混合植被图 - height, width = base_shape - blended = np.zeros(base_shape, dtype=np.int32) - - # 为每个像素计算加权植被类型 - for y in range(height): - for x in range(width): - # 计算每个植被类型的加权投票 - veg_votes = {} - for i, (vm, weight) in enumerate(zip(vegetation_maps, weights)): - veg_type = vm[y, x] - veg_votes[veg_type] = veg_votes.get(veg_type, 0.0) + weight - - # 选择得票最多的植被类型 - blended[y, x] = max(veg_votes, key=veg_votes.get) - - return blended - - except Exception as e: - print(f"✗ 植被图混合失败: {e}") - import traceback - traceback.print_exc() - # 返回第一个植被图的副本 - return vegetation_maps[0].copy() if vegetation_maps else np.array([]) - - def smooth_vegetation_map(self, vegetation_map: np.ndarray, iterations: int = 1) -> np.ndarray: - """ - 平滑植被图 - - Args: - vegetation_map: 原始植被图 - iterations: 平滑迭代次数 - - Returns: - 平滑后的植被图 - """ - try: - smoothed = vegetation_map.copy() - height, width = smoothed.shape - - for _ in range(iterations): - new_map = smoothed.copy() - - # 对每个像素进行平滑处理 - for y in range(height): - for x in range(width): - # 统计邻居的植被类型 - neighbor_types = [] - - # 检查8个邻居 - for dy in [-1, 0, 1]: - for dx in [-1, 0, 1]: - if dy == 0 and dx == 0: - continue - - ny, nx = y + dy, x + dx - if 0 <= ny < height and 0 <= nx < width: - neighbor_types.append(smoothed[ny, nx]) - - # 如果大多数邻居是相同类型,则改变当前像素 - if neighbor_types: - unique, counts = np.unique(neighbor_types, return_counts=True) - most_common_type = unique[np.argmax(counts)] - most_common_count = np.max(counts) - - # 如果超过一半的邻居是相同类型,则采用该类型 - if most_common_count > len(neighbor_types) / 2: - new_map[y, x] = most_common_type - - smoothed = new_map - - return smoothed - - except Exception as e: - print(f"✗ 植被图平滑失败: {e}") - return vegetation_map - - def generate_vegetation_colors(self, vegetation_map: np.ndarray) -> np.ndarray: - """ - 为植被图生成颜色表示 - - Args: - vegetation_map: 植被图 - - Returns: - 颜色图 (height, width, 3) - """ - try: - height, width = vegetation_map.shape - color_map = np.zeros((height, width, 3), dtype=np.uint8) - - # 为每个植被类型分配颜色 - for y in range(height): - for x in range(width): - veg_id = vegetation_map[y, x] - veg_info = self.vegetation_types.get(veg_id, {'color': (128, 128, 128)}) - color_map[y, x] = veg_info['color'] - - return color_map - - except Exception as e: - print(f"✗ 植被颜色图生成失败: {e}") - height, width = vegetation_map.shape - return np.zeros((height, width, 3), dtype=np.uint8) - - def export_vegetation_legend(self, filename: str) -> bool: - """ - 导出植被图例 - - Args: - filename: 文件名 - - Returns: - 是否导出成功 - """ - try: - import json - - legend_data = {} - for veg_id, veg_info in self.vegetation_types.items(): - legend_data[veg_id] = { - 'name': veg_info['name'], - 'color': veg_info['color'], - 'density': veg_info['density'] - } - - with open(filename, 'w', encoding='utf-8') as f: - json.dump(legend_data, f, ensure_ascii=False, indent=2) - - print(f"✓ 植被图例已导出到: {filename}") - return True - - except Exception as e: - print(f"✗ 植被图例导出失败: {e}") - return False - - def import_vegetation_legend(self, filename: str) -> bool: - """ - 导入植被图例 - - Args: - filename: 文件名 - - Returns: - 是否导入成功 - """ - try: - import json - - with open(filename, 'r', encoding='utf-8') as f: - legend_data = json.load(f) - - # 更新植被类型 - self.vegetation_types = {} - for veg_id, veg_info in legend_data.items(): - self.vegetation_types[int(veg_id)] = { - 'name': veg_info['name'], - 'color': tuple(veg_info['color']), - 'density': veg_info['density'] - } - - print(f"✓ 植被图例已从 {filename} 导入") - return True - - except Exception as e: - print(f"✗ 植被图例导入失败: {e}") - return False - - def get_vegetation_distribution_stats(self) -> Dict[str, int]: - """ - 获取植被分布统计 - - Returns: - 植被分布统计字典 - """ - return self.stats['vegetation_distribution'].copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'vegetation_generated': 0, - 'total_generation_time': 0.0, - 'average_generation_time': 0.0, - 'vegetation_distribution': {} - } - print("✓ 植被生成器统计信息已重置") - - def set_vegetation_types(self, vegetation_types: Dict[int, Dict[str, Any]]): - """ - 设置植被类型 - - Args: - vegetation_types: 植被类型字典 - """ - self.vegetation_types = vegetation_types - print(f"✓ 植被类型已更新,共 {len(vegetation_types)} 种植被") - - def add_vegetation_type(self, vegetation_id: int, vegetation_info: Dict[str, Any]): - """ - 添加植被类型 - - Args: - vegetation_id: 植被ID - vegetation_info: 植被信息 - """ - self.vegetation_types[vegetation_id] = vegetation_info - print(f"✓ 植被类型 {vegetation_info['name']} (ID: {vegetation_id}) 已添加") - - def remove_vegetation_type(self, vegetation_id: int): - """ - 移除植被类型 - - Args: - vegetation_id: 植被ID - """ - if vegetation_id in self.vegetation_types: - veg_name = self.vegetation_types[vegetation_id]['name'] - del self.vegetation_types[vegetation_id] - print(f"✓ 植被类型 {veg_name} (ID: {vegetation_id}) 已移除") - else: - print(f"✗ 无效的植被ID: {vegetation_id}") - - def modify_vegetation_type(self, vegetation_id: int, modifications: Dict[str, Any]): - """ - 修改植被类型 - - Args: - vegetation_id: 植被ID - modifications: 修改内容 - """ - if vegetation_id in self.vegetation_types: - self.vegetation_types[vegetation_id].update(modifications) - print(f"✓ 植被类型 (ID: {vegetation_id}) 已修改") - else: - print(f"✗ 无效的植被ID: {vegetation_id}") - - def set_biome_vegetation(self, biome_id: int, vegetation_types: List[int]): - """ - 设置生物群落的植被类型 - - Args: - biome_id: 生物群落ID - vegetation_types: 植被类型列表 - """ - self.biome_vegetation[biome_id] = vegetation_types - print(f"✓ 生物群落 {biome_id} 的植被类型已设置为: {vegetation_types}") - - def get_biome_vegetation(self, biome_id: int) -> List[int]: - """ - 获取生物群落的植被类型 - - Args: - biome_id: 生物群落ID - - Returns: - 植被类型列表 - """ - return self.biome_vegetation.get(biome_id, []).copy() - - def generate_vegetation_density_map(self, heightmap: np.ndarray, biome_map: np.ndarray) -> np.ndarray: - """ - 生成植被密度图 - - Args: - heightmap: 高度图数据 - biome_map: 生物群落图数据 - - Returns: - 植被密度图 - """ - try: - height, width = heightmap.shape - density_map = np.zeros((height, width), dtype=np.float32) - - # 为每个生物群落生成不同的密度模式 - for y in range(height): - for x in range(width): - biome_type = biome_map[y, x] if y < biome_map.shape[0] and x < biome_map.shape[1] else 0 - height_value = heightmap[y, x] - - # 根据生物群落类型确定基础密度 - if biome_type == 0: # ocean - density_map[y, x] = 0.0 - elif biome_type == 1: # beach - density_map[y, x] = 0.1 - elif biome_type == 2: # plains - density_map[y, x] = 0.4 - elif biome_type in [3, 4]: # forest, jungle - density_map[y, x] = 0.8 - elif biome_type == 5: # desert - density_map[y, x] = 0.05 - elif biome_type == 6: # mountain - # 山地密度随高度变化 - density_map[y, x] = max(0.0, 0.6 - height_value) - elif biome_type in [7, 8, 9]: # snow, taiga, tundra - density_map[y, x] = 0.3 - elif biome_type == 10: # swamp - density_map[y, x] = 0.6 - elif biome_type == 11: # savanna - density_map[y, x] = 0.3 - else: - density_map[y, x] = 0.2 - - # 应用噪声以增加自然变化 - if self.noise_generator: - for y in range(height): - for x in range(width): - noise_value = self.noise_generator.generate_noise( - x / 20.0, y / 20.0, self.seed + 5000 - ) - density_map[y, x] = max(0.0, min(1.0, density_map[y, x] + noise_value * 0.1)) - - return density_map - - except Exception as e: - print(f"✗ 植被密度图生成失败: {e}") - return np.zeros_like(heightmap, dtype=np.float32) - - def apply_vegetation_mask(self, vegetation_map: np.ndarray, mask: np.ndarray) -> np.ndarray: - """ - 应用掩码到植被图 - - Args: - vegetation_map: 原始植被图 - mask: 掩码(0-1范围,0表示移除植被) - - Returns: - 处理后的植被图 - """ - try: - if mask.shape != vegetation_map.shape: - print("✗ 掩码尺寸与植被图不匹配") - return vegetation_map - - masked = vegetation_map.copy() - - # 根据掩码值调整植被 - for y in range(masked.shape[0]): - for x in range(masked.shape[1]): - # 掩码值越小,移除植被的概率越大 - if np.random.random() > mask[y, x]: - masked[y, x] = 0 # 移除植被 - - return masked - - except Exception as e: - print(f"✗ 植被掩码应用失败: {e}") - return vegetation_map - - def generate_clustered_vegetation(self, heightmap: np.ndarray, biome_map: np.ndarray, - cluster_density: float = 0.2, seed: int = None) -> np.ndarray: - """ - 生成聚类植被 - - Args: - heightmap: 高度图数据 - biome_map: 生物群落图数据 - cluster_density: 聚类密度 - seed: 随机种子 - - Returns: - 植被分布图 - """ - try: - import time - generation_start_time = time.time() - - if not self.enabled: - print("✗ 植被生成器未启用") - return np.zeros_like(heightmap, dtype=np.int32) - - # 更新种子 - if seed is not None: - self.seed = seed - - print("✓ 开始生成聚类植被...") - - # 初始化随机数生成器 - random.seed(self.seed) - np.random.seed(self.seed) - - # 创建植被图 - height, width = heightmap.shape - self.vegetation_map = np.zeros((height, width), dtype=np.int32) - - # 生成聚类中心点 - num_clusters = int(width * height * cluster_density / 100) - clusters = [] - for _ in range(num_clusters): - x = np.random.randint(0, width) - y = np.random.randint(0, height) - biome_type = biome_map[y, x] if y < biome_map.shape[0] and x < biome_map.shape[1] else 0 - # 获取该生物群落的植被类型 - veg_types = self.biome_vegetation.get(biome_type, [1, 2]) # 默认为草和灌木 - veg_type = np.random.choice(veg_types) if veg_types else 1 - cluster_size = np.random.randint(5, 20) # 聚类大小 - clusters.append((x, y, veg_type, cluster_size)) - - # 在聚类中心点周围生成植被 - print(" → 生成植被聚类...") - for cx, cy, veg_type, cluster_size in clusters: - for _ in range(cluster_size): - # 在聚类中心附近随机生成植被 - angle = np.random.random() * 2 * np.pi - distance = np.random.random() * cluster_size / 2 - x = int(cx + np.cos(angle) * distance) - y = int(cy + np.sin(angle) * distance) - - # 确保坐标在有效范围内 - if 0 <= x < width and 0 <= y < height: - # 检查该位置是否适合该植被类型 - height_value = heightmap[y, x] - biome_type = biome_map[y, x] if y < biome_map.shape[0] and x < biome_map.shape[1] else 0 - - # 简单的适宜性检查 - if height_value > 0.1 and biome_type != 0: # 不在水中 - self.vegetation_map[y, x] = veg_type - - # 应用后处理 - print(" → 应用后处理...") - self.vegetation_map = self._post_process_vegetation(self.vegetation_map, heightmap, biome_map) - - # 更新统计信息 - generation_time = time.time() - generation_start_time - self.stats['vegetation_generated'] += 1 - self.stats['total_generation_time'] += generation_time - self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['vegetation_generated'] - - # 更新植被分布统计 - self._update_vegetation_distribution_stats(self.vegetation_map) - - print(f"✓ 聚类植被生成完成,耗时: {generation_time:.2f}秒") - return self.vegetation_map - - except Exception as e: - print(f"✗ 聚类植被生成失败: {e}") - import traceback - traceback.print_exc() +""" +植被生成器 +负责生成程序化地形的植被分布 +""" + +import numpy as np +import math +from typing import Dict, Any, List, Tuple +import random + +class VegetationGenerator: + """ + 植被生成器 + 负责生成程序化地形的植被分布,基于生物群落、湿度、温度等因素 + """ + + def __init__(self, plugin): + """ + 初始化植被生成器 + + Args: + plugin: 程序化地形生成插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 植被配置 + self.seed = plugin.config.get('seed', 12345) + + # 植被类型定义 + self.vegetation_types = { + 0: {'name': 'none', 'density': 0.0, 'color': (0, 0, 0)}, + 1: {'name': 'grass', 'density': 0.3, 'color': (144, 238, 144)}, + 2: {'name': 'shrub', 'density': 0.2, 'color': (107, 142, 35)}, + 3: {'name': 'flower', 'density': 0.1, 'color': (255, 105, 180)}, + 4: {'name': 'tree', 'density': 0.15, 'color': (34, 139, 34)}, + 5: {'name': 'pine_tree', 'density': 0.12, 'color': (0, 128, 0)}, + 6: {'name': 'palm_tree', 'density': 0.08, 'color': (0, 100, 0)}, + 7: {'name': 'cactus', 'density': 0.05, 'color': (0, 100, 0)}, + 8: {'name': 'bush', 'density': 0.25, 'color': (107, 142, 35)}, + 9: {'name': 'fern', 'density': 0.2, 'color': (34, 139, 34)}, + 10: {'name': 'moss', 'density': 0.15, 'color': (144, 238, 144)}, + 11: {'name': 'vine', 'density': 0.1, 'color': (0, 128, 0)}, + 12: {'name': 'seaweed', 'density': 0.4, 'color': (0, 128, 0)}, + 13: {'name': 'algae', 'density': 0.35, 'color': (0, 100, 0)}, + 14: {'name': 'kelp', 'density': 0.3, 'color': (0, 128, 0)}, + 15: {'name': 'coral', 'density': 0.25, 'color': (255, 99, 71)} + } + + # 生物群落植被分布 + self.biome_vegetation = { + 0: [0, 12, 13, 14, 15], # ocean + 1: [0, 1, 2], # beach + 2: [0, 1, 2, 8], # plains + 3: [0, 1, 2, 4, 9], # forest + 4: [0, 1, 2, 4, 6, 9, 11], # jungle + 5: [0, 1, 2, 7], # desert + 6: [0, 1, 2, 5], # mountain + 7: [0, 1, 2], # snow + 8: [0, 1, 2, 5, 10], # taiga + 9: [0, 1, 2], # tundra + 10: [0, 1, 2, 3, 9, 10], # swamp + 11: [0, 1, 2, 8] # savanna + } + + # 植被生成参数 + self.vegetation_params = { + 'density_scale': 10.0, + 'clumping_factor': 0.7, + 'slope_tolerance': 0.3, + 'height_min': 0.0, + 'height_max': 1.0, + 'humidity_min': 0.0, + 'humidity_max': 1.0, + 'temperature_min': 0.0, + 'temperature_max': 1.0, + 'edge_blending': 0.05 + } + + # 噪声生成器引用 + self.noise_generator = None + + # 植被分布图 + self.vegetation_map = None + + # 统计信息 + self.stats = { + 'vegetation_generated': 0, + 'total_generation_time': 0.0, + 'average_generation_time': 0.0, + 'vegetation_distribution': {} + } + + print("✓ 植被生成器已创建") + + def initialize(self) -> bool: + """ + 初始化植被生成器 + + Returns: + 是否初始化成功 + """ + try: + # 获取噪声生成器引用 + self.noise_generator = self.plugin.noise_generator + + self.initialized = True + print("✓ 植被生成器初始化完成") + return True + + except Exception as e: + print(f"✗ 植被生成器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用植被生成器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 植被生成器未初始化") + return False + + self.enabled = True + print("✓ 植被生成器已启用") + return True + + except Exception as e: + print(f"✗ 植被生成器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用植被生成器""" + try: + self.enabled = False + print("✓ 植被生成器已禁用") + + except Exception as e: + print(f"✗ 植被生成器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理植被生成器资源""" + try: + self.disable() + self.initialized = False + print("✓ 植被生成器资源已清理") + + except Exception as e: + print(f"✗ 植被生成器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新植被生成器状态 + + Args: + dt: 时间增量 + """ + # 处理更新逻辑 + pass + + def generate_vegetation(self, heightmap: np.ndarray, biome_map: np.ndarray, seed: int = None) -> np.ndarray: + """ + 生成植被分布 + + Args: + heightmap: 高度图数据 + biome_map: 生物群落图数据 + seed: 随机种子 + + Returns: + 植被分布图 + """ + try: + import time + generation_start_time = time.time() + + if not self.enabled: + print("✗ 植被生成器未启用") + return np.zeros_like(heightmap, dtype=np.int32) + + # 更新种子 + if seed is not None: + self.seed = seed + + print("✓ 开始生成植被分布...") + + # 初始化随机数生成器 + random.seed(self.seed) + np.random.seed(self.seed) + + # 创建植被图 + height, width = heightmap.shape + self.vegetation_map = np.zeros((height, width), dtype=np.int32) + + # 生成基础植被噪声 + vegetation_noise = self._generate_vegetation_noise(width, height) + + # 为每个像素生成植被 + print(" → 生成植被类型...") + for y in range(height): + for x in range(width): + # 获取当前位置的属性 + height_value = heightmap[y, x] + biome_type = biome_map[y, x] if y < biome_map.shape[0] and x < biome_map.shape[1] else 0 + + # 生成植被 + vegetation_type = self._generate_vegetation_at_position( + x, y, height_value, biome_type, vegetation_noise[y, x] + ) + self.vegetation_map[y, x] = vegetation_type + + # 应用后处理 + print(" → 应用后处理...") + self.vegetation_map = self._post_process_vegetation(self.vegetation_map, heightmap, biome_map) + + # 更新统计信息 + generation_time = time.time() - generation_start_time + self.stats['vegetation_generated'] += 1 + self.stats['total_generation_time'] += generation_time + self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['vegetation_generated'] + + # 更新植被分布统计 + self._update_vegetation_distribution_stats(self.vegetation_map) + + print(f"✓ 植被生成完成,耗时: {generation_time:.2f}秒") + return self.vegetation_map + + except Exception as e: + print(f"✗ 植被生成失败: {e}") + import traceback + traceback.print_exc() + return np.zeros_like(heightmap, dtype=np.int32) + + def _generate_vegetation_noise(self, width: int, height: int) -> np.ndarray: + """ + 生成植被噪声图 + + Args: + width: 宽度 + height: 高度 + + Returns: + 植被噪声图 + """ + try: + noise_map = np.zeros((height, width), dtype=np.float32) + + if self.noise_generator: + # 生成分形噪声 + for y in range(height): + for x in range(width): + noise_value = self.noise_generator.generate_fractal_noise( + x / self.vegetation_params['density_scale'], + y / self.vegetation_params['density_scale'], + self.seed, + octaves=3, + persistence=0.7, + lacunarity=2.0 + ) + noise_map[y, x] = (noise_value + 1.0) / 2.0 # 转换到0-1范围 + else: + # 如果没有噪声生成器,使用随机噪声 + noise_map = np.random.rand(height, width).astype(np.float32) + + return noise_map + + except Exception as e: + print(f"✗ 植被噪声生成失败: {e}") + return np.random.rand(height, width).astype(np.float32) + + def _generate_vegetation_at_position(self, x: int, y: int, height: float, biome: int, noise: float) -> int: + """ + 在指定位置生成植被 + + Args: + x: X坐标 + y: Y坐标 + height: 高度值 + biome: 生物群落类型 + noise: 噪声值 + + Returns: + 植被类型ID + """ + try: + # 检查高度限制 + if height < self.vegetation_params['height_min'] or height > self.vegetation_params['height_max']: + return 0 # 无植被 + + # 获取生物群落的植被类型 + biome_veg_types = self.biome_vegetation.get(biome, [0]) + + # 根据噪声值选择植被类型 + if noise > 0.7 and len(biome_veg_types) > 1: + # 高噪声值,选择较稀有的植被 + weights = [self.vegetation_types.get(v, {'density': 0.1})['density'] for v in biome_veg_types] + # 反转权重以使稀有植被更容易被选中 + weights = [1.0 / (w + 0.01) for w in weights] + vegetation_type = np.random.choice(biome_veg_types, p=np.array(weights) / sum(weights)) + elif noise > 0.4 and len(biome_veg_types) > 1: + # 中等噪声值,随机选择 + vegetation_type = np.random.choice(biome_veg_types) + elif len(biome_veg_types) > 0: + # 低噪声值,选择常见的植被 + weights = [self.vegetation_types.get(v, {'density': 0.1})['density'] for v in biome_veg_types] + vegetation_type = np.random.choice(biome_veg_types, p=np.array(weights) / sum(weights)) + else: + vegetation_type = 0 # 无植被 + + return vegetation_type + + except Exception as e: + print(f"✗ 位置植被生成失败: {e}") + return 0 + + def _post_process_vegetation(self, vegetation_map: np.ndarray, heightmap: np.ndarray, biome_map: np.ndarray) -> np.ndarray: + """ + 后处理植被图 + + Args: + vegetation_map: 原始植被图 + heightmap: 高度图 + biome_map: 生物群落图 + + Returns: + 处理后的植被图 + """ + try: + processed = vegetation_map.copy() + height, width = processed.shape + + # 应用聚类效果 + processed = self._apply_clumping(processed, heightmap) + + # 应用坡度限制 + processed = self._apply_slope_limit(processed, heightmap) + + # 应用边缘混合 + processed = self._apply_edge_blending(processed) + + return processed + + except Exception as e: + print(f"✗ 植被后处理失败: {e}") + return vegetation_map + + def _apply_clumping(self, vegetation_map: np.ndarray, heightmap: np.ndarray) -> np.ndarray: + """ + 应用植被聚类效果 + + Args: + vegetation_map: 植被图 + heightmap: 高度图 + + Returns: + 处理后的植被图 + """ + try: + processed = vegetation_map.copy() + height, width = processed.shape + clumping_factor = self.vegetation_params['clumping_factor'] + + # 对每个植被类型应用聚类 + for y in range(height): + for x in range(width): + if vegetation_map[y, x] != 0: # 如果有植被 + # 检查邻居是否有相同类型的植被 + same_type_neighbors = 0 + total_neighbors = 0 + + for dy in [-1, 0, 1]: + for dx in [-1, 0, 1]: + if dy == 0 and dx == 0: + continue + + ny, nx = y + dy, x + dx + if 0 <= ny < height and 0 <= nx < width: + total_neighbors += 1 + if vegetation_map[ny, nx] == vegetation_map[y, x]: + same_type_neighbors += 1 + + # 根据邻居情况调整植被类型 + if total_neighbors > 0: + similarity = same_type_neighbors / total_neighbors + if similarity < clumping_factor and np.random.random() > 0.5: + # 减少孤立的植被 + processed[y, x] = 0 + + return processed + + except Exception as e: + print(f"✗ 植被聚类应用失败: {e}") + return vegetation_map + + def _apply_slope_limit(self, vegetation_map: np.ndarray, heightmap: np.ndarray) -> np.ndarray: + """ + 应用坡度限制 + + Args: + vegetation_map: 植被图 + heightmap: 高度图 + + Returns: + 处理后的植被图 + """ + try: + processed = vegetation_map.copy() + height, width = processed.shape + slope_tolerance = self.vegetation_params['slope_tolerance'] + + # 计算坡度并应用限制 + for y in range(height): + for x in range(width): + if vegetation_map[y, x] != 0: # 如果有植被 + # 计算当前位置的坡度 + slope = self._calculate_slope(heightmap, x, y) + + # 如果坡度太大,移除植被 + if slope > slope_tolerance: + processed[y, x] = 0 + + return processed + + except Exception as e: + print(f"✗ 坡度限制应用失败: {e}") + return vegetation_map + + def _calculate_slope(self, heightmap: np.ndarray, x: int, y: int) -> float: + """ + 计算指定位置的坡度 + + Args: + heightmap: 高度图 + x: X坐标 + y: Y坐标 + + Returns: + 坡度值 + """ + try: + height, width = heightmap.shape + + # 计算梯度 + dx = 0.0 + dy = 0.0 + + # X方向梯度 + if x > 0 and x < width - 1: + dx = (heightmap[y, x + 1] - heightmap[y, x - 1]) / 2.0 + elif x == 0: + dx = heightmap[y, x + 1] - heightmap[y, x] + else: + dx = heightmap[y, x] - heightmap[y, x - 1] + + # Y方向梯度 + if y > 0 and y < height - 1: + dy = (heightmap[y + 1, x] - heightmap[y - 1, x]) / 2.0 + elif y == 0: + dy = heightmap[y + 1, x] - heightmap[y, x] + else: + dy = heightmap[y, x] - heightmap[y - 1, x] + + # 计算坡度(梯度的模) + slope = math.sqrt(dx * dx + dy * dy) + return slope + + except Exception as e: + print(f"✗ 坡度计算失败: {e}") + return 0.0 + + def _apply_edge_blending(self, vegetation_map: np.ndarray) -> np.ndarray: + """ + 应用边缘混合 + + Args: + vegetation_map: 植被图 + + Returns: + 处理后的植被图 + """ + try: + processed = vegetation_map.copy() + height, width = processed.shape + blend_width = max(1, int(min(height, width) * self.vegetation_params['edge_blending'])) + + # 简化的边缘混合 + for y in range(height): + for x in range(width): + # 在边缘区域随机减少植被 + if (x < blend_width or x > width - blend_width or + y < blend_width or y > height - blend_width): + if np.random.random() < 0.3: # 30%概率移除边缘植被 + processed[y, x] = 0 + + return processed + + except Exception as e: + print(f"✗ 边缘混合应用失败: {e}") + return vegetation_map + + def _update_vegetation_distribution_stats(self, vegetation_map: np.ndarray): + """更新植被分布统计""" + try: + unique, counts = np.unique(vegetation_map, return_counts=True) + for veg_id, count in zip(unique, counts): + veg_name = self.vegetation_types.get(veg_id, {}).get('name', f'vegetation_{veg_id}') + self.stats['vegetation_distribution'][veg_name] = self.stats['vegetation_distribution'].get(veg_name, 0) + count + + except Exception as e: + print(f"✗ 植被分布统计更新失败: {e}") + + def set_seed(self, seed: int): + """ + 设置随机种子 + + Args: + seed: 随机种子 + """ + self.seed = seed + print(f"✓ 植被生成器随机种子设置为: {seed}") + + def set_vegetation_parameters(self, params: Dict[str, Any]): + """ + 设置植被生成参数 + + Args: + params: 参数字典 + """ + self.vegetation_params.update(params) + print(f"✓ 植被生成参数已更新: {self.vegetation_params}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + # 更新平均生成时间 + if self.stats['vegetation_generated'] > 0: + self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['vegetation_generated'] + return self.stats.copy() + + def get_vegetation_info(self, vegetation_id: int) -> Dict[str, Any]: + """ + 获取植被信息 + + Args: + vegetation_id: 植被ID + + Returns: + 植被信息字典 + """ + return self.vegetation_types.get(vegetation_id, { + 'name': f'unknown_{vegetation_id}', + 'density': 0.1, + 'color': (128, 128, 128) + }) + + def get_available_vegetation(self) -> List[Dict[str, Any]]: + """ + 获取可用的植被类型列表 + + Returns: + 植被信息列表 + """ + return [self.get_vegetation_info(veg_id) for veg_id in sorted(self.vegetation_types.keys())] + + def generate_vegetation_with_density(self, heightmap: np.ndarray, biome_map: np.ndarray, + density_map: np.ndarray, seed: int = None) -> np.ndarray: + """ + 根据密度图生成植被 + + Args: + heightmap: 高度图数据 + biome_map: 生物群落图数据 + density_map: 植被密度图数据 + seed: 随机种子 + + Returns: + 植被分布图 + """ + try: + import time + generation_start_time = time.time() + + if not self.enabled: + print("✗ 植被生成器未启用") + return np.zeros_like(heightmap, dtype=np.int32) + + # 更新种子 + if seed is not None: + self.seed = seed + + print("✓ 开始生成基于密度的植被分布...") + + # 初始化随机数生成器 + random.seed(self.seed) + np.random.seed(self.seed) + + # 确保密度图尺寸匹配 + if density_map.shape != heightmap.shape: + print("✗ 密度图尺寸与高度图不匹配") + return np.zeros_like(heightmap, dtype=np.int32) + + # 创建植被图 + height, width = heightmap.shape + self.vegetation_map = np.zeros((height, width), dtype=np.int32) + + # 为每个像素生成植被 + print(" → 生成密度控制的植被...") + for y in range(height): + for x in range(width): + # 获取当前位置的属性 + height_value = heightmap[y, x] + biome_type = biome_map[y, x] if y < biome_map.shape[0] and x < biome_map.shape[1] else 0 + density_value = density_map[y, x] + + # 根据密度值决定是否生成植被 + if np.random.random() < density_value: + # 生成植被 + vegetation_type = self._generate_vegetation_at_position( + x, y, height_value, biome_type, np.random.random() + ) + self.vegetation_map[y, x] = vegetation_type + + # 应用后处理 + print(" → 应用后处理...") + self.vegetation_map = self._post_process_vegetation(self.vegetation_map, heightmap, biome_map) + + # 更新统计信息 + generation_time = time.time() - generation_start_time + self.stats['vegetation_generated'] += 1 + self.stats['total_generation_time'] += generation_time + self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['vegetation_generated'] + + # 更新植被分布统计 + self._update_vegetation_distribution_stats(self.vegetation_map) + + print(f"✓ 基于密度的植被生成完成,耗时: {generation_time:.2f}秒") + return self.vegetation_map + + except Exception as e: + print(f"✗ 基于密度的植被生成失败: {e}") + import traceback + traceback.print_exc() + return np.zeros_like(heightmap, dtype=np.int32) + + def generate_forest_vegetation(self, heightmap: np.ndarray, biome_map: np.ndarray, + tree_density: float = 0.15, seed: int = None) -> np.ndarray: + """ + 生成森林植被(专门用于森林区域) + + Args: + heightmap: 高度图数据 + biome_map: 生物群落图数据 + tree_density: 树木密度 + seed: 随机种子 + + Returns: + 植被分布图 + """ + try: + import time + generation_start_time = time.time() + + if not self.enabled: + print("✗ 植被生成器未启用") + return np.zeros_like(heightmap, dtype=np.int32) + + # 更新种子 + if seed is not None: + self.seed = seed + + print("✓ 开始生成森林植被...") + + # 初始化随机数生成器 + random.seed(self.seed) + np.random.seed(self.seed) + + # 创建植被图 + height, width = heightmap.shape + self.vegetation_map = np.zeros((height, width), dtype=np.int32) + + # 专门处理森林生物群落(ID 3 和 4) + forest_biomes = [3, 4] # forest 和 jungle + + # 为每个像素生成森林植被 + print(" → 生成森林植被...") + for y in range(height): + for x in range(width): + height_value = heightmap[y, x] + biome_type = biome_map[y, x] if y < biome_map.shape[0] and x < biome_map.shape[1] else 0 + + # 只在森林生物群落中生成植被 + if biome_type in forest_biomes: + # 根据树木密度决定是否生成树 + if np.random.random() < tree_density: + # 选择合适的树种 + if biome_type == 4: # jungle + tree_type = 6 if np.random.random() < 0.3 else 4 # 30%概率生成棕榈树 + else: # forest + tree_type = 5 if height_value > 0.8 else 4 # 高海拔生成松树 + + self.vegetation_map[y, x] = tree_type + else: + # 生成地面植被 + ground_vegetation = [1, 2, 8, 9] # grass, shrub, bush, fern + self.vegetation_map[y, x] = np.random.choice(ground_vegetation) + + # 应用后处理 + print(" → 应用后处理...") + self.vegetation_map = self._post_process_vegetation(self.vegetation_map, heightmap, biome_map) + + # 更新统计信息 + generation_time = time.time() - generation_start_time + self.stats['vegetation_generated'] += 1 + self.stats['total_generation_time'] += generation_time + self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['vegetation_generated'] + + # 更新植被分布统计 + self._update_vegetation_distribution_stats(self.vegetation_map) + + print(f"✓ 森林植被生成完成,耗时: {generation_time:.2f}秒") + return self.vegetation_map + + except Exception as e: + print(f"✗ 森林植被生成失败: {e}") + import traceback + traceback.print_exc() + return np.zeros_like(heightmap, dtype=np.int32) + + def generate_desert_vegetation(self, heightmap: np.ndarray, biome_map: np.ndarray, + cactus_density: float = 0.05, seed: int = None) -> np.ndarray: + """ + 生成沙漠植被(专门用于沙漠区域) + + Args: + heightmap: 高度图数据 + biome_map: 生物群落图数据 + cactus_density: 仙人掌密度 + seed: 随机种子 + + Returns: + 植被分布图 + """ + try: + import time + generation_start_time = time.time() + + if not self.enabled: + print("✗ 植被生成器未启用") + return np.zeros_like(heightmap, dtype=np.int32) + + # 更新种子 + if seed is not None: + self.seed = seed + + print("✓ 开始生成沙漠植被...") + + # 初始化随机数生成器 + random.seed(self.seed) + np.random.seed(self.seed) + + # 创建植被图 + height, width = heightmap.shape + self.vegetation_map = np.zeros((height, width), dtype=np.int32) + + # 专门处理沙漠生物群落(ID 5) + desert_biome = 5 + + # 为每个像素生成沙漠植被 + print(" → 生成沙漠植被...") + for y in range(height): + for x in range(width): + biome_type = biome_map[y, x] if y < biome_map.shape[0] and x < biome_map.shape[1] else 0 + + # 只在沙漠生物群落中生成植被 + if biome_type == desert_biome: + # 根据仙人掌密度决定是否生成仙人掌 + if np.random.random() < cactus_density: + self.vegetation_map[y, x] = 7 # cactus + else: + # 生成稀疏的地面植被 + if np.random.random() < 0.1: # 10%概率 + ground_vegetation = [1, 2, 8] # grass, shrub, bush + self.vegetation_map[y, x] = np.random.choice(ground_vegetation) + + # 应用后处理 + print(" → 应用后处理...") + self.vegetation_map = self._post_process_vegetation(self.vegetation_map, heightmap, biome_map) + + # 更新统计信息 + generation_time = time.time() - generation_start_time + self.stats['vegetation_generated'] += 1 + self.stats['total_generation_time'] += generation_time + self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['vegetation_generated'] + + # 更新植被分布统计 + self._update_vegetation_distribution_stats(self.vegetation_map) + + print(f"✓ 沙漠植被生成完成,耗时: {generation_time:.2f}秒") + return self.vegetation_map + + except Exception as e: + print(f"✗ 沙漠植被生成失败: {e}") + import traceback + traceback.print_exc() + return np.zeros_like(heightmap, dtype=np.int32) + + def generate_water_vegetation(self, heightmap: np.ndarray, biome_map: np.ndarray, + water_level: float = 0.2, seed: int = None) -> np.ndarray: + """ + 生成水下植被(专门用于水域) + + Args: + heightmap: 高度图数据 + biome_map: 生物群落图数据 + water_level: 水位 + seed: 随机种子 + + Returns: + 植被分布图 + """ + try: + import time + generation_start_time = time.time() + + if not self.enabled: + print("✗ 植被生成器未启用") + return np.zeros_like(heightmap, dtype=np.int32) + + # 更新种子 + if seed is not None: + self.seed = seed + + print("✓ 开始生成水下植被...") + + # 初始化随机数生成器 + random.seed(self.seed) + np.random.seed(self.seed) + + # 创建植被图 + height, width = heightmap.shape + self.vegetation_map = np.zeros((height, width), dtype=np.int32) + + # 专门处理水域生物群落(ID 0) + water_biome = 0 + + # 为每个像素生成水下植被 + print(" → 生成水下植被...") + for y in range(height): + for x in range(width): + height_value = heightmap[y, x] + biome_type = biome_map[y, x] if y < biome_map.shape[0] and x < biome_map.shape[1] else 0 + + # 只在水域中且低于水位的地方生成植被 + if biome_type == water_biome and height_value < water_level: + # 生成水下植被 + water_vegetation = [12, 13, 14, 15] # seaweed, algae, kelp, coral + self.vegetation_map[y, x] = np.random.choice(water_vegetation) + + # 应用后处理 + print(" → 应用后处理...") + self.vegetation_map = self._post_process_vegetation(self.vegetation_map, heightmap, biome_map) + + # 更新统计信息 + generation_time = time.time() - generation_start_time + self.stats['vegetation_generated'] += 1 + self.stats['total_generation_time'] += generation_time + self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['vegetation_generated'] + + # 更新植被分布统计 + self._update_vegetation_distribution_stats(self.vegetation_map) + + print(f"✓ 水下植被生成完成,耗时: {generation_time:.2f}秒") + return self.vegetation_map + + except Exception as e: + print(f"✗ 水下植被生成失败: {e}") + import traceback + traceback.print_exc() + return np.zeros_like(heightmap, dtype=np.int32) + + def blend_vegetation_maps(self, vegetation_maps: List[np.ndarray], weights: List[float] = None) -> np.ndarray: + """ + 混合多个植被图 + + Args: + vegetation_maps: 植被图列表 + weights: 权重列表(如果为None,则平均分配权重) + + Returns: + 混合后的植被图 + """ + try: + if not vegetation_maps: + return np.array([]) + + if len(vegetation_maps) == 1: + return vegetation_maps[0].copy() + + # 确保所有植被图尺寸相同 + base_shape = vegetation_maps[0].shape + for vm in vegetation_maps: + if vm.shape != base_shape: + raise ValueError("所有植被图必须具有相同的尺寸") + + # 处理权重 + if weights is None: + weights = [1.0 / len(vegetation_maps)] * len(vegetation_maps) + elif len(weights) != len(vegetation_maps): + raise ValueError("权重数量必须与植被图数量相同") + + # 归一化权重 + total_weight = sum(weights) + if total_weight > 0: + weights = [w / total_weight for w in weights] + + # 混合植被图 + height, width = base_shape + blended = np.zeros(base_shape, dtype=np.int32) + + # 为每个像素计算加权植被类型 + for y in range(height): + for x in range(width): + # 计算每个植被类型的加权投票 + veg_votes = {} + for i, (vm, weight) in enumerate(zip(vegetation_maps, weights)): + veg_type = vm[y, x] + veg_votes[veg_type] = veg_votes.get(veg_type, 0.0) + weight + + # 选择得票最多的植被类型 + blended[y, x] = max(veg_votes, key=veg_votes.get) + + return blended + + except Exception as e: + print(f"✗ 植被图混合失败: {e}") + import traceback + traceback.print_exc() + # 返回第一个植被图的副本 + return vegetation_maps[0].copy() if vegetation_maps else np.array([]) + + def smooth_vegetation_map(self, vegetation_map: np.ndarray, iterations: int = 1) -> np.ndarray: + """ + 平滑植被图 + + Args: + vegetation_map: 原始植被图 + iterations: 平滑迭代次数 + + Returns: + 平滑后的植被图 + """ + try: + smoothed = vegetation_map.copy() + height, width = smoothed.shape + + for _ in range(iterations): + new_map = smoothed.copy() + + # 对每个像素进行平滑处理 + for y in range(height): + for x in range(width): + # 统计邻居的植被类型 + neighbor_types = [] + + # 检查8个邻居 + for dy in [-1, 0, 1]: + for dx in [-1, 0, 1]: + if dy == 0 and dx == 0: + continue + + ny, nx = y + dy, x + dx + if 0 <= ny < height and 0 <= nx < width: + neighbor_types.append(smoothed[ny, nx]) + + # 如果大多数邻居是相同类型,则改变当前像素 + if neighbor_types: + unique, counts = np.unique(neighbor_types, return_counts=True) + most_common_type = unique[np.argmax(counts)] + most_common_count = np.max(counts) + + # 如果超过一半的邻居是相同类型,则采用该类型 + if most_common_count > len(neighbor_types) / 2: + new_map[y, x] = most_common_type + + smoothed = new_map + + return smoothed + + except Exception as e: + print(f"✗ 植被图平滑失败: {e}") + return vegetation_map + + def generate_vegetation_colors(self, vegetation_map: np.ndarray) -> np.ndarray: + """ + 为植被图生成颜色表示 + + Args: + vegetation_map: 植被图 + + Returns: + 颜色图 (height, width, 3) + """ + try: + height, width = vegetation_map.shape + color_map = np.zeros((height, width, 3), dtype=np.uint8) + + # 为每个植被类型分配颜色 + for y in range(height): + for x in range(width): + veg_id = vegetation_map[y, x] + veg_info = self.vegetation_types.get(veg_id, {'color': (128, 128, 128)}) + color_map[y, x] = veg_info['color'] + + return color_map + + except Exception as e: + print(f"✗ 植被颜色图生成失败: {e}") + height, width = vegetation_map.shape + return np.zeros((height, width, 3), dtype=np.uint8) + + def export_vegetation_legend(self, filename: str) -> bool: + """ + 导出植被图例 + + Args: + filename: 文件名 + + Returns: + 是否导出成功 + """ + try: + import json + + legend_data = {} + for veg_id, veg_info in self.vegetation_types.items(): + legend_data[veg_id] = { + 'name': veg_info['name'], + 'color': veg_info['color'], + 'density': veg_info['density'] + } + + with open(filename, 'w', encoding='utf-8') as f: + json.dump(legend_data, f, ensure_ascii=False, indent=2) + + print(f"✓ 植被图例已导出到: {filename}") + return True + + except Exception as e: + print(f"✗ 植被图例导出失败: {e}") + return False + + def import_vegetation_legend(self, filename: str) -> bool: + """ + 导入植被图例 + + Args: + filename: 文件名 + + Returns: + 是否导入成功 + """ + try: + import json + + with open(filename, 'r', encoding='utf-8') as f: + legend_data = json.load(f) + + # 更新植被类型 + self.vegetation_types = {} + for veg_id, veg_info in legend_data.items(): + self.vegetation_types[int(veg_id)] = { + 'name': veg_info['name'], + 'color': tuple(veg_info['color']), + 'density': veg_info['density'] + } + + print(f"✓ 植被图例已从 {filename} 导入") + return True + + except Exception as e: + print(f"✗ 植被图例导入失败: {e}") + return False + + def get_vegetation_distribution_stats(self) -> Dict[str, int]: + """ + 获取植被分布统计 + + Returns: + 植被分布统计字典 + """ + return self.stats['vegetation_distribution'].copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'vegetation_generated': 0, + 'total_generation_time': 0.0, + 'average_generation_time': 0.0, + 'vegetation_distribution': {} + } + print("✓ 植被生成器统计信息已重置") + + def set_vegetation_types(self, vegetation_types: Dict[int, Dict[str, Any]]): + """ + 设置植被类型 + + Args: + vegetation_types: 植被类型字典 + """ + self.vegetation_types = vegetation_types + print(f"✓ 植被类型已更新,共 {len(vegetation_types)} 种植被") + + def add_vegetation_type(self, vegetation_id: int, vegetation_info: Dict[str, Any]): + """ + 添加植被类型 + + Args: + vegetation_id: 植被ID + vegetation_info: 植被信息 + """ + self.vegetation_types[vegetation_id] = vegetation_info + print(f"✓ 植被类型 {vegetation_info['name']} (ID: {vegetation_id}) 已添加") + + def remove_vegetation_type(self, vegetation_id: int): + """ + 移除植被类型 + + Args: + vegetation_id: 植被ID + """ + if vegetation_id in self.vegetation_types: + veg_name = self.vegetation_types[vegetation_id]['name'] + del self.vegetation_types[vegetation_id] + print(f"✓ 植被类型 {veg_name} (ID: {vegetation_id}) 已移除") + else: + print(f"✗ 无效的植被ID: {vegetation_id}") + + def modify_vegetation_type(self, vegetation_id: int, modifications: Dict[str, Any]): + """ + 修改植被类型 + + Args: + vegetation_id: 植被ID + modifications: 修改内容 + """ + if vegetation_id in self.vegetation_types: + self.vegetation_types[vegetation_id].update(modifications) + print(f"✓ 植被类型 (ID: {vegetation_id}) 已修改") + else: + print(f"✗ 无效的植被ID: {vegetation_id}") + + def set_biome_vegetation(self, biome_id: int, vegetation_types: List[int]): + """ + 设置生物群落的植被类型 + + Args: + biome_id: 生物群落ID + vegetation_types: 植被类型列表 + """ + self.biome_vegetation[biome_id] = vegetation_types + print(f"✓ 生物群落 {biome_id} 的植被类型已设置为: {vegetation_types}") + + def get_biome_vegetation(self, biome_id: int) -> List[int]: + """ + 获取生物群落的植被类型 + + Args: + biome_id: 生物群落ID + + Returns: + 植被类型列表 + """ + return self.biome_vegetation.get(biome_id, []).copy() + + def generate_vegetation_density_map(self, heightmap: np.ndarray, biome_map: np.ndarray) -> np.ndarray: + """ + 生成植被密度图 + + Args: + heightmap: 高度图数据 + biome_map: 生物群落图数据 + + Returns: + 植被密度图 + """ + try: + height, width = heightmap.shape + density_map = np.zeros((height, width), dtype=np.float32) + + # 为每个生物群落生成不同的密度模式 + for y in range(height): + for x in range(width): + biome_type = biome_map[y, x] if y < biome_map.shape[0] and x < biome_map.shape[1] else 0 + height_value = heightmap[y, x] + + # 根据生物群落类型确定基础密度 + if biome_type == 0: # ocean + density_map[y, x] = 0.0 + elif biome_type == 1: # beach + density_map[y, x] = 0.1 + elif biome_type == 2: # plains + density_map[y, x] = 0.4 + elif biome_type in [3, 4]: # forest, jungle + density_map[y, x] = 0.8 + elif biome_type == 5: # desert + density_map[y, x] = 0.05 + elif biome_type == 6: # mountain + # 山地密度随高度变化 + density_map[y, x] = max(0.0, 0.6 - height_value) + elif biome_type in [7, 8, 9]: # snow, taiga, tundra + density_map[y, x] = 0.3 + elif biome_type == 10: # swamp + density_map[y, x] = 0.6 + elif biome_type == 11: # savanna + density_map[y, x] = 0.3 + else: + density_map[y, x] = 0.2 + + # 应用噪声以增加自然变化 + if self.noise_generator: + for y in range(height): + for x in range(width): + noise_value = self.noise_generator.generate_noise( + x / 20.0, y / 20.0, self.seed + 5000 + ) + density_map[y, x] = max(0.0, min(1.0, density_map[y, x] + noise_value * 0.1)) + + return density_map + + except Exception as e: + print(f"✗ 植被密度图生成失败: {e}") + return np.zeros_like(heightmap, dtype=np.float32) + + def apply_vegetation_mask(self, vegetation_map: np.ndarray, mask: np.ndarray) -> np.ndarray: + """ + 应用掩码到植被图 + + Args: + vegetation_map: 原始植被图 + mask: 掩码(0-1范围,0表示移除植被) + + Returns: + 处理后的植被图 + """ + try: + if mask.shape != vegetation_map.shape: + print("✗ 掩码尺寸与植被图不匹配") + return vegetation_map + + masked = vegetation_map.copy() + + # 根据掩码值调整植被 + for y in range(masked.shape[0]): + for x in range(masked.shape[1]): + # 掩码值越小,移除植被的概率越大 + if np.random.random() > mask[y, x]: + masked[y, x] = 0 # 移除植被 + + return masked + + except Exception as e: + print(f"✗ 植被掩码应用失败: {e}") + return vegetation_map + + def generate_clustered_vegetation(self, heightmap: np.ndarray, biome_map: np.ndarray, + cluster_density: float = 0.2, seed: int = None) -> np.ndarray: + """ + 生成聚类植被 + + Args: + heightmap: 高度图数据 + biome_map: 生物群落图数据 + cluster_density: 聚类密度 + seed: 随机种子 + + Returns: + 植被分布图 + """ + try: + import time + generation_start_time = time.time() + + if not self.enabled: + print("✗ 植被生成器未启用") + return np.zeros_like(heightmap, dtype=np.int32) + + # 更新种子 + if seed is not None: + self.seed = seed + + print("✓ 开始生成聚类植被...") + + # 初始化随机数生成器 + random.seed(self.seed) + np.random.seed(self.seed) + + # 创建植被图 + height, width = heightmap.shape + self.vegetation_map = np.zeros((height, width), dtype=np.int32) + + # 生成聚类中心点 + num_clusters = int(width * height * cluster_density / 100) + clusters = [] + for _ in range(num_clusters): + x = np.random.randint(0, width) + y = np.random.randint(0, height) + biome_type = biome_map[y, x] if y < biome_map.shape[0] and x < biome_map.shape[1] else 0 + # 获取该生物群落的植被类型 + veg_types = self.biome_vegetation.get(biome_type, [1, 2]) # 默认为草和灌木 + veg_type = np.random.choice(veg_types) if veg_types else 1 + cluster_size = np.random.randint(5, 20) # 聚类大小 + clusters.append((x, y, veg_type, cluster_size)) + + # 在聚类中心点周围生成植被 + print(" → 生成植被聚类...") + for cx, cy, veg_type, cluster_size in clusters: + for _ in range(cluster_size): + # 在聚类中心附近随机生成植被 + angle = np.random.random() * 2 * np.pi + distance = np.random.random() * cluster_size / 2 + x = int(cx + np.cos(angle) * distance) + y = int(cy + np.sin(angle) * distance) + + # 确保坐标在有效范围内 + if 0 <= x < width and 0 <= y < height: + # 检查该位置是否适合该植被类型 + height_value = heightmap[y, x] + biome_type = biome_map[y, x] if y < biome_map.shape[0] and x < biome_map.shape[1] else 0 + + # 简单的适宜性检查 + if height_value > 0.1 and biome_type != 0: # 不在水中 + self.vegetation_map[y, x] = veg_type + + # 应用后处理 + print(" → 应用后处理...") + self.vegetation_map = self._post_process_vegetation(self.vegetation_map, heightmap, biome_map) + + # 更新统计信息 + generation_time = time.time() - generation_start_time + self.stats['vegetation_generated'] += 1 + self.stats['total_generation_time'] += generation_time + self.stats['average_generation_time'] = self.stats['total_generation_time'] / self.stats['vegetation_generated'] + + # 更新植被分布统计 + self._update_vegetation_distribution_stats(self.vegetation_map) + + print(f"✓ 聚类植被生成完成,耗时: {generation_time:.2f}秒") + return self.vegetation_map + + except Exception as e: + print(f"✗ 聚类植被生成失败: {e}") + import traceback + traceback.print_exc() return np.zeros_like(heightmap, dtype=np.int32) \ No newline at end of file diff --git a/plugins/user/realtime_communication/README.md b/plugins/user/realtime_communication/README.md index e2337965..6c769de6 100644 --- a/plugins/user/realtime_communication/README.md +++ b/plugins/user/realtime_communication/README.md @@ -1,331 +1,331 @@ -# 实时通信插件 - -为EG引擎提供完整的实时通信功能,支持WebSocket双向通信、客户端管理、消息路由、房间系统等功能。 - -## 功能特性 - -### 1. WebSocket服务器 -- 实时双向通信支持 -- SSL/TLS加密连接 -- 心跳检测和连接管理 -- 消息压缩和优化 - -### 2. 客户端管理 -- 客户端连接和断开管理 -- 认证和授权系统 -- 黑名单和速率限制 -- 客户端状态跟踪 - -### 3. 消息路由 -- 智能消息分发 -- 房间内消息广播 -- 点对点消息传递 -- 消息过滤和验证 - -### 4. 房间系统 -- 动态房间创建和销毁 -- 房间密码保护 -- 客户端加入/离开管理 -- 房间状态同步 - -### 5. 协议处理 -- 多协议支持(JSON、二进制等) -- 消息压缩和解压缩 -- 数据序列化和反序列化 -- 消息分片和重组 - -### 6. 安全认证 -- Token认证机制 -- 密码哈希和验证 -- 会话管理 -- IP过滤和阻止 - -### 7. 监控系统 -- 实时性能监控 -- 日志记录和分析 -- 警报系统 -- 指标收集和展示 - -### 8. 配置管理 -- 灵活的配置系统 -- 实时配置更新 -- 配置导入导出 -- 默认配置模板 - -### 9. 事件系统 -- 事件驱动架构 -- 异步事件处理 -- 事件过滤和优先级 -- 事件监听器系统 - -### 10. 可视化编辑器 -- 实时数据监控 -- 配置界面 -- 统计信息展示 -- 数据导出功能 - -## 系统架构 - -``` -实时通信插件 -├── 核心模块 -│ └── WebSocket服务器 (core/websocket_server.py) -├── 客户端管理 -│ └── 客户端管理器 (clients/client_manager.py) -├── 消息处理 -│ ├── 消息路由器 (messaging/message_router.py) -│ ├── 协议处理器 (protocol/protocol_handler.py) -│ └── 数据序列化器 (serialization/data_serializer.py) -├── 房间系统 -│ └── 房间管理器 (rooms/room_manager.py) -├── 安全系统 -│ └── 认证管理器 (auth/auth_manager.py) -├── 监控系统 -│ └── 通信监控器 (monitoring/communication_monitor.py) -├── 配置管理 -│ └── 通信配置器 (config/comm_config.py) -├── 事件系统 -│ └── 事件处理器 (events/event_handler.py) -├── 编辑器接口 -│ └── 通信编辑器 (editor/comm_editor.py) -└── 工具模块 - └── 通信工具类 (utils/comm_utils.py) -``` - -## 安装和使用 - -### 安装依赖 - -```bash -# 安装必要的Python包 -pip install psutil -``` - -### 配置插件 - -插件配置文件位于 `config/communication_config.json`,包含以下主要配置项: - -```json -{ - "server": { - "host": "0.0.0.0", - "port": 8080, - "enable_ssl": false - }, - "client": { - "max_clients": 1000, - "enable_authentication": true - }, - "message": { - "max_message_size": 65536, - "enable_message_compression": true - } -} -``` - -### 启动插件 - -```python -# 初始化插件 -plugin = RealtimeCommunicationPlugin(engine) -plugin.initialize() -plugin.enable() - -# 启动WebSocket服务器 -plugin.websocket_server.start_server() - -# 插件会自动处理客户端连接、消息路由等操作 -``` - -## API参考 - -### WebSocket服务器 - -```python -# 发送消息到客户端 -plugin.websocket_server.send_message_to_client(client_id, message) - -# 广播消息 -plugin.websocket_server.broadcast_message(message) - -# 断开客户端连接 -plugin.websocket_server.disconnect_client(client_id, reason) -``` - -### 客户端管理 - -```python -# 添加客户端 -plugin.client_manager.add_client(client_id, client_info) - -# 认证客户端 -plugin.client_manager.authenticate_client(client_id, auth_data) - -# 禁止客户端 -plugin.client_manager.ban_client(client_id, reason) -``` - -### 消息路由 - -```python -# 路由消息 -plugin.message_router.route_message(client_id, message) - -# 发送直接消息 -plugin.message_router.send_direct_message(from_client_id, to_client_id, message) - -# 房间广播 -plugin.message_router.broadcast_to_room(room_id, message) -``` - -### 房间管理 - -```python -# 创建房间 -room_id = plugin.room_manager.create_room(room_name, settings) - -# 添加客户端到房间 -plugin.room_manager.add_client_to_room(room_id, client_id) - -# 从房间移除客户端 -plugin.room_manager.remove_client_from_room(room_id, client_id) - -# 销毁房间 -plugin.room_manager.destroy_room(room_id) -``` - -## 消息格式 - -### 系统消息 - -```json -{ - "type": "ping", - "timestamp": 1234567890.123 -} -``` - -```json -{ - "type": "pong", - "timestamp": 1234567890.123, - "ping_timestamp": 1234567890.120 -} -``` - -### 聊天消息 - -```json -{ - "type": "chat", - "content": "Hello, world!", - "sender": "user123" -} -``` - -### 房间消息 - -```json -{ - "type": "join_room", - "room_id": "room123" -} -``` - -```json -{ - "type": "leave_room", - "room_id": "room123" -} -``` - -## 性能优化 - -1. **连接管理**:使用连接池和复用机制减少资源消耗 -2. **消息压缩**:对大数据消息进行自动压缩 -3. **异步处理**:采用多线程和异步I/O提高并发性能 -4. **缓存机制**:对频繁访问的数据进行缓存 -5. **内存优化**:及时清理无用对象和连接 - -## 安全特性 - -1. **认证机制**:支持Token和密码认证 -2. **权限控制**:基于角色的访问控制 -3. **数据加密**:支持SSL/TLS传输加密 -4. **防攻击**:IP过滤、速率限制、防DDoS -5. **审计日志**:完整操作日志记录 - -## 监控和调试 - -### 实时监控 -- CPU和内存使用率 -- 网络流量统计 -- 连接数和消息数 -- 错误率和响应时间 - -### 日志系统 -- 详细的操作日志 -- 错误和异常记录 -- 性能指标日志 -- 安全审计日志 - -## 扩展开发 - -### 添加新的消息类型 - -```python -# 在消息路由器中注册新的处理器 -plugin.message_router.register_message_handler("custom_type", custom_handler) - -def custom_handler(client_id, message): - # 处理自定义消息 - pass -``` - -### 添加新的协议 - -```python -# 在协议处理器中注册新的协议 -plugin.protocol_handler.add_protocol_handler("custom", encode_func, decode_func) -``` - -### 添加事件监听器 - -```python -# 注册事件监听器 -plugin.event_handler.register_event_handler("client_connected", on_client_connected) - -def on_client_connected(payload): - # 处理客户端连接事件 - pass -``` - -## 故障排除 - -### 常见问题 - -1. **连接失败**:检查端口是否被占用,防火墙设置 -2. **认证失败**:检查认证配置和凭证 -3. **性能问题**:检查系统资源使用情况,优化配置 -4. **消息丢失**:检查网络连接,调整消息队列大小 - -### 日志分析 - -查看日志文件以诊断问题: -``` -logs/communication_monitor.log -``` - -## 版本信息 - -- 版本:1.0.0 -- 作者:EG Plugin System -- 许可证:MIT - -## 贡献指南 - -欢迎提交Issue和Pull Request来改进这个插件。 - -## 支持 - +# 实时通信插件 + +为EG引擎提供完整的实时通信功能,支持WebSocket双向通信、客户端管理、消息路由、房间系统等功能。 + +## 功能特性 + +### 1. WebSocket服务器 +- 实时双向通信支持 +- SSL/TLS加密连接 +- 心跳检测和连接管理 +- 消息压缩和优化 + +### 2. 客户端管理 +- 客户端连接和断开管理 +- 认证和授权系统 +- 黑名单和速率限制 +- 客户端状态跟踪 + +### 3. 消息路由 +- 智能消息分发 +- 房间内消息广播 +- 点对点消息传递 +- 消息过滤和验证 + +### 4. 房间系统 +- 动态房间创建和销毁 +- 房间密码保护 +- 客户端加入/离开管理 +- 房间状态同步 + +### 5. 协议处理 +- 多协议支持(JSON、二进制等) +- 消息压缩和解压缩 +- 数据序列化和反序列化 +- 消息分片和重组 + +### 6. 安全认证 +- Token认证机制 +- 密码哈希和验证 +- 会话管理 +- IP过滤和阻止 + +### 7. 监控系统 +- 实时性能监控 +- 日志记录和分析 +- 警报系统 +- 指标收集和展示 + +### 8. 配置管理 +- 灵活的配置系统 +- 实时配置更新 +- 配置导入导出 +- 默认配置模板 + +### 9. 事件系统 +- 事件驱动架构 +- 异步事件处理 +- 事件过滤和优先级 +- 事件监听器系统 + +### 10. 可视化编辑器 +- 实时数据监控 +- 配置界面 +- 统计信息展示 +- 数据导出功能 + +## 系统架构 + +``` +实时通信插件 +├── 核心模块 +│ └── WebSocket服务器 (core/websocket_server.py) +├── 客户端管理 +│ └── 客户端管理器 (clients/client_manager.py) +├── 消息处理 +│ ├── 消息路由器 (messaging/message_router.py) +│ ├── 协议处理器 (protocol/protocol_handler.py) +│ └── 数据序列化器 (serialization/data_serializer.py) +├── 房间系统 +│ └── 房间管理器 (rooms/room_manager.py) +├── 安全系统 +│ └── 认证管理器 (auth/auth_manager.py) +├── 监控系统 +│ └── 通信监控器 (monitoring/communication_monitor.py) +├── 配置管理 +│ └── 通信配置器 (config/comm_config.py) +├── 事件系统 +│ └── 事件处理器 (events/event_handler.py) +├── 编辑器接口 +│ └── 通信编辑器 (editor/comm_editor.py) +└── 工具模块 + └── 通信工具类 (utils/comm_utils.py) +``` + +## 安装和使用 + +### 安装依赖 + +```bash +# 安装必要的Python包 +pip install psutil +``` + +### 配置插件 + +插件配置文件位于 `config/communication_config.json`,包含以下主要配置项: + +```json +{ + "server": { + "host": "0.0.0.0", + "port": 8080, + "enable_ssl": false + }, + "client": { + "max_clients": 1000, + "enable_authentication": true + }, + "message": { + "max_message_size": 65536, + "enable_message_compression": true + } +} +``` + +### 启动插件 + +```python +# 初始化插件 +plugin = RealtimeCommunicationPlugin(engine) +plugin.initialize() +plugin.enable() + +# 启动WebSocket服务器 +plugin.websocket_server.start_server() + +# 插件会自动处理客户端连接、消息路由等操作 +``` + +## API参考 + +### WebSocket服务器 + +```python +# 发送消息到客户端 +plugin.websocket_server.send_message_to_client(client_id, message) + +# 广播消息 +plugin.websocket_server.broadcast_message(message) + +# 断开客户端连接 +plugin.websocket_server.disconnect_client(client_id, reason) +``` + +### 客户端管理 + +```python +# 添加客户端 +plugin.client_manager.add_client(client_id, client_info) + +# 认证客户端 +plugin.client_manager.authenticate_client(client_id, auth_data) + +# 禁止客户端 +plugin.client_manager.ban_client(client_id, reason) +``` + +### 消息路由 + +```python +# 路由消息 +plugin.message_router.route_message(client_id, message) + +# 发送直接消息 +plugin.message_router.send_direct_message(from_client_id, to_client_id, message) + +# 房间广播 +plugin.message_router.broadcast_to_room(room_id, message) +``` + +### 房间管理 + +```python +# 创建房间 +room_id = plugin.room_manager.create_room(room_name, settings) + +# 添加客户端到房间 +plugin.room_manager.add_client_to_room(room_id, client_id) + +# 从房间移除客户端 +plugin.room_manager.remove_client_from_room(room_id, client_id) + +# 销毁房间 +plugin.room_manager.destroy_room(room_id) +``` + +## 消息格式 + +### 系统消息 + +```json +{ + "type": "ping", + "timestamp": 1234567890.123 +} +``` + +```json +{ + "type": "pong", + "timestamp": 1234567890.123, + "ping_timestamp": 1234567890.120 +} +``` + +### 聊天消息 + +```json +{ + "type": "chat", + "content": "Hello, world!", + "sender": "user123" +} +``` + +### 房间消息 + +```json +{ + "type": "join_room", + "room_id": "room123" +} +``` + +```json +{ + "type": "leave_room", + "room_id": "room123" +} +``` + +## 性能优化 + +1. **连接管理**:使用连接池和复用机制减少资源消耗 +2. **消息压缩**:对大数据消息进行自动压缩 +3. **异步处理**:采用多线程和异步I/O提高并发性能 +4. **缓存机制**:对频繁访问的数据进行缓存 +5. **内存优化**:及时清理无用对象和连接 + +## 安全特性 + +1. **认证机制**:支持Token和密码认证 +2. **权限控制**:基于角色的访问控制 +3. **数据加密**:支持SSL/TLS传输加密 +4. **防攻击**:IP过滤、速率限制、防DDoS +5. **审计日志**:完整操作日志记录 + +## 监控和调试 + +### 实时监控 +- CPU和内存使用率 +- 网络流量统计 +- 连接数和消息数 +- 错误率和响应时间 + +### 日志系统 +- 详细的操作日志 +- 错误和异常记录 +- 性能指标日志 +- 安全审计日志 + +## 扩展开发 + +### 添加新的消息类型 + +```python +# 在消息路由器中注册新的处理器 +plugin.message_router.register_message_handler("custom_type", custom_handler) + +def custom_handler(client_id, message): + # 处理自定义消息 + pass +``` + +### 添加新的协议 + +```python +# 在协议处理器中注册新的协议 +plugin.protocol_handler.add_protocol_handler("custom", encode_func, decode_func) +``` + +### 添加事件监听器 + +```python +# 注册事件监听器 +plugin.event_handler.register_event_handler("client_connected", on_client_connected) + +def on_client_connected(payload): + # 处理客户端连接事件 + pass +``` + +## 故障排除 + +### 常见问题 + +1. **连接失败**:检查端口是否被占用,防火墙设置 +2. **认证失败**:检查认证配置和凭证 +3. **性能问题**:检查系统资源使用情况,优化配置 +4. **消息丢失**:检查网络连接,调整消息队列大小 + +### 日志分析 + +查看日志文件以诊断问题: +``` +logs/communication_monitor.log +``` + +## 版本信息 + +- 版本:1.0.0 +- 作者:EG Plugin System +- 许可证:MIT + +## 贡献指南 + +欢迎提交Issue和Pull Request来改进这个插件。 + +## 支持 + 如需技术支持,请联系插件维护团队或查看相关文档。 \ No newline at end of file diff --git a/plugins/user/realtime_communication/auth/auth_manager.py b/plugins/user/realtime_communication/auth/auth_manager.py index 3b0009ee..6997d021 100644 --- a/plugins/user/realtime_communication/auth/auth_manager.py +++ b/plugins/user/realtime_communication/auth/auth_manager.py @@ -1,900 +1,900 @@ -""" -认证管理器模块 -确保通信安全 -""" - -import time -import hashlib -import secrets -import hmac -from typing import Dict, Any, List, Optional - -class AuthManager: - """ - 认证管理器 - 确保通信安全 - """ - - def __init__(self, plugin): - """ - 初始化认证管理器 - - Args: - plugin: 实时通信插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 认证配置 - self.auth_config = { - "enable_authentication": True, - "auth_timeout": 30.0, - "enable_token_auth": True, - "enable_password_auth": True, - "token_lifetime": 86400, # 24小时 - "enable_session_management": True, - "session_timeout": 3600, # 1小时 - "enable_two_factor_auth": False, - "max_login_attempts": 5, - "login_lockout_duration": 900, # 15分钟 - "enable_ip_filtering": True, - "enable_audit_logging": True - } - - # 认证状态 - self.auth_state = { - "active_sessions": 0, - "total_authentications": 0, - "successful_authentications": 0, - "failed_authentications": 0 - } - - # 认证存储 - self.sessions = {} - self.tokens = {} - self.login_attempts = {} - self.blocked_ips = {} - - # 认证统计 - self.auth_stats = { - "tokens_generated": 0, - "sessions_created": 0, - "sessions_expired": 0, - "users_banned": 0, - "auth_errors": 0 - } - - # 密钥管理 - self.secret_key = self._generate_secret_key() - - # 回调函数 - self.auth_callbacks = { - "auth_success": [], - "auth_failed": [], - "session_created": [], - "session_expired": [], - "user_banned": [], - "auth_error": [] - } - - # 时间戳记录 - self.last_session_cleanup = 0.0 - self.last_auth_event = 0.0 - - print("✓ 认证管理器已创建") - - def initialize(self) -> bool: - """ - 初始化认证管理器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化认证管理器...") - - # 生成密钥 - self.secret_key = self._generate_secret_key() - - self.initialized = True - print("✓ 认证管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 认证管理器初始化失败: {e}") - self.auth_stats["auth_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def _generate_secret_key(self) -> str: - """ - 生成密钥 - - Returns: - 生成的密钥 - """ - try: - return secrets.token_hex(32) - except Exception as e: - print(f"✗ 密钥生成失败: {e}") - # 使用默认密钥(仅用于开发环境) - return "default_secret_key_for_development_purposes_only" - - def enable(self) -> bool: - """ - 启用认证管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 认证管理器未初始化") - return False - - self.enabled = True - print("✓ 认证管理器已启用") - return True - - except Exception as e: - print(f"✗ 认证管理器启用失败: {e}") - self.auth_stats["auth_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用认证管理器""" - try: - self.enabled = False - - # 清理会话和令牌 - self.sessions.clear() - self.tokens.clear() - - print("✓ 认证管理器已禁用") - - except Exception as e: - print(f"✗ 认证管理器禁用失败: {e}") - self.auth_stats["auth_errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理认证管理器资源""" - try: - # 禁用认证管理器 - if self.enabled: - self.disable() - - # 清理回调 - self.auth_callbacks.clear() - - self.initialized = False - print("✓ 认证管理器资源已清理") - - except Exception as e: - print(f"✗ 认证管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新认证管理器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 定期清理过期会话 - if current_time - self.last_session_cleanup > 300: # 每5分钟清理一次 - self._cleanup_expired_sessions(current_time) - self.last_session_cleanup = current_time - - # 定期清理登录尝试记录 - self._cleanup_login_attempts(current_time) - - self.last_auth_event = current_time - - except Exception as e: - print(f"✗ 认证管理器更新失败: {e}") - self.auth_stats["auth_errors"] += 1 - import traceback - traceback.print_exc() - - def _cleanup_expired_sessions(self, current_time: float): - """清理过期会话""" - try: - expired_sessions = [] - for session_id, session_data in self.sessions.items(): - if current_time - session_data["login_time"] > self.auth_config["session_timeout"]: - expired_sessions.append(session_id) - - for session_id in expired_sessions: - del self.sessions[session_id] - self.auth_state["active_sessions"] -= 1 - self.auth_stats["sessions_expired"] += 1 - - # 触发会话过期回调 - self._trigger_auth_callback("session_expired", { - "session_id": session_id, - "timestamp": current_time - }) - - except Exception as e: - print(f"✗ 过期会话清理失败: {e}") - self.auth_stats["auth_errors"] += 1 - - def _cleanup_login_attempts(self, current_time: float): - """清理登录尝试记录""" - try: - expired_attempts = [] - for ip_address, attempt_data in self.login_attempts.items(): - if current_time - attempt_data["last_attempt"] > self.auth_config["login_lockout_duration"]: - expired_attempts.append(ip_address) - - for ip_address in expired_attempts: - del self.login_attempts[ip_address] - - except Exception as e: - print(f"✗ 登录尝试记录清理失败: {e}") - self.auth_stats["auth_errors"] += 1 - - def authenticate_client(self, client_id: str, auth_data: Dict[str, Any]) -> bool: - """ - 认证客户端 - - Args: - client_id: 客户端ID - auth_data: 认证数据 - - Returns: - 是否认证成功 - """ - try: - if not self.enabled or not self.auth_config["enable_authentication"]: - # 如果未启用认证,直接通过 - return True - - # 检查IP是否被阻止 - client_ip = auth_data.get("client_ip", "") - if self._is_ip_blocked(client_ip): - self.auth_stats["failed_authentications"] += 1 - return False - - # 检查登录尝试次数 - if self._is_login_blocked(client_ip): - self.auth_stats["failed_authentications"] += 1 - return False - - # 执行认证 - auth_result = self._perform_authentication(client_id, auth_data) - - if auth_result: - self.auth_state["successful_authentications"] += 1 - self.auth_state["total_authentications"] += 1 - - # 触发认证成功回调 - self._trigger_auth_callback("auth_success", { - "client_id": client_id, - "auth_data": auth_data, - "timestamp": time.time() - }) - - # 记录审计日志 - if self.auth_config["enable_audit_logging"]: - self._log_auth_event("auth_success", client_id, client_ip) - - else: - self.auth_state["failed_authentications"] += 1 - self.auth_state["total_authentications"] += 1 - - # 记录失败的登录尝试 - self._record_failed_login(client_ip) - - # 触发认证失败回调 - self._trigger_auth_callback("auth_failed", { - "client_id": client_id, - "auth_data": auth_data, - "timestamp": time.time() - }) - - # 记录审计日志 - if self.auth_config["enable_audit_logging"]: - self._log_auth_event("auth_failed", client_id, client_ip) - - return auth_result - - except Exception as e: - print(f"✗ 客户端认证失败: {e}") - self.auth_stats["auth_errors"] += 1 - return False - - def _is_ip_blocked(self, client_ip: str) -> bool: - """ - 检查IP是否被阻止 - - Args: - client_ip: 客户端IP - - Returns: - IP是否被阻止 - """ - try: - if not self.auth_config["enable_ip_filtering"]: - return False - - if client_ip in self.blocked_ips: - block_data = self.blocked_ips[client_ip] - if time.time() - block_data["block_time"] < block_data["duration"]: - return True - else: - # 阻止时间已过期,移除 - del self.blocked_ips[client_ip] - - return False - - except Exception as e: - print(f"✗ IP阻止检查失败: {e}") - self.auth_stats["auth_errors"] += 1 - return False - - def _is_login_blocked(self, client_ip: str) -> bool: - """ - 检查登录是否被阻止 - - Args: - client_ip: 客户端IP - - Returns: - 登录是否被阻止 - """ - try: - if client_ip in self.login_attempts: - attempts = self.login_attempts[client_ip] - if (attempts["count"] >= self.auth_config["max_login_attempts"] and - time.time() - attempts["last_attempt"] < self.auth_config["login_lockout_duration"]): - return True - - return False - - except Exception as e: - print(f"✗ 登录阻止检查失败: {e}") - self.auth_stats["auth_errors"] += 1 - return False - - def _record_failed_login(self, client_ip: str): - """ - 记录失败的登录尝试 - - Args: - client_ip: 客户端IP - """ - try: - current_time = time.time() - - if client_ip not in self.login_attempts: - self.login_attempts[client_ip] = { - "count": 1, - "last_attempt": current_time - } - else: - self.login_attempts[client_ip]["count"] += 1 - self.login_attempts[client_ip]["last_attempt"] = current_time - - # 如果超过最大尝试次数,阻止IP - if (self.login_attempts[client_ip]["count"] >= - self.auth_config["max_login_attempts"]): - self._block_ip(client_ip, self.auth_config["login_lockout_duration"]) - - except Exception as e: - print(f"✗ 失败登录记录失败: {e}") - self.auth_stats["auth_errors"] += 1 - - def _perform_authentication(self, client_id: str, auth_data: Dict[str, Any]) -> bool: - """ - 执行认证 - - Args: - client_id: 客户端ID - auth_data: 认证数据 - - Returns: - 是否认证成功 - """ - try: - # 检查认证类型 - auth_type = auth_data.get("auth_type", "token") - - if auth_type == "token": - return self._authenticate_with_token(client_id, auth_data) - elif auth_type == "password": - return self._authenticate_with_password(client_id, auth_data) - elif auth_type == "session": - return self._authenticate_with_session(client_id, auth_data) - else: - # 默认认证方式 - return self._authenticate_with_token(client_id, auth_data) - - except Exception as e: - print(f"✗ 认证执行失败: {e}") - self.auth_stats["auth_errors"] += 1 - return False - - def _authenticate_with_token(self, client_id: str, auth_data: Dict[str, Any]) -> bool: - """ - 使用令牌认证 - - Args: - client_id: 客户端ID - auth_data: 认证数据 - - Returns: - 是否认证成功 - """ - try: - token = auth_data.get("token") - if not token: - return False - - # 验证令牌 - if token in self.tokens: - token_data = self.tokens[token] - if time.time() - token_data["created_time"] < self.auth_config["token_lifetime"]: - # 令牌有效,创建会话 - self._create_session(client_id, auth_data) - return True - else: - # 令牌过期,移除 - del self.tokens[token] - - return False - - except Exception as e: - print(f"✗ 令牌认证失败: {e}") - self.auth_stats["auth_errors"] += 1 - return False - - def _authenticate_with_password(self, client_id: str, auth_data: Dict[str, Any]) -> bool: - """ - 使用密码认证 - - Args: - client_id: 客户端ID - auth_data: 认证数据 - - Returns: - 是否认证成功 - """ - try: - username = auth_data.get("username") - password = auth_data.get("password") - - if not username or not password: - return False - - # 验证密码(简化实现) - # 在实际应用中,这里应该连接到用户数据库验证用户名和密码 - if username == "test_user" and password == "test_password": - # 创建会话 - self._create_session(client_id, auth_data) - return True - else: - return False - - except Exception as e: - print(f"✗ 密码认证失败: {e}") - self.auth_stats["auth_errors"] += 1 - return False - - def _authenticate_with_session(self, client_id: str, auth_data: Dict[str, Any]) -> bool: - """ - 使用会话认证 - - Args: - client_id: 客户端ID - auth_data: 认证数据 - - Returns: - 是否认证成功 - """ - try: - session_id = auth_data.get("session_id") - if not session_id: - return False - - # 检查会话是否存在且有效 - if session_id in self.sessions: - session_data = self.sessions[session_id] - if time.time() - session_data["login_time"] < self.auth_config["session_timeout"]: - # 更新会话最后活动时间 - session_data["last_activity"] = time.time() - return True - - return False - - except Exception as e: - print(f"✗ 会话认证失败: {e}") - self.auth_stats["auth_errors"] += 1 - return False - - def _create_session(self, client_id: str, auth_data: Dict[str, Any]): - """ - 创建会话 - - Args: - client_id: 客户端ID - auth_data: 认证数据 - """ - try: - session_id = self._generate_session_id() - - session_data = { - "client_id": client_id, - "login_time": time.time(), - "last_activity": time.time(), - "user_id": auth_data.get("user_id", ""), - "username": auth_data.get("username", ""), - "permissions": auth_data.get("permissions", []) - } - - self.sessions[session_id] = session_data - self.auth_state["active_sessions"] += 1 - self.auth_stats["sessions_created"] += 1 - - # 触发会话创建回调 - self._trigger_auth_callback("session_created", { - "session_id": session_id, - "client_id": client_id, - "auth_data": auth_data, - "timestamp": time.time() - }) - - except Exception as e: - print(f"✗ 会话创建失败: {e}") - self.auth_stats["auth_errors"] += 1 - - def _generate_session_id(self) -> str: - """ - 生成会话ID - - Returns: - 生成的会话ID - """ - try: - return secrets.token_urlsafe(32) - except Exception as e: - print(f"✗ 会话ID生成失败: {e}") - self.auth_stats["auth_errors"] += 1 - return f"session_{int(time.time() * 1000000)}" - - def generate_token(self, user_id: str, permissions: List[str] = None) -> str: - """ - 生成令牌 - - Args: - user_id: 用户ID - permissions: 权限列表 - - Returns: - 生成的令牌 - """ - try: - token = secrets.token_urlsafe(64) - - self.tokens[token] = { - "user_id": user_id, - "permissions": permissions or [], - "created_time": time.time() - } - - self.auth_stats["tokens_generated"] += 1 - - return token - - except Exception as e: - print(f"✗ 令牌生成失败: {e}") - self.auth_stats["auth_errors"] += 1 - return "" - - def validate_token(self, token: str) -> bool: - """ - 验证令牌 - - Args: - token: 令牌 - - Returns: - 令牌是否有效 - """ - try: - if not token: - return False - - if token in self.tokens: - token_data = self.tokens[token] - if time.time() - token_data["created_time"] < self.auth_config["token_lifetime"]: - return True - else: - # 令牌过期,移除 - del self.tokens[token] - - return False - - except Exception as e: - print(f"✗ 令牌验证失败: {e}") - self.auth_stats["auth_errors"] += 1 - return False - - def hash_password(self, password: str, salt: str = None) -> Dict[str, str]: - """ - 哈希密码 - - Args: - password: 密码 - salt: 盐值(可选) - - Returns: - 包含哈希值和盐值的字典 - """ - try: - if salt is None: - salt = secrets.token_hex(16) - - # 使用PBKDF2哈希密码 - password_hash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt.encode('utf-8'), 100000) - return { - "hash": password_hash.hex(), - "salt": salt - } - - except Exception as e: - print(f"✗ 密码哈希失败: {e}") - self.auth_stats["auth_errors"] += 1 - return {} - - def verify_password(self, password: str, password_hash: str, salt: str) -> bool: - """ - 验证密码 - - Args: - password: 密码 - password_hash: 密码哈希值 - salt: 盐值 - - Returns: - 密码是否正确 - """ - try: - # 重新哈希密码 - new_hash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt.encode('utf-8'), 100000) - return hmac.compare_digest(new_hash.hex(), password_hash) - - except Exception as e: - print(f"✗ 密码验证失败: {e}") - self.auth_stats["auth_errors"] += 1 - return False - - def _block_ip(self, client_ip: str, duration: float): - """ - 阻止IP地址 - - Args: - client_ip: 客户端IP - duration: 阻止持续时间(秒) - """ - try: - self.blocked_ips[client_ip] = { - "block_time": time.time(), - "duration": duration - } - - # 触发用户禁止回调 - self._trigger_auth_callback("user_banned", { - "ip_address": client_ip, - "duration": duration, - "timestamp": time.time() - }) - - # 记录审计日志 - if self.auth_config["enable_audit_logging"]: - self._log_auth_event("ip_blocked", None, client_ip) - - print(f"✓ IP地址已被阻止: {client_ip} (持续 {duration} 秒)") - - except Exception as e: - print(f"✗ IP阻止失败: {e}") - self.auth_stats["auth_errors"] += 1 - - def unblock_ip(self, client_ip: str) -> bool: - """ - 解除IP阻止 - - Args: - client_ip: 客户端IP - - Returns: - 是否解除成功 - """ - try: - if client_ip in self.blocked_ips: - del self.blocked_ips[client_ip] - print(f"✓ IP地址阻止已解除: {client_ip}") - return True - else: - print(f"✗ IP地址未被阻止: {client_ip}") - return False - - except Exception as e: - print(f"✗ IP阻止解除失败: {e}") - self.auth_stats["auth_errors"] += 1 - return False - - def _log_auth_event(self, event_type: str, client_id: str = None, client_ip: str = None): - """ - 记录认证事件 - - Args: - event_type: 事件类型 - client_id: 客户端ID(可选) - client_ip: 客户端IP(可选) - """ - try: - if not self.auth_config["enable_audit_logging"]: - return - - event = { - "event_type": event_type, - "client_id": client_id, - "client_ip": client_ip, - "timestamp": time.time() - } - - # 在实际实现中,这里会将事件记录到日志文件或数据库 - print(f"[认证日志] {event_type} - 客户端: {client_id}, IP: {client_ip}") - - except Exception as e: - print(f"✗ 认证事件记录失败: {e}") - self.auth_stats["auth_errors"] += 1 - - def get_active_sessions(self) -> Dict[str, Any]: - """ - 获取活跃会话 - - Returns: - 活跃会话字典 - """ - try: - return self.sessions.copy() - except Exception as e: - print(f"✗ 获取活跃会话失败: {e}") - self.auth_stats["auth_errors"] += 1 - return {} - - def get_blocked_ips(self) -> Dict[str, Any]: - """ - 获取被阻止的IP地址 - - Returns: - 被阻止的IP地址字典 - """ - try: - return self.blocked_ips.copy() - except Exception as e: - print(f"✗ 获取被阻止的IP地址失败: {e}") - self.auth_stats["auth_errors"] += 1 - return {} - - def get_auth_stats(self) -> Dict[str, Any]: - """ - 获取认证统计信息 - - Returns: - 认证统计字典 - """ - return { - "state": self.auth_state.copy(), - "stats": self.auth_stats.copy(), - "config": self.auth_config.copy() - } - - def reset_stats(self): - """重置认证统计信息""" - try: - self.auth_stats = { - "tokens_generated": 0, - "sessions_created": 0, - "sessions_expired": 0, - "users_banned": 0, - "auth_errors": 0 - } - print("✓ 认证统计信息已重置") - except Exception as e: - print(f"✗ 认证统计信息重置失败: {e}") - - def set_auth_config(self, config: Dict[str, Any]) -> bool: - """ - 设置认证配置 - - Args: - config: 认证配置字典 - - Returns: - 是否设置成功 - """ - try: - self.auth_config.update(config) - print(f"✓ 认证配置已更新: {self.auth_config}") - return True - except Exception as e: - print(f"✗ 认证配置设置失败: {e}") - return False - - def get_auth_config(self) -> Dict[str, Any]: - """ - 获取认证配置 - - Returns: - 认证配置字典 - """ - return self.auth_config.copy() - - def _trigger_auth_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发认证回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.auth_callbacks: - for callback in self.auth_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 认证回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 认证回调触发失败: {e}") - - def register_auth_callback(self, callback_type: str, callback: callable): - """ - 注册认证回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.auth_callbacks: - self.auth_callbacks[callback_type].append(callback) - print(f"✓ 认证回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 认证回调注册失败: {e}") - - def unregister_auth_callback(self, callback_type: str, callback: callable): - """ - 注销认证回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.auth_callbacks: - if callback in self.auth_callbacks[callback_type]: - self.auth_callbacks[callback_type].remove(callback) - print(f"✓ 认证回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +认证管理器模块 +确保通信安全 +""" + +import time +import hashlib +import secrets +import hmac +from typing import Dict, Any, List, Optional + +class AuthManager: + """ + 认证管理器 + 确保通信安全 + """ + + def __init__(self, plugin): + """ + 初始化认证管理器 + + Args: + plugin: 实时通信插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 认证配置 + self.auth_config = { + "enable_authentication": True, + "auth_timeout": 30.0, + "enable_token_auth": True, + "enable_password_auth": True, + "token_lifetime": 86400, # 24小时 + "enable_session_management": True, + "session_timeout": 3600, # 1小时 + "enable_two_factor_auth": False, + "max_login_attempts": 5, + "login_lockout_duration": 900, # 15分钟 + "enable_ip_filtering": True, + "enable_audit_logging": True + } + + # 认证状态 + self.auth_state = { + "active_sessions": 0, + "total_authentications": 0, + "successful_authentications": 0, + "failed_authentications": 0 + } + + # 认证存储 + self.sessions = {} + self.tokens = {} + self.login_attempts = {} + self.blocked_ips = {} + + # 认证统计 + self.auth_stats = { + "tokens_generated": 0, + "sessions_created": 0, + "sessions_expired": 0, + "users_banned": 0, + "auth_errors": 0 + } + + # 密钥管理 + self.secret_key = self._generate_secret_key() + + # 回调函数 + self.auth_callbacks = { + "auth_success": [], + "auth_failed": [], + "session_created": [], + "session_expired": [], + "user_banned": [], + "auth_error": [] + } + + # 时间戳记录 + self.last_session_cleanup = 0.0 + self.last_auth_event = 0.0 + + print("✓ 认证管理器已创建") + + def initialize(self) -> bool: + """ + 初始化认证管理器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化认证管理器...") + + # 生成密钥 + self.secret_key = self._generate_secret_key() + + self.initialized = True + print("✓ 认证管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 认证管理器初始化失败: {e}") + self.auth_stats["auth_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def _generate_secret_key(self) -> str: + """ + 生成密钥 + + Returns: + 生成的密钥 + """ + try: + return secrets.token_hex(32) + except Exception as e: + print(f"✗ 密钥生成失败: {e}") + # 使用默认密钥(仅用于开发环境) + return "default_secret_key_for_development_purposes_only" + + def enable(self) -> bool: + """ + 启用认证管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 认证管理器未初始化") + return False + + self.enabled = True + print("✓ 认证管理器已启用") + return True + + except Exception as e: + print(f"✗ 认证管理器启用失败: {e}") + self.auth_stats["auth_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用认证管理器""" + try: + self.enabled = False + + # 清理会话和令牌 + self.sessions.clear() + self.tokens.clear() + + print("✓ 认证管理器已禁用") + + except Exception as e: + print(f"✗ 认证管理器禁用失败: {e}") + self.auth_stats["auth_errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理认证管理器资源""" + try: + # 禁用认证管理器 + if self.enabled: + self.disable() + + # 清理回调 + self.auth_callbacks.clear() + + self.initialized = False + print("✓ 认证管理器资源已清理") + + except Exception as e: + print(f"✗ 认证管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新认证管理器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 定期清理过期会话 + if current_time - self.last_session_cleanup > 300: # 每5分钟清理一次 + self._cleanup_expired_sessions(current_time) + self.last_session_cleanup = current_time + + # 定期清理登录尝试记录 + self._cleanup_login_attempts(current_time) + + self.last_auth_event = current_time + + except Exception as e: + print(f"✗ 认证管理器更新失败: {e}") + self.auth_stats["auth_errors"] += 1 + import traceback + traceback.print_exc() + + def _cleanup_expired_sessions(self, current_time: float): + """清理过期会话""" + try: + expired_sessions = [] + for session_id, session_data in self.sessions.items(): + if current_time - session_data["login_time"] > self.auth_config["session_timeout"]: + expired_sessions.append(session_id) + + for session_id in expired_sessions: + del self.sessions[session_id] + self.auth_state["active_sessions"] -= 1 + self.auth_stats["sessions_expired"] += 1 + + # 触发会话过期回调 + self._trigger_auth_callback("session_expired", { + "session_id": session_id, + "timestamp": current_time + }) + + except Exception as e: + print(f"✗ 过期会话清理失败: {e}") + self.auth_stats["auth_errors"] += 1 + + def _cleanup_login_attempts(self, current_time: float): + """清理登录尝试记录""" + try: + expired_attempts = [] + for ip_address, attempt_data in self.login_attempts.items(): + if current_time - attempt_data["last_attempt"] > self.auth_config["login_lockout_duration"]: + expired_attempts.append(ip_address) + + for ip_address in expired_attempts: + del self.login_attempts[ip_address] + + except Exception as e: + print(f"✗ 登录尝试记录清理失败: {e}") + self.auth_stats["auth_errors"] += 1 + + def authenticate_client(self, client_id: str, auth_data: Dict[str, Any]) -> bool: + """ + 认证客户端 + + Args: + client_id: 客户端ID + auth_data: 认证数据 + + Returns: + 是否认证成功 + """ + try: + if not self.enabled or not self.auth_config["enable_authentication"]: + # 如果未启用认证,直接通过 + return True + + # 检查IP是否被阻止 + client_ip = auth_data.get("client_ip", "") + if self._is_ip_blocked(client_ip): + self.auth_stats["failed_authentications"] += 1 + return False + + # 检查登录尝试次数 + if self._is_login_blocked(client_ip): + self.auth_stats["failed_authentications"] += 1 + return False + + # 执行认证 + auth_result = self._perform_authentication(client_id, auth_data) + + if auth_result: + self.auth_state["successful_authentications"] += 1 + self.auth_state["total_authentications"] += 1 + + # 触发认证成功回调 + self._trigger_auth_callback("auth_success", { + "client_id": client_id, + "auth_data": auth_data, + "timestamp": time.time() + }) + + # 记录审计日志 + if self.auth_config["enable_audit_logging"]: + self._log_auth_event("auth_success", client_id, client_ip) + + else: + self.auth_state["failed_authentications"] += 1 + self.auth_state["total_authentications"] += 1 + + # 记录失败的登录尝试 + self._record_failed_login(client_ip) + + # 触发认证失败回调 + self._trigger_auth_callback("auth_failed", { + "client_id": client_id, + "auth_data": auth_data, + "timestamp": time.time() + }) + + # 记录审计日志 + if self.auth_config["enable_audit_logging"]: + self._log_auth_event("auth_failed", client_id, client_ip) + + return auth_result + + except Exception as e: + print(f"✗ 客户端认证失败: {e}") + self.auth_stats["auth_errors"] += 1 + return False + + def _is_ip_blocked(self, client_ip: str) -> bool: + """ + 检查IP是否被阻止 + + Args: + client_ip: 客户端IP + + Returns: + IP是否被阻止 + """ + try: + if not self.auth_config["enable_ip_filtering"]: + return False + + if client_ip in self.blocked_ips: + block_data = self.blocked_ips[client_ip] + if time.time() - block_data["block_time"] < block_data["duration"]: + return True + else: + # 阻止时间已过期,移除 + del self.blocked_ips[client_ip] + + return False + + except Exception as e: + print(f"✗ IP阻止检查失败: {e}") + self.auth_stats["auth_errors"] += 1 + return False + + def _is_login_blocked(self, client_ip: str) -> bool: + """ + 检查登录是否被阻止 + + Args: + client_ip: 客户端IP + + Returns: + 登录是否被阻止 + """ + try: + if client_ip in self.login_attempts: + attempts = self.login_attempts[client_ip] + if (attempts["count"] >= self.auth_config["max_login_attempts"] and + time.time() - attempts["last_attempt"] < self.auth_config["login_lockout_duration"]): + return True + + return False + + except Exception as e: + print(f"✗ 登录阻止检查失败: {e}") + self.auth_stats["auth_errors"] += 1 + return False + + def _record_failed_login(self, client_ip: str): + """ + 记录失败的登录尝试 + + Args: + client_ip: 客户端IP + """ + try: + current_time = time.time() + + if client_ip not in self.login_attempts: + self.login_attempts[client_ip] = { + "count": 1, + "last_attempt": current_time + } + else: + self.login_attempts[client_ip]["count"] += 1 + self.login_attempts[client_ip]["last_attempt"] = current_time + + # 如果超过最大尝试次数,阻止IP + if (self.login_attempts[client_ip]["count"] >= + self.auth_config["max_login_attempts"]): + self._block_ip(client_ip, self.auth_config["login_lockout_duration"]) + + except Exception as e: + print(f"✗ 失败登录记录失败: {e}") + self.auth_stats["auth_errors"] += 1 + + def _perform_authentication(self, client_id: str, auth_data: Dict[str, Any]) -> bool: + """ + 执行认证 + + Args: + client_id: 客户端ID + auth_data: 认证数据 + + Returns: + 是否认证成功 + """ + try: + # 检查认证类型 + auth_type = auth_data.get("auth_type", "token") + + if auth_type == "token": + return self._authenticate_with_token(client_id, auth_data) + elif auth_type == "password": + return self._authenticate_with_password(client_id, auth_data) + elif auth_type == "session": + return self._authenticate_with_session(client_id, auth_data) + else: + # 默认认证方式 + return self._authenticate_with_token(client_id, auth_data) + + except Exception as e: + print(f"✗ 认证执行失败: {e}") + self.auth_stats["auth_errors"] += 1 + return False + + def _authenticate_with_token(self, client_id: str, auth_data: Dict[str, Any]) -> bool: + """ + 使用令牌认证 + + Args: + client_id: 客户端ID + auth_data: 认证数据 + + Returns: + 是否认证成功 + """ + try: + token = auth_data.get("token") + if not token: + return False + + # 验证令牌 + if token in self.tokens: + token_data = self.tokens[token] + if time.time() - token_data["created_time"] < self.auth_config["token_lifetime"]: + # 令牌有效,创建会话 + self._create_session(client_id, auth_data) + return True + else: + # 令牌过期,移除 + del self.tokens[token] + + return False + + except Exception as e: + print(f"✗ 令牌认证失败: {e}") + self.auth_stats["auth_errors"] += 1 + return False + + def _authenticate_with_password(self, client_id: str, auth_data: Dict[str, Any]) -> bool: + """ + 使用密码认证 + + Args: + client_id: 客户端ID + auth_data: 认证数据 + + Returns: + 是否认证成功 + """ + try: + username = auth_data.get("username") + password = auth_data.get("password") + + if not username or not password: + return False + + # 验证密码(简化实现) + # 在实际应用中,这里应该连接到用户数据库验证用户名和密码 + if username == "test_user" and password == "test_password": + # 创建会话 + self._create_session(client_id, auth_data) + return True + else: + return False + + except Exception as e: + print(f"✗ 密码认证失败: {e}") + self.auth_stats["auth_errors"] += 1 + return False + + def _authenticate_with_session(self, client_id: str, auth_data: Dict[str, Any]) -> bool: + """ + 使用会话认证 + + Args: + client_id: 客户端ID + auth_data: 认证数据 + + Returns: + 是否认证成功 + """ + try: + session_id = auth_data.get("session_id") + if not session_id: + return False + + # 检查会话是否存在且有效 + if session_id in self.sessions: + session_data = self.sessions[session_id] + if time.time() - session_data["login_time"] < self.auth_config["session_timeout"]: + # 更新会话最后活动时间 + session_data["last_activity"] = time.time() + return True + + return False + + except Exception as e: + print(f"✗ 会话认证失败: {e}") + self.auth_stats["auth_errors"] += 1 + return False + + def _create_session(self, client_id: str, auth_data: Dict[str, Any]): + """ + 创建会话 + + Args: + client_id: 客户端ID + auth_data: 认证数据 + """ + try: + session_id = self._generate_session_id() + + session_data = { + "client_id": client_id, + "login_time": time.time(), + "last_activity": time.time(), + "user_id": auth_data.get("user_id", ""), + "username": auth_data.get("username", ""), + "permissions": auth_data.get("permissions", []) + } + + self.sessions[session_id] = session_data + self.auth_state["active_sessions"] += 1 + self.auth_stats["sessions_created"] += 1 + + # 触发会话创建回调 + self._trigger_auth_callback("session_created", { + "session_id": session_id, + "client_id": client_id, + "auth_data": auth_data, + "timestamp": time.time() + }) + + except Exception as e: + print(f"✗ 会话创建失败: {e}") + self.auth_stats["auth_errors"] += 1 + + def _generate_session_id(self) -> str: + """ + 生成会话ID + + Returns: + 生成的会话ID + """ + try: + return secrets.token_urlsafe(32) + except Exception as e: + print(f"✗ 会话ID生成失败: {e}") + self.auth_stats["auth_errors"] += 1 + return f"session_{int(time.time() * 1000000)}" + + def generate_token(self, user_id: str, permissions: List[str] = None) -> str: + """ + 生成令牌 + + Args: + user_id: 用户ID + permissions: 权限列表 + + Returns: + 生成的令牌 + """ + try: + token = secrets.token_urlsafe(64) + + self.tokens[token] = { + "user_id": user_id, + "permissions": permissions or [], + "created_time": time.time() + } + + self.auth_stats["tokens_generated"] += 1 + + return token + + except Exception as e: + print(f"✗ 令牌生成失败: {e}") + self.auth_stats["auth_errors"] += 1 + return "" + + def validate_token(self, token: str) -> bool: + """ + 验证令牌 + + Args: + token: 令牌 + + Returns: + 令牌是否有效 + """ + try: + if not token: + return False + + if token in self.tokens: + token_data = self.tokens[token] + if time.time() - token_data["created_time"] < self.auth_config["token_lifetime"]: + return True + else: + # 令牌过期,移除 + del self.tokens[token] + + return False + + except Exception as e: + print(f"✗ 令牌验证失败: {e}") + self.auth_stats["auth_errors"] += 1 + return False + + def hash_password(self, password: str, salt: str = None) -> Dict[str, str]: + """ + 哈希密码 + + Args: + password: 密码 + salt: 盐值(可选) + + Returns: + 包含哈希值和盐值的字典 + """ + try: + if salt is None: + salt = secrets.token_hex(16) + + # 使用PBKDF2哈希密码 + password_hash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt.encode('utf-8'), 100000) + return { + "hash": password_hash.hex(), + "salt": salt + } + + except Exception as e: + print(f"✗ 密码哈希失败: {e}") + self.auth_stats["auth_errors"] += 1 + return {} + + def verify_password(self, password: str, password_hash: str, salt: str) -> bool: + """ + 验证密码 + + Args: + password: 密码 + password_hash: 密码哈希值 + salt: 盐值 + + Returns: + 密码是否正确 + """ + try: + # 重新哈希密码 + new_hash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt.encode('utf-8'), 100000) + return hmac.compare_digest(new_hash.hex(), password_hash) + + except Exception as e: + print(f"✗ 密码验证失败: {e}") + self.auth_stats["auth_errors"] += 1 + return False + + def _block_ip(self, client_ip: str, duration: float): + """ + 阻止IP地址 + + Args: + client_ip: 客户端IP + duration: 阻止持续时间(秒) + """ + try: + self.blocked_ips[client_ip] = { + "block_time": time.time(), + "duration": duration + } + + # 触发用户禁止回调 + self._trigger_auth_callback("user_banned", { + "ip_address": client_ip, + "duration": duration, + "timestamp": time.time() + }) + + # 记录审计日志 + if self.auth_config["enable_audit_logging"]: + self._log_auth_event("ip_blocked", None, client_ip) + + print(f"✓ IP地址已被阻止: {client_ip} (持续 {duration} 秒)") + + except Exception as e: + print(f"✗ IP阻止失败: {e}") + self.auth_stats["auth_errors"] += 1 + + def unblock_ip(self, client_ip: str) -> bool: + """ + 解除IP阻止 + + Args: + client_ip: 客户端IP + + Returns: + 是否解除成功 + """ + try: + if client_ip in self.blocked_ips: + del self.blocked_ips[client_ip] + print(f"✓ IP地址阻止已解除: {client_ip}") + return True + else: + print(f"✗ IP地址未被阻止: {client_ip}") + return False + + except Exception as e: + print(f"✗ IP阻止解除失败: {e}") + self.auth_stats["auth_errors"] += 1 + return False + + def _log_auth_event(self, event_type: str, client_id: str = None, client_ip: str = None): + """ + 记录认证事件 + + Args: + event_type: 事件类型 + client_id: 客户端ID(可选) + client_ip: 客户端IP(可选) + """ + try: + if not self.auth_config["enable_audit_logging"]: + return + + event = { + "event_type": event_type, + "client_id": client_id, + "client_ip": client_ip, + "timestamp": time.time() + } + + # 在实际实现中,这里会将事件记录到日志文件或数据库 + print(f"[认证日志] {event_type} - 客户端: {client_id}, IP: {client_ip}") + + except Exception as e: + print(f"✗ 认证事件记录失败: {e}") + self.auth_stats["auth_errors"] += 1 + + def get_active_sessions(self) -> Dict[str, Any]: + """ + 获取活跃会话 + + Returns: + 活跃会话字典 + """ + try: + return self.sessions.copy() + except Exception as e: + print(f"✗ 获取活跃会话失败: {e}") + self.auth_stats["auth_errors"] += 1 + return {} + + def get_blocked_ips(self) -> Dict[str, Any]: + """ + 获取被阻止的IP地址 + + Returns: + 被阻止的IP地址字典 + """ + try: + return self.blocked_ips.copy() + except Exception as e: + print(f"✗ 获取被阻止的IP地址失败: {e}") + self.auth_stats["auth_errors"] += 1 + return {} + + def get_auth_stats(self) -> Dict[str, Any]: + """ + 获取认证统计信息 + + Returns: + 认证统计字典 + """ + return { + "state": self.auth_state.copy(), + "stats": self.auth_stats.copy(), + "config": self.auth_config.copy() + } + + def reset_stats(self): + """重置认证统计信息""" + try: + self.auth_stats = { + "tokens_generated": 0, + "sessions_created": 0, + "sessions_expired": 0, + "users_banned": 0, + "auth_errors": 0 + } + print("✓ 认证统计信息已重置") + except Exception as e: + print(f"✗ 认证统计信息重置失败: {e}") + + def set_auth_config(self, config: Dict[str, Any]) -> bool: + """ + 设置认证配置 + + Args: + config: 认证配置字典 + + Returns: + 是否设置成功 + """ + try: + self.auth_config.update(config) + print(f"✓ 认证配置已更新: {self.auth_config}") + return True + except Exception as e: + print(f"✗ 认证配置设置失败: {e}") + return False + + def get_auth_config(self) -> Dict[str, Any]: + """ + 获取认证配置 + + Returns: + 认证配置字典 + """ + return self.auth_config.copy() + + def _trigger_auth_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发认证回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.auth_callbacks: + for callback in self.auth_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 认证回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 认证回调触发失败: {e}") + + def register_auth_callback(self, callback_type: str, callback: callable): + """ + 注册认证回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.auth_callbacks: + self.auth_callbacks[callback_type].append(callback) + print(f"✓ 认证回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 认证回调注册失败: {e}") + + def unregister_auth_callback(self, callback_type: str, callback: callable): + """ + 注销认证回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.auth_callbacks: + if callback in self.auth_callbacks[callback_type]: + self.auth_callbacks[callback_type].remove(callback) + print(f"✓ 认证回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 认证回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/realtime_communication/clients/client_manager.py b/plugins/user/realtime_communication/clients/client_manager.py index 67fd9ae8..4688a1b2 100644 --- a/plugins/user/realtime_communication/clients/client_manager.py +++ b/plugins/user/realtime_communication/clients/client_manager.py @@ -1,724 +1,724 @@ -""" -客户端管理器模块 -管理客户端连接和状态 -""" - -import time -import threading -import hashlib -from typing import Dict, Any, List, Optional - -class ClientManager: - """ - 客户端管理器 - 管理客户端连接和状态 - """ - - def __init__(self, plugin): - """ - 初始化客户端管理器 - - Args: - plugin: 实时通信插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 客户端配置 - self.client_config = { - "enable_authentication": True, - "auth_timeout": 30.0, - "max_clients": 1000, - "enable_heartbeat": True, - "heartbeat_interval": 30.0, - "enable_rate_limiting": True, - "max_messages_per_second": 100, - "enable_banning": True, - "ban_duration": 3600, - "client_timeout": 60.0 - } - - # 客户端状态 - self.client_state = { - "current_clients": 0, - "total_clients": 0, - "authenticated_clients": 0, - "banned_clients": 0 - } - - # 客户端存储 - self.clients = {} - self.client_lock = threading.RLock() - - # 客户端统计 - self.client_stats = { - "clients_joined": 0, - "clients_left": 0, - "authentication_success": 0, - "authentication_failed": 0, - "messages_sent": 0, - "messages_received": 0, - "banned_clients": 0, - "errors": 0 - } - - # 黑名单管理 - self.banned_ips = {} - self.banned_users = {} - - # 回调函数 - self.client_callbacks = { - "client_joined": [], - "client_left": [], - "client_authenticated": [], - "client_banned": [], - "client_error": [] - } - - # 时间戳记录 - self.last_heartbeat_check = 0.0 - self.last_cleanup = 0.0 - - print("✓ 客户端管理器已创建") - - def initialize(self) -> bool: - """ - 初始化客户端管理器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化客户端管理器...") - - self.initialized = True - print("✓ 客户端管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 客户端管理器初始化失败: {e}") - self.client_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用客户端管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 客户端管理器未初始化") - return False - - self.enabled = True - print("✓ 客户端管理器已启用") - return True - - except Exception as e: - print(f"✗ 客户端管理器启用失败: {e}") - self.client_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用客户端管理器""" - try: - self.enabled = False - - # 断开所有客户端 - self._disconnect_all_clients() - - print("✓ 客户端管理器已禁用") - - except Exception as e: - print(f"✗ 客户端管理器禁用失败: {e}") - self.client_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理客户端管理器资源""" - try: - # 禁用客户端管理器 - if self.enabled: - self.disable() - - # 清理回调 - self.client_callbacks.clear() - - self.initialized = False - print("✓ 客户端管理器资源已清理") - - except Exception as e: - print(f"✗ 客户端管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新客户端管理器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 定期检查心跳 - if current_time - self.last_heartbeat_check > self.client_config["heartbeat_interval"]: - self._check_client_heartbeats(current_time) - self.last_heartbeat_check = current_time - - # 定期清理 - if current_time - self.last_cleanup > 60.0: # 每分钟清理一次 - self._cleanup_banned_clients(current_time) - self.last_cleanup = current_time - - except Exception as e: - print(f"✗ 客户端管理器更新失败: {e}") - self.client_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def _check_client_heartbeats(self, current_time: float): - """检查客户端心跳""" - try: - if not self.client_config["enable_heartbeat"]: - return - - clients_to_disconnect = [] - with self.client_lock: - for client_id, client_data in self.clients.items(): - last_heartbeat = client_data.get("last_heartbeat", client_data["connect_time"]) - if current_time - last_heartbeat > self.client_config["client_timeout"]: - clients_to_disconnect.append(client_id) - - for client_id in clients_to_disconnect: - self.disconnect_client(client_id, "timeout") - - except Exception as e: - print(f"✗ 客户端心跳检查失败: {e}") - self.client_stats["errors"] += 1 - - def _cleanup_banned_clients(self, current_time: float): - """清理过期的黑名单客户端""" - try: - expired_bans = [] - for ip, ban_data in self.banned_ips.items(): - if current_time - ban_data["ban_time"] > self.client_config["ban_duration"]: - expired_bans.append(ip) - - for ip in expired_bans: - del self.banned_ips[ip] - self.client_state["banned_clients"] -= 1 - - except Exception as e: - print(f"✗ 黑名单清理失败: {e}") - self.client_stats["errors"] += 1 - - def _disconnect_all_clients(self): - """断开所有客户端连接""" - try: - with self.client_lock: - client_ids = list(self.clients.keys()) - - for client_id in client_ids: - self.disconnect_client(client_id, "server_shutdown") - - except Exception as e: - print(f"✗ 断开所有客户端连接失败: {e}") - self.client_stats["errors"] += 1 - - def add_client(self, client_id: str, client_info: Dict[str, Any]) -> bool: - """ - 添加客户端 - - Args: - client_id: 客户端ID - client_info: 客户端信息 - - Returns: - 是否添加成功 - """ - try: - if not self.enabled: - return False - - # 检查客户端数量限制 - with self.client_lock: - if len(self.clients) >= self.client_config["max_clients"]: - print("✗ 客户端数量已达上限") - return False - - # 检查IP是否被禁止 - client_ip = client_info.get("ip_address", "") - if self._is_ip_banned(client_ip): - print(f"✗ IP地址被禁止: {client_ip}") - return False - - # 添加客户端 - client_data = { - "client_id": client_id, - "client_info": client_info, - "connect_time": time.time(), - "last_heartbeat": time.time(), - "is_authenticated": False, - "user_id": "", - "username": "", - "permissions": [], - "messages_sent": 0, - "messages_received": 0, - "bytes_sent": 0, - "bytes_received": 0 - } - - with self.client_lock: - self.clients[client_id] = client_data - - self.client_state["current_clients"] = len(self.clients) - self.client_state["total_clients"] += 1 - self.client_stats["clients_joined"] += 1 - - # 触发客户端加入回调 - self._trigger_client_callback("client_joined", { - "client_id": client_id, - "client_info": client_info, - "timestamp": time.time() - }) - - print(f"✓ 客户端已添加: {client_id}") - return True - - except Exception as e: - print(f"✗ 添加客户端失败: {e}") - self.client_stats["errors"] += 1 - return False - - def remove_client(self, client_id: str, reason: str = "unknown") -> bool: - """ - 移除客户端 - - Args: - client_id: 客户端ID - reason: 移除原因 - - Returns: - 是否移除成功 - """ - try: - with self.client_lock: - if client_id in self.clients: - client_data = self.clients[client_id] - if client_data["is_authenticated"]: - self.client_state["authenticated_clients"] -= 1 - del self.clients[client_id] - - self.client_state["current_clients"] = len(self.clients) - self.client_stats["clients_left"] += 1 - - # 触发客户端离开回调 - self._trigger_client_callback("client_left", { - "client_id": client_id, - "reason": reason, - "timestamp": time.time() - }) - - print(f"✓ 客户端已移除: {client_id} (原因: {reason})") - return True - - except Exception as e: - print(f"✗ 移除客户端失败: {e}") - self.client_stats["errors"] += 1 - return False - - def disconnect_client(self, client_id: str, reason: str = "kicked") -> bool: - """ - 断开客户端连接 - - Args: - client_id: 客户端ID - reason: 断开原因 - - Returns: - 是否断开成功 - """ - try: - # 通知WebSocket服务器断开连接 - if self.plugin.websocket_server: - self.plugin.websocket_server.disconnect_client(client_id, reason) - - # 移除客户端 - return self.remove_client(client_id, reason) - - except Exception as e: - print(f"✗ 断开客户端连接失败: {e}") - self.client_stats["errors"] += 1 - return False - - def authenticate_client(self, client_id: str, auth_data: Dict[str, Any]) -> bool: - """ - 认证客户端 - - Args: - client_id: 客户端ID - auth_data: 认证数据 - - Returns: - 是否认证成功 - """ - try: - if not self.enabled or not self.client_config["enable_authentication"]: - # 如果未启用认证,直接通过 - with self.client_lock: - if client_id in self.clients: - self.clients[client_id]["is_authenticated"] = True - return True - - # 使用认证管理器进行认证 - if self.plugin.auth_manager: - auth_result = self.plugin.auth_manager.authenticate_client(client_id, auth_data) - if auth_result: - # 更新客户端认证状态 - with self.client_lock: - if client_id in self.clients: - self.clients[client_id]["is_authenticated"] = True - self.clients[client_id]["user_id"] = auth_data.get("user_id", "") - self.clients[client_id]["username"] = auth_data.get("username", "") - self.clients[client_id]["permissions"] = auth_data.get("permissions", []) - - self.client_state["authenticated_clients"] += 1 - self.client_stats["authentication_success"] += 1 - - # 触发客户端认证回调 - self._trigger_client_callback("client_authenticated", { - "client_id": client_id, - "user_id": auth_data.get("user_id", ""), - "username": auth_data.get("username", ""), - "timestamp": time.time() - }) - - print(f"✓ 客户端认证成功: {client_id}") - return True - else: - self.client_stats["authentication_failed"] += 1 - print(f"✗ 客户端认证失败: {client_id}") - return False - else: - print("✗ 认证管理器不可用") - return False - - except Exception as e: - print(f"✗ 客户端认证失败: {e}") - self.client_stats["errors"] += 1 - return False - - def update_client_heartbeat(self, client_id: str) -> bool: - """ - 更新客户端心跳 - - Args: - client_id: 客户端ID - - Returns: - 是否更新成功 - """ - try: - with self.client_lock: - if client_id in self.clients: - self.clients[client_id]["last_heartbeat"] = time.time() - return True - else: - return False - - except Exception as e: - print(f"✗ 更新客户端心跳失败: {e}") - self.client_stats["errors"] += 1 - return False - - def get_client(self, client_id: str) -> Optional[Dict[str, Any]]: - """ - 获取客户端信息 - - Args: - client_id: 客户端ID - - Returns: - 客户端信息或None - """ - try: - with self.client_lock: - if client_id in self.clients: - return self.clients[client_id].copy() - else: - return None - - except Exception as e: - print(f"✗ 获取客户端信息失败: {e}") - self.client_stats["errors"] += 1 - return None - - def get_all_clients(self) -> Dict[str, Any]: - """ - 获取所有客户端信息 - - Returns: - 所有客户端信息字典 - """ - try: - with self.client_lock: - return {k: v.copy() for k, v in self.clients.items()} - except Exception as e: - print(f"✗ 获取所有客户端信息失败: {e}") - self.client_stats["errors"] += 1 - return {} - - def ban_client(self, client_id: str, reason: str = "violation", duration: float = 3600) -> bool: - """ - 禁止客户端 - - Args: - client_id: 客户端ID - reason: 禁止原因 - duration: 禁止时长(秒) - - Returns: - 是否禁止成功 - """ - try: - if not self.client_config["enable_banning"]: - return False - - client_data = self.get_client(client_id) - if not client_data: - return False - - client_ip = client_data["client_info"].get("ip_address", "") - if client_ip: - # 添加到IP黑名单 - self.banned_ips[client_ip] = { - "client_id": client_id, - "ban_time": time.time(), - "reason": reason, - "duration": duration - } - - self.client_state["banned_clients"] += 1 - self.client_stats["banned_clients"] += 1 - - # 断开客户端连接 - self.disconnect_client(client_id, f"banned: {reason}") - - # 触发客户端禁止回调 - self._trigger_client_callback("client_banned", { - "client_id": client_id, - "ip_address": client_ip, - "reason": reason, - "duration": duration, - "timestamp": time.time() - }) - - print(f"✓ 客户端已被禁止: {client_id} (IP: {client_ip})") - return True - else: - return False - - except Exception as e: - print(f"✗ 禁止客户端失败: {e}") - self.client_stats["errors"] += 1 - return False - - def _is_ip_banned(self, ip_address: str) -> bool: - """ - 检查IP是否被禁止 - - Args: - ip_address: IP地址 - - Returns: - IP是否被禁止 - """ - try: - if not self.client_config["enable_banning"]: - return False - - if ip_address in self.banned_ips: - ban_data = self.banned_ips[ip_address] - if time.time() - ban_data["ban_time"] < ban_data["duration"]: - return True - else: - # 禁止时间已过期,移除 - del self.banned_ips[ip_address] - self.client_state["banned_clients"] -= 1 - - return False - - except Exception as e: - print(f"✗ IP禁止检查失败: {e}") - self.client_stats["errors"] += 1 - return False - - def unban_ip(self, ip_address: str) -> bool: - """ - 解除IP禁止 - - Args: - ip_address: IP地址 - - Returns: - 是否解除成功 - """ - try: - if ip_address in self.banned_ips: - del self.banned_ips[ip_address] - self.client_state["banned_clients"] -= 1 - print(f"✓ IP禁止已解除: {ip_address}") - return True - else: - print(f"✗ IP未被禁止: {ip_address}") - return False - - except Exception as e: - print(f"✗ 解除IP禁止失败: {e}") - self.client_stats["errors"] += 1 - return False - - def is_client_authenticated(self, client_id: str) -> bool: - """ - 检查客户端是否已认证 - - Args: - client_id: 客户端ID - - Returns: - 客户端是否已认证 - """ - try: - with self.client_lock: - if client_id in self.clients: - return self.clients[client_id]["is_authenticated"] - else: - return False - - except Exception as e: - print(f"✗ 客户端认证检查失败: {e}") - self.client_stats["errors"] += 1 - return False - - def get_client_stats(self) -> Dict[str, Any]: - """ - 获取客户端统计信息 - - Returns: - 客户端统计字典 - """ - return { - "state": self.client_state.copy(), - "stats": self.client_stats.copy(), - "config": self.client_config.copy() - } - - def reset_stats(self): - """重置客户端统计信息""" - try: - self.client_stats = { - "clients_joined": 0, - "clients_left": 0, - "authentication_success": 0, - "authentication_failed": 0, - "messages_sent": 0, - "messages_received": 0, - "banned_clients": 0, - "errors": 0 - } - print("✓ 客户端统计信息已重置") - except Exception as e: - print(f"✗ 客户端统计信息重置失败: {e}") - - def set_client_config(self, config: Dict[str, Any]) -> bool: - """ - 设置客户端配置 - - Args: - config: 客户端配置字典 - - Returns: - 是否设置成功 - """ - try: - self.client_config.update(config) - print(f"✓ 客户端配置已更新: {self.client_config}") - return True - except Exception as e: - print(f"✗ 客户端配置设置失败: {e}") - return False - - def get_client_config(self) -> Dict[str, Any]: - """ - 获取客户端配置 - - Returns: - 客户端配置字典 - """ - return self.client_config.copy() - - def _trigger_client_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发客户端回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.client_callbacks: - for callback in self.client_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 客户端回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 客户端回调触发失败: {e}") - - def register_client_callback(self, callback_type: str, callback: callable): - """ - 注册客户端回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.client_callbacks: - self.client_callbacks[callback_type].append(callback) - print(f"✓ 客户端回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 客户端回调注册失败: {e}") - - def unregister_client_callback(self, callback_type: str, callback: callable): - """ - 注销客户端回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.client_callbacks: - if callback in self.client_callbacks[callback_type]: - self.client_callbacks[callback_type].remove(callback) - print(f"✓ 客户端回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +客户端管理器模块 +管理客户端连接和状态 +""" + +import time +import threading +import hashlib +from typing import Dict, Any, List, Optional + +class ClientManager: + """ + 客户端管理器 + 管理客户端连接和状态 + """ + + def __init__(self, plugin): + """ + 初始化客户端管理器 + + Args: + plugin: 实时通信插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 客户端配置 + self.client_config = { + "enable_authentication": True, + "auth_timeout": 30.0, + "max_clients": 1000, + "enable_heartbeat": True, + "heartbeat_interval": 30.0, + "enable_rate_limiting": True, + "max_messages_per_second": 100, + "enable_banning": True, + "ban_duration": 3600, + "client_timeout": 60.0 + } + + # 客户端状态 + self.client_state = { + "current_clients": 0, + "total_clients": 0, + "authenticated_clients": 0, + "banned_clients": 0 + } + + # 客户端存储 + self.clients = {} + self.client_lock = threading.RLock() + + # 客户端统计 + self.client_stats = { + "clients_joined": 0, + "clients_left": 0, + "authentication_success": 0, + "authentication_failed": 0, + "messages_sent": 0, + "messages_received": 0, + "banned_clients": 0, + "errors": 0 + } + + # 黑名单管理 + self.banned_ips = {} + self.banned_users = {} + + # 回调函数 + self.client_callbacks = { + "client_joined": [], + "client_left": [], + "client_authenticated": [], + "client_banned": [], + "client_error": [] + } + + # 时间戳记录 + self.last_heartbeat_check = 0.0 + self.last_cleanup = 0.0 + + print("✓ 客户端管理器已创建") + + def initialize(self) -> bool: + """ + 初始化客户端管理器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化客户端管理器...") + + self.initialized = True + print("✓ 客户端管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 客户端管理器初始化失败: {e}") + self.client_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用客户端管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 客户端管理器未初始化") + return False + + self.enabled = True + print("✓ 客户端管理器已启用") + return True + + except Exception as e: + print(f"✗ 客户端管理器启用失败: {e}") + self.client_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用客户端管理器""" + try: + self.enabled = False + + # 断开所有客户端 + self._disconnect_all_clients() + + print("✓ 客户端管理器已禁用") + + except Exception as e: + print(f"✗ 客户端管理器禁用失败: {e}") + self.client_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理客户端管理器资源""" + try: + # 禁用客户端管理器 + if self.enabled: + self.disable() + + # 清理回调 + self.client_callbacks.clear() + + self.initialized = False + print("✓ 客户端管理器资源已清理") + + except Exception as e: + print(f"✗ 客户端管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新客户端管理器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 定期检查心跳 + if current_time - self.last_heartbeat_check > self.client_config["heartbeat_interval"]: + self._check_client_heartbeats(current_time) + self.last_heartbeat_check = current_time + + # 定期清理 + if current_time - self.last_cleanup > 60.0: # 每分钟清理一次 + self._cleanup_banned_clients(current_time) + self.last_cleanup = current_time + + except Exception as e: + print(f"✗ 客户端管理器更新失败: {e}") + self.client_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def _check_client_heartbeats(self, current_time: float): + """检查客户端心跳""" + try: + if not self.client_config["enable_heartbeat"]: + return + + clients_to_disconnect = [] + with self.client_lock: + for client_id, client_data in self.clients.items(): + last_heartbeat = client_data.get("last_heartbeat", client_data["connect_time"]) + if current_time - last_heartbeat > self.client_config["client_timeout"]: + clients_to_disconnect.append(client_id) + + for client_id in clients_to_disconnect: + self.disconnect_client(client_id, "timeout") + + except Exception as e: + print(f"✗ 客户端心跳检查失败: {e}") + self.client_stats["errors"] += 1 + + def _cleanup_banned_clients(self, current_time: float): + """清理过期的黑名单客户端""" + try: + expired_bans = [] + for ip, ban_data in self.banned_ips.items(): + if current_time - ban_data["ban_time"] > self.client_config["ban_duration"]: + expired_bans.append(ip) + + for ip in expired_bans: + del self.banned_ips[ip] + self.client_state["banned_clients"] -= 1 + + except Exception as e: + print(f"✗ 黑名单清理失败: {e}") + self.client_stats["errors"] += 1 + + def _disconnect_all_clients(self): + """断开所有客户端连接""" + try: + with self.client_lock: + client_ids = list(self.clients.keys()) + + for client_id in client_ids: + self.disconnect_client(client_id, "server_shutdown") + + except Exception as e: + print(f"✗ 断开所有客户端连接失败: {e}") + self.client_stats["errors"] += 1 + + def add_client(self, client_id: str, client_info: Dict[str, Any]) -> bool: + """ + 添加客户端 + + Args: + client_id: 客户端ID + client_info: 客户端信息 + + Returns: + 是否添加成功 + """ + try: + if not self.enabled: + return False + + # 检查客户端数量限制 + with self.client_lock: + if len(self.clients) >= self.client_config["max_clients"]: + print("✗ 客户端数量已达上限") + return False + + # 检查IP是否被禁止 + client_ip = client_info.get("ip_address", "") + if self._is_ip_banned(client_ip): + print(f"✗ IP地址被禁止: {client_ip}") + return False + + # 添加客户端 + client_data = { + "client_id": client_id, + "client_info": client_info, + "connect_time": time.time(), + "last_heartbeat": time.time(), + "is_authenticated": False, + "user_id": "", + "username": "", + "permissions": [], + "messages_sent": 0, + "messages_received": 0, + "bytes_sent": 0, + "bytes_received": 0 + } + + with self.client_lock: + self.clients[client_id] = client_data + + self.client_state["current_clients"] = len(self.clients) + self.client_state["total_clients"] += 1 + self.client_stats["clients_joined"] += 1 + + # 触发客户端加入回调 + self._trigger_client_callback("client_joined", { + "client_id": client_id, + "client_info": client_info, + "timestamp": time.time() + }) + + print(f"✓ 客户端已添加: {client_id}") + return True + + except Exception as e: + print(f"✗ 添加客户端失败: {e}") + self.client_stats["errors"] += 1 + return False + + def remove_client(self, client_id: str, reason: str = "unknown") -> bool: + """ + 移除客户端 + + Args: + client_id: 客户端ID + reason: 移除原因 + + Returns: + 是否移除成功 + """ + try: + with self.client_lock: + if client_id in self.clients: + client_data = self.clients[client_id] + if client_data["is_authenticated"]: + self.client_state["authenticated_clients"] -= 1 + del self.clients[client_id] + + self.client_state["current_clients"] = len(self.clients) + self.client_stats["clients_left"] += 1 + + # 触发客户端离开回调 + self._trigger_client_callback("client_left", { + "client_id": client_id, + "reason": reason, + "timestamp": time.time() + }) + + print(f"✓ 客户端已移除: {client_id} (原因: {reason})") + return True + + except Exception as e: + print(f"✗ 移除客户端失败: {e}") + self.client_stats["errors"] += 1 + return False + + def disconnect_client(self, client_id: str, reason: str = "kicked") -> bool: + """ + 断开客户端连接 + + Args: + client_id: 客户端ID + reason: 断开原因 + + Returns: + 是否断开成功 + """ + try: + # 通知WebSocket服务器断开连接 + if self.plugin.websocket_server: + self.plugin.websocket_server.disconnect_client(client_id, reason) + + # 移除客户端 + return self.remove_client(client_id, reason) + + except Exception as e: + print(f"✗ 断开客户端连接失败: {e}") + self.client_stats["errors"] += 1 + return False + + def authenticate_client(self, client_id: str, auth_data: Dict[str, Any]) -> bool: + """ + 认证客户端 + + Args: + client_id: 客户端ID + auth_data: 认证数据 + + Returns: + 是否认证成功 + """ + try: + if not self.enabled or not self.client_config["enable_authentication"]: + # 如果未启用认证,直接通过 + with self.client_lock: + if client_id in self.clients: + self.clients[client_id]["is_authenticated"] = True + return True + + # 使用认证管理器进行认证 + if self.plugin.auth_manager: + auth_result = self.plugin.auth_manager.authenticate_client(client_id, auth_data) + if auth_result: + # 更新客户端认证状态 + with self.client_lock: + if client_id in self.clients: + self.clients[client_id]["is_authenticated"] = True + self.clients[client_id]["user_id"] = auth_data.get("user_id", "") + self.clients[client_id]["username"] = auth_data.get("username", "") + self.clients[client_id]["permissions"] = auth_data.get("permissions", []) + + self.client_state["authenticated_clients"] += 1 + self.client_stats["authentication_success"] += 1 + + # 触发客户端认证回调 + self._trigger_client_callback("client_authenticated", { + "client_id": client_id, + "user_id": auth_data.get("user_id", ""), + "username": auth_data.get("username", ""), + "timestamp": time.time() + }) + + print(f"✓ 客户端认证成功: {client_id}") + return True + else: + self.client_stats["authentication_failed"] += 1 + print(f"✗ 客户端认证失败: {client_id}") + return False + else: + print("✗ 认证管理器不可用") + return False + + except Exception as e: + print(f"✗ 客户端认证失败: {e}") + self.client_stats["errors"] += 1 + return False + + def update_client_heartbeat(self, client_id: str) -> bool: + """ + 更新客户端心跳 + + Args: + client_id: 客户端ID + + Returns: + 是否更新成功 + """ + try: + with self.client_lock: + if client_id in self.clients: + self.clients[client_id]["last_heartbeat"] = time.time() + return True + else: + return False + + except Exception as e: + print(f"✗ 更新客户端心跳失败: {e}") + self.client_stats["errors"] += 1 + return False + + def get_client(self, client_id: str) -> Optional[Dict[str, Any]]: + """ + 获取客户端信息 + + Args: + client_id: 客户端ID + + Returns: + 客户端信息或None + """ + try: + with self.client_lock: + if client_id in self.clients: + return self.clients[client_id].copy() + else: + return None + + except Exception as e: + print(f"✗ 获取客户端信息失败: {e}") + self.client_stats["errors"] += 1 + return None + + def get_all_clients(self) -> Dict[str, Any]: + """ + 获取所有客户端信息 + + Returns: + 所有客户端信息字典 + """ + try: + with self.client_lock: + return {k: v.copy() for k, v in self.clients.items()} + except Exception as e: + print(f"✗ 获取所有客户端信息失败: {e}") + self.client_stats["errors"] += 1 + return {} + + def ban_client(self, client_id: str, reason: str = "violation", duration: float = 3600) -> bool: + """ + 禁止客户端 + + Args: + client_id: 客户端ID + reason: 禁止原因 + duration: 禁止时长(秒) + + Returns: + 是否禁止成功 + """ + try: + if not self.client_config["enable_banning"]: + return False + + client_data = self.get_client(client_id) + if not client_data: + return False + + client_ip = client_data["client_info"].get("ip_address", "") + if client_ip: + # 添加到IP黑名单 + self.banned_ips[client_ip] = { + "client_id": client_id, + "ban_time": time.time(), + "reason": reason, + "duration": duration + } + + self.client_state["banned_clients"] += 1 + self.client_stats["banned_clients"] += 1 + + # 断开客户端连接 + self.disconnect_client(client_id, f"banned: {reason}") + + # 触发客户端禁止回调 + self._trigger_client_callback("client_banned", { + "client_id": client_id, + "ip_address": client_ip, + "reason": reason, + "duration": duration, + "timestamp": time.time() + }) + + print(f"✓ 客户端已被禁止: {client_id} (IP: {client_ip})") + return True + else: + return False + + except Exception as e: + print(f"✗ 禁止客户端失败: {e}") + self.client_stats["errors"] += 1 + return False + + def _is_ip_banned(self, ip_address: str) -> bool: + """ + 检查IP是否被禁止 + + Args: + ip_address: IP地址 + + Returns: + IP是否被禁止 + """ + try: + if not self.client_config["enable_banning"]: + return False + + if ip_address in self.banned_ips: + ban_data = self.banned_ips[ip_address] + if time.time() - ban_data["ban_time"] < ban_data["duration"]: + return True + else: + # 禁止时间已过期,移除 + del self.banned_ips[ip_address] + self.client_state["banned_clients"] -= 1 + + return False + + except Exception as e: + print(f"✗ IP禁止检查失败: {e}") + self.client_stats["errors"] += 1 + return False + + def unban_ip(self, ip_address: str) -> bool: + """ + 解除IP禁止 + + Args: + ip_address: IP地址 + + Returns: + 是否解除成功 + """ + try: + if ip_address in self.banned_ips: + del self.banned_ips[ip_address] + self.client_state["banned_clients"] -= 1 + print(f"✓ IP禁止已解除: {ip_address}") + return True + else: + print(f"✗ IP未被禁止: {ip_address}") + return False + + except Exception as e: + print(f"✗ 解除IP禁止失败: {e}") + self.client_stats["errors"] += 1 + return False + + def is_client_authenticated(self, client_id: str) -> bool: + """ + 检查客户端是否已认证 + + Args: + client_id: 客户端ID + + Returns: + 客户端是否已认证 + """ + try: + with self.client_lock: + if client_id in self.clients: + return self.clients[client_id]["is_authenticated"] + else: + return False + + except Exception as e: + print(f"✗ 客户端认证检查失败: {e}") + self.client_stats["errors"] += 1 + return False + + def get_client_stats(self) -> Dict[str, Any]: + """ + 获取客户端统计信息 + + Returns: + 客户端统计字典 + """ + return { + "state": self.client_state.copy(), + "stats": self.client_stats.copy(), + "config": self.client_config.copy() + } + + def reset_stats(self): + """重置客户端统计信息""" + try: + self.client_stats = { + "clients_joined": 0, + "clients_left": 0, + "authentication_success": 0, + "authentication_failed": 0, + "messages_sent": 0, + "messages_received": 0, + "banned_clients": 0, + "errors": 0 + } + print("✓ 客户端统计信息已重置") + except Exception as e: + print(f"✗ 客户端统计信息重置失败: {e}") + + def set_client_config(self, config: Dict[str, Any]) -> bool: + """ + 设置客户端配置 + + Args: + config: 客户端配置字典 + + Returns: + 是否设置成功 + """ + try: + self.client_config.update(config) + print(f"✓ 客户端配置已更新: {self.client_config}") + return True + except Exception as e: + print(f"✗ 客户端配置设置失败: {e}") + return False + + def get_client_config(self) -> Dict[str, Any]: + """ + 获取客户端配置 + + Returns: + 客户端配置字典 + """ + return self.client_config.copy() + + def _trigger_client_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发客户端回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.client_callbacks: + for callback in self.client_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 客户端回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 客户端回调触发失败: {e}") + + def register_client_callback(self, callback_type: str, callback: callable): + """ + 注册客户端回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.client_callbacks: + self.client_callbacks[callback_type].append(callback) + print(f"✓ 客户端回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 客户端回调注册失败: {e}") + + def unregister_client_callback(self, callback_type: str, callback: callable): + """ + 注销客户端回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.client_callbacks: + if callback in self.client_callbacks[callback_type]: + self.client_callbacks[callback_type].remove(callback) + print(f"✓ 客户端回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 客户端回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/realtime_communication/config/comm_config.py b/plugins/user/realtime_communication/config/comm_config.py index 23549f94..9125e16d 100644 --- a/plugins/user/realtime_communication/config/comm_config.py +++ b/plugins/user/realtime_communication/config/comm_config.py @@ -1,759 +1,759 @@ -""" -通信配置管理器模块 -管理通信配置和用户偏好 -""" - -import time -import json -import os -from typing import Dict, Any, List, Optional - -class CommConfig: - """ - 通信配置管理器 - 管理通信配置和用户偏好 - """ - - def __init__(self, plugin): - """ - 初始化通信配置管理器 - - Args: - plugin: 实时通信插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 配置文件路径 - self.config_file_path = "/home/hello/EG/plugins/user/realtime_communication/config/communication_config.json" - - # 默认配置 - self.default_config = { - "server": { - "host": "0.0.0.0", - "port": 8080, - "enable_ssl": False, - "ssl_cert_file": "", - "ssl_key_file": "", - "max_connections": 1000, - "connection_timeout": 30.0, - "ping_interval": 30.0 - }, - "client": { - "enable_authentication": True, - "auth_timeout": 30.0, - "max_clients": 1000, - "enable_heartbeat": True, - "heartbeat_interval": 30.0, - "enable_rate_limiting": True, - "max_messages_per_second": 100, - "enable_banning": True, - "ban_duration": 3600 - }, - "message": { - "enable_message_filtering": True, - "max_message_size": 65536, - "enable_message_queue": True, - "message_queue_size": 10000, - "enable_message_compression": True, - "compression_threshold": 1024 - }, - "room": { - "max_rooms": 1000, - "max_clients_per_room": 100, - "enable_room_persistence": False, - "room_timeout": 3600, - "enable_room_passwords": True - }, - "protocol": { - "supported_protocols": ["websocket", "json", "binary"], - "default_protocol": "json", - "enable_compression": True, - "compression_threshold": 1024, - "enable_encryption": False, - "max_message_size": 65536 - }, - "auth": { - "enable_authentication": True, - "auth_timeout": 30.0, - "enable_token_auth": True, - "enable_password_auth": True, - "token_lifetime": 86400, - "enable_session_management": True, - "session_timeout": 3600, - "max_login_attempts": 5, - "login_lockout_duration": 900 - }, - "serialization": { - "default_format": "json", - "supported_formats": ["json", "pickle", "binary"], - "enable_compression": True, - "compression_threshold": 1024, - "enable_caching": True, - "cache_size": 1000 - }, - "monitoring": { - "enable_monitoring": True, - "monitoring_interval": 5.0, - "log_level": "INFO", - "enable_file_logging": True, - "log_file_path": "/home/hello/EG/plugins/user/realtime_communication/logs/communication_monitor.log" - }, - "events": { - "enable_event_system": True, - "max_event_queue_size": 10000, - "enable_event_filtering": True - } - } - - # 当前配置 - self.current_config = self.default_config.copy() - - # 配置状态 - self.config_state = { - "last_loaded": 0.0, - "last_saved": 0.0, - "config_version": "1.0.0", - "is_dirty": False - } - - # 配置统计 - self.config_stats = { - "loads": 0, - "saves": 0, - "updates": 0, - "config_errors": 0 - } - - # 配置监听器 - self.config_listeners = {} - - # 回调函数 - self.config_callbacks = { - "config_loaded": [], - "config_saved": [], - "config_updated": [], - "config_error": [] - } - - print("✓ 通信配置管理器已创建") - - def initialize(self) -> bool: - """ - 初始化通信配置管理器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化通信配置管理器...") - - # 创建配置目录 - config_dir = os.path.dirname(self.config_file_path) - if not os.path.exists(config_dir): - os.makedirs(config_dir) - - # 创建日志目录 - if "monitoring" in self.default_config: - log_dir = os.path.dirname(self.default_config["monitoring"]["log_file_path"]) - if not os.path.exists(log_dir): - os.makedirs(log_dir) - - # 加载配置 - self.load_config() - - self.initialized = True - print("✓ 通信配置管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 通信配置管理器初始化失败: {e}") - self.config_stats["config_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用通信配置管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 通信配置管理器未初始化") - return False - - self.enabled = True - print("✓ 通信配置管理器已启用") - return True - - except Exception as e: - print(f"✗ 通信配置管理器启用失败: {e}") - self.config_stats["config_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用通信配置管理器""" - try: - self.enabled = False - - # 保存配置(如果有更改) - if self.config_state["is_dirty"]: - self.save_config() - - print("✓ 通信配置管理器已禁用") - - except Exception as e: - print(f"✗ 通信配置管理器禁用失败: {e}") - self.config_stats["config_errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理通信配置管理器资源""" - try: - # 禁用通信配置管理器 - if self.enabled: - self.disable() - - # 清理回调 - self.config_callbacks.clear() - self.config_listeners.clear() - - self.initialized = False - print("✓ 通信配置管理器资源已清理") - - except Exception as e: - print(f"✗ 通信配置管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新通信配置管理器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - # 检查配置文件是否被外部修改 - self._check_config_file_changes() - - except Exception as e: - print(f"✗ 通信配置管理器更新失败: {e}") - self.config_stats["config_errors"] += 1 - import traceback - traceback.print_exc() - - def _check_config_file_changes(self): - """检查配置文件是否被外部修改""" - try: - if not os.path.exists(self.config_file_path): - return - - last_modified = os.path.getmtime(self.config_file_path) - if last_modified > self.config_state["last_loaded"]: - # 配置文件已被修改,重新加载 - print("检测到配置文件被外部修改,正在重新加载...") - self.load_config() - - except Exception as e: - print(f"✗ 配置文件变更检查失败: {e}") - self.config_stats["config_errors"] += 1 - - def load_config(self) -> bool: - """ - 加载配置 - - Returns: - 是否加载成功 - """ - try: - # 检查配置文件是否存在 - if os.path.exists(self.config_file_path): - with open(self.config_file_path, 'r', encoding='utf-8') as f: - loaded_config = json.load(f) - - # 合并配置(使用默认配置作为基础) - self.current_config = self._merge_config(self.default_config, loaded_config) - - print(f"✓ 配置已从文件加载: {self.config_file_path}") - else: - # 使用默认配置 - self.current_config = self.default_config.copy() - print("✓ 使用默认配置") - - self.config_state["last_loaded"] = time.time() - self.config_state["is_dirty"] = False - self.config_stats["loads"] += 1 - - # 触发配置加载回调 - self._trigger_config_callback("config_loaded", { - "config": self.current_config, - "timestamp": self.config_state["last_loaded"] - }) - - return True - - except Exception as e: - print(f"✗ 配置加载失败: {e}") - self.config_stats["config_errors"] += 1 - - # 使用默认配置 - self.current_config = self.default_config.copy() - return False - - def _merge_config(self, base_config: Dict[str, Any], override_config: Dict[str, Any]) -> Dict[str, Any]: - """ - 合并配置(递归) - - Args: - base_config: 基础配置 - override_config: 覆盖配置 - - Returns: - 合并后的配置 - """ - try: - merged = base_config.copy() - - for key, value in override_config.items(): - if key in merged and isinstance(merged[key], dict) and isinstance(value, dict): - # 递归合并嵌套字典 - merged[key] = self._merge_config(merged[key], value) - else: - # 直接覆盖 - merged[key] = value - - return merged - - except Exception as e: - print(f"✗ 配置合并失败: {e}") - self.config_stats["config_errors"] += 1 - return base_config - - def save_config(self) -> bool: - """ - 保存配置 - - Returns: - 是否保存成功 - """ - try: - # 创建配置目录(如果不存在) - config_dir = os.path.dirname(self.config_file_path) - if not os.path.exists(config_dir): - os.makedirs(config_dir) - - # 保存配置到文件 - with open(self.config_file_path, 'w', encoding='utf-8') as f: - json.dump(self.current_config, f, indent=2, ensure_ascii=False) - - self.config_state["last_saved"] = time.time() - self.config_state["is_dirty"] = False - self.config_stats["saves"] += 1 - - # 触发配置保存回调 - self._trigger_config_callback("config_saved", { - "config": self.current_config, - "timestamp": self.config_state["last_saved"] - }) - - print(f"✓ 配置已保存到文件: {self.config_file_path}") - return True - - except Exception as e: - print(f"✗ 配置保存失败: {e}") - self.config_stats["config_errors"] += 1 - return False - - def get_config(self, section: str = None, key: str = None, default_value: Any = None) -> Any: - """ - 获取配置值 - - Args: - section: 配置节(可选) - key: 配置键(可选) - default_value: 默认值 - - Returns: - 配置值 - """ - try: - if section is None: - return self.current_config.copy() - - if section not in self.current_config: - return default_value - - if key is None: - return self.current_config[section].copy() - - if key not in self.current_config[section]: - return default_value - - return self.current_config[section][key] - - except Exception as e: - print(f"✗ 获取配置失败: {e}") - self.config_stats["config_errors"] += 1 - return default_value - - def set_config(self, section: str, key: str, value: Any) -> bool: - """ - 设置配置值 - - Args: - section: 配置节 - key: 配置键 - value: 配置值 - - Returns: - 是否设置成功 - """ - try: - if section not in self.current_config: - self.current_config[section] = {} - - self.current_config[section][key] = value - self.config_state["is_dirty"] = True - self.config_stats["updates"] += 1 - - # 通知配置监听器 - self._notify_config_listeners(section, key, value) - - # 触发配置更新回调 - self._trigger_config_callback("config_updated", { - "section": section, - "key": key, - "value": value, - "timestamp": time.time() - }) - - return True - - except Exception as e: - print(f"✗ 设置配置失败: {e}") - self.config_stats["config_errors"] += 1 - return False - - def update_config_section(self, section: str, config_data: Dict[str, Any]) -> bool: - """ - 更新配置节 - - Args: - section: 配置节 - config_data: 配置数据 - - Returns: - 是否更新成功 - """ - try: - if section not in self.current_config: - self.current_config[section] = {} - - self.current_config[section].update(config_data) - self.config_state["is_dirty"] = True - self.config_stats["updates"] += 1 - - # 通知配置监听器 - for key, value in config_data.items(): - self._notify_config_listeners(section, key, value) - - # 触发配置更新回调 - self._trigger_config_callback("config_updated", { - "section": section, - "config_data": config_data, - "timestamp": time.time() - }) - - return True - - except Exception as e: - print(f"✗ 更新配置节失败: {e}") - self.config_stats["config_errors"] += 1 - return False - - def reset_config(self) -> bool: - """ - 重置配置为默认值 - - Returns: - 是否重置成功 - """ - try: - self.current_config = self.default_config.copy() - self.config_state["is_dirty"] = True - - # 通知所有配置监听器 - for section_name, section_data in self.current_config.items(): - if isinstance(section_data, dict): - for key, value in section_data.items(): - self._notify_config_listeners(section_name, key, value) - - print("✓ 配置已重置为默认值") - return True - - except Exception as e: - print(f"✗ 配置重置失败: {e}") - self.config_stats["config_errors"] += 1 - return False - - def register_config_listener(self, section: str, key: str, listener: callable): - """ - 注册配置监听器 - - Args: - section: 配置节 - key: 配置键 - listener: 监听器函数 - """ - try: - listener_key = f"{section}.{key}" - if listener_key not in self.config_listeners: - self.config_listeners[listener_key] = [] - - self.config_listeners[listener_key].append(listener) - print(f"✓ 配置监听器已注册: {listener_key}") - - except Exception as e: - print(f"✗ 配置监听器注册失败: {e}") - self.config_stats["config_errors"] += 1 - - def unregister_config_listener(self, section: str, key: str, listener: callable): - """ - 注销配置监听器 - - Args: - section: 配置节 - key: 配置键 - listener: 监听器函数 - """ - try: - listener_key = f"{section}.{key}" - if listener_key in self.config_listeners: - if listener in self.config_listeners[listener_key]: - self.config_listeners[listener_key].remove(listener) - print(f"✓ 配置监听器已注销: {listener_key}") - - except Exception as e: - print(f"✗ 配置监听器注销失败: {e}") - self.config_stats["config_errors"] += 1 - - def _notify_config_listeners(self, section: str, key: str, value: Any): - """ - 通知配置监听器 - - Args: - section: 配置节 - key: 配置键 - value: 配置值 - """ - try: - listener_key = f"{section}.{key}" - if listener_key in self.config_listeners: - for listener in self.config_listeners[listener_key]: - try: - listener(section, key, value) - except Exception as e: - print(f"✗ 配置监听器执行失败: {listener_key} - {e}") - - except Exception as e: - print(f"✗ 配置监听器通知失败: {e}") - self.config_stats["config_errors"] += 1 - - def get_config_stats(self) -> Dict[str, Any]: - """ - 获取配置统计信息 - - Returns: - 配置统计字典 - """ - return { - "state": self.config_state.copy(), - "stats": self.config_stats.copy(), - "config_sections": list(self.current_config.keys()) - } - - def reset_stats(self): - """重置配置统计信息""" - try: - self.config_stats = { - "loads": 0, - "saves": 0, - "updates": 0, - "config_errors": 0 - } - print("✓ 配置统计信息已重置") - except Exception as e: - print(f"✗ 配置统计信息重置失败: {e}") - - def validate_config(self) -> bool: - """ - 验证配置 - - Returns: - 配置是否有效 - """ - try: - # 检查必需的配置项 - required_sections = ["server", "client", "message", "room", "protocol", "auth", "serialization"] - for section in required_sections: - if section not in self.current_config: - print(f"✗ 缺少必需的配置节: {section}") - return False - - # 检查服务器配置 - server_config = self.current_config["server"] - if "port" not in server_config or not isinstance(server_config["port"], int): - print("✗ 无效的服务器端口配置") - return False - - if "max_connections" not in server_config or not isinstance(server_config["max_connections"], int): - print("✗ 无效的最大连接数配置") - return False - - # 检查客户端配置 - client_config = self.current_config["client"] - if "max_clients" not in client_config or not isinstance(client_config["max_clients"], int): - print("✗ 无效的最大客户端数配置") - return False - - print("✓ 配置验证通过") - return True - - except Exception as e: - print(f"✗ 配置验证失败: {e}") - self.config_stats["config_errors"] += 1 - return False - - def export_config(self, file_path: str) -> bool: - """ - 导出配置到文件 - - Args: - file_path: 导出文件路径 - - Returns: - 是否导出成功 - """ - try: - # 创建导出目录 - export_dir = os.path.dirname(file_path) - if not os.path.exists(export_dir): - os.makedirs(export_dir) - - # 导出配置 - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(self.current_config, f, indent=2, ensure_ascii=False) - - print(f"✓ 配置已导出到: {file_path}") - return True - - except Exception as e: - print(f"✗ 配置导出失败: {e}") - self.config_stats["config_errors"] += 1 - return False - - def import_config(self, file_path: str) -> bool: - """ - 从文件导入配置 - - Args: - file_path: 导入文件路径 - - Returns: - 是否导入成功 - """ - try: - if not os.path.exists(file_path): - print(f"✗ 配置文件不存在: {file_path}") - return False - - # 读取配置文件 - with open(file_path, 'r', encoding='utf-8') as f: - imported_config = json.load(f) - - # 验证配置 - if not isinstance(imported_config, dict): - print("✗ 无效的配置文件格式") - return False - - # 合并配置 - self.current_config = self._merge_config(self.default_config, imported_config) - self.config_state["is_dirty"] = True - - # 通知配置监听器 - for section_name, section_data in self.current_config.items(): - if isinstance(section_data, dict): - for key, value in section_data.items(): - self._notify_config_listeners(section_name, key, value) - - print(f"✓ 配置已从文件导入: {file_path}") - return True - - except Exception as e: - print(f"✗ 配置导入失败: {e}") - self.config_stats["config_errors"] += 1 - return False - - def _trigger_config_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发配置回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.config_callbacks: - for callback in self.config_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 配置回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 配置回调触发失败: {e}") - - def register_config_callback(self, callback_type: str, callback: callable): - """ - 注册配置回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.config_callbacks: - self.config_callbacks[callback_type].append(callback) - print(f"✓ 配置回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 配置回调注册失败: {e}") - - def unregister_config_callback(self, callback_type: str, callback: callable): - """ - 注销配置回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.config_callbacks: - if callback in self.config_callbacks[callback_type]: - self.config_callbacks[callback_type].remove(callback) - print(f"✓ 配置回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +通信配置管理器模块 +管理通信配置和用户偏好 +""" + +import time +import json +import os +from typing import Dict, Any, List, Optional + +class CommConfig: + """ + 通信配置管理器 + 管理通信配置和用户偏好 + """ + + def __init__(self, plugin): + """ + 初始化通信配置管理器 + + Args: + plugin: 实时通信插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 配置文件路径 + self.config_file_path = "/home/hello/EG/plugins/user/realtime_communication/config/communication_config.json" + + # 默认配置 + self.default_config = { + "server": { + "host": "0.0.0.0", + "port": 8080, + "enable_ssl": False, + "ssl_cert_file": "", + "ssl_key_file": "", + "max_connections": 1000, + "connection_timeout": 30.0, + "ping_interval": 30.0 + }, + "client": { + "enable_authentication": True, + "auth_timeout": 30.0, + "max_clients": 1000, + "enable_heartbeat": True, + "heartbeat_interval": 30.0, + "enable_rate_limiting": True, + "max_messages_per_second": 100, + "enable_banning": True, + "ban_duration": 3600 + }, + "message": { + "enable_message_filtering": True, + "max_message_size": 65536, + "enable_message_queue": True, + "message_queue_size": 10000, + "enable_message_compression": True, + "compression_threshold": 1024 + }, + "room": { + "max_rooms": 1000, + "max_clients_per_room": 100, + "enable_room_persistence": False, + "room_timeout": 3600, + "enable_room_passwords": True + }, + "protocol": { + "supported_protocols": ["websocket", "json", "binary"], + "default_protocol": "json", + "enable_compression": True, + "compression_threshold": 1024, + "enable_encryption": False, + "max_message_size": 65536 + }, + "auth": { + "enable_authentication": True, + "auth_timeout": 30.0, + "enable_token_auth": True, + "enable_password_auth": True, + "token_lifetime": 86400, + "enable_session_management": True, + "session_timeout": 3600, + "max_login_attempts": 5, + "login_lockout_duration": 900 + }, + "serialization": { + "default_format": "json", + "supported_formats": ["json", "pickle", "binary"], + "enable_compression": True, + "compression_threshold": 1024, + "enable_caching": True, + "cache_size": 1000 + }, + "monitoring": { + "enable_monitoring": True, + "monitoring_interval": 5.0, + "log_level": "INFO", + "enable_file_logging": True, + "log_file_path": "/home/hello/EG/plugins/user/realtime_communication/logs/communication_monitor.log" + }, + "events": { + "enable_event_system": True, + "max_event_queue_size": 10000, + "enable_event_filtering": True + } + } + + # 当前配置 + self.current_config = self.default_config.copy() + + # 配置状态 + self.config_state = { + "last_loaded": 0.0, + "last_saved": 0.0, + "config_version": "1.0.0", + "is_dirty": False + } + + # 配置统计 + self.config_stats = { + "loads": 0, + "saves": 0, + "updates": 0, + "config_errors": 0 + } + + # 配置监听器 + self.config_listeners = {} + + # 回调函数 + self.config_callbacks = { + "config_loaded": [], + "config_saved": [], + "config_updated": [], + "config_error": [] + } + + print("✓ 通信配置管理器已创建") + + def initialize(self) -> bool: + """ + 初始化通信配置管理器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化通信配置管理器...") + + # 创建配置目录 + config_dir = os.path.dirname(self.config_file_path) + if not os.path.exists(config_dir): + os.makedirs(config_dir) + + # 创建日志目录 + if "monitoring" in self.default_config: + log_dir = os.path.dirname(self.default_config["monitoring"]["log_file_path"]) + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + # 加载配置 + self.load_config() + + self.initialized = True + print("✓ 通信配置管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 通信配置管理器初始化失败: {e}") + self.config_stats["config_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用通信配置管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 通信配置管理器未初始化") + return False + + self.enabled = True + print("✓ 通信配置管理器已启用") + return True + + except Exception as e: + print(f"✗ 通信配置管理器启用失败: {e}") + self.config_stats["config_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用通信配置管理器""" + try: + self.enabled = False + + # 保存配置(如果有更改) + if self.config_state["is_dirty"]: + self.save_config() + + print("✓ 通信配置管理器已禁用") + + except Exception as e: + print(f"✗ 通信配置管理器禁用失败: {e}") + self.config_stats["config_errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理通信配置管理器资源""" + try: + # 禁用通信配置管理器 + if self.enabled: + self.disable() + + # 清理回调 + self.config_callbacks.clear() + self.config_listeners.clear() + + self.initialized = False + print("✓ 通信配置管理器资源已清理") + + except Exception as e: + print(f"✗ 通信配置管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新通信配置管理器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + # 检查配置文件是否被外部修改 + self._check_config_file_changes() + + except Exception as e: + print(f"✗ 通信配置管理器更新失败: {e}") + self.config_stats["config_errors"] += 1 + import traceback + traceback.print_exc() + + def _check_config_file_changes(self): + """检查配置文件是否被外部修改""" + try: + if not os.path.exists(self.config_file_path): + return + + last_modified = os.path.getmtime(self.config_file_path) + if last_modified > self.config_state["last_loaded"]: + # 配置文件已被修改,重新加载 + print("检测到配置文件被外部修改,正在重新加载...") + self.load_config() + + except Exception as e: + print(f"✗ 配置文件变更检查失败: {e}") + self.config_stats["config_errors"] += 1 + + def load_config(self) -> bool: + """ + 加载配置 + + Returns: + 是否加载成功 + """ + try: + # 检查配置文件是否存在 + if os.path.exists(self.config_file_path): + with open(self.config_file_path, 'r', encoding='utf-8') as f: + loaded_config = json.load(f) + + # 合并配置(使用默认配置作为基础) + self.current_config = self._merge_config(self.default_config, loaded_config) + + print(f"✓ 配置已从文件加载: {self.config_file_path}") + else: + # 使用默认配置 + self.current_config = self.default_config.copy() + print("✓ 使用默认配置") + + self.config_state["last_loaded"] = time.time() + self.config_state["is_dirty"] = False + self.config_stats["loads"] += 1 + + # 触发配置加载回调 + self._trigger_config_callback("config_loaded", { + "config": self.current_config, + "timestamp": self.config_state["last_loaded"] + }) + + return True + + except Exception as e: + print(f"✗ 配置加载失败: {e}") + self.config_stats["config_errors"] += 1 + + # 使用默认配置 + self.current_config = self.default_config.copy() + return False + + def _merge_config(self, base_config: Dict[str, Any], override_config: Dict[str, Any]) -> Dict[str, Any]: + """ + 合并配置(递归) + + Args: + base_config: 基础配置 + override_config: 覆盖配置 + + Returns: + 合并后的配置 + """ + try: + merged = base_config.copy() + + for key, value in override_config.items(): + if key in merged and isinstance(merged[key], dict) and isinstance(value, dict): + # 递归合并嵌套字典 + merged[key] = self._merge_config(merged[key], value) + else: + # 直接覆盖 + merged[key] = value + + return merged + + except Exception as e: + print(f"✗ 配置合并失败: {e}") + self.config_stats["config_errors"] += 1 + return base_config + + def save_config(self) -> bool: + """ + 保存配置 + + Returns: + 是否保存成功 + """ + try: + # 创建配置目录(如果不存在) + config_dir = os.path.dirname(self.config_file_path) + if not os.path.exists(config_dir): + os.makedirs(config_dir) + + # 保存配置到文件 + with open(self.config_file_path, 'w', encoding='utf-8') as f: + json.dump(self.current_config, f, indent=2, ensure_ascii=False) + + self.config_state["last_saved"] = time.time() + self.config_state["is_dirty"] = False + self.config_stats["saves"] += 1 + + # 触发配置保存回调 + self._trigger_config_callback("config_saved", { + "config": self.current_config, + "timestamp": self.config_state["last_saved"] + }) + + print(f"✓ 配置已保存到文件: {self.config_file_path}") + return True + + except Exception as e: + print(f"✗ 配置保存失败: {e}") + self.config_stats["config_errors"] += 1 + return False + + def get_config(self, section: str = None, key: str = None, default_value: Any = None) -> Any: + """ + 获取配置值 + + Args: + section: 配置节(可选) + key: 配置键(可选) + default_value: 默认值 + + Returns: + 配置值 + """ + try: + if section is None: + return self.current_config.copy() + + if section not in self.current_config: + return default_value + + if key is None: + return self.current_config[section].copy() + + if key not in self.current_config[section]: + return default_value + + return self.current_config[section][key] + + except Exception as e: + print(f"✗ 获取配置失败: {e}") + self.config_stats["config_errors"] += 1 + return default_value + + def set_config(self, section: str, key: str, value: Any) -> bool: + """ + 设置配置值 + + Args: + section: 配置节 + key: 配置键 + value: 配置值 + + Returns: + 是否设置成功 + """ + try: + if section not in self.current_config: + self.current_config[section] = {} + + self.current_config[section][key] = value + self.config_state["is_dirty"] = True + self.config_stats["updates"] += 1 + + # 通知配置监听器 + self._notify_config_listeners(section, key, value) + + # 触发配置更新回调 + self._trigger_config_callback("config_updated", { + "section": section, + "key": key, + "value": value, + "timestamp": time.time() + }) + + return True + + except Exception as e: + print(f"✗ 设置配置失败: {e}") + self.config_stats["config_errors"] += 1 + return False + + def update_config_section(self, section: str, config_data: Dict[str, Any]) -> bool: + """ + 更新配置节 + + Args: + section: 配置节 + config_data: 配置数据 + + Returns: + 是否更新成功 + """ + try: + if section not in self.current_config: + self.current_config[section] = {} + + self.current_config[section].update(config_data) + self.config_state["is_dirty"] = True + self.config_stats["updates"] += 1 + + # 通知配置监听器 + for key, value in config_data.items(): + self._notify_config_listeners(section, key, value) + + # 触发配置更新回调 + self._trigger_config_callback("config_updated", { + "section": section, + "config_data": config_data, + "timestamp": time.time() + }) + + return True + + except Exception as e: + print(f"✗ 更新配置节失败: {e}") + self.config_stats["config_errors"] += 1 + return False + + def reset_config(self) -> bool: + """ + 重置配置为默认值 + + Returns: + 是否重置成功 + """ + try: + self.current_config = self.default_config.copy() + self.config_state["is_dirty"] = True + + # 通知所有配置监听器 + for section_name, section_data in self.current_config.items(): + if isinstance(section_data, dict): + for key, value in section_data.items(): + self._notify_config_listeners(section_name, key, value) + + print("✓ 配置已重置为默认值") + return True + + except Exception as e: + print(f"✗ 配置重置失败: {e}") + self.config_stats["config_errors"] += 1 + return False + + def register_config_listener(self, section: str, key: str, listener: callable): + """ + 注册配置监听器 + + Args: + section: 配置节 + key: 配置键 + listener: 监听器函数 + """ + try: + listener_key = f"{section}.{key}" + if listener_key not in self.config_listeners: + self.config_listeners[listener_key] = [] + + self.config_listeners[listener_key].append(listener) + print(f"✓ 配置监听器已注册: {listener_key}") + + except Exception as e: + print(f"✗ 配置监听器注册失败: {e}") + self.config_stats["config_errors"] += 1 + + def unregister_config_listener(self, section: str, key: str, listener: callable): + """ + 注销配置监听器 + + Args: + section: 配置节 + key: 配置键 + listener: 监听器函数 + """ + try: + listener_key = f"{section}.{key}" + if listener_key in self.config_listeners: + if listener in self.config_listeners[listener_key]: + self.config_listeners[listener_key].remove(listener) + print(f"✓ 配置监听器已注销: {listener_key}") + + except Exception as e: + print(f"✗ 配置监听器注销失败: {e}") + self.config_stats["config_errors"] += 1 + + def _notify_config_listeners(self, section: str, key: str, value: Any): + """ + 通知配置监听器 + + Args: + section: 配置节 + key: 配置键 + value: 配置值 + """ + try: + listener_key = f"{section}.{key}" + if listener_key in self.config_listeners: + for listener in self.config_listeners[listener_key]: + try: + listener(section, key, value) + except Exception as e: + print(f"✗ 配置监听器执行失败: {listener_key} - {e}") + + except Exception as e: + print(f"✗ 配置监听器通知失败: {e}") + self.config_stats["config_errors"] += 1 + + def get_config_stats(self) -> Dict[str, Any]: + """ + 获取配置统计信息 + + Returns: + 配置统计字典 + """ + return { + "state": self.config_state.copy(), + "stats": self.config_stats.copy(), + "config_sections": list(self.current_config.keys()) + } + + def reset_stats(self): + """重置配置统计信息""" + try: + self.config_stats = { + "loads": 0, + "saves": 0, + "updates": 0, + "config_errors": 0 + } + print("✓ 配置统计信息已重置") + except Exception as e: + print(f"✗ 配置统计信息重置失败: {e}") + + def validate_config(self) -> bool: + """ + 验证配置 + + Returns: + 配置是否有效 + """ + try: + # 检查必需的配置项 + required_sections = ["server", "client", "message", "room", "protocol", "auth", "serialization"] + for section in required_sections: + if section not in self.current_config: + print(f"✗ 缺少必需的配置节: {section}") + return False + + # 检查服务器配置 + server_config = self.current_config["server"] + if "port" not in server_config or not isinstance(server_config["port"], int): + print("✗ 无效的服务器端口配置") + return False + + if "max_connections" not in server_config or not isinstance(server_config["max_connections"], int): + print("✗ 无效的最大连接数配置") + return False + + # 检查客户端配置 + client_config = self.current_config["client"] + if "max_clients" not in client_config or not isinstance(client_config["max_clients"], int): + print("✗ 无效的最大客户端数配置") + return False + + print("✓ 配置验证通过") + return True + + except Exception as e: + print(f"✗ 配置验证失败: {e}") + self.config_stats["config_errors"] += 1 + return False + + def export_config(self, file_path: str) -> bool: + """ + 导出配置到文件 + + Args: + file_path: 导出文件路径 + + Returns: + 是否导出成功 + """ + try: + # 创建导出目录 + export_dir = os.path.dirname(file_path) + if not os.path.exists(export_dir): + os.makedirs(export_dir) + + # 导出配置 + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(self.current_config, f, indent=2, ensure_ascii=False) + + print(f"✓ 配置已导出到: {file_path}") + return True + + except Exception as e: + print(f"✗ 配置导出失败: {e}") + self.config_stats["config_errors"] += 1 + return False + + def import_config(self, file_path: str) -> bool: + """ + 从文件导入配置 + + Args: + file_path: 导入文件路径 + + Returns: + 是否导入成功 + """ + try: + if not os.path.exists(file_path): + print(f"✗ 配置文件不存在: {file_path}") + return False + + # 读取配置文件 + with open(file_path, 'r', encoding='utf-8') as f: + imported_config = json.load(f) + + # 验证配置 + if not isinstance(imported_config, dict): + print("✗ 无效的配置文件格式") + return False + + # 合并配置 + self.current_config = self._merge_config(self.default_config, imported_config) + self.config_state["is_dirty"] = True + + # 通知配置监听器 + for section_name, section_data in self.current_config.items(): + if isinstance(section_data, dict): + for key, value in section_data.items(): + self._notify_config_listeners(section_name, key, value) + + print(f"✓ 配置已从文件导入: {file_path}") + return True + + except Exception as e: + print(f"✗ 配置导入失败: {e}") + self.config_stats["config_errors"] += 1 + return False + + def _trigger_config_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发配置回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.config_callbacks: + for callback in self.config_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 配置回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 配置回调触发失败: {e}") + + def register_config_callback(self, callback_type: str, callback: callable): + """ + 注册配置回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.config_callbacks: + self.config_callbacks[callback_type].append(callback) + print(f"✓ 配置回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 配置回调注册失败: {e}") + + def unregister_config_callback(self, callback_type: str, callback: callable): + """ + 注销配置回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.config_callbacks: + if callback in self.config_callbacks[callback_type]: + self.config_callbacks[callback_type].remove(callback) + print(f"✓ 配置回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 配置回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/realtime_communication/core/websocket_server.py b/plugins/user/realtime_communication/core/websocket_server.py index 3843d058..6bdab139 100644 --- a/plugins/user/realtime_communication/core/websocket_server.py +++ b/plugins/user/realtime_communication/core/websocket_server.py @@ -1,698 +1,698 @@ -""" -WebSocket服务器模块 -处理实时双向通信连接 -""" - -import time -import threading -import json -import base64 -from typing import Dict, Any, List, Optional - -class WebSocketServer: - """ - WebSocket服务器 - 处理实时双向通信连接 - """ - - def __init__(self, plugin): - """ - 初始化WebSocket服务器 - - Args: - plugin: 实时通信插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # WebSocket配置 - self.websocket_config = { - "host": "0.0.0.0", - "port": 8080, - "enable_ssl": False, - "ssl_cert_file": "", - "ssl_key_file": "", - "max_connections": 1000, - "enable_compression": True, - "ping_interval": 30.0, - "ping_timeout": 10.0, - "message_queue_size": 1000, - "enable_cors": True - } - - # WebSocket服务器状态 - self.server_state = { - "is_running": False, - "start_time": 0.0, - "uptime": 0.0, - "current_connections": 0, - "total_connections": 0, - "bytes_sent": 0, - "bytes_received": 0 - } - - # WebSocket服务器统计 - self.server_stats = { - "messages_sent": 0, - "messages_received": 0, - "connections_accepted": 0, - "connections_dropped": 0, - "errors": 0 - } - - # 连接存储 - self.connections = {} - self.connection_lock = threading.RLock() - - # 消息队列 - self.message_queue = [] - self.message_queue_lock = threading.RLock() - - # 服务器线程 - self.server_thread = None - self.server_thread_running = False - - # 回调函数 - self.websocket_callbacks = { - "server_started": [], - "server_stopped": [], - "client_connected": [], - "client_disconnected": [], - "message_received": [], - "websocket_error": [] - } - - # 时间戳记录 - self.last_ping_time = 0.0 - self.last_message_time = 0.0 - - print("✓ WebSocket服务器已创建") - - def initialize(self) -> bool: - """ - 初始化WebSocket服务器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化WebSocket服务器...") - - self.initialized = True - print("✓ WebSocket服务器初始化完成") - return True - - except Exception as e: - print(f"✗ WebSocket服务器初始化失败: {e}") - self.server_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用WebSocket服务器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ WebSocket服务器未初始化") - return False - - # 启动WebSocket服务器 - self._start_websocket_server() - - self.enabled = True - print("✓ WebSocket服务器已启用") - return True - - except Exception as e: - print(f"✗ WebSocket服务器启用失败: {e}") - self.server_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用WebSocket服务器""" - try: - self.enabled = False - - # 停止WebSocket服务器 - self._stop_websocket_server() - - print("✓ WebSocket服务器已禁用") - - except Exception as e: - print(f"✗ WebSocket服务器禁用失败: {e}") - self.server_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理WebSocket服务器资源""" - try: - # 禁用WebSocket服务器 - if self.enabled: - self.disable() - - # 清理回调 - self.websocket_callbacks.clear() - - self.initialized = False - print("✓ WebSocket服务器资源已清理") - - except Exception as e: - print(f"✗ WebSocket服务器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新WebSocket服务器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 更新运行时间 - if self.server_state["is_running"]: - self.server_state["uptime"] = current_time - self.server_state["start_time"] - - # 处理消息队列 - self._process_message_queue() - - # 发送心跳 - if current_time - self.last_ping_time > self.websocket_config["ping_interval"]: - self._send_ping_to_all_clients() - self.last_ping_time = current_time - - self.last_message_time = current_time - - except Exception as e: - print(f"✗ WebSocket服务器更新失败: {e}") - self.server_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def _start_websocket_server(self): - """启动WebSocket服务器""" - try: - if self.server_state["is_running"]: - return - - # 在实际实现中,这里会启动一个WebSocket服务器 - # 由于这是一个示例,我们模拟服务器启动过程 - self.server_state["is_running"] = True - self.server_state["start_time"] = time.time() - - # 触发服务器启动回调 - self._trigger_websocket_callback("server_started", { - "host": self.websocket_config["host"], - "port": self.websocket_config["port"], - "timestamp": self.server_state["start_time"] - }) - - print(f"✓ WebSocket服务器已启动: ws://{self.websocket_config['host']}:{self.websocket_config['port']}") - - except Exception as e: - print(f"✗ WebSocket服务器启动失败: {e}") - self.server_stats["errors"] += 1 - - def _stop_websocket_server(self): - """停止WebSocket服务器""" - try: - if not self.server_state["is_running"]: - return - - # 断开所有客户端连接 - self._disconnect_all_clients() - - self.server_state["is_running"] = False - self.server_state["start_time"] = 0.0 - self.server_state["uptime"] = 0.0 - - # 触发服务器停止回调 - self._trigger_websocket_callback("server_stopped", { - "timestamp": time.time() - }) - - print("✓ WebSocket服务器已停止") - - except Exception as e: - print(f"✗ WebSocket服务器停止失败: {e}") - self.server_stats["errors"] += 1 - - def _process_message_queue(self): - """处理消息队列""" - try: - if not self.message_queue: - return - - with self.message_queue_lock: - messages_to_process = self.message_queue[:] - self.message_queue.clear() - - for message_data in messages_to_process: - try: - client_id = message_data.get("client_id") - message = message_data.get("message") - - # 处理消息 - self._handle_incoming_message(client_id, message) - - except Exception as e: - print(f"✗ 消息处理失败: {e}") - self.server_stats["errors"] += 1 - - except Exception as e: - print(f"✗ 消息队列处理失败: {e}") - self.server_stats["errors"] += 1 - - def _handle_incoming_message(self, client_id: str, message: Dict[str, Any]): - """ - 处理传入消息 - - Args: - client_id: 客户端ID - message: 消息数据 - """ - try: - self.server_stats["messages_received"] += 1 - - # 触发消息接收回调 - self._trigger_websocket_callback("message_received", { - "client_id": client_id, - "message": message, - "timestamp": time.time() - }) - - # 将消息传递给消息路由器处理 - if self.plugin.message_router: - self.plugin.message_router.route_message(client_id, message) - - except Exception as e: - print(f"✗ 传入消息处理失败: {e}") - self.server_stats["errors"] += 1 - - def _send_ping_to_all_clients(self): - """向所有客户端发送心跳""" - try: - ping_message = { - "type": "ping", - "timestamp": time.time() - } - - with self.connection_lock: - for client_id, connection in self.connections.items(): - try: - self._send_message_to_client(client_id, ping_message) - except Exception as e: - print(f"✗ 心跳发送失败: {client_id} - {e}") - - except Exception as e: - print(f"✗ 心跳发送失败: {e}") - self.server_stats["errors"] += 1 - - def _disconnect_all_clients(self): - """断开所有客户端连接""" - try: - with self.connection_lock: - client_ids = list(self.connections.keys()) - - for client_id in client_ids: - self.disconnect_client(client_id, "server_shutdown") - - except Exception as e: - print(f"✗ 断开所有客户端连接失败: {e}") - self.server_stats["errors"] += 1 - - def send_message_to_client(self, client_id: str, message: Dict[str, Any]) -> bool: - """ - 向客户端发送消息 - - Args: - client_id: 客户端ID - message: 消息数据 - - Returns: - 是否发送成功 - """ - try: - if not self.enabled: - return False - - return self._send_message_to_client(client_id, message) - - except Exception as e: - print(f"✗ 向客户端发送消息失败: {e}") - self.server_stats["errors"] += 1 - return False - - def _send_message_to_client(self, client_id: str, message: Dict[str, Any]) -> bool: - """ - 向客户端发送消息(内部实现) - - Args: - client_id: 客户端ID - message: 消息数据 - - Returns: - 是否发送成功 - """ - try: - # 检查客户端连接是否存在 - with self.connection_lock: - if client_id not in self.connections: - return False - - # 序列化消息 - if self.plugin.data_serializer: - serialized_message = self.plugin.data_serializer.serialize(message) - else: - serialized_message = json.dumps(message, ensure_ascii=False) - - # 发送消息(模拟) - message_size = len(serialized_message.encode('utf-8')) - self.server_state["bytes_sent"] += message_size - self.server_stats["messages_sent"] += 1 - - return True - - except Exception as e: - print(f"✗ 向客户端发送消息失败: {e}") - self.server_stats["errors"] += 1 - return False - - def broadcast_message(self, message: Dict[str, Any], exclude_client_id: str = None) -> bool: - """ - 广播消息到所有客户端 - - Args: - message: 消息数据 - exclude_client_id: 要排除的客户端ID - - Returns: - 是否广播成功 - """ - try: - if not self.enabled: - return False - - success_count = 0 - with self.connection_lock: - for client_id in self.connections.keys(): - # 跳过排除的客户端 - if client_id == exclude_client_id: - continue - - if self._send_message_to_client(client_id, message): - success_count += 1 - - return success_count > 0 - - except Exception as e: - print(f"✗ 消息广播失败: {e}") - self.server_stats["errors"] += 1 - return False - - def add_client_connection(self, client_id: str, connection_data: Dict[str, Any]) -> bool: - """ - 添加客户端连接 - - Args: - client_id: 客户端ID - connection_data: 连接数据 - - Returns: - 是否添加成功 - """ - try: - if not self.enabled: - return False - - with self.connection_lock: - # 检查连接数限制 - if len(self.connections) >= self.websocket_config["max_connections"]: - print("✗ 连接数已达上限") - return False - - # 添加连接 - self.connections[client_id] = { - "connection_data": connection_data, - "connect_time": time.time(), - "last_activity": time.time(), - "bytes_sent": 0, - "bytes_received": 0, - "messages_sent": 0, - "messages_received": 0 - } - - self.server_state["current_connections"] = len(self.connections) - self.server_state["total_connections"] += 1 - self.server_stats["connections_accepted"] += 1 - - # 触发客户端连接回调 - self._trigger_websocket_callback("client_connected", { - "client_id": client_id, - "connection_data": connection_data, - "timestamp": time.time() - }) - - print(f"✓ 客户端已连接: {client_id}") - return True - - except Exception as e: - print(f"✗ 添加客户端连接失败: {e}") - self.server_stats["errors"] += 1 - return False - - def remove_client_connection(self, client_id: str, reason: str = "unknown") -> bool: - """ - 移除客户端连接 - - Args: - client_id: 客户端ID - reason: 断开原因 - - Returns: - 是否移除成功 - """ - try: - with self.connection_lock: - if client_id in self.connections: - del self.connections[client_id] - - self.server_state["current_connections"] = len(self.connections) - self.server_stats["connections_dropped"] += 1 - - # 触发客户端断开回调 - self._trigger_websocket_callback("client_disconnected", { - "client_id": client_id, - "reason": reason, - "timestamp": time.time() - }) - - print(f"✓ 客户端已断开: {client_id} (原因: {reason})") - return True - - except Exception as e: - print(f"✗ 移除客户端连接失败: {e}") - self.server_stats["errors"] += 1 - return False - - def disconnect_client(self, client_id: str, reason: str = "kicked") -> bool: - """ - 断开客户端连接 - - Args: - client_id: 客户端ID - reason: 断开原因 - - Returns: - 是否断开成功 - """ - try: - # 通知客户端断开连接 - disconnect_message = { - "type": "disconnect", - "reason": reason, - "timestamp": time.time() - } - - self._send_message_to_client(client_id, disconnect_message) - - # 移除连接 - return self.remove_client_connection(client_id, reason) - - except Exception as e: - print(f"✗ 断开客户端连接失败: {e}") - self.server_stats["errors"] += 1 - return False - - def get_connection_info(self, client_id: str) -> Optional[Dict[str, Any]]: - """ - 获取连接信息 - - Args: - client_id: 客户端ID - - Returns: - 连接信息或None - """ - try: - with self.connection_lock: - if client_id in self.connections: - return self.connections[client_id].copy() - else: - return None - - except Exception as e: - print(f"✗ 获取连接信息失败: {e}") - self.server_stats["errors"] += 1 - return None - - def get_all_connections(self) -> Dict[str, Any]: - """ - 获取所有连接信息 - - Returns: - 所有连接信息字典 - """ - try: - with self.connection_lock: - return {k: v.copy() for k, v in self.connections.items()} - except Exception as e: - print(f"✗ 获取所有连接信息失败: {e}") - self.server_stats["errors"] += 1 - return {} - - def get_server_stats(self) -> Dict[str, Any]: - """ - 获取服务器统计信息 - - Returns: - 服务器统计字典 - """ - return { - "state": self.server_state.copy(), - "stats": self.server_stats.copy(), - "config": self.websocket_config.copy() - } - - def reset_stats(self): - """重置服务器统计信息""" - try: - self.server_stats = { - "messages_sent": 0, - "messages_received": 0, - "connections_accepted": 0, - "connections_dropped": 0, - "errors": 0 - } - - self.server_state["bytes_sent"] = 0 - self.server_state["bytes_received"] = 0 - - print("✓ 服务器统计信息已重置") - except Exception as e: - print(f"✗ 服务器统计信息重置失败: {e}") - - def set_websocket_config(self, config: Dict[str, Any]) -> bool: - """ - 设置WebSocket配置 - - Args: - config: WebSocket配置字典 - - Returns: - 是否设置成功 - """ - try: - old_port = self.websocket_config.get("port") - new_port = config.get("port", old_port) - - # 如果服务器正在运行且端口发生变化,重启服务器 - if (self.server_state["is_running"] and - old_port != new_port): - self._stop_websocket_server() - self.websocket_config.update(config) - self._start_websocket_server() - else: - self.websocket_config.update(config) - - print(f"✓ WebSocket配置已更新: {self.websocket_config}") - return True - except Exception as e: - print(f"✗ WebSocket配置设置失败: {e}") - return False - - def get_websocket_config(self) -> Dict[str, Any]: - """ - 获取WebSocket配置 - - Returns: - WebSocket配置字典 - """ - return self.websocket_config.copy() - - def _trigger_websocket_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发WebSocket回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.websocket_callbacks: - for callback in self.websocket_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ WebSocket回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ WebSocket回调触发失败: {e}") - - def register_websocket_callback(self, callback_type: str, callback: callable): - """ - 注册WebSocket回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.websocket_callbacks: - self.websocket_callbacks[callback_type].append(callback) - print(f"✓ WebSocket回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ WebSocket回调注册失败: {e}") - - def unregister_websocket_callback(self, callback_type: str, callback: callable): - """ - 注销WebSocket回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.websocket_callbacks: - if callback in self.websocket_callbacks[callback_type]: - self.websocket_callbacks[callback_type].remove(callback) - print(f"✓ WebSocket回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +WebSocket服务器模块 +处理实时双向通信连接 +""" + +import time +import threading +import json +import base64 +from typing import Dict, Any, List, Optional + +class WebSocketServer: + """ + WebSocket服务器 + 处理实时双向通信连接 + """ + + def __init__(self, plugin): + """ + 初始化WebSocket服务器 + + Args: + plugin: 实时通信插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # WebSocket配置 + self.websocket_config = { + "host": "0.0.0.0", + "port": 8080, + "enable_ssl": False, + "ssl_cert_file": "", + "ssl_key_file": "", + "max_connections": 1000, + "enable_compression": True, + "ping_interval": 30.0, + "ping_timeout": 10.0, + "message_queue_size": 1000, + "enable_cors": True + } + + # WebSocket服务器状态 + self.server_state = { + "is_running": False, + "start_time": 0.0, + "uptime": 0.0, + "current_connections": 0, + "total_connections": 0, + "bytes_sent": 0, + "bytes_received": 0 + } + + # WebSocket服务器统计 + self.server_stats = { + "messages_sent": 0, + "messages_received": 0, + "connections_accepted": 0, + "connections_dropped": 0, + "errors": 0 + } + + # 连接存储 + self.connections = {} + self.connection_lock = threading.RLock() + + # 消息队列 + self.message_queue = [] + self.message_queue_lock = threading.RLock() + + # 服务器线程 + self.server_thread = None + self.server_thread_running = False + + # 回调函数 + self.websocket_callbacks = { + "server_started": [], + "server_stopped": [], + "client_connected": [], + "client_disconnected": [], + "message_received": [], + "websocket_error": [] + } + + # 时间戳记录 + self.last_ping_time = 0.0 + self.last_message_time = 0.0 + + print("✓ WebSocket服务器已创建") + + def initialize(self) -> bool: + """ + 初始化WebSocket服务器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化WebSocket服务器...") + + self.initialized = True + print("✓ WebSocket服务器初始化完成") + return True + + except Exception as e: + print(f"✗ WebSocket服务器初始化失败: {e}") + self.server_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用WebSocket服务器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ WebSocket服务器未初始化") + return False + + # 启动WebSocket服务器 + self._start_websocket_server() + + self.enabled = True + print("✓ WebSocket服务器已启用") + return True + + except Exception as e: + print(f"✗ WebSocket服务器启用失败: {e}") + self.server_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用WebSocket服务器""" + try: + self.enabled = False + + # 停止WebSocket服务器 + self._stop_websocket_server() + + print("✓ WebSocket服务器已禁用") + + except Exception as e: + print(f"✗ WebSocket服务器禁用失败: {e}") + self.server_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理WebSocket服务器资源""" + try: + # 禁用WebSocket服务器 + if self.enabled: + self.disable() + + # 清理回调 + self.websocket_callbacks.clear() + + self.initialized = False + print("✓ WebSocket服务器资源已清理") + + except Exception as e: + print(f"✗ WebSocket服务器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新WebSocket服务器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 更新运行时间 + if self.server_state["is_running"]: + self.server_state["uptime"] = current_time - self.server_state["start_time"] + + # 处理消息队列 + self._process_message_queue() + + # 发送心跳 + if current_time - self.last_ping_time > self.websocket_config["ping_interval"]: + self._send_ping_to_all_clients() + self.last_ping_time = current_time + + self.last_message_time = current_time + + except Exception as e: + print(f"✗ WebSocket服务器更新失败: {e}") + self.server_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def _start_websocket_server(self): + """启动WebSocket服务器""" + try: + if self.server_state["is_running"]: + return + + # 在实际实现中,这里会启动一个WebSocket服务器 + # 由于这是一个示例,我们模拟服务器启动过程 + self.server_state["is_running"] = True + self.server_state["start_time"] = time.time() + + # 触发服务器启动回调 + self._trigger_websocket_callback("server_started", { + "host": self.websocket_config["host"], + "port": self.websocket_config["port"], + "timestamp": self.server_state["start_time"] + }) + + print(f"✓ WebSocket服务器已启动: ws://{self.websocket_config['host']}:{self.websocket_config['port']}") + + except Exception as e: + print(f"✗ WebSocket服务器启动失败: {e}") + self.server_stats["errors"] += 1 + + def _stop_websocket_server(self): + """停止WebSocket服务器""" + try: + if not self.server_state["is_running"]: + return + + # 断开所有客户端连接 + self._disconnect_all_clients() + + self.server_state["is_running"] = False + self.server_state["start_time"] = 0.0 + self.server_state["uptime"] = 0.0 + + # 触发服务器停止回调 + self._trigger_websocket_callback("server_stopped", { + "timestamp": time.time() + }) + + print("✓ WebSocket服务器已停止") + + except Exception as e: + print(f"✗ WebSocket服务器停止失败: {e}") + self.server_stats["errors"] += 1 + + def _process_message_queue(self): + """处理消息队列""" + try: + if not self.message_queue: + return + + with self.message_queue_lock: + messages_to_process = self.message_queue[:] + self.message_queue.clear() + + for message_data in messages_to_process: + try: + client_id = message_data.get("client_id") + message = message_data.get("message") + + # 处理消息 + self._handle_incoming_message(client_id, message) + + except Exception as e: + print(f"✗ 消息处理失败: {e}") + self.server_stats["errors"] += 1 + + except Exception as e: + print(f"✗ 消息队列处理失败: {e}") + self.server_stats["errors"] += 1 + + def _handle_incoming_message(self, client_id: str, message: Dict[str, Any]): + """ + 处理传入消息 + + Args: + client_id: 客户端ID + message: 消息数据 + """ + try: + self.server_stats["messages_received"] += 1 + + # 触发消息接收回调 + self._trigger_websocket_callback("message_received", { + "client_id": client_id, + "message": message, + "timestamp": time.time() + }) + + # 将消息传递给消息路由器处理 + if self.plugin.message_router: + self.plugin.message_router.route_message(client_id, message) + + except Exception as e: + print(f"✗ 传入消息处理失败: {e}") + self.server_stats["errors"] += 1 + + def _send_ping_to_all_clients(self): + """向所有客户端发送心跳""" + try: + ping_message = { + "type": "ping", + "timestamp": time.time() + } + + with self.connection_lock: + for client_id, connection in self.connections.items(): + try: + self._send_message_to_client(client_id, ping_message) + except Exception as e: + print(f"✗ 心跳发送失败: {client_id} - {e}") + + except Exception as e: + print(f"✗ 心跳发送失败: {e}") + self.server_stats["errors"] += 1 + + def _disconnect_all_clients(self): + """断开所有客户端连接""" + try: + with self.connection_lock: + client_ids = list(self.connections.keys()) + + for client_id in client_ids: + self.disconnect_client(client_id, "server_shutdown") + + except Exception as e: + print(f"✗ 断开所有客户端连接失败: {e}") + self.server_stats["errors"] += 1 + + def send_message_to_client(self, client_id: str, message: Dict[str, Any]) -> bool: + """ + 向客户端发送消息 + + Args: + client_id: 客户端ID + message: 消息数据 + + Returns: + 是否发送成功 + """ + try: + if not self.enabled: + return False + + return self._send_message_to_client(client_id, message) + + except Exception as e: + print(f"✗ 向客户端发送消息失败: {e}") + self.server_stats["errors"] += 1 + return False + + def _send_message_to_client(self, client_id: str, message: Dict[str, Any]) -> bool: + """ + 向客户端发送消息(内部实现) + + Args: + client_id: 客户端ID + message: 消息数据 + + Returns: + 是否发送成功 + """ + try: + # 检查客户端连接是否存在 + with self.connection_lock: + if client_id not in self.connections: + return False + + # 序列化消息 + if self.plugin.data_serializer: + serialized_message = self.plugin.data_serializer.serialize(message) + else: + serialized_message = json.dumps(message, ensure_ascii=False) + + # 发送消息(模拟) + message_size = len(serialized_message.encode('utf-8')) + self.server_state["bytes_sent"] += message_size + self.server_stats["messages_sent"] += 1 + + return True + + except Exception as e: + print(f"✗ 向客户端发送消息失败: {e}") + self.server_stats["errors"] += 1 + return False + + def broadcast_message(self, message: Dict[str, Any], exclude_client_id: str = None) -> bool: + """ + 广播消息到所有客户端 + + Args: + message: 消息数据 + exclude_client_id: 要排除的客户端ID + + Returns: + 是否广播成功 + """ + try: + if not self.enabled: + return False + + success_count = 0 + with self.connection_lock: + for client_id in self.connections.keys(): + # 跳过排除的客户端 + if client_id == exclude_client_id: + continue + + if self._send_message_to_client(client_id, message): + success_count += 1 + + return success_count > 0 + + except Exception as e: + print(f"✗ 消息广播失败: {e}") + self.server_stats["errors"] += 1 + return False + + def add_client_connection(self, client_id: str, connection_data: Dict[str, Any]) -> bool: + """ + 添加客户端连接 + + Args: + client_id: 客户端ID + connection_data: 连接数据 + + Returns: + 是否添加成功 + """ + try: + if not self.enabled: + return False + + with self.connection_lock: + # 检查连接数限制 + if len(self.connections) >= self.websocket_config["max_connections"]: + print("✗ 连接数已达上限") + return False + + # 添加连接 + self.connections[client_id] = { + "connection_data": connection_data, + "connect_time": time.time(), + "last_activity": time.time(), + "bytes_sent": 0, + "bytes_received": 0, + "messages_sent": 0, + "messages_received": 0 + } + + self.server_state["current_connections"] = len(self.connections) + self.server_state["total_connections"] += 1 + self.server_stats["connections_accepted"] += 1 + + # 触发客户端连接回调 + self._trigger_websocket_callback("client_connected", { + "client_id": client_id, + "connection_data": connection_data, + "timestamp": time.time() + }) + + print(f"✓ 客户端已连接: {client_id}") + return True + + except Exception as e: + print(f"✗ 添加客户端连接失败: {e}") + self.server_stats["errors"] += 1 + return False + + def remove_client_connection(self, client_id: str, reason: str = "unknown") -> bool: + """ + 移除客户端连接 + + Args: + client_id: 客户端ID + reason: 断开原因 + + Returns: + 是否移除成功 + """ + try: + with self.connection_lock: + if client_id in self.connections: + del self.connections[client_id] + + self.server_state["current_connections"] = len(self.connections) + self.server_stats["connections_dropped"] += 1 + + # 触发客户端断开回调 + self._trigger_websocket_callback("client_disconnected", { + "client_id": client_id, + "reason": reason, + "timestamp": time.time() + }) + + print(f"✓ 客户端已断开: {client_id} (原因: {reason})") + return True + + except Exception as e: + print(f"✗ 移除客户端连接失败: {e}") + self.server_stats["errors"] += 1 + return False + + def disconnect_client(self, client_id: str, reason: str = "kicked") -> bool: + """ + 断开客户端连接 + + Args: + client_id: 客户端ID + reason: 断开原因 + + Returns: + 是否断开成功 + """ + try: + # 通知客户端断开连接 + disconnect_message = { + "type": "disconnect", + "reason": reason, + "timestamp": time.time() + } + + self._send_message_to_client(client_id, disconnect_message) + + # 移除连接 + return self.remove_client_connection(client_id, reason) + + except Exception as e: + print(f"✗ 断开客户端连接失败: {e}") + self.server_stats["errors"] += 1 + return False + + def get_connection_info(self, client_id: str) -> Optional[Dict[str, Any]]: + """ + 获取连接信息 + + Args: + client_id: 客户端ID + + Returns: + 连接信息或None + """ + try: + with self.connection_lock: + if client_id in self.connections: + return self.connections[client_id].copy() + else: + return None + + except Exception as e: + print(f"✗ 获取连接信息失败: {e}") + self.server_stats["errors"] += 1 + return None + + def get_all_connections(self) -> Dict[str, Any]: + """ + 获取所有连接信息 + + Returns: + 所有连接信息字典 + """ + try: + with self.connection_lock: + return {k: v.copy() for k, v in self.connections.items()} + except Exception as e: + print(f"✗ 获取所有连接信息失败: {e}") + self.server_stats["errors"] += 1 + return {} + + def get_server_stats(self) -> Dict[str, Any]: + """ + 获取服务器统计信息 + + Returns: + 服务器统计字典 + """ + return { + "state": self.server_state.copy(), + "stats": self.server_stats.copy(), + "config": self.websocket_config.copy() + } + + def reset_stats(self): + """重置服务器统计信息""" + try: + self.server_stats = { + "messages_sent": 0, + "messages_received": 0, + "connections_accepted": 0, + "connections_dropped": 0, + "errors": 0 + } + + self.server_state["bytes_sent"] = 0 + self.server_state["bytes_received"] = 0 + + print("✓ 服务器统计信息已重置") + except Exception as e: + print(f"✗ 服务器统计信息重置失败: {e}") + + def set_websocket_config(self, config: Dict[str, Any]) -> bool: + """ + 设置WebSocket配置 + + Args: + config: WebSocket配置字典 + + Returns: + 是否设置成功 + """ + try: + old_port = self.websocket_config.get("port") + new_port = config.get("port", old_port) + + # 如果服务器正在运行且端口发生变化,重启服务器 + if (self.server_state["is_running"] and + old_port != new_port): + self._stop_websocket_server() + self.websocket_config.update(config) + self._start_websocket_server() + else: + self.websocket_config.update(config) + + print(f"✓ WebSocket配置已更新: {self.websocket_config}") + return True + except Exception as e: + print(f"✗ WebSocket配置设置失败: {e}") + return False + + def get_websocket_config(self) -> Dict[str, Any]: + """ + 获取WebSocket配置 + + Returns: + WebSocket配置字典 + """ + return self.websocket_config.copy() + + def _trigger_websocket_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发WebSocket回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.websocket_callbacks: + for callback in self.websocket_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ WebSocket回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ WebSocket回调触发失败: {e}") + + def register_websocket_callback(self, callback_type: str, callback: callable): + """ + 注册WebSocket回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.websocket_callbacks: + self.websocket_callbacks[callback_type].append(callback) + print(f"✓ WebSocket回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ WebSocket回调注册失败: {e}") + + def unregister_websocket_callback(self, callback_type: str, callback: callable): + """ + 注销WebSocket回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.websocket_callbacks: + if callback in self.websocket_callbacks[callback_type]: + self.websocket_callbacks[callback_type].remove(callback) + print(f"✓ WebSocket回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ WebSocket回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/realtime_communication/editor/comm_editor.py b/plugins/user/realtime_communication/editor/comm_editor.py index 6adf62fb..03b5bd4a 100644 --- a/plugins/user/realtime_communication/editor/comm_editor.py +++ b/plugins/user/realtime_communication/editor/comm_editor.py @@ -1,596 +1,596 @@ -""" -通信编辑器模块 -提供图形界面配置通信参数 -""" - -import time -from typing import Dict, Any, List, Optional - -class CommEditor: - """ - 通信编辑器 - 提供图形界面配置通信参数 - """ - - def __init__(self, plugin): - """ - 初始化通信编辑器 - - Args: - plugin: 实时通信插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 编辑器配置 - self.editor_config = { - "enable_editor": True, - "enable_realtime_monitoring": True, - "enable_performance_display": True, - "enable_config_editor": True, - "enable_statistics_display": True, - "refresh_interval": 1.0, - "max_data_points": 100 - } - - # 编辑器状态 - self.editor_state = { - "is_visible": False, - "last_update": 0.0, - "active_tab": "overview", - "selected_client": None, - "selected_room": None - } - - # UI组件 - self.ui_components = { - "main_window": None, - "tabs": {}, - "panels": {}, - "charts": {}, - "controls": {} - } - - # 编辑器统计 - self.editor_stats = { - "updates": 0, - "ui_refreshes": 0, - "data_points": 0, - "editor_errors": 0 - } - - # 实时数据缓存 - self.realtime_data = { - "performance_metrics": {}, - "client_data": {}, - "room_data": {}, - "message_stats": {}, - "network_stats": {} - } - - # 回调函数 - self.editor_callbacks = { - "ui_updated": [], - "data_refreshed": [], - "config_changed": [], - "editor_error": [] - } - - # 时间戳记录 - self.last_ui_update = 0.0 - self.last_data_refresh = 0.0 - - print("✓ 通信编辑器已创建") - - def initialize(self) -> bool: - """ - 初始化通信编辑器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化通信编辑器...") - - # 初始化UI组件(模拟) - self._initialize_ui_components() - - self.initialized = True - print("✓ 通信编辑器初始化完成") - return True - - except Exception as e: - print(f"✗ 通信编辑器初始化失败: {e}") - self.editor_stats["editor_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def _initialize_ui_components(self): - """初始化UI组件""" - try: - # 创建主窗口 - self.ui_components["main_window"] = { - "title": "实时通信插件编辑器", - "size": (1200, 800), - "position": (100, 100) - } - - # 创建标签页 - self.ui_components["tabs"] = { - "overview": {"name": "概览", "icon": "dashboard"}, - "clients": {"name": "客户端", "icon": "people"}, - "rooms": {"name": "房间", "icon": "meeting_room"}, - "messages": {"name": "消息", "icon": "message"}, - "network": {"name": "网络", "icon": "network"}, - "config": {"name": "配置", "icon": "settings"}, - "statistics": {"name": "统计", "icon": "bar_chart"} - } - - # 创建面板 - self.ui_components["panels"] = { - "server_status": {"title": "服务器状态", "visible": True}, - "performance": {"title": "性能指标", "visible": True}, - "clients_list": {"title": "客户端列表", "visible": True}, - "rooms_list": {"title": "房间列表", "visible": True}, - "message_stats": {"title": "消息统计", "visible": True} - } - - print("✓ UI组件已初始化") - - except Exception as e: - print(f"✗ UI组件初始化失败: {e}") - self.editor_stats["editor_errors"] += 1 - - def enable(self) -> bool: - """ - 启用通信编辑器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 通信编辑器未初始化") - return False - - self.enabled = True - print("✓ 通信编辑器已启用") - return True - - except Exception as e: - print(f"✗ 通信编辑器启用失败: {e}") - self.editor_stats["editor_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用通信编辑器""" - try: - self.enabled = False - - # 隐藏UI - self.hide_editor() - - print("✓ 通信编辑器已禁用") - - except Exception as e: - print(f"✗ 通信编辑器禁用失败: {e}") - self.editor_stats["editor_errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理通信编辑器资源""" - try: - # 禁用通信编辑器 - if self.enabled: - self.disable() - - # 清理回调 - self.editor_callbacks.clear() - - self.initialized = False - print("✓ 通信编辑器资源已清理") - - except Exception as e: - print(f"✗ 通信编辑器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新通信编辑器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 定期刷新UI - if current_time - self.last_ui_update >= self.editor_config["refresh_interval"]: - self._refresh_ui() - self.last_ui_update = current_time - - # 定期更新数据 - if current_time - self.last_data_refresh >= 0.5: # 每0.5秒更新一次数据 - self._update_realtime_data() - self.last_data_refresh = current_time - - self.editor_state["last_update"] = current_time - - except Exception as e: - print(f"✗ 通信编辑器更新失败: {e}") - self.editor_stats["editor_errors"] += 1 - import traceback - traceback.print_exc() - - def _refresh_ui(self): - """刷新UI""" - try: - # 更新UI组件状态 - self._update_ui_components() - - # 更新统计信息 - self.editor_stats["ui_refreshes"] += 1 - - # 触发UI更新回调 - self._trigger_editor_callback("ui_updated", { - "timestamp": time.time() - }) - - except Exception as e: - print(f"✗ UI刷新失败: {e}") - self.editor_stats["editor_errors"] += 1 - - def _update_ui_components(self): - """更新UI组件""" - try: - # 更新服务器状态面板 - if self.plugin.websocket_server: - server_stats = self.plugin.websocket_server.get_server_stats() - self.ui_components["panels"]["server_status"]["data"] = server_stats - - # 更新客户端列表面板 - if self.plugin.client_manager: - clients = self.plugin.client_manager.get_all_clients() - self.ui_components["panels"]["clients_list"]["data"] = clients - - # 更新房间列表面板 - if self.plugin.room_manager: - rooms = self.plugin.room_manager.get_all_rooms() - self.ui_components["panels"]["rooms_list"]["data"] = rooms - - # 更新消息统计面板 - if self.plugin.message_router: - message_stats = self.plugin.message_router.get_message_stats() - self.ui_components["panels"]["message_stats"]["data"] = message_stats - - except Exception as e: - print(f"✗ UI组件更新失败: {e}") - self.editor_stats["editor_errors"] += 1 - - def _update_realtime_data(self): - """更新实时数据""" - try: - current_time = time.time() - - # 更新性能指标 - if self.plugin.monitor: - metrics = self.plugin.monitor.get_performance_metrics() - self.realtime_data["performance_metrics"] = metrics - - # 更新客户端数据 - if self.plugin.client_manager: - clients = self.plugin.client_manager.get_all_clients() - self.realtime_data["client_data"] = clients - - # 更新房间数据 - if self.plugin.room_manager: - rooms = self.plugin.room_manager.get_all_rooms() - self.realtime_data["room_data"] = rooms - - # 更新消息统计 - if self.plugin.message_router: - message_stats = self.plugin.message_router.get_message_stats() - self.realtime_data["message_stats"] = message_stats - - # 更新网络统计 - if self.plugin.websocket_server: - network_stats = self.plugin.websocket_server.get_server_stats() - self.realtime_data["network_stats"] = network_stats - - # 触发数据刷新回调 - self._trigger_editor_callback("data_refreshed", { - "data": self.realtime_data, - "timestamp": current_time - }) - - except Exception as e: - print(f"✗ 实时数据更新失败: {e}") - self.editor_stats["editor_errors"] += 1 - - def show_editor(self): - """显示编辑器""" - try: - self.editor_state["is_visible"] = True - print("✓ 通信编辑器已显示") - - # 刷新数据 - self._update_realtime_data() - - except Exception as e: - print(f"✗ 编辑器显示失败: {e}") - self.editor_stats["editor_errors"] += 1 - - def hide_editor(self): - """隐藏编辑器""" - try: - self.editor_state["is_visible"] = False - print("✓ 通信编辑器已隐藏") - - except Exception as e: - print(f"✗ 编辑器隐藏失败: {e}") - self.editor_stats["editor_errors"] += 1 - - def switch_tab(self, tab_name: str): - """ - 切换标签页 - - Args: - tab_name: 标签页名称 - """ - try: - if tab_name in self.ui_components["tabs"]: - self.editor_state["active_tab"] = tab_name - print(f"✓ 切换到标签页: {tab_name}") - else: - print(f"✗ 无效的标签页: {tab_name}") - - except Exception as e: - print(f"✗ 标签页切换失败: {e}") - self.editor_stats["editor_errors"] += 1 - - def select_client(self, client_id: str): - """ - 选择客户端 - - Args: - client_id: 客户端ID - """ - try: - self.editor_state["selected_client"] = client_id - print(f"✓ 选择客户端: {client_id}") - - except Exception as e: - print(f"✗ 客户端选择失败: {e}") - self.editor_stats["editor_errors"] += 1 - - def select_room(self, room_id: str): - """ - 选择房间 - - Args: - room_id: 房间ID - """ - try: - self.editor_state["selected_room"] = room_id - print(f"✓ 选择房间: {room_id}") - - except Exception as e: - print(f"✗ 房间选择失败: {e}") - self.editor_stats["editor_errors"] += 1 - - def update_config(self, section: str, key: str, value: Any) -> bool: - """ - 更新配置 - - Args: - section: 配置节 - key: 配置键 - value: 配置值 - - Returns: - 是否更新成功 - """ - try: - # 通过配置管理器更新配置 - if self.plugin.config_manager: - result = self.plugin.config_manager.set_config(section, key, value) - - if result: - self.editor_stats["updates"] += 1 - - # 触发配置更改回调 - self._trigger_editor_callback("config_changed", { - "section": section, - "key": key, - "value": value, - "timestamp": time.time() - }) - - print(f"✓ 配置已更新: {section}.{key} = {value}") - return True - else: - print(f"✗ 配置更新失败: {section}.{key}") - return False - else: - print("✗ 配置管理器不可用") - return False - - except Exception as e: - print(f"✗ 配置更新失败: {e}") - self.editor_stats["editor_errors"] += 1 - return False - - def get_editor_stats(self) -> Dict[str, Any]: - """ - 获取编辑器统计信息 - - Returns: - 编辑器统计字典 - """ - return { - "state": self.editor_state.copy(), - "stats": self.editor_stats.copy(), - "config": self.editor_config.copy(), - "ui_components": len(self.ui_components["tabs"]) - } - - def reset_stats(self): - """重置编辑器统计信息""" - try: - self.editor_stats = { - "updates": 0, - "ui_refreshes": 0, - "data_points": 0, - "editor_errors": 0 - } - print("✓ 编辑器统计信息已重置") - except Exception as e: - print(f"✗ 编辑器统计信息重置失败: {e}") - - def set_editor_config(self, config: Dict[str, Any]) -> bool: - """ - 设置编辑器配置 - - Args: - config: 编辑器配置字典 - - Returns: - 是否设置成功 - """ - try: - self.editor_config.update(config) - print(f"✓ 编辑器配置已更新: {self.editor_config}") - return True - except Exception as e: - print(f"✗ 编辑器配置设置失败: {e}") - return False - - def get_editor_config(self) -> Dict[str, Any]: - """ - 获取编辑器配置 - - Returns: - 编辑器配置字典 - """ - return self.editor_config.copy() - - def get_realtime_data(self) -> Dict[str, Any]: - """ - 获取实时数据 - - Returns: - 实时数据字典 - """ - return self.realtime_data.copy() - - def export_data(self, data_type: str, file_path: str) -> bool: - """ - 导出数据 - - Args: - data_type: 数据类型 - file_path: 导出文件路径 - - Returns: - 是否导出成功 - """ - try: - import json - import os - - # 创建导出目录 - export_dir = os.path.dirname(file_path) - if not os.path.exists(export_dir): - os.makedirs(export_dir) - - # 获取要导出的数据 - export_data = None - if data_type == "performance": - export_data = self.realtime_data["performance_metrics"] - elif data_type == "clients": - export_data = self.realtime_data["client_data"] - elif data_type == "rooms": - export_data = self.realtime_data["room_data"] - elif data_type == "messages": - export_data = self.realtime_data["message_stats"] - elif data_type == "network": - export_data = self.realtime_data["network_stats"] - elif data_type == "all": - export_data = self.realtime_data - else: - print(f"✗ 不支持的数据类型: {data_type}") - return False - - # 导出数据 - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(export_data, f, indent=2, ensure_ascii=False, default=str) - - print(f"✓ 数据已导出到: {file_path}") - return True - - except Exception as e: - print(f"✗ 数据导出失败: {e}") - self.editor_stats["editor_errors"] += 1 - return False - - def _trigger_editor_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发编辑器回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.editor_callbacks: - for callback in self.editor_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 编辑器回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 编辑器回调触发失败: {e}") - - def register_editor_callback(self, callback_type: str, callback: callable): - """ - 注册编辑器回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.editor_callbacks: - self.editor_callbacks[callback_type].append(callback) - print(f"✓ 编辑器回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 编辑器回调注册失败: {e}") - - def unregister_editor_callback(self, callback_type: str, callback: callable): - """ - 注销编辑器回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.editor_callbacks: - if callback in self.editor_callbacks[callback_type]: - self.editor_callbacks[callback_type].remove(callback) - print(f"✓ 编辑器回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +通信编辑器模块 +提供图形界面配置通信参数 +""" + +import time +from typing import Dict, Any, List, Optional + +class CommEditor: + """ + 通信编辑器 + 提供图形界面配置通信参数 + """ + + def __init__(self, plugin): + """ + 初始化通信编辑器 + + Args: + plugin: 实时通信插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 编辑器配置 + self.editor_config = { + "enable_editor": True, + "enable_realtime_monitoring": True, + "enable_performance_display": True, + "enable_config_editor": True, + "enable_statistics_display": True, + "refresh_interval": 1.0, + "max_data_points": 100 + } + + # 编辑器状态 + self.editor_state = { + "is_visible": False, + "last_update": 0.0, + "active_tab": "overview", + "selected_client": None, + "selected_room": None + } + + # UI组件 + self.ui_components = { + "main_window": None, + "tabs": {}, + "panels": {}, + "charts": {}, + "controls": {} + } + + # 编辑器统计 + self.editor_stats = { + "updates": 0, + "ui_refreshes": 0, + "data_points": 0, + "editor_errors": 0 + } + + # 实时数据缓存 + self.realtime_data = { + "performance_metrics": {}, + "client_data": {}, + "room_data": {}, + "message_stats": {}, + "network_stats": {} + } + + # 回调函数 + self.editor_callbacks = { + "ui_updated": [], + "data_refreshed": [], + "config_changed": [], + "editor_error": [] + } + + # 时间戳记录 + self.last_ui_update = 0.0 + self.last_data_refresh = 0.0 + + print("✓ 通信编辑器已创建") + + def initialize(self) -> bool: + """ + 初始化通信编辑器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化通信编辑器...") + + # 初始化UI组件(模拟) + self._initialize_ui_components() + + self.initialized = True + print("✓ 通信编辑器初始化完成") + return True + + except Exception as e: + print(f"✗ 通信编辑器初始化失败: {e}") + self.editor_stats["editor_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def _initialize_ui_components(self): + """初始化UI组件""" + try: + # 创建主窗口 + self.ui_components["main_window"] = { + "title": "实时通信插件编辑器", + "size": (1200, 800), + "position": (100, 100) + } + + # 创建标签页 + self.ui_components["tabs"] = { + "overview": {"name": "概览", "icon": "dashboard"}, + "clients": {"name": "客户端", "icon": "people"}, + "rooms": {"name": "房间", "icon": "meeting_room"}, + "messages": {"name": "消息", "icon": "message"}, + "network": {"name": "网络", "icon": "network"}, + "config": {"name": "配置", "icon": "settings"}, + "statistics": {"name": "统计", "icon": "bar_chart"} + } + + # 创建面板 + self.ui_components["panels"] = { + "server_status": {"title": "服务器状态", "visible": True}, + "performance": {"title": "性能指标", "visible": True}, + "clients_list": {"title": "客户端列表", "visible": True}, + "rooms_list": {"title": "房间列表", "visible": True}, + "message_stats": {"title": "消息统计", "visible": True} + } + + print("✓ UI组件已初始化") + + except Exception as e: + print(f"✗ UI组件初始化失败: {e}") + self.editor_stats["editor_errors"] += 1 + + def enable(self) -> bool: + """ + 启用通信编辑器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 通信编辑器未初始化") + return False + + self.enabled = True + print("✓ 通信编辑器已启用") + return True + + except Exception as e: + print(f"✗ 通信编辑器启用失败: {e}") + self.editor_stats["editor_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用通信编辑器""" + try: + self.enabled = False + + # 隐藏UI + self.hide_editor() + + print("✓ 通信编辑器已禁用") + + except Exception as e: + print(f"✗ 通信编辑器禁用失败: {e}") + self.editor_stats["editor_errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理通信编辑器资源""" + try: + # 禁用通信编辑器 + if self.enabled: + self.disable() + + # 清理回调 + self.editor_callbacks.clear() + + self.initialized = False + print("✓ 通信编辑器资源已清理") + + except Exception as e: + print(f"✗ 通信编辑器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新通信编辑器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 定期刷新UI + if current_time - self.last_ui_update >= self.editor_config["refresh_interval"]: + self._refresh_ui() + self.last_ui_update = current_time + + # 定期更新数据 + if current_time - self.last_data_refresh >= 0.5: # 每0.5秒更新一次数据 + self._update_realtime_data() + self.last_data_refresh = current_time + + self.editor_state["last_update"] = current_time + + except Exception as e: + print(f"✗ 通信编辑器更新失败: {e}") + self.editor_stats["editor_errors"] += 1 + import traceback + traceback.print_exc() + + def _refresh_ui(self): + """刷新UI""" + try: + # 更新UI组件状态 + self._update_ui_components() + + # 更新统计信息 + self.editor_stats["ui_refreshes"] += 1 + + # 触发UI更新回调 + self._trigger_editor_callback("ui_updated", { + "timestamp": time.time() + }) + + except Exception as e: + print(f"✗ UI刷新失败: {e}") + self.editor_stats["editor_errors"] += 1 + + def _update_ui_components(self): + """更新UI组件""" + try: + # 更新服务器状态面板 + if self.plugin.websocket_server: + server_stats = self.plugin.websocket_server.get_server_stats() + self.ui_components["panels"]["server_status"]["data"] = server_stats + + # 更新客户端列表面板 + if self.plugin.client_manager: + clients = self.plugin.client_manager.get_all_clients() + self.ui_components["panels"]["clients_list"]["data"] = clients + + # 更新房间列表面板 + if self.plugin.room_manager: + rooms = self.plugin.room_manager.get_all_rooms() + self.ui_components["panels"]["rooms_list"]["data"] = rooms + + # 更新消息统计面板 + if self.plugin.message_router: + message_stats = self.plugin.message_router.get_message_stats() + self.ui_components["panels"]["message_stats"]["data"] = message_stats + + except Exception as e: + print(f"✗ UI组件更新失败: {e}") + self.editor_stats["editor_errors"] += 1 + + def _update_realtime_data(self): + """更新实时数据""" + try: + current_time = time.time() + + # 更新性能指标 + if self.plugin.monitor: + metrics = self.plugin.monitor.get_performance_metrics() + self.realtime_data["performance_metrics"] = metrics + + # 更新客户端数据 + if self.plugin.client_manager: + clients = self.plugin.client_manager.get_all_clients() + self.realtime_data["client_data"] = clients + + # 更新房间数据 + if self.plugin.room_manager: + rooms = self.plugin.room_manager.get_all_rooms() + self.realtime_data["room_data"] = rooms + + # 更新消息统计 + if self.plugin.message_router: + message_stats = self.plugin.message_router.get_message_stats() + self.realtime_data["message_stats"] = message_stats + + # 更新网络统计 + if self.plugin.websocket_server: + network_stats = self.plugin.websocket_server.get_server_stats() + self.realtime_data["network_stats"] = network_stats + + # 触发数据刷新回调 + self._trigger_editor_callback("data_refreshed", { + "data": self.realtime_data, + "timestamp": current_time + }) + + except Exception as e: + print(f"✗ 实时数据更新失败: {e}") + self.editor_stats["editor_errors"] += 1 + + def show_editor(self): + """显示编辑器""" + try: + self.editor_state["is_visible"] = True + print("✓ 通信编辑器已显示") + + # 刷新数据 + self._update_realtime_data() + + except Exception as e: + print(f"✗ 编辑器显示失败: {e}") + self.editor_stats["editor_errors"] += 1 + + def hide_editor(self): + """隐藏编辑器""" + try: + self.editor_state["is_visible"] = False + print("✓ 通信编辑器已隐藏") + + except Exception as e: + print(f"✗ 编辑器隐藏失败: {e}") + self.editor_stats["editor_errors"] += 1 + + def switch_tab(self, tab_name: str): + """ + 切换标签页 + + Args: + tab_name: 标签页名称 + """ + try: + if tab_name in self.ui_components["tabs"]: + self.editor_state["active_tab"] = tab_name + print(f"✓ 切换到标签页: {tab_name}") + else: + print(f"✗ 无效的标签页: {tab_name}") + + except Exception as e: + print(f"✗ 标签页切换失败: {e}") + self.editor_stats["editor_errors"] += 1 + + def select_client(self, client_id: str): + """ + 选择客户端 + + Args: + client_id: 客户端ID + """ + try: + self.editor_state["selected_client"] = client_id + print(f"✓ 选择客户端: {client_id}") + + except Exception as e: + print(f"✗ 客户端选择失败: {e}") + self.editor_stats["editor_errors"] += 1 + + def select_room(self, room_id: str): + """ + 选择房间 + + Args: + room_id: 房间ID + """ + try: + self.editor_state["selected_room"] = room_id + print(f"✓ 选择房间: {room_id}") + + except Exception as e: + print(f"✗ 房间选择失败: {e}") + self.editor_stats["editor_errors"] += 1 + + def update_config(self, section: str, key: str, value: Any) -> bool: + """ + 更新配置 + + Args: + section: 配置节 + key: 配置键 + value: 配置值 + + Returns: + 是否更新成功 + """ + try: + # 通过配置管理器更新配置 + if self.plugin.config_manager: + result = self.plugin.config_manager.set_config(section, key, value) + + if result: + self.editor_stats["updates"] += 1 + + # 触发配置更改回调 + self._trigger_editor_callback("config_changed", { + "section": section, + "key": key, + "value": value, + "timestamp": time.time() + }) + + print(f"✓ 配置已更新: {section}.{key} = {value}") + return True + else: + print(f"✗ 配置更新失败: {section}.{key}") + return False + else: + print("✗ 配置管理器不可用") + return False + + except Exception as e: + print(f"✗ 配置更新失败: {e}") + self.editor_stats["editor_errors"] += 1 + return False + + def get_editor_stats(self) -> Dict[str, Any]: + """ + 获取编辑器统计信息 + + Returns: + 编辑器统计字典 + """ + return { + "state": self.editor_state.copy(), + "stats": self.editor_stats.copy(), + "config": self.editor_config.copy(), + "ui_components": len(self.ui_components["tabs"]) + } + + def reset_stats(self): + """重置编辑器统计信息""" + try: + self.editor_stats = { + "updates": 0, + "ui_refreshes": 0, + "data_points": 0, + "editor_errors": 0 + } + print("✓ 编辑器统计信息已重置") + except Exception as e: + print(f"✗ 编辑器统计信息重置失败: {e}") + + def set_editor_config(self, config: Dict[str, Any]) -> bool: + """ + 设置编辑器配置 + + Args: + config: 编辑器配置字典 + + Returns: + 是否设置成功 + """ + try: + self.editor_config.update(config) + print(f"✓ 编辑器配置已更新: {self.editor_config}") + return True + except Exception as e: + print(f"✗ 编辑器配置设置失败: {e}") + return False + + def get_editor_config(self) -> Dict[str, Any]: + """ + 获取编辑器配置 + + Returns: + 编辑器配置字典 + """ + return self.editor_config.copy() + + def get_realtime_data(self) -> Dict[str, Any]: + """ + 获取实时数据 + + Returns: + 实时数据字典 + """ + return self.realtime_data.copy() + + def export_data(self, data_type: str, file_path: str) -> bool: + """ + 导出数据 + + Args: + data_type: 数据类型 + file_path: 导出文件路径 + + Returns: + 是否导出成功 + """ + try: + import json + import os + + # 创建导出目录 + export_dir = os.path.dirname(file_path) + if not os.path.exists(export_dir): + os.makedirs(export_dir) + + # 获取要导出的数据 + export_data = None + if data_type == "performance": + export_data = self.realtime_data["performance_metrics"] + elif data_type == "clients": + export_data = self.realtime_data["client_data"] + elif data_type == "rooms": + export_data = self.realtime_data["room_data"] + elif data_type == "messages": + export_data = self.realtime_data["message_stats"] + elif data_type == "network": + export_data = self.realtime_data["network_stats"] + elif data_type == "all": + export_data = self.realtime_data + else: + print(f"✗ 不支持的数据类型: {data_type}") + return False + + # 导出数据 + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(export_data, f, indent=2, ensure_ascii=False, default=str) + + print(f"✓ 数据已导出到: {file_path}") + return True + + except Exception as e: + print(f"✗ 数据导出失败: {e}") + self.editor_stats["editor_errors"] += 1 + return False + + def _trigger_editor_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发编辑器回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.editor_callbacks: + for callback in self.editor_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 编辑器回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 编辑器回调触发失败: {e}") + + def register_editor_callback(self, callback_type: str, callback: callable): + """ + 注册编辑器回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.editor_callbacks: + self.editor_callbacks[callback_type].append(callback) + print(f"✓ 编辑器回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 编辑器回调注册失败: {e}") + + def unregister_editor_callback(self, callback_type: str, callback: callable): + """ + 注销编辑器回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.editor_callbacks: + if callback in self.editor_callbacks[callback_type]: + self.editor_callbacks[callback_type].remove(callback) + print(f"✓ 编辑器回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 编辑器回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/realtime_communication/events/event_handler.py b/plugins/user/realtime_communication/events/event_handler.py index fbf2fe81..9d59c710 100644 --- a/plugins/user/realtime_communication/events/event_handler.py +++ b/plugins/user/realtime_communication/events/event_handler.py @@ -1,716 +1,716 @@ -""" -事件处理器模块 -处理通信事件和系统响应 -""" - -import time -import threading -import queue -from typing import Dict, Any, List, Optional, Callable - -class EventHandler: - """ - 事件处理器 - 处理通信事件和系统响应 - """ - - def __init__(self, plugin): - """ - 初始化事件处理器 - - Args: - plugin: 实时通信插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 事件配置 - self.event_config = { - "enable_event_system": True, - "max_event_queue_size": 10000, - "enable_event_filtering": True, - "event_processing_interval": 0.01, # 10ms - "enable_async_processing": True, - "max_concurrent_events": 100, - "enable_event_prioritization": True, - "enable_event_logging": True - } - - # 事件状态 - self.event_state = { - "is_processing": False, - "pending_events": 0, - "processed_events": 0, - "dropped_events": 0, - "last_event_time": 0.0 - } - - # 事件队列 - self.event_queue = queue.Queue(maxsize=self.event_config["max_event_queue_size"]) - self.event_queue_lock = threading.RLock() - - # 事件处理器存储 - self.event_handlers = {} - - # 事件统计 - self.event_stats = { - "events_received": 0, - "events_processed": 0, - "events_dropped": 0, - "handler_errors": 0, - "async_events": 0 - } - - # 事件过滤器 - self.event_filters = {} - - # 回调函数 - self.event_callbacks = { - "event_received": [], - "event_processed": [], - "event_dropped": [], - "event_error": [] - } - - # 事件处理线程 - self.event_thread = None - self.event_thread_running = False - - # 时间戳记录 - self.last_event_process = 0.0 - self.last_stats_reset = 0.0 - - print("✓ 事件处理器已创建") - - def initialize(self) -> bool: - """ - 初始化事件处理器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化事件处理器...") - - # 注册默认事件处理器 - self._register_default_handlers() - - # 启动事件处理线程 - self._start_event_thread() - - self.initialized = True - print("✓ 事件处理器初始化完成") - return True - - except Exception as e: - print(f"✗ 事件处理器初始化失败: {e}") - self.event_stats["handler_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def _register_default_handlers(self): - """注册默认事件处理器""" - try: - # 注册系统事件处理器 - self.register_event_handler("client_connected", self._handle_client_connected) - self.register_event_handler("client_disconnected", self._handle_client_disconnected) - self.register_event_handler("message_received", self._handle_message_received) - self.register_event_handler("room_created", self._handle_room_created) - self.register_event_handler("room_destroyed", self._handle_room_destroyed) - self.register_event_handler("client_joined_room", self._handle_client_joined_room) - self.register_event_handler("client_left_room", self._handle_client_left_room) - - print("✓ 默认事件处理器已注册") - - except Exception as e: - print(f"✗ 默认事件处理器注册失败: {e}") - self.event_stats["handler_errors"] += 1 - - def _start_event_thread(self): - """启动事件处理线程""" - try: - self.event_thread_running = True - self.event_thread = threading.Thread(target=self._event_loop, daemon=True) - self.event_thread.start() - print("✓ 事件处理线程已启动") - except Exception as e: - print(f"✗ 事件处理线程启动失败: {e}") - self.event_stats["handler_errors"] += 1 - - def _event_loop(self): - """事件处理循环""" - try: - while self.event_thread_running: - try: - if self.enabled and self.event_config["enable_event_system"]: - # 处理事件队列 - self._process_event_queue() - - # 短暂休眠 - time.sleep(self.event_config["event_processing_interval"]) - - except Exception as e: - print(f"✗ 事件处理循环错误: {e}") - self.event_stats["handler_errors"] += 1 - time.sleep(1.0) # 出错时延长休眠 - - except Exception as e: - print(f"✗ 事件处理线程失败: {e}") - self.event_stats["handler_errors"] += 1 - - def enable(self) -> bool: - """ - 启用事件处理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 事件处理器未初始化") - return False - - self.enabled = True - print("✓ 事件处理器已启用") - return True - - except Exception as e: - print(f"✗ 事件处理器启用失败: {e}") - self.event_stats["handler_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用事件处理器""" - try: - self.enabled = False - - # 停止事件处理线程 - if self.event_thread_running: - self.event_thread_running = False - if self.event_thread and self.event_thread.is_alive(): - self.event_thread.join(timeout=5.0) - - print("✓ 事件处理器已禁用") - - except Exception as e: - print(f"✗ 事件处理器禁用失败: {e}") - self.event_stats["handler_errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理事件处理器资源""" - try: - # 禁用事件处理器 - if self.enabled: - self.disable() - - # 清理回调和处理器 - self.event_callbacks.clear() - self.event_handlers.clear() - self.event_filters.clear() - - self.initialized = False - print("✓ 事件处理器资源已清理") - - except Exception as e: - print(f"✗ 事件处理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新事件处理器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.event_state["last_event_time"] = current_time - - except Exception as e: - print(f"✗ 事件处理器更新失败: {e}") - self.event_stats["handler_errors"] += 1 - import traceback - traceback.print_exc() - - def _process_event_queue(self): - """处理事件队列""" - try: - processed_count = 0 - max_process_per_loop = 100 # 每次循环最多处理100个事件 - - while not self.event_queue.empty() and processed_count < max_process_per_loop: - try: - # 从队列获取事件 - event_data = self.event_queue.get_nowait() - - # 处理事件 - self._handle_event_internal(event_data) - - processed_count += 1 - self.event_stats["events_processed"] += 1 - - except queue.Empty: - break - except Exception as e: - print(f"✗ 事件队列处理失败: {e}") - self.event_stats["handler_errors"] += 1 - - self.event_state["pending_events"] = self.event_queue.qsize() - - except Exception as e: - print(f"✗ 事件队列处理失败: {e}") - self.event_stats["handler_errors"] += 1 - - def _handle_event_internal(self, event_data: Dict[str, Any]): - """ - 内部事件处理 - - Args: - event_data: 事件数据 - """ - try: - event_type = event_data.get("type") - event_payload = event_data.get("payload", {}) - event_priority = event_data.get("priority", 0) - event_timestamp = event_data.get("timestamp", time.time()) - - # 应用事件过滤器 - if self.event_config["enable_event_filtering"]: - if not self._apply_event_filters(event_type, event_payload): - # 事件被过滤掉 - return - - # 查找事件处理器 - if event_type in self.event_handlers: - handlers = self.event_handlers[event_type] - - # 按优先级排序处理器 - if self.event_config["enable_event_prioritization"]: - handlers = sorted(handlers, key=lambda x: x.get("priority", 0), reverse=True) - - # 调用所有处理器 - for handler_info in handlers: - try: - handler_func = handler_info["function"] - handler_result = handler_func(event_payload) - - # 检查是否需要停止传播 - if handler_result is False: - break - - except Exception as e: - print(f"✗ 事件处理器执行失败: {event_type} - {e}") - self.event_stats["handler_errors"] += 1 - - # 触发事件处理回调 - self._trigger_event_callback("event_processed", { - "event_type": event_type, - "event_payload": event_payload, - "processing_time": time.time() - event_timestamp, - "timestamp": time.time() - }) - - except Exception as e: - print(f"✗ 内部事件处理失败: {e}") - self.event_stats["handler_errors"] += 1 - - def _apply_event_filters(self, event_type: str, event_payload: Dict[str, Any]) -> bool: - """ - 应用事件过滤器 - - Args: - event_type: 事件类型 - event_payload: 事件载荷 - - Returns: - 是否通过过滤器 - """ - try: - if event_type in self.event_filters: - filters = self.event_filters[event_type] - for filter_func in filters: - if not filter_func(event_payload): - return False - - return True - - except Exception as e: - print(f"✗ 事件过滤器应用失败: {e}") - self.event_stats["handler_errors"] += 1 - return True - - def emit_event(self, event_type: str, event_payload: Dict[str, Any] = None, - priority: int = 0, async_process: bool = None) -> bool: - """ - 发出事件 - - Args: - event_type: 事件类型 - event_payload: 事件载荷 - priority: 事件优先级 - async_process: 是否异步处理 - - Returns: - 是否发出成功 - """ - try: - if not self.enabled or not self.event_config["enable_event_system"]: - return False - - # 使用默认异步处理设置或指定设置 - if async_process is None: - async_process = self.event_config["enable_async_processing"] - - event_data = { - "type": event_type, - "payload": event_payload or {}, - "priority": priority, - "timestamp": time.time(), - "async": async_process - } - - # 触发事件接收回调 - self._trigger_event_callback("event_received", { - "event_type": event_type, - "event_payload": event_payload, - "priority": priority, - "timestamp": time.time() - }) - - # 更新统计 - self.event_stats["events_received"] += 1 - - if async_process: - # 异步处理:添加到队列 - try: - self.event_queue.put_nowait(event_data) - self.event_stats["async_events"] += 1 - return True - except queue.Full: - # 队列已满,丢弃事件 - self.event_stats["events_dropped"] += 1 - self.event_state["dropped_events"] += 1 - - # 触发事件丢弃回调 - self._trigger_event_callback("event_dropped", { - "event_type": event_type, - "event_payload": event_payload, - "reason": "queue_full", - "timestamp": time.time() - }) - - return False - else: - # 同步处理:立即处理 - self._handle_event_internal(event_data) - return True - - except Exception as e: - print(f"✗ 事件发出失败: {e}") - self.event_stats["handler_errors"] += 1 - return False - - def register_event_handler(self, event_type: str, handler: Callable, priority: int = 0): - """ - 注册事件处理器 - - Args: - event_type: 事件类型 - handler: 处理器函数 - priority: 处理器优先级 - """ - try: - if event_type not in self.event_handlers: - self.event_handlers[event_type] = [] - - handler_info = { - "function": handler, - "priority": priority - } - - self.event_handlers[event_type].append(handler_info) - print(f"✓ 事件处理器已注册: {event_type} (优先级: {priority})") - - except Exception as e: - print(f"✗ 事件处理器注册失败: {e}") - self.event_stats["handler_errors"] += 1 - - def unregister_event_handler(self, event_type: str, handler: Callable): - """ - 注销事件处理器 - - Args: - event_type: 事件类型 - handler: 处理器函数 - """ - try: - if event_type in self.event_handlers: - handlers = self.event_handlers[event_type] - for i, handler_info in enumerate(handlers): - if handler_info["function"] == handler: - del handlers[i] - print(f"✓ 事件处理器已注销: {event_type}") - return - - print(f"✗ 事件处理器不存在: {event_type}") - - except Exception as e: - print(f"✗ 事件处理器注销失败: {e}") - self.event_stats["handler_errors"] += 1 - - def register_event_filter(self, event_type: str, filter_func: Callable): - """ - 注册事件过滤器 - - Args: - event_type: 事件类型 - filter_func: 过滤器函数 - """ - try: - if event_type not in self.event_filters: - self.event_filters[event_type] = [] - - self.event_filters[event_type].append(filter_func) - print(f"✓ 事件过滤器已注册: {event_type}") - - except Exception as e: - print(f"✗ 事件过滤器注册失败: {e}") - self.event_stats["handler_errors"] += 1 - - def unregister_event_filter(self, event_type: str, filter_func: Callable): - """ - 注销事件过滤器 - - Args: - event_type: 事件类型 - filter_func: 过滤器函数 - """ - try: - if event_type in self.event_filters: - filters = self.event_filters[event_type] - if filter_func in filters: - filters.remove(filter_func) - print(f"✓ 事件过滤器已注销: {event_type}") - return - - print(f"✗ 事件过滤器不存在: {event_type}") - - except Exception as e: - print(f"✗ 事件过滤器注销失败: {e}") - self.event_stats["handler_errors"] += 1 - - # 默认事件处理器 - def _handle_client_connected(self, payload: Dict[str, Any]): - """处理客户端连接事件""" - try: - client_id = payload.get("client_id") - connection_data = payload.get("connection_data") - - # 添加客户端到客户端管理器 - if self.plugin.client_manager: - self.plugin.client_manager.add_client(client_id, { - "connection_data": connection_data, - "ip_address": connection_data.get("remote_address", "") if connection_data else "" - }) - - except Exception as e: - print(f"✗ 客户端连接事件处理失败: {e}") - self.event_stats["handler_errors"] += 1 - - def _handle_client_disconnected(self, payload: Dict[str, Any]): - """处理客户端断开连接事件""" - try: - client_id = payload.get("client_id") - reason = payload.get("reason", "unknown") - - # 从客户端管理器移除客户端 - if self.plugin.client_manager: - self.plugin.client_manager.remove_client(client_id, reason) - - except Exception as e: - print(f"✗ 客户端断开连接事件处理失败: {e}") - self.event_stats["handler_errors"] += 1 - - def _handle_message_received(self, payload: Dict[str, Any]): - """处理消息接收事件""" - try: - client_id = payload.get("client_id") - message = payload.get("message") - - # 将消息传递给消息路由器 - if self.plugin.message_router: - self.plugin.message_router.route_message(client_id, message) - - except Exception as e: - print(f"✗ 消息接收事件处理失败: {e}") - self.event_stats["handler_errors"] += 1 - - def _handle_room_created(self, payload: Dict[str, Any]): - """处理房间创建事件""" - try: - room_id = payload.get("room_id") - room_name = payload.get("room_name") - - # 可以在这里添加额外的房间创建逻辑 - print(f"房间已创建: {room_name} ({room_id})") - - except Exception as e: - print(f"✗ 房间创建事件处理失败: {e}") - self.event_stats["handler_errors"] += 1 - - def _handle_room_destroyed(self, payload: Dict[str, Any]): - """处理房间销毁事件""" - try: - room_id = payload.get("room_id") - room_name = payload.get("room_name") - - # 可以在这里添加额外的房间销毁逻辑 - print(f"房间已销毁: {room_name} ({room_id})") - - except Exception as e: - print(f"✗ 房间销毁事件处理失败: {e}") - self.event_stats["handler_errors"] += 1 - - def _handle_client_joined_room(self, payload: Dict[str, Any]): - """处理客户端加入房间事件""" - try: - room_id = payload.get("room_id") - client_id = payload.get("client_id") - - # 可以在这里添加额外的客户端加入房间逻辑 - print(f"客户端 {client_id} 已加入房间 {room_id}") - - except Exception as e: - print(f"✗ 客户端加入房间事件处理失败: {e}") - self.event_stats["handler_errors"] += 1 - - def _handle_client_left_room(self, payload: Dict[str, Any]): - """处理客户端离开房间事件""" - try: - room_id = payload.get("room_id") - client_id = payload.get("client_id") - - # 可以在这里添加额外的客户端离开房间逻辑 - print(f"客户端 {client_id} 已离开房间 {room_id}") - - except Exception as e: - print(f"✗ 客户端离开房间事件处理失败: {e}") - self.event_stats["handler_errors"] += 1 - - def get_event_stats(self) -> Dict[str, Any]: - """ - 获取事件统计信息 - - Returns: - 事件统计字典 - """ - return { - "state": self.event_state.copy(), - "stats": self.event_stats.copy(), - "config": self.event_config.copy(), - "pending_events": self.event_queue.qsize() - } - - def reset_stats(self): - """重置事件统计信息""" - try: - self.event_stats = { - "events_received": 0, - "events_processed": 0, - "events_dropped": 0, - "handler_errors": 0, - "async_events": 0 - } - print("✓ 事件统计信息已重置") - except Exception as e: - print(f"✗ 事件统计信息重置失败: {e}") - - def set_event_config(self, config: Dict[str, Any]) -> bool: - """ - 设置事件配置 - - Args: - config: 事件配置字典 - - Returns: - 是否设置成功 - """ - try: - self.event_config.update(config) - print(f"✓ 事件配置已更新: {self.event_config}") - return True - except Exception as e: - print(f"✗ 事件配置设置失败: {e}") - return False - - def get_event_config(self) -> Dict[str, Any]: - """ - 获取事件配置 - - Returns: - 事件配置字典 - """ - return self.event_config.copy() - - def _trigger_event_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发事件回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.event_callbacks: - for callback in self.event_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 事件回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 事件回调触发失败: {e}") - - def register_event_callback(self, callback_type: str, callback: callable): - """ - 注册事件回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.event_callbacks: - self.event_callbacks[callback_type].append(callback) - print(f"✓ 事件回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 事件回调注册失败: {e}") - - def unregister_event_callback(self, callback_type: str, callback: callable): - """ - 注销事件回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.event_callbacks: - if callback in self.event_callbacks[callback_type]: - self.event_callbacks[callback_type].remove(callback) - print(f"✓ 事件回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +事件处理器模块 +处理通信事件和系统响应 +""" + +import time +import threading +import queue +from typing import Dict, Any, List, Optional, Callable + +class EventHandler: + """ + 事件处理器 + 处理通信事件和系统响应 + """ + + def __init__(self, plugin): + """ + 初始化事件处理器 + + Args: + plugin: 实时通信插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 事件配置 + self.event_config = { + "enable_event_system": True, + "max_event_queue_size": 10000, + "enable_event_filtering": True, + "event_processing_interval": 0.01, # 10ms + "enable_async_processing": True, + "max_concurrent_events": 100, + "enable_event_prioritization": True, + "enable_event_logging": True + } + + # 事件状态 + self.event_state = { + "is_processing": False, + "pending_events": 0, + "processed_events": 0, + "dropped_events": 0, + "last_event_time": 0.0 + } + + # 事件队列 + self.event_queue = queue.Queue(maxsize=self.event_config["max_event_queue_size"]) + self.event_queue_lock = threading.RLock() + + # 事件处理器存储 + self.event_handlers = {} + + # 事件统计 + self.event_stats = { + "events_received": 0, + "events_processed": 0, + "events_dropped": 0, + "handler_errors": 0, + "async_events": 0 + } + + # 事件过滤器 + self.event_filters = {} + + # 回调函数 + self.event_callbacks = { + "event_received": [], + "event_processed": [], + "event_dropped": [], + "event_error": [] + } + + # 事件处理线程 + self.event_thread = None + self.event_thread_running = False + + # 时间戳记录 + self.last_event_process = 0.0 + self.last_stats_reset = 0.0 + + print("✓ 事件处理器已创建") + + def initialize(self) -> bool: + """ + 初始化事件处理器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化事件处理器...") + + # 注册默认事件处理器 + self._register_default_handlers() + + # 启动事件处理线程 + self._start_event_thread() + + self.initialized = True + print("✓ 事件处理器初始化完成") + return True + + except Exception as e: + print(f"✗ 事件处理器初始化失败: {e}") + self.event_stats["handler_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def _register_default_handlers(self): + """注册默认事件处理器""" + try: + # 注册系统事件处理器 + self.register_event_handler("client_connected", self._handle_client_connected) + self.register_event_handler("client_disconnected", self._handle_client_disconnected) + self.register_event_handler("message_received", self._handle_message_received) + self.register_event_handler("room_created", self._handle_room_created) + self.register_event_handler("room_destroyed", self._handle_room_destroyed) + self.register_event_handler("client_joined_room", self._handle_client_joined_room) + self.register_event_handler("client_left_room", self._handle_client_left_room) + + print("✓ 默认事件处理器已注册") + + except Exception as e: + print(f"✗ 默认事件处理器注册失败: {e}") + self.event_stats["handler_errors"] += 1 + + def _start_event_thread(self): + """启动事件处理线程""" + try: + self.event_thread_running = True + self.event_thread = threading.Thread(target=self._event_loop, daemon=True) + self.event_thread.start() + print("✓ 事件处理线程已启动") + except Exception as e: + print(f"✗ 事件处理线程启动失败: {e}") + self.event_stats["handler_errors"] += 1 + + def _event_loop(self): + """事件处理循环""" + try: + while self.event_thread_running: + try: + if self.enabled and self.event_config["enable_event_system"]: + # 处理事件队列 + self._process_event_queue() + + # 短暂休眠 + time.sleep(self.event_config["event_processing_interval"]) + + except Exception as e: + print(f"✗ 事件处理循环错误: {e}") + self.event_stats["handler_errors"] += 1 + time.sleep(1.0) # 出错时延长休眠 + + except Exception as e: + print(f"✗ 事件处理线程失败: {e}") + self.event_stats["handler_errors"] += 1 + + def enable(self) -> bool: + """ + 启用事件处理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 事件处理器未初始化") + return False + + self.enabled = True + print("✓ 事件处理器已启用") + return True + + except Exception as e: + print(f"✗ 事件处理器启用失败: {e}") + self.event_stats["handler_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用事件处理器""" + try: + self.enabled = False + + # 停止事件处理线程 + if self.event_thread_running: + self.event_thread_running = False + if self.event_thread and self.event_thread.is_alive(): + self.event_thread.join(timeout=5.0) + + print("✓ 事件处理器已禁用") + + except Exception as e: + print(f"✗ 事件处理器禁用失败: {e}") + self.event_stats["handler_errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理事件处理器资源""" + try: + # 禁用事件处理器 + if self.enabled: + self.disable() + + # 清理回调和处理器 + self.event_callbacks.clear() + self.event_handlers.clear() + self.event_filters.clear() + + self.initialized = False + print("✓ 事件处理器资源已清理") + + except Exception as e: + print(f"✗ 事件处理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新事件处理器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.event_state["last_event_time"] = current_time + + except Exception as e: + print(f"✗ 事件处理器更新失败: {e}") + self.event_stats["handler_errors"] += 1 + import traceback + traceback.print_exc() + + def _process_event_queue(self): + """处理事件队列""" + try: + processed_count = 0 + max_process_per_loop = 100 # 每次循环最多处理100个事件 + + while not self.event_queue.empty() and processed_count < max_process_per_loop: + try: + # 从队列获取事件 + event_data = self.event_queue.get_nowait() + + # 处理事件 + self._handle_event_internal(event_data) + + processed_count += 1 + self.event_stats["events_processed"] += 1 + + except queue.Empty: + break + except Exception as e: + print(f"✗ 事件队列处理失败: {e}") + self.event_stats["handler_errors"] += 1 + + self.event_state["pending_events"] = self.event_queue.qsize() + + except Exception as e: + print(f"✗ 事件队列处理失败: {e}") + self.event_stats["handler_errors"] += 1 + + def _handle_event_internal(self, event_data: Dict[str, Any]): + """ + 内部事件处理 + + Args: + event_data: 事件数据 + """ + try: + event_type = event_data.get("type") + event_payload = event_data.get("payload", {}) + event_priority = event_data.get("priority", 0) + event_timestamp = event_data.get("timestamp", time.time()) + + # 应用事件过滤器 + if self.event_config["enable_event_filtering"]: + if not self._apply_event_filters(event_type, event_payload): + # 事件被过滤掉 + return + + # 查找事件处理器 + if event_type in self.event_handlers: + handlers = self.event_handlers[event_type] + + # 按优先级排序处理器 + if self.event_config["enable_event_prioritization"]: + handlers = sorted(handlers, key=lambda x: x.get("priority", 0), reverse=True) + + # 调用所有处理器 + for handler_info in handlers: + try: + handler_func = handler_info["function"] + handler_result = handler_func(event_payload) + + # 检查是否需要停止传播 + if handler_result is False: + break + + except Exception as e: + print(f"✗ 事件处理器执行失败: {event_type} - {e}") + self.event_stats["handler_errors"] += 1 + + # 触发事件处理回调 + self._trigger_event_callback("event_processed", { + "event_type": event_type, + "event_payload": event_payload, + "processing_time": time.time() - event_timestamp, + "timestamp": time.time() + }) + + except Exception as e: + print(f"✗ 内部事件处理失败: {e}") + self.event_stats["handler_errors"] += 1 + + def _apply_event_filters(self, event_type: str, event_payload: Dict[str, Any]) -> bool: + """ + 应用事件过滤器 + + Args: + event_type: 事件类型 + event_payload: 事件载荷 + + Returns: + 是否通过过滤器 + """ + try: + if event_type in self.event_filters: + filters = self.event_filters[event_type] + for filter_func in filters: + if not filter_func(event_payload): + return False + + return True + + except Exception as e: + print(f"✗ 事件过滤器应用失败: {e}") + self.event_stats["handler_errors"] += 1 + return True + + def emit_event(self, event_type: str, event_payload: Dict[str, Any] = None, + priority: int = 0, async_process: bool = None) -> bool: + """ + 发出事件 + + Args: + event_type: 事件类型 + event_payload: 事件载荷 + priority: 事件优先级 + async_process: 是否异步处理 + + Returns: + 是否发出成功 + """ + try: + if not self.enabled or not self.event_config["enable_event_system"]: + return False + + # 使用默认异步处理设置或指定设置 + if async_process is None: + async_process = self.event_config["enable_async_processing"] + + event_data = { + "type": event_type, + "payload": event_payload or {}, + "priority": priority, + "timestamp": time.time(), + "async": async_process + } + + # 触发事件接收回调 + self._trigger_event_callback("event_received", { + "event_type": event_type, + "event_payload": event_payload, + "priority": priority, + "timestamp": time.time() + }) + + # 更新统计 + self.event_stats["events_received"] += 1 + + if async_process: + # 异步处理:添加到队列 + try: + self.event_queue.put_nowait(event_data) + self.event_stats["async_events"] += 1 + return True + except queue.Full: + # 队列已满,丢弃事件 + self.event_stats["events_dropped"] += 1 + self.event_state["dropped_events"] += 1 + + # 触发事件丢弃回调 + self._trigger_event_callback("event_dropped", { + "event_type": event_type, + "event_payload": event_payload, + "reason": "queue_full", + "timestamp": time.time() + }) + + return False + else: + # 同步处理:立即处理 + self._handle_event_internal(event_data) + return True + + except Exception as e: + print(f"✗ 事件发出失败: {e}") + self.event_stats["handler_errors"] += 1 + return False + + def register_event_handler(self, event_type: str, handler: Callable, priority: int = 0): + """ + 注册事件处理器 + + Args: + event_type: 事件类型 + handler: 处理器函数 + priority: 处理器优先级 + """ + try: + if event_type not in self.event_handlers: + self.event_handlers[event_type] = [] + + handler_info = { + "function": handler, + "priority": priority + } + + self.event_handlers[event_type].append(handler_info) + print(f"✓ 事件处理器已注册: {event_type} (优先级: {priority})") + + except Exception as e: + print(f"✗ 事件处理器注册失败: {e}") + self.event_stats["handler_errors"] += 1 + + def unregister_event_handler(self, event_type: str, handler: Callable): + """ + 注销事件处理器 + + Args: + event_type: 事件类型 + handler: 处理器函数 + """ + try: + if event_type in self.event_handlers: + handlers = self.event_handlers[event_type] + for i, handler_info in enumerate(handlers): + if handler_info["function"] == handler: + del handlers[i] + print(f"✓ 事件处理器已注销: {event_type}") + return + + print(f"✗ 事件处理器不存在: {event_type}") + + except Exception as e: + print(f"✗ 事件处理器注销失败: {e}") + self.event_stats["handler_errors"] += 1 + + def register_event_filter(self, event_type: str, filter_func: Callable): + """ + 注册事件过滤器 + + Args: + event_type: 事件类型 + filter_func: 过滤器函数 + """ + try: + if event_type not in self.event_filters: + self.event_filters[event_type] = [] + + self.event_filters[event_type].append(filter_func) + print(f"✓ 事件过滤器已注册: {event_type}") + + except Exception as e: + print(f"✗ 事件过滤器注册失败: {e}") + self.event_stats["handler_errors"] += 1 + + def unregister_event_filter(self, event_type: str, filter_func: Callable): + """ + 注销事件过滤器 + + Args: + event_type: 事件类型 + filter_func: 过滤器函数 + """ + try: + if event_type in self.event_filters: + filters = self.event_filters[event_type] + if filter_func in filters: + filters.remove(filter_func) + print(f"✓ 事件过滤器已注销: {event_type}") + return + + print(f"✗ 事件过滤器不存在: {event_type}") + + except Exception as e: + print(f"✗ 事件过滤器注销失败: {e}") + self.event_stats["handler_errors"] += 1 + + # 默认事件处理器 + def _handle_client_connected(self, payload: Dict[str, Any]): + """处理客户端连接事件""" + try: + client_id = payload.get("client_id") + connection_data = payload.get("connection_data") + + # 添加客户端到客户端管理器 + if self.plugin.client_manager: + self.plugin.client_manager.add_client(client_id, { + "connection_data": connection_data, + "ip_address": connection_data.get("remote_address", "") if connection_data else "" + }) + + except Exception as e: + print(f"✗ 客户端连接事件处理失败: {e}") + self.event_stats["handler_errors"] += 1 + + def _handle_client_disconnected(self, payload: Dict[str, Any]): + """处理客户端断开连接事件""" + try: + client_id = payload.get("client_id") + reason = payload.get("reason", "unknown") + + # 从客户端管理器移除客户端 + if self.plugin.client_manager: + self.plugin.client_manager.remove_client(client_id, reason) + + except Exception as e: + print(f"✗ 客户端断开连接事件处理失败: {e}") + self.event_stats["handler_errors"] += 1 + + def _handle_message_received(self, payload: Dict[str, Any]): + """处理消息接收事件""" + try: + client_id = payload.get("client_id") + message = payload.get("message") + + # 将消息传递给消息路由器 + if self.plugin.message_router: + self.plugin.message_router.route_message(client_id, message) + + except Exception as e: + print(f"✗ 消息接收事件处理失败: {e}") + self.event_stats["handler_errors"] += 1 + + def _handle_room_created(self, payload: Dict[str, Any]): + """处理房间创建事件""" + try: + room_id = payload.get("room_id") + room_name = payload.get("room_name") + + # 可以在这里添加额外的房间创建逻辑 + print(f"房间已创建: {room_name} ({room_id})") + + except Exception as e: + print(f"✗ 房间创建事件处理失败: {e}") + self.event_stats["handler_errors"] += 1 + + def _handle_room_destroyed(self, payload: Dict[str, Any]): + """处理房间销毁事件""" + try: + room_id = payload.get("room_id") + room_name = payload.get("room_name") + + # 可以在这里添加额外的房间销毁逻辑 + print(f"房间已销毁: {room_name} ({room_id})") + + except Exception as e: + print(f"✗ 房间销毁事件处理失败: {e}") + self.event_stats["handler_errors"] += 1 + + def _handle_client_joined_room(self, payload: Dict[str, Any]): + """处理客户端加入房间事件""" + try: + room_id = payload.get("room_id") + client_id = payload.get("client_id") + + # 可以在这里添加额外的客户端加入房间逻辑 + print(f"客户端 {client_id} 已加入房间 {room_id}") + + except Exception as e: + print(f"✗ 客户端加入房间事件处理失败: {e}") + self.event_stats["handler_errors"] += 1 + + def _handle_client_left_room(self, payload: Dict[str, Any]): + """处理客户端离开房间事件""" + try: + room_id = payload.get("room_id") + client_id = payload.get("client_id") + + # 可以在这里添加额外的客户端离开房间逻辑 + print(f"客户端 {client_id} 已离开房间 {room_id}") + + except Exception as e: + print(f"✗ 客户端离开房间事件处理失败: {e}") + self.event_stats["handler_errors"] += 1 + + def get_event_stats(self) -> Dict[str, Any]: + """ + 获取事件统计信息 + + Returns: + 事件统计字典 + """ + return { + "state": self.event_state.copy(), + "stats": self.event_stats.copy(), + "config": self.event_config.copy(), + "pending_events": self.event_queue.qsize() + } + + def reset_stats(self): + """重置事件统计信息""" + try: + self.event_stats = { + "events_received": 0, + "events_processed": 0, + "events_dropped": 0, + "handler_errors": 0, + "async_events": 0 + } + print("✓ 事件统计信息已重置") + except Exception as e: + print(f"✗ 事件统计信息重置失败: {e}") + + def set_event_config(self, config: Dict[str, Any]) -> bool: + """ + 设置事件配置 + + Args: + config: 事件配置字典 + + Returns: + 是否设置成功 + """ + try: + self.event_config.update(config) + print(f"✓ 事件配置已更新: {self.event_config}") + return True + except Exception as e: + print(f"✗ 事件配置设置失败: {e}") + return False + + def get_event_config(self) -> Dict[str, Any]: + """ + 获取事件配置 + + Returns: + 事件配置字典 + """ + return self.event_config.copy() + + def _trigger_event_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发事件回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.event_callbacks: + for callback in self.event_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 事件回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 事件回调触发失败: {e}") + + def register_event_callback(self, callback_type: str, callback: callable): + """ + 注册事件回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.event_callbacks: + self.event_callbacks[callback_type].append(callback) + print(f"✓ 事件回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 事件回调注册失败: {e}") + + def unregister_event_callback(self, callback_type: str, callback: callable): + """ + 注销事件回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.event_callbacks: + if callback in self.event_callbacks[callback_type]: + self.event_callbacks[callback_type].remove(callback) + print(f"✓ 事件回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 事件回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/realtime_communication/messaging/message_router.py b/plugins/user/realtime_communication/messaging/message_router.py index e9304ee0..7940278e 100644 --- a/plugins/user/realtime_communication/messaging/message_router.py +++ b/plugins/user/realtime_communication/messaging/message_router.py @@ -1,771 +1,771 @@ -""" -消息路由器模块 -处理客户端间的消息传递 -""" - -import time -import threading -import json -from typing import Dict, Any, List, Optional - -class MessageRouter: - """ - 消息路由器 - 处理客户端间的消息传递 - """ - - def __init__(self, plugin): - """ - 初始化消息路由器 - - Args: - plugin: 实时通信插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 消息路由配置 - self.message_config = { - "enable_message_filtering": True, - "max_message_size": 65536, # 64KB - "enable_message_queue": True, - "message_queue_size": 10000, - "enable_broadcast_optimization": True, - "enable_message_compression": True, - "enable_rate_limiting": True, - "max_messages_per_second_per_client": 100, - "enable_message_logging": False - } - - # 消息路由状态 - self.router_state = { - "is_processing": False, - "pending_messages": 0, - "active_routes": 0, - "last_message_time": 0.0 - } - - # 消息路由统计 - self.message_stats = { - "messages_routed": 0, - "messages_broadcast": 0, - "messages_filtered": 0, - "messages_dropped": 0, - "bytes_routed": 0, - "routing_errors": 0 - } - - # 消息处理器存储 - self.message_handlers = {} - - # 消息队列 - self.message_queue = [] - self.message_queue_lock = threading.RLock() - - # 路由表 - self.routing_table = {} - - # 回调函数 - self.message_callbacks = { - "message_routed": [], - "message_broadcast": [], - "message_filtered": [], - "message_error": [] - } - - # 时间戳记录 - self.last_queue_process = 0.0 - self.last_stats_reset = 0.0 - - print("✓ 消息路由器已创建") - - def initialize(self) -> bool: - """ - 初始化消息路由器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化消息路由器...") - - # 注册默认消息处理器 - self._register_default_handlers() - - self.initialized = True - print("✓ 消息路由器初始化完成") - return True - - except Exception as e: - print(f"✗ 消息路由器初始化失败: {e}") - self.message_stats["routing_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def _register_default_handlers(self): - """注册默认消息处理器""" - try: - # 注册系统消息处理器 - self.register_message_handler("ping", self._handle_ping_message) - self.register_message_handler("pong", self._handle_pong_message) - self.register_message_handler("chat", self._handle_chat_message) - self.register_message_handler("join_room", self._handle_join_room_message) - self.register_message_handler("leave_room", self._handle_leave_room_message) - self.register_message_handler("broadcast", self._handle_broadcast_message) - - print("✓ 默认消息处理器已注册") - - except Exception as e: - print(f"✗ 默认消息处理器注册失败: {e}") - self.message_stats["routing_errors"] += 1 - - def enable(self) -> bool: - """ - 启用消息路由器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 消息路由器未初始化") - return False - - self.enabled = True - print("✓ 消息路由器已启用") - return True - - except Exception as e: - print(f"✗ 消息路由器启用失败: {e}") - self.message_stats["routing_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用消息路由器""" - try: - self.enabled = False - - # 清空消息队列 - with self.message_queue_lock: - self.message_queue.clear() - - print("✓ 消息路由器已禁用") - - except Exception as e: - print(f"✗ 消息路由器禁用失败: {e}") - self.message_stats["routing_errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理消息路由器资源""" - try: - # 禁用消息路由器 - if self.enabled: - self.disable() - - # 清理回调和处理器 - self.message_callbacks.clear() - self.message_handlers.clear() - - self.initialized = False - print("✓ 消息路由器资源已清理") - - except Exception as e: - print(f"✗ 消息路由器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新消息路由器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 处理消息队列 - if self.message_config["enable_message_queue"]: - self._process_message_queue() - - self.router_state["last_message_time"] = current_time - - except Exception as e: - print(f"✗ 消息路由器更新失败: {e}") - self.message_stats["routing_errors"] += 1 - import traceback - traceback.print_exc() - - def _process_message_queue(self): - """处理消息队列""" - try: - if not self.message_queue: - return - - with self.message_queue_lock: - messages_to_process = self.message_queue[:] - self.message_queue.clear() - - for message_data in messages_to_process: - try: - client_id = message_data.get("client_id") - message = message_data.get("message") - - # 处理消息 - self._route_message_internal(client_id, message) - - except Exception as e: - print(f"✗ 队列消息处理失败: {e}") - self.message_stats["routing_errors"] += 1 - - self.router_state["pending_messages"] = len(self.message_queue) - - except Exception as e: - print(f"✗ 消息队列处理失败: {e}") - self.message_stats["routing_errors"] += 1 - - def register_message_handler(self, message_type: str, handler: callable): - """ - 注册消息处理器 - - Args: - message_type: 消息类型 - handler: 处理器函数 - """ - try: - self.message_handlers[message_type] = handler - print(f"✓ 消息处理器已注册: {message_type}") - except Exception as e: - print(f"✗ 消息处理器注册失败: {e}") - self.message_stats["routing_errors"] += 1 - - def unregister_message_handler(self, message_type: str): - """ - 注销消息处理器 - - Args: - message_type: 消息类型 - """ - try: - if message_type in self.message_handlers: - del self.message_handlers[message_type] - print(f"✓ 消息处理器已注销: {message_type}") - else: - print(f"✗ 消息处理器不存在: {message_type}") - except Exception as e: - print(f"✗ 消息处理器注销失败: {e}") - self.message_stats["routing_errors"] += 1 - - def route_message(self, client_id: str, message: Dict[str, Any]) -> bool: - """ - 路由消息 - - Args: - client_id: 客户端ID - message: 消息数据 - - Returns: - 是否路由成功 - """ - try: - if not self.enabled: - return False - - # 检查消息大小 - if self.message_config["enable_message_filtering"]: - message_size = len(json.dumps(message, ensure_ascii=False).encode('utf-8')) - if message_size > self.message_config["max_message_size"]: - print(f"✗ 消息过大: {message_size} bytes") - self.message_stats["messages_dropped"] += 1 - return False - - # 如果启用消息队列,将消息加入队列 - if self.message_config["enable_message_queue"]: - with self.message_queue_lock: - if len(self.message_queue) >= self.message_config["message_queue_size"]: - print("✗ 消息队列已满,丢弃消息") - self.message_stats["messages_dropped"] += 1 - return False - - self.message_queue.append({ - "client_id": client_id, - "message": message, - "timestamp": time.time() - }) - - self.router_state["pending_messages"] = len(self.message_queue) - return True - else: - # 直接处理消息 - return self._route_message_internal(client_id, message) - - except Exception as e: - print(f"✗ 消息路由失败: {e}") - self.message_stats["routing_errors"] += 1 - return False - - def _route_message_internal(self, client_id: str, message: Dict[str, Any]) -> bool: - """ - 内部消息路由处理 - - Args: - client_id: 客户端ID - message: 消息数据 - - Returns: - 是否路由成功 - """ - try: - # 更新统计信息 - self.message_stats["messages_routed"] += 1 - message_size = len(json.dumps(message, ensure_ascii=False).encode('utf-8')) - self.message_stats["bytes_routed"] += message_size - - # 触发消息路由回调 - self._trigger_message_callback("message_routed", { - "client_id": client_id, - "message": message, - "timestamp": time.time() - }) - - # 获取消息类型 - message_type = message.get("type", "unknown") - - # 查找对应的消息处理器 - if message_type in self.message_handlers: - try: - # 调用消息处理器 - handler_result = self.message_handlers[message_type](client_id, message) - return handler_result if handler_result is not None else True - except Exception as e: - print(f"✗ 消息处理器执行失败: {message_type} - {e}") - self.message_stats["routing_errors"] += 1 - return False - else: - # 默认处理:广播给所有客户端(除了发送者) - return self._handle_default_message(client_id, message) - - except Exception as e: - print(f"✗ 内部消息路由处理失败: {e}") - self.message_stats["routing_errors"] += 1 - return False - - def _handle_default_message(self, client_id: str, message: Dict[str, Any]) -> bool: - """ - 处理默认消息(广播给其他客户端) - - Args: - client_id: 发送客户端ID - message: 消息数据 - - Returns: - 是否处理成功 - """ - try: - # 广播消息给所有其他客户端 - if self.plugin.websocket_server: - broadcast_success = self.plugin.websocket_server.broadcast_message( - message, exclude_client_id=client_id) - if broadcast_success: - self.message_stats["messages_broadcast"] += 1 - return True - else: - return False - else: - return False - - except Exception as e: - print(f"✗ 默认消息处理失败: {e}") - self.message_stats["routing_errors"] += 1 - return False - - # 默认消息处理器 - def _handle_ping_message(self, client_id: str, message: Dict[str, Any]) -> bool: - """ - 处理ping消息 - - Args: - client_id: 客户端ID - message: 消息数据 - - Returns: - 是否处理成功 - """ - try: - # 更新客户端心跳 - if self.plugin.client_manager: - self.plugin.client_manager.update_client_heartbeat(client_id) - - # 回复pong消息 - pong_message = { - "type": "pong", - "timestamp": time.time(), - "ping_timestamp": message.get("timestamp", 0) - } - - if self.plugin.websocket_server: - return self.plugin.websocket_server.send_message_to_client(client_id, pong_message) - else: - return False - - except Exception as e: - print(f"✗ ping消息处理失败: {e}") - self.message_stats["routing_errors"] += 1 - return False - - def _handle_pong_message(self, client_id: str, message: Dict[str, Any]) -> bool: - """ - 处理pong消息 - - Args: - client_id: 客户端ID - message: 消息数据 - - Returns: - 是否处理成功 - """ - try: - # 更新客户端心跳 - if self.plugin.client_manager: - self.plugin.client_manager.update_client_heartbeat(client_id) - - return True - - except Exception as e: - print(f"✗ pong消息处理失败: {e}") - self.message_stats["routing_errors"] += 1 - return False - - def _handle_chat_message(self, client_id: str, message: Dict[str, Any]) -> bool: - """ - 处理聊天消息 - - Args: - client_id: 客户端ID - message: 消息数据 - - Returns: - 是否处理成功 - """ - try: - # 获取客户端信息 - client_data = self.plugin.client_manager.get_client(client_id) if self.plugin.client_manager else None - if not client_data: - return False - - # 添加发送者信息 - message["sender_id"] = client_id - message["sender_name"] = client_data.get("username", "Unknown") - message["sender_time"] = time.time() - - # 广播聊天消息 - if self.plugin.websocket_server: - broadcast_success = self.plugin.websocket_server.broadcast_message(message) - if broadcast_success: - self.message_stats["messages_broadcast"] += 1 - return broadcast_success - else: - return False - - except Exception as e: - print(f"✗ 聊天消息处理失败: {e}") - self.message_stats["routing_errors"] += 1 - return False - - def _handle_join_room_message(self, client_id: str, message: Dict[str, Any]) -> bool: - """ - 处理加入房间消息 - - Args: - client_id: 客户端ID - message: 消息数据 - - Returns: - 是否处理成功 - """ - try: - room_id = message.get("room_id") - if not room_id: - return False - - # 使用房间管理器处理加入房间请求 - if self.plugin.room_manager: - join_success = self.plugin.room_manager.add_client_to_room(room_id, client_id) - if join_success: - # 通知客户端加入成功 - response_message = { - "type": "room_joined", - "room_id": room_id, - "timestamp": time.time() - } - - if self.plugin.websocket_server: - self.plugin.websocket_server.send_message_to_client(client_id, response_message) - - return join_success - else: - return False - - except Exception as e: - print(f"✗ 加入房间消息处理失败: {e}") - self.message_stats["routing_errors"] += 1 - return False - - def _handle_leave_room_message(self, client_id: str, message: Dict[str, Any]) -> bool: - """ - 处理离开房间消息 - - Args: - client_id: 客户端ID - message: 消息数据 - - Returns: - 是否处理成功 - """ - try: - room_id = message.get("room_id") - if not room_id: - return False - - # 使用房间管理器处理离开房间请求 - if self.plugin.room_manager: - leave_success = self.plugin.room_manager.remove_client_from_room(room_id, client_id) - if leave_success: - # 通知客户端离开成功 - response_message = { - "type": "room_left", - "room_id": room_id, - "timestamp": time.time() - } - - if self.plugin.websocket_server: - self.plugin.websocket_server.send_message_to_client(client_id, response_message) - - return leave_success - else: - return False - - except Exception as e: - print(f"✗ 离开房间消息处理失败: {e}") - self.message_stats["routing_errors"] += 1 - return False - - def _handle_broadcast_message(self, client_id: str, message: Dict[str, Any]) -> bool: - """ - 处理广播消息 - - Args: - client_id: 客户端ID - message: 消息数据 - - Returns: - 是否处理成功 - """ - try: - # 广播消息给所有客户端 - if self.plugin.websocket_server: - broadcast_success = self.plugin.websocket_server.broadcast_message(message) - if broadcast_success: - self.message_stats["messages_broadcast"] += 1 - return broadcast_success - else: - return False - - except Exception as e: - print(f"✗ 广播消息处理失败: {e}") - self.message_stats["routing_errors"] += 1 - return False - - def send_direct_message(self, from_client_id: str, to_client_id: str, message: Dict[str, Any]) -> bool: - """ - 发送直接消息 - - Args: - from_client_id: 发送客户端ID - to_client_id: 接收客户端ID - message: 消息数据 - - Returns: - 是否发送成功 - """ - try: - if not self.enabled: - return False - - # 添加发送者信息 - message["sender_id"] = from_client_id - message["timestamp"] = time.time() - - # 发送消息到目标客户端 - if self.plugin.websocket_server: - send_success = self.plugin.websocket_server.send_message_to_client(to_client_id, message) - if send_success: - self.message_stats["messages_routed"] += 1 - return send_success - else: - return False - - except Exception as e: - print(f"✗ 直接消息发送失败: {e}") - self.message_stats["routing_errors"] += 1 - return False - - def broadcast_to_room(self, room_id: str, message: Dict[str, Any], exclude_client_id: str = None) -> bool: - """ - 广播消息到房间 - - Args: - room_id: 房间ID - message: 消息数据 - exclude_client_id: 要排除的客户端ID - - Returns: - 是否广播成功 - """ - try: - if not self.enabled: - return False - - # 使用房间管理器获取房间内的客户端 - if self.plugin.room_manager: - room_clients = self.plugin.room_manager.get_room_clients(room_id) - if not room_clients: - return False - - success_count = 0 - for client_id in room_clients: - # 跳过排除的客户端 - if client_id == exclude_client_id: - continue - - # 发送消息 - if self.plugin.websocket_server: - send_success = self.plugin.websocket_server.send_message_to_client(client_id, message) - if send_success: - success_count += 1 - - return success_count > 0 - else: - return False - - except Exception as e: - print(f"✗ 房间广播失败: {e}") - self.message_stats["routing_errors"] += 1 - return False - - def get_message_stats(self) -> Dict[str, Any]: - """ - 获取消息统计信息 - - Returns: - 消息统计字典 - """ - return { - "state": self.router_state.copy(), - "stats": self.message_stats.copy(), - "config": self.message_config.copy() - } - - def reset_stats(self): - """重置消息统计信息""" - try: - self.message_stats = { - "messages_routed": 0, - "messages_broadcast": 0, - "messages_filtered": 0, - "messages_dropped": 0, - "bytes_routed": 0, - "routing_errors": 0 - } - print("✓ 消息统计信息已重置") - except Exception as e: - print(f"✗ 消息统计信息重置失败: {e}") - - def set_message_config(self, config: Dict[str, Any]) -> bool: - """ - 设置消息配置 - - Args: - config: 消息配置字典 - - Returns: - 是否设置成功 - """ - try: - self.message_config.update(config) - print(f"✓ 消息配置已更新: {self.message_config}") - return True - except Exception as e: - print(f"✗ 消息配置设置失败: {e}") - return False - - def get_message_config(self) -> Dict[str, Any]: - """ - 获取消息配置 - - Returns: - 消息配置字典 - """ - return self.message_config.copy() - - def _trigger_message_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发消息回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.message_callbacks: - for callback in self.message_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 消息回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 消息回调触发失败: {e}") - - def register_message_callback(self, callback_type: str, callback: callable): - """ - 注册消息回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.message_callbacks: - self.message_callbacks[callback_type].append(callback) - print(f"✓ 消息回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 消息回调注册失败: {e}") - - def unregister_message_callback(self, callback_type: str, callback: callable): - """ - 注销消息回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.message_callbacks: - if callback in self.message_callbacks[callback_type]: - self.message_callbacks[callback_type].remove(callback) - print(f"✓ 消息回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +消息路由器模块 +处理客户端间的消息传递 +""" + +import time +import threading +import json +from typing import Dict, Any, List, Optional + +class MessageRouter: + """ + 消息路由器 + 处理客户端间的消息传递 + """ + + def __init__(self, plugin): + """ + 初始化消息路由器 + + Args: + plugin: 实时通信插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 消息路由配置 + self.message_config = { + "enable_message_filtering": True, + "max_message_size": 65536, # 64KB + "enable_message_queue": True, + "message_queue_size": 10000, + "enable_broadcast_optimization": True, + "enable_message_compression": True, + "enable_rate_limiting": True, + "max_messages_per_second_per_client": 100, + "enable_message_logging": False + } + + # 消息路由状态 + self.router_state = { + "is_processing": False, + "pending_messages": 0, + "active_routes": 0, + "last_message_time": 0.0 + } + + # 消息路由统计 + self.message_stats = { + "messages_routed": 0, + "messages_broadcast": 0, + "messages_filtered": 0, + "messages_dropped": 0, + "bytes_routed": 0, + "routing_errors": 0 + } + + # 消息处理器存储 + self.message_handlers = {} + + # 消息队列 + self.message_queue = [] + self.message_queue_lock = threading.RLock() + + # 路由表 + self.routing_table = {} + + # 回调函数 + self.message_callbacks = { + "message_routed": [], + "message_broadcast": [], + "message_filtered": [], + "message_error": [] + } + + # 时间戳记录 + self.last_queue_process = 0.0 + self.last_stats_reset = 0.0 + + print("✓ 消息路由器已创建") + + def initialize(self) -> bool: + """ + 初始化消息路由器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化消息路由器...") + + # 注册默认消息处理器 + self._register_default_handlers() + + self.initialized = True + print("✓ 消息路由器初始化完成") + return True + + except Exception as e: + print(f"✗ 消息路由器初始化失败: {e}") + self.message_stats["routing_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def _register_default_handlers(self): + """注册默认消息处理器""" + try: + # 注册系统消息处理器 + self.register_message_handler("ping", self._handle_ping_message) + self.register_message_handler("pong", self._handle_pong_message) + self.register_message_handler("chat", self._handle_chat_message) + self.register_message_handler("join_room", self._handle_join_room_message) + self.register_message_handler("leave_room", self._handle_leave_room_message) + self.register_message_handler("broadcast", self._handle_broadcast_message) + + print("✓ 默认消息处理器已注册") + + except Exception as e: + print(f"✗ 默认消息处理器注册失败: {e}") + self.message_stats["routing_errors"] += 1 + + def enable(self) -> bool: + """ + 启用消息路由器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 消息路由器未初始化") + return False + + self.enabled = True + print("✓ 消息路由器已启用") + return True + + except Exception as e: + print(f"✗ 消息路由器启用失败: {e}") + self.message_stats["routing_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用消息路由器""" + try: + self.enabled = False + + # 清空消息队列 + with self.message_queue_lock: + self.message_queue.clear() + + print("✓ 消息路由器已禁用") + + except Exception as e: + print(f"✗ 消息路由器禁用失败: {e}") + self.message_stats["routing_errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理消息路由器资源""" + try: + # 禁用消息路由器 + if self.enabled: + self.disable() + + # 清理回调和处理器 + self.message_callbacks.clear() + self.message_handlers.clear() + + self.initialized = False + print("✓ 消息路由器资源已清理") + + except Exception as e: + print(f"✗ 消息路由器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新消息路由器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 处理消息队列 + if self.message_config["enable_message_queue"]: + self._process_message_queue() + + self.router_state["last_message_time"] = current_time + + except Exception as e: + print(f"✗ 消息路由器更新失败: {e}") + self.message_stats["routing_errors"] += 1 + import traceback + traceback.print_exc() + + def _process_message_queue(self): + """处理消息队列""" + try: + if not self.message_queue: + return + + with self.message_queue_lock: + messages_to_process = self.message_queue[:] + self.message_queue.clear() + + for message_data in messages_to_process: + try: + client_id = message_data.get("client_id") + message = message_data.get("message") + + # 处理消息 + self._route_message_internal(client_id, message) + + except Exception as e: + print(f"✗ 队列消息处理失败: {e}") + self.message_stats["routing_errors"] += 1 + + self.router_state["pending_messages"] = len(self.message_queue) + + except Exception as e: + print(f"✗ 消息队列处理失败: {e}") + self.message_stats["routing_errors"] += 1 + + def register_message_handler(self, message_type: str, handler: callable): + """ + 注册消息处理器 + + Args: + message_type: 消息类型 + handler: 处理器函数 + """ + try: + self.message_handlers[message_type] = handler + print(f"✓ 消息处理器已注册: {message_type}") + except Exception as e: + print(f"✗ 消息处理器注册失败: {e}") + self.message_stats["routing_errors"] += 1 + + def unregister_message_handler(self, message_type: str): + """ + 注销消息处理器 + + Args: + message_type: 消息类型 + """ + try: + if message_type in self.message_handlers: + del self.message_handlers[message_type] + print(f"✓ 消息处理器已注销: {message_type}") + else: + print(f"✗ 消息处理器不存在: {message_type}") + except Exception as e: + print(f"✗ 消息处理器注销失败: {e}") + self.message_stats["routing_errors"] += 1 + + def route_message(self, client_id: str, message: Dict[str, Any]) -> bool: + """ + 路由消息 + + Args: + client_id: 客户端ID + message: 消息数据 + + Returns: + 是否路由成功 + """ + try: + if not self.enabled: + return False + + # 检查消息大小 + if self.message_config["enable_message_filtering"]: + message_size = len(json.dumps(message, ensure_ascii=False).encode('utf-8')) + if message_size > self.message_config["max_message_size"]: + print(f"✗ 消息过大: {message_size} bytes") + self.message_stats["messages_dropped"] += 1 + return False + + # 如果启用消息队列,将消息加入队列 + if self.message_config["enable_message_queue"]: + with self.message_queue_lock: + if len(self.message_queue) >= self.message_config["message_queue_size"]: + print("✗ 消息队列已满,丢弃消息") + self.message_stats["messages_dropped"] += 1 + return False + + self.message_queue.append({ + "client_id": client_id, + "message": message, + "timestamp": time.time() + }) + + self.router_state["pending_messages"] = len(self.message_queue) + return True + else: + # 直接处理消息 + return self._route_message_internal(client_id, message) + + except Exception as e: + print(f"✗ 消息路由失败: {e}") + self.message_stats["routing_errors"] += 1 + return False + + def _route_message_internal(self, client_id: str, message: Dict[str, Any]) -> bool: + """ + 内部消息路由处理 + + Args: + client_id: 客户端ID + message: 消息数据 + + Returns: + 是否路由成功 + """ + try: + # 更新统计信息 + self.message_stats["messages_routed"] += 1 + message_size = len(json.dumps(message, ensure_ascii=False).encode('utf-8')) + self.message_stats["bytes_routed"] += message_size + + # 触发消息路由回调 + self._trigger_message_callback("message_routed", { + "client_id": client_id, + "message": message, + "timestamp": time.time() + }) + + # 获取消息类型 + message_type = message.get("type", "unknown") + + # 查找对应的消息处理器 + if message_type in self.message_handlers: + try: + # 调用消息处理器 + handler_result = self.message_handlers[message_type](client_id, message) + return handler_result if handler_result is not None else True + except Exception as e: + print(f"✗ 消息处理器执行失败: {message_type} - {e}") + self.message_stats["routing_errors"] += 1 + return False + else: + # 默认处理:广播给所有客户端(除了发送者) + return self._handle_default_message(client_id, message) + + except Exception as e: + print(f"✗ 内部消息路由处理失败: {e}") + self.message_stats["routing_errors"] += 1 + return False + + def _handle_default_message(self, client_id: str, message: Dict[str, Any]) -> bool: + """ + 处理默认消息(广播给其他客户端) + + Args: + client_id: 发送客户端ID + message: 消息数据 + + Returns: + 是否处理成功 + """ + try: + # 广播消息给所有其他客户端 + if self.plugin.websocket_server: + broadcast_success = self.plugin.websocket_server.broadcast_message( + message, exclude_client_id=client_id) + if broadcast_success: + self.message_stats["messages_broadcast"] += 1 + return True + else: + return False + else: + return False + + except Exception as e: + print(f"✗ 默认消息处理失败: {e}") + self.message_stats["routing_errors"] += 1 + return False + + # 默认消息处理器 + def _handle_ping_message(self, client_id: str, message: Dict[str, Any]) -> bool: + """ + 处理ping消息 + + Args: + client_id: 客户端ID + message: 消息数据 + + Returns: + 是否处理成功 + """ + try: + # 更新客户端心跳 + if self.plugin.client_manager: + self.plugin.client_manager.update_client_heartbeat(client_id) + + # 回复pong消息 + pong_message = { + "type": "pong", + "timestamp": time.time(), + "ping_timestamp": message.get("timestamp", 0) + } + + if self.plugin.websocket_server: + return self.plugin.websocket_server.send_message_to_client(client_id, pong_message) + else: + return False + + except Exception as e: + print(f"✗ ping消息处理失败: {e}") + self.message_stats["routing_errors"] += 1 + return False + + def _handle_pong_message(self, client_id: str, message: Dict[str, Any]) -> bool: + """ + 处理pong消息 + + Args: + client_id: 客户端ID + message: 消息数据 + + Returns: + 是否处理成功 + """ + try: + # 更新客户端心跳 + if self.plugin.client_manager: + self.plugin.client_manager.update_client_heartbeat(client_id) + + return True + + except Exception as e: + print(f"✗ pong消息处理失败: {e}") + self.message_stats["routing_errors"] += 1 + return False + + def _handle_chat_message(self, client_id: str, message: Dict[str, Any]) -> bool: + """ + 处理聊天消息 + + Args: + client_id: 客户端ID + message: 消息数据 + + Returns: + 是否处理成功 + """ + try: + # 获取客户端信息 + client_data = self.plugin.client_manager.get_client(client_id) if self.plugin.client_manager else None + if not client_data: + return False + + # 添加发送者信息 + message["sender_id"] = client_id + message["sender_name"] = client_data.get("username", "Unknown") + message["sender_time"] = time.time() + + # 广播聊天消息 + if self.plugin.websocket_server: + broadcast_success = self.plugin.websocket_server.broadcast_message(message) + if broadcast_success: + self.message_stats["messages_broadcast"] += 1 + return broadcast_success + else: + return False + + except Exception as e: + print(f"✗ 聊天消息处理失败: {e}") + self.message_stats["routing_errors"] += 1 + return False + + def _handle_join_room_message(self, client_id: str, message: Dict[str, Any]) -> bool: + """ + 处理加入房间消息 + + Args: + client_id: 客户端ID + message: 消息数据 + + Returns: + 是否处理成功 + """ + try: + room_id = message.get("room_id") + if not room_id: + return False + + # 使用房间管理器处理加入房间请求 + if self.plugin.room_manager: + join_success = self.plugin.room_manager.add_client_to_room(room_id, client_id) + if join_success: + # 通知客户端加入成功 + response_message = { + "type": "room_joined", + "room_id": room_id, + "timestamp": time.time() + } + + if self.plugin.websocket_server: + self.plugin.websocket_server.send_message_to_client(client_id, response_message) + + return join_success + else: + return False + + except Exception as e: + print(f"✗ 加入房间消息处理失败: {e}") + self.message_stats["routing_errors"] += 1 + return False + + def _handle_leave_room_message(self, client_id: str, message: Dict[str, Any]) -> bool: + """ + 处理离开房间消息 + + Args: + client_id: 客户端ID + message: 消息数据 + + Returns: + 是否处理成功 + """ + try: + room_id = message.get("room_id") + if not room_id: + return False + + # 使用房间管理器处理离开房间请求 + if self.plugin.room_manager: + leave_success = self.plugin.room_manager.remove_client_from_room(room_id, client_id) + if leave_success: + # 通知客户端离开成功 + response_message = { + "type": "room_left", + "room_id": room_id, + "timestamp": time.time() + } + + if self.plugin.websocket_server: + self.plugin.websocket_server.send_message_to_client(client_id, response_message) + + return leave_success + else: + return False + + except Exception as e: + print(f"✗ 离开房间消息处理失败: {e}") + self.message_stats["routing_errors"] += 1 + return False + + def _handle_broadcast_message(self, client_id: str, message: Dict[str, Any]) -> bool: + """ + 处理广播消息 + + Args: + client_id: 客户端ID + message: 消息数据 + + Returns: + 是否处理成功 + """ + try: + # 广播消息给所有客户端 + if self.plugin.websocket_server: + broadcast_success = self.plugin.websocket_server.broadcast_message(message) + if broadcast_success: + self.message_stats["messages_broadcast"] += 1 + return broadcast_success + else: + return False + + except Exception as e: + print(f"✗ 广播消息处理失败: {e}") + self.message_stats["routing_errors"] += 1 + return False + + def send_direct_message(self, from_client_id: str, to_client_id: str, message: Dict[str, Any]) -> bool: + """ + 发送直接消息 + + Args: + from_client_id: 发送客户端ID + to_client_id: 接收客户端ID + message: 消息数据 + + Returns: + 是否发送成功 + """ + try: + if not self.enabled: + return False + + # 添加发送者信息 + message["sender_id"] = from_client_id + message["timestamp"] = time.time() + + # 发送消息到目标客户端 + if self.plugin.websocket_server: + send_success = self.plugin.websocket_server.send_message_to_client(to_client_id, message) + if send_success: + self.message_stats["messages_routed"] += 1 + return send_success + else: + return False + + except Exception as e: + print(f"✗ 直接消息发送失败: {e}") + self.message_stats["routing_errors"] += 1 + return False + + def broadcast_to_room(self, room_id: str, message: Dict[str, Any], exclude_client_id: str = None) -> bool: + """ + 广播消息到房间 + + Args: + room_id: 房间ID + message: 消息数据 + exclude_client_id: 要排除的客户端ID + + Returns: + 是否广播成功 + """ + try: + if not self.enabled: + return False + + # 使用房间管理器获取房间内的客户端 + if self.plugin.room_manager: + room_clients = self.plugin.room_manager.get_room_clients(room_id) + if not room_clients: + return False + + success_count = 0 + for client_id in room_clients: + # 跳过排除的客户端 + if client_id == exclude_client_id: + continue + + # 发送消息 + if self.plugin.websocket_server: + send_success = self.plugin.websocket_server.send_message_to_client(client_id, message) + if send_success: + success_count += 1 + + return success_count > 0 + else: + return False + + except Exception as e: + print(f"✗ 房间广播失败: {e}") + self.message_stats["routing_errors"] += 1 + return False + + def get_message_stats(self) -> Dict[str, Any]: + """ + 获取消息统计信息 + + Returns: + 消息统计字典 + """ + return { + "state": self.router_state.copy(), + "stats": self.message_stats.copy(), + "config": self.message_config.copy() + } + + def reset_stats(self): + """重置消息统计信息""" + try: + self.message_stats = { + "messages_routed": 0, + "messages_broadcast": 0, + "messages_filtered": 0, + "messages_dropped": 0, + "bytes_routed": 0, + "routing_errors": 0 + } + print("✓ 消息统计信息已重置") + except Exception as e: + print(f"✗ 消息统计信息重置失败: {e}") + + def set_message_config(self, config: Dict[str, Any]) -> bool: + """ + 设置消息配置 + + Args: + config: 消息配置字典 + + Returns: + 是否设置成功 + """ + try: + self.message_config.update(config) + print(f"✓ 消息配置已更新: {self.message_config}") + return True + except Exception as e: + print(f"✗ 消息配置设置失败: {e}") + return False + + def get_message_config(self) -> Dict[str, Any]: + """ + 获取消息配置 + + Returns: + 消息配置字典 + """ + return self.message_config.copy() + + def _trigger_message_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发消息回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.message_callbacks: + for callback in self.message_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 消息回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 消息回调触发失败: {e}") + + def register_message_callback(self, callback_type: str, callback: callable): + """ + 注册消息回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.message_callbacks: + self.message_callbacks[callback_type].append(callback) + print(f"✓ 消息回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 消息回调注册失败: {e}") + + def unregister_message_callback(self, callback_type: str, callback: callable): + """ + 注销消息回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.message_callbacks: + if callback in self.message_callbacks[callback_type]: + self.message_callbacks[callback_type].remove(callback) + print(f"✓ 消息回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 消息回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/realtime_communication/monitoring/communication_monitor.py b/plugins/user/realtime_communication/monitoring/communication_monitor.py index 05b17d93..614b6058 100644 --- a/plugins/user/realtime_communication/monitoring/communication_monitor.py +++ b/plugins/user/realtime_communication/monitoring/communication_monitor.py @@ -1,825 +1,825 @@ -""" -通信监控模块 -实时监控通信性能和记录日志 -""" - -import time -import threading -import json -import os -from typing import Dict, Any, List, Optional -from collections import deque - -class CommunicationMonitor: - """ - 通信监控器 - 实时监控通信性能和记录日志 - """ - - def __init__(self, plugin): - """ - 初始化通信监控器 - - Args: - plugin: 实时通信插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 监控配置 - self.monitor_config = { - "enable_monitoring": True, - "monitoring_interval": 5.0, - "log_level": "INFO", - "enable_file_logging": True, - "log_file_path": "/home/hello/EG/plugins/user/realtime_communication/logs/communication_monitor.log", - "max_log_file_size": 10485760, # 10MB - "enable_performance_monitoring": True, - "enable_alerts": True, - "alert_thresholds": { - "cpu_usage": 80.0, - "memory_usage": 85.0, - "network_latency": 100.0, - "error_rate": 5.0, - "message_queue_size": 1000 - }, - "enable_metrics_collection": True, - "metrics_retention_period": 3600, # 1小时 - "enable_realtime_dashboard": True - } - - # 性能指标存储 - self.performance_metrics = { - "cpu_usage": deque(maxlen=1000), - "memory_usage": deque(maxlen=1000), - "network_in": deque(maxlen=1000), - "network_out": deque(maxlen=1000), - "active_connections": deque(maxlen=1000), - "messages_per_second": deque(maxlen=1000), - "message_queue_size": deque(maxlen=1000), - "error_rate": deque(maxlen=1000) - } - - # 监控状态 - self.monitor_state = { - "last_monitoring_update": 0.0, - "last_performance_check": 0.0, - "total_logs": 0, - "total_alerts": 0, - "active_alerts": 0 - } - - # 警报系统 - self.active_alerts = {} - self.alert_history = deque(maxlen=1000) - - # 日志文件 - self.log_file = None - self.log_file_size = 0 - - # 监控统计 - self.monitor_stats = { - "metrics_collected": 0, - "logs_written": 0, - "alerts_generated": 0, - "alerts_resolved": 0, - "performance_checks": 0, - "monitor_errors": 0 - } - - # 线程锁 - self.monitor_lock = threading.RLock() - - # 回调函数 - self.monitor_callbacks = { - "metric_collected": [], - "alert_triggered": [], - "alert_resolved": [], - "log_entry": [], - "performance_degraded": [] - } - - # 监控线程 - self.monitor_thread = None - self.monitor_thread_running = False - - # 时间戳记录 - self.last_log_write = 0.0 - self.last_metric_collection = 0.0 - - print("✓ 通信监控器已创建") - - def initialize(self) -> bool: - """ - 初始化通信监控器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化通信监控器...") - - # 创建日志目录 - log_dir = os.path.dirname(self.monitor_config["log_file_path"]) - if not os.path.exists(log_dir): - os.makedirs(log_dir) - - # 初始化日志文件 - if self.monitor_config["enable_file_logging"]: - self._initialize_log_file() - - # 启动监控线程 - self._start_monitor_thread() - - self.initialized = True - print("✓ 通信监控器初始化完成") - return True - - except Exception as e: - print(f"✗ 通信监控器初始化失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def _initialize_log_file(self): - """初始化日志文件""" - try: - # 检查文件是否存在 - if os.path.exists(self.monitor_config["log_file_path"]): - self.log_file_size = os.path.getsize(self.monitor_config["log_file_path"]) - - # 打开日志文件 - self.log_file = open(self.monitor_config["log_file_path"], "a", encoding="utf-8") - print(f"✓ 日志文件已初始化: {self.monitor_config['log_file_path']}") - - except Exception as e: - print(f"✗ 日志文件初始化失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - - def _start_monitor_thread(self): - """启动监控线程""" - try: - self.monitor_thread_running = True - self.monitor_thread = threading.Thread(target=self._monitor_loop, daemon=True) - self.monitor_thread.start() - print("✓ 监控线程已启动") - except Exception as e: - print(f"✗ 监控线程启动失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - - def _monitor_loop(self): - """监控线程循环""" - try: - while self.monitor_thread_running: - try: - if self.enabled and self.monitor_config["enable_monitoring"]: - current_time = time.time() - - # 收集性能指标 - if current_time - self.last_metric_collection >= self.monitor_config["monitoring_interval"]: - self._collect_performance_metrics() - self.last_metric_collection = current_time - - # 检查警报 - self._check_alerts() - - # 检查日志文件大小 - self._check_log_file_size() - - # 短暂休眠 - time.sleep(1.0) - - except Exception as e: - print(f"✗ 监控循环错误: {e}") - self.monitor_stats["monitor_errors"] += 1 - time.sleep(5.0) # 出错时延长休眠 - - except Exception as e: - print(f"✗ 监控线程失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - - def enable(self) -> bool: - """ - 启用通信监控器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 通信监控器未初始化") - return False - - self.enabled = True - print("✓ 通信监控器已启用") - return True - - except Exception as e: - print(f"✗ 通信监控器启用失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用通信监控器""" - try: - self.enabled = False - - # 停止监控线程 - if self.monitor_thread_running: - self.monitor_thread_running = False - if self.monitor_thread and self.monitor_thread.is_alive(): - self.monitor_thread.join(timeout=5.0) - - # 关闭日志文件 - if self.log_file: - self.log_file.close() - self.log_file = None - - print("✓ 通信监控器已禁用") - - except Exception as e: - print(f"✗ 通信监控器禁用失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理通信监控器资源""" - try: - # 禁用通信监控器 - if self.enabled: - self.disable() - - # 清理回调 - self.monitor_callbacks.clear() - - self.initialized = False - print("✓ 通信监控器资源已清理") - - except Exception as e: - print(f"✗ 通信监控器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新通信监控器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.monitor_state["last_monitoring_update"] = current_time - - except Exception as e: - print(f"✗ 通信监控器更新失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - import traceback - traceback.print_exc() - - def _collect_performance_metrics(self): - """收集性能指标""" - try: - current_time = time.time() - metrics = {} - - # 收集系统资源使用情况 - try: - import psutil - - # CPU使用率 - cpu_percent = psutil.cpu_percent(interval=0.1) - metrics["cpu_usage"] = cpu_percent - self.performance_metrics["cpu_usage"].append((current_time, cpu_percent)) - - # 内存使用率 - memory_info = psutil.virtual_memory() - memory_percent = memory_info.percent - metrics["memory_usage"] = memory_percent - self.performance_metrics["memory_usage"].append((current_time, memory_percent)) - - # 网络使用情况 - net_io = psutil.net_io_counters() - metrics["network_in"] = net_io.bytes_recv - metrics["network_out"] = net_io.bytes_sent - self.performance_metrics["network_in"].append((current_time, net_io.bytes_recv)) - self.performance_metrics["network_out"].append((current_time, net_io.bytes_sent)) - - except ImportError: - # 如果没有psutil,使用简化的指标 - metrics["cpu_usage"] = 0.0 - metrics["memory_usage"] = 0.0 - metrics["network_in"] = 0 - metrics["network_out"] = 0 - - # 收集服务器指标 - if self.plugin.websocket_server: - server_stats = self.plugin.websocket_server.get_server_stats() - server_state = server_stats.get("state", {}) - - # 活跃连接数 - active_connections = server_state.get("current_connections", 0) - metrics["active_connections"] = active_connections - self.performance_metrics["active_connections"].append((current_time, active_connections)) - - # 数据传输量 - bytes_sent = server_state.get("bytes_sent", 0) - bytes_received = server_state.get("bytes_received", 0) - metrics["bytes_sent"] = bytes_sent - metrics["bytes_received"] = bytes_received - - # 收集客户端指标 - if self.plugin.client_manager: - client_stats = self.plugin.client_manager.get_client_stats() - client_state = client_stats.get("state", {}) - client_count = client_state.get("current_clients", 0) - metrics["client_count"] = client_count - - # 收集消息指标 - if self.plugin.message_router: - message_stats = self.plugin.message_router.get_message_stats() - message_state = message_stats.get("state", {}) - message_stats_data = message_stats.get("stats", {}) - - # 消息队列大小 - pending_messages = message_state.get("pending_messages", 0) - metrics["message_queue_size"] = pending_messages - self.performance_metrics["message_queue_size"].append((current_time, pending_messages)) - - # 消息处理速率 - messages_routed = message_stats_data.get("messages_routed", 0) - if hasattr(self, '_last_messages_routed'): - mps = (messages_routed - self._last_messages_routed) / self.monitor_config["monitoring_interval"] - metrics["messages_per_second"] = max(0, mps) - self.performance_metrics["messages_per_second"].append((current_time, metrics["messages_per_second"])) - else: - metrics["messages_per_second"] = 0 - self.performance_metrics["messages_per_second"].append((current_time, 0)) - - self._last_messages_routed = messages_routed - - # 收集房间指标 - if self.plugin.room_manager: - room_stats = self.plugin.room_manager.get_room_stats() - room_state = room_stats.get("state", {}) - active_rooms = room_state.get("active_rooms", 0) - metrics["active_rooms"] = active_rooms - - # 更新统计 - self.monitor_stats["metrics_collected"] += 1 - self.last_metric_collection = current_time - - # 触发指标收集回调 - self._trigger_monitor_callback("metric_collected", { - "metrics": metrics, - "timestamp": current_time - }) - - # 记录日志 - if self._should_log("DEBUG"): - self._write_log("DEBUG", f"性能指标收集: {metrics}") - - except Exception as e: - print(f"✗ 性能指标收集失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - - def _check_alerts(self): - """检查警报条件""" - try: - if not self.monitor_config["enable_alerts"]: - return - - current_time = time.time() - thresholds = self.monitor_config["alert_thresholds"] - - # 检查CPU使用率 - if self.performance_metrics["cpu_usage"]: - latest_cpu = self.performance_metrics["cpu_usage"][-1][1] - if latest_cpu > thresholds["cpu_usage"]: - self._trigger_alert("high_cpu_usage", f"CPU使用率过高: {latest_cpu:.2f}%", current_time) - - # 检查内存使用率 - if self.performance_metrics["memory_usage"]: - latest_memory = self.performance_metrics["memory_usage"][-1][1] - if latest_memory > thresholds["memory_usage"]: - self._trigger_alert("high_memory_usage", f"内存使用率过高: {latest_memory:.2f}%", current_time) - - # 检查消息队列大小 - if self.performance_metrics["message_queue_size"]: - latest_queue_size = self.performance_metrics["message_queue_size"][-1][1] - if latest_queue_size > thresholds["message_queue_size"]: - self._trigger_alert("high_message_queue", f"消息队列过大: {latest_queue_size}", current_time) - - # 检查错误率 - if self.performance_metrics["error_rate"]: - latest_error_rate = self.performance_metrics["error_rate"][-1][1] - if latest_error_rate > thresholds["error_rate"]: - self._trigger_alert("high_error_rate", f"错误率过高: {latest_error_rate:.2f}%", current_time) - - # 检查活跃连接数 - if self.performance_metrics["active_connections"]: - latest_connections = self.performance_metrics["active_connections"][-1][1] - if self.plugin.websocket_server: - max_connections = self.plugin.websocket_server.websocket_config.get("max_connections", 1000) - if latest_connections > max_connections * 0.9: # 90%阈值 - self._trigger_alert("high_connections", f"连接数接近上限: {latest_connections}/{max_connections}", current_time) - - except Exception as e: - print(f"✗ 警报检查失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - - def _trigger_alert(self, alert_type: str, message: str, timestamp: float = None): - """ - 触发警报 - - Args: - alert_type: 警报类型 - message: 警报消息 - timestamp: 时间戳 - """ - try: - if timestamp is None: - timestamp = time.time() - - alert_id = f"{alert_type}_{int(timestamp)}" - - # 检查是否已存在相同类型的活动警报 - with self.monitor_lock: - if alert_type in self.active_alerts: - # 更新现有警报 - self.active_alerts[alert_type]["count"] += 1 - self.active_alerts[alert_type]["last_triggered"] = timestamp - else: - # 创建新警报 - self.active_alerts[alert_type] = { - "id": alert_id, - "type": alert_type, - "message": message, - "first_triggered": timestamp, - "last_triggered": timestamp, - "count": 1, - "resolved": False - } - - self.monitor_state["active_alerts"] += 1 - self.monitor_stats["alerts_generated"] += 1 - - # 添加到警报历史 - self.alert_history.append({ - "id": alert_id, - "type": alert_type, - "message": message, - "timestamp": timestamp, - "resolved": False - }) - - # 触发警报回调 - self._trigger_monitor_callback("alert_triggered", { - "alert_id": alert_id, - "alert_type": alert_type, - "message": message, - "timestamp": timestamp - }) - - # 记录日志 - self._write_log("WARNING", f"警报触发 [{alert_type}]: {message}") - - except Exception as e: - print(f"✗ 警报触发失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - - def resolve_alert(self, alert_type: str): - """ - 解决警报 - - Args: - alert_type: 警报类型 - """ - try: - with self.monitor_lock: - if alert_type in self.active_alerts: - alert_data = self.active_alerts[alert_type] - alert_data["resolved"] = True - alert_data["resolved_time"] = time.time() - - self.monitor_state["active_alerts"] -= 1 - self.monitor_stats["alerts_resolved"] += 1 - - # 触发警报解决回调 - self._trigger_monitor_callback("alert_resolved", { - "alert_type": alert_type, - "alert_data": alert_data - }) - - # 从活动警报中移除 - del self.active_alerts[alert_type] - - # 记录日志 - self._write_log("INFO", f"警报已解决: {alert_type}") - - except Exception as e: - print(f"✗ 警报解决失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - - def _check_log_file_size(self): - """检查日志文件大小""" - try: - if not self.monitor_config["enable_file_logging"] or not self.log_file: - return - - # 检查文件大小 - if os.path.exists(self.monitor_config["log_file_path"]): - current_size = os.path.getsize(self.monitor_config["log_file_path"]) - if current_size > self.monitor_config["max_log_file_size"]: - # 轮转日志文件 - self._rotate_log_file() - - except Exception as e: - print(f"✗ 日志文件大小检查失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - - def _rotate_log_file(self): - """轮转日志文件""" - try: - if self.log_file: - self.log_file.close() - - # 重命名当前日志文件 - timestamp = int(time.time()) - rotated_filename = f"{self.monitor_config['log_file_path']}.{timestamp}" - os.rename(self.monitor_config["log_file_path"], rotated_filename) - - # 重新初始化日志文件 - self._initialize_log_file() - - print(f"✓ 日志文件已轮转: {rotated_filename}") - - except Exception as e: - print(f"✗ 日志文件轮转失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - - def _write_log(self, level: str, message: str, extra_data: Dict[str, Any] = None): - """ - 写入日志 - - Args: - level: 日志级别 - message: 日志消息 - extra_data: 额外数据 - """ - try: - if not self._should_log(level): - return - - timestamp = time.time() - log_entry = { - "timestamp": timestamp, - "level": level, - "message": message, - "extra_data": extra_data - } - - # 控制台输出 - print(f"[{level}] {message}") - - # 文件日志 - if self.monitor_config["enable_file_logging"] and self.log_file: - try: - log_line = json.dumps(log_entry, ensure_ascii=False) - self.log_file.write(log_line + "\n") - self.log_file.flush() - - self.log_file_size += len(log_line) + 1 - self.monitor_stats["logs_written"] += 1 - self.monitor_state["total_logs"] += 1 - - except Exception as e: - print(f"✗ 文件日志写入失败: {e}") - - # 触发日志条目回调 - self._trigger_monitor_callback("log_entry", log_entry) - - self.last_log_write = timestamp - - except Exception as e: - print(f"✗ 日志写入失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - - def _should_log(self, level: str) -> bool: - """ - 检查是否应该记录指定级别的日志 - - Args: - level: 日志级别 - - Returns: - 是否应该记录日志 - """ - try: - log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] - current_level = self.monitor_config["log_level"] - - if current_level not in log_levels: - return True # 默认记录所有日志 - - if level not in log_levels: - return True # 默认记录未知级别的日志 - - current_index = log_levels.index(current_level) - level_index = log_levels.index(level) - - # 只记录等于或高于当前级别的日志 - return level_index >= current_index - - except Exception as e: - print(f"✗ 日志级别检查失败: {e}") - return True - - def get_performance_metrics(self, metric_name: str = None, limit: int = 100) -> Any: - """ - 获取性能指标 - - Args: - metric_name: 指标名称(可选) - limit: 限制返回的数量 - - Returns: - 性能指标数据 - """ - try: - with self.monitor_lock: - if metric_name: - if metric_name in self.performance_metrics: - metrics = list(self.performance_metrics[metric_name]) - return metrics[-limit:] if len(metrics) > limit else metrics - else: - return [] - else: - # 返回所有指标 - result = {} - for name, metrics in self.performance_metrics.items(): - metric_list = list(metrics) - result[name] = metric_list[-limit:] if len(metric_list) > limit else metric_list - return result - - except Exception as e: - print(f"✗ 获取性能指标失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - return {} - - def get_active_alerts(self) -> Dict[str, Any]: - """ - 获取活动警报 - - Returns: - 活动警报字典 - """ - try: - with self.monitor_lock: - return self.active_alerts.copy() - except Exception as e: - print(f"✗ 获取活动警报失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - return {} - - def get_alert_history(self, limit: int = 100) -> List[Dict[str, Any]]: - """ - 获取警报历史 - - Args: - limit: 限制返回的数量 - - Returns: - 警报历史列表 - """ - try: - with self.monitor_lock: - history = list(self.alert_history) - return history[-limit:] if len(history) > limit else history - except Exception as e: - print(f"✗ 获取警报历史失败: {e}") - self.monitor_stats["monitor_errors"] += 1 - return [] - - def get_monitor_stats(self) -> Dict[str, Any]: - """ - 获取监控统计信息 - - Returns: - 监控统计字典 - """ - return { - "state": self.monitor_state.copy(), - "stats": self.monitor_stats.copy(), - "config": self.monitor_config.copy(), - "active_alerts_count": len(self.active_alerts), - "alert_history_count": len(self.alert_history) - } - - def reset_stats(self): - """重置监控统计信息""" - try: - self.monitor_stats = { - "metrics_collected": 0, - "logs_written": 0, - "alerts_generated": 0, - "alerts_resolved": 0, - "performance_checks": 0, - "monitor_errors": 0 - } - - self.monitor_state["total_logs"] = 0 - self.monitor_state["total_alerts"] = 0 - self.monitor_state["active_alerts"] = 0 - - print("✓ 监控统计信息已重置") - except Exception as e: - print(f"✗ 监控统计信息重置失败: {e}") - - def set_monitor_config(self, config: Dict[str, Any]) -> bool: - """ - 设置监控配置 - - Args: - config: 监控配置字典 - - Returns: - 是否设置成功 - """ - try: - self.monitor_config.update(config) - print(f"✓ 监控配置已更新: {self.monitor_config}") - return True - except Exception as e: - print(f"✗ 监控配置设置失败: {e}") - return False - - def get_monitor_config(self) -> Dict[str, Any]: - """ - 获取监控配置 - - Returns: - 监控配置字典 - """ - return self.monitor_config.copy() - - def _trigger_monitor_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发监控回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.monitor_callbacks: - for callback in self.monitor_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 监控回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 监控回调触发失败: {e}") - - def register_monitor_callback(self, callback_type: str, callback: callable): - """ - 注册监控回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.monitor_callbacks: - self.monitor_callbacks[callback_type].append(callback) - print(f"✓ 监控回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 监控回调注册失败: {e}") - - def unregister_monitor_callback(self, callback_type: str, callback: callable): - """ - 注销监控回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.monitor_callbacks: - if callback in self.monitor_callbacks[callback_type]: - self.monitor_callbacks[callback_type].remove(callback) - print(f"✓ 监控回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +通信监控模块 +实时监控通信性能和记录日志 +""" + +import time +import threading +import json +import os +from typing import Dict, Any, List, Optional +from collections import deque + +class CommunicationMonitor: + """ + 通信监控器 + 实时监控通信性能和记录日志 + """ + + def __init__(self, plugin): + """ + 初始化通信监控器 + + Args: + plugin: 实时通信插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 监控配置 + self.monitor_config = { + "enable_monitoring": True, + "monitoring_interval": 5.0, + "log_level": "INFO", + "enable_file_logging": True, + "log_file_path": "/home/hello/EG/plugins/user/realtime_communication/logs/communication_monitor.log", + "max_log_file_size": 10485760, # 10MB + "enable_performance_monitoring": True, + "enable_alerts": True, + "alert_thresholds": { + "cpu_usage": 80.0, + "memory_usage": 85.0, + "network_latency": 100.0, + "error_rate": 5.0, + "message_queue_size": 1000 + }, + "enable_metrics_collection": True, + "metrics_retention_period": 3600, # 1小时 + "enable_realtime_dashboard": True + } + + # 性能指标存储 + self.performance_metrics = { + "cpu_usage": deque(maxlen=1000), + "memory_usage": deque(maxlen=1000), + "network_in": deque(maxlen=1000), + "network_out": deque(maxlen=1000), + "active_connections": deque(maxlen=1000), + "messages_per_second": deque(maxlen=1000), + "message_queue_size": deque(maxlen=1000), + "error_rate": deque(maxlen=1000) + } + + # 监控状态 + self.monitor_state = { + "last_monitoring_update": 0.0, + "last_performance_check": 0.0, + "total_logs": 0, + "total_alerts": 0, + "active_alerts": 0 + } + + # 警报系统 + self.active_alerts = {} + self.alert_history = deque(maxlen=1000) + + # 日志文件 + self.log_file = None + self.log_file_size = 0 + + # 监控统计 + self.monitor_stats = { + "metrics_collected": 0, + "logs_written": 0, + "alerts_generated": 0, + "alerts_resolved": 0, + "performance_checks": 0, + "monitor_errors": 0 + } + + # 线程锁 + self.monitor_lock = threading.RLock() + + # 回调函数 + self.monitor_callbacks = { + "metric_collected": [], + "alert_triggered": [], + "alert_resolved": [], + "log_entry": [], + "performance_degraded": [] + } + + # 监控线程 + self.monitor_thread = None + self.monitor_thread_running = False + + # 时间戳记录 + self.last_log_write = 0.0 + self.last_metric_collection = 0.0 + + print("✓ 通信监控器已创建") + + def initialize(self) -> bool: + """ + 初始化通信监控器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化通信监控器...") + + # 创建日志目录 + log_dir = os.path.dirname(self.monitor_config["log_file_path"]) + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + # 初始化日志文件 + if self.monitor_config["enable_file_logging"]: + self._initialize_log_file() + + # 启动监控线程 + self._start_monitor_thread() + + self.initialized = True + print("✓ 通信监控器初始化完成") + return True + + except Exception as e: + print(f"✗ 通信监控器初始化失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def _initialize_log_file(self): + """初始化日志文件""" + try: + # 检查文件是否存在 + if os.path.exists(self.monitor_config["log_file_path"]): + self.log_file_size = os.path.getsize(self.monitor_config["log_file_path"]) + + # 打开日志文件 + self.log_file = open(self.monitor_config["log_file_path"], "a", encoding="utf-8") + print(f"✓ 日志文件已初始化: {self.monitor_config['log_file_path']}") + + except Exception as e: + print(f"✗ 日志文件初始化失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + + def _start_monitor_thread(self): + """启动监控线程""" + try: + self.monitor_thread_running = True + self.monitor_thread = threading.Thread(target=self._monitor_loop, daemon=True) + self.monitor_thread.start() + print("✓ 监控线程已启动") + except Exception as e: + print(f"✗ 监控线程启动失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + + def _monitor_loop(self): + """监控线程循环""" + try: + while self.monitor_thread_running: + try: + if self.enabled and self.monitor_config["enable_monitoring"]: + current_time = time.time() + + # 收集性能指标 + if current_time - self.last_metric_collection >= self.monitor_config["monitoring_interval"]: + self._collect_performance_metrics() + self.last_metric_collection = current_time + + # 检查警报 + self._check_alerts() + + # 检查日志文件大小 + self._check_log_file_size() + + # 短暂休眠 + time.sleep(1.0) + + except Exception as e: + print(f"✗ 监控循环错误: {e}") + self.monitor_stats["monitor_errors"] += 1 + time.sleep(5.0) # 出错时延长休眠 + + except Exception as e: + print(f"✗ 监控线程失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + + def enable(self) -> bool: + """ + 启用通信监控器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 通信监控器未初始化") + return False + + self.enabled = True + print("✓ 通信监控器已启用") + return True + + except Exception as e: + print(f"✗ 通信监控器启用失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用通信监控器""" + try: + self.enabled = False + + # 停止监控线程 + if self.monitor_thread_running: + self.monitor_thread_running = False + if self.monitor_thread and self.monitor_thread.is_alive(): + self.monitor_thread.join(timeout=5.0) + + # 关闭日志文件 + if self.log_file: + self.log_file.close() + self.log_file = None + + print("✓ 通信监控器已禁用") + + except Exception as e: + print(f"✗ 通信监控器禁用失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理通信监控器资源""" + try: + # 禁用通信监控器 + if self.enabled: + self.disable() + + # 清理回调 + self.monitor_callbacks.clear() + + self.initialized = False + print("✓ 通信监控器资源已清理") + + except Exception as e: + print(f"✗ 通信监控器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新通信监控器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.monitor_state["last_monitoring_update"] = current_time + + except Exception as e: + print(f"✗ 通信监控器更新失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + import traceback + traceback.print_exc() + + def _collect_performance_metrics(self): + """收集性能指标""" + try: + current_time = time.time() + metrics = {} + + # 收集系统资源使用情况 + try: + import psutil + + # CPU使用率 + cpu_percent = psutil.cpu_percent(interval=0.1) + metrics["cpu_usage"] = cpu_percent + self.performance_metrics["cpu_usage"].append((current_time, cpu_percent)) + + # 内存使用率 + memory_info = psutil.virtual_memory() + memory_percent = memory_info.percent + metrics["memory_usage"] = memory_percent + self.performance_metrics["memory_usage"].append((current_time, memory_percent)) + + # 网络使用情况 + net_io = psutil.net_io_counters() + metrics["network_in"] = net_io.bytes_recv + metrics["network_out"] = net_io.bytes_sent + self.performance_metrics["network_in"].append((current_time, net_io.bytes_recv)) + self.performance_metrics["network_out"].append((current_time, net_io.bytes_sent)) + + except ImportError: + # 如果没有psutil,使用简化的指标 + metrics["cpu_usage"] = 0.0 + metrics["memory_usage"] = 0.0 + metrics["network_in"] = 0 + metrics["network_out"] = 0 + + # 收集服务器指标 + if self.plugin.websocket_server: + server_stats = self.plugin.websocket_server.get_server_stats() + server_state = server_stats.get("state", {}) + + # 活跃连接数 + active_connections = server_state.get("current_connections", 0) + metrics["active_connections"] = active_connections + self.performance_metrics["active_connections"].append((current_time, active_connections)) + + # 数据传输量 + bytes_sent = server_state.get("bytes_sent", 0) + bytes_received = server_state.get("bytes_received", 0) + metrics["bytes_sent"] = bytes_sent + metrics["bytes_received"] = bytes_received + + # 收集客户端指标 + if self.plugin.client_manager: + client_stats = self.plugin.client_manager.get_client_stats() + client_state = client_stats.get("state", {}) + client_count = client_state.get("current_clients", 0) + metrics["client_count"] = client_count + + # 收集消息指标 + if self.plugin.message_router: + message_stats = self.plugin.message_router.get_message_stats() + message_state = message_stats.get("state", {}) + message_stats_data = message_stats.get("stats", {}) + + # 消息队列大小 + pending_messages = message_state.get("pending_messages", 0) + metrics["message_queue_size"] = pending_messages + self.performance_metrics["message_queue_size"].append((current_time, pending_messages)) + + # 消息处理速率 + messages_routed = message_stats_data.get("messages_routed", 0) + if hasattr(self, '_last_messages_routed'): + mps = (messages_routed - self._last_messages_routed) / self.monitor_config["monitoring_interval"] + metrics["messages_per_second"] = max(0, mps) + self.performance_metrics["messages_per_second"].append((current_time, metrics["messages_per_second"])) + else: + metrics["messages_per_second"] = 0 + self.performance_metrics["messages_per_second"].append((current_time, 0)) + + self._last_messages_routed = messages_routed + + # 收集房间指标 + if self.plugin.room_manager: + room_stats = self.plugin.room_manager.get_room_stats() + room_state = room_stats.get("state", {}) + active_rooms = room_state.get("active_rooms", 0) + metrics["active_rooms"] = active_rooms + + # 更新统计 + self.monitor_stats["metrics_collected"] += 1 + self.last_metric_collection = current_time + + # 触发指标收集回调 + self._trigger_monitor_callback("metric_collected", { + "metrics": metrics, + "timestamp": current_time + }) + + # 记录日志 + if self._should_log("DEBUG"): + self._write_log("DEBUG", f"性能指标收集: {metrics}") + + except Exception as e: + print(f"✗ 性能指标收集失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + + def _check_alerts(self): + """检查警报条件""" + try: + if not self.monitor_config["enable_alerts"]: + return + + current_time = time.time() + thresholds = self.monitor_config["alert_thresholds"] + + # 检查CPU使用率 + if self.performance_metrics["cpu_usage"]: + latest_cpu = self.performance_metrics["cpu_usage"][-1][1] + if latest_cpu > thresholds["cpu_usage"]: + self._trigger_alert("high_cpu_usage", f"CPU使用率过高: {latest_cpu:.2f}%", current_time) + + # 检查内存使用率 + if self.performance_metrics["memory_usage"]: + latest_memory = self.performance_metrics["memory_usage"][-1][1] + if latest_memory > thresholds["memory_usage"]: + self._trigger_alert("high_memory_usage", f"内存使用率过高: {latest_memory:.2f}%", current_time) + + # 检查消息队列大小 + if self.performance_metrics["message_queue_size"]: + latest_queue_size = self.performance_metrics["message_queue_size"][-1][1] + if latest_queue_size > thresholds["message_queue_size"]: + self._trigger_alert("high_message_queue", f"消息队列过大: {latest_queue_size}", current_time) + + # 检查错误率 + if self.performance_metrics["error_rate"]: + latest_error_rate = self.performance_metrics["error_rate"][-1][1] + if latest_error_rate > thresholds["error_rate"]: + self._trigger_alert("high_error_rate", f"错误率过高: {latest_error_rate:.2f}%", current_time) + + # 检查活跃连接数 + if self.performance_metrics["active_connections"]: + latest_connections = self.performance_metrics["active_connections"][-1][1] + if self.plugin.websocket_server: + max_connections = self.plugin.websocket_server.websocket_config.get("max_connections", 1000) + if latest_connections > max_connections * 0.9: # 90%阈值 + self._trigger_alert("high_connections", f"连接数接近上限: {latest_connections}/{max_connections}", current_time) + + except Exception as e: + print(f"✗ 警报检查失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + + def _trigger_alert(self, alert_type: str, message: str, timestamp: float = None): + """ + 触发警报 + + Args: + alert_type: 警报类型 + message: 警报消息 + timestamp: 时间戳 + """ + try: + if timestamp is None: + timestamp = time.time() + + alert_id = f"{alert_type}_{int(timestamp)}" + + # 检查是否已存在相同类型的活动警报 + with self.monitor_lock: + if alert_type in self.active_alerts: + # 更新现有警报 + self.active_alerts[alert_type]["count"] += 1 + self.active_alerts[alert_type]["last_triggered"] = timestamp + else: + # 创建新警报 + self.active_alerts[alert_type] = { + "id": alert_id, + "type": alert_type, + "message": message, + "first_triggered": timestamp, + "last_triggered": timestamp, + "count": 1, + "resolved": False + } + + self.monitor_state["active_alerts"] += 1 + self.monitor_stats["alerts_generated"] += 1 + + # 添加到警报历史 + self.alert_history.append({ + "id": alert_id, + "type": alert_type, + "message": message, + "timestamp": timestamp, + "resolved": False + }) + + # 触发警报回调 + self._trigger_monitor_callback("alert_triggered", { + "alert_id": alert_id, + "alert_type": alert_type, + "message": message, + "timestamp": timestamp + }) + + # 记录日志 + self._write_log("WARNING", f"警报触发 [{alert_type}]: {message}") + + except Exception as e: + print(f"✗ 警报触发失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + + def resolve_alert(self, alert_type: str): + """ + 解决警报 + + Args: + alert_type: 警报类型 + """ + try: + with self.monitor_lock: + if alert_type in self.active_alerts: + alert_data = self.active_alerts[alert_type] + alert_data["resolved"] = True + alert_data["resolved_time"] = time.time() + + self.monitor_state["active_alerts"] -= 1 + self.monitor_stats["alerts_resolved"] += 1 + + # 触发警报解决回调 + self._trigger_monitor_callback("alert_resolved", { + "alert_type": alert_type, + "alert_data": alert_data + }) + + # 从活动警报中移除 + del self.active_alerts[alert_type] + + # 记录日志 + self._write_log("INFO", f"警报已解决: {alert_type}") + + except Exception as e: + print(f"✗ 警报解决失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + + def _check_log_file_size(self): + """检查日志文件大小""" + try: + if not self.monitor_config["enable_file_logging"] or not self.log_file: + return + + # 检查文件大小 + if os.path.exists(self.monitor_config["log_file_path"]): + current_size = os.path.getsize(self.monitor_config["log_file_path"]) + if current_size > self.monitor_config["max_log_file_size"]: + # 轮转日志文件 + self._rotate_log_file() + + except Exception as e: + print(f"✗ 日志文件大小检查失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + + def _rotate_log_file(self): + """轮转日志文件""" + try: + if self.log_file: + self.log_file.close() + + # 重命名当前日志文件 + timestamp = int(time.time()) + rotated_filename = f"{self.monitor_config['log_file_path']}.{timestamp}" + os.rename(self.monitor_config["log_file_path"], rotated_filename) + + # 重新初始化日志文件 + self._initialize_log_file() + + print(f"✓ 日志文件已轮转: {rotated_filename}") + + except Exception as e: + print(f"✗ 日志文件轮转失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + + def _write_log(self, level: str, message: str, extra_data: Dict[str, Any] = None): + """ + 写入日志 + + Args: + level: 日志级别 + message: 日志消息 + extra_data: 额外数据 + """ + try: + if not self._should_log(level): + return + + timestamp = time.time() + log_entry = { + "timestamp": timestamp, + "level": level, + "message": message, + "extra_data": extra_data + } + + # 控制台输出 + print(f"[{level}] {message}") + + # 文件日志 + if self.monitor_config["enable_file_logging"] and self.log_file: + try: + log_line = json.dumps(log_entry, ensure_ascii=False) + self.log_file.write(log_line + "\n") + self.log_file.flush() + + self.log_file_size += len(log_line) + 1 + self.monitor_stats["logs_written"] += 1 + self.monitor_state["total_logs"] += 1 + + except Exception as e: + print(f"✗ 文件日志写入失败: {e}") + + # 触发日志条目回调 + self._trigger_monitor_callback("log_entry", log_entry) + + self.last_log_write = timestamp + + except Exception as e: + print(f"✗ 日志写入失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + + def _should_log(self, level: str) -> bool: + """ + 检查是否应该记录指定级别的日志 + + Args: + level: 日志级别 + + Returns: + 是否应该记录日志 + """ + try: + log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + current_level = self.monitor_config["log_level"] + + if current_level not in log_levels: + return True # 默认记录所有日志 + + if level not in log_levels: + return True # 默认记录未知级别的日志 + + current_index = log_levels.index(current_level) + level_index = log_levels.index(level) + + # 只记录等于或高于当前级别的日志 + return level_index >= current_index + + except Exception as e: + print(f"✗ 日志级别检查失败: {e}") + return True + + def get_performance_metrics(self, metric_name: str = None, limit: int = 100) -> Any: + """ + 获取性能指标 + + Args: + metric_name: 指标名称(可选) + limit: 限制返回的数量 + + Returns: + 性能指标数据 + """ + try: + with self.monitor_lock: + if metric_name: + if metric_name in self.performance_metrics: + metrics = list(self.performance_metrics[metric_name]) + return metrics[-limit:] if len(metrics) > limit else metrics + else: + return [] + else: + # 返回所有指标 + result = {} + for name, metrics in self.performance_metrics.items(): + metric_list = list(metrics) + result[name] = metric_list[-limit:] if len(metric_list) > limit else metric_list + return result + + except Exception as e: + print(f"✗ 获取性能指标失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + return {} + + def get_active_alerts(self) -> Dict[str, Any]: + """ + 获取活动警报 + + Returns: + 活动警报字典 + """ + try: + with self.monitor_lock: + return self.active_alerts.copy() + except Exception as e: + print(f"✗ 获取活动警报失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + return {} + + def get_alert_history(self, limit: int = 100) -> List[Dict[str, Any]]: + """ + 获取警报历史 + + Args: + limit: 限制返回的数量 + + Returns: + 警报历史列表 + """ + try: + with self.monitor_lock: + history = list(self.alert_history) + return history[-limit:] if len(history) > limit else history + except Exception as e: + print(f"✗ 获取警报历史失败: {e}") + self.monitor_stats["monitor_errors"] += 1 + return [] + + def get_monitor_stats(self) -> Dict[str, Any]: + """ + 获取监控统计信息 + + Returns: + 监控统计字典 + """ + return { + "state": self.monitor_state.copy(), + "stats": self.monitor_stats.copy(), + "config": self.monitor_config.copy(), + "active_alerts_count": len(self.active_alerts), + "alert_history_count": len(self.alert_history) + } + + def reset_stats(self): + """重置监控统计信息""" + try: + self.monitor_stats = { + "metrics_collected": 0, + "logs_written": 0, + "alerts_generated": 0, + "alerts_resolved": 0, + "performance_checks": 0, + "monitor_errors": 0 + } + + self.monitor_state["total_logs"] = 0 + self.monitor_state["total_alerts"] = 0 + self.monitor_state["active_alerts"] = 0 + + print("✓ 监控统计信息已重置") + except Exception as e: + print(f"✗ 监控统计信息重置失败: {e}") + + def set_monitor_config(self, config: Dict[str, Any]) -> bool: + """ + 设置监控配置 + + Args: + config: 监控配置字典 + + Returns: + 是否设置成功 + """ + try: + self.monitor_config.update(config) + print(f"✓ 监控配置已更新: {self.monitor_config}") + return True + except Exception as e: + print(f"✗ 监控配置设置失败: {e}") + return False + + def get_monitor_config(self) -> Dict[str, Any]: + """ + 获取监控配置 + + Returns: + 监控配置字典 + """ + return self.monitor_config.copy() + + def _trigger_monitor_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发监控回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.monitor_callbacks: + for callback in self.monitor_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 监控回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 监控回调触发失败: {e}") + + def register_monitor_callback(self, callback_type: str, callback: callable): + """ + 注册监控回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.monitor_callbacks: + self.monitor_callbacks[callback_type].append(callback) + print(f"✓ 监控回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 监控回调注册失败: {e}") + + def unregister_monitor_callback(self, callback_type: str, callback: callable): + """ + 注销监控回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.monitor_callbacks: + if callback in self.monitor_callbacks[callback_type]: + self.monitor_callbacks[callback_type].remove(callback) + print(f"✓ 监控回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 监控回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/realtime_communication/plugin.py b/plugins/user/realtime_communication/plugin.py index edebc54e..09ce3943 100644 --- a/plugins/user/realtime_communication/plugin.py +++ b/plugins/user/realtime_communication/plugin.py @@ -1,391 +1,391 @@ -""" -实时通信插件主文件 -为EG引擎提供实时通信功能 -""" - -import time -from typing import Dict, Any - -# 导入各个模块 -from .core.websocket_server import WebSocketServer -from .clients.client_manager import ClientManager -from .messaging.message_router import MessageRouter -from .rooms.room_manager import RoomManager -from .protocol.protocol_handler import ProtocolHandler -from .auth.auth_manager import AuthManager -from .serialization.data_serializer import DataSerializer -from .monitoring.communication_monitor import CommunicationMonitor -from .config.comm_config import CommConfig -from .events.event_handler import EventHandler -from .editor.comm_editor import CommEditor - -class RealtimeCommunicationPlugin: - """ - 实时通信插件主类 - 整合所有通信功能模块 - """ - - def __init__(self, engine): - """ - 初始化实时通信插件 - - Args: - engine: EG引擎实例 - """ - self.engine = engine - self.enabled = False - self.initialized = False - - # 插件信息 - self.plugin_info = { - "name": "Realtime Communication Plugin", - "version": "1.0.0", - "author": "EG Plugin System", - "description": "为EG引擎提供实时通信功能" - } - - # 插件状态 - self.plugin_state = { - "initialized_modules": [], - "last_update": 0.0, - "total_messages": 0, - "active_connections": 0 - } - - # 插件统计 - self.plugin_stats = { - "messages_sent": 0, - "messages_received": 0, - "connections_accepted": 0, - "connections_dropped": 0, - "errors": 0 - } - - # 功能模块 - self.websocket_server = None - self.client_manager = None - self.message_router = None - self.room_manager = None - self.protocol_handler = None - self.auth_manager = None - self.data_serializer = None - self.monitor = None - self.config_manager = None - self.event_handler = None - self.editor = None - - print("✓ 实时通信插件已创建") - - def initialize(self) -> bool: - """ - 初始化插件 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化实时通信插件...") - - # 初始化配置管理器 - self.config_manager = CommConfig(self) - if self.config_manager.initialize(): - self.plugin_state["initialized_modules"].append("config_manager") - else: - print("✗ 配置管理器初始化失败") - return False - - # 初始化认证管理器 - self.auth_manager = AuthManager(self) - if self.auth_manager.initialize(): - self.plugin_state["initialized_modules"].append("auth_manager") - else: - print("✗ 认证管理器初始化失败") - return False - - # 初始化数据序列化器 - self.data_serializer = DataSerializer(self) - if self.data_serializer.initialize(): - self.plugin_state["initialized_modules"].append("data_serializer") - else: - print("✗ 数据序列化器初始化失败") - return False - - # 初始化协议处理器 - self.protocol_handler = ProtocolHandler(self) - if self.protocol_handler.initialize(): - self.plugin_state["initialized_modules"].append("protocol_handler") - else: - print("✗ 协议处理器初始化失败") - return False - - # 初始化客户端管理器 - self.client_manager = ClientManager(self) - if self.client_manager.initialize(): - self.plugin_state["initialized_modules"].append("client_manager") - else: - print("✗ 客户端管理器初始化失败") - return False - - # 初始化消息路由器 - self.message_router = MessageRouter(self) - if self.message_router.initialize(): - self.plugin_state["initialized_modules"].append("message_router") - else: - print("✗ 消息路由器初始化失败") - return False - - # 初始化房间管理器 - self.room_manager = RoomManager(self) - if self.room_manager.initialize(): - self.plugin_state["initialized_modules"].append("room_manager") - else: - print("✗ 房间管理器初始化失败") - return False - - # 初始化WebSocket服务器 - self.websocket_server = WebSocketServer(self) - if self.websocket_server.initialize(): - self.plugin_state["initialized_modules"].append("websocket_server") - else: - print("✗ WebSocket服务器初始化失败") - return False - - # 初始化事件处理器 - self.event_handler = EventHandler(self) - if self.event_handler.initialize(): - self.plugin_state["initialized_modules"].append("event_handler") - else: - print("✗ 事件处理器初始化失败") - return False - - # 初始化监控系统 - self.monitor = CommunicationMonitor(self) - if self.monitor.initialize(): - self.plugin_state["initialized_modules"].append("monitor") - else: - print("✗ 监控系统初始化失败") - return False - - # 初始化编辑器接口 - self.editor = CommEditor(self) - if self.editor.initialize(): - self.plugin_state["initialized_modules"].append("editor") - else: - print("✗ 编辑器接口初始化失败") - return False - - self.initialized = True - print("✓ 实时通信插件初始化完成") - return True - - except Exception as e: - print(f"✗ 实时通信插件初始化失败: {e}") - self.plugin_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 实时通信插件未初始化") - return False - - print("正在启用实时通信插件...") - - # 启用各模块(按依赖顺序) - modules_to_enable = [ - ("config_manager", self.config_manager), - ("auth_manager", self.auth_manager), - ("data_serializer", self.data_serializer), - ("protocol_handler", self.protocol_handler), - ("client_manager", self.client_manager), - ("message_router", self.message_router), - ("room_manager", self.room_manager), - ("event_handler", self.event_handler), - ("monitor", self.monitor), - ("editor", self.editor), - ("websocket_server", self.websocket_server) - ] - - for module_name, module in modules_to_enable: - if module and not module.enable(): - print(f"✗ {module_name} 启用失败") - return False - elif module: - print(f"✓ {module_name} 已启用") - - self.enabled = True - print("✓ 实时通信插件已启用") - return True - - except Exception as e: - print(f"✗ 实时通信插件启用失败: {e}") - self.plugin_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用插件""" - try: - print("正在禁用实时通信插件...") - - # 禁用各模块(按依赖顺序倒序) - modules_to_disable = [ - ("websocket_server", self.websocket_server), - ("editor", self.editor), - ("monitor", self.monitor), - ("event_handler", self.event_handler), - ("room_manager", self.room_manager), - ("message_router", self.message_router), - ("client_manager", self.client_manager), - ("protocol_handler", self.protocol_handler), - ("data_serializer", self.data_serializer), - ("auth_manager", self.auth_manager), - ("config_manager", self.config_manager) - ] - - for module_name, module in modules_to_disable: - if module: - module.disable() - print(f"✓ {module_name} 已禁用") - - self.enabled = False - print("✓ 实时通信插件已禁用") - - except Exception as e: - print(f"✗ 实时通信插件禁用失败: {e}") - self.plugin_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理插件资源""" - try: - print("正在清理实时通信插件资源...") - - # 禁用插件 - if self.enabled: - self.disable() - - # 清理各模块 - modules_to_finalize = [ - self.websocket_server, - self.client_manager, - self.message_router, - self.room_manager, - self.protocol_handler, - self.auth_manager, - self.data_serializer, - self.monitor, - self.config_manager, - self.event_handler, - self.editor - ] - - for module in modules_to_finalize: - if module: - module.finalize() - - self.initialized = False - print("✓ 实时通信插件资源已清理") - - except Exception as e: - print(f"✗ 实时通信插件资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新插件状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 更新各模块 - if self.websocket_server: - self.websocket_server.update(dt) - - if self.client_manager: - self.client_manager.update(dt) - - if self.message_router: - self.message_router.update(dt) - - if self.room_manager: - self.room_manager.update(dt) - - if self.monitor: - self.monitor.update(dt) - - if self.editor: - self.editor.update(dt) - - self.plugin_state["last_update"] = current_time - - except Exception as e: - print(f"✗ 实时通信插件更新失败: {e}") - self.plugin_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def get_plugin_info(self) -> Dict[str, Any]: - """ - 获取插件信息 - - Returns: - 插件信息字典 - """ - return self.plugin_info.copy() - - def get_plugin_state(self) -> Dict[str, Any]: - """ - 获取插件状态 - - Returns: - 插件状态字典 - """ - return self.plugin_state.copy() - - def get_plugin_stats(self) -> Dict[str, Any]: - """ - 获取插件统计信息 - - Returns: - 插件统计字典 - """ - return { - "info": self.plugin_info.copy(), - "state": self.plugin_state.copy(), - "stats": self.plugin_stats.copy() - } - - def reset_stats(self): - """重置插件统计信息""" - try: - self.plugin_stats = { - "messages_sent": 0, - "messages_received": 0, - "connections_accepted": 0, - "connections_dropped": 0, - "errors": 0 - } - print("✓ 插件统计信息已重置") - except Exception as e: - print(f"✗ 插件统计信息重置失败: {e}") - -# 插件导出 +""" +实时通信插件主文件 +为EG引擎提供实时通信功能 +""" + +import time +from typing import Dict, Any + +# 导入各个模块 +from .core.websocket_server import WebSocketServer +from .clients.client_manager import ClientManager +from .messaging.message_router import MessageRouter +from .rooms.room_manager import RoomManager +from .protocol.protocol_handler import ProtocolHandler +from .auth.auth_manager import AuthManager +from .serialization.data_serializer import DataSerializer +from .monitoring.communication_monitor import CommunicationMonitor +from .config.comm_config import CommConfig +from .events.event_handler import EventHandler +from .editor.comm_editor import CommEditor + +class RealtimeCommunicationPlugin: + """ + 实时通信插件主类 + 整合所有通信功能模块 + """ + + def __init__(self, engine): + """ + 初始化实时通信插件 + + Args: + engine: EG引擎实例 + """ + self.engine = engine + self.enabled = False + self.initialized = False + + # 插件信息 + self.plugin_info = { + "name": "Realtime Communication Plugin", + "version": "1.0.0", + "author": "EG Plugin System", + "description": "为EG引擎提供实时通信功能" + } + + # 插件状态 + self.plugin_state = { + "initialized_modules": [], + "last_update": 0.0, + "total_messages": 0, + "active_connections": 0 + } + + # 插件统计 + self.plugin_stats = { + "messages_sent": 0, + "messages_received": 0, + "connections_accepted": 0, + "connections_dropped": 0, + "errors": 0 + } + + # 功能模块 + self.websocket_server = None + self.client_manager = None + self.message_router = None + self.room_manager = None + self.protocol_handler = None + self.auth_manager = None + self.data_serializer = None + self.monitor = None + self.config_manager = None + self.event_handler = None + self.editor = None + + print("✓ 实时通信插件已创建") + + def initialize(self) -> bool: + """ + 初始化插件 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化实时通信插件...") + + # 初始化配置管理器 + self.config_manager = CommConfig(self) + if self.config_manager.initialize(): + self.plugin_state["initialized_modules"].append("config_manager") + else: + print("✗ 配置管理器初始化失败") + return False + + # 初始化认证管理器 + self.auth_manager = AuthManager(self) + if self.auth_manager.initialize(): + self.plugin_state["initialized_modules"].append("auth_manager") + else: + print("✗ 认证管理器初始化失败") + return False + + # 初始化数据序列化器 + self.data_serializer = DataSerializer(self) + if self.data_serializer.initialize(): + self.plugin_state["initialized_modules"].append("data_serializer") + else: + print("✗ 数据序列化器初始化失败") + return False + + # 初始化协议处理器 + self.protocol_handler = ProtocolHandler(self) + if self.protocol_handler.initialize(): + self.plugin_state["initialized_modules"].append("protocol_handler") + else: + print("✗ 协议处理器初始化失败") + return False + + # 初始化客户端管理器 + self.client_manager = ClientManager(self) + if self.client_manager.initialize(): + self.plugin_state["initialized_modules"].append("client_manager") + else: + print("✗ 客户端管理器初始化失败") + return False + + # 初始化消息路由器 + self.message_router = MessageRouter(self) + if self.message_router.initialize(): + self.plugin_state["initialized_modules"].append("message_router") + else: + print("✗ 消息路由器初始化失败") + return False + + # 初始化房间管理器 + self.room_manager = RoomManager(self) + if self.room_manager.initialize(): + self.plugin_state["initialized_modules"].append("room_manager") + else: + print("✗ 房间管理器初始化失败") + return False + + # 初始化WebSocket服务器 + self.websocket_server = WebSocketServer(self) + if self.websocket_server.initialize(): + self.plugin_state["initialized_modules"].append("websocket_server") + else: + print("✗ WebSocket服务器初始化失败") + return False + + # 初始化事件处理器 + self.event_handler = EventHandler(self) + if self.event_handler.initialize(): + self.plugin_state["initialized_modules"].append("event_handler") + else: + print("✗ 事件处理器初始化失败") + return False + + # 初始化监控系统 + self.monitor = CommunicationMonitor(self) + if self.monitor.initialize(): + self.plugin_state["initialized_modules"].append("monitor") + else: + print("✗ 监控系统初始化失败") + return False + + # 初始化编辑器接口 + self.editor = CommEditor(self) + if self.editor.initialize(): + self.plugin_state["initialized_modules"].append("editor") + else: + print("✗ 编辑器接口初始化失败") + return False + + self.initialized = True + print("✓ 实时通信插件初始化完成") + return True + + except Exception as e: + print(f"✗ 实时通信插件初始化失败: {e}") + self.plugin_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 实时通信插件未初始化") + return False + + print("正在启用实时通信插件...") + + # 启用各模块(按依赖顺序) + modules_to_enable = [ + ("config_manager", self.config_manager), + ("auth_manager", self.auth_manager), + ("data_serializer", self.data_serializer), + ("protocol_handler", self.protocol_handler), + ("client_manager", self.client_manager), + ("message_router", self.message_router), + ("room_manager", self.room_manager), + ("event_handler", self.event_handler), + ("monitor", self.monitor), + ("editor", self.editor), + ("websocket_server", self.websocket_server) + ] + + for module_name, module in modules_to_enable: + if module and not module.enable(): + print(f"✗ {module_name} 启用失败") + return False + elif module: + print(f"✓ {module_name} 已启用") + + self.enabled = True + print("✓ 实时通信插件已启用") + return True + + except Exception as e: + print(f"✗ 实时通信插件启用失败: {e}") + self.plugin_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用插件""" + try: + print("正在禁用实时通信插件...") + + # 禁用各模块(按依赖顺序倒序) + modules_to_disable = [ + ("websocket_server", self.websocket_server), + ("editor", self.editor), + ("monitor", self.monitor), + ("event_handler", self.event_handler), + ("room_manager", self.room_manager), + ("message_router", self.message_router), + ("client_manager", self.client_manager), + ("protocol_handler", self.protocol_handler), + ("data_serializer", self.data_serializer), + ("auth_manager", self.auth_manager), + ("config_manager", self.config_manager) + ] + + for module_name, module in modules_to_disable: + if module: + module.disable() + print(f"✓ {module_name} 已禁用") + + self.enabled = False + print("✓ 实时通信插件已禁用") + + except Exception as e: + print(f"✗ 实时通信插件禁用失败: {e}") + self.plugin_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理插件资源""" + try: + print("正在清理实时通信插件资源...") + + # 禁用插件 + if self.enabled: + self.disable() + + # 清理各模块 + modules_to_finalize = [ + self.websocket_server, + self.client_manager, + self.message_router, + self.room_manager, + self.protocol_handler, + self.auth_manager, + self.data_serializer, + self.monitor, + self.config_manager, + self.event_handler, + self.editor + ] + + for module in modules_to_finalize: + if module: + module.finalize() + + self.initialized = False + print("✓ 实时通信插件资源已清理") + + except Exception as e: + print(f"✗ 实时通信插件资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新插件状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 更新各模块 + if self.websocket_server: + self.websocket_server.update(dt) + + if self.client_manager: + self.client_manager.update(dt) + + if self.message_router: + self.message_router.update(dt) + + if self.room_manager: + self.room_manager.update(dt) + + if self.monitor: + self.monitor.update(dt) + + if self.editor: + self.editor.update(dt) + + self.plugin_state["last_update"] = current_time + + except Exception as e: + print(f"✗ 实时通信插件更新失败: {e}") + self.plugin_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def get_plugin_info(self) -> Dict[str, Any]: + """ + 获取插件信息 + + Returns: + 插件信息字典 + """ + return self.plugin_info.copy() + + def get_plugin_state(self) -> Dict[str, Any]: + """ + 获取插件状态 + + Returns: + 插件状态字典 + """ + return self.plugin_state.copy() + + def get_plugin_stats(self) -> Dict[str, Any]: + """ + 获取插件统计信息 + + Returns: + 插件统计字典 + """ + return { + "info": self.plugin_info.copy(), + "state": self.plugin_state.copy(), + "stats": self.plugin_stats.copy() + } + + def reset_stats(self): + """重置插件统计信息""" + try: + self.plugin_stats = { + "messages_sent": 0, + "messages_received": 0, + "connections_accepted": 0, + "connections_dropped": 0, + "errors": 0 + } + print("✓ 插件统计信息已重置") + except Exception as e: + print(f"✗ 插件统计信息重置失败: {e}") + +# 插件导出 plugin = RealtimeCommunicationPlugin \ No newline at end of file diff --git a/plugins/user/realtime_communication/protocol/protocol_handler.py b/plugins/user/realtime_communication/protocol/protocol_handler.py index f5c35423..bb0df0cd 100644 --- a/plugins/user/realtime_communication/protocol/protocol_handler.py +++ b/plugins/user/realtime_communication/protocol/protocol_handler.py @@ -1,658 +1,658 @@ -""" -协议处理器模块 -处理各种通信协议和消息格式 -""" - -import time -import json -import zlib -import base64 -from typing import Dict, Any, List, Optional - -class ProtocolHandler: - """ - 协议处理器 - 处理各种通信协议和消息格式 - """ - - def __init__(self, plugin): - """ - 初始化协议处理器 - - Args: - plugin: 实时通信插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 协议配置 - self.protocol_config = { - "supported_protocols": ["websocket", "json", "binary"], - "default_protocol": "json", - "enable_compression": True, - "compression_threshold": 1024, # 1KB - "enable_encryption": False, - "enable_message_validation": True, - "max_message_size": 65536, # 64KB - "enable_fragmentation": True, - "fragment_size": 8192, # 8KB - "enable_checksum": True - } - - # 协议状态 - self.protocol_state = { - "active_protocols": [], - "compressed_messages": 0, - "encrypted_messages": 0, - "fragmented_messages": 0 - } - - # 协议统计 - self.protocol_stats = { - "messages_encoded": 0, - "messages_decoded": 0, - "bytes_encoded": 0, - "bytes_decoded": 0, - "compression_savings": 0, - "protocol_errors": 0 - } - - # 协议处理器映射 - self.protocol_handlers = { - "json": { - "encode": self._encode_json_message, - "decode": self._decode_json_message - }, - "binary": { - "encode": self._encode_binary_message, - "decode": self._decode_binary_message - } - } - - # 回调函数 - self.protocol_callbacks = { - "message_encoded": [], - "message_decoded": [], - "compression_applied": [], - "encryption_applied": [], - "protocol_error": [] - } - - # 时间戳记录 - self.last_compression = 0.0 - self.last_encryption = 0.0 - - print("✓ 协议处理器已创建") - - def initialize(self) -> bool: - """ - 初始化协议处理器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化协议处理器...") - - self.initialized = True - print("✓ 协议处理器初始化完成") - return True - - except Exception as e: - print(f"✗ 协议处理器初始化失败: {e}") - self.protocol_stats["protocol_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用协议处理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 协议处理器未初始化") - return False - - self.enabled = True - print("✓ 协议处理器已启用") - return True - - except Exception as e: - print(f"✗ 协议处理器启用失败: {e}") - self.protocol_stats["protocol_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用协议处理器""" - try: - self.enabled = False - print("✓ 协议处理器已禁用") - - except Exception as e: - print(f"✗ 协议处理器禁用失败: {e}") - self.protocol_stats["protocol_errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理协议处理器资源""" - try: - # 禁用协议处理器 - if self.enabled: - self.disable() - - # 清理回调 - self.protocol_callbacks.clear() - - self.initialized = False - print("✓ 协议处理器资源已清理") - - except Exception as e: - print(f"✗ 协议处理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新协议处理器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - # 协议处理器不需要频繁更新 - pass - - except Exception as e: - print(f"✗ 协议处理器更新失败: {e}") - self.protocol_stats["protocol_errors"] += 1 - import traceback - traceback.print_exc() - - def encode_message(self, message: Dict[str, Any], protocol: str = None) -> Optional[bytes]: - """ - 编码消息 - - Args: - message: 消息数据 - protocol: 协议类型 - - Returns: - 编码后的消息或None - """ - try: - if not self.enabled: - return None - - # 使用默认协议或指定协议 - if protocol is None: - protocol = self.protocol_config["default_protocol"] - - # 验证协议支持 - if protocol not in self.protocol_handlers: - print(f"✗ 不支持的协议: {protocol}") - self.protocol_stats["protocol_errors"] += 1 - return None - - # 验证消息大小 - if self.protocol_config["enable_message_validation"]: - message_size = len(json.dumps(message, ensure_ascii=False).encode('utf-8')) - if message_size > self.protocol_config["max_message_size"]: - print(f"✗ 消息过大: {message_size} bytes") - self.protocol_stats["protocol_errors"] += 1 - return None - - # 获取协议处理器 - encoder = self.protocol_handlers[protocol]["encode"] - - # 编码消息 - encoded_message = encoder(message) - if encoded_message is None: - return None - - # 更新统计 - self.protocol_stats["messages_encoded"] += 1 - self.protocol_stats["bytes_encoded"] += len(encoded_message) - - # 触发消息编码回调 - self._trigger_protocol_callback("message_encoded", { - "protocol": protocol, - "message_size": len(encoded_message), - "timestamp": time.time() - }) - - return encoded_message - - except Exception as e: - print(f"✗ 消息编码失败: {e}") - self.protocol_stats["protocol_errors"] += 1 - return None - - def decode_message(self, encoded_message: bytes, protocol: str = None) -> Optional[Dict[str, Any]]: - """ - 解码消息 - - Args: - encoded_message: 编码后的消息 - protocol: 协议类型 - - Returns: - 解码后的消息或None - """ - try: - if not self.enabled: - return None - - # 使用默认协议或指定协议 - if protocol is None: - protocol = self.protocol_config["default_protocol"] - - # 验证协议支持 - if protocol not in self.protocol_handlers: - print(f"✗ 不支持的协议: {protocol}") - self.protocol_stats["protocol_errors"] += 1 - return None - - # 获取协议处理器 - decoder = self.protocol_handlers[protocol]["decode"] - - # 解码消息 - decoded_message = decoder(encoded_message) - if decoded_message is None: - return None - - # 更新统计 - self.protocol_stats["messages_decoded"] += 1 - self.protocol_stats["bytes_decoded"] += len(encoded_message) - - # 触发消息解码回调 - self._trigger_protocol_callback("message_decoded", { - "protocol": protocol, - "message_size": len(encoded_message), - "timestamp": time.time() - }) - - return decoded_message - - except Exception as e: - print(f"✗ 消息解码失败: {e}") - self.protocol_stats["protocol_errors"] += 1 - return None - - def _encode_json_message(self, message: Dict[str, Any]) -> Optional[bytes]: - """ - JSON消息编码 - - Args: - message: 消息数据 - - Returns: - 编码后的消息或None - """ - try: - # 序列化为JSON - json_string = json.dumps(message, ensure_ascii=False) - encoded_message = json_string.encode('utf-8') - - # 检查是否需要压缩 - if (self.protocol_config["enable_compression"] and - len(encoded_message) > self.protocol_config["compression_threshold"]): - compressed_message = zlib.compress(encoded_message) - if len(compressed_message) < len(encoded_message): - # 使用压缩版本 - encoded_message = compressed_message - self.protocol_state["compressed_messages"] += 1 - self.protocol_stats["compression_savings"] += len(encoded_message) - len(compressed_message) - - # 触发压缩应用回调 - self._trigger_protocol_callback("compression_applied", { - "original_size": len(json_string.encode('utf-8')), - "compressed_size": len(compressed_message), - "timestamp": time.time() - }) - - return encoded_message - - except Exception as e: - print(f"✗ JSON消息编码失败: {e}") - self.protocol_stats["protocol_errors"] += 1 - return None - - def _decode_json_message(self, encoded_message: bytes) -> Optional[Dict[str, Any]]: - """ - JSON消息解码 - - Args: - encoded_message: 编码后的消息 - - Returns: - 解码后的消息或None - """ - try: - # 尝试解压缩 - try: - decompressed_message = zlib.decompress(encoded_message) - encoded_message = decompressed_message - except zlib.error: - # 不是压缩数据,使用原始数据 - pass - - # 解码为JSON - json_string = encoded_message.decode('utf-8') - decoded_message = json.loads(json_string) - - return decoded_message - - except Exception as e: - print(f"✗ JSON消息解码失败: {e}") - self.protocol_stats["protocol_errors"] += 1 - return None - - def _encode_binary_message(self, message: Dict[str, Any]) -> Optional[bytes]: - """ - 二进制消息编码 - - Args: - message: 消息数据 - - Returns: - 编码后的消息或None - """ - try: - # 将消息转换为二进制格式(简化实现) - json_string = json.dumps(message, ensure_ascii=False) - binary_message = json_string.encode('utf-8') - - # 添加简单的头部信息 - header = len(binary_message).to_bytes(4, byteorder='big') - encoded_message = header + binary_message - - # 检查是否需要压缩 - if (self.protocol_config["enable_compression"] and - len(encoded_message) > self.protocol_config["compression_threshold"]): - compressed_message = zlib.compress(encoded_message) - if len(compressed_message) < len(encoded_message): - # 使用压缩版本 - encoded_message = compressed_message - self.protocol_state["compressed_messages"] += 1 - self.protocol_stats["compression_savings"] += len(encoded_message) - len(compressed_message) - - # 触发压缩应用回调 - self._trigger_protocol_callback("compression_applied", { - "original_size": len(header + binary_message), - "compressed_size": len(compressed_message), - "timestamp": time.time() - }) - - return encoded_message - - except Exception as e: - print(f"✗ 二进制消息编码失败: {e}") - self.protocol_stats["protocol_errors"] += 1 - return None - - def _decode_binary_message(self, encoded_message: bytes) -> Optional[Dict[str, Any]]: - """ - 二进制消息解码 - - Args: - encoded_message: 编码后的消息 - - Returns: - 解码后的消息或None - """ - try: - # 尝试解压缩 - try: - decompressed_message = zlib.decompress(encoded_message) - encoded_message = decompressed_message - except zlib.error: - # 不是压缩数据,使用原始数据 - pass - - # 解析头部信息 - if len(encoded_message) < 4: - raise ValueError("消息太短") - - message_length = int.from_bytes(encoded_message[:4], byteorder='big') - binary_message = encoded_message[4:4 + message_length] - - # 解码为JSON - json_string = binary_message.decode('utf-8') - decoded_message = json.loads(json_string) - - return decoded_message - - except Exception as e: - print(f"✗ 二进制消息解码失败: {e}") - self.protocol_stats["protocol_errors"] += 1 - return None - - def fragment_message(self, message: bytes, fragment_size: int = None) -> List[bytes]: - """ - 分片消息 - - Args: - message: 消息数据 - fragment_size: 分片大小 - - Returns: - 分片列表 - """ - try: - if not self.protocol_config["enable_fragmentation"]: - return [message] - - if fragment_size is None: - fragment_size = self.protocol_config["fragment_size"] - - fragments = [] - for i in range(0, len(message), fragment_size): - fragment = message[i:i + fragment_size] - fragments.append(fragment) - - self.protocol_state["fragmented_messages"] += 1 - - return fragments - - except Exception as e: - print(f"✗ 消息分片失败: {e}") - self.protocol_stats["protocol_errors"] += 1 - return [message] - - def reassemble_message(self, fragments: List[bytes]) -> Optional[bytes]: - """ - 重组消息 - - Args: - fragments: 分片列表 - - Returns: - 重组后的消息或None - """ - try: - if not fragments: - return None - - reassembled_message = b''.join(fragments) - return reassembled_message - - except Exception as e: - print(f"✗ 消息重组失败: {e}") - self.protocol_stats["protocol_errors"] += 1 - return None - - def add_protocol_handler(self, protocol: str, encode_func: callable, decode_func: callable) -> bool: - """ - 添加协议处理器 - - Args: - protocol: 协议名称 - encode_func: 编码函数 - decode_func: 解码函数 - - Returns: - 是否添加成功 - """ - try: - self.protocol_handlers[protocol] = { - "encode": encode_func, - "decode": decode_func - } - - if protocol not in self.protocol_config["supported_protocols"]: - self.protocol_config["supported_protocols"].append(protocol) - - print(f"✓ 协议处理器已添加: {protocol}") - return True - - except Exception as e: - print(f"✗ 协议处理器添加失败: {e}") - self.protocol_stats["protocol_errors"] += 1 - return False - - def remove_protocol_handler(self, protocol: str) -> bool: - """ - 移除协议处理器 - - Args: - protocol: 协议名称 - - Returns: - 是否移除成功 - """ - try: - if protocol in self.protocol_handlers: - del self.protocol_handlers[protocol] - - if protocol in self.protocol_config["supported_protocols"]: - self.protocol_config["supported_protocols"].remove(protocol) - - print(f"✓ 协议处理器已移除: {protocol}") - return True - else: - print(f"✗ 协议处理器不存在: {protocol}") - return False - - except Exception as e: - print(f"✗ 协议处理器移除失败: {e}") - self.protocol_stats["protocol_errors"] += 1 - return False - - def get_protocol_stats(self) -> Dict[str, Any]: - """ - 获取协议统计信息 - - Returns: - 协议统计字典 - """ - return { - "state": self.protocol_state.copy(), - "stats": self.protocol_stats.copy(), - "config": self.protocol_config.copy() - } - - def reset_stats(self): - """重置协议统计信息""" - try: - self.protocol_stats = { - "messages_encoded": 0, - "messages_decoded": 0, - "bytes_encoded": 0, - "bytes_decoded": 0, - "compression_savings": 0, - "protocol_errors": 0 - } - print("✓ 协议统计信息已重置") - except Exception as e: - print(f"✗ 协议统计信息重置失败: {e}") - - def set_protocol_config(self, config: Dict[str, Any]) -> bool: - """ - 设置协议配置 - - Args: - config: 协议配置字典 - - Returns: - 是否设置成功 - """ - try: - self.protocol_config.update(config) - print(f"✓ 协议配置已更新: {self.protocol_config}") - return True - except Exception as e: - print(f"✗ 协议配置设置失败: {e}") - return False - - def get_protocol_config(self) -> Dict[str, Any]: - """ - 获取协议配置 - - Returns: - 协议配置字典 - """ - return self.protocol_config.copy() - - def _trigger_protocol_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发协议回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.protocol_callbacks: - for callback in self.protocol_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 协议回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 协议回调触发失败: {e}") - - def register_protocol_callback(self, callback_type: str, callback: callable): - """ - 注册协议回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.protocol_callbacks: - self.protocol_callbacks[callback_type].append(callback) - print(f"✓ 协议回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 协议回调注册失败: {e}") - - def unregister_protocol_callback(self, callback_type: str, callback: callable): - """ - 注销协议回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.protocol_callbacks: - if callback in self.protocol_callbacks[callback_type]: - self.protocol_callbacks[callback_type].remove(callback) - print(f"✓ 协议回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +协议处理器模块 +处理各种通信协议和消息格式 +""" + +import time +import json +import zlib +import base64 +from typing import Dict, Any, List, Optional + +class ProtocolHandler: + """ + 协议处理器 + 处理各种通信协议和消息格式 + """ + + def __init__(self, plugin): + """ + 初始化协议处理器 + + Args: + plugin: 实时通信插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 协议配置 + self.protocol_config = { + "supported_protocols": ["websocket", "json", "binary"], + "default_protocol": "json", + "enable_compression": True, + "compression_threshold": 1024, # 1KB + "enable_encryption": False, + "enable_message_validation": True, + "max_message_size": 65536, # 64KB + "enable_fragmentation": True, + "fragment_size": 8192, # 8KB + "enable_checksum": True + } + + # 协议状态 + self.protocol_state = { + "active_protocols": [], + "compressed_messages": 0, + "encrypted_messages": 0, + "fragmented_messages": 0 + } + + # 协议统计 + self.protocol_stats = { + "messages_encoded": 0, + "messages_decoded": 0, + "bytes_encoded": 0, + "bytes_decoded": 0, + "compression_savings": 0, + "protocol_errors": 0 + } + + # 协议处理器映射 + self.protocol_handlers = { + "json": { + "encode": self._encode_json_message, + "decode": self._decode_json_message + }, + "binary": { + "encode": self._encode_binary_message, + "decode": self._decode_binary_message + } + } + + # 回调函数 + self.protocol_callbacks = { + "message_encoded": [], + "message_decoded": [], + "compression_applied": [], + "encryption_applied": [], + "protocol_error": [] + } + + # 时间戳记录 + self.last_compression = 0.0 + self.last_encryption = 0.0 + + print("✓ 协议处理器已创建") + + def initialize(self) -> bool: + """ + 初始化协议处理器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化协议处理器...") + + self.initialized = True + print("✓ 协议处理器初始化完成") + return True + + except Exception as e: + print(f"✗ 协议处理器初始化失败: {e}") + self.protocol_stats["protocol_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用协议处理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 协议处理器未初始化") + return False + + self.enabled = True + print("✓ 协议处理器已启用") + return True + + except Exception as e: + print(f"✗ 协议处理器启用失败: {e}") + self.protocol_stats["protocol_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用协议处理器""" + try: + self.enabled = False + print("✓ 协议处理器已禁用") + + except Exception as e: + print(f"✗ 协议处理器禁用失败: {e}") + self.protocol_stats["protocol_errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理协议处理器资源""" + try: + # 禁用协议处理器 + if self.enabled: + self.disable() + + # 清理回调 + self.protocol_callbacks.clear() + + self.initialized = False + print("✓ 协议处理器资源已清理") + + except Exception as e: + print(f"✗ 协议处理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新协议处理器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + # 协议处理器不需要频繁更新 + pass + + except Exception as e: + print(f"✗ 协议处理器更新失败: {e}") + self.protocol_stats["protocol_errors"] += 1 + import traceback + traceback.print_exc() + + def encode_message(self, message: Dict[str, Any], protocol: str = None) -> Optional[bytes]: + """ + 编码消息 + + Args: + message: 消息数据 + protocol: 协议类型 + + Returns: + 编码后的消息或None + """ + try: + if not self.enabled: + return None + + # 使用默认协议或指定协议 + if protocol is None: + protocol = self.protocol_config["default_protocol"] + + # 验证协议支持 + if protocol not in self.protocol_handlers: + print(f"✗ 不支持的协议: {protocol}") + self.protocol_stats["protocol_errors"] += 1 + return None + + # 验证消息大小 + if self.protocol_config["enable_message_validation"]: + message_size = len(json.dumps(message, ensure_ascii=False).encode('utf-8')) + if message_size > self.protocol_config["max_message_size"]: + print(f"✗ 消息过大: {message_size} bytes") + self.protocol_stats["protocol_errors"] += 1 + return None + + # 获取协议处理器 + encoder = self.protocol_handlers[protocol]["encode"] + + # 编码消息 + encoded_message = encoder(message) + if encoded_message is None: + return None + + # 更新统计 + self.protocol_stats["messages_encoded"] += 1 + self.protocol_stats["bytes_encoded"] += len(encoded_message) + + # 触发消息编码回调 + self._trigger_protocol_callback("message_encoded", { + "protocol": protocol, + "message_size": len(encoded_message), + "timestamp": time.time() + }) + + return encoded_message + + except Exception as e: + print(f"✗ 消息编码失败: {e}") + self.protocol_stats["protocol_errors"] += 1 + return None + + def decode_message(self, encoded_message: bytes, protocol: str = None) -> Optional[Dict[str, Any]]: + """ + 解码消息 + + Args: + encoded_message: 编码后的消息 + protocol: 协议类型 + + Returns: + 解码后的消息或None + """ + try: + if not self.enabled: + return None + + # 使用默认协议或指定协议 + if protocol is None: + protocol = self.protocol_config["default_protocol"] + + # 验证协议支持 + if protocol not in self.protocol_handlers: + print(f"✗ 不支持的协议: {protocol}") + self.protocol_stats["protocol_errors"] += 1 + return None + + # 获取协议处理器 + decoder = self.protocol_handlers[protocol]["decode"] + + # 解码消息 + decoded_message = decoder(encoded_message) + if decoded_message is None: + return None + + # 更新统计 + self.protocol_stats["messages_decoded"] += 1 + self.protocol_stats["bytes_decoded"] += len(encoded_message) + + # 触发消息解码回调 + self._trigger_protocol_callback("message_decoded", { + "protocol": protocol, + "message_size": len(encoded_message), + "timestamp": time.time() + }) + + return decoded_message + + except Exception as e: + print(f"✗ 消息解码失败: {e}") + self.protocol_stats["protocol_errors"] += 1 + return None + + def _encode_json_message(self, message: Dict[str, Any]) -> Optional[bytes]: + """ + JSON消息编码 + + Args: + message: 消息数据 + + Returns: + 编码后的消息或None + """ + try: + # 序列化为JSON + json_string = json.dumps(message, ensure_ascii=False) + encoded_message = json_string.encode('utf-8') + + # 检查是否需要压缩 + if (self.protocol_config["enable_compression"] and + len(encoded_message) > self.protocol_config["compression_threshold"]): + compressed_message = zlib.compress(encoded_message) + if len(compressed_message) < len(encoded_message): + # 使用压缩版本 + encoded_message = compressed_message + self.protocol_state["compressed_messages"] += 1 + self.protocol_stats["compression_savings"] += len(encoded_message) - len(compressed_message) + + # 触发压缩应用回调 + self._trigger_protocol_callback("compression_applied", { + "original_size": len(json_string.encode('utf-8')), + "compressed_size": len(compressed_message), + "timestamp": time.time() + }) + + return encoded_message + + except Exception as e: + print(f"✗ JSON消息编码失败: {e}") + self.protocol_stats["protocol_errors"] += 1 + return None + + def _decode_json_message(self, encoded_message: bytes) -> Optional[Dict[str, Any]]: + """ + JSON消息解码 + + Args: + encoded_message: 编码后的消息 + + Returns: + 解码后的消息或None + """ + try: + # 尝试解压缩 + try: + decompressed_message = zlib.decompress(encoded_message) + encoded_message = decompressed_message + except zlib.error: + # 不是压缩数据,使用原始数据 + pass + + # 解码为JSON + json_string = encoded_message.decode('utf-8') + decoded_message = json.loads(json_string) + + return decoded_message + + except Exception as e: + print(f"✗ JSON消息解码失败: {e}") + self.protocol_stats["protocol_errors"] += 1 + return None + + def _encode_binary_message(self, message: Dict[str, Any]) -> Optional[bytes]: + """ + 二进制消息编码 + + Args: + message: 消息数据 + + Returns: + 编码后的消息或None + """ + try: + # 将消息转换为二进制格式(简化实现) + json_string = json.dumps(message, ensure_ascii=False) + binary_message = json_string.encode('utf-8') + + # 添加简单的头部信息 + header = len(binary_message).to_bytes(4, byteorder='big') + encoded_message = header + binary_message + + # 检查是否需要压缩 + if (self.protocol_config["enable_compression"] and + len(encoded_message) > self.protocol_config["compression_threshold"]): + compressed_message = zlib.compress(encoded_message) + if len(compressed_message) < len(encoded_message): + # 使用压缩版本 + encoded_message = compressed_message + self.protocol_state["compressed_messages"] += 1 + self.protocol_stats["compression_savings"] += len(encoded_message) - len(compressed_message) + + # 触发压缩应用回调 + self._trigger_protocol_callback("compression_applied", { + "original_size": len(header + binary_message), + "compressed_size": len(compressed_message), + "timestamp": time.time() + }) + + return encoded_message + + except Exception as e: + print(f"✗ 二进制消息编码失败: {e}") + self.protocol_stats["protocol_errors"] += 1 + return None + + def _decode_binary_message(self, encoded_message: bytes) -> Optional[Dict[str, Any]]: + """ + 二进制消息解码 + + Args: + encoded_message: 编码后的消息 + + Returns: + 解码后的消息或None + """ + try: + # 尝试解压缩 + try: + decompressed_message = zlib.decompress(encoded_message) + encoded_message = decompressed_message + except zlib.error: + # 不是压缩数据,使用原始数据 + pass + + # 解析头部信息 + if len(encoded_message) < 4: + raise ValueError("消息太短") + + message_length = int.from_bytes(encoded_message[:4], byteorder='big') + binary_message = encoded_message[4:4 + message_length] + + # 解码为JSON + json_string = binary_message.decode('utf-8') + decoded_message = json.loads(json_string) + + return decoded_message + + except Exception as e: + print(f"✗ 二进制消息解码失败: {e}") + self.protocol_stats["protocol_errors"] += 1 + return None + + def fragment_message(self, message: bytes, fragment_size: int = None) -> List[bytes]: + """ + 分片消息 + + Args: + message: 消息数据 + fragment_size: 分片大小 + + Returns: + 分片列表 + """ + try: + if not self.protocol_config["enable_fragmentation"]: + return [message] + + if fragment_size is None: + fragment_size = self.protocol_config["fragment_size"] + + fragments = [] + for i in range(0, len(message), fragment_size): + fragment = message[i:i + fragment_size] + fragments.append(fragment) + + self.protocol_state["fragmented_messages"] += 1 + + return fragments + + except Exception as e: + print(f"✗ 消息分片失败: {e}") + self.protocol_stats["protocol_errors"] += 1 + return [message] + + def reassemble_message(self, fragments: List[bytes]) -> Optional[bytes]: + """ + 重组消息 + + Args: + fragments: 分片列表 + + Returns: + 重组后的消息或None + """ + try: + if not fragments: + return None + + reassembled_message = b''.join(fragments) + return reassembled_message + + except Exception as e: + print(f"✗ 消息重组失败: {e}") + self.protocol_stats["protocol_errors"] += 1 + return None + + def add_protocol_handler(self, protocol: str, encode_func: callable, decode_func: callable) -> bool: + """ + 添加协议处理器 + + Args: + protocol: 协议名称 + encode_func: 编码函数 + decode_func: 解码函数 + + Returns: + 是否添加成功 + """ + try: + self.protocol_handlers[protocol] = { + "encode": encode_func, + "decode": decode_func + } + + if protocol not in self.protocol_config["supported_protocols"]: + self.protocol_config["supported_protocols"].append(protocol) + + print(f"✓ 协议处理器已添加: {protocol}") + return True + + except Exception as e: + print(f"✗ 协议处理器添加失败: {e}") + self.protocol_stats["protocol_errors"] += 1 + return False + + def remove_protocol_handler(self, protocol: str) -> bool: + """ + 移除协议处理器 + + Args: + protocol: 协议名称 + + Returns: + 是否移除成功 + """ + try: + if protocol in self.protocol_handlers: + del self.protocol_handlers[protocol] + + if protocol in self.protocol_config["supported_protocols"]: + self.protocol_config["supported_protocols"].remove(protocol) + + print(f"✓ 协议处理器已移除: {protocol}") + return True + else: + print(f"✗ 协议处理器不存在: {protocol}") + return False + + except Exception as e: + print(f"✗ 协议处理器移除失败: {e}") + self.protocol_stats["protocol_errors"] += 1 + return False + + def get_protocol_stats(self) -> Dict[str, Any]: + """ + 获取协议统计信息 + + Returns: + 协议统计字典 + """ + return { + "state": self.protocol_state.copy(), + "stats": self.protocol_stats.copy(), + "config": self.protocol_config.copy() + } + + def reset_stats(self): + """重置协议统计信息""" + try: + self.protocol_stats = { + "messages_encoded": 0, + "messages_decoded": 0, + "bytes_encoded": 0, + "bytes_decoded": 0, + "compression_savings": 0, + "protocol_errors": 0 + } + print("✓ 协议统计信息已重置") + except Exception as e: + print(f"✗ 协议统计信息重置失败: {e}") + + def set_protocol_config(self, config: Dict[str, Any]) -> bool: + """ + 设置协议配置 + + Args: + config: 协议配置字典 + + Returns: + 是否设置成功 + """ + try: + self.protocol_config.update(config) + print(f"✓ 协议配置已更新: {self.protocol_config}") + return True + except Exception as e: + print(f"✗ 协议配置设置失败: {e}") + return False + + def get_protocol_config(self) -> Dict[str, Any]: + """ + 获取协议配置 + + Returns: + 协议配置字典 + """ + return self.protocol_config.copy() + + def _trigger_protocol_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发协议回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.protocol_callbacks: + for callback in self.protocol_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 协议回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 协议回调触发失败: {e}") + + def register_protocol_callback(self, callback_type: str, callback: callable): + """ + 注册协议回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.protocol_callbacks: + self.protocol_callbacks[callback_type].append(callback) + print(f"✓ 协议回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 协议回调注册失败: {e}") + + def unregister_protocol_callback(self, callback_type: str, callback: callable): + """ + 注销协议回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.protocol_callbacks: + if callback in self.protocol_callbacks[callback_type]: + self.protocol_callbacks[callback_type].remove(callback) + print(f"✓ 协议回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 协议回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/realtime_communication/rooms/room_manager.py b/plugins/user/realtime_communication/rooms/room_manager.py index 49904640..57824f45 100644 --- a/plugins/user/realtime_communication/rooms/room_manager.py +++ b/plugins/user/realtime_communication/rooms/room_manager.py @@ -1,894 +1,894 @@ -""" -房间管理器模块 -管理通信组和广播消息 -""" - -import time -import threading -import uuid -from typing import Dict, Any, List, Optional - -class RoomManager: - """ - 房间管理器 - 管理通信组和广播消息 - """ - - def __init__(self, plugin): - """ - 初始化房间管理器 - - Args: - plugin: 实时通信插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 房间配置 - self.room_config = { - "max_rooms": 1000, - "max_clients_per_room": 100, - "enable_room_persistence": False, - "room_timeout": 3600, # 1小时无活动房间自动关闭 - "enable_room_passwords": True, - "enable_room_visibility": True, - "default_room_settings": { - "max_clients": 50, - "is_public": True, - "allow_spectators": True, - "enable_voice_chat": True - } - } - - # 房间状态 - self.room_state = { - "total_rooms": 0, - "active_rooms": 0, - "clients_in_rooms": 0 - } - - # 房间存储 - self.rooms = {} - self.client_rooms = {} # 客户端所在房间映射 - self.room_lock = threading.RLock() - - # 房间统计 - self.room_stats = { - "rooms_created": 0, - "rooms_destroyed": 0, - "clients_joined": 0, - "clients_left": 0, - "messages_broadcast": 0, - "errors": 0 - } - - # 回调函数 - self.room_callbacks = { - "room_created": [], - "room_destroyed": [], - "client_joined": [], - "client_left": [], - "room_updated": [], - "room_error": [] - } - - # 时间戳记录 - self.last_cleanup = 0.0 - self.last_room_check = 0.0 - - print("✓ 房间管理器已创建") - - def initialize(self) -> bool: - """ - 初始化房间管理器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化房间管理器...") - - self.initialized = True - print("✓ 房间管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 房间管理器初始化失败: {e}") - self.room_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用房间管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 房间管理器未初始化") - return False - - self.enabled = True - print("✓ 房间管理器已启用") - return True - - except Exception as e: - print(f"✗ 房间管理器启用失败: {e}") - self.room_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用房间管理器""" - try: - self.enabled = False - - # 销毁所有房间 - self._destroy_all_rooms() - - print("✓ 房间管理器已禁用") - - except Exception as e: - print(f"✗ 房间管理器禁用失败: {e}") - self.room_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理房间管理器资源""" - try: - # 禁用房间管理器 - if self.enabled: - self.disable() - - # 清理回调 - self.room_callbacks.clear() - - self.initialized = False - print("✓ 房间管理器资源已清理") - - except Exception as e: - print(f"✗ 房间管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新房间管理器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 定期清理过期房间 - if current_time - self.last_cleanup > 60.0: # 每分钟清理一次 - self._cleanup_expired_rooms(current_time) - self.last_cleanup = current_time - - except Exception as e: - print(f"✗ 房间管理器更新失败: {e}") - self.room_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def _cleanup_expired_rooms(self, current_time: float): - """清理过期房间""" - try: - expired_rooms = [] - with self.room_lock: - for room_id, room_data in self.rooms.items(): - # 检查房间是否超时 - if current_time - room_data["last_activity"] > self.room_config["room_timeout"]: - # 检查房间是否为空 - if len(room_data["clients"]) == 0: - expired_rooms.append(room_id) - - # 销毁过期房间 - for room_id in expired_rooms: - self.destroy_room(room_id) - - except Exception as e: - print(f"✗ 过期房间清理失败: {e}") - self.room_stats["errors"] += 1 - - def _destroy_all_rooms(self): - """销毁所有房间""" - try: - with self.room_lock: - room_ids = list(self.rooms.keys()) - - for room_id in room_ids: - self.destroy_room(room_id) - - except Exception as e: - print(f"✗ 销毁所有房间失败: {e}") - self.room_stats["errors"] += 1 - - def create_room(self, room_name: str = None, room_settings: Dict[str, Any] = None, - room_password: str = None) -> Optional[str]: - """ - 创建房间 - - Args: - room_name: 房间名称 - room_settings: 房间设置 - room_password: 房间密码 - - Returns: - 房间ID或None - """ - try: - if not self.enabled: - return None - - # 检查房间数量限制 - with self.room_lock: - if len(self.rooms) >= self.room_config["max_rooms"]: - print("✗ 房间数量已达上限") - return None - - # 生成房间ID - room_id = str(uuid.uuid4()) - - # 使用默认设置或传入设置 - settings = self.room_config["default_room_settings"].copy() - if room_settings: - settings.update(room_settings) - - # 创建房间数据 - room_data = { - "room_id": room_id, - "room_name": room_name or f"房间-{room_id[:8]}", - "settings": settings, - "password": room_password, - "clients": {}, # 客户端ID到客户端数据的映射 - "host_client_id": None, - "created_time": time.time(), - "last_activity": time.time(), - "room_state": {}, # 房间状态数据 - "custom_data": {} # 自定义房间数据 - } - - # 添加到房间列表 - with self.room_lock: - self.rooms[room_id] = room_data - - self.room_state["total_rooms"] += 1 - self.room_state["active_rooms"] += 1 - self.room_stats["rooms_created"] += 1 - - # 触发房间创建回调 - self._trigger_room_callback("room_created", { - "room_id": room_id, - "room_name": room_data["room_name"], - "settings": settings, - "timestamp": room_data["created_time"] - }) - - print(f"✓ 房间已创建: {room_id} ({room_data['room_name']})") - return room_id - - except Exception as e: - print(f"✗ 房间创建失败: {e}") - self.room_stats["errors"] += 1 - return None - - def destroy_room(self, room_id: str) -> bool: - """ - 销毁房间 - - Args: - room_id: 房间ID - - Returns: - 是否销毁成功 - """ - try: - if not self.enabled: - return False - - # 检查房间是否存在 - with self.room_lock: - if room_id not in self.rooms: - print(f"✗ 房间不存在: {room_id}") - return False - - room_data = self.rooms[room_id] - - # 通知房间内所有客户端 - for client_id in list(room_data["clients"].keys()): - self._notify_client_room_destroyed(client_id, room_id) - - # 移除房间 - del self.rooms[room_id] - - self.room_state["active_rooms"] -= 1 - self.room_stats["rooms_destroyed"] += 1 - - # 触发房间销毁回调 - self._trigger_room_callback("room_destroyed", { - "room_id": room_id, - "room_name": room_data["room_name"], - "timestamp": time.time() - }) - - print(f"✓ 房间已销毁: {room_id}") - return True - - except Exception as e: - print(f"✗ 房间销毁失败: {e}") - self.room_stats["errors"] += 1 - return False - - def _notify_client_room_destroyed(self, client_id: str, room_id: str): - """ - 通知客户端房间被销毁 - - Args: - client_id: 客户端ID - room_id: 房间ID - """ - try: - # 从客户端房间映射中移除 - if client_id in self.client_rooms: - del self.client_rooms[client_id] - - # 发送房间销毁通知 - if self.plugin.websocket_server: - destroy_message = { - "type": "room_destroyed", - "room_id": room_id, - "timestamp": time.time() - } - - self.plugin.websocket_server.send_message_to_client(client_id, destroy_message) - - except Exception as e: - print(f"✗ 通知客户端房间销毁失败: {e}") - self.room_stats["errors"] += 1 - - def add_client_to_room(self, room_id: str, client_id: str, client_data: Dict[str, Any] = None) -> bool: - """ - 添加客户端到房间 - - Args: - room_id: 房间ID - client_id: 客户端ID - client_data: 客户端数据 - - Returns: - 是否添加成功 - """ - try: - if not self.enabled: - return False - - # 检查客户端是否已认证 - if self.plugin.client_manager and not self.plugin.client_manager.is_client_authenticated(client_id): - print(f"✗ 客户端未认证: {client_id}") - return False - - # 检查房间是否存在 - with self.room_lock: - if room_id not in self.rooms: - print(f"✗ 房间不存在: {room_id}") - return False - - room_data = self.rooms[room_id] - - # 检查房间是否已满 - if len(room_data["clients"]) >= room_data["settings"]["max_clients"]: - print(f"✗ 房间已满: {room_id}") - return False - - # 检查房间密码 - if room_data["password"] and not self._verify_room_password(room_id, client_data): - print(f"✗ 房间密码错误: {room_id}") - return False - - # 添加客户端到房间 - client_info = client_data or {} - client_info["join_time"] = time.time() - client_info["last_activity"] = time.time() - - room_data["clients"][client_id] = client_info - room_data["last_activity"] = time.time() - - # 设置房间主机(如果是第一个客户端) - if room_data["host_client_id"] is None: - room_data["host_client_id"] = client_id - - # 更新客户端房间映射 - self.client_rooms[client_id] = room_id - - self.room_state["clients_in_rooms"] += 1 - self.room_stats["clients_joined"] += 1 - - # 触发客户端加入回调 - self._trigger_room_callback("client_joined", { - "room_id": room_id, - "room_name": room_data["room_name"], - "client_id": client_id, - "timestamp": time.time() - }) - - # 通知客户端加入成功 - join_success_message = { - "type": "room_join_success", - "room_id": room_id, - "room_name": room_data["room_name"], - "timestamp": time.time() - } - - if self.plugin.websocket_server: - self.plugin.websocket_server.send_message_to_client(client_id, join_success_message) - - print(f"✓ 客户端已加入房间: {client_id} -> {room_id}") - return True - - except Exception as e: - print(f"✗ 添加客户端到房间失败: {e}") - self.room_stats["errors"] += 1 - return False - - def remove_client_from_room(self, room_id: str, client_id: str) -> bool: - """ - 从房间移除客户端 - - Args: - room_id: 房间ID - client_id: 客户端ID - - Returns: - 是否移除成功 - """ - try: - if not self.enabled: - return False - - # 检查房间是否存在 - with self.room_lock: - if room_id not in self.rooms: - print(f"✗ 房间不存在: {room_id}") - return False - - room_data = self.rooms[room_id] - - # 检查客户端是否在房间中 - if client_id not in room_data["clients"]: - print(f"✗ 客户端不在房间中: {client_id}") - return False - - # 移除客户端 - del room_data["clients"][client_id] - room_data["last_activity"] = time.time() - - # 更新主机(如果移除的是主机) - if room_data["host_client_id"] == client_id: - if room_data["clients"]: - # 选择第一个客户端作为新主机 - room_data["host_client_id"] = next(iter(room_data["clients"])) - else: - room_data["host_client_id"] = None - - # 移除客户端房间映射 - if client_id in self.client_rooms: - del self.client_rooms[client_id] - - self.room_state["clients_in_rooms"] -= 1 - self.room_stats["clients_left"] += 1 - - # 触发客户端离开回调 - self._trigger_room_callback("client_left", { - "room_id": room_id, - "room_name": room_data["room_name"], - "client_id": client_id, - "timestamp": time.time() - }) - - # 通知客户端离开成功 - leave_success_message = { - "type": "room_leave_success", - "room_id": room_id, - "timestamp": time.time() - } - - if self.plugin.websocket_server: - self.plugin.websocket_server.send_message_to_client(client_id, leave_success_message) - - # 如果房间为空,销毁房间 - if len(room_data["clients"]) == 0: - self.destroy_room(room_id) - - print(f"✓ 客户端已离开房间: {client_id} <- {room_id}") - return True - - except Exception as e: - print(f"✗ 从房间移除客户端失败: {e}") - self.room_stats["errors"] += 1 - return False - - def _verify_room_password(self, room_id: str, client_data: Dict[str, Any]) -> bool: - """ - 验证房间密码 - - Args: - room_id: 房间ID - client_data: 客户端数据 - - Returns: - 密码是否正确 - """ - try: - if not self.room_config["enable_room_passwords"]: - return True - - with self.room_lock: - if room_id not in self.rooms: - return False - - room_data = self.rooms[room_id] - if not room_data["password"]: - return True - - client_password = client_data.get("password") if client_data else None - return client_password == room_data["password"] - - except Exception as e: - print(f"✗ 房间密码验证失败: {e}") - self.room_stats["errors"] += 1 - return False - - def get_room_info(self, room_id: str) -> Optional[Dict[str, Any]]: - """ - 获取房间信息 - - Args: - room_id: 房间ID - - Returns: - 房间信息或None - """ - try: - with self.room_lock: - if room_id in self.rooms: - room_data = self.rooms[room_id] - # 返回不包含敏感信息的房间信息 - safe_room_data = { - "room_id": room_data["room_id"], - "room_name": room_data["room_name"], - "settings": room_data["settings"], - "client_count": len(room_data["clients"]), - "has_password": bool(room_data["password"]), - "host_client_id": room_data["host_client_id"], - "created_time": room_data["created_time"], - "last_activity": room_data["last_activity"] - } - return safe_room_data - else: - return None - - except Exception as e: - print(f"✗ 获取房间信息失败: {e}") - self.room_stats["errors"] += 1 - return None - - def get_all_rooms(self) -> List[Dict[str, Any]]: - """ - 获取所有房间信息 - - Returns: - 房间信息列表 - """ - try: - rooms_info = [] - with self.room_lock: - for room_id, room_data in self.rooms.items(): - # 只返回公开房间或用户所在的房间 - if room_data["settings"].get("is_public", True): - safe_room_data = { - "room_id": room_data["room_id"], - "room_name": room_data["room_name"], - "settings": room_data["settings"], - "client_count": len(room_data["clients"]), - "has_password": bool(room_data["password"]), - "host_client_id": room_data["host_client_id"], - "created_time": room_data["created_time"], - "last_activity": room_data["last_activity"] - } - rooms_info.append(safe_room_data) - - return rooms_info - - except Exception as e: - print(f"✗ 获取所有房间信息失败: {e}") - self.room_stats["errors"] += 1 - return [] - - def get_client_room(self, client_id: str) -> Optional[str]: - """ - 获取客户端所在房间 - - Args: - client_id: 客户端ID - - Returns: - 房间ID或None - """ - try: - return self.client_rooms.get(client_id) - except Exception as e: - print(f"✗ 获取客户端房间失败: {e}") - self.room_stats["errors"] += 1 - return None - - def get_room_clients(self, room_id: str) -> List[str]: - """ - 获取房间内所有客户端 - - Args: - room_id: 房间ID - - Returns: - 客户端ID列表 - """ - try: - with self.room_lock: - if room_id in self.rooms: - return list(self.rooms[room_id]["clients"].keys()) - else: - return [] - - except Exception as e: - print(f"✗ 获取房间客户端失败: {e}") - self.room_stats["errors"] += 1 - return [] - - def update_room_state(self, room_id: str, state_data: Dict[str, Any]) -> bool: - """ - 更新房间状态 - - Args: - room_id: 房间ID - state_data: 状态数据 - - Returns: - 是否更新成功 - """ - try: - with self.room_lock: - if room_id not in self.rooms: - return False - - self.rooms[room_id]["room_state"].update(state_data) - self.rooms[room_id]["last_activity"] = time.time() - - # 触发房间更新回调 - self._trigger_room_callback("room_updated", { - "room_id": room_id, - "state_data": state_data, - "timestamp": time.time() - }) - - return True - - except Exception as e: - print(f"✗ 更新房间状态失败: {e}") - self.room_stats["errors"] += 1 - return False - - def broadcast_to_room(self, room_id: str, message_data: Dict[str, Any], - exclude_client_id: str = None) -> bool: - """ - 向房间内所有客户端广播消息 - - Args: - room_id: 房间ID - message_data: 消息数据 - exclude_client_id: 要排除的客户端ID - - Returns: - 是否广播成功 - """ - try: - if not self.enabled: - return False - - with self.room_lock: - if room_id not in self.rooms: - return False - - room_data = self.rooms[room_id] - success_count = 0 - - # 向房间内每个客户端发送消息 - for client_id in room_data["clients"]: - # 跳过排除的客户端 - if client_id == exclude_client_id: - continue - - # 发送消息 - if self.plugin.websocket_server: - send_success = self.plugin.websocket_server.send_message_to_client( - client_id, message_data) - if send_success: - success_count += 1 - - if success_count > 0: - self.room_stats["messages_broadcast"] += 1 - - return success_count > 0 - - except Exception as e: - print(f"✗ 房间广播失败: {e}") - self.room_stats["errors"] += 1 - return False - - def set_room_custom_data(self, room_id: str, key: str, value: Any) -> bool: - """ - 设置房间自定义数据 - - Args: - room_id: 房间ID - key: 数据键 - value: 数据值 - - Returns: - 是否设置成功 - """ - try: - with self.room_lock: - if room_id not in self.rooms: - return False - - self.rooms[room_id]["custom_data"][key] = value - return True - - except Exception as e: - print(f"✗ 设置房间自定义数据失败: {e}") - self.room_stats["errors"] += 1 - return False - - def get_room_custom_data(self, room_id: str, key: str, default: Any = None) -> Any: - """ - 获取房间自定义数据 - - Args: - room_id: 房间ID - key: 数据键 - default: 默认值 - - Returns: - 数据值或默认值 - """ - try: - with self.room_lock: - if room_id not in self.rooms: - return default - - return self.rooms[room_id]["custom_data"].get(key, default) - - except Exception as e: - print(f"✗ 获取房间自定义数据失败: {e}") - self.room_stats["errors"] += 1 - return default - - def get_room_stats(self) -> Dict[str, Any]: - """ - 获取房间统计信息 - - Returns: - 房间统计字典 - """ - return { - "state": self.room_state.copy(), - "stats": self.room_stats.copy(), - "config": self.room_config.copy(), - "current_rooms": len(self.rooms), - "current_clients_in_rooms": len(self.client_rooms) - } - - def reset_stats(self): - """重置房间统计信息""" - try: - self.room_stats = { - "rooms_created": 0, - "rooms_destroyed": 0, - "clients_joined": 0, - "clients_left": 0, - "messages_broadcast": 0, - "errors": 0 - } - print("✓ 房间统计信息已重置") - except Exception as e: - print(f"✗ 房间统计信息重置失败: {e}") - - def set_room_config(self, config: Dict[str, Any]) -> bool: - """ - 设置房间配置 - - Args: - config: 房间配置字典 - - Returns: - 是否设置成功 - """ - try: - self.room_config.update(config) - print(f"✓ 房间配置已更新: {self.room_config}") - return True - except Exception as e: - print(f"✗ 房间配置设置失败: {e}") - return False - - def get_room_config(self) -> Dict[str, Any]: - """ - 获取房间配置 - - Returns: - 房间配置字典 - """ - return self.room_config.copy() - - def _trigger_room_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发房间回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.room_callbacks: - for callback in self.room_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 房间回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 房间回调触发失败: {e}") - - def register_room_callback(self, callback_type: str, callback: callable): - """ - 注册房间回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.room_callbacks: - self.room_callbacks[callback_type].append(callback) - print(f"✓ 房间回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 房间回调注册失败: {e}") - - def unregister_room_callback(self, callback_type: str, callback: callable): - """ - 注销房间回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.room_callbacks: - if callback in self.room_callbacks[callback_type]: - self.room_callbacks[callback_type].remove(callback) - print(f"✓ 房间回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +房间管理器模块 +管理通信组和广播消息 +""" + +import time +import threading +import uuid +from typing import Dict, Any, List, Optional + +class RoomManager: + """ + 房间管理器 + 管理通信组和广播消息 + """ + + def __init__(self, plugin): + """ + 初始化房间管理器 + + Args: + plugin: 实时通信插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 房间配置 + self.room_config = { + "max_rooms": 1000, + "max_clients_per_room": 100, + "enable_room_persistence": False, + "room_timeout": 3600, # 1小时无活动房间自动关闭 + "enable_room_passwords": True, + "enable_room_visibility": True, + "default_room_settings": { + "max_clients": 50, + "is_public": True, + "allow_spectators": True, + "enable_voice_chat": True + } + } + + # 房间状态 + self.room_state = { + "total_rooms": 0, + "active_rooms": 0, + "clients_in_rooms": 0 + } + + # 房间存储 + self.rooms = {} + self.client_rooms = {} # 客户端所在房间映射 + self.room_lock = threading.RLock() + + # 房间统计 + self.room_stats = { + "rooms_created": 0, + "rooms_destroyed": 0, + "clients_joined": 0, + "clients_left": 0, + "messages_broadcast": 0, + "errors": 0 + } + + # 回调函数 + self.room_callbacks = { + "room_created": [], + "room_destroyed": [], + "client_joined": [], + "client_left": [], + "room_updated": [], + "room_error": [] + } + + # 时间戳记录 + self.last_cleanup = 0.0 + self.last_room_check = 0.0 + + print("✓ 房间管理器已创建") + + def initialize(self) -> bool: + """ + 初始化房间管理器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化房间管理器...") + + self.initialized = True + print("✓ 房间管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 房间管理器初始化失败: {e}") + self.room_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用房间管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 房间管理器未初始化") + return False + + self.enabled = True + print("✓ 房间管理器已启用") + return True + + except Exception as e: + print(f"✗ 房间管理器启用失败: {e}") + self.room_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用房间管理器""" + try: + self.enabled = False + + # 销毁所有房间 + self._destroy_all_rooms() + + print("✓ 房间管理器已禁用") + + except Exception as e: + print(f"✗ 房间管理器禁用失败: {e}") + self.room_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理房间管理器资源""" + try: + # 禁用房间管理器 + if self.enabled: + self.disable() + + # 清理回调 + self.room_callbacks.clear() + + self.initialized = False + print("✓ 房间管理器资源已清理") + + except Exception as e: + print(f"✗ 房间管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新房间管理器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 定期清理过期房间 + if current_time - self.last_cleanup > 60.0: # 每分钟清理一次 + self._cleanup_expired_rooms(current_time) + self.last_cleanup = current_time + + except Exception as e: + print(f"✗ 房间管理器更新失败: {e}") + self.room_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def _cleanup_expired_rooms(self, current_time: float): + """清理过期房间""" + try: + expired_rooms = [] + with self.room_lock: + for room_id, room_data in self.rooms.items(): + # 检查房间是否超时 + if current_time - room_data["last_activity"] > self.room_config["room_timeout"]: + # 检查房间是否为空 + if len(room_data["clients"]) == 0: + expired_rooms.append(room_id) + + # 销毁过期房间 + for room_id in expired_rooms: + self.destroy_room(room_id) + + except Exception as e: + print(f"✗ 过期房间清理失败: {e}") + self.room_stats["errors"] += 1 + + def _destroy_all_rooms(self): + """销毁所有房间""" + try: + with self.room_lock: + room_ids = list(self.rooms.keys()) + + for room_id in room_ids: + self.destroy_room(room_id) + + except Exception as e: + print(f"✗ 销毁所有房间失败: {e}") + self.room_stats["errors"] += 1 + + def create_room(self, room_name: str = None, room_settings: Dict[str, Any] = None, + room_password: str = None) -> Optional[str]: + """ + 创建房间 + + Args: + room_name: 房间名称 + room_settings: 房间设置 + room_password: 房间密码 + + Returns: + 房间ID或None + """ + try: + if not self.enabled: + return None + + # 检查房间数量限制 + with self.room_lock: + if len(self.rooms) >= self.room_config["max_rooms"]: + print("✗ 房间数量已达上限") + return None + + # 生成房间ID + room_id = str(uuid.uuid4()) + + # 使用默认设置或传入设置 + settings = self.room_config["default_room_settings"].copy() + if room_settings: + settings.update(room_settings) + + # 创建房间数据 + room_data = { + "room_id": room_id, + "room_name": room_name or f"房间-{room_id[:8]}", + "settings": settings, + "password": room_password, + "clients": {}, # 客户端ID到客户端数据的映射 + "host_client_id": None, + "created_time": time.time(), + "last_activity": time.time(), + "room_state": {}, # 房间状态数据 + "custom_data": {} # 自定义房间数据 + } + + # 添加到房间列表 + with self.room_lock: + self.rooms[room_id] = room_data + + self.room_state["total_rooms"] += 1 + self.room_state["active_rooms"] += 1 + self.room_stats["rooms_created"] += 1 + + # 触发房间创建回调 + self._trigger_room_callback("room_created", { + "room_id": room_id, + "room_name": room_data["room_name"], + "settings": settings, + "timestamp": room_data["created_time"] + }) + + print(f"✓ 房间已创建: {room_id} ({room_data['room_name']})") + return room_id + + except Exception as e: + print(f"✗ 房间创建失败: {e}") + self.room_stats["errors"] += 1 + return None + + def destroy_room(self, room_id: str) -> bool: + """ + 销毁房间 + + Args: + room_id: 房间ID + + Returns: + 是否销毁成功 + """ + try: + if not self.enabled: + return False + + # 检查房间是否存在 + with self.room_lock: + if room_id not in self.rooms: + print(f"✗ 房间不存在: {room_id}") + return False + + room_data = self.rooms[room_id] + + # 通知房间内所有客户端 + for client_id in list(room_data["clients"].keys()): + self._notify_client_room_destroyed(client_id, room_id) + + # 移除房间 + del self.rooms[room_id] + + self.room_state["active_rooms"] -= 1 + self.room_stats["rooms_destroyed"] += 1 + + # 触发房间销毁回调 + self._trigger_room_callback("room_destroyed", { + "room_id": room_id, + "room_name": room_data["room_name"], + "timestamp": time.time() + }) + + print(f"✓ 房间已销毁: {room_id}") + return True + + except Exception as e: + print(f"✗ 房间销毁失败: {e}") + self.room_stats["errors"] += 1 + return False + + def _notify_client_room_destroyed(self, client_id: str, room_id: str): + """ + 通知客户端房间被销毁 + + Args: + client_id: 客户端ID + room_id: 房间ID + """ + try: + # 从客户端房间映射中移除 + if client_id in self.client_rooms: + del self.client_rooms[client_id] + + # 发送房间销毁通知 + if self.plugin.websocket_server: + destroy_message = { + "type": "room_destroyed", + "room_id": room_id, + "timestamp": time.time() + } + + self.plugin.websocket_server.send_message_to_client(client_id, destroy_message) + + except Exception as e: + print(f"✗ 通知客户端房间销毁失败: {e}") + self.room_stats["errors"] += 1 + + def add_client_to_room(self, room_id: str, client_id: str, client_data: Dict[str, Any] = None) -> bool: + """ + 添加客户端到房间 + + Args: + room_id: 房间ID + client_id: 客户端ID + client_data: 客户端数据 + + Returns: + 是否添加成功 + """ + try: + if not self.enabled: + return False + + # 检查客户端是否已认证 + if self.plugin.client_manager and not self.plugin.client_manager.is_client_authenticated(client_id): + print(f"✗ 客户端未认证: {client_id}") + return False + + # 检查房间是否存在 + with self.room_lock: + if room_id not in self.rooms: + print(f"✗ 房间不存在: {room_id}") + return False + + room_data = self.rooms[room_id] + + # 检查房间是否已满 + if len(room_data["clients"]) >= room_data["settings"]["max_clients"]: + print(f"✗ 房间已满: {room_id}") + return False + + # 检查房间密码 + if room_data["password"] and not self._verify_room_password(room_id, client_data): + print(f"✗ 房间密码错误: {room_id}") + return False + + # 添加客户端到房间 + client_info = client_data or {} + client_info["join_time"] = time.time() + client_info["last_activity"] = time.time() + + room_data["clients"][client_id] = client_info + room_data["last_activity"] = time.time() + + # 设置房间主机(如果是第一个客户端) + if room_data["host_client_id"] is None: + room_data["host_client_id"] = client_id + + # 更新客户端房间映射 + self.client_rooms[client_id] = room_id + + self.room_state["clients_in_rooms"] += 1 + self.room_stats["clients_joined"] += 1 + + # 触发客户端加入回调 + self._trigger_room_callback("client_joined", { + "room_id": room_id, + "room_name": room_data["room_name"], + "client_id": client_id, + "timestamp": time.time() + }) + + # 通知客户端加入成功 + join_success_message = { + "type": "room_join_success", + "room_id": room_id, + "room_name": room_data["room_name"], + "timestamp": time.time() + } + + if self.plugin.websocket_server: + self.plugin.websocket_server.send_message_to_client(client_id, join_success_message) + + print(f"✓ 客户端已加入房间: {client_id} -> {room_id}") + return True + + except Exception as e: + print(f"✗ 添加客户端到房间失败: {e}") + self.room_stats["errors"] += 1 + return False + + def remove_client_from_room(self, room_id: str, client_id: str) -> bool: + """ + 从房间移除客户端 + + Args: + room_id: 房间ID + client_id: 客户端ID + + Returns: + 是否移除成功 + """ + try: + if not self.enabled: + return False + + # 检查房间是否存在 + with self.room_lock: + if room_id not in self.rooms: + print(f"✗ 房间不存在: {room_id}") + return False + + room_data = self.rooms[room_id] + + # 检查客户端是否在房间中 + if client_id not in room_data["clients"]: + print(f"✗ 客户端不在房间中: {client_id}") + return False + + # 移除客户端 + del room_data["clients"][client_id] + room_data["last_activity"] = time.time() + + # 更新主机(如果移除的是主机) + if room_data["host_client_id"] == client_id: + if room_data["clients"]: + # 选择第一个客户端作为新主机 + room_data["host_client_id"] = next(iter(room_data["clients"])) + else: + room_data["host_client_id"] = None + + # 移除客户端房间映射 + if client_id in self.client_rooms: + del self.client_rooms[client_id] + + self.room_state["clients_in_rooms"] -= 1 + self.room_stats["clients_left"] += 1 + + # 触发客户端离开回调 + self._trigger_room_callback("client_left", { + "room_id": room_id, + "room_name": room_data["room_name"], + "client_id": client_id, + "timestamp": time.time() + }) + + # 通知客户端离开成功 + leave_success_message = { + "type": "room_leave_success", + "room_id": room_id, + "timestamp": time.time() + } + + if self.plugin.websocket_server: + self.plugin.websocket_server.send_message_to_client(client_id, leave_success_message) + + # 如果房间为空,销毁房间 + if len(room_data["clients"]) == 0: + self.destroy_room(room_id) + + print(f"✓ 客户端已离开房间: {client_id} <- {room_id}") + return True + + except Exception as e: + print(f"✗ 从房间移除客户端失败: {e}") + self.room_stats["errors"] += 1 + return False + + def _verify_room_password(self, room_id: str, client_data: Dict[str, Any]) -> bool: + """ + 验证房间密码 + + Args: + room_id: 房间ID + client_data: 客户端数据 + + Returns: + 密码是否正确 + """ + try: + if not self.room_config["enable_room_passwords"]: + return True + + with self.room_lock: + if room_id not in self.rooms: + return False + + room_data = self.rooms[room_id] + if not room_data["password"]: + return True + + client_password = client_data.get("password") if client_data else None + return client_password == room_data["password"] + + except Exception as e: + print(f"✗ 房间密码验证失败: {e}") + self.room_stats["errors"] += 1 + return False + + def get_room_info(self, room_id: str) -> Optional[Dict[str, Any]]: + """ + 获取房间信息 + + Args: + room_id: 房间ID + + Returns: + 房间信息或None + """ + try: + with self.room_lock: + if room_id in self.rooms: + room_data = self.rooms[room_id] + # 返回不包含敏感信息的房间信息 + safe_room_data = { + "room_id": room_data["room_id"], + "room_name": room_data["room_name"], + "settings": room_data["settings"], + "client_count": len(room_data["clients"]), + "has_password": bool(room_data["password"]), + "host_client_id": room_data["host_client_id"], + "created_time": room_data["created_time"], + "last_activity": room_data["last_activity"] + } + return safe_room_data + else: + return None + + except Exception as e: + print(f"✗ 获取房间信息失败: {e}") + self.room_stats["errors"] += 1 + return None + + def get_all_rooms(self) -> List[Dict[str, Any]]: + """ + 获取所有房间信息 + + Returns: + 房间信息列表 + """ + try: + rooms_info = [] + with self.room_lock: + for room_id, room_data in self.rooms.items(): + # 只返回公开房间或用户所在的房间 + if room_data["settings"].get("is_public", True): + safe_room_data = { + "room_id": room_data["room_id"], + "room_name": room_data["room_name"], + "settings": room_data["settings"], + "client_count": len(room_data["clients"]), + "has_password": bool(room_data["password"]), + "host_client_id": room_data["host_client_id"], + "created_time": room_data["created_time"], + "last_activity": room_data["last_activity"] + } + rooms_info.append(safe_room_data) + + return rooms_info + + except Exception as e: + print(f"✗ 获取所有房间信息失败: {e}") + self.room_stats["errors"] += 1 + return [] + + def get_client_room(self, client_id: str) -> Optional[str]: + """ + 获取客户端所在房间 + + Args: + client_id: 客户端ID + + Returns: + 房间ID或None + """ + try: + return self.client_rooms.get(client_id) + except Exception as e: + print(f"✗ 获取客户端房间失败: {e}") + self.room_stats["errors"] += 1 + return None + + def get_room_clients(self, room_id: str) -> List[str]: + """ + 获取房间内所有客户端 + + Args: + room_id: 房间ID + + Returns: + 客户端ID列表 + """ + try: + with self.room_lock: + if room_id in self.rooms: + return list(self.rooms[room_id]["clients"].keys()) + else: + return [] + + except Exception as e: + print(f"✗ 获取房间客户端失败: {e}") + self.room_stats["errors"] += 1 + return [] + + def update_room_state(self, room_id: str, state_data: Dict[str, Any]) -> bool: + """ + 更新房间状态 + + Args: + room_id: 房间ID + state_data: 状态数据 + + Returns: + 是否更新成功 + """ + try: + with self.room_lock: + if room_id not in self.rooms: + return False + + self.rooms[room_id]["room_state"].update(state_data) + self.rooms[room_id]["last_activity"] = time.time() + + # 触发房间更新回调 + self._trigger_room_callback("room_updated", { + "room_id": room_id, + "state_data": state_data, + "timestamp": time.time() + }) + + return True + + except Exception as e: + print(f"✗ 更新房间状态失败: {e}") + self.room_stats["errors"] += 1 + return False + + def broadcast_to_room(self, room_id: str, message_data: Dict[str, Any], + exclude_client_id: str = None) -> bool: + """ + 向房间内所有客户端广播消息 + + Args: + room_id: 房间ID + message_data: 消息数据 + exclude_client_id: 要排除的客户端ID + + Returns: + 是否广播成功 + """ + try: + if not self.enabled: + return False + + with self.room_lock: + if room_id not in self.rooms: + return False + + room_data = self.rooms[room_id] + success_count = 0 + + # 向房间内每个客户端发送消息 + for client_id in room_data["clients"]: + # 跳过排除的客户端 + if client_id == exclude_client_id: + continue + + # 发送消息 + if self.plugin.websocket_server: + send_success = self.plugin.websocket_server.send_message_to_client( + client_id, message_data) + if send_success: + success_count += 1 + + if success_count > 0: + self.room_stats["messages_broadcast"] += 1 + + return success_count > 0 + + except Exception as e: + print(f"✗ 房间广播失败: {e}") + self.room_stats["errors"] += 1 + return False + + def set_room_custom_data(self, room_id: str, key: str, value: Any) -> bool: + """ + 设置房间自定义数据 + + Args: + room_id: 房间ID + key: 数据键 + value: 数据值 + + Returns: + 是否设置成功 + """ + try: + with self.room_lock: + if room_id not in self.rooms: + return False + + self.rooms[room_id]["custom_data"][key] = value + return True + + except Exception as e: + print(f"✗ 设置房间自定义数据失败: {e}") + self.room_stats["errors"] += 1 + return False + + def get_room_custom_data(self, room_id: str, key: str, default: Any = None) -> Any: + """ + 获取房间自定义数据 + + Args: + room_id: 房间ID + key: 数据键 + default: 默认值 + + Returns: + 数据值或默认值 + """ + try: + with self.room_lock: + if room_id not in self.rooms: + return default + + return self.rooms[room_id]["custom_data"].get(key, default) + + except Exception as e: + print(f"✗ 获取房间自定义数据失败: {e}") + self.room_stats["errors"] += 1 + return default + + def get_room_stats(self) -> Dict[str, Any]: + """ + 获取房间统计信息 + + Returns: + 房间统计字典 + """ + return { + "state": self.room_state.copy(), + "stats": self.room_stats.copy(), + "config": self.room_config.copy(), + "current_rooms": len(self.rooms), + "current_clients_in_rooms": len(self.client_rooms) + } + + def reset_stats(self): + """重置房间统计信息""" + try: + self.room_stats = { + "rooms_created": 0, + "rooms_destroyed": 0, + "clients_joined": 0, + "clients_left": 0, + "messages_broadcast": 0, + "errors": 0 + } + print("✓ 房间统计信息已重置") + except Exception as e: + print(f"✗ 房间统计信息重置失败: {e}") + + def set_room_config(self, config: Dict[str, Any]) -> bool: + """ + 设置房间配置 + + Args: + config: 房间配置字典 + + Returns: + 是否设置成功 + """ + try: + self.room_config.update(config) + print(f"✓ 房间配置已更新: {self.room_config}") + return True + except Exception as e: + print(f"✗ 房间配置设置失败: {e}") + return False + + def get_room_config(self) -> Dict[str, Any]: + """ + 获取房间配置 + + Returns: + 房间配置字典 + """ + return self.room_config.copy() + + def _trigger_room_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发房间回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.room_callbacks: + for callback in self.room_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 房间回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 房间回调触发失败: {e}") + + def register_room_callback(self, callback_type: str, callback: callable): + """ + 注册房间回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.room_callbacks: + self.room_callbacks[callback_type].append(callback) + print(f"✓ 房间回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 房间回调注册失败: {e}") + + def unregister_room_callback(self, callback_type: str, callback: callable): + """ + 注销房间回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.room_callbacks: + if callback in self.room_callbacks[callback_type]: + self.room_callbacks[callback_type].remove(callback) + print(f"✓ 房间回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 房间回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/realtime_communication/serialization/data_serializer.py b/plugins/user/realtime_communication/serialization/data_serializer.py index d4ca9e0f..f41b5f30 100644 --- a/plugins/user/realtime_communication/serialization/data_serializer.py +++ b/plugins/user/realtime_communication/serialization/data_serializer.py @@ -1,773 +1,773 @@ -""" -数据序列化器模块 -将消息序列化为网络数据 -""" - -import time -import json -import pickle -import zlib -from typing import Dict, Any, List, Optional, Union - -class DataSerializer: - """ - 数据序列化器 - 将消息序列化为网络数据 - """ - - def __init__(self, plugin): - """ - 初始化数据序列化器 - - Args: - plugin: 实时通信插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 序列化配置 - self.serializer_config = { - "default_format": "json", - "supported_formats": ["json", "pickle", "binary"], - "enable_compression": True, - "compression_threshold": 1024, # 1KB - "enable_caching": True, - "cache_size": 1000, - "enable_incremental_encoding": True, - "enable_type_preservation": True - } - - # 序列化状态 - self.serializer_state = { - "cached_objects": 0, - "compressed_objects": 0, - "serialized_objects": 0 - } - - # 序列化统计 - self.serializer_stats = { - "objects_serialized": 0, - "objects_deserialized": 0, - "bytes_serialized": 0, - "bytes_deserialized": 0, - "compression_savings": 0, - "cache_hits": 0, - "cache_misses": 0, - "serializer_errors": 0 - } - - # 缓存管理 - self.serialization_cache = {} - self.cache_timestamps = {} - - # 序列化器映射 - self.serializers = { - "json": { - "serialize": self._serialize_json, - "deserialize": self._deserialize_json - }, - "pickle": { - "serialize": self._serialize_pickle, - "deserialize": self._deserialize_pickle - }, - "binary": { - "serialize": self._serialize_binary, - "deserialize": self._deserialize_binary - } - } - - # 回调函数 - self.serializer_callbacks = { - "object_serialized": [], - "object_deserialized": [], - "compression_applied": [], - "cache_hit": [], - "serializer_error": [] - } - - # 时间戳记录 - self.last_serialization = 0.0 - self.last_cache_cleanup = 0.0 - - print("✓ 数据序列化器已创建") - - def initialize(self) -> bool: - """ - 初始化数据序列化器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化数据序列化器...") - - self.initialized = True - print("✓ 数据序列化器初始化完成") - return True - - except Exception as e: - print(f"✗ 数据序列化器初始化失败: {e}") - self.serializer_stats["serializer_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用数据序列化器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 数据序列化器未初始化") - return False - - self.enabled = True - print("✓ 数据序列化器已启用") - return True - - except Exception as e: - print(f"✗ 数据序列化器启用失败: {e}") - self.serializer_stats["serializer_errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用数据序列化器""" - try: - self.enabled = False - - # 清理缓存 - self.serialization_cache.clear() - self.cache_timestamps.clear() - - print("✓ 数据序列化器已禁用") - - except Exception as e: - print(f"✗ 数据序列化器禁用失败: {e}") - self.serializer_stats["serializer_errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理数据序列化器资源""" - try: - # 禁用数据序列化器 - if self.enabled: - self.disable() - - # 清理回调 - self.serializer_callbacks.clear() - - self.initialized = False - print("✓ 数据序列化器资源已清理") - - except Exception as e: - print(f"✗ 数据序列化器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新数据序列化器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 定期清理缓存 - if current_time - self.last_cache_cleanup > 300: # 每5分钟清理一次 - self._cleanup_cache(current_time) - self.last_cache_cleanup = current_time - - self.last_serialization = current_time - - except Exception as e: - print(f"✗ 数据序列化器更新失败: {e}") - self.serializer_stats["serializer_errors"] += 1 - import traceback - traceback.print_exc() - - def _cleanup_cache(self, current_time: float): - """清理缓存""" - try: - if not self.serializer_config["enable_caching"]: - return - - expired_keys = [] - for key, timestamp in self.cache_timestamps.items(): - # 缓存对象超过10分钟未使用则清除 - if current_time - timestamp > 600: - expired_keys.append(key) - - for key in expired_keys: - if key in self.serialization_cache: - del self.serialization_cache[key] - if key in self.cache_timestamps: - del self.cache_timestamps[key] - - self.serializer_state["cached_objects"] = len(self.serialization_cache) - - except Exception as e: - print(f"✗ 缓存清理失败: {e}") - self.serializer_stats["serializer_errors"] += 1 - - def serialize(self, obj: Any, format: str = None) -> Optional[bytes]: - """ - 序列化对象 - - Args: - obj: 要序列化的对象 - format: 序列化格式 - - Returns: - 序列化后的字节数据或None - """ - try: - if not self.enabled: - return None - - # 使用默认格式或指定格式 - if format is None: - format = self.serializer_config["default_format"] - - # 验证格式支持 - if format not in self.serializers: - print(f"✗ 不支持的序列化格式: {format}") - self.serializer_stats["serializer_errors"] += 1 - return None - - # 检查缓存 - cache_key = None - if self.serializer_config["enable_caching"]: - cache_key = self._generate_cache_key(obj, format) - if cache_key in self.serialization_cache: - self.serializer_stats["cache_hits"] += 1 - self.cache_timestamps[cache_key] = time.time() - - # 触发缓存命中回调 - self._trigger_serializer_callback("cache_hit", { - "cache_key": cache_key, - "format": format, - "timestamp": time.time() - }) - - return self.serialization_cache[cache_key] - else: - self.serializer_stats["cache_misses"] += 1 - - # 获取序列化器 - serializer = self.serializers[format]["serialize"] - - # 序列化对象 - serialized_data = serializer(obj) - if serialized_data is None: - return None - - # 更新统计 - self.serializer_stats["objects_serialized"] += 1 - self.serializer_stats["bytes_serialized"] += len(serialized_data) - self.serializer_state["serialized_objects"] += 1 - - # 缓存结果 - if (self.serializer_config["enable_caching"] and - cache_key and - len(self.serialization_cache) < self.serializer_config["cache_size"]): - self.serialization_cache[cache_key] = serialized_data - self.cache_timestamps[cache_key] = time.time() - self.serializer_state["cached_objects"] = len(self.serialization_cache) - - # 触发对象序列化回调 - self._trigger_serializer_callback("object_serialized", { - "format": format, - "data_size": len(serialized_data), - "timestamp": time.time() - }) - - return serialized_data - - except Exception as e: - print(f"✗ 对象序列化失败: {e}") - self.serializer_stats["serializer_errors"] += 1 - return None - - def deserialize(self, data: bytes, format: str = None) -> Optional[Any]: - """ - 反序列化对象 - - Args: - data: 要反序列化的字节数据 - format: 序列化格式 - - Returns: - 反序列化后的对象或None - """ - try: - if not self.enabled: - return None - - # 使用默认格式或指定格式 - if format is None: - format = self.serializer_config["default_format"] - - # 验证格式支持 - if format not in self.serializers: - print(f"✗ 不支持的反序列化格式: {format}") - self.serializer_stats["serializer_errors"] += 1 - return None - - # 获取反序列化器 - deserializer = self.serializers[format]["deserialize"] - - # 反序列化对象 - deserialized_obj = deserializer(data) - if deserialized_obj is None: - return None - - # 更新统计 - self.serializer_stats["objects_deserialized"] += 1 - self.serializer_stats["bytes_deserialized"] += len(data) - - # 触发对象反序列化回调 - self._trigger_serializer_callback("object_deserialized", { - "format": format, - "data_size": len(data), - "timestamp": time.time() - }) - - return deserialized_obj - - except Exception as e: - print(f"✗ 对象反序列化失败: {e}") - self.serializer_stats["serializer_errors"] += 1 - return None - - def _serialize_json(self, obj: Any) -> Optional[bytes]: - """ - JSON序列化 - - Args: - obj: 要序列化的对象 - - Returns: - 序列化后的字节数据或None - """ - try: - # 序列化为JSON字符串 - json_string = json.dumps(obj, ensure_ascii=False, default=self._json_default) - serialized_data = json_string.encode('utf-8') - - # 检查是否需要压缩 - if (self.serializer_config["enable_compression"] and - len(serialized_data) > self.serializer_config["compression_threshold"]): - compressed_data = zlib.compress(serialized_data) - if len(compressed_data) < len(serialized_data): - # 使用压缩版本 - serialized_data = compressed_data - self.serializer_state["compressed_objects"] += 1 - self.serializer_stats["compression_savings"] += len(serialized_data) - len(compressed_data) - - # 触发压缩应用回调 - self._trigger_serializer_callback("compression_applied", { - "original_size": len(json_string.encode('utf-8')), - "compressed_size": len(compressed_data), - "compression_ratio": len(compressed_data) / len(json_string.encode('utf-8')), - "timestamp": time.time() - }) - - return serialized_data - - except Exception as e: - print(f"✗ JSON序列化失败: {e}") - self.serializer_stats["serializer_errors"] += 1 - return None - - def _deserialize_json(self, data: bytes) -> Optional[Any]: - """ - JSON反序列化 - - Args: - data: 要反序列化的字节数据 - - Returns: - 反序列化后的对象或None - """ - try: - # 尝试解压缩 - try: - decompressed_data = zlib.decompress(data) - data = decompressed_data - except zlib.error: - # 不是压缩数据,使用原始数据 - pass - - # 解码为JSON - json_string = data.decode('utf-8') - deserialized_obj = json.loads(json_string) - - return deserialized_obj - - except Exception as e: - print(f"✗ JSON反序列化失败: {e}") - self.serializer_stats["serializer_errors"] += 1 - return None - - def _json_default(self, obj): - """ - JSON默认序列化方法 - - Args: - obj: 要序列化的对象 - - Returns: - 可序列化的对象 - """ - # 处理不能直接序列化的对象 - return str(obj) - - def _serialize_pickle(self, obj: Any) -> Optional[bytes]: - """ - Pickle序列化 - - Args: - obj: 要序列化的对象 - - Returns: - 序列化后的字节数据或None - """ - try: - # 序列化为pickle数据 - pickle_data = pickle.dumps(obj) - - # 检查是否需要压缩 - if (self.serializer_config["enable_compression"] and - len(pickle_data) > self.serializer_config["compression_threshold"]): - compressed_data = zlib.compress(pickle_data) - if len(compressed_data) < len(pickle_data): - # 使用压缩版本 - pickle_data = compressed_data - self.serializer_state["compressed_objects"] += 1 - self.serializer_stats["compression_savings"] += len(pickle_data) - len(compressed_data) - - # 触发压缩应用回调 - self._trigger_serializer_callback("compression_applied", { - "original_size": len(pickle.dumps(obj)), - "compressed_size": len(compressed_data), - "compression_ratio": len(compressed_data) / len(pickle.dumps(obj)), - "timestamp": time.time() - }) - - return pickle_data - - except Exception as e: - print(f"✗ Pickle序列化失败: {e}") - self.serializer_stats["serializer_errors"] += 1 - return None - - def _deserialize_pickle(self, data: bytes) -> Optional[Any]: - """ - Pickle反序列化 - - Args: - data: 要反序列化的字节数据 - - Returns: - 反序列化后的对象或None - """ - try: - # 尝试解压缩 - try: - decompressed_data = zlib.decompress(data) - data = decompressed_data - except zlib.error: - # 不是压缩数据,使用原始数据 - pass - - # 反序列化pickle数据 - deserialized_obj = pickle.loads(data) - - return deserialized_obj - - except Exception as e: - print(f"✗ Pickle反序列化失败: {e}") - self.serializer_stats["serializer_errors"] += 1 - return None - - def _serialize_binary(self, obj: Any) -> Optional[bytes]: - """ - 二进制序列化 - - Args: - obj: 要序列化的对象 - - Returns: - 序列化后的字节数据或None - """ - try: - # 简化实现:将对象转换为JSON再编码为二进制 - json_string = json.dumps(obj, ensure_ascii=False, default=self._json_default) - binary_data = json_string.encode('utf-8') - - # 添加简单的头部信息 - header = len(binary_data).to_bytes(4, byteorder='big') - serialized_data = header + binary_data - - # 检查是否需要压缩 - if (self.serializer_config["enable_compression"] and - len(serialized_data) > self.serializer_config["compression_threshold"]): - compressed_data = zlib.compress(serialized_data) - if len(compressed_data) < len(serialized_data): - # 使用压缩版本 - serialized_data = compressed_data - self.serializer_state["compressed_objects"] += 1 - self.serializer_stats["compression_savings"] += len(serialized_data) - len(compressed_data) - - # 触发压缩应用回调 - self._trigger_serializer_callback("compression_applied", { - "original_size": len(header + binary_data), - "compressed_size": len(compressed_data), - "compression_ratio": len(compressed_data) / len(header + binary_data), - "timestamp": time.time() - }) - - return serialized_data - - except Exception as e: - print(f"✗ 二进制序列化失败: {e}") - self.serializer_stats["serializer_errors"] += 1 - return None - - def _deserialize_binary(self, data: bytes) -> Optional[Any]: - """ - 二进制反序列化 - - Args: - data: 要反序列化的字节数据 - - Returns: - 反序列化后的对象或None - """ - try: - # 尝试解压缩 - try: - decompressed_data = zlib.decompress(data) - data = decompressed_data - except zlib.error: - # 不是压缩数据,使用原始数据 - pass - - # 解析头部信息 - if len(data) < 4: - raise ValueError("数据太短") - - message_length = int.from_bytes(data[:4], byteorder='big') - binary_data = data[4:4 + message_length] - - # 解码为JSON - json_string = binary_data.decode('utf-8') - deserialized_obj = json.loads(json_string) - - return deserialized_obj - - except Exception as e: - print(f"✗ 二进制反序列化失败: {e}") - self.serializer_stats["serializer_errors"] += 1 - return None - - def _generate_cache_key(self, obj: Any, format: str) -> str: - """ - 生成缓存键 - - Args: - obj: 对象 - format: 格式 - - Returns: - 缓存键 - """ - try: - # 简化实现:使用对象的字符串表示和格式生成键 - obj_str = str(obj) - cache_key = f"{format}:{hash(obj_str)}:{len(obj_str)}" - return cache_key - except Exception as e: - print(f"✗ 缓存键生成失败: {e}") - self.serializer_stats["serializer_errors"] += 1 - return f"error_key_{int(time.time() * 1000000)}" - - def add_serializer(self, format: str, serialize_func: callable, deserialize_func: callable) -> bool: - """ - 添加序列化器 - - Args: - format: 格式名称 - serialize_func: 序列化函数 - deserialize_func: 反序列化函数 - - Returns: - 是否添加成功 - """ - try: - self.serializers[format] = { - "serialize": serialize_func, - "deserialize": deserialize_func - } - - if format not in self.serializer_config["supported_formats"]: - self.serializer_config["supported_formats"].append(format) - - print(f"✓ 序列化器已添加: {format}") - return True - - except Exception as e: - print(f"✗ 序列化器添加失败: {e}") - self.serializer_stats["serializer_errors"] += 1 - return False - - def remove_serializer(self, format: str) -> bool: - """ - 移除序列化器 - - Args: - format: 格式名称 - - Returns: - 是否移除成功 - """ - try: - if format in self.serializers: - del self.serializers[format] - - if format in self.serializer_config["supported_formats"]: - self.serializer_config["supported_formats"].remove(format) - - print(f"✓ 序列化器已移除: {format}") - return True - else: - print(f"✗ 序列化器不存在: {format}") - return False - - except Exception as e: - print(f"✗ 序列化器移除失败: {e}") - self.serializer_stats["serializer_errors"] += 1 - return False - - def get_serializer_stats(self) -> Dict[str, Any]: - """ - 获取序列化统计信息 - - Returns: - 序列化统计字典 - """ - return { - "state": self.serializer_state.copy(), - "stats": self.serializer_stats.copy(), - "config": self.serializer_config.copy(), - "cache_size": len(self.serialization_cache) - } - - def reset_stats(self): - """重置序列化统计信息""" - try: - self.serializer_stats = { - "objects_serialized": 0, - "objects_deserialized": 0, - "bytes_serialized": 0, - "bytes_deserialized": 0, - "compression_savings": 0, - "cache_hits": 0, - "cache_misses": 0, - "serializer_errors": 0 - } - print("✓ 序列化统计信息已重置") - except Exception as e: - print(f"✗ 序列化统计信息重置失败: {e}") - - def set_serializer_config(self, config: Dict[str, Any]) -> bool: - """ - 设置序列化配置 - - Args: - config: 序列化配置字典 - - Returns: - 是否设置成功 - """ - try: - self.serializer_config.update(config) - print(f"✓ 序列化配置已更新: {self.serializer_config}") - return True - except Exception as e: - print(f"✗ 序列化配置设置失败: {e}") - return False - - def get_serializer_config(self) -> Dict[str, Any]: - """ - 获取序列化配置 - - Returns: - 序列化配置字典 - """ - return self.serializer_config.copy() - - def _trigger_serializer_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发序列化回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.serializer_callbacks: - for callback in self.serializer_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 序列化回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 序列化回调触发失败: {e}") - - def register_serializer_callback(self, callback_type: str, callback: callable): - """ - 注册序列化回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.serializer_callbacks: - self.serializer_callbacks[callback_type].append(callback) - print(f"✓ 序列化回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 序列化回调注册失败: {e}") - - def unregister_serializer_callback(self, callback_type: str, callback: callable): - """ - 注销序列化回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.serializer_callbacks: - if callback in self.serializer_callbacks[callback_type]: - self.serializer_callbacks[callback_type].remove(callback) - print(f"✓ 序列化回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +数据序列化器模块 +将消息序列化为网络数据 +""" + +import time +import json +import pickle +import zlib +from typing import Dict, Any, List, Optional, Union + +class DataSerializer: + """ + 数据序列化器 + 将消息序列化为网络数据 + """ + + def __init__(self, plugin): + """ + 初始化数据序列化器 + + Args: + plugin: 实时通信插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 序列化配置 + self.serializer_config = { + "default_format": "json", + "supported_formats": ["json", "pickle", "binary"], + "enable_compression": True, + "compression_threshold": 1024, # 1KB + "enable_caching": True, + "cache_size": 1000, + "enable_incremental_encoding": True, + "enable_type_preservation": True + } + + # 序列化状态 + self.serializer_state = { + "cached_objects": 0, + "compressed_objects": 0, + "serialized_objects": 0 + } + + # 序列化统计 + self.serializer_stats = { + "objects_serialized": 0, + "objects_deserialized": 0, + "bytes_serialized": 0, + "bytes_deserialized": 0, + "compression_savings": 0, + "cache_hits": 0, + "cache_misses": 0, + "serializer_errors": 0 + } + + # 缓存管理 + self.serialization_cache = {} + self.cache_timestamps = {} + + # 序列化器映射 + self.serializers = { + "json": { + "serialize": self._serialize_json, + "deserialize": self._deserialize_json + }, + "pickle": { + "serialize": self._serialize_pickle, + "deserialize": self._deserialize_pickle + }, + "binary": { + "serialize": self._serialize_binary, + "deserialize": self._deserialize_binary + } + } + + # 回调函数 + self.serializer_callbacks = { + "object_serialized": [], + "object_deserialized": [], + "compression_applied": [], + "cache_hit": [], + "serializer_error": [] + } + + # 时间戳记录 + self.last_serialization = 0.0 + self.last_cache_cleanup = 0.0 + + print("✓ 数据序列化器已创建") + + def initialize(self) -> bool: + """ + 初始化数据序列化器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化数据序列化器...") + + self.initialized = True + print("✓ 数据序列化器初始化完成") + return True + + except Exception as e: + print(f"✗ 数据序列化器初始化失败: {e}") + self.serializer_stats["serializer_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用数据序列化器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 数据序列化器未初始化") + return False + + self.enabled = True + print("✓ 数据序列化器已启用") + return True + + except Exception as e: + print(f"✗ 数据序列化器启用失败: {e}") + self.serializer_stats["serializer_errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用数据序列化器""" + try: + self.enabled = False + + # 清理缓存 + self.serialization_cache.clear() + self.cache_timestamps.clear() + + print("✓ 数据序列化器已禁用") + + except Exception as e: + print(f"✗ 数据序列化器禁用失败: {e}") + self.serializer_stats["serializer_errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理数据序列化器资源""" + try: + # 禁用数据序列化器 + if self.enabled: + self.disable() + + # 清理回调 + self.serializer_callbacks.clear() + + self.initialized = False + print("✓ 数据序列化器资源已清理") + + except Exception as e: + print(f"✗ 数据序列化器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新数据序列化器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 定期清理缓存 + if current_time - self.last_cache_cleanup > 300: # 每5分钟清理一次 + self._cleanup_cache(current_time) + self.last_cache_cleanup = current_time + + self.last_serialization = current_time + + except Exception as e: + print(f"✗ 数据序列化器更新失败: {e}") + self.serializer_stats["serializer_errors"] += 1 + import traceback + traceback.print_exc() + + def _cleanup_cache(self, current_time: float): + """清理缓存""" + try: + if not self.serializer_config["enable_caching"]: + return + + expired_keys = [] + for key, timestamp in self.cache_timestamps.items(): + # 缓存对象超过10分钟未使用则清除 + if current_time - timestamp > 600: + expired_keys.append(key) + + for key in expired_keys: + if key in self.serialization_cache: + del self.serialization_cache[key] + if key in self.cache_timestamps: + del self.cache_timestamps[key] + + self.serializer_state["cached_objects"] = len(self.serialization_cache) + + except Exception as e: + print(f"✗ 缓存清理失败: {e}") + self.serializer_stats["serializer_errors"] += 1 + + def serialize(self, obj: Any, format: str = None) -> Optional[bytes]: + """ + 序列化对象 + + Args: + obj: 要序列化的对象 + format: 序列化格式 + + Returns: + 序列化后的字节数据或None + """ + try: + if not self.enabled: + return None + + # 使用默认格式或指定格式 + if format is None: + format = self.serializer_config["default_format"] + + # 验证格式支持 + if format not in self.serializers: + print(f"✗ 不支持的序列化格式: {format}") + self.serializer_stats["serializer_errors"] += 1 + return None + + # 检查缓存 + cache_key = None + if self.serializer_config["enable_caching"]: + cache_key = self._generate_cache_key(obj, format) + if cache_key in self.serialization_cache: + self.serializer_stats["cache_hits"] += 1 + self.cache_timestamps[cache_key] = time.time() + + # 触发缓存命中回调 + self._trigger_serializer_callback("cache_hit", { + "cache_key": cache_key, + "format": format, + "timestamp": time.time() + }) + + return self.serialization_cache[cache_key] + else: + self.serializer_stats["cache_misses"] += 1 + + # 获取序列化器 + serializer = self.serializers[format]["serialize"] + + # 序列化对象 + serialized_data = serializer(obj) + if serialized_data is None: + return None + + # 更新统计 + self.serializer_stats["objects_serialized"] += 1 + self.serializer_stats["bytes_serialized"] += len(serialized_data) + self.serializer_state["serialized_objects"] += 1 + + # 缓存结果 + if (self.serializer_config["enable_caching"] and + cache_key and + len(self.serialization_cache) < self.serializer_config["cache_size"]): + self.serialization_cache[cache_key] = serialized_data + self.cache_timestamps[cache_key] = time.time() + self.serializer_state["cached_objects"] = len(self.serialization_cache) + + # 触发对象序列化回调 + self._trigger_serializer_callback("object_serialized", { + "format": format, + "data_size": len(serialized_data), + "timestamp": time.time() + }) + + return serialized_data + + except Exception as e: + print(f"✗ 对象序列化失败: {e}") + self.serializer_stats["serializer_errors"] += 1 + return None + + def deserialize(self, data: bytes, format: str = None) -> Optional[Any]: + """ + 反序列化对象 + + Args: + data: 要反序列化的字节数据 + format: 序列化格式 + + Returns: + 反序列化后的对象或None + """ + try: + if not self.enabled: + return None + + # 使用默认格式或指定格式 + if format is None: + format = self.serializer_config["default_format"] + + # 验证格式支持 + if format not in self.serializers: + print(f"✗ 不支持的反序列化格式: {format}") + self.serializer_stats["serializer_errors"] += 1 + return None + + # 获取反序列化器 + deserializer = self.serializers[format]["deserialize"] + + # 反序列化对象 + deserialized_obj = deserializer(data) + if deserialized_obj is None: + return None + + # 更新统计 + self.serializer_stats["objects_deserialized"] += 1 + self.serializer_stats["bytes_deserialized"] += len(data) + + # 触发对象反序列化回调 + self._trigger_serializer_callback("object_deserialized", { + "format": format, + "data_size": len(data), + "timestamp": time.time() + }) + + return deserialized_obj + + except Exception as e: + print(f"✗ 对象反序列化失败: {e}") + self.serializer_stats["serializer_errors"] += 1 + return None + + def _serialize_json(self, obj: Any) -> Optional[bytes]: + """ + JSON序列化 + + Args: + obj: 要序列化的对象 + + Returns: + 序列化后的字节数据或None + """ + try: + # 序列化为JSON字符串 + json_string = json.dumps(obj, ensure_ascii=False, default=self._json_default) + serialized_data = json_string.encode('utf-8') + + # 检查是否需要压缩 + if (self.serializer_config["enable_compression"] and + len(serialized_data) > self.serializer_config["compression_threshold"]): + compressed_data = zlib.compress(serialized_data) + if len(compressed_data) < len(serialized_data): + # 使用压缩版本 + serialized_data = compressed_data + self.serializer_state["compressed_objects"] += 1 + self.serializer_stats["compression_savings"] += len(serialized_data) - len(compressed_data) + + # 触发压缩应用回调 + self._trigger_serializer_callback("compression_applied", { + "original_size": len(json_string.encode('utf-8')), + "compressed_size": len(compressed_data), + "compression_ratio": len(compressed_data) / len(json_string.encode('utf-8')), + "timestamp": time.time() + }) + + return serialized_data + + except Exception as e: + print(f"✗ JSON序列化失败: {e}") + self.serializer_stats["serializer_errors"] += 1 + return None + + def _deserialize_json(self, data: bytes) -> Optional[Any]: + """ + JSON反序列化 + + Args: + data: 要反序列化的字节数据 + + Returns: + 反序列化后的对象或None + """ + try: + # 尝试解压缩 + try: + decompressed_data = zlib.decompress(data) + data = decompressed_data + except zlib.error: + # 不是压缩数据,使用原始数据 + pass + + # 解码为JSON + json_string = data.decode('utf-8') + deserialized_obj = json.loads(json_string) + + return deserialized_obj + + except Exception as e: + print(f"✗ JSON反序列化失败: {e}") + self.serializer_stats["serializer_errors"] += 1 + return None + + def _json_default(self, obj): + """ + JSON默认序列化方法 + + Args: + obj: 要序列化的对象 + + Returns: + 可序列化的对象 + """ + # 处理不能直接序列化的对象 + return str(obj) + + def _serialize_pickle(self, obj: Any) -> Optional[bytes]: + """ + Pickle序列化 + + Args: + obj: 要序列化的对象 + + Returns: + 序列化后的字节数据或None + """ + try: + # 序列化为pickle数据 + pickle_data = pickle.dumps(obj) + + # 检查是否需要压缩 + if (self.serializer_config["enable_compression"] and + len(pickle_data) > self.serializer_config["compression_threshold"]): + compressed_data = zlib.compress(pickle_data) + if len(compressed_data) < len(pickle_data): + # 使用压缩版本 + pickle_data = compressed_data + self.serializer_state["compressed_objects"] += 1 + self.serializer_stats["compression_savings"] += len(pickle_data) - len(compressed_data) + + # 触发压缩应用回调 + self._trigger_serializer_callback("compression_applied", { + "original_size": len(pickle.dumps(obj)), + "compressed_size": len(compressed_data), + "compression_ratio": len(compressed_data) / len(pickle.dumps(obj)), + "timestamp": time.time() + }) + + return pickle_data + + except Exception as e: + print(f"✗ Pickle序列化失败: {e}") + self.serializer_stats["serializer_errors"] += 1 + return None + + def _deserialize_pickle(self, data: bytes) -> Optional[Any]: + """ + Pickle反序列化 + + Args: + data: 要反序列化的字节数据 + + Returns: + 反序列化后的对象或None + """ + try: + # 尝试解压缩 + try: + decompressed_data = zlib.decompress(data) + data = decompressed_data + except zlib.error: + # 不是压缩数据,使用原始数据 + pass + + # 反序列化pickle数据 + deserialized_obj = pickle.loads(data) + + return deserialized_obj + + except Exception as e: + print(f"✗ Pickle反序列化失败: {e}") + self.serializer_stats["serializer_errors"] += 1 + return None + + def _serialize_binary(self, obj: Any) -> Optional[bytes]: + """ + 二进制序列化 + + Args: + obj: 要序列化的对象 + + Returns: + 序列化后的字节数据或None + """ + try: + # 简化实现:将对象转换为JSON再编码为二进制 + json_string = json.dumps(obj, ensure_ascii=False, default=self._json_default) + binary_data = json_string.encode('utf-8') + + # 添加简单的头部信息 + header = len(binary_data).to_bytes(4, byteorder='big') + serialized_data = header + binary_data + + # 检查是否需要压缩 + if (self.serializer_config["enable_compression"] and + len(serialized_data) > self.serializer_config["compression_threshold"]): + compressed_data = zlib.compress(serialized_data) + if len(compressed_data) < len(serialized_data): + # 使用压缩版本 + serialized_data = compressed_data + self.serializer_state["compressed_objects"] += 1 + self.serializer_stats["compression_savings"] += len(serialized_data) - len(compressed_data) + + # 触发压缩应用回调 + self._trigger_serializer_callback("compression_applied", { + "original_size": len(header + binary_data), + "compressed_size": len(compressed_data), + "compression_ratio": len(compressed_data) / len(header + binary_data), + "timestamp": time.time() + }) + + return serialized_data + + except Exception as e: + print(f"✗ 二进制序列化失败: {e}") + self.serializer_stats["serializer_errors"] += 1 + return None + + def _deserialize_binary(self, data: bytes) -> Optional[Any]: + """ + 二进制反序列化 + + Args: + data: 要反序列化的字节数据 + + Returns: + 反序列化后的对象或None + """ + try: + # 尝试解压缩 + try: + decompressed_data = zlib.decompress(data) + data = decompressed_data + except zlib.error: + # 不是压缩数据,使用原始数据 + pass + + # 解析头部信息 + if len(data) < 4: + raise ValueError("数据太短") + + message_length = int.from_bytes(data[:4], byteorder='big') + binary_data = data[4:4 + message_length] + + # 解码为JSON + json_string = binary_data.decode('utf-8') + deserialized_obj = json.loads(json_string) + + return deserialized_obj + + except Exception as e: + print(f"✗ 二进制反序列化失败: {e}") + self.serializer_stats["serializer_errors"] += 1 + return None + + def _generate_cache_key(self, obj: Any, format: str) -> str: + """ + 生成缓存键 + + Args: + obj: 对象 + format: 格式 + + Returns: + 缓存键 + """ + try: + # 简化实现:使用对象的字符串表示和格式生成键 + obj_str = str(obj) + cache_key = f"{format}:{hash(obj_str)}:{len(obj_str)}" + return cache_key + except Exception as e: + print(f"✗ 缓存键生成失败: {e}") + self.serializer_stats["serializer_errors"] += 1 + return f"error_key_{int(time.time() * 1000000)}" + + def add_serializer(self, format: str, serialize_func: callable, deserialize_func: callable) -> bool: + """ + 添加序列化器 + + Args: + format: 格式名称 + serialize_func: 序列化函数 + deserialize_func: 反序列化函数 + + Returns: + 是否添加成功 + """ + try: + self.serializers[format] = { + "serialize": serialize_func, + "deserialize": deserialize_func + } + + if format not in self.serializer_config["supported_formats"]: + self.serializer_config["supported_formats"].append(format) + + print(f"✓ 序列化器已添加: {format}") + return True + + except Exception as e: + print(f"✗ 序列化器添加失败: {e}") + self.serializer_stats["serializer_errors"] += 1 + return False + + def remove_serializer(self, format: str) -> bool: + """ + 移除序列化器 + + Args: + format: 格式名称 + + Returns: + 是否移除成功 + """ + try: + if format in self.serializers: + del self.serializers[format] + + if format in self.serializer_config["supported_formats"]: + self.serializer_config["supported_formats"].remove(format) + + print(f"✓ 序列化器已移除: {format}") + return True + else: + print(f"✗ 序列化器不存在: {format}") + return False + + except Exception as e: + print(f"✗ 序列化器移除失败: {e}") + self.serializer_stats["serializer_errors"] += 1 + return False + + def get_serializer_stats(self) -> Dict[str, Any]: + """ + 获取序列化统计信息 + + Returns: + 序列化统计字典 + """ + return { + "state": self.serializer_state.copy(), + "stats": self.serializer_stats.copy(), + "config": self.serializer_config.copy(), + "cache_size": len(self.serialization_cache) + } + + def reset_stats(self): + """重置序列化统计信息""" + try: + self.serializer_stats = { + "objects_serialized": 0, + "objects_deserialized": 0, + "bytes_serialized": 0, + "bytes_deserialized": 0, + "compression_savings": 0, + "cache_hits": 0, + "cache_misses": 0, + "serializer_errors": 0 + } + print("✓ 序列化统计信息已重置") + except Exception as e: + print(f"✗ 序列化统计信息重置失败: {e}") + + def set_serializer_config(self, config: Dict[str, Any]) -> bool: + """ + 设置序列化配置 + + Args: + config: 序列化配置字典 + + Returns: + 是否设置成功 + """ + try: + self.serializer_config.update(config) + print(f"✓ 序列化配置已更新: {self.serializer_config}") + return True + except Exception as e: + print(f"✗ 序列化配置设置失败: {e}") + return False + + def get_serializer_config(self) -> Dict[str, Any]: + """ + 获取序列化配置 + + Returns: + 序列化配置字典 + """ + return self.serializer_config.copy() + + def _trigger_serializer_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发序列化回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.serializer_callbacks: + for callback in self.serializer_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 序列化回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 序列化回调触发失败: {e}") + + def register_serializer_callback(self, callback_type: str, callback: callable): + """ + 注册序列化回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.serializer_callbacks: + self.serializer_callbacks[callback_type].append(callback) + print(f"✓ 序列化回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 序列化回调注册失败: {e}") + + def unregister_serializer_callback(self, callback_type: str, callback: callable): + """ + 注销序列化回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.serializer_callbacks: + if callback in self.serializer_callbacks[callback_type]: + self.serializer_callbacks[callback_type].remove(callback) + print(f"✓ 序列化回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 序列化回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/realtime_communication/utils/comm_utils.py b/plugins/user/realtime_communication/utils/comm_utils.py index 3fac73a3..7aea5948 100644 --- a/plugins/user/realtime_communication/utils/comm_utils.py +++ b/plugins/user/realtime_communication/utils/comm_utils.py @@ -1,248 +1,248 @@ -""" -通信工具类模块 -提供实用工具函数 -""" - -import time -import hashlib -import json -from typing import Dict, Any, List, Optional - -class CommUtils: - """ - 通信工具类 - 提供实用工具函数 - """ - - @staticmethod - def generate_unique_id(prefix: str = "") -> str: - """ - 生成唯一ID - - Args: - prefix: ID前缀 - - Returns: - 生成的唯一ID - """ - try: - timestamp = int(time.time() * 1000000) # 微秒时间戳 - unique_id = f"{prefix}{timestamp}" if prefix else str(timestamp) - return unique_id - except Exception as e: - print(f"✗ 唯一ID生成失败: {e}") - return f"error_id_{int(time.time() * 1000000)}" - - @staticmethod - def calculate_checksum(data: bytes) -> str: - """ - 计算数据校验和 - - Args: - data: 要计算校验和的数据 - - Returns: - 校验和字符串 - """ - try: - checksum = hashlib.md5(data).hexdigest() - return checksum - except Exception as e: - print(f"✗ 校验和计算失败: {e}") - return "" - - @staticmethod - def format_bytes(bytes_count: int) -> str: - """ - 格式化字节数 - - Args: - bytes_count: 字节数 - - Returns: - 格式化后的字符串 - """ - try: - if bytes_count < 1024: - return f"{bytes_count} B" - elif bytes_count < 1024 * 1024: - return f"{bytes_count / 1024:.2f} KB" - elif bytes_count < 1024 * 1024 * 1024: - return f"{bytes_count / (1024 * 1024):.2f} MB" - else: - return f"{bytes_count / (1024 * 1024 * 1024):.2f} GB" - except Exception as e: - print(f"✗ 字节格式化失败: {e}") - return f"{bytes_count} B" - - @staticmethod - def validate_json(data: str) -> bool: - """ - 验证JSON数据 - - Args: - data: JSON字符串 - - Returns: - 是否为有效JSON - """ - try: - json.loads(data) - return True - except Exception: - return False - - @staticmethod - def sanitize_input(input_str: str) -> str: - """ - 清理输入字符串 - - Args: - input_str: 输入字符串 - - Returns: - 清理后的字符串 - """ - try: - # 移除潜在的危险字符 - sanitized = input_str.replace("<", "<").replace(">", ">") - sanitized = sanitized.replace("\"", """).replace("'", "'") - return sanitized - except Exception as e: - print(f"✗ 输入清理失败: {e}") - return input_str - - @staticmethod - def create_response(success: bool, message: str = "", data: Any = None) -> Dict[str, Any]: - """ - 创建响应数据 - - Args: - success: 是否成功 - message: 消息 - data: 数据 - - Returns: - 响应数据字典 - """ - try: - response = { - "success": success, - "message": message, - "timestamp": time.time() - } - - if data is not None: - response["data"] = data - - return response - except Exception as e: - print(f"✗ 响应创建失败: {e}") - return { - "success": False, - "message": "响应创建失败", - "timestamp": time.time() - } - - @staticmethod - def merge_dicts(dict1: Dict[str, Any], dict2: Dict[str, Any]) -> Dict[str, Any]: - """ - 合并字典(递归) - - Args: - dict1: 第一个字典 - dict2: 第二个字典 - - Returns: - 合并后的字典 - """ - try: - result = dict1.copy() - - for key, value in dict2.items(): - if key in result and isinstance(result[key], dict) and isinstance(value, dict): - # 递归合并嵌套字典 - result[key] = CommUtils.merge_dicts(result[key], value) - else: - # 直接覆盖 - result[key] = value - - return result - except Exception as e: - print(f"✗ 字典合并失败: {e}") - return dict1 - - @staticmethod - def get_system_info() -> Dict[str, Any]: - """ - 获取系统信息 - - Returns: - 系统信息字典 - """ - try: - import platform - import psutil - - # 系统信息 - system_info = { - "platform": platform.system(), - "platform_version": platform.version(), - "architecture": platform.architecture()[0], - "processor": platform.processor(), - "python_version": platform.python_version() - } - - # CPU信息 - cpu_info = { - "physical_cores": psutil.cpu_count(logical=False), - "total_cores": psutil.cpu_count(logical=True), - "max_frequency": psutil.cpu_freq().max if psutil.cpu_freq() else 0, - "current_frequency": psutil.cpu_freq().current if psutil.cpu_freq() else 0 - } - - # 内存信息 - memory_info = psutil.virtual_memory() - memory_stats = { - "total": memory_info.total, - "available": memory_info.available, - "used": memory_info.used, - "percentage": memory_info.percent - } - - # 网络信息 - net_info = psutil.net_io_counters() - network_stats = { - "bytes_sent": net_info.bytes_sent, - "bytes_recv": net_info.bytes_recv, - "packets_sent": net_info.packets_sent, - "packets_recv": net_info.packets_recv - } - - return { - "system": system_info, - "cpu": cpu_info, - "memory": memory_stats, - "network": network_stats, - "timestamp": time.time() - } - - except Exception as e: - print(f"✗ 系统信息获取失败: {e}") - return { - "system": {}, - "cpu": {}, - "memory": {}, - "network": {}, - "timestamp": time.time() - } - -# 工具函数别名 -generate_id = CommUtils.generate_unique_id -calculate_checksum = CommUtils.calculate_checksum -format_bytes = CommUtils.format_bytes -validate_json = CommUtils.validate_json -sanitize_input = CommUtils.sanitize_input -create_response = CommUtils.create_response -merge_dicts = CommUtils.merge_dicts +""" +通信工具类模块 +提供实用工具函数 +""" + +import time +import hashlib +import json +from typing import Dict, Any, List, Optional + +class CommUtils: + """ + 通信工具类 + 提供实用工具函数 + """ + + @staticmethod + def generate_unique_id(prefix: str = "") -> str: + """ + 生成唯一ID + + Args: + prefix: ID前缀 + + Returns: + 生成的唯一ID + """ + try: + timestamp = int(time.time() * 1000000) # 微秒时间戳 + unique_id = f"{prefix}{timestamp}" if prefix else str(timestamp) + return unique_id + except Exception as e: + print(f"✗ 唯一ID生成失败: {e}") + return f"error_id_{int(time.time() * 1000000)}" + + @staticmethod + def calculate_checksum(data: bytes) -> str: + """ + 计算数据校验和 + + Args: + data: 要计算校验和的数据 + + Returns: + 校验和字符串 + """ + try: + checksum = hashlib.md5(data).hexdigest() + return checksum + except Exception as e: + print(f"✗ 校验和计算失败: {e}") + return "" + + @staticmethod + def format_bytes(bytes_count: int) -> str: + """ + 格式化字节数 + + Args: + bytes_count: 字节数 + + Returns: + 格式化后的字符串 + """ + try: + if bytes_count < 1024: + return f"{bytes_count} B" + elif bytes_count < 1024 * 1024: + return f"{bytes_count / 1024:.2f} KB" + elif bytes_count < 1024 * 1024 * 1024: + return f"{bytes_count / (1024 * 1024):.2f} MB" + else: + return f"{bytes_count / (1024 * 1024 * 1024):.2f} GB" + except Exception as e: + print(f"✗ 字节格式化失败: {e}") + return f"{bytes_count} B" + + @staticmethod + def validate_json(data: str) -> bool: + """ + 验证JSON数据 + + Args: + data: JSON字符串 + + Returns: + 是否为有效JSON + """ + try: + json.loads(data) + return True + except Exception: + return False + + @staticmethod + def sanitize_input(input_str: str) -> str: + """ + 清理输入字符串 + + Args: + input_str: 输入字符串 + + Returns: + 清理后的字符串 + """ + try: + # 移除潜在的危险字符 + sanitized = input_str.replace("<", "<").replace(">", ">") + sanitized = sanitized.replace("\"", """).replace("'", "'") + return sanitized + except Exception as e: + print(f"✗ 输入清理失败: {e}") + return input_str + + @staticmethod + def create_response(success: bool, message: str = "", data: Any = None) -> Dict[str, Any]: + """ + 创建响应数据 + + Args: + success: 是否成功 + message: 消息 + data: 数据 + + Returns: + 响应数据字典 + """ + try: + response = { + "success": success, + "message": message, + "timestamp": time.time() + } + + if data is not None: + response["data"] = data + + return response + except Exception as e: + print(f"✗ 响应创建失败: {e}") + return { + "success": False, + "message": "响应创建失败", + "timestamp": time.time() + } + + @staticmethod + def merge_dicts(dict1: Dict[str, Any], dict2: Dict[str, Any]) -> Dict[str, Any]: + """ + 合并字典(递归) + + Args: + dict1: 第一个字典 + dict2: 第二个字典 + + Returns: + 合并后的字典 + """ + try: + result = dict1.copy() + + for key, value in dict2.items(): + if key in result and isinstance(result[key], dict) and isinstance(value, dict): + # 递归合并嵌套字典 + result[key] = CommUtils.merge_dicts(result[key], value) + else: + # 直接覆盖 + result[key] = value + + return result + except Exception as e: + print(f"✗ 字典合并失败: {e}") + return dict1 + + @staticmethod + def get_system_info() -> Dict[str, Any]: + """ + 获取系统信息 + + Returns: + 系统信息字典 + """ + try: + import platform + import psutil + + # 系统信息 + system_info = { + "platform": platform.system(), + "platform_version": platform.version(), + "architecture": platform.architecture()[0], + "processor": platform.processor(), + "python_version": platform.python_version() + } + + # CPU信息 + cpu_info = { + "physical_cores": psutil.cpu_count(logical=False), + "total_cores": psutil.cpu_count(logical=True), + "max_frequency": psutil.cpu_freq().max if psutil.cpu_freq() else 0, + "current_frequency": psutil.cpu_freq().current if psutil.cpu_freq() else 0 + } + + # 内存信息 + memory_info = psutil.virtual_memory() + memory_stats = { + "total": memory_info.total, + "available": memory_info.available, + "used": memory_info.used, + "percentage": memory_info.percent + } + + # 网络信息 + net_info = psutil.net_io_counters() + network_stats = { + "bytes_sent": net_info.bytes_sent, + "bytes_recv": net_info.bytes_recv, + "packets_sent": net_info.packets_sent, + "packets_recv": net_info.packets_recv + } + + return { + "system": system_info, + "cpu": cpu_info, + "memory": memory_stats, + "network": network_stats, + "timestamp": time.time() + } + + except Exception as e: + print(f"✗ 系统信息获取失败: {e}") + return { + "system": {}, + "cpu": {}, + "memory": {}, + "network": {}, + "timestamp": time.time() + } + +# 工具函数别名 +generate_id = CommUtils.generate_unique_id +calculate_checksum = CommUtils.calculate_checksum +format_bytes = CommUtils.format_bytes +validate_json = CommUtils.validate_json +sanitize_input = CommUtils.sanitize_input +create_response = CommUtils.create_response +merge_dicts = CommUtils.merge_dicts get_system_info = CommUtils.get_system_info \ No newline at end of file diff --git a/plugins/user/rigid_body_physics/README.md b/plugins/user/rigid_body_physics/README.md index d6e88a94..18633a21 100644 --- a/plugins/user/rigid_body_physics/README.md +++ b/plugins/user/rigid_body_physics/README.md @@ -1,613 +1,613 @@ -# 刚体动力学插件使用文档 - -## 简介 - -刚体动力学插件是一个完整的物理模拟系统,基于Bullet物理引擎,提供了丰富的物理功能,包括刚体动力学、约束系统、车辆模拟、粒子系统等。 - -## 功能特性 - -### 1. 物理世界管理 -- 重力设置和控制 -- 时间步长和子步长模拟 -- 性能监控和优化 -- 多线程支持 -- 空间分区优化 - -### 2. 刚体系统 -- 静态、动态、运动学刚体 -- 多种碰撞形状(盒体、球体、胶囊体、圆锥体、圆柱体、凸包、网格、复合形状) -- 质量、摩擦、弹性等物理属性 -- 连续碰撞检测(CCD) -- 力和扭矩应用 -- 速度和加速度控制 -- 能量计算(动能、势能) -- 碰撞历史记录 - -### 3. 材质系统 -- 预定义常用材质(混凝土、木材、金属、橡胶、冰、沙、水、玻璃、肉体等) -- 自定义材质属性 - - 基础物理属性(摩擦、弹性、密度等) - - 热力学属性(热导率、比热容等) - - 电学属性(电导率、介电常数等) - - 流体属性(粘度、表面张力等) - - 视觉属性(颜色、粗糙度等) - - 环境属性(耐候性、腐蚀率等) - - 特殊属性(易燃性、毒性、放射性) -- 高级材质功能 - - 热膨胀和相变模拟 - - 环境老化效应(雨水、紫外线、风力等) - - 疲劳累积和损伤分析 - - 化学反应和腐蚀模拟 - - 流体动力学效应(阻力、升力、浮力) - - 能量吸收和耗散 - - 失效准则和结构完整性评估 - - 动态摩擦和恢复系数计算 -- 材质交互矩阵 -- 材质状态管理(温度、湿度、磨损、疲劳、污染等) -- 材质历史记录和统计分析 -- 材质保存和加载 - -### 4. 约束和关节 -- 点对点约束 -- 铰链约束 -- 滑动约束 -- 锥形扭转约束 -- 6自由度约束 -- 6自由度弹簧约束 -- 齿轮约束 -- 固定约束 -- 约束组和标签管理 -- 约束马达和弹簧控制 -- 约束限制设置 -- 约束状态监控 - -### 5. 车辆系统 -- 底盘和车轮物理 -- 悬挂系统 -- 引擎和刹车控制 -- 转向系统 -- 差速器类型支持 -- 齿轮系统 -- 发动机扭矩曲线 -- 防抱死刹车(ABS) -- 牵引力控制(TC) -- 车身稳定控制(ESP) -- 空气动力学效应 -- 防倾杆系统 -- 车辆损伤系统 -- 燃料系统 -- 性能统计 - -### 6. 粒子系统 -- 大量粒子模拟 -- 多种粒子类型(烟雾、火焰、水、碎片、爆炸、火花、气泡、叶子、雪、雨等) -- 发射器控制 -- 粒子生命周期管理 -- 环境影响(风力、温度等) -- 特殊效果(涡流等) -- 粒子子发射器 -- 粒子轨迹和淡出效果 -- 粒子碰撞和死亡回调 -- 爆炸和火焰特效 - -### 7. 调试和可视化 -- 碰撞体可视化 -- 约束可视化 -- 接触点可视化 -- 法线可视化 -- 速度和力可视化 -- 坐标轴可视化 -- 质心可视化 -- 休眠状态可视化 -- 粒子轨迹可视化 -- 性能监控面板 -- 调试文本显示 -- 可视化选项控制 - -### 8. 实用工具 -- 物理计算工具(质量计算、冲量应用、扭矩计算等) -- 配置管理器 -- 碰撞组定义 -- 物理常量库 - -## 安装和配置 - -### 目录结构 -``` -rigid_body_physics/ -├── __init__.py -├── plugin_manager.py -├── README.md -├── core/ -│ ├── physics_world.py -│ ├── rigid_body.py -│ └── material.py -├── constraints/ -│ └── constraint_manager.py -├── vehicles/ -│ └── vehicle_system.py -├── particles/ -│ └── particle_system.py -├── debug/ -│ └── debug_tools.py -├── utils/ -│ └── physics_utils.py -└── config/ - └── physics_config.json -``` - -## 快速开始 - -### 1. 初始化插件 -```python -from plugins.user.rigid_body_physics.plugin_manager import RigidBodyPhysicsPlugin - -# 创建物理插件实例 -physics_plugin = RigidBodyPhysicsPlugin(world_node=render) -``` - -### 2. 创建刚体 -```python -# 创建一个盒子形状的动态刚体 -box_body = physics_plugin.create_rigid_body( - node_path=box_node, - shape_type='box', - mass=1.0, - body_type=RigidBody.DYNAMIC -) - -# 设置物理属性 -box_body.set_friction(0.5) -box_body.set_restitution(0.3) - -# 应用力和扭矩 -box_body.apply_force(Vec3(0, 0, 100)) # 向上施加力 -box_body.apply_torque(Vec3(0, 10, 0)) # 绕Y轴施加扭矩 -``` - -### 3. 创建约束 -```python -# 创建铰链约束 -hinge_constraint = physics_plugin.create_constraint( - constraint_type='hinge', - body_a=body_a, - body_b=body_b, - pivot_a=Point3(0, 0, 0), - pivot_b=Point3(0, 0, 0), - axis_a=Vec3(0, 0, 1), - axis_b=Vec3(0, 0, 1) -) -``` - -### 4. 创建车辆 -```python -# 定义车轮配置 -wheel_configs = [ - { - 'connection_point': Point3(-1, 1, 0), - 'wheel_direction': Vec3(0, 0, -1), - 'wheel_axle': Vec3(1, 0, 0), - 'wheel_radius': 0.3 - }, - # ... 其他车轮配置 -] - -# 创建车辆 -vehicle = physics_plugin.create_vehicle(chassis_body, wheel_configs) - -# 控制车辆 -vehicle.set_steering(0.5) # 转向 -vehicle.set_engine_force(1.0) # 加油 -vehicle.set_brake_force(0.3) # 刹车 -``` - -### 5. 创建粒子系统 -```python -# 创建粒子系统 -particle_system = physics_plugin.create_particle_system(max_particles=500) - -# 设置发射器属性 -particle_system.set_emitter_properties( - position=Point3(0, 0, 5), - velocity=Vec3(0, 0, 10), - spread=0.5 -) - -# 创建爆炸效果 -particle_system.create_explosion( - position=Point3(0, 0, 0), - particle_count=100, - explosion_force=50.0 -) -``` - -### 6. 主循环更新 -```python -def update(task): - dt = globalClock.getDt() - physics_plugin.update(dt) - - # 更新车辆 - for vehicle in physics_plugin.vehicles: - vehicle.update(dt) - - return task.cont - -taskMgr.add(update, "physics_update") -``` - -## 高级功能 - -### 材质系统 -```python -# 使用预定义材质 -concrete_material = physics_plugin.get_predefined_material('concrete') - -# 创建自定义材质 -custom_material = physics_plugin.create_custom_material( - 'my_material', - { - 'friction': 0.8, - 'restitution': 0.2, - 'density': 2000.0, - 'thermal_conductivity': 2.0, - 'color': Vec4(0.8, 0.6, 0.4, 1.0) - } -) - -# 设置材质交互 -custom_material.set_interaction_coefficient('metal', 1.2) - -# 更新材质状态 -custom_material.update_state( - temperature=50.0, - wear=0.1, - stress=100.0 -) -``` - -### 调试可视化 -```python -# 启用调试可视化 -physics_plugin.enable_debug_visualization(True) - -# 设置调试选项 -physics_plugin.set_debug_options( - show_constraints=True, - show_aabbs=True, - show_velocities=True, - show_forces=True -) - -# 性能分析 -profiler = physics_plugin.profiler -performance_report = profiler.get_performance_report() -print(f"平均FPS: {performance_report['average_fps']}") -``` - -### 性能监控 -```python -# 获取性能统计 -stats = physics_plugin.get_performance_stats() -print(f"平均更新时间: {stats['avg_update_time']}") -print(f"刚体数量: {stats['objects']['rigid_bodies']}") - -# 获取详细性能分析 -analysis = physics_plugin.profiler.get_performance_analysis() -print(f"性能等级: {analysis['performance_grade']}") -print(f"瓶颈: {analysis['bottlenecks']}") -print(f"建议: {analysis['recommendations']}") -``` - -## 配置文件 - -插件支持通过JSON配置文件进行配置: - -```json -{ - "physics": { - "gravity": [0, 0, -9.81], - "time_step": 0.016667, - "solver_iterations": 10, - "solver_tolerance": 0.001 - }, - "materials": { - "concrete": { - "friction": 0.7, - "restitution": 0.1, - "density": 2400.0 - } - }, - "vehicle": { - "max_engine_force": 3000.0, - "max_brake_force": 100.0, - "max_steering": 0.7854 - } -} -``` - -## API参考 - -### RigidBodyPhysicsPlugin类 - -#### 构造函数 -```python -RigidBodyPhysicsPlugin(world_node=None, config_file=None) -``` - -#### 主要方法 -- `create_rigid_body(node_path, shape_type, mass, body_type, material)`: 创建刚体 -- `create_constraint(constraint_type, body_a, body_b, **kwargs)`: 创建约束 -- `create_vehicle(chassis_body, wheel_configurations)`: 创建车辆 -- `create_particle_system(max_particles)`: 创建粒子系统 -- `update(dt)`: 更新物理系统 -- `ray_test(from_pos, to_pos)`: 射线检测 -- `get_performance_stats()`: 获取性能统计 -- `enable_debug_visualization(enabled)`: 启用调试可视化 -- `set_debug_options(**kwargs)`: 设置调试选项 -- `get_predefined_material(material_name)`: 获取预定义材质 -- `create_custom_material(name, properties)`: 创建自定义材质 -- `set_gravity(gravity)`: 设置重力 -- `set_time_step(time_step, max_substeps)`: 设置时间步长 -- `get_collision_groups()`: 获取碰撞组工具 -- `save_config(config_file)`: 保存配置 -- `load_config(config_file)`: 加载配置 -- `clear_all()`: 清除所有物理对象 -- `destroy()`: 销毁物理插件 - -### RigidBody类 - -#### 主要方法 -- `set_friction(friction, static_friction, dynamic_friction)`: 设置摩擦系数 -- `set_restitution(restitution)`: 设置恢复系数 -- `set_damping(linear_damping, angular_damping)`: 设置阻尼 -- `set_factors(linear_factor, angular_factor)`: 设置线性和角因子 -- `set_activation_state(active, always_active)`: 设置激活状态 -- `set_sleeping_thresholds(linear_threshold, angular_threshold)`: 设置休眠阈值 -- `set_ccd_parameters(motion_threshold, swept_sphere_radius)`: 设置CCD参数 -- `apply_force(force, position, duration)`: 应用力 -- `apply_torque(torque, duration)`: 应用扭矩 -- `apply_impulse(impulse, position, duration)`: 应用冲量 -- `apply_torque_impulse(torque_impulse)`: 应用扭矩冲量 -- `get_linear_velocity()`: 获取线性速度 -- `set_linear_velocity(velocity)`: 设置线性速度 -- `get_angular_velocity()`: 获取角速度 -- `set_angular_velocity(velocity)`: 设置角速度 -- `get_position()`: 获取位置 -- `set_position(position)`: 设置位置 -- `get_rotation()`: 获取旋转 -- `set_rotation(rotation)`: 设置旋转 -- `sync_visual_node()`: 同步物理节点和可视化节点 -- `get_mass()`: 获取质量 -- `set_mass(mass)`: 设置质量 -- `get_kinetic_energy()`: 计算动能 -- `get_potential_energy(gravity)`: 计算势能 -- `get_total_energy(gravity)`: 计算总能量 -- `set_custom_data(key, value)`: 设置自定义数据 -- `get_custom_data(key, default)`: 获取自定义数据 -- `destroy()`: 销毁刚体 - -### PhysicsMaterial类 - -#### 主要方法 -- `set_property(property_name, value)`: 设置材质属性 -- `get_property(property_name)`: 获取材质属性 -- `set_friction(friction, static_friction, dynamic_friction)`: 设置摩擦系数 -- `set_restitution(restitution)`: 设置恢复系数 -- `set_density(density)`: 设置密度 -- `set_color(color)`: 设置颜色 -- `set_thermal_properties(conductivity, specific_heat, expansion)`: 设置热力学属性 -- `set_electrical_properties(conductivity, dielectric, permeability)`: 设置电学属性 -- `set_fluid_properties(viscosity, surface_tension, compressibility)`: 设置流体属性 -- `set_visual_properties(roughness, metallic, specular, emissive)`: 设置视觉属性 -- `set_environmental_properties(weather_resistance, corrosion_rate, aging_rate)`: 设置环境属性 -- `set_special_properties(flammability, toxicity, radioactivity)`: 设置特殊属性 -- `update_state(temperature, humidity, wear, contamination, stress, fatigue)`: 更新材质状态 -- `get_effective_friction(other_material, velocity)`: 获取有效摩擦系数 -- `get_effective_restitution(other_material, impact_velocity)`: 获取有效恢复系数 -- `set_interaction_coefficient(other_material_name, coefficient)`: 设置交互系数 -- `apply_to_rigid_body(rigid_body)`: 应用到刚体 -- `to_dict()`: 转换为字典 -- `from_dict(data)`: 从字典创建 -- `save_to_file(filepath)`: 保存到文件 -- `load_from_file(filepath)`: 从文件加载 - -### ConstraintManager类 - -#### 支持的约束类型 -- `CONSTRAINT_POINT2POINT`: 点对点约束 -- `CONSTRAINT_HINGE`: 铰链约束 -- `CONSTRAINT_SLIDER`: 滑动约束 -- `CONSTRAINT_CONETWIST`: 锥形扭转约束 -- `CONSTRAINT_6DOF`: 6自由度约束 -- `CONSTRAINT_6DOF_SPRING`: 6自由度弹簧约束 -- `CONSTRAINT_GEAR`: 齿轮约束 -- `CONSTRAINT_FIXED`: 固定约束 - -#### 主要方法 -- `create_point2point_constraint(body_a, body_b, pivot_a, pivot_b, ...)`: 创建点对点约束 -- `create_hinge_constraint(body_a, body_b, pivot_a, pivot_b, axis_a, axis_b, ...)`: 创建铰链约束 -- `create_slider_constraint(body_a, body_b, frame_a, frame_b, ...)`: 创建滑动约束 -- `create_cone_twist_constraint(body_a, body_b, frame_a, frame_b, ...)`: 创建锥形扭转约束 -- `create_6dof_constraint(body_a, body_b, frame_a, frame_b, ...)`: 创建6自由度约束 -- `create_6dof_spring_constraint(body_a, body_b, frame_a, frame_b, ...)`: 创建6自由度弹簧约束 -- `create_gear_constraint(body_a, body_b, axis_a, axis_b, ratio)`: 创建齿轮约束 -- `create_fixed_constraint(body_a, body_b, frame_a, frame_b, ...)`: 创建固定约束 -- `set_motor(constraint, axis, target_velocity, max_impulse)`: 设置马达 -- `disable_motor(constraint)`: 禁用马达 -- `set_spring(constraint, axis, stiffness, damping, equilibrium)`: 设置弹簧 -- `disable_spring(constraint, axis)`: 禁用弹簧 -- `set_limit(constraint, axis, lower_limit, upper_limit)`: 设置限制 -- `add_constraint_to_group(constraint, group_name)`: 添加到组 -- `remove_constraint_from_group(constraint, group_name)`: 从组移除 -- `add_constraint_tag(constraint, tag)`: 添加标签 -- `remove_constraint_tag(constraint, tag)`: 移除标签 -- `get_constraints_by_tag(tag)`: 根据标签获取约束 -- `get_constraints_by_group(group_name)`: 根据组获取约束 -- `get_constraint_state(constraint)`: 获取约束状态 -- `get_constraint_statistics()`: 获取约束统计 - -### VehicleSystem类 - -#### 主要方法 -- `add_wheel(...)`: 添加车轮 -- `set_steering(steering)`: 设置转向 -- `set_engine_force(force)`: 设置引擎力 -- `set_brake_force(force)`: 设置刹车力 -- `set_handbrake_force(force)`: 设置手刹力 -- `shift_up()`: 升档 -- `shift_down()`: 降档 -- `set_gear(gear)`: 设置档位 -- `update(dt)`: 更新车辆 -- `get_current_speed_km_hour()`: 获取当前速度(km/h) -- `get_wheel_info(wheel_index)`: 获取车轮信息 -- `get_vehicle_state()`: 获取车辆状态 -- `get_engine_torque()`: 获取发动机扭矩 -- `get_engine_power()`: 获取发动机功率 -- `apply_damage(component, amount)`: 应用损伤 -- `repair_component(component, amount)`: 修复组件 -- `refuel(amount)`: 加油 -- `get_performance_stats()`: 获取性能统计 -- `enable_anti_lock_brake(enabled)`: 启用ABS -- `enable_traction_control(enabled)`: 启用牵引力控制 -- `enable_stability_control(enabled)`: 启用车身稳定控制 -- `destroy()`: 销毁车辆 - -### ParticleSystem类 - -#### 主要方法 -- `set_emitter_properties(...)`: 设置发射器属性 -- `set_particle_properties(...)`: 设置粒子属性 -- `set_physics_properties(...)`: 设置物理属性 -- `set_collision_properties(...)`: 设置碰撞属性 -- `set_environmental_effects(...)`: 设置环境影响 -- `set_special_effects(...)`: 设置特殊效果 -- `set_lifetime_behaviors(...)`: 设置生命周期行为 -- `set_sub_emitter(...)`: 设置子发射器 -- `set_performance_options(...)`: 设置性能选项 -- `set_emission_rate(rate)`: 设置发射速率 -- `set_emission_burst(count, interval)`: 设置爆发发射 -- `emit_particle(position, velocity, lifetime, color)`: 发射粒子 -- `update(dt)`: 更新粒子系统 -- `get_particle_count()`: 获取粒子数量 -- `get_statistics()`: 获取统计信息 -- `clear_particles()`: 清除粒子 -- `pause(paused)`: 暂停/恢复 -- `set_time_scale(scale)`: 设置时间缩放 -- `create_explosion(position, particle_count, explosion_force, ...)`: 创建爆炸 -- `create_fire_effect(position, particle_count, rise_speed, ...)`: 创建火焰 -- `destroy()`: 销毁粒子系统 - -### PhysicsWorld类 - -#### 主要方法 -- `set_gravity(gravity)`: 设置重力 -- `set_time_step(time_step, max_substeps)`: 设置时间步长 -- `set_solver_parameters(iterations, tolerance)`: 设置求解器参数 -- `set_split_impulse(enable, penetration_threshold)`: 设置分离冲量 -- `set_ccd_parameters(threshold, radius)`: 设置CCD参数 -- `enable_multithreading(enable, thread_count)`: 启用多线程 -- `setup_spatial_partitioning(partition_size)`: 设置空间分区 -- `update(dt)`: 更新物理世界 -- `add_rigid_body(rigid_body)`: 添加刚体 -- `remove_rigid_body(rigid_body)`: 移除刚体 -- `add_constraint(constraint)`: 添加约束 -- `remove_constraint(constraint)`: 移除约束 -- `add_vehicle(vehicle)`: 添加车辆 -- `remove_vehicle(vehicle)`: 移除车辆 -- `add_particle_system(particle_system)`: 添加粒子系统 -- `remove_particle_system(particle_system)`: 移除粒子系统 -- `ray_test(from_pos, to_pos)`: 射线检测 -- `ray_test_all(from_pos, to_pos)`: 射线检测(所有结果) -- `get_performance_stats()`: 获取性能统计 -- `pause(paused)`: 暂停/恢复 -- `set_time_scale(scale)`: 设置时间缩放 -- `clear()`: 清除所有对象 -- `reset()`: 重置物理世界 - -### PhysicsDebug类 - -#### 主要方法 -- `enable_debug(enabled)`: 启用/禁用调试 -- `set_debug_options(...)`: 设置调试选项 -- `update_debug_visualization()`: 更新调试可视化 -- `add_contact_point(position, normal, impulse)`: 添加接触点 -- `add_collision_event(body_a, body_b, contact_points)`: 添加碰撞事件 -- `set_line_thickness(thickness)`: 设置线条粗细 -- `set_axis_length(length)`: 设置坐标轴长度 -- `set_velocity_scale(scale)`: 设置速度缩放 -- `set_force_scale(scale)`: 设置力缩放 -- `set_contact_point_size(size)`: 设置接触点大小 -- `set_normal_scale(scale)`: 设置法线缩放 -- `enable_debug_text(enabled)`: 启用/禁用调试文本 -- `enable_stats_display(enabled)`: 启用/禁用统计显示 -- `set_debug_update_frequency(frequency)`: 设置调试更新频率 -- `hide_debug_visualization()`: 隐藏调试可视化 -- `show_debug_visualization()`: 显示调试可视化 -- `destroy()`: 销毁调试工具 - -### PhysicsProfiler类 - -#### 主要方法 -- `record_frame()`: 记录帧数据 -- `get_average_update_time()`: 获取平均更新时间 -- `get_max_update_time()`: 获取最大更新时间 -- `get_min_update_time()`: 获取最小更新时间 -- `get_average_fps()`: 获取平均FPS -- `get_object_counts()`: 获取对象数量 -- `get_average_object_counts()`: 获取平均对象数量 -- `get_performance_report()`: 获取性能报告 -- `get_performance_analysis()`: 获取性能分析 -- `reset_statistics()`: 重置统计 -- `export_statistics(filepath)`: 导出统计 -- `destroy()`: 销毁分析器 - -## 性能优化建议 - -1. **合理设置时间步长**: 过小的时间步长会影响性能,建议在1/60秒左右 -2. **控制刚体数量**: 过多的刚体会影响性能,考虑使用休眠机制和剔除 -3. **使用适当的碰撞形状**: 复杂形状比简单形状消耗更多资源,优先使用基本形状 -4. **启用休眠机制**: 让静止的物体进入休眠状态以节省计算资源 -5. **使用连续碰撞检测**: 对于高速运动的物体启用CCD以避免穿透 -6. **合理设置求解器参数**: 迭代次数和容差影响精度和性能的平衡 -7. **启用调试可视化仅在开发时**: 调试可视化会消耗额外资源 -8. **及时销毁不需要的对象**: 避免内存泄漏 -9. **使用对象池**: 对于频繁创建销毁的对象使用对象池 -10. **空间分区**: 对于大型场景使用空间分区优化碰撞检测 - -## 故障排除 - -### 常见问题 - -1. **刚体穿透**: - - 检查时间步长是否过小,增加求解器迭代次数 - - 对高速物体启用连续碰撞检测(CCD) - - 调整碰撞形状边界 - -2. **性能问题**: - - 减少刚体和约束数量 - - 优化碰撞形状复杂度 - - 启用休眠机制 - - 使用空间分区 - - 调整求解器参数 - -3. **约束不稳定**: - - 检查约束参数,适当调整限制范围 - - 增加求解器迭代次数 - - 调整约束的破裂阈值 - -4. **车辆控制不自然**: - - 调整悬挂参数 - - 调整轮胎摩擦系数 - - 调整发动机扭矩曲线 - - 启用或调整ABS、TC等辅助系统 - -5. **粒子效果不理想**: - - 调整发射器参数 - - 调整粒子生命周期参数 - - 调整物理属性(重力、阻力等) - - 调整环境影响参数 - -### 联系支持 - +# 刚体动力学插件使用文档 + +## 简介 + +刚体动力学插件是一个完整的物理模拟系统,基于Bullet物理引擎,提供了丰富的物理功能,包括刚体动力学、约束系统、车辆模拟、粒子系统等。 + +## 功能特性 + +### 1. 物理世界管理 +- 重力设置和控制 +- 时间步长和子步长模拟 +- 性能监控和优化 +- 多线程支持 +- 空间分区优化 + +### 2. 刚体系统 +- 静态、动态、运动学刚体 +- 多种碰撞形状(盒体、球体、胶囊体、圆锥体、圆柱体、凸包、网格、复合形状) +- 质量、摩擦、弹性等物理属性 +- 连续碰撞检测(CCD) +- 力和扭矩应用 +- 速度和加速度控制 +- 能量计算(动能、势能) +- 碰撞历史记录 + +### 3. 材质系统 +- 预定义常用材质(混凝土、木材、金属、橡胶、冰、沙、水、玻璃、肉体等) +- 自定义材质属性 + - 基础物理属性(摩擦、弹性、密度等) + - 热力学属性(热导率、比热容等) + - 电学属性(电导率、介电常数等) + - 流体属性(粘度、表面张力等) + - 视觉属性(颜色、粗糙度等) + - 环境属性(耐候性、腐蚀率等) + - 特殊属性(易燃性、毒性、放射性) +- 高级材质功能 + - 热膨胀和相变模拟 + - 环境老化效应(雨水、紫外线、风力等) + - 疲劳累积和损伤分析 + - 化学反应和腐蚀模拟 + - 流体动力学效应(阻力、升力、浮力) + - 能量吸收和耗散 + - 失效准则和结构完整性评估 + - 动态摩擦和恢复系数计算 +- 材质交互矩阵 +- 材质状态管理(温度、湿度、磨损、疲劳、污染等) +- 材质历史记录和统计分析 +- 材质保存和加载 + +### 4. 约束和关节 +- 点对点约束 +- 铰链约束 +- 滑动约束 +- 锥形扭转约束 +- 6自由度约束 +- 6自由度弹簧约束 +- 齿轮约束 +- 固定约束 +- 约束组和标签管理 +- 约束马达和弹簧控制 +- 约束限制设置 +- 约束状态监控 + +### 5. 车辆系统 +- 底盘和车轮物理 +- 悬挂系统 +- 引擎和刹车控制 +- 转向系统 +- 差速器类型支持 +- 齿轮系统 +- 发动机扭矩曲线 +- 防抱死刹车(ABS) +- 牵引力控制(TC) +- 车身稳定控制(ESP) +- 空气动力学效应 +- 防倾杆系统 +- 车辆损伤系统 +- 燃料系统 +- 性能统计 + +### 6. 粒子系统 +- 大量粒子模拟 +- 多种粒子类型(烟雾、火焰、水、碎片、爆炸、火花、气泡、叶子、雪、雨等) +- 发射器控制 +- 粒子生命周期管理 +- 环境影响(风力、温度等) +- 特殊效果(涡流等) +- 粒子子发射器 +- 粒子轨迹和淡出效果 +- 粒子碰撞和死亡回调 +- 爆炸和火焰特效 + +### 7. 调试和可视化 +- 碰撞体可视化 +- 约束可视化 +- 接触点可视化 +- 法线可视化 +- 速度和力可视化 +- 坐标轴可视化 +- 质心可视化 +- 休眠状态可视化 +- 粒子轨迹可视化 +- 性能监控面板 +- 调试文本显示 +- 可视化选项控制 + +### 8. 实用工具 +- 物理计算工具(质量计算、冲量应用、扭矩计算等) +- 配置管理器 +- 碰撞组定义 +- 物理常量库 + +## 安装和配置 + +### 目录结构 +``` +rigid_body_physics/ +├── __init__.py +├── plugin_manager.py +├── README.md +├── core/ +│ ├── physics_world.py +│ ├── rigid_body.py +│ └── material.py +├── constraints/ +│ └── constraint_manager.py +├── vehicles/ +│ └── vehicle_system.py +├── particles/ +│ └── particle_system.py +├── debug/ +│ └── debug_tools.py +├── utils/ +│ └── physics_utils.py +└── config/ + └── physics_config.json +``` + +## 快速开始 + +### 1. 初始化插件 +```python +from plugins.user.rigid_body_physics.plugin_manager import RigidBodyPhysicsPlugin + +# 创建物理插件实例 +physics_plugin = RigidBodyPhysicsPlugin(world_node=render) +``` + +### 2. 创建刚体 +```python +# 创建一个盒子形状的动态刚体 +box_body = physics_plugin.create_rigid_body( + node_path=box_node, + shape_type='box', + mass=1.0, + body_type=RigidBody.DYNAMIC +) + +# 设置物理属性 +box_body.set_friction(0.5) +box_body.set_restitution(0.3) + +# 应用力和扭矩 +box_body.apply_force(Vec3(0, 0, 100)) # 向上施加力 +box_body.apply_torque(Vec3(0, 10, 0)) # 绕Y轴施加扭矩 +``` + +### 3. 创建约束 +```python +# 创建铰链约束 +hinge_constraint = physics_plugin.create_constraint( + constraint_type='hinge', + body_a=body_a, + body_b=body_b, + pivot_a=Point3(0, 0, 0), + pivot_b=Point3(0, 0, 0), + axis_a=Vec3(0, 0, 1), + axis_b=Vec3(0, 0, 1) +) +``` + +### 4. 创建车辆 +```python +# 定义车轮配置 +wheel_configs = [ + { + 'connection_point': Point3(-1, 1, 0), + 'wheel_direction': Vec3(0, 0, -1), + 'wheel_axle': Vec3(1, 0, 0), + 'wheel_radius': 0.3 + }, + # ... 其他车轮配置 +] + +# 创建车辆 +vehicle = physics_plugin.create_vehicle(chassis_body, wheel_configs) + +# 控制车辆 +vehicle.set_steering(0.5) # 转向 +vehicle.set_engine_force(1.0) # 加油 +vehicle.set_brake_force(0.3) # 刹车 +``` + +### 5. 创建粒子系统 +```python +# 创建粒子系统 +particle_system = physics_plugin.create_particle_system(max_particles=500) + +# 设置发射器属性 +particle_system.set_emitter_properties( + position=Point3(0, 0, 5), + velocity=Vec3(0, 0, 10), + spread=0.5 +) + +# 创建爆炸效果 +particle_system.create_explosion( + position=Point3(0, 0, 0), + particle_count=100, + explosion_force=50.0 +) +``` + +### 6. 主循环更新 +```python +def update(task): + dt = globalClock.getDt() + physics_plugin.update(dt) + + # 更新车辆 + for vehicle in physics_plugin.vehicles: + vehicle.update(dt) + + return task.cont + +taskMgr.add(update, "physics_update") +``` + +## 高级功能 + +### 材质系统 +```python +# 使用预定义材质 +concrete_material = physics_plugin.get_predefined_material('concrete') + +# 创建自定义材质 +custom_material = physics_plugin.create_custom_material( + 'my_material', + { + 'friction': 0.8, + 'restitution': 0.2, + 'density': 2000.0, + 'thermal_conductivity': 2.0, + 'color': Vec4(0.8, 0.6, 0.4, 1.0) + } +) + +# 设置材质交互 +custom_material.set_interaction_coefficient('metal', 1.2) + +# 更新材质状态 +custom_material.update_state( + temperature=50.0, + wear=0.1, + stress=100.0 +) +``` + +### 调试可视化 +```python +# 启用调试可视化 +physics_plugin.enable_debug_visualization(True) + +# 设置调试选项 +physics_plugin.set_debug_options( + show_constraints=True, + show_aabbs=True, + show_velocities=True, + show_forces=True +) + +# 性能分析 +profiler = physics_plugin.profiler +performance_report = profiler.get_performance_report() +print(f"平均FPS: {performance_report['average_fps']}") +``` + +### 性能监控 +```python +# 获取性能统计 +stats = physics_plugin.get_performance_stats() +print(f"平均更新时间: {stats['avg_update_time']}") +print(f"刚体数量: {stats['objects']['rigid_bodies']}") + +# 获取详细性能分析 +analysis = physics_plugin.profiler.get_performance_analysis() +print(f"性能等级: {analysis['performance_grade']}") +print(f"瓶颈: {analysis['bottlenecks']}") +print(f"建议: {analysis['recommendations']}") +``` + +## 配置文件 + +插件支持通过JSON配置文件进行配置: + +```json +{ + "physics": { + "gravity": [0, 0, -9.81], + "time_step": 0.016667, + "solver_iterations": 10, + "solver_tolerance": 0.001 + }, + "materials": { + "concrete": { + "friction": 0.7, + "restitution": 0.1, + "density": 2400.0 + } + }, + "vehicle": { + "max_engine_force": 3000.0, + "max_brake_force": 100.0, + "max_steering": 0.7854 + } +} +``` + +## API参考 + +### RigidBodyPhysicsPlugin类 + +#### 构造函数 +```python +RigidBodyPhysicsPlugin(world_node=None, config_file=None) +``` + +#### 主要方法 +- `create_rigid_body(node_path, shape_type, mass, body_type, material)`: 创建刚体 +- `create_constraint(constraint_type, body_a, body_b, **kwargs)`: 创建约束 +- `create_vehicle(chassis_body, wheel_configurations)`: 创建车辆 +- `create_particle_system(max_particles)`: 创建粒子系统 +- `update(dt)`: 更新物理系统 +- `ray_test(from_pos, to_pos)`: 射线检测 +- `get_performance_stats()`: 获取性能统计 +- `enable_debug_visualization(enabled)`: 启用调试可视化 +- `set_debug_options(**kwargs)`: 设置调试选项 +- `get_predefined_material(material_name)`: 获取预定义材质 +- `create_custom_material(name, properties)`: 创建自定义材质 +- `set_gravity(gravity)`: 设置重力 +- `set_time_step(time_step, max_substeps)`: 设置时间步长 +- `get_collision_groups()`: 获取碰撞组工具 +- `save_config(config_file)`: 保存配置 +- `load_config(config_file)`: 加载配置 +- `clear_all()`: 清除所有物理对象 +- `destroy()`: 销毁物理插件 + +### RigidBody类 + +#### 主要方法 +- `set_friction(friction, static_friction, dynamic_friction)`: 设置摩擦系数 +- `set_restitution(restitution)`: 设置恢复系数 +- `set_damping(linear_damping, angular_damping)`: 设置阻尼 +- `set_factors(linear_factor, angular_factor)`: 设置线性和角因子 +- `set_activation_state(active, always_active)`: 设置激活状态 +- `set_sleeping_thresholds(linear_threshold, angular_threshold)`: 设置休眠阈值 +- `set_ccd_parameters(motion_threshold, swept_sphere_radius)`: 设置CCD参数 +- `apply_force(force, position, duration)`: 应用力 +- `apply_torque(torque, duration)`: 应用扭矩 +- `apply_impulse(impulse, position, duration)`: 应用冲量 +- `apply_torque_impulse(torque_impulse)`: 应用扭矩冲量 +- `get_linear_velocity()`: 获取线性速度 +- `set_linear_velocity(velocity)`: 设置线性速度 +- `get_angular_velocity()`: 获取角速度 +- `set_angular_velocity(velocity)`: 设置角速度 +- `get_position()`: 获取位置 +- `set_position(position)`: 设置位置 +- `get_rotation()`: 获取旋转 +- `set_rotation(rotation)`: 设置旋转 +- `sync_visual_node()`: 同步物理节点和可视化节点 +- `get_mass()`: 获取质量 +- `set_mass(mass)`: 设置质量 +- `get_kinetic_energy()`: 计算动能 +- `get_potential_energy(gravity)`: 计算势能 +- `get_total_energy(gravity)`: 计算总能量 +- `set_custom_data(key, value)`: 设置自定义数据 +- `get_custom_data(key, default)`: 获取自定义数据 +- `destroy()`: 销毁刚体 + +### PhysicsMaterial类 + +#### 主要方法 +- `set_property(property_name, value)`: 设置材质属性 +- `get_property(property_name)`: 获取材质属性 +- `set_friction(friction, static_friction, dynamic_friction)`: 设置摩擦系数 +- `set_restitution(restitution)`: 设置恢复系数 +- `set_density(density)`: 设置密度 +- `set_color(color)`: 设置颜色 +- `set_thermal_properties(conductivity, specific_heat, expansion)`: 设置热力学属性 +- `set_electrical_properties(conductivity, dielectric, permeability)`: 设置电学属性 +- `set_fluid_properties(viscosity, surface_tension, compressibility)`: 设置流体属性 +- `set_visual_properties(roughness, metallic, specular, emissive)`: 设置视觉属性 +- `set_environmental_properties(weather_resistance, corrosion_rate, aging_rate)`: 设置环境属性 +- `set_special_properties(flammability, toxicity, radioactivity)`: 设置特殊属性 +- `update_state(temperature, humidity, wear, contamination, stress, fatigue)`: 更新材质状态 +- `get_effective_friction(other_material, velocity)`: 获取有效摩擦系数 +- `get_effective_restitution(other_material, impact_velocity)`: 获取有效恢复系数 +- `set_interaction_coefficient(other_material_name, coefficient)`: 设置交互系数 +- `apply_to_rigid_body(rigid_body)`: 应用到刚体 +- `to_dict()`: 转换为字典 +- `from_dict(data)`: 从字典创建 +- `save_to_file(filepath)`: 保存到文件 +- `load_from_file(filepath)`: 从文件加载 + +### ConstraintManager类 + +#### 支持的约束类型 +- `CONSTRAINT_POINT2POINT`: 点对点约束 +- `CONSTRAINT_HINGE`: 铰链约束 +- `CONSTRAINT_SLIDER`: 滑动约束 +- `CONSTRAINT_CONETWIST`: 锥形扭转约束 +- `CONSTRAINT_6DOF`: 6自由度约束 +- `CONSTRAINT_6DOF_SPRING`: 6自由度弹簧约束 +- `CONSTRAINT_GEAR`: 齿轮约束 +- `CONSTRAINT_FIXED`: 固定约束 + +#### 主要方法 +- `create_point2point_constraint(body_a, body_b, pivot_a, pivot_b, ...)`: 创建点对点约束 +- `create_hinge_constraint(body_a, body_b, pivot_a, pivot_b, axis_a, axis_b, ...)`: 创建铰链约束 +- `create_slider_constraint(body_a, body_b, frame_a, frame_b, ...)`: 创建滑动约束 +- `create_cone_twist_constraint(body_a, body_b, frame_a, frame_b, ...)`: 创建锥形扭转约束 +- `create_6dof_constraint(body_a, body_b, frame_a, frame_b, ...)`: 创建6自由度约束 +- `create_6dof_spring_constraint(body_a, body_b, frame_a, frame_b, ...)`: 创建6自由度弹簧约束 +- `create_gear_constraint(body_a, body_b, axis_a, axis_b, ratio)`: 创建齿轮约束 +- `create_fixed_constraint(body_a, body_b, frame_a, frame_b, ...)`: 创建固定约束 +- `set_motor(constraint, axis, target_velocity, max_impulse)`: 设置马达 +- `disable_motor(constraint)`: 禁用马达 +- `set_spring(constraint, axis, stiffness, damping, equilibrium)`: 设置弹簧 +- `disable_spring(constraint, axis)`: 禁用弹簧 +- `set_limit(constraint, axis, lower_limit, upper_limit)`: 设置限制 +- `add_constraint_to_group(constraint, group_name)`: 添加到组 +- `remove_constraint_from_group(constraint, group_name)`: 从组移除 +- `add_constraint_tag(constraint, tag)`: 添加标签 +- `remove_constraint_tag(constraint, tag)`: 移除标签 +- `get_constraints_by_tag(tag)`: 根据标签获取约束 +- `get_constraints_by_group(group_name)`: 根据组获取约束 +- `get_constraint_state(constraint)`: 获取约束状态 +- `get_constraint_statistics()`: 获取约束统计 + +### VehicleSystem类 + +#### 主要方法 +- `add_wheel(...)`: 添加车轮 +- `set_steering(steering)`: 设置转向 +- `set_engine_force(force)`: 设置引擎力 +- `set_brake_force(force)`: 设置刹车力 +- `set_handbrake_force(force)`: 设置手刹力 +- `shift_up()`: 升档 +- `shift_down()`: 降档 +- `set_gear(gear)`: 设置档位 +- `update(dt)`: 更新车辆 +- `get_current_speed_km_hour()`: 获取当前速度(km/h) +- `get_wheel_info(wheel_index)`: 获取车轮信息 +- `get_vehicle_state()`: 获取车辆状态 +- `get_engine_torque()`: 获取发动机扭矩 +- `get_engine_power()`: 获取发动机功率 +- `apply_damage(component, amount)`: 应用损伤 +- `repair_component(component, amount)`: 修复组件 +- `refuel(amount)`: 加油 +- `get_performance_stats()`: 获取性能统计 +- `enable_anti_lock_brake(enabled)`: 启用ABS +- `enable_traction_control(enabled)`: 启用牵引力控制 +- `enable_stability_control(enabled)`: 启用车身稳定控制 +- `destroy()`: 销毁车辆 + +### ParticleSystem类 + +#### 主要方法 +- `set_emitter_properties(...)`: 设置发射器属性 +- `set_particle_properties(...)`: 设置粒子属性 +- `set_physics_properties(...)`: 设置物理属性 +- `set_collision_properties(...)`: 设置碰撞属性 +- `set_environmental_effects(...)`: 设置环境影响 +- `set_special_effects(...)`: 设置特殊效果 +- `set_lifetime_behaviors(...)`: 设置生命周期行为 +- `set_sub_emitter(...)`: 设置子发射器 +- `set_performance_options(...)`: 设置性能选项 +- `set_emission_rate(rate)`: 设置发射速率 +- `set_emission_burst(count, interval)`: 设置爆发发射 +- `emit_particle(position, velocity, lifetime, color)`: 发射粒子 +- `update(dt)`: 更新粒子系统 +- `get_particle_count()`: 获取粒子数量 +- `get_statistics()`: 获取统计信息 +- `clear_particles()`: 清除粒子 +- `pause(paused)`: 暂停/恢复 +- `set_time_scale(scale)`: 设置时间缩放 +- `create_explosion(position, particle_count, explosion_force, ...)`: 创建爆炸 +- `create_fire_effect(position, particle_count, rise_speed, ...)`: 创建火焰 +- `destroy()`: 销毁粒子系统 + +### PhysicsWorld类 + +#### 主要方法 +- `set_gravity(gravity)`: 设置重力 +- `set_time_step(time_step, max_substeps)`: 设置时间步长 +- `set_solver_parameters(iterations, tolerance)`: 设置求解器参数 +- `set_split_impulse(enable, penetration_threshold)`: 设置分离冲量 +- `set_ccd_parameters(threshold, radius)`: 设置CCD参数 +- `enable_multithreading(enable, thread_count)`: 启用多线程 +- `setup_spatial_partitioning(partition_size)`: 设置空间分区 +- `update(dt)`: 更新物理世界 +- `add_rigid_body(rigid_body)`: 添加刚体 +- `remove_rigid_body(rigid_body)`: 移除刚体 +- `add_constraint(constraint)`: 添加约束 +- `remove_constraint(constraint)`: 移除约束 +- `add_vehicle(vehicle)`: 添加车辆 +- `remove_vehicle(vehicle)`: 移除车辆 +- `add_particle_system(particle_system)`: 添加粒子系统 +- `remove_particle_system(particle_system)`: 移除粒子系统 +- `ray_test(from_pos, to_pos)`: 射线检测 +- `ray_test_all(from_pos, to_pos)`: 射线检测(所有结果) +- `get_performance_stats()`: 获取性能统计 +- `pause(paused)`: 暂停/恢复 +- `set_time_scale(scale)`: 设置时间缩放 +- `clear()`: 清除所有对象 +- `reset()`: 重置物理世界 + +### PhysicsDebug类 + +#### 主要方法 +- `enable_debug(enabled)`: 启用/禁用调试 +- `set_debug_options(...)`: 设置调试选项 +- `update_debug_visualization()`: 更新调试可视化 +- `add_contact_point(position, normal, impulse)`: 添加接触点 +- `add_collision_event(body_a, body_b, contact_points)`: 添加碰撞事件 +- `set_line_thickness(thickness)`: 设置线条粗细 +- `set_axis_length(length)`: 设置坐标轴长度 +- `set_velocity_scale(scale)`: 设置速度缩放 +- `set_force_scale(scale)`: 设置力缩放 +- `set_contact_point_size(size)`: 设置接触点大小 +- `set_normal_scale(scale)`: 设置法线缩放 +- `enable_debug_text(enabled)`: 启用/禁用调试文本 +- `enable_stats_display(enabled)`: 启用/禁用统计显示 +- `set_debug_update_frequency(frequency)`: 设置调试更新频率 +- `hide_debug_visualization()`: 隐藏调试可视化 +- `show_debug_visualization()`: 显示调试可视化 +- `destroy()`: 销毁调试工具 + +### PhysicsProfiler类 + +#### 主要方法 +- `record_frame()`: 记录帧数据 +- `get_average_update_time()`: 获取平均更新时间 +- `get_max_update_time()`: 获取最大更新时间 +- `get_min_update_time()`: 获取最小更新时间 +- `get_average_fps()`: 获取平均FPS +- `get_object_counts()`: 获取对象数量 +- `get_average_object_counts()`: 获取平均对象数量 +- `get_performance_report()`: 获取性能报告 +- `get_performance_analysis()`: 获取性能分析 +- `reset_statistics()`: 重置统计 +- `export_statistics(filepath)`: 导出统计 +- `destroy()`: 销毁分析器 + +## 性能优化建议 + +1. **合理设置时间步长**: 过小的时间步长会影响性能,建议在1/60秒左右 +2. **控制刚体数量**: 过多的刚体会影响性能,考虑使用休眠机制和剔除 +3. **使用适当的碰撞形状**: 复杂形状比简单形状消耗更多资源,优先使用基本形状 +4. **启用休眠机制**: 让静止的物体进入休眠状态以节省计算资源 +5. **使用连续碰撞检测**: 对于高速运动的物体启用CCD以避免穿透 +6. **合理设置求解器参数**: 迭代次数和容差影响精度和性能的平衡 +7. **启用调试可视化仅在开发时**: 调试可视化会消耗额外资源 +8. **及时销毁不需要的对象**: 避免内存泄漏 +9. **使用对象池**: 对于频繁创建销毁的对象使用对象池 +10. **空间分区**: 对于大型场景使用空间分区优化碰撞检测 + +## 故障排除 + +### 常见问题 + +1. **刚体穿透**: + - 检查时间步长是否过小,增加求解器迭代次数 + - 对高速物体启用连续碰撞检测(CCD) + - 调整碰撞形状边界 + +2. **性能问题**: + - 减少刚体和约束数量 + - 优化碰撞形状复杂度 + - 启用休眠机制 + - 使用空间分区 + - 调整求解器参数 + +3. **约束不稳定**: + - 检查约束参数,适当调整限制范围 + - 增加求解器迭代次数 + - 调整约束的破裂阈值 + +4. **车辆控制不自然**: + - 调整悬挂参数 + - 调整轮胎摩擦系数 + - 调整发动机扭矩曲线 + - 启用或调整ABS、TC等辅助系统 + +5. **粒子效果不理想**: + - 调整发射器参数 + - 调整粒子生命周期参数 + - 调整物理属性(重力、阻力等) + - 调整环境影响参数 + +### 联系支持 + 如有问题,请联系插件开发者或查看相关文档。 \ No newline at end of file diff --git a/plugins/user/rigid_body_physics/__init__.py b/plugins/user/rigid_body_physics/__init__.py index 6f884002..764d9099 100644 --- a/plugins/user/rigid_body_physics/__init__.py +++ b/plugins/user/rigid_body_physics/__init__.py @@ -1,19 +1,19 @@ -""" -刚体动力学插件初始化文件 -""" - -from .core.physics_world import PhysicsWorld -from .core.rigid_body import RigidBody -from .core.material import PhysicsMaterial -from .constraints.constraint_manager import ConstraintManager -from .vehicles.vehicle_system import VehicleSystem -from .particles.particle_system import ParticleSystem - -__all__ = [ - 'PhysicsWorld', - 'RigidBody', - 'PhysicsMaterial', - 'ConstraintManager', - 'VehicleSystem', - 'ParticleSystem' +""" +刚体动力学插件初始化文件 +""" + +from .core.physics_world import PhysicsWorld +from .core.rigid_body import RigidBody +from .core.material import PhysicsMaterial +from .constraints.constraint_manager import ConstraintManager +from .vehicles.vehicle_system import VehicleSystem +from .particles.particle_system import ParticleSystem + +__all__ = [ + 'PhysicsWorld', + 'RigidBody', + 'PhysicsMaterial', + 'ConstraintManager', + 'VehicleSystem', + 'ParticleSystem' ] \ No newline at end of file diff --git a/plugins/user/rigid_body_physics/config/physics_config.json b/plugins/user/rigid_body_physics/config/physics_config.json index 6f5c5adc..57b58d91 100644 --- a/plugins/user/rigid_body_physics/config/physics_config.json +++ b/plugins/user/rigid_body_physics/config/physics_config.json @@ -1,52 +1,52 @@ -{ - "physics": { - "gravity": [0, 0, -9.81], - "time_step": 0.016667, - "max_substeps": 10, - "solver_iterations": 10, - "solver_tolerance": 0.001 - }, - "materials": { - "concrete": { - "friction": 0.7, - "restitution": 0.1, - "density": 2400.0, - "color": [0.6, 0.6, 0.6, 1.0] - }, - "wood": { - "friction": 0.4, - "restitution": 0.2, - "density": 700.0, - "color": [0.8, 0.6, 0.4, 1.0] - }, - "metal": { - "friction": 0.3, - "restitution": 0.4, - "density": 7800.0, - "color": [0.7, 0.7, 0.8, 1.0] - }, - "rubber": { - "friction": 0.9, - "restitution": 0.8, - "density": 1200.0, - "color": [0.2, 0.2, 0.2, 1.0] - }, - "ice": { - "friction": 0.03, - "restitution": 0.1, - "density": 917.0, - "color": [0.8, 0.9, 1.0, 1.0] - } - }, - "debug": { - "enabled": false, - "show_constraints": true, - "show_aabbs": false, - "show_contacts": false, - "show_normals": false - }, - "performance": { - "max_particles": 1000, - "profiler_enabled": true - } +{ + "physics": { + "gravity": [0, 0, -9.81], + "time_step": 0.016667, + "max_substeps": 10, + "solver_iterations": 10, + "solver_tolerance": 0.001 + }, + "materials": { + "concrete": { + "friction": 0.7, + "restitution": 0.1, + "density": 2400.0, + "color": [0.6, 0.6, 0.6, 1.0] + }, + "wood": { + "friction": 0.4, + "restitution": 0.2, + "density": 700.0, + "color": [0.8, 0.6, 0.4, 1.0] + }, + "metal": { + "friction": 0.3, + "restitution": 0.4, + "density": 7800.0, + "color": [0.7, 0.7, 0.8, 1.0] + }, + "rubber": { + "friction": 0.9, + "restitution": 0.8, + "density": 1200.0, + "color": [0.2, 0.2, 0.2, 1.0] + }, + "ice": { + "friction": 0.03, + "restitution": 0.1, + "density": 917.0, + "color": [0.8, 0.9, 1.0, 1.0] + } + }, + "debug": { + "enabled": false, + "show_constraints": true, + "show_aabbs": false, + "show_contacts": false, + "show_normals": false + }, + "performance": { + "max_particles": 1000, + "profiler_enabled": true + } } \ No newline at end of file diff --git a/plugins/user/rigid_body_physics/constraints/constraint_manager.py b/plugins/user/rigid_body_physics/constraints/constraint_manager.py index a8c38b66..145abca4 100644 --- a/plugins/user/rigid_body_physics/constraints/constraint_manager.py +++ b/plugins/user/rigid_body_physics/constraints/constraint_manager.py @@ -1,1022 +1,1022 @@ -""" -约束和关节系统模块 -负责各种物理约束和关节的创建和管理 -""" - -from panda3d.core import Point3, Vec3, TransformState, LVector3f -from panda3d.bullet import BulletConstraint -from panda3d.bullet import BulletPoint2PointConstraint, BulletHingeConstraint -from panda3d.bullet import BulletSliderConstraint, BulletConeTwistConstraint -from panda3d.bullet import BulletGeneric6DofConstraint, BulletGeneric6DofSpringConstraint -from panda3d.bullet import BulletGearConstraint, BulletFixedConstraint -import math -import time -from collections import deque - - -class ConstraintManager: - """ - 约束管理器类 - 管理各种类型的物理约束和关节 - """ - - CONSTRAINT_POINT2POINT = 'point2point' - CONSTRAINT_HINGE = 'hinge' - CONSTRAINT_SLIDER = 'slider' - CONSTRAINT_CONETWIST = 'conetwist' - CONSTRAINT_6DOF = '6dof' - CONSTRAINT_6DOF_SPRING = '6dof_spring' - CONSTRAINT_GEAR = 'gear' - CONSTRAINT_FIXED = 'fixed' - - def __init__(self): - """ - 初始化约束管理器 - """ - self.constraints = [] - self.constraint_groups = {} # 约束组 - self.constraint_limits = {} # 约束限制 - self.constraint_motors = {} # 约束马达 - self.constraint_springs = {} # 约束弹簧 - - print("约束管理器初始化完成") - - def create_point2point_constraint(self, body_a, body_b, pivot_a, pivot_b, - collision=True, breaking_threshold=0.0, - disable_collision_between_linked_bodies=True): - """ - 创建点对点约束 - - Args: - body_a (RigidBody): 第一个刚体 - body_b (RigidBody): 第二个刚体 - pivot_a (Point3): 第一个刚体上的连接点 - pivot_b (Point3): 第二个刚体上的连接点 - collision (bool): 是否允许两个刚体碰撞 - breaking_threshold (float): 破裂阈值 - disable_collision_between_linked_bodies (bool): 是否禁用连接刚体间的碰撞 - - Returns: - BulletPoint2PointConstraint: 点对点约束对象 - """ - constraint = BulletPoint2PointConstraint( - body_a.bullet_body, body_b.bullet_body, pivot_a, pivot_b) - - # 设置约束属性 - constraint.setOverrideEnabled(not collision) - if breaking_threshold > 0: - constraint.setBreakingImpulseThreshold(breaking_threshold) - - # 禁用连接刚体间的碰撞 - if disable_collision_between_linked_bodies: - body_a.bullet_body.setIgnoreCollisionCheck(body_b.bullet_body, True) - - # 保存约束信息 - constraint_info = { - 'type': self.CONSTRAINT_POINT2POINT, - 'constraint': constraint, - 'body_a': body_a, - 'body_b': body_b, - 'properties': { - 'pivot_a': pivot_a, - 'pivot_b': pivot_b, - 'collision': collision, - 'breaking_threshold': breaking_threshold, - 'disable_collision': disable_collision_between_linked_bodies - }, - 'creation_time': time.time(), - 'is_enabled': True, - 'group': None, - 'tags': set(), - 'feedback': None - } - - self.constraints.append(constraint_info) - return constraint - - def create_hinge_constraint(self, body_a, body_b, pivot_a, pivot_b, axis_a, axis_b, - collision=True, breaking_threshold=0.0, limits=None, - motor=None, disable_collision_between_linked_bodies=True): - """ - 创建铰链约束 - - Args: - body_a (RigidBody): 第一个刚体 - body_b (RigidBody): 第二个刚体 - pivot_a (Point3): 第一个刚体上的连接点 - pivot_b (Point3): 第二个刚体上的连接点 - axis_a (Vec3): 第一个刚体上的轴向 - axis_b (Vec3): 第二个刚体上的轴向 - collision (bool): 是否允许两个刚体碰撞 - breaking_threshold (float): 破裂阈值 - limits (tuple): 旋转限制 (min_angle, max_angle) 或 None - motor (dict): 马达参数 {'target_velocity': float, 'max_impulse': float} - disable_collision_between_linked_bodies (bool): 是否禁用连接刚体间的碰撞 - - Returns: - BulletHingeConstraint: 铰链约束对象 - """ - constraint = BulletHingeConstraint( - body_a.bullet_body, body_b.bullet_body, pivot_a, pivot_b, axis_a, axis_b) - - # 设置约束属性 - constraint.setOverrideEnabled(not collision) - if breaking_threshold > 0: - constraint.setBreakingImpulseThreshold(breaking_threshold) - - # 设置旋转限制 - if limits: - min_angle, max_angle = limits - constraint.setLimit(min_angle, max_angle) - - # 设置马达 - if motor: - target_velocity = motor.get('target_velocity', 0.0) - max_impulse = motor.get('max_impulse', 1.0) - constraint.enableAngularMotor(True, target_velocity, max_impulse) - - # 禁用连接刚体间的碰撞 - if disable_collision_between_linked_bodies: - body_a.bullet_body.setIgnoreCollisionCheck(body_b.bullet_body, True) - - # 保存约束信息 - constraint_info = { - 'type': self.CONSTRAINT_HINGE, - 'constraint': constraint, - 'body_a': body_a, - 'body_b': body_b, - 'properties': { - 'pivot_a': pivot_a, - 'pivot_b': pivot_b, - 'axis_a': axis_a, - 'axis_b': axis_b, - 'collision': collision, - 'breaking_threshold': breaking_threshold, - 'limits': limits, - 'motor': motor, - 'disable_collision': disable_collision_between_linked_bodies - }, - 'creation_time': time.time(), - 'is_enabled': True, - 'group': None, - 'tags': set(), - 'feedback': None, - 'motor_enabled': motor is not None, - 'current_angle': 0.0, - 'angular_velocity': 0.0 - } - - self.constraints.append(constraint_info) - return constraint - - def create_slider_constraint(self, body_a, body_b, frame_a=None, frame_b=None, - collision=True, breaking_threshold=0.0, limits=None, - motor=None, disable_collision_between_linked_bodies=True): - """ - 创建滑动约束 - - Args: - body_a (RigidBody): 第一个刚体 - body_b (RigidBody): 第二个刚体 - frame_a (TransformState): 第一个刚体上的参考帧 - frame_b (TransformState): 第二个刚体上的参考帧 - collision (bool): 是否允许两个刚体碰撞 - breaking_threshold (float): 破裂阈值 - limits (tuple): 滑动限制 (linear_min, linear_max, angular_min, angular_max) 或 None - motor (dict): 马达参数 {'target_linear_velocity': float, 'max_linear_force': float, - 'target_angular_velocity': float, 'max_angular_force': float} - disable_collision_between_linked_bodies (bool): 是否禁用连接刚体间的碰撞 - - Returns: - BulletSliderConstraint: 滑动约束对象 - """ - # 如果没有提供参考帧,创建默认的 - if frame_a is None: - frame_a = TransformState.makePos(Point3(0, 0, 0)) - if frame_b is None: - frame_b = TransformState.makePos(Point3(0, 0, 0)) - - constraint = BulletSliderConstraint( - body_a.bullet_body, body_b.bullet_body, frame_a, frame_b, useLinearReferenceFrameA=True) - - # 设置约束属性 - constraint.setOverrideEnabled(not collision) - if breaking_threshold > 0: - constraint.setBreakingImpulseThreshold(breaking_threshold) - - # 设置滑动限制 - if limits: - linear_min, linear_max, angular_min, angular_max = limits - constraint.setLowerLinLimit(linear_min) - constraint.setUpperLinLimit(linear_max) - constraint.setLowerAngLimit(angular_min) - constraint.setUpperAngLimit(angular_max) - - # 设置马达 - if motor: - # 滑动约束的马达设置比较复杂,需要分别设置线性和角马达 - pass - - # 禁用连接刚体间的碰撞 - if disable_collision_between_linked_bodies: - body_a.bullet_body.setIgnoreCollisionCheck(body_b.bullet_body, True) - - # 保存约束信息 - constraint_info = { - 'type': self.CONSTRAINT_SLIDER, - 'constraint': constraint, - 'body_a': body_a, - 'body_b': body_b, - 'properties': { - 'frame_a': frame_a, - 'frame_b': frame_b, - 'collision': collision, - 'breaking_threshold': breaking_threshold, - 'limits': limits, - 'motor': motor, - 'disable_collision': disable_collision_between_linked_bodies - }, - 'creation_time': time.time(), - 'is_enabled': True, - 'group': None, - 'tags': set(), - 'feedback': None, - 'motor_enabled': motor is not None, - 'current_linear_position': 0.0, - 'current_angular_position': 0.0 - } - - self.constraints.append(constraint_info) - return constraint - - def create_cone_twist_constraint(self, body_a, body_b, frame_a=None, frame_b=None, - collision=True, breaking_threshold=0.0, limits=None, - motor=None, disable_collision_between_linked_bodies=True): - """ - 创建锥形扭转约束 - - Args: - body_a (RigidBody): 第一个刚体 - body_b (RigidBody): 第二个刚体 - frame_a (TransformState): 第一个刚体上的参考帧 - frame_b (TransformState): 第二个刚体上的参考帧 - collision (bool): 是否允许两个刚体碰撞 - breaking_threshold (float): 破裂阈值 - limits (tuple): 限制 (swing_span1, swing_span2, twist_span) 或 None - motor (dict): 马达参数 - disable_collision_between_linked_bodies (bool): 是否禁用连接刚体间的碰撞 - - Returns: - BulletConeTwistConstraint: 锥形扭转约束对象 - """ - # 如果没有提供参考帧,创建默认的 - if frame_a is None: - frame_a = TransformState.makePos(Point3(0, 0, 0)) - if frame_b is None: - frame_b = TransformState.makePos(Point3(0, 0, 0)) - - constraint = BulletConeTwistConstraint( - body_a.bullet_body, body_b.bullet_body, frame_a, frame_b) - - # 设置约束属性 - constraint.setOverrideEnabled(not collision) - if breaking_threshold > 0: - constraint.setBreakingImpulseThreshold(breaking_threshold) - - # 设置限制 - if limits: - swing_span1, swing_span2, twist_span = limits - constraint.setLimit(swing_span1, swing_span2, twist_span) - - # 禁用连接刚体间的碰撞 - if disable_collision_between_linked_bodies: - body_a.bullet_body.setIgnoreCollisionCheck(body_b.bullet_body, True) - - # 保存约束信息 - constraint_info = { - 'type': self.CONSTRAINT_CONETWIST, - 'constraint': constraint, - 'body_a': body_a, - 'body_b': body_b, - 'properties': { - 'frame_a': frame_a, - 'frame_b': frame_b, - 'collision': collision, - 'breaking_threshold': breaking_threshold, - 'limits': limits, - 'motor': motor, - 'disable_collision': disable_collision_between_linked_bodies - }, - 'creation_time': time.time(), - 'is_enabled': True, - 'group': None, - 'tags': set(), - 'feedback': None - } - - self.constraints.append(constraint_info) - return constraint - - def create_6dof_constraint(self, body_a, body_b, frame_a=None, frame_b=None, - collision=True, breaking_threshold=0.0, linear_limits=None, - angular_limits=None, motors=None, springs=None, - disable_collision_between_linked_bodies=True): - """ - 创建6自由度约束 - - Args: - body_a (RigidBody): 第一个刚体 - body_b (RigidBody): 第二个刚体 - frame_a (TransformState): 第一个刚体上的参考帧 - frame_b (TransformState): 第二个刚体上的参考帧 - collision (bool): 是否允许两个刚体碰撞 - breaking_threshold (float): 破裂阈值 - linear_limits (tuple): 线性限制 (x_min, x_max, y_min, y_max, z_min, z_max) 或 None - angular_limits (tuple): 角度限制 (x_min, x_max, y_min, y_max, z_min, z_max) 或 None - motors (dict): 马达参数 {'linear': {axis: {'target_velocity', 'max_force'}}, - 'angular': {axis: {'target_velocity', 'max_force'}}} - springs (dict): 弹簧参数 {'linear': {axis: {'stiffness', 'damping', 'equilibrium'}}, - 'angular': {axis: {'stiffness', 'damping', 'equilibrium'}}} - disable_collision_between_linked_bodies (bool): 是否禁用连接刚体间的碰撞 - - Returns: - BulletGeneric6DofConstraint: 6自由度约束对象 - """ - # 如果没有提供参考帧,创建默认的 - if frame_a is None: - frame_a = TransformState.makePos(Point3(0, 0, 0)) - if frame_b is None: - frame_b = TransformState.makePos(Point3(0, 0, 0)) - - constraint = BulletGeneric6DofConstraint( - body_a.bullet_body, body_b.bullet_body, frame_a, frame_b, useLinearReferenceFrameA=True) - - # 设置约束属性 - constraint.setOverrideEnabled(not collision) - if breaking_threshold > 0: - constraint.setBreakingImpulseThreshold(breaking_threshold) - - # 设置线性限制 - if linear_limits: - x_min, x_max, y_min, y_max, z_min, z_max = linear_limits - constraint.setLinearLowerLimit(Vec3(x_min, y_min, z_min)) - constraint.setLinearUpperLimit(Vec3(x_max, y_max, z_max)) - - # 设置角度限制 - if angular_limits: - x_min, x_max, y_min, y_max, z_min, z_max = angular_limits - constraint.setAngularLowerLimit(Vec3(x_min, y_min, z_min)) - constraint.setAngularUpperLimit(Vec3(x_max, y_max, z_max)) - - # 设置马达 - if motors: - linear_motors = motors.get('linear', {}) - angular_motors = motors.get('angular', {}) - - for axis in range(3): - # 线性马达 - if axis in linear_motors: - motor_info = linear_motors[axis] - constraint.setTargetVelocity(axis, motor_info.get('target_velocity', 0.0)) - constraint.setMaxMotorForce(axis, motor_info.get('max_force', 1.0)) - constraint.setParam(axis, 1) # 启用马达 - - # 角马达 - if axis in angular_motors: - motor_info = angular_motors[axis] - constraint.setTargetVelocity(axis + 3, motor_info.get('target_velocity', 0.0)) - constraint.setMaxMotorForce(axis + 3, motor_info.get('max_force', 1.0)) - constraint.setParam(axis + 3, 1) # 启用马达 - - # 禁用连接刚体间的碰撞 - if disable_collision_between_linked_bodies: - body_a.bullet_body.setIgnoreCollisionCheck(body_b.bullet_body, True) - - # 保存约束信息 - constraint_info = { - 'type': self.CONSTRAINT_6DOF, - 'constraint': constraint, - 'body_a': body_a, - 'body_b': body_b, - 'properties': { - 'frame_a': frame_a, - 'frame_b': frame_b, - 'collision': collision, - 'breaking_threshold': breaking_threshold, - 'linear_limits': linear_limits, - 'angular_limits': angular_limits, - 'motors': motors, - 'springs': springs, - 'disable_collision': disable_collision_between_linked_bodies - }, - 'creation_time': time.time(), - 'is_enabled': True, - 'group': None, - 'tags': set(), - 'feedback': None, - 'motor_enabled': motors is not None, - 'spring_enabled': springs is not None - } - - self.constraints.append(constraint_info) - return constraint - - def create_6dof_spring_constraint(self, body_a, body_b, frame_a=None, frame_b=None, - collision=True, breaking_threshold=0.0, linear_limits=None, - angular_limits=None, springs=None, motors=None, - disable_collision_between_linked_bodies=True): - """ - 创建6自由度弹簧约束 - - Args: - body_a (RigidBody): 第一个刚体 - body_b (RigidBody): 第二个刚体 - frame_a (TransformState): 第一个刚体上的参考帧 - frame_b (TransformState): 第二个刚体上的参考帧 - collision (bool): 是否允许两个刚体碰撞 - breaking_threshold (float): 破裂阈值 - linear_limits (tuple): 线性限制 (x_min, x_max, y_min, y_max, z_min, z_max) 或 None - angular_limits (tuple): 角度限制 (x_min, x_max, y_min, y_max, z_min, z_max) 或 None - springs (dict): 弹簧参数 {'linear': {axis: {'stiffness', 'damping', 'equilibrium'}}, - 'angular': {axis: {'stiffness', 'damping', 'equilibrium'}}} - motors (dict): 马达参数 - disable_collision_between_linked_bodies (bool): 是否禁用连接刚体间的碰撞 - - Returns: - BulletGeneric6DofSpringConstraint: 6自由度弹簧约束对象 - """ - # 如果没有提供参考帧,创建默认的 - if frame_a is None: - frame_a = TransformState.makePos(Point3(0, 0, 0)) - if frame_b is None: - frame_b = TransformState.makePos(Point3(0, 0, 0)) - - constraint = BulletGeneric6DofSpringConstraint( - body_a.bullet_body, body_b.bullet_body, frame_a, frame_b, useLinearReferenceFrameA=True) - - # 设置约束属性 - constraint.setOverrideEnabled(not collision) - if breaking_threshold > 0: - constraint.setBreakingImpulseThreshold(breaking_threshold) - - # 设置线性限制 - if linear_limits: - x_min, x_max, y_min, y_max, z_min, z_max = linear_limits - constraint.setLinearLowerLimit(Vec3(x_min, y_min, z_min)) - constraint.setLinearUpperLimit(Vec3(x_max, y_max, z_max)) - - # 设置角度限制 - if angular_limits: - x_min, x_max, y_min, y_max, z_min, z_max = angular_limits - constraint.setAngularLowerLimit(Vec3(x_min, y_min, z_min)) - constraint.setAngularUpperLimit(Vec3(x_max, y_max, z_max)) - - # 设置弹簧 - if springs: - linear_springs = springs.get('linear', {}) - angular_springs = springs.get('angular', {}) - - for axis in range(3): - # 线性弹簧 - if axis in linear_springs: - spring_info = linear_springs[axis] - constraint.setStiffness(axis, spring_info.get('stiffness', 0.0)) - constraint.setDamping(axis, spring_info.get('damping', 0.0)) - constraint.setEquilibriumPoint(axis, spring_info.get('equilibrium', 0.0)) - constraint.enableSpring(axis, True) - - # 角弹簧 - if axis in angular_springs: - spring_info = angular_springs[axis] - constraint.setStiffness(axis + 3, spring_info.get('stiffness', 0.0)) - constraint.setDamping(axis + 3, spring_info.get('damping', 0.0)) - constraint.setEquilibriumPoint(axis + 3, spring_info.get('equilibrium', 0.0)) - constraint.enableSpring(axis + 3, True) - - # 禁用连接刚体间的碰撞 - if disable_collision_between_linked_bodies: - body_a.bullet_body.setIgnoreCollisionCheck(body_b.bullet_body, True) - - # 保存约束信息 - constraint_info = { - 'type': self.CONSTRAINT_6DOF_SPRING, - 'constraint': constraint, - 'body_a': body_a, - 'body_b': body_b, - 'properties': { - 'frame_a': frame_a, - 'frame_b': frame_b, - 'collision': collision, - 'breaking_threshold': breaking_threshold, - 'linear_limits': linear_limits, - 'angular_limits': angular_limits, - 'springs': springs, - 'motors': motors, - 'disable_collision': disable_collision_between_linked_bodies - }, - 'creation_time': time.time(), - 'is_enabled': True, - 'group': None, - 'tags': set(), - 'feedback': None, - 'spring_enabled': springs is not None, - 'motor_enabled': motors is not None - } - - self.constraints.append(constraint_info) - return constraint - - def create_gear_constraint(self, body_a, body_b, axis_a, axis_b, ratio=1.0, - collision=True, breaking_threshold=0.0): - """ - 创建齿轮约束 - - Args: - body_a (RigidBody): 第一个刚体 - body_b (RigidBody): 第二个刚体 - axis_a (Vec3): 第一个刚体上的轴向 - axis_b (Vec3): 第二个刚体上的轴向 - ratio (float): 齿轮比 - collision (bool): 是否允许两个刚体碰撞 - breaking_threshold (float): 破裂阈值 - - Returns: - BulletGearConstraint: 齿轮约束对象 - """ - constraint = BulletGearConstraint( - body_a.bullet_body, body_b.bullet_body, axis_a, axis_b, ratio) - - # 设置约束属性 - constraint.setOverrideEnabled(not collision) - if breaking_threshold > 0: - constraint.setBreakingImpulseThreshold(breaking_threshold) - - # 保存约束信息 - constraint_info = { - 'type': self.CONSTRAINT_GEAR, - 'constraint': constraint, - 'body_a': body_a, - 'body_b': body_b, - 'properties': { - 'axis_a': axis_a, - 'axis_b': axis_b, - 'ratio': ratio, - 'collision': collision, - 'breaking_threshold': breaking_threshold - }, - 'creation_time': time.time(), - 'is_enabled': True, - 'group': None, - 'tags': set(), - 'feedback': None - } - - self.constraints.append(constraint_info) - return constraint - - def create_fixed_constraint(self, body_a, body_b, frame_a=None, frame_b=None, - collision=True, breaking_threshold=0.0, - disable_collision_between_linked_bodies=True): - """ - 创建固定约束 - - Args: - body_a (RigidBody): 第一个刚体 - body_b (RigidBody): 第二个刚体 - frame_a (TransformState): 第一个刚体上的参考帧 - frame_b (TransformState): 第二个刚体上的参考帧 - collision (bool): 是否允许两个刚体碰撞 - breaking_threshold (float): 破裂阈值 - disable_collision_between_linked_bodies (bool): 是否禁用连接刚体间的碰撞 - - Returns: - BulletFixedConstraint: 固定约束对象 - """ - # 如果没有提供参考帧,创建默认的 - if frame_a is None: - frame_a = TransformState.makePos(Point3(0, 0, 0)) - if frame_b is None: - frame_b = TransformState.makePos(Point3(0, 0, 0)) - - constraint = BulletFixedConstraint( - body_a.bullet_body, body_b.bullet_body, frame_a, frame_b) - - # 设置约束属性 - constraint.setOverrideEnabled(not collision) - if breaking_threshold > 0: - constraint.setBreakingImpulseThreshold(breaking_threshold) - - # 禁用连接刚体间的碰撞 - if disable_collision_between_linked_bodies: - body_a.bullet_body.setIgnoreCollisionCheck(body_b.bullet_body, True) - - # 保存约束信息 - constraint_info = { - 'type': self.CONSTRAINT_FIXED, - 'constraint': constraint, - 'body_a': body_a, - 'body_b': body_b, - 'properties': { - 'frame_a': frame_a, - 'frame_b': frame_b, - 'collision': collision, - 'breaking_threshold': breaking_threshold, - 'disable_collision': disable_collision_between_linked_bodies - }, - 'creation_time': time.time(), - 'is_enabled': True, - 'group': None, - 'tags': set(), - 'feedback': None - } - - self.constraints.append(constraint_info) - return constraint - - def remove_constraint(self, constraint): - """ - 移除约束 - - Args: - constraint: 约束对象 - """ - for constraint_info in self.constraints: - if constraint_info['constraint'] == constraint: - # 重新启用刚体间的碰撞 - if constraint_info['properties'].get('disable_collision', False): - body_a = constraint_info['body_a'] - body_b = constraint_info['body_b'] - body_a.bullet_body.setIgnoreCollisionCheck(body_b.bullet_body, False) - - self.constraints.remove(constraint_info) - return True - return False - - def get_constraint_info(self, constraint): - """ - 获取约束信息 - - Args: - constraint: 约束对象 - - Returns: - dict: 约束信息字典 - """ - for constraint_info in self.constraints: - if constraint_info['constraint'] == constraint: - return constraint_info - return None - - def set_motor(self, constraint, axis, target_velocity, max_impulse): - """ - 设置约束马达 - - Args: - constraint: 约束对象 - axis (int): 轴向 (0=X, 1=Y, 2=Z for linear, 3=X, 4=Y, 5=Z for angular) - target_velocity (float): 目标速度 - max_impulse (float): 最大冲量 - """ - constraint_info = self.get_constraint_info(constraint) - if not constraint_info: - return - - constraint_type = constraint_info['type'] - if constraint_type == self.CONSTRAINT_HINGE: - constraint.setMotorTargetVelocity(target_velocity) - constraint.setMaxMotorImpulse(max_impulse) - constraint.enableAngularMotor(True) - constraint_info['motor_enabled'] = True - elif constraint_type == self.CONSTRAINT_6DOF: - constraint.setTargetVelocity(axis, target_velocity) - constraint.setMaxMotorForce(axis, max_impulse) - constraint.setParam(axis, 1) # 启用马达 - constraint_info['motor_enabled'] = True - elif constraint_type == self.CONSTRAINT_6DOF_SPRING: - constraint.setTargetVelocity(axis, target_velocity) - constraint.setMaxMotorForce(axis, max_impulse) - constraint.setParam(axis, 1) # 启用马达 - constraint_info['motor_enabled'] = True - - def disable_motor(self, constraint): - """ - 禁用约束马达 - - Args: - constraint: 约束对象 - """ - constraint_info = self.get_constraint_info(constraint) - if not constraint_info: - return - - constraint_type = constraint_info['type'] - if constraint_type == self.CONSTRAINT_HINGE: - constraint.enableAngularMotor(False) - constraint_info['motor_enabled'] = False - elif constraint_type == self.CONSTRAINT_6DOF: - # 禁用所有轴的马达 - for axis in range(6): - constraint.setParam(axis, 0) - constraint_info['motor_enabled'] = False - elif constraint_type == self.CONSTRAINT_6DOF_SPRING: - # 禁用所有轴的马达 - for axis in range(6): - constraint.setParam(axis, 0) - constraint_info['motor_enabled'] = False - - def set_spring(self, constraint, axis, stiffness, damping, equilibrium=0.0): - """ - 设置约束弹簧 - - Args: - constraint: 约束对象 - axis (int): 轴向 (0=X, 1=Y, 2=Z for linear, 3=X, 4=Y, 5=Z for angular) - stiffness (float): 刚度 - damping (float): 阻尼 - equilibrium (float): 平衡点 - """ - constraint_info = self.get_constraint_info(constraint) - if not constraint_info: - return - - constraint_type = constraint_info['type'] - if constraint_type == self.CONSTRAINT_6DOF_SPRING: - constraint.setStiffness(axis, stiffness) - constraint.setDamping(axis, damping) - constraint.setEquilibriumPoint(axis, equilibrium) - constraint.enableSpring(axis, True) - constraint_info['spring_enabled'] = True - - def disable_spring(self, constraint, axis): - """ - 禁用约束弹簧 - - Args: - constraint: 约束对象 - axis (int): 轴向 - """ - constraint_info = self.get_constraint_info(constraint) - if not constraint_info: - return - - constraint_type = constraint_info['type'] - if constraint_type == self.CONSTRAINT_6DOF_SPRING: - constraint.enableSpring(axis, False) - constraint_info['spring_enabled'] = False - - def set_limit(self, constraint, axis, lower_limit, upper_limit): - """ - 设置约束限制 - - Args: - constraint: 约束对象 - axis (int): 轴向 - lower_limit (float): 下限 - upper_limit (float): 上限 - """ - constraint_info = self.get_constraint_info(constraint) - if not constraint_info: - return - - constraint_type = constraint_info['type'] - if constraint_type == self.CONSTRAINT_6DOF: - if axis < 3: # 线性轴 - current_lower = constraint.getLinearLowerLimit() - current_upper = constraint.getLinearUpperLimit() - if axis == 0: - current_lower.setX(lower_limit) - current_upper.setX(upper_limit) - elif axis == 1: - current_lower.setY(lower_limit) - current_upper.setY(upper_limit) - elif axis == 2: - current_lower.setZ(lower_limit) - current_upper.setZ(upper_limit) - constraint.setLinearLowerLimit(current_lower) - constraint.setLinearUpperLimit(current_upper) - else: # 角轴 - current_lower = constraint.getAngularLowerLimit() - current_upper = constraint.getAngularUpperLimit() - axis_index = axis - 3 - if axis_index == 0: - current_lower.setX(lower_limit) - current_upper.setX(upper_limit) - elif axis_index == 1: - current_lower.setY(lower_limit) - current_upper.setY(upper_limit) - elif axis_index == 2: - current_lower.setZ(lower_limit) - current_upper.setZ(upper_limit) - constraint.setAngularLowerLimit(current_lower) - constraint.setAngularUpperLimit(current_upper) - - def enable_constraint(self, constraint, enabled=True): - """ - 启用或禁用约束 - - Args: - constraint: 约束对象 - enabled (bool): 是否启用 - """ - constraint_info = self.get_constraint_info(constraint) - if not constraint_info: - return - - constraint_info['is_enabled'] = enabled - - def add_constraint_to_group(self, constraint, group_name): - """ - 将约束添加到组 - - Args: - constraint: 约束对象 - group_name (str): 组名 - """ - constraint_info = self.get_constraint_info(constraint) - if not constraint_info: - return - - constraint_info['group'] = group_name - if group_name not in self.constraint_groups: - self.constraint_groups[group_name] = [] - if constraint not in self.constraint_groups[group_name]: - self.constraint_groups[group_name].append(constraint) - - def remove_constraint_from_group(self, constraint, group_name): - """ - 将约束从组中移除 - - Args: - constraint: 约束对象 - group_name (str): 组名 - """ - constraint_info = self.get_constraint_info(constraint) - if not constraint_info: - return - - constraint_info['group'] = None - if group_name in self.constraint_groups and constraint in self.constraint_groups[group_name]: - self.constraint_groups[group_name].remove(constraint) - - def add_constraint_tag(self, constraint, tag): - """ - 为约束添加标签 - - Args: - constraint: 约束对象 - tag (str): 标签 - """ - constraint_info = self.get_constraint_info(constraint) - if not constraint_info: - return - - constraint_info['tags'].add(tag) - - def remove_constraint_tag(self, constraint, tag): - """ - 为约束移除标签 - - Args: - constraint: 约束对象 - tag (str): 标签 - """ - constraint_info = self.get_constraint_info(constraint) - if not constraint_info: - return - - constraint_info['tags'].discard(tag) - - def get_constraints_by_tag(self, tag): - """ - 根据标签获取约束 - - Args: - tag (str): 标签 - - Returns: - list: 约束列表 - """ - result = [] - for constraint_info in self.constraints: - if tag in constraint_info['tags']: - result.append(constraint_info['constraint']) - return result - - def get_constraints_by_group(self, group_name): - """ - 根据组名获取约束 - - Args: - group_name (str): 组名 - - Returns: - list: 约束列表 - """ - return self.constraint_groups.get(group_name, []) - - def get_constraint_state(self, constraint): - """ - 获取约束状态 - - Args: - constraint: 约束对象 - - Returns: - dict: 约束状态 - """ - constraint_info = self.get_constraint_info(constraint) - if not constraint_info: - return None - - state = { - 'type': constraint_info['type'], - 'is_enabled': constraint_info['is_enabled'], - 'group': constraint_info['group'], - 'tags': list(constraint_info['tags']), - 'creation_time': constraint_info['creation_time'], - 'age': time.time() - constraint_info['creation_time'] - } - - # 添加特定于约束类型的状态信息 - if constraint_info['type'] == self.CONSTRAINT_HINGE: - state['current_angle'] = constraint.getHingeAngle() - state['motor_enabled'] = constraint_info.get('motor_enabled', False) - elif constraint_info['type'] == self.CONSTRAINT_6DOF: - state['motor_enabled'] = constraint_info.get('motor_enabled', False) - state['spring_enabled'] = constraint_info.get('spring_enabled', False) - elif constraint_info['type'] == self.CONSTRAINT_6DOF_SPRING: - state['motor_enabled'] = constraint_info.get('motor_enabled', False) - state['spring_enabled'] = constraint_info.get('spring_enabled', False) - - return state - - def clear_all_constraints(self): - """ - 清除所有约束 - """ - # 重新启用所有刚体间的碰撞 - for constraint_info in self.constraints: - if constraint_info['properties'].get('disable_collision', False): - body_a = constraint_info['body_a'] - body_b = constraint_info['body_b'] - body_a.bullet_body.setIgnoreCollisionCheck(body_b.bullet_body, False) - - self.constraints.clear() - self.constraint_groups.clear() - self.constraint_limits.clear() - self.constraint_motors.clear() - self.constraint_springs.clear() - - print("所有约束已清除") - - def update_constraint_feedback(self, time_delta): - """ - 更新约束反馈信息 - - Args: - time_delta (float): 时间增量 - """ - for constraint_info in self.constraints: - constraint = constraint_info['constraint'] - constraint_type = constraint_info['type'] - - # 更新约束反馈信息 - if constraint_type == self.CONSTRAINT_HINGE: - constraint_info['current_angle'] = constraint.getHingeAngle() - # 可以添加更多反馈信息 - elif constraint_type == self.CONSTRAINT_SLIDER: - constraint_info['current_linear_position'] = constraint.getLinearPos() - constraint_info['current_angular_position'] = constraint.getAngularPos() - - def get_constraint_statistics(self): - """ - 获取约束统计信息 - - Returns: - dict: 统计信息 - """ - type_counts = {} - group_counts = {} - tag_counts = {} - - for constraint_info in self.constraints: - # 统计类型 - constraint_type = constraint_info['type'] - type_counts[constraint_type] = type_counts.get(constraint_type, 0) + 1 - - # 统计组 - group = constraint_info['group'] - if group: - group_counts[group] = group_counts.get(group, 0) + 1 - - # 统计标签 - for tag in constraint_info['tags']: - tag_counts[tag] = tag_counts.get(tag, 0) + 1 - - return { - 'total_constraints': len(self.constraints), - 'type_distribution': type_counts, - 'group_distribution': group_counts, - 'tag_distribution': tag_counts, - 'groups': list(self.constraint_groups.keys()) +""" +约束和关节系统模块 +负责各种物理约束和关节的创建和管理 +""" + +from panda3d.core import Point3, Vec3, TransformState, LVector3f +from panda3d.bullet import BulletConstraint +from panda3d.bullet import BulletPoint2PointConstraint, BulletHingeConstraint +from panda3d.bullet import BulletSliderConstraint, BulletConeTwistConstraint +from panda3d.bullet import BulletGeneric6DofConstraint, BulletGeneric6DofSpringConstraint +from panda3d.bullet import BulletGearConstraint, BulletFixedConstraint +import math +import time +from collections import deque + + +class ConstraintManager: + """ + 约束管理器类 + 管理各种类型的物理约束和关节 + """ + + CONSTRAINT_POINT2POINT = 'point2point' + CONSTRAINT_HINGE = 'hinge' + CONSTRAINT_SLIDER = 'slider' + CONSTRAINT_CONETWIST = 'conetwist' + CONSTRAINT_6DOF = '6dof' + CONSTRAINT_6DOF_SPRING = '6dof_spring' + CONSTRAINT_GEAR = 'gear' + CONSTRAINT_FIXED = 'fixed' + + def __init__(self): + """ + 初始化约束管理器 + """ + self.constraints = [] + self.constraint_groups = {} # 约束组 + self.constraint_limits = {} # 约束限制 + self.constraint_motors = {} # 约束马达 + self.constraint_springs = {} # 约束弹簧 + + print("约束管理器初始化完成") + + def create_point2point_constraint(self, body_a, body_b, pivot_a, pivot_b, + collision=True, breaking_threshold=0.0, + disable_collision_between_linked_bodies=True): + """ + 创建点对点约束 + + Args: + body_a (RigidBody): 第一个刚体 + body_b (RigidBody): 第二个刚体 + pivot_a (Point3): 第一个刚体上的连接点 + pivot_b (Point3): 第二个刚体上的连接点 + collision (bool): 是否允许两个刚体碰撞 + breaking_threshold (float): 破裂阈值 + disable_collision_between_linked_bodies (bool): 是否禁用连接刚体间的碰撞 + + Returns: + BulletPoint2PointConstraint: 点对点约束对象 + """ + constraint = BulletPoint2PointConstraint( + body_a.bullet_body, body_b.bullet_body, pivot_a, pivot_b) + + # 设置约束属性 + constraint.setOverrideEnabled(not collision) + if breaking_threshold > 0: + constraint.setBreakingImpulseThreshold(breaking_threshold) + + # 禁用连接刚体间的碰撞 + if disable_collision_between_linked_bodies: + body_a.bullet_body.setIgnoreCollisionCheck(body_b.bullet_body, True) + + # 保存约束信息 + constraint_info = { + 'type': self.CONSTRAINT_POINT2POINT, + 'constraint': constraint, + 'body_a': body_a, + 'body_b': body_b, + 'properties': { + 'pivot_a': pivot_a, + 'pivot_b': pivot_b, + 'collision': collision, + 'breaking_threshold': breaking_threshold, + 'disable_collision': disable_collision_between_linked_bodies + }, + 'creation_time': time.time(), + 'is_enabled': True, + 'group': None, + 'tags': set(), + 'feedback': None + } + + self.constraints.append(constraint_info) + return constraint + + def create_hinge_constraint(self, body_a, body_b, pivot_a, pivot_b, axis_a, axis_b, + collision=True, breaking_threshold=0.0, limits=None, + motor=None, disable_collision_between_linked_bodies=True): + """ + 创建铰链约束 + + Args: + body_a (RigidBody): 第一个刚体 + body_b (RigidBody): 第二个刚体 + pivot_a (Point3): 第一个刚体上的连接点 + pivot_b (Point3): 第二个刚体上的连接点 + axis_a (Vec3): 第一个刚体上的轴向 + axis_b (Vec3): 第二个刚体上的轴向 + collision (bool): 是否允许两个刚体碰撞 + breaking_threshold (float): 破裂阈值 + limits (tuple): 旋转限制 (min_angle, max_angle) 或 None + motor (dict): 马达参数 {'target_velocity': float, 'max_impulse': float} + disable_collision_between_linked_bodies (bool): 是否禁用连接刚体间的碰撞 + + Returns: + BulletHingeConstraint: 铰链约束对象 + """ + constraint = BulletHingeConstraint( + body_a.bullet_body, body_b.bullet_body, pivot_a, pivot_b, axis_a, axis_b) + + # 设置约束属性 + constraint.setOverrideEnabled(not collision) + if breaking_threshold > 0: + constraint.setBreakingImpulseThreshold(breaking_threshold) + + # 设置旋转限制 + if limits: + min_angle, max_angle = limits + constraint.setLimit(min_angle, max_angle) + + # 设置马达 + if motor: + target_velocity = motor.get('target_velocity', 0.0) + max_impulse = motor.get('max_impulse', 1.0) + constraint.enableAngularMotor(True, target_velocity, max_impulse) + + # 禁用连接刚体间的碰撞 + if disable_collision_between_linked_bodies: + body_a.bullet_body.setIgnoreCollisionCheck(body_b.bullet_body, True) + + # 保存约束信息 + constraint_info = { + 'type': self.CONSTRAINT_HINGE, + 'constraint': constraint, + 'body_a': body_a, + 'body_b': body_b, + 'properties': { + 'pivot_a': pivot_a, + 'pivot_b': pivot_b, + 'axis_a': axis_a, + 'axis_b': axis_b, + 'collision': collision, + 'breaking_threshold': breaking_threshold, + 'limits': limits, + 'motor': motor, + 'disable_collision': disable_collision_between_linked_bodies + }, + 'creation_time': time.time(), + 'is_enabled': True, + 'group': None, + 'tags': set(), + 'feedback': None, + 'motor_enabled': motor is not None, + 'current_angle': 0.0, + 'angular_velocity': 0.0 + } + + self.constraints.append(constraint_info) + return constraint + + def create_slider_constraint(self, body_a, body_b, frame_a=None, frame_b=None, + collision=True, breaking_threshold=0.0, limits=None, + motor=None, disable_collision_between_linked_bodies=True): + """ + 创建滑动约束 + + Args: + body_a (RigidBody): 第一个刚体 + body_b (RigidBody): 第二个刚体 + frame_a (TransformState): 第一个刚体上的参考帧 + frame_b (TransformState): 第二个刚体上的参考帧 + collision (bool): 是否允许两个刚体碰撞 + breaking_threshold (float): 破裂阈值 + limits (tuple): 滑动限制 (linear_min, linear_max, angular_min, angular_max) 或 None + motor (dict): 马达参数 {'target_linear_velocity': float, 'max_linear_force': float, + 'target_angular_velocity': float, 'max_angular_force': float} + disable_collision_between_linked_bodies (bool): 是否禁用连接刚体间的碰撞 + + Returns: + BulletSliderConstraint: 滑动约束对象 + """ + # 如果没有提供参考帧,创建默认的 + if frame_a is None: + frame_a = TransformState.makePos(Point3(0, 0, 0)) + if frame_b is None: + frame_b = TransformState.makePos(Point3(0, 0, 0)) + + constraint = BulletSliderConstraint( + body_a.bullet_body, body_b.bullet_body, frame_a, frame_b, useLinearReferenceFrameA=True) + + # 设置约束属性 + constraint.setOverrideEnabled(not collision) + if breaking_threshold > 0: + constraint.setBreakingImpulseThreshold(breaking_threshold) + + # 设置滑动限制 + if limits: + linear_min, linear_max, angular_min, angular_max = limits + constraint.setLowerLinLimit(linear_min) + constraint.setUpperLinLimit(linear_max) + constraint.setLowerAngLimit(angular_min) + constraint.setUpperAngLimit(angular_max) + + # 设置马达 + if motor: + # 滑动约束的马达设置比较复杂,需要分别设置线性和角马达 + pass + + # 禁用连接刚体间的碰撞 + if disable_collision_between_linked_bodies: + body_a.bullet_body.setIgnoreCollisionCheck(body_b.bullet_body, True) + + # 保存约束信息 + constraint_info = { + 'type': self.CONSTRAINT_SLIDER, + 'constraint': constraint, + 'body_a': body_a, + 'body_b': body_b, + 'properties': { + 'frame_a': frame_a, + 'frame_b': frame_b, + 'collision': collision, + 'breaking_threshold': breaking_threshold, + 'limits': limits, + 'motor': motor, + 'disable_collision': disable_collision_between_linked_bodies + }, + 'creation_time': time.time(), + 'is_enabled': True, + 'group': None, + 'tags': set(), + 'feedback': None, + 'motor_enabled': motor is not None, + 'current_linear_position': 0.0, + 'current_angular_position': 0.0 + } + + self.constraints.append(constraint_info) + return constraint + + def create_cone_twist_constraint(self, body_a, body_b, frame_a=None, frame_b=None, + collision=True, breaking_threshold=0.0, limits=None, + motor=None, disable_collision_between_linked_bodies=True): + """ + 创建锥形扭转约束 + + Args: + body_a (RigidBody): 第一个刚体 + body_b (RigidBody): 第二个刚体 + frame_a (TransformState): 第一个刚体上的参考帧 + frame_b (TransformState): 第二个刚体上的参考帧 + collision (bool): 是否允许两个刚体碰撞 + breaking_threshold (float): 破裂阈值 + limits (tuple): 限制 (swing_span1, swing_span2, twist_span) 或 None + motor (dict): 马达参数 + disable_collision_between_linked_bodies (bool): 是否禁用连接刚体间的碰撞 + + Returns: + BulletConeTwistConstraint: 锥形扭转约束对象 + """ + # 如果没有提供参考帧,创建默认的 + if frame_a is None: + frame_a = TransformState.makePos(Point3(0, 0, 0)) + if frame_b is None: + frame_b = TransformState.makePos(Point3(0, 0, 0)) + + constraint = BulletConeTwistConstraint( + body_a.bullet_body, body_b.bullet_body, frame_a, frame_b) + + # 设置约束属性 + constraint.setOverrideEnabled(not collision) + if breaking_threshold > 0: + constraint.setBreakingImpulseThreshold(breaking_threshold) + + # 设置限制 + if limits: + swing_span1, swing_span2, twist_span = limits + constraint.setLimit(swing_span1, swing_span2, twist_span) + + # 禁用连接刚体间的碰撞 + if disable_collision_between_linked_bodies: + body_a.bullet_body.setIgnoreCollisionCheck(body_b.bullet_body, True) + + # 保存约束信息 + constraint_info = { + 'type': self.CONSTRAINT_CONETWIST, + 'constraint': constraint, + 'body_a': body_a, + 'body_b': body_b, + 'properties': { + 'frame_a': frame_a, + 'frame_b': frame_b, + 'collision': collision, + 'breaking_threshold': breaking_threshold, + 'limits': limits, + 'motor': motor, + 'disable_collision': disable_collision_between_linked_bodies + }, + 'creation_time': time.time(), + 'is_enabled': True, + 'group': None, + 'tags': set(), + 'feedback': None + } + + self.constraints.append(constraint_info) + return constraint + + def create_6dof_constraint(self, body_a, body_b, frame_a=None, frame_b=None, + collision=True, breaking_threshold=0.0, linear_limits=None, + angular_limits=None, motors=None, springs=None, + disable_collision_between_linked_bodies=True): + """ + 创建6自由度约束 + + Args: + body_a (RigidBody): 第一个刚体 + body_b (RigidBody): 第二个刚体 + frame_a (TransformState): 第一个刚体上的参考帧 + frame_b (TransformState): 第二个刚体上的参考帧 + collision (bool): 是否允许两个刚体碰撞 + breaking_threshold (float): 破裂阈值 + linear_limits (tuple): 线性限制 (x_min, x_max, y_min, y_max, z_min, z_max) 或 None + angular_limits (tuple): 角度限制 (x_min, x_max, y_min, y_max, z_min, z_max) 或 None + motors (dict): 马达参数 {'linear': {axis: {'target_velocity', 'max_force'}}, + 'angular': {axis: {'target_velocity', 'max_force'}}} + springs (dict): 弹簧参数 {'linear': {axis: {'stiffness', 'damping', 'equilibrium'}}, + 'angular': {axis: {'stiffness', 'damping', 'equilibrium'}}} + disable_collision_between_linked_bodies (bool): 是否禁用连接刚体间的碰撞 + + Returns: + BulletGeneric6DofConstraint: 6自由度约束对象 + """ + # 如果没有提供参考帧,创建默认的 + if frame_a is None: + frame_a = TransformState.makePos(Point3(0, 0, 0)) + if frame_b is None: + frame_b = TransformState.makePos(Point3(0, 0, 0)) + + constraint = BulletGeneric6DofConstraint( + body_a.bullet_body, body_b.bullet_body, frame_a, frame_b, useLinearReferenceFrameA=True) + + # 设置约束属性 + constraint.setOverrideEnabled(not collision) + if breaking_threshold > 0: + constraint.setBreakingImpulseThreshold(breaking_threshold) + + # 设置线性限制 + if linear_limits: + x_min, x_max, y_min, y_max, z_min, z_max = linear_limits + constraint.setLinearLowerLimit(Vec3(x_min, y_min, z_min)) + constraint.setLinearUpperLimit(Vec3(x_max, y_max, z_max)) + + # 设置角度限制 + if angular_limits: + x_min, x_max, y_min, y_max, z_min, z_max = angular_limits + constraint.setAngularLowerLimit(Vec3(x_min, y_min, z_min)) + constraint.setAngularUpperLimit(Vec3(x_max, y_max, z_max)) + + # 设置马达 + if motors: + linear_motors = motors.get('linear', {}) + angular_motors = motors.get('angular', {}) + + for axis in range(3): + # 线性马达 + if axis in linear_motors: + motor_info = linear_motors[axis] + constraint.setTargetVelocity(axis, motor_info.get('target_velocity', 0.0)) + constraint.setMaxMotorForce(axis, motor_info.get('max_force', 1.0)) + constraint.setParam(axis, 1) # 启用马达 + + # 角马达 + if axis in angular_motors: + motor_info = angular_motors[axis] + constraint.setTargetVelocity(axis + 3, motor_info.get('target_velocity', 0.0)) + constraint.setMaxMotorForce(axis + 3, motor_info.get('max_force', 1.0)) + constraint.setParam(axis + 3, 1) # 启用马达 + + # 禁用连接刚体间的碰撞 + if disable_collision_between_linked_bodies: + body_a.bullet_body.setIgnoreCollisionCheck(body_b.bullet_body, True) + + # 保存约束信息 + constraint_info = { + 'type': self.CONSTRAINT_6DOF, + 'constraint': constraint, + 'body_a': body_a, + 'body_b': body_b, + 'properties': { + 'frame_a': frame_a, + 'frame_b': frame_b, + 'collision': collision, + 'breaking_threshold': breaking_threshold, + 'linear_limits': linear_limits, + 'angular_limits': angular_limits, + 'motors': motors, + 'springs': springs, + 'disable_collision': disable_collision_between_linked_bodies + }, + 'creation_time': time.time(), + 'is_enabled': True, + 'group': None, + 'tags': set(), + 'feedback': None, + 'motor_enabled': motors is not None, + 'spring_enabled': springs is not None + } + + self.constraints.append(constraint_info) + return constraint + + def create_6dof_spring_constraint(self, body_a, body_b, frame_a=None, frame_b=None, + collision=True, breaking_threshold=0.0, linear_limits=None, + angular_limits=None, springs=None, motors=None, + disable_collision_between_linked_bodies=True): + """ + 创建6自由度弹簧约束 + + Args: + body_a (RigidBody): 第一个刚体 + body_b (RigidBody): 第二个刚体 + frame_a (TransformState): 第一个刚体上的参考帧 + frame_b (TransformState): 第二个刚体上的参考帧 + collision (bool): 是否允许两个刚体碰撞 + breaking_threshold (float): 破裂阈值 + linear_limits (tuple): 线性限制 (x_min, x_max, y_min, y_max, z_min, z_max) 或 None + angular_limits (tuple): 角度限制 (x_min, x_max, y_min, y_max, z_min, z_max) 或 None + springs (dict): 弹簧参数 {'linear': {axis: {'stiffness', 'damping', 'equilibrium'}}, + 'angular': {axis: {'stiffness', 'damping', 'equilibrium'}}} + motors (dict): 马达参数 + disable_collision_between_linked_bodies (bool): 是否禁用连接刚体间的碰撞 + + Returns: + BulletGeneric6DofSpringConstraint: 6自由度弹簧约束对象 + """ + # 如果没有提供参考帧,创建默认的 + if frame_a is None: + frame_a = TransformState.makePos(Point3(0, 0, 0)) + if frame_b is None: + frame_b = TransformState.makePos(Point3(0, 0, 0)) + + constraint = BulletGeneric6DofSpringConstraint( + body_a.bullet_body, body_b.bullet_body, frame_a, frame_b, useLinearReferenceFrameA=True) + + # 设置约束属性 + constraint.setOverrideEnabled(not collision) + if breaking_threshold > 0: + constraint.setBreakingImpulseThreshold(breaking_threshold) + + # 设置线性限制 + if linear_limits: + x_min, x_max, y_min, y_max, z_min, z_max = linear_limits + constraint.setLinearLowerLimit(Vec3(x_min, y_min, z_min)) + constraint.setLinearUpperLimit(Vec3(x_max, y_max, z_max)) + + # 设置角度限制 + if angular_limits: + x_min, x_max, y_min, y_max, z_min, z_max = angular_limits + constraint.setAngularLowerLimit(Vec3(x_min, y_min, z_min)) + constraint.setAngularUpperLimit(Vec3(x_max, y_max, z_max)) + + # 设置弹簧 + if springs: + linear_springs = springs.get('linear', {}) + angular_springs = springs.get('angular', {}) + + for axis in range(3): + # 线性弹簧 + if axis in linear_springs: + spring_info = linear_springs[axis] + constraint.setStiffness(axis, spring_info.get('stiffness', 0.0)) + constraint.setDamping(axis, spring_info.get('damping', 0.0)) + constraint.setEquilibriumPoint(axis, spring_info.get('equilibrium', 0.0)) + constraint.enableSpring(axis, True) + + # 角弹簧 + if axis in angular_springs: + spring_info = angular_springs[axis] + constraint.setStiffness(axis + 3, spring_info.get('stiffness', 0.0)) + constraint.setDamping(axis + 3, spring_info.get('damping', 0.0)) + constraint.setEquilibriumPoint(axis + 3, spring_info.get('equilibrium', 0.0)) + constraint.enableSpring(axis + 3, True) + + # 禁用连接刚体间的碰撞 + if disable_collision_between_linked_bodies: + body_a.bullet_body.setIgnoreCollisionCheck(body_b.bullet_body, True) + + # 保存约束信息 + constraint_info = { + 'type': self.CONSTRAINT_6DOF_SPRING, + 'constraint': constraint, + 'body_a': body_a, + 'body_b': body_b, + 'properties': { + 'frame_a': frame_a, + 'frame_b': frame_b, + 'collision': collision, + 'breaking_threshold': breaking_threshold, + 'linear_limits': linear_limits, + 'angular_limits': angular_limits, + 'springs': springs, + 'motors': motors, + 'disable_collision': disable_collision_between_linked_bodies + }, + 'creation_time': time.time(), + 'is_enabled': True, + 'group': None, + 'tags': set(), + 'feedback': None, + 'spring_enabled': springs is not None, + 'motor_enabled': motors is not None + } + + self.constraints.append(constraint_info) + return constraint + + def create_gear_constraint(self, body_a, body_b, axis_a, axis_b, ratio=1.0, + collision=True, breaking_threshold=0.0): + """ + 创建齿轮约束 + + Args: + body_a (RigidBody): 第一个刚体 + body_b (RigidBody): 第二个刚体 + axis_a (Vec3): 第一个刚体上的轴向 + axis_b (Vec3): 第二个刚体上的轴向 + ratio (float): 齿轮比 + collision (bool): 是否允许两个刚体碰撞 + breaking_threshold (float): 破裂阈值 + + Returns: + BulletGearConstraint: 齿轮约束对象 + """ + constraint = BulletGearConstraint( + body_a.bullet_body, body_b.bullet_body, axis_a, axis_b, ratio) + + # 设置约束属性 + constraint.setOverrideEnabled(not collision) + if breaking_threshold > 0: + constraint.setBreakingImpulseThreshold(breaking_threshold) + + # 保存约束信息 + constraint_info = { + 'type': self.CONSTRAINT_GEAR, + 'constraint': constraint, + 'body_a': body_a, + 'body_b': body_b, + 'properties': { + 'axis_a': axis_a, + 'axis_b': axis_b, + 'ratio': ratio, + 'collision': collision, + 'breaking_threshold': breaking_threshold + }, + 'creation_time': time.time(), + 'is_enabled': True, + 'group': None, + 'tags': set(), + 'feedback': None + } + + self.constraints.append(constraint_info) + return constraint + + def create_fixed_constraint(self, body_a, body_b, frame_a=None, frame_b=None, + collision=True, breaking_threshold=0.0, + disable_collision_between_linked_bodies=True): + """ + 创建固定约束 + + Args: + body_a (RigidBody): 第一个刚体 + body_b (RigidBody): 第二个刚体 + frame_a (TransformState): 第一个刚体上的参考帧 + frame_b (TransformState): 第二个刚体上的参考帧 + collision (bool): 是否允许两个刚体碰撞 + breaking_threshold (float): 破裂阈值 + disable_collision_between_linked_bodies (bool): 是否禁用连接刚体间的碰撞 + + Returns: + BulletFixedConstraint: 固定约束对象 + """ + # 如果没有提供参考帧,创建默认的 + if frame_a is None: + frame_a = TransformState.makePos(Point3(0, 0, 0)) + if frame_b is None: + frame_b = TransformState.makePos(Point3(0, 0, 0)) + + constraint = BulletFixedConstraint( + body_a.bullet_body, body_b.bullet_body, frame_a, frame_b) + + # 设置约束属性 + constraint.setOverrideEnabled(not collision) + if breaking_threshold > 0: + constraint.setBreakingImpulseThreshold(breaking_threshold) + + # 禁用连接刚体间的碰撞 + if disable_collision_between_linked_bodies: + body_a.bullet_body.setIgnoreCollisionCheck(body_b.bullet_body, True) + + # 保存约束信息 + constraint_info = { + 'type': self.CONSTRAINT_FIXED, + 'constraint': constraint, + 'body_a': body_a, + 'body_b': body_b, + 'properties': { + 'frame_a': frame_a, + 'frame_b': frame_b, + 'collision': collision, + 'breaking_threshold': breaking_threshold, + 'disable_collision': disable_collision_between_linked_bodies + }, + 'creation_time': time.time(), + 'is_enabled': True, + 'group': None, + 'tags': set(), + 'feedback': None + } + + self.constraints.append(constraint_info) + return constraint + + def remove_constraint(self, constraint): + """ + 移除约束 + + Args: + constraint: 约束对象 + """ + for constraint_info in self.constraints: + if constraint_info['constraint'] == constraint: + # 重新启用刚体间的碰撞 + if constraint_info['properties'].get('disable_collision', False): + body_a = constraint_info['body_a'] + body_b = constraint_info['body_b'] + body_a.bullet_body.setIgnoreCollisionCheck(body_b.bullet_body, False) + + self.constraints.remove(constraint_info) + return True + return False + + def get_constraint_info(self, constraint): + """ + 获取约束信息 + + Args: + constraint: 约束对象 + + Returns: + dict: 约束信息字典 + """ + for constraint_info in self.constraints: + if constraint_info['constraint'] == constraint: + return constraint_info + return None + + def set_motor(self, constraint, axis, target_velocity, max_impulse): + """ + 设置约束马达 + + Args: + constraint: 约束对象 + axis (int): 轴向 (0=X, 1=Y, 2=Z for linear, 3=X, 4=Y, 5=Z for angular) + target_velocity (float): 目标速度 + max_impulse (float): 最大冲量 + """ + constraint_info = self.get_constraint_info(constraint) + if not constraint_info: + return + + constraint_type = constraint_info['type'] + if constraint_type == self.CONSTRAINT_HINGE: + constraint.setMotorTargetVelocity(target_velocity) + constraint.setMaxMotorImpulse(max_impulse) + constraint.enableAngularMotor(True) + constraint_info['motor_enabled'] = True + elif constraint_type == self.CONSTRAINT_6DOF: + constraint.setTargetVelocity(axis, target_velocity) + constraint.setMaxMotorForce(axis, max_impulse) + constraint.setParam(axis, 1) # 启用马达 + constraint_info['motor_enabled'] = True + elif constraint_type == self.CONSTRAINT_6DOF_SPRING: + constraint.setTargetVelocity(axis, target_velocity) + constraint.setMaxMotorForce(axis, max_impulse) + constraint.setParam(axis, 1) # 启用马达 + constraint_info['motor_enabled'] = True + + def disable_motor(self, constraint): + """ + 禁用约束马达 + + Args: + constraint: 约束对象 + """ + constraint_info = self.get_constraint_info(constraint) + if not constraint_info: + return + + constraint_type = constraint_info['type'] + if constraint_type == self.CONSTRAINT_HINGE: + constraint.enableAngularMotor(False) + constraint_info['motor_enabled'] = False + elif constraint_type == self.CONSTRAINT_6DOF: + # 禁用所有轴的马达 + for axis in range(6): + constraint.setParam(axis, 0) + constraint_info['motor_enabled'] = False + elif constraint_type == self.CONSTRAINT_6DOF_SPRING: + # 禁用所有轴的马达 + for axis in range(6): + constraint.setParam(axis, 0) + constraint_info['motor_enabled'] = False + + def set_spring(self, constraint, axis, stiffness, damping, equilibrium=0.0): + """ + 设置约束弹簧 + + Args: + constraint: 约束对象 + axis (int): 轴向 (0=X, 1=Y, 2=Z for linear, 3=X, 4=Y, 5=Z for angular) + stiffness (float): 刚度 + damping (float): 阻尼 + equilibrium (float): 平衡点 + """ + constraint_info = self.get_constraint_info(constraint) + if not constraint_info: + return + + constraint_type = constraint_info['type'] + if constraint_type == self.CONSTRAINT_6DOF_SPRING: + constraint.setStiffness(axis, stiffness) + constraint.setDamping(axis, damping) + constraint.setEquilibriumPoint(axis, equilibrium) + constraint.enableSpring(axis, True) + constraint_info['spring_enabled'] = True + + def disable_spring(self, constraint, axis): + """ + 禁用约束弹簧 + + Args: + constraint: 约束对象 + axis (int): 轴向 + """ + constraint_info = self.get_constraint_info(constraint) + if not constraint_info: + return + + constraint_type = constraint_info['type'] + if constraint_type == self.CONSTRAINT_6DOF_SPRING: + constraint.enableSpring(axis, False) + constraint_info['spring_enabled'] = False + + def set_limit(self, constraint, axis, lower_limit, upper_limit): + """ + 设置约束限制 + + Args: + constraint: 约束对象 + axis (int): 轴向 + lower_limit (float): 下限 + upper_limit (float): 上限 + """ + constraint_info = self.get_constraint_info(constraint) + if not constraint_info: + return + + constraint_type = constraint_info['type'] + if constraint_type == self.CONSTRAINT_6DOF: + if axis < 3: # 线性轴 + current_lower = constraint.getLinearLowerLimit() + current_upper = constraint.getLinearUpperLimit() + if axis == 0: + current_lower.setX(lower_limit) + current_upper.setX(upper_limit) + elif axis == 1: + current_lower.setY(lower_limit) + current_upper.setY(upper_limit) + elif axis == 2: + current_lower.setZ(lower_limit) + current_upper.setZ(upper_limit) + constraint.setLinearLowerLimit(current_lower) + constraint.setLinearUpperLimit(current_upper) + else: # 角轴 + current_lower = constraint.getAngularLowerLimit() + current_upper = constraint.getAngularUpperLimit() + axis_index = axis - 3 + if axis_index == 0: + current_lower.setX(lower_limit) + current_upper.setX(upper_limit) + elif axis_index == 1: + current_lower.setY(lower_limit) + current_upper.setY(upper_limit) + elif axis_index == 2: + current_lower.setZ(lower_limit) + current_upper.setZ(upper_limit) + constraint.setAngularLowerLimit(current_lower) + constraint.setAngularUpperLimit(current_upper) + + def enable_constraint(self, constraint, enabled=True): + """ + 启用或禁用约束 + + Args: + constraint: 约束对象 + enabled (bool): 是否启用 + """ + constraint_info = self.get_constraint_info(constraint) + if not constraint_info: + return + + constraint_info['is_enabled'] = enabled + + def add_constraint_to_group(self, constraint, group_name): + """ + 将约束添加到组 + + Args: + constraint: 约束对象 + group_name (str): 组名 + """ + constraint_info = self.get_constraint_info(constraint) + if not constraint_info: + return + + constraint_info['group'] = group_name + if group_name not in self.constraint_groups: + self.constraint_groups[group_name] = [] + if constraint not in self.constraint_groups[group_name]: + self.constraint_groups[group_name].append(constraint) + + def remove_constraint_from_group(self, constraint, group_name): + """ + 将约束从组中移除 + + Args: + constraint: 约束对象 + group_name (str): 组名 + """ + constraint_info = self.get_constraint_info(constraint) + if not constraint_info: + return + + constraint_info['group'] = None + if group_name in self.constraint_groups and constraint in self.constraint_groups[group_name]: + self.constraint_groups[group_name].remove(constraint) + + def add_constraint_tag(self, constraint, tag): + """ + 为约束添加标签 + + Args: + constraint: 约束对象 + tag (str): 标签 + """ + constraint_info = self.get_constraint_info(constraint) + if not constraint_info: + return + + constraint_info['tags'].add(tag) + + def remove_constraint_tag(self, constraint, tag): + """ + 为约束移除标签 + + Args: + constraint: 约束对象 + tag (str): 标签 + """ + constraint_info = self.get_constraint_info(constraint) + if not constraint_info: + return + + constraint_info['tags'].discard(tag) + + def get_constraints_by_tag(self, tag): + """ + 根据标签获取约束 + + Args: + tag (str): 标签 + + Returns: + list: 约束列表 + """ + result = [] + for constraint_info in self.constraints: + if tag in constraint_info['tags']: + result.append(constraint_info['constraint']) + return result + + def get_constraints_by_group(self, group_name): + """ + 根据组名获取约束 + + Args: + group_name (str): 组名 + + Returns: + list: 约束列表 + """ + return self.constraint_groups.get(group_name, []) + + def get_constraint_state(self, constraint): + """ + 获取约束状态 + + Args: + constraint: 约束对象 + + Returns: + dict: 约束状态 + """ + constraint_info = self.get_constraint_info(constraint) + if not constraint_info: + return None + + state = { + 'type': constraint_info['type'], + 'is_enabled': constraint_info['is_enabled'], + 'group': constraint_info['group'], + 'tags': list(constraint_info['tags']), + 'creation_time': constraint_info['creation_time'], + 'age': time.time() - constraint_info['creation_time'] + } + + # 添加特定于约束类型的状态信息 + if constraint_info['type'] == self.CONSTRAINT_HINGE: + state['current_angle'] = constraint.getHingeAngle() + state['motor_enabled'] = constraint_info.get('motor_enabled', False) + elif constraint_info['type'] == self.CONSTRAINT_6DOF: + state['motor_enabled'] = constraint_info.get('motor_enabled', False) + state['spring_enabled'] = constraint_info.get('spring_enabled', False) + elif constraint_info['type'] == self.CONSTRAINT_6DOF_SPRING: + state['motor_enabled'] = constraint_info.get('motor_enabled', False) + state['spring_enabled'] = constraint_info.get('spring_enabled', False) + + return state + + def clear_all_constraints(self): + """ + 清除所有约束 + """ + # 重新启用所有刚体间的碰撞 + for constraint_info in self.constraints: + if constraint_info['properties'].get('disable_collision', False): + body_a = constraint_info['body_a'] + body_b = constraint_info['body_b'] + body_a.bullet_body.setIgnoreCollisionCheck(body_b.bullet_body, False) + + self.constraints.clear() + self.constraint_groups.clear() + self.constraint_limits.clear() + self.constraint_motors.clear() + self.constraint_springs.clear() + + print("所有约束已清除") + + def update_constraint_feedback(self, time_delta): + """ + 更新约束反馈信息 + + Args: + time_delta (float): 时间增量 + """ + for constraint_info in self.constraints: + constraint = constraint_info['constraint'] + constraint_type = constraint_info['type'] + + # 更新约束反馈信息 + if constraint_type == self.CONSTRAINT_HINGE: + constraint_info['current_angle'] = constraint.getHingeAngle() + # 可以添加更多反馈信息 + elif constraint_type == self.CONSTRAINT_SLIDER: + constraint_info['current_linear_position'] = constraint.getLinearPos() + constraint_info['current_angular_position'] = constraint.getAngularPos() + + def get_constraint_statistics(self): + """ + 获取约束统计信息 + + Returns: + dict: 统计信息 + """ + type_counts = {} + group_counts = {} + tag_counts = {} + + for constraint_info in self.constraints: + # 统计类型 + constraint_type = constraint_info['type'] + type_counts[constraint_type] = type_counts.get(constraint_type, 0) + 1 + + # 统计组 + group = constraint_info['group'] + if group: + group_counts[group] = group_counts.get(group, 0) + 1 + + # 统计标签 + for tag in constraint_info['tags']: + tag_counts[tag] = tag_counts.get(tag, 0) + 1 + + return { + 'total_constraints': len(self.constraints), + 'type_distribution': type_counts, + 'group_distribution': group_counts, + 'tag_distribution': tag_counts, + 'groups': list(self.constraint_groups.keys()) } \ No newline at end of file diff --git a/plugins/user/rigid_body_physics/core/material.py b/plugins/user/rigid_body_physics/core/material.py index a311e336..155af57a 100644 --- a/plugins/user/rigid_body_physics/core/material.py +++ b/plugins/user/rigid_body_physics/core/material.py @@ -1,1963 +1,1963 @@ -""" -物理材质系统模块 -负责物理材质的定义和管理 -""" - -from panda3d.core import Vec3, Vec4 -import json -import math -import time -from collections import defaultdict - - -class PhysicsMaterial: - """ - 物理材质类 - 定义物体的物理表面属性 - """ - - # 预定义材质类型 - TYPE_DEFAULT = 'default' - TYPE_METAL = 'metal' - TYPE_WOOD = 'wood' - TYPE_CONCRETE = 'concrete' - TYPE_RUBBER = 'rubber' - TYPE_ICE = 'ice' - TYPE_SAND = 'sand' - TYPE_WATER = 'water' - TYPE_GLASS = 'glass' - TYPE_FLESH = 'flesh' - TYPE_PLASTIC = 'plastic' - TYPE_CLOTH = 'cloth' - TYPE_LEATHER = 'leather' - TYPE_STONE = 'stone' - TYPE_SNOW = 'snow' - TYPE_MUD = 'mud' - - def __init__(self, name, properties=None, material_type=TYPE_DEFAULT): - """ - 初始化物理材质 - - Args: - name (str): 材质名称 - properties (dict): 材质属性字典 - material_type (str): 材质类型 - """ - self.name = name - self.material_type = material_type - self.creation_time = time.time() - - # 默认材质属性 - self.properties = { - # 基础物理属性 - 'friction': 0.5, # 摩擦系数 - 'restitution': 0.1, # 恢复系数(弹性) - 'rolling_friction': 0.1, # 滚动摩擦 - 'spinning_friction': 0.1, # 自旋摩擦 - 'static_friction': 0.6, # 静摩擦 - 'dynamic_friction': 0.4, # 动摩擦 - - # 高级物理属性 - 'anisotropic_friction': Vec3(1, 1, 1), # 各向异性摩擦 - 'contact_cfm': 0.0, # 接触约束力混合参数 - 'contact_erp': 0.2, # 接触误差减少参数 - 'contact_threshold': 1e30, # 接触处理阈值 - 'stiffness': 1000000.0, # 刚度 - 'damping': 1.0, # 阻尼 - 'density': 1000.0, # 密度(kg/m³) - - # 热力学属性 - 'thermal_conductivity': 1.0, # 热导率 - 'specific_heat': 1000.0, # 比热容 - 'temperature': 20.0, # 温度(摄氏度) - 'melting_point': 1000.0, # 熔点 - 'thermal_expansion': 0.0001, # 热膨胀系数 - 'heat_generation': 0.0, # 热量生成率 - - # 电学属性 - 'electrical_conductivity': 0.0, # 电导率 - 'dielectric_constant': 1.0, # 介电常数 - 'magnetic_permeability': 1.0, # 磁导率 - 'charge_accumulation': 0.0, # 电荷积累 - - # 流体属性 - 'viscosity': 0.001, # 粘度 - 'surface_tension': 0.072, # 表面张力 - 'compressibility': 0.0, # 压缩性 - 'buoyancy': 1.0, # 浮力系数 - - # 视觉属性 - 'color': Vec4(0.5, 0.5, 0.5, 1.0), # 显示颜色 - 'surface_velocity': Vec3(0, 0, 0), # 表面速度 - 'roughness': 0.5, # 粗糙度 - 'metallic': 0.0, # 金属度 - 'specular': 0.5, # 镜面反射 - 'emissive': Vec3(0, 0, 0), # 自发光 - 'transparency': 0.0, # 透明度 - - # 环境属性 - 'weather_resistance': 1.0, # 耐候性 - 'corrosion_rate': 0.0, # 腐蚀率 - 'aging_rate': 0.0, # 老化率 - 'uv_resistance': 1.0, # 紫外线抗性 - - # 特殊属性 - 'flammability': 0.0, # 易燃性 - 'toxicity': 0.0, # 毒性 - 'radioactivity': 0.0, # 放射性 - 'explosiveness': 0.0, # 爆炸性 - 'flammability_threshold': 100.0, # 燃烧阈值 - } - - # 材质状态 - self.state = { - 'temperature': 20.0, # 当前温度 - 'humidity': 50.0, # 湿度 - 'wear': 0.0, # 磨损度 - 'contamination': 0.0, # 污染度 - 'stress': 0.0, # 应力 - 'fatigue': 0.0, # 疲劳度 - 'charge': 0.0, # 电荷 - 'wetness': 0.0, # 湿润度 - 'dirtiness': 0.0, # 脏污度 - } - - # 材质交互矩阵(与其他材质的交互系数) - self.interaction_matrix = defaultdict(lambda: 1.0) - - # 材质历史记录 - self.history = { - 'temperature_history': [], # 温度历史 - 'wear_history': [], # 磨损历史 - 'stress_history': [], # 应力历史 - 'aging_history': [], # 老化历史 - 'fatigue_history': [], # 疲劳历史 - 'chemical_history': [], # 化学反应历史 - } - - # 应用自定义属性 - if properties: - self.properties.update(properties) - - # 根据材质类型设置默认属性 - self._set_defaults_by_type() - - print(f"物理材质创建完成: {name} ({material_type})") - - def _set_defaults_by_type(self): - """ - 根据材质类型设置默认属性 - """ - if self.material_type == self.TYPE_METAL: - self.properties.update({ - 'friction': 0.3, - 'restitution': 0.4, - 'density': 7800.0, - 'color': Vec4(0.7, 0.7, 0.8, 1.0), - 'thermal_conductivity': 80.0, - 'electrical_conductivity': 1e7, - 'roughness': 0.3, - 'metallic': 1.0, - 'specular': 0.9, - 'corrosion_rate': 0.1, - }) - elif self.material_type == self.TYPE_WOOD: - self.properties.update({ - 'friction': 0.4, - 'restitution': 0.2, - 'density': 700.0, - 'color': Vec4(0.8, 0.6, 0.4, 1.0), - 'thermal_conductivity': 0.1, - 'flammability': 0.8, - 'roughness': 0.7, - 'aging_rate': 0.05, - }) - elif self.material_type == self.TYPE_CONCRETE: - self.properties.update({ - 'friction': 0.7, - 'restitution': 0.1, - 'density': 2400.0, - 'color': Vec4(0.6, 0.6, 0.6, 1.0), - 'roughness': 0.9, - 'weather_resistance': 0.9, - }) - elif self.material_type == self.TYPE_RUBBER: - self.properties.update({ - 'friction': 0.9, - 'restitution': 0.8, - 'density': 1200.0, - 'color': Vec4(0.2, 0.2, 0.2, 1.0), - 'roughness': 0.8, - 'specular': 0.1, - 'elasticity': 0.9, - }) - elif self.material_type == self.TYPE_ICE: - self.properties.update({ - 'friction': 0.03, - 'restitution': 0.1, - 'density': 917.0, - 'color': Vec4(0.8, 0.9, 1.0, 0.8), - 'thermal_conductivity': 2.2, - 'roughness': 0.1, - 'specular': 0.9, - 'melting_point': 0.0, - }) - elif self.material_type == self.TYPE_SAND: - self.properties.update({ - 'friction': 0.6, - 'restitution': 0.05, - 'density': 1600.0, - 'color': Vec4(0.9, 0.8, 0.6, 1.0), - 'roughness': 0.9, - 'flowability': 0.8, - }) - elif self.material_type == self.TYPE_WATER: - self.properties.update({ - 'friction': 0.1, - 'restitution': 0.0, - 'density': 1000.0, - 'color': Vec4(0.0, 0.5, 1.0, 0.7), - 'viscosity': 0.001, - 'surface_tension': 0.072, - 'buoyancy': 1.0, - 'transparency': 0.8, - }) - elif self.material_type == self.TYPE_GLASS: - self.properties.update({ - 'friction': 0.2, - 'restitution': 0.9, - 'density': 2500.0, - 'color': Vec4(0.8, 0.9, 1.0, 0.3), - 'roughness': 0.1, - 'specular': 0.95, - 'transparency': 0.9, - 'fragility': 0.8, - }) - elif self.material_type == self.TYPE_FLESH: - self.properties.update({ - 'friction': 0.8, - 'restitution': 0.1, - 'density': 1060.0, - 'color': Vec4(0.9, 0.7, 0.7, 1.0), - 'roughness': 0.6, - 'toxicity': 0.1, - 'elasticity': 0.3, - }) - elif self.material_type == self.TYPE_PLASTIC: - self.properties.update({ - 'friction': 0.4, - 'restitution': 0.3, - 'density': 1200.0, - 'color': Vec4(0.9, 0.9, 0.9, 1.0), - 'roughness': 0.5, - 'specular': 0.6, - 'flammability': 0.3, - 'elasticity': 0.5, - }) - elif self.material_type == self.TYPE_CLOTH: - self.properties.update({ - 'friction': 0.6, - 'restitution': 0.1, - 'density': 500.0, - 'color': Vec4(0.8, 0.8, 0.8, 1.0), - 'roughness': 0.8, - 'flexibility': 0.9, - 'breathability': 0.7, - }) - elif self.material_type == self.TYPE_LEATHER: - self.properties.update({ - 'friction': 0.5, - 'restitution': 0.2, - 'density': 900.0, - 'color': Vec4(0.5, 0.3, 0.2, 1.0), - 'roughness': 0.6, - 'durability': 0.8, - 'water_resistance': 0.6, - }) - elif self.material_type == self.TYPE_STONE: - self.properties.update({ - 'friction': 0.7, - 'restitution': 0.1, - 'density': 2600.0, - 'color': Vec4(0.7, 0.7, 0.7, 1.0), - 'roughness': 0.8, - 'hardness': 0.9, - 'weather_resistance': 0.95, - }) - elif self.material_type == self.TYPE_SNOW: - self.properties.update({ - 'friction': 0.2, - 'restitution': 0.05, - 'density': 100.0, - 'color': Vec4(1.0, 1.0, 1.0, 0.9), - 'roughness': 0.3, - 'melting_point': 0.0, - 'compressibility': 0.5, - }) - elif self.material_type == self.TYPE_MUD: - self.properties.update({ - 'friction': 0.8, - 'restitution': 0.01, - 'density': 1300.0, - 'color': Vec4(0.4, 0.3, 0.2, 1.0), - 'roughness': 0.9, - 'stickiness': 0.7, - 'viscosity': 0.1, - }) - - def set_property(self, property_name, value): - """ - 设置材质属性 - - Args: - property_name (str): 属性名称 - value: 属性值 - """ - if property_name in self.properties: - self.properties[property_name] = value - else: - raise ValueError(f"未知的材质属性: {property_name}") - - def get_property(self, property_name): - """ - 获取材质属性 - - Args: - property_name (str): 属性名称 - - Returns: - 属性值 - """ - if property_name in self.properties: - return self.properties[property_name] - else: - raise ValueError(f"未知的材质属性: {property_name}") - - def set_friction(self, friction, static_friction=None, dynamic_friction=None): - """ - 设置摩擦系数 - - Args: - friction (float): 摩擦系数 - static_friction (float): 静摩擦系数(可选) - dynamic_friction (float): 动摩擦系数(可选) - """ - self.properties['friction'] = friction - if static_friction is not None: - self.properties['static_friction'] = static_friction - if dynamic_friction is not None: - self.properties['dynamic_friction'] = dynamic_friction - - def set_restitution(self, restitution): - """ - 设置恢复系数 - - Args: - restitution (float): 恢复系数 - """ - self.properties['restitution'] = restitution - - def set_density(self, density): - """ - 设置密度 - - Args: - density (float): 密度(kg/m³) - """ - self.properties['density'] = density - - def set_color(self, color): - """ - 设置显示颜色 - - Args: - color (Vec4): RGBA颜色值 - """ - self.properties['color'] = color - - def set_thermal_properties(self, conductivity=None, specific_heat=None, - expansion=None, heat_generation=None): - """ - 设置热力学属性 - - Args: - conductivity (float): 热导率 - specific_heat (float): 比热容 - expansion (float): 热膨胀系数 - heat_generation (float): 热量生成率 - """ - if conductivity is not None: - self.properties['thermal_conductivity'] = conductivity - if specific_heat is not None: - self.properties['specific_heat'] = specific_heat - if expansion is not None: - self.properties['thermal_expansion'] = expansion - if heat_generation is not None: - self.properties['heat_generation'] = heat_generation - - def set_electrical_properties(self, conductivity=None, dielectric=None, - permeability=None, charge_accumulation=None): - """ - 设置电学属性 - - Args: - conductivity (float): 电导率 - dielectric (float): 介电常数 - permeability (float): 磁导率 - charge_accumulation (float): 电荷积累 - """ - if conductivity is not None: - self.properties['electrical_conductivity'] = conductivity - if dielectric is not None: - self.properties['dielectric_constant'] = dielectric - if permeability is not None: - self.properties['magnetic_permeability'] = permeability - if charge_accumulation is not None: - self.properties['charge_accumulation'] = charge_accumulation - - def set_fluid_properties(self, viscosity=None, surface_tension=None, - compressibility=None, buoyancy=None): - """ - 设置流体属性 - - Args: - viscosity (float): 粘度 - surface_tension (float): 表面张力 - compressibility (float): 压缩性 - buoyancy (float): 浮力系数 - """ - if viscosity is not None: - self.properties['viscosity'] = viscosity - if surface_tension is not None: - self.properties['surface_tension'] = surface_tension - if compressibility is not None: - self.properties['compressibility'] = compressibility - if buoyancy is not None: - self.properties['buoyancy'] = buoyancy - - def set_visual_properties(self, roughness=None, metallic=None, specular=None, - emissive=None, transparency=None): - """ - 设置视觉属性 - - Args: - roughness (float): 粗糙度 - metallic (float): 金属度 - specular (float): 镜面反射 - emissive (Vec3): 自发光 - transparency (float): 透明度 - """ - if roughness is not None: - self.properties['roughness'] = roughness - if metallic is not None: - self.properties['metallic'] = metallic - if specular is not None: - self.properties['specular'] = specular - if emissive is not None: - self.properties['emissive'] = emissive - if transparency is not None: - self.properties['transparency'] = transparency - - def set_environmental_properties(self, weather_resistance=None, - corrosion_rate=None, aging_rate=None, - uv_resistance=None): - """ - 设置环境属性 - - Args: - weather_resistance (float): 耐候性 - corrosion_rate (float): 腐蚀率 - aging_rate (float): 老化率 - uv_resistance (float): 紫外线抗性 - """ - if weather_resistance is not None: - self.properties['weather_resistance'] = weather_resistance - if corrosion_rate is not None: - self.properties['corrosion_rate'] = corrosion_rate - if aging_rate is not None: - self.properties['aging_rate'] = aging_rate - if uv_resistance is not None: - self.properties['uv_resistance'] = uv_resistance - - def set_special_properties(self, flammability=None, toxicity=None, - radioactivity=None, explosiveness=None, - flammability_threshold=None): - """ - 设置特殊属性 - - Args: - flammability (float): 易燃性 - toxicity (float): 毒性 - radioactivity (float): 放射性 - explosiveness (float): 爆炸性 - flammability_threshold (float): 燃烧阈值 - """ - if flammability is not None: - self.properties['flammability'] = flammability - if toxicity is not None: - self.properties['toxicity'] = toxicity - if radioactivity is not None: - self.properties['radioactivity'] = radioactivity - if explosiveness is not None: - self.properties['explosiveness'] = explosiveness - if flammability_threshold is not None: - self.properties['flammability_threshold'] = flammability_threshold - - def update_state(self, temperature=None, humidity=None, wear=None, - contamination=None, stress=None, fatigue=None, - charge=None, wetness=None, dirtiness=None): - """ - 更新材质状态 - - Args: - temperature (float): 温度 - humidity (float): 湿度 - wear (float): 磨损度 - contamination (float): 污染度 - stress (float): 应力 - fatigue (float): 疲劳度 - charge (float): 电荷 - wetness (float): 湿润度 - dirtiness (float): 脏污度 - """ - current_time = time.time() - - if temperature is not None: - self.state['temperature'] = temperature - self.history['temperature_history'].append((temperature, current_time)) - # 保持历史记录在合理范围内 - if len(self.history['temperature_history']) > 100: - self.history['temperature_history'].pop(0) - - if humidity is not None: - self.state['humidity'] = humidity - - if wear is not None: - self.state['wear'] = max(0.0, min(1.0, wear)) - self.history['wear_history'].append((wear, current_time)) - if len(self.history['wear_history']) > 100: - self.history['wear_history'].pop(0) - - if contamination is not None: - self.state['contamination'] = max(0.0, min(1.0, contamination)) - - if stress is not None: - self.state['stress'] = max(0.0, stress) - self.history['stress_history'].append((stress, current_time)) - if len(self.history['stress_history']) > 100: - self.history['stress_history'].pop(0) - - if fatigue is not None: - self.state['fatigue'] = max(0.0, min(1.0, fatigue)) - - if charge is not None: - self.state['charge'] = charge - - if wetness is not None: - self.state['wetness'] = max(0.0, min(1.0, wetness)) - - if dirtiness is not None: - self.state['dirtiness'] = max(0.0, min(1.0, dirtiness)) - - def get_effective_friction(self, other_material=None, velocity=None, normal_force=None, surface_area=None): - """ - 获取有效摩擦系数(考虑状态和交互) - - Args: - other_material (PhysicsMaterial): 另一个材质 - velocity (Vec3): 相对速度 - normal_force (float): 法向力 - surface_area (float): 接触面积 - - Returns: - float: 有效摩擦系数 - """ - base_friction = self.properties['friction'] - - # 考虑磨损影响 - wear_factor = 1.0 - self.state['wear'] * 0.3 - - # 考虑污染影响 - contamination_factor = 1.0 - self.state['contamination'] * 0.2 - - # 考虑湿润度影响 - wetness_factor = 1.0 - self.state['wetness'] * 0.5 - - # 考虑脏污度影响 - dirtiness_factor = 1.0 - self.state['dirtiness'] * 0.1 - - # 考虑速度影响(高速时摩擦减小) - velocity_factor = 1.0 - if velocity: - speed = velocity.length() - if speed > 1.0: - velocity_factor = max(0.5, 1.0 - speed * 0.01) - - # 考虑法向力影响(压力影响摩擦) - pressure_factor = 1.0 - if normal_force and surface_area and surface_area > 0: - pressure = normal_force / surface_area - # 压力增加会略微增加摩擦 - pressure_factor = 1.0 + min(0.2, pressure * 0.0001) - - # 考虑温度影响 - temperature = self.state['temperature'] - temperature_factor = 1.0 - # 高温可能降低摩擦(简化模型) - if temperature > 100: - temperature_factor = max(0.7, 1.0 - (temperature - 100) * 0.001) - - # 考虑材质交互 - interaction_factor = 1.0 - if other_material: - interaction_key = other_material.name - interaction_factor = self.interaction_matrix[interaction_key] - - effective_friction = base_friction * wear_factor * contamination_factor * wetness_factor * dirtiness_factor * velocity_factor * pressure_factor * temperature_factor * interaction_factor - return max(0.0, effective_friction) - - def get_dynamic_friction(self, other_material=None, velocity=None, normal_force=None, surface_area=None): - """ - 获取动态摩擦系数(考虑滑动状态) - - Args: - other_material (PhysicsMaterial): 另一个材质 - velocity (Vec3): 相对速度 - normal_force (float): 法向力 - surface_area (float): 接触面积 - - Returns: - float: 动态摩擦系数 - """ - # 静摩擦系数 - static_friction = self.properties.get('static_friction', self.properties['friction'] * 1.2) - - # 动摩擦系数 - dynamic_friction = self.properties.get('dynamic_friction', self.properties['friction']) - - # 速度影响(滑动时摩擦略有不同) - speed_effect = 1.0 - if velocity: - speed = velocity.length() - # 低速时接近静摩擦,高速时接近动摩擦 - if speed < 0.1: - speed_effect = static_friction / dynamic_friction if dynamic_friction > 0 else 1.0 - elif speed < 1.0: - # 插值 - t = speed / 0.1 - speed_effect = static_friction / dynamic_friction * (1 - t) + t if dynamic_friction > 0 else 1.0 - - # 使用有效摩擦系数作为基础 - effective_friction = self.get_effective_friction(other_material, velocity, normal_force, surface_area) - - # 应用速度效应 - return effective_friction * speed_effect - - def get_effective_restitution(self, other_material=None, impact_velocity=None, temperature=None): - """ - 获取有效恢复系数(考虑状态和交互) - - Args: - other_material (PhysicsMaterial): 另一个材质 - impact_velocity (float): 撞击速度 - temperature (float): 温度(可选,使用当前温度) - - Returns: - float: 有效恢复系数 - """ - base_restitution = self.properties['restitution'] - - # 考虑疲劳影响 - fatigue_factor = 1.0 - self.state['fatigue'] * 0.5 - - # 考虑应力影响 - stress_factor = 1.0 - min(1.0, self.state['stress'] / 1000.0) * 0.3 - - # 考虑撞击速度影响(高速时弹性减小) - velocity_factor = 1.0 - if impact_velocity and impact_velocity > 5.0: - velocity_factor = max(0.3, 1.0 - (impact_velocity - 5.0) * 0.05) - - # 考虑温度影响 - temp = temperature if temperature is not None else self.state['temperature'] - temperature_factor = 1.0 - # 高温可能降低弹性(简化模型) - if temp > 100: - temperature_factor = max(0.5, 1.0 - (temp - 100) * 0.002) - # 低温可能增加脆性(降低弹性) - elif temp < 0: - temperature_factor = max(0.3, 1.0 + temp * 0.005) - - # 考虑老化影响 - aging_factor = 1.0 - self.state['wear'] * 0.4 # 老化降低弹性 - - # 考虑材质交互 - interaction_factor = 1.0 - if other_material: - interaction_key = other_material.name - interaction_factor = self.interaction_matrix[interaction_key] - - effective_restitution = base_restitution * fatigue_factor * stress_factor * velocity_factor * temperature_factor * aging_factor * interaction_factor - return max(0.0, min(1.0, effective_restitution)) - - def get_energy_absorption(self, impact_energy, contact_area): - """ - 计算材质的能量吸收特性 - - Args: - impact_energy (float): 撞击能量 - contact_area (float): 接触面积 - - Returns: - dict: 能量吸收结果 - """ - # 材质的阻尼属性 - damping = self.properties.get('damping', 0.1) - - # 能量吸收(部分能量转化为热量和形变) - absorbed_energy = impact_energy * damping - reflected_energy = impact_energy - absorbed_energy - - # 计算温升 - specific_heat = self.properties.get('specific_heat', 1000.0) - mass = self.properties.get('density', 1000.0) * contact_area * 0.01 # 简化质量计算 - temperature_rise = absorbed_energy / (mass * specific_heat) if mass * specific_heat > 0 else 0 - - # 更新温度状态 - self.update_state(temperature=self.state['temperature'] + temperature_rise) - - # 计算应力增加 - stress_increase = absorbed_energy * 0.01 / contact_area if contact_area > 0 else 0 - self.update_state(stress=max(0.0, self.state['stress'] + stress_increase)) - - # 计算疲劳增加 - fatigue_increase = absorbed_energy * 0.001 / contact_area if contact_area > 0 else 0 - self.update_state(fatigue=min(1.0, self.state['fatigue'] + fatigue_increase)) - - return { - 'absorbed_energy': absorbed_energy, - 'reflected_energy': reflected_energy, - 'temperature_rise': temperature_rise, - 'stress_increase': stress_increase, - 'fatigue_increase': fatigue_increase - } - - def set_interaction_coefficient(self, other_material_name, coefficient): - """ - 设置与其他材质的交互系数 - - Args: - other_material_name (str): 其他材质名称 - coefficient (float): 交互系数 - """ - self.interaction_matrix[other_material_name] = coefficient - - def get_interaction_coefficient(self, other_material_name): - """ - 获取与其他材质的交互系数 - - Args: - other_material_name (str): 其他材质名称 - - Returns: - float: 交互系数 - """ - return self.interaction_matrix[other_material_name] - - def apply_to_rigid_body(self, rigid_body): - """ - 应用材质属性到刚体 - - Args: - rigid_body (RigidBody): 刚体对象 - """ - # 应用摩擦属性 - effective_friction = self.get_effective_friction() - rigid_body.set_friction(effective_friction) - rigid_body.bullet_body.setAnisotropicFriction(self.properties['anisotropic_friction']) - rigid_body.bullet_body.setContactProcessingThreshold(self.properties['contact_threshold']) - - # 应用恢复系数 - effective_restitution = self.get_effective_restitution() - rigid_body.set_restitution(effective_restitution) - - # 应用CFM和ERP参数 - rigid_body.bullet_body.setContactCfm(self.properties['contact_cfm']) - rigid_body.bullet_body.setContactErp(self.properties['contact_erp']) - - # 设置自定义数据 - rigid_body.set_custom_data('material_name', self.name) - rigid_body.set_custom_data('material_type', self.material_type) - - def calculate_thermal_effect(self, time_delta, environment_temp=20.0, heat_sources=None): - """ - 计算热效应 - - Args: - time_delta (float): 时间增量 - environment_temp (float): 环境温度 - heat_sources (list): 热源列表 [{'position': Point3, 'temperature': float, 'radius': float}] - - Returns: - float: 温度变化 - """ - current_temp = self.state['temperature'] - conductivity = self.properties['thermal_conductivity'] - specific_heat = self.properties['specific_heat'] - heat_generation = self.properties.get('heat_generation', 0.0) - thermal_expansion = self.properties.get('thermal_expansion', 0.0) - - # 基础热传导计算 - temp_difference = environment_temp - current_temp - heat_transfer = temp_difference * conductivity * time_delta - - # 内部热量生成 - internal_heat = heat_generation * time_delta - - # 外部热源影响 - external_heat = 0.0 - if heat_sources: - for source in heat_sources: - # 简化的热源影响计算 - source_temp = source.get('temperature', 0.0) - source_radius = source.get('radius', 1.0) - distance_factor = max(0.1, source_radius) # 简化距离影响 - external_heat += (source_temp - current_temp) * 0.1 / distance_factor * time_delta - - # 温度变化 = (热传导 + 内部热量 + 外部热源) / (质量 * 比热容) - # 这里假设单位质量为1kg - temp_change = (heat_transfer + internal_heat + external_heat) / specific_heat if specific_heat > 0 else 0 - - # 考虑相变(简化实现) - melting_point = self.properties.get('melting_point', 1000.0) - if current_temp < melting_point <= current_temp + temp_change: - # 材质开始熔化,吸收潜热 - latent_heat = self.properties.get('latent_heat_of_fusion', 0.0) - if latent_heat > 0: - temp_change *= 0.1 # 熔化过程中温度变化减缓 - - return temp_change - - def calculate_thermal_expansion(self, temperature_change): - """ - 计算热膨胀效应 - - Args: - temperature_change (float): 温度变化 - - Returns: - float: 尺寸变化因子 - """ - thermal_expansion = self.properties.get('thermal_expansion', 0.0) - return 1.0 + thermal_expansion * temperature_change - - def calculate_environmental_effects(self, time_delta, weather_conditions=None): - """ - 计算环境效应对材质的影响 - - Args: - time_delta (float): 时间增量 - weather_conditions (dict): 天气条件 {'rain': float, 'wind': float, 'humidity': float, 'uv': float} - - Returns: - dict: 环境效应结果 - """ - if weather_conditions is None: - weather_conditions = {'rain': 0.0, 'wind': 0.0, 'humidity': 0.5, 'uv': 0.0} - - # 获取材质属性 - weather_resistance = self.properties.get('weather_resistance', 1.0) - corrosion_rate = self.properties.get('corrosion_rate', 0.0) - uv_resistance = self.properties.get('uv_resistance', 1.0) - aging_rate = self.properties.get('aging_rate', 0.0) - - # 雨水影响 - rain_intensity = weather_conditions.get('rain', 0.0) - wetness_increase = rain_intensity * (1.0 - weather_resistance) * time_delta - self.update_state(wetness=min(1.0, self.state['wetness'] + wetness_increase)) - - # 湿度影响 - humidity = weather_conditions.get('humidity', 0.5) - humidity_effect = (humidity - 0.5) * 0.1 * time_delta - self.update_state(humidity=max(0.0, min(100.0, self.state['humidity'] + humidity_effect))) - - # 腐蚀影响 - corrosion_increase = rain_intensity * corrosion_rate * (1.0 - weather_resistance) * time_delta - self.update_state(contamination=min(1.0, self.state['contamination'] + corrosion_increase)) - - # UV老化影响 - uv_intensity = weather_conditions.get('uv', 0.0) - uv_damage = uv_intensity * aging_rate * (1.0 - uv_resistance) * time_delta - self.update_state(wear=min(1.0, self.state['wear'] + uv_damage)) - - # 风力影响(清洁效果) - wind_speed = weather_conditions.get('wind', 0.0) - dirt_removal = wind_speed * 0.01 * time_delta - self.update_state(dirtiness=max(0.0, self.state['dirtiness'] - dirt_removal)) - - return { - 'wetness_change': wetness_increase, - 'humidity_change': humidity_effect, - 'corrosion_change': corrosion_increase, - 'uv_damage': uv_damage, - 'dirt_removal': dirt_removal - } - - def simulate_aging(self, time_delta, environmental_factors=None): - """ - 模拟材质老化过程 - - Args: - time_delta (float): 时间增量 - environmental_factors (dict): 环境因素 - - Returns: - dict: 老化效应 - """ - if environmental_factors is None: - environmental_factors = {} - - aging_rate = self.properties.get('aging_rate', 0.0) - weather_resistance = self.properties.get('weather_resistance', 1.0) - - # 基础老化 - base_aging = aging_rate * time_delta - - # 环境加速老化 - environmental_aging = 0.0 - for factor, intensity in environmental_factors.items(): - if factor == 'temperature': - # 高温加速老化 - if intensity > 50: # 超过50度 - environmental_aging += (intensity - 50) * 0.001 * time_delta - elif factor == 'humidity': - # 高湿影响 - if intensity > 0.8: # 湿度超过80% - environmental_aging += (intensity - 0.8) * 0.1 * time_delta - elif factor == 'chemical_exposure': - # 化学暴露 - environmental_aging += intensity * 0.05 * time_delta - - # 总老化效应 - total_aging = base_aging + environmental_aging - - # 应用老化效应到材质属性 - current_wear = self.state['wear'] - new_wear = min(1.0, current_wear + total_aging) - self.update_state(wear=new_wear) - - # 老化对物理属性的影响 - aging_factor = new_wear # 简化处理,磨损程度等于老化程度 - - # 老化对摩擦系数的影响 - friction_change = aging_factor * 0.1 # 老化增加摩擦变化 - - # 老化对弹性的影响 - restitution_degradation = aging_factor * 0.2 # 老化降低弹性 - - # 更新老化历史 - current_time = time.time() - self.history['aging_history'].append((total_aging, current_time)) - if len(self.history['aging_history']) > 100: - self.history['aging_history'].pop(0) - - return { - 'aging_amount': total_aging, - 'friction_change': friction_change, - 'restitution_degradation': restitution_degradation, - 'new_wear_level': new_wear - } - - def get_aging_stats(self): - """ - 获取老化统计数据 - - Returns: - dict: 老化统计数据 - """ - if not self.history['aging_history']: - return {'total_aging': 0.0, 'average_aging_rate': 0.0} - - total_aging = sum([aging for aging, _ in self.history['aging_history']]) - avg_aging_rate = total_aging / len(self.history['aging_history']) - - return { - 'total_aging': total_aging, - 'average_aging_rate': avg_aging_rate, - 'current_wear_level': self.state['wear'] - } - - def calculate_wear_rate(self, relative_velocity, normal_force, time_delta): - """ - 计算磨损率 - - Args: - relative_velocity (Vec3): 相对速度 - normal_force (float): 法向力 - time_delta (float): 时间增量 - - Returns: - float: 磨损率 - """ - speed = relative_velocity.length() - pressure = normal_force # 简化处理 - - # 磨损率 = k * 压力 * 速度 * 时间 - wear_constant = 1e-6 # 磨损常数 - wear_rate = wear_constant * pressure * speed * time_delta - - # 考虑材质硬度影响 - hardness = self.properties.get('hardness', 1.0) - wear_rate *= (1.0 - hardness * 0.5) - - # 考虑环境因素 - wetness_factor = 1.0 + self.state['wetness'] * 0.2 # 湿润可能增加磨损 - wear_rate *= wetness_factor - - return wear_rate - - def calculate_fatigue_accumulation(self, stress_amplitude, cycles, material_properties=None): - """ - 计算疲劳累积(基于Palmgren-Miner线性累积损伤法则) - - Args: - stress_amplitude (float): 应力幅值 - cycles (int): 循环次数 - material_properties (dict): 材料特性参数 - - Returns: - float: 疲劳损伤累积 - """ - if material_properties is None: - material_properties = {} - - # 材料的疲劳参数 - fatigue_strength_coefficient = material_properties.get('fatigue_strength_coefficient', - self.properties.get('fatigue_strength_coefficient', 1000.0)) - fatigue_strength_exponent = material_properties.get('fatigue_strength_exponent', - self.properties.get('fatigue_strength_exponent', -0.1)) - fatigue_ductility_coefficient = material_properties.get('fatigue_ductility_coefficient', - self.properties.get('fatigue_ductility_coefficient', 1.0)) - fatigue_ductility_exponent = material_properties.get('fatigue_ductility_exponent', - self.properties.get('fatigue_ductility_exponent', -0.6)) - - # 计算疲劳寿命(N) - # 简化的疲劳寿命计算模型 - if stress_amplitude > 0: - # 基于应力幅值的疲劳寿命估算 - N = (fatigue_strength_coefficient / stress_amplitude) ** (1 / abs(fatigue_strength_exponent)) - else: - N = 1e10 # 极大值表示几乎不会疲劳 - - # Palmgren-Miner线性累积损伤法则 - # D = Σ(ni/Ni) ,其中D为累积损伤,ni为实际循环次数,Ni为对应应力下的疲劳寿命 - damage_per_cycle = 1.0 / N if N > 0 else 0 - total_damage = damage_per_cycle * cycles - - # 更新疲劳状态 - current_fatigue = self.state['fatigue'] - new_fatigue = min(1.0, current_fatigue + total_damage) - self.update_state(fatigue=new_fatigue) - - # 记录疲劳历史 - current_time = time.time() - self.history['fatigue_history'].append((total_damage, current_time)) - if len(self.history['fatigue_history']) > 100: - self.history['fatigue_history'].pop(0) - - return { - 'damage_per_cycle': damage_per_cycle, - 'total_damage': total_damage, - 'fatigue_life_cycles': N, - 'new_fatigue_level': new_fatigue - } - - def get_fatigue_stats(self): - """ - 获取疲劳统计数据 - - Returns: - dict: 疲劳统计数据 - """ - if not self.history['fatigue_history']: - return {'total_fatigue_damage': 0.0, 'average_fatigue_rate': 0.0} - - total_damage = sum([damage for damage, _ in self.history['fatigue_history']]) - avg_fatigue_rate = total_damage / len(self.history['fatigue_history']) - - return { - 'total_fatigue_damage': total_damage, - 'average_fatigue_rate': avg_fatigue_rate, - 'current_fatigue_level': self.state['fatigue'] - } - - def calculate_electrical_effect(self, time_delta): - """ - 计算电学效应 - - Args: - time_delta (float): 时间增量 - - Returns: - float: 电荷变化 - """ - conductivity = self.properties['electrical_conductivity'] - charge_accumulation = self.properties.get('charge_accumulation', 0.0) - - # 电荷变化 = 积累率 * 时间 - 传导率 * 当前电荷 * 时间 - charge_change = charge_accumulation * time_delta - if self.state['charge'] != 0 and conductivity > 0: - charge_change -= conductivity * self.state['charge'] * time_delta - - return charge_change - - def calculate_fluid_dynamics(self, fluid_velocity, fluid_density, time_delta): - """ - 计算流体动力学效应 - - Args: - fluid_velocity (Vec3): 流体速度 - fluid_density (float): 流体密度 - time_delta (float): 时间增量 - - Returns: - dict: 流体动力学结果 - """ - # 材质属性 - drag_coefficient = self.properties.get('drag_coefficient', 0.5) - lift_coefficient = self.properties.get('lift_coefficient', 0.1) - surface_area = self.properties.get('surface_area', 1.0) - - # 相对速度 - relative_velocity = fluid_velocity # 简化处理,假设材质相对静止 - speed = relative_velocity.length() - - # 阻力计算 (D = 0.5 * ρ * v² * Cd * A) - drag_force = 0.5 * fluid_density * speed * speed * drag_coefficient * surface_area if speed > 0 else 0.0 - - # 升力计算 (L = 0.5 * ρ * v² * Cl * A) - # 简化处理,假设升力方向为垂直于流体流动方向 - lift_force = 0.5 * fluid_density * speed * speed * lift_coefficient * surface_area if speed > 0 else 0.0 - - # 浮力计算 - material_volume = self.properties.get('density', 1000.0) / fluid_density # 简化体积计算 - buoyancy_force = fluid_density * 9.81 * material_volume - - # 更新湿润状态(与流体接触) - wetness_increase = min(1.0, speed * 0.001 * time_delta) - self.update_state(wetness=min(1.0, self.state['wetness'] + wetness_increase)) - - return { - 'drag_force': drag_force, - 'lift_force': lift_force, - 'buoyancy_force': buoyancy_force, - 'wetness_increase': wetness_increase - } - - def calculate_chemical_reaction(self, chemical_composition, temperature=None, time_delta=1.0): - """ - 计算化学反应效应 - - Args: - chemical_composition (dict): 化学成分 {'acid': float, 'base': float, 'oxidizer': float} - temperature (float): 温度(可选,使用当前温度) - time_delta (float): 时间增量 - - Returns: - dict: 化学反应结果 - """ - temp = temperature if temperature is not None else self.state['temperature'] - - # 材质抗性 - chemical_resistance = self.properties.get('chemical_resistance', 1.0) - - # 各类化学物质的影响 - acid_concentration = chemical_composition.get('acid', 0.0) - base_concentration = chemical_composition.get('base', 0.0) - oxidizer_concentration = chemical_composition.get('oxidizer', 0.0) - - # 腐蚀速率计算 - corrosion_rate = self.properties.get('corrosion_rate', 0.0) - - # 酸腐蚀 - acid_damage = acid_concentration * (1.0 - chemical_resistance) * corrosion_rate * time_delta - # 碱腐蚀 - base_damage = base_concentration * (1.0 - chemical_resistance) * corrosion_rate * 0.5 * time_delta - # 氧化 - oxidation_damage = oxidizer_concentration * (1.0 - chemical_resistance) * 0.1 * time_delta - - # 温度加速效应 - temperature_factor = 1.0 - if temp > 25: # 室温以上 - temperature_factor = 1.0 + (temp - 25) * 0.02 # 每度增加2%反应速率 - - total_damage = (acid_damage + base_damage + oxidation_damage) * temperature_factor - - # 更新污染状态 - contamination_increase = min(1.0, total_damage * 0.1) - self.update_state(contamination=min(1.0, self.state['contamination'] + contamination_increase)) - - # 更新磨损状态 - wear_increase = min(1.0, total_damage) - self.update_state(wear=min(1.0, self.state['wear'] + wear_increase)) - - # 记录化学反应历史 - current_time = time.time() - self.history['chemical_history'].append((total_damage, current_time)) - if len(self.history['chemical_history']) > 100: - self.history['chemical_history'].pop(0) - - return { - 'acid_damage': acid_damage * temperature_factor, - 'base_damage': base_damage * temperature_factor, - 'oxidation_damage': oxidation_damage * temperature_factor, - 'total_damage': total_damage, - 'contamination_increase': contamination_increase, - 'wear_increase': wear_increase - } - - def get_chemical_stats(self): - """ - 获取化学反应统计数据 - - Returns: - dict: 化学反应统计数据 - """ - if not self.history['chemical_history']: - return {'total_chemical_damage': 0.0, 'average_chemical_rate': 0.0} - - total_damage = sum([damage for damage, _ in self.history['chemical_history']]) - avg_chemical_rate = total_damage / len(self.history['chemical_history']) - - return { - 'total_chemical_damage': total_damage, - 'average_chemical_rate': avg_chemical_rate, - 'current_contamination_level': self.state['contamination'] - } - - def check_failure_criteria(self, stress_tensor, strain_tensor=None): - """ - 检查材质失效准则 - - Args: - stress_tensor (list): 应力张量 [σxx, σyy, σzz, σxy, σyz, σzx] - strain_tensor (list): 应变张量 [εxx, εyy, εzz, εxy, εyz, εzx](可选) - - Returns: - dict: 失效分析结果 - """ - # 材质强度参数 - tensile_strength = self.properties.get('tensile_strength', 1000.0) - compressive_strength = self.properties.get('compressive_strength', 1000.0) - shear_strength = self.properties.get('shear_strength', 500.0) - fatigue_limit = self.properties.get('fatigue_limit', 200.0) - - # 提取应力分量 - sigma_xx, sigma_yy, sigma_zz = stress_tensor[0], stress_tensor[1], stress_tensor[2] - sigma_xy, sigma_yz, sigma_zx = stress_tensor[3], stress_tensor[4], stress_tensor[5] - - # 计算主应力 - # 简化处理,只计算最大主应力 - max_principal_stress = max(sigma_xx, sigma_yy, sigma_zz) - min_principal_stress = min(sigma_xx, sigma_yy, sigma_zz) - - # 计算等效应力(von Mises应力) - deviatoric_stress = [ - sigma_xx - (sigma_xx + sigma_yy + sigma_zz) / 3, - sigma_yy - (sigma_xx + sigma_yy + sigma_zz) / 3, - sigma_zz - (sigma_xx + sigma_yy + sigma_zz) / 3, - sigma_xy, sigma_yz, sigma_zx - ] - - von_mises_stress = math.sqrt( - 0.5 * ( - deviatoric_stress[0]**2 + deviatoric_stress[1]**2 + deviatoric_stress[2]**2 + - 2 * (deviatoric_stress[3]**2 + deviatoric_stress[4]**2 + deviatoric_stress[5]**2) - ) - ) - - # 检查各种失效准则 - failures = {} - - # 1. 最大拉应力准则 - if max_principal_stress > tensile_strength: - failures['tensile_failure'] = { - 'safety_factor': tensile_strength / max_principal_stress if max_principal_stress > 0 else 0, - 'excess_stress': max_principal_stress - tensile_strength - } - - # 2. 最大压应力准则 - if min_principal_stress < -compressive_strength: - failures['compressive_failure'] = { - 'safety_factor': compressive_strength / abs(min_principal_stress) if min_principal_stress < 0 else 0, - 'excess_stress': abs(min_principal_stress) - compressive_strength - } - - # 3. 最大剪应力准则(Tresca准则) - max_shear_stress = (max_principal_stress - min_principal_stress) / 2 - if max_shear_stress > shear_strength: - failures['shear_failure'] = { - 'safety_factor': shear_strength / max_shear_stress if max_shear_stress > 0 else 0, - 'excess_stress': max_shear_stress - shear_strength - } - - # 4. von Mises失效准则 - if von_mises_stress > tensile_strength: # 简化处理,使用拉伸强度作为屈服强度 - failures['von_mises_failure'] = { - 'safety_factor': tensile_strength / von_mises_stress if von_mises_stress > 0 else 0, - 'excess_stress': von_mises_stress - tensile_strength - } - - # 5. 疲劳失效准则 - current_fatigue = self.state['fatigue'] - if current_fatigue > 0.9: # 疲劳度超过90%时可能发生疲劳失效 - fatigue_damage = current_fatigue * fatigue_limit - if von_mises_stress > fatigue_limit: - failures['fatigue_failure'] = { - 'safety_factor': fatigue_limit / von_mises_stress if von_mises_stress > 0 else 0, - 'fatigue_level': current_fatigue - } - - # 6. 蠕变失效准则(高温下) - temperature = self.state['temperature'] - if temperature > 0.5 * self.properties.get('melting_point', 1000.0): # 超过熔点的一半 - creep_threshold = tensile_strength * 0.1 # 蠕变阈值 - if von_mises_stress > creep_threshold: - failures['creep_failure'] = { - 'temperature_factor': temperature / self.properties.get('melting_point', 1000.0), - 'stress_ratio': von_mises_stress / creep_threshold - } - - # 更新应力历史 - current_time = time.time() - self.history['stress_history'].append((von_mises_stress, current_time)) - if len(self.history['stress_history']) > 100: - self.history['stress_history'].pop(0) - - # 更新应力状态 - self.update_state(stress=max(von_mises_stress, self.state['stress'])) - - return { - 'principal_stresses': [max_principal_stress, min_principal_stress], - 'von_mises_stress': von_mises_stress, - 'failures': failures, - 'safety_margins': self._calculate_safety_margins( - max_principal_stress, min_principal_stress, von_mises_stress, - tensile_strength, compressive_strength, shear_strength - ) - } - - def _calculate_safety_margins(self, max_stress, min_stress, von_mises_stress, - tensile_strength, compressive_strength, shear_strength): - """ - 计算安全裕度 - - Args: - max_stress (float): 最大主应力 - min_stress (float): 最小主应力 - von_mises_stress (float): von Mises应力 - tensile_strength (float): 抗拉强度 - compressive_strength (float): 抗压强度 - shear_strength (float): 抗剪强度 - - Returns: - dict: 安全裕度 - """ - safety_margins = {} - - # 拉伸安全裕度 - if max_stress > 0: - safety_margins['tensile'] = tensile_strength / max_stress if max_stress > 0 else float('inf') - else: - safety_margins['tensile'] = float('inf') - - # 压缩安全裕度 - if min_stress < 0: - safety_margins['compressive'] = compressive_strength / abs(min_stress) if min_stress < 0 else float('inf') - else: - safety_margins['compressive'] = float('inf') - - # 剪切安全裕度 - max_shear = (max_stress - min_stress) / 2 - safety_margins['shear'] = shear_strength / max_shear if max_shear > 0 else float('inf') - - # von Mises安全裕度 - safety_margins['von_mises'] = tensile_strength / von_mises_stress if von_mises_stress > 0 else float('inf') - - return safety_margins - - def simulate_fracture(self, impact_energy, impact_location): - """ - 模拟材质断裂 - - Args: - impact_energy (float): 撞击能量 - impact_location (Point3): 撞击位置 - - Returns: - dict: 断裂模拟结果 - """ - # 材质韧性参数 - fracture_toughness = self.properties.get('fracture_toughness', 50.0) - toughness = self.properties.get('toughness', 0.1) - - # 计算断裂概率 - # 基于能量密度和材质韧性 - critical_energy = fracture_toughness * 100 # 简化的临界能量 - energy_density = impact_energy / (self.properties.get('density', 1000.0) * 0.001) # 简化的能量密度 - - fracture_probability = 1.0 - math.exp(-energy_density / critical_energy) - - # 考虑材质韧性降低断裂概率 - fracture_probability *= (1.0 - toughness) - - # 考虑已有损伤状态 - current_wear = self.state['wear'] - current_fatigue = self.state['fatigue'] - fracture_probability *= (1.0 + current_wear * 0.5 + current_fatigue * 0.3) - - # 确定是否发生断裂 - import random - fracture_occurred = random.random() < min(1.0, fracture_probability) - - if fracture_occurred: - # 计算断裂严重程度 - severity = min(1.0, energy_density / critical_energy) - - # 更新材质状态 - self.update_state( - wear=min(1.0, self.state['wear'] + severity * 0.3), - stress=max(0.0, self.state['stress'] - severity * 100), # 断裂释放应力 - fatigue=min(1.0, self.state['fatigue'] + severity * 0.2) - ) - - # 记录断裂事件 - current_time = time.time() - fracture_event = { - 'time': current_time, - 'energy': impact_energy, - 'location': impact_location, - 'severity': severity, - 'probability': fracture_probability - } - - if 'fracture_history' not in self.history: - self.history['fracture_history'] = [] - - self.history['fracture_history'].append(fracture_event) - if len(self.history['fracture_history']) > 50: - self.history['fracture_history'].pop(0) - - return { - 'fracture_occurred': True, - 'severity': severity, - 'probability': fracture_probability, - 'critical_energy': critical_energy, - 'energy_density': energy_density, - 'fracture_event': fracture_event - } - else: - return { - 'fracture_occurred': False, - 'probability': fracture_probability, - 'critical_energy': critical_energy, - 'energy_density': energy_density - } - - def get_structural_integrity(self): - """ - 获取结构完整性评估 - - Returns: - dict: 结构完整性评估结果 - """ - # 综合评估各种损伤因素 - wear_factor = 1.0 - self.state['wear'] - fatigue_factor = 1.0 - self.state['fatigue'] - contamination_factor = 1.0 - self.state['contamination'] * 0.5 - stress_factor = max(0.1, 1.0 - self.state['stress'] / 10000.0) # 假设10000为临界应力 - - # 结构完整性 = 各因素加权平均 - integrity = ( - wear_factor * 0.3 + - fatigue_factor * 0.3 + - contamination_factor * 0.2 + - stress_factor * 0.2 - ) - - # 确保完整性在合理范围内 - integrity = max(0.0, min(1.0, integrity)) - - # 评估等级 - if integrity > 0.9: - integrity_level = "Excellent" - elif integrity > 0.7: - integrity_level = "Good" - elif integrity > 0.5: - integrity_level = "Fair" - elif integrity > 0.3: - integrity_level = "Poor" - else: - integrity_level = "Critical" - - return { - 'integrity': integrity, - 'integrity_level': integrity_level, - 'factors': { - 'wear_factor': wear_factor, - 'fatigue_factor': fatigue_factor, - 'contamination_factor': contamination_factor, - 'stress_factor': stress_factor - }, - 'recommendations': self._generate_maintenance_recommendations(integrity) - } - - def _generate_maintenance_recommendations(self, integrity): - """ - 生成维护建议 - - Args: - integrity (float): 结构完整性 - - Returns: - list: 维护建议列表 - """ - recommendations = [] - - if integrity < 0.3: - recommendations.append("URGENT: Immediate replacement required") - elif integrity < 0.5: - recommendations.append("Replace as soon as possible") - elif integrity < 0.7: - recommendations.append("Schedule inspection and maintenance") - - # 基于具体因素的建议 - if self.state['wear'] > 0.7: - recommendations.append("High wear detected - consider replacement") - if self.state['fatigue'] > 0.8: - recommendations.append("High fatigue level - reduce load or replace") - if self.state['contamination'] > 0.6: - recommendations.append("High contamination - cleaning recommended") - if self.state['stress'] > 5000: # 假设5000为高应力阈值 - recommendations.append("High stress levels detected - check loading conditions") - - return recommendations - - def get_history_stats(self): - """ - 获取历史统计数据 - - Returns: - dict: 历史统计数据 - """ - stats = {} - - # 温度统计 - if self.history['temperature_history']: - temps = [temp for temp, _ in self.history['temperature_history']] - stats['temperature'] = { - 'min': min(temps), - 'max': max(temps), - 'avg': sum(temps) / len(temps) - } - - # 磨损统计 - if self.history['wear_history']: - wears = [wear for wear, _ in self.history['wear_history']] - stats['wear'] = { - 'min': min(wears), - 'max': max(wears), - 'avg': sum(wears) / len(wears) - } - - # 应力统计 - if self.history['stress_history']: - stresses = [stress for stress, _ in self.history['stress_history']] - stats['stress'] = { - 'min': min(stresses), - 'max': max(stresses), - 'avg': sum(stresses) / len(stresses) - } - - return stats - - def to_dict(self): - """ - 将材质转换为字典格式 - - Returns: - dict: 材质属性字典 - """ - # 转换不可序列化的对象 - props = self.properties.copy() - if isinstance(props['anisotropic_friction'], Vec3): - props['anisotropic_friction'] = [ - props['anisotropic_friction'].getX(), - props['anisotropic_friction'].getY(), - props['anisotropic_friction'].getZ() - ] - if isinstance(props['color'], Vec4): - props['color'] = [ - props['color'].getX(), - props['color'].getY(), - props['color'].getZ(), - props['color'].getW() - ] - if isinstance(props['surface_velocity'], Vec3): - props['surface_velocity'] = [ - props['surface_velocity'].getX(), - props['surface_velocity'].getY(), - props['surface_velocity'].getZ() - ] - if isinstance(props['emissive'], Vec3): - props['emissive'] = [ - props['emissive'].getX(), - props['emissive'].getY(), - props['emissive'].getZ() - ] - - # 转换交互矩阵 - interaction_matrix = {} - for key, value in self.interaction_matrix.items(): - interaction_matrix[key] = value - - # 转换历史记录 - history = {} - for key, value in self.history.items(): - history[key] = [(v, t) for v, t in value] - - return { - 'name': self.name, - 'material_type': self.material_type, - 'properties': props, - 'state': self.state.copy(), - 'interaction_matrix': interaction_matrix, - 'history': history, - 'creation_time': self.creation_time - } - - @classmethod - def from_dict(cls, data): - """ - 从字典创建材质对象 - - Args: - data (dict): 材质数据字典 - - Returns: - PhysicsMaterial: 材质对象 - """ - # 转换序列化数据回对象 - properties = data.get('properties', {}).copy() - if 'anisotropic_friction' in properties and isinstance(properties['anisotropic_friction'], list): - properties['anisotropic_friction'] = Vec3(*properties['anisotropic_friction']) - if 'color' in properties and isinstance(properties['color'], list): - properties['color'] = Vec4(*properties['color']) - if 'surface_velocity' in properties and isinstance(properties['surface_velocity'], list): - properties['surface_velocity'] = Vec3(*properties['surface_velocity']) - if 'emissive' in properties and isinstance(properties['emissive'], list): - properties['emissive'] = Vec3(*properties['emissive']) - - material = cls(data['name'], properties, data.get('material_type', cls.TYPE_DEFAULT)) - material.state = data.get('state', material.state) - material.creation_time = data.get('creation_time', time.time()) - - # 恢复交互矩阵 - interaction_matrix = data.get('interaction_matrix', {}) - for key, value in interaction_matrix.items(): - material.interaction_matrix[key] = value - - # 恢复历史记录 - history = data.get('history', {}) - for key, value in history.items(): - if key in material.history: - material.history[key] = [(v, t) for v, t in value] - - return material - - def save_to_file(self, filepath): - """ - 保存材质到文件 - - Args: - filepath (str): 文件路径 - """ - data = self.to_dict() - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(data, f, indent=2, ensure_ascii=False) - - @classmethod - def load_from_file(cls, filepath): - """ - 从文件加载材质 - - Args: - filepath (str): 文件路径 - - Returns: - PhysicsMaterial: 材质对象 - """ - with open(filepath, 'r', encoding='utf-8') as f: - data = json.load(f) - return cls.from_dict(data) - - def __str__(self): - """ - 字符串表示 - """ - return f"PhysicsMaterial('{self.name}', type={self.material_type}, friction={self.properties['friction']}, restitution={self.properties['restitution']})" - - def __repr__(self): - """ - 详细字符串表示 - """ - return self.__str__() - - -# 预定义常用材质 -MATERIALS = { - 'concrete': PhysicsMaterial('concrete', material_type=PhysicsMaterial.TYPE_CONCRETE, properties={ - 'friction': 0.7, - 'restitution': 0.1, - 'density': 2400.0, - 'color': Vec4(0.6, 0.6, 0.6, 1.0), - 'roughness': 0.9, - 'weather_resistance': 0.9, - 'compressive_strength': 20000.0, - 'tensile_strength': 2000.0, - 'shear_strength': 1000.0, - 'thermal_conductivity': 1.0, - 'specific_heat': 880.0, - 'thermal_expansion': 0.00001, - 'fatigue_strength_coefficient': 8000.0, - 'fatigue_strength_exponent': -0.1, - }), - 'wood': PhysicsMaterial('wood', material_type=PhysicsMaterial.TYPE_WOOD, properties={ - 'friction': 0.4, - 'restitution': 0.2, - 'density': 700.0, - 'color': Vec4(0.8, 0.6, 0.4, 1.0), - 'thermal_conductivity': 0.1, - 'flammability': 0.8, - 'roughness': 0.7, - 'aging_rate': 0.05, - 'tensile_strength': 1000.0, - 'compressive_strength': 5000.0, - 'shear_strength': 800.0, - 'specific_heat': 1700.0, - 'thermal_expansion': 0.000005, - 'hardness': 0.6, - 'chemical_resistance': 0.7, - }), - 'metal': PhysicsMaterial('metal', material_type=PhysicsMaterial.TYPE_METAL, properties={ - 'friction': 0.3, - 'restitution': 0.4, - 'density': 7800.0, - 'color': Vec4(0.7, 0.7, 0.8, 1.0), - 'thermal_conductivity': 80.0, - 'electrical_conductivity': 1e7, - 'roughness': 0.3, - 'metallic': 1.0, - 'specular': 0.9, - 'corrosion_rate': 0.1, - 'tensile_strength': 40000.0, - 'compressive_strength': 40000.0, - 'shear_strength': 20000.0, - 'specific_heat': 490.0, - 'thermal_expansion': 0.000012, - 'fatigue_limit': 20000.0, - 'fracture_toughness': 100.0, - 'hardness': 0.9, - }), - 'rubber': PhysicsMaterial('rubber', material_type=PhysicsMaterial.TYPE_RUBBER, properties={ - 'friction': 0.9, - 'restitution': 0.8, - 'density': 1200.0, - 'color': Vec4(0.2, 0.2, 0.2, 1.0), - 'roughness': 0.8, - 'specular': 0.1, - 'elasticity': 0.9, - 'tensile_strength': 500.0, - 'compressive_strength': 1000.0, - 'shear_strength': 300.0, - 'specific_heat': 1500.0, - 'thermal_conductivity': 0.2, - 'damping': 0.3, - 'toughness': 0.2, - }), - 'ice': PhysicsMaterial('ice', material_type=PhysicsMaterial.TYPE_ICE, properties={ - 'friction': 0.03, - 'restitution': 0.1, - 'density': 917.0, - 'color': Vec4(0.8, 0.9, 1.0, 0.8), - 'thermal_conductivity': 2.2, - 'roughness': 0.1, - 'specular': 0.9, - 'melting_point': 0.0, - 'compressive_strength': 500.0, - 'tensile_strength': 100.0, - 'specific_heat': 2100.0, - 'thermal_expansion': 0.00005, - }), - 'sand': PhysicsMaterial('sand', material_type=PhysicsMaterial.TYPE_SAND, properties={ - 'friction': 0.6, - 'restitution': 0.05, - 'density': 1600.0, - 'color': Vec4(0.9, 0.8, 0.6, 1.0), - 'roughness': 0.9, - 'flowability': 0.8, - 'compressive_strength': 100.0, - 'tensile_strength': 10.0, - 'specific_heat': 830.0, - 'thermal_conductivity': 0.25, - }), - 'water': PhysicsMaterial('water', material_type=PhysicsMaterial.TYPE_WATER, properties={ - 'friction': 0.1, - 'restitution': 0.0, - 'density': 1000.0, - 'color': Vec4(0.0, 0.5, 1.0, 0.7), - 'viscosity': 0.001, - 'surface_tension': 0.072, - 'buoyancy': 1.0, - 'transparency': 0.8, - 'compressive_strength': 2000000.0, # 水几乎不可压缩 - 'specific_heat': 4186.0, - 'thermal_conductivity': 0.6, - }), - 'glass': PhysicsMaterial('glass', material_type=PhysicsMaterial.TYPE_GLASS, properties={ - 'friction': 0.2, - 'restitution': 0.9, - 'density': 2500.0, - 'color': Vec4(0.8, 0.9, 1.0, 0.3), - 'roughness': 0.1, - 'specular': 0.95, - 'transparency': 0.9, - 'fragility': 0.8, - 'tensile_strength': 7000.0, - 'compressive_strength': 10000.0, - 'shear_strength': 3000.0, - 'specific_heat': 840.0, - 'thermal_conductivity': 1.0, - 'fracture_toughness': 20.0, - }), - 'flesh': PhysicsMaterial('flesh', material_type=PhysicsMaterial.TYPE_FLESH, properties={ - 'friction': 0.8, - 'restitution': 0.1, - 'density': 1060.0, - 'color': Vec4(0.9, 0.7, 0.7, 1.0), - 'roughness': 0.6, - 'toxicity': 0.1, - 'elasticity': 0.3, - 'tensile_strength': 50.0, - 'compressive_strength': 200.0, - 'shear_strength': 30.0, - 'specific_heat': 3500.0, - 'thermal_conductivity': 0.3, - 'damping': 0.2, - }), - 'plastic': PhysicsMaterial('plastic', material_type=PhysicsMaterial.TYPE_PLASTIC, properties={ - 'friction': 0.4, - 'restitution': 0.3, - 'density': 1200.0, - 'color': Vec4(0.9, 0.9, 0.9, 1.0), - 'roughness': 0.5, - 'specular': 0.6, - 'flammability': 0.3, - 'elasticity': 0.5, - 'tensile_strength': 5000.0, - 'compressive_strength': 8000.0, - 'shear_strength': 3000.0, - 'specific_heat': 1300.0, - 'thermal_conductivity': 0.2, - 'chemical_resistance': 0.8, - }), - 'cloth': PhysicsMaterial('cloth', material_type=PhysicsMaterial.TYPE_CLOTH, properties={ - 'friction': 0.6, - 'restitution': 0.1, - 'density': 500.0, - 'color': Vec4(0.8, 0.8, 0.8, 1.0), - 'roughness': 0.8, - 'flexibility': 0.9, - 'breathability': 0.7, - 'tensile_strength': 200.0, - 'specific_heat': 1200.0, - 'thermal_conductivity': 0.05, - 'damping': 0.1, - }), - 'leather': PhysicsMaterial('leather', material_type=PhysicsMaterial.TYPE_LEATHER, properties={ - 'friction': 0.5, - 'restitution': 0.2, - 'density': 900.0, - 'color': Vec4(0.5, 0.3, 0.2, 1.0), - 'roughness': 0.6, - 'durability': 0.8, - 'water_resistance': 0.6, - 'tensile_strength': 800.0, - 'specific_heat': 1500.0, - 'thermal_conductivity': 0.15, - 'aging_rate': 0.02, - }), - 'stone': PhysicsMaterial('stone', material_type=PhysicsMaterial.TYPE_STONE, properties={ - 'friction': 0.7, - 'restitution': 0.1, - 'density': 2600.0, - 'color': Vec4(0.7, 0.7, 0.7, 1.0), - 'roughness': 0.8, - 'hardness': 0.9, - 'weather_resistance': 0.95, - 'tensile_strength': 3000.0, - 'compressive_strength': 20000.0, - 'shear_strength': 1000.0, - 'specific_heat': 800.0, - 'thermal_conductivity': 2.0, - }), - 'snow': PhysicsMaterial('snow', material_type=PhysicsMaterial.TYPE_SNOW, properties={ - 'friction': 0.2, - 'restitution': 0.05, - 'density': 100.0, - 'color': Vec4(1.0, 1.0, 1.0, 0.9), - 'roughness': 0.3, - 'melting_point': 0.0, - 'compressibility': 0.5, - 'tensile_strength': 50.0, - 'specific_heat': 2100.0, - 'thermal_conductivity': 0.3, - }), - 'mud': PhysicsMaterial('mud', material_type=PhysicsMaterial.TYPE_MUD, properties={ - 'friction': 0.8, - 'restitution': 0.01, - 'density': 1300.0, - 'color': Vec4(0.4, 0.3, 0.2, 1.0), - 'roughness': 0.9, - 'stickiness': 0.7, - 'viscosity': 0.1, - 'tensile_strength': 20.0, - 'specific_heat': 1800.0, - 'thermal_conductivity': 0.8, - }), -} - - -# 材质交互预设 -MATERIAL_INTERACTIONS = { - ('metal', 'metal'): 1.2, - ('metal', 'wood'): 0.8, - ('metal', 'concrete'): 0.9, - ('metal', 'rubber'): 1.1, - ('wood', 'wood'): 1.0, - ('wood', 'concrete'): 0.7, - ('concrete', 'concrete'): 1.0, - ('rubber', 'rubber'): 1.3, - ('ice', 'ice'): 0.1, - ('ice', 'metal'): 0.15, - ('sand', 'sand'): 0.9, - ('water', 'metal'): 0.1, - ('water', 'wood'): 0.2, - ('glass', 'glass'): 0.95, - ('plastic', 'plastic'): 0.6, - ('cloth', 'cloth'): 0.8, - ('leather', 'leather'): 0.7, - ('stone', 'stone'): 0.85, - ('snow', 'snow'): 0.3, - ('mud', 'mud'): 0.4, +""" +物理材质系统模块 +负责物理材质的定义和管理 +""" + +from panda3d.core import Vec3, Vec4 +import json +import math +import time +from collections import defaultdict + + +class PhysicsMaterial: + """ + 物理材质类 + 定义物体的物理表面属性 + """ + + # 预定义材质类型 + TYPE_DEFAULT = 'default' + TYPE_METAL = 'metal' + TYPE_WOOD = 'wood' + TYPE_CONCRETE = 'concrete' + TYPE_RUBBER = 'rubber' + TYPE_ICE = 'ice' + TYPE_SAND = 'sand' + TYPE_WATER = 'water' + TYPE_GLASS = 'glass' + TYPE_FLESH = 'flesh' + TYPE_PLASTIC = 'plastic' + TYPE_CLOTH = 'cloth' + TYPE_LEATHER = 'leather' + TYPE_STONE = 'stone' + TYPE_SNOW = 'snow' + TYPE_MUD = 'mud' + + def __init__(self, name, properties=None, material_type=TYPE_DEFAULT): + """ + 初始化物理材质 + + Args: + name (str): 材质名称 + properties (dict): 材质属性字典 + material_type (str): 材质类型 + """ + self.name = name + self.material_type = material_type + self.creation_time = time.time() + + # 默认材质属性 + self.properties = { + # 基础物理属性 + 'friction': 0.5, # 摩擦系数 + 'restitution': 0.1, # 恢复系数(弹性) + 'rolling_friction': 0.1, # 滚动摩擦 + 'spinning_friction': 0.1, # 自旋摩擦 + 'static_friction': 0.6, # 静摩擦 + 'dynamic_friction': 0.4, # 动摩擦 + + # 高级物理属性 + 'anisotropic_friction': Vec3(1, 1, 1), # 各向异性摩擦 + 'contact_cfm': 0.0, # 接触约束力混合参数 + 'contact_erp': 0.2, # 接触误差减少参数 + 'contact_threshold': 1e30, # 接触处理阈值 + 'stiffness': 1000000.0, # 刚度 + 'damping': 1.0, # 阻尼 + 'density': 1000.0, # 密度(kg/m³) + + # 热力学属性 + 'thermal_conductivity': 1.0, # 热导率 + 'specific_heat': 1000.0, # 比热容 + 'temperature': 20.0, # 温度(摄氏度) + 'melting_point': 1000.0, # 熔点 + 'thermal_expansion': 0.0001, # 热膨胀系数 + 'heat_generation': 0.0, # 热量生成率 + + # 电学属性 + 'electrical_conductivity': 0.0, # 电导率 + 'dielectric_constant': 1.0, # 介电常数 + 'magnetic_permeability': 1.0, # 磁导率 + 'charge_accumulation': 0.0, # 电荷积累 + + # 流体属性 + 'viscosity': 0.001, # 粘度 + 'surface_tension': 0.072, # 表面张力 + 'compressibility': 0.0, # 压缩性 + 'buoyancy': 1.0, # 浮力系数 + + # 视觉属性 + 'color': Vec4(0.5, 0.5, 0.5, 1.0), # 显示颜色 + 'surface_velocity': Vec3(0, 0, 0), # 表面速度 + 'roughness': 0.5, # 粗糙度 + 'metallic': 0.0, # 金属度 + 'specular': 0.5, # 镜面反射 + 'emissive': Vec3(0, 0, 0), # 自发光 + 'transparency': 0.0, # 透明度 + + # 环境属性 + 'weather_resistance': 1.0, # 耐候性 + 'corrosion_rate': 0.0, # 腐蚀率 + 'aging_rate': 0.0, # 老化率 + 'uv_resistance': 1.0, # 紫外线抗性 + + # 特殊属性 + 'flammability': 0.0, # 易燃性 + 'toxicity': 0.0, # 毒性 + 'radioactivity': 0.0, # 放射性 + 'explosiveness': 0.0, # 爆炸性 + 'flammability_threshold': 100.0, # 燃烧阈值 + } + + # 材质状态 + self.state = { + 'temperature': 20.0, # 当前温度 + 'humidity': 50.0, # 湿度 + 'wear': 0.0, # 磨损度 + 'contamination': 0.0, # 污染度 + 'stress': 0.0, # 应力 + 'fatigue': 0.0, # 疲劳度 + 'charge': 0.0, # 电荷 + 'wetness': 0.0, # 湿润度 + 'dirtiness': 0.0, # 脏污度 + } + + # 材质交互矩阵(与其他材质的交互系数) + self.interaction_matrix = defaultdict(lambda: 1.0) + + # 材质历史记录 + self.history = { + 'temperature_history': [], # 温度历史 + 'wear_history': [], # 磨损历史 + 'stress_history': [], # 应力历史 + 'aging_history': [], # 老化历史 + 'fatigue_history': [], # 疲劳历史 + 'chemical_history': [], # 化学反应历史 + } + + # 应用自定义属性 + if properties: + self.properties.update(properties) + + # 根据材质类型设置默认属性 + self._set_defaults_by_type() + + print(f"物理材质创建完成: {name} ({material_type})") + + def _set_defaults_by_type(self): + """ + 根据材质类型设置默认属性 + """ + if self.material_type == self.TYPE_METAL: + self.properties.update({ + 'friction': 0.3, + 'restitution': 0.4, + 'density': 7800.0, + 'color': Vec4(0.7, 0.7, 0.8, 1.0), + 'thermal_conductivity': 80.0, + 'electrical_conductivity': 1e7, + 'roughness': 0.3, + 'metallic': 1.0, + 'specular': 0.9, + 'corrosion_rate': 0.1, + }) + elif self.material_type == self.TYPE_WOOD: + self.properties.update({ + 'friction': 0.4, + 'restitution': 0.2, + 'density': 700.0, + 'color': Vec4(0.8, 0.6, 0.4, 1.0), + 'thermal_conductivity': 0.1, + 'flammability': 0.8, + 'roughness': 0.7, + 'aging_rate': 0.05, + }) + elif self.material_type == self.TYPE_CONCRETE: + self.properties.update({ + 'friction': 0.7, + 'restitution': 0.1, + 'density': 2400.0, + 'color': Vec4(0.6, 0.6, 0.6, 1.0), + 'roughness': 0.9, + 'weather_resistance': 0.9, + }) + elif self.material_type == self.TYPE_RUBBER: + self.properties.update({ + 'friction': 0.9, + 'restitution': 0.8, + 'density': 1200.0, + 'color': Vec4(0.2, 0.2, 0.2, 1.0), + 'roughness': 0.8, + 'specular': 0.1, + 'elasticity': 0.9, + }) + elif self.material_type == self.TYPE_ICE: + self.properties.update({ + 'friction': 0.03, + 'restitution': 0.1, + 'density': 917.0, + 'color': Vec4(0.8, 0.9, 1.0, 0.8), + 'thermal_conductivity': 2.2, + 'roughness': 0.1, + 'specular': 0.9, + 'melting_point': 0.0, + }) + elif self.material_type == self.TYPE_SAND: + self.properties.update({ + 'friction': 0.6, + 'restitution': 0.05, + 'density': 1600.0, + 'color': Vec4(0.9, 0.8, 0.6, 1.0), + 'roughness': 0.9, + 'flowability': 0.8, + }) + elif self.material_type == self.TYPE_WATER: + self.properties.update({ + 'friction': 0.1, + 'restitution': 0.0, + 'density': 1000.0, + 'color': Vec4(0.0, 0.5, 1.0, 0.7), + 'viscosity': 0.001, + 'surface_tension': 0.072, + 'buoyancy': 1.0, + 'transparency': 0.8, + }) + elif self.material_type == self.TYPE_GLASS: + self.properties.update({ + 'friction': 0.2, + 'restitution': 0.9, + 'density': 2500.0, + 'color': Vec4(0.8, 0.9, 1.0, 0.3), + 'roughness': 0.1, + 'specular': 0.95, + 'transparency': 0.9, + 'fragility': 0.8, + }) + elif self.material_type == self.TYPE_FLESH: + self.properties.update({ + 'friction': 0.8, + 'restitution': 0.1, + 'density': 1060.0, + 'color': Vec4(0.9, 0.7, 0.7, 1.0), + 'roughness': 0.6, + 'toxicity': 0.1, + 'elasticity': 0.3, + }) + elif self.material_type == self.TYPE_PLASTIC: + self.properties.update({ + 'friction': 0.4, + 'restitution': 0.3, + 'density': 1200.0, + 'color': Vec4(0.9, 0.9, 0.9, 1.0), + 'roughness': 0.5, + 'specular': 0.6, + 'flammability': 0.3, + 'elasticity': 0.5, + }) + elif self.material_type == self.TYPE_CLOTH: + self.properties.update({ + 'friction': 0.6, + 'restitution': 0.1, + 'density': 500.0, + 'color': Vec4(0.8, 0.8, 0.8, 1.0), + 'roughness': 0.8, + 'flexibility': 0.9, + 'breathability': 0.7, + }) + elif self.material_type == self.TYPE_LEATHER: + self.properties.update({ + 'friction': 0.5, + 'restitution': 0.2, + 'density': 900.0, + 'color': Vec4(0.5, 0.3, 0.2, 1.0), + 'roughness': 0.6, + 'durability': 0.8, + 'water_resistance': 0.6, + }) + elif self.material_type == self.TYPE_STONE: + self.properties.update({ + 'friction': 0.7, + 'restitution': 0.1, + 'density': 2600.0, + 'color': Vec4(0.7, 0.7, 0.7, 1.0), + 'roughness': 0.8, + 'hardness': 0.9, + 'weather_resistance': 0.95, + }) + elif self.material_type == self.TYPE_SNOW: + self.properties.update({ + 'friction': 0.2, + 'restitution': 0.05, + 'density': 100.0, + 'color': Vec4(1.0, 1.0, 1.0, 0.9), + 'roughness': 0.3, + 'melting_point': 0.0, + 'compressibility': 0.5, + }) + elif self.material_type == self.TYPE_MUD: + self.properties.update({ + 'friction': 0.8, + 'restitution': 0.01, + 'density': 1300.0, + 'color': Vec4(0.4, 0.3, 0.2, 1.0), + 'roughness': 0.9, + 'stickiness': 0.7, + 'viscosity': 0.1, + }) + + def set_property(self, property_name, value): + """ + 设置材质属性 + + Args: + property_name (str): 属性名称 + value: 属性值 + """ + if property_name in self.properties: + self.properties[property_name] = value + else: + raise ValueError(f"未知的材质属性: {property_name}") + + def get_property(self, property_name): + """ + 获取材质属性 + + Args: + property_name (str): 属性名称 + + Returns: + 属性值 + """ + if property_name in self.properties: + return self.properties[property_name] + else: + raise ValueError(f"未知的材质属性: {property_name}") + + def set_friction(self, friction, static_friction=None, dynamic_friction=None): + """ + 设置摩擦系数 + + Args: + friction (float): 摩擦系数 + static_friction (float): 静摩擦系数(可选) + dynamic_friction (float): 动摩擦系数(可选) + """ + self.properties['friction'] = friction + if static_friction is not None: + self.properties['static_friction'] = static_friction + if dynamic_friction is not None: + self.properties['dynamic_friction'] = dynamic_friction + + def set_restitution(self, restitution): + """ + 设置恢复系数 + + Args: + restitution (float): 恢复系数 + """ + self.properties['restitution'] = restitution + + def set_density(self, density): + """ + 设置密度 + + Args: + density (float): 密度(kg/m³) + """ + self.properties['density'] = density + + def set_color(self, color): + """ + 设置显示颜色 + + Args: + color (Vec4): RGBA颜色值 + """ + self.properties['color'] = color + + def set_thermal_properties(self, conductivity=None, specific_heat=None, + expansion=None, heat_generation=None): + """ + 设置热力学属性 + + Args: + conductivity (float): 热导率 + specific_heat (float): 比热容 + expansion (float): 热膨胀系数 + heat_generation (float): 热量生成率 + """ + if conductivity is not None: + self.properties['thermal_conductivity'] = conductivity + if specific_heat is not None: + self.properties['specific_heat'] = specific_heat + if expansion is not None: + self.properties['thermal_expansion'] = expansion + if heat_generation is not None: + self.properties['heat_generation'] = heat_generation + + def set_electrical_properties(self, conductivity=None, dielectric=None, + permeability=None, charge_accumulation=None): + """ + 设置电学属性 + + Args: + conductivity (float): 电导率 + dielectric (float): 介电常数 + permeability (float): 磁导率 + charge_accumulation (float): 电荷积累 + """ + if conductivity is not None: + self.properties['electrical_conductivity'] = conductivity + if dielectric is not None: + self.properties['dielectric_constant'] = dielectric + if permeability is not None: + self.properties['magnetic_permeability'] = permeability + if charge_accumulation is not None: + self.properties['charge_accumulation'] = charge_accumulation + + def set_fluid_properties(self, viscosity=None, surface_tension=None, + compressibility=None, buoyancy=None): + """ + 设置流体属性 + + Args: + viscosity (float): 粘度 + surface_tension (float): 表面张力 + compressibility (float): 压缩性 + buoyancy (float): 浮力系数 + """ + if viscosity is not None: + self.properties['viscosity'] = viscosity + if surface_tension is not None: + self.properties['surface_tension'] = surface_tension + if compressibility is not None: + self.properties['compressibility'] = compressibility + if buoyancy is not None: + self.properties['buoyancy'] = buoyancy + + def set_visual_properties(self, roughness=None, metallic=None, specular=None, + emissive=None, transparency=None): + """ + 设置视觉属性 + + Args: + roughness (float): 粗糙度 + metallic (float): 金属度 + specular (float): 镜面反射 + emissive (Vec3): 自发光 + transparency (float): 透明度 + """ + if roughness is not None: + self.properties['roughness'] = roughness + if metallic is not None: + self.properties['metallic'] = metallic + if specular is not None: + self.properties['specular'] = specular + if emissive is not None: + self.properties['emissive'] = emissive + if transparency is not None: + self.properties['transparency'] = transparency + + def set_environmental_properties(self, weather_resistance=None, + corrosion_rate=None, aging_rate=None, + uv_resistance=None): + """ + 设置环境属性 + + Args: + weather_resistance (float): 耐候性 + corrosion_rate (float): 腐蚀率 + aging_rate (float): 老化率 + uv_resistance (float): 紫外线抗性 + """ + if weather_resistance is not None: + self.properties['weather_resistance'] = weather_resistance + if corrosion_rate is not None: + self.properties['corrosion_rate'] = corrosion_rate + if aging_rate is not None: + self.properties['aging_rate'] = aging_rate + if uv_resistance is not None: + self.properties['uv_resistance'] = uv_resistance + + def set_special_properties(self, flammability=None, toxicity=None, + radioactivity=None, explosiveness=None, + flammability_threshold=None): + """ + 设置特殊属性 + + Args: + flammability (float): 易燃性 + toxicity (float): 毒性 + radioactivity (float): 放射性 + explosiveness (float): 爆炸性 + flammability_threshold (float): 燃烧阈值 + """ + if flammability is not None: + self.properties['flammability'] = flammability + if toxicity is not None: + self.properties['toxicity'] = toxicity + if radioactivity is not None: + self.properties['radioactivity'] = radioactivity + if explosiveness is not None: + self.properties['explosiveness'] = explosiveness + if flammability_threshold is not None: + self.properties['flammability_threshold'] = flammability_threshold + + def update_state(self, temperature=None, humidity=None, wear=None, + contamination=None, stress=None, fatigue=None, + charge=None, wetness=None, dirtiness=None): + """ + 更新材质状态 + + Args: + temperature (float): 温度 + humidity (float): 湿度 + wear (float): 磨损度 + contamination (float): 污染度 + stress (float): 应力 + fatigue (float): 疲劳度 + charge (float): 电荷 + wetness (float): 湿润度 + dirtiness (float): 脏污度 + """ + current_time = time.time() + + if temperature is not None: + self.state['temperature'] = temperature + self.history['temperature_history'].append((temperature, current_time)) + # 保持历史记录在合理范围内 + if len(self.history['temperature_history']) > 100: + self.history['temperature_history'].pop(0) + + if humidity is not None: + self.state['humidity'] = humidity + + if wear is not None: + self.state['wear'] = max(0.0, min(1.0, wear)) + self.history['wear_history'].append((wear, current_time)) + if len(self.history['wear_history']) > 100: + self.history['wear_history'].pop(0) + + if contamination is not None: + self.state['contamination'] = max(0.0, min(1.0, contamination)) + + if stress is not None: + self.state['stress'] = max(0.0, stress) + self.history['stress_history'].append((stress, current_time)) + if len(self.history['stress_history']) > 100: + self.history['stress_history'].pop(0) + + if fatigue is not None: + self.state['fatigue'] = max(0.0, min(1.0, fatigue)) + + if charge is not None: + self.state['charge'] = charge + + if wetness is not None: + self.state['wetness'] = max(0.0, min(1.0, wetness)) + + if dirtiness is not None: + self.state['dirtiness'] = max(0.0, min(1.0, dirtiness)) + + def get_effective_friction(self, other_material=None, velocity=None, normal_force=None, surface_area=None): + """ + 获取有效摩擦系数(考虑状态和交互) + + Args: + other_material (PhysicsMaterial): 另一个材质 + velocity (Vec3): 相对速度 + normal_force (float): 法向力 + surface_area (float): 接触面积 + + Returns: + float: 有效摩擦系数 + """ + base_friction = self.properties['friction'] + + # 考虑磨损影响 + wear_factor = 1.0 - self.state['wear'] * 0.3 + + # 考虑污染影响 + contamination_factor = 1.0 - self.state['contamination'] * 0.2 + + # 考虑湿润度影响 + wetness_factor = 1.0 - self.state['wetness'] * 0.5 + + # 考虑脏污度影响 + dirtiness_factor = 1.0 - self.state['dirtiness'] * 0.1 + + # 考虑速度影响(高速时摩擦减小) + velocity_factor = 1.0 + if velocity: + speed = velocity.length() + if speed > 1.0: + velocity_factor = max(0.5, 1.0 - speed * 0.01) + + # 考虑法向力影响(压力影响摩擦) + pressure_factor = 1.0 + if normal_force and surface_area and surface_area > 0: + pressure = normal_force / surface_area + # 压力增加会略微增加摩擦 + pressure_factor = 1.0 + min(0.2, pressure * 0.0001) + + # 考虑温度影响 + temperature = self.state['temperature'] + temperature_factor = 1.0 + # 高温可能降低摩擦(简化模型) + if temperature > 100: + temperature_factor = max(0.7, 1.0 - (temperature - 100) * 0.001) + + # 考虑材质交互 + interaction_factor = 1.0 + if other_material: + interaction_key = other_material.name + interaction_factor = self.interaction_matrix[interaction_key] + + effective_friction = base_friction * wear_factor * contamination_factor * wetness_factor * dirtiness_factor * velocity_factor * pressure_factor * temperature_factor * interaction_factor + return max(0.0, effective_friction) + + def get_dynamic_friction(self, other_material=None, velocity=None, normal_force=None, surface_area=None): + """ + 获取动态摩擦系数(考虑滑动状态) + + Args: + other_material (PhysicsMaterial): 另一个材质 + velocity (Vec3): 相对速度 + normal_force (float): 法向力 + surface_area (float): 接触面积 + + Returns: + float: 动态摩擦系数 + """ + # 静摩擦系数 + static_friction = self.properties.get('static_friction', self.properties['friction'] * 1.2) + + # 动摩擦系数 + dynamic_friction = self.properties.get('dynamic_friction', self.properties['friction']) + + # 速度影响(滑动时摩擦略有不同) + speed_effect = 1.0 + if velocity: + speed = velocity.length() + # 低速时接近静摩擦,高速时接近动摩擦 + if speed < 0.1: + speed_effect = static_friction / dynamic_friction if dynamic_friction > 0 else 1.0 + elif speed < 1.0: + # 插值 + t = speed / 0.1 + speed_effect = static_friction / dynamic_friction * (1 - t) + t if dynamic_friction > 0 else 1.0 + + # 使用有效摩擦系数作为基础 + effective_friction = self.get_effective_friction(other_material, velocity, normal_force, surface_area) + + # 应用速度效应 + return effective_friction * speed_effect + + def get_effective_restitution(self, other_material=None, impact_velocity=None, temperature=None): + """ + 获取有效恢复系数(考虑状态和交互) + + Args: + other_material (PhysicsMaterial): 另一个材质 + impact_velocity (float): 撞击速度 + temperature (float): 温度(可选,使用当前温度) + + Returns: + float: 有效恢复系数 + """ + base_restitution = self.properties['restitution'] + + # 考虑疲劳影响 + fatigue_factor = 1.0 - self.state['fatigue'] * 0.5 + + # 考虑应力影响 + stress_factor = 1.0 - min(1.0, self.state['stress'] / 1000.0) * 0.3 + + # 考虑撞击速度影响(高速时弹性减小) + velocity_factor = 1.0 + if impact_velocity and impact_velocity > 5.0: + velocity_factor = max(0.3, 1.0 - (impact_velocity - 5.0) * 0.05) + + # 考虑温度影响 + temp = temperature if temperature is not None else self.state['temperature'] + temperature_factor = 1.0 + # 高温可能降低弹性(简化模型) + if temp > 100: + temperature_factor = max(0.5, 1.0 - (temp - 100) * 0.002) + # 低温可能增加脆性(降低弹性) + elif temp < 0: + temperature_factor = max(0.3, 1.0 + temp * 0.005) + + # 考虑老化影响 + aging_factor = 1.0 - self.state['wear'] * 0.4 # 老化降低弹性 + + # 考虑材质交互 + interaction_factor = 1.0 + if other_material: + interaction_key = other_material.name + interaction_factor = self.interaction_matrix[interaction_key] + + effective_restitution = base_restitution * fatigue_factor * stress_factor * velocity_factor * temperature_factor * aging_factor * interaction_factor + return max(0.0, min(1.0, effective_restitution)) + + def get_energy_absorption(self, impact_energy, contact_area): + """ + 计算材质的能量吸收特性 + + Args: + impact_energy (float): 撞击能量 + contact_area (float): 接触面积 + + Returns: + dict: 能量吸收结果 + """ + # 材质的阻尼属性 + damping = self.properties.get('damping', 0.1) + + # 能量吸收(部分能量转化为热量和形变) + absorbed_energy = impact_energy * damping + reflected_energy = impact_energy - absorbed_energy + + # 计算温升 + specific_heat = self.properties.get('specific_heat', 1000.0) + mass = self.properties.get('density', 1000.0) * contact_area * 0.01 # 简化质量计算 + temperature_rise = absorbed_energy / (mass * specific_heat) if mass * specific_heat > 0 else 0 + + # 更新温度状态 + self.update_state(temperature=self.state['temperature'] + temperature_rise) + + # 计算应力增加 + stress_increase = absorbed_energy * 0.01 / contact_area if contact_area > 0 else 0 + self.update_state(stress=max(0.0, self.state['stress'] + stress_increase)) + + # 计算疲劳增加 + fatigue_increase = absorbed_energy * 0.001 / contact_area if contact_area > 0 else 0 + self.update_state(fatigue=min(1.0, self.state['fatigue'] + fatigue_increase)) + + return { + 'absorbed_energy': absorbed_energy, + 'reflected_energy': reflected_energy, + 'temperature_rise': temperature_rise, + 'stress_increase': stress_increase, + 'fatigue_increase': fatigue_increase + } + + def set_interaction_coefficient(self, other_material_name, coefficient): + """ + 设置与其他材质的交互系数 + + Args: + other_material_name (str): 其他材质名称 + coefficient (float): 交互系数 + """ + self.interaction_matrix[other_material_name] = coefficient + + def get_interaction_coefficient(self, other_material_name): + """ + 获取与其他材质的交互系数 + + Args: + other_material_name (str): 其他材质名称 + + Returns: + float: 交互系数 + """ + return self.interaction_matrix[other_material_name] + + def apply_to_rigid_body(self, rigid_body): + """ + 应用材质属性到刚体 + + Args: + rigid_body (RigidBody): 刚体对象 + """ + # 应用摩擦属性 + effective_friction = self.get_effective_friction() + rigid_body.set_friction(effective_friction) + rigid_body.bullet_body.setAnisotropicFriction(self.properties['anisotropic_friction']) + rigid_body.bullet_body.setContactProcessingThreshold(self.properties['contact_threshold']) + + # 应用恢复系数 + effective_restitution = self.get_effective_restitution() + rigid_body.set_restitution(effective_restitution) + + # 应用CFM和ERP参数 + rigid_body.bullet_body.setContactCfm(self.properties['contact_cfm']) + rigid_body.bullet_body.setContactErp(self.properties['contact_erp']) + + # 设置自定义数据 + rigid_body.set_custom_data('material_name', self.name) + rigid_body.set_custom_data('material_type', self.material_type) + + def calculate_thermal_effect(self, time_delta, environment_temp=20.0, heat_sources=None): + """ + 计算热效应 + + Args: + time_delta (float): 时间增量 + environment_temp (float): 环境温度 + heat_sources (list): 热源列表 [{'position': Point3, 'temperature': float, 'radius': float}] + + Returns: + float: 温度变化 + """ + current_temp = self.state['temperature'] + conductivity = self.properties['thermal_conductivity'] + specific_heat = self.properties['specific_heat'] + heat_generation = self.properties.get('heat_generation', 0.0) + thermal_expansion = self.properties.get('thermal_expansion', 0.0) + + # 基础热传导计算 + temp_difference = environment_temp - current_temp + heat_transfer = temp_difference * conductivity * time_delta + + # 内部热量生成 + internal_heat = heat_generation * time_delta + + # 外部热源影响 + external_heat = 0.0 + if heat_sources: + for source in heat_sources: + # 简化的热源影响计算 + source_temp = source.get('temperature', 0.0) + source_radius = source.get('radius', 1.0) + distance_factor = max(0.1, source_radius) # 简化距离影响 + external_heat += (source_temp - current_temp) * 0.1 / distance_factor * time_delta + + # 温度变化 = (热传导 + 内部热量 + 外部热源) / (质量 * 比热容) + # 这里假设单位质量为1kg + temp_change = (heat_transfer + internal_heat + external_heat) / specific_heat if specific_heat > 0 else 0 + + # 考虑相变(简化实现) + melting_point = self.properties.get('melting_point', 1000.0) + if current_temp < melting_point <= current_temp + temp_change: + # 材质开始熔化,吸收潜热 + latent_heat = self.properties.get('latent_heat_of_fusion', 0.0) + if latent_heat > 0: + temp_change *= 0.1 # 熔化过程中温度变化减缓 + + return temp_change + + def calculate_thermal_expansion(self, temperature_change): + """ + 计算热膨胀效应 + + Args: + temperature_change (float): 温度变化 + + Returns: + float: 尺寸变化因子 + """ + thermal_expansion = self.properties.get('thermal_expansion', 0.0) + return 1.0 + thermal_expansion * temperature_change + + def calculate_environmental_effects(self, time_delta, weather_conditions=None): + """ + 计算环境效应对材质的影响 + + Args: + time_delta (float): 时间增量 + weather_conditions (dict): 天气条件 {'rain': float, 'wind': float, 'humidity': float, 'uv': float} + + Returns: + dict: 环境效应结果 + """ + if weather_conditions is None: + weather_conditions = {'rain': 0.0, 'wind': 0.0, 'humidity': 0.5, 'uv': 0.0} + + # 获取材质属性 + weather_resistance = self.properties.get('weather_resistance', 1.0) + corrosion_rate = self.properties.get('corrosion_rate', 0.0) + uv_resistance = self.properties.get('uv_resistance', 1.0) + aging_rate = self.properties.get('aging_rate', 0.0) + + # 雨水影响 + rain_intensity = weather_conditions.get('rain', 0.0) + wetness_increase = rain_intensity * (1.0 - weather_resistance) * time_delta + self.update_state(wetness=min(1.0, self.state['wetness'] + wetness_increase)) + + # 湿度影响 + humidity = weather_conditions.get('humidity', 0.5) + humidity_effect = (humidity - 0.5) * 0.1 * time_delta + self.update_state(humidity=max(0.0, min(100.0, self.state['humidity'] + humidity_effect))) + + # 腐蚀影响 + corrosion_increase = rain_intensity * corrosion_rate * (1.0 - weather_resistance) * time_delta + self.update_state(contamination=min(1.0, self.state['contamination'] + corrosion_increase)) + + # UV老化影响 + uv_intensity = weather_conditions.get('uv', 0.0) + uv_damage = uv_intensity * aging_rate * (1.0 - uv_resistance) * time_delta + self.update_state(wear=min(1.0, self.state['wear'] + uv_damage)) + + # 风力影响(清洁效果) + wind_speed = weather_conditions.get('wind', 0.0) + dirt_removal = wind_speed * 0.01 * time_delta + self.update_state(dirtiness=max(0.0, self.state['dirtiness'] - dirt_removal)) + + return { + 'wetness_change': wetness_increase, + 'humidity_change': humidity_effect, + 'corrosion_change': corrosion_increase, + 'uv_damage': uv_damage, + 'dirt_removal': dirt_removal + } + + def simulate_aging(self, time_delta, environmental_factors=None): + """ + 模拟材质老化过程 + + Args: + time_delta (float): 时间增量 + environmental_factors (dict): 环境因素 + + Returns: + dict: 老化效应 + """ + if environmental_factors is None: + environmental_factors = {} + + aging_rate = self.properties.get('aging_rate', 0.0) + weather_resistance = self.properties.get('weather_resistance', 1.0) + + # 基础老化 + base_aging = aging_rate * time_delta + + # 环境加速老化 + environmental_aging = 0.0 + for factor, intensity in environmental_factors.items(): + if factor == 'temperature': + # 高温加速老化 + if intensity > 50: # 超过50度 + environmental_aging += (intensity - 50) * 0.001 * time_delta + elif factor == 'humidity': + # 高湿影响 + if intensity > 0.8: # 湿度超过80% + environmental_aging += (intensity - 0.8) * 0.1 * time_delta + elif factor == 'chemical_exposure': + # 化学暴露 + environmental_aging += intensity * 0.05 * time_delta + + # 总老化效应 + total_aging = base_aging + environmental_aging + + # 应用老化效应到材质属性 + current_wear = self.state['wear'] + new_wear = min(1.0, current_wear + total_aging) + self.update_state(wear=new_wear) + + # 老化对物理属性的影响 + aging_factor = new_wear # 简化处理,磨损程度等于老化程度 + + # 老化对摩擦系数的影响 + friction_change = aging_factor * 0.1 # 老化增加摩擦变化 + + # 老化对弹性的影响 + restitution_degradation = aging_factor * 0.2 # 老化降低弹性 + + # 更新老化历史 + current_time = time.time() + self.history['aging_history'].append((total_aging, current_time)) + if len(self.history['aging_history']) > 100: + self.history['aging_history'].pop(0) + + return { + 'aging_amount': total_aging, + 'friction_change': friction_change, + 'restitution_degradation': restitution_degradation, + 'new_wear_level': new_wear + } + + def get_aging_stats(self): + """ + 获取老化统计数据 + + Returns: + dict: 老化统计数据 + """ + if not self.history['aging_history']: + return {'total_aging': 0.0, 'average_aging_rate': 0.0} + + total_aging = sum([aging for aging, _ in self.history['aging_history']]) + avg_aging_rate = total_aging / len(self.history['aging_history']) + + return { + 'total_aging': total_aging, + 'average_aging_rate': avg_aging_rate, + 'current_wear_level': self.state['wear'] + } + + def calculate_wear_rate(self, relative_velocity, normal_force, time_delta): + """ + 计算磨损率 + + Args: + relative_velocity (Vec3): 相对速度 + normal_force (float): 法向力 + time_delta (float): 时间增量 + + Returns: + float: 磨损率 + """ + speed = relative_velocity.length() + pressure = normal_force # 简化处理 + + # 磨损率 = k * 压力 * 速度 * 时间 + wear_constant = 1e-6 # 磨损常数 + wear_rate = wear_constant * pressure * speed * time_delta + + # 考虑材质硬度影响 + hardness = self.properties.get('hardness', 1.0) + wear_rate *= (1.0 - hardness * 0.5) + + # 考虑环境因素 + wetness_factor = 1.0 + self.state['wetness'] * 0.2 # 湿润可能增加磨损 + wear_rate *= wetness_factor + + return wear_rate + + def calculate_fatigue_accumulation(self, stress_amplitude, cycles, material_properties=None): + """ + 计算疲劳累积(基于Palmgren-Miner线性累积损伤法则) + + Args: + stress_amplitude (float): 应力幅值 + cycles (int): 循环次数 + material_properties (dict): 材料特性参数 + + Returns: + float: 疲劳损伤累积 + """ + if material_properties is None: + material_properties = {} + + # 材料的疲劳参数 + fatigue_strength_coefficient = material_properties.get('fatigue_strength_coefficient', + self.properties.get('fatigue_strength_coefficient', 1000.0)) + fatigue_strength_exponent = material_properties.get('fatigue_strength_exponent', + self.properties.get('fatigue_strength_exponent', -0.1)) + fatigue_ductility_coefficient = material_properties.get('fatigue_ductility_coefficient', + self.properties.get('fatigue_ductility_coefficient', 1.0)) + fatigue_ductility_exponent = material_properties.get('fatigue_ductility_exponent', + self.properties.get('fatigue_ductility_exponent', -0.6)) + + # 计算疲劳寿命(N) + # 简化的疲劳寿命计算模型 + if stress_amplitude > 0: + # 基于应力幅值的疲劳寿命估算 + N = (fatigue_strength_coefficient / stress_amplitude) ** (1 / abs(fatigue_strength_exponent)) + else: + N = 1e10 # 极大值表示几乎不会疲劳 + + # Palmgren-Miner线性累积损伤法则 + # D = Σ(ni/Ni) ,其中D为累积损伤,ni为实际循环次数,Ni为对应应力下的疲劳寿命 + damage_per_cycle = 1.0 / N if N > 0 else 0 + total_damage = damage_per_cycle * cycles + + # 更新疲劳状态 + current_fatigue = self.state['fatigue'] + new_fatigue = min(1.0, current_fatigue + total_damage) + self.update_state(fatigue=new_fatigue) + + # 记录疲劳历史 + current_time = time.time() + self.history['fatigue_history'].append((total_damage, current_time)) + if len(self.history['fatigue_history']) > 100: + self.history['fatigue_history'].pop(0) + + return { + 'damage_per_cycle': damage_per_cycle, + 'total_damage': total_damage, + 'fatigue_life_cycles': N, + 'new_fatigue_level': new_fatigue + } + + def get_fatigue_stats(self): + """ + 获取疲劳统计数据 + + Returns: + dict: 疲劳统计数据 + """ + if not self.history['fatigue_history']: + return {'total_fatigue_damage': 0.0, 'average_fatigue_rate': 0.0} + + total_damage = sum([damage for damage, _ in self.history['fatigue_history']]) + avg_fatigue_rate = total_damage / len(self.history['fatigue_history']) + + return { + 'total_fatigue_damage': total_damage, + 'average_fatigue_rate': avg_fatigue_rate, + 'current_fatigue_level': self.state['fatigue'] + } + + def calculate_electrical_effect(self, time_delta): + """ + 计算电学效应 + + Args: + time_delta (float): 时间增量 + + Returns: + float: 电荷变化 + """ + conductivity = self.properties['electrical_conductivity'] + charge_accumulation = self.properties.get('charge_accumulation', 0.0) + + # 电荷变化 = 积累率 * 时间 - 传导率 * 当前电荷 * 时间 + charge_change = charge_accumulation * time_delta + if self.state['charge'] != 0 and conductivity > 0: + charge_change -= conductivity * self.state['charge'] * time_delta + + return charge_change + + def calculate_fluid_dynamics(self, fluid_velocity, fluid_density, time_delta): + """ + 计算流体动力学效应 + + Args: + fluid_velocity (Vec3): 流体速度 + fluid_density (float): 流体密度 + time_delta (float): 时间增量 + + Returns: + dict: 流体动力学结果 + """ + # 材质属性 + drag_coefficient = self.properties.get('drag_coefficient', 0.5) + lift_coefficient = self.properties.get('lift_coefficient', 0.1) + surface_area = self.properties.get('surface_area', 1.0) + + # 相对速度 + relative_velocity = fluid_velocity # 简化处理,假设材质相对静止 + speed = relative_velocity.length() + + # 阻力计算 (D = 0.5 * ρ * v² * Cd * A) + drag_force = 0.5 * fluid_density * speed * speed * drag_coefficient * surface_area if speed > 0 else 0.0 + + # 升力计算 (L = 0.5 * ρ * v² * Cl * A) + # 简化处理,假设升力方向为垂直于流体流动方向 + lift_force = 0.5 * fluid_density * speed * speed * lift_coefficient * surface_area if speed > 0 else 0.0 + + # 浮力计算 + material_volume = self.properties.get('density', 1000.0) / fluid_density # 简化体积计算 + buoyancy_force = fluid_density * 9.81 * material_volume + + # 更新湿润状态(与流体接触) + wetness_increase = min(1.0, speed * 0.001 * time_delta) + self.update_state(wetness=min(1.0, self.state['wetness'] + wetness_increase)) + + return { + 'drag_force': drag_force, + 'lift_force': lift_force, + 'buoyancy_force': buoyancy_force, + 'wetness_increase': wetness_increase + } + + def calculate_chemical_reaction(self, chemical_composition, temperature=None, time_delta=1.0): + """ + 计算化学反应效应 + + Args: + chemical_composition (dict): 化学成分 {'acid': float, 'base': float, 'oxidizer': float} + temperature (float): 温度(可选,使用当前温度) + time_delta (float): 时间增量 + + Returns: + dict: 化学反应结果 + """ + temp = temperature if temperature is not None else self.state['temperature'] + + # 材质抗性 + chemical_resistance = self.properties.get('chemical_resistance', 1.0) + + # 各类化学物质的影响 + acid_concentration = chemical_composition.get('acid', 0.0) + base_concentration = chemical_composition.get('base', 0.0) + oxidizer_concentration = chemical_composition.get('oxidizer', 0.0) + + # 腐蚀速率计算 + corrosion_rate = self.properties.get('corrosion_rate', 0.0) + + # 酸腐蚀 + acid_damage = acid_concentration * (1.0 - chemical_resistance) * corrosion_rate * time_delta + # 碱腐蚀 + base_damage = base_concentration * (1.0 - chemical_resistance) * corrosion_rate * 0.5 * time_delta + # 氧化 + oxidation_damage = oxidizer_concentration * (1.0 - chemical_resistance) * 0.1 * time_delta + + # 温度加速效应 + temperature_factor = 1.0 + if temp > 25: # 室温以上 + temperature_factor = 1.0 + (temp - 25) * 0.02 # 每度增加2%反应速率 + + total_damage = (acid_damage + base_damage + oxidation_damage) * temperature_factor + + # 更新污染状态 + contamination_increase = min(1.0, total_damage * 0.1) + self.update_state(contamination=min(1.0, self.state['contamination'] + contamination_increase)) + + # 更新磨损状态 + wear_increase = min(1.0, total_damage) + self.update_state(wear=min(1.0, self.state['wear'] + wear_increase)) + + # 记录化学反应历史 + current_time = time.time() + self.history['chemical_history'].append((total_damage, current_time)) + if len(self.history['chemical_history']) > 100: + self.history['chemical_history'].pop(0) + + return { + 'acid_damage': acid_damage * temperature_factor, + 'base_damage': base_damage * temperature_factor, + 'oxidation_damage': oxidation_damage * temperature_factor, + 'total_damage': total_damage, + 'contamination_increase': contamination_increase, + 'wear_increase': wear_increase + } + + def get_chemical_stats(self): + """ + 获取化学反应统计数据 + + Returns: + dict: 化学反应统计数据 + """ + if not self.history['chemical_history']: + return {'total_chemical_damage': 0.0, 'average_chemical_rate': 0.0} + + total_damage = sum([damage for damage, _ in self.history['chemical_history']]) + avg_chemical_rate = total_damage / len(self.history['chemical_history']) + + return { + 'total_chemical_damage': total_damage, + 'average_chemical_rate': avg_chemical_rate, + 'current_contamination_level': self.state['contamination'] + } + + def check_failure_criteria(self, stress_tensor, strain_tensor=None): + """ + 检查材质失效准则 + + Args: + stress_tensor (list): 应力张量 [σxx, σyy, σzz, σxy, σyz, σzx] + strain_tensor (list): 应变张量 [εxx, εyy, εzz, εxy, εyz, εzx](可选) + + Returns: + dict: 失效分析结果 + """ + # 材质强度参数 + tensile_strength = self.properties.get('tensile_strength', 1000.0) + compressive_strength = self.properties.get('compressive_strength', 1000.0) + shear_strength = self.properties.get('shear_strength', 500.0) + fatigue_limit = self.properties.get('fatigue_limit', 200.0) + + # 提取应力分量 + sigma_xx, sigma_yy, sigma_zz = stress_tensor[0], stress_tensor[1], stress_tensor[2] + sigma_xy, sigma_yz, sigma_zx = stress_tensor[3], stress_tensor[4], stress_tensor[5] + + # 计算主应力 + # 简化处理,只计算最大主应力 + max_principal_stress = max(sigma_xx, sigma_yy, sigma_zz) + min_principal_stress = min(sigma_xx, sigma_yy, sigma_zz) + + # 计算等效应力(von Mises应力) + deviatoric_stress = [ + sigma_xx - (sigma_xx + sigma_yy + sigma_zz) / 3, + sigma_yy - (sigma_xx + sigma_yy + sigma_zz) / 3, + sigma_zz - (sigma_xx + sigma_yy + sigma_zz) / 3, + sigma_xy, sigma_yz, sigma_zx + ] + + von_mises_stress = math.sqrt( + 0.5 * ( + deviatoric_stress[0]**2 + deviatoric_stress[1]**2 + deviatoric_stress[2]**2 + + 2 * (deviatoric_stress[3]**2 + deviatoric_stress[4]**2 + deviatoric_stress[5]**2) + ) + ) + + # 检查各种失效准则 + failures = {} + + # 1. 最大拉应力准则 + if max_principal_stress > tensile_strength: + failures['tensile_failure'] = { + 'safety_factor': tensile_strength / max_principal_stress if max_principal_stress > 0 else 0, + 'excess_stress': max_principal_stress - tensile_strength + } + + # 2. 最大压应力准则 + if min_principal_stress < -compressive_strength: + failures['compressive_failure'] = { + 'safety_factor': compressive_strength / abs(min_principal_stress) if min_principal_stress < 0 else 0, + 'excess_stress': abs(min_principal_stress) - compressive_strength + } + + # 3. 最大剪应力准则(Tresca准则) + max_shear_stress = (max_principal_stress - min_principal_stress) / 2 + if max_shear_stress > shear_strength: + failures['shear_failure'] = { + 'safety_factor': shear_strength / max_shear_stress if max_shear_stress > 0 else 0, + 'excess_stress': max_shear_stress - shear_strength + } + + # 4. von Mises失效准则 + if von_mises_stress > tensile_strength: # 简化处理,使用拉伸强度作为屈服强度 + failures['von_mises_failure'] = { + 'safety_factor': tensile_strength / von_mises_stress if von_mises_stress > 0 else 0, + 'excess_stress': von_mises_stress - tensile_strength + } + + # 5. 疲劳失效准则 + current_fatigue = self.state['fatigue'] + if current_fatigue > 0.9: # 疲劳度超过90%时可能发生疲劳失效 + fatigue_damage = current_fatigue * fatigue_limit + if von_mises_stress > fatigue_limit: + failures['fatigue_failure'] = { + 'safety_factor': fatigue_limit / von_mises_stress if von_mises_stress > 0 else 0, + 'fatigue_level': current_fatigue + } + + # 6. 蠕变失效准则(高温下) + temperature = self.state['temperature'] + if temperature > 0.5 * self.properties.get('melting_point', 1000.0): # 超过熔点的一半 + creep_threshold = tensile_strength * 0.1 # 蠕变阈值 + if von_mises_stress > creep_threshold: + failures['creep_failure'] = { + 'temperature_factor': temperature / self.properties.get('melting_point', 1000.0), + 'stress_ratio': von_mises_stress / creep_threshold + } + + # 更新应力历史 + current_time = time.time() + self.history['stress_history'].append((von_mises_stress, current_time)) + if len(self.history['stress_history']) > 100: + self.history['stress_history'].pop(0) + + # 更新应力状态 + self.update_state(stress=max(von_mises_stress, self.state['stress'])) + + return { + 'principal_stresses': [max_principal_stress, min_principal_stress], + 'von_mises_stress': von_mises_stress, + 'failures': failures, + 'safety_margins': self._calculate_safety_margins( + max_principal_stress, min_principal_stress, von_mises_stress, + tensile_strength, compressive_strength, shear_strength + ) + } + + def _calculate_safety_margins(self, max_stress, min_stress, von_mises_stress, + tensile_strength, compressive_strength, shear_strength): + """ + 计算安全裕度 + + Args: + max_stress (float): 最大主应力 + min_stress (float): 最小主应力 + von_mises_stress (float): von Mises应力 + tensile_strength (float): 抗拉强度 + compressive_strength (float): 抗压强度 + shear_strength (float): 抗剪强度 + + Returns: + dict: 安全裕度 + """ + safety_margins = {} + + # 拉伸安全裕度 + if max_stress > 0: + safety_margins['tensile'] = tensile_strength / max_stress if max_stress > 0 else float('inf') + else: + safety_margins['tensile'] = float('inf') + + # 压缩安全裕度 + if min_stress < 0: + safety_margins['compressive'] = compressive_strength / abs(min_stress) if min_stress < 0 else float('inf') + else: + safety_margins['compressive'] = float('inf') + + # 剪切安全裕度 + max_shear = (max_stress - min_stress) / 2 + safety_margins['shear'] = shear_strength / max_shear if max_shear > 0 else float('inf') + + # von Mises安全裕度 + safety_margins['von_mises'] = tensile_strength / von_mises_stress if von_mises_stress > 0 else float('inf') + + return safety_margins + + def simulate_fracture(self, impact_energy, impact_location): + """ + 模拟材质断裂 + + Args: + impact_energy (float): 撞击能量 + impact_location (Point3): 撞击位置 + + Returns: + dict: 断裂模拟结果 + """ + # 材质韧性参数 + fracture_toughness = self.properties.get('fracture_toughness', 50.0) + toughness = self.properties.get('toughness', 0.1) + + # 计算断裂概率 + # 基于能量密度和材质韧性 + critical_energy = fracture_toughness * 100 # 简化的临界能量 + energy_density = impact_energy / (self.properties.get('density', 1000.0) * 0.001) # 简化的能量密度 + + fracture_probability = 1.0 - math.exp(-energy_density / critical_energy) + + # 考虑材质韧性降低断裂概率 + fracture_probability *= (1.0 - toughness) + + # 考虑已有损伤状态 + current_wear = self.state['wear'] + current_fatigue = self.state['fatigue'] + fracture_probability *= (1.0 + current_wear * 0.5 + current_fatigue * 0.3) + + # 确定是否发生断裂 + import random + fracture_occurred = random.random() < min(1.0, fracture_probability) + + if fracture_occurred: + # 计算断裂严重程度 + severity = min(1.0, energy_density / critical_energy) + + # 更新材质状态 + self.update_state( + wear=min(1.0, self.state['wear'] + severity * 0.3), + stress=max(0.0, self.state['stress'] - severity * 100), # 断裂释放应力 + fatigue=min(1.0, self.state['fatigue'] + severity * 0.2) + ) + + # 记录断裂事件 + current_time = time.time() + fracture_event = { + 'time': current_time, + 'energy': impact_energy, + 'location': impact_location, + 'severity': severity, + 'probability': fracture_probability + } + + if 'fracture_history' not in self.history: + self.history['fracture_history'] = [] + + self.history['fracture_history'].append(fracture_event) + if len(self.history['fracture_history']) > 50: + self.history['fracture_history'].pop(0) + + return { + 'fracture_occurred': True, + 'severity': severity, + 'probability': fracture_probability, + 'critical_energy': critical_energy, + 'energy_density': energy_density, + 'fracture_event': fracture_event + } + else: + return { + 'fracture_occurred': False, + 'probability': fracture_probability, + 'critical_energy': critical_energy, + 'energy_density': energy_density + } + + def get_structural_integrity(self): + """ + 获取结构完整性评估 + + Returns: + dict: 结构完整性评估结果 + """ + # 综合评估各种损伤因素 + wear_factor = 1.0 - self.state['wear'] + fatigue_factor = 1.0 - self.state['fatigue'] + contamination_factor = 1.0 - self.state['contamination'] * 0.5 + stress_factor = max(0.1, 1.0 - self.state['stress'] / 10000.0) # 假设10000为临界应力 + + # 结构完整性 = 各因素加权平均 + integrity = ( + wear_factor * 0.3 + + fatigue_factor * 0.3 + + contamination_factor * 0.2 + + stress_factor * 0.2 + ) + + # 确保完整性在合理范围内 + integrity = max(0.0, min(1.0, integrity)) + + # 评估等级 + if integrity > 0.9: + integrity_level = "Excellent" + elif integrity > 0.7: + integrity_level = "Good" + elif integrity > 0.5: + integrity_level = "Fair" + elif integrity > 0.3: + integrity_level = "Poor" + else: + integrity_level = "Critical" + + return { + 'integrity': integrity, + 'integrity_level': integrity_level, + 'factors': { + 'wear_factor': wear_factor, + 'fatigue_factor': fatigue_factor, + 'contamination_factor': contamination_factor, + 'stress_factor': stress_factor + }, + 'recommendations': self._generate_maintenance_recommendations(integrity) + } + + def _generate_maintenance_recommendations(self, integrity): + """ + 生成维护建议 + + Args: + integrity (float): 结构完整性 + + Returns: + list: 维护建议列表 + """ + recommendations = [] + + if integrity < 0.3: + recommendations.append("URGENT: Immediate replacement required") + elif integrity < 0.5: + recommendations.append("Replace as soon as possible") + elif integrity < 0.7: + recommendations.append("Schedule inspection and maintenance") + + # 基于具体因素的建议 + if self.state['wear'] > 0.7: + recommendations.append("High wear detected - consider replacement") + if self.state['fatigue'] > 0.8: + recommendations.append("High fatigue level - reduce load or replace") + if self.state['contamination'] > 0.6: + recommendations.append("High contamination - cleaning recommended") + if self.state['stress'] > 5000: # 假设5000为高应力阈值 + recommendations.append("High stress levels detected - check loading conditions") + + return recommendations + + def get_history_stats(self): + """ + 获取历史统计数据 + + Returns: + dict: 历史统计数据 + """ + stats = {} + + # 温度统计 + if self.history['temperature_history']: + temps = [temp for temp, _ in self.history['temperature_history']] + stats['temperature'] = { + 'min': min(temps), + 'max': max(temps), + 'avg': sum(temps) / len(temps) + } + + # 磨损统计 + if self.history['wear_history']: + wears = [wear for wear, _ in self.history['wear_history']] + stats['wear'] = { + 'min': min(wears), + 'max': max(wears), + 'avg': sum(wears) / len(wears) + } + + # 应力统计 + if self.history['stress_history']: + stresses = [stress for stress, _ in self.history['stress_history']] + stats['stress'] = { + 'min': min(stresses), + 'max': max(stresses), + 'avg': sum(stresses) / len(stresses) + } + + return stats + + def to_dict(self): + """ + 将材质转换为字典格式 + + Returns: + dict: 材质属性字典 + """ + # 转换不可序列化的对象 + props = self.properties.copy() + if isinstance(props['anisotropic_friction'], Vec3): + props['anisotropic_friction'] = [ + props['anisotropic_friction'].getX(), + props['anisotropic_friction'].getY(), + props['anisotropic_friction'].getZ() + ] + if isinstance(props['color'], Vec4): + props['color'] = [ + props['color'].getX(), + props['color'].getY(), + props['color'].getZ(), + props['color'].getW() + ] + if isinstance(props['surface_velocity'], Vec3): + props['surface_velocity'] = [ + props['surface_velocity'].getX(), + props['surface_velocity'].getY(), + props['surface_velocity'].getZ() + ] + if isinstance(props['emissive'], Vec3): + props['emissive'] = [ + props['emissive'].getX(), + props['emissive'].getY(), + props['emissive'].getZ() + ] + + # 转换交互矩阵 + interaction_matrix = {} + for key, value in self.interaction_matrix.items(): + interaction_matrix[key] = value + + # 转换历史记录 + history = {} + for key, value in self.history.items(): + history[key] = [(v, t) for v, t in value] + + return { + 'name': self.name, + 'material_type': self.material_type, + 'properties': props, + 'state': self.state.copy(), + 'interaction_matrix': interaction_matrix, + 'history': history, + 'creation_time': self.creation_time + } + + @classmethod + def from_dict(cls, data): + """ + 从字典创建材质对象 + + Args: + data (dict): 材质数据字典 + + Returns: + PhysicsMaterial: 材质对象 + """ + # 转换序列化数据回对象 + properties = data.get('properties', {}).copy() + if 'anisotropic_friction' in properties and isinstance(properties['anisotropic_friction'], list): + properties['anisotropic_friction'] = Vec3(*properties['anisotropic_friction']) + if 'color' in properties and isinstance(properties['color'], list): + properties['color'] = Vec4(*properties['color']) + if 'surface_velocity' in properties and isinstance(properties['surface_velocity'], list): + properties['surface_velocity'] = Vec3(*properties['surface_velocity']) + if 'emissive' in properties and isinstance(properties['emissive'], list): + properties['emissive'] = Vec3(*properties['emissive']) + + material = cls(data['name'], properties, data.get('material_type', cls.TYPE_DEFAULT)) + material.state = data.get('state', material.state) + material.creation_time = data.get('creation_time', time.time()) + + # 恢复交互矩阵 + interaction_matrix = data.get('interaction_matrix', {}) + for key, value in interaction_matrix.items(): + material.interaction_matrix[key] = value + + # 恢复历史记录 + history = data.get('history', {}) + for key, value in history.items(): + if key in material.history: + material.history[key] = [(v, t) for v, t in value] + + return material + + def save_to_file(self, filepath): + """ + 保存材质到文件 + + Args: + filepath (str): 文件路径 + """ + data = self.to_dict() + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(data, f, indent=2, ensure_ascii=False) + + @classmethod + def load_from_file(cls, filepath): + """ + 从文件加载材质 + + Args: + filepath (str): 文件路径 + + Returns: + PhysicsMaterial: 材质对象 + """ + with open(filepath, 'r', encoding='utf-8') as f: + data = json.load(f) + return cls.from_dict(data) + + def __str__(self): + """ + 字符串表示 + """ + return f"PhysicsMaterial('{self.name}', type={self.material_type}, friction={self.properties['friction']}, restitution={self.properties['restitution']})" + + def __repr__(self): + """ + 详细字符串表示 + """ + return self.__str__() + + +# 预定义常用材质 +MATERIALS = { + 'concrete': PhysicsMaterial('concrete', material_type=PhysicsMaterial.TYPE_CONCRETE, properties={ + 'friction': 0.7, + 'restitution': 0.1, + 'density': 2400.0, + 'color': Vec4(0.6, 0.6, 0.6, 1.0), + 'roughness': 0.9, + 'weather_resistance': 0.9, + 'compressive_strength': 20000.0, + 'tensile_strength': 2000.0, + 'shear_strength': 1000.0, + 'thermal_conductivity': 1.0, + 'specific_heat': 880.0, + 'thermal_expansion': 0.00001, + 'fatigue_strength_coefficient': 8000.0, + 'fatigue_strength_exponent': -0.1, + }), + 'wood': PhysicsMaterial('wood', material_type=PhysicsMaterial.TYPE_WOOD, properties={ + 'friction': 0.4, + 'restitution': 0.2, + 'density': 700.0, + 'color': Vec4(0.8, 0.6, 0.4, 1.0), + 'thermal_conductivity': 0.1, + 'flammability': 0.8, + 'roughness': 0.7, + 'aging_rate': 0.05, + 'tensile_strength': 1000.0, + 'compressive_strength': 5000.0, + 'shear_strength': 800.0, + 'specific_heat': 1700.0, + 'thermal_expansion': 0.000005, + 'hardness': 0.6, + 'chemical_resistance': 0.7, + }), + 'metal': PhysicsMaterial('metal', material_type=PhysicsMaterial.TYPE_METAL, properties={ + 'friction': 0.3, + 'restitution': 0.4, + 'density': 7800.0, + 'color': Vec4(0.7, 0.7, 0.8, 1.0), + 'thermal_conductivity': 80.0, + 'electrical_conductivity': 1e7, + 'roughness': 0.3, + 'metallic': 1.0, + 'specular': 0.9, + 'corrosion_rate': 0.1, + 'tensile_strength': 40000.0, + 'compressive_strength': 40000.0, + 'shear_strength': 20000.0, + 'specific_heat': 490.0, + 'thermal_expansion': 0.000012, + 'fatigue_limit': 20000.0, + 'fracture_toughness': 100.0, + 'hardness': 0.9, + }), + 'rubber': PhysicsMaterial('rubber', material_type=PhysicsMaterial.TYPE_RUBBER, properties={ + 'friction': 0.9, + 'restitution': 0.8, + 'density': 1200.0, + 'color': Vec4(0.2, 0.2, 0.2, 1.0), + 'roughness': 0.8, + 'specular': 0.1, + 'elasticity': 0.9, + 'tensile_strength': 500.0, + 'compressive_strength': 1000.0, + 'shear_strength': 300.0, + 'specific_heat': 1500.0, + 'thermal_conductivity': 0.2, + 'damping': 0.3, + 'toughness': 0.2, + }), + 'ice': PhysicsMaterial('ice', material_type=PhysicsMaterial.TYPE_ICE, properties={ + 'friction': 0.03, + 'restitution': 0.1, + 'density': 917.0, + 'color': Vec4(0.8, 0.9, 1.0, 0.8), + 'thermal_conductivity': 2.2, + 'roughness': 0.1, + 'specular': 0.9, + 'melting_point': 0.0, + 'compressive_strength': 500.0, + 'tensile_strength': 100.0, + 'specific_heat': 2100.0, + 'thermal_expansion': 0.00005, + }), + 'sand': PhysicsMaterial('sand', material_type=PhysicsMaterial.TYPE_SAND, properties={ + 'friction': 0.6, + 'restitution': 0.05, + 'density': 1600.0, + 'color': Vec4(0.9, 0.8, 0.6, 1.0), + 'roughness': 0.9, + 'flowability': 0.8, + 'compressive_strength': 100.0, + 'tensile_strength': 10.0, + 'specific_heat': 830.0, + 'thermal_conductivity': 0.25, + }), + 'water': PhysicsMaterial('water', material_type=PhysicsMaterial.TYPE_WATER, properties={ + 'friction': 0.1, + 'restitution': 0.0, + 'density': 1000.0, + 'color': Vec4(0.0, 0.5, 1.0, 0.7), + 'viscosity': 0.001, + 'surface_tension': 0.072, + 'buoyancy': 1.0, + 'transparency': 0.8, + 'compressive_strength': 2000000.0, # 水几乎不可压缩 + 'specific_heat': 4186.0, + 'thermal_conductivity': 0.6, + }), + 'glass': PhysicsMaterial('glass', material_type=PhysicsMaterial.TYPE_GLASS, properties={ + 'friction': 0.2, + 'restitution': 0.9, + 'density': 2500.0, + 'color': Vec4(0.8, 0.9, 1.0, 0.3), + 'roughness': 0.1, + 'specular': 0.95, + 'transparency': 0.9, + 'fragility': 0.8, + 'tensile_strength': 7000.0, + 'compressive_strength': 10000.0, + 'shear_strength': 3000.0, + 'specific_heat': 840.0, + 'thermal_conductivity': 1.0, + 'fracture_toughness': 20.0, + }), + 'flesh': PhysicsMaterial('flesh', material_type=PhysicsMaterial.TYPE_FLESH, properties={ + 'friction': 0.8, + 'restitution': 0.1, + 'density': 1060.0, + 'color': Vec4(0.9, 0.7, 0.7, 1.0), + 'roughness': 0.6, + 'toxicity': 0.1, + 'elasticity': 0.3, + 'tensile_strength': 50.0, + 'compressive_strength': 200.0, + 'shear_strength': 30.0, + 'specific_heat': 3500.0, + 'thermal_conductivity': 0.3, + 'damping': 0.2, + }), + 'plastic': PhysicsMaterial('plastic', material_type=PhysicsMaterial.TYPE_PLASTIC, properties={ + 'friction': 0.4, + 'restitution': 0.3, + 'density': 1200.0, + 'color': Vec4(0.9, 0.9, 0.9, 1.0), + 'roughness': 0.5, + 'specular': 0.6, + 'flammability': 0.3, + 'elasticity': 0.5, + 'tensile_strength': 5000.0, + 'compressive_strength': 8000.0, + 'shear_strength': 3000.0, + 'specific_heat': 1300.0, + 'thermal_conductivity': 0.2, + 'chemical_resistance': 0.8, + }), + 'cloth': PhysicsMaterial('cloth', material_type=PhysicsMaterial.TYPE_CLOTH, properties={ + 'friction': 0.6, + 'restitution': 0.1, + 'density': 500.0, + 'color': Vec4(0.8, 0.8, 0.8, 1.0), + 'roughness': 0.8, + 'flexibility': 0.9, + 'breathability': 0.7, + 'tensile_strength': 200.0, + 'specific_heat': 1200.0, + 'thermal_conductivity': 0.05, + 'damping': 0.1, + }), + 'leather': PhysicsMaterial('leather', material_type=PhysicsMaterial.TYPE_LEATHER, properties={ + 'friction': 0.5, + 'restitution': 0.2, + 'density': 900.0, + 'color': Vec4(0.5, 0.3, 0.2, 1.0), + 'roughness': 0.6, + 'durability': 0.8, + 'water_resistance': 0.6, + 'tensile_strength': 800.0, + 'specific_heat': 1500.0, + 'thermal_conductivity': 0.15, + 'aging_rate': 0.02, + }), + 'stone': PhysicsMaterial('stone', material_type=PhysicsMaterial.TYPE_STONE, properties={ + 'friction': 0.7, + 'restitution': 0.1, + 'density': 2600.0, + 'color': Vec4(0.7, 0.7, 0.7, 1.0), + 'roughness': 0.8, + 'hardness': 0.9, + 'weather_resistance': 0.95, + 'tensile_strength': 3000.0, + 'compressive_strength': 20000.0, + 'shear_strength': 1000.0, + 'specific_heat': 800.0, + 'thermal_conductivity': 2.0, + }), + 'snow': PhysicsMaterial('snow', material_type=PhysicsMaterial.TYPE_SNOW, properties={ + 'friction': 0.2, + 'restitution': 0.05, + 'density': 100.0, + 'color': Vec4(1.0, 1.0, 1.0, 0.9), + 'roughness': 0.3, + 'melting_point': 0.0, + 'compressibility': 0.5, + 'tensile_strength': 50.0, + 'specific_heat': 2100.0, + 'thermal_conductivity': 0.3, + }), + 'mud': PhysicsMaterial('mud', material_type=PhysicsMaterial.TYPE_MUD, properties={ + 'friction': 0.8, + 'restitution': 0.01, + 'density': 1300.0, + 'color': Vec4(0.4, 0.3, 0.2, 1.0), + 'roughness': 0.9, + 'stickiness': 0.7, + 'viscosity': 0.1, + 'tensile_strength': 20.0, + 'specific_heat': 1800.0, + 'thermal_conductivity': 0.8, + }), +} + + +# 材质交互预设 +MATERIAL_INTERACTIONS = { + ('metal', 'metal'): 1.2, + ('metal', 'wood'): 0.8, + ('metal', 'concrete'): 0.9, + ('metal', 'rubber'): 1.1, + ('wood', 'wood'): 1.0, + ('wood', 'concrete'): 0.7, + ('concrete', 'concrete'): 1.0, + ('rubber', 'rubber'): 1.3, + ('ice', 'ice'): 0.1, + ('ice', 'metal'): 0.15, + ('sand', 'sand'): 0.9, + ('water', 'metal'): 0.1, + ('water', 'wood'): 0.2, + ('glass', 'glass'): 0.95, + ('plastic', 'plastic'): 0.6, + ('cloth', 'cloth'): 0.8, + ('leather', 'leather'): 0.7, + ('stone', 'stone'): 0.85, + ('snow', 'snow'): 0.3, + ('mud', 'mud'): 0.4, } \ No newline at end of file diff --git a/plugins/user/rigid_body_physics/core/material_advanced.py b/plugins/user/rigid_body_physics/core/material_advanced.py index 5b195691..122edb80 100644 --- a/plugins/user/rigid_body_physics/core/material_advanced.py +++ b/plugins/user/rigid_body_physics/core/material_advanced.py @@ -1,306 +1,306 @@ -""" -物理材质系统高级功能扩展模块 -提供更复杂的物理材质功能 -""" - -from panda3d.core import Vec3, Vec4, Point3 -import math -import time -from collections import defaultdict - - -class PhysicsMaterialAdvanced: - """ - 物理材质高级功能扩展类 - 提供更复杂的物理材质功能 - """ - - @staticmethod - def calculate_thermal_effect_advanced(material, time_delta, environment_temp=20.0, heat_sources=None): - """ - 计算高级热效应 - - Args: - material (PhysicsMaterial): 材质对象 - time_delta (float): 时间增量 - environment_temp (float): 环境温度 - heat_sources (list): 热源列表 [{'position': Point3, 'temperature': float, 'radius': float}] - - Returns: - dict: 温度变化和相关效应 - """ - current_temp = material.state['temperature'] - conductivity = material.properties['thermal_conductivity'] - specific_heat = material.properties['specific_heat'] - heat_generation = material.properties.get('heat_generation', 0.0) - thermal_expansion = material.properties.get('thermal_expansion', 0.0) - - # 基础热传导计算 - temp_difference = environment_temp - current_temp - heat_transfer = temp_difference * conductivity * time_delta - - # 内部热量生成 - internal_heat = heat_generation * time_delta - - # 外部热源影响 - external_heat = 0.0 - if heat_sources: - for source in heat_sources: - # 简化的热源影响计算 - source_temp = source.get('temperature', 0.0) - source_radius = source.get('radius', 1.0) - distance_factor = max(0.1, source_radius) # 简化距离影响 - external_heat += (source_temp - current_temp) * 0.1 / distance_factor * time_delta - - # 温度变化 = (热传导 + 内部热量 + 外部热源) / (质量 * 比热容) - # 这里假设单位质量为1kg - temp_change = (heat_transfer + internal_heat + external_heat) / specific_heat if specific_heat > 0 else 0 - - # 考虑相变(简化实现) - melting_point = material.properties.get('melting_point', 1000.0) - phase_change_effect = 1.0 - if current_temp < melting_point <= current_temp + temp_change: - # 材质开始熔化,吸收潜热 - latent_heat = material.properties.get('latent_heat_of_fusion', 0.0) - if latent_heat > 0: - temp_change *= 0.1 # 熔化过程中温度变化减缓 - phase_change_effect = 0.5 # 相变期间物理属性变化 - - # 热膨胀效应 - expansion_factor = 1.0 + thermal_expansion * temp_change - - return { - 'temperature_change': temp_change, - 'expansion_factor': expansion_factor, - 'phase_change_effect': phase_change_effect, - 'total_heat': heat_transfer + internal_heat + external_heat - } - - @staticmethod - def calculate_environmental_effects(material, time_delta, weather_conditions=None): - """ - 计算环境效应对材质的影响 - - Args: - material (PhysicsMaterial): 材质对象 - time_delta (float): 时间增量 - weather_conditions (dict): 天气条件 {'rain': float, 'wind': float, 'humidity': float, 'uv': float} - - Returns: - dict: 环境效应结果 - """ - if weather_conditions is None: - weather_conditions = {'rain': 0.0, 'wind': 0.0, 'humidity': 0.5, 'uv': 0.0} - - # 获取材质属性 - weather_resistance = material.properties.get('weather_resistance', 1.0) - corrosion_rate = material.properties.get('corrosion_rate', 0.0) - uv_resistance = material.properties.get('uv_resistance', 1.0) - aging_rate = material.properties.get('aging_rate', 0.0) - - # 雨水影响 - rain_intensity = weather_conditions.get('rain', 0.0) - wetness_increase = rain_intensity * (1.0 - weather_resistance) * time_delta - material.update_state(wetness=min(1.0, material.state['wetness'] + wetness_increase)) - - # 湿度影响 - humidity = weather_conditions.get('humidity', 0.5) - humidity_effect = (humidity - 0.5) * 0.1 * time_delta - material.update_state(humidity=max(0.0, min(100.0, material.state['humidity'] + humidity_effect))) - - # 腐蚀影响 - corrosion_increase = rain_intensity * corrosion_rate * (1.0 - weather_resistance) * time_delta - material.update_state(contamination=min(1.0, material.state['contamination'] + corrosion_increase)) - - # UV老化影响 - uv_intensity = weather_conditions.get('uv', 0.0) - uv_damage = uv_intensity * aging_rate * (1.0 - uv_resistance) * time_delta - material.update_state(wear=min(1.0, material.state['wear'] + uv_damage)) - - # 风力影响(清洁效果) - wind_speed = weather_conditions.get('wind', 0.0) - dirt_removal = wind_speed * 0.01 * time_delta - material.update_state(dirtiness=max(0.0, material.state['dirtiness'] - dirt_removal)) - - return { - 'wetness_change': wetness_increase, - 'humidity_change': humidity_effect, - 'corrosion_change': corrosion_increase, - 'uv_damage': uv_damage, - 'dirt_removal': dirt_removal - } - - @staticmethod - def calculate_material_interaction(material_a, material_b, velocity, normal_force, time_delta): - """ - 计算两种材质之间的复杂交互 - - Args: - material_a (PhysicsMaterial): 材质A - material_b (PhysicsMaterial): 材质B - velocity (Vec3): 相对速度 - normal_force (float): 法向力 - time_delta (float): 时间增量 - - Returns: - dict: 交互结果 - """ - speed = velocity.length() - - # 获取交互系数 - interaction_coeff = material_a.get_interaction_coefficient(material_b.name) - - # 计算磨损 - wear_a = material_a.calculate_wear_rate(velocity, normal_force, time_delta) - wear_b = material_b.calculate_wear_rate(velocity, normal_force, time_delta) - - # 考虑交互系数 - effective_wear_a = wear_a * interaction_coeff - effective_wear_b = wear_b * interaction_coeff - - # 更新材质状态 - material_a.update_state(wear=min(1.0, material_a.state['wear'] + effective_wear_a)) - material_b.update_state(wear=min(1.0, material_b.state['wear'] + effective_wear_b)) - - # 计算热量生成(摩擦生热) - friction_heat = normal_force * speed * 0.1 * time_delta * interaction_coeff - - # 平分热量 - heat_a = friction_heat * 0.5 - heat_b = friction_heat * 0.5 - - # 更新温度状态 - material_a.update_state(temperature=material_a.state['temperature'] + heat_a * 0.01) - material_b.update_state(temperature=material_b.state['temperature'] + heat_b * 0.01) - - # 计算电荷积累 - charge_a = material_a.calculate_electrical_effect(time_delta) - charge_b = material_b.calculate_electrical_effect(time_delta) - - # 摩擦电效应 - triboelectric_effect = speed * normal_force * 0.001 * time_delta - material_a.update_state(charge=material_a.state['charge'] + triboelectric_effect) - material_b.update_state(charge=material_b.state['charge'] - triboelectric_effect) - - return { - 'wear_a': effective_wear_a, - 'wear_b': effective_wear_b, - 'heat_generated': friction_heat, - 'charge_a': triboelectric_effect, - 'charge_b': -triboelectric_effect, - 'interaction_coefficient': interaction_coeff - } - - @staticmethod - def simulate_material_aging(material, time_delta, environmental_factors=None): - """ - 模拟材质老化过程 - - Args: - material (PhysicsMaterial): 材质对象 - time_delta (float): 时间增量 - environmental_factors (dict): 环境因素 - - Returns: - dict: 老化效应 - """ - if environmental_factors is None: - environmental_factors = {} - - aging_rate = material.properties.get('aging_rate', 0.0) - weather_resistance = material.properties.get('weather_resistance', 1.0) - - # 基础老化 - base_aging = aging_rate * time_delta - - # 环境加速老化 - environmental_aging = 0.0 - for factor, intensity in environmental_factors.items(): - if factor == 'temperature': - # 高温加速老化 - if intensity > 50: # 超过50度 - environmental_aging += (intensity - 50) * 0.001 * time_delta - elif factor == 'humidity': - # 高湿度影响 - if intensity > 0.8: # 湿度超过80% - environmental_aging += (intensity - 0.8) * 0.1 * time_delta - elif factor == 'chemical_exposure': - # 化学暴露 - environmental_aging += intensity * 0.05 * time_delta - - # 总老化效应 - total_aging = base_aging + environmental_aging - - # 应用老化效应到材质属性 - current_wear = material.state['wear'] - new_wear = min(1.0, current_wear + total_aging) - material.update_state(wear=new_wear) - - # 老化对物理属性的影响 - aging_factor = new_wear # 简化处理,磨损程度等于老化程度 - - # 老化对摩擦系数的影响 - friction_change = aging_factor * 0.1 # 老化增加摩擦变化 - - # 老化对弹性的影响 - restitution_degradation = aging_factor * 0.2 # 老化降低弹性 - - return { - 'aging_amount': total_aging, - 'friction_change': friction_change, - 'restitution_degradation': restitution_degradation, - 'new_wear_level': new_wear - } - - @staticmethod - def calculate_fluid_interaction(material, fluid_properties, velocity, time_delta): - """ - 计算材质与流体的交互 - - Args: - material (PhysicsMaterial): 材质对象 - fluid_properties (dict): 流体属性 {'density': float, 'viscosity': float, 'temperature': float} - velocity (Vec3): 相对速度 - time_delta (float): 时间增量 - - Returns: - dict: 流体交互结果 - """ - speed = velocity.length() - fluid_density = fluid_properties.get('density', 1000.0) - fluid_viscosity = fluid_properties.get('viscosity', 0.001) - fluid_temp = fluid_properties.get('temperature', 20.0) - - # 浮力计算 - material_density = material.properties['density'] - buoyancy_force = fluid_density * 9.81 * (material_density / fluid_density) - - # 阻力计算 - drag_coefficient = material.properties.get('drag_coefficient', 0.5) - drag_force = 0.5 * fluid_density * speed * speed * drag_coefficient - - # 粘性阻力 - viscous_drag = fluid_viscosity * speed * material.properties.get('surface_area', 1.0) - - # 热交换 - material_temp = material.state['temperature'] - temp_difference = fluid_temp - material_temp - heat_transfer = temp_difference * material.properties.get('thermal_conductivity', 1.0) * time_delta - - # 更新材质温度 - specific_heat = material.properties.get('specific_heat', 1000.0) - if specific_heat > 0: - temp_change = heat_transfer / specific_heat - material.update_state(temperature=material_temp + temp_change) - - # 浸湿效应 - wetness_increase = min(1.0, speed * 0.01 * time_delta) - material.update_state(wetness=min(1.0, material.state['wetness'] + wetness_increase)) - - return { - 'buoyancy_force': buoyancy_force, - 'drag_force': drag_force, - 'viscous_drag': viscous_drag, - 'heat_transfer': heat_transfer, - 'wetness_increase': wetness_increase +""" +物理材质系统高级功能扩展模块 +提供更复杂的物理材质功能 +""" + +from panda3d.core import Vec3, Vec4, Point3 +import math +import time +from collections import defaultdict + + +class PhysicsMaterialAdvanced: + """ + 物理材质高级功能扩展类 + 提供更复杂的物理材质功能 + """ + + @staticmethod + def calculate_thermal_effect_advanced(material, time_delta, environment_temp=20.0, heat_sources=None): + """ + 计算高级热效应 + + Args: + material (PhysicsMaterial): 材质对象 + time_delta (float): 时间增量 + environment_temp (float): 环境温度 + heat_sources (list): 热源列表 [{'position': Point3, 'temperature': float, 'radius': float}] + + Returns: + dict: 温度变化和相关效应 + """ + current_temp = material.state['temperature'] + conductivity = material.properties['thermal_conductivity'] + specific_heat = material.properties['specific_heat'] + heat_generation = material.properties.get('heat_generation', 0.0) + thermal_expansion = material.properties.get('thermal_expansion', 0.0) + + # 基础热传导计算 + temp_difference = environment_temp - current_temp + heat_transfer = temp_difference * conductivity * time_delta + + # 内部热量生成 + internal_heat = heat_generation * time_delta + + # 外部热源影响 + external_heat = 0.0 + if heat_sources: + for source in heat_sources: + # 简化的热源影响计算 + source_temp = source.get('temperature', 0.0) + source_radius = source.get('radius', 1.0) + distance_factor = max(0.1, source_radius) # 简化距离影响 + external_heat += (source_temp - current_temp) * 0.1 / distance_factor * time_delta + + # 温度变化 = (热传导 + 内部热量 + 外部热源) / (质量 * 比热容) + # 这里假设单位质量为1kg + temp_change = (heat_transfer + internal_heat + external_heat) / specific_heat if specific_heat > 0 else 0 + + # 考虑相变(简化实现) + melting_point = material.properties.get('melting_point', 1000.0) + phase_change_effect = 1.0 + if current_temp < melting_point <= current_temp + temp_change: + # 材质开始熔化,吸收潜热 + latent_heat = material.properties.get('latent_heat_of_fusion', 0.0) + if latent_heat > 0: + temp_change *= 0.1 # 熔化过程中温度变化减缓 + phase_change_effect = 0.5 # 相变期间物理属性变化 + + # 热膨胀效应 + expansion_factor = 1.0 + thermal_expansion * temp_change + + return { + 'temperature_change': temp_change, + 'expansion_factor': expansion_factor, + 'phase_change_effect': phase_change_effect, + 'total_heat': heat_transfer + internal_heat + external_heat + } + + @staticmethod + def calculate_environmental_effects(material, time_delta, weather_conditions=None): + """ + 计算环境效应对材质的影响 + + Args: + material (PhysicsMaterial): 材质对象 + time_delta (float): 时间增量 + weather_conditions (dict): 天气条件 {'rain': float, 'wind': float, 'humidity': float, 'uv': float} + + Returns: + dict: 环境效应结果 + """ + if weather_conditions is None: + weather_conditions = {'rain': 0.0, 'wind': 0.0, 'humidity': 0.5, 'uv': 0.0} + + # 获取材质属性 + weather_resistance = material.properties.get('weather_resistance', 1.0) + corrosion_rate = material.properties.get('corrosion_rate', 0.0) + uv_resistance = material.properties.get('uv_resistance', 1.0) + aging_rate = material.properties.get('aging_rate', 0.0) + + # 雨水影响 + rain_intensity = weather_conditions.get('rain', 0.0) + wetness_increase = rain_intensity * (1.0 - weather_resistance) * time_delta + material.update_state(wetness=min(1.0, material.state['wetness'] + wetness_increase)) + + # 湿度影响 + humidity = weather_conditions.get('humidity', 0.5) + humidity_effect = (humidity - 0.5) * 0.1 * time_delta + material.update_state(humidity=max(0.0, min(100.0, material.state['humidity'] + humidity_effect))) + + # 腐蚀影响 + corrosion_increase = rain_intensity * corrosion_rate * (1.0 - weather_resistance) * time_delta + material.update_state(contamination=min(1.0, material.state['contamination'] + corrosion_increase)) + + # UV老化影响 + uv_intensity = weather_conditions.get('uv', 0.0) + uv_damage = uv_intensity * aging_rate * (1.0 - uv_resistance) * time_delta + material.update_state(wear=min(1.0, material.state['wear'] + uv_damage)) + + # 风力影响(清洁效果) + wind_speed = weather_conditions.get('wind', 0.0) + dirt_removal = wind_speed * 0.01 * time_delta + material.update_state(dirtiness=max(0.0, material.state['dirtiness'] - dirt_removal)) + + return { + 'wetness_change': wetness_increase, + 'humidity_change': humidity_effect, + 'corrosion_change': corrosion_increase, + 'uv_damage': uv_damage, + 'dirt_removal': dirt_removal + } + + @staticmethod + def calculate_material_interaction(material_a, material_b, velocity, normal_force, time_delta): + """ + 计算两种材质之间的复杂交互 + + Args: + material_a (PhysicsMaterial): 材质A + material_b (PhysicsMaterial): 材质B + velocity (Vec3): 相对速度 + normal_force (float): 法向力 + time_delta (float): 时间增量 + + Returns: + dict: 交互结果 + """ + speed = velocity.length() + + # 获取交互系数 + interaction_coeff = material_a.get_interaction_coefficient(material_b.name) + + # 计算磨损 + wear_a = material_a.calculate_wear_rate(velocity, normal_force, time_delta) + wear_b = material_b.calculate_wear_rate(velocity, normal_force, time_delta) + + # 考虑交互系数 + effective_wear_a = wear_a * interaction_coeff + effective_wear_b = wear_b * interaction_coeff + + # 更新材质状态 + material_a.update_state(wear=min(1.0, material_a.state['wear'] + effective_wear_a)) + material_b.update_state(wear=min(1.0, material_b.state['wear'] + effective_wear_b)) + + # 计算热量生成(摩擦生热) + friction_heat = normal_force * speed * 0.1 * time_delta * interaction_coeff + + # 平分热量 + heat_a = friction_heat * 0.5 + heat_b = friction_heat * 0.5 + + # 更新温度状态 + material_a.update_state(temperature=material_a.state['temperature'] + heat_a * 0.01) + material_b.update_state(temperature=material_b.state['temperature'] + heat_b * 0.01) + + # 计算电荷积累 + charge_a = material_a.calculate_electrical_effect(time_delta) + charge_b = material_b.calculate_electrical_effect(time_delta) + + # 摩擦电效应 + triboelectric_effect = speed * normal_force * 0.001 * time_delta + material_a.update_state(charge=material_a.state['charge'] + triboelectric_effect) + material_b.update_state(charge=material_b.state['charge'] - triboelectric_effect) + + return { + 'wear_a': effective_wear_a, + 'wear_b': effective_wear_b, + 'heat_generated': friction_heat, + 'charge_a': triboelectric_effect, + 'charge_b': -triboelectric_effect, + 'interaction_coefficient': interaction_coeff + } + + @staticmethod + def simulate_material_aging(material, time_delta, environmental_factors=None): + """ + 模拟材质老化过程 + + Args: + material (PhysicsMaterial): 材质对象 + time_delta (float): 时间增量 + environmental_factors (dict): 环境因素 + + Returns: + dict: 老化效应 + """ + if environmental_factors is None: + environmental_factors = {} + + aging_rate = material.properties.get('aging_rate', 0.0) + weather_resistance = material.properties.get('weather_resistance', 1.0) + + # 基础老化 + base_aging = aging_rate * time_delta + + # 环境加速老化 + environmental_aging = 0.0 + for factor, intensity in environmental_factors.items(): + if factor == 'temperature': + # 高温加速老化 + if intensity > 50: # 超过50度 + environmental_aging += (intensity - 50) * 0.001 * time_delta + elif factor == 'humidity': + # 高湿度影响 + if intensity > 0.8: # 湿度超过80% + environmental_aging += (intensity - 0.8) * 0.1 * time_delta + elif factor == 'chemical_exposure': + # 化学暴露 + environmental_aging += intensity * 0.05 * time_delta + + # 总老化效应 + total_aging = base_aging + environmental_aging + + # 应用老化效应到材质属性 + current_wear = material.state['wear'] + new_wear = min(1.0, current_wear + total_aging) + material.update_state(wear=new_wear) + + # 老化对物理属性的影响 + aging_factor = new_wear # 简化处理,磨损程度等于老化程度 + + # 老化对摩擦系数的影响 + friction_change = aging_factor * 0.1 # 老化增加摩擦变化 + + # 老化对弹性的影响 + restitution_degradation = aging_factor * 0.2 # 老化降低弹性 + + return { + 'aging_amount': total_aging, + 'friction_change': friction_change, + 'restitution_degradation': restitution_degradation, + 'new_wear_level': new_wear + } + + @staticmethod + def calculate_fluid_interaction(material, fluid_properties, velocity, time_delta): + """ + 计算材质与流体的交互 + + Args: + material (PhysicsMaterial): 材质对象 + fluid_properties (dict): 流体属性 {'density': float, 'viscosity': float, 'temperature': float} + velocity (Vec3): 相对速度 + time_delta (float): 时间增量 + + Returns: + dict: 流体交互结果 + """ + speed = velocity.length() + fluid_density = fluid_properties.get('density', 1000.0) + fluid_viscosity = fluid_properties.get('viscosity', 0.001) + fluid_temp = fluid_properties.get('temperature', 20.0) + + # 浮力计算 + material_density = material.properties['density'] + buoyancy_force = fluid_density * 9.81 * (material_density / fluid_density) + + # 阻力计算 + drag_coefficient = material.properties.get('drag_coefficient', 0.5) + drag_force = 0.5 * fluid_density * speed * speed * drag_coefficient + + # 粘性阻力 + viscous_drag = fluid_viscosity * speed * material.properties.get('surface_area', 1.0) + + # 热交换 + material_temp = material.state['temperature'] + temp_difference = fluid_temp - material_temp + heat_transfer = temp_difference * material.properties.get('thermal_conductivity', 1.0) * time_delta + + # 更新材质温度 + specific_heat = material.properties.get('specific_heat', 1000.0) + if specific_heat > 0: + temp_change = heat_transfer / specific_heat + material.update_state(temperature=material_temp + temp_change) + + # 浸湿效应 + wetness_increase = min(1.0, speed * 0.01 * time_delta) + material.update_state(wetness=min(1.0, material.state['wetness'] + wetness_increase)) + + return { + 'buoyancy_force': buoyancy_force, + 'drag_force': drag_force, + 'viscous_drag': viscous_drag, + 'heat_transfer': heat_transfer, + 'wetness_increase': wetness_increase } \ No newline at end of file diff --git a/plugins/user/rigid_body_physics/core/physics_world.py b/plugins/user/rigid_body_physics/core/physics_world.py index a995724b..6353d09e 100644 --- a/plugins/user/rigid_body_physics/core/physics_world.py +++ b/plugins/user/rigid_body_physics/core/physics_world.py @@ -1,1070 +1,1070 @@ -""" -物理世界管理模块 -负责物理世界的创建、配置、更新和销毁 -""" - -from panda3d.core import Vec3, Point3, ClockObject, ConfigVariableInt -from panda3d.bullet import BulletWorld, BulletSoftBodyWorldInfo -from panda3d.bullet import BulletPersistentManifold -import math -import time -import threading -from collections import deque - - -class PhysicsWorld: - """ - 物理世界管理类 - 管理整个物理模拟环境,包括重力、时间步长、更新循环等 - """ - - def __init__(self, gravity=Vec3(0, 0, -9.81)): - """ - 初始化物理世界 - - Args: - gravity (Vec3): 重力向量,默认为地球重力 - """ - # 创建Bullet物理世界 - self.bullet_world = BulletWorld() - self.bullet_world.setGravity(gravity) - - # 软体世界信息(用于布料和软体物理) - self.soft_body_world_info = BulletSoftBodyWorldInfo() - self.soft_body_world_info.setGravity(gravity) - self.soft_body_world_info.setAirDensity(1.225) - self.soft_body_world_info.setWaterDensity(1000.0) - self.soft_body_world_info.setWaterOffset(0.0) - self.soft_body_world_info.setWaterNormal(Vec3(0, 0, 1)) - - # 物理参数 - self.gravity = gravity - self.time_step = 1.0/60.0 # 默认60FPS - self.max_substeps = 10 # 最大子步数 - self.internal_time_step = 1.0/60.0 # 内部时间步长 - self.solver_iterations = 10 # 求解器迭代次数 - self.solver_tolerance = 0.001 # 求解器容差 - - # 高级物理参数 - self.enable_split_impulse = True # 启用分离冲量 - self.split_impulse_penetration_threshold = -0.04 # 分离冲量穿透阈值 - self.ccd_threshold = 0.0 # 连续碰撞检测阈值 - self.ccd_radius = 0.0 # 连续碰撞检测半径 - self.warm_starting = True # 启用预热 - self.mpr_threshold = 0.0 # MPR阈值 - - # 力场参数 - self.global_wind = Vec3(0, 0, 0) - self.global_magnetic_field = Vec3(0, 0, 0) - self.fluid_density = 1000.0 - self.fluid_viscosity = 0.001 - self.fluid_drag_coefficient = 0.01 - self.fluid_lift_coefficient = 0.01 - - # 性能监控 - self.performance_stats = { - 'update_count': 0, - 'last_update_time': 0.0, - 'avg_update_time': 0.0, - 'max_update_time': 0.0, - 'min_update_time': float('inf'), - 'frame_times': deque(maxlen=1000), # 最近1000帧的时间 - 'collision_count': 0, - 'constraint_count': 0, - 'active_rigid_bodies': 0, - 'sleeping_rigid_bodies': 0, - 'memory_usage': 0.0, - 'collision_detection_time': 0.0, - 'constraint_solving_time': 0.0, - 'integration_time': 0.0, - 'broad_phase_time': 0.0, - 'narrow_phase_time': 0.0, - 'solver_time': 0.0, - 'islands_count': 0, - 'manifolds_count': 0, - 'contact_points_count': 0, - 'raycast_count': 0, - 'broad_phase_pairs': 0, - 'narrow_phase_pairs': 0 - } - - # 时间累积器 - self.time_accumulator = 0.0 - - # 物理对象容器 - self.rigid_bodies = [] - self.constraints = [] - self.vehicles = [] - self.particles = [] - self.soft_bodies = [] - self.ghost_objects = [] # 幽灵对象(用于触发器) - self.sensors = [] # 传感器对象 - self.triggers = [] # 触发器对象 - self.force_fields = [] # 力场对象 - self.fluids = [] # 流体对象 - - # 碰撞回调 - self.collision_callbacks = [] - self.contact_callbacks = [] - self.raycast_callbacks = [] - - # 多线程支持 - self.use_multithreading = False - self.thread_count = 1 - self.worker_threads = [] - self.physics_thread = None - self.physics_thread_lock = threading.Lock() - - # 物理场景分区(用于大型场景优化) - self.spatial_partitioning = None - self.partition_size = 100.0 - - # 物理场景状态 - self.is_paused = False - self.time_scale = 1.0 - self.frame_counter = 0 - - # 碰撞检测优化 - self.enable_broad_phase = True - self.enable_narrow_phase = True - self.enable_continuous_collision = True - self.enable_contact_caching = True - self.enable_island_generation = True - - # 物理世界配置 - self.enable_sleeping = True - self.enable_deactivation = True - self.sleeping_threshold = 0.01 - self.deactivation_time = 2.0 - self.max_angular_velocity = 1000.0 - - # 对象池 - self.rigid_body_pool = [] - self.constraint_pool = [] - - # 事件系统 - self.event_queue = deque(maxlen=1000) - self.collision_event_history = deque(maxlen=100) - - # 配置求解器 - self._configure_solver() - - # 设置分离冲量 - self._configure_split_impulse() - - # 初始化性能分析器 - self._init_profiler() - - print("物理世界初始化完成") - - def _configure_solver(self): - """ - 配置物理求解器 - """ - # 设置求解器参数 - self.bullet_world.setSolverNumIterations(self.solver_iterations) - self.bullet_world.setSolverSorParameter(self.solver_tolerance) - - # 启用各种优化 - self.bullet_world.setEnableWarmStarting(self.warm_starting) - self.bullet_world.setEnableCcd(self.enable_continuous_collision) - - # 设置最大角速度 - self.bullet_world.setMaxAngularVelocity(self.max_angular_velocity) - - def _configure_split_impulse(self): - """ - 配置分离冲量 - """ - if self.enable_split_impulse: - self.bullet_world.setSplitImpulseEnabled(True) - self.bullet_world.setSplitImpulsePenetrationThreshold( - self.split_impulse_penetration_threshold) - - def _init_profiler(self): - """ - 初始化性能分析器 - """ - # 初始化性能分析器数据结构 - self.collision_detection_timer = 0.0 - self.constraint_solving_timer = 0.0 - self.integration_timer = 0.0 - self.broad_phase_timer = 0.0 - self.narrow_phase_timer = 0.0 - self.solver_timer = 0.0 - - def set_gravity(self, gravity): - """ - 设置重力 - - Args: - gravity (Vec3): 重力向量 - """ - self.gravity = gravity - self.bullet_world.setGravity(gravity) - self.soft_body_world_info.setGravity(gravity) - - def set_time_step(self, time_step, max_substeps=10): - """ - 设置时间步长参数 - - Args: - time_step (float): 时间步长 - max_substeps (int): 最大子步数 - """ - self.time_step = time_step - self.max_substeps = max_substeps - self.internal_time_step = time_step - - def set_solver_parameters(self, iterations=None, tolerance=None, warm_starting=None): - """ - 设置求解器参数 - - Args: - iterations (int): 迭代次数 - tolerance (float): 容差 - warm_starting (bool): 是否启用预热 - """ - if iterations is not None: - self.solver_iterations = iterations - self.bullet_world.setSolverNumIterations(iterations) - if tolerance is not None: - self.solver_tolerance = tolerance - self.bullet_world.setSolverSorParameter(tolerance) - if warm_starting is not None: - self.warm_starting = warm_starting - self.bullet_world.setEnableWarmStarting(warm_starting) - self._configure_solver() - - def set_split_impulse(self, enable=True, penetration_threshold=None): - """ - 设置分离冲量参数 - - Args: - enable (bool): 是否启用 - penetration_threshold (float): 穿透阈值 - """ - self.enable_split_impulse = enable - if penetration_threshold is not None: - self.split_impulse_penetration_threshold = penetration_threshold - self._configure_split_impulse() - - def set_ccd_parameters(self, threshold=None, radius=None): - """ - 设置连续碰撞检测参数 - - Args: - threshold (float): CCD阈值 - radius (float): CCD半径 - """ - if threshold is not None: - self.ccd_threshold = threshold - if radius is not None: - self.ccd_radius = radius - - def set_force_field_parameters(self, wind=None, magnetic_field=None, - fluid_density=None, fluid_viscosity=None, - drag_coefficient=None, lift_coefficient=None): - """ - 设置力场参数 - - Args: - wind (Vec3): 风向量 - magnetic_field (Vec3): 磁场向量 - fluid_density (float): 流体密度 - fluid_viscosity (float): 流体粘度 - drag_coefficient (float): 阻力系数 - lift_coefficient (float): 升力系数 - """ - if wind is not None: - self.global_wind = wind - if magnetic_field is not None: - self.global_magnetic_field = magnetic_field - if fluid_density is not None: - self.fluid_density = fluid_density - if fluid_viscosity is not None: - self.fluid_viscosity = fluid_viscosity - if drag_coefficient is not None: - self.fluid_drag_coefficient = drag_coefficient - if lift_coefficient is not None: - self.fluid_lift_coefficient = lift_coefficient - - def enable_multithreading(self, enable=True, thread_count=4): - """ - 启用多线程支持 - - Args: - enable (bool): 是否启用多线程 - thread_count (int): 线程数量 - """ - self.use_multithreading = enable - self.thread_count = max(1, thread_count) - - # 配置Bullet多线程支持(如果可用) - try: - if enable: - # 注意:Bullet Python绑定可能不支持多线程 - pass - except Exception as e: - print(f"多线程配置失败: {e}") - - def setup_spatial_partitioning(self, partition_size=100.0): - """ - 设置空间分区 - - Args: - partition_size (float): 分区大小 - """ - self.partition_size = partition_size - # 这里可以实现八叉树或网格分区系统 - self.spatial_partitioning = {} - - def _update_spatial_partitioning(self): - """ - 更新空间分区 - """ - if self.spatial_partitioning is not None: - # 清空现有分区 - self.spatial_partitioning.clear() - - # 重新分配对象到分区 - for body in self.rigid_bodies: - pos = body.physics_node.getPos() - # 计算分区索引 - partition_x = int(pos.getX() // self.partition_size) - partition_y = int(pos.getY() // self.partition_size) - partition_z = int(pos.getZ() // self.partition_size) - partition_key = (partition_x, partition_y, partition_z) - - # 添加到分区 - if partition_key not in self.spatial_partitioning: - self.spatial_partitioning[partition_key] = [] - self.spatial_partitioning[partition_key].append(body) - - def update(self, dt): - """ - 更新物理世界 - - Args: - dt (float): 帧时间增量 - """ - # 如果暂停,不更新物理 - if self.is_paused: - return - - # 应用时间缩放 - dt *= self.time_scale - - try: - # 性能监控开始 - start_time = time.time() - - # 更新帧计数器 - self.frame_counter += 1 - - # 累积时间 - self.time_accumulator += dt - - # 计算子步数 - num_substeps = min( - int(self.time_accumulator / self.internal_time_step), - self.max_substeps - ) - - # 至少执行一次更新 - if num_substeps == 0 and self.time_accumulator > 0: - num_substeps = 1 - - # 执行子步长模拟 - for i in range(num_substeps): - # 应用全局力场 - self._apply_global_force_fields() - - # 在多线程模式下更新物理世界 - if self.use_multithreading and self.thread_count > 1: - self._update_multithreaded(self.internal_time_step) - else: - # 记录各个阶段的时间 - broad_start = time.time() - # 这里应用Broad Phase时间记录 - broad_end = time.time() - self.broad_phase_timer += broad_end - broad_start - - narrow_start = time.time() - # 这里应用Narrow Phase时间记录 - narrow_end = time.time() - self.narrow_phase_timer += narrow_end - narrow_start - - solver_start = time.time() - # 这里应用Solver时间记录 - solver_end = time.time() - self.solver_timer += solver_end - solver_start - - # 执行物理模拟 - self.bullet_world.doPhysics(self.internal_time_step) - - # 减少累积时间 - if num_substeps > 0: - self.time_accumulator -= num_substeps * self.internal_time_step - - # 更新空间分区 - self._update_spatial_partitioning() - - # 更新性能统计 - end_time = time.time() - update_time = end_time - start_time - self.performance_stats['last_update_time'] = update_time - self.performance_stats['update_count'] += 1 - self.performance_stats['frame_times'].append(update_time) - - # 计算平均更新时间 - if self.performance_stats['update_count'] > 1: - alpha = 0.1 - self.performance_stats['avg_update_time'] = ( - alpha * update_time + - (1 - alpha) * self.performance_stats['avg_update_time'] - ) - else: - self.performance_stats['avg_update_time'] = update_time - - # 更新最大和最小更新时间 - if update_time > self.performance_stats['max_update_time']: - self.performance_stats['max_update_time'] = update_time - if update_time < self.performance_stats['min_update_time']: - self.performance_stats['min_update_time'] = update_time - - # 更新对象状态统计 - self._update_object_stats() - - # 更新性能分析计时器 - self.performance_stats['collision_detection_time'] = self.collision_detection_timer - self.performance_stats['constraint_solving_time'] = self.constraint_solving_timer - self.performance_stats['integration_time'] = self.integration_timer - self.performance_stats['broad_phase_time'] = self.broad_phase_timer - self.performance_stats['narrow_phase_time'] = self.narrow_phase_timer - self.performance_stats['solver_time'] = self.solver_timer - - except Exception as e: - print(f"更新物理世界时出错: {e}") - - def _apply_global_force_fields(self): - """ - 应用全局力场 - """ - # 应用全局风力 - for body in self.rigid_bodies: - if not body.is_static(): - # 计算风力 - velocity = body.get_linear_velocity() - relative_wind = self.global_wind - velocity - wind_force = relative_wind * self.fluid_drag_coefficient * body.mass - body.bullet_body.applyCentralForce(wind_force) - - # 计算流体阻力 - drag_force = -velocity * self.fluid_drag_coefficient * body.mass - body.bullet_body.applyCentralForce(drag_force) - - # 计算升力(基于角速度) - angular_velocity = body.get_angular_velocity() - lift_force = angular_velocity.cross(velocity) * self.fluid_lift_coefficient * body.mass - body.bullet_body.applyCentralForce(lift_force) - - # 应用磁场力(如果定义) - if self.global_magnetic_field.length() > 0: - magnetic_force = self.global_magnetic_field * 0.1 * body.mass - body.bullet_body.applyCentralForce(magnetic_force) - - def _update_multithreaded(self, time_step): - """ - 多线程更新物理世界 - - Args: - time_step (float): 时间步长 - """ - # 这里可以实现多线程物理更新 - # 由于Bullet Python绑定的限制,这里使用单线程模拟 - self.bullet_world.doPhysics(time_step) - - def _update_object_stats(self): - """ - 更新对象状态统计 - """ - active_count = 0 - sleeping_count = 0 - - for body in self.rigid_bodies: - if body.bullet_body.isActive(): - active_count += 1 - else: - sleeping_count += 1 - - self.performance_stats['active_rigid_bodies'] = active_count - self.performance_stats['sleeping_rigid_bodies'] = sleeping_count - self.performance_stats['constraint_count'] = len(self.constraints) - - def add_rigid_body(self, rigid_body): - """ - 添加刚体到物理世界 - - Args: - rigid_body (RigidBody): 刚体对象 - """ - self.rigid_bodies.append(rigid_body) - self.bullet_world.attachRigidBody(rigid_body.bullet_body) - - # 设置CCD参数(如果启用) - if self.ccd_threshold > 0 or self.ccd_radius > 0: - rigid_body.bullet_body.setCcdMotionThreshold(self.ccd_threshold) - rigid_body.bullet_body.setCcdSweptSphereRadius(self.ccd_radius) - - def remove_rigid_body(self, rigid_body): - """ - 从物理世界移除刚体 - - Args: - rigid_body (RigidBody): 刚体对象 - """ - if rigid_body in self.rigid_bodies: - self.rigid_bodies.remove(rigid_body) - self.bullet_world.removeRigidBody(rigid_body.bullet_body) - - def add_constraint(self, constraint): - """ - 添加约束到物理世界 - - Args: - constraint: 约束对象 - """ - self.constraints.append(constraint) - self.bullet_world.attachConstraint(constraint.bullet_constraint) - - def remove_constraint(self, constraint): - """ - 从物理世界移除约束 - - Args: - constraint: 约束对象 - """ - if constraint in self.constraints: - self.constraints.remove(constraint) - self.bullet_world.removeConstraint(constraint.bullet_constraint) - - def add_vehicle(self, vehicle): - """ - 添加车辆到物理世界 - - Args: - vehicle: 车辆对象 - """ - self.vehicles.append(vehicle) - self.bullet_world.attachVehicle(vehicle.bullet_vehicle) - - def remove_vehicle(self, vehicle): - """ - 从物理世界移除车辆 - - Args: - vehicle: 车辆对象 - """ - if vehicle in self.vehicles: - self.vehicles.remove(vehicle) - self.bullet_world.removeVehicle(vehicle.bullet_vehicle) - - def add_particle_system(self, particle_system): - """ - 添加粒子系统到物理世界 - - Args: - particle_system: 粒子系统对象 - """ - self.particles.append(particle_system) - - def remove_particle_system(self, particle_system): - """ - 从物理世界移除粒子系统 - - Args: - particle_system: 粒子系统对象 - """ - if particle_system in self.particles: - self.particles.remove(particle_system) - - def add_soft_body(self, soft_body): - """ - 添加软体到物理世界 - - Args: - soft_body: 软体对象 - """ - self.soft_bodies.append(soft_body) - self.bullet_world.attachSoftBody(soft_body.bullet_soft_body) - - def remove_soft_body(self, soft_body): - """ - 从物理世界移除软体 - - Args: - soft_body: 软体对象 - """ - if soft_body in self.soft_bodies: - self.soft_bodies.remove(soft_body) - self.bullet_world.removeSoftBody(soft_body.bullet_soft_body) - - def add_ghost_object(self, ghost_object): - """ - 添加幽灵对象到物理世界 - - Args: - ghost_object: 幽灵对象 - """ - self.ghost_objects.append(ghost_object) - self.bullet_world.attachGhostObject(ghost_object.bullet_ghost) - - def remove_ghost_object(self, ghost_object): - """ - 从物理世界移除幽灵对象 - - Args: - ghost_object: 幽灵对象 - """ - if ghost_object in self.ghost_objects: - self.ghost_objects.remove(ghost_object) - self.bullet_world.removeGhostObject(ghost_object.bullet_ghost) - - def add_sensor(self, sensor): - """ - 添加传感器到物理世界 - - Args: - sensor: 传感器对象 - """ - self.sensors.append(sensor) - - def remove_sensor(self, sensor): - """ - 从物理世界移除传感器 - - Args: - sensor: 传感器对象 - """ - if sensor in self.sensors: - self.sensors.remove(sensor) - - def add_trigger(self, trigger): - """ - 添加触发器到物理世界 - - Args: - trigger: 触发器对象 - """ - self.triggers.append(trigger) - - def remove_trigger(self, trigger): - """ - 从物理世界移除触发器 - - Args: - trigger: 触发器对象 - """ - if trigger in self.triggers: - self.triggers.remove(trigger) - - def add_force_field(self, force_field): - """ - 添加力场到物理世界 - - Args: - force_field: 力场对象 - """ - self.force_fields.append(force_field) - - def remove_force_field(self, force_field): - """ - 从物理世界移除力场 - - Args: - force_field: 力场对象 - """ - if force_field in self.force_fields: - self.force_fields.remove(force_field) - - def add_fluid(self, fluid): - """ - 添加流体到物理世界 - - Args: - fluid: 流体对象 - """ - self.fluids.append(fluid) - - def remove_fluid(self, fluid): - """ - 从物理世界移除流体 - - Args: - fluid: 流体对象 - """ - if fluid in self.fluids: - self.fluids.remove(fluid) - - def ray_test(self, from_pos, to_pos): - """ - 射线检测 - - Args: - from_pos (Point3): 起点 - to_pos (Point3): 终点 - - Returns: - BulletRayHit: 射线检测结果 - """ - self.performance_stats['raycast_count'] += 1 - return self.bullet_world.rayTestClosest(from_pos, to_pos) - - def ray_test_all(self, from_pos, to_pos): - """ - 射线检测(所有结果) - - Args: - from_pos (Point3): 起点 - to_pos (Point3): 终点 - - Returns: - list: 射线检测结果列表 - """ - self.performance_stats['raycast_count'] += 1 - return self.bullet_world.rayTestAll(from_pos, to_pos) - - def sweep_test(self, shape, from_transform, to_transform): - """ - 扫掠检测 - - Args: - shape: 碰撞形状 - from_transform (TransformState): 起始变换 - to_transform (TransformState): 结束变换 - - Returns: - BulletConvexResult: 扫掠检测结果 - """ - return self.bullet_world.sweepTestClosest(shape, from_transform, to_transform) - - def contact_test(self, body, contact_results=None): - """ - 接触检测 - - Args: - body: 刚体对象 - contact_results: 接触结果容器 - - Returns: - list: 接触结果列表 - """ - if contact_results is None: - contact_results = [] - self.bullet_world.contactTest(body.bullet_body, contact_results) - return contact_results - - def get_performance_stats(self): - """ - 获取性能统计信息 - - Returns: - dict: 性能统计信息 - """ - # 计算帧率统计 - if self.performance_stats['frame_times']: - avg_frame_time = sum(self.performance_stats['frame_times']) / len(self.performance_stats['frame_times']) - fps = 1.0 / avg_frame_time if avg_frame_time > 0 else 0 - else: - fps = 0 - - stats = self.performance_stats.copy() - stats['current_fps'] = fps - stats['objects'] = { - 'rigid_bodies': len(self.rigid_bodies), - 'constraints': len(self.constraints), - 'vehicles': len(self.vehicles), - 'particles': len(self.particles), - 'soft_bodies': len(self.soft_bodies), - 'ghost_objects': len(self.ghost_objects), - 'sensors': len(self.sensors), - 'triggers': len(self.triggers), - 'force_fields': len(self.force_fields), - 'fluids': len(self.fluids) - } - - return stats - - def get_detailed_performance_stats(self): - """ - 获取详细的性能统计信息 - - Returns: - dict: 详细性能统计信息 - """ - stats = self.get_performance_stats() - - # 添加额外的详细统计 - stats['detailed'] = { - 'solver_iterations': self.solver_iterations, - 'time_step': self.time_step, - 'max_substeps': self.max_substeps, - 'warm_starting': self.warm_starting, - 'split_impulse': self.enable_split_impulse, - 'continuous_collision': self.enable_continuous_collision, - 'sleeping_enabled': self.enable_sleeping, - 'deactivation_enabled': self.enable_deactivation, - 'broad_phase_enabled': self.enable_broad_phase, - 'narrow_phase_enabled': self.enable_narrow_phase - } - - return stats - - def pause(self, paused=True): - """ - 暂停或恢复物理模拟 - - Args: - paused (bool): 是否暂停 - """ - self.is_paused = paused - - def set_time_scale(self, scale): - """ - 设置时间缩放 - - Args: - scale (float): 时间缩放因子 - """ - self.time_scale = max(0.0, scale) - - def clear(self): - """ - 清除物理世界中的所有对象 - """ - # 移除所有约束 - for constraint in self.constraints[:]: - self.remove_constraint(constraint) - - # 移除所有刚体 - for rigid_body in self.rigid_bodies[:]: - self.remove_rigid_body(rigid_body) - - # 移除所有车辆 - for vehicle in self.vehicles[:]: - self.remove_vehicle(vehicle) - - # 移除所有软体 - for soft_body in self.soft_bodies[:]: - self.remove_soft_body(soft_body) - - # 移除所有幽灵对象 - for ghost_object in self.ghost_objects[:]: - self.remove_ghost_object(ghost_object) - - # 移除所有传感器 - for sensor in self.sensors[:]: - self.remove_sensor(sensor) - - # 移除所有触发器 - for trigger in self.triggers[:]: - self.remove_trigger(trigger) - - # 移除所有力场 - for force_field in self.force_fields[:]: - self.remove_force_field(force_field) - - # 移除所有流体 - for fluid in self.fluids[:]: - self.remove_fluid(fluid) - - # 清空粒子系统 - self.particles.clear() - - # 清空事件队列 - self.event_queue.clear() - self.collision_event_history.clear() - - # 重置统计信息 - self.performance_stats['update_count'] = 0 - self.performance_stats['collision_count'] = 0 - self.performance_stats['constraint_count'] = 0 - self.performance_stats['active_rigid_bodies'] = 0 - self.performance_stats['sleeping_rigid_bodies'] = 0 - self.performance_stats['raycast_count'] = 0 - - # 重置时间相关参数 - self.time_accumulator = 0.0 - self.frame_counter = 0 - - print("物理世界已清除") - - def reset(self): - """ - 重置物理世界到初始状态 - """ - self.clear() - self.is_paused = False - self.time_scale = 1.0 - self._init_profiler() - - def optimize_for_performance(self): - """ - 优化物理世界性能 - """ - # 调整求解器参数以提高性能 - self.set_solver_parameters(iterations=5, tolerance=0.01) # 减少迭代次数和容差 - - # 禁用某些高级功能以提高性能 - self.enable_split_impulse = False - self._configure_split_impulse() - - # 优化睡眠参数 - self.sleeping_threshold = 0.1 # 增加睡眠阈值 - self.deactivation_time = 1.0 # 减少停用时间 - - # 优化时间步长 - self.time_step = 1.0/30.0 # 降低更新频率以提高性能 - - print("物理世界已优化性能") - - def optimize_for_accuracy(self): - """ - 优化物理世界精度 - """ - # 调整求解器参数以提高精度 - self.set_solver_parameters(iterations=20, tolerance=0.001) # 增加迭代次数和减小容差 - - # 启用高级功能以提高精度 - self.enable_split_impulse = True - self._configure_split_impulse() - - # 优化睡眠参数 - self.sleeping_threshold = 0.01 # 降低睡眠阈值 - self.deactivation_time = 2.0 # 增加停用时间 - - # 优化时间步长 - self.time_step = 1.0/120.0 # 提高更新频率以提高精度 - - print("物理世界已优化精度") - - def get_memory_usage(self): - """ - 获取内存使用情况 - - Returns: - dict: 内存使用情况 - """ - # 这里可以实现更精确的内存使用计算 - object_counts = { - 'rigid_bodies': len(self.rigid_bodies), - 'constraints': len(self.constraints), - 'vehicles': len(self.vehicles), - 'particles': len(self.particles), - 'soft_bodies': len(self.soft_bodies), - 'ghost_objects': len(self.ghost_objects), - 'sensors': len(self.sensors), - 'triggers': len(self.triggers), - 'force_fields': len(self.force_fields), - 'fluids': len(self.fluids) - } - - total_objects = sum(object_counts.values()) - - return { - 'object_counts': object_counts, - 'total_objects': total_objects, - 'estimated_memory_mb': total_objects * 0.1 # 估算每个对象占用0.1MB - } - - def add_collision_event(self, body_a, body_b, contact_points, impulse): - """ - 添加碰撞事件到历史记录 - - Args: - body_a (RigidBody): 刚体A - body_b (RigidBody): 刚体B - contact_points (list): 接触点列表 - impulse (float): 冲量 - """ - collision_event = { - 'body_a': body_a, - 'body_b': body_b, - 'contact_points': contact_points, - 'impulse': impulse, - 'timestamp': time.time() - } - self.collision_event_history.append(collision_event) - - def get_recent_collision_events(self, duration=5.0): - """ - 获取最近的碰撞事件 - - Args: - duration (float): 时间范围(秒) - - Returns: - list: 碰撞事件列表 - """ - current_time = time.time() - start_time = current_time - duration - - recent_events = [] - for event in self.collision_event_history: - if event['timestamp'] >= start_time: - recent_events.append(event) - - return recent_events - - def get_collision_statistics(self): - """ - 获取碰撞统计信息 - - Returns: - dict: 碰撞统计信息 - """ - recent_events = self.get_recent_collision_events(duration=1.0) - - return { - 'total_collisions': len(self.collision_event_history), - 'recent_collisions': len(recent_events), - 'collision_rate_per_second': len(recent_events), - 'average_impulse': sum([e['impulse'] for e in recent_events if 'impulse' in e]) / max(1, len(recent_events)), - 'most_collided_bodies': self._get_most_collided_bodies() - } - - def _get_most_collided_bodies(self): - """ - 获取碰撞最多的刚体 - - Returns: - list: 碰撞计数排序的刚体列表 - """ - collision_counts = {} - current_time = time.time() - - for event in self.collision_event_history: - if current_time - event['timestamp'] <= 5.0: # 5秒内的事件 - body_a_name = event['body_a'].bullet_body.getName() if hasattr(event['body_a'], 'bullet_body') else 'unknown' - body_b_name = event['body_b'].bullet_body.getName() if hasattr(event['body_b'], 'bullet_body') else 'unknown' - - collision_counts[body_a_name] = collision_counts.get(body_a_name, 0) + 1 - collision_counts[body_b_name] = collision_counts.get(body_b_name, 0) + 1 - - # 按碰撞次数排序 - sorted_bodies = sorted(collision_counts.items(), key=lambda x: x[1], reverse=True) - return sorted_bodies[:10] # 返回前10个 - - def __del__(self): - """ - 析构函数,清理资源 - """ - self.clear() - # 清理多线程资源 - if self.worker_threads: - for thread in self.worker_threads: - if thread.is_alive(): - thread.join(timeout=1.0) - if self.physics_thread and self.physics_thread.is_alive(): +""" +物理世界管理模块 +负责物理世界的创建、配置、更新和销毁 +""" + +from panda3d.core import Vec3, Point3, ClockObject, ConfigVariableInt +from panda3d.bullet import BulletWorld, BulletSoftBodyWorldInfo +from panda3d.bullet import BulletPersistentManifold +import math +import time +import threading +from collections import deque + + +class PhysicsWorld: + """ + 物理世界管理类 + 管理整个物理模拟环境,包括重力、时间步长、更新循环等 + """ + + def __init__(self, gravity=Vec3(0, 0, -9.81)): + """ + 初始化物理世界 + + Args: + gravity (Vec3): 重力向量,默认为地球重力 + """ + # 创建Bullet物理世界 + self.bullet_world = BulletWorld() + self.bullet_world.setGravity(gravity) + + # 软体世界信息(用于布料和软体物理) + self.soft_body_world_info = BulletSoftBodyWorldInfo() + self.soft_body_world_info.setGravity(gravity) + self.soft_body_world_info.setAirDensity(1.225) + self.soft_body_world_info.setWaterDensity(1000.0) + self.soft_body_world_info.setWaterOffset(0.0) + self.soft_body_world_info.setWaterNormal(Vec3(0, 0, 1)) + + # 物理参数 + self.gravity = gravity + self.time_step = 1.0/60.0 # 默认60FPS + self.max_substeps = 10 # 最大子步数 + self.internal_time_step = 1.0/60.0 # 内部时间步长 + self.solver_iterations = 10 # 求解器迭代次数 + self.solver_tolerance = 0.001 # 求解器容差 + + # 高级物理参数 + self.enable_split_impulse = True # 启用分离冲量 + self.split_impulse_penetration_threshold = -0.04 # 分离冲量穿透阈值 + self.ccd_threshold = 0.0 # 连续碰撞检测阈值 + self.ccd_radius = 0.0 # 连续碰撞检测半径 + self.warm_starting = True # 启用预热 + self.mpr_threshold = 0.0 # MPR阈值 + + # 力场参数 + self.global_wind = Vec3(0, 0, 0) + self.global_magnetic_field = Vec3(0, 0, 0) + self.fluid_density = 1000.0 + self.fluid_viscosity = 0.001 + self.fluid_drag_coefficient = 0.01 + self.fluid_lift_coefficient = 0.01 + + # 性能监控 + self.performance_stats = { + 'update_count': 0, + 'last_update_time': 0.0, + 'avg_update_time': 0.0, + 'max_update_time': 0.0, + 'min_update_time': float('inf'), + 'frame_times': deque(maxlen=1000), # 最近1000帧的时间 + 'collision_count': 0, + 'constraint_count': 0, + 'active_rigid_bodies': 0, + 'sleeping_rigid_bodies': 0, + 'memory_usage': 0.0, + 'collision_detection_time': 0.0, + 'constraint_solving_time': 0.0, + 'integration_time': 0.0, + 'broad_phase_time': 0.0, + 'narrow_phase_time': 0.0, + 'solver_time': 0.0, + 'islands_count': 0, + 'manifolds_count': 0, + 'contact_points_count': 0, + 'raycast_count': 0, + 'broad_phase_pairs': 0, + 'narrow_phase_pairs': 0 + } + + # 时间累积器 + self.time_accumulator = 0.0 + + # 物理对象容器 + self.rigid_bodies = [] + self.constraints = [] + self.vehicles = [] + self.particles = [] + self.soft_bodies = [] + self.ghost_objects = [] # 幽灵对象(用于触发器) + self.sensors = [] # 传感器对象 + self.triggers = [] # 触发器对象 + self.force_fields = [] # 力场对象 + self.fluids = [] # 流体对象 + + # 碰撞回调 + self.collision_callbacks = [] + self.contact_callbacks = [] + self.raycast_callbacks = [] + + # 多线程支持 + self.use_multithreading = False + self.thread_count = 1 + self.worker_threads = [] + self.physics_thread = None + self.physics_thread_lock = threading.Lock() + + # 物理场景分区(用于大型场景优化) + self.spatial_partitioning = None + self.partition_size = 100.0 + + # 物理场景状态 + self.is_paused = False + self.time_scale = 1.0 + self.frame_counter = 0 + + # 碰撞检测优化 + self.enable_broad_phase = True + self.enable_narrow_phase = True + self.enable_continuous_collision = True + self.enable_contact_caching = True + self.enable_island_generation = True + + # 物理世界配置 + self.enable_sleeping = True + self.enable_deactivation = True + self.sleeping_threshold = 0.01 + self.deactivation_time = 2.0 + self.max_angular_velocity = 1000.0 + + # 对象池 + self.rigid_body_pool = [] + self.constraint_pool = [] + + # 事件系统 + self.event_queue = deque(maxlen=1000) + self.collision_event_history = deque(maxlen=100) + + # 配置求解器 + self._configure_solver() + + # 设置分离冲量 + self._configure_split_impulse() + + # 初始化性能分析器 + self._init_profiler() + + print("物理世界初始化完成") + + def _configure_solver(self): + """ + 配置物理求解器 + """ + # 设置求解器参数 + self.bullet_world.setSolverNumIterations(self.solver_iterations) + self.bullet_world.setSolverSorParameter(self.solver_tolerance) + + # 启用各种优化 + self.bullet_world.setEnableWarmStarting(self.warm_starting) + self.bullet_world.setEnableCcd(self.enable_continuous_collision) + + # 设置最大角速度 + self.bullet_world.setMaxAngularVelocity(self.max_angular_velocity) + + def _configure_split_impulse(self): + """ + 配置分离冲量 + """ + if self.enable_split_impulse: + self.bullet_world.setSplitImpulseEnabled(True) + self.bullet_world.setSplitImpulsePenetrationThreshold( + self.split_impulse_penetration_threshold) + + def _init_profiler(self): + """ + 初始化性能分析器 + """ + # 初始化性能分析器数据结构 + self.collision_detection_timer = 0.0 + self.constraint_solving_timer = 0.0 + self.integration_timer = 0.0 + self.broad_phase_timer = 0.0 + self.narrow_phase_timer = 0.0 + self.solver_timer = 0.0 + + def set_gravity(self, gravity): + """ + 设置重力 + + Args: + gravity (Vec3): 重力向量 + """ + self.gravity = gravity + self.bullet_world.setGravity(gravity) + self.soft_body_world_info.setGravity(gravity) + + def set_time_step(self, time_step, max_substeps=10): + """ + 设置时间步长参数 + + Args: + time_step (float): 时间步长 + max_substeps (int): 最大子步数 + """ + self.time_step = time_step + self.max_substeps = max_substeps + self.internal_time_step = time_step + + def set_solver_parameters(self, iterations=None, tolerance=None, warm_starting=None): + """ + 设置求解器参数 + + Args: + iterations (int): 迭代次数 + tolerance (float): 容差 + warm_starting (bool): 是否启用预热 + """ + if iterations is not None: + self.solver_iterations = iterations + self.bullet_world.setSolverNumIterations(iterations) + if tolerance is not None: + self.solver_tolerance = tolerance + self.bullet_world.setSolverSorParameter(tolerance) + if warm_starting is not None: + self.warm_starting = warm_starting + self.bullet_world.setEnableWarmStarting(warm_starting) + self._configure_solver() + + def set_split_impulse(self, enable=True, penetration_threshold=None): + """ + 设置分离冲量参数 + + Args: + enable (bool): 是否启用 + penetration_threshold (float): 穿透阈值 + """ + self.enable_split_impulse = enable + if penetration_threshold is not None: + self.split_impulse_penetration_threshold = penetration_threshold + self._configure_split_impulse() + + def set_ccd_parameters(self, threshold=None, radius=None): + """ + 设置连续碰撞检测参数 + + Args: + threshold (float): CCD阈值 + radius (float): CCD半径 + """ + if threshold is not None: + self.ccd_threshold = threshold + if radius is not None: + self.ccd_radius = radius + + def set_force_field_parameters(self, wind=None, magnetic_field=None, + fluid_density=None, fluid_viscosity=None, + drag_coefficient=None, lift_coefficient=None): + """ + 设置力场参数 + + Args: + wind (Vec3): 风向量 + magnetic_field (Vec3): 磁场向量 + fluid_density (float): 流体密度 + fluid_viscosity (float): 流体粘度 + drag_coefficient (float): 阻力系数 + lift_coefficient (float): 升力系数 + """ + if wind is not None: + self.global_wind = wind + if magnetic_field is not None: + self.global_magnetic_field = magnetic_field + if fluid_density is not None: + self.fluid_density = fluid_density + if fluid_viscosity is not None: + self.fluid_viscosity = fluid_viscosity + if drag_coefficient is not None: + self.fluid_drag_coefficient = drag_coefficient + if lift_coefficient is not None: + self.fluid_lift_coefficient = lift_coefficient + + def enable_multithreading(self, enable=True, thread_count=4): + """ + 启用多线程支持 + + Args: + enable (bool): 是否启用多线程 + thread_count (int): 线程数量 + """ + self.use_multithreading = enable + self.thread_count = max(1, thread_count) + + # 配置Bullet多线程支持(如果可用) + try: + if enable: + # 注意:Bullet Python绑定可能不支持多线程 + pass + except Exception as e: + print(f"多线程配置失败: {e}") + + def setup_spatial_partitioning(self, partition_size=100.0): + """ + 设置空间分区 + + Args: + partition_size (float): 分区大小 + """ + self.partition_size = partition_size + # 这里可以实现八叉树或网格分区系统 + self.spatial_partitioning = {} + + def _update_spatial_partitioning(self): + """ + 更新空间分区 + """ + if self.spatial_partitioning is not None: + # 清空现有分区 + self.spatial_partitioning.clear() + + # 重新分配对象到分区 + for body in self.rigid_bodies: + pos = body.physics_node.getPos() + # 计算分区索引 + partition_x = int(pos.getX() // self.partition_size) + partition_y = int(pos.getY() // self.partition_size) + partition_z = int(pos.getZ() // self.partition_size) + partition_key = (partition_x, partition_y, partition_z) + + # 添加到分区 + if partition_key not in self.spatial_partitioning: + self.spatial_partitioning[partition_key] = [] + self.spatial_partitioning[partition_key].append(body) + + def update(self, dt): + """ + 更新物理世界 + + Args: + dt (float): 帧时间增量 + """ + # 如果暂停,不更新物理 + if self.is_paused: + return + + # 应用时间缩放 + dt *= self.time_scale + + try: + # 性能监控开始 + start_time = time.time() + + # 更新帧计数器 + self.frame_counter += 1 + + # 累积时间 + self.time_accumulator += dt + + # 计算子步数 + num_substeps = min( + int(self.time_accumulator / self.internal_time_step), + self.max_substeps + ) + + # 至少执行一次更新 + if num_substeps == 0 and self.time_accumulator > 0: + num_substeps = 1 + + # 执行子步长模拟 + for i in range(num_substeps): + # 应用全局力场 + self._apply_global_force_fields() + + # 在多线程模式下更新物理世界 + if self.use_multithreading and self.thread_count > 1: + self._update_multithreaded(self.internal_time_step) + else: + # 记录各个阶段的时间 + broad_start = time.time() + # 这里应用Broad Phase时间记录 + broad_end = time.time() + self.broad_phase_timer += broad_end - broad_start + + narrow_start = time.time() + # 这里应用Narrow Phase时间记录 + narrow_end = time.time() + self.narrow_phase_timer += narrow_end - narrow_start + + solver_start = time.time() + # 这里应用Solver时间记录 + solver_end = time.time() + self.solver_timer += solver_end - solver_start + + # 执行物理模拟 + self.bullet_world.doPhysics(self.internal_time_step) + + # 减少累积时间 + if num_substeps > 0: + self.time_accumulator -= num_substeps * self.internal_time_step + + # 更新空间分区 + self._update_spatial_partitioning() + + # 更新性能统计 + end_time = time.time() + update_time = end_time - start_time + self.performance_stats['last_update_time'] = update_time + self.performance_stats['update_count'] += 1 + self.performance_stats['frame_times'].append(update_time) + + # 计算平均更新时间 + if self.performance_stats['update_count'] > 1: + alpha = 0.1 + self.performance_stats['avg_update_time'] = ( + alpha * update_time + + (1 - alpha) * self.performance_stats['avg_update_time'] + ) + else: + self.performance_stats['avg_update_time'] = update_time + + # 更新最大和最小更新时间 + if update_time > self.performance_stats['max_update_time']: + self.performance_stats['max_update_time'] = update_time + if update_time < self.performance_stats['min_update_time']: + self.performance_stats['min_update_time'] = update_time + + # 更新对象状态统计 + self._update_object_stats() + + # 更新性能分析计时器 + self.performance_stats['collision_detection_time'] = self.collision_detection_timer + self.performance_stats['constraint_solving_time'] = self.constraint_solving_timer + self.performance_stats['integration_time'] = self.integration_timer + self.performance_stats['broad_phase_time'] = self.broad_phase_timer + self.performance_stats['narrow_phase_time'] = self.narrow_phase_timer + self.performance_stats['solver_time'] = self.solver_timer + + except Exception as e: + print(f"更新物理世界时出错: {e}") + + def _apply_global_force_fields(self): + """ + 应用全局力场 + """ + # 应用全局风力 + for body in self.rigid_bodies: + if not body.is_static(): + # 计算风力 + velocity = body.get_linear_velocity() + relative_wind = self.global_wind - velocity + wind_force = relative_wind * self.fluid_drag_coefficient * body.mass + body.bullet_body.applyCentralForce(wind_force) + + # 计算流体阻力 + drag_force = -velocity * self.fluid_drag_coefficient * body.mass + body.bullet_body.applyCentralForce(drag_force) + + # 计算升力(基于角速度) + angular_velocity = body.get_angular_velocity() + lift_force = angular_velocity.cross(velocity) * self.fluid_lift_coefficient * body.mass + body.bullet_body.applyCentralForce(lift_force) + + # 应用磁场力(如果定义) + if self.global_magnetic_field.length() > 0: + magnetic_force = self.global_magnetic_field * 0.1 * body.mass + body.bullet_body.applyCentralForce(magnetic_force) + + def _update_multithreaded(self, time_step): + """ + 多线程更新物理世界 + + Args: + time_step (float): 时间步长 + """ + # 这里可以实现多线程物理更新 + # 由于Bullet Python绑定的限制,这里使用单线程模拟 + self.bullet_world.doPhysics(time_step) + + def _update_object_stats(self): + """ + 更新对象状态统计 + """ + active_count = 0 + sleeping_count = 0 + + for body in self.rigid_bodies: + if body.bullet_body.isActive(): + active_count += 1 + else: + sleeping_count += 1 + + self.performance_stats['active_rigid_bodies'] = active_count + self.performance_stats['sleeping_rigid_bodies'] = sleeping_count + self.performance_stats['constraint_count'] = len(self.constraints) + + def add_rigid_body(self, rigid_body): + """ + 添加刚体到物理世界 + + Args: + rigid_body (RigidBody): 刚体对象 + """ + self.rigid_bodies.append(rigid_body) + self.bullet_world.attachRigidBody(rigid_body.bullet_body) + + # 设置CCD参数(如果启用) + if self.ccd_threshold > 0 or self.ccd_radius > 0: + rigid_body.bullet_body.setCcdMotionThreshold(self.ccd_threshold) + rigid_body.bullet_body.setCcdSweptSphereRadius(self.ccd_radius) + + def remove_rigid_body(self, rigid_body): + """ + 从物理世界移除刚体 + + Args: + rigid_body (RigidBody): 刚体对象 + """ + if rigid_body in self.rigid_bodies: + self.rigid_bodies.remove(rigid_body) + self.bullet_world.removeRigidBody(rigid_body.bullet_body) + + def add_constraint(self, constraint): + """ + 添加约束到物理世界 + + Args: + constraint: 约束对象 + """ + self.constraints.append(constraint) + self.bullet_world.attachConstraint(constraint.bullet_constraint) + + def remove_constraint(self, constraint): + """ + 从物理世界移除约束 + + Args: + constraint: 约束对象 + """ + if constraint in self.constraints: + self.constraints.remove(constraint) + self.bullet_world.removeConstraint(constraint.bullet_constraint) + + def add_vehicle(self, vehicle): + """ + 添加车辆到物理世界 + + Args: + vehicle: 车辆对象 + """ + self.vehicles.append(vehicle) + self.bullet_world.attachVehicle(vehicle.bullet_vehicle) + + def remove_vehicle(self, vehicle): + """ + 从物理世界移除车辆 + + Args: + vehicle: 车辆对象 + """ + if vehicle in self.vehicles: + self.vehicles.remove(vehicle) + self.bullet_world.removeVehicle(vehicle.bullet_vehicle) + + def add_particle_system(self, particle_system): + """ + 添加粒子系统到物理世界 + + Args: + particle_system: 粒子系统对象 + """ + self.particles.append(particle_system) + + def remove_particle_system(self, particle_system): + """ + 从物理世界移除粒子系统 + + Args: + particle_system: 粒子系统对象 + """ + if particle_system in self.particles: + self.particles.remove(particle_system) + + def add_soft_body(self, soft_body): + """ + 添加软体到物理世界 + + Args: + soft_body: 软体对象 + """ + self.soft_bodies.append(soft_body) + self.bullet_world.attachSoftBody(soft_body.bullet_soft_body) + + def remove_soft_body(self, soft_body): + """ + 从物理世界移除软体 + + Args: + soft_body: 软体对象 + """ + if soft_body in self.soft_bodies: + self.soft_bodies.remove(soft_body) + self.bullet_world.removeSoftBody(soft_body.bullet_soft_body) + + def add_ghost_object(self, ghost_object): + """ + 添加幽灵对象到物理世界 + + Args: + ghost_object: 幽灵对象 + """ + self.ghost_objects.append(ghost_object) + self.bullet_world.attachGhostObject(ghost_object.bullet_ghost) + + def remove_ghost_object(self, ghost_object): + """ + 从物理世界移除幽灵对象 + + Args: + ghost_object: 幽灵对象 + """ + if ghost_object in self.ghost_objects: + self.ghost_objects.remove(ghost_object) + self.bullet_world.removeGhostObject(ghost_object.bullet_ghost) + + def add_sensor(self, sensor): + """ + 添加传感器到物理世界 + + Args: + sensor: 传感器对象 + """ + self.sensors.append(sensor) + + def remove_sensor(self, sensor): + """ + 从物理世界移除传感器 + + Args: + sensor: 传感器对象 + """ + if sensor in self.sensors: + self.sensors.remove(sensor) + + def add_trigger(self, trigger): + """ + 添加触发器到物理世界 + + Args: + trigger: 触发器对象 + """ + self.triggers.append(trigger) + + def remove_trigger(self, trigger): + """ + 从物理世界移除触发器 + + Args: + trigger: 触发器对象 + """ + if trigger in self.triggers: + self.triggers.remove(trigger) + + def add_force_field(self, force_field): + """ + 添加力场到物理世界 + + Args: + force_field: 力场对象 + """ + self.force_fields.append(force_field) + + def remove_force_field(self, force_field): + """ + 从物理世界移除力场 + + Args: + force_field: 力场对象 + """ + if force_field in self.force_fields: + self.force_fields.remove(force_field) + + def add_fluid(self, fluid): + """ + 添加流体到物理世界 + + Args: + fluid: 流体对象 + """ + self.fluids.append(fluid) + + def remove_fluid(self, fluid): + """ + 从物理世界移除流体 + + Args: + fluid: 流体对象 + """ + if fluid in self.fluids: + self.fluids.remove(fluid) + + def ray_test(self, from_pos, to_pos): + """ + 射线检测 + + Args: + from_pos (Point3): 起点 + to_pos (Point3): 终点 + + Returns: + BulletRayHit: 射线检测结果 + """ + self.performance_stats['raycast_count'] += 1 + return self.bullet_world.rayTestClosest(from_pos, to_pos) + + def ray_test_all(self, from_pos, to_pos): + """ + 射线检测(所有结果) + + Args: + from_pos (Point3): 起点 + to_pos (Point3): 终点 + + Returns: + list: 射线检测结果列表 + """ + self.performance_stats['raycast_count'] += 1 + return self.bullet_world.rayTestAll(from_pos, to_pos) + + def sweep_test(self, shape, from_transform, to_transform): + """ + 扫掠检测 + + Args: + shape: 碰撞形状 + from_transform (TransformState): 起始变换 + to_transform (TransformState): 结束变换 + + Returns: + BulletConvexResult: 扫掠检测结果 + """ + return self.bullet_world.sweepTestClosest(shape, from_transform, to_transform) + + def contact_test(self, body, contact_results=None): + """ + 接触检测 + + Args: + body: 刚体对象 + contact_results: 接触结果容器 + + Returns: + list: 接触结果列表 + """ + if contact_results is None: + contact_results = [] + self.bullet_world.contactTest(body.bullet_body, contact_results) + return contact_results + + def get_performance_stats(self): + """ + 获取性能统计信息 + + Returns: + dict: 性能统计信息 + """ + # 计算帧率统计 + if self.performance_stats['frame_times']: + avg_frame_time = sum(self.performance_stats['frame_times']) / len(self.performance_stats['frame_times']) + fps = 1.0 / avg_frame_time if avg_frame_time > 0 else 0 + else: + fps = 0 + + stats = self.performance_stats.copy() + stats['current_fps'] = fps + stats['objects'] = { + 'rigid_bodies': len(self.rigid_bodies), + 'constraints': len(self.constraints), + 'vehicles': len(self.vehicles), + 'particles': len(self.particles), + 'soft_bodies': len(self.soft_bodies), + 'ghost_objects': len(self.ghost_objects), + 'sensors': len(self.sensors), + 'triggers': len(self.triggers), + 'force_fields': len(self.force_fields), + 'fluids': len(self.fluids) + } + + return stats + + def get_detailed_performance_stats(self): + """ + 获取详细的性能统计信息 + + Returns: + dict: 详细性能统计信息 + """ + stats = self.get_performance_stats() + + # 添加额外的详细统计 + stats['detailed'] = { + 'solver_iterations': self.solver_iterations, + 'time_step': self.time_step, + 'max_substeps': self.max_substeps, + 'warm_starting': self.warm_starting, + 'split_impulse': self.enable_split_impulse, + 'continuous_collision': self.enable_continuous_collision, + 'sleeping_enabled': self.enable_sleeping, + 'deactivation_enabled': self.enable_deactivation, + 'broad_phase_enabled': self.enable_broad_phase, + 'narrow_phase_enabled': self.enable_narrow_phase + } + + return stats + + def pause(self, paused=True): + """ + 暂停或恢复物理模拟 + + Args: + paused (bool): 是否暂停 + """ + self.is_paused = paused + + def set_time_scale(self, scale): + """ + 设置时间缩放 + + Args: + scale (float): 时间缩放因子 + """ + self.time_scale = max(0.0, scale) + + def clear(self): + """ + 清除物理世界中的所有对象 + """ + # 移除所有约束 + for constraint in self.constraints[:]: + self.remove_constraint(constraint) + + # 移除所有刚体 + for rigid_body in self.rigid_bodies[:]: + self.remove_rigid_body(rigid_body) + + # 移除所有车辆 + for vehicle in self.vehicles[:]: + self.remove_vehicle(vehicle) + + # 移除所有软体 + for soft_body in self.soft_bodies[:]: + self.remove_soft_body(soft_body) + + # 移除所有幽灵对象 + for ghost_object in self.ghost_objects[:]: + self.remove_ghost_object(ghost_object) + + # 移除所有传感器 + for sensor in self.sensors[:]: + self.remove_sensor(sensor) + + # 移除所有触发器 + for trigger in self.triggers[:]: + self.remove_trigger(trigger) + + # 移除所有力场 + for force_field in self.force_fields[:]: + self.remove_force_field(force_field) + + # 移除所有流体 + for fluid in self.fluids[:]: + self.remove_fluid(fluid) + + # 清空粒子系统 + self.particles.clear() + + # 清空事件队列 + self.event_queue.clear() + self.collision_event_history.clear() + + # 重置统计信息 + self.performance_stats['update_count'] = 0 + self.performance_stats['collision_count'] = 0 + self.performance_stats['constraint_count'] = 0 + self.performance_stats['active_rigid_bodies'] = 0 + self.performance_stats['sleeping_rigid_bodies'] = 0 + self.performance_stats['raycast_count'] = 0 + + # 重置时间相关参数 + self.time_accumulator = 0.0 + self.frame_counter = 0 + + print("物理世界已清除") + + def reset(self): + """ + 重置物理世界到初始状态 + """ + self.clear() + self.is_paused = False + self.time_scale = 1.0 + self._init_profiler() + + def optimize_for_performance(self): + """ + 优化物理世界性能 + """ + # 调整求解器参数以提高性能 + self.set_solver_parameters(iterations=5, tolerance=0.01) # 减少迭代次数和容差 + + # 禁用某些高级功能以提高性能 + self.enable_split_impulse = False + self._configure_split_impulse() + + # 优化睡眠参数 + self.sleeping_threshold = 0.1 # 增加睡眠阈值 + self.deactivation_time = 1.0 # 减少停用时间 + + # 优化时间步长 + self.time_step = 1.0/30.0 # 降低更新频率以提高性能 + + print("物理世界已优化性能") + + def optimize_for_accuracy(self): + """ + 优化物理世界精度 + """ + # 调整求解器参数以提高精度 + self.set_solver_parameters(iterations=20, tolerance=0.001) # 增加迭代次数和减小容差 + + # 启用高级功能以提高精度 + self.enable_split_impulse = True + self._configure_split_impulse() + + # 优化睡眠参数 + self.sleeping_threshold = 0.01 # 降低睡眠阈值 + self.deactivation_time = 2.0 # 增加停用时间 + + # 优化时间步长 + self.time_step = 1.0/120.0 # 提高更新频率以提高精度 + + print("物理世界已优化精度") + + def get_memory_usage(self): + """ + 获取内存使用情况 + + Returns: + dict: 内存使用情况 + """ + # 这里可以实现更精确的内存使用计算 + object_counts = { + 'rigid_bodies': len(self.rigid_bodies), + 'constraints': len(self.constraints), + 'vehicles': len(self.vehicles), + 'particles': len(self.particles), + 'soft_bodies': len(self.soft_bodies), + 'ghost_objects': len(self.ghost_objects), + 'sensors': len(self.sensors), + 'triggers': len(self.triggers), + 'force_fields': len(self.force_fields), + 'fluids': len(self.fluids) + } + + total_objects = sum(object_counts.values()) + + return { + 'object_counts': object_counts, + 'total_objects': total_objects, + 'estimated_memory_mb': total_objects * 0.1 # 估算每个对象占用0.1MB + } + + def add_collision_event(self, body_a, body_b, contact_points, impulse): + """ + 添加碰撞事件到历史记录 + + Args: + body_a (RigidBody): 刚体A + body_b (RigidBody): 刚体B + contact_points (list): 接触点列表 + impulse (float): 冲量 + """ + collision_event = { + 'body_a': body_a, + 'body_b': body_b, + 'contact_points': contact_points, + 'impulse': impulse, + 'timestamp': time.time() + } + self.collision_event_history.append(collision_event) + + def get_recent_collision_events(self, duration=5.0): + """ + 获取最近的碰撞事件 + + Args: + duration (float): 时间范围(秒) + + Returns: + list: 碰撞事件列表 + """ + current_time = time.time() + start_time = current_time - duration + + recent_events = [] + for event in self.collision_event_history: + if event['timestamp'] >= start_time: + recent_events.append(event) + + return recent_events + + def get_collision_statistics(self): + """ + 获取碰撞统计信息 + + Returns: + dict: 碰撞统计信息 + """ + recent_events = self.get_recent_collision_events(duration=1.0) + + return { + 'total_collisions': len(self.collision_event_history), + 'recent_collisions': len(recent_events), + 'collision_rate_per_second': len(recent_events), + 'average_impulse': sum([e['impulse'] for e in recent_events if 'impulse' in e]) / max(1, len(recent_events)), + 'most_collided_bodies': self._get_most_collided_bodies() + } + + def _get_most_collided_bodies(self): + """ + 获取碰撞最多的刚体 + + Returns: + list: 碰撞计数排序的刚体列表 + """ + collision_counts = {} + current_time = time.time() + + for event in self.collision_event_history: + if current_time - event['timestamp'] <= 5.0: # 5秒内的事件 + body_a_name = event['body_a'].bullet_body.getName() if hasattr(event['body_a'], 'bullet_body') else 'unknown' + body_b_name = event['body_b'].bullet_body.getName() if hasattr(event['body_b'], 'bullet_body') else 'unknown' + + collision_counts[body_a_name] = collision_counts.get(body_a_name, 0) + 1 + collision_counts[body_b_name] = collision_counts.get(body_b_name, 0) + 1 + + # 按碰撞次数排序 + sorted_bodies = sorted(collision_counts.items(), key=lambda x: x[1], reverse=True) + return sorted_bodies[:10] # 返回前10个 + + def __del__(self): + """ + 析构函数,清理资源 + """ + self.clear() + # 清理多线程资源 + if self.worker_threads: + for thread in self.worker_threads: + if thread.is_alive(): + thread.join(timeout=1.0) + if self.physics_thread and self.physics_thread.is_alive(): self.physics_thread.join(timeout=1.0) \ No newline at end of file diff --git a/plugins/user/rigid_body_physics/core/rigid_body.py b/plugins/user/rigid_body_physics/core/rigid_body.py index 7f267ef0..9a5a0be4 100644 --- a/plugins/user/rigid_body_physics/core/rigid_body.py +++ b/plugins/user/rigid_body_physics/core/rigid_body.py @@ -1,1139 +1,1139 @@ -""" -刚体系统核心模块 -负责刚体的创建、配置和管理 -""" - -from panda3d.core import Vec3, Point3, TransformState, LVector3f -from panda3d.bullet import BulletRigidBodyNode, BulletBoxShape, BulletSphereShape -from panda3d.bullet import BulletCapsuleShape, BulletConeShape, BulletCylinderShape -from panda3d.bullet import BulletConvexHullShape, BulletTriangleMeshShape, BulletTriangleMesh -from panda3d.bullet import BulletCollisionShape, BulletCompoundShape -import math -import time -from collections import deque - - -class RigidBody: - """ - 刚体类 - 管理单个刚体的物理属性和行为 - """ - - # 刚体类型常量 - STATIC = 0 # 静态刚体 - DYNAMIC = 1 # 动态刚体 - KINEMATIC = 2 # 运动学刚体 - - # 碰撞形状类型常量 - SHAPE_BOX = 'box' - SHAPE_SPHERE = 'sphere' - SHAPE_CAPSULE = 'capsule' - SHAPE_CONE = 'cone' - SHAPE_CYLINDER = 'cylinder' - SHAPE_CONVEX_HULL = 'convex_hull' - SHAPE_MESH = 'mesh' - SHAPE_COMPOUND = 'compound' - - # 碰撞组常量 - GROUP_STATIC = 1 - GROUP_DYNAMIC = 2 - GROUP_KINEMATIC = 4 - GROUP_SENSOR = 8 - GROUP_VEHICLE = 16 - GROUP_CHARACTER = 32 - GROUP_PARTICLE = 64 - GROUP_CUSTOM1 = 128 - GROUP_CUSTOM2 = 256 - - def __init__(self, node_path, shape_type='box', mass=1.0, body_type=DYNAMIC, - shape_params=None, compound_shapes=None, collision_group=None, collision_mask=None): - """ - 初始化刚体 - - Args: - node_path: Panda3D节点路径 - shape_type (str): 碰撞形状类型 - mass (float): 质量 - body_type (int): 刚体类型 - shape_params (dict): 形状参数 - compound_shapes (list): 复合形状列表 - collision_group (int): 碰撞组 - collision_mask (int): 碰撞掩码 - """ - self.node_path = node_path - self.mass = mass - self.body_type = body_type - self.shape_type = shape_type - self.shape_params = shape_params or {} - self.compound_shapes = compound_shapes or [] - self.collision_group = collision_group or self.GROUP_DYNAMIC - self.collision_mask = collision_mask or 0xFFFFFFFF - - # 创建碰撞形状 - self.shape = self._create_collision_shape(shape_type, shape_params, compound_shapes) - - # 创建Bullet刚体节点 - self.bullet_body = BulletRigidBodyNode('RigidBody') - self.bullet_body.addShape(self.shape) - - # 设置刚体属性 - if body_type == self.STATIC: - self.bullet_body.setMass(0) - self.collision_group = self.GROUP_STATIC - else: - self.bullet_body.setMass(mass) - - if body_type == self.KINEMATIC: - self.bullet_body.setKinematic(True) - self.collision_group = self.GROUP_KINEMATIC - - # 设置碰撞组和掩码 - self.bullet_body.setCollideFilterGroup(self.collision_group) - self.bullet_body.setCollideFilterMask(self.collision_mask) - - # 创建物理节点并附加到场景 - self.physics_node = self.node_path.attachNewNode(self.bullet_body) - self.physics_node.setTransform(self.node_path.getTransform()) - - # 物理属性 - self.friction = 0.5 - self.restitution = 0.1 - self.rolling_friction = 0.1 - self.spinning_friction = 0.1 - self.linear_damping = 0.0 - self.angular_damping = 0.0 - self.linear_factor = Vec3(1, 1, 1) - self.angular_factor = Vec3(1, 1, 1) - self.ccd_motion_threshold = 0.0 - self.ccd_swept_sphere_radius = 0.0 - self.anisotropic_friction = Vec3(1, 1, 1) - self.contact_processing_threshold = 1e30 - self.deactivation_time = 0.0 - self.linear_sleeping_threshold = 0.8 - self.angular_sleeping_threshold = 1.0 - - # 高级物理属性 - self.air_resistance = 0.0 - self.buoyancy = 0.0 - self.drag_coefficient = 0.0 - self.lift_coefficient = 0.0 - self.wind_resistance = 0.0 - self.magnetic_field = Vec3(0, 0, 0) - - # 力和扭矩历史记录 - self.force_history = deque(maxlen=100) - self.torque_history = deque(maxlen=100) - self.impulse_history = deque(maxlen=100) - - # 碰撞历史记录 - self.collision_history = deque(maxlen=50) - - # 运动历史记录 - self.position_history = deque(maxlen=100) - self.velocity_history = deque(maxlen=100) - self.angular_velocity_history = deque(maxlen=100) - self.acceleration_history = deque(maxlen=100) - - # 状态标志 - self.is_sleeping = False - self.is_kinematic_controlled = False - self.is_ghost = False - self.is_frozen = False - self.is_submerged = False - - # 自定义数据 - self.custom_data = {} - - # 时间戳 - self.creation_time = time.time() - self.last_update_time = self.creation_time - self.last_collision_time = 0.0 - - # 碰撞回调 - self.collision_callbacks = [] - - # 应用默认属性 - self._apply_physics_properties() - - # 记录初始位置 - self._record_state() - - print(f"刚体创建完成: {self.bullet_body.getName()}") - - def _create_collision_shape(self, shape_type, shape_params=None, compound_shapes=None): - """ - 创建碰撞形状 - - Args: - shape_type (str): 形状类型 - shape_params (dict): 形状参数 - compound_shapes (list): 复合形状列表 - - Returns: - BulletShape: 碰撞形状对象 - """ - shape_params = shape_params or {} - - if shape_type == self.SHAPE_COMPOUND and compound_shapes: - # 创建复合形状 - compound_shape = BulletCompoundShape() - - for shape_info in compound_shapes: - sub_shape_type = shape_info.get('type', 'box') - sub_shape_params = shape_info.get('params', {}) - transform = shape_info.get('transform', TransformState.makeIdentity()) - - sub_shape = self._create_single_shape(sub_shape_type, sub_shape_params) - compound_shape.addChildShape(transform, sub_shape) - - return compound_shape - - else: - # 创建单一形状 - return self._create_single_shape(shape_type, shape_params) - - def _create_single_shape(self, shape_type, shape_params): - """ - 创建单一碰撞形状 - - Args: - shape_type (str): 形状类型 - shape_params (dict): 形状参数 - - Returns: - BulletShape: 碰撞形状对象 - """ - bounds = self.node_path.getBounds() - margin = shape_params.get('margin', 0.04) # 默认边界 - - if shape_type == self.SHAPE_BOX: - # 盒体形状 - min_point = bounds.getMin() - max_point = bounds.getMax() - extents = (max_point - min_point) * 0.5 - extents.setX(max(extents.getX(), margin)) - extents.setY(max(extents.getY(), margin)) - extents.setZ(max(extents.getZ(), margin)) - shape = BulletBoxShape(extents) - - elif shape_type == self.SHAPE_SPHERE: - # 球体形状 - radius = bounds.getRadius() - radius = max(radius, margin) - shape = BulletSphereShape(radius) - - elif shape_type == self.SHAPE_CAPSULE: - # 胶囊形状 - radius = bounds.getRadius() - height = bounds.getMax().getZ() - bounds.getMin().getZ() - radius = max(radius, margin) - height = max(height, margin * 2) - shape = BulletCapsuleShape(radius, height, 2) # Z轴 - - elif shape_type == self.SHAPE_CONE: - # 圆锥形状 - radius = bounds.getRadius() - height = bounds.getMax().getZ() - bounds.getMin().getZ() - radius = max(radius, margin) - height = max(height, margin) - shape = BulletConeShape(radius, height, 2) # Z轴 - - elif shape_type == self.SHAPE_CYLINDER: - # 圆柱形状 - radius = bounds.getRadius() - height = bounds.getMax().getZ() - bounds.getMin().getZ() - radius = max(radius, margin) - height = max(height, margin) - shape = BulletCylinderShape(radius, height, 2) # Z轴 - - elif shape_type == self.SHAPE_CONVEX_HULL: - # 凸包形状 - shape = BulletConvexHullShape() - # 从节点几何体创建凸包 - geom_node = self.node_path.node() - if hasattr(geom_node, 'getGeom'): - geom = geom_node.getGeom() - shape.addGeom(geom) - else: - # 简化实现,使用包围盒 - min_point = bounds.getMin() - max_point = bounds.getMax() - shape.addPoint(min_point) - shape.addPoint(max_point) - - elif shape_type == self.SHAPE_MESH: - # 三角网格形状(用于静态物体) - mesh = BulletTriangleMesh() - # 从节点几何体创建网格 - geom_node = self.node_path.node() - if hasattr(geom_node, 'getGeom'): - geom = geom_node.getGeom() - mesh.addGeom(geom) - - dynamic = (self.body_type != self.STATIC) - shape = BulletTriangleMeshShape(mesh, dynamic=dynamic) - - else: - # 默认使用盒体形状 - min_point = bounds.getMin() - max_point = bounds.getMax() - extents = (max_point - min_point) * 0.5 - extents.setX(max(extents.getX(), margin)) - extents.setY(max(extents.getY(), margin)) - extents.setZ(max(extents.getZ(), margin)) - shape = BulletBoxShape(extents) - - # 设置形状边界 - shape.setMargin(margin) - - return shape - - def _apply_physics_properties(self): - """ - 应用物理属性到Bullet刚体 - """ - self.bullet_body.setFriction(self.friction) - self.bullet_body.setRestitution(self.restitution) - self.bullet_body.setRollingFriction(self.rolling_friction) - self.bullet_body.setSpinningFriction(self.spinning_friction) - self.bullet_body.setLinearDamping(self.linear_damping) - self.bullet_body.setAngularDamping(self.angular_damping) - self.bullet_body.setLinearFactor(self.linear_factor) - self.bullet_body.setAngularFactor(self.angular_factor) - self.bullet_body.setAnisotropicFriction(self.anisotropic_friction) - self.bullet_body.setContactProcessingThreshold(self.contact_processing_threshold) - self.bullet_body.setDeactivationTime(self.deactivation_time) - self.bullet_body.setSleepingThresholds( - self.linear_sleeping_threshold, - self.angular_sleeping_threshold - ) - - # 设置CCD参数 - if self.ccd_motion_threshold > 0: - self.bullet_body.setCcdMotionThreshold(self.ccd_motion_threshold) - if self.ccd_swept_sphere_radius > 0: - self.bullet_body.setCcdSweptSphereRadius(self.ccd_swept_sphere_radius) - - def set_friction(self, friction, static_friction=None, dynamic_friction=None): - """ - 设置摩擦系数 - - Args: - friction (float): 摩擦系数 - static_friction (float): 静摩擦系数(可选) - dynamic_friction (float): 动摩擦系数(可选) - """ - self.friction = friction - self.bullet_body.setFriction(friction) - - # 如果提供了静摩擦和动摩擦,可以使用更复杂的摩擦模型 - if static_friction is not None or dynamic_friction is not None: - # 这里可以实现更复杂的摩擦模型 - pass - - def set_restitution(self, restitution): - """ - 设置恢复系数(弹性) - - Args: - restitution (float): 恢复系数 - """ - self.restitution = restitution - self.bullet_body.setRestitution(restitution) - - def set_damping(self, linear_damping, angular_damping): - """ - 设置阻尼 - - Args: - linear_damping (float): 线性阻尼 - angular_damping (float): 角阻尼 - """ - self.linear_damping = linear_damping - self.angular_damping = angular_damping - self.bullet_body.setLinearDamping(linear_damping) - self.bullet_body.setAngularDamping(angular_damping) - - def set_factors(self, linear_factor, angular_factor): - """ - 设置线性和角因子 - - Args: - linear_factor (Vec3): 线性因子 - angular_factor (Vec3): 角因子 - """ - self.linear_factor = linear_factor - self.angular_factor = angular_factor - self.bullet_body.setLinearFactor(linear_factor) - self.bullet_body.setAngularFactor(angular_factor) - - def set_activation_state(self, active=True, always_active=False): - """ - 设置激活状态 - - Args: - active (bool): 是否激活 - always_active (bool): 是否始终保持激活 - """ - if always_active: - self.bullet_body.setDeactivationEnabled(False) - else: - self.bullet_body.setDeactivationEnabled(True) - self.bullet_body.setActive(active) - - def set_sleeping_thresholds(self, linear_threshold, angular_threshold): - """ - 设置休眠阈值 - - Args: - linear_threshold (float): 线性休眠阈值 - angular_threshold (float): 角休眠阈值 - """ - self.linear_sleeping_threshold = linear_threshold - self.angular_sleeping_threshold = angular_threshold - self.bullet_body.setSleepingThresholds(linear_threshold, angular_threshold) - - def set_ccd_parameters(self, motion_threshold, swept_sphere_radius): - """ - 设置连续碰撞检测参数 - - Args: - motion_threshold (float): 运动阈值 - swept_sphere_radius (float): 扫掠球半径 - """ - self.ccd_motion_threshold = motion_threshold - self.ccd_swept_sphere_radius = swept_sphere_radius - self.bullet_body.setCcdMotionThreshold(motion_threshold) - self.bullet_body.setCcdSweptSphereRadius(swept_sphere_radius) - - def set_anisotropic_friction(self, friction): - """ - 设置各向异性摩擦 - - Args: - friction (Vec3): 各向异性摩擦系数 - """ - self.anisotropic_friction = friction - self.bullet_body.setAnisotropicFriction(friction) - - def set_contact_threshold(self, threshold): - """ - 设置接触处理阈值 - - Args: - threshold (float): 接触阈值 - """ - self.contact_processing_threshold = threshold - self.bullet_body.setContactProcessingThreshold(threshold) - - def set_collision_filter(self, group=None, mask=None): - """ - 设置碰撞过滤器 - - Args: - group (int): 碰撞组 - mask (int): 碰撞掩码 - """ - if group is not None: - self.collision_group = group - self.bullet_body.setCollideFilterGroup(group) - if mask is not None: - self.collision_mask = mask - self.bullet_body.setCollideFilterMask(mask) - - def apply_force(self, force, position=None, duration=0.0): - """ - 应用力 - - Args: - force (Vec3): 力向量 - position (Point3): 作用点(可选) - duration (float): 持续时间(秒,0表示瞬时) - """ - # 记录力历史 - force_record = { - 'force': force, - 'position': position, - 'duration': duration, - 'timestamp': time.time() - } - self.force_history.append(force_record) - - if position is not None: - self.bullet_body.applyForce(force, position) - else: - self.bullet_body.applyCentralForce(force) - - def apply_torque(self, torque, duration=0.0): - """ - 应用扭矩 - - Args: - torque (Vec3): 扭矩向量 - duration (float): 持续时间(秒,0表示瞬时) - """ - # 记录扭矩历史 - torque_record = { - 'torque': torque, - 'duration': duration, - 'timestamp': time.time() - } - self.torque_history.append(torque_record) - - self.bullet_body.applyTorque(torque) - - def apply_impulse(self, impulse, position=None, duration=0.0): - """ - 应用冲量 - - Args: - impulse (Vec3): 冲量向量 - position (Point3): 作用点(可选) - duration (float): 持续时间(秒,0表示瞬时) - """ - # 记录冲量历史 - impulse_record = { - 'impulse': impulse, - 'position': position, - 'duration': duration, - 'timestamp': time.time() - } - self.impulse_history.append(impulse_record) - - if position is not None: - self.bullet_body.applyImpulse(impulse, position) - else: - self.bullet_body.applyCentralImpulse(impulse) - - def apply_torque_impulse(self, torque_impulse): - """ - 应用扭矩冲量 - - Args: - torque_impulse (Vec3): 扭矩冲量向量 - """ - # 记录扭矩冲量历史 - torque_record = { - 'torque': torque_impulse, - 'duration': 0.0, - 'is_impulse': True, - 'timestamp': time.time() - } - self.torque_history.append(torque_record) - - self.bullet_body.applyTorqueImpulse(torque_impulse) - - def apply_force_field(self, field_type, field_params): - """ - 应用力场效果 - - Args: - field_type (str): 力场类型 ('gravity', 'wind', 'magnetic', 'buoyancy') - field_params (dict): 力场参数 - """ - if field_type == 'gravity': - gravity = field_params.get('gravity', Vec3(0, 0, -9.81)) - self.bullet_body.applyCentralForce(gravity * self.mass) - elif field_type == 'wind': - wind_vector = field_params.get('wind_vector', Vec3(0, 0, 0)) - wind_coefficient = field_params.get('wind_coefficient', 1.0) - self.bullet_body.applyCentralForce(wind_vector * wind_coefficient) - elif field_type == 'magnetic': - magnetic_field = field_params.get('magnetic_field', Vec3(0, 0, 0)) - magnetic_coefficient = field_params.get('magnetic_coefficient', 1.0) - self.bullet_body.applyCentralForce(magnetic_field * magnetic_coefficient) - elif field_type == 'buoyancy': - fluid_density = field_params.get('fluid_density', 1000.0) - volume = self.mass / 1000.0 # 简化计算 - buoyancy_force = Vec3(0, 0, fluid_density * 9.81 * volume) - self.bullet_body.applyCentralForce(buoyancy_force) - - def get_linear_velocity(self): - """ - 获取线性速度 - - Returns: - Vec3: 线性速度 - """ - return self.bullet_body.getLinearVelocity() - - def set_linear_velocity(self, velocity): - """ - 设置线性速度 - - Args: - velocity (Vec3): 线性速度 - """ - self.bullet_body.setLinearVelocity(velocity) - - def get_angular_velocity(self): - """ - 获取角速度 - - Returns: - Vec3: 角速度 - """ - return self.bullet_body.getAngularVelocity() - - def set_angular_velocity(self, velocity): - """ - 设置角速度 - - Args: - velocity (Vec3): 角速度 - """ - self.bullet_body.setAngularVelocity(velocity) - - def get_position(self): - """ - 获取位置 - - Returns: - Point3: 位置 - """ - return self.physics_node.getPos() - - def set_position(self, position): - """ - 设置位置 - - Args: - position (Point3): 位置 - """ - self.physics_node.setPos(position) - if self.body_type == self.KINEMATIC: - self.bullet_body.setKinematicPos(position) - - def get_rotation(self): - """ - 获取旋转 - - Returns: - Vec3: 旋转(HPR) - """ - return self.physics_node.getHpr() - - def set_rotation(self, rotation): - """ - 设置旋转 - - Args: - rotation (Vec3): 旋转(HPR) - """ - self.physics_node.setHpr(rotation) - if self.body_type == self.KINEMATIC: - self.bullet_body.setKinematicHpr(rotation) - - def get_transform(self): - """ - 获取变换矩阵 - - Returns: - TransformState: 变换状态 - """ - return self.physics_node.getTransform() - - def set_transform(self, transform): - """ - 设置变换矩阵 - - Args: - transform (TransformState): 变换状态 - """ - self.physics_node.setTransform(transform) - if self.body_type == self.KINEMATIC: - self.bullet_body.setKinematicPos(transform.getPos()) - self.bullet_body.setKinematicHpr(transform.getHpr()) - - def sync_visual_node(self): - """ - 同步物理节点和可视化节点的位置 - """ - if self.node_path and not self.node_path.isEmpty(): - self.node_path.setTransform(self.physics_node.getTransform()) - - def get_mass(self): - """ - 获取质量 - - Returns: - float: 质量 - """ - return self.bullet_body.getMass() - - def set_mass(self, mass): - """ - 设置质量 - - Args: - mass (float): 质量 - """ - self.mass = mass - if self.body_type != self.STATIC: - self.bullet_body.setMass(mass) - - def get_inertia(self): - """ - 获取惯性张量 - - Returns: - Vec3: 惯性张量 - """ - return self.bullet_body.getInertia() - - def set_inertia(self, inertia): - """ - 设置惯性张量 - - Args: - inertia (Vec3): 惯性张量 - """ - # 注意:Bullet可能不直接支持设置惯性张量 - pass - - def is_active(self): - """ - 检查刚体是否激活 - - Returns: - bool: 是否激活 - """ - return self.bullet_body.isActive() - - def is_static(self): - """ - 检查刚体是否为静态 - - Returns: - bool: 是否为静态 - """ - return self.body_type == self.STATIC - - def is_dynamic(self): - """ - 检查刚体是否为动态 - - Returns: - bool: 是否为动态 - """ - return self.body_type == self.DYNAMIC - - def is_kinematic(self): - """ - 检查刚体是否为运动学 - - Returns: - bool: 是否为运动学 - """ - return self.body_type == self.KINEMATIC - - def get_aabb(self): - """ - 获取包围盒 - - Returns: - tuple: (min_point, max_point) - """ - aabb = self.bullet_body.getAabb() - return (aabb.getMin(), aabb.getMax()) - - def get_center_of_mass(self): - """ - 获取质心位置 - - Returns: - Point3: 质心位置 - """ - return self.bullet_body.getCenterOfMassTransform().getPos() - - def set_center_of_mass(self, center): - """ - 设置质心位置 - - Args: - center (Point3): 质心位置 - """ - # 注意:Bullet可能不直接支持设置质心 - pass - - def add_collision_callback(self, callback): - """ - 添加碰撞回调 - - Args: - callback (function): 回调函数 - """ - if callback not in self.collision_callbacks: - self.collision_callbacks.append(callback) - - def remove_collision_callback(self, callback): - """ - 移除碰撞回调 - - Args: - callback (function): 回调函数 - """ - if callback in self.collision_callbacks: - self.collision_callbacks.remove(callback) - - def _record_state(self): - """ - 记录当前状态 - """ - current_time = time.time() - self.last_update_time = current_time - - # 记录位置和速度 - pos = self.get_position() - vel = self.get_linear_velocity() - ang_vel = self.get_angular_velocity() - - self.position_history.append((pos, current_time)) - self.velocity_history.append((vel, current_time)) - self.angular_velocity_history.append((ang_vel, current_time)) - - # 计算并记录加速度 - if len(self.velocity_history) >= 2: - vel2, time2 = self.velocity_history[-1] - vel1, time1 = self.velocity_history[-2] - if time2 - time1 > 0: - acceleration = (vel2 - vel1) / (time2 - time1) - self.acceleration_history.append((acceleration, current_time)) - - # 更新休眠状态 - self.is_sleeping = not self.bullet_body.isActive() - - def get_velocity_history(self, duration=1.0): - """ - 获取指定时间段内的速度历史 - - Args: - duration (float): 时间段(秒) - - Returns: - list: 速度历史记录 - """ - current_time = time.time() - start_time = current_time - duration - - history = [] - for vel, timestamp in self.velocity_history: - if timestamp >= start_time: - history.append((vel, timestamp)) - - return history - - def get_acceleration(self, time_window=0.1): - """ - 计算加速度 - - Args: - time_window (float): 时间窗口(秒) - - Returns: - Vec3: 加速度向量 - """ - history = self.get_velocity_history(time_window) - if len(history) < 2: - return Vec3(0, 0, 0) - - # 计算最近两个时间点的速度差 - vel2, time2 = history[-1] - vel1, time1 = history[-2] - - if time2 - time1 <= 0: - return Vec3(0, 0, 0) - - acceleration = (vel2 - vel1) / (time2 - time1) - return acceleration - - def get_kinetic_energy(self): - """ - 计算动能 - - Returns: - float: 动能 - """ - velocity = self.get_linear_velocity() - angular_velocity = self.get_angular_velocity() - inertia = self.get_inertia() - - # 平移动能: 0.5 * m * v^2 - linear_ke = 0.5 * self.mass * velocity.lengthSquared() - - # 转动动能: 0.5 * I * ω^2 - angular_ke = 0.5 * inertia.length() * angular_velocity.lengthSquared() - - return linear_ke + angular_ke - - def get_potential_energy(self, gravity=Vec3(0, 0, -9.81)): - """ - 计算重力势能 - - Args: - gravity (Vec3): 重力向量 - - Returns: - float: 重力势能 - """ - height = self.get_position().getZ() - # E = m * g * h - return self.mass * abs(gravity.getZ()) * height - - def get_total_energy(self, gravity=Vec3(0, 0, -9.81)): - """ - 计算总能量(动能+势能) - - Args: - gravity (Vec3): 重力向量 - - Returns: - float: 总能量 - """ - return self.get_kinetic_energy() + self.get_potential_energy(gravity) - - def get_momentum(self): - """ - 计算动量 - - Returns: - Vec3: 动量向量 - """ - velocity = self.get_linear_velocity() - return velocity * self.mass - - def get_angular_momentum(self): - """ - 计算角动量 - - Returns: - Vec3: 角动量向量 - """ - angular_velocity = self.get_angular_velocity() - inertia = self.get_inertia() - return angular_velocity * inertia.length() - - def set_custom_data(self, key, value): - """ - 设置自定义数据 - - Args: - key (str): 键 - value: 值 - """ - self.custom_data[key] = value - - def get_custom_data(self, key, default=None): - """ - 获取自定义数据 - - Args: - key (str): 键 - default: 默认值 - - Returns: - 自定义数据值 - """ - return self.custom_data.get(key, default) - - def freeze(self, frozen=True): - """ - 冻结或解冻刚体 - - Args: - frozen (bool): 是否冻结 - """ - self.is_frozen = frozen - if frozen: - self.bullet_body.setActivationState(0) # 禁用激活 - else: - self.bullet_body.setActivationState(1) # 启用激活 - - def set_ghost(self, is_ghost=True): - """ - 设置为幽灵对象(不参与碰撞检测) - - Args: - is_ghost (bool): 是否为幽灵对象 - """ - self.is_ghost = is_ghost - if is_ghost: - self.bullet_body.setCollideFilterGroup(0) - self.bullet_body.setCollideFilterMask(0) - else: - self.bullet_body.setCollideFilterGroup(self.collision_group) - self.bullet_body.setCollideFilterMask(self.collision_mask) - - def get_collision_info(self): - """ - 获取碰撞信息 - - Returns: - dict: 碰撞信息 - """ - return { - 'collision_count': len(self.collision_history), - 'last_collision_time': self.last_collision_time, - 'is_colliding': len(self.collision_history) > 0 and \ - time.time() - self.last_collision_time < 0.1 - } - - def add_collision_event(self, other_body, contact_points, impulse): - """ - 添加碰撞事件 - - Args: - other_body (RigidBody): 另一个刚体 - contact_points (list): 接触点列表 - impulse (float): 冲量 - """ - collision_info = { - 'other_body': other_body, - 'contact_points': contact_points, - 'impulse': impulse, - 'timestamp': time.time() - } - self.collision_history.append(collision_info) - self.last_collision_time = time.time() - - # 调用碰撞回调 - for callback in self.collision_callbacks: - try: - callback(self, other_body, contact_points, impulse) - except Exception as e: - print(f"碰撞回调执行错误: {e}") - - def clear_collision_history(self): - """ - 清除碰撞历史 - """ - self.collision_history.clear() - self.last_collision_time = 0.0 - - def get_force_history(self, duration=1.0): - """ - 获取指定时间段内的力历史 - - Args: - duration (float): 时间段(秒) - - Returns: - list: 力历史记录 - """ - current_time = time.time() - start_time = current_time - duration - - history = [] - for force_record in self.force_history: - if force_record['timestamp'] >= start_time: - history.append(force_record) - - return history - - def get_torque_history(self, duration=1.0): - """ - 获取指定时间段内的扭矩历史 - - Args: - duration (float): 时间段(秒) - - Returns: - list: 扭矩历史记录 - """ - current_time = time.time() - start_time = current_time - duration - - history = [] - for torque_record in self.torque_history: - if torque_record['timestamp'] >= start_time: - history.append(torque_record) - - return history - - def get_impulse_history(self, duration=1.0): - """ - 获取指定时间段内的冲量历史 - - Args: - duration (float): 时间段(秒) - - Returns: - list: 冲量历史记录 - """ - current_time = time.time() - start_time = current_time - duration - - history = [] - for impulse_record in self.impulse_history: - if impulse_record['timestamp'] >= start_time: - history.append(impulse_record) - - return history - - def get_statistics(self): - """ - 获取刚体统计信息 - - Returns: - dict: 统计信息 - """ - return { - 'name': self.bullet_body.getName(), - 'body_type': self.body_type, - 'mass': self.get_mass(), - 'position': self.get_position(), - 'velocity': self.get_linear_velocity(), - 'angular_velocity': self.get_angular_velocity(), - 'kinetic_energy': self.get_kinetic_energy(), - 'potential_energy': self.get_potential_energy(), - 'total_energy': self.get_total_energy(), - 'is_active': self.is_active(), - 'is_sleeping': self.is_sleeping, - 'collision_count': len(self.collision_history), - 'force_count': len(self.force_history), - 'torque_count': len(self.torque_history), - 'impulse_count': len(self.impulse_history), - 'age': time.time() - self.creation_time - } - - def reset_forces(self): - """ - 重置所有力和扭矩 - """ - self.force_history.clear() - self.torque_history.clear() - self.impulse_history.clear() - - def reset_motion(self): - """ - 重置运动状态 - """ - self.set_linear_velocity(Vec3(0, 0, 0)) - self.set_angular_velocity(Vec3(0, 0, 0)) - self.reset_forces() - - def destroy(self): - """ - 销毁刚体 - """ - # 清理历史记录 - self.force_history.clear() - self.torque_history.clear() - self.impulse_history.clear() - self.collision_history.clear() - self.position_history.clear() - self.velocity_history.clear() - self.angular_velocity_history.clear() - self.acceleration_history.clear() - - # 清理回调 - self.collision_callbacks.clear() - - # 清理自定义数据 - self.custom_data.clear() - - # 移除物理节点 - if self.physics_node and not self.physics_node.isEmpty(): - self.physics_node.removeNode() - +""" +刚体系统核心模块 +负责刚体的创建、配置和管理 +""" + +from panda3d.core import Vec3, Point3, TransformState, LVector3f +from panda3d.bullet import BulletRigidBodyNode, BulletBoxShape, BulletSphereShape +from panda3d.bullet import BulletCapsuleShape, BulletConeShape, BulletCylinderShape +from panda3d.bullet import BulletConvexHullShape, BulletTriangleMeshShape, BulletTriangleMesh +from panda3d.bullet import BulletCollisionShape, BulletCompoundShape +import math +import time +from collections import deque + + +class RigidBody: + """ + 刚体类 + 管理单个刚体的物理属性和行为 + """ + + # 刚体类型常量 + STATIC = 0 # 静态刚体 + DYNAMIC = 1 # 动态刚体 + KINEMATIC = 2 # 运动学刚体 + + # 碰撞形状类型常量 + SHAPE_BOX = 'box' + SHAPE_SPHERE = 'sphere' + SHAPE_CAPSULE = 'capsule' + SHAPE_CONE = 'cone' + SHAPE_CYLINDER = 'cylinder' + SHAPE_CONVEX_HULL = 'convex_hull' + SHAPE_MESH = 'mesh' + SHAPE_COMPOUND = 'compound' + + # 碰撞组常量 + GROUP_STATIC = 1 + GROUP_DYNAMIC = 2 + GROUP_KINEMATIC = 4 + GROUP_SENSOR = 8 + GROUP_VEHICLE = 16 + GROUP_CHARACTER = 32 + GROUP_PARTICLE = 64 + GROUP_CUSTOM1 = 128 + GROUP_CUSTOM2 = 256 + + def __init__(self, node_path, shape_type='box', mass=1.0, body_type=DYNAMIC, + shape_params=None, compound_shapes=None, collision_group=None, collision_mask=None): + """ + 初始化刚体 + + Args: + node_path: Panda3D节点路径 + shape_type (str): 碰撞形状类型 + mass (float): 质量 + body_type (int): 刚体类型 + shape_params (dict): 形状参数 + compound_shapes (list): 复合形状列表 + collision_group (int): 碰撞组 + collision_mask (int): 碰撞掩码 + """ + self.node_path = node_path + self.mass = mass + self.body_type = body_type + self.shape_type = shape_type + self.shape_params = shape_params or {} + self.compound_shapes = compound_shapes or [] + self.collision_group = collision_group or self.GROUP_DYNAMIC + self.collision_mask = collision_mask or 0xFFFFFFFF + + # 创建碰撞形状 + self.shape = self._create_collision_shape(shape_type, shape_params, compound_shapes) + + # 创建Bullet刚体节点 + self.bullet_body = BulletRigidBodyNode('RigidBody') + self.bullet_body.addShape(self.shape) + + # 设置刚体属性 + if body_type == self.STATIC: + self.bullet_body.setMass(0) + self.collision_group = self.GROUP_STATIC + else: + self.bullet_body.setMass(mass) + + if body_type == self.KINEMATIC: + self.bullet_body.setKinematic(True) + self.collision_group = self.GROUP_KINEMATIC + + # 设置碰撞组和掩码 + self.bullet_body.setCollideFilterGroup(self.collision_group) + self.bullet_body.setCollideFilterMask(self.collision_mask) + + # 创建物理节点并附加到场景 + self.physics_node = self.node_path.attachNewNode(self.bullet_body) + self.physics_node.setTransform(self.node_path.getTransform()) + + # 物理属性 + self.friction = 0.5 + self.restitution = 0.1 + self.rolling_friction = 0.1 + self.spinning_friction = 0.1 + self.linear_damping = 0.0 + self.angular_damping = 0.0 + self.linear_factor = Vec3(1, 1, 1) + self.angular_factor = Vec3(1, 1, 1) + self.ccd_motion_threshold = 0.0 + self.ccd_swept_sphere_radius = 0.0 + self.anisotropic_friction = Vec3(1, 1, 1) + self.contact_processing_threshold = 1e30 + self.deactivation_time = 0.0 + self.linear_sleeping_threshold = 0.8 + self.angular_sleeping_threshold = 1.0 + + # 高级物理属性 + self.air_resistance = 0.0 + self.buoyancy = 0.0 + self.drag_coefficient = 0.0 + self.lift_coefficient = 0.0 + self.wind_resistance = 0.0 + self.magnetic_field = Vec3(0, 0, 0) + + # 力和扭矩历史记录 + self.force_history = deque(maxlen=100) + self.torque_history = deque(maxlen=100) + self.impulse_history = deque(maxlen=100) + + # 碰撞历史记录 + self.collision_history = deque(maxlen=50) + + # 运动历史记录 + self.position_history = deque(maxlen=100) + self.velocity_history = deque(maxlen=100) + self.angular_velocity_history = deque(maxlen=100) + self.acceleration_history = deque(maxlen=100) + + # 状态标志 + self.is_sleeping = False + self.is_kinematic_controlled = False + self.is_ghost = False + self.is_frozen = False + self.is_submerged = False + + # 自定义数据 + self.custom_data = {} + + # 时间戳 + self.creation_time = time.time() + self.last_update_time = self.creation_time + self.last_collision_time = 0.0 + + # 碰撞回调 + self.collision_callbacks = [] + + # 应用默认属性 + self._apply_physics_properties() + + # 记录初始位置 + self._record_state() + + print(f"刚体创建完成: {self.bullet_body.getName()}") + + def _create_collision_shape(self, shape_type, shape_params=None, compound_shapes=None): + """ + 创建碰撞形状 + + Args: + shape_type (str): 形状类型 + shape_params (dict): 形状参数 + compound_shapes (list): 复合形状列表 + + Returns: + BulletShape: 碰撞形状对象 + """ + shape_params = shape_params or {} + + if shape_type == self.SHAPE_COMPOUND and compound_shapes: + # 创建复合形状 + compound_shape = BulletCompoundShape() + + for shape_info in compound_shapes: + sub_shape_type = shape_info.get('type', 'box') + sub_shape_params = shape_info.get('params', {}) + transform = shape_info.get('transform', TransformState.makeIdentity()) + + sub_shape = self._create_single_shape(sub_shape_type, sub_shape_params) + compound_shape.addChildShape(transform, sub_shape) + + return compound_shape + + else: + # 创建单一形状 + return self._create_single_shape(shape_type, shape_params) + + def _create_single_shape(self, shape_type, shape_params): + """ + 创建单一碰撞形状 + + Args: + shape_type (str): 形状类型 + shape_params (dict): 形状参数 + + Returns: + BulletShape: 碰撞形状对象 + """ + bounds = self.node_path.getBounds() + margin = shape_params.get('margin', 0.04) # 默认边界 + + if shape_type == self.SHAPE_BOX: + # 盒体形状 + min_point = bounds.getMin() + max_point = bounds.getMax() + extents = (max_point - min_point) * 0.5 + extents.setX(max(extents.getX(), margin)) + extents.setY(max(extents.getY(), margin)) + extents.setZ(max(extents.getZ(), margin)) + shape = BulletBoxShape(extents) + + elif shape_type == self.SHAPE_SPHERE: + # 球体形状 + radius = bounds.getRadius() + radius = max(radius, margin) + shape = BulletSphereShape(radius) + + elif shape_type == self.SHAPE_CAPSULE: + # 胶囊形状 + radius = bounds.getRadius() + height = bounds.getMax().getZ() - bounds.getMin().getZ() + radius = max(radius, margin) + height = max(height, margin * 2) + shape = BulletCapsuleShape(radius, height, 2) # Z轴 + + elif shape_type == self.SHAPE_CONE: + # 圆锥形状 + radius = bounds.getRadius() + height = bounds.getMax().getZ() - bounds.getMin().getZ() + radius = max(radius, margin) + height = max(height, margin) + shape = BulletConeShape(radius, height, 2) # Z轴 + + elif shape_type == self.SHAPE_CYLINDER: + # 圆柱形状 + radius = bounds.getRadius() + height = bounds.getMax().getZ() - bounds.getMin().getZ() + radius = max(radius, margin) + height = max(height, margin) + shape = BulletCylinderShape(radius, height, 2) # Z轴 + + elif shape_type == self.SHAPE_CONVEX_HULL: + # 凸包形状 + shape = BulletConvexHullShape() + # 从节点几何体创建凸包 + geom_node = self.node_path.node() + if hasattr(geom_node, 'getGeom'): + geom = geom_node.getGeom() + shape.addGeom(geom) + else: + # 简化实现,使用包围盒 + min_point = bounds.getMin() + max_point = bounds.getMax() + shape.addPoint(min_point) + shape.addPoint(max_point) + + elif shape_type == self.SHAPE_MESH: + # 三角网格形状(用于静态物体) + mesh = BulletTriangleMesh() + # 从节点几何体创建网格 + geom_node = self.node_path.node() + if hasattr(geom_node, 'getGeom'): + geom = geom_node.getGeom() + mesh.addGeom(geom) + + dynamic = (self.body_type != self.STATIC) + shape = BulletTriangleMeshShape(mesh, dynamic=dynamic) + + else: + # 默认使用盒体形状 + min_point = bounds.getMin() + max_point = bounds.getMax() + extents = (max_point - min_point) * 0.5 + extents.setX(max(extents.getX(), margin)) + extents.setY(max(extents.getY(), margin)) + extents.setZ(max(extents.getZ(), margin)) + shape = BulletBoxShape(extents) + + # 设置形状边界 + shape.setMargin(margin) + + return shape + + def _apply_physics_properties(self): + """ + 应用物理属性到Bullet刚体 + """ + self.bullet_body.setFriction(self.friction) + self.bullet_body.setRestitution(self.restitution) + self.bullet_body.setRollingFriction(self.rolling_friction) + self.bullet_body.setSpinningFriction(self.spinning_friction) + self.bullet_body.setLinearDamping(self.linear_damping) + self.bullet_body.setAngularDamping(self.angular_damping) + self.bullet_body.setLinearFactor(self.linear_factor) + self.bullet_body.setAngularFactor(self.angular_factor) + self.bullet_body.setAnisotropicFriction(self.anisotropic_friction) + self.bullet_body.setContactProcessingThreshold(self.contact_processing_threshold) + self.bullet_body.setDeactivationTime(self.deactivation_time) + self.bullet_body.setSleepingThresholds( + self.linear_sleeping_threshold, + self.angular_sleeping_threshold + ) + + # 设置CCD参数 + if self.ccd_motion_threshold > 0: + self.bullet_body.setCcdMotionThreshold(self.ccd_motion_threshold) + if self.ccd_swept_sphere_radius > 0: + self.bullet_body.setCcdSweptSphereRadius(self.ccd_swept_sphere_radius) + + def set_friction(self, friction, static_friction=None, dynamic_friction=None): + """ + 设置摩擦系数 + + Args: + friction (float): 摩擦系数 + static_friction (float): 静摩擦系数(可选) + dynamic_friction (float): 动摩擦系数(可选) + """ + self.friction = friction + self.bullet_body.setFriction(friction) + + # 如果提供了静摩擦和动摩擦,可以使用更复杂的摩擦模型 + if static_friction is not None or dynamic_friction is not None: + # 这里可以实现更复杂的摩擦模型 + pass + + def set_restitution(self, restitution): + """ + 设置恢复系数(弹性) + + Args: + restitution (float): 恢复系数 + """ + self.restitution = restitution + self.bullet_body.setRestitution(restitution) + + def set_damping(self, linear_damping, angular_damping): + """ + 设置阻尼 + + Args: + linear_damping (float): 线性阻尼 + angular_damping (float): 角阻尼 + """ + self.linear_damping = linear_damping + self.angular_damping = angular_damping + self.bullet_body.setLinearDamping(linear_damping) + self.bullet_body.setAngularDamping(angular_damping) + + def set_factors(self, linear_factor, angular_factor): + """ + 设置线性和角因子 + + Args: + linear_factor (Vec3): 线性因子 + angular_factor (Vec3): 角因子 + """ + self.linear_factor = linear_factor + self.angular_factor = angular_factor + self.bullet_body.setLinearFactor(linear_factor) + self.bullet_body.setAngularFactor(angular_factor) + + def set_activation_state(self, active=True, always_active=False): + """ + 设置激活状态 + + Args: + active (bool): 是否激活 + always_active (bool): 是否始终保持激活 + """ + if always_active: + self.bullet_body.setDeactivationEnabled(False) + else: + self.bullet_body.setDeactivationEnabled(True) + self.bullet_body.setActive(active) + + def set_sleeping_thresholds(self, linear_threshold, angular_threshold): + """ + 设置休眠阈值 + + Args: + linear_threshold (float): 线性休眠阈值 + angular_threshold (float): 角休眠阈值 + """ + self.linear_sleeping_threshold = linear_threshold + self.angular_sleeping_threshold = angular_threshold + self.bullet_body.setSleepingThresholds(linear_threshold, angular_threshold) + + def set_ccd_parameters(self, motion_threshold, swept_sphere_radius): + """ + 设置连续碰撞检测参数 + + Args: + motion_threshold (float): 运动阈值 + swept_sphere_radius (float): 扫掠球半径 + """ + self.ccd_motion_threshold = motion_threshold + self.ccd_swept_sphere_radius = swept_sphere_radius + self.bullet_body.setCcdMotionThreshold(motion_threshold) + self.bullet_body.setCcdSweptSphereRadius(swept_sphere_radius) + + def set_anisotropic_friction(self, friction): + """ + 设置各向异性摩擦 + + Args: + friction (Vec3): 各向异性摩擦系数 + """ + self.anisotropic_friction = friction + self.bullet_body.setAnisotropicFriction(friction) + + def set_contact_threshold(self, threshold): + """ + 设置接触处理阈值 + + Args: + threshold (float): 接触阈值 + """ + self.contact_processing_threshold = threshold + self.bullet_body.setContactProcessingThreshold(threshold) + + def set_collision_filter(self, group=None, mask=None): + """ + 设置碰撞过滤器 + + Args: + group (int): 碰撞组 + mask (int): 碰撞掩码 + """ + if group is not None: + self.collision_group = group + self.bullet_body.setCollideFilterGroup(group) + if mask is not None: + self.collision_mask = mask + self.bullet_body.setCollideFilterMask(mask) + + def apply_force(self, force, position=None, duration=0.0): + """ + 应用力 + + Args: + force (Vec3): 力向量 + position (Point3): 作用点(可选) + duration (float): 持续时间(秒,0表示瞬时) + """ + # 记录力历史 + force_record = { + 'force': force, + 'position': position, + 'duration': duration, + 'timestamp': time.time() + } + self.force_history.append(force_record) + + if position is not None: + self.bullet_body.applyForce(force, position) + else: + self.bullet_body.applyCentralForce(force) + + def apply_torque(self, torque, duration=0.0): + """ + 应用扭矩 + + Args: + torque (Vec3): 扭矩向量 + duration (float): 持续时间(秒,0表示瞬时) + """ + # 记录扭矩历史 + torque_record = { + 'torque': torque, + 'duration': duration, + 'timestamp': time.time() + } + self.torque_history.append(torque_record) + + self.bullet_body.applyTorque(torque) + + def apply_impulse(self, impulse, position=None, duration=0.0): + """ + 应用冲量 + + Args: + impulse (Vec3): 冲量向量 + position (Point3): 作用点(可选) + duration (float): 持续时间(秒,0表示瞬时) + """ + # 记录冲量历史 + impulse_record = { + 'impulse': impulse, + 'position': position, + 'duration': duration, + 'timestamp': time.time() + } + self.impulse_history.append(impulse_record) + + if position is not None: + self.bullet_body.applyImpulse(impulse, position) + else: + self.bullet_body.applyCentralImpulse(impulse) + + def apply_torque_impulse(self, torque_impulse): + """ + 应用扭矩冲量 + + Args: + torque_impulse (Vec3): 扭矩冲量向量 + """ + # 记录扭矩冲量历史 + torque_record = { + 'torque': torque_impulse, + 'duration': 0.0, + 'is_impulse': True, + 'timestamp': time.time() + } + self.torque_history.append(torque_record) + + self.bullet_body.applyTorqueImpulse(torque_impulse) + + def apply_force_field(self, field_type, field_params): + """ + 应用力场效果 + + Args: + field_type (str): 力场类型 ('gravity', 'wind', 'magnetic', 'buoyancy') + field_params (dict): 力场参数 + """ + if field_type == 'gravity': + gravity = field_params.get('gravity', Vec3(0, 0, -9.81)) + self.bullet_body.applyCentralForce(gravity * self.mass) + elif field_type == 'wind': + wind_vector = field_params.get('wind_vector', Vec3(0, 0, 0)) + wind_coefficient = field_params.get('wind_coefficient', 1.0) + self.bullet_body.applyCentralForce(wind_vector * wind_coefficient) + elif field_type == 'magnetic': + magnetic_field = field_params.get('magnetic_field', Vec3(0, 0, 0)) + magnetic_coefficient = field_params.get('magnetic_coefficient', 1.0) + self.bullet_body.applyCentralForce(magnetic_field * magnetic_coefficient) + elif field_type == 'buoyancy': + fluid_density = field_params.get('fluid_density', 1000.0) + volume = self.mass / 1000.0 # 简化计算 + buoyancy_force = Vec3(0, 0, fluid_density * 9.81 * volume) + self.bullet_body.applyCentralForce(buoyancy_force) + + def get_linear_velocity(self): + """ + 获取线性速度 + + Returns: + Vec3: 线性速度 + """ + return self.bullet_body.getLinearVelocity() + + def set_linear_velocity(self, velocity): + """ + 设置线性速度 + + Args: + velocity (Vec3): 线性速度 + """ + self.bullet_body.setLinearVelocity(velocity) + + def get_angular_velocity(self): + """ + 获取角速度 + + Returns: + Vec3: 角速度 + """ + return self.bullet_body.getAngularVelocity() + + def set_angular_velocity(self, velocity): + """ + 设置角速度 + + Args: + velocity (Vec3): 角速度 + """ + self.bullet_body.setAngularVelocity(velocity) + + def get_position(self): + """ + 获取位置 + + Returns: + Point3: 位置 + """ + return self.physics_node.getPos() + + def set_position(self, position): + """ + 设置位置 + + Args: + position (Point3): 位置 + """ + self.physics_node.setPos(position) + if self.body_type == self.KINEMATIC: + self.bullet_body.setKinematicPos(position) + + def get_rotation(self): + """ + 获取旋转 + + Returns: + Vec3: 旋转(HPR) + """ + return self.physics_node.getHpr() + + def set_rotation(self, rotation): + """ + 设置旋转 + + Args: + rotation (Vec3): 旋转(HPR) + """ + self.physics_node.setHpr(rotation) + if self.body_type == self.KINEMATIC: + self.bullet_body.setKinematicHpr(rotation) + + def get_transform(self): + """ + 获取变换矩阵 + + Returns: + TransformState: 变换状态 + """ + return self.physics_node.getTransform() + + def set_transform(self, transform): + """ + 设置变换矩阵 + + Args: + transform (TransformState): 变换状态 + """ + self.physics_node.setTransform(transform) + if self.body_type == self.KINEMATIC: + self.bullet_body.setKinematicPos(transform.getPos()) + self.bullet_body.setKinematicHpr(transform.getHpr()) + + def sync_visual_node(self): + """ + 同步物理节点和可视化节点的位置 + """ + if self.node_path and not self.node_path.isEmpty(): + self.node_path.setTransform(self.physics_node.getTransform()) + + def get_mass(self): + """ + 获取质量 + + Returns: + float: 质量 + """ + return self.bullet_body.getMass() + + def set_mass(self, mass): + """ + 设置质量 + + Args: + mass (float): 质量 + """ + self.mass = mass + if self.body_type != self.STATIC: + self.bullet_body.setMass(mass) + + def get_inertia(self): + """ + 获取惯性张量 + + Returns: + Vec3: 惯性张量 + """ + return self.bullet_body.getInertia() + + def set_inertia(self, inertia): + """ + 设置惯性张量 + + Args: + inertia (Vec3): 惯性张量 + """ + # 注意:Bullet可能不直接支持设置惯性张量 + pass + + def is_active(self): + """ + 检查刚体是否激活 + + Returns: + bool: 是否激活 + """ + return self.bullet_body.isActive() + + def is_static(self): + """ + 检查刚体是否为静态 + + Returns: + bool: 是否为静态 + """ + return self.body_type == self.STATIC + + def is_dynamic(self): + """ + 检查刚体是否为动态 + + Returns: + bool: 是否为动态 + """ + return self.body_type == self.DYNAMIC + + def is_kinematic(self): + """ + 检查刚体是否为运动学 + + Returns: + bool: 是否为运动学 + """ + return self.body_type == self.KINEMATIC + + def get_aabb(self): + """ + 获取包围盒 + + Returns: + tuple: (min_point, max_point) + """ + aabb = self.bullet_body.getAabb() + return (aabb.getMin(), aabb.getMax()) + + def get_center_of_mass(self): + """ + 获取质心位置 + + Returns: + Point3: 质心位置 + """ + return self.bullet_body.getCenterOfMassTransform().getPos() + + def set_center_of_mass(self, center): + """ + 设置质心位置 + + Args: + center (Point3): 质心位置 + """ + # 注意:Bullet可能不直接支持设置质心 + pass + + def add_collision_callback(self, callback): + """ + 添加碰撞回调 + + Args: + callback (function): 回调函数 + """ + if callback not in self.collision_callbacks: + self.collision_callbacks.append(callback) + + def remove_collision_callback(self, callback): + """ + 移除碰撞回调 + + Args: + callback (function): 回调函数 + """ + if callback in self.collision_callbacks: + self.collision_callbacks.remove(callback) + + def _record_state(self): + """ + 记录当前状态 + """ + current_time = time.time() + self.last_update_time = current_time + + # 记录位置和速度 + pos = self.get_position() + vel = self.get_linear_velocity() + ang_vel = self.get_angular_velocity() + + self.position_history.append((pos, current_time)) + self.velocity_history.append((vel, current_time)) + self.angular_velocity_history.append((ang_vel, current_time)) + + # 计算并记录加速度 + if len(self.velocity_history) >= 2: + vel2, time2 = self.velocity_history[-1] + vel1, time1 = self.velocity_history[-2] + if time2 - time1 > 0: + acceleration = (vel2 - vel1) / (time2 - time1) + self.acceleration_history.append((acceleration, current_time)) + + # 更新休眠状态 + self.is_sleeping = not self.bullet_body.isActive() + + def get_velocity_history(self, duration=1.0): + """ + 获取指定时间段内的速度历史 + + Args: + duration (float): 时间段(秒) + + Returns: + list: 速度历史记录 + """ + current_time = time.time() + start_time = current_time - duration + + history = [] + for vel, timestamp in self.velocity_history: + if timestamp >= start_time: + history.append((vel, timestamp)) + + return history + + def get_acceleration(self, time_window=0.1): + """ + 计算加速度 + + Args: + time_window (float): 时间窗口(秒) + + Returns: + Vec3: 加速度向量 + """ + history = self.get_velocity_history(time_window) + if len(history) < 2: + return Vec3(0, 0, 0) + + # 计算最近两个时间点的速度差 + vel2, time2 = history[-1] + vel1, time1 = history[-2] + + if time2 - time1 <= 0: + return Vec3(0, 0, 0) + + acceleration = (vel2 - vel1) / (time2 - time1) + return acceleration + + def get_kinetic_energy(self): + """ + 计算动能 + + Returns: + float: 动能 + """ + velocity = self.get_linear_velocity() + angular_velocity = self.get_angular_velocity() + inertia = self.get_inertia() + + # 平移动能: 0.5 * m * v^2 + linear_ke = 0.5 * self.mass * velocity.lengthSquared() + + # 转动动能: 0.5 * I * ω^2 + angular_ke = 0.5 * inertia.length() * angular_velocity.lengthSquared() + + return linear_ke + angular_ke + + def get_potential_energy(self, gravity=Vec3(0, 0, -9.81)): + """ + 计算重力势能 + + Args: + gravity (Vec3): 重力向量 + + Returns: + float: 重力势能 + """ + height = self.get_position().getZ() + # E = m * g * h + return self.mass * abs(gravity.getZ()) * height + + def get_total_energy(self, gravity=Vec3(0, 0, -9.81)): + """ + 计算总能量(动能+势能) + + Args: + gravity (Vec3): 重力向量 + + Returns: + float: 总能量 + """ + return self.get_kinetic_energy() + self.get_potential_energy(gravity) + + def get_momentum(self): + """ + 计算动量 + + Returns: + Vec3: 动量向量 + """ + velocity = self.get_linear_velocity() + return velocity * self.mass + + def get_angular_momentum(self): + """ + 计算角动量 + + Returns: + Vec3: 角动量向量 + """ + angular_velocity = self.get_angular_velocity() + inertia = self.get_inertia() + return angular_velocity * inertia.length() + + def set_custom_data(self, key, value): + """ + 设置自定义数据 + + Args: + key (str): 键 + value: 值 + """ + self.custom_data[key] = value + + def get_custom_data(self, key, default=None): + """ + 获取自定义数据 + + Args: + key (str): 键 + default: 默认值 + + Returns: + 自定义数据值 + """ + return self.custom_data.get(key, default) + + def freeze(self, frozen=True): + """ + 冻结或解冻刚体 + + Args: + frozen (bool): 是否冻结 + """ + self.is_frozen = frozen + if frozen: + self.bullet_body.setActivationState(0) # 禁用激活 + else: + self.bullet_body.setActivationState(1) # 启用激活 + + def set_ghost(self, is_ghost=True): + """ + 设置为幽灵对象(不参与碰撞检测) + + Args: + is_ghost (bool): 是否为幽灵对象 + """ + self.is_ghost = is_ghost + if is_ghost: + self.bullet_body.setCollideFilterGroup(0) + self.bullet_body.setCollideFilterMask(0) + else: + self.bullet_body.setCollideFilterGroup(self.collision_group) + self.bullet_body.setCollideFilterMask(self.collision_mask) + + def get_collision_info(self): + """ + 获取碰撞信息 + + Returns: + dict: 碰撞信息 + """ + return { + 'collision_count': len(self.collision_history), + 'last_collision_time': self.last_collision_time, + 'is_colliding': len(self.collision_history) > 0 and \ + time.time() - self.last_collision_time < 0.1 + } + + def add_collision_event(self, other_body, contact_points, impulse): + """ + 添加碰撞事件 + + Args: + other_body (RigidBody): 另一个刚体 + contact_points (list): 接触点列表 + impulse (float): 冲量 + """ + collision_info = { + 'other_body': other_body, + 'contact_points': contact_points, + 'impulse': impulse, + 'timestamp': time.time() + } + self.collision_history.append(collision_info) + self.last_collision_time = time.time() + + # 调用碰撞回调 + for callback in self.collision_callbacks: + try: + callback(self, other_body, contact_points, impulse) + except Exception as e: + print(f"碰撞回调执行错误: {e}") + + def clear_collision_history(self): + """ + 清除碰撞历史 + """ + self.collision_history.clear() + self.last_collision_time = 0.0 + + def get_force_history(self, duration=1.0): + """ + 获取指定时间段内的力历史 + + Args: + duration (float): 时间段(秒) + + Returns: + list: 力历史记录 + """ + current_time = time.time() + start_time = current_time - duration + + history = [] + for force_record in self.force_history: + if force_record['timestamp'] >= start_time: + history.append(force_record) + + return history + + def get_torque_history(self, duration=1.0): + """ + 获取指定时间段内的扭矩历史 + + Args: + duration (float): 时间段(秒) + + Returns: + list: 扭矩历史记录 + """ + current_time = time.time() + start_time = current_time - duration + + history = [] + for torque_record in self.torque_history: + if torque_record['timestamp'] >= start_time: + history.append(torque_record) + + return history + + def get_impulse_history(self, duration=1.0): + """ + 获取指定时间段内的冲量历史 + + Args: + duration (float): 时间段(秒) + + Returns: + list: 冲量历史记录 + """ + current_time = time.time() + start_time = current_time - duration + + history = [] + for impulse_record in self.impulse_history: + if impulse_record['timestamp'] >= start_time: + history.append(impulse_record) + + return history + + def get_statistics(self): + """ + 获取刚体统计信息 + + Returns: + dict: 统计信息 + """ + return { + 'name': self.bullet_body.getName(), + 'body_type': self.body_type, + 'mass': self.get_mass(), + 'position': self.get_position(), + 'velocity': self.get_linear_velocity(), + 'angular_velocity': self.get_angular_velocity(), + 'kinetic_energy': self.get_kinetic_energy(), + 'potential_energy': self.get_potential_energy(), + 'total_energy': self.get_total_energy(), + 'is_active': self.is_active(), + 'is_sleeping': self.is_sleeping, + 'collision_count': len(self.collision_history), + 'force_count': len(self.force_history), + 'torque_count': len(self.torque_history), + 'impulse_count': len(self.impulse_history), + 'age': time.time() - self.creation_time + } + + def reset_forces(self): + """ + 重置所有力和扭矩 + """ + self.force_history.clear() + self.torque_history.clear() + self.impulse_history.clear() + + def reset_motion(self): + """ + 重置运动状态 + """ + self.set_linear_velocity(Vec3(0, 0, 0)) + self.set_angular_velocity(Vec3(0, 0, 0)) + self.reset_forces() + + def destroy(self): + """ + 销毁刚体 + """ + # 清理历史记录 + self.force_history.clear() + self.torque_history.clear() + self.impulse_history.clear() + self.collision_history.clear() + self.position_history.clear() + self.velocity_history.clear() + self.angular_velocity_history.clear() + self.acceleration_history.clear() + + # 清理回调 + self.collision_callbacks.clear() + + # 清理自定义数据 + self.custom_data.clear() + + # 移除物理节点 + if self.physics_node and not self.physics_node.isEmpty(): + self.physics_node.removeNode() + print(f"刚体已销毁: {self.bullet_body.getName()}") \ No newline at end of file diff --git a/plugins/user/rigid_body_physics/debug/debug_tools.py b/plugins/user/rigid_body_physics/debug/debug_tools.py index fc4d3fcd..5c09adba 100644 --- a/plugins/user/rigid_body_physics/debug/debug_tools.py +++ b/plugins/user/rigid_body_physics/debug/debug_tools.py @@ -1,1227 +1,1227 @@ - -""" -调试和可视化工具模块 -负责物理系统的调试可视化和性能分析 -""" - -from panda3d.core import Vec3, Point3, Vec4, LVector3f -from panda3d.core import NodePath, CardMaker, TextNode, LineSegs -from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter -from panda3d.core import GeomTriangles, Geom, GeomNode -from panda3d.core import ClockObject, ConfigVariableBool -import math -import time -from collections import deque - - -class PhysicsDebug: - """ - 物理调试类 - 提供物理系统的可视化和调试功能 - """ - - def __init__(self, physics_world, render_node): - """ - 初始化物理调试工具 - - Args: - physics_world (PhysicsWorld): 物理世界对象 - render_node (NodePath): 渲染节点 - """ - self.physics_world = physics_world - self.render_node = render_node - - # 调试可视化设置 - self.debug_enabled = ConfigVariableBool('physics-debug-enabled', False).getValue() - self.show_constraints = ConfigVariableBool('physics-show-constraints', True).getValue() - self.show_aabbs = ConfigVariableBool('physics-show-aabbs', False).getValue() - self.show_contacts = ConfigVariableBool('physics-show-contacts', False).getValue() - self.show_normals = ConfigVariableBool('physics-show-normals', False).getValue() - self.show_velocities = ConfigVariableBool('physics-show-velocities', False).getValue() - self.show_forces = ConfigVariableBool('physics-show-forces', False).getValue() - self.show_axes = ConfigVariableBool('physics-show-axes', False).getValue() - self.show_mass_centers = ConfigVariableBool('physics-show-mass-centers', False).getValue() - self.show_collision_shapes = ConfigVariableBool('physics-show-collision-shapes', True).getValue() - self.show_sleeping_states = ConfigVariableBool('physics-show-sleeping-states', True).getValue() - self.show_particle_trails = ConfigVariableBool('physics-show-particle-trails', False).getValue() - - # 可视化节点 - self.debug_root = self.render_node.attachNewNode("PhysicsDebug") - self.constraint_node = self.debug_root.attachNewNode("Constraints") - self.aabb_node = self.debug_root.attachNewNode("AABBs") - self.contact_node = self.debug_root.attachNewNode("Contacts") - self.normal_node = self.debug_root.attachNewNode("Normals") - self.velocity_node = self.debug_root.attachNewNode("Velocities") - self.force_node = self.debug_root.attachNewNode("Forces") - self.axes_node = self.debug_root.attachNewNode("Axes") - self.mass_center_node = self.debug_root.attachNewNode("MassCenters") - self.collision_shape_node = self.debug_root.attachNewNode("CollisionShapes") - self.sleeping_node = self.debug_root.attachNewNode("SleepingStates") - self.particle_trail_node = self.debug_root.attachNewNode("ParticleTrails") - - # 调试颜色配置 - self.colors = { - 'static': Vec4(0.5, 0.5, 0.5, 0.7), # 灰色 - 静态物体 - 'dynamic': Vec4(0.0, 1.0, 0.0, 0.7), # 绿色 - 动态物体 - 'kinematic': Vec4(0.0, 0.0, 1.0, 0.7), # 蓝色 - 运动物体 - 'sleeping': Vec4(0.3, 0.3, 0.3, 0.5), # 深灰色 - 休眠物体 - 'constraint': Vec4(1.0, 1.0, 0.0, 0.8), # 黄色 - 约束 - 'contact': Vec4(1.0, 0.0, 0.0, 0.8), # 红色 - 接触点 - 'normal': Vec4(0.0, 1.0, 1.0, 0.8), # 青色 - 法线 - 'velocity': Vec4(1.0, 0.5, 0.0, 0.8), # 橙色 - 速度 - 'force': Vec4(1.0, 0.0, 1.0, 0.8), # 紫色 - 力 - 'positive_axis': Vec4(1.0, 0.0, 0.0, 0.9), # 红色 - X轴 - 'negative_axis': Vec4(0.0, 1.0, 0.0, 0.9), # 绿色 - Y轴 - 'neutral_axis': Vec4(0.0, 0.0, 1.0, 0.9), # 蓝色 - Z轴 - 'mass_center': Vec4(1.0, 1.0, 0.0, 0.9), # 黄色 - 质心 - 'particle_trail': Vec4(0.8, 0.8, 1.0, 0.6), # 浅蓝色 - 粒子轨迹 - } - - # 可视化参数 - self.line_thickness = 2.0 - self.axis_length = 1.0 - self.velocity_scale = 0.1 - self.force_scale = 0.01 - self.contact_point_size = 0.1 - self.normal_scale = 0.5 - self.aabb_line_style = 'solid' # solid, dashed, dotted - self.collision_shape_style = 'wireframe' # wireframe, solid, transparent - - # 性能监控 - self.debug_update_frequency = 30 # 每秒更新次数 - self.debug_update_counter = 0 - self.last_debug_update = 0.0 - - # 调试文本显示 - self.debug_text_enabled = True - self.debug_text_node = None - self.debug_text = "" - - # 碰撞可视化历史 - self.contact_history = deque(maxlen=100) - self.collision_history = deque(maxlen=50) - - # 性能统计显示 - self.stats_display_enabled = True - self.stats_text_node = None - - # 初始化调试文本 - self._init_debug_text() - - # 根据初始状态显示或隐藏 - if self.debug_enabled: - self.debug_root.show() - else: - self.debug_root.hide() - - print("物理调试工具初始化完成") - - def _init_debug_text(self): - """ - 初始化调试文本显示 - """ - # 创建调试文本节点 - self.debug_text_node = TextNode("PhysicsDebugText") - self.debug_text_node.setText("") - self.debug_text_node.setTextColor(1, 1, 1, 1) # 白色 - self.debug_text_node.setAlign(TextNode.ALeft) - self.debug_text_node.setFontSize(16) - - # 创建文本节点路径 - text_np = self.render_node.attachNewNode(self.debug_text_node) - text_np.setScale(0.05) # 调整文本大小 - text_np.setPos(-1.5, 0, 0.9) # 屏幕左上角 - text_np.setBin("fixed", 100) # 确保在最上层 - text_np.setDepthWrite(False) - text_np.setDepthTest(False) - - # 创建性能统计文本节点 - self.stats_text_node = TextNode("PhysicsStatsText") - self.stats_text_node.setText("") - self.stats_text_node.setTextColor(1, 1, 1, 1) # 白色 - self.stats_text_node.setAlign(TextNode.ARight) - self.stats_text_node.setFontSize(14) - - # 创建统计文本节点路径 - stats_np = self.render_node.attachNewNode(self.stats_text_node) - stats_np.setScale(0.04) # 调整文本大小 - stats_np.setPos(1.5, 0, 0.9) # 屏幕右上角 - stats_np.setBin("fixed", 100) - stats_np.setDepthWrite(False) - stats_np.setDepthTest(False) - - def enable_debug(self, enabled=True): - """ - 启用或禁用调试可视化 - - Args: - enabled (bool): 是否启用调试 - """ - self.debug_enabled = enabled - if enabled: - self.debug_root.show() - else: - self.debug_root.hide() - - def set_debug_options(self, show_constraints=None, show_aabbs=None, - show_contacts=None, show_normals=None, show_velocities=None, - show_forces=None, show_axes=None, show_mass_centers=None, - show_collision_shapes=None, show_sleeping_states=None, - show_particle_trails=None): - """ - 设置调试选项 - - Args: - show_constraints (bool): 是否显示约束 - show_aabbs (bool): 是否显示包围盒 - show_contacts (bool): 是否显示接触点 - show_normals (bool): 是否显示法线 - show_velocities (bool): 是否显示速度 - show_forces (bool): 是否显示力 - show_axes (bool): 是否显示坐标轴 - show_mass_centers (bool): 是否显示质心 - show_collision_shapes (bool): 是否显示碰撞形状 - show_sleeping_states (bool): 是否显示休眠状态 - show_particle_trails (bool): 是否显示粒子轨迹 - """ - if show_constraints is not None: - self.show_constraints = show_constraints - if show_constraints: - self.constraint_node.show() - else: - self.constraint_node.hide() - - if show_aabbs is not None: - self.show_aabbs = show_aabbs - if show_aabbs: - self.aabb_node.show() - else: - self.aabb_node.hide() - - if show_contacts is not None: - self.show_contacts = show_contacts - if show_contacts: - self.contact_node.show() - else: - self.contact_node.hide() - - if show_normals is not None: - self.show_normals = show_normals - if show_normals: - self.normal_node.show() - else: - self.normal_node.hide() - - if show_velocities is not None: - self.show_velocities = show_velocities - if show_velocities: - self.velocity_node.show() - else: - self.velocity_node.hide() - - if show_forces is not None: - self.show_forces = show_forces - if show_forces: - self.force_node.show() - else: - self.force_node.hide() - - if show_axes is not None: - self.show_axes = show_axes - if show_axes: - self.axes_node.show() - else: - self.axes_node.hide() - - if show_mass_centers is not None: - self.show_mass_centers = show_mass_centers - if show_mass_centers: - self.mass_center_node.show() - else: - self.mass_center_node.hide() - - if show_collision_shapes is not None: - self.show_collision_shapes = show_collision_shapes - if show_collision_shapes: - self.collision_shape_node.show() - else: - self.collision_shape_node.hide() - - if show_sleeping_states is not None: - self.show_sleeping_states = show_sleeping_states - if show_sleeping_states: - self.sleeping_node.show() - else: - self.sleeping_node.hide() - - if show_particle_trails is not None: - self.show_particle_trails = show_particle_trails - if show_particle_trails: - self.particle_trail_node.show() - else: - self.particle_trail_node.hide() - - def update_debug_visualization(self): - """ - 更新调试可视化 - """ - if not self.debug_enabled: - return - - # 控制更新频率 - current_time = ClockObject.getGlobalClock().getFrameTime() - if current_time - self.last_debug_update < 1.0 / self.debug_update_frequency: - return - - self.last_debug_update = current_time - self.debug_update_counter += 1 - - # 清除之前的可视化 - self._clear_debug_visualization() - - # 可视化刚体 - self._visualize_rigid_bodies() - - # 可视化约束 - if self.show_constraints: - self._visualize_constraints() - - # 可视化接触点 - if self.show_contacts: - self._visualize_contacts() - - # 可视化法线 - if self.show_normals: - self._visualize_normals() - - # 可视化速度 - if self.show_velocities: - self._visualize_velocities() - - # 可视化力 - if self.show_forces: - self._visualize_forces() - - # 可视化坐标轴 - if self.show_axes: - self._visualize_axes() - - # 可视化质心 - if self.show_mass_centers: - self._visualize_mass_centers() - - # 可视化碰撞形状 - if self.show_collision_shapes: - self._visualize_collision_shapes() - - # 可视化休眠状态 - if self.show_sleeping_states: - self._visualize_sleeping_states() - - # 可视化粒子轨迹 - if self.show_particle_trails: - self._visualize_particle_trails() - - # 更新调试文本 - if self.debug_text_enabled: - self._update_debug_text() - - # 更新性能统计 - if self.stats_display_enabled: - self._update_stats_display() - - def _clear_debug_visualization(self): - """ - 清除调试可视化 - """ - # 清除所有子节点 - for child in self.constraint_node.getChildren(): - child.removeNode() - for child in self.aabb_node.getChildren(): - child.removeNode() - for child in self.contact_node.getChildren(): - child.removeNode() - for child in self.normal_node.getChildren(): - child.removeNode() - for child in self.velocity_node.getChildren(): - child.removeNode() - for child in self.force_node.getChildren(): - child.removeNode() - for child in self.axes_node.getChildren(): - child.removeNode() - for child in self.mass_center_node.getChildren(): - child.removeNode() - for child in self.collision_shape_node.getChildren(): - child.removeNode() - for child in self.sleeping_node.getChildren(): - child.removeNode() - for child in self.particle_trail_node.getChildren(): - child.removeNode() - - def _visualize_rigid_bodies(self): - """ - 可视化刚体 - """ - for rigid_body in self.physics_world.rigid_bodies: - # 获取刚体的AABB - aabb = rigid_body.bullet_body.getAabb() - min_point = aabb.getMin() - max_point = aabb.getMax() - - # 创建包围盒可视化 - if self.show_aabbs: - self._draw_aabb(min_point, max_point, rigid_body) - - def _draw_aabb(self, min_point, max_point, rigid_body): - """ - 绘制AABB包围盒 - - Args: - min_point (Point3): 最小点 - max_point (Point3): 最大点 - rigid_body (RigidBody): 刚体对象 - """ - # 确定颜色 - if rigid_body.body_type == rigid_body.STATIC: - color = self.colors['static'] - elif rigid_body.body_type == rigid_body.KINEMATIC: - color = self.colors['kinematic'] - else: - color = self.colors['dynamic'] - - # 如果显示休眠状态且刚体休眠,使用休眠颜色 - if self.show_sleeping_states and not rigid_body.bullet_body.isActive(): - color = self.colors['sleeping'] - - # 创建线条几何体 - vformat = GeomVertexFormat.getV3c4() - vdata = GeomVertexData('aabb', vformat, Geom.UHStatic) - vertex = GeomVertexWriter(vdata, 'vertex') - color_writer = GeomVertexWriter(vdata, 'color') - - # 添加8个顶点 - vertices = [ - Point3(min_point.getX(), min_point.getY(), min_point.getZ()), - Point3(max_point.getX(), min_point.getY(), min_point.getZ()), - Point3(max_point.getX(), max_point.getY(), min_point.getZ()), - Point3(min_point.getX(), max_point.getY(), min_point.getZ()), - Point3(min_point.getX(), min_point.getY(), max_point.getZ()), - Point3(max_point.getX(), min_point.getY(), max_point.getZ()), - Point3(max_point.getX(), max_point.getY(), max_point.getZ()), - Point3(min_point.getX(), max_point.getY(), max_point.getZ()) - ] - - for vert in vertices: - vertex.addData3(vert) - color_writer.addData4(color) - - # 创建线条 - geom = Geom(vdata) - lines = GeomLines(Geom.UHStatic) - - # 添加12条边 - edges = [ - (0, 1), (1, 2), (2, 3), (3, 0), # 底面 - (4, 5), (5, 6), (6, 7), (7, 4), # 顶面 - (0, 4), (1, 5), (2, 6), (3, 7) # 垂直边 - ] - - for start, end in edges: - lines.addVertex(start) - lines.addVertex(end) - lines.closePrimitive() - - geom.addPrimitive(lines) - - # 创建节点 - node = GeomNode('aabb_geom') - node.addGeom(geom) - - # 添加到场景 - aabb_np = self.aabb_node.attachNewNode(node) - aabb_np.setRenderModeWireframe() - - def _visualize_constraints(self): - """ - 可视化约束 - """ - for constraint_info in self.physics_world.constraints: - constraint = constraint_info['constraint'] - body_a = constraint_info['body_a'] - body_b = constraint_info['body_b'] - - # 获取连接点 - if hasattr(constraint, 'getPivotA'): - pivot_a = constraint.getPivotA() - pivot_b = constraint.getPivotB() - - # 转换到世界坐标 - world_pivot_a = body_a.physics_node.getTransform().getPos() + pivot_a - world_pivot_b = body_b.physics_node.getTransform().getPos() + pivot_b - - # 绘制连接线 - self._draw_line(world_pivot_a, world_pivot_b, self.colors['constraint']) - - def _visualize_contacts(self): - """ - 可视化接触点 - """ - # 注意:Bullet不直接提供接触点信息,需要通过碰撞回调获取 - # 这里使用简化的实现,从历史记录中获取 - for contact_info in list(self.contact_history): - position = contact_info['position'] - normal = contact_info['normal'] - - # 绘制接触点 - self._draw_point(position, self.colors['contact'], self.contact_point_size) - - # 绘制法线 - if self.show_normals: - end_point = position + normal * self.normal_scale - self._draw_line(position, end_point, self.colors['normal']) - - def _visualize_normals(self): - """ - 可视化法线 - """ - # 从接触历史中绘制法线 - for contact_info in list(self.contact_history): - position = contact_info['position'] - normal = contact_info['normal'] - end_point = position + normal * self.normal_scale - self._draw_line(position, end_point, self.colors['normal']) - - def _visualize_velocities(self): - """ - 可视化速度 - """ - for rigid_body in self.physics_world.rigid_bodies: - velocity = rigid_body.bullet_body.getLinearVelocity() - if velocity.length() > 0.1: # 只显示大于阈值的速度 - position = rigid_body.physics_node.getPos() - end_point = position + velocity * self.velocity_scale - self._draw_line(position, end_point, self.colors['velocity']) - - # 添加箭头 - self._draw_arrow_head(end_point, velocity, self.colors['velocity']) - - def _visualize_forces(self): - """ - 可视化力 - """ - # 从刚体历史中获取力信息 - for rigid_body in self.physics_world.rigid_bodies: - # 这里需要从刚体中获取力历史,简化实现 - pass - - def _visualize_axes(self): - """ - 可视化坐标轴 - """ - for rigid_body in self.physics_world.rigid_bodies: - position = rigid_body.physics_node.getPos() - rotation = rigid_body.physics_node.getHpr() - - # 创建坐标系变换 - from panda3d.core import TransformState - transform = TransformState.makePosHpr(position, rotation) - - # X轴(红色) - x_end = position + transform.getQuat().getRight() * self.axis_length - self._draw_line(position, x_end, self.colors['positive_axis']) - self._draw_arrow_head(x_end, transform.getQuat().getRight(), self.colors['positive_axis']) - - # Y轴(绿色) - y_end = position + transform.getQuat().getForward() * self.axis_length - self._draw_line(position, y_end, self.colors['negative_axis']) - self._draw_arrow_head(y_end, transform.getQuat().getForward(), self.colors['negative_axis']) - - # Z轴(蓝色) - z_end = position + transform.getQuat().getUp() * self.axis_length - self._draw_line(position, z_end, self.colors['neutral_axis']) - self._draw_arrow_head(z_end, transform.getQuat().getUp(), self.colors['neutral_axis']) - - def _visualize_mass_centers(self): - """ - 可视化质心 - """ - for rigid_body in self.physics_world.rigid_bodies: - # 获取质心位置 - center_of_mass = rigid_body.bullet_body.getCenterOfMassTransform().getPos() - world_position = rigid_body.physics_node.getTransform().getPos() + center_of_mass - - # 绘制质心点 - self._draw_point(world_position, self.colors['mass_center'], 0.05) - - def _visualize_collision_shapes(self): - """ - 可视化碰撞形状 - """ - for rigid_body in self.physics_world.rigid_bodies: - # 获取碰撞形状信息并绘制 - # 这里需要更复杂的实现来准确绘制不同形状 - pass - - def _visualize_sleeping_states(self): - """ - 可视化休眠状态 - """ - # 休眠状态已经在其他可视化中体现,这里可以添加额外的标识 - for rigid_body in self.physics_world.rigid_bodies: - if not rigid_body.bullet_body.isActive(): - # 在休眠物体上添加特殊标记 - position = rigid_body.physics_node.getPos() - self._draw_point(position + Vec3(0, 0, 0.1), self.colors['sleeping'], 0.03) - - def _visualize_particle_trails(self): - """ - 可视化粒子轨迹 - """ - # 从粒子系统中获取轨迹信息 - for particle_system in self.physics_world.particles: - # 绘制每个粒子的轨迹 - # 这里需要访问粒子系统的轨迹数据 - pass - - def _draw_line(self, start, end, color, thickness=None): - """ - 绘制线条 - - Args: - start (Point3): 起点 - end (Point3): 终点 - color (Vec4): 颜色 - thickness (float): 线条粗细 - """ - if thickness is None: - thickness = self.line_thickness - - # 使用LineSegs绘制线条 - segs = LineSegs('debug_line') - segs.setThickness(thickness) - segs.setColor(color) - segs.moveTo(start) - segs.drawTo(end) - - # 创建节点 - node = segs.create() - line_np = self.debug_root.attachNewNode(node) - - def _draw_point(self, position, color, size=0.1): - """ - 绘制点 - - Args: - position (Point3): 位置 - color (Vec4): 颜色 - size (float): 点大小 - """ - # 创建一个小球体表示点 - cm = CardMaker('debug_point') - cm.setFrame(-size, size, -size, size) - point_np = self.debug_root.attachNewNode(cm.generate()) - point_np.setPos(position) - point_np.setColor(color) - - def _draw_arrow_head(self, tip_position, direction, color, size=0.1): - """ - 绘制箭头头部 - - Args: - tip_position (Point3): 箭头顶点位置 - direction (Vec3): 方向向量 - color (Vec4): 颜色 - size (float): 箭头大小 - """ - # 标准化方向向量 - direction.normalize() - - # 计算垂直向量 - perpendicular = direction.cross(Vec3(0, 0, 1)) - if perpendicular.length() < 0.001: - perpendicular = direction.cross(Vec3(1, 0, 0)) - perpendicular.normalize() - - # 计算箭头翼点 - wing1 = tip_position - direction * size + perpendicular * size * 0.5 - wing2 = tip_position - direction * size - perpendicular * size * 0.5 - - # 绘制箭头翼 - self._draw_line(tip_position, wing1, color) - self._draw_line(tip_position, wing2, color) - - def _update_debug_text(self): - """ - 更新调试文本 - """ - if not self.debug_text_node: - return - - # 构建调试文本 - debug_lines = [] - - # 添加基本统计 - stats = self.physics_world.get_performance_stats() - debug_lines.append(f"Physics Debug Info:") - debug_lines.append(f" FPS: {stats.get('current_fps', 0):.1f}") - debug_lines.append(f" Update Time: {stats.get('last_update_time', 0)*1000:.2f}ms") - debug_lines.append(f" Objects: {len(self.physics_world.rigid_bodies)} rigid bodies") - debug_lines.append(f" Constraints: {len(self.physics_world.constraints)} constraints") - debug_lines.append(f" Particles: {len(self.physics_world.particles)} systems") - - # 添加活动物体统计 - active_count = sum(1 for body in self.physics_world.rigid_bodies if body.bullet_body.isActive()) - sleeping_count = len(self.physics_world.rigid_bodies) - active_count - debug_lines.append(f" Active Bodies: {active_count}") - debug_lines.append(f" Sleeping Bodies: {sleeping_count}") - - # 更新文本节点 - self.debug_text = '\n'.join(debug_lines) - self.debug_text_node.setText(self.debug_text) - - def _update_stats_display(self): - """ - 更新性能统计显示 - """ - if not self.stats_text_node: - return - - # 获取性能统计 - stats = self.physics_world.get_performance_stats() - - # 构建统计文本 - stat_lines = [] - stat_lines.append("Performance Stats:") - stat_lines.append(f"Avg Update: {stats.get('avg_update_time', 0)*1000:.2f}ms") - stat_lines.append(f"Max Update: {stats.get('max_update_time', 0)*1000:.2f}ms") - stat_lines.append(f"Min Update: {stats.get('min_update_time', 0)*1000:.2f}ms") - stat_lines.append(f"Objects: {stats.get('objects', {}).get('rigid_bodies', 0)} bodies") - stat_lines.append(f"Constraints: {stats.get('objects', {}).get('constraints', 0)}") - stat_lines.append(f"Particles: {stats.get('objects', {}).get('particles', 0)}") - - # 更新统计文本节点 - stats_text = '\n'.join(stat_lines) - self.stats_text_node.setText(stats_text) - - def add_contact_point(self, position, normal, impulse): - """ - 添加接触点信息(供外部调用) - - Args: - position (Point3): 接触点位置 - normal (Vec3): 接触法线 - impulse (float): 冲量 - """ - contact_info = { - 'position': position, - 'normal': normal, - 'impulse': impulse, - 'timestamp': time.time() - } - self.contact_history.append(contact_info) - - def add_collision_event(self, body_a, body_b, contact_points): - """ - 添加碰撞事件(供外部调用) - - Args: - body_a (RigidBody): 第一个刚体 - body_b (RigidBody): 第二个刚体 - contact_points (list): 接触点列表 - """ - collision_info = { - 'body_a': body_a, - 'body_b': body_b, - 'contact_points': contact_points, - 'timestamp': time.time() - } - self.collision_history.append(collision_info) - - def set_line_thickness(self, thickness): - """ - 设置线条粗细 - - Args: - thickness (float): 线条粗细 - """ - self.line_thickness = max(0.1, thickness) - - def set_axis_length(self, length): - """ - 设置坐标轴长度 - - Args: - length (float): 坐标轴长度 - """ - self.axis_length = max(0.1, length) - - def set_velocity_scale(self, scale): - """ - 设置速度可视化缩放 - - Args: - scale (float): 速度缩放因子 - """ - self.velocity_scale = max(0.001, scale) - - def set_force_scale(self, scale): - """ - 设置力可视化缩放 - - Args: - scale (float): 力缩放因子 - """ - self.force_scale = max(0.0001, scale) - - def set_contact_point_size(self, size): - """ - 设置接触点大小 - - Args: - size (float): 接触点大小 - """ - self.contact_point_size = max(0.01, size) - - def set_normal_scale(self, scale): - """ - 设置法线可视化缩放 - - Args: - scale (float): 法线缩放因子 - """ - self.normal_scale = max(0.01, scale) - - def enable_debug_text(self, enabled=True): - """ - 启用或禁用调试文本显示 - - Args: - enabled (bool): 是否启用调试文本 - """ - self.debug_text_enabled = enabled - if self.debug_text_node: - if enabled: - self.debug_text_node.show() - else: - self.debug_text_node.hide() - - def enable_stats_display(self, enabled=True): - """ - 启用或禁用性能统计显示 - - Args: - enabled (bool): 是否启用性能统计显示 - """ - self.stats_display_enabled = enabled - if self.stats_text_node: - if enabled: - self.stats_text_node.show() - else: - self.stats_text_node.hide() - - def set_debug_update_frequency(self, frequency): - """ - 设置调试更新频率 - - Args: - frequency (int): 更新频率(Hz) - """ - self.debug_update_frequency = max(1, frequency) - - def hide_debug_visualization(self): - """ - 隐藏调试可视化 - """ - self.debug_root.hide() - - def show_debug_visualization(self): - """ - 显示调试可视化 - """ - if self.debug_enabled: - self.debug_root.show() - - def destroy(self): - """ - 销毁调试工具 - """ - # 清除所有可视化 - self._clear_debug_visualization() - - # 移除文本节点 - if self.debug_text_node and not self.debug_text_node.isEmpty(): - self.debug_text_node.removeNode() - if self.stats_text_node and not self.stats_text_node.isEmpty(): - self.stats_text_node.removeNode() - - # 移除节点 - if self.debug_root and not self.debug_root.isEmpty(): - self.debug_root.removeNode() - - # 清空历史记录 - self.contact_history.clear() - self.collision_history.clear() - - print("物理调试工具已销毁") - - -class PhysicsProfiler: - """ - 物理性能分析器 - 监控和分析物理系统的性能 - """ - - def __init__(self, physics_world): - """ - 初始化性能分析器 - - Args: - physics_world (PhysicsWorld): 物理世界对象 - """ - self.physics_world = physics_world - - # 性能数据 - self.update_times = deque(maxlen=1000) - self.rigid_body_counts = deque(maxlen=1000) - self.constraint_counts = deque(maxlen=1000) - self.particle_counts = deque(maxlen=1000) - self.collision_counts = deque(maxlen=1000) - self.active_body_counts = deque(maxlen=1000) - self.sleeping_body_counts = deque(maxlen=1000) - - # 分析参数 - self.max_history = 1000 # 最大历史记录数 - self.sampling_interval = 1.0 # 采样间隔(秒) - self.last_sample_time = 0.0 - - # 性能阈值 - self.warning_threshold = 0.016 # 警告阈值(秒) - self.error_threshold = 0.033 # 错误阈值(秒) - - # 统计计数器 - self.total_updates = 0 - self.total_collisions = 0 - self.total_constraints_solved = 0 - self.peak_performance = { - 'max_update_time': 0.0, - 'max_rigid_bodies': 0, - 'max_constraints': 0, - 'max_particles': 0, - 'max_collisions': 0 - } - - print("物理性能分析器初始化完成") - - def record_frame(self): - """ - 记录帧数据 - """ - current_time = time.time() - - # 检查采样间隔 - if current_time - self.last_sample_time < self.sampling_interval: - return - - self.last_sample_time = current_time - - # 获取性能统计 - stats = self.physics_world.get_performance_stats() - - # 记录更新时间 - update_time = stats.get('last_update_time', 0.0) - self.update_times.append(update_time) - - # 更新峰值性能记录 - if update_time > self.peak_performance['max_update_time']: - self.peak_performance['max_update_time'] = update_time - - # 记录对象数量 - objects = stats.get('objects', {}) - rigid_bodies = objects.get('rigid_bodies', 0) - constraints = objects.get('constraints', 0) - particles = objects.get('particles', 0) - - self.rigid_body_counts.append(rigid_bodies) - self.constraint_counts.append(constraints) - self.particle_counts.append(particles) - - # 更新峰值记录 - if rigid_bodies > self.peak_performance['max_rigid_bodies']: - self.peak_performance['max_rigid_bodies'] = rigid_bodies - if constraints > self.peak_performance['max_constraints']: - self.peak_performance['max_constraints'] = constraints - if particles > self.peak_performance['max_particles']: - self.peak_performance['max_particles'] = particles - - # 记录活动和休眠物体数量 - active_count = 0 - sleeping_count = 0 - for body in self.physics_world.rigid_bodies: - if body.bullet_body.isActive(): - active_count += 1 - else: - sleeping_count += 1 - - self.active_body_counts.append(active_count) - self.sleeping_body_counts.append(sleeping_count) - - # 更新总计数器 - self.total_updates += 1 - - # 检查性能警告 - self._check_performance_warnings(update_time) - - def _check_performance_warnings(self, update_time): - """ - 检查性能警告 - - Args: - update_time (float): 更新时间 - """ - if update_time > self.error_threshold: - print(f"[ERROR] Physics update time exceeded error threshold: {update_time*1000:.2f}ms") - elif update_time > self.warning_threshold: - print(f"[WARNING] Physics update time exceeded warning threshold: {update_time*1000:.2f}ms") - - def get_average_update_time(self): - """ - 获取平均更新时间 - - Returns: - float: 平均更新时间(秒) - """ - if not self.update_times: - return 0.0 - return sum(self.update_times) / len(self.update_times) - - def get_max_update_time(self): - """ - 获取最大更新时间 - - Returns: - float: 最大更新时间(秒) - """ - if not self.update_times: - return 0.0 - return max(self.update_times) - - def get_min_update_time(self): - """ - 获取最小更新时间 - - Returns: - float: 最小更新时间(秒) - """ - if not self.update_times: - return 0.0 - return min(self.update_times) - - def get_average_fps(self): - """ - 获取平均帧率 - - Returns: - float: 平均帧率 - """ - avg_update_time = self.get_average_update_time() - if avg_update_time > 0: - return 1.0 / avg_update_time - return 0.0 - - def get_object_counts(self): - """ - 获取对象数量统计 - - Returns: - dict: 对象数量统计 - """ - return { - 'rigid_bodies': len(self.physics_world.rigid_bodies), - 'constraints': len(self.physics_world.constraints), - 'vehicles': len(self.physics_world.vehicles), - 'particles': sum([ps.get_particle_count() - for ps in self.physics_world.particles]), - 'soft_bodies': len(self.physics_world.soft_bodies), - 'ghost_objects': len(self.physics_world.ghost_objects) - } - - def get_average_object_counts(self): - """ - 获取平均对象数量 - - Returns: - dict: 平均对象数量统计 - """ - if not self.rigid_body_counts: - return self.get_object_counts() - - return { - 'rigid_bodies': sum(self.rigid_body_counts) / len(self.rigid_body_counts), - 'constraints': sum(self.constraint_counts) / len(self.constraint_counts), - 'particles': sum(self.particle_counts) / len(self.particle_counts), - 'active_bodies': sum(self.active_body_counts) / len(self.active_body_counts) if self.active_body_counts else 0, - 'sleeping_bodies': sum(self.sleeping_body_counts) / len(self.sleeping_body_counts) if self.sleeping_body_counts else 0 - } - - def get_performance_report(self): - """ - 获取性能报告 - - Returns: - dict: 性能报告 - """ - return { - 'average_update_time': self.get_average_update_time(), - 'max_update_time': self.get_max_update_time(), - 'min_update_time': self.get_min_update_time(), - 'average_fps': self.get_average_fps(), - 'current_fps': 1.0 / self.get_average_update_time() if self.get_average_update_time() > 0 else 0, - 'object_counts': self.get_object_counts(), - 'average_object_counts': self.get_average_object_counts(), - 'peak_performance': self.peak_performance.copy(), - 'total_updates': self.total_updates, - 'total_collisions': self.total_collisions, - 'history_length': len(self.update_times), - 'sampling_interval': self.sampling_interval - } - - def get_performance_analysis(self): - """ - 获取性能分析 - - Returns: - dict: 性能分析结果 - """ - report = self.get_performance_report() - - # 计算性能等级 - avg_update_time = report['average_update_time'] - if avg_update_time <= 0.008: # 8ms - performance_grade = "Excellent" - elif avg_update_time <= 0.016: # 16ms - performance_grade = "Good" - elif avg_update_time <= 0.024: # 24ms - performance_grade = "Acceptable" - elif avg_update_time <= 0.033: # 33ms - performance_grade = "Poor" - else: - performance_grade = "Critical" - - # 分析瓶颈 - bottlenecks = [] - if report['object_counts']['rigid_bodies'] > 500: - bottlenecks.append("High rigid body count") - if report['object_counts']['constraints'] > 100: - bottlenecks.append("High constraint count") - if report['object_counts']['particles'] > 1000: - bottlenecks.append("High particle count") - if report['average_object_counts']['active_bodies'] > 200: - bottlenecks.append("Too many active bodies") - - return { - 'performance_grade': performance_grade, - 'bottlenecks': bottlenecks, - 'recommendations': self._generate_recommendations(report), - 'report': report - } - - def _generate_recommendations(self, report): - """ - 生成性能优化建议 - - Args: - report (dict): 性能报告 - - Returns: - list: 优化建议列表 - """ - recommendations = [] - - # 基于性能等级的建议 - if report['performance_grade'] == "Poor": - recommendations.append("Consider reducing physics complexity") - elif report['performance_grade'] == "Critical": - recommendations.append("URGENT: Physics performance critical - immediate optimization needed") - - # 基于对象数量的建议 - if report['object_counts']['rigid_bodies'] > 500: - recommendations.append("Reduce rigid body count through batching or simplification") - if report['object_counts']['constraints'] > 100: - recommendations.append("Optimize constraint usage - consider removing unnecessary constraints") - if report['object_counts']['particles'] > 1000: - recommendations.append("Reduce particle system complexity or limit particle count") - - # 基于活动物体的建议 - avg_active = report['average_object_counts']['active_bodies'] - if avg_active > 200: - recommendations.append("Improve sleeping thresholds to reduce active bodies") - - # 基于更新时间的建议 - if report['average_update_time'] > 0.016: - recommendations.append("Profile physics update to identify slow operations") - recommendations.append("Consider multithreading if available") - - return recommendations - - def reset_statistics(self): - """ - 重置统计信息 - """ - self.update_times.clear() - self.rigid_body_counts.clear() - self.constraint_counts.clear() - self.particle_counts.clear() - self.collision_counts.clear() - self.active_body_counts.clear() - self.sleeping_body_counts.clear() - - self.total_updates = 0 - self.total_collisions = 0 - self.total_constraints_solved = 0 - - # 重置峰值记录(保留一些) - self.peak_performance['max_update_time'] = 0.0 - self.peak_performance['max_collisions'] = 0 - - print("性能统计已重置") - - def export_statistics(self, filepath): - """ - 导出统计信息到文件 - - Args: - filepath (str): 导出文件路径 - """ - import json - - # 构建统计数据 - stats_data = { - 'performance_report': self.get_performance_report(), - 'performance_analysis': self.get_performance_analysis(), - 'raw_data': { - 'update_times': list(self.update_times), - 'rigid_body_counts': list(self.rigid_body_counts), - 'constraint_counts': list(self.constraint_counts), - 'particle_counts': list(self.particle_counts), - 'collision_counts': list(self.collision_counts), - 'active_body_counts': list(self.active_body_counts), - 'sleeping_body_counts': list(self.sleeping_body_counts) - }, - 'export_timestamp': time.time(), - 'export_date': time.strftime('%Y-%m-%d %H:%M:%S') - } - - # 导出到JSON文件 - try: - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(stats_data, f, indent=2, ensure_ascii=False) - print(f"性能统计已导出到: {filepath}") - except Exception as e: - print(f"导出性能统计失败: {e}") - - def destroy(self): - """ - 销毁性能分析器 - """ - self.reset_statistics() - print("物理性能分析器已销毁") + +""" +调试和可视化工具模块 +负责物理系统的调试可视化和性能分析 +""" + +from panda3d.core import Vec3, Point3, Vec4, LVector3f +from panda3d.core import NodePath, CardMaker, TextNode, LineSegs +from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter +from panda3d.core import GeomTriangles, Geom, GeomNode +from panda3d.core import ClockObject, ConfigVariableBool +import math +import time +from collections import deque + + +class PhysicsDebug: + """ + 物理调试类 + 提供物理系统的可视化和调试功能 + """ + + def __init__(self, physics_world, render_node): + """ + 初始化物理调试工具 + + Args: + physics_world (PhysicsWorld): 物理世界对象 + render_node (NodePath): 渲染节点 + """ + self.physics_world = physics_world + self.render_node = render_node + + # 调试可视化设置 + self.debug_enabled = ConfigVariableBool('physics-debug-enabled', False).getValue() + self.show_constraints = ConfigVariableBool('physics-show-constraints', True).getValue() + self.show_aabbs = ConfigVariableBool('physics-show-aabbs', False).getValue() + self.show_contacts = ConfigVariableBool('physics-show-contacts', False).getValue() + self.show_normals = ConfigVariableBool('physics-show-normals', False).getValue() + self.show_velocities = ConfigVariableBool('physics-show-velocities', False).getValue() + self.show_forces = ConfigVariableBool('physics-show-forces', False).getValue() + self.show_axes = ConfigVariableBool('physics-show-axes', False).getValue() + self.show_mass_centers = ConfigVariableBool('physics-show-mass-centers', False).getValue() + self.show_collision_shapes = ConfigVariableBool('physics-show-collision-shapes', True).getValue() + self.show_sleeping_states = ConfigVariableBool('physics-show-sleeping-states', True).getValue() + self.show_particle_trails = ConfigVariableBool('physics-show-particle-trails', False).getValue() + + # 可视化节点 + self.debug_root = self.render_node.attachNewNode("PhysicsDebug") + self.constraint_node = self.debug_root.attachNewNode("Constraints") + self.aabb_node = self.debug_root.attachNewNode("AABBs") + self.contact_node = self.debug_root.attachNewNode("Contacts") + self.normal_node = self.debug_root.attachNewNode("Normals") + self.velocity_node = self.debug_root.attachNewNode("Velocities") + self.force_node = self.debug_root.attachNewNode("Forces") + self.axes_node = self.debug_root.attachNewNode("Axes") + self.mass_center_node = self.debug_root.attachNewNode("MassCenters") + self.collision_shape_node = self.debug_root.attachNewNode("CollisionShapes") + self.sleeping_node = self.debug_root.attachNewNode("SleepingStates") + self.particle_trail_node = self.debug_root.attachNewNode("ParticleTrails") + + # 调试颜色配置 + self.colors = { + 'static': Vec4(0.5, 0.5, 0.5, 0.7), # 灰色 - 静态物体 + 'dynamic': Vec4(0.0, 1.0, 0.0, 0.7), # 绿色 - 动态物体 + 'kinematic': Vec4(0.0, 0.0, 1.0, 0.7), # 蓝色 - 运动物体 + 'sleeping': Vec4(0.3, 0.3, 0.3, 0.5), # 深灰色 - 休眠物体 + 'constraint': Vec4(1.0, 1.0, 0.0, 0.8), # 黄色 - 约束 + 'contact': Vec4(1.0, 0.0, 0.0, 0.8), # 红色 - 接触点 + 'normal': Vec4(0.0, 1.0, 1.0, 0.8), # 青色 - 法线 + 'velocity': Vec4(1.0, 0.5, 0.0, 0.8), # 橙色 - 速度 + 'force': Vec4(1.0, 0.0, 1.0, 0.8), # 紫色 - 力 + 'positive_axis': Vec4(1.0, 0.0, 0.0, 0.9), # 红色 - X轴 + 'negative_axis': Vec4(0.0, 1.0, 0.0, 0.9), # 绿色 - Y轴 + 'neutral_axis': Vec4(0.0, 0.0, 1.0, 0.9), # 蓝色 - Z轴 + 'mass_center': Vec4(1.0, 1.0, 0.0, 0.9), # 黄色 - 质心 + 'particle_trail': Vec4(0.8, 0.8, 1.0, 0.6), # 浅蓝色 - 粒子轨迹 + } + + # 可视化参数 + self.line_thickness = 2.0 + self.axis_length = 1.0 + self.velocity_scale = 0.1 + self.force_scale = 0.01 + self.contact_point_size = 0.1 + self.normal_scale = 0.5 + self.aabb_line_style = 'solid' # solid, dashed, dotted + self.collision_shape_style = 'wireframe' # wireframe, solid, transparent + + # 性能监控 + self.debug_update_frequency = 30 # 每秒更新次数 + self.debug_update_counter = 0 + self.last_debug_update = 0.0 + + # 调试文本显示 + self.debug_text_enabled = True + self.debug_text_node = None + self.debug_text = "" + + # 碰撞可视化历史 + self.contact_history = deque(maxlen=100) + self.collision_history = deque(maxlen=50) + + # 性能统计显示 + self.stats_display_enabled = True + self.stats_text_node = None + + # 初始化调试文本 + self._init_debug_text() + + # 根据初始状态显示或隐藏 + if self.debug_enabled: + self.debug_root.show() + else: + self.debug_root.hide() + + print("物理调试工具初始化完成") + + def _init_debug_text(self): + """ + 初始化调试文本显示 + """ + # 创建调试文本节点 + self.debug_text_node = TextNode("PhysicsDebugText") + self.debug_text_node.setText("") + self.debug_text_node.setTextColor(1, 1, 1, 1) # 白色 + self.debug_text_node.setAlign(TextNode.ALeft) + self.debug_text_node.setFontSize(16) + + # 创建文本节点路径 + text_np = self.render_node.attachNewNode(self.debug_text_node) + text_np.setScale(0.05) # 调整文本大小 + text_np.setPos(-1.5, 0, 0.9) # 屏幕左上角 + text_np.setBin("fixed", 100) # 确保在最上层 + text_np.setDepthWrite(False) + text_np.setDepthTest(False) + + # 创建性能统计文本节点 + self.stats_text_node = TextNode("PhysicsStatsText") + self.stats_text_node.setText("") + self.stats_text_node.setTextColor(1, 1, 1, 1) # 白色 + self.stats_text_node.setAlign(TextNode.ARight) + self.stats_text_node.setFontSize(14) + + # 创建统计文本节点路径 + stats_np = self.render_node.attachNewNode(self.stats_text_node) + stats_np.setScale(0.04) # 调整文本大小 + stats_np.setPos(1.5, 0, 0.9) # 屏幕右上角 + stats_np.setBin("fixed", 100) + stats_np.setDepthWrite(False) + stats_np.setDepthTest(False) + + def enable_debug(self, enabled=True): + """ + 启用或禁用调试可视化 + + Args: + enabled (bool): 是否启用调试 + """ + self.debug_enabled = enabled + if enabled: + self.debug_root.show() + else: + self.debug_root.hide() + + def set_debug_options(self, show_constraints=None, show_aabbs=None, + show_contacts=None, show_normals=None, show_velocities=None, + show_forces=None, show_axes=None, show_mass_centers=None, + show_collision_shapes=None, show_sleeping_states=None, + show_particle_trails=None): + """ + 设置调试选项 + + Args: + show_constraints (bool): 是否显示约束 + show_aabbs (bool): 是否显示包围盒 + show_contacts (bool): 是否显示接触点 + show_normals (bool): 是否显示法线 + show_velocities (bool): 是否显示速度 + show_forces (bool): 是否显示力 + show_axes (bool): 是否显示坐标轴 + show_mass_centers (bool): 是否显示质心 + show_collision_shapes (bool): 是否显示碰撞形状 + show_sleeping_states (bool): 是否显示休眠状态 + show_particle_trails (bool): 是否显示粒子轨迹 + """ + if show_constraints is not None: + self.show_constraints = show_constraints + if show_constraints: + self.constraint_node.show() + else: + self.constraint_node.hide() + + if show_aabbs is not None: + self.show_aabbs = show_aabbs + if show_aabbs: + self.aabb_node.show() + else: + self.aabb_node.hide() + + if show_contacts is not None: + self.show_contacts = show_contacts + if show_contacts: + self.contact_node.show() + else: + self.contact_node.hide() + + if show_normals is not None: + self.show_normals = show_normals + if show_normals: + self.normal_node.show() + else: + self.normal_node.hide() + + if show_velocities is not None: + self.show_velocities = show_velocities + if show_velocities: + self.velocity_node.show() + else: + self.velocity_node.hide() + + if show_forces is not None: + self.show_forces = show_forces + if show_forces: + self.force_node.show() + else: + self.force_node.hide() + + if show_axes is not None: + self.show_axes = show_axes + if show_axes: + self.axes_node.show() + else: + self.axes_node.hide() + + if show_mass_centers is not None: + self.show_mass_centers = show_mass_centers + if show_mass_centers: + self.mass_center_node.show() + else: + self.mass_center_node.hide() + + if show_collision_shapes is not None: + self.show_collision_shapes = show_collision_shapes + if show_collision_shapes: + self.collision_shape_node.show() + else: + self.collision_shape_node.hide() + + if show_sleeping_states is not None: + self.show_sleeping_states = show_sleeping_states + if show_sleeping_states: + self.sleeping_node.show() + else: + self.sleeping_node.hide() + + if show_particle_trails is not None: + self.show_particle_trails = show_particle_trails + if show_particle_trails: + self.particle_trail_node.show() + else: + self.particle_trail_node.hide() + + def update_debug_visualization(self): + """ + 更新调试可视化 + """ + if not self.debug_enabled: + return + + # 控制更新频率 + current_time = ClockObject.getGlobalClock().getFrameTime() + if current_time - self.last_debug_update < 1.0 / self.debug_update_frequency: + return + + self.last_debug_update = current_time + self.debug_update_counter += 1 + + # 清除之前的可视化 + self._clear_debug_visualization() + + # 可视化刚体 + self._visualize_rigid_bodies() + + # 可视化约束 + if self.show_constraints: + self._visualize_constraints() + + # 可视化接触点 + if self.show_contacts: + self._visualize_contacts() + + # 可视化法线 + if self.show_normals: + self._visualize_normals() + + # 可视化速度 + if self.show_velocities: + self._visualize_velocities() + + # 可视化力 + if self.show_forces: + self._visualize_forces() + + # 可视化坐标轴 + if self.show_axes: + self._visualize_axes() + + # 可视化质心 + if self.show_mass_centers: + self._visualize_mass_centers() + + # 可视化碰撞形状 + if self.show_collision_shapes: + self._visualize_collision_shapes() + + # 可视化休眠状态 + if self.show_sleeping_states: + self._visualize_sleeping_states() + + # 可视化粒子轨迹 + if self.show_particle_trails: + self._visualize_particle_trails() + + # 更新调试文本 + if self.debug_text_enabled: + self._update_debug_text() + + # 更新性能统计 + if self.stats_display_enabled: + self._update_stats_display() + + def _clear_debug_visualization(self): + """ + 清除调试可视化 + """ + # 清除所有子节点 + for child in self.constraint_node.getChildren(): + child.removeNode() + for child in self.aabb_node.getChildren(): + child.removeNode() + for child in self.contact_node.getChildren(): + child.removeNode() + for child in self.normal_node.getChildren(): + child.removeNode() + for child in self.velocity_node.getChildren(): + child.removeNode() + for child in self.force_node.getChildren(): + child.removeNode() + for child in self.axes_node.getChildren(): + child.removeNode() + for child in self.mass_center_node.getChildren(): + child.removeNode() + for child in self.collision_shape_node.getChildren(): + child.removeNode() + for child in self.sleeping_node.getChildren(): + child.removeNode() + for child in self.particle_trail_node.getChildren(): + child.removeNode() + + def _visualize_rigid_bodies(self): + """ + 可视化刚体 + """ + for rigid_body in self.physics_world.rigid_bodies: + # 获取刚体的AABB + aabb = rigid_body.bullet_body.getAabb() + min_point = aabb.getMin() + max_point = aabb.getMax() + + # 创建包围盒可视化 + if self.show_aabbs: + self._draw_aabb(min_point, max_point, rigid_body) + + def _draw_aabb(self, min_point, max_point, rigid_body): + """ + 绘制AABB包围盒 + + Args: + min_point (Point3): 最小点 + max_point (Point3): 最大点 + rigid_body (RigidBody): 刚体对象 + """ + # 确定颜色 + if rigid_body.body_type == rigid_body.STATIC: + color = self.colors['static'] + elif rigid_body.body_type == rigid_body.KINEMATIC: + color = self.colors['kinematic'] + else: + color = self.colors['dynamic'] + + # 如果显示休眠状态且刚体休眠,使用休眠颜色 + if self.show_sleeping_states and not rigid_body.bullet_body.isActive(): + color = self.colors['sleeping'] + + # 创建线条几何体 + vformat = GeomVertexFormat.getV3c4() + vdata = GeomVertexData('aabb', vformat, Geom.UHStatic) + vertex = GeomVertexWriter(vdata, 'vertex') + color_writer = GeomVertexWriter(vdata, 'color') + + # 添加8个顶点 + vertices = [ + Point3(min_point.getX(), min_point.getY(), min_point.getZ()), + Point3(max_point.getX(), min_point.getY(), min_point.getZ()), + Point3(max_point.getX(), max_point.getY(), min_point.getZ()), + Point3(min_point.getX(), max_point.getY(), min_point.getZ()), + Point3(min_point.getX(), min_point.getY(), max_point.getZ()), + Point3(max_point.getX(), min_point.getY(), max_point.getZ()), + Point3(max_point.getX(), max_point.getY(), max_point.getZ()), + Point3(min_point.getX(), max_point.getY(), max_point.getZ()) + ] + + for vert in vertices: + vertex.addData3(vert) + color_writer.addData4(color) + + # 创建线条 + geom = Geom(vdata) + lines = GeomLines(Geom.UHStatic) + + # 添加12条边 + edges = [ + (0, 1), (1, 2), (2, 3), (3, 0), # 底面 + (4, 5), (5, 6), (6, 7), (7, 4), # 顶面 + (0, 4), (1, 5), (2, 6), (3, 7) # 垂直边 + ] + + for start, end in edges: + lines.addVertex(start) + lines.addVertex(end) + lines.closePrimitive() + + geom.addPrimitive(lines) + + # 创建节点 + node = GeomNode('aabb_geom') + node.addGeom(geom) + + # 添加到场景 + aabb_np = self.aabb_node.attachNewNode(node) + aabb_np.setRenderModeWireframe() + + def _visualize_constraints(self): + """ + 可视化约束 + """ + for constraint_info in self.physics_world.constraints: + constraint = constraint_info['constraint'] + body_a = constraint_info['body_a'] + body_b = constraint_info['body_b'] + + # 获取连接点 + if hasattr(constraint, 'getPivotA'): + pivot_a = constraint.getPivotA() + pivot_b = constraint.getPivotB() + + # 转换到世界坐标 + world_pivot_a = body_a.physics_node.getTransform().getPos() + pivot_a + world_pivot_b = body_b.physics_node.getTransform().getPos() + pivot_b + + # 绘制连接线 + self._draw_line(world_pivot_a, world_pivot_b, self.colors['constraint']) + + def _visualize_contacts(self): + """ + 可视化接触点 + """ + # 注意:Bullet不直接提供接触点信息,需要通过碰撞回调获取 + # 这里使用简化的实现,从历史记录中获取 + for contact_info in list(self.contact_history): + position = contact_info['position'] + normal = contact_info['normal'] + + # 绘制接触点 + self._draw_point(position, self.colors['contact'], self.contact_point_size) + + # 绘制法线 + if self.show_normals: + end_point = position + normal * self.normal_scale + self._draw_line(position, end_point, self.colors['normal']) + + def _visualize_normals(self): + """ + 可视化法线 + """ + # 从接触历史中绘制法线 + for contact_info in list(self.contact_history): + position = contact_info['position'] + normal = contact_info['normal'] + end_point = position + normal * self.normal_scale + self._draw_line(position, end_point, self.colors['normal']) + + def _visualize_velocities(self): + """ + 可视化速度 + """ + for rigid_body in self.physics_world.rigid_bodies: + velocity = rigid_body.bullet_body.getLinearVelocity() + if velocity.length() > 0.1: # 只显示大于阈值的速度 + position = rigid_body.physics_node.getPos() + end_point = position + velocity * self.velocity_scale + self._draw_line(position, end_point, self.colors['velocity']) + + # 添加箭头 + self._draw_arrow_head(end_point, velocity, self.colors['velocity']) + + def _visualize_forces(self): + """ + 可视化力 + """ + # 从刚体历史中获取力信息 + for rigid_body in self.physics_world.rigid_bodies: + # 这里需要从刚体中获取力历史,简化实现 + pass + + def _visualize_axes(self): + """ + 可视化坐标轴 + """ + for rigid_body in self.physics_world.rigid_bodies: + position = rigid_body.physics_node.getPos() + rotation = rigid_body.physics_node.getHpr() + + # 创建坐标系变换 + from panda3d.core import TransformState + transform = TransformState.makePosHpr(position, rotation) + + # X轴(红色) + x_end = position + transform.getQuat().getRight() * self.axis_length + self._draw_line(position, x_end, self.colors['positive_axis']) + self._draw_arrow_head(x_end, transform.getQuat().getRight(), self.colors['positive_axis']) + + # Y轴(绿色) + y_end = position + transform.getQuat().getForward() * self.axis_length + self._draw_line(position, y_end, self.colors['negative_axis']) + self._draw_arrow_head(y_end, transform.getQuat().getForward(), self.colors['negative_axis']) + + # Z轴(蓝色) + z_end = position + transform.getQuat().getUp() * self.axis_length + self._draw_line(position, z_end, self.colors['neutral_axis']) + self._draw_arrow_head(z_end, transform.getQuat().getUp(), self.colors['neutral_axis']) + + def _visualize_mass_centers(self): + """ + 可视化质心 + """ + for rigid_body in self.physics_world.rigid_bodies: + # 获取质心位置 + center_of_mass = rigid_body.bullet_body.getCenterOfMassTransform().getPos() + world_position = rigid_body.physics_node.getTransform().getPos() + center_of_mass + + # 绘制质心点 + self._draw_point(world_position, self.colors['mass_center'], 0.05) + + def _visualize_collision_shapes(self): + """ + 可视化碰撞形状 + """ + for rigid_body in self.physics_world.rigid_bodies: + # 获取碰撞形状信息并绘制 + # 这里需要更复杂的实现来准确绘制不同形状 + pass + + def _visualize_sleeping_states(self): + """ + 可视化休眠状态 + """ + # 休眠状态已经在其他可视化中体现,这里可以添加额外的标识 + for rigid_body in self.physics_world.rigid_bodies: + if not rigid_body.bullet_body.isActive(): + # 在休眠物体上添加特殊标记 + position = rigid_body.physics_node.getPos() + self._draw_point(position + Vec3(0, 0, 0.1), self.colors['sleeping'], 0.03) + + def _visualize_particle_trails(self): + """ + 可视化粒子轨迹 + """ + # 从粒子系统中获取轨迹信息 + for particle_system in self.physics_world.particles: + # 绘制每个粒子的轨迹 + # 这里需要访问粒子系统的轨迹数据 + pass + + def _draw_line(self, start, end, color, thickness=None): + """ + 绘制线条 + + Args: + start (Point3): 起点 + end (Point3): 终点 + color (Vec4): 颜色 + thickness (float): 线条粗细 + """ + if thickness is None: + thickness = self.line_thickness + + # 使用LineSegs绘制线条 + segs = LineSegs('debug_line') + segs.setThickness(thickness) + segs.setColor(color) + segs.moveTo(start) + segs.drawTo(end) + + # 创建节点 + node = segs.create() + line_np = self.debug_root.attachNewNode(node) + + def _draw_point(self, position, color, size=0.1): + """ + 绘制点 + + Args: + position (Point3): 位置 + color (Vec4): 颜色 + size (float): 点大小 + """ + # 创建一个小球体表示点 + cm = CardMaker('debug_point') + cm.setFrame(-size, size, -size, size) + point_np = self.debug_root.attachNewNode(cm.generate()) + point_np.setPos(position) + point_np.setColor(color) + + def _draw_arrow_head(self, tip_position, direction, color, size=0.1): + """ + 绘制箭头头部 + + Args: + tip_position (Point3): 箭头顶点位置 + direction (Vec3): 方向向量 + color (Vec4): 颜色 + size (float): 箭头大小 + """ + # 标准化方向向量 + direction.normalize() + + # 计算垂直向量 + perpendicular = direction.cross(Vec3(0, 0, 1)) + if perpendicular.length() < 0.001: + perpendicular = direction.cross(Vec3(1, 0, 0)) + perpendicular.normalize() + + # 计算箭头翼点 + wing1 = tip_position - direction * size + perpendicular * size * 0.5 + wing2 = tip_position - direction * size - perpendicular * size * 0.5 + + # 绘制箭头翼 + self._draw_line(tip_position, wing1, color) + self._draw_line(tip_position, wing2, color) + + def _update_debug_text(self): + """ + 更新调试文本 + """ + if not self.debug_text_node: + return + + # 构建调试文本 + debug_lines = [] + + # 添加基本统计 + stats = self.physics_world.get_performance_stats() + debug_lines.append(f"Physics Debug Info:") + debug_lines.append(f" FPS: {stats.get('current_fps', 0):.1f}") + debug_lines.append(f" Update Time: {stats.get('last_update_time', 0)*1000:.2f}ms") + debug_lines.append(f" Objects: {len(self.physics_world.rigid_bodies)} rigid bodies") + debug_lines.append(f" Constraints: {len(self.physics_world.constraints)} constraints") + debug_lines.append(f" Particles: {len(self.physics_world.particles)} systems") + + # 添加活动物体统计 + active_count = sum(1 for body in self.physics_world.rigid_bodies if body.bullet_body.isActive()) + sleeping_count = len(self.physics_world.rigid_bodies) - active_count + debug_lines.append(f" Active Bodies: {active_count}") + debug_lines.append(f" Sleeping Bodies: {sleeping_count}") + + # 更新文本节点 + self.debug_text = '\n'.join(debug_lines) + self.debug_text_node.setText(self.debug_text) + + def _update_stats_display(self): + """ + 更新性能统计显示 + """ + if not self.stats_text_node: + return + + # 获取性能统计 + stats = self.physics_world.get_performance_stats() + + # 构建统计文本 + stat_lines = [] + stat_lines.append("Performance Stats:") + stat_lines.append(f"Avg Update: {stats.get('avg_update_time', 0)*1000:.2f}ms") + stat_lines.append(f"Max Update: {stats.get('max_update_time', 0)*1000:.2f}ms") + stat_lines.append(f"Min Update: {stats.get('min_update_time', 0)*1000:.2f}ms") + stat_lines.append(f"Objects: {stats.get('objects', {}).get('rigid_bodies', 0)} bodies") + stat_lines.append(f"Constraints: {stats.get('objects', {}).get('constraints', 0)}") + stat_lines.append(f"Particles: {stats.get('objects', {}).get('particles', 0)}") + + # 更新统计文本节点 + stats_text = '\n'.join(stat_lines) + self.stats_text_node.setText(stats_text) + + def add_contact_point(self, position, normal, impulse): + """ + 添加接触点信息(供外部调用) + + Args: + position (Point3): 接触点位置 + normal (Vec3): 接触法线 + impulse (float): 冲量 + """ + contact_info = { + 'position': position, + 'normal': normal, + 'impulse': impulse, + 'timestamp': time.time() + } + self.contact_history.append(contact_info) + + def add_collision_event(self, body_a, body_b, contact_points): + """ + 添加碰撞事件(供外部调用) + + Args: + body_a (RigidBody): 第一个刚体 + body_b (RigidBody): 第二个刚体 + contact_points (list): 接触点列表 + """ + collision_info = { + 'body_a': body_a, + 'body_b': body_b, + 'contact_points': contact_points, + 'timestamp': time.time() + } + self.collision_history.append(collision_info) + + def set_line_thickness(self, thickness): + """ + 设置线条粗细 + + Args: + thickness (float): 线条粗细 + """ + self.line_thickness = max(0.1, thickness) + + def set_axis_length(self, length): + """ + 设置坐标轴长度 + + Args: + length (float): 坐标轴长度 + """ + self.axis_length = max(0.1, length) + + def set_velocity_scale(self, scale): + """ + 设置速度可视化缩放 + + Args: + scale (float): 速度缩放因子 + """ + self.velocity_scale = max(0.001, scale) + + def set_force_scale(self, scale): + """ + 设置力可视化缩放 + + Args: + scale (float): 力缩放因子 + """ + self.force_scale = max(0.0001, scale) + + def set_contact_point_size(self, size): + """ + 设置接触点大小 + + Args: + size (float): 接触点大小 + """ + self.contact_point_size = max(0.01, size) + + def set_normal_scale(self, scale): + """ + 设置法线可视化缩放 + + Args: + scale (float): 法线缩放因子 + """ + self.normal_scale = max(0.01, scale) + + def enable_debug_text(self, enabled=True): + """ + 启用或禁用调试文本显示 + + Args: + enabled (bool): 是否启用调试文本 + """ + self.debug_text_enabled = enabled + if self.debug_text_node: + if enabled: + self.debug_text_node.show() + else: + self.debug_text_node.hide() + + def enable_stats_display(self, enabled=True): + """ + 启用或禁用性能统计显示 + + Args: + enabled (bool): 是否启用性能统计显示 + """ + self.stats_display_enabled = enabled + if self.stats_text_node: + if enabled: + self.stats_text_node.show() + else: + self.stats_text_node.hide() + + def set_debug_update_frequency(self, frequency): + """ + 设置调试更新频率 + + Args: + frequency (int): 更新频率(Hz) + """ + self.debug_update_frequency = max(1, frequency) + + def hide_debug_visualization(self): + """ + 隐藏调试可视化 + """ + self.debug_root.hide() + + def show_debug_visualization(self): + """ + 显示调试可视化 + """ + if self.debug_enabled: + self.debug_root.show() + + def destroy(self): + """ + 销毁调试工具 + """ + # 清除所有可视化 + self._clear_debug_visualization() + + # 移除文本节点 + if self.debug_text_node and not self.debug_text_node.isEmpty(): + self.debug_text_node.removeNode() + if self.stats_text_node and not self.stats_text_node.isEmpty(): + self.stats_text_node.removeNode() + + # 移除节点 + if self.debug_root and not self.debug_root.isEmpty(): + self.debug_root.removeNode() + + # 清空历史记录 + self.contact_history.clear() + self.collision_history.clear() + + print("物理调试工具已销毁") + + +class PhysicsProfiler: + """ + 物理性能分析器 + 监控和分析物理系统的性能 + """ + + def __init__(self, physics_world): + """ + 初始化性能分析器 + + Args: + physics_world (PhysicsWorld): 物理世界对象 + """ + self.physics_world = physics_world + + # 性能数据 + self.update_times = deque(maxlen=1000) + self.rigid_body_counts = deque(maxlen=1000) + self.constraint_counts = deque(maxlen=1000) + self.particle_counts = deque(maxlen=1000) + self.collision_counts = deque(maxlen=1000) + self.active_body_counts = deque(maxlen=1000) + self.sleeping_body_counts = deque(maxlen=1000) + + # 分析参数 + self.max_history = 1000 # 最大历史记录数 + self.sampling_interval = 1.0 # 采样间隔(秒) + self.last_sample_time = 0.0 + + # 性能阈值 + self.warning_threshold = 0.016 # 警告阈值(秒) + self.error_threshold = 0.033 # 错误阈值(秒) + + # 统计计数器 + self.total_updates = 0 + self.total_collisions = 0 + self.total_constraints_solved = 0 + self.peak_performance = { + 'max_update_time': 0.0, + 'max_rigid_bodies': 0, + 'max_constraints': 0, + 'max_particles': 0, + 'max_collisions': 0 + } + + print("物理性能分析器初始化完成") + + def record_frame(self): + """ + 记录帧数据 + """ + current_time = time.time() + + # 检查采样间隔 + if current_time - self.last_sample_time < self.sampling_interval: + return + + self.last_sample_time = current_time + + # 获取性能统计 + stats = self.physics_world.get_performance_stats() + + # 记录更新时间 + update_time = stats.get('last_update_time', 0.0) + self.update_times.append(update_time) + + # 更新峰值性能记录 + if update_time > self.peak_performance['max_update_time']: + self.peak_performance['max_update_time'] = update_time + + # 记录对象数量 + objects = stats.get('objects', {}) + rigid_bodies = objects.get('rigid_bodies', 0) + constraints = objects.get('constraints', 0) + particles = objects.get('particles', 0) + + self.rigid_body_counts.append(rigid_bodies) + self.constraint_counts.append(constraints) + self.particle_counts.append(particles) + + # 更新峰值记录 + if rigid_bodies > self.peak_performance['max_rigid_bodies']: + self.peak_performance['max_rigid_bodies'] = rigid_bodies + if constraints > self.peak_performance['max_constraints']: + self.peak_performance['max_constraints'] = constraints + if particles > self.peak_performance['max_particles']: + self.peak_performance['max_particles'] = particles + + # 记录活动和休眠物体数量 + active_count = 0 + sleeping_count = 0 + for body in self.physics_world.rigid_bodies: + if body.bullet_body.isActive(): + active_count += 1 + else: + sleeping_count += 1 + + self.active_body_counts.append(active_count) + self.sleeping_body_counts.append(sleeping_count) + + # 更新总计数器 + self.total_updates += 1 + + # 检查性能警告 + self._check_performance_warnings(update_time) + + def _check_performance_warnings(self, update_time): + """ + 检查性能警告 + + Args: + update_time (float): 更新时间 + """ + if update_time > self.error_threshold: + print(f"[ERROR] Physics update time exceeded error threshold: {update_time*1000:.2f}ms") + elif update_time > self.warning_threshold: + print(f"[WARNING] Physics update time exceeded warning threshold: {update_time*1000:.2f}ms") + + def get_average_update_time(self): + """ + 获取平均更新时间 + + Returns: + float: 平均更新时间(秒) + """ + if not self.update_times: + return 0.0 + return sum(self.update_times) / len(self.update_times) + + def get_max_update_time(self): + """ + 获取最大更新时间 + + Returns: + float: 最大更新时间(秒) + """ + if not self.update_times: + return 0.0 + return max(self.update_times) + + def get_min_update_time(self): + """ + 获取最小更新时间 + + Returns: + float: 最小更新时间(秒) + """ + if not self.update_times: + return 0.0 + return min(self.update_times) + + def get_average_fps(self): + """ + 获取平均帧率 + + Returns: + float: 平均帧率 + """ + avg_update_time = self.get_average_update_time() + if avg_update_time > 0: + return 1.0 / avg_update_time + return 0.0 + + def get_object_counts(self): + """ + 获取对象数量统计 + + Returns: + dict: 对象数量统计 + """ + return { + 'rigid_bodies': len(self.physics_world.rigid_bodies), + 'constraints': len(self.physics_world.constraints), + 'vehicles': len(self.physics_world.vehicles), + 'particles': sum([ps.get_particle_count() + for ps in self.physics_world.particles]), + 'soft_bodies': len(self.physics_world.soft_bodies), + 'ghost_objects': len(self.physics_world.ghost_objects) + } + + def get_average_object_counts(self): + """ + 获取平均对象数量 + + Returns: + dict: 平均对象数量统计 + """ + if not self.rigid_body_counts: + return self.get_object_counts() + + return { + 'rigid_bodies': sum(self.rigid_body_counts) / len(self.rigid_body_counts), + 'constraints': sum(self.constraint_counts) / len(self.constraint_counts), + 'particles': sum(self.particle_counts) / len(self.particle_counts), + 'active_bodies': sum(self.active_body_counts) / len(self.active_body_counts) if self.active_body_counts else 0, + 'sleeping_bodies': sum(self.sleeping_body_counts) / len(self.sleeping_body_counts) if self.sleeping_body_counts else 0 + } + + def get_performance_report(self): + """ + 获取性能报告 + + Returns: + dict: 性能报告 + """ + return { + 'average_update_time': self.get_average_update_time(), + 'max_update_time': self.get_max_update_time(), + 'min_update_time': self.get_min_update_time(), + 'average_fps': self.get_average_fps(), + 'current_fps': 1.0 / self.get_average_update_time() if self.get_average_update_time() > 0 else 0, + 'object_counts': self.get_object_counts(), + 'average_object_counts': self.get_average_object_counts(), + 'peak_performance': self.peak_performance.copy(), + 'total_updates': self.total_updates, + 'total_collisions': self.total_collisions, + 'history_length': len(self.update_times), + 'sampling_interval': self.sampling_interval + } + + def get_performance_analysis(self): + """ + 获取性能分析 + + Returns: + dict: 性能分析结果 + """ + report = self.get_performance_report() + + # 计算性能等级 + avg_update_time = report['average_update_time'] + if avg_update_time <= 0.008: # 8ms + performance_grade = "Excellent" + elif avg_update_time <= 0.016: # 16ms + performance_grade = "Good" + elif avg_update_time <= 0.024: # 24ms + performance_grade = "Acceptable" + elif avg_update_time <= 0.033: # 33ms + performance_grade = "Poor" + else: + performance_grade = "Critical" + + # 分析瓶颈 + bottlenecks = [] + if report['object_counts']['rigid_bodies'] > 500: + bottlenecks.append("High rigid body count") + if report['object_counts']['constraints'] > 100: + bottlenecks.append("High constraint count") + if report['object_counts']['particles'] > 1000: + bottlenecks.append("High particle count") + if report['average_object_counts']['active_bodies'] > 200: + bottlenecks.append("Too many active bodies") + + return { + 'performance_grade': performance_grade, + 'bottlenecks': bottlenecks, + 'recommendations': self._generate_recommendations(report), + 'report': report + } + + def _generate_recommendations(self, report): + """ + 生成性能优化建议 + + Args: + report (dict): 性能报告 + + Returns: + list: 优化建议列表 + """ + recommendations = [] + + # 基于性能等级的建议 + if report['performance_grade'] == "Poor": + recommendations.append("Consider reducing physics complexity") + elif report['performance_grade'] == "Critical": + recommendations.append("URGENT: Physics performance critical - immediate optimization needed") + + # 基于对象数量的建议 + if report['object_counts']['rigid_bodies'] > 500: + recommendations.append("Reduce rigid body count through batching or simplification") + if report['object_counts']['constraints'] > 100: + recommendations.append("Optimize constraint usage - consider removing unnecessary constraints") + if report['object_counts']['particles'] > 1000: + recommendations.append("Reduce particle system complexity or limit particle count") + + # 基于活动物体的建议 + avg_active = report['average_object_counts']['active_bodies'] + if avg_active > 200: + recommendations.append("Improve sleeping thresholds to reduce active bodies") + + # 基于更新时间的建议 + if report['average_update_time'] > 0.016: + recommendations.append("Profile physics update to identify slow operations") + recommendations.append("Consider multithreading if available") + + return recommendations + + def reset_statistics(self): + """ + 重置统计信息 + """ + self.update_times.clear() + self.rigid_body_counts.clear() + self.constraint_counts.clear() + self.particle_counts.clear() + self.collision_counts.clear() + self.active_body_counts.clear() + self.sleeping_body_counts.clear() + + self.total_updates = 0 + self.total_collisions = 0 + self.total_constraints_solved = 0 + + # 重置峰值记录(保留一些) + self.peak_performance['max_update_time'] = 0.0 + self.peak_performance['max_collisions'] = 0 + + print("性能统计已重置") + + def export_statistics(self, filepath): + """ + 导出统计信息到文件 + + Args: + filepath (str): 导出文件路径 + """ + import json + + # 构建统计数据 + stats_data = { + 'performance_report': self.get_performance_report(), + 'performance_analysis': self.get_performance_analysis(), + 'raw_data': { + 'update_times': list(self.update_times), + 'rigid_body_counts': list(self.rigid_body_counts), + 'constraint_counts': list(self.constraint_counts), + 'particle_counts': list(self.particle_counts), + 'collision_counts': list(self.collision_counts), + 'active_body_counts': list(self.active_body_counts), + 'sleeping_body_counts': list(self.sleeping_body_counts) + }, + 'export_timestamp': time.time(), + 'export_date': time.strftime('%Y-%m-%d %H:%M:%S') + } + + # 导出到JSON文件 + try: + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(stats_data, f, indent=2, ensure_ascii=False) + print(f"性能统计已导出到: {filepath}") + except Exception as e: + print(f"导出性能统计失败: {e}") + + def destroy(self): + """ + 销毁性能分析器 + """ + self.reset_statistics() + print("物理性能分析器已销毁") diff --git a/plugins/user/rigid_body_physics/particles/particle_system.py b/plugins/user/rigid_body_physics/particles/particle_system.py index 4e1af1ae..68b2a251 100644 --- a/plugins/user/rigid_body_physics/particles/particle_system.py +++ b/plugins/user/rigid_body_physics/particles/particle_system.py @@ -1,1158 +1,1158 @@ -""" -粒子系统模块 -负责物理粒子的创建和管理 -""" - -from panda3d.core import Vec3, Point3, LVector3f, ClockObject -from panda3d.bullet import BulletRigidBodyNode, BulletSphereShape, BulletBoxShape -import random -import math -import time -from collections import deque - - -class ParticleSystem: - """ - 粒子系统类 - 管理大量物理粒子的创建、更新和销毁 - """ - - # 粒子类型常量 - TYPE_DEFAULT = 'default' - TYPE_SMOKE = 'smoke' - TYPE_FIRE = 'fire' - TYPE_WATER = 'water' - TYPE_DEBRIS = 'debris' - TYPE_EXPLOSION = 'explosion' - TYPE_SPARK = 'spark' - TYPE_BUBBLE = 'bubble' - TYPE_LEAF = 'leaf' - TYPE_SNOW = 'snow' - TYPE_RAIN = 'rain' - - def __init__(self, physics_world, max_particles=1000, particle_type=TYPE_DEFAULT): - """ - 初始化粒子系统 - - Args: - physics_world (PhysicsWorld): 物理世界对象 - max_particles (int): 最大粒子数 - particle_type (str): 粒子类型 - """ - self.physics_world = physics_world - self.max_particles = max_particles - self.particle_type = particle_type - - # 粒子容器 - self.particles = [] - self.active_particles = [] - self.inactive_particles = deque() # 对象池 - - # 粒子发射器参数 - self.emission_rate = 100 # 每秒发射粒子数 - self.emission_accumulator = 0.0 - self.emission_burst_count = 0 # 爆发粒子数 - self.emission_burst_timer = 0.0 - self.emission_burst_interval = 0.0 # 爆发间隔 - - # 粒子默认属性 - self.particle_mass = 0.1 - self.particle_radius = 0.1 - self.particle_size = Vec3(0.1, 0.1, 0.1) # 非球形粒子尺寸 - self.particle_lifetime = 5.0 - self.particle_color = Vec3(1, 1, 1) - self.particle_shape_type = 'sphere' # sphere, box, capsule - - # 发射器属性 - self.emitter_position = Point3(0, 0, 0) - self.emitter_velocity = Vec3(0, 0, 0) - self.emitter_spread = 0.1 - self.emitter_radius = 0.0 - self.emitter_shape = 'point' # point, circle, box, sphere - self.emitter_dimensions = Vec3(1, 1, 1) # 发射器尺寸 - - # 物理属性 - self.gravity_scale = 1.0 - self.air_resistance = 0.01 - self.bounce_factor = 0.5 - self.friction = 0.1 - self.restitution = 0.3 - self.linear_damping = 0.01 - self.angular_damping = 0.01 - - # 碰撞属性 - self.collision_enabled = True - self.collision_group = 1 - self.collision_mask = 0xFFFFFFFF - self.collision_response = True # 是否响应碰撞 - - # 高级粒子属性 - self.particle_trail_enabled = False - self.particle_trail_length = 10 - self.particle_fade_enabled = True - self.particle_size_variation = 0.2 - self.particle_velocity_variation = 0.3 - self.particle_rotation_enabled = False - self.particle_rotation_speed = 0.0 - self.particle_rotation_variation = 0.5 - - # 环境影响 - self.wind_enabled = False - self.wind_vector = Vec3(0, 0, 0) - self.wind_turbulence = 0.0 - self.temperature_effect = 0.0 # 温度影响(浮力等) - - # 特殊效果 - self.vortex_enabled = False - self.vortex_center = Point3(0, 0, 0) - self.vortex_strength = 0.0 - self.vortex_axis = Vec3(0, 0, 1) - - # 粒子生命周期行为 - self.size_over_lifetime = None # [(time, scale), ...] - self.color_over_lifetime = None # [(time, color), ...] - self.force_over_lifetime = None # [(time, force), ...] - - # 粒子子发射器 - self.sub_emitter_enabled = False - self.sub_emitter_rate = 0.0 - self.sub_emitter_count = 1 - self.sub_emitter_lifetime = 1.0 - - # 性能优化 - self.sleeping_threshold = 0.1 # 速度低于此值时休眠 - self.culling_distance = 100.0 # 距离摄像机超过此距离时剔除 - self.batch_processing = True # 批量处理 - - # 统计信息 - self.emitted_count = 0 - self.recycled_count = 0 - self.collision_count = 0 - self.max_active_particles = 0 - - # 时间控制 - self.time_scale = 1.0 - self.paused = False - - # 粒子材质 - self.particle_material = None - - # 粒子纹理(如果需要渲染) - self.particle_texture = None - self.particle_blend_mode = 'alpha' # alpha, additive, multiply - - # 粒子死亡回调 - self.death_callbacks = [] - - # 粒子碰撞回调 - self.collision_callbacks = [] - - print(f"粒子系统初始化完成,最大粒子数: {max_particles},类型: {particle_type}") - - def set_emitter_properties(self, position=None, velocity=None, spread=None, radius=None, - shape=None, dimensions=None): - """ - 设置发射器属性 - - Args: - position (Point3): 发射器位置 - velocity (Vec3): 发射器基础速度 - spread (float): 发射扩散角度 - radius (float): 发射器半径 - shape (str): 发射器形状 - dimensions (Vec3): 发射器尺寸 - """ - if position is not None: - self.emitter_position = position - if velocity is not None: - self.emitter_velocity = velocity - if spread is not None: - self.emitter_spread = spread - if radius is not None: - self.emitter_radius = radius - if shape is not None: - self.emitter_shape = shape - if dimensions is not None: - self.emitter_dimensions = dimensions - - def set_particle_properties(self, mass=None, radius=None, size=None, lifetime=None, - color=None, shape_type=None): - """ - 设置粒子属性 - - Args: - mass (float): 粒子质量 - radius (float): 粒子半径(球形) - size (Vec3): 粒子尺寸(非球形) - lifetime (float): 粒子生命周期 - color (Vec3): 粒子颜色 - shape_type (str): 粒子形状类型 - """ - if mass is not None: - self.particle_mass = mass - if radius is not None: - self.particle_radius = radius - if size is not None: - self.particle_size = size - if lifetime is not None: - self.particle_lifetime = lifetime - if color is not None: - self.particle_color = color - if shape_type is not None: - self.particle_shape_type = shape_type - - def set_physics_properties(self, gravity_scale=None, air_resistance=None, - bounce_factor=None, friction=None, restitution=None, - linear_damping=None, angular_damping=None): - """ - 设置物理属性 - - Args: - gravity_scale (float): 重力缩放 - air_resistance (float): 空气阻力 - bounce_factor (float): 弹跳因子 - friction (float): 摩擦系数 - restitution (float): 恢复系数 - linear_damping (float): 线性阻尼 - angular_damping (float): 角阻尼 - """ - if gravity_scale is not None: - self.gravity_scale = gravity_scale - if air_resistance is not None: - self.air_resistance = air_resistance - if bounce_factor is not None: - self.bounce_factor = bounce_factor - if friction is not None: - self.friction = friction - if restitution is not None: - self.restitution = restitution - if linear_damping is not None: - self.linear_damping = linear_damping - if angular_damping is not None: - self.angular_damping = angular_damping - - def set_collision_properties(self, enabled=None, group=None, mask=None, response=None): - """ - 设置碰撞属性 - - Args: - enabled (bool): 是否启用碰撞 - group (int): 碰撞组 - mask (int): 碰撞掩码 - response (bool): 是否响应碰撞 - """ - if enabled is not None: - self.collision_enabled = enabled - if group is not None: - self.collision_group = group - if mask is not None: - self.collision_mask = mask - if response is not None: - self.collision_response = response - - def set_environmental_effects(self, wind_enabled=None, wind_vector=None, - wind_turbulence=None, temperature_effect=None): - """ - 设置环境影响 - - Args: - wind_enabled (bool): 是否启用风 - wind_vector (Vec3): 风向量 - wind_turbulence (float): 风湍流 - temperature_effect (float): 温度影响 - """ - if wind_enabled is not None: - self.wind_enabled = wind_enabled - if wind_vector is not None: - self.wind_vector = wind_vector - if wind_turbulence is not None: - self.wind_turbulence = wind_turbulence - if temperature_effect is not None: - self.temperature_effect = temperature_effect - - def set_special_effects(self, vortex_enabled=None, vortex_center=None, - vortex_strength=None, vortex_axis=None): - """ - 设置特殊效果 - - Args: - vortex_enabled (bool): 是否启用涡流 - vortex_center (Point3): 涡流中心 - vortex_strength (float): 涡流强度 - vortex_axis (Vec3): 涡流轴向 - """ - if vortex_enabled is not None: - self.vortex_enabled = vortex_enabled - if vortex_center is not None: - self.vortex_center = vortex_center - if vortex_strength is not None: - self.vortex_strength = vortex_strength - if vortex_axis is not None: - self.vortex_axis = vortex_axis - - def set_lifetime_behaviors(self, size_curve=None, color_curve=None, force_curve=None): - """ - 设置粒子生命周期行为 - - Args: - size_curve (list): 尺寸随时间变化曲线 [(time, scale), ...] - color_curve (list): 颜色随时间变化曲线 [(time, color), ...] - force_curve (list): 力随时间变化曲线 [(time, force), ...] - """ - if size_curve is not None: - self.size_over_lifetime = size_curve - if color_curve is not None: - self.color_over_lifetime = color_curve - if force_curve is not None: - self.force_over_lifetime = force_curve - - def set_sub_emitter(self, enabled=None, rate=None, count=None, lifetime=None): - """ - 设置粒子子发射器 - - Args: - enabled (bool): 是否启用子发射器 - rate (float): 子发射器速率 - count (int): 每次发射的子粒子数 - lifetime (float): 子粒子生命周期 - """ - if enabled is not None: - self.sub_emitter_enabled = enabled - if rate is not None: - self.sub_emitter_rate = rate - if count is not None: - self.sub_emitter_count = count - if lifetime is not None: - self.sub_emitter_lifetime = lifetime - - def set_performance_options(self, sleeping_threshold=None, culling_distance=None, - batch_processing=None): - """ - 设置性能优化选项 - - Args: - sleeping_threshold (float): 休眠阈值 - culling_distance (float): 剔除距离 - batch_processing (bool): 批量处理 - """ - if sleeping_threshold is not None: - self.sleeping_threshold = sleeping_threshold - if culling_distance is not None: - self.culling_distance = culling_distance - if batch_processing is not None: - self.batch_processing = batch_processing - - def set_emission_rate(self, rate): - """ - 设置发射速率 - - Args: - rate (float): 每秒发射粒子数 - """ - self.emission_rate = max(0, rate) - - def set_emission_burst(self, count, interval=0.0): - """ - 设置爆发发射 - - Args: - count (int): 每次爆发的粒子数 - interval (float): 爆发间隔(秒) - """ - self.emission_burst_count = count - self.emission_burst_interval = interval - - def emit_particle(self, position=None, velocity=None, lifetime=None, color=None): - """ - 发射单个粒子 - - Args: - position (Point3): 粒子位置 - velocity (Vec3): 粒子速度 - lifetime (float): 粒子生命周期 - color (Vec3): 粒子颜色 - - Returns: - dict: 粒子信息字典 - """ - # 检查是否达到最大粒子数 - if len(self.active_particles) >= self.max_particles: - return None - - # 从对象池获取粒子或创建新粒子 - if self.inactive_particles: - particle_info = self.inactive_particles.popleft() - self.recycled_count += 1 - else: - particle_info = self._create_new_particle() - - # 设置粒子属性 - if position is None: - position = self._calculate_emission_position() - if velocity is None: - velocity = self._calculate_emission_velocity() - if lifetime is None: - lifetime = self.particle_lifetime * random.uniform( - 1.0 - self.particle_lifetime * 0.2, - 1.0 + self.particle_lifetime * 0.2 - ) - if color is None: - color = self.particle_color - - # 初始化粒子状态 - particle_info['position'] = position - particle_info['velocity'] = velocity - particle_info['lifetime'] = lifetime - particle_info['age'] = 0.0 - particle_info['color'] = color - particle_info['initial_size'] = particle_info['size'] - particle_info['initial_color'] = color - particle_info['initial_velocity'] = velocity - particle_info['rotation'] = random.uniform(0, 360) if self.particle_rotation_enabled else 0.0 - particle_info['rotation_speed'] = self.particle_rotation_speed * random.uniform( - 1.0 - self.particle_rotation_variation, - 1.0 + self.particle_rotation_variation - ) - particle_info['trail'] = [] if self.particle_trail_enabled else None - particle_info['collisions'] = 0 - particle_info['sleeping'] = False - particle_info['culled'] = False - - # 添加到物理世界 - self.physics_world.bullet_world.attachRigidBody(particle_info['body']) - - # 设置物理属性 - particle_info['body'].setLinearVelocity(velocity) - particle_info['body'].setFriction(self.friction) - particle_info['body'].setRestitution(self.restitution) - particle_info['body'].setLinearDamping(self.linear_damping) - particle_info['body'].setAngularDamping(self.angular_damping) - - # 设置碰撞属性 - if self.collision_enabled: - particle_info['body'].setCollideFilterGroup(self.collision_group) - particle_info['body'].setCollideFilterMask(self.collision_mask) - else: - particle_info['body'].setCollideFilterGroup(0) - particle_info['body'].setCollideFilterMask(0) - - # 设置位置 - particle_info['node'].setPos(position) - - # 添加到活动粒子列表 - self.active_particles.append(particle_info) - self.emitted_count += 1 - - # 更新统计 - self.max_active_particles = max(self.max_active_particles, len(self.active_particles)) - - return particle_info - - def _create_new_particle(self): - """ - 创建新粒子 - - Returns: - dict: 粒子信息字典 - """ - # 创建粒子形状 - if self.particle_shape_type == 'sphere': - size = self.particle_radius * random.uniform( - 1.0 - self.particle_size_variation, - 1.0 + self.particle_size_variation - ) - particle_shape = BulletSphereShape(size) - elif self.particle_shape_type == 'box': - size = self.particle_size * random.uniform( - 1.0 - self.particle_size_variation, - 1.0 + self.particle_size_variation - ) - particle_shape = BulletBoxShape(size * 0.5) - else: - # 默认使用球形 - size = self.particle_radius * random.uniform( - 1.0 - self.particle_size_variation, - 1.0 + self.particle_size_variation - ) - particle_shape = BulletSphereShape(size) - - # 创建粒子刚体 - particle_body = BulletRigidBodyNode(f'Particle_{len(self.particles)}') - particle_body.addShape(particle_shape) - particle_body.setMass(self.particle_mass) - - # 创建粒子节点 - from panda3d.core import NodePath - dummy_np = NodePath("dummy") - particle_node = dummy_np.attachNewNode(particle_body) - - # 创建粒子信息 - particle_info = { - 'node': particle_node, - 'body': particle_body, - 'shape': particle_shape, - 'position': Point3(0, 0, 0), - 'velocity': Vec3(0, 0, 0), - 'lifetime': self.particle_lifetime, - 'age': 0.0, - 'color': self.particle_color, - 'size': size, - 'mass': self.particle_mass, - 'radius': size if self.particle_shape_type == 'sphere' else None, - 'type': self.particle_type, - 'material': self.particle_material, - 'creation_time': time.time(), - } - - self.particles.append(particle_info) - return particle_info - - def _calculate_emission_position(self): - """ - 计算发射位置 - - Returns: - Point3: 发射位置 - """ - if self.emitter_shape == 'point': - return self.emitter_position - elif self.emitter_shape == 'circle': - angle = random.uniform(0, 2 * math.pi) - distance = random.uniform(0, self.emitter_radius) - offset = Point3( - math.cos(angle) * distance, - math.sin(angle) * distance, - 0 - ) - return self.emitter_position + offset - elif self.emitter_shape == 'box': - offset = Point3( - random.uniform(-self.emitter_dimensions.getX() * 0.5, self.emitter_dimensions.getX() * 0.5), - random.uniform(-self.emitter_dimensions.getY() * 0.5, self.emitter_dimensions.getY() * 0.5), - random.uniform(-self.emitter_dimensions.getZ() * 0.5, self.emitter_dimensions.getZ() * 0.5) - ) - return self.emitter_position + offset - elif self.emitter_shape == 'sphere': - # 球面均匀分布 - theta = random.uniform(0, 2 * math.pi) - phi = math.acos(2 * random.uniform(0, 1) - 1) - radius = self.emitter_radius - offset = Point3( - radius * math.sin(phi) * math.cos(theta), - radius * math.sin(phi) * math.sin(theta), - radius * math.cos(phi) - ) - return self.emitter_position + offset - else: - return self.emitter_position - - def _calculate_emission_velocity(self): - """ - 计算发射速度 - - Returns: - Vec3: 发射速度 - """ - # 基础速度 - base_velocity = self.emitter_velocity.copy() - - # 添加随机扩散 - if self.emitter_spread > 0: - spread_angle = random.uniform(0, self.emitter_spread) - spread_direction = random.uniform(0, 2 * math.pi) - spread_offset = Vec3( - math.cos(spread_direction) * math.sin(spread_angle), - math.sin(spread_direction) * math.sin(spread_angle), - math.cos(spread_angle) - ) - base_velocity += spread_offset * base_velocity.length() - - # 添加速度变化 - if self.particle_velocity_variation > 0: - variation = random.uniform( - 1.0 - self.particle_velocity_variation, - 1.0 + self.particle_velocity_variation - ) - base_velocity *= variation - - # 添加随机速度分量 - random_velocity = Vec3( - random.uniform(-1, 1), - random.uniform(-1, 1), - random.uniform(-1, 1) - ) * base_velocity.length() * 0.1 - - return base_velocity + random_velocity - - def update(self, dt): - """ - 更新粒子系统 - - Args: - dt (float): 时间增量 - """ - if self.paused: - return - - # 应用时间缩放 - dt *= self.time_scale - - # 更新现有粒子 - for particle in self.active_particles[:]: # 使用副本避免迭代时修改 - self._update_particle(particle, dt) - - # 发射新粒子 - self._emit_particles(dt) - - # 处理爆发发射 - self._handle_burst_emission(dt) - - def _update_particle(self, particle, dt): - """ - 更新单个粒子 - - Args: - particle (dict): 粒子信息 - dt (float): 时间增量 - """ - # 更新粒子年龄 - particle['age'] += dt - - # 检查生命周期 - if particle['age'] >= particle['lifetime']: - self._destroy_particle(particle) - return - - # 检查是否休眠 - velocity = particle['body'].getLinearVelocity() - if velocity.length() < self.sleeping_threshold: - particle['sleeping'] = True - else: - particle['sleeping'] = False - - # 如果休眠,跳过物理更新 - if particle['sleeping']: - return - - # 应用重力 - if self.gravity_scale > 0: - gravity_force = Vec3(0, 0, -9.81) * self.gravity_scale * particle['mass'] - particle['body'].applyCentralForce(gravity_force) - - # 应用空气阻力 - if self.air_resistance > 0: - drag_force = -velocity * self.air_resistance - particle['body'].applyCentralForce(drag_force) - - # 应用风力 - if self.wind_enabled and self.wind_vector.length() > 0: - wind_force = self.wind_vector * particle['mass'] - if self.wind_turbulence > 0: - turbulence = Vec3( - random.uniform(-1, 1), - random.uniform(-1, 1), - random.uniform(-1, 1) - ) * self.wind_turbulence - wind_force += turbulence - particle['body'].applyCentralForce(wind_force) - - # 应用涡流力 - if self.vortex_enabled and self.vortex_strength > 0: - self._apply_vortex_force(particle) - - # 应用温度影响(浮力等) - if self.temperature_effect != 0: - # 简化的浮力计算 - buoyancy_force = Vec3(0, 0, 1) * self.temperature_effect * particle['mass'] - particle['body'].applyCentralForce(buoyancy_force) - - # 应用生命周期力 - if self.force_over_lifetime: - self._apply_lifetime_force(particle) - - # 更新轨迹 - if self.particle_trail_enabled and particle['trail'] is not None: - position = particle['node'].getPos() - particle['trail'].append(position) - if len(particle['trail']) > self.particle_trail_length: - particle['trail'].pop(0) - - # 更新旋转 - if self.particle_rotation_enabled: - particle['rotation'] += particle['rotation_speed'] * dt - - # 更新尺寸 - if self.size_over_lifetime: - self._update_particle_size(particle) - - # 更新颜色 - if self.particle_fade_enabled or self.color_over_lifetime: - self._update_particle_color(particle) - - # 处理子发射器 - if self.sub_emitter_enabled and self.sub_emitter_rate > 0: - self._handle_sub_emitter(particle, dt) - - def _apply_vortex_force(self, particle): - """ - 应用涡流力 - - Args: - particle (dict): 粒子信息 - """ - position = particle['node'].getPos() - to_center = self.vortex_center - position - distance = to_center.length() - - if distance > 0: - # 径向力(向中心吸引) - radial_force = to_center.normalized() * self.vortex_strength / (distance + 1.0) - - # 切向力(绕轴旋转) - tangent_dir = self.vortex_axis.cross(to_center).normalized() - tangent_force = tangent_dir * self.vortex_strength - - total_force = (radial_force + tangent_force) * particle['mass'] - particle['body'].applyCentralForce(total_force) - - def _apply_lifetime_force(self, particle): - """ - 应用生命周期力 - - Args: - particle (dict): 粒子信息 - """ - normalized_age = particle['age'] / particle['lifetime'] - - # 在力曲线上插值 - for i in range(len(self.force_over_lifetime) - 1): - time1, force1 = self.force_over_lifetime[i] - time2, force2 = self.force_over_lifetime[i + 1] - - if time1 <= normalized_age <= time2: - t = (normalized_age - time1) / (time2 - time1) if time2 != time1 else 0 - interpolated_force = force1 + (force2 - force1) * t - particle['body'].applyCentralForce(interpolated_force * particle['mass']) - break - - def _update_particle_size(self, particle): - """ - 更新粒子尺寸 - - Args: - particle (dict): 粒子信息 - """ - normalized_age = particle['age'] / particle['lifetime'] - - # 在尺寸曲线上插值 - for i in range(len(self.size_over_lifetime) - 1): - time1, scale1 = self.size_over_lifetime[i] - time2, scale2 = self.size_over_lifetime[i + 1] - - if time1 <= normalized_age <= time2: - t = (normalized_age - time1) / (time2 - time1) if time2 != time1 else 0 - interpolated_scale = scale1 + (scale2 - scale1) * t - - # 更新粒子尺寸(这个功能需要与渲染系统集成) - # 这里只是更新记录,实际渲染需要在渲染系统中实现 - if isinstance(particle['size'], (int, float)): - particle['size'] = particle['initial_size'] * interpolated_scale - break - - def _update_particle_color(self, particle): - """ - 更新粒子颜色 - - Args: - particle (dict): 粒子信息 - """ - normalized_age = particle['age'] / particle['lifetime'] - - # 淡出效果 - if self.particle_fade_enabled: - fade_factor = 1.0 - (normalized_age * normalized_age) # 二次淡出 - particle['color'] = particle['initial_color'] * fade_factor - - # 生命周期颜色变化 - if self.color_over_lifetime: - for i in range(len(self.color_over_lifetime) - 1): - time1, color1 = self.color_over_lifetime[i] - time2, color2 = self.color_over_lifetime[i + 1] - - if time1 <= normalized_age <= time2: - t = (normalized_age - time1) / (time2 - time1) if time2 != time1 else 0 - interpolated_color = color1 + (color2 - color1) * t - if self.particle_fade_enabled: - # 结合淡出效果 - particle['color'] = interpolated_color * (1.0 - normalized_age) - else: - particle['color'] = interpolated_color - break - - def _handle_sub_emitter(self, particle, dt): - """ - 处理子发射器 - - Args: - particle (dict): 粒子信息 - dt (float): 时间增量 - """ - # 简化的子发射器实现 - pass # 需要更复杂的实现 - - def _emit_particles(self, dt): - """ - 发射粒子 - - Args: - dt (float): 时间增量 - """ - # 基于发射速率发射粒子 - self.emission_accumulator += dt * self.emission_rate - while self.emission_accumulator >= 1.0 and len(self.active_particles) < self.max_particles: - self.emit_particle() - self.emission_accumulator -= 1.0 - - def _handle_burst_emission(self, dt): - """ - 处理爆发发射 - - Args: - dt (float): 时间增量 - """ - if self.emission_burst_count > 0 and self.emission_burst_interval > 0: - self.emission_burst_timer += dt - if self.emission_burst_timer >= self.emission_burst_interval: - for _ in range(min(self.emission_burst_count, self.max_particles - len(self.active_particles))): - self.emit_particle() - self.emission_burst_timer = 0.0 - - def _destroy_particle(self, particle): - """ - 销毁粒子 - - Args: - particle (dict): 粒子信息 - """ - if particle in self.active_particles: - # 从物理世界移除 - self.physics_world.bullet_world.removeRigidBody(particle['body']) - - # 移除节点 - if particle['node'] and not particle['node'].isEmpty(): - particle['node'].removeNode() - - # 从活动列表移除 - self.active_particles.remove(particle) - - # 添加到对象池 - self.inactive_particles.append(particle) - - # 调用死亡回调 - for callback in self.death_callbacks: - try: - callback(particle) - except Exception as e: - print(f"粒子死亡回调错误: {e}") - - def get_particle_count(self): - """ - 获取活动粒子数量 - - Returns: - int: 活动粒子数量 - """ - return len(self.active_particles) - - def get_total_emitted(self): - """ - 获取总发射粒子数 - - Returns: - int: 总发射粒子数 - """ - return self.emitted_count - - def get_statistics(self): - """ - 获取粒子系统统计信息 - - Returns: - dict: 统计信息 - """ - return { - 'active_particles': len(self.active_particles), - 'total_emitted': self.emitted_count, - 'recycled_particles': self.recycled_count, - 'collision_count': self.collision_count, - 'max_active_particles': self.max_active_particles, - 'emission_rate': self.emission_rate, - 'particle_type': self.particle_type, - 'system_age': time.time() - (self.particles[0]['creation_time'] if self.particles else time.time()), - } - - def clear_particles(self): - """ - 清除所有粒子 - """ - # 销毁所有活动粒子 - for particle in self.active_particles[:]: - self._destroy_particle(particle) - - # 清空对象池 - self.inactive_particles.clear() - - # 清空粒子列表 - self.particles.clear() - self.active_particles.clear() - - # 重置统计信息 - self.emitted_count = 0 - self.recycled_count = 0 - self.collision_count = 0 - self.max_active_particles = 0 - - # 重置发射器 - self.emission_accumulator = 0.0 - self.emission_burst_timer = 0.0 - - print("所有粒子已清除") - - def pause(self, paused=True): - """ - 暂停或恢复粒子系统 - - Args: - paused (bool): 是否暂停 - """ - self.paused = paused - - def set_time_scale(self, scale): - """ - 设置时间缩放 - - Args: - scale (float): 时间缩放因子 - """ - self.time_scale = max(0.0, scale) - - def add_death_callback(self, callback): - """ - 添加粒子死亡回调 - - Args: - callback (function): 回调函数 - """ - if callback not in self.death_callbacks: - self.death_callbacks.append(callback) - - def remove_death_callback(self, callback): - """ - 移除粒子死亡回调 - - Args: - callback (function): 回调函数 - """ - if callback in self.death_callbacks: - self.death_callbacks.remove(callback) - - def add_collision_callback(self, callback): - """ - 添加粒子碰撞回调 - - Args: - callback (function): 回调函数 - """ - if callback not in self.collision_callbacks: - self.collision_callbacks.append(callback) - - def remove_collision_callback(self, callback): - """ - 移除粒子碰撞回调 - - Args: - callback (function): 回调函数 - """ - if callback in self.collision_callbacks: - self.collision_callbacks.remove(callback) - - def create_explosion(self, position, particle_count=50, explosion_force=100.0, - particle_radius=None, particle_lifetime=None, explosion_radius=5.0): - """ - 创建爆炸效果 - - Args: - position (Point3): 爆炸位置 - particle_count (int): 粒子数量 - explosion_force (float): 爆炸力 - particle_radius (float): 粒子半径 - particle_lifetime (float): 粒子生命周期 - explosion_radius (float): 爆炸半径 - """ - if particle_radius is None: - particle_radius = self.particle_radius - if particle_lifetime is None: - particle_lifetime = self.particle_lifetime - - for i in range(particle_count): - # 随机方向 - direction = Vec3( - random.uniform(-1, 1), - random.uniform(-1, 1), - random.uniform(0, 1) - ) - direction.normalize() - - # 粒子速度 - velocity = direction * random.uniform(explosion_force * 0.5, explosion_force) - - # 粒子位置(在爆炸半径内) - offset = direction * random.uniform(0, explosion_radius) - particle_position = position + offset - - # 创建粒子 - particle_shape = BulletSphereShape(particle_radius * random.uniform(0.5, 2.0)) - particle_body = BulletRigidBodyNode(f'ExplosionParticle_{self.emitted_count + i}') - particle_body.addShape(particle_shape) - particle_body.setMass(self.particle_mass * 0.5) # 爆炸粒子质量较小 - - # 设置碰撞属性 - if self.collision_enabled: - particle_body.setCollideFilterGroup(self.collision_group) - particle_body.setCollideFilterMask(self.collision_mask) - - # 创建粒子节点 - from panda3d.core import NodePath - dummy_np = NodePath("dummy") - particle_node = dummy_np.attachNewNode(particle_body) - particle_node.setPos(particle_position) - - # 添加到物理世界 - self.physics_world.bullet_world.attachRigidBody(particle_body) - - # 应用初始力 - particle_body.setLinearVelocity(velocity) - - # 创建粒子信息 - particle_info = { - 'node': particle_node, - 'body': particle_body, - 'shape': particle_shape, - 'position': particle_position, - 'velocity': velocity, - 'lifetime': particle_lifetime * random.uniform(0.5, 1.5), - 'age': 0.0, - 'color': Vec3(1, random.uniform(0.5, 1), 0), # 橙色/红色 - 'size': particle_radius, - 'mass': self.particle_mass * 0.5, - 'radius': particle_radius, - 'type': self.TYPE_EXPLOSION, - 'material': self.particle_material, - 'creation_time': time.time(), - 'rotation': random.uniform(0, 360), - 'rotation_speed': random.uniform(-10, 10), - 'trail': [] if self.particle_trail_enabled else None, - 'collisions': 0, - 'sleeping': False, - 'culled': False - } - - self.particles.append(particle_info) - self.active_particles.append(particle_info) - self.emitted_count += 1 - - print(f"创建爆炸效果,位置: {position}, 粒子数: {particle_count}") - - def create_fire_effect(self, position, particle_count=30, rise_speed=5.0, - particle_radius=None, particle_lifetime=None): - """ - 创建火焰效果 - - Args: - position (Point3): 火焰位置 - particle_count (int): 粒子数量 - rise_speed (float): 上升速度 - particle_radius (float): 粒子半径 - particle_lifetime (float): 粒子生命周期 - """ - if particle_radius is None: - particle_radius = self.particle_radius - if particle_lifetime is None: - particle_lifetime = self.particle_lifetime * 2.0 # 火焰粒子寿命更长 - - for i in range(particle_count): - # 粒子位置(在火焰底部附近) - offset = Vec3( - random.uniform(-0.5, 0.5), - random.uniform(-0.5, 0.5), - random.uniform(0, 1) - ) - particle_position = position + offset - - # 粒子速度(向上) - velocity = Vec3( - random.uniform(-0.5, 0.5), - random.uniform(-0.5, 0.5), - random.uniform(rise_speed * 0.8, rise_speed * 1.2) - ) - - # 创建粒子 - particle_shape = BulletSphereShape(particle_radius * random.uniform(0.8, 1.5)) - particle_body = BulletRigidBodyNode(f'FireParticle_{self.emitted_count + i}') - particle_body.addShape(particle_shape) - particle_body.setMass(self.particle_mass * 0.1) # 火焰粒子质量很小 - - # 设置物理属性(火焰粒子受重力影响较小) - particle_body.setFriction(0.1) - particle_body.setRestitution(0.1) - - # 创建粒子节点 - from panda3d.core import NodePath - dummy_np = NodePath("dummy") - particle_node = dummy_np.attachNewNode(particle_body) - particle_node.setPos(particle_position) - - # 添加到物理世界 - self.physics_world.bullet_world.attachRigidBody(particle_body) - - # 应用初始速度 - particle_body.setLinearVelocity(velocity) - - # 创建粒子信息 - particle_info = { - 'node': particle_node, - 'body': particle_body, - 'shape': particle_shape, - 'position': particle_position, - 'velocity': velocity, - 'lifetime': particle_lifetime * random.uniform(0.8, 1.2), - 'age': 0.0, - 'color': Vec3(1, random.uniform(0.3, 0.7), 0), # 黄色/橙色 - 'size': particle_radius, - 'mass': self.particle_mass * 0.1, - 'radius': particle_radius, - 'type': self.TYPE_FIRE, - 'material': self.particle_material, - 'creation_time': time.time(), - 'rotation': random.uniform(0, 360), - 'rotation_speed': random.uniform(-5, 5), - 'trail': [] if self.particle_trail_enabled else None, - 'collisions': 0, - 'sleeping': False, - 'culled': False - } - - self.particles.append(particle_info) - self.active_particles.append(particle_info) - self.emitted_count += 1 - - print(f"创建火焰效果,位置: {position}, 粒子数: {particle_count}") - - def destroy(self): - """ - 销毁粒子系统 - """ - self.clear_particles() - self.death_callbacks.clear() - self.collision_callbacks.clear() +""" +粒子系统模块 +负责物理粒子的创建和管理 +""" + +from panda3d.core import Vec3, Point3, LVector3f, ClockObject +from panda3d.bullet import BulletRigidBodyNode, BulletSphereShape, BulletBoxShape +import random +import math +import time +from collections import deque + + +class ParticleSystem: + """ + 粒子系统类 + 管理大量物理粒子的创建、更新和销毁 + """ + + # 粒子类型常量 + TYPE_DEFAULT = 'default' + TYPE_SMOKE = 'smoke' + TYPE_FIRE = 'fire' + TYPE_WATER = 'water' + TYPE_DEBRIS = 'debris' + TYPE_EXPLOSION = 'explosion' + TYPE_SPARK = 'spark' + TYPE_BUBBLE = 'bubble' + TYPE_LEAF = 'leaf' + TYPE_SNOW = 'snow' + TYPE_RAIN = 'rain' + + def __init__(self, physics_world, max_particles=1000, particle_type=TYPE_DEFAULT): + """ + 初始化粒子系统 + + Args: + physics_world (PhysicsWorld): 物理世界对象 + max_particles (int): 最大粒子数 + particle_type (str): 粒子类型 + """ + self.physics_world = physics_world + self.max_particles = max_particles + self.particle_type = particle_type + + # 粒子容器 + self.particles = [] + self.active_particles = [] + self.inactive_particles = deque() # 对象池 + + # 粒子发射器参数 + self.emission_rate = 100 # 每秒发射粒子数 + self.emission_accumulator = 0.0 + self.emission_burst_count = 0 # 爆发粒子数 + self.emission_burst_timer = 0.0 + self.emission_burst_interval = 0.0 # 爆发间隔 + + # 粒子默认属性 + self.particle_mass = 0.1 + self.particle_radius = 0.1 + self.particle_size = Vec3(0.1, 0.1, 0.1) # 非球形粒子尺寸 + self.particle_lifetime = 5.0 + self.particle_color = Vec3(1, 1, 1) + self.particle_shape_type = 'sphere' # sphere, box, capsule + + # 发射器属性 + self.emitter_position = Point3(0, 0, 0) + self.emitter_velocity = Vec3(0, 0, 0) + self.emitter_spread = 0.1 + self.emitter_radius = 0.0 + self.emitter_shape = 'point' # point, circle, box, sphere + self.emitter_dimensions = Vec3(1, 1, 1) # 发射器尺寸 + + # 物理属性 + self.gravity_scale = 1.0 + self.air_resistance = 0.01 + self.bounce_factor = 0.5 + self.friction = 0.1 + self.restitution = 0.3 + self.linear_damping = 0.01 + self.angular_damping = 0.01 + + # 碰撞属性 + self.collision_enabled = True + self.collision_group = 1 + self.collision_mask = 0xFFFFFFFF + self.collision_response = True # 是否响应碰撞 + + # 高级粒子属性 + self.particle_trail_enabled = False + self.particle_trail_length = 10 + self.particle_fade_enabled = True + self.particle_size_variation = 0.2 + self.particle_velocity_variation = 0.3 + self.particle_rotation_enabled = False + self.particle_rotation_speed = 0.0 + self.particle_rotation_variation = 0.5 + + # 环境影响 + self.wind_enabled = False + self.wind_vector = Vec3(0, 0, 0) + self.wind_turbulence = 0.0 + self.temperature_effect = 0.0 # 温度影响(浮力等) + + # 特殊效果 + self.vortex_enabled = False + self.vortex_center = Point3(0, 0, 0) + self.vortex_strength = 0.0 + self.vortex_axis = Vec3(0, 0, 1) + + # 粒子生命周期行为 + self.size_over_lifetime = None # [(time, scale), ...] + self.color_over_lifetime = None # [(time, color), ...] + self.force_over_lifetime = None # [(time, force), ...] + + # 粒子子发射器 + self.sub_emitter_enabled = False + self.sub_emitter_rate = 0.0 + self.sub_emitter_count = 1 + self.sub_emitter_lifetime = 1.0 + + # 性能优化 + self.sleeping_threshold = 0.1 # 速度低于此值时休眠 + self.culling_distance = 100.0 # 距离摄像机超过此距离时剔除 + self.batch_processing = True # 批量处理 + + # 统计信息 + self.emitted_count = 0 + self.recycled_count = 0 + self.collision_count = 0 + self.max_active_particles = 0 + + # 时间控制 + self.time_scale = 1.0 + self.paused = False + + # 粒子材质 + self.particle_material = None + + # 粒子纹理(如果需要渲染) + self.particle_texture = None + self.particle_blend_mode = 'alpha' # alpha, additive, multiply + + # 粒子死亡回调 + self.death_callbacks = [] + + # 粒子碰撞回调 + self.collision_callbacks = [] + + print(f"粒子系统初始化完成,最大粒子数: {max_particles},类型: {particle_type}") + + def set_emitter_properties(self, position=None, velocity=None, spread=None, radius=None, + shape=None, dimensions=None): + """ + 设置发射器属性 + + Args: + position (Point3): 发射器位置 + velocity (Vec3): 发射器基础速度 + spread (float): 发射扩散角度 + radius (float): 发射器半径 + shape (str): 发射器形状 + dimensions (Vec3): 发射器尺寸 + """ + if position is not None: + self.emitter_position = position + if velocity is not None: + self.emitter_velocity = velocity + if spread is not None: + self.emitter_spread = spread + if radius is not None: + self.emitter_radius = radius + if shape is not None: + self.emitter_shape = shape + if dimensions is not None: + self.emitter_dimensions = dimensions + + def set_particle_properties(self, mass=None, radius=None, size=None, lifetime=None, + color=None, shape_type=None): + """ + 设置粒子属性 + + Args: + mass (float): 粒子质量 + radius (float): 粒子半径(球形) + size (Vec3): 粒子尺寸(非球形) + lifetime (float): 粒子生命周期 + color (Vec3): 粒子颜色 + shape_type (str): 粒子形状类型 + """ + if mass is not None: + self.particle_mass = mass + if radius is not None: + self.particle_radius = radius + if size is not None: + self.particle_size = size + if lifetime is not None: + self.particle_lifetime = lifetime + if color is not None: + self.particle_color = color + if shape_type is not None: + self.particle_shape_type = shape_type + + def set_physics_properties(self, gravity_scale=None, air_resistance=None, + bounce_factor=None, friction=None, restitution=None, + linear_damping=None, angular_damping=None): + """ + 设置物理属性 + + Args: + gravity_scale (float): 重力缩放 + air_resistance (float): 空气阻力 + bounce_factor (float): 弹跳因子 + friction (float): 摩擦系数 + restitution (float): 恢复系数 + linear_damping (float): 线性阻尼 + angular_damping (float): 角阻尼 + """ + if gravity_scale is not None: + self.gravity_scale = gravity_scale + if air_resistance is not None: + self.air_resistance = air_resistance + if bounce_factor is not None: + self.bounce_factor = bounce_factor + if friction is not None: + self.friction = friction + if restitution is not None: + self.restitution = restitution + if linear_damping is not None: + self.linear_damping = linear_damping + if angular_damping is not None: + self.angular_damping = angular_damping + + def set_collision_properties(self, enabled=None, group=None, mask=None, response=None): + """ + 设置碰撞属性 + + Args: + enabled (bool): 是否启用碰撞 + group (int): 碰撞组 + mask (int): 碰撞掩码 + response (bool): 是否响应碰撞 + """ + if enabled is not None: + self.collision_enabled = enabled + if group is not None: + self.collision_group = group + if mask is not None: + self.collision_mask = mask + if response is not None: + self.collision_response = response + + def set_environmental_effects(self, wind_enabled=None, wind_vector=None, + wind_turbulence=None, temperature_effect=None): + """ + 设置环境影响 + + Args: + wind_enabled (bool): 是否启用风 + wind_vector (Vec3): 风向量 + wind_turbulence (float): 风湍流 + temperature_effect (float): 温度影响 + """ + if wind_enabled is not None: + self.wind_enabled = wind_enabled + if wind_vector is not None: + self.wind_vector = wind_vector + if wind_turbulence is not None: + self.wind_turbulence = wind_turbulence + if temperature_effect is not None: + self.temperature_effect = temperature_effect + + def set_special_effects(self, vortex_enabled=None, vortex_center=None, + vortex_strength=None, vortex_axis=None): + """ + 设置特殊效果 + + Args: + vortex_enabled (bool): 是否启用涡流 + vortex_center (Point3): 涡流中心 + vortex_strength (float): 涡流强度 + vortex_axis (Vec3): 涡流轴向 + """ + if vortex_enabled is not None: + self.vortex_enabled = vortex_enabled + if vortex_center is not None: + self.vortex_center = vortex_center + if vortex_strength is not None: + self.vortex_strength = vortex_strength + if vortex_axis is not None: + self.vortex_axis = vortex_axis + + def set_lifetime_behaviors(self, size_curve=None, color_curve=None, force_curve=None): + """ + 设置粒子生命周期行为 + + Args: + size_curve (list): 尺寸随时间变化曲线 [(time, scale), ...] + color_curve (list): 颜色随时间变化曲线 [(time, color), ...] + force_curve (list): 力随时间变化曲线 [(time, force), ...] + """ + if size_curve is not None: + self.size_over_lifetime = size_curve + if color_curve is not None: + self.color_over_lifetime = color_curve + if force_curve is not None: + self.force_over_lifetime = force_curve + + def set_sub_emitter(self, enabled=None, rate=None, count=None, lifetime=None): + """ + 设置粒子子发射器 + + Args: + enabled (bool): 是否启用子发射器 + rate (float): 子发射器速率 + count (int): 每次发射的子粒子数 + lifetime (float): 子粒子生命周期 + """ + if enabled is not None: + self.sub_emitter_enabled = enabled + if rate is not None: + self.sub_emitter_rate = rate + if count is not None: + self.sub_emitter_count = count + if lifetime is not None: + self.sub_emitter_lifetime = lifetime + + def set_performance_options(self, sleeping_threshold=None, culling_distance=None, + batch_processing=None): + """ + 设置性能优化选项 + + Args: + sleeping_threshold (float): 休眠阈值 + culling_distance (float): 剔除距离 + batch_processing (bool): 批量处理 + """ + if sleeping_threshold is not None: + self.sleeping_threshold = sleeping_threshold + if culling_distance is not None: + self.culling_distance = culling_distance + if batch_processing is not None: + self.batch_processing = batch_processing + + def set_emission_rate(self, rate): + """ + 设置发射速率 + + Args: + rate (float): 每秒发射粒子数 + """ + self.emission_rate = max(0, rate) + + def set_emission_burst(self, count, interval=0.0): + """ + 设置爆发发射 + + Args: + count (int): 每次爆发的粒子数 + interval (float): 爆发间隔(秒) + """ + self.emission_burst_count = count + self.emission_burst_interval = interval + + def emit_particle(self, position=None, velocity=None, lifetime=None, color=None): + """ + 发射单个粒子 + + Args: + position (Point3): 粒子位置 + velocity (Vec3): 粒子速度 + lifetime (float): 粒子生命周期 + color (Vec3): 粒子颜色 + + Returns: + dict: 粒子信息字典 + """ + # 检查是否达到最大粒子数 + if len(self.active_particles) >= self.max_particles: + return None + + # 从对象池获取粒子或创建新粒子 + if self.inactive_particles: + particle_info = self.inactive_particles.popleft() + self.recycled_count += 1 + else: + particle_info = self._create_new_particle() + + # 设置粒子属性 + if position is None: + position = self._calculate_emission_position() + if velocity is None: + velocity = self._calculate_emission_velocity() + if lifetime is None: + lifetime = self.particle_lifetime * random.uniform( + 1.0 - self.particle_lifetime * 0.2, + 1.0 + self.particle_lifetime * 0.2 + ) + if color is None: + color = self.particle_color + + # 初始化粒子状态 + particle_info['position'] = position + particle_info['velocity'] = velocity + particle_info['lifetime'] = lifetime + particle_info['age'] = 0.0 + particle_info['color'] = color + particle_info['initial_size'] = particle_info['size'] + particle_info['initial_color'] = color + particle_info['initial_velocity'] = velocity + particle_info['rotation'] = random.uniform(0, 360) if self.particle_rotation_enabled else 0.0 + particle_info['rotation_speed'] = self.particle_rotation_speed * random.uniform( + 1.0 - self.particle_rotation_variation, + 1.0 + self.particle_rotation_variation + ) + particle_info['trail'] = [] if self.particle_trail_enabled else None + particle_info['collisions'] = 0 + particle_info['sleeping'] = False + particle_info['culled'] = False + + # 添加到物理世界 + self.physics_world.bullet_world.attachRigidBody(particle_info['body']) + + # 设置物理属性 + particle_info['body'].setLinearVelocity(velocity) + particle_info['body'].setFriction(self.friction) + particle_info['body'].setRestitution(self.restitution) + particle_info['body'].setLinearDamping(self.linear_damping) + particle_info['body'].setAngularDamping(self.angular_damping) + + # 设置碰撞属性 + if self.collision_enabled: + particle_info['body'].setCollideFilterGroup(self.collision_group) + particle_info['body'].setCollideFilterMask(self.collision_mask) + else: + particle_info['body'].setCollideFilterGroup(0) + particle_info['body'].setCollideFilterMask(0) + + # 设置位置 + particle_info['node'].setPos(position) + + # 添加到活动粒子列表 + self.active_particles.append(particle_info) + self.emitted_count += 1 + + # 更新统计 + self.max_active_particles = max(self.max_active_particles, len(self.active_particles)) + + return particle_info + + def _create_new_particle(self): + """ + 创建新粒子 + + Returns: + dict: 粒子信息字典 + """ + # 创建粒子形状 + if self.particle_shape_type == 'sphere': + size = self.particle_radius * random.uniform( + 1.0 - self.particle_size_variation, + 1.0 + self.particle_size_variation + ) + particle_shape = BulletSphereShape(size) + elif self.particle_shape_type == 'box': + size = self.particle_size * random.uniform( + 1.0 - self.particle_size_variation, + 1.0 + self.particle_size_variation + ) + particle_shape = BulletBoxShape(size * 0.5) + else: + # 默认使用球形 + size = self.particle_radius * random.uniform( + 1.0 - self.particle_size_variation, + 1.0 + self.particle_size_variation + ) + particle_shape = BulletSphereShape(size) + + # 创建粒子刚体 + particle_body = BulletRigidBodyNode(f'Particle_{len(self.particles)}') + particle_body.addShape(particle_shape) + particle_body.setMass(self.particle_mass) + + # 创建粒子节点 + from panda3d.core import NodePath + dummy_np = NodePath("dummy") + particle_node = dummy_np.attachNewNode(particle_body) + + # 创建粒子信息 + particle_info = { + 'node': particle_node, + 'body': particle_body, + 'shape': particle_shape, + 'position': Point3(0, 0, 0), + 'velocity': Vec3(0, 0, 0), + 'lifetime': self.particle_lifetime, + 'age': 0.0, + 'color': self.particle_color, + 'size': size, + 'mass': self.particle_mass, + 'radius': size if self.particle_shape_type == 'sphere' else None, + 'type': self.particle_type, + 'material': self.particle_material, + 'creation_time': time.time(), + } + + self.particles.append(particle_info) + return particle_info + + def _calculate_emission_position(self): + """ + 计算发射位置 + + Returns: + Point3: 发射位置 + """ + if self.emitter_shape == 'point': + return self.emitter_position + elif self.emitter_shape == 'circle': + angle = random.uniform(0, 2 * math.pi) + distance = random.uniform(0, self.emitter_radius) + offset = Point3( + math.cos(angle) * distance, + math.sin(angle) * distance, + 0 + ) + return self.emitter_position + offset + elif self.emitter_shape == 'box': + offset = Point3( + random.uniform(-self.emitter_dimensions.getX() * 0.5, self.emitter_dimensions.getX() * 0.5), + random.uniform(-self.emitter_dimensions.getY() * 0.5, self.emitter_dimensions.getY() * 0.5), + random.uniform(-self.emitter_dimensions.getZ() * 0.5, self.emitter_dimensions.getZ() * 0.5) + ) + return self.emitter_position + offset + elif self.emitter_shape == 'sphere': + # 球面均匀分布 + theta = random.uniform(0, 2 * math.pi) + phi = math.acos(2 * random.uniform(0, 1) - 1) + radius = self.emitter_radius + offset = Point3( + radius * math.sin(phi) * math.cos(theta), + radius * math.sin(phi) * math.sin(theta), + radius * math.cos(phi) + ) + return self.emitter_position + offset + else: + return self.emitter_position + + def _calculate_emission_velocity(self): + """ + 计算发射速度 + + Returns: + Vec3: 发射速度 + """ + # 基础速度 + base_velocity = self.emitter_velocity.copy() + + # 添加随机扩散 + if self.emitter_spread > 0: + spread_angle = random.uniform(0, self.emitter_spread) + spread_direction = random.uniform(0, 2 * math.pi) + spread_offset = Vec3( + math.cos(spread_direction) * math.sin(spread_angle), + math.sin(spread_direction) * math.sin(spread_angle), + math.cos(spread_angle) + ) + base_velocity += spread_offset * base_velocity.length() + + # 添加速度变化 + if self.particle_velocity_variation > 0: + variation = random.uniform( + 1.0 - self.particle_velocity_variation, + 1.0 + self.particle_velocity_variation + ) + base_velocity *= variation + + # 添加随机速度分量 + random_velocity = Vec3( + random.uniform(-1, 1), + random.uniform(-1, 1), + random.uniform(-1, 1) + ) * base_velocity.length() * 0.1 + + return base_velocity + random_velocity + + def update(self, dt): + """ + 更新粒子系统 + + Args: + dt (float): 时间增量 + """ + if self.paused: + return + + # 应用时间缩放 + dt *= self.time_scale + + # 更新现有粒子 + for particle in self.active_particles[:]: # 使用副本避免迭代时修改 + self._update_particle(particle, dt) + + # 发射新粒子 + self._emit_particles(dt) + + # 处理爆发发射 + self._handle_burst_emission(dt) + + def _update_particle(self, particle, dt): + """ + 更新单个粒子 + + Args: + particle (dict): 粒子信息 + dt (float): 时间增量 + """ + # 更新粒子年龄 + particle['age'] += dt + + # 检查生命周期 + if particle['age'] >= particle['lifetime']: + self._destroy_particle(particle) + return + + # 检查是否休眠 + velocity = particle['body'].getLinearVelocity() + if velocity.length() < self.sleeping_threshold: + particle['sleeping'] = True + else: + particle['sleeping'] = False + + # 如果休眠,跳过物理更新 + if particle['sleeping']: + return + + # 应用重力 + if self.gravity_scale > 0: + gravity_force = Vec3(0, 0, -9.81) * self.gravity_scale * particle['mass'] + particle['body'].applyCentralForce(gravity_force) + + # 应用空气阻力 + if self.air_resistance > 0: + drag_force = -velocity * self.air_resistance + particle['body'].applyCentralForce(drag_force) + + # 应用风力 + if self.wind_enabled and self.wind_vector.length() > 0: + wind_force = self.wind_vector * particle['mass'] + if self.wind_turbulence > 0: + turbulence = Vec3( + random.uniform(-1, 1), + random.uniform(-1, 1), + random.uniform(-1, 1) + ) * self.wind_turbulence + wind_force += turbulence + particle['body'].applyCentralForce(wind_force) + + # 应用涡流力 + if self.vortex_enabled and self.vortex_strength > 0: + self._apply_vortex_force(particle) + + # 应用温度影响(浮力等) + if self.temperature_effect != 0: + # 简化的浮力计算 + buoyancy_force = Vec3(0, 0, 1) * self.temperature_effect * particle['mass'] + particle['body'].applyCentralForce(buoyancy_force) + + # 应用生命周期力 + if self.force_over_lifetime: + self._apply_lifetime_force(particle) + + # 更新轨迹 + if self.particle_trail_enabled and particle['trail'] is not None: + position = particle['node'].getPos() + particle['trail'].append(position) + if len(particle['trail']) > self.particle_trail_length: + particle['trail'].pop(0) + + # 更新旋转 + if self.particle_rotation_enabled: + particle['rotation'] += particle['rotation_speed'] * dt + + # 更新尺寸 + if self.size_over_lifetime: + self._update_particle_size(particle) + + # 更新颜色 + if self.particle_fade_enabled or self.color_over_lifetime: + self._update_particle_color(particle) + + # 处理子发射器 + if self.sub_emitter_enabled and self.sub_emitter_rate > 0: + self._handle_sub_emitter(particle, dt) + + def _apply_vortex_force(self, particle): + """ + 应用涡流力 + + Args: + particle (dict): 粒子信息 + """ + position = particle['node'].getPos() + to_center = self.vortex_center - position + distance = to_center.length() + + if distance > 0: + # 径向力(向中心吸引) + radial_force = to_center.normalized() * self.vortex_strength / (distance + 1.0) + + # 切向力(绕轴旋转) + tangent_dir = self.vortex_axis.cross(to_center).normalized() + tangent_force = tangent_dir * self.vortex_strength + + total_force = (radial_force + tangent_force) * particle['mass'] + particle['body'].applyCentralForce(total_force) + + def _apply_lifetime_force(self, particle): + """ + 应用生命周期力 + + Args: + particle (dict): 粒子信息 + """ + normalized_age = particle['age'] / particle['lifetime'] + + # 在力曲线上插值 + for i in range(len(self.force_over_lifetime) - 1): + time1, force1 = self.force_over_lifetime[i] + time2, force2 = self.force_over_lifetime[i + 1] + + if time1 <= normalized_age <= time2: + t = (normalized_age - time1) / (time2 - time1) if time2 != time1 else 0 + interpolated_force = force1 + (force2 - force1) * t + particle['body'].applyCentralForce(interpolated_force * particle['mass']) + break + + def _update_particle_size(self, particle): + """ + 更新粒子尺寸 + + Args: + particle (dict): 粒子信息 + """ + normalized_age = particle['age'] / particle['lifetime'] + + # 在尺寸曲线上插值 + for i in range(len(self.size_over_lifetime) - 1): + time1, scale1 = self.size_over_lifetime[i] + time2, scale2 = self.size_over_lifetime[i + 1] + + if time1 <= normalized_age <= time2: + t = (normalized_age - time1) / (time2 - time1) if time2 != time1 else 0 + interpolated_scale = scale1 + (scale2 - scale1) * t + + # 更新粒子尺寸(这个功能需要与渲染系统集成) + # 这里只是更新记录,实际渲染需要在渲染系统中实现 + if isinstance(particle['size'], (int, float)): + particle['size'] = particle['initial_size'] * interpolated_scale + break + + def _update_particle_color(self, particle): + """ + 更新粒子颜色 + + Args: + particle (dict): 粒子信息 + """ + normalized_age = particle['age'] / particle['lifetime'] + + # 淡出效果 + if self.particle_fade_enabled: + fade_factor = 1.0 - (normalized_age * normalized_age) # 二次淡出 + particle['color'] = particle['initial_color'] * fade_factor + + # 生命周期颜色变化 + if self.color_over_lifetime: + for i in range(len(self.color_over_lifetime) - 1): + time1, color1 = self.color_over_lifetime[i] + time2, color2 = self.color_over_lifetime[i + 1] + + if time1 <= normalized_age <= time2: + t = (normalized_age - time1) / (time2 - time1) if time2 != time1 else 0 + interpolated_color = color1 + (color2 - color1) * t + if self.particle_fade_enabled: + # 结合淡出效果 + particle['color'] = interpolated_color * (1.0 - normalized_age) + else: + particle['color'] = interpolated_color + break + + def _handle_sub_emitter(self, particle, dt): + """ + 处理子发射器 + + Args: + particle (dict): 粒子信息 + dt (float): 时间增量 + """ + # 简化的子发射器实现 + pass # 需要更复杂的实现 + + def _emit_particles(self, dt): + """ + 发射粒子 + + Args: + dt (float): 时间增量 + """ + # 基于发射速率发射粒子 + self.emission_accumulator += dt * self.emission_rate + while self.emission_accumulator >= 1.0 and len(self.active_particles) < self.max_particles: + self.emit_particle() + self.emission_accumulator -= 1.0 + + def _handle_burst_emission(self, dt): + """ + 处理爆发发射 + + Args: + dt (float): 时间增量 + """ + if self.emission_burst_count > 0 and self.emission_burst_interval > 0: + self.emission_burst_timer += dt + if self.emission_burst_timer >= self.emission_burst_interval: + for _ in range(min(self.emission_burst_count, self.max_particles - len(self.active_particles))): + self.emit_particle() + self.emission_burst_timer = 0.0 + + def _destroy_particle(self, particle): + """ + 销毁粒子 + + Args: + particle (dict): 粒子信息 + """ + if particle in self.active_particles: + # 从物理世界移除 + self.physics_world.bullet_world.removeRigidBody(particle['body']) + + # 移除节点 + if particle['node'] and not particle['node'].isEmpty(): + particle['node'].removeNode() + + # 从活动列表移除 + self.active_particles.remove(particle) + + # 添加到对象池 + self.inactive_particles.append(particle) + + # 调用死亡回调 + for callback in self.death_callbacks: + try: + callback(particle) + except Exception as e: + print(f"粒子死亡回调错误: {e}") + + def get_particle_count(self): + """ + 获取活动粒子数量 + + Returns: + int: 活动粒子数量 + """ + return len(self.active_particles) + + def get_total_emitted(self): + """ + 获取总发射粒子数 + + Returns: + int: 总发射粒子数 + """ + return self.emitted_count + + def get_statistics(self): + """ + 获取粒子系统统计信息 + + Returns: + dict: 统计信息 + """ + return { + 'active_particles': len(self.active_particles), + 'total_emitted': self.emitted_count, + 'recycled_particles': self.recycled_count, + 'collision_count': self.collision_count, + 'max_active_particles': self.max_active_particles, + 'emission_rate': self.emission_rate, + 'particle_type': self.particle_type, + 'system_age': time.time() - (self.particles[0]['creation_time'] if self.particles else time.time()), + } + + def clear_particles(self): + """ + 清除所有粒子 + """ + # 销毁所有活动粒子 + for particle in self.active_particles[:]: + self._destroy_particle(particle) + + # 清空对象池 + self.inactive_particles.clear() + + # 清空粒子列表 + self.particles.clear() + self.active_particles.clear() + + # 重置统计信息 + self.emitted_count = 0 + self.recycled_count = 0 + self.collision_count = 0 + self.max_active_particles = 0 + + # 重置发射器 + self.emission_accumulator = 0.0 + self.emission_burst_timer = 0.0 + + print("所有粒子已清除") + + def pause(self, paused=True): + """ + 暂停或恢复粒子系统 + + Args: + paused (bool): 是否暂停 + """ + self.paused = paused + + def set_time_scale(self, scale): + """ + 设置时间缩放 + + Args: + scale (float): 时间缩放因子 + """ + self.time_scale = max(0.0, scale) + + def add_death_callback(self, callback): + """ + 添加粒子死亡回调 + + Args: + callback (function): 回调函数 + """ + if callback not in self.death_callbacks: + self.death_callbacks.append(callback) + + def remove_death_callback(self, callback): + """ + 移除粒子死亡回调 + + Args: + callback (function): 回调函数 + """ + if callback in self.death_callbacks: + self.death_callbacks.remove(callback) + + def add_collision_callback(self, callback): + """ + 添加粒子碰撞回调 + + Args: + callback (function): 回调函数 + """ + if callback not in self.collision_callbacks: + self.collision_callbacks.append(callback) + + def remove_collision_callback(self, callback): + """ + 移除粒子碰撞回调 + + Args: + callback (function): 回调函数 + """ + if callback in self.collision_callbacks: + self.collision_callbacks.remove(callback) + + def create_explosion(self, position, particle_count=50, explosion_force=100.0, + particle_radius=None, particle_lifetime=None, explosion_radius=5.0): + """ + 创建爆炸效果 + + Args: + position (Point3): 爆炸位置 + particle_count (int): 粒子数量 + explosion_force (float): 爆炸力 + particle_radius (float): 粒子半径 + particle_lifetime (float): 粒子生命周期 + explosion_radius (float): 爆炸半径 + """ + if particle_radius is None: + particle_radius = self.particle_radius + if particle_lifetime is None: + particle_lifetime = self.particle_lifetime + + for i in range(particle_count): + # 随机方向 + direction = Vec3( + random.uniform(-1, 1), + random.uniform(-1, 1), + random.uniform(0, 1) + ) + direction.normalize() + + # 粒子速度 + velocity = direction * random.uniform(explosion_force * 0.5, explosion_force) + + # 粒子位置(在爆炸半径内) + offset = direction * random.uniform(0, explosion_radius) + particle_position = position + offset + + # 创建粒子 + particle_shape = BulletSphereShape(particle_radius * random.uniform(0.5, 2.0)) + particle_body = BulletRigidBodyNode(f'ExplosionParticle_{self.emitted_count + i}') + particle_body.addShape(particle_shape) + particle_body.setMass(self.particle_mass * 0.5) # 爆炸粒子质量较小 + + # 设置碰撞属性 + if self.collision_enabled: + particle_body.setCollideFilterGroup(self.collision_group) + particle_body.setCollideFilterMask(self.collision_mask) + + # 创建粒子节点 + from panda3d.core import NodePath + dummy_np = NodePath("dummy") + particle_node = dummy_np.attachNewNode(particle_body) + particle_node.setPos(particle_position) + + # 添加到物理世界 + self.physics_world.bullet_world.attachRigidBody(particle_body) + + # 应用初始力 + particle_body.setLinearVelocity(velocity) + + # 创建粒子信息 + particle_info = { + 'node': particle_node, + 'body': particle_body, + 'shape': particle_shape, + 'position': particle_position, + 'velocity': velocity, + 'lifetime': particle_lifetime * random.uniform(0.5, 1.5), + 'age': 0.0, + 'color': Vec3(1, random.uniform(0.5, 1), 0), # 橙色/红色 + 'size': particle_radius, + 'mass': self.particle_mass * 0.5, + 'radius': particle_radius, + 'type': self.TYPE_EXPLOSION, + 'material': self.particle_material, + 'creation_time': time.time(), + 'rotation': random.uniform(0, 360), + 'rotation_speed': random.uniform(-10, 10), + 'trail': [] if self.particle_trail_enabled else None, + 'collisions': 0, + 'sleeping': False, + 'culled': False + } + + self.particles.append(particle_info) + self.active_particles.append(particle_info) + self.emitted_count += 1 + + print(f"创建爆炸效果,位置: {position}, 粒子数: {particle_count}") + + def create_fire_effect(self, position, particle_count=30, rise_speed=5.0, + particle_radius=None, particle_lifetime=None): + """ + 创建火焰效果 + + Args: + position (Point3): 火焰位置 + particle_count (int): 粒子数量 + rise_speed (float): 上升速度 + particle_radius (float): 粒子半径 + particle_lifetime (float): 粒子生命周期 + """ + if particle_radius is None: + particle_radius = self.particle_radius + if particle_lifetime is None: + particle_lifetime = self.particle_lifetime * 2.0 # 火焰粒子寿命更长 + + for i in range(particle_count): + # 粒子位置(在火焰底部附近) + offset = Vec3( + random.uniform(-0.5, 0.5), + random.uniform(-0.5, 0.5), + random.uniform(0, 1) + ) + particle_position = position + offset + + # 粒子速度(向上) + velocity = Vec3( + random.uniform(-0.5, 0.5), + random.uniform(-0.5, 0.5), + random.uniform(rise_speed * 0.8, rise_speed * 1.2) + ) + + # 创建粒子 + particle_shape = BulletSphereShape(particle_radius * random.uniform(0.8, 1.5)) + particle_body = BulletRigidBodyNode(f'FireParticle_{self.emitted_count + i}') + particle_body.addShape(particle_shape) + particle_body.setMass(self.particle_mass * 0.1) # 火焰粒子质量很小 + + # 设置物理属性(火焰粒子受重力影响较小) + particle_body.setFriction(0.1) + particle_body.setRestitution(0.1) + + # 创建粒子节点 + from panda3d.core import NodePath + dummy_np = NodePath("dummy") + particle_node = dummy_np.attachNewNode(particle_body) + particle_node.setPos(particle_position) + + # 添加到物理世界 + self.physics_world.bullet_world.attachRigidBody(particle_body) + + # 应用初始速度 + particle_body.setLinearVelocity(velocity) + + # 创建粒子信息 + particle_info = { + 'node': particle_node, + 'body': particle_body, + 'shape': particle_shape, + 'position': particle_position, + 'velocity': velocity, + 'lifetime': particle_lifetime * random.uniform(0.8, 1.2), + 'age': 0.0, + 'color': Vec3(1, random.uniform(0.3, 0.7), 0), # 黄色/橙色 + 'size': particle_radius, + 'mass': self.particle_mass * 0.1, + 'radius': particle_radius, + 'type': self.TYPE_FIRE, + 'material': self.particle_material, + 'creation_time': time.time(), + 'rotation': random.uniform(0, 360), + 'rotation_speed': random.uniform(-5, 5), + 'trail': [] if self.particle_trail_enabled else None, + 'collisions': 0, + 'sleeping': False, + 'culled': False + } + + self.particles.append(particle_info) + self.active_particles.append(particle_info) + self.emitted_count += 1 + + print(f"创建火焰效果,位置: {position}, 粒子数: {particle_count}") + + def destroy(self): + """ + 销毁粒子系统 + """ + self.clear_particles() + self.death_callbacks.clear() + self.collision_callbacks.clear() print("粒子系统已销毁") \ No newline at end of file diff --git a/plugins/user/rigid_body_physics/plugin_manager.py b/plugins/user/rigid_body_physics/plugin_manager.py index 423af9d1..faeb2d11 100644 --- a/plugins/user/rigid_body_physics/plugin_manager.py +++ b/plugins/user/rigid_body_physics/plugin_manager.py @@ -1,348 +1,348 @@ -""" -刚体动力学插件主管理器 -整合所有物理功能模块,提供统一的接口 -""" - -from panda3d.core import Vec3, Point3 -from .core.physics_world import PhysicsWorld -from .core.rigid_body import RigidBody -from .core.material import PhysicsMaterial, MATERIALS -from .constraints.constraint_manager import ConstraintManager -from .vehicles.vehicle_system import VehicleSystem -from .particles.particle_system import ParticleSystem -from .debug.debug_tools import PhysicsDebug, PhysicsProfiler -from .utils.physics_utils import PhysicsUtils, PhysicsConfigManager, PhysicsCollisionGroups - - -class RigidBodyPhysicsPlugin: - """ - 刚体动力学插件主管理器 - 提供统一的物理系统接口 - """ - - def __init__(self, world_node=None, config_file=None): - """ - 初始化物理插件 - - Args: - world_node: Panda3D世界节点(用于调试可视化) - config_file (str): 配置文件路径 - """ - # 创建物理世界 - self.physics_world = PhysicsWorld() - - # 创建约束管理器 - self.constraint_manager = ConstraintManager() - - # 配置管理器 - self.config_manager = PhysicsConfigManager(config_file) - - # 调试工具 - self.debug_tools = None - self.profiler = None - if world_node: - self.debug_tools = PhysicsDebug(self.physics_world, world_node) - self.profiler = PhysicsProfiler(self.physics_world) - - # 活动对象列表 - self.vehicles = [] - self.particle_systems = [] - - print("刚体动力学插件初始化完成") - - def create_rigid_body(self, node_path, shape_type='box', mass=1.0, - body_type=RigidBody.DYNAMIC, material=None): - """ - 创建刚体 - - Args: - node_path: Panda3D节点路径 - shape_type (str): 碰撞形状类型 - mass (float): 质量 - body_type (int): 刚体类型 - material (PhysicsMaterial): 物理材质 - - Returns: - RigidBody: 刚体对象 - """ - # 创建刚体 - rigid_body = RigidBody(node_path, shape_type, mass, body_type) - - # 应用材质 - if material: - material.apply_to_rigid_body(rigid_body) - else: - # 使用默认材质 - default_material = PhysicsMaterial('default') - default_material.apply_to_rigid_body(rigid_body) - - # 添加到物理世界 - self.physics_world.add_rigid_body(rigid_body) - - return rigid_body - - def create_constraint(self, constraint_type, body_a, body_b, **kwargs): - """ - 创建约束 - - Args: - constraint_type (str): 约束类型 - body_a (RigidBody): 第一个刚体 - body_b (RigidBody): 第二个刚体 - **kwargs: 约束参数 - - Returns: - 约束对象 - """ - if constraint_type == ConstraintManager.CONSTRAINT_POINT2POINT: - constraint = self.constraint_manager.create_point2point_constraint( - body_a, body_b, **kwargs) - elif constraint_type == ConstraintManager.CONSTRAINT_HINGE: - constraint = self.constraint_manager.create_hinge_constraint( - body_a, body_b, **kwargs) - elif constraint_type == ConstraintManager.CONSTRAINT_SLIDER: - constraint = self.constraint_manager.create_slider_constraint( - body_a, body_b, **kwargs) - elif constraint_type == ConstraintManager.CONSTRAINT_CONETWIST: - constraint = self.constraint_manager.create_cone_twist_constraint( - body_a, body_b, **kwargs) - elif constraint_type == ConstraintManager.CONSTRAINT_6DOF: - constraint = self.constraint_manager.create_6dof_constraint( - body_a, body_b, **kwargs) - else: - raise ValueError(f"未知的约束类型: {constraint_type}") - - # 添加到物理世界 - self.physics_world.add_constraint(constraint) - - return constraint - - def create_vehicle(self, chassis_body, wheel_configurations): - """ - 创建车辆 - - Args: - chassis_body (RigidBody): 底盘刚体 - wheel_configurations (list): 车轮配置列表 - - Returns: - VehicleSystem: 车辆系统对象 - """ - # 创建车辆系统 - vehicle = VehicleSystem(self.physics_world, chassis_body) - - # 添加车轮 - for wheel_config in wheel_configurations: - vehicle.add_wheel(**wheel_config) - - # 保存车辆引用 - self.vehicles.append(vehicle) - - return vehicle - - def create_particle_system(self, max_particles=1000): - """ - 创建粒子系统 - - Args: - max_particles (int): 最大粒子数 - - Returns: - ParticleSystem: 粒子系统对象 - """ - # 创建粒子系统 - particle_system = ParticleSystem(self.physics_world, max_particles) - - # 保存粒子系统引用 - self.particle_systems.append(particle_system) - - # 添加到物理世界 - self.physics_world.add_particle_system(particle_system) - - return particle_system - - def update(self, dt): - """ - 更新物理系统 - - Args: - dt (float): 时间增量 - """ - # 更新物理世界 - self.physics_world.update(dt) - - # 更新粒子系统 - for particle_system in self.particle_systems: - particle_system.update(dt) - - # 更新调试工具 - if self.debug_tools: - self.debug_tools.update_debug_visualization() - - # 更新性能分析器 - if self.profiler: - self.profiler.record_frame() - - def ray_test(self, from_pos, to_pos): - """ - 射线检测 - - Args: - from_pos (Point3): 起点 - to_pos (Point3): 终点 - - Returns: - BulletRayHit: 射线检测结果 - """ - return self.physics_world.ray_test(from_pos, to_pos) - - def get_performance_stats(self): - """ - 获取性能统计 - - Returns: - dict: 性能统计信息 - """ - stats = self.physics_world.get_performance_stats() - - # 添加对象计数 - stats['objects'] = { - 'rigid_bodies': len(self.physics_world.rigid_bodies), - 'constraints': len(self.physics_world.constraints), - 'vehicles': len(self.vehicles), - 'particle_systems': len(self.particle_systems) - } - - return stats - - def enable_debug_visualization(self, enabled=True): - """ - 启用调试可视化 - - Args: - enabled (bool): 是否启用 - """ - if self.debug_tools: - self.debug_tools.enable_debug(enabled) - - def set_debug_options(self, **kwargs): - """ - 设置调试选项 - - Args: - **kwargs: 调试选项 - """ - if self.debug_tools: - self.debug_tools.set_debug_options(**kwargs) - - def get_predefined_material(self, material_name): - """ - 获取预定义材质 - - Args: - material_name (str): 材质名称 - - Returns: - PhysicsMaterial: 物理材质 - """ - return MATERIALS.get(material_name) - - def create_custom_material(self, name, properties): - """ - 创建自定义材质 - - Args: - name (str): 材质名称 - properties (dict): 材质属性 - - Returns: - PhysicsMaterial: 物理材质 - """ - return PhysicsMaterial(name, properties) - - def set_gravity(self, gravity): - """ - 设置重力 - - Args: - gravity (Vec3): 重力向量 - """ - self.physics_world.set_gravity(gravity) - - def set_time_step(self, time_step, max_substeps=10): - """ - 设置时间步长 - - Args: - time_step (float): 时间步长 - max_substeps (int): 最大子步数 - """ - self.physics_world.set_time_step(time_step, max_substeps) - - def get_collision_groups(self): - """ - 获取碰撞组工具 - - Returns: - PhysicsCollisionGroups: 碰撞组工具 - """ - return PhysicsCollisionGroups - - def save_config(self, config_file=None): - """ - 保存配置 - - Args: - config_file (str): 配置文件路径 - """ - if self.config_manager: - self.config_manager.save_config(config_file) - - def load_config(self, config_file): - """ - 加载配置 - - Args: - config_file (str): 配置文件路径 - """ - if self.config_manager: - self.config_manager.load_config(config_file) - - def clear_all(self): - """ - 清除所有物理对象 - """ - # 销毁所有车辆 - for vehicle in self.vehicles[:]: - vehicle.destroy() - self.vehicles.clear() - - # 销毁所有粒子系统 - for particle_system in self.particle_systems[:]: - particle_system.destroy() - self.particle_systems.clear() - - # 清除物理世界 - self.physics_world.clear() - - print("所有物理对象已清除") - - def destroy(self): - """ - 销毁物理插件 - """ - # 清除所有对象 - self.clear_all() - - # 销毁调试工具 - if self.debug_tools: - self.debug_tools.destroy() - - # 销毁性能分析器 - if self.profiler: - self.profiler.destroy() - - # 销毁物理世界 - del self.physics_world - +""" +刚体动力学插件主管理器 +整合所有物理功能模块,提供统一的接口 +""" + +from panda3d.core import Vec3, Point3 +from .core.physics_world import PhysicsWorld +from .core.rigid_body import RigidBody +from .core.material import PhysicsMaterial, MATERIALS +from .constraints.constraint_manager import ConstraintManager +from .vehicles.vehicle_system import VehicleSystem +from .particles.particle_system import ParticleSystem +from .debug.debug_tools import PhysicsDebug, PhysicsProfiler +from .utils.physics_utils import PhysicsUtils, PhysicsConfigManager, PhysicsCollisionGroups + + +class RigidBodyPhysicsPlugin: + """ + 刚体动力学插件主管理器 + 提供统一的物理系统接口 + """ + + def __init__(self, world_node=None, config_file=None): + """ + 初始化物理插件 + + Args: + world_node: Panda3D世界节点(用于调试可视化) + config_file (str): 配置文件路径 + """ + # 创建物理世界 + self.physics_world = PhysicsWorld() + + # 创建约束管理器 + self.constraint_manager = ConstraintManager() + + # 配置管理器 + self.config_manager = PhysicsConfigManager(config_file) + + # 调试工具 + self.debug_tools = None + self.profiler = None + if world_node: + self.debug_tools = PhysicsDebug(self.physics_world, world_node) + self.profiler = PhysicsProfiler(self.physics_world) + + # 活动对象列表 + self.vehicles = [] + self.particle_systems = [] + + print("刚体动力学插件初始化完成") + + def create_rigid_body(self, node_path, shape_type='box', mass=1.0, + body_type=RigidBody.DYNAMIC, material=None): + """ + 创建刚体 + + Args: + node_path: Panda3D节点路径 + shape_type (str): 碰撞形状类型 + mass (float): 质量 + body_type (int): 刚体类型 + material (PhysicsMaterial): 物理材质 + + Returns: + RigidBody: 刚体对象 + """ + # 创建刚体 + rigid_body = RigidBody(node_path, shape_type, mass, body_type) + + # 应用材质 + if material: + material.apply_to_rigid_body(rigid_body) + else: + # 使用默认材质 + default_material = PhysicsMaterial('default') + default_material.apply_to_rigid_body(rigid_body) + + # 添加到物理世界 + self.physics_world.add_rigid_body(rigid_body) + + return rigid_body + + def create_constraint(self, constraint_type, body_a, body_b, **kwargs): + """ + 创建约束 + + Args: + constraint_type (str): 约束类型 + body_a (RigidBody): 第一个刚体 + body_b (RigidBody): 第二个刚体 + **kwargs: 约束参数 + + Returns: + 约束对象 + """ + if constraint_type == ConstraintManager.CONSTRAINT_POINT2POINT: + constraint = self.constraint_manager.create_point2point_constraint( + body_a, body_b, **kwargs) + elif constraint_type == ConstraintManager.CONSTRAINT_HINGE: + constraint = self.constraint_manager.create_hinge_constraint( + body_a, body_b, **kwargs) + elif constraint_type == ConstraintManager.CONSTRAINT_SLIDER: + constraint = self.constraint_manager.create_slider_constraint( + body_a, body_b, **kwargs) + elif constraint_type == ConstraintManager.CONSTRAINT_CONETWIST: + constraint = self.constraint_manager.create_cone_twist_constraint( + body_a, body_b, **kwargs) + elif constraint_type == ConstraintManager.CONSTRAINT_6DOF: + constraint = self.constraint_manager.create_6dof_constraint( + body_a, body_b, **kwargs) + else: + raise ValueError(f"未知的约束类型: {constraint_type}") + + # 添加到物理世界 + self.physics_world.add_constraint(constraint) + + return constraint + + def create_vehicle(self, chassis_body, wheel_configurations): + """ + 创建车辆 + + Args: + chassis_body (RigidBody): 底盘刚体 + wheel_configurations (list): 车轮配置列表 + + Returns: + VehicleSystem: 车辆系统对象 + """ + # 创建车辆系统 + vehicle = VehicleSystem(self.physics_world, chassis_body) + + # 添加车轮 + for wheel_config in wheel_configurations: + vehicle.add_wheel(**wheel_config) + + # 保存车辆引用 + self.vehicles.append(vehicle) + + return vehicle + + def create_particle_system(self, max_particles=1000): + """ + 创建粒子系统 + + Args: + max_particles (int): 最大粒子数 + + Returns: + ParticleSystem: 粒子系统对象 + """ + # 创建粒子系统 + particle_system = ParticleSystem(self.physics_world, max_particles) + + # 保存粒子系统引用 + self.particle_systems.append(particle_system) + + # 添加到物理世界 + self.physics_world.add_particle_system(particle_system) + + return particle_system + + def update(self, dt): + """ + 更新物理系统 + + Args: + dt (float): 时间增量 + """ + # 更新物理世界 + self.physics_world.update(dt) + + # 更新粒子系统 + for particle_system in self.particle_systems: + particle_system.update(dt) + + # 更新调试工具 + if self.debug_tools: + self.debug_tools.update_debug_visualization() + + # 更新性能分析器 + if self.profiler: + self.profiler.record_frame() + + def ray_test(self, from_pos, to_pos): + """ + 射线检测 + + Args: + from_pos (Point3): 起点 + to_pos (Point3): 终点 + + Returns: + BulletRayHit: 射线检测结果 + """ + return self.physics_world.ray_test(from_pos, to_pos) + + def get_performance_stats(self): + """ + 获取性能统计 + + Returns: + dict: 性能统计信息 + """ + stats = self.physics_world.get_performance_stats() + + # 添加对象计数 + stats['objects'] = { + 'rigid_bodies': len(self.physics_world.rigid_bodies), + 'constraints': len(self.physics_world.constraints), + 'vehicles': len(self.vehicles), + 'particle_systems': len(self.particle_systems) + } + + return stats + + def enable_debug_visualization(self, enabled=True): + """ + 启用调试可视化 + + Args: + enabled (bool): 是否启用 + """ + if self.debug_tools: + self.debug_tools.enable_debug(enabled) + + def set_debug_options(self, **kwargs): + """ + 设置调试选项 + + Args: + **kwargs: 调试选项 + """ + if self.debug_tools: + self.debug_tools.set_debug_options(**kwargs) + + def get_predefined_material(self, material_name): + """ + 获取预定义材质 + + Args: + material_name (str): 材质名称 + + Returns: + PhysicsMaterial: 物理材质 + """ + return MATERIALS.get(material_name) + + def create_custom_material(self, name, properties): + """ + 创建自定义材质 + + Args: + name (str): 材质名称 + properties (dict): 材质属性 + + Returns: + PhysicsMaterial: 物理材质 + """ + return PhysicsMaterial(name, properties) + + def set_gravity(self, gravity): + """ + 设置重力 + + Args: + gravity (Vec3): 重力向量 + """ + self.physics_world.set_gravity(gravity) + + def set_time_step(self, time_step, max_substeps=10): + """ + 设置时间步长 + + Args: + time_step (float): 时间步长 + max_substeps (int): 最大子步数 + """ + self.physics_world.set_time_step(time_step, max_substeps) + + def get_collision_groups(self): + """ + 获取碰撞组工具 + + Returns: + PhysicsCollisionGroups: 碰撞组工具 + """ + return PhysicsCollisionGroups + + def save_config(self, config_file=None): + """ + 保存配置 + + Args: + config_file (str): 配置文件路径 + """ + if self.config_manager: + self.config_manager.save_config(config_file) + + def load_config(self, config_file): + """ + 加载配置 + + Args: + config_file (str): 配置文件路径 + """ + if self.config_manager: + self.config_manager.load_config(config_file) + + def clear_all(self): + """ + 清除所有物理对象 + """ + # 销毁所有车辆 + for vehicle in self.vehicles[:]: + vehicle.destroy() + self.vehicles.clear() + + # 销毁所有粒子系统 + for particle_system in self.particle_systems[:]: + particle_system.destroy() + self.particle_systems.clear() + + # 清除物理世界 + self.physics_world.clear() + + print("所有物理对象已清除") + + def destroy(self): + """ + 销毁物理插件 + """ + # 清除所有对象 + self.clear_all() + + # 销毁调试工具 + if self.debug_tools: + self.debug_tools.destroy() + + # 销毁性能分析器 + if self.profiler: + self.profiler.destroy() + + # 销毁物理世界 + del self.physics_world + print("刚体动力学插件已销毁") \ No newline at end of file diff --git a/plugins/user/rigid_body_physics/test_materials.py b/plugins/user/rigid_body_physics/test_materials.py index 93a44ef4..3777efa6 100644 --- a/plugins/user/rigid_body_physics/test_materials.py +++ b/plugins/user/rigid_body_physics/test_materials.py @@ -1,181 +1,181 @@ -""" -物理材质系统测试脚本 -测试增强的物理材质功能 -""" - -import sys -import os -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -from panda3d.core import Vec3, Vec4, Point3 -from plugins.user.rigid_body_physics.core.material import PhysicsMaterial - - -def test_advanced_material_features(): - """测试增强的材质功能""" - print("=== 测试增强的物理材质系统 ===\n") - - # 创建一个测试材质 - test_material = PhysicsMaterial('test_material', material_type=PhysicsMaterial.TYPE_METAL) - - print("1. 测试热效应计算:") - # 测试热效应计算 - temp_change = test_material.calculate_thermal_effect(0.1, environment_temp=25.0) - print(f" 基础温度变化: {temp_change:.4f}°C") - - # 测试带热源的热效应计算 - heat_sources = [{'position': Point3(0, 0, 0), 'temperature': 100.0, 'radius': 1.0}] - thermal_result = test_material.calculate_thermal_effect(0.1, environment_temp=25.0, heat_sources=heat_sources) - print(f" 带热源温度变化: {thermal_result:.4f}°C") - - # 测试热膨胀 - expansion_factor = test_material.calculate_thermal_expansion(temp_change) - print(f" 热膨胀因子: {expansion_factor:.6f}") - print() - - print("2. 测试环境效应:") - # 测试环境效应 - weather_conditions = { - 'rain': 0.8, - 'wind': 0.5, - 'humidity': 0.9, - 'uv': 0.7 - } - env_effects = test_material.calculate_environmental_effects(1.0, weather_conditions) - print(f" 环境效应结果: {env_effects}") - print(f" 当前湿润度: {test_material.state['wetness']:.3f}") - print(f" 当前污染度: {test_material.state['contamination']:.3f}") - print(f" 当前磨损度: {test_material.state['wear']:.3f}") - print() - - print("3. 测试老化模拟:") - # 测试老化模拟 - environmental_factors = { - 'temperature': 60, # 60°C - 'humidity': 0.9, # 90%湿度 - 'chemical_exposure': 0.3 - } - aging_result = test_material.simulate_aging(10.0, environmental_factors) - print(f" 老化结果: {aging_result}") - print(f" 当前磨损度: {test_material.state['wear']:.3f}") - print() - - print("4. 测试摩擦系数计算:") - # 测试摩擦系数计算 - other_material = PhysicsMaterial('other_material', material_type=PhysicsMaterial.TYPE_CONCRETE) - effective_friction = test_material.get_effective_friction( - other_material=other_material, - velocity=Vec3(2.0, 0, 0), - normal_force=100.0, - surface_area=0.5 - ) - print(f" 有效摩擦系数: {effective_friction:.4f}") - - dynamic_friction = test_material.get_dynamic_friction( - other_material=other_material, - velocity=Vec3(2.0, 0, 0), - normal_force=100.0, - surface_area=0.5 - ) - print(f" 动态摩擦系数: {dynamic_friction:.4f}") - print() - - print("5. 测试恢复系数计算:") - # 测试恢复系数计算 - effective_restitution = test_material.get_effective_restitution( - other_material=other_material, - impact_velocity=10.0, - temperature=50.0 - ) - print(f" 有效恢复系数: {effective_restitution:.4f}") - print() - - print("6. 测试能量吸收:") - # 测试能量吸收 - energy_absorption = test_material.get_energy_absorption(100.0, 0.1) - print(f" 能量吸收结果: {energy_absorption}") - print(f" 当前温度: {test_material.state['temperature']:.2f}°C") - print(f" 当前应力: {test_material.state['stress']:.2f}") - print() - - print("7. 测试磨损和疲劳:") - # 测试磨损率计算 - wear_rate = test_material.calculate_wear_rate(Vec3(1.0, 0, 0), 50.0, 1.0) - print(f" 磨损率: {wear_rate:.6f}") - - # 测试疲劳累积 - fatigue_result = test_material.calculate_fatigue_accumulation(200.0, 1000) - print(f" 疲劳累积结果: {fatigue_result}") - print(f" 当前疲劳度: {test_material.state['fatigue']:.4f}") - print() - - print("8. 测试流体动力学:") - # 测试流体动力学 - fluid_result = test_material.calculate_fluid_dynamics(Vec3(5.0, 0, 0), 1000.0, 1.0) - print(f" 流体动力学结果: {fluid_result}") - print(f" 当前湿润度: {test_material.state['wetness']:.3f}") - print() - - print("9. 测试化学反应:") - # 测试化学反应 - chemical_composition = { - 'acid': 0.5, - 'base': 0.2, - 'oxidizer': 0.3 - } - chemical_result = test_material.calculate_chemical_reaction(chemical_composition, temperature=30.0, time_delta=1.0) - print(f" 化学反应结果: {chemical_result}") - print(f" 当前污染度: {test_material.state['contamination']:.3f}") - print() - - print("10. 测试失效准则:") - # 测试失效准则 - stress_tensor = [1000.0, 500.0, -200.0, 100.0, 50.0, 75.0] - failure_analysis = test_material.check_failure_criteria(stress_tensor) - print(f" 失效分析结果: {failure_analysis}") - print() - - print("11. 测试结构完整性:") - # 测试结构完整性 - integrity_assessment = test_material.get_structural_integrity() - print(f" 结构完整性: {integrity_assessment}") - print() - - print("12. 测试历史统计:") - # 测试历史统计 - aging_stats = test_material.get_aging_stats() - fatigue_stats = test_material.get_fatigue_stats() - chemical_stats = test_material.get_chemical_stats() - print(f" 老化统计: {aging_stats}") - print(f" 疲劳统计: {fatigue_stats}") - print(f" 化学统计: {chemical_stats}") - print() - - print("=== 测试完成 ===") - - -def test_predefined_materials(): - """测试预定义材质""" - print("=== 测试预定义材质 ===\n") - - # 测试几种预定义材质 - material_names = ['concrete', 'metal', 'wood', 'rubber'] - - for name in material_names: - material = PhysicsMaterial.MATERIALS.get(name) - if material: - print(f"{name.capitalize()} 材质属性:") - print(f" 密度: {material.properties['density']} kg/m³") - print(f" 摩擦系数: {material.properties['friction']}") - print(f" 恢复系数: {material.properties['restitution']}") - if 'tensile_strength' in material.properties: - print(f" 抗拉强度: {material.properties['tensile_strength']} Pa") - if 'compressive_strength' in material.properties: - print(f" 抗压强度: {material.properties['compressive_strength']} Pa") - print() - - -if __name__ == "__main__": - test_advanced_material_features() - print("\n" + "="*50 + "\n") +""" +物理材质系统测试脚本 +测试增强的物理材质功能 +""" + +import sys +import os +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from panda3d.core import Vec3, Vec4, Point3 +from plugins.user.rigid_body_physics.core.material import PhysicsMaterial + + +def test_advanced_material_features(): + """测试增强的材质功能""" + print("=== 测试增强的物理材质系统 ===\n") + + # 创建一个测试材质 + test_material = PhysicsMaterial('test_material', material_type=PhysicsMaterial.TYPE_METAL) + + print("1. 测试热效应计算:") + # 测试热效应计算 + temp_change = test_material.calculate_thermal_effect(0.1, environment_temp=25.0) + print(f" 基础温度变化: {temp_change:.4f}°C") + + # 测试带热源的热效应计算 + heat_sources = [{'position': Point3(0, 0, 0), 'temperature': 100.0, 'radius': 1.0}] + thermal_result = test_material.calculate_thermal_effect(0.1, environment_temp=25.0, heat_sources=heat_sources) + print(f" 带热源温度变化: {thermal_result:.4f}°C") + + # 测试热膨胀 + expansion_factor = test_material.calculate_thermal_expansion(temp_change) + print(f" 热膨胀因子: {expansion_factor:.6f}") + print() + + print("2. 测试环境效应:") + # 测试环境效应 + weather_conditions = { + 'rain': 0.8, + 'wind': 0.5, + 'humidity': 0.9, + 'uv': 0.7 + } + env_effects = test_material.calculate_environmental_effects(1.0, weather_conditions) + print(f" 环境效应结果: {env_effects}") + print(f" 当前湿润度: {test_material.state['wetness']:.3f}") + print(f" 当前污染度: {test_material.state['contamination']:.3f}") + print(f" 当前磨损度: {test_material.state['wear']:.3f}") + print() + + print("3. 测试老化模拟:") + # 测试老化模拟 + environmental_factors = { + 'temperature': 60, # 60°C + 'humidity': 0.9, # 90%湿度 + 'chemical_exposure': 0.3 + } + aging_result = test_material.simulate_aging(10.0, environmental_factors) + print(f" 老化结果: {aging_result}") + print(f" 当前磨损度: {test_material.state['wear']:.3f}") + print() + + print("4. 测试摩擦系数计算:") + # 测试摩擦系数计算 + other_material = PhysicsMaterial('other_material', material_type=PhysicsMaterial.TYPE_CONCRETE) + effective_friction = test_material.get_effective_friction( + other_material=other_material, + velocity=Vec3(2.0, 0, 0), + normal_force=100.0, + surface_area=0.5 + ) + print(f" 有效摩擦系数: {effective_friction:.4f}") + + dynamic_friction = test_material.get_dynamic_friction( + other_material=other_material, + velocity=Vec3(2.0, 0, 0), + normal_force=100.0, + surface_area=0.5 + ) + print(f" 动态摩擦系数: {dynamic_friction:.4f}") + print() + + print("5. 测试恢复系数计算:") + # 测试恢复系数计算 + effective_restitution = test_material.get_effective_restitution( + other_material=other_material, + impact_velocity=10.0, + temperature=50.0 + ) + print(f" 有效恢复系数: {effective_restitution:.4f}") + print() + + print("6. 测试能量吸收:") + # 测试能量吸收 + energy_absorption = test_material.get_energy_absorption(100.0, 0.1) + print(f" 能量吸收结果: {energy_absorption}") + print(f" 当前温度: {test_material.state['temperature']:.2f}°C") + print(f" 当前应力: {test_material.state['stress']:.2f}") + print() + + print("7. 测试磨损和疲劳:") + # 测试磨损率计算 + wear_rate = test_material.calculate_wear_rate(Vec3(1.0, 0, 0), 50.0, 1.0) + print(f" 磨损率: {wear_rate:.6f}") + + # 测试疲劳累积 + fatigue_result = test_material.calculate_fatigue_accumulation(200.0, 1000) + print(f" 疲劳累积结果: {fatigue_result}") + print(f" 当前疲劳度: {test_material.state['fatigue']:.4f}") + print() + + print("8. 测试流体动力学:") + # 测试流体动力学 + fluid_result = test_material.calculate_fluid_dynamics(Vec3(5.0, 0, 0), 1000.0, 1.0) + print(f" 流体动力学结果: {fluid_result}") + print(f" 当前湿润度: {test_material.state['wetness']:.3f}") + print() + + print("9. 测试化学反应:") + # 测试化学反应 + chemical_composition = { + 'acid': 0.5, + 'base': 0.2, + 'oxidizer': 0.3 + } + chemical_result = test_material.calculate_chemical_reaction(chemical_composition, temperature=30.0, time_delta=1.0) + print(f" 化学反应结果: {chemical_result}") + print(f" 当前污染度: {test_material.state['contamination']:.3f}") + print() + + print("10. 测试失效准则:") + # 测试失效准则 + stress_tensor = [1000.0, 500.0, -200.0, 100.0, 50.0, 75.0] + failure_analysis = test_material.check_failure_criteria(stress_tensor) + print(f" 失效分析结果: {failure_analysis}") + print() + + print("11. 测试结构完整性:") + # 测试结构完整性 + integrity_assessment = test_material.get_structural_integrity() + print(f" 结构完整性: {integrity_assessment}") + print() + + print("12. 测试历史统计:") + # 测试历史统计 + aging_stats = test_material.get_aging_stats() + fatigue_stats = test_material.get_fatigue_stats() + chemical_stats = test_material.get_chemical_stats() + print(f" 老化统计: {aging_stats}") + print(f" 疲劳统计: {fatigue_stats}") + print(f" 化学统计: {chemical_stats}") + print() + + print("=== 测试完成 ===") + + +def test_predefined_materials(): + """测试预定义材质""" + print("=== 测试预定义材质 ===\n") + + # 测试几种预定义材质 + material_names = ['concrete', 'metal', 'wood', 'rubber'] + + for name in material_names: + material = PhysicsMaterial.MATERIALS.get(name) + if material: + print(f"{name.capitalize()} 材质属性:") + print(f" 密度: {material.properties['density']} kg/m³") + print(f" 摩擦系数: {material.properties['friction']}") + print(f" 恢复系数: {material.properties['restitution']}") + if 'tensile_strength' in material.properties: + print(f" 抗拉强度: {material.properties['tensile_strength']} Pa") + if 'compressive_strength' in material.properties: + print(f" 抗压强度: {material.properties['compressive_strength']} Pa") + print() + + +if __name__ == "__main__": + test_advanced_material_features() + print("\n" + "="*50 + "\n") test_predefined_materials() \ No newline at end of file diff --git a/plugins/user/rigid_body_physics/utils/physics_utils.py b/plugins/user/rigid_body_physics/utils/physics_utils.py index f465acce..c563c18d 100644 --- a/plugins/user/rigid_body_physics/utils/physics_utils.py +++ b/plugins/user/rigid_body_physics/utils/physics_utils.py @@ -1,448 +1,448 @@ -""" -实用工具模块 -提供物理系统相关的实用函数和工具类 -""" - -from panda3d.core import Vec3, Point3, LVector3f -from panda3d.core import TransformState -import math -import json - - -class PhysicsUtils: - """ - 物理实用工具类 - 提供常用的物理计算和转换函数 - """ - - @staticmethod - def calculate_mass_from_density(shape, density): - """ - 根据密度计算质量 - - Args: - shape: 碰撞形状对象 - density (float): 密度(kg/m³) - - Returns: - float: 质量(kg) - """ - # 获取形状体积(简化实现) - volume = PhysicsUtils.calculate_volume(shape) - return volume * density - - @staticmethod - def calculate_volume(shape): - """ - 计算形状体积(简化实现) - - Args: - shape: 碰撞形状对象 - - Returns: - float: 体积(m³) - """ - # 这里只是一个简化的实现,实际应该根据形状类型计算 - try: - # 尝试获取形状的AABB - aabb = shape.getAabb() - min_point = aabb.getMin() - max_point = aabb.getMax() - - # 计算包围盒体积 - dimensions = max_point - min_point - volume = dimensions.getX() * dimensions.getY() * dimensions.getZ() - - # 简化:假设实际体积是包围盒体积的一半 - return volume * 0.5 - except: - # 默认返回1立方米 - return 1.0 - - @staticmethod - def apply_impulse_at_point(body, impulse, point, relative_to_world=True): - """ - 在指定点应用冲量 - - Args: - body: 刚体对象 - impulse (Vec3): 冲量向量 - point (Point3): 作用点 - relative_to_world (bool): 点坐标是否相对于世界坐标系 - """ - if relative_to_world: - # 转换到局部坐标系 - local_point = body.physics_node.getTransform().getInverse().xformPoint(point) - else: - local_point = point - - body.bullet_body.applyImpulse(impulse, local_point) - - @staticmethod - def calculate_torque_from_force(force, point, center_of_mass=Point3(0, 0, 0)): - """ - 根据力和作用点计算扭矩 - - Args: - force (Vec3): 力向量 - point (Point3): 作用点 - center_of_mass (Point3): 质心位置 - - Returns: - Vec3: 扭矩向量 - """ - r = point - center_of_mass - return r.cross(force) - - @staticmethod - def interpolate_transforms(transform_a, transform_b, t): - """ - 插值两个变换 - - Args: - transform_a (TransformState): 起始变换 - transform_b (TransformState): 结束变换 - t (float): 插值因子 (0.0-1.0) - - Returns: - TransformState: 插值后的变换 - """ - # 插值位置 - pos_a = transform_a.getPos() - pos_b = transform_b.getPos() - interpolated_pos = pos_a + (pos_b - pos_a) * t - - # 插值旋转(简化实现) - hpr_a = transform_a.getHpr() - hpr_b = transform_b.getHpr() - interpolated_hpr = hpr_a + (hpr_b - hpr_a) * t - - # 插值缩放 - scale_a = transform_a.getScale() - scale_b = transform_b.getScale() - interpolated_scale = scale_a + (scale_b - scale_a) * t - - return TransformState.makePosHprScale(interpolated_pos, interpolated_hpr, interpolated_scale) - - @staticmethod - def clamp_vector(vector, max_length): - """ - 限制向量长度 - - Args: - vector (Vec3): 输入向量 - max_length (float): 最大长度 - - Returns: - Vec3: 限制后的向量 - """ - length = vector.length() - if length > max_length and length > 0: - return vector * (max_length / length) - return vector - - @staticmethod - def reflect_vector(vector, normal): - """ - 计算向量在法线上的反射 - - Args: - vector (Vec3): 入射向量 - normal (Vec3): 法线向量(应为单位向量) - - Returns: - Vec3: 反射向量 - """ - # R = V - 2 * (V · N) * N - dot_product = vector.dot(normal) - reflection = vector - normal * (2.0 * dot_product) - return reflection - - @staticmethod - def calculate_angular_velocity_from_rotation(start_rotation, end_rotation, time_delta): - """ - 根据旋转变化计算角速度 - - Args: - start_rotation (Vec3): 起始旋转 - end_rotation (Vec3): 结束旋转 - time_delta (float): 时间差 - - Returns: - Vec3: 角速度 - """ - if time_delta <= 0: - return Vec3(0, 0, 0) - - # 计算旋转差 - rotation_delta = end_rotation - start_rotation - - # 处理角度环绕 - for i in range(3): # H, P, R - while rotation_delta[i] > 180: - rotation_delta[i] -= 360 - while rotation_delta[i] < -180: - rotation_delta[i] += 360 - - # 计算角速度 - return rotation_delta / time_delta - - @staticmethod - def create_transform_from_position_rotation(position, rotation): - """ - 根据位置和旋转创建变换 - - Args: - position (Point3): 位置 - rotation (Vec3): 旋转(HPR) - - Returns: - TransformState: 变换状态 - """ - return TransformState.makePosHpr(position, rotation) - - @staticmethod - def get_transform_matrix(transform): - """ - 获取变换的矩阵表示 - - Args: - transform (TransformState): 变换状态 - - Returns: - LMatrix4f: 变换矩阵 - """ - return transform.getMat() - - @staticmethod - def transform_point(transform, point): - """ - 变换点 - - Args: - transform (TransformState): 变换状态 - point (Point3): 点 - - Returns: - Point3: 变换后的点 - """ - return transform.getMat().xformPoint(point) - - @staticmethod - def transform_vector(transform, vector): - """ - 变换向量 - - Args: - transform (TransformState): 变换状态 - vector (Vec3): 向量 - - Returns: - Vec3: 变换后的向量 - """ - return transform.getMat().xformVec(vector) - - -class PhysicsConfigManager: - """ - 物理配置管理器 - 管理物理系统的配置文件 - """ - - def __init__(self, config_file=None): - """ - 初始化配置管理器 - - Args: - config_file (str): 配置文件路径 - """ - self.config_file = config_file - self.config_data = {} - - if config_file: - self.load_config(config_file) - - def load_config(self, config_file): - """ - 加载配置文件 - - Args: - config_file (str): 配置文件路径 - """ - try: - with open(config_file, 'r', encoding='utf-8') as f: - self.config_data = json.load(f) - self.config_file = config_file - print(f"配置文件加载成功: {config_file}") - except Exception as e: - print(f"加载配置文件失败: {e}") - - def save_config(self, config_file=None): - """ - 保存配置文件 - - Args: - config_file (str): 配置文件路径(可选) - """ - save_path = config_file or self.config_file - if not save_path: - print("未指定配置文件路径") - return - - try: - with open(save_path, 'w', encoding='utf-8') as f: - json.dump(self.config_data, f, indent=2, ensure_ascii=False) - print(f"配置文件保存成功: {save_path}") - except Exception as e: - print(f"保存配置文件失败: {e}") - - def get_setting(self, key, default=None): - """ - 获取配置项 - - Args: - key (str): 配置项键名 - default: 默认值 - - Returns: - 配置项值 - """ - return self.config_data.get(key, default) - - def set_setting(self, key, value): - """ - 设置配置项 - - Args: - key (str): 配置项键名 - value: 配置项值 - """ - self.config_data[key] = value - - def get_physics_settings(self): - """ - 获取物理设置 - - Returns: - dict: 物理设置 - """ - return self.config_data.get('physics', {}) - - def set_physics_settings(self, settings): - """ - 设置物理设置 - - Args: - settings (dict): 物理设置 - """ - self.config_data['physics'] = settings - - def get_material_settings(self, material_name): - """ - 获取材质设置 - - Args: - material_name (str): 材质名称 - - Returns: - dict: 材质设置 - """ - materials = self.config_data.get('materials', {}) - return materials.get(material_name, {}) - - def set_material_settings(self, material_name, settings): - """ - 设置材质设置 - - Args: - material_name (str): 材质名称 - settings (dict): 材质设置 - """ - if 'materials' not in self.config_data: - self.config_data['materials'] = {} - self.config_data['materials'][material_name] = settings - - -class PhysicsCollisionGroups: - """ - 物理碰撞组定义 - 定义常用的碰撞组和掩码 - """ - - # 基础碰撞组 - GROUP_STATIC = 1 # 静态物体 - GROUP_DYNAMIC = 2 # 动态物体 - GROUP_KINEMATIC = 4 # 运动物体 - GROUP_SENSOR = 8 # 传感器 - GROUP_PARTICLE = 16 # 粒子 - GROUP_VEHICLE = 32 # 车辆 - GROUP_CHARACTER = 64 # 角色 - GROUP_TRIGGER = 128 # 触发器 - - # 常用掩码组合 - MASK_ALL = 0xFFFFFFFF # 所有组 - MASK_STATIC_DYNAMIC = GROUP_STATIC | GROUP_DYNAMIC # 静态与动态 - MASK_DYNAMIC_ONLY = GROUP_DYNAMIC # 仅动态 - MASK_VEHICLE_WORLD = GROUP_STATIC | GROUP_DYNAMIC | GROUP_VEHICLE # 车辆与世界 - MASK_CHARACTER_WORLD = GROUP_STATIC | GROUP_DYNAMIC | GROUP_CHARACTER # 角色与世界 - MASK_PARTICLE_WORLD = GROUP_STATIC | GROUP_DYNAMIC | GROUP_PARTICLE # 粒子与世界 - - @staticmethod - def create_mask(*groups): - """ - 创建掩码 - - Args: - *groups: 碰撞组 - - Returns: - int: 掩码值 - """ - mask = 0 - for group in groups: - mask |= group - return mask - - @staticmethod - def add_to_mask(mask, *groups): - """ - 添加组到掩码 - - Args: - mask (int): 原始掩码 - *groups: 要添加的碰撞组 - - Returns: - int: 新掩码 - """ - for group in groups: - mask |= group - return mask - - @staticmethod - def remove_from_mask(mask, *groups): - """ - 从掩码移除组 - - Args: - mask (int): 原始掩码 - *groups: 要移除的碰撞组 - - Returns: - int: 新掩码 - """ - for group in groups: - mask &= ~group - return mask - - -# 预定义的物理常量 -PHYSICS_CONSTANTS = { - 'GRAVITY_EARTH': Vec3(0, 0, -9.81), # 地球重力 - 'GRAVITY_MOON': Vec3(0, 0, -1.62), # 月球重力 - 'GRAVITY_MARS': Vec3(0, 0, -3.71), # 火星重力 - 'AIR_DENSITY_SEA_LEVEL': 1.225, # 海平面空气密度 - 'WATER_DENSITY': 1000.0, # 水密度 - 'SPEED_OF_SOUND_AIR': 343.0, # 空气中声速 - 'SPEED_OF_LIGHT': 299792458.0 # 光速 +""" +实用工具模块 +提供物理系统相关的实用函数和工具类 +""" + +from panda3d.core import Vec3, Point3, LVector3f +from panda3d.core import TransformState +import math +import json + + +class PhysicsUtils: + """ + 物理实用工具类 + 提供常用的物理计算和转换函数 + """ + + @staticmethod + def calculate_mass_from_density(shape, density): + """ + 根据密度计算质量 + + Args: + shape: 碰撞形状对象 + density (float): 密度(kg/m³) + + Returns: + float: 质量(kg) + """ + # 获取形状体积(简化实现) + volume = PhysicsUtils.calculate_volume(shape) + return volume * density + + @staticmethod + def calculate_volume(shape): + """ + 计算形状体积(简化实现) + + Args: + shape: 碰撞形状对象 + + Returns: + float: 体积(m³) + """ + # 这里只是一个简化的实现,实际应该根据形状类型计算 + try: + # 尝试获取形状的AABB + aabb = shape.getAabb() + min_point = aabb.getMin() + max_point = aabb.getMax() + + # 计算包围盒体积 + dimensions = max_point - min_point + volume = dimensions.getX() * dimensions.getY() * dimensions.getZ() + + # 简化:假设实际体积是包围盒体积的一半 + return volume * 0.5 + except: + # 默认返回1立方米 + return 1.0 + + @staticmethod + def apply_impulse_at_point(body, impulse, point, relative_to_world=True): + """ + 在指定点应用冲量 + + Args: + body: 刚体对象 + impulse (Vec3): 冲量向量 + point (Point3): 作用点 + relative_to_world (bool): 点坐标是否相对于世界坐标系 + """ + if relative_to_world: + # 转换到局部坐标系 + local_point = body.physics_node.getTransform().getInverse().xformPoint(point) + else: + local_point = point + + body.bullet_body.applyImpulse(impulse, local_point) + + @staticmethod + def calculate_torque_from_force(force, point, center_of_mass=Point3(0, 0, 0)): + """ + 根据力和作用点计算扭矩 + + Args: + force (Vec3): 力向量 + point (Point3): 作用点 + center_of_mass (Point3): 质心位置 + + Returns: + Vec3: 扭矩向量 + """ + r = point - center_of_mass + return r.cross(force) + + @staticmethod + def interpolate_transforms(transform_a, transform_b, t): + """ + 插值两个变换 + + Args: + transform_a (TransformState): 起始变换 + transform_b (TransformState): 结束变换 + t (float): 插值因子 (0.0-1.0) + + Returns: + TransformState: 插值后的变换 + """ + # 插值位置 + pos_a = transform_a.getPos() + pos_b = transform_b.getPos() + interpolated_pos = pos_a + (pos_b - pos_a) * t + + # 插值旋转(简化实现) + hpr_a = transform_a.getHpr() + hpr_b = transform_b.getHpr() + interpolated_hpr = hpr_a + (hpr_b - hpr_a) * t + + # 插值缩放 + scale_a = transform_a.getScale() + scale_b = transform_b.getScale() + interpolated_scale = scale_a + (scale_b - scale_a) * t + + return TransformState.makePosHprScale(interpolated_pos, interpolated_hpr, interpolated_scale) + + @staticmethod + def clamp_vector(vector, max_length): + """ + 限制向量长度 + + Args: + vector (Vec3): 输入向量 + max_length (float): 最大长度 + + Returns: + Vec3: 限制后的向量 + """ + length = vector.length() + if length > max_length and length > 0: + return vector * (max_length / length) + return vector + + @staticmethod + def reflect_vector(vector, normal): + """ + 计算向量在法线上的反射 + + Args: + vector (Vec3): 入射向量 + normal (Vec3): 法线向量(应为单位向量) + + Returns: + Vec3: 反射向量 + """ + # R = V - 2 * (V · N) * N + dot_product = vector.dot(normal) + reflection = vector - normal * (2.0 * dot_product) + return reflection + + @staticmethod + def calculate_angular_velocity_from_rotation(start_rotation, end_rotation, time_delta): + """ + 根据旋转变化计算角速度 + + Args: + start_rotation (Vec3): 起始旋转 + end_rotation (Vec3): 结束旋转 + time_delta (float): 时间差 + + Returns: + Vec3: 角速度 + """ + if time_delta <= 0: + return Vec3(0, 0, 0) + + # 计算旋转差 + rotation_delta = end_rotation - start_rotation + + # 处理角度环绕 + for i in range(3): # H, P, R + while rotation_delta[i] > 180: + rotation_delta[i] -= 360 + while rotation_delta[i] < -180: + rotation_delta[i] += 360 + + # 计算角速度 + return rotation_delta / time_delta + + @staticmethod + def create_transform_from_position_rotation(position, rotation): + """ + 根据位置和旋转创建变换 + + Args: + position (Point3): 位置 + rotation (Vec3): 旋转(HPR) + + Returns: + TransformState: 变换状态 + """ + return TransformState.makePosHpr(position, rotation) + + @staticmethod + def get_transform_matrix(transform): + """ + 获取变换的矩阵表示 + + Args: + transform (TransformState): 变换状态 + + Returns: + LMatrix4f: 变换矩阵 + """ + return transform.getMat() + + @staticmethod + def transform_point(transform, point): + """ + 变换点 + + Args: + transform (TransformState): 变换状态 + point (Point3): 点 + + Returns: + Point3: 变换后的点 + """ + return transform.getMat().xformPoint(point) + + @staticmethod + def transform_vector(transform, vector): + """ + 变换向量 + + Args: + transform (TransformState): 变换状态 + vector (Vec3): 向量 + + Returns: + Vec3: 变换后的向量 + """ + return transform.getMat().xformVec(vector) + + +class PhysicsConfigManager: + """ + 物理配置管理器 + 管理物理系统的配置文件 + """ + + def __init__(self, config_file=None): + """ + 初始化配置管理器 + + Args: + config_file (str): 配置文件路径 + """ + self.config_file = config_file + self.config_data = {} + + if config_file: + self.load_config(config_file) + + def load_config(self, config_file): + """ + 加载配置文件 + + Args: + config_file (str): 配置文件路径 + """ + try: + with open(config_file, 'r', encoding='utf-8') as f: + self.config_data = json.load(f) + self.config_file = config_file + print(f"配置文件加载成功: {config_file}") + except Exception as e: + print(f"加载配置文件失败: {e}") + + def save_config(self, config_file=None): + """ + 保存配置文件 + + Args: + config_file (str): 配置文件路径(可选) + """ + save_path = config_file or self.config_file + if not save_path: + print("未指定配置文件路径") + return + + try: + with open(save_path, 'w', encoding='utf-8') as f: + json.dump(self.config_data, f, indent=2, ensure_ascii=False) + print(f"配置文件保存成功: {save_path}") + except Exception as e: + print(f"保存配置文件失败: {e}") + + def get_setting(self, key, default=None): + """ + 获取配置项 + + Args: + key (str): 配置项键名 + default: 默认值 + + Returns: + 配置项值 + """ + return self.config_data.get(key, default) + + def set_setting(self, key, value): + """ + 设置配置项 + + Args: + key (str): 配置项键名 + value: 配置项值 + """ + self.config_data[key] = value + + def get_physics_settings(self): + """ + 获取物理设置 + + Returns: + dict: 物理设置 + """ + return self.config_data.get('physics', {}) + + def set_physics_settings(self, settings): + """ + 设置物理设置 + + Args: + settings (dict): 物理设置 + """ + self.config_data['physics'] = settings + + def get_material_settings(self, material_name): + """ + 获取材质设置 + + Args: + material_name (str): 材质名称 + + Returns: + dict: 材质设置 + """ + materials = self.config_data.get('materials', {}) + return materials.get(material_name, {}) + + def set_material_settings(self, material_name, settings): + """ + 设置材质设置 + + Args: + material_name (str): 材质名称 + settings (dict): 材质设置 + """ + if 'materials' not in self.config_data: + self.config_data['materials'] = {} + self.config_data['materials'][material_name] = settings + + +class PhysicsCollisionGroups: + """ + 物理碰撞组定义 + 定义常用的碰撞组和掩码 + """ + + # 基础碰撞组 + GROUP_STATIC = 1 # 静态物体 + GROUP_DYNAMIC = 2 # 动态物体 + GROUP_KINEMATIC = 4 # 运动物体 + GROUP_SENSOR = 8 # 传感器 + GROUP_PARTICLE = 16 # 粒子 + GROUP_VEHICLE = 32 # 车辆 + GROUP_CHARACTER = 64 # 角色 + GROUP_TRIGGER = 128 # 触发器 + + # 常用掩码组合 + MASK_ALL = 0xFFFFFFFF # 所有组 + MASK_STATIC_DYNAMIC = GROUP_STATIC | GROUP_DYNAMIC # 静态与动态 + MASK_DYNAMIC_ONLY = GROUP_DYNAMIC # 仅动态 + MASK_VEHICLE_WORLD = GROUP_STATIC | GROUP_DYNAMIC | GROUP_VEHICLE # 车辆与世界 + MASK_CHARACTER_WORLD = GROUP_STATIC | GROUP_DYNAMIC | GROUP_CHARACTER # 角色与世界 + MASK_PARTICLE_WORLD = GROUP_STATIC | GROUP_DYNAMIC | GROUP_PARTICLE # 粒子与世界 + + @staticmethod + def create_mask(*groups): + """ + 创建掩码 + + Args: + *groups: 碰撞组 + + Returns: + int: 掩码值 + """ + mask = 0 + for group in groups: + mask |= group + return mask + + @staticmethod + def add_to_mask(mask, *groups): + """ + 添加组到掩码 + + Args: + mask (int): 原始掩码 + *groups: 要添加的碰撞组 + + Returns: + int: 新掩码 + """ + for group in groups: + mask |= group + return mask + + @staticmethod + def remove_from_mask(mask, *groups): + """ + 从掩码移除组 + + Args: + mask (int): 原始掩码 + *groups: 要移除的碰撞组 + + Returns: + int: 新掩码 + """ + for group in groups: + mask &= ~group + return mask + + +# 预定义的物理常量 +PHYSICS_CONSTANTS = { + 'GRAVITY_EARTH': Vec3(0, 0, -9.81), # 地球重力 + 'GRAVITY_MOON': Vec3(0, 0, -1.62), # 月球重力 + 'GRAVITY_MARS': Vec3(0, 0, -3.71), # 火星重力 + 'AIR_DENSITY_SEA_LEVEL': 1.225, # 海平面空气密度 + 'WATER_DENSITY': 1000.0, # 水密度 + 'SPEED_OF_SOUND_AIR': 343.0, # 空气中声速 + 'SPEED_OF_LIGHT': 299792458.0 # 光速 } \ No newline at end of file diff --git a/plugins/user/rigid_body_physics/vehicles/vehicle_system.py b/plugins/user/rigid_body_physics/vehicles/vehicle_system.py index b8f1d41e..57cfb5a7 100644 --- a/plugins/user/rigid_body_physics/vehicles/vehicle_system.py +++ b/plugins/user/rigid_body_physics/vehicles/vehicle_system.py @@ -1,925 +1,925 @@ -""" -车辆系统模块 -负责车辆物理的创建和管理 -""" - -from panda3d.core import Vec3, Point3, LVector3f -from panda3d.bullet import BulletVehicle, BulletWheel, BulletRigidBodyNode -import math -import time -from collections import deque - - -class VehicleSystem: - """ - 车辆系统类 - 管理车辆的物理特性和控制 - """ - - def __init__(self, physics_world, chassis_body, vehicle_config=None): - """ - 初始化车辆系统 - - Args: - physics_world (PhysicsWorld): 物理世界对象 - chassis_body (RigidBody): 底盘刚体 - vehicle_config (dict): 车辆配置参数 - """ - self.physics_world = physics_world - self.chassis_body = chassis_body - - # 车辆配置 - self.config = { - 'max_engine_force': 3000.0, - 'max_brake_force': 100.0, - 'max_steering': math.pi / 4, # 45度 - 'steering_return_speed': 2.0, # 转向回正速度 - 'steering_sensitivity': 1.0, # 转向灵敏度 - 'center_of_mass_offset': Vec3(0, 0, -0.5), # 质心偏移 - 'suspension_stiffness': 50.0, - 'suspension_damping': 2.3, - 'suspension_compression': 4.4, - 'suspension_rest_length': 0.5, - 'roll_influence': 0.1, - 'friction_slip': 100.0, - 'wheel_radius': 0.3, - 'wheel_width': 0.2, - 'wheel_mass': 1.0, - 'anti_roll_bar_force': 100.0, # 防倾杆力 - 'aerodynamic_downforce': 0.0, # 空气动力学下压力 - 'aerodynamic_drag': 0.0, # 空气动力学阻力 - 'differential_type': 'open', # 差速器类型: open, locked, limited_slip - 'gear_ratios': [2.66, 1.78, 1.30, 1.00, 0.74, 0.50], # 齿轮比 - 'final_drive_ratio': 3.42, # 最终传动比 - 'max_rpm': 7000, # 最大转速 - 'idle_rpm': 800, # 怠速转速 - 'engine_torque_curve': [(0, 0), (1000, 200), (2000, 300), (3000, 350), - (4000, 380), (5000, 350), (6000, 300), (7000, 200)], # 扭矩曲线 - } - - # 应用自定义配置 - if vehicle_config: - self.config.update(vehicle_config) - - # 创建Bullet车辆 - self.bullet_vehicle = BulletVehicle( - physics_world.bullet_world.getBroadphase(), - chassis_body.bullet_body - ) - self.bullet_vehicle.setCoordinateSystem(0, 1, 2) # Right, Up, Forward - - # 车轮列表 - self.wheels = [] - self.wheel_info = [] # 详细的车轮信息 - - # 车辆参数 - self.max_engine_force = self.config['max_engine_force'] - self.max_brake_force = self.config['max_brake_force'] - self.max_steering = self.config['max_steering'] - self.steering_return_speed = self.config['steering_return_speed'] - self.steering_sensitivity = self.config['steering_sensitivity'] - - # 控制状态 - self.steering = 0.0 - self.target_steering = 0.0 - self.engine_force = 0.0 - self.brake_force = 0.0 - self.handbrake_force = 0.0 - self.gear = 1 # 当前档位 - self.rpm = self.config['idle_rpm'] # 发动机转速 - self.clutch_engaged = True # 离合器状态 - - # 车辆状态 - self.speed = 0.0 - self.speed_kmh = 0.0 - self.acceleration = 0.0 - self.lateral_acceleration = 0.0 - self.yaw_rate = 0.0 - self.pitch = 0.0 - self.roll = 0.0 - - # 历史数据 - self.speed_history = deque(maxlen=100) - self.acceleration_history = deque(maxlen=100) - self.steering_history = deque(maxlen=100) - - # 高级特性 - self.anti_lock_brake_enabled = True - self.traction_control_enabled = True - self.stability_control_enabled = True - self.abs_threshold = 0.8 # ABS触发阈值 - self.tc_slip_threshold = 0.2 # 牵引力控制滑移阈值 - - # 车辆损伤系统 - self.damage = { - 'engine': 0.0, # 发动机损伤 - 'transmission': 0.0, # 传动系统损伤 - 'suspension': 0.0, # 悬挂损伤 - 'wheels': [0.0] * 4, # 车轮损伤 - 'body': 0.0, # 车身损伤 - } - - # 燃料系统 - self.fuel_capacity = 60.0 # 升 - self.fuel_level = 60.0 # 升 - self.fuel_consumption_rate = 0.01 # 升/秒/单位力 - - # 时间戳 - self.creation_time = time.time() - self.last_update_time = self.creation_time - - # 添加到物理世界 - physics_world.add_vehicle(self) - - # 设置底盘质心偏移 - self._set_center_of_mass() - - print("车辆系统初始化完成") - - def _set_center_of_mass(self): - """ - 设置底盘质心偏移 - """ - com_offset = self.config['center_of_mass_offset'] - if com_offset.length() > 0: - self.chassis_body.bullet_body.setCenterOfMassTransform( - TransformState.makePos(com_offset) - ) - - def add_wheel(self, connection_point, wheel_direction, wheel_axle, wheel_radius=None, - suspension_rest_length=None, suspension_stiffness=None, - damping_relaxation=None, damping_compression=None, - friction_slip=None, roll_influence=None, wheel_width=None, - wheel_mass=None, is_front_wheel=True, is_steered_wheel=True, - is_driven_wheel=True, is_braked_wheel=True): - """ - 添加车轮 - - Args: - connection_point (Point3): 底盘连接点 - wheel_direction (Vec3): 车轮方向(通常向下) - wheel_axle (Vec3): 车轮轴向(通常向右) - wheel_radius (float): 车轮半径 - suspension_rest_length (float): 悬挂静止长度 - suspension_stiffness (float): 悬挂刚度 - damping_relaxation (float): 放松阻尼 - damping_compression (float): 压缩阻尼 - friction_slip (float): 滑动摩擦 - roll_influence (float): 翻滚影响 - wheel_width (float): 车轮宽度 - wheel_mass (float): 车轮质量 - is_front_wheel (bool): 是否为前轮 - is_steered_wheel (bool): 是否为转向轮 - is_driven_wheel (bool): 是否为驱动轮 - is_braked_wheel (bool): 是否为刹车轮 - - Returns: - BulletWheel: 车轮对象 - """ - # 使用配置参数或默认值 - wheel_radius = wheel_radius or self.config['wheel_radius'] - suspension_rest_length = suspension_rest_length or self.config['suspension_rest_length'] - suspension_stiffness = suspension_stiffness or self.config['suspension_stiffness'] - damping_relaxation = damping_relaxation or self.config['suspension_damping'] - damping_compression = damping_compression or self.config['suspension_compression'] - friction_slip = friction_slip or self.config['friction_slip'] - roll_influence = roll_influence or self.config['roll_influence'] - wheel_width = wheel_width or self.config['wheel_width'] - wheel_mass = wheel_mass or self.config['wheel_mass'] - - wheel = self.bullet_vehicle.createWheel() - - # 设置车轮参数 - wheel.setChassisConnectionPointCs(connection_point) - wheel.setWheelDirectionCs(wheel_direction) - wheel.setWheelAxleCs(wheel_axle) - wheel.setWheelRadius(wheel_radius) - wheel.setWheelWidth(wheel_width) - wheel.setMaxSuspensionTravelCm(500.0) - wheel.setSuspensionStiffness(suspension_stiffness) - wheel.setWheelsDampingRelaxation(damping_relaxation) - wheel.setWheelsDampingCompression(damping_compression) - wheel.setFrictionSlip(friction_slip) - wheel.setRollInfluence(roll_influence) - wheel.setSteering(is_steered_wheel) - wheel.setDriving(is_driven_wheel) - wheel.setBraking(is_braked_wheel) - - # 创建车轮刚体 - wheel_shape = BulletCylinderShape(wheel_radius, wheel_width, 1) # Y轴 - wheel_body = BulletRigidBodyNode(f'Wheel_{len(self.wheels)}') - wheel_body.addShape(wheel_shape) - wheel_body.setMass(wheel_mass) - - # 保存车轮信息 - wheel_info = { - 'wheel': wheel, - 'body': wheel_body, - 'connection_point': connection_point, - 'direction': wheel_direction, - 'axle': wheel_axle, - 'radius': wheel_radius, - 'width': wheel_width, - 'mass': wheel_mass, - 'properties': { - 'suspension_rest_length': suspension_rest_length, - 'suspension_stiffness': suspension_stiffness, - 'damping_relaxation': damping_relaxation, - 'damping_compression': damping_compression, - 'friction_slip': friction_slip, - 'roll_influence': roll_influence - }, - 'is_front_wheel': is_front_wheel, - 'is_steered_wheel': is_steered_wheel, - 'is_driven_wheel': is_driven_wheel, - 'is_braked_wheel': is_braked_wheel, - 'rotation': 0.0, - 'rpm': 0.0, - 'skid_info': 0.0, - 'suspension_relative_velocity': 0.0, - 'clipped_inv_contact_dot_suspension': 0.0, - 'suspension_force': 0.0, - 'side_impulse': 0.0, - 'forward_impulse': 0.0, - 'side_friction_stiffness': 1.0, - 'forward_friction_stiffness': 1.0, - 'skid_energy': 0.0, - 'slip_energy': 0.0, - 'temperature': 20.0, # 车轮温度 - 'wear': 0.0, # 车轮磨损 - 'flat_spot': False, # 是否有平点 - } - - self.wheels.append(wheel_info) - self.wheel_info.append(wheel_info) - - # 更新车轮损伤数组大小 - while len(self.damage['wheels']) < len(self.wheels): - self.damage['wheels'].append(0.0) - - return wheel - - def set_steering(self, steering): - """ - 设置转向 - - Args: - steering (float): 转向值 (-1.0 到 1.0) - """ - self.target_steering = max(-1.0, min(1.0, steering)) - - def set_engine_force(self, force): - """ - 设置引擎力 - - Args: - force (float): 引擎力 (-1.0 到 1.0) - """ - self.engine_force = max(-1.0, min(1.0, force)) - - def set_brake_force(self, force): - """ - 设置刹车力 - - Args: - force (float): 刹车力 (0.0 到 1.0) - """ - self.brake_force = max(0.0, min(1.0, force)) - - def set_handbrake_force(self, force): - """ - 设置手刹力 - - Args: - force (float): 手刹力 (0.0 到 1.0) - """ - self.handbrake_force = max(0.0, min(1.0, force)) - - def shift_up(self): - """ - 升档 - """ - if self.gear < len(self.config['gear_ratios']): - self.gear += 1 - - def shift_down(self): - """ - 降档 - """ - if self.gear > 1: - self.gear -= 1 - - def set_gear(self, gear): - """ - 设置档位 - - Args: - gear (int): 档位 - """ - self.gear = max(1, min(len(self.config['gear_ratios']), gear)) - - def update(self, dt): - """ - 更新车辆状态 - - Args: - dt (float): 时间增量 - """ - current_time = time.time() - self.last_update_time = current_time - - # 平滑转向控制 - steering_diff = self.target_steering - self.steering - if abs(steering_diff) > 0.01: - steering_change = steering_diff * self.steering_sensitivity * dt * 10 - self.steering += steering_change - else: - # 转向回正 - if abs(self.steering) > 0.01: - self.steering -= self.steering * self.steering_return_speed * dt - else: - self.steering = 0.0 - - # 应用转向到前轮 - self._apply_steering() - - # 计算发动机转速 - self._calculate_engine_rpm() - - # 应用引擎力和刹车力 - self._apply_forces_and_brakes(dt) - - # 应用手刹 - self._apply_handbrake() - - # 更新车辆状态 - self._update_vehicle_state(dt) - - # 更新车轮状态 - self._update_wheel_states() - - # 更新燃料消耗 - self._update_fuel_consumption(dt) - - # 记录历史数据 - self._record_history() - - def _apply_steering(self): - """ - 应用转向 - """ - steering_angle = self.steering * self.max_steering - - # 应用转向到转向轮(假设前两个轮子是前轮) - for i, wheel_info in enumerate(self.wheels): - if wheel_info['is_steered_wheel']: - wheel_info['wheel'].setSteering(steering_angle) - - def _calculate_engine_rpm(self): - """ - 计算发动机转速 - """ - # 简化的转速计算 - wheel_rpm = 0.0 - driven_wheels = 0 - - for wheel_info in self.wheels: - if wheel_info['is_driven_wheel']: - wheel_rpm += abs(wheel_info['rpm']) - driven_wheels += 1 - - if driven_wheels > 0: - avg_wheel_rpm = wheel_rpm / driven_wheels - # 考虑当前档位和最终传动比 - gear_ratio = self.config['gear_ratios'][self.gear - 1] - final_ratio = self.config['final_drive_ratio'] - self.rpm = avg_wheel_rpm * gear_ratio * final_ratio - - # 限制转速范围 - self.rpm = max(self.config['idle_rpm'], min(self.config['max_rpm'], self.rpm)) - - def _apply_forces_and_brakes(self, dt): - """ - 应用引擎力和刹车力 - - Args: - dt (float): 时间增量 - """ - # 计算实际引擎力(考虑损伤和燃料) - actual_engine_force = self.engine_force * self.max_engine_force - if self.damage['engine'] > 0.5: - actual_engine_force *= (1.0 - self.damage['engine'] * 0.8) - if self.fuel_level <= 0: - actual_engine_force = 0.0 - - # 计算实际刹车力 - actual_brake_force = self.brake_force * self.max_brake_force - - # 应用牵引力控制 - if self.traction_control_enabled: - actual_engine_force = self._apply_traction_control(actual_engine_force, dt) - - # 应用到驱动轮 - for i, wheel_info in enumerate(self.wheels): - if wheel_info['is_driven_wheel']: - wheel_info['wheel'].setEngineForce(actual_engine_force) - if wheel_info['is_braked_wheel']: - wheel_info['wheel'].setBrake(actual_brake_force) - - def _apply_traction_control(self, engine_force, dt): - """ - 应用牵引力控制 - - Args: - engine_force (float): 引擎力 - dt (float): 时间增量 - - Returns: - float: 调整后的引擎力 - """ - # 检查是否有车轮打滑 - slip_detected = False - for wheel_info in self.wheels: - if wheel_info['is_driven_wheel']: - slip_ratio = wheel_info['skid_info'] - if slip_ratio > self.tc_slip_threshold: - slip_detected = True - break - - # 如果检测到打滑,减少引擎力 - if slip_detected: - return engine_force * 0.5 - - return engine_force - - def _apply_handbrake(self): - """ - 应用手刹 - """ - handbrake_force = self.handbrake_force * self.max_brake_force * 2 # 手刹力度更大 - - # 应用到后轮(假设后两个轮子是后轮) - for i, wheel_info in enumerate(self.wheels): - if not wheel_info['is_front_wheel']: # 后轮 - wheel_info['wheel'].setBrake( - wheel_info['wheel'].getBrake() + handbrake_force - ) - - def _update_vehicle_state(self, dt): - """ - 更新车辆状态 - - Args: - dt (float): 时间增量 - """ - # 获取当前速度 - linear_velocity = self.chassis_body.get_linear_velocity() - self.speed = linear_velocity.length() - self.speed_kmh = self.speed * 3.6 # 转换为km/h - - # 计算加速度 - if len(self.speed_history) > 0: - prev_speed = self.speed_history[-1][0] - time_diff = dt - if time_diff > 0: - self.acceleration = (self.speed - prev_speed) / time_diff - else: - self.acceleration = 0.0 - - # 计算横向加速度 - # 这里需要更复杂的计算,简化处理 - self.lateral_acceleration = 0.0 - - # 计算横摆角速度 - angular_velocity = self.chassis_body.get_angular_velocity() - self.yaw_rate = angular_velocity.getZ() - - # 计算俯仰和翻滚 - rotation = self.chassis_body.get_rotation() - self.pitch = rotation.getY() - self.roll = rotation.getX() - - # 应用空气动力学效应 - self._apply_aerodynamics(linear_velocity, dt) - - # 应用防倾杆力 - self._apply_anti_roll_bars() - - def _apply_aerodynamics(self, velocity, dt): - """ - 应用空气动力学效应 - - Args: - velocity (Vec3): 速度向量 - dt (float): 时间增量 - """ - speed = velocity.length() - if speed > 0: - # 下压力(随速度平方增加) - downforce = self.config['aerodynamic_downforce'] * speed * speed - if downforce > 0: - downforce_vector = Vec3(0, 0, -downforce) - self.chassis_body.apply_force(downforce_vector) - - # 空气阻力 - drag_coefficient = self.config['aerodynamic_drag'] - drag_force = drag_coefficient * speed * speed - if drag_force > 0: - drag_direction = -velocity.normalized() - drag_vector = drag_direction * drag_force - self.chassis_body.apply_force(drag_vector) - - def _apply_anti_roll_bars(self): - """ - 应用防倾杆力 - """ - if len(self.wheels) >= 4: - # 简化的防倾杆实现 - front_left = self.wheels[0]['wheel'].getSuspensionRelativeVelocity() - front_right = self.wheels[1]['wheel'].getSuspensionRelativeVelocity() - rear_left = self.wheels[2]['wheel'].getSuspensionRelativeVelocity() - rear_right = self.wheels[3]['wheel'].getSuspensionRelativeVelocity() - - # 前轴防倾 - front_roll = (front_left - front_right) * self.config['anti_roll_bar_force'] - # 后轴防倾 - rear_roll = (rear_left - rear_right) * self.config['anti_roll_bar_force'] - - # 这里应该应用力到车身上,简化处理 - - def _update_wheel_states(self): - """ - 更新车轮状态 - """ - for i, wheel_info in enumerate(self.wheels): - wheel = wheel_info['wheel'] - - # 更新车轮旋转 - wheel_info['rotation'] = wheel.getWheelRotation() - - # 更新车轮转速 - wheel_info['rpm'] = wheel.getWheelRpm() - - # 更新滑移信息 - wheel_info['skid_info'] = wheel.getSkidInfo() - - # 更新悬挂信息 - wheel_info['suspension_relative_velocity'] = wheel.getSuspensionRelativeVelocity() - wheel_info['clipped_inv_contact_dot_suspension'] = wheel.getClippedInvContactDotSuspension() - wheel_info['suspension_force'] = wheel.getWheelsSuspensionForce() - - # 更新冲量信息 - wheel_info['side_impulse'] = wheel.getSideImpulse() - wheel_info['forward_impulse'] = wheel.getForwardImpulse() - - # 计算能量损耗(用于磨损计算) - skid_energy = abs(wheel_info['skid_info']) * abs(wheel_info['side_impulse']) - slip_energy = abs(wheel.getWheelRotation()) * abs(wheel_info['forward_impulse']) - wheel_info['skid_energy'] = skid_energy - wheel_info['slip_energy'] = slip_energy - - # 更新车轮温度(简化) - energy_input = skid_energy + slip_energy - wheel_info['temperature'] += energy_input * 0.001 * 0.1 # 简化的热模型 - wheel_info['temperature'] = max(20.0, wheel_info['temperature'] * 0.99) # 冷却 - - # 更新车轮磨损 - wear_rate = (skid_energy + slip_energy) * 0.0001 - wheel_info['wear'] = min(1.0, wheel_info['wear'] + wear_rate) - - # 检查是否产生平点 - if wheel_info['temperature'] > 100.0 and wheel_info['wear'] > 0.5: - wheel_info['flat_spot'] = True - - def _update_fuel_consumption(self, dt): - """ - 更新燃料消耗 - - Args: - dt (float): 时间增量 - """ - # 基础燃料消耗(怠速) - base_consumption = 0.001 # 升/秒 - - # 负荷燃料消耗 - load_consumption = abs(self.engine_force) * self.fuel_consumption_rate - - # 转速燃料消耗 - rpm_factor = self.rpm / self.config['max_rpm'] - rpm_consumption = rpm_factor * 0.002 - - total_consumption = (base_consumption + load_consumption + rpm_consumption) * dt - self.fuel_level = max(0.0, self.fuel_level - total_consumption) - - def _record_history(self): - """ - 记录历史数据 - """ - current_time = time.time() - self.speed_history.append((self.speed, current_time)) - self.acceleration_history.append((self.acceleration, current_time)) - self.steering_history.append((self.steering, current_time)) - - def get_current_speed_km_hour(self): - """ - 获取当前速度(km/h) - - Returns: - float: 当前速度 - """ - return self.bullet_vehicle.getCurrentSpeedKmHour() - - def get_wheel_info(self, wheel_index): - """ - 获取车轮信息 - - Args: - wheel_index (int): 车轮索引 - - Returns: - dict: 车轮信息 - """ - if 0 <= wheel_index < len(self.wheels): - return self.wheels[wheel_index] - return None - - def get_wheel_rotation(self, wheel_index): - """ - 获取车轮旋转角度 - - Args: - wheel_index (int): 车轮索引 - - Returns: - float: 旋转角度 - """ - if 0 <= wheel_index < len(self.wheels): - return self.wheels[wheel_index]['wheel'].getWheelRotation() - return 0.0 - - def get_wheel_rpm(self, wheel_index): - """ - 获取车轮转速(RPM) - - Args: - wheel_index (int): 车轮索引 - - Returns: - float: 转速(RPM) - """ - if 0 <= wheel_index < len(self.wheels): - return self.wheels[wheel_index]['wheel'].getWheelRpm() - return 0.0 - - def get_wheel_skid_info(self, wheel_index): - """ - 获取车轮滑移信息 - - Args: - wheel_index (int): 车轮索引 - - Returns: - float: 滑移信息 - """ - if 0 <= wheel_index < len(self.wheels): - return self.wheels[wheel_index]['wheel'].getSkidInfo() - return 0.0 - - def set_vehicle_parameters(self, max_engine_force=None, max_brake_force=None, - max_steering=None, steering_sensitivity=None): - """ - 设置车辆参数 - - Args: - max_engine_force (float): 最大引擎力 - max_brake_force (float): 最大刹车力 - max_steering (float): 最大转向角度 - steering_sensitivity (float): 转向灵敏度 - """ - if max_engine_force is not None: - self.max_engine_force = max_engine_force - if max_brake_force is not None: - self.max_brake_force = max_brake_force - if max_steering is not None: - self.max_steering = max_steering - if steering_sensitivity is not None: - self.steering_sensitivity = steering_sensitivity - - def apply_handbrake(self, force=1.0): - """ - 应用手刹 - - Args: - force (float): 手刹力度 (0.0 到 1.0) - """ - self.set_handbrake_force(force) - - def reset_controls(self): - """ - 重置控制状态 - """ - self.steering = 0.0 - self.target_steering = 0.0 - self.engine_force = 0.0 - self.brake_force = 0.0 - self.handbrake_force = 0.0 - - # 重置所有车轮控制 - for wheel_info in self.wheels: - wheel_info['wheel'].setSteering(0.0) - wheel_info['wheel'].setEngineForce(0.0) - wheel_info['wheel'].setBrake(0.0) - - def get_vehicle_state(self): - """ - 获取车辆状态 - - Returns: - dict: 车辆状态信息 - """ - return { - 'speed': self.speed, - 'speed_kmh': self.speed_kmh, - 'acceleration': self.acceleration, - 'lateral_acceleration': self.lateral_acceleration, - 'yaw_rate': self.yaw_rate, - 'pitch': self.pitch, - 'roll': self.roll, - 'steering': self.steering, - 'engine_force': self.engine_force, - 'brake_force': self.brake_force, - 'handbrake_force': self.handbrake_force, - 'gear': self.gear, - 'rpm': self.rpm, - 'fuel_level': self.fuel_level, - 'fuel_percentage': self.fuel_level / self.fuel_capacity * 100, - 'damage': self.damage.copy(), - } - - def get_engine_torque(self): - """ - 获取发动机扭矩 - - Returns: - float: 发动机扭矩 - """ - torque_curve = self.config['engine_torque_curve'] - - # 在扭矩曲线上插值 - for i in range(len(torque_curve) - 1): - rpm1, torque1 = torque_curve[i] - rpm2, torque2 = torque_curve[i + 1] - - if rpm1 <= self.rpm <= rpm2: - t = (self.rpm - rpm1) / (rpm2 - rpm1) if rpm2 != rpm1 else 0 - return torque1 + (torque2 - torque1) * t - - # 如果超出范围,返回最近点的扭矩 - if self.rpm < torque_curve[0][0]: - return torque_curve[0][1] - else: - return torque_curve[-1][1] - - def get_engine_power(self): - """ - 获取发动机功率(马力) - - Returns: - float: 发动机功率(马力) - """ - torque = self.get_engine_torque() - # 功率 = 扭矩 * 转速 / 常数 - power_kw = torque * self.rpm / 9549.3 # 转换为千瓦 - power_hp = power_kw * 1.34102 # 转换为马力 - return power_hp - - def apply_damage(self, component, amount): - """ - 应用车辆损伤 - - Args: - component (str): 组件名称 - amount (float): 损伤量 (0.0 到 1.0) - """ - if component in self.damage: - if isinstance(self.damage[component], list): - # 车轮损伤 - for i in range(len(self.damage[component])): - self.damage[component][i] = min(1.0, self.damage[component][i] + amount) - else: - # 其他组件损伤 - self.damage[component] = min(1.0, self.damage[component] + amount) - - def repair_component(self, component, amount): - """ - 修复车辆组件 - - Args: - component (str): 组件名称 - amount (float): 修复量 (0.0 到 1.0) - """ - if component in self.damage: - if isinstance(self.damage[component], list): - # 车轮修复 - for i in range(len(self.damage[component])): - self.damage[component][i] = max(0.0, self.damage[component][i] - amount) - else: - # 其他组件修复 - self.damage[component] = max(0.0, self.damage[component] - amount) - - def refuel(self, amount): - """ - 加油 - - Args: - amount (float): 燃料量(升) - """ - self.fuel_level = min(self.fuel_capacity, self.fuel_level + amount) - - def get_performance_stats(self): - """ - 获取性能统计 - - Returns: - dict: 性能统计信息 - """ - # 计算平均速度 - if len(self.speed_history) > 0: - avg_speed = sum([speed for speed, _ in self.speed_history]) / len(self.speed_history) - else: - avg_speed = 0.0 - - # 计算最大速度 - max_speed = max([speed for speed, _ in self.speed_history]) if len(self.speed_history) > 0 else 0.0 - - # 计算平均加速度 - if len(self.acceleration_history) > 0: - avg_acceleration = sum([acc for acc, _ in self.acceleration_history]) / len(self.acceleration_history) - else: - avg_acceleration = 0.0 - - return { - 'current_speed_kmh': self.speed_kmh, - 'average_speed_kmh': avg_speed * 3.6, - 'max_speed_kmh': max_speed * 3.6, - 'average_acceleration': avg_acceleration, - 'engine_rpm': self.rpm, - 'engine_power_hp': self.get_engine_power(), - 'engine_torque_nm': self.get_engine_torque(), - 'fuel_level_l': self.fuel_level, - 'fuel_percentage': self.fuel_level / self.fuel_capacity * 100, - 'gear': self.gear, - 'steering_input': self.steering, - 'vehicle_age': time.time() - self.creation_time, - 'wheel_count': len(self.wheels), - 'damage_stats': self.damage.copy(), - } - - def enable_anti_lock_brake(self, enabled=True): - """ - 启用或禁用防抱死刹车 - - Args: - enabled (bool): 是否启用 - """ - self.anti_lock_brake_enabled = enabled - - def enable_traction_control(self, enabled=True): - """ - 启用或禁用牵引力控制 - - Args: - enabled (bool): 是否启用 - """ - self.traction_control_enabled = enabled - - def enable_stability_control(self, enabled=True): - """ - 启用或禁用车身稳定控制 - - Args: - enabled (bool): 是否启用 - """ - self.stability_control_enabled = enabled - - def destroy(self): - """ - 销毁车辆 - """ - # 重置控制 - self.reset_controls() - - # 从物理世界移除 - if self.physics_world: - self.physics_world.remove_vehicle(self) - - # 清空车轮列表 - self.wheels.clear() - self.wheel_info.clear() - - # 清空历史数据 - self.speed_history.clear() - self.acceleration_history.clear() - self.steering_history.clear() - +""" +车辆系统模块 +负责车辆物理的创建和管理 +""" + +from panda3d.core import Vec3, Point3, LVector3f +from panda3d.bullet import BulletVehicle, BulletWheel, BulletRigidBodyNode +import math +import time +from collections import deque + + +class VehicleSystem: + """ + 车辆系统类 + 管理车辆的物理特性和控制 + """ + + def __init__(self, physics_world, chassis_body, vehicle_config=None): + """ + 初始化车辆系统 + + Args: + physics_world (PhysicsWorld): 物理世界对象 + chassis_body (RigidBody): 底盘刚体 + vehicle_config (dict): 车辆配置参数 + """ + self.physics_world = physics_world + self.chassis_body = chassis_body + + # 车辆配置 + self.config = { + 'max_engine_force': 3000.0, + 'max_brake_force': 100.0, + 'max_steering': math.pi / 4, # 45度 + 'steering_return_speed': 2.0, # 转向回正速度 + 'steering_sensitivity': 1.0, # 转向灵敏度 + 'center_of_mass_offset': Vec3(0, 0, -0.5), # 质心偏移 + 'suspension_stiffness': 50.0, + 'suspension_damping': 2.3, + 'suspension_compression': 4.4, + 'suspension_rest_length': 0.5, + 'roll_influence': 0.1, + 'friction_slip': 100.0, + 'wheel_radius': 0.3, + 'wheel_width': 0.2, + 'wheel_mass': 1.0, + 'anti_roll_bar_force': 100.0, # 防倾杆力 + 'aerodynamic_downforce': 0.0, # 空气动力学下压力 + 'aerodynamic_drag': 0.0, # 空气动力学阻力 + 'differential_type': 'open', # 差速器类型: open, locked, limited_slip + 'gear_ratios': [2.66, 1.78, 1.30, 1.00, 0.74, 0.50], # 齿轮比 + 'final_drive_ratio': 3.42, # 最终传动比 + 'max_rpm': 7000, # 最大转速 + 'idle_rpm': 800, # 怠速转速 + 'engine_torque_curve': [(0, 0), (1000, 200), (2000, 300), (3000, 350), + (4000, 380), (5000, 350), (6000, 300), (7000, 200)], # 扭矩曲线 + } + + # 应用自定义配置 + if vehicle_config: + self.config.update(vehicle_config) + + # 创建Bullet车辆 + self.bullet_vehicle = BulletVehicle( + physics_world.bullet_world.getBroadphase(), + chassis_body.bullet_body + ) + self.bullet_vehicle.setCoordinateSystem(0, 1, 2) # Right, Up, Forward + + # 车轮列表 + self.wheels = [] + self.wheel_info = [] # 详细的车轮信息 + + # 车辆参数 + self.max_engine_force = self.config['max_engine_force'] + self.max_brake_force = self.config['max_brake_force'] + self.max_steering = self.config['max_steering'] + self.steering_return_speed = self.config['steering_return_speed'] + self.steering_sensitivity = self.config['steering_sensitivity'] + + # 控制状态 + self.steering = 0.0 + self.target_steering = 0.0 + self.engine_force = 0.0 + self.brake_force = 0.0 + self.handbrake_force = 0.0 + self.gear = 1 # 当前档位 + self.rpm = self.config['idle_rpm'] # 发动机转速 + self.clutch_engaged = True # 离合器状态 + + # 车辆状态 + self.speed = 0.0 + self.speed_kmh = 0.0 + self.acceleration = 0.0 + self.lateral_acceleration = 0.0 + self.yaw_rate = 0.0 + self.pitch = 0.0 + self.roll = 0.0 + + # 历史数据 + self.speed_history = deque(maxlen=100) + self.acceleration_history = deque(maxlen=100) + self.steering_history = deque(maxlen=100) + + # 高级特性 + self.anti_lock_brake_enabled = True + self.traction_control_enabled = True + self.stability_control_enabled = True + self.abs_threshold = 0.8 # ABS触发阈值 + self.tc_slip_threshold = 0.2 # 牵引力控制滑移阈值 + + # 车辆损伤系统 + self.damage = { + 'engine': 0.0, # 发动机损伤 + 'transmission': 0.0, # 传动系统损伤 + 'suspension': 0.0, # 悬挂损伤 + 'wheels': [0.0] * 4, # 车轮损伤 + 'body': 0.0, # 车身损伤 + } + + # 燃料系统 + self.fuel_capacity = 60.0 # 升 + self.fuel_level = 60.0 # 升 + self.fuel_consumption_rate = 0.01 # 升/秒/单位力 + + # 时间戳 + self.creation_time = time.time() + self.last_update_time = self.creation_time + + # 添加到物理世界 + physics_world.add_vehicle(self) + + # 设置底盘质心偏移 + self._set_center_of_mass() + + print("车辆系统初始化完成") + + def _set_center_of_mass(self): + """ + 设置底盘质心偏移 + """ + com_offset = self.config['center_of_mass_offset'] + if com_offset.length() > 0: + self.chassis_body.bullet_body.setCenterOfMassTransform( + TransformState.makePos(com_offset) + ) + + def add_wheel(self, connection_point, wheel_direction, wheel_axle, wheel_radius=None, + suspension_rest_length=None, suspension_stiffness=None, + damping_relaxation=None, damping_compression=None, + friction_slip=None, roll_influence=None, wheel_width=None, + wheel_mass=None, is_front_wheel=True, is_steered_wheel=True, + is_driven_wheel=True, is_braked_wheel=True): + """ + 添加车轮 + + Args: + connection_point (Point3): 底盘连接点 + wheel_direction (Vec3): 车轮方向(通常向下) + wheel_axle (Vec3): 车轮轴向(通常向右) + wheel_radius (float): 车轮半径 + suspension_rest_length (float): 悬挂静止长度 + suspension_stiffness (float): 悬挂刚度 + damping_relaxation (float): 放松阻尼 + damping_compression (float): 压缩阻尼 + friction_slip (float): 滑动摩擦 + roll_influence (float): 翻滚影响 + wheel_width (float): 车轮宽度 + wheel_mass (float): 车轮质量 + is_front_wheel (bool): 是否为前轮 + is_steered_wheel (bool): 是否为转向轮 + is_driven_wheel (bool): 是否为驱动轮 + is_braked_wheel (bool): 是否为刹车轮 + + Returns: + BulletWheel: 车轮对象 + """ + # 使用配置参数或默认值 + wheel_radius = wheel_radius or self.config['wheel_radius'] + suspension_rest_length = suspension_rest_length or self.config['suspension_rest_length'] + suspension_stiffness = suspension_stiffness or self.config['suspension_stiffness'] + damping_relaxation = damping_relaxation or self.config['suspension_damping'] + damping_compression = damping_compression or self.config['suspension_compression'] + friction_slip = friction_slip or self.config['friction_slip'] + roll_influence = roll_influence or self.config['roll_influence'] + wheel_width = wheel_width or self.config['wheel_width'] + wheel_mass = wheel_mass or self.config['wheel_mass'] + + wheel = self.bullet_vehicle.createWheel() + + # 设置车轮参数 + wheel.setChassisConnectionPointCs(connection_point) + wheel.setWheelDirectionCs(wheel_direction) + wheel.setWheelAxleCs(wheel_axle) + wheel.setWheelRadius(wheel_radius) + wheel.setWheelWidth(wheel_width) + wheel.setMaxSuspensionTravelCm(500.0) + wheel.setSuspensionStiffness(suspension_stiffness) + wheel.setWheelsDampingRelaxation(damping_relaxation) + wheel.setWheelsDampingCompression(damping_compression) + wheel.setFrictionSlip(friction_slip) + wheel.setRollInfluence(roll_influence) + wheel.setSteering(is_steered_wheel) + wheel.setDriving(is_driven_wheel) + wheel.setBraking(is_braked_wheel) + + # 创建车轮刚体 + wheel_shape = BulletCylinderShape(wheel_radius, wheel_width, 1) # Y轴 + wheel_body = BulletRigidBodyNode(f'Wheel_{len(self.wheels)}') + wheel_body.addShape(wheel_shape) + wheel_body.setMass(wheel_mass) + + # 保存车轮信息 + wheel_info = { + 'wheel': wheel, + 'body': wheel_body, + 'connection_point': connection_point, + 'direction': wheel_direction, + 'axle': wheel_axle, + 'radius': wheel_radius, + 'width': wheel_width, + 'mass': wheel_mass, + 'properties': { + 'suspension_rest_length': suspension_rest_length, + 'suspension_stiffness': suspension_stiffness, + 'damping_relaxation': damping_relaxation, + 'damping_compression': damping_compression, + 'friction_slip': friction_slip, + 'roll_influence': roll_influence + }, + 'is_front_wheel': is_front_wheel, + 'is_steered_wheel': is_steered_wheel, + 'is_driven_wheel': is_driven_wheel, + 'is_braked_wheel': is_braked_wheel, + 'rotation': 0.0, + 'rpm': 0.0, + 'skid_info': 0.0, + 'suspension_relative_velocity': 0.0, + 'clipped_inv_contact_dot_suspension': 0.0, + 'suspension_force': 0.0, + 'side_impulse': 0.0, + 'forward_impulse': 0.0, + 'side_friction_stiffness': 1.0, + 'forward_friction_stiffness': 1.0, + 'skid_energy': 0.0, + 'slip_energy': 0.0, + 'temperature': 20.0, # 车轮温度 + 'wear': 0.0, # 车轮磨损 + 'flat_spot': False, # 是否有平点 + } + + self.wheels.append(wheel_info) + self.wheel_info.append(wheel_info) + + # 更新车轮损伤数组大小 + while len(self.damage['wheels']) < len(self.wheels): + self.damage['wheels'].append(0.0) + + return wheel + + def set_steering(self, steering): + """ + 设置转向 + + Args: + steering (float): 转向值 (-1.0 到 1.0) + """ + self.target_steering = max(-1.0, min(1.0, steering)) + + def set_engine_force(self, force): + """ + 设置引擎力 + + Args: + force (float): 引擎力 (-1.0 到 1.0) + """ + self.engine_force = max(-1.0, min(1.0, force)) + + def set_brake_force(self, force): + """ + 设置刹车力 + + Args: + force (float): 刹车力 (0.0 到 1.0) + """ + self.brake_force = max(0.0, min(1.0, force)) + + def set_handbrake_force(self, force): + """ + 设置手刹力 + + Args: + force (float): 手刹力 (0.0 到 1.0) + """ + self.handbrake_force = max(0.0, min(1.0, force)) + + def shift_up(self): + """ + 升档 + """ + if self.gear < len(self.config['gear_ratios']): + self.gear += 1 + + def shift_down(self): + """ + 降档 + """ + if self.gear > 1: + self.gear -= 1 + + def set_gear(self, gear): + """ + 设置档位 + + Args: + gear (int): 档位 + """ + self.gear = max(1, min(len(self.config['gear_ratios']), gear)) + + def update(self, dt): + """ + 更新车辆状态 + + Args: + dt (float): 时间增量 + """ + current_time = time.time() + self.last_update_time = current_time + + # 平滑转向控制 + steering_diff = self.target_steering - self.steering + if abs(steering_diff) > 0.01: + steering_change = steering_diff * self.steering_sensitivity * dt * 10 + self.steering += steering_change + else: + # 转向回正 + if abs(self.steering) > 0.01: + self.steering -= self.steering * self.steering_return_speed * dt + else: + self.steering = 0.0 + + # 应用转向到前轮 + self._apply_steering() + + # 计算发动机转速 + self._calculate_engine_rpm() + + # 应用引擎力和刹车力 + self._apply_forces_and_brakes(dt) + + # 应用手刹 + self._apply_handbrake() + + # 更新车辆状态 + self._update_vehicle_state(dt) + + # 更新车轮状态 + self._update_wheel_states() + + # 更新燃料消耗 + self._update_fuel_consumption(dt) + + # 记录历史数据 + self._record_history() + + def _apply_steering(self): + """ + 应用转向 + """ + steering_angle = self.steering * self.max_steering + + # 应用转向到转向轮(假设前两个轮子是前轮) + for i, wheel_info in enumerate(self.wheels): + if wheel_info['is_steered_wheel']: + wheel_info['wheel'].setSteering(steering_angle) + + def _calculate_engine_rpm(self): + """ + 计算发动机转速 + """ + # 简化的转速计算 + wheel_rpm = 0.0 + driven_wheels = 0 + + for wheel_info in self.wheels: + if wheel_info['is_driven_wheel']: + wheel_rpm += abs(wheel_info['rpm']) + driven_wheels += 1 + + if driven_wheels > 0: + avg_wheel_rpm = wheel_rpm / driven_wheels + # 考虑当前档位和最终传动比 + gear_ratio = self.config['gear_ratios'][self.gear - 1] + final_ratio = self.config['final_drive_ratio'] + self.rpm = avg_wheel_rpm * gear_ratio * final_ratio + + # 限制转速范围 + self.rpm = max(self.config['idle_rpm'], min(self.config['max_rpm'], self.rpm)) + + def _apply_forces_and_brakes(self, dt): + """ + 应用引擎力和刹车力 + + Args: + dt (float): 时间增量 + """ + # 计算实际引擎力(考虑损伤和燃料) + actual_engine_force = self.engine_force * self.max_engine_force + if self.damage['engine'] > 0.5: + actual_engine_force *= (1.0 - self.damage['engine'] * 0.8) + if self.fuel_level <= 0: + actual_engine_force = 0.0 + + # 计算实际刹车力 + actual_brake_force = self.brake_force * self.max_brake_force + + # 应用牵引力控制 + if self.traction_control_enabled: + actual_engine_force = self._apply_traction_control(actual_engine_force, dt) + + # 应用到驱动轮 + for i, wheel_info in enumerate(self.wheels): + if wheel_info['is_driven_wheel']: + wheel_info['wheel'].setEngineForce(actual_engine_force) + if wheel_info['is_braked_wheel']: + wheel_info['wheel'].setBrake(actual_brake_force) + + def _apply_traction_control(self, engine_force, dt): + """ + 应用牵引力控制 + + Args: + engine_force (float): 引擎力 + dt (float): 时间增量 + + Returns: + float: 调整后的引擎力 + """ + # 检查是否有车轮打滑 + slip_detected = False + for wheel_info in self.wheels: + if wheel_info['is_driven_wheel']: + slip_ratio = wheel_info['skid_info'] + if slip_ratio > self.tc_slip_threshold: + slip_detected = True + break + + # 如果检测到打滑,减少引擎力 + if slip_detected: + return engine_force * 0.5 + + return engine_force + + def _apply_handbrake(self): + """ + 应用手刹 + """ + handbrake_force = self.handbrake_force * self.max_brake_force * 2 # 手刹力度更大 + + # 应用到后轮(假设后两个轮子是后轮) + for i, wheel_info in enumerate(self.wheels): + if not wheel_info['is_front_wheel']: # 后轮 + wheel_info['wheel'].setBrake( + wheel_info['wheel'].getBrake() + handbrake_force + ) + + def _update_vehicle_state(self, dt): + """ + 更新车辆状态 + + Args: + dt (float): 时间增量 + """ + # 获取当前速度 + linear_velocity = self.chassis_body.get_linear_velocity() + self.speed = linear_velocity.length() + self.speed_kmh = self.speed * 3.6 # 转换为km/h + + # 计算加速度 + if len(self.speed_history) > 0: + prev_speed = self.speed_history[-1][0] + time_diff = dt + if time_diff > 0: + self.acceleration = (self.speed - prev_speed) / time_diff + else: + self.acceleration = 0.0 + + # 计算横向加速度 + # 这里需要更复杂的计算,简化处理 + self.lateral_acceleration = 0.0 + + # 计算横摆角速度 + angular_velocity = self.chassis_body.get_angular_velocity() + self.yaw_rate = angular_velocity.getZ() + + # 计算俯仰和翻滚 + rotation = self.chassis_body.get_rotation() + self.pitch = rotation.getY() + self.roll = rotation.getX() + + # 应用空气动力学效应 + self._apply_aerodynamics(linear_velocity, dt) + + # 应用防倾杆力 + self._apply_anti_roll_bars() + + def _apply_aerodynamics(self, velocity, dt): + """ + 应用空气动力学效应 + + Args: + velocity (Vec3): 速度向量 + dt (float): 时间增量 + """ + speed = velocity.length() + if speed > 0: + # 下压力(随速度平方增加) + downforce = self.config['aerodynamic_downforce'] * speed * speed + if downforce > 0: + downforce_vector = Vec3(0, 0, -downforce) + self.chassis_body.apply_force(downforce_vector) + + # 空气阻力 + drag_coefficient = self.config['aerodynamic_drag'] + drag_force = drag_coefficient * speed * speed + if drag_force > 0: + drag_direction = -velocity.normalized() + drag_vector = drag_direction * drag_force + self.chassis_body.apply_force(drag_vector) + + def _apply_anti_roll_bars(self): + """ + 应用防倾杆力 + """ + if len(self.wheels) >= 4: + # 简化的防倾杆实现 + front_left = self.wheels[0]['wheel'].getSuspensionRelativeVelocity() + front_right = self.wheels[1]['wheel'].getSuspensionRelativeVelocity() + rear_left = self.wheels[2]['wheel'].getSuspensionRelativeVelocity() + rear_right = self.wheels[3]['wheel'].getSuspensionRelativeVelocity() + + # 前轴防倾 + front_roll = (front_left - front_right) * self.config['anti_roll_bar_force'] + # 后轴防倾 + rear_roll = (rear_left - rear_right) * self.config['anti_roll_bar_force'] + + # 这里应该应用力到车身上,简化处理 + + def _update_wheel_states(self): + """ + 更新车轮状态 + """ + for i, wheel_info in enumerate(self.wheels): + wheel = wheel_info['wheel'] + + # 更新车轮旋转 + wheel_info['rotation'] = wheel.getWheelRotation() + + # 更新车轮转速 + wheel_info['rpm'] = wheel.getWheelRpm() + + # 更新滑移信息 + wheel_info['skid_info'] = wheel.getSkidInfo() + + # 更新悬挂信息 + wheel_info['suspension_relative_velocity'] = wheel.getSuspensionRelativeVelocity() + wheel_info['clipped_inv_contact_dot_suspension'] = wheel.getClippedInvContactDotSuspension() + wheel_info['suspension_force'] = wheel.getWheelsSuspensionForce() + + # 更新冲量信息 + wheel_info['side_impulse'] = wheel.getSideImpulse() + wheel_info['forward_impulse'] = wheel.getForwardImpulse() + + # 计算能量损耗(用于磨损计算) + skid_energy = abs(wheel_info['skid_info']) * abs(wheel_info['side_impulse']) + slip_energy = abs(wheel.getWheelRotation()) * abs(wheel_info['forward_impulse']) + wheel_info['skid_energy'] = skid_energy + wheel_info['slip_energy'] = slip_energy + + # 更新车轮温度(简化) + energy_input = skid_energy + slip_energy + wheel_info['temperature'] += energy_input * 0.001 * 0.1 # 简化的热模型 + wheel_info['temperature'] = max(20.0, wheel_info['temperature'] * 0.99) # 冷却 + + # 更新车轮磨损 + wear_rate = (skid_energy + slip_energy) * 0.0001 + wheel_info['wear'] = min(1.0, wheel_info['wear'] + wear_rate) + + # 检查是否产生平点 + if wheel_info['temperature'] > 100.0 and wheel_info['wear'] > 0.5: + wheel_info['flat_spot'] = True + + def _update_fuel_consumption(self, dt): + """ + 更新燃料消耗 + + Args: + dt (float): 时间增量 + """ + # 基础燃料消耗(怠速) + base_consumption = 0.001 # 升/秒 + + # 负荷燃料消耗 + load_consumption = abs(self.engine_force) * self.fuel_consumption_rate + + # 转速燃料消耗 + rpm_factor = self.rpm / self.config['max_rpm'] + rpm_consumption = rpm_factor * 0.002 + + total_consumption = (base_consumption + load_consumption + rpm_consumption) * dt + self.fuel_level = max(0.0, self.fuel_level - total_consumption) + + def _record_history(self): + """ + 记录历史数据 + """ + current_time = time.time() + self.speed_history.append((self.speed, current_time)) + self.acceleration_history.append((self.acceleration, current_time)) + self.steering_history.append((self.steering, current_time)) + + def get_current_speed_km_hour(self): + """ + 获取当前速度(km/h) + + Returns: + float: 当前速度 + """ + return self.bullet_vehicle.getCurrentSpeedKmHour() + + def get_wheel_info(self, wheel_index): + """ + 获取车轮信息 + + Args: + wheel_index (int): 车轮索引 + + Returns: + dict: 车轮信息 + """ + if 0 <= wheel_index < len(self.wheels): + return self.wheels[wheel_index] + return None + + def get_wheel_rotation(self, wheel_index): + """ + 获取车轮旋转角度 + + Args: + wheel_index (int): 车轮索引 + + Returns: + float: 旋转角度 + """ + if 0 <= wheel_index < len(self.wheels): + return self.wheels[wheel_index]['wheel'].getWheelRotation() + return 0.0 + + def get_wheel_rpm(self, wheel_index): + """ + 获取车轮转速(RPM) + + Args: + wheel_index (int): 车轮索引 + + Returns: + float: 转速(RPM) + """ + if 0 <= wheel_index < len(self.wheels): + return self.wheels[wheel_index]['wheel'].getWheelRpm() + return 0.0 + + def get_wheel_skid_info(self, wheel_index): + """ + 获取车轮滑移信息 + + Args: + wheel_index (int): 车轮索引 + + Returns: + float: 滑移信息 + """ + if 0 <= wheel_index < len(self.wheels): + return self.wheels[wheel_index]['wheel'].getSkidInfo() + return 0.0 + + def set_vehicle_parameters(self, max_engine_force=None, max_brake_force=None, + max_steering=None, steering_sensitivity=None): + """ + 设置车辆参数 + + Args: + max_engine_force (float): 最大引擎力 + max_brake_force (float): 最大刹车力 + max_steering (float): 最大转向角度 + steering_sensitivity (float): 转向灵敏度 + """ + if max_engine_force is not None: + self.max_engine_force = max_engine_force + if max_brake_force is not None: + self.max_brake_force = max_brake_force + if max_steering is not None: + self.max_steering = max_steering + if steering_sensitivity is not None: + self.steering_sensitivity = steering_sensitivity + + def apply_handbrake(self, force=1.0): + """ + 应用手刹 + + Args: + force (float): 手刹力度 (0.0 到 1.0) + """ + self.set_handbrake_force(force) + + def reset_controls(self): + """ + 重置控制状态 + """ + self.steering = 0.0 + self.target_steering = 0.0 + self.engine_force = 0.0 + self.brake_force = 0.0 + self.handbrake_force = 0.0 + + # 重置所有车轮控制 + for wheel_info in self.wheels: + wheel_info['wheel'].setSteering(0.0) + wheel_info['wheel'].setEngineForce(0.0) + wheel_info['wheel'].setBrake(0.0) + + def get_vehicle_state(self): + """ + 获取车辆状态 + + Returns: + dict: 车辆状态信息 + """ + return { + 'speed': self.speed, + 'speed_kmh': self.speed_kmh, + 'acceleration': self.acceleration, + 'lateral_acceleration': self.lateral_acceleration, + 'yaw_rate': self.yaw_rate, + 'pitch': self.pitch, + 'roll': self.roll, + 'steering': self.steering, + 'engine_force': self.engine_force, + 'brake_force': self.brake_force, + 'handbrake_force': self.handbrake_force, + 'gear': self.gear, + 'rpm': self.rpm, + 'fuel_level': self.fuel_level, + 'fuel_percentage': self.fuel_level / self.fuel_capacity * 100, + 'damage': self.damage.copy(), + } + + def get_engine_torque(self): + """ + 获取发动机扭矩 + + Returns: + float: 发动机扭矩 + """ + torque_curve = self.config['engine_torque_curve'] + + # 在扭矩曲线上插值 + for i in range(len(torque_curve) - 1): + rpm1, torque1 = torque_curve[i] + rpm2, torque2 = torque_curve[i + 1] + + if rpm1 <= self.rpm <= rpm2: + t = (self.rpm - rpm1) / (rpm2 - rpm1) if rpm2 != rpm1 else 0 + return torque1 + (torque2 - torque1) * t + + # 如果超出范围,返回最近点的扭矩 + if self.rpm < torque_curve[0][0]: + return torque_curve[0][1] + else: + return torque_curve[-1][1] + + def get_engine_power(self): + """ + 获取发动机功率(马力) + + Returns: + float: 发动机功率(马力) + """ + torque = self.get_engine_torque() + # 功率 = 扭矩 * 转速 / 常数 + power_kw = torque * self.rpm / 9549.3 # 转换为千瓦 + power_hp = power_kw * 1.34102 # 转换为马力 + return power_hp + + def apply_damage(self, component, amount): + """ + 应用车辆损伤 + + Args: + component (str): 组件名称 + amount (float): 损伤量 (0.0 到 1.0) + """ + if component in self.damage: + if isinstance(self.damage[component], list): + # 车轮损伤 + for i in range(len(self.damage[component])): + self.damage[component][i] = min(1.0, self.damage[component][i] + amount) + else: + # 其他组件损伤 + self.damage[component] = min(1.0, self.damage[component] + amount) + + def repair_component(self, component, amount): + """ + 修复车辆组件 + + Args: + component (str): 组件名称 + amount (float): 修复量 (0.0 到 1.0) + """ + if component in self.damage: + if isinstance(self.damage[component], list): + # 车轮修复 + for i in range(len(self.damage[component])): + self.damage[component][i] = max(0.0, self.damage[component][i] - amount) + else: + # 其他组件修复 + self.damage[component] = max(0.0, self.damage[component] - amount) + + def refuel(self, amount): + """ + 加油 + + Args: + amount (float): 燃料量(升) + """ + self.fuel_level = min(self.fuel_capacity, self.fuel_level + amount) + + def get_performance_stats(self): + """ + 获取性能统计 + + Returns: + dict: 性能统计信息 + """ + # 计算平均速度 + if len(self.speed_history) > 0: + avg_speed = sum([speed for speed, _ in self.speed_history]) / len(self.speed_history) + else: + avg_speed = 0.0 + + # 计算最大速度 + max_speed = max([speed for speed, _ in self.speed_history]) if len(self.speed_history) > 0 else 0.0 + + # 计算平均加速度 + if len(self.acceleration_history) > 0: + avg_acceleration = sum([acc for acc, _ in self.acceleration_history]) / len(self.acceleration_history) + else: + avg_acceleration = 0.0 + + return { + 'current_speed_kmh': self.speed_kmh, + 'average_speed_kmh': avg_speed * 3.6, + 'max_speed_kmh': max_speed * 3.6, + 'average_acceleration': avg_acceleration, + 'engine_rpm': self.rpm, + 'engine_power_hp': self.get_engine_power(), + 'engine_torque_nm': self.get_engine_torque(), + 'fuel_level_l': self.fuel_level, + 'fuel_percentage': self.fuel_level / self.fuel_capacity * 100, + 'gear': self.gear, + 'steering_input': self.steering, + 'vehicle_age': time.time() - self.creation_time, + 'wheel_count': len(self.wheels), + 'damage_stats': self.damage.copy(), + } + + def enable_anti_lock_brake(self, enabled=True): + """ + 启用或禁用防抱死刹车 + + Args: + enabled (bool): 是否启用 + """ + self.anti_lock_brake_enabled = enabled + + def enable_traction_control(self, enabled=True): + """ + 启用或禁用牵引力控制 + + Args: + enabled (bool): 是否启用 + """ + self.traction_control_enabled = enabled + + def enable_stability_control(self, enabled=True): + """ + 启用或禁用车身稳定控制 + + Args: + enabled (bool): 是否启用 + """ + self.stability_control_enabled = enabled + + def destroy(self): + """ + 销毁车辆 + """ + # 重置控制 + self.reset_controls() + + # 从物理世界移除 + if self.physics_world: + self.physics_world.remove_vehicle(self) + + # 清空车轮列表 + self.wheels.clear() + self.wheel_info.clear() + + # 清空历史数据 + self.speed_history.clear() + self.acceleration_history.clear() + self.steering_history.clear() + print("车辆系统已销毁") \ No newline at end of file diff --git a/plugins/user/server_architecture/README.md b/plugins/user/server_architecture/README.md index 60b84b2f..c9327659 100644 --- a/plugins/user/server_architecture/README.md +++ b/plugins/user/server_architecture/README.md @@ -1,321 +1,321 @@ -# 服务器架构插件 - -## 概述 - -服务器架构插件为EG引擎提供了完整的服务器架构支持,包括服务器管理、客户端管理、消息路由、房间管理、数据存储、负载均衡、监控和安全等功能。该插件采用模块化设计,具有高性能、可扩展和易配置的特点。 - -## 功能特性 - -### 1. 核心功能 -- **服务器管理**: 完整的服务器启动、停止和状态管理 -- **客户端管理**: 客户端连接、认证、断开和权限管理 -- **消息路由**: 高效的消息传递和路由系统 -- **房间管理**: 多房间支持和房间状态管理 -- **数据存储**: 持久化数据存储和缓存管理 -- **负载均衡**: 多服务器实例间的负载分配 -- **监控系统**: 实时性能监控和日志记录 -- **安全系统**: 认证、授权和攻击防护 -- **API接口**: RESTful API供外部系统调用 -- **可视化编辑器**: 图形化配置和监控界面 - -### 2. 高级功能 -- **SSL/TLS加密支持**: 安全的网络通信 -- **消息压缩**: 减少网络带宽使用 -- **心跳检测**: 连接状态监控 -- **速率限制**: 防止滥用和DDoS攻击 -- **会话管理**: 用户会话跟踪和管理 -- **配置管理**: 灵活的配置系统 -- **实时监控**: 性能指标和警报系统 -- **数据备份**: 自动数据备份和恢复 - -## 模块架构 - -``` -server_architecture/ -├── plugin.py # 插件主入口 -├── core/ -│ └── server_manager.py # 服务器管理器 -├── clients/ -│ └── client_manager.py # 客户端管理器 -├── messaging/ -│ └── message_router.py # 消息路由器 -├── rooms/ -│ └── room_manager.py # 房间管理器 -├── storage/ -│ └── storage_manager.py # 数据存储管理器 -├── loadbalancer/ -│ └── load_balancer.py # 负载均衡器 -├── monitoring/ -│ └── monitoring_system.py # 监控系统 -├── config/ -│ └── config_manager.py # 配置管理器 -├── security/ -│ └── security_manager.py # 安全管理器 -├── api/ -│ └── api_interface.py # API接口 -├── editor/ -│ └── editor_interface.py # 编辑器接口 -├── utils/ -│ └── server_utils.py # 工具类 -└── README.md # 说明文档 -``` - -## 安装与配置 - -### 安装依赖 - -```bash -pip install psutil -``` - -### 基本配置 - -在插件配置文件中设置基本参数: - -```python -# 服务器配置 -server_config = { - "host": "0.0.0.0", - "port": 8080, - "max_connections": 1000, - "enable_ssl": False -} - -# 客户端配置 -client_config = { - "enable_authentication": True, - "max_clients": 1000, - "enable_rate_limiting": True -} -``` - -## 使用方法 - -### 1. 初始化插件 - -```python -from plugins.user.server_architecture.plugin import ServerArchitecturePlugin - -# 创建插件实例 -plugin = ServerArchitecturePlugin(engine) - -# 初始化插件 -if plugin.initialize(): - print("插件初始化成功") - -# 启用插件 -if plugin.enable(): - print("插件已启用") -``` - -### 2. 启动服务器 - -```python -# 启动服务器 -if plugin.server_manager.start_server(port=8080): - print("服务器启动成功") -``` - -### 3. 管理客户端 - -```python -# 获取客户端列表 -clients = plugin.client_manager.get_all_clients() - -# 断开客户端连接 -plugin.client_manager.disconnect_client("client_123", "kicked_by_admin") -``` - -### 4. 管理房间 - -```python -# 创建房间 -room_id = plugin.room_manager.create_room("游戏房间1") - -# 添加客户端到房间 -plugin.room_manager.add_client_to_room(room_id, "client_123") - -# 广播消息到房间 -message = {"type": "chat", "content": "Hello everyone!"} -plugin.room_manager.broadcast_to_room(room_id, message) -``` - -## API参考 - -### 主要类和方法 - -#### ServerArchitecturePlugin -- `initialize()`: 初始化插件 -- `enable()`: 启用插件 -- `disable()`: 禁用插件 -- `get_stats()`: 获取插件统计信息 - -#### ServerManager -- `start_server(port)`: 启动服务器 -- `stop_server()`: 停止服务器 -- `get_server_stats()`: 获取服务器统计信息 - -#### ClientManager -- `authenticate_client(client_id, auth_data)`: 认证客户端 -- `disconnect_client(client_id, reason)`: 断开客户端连接 -- `get_all_clients()`: 获取所有客户端信息 - -#### MessageRouter -- `register_message_handler(type, handler)`: 注册消息处理器 -- `send_message(message, client_socket)`: 发送消息到客户端 -- `broadcast_message(message)`: 广播消息到所有客户端 - -#### RoomManager -- `create_room(name, settings)`: 创建房间 -- `destroy_room(room_id)`: 销毁房间 -- `add_client_to_room(room_id, client_id)`: 添加客户端到房间 -- `broadcast_to_room(room_id, message)`: 广播消息到房间 - -#### StorageManager -- `save_user(user_id, user_data)`: 保存用户数据 -- `load_user(user_id)`: 加载用户数据 -- `save_room(room_id, room_data)`: 保存房间数据 - -#### LoadBalancer -- `add_server_instance(instance_id, instance_data)`: 添加服务器实例 -- `remove_server_instance(instance_id)`: 移除服务器实例 -- `route_request(client_ip)`: 路由请求到合适的实例 - -#### MonitoringSystem -- `get_performance_metrics()`: 获取性能指标 -- `get_active_alerts()`: 获取活动警报 -- `resolve_alert(alert_type)`: 解决警报 - -#### ConfigManager -- `get_config(section, key)`: 获取配置值 -- `set_config(section, key, value)`: 设置配置值 -- `load_config()`: 加载配置 -- `save_config()`: 保存配置 - -#### SecurityManager -- `authenticate_client(client_id, auth_data, client_address)`: 认证客户端 -- `validate_token(token)`: 验证令牌 -- `hash_password(password)`: 哈希密码 -- `block_ip(ip_address)`: 阻止IP地址 - -#### APIInterface -- `register_route(method, path, handler)`: 注册API路由 -- `get_api_stats()`: 获取API统计信息 - -#### EditorInterface -- `handle_ui_interaction(interaction_data)`: 处理UI交互 -- `export_data(export_type)`: 导出数据 -- `import_data(import_data, import_type)`: 导入数据 - -## 性能优化 - -### 1. 网络优化 -- 启用消息压缩减少数据传输 -- 使用连接池减少连接开销 -- 实现智能心跳检测避免无用通信 - -### 2. 内存优化 -- 使用对象池减少内存分配 -- 实现LRU缓存策略 -- 及时清理过期数据 - -### 3. CPU优化 -- 多线程处理避免阻塞 -- 异步I/O提高并发性能 -- 智能负载均衡分配任务 - -## 安全特性 - -### 1. 认证与授权 -- 支持令牌和密码认证 -- 细粒度权限控制 -- 会话管理和超时机制 - -### 2. 数据安全 -- SSL/TLS加密传输 -- 敏感数据遮蔽 -- 数据库访问控制 - -### 3. 攻击防护 -- DDoS防护和速率限制 -- IP黑名单管理 -- 输入验证和清理 - -## 监控与日志 - -### 1. 实时监控 -- CPU、内存、磁盘使用率 -- 网络流量统计 -- 客户端连接状态 -- 消息处理性能 - -### 2. 警报系统 -- 资源使用率警报 -- 错误率监控 -- 性能降级通知 -- 自定义警报规则 - -### 3. 日志管理 -- 多级别日志记录 -- 日志文件轮转 -- 结构化日志格式 -- 审计日志跟踪 - -## 扩展开发 - -### 添加新功能模块 - -1. 创建新的模块文件 -2. 实现标准接口方法 -3. 在主插件中注册模块 -4. 添加配置选项 - -### 自定义消息处理器 - -1. 实现消息处理函数 -2. 注册到MessageRouter -3. 处理特定类型消息 -4. 返回处理结果 - -### 扩展API接口 - -1. 注册新的API路由 -2. 实现处理函数 -3. 添加权限验证 -4. 返回JSON格式响应 - -## 故障排除 - -### 常见问题 - -1. **服务器启动失败** - - 检查端口是否被占用 - - 确认配置文件正确 - - 查看日志文件详情 - -2. **客户端连接失败** - - 检查网络连接 - - 确认服务器状态 - - 验证认证信息 - -3. **性能问题** - - 监控资源使用情况 - - 检查配置参数 - - 优化数据库查询 - -### 日志查看 - -```python -# 获取插件统计信息 -stats = plugin.get_stats() -print(f"总连接数: {stats['state']['total_connections']}") -print(f"错误数: {stats['stats']['errors']}") -``` - -## 许可证 - -本插件采用MIT许可证,详情请查看LICENSE文件。 - -## 联系方式 - +# 服务器架构插件 + +## 概述 + +服务器架构插件为EG引擎提供了完整的服务器架构支持,包括服务器管理、客户端管理、消息路由、房间管理、数据存储、负载均衡、监控和安全等功能。该插件采用模块化设计,具有高性能、可扩展和易配置的特点。 + +## 功能特性 + +### 1. 核心功能 +- **服务器管理**: 完整的服务器启动、停止和状态管理 +- **客户端管理**: 客户端连接、认证、断开和权限管理 +- **消息路由**: 高效的消息传递和路由系统 +- **房间管理**: 多房间支持和房间状态管理 +- **数据存储**: 持久化数据存储和缓存管理 +- **负载均衡**: 多服务器实例间的负载分配 +- **监控系统**: 实时性能监控和日志记录 +- **安全系统**: 认证、授权和攻击防护 +- **API接口**: RESTful API供外部系统调用 +- **可视化编辑器**: 图形化配置和监控界面 + +### 2. 高级功能 +- **SSL/TLS加密支持**: 安全的网络通信 +- **消息压缩**: 减少网络带宽使用 +- **心跳检测**: 连接状态监控 +- **速率限制**: 防止滥用和DDoS攻击 +- **会话管理**: 用户会话跟踪和管理 +- **配置管理**: 灵活的配置系统 +- **实时监控**: 性能指标和警报系统 +- **数据备份**: 自动数据备份和恢复 + +## 模块架构 + +``` +server_architecture/ +├── plugin.py # 插件主入口 +├── core/ +│ └── server_manager.py # 服务器管理器 +├── clients/ +│ └── client_manager.py # 客户端管理器 +├── messaging/ +│ └── message_router.py # 消息路由器 +├── rooms/ +│ └── room_manager.py # 房间管理器 +├── storage/ +│ └── storage_manager.py # 数据存储管理器 +├── loadbalancer/ +│ └── load_balancer.py # 负载均衡器 +├── monitoring/ +│ └── monitoring_system.py # 监控系统 +├── config/ +│ └── config_manager.py # 配置管理器 +├── security/ +│ └── security_manager.py # 安全管理器 +├── api/ +│ └── api_interface.py # API接口 +├── editor/ +│ └── editor_interface.py # 编辑器接口 +├── utils/ +│ └── server_utils.py # 工具类 +└── README.md # 说明文档 +``` + +## 安装与配置 + +### 安装依赖 + +```bash +pip install psutil +``` + +### 基本配置 + +在插件配置文件中设置基本参数: + +```python +# 服务器配置 +server_config = { + "host": "0.0.0.0", + "port": 8080, + "max_connections": 1000, + "enable_ssl": False +} + +# 客户端配置 +client_config = { + "enable_authentication": True, + "max_clients": 1000, + "enable_rate_limiting": True +} +``` + +## 使用方法 + +### 1. 初始化插件 + +```python +from plugins.user.server_architecture.plugin import ServerArchitecturePlugin + +# 创建插件实例 +plugin = ServerArchitecturePlugin(engine) + +# 初始化插件 +if plugin.initialize(): + print("插件初始化成功") + +# 启用插件 +if plugin.enable(): + print("插件已启用") +``` + +### 2. 启动服务器 + +```python +# 启动服务器 +if plugin.server_manager.start_server(port=8080): + print("服务器启动成功") +``` + +### 3. 管理客户端 + +```python +# 获取客户端列表 +clients = plugin.client_manager.get_all_clients() + +# 断开客户端连接 +plugin.client_manager.disconnect_client("client_123", "kicked_by_admin") +``` + +### 4. 管理房间 + +```python +# 创建房间 +room_id = plugin.room_manager.create_room("游戏房间1") + +# 添加客户端到房间 +plugin.room_manager.add_client_to_room(room_id, "client_123") + +# 广播消息到房间 +message = {"type": "chat", "content": "Hello everyone!"} +plugin.room_manager.broadcast_to_room(room_id, message) +``` + +## API参考 + +### 主要类和方法 + +#### ServerArchitecturePlugin +- `initialize()`: 初始化插件 +- `enable()`: 启用插件 +- `disable()`: 禁用插件 +- `get_stats()`: 获取插件统计信息 + +#### ServerManager +- `start_server(port)`: 启动服务器 +- `stop_server()`: 停止服务器 +- `get_server_stats()`: 获取服务器统计信息 + +#### ClientManager +- `authenticate_client(client_id, auth_data)`: 认证客户端 +- `disconnect_client(client_id, reason)`: 断开客户端连接 +- `get_all_clients()`: 获取所有客户端信息 + +#### MessageRouter +- `register_message_handler(type, handler)`: 注册消息处理器 +- `send_message(message, client_socket)`: 发送消息到客户端 +- `broadcast_message(message)`: 广播消息到所有客户端 + +#### RoomManager +- `create_room(name, settings)`: 创建房间 +- `destroy_room(room_id)`: 销毁房间 +- `add_client_to_room(room_id, client_id)`: 添加客户端到房间 +- `broadcast_to_room(room_id, message)`: 广播消息到房间 + +#### StorageManager +- `save_user(user_id, user_data)`: 保存用户数据 +- `load_user(user_id)`: 加载用户数据 +- `save_room(room_id, room_data)`: 保存房间数据 + +#### LoadBalancer +- `add_server_instance(instance_id, instance_data)`: 添加服务器实例 +- `remove_server_instance(instance_id)`: 移除服务器实例 +- `route_request(client_ip)`: 路由请求到合适的实例 + +#### MonitoringSystem +- `get_performance_metrics()`: 获取性能指标 +- `get_active_alerts()`: 获取活动警报 +- `resolve_alert(alert_type)`: 解决警报 + +#### ConfigManager +- `get_config(section, key)`: 获取配置值 +- `set_config(section, key, value)`: 设置配置值 +- `load_config()`: 加载配置 +- `save_config()`: 保存配置 + +#### SecurityManager +- `authenticate_client(client_id, auth_data, client_address)`: 认证客户端 +- `validate_token(token)`: 验证令牌 +- `hash_password(password)`: 哈希密码 +- `block_ip(ip_address)`: 阻止IP地址 + +#### APIInterface +- `register_route(method, path, handler)`: 注册API路由 +- `get_api_stats()`: 获取API统计信息 + +#### EditorInterface +- `handle_ui_interaction(interaction_data)`: 处理UI交互 +- `export_data(export_type)`: 导出数据 +- `import_data(import_data, import_type)`: 导入数据 + +## 性能优化 + +### 1. 网络优化 +- 启用消息压缩减少数据传输 +- 使用连接池减少连接开销 +- 实现智能心跳检测避免无用通信 + +### 2. 内存优化 +- 使用对象池减少内存分配 +- 实现LRU缓存策略 +- 及时清理过期数据 + +### 3. CPU优化 +- 多线程处理避免阻塞 +- 异步I/O提高并发性能 +- 智能负载均衡分配任务 + +## 安全特性 + +### 1. 认证与授权 +- 支持令牌和密码认证 +- 细粒度权限控制 +- 会话管理和超时机制 + +### 2. 数据安全 +- SSL/TLS加密传输 +- 敏感数据遮蔽 +- 数据库访问控制 + +### 3. 攻击防护 +- DDoS防护和速率限制 +- IP黑名单管理 +- 输入验证和清理 + +## 监控与日志 + +### 1. 实时监控 +- CPU、内存、磁盘使用率 +- 网络流量统计 +- 客户端连接状态 +- 消息处理性能 + +### 2. 警报系统 +- 资源使用率警报 +- 错误率监控 +- 性能降级通知 +- 自定义警报规则 + +### 3. 日志管理 +- 多级别日志记录 +- 日志文件轮转 +- 结构化日志格式 +- 审计日志跟踪 + +## 扩展开发 + +### 添加新功能模块 + +1. 创建新的模块文件 +2. 实现标准接口方法 +3. 在主插件中注册模块 +4. 添加配置选项 + +### 自定义消息处理器 + +1. 实现消息处理函数 +2. 注册到MessageRouter +3. 处理特定类型消息 +4. 返回处理结果 + +### 扩展API接口 + +1. 注册新的API路由 +2. 实现处理函数 +3. 添加权限验证 +4. 返回JSON格式响应 + +## 故障排除 + +### 常见问题 + +1. **服务器启动失败** + - 检查端口是否被占用 + - 确认配置文件正确 + - 查看日志文件详情 + +2. **客户端连接失败** + - 检查网络连接 + - 确认服务器状态 + - 验证认证信息 + +3. **性能问题** + - 监控资源使用情况 + - 检查配置参数 + - 优化数据库查询 + +### 日志查看 + +```python +# 获取插件统计信息 +stats = plugin.get_stats() +print(f"总连接数: {stats['state']['total_connections']}") +print(f"错误数: {stats['stats']['errors']}") +``` + +## 许可证 + +本插件采用MIT许可证,详情请查看LICENSE文件。 + +## 联系方式 + 如有问题或建议,请联系开发团队。 \ No newline at end of file diff --git a/plugins/user/server_architecture/api/api_interface.py b/plugins/user/server_architecture/api/api_interface.py index 32612fe8..9503ab1f 100644 --- a/plugins/user/server_architecture/api/api_interface.py +++ b/plugins/user/server_architecture/api/api_interface.py @@ -1,880 +1,880 @@ -""" -API接口模块 -提供RESTful API供外部系统调用 -""" - -import time -import threading -import json -from typing import Dict, Any, List, Optional -from http.server import HTTPServer, BaseHTTPRequestHandler -from urllib.parse import urlparse, parse_qs - -class APIInterface: - """ - API接口 - 提供RESTful API供外部系统调用 - """ - - def __init__(self, plugin): - """ - 初始化API接口 - - Args: - plugin: 服务器架构插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # API配置 - self.api_config = { - "enable_api": True, - "api_host": "0.0.0.0", - "api_port": 8081, - "enable_cors": True, - "allowed_origins": ["*"], - "enable_authentication": True, - "api_version": "v1", - "max_request_size": 1048576, # 1MB - "enable_rate_limiting": True, - "requests_per_minute": 60, - "enable_logging": True, - "enable_swagger": True - } - - # API路由 - self.routes = { - "GET": {}, - "POST": {}, - "PUT": {}, - "DELETE": {} - } - - # API状态 - self.api_state = { - "is_running": False, - "start_time": 0.0, - "uptime": 0.0, - "total_requests": 0, - "successful_requests": 0, - "failed_requests": 0, - "active_connections": 0 - } - - # API统计 - self.api_stats = { - "requests_handled": 0, - "bytes_sent": 0, - "bytes_received": 0, - "average_response_time": 0.0, - "errors": 0 - } - - # HTTP服务器 - self.http_server = None - self.server_thread = None - - # 回调函数 - self.api_callbacks = { - "api_started": [], - "api_stopped": [], - "request_received": [], - "request_processed": [], - "api_error": [] - } - - # 时间戳记录 - self.last_request_time = 0.0 - self.last_stats_update = 0.0 - - print("✓ API接口已创建") - - def initialize(self) -> bool: - """ - 初始化API接口 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化API接口...") - - # 注册默认路由 - self._register_default_routes() - - self.initialized = True - print("✓ API接口初始化完成") - return True - - except Exception as e: - print(f"✗ API接口初始化失败: {e}") - self.api_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def _register_default_routes(self): - """注册默认路由""" - try: - # 系统信息路由 - self.register_route("GET", "/api/v1/system/info", self._handle_system_info) - self.register_route("GET", "/api/v1/system/stats", self._handle_system_stats) - self.register_route("GET", "/api/v1/system/config", self._handle_system_config) - - # 客户端管理路由 - self.register_route("GET", "/api/v1/clients", self._handle_get_clients) - self.register_route("GET", "/api/v1/clients/{client_id}", self._handle_get_client) - self.register_route("DELETE", "/api/v1/clients/{client_id}", self._handle_kick_client) - - # 房间管理路由 - self.register_route("GET", "/api/v1/rooms", self._handle_get_rooms) - self.register_route("POST", "/api/v1/rooms", self._handle_create_room) - self.register_route("GET", "/api/v1/rooms/{room_id}", self._handle_get_room) - self.register_route("DELETE", "/api/v1/rooms/{room_id}", self._handle_destroy_room) - - # 监控路由 - self.register_route("GET", "/api/v1/monitoring/metrics", self._handle_get_metrics) - self.register_route("GET", "/api/v1/monitoring/alerts", self._handle_get_alerts) - - # Swagger UI路由 - if self.api_config["enable_swagger"]: - self.register_route("GET", "/api/swagger", self._handle_swagger_ui) - self.register_route("GET", "/api/swagger.json", self._handle_swagger_spec) - - print("✓ 默认API路由已注册") - - except Exception as e: - print(f"✗ 默认API路由注册失败: {e}") - self.api_stats["errors"] += 1 - - def enable(self) -> bool: - """ - 启用API接口 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ API接口未初始化") - return False - - # 启动HTTP服务器 - self._start_http_server() - - self.enabled = True - print("✓ API接口已启用") - return True - - except Exception as e: - print(f"✗ API接口启用失败: {e}") - self.api_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用API接口""" - try: - self.enabled = False - - # 停止HTTP服务器 - self._stop_http_server() - - print("✓ API接口已禁用") - - except Exception as e: - print(f"✗ API接口禁用失败: {e}") - self.api_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理API接口资源""" - try: - # 禁用API接口 - if self.enabled: - self.disable() - - # 清理回调 - self.api_callbacks.clear() - self.routes.clear() - - self.initialized = False - print("✓ API接口资源已清理") - - except Exception as e: - print(f"✗ API接口资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新API接口状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.last_request_time = current_time - - # 更新运行时间 - if self.api_state["is_running"]: - self.api_state["uptime"] = current_time - self.api_state["start_time"] - - except Exception as e: - print(f"✗ API接口更新失败: {e}") - self.api_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def _start_http_server(self): - """启动HTTP服务器""" - try: - if self.api_state["is_running"]: - return - - # 创建HTTP请求处理器 - api_interface = self - - class APIRequestHandler(BaseHTTPRequestHandler): - def do_GET(self): - api_interface._handle_request(self, "GET") - - def do_POST(self): - api_interface._handle_request(self, "POST") - - def do_PUT(self): - api_interface._handle_request(self, "PUT") - - def do_DELETE(self): - api_interface._handle_request(self, "DELETE") - - def log_message(self, format, *args): - if api_interface.api_config["enable_logging"]: - print(f"[API] {format % args}") - - # 启动HTTP服务器 - host = self.api_config["api_host"] - port = self.api_config["api_port"] - - self.http_server = HTTPServer((host, port), APIRequestHandler) - self.server_thread = threading.Thread(target=self.http_server.serve_forever, daemon=True) - self.server_thread.start() - - self.api_state["is_running"] = True - self.api_state["start_time"] = time.time() - - # 触发API启动回调 - self._trigger_api_callback("api_started", { - "host": host, - "port": port, - "timestamp": self.api_state["start_time"] - }) - - print(f"✓ API服务器已启动: http://{host}:{port}") - - except Exception as e: - print(f"✗ HTTP服务器启动失败: {e}") - self.api_stats["errors"] += 1 - - def _stop_http_server(self): - """停止HTTP服务器""" - try: - if not self.api_state["is_running"]: - return - - if self.http_server: - self.http_server.shutdown() - self.http_server.server_close() - self.http_server = None - - if self.server_thread and self.server_thread.is_alive(): - self.server_thread.join(timeout=5.0) - self.server_thread = None - - self.api_state["is_running"] = False - self.api_state["start_time"] = 0.0 - self.api_state["uptime"] = 0.0 - - # 触发API停止回调 - self._trigger_api_callback("api_stopped", { - "timestamp": time.time() - }) - - print("✓ API服务器已停止") - - except Exception as e: - print(f"✗ HTTP服务器停止失败: {e}") - self.api_stats["errors"] += 1 - - def _handle_request(self, handler: BaseHTTPRequestHandler, method: str): - """ - 处理HTTP请求 - - Args: - handler: HTTP请求处理器 - method: HTTP方法 - """ - try: - start_time = time.time() - - # 解析请求 - parsed_url = urlparse(handler.path) - path = parsed_url.path - query_params = parse_qs(parsed_url.query) - - # 读取请求体 - content_length = int(handler.headers.get('Content-Length', 0)) - if content_length > self.api_config["max_request_size"]: - self._send_error_response(handler, 413, "Request Entity Too Large") - return - - request_body = handler.rfile.read(content_length) if content_length > 0 else b"" - - # 解析JSON请求体 - request_data = {} - if request_body: - try: - request_data = json.loads(request_body.decode('utf-8')) - except json.JSONDecodeError: - self._send_error_response(handler, 400, "Invalid JSON") - return - - # 记录请求 - self.api_state["total_requests"] += 1 - - # 触发请求接收回调 - self._trigger_api_callback("request_received", { - "method": method, - "path": path, - "client_address": handler.client_address, - "timestamp": start_time - }) - - # 查找路由处理器 - route_handler = self._find_route_handler(method, path) - if not route_handler: - self._send_error_response(handler, 404, "Not Found") - self.api_state["failed_requests"] += 1 - return - - # 处理请求 - try: - response_data = route_handler({ - "method": method, - "path": path, - "query_params": query_params, - "body": request_data, - "headers": dict(handler.headers), - "client_address": handler.client_address - }) - - # 发送响应 - self._send_json_response(handler, 200, response_data) - self.api_state["successful_requests"] += 1 - - except Exception as e: - print(f"✗ API路由处理失败: {e}") - self._send_error_response(handler, 500, f"Internal Server Error: {str(e)}") - self.api_state["failed_requests"] += 1 - self.api_stats["errors"] += 1 - return - - # 更新统计 - end_time = time.time() - response_time = end_time - start_time - self.api_stats["requests_handled"] += 1 - self.api_stats["average_response_time"] = ( - (self.api_stats["average_response_time"] * (self.api_stats["requests_handled"] - 1) + response_time) - / self.api_stats["requests_handled"] - ) - - # 触发请求处理回调 - self._trigger_api_callback("request_processed", { - "method": method, - "path": path, - "response_time": response_time, - "timestamp": end_time - }) - - except Exception as e: - print(f"✗ HTTP请求处理失败: {e}") - self.api_stats["errors"] += 1 - try: - self._send_error_response(handler, 500, f"Internal Server Error: {str(e)}") - except: - pass - - def _find_route_handler(self, method: str, path: str): - """ - 查找路由处理器 - - Args: - method: HTTP方法 - path: 请求路径 - - Returns: - 路由处理器函数或None - """ - try: - if method not in self.routes: - return None - - # 精确匹配 - if path in self.routes[method]: - return self.routes[method][path] - - # 参数化路径匹配 - for route_path, handler in self.routes[method].items(): - if "{" in route_path and "}" in route_path: - # 简单的参数化路径匹配 - route_parts = route_path.split("/") - path_parts = path.split("/") - - if len(route_parts) == len(path_parts): - match = True - for i in range(len(route_parts)): - if route_parts[i] != path_parts[i] and not (route_parts[i].startswith("{") and route_parts[i].endswith("}")): - match = False - break - - if match: - return handler - - return None - - except Exception as e: - print(f"✗ 路由查找失败: {e}") - self.api_stats["errors"] += 1 - return None - - def register_route(self, method: str, path: str, handler: callable): - """ - 注册API路由 - - Args: - method: HTTP方法 (GET, POST, PUT, DELETE) - path: 路径 - handler: 处理器函数 - """ - try: - if method not in self.routes: - self.routes[method] = {} - - self.routes[method][path] = handler - print(f"✓ API路由已注册: {method} {path}") - - except Exception as e: - print(f"✗ API路由注册失败: {e}") - self.api_stats["errors"] += 1 - - def _send_json_response(self, handler: BaseHTTPRequestHandler, status_code: int, data: Any): - """ - 发送JSON响应 - - Args: - handler: HTTP请求处理器 - status_code: 状态码 - data: 响应数据 - """ - try: - response_data = json.dumps(data, ensure_ascii=False) - response_bytes = response_data.encode('utf-8') - - handler.send_response(status_code) - handler.send_header('Content-Type', 'application/json; charset=utf-8') - handler.send_header('Content-Length', str(len(response_bytes))) - - # CORS头 - if self.api_config["enable_cors"]: - handler.send_header('Access-Control-Allow-Origin', '*') - handler.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') - handler.send_header('Access-Control-Allow-Headers', '*') - - handler.end_headers() - handler.wfile.write(response_bytes) - - self.api_stats["bytes_sent"] += len(response_bytes) - - except Exception as e: - print(f"✗ JSON响应发送失败: {e}") - self.api_stats["errors"] += 1 - - def _send_error_response(self, handler: BaseHTTPRequestHandler, status_code: int, message: str): - """ - 发送错误响应 - - Args: - handler: HTTP请求处理器 - status_code: 状态码 - message: 错误消息 - """ - try: - error_data = { - "error": message, - "status_code": status_code, - "timestamp": time.time() - } - - self._send_json_response(handler, status_code, error_data) - - except Exception as e: - print(f"✗ 错误响应发送失败: {e}") - self.api_stats["errors"] += 1 - - # 默认路由处理器 - def _handle_system_info(self, request: Dict[str, Any]) -> Dict[str, Any]: - """处理系统信息请求""" - return { - "name": "EG Server Architecture", - "version": "1.0.0", - "status": "running", - "uptime": self.api_state["uptime"], - "timestamp": time.time() - } - - def _handle_system_stats(self, request: Dict[str, Any]) -> Dict[str, Any]: - """处理系统统计请求""" - # 收集各模块统计信息 - stats = { - "server": self.plugin.server_manager.get_server_stats() if self.plugin.server_manager else {}, - "clients": self.plugin.client_manager.get_client_stats() if self.plugin.client_manager else {}, - "rooms": self.plugin.room_manager.get_room_stats() if self.plugin.room_manager else {}, - "messages": self.plugin.message_router.get_message_stats() if self.plugin.message_router else {}, - "storage": self.plugin.storage_manager.get_storage_stats() if self.plugin.storage_manager else {}, - "loadbalancer": self.plugin.load_balancer.get_loadbalancer_stats() if self.plugin.load_balancer else {}, - "monitoring": self.plugin.monitoring_system.get_monitoring_stats() if self.plugin.monitoring_system else {}, - "security": self.plugin.security_manager.get_security_stats() if self.plugin.security_manager else {}, - "api": self.api_state.copy() - } - - return { - "stats": stats, - "timestamp": time.time() - } - - def _handle_system_config(self, request: Dict[str, Any]) -> Dict[str, Any]: - """处理系统配置请求""" - config = { - "server": self.plugin.config_manager.get_config("server") if self.plugin.config_manager else {}, - "client": self.plugin.config_manager.get_config("client") if self.plugin.config_manager else {}, - "room": self.plugin.config_manager.get_config("room") if self.plugin.config_manager else {}, - "storage": self.plugin.config_manager.get_config("storage") if self.plugin.config_manager else {}, - "loadbalancer": self.plugin.config_manager.get_config("loadbalancer") if self.plugin.config_manager else {}, - "monitoring": self.plugin.config_manager.get_config("monitoring") if self.plugin.config_manager else {}, - "security": self.plugin.config_manager.get_config("security") if self.plugin.config_manager else {}, - "api": self.api_config.copy() - } - - return { - "config": config, - "timestamp": time.time() - } - - def _handle_get_clients(self, request: Dict[str, Any]) -> Dict[str, Any]: - """处理获取客户端列表请求""" - clients = self.plugin.client_manager.get_all_clients() if self.plugin.client_manager else {} - return { - "clients": clients, - "count": len(clients), - "timestamp": time.time() - } - - def _handle_get_client(self, request: Dict[str, Any]) -> Dict[str, Any]: - """处理获取客户端信息请求""" - path_parts = request["path"].split("/") - client_id = path_parts[-1] if len(path_parts) > 0 else None - - if not client_id: - return {"error": "Missing client_id"} - - client_info = self.plugin.client_manager.get_client(client_id) if self.plugin.client_manager else None - if not client_info: - return {"error": "Client not found"} - - return { - "client": client_info, - "timestamp": time.time() - } - - def _handle_kick_client(self, request: Dict[str, Any]) -> Dict[str, Any]: - """处理踢出客户端请求""" - path_parts = request["path"].split("/") - client_id = path_parts[-1] if len(path_parts) > 0 else None - - if not client_id: - return {"error": "Missing client_id"} - - if self.plugin.client_manager: - self.plugin.client_manager.disconnect_client(client_id, "kicked_by_admin") - return { - "message": f"Client {client_id} kicked successfully", - "timestamp": time.time() - } - else: - return {"error": "Client manager not available"} - - def _handle_get_rooms(self, request: Dict[str, Any]) -> Dict[str, Any]: - """处理获取房间列表请求""" - rooms = self.plugin.room_manager.get_all_rooms() if self.plugin.room_manager else [] - return { - "rooms": rooms, - "count": len(rooms), - "timestamp": time.time() - } - - def _handle_create_room(self, request: Dict[str, Any]) -> Dict[str, Any]: - """处理创建房间请求""" - room_data = request.get("body", {}) - room_name = room_data.get("room_name") - room_settings = room_data.get("settings", {}) - room_password = room_data.get("password") - - if self.plugin.room_manager: - room_id = self.plugin.room_manager.create_room(room_name, room_settings, room_password) - if room_id: - return { - "message": "Room created successfully", - "room_id": room_id, - "timestamp": time.time() - } - else: - return {"error": "Failed to create room"} - else: - return {"error": "Room manager not available"} - - def _handle_get_room(self, request: Dict[str, Any]) -> Dict[str, Any]: - """处理获取房间信息请求""" - path_parts = request["path"].split("/") - room_id = path_parts[-1] if len(path_parts) > 0 else None - - if not room_id: - return {"error": "Missing room_id"} - - room_info = self.plugin.room_manager.get_room_info(room_id) if self.plugin.room_manager else None - if not room_info: - return {"error": "Room not found"} - - return { - "room": room_info, - "timestamp": time.time() - } - - def _handle_destroy_room(self, request: Dict[str, Any]) -> Dict[str, Any]: - """处理销毁房间请求""" - path_parts = request["path"].split("/") - room_id = path_parts[-1] if len(path_parts) > 0 else None - - if not room_id: - return {"error": "Missing room_id"} - - if self.plugin.room_manager: - success = self.plugin.room_manager.destroy_room(room_id) - if success: - return { - "message": f"Room {room_id} destroyed successfully", - "timestamp": time.time() - } - else: - return {"error": "Failed to destroy room"} - else: - return {"error": "Room manager not available"} - - def _handle_get_metrics(self, request: Dict[str, Any]) -> Dict[str, Any]: - """处理获取监控指标请求""" - metrics = {} - if self.plugin.monitoring_system: - metrics = self.plugin.monitoring_system.get_performance_metrics() - - return { - "metrics": metrics, - "timestamp": time.time() - } - - def _handle_get_alerts(self, request: Dict[str, Any]) -> Dict[str, Any]: - """处理获取警报信息请求""" - alerts = {} - alert_history = [] - - if self.plugin.monitoring_system: - alerts = self.plugin.monitoring_system.get_active_alerts() - alert_history = self.plugin.monitoring_system.get_alert_history() - - return { - "active_alerts": alerts, - "alert_history": alert_history, - "timestamp": time.time() - } - - def _handle_swagger_ui(self, request: Dict[str, Any]) -> Dict[str, Any]: - """处理Swagger UI请求""" - return { - "message": "Swagger UI would be served here", - "swagger_spec_url": "/api/swagger.json", - "timestamp": time.time() - } - - def _handle_swagger_spec(self, request: Dict[str, Any]) -> Dict[str, Any]: - """处理Swagger规范请求""" - spec = { - "swagger": "2.0", - "info": { - "title": "EG Server Architecture API", - "version": "1.0.0", - "description": "API for managing EG server architecture" - }, - "paths": { - "/api/v1/system/info": { - "get": { - "summary": "Get system information", - "responses": { - "200": { - "description": "System information" - } - } - } - }, - "/api/v1/system/stats": { - "get": { - "summary": "Get system statistics", - "responses": { - "200": { - "description": "System statistics" - } - } - } - } - # 这里可以继续添加更多API端点的规范 - } - } - - return spec - - def get_api_stats(self) -> Dict[str, Any]: - """ - 获取API统计信息 - - Returns: - API统计字典 - """ - return { - "state": self.api_state.copy(), - "stats": self.api_stats.copy(), - "config": self.api_config.copy() - } - - def reset_stats(self): - """重置API统计信息""" - try: - self.api_stats = { - "requests_handled": 0, - "bytes_sent": 0, - "bytes_received": 0, - "average_response_time": 0.0, - "errors": 0 - } - - self.api_state["total_requests"] = 0 - self.api_state["successful_requests"] = 0 - self.api_state["failed_requests"] = 0 - - print("✓ API统计信息已重置") - except Exception as e: - print(f"✗ API统计信息重置失败: {e}") - - def set_api_config(self, config: Dict[str, Any]) -> bool: - """ - 设置API配置 - - Args: - config: API配置字典 - - Returns: - 是否设置成功 - """ - try: - old_port = self.api_config.get("api_port") - new_port = config.get("api_port", old_port) - - # 如果端口发生变化,重启服务器 - if old_port != new_port and self.api_state["is_running"]: - self._stop_http_server() - self.api_config.update(config) - self._start_http_server() - else: - self.api_config.update(config) - - print(f"✓ API配置已更新: {self.api_config}") - return True - - except Exception as e: - print(f"✗ API配置设置失败: {e}") - return False - - def get_api_config(self) -> Dict[str, Any]: - """ - 获取API配置 - - Returns: - API配置字典 - """ - return self.api_config.copy() - - def _trigger_api_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发API回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.api_callbacks: - for callback in self.api_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ API回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ API回调触发失败: {e}") - - def register_api_callback(self, callback_type: str, callback: callable): - """ - 注册API回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.api_callbacks: - self.api_callbacks[callback_type].append(callback) - print(f"✓ API回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ API回调注册失败: {e}") - - def unregister_api_callback(self, callback_type: str, callback: callable): - """ - 注销API回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.api_callbacks: - if callback in self.api_callbacks[callback_type]: - self.api_callbacks[callback_type].remove(callback) - print(f"✓ API回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +API接口模块 +提供RESTful API供外部系统调用 +""" + +import time +import threading +import json +from typing import Dict, Any, List, Optional +from http.server import HTTPServer, BaseHTTPRequestHandler +from urllib.parse import urlparse, parse_qs + +class APIInterface: + """ + API接口 + 提供RESTful API供外部系统调用 + """ + + def __init__(self, plugin): + """ + 初始化API接口 + + Args: + plugin: 服务器架构插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # API配置 + self.api_config = { + "enable_api": True, + "api_host": "0.0.0.0", + "api_port": 8081, + "enable_cors": True, + "allowed_origins": ["*"], + "enable_authentication": True, + "api_version": "v1", + "max_request_size": 1048576, # 1MB + "enable_rate_limiting": True, + "requests_per_minute": 60, + "enable_logging": True, + "enable_swagger": True + } + + # API路由 + self.routes = { + "GET": {}, + "POST": {}, + "PUT": {}, + "DELETE": {} + } + + # API状态 + self.api_state = { + "is_running": False, + "start_time": 0.0, + "uptime": 0.0, + "total_requests": 0, + "successful_requests": 0, + "failed_requests": 0, + "active_connections": 0 + } + + # API统计 + self.api_stats = { + "requests_handled": 0, + "bytes_sent": 0, + "bytes_received": 0, + "average_response_time": 0.0, + "errors": 0 + } + + # HTTP服务器 + self.http_server = None + self.server_thread = None + + # 回调函数 + self.api_callbacks = { + "api_started": [], + "api_stopped": [], + "request_received": [], + "request_processed": [], + "api_error": [] + } + + # 时间戳记录 + self.last_request_time = 0.0 + self.last_stats_update = 0.0 + + print("✓ API接口已创建") + + def initialize(self) -> bool: + """ + 初始化API接口 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化API接口...") + + # 注册默认路由 + self._register_default_routes() + + self.initialized = True + print("✓ API接口初始化完成") + return True + + except Exception as e: + print(f"✗ API接口初始化失败: {e}") + self.api_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def _register_default_routes(self): + """注册默认路由""" + try: + # 系统信息路由 + self.register_route("GET", "/api/v1/system/info", self._handle_system_info) + self.register_route("GET", "/api/v1/system/stats", self._handle_system_stats) + self.register_route("GET", "/api/v1/system/config", self._handle_system_config) + + # 客户端管理路由 + self.register_route("GET", "/api/v1/clients", self._handle_get_clients) + self.register_route("GET", "/api/v1/clients/{client_id}", self._handle_get_client) + self.register_route("DELETE", "/api/v1/clients/{client_id}", self._handle_kick_client) + + # 房间管理路由 + self.register_route("GET", "/api/v1/rooms", self._handle_get_rooms) + self.register_route("POST", "/api/v1/rooms", self._handle_create_room) + self.register_route("GET", "/api/v1/rooms/{room_id}", self._handle_get_room) + self.register_route("DELETE", "/api/v1/rooms/{room_id}", self._handle_destroy_room) + + # 监控路由 + self.register_route("GET", "/api/v1/monitoring/metrics", self._handle_get_metrics) + self.register_route("GET", "/api/v1/monitoring/alerts", self._handle_get_alerts) + + # Swagger UI路由 + if self.api_config["enable_swagger"]: + self.register_route("GET", "/api/swagger", self._handle_swagger_ui) + self.register_route("GET", "/api/swagger.json", self._handle_swagger_spec) + + print("✓ 默认API路由已注册") + + except Exception as e: + print(f"✗ 默认API路由注册失败: {e}") + self.api_stats["errors"] += 1 + + def enable(self) -> bool: + """ + 启用API接口 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ API接口未初始化") + return False + + # 启动HTTP服务器 + self._start_http_server() + + self.enabled = True + print("✓ API接口已启用") + return True + + except Exception as e: + print(f"✗ API接口启用失败: {e}") + self.api_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用API接口""" + try: + self.enabled = False + + # 停止HTTP服务器 + self._stop_http_server() + + print("✓ API接口已禁用") + + except Exception as e: + print(f"✗ API接口禁用失败: {e}") + self.api_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理API接口资源""" + try: + # 禁用API接口 + if self.enabled: + self.disable() + + # 清理回调 + self.api_callbacks.clear() + self.routes.clear() + + self.initialized = False + print("✓ API接口资源已清理") + + except Exception as e: + print(f"✗ API接口资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新API接口状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.last_request_time = current_time + + # 更新运行时间 + if self.api_state["is_running"]: + self.api_state["uptime"] = current_time - self.api_state["start_time"] + + except Exception as e: + print(f"✗ API接口更新失败: {e}") + self.api_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def _start_http_server(self): + """启动HTTP服务器""" + try: + if self.api_state["is_running"]: + return + + # 创建HTTP请求处理器 + api_interface = self + + class APIRequestHandler(BaseHTTPRequestHandler): + def do_GET(self): + api_interface._handle_request(self, "GET") + + def do_POST(self): + api_interface._handle_request(self, "POST") + + def do_PUT(self): + api_interface._handle_request(self, "PUT") + + def do_DELETE(self): + api_interface._handle_request(self, "DELETE") + + def log_message(self, format, *args): + if api_interface.api_config["enable_logging"]: + print(f"[API] {format % args}") + + # 启动HTTP服务器 + host = self.api_config["api_host"] + port = self.api_config["api_port"] + + self.http_server = HTTPServer((host, port), APIRequestHandler) + self.server_thread = threading.Thread(target=self.http_server.serve_forever, daemon=True) + self.server_thread.start() + + self.api_state["is_running"] = True + self.api_state["start_time"] = time.time() + + # 触发API启动回调 + self._trigger_api_callback("api_started", { + "host": host, + "port": port, + "timestamp": self.api_state["start_time"] + }) + + print(f"✓ API服务器已启动: http://{host}:{port}") + + except Exception as e: + print(f"✗ HTTP服务器启动失败: {e}") + self.api_stats["errors"] += 1 + + def _stop_http_server(self): + """停止HTTP服务器""" + try: + if not self.api_state["is_running"]: + return + + if self.http_server: + self.http_server.shutdown() + self.http_server.server_close() + self.http_server = None + + if self.server_thread and self.server_thread.is_alive(): + self.server_thread.join(timeout=5.0) + self.server_thread = None + + self.api_state["is_running"] = False + self.api_state["start_time"] = 0.0 + self.api_state["uptime"] = 0.0 + + # 触发API停止回调 + self._trigger_api_callback("api_stopped", { + "timestamp": time.time() + }) + + print("✓ API服务器已停止") + + except Exception as e: + print(f"✗ HTTP服务器停止失败: {e}") + self.api_stats["errors"] += 1 + + def _handle_request(self, handler: BaseHTTPRequestHandler, method: str): + """ + 处理HTTP请求 + + Args: + handler: HTTP请求处理器 + method: HTTP方法 + """ + try: + start_time = time.time() + + # 解析请求 + parsed_url = urlparse(handler.path) + path = parsed_url.path + query_params = parse_qs(parsed_url.query) + + # 读取请求体 + content_length = int(handler.headers.get('Content-Length', 0)) + if content_length > self.api_config["max_request_size"]: + self._send_error_response(handler, 413, "Request Entity Too Large") + return + + request_body = handler.rfile.read(content_length) if content_length > 0 else b"" + + # 解析JSON请求体 + request_data = {} + if request_body: + try: + request_data = json.loads(request_body.decode('utf-8')) + except json.JSONDecodeError: + self._send_error_response(handler, 400, "Invalid JSON") + return + + # 记录请求 + self.api_state["total_requests"] += 1 + + # 触发请求接收回调 + self._trigger_api_callback("request_received", { + "method": method, + "path": path, + "client_address": handler.client_address, + "timestamp": start_time + }) + + # 查找路由处理器 + route_handler = self._find_route_handler(method, path) + if not route_handler: + self._send_error_response(handler, 404, "Not Found") + self.api_state["failed_requests"] += 1 + return + + # 处理请求 + try: + response_data = route_handler({ + "method": method, + "path": path, + "query_params": query_params, + "body": request_data, + "headers": dict(handler.headers), + "client_address": handler.client_address + }) + + # 发送响应 + self._send_json_response(handler, 200, response_data) + self.api_state["successful_requests"] += 1 + + except Exception as e: + print(f"✗ API路由处理失败: {e}") + self._send_error_response(handler, 500, f"Internal Server Error: {str(e)}") + self.api_state["failed_requests"] += 1 + self.api_stats["errors"] += 1 + return + + # 更新统计 + end_time = time.time() + response_time = end_time - start_time + self.api_stats["requests_handled"] += 1 + self.api_stats["average_response_time"] = ( + (self.api_stats["average_response_time"] * (self.api_stats["requests_handled"] - 1) + response_time) + / self.api_stats["requests_handled"] + ) + + # 触发请求处理回调 + self._trigger_api_callback("request_processed", { + "method": method, + "path": path, + "response_time": response_time, + "timestamp": end_time + }) + + except Exception as e: + print(f"✗ HTTP请求处理失败: {e}") + self.api_stats["errors"] += 1 + try: + self._send_error_response(handler, 500, f"Internal Server Error: {str(e)}") + except: + pass + + def _find_route_handler(self, method: str, path: str): + """ + 查找路由处理器 + + Args: + method: HTTP方法 + path: 请求路径 + + Returns: + 路由处理器函数或None + """ + try: + if method not in self.routes: + return None + + # 精确匹配 + if path in self.routes[method]: + return self.routes[method][path] + + # 参数化路径匹配 + for route_path, handler in self.routes[method].items(): + if "{" in route_path and "}" in route_path: + # 简单的参数化路径匹配 + route_parts = route_path.split("/") + path_parts = path.split("/") + + if len(route_parts) == len(path_parts): + match = True + for i in range(len(route_parts)): + if route_parts[i] != path_parts[i] and not (route_parts[i].startswith("{") and route_parts[i].endswith("}")): + match = False + break + + if match: + return handler + + return None + + except Exception as e: + print(f"✗ 路由查找失败: {e}") + self.api_stats["errors"] += 1 + return None + + def register_route(self, method: str, path: str, handler: callable): + """ + 注册API路由 + + Args: + method: HTTP方法 (GET, POST, PUT, DELETE) + path: 路径 + handler: 处理器函数 + """ + try: + if method not in self.routes: + self.routes[method] = {} + + self.routes[method][path] = handler + print(f"✓ API路由已注册: {method} {path}") + + except Exception as e: + print(f"✗ API路由注册失败: {e}") + self.api_stats["errors"] += 1 + + def _send_json_response(self, handler: BaseHTTPRequestHandler, status_code: int, data: Any): + """ + 发送JSON响应 + + Args: + handler: HTTP请求处理器 + status_code: 状态码 + data: 响应数据 + """ + try: + response_data = json.dumps(data, ensure_ascii=False) + response_bytes = response_data.encode('utf-8') + + handler.send_response(status_code) + handler.send_header('Content-Type', 'application/json; charset=utf-8') + handler.send_header('Content-Length', str(len(response_bytes))) + + # CORS头 + if self.api_config["enable_cors"]: + handler.send_header('Access-Control-Allow-Origin', '*') + handler.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') + handler.send_header('Access-Control-Allow-Headers', '*') + + handler.end_headers() + handler.wfile.write(response_bytes) + + self.api_stats["bytes_sent"] += len(response_bytes) + + except Exception as e: + print(f"✗ JSON响应发送失败: {e}") + self.api_stats["errors"] += 1 + + def _send_error_response(self, handler: BaseHTTPRequestHandler, status_code: int, message: str): + """ + 发送错误响应 + + Args: + handler: HTTP请求处理器 + status_code: 状态码 + message: 错误消息 + """ + try: + error_data = { + "error": message, + "status_code": status_code, + "timestamp": time.time() + } + + self._send_json_response(handler, status_code, error_data) + + except Exception as e: + print(f"✗ 错误响应发送失败: {e}") + self.api_stats["errors"] += 1 + + # 默认路由处理器 + def _handle_system_info(self, request: Dict[str, Any]) -> Dict[str, Any]: + """处理系统信息请求""" + return { + "name": "EG Server Architecture", + "version": "1.0.0", + "status": "running", + "uptime": self.api_state["uptime"], + "timestamp": time.time() + } + + def _handle_system_stats(self, request: Dict[str, Any]) -> Dict[str, Any]: + """处理系统统计请求""" + # 收集各模块统计信息 + stats = { + "server": self.plugin.server_manager.get_server_stats() if self.plugin.server_manager else {}, + "clients": self.plugin.client_manager.get_client_stats() if self.plugin.client_manager else {}, + "rooms": self.plugin.room_manager.get_room_stats() if self.plugin.room_manager else {}, + "messages": self.plugin.message_router.get_message_stats() if self.plugin.message_router else {}, + "storage": self.plugin.storage_manager.get_storage_stats() if self.plugin.storage_manager else {}, + "loadbalancer": self.plugin.load_balancer.get_loadbalancer_stats() if self.plugin.load_balancer else {}, + "monitoring": self.plugin.monitoring_system.get_monitoring_stats() if self.plugin.monitoring_system else {}, + "security": self.plugin.security_manager.get_security_stats() if self.plugin.security_manager else {}, + "api": self.api_state.copy() + } + + return { + "stats": stats, + "timestamp": time.time() + } + + def _handle_system_config(self, request: Dict[str, Any]) -> Dict[str, Any]: + """处理系统配置请求""" + config = { + "server": self.plugin.config_manager.get_config("server") if self.plugin.config_manager else {}, + "client": self.plugin.config_manager.get_config("client") if self.plugin.config_manager else {}, + "room": self.plugin.config_manager.get_config("room") if self.plugin.config_manager else {}, + "storage": self.plugin.config_manager.get_config("storage") if self.plugin.config_manager else {}, + "loadbalancer": self.plugin.config_manager.get_config("loadbalancer") if self.plugin.config_manager else {}, + "monitoring": self.plugin.config_manager.get_config("monitoring") if self.plugin.config_manager else {}, + "security": self.plugin.config_manager.get_config("security") if self.plugin.config_manager else {}, + "api": self.api_config.copy() + } + + return { + "config": config, + "timestamp": time.time() + } + + def _handle_get_clients(self, request: Dict[str, Any]) -> Dict[str, Any]: + """处理获取客户端列表请求""" + clients = self.plugin.client_manager.get_all_clients() if self.plugin.client_manager else {} + return { + "clients": clients, + "count": len(clients), + "timestamp": time.time() + } + + def _handle_get_client(self, request: Dict[str, Any]) -> Dict[str, Any]: + """处理获取客户端信息请求""" + path_parts = request["path"].split("/") + client_id = path_parts[-1] if len(path_parts) > 0 else None + + if not client_id: + return {"error": "Missing client_id"} + + client_info = self.plugin.client_manager.get_client(client_id) if self.plugin.client_manager else None + if not client_info: + return {"error": "Client not found"} + + return { + "client": client_info, + "timestamp": time.time() + } + + def _handle_kick_client(self, request: Dict[str, Any]) -> Dict[str, Any]: + """处理踢出客户端请求""" + path_parts = request["path"].split("/") + client_id = path_parts[-1] if len(path_parts) > 0 else None + + if not client_id: + return {"error": "Missing client_id"} + + if self.plugin.client_manager: + self.plugin.client_manager.disconnect_client(client_id, "kicked_by_admin") + return { + "message": f"Client {client_id} kicked successfully", + "timestamp": time.time() + } + else: + return {"error": "Client manager not available"} + + def _handle_get_rooms(self, request: Dict[str, Any]) -> Dict[str, Any]: + """处理获取房间列表请求""" + rooms = self.plugin.room_manager.get_all_rooms() if self.plugin.room_manager else [] + return { + "rooms": rooms, + "count": len(rooms), + "timestamp": time.time() + } + + def _handle_create_room(self, request: Dict[str, Any]) -> Dict[str, Any]: + """处理创建房间请求""" + room_data = request.get("body", {}) + room_name = room_data.get("room_name") + room_settings = room_data.get("settings", {}) + room_password = room_data.get("password") + + if self.plugin.room_manager: + room_id = self.plugin.room_manager.create_room(room_name, room_settings, room_password) + if room_id: + return { + "message": "Room created successfully", + "room_id": room_id, + "timestamp": time.time() + } + else: + return {"error": "Failed to create room"} + else: + return {"error": "Room manager not available"} + + def _handle_get_room(self, request: Dict[str, Any]) -> Dict[str, Any]: + """处理获取房间信息请求""" + path_parts = request["path"].split("/") + room_id = path_parts[-1] if len(path_parts) > 0 else None + + if not room_id: + return {"error": "Missing room_id"} + + room_info = self.plugin.room_manager.get_room_info(room_id) if self.plugin.room_manager else None + if not room_info: + return {"error": "Room not found"} + + return { + "room": room_info, + "timestamp": time.time() + } + + def _handle_destroy_room(self, request: Dict[str, Any]) -> Dict[str, Any]: + """处理销毁房间请求""" + path_parts = request["path"].split("/") + room_id = path_parts[-1] if len(path_parts) > 0 else None + + if not room_id: + return {"error": "Missing room_id"} + + if self.plugin.room_manager: + success = self.plugin.room_manager.destroy_room(room_id) + if success: + return { + "message": f"Room {room_id} destroyed successfully", + "timestamp": time.time() + } + else: + return {"error": "Failed to destroy room"} + else: + return {"error": "Room manager not available"} + + def _handle_get_metrics(self, request: Dict[str, Any]) -> Dict[str, Any]: + """处理获取监控指标请求""" + metrics = {} + if self.plugin.monitoring_system: + metrics = self.plugin.monitoring_system.get_performance_metrics() + + return { + "metrics": metrics, + "timestamp": time.time() + } + + def _handle_get_alerts(self, request: Dict[str, Any]) -> Dict[str, Any]: + """处理获取警报信息请求""" + alerts = {} + alert_history = [] + + if self.plugin.monitoring_system: + alerts = self.plugin.monitoring_system.get_active_alerts() + alert_history = self.plugin.monitoring_system.get_alert_history() + + return { + "active_alerts": alerts, + "alert_history": alert_history, + "timestamp": time.time() + } + + def _handle_swagger_ui(self, request: Dict[str, Any]) -> Dict[str, Any]: + """处理Swagger UI请求""" + return { + "message": "Swagger UI would be served here", + "swagger_spec_url": "/api/swagger.json", + "timestamp": time.time() + } + + def _handle_swagger_spec(self, request: Dict[str, Any]) -> Dict[str, Any]: + """处理Swagger规范请求""" + spec = { + "swagger": "2.0", + "info": { + "title": "EG Server Architecture API", + "version": "1.0.0", + "description": "API for managing EG server architecture" + }, + "paths": { + "/api/v1/system/info": { + "get": { + "summary": "Get system information", + "responses": { + "200": { + "description": "System information" + } + } + } + }, + "/api/v1/system/stats": { + "get": { + "summary": "Get system statistics", + "responses": { + "200": { + "description": "System statistics" + } + } + } + } + # 这里可以继续添加更多API端点的规范 + } + } + + return spec + + def get_api_stats(self) -> Dict[str, Any]: + """ + 获取API统计信息 + + Returns: + API统计字典 + """ + return { + "state": self.api_state.copy(), + "stats": self.api_stats.copy(), + "config": self.api_config.copy() + } + + def reset_stats(self): + """重置API统计信息""" + try: + self.api_stats = { + "requests_handled": 0, + "bytes_sent": 0, + "bytes_received": 0, + "average_response_time": 0.0, + "errors": 0 + } + + self.api_state["total_requests"] = 0 + self.api_state["successful_requests"] = 0 + self.api_state["failed_requests"] = 0 + + print("✓ API统计信息已重置") + except Exception as e: + print(f"✗ API统计信息重置失败: {e}") + + def set_api_config(self, config: Dict[str, Any]) -> bool: + """ + 设置API配置 + + Args: + config: API配置字典 + + Returns: + 是否设置成功 + """ + try: + old_port = self.api_config.get("api_port") + new_port = config.get("api_port", old_port) + + # 如果端口发生变化,重启服务器 + if old_port != new_port and self.api_state["is_running"]: + self._stop_http_server() + self.api_config.update(config) + self._start_http_server() + else: + self.api_config.update(config) + + print(f"✓ API配置已更新: {self.api_config}") + return True + + except Exception as e: + print(f"✗ API配置设置失败: {e}") + return False + + def get_api_config(self) -> Dict[str, Any]: + """ + 获取API配置 + + Returns: + API配置字典 + """ + return self.api_config.copy() + + def _trigger_api_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发API回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.api_callbacks: + for callback in self.api_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ API回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ API回调触发失败: {e}") + + def register_api_callback(self, callback_type: str, callback: callable): + """ + 注册API回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.api_callbacks: + self.api_callbacks[callback_type].append(callback) + print(f"✓ API回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ API回调注册失败: {e}") + + def unregister_api_callback(self, callback_type: str, callback: callable): + """ + 注销API回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.api_callbacks: + if callback in self.api_callbacks[callback_type]: + self.api_callbacks[callback_type].remove(callback) + print(f"✓ API回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ API回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/server_architecture/clients/client_manager.py b/plugins/user/server_architecture/clients/client_manager.py index 7a7166ea..f7f1e211 100644 --- a/plugins/user/server_architecture/clients/client_manager.py +++ b/plugins/user/server_architecture/clients/client_manager.py @@ -1,847 +1,847 @@ -""" -客户端管理器模块 -负责管理客户端连接、认证和断开 -""" - -import time -import threading -import uuid -from typing import Dict, Any, List, Optional - -class ClientManager: - """ - 客户端管理器 - 负责管理客户端连接、认证和断开 - """ - - def __init__(self, plugin): - """ - 初始化客户端管理器 - - Args: - plugin: 服务器架构插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 客户端配置 - self.client_config = { - "enable_authentication": True, - "auth_timeout": 30.0, - "max_clients": 1000, - "enable_heartbeat": True, - "heartbeat_interval": 30.0, - "enable_rate_limiting": True, - "max_requests_per_second": 100, - "enable_banning": True, - "ban_duration": 3600 # 1小时 - } - - # 客户端存储 - self.clients = {} # 已认证客户端 - self.pending_clients = {} # 待认证客户端 - self.banned_clients = {} # 被禁止的客户端 - - # 客户端统计 - self.client_stats = { - "total_clients": 0, - "authenticated_clients": 0, - "pending_clients": 0, - "disconnected_clients": 0, - "banned_clients": 0, - "authentication_success": 0, - "authentication_failed": 0, - "rate_limit_exceeded": 0, - "heartbeat_timeouts": 0, - "errors": 0 - } - - # 线程锁 - self.clients_lock = threading.RLock() - - # 回调函数 - self.client_callbacks = { - "client_authenticated": [], - "client_disconnected": [], - "client_banned": [], - "authentication_failed": [], - "rate_limit_exceeded": [] - } - - # 时间戳记录 - self.last_cleanup = 0.0 - self.last_heartbeat_check = 0.0 - - print("✓ 客户端管理器已创建") - - def initialize(self) -> bool: - """ - 初始化客户端管理器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化客户端管理器...") - - self.initialized = True - print("✓ 客户端管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 客户端管理器初始化失败: {e}") - self.client_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用客户端管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 客户端管理器未初始化") - return False - - self.enabled = True - print("✓ 客户端管理器已启用") - return True - - except Exception as e: - print(f"✗ 客户端管理器启用失败: {e}") - self.client_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用客户端管理器""" - try: - self.enabled = False - - # 断开所有客户端 - self.disconnect_all_clients() - - print("✓ 客户端管理器已禁用") - - except Exception as e: - print(f"✗ 客户端管理器禁用失败: {e}") - self.client_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理客户端管理器资源""" - try: - # 禁用管理器 - if self.enabled: - self.disable() - - # 清理回调 - self.client_callbacks.clear() - - self.initialized = False - print("✓ 客户端管理器资源已清理") - - except Exception as e: - print(f"✗ 客户端管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新客户端管理器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 定期清理过期数据 - if current_time - self.last_cleanup > 60.0: # 每分钟清理一次 - self._cleanup_expired_data(current_time) - self.last_cleanup = current_time - - # 检查心跳超时 - if self.client_config["enable_heartbeat"]: - if current_time - self.last_heartbeat_check > self.client_config["heartbeat_interval"]: - self._check_heartbeat_timeouts(current_time) - self.last_heartbeat_check = current_time - - except Exception as e: - print(f"✗ 客户端管理器更新失败: {e}") - self.client_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def _cleanup_expired_data(self, current_time: float): - """清理过期数据""" - try: - # 清理过期的待认证客户端 - expired_pending = [] - for client_id, client_data in self.pending_clients.items(): - if current_time - client_data["connect_time"] > self.client_config["auth_timeout"]: - expired_pending.append(client_id) - - for client_id in expired_pending: - del self.pending_clients[client_id] - self.client_stats["pending_clients"] -= 1 - - # 清理过期的禁止列表 - expired_bans = [] - for client_addr, ban_data in self.banned_clients.items(): - if current_time - ban_data["ban_time"] > self.client_config["ban_duration"]: - expired_bans.append(client_addr) - - for client_addr in expired_bans: - del self.banned_clients[client_addr] - self.client_stats["banned_clients"] -= 1 - - except Exception as e: - print(f"✗ 过期数据清理失败: {e}") - self.client_stats["errors"] += 1 - - def _check_heartbeat_timeouts(self, current_time: float): - """检查心跳超时""" - try: - timed_out_clients = [] - with self.clients_lock: - for client_id, client_data in self.clients.items(): - last_heartbeat = client_data.get("last_heartbeat", client_data["connect_time"]) - if current_time - last_heartbeat > self.client_config["heartbeat_interval"] * 3: - timed_out_clients.append(client_id) - - for client_id in timed_out_clients: - self.disconnect_client(client_id, "heartbeat_timeout") - self.client_stats["heartbeat_timeouts"] += 1 - - except Exception as e: - print(f"✗ 心跳超时检查失败: {e}") - self.client_stats["errors"] += 1 - - def add_pending_client(self, client_socket, client_address) -> str: - """ - 添加待认证客户端 - - Args: - client_socket: 客户端套接字 - client_address: 客户端地址 - - Returns: - 客户端ID - """ - try: - # 检查是否被禁止 - if self._is_client_banned(client_address): - client_socket.close() - self.client_stats["banned_clients"] += 1 - return "" - - # 检查连接限制 - if len(self.pending_clients) + len(self.clients) >= self.client_config["max_clients"]: - client_socket.close() - return "" - - # 生成客户端ID - client_id = str(uuid.uuid4()) - - # 添加到待认证列表 - with self.clients_lock: - self.pending_clients[client_id] = { - "socket": client_socket, - "address": client_address, - "connect_time": time.time(), - "last_activity": time.time(), - "request_count": 0, - "last_request_time": time.time() - } - - self.client_stats["pending_clients"] += 1 - - return client_id - - except Exception as e: - print(f"✗ 添加待认证客户端失败: {e}") - self.client_stats["errors"] += 1 - return "" - - def authenticate_client(self, client_id: str, auth_data: Dict[str, Any]) -> bool: - """ - 认证客户端 - - Args: - client_id: 客户端ID - auth_data: 认证数据 - - Returns: - 是否认证成功 - """ - try: - if not self.enabled: - return False - - if not self.client_config["enable_authentication"]: - # 如果禁用认证,直接通过 - return self._complete_authentication(client_id, auth_data) - - # 检查客户端是否在待认证列表中 - if client_id not in self.pending_clients: - self.client_stats["authentication_failed"] += 1 - return False - - # 使用安全管理系统进行认证 - if self.plugin.security_manager: - auth_result = self.plugin.security_manager.authenticate_client( - client_id, - auth_data, - self.pending_clients[client_id]["address"] - ) - - if auth_result: - return self._complete_authentication(client_id, auth_data) - else: - self.client_stats["authentication_failed"] += 1 - - # 触发认证失败回调 - self._trigger_client_callback("authentication_failed", { - "client_id": client_id, - "auth_data": auth_data, - "timestamp": time.time() - }) - - # 移除待认证客户端 - with self.clients_lock: - if client_id in self.pending_clients: - del self.pending_clients[client_id] - self.client_stats["pending_clients"] -= 1 - - return False - else: - # 如果没有安全管理系统,使用简单认证 - return self._complete_authentication(client_id, auth_data) - - except Exception as e: - print(f"✗ 客户端认证失败: {e}") - self.client_stats["errors"] += 1 - return False - - def _complete_authentication(self, client_id: str, auth_data: Dict[str, Any]) -> bool: - """ - 完成客户端认证 - - Args: - client_id: 客户端ID - auth_data: 认证数据 - - Returns: - 是否完成认证 - """ - try: - # 检查客户端是否在待认证列表中 - if client_id not in self.pending_clients: - return False - - # 获取待认证客户端数据 - pending_client = self.pending_clients[client_id] - - # 创建已认证客户端数据 - client_data = { - "socket": pending_client["socket"], - "address": pending_client["address"], - "connect_time": pending_client["connect_time"], - "last_activity": time.time(), - "last_heartbeat": time.time(), - "user_id": auth_data.get("user_id", ""), - "username": auth_data.get("username", ""), - "permissions": auth_data.get("permissions", []), - "session_id": str(uuid.uuid4()), - "bytes_sent": 0, - "bytes_received": 0, - "messages_sent": 0, - "messages_received": 0 - } - - # 移动客户端到已认证列表 - with self.clients_lock: - self.clients[client_id] = client_data - del self.pending_clients[client_id] - - self.client_stats["authenticated_clients"] += 1 - self.client_stats["pending_clients"] -= 1 - self.client_stats["authentication_success"] += 1 - - # 触发客户端认证回调 - self._trigger_client_callback("client_authenticated", { - "client_id": client_id, - "user_id": client_data["user_id"], - "username": client_data["username"], - "address": client_data["address"], - "timestamp": client_data["last_activity"] - }) - - return True - - except Exception as e: - print(f"✗ 完成客户端认证失败: {e}") - self.client_stats["errors"] += 1 - return False - - def disconnect_client(self, client_id: str, reason: str = "unknown"): - """ - 断开客户端连接 - - Args: - client_id: 客户端ID - reason: 断开原因 - """ - try: - client_data = None - with self.clients_lock: - if client_id in self.clients: - client_data = self.clients[client_id] - del self.clients[client_id] - self.client_stats["authenticated_clients"] -= 1 - elif client_id in self.pending_clients: - client_data = self.pending_clients[client_id] - del self.pending_clients[client_id] - self.client_stats["pending_clients"] -= 1 - - if client_data: - # 关闭套接字 - try: - client_data["socket"].close() - except: - pass - - self.client_stats["disconnected_clients"] += 1 - - # 触发客户端断开回调 - self._trigger_client_callback("client_disconnected", { - "client_id": client_id, - "address": client_data["address"], - "reason": reason, - "timestamp": time.time() - }) - - print(f"✓ 客户端已断开: {client_id} ({reason})") - - except Exception as e: - print(f"✗ 断开客户端连接失败: {e}") - self.client_stats["errors"] += 1 - - def disconnect_all_clients(self): - """断开所有客户端连接""" - try: - with self.clients_lock: - # 断开已认证客户端 - client_ids = list(self.clients.keys()) - for client_id in client_ids: - self.disconnect_client(client_id, "server_shutdown") - - # 断开待认证客户端 - pending_ids = list(self.pending_clients.keys()) - for client_id in pending_ids: - self.disconnect_client(client_id, "server_shutdown") - - except Exception as e: - print(f"✗ 断开所有客户端连接失败: {e}") - self.client_stats["errors"] += 1 - - def ban_client(self, client_address, reason: str = "unknown"): - """ - 禁止客户端连接 - - Args: - client_address: 客户端地址 - reason: 禁止原因 - """ - try: - if not self.client_config["enable_banning"]: - return - - ban_time = time.time() - self.banned_clients[client_address] = { - "ban_time": ban_time, - "reason": reason, - "ban_duration": self.client_config["ban_duration"] - } - - self.client_stats["banned_clients"] += 1 - - # 断开该地址的所有客户端 - self._disconnect_clients_by_address(client_address) - - # 触发客户端禁止回调 - self._trigger_client_callback("client_banned", { - "address": client_address, - "reason": reason, - "ban_time": ban_time, - "duration": self.client_config["ban_duration"] - }) - - print(f"✓ 客户端已被禁止: {client_address} ({reason})") - - except Exception as e: - print(f"✗ 禁止客户端失败: {e}") - self.client_stats["errors"] += 1 - - def _disconnect_clients_by_address(self, client_address): - """ - 根据地址断开客户端连接 - - Args: - client_address: 客户端地址 - """ - try: - # 断开已认证客户端 - clients_to_disconnect = [] - with self.clients_lock: - for client_id, client_data in self.clients.items(): - if client_data["address"] == client_address: - clients_to_disconnect.append(client_id) - - for client_id in clients_to_disconnect: - self.disconnect_client(client_id, "banned") - - # 断开待认证客户端 - pending_to_disconnect = [] - with self.clients_lock: - for client_id, client_data in self.pending_clients.items(): - if client_data["address"] == client_address: - pending_to_disconnect.append(client_id) - - for client_id in pending_to_disconnect: - self.disconnect_client(client_id, "banned") - - except Exception as e: - print(f"✗ 根据地址断开客户端失败: {e}") - self.client_stats["errors"] += 1 - - def _is_client_banned(self, client_address) -> bool: - """ - 检查客户端是否被禁止 - - Args: - client_address: 客户端地址 - - Returns: - 是否被禁止 - """ - try: - if not self.client_config["enable_banning"]: - return False - - if client_address in self.banned_clients: - ban_data = self.banned_clients[client_address] - if time.time() - ban_data["ban_time"] < ban_data["ban_duration"]: - return True - else: - # 禁止时间已过期,移除 - del self.banned_clients[client_address] - self.client_stats["banned_clients"] -= 1 - - return False - - except Exception as e: - print(f"✗ 检查客户端禁止状态失败: {e}") - self.client_stats["errors"] += 1 - return False - - def update_client_activity(self, client_id: str): - """ - 更新客户端活动时间 - - Args: - client_id: 客户端ID - """ - try: - current_time = time.time() - with self.clients_lock: - if client_id in self.clients: - self.clients[client_id]["last_activity"] = current_time - elif client_id in self.pending_clients: - self.pending_clients[client_id]["last_activity"] = current_time - - except Exception as e: - print(f"✗ 更新客户端活动时间失败: {e}") - self.client_stats["errors"] += 1 - - def update_client_heartbeat(self, client_id: str): - """ - 更新客户端心跳时间 - - Args: - client_id: 客户端ID - """ - try: - current_time = time.time() - with self.clients_lock: - if client_id in self.clients: - self.clients[client_id]["last_heartbeat"] = current_time - - except Exception as e: - print(f"✗ 更新客户端心跳时间失败: {e}") - self.client_stats["errors"] += 1 - - def send_heartbeat(self): - """发送心跳消息到所有客户端""" - try: - if not self.client_config["enable_heartbeat"]: - return - - heartbeat_message = b'{"type": "heartbeat"}' - with self.clients_lock: - for client_id, client_data in self.clients.items(): - try: - client_data["socket"].send(heartbeat_message) - client_data["messages_sent"] += 1 - client_data["bytes_sent"] += len(heartbeat_message) - except Exception as e: - print(f"✗ 心跳消息发送失败: {client_id} - {e}") - self.client_stats["errors"] += 1 - - except Exception as e: - print(f"✗ 发送心跳消息失败: {e}") - self.client_stats["errors"] += 1 - - def get_client(self, client_id: str) -> Optional[Dict[str, Any]]: - """ - 获取客户端信息 - - Args: - client_id: 客户端ID - - Returns: - 客户端信息或None - """ - try: - with self.clients_lock: - return self.clients.get(client_id, {}).copy() - except Exception as e: - print(f"✗ 获取客户端信息失败: {e}") - self.client_stats["errors"] += 1 - return None - - def get_all_clients(self) -> Dict[str, Dict[str, Any]]: - """ - 获取所有客户端信息 - - Returns: - 所有客户端信息字典 - """ - try: - with self.clients_lock: - return {k: v.copy() for k, v in self.clients.items()} - except Exception as e: - print(f"✗ 获取所有客户端信息失败: {e}") - self.client_stats["errors"] += 1 - return {} - - def get_client_count(self) -> int: - """ - 获取客户端数量 - - Returns: - 客户端数量 - """ - try: - with self.clients_lock: - return len(self.clients) - except Exception as e: - print(f"✗ 获取客户端数量失败: {e}") - self.client_stats["errors"] += 1 - return 0 - - def is_client_authenticated(self, client_id: str) -> bool: - """ - 检查客户端是否已认证 - - Args: - client_id: 客户端ID - - Returns: - 客户端是否已认证 - """ - try: - with self.clients_lock: - return client_id in self.clients - except Exception as e: - print(f"✗ 检查客户端认证状态失败: {e}") - self.client_stats["errors"] += 1 - return False - - def check_rate_limit(self, client_id: str) -> bool: - """ - 检查客户端速率限制 - - Args: - client_id: 客户端ID - - Returns: - 是否超过速率限制 - """ - try: - if not self.client_config["enable_rate_limiting"]: - return False # 未启用速率限制 - - current_time = time.time() - with self.clients_lock: - client_data = None - if client_id in self.clients: - client_data = self.clients[client_id] - elif client_id in self.pending_clients: - client_data = self.pending_clients[client_id] - else: - return False # 客户端不存在 - - # 计算请求速率 - time_diff = current_time - client_data["last_request_time"] - if time_diff >= 1.0: # 重置计数器 - client_data["request_count"] = 0 - client_data["last_request_time"] = current_time - - client_data["request_count"] += 1 - - # 检查是否超过限制 - if client_data["request_count"] > self.client_config["max_requests_per_second"]: - self.client_stats["rate_limit_exceeded"] += 1 - - # 触发速率限制回调 - self._trigger_client_callback("rate_limit_exceeded", { - "client_id": client_id, - "request_count": client_data["request_count"], - "max_requests": self.client_config["max_requests_per_second"], - "timestamp": current_time - }) - - return True # 超过限制 - - return False # 未超过限制 - - except Exception as e: - print(f"✗ 检查速率限制失败: {e}") - self.client_stats["errors"] += 1 - return False - - def get_client_stats(self) -> Dict[str, Any]: - """ - 获取客户端统计信息 - - Returns: - 客户端统计字典 - """ - return { - "clients": self.client_stats.copy(), - "config": self.client_config.copy(), - "current_clients": len(self.clients), - "current_pending": len(self.pending_clients), - "current_banned": len(self.banned_clients) - } - - def reset_stats(self): - """重置客户端统计信息""" - try: - self.client_stats = { - "total_clients": 0, - "authenticated_clients": 0, - "pending_clients": 0, - "disconnected_clients": 0, - "banned_clients": 0, - "authentication_success": 0, - "authentication_failed": 0, - "rate_limit_exceeded": 0, - "heartbeat_timeouts": 0, - "errors": 0 - } - print("✓ 客户端统计信息已重置") - except Exception as e: - print(f"✗ 客户端统计信息重置失败: {e}") - - def set_client_config(self, config: Dict[str, Any]) -> bool: - """ - 设置客户端配置 - - Args: - config: 客户端配置字典 - - Returns: - 是否设置成功 - """ - try: - self.client_config.update(config) - print(f"✓ 客户端配置已更新: {self.client_config}") - return True - except Exception as e: - print(f"✗ 客户端配置设置失败: {e}") - return False - - def get_client_config(self) -> Dict[str, Any]: - """ - 获取客户端配置 - - Returns: - 客户端配置字典 - """ - return self.client_config.copy() - - def _trigger_client_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发客户端回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.client_callbacks: - for callback in self.client_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 客户端回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 客户端回调触发失败: {e}") - - def register_client_callback(self, callback_type: str, callback: callable): - """ - 注册客户端回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.client_callbacks: - self.client_callbacks[callback_type].append(callback) - print(f"✓ 客户端回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 客户端回调注册失败: {e}") - - def unregister_client_callback(self, callback_type: str, callback: callable): - """ - 注销客户端回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.client_callbacks: - if callback in self.client_callbacks[callback_type]: - self.client_callbacks[callback_type].remove(callback) - print(f"✓ 客户端回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +客户端管理器模块 +负责管理客户端连接、认证和断开 +""" + +import time +import threading +import uuid +from typing import Dict, Any, List, Optional + +class ClientManager: + """ + 客户端管理器 + 负责管理客户端连接、认证和断开 + """ + + def __init__(self, plugin): + """ + 初始化客户端管理器 + + Args: + plugin: 服务器架构插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 客户端配置 + self.client_config = { + "enable_authentication": True, + "auth_timeout": 30.0, + "max_clients": 1000, + "enable_heartbeat": True, + "heartbeat_interval": 30.0, + "enable_rate_limiting": True, + "max_requests_per_second": 100, + "enable_banning": True, + "ban_duration": 3600 # 1小时 + } + + # 客户端存储 + self.clients = {} # 已认证客户端 + self.pending_clients = {} # 待认证客户端 + self.banned_clients = {} # 被禁止的客户端 + + # 客户端统计 + self.client_stats = { + "total_clients": 0, + "authenticated_clients": 0, + "pending_clients": 0, + "disconnected_clients": 0, + "banned_clients": 0, + "authentication_success": 0, + "authentication_failed": 0, + "rate_limit_exceeded": 0, + "heartbeat_timeouts": 0, + "errors": 0 + } + + # 线程锁 + self.clients_lock = threading.RLock() + + # 回调函数 + self.client_callbacks = { + "client_authenticated": [], + "client_disconnected": [], + "client_banned": [], + "authentication_failed": [], + "rate_limit_exceeded": [] + } + + # 时间戳记录 + self.last_cleanup = 0.0 + self.last_heartbeat_check = 0.0 + + print("✓ 客户端管理器已创建") + + def initialize(self) -> bool: + """ + 初始化客户端管理器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化客户端管理器...") + + self.initialized = True + print("✓ 客户端管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 客户端管理器初始化失败: {e}") + self.client_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用客户端管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 客户端管理器未初始化") + return False + + self.enabled = True + print("✓ 客户端管理器已启用") + return True + + except Exception as e: + print(f"✗ 客户端管理器启用失败: {e}") + self.client_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用客户端管理器""" + try: + self.enabled = False + + # 断开所有客户端 + self.disconnect_all_clients() + + print("✓ 客户端管理器已禁用") + + except Exception as e: + print(f"✗ 客户端管理器禁用失败: {e}") + self.client_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理客户端管理器资源""" + try: + # 禁用管理器 + if self.enabled: + self.disable() + + # 清理回调 + self.client_callbacks.clear() + + self.initialized = False + print("✓ 客户端管理器资源已清理") + + except Exception as e: + print(f"✗ 客户端管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新客户端管理器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 定期清理过期数据 + if current_time - self.last_cleanup > 60.0: # 每分钟清理一次 + self._cleanup_expired_data(current_time) + self.last_cleanup = current_time + + # 检查心跳超时 + if self.client_config["enable_heartbeat"]: + if current_time - self.last_heartbeat_check > self.client_config["heartbeat_interval"]: + self._check_heartbeat_timeouts(current_time) + self.last_heartbeat_check = current_time + + except Exception as e: + print(f"✗ 客户端管理器更新失败: {e}") + self.client_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def _cleanup_expired_data(self, current_time: float): + """清理过期数据""" + try: + # 清理过期的待认证客户端 + expired_pending = [] + for client_id, client_data in self.pending_clients.items(): + if current_time - client_data["connect_time"] > self.client_config["auth_timeout"]: + expired_pending.append(client_id) + + for client_id in expired_pending: + del self.pending_clients[client_id] + self.client_stats["pending_clients"] -= 1 + + # 清理过期的禁止列表 + expired_bans = [] + for client_addr, ban_data in self.banned_clients.items(): + if current_time - ban_data["ban_time"] > self.client_config["ban_duration"]: + expired_bans.append(client_addr) + + for client_addr in expired_bans: + del self.banned_clients[client_addr] + self.client_stats["banned_clients"] -= 1 + + except Exception as e: + print(f"✗ 过期数据清理失败: {e}") + self.client_stats["errors"] += 1 + + def _check_heartbeat_timeouts(self, current_time: float): + """检查心跳超时""" + try: + timed_out_clients = [] + with self.clients_lock: + for client_id, client_data in self.clients.items(): + last_heartbeat = client_data.get("last_heartbeat", client_data["connect_time"]) + if current_time - last_heartbeat > self.client_config["heartbeat_interval"] * 3: + timed_out_clients.append(client_id) + + for client_id in timed_out_clients: + self.disconnect_client(client_id, "heartbeat_timeout") + self.client_stats["heartbeat_timeouts"] += 1 + + except Exception as e: + print(f"✗ 心跳超时检查失败: {e}") + self.client_stats["errors"] += 1 + + def add_pending_client(self, client_socket, client_address) -> str: + """ + 添加待认证客户端 + + Args: + client_socket: 客户端套接字 + client_address: 客户端地址 + + Returns: + 客户端ID + """ + try: + # 检查是否被禁止 + if self._is_client_banned(client_address): + client_socket.close() + self.client_stats["banned_clients"] += 1 + return "" + + # 检查连接限制 + if len(self.pending_clients) + len(self.clients) >= self.client_config["max_clients"]: + client_socket.close() + return "" + + # 生成客户端ID + client_id = str(uuid.uuid4()) + + # 添加到待认证列表 + with self.clients_lock: + self.pending_clients[client_id] = { + "socket": client_socket, + "address": client_address, + "connect_time": time.time(), + "last_activity": time.time(), + "request_count": 0, + "last_request_time": time.time() + } + + self.client_stats["pending_clients"] += 1 + + return client_id + + except Exception as e: + print(f"✗ 添加待认证客户端失败: {e}") + self.client_stats["errors"] += 1 + return "" + + def authenticate_client(self, client_id: str, auth_data: Dict[str, Any]) -> bool: + """ + 认证客户端 + + Args: + client_id: 客户端ID + auth_data: 认证数据 + + Returns: + 是否认证成功 + """ + try: + if not self.enabled: + return False + + if not self.client_config["enable_authentication"]: + # 如果禁用认证,直接通过 + return self._complete_authentication(client_id, auth_data) + + # 检查客户端是否在待认证列表中 + if client_id not in self.pending_clients: + self.client_stats["authentication_failed"] += 1 + return False + + # 使用安全管理系统进行认证 + if self.plugin.security_manager: + auth_result = self.plugin.security_manager.authenticate_client( + client_id, + auth_data, + self.pending_clients[client_id]["address"] + ) + + if auth_result: + return self._complete_authentication(client_id, auth_data) + else: + self.client_stats["authentication_failed"] += 1 + + # 触发认证失败回调 + self._trigger_client_callback("authentication_failed", { + "client_id": client_id, + "auth_data": auth_data, + "timestamp": time.time() + }) + + # 移除待认证客户端 + with self.clients_lock: + if client_id in self.pending_clients: + del self.pending_clients[client_id] + self.client_stats["pending_clients"] -= 1 + + return False + else: + # 如果没有安全管理系统,使用简单认证 + return self._complete_authentication(client_id, auth_data) + + except Exception as e: + print(f"✗ 客户端认证失败: {e}") + self.client_stats["errors"] += 1 + return False + + def _complete_authentication(self, client_id: str, auth_data: Dict[str, Any]) -> bool: + """ + 完成客户端认证 + + Args: + client_id: 客户端ID + auth_data: 认证数据 + + Returns: + 是否完成认证 + """ + try: + # 检查客户端是否在待认证列表中 + if client_id not in self.pending_clients: + return False + + # 获取待认证客户端数据 + pending_client = self.pending_clients[client_id] + + # 创建已认证客户端数据 + client_data = { + "socket": pending_client["socket"], + "address": pending_client["address"], + "connect_time": pending_client["connect_time"], + "last_activity": time.time(), + "last_heartbeat": time.time(), + "user_id": auth_data.get("user_id", ""), + "username": auth_data.get("username", ""), + "permissions": auth_data.get("permissions", []), + "session_id": str(uuid.uuid4()), + "bytes_sent": 0, + "bytes_received": 0, + "messages_sent": 0, + "messages_received": 0 + } + + # 移动客户端到已认证列表 + with self.clients_lock: + self.clients[client_id] = client_data + del self.pending_clients[client_id] + + self.client_stats["authenticated_clients"] += 1 + self.client_stats["pending_clients"] -= 1 + self.client_stats["authentication_success"] += 1 + + # 触发客户端认证回调 + self._trigger_client_callback("client_authenticated", { + "client_id": client_id, + "user_id": client_data["user_id"], + "username": client_data["username"], + "address": client_data["address"], + "timestamp": client_data["last_activity"] + }) + + return True + + except Exception as e: + print(f"✗ 完成客户端认证失败: {e}") + self.client_stats["errors"] += 1 + return False + + def disconnect_client(self, client_id: str, reason: str = "unknown"): + """ + 断开客户端连接 + + Args: + client_id: 客户端ID + reason: 断开原因 + """ + try: + client_data = None + with self.clients_lock: + if client_id in self.clients: + client_data = self.clients[client_id] + del self.clients[client_id] + self.client_stats["authenticated_clients"] -= 1 + elif client_id in self.pending_clients: + client_data = self.pending_clients[client_id] + del self.pending_clients[client_id] + self.client_stats["pending_clients"] -= 1 + + if client_data: + # 关闭套接字 + try: + client_data["socket"].close() + except: + pass + + self.client_stats["disconnected_clients"] += 1 + + # 触发客户端断开回调 + self._trigger_client_callback("client_disconnected", { + "client_id": client_id, + "address": client_data["address"], + "reason": reason, + "timestamp": time.time() + }) + + print(f"✓ 客户端已断开: {client_id} ({reason})") + + except Exception as e: + print(f"✗ 断开客户端连接失败: {e}") + self.client_stats["errors"] += 1 + + def disconnect_all_clients(self): + """断开所有客户端连接""" + try: + with self.clients_lock: + # 断开已认证客户端 + client_ids = list(self.clients.keys()) + for client_id in client_ids: + self.disconnect_client(client_id, "server_shutdown") + + # 断开待认证客户端 + pending_ids = list(self.pending_clients.keys()) + for client_id in pending_ids: + self.disconnect_client(client_id, "server_shutdown") + + except Exception as e: + print(f"✗ 断开所有客户端连接失败: {e}") + self.client_stats["errors"] += 1 + + def ban_client(self, client_address, reason: str = "unknown"): + """ + 禁止客户端连接 + + Args: + client_address: 客户端地址 + reason: 禁止原因 + """ + try: + if not self.client_config["enable_banning"]: + return + + ban_time = time.time() + self.banned_clients[client_address] = { + "ban_time": ban_time, + "reason": reason, + "ban_duration": self.client_config["ban_duration"] + } + + self.client_stats["banned_clients"] += 1 + + # 断开该地址的所有客户端 + self._disconnect_clients_by_address(client_address) + + # 触发客户端禁止回调 + self._trigger_client_callback("client_banned", { + "address": client_address, + "reason": reason, + "ban_time": ban_time, + "duration": self.client_config["ban_duration"] + }) + + print(f"✓ 客户端已被禁止: {client_address} ({reason})") + + except Exception as e: + print(f"✗ 禁止客户端失败: {e}") + self.client_stats["errors"] += 1 + + def _disconnect_clients_by_address(self, client_address): + """ + 根据地址断开客户端连接 + + Args: + client_address: 客户端地址 + """ + try: + # 断开已认证客户端 + clients_to_disconnect = [] + with self.clients_lock: + for client_id, client_data in self.clients.items(): + if client_data["address"] == client_address: + clients_to_disconnect.append(client_id) + + for client_id in clients_to_disconnect: + self.disconnect_client(client_id, "banned") + + # 断开待认证客户端 + pending_to_disconnect = [] + with self.clients_lock: + for client_id, client_data in self.pending_clients.items(): + if client_data["address"] == client_address: + pending_to_disconnect.append(client_id) + + for client_id in pending_to_disconnect: + self.disconnect_client(client_id, "banned") + + except Exception as e: + print(f"✗ 根据地址断开客户端失败: {e}") + self.client_stats["errors"] += 1 + + def _is_client_banned(self, client_address) -> bool: + """ + 检查客户端是否被禁止 + + Args: + client_address: 客户端地址 + + Returns: + 是否被禁止 + """ + try: + if not self.client_config["enable_banning"]: + return False + + if client_address in self.banned_clients: + ban_data = self.banned_clients[client_address] + if time.time() - ban_data["ban_time"] < ban_data["ban_duration"]: + return True + else: + # 禁止时间已过期,移除 + del self.banned_clients[client_address] + self.client_stats["banned_clients"] -= 1 + + return False + + except Exception as e: + print(f"✗ 检查客户端禁止状态失败: {e}") + self.client_stats["errors"] += 1 + return False + + def update_client_activity(self, client_id: str): + """ + 更新客户端活动时间 + + Args: + client_id: 客户端ID + """ + try: + current_time = time.time() + with self.clients_lock: + if client_id in self.clients: + self.clients[client_id]["last_activity"] = current_time + elif client_id in self.pending_clients: + self.pending_clients[client_id]["last_activity"] = current_time + + except Exception as e: + print(f"✗ 更新客户端活动时间失败: {e}") + self.client_stats["errors"] += 1 + + def update_client_heartbeat(self, client_id: str): + """ + 更新客户端心跳时间 + + Args: + client_id: 客户端ID + """ + try: + current_time = time.time() + with self.clients_lock: + if client_id in self.clients: + self.clients[client_id]["last_heartbeat"] = current_time + + except Exception as e: + print(f"✗ 更新客户端心跳时间失败: {e}") + self.client_stats["errors"] += 1 + + def send_heartbeat(self): + """发送心跳消息到所有客户端""" + try: + if not self.client_config["enable_heartbeat"]: + return + + heartbeat_message = b'{"type": "heartbeat"}' + with self.clients_lock: + for client_id, client_data in self.clients.items(): + try: + client_data["socket"].send(heartbeat_message) + client_data["messages_sent"] += 1 + client_data["bytes_sent"] += len(heartbeat_message) + except Exception as e: + print(f"✗ 心跳消息发送失败: {client_id} - {e}") + self.client_stats["errors"] += 1 + + except Exception as e: + print(f"✗ 发送心跳消息失败: {e}") + self.client_stats["errors"] += 1 + + def get_client(self, client_id: str) -> Optional[Dict[str, Any]]: + """ + 获取客户端信息 + + Args: + client_id: 客户端ID + + Returns: + 客户端信息或None + """ + try: + with self.clients_lock: + return self.clients.get(client_id, {}).copy() + except Exception as e: + print(f"✗ 获取客户端信息失败: {e}") + self.client_stats["errors"] += 1 + return None + + def get_all_clients(self) -> Dict[str, Dict[str, Any]]: + """ + 获取所有客户端信息 + + Returns: + 所有客户端信息字典 + """ + try: + with self.clients_lock: + return {k: v.copy() for k, v in self.clients.items()} + except Exception as e: + print(f"✗ 获取所有客户端信息失败: {e}") + self.client_stats["errors"] += 1 + return {} + + def get_client_count(self) -> int: + """ + 获取客户端数量 + + Returns: + 客户端数量 + """ + try: + with self.clients_lock: + return len(self.clients) + except Exception as e: + print(f"✗ 获取客户端数量失败: {e}") + self.client_stats["errors"] += 1 + return 0 + + def is_client_authenticated(self, client_id: str) -> bool: + """ + 检查客户端是否已认证 + + Args: + client_id: 客户端ID + + Returns: + 客户端是否已认证 + """ + try: + with self.clients_lock: + return client_id in self.clients + except Exception as e: + print(f"✗ 检查客户端认证状态失败: {e}") + self.client_stats["errors"] += 1 + return False + + def check_rate_limit(self, client_id: str) -> bool: + """ + 检查客户端速率限制 + + Args: + client_id: 客户端ID + + Returns: + 是否超过速率限制 + """ + try: + if not self.client_config["enable_rate_limiting"]: + return False # 未启用速率限制 + + current_time = time.time() + with self.clients_lock: + client_data = None + if client_id in self.clients: + client_data = self.clients[client_id] + elif client_id in self.pending_clients: + client_data = self.pending_clients[client_id] + else: + return False # 客户端不存在 + + # 计算请求速率 + time_diff = current_time - client_data["last_request_time"] + if time_diff >= 1.0: # 重置计数器 + client_data["request_count"] = 0 + client_data["last_request_time"] = current_time + + client_data["request_count"] += 1 + + # 检查是否超过限制 + if client_data["request_count"] > self.client_config["max_requests_per_second"]: + self.client_stats["rate_limit_exceeded"] += 1 + + # 触发速率限制回调 + self._trigger_client_callback("rate_limit_exceeded", { + "client_id": client_id, + "request_count": client_data["request_count"], + "max_requests": self.client_config["max_requests_per_second"], + "timestamp": current_time + }) + + return True # 超过限制 + + return False # 未超过限制 + + except Exception as e: + print(f"✗ 检查速率限制失败: {e}") + self.client_stats["errors"] += 1 + return False + + def get_client_stats(self) -> Dict[str, Any]: + """ + 获取客户端统计信息 + + Returns: + 客户端统计字典 + """ + return { + "clients": self.client_stats.copy(), + "config": self.client_config.copy(), + "current_clients": len(self.clients), + "current_pending": len(self.pending_clients), + "current_banned": len(self.banned_clients) + } + + def reset_stats(self): + """重置客户端统计信息""" + try: + self.client_stats = { + "total_clients": 0, + "authenticated_clients": 0, + "pending_clients": 0, + "disconnected_clients": 0, + "banned_clients": 0, + "authentication_success": 0, + "authentication_failed": 0, + "rate_limit_exceeded": 0, + "heartbeat_timeouts": 0, + "errors": 0 + } + print("✓ 客户端统计信息已重置") + except Exception as e: + print(f"✗ 客户端统计信息重置失败: {e}") + + def set_client_config(self, config: Dict[str, Any]) -> bool: + """ + 设置客户端配置 + + Args: + config: 客户端配置字典 + + Returns: + 是否设置成功 + """ + try: + self.client_config.update(config) + print(f"✓ 客户端配置已更新: {self.client_config}") + return True + except Exception as e: + print(f"✗ 客户端配置设置失败: {e}") + return False + + def get_client_config(self) -> Dict[str, Any]: + """ + 获取客户端配置 + + Returns: + 客户端配置字典 + """ + return self.client_config.copy() + + def _trigger_client_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发客户端回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.client_callbacks: + for callback in self.client_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 客户端回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 客户端回调触发失败: {e}") + + def register_client_callback(self, callback_type: str, callback: callable): + """ + 注册客户端回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.client_callbacks: + self.client_callbacks[callback_type].append(callback) + print(f"✓ 客户端回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 客户端回调注册失败: {e}") + + def unregister_client_callback(self, callback_type: str, callback: callable): + """ + 注销客户端回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.client_callbacks: + if callback in self.client_callbacks[callback_type]: + self.client_callbacks[callback_type].remove(callback) + print(f"✓ 客户端回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 客户端回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/server_architecture/config/config_manager.py b/plugins/user/server_architecture/config/config_manager.py index eb7936fb..c6278171 100644 --- a/plugins/user/server_architecture/config/config_manager.py +++ b/plugins/user/server_architecture/config/config_manager.py @@ -1,757 +1,757 @@ -""" -配置管理模块 -负责管理服务器配置参数 -""" - -import time -import json -import os -from typing import Dict, Any, List, Optional - -class ConfigManager: - """ - 配置管理器 - 负责管理服务器配置参数 - """ - - def __init__(self, plugin): - """ - 初始化配置管理器 - - Args: - plugin: 服务器架构插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 配置文件路径 - self.config_file_path = "/home/hello/EG/plugins/user/server_architecture/config/server_config.json" - - # 默认配置 - self.default_config = { - "server": { - "host": "0.0.0.0", - "port": 8080, - "max_connections": 1000, - "enable_ssl": False, - "ssl_cert_file": "", - "ssl_key_file": "", - "connection_timeout": 30.0, - "heartbeat_interval": 5.0 - }, - "client": { - "enable_authentication": True, - "auth_timeout": 30.0, - "max_clients": 1000, - "enable_heartbeat": True, - "heartbeat_interval": 30.0, - "enable_rate_limiting": True, - "max_requests_per_second": 100, - "enable_banning": True, - "ban_duration": 3600 - }, - "room": { - "max_rooms": 100, - "max_clients_per_room": 50, - "enable_room_persistence": False, - "room_timeout": 3600, - "enable_room_passwords": True - }, - "storage": { - "database_path": "/home/hello/EG/plugins/user/server_architecture/data/server_database.db", - "enable_caching": True, - "cache_size": 1000, - "enable_compression": True, - "backup_interval": 3600, - "enable_auto_backup": True - }, - "loadbalancer": { - "enable_load_balancing": True, - "balancing_algorithm": "round_robin", - "health_check_interval": 30.0, - "enable_health_check": True - }, - "monitoring": { - "enable_monitoring": True, - "monitoring_interval": 5.0, - "log_level": "INFO", - "enable_file_logging": True, - "log_file_path": "/home/hello/EG/plugins/user/server_architecture/logs/server_monitor.log" - }, - "security": { - "enable_encryption": False, - "enable_message_authentication": True, - "enable_session_keys": True, - "session_key_lifetime": 3600 - }, - "api": { - "enable_api": True, - "api_port": 8081, - "enable_cors": True, - "allowed_origins": ["*"] - } - } - - # 当前配置 - self.current_config = self.default_config.copy() - - # 配置状态 - self.config_state = { - "last_loaded": 0.0, - "last_saved": 0.0, - "config_version": "1.0.0", - "is_dirty": False - } - - # 配置统计 - self.config_stats = { - "loads": 0, - "saves": 0, - "updates": 0, - "errors": 0 - } - - # 配置监听器 - self.config_listeners = {} - - # 回调函数 - self.config_callbacks = { - "config_loaded": [], - "config_saved": [], - "config_updated": [], - "config_error": [] - } - - print("✓ 配置管理器已创建") - - def initialize(self) -> bool: - """ - 初始化配置管理器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化配置管理器...") - - # 创建配置目录 - config_dir = os.path.dirname(self.config_file_path) - if not os.path.exists(config_dir): - os.makedirs(config_dir) - - # 创建日志目录 - if "monitoring" in self.default_config: - log_dir = os.path.dirname(self.default_config["monitoring"]["log_file_path"]) - if not os.path.exists(log_dir): - os.makedirs(log_dir) - - # 创建数据目录 - if "storage" in self.default_config: - data_dir = os.path.dirname(self.default_config["storage"]["database_path"]) - if not os.path.exists(data_dir): - os.makedirs(data_dir) - - # 加载配置 - self.load_config() - - self.initialized = True - print("✓ 配置管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 配置管理器初始化失败: {e}") - self.config_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用配置管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 配置管理器未初始化") - return False - - self.enabled = True - print("✓ 配置管理器已启用") - return True - - except Exception as e: - print(f"✗ 配置管理器启用失败: {e}") - self.config_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用配置管理器""" - try: - self.enabled = False - - # 保存配置(如果有更改) - if self.config_state["is_dirty"]: - self.save_config() - - print("✓ 配置管理器已禁用") - - except Exception as e: - print(f"✗ 配置管理器禁用失败: {e}") - self.config_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理配置管理器资源""" - try: - # 禁用配置管理器 - if self.enabled: - self.disable() - - # 清理回调 - self.config_callbacks.clear() - self.config_listeners.clear() - - self.initialized = False - print("✓ 配置管理器资源已清理") - - except Exception as e: - print(f"✗ 配置管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新配置管理器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - # 检查配置文件是否被外部修改 - self._check_config_file_changes() - - except Exception as e: - print(f"✗ 配置管理器更新失败: {e}") - self.config_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def _check_config_file_changes(self): - """检查配置文件是否被外部修改""" - try: - if not os.path.exists(self.config_file_path): - return - - last_modified = os.path.getmtime(self.config_file_path) - if last_modified > self.config_state["last_loaded"]: - # 配置文件已被修改,重新加载 - print("检测到配置文件被外部修改,正在重新加载...") - self.load_config() - - except Exception as e: - print(f"✗ 配置文件变更检查失败: {e}") - self.config_stats["errors"] += 1 - - def load_config(self) -> bool: - """ - 加载配置 - - Returns: - 是否加载成功 - """ - try: - # 检查配置文件是否存在 - if os.path.exists(self.config_file_path): - with open(self.config_file_path, 'r', encoding='utf-8') as f: - loaded_config = json.load(f) - - # 合并配置(使用默认配置作为基础) - self.current_config = self._merge_config(self.default_config, loaded_config) - - print(f"✓ 配置已从文件加载: {self.config_file_path}") - else: - # 使用默认配置 - self.current_config = self.default_config.copy() - print("✓ 使用默认配置") - - self.config_state["last_loaded"] = time.time() - self.config_state["is_dirty"] = False - self.config_stats["loads"] += 1 - - # 触发配置加载回调 - self._trigger_config_callback("config_loaded", { - "config": self.current_config, - "timestamp": self.config_state["last_loaded"] - }) - - return True - - except Exception as e: - print(f"✗ 配置加载失败: {e}") - self.config_stats["errors"] += 1 - - # 使用默认配置 - self.current_config = self.default_config.copy() - return False - - def _merge_config(self, base_config: Dict[str, Any], override_config: Dict[str, Any]) -> Dict[str, Any]: - """ - 合并配置(递归) - - Args: - base_config: 基础配置 - override_config: 覆盖配置 - - Returns: - 合并后的配置 - """ - try: - merged = base_config.copy() - - for key, value in override_config.items(): - if key in merged and isinstance(merged[key], dict) and isinstance(value, dict): - # 递归合并嵌套字典 - merged[key] = self._merge_config(merged[key], value) - else: - # 直接覆盖 - merged[key] = value - - return merged - - except Exception as e: - print(f"✗ 配置合并失败: {e}") - self.config_stats["errors"] += 1 - return base_config - - def save_config(self) -> bool: - """ - 保存配置 - - Returns: - 是否保存成功 - """ - try: - # 创建配置目录(如果不存在) - config_dir = os.path.dirname(self.config_file_path) - if not os.path.exists(config_dir): - os.makedirs(config_dir) - - # 保存配置到文件 - with open(self.config_file_path, 'w', encoding='utf-8') as f: - json.dump(self.current_config, f, indent=2, ensure_ascii=False) - - self.config_state["last_saved"] = time.time() - self.config_state["is_dirty"] = False - self.config_stats["saves"] += 1 - - # 触发配置保存回调 - self._trigger_config_callback("config_saved", { - "config": self.current_config, - "timestamp": self.config_state["last_saved"] - }) - - print(f"✓ 配置已保存到文件: {self.config_file_path}") - return True - - except Exception as e: - print(f"✗ 配置保存失败: {e}") - self.config_stats["errors"] += 1 - return False - - def get_config(self, section: str = None, key: str = None, default_value: Any = None) -> Any: - """ - 获取配置值 - - Args: - section: 配置节(可选) - key: 配置键(可选) - default_value: 默认值 - - Returns: - 配置值 - """ - try: - if section is None: - return self.current_config.copy() - - if section not in self.current_config: - return default_value - - if key is None: - return self.current_config[section].copy() - - if key not in self.current_config[section]: - return default_value - - return self.current_config[section][key] - - except Exception as e: - print(f"✗ 获取配置失败: {e}") - self.config_stats["errors"] += 1 - return default_value - - def set_config(self, section: str, key: str, value: Any) -> bool: - """ - 设置配置值 - - Args: - section: 配置节 - key: 配置键 - value: 配置值 - - Returns: - 是否设置成功 - """ - try: - if section not in self.current_config: - self.current_config[section] = {} - - self.current_config[section][key] = value - self.config_state["is_dirty"] = True - self.config_stats["updates"] += 1 - - # 通知配置监听器 - self._notify_config_listeners(section, key, value) - - # 触发配置更新回调 - self._trigger_config_callback("config_updated", { - "section": section, - "key": key, - "value": value, - "timestamp": time.time() - }) - - return True - - except Exception as e: - print(f"✗ 设置配置失败: {e}") - self.config_stats["errors"] += 1 - return False - - def update_config_section(self, section: str, config_data: Dict[str, Any]) -> bool: - """ - 更新配置节 - - Args: - section: 配置节 - config_data: 配置数据 - - Returns: - 是否更新成功 - """ - try: - if section not in self.current_config: - self.current_config[section] = {} - - self.current_config[section].update(config_data) - self.config_state["is_dirty"] = True - self.config_stats["updates"] += 1 - - # 通知配置监听器 - for key, value in config_data.items(): - self._notify_config_listeners(section, key, value) - - # 触发配置更新回调 - self._trigger_config_callback("config_updated", { - "section": section, - "config_data": config_data, - "timestamp": time.time() - }) - - return True - - except Exception as e: - print(f"✗ 更新配置节失败: {e}") - self.config_stats["errors"] += 1 - return False - - def reset_config(self) -> bool: - """ - 重置配置为默认值 - - Returns: - 是否重置成功 - """ - try: - self.current_config = self.default_config.copy() - self.config_state["is_dirty"] = True - - # 通知所有配置监听器 - for section_name, section_data in self.current_config.items(): - if isinstance(section_data, dict): - for key, value in section_data.items(): - self._notify_config_listeners(section_name, key, value) - - print("✓ 配置已重置为默认值") - return True - - except Exception as e: - print(f"✗ 配置重置失败: {e}") - self.config_stats["errors"] += 1 - return False - - def register_config_listener(self, section: str, key: str, listener: callable): - """ - 注册配置监听器 - - Args: - section: 配置节 - key: 配置键 - listener: 监听器函数 - """ - try: - listener_key = f"{section}.{key}" - if listener_key not in self.config_listeners: - self.config_listeners[listener_key] = [] - - self.config_listeners[listener_key].append(listener) - print(f"✓ 配置监听器已注册: {listener_key}") - - except Exception as e: - print(f"✗ 配置监听器注册失败: {e}") - self.config_stats["errors"] += 1 - - def unregister_config_listener(self, section: str, key: str, listener: callable): - """ - 注销配置监听器 - - Args: - section: 配置节 - key: 配置键 - listener: 监听器函数 - """ - try: - listener_key = f"{section}.{key}" - if listener_key in self.config_listeners: - if listener in self.config_listeners[listener_key]: - self.config_listeners[listener_key].remove(listener) - print(f"✓ 配置监听器已注销: {listener_key}") - - except Exception as e: - print(f"✗ 配置监听器注销失败: {e}") - self.config_stats["errors"] += 1 - - def _notify_config_listeners(self, section: str, key: str, value: Any): - """ - 通知配置监听器 - - Args: - section: 配置节 - key: 配置键 - value: 配置值 - """ - try: - listener_key = f"{section}.{key}" - if listener_key in self.config_listeners: - for listener in self.config_listeners[listener_key]: - try: - listener(section, key, value) - except Exception as e: - print(f"✗ 配置监听器执行失败: {listener_key} - {e}") - - except Exception as e: - print(f"✗ 配置监听器通知失败: {e}") - self.config_stats["errors"] += 1 - - def get_config_stats(self) -> Dict[str, Any]: - """ - 获取配置统计信息 - - Returns: - 配置统计字典 - """ - return { - "state": self.config_state.copy(), - "stats": self.config_stats.copy(), - "config_sections": list(self.current_config.keys()) - } - - def reset_stats(self): - """重置配置统计信息""" - try: - self.config_stats = { - "loads": 0, - "saves": 0, - "updates": 0, - "errors": 0 - } - print("✓ 配置统计信息已重置") - except Exception as e: - print(f"✗ 配置统计信息重置失败: {e}") - - def validate_config(self) -> bool: - """ - 验证配置 - - Returns: - 配置是否有效 - """ - try: - # 检查必需的配置项 - required_sections = ["server", "client", "room", "storage"] - for section in required_sections: - if section not in self.current_config: - print(f"✗ 缺少必需的配置节: {section}") - return False - - # 检查服务器配置 - server_config = self.current_config["server"] - if "port" not in server_config or not isinstance(server_config["port"], int): - print("✗ 无效的服务器端口配置") - return False - - if "max_connections" not in server_config or not isinstance(server_config["max_connections"], int): - print("✗ 无效的最大连接数配置") - return False - - # 检查客户端配置 - client_config = self.current_config["client"] - if "max_clients" not in client_config or not isinstance(client_config["max_clients"], int): - print("✗ 无效的最大客户端数配置") - return False - - # 检查存储配置 - storage_config = self.current_config["storage"] - if "database_path" not in storage_config or not isinstance(storage_config["database_path"], str): - print("✗ 无效的数据库路径配置") - return False - - print("✓ 配置验证通过") - return True - - except Exception as e: - print(f"✗ 配置验证失败: {e}") - self.config_stats["errors"] += 1 - return False - - def export_config(self, file_path: str) -> bool: - """ - 导出配置到文件 - - Args: - file_path: 导出文件路径 - - Returns: - 是否导出成功 - """ - try: - # 创建导出目录 - export_dir = os.path.dirname(file_path) - if not os.path.exists(export_dir): - os.makedirs(export_dir) - - # 导出配置 - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(self.current_config, f, indent=2, ensure_ascii=False) - - print(f"✓ 配置已导出到: {file_path}") - return True - - except Exception as e: - print(f"✗ 配置导出失败: {e}") - self.config_stats["errors"] += 1 - return False - - def import_config(self, file_path: str) -> bool: - """ - 从文件导入配置 - - Args: - file_path: 导入文件路径 - - Returns: - 是否导入成功 - """ - try: - if not os.path.exists(file_path): - print(f"✗ 配置文件不存在: {file_path}") - return False - - # 读取配置文件 - with open(file_path, 'r', encoding='utf-8') as f: - imported_config = json.load(f) - - # 验证配置 - if not isinstance(imported_config, dict): - print("✗ 无效的配置文件格式") - return False - - # 合并配置 - self.current_config = self._merge_config(self.default_config, imported_config) - self.config_state["is_dirty"] = True - - # 通知配置监听器 - for section_name, section_data in self.current_config.items(): - if isinstance(section_data, dict): - for key, value in section_data.items(): - self._notify_config_listeners(section_name, key, value) - - print(f"✓ 配置已从文件导入: {file_path}") - return True - - except Exception as e: - print(f"✗ 配置导入失败: {e}") - self.config_stats["errors"] += 1 - return False - - def _trigger_config_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发配置回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.config_callbacks: - for callback in self.config_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 配置回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 配置回调触发失败: {e}") - - def register_config_callback(self, callback_type: str, callback: callable): - """ - 注册配置回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.config_callbacks: - self.config_callbacks[callback_type].append(callback) - print(f"✓ 配置回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 配置回调注册失败: {e}") - - def unregister_config_callback(self, callback_type: str, callback: callable): - """ - 注销配置回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.config_callbacks: - if callback in self.config_callbacks[callback_type]: - self.config_callbacks[callback_type].remove(callback) - print(f"✓ 配置回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +配置管理模块 +负责管理服务器配置参数 +""" + +import time +import json +import os +from typing import Dict, Any, List, Optional + +class ConfigManager: + """ + 配置管理器 + 负责管理服务器配置参数 + """ + + def __init__(self, plugin): + """ + 初始化配置管理器 + + Args: + plugin: 服务器架构插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 配置文件路径 + self.config_file_path = "/home/hello/EG/plugins/user/server_architecture/config/server_config.json" + + # 默认配置 + self.default_config = { + "server": { + "host": "0.0.0.0", + "port": 8080, + "max_connections": 1000, + "enable_ssl": False, + "ssl_cert_file": "", + "ssl_key_file": "", + "connection_timeout": 30.0, + "heartbeat_interval": 5.0 + }, + "client": { + "enable_authentication": True, + "auth_timeout": 30.0, + "max_clients": 1000, + "enable_heartbeat": True, + "heartbeat_interval": 30.0, + "enable_rate_limiting": True, + "max_requests_per_second": 100, + "enable_banning": True, + "ban_duration": 3600 + }, + "room": { + "max_rooms": 100, + "max_clients_per_room": 50, + "enable_room_persistence": False, + "room_timeout": 3600, + "enable_room_passwords": True + }, + "storage": { + "database_path": "/home/hello/EG/plugins/user/server_architecture/data/server_database.db", + "enable_caching": True, + "cache_size": 1000, + "enable_compression": True, + "backup_interval": 3600, + "enable_auto_backup": True + }, + "loadbalancer": { + "enable_load_balancing": True, + "balancing_algorithm": "round_robin", + "health_check_interval": 30.0, + "enable_health_check": True + }, + "monitoring": { + "enable_monitoring": True, + "monitoring_interval": 5.0, + "log_level": "INFO", + "enable_file_logging": True, + "log_file_path": "/home/hello/EG/plugins/user/server_architecture/logs/server_monitor.log" + }, + "security": { + "enable_encryption": False, + "enable_message_authentication": True, + "enable_session_keys": True, + "session_key_lifetime": 3600 + }, + "api": { + "enable_api": True, + "api_port": 8081, + "enable_cors": True, + "allowed_origins": ["*"] + } + } + + # 当前配置 + self.current_config = self.default_config.copy() + + # 配置状态 + self.config_state = { + "last_loaded": 0.0, + "last_saved": 0.0, + "config_version": "1.0.0", + "is_dirty": False + } + + # 配置统计 + self.config_stats = { + "loads": 0, + "saves": 0, + "updates": 0, + "errors": 0 + } + + # 配置监听器 + self.config_listeners = {} + + # 回调函数 + self.config_callbacks = { + "config_loaded": [], + "config_saved": [], + "config_updated": [], + "config_error": [] + } + + print("✓ 配置管理器已创建") + + def initialize(self) -> bool: + """ + 初始化配置管理器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化配置管理器...") + + # 创建配置目录 + config_dir = os.path.dirname(self.config_file_path) + if not os.path.exists(config_dir): + os.makedirs(config_dir) + + # 创建日志目录 + if "monitoring" in self.default_config: + log_dir = os.path.dirname(self.default_config["monitoring"]["log_file_path"]) + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + # 创建数据目录 + if "storage" in self.default_config: + data_dir = os.path.dirname(self.default_config["storage"]["database_path"]) + if not os.path.exists(data_dir): + os.makedirs(data_dir) + + # 加载配置 + self.load_config() + + self.initialized = True + print("✓ 配置管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 配置管理器初始化失败: {e}") + self.config_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用配置管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 配置管理器未初始化") + return False + + self.enabled = True + print("✓ 配置管理器已启用") + return True + + except Exception as e: + print(f"✗ 配置管理器启用失败: {e}") + self.config_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用配置管理器""" + try: + self.enabled = False + + # 保存配置(如果有更改) + if self.config_state["is_dirty"]: + self.save_config() + + print("✓ 配置管理器已禁用") + + except Exception as e: + print(f"✗ 配置管理器禁用失败: {e}") + self.config_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理配置管理器资源""" + try: + # 禁用配置管理器 + if self.enabled: + self.disable() + + # 清理回调 + self.config_callbacks.clear() + self.config_listeners.clear() + + self.initialized = False + print("✓ 配置管理器资源已清理") + + except Exception as e: + print(f"✗ 配置管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新配置管理器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + # 检查配置文件是否被外部修改 + self._check_config_file_changes() + + except Exception as e: + print(f"✗ 配置管理器更新失败: {e}") + self.config_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def _check_config_file_changes(self): + """检查配置文件是否被外部修改""" + try: + if not os.path.exists(self.config_file_path): + return + + last_modified = os.path.getmtime(self.config_file_path) + if last_modified > self.config_state["last_loaded"]: + # 配置文件已被修改,重新加载 + print("检测到配置文件被外部修改,正在重新加载...") + self.load_config() + + except Exception as e: + print(f"✗ 配置文件变更检查失败: {e}") + self.config_stats["errors"] += 1 + + def load_config(self) -> bool: + """ + 加载配置 + + Returns: + 是否加载成功 + """ + try: + # 检查配置文件是否存在 + if os.path.exists(self.config_file_path): + with open(self.config_file_path, 'r', encoding='utf-8') as f: + loaded_config = json.load(f) + + # 合并配置(使用默认配置作为基础) + self.current_config = self._merge_config(self.default_config, loaded_config) + + print(f"✓ 配置已从文件加载: {self.config_file_path}") + else: + # 使用默认配置 + self.current_config = self.default_config.copy() + print("✓ 使用默认配置") + + self.config_state["last_loaded"] = time.time() + self.config_state["is_dirty"] = False + self.config_stats["loads"] += 1 + + # 触发配置加载回调 + self._trigger_config_callback("config_loaded", { + "config": self.current_config, + "timestamp": self.config_state["last_loaded"] + }) + + return True + + except Exception as e: + print(f"✗ 配置加载失败: {e}") + self.config_stats["errors"] += 1 + + # 使用默认配置 + self.current_config = self.default_config.copy() + return False + + def _merge_config(self, base_config: Dict[str, Any], override_config: Dict[str, Any]) -> Dict[str, Any]: + """ + 合并配置(递归) + + Args: + base_config: 基础配置 + override_config: 覆盖配置 + + Returns: + 合并后的配置 + """ + try: + merged = base_config.copy() + + for key, value in override_config.items(): + if key in merged and isinstance(merged[key], dict) and isinstance(value, dict): + # 递归合并嵌套字典 + merged[key] = self._merge_config(merged[key], value) + else: + # 直接覆盖 + merged[key] = value + + return merged + + except Exception as e: + print(f"✗ 配置合并失败: {e}") + self.config_stats["errors"] += 1 + return base_config + + def save_config(self) -> bool: + """ + 保存配置 + + Returns: + 是否保存成功 + """ + try: + # 创建配置目录(如果不存在) + config_dir = os.path.dirname(self.config_file_path) + if not os.path.exists(config_dir): + os.makedirs(config_dir) + + # 保存配置到文件 + with open(self.config_file_path, 'w', encoding='utf-8') as f: + json.dump(self.current_config, f, indent=2, ensure_ascii=False) + + self.config_state["last_saved"] = time.time() + self.config_state["is_dirty"] = False + self.config_stats["saves"] += 1 + + # 触发配置保存回调 + self._trigger_config_callback("config_saved", { + "config": self.current_config, + "timestamp": self.config_state["last_saved"] + }) + + print(f"✓ 配置已保存到文件: {self.config_file_path}") + return True + + except Exception as e: + print(f"✗ 配置保存失败: {e}") + self.config_stats["errors"] += 1 + return False + + def get_config(self, section: str = None, key: str = None, default_value: Any = None) -> Any: + """ + 获取配置值 + + Args: + section: 配置节(可选) + key: 配置键(可选) + default_value: 默认值 + + Returns: + 配置值 + """ + try: + if section is None: + return self.current_config.copy() + + if section not in self.current_config: + return default_value + + if key is None: + return self.current_config[section].copy() + + if key not in self.current_config[section]: + return default_value + + return self.current_config[section][key] + + except Exception as e: + print(f"✗ 获取配置失败: {e}") + self.config_stats["errors"] += 1 + return default_value + + def set_config(self, section: str, key: str, value: Any) -> bool: + """ + 设置配置值 + + Args: + section: 配置节 + key: 配置键 + value: 配置值 + + Returns: + 是否设置成功 + """ + try: + if section not in self.current_config: + self.current_config[section] = {} + + self.current_config[section][key] = value + self.config_state["is_dirty"] = True + self.config_stats["updates"] += 1 + + # 通知配置监听器 + self._notify_config_listeners(section, key, value) + + # 触发配置更新回调 + self._trigger_config_callback("config_updated", { + "section": section, + "key": key, + "value": value, + "timestamp": time.time() + }) + + return True + + except Exception as e: + print(f"✗ 设置配置失败: {e}") + self.config_stats["errors"] += 1 + return False + + def update_config_section(self, section: str, config_data: Dict[str, Any]) -> bool: + """ + 更新配置节 + + Args: + section: 配置节 + config_data: 配置数据 + + Returns: + 是否更新成功 + """ + try: + if section not in self.current_config: + self.current_config[section] = {} + + self.current_config[section].update(config_data) + self.config_state["is_dirty"] = True + self.config_stats["updates"] += 1 + + # 通知配置监听器 + for key, value in config_data.items(): + self._notify_config_listeners(section, key, value) + + # 触发配置更新回调 + self._trigger_config_callback("config_updated", { + "section": section, + "config_data": config_data, + "timestamp": time.time() + }) + + return True + + except Exception as e: + print(f"✗ 更新配置节失败: {e}") + self.config_stats["errors"] += 1 + return False + + def reset_config(self) -> bool: + """ + 重置配置为默认值 + + Returns: + 是否重置成功 + """ + try: + self.current_config = self.default_config.copy() + self.config_state["is_dirty"] = True + + # 通知所有配置监听器 + for section_name, section_data in self.current_config.items(): + if isinstance(section_data, dict): + for key, value in section_data.items(): + self._notify_config_listeners(section_name, key, value) + + print("✓ 配置已重置为默认值") + return True + + except Exception as e: + print(f"✗ 配置重置失败: {e}") + self.config_stats["errors"] += 1 + return False + + def register_config_listener(self, section: str, key: str, listener: callable): + """ + 注册配置监听器 + + Args: + section: 配置节 + key: 配置键 + listener: 监听器函数 + """ + try: + listener_key = f"{section}.{key}" + if listener_key not in self.config_listeners: + self.config_listeners[listener_key] = [] + + self.config_listeners[listener_key].append(listener) + print(f"✓ 配置监听器已注册: {listener_key}") + + except Exception as e: + print(f"✗ 配置监听器注册失败: {e}") + self.config_stats["errors"] += 1 + + def unregister_config_listener(self, section: str, key: str, listener: callable): + """ + 注销配置监听器 + + Args: + section: 配置节 + key: 配置键 + listener: 监听器函数 + """ + try: + listener_key = f"{section}.{key}" + if listener_key in self.config_listeners: + if listener in self.config_listeners[listener_key]: + self.config_listeners[listener_key].remove(listener) + print(f"✓ 配置监听器已注销: {listener_key}") + + except Exception as e: + print(f"✗ 配置监听器注销失败: {e}") + self.config_stats["errors"] += 1 + + def _notify_config_listeners(self, section: str, key: str, value: Any): + """ + 通知配置监听器 + + Args: + section: 配置节 + key: 配置键 + value: 配置值 + """ + try: + listener_key = f"{section}.{key}" + if listener_key in self.config_listeners: + for listener in self.config_listeners[listener_key]: + try: + listener(section, key, value) + except Exception as e: + print(f"✗ 配置监听器执行失败: {listener_key} - {e}") + + except Exception as e: + print(f"✗ 配置监听器通知失败: {e}") + self.config_stats["errors"] += 1 + + def get_config_stats(self) -> Dict[str, Any]: + """ + 获取配置统计信息 + + Returns: + 配置统计字典 + """ + return { + "state": self.config_state.copy(), + "stats": self.config_stats.copy(), + "config_sections": list(self.current_config.keys()) + } + + def reset_stats(self): + """重置配置统计信息""" + try: + self.config_stats = { + "loads": 0, + "saves": 0, + "updates": 0, + "errors": 0 + } + print("✓ 配置统计信息已重置") + except Exception as e: + print(f"✗ 配置统计信息重置失败: {e}") + + def validate_config(self) -> bool: + """ + 验证配置 + + Returns: + 配置是否有效 + """ + try: + # 检查必需的配置项 + required_sections = ["server", "client", "room", "storage"] + for section in required_sections: + if section not in self.current_config: + print(f"✗ 缺少必需的配置节: {section}") + return False + + # 检查服务器配置 + server_config = self.current_config["server"] + if "port" not in server_config or not isinstance(server_config["port"], int): + print("✗ 无效的服务器端口配置") + return False + + if "max_connections" not in server_config or not isinstance(server_config["max_connections"], int): + print("✗ 无效的最大连接数配置") + return False + + # 检查客户端配置 + client_config = self.current_config["client"] + if "max_clients" not in client_config or not isinstance(client_config["max_clients"], int): + print("✗ 无效的最大客户端数配置") + return False + + # 检查存储配置 + storage_config = self.current_config["storage"] + if "database_path" not in storage_config or not isinstance(storage_config["database_path"], str): + print("✗ 无效的数据库路径配置") + return False + + print("✓ 配置验证通过") + return True + + except Exception as e: + print(f"✗ 配置验证失败: {e}") + self.config_stats["errors"] += 1 + return False + + def export_config(self, file_path: str) -> bool: + """ + 导出配置到文件 + + Args: + file_path: 导出文件路径 + + Returns: + 是否导出成功 + """ + try: + # 创建导出目录 + export_dir = os.path.dirname(file_path) + if not os.path.exists(export_dir): + os.makedirs(export_dir) + + # 导出配置 + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(self.current_config, f, indent=2, ensure_ascii=False) + + print(f"✓ 配置已导出到: {file_path}") + return True + + except Exception as e: + print(f"✗ 配置导出失败: {e}") + self.config_stats["errors"] += 1 + return False + + def import_config(self, file_path: str) -> bool: + """ + 从文件导入配置 + + Args: + file_path: 导入文件路径 + + Returns: + 是否导入成功 + """ + try: + if not os.path.exists(file_path): + print(f"✗ 配置文件不存在: {file_path}") + return False + + # 读取配置文件 + with open(file_path, 'r', encoding='utf-8') as f: + imported_config = json.load(f) + + # 验证配置 + if not isinstance(imported_config, dict): + print("✗ 无效的配置文件格式") + return False + + # 合并配置 + self.current_config = self._merge_config(self.default_config, imported_config) + self.config_state["is_dirty"] = True + + # 通知配置监听器 + for section_name, section_data in self.current_config.items(): + if isinstance(section_data, dict): + for key, value in section_data.items(): + self._notify_config_listeners(section_name, key, value) + + print(f"✓ 配置已从文件导入: {file_path}") + return True + + except Exception as e: + print(f"✗ 配置导入失败: {e}") + self.config_stats["errors"] += 1 + return False + + def _trigger_config_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发配置回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.config_callbacks: + for callback in self.config_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 配置回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 配置回调触发失败: {e}") + + def register_config_callback(self, callback_type: str, callback: callable): + """ + 注册配置回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.config_callbacks: + self.config_callbacks[callback_type].append(callback) + print(f"✓ 配置回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 配置回调注册失败: {e}") + + def unregister_config_callback(self, callback_type: str, callback: callable): + """ + 注销配置回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.config_callbacks: + if callback in self.config_callbacks[callback_type]: + self.config_callbacks[callback_type].remove(callback) + print(f"✓ 配置回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 配置回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/server_architecture/core/server_manager.py b/plugins/user/server_architecture/core/server_manager.py index 025d4231..99fc30d7 100644 --- a/plugins/user/server_architecture/core/server_manager.py +++ b/plugins/user/server_architecture/core/server_manager.py @@ -1,663 +1,663 @@ -""" -服务器管理器模块 -负责服务器的启动、停止和状态管理 -""" - -import time -import threading -import socket -from typing import Dict, Any, List, Optional - -class ServerManager: - """ - 服务器管理器 - 负责服务器的启动、停止和状态管理 - """ - - def __init__(self, plugin): - """ - 初始化服务器管理器 - - Args: - plugin: 服务器架构插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - self.running = False - - # 服务器配置 - self.server_config = { - "host": "0.0.0.0", - "port": 8080, - "max_connections": 1000, - "enable_ssl": False, - "ssl_cert_file": "", - "ssl_key_file": "", - "connection_timeout": 30.0, - "heartbeat_interval": 5.0, - "enable_compression": True, - "max_message_size": 65536 - } - - # 服务器状态 - self.server_state = { - "is_running": False, - "start_time": 0.0, - "uptime": 0.0, - "current_connections": 0, - "total_connections": 0, - "peak_connections": 0, - "bytes_sent": 0, - "bytes_received": 0, - "packets_sent": 0, - "packets_received": 0 - } - - # 服务器统计 - self.server_stats = { - "servers_started": 0, - "servers_stopped": 0, - "connection_attempts": 0, - "successful_connections": 0, - "failed_connections": 0, - "disconnections": 0, - "timeouts": 0, - "errors": 0 - } - - # 服务器套接字 - self.server_socket = None - self.ssl_context = None - - # 线程管理 - self.server_thread = None - self.heartbeat_thread = None - self.client_threads = [] - - # 回调函数 - self.server_callbacks = { - "server_started": [], - "server_stopped": [], - "client_connected": [], - "client_disconnected": [], - "error_occurred": [] - } - - # 时间戳记录 - self.last_heartbeat = 0.0 - self.last_update = 0.0 - - print("✓ 服务器管理器已创建") - - def initialize(self) -> bool: - """ - 初始化服务器管理器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化服务器管理器...") - - # 初始化SSL上下文(如果启用) - if self.server_config["enable_ssl"]: - self._initialize_ssl() - - self.initialized = True - print("✓ 服务器管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 服务器管理器初始化失败: {e}") - self.server_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def _initialize_ssl(self): - """初始化SSL上下文""" - try: - import ssl - - self.ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) - if self.server_config["ssl_cert_file"] and self.server_config["ssl_key_file"]: - self.ssl_context.load_cert_chain( - self.server_config["ssl_cert_file"], - self.server_config["ssl_key_file"] - ) - print("✓ SSL上下文已初始化") - except Exception as e: - print(f"✗ SSL初始化失败: {e}") - self.server_config["enable_ssl"] = False - - def enable(self) -> bool: - """ - 启用服务器管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 服务器管理器未初始化") - return False - - self.enabled = True - print("✓ 服务器管理器已启用") - return True - - except Exception as e: - print(f"✗ 服务器管理器启用失败: {e}") - self.server_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用服务器管理器""" - try: - self.enabled = False - - # 停止服务器 - if self.server_state["is_running"]: - self.stop_server() - - print("✓ 服务器管理器已禁用") - - except Exception as e: - print(f"✗ 服务器管理器禁用失败: {e}") - self.server_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理服务器管理器资源""" - try: - # 禁用管理器 - if self.enabled: - self.disable() - - # 清理回调 - self.server_callbacks.clear() - - self.initialized = False - print("✓ 服务器管理器资源已清理") - - except Exception as e: - print(f"✗ 服务器管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新服务器管理器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.last_update = current_time - - # 更新服务器运行时间 - if self.server_state["is_running"]: - self.server_state["uptime"] = current_time - self.server_state["start_time"] - - except Exception as e: - print(f"✗ 服务器管理器更新失败: {e}") - self.server_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def start_server(self, host: str = None, port: int = None) -> bool: - """ - 启动服务器 - - Args: - host: 服务器主机地址 - port: 服务器端口 - - Returns: - 是否启动成功 - """ - try: - if not self.enabled: - print("✗ 服务器管理器未启用") - return False - - if self.server_state["is_running"]: - print("✗ 服务器已在运行") - return False - - # 使用配置的默认值或传入的参数 - if host is None: - host = self.server_config["host"] - if port is None: - port = self.server_config["port"] - - # 创建服务器套接字 - self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - - # 绑定地址和端口 - self.server_socket.bind((host, port)) - self.server_socket.listen(self.server_config["max_connections"]) - - # 如果启用SSL,包装套接字 - if self.server_config["enable_ssl"] and self.ssl_context: - self.server_socket = self.ssl_context.wrap_socket( - self.server_socket, - server_side=True - ) - - self.running = True - self.server_state["is_running"] = True - self.server_state["start_time"] = time.time() - - # 启动服务器线程 - self.server_thread = threading.Thread(target=self._server_loop, daemon=True) - self.server_thread.start() - - # 启动心跳线程 - self.heartbeat_thread = threading.Thread(target=self._heartbeat_loop, daemon=True) - self.heartbeat_thread.start() - - self.server_stats["servers_started"] += 1 - - # 触发服务器启动回调 - self._trigger_server_callback("server_started", { - "host": host, - "port": port, - "timestamp": self.server_state["start_time"] - }) - - print(f"✓ 服务器已启动: {host}:{port}") - return True - - except Exception as e: - print(f"✗ 服务器启动失败: {e}") - self.server_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def stop_server(self): - """停止服务器""" - try: - if not self.server_state["is_running"]: - return - - print("正在停止服务器...") - - # 停止服务器循环 - self.running = False - - # 关闭服务器套接字 - if self.server_socket: - self.server_socket.close() - self.server_socket = None - - # 等待线程结束 - if self.server_thread and self.server_thread.is_alive(): - self.server_thread.join(timeout=5.0) - - if self.heartbeat_thread and self.heartbeat_thread.is_alive(): - self.heartbeat_thread.join(timeout=5.0) - - # 清理客户端线程 - for client_thread in self.client_threads: - if client_thread.is_alive(): - client_thread.join(timeout=1.0) - self.client_threads.clear() - - # 重置服务器状态 - self.server_state["is_running"] = False - self.server_state["start_time"] = 0.0 - self.server_state["uptime"] = 0.0 - - self.server_stats["servers_stopped"] += 1 - - # 触发服务器停止回调 - self._trigger_server_callback("server_stopped", { - "timestamp": time.time() - }) - - print("✓ 服务器已停止") - - except Exception as e: - print(f"✗ 服务器停止失败: {e}") - self.server_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def _server_loop(self): - """服务器主循环""" - try: - print("服务器主循环已启动") - - while self.running and self.server_socket: - try: - # 接受客户端连接 - client_socket, client_address = self.server_socket.accept() - - self.server_stats["connection_attempts"] += 1 - - # 检查连接限制 - if self.server_state["current_connections"] >= self.server_config["max_connections"]: - client_socket.close() - self.server_stats["failed_connections"] += 1 - continue - - # 处理客户端连接 - client_thread = threading.Thread( - target=self._handle_client, - args=(client_socket, client_address), - daemon=True - ) - client_thread.start() - self.client_threads.append(client_thread) - - self.server_stats["successful_connections"] += 1 - self.server_state["current_connections"] += 1 - self.server_state["total_connections"] += 1 - if self.server_state["current_connections"] > self.server_state["peak_connections"]: - self.server_state["peak_connections"] = self.server_state["current_connections"] - - except socket.error as e: - if self.running: # 只在服务器运行时报告错误 - print(f"✗ 接受连接时出错: {e}") - self.server_stats["errors"] += 1 - except Exception as e: - if self.running: - print(f"✗ 服务器循环错误: {e}") - self.server_stats["errors"] += 1 - - except Exception as e: - print(f"✗ 服务器主循环失败: {e}") - self.server_stats["errors"] += 1 - finally: - print("服务器主循环已结束") - - def _handle_client(self, client_socket, client_address): - """ - 处理客户端连接 - - Args: - client_socket: 客户端套接字 - client_address: 客户端地址 - """ - try: - print(f"✓ 客户端已连接: {client_address}") - - # 触发客户端连接回调 - self._trigger_server_callback("client_connected", { - "address": client_address, - "timestamp": time.time() - }) - - # 处理客户端消息循环 - self._client_message_loop(client_socket, client_address) - - except Exception as e: - print(f"✗ 客户端处理失败: {client_address} - {e}") - self.server_stats["errors"] += 1 - finally: - # 清理客户端连接 - try: - client_socket.close() - except: - pass - - self.server_state["current_connections"] -= 1 - self.server_stats["disconnections"] += 1 - - # 触发客户端断开回调 - self._trigger_server_callback("client_disconnected", { - "address": client_address, - "timestamp": time.time() - }) - - print(f"✓ 客户端已断开: {client_address}") - - def _client_message_loop(self, client_socket, client_address): - """ - 客户端消息循环 - - Args: - client_socket: 客户端套接字 - client_address: 客户端地址 - """ - try: - # 设置超时 - client_socket.settimeout(self.server_config["connection_timeout"]) - - while self.running: - try: - # 接收消息 - message = client_socket.recv(self.server_config["max_message_size"]) - if not message: - break - - self.server_state["bytes_received"] += len(message) - self.server_state["packets_received"] += 1 - - # 处理消息 - self._process_client_message(message, client_socket, client_address) - - except socket.timeout: - print(f"✗ 客户端超时: {client_address}") - self.server_stats["timeouts"] += 1 - break - except socket.error as e: - if self.running: - print(f"✗ 客户端通信错误: {client_address} - {e}") - self.server_stats["errors"] += 1 - break - except Exception as e: - if self.running: - print(f"✗ 客户端消息处理错误: {client_address} - {e}") - self.server_stats["errors"] += 1 - break - - except Exception as e: - print(f"✗ 客户端消息循环失败: {client_address} - {e}") - self.server_stats["errors"] += 1 - - def _process_client_message(self, message: bytes, client_socket, client_address): - """ - 处理客户端消息 - - Args: - message: 接收到的消息 - client_socket: 客户端套接字 - client_address: 客户端地址 - """ - try: - # 将消息传递给消息路由系统处理 - if self.plugin.message_router: - self.plugin.message_router.route_message( - message, - client_address, - client_socket - ) - except Exception as e: - print(f"✗ 消息处理失败: {client_address} - {e}") - self.server_stats["errors"] += 1 - - def _heartbeat_loop(self): - """心跳循环""" - try: - print("心跳循环已启动") - - while self.running: - try: - current_time = time.time() - - # 发送心跳消息 - if self.plugin.client_manager: - self.plugin.client_manager.send_heartbeat() - - self.last_heartbeat = current_time - - # 等待下一个心跳间隔 - time.sleep(self.server_config["heartbeat_interval"]) - - except Exception as e: - if self.running: - print(f"✗ 心跳循环错误: {e}") - self.server_stats["errors"] += 1 - time.sleep(1.0) # 出错时短暂休眠 - - except Exception as e: - print(f"✗ 心跳循环失败: {e}") - self.server_stats["errors"] += 1 - finally: - print("心跳循环已结束") - - def send_message_to_client(self, message: bytes, client_socket) -> bool: - """ - 发送消息到客户端 - - Args: - message: 要发送的消息 - client_socket: 客户端套接字 - - Returns: - 是否发送成功 - """ - try: - if not self.server_state["is_running"]: - return False - - client_socket.send(message) - self.server_state["bytes_sent"] += len(message) - self.server_state["packets_sent"] += 1 - return True - - except Exception as e: - print(f"✗ 消息发送失败: {e}") - self.server_stats["errors"] += 1 - return False - - def get_server_stats(self) -> Dict[str, Any]: - """ - 获取服务器统计信息 - - Returns: - 服务器统计字典 - """ - return { - "state": self.server_state.copy(), - "stats": self.server_stats.copy(), - "config": self.server_config.copy() - } - - def reset_stats(self): - """重置服务器统计信息""" - try: - self.server_stats = { - "servers_started": 0, - "servers_stopped": 0, - "connection_attempts": 0, - "successful_connections": 0, - "failed_connections": 0, - "disconnections": 0, - "timeouts": 0, - "errors": 0 - } - print("✓ 服务器统计信息已重置") - except Exception as e: - print(f"✗ 服务器统计信息重置失败: {e}") - - def set_server_config(self, config: Dict[str, Any]) -> bool: - """ - 设置服务器配置 - - Args: - config: 服务器配置字典 - - Returns: - 是否设置成功 - """ - try: - self.server_config.update(config) - print(f"✓ 服务器配置已更新: {self.server_config}") - return True - except Exception as e: - print(f"✗ 服务器配置设置失败: {e}") - return False - - def get_server_config(self) -> Dict[str, Any]: - """ - 获取服务器配置 - - Returns: - 服务器配置字典 - """ - return self.server_config.copy() - - def is_server_running(self) -> bool: - """ - 检查服务器是否正在运行 - - Returns: - 服务器是否正在运行 - """ - return self.server_state["is_running"] - - def _trigger_server_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发服务器回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.server_callbacks: - for callback in self.server_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 服务器回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 服务器回调触发失败: {e}") - - def register_server_callback(self, callback_type: str, callback: callable): - """ - 注册服务器回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.server_callbacks: - self.server_callbacks[callback_type].append(callback) - print(f"✓ 服务器回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 服务器回调注册失败: {e}") - - def unregister_server_callback(self, callback_type: str, callback: callable): - """ - 注销服务器回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.server_callbacks: - if callback in self.server_callbacks[callback_type]: - self.server_callbacks[callback_type].remove(callback) - print(f"✓ 服务器回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +服务器管理器模块 +负责服务器的启动、停止和状态管理 +""" + +import time +import threading +import socket +from typing import Dict, Any, List, Optional + +class ServerManager: + """ + 服务器管理器 + 负责服务器的启动、停止和状态管理 + """ + + def __init__(self, plugin): + """ + 初始化服务器管理器 + + Args: + plugin: 服务器架构插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + self.running = False + + # 服务器配置 + self.server_config = { + "host": "0.0.0.0", + "port": 8080, + "max_connections": 1000, + "enable_ssl": False, + "ssl_cert_file": "", + "ssl_key_file": "", + "connection_timeout": 30.0, + "heartbeat_interval": 5.0, + "enable_compression": True, + "max_message_size": 65536 + } + + # 服务器状态 + self.server_state = { + "is_running": False, + "start_time": 0.0, + "uptime": 0.0, + "current_connections": 0, + "total_connections": 0, + "peak_connections": 0, + "bytes_sent": 0, + "bytes_received": 0, + "packets_sent": 0, + "packets_received": 0 + } + + # 服务器统计 + self.server_stats = { + "servers_started": 0, + "servers_stopped": 0, + "connection_attempts": 0, + "successful_connections": 0, + "failed_connections": 0, + "disconnections": 0, + "timeouts": 0, + "errors": 0 + } + + # 服务器套接字 + self.server_socket = None + self.ssl_context = None + + # 线程管理 + self.server_thread = None + self.heartbeat_thread = None + self.client_threads = [] + + # 回调函数 + self.server_callbacks = { + "server_started": [], + "server_stopped": [], + "client_connected": [], + "client_disconnected": [], + "error_occurred": [] + } + + # 时间戳记录 + self.last_heartbeat = 0.0 + self.last_update = 0.0 + + print("✓ 服务器管理器已创建") + + def initialize(self) -> bool: + """ + 初始化服务器管理器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化服务器管理器...") + + # 初始化SSL上下文(如果启用) + if self.server_config["enable_ssl"]: + self._initialize_ssl() + + self.initialized = True + print("✓ 服务器管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 服务器管理器初始化失败: {e}") + self.server_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def _initialize_ssl(self): + """初始化SSL上下文""" + try: + import ssl + + self.ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + if self.server_config["ssl_cert_file"] and self.server_config["ssl_key_file"]: + self.ssl_context.load_cert_chain( + self.server_config["ssl_cert_file"], + self.server_config["ssl_key_file"] + ) + print("✓ SSL上下文已初始化") + except Exception as e: + print(f"✗ SSL初始化失败: {e}") + self.server_config["enable_ssl"] = False + + def enable(self) -> bool: + """ + 启用服务器管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 服务器管理器未初始化") + return False + + self.enabled = True + print("✓ 服务器管理器已启用") + return True + + except Exception as e: + print(f"✗ 服务器管理器启用失败: {e}") + self.server_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用服务器管理器""" + try: + self.enabled = False + + # 停止服务器 + if self.server_state["is_running"]: + self.stop_server() + + print("✓ 服务器管理器已禁用") + + except Exception as e: + print(f"✗ 服务器管理器禁用失败: {e}") + self.server_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理服务器管理器资源""" + try: + # 禁用管理器 + if self.enabled: + self.disable() + + # 清理回调 + self.server_callbacks.clear() + + self.initialized = False + print("✓ 服务器管理器资源已清理") + + except Exception as e: + print(f"✗ 服务器管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新服务器管理器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.last_update = current_time + + # 更新服务器运行时间 + if self.server_state["is_running"]: + self.server_state["uptime"] = current_time - self.server_state["start_time"] + + except Exception as e: + print(f"✗ 服务器管理器更新失败: {e}") + self.server_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def start_server(self, host: str = None, port: int = None) -> bool: + """ + 启动服务器 + + Args: + host: 服务器主机地址 + port: 服务器端口 + + Returns: + 是否启动成功 + """ + try: + if not self.enabled: + print("✗ 服务器管理器未启用") + return False + + if self.server_state["is_running"]: + print("✗ 服务器已在运行") + return False + + # 使用配置的默认值或传入的参数 + if host is None: + host = self.server_config["host"] + if port is None: + port = self.server_config["port"] + + # 创建服务器套接字 + self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + # 绑定地址和端口 + self.server_socket.bind((host, port)) + self.server_socket.listen(self.server_config["max_connections"]) + + # 如果启用SSL,包装套接字 + if self.server_config["enable_ssl"] and self.ssl_context: + self.server_socket = self.ssl_context.wrap_socket( + self.server_socket, + server_side=True + ) + + self.running = True + self.server_state["is_running"] = True + self.server_state["start_time"] = time.time() + + # 启动服务器线程 + self.server_thread = threading.Thread(target=self._server_loop, daemon=True) + self.server_thread.start() + + # 启动心跳线程 + self.heartbeat_thread = threading.Thread(target=self._heartbeat_loop, daemon=True) + self.heartbeat_thread.start() + + self.server_stats["servers_started"] += 1 + + # 触发服务器启动回调 + self._trigger_server_callback("server_started", { + "host": host, + "port": port, + "timestamp": self.server_state["start_time"] + }) + + print(f"✓ 服务器已启动: {host}:{port}") + return True + + except Exception as e: + print(f"✗ 服务器启动失败: {e}") + self.server_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def stop_server(self): + """停止服务器""" + try: + if not self.server_state["is_running"]: + return + + print("正在停止服务器...") + + # 停止服务器循环 + self.running = False + + # 关闭服务器套接字 + if self.server_socket: + self.server_socket.close() + self.server_socket = None + + # 等待线程结束 + if self.server_thread and self.server_thread.is_alive(): + self.server_thread.join(timeout=5.0) + + if self.heartbeat_thread and self.heartbeat_thread.is_alive(): + self.heartbeat_thread.join(timeout=5.0) + + # 清理客户端线程 + for client_thread in self.client_threads: + if client_thread.is_alive(): + client_thread.join(timeout=1.0) + self.client_threads.clear() + + # 重置服务器状态 + self.server_state["is_running"] = False + self.server_state["start_time"] = 0.0 + self.server_state["uptime"] = 0.0 + + self.server_stats["servers_stopped"] += 1 + + # 触发服务器停止回调 + self._trigger_server_callback("server_stopped", { + "timestamp": time.time() + }) + + print("✓ 服务器已停止") + + except Exception as e: + print(f"✗ 服务器停止失败: {e}") + self.server_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def _server_loop(self): + """服务器主循环""" + try: + print("服务器主循环已启动") + + while self.running and self.server_socket: + try: + # 接受客户端连接 + client_socket, client_address = self.server_socket.accept() + + self.server_stats["connection_attempts"] += 1 + + # 检查连接限制 + if self.server_state["current_connections"] >= self.server_config["max_connections"]: + client_socket.close() + self.server_stats["failed_connections"] += 1 + continue + + # 处理客户端连接 + client_thread = threading.Thread( + target=self._handle_client, + args=(client_socket, client_address), + daemon=True + ) + client_thread.start() + self.client_threads.append(client_thread) + + self.server_stats["successful_connections"] += 1 + self.server_state["current_connections"] += 1 + self.server_state["total_connections"] += 1 + if self.server_state["current_connections"] > self.server_state["peak_connections"]: + self.server_state["peak_connections"] = self.server_state["current_connections"] + + except socket.error as e: + if self.running: # 只在服务器运行时报告错误 + print(f"✗ 接受连接时出错: {e}") + self.server_stats["errors"] += 1 + except Exception as e: + if self.running: + print(f"✗ 服务器循环错误: {e}") + self.server_stats["errors"] += 1 + + except Exception as e: + print(f"✗ 服务器主循环失败: {e}") + self.server_stats["errors"] += 1 + finally: + print("服务器主循环已结束") + + def _handle_client(self, client_socket, client_address): + """ + 处理客户端连接 + + Args: + client_socket: 客户端套接字 + client_address: 客户端地址 + """ + try: + print(f"✓ 客户端已连接: {client_address}") + + # 触发客户端连接回调 + self._trigger_server_callback("client_connected", { + "address": client_address, + "timestamp": time.time() + }) + + # 处理客户端消息循环 + self._client_message_loop(client_socket, client_address) + + except Exception as e: + print(f"✗ 客户端处理失败: {client_address} - {e}") + self.server_stats["errors"] += 1 + finally: + # 清理客户端连接 + try: + client_socket.close() + except: + pass + + self.server_state["current_connections"] -= 1 + self.server_stats["disconnections"] += 1 + + # 触发客户端断开回调 + self._trigger_server_callback("client_disconnected", { + "address": client_address, + "timestamp": time.time() + }) + + print(f"✓ 客户端已断开: {client_address}") + + def _client_message_loop(self, client_socket, client_address): + """ + 客户端消息循环 + + Args: + client_socket: 客户端套接字 + client_address: 客户端地址 + """ + try: + # 设置超时 + client_socket.settimeout(self.server_config["connection_timeout"]) + + while self.running: + try: + # 接收消息 + message = client_socket.recv(self.server_config["max_message_size"]) + if not message: + break + + self.server_state["bytes_received"] += len(message) + self.server_state["packets_received"] += 1 + + # 处理消息 + self._process_client_message(message, client_socket, client_address) + + except socket.timeout: + print(f"✗ 客户端超时: {client_address}") + self.server_stats["timeouts"] += 1 + break + except socket.error as e: + if self.running: + print(f"✗ 客户端通信错误: {client_address} - {e}") + self.server_stats["errors"] += 1 + break + except Exception as e: + if self.running: + print(f"✗ 客户端消息处理错误: {client_address} - {e}") + self.server_stats["errors"] += 1 + break + + except Exception as e: + print(f"✗ 客户端消息循环失败: {client_address} - {e}") + self.server_stats["errors"] += 1 + + def _process_client_message(self, message: bytes, client_socket, client_address): + """ + 处理客户端消息 + + Args: + message: 接收到的消息 + client_socket: 客户端套接字 + client_address: 客户端地址 + """ + try: + # 将消息传递给消息路由系统处理 + if self.plugin.message_router: + self.plugin.message_router.route_message( + message, + client_address, + client_socket + ) + except Exception as e: + print(f"✗ 消息处理失败: {client_address} - {e}") + self.server_stats["errors"] += 1 + + def _heartbeat_loop(self): + """心跳循环""" + try: + print("心跳循环已启动") + + while self.running: + try: + current_time = time.time() + + # 发送心跳消息 + if self.plugin.client_manager: + self.plugin.client_manager.send_heartbeat() + + self.last_heartbeat = current_time + + # 等待下一个心跳间隔 + time.sleep(self.server_config["heartbeat_interval"]) + + except Exception as e: + if self.running: + print(f"✗ 心跳循环错误: {e}") + self.server_stats["errors"] += 1 + time.sleep(1.0) # 出错时短暂休眠 + + except Exception as e: + print(f"✗ 心跳循环失败: {e}") + self.server_stats["errors"] += 1 + finally: + print("心跳循环已结束") + + def send_message_to_client(self, message: bytes, client_socket) -> bool: + """ + 发送消息到客户端 + + Args: + message: 要发送的消息 + client_socket: 客户端套接字 + + Returns: + 是否发送成功 + """ + try: + if not self.server_state["is_running"]: + return False + + client_socket.send(message) + self.server_state["bytes_sent"] += len(message) + self.server_state["packets_sent"] += 1 + return True + + except Exception as e: + print(f"✗ 消息发送失败: {e}") + self.server_stats["errors"] += 1 + return False + + def get_server_stats(self) -> Dict[str, Any]: + """ + 获取服务器统计信息 + + Returns: + 服务器统计字典 + """ + return { + "state": self.server_state.copy(), + "stats": self.server_stats.copy(), + "config": self.server_config.copy() + } + + def reset_stats(self): + """重置服务器统计信息""" + try: + self.server_stats = { + "servers_started": 0, + "servers_stopped": 0, + "connection_attempts": 0, + "successful_connections": 0, + "failed_connections": 0, + "disconnections": 0, + "timeouts": 0, + "errors": 0 + } + print("✓ 服务器统计信息已重置") + except Exception as e: + print(f"✗ 服务器统计信息重置失败: {e}") + + def set_server_config(self, config: Dict[str, Any]) -> bool: + """ + 设置服务器配置 + + Args: + config: 服务器配置字典 + + Returns: + 是否设置成功 + """ + try: + self.server_config.update(config) + print(f"✓ 服务器配置已更新: {self.server_config}") + return True + except Exception as e: + print(f"✗ 服务器配置设置失败: {e}") + return False + + def get_server_config(self) -> Dict[str, Any]: + """ + 获取服务器配置 + + Returns: + 服务器配置字典 + """ + return self.server_config.copy() + + def is_server_running(self) -> bool: + """ + 检查服务器是否正在运行 + + Returns: + 服务器是否正在运行 + """ + return self.server_state["is_running"] + + def _trigger_server_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发服务器回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.server_callbacks: + for callback in self.server_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 服务器回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 服务器回调触发失败: {e}") + + def register_server_callback(self, callback_type: str, callback: callable): + """ + 注册服务器回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.server_callbacks: + self.server_callbacks[callback_type].append(callback) + print(f"✓ 服务器回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 服务器回调注册失败: {e}") + + def unregister_server_callback(self, callback_type: str, callback: callable): + """ + 注销服务器回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.server_callbacks: + if callback in self.server_callbacks[callback_type]: + self.server_callbacks[callback_type].remove(callback) + print(f"✓ 服务器回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 服务器回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/server_architecture/editor/editor_interface.py b/plugins/user/server_architecture/editor/editor_interface.py index 7f62b9df..70cabd19 100644 --- a/plugins/user/server_architecture/editor/editor_interface.py +++ b/plugins/user/server_architecture/editor/editor_interface.py @@ -1,638 +1,638 @@ -""" -编辑器接口模块 -提供可视化配置和监控界面 -""" - -import time -import threading -import json -from typing import Dict, Any, List, Optional - -class EditorInterface: - """ - 编辑器接口 - 提供可视化配置和监控界面 - """ - - def __init__(self, plugin): - """ - 初始化编辑器接口 - - Args: - plugin: 服务器架构插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 编辑器配置 - self.editor_config = { - "enable_editor": True, - "editor_port": 8082, - "enable_realtime_updates": True, - "update_interval": 1.0, - "enable_data_export": True, - "enable_data_import": True, - "max_history_points": 1000, - "enable_theme_customization": True, - "default_theme": "dark" - } - - # 编辑器状态 - self.editor_state = { - "is_running": False, - "last_update": 0.0, - "connected_clients": 0, - "total_updates": 0, - "ui_elements": {}, - "selected_tab": "overview" - } - - # 编辑器数据 - self.editor_data = { - "system_stats": {}, - "performance_metrics": {}, - "active_alerts": [], - "client_list": [], - "room_list": [], - "server_config": {} - } - - # 编辑器统计 - self.editor_stats = { - "updates_sent": 0, - "data_points_collected": 0, - "ui_interactions": 0, - "export_operations": 0, - "import_operations": 0, - "errors": 0 - } - - # 实时更新线程 - self.update_thread = None - self.update_thread_running = False - - # 回调函数 - self.editor_callbacks = { - "editor_started": [], - "editor_stopped": [], - "data_updated": [], - "ui_interaction": [], - "editor_error": [] - } - - # 时间戳记录 - self.last_data_update = 0.0 - self.last_ui_interaction = 0.0 - - print("✓ 编辑器接口已创建") - - def initialize(self) -> bool: - """ - 初始化编辑器接口 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化编辑器接口...") - - # 初始化UI元素 - self._initialize_ui_elements() - - self.initialized = True - print("✓ 编辑器接口初始化完成") - return True - - except Exception as e: - print(f"✗ 编辑器接口初始化失败: {e}") - self.editor_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def _initialize_ui_elements(self): - """初始化UI元素""" - try: - self.editor_state["ui_elements"] = { - "overview_tab": { - "name": "系统概览", - "icon": "dashboard", - "visible": True - }, - "clients_tab": { - "name": "客户端管理", - "icon": "people", - "visible": True - }, - "rooms_tab": { - "name": "房间管理", - "icon": "meeting_room", - "visible": True - }, - "monitoring_tab": { - "name": "监控面板", - "icon": "monitor", - "visible": True - }, - "config_tab": { - "name": "配置管理", - "icon": "settings", - "visible": True - }, - "logs_tab": { - "name": "日志查看", - "icon": "description", - "visible": True - }, - "alerts_tab": { - "name": "警报管理", - "icon": "warning", - "visible": True - } - } - - print("✓ UI元素已初始化") - - except Exception as e: - print(f"✗ UI元素初始化失败: {e}") - self.editor_stats["errors"] += 1 - - def enable(self) -> bool: - """ - 启用编辑器接口 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 编辑器接口未初始化") - return False - - # 启动实时更新线程 - self._start_update_thread() - - self.enabled = True - print("✓ 编辑器接口已启用") - return True - - except Exception as e: - print(f"✗ 编辑器接口启用失败: {e}") - self.editor_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用编辑器接口""" - try: - self.enabled = False - - # 停止实时更新线程 - self._stop_update_thread() - - print("✓ 编辑器接口已禁用") - - except Exception as e: - print(f"✗ 编辑器接口禁用失败: {e}") - self.editor_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理编辑器接口资源""" - try: - # 禁用编辑器接口 - if self.enabled: - self.disable() - - # 清理回调 - self.editor_callbacks.clear() - - self.initialized = False - print("✓ 编辑器接口资源已清理") - - except Exception as e: - print(f"✗ 编辑器接口资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新编辑器接口状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.editor_state["last_update"] = current_time - - except Exception as e: - print(f"✗ 编辑器接口更新失败: {e}") - self.editor_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def _start_update_thread(self): - """启动实时更新线程""" - try: - self.update_thread_running = True - self.update_thread = threading.Thread(target=self._update_loop, daemon=True) - self.update_thread.start() - print("✓ 实时更新线程已启动") - except Exception as e: - print(f"✗ 实时更新线程启动失败: {e}") - self.editor_stats["errors"] += 1 - - def _stop_update_thread(self): - """停止实时更新线程""" - try: - self.update_thread_running = False - if self.update_thread and self.update_thread.is_alive(): - self.update_thread.join(timeout=5.0) - self.update_thread = None - print("✓ 实时更新线程已停止") - except Exception as e: - print(f"✗ 实时更新线程停止失败: {e}") - self.editor_stats["errors"] += 1 - - def _update_loop(self): - """实时更新循环""" - try: - while self.update_thread_running: - try: - if self.enabled and self.editor_config["enable_realtime_updates"]: - # 收集数据 - self._collect_editor_data() - - # 发送更新 - self._send_editor_updates() - - self.editor_state["total_updates"] += 1 - - # 等待下一个更新间隔 - time.sleep(self.editor_config["update_interval"]) - - except Exception as e: - print(f"✗ 实时更新循环错误: {e}") - self.editor_stats["errors"] += 1 - time.sleep(1.0) # 出错时短暂休眠 - - except Exception as e: - print(f"✗ 实时更新线程失败: {e}") - self.editor_stats["errors"] += 1 - - def _collect_editor_data(self): - """收集编辑器数据""" - try: - current_time = time.time() - - # 收集系统统计数据 - self.editor_data["system_stats"] = { - "server": self.plugin.server_manager.get_server_stats() if self.plugin.server_manager else {}, - "clients": self.plugin.client_manager.get_client_stats() if self.plugin.client_manager else {}, - "rooms": self.plugin.room_manager.get_room_stats() if self.plugin.room_manager else {}, - "messages": self.plugin.message_router.get_message_stats() if self.plugin.message_router else {}, - "storage": self.plugin.storage_manager.get_storage_stats() if self.plugin.storage_manager else {}, - "loadbalancer": self.plugin.load_balancer.get_loadbalancer_stats() if self.plugin.load_balancer else {}, - "monitoring": self.plugin.monitoring_system.get_monitoring_stats() if self.plugin.monitoring_system else {}, - "security": self.plugin.security_manager.get_security_stats() if self.plugin.security_manager else {}, - "api": self.plugin.api_interface.get_api_stats() if self.plugin.api_interface else {} - } - - # 收集性能指标 - if self.plugin.monitoring_system: - self.editor_data["performance_metrics"] = self.plugin.monitoring_system.get_performance_metrics() - - # 收集活动警报 - if self.plugin.monitoring_system: - self.editor_data["active_alerts"] = list(self.plugin.monitoring_system.get_active_alerts().values()) - - # 收集客户端列表 - if self.plugin.client_manager: - self.editor_data["client_list"] = list(self.plugin.client_manager.get_all_clients().values()) - - # 收集房间列表 - if self.plugin.room_manager: - self.editor_data["room_list"] = self.plugin.room_manager.get_all_rooms() - - # 收集服务器配置 - if self.plugin.config_manager: - self.editor_data["server_config"] = self.plugin.config_manager.get_config() - - self.editor_stats["data_points_collected"] += 1 - self.last_data_update = current_time - - except Exception as e: - print(f"✗ 编辑器数据收集失败: {e}") - self.editor_stats["errors"] += 1 - - def _send_editor_updates(self): - """发送编辑器更新""" - try: - # 在实际实现中,这里会通过WebSocket或其他方式发送数据到编辑器前端 - # 此处简化处理,仅触发回调 - - self._trigger_editor_callback("data_updated", { - "data": self.editor_data, - "timestamp": time.time() - }) - - self.editor_stats["updates_sent"] += 1 - - except Exception as e: - print(f"✗ 编辑器更新发送失败: {e}") - self.editor_stats["errors"] += 1 - - def handle_ui_interaction(self, interaction_data: Dict[str, Any]): - """ - 处理UI交互 - - Args: - interaction_data: 交互数据 - """ - try: - self.editor_stats["ui_interactions"] += 1 - self.last_ui_interaction = time.time() - - # 触发UI交互回调 - self._trigger_editor_callback("ui_interaction", { - "interaction": interaction_data, - "timestamp": self.last_ui_interaction - }) - - # 处理特定交互类型 - interaction_type = interaction_data.get("type") - if interaction_type == "tab_change": - self.editor_state["selected_tab"] = interaction_data.get("tab_name", "overview") - elif interaction_type == "config_update": - self._handle_config_update(interaction_data.get("config_data", {})) - elif interaction_type == "client_action": - self._handle_client_action(interaction_data.get("action_data", {})) - elif interaction_type == "room_action": - self._handle_room_action(interaction_data.get("action_data", {})) - - except Exception as e: - print(f"✗ UI交互处理失败: {e}") - self.editor_stats["errors"] += 1 - - def _handle_config_update(self, config_data: Dict[str, Any]): - """ - 处理配置更新 - - Args: - config_data: 配置数据 - """ - try: - if not self.plugin.config_manager: - return - - # 应用配置更新 - for section, section_data in config_data.items(): - if isinstance(section_data, dict): - self.plugin.config_manager.update_config_section(section, section_data) - - print("✓ 配置已更新") - - except Exception as e: - print(f"✗ 配置更新处理失败: {e}") - self.editor_stats["errors"] += 1 - - def _handle_client_action(self, action_data: Dict[str, Any]): - """ - 处理客户端操作 - - Args: - action_data: 操作数据 - """ - try: - if not self.plugin.client_manager: - return - - action = action_data.get("action") - client_id = action_data.get("client_id") - - if action == "kick": - self.plugin.client_manager.disconnect_client(client_id, "kicked_by_admin") - print(f"✓ 客户端已踢出: {client_id}") - elif action == "ban": - client_data = self.plugin.client_manager.get_client(client_id) - if client_data: - client_address = client_data.get("address") - if client_address: - self.plugin.security_manager._block_ip(client_address[0], 3600) # 阻止1小时 - print(f"✓ 客户端IP已阻止: {client_address[0]}") - - except Exception as e: - print(f"✗ 客户端操作处理失败: {e}") - self.editor_stats["errors"] += 1 - - def _handle_room_action(self, action_data: Dict[str, Any]): - """ - 处理房间操作 - - Args: - action_data: 操作数据 - """ - try: - if not self.plugin.room_manager: - return - - action = action_data.get("action") - room_id = action_data.get("room_id") - - if action == "destroy": - self.plugin.room_manager.destroy_room(room_id) - print(f"✓ 房间已销毁: {room_id}") - - except Exception as e: - print(f"✗ 房间操作处理失败: {e}") - self.editor_stats["errors"] += 1 - - def export_data(self, export_type: str = "json") -> Optional[str]: - """ - 导出数据 - - Args: - export_type: 导出类型 (json, csv等) - - Returns: - 导出的数据字符串或None - """ - try: - self.editor_stats["export_operations"] += 1 - - if export_type == "json": - export_data = { - "export_time": time.time(), - "data": self.editor_data, - "stats": self.editor_stats - } - return json.dumps(export_data, ensure_ascii=False, indent=2) - else: - print(f"✗ 不支持的导出类型: {export_type}") - return None - - except Exception as e: - print(f"✗ 数据导出失败: {e}") - self.editor_stats["errors"] += 1 - return None - - def import_data(self, import_data: str, import_type: str = "json") -> bool: - """ - 导入数据 - - Args: - import_data: 导入的数据 - import_type: 导入类型 (json, csv等) - - Returns: - 是否导入成功 - """ - try: - self.editor_stats["import_operations"] += 1 - - if import_type == "json": - data = json.loads(import_data) - # 在实际实现中,这里会处理导入的数据 - print("✓ 数据导入完成") - return True - else: - print(f"✗ 不支持的导入类型: {import_type}") - return False - - except Exception as e: - print(f"✗ 数据导入失败: {e}") - self.editor_stats["errors"] += 1 - return False - - def get_editor_state(self) -> Dict[str, Any]: - """ - 获取编辑器状态 - - Returns: - 编辑器状态字典 - """ - return self.editor_state.copy() - - def get_editor_data(self) -> Dict[str, Any]: - """ - 获取编辑器数据 - - Returns: - 编辑器数据字典 - """ - return self.editor_data.copy() - - def get_editor_stats(self) -> Dict[str, Any]: - """ - 获取编辑器统计信息 - - Returns: - 编辑器统计字典 - """ - return { - "state": self.editor_state.copy(), - "stats": self.editor_stats.copy(), - "config": self.editor_config.copy() - } - - def reset_stats(self): - """重置编辑器统计信息""" - try: - self.editor_stats = { - "updates_sent": 0, - "data_points_collected": 0, - "ui_interactions": 0, - "export_operations": 0, - "import_operations": 0, - "errors": 0 - } - print("✓ 编辑器统计信息已重置") - except Exception as e: - print(f"✗ 编辑器统计信息重置失败: {e}") - - def set_editor_config(self, config: Dict[str, Any]) -> bool: - """ - 设置编辑器配置 - - Args: - config: 编辑器配置字典 - - Returns: - 是否设置成功 - """ - try: - self.editor_config.update(config) - print(f"✓ 编辑器配置已更新: {self.editor_config}") - return True - except Exception as e: - print(f"✗ 编辑器配置设置失败: {e}") - return False - - def get_editor_config(self) -> Dict[str, Any]: - """ - 获取编辑器配置 - - Returns: - 编辑器配置字典 - """ - return self.editor_config.copy() - - def _trigger_editor_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发编辑器回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.editor_callbacks: - for callback in self.editor_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 编辑器回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 编辑器回调触发失败: {e}") - - def register_editor_callback(self, callback_type: str, callback: callable): - """ - 注册编辑器回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.editor_callbacks: - self.editor_callbacks[callback_type].append(callback) - print(f"✓ 编辑器回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 编辑器回调注册失败: {e}") - - def unregister_editor_callback(self, callback_type: str, callback: callable): - """ - 注销编辑器回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.editor_callbacks: - if callback in self.editor_callbacks[callback_type]: - self.editor_callbacks[callback_type].remove(callback) - print(f"✓ 编辑器回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +编辑器接口模块 +提供可视化配置和监控界面 +""" + +import time +import threading +import json +from typing import Dict, Any, List, Optional + +class EditorInterface: + """ + 编辑器接口 + 提供可视化配置和监控界面 + """ + + def __init__(self, plugin): + """ + 初始化编辑器接口 + + Args: + plugin: 服务器架构插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 编辑器配置 + self.editor_config = { + "enable_editor": True, + "editor_port": 8082, + "enable_realtime_updates": True, + "update_interval": 1.0, + "enable_data_export": True, + "enable_data_import": True, + "max_history_points": 1000, + "enable_theme_customization": True, + "default_theme": "dark" + } + + # 编辑器状态 + self.editor_state = { + "is_running": False, + "last_update": 0.0, + "connected_clients": 0, + "total_updates": 0, + "ui_elements": {}, + "selected_tab": "overview" + } + + # 编辑器数据 + self.editor_data = { + "system_stats": {}, + "performance_metrics": {}, + "active_alerts": [], + "client_list": [], + "room_list": [], + "server_config": {} + } + + # 编辑器统计 + self.editor_stats = { + "updates_sent": 0, + "data_points_collected": 0, + "ui_interactions": 0, + "export_operations": 0, + "import_operations": 0, + "errors": 0 + } + + # 实时更新线程 + self.update_thread = None + self.update_thread_running = False + + # 回调函数 + self.editor_callbacks = { + "editor_started": [], + "editor_stopped": [], + "data_updated": [], + "ui_interaction": [], + "editor_error": [] + } + + # 时间戳记录 + self.last_data_update = 0.0 + self.last_ui_interaction = 0.0 + + print("✓ 编辑器接口已创建") + + def initialize(self) -> bool: + """ + 初始化编辑器接口 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化编辑器接口...") + + # 初始化UI元素 + self._initialize_ui_elements() + + self.initialized = True + print("✓ 编辑器接口初始化完成") + return True + + except Exception as e: + print(f"✗ 编辑器接口初始化失败: {e}") + self.editor_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def _initialize_ui_elements(self): + """初始化UI元素""" + try: + self.editor_state["ui_elements"] = { + "overview_tab": { + "name": "系统概览", + "icon": "dashboard", + "visible": True + }, + "clients_tab": { + "name": "客户端管理", + "icon": "people", + "visible": True + }, + "rooms_tab": { + "name": "房间管理", + "icon": "meeting_room", + "visible": True + }, + "monitoring_tab": { + "name": "监控面板", + "icon": "monitor", + "visible": True + }, + "config_tab": { + "name": "配置管理", + "icon": "settings", + "visible": True + }, + "logs_tab": { + "name": "日志查看", + "icon": "description", + "visible": True + }, + "alerts_tab": { + "name": "警报管理", + "icon": "warning", + "visible": True + } + } + + print("✓ UI元素已初始化") + + except Exception as e: + print(f"✗ UI元素初始化失败: {e}") + self.editor_stats["errors"] += 1 + + def enable(self) -> bool: + """ + 启用编辑器接口 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 编辑器接口未初始化") + return False + + # 启动实时更新线程 + self._start_update_thread() + + self.enabled = True + print("✓ 编辑器接口已启用") + return True + + except Exception as e: + print(f"✗ 编辑器接口启用失败: {e}") + self.editor_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用编辑器接口""" + try: + self.enabled = False + + # 停止实时更新线程 + self._stop_update_thread() + + print("✓ 编辑器接口已禁用") + + except Exception as e: + print(f"✗ 编辑器接口禁用失败: {e}") + self.editor_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理编辑器接口资源""" + try: + # 禁用编辑器接口 + if self.enabled: + self.disable() + + # 清理回调 + self.editor_callbacks.clear() + + self.initialized = False + print("✓ 编辑器接口资源已清理") + + except Exception as e: + print(f"✗ 编辑器接口资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新编辑器接口状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.editor_state["last_update"] = current_time + + except Exception as e: + print(f"✗ 编辑器接口更新失败: {e}") + self.editor_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def _start_update_thread(self): + """启动实时更新线程""" + try: + self.update_thread_running = True + self.update_thread = threading.Thread(target=self._update_loop, daemon=True) + self.update_thread.start() + print("✓ 实时更新线程已启动") + except Exception as e: + print(f"✗ 实时更新线程启动失败: {e}") + self.editor_stats["errors"] += 1 + + def _stop_update_thread(self): + """停止实时更新线程""" + try: + self.update_thread_running = False + if self.update_thread and self.update_thread.is_alive(): + self.update_thread.join(timeout=5.0) + self.update_thread = None + print("✓ 实时更新线程已停止") + except Exception as e: + print(f"✗ 实时更新线程停止失败: {e}") + self.editor_stats["errors"] += 1 + + def _update_loop(self): + """实时更新循环""" + try: + while self.update_thread_running: + try: + if self.enabled and self.editor_config["enable_realtime_updates"]: + # 收集数据 + self._collect_editor_data() + + # 发送更新 + self._send_editor_updates() + + self.editor_state["total_updates"] += 1 + + # 等待下一个更新间隔 + time.sleep(self.editor_config["update_interval"]) + + except Exception as e: + print(f"✗ 实时更新循环错误: {e}") + self.editor_stats["errors"] += 1 + time.sleep(1.0) # 出错时短暂休眠 + + except Exception as e: + print(f"✗ 实时更新线程失败: {e}") + self.editor_stats["errors"] += 1 + + def _collect_editor_data(self): + """收集编辑器数据""" + try: + current_time = time.time() + + # 收集系统统计数据 + self.editor_data["system_stats"] = { + "server": self.plugin.server_manager.get_server_stats() if self.plugin.server_manager else {}, + "clients": self.plugin.client_manager.get_client_stats() if self.plugin.client_manager else {}, + "rooms": self.plugin.room_manager.get_room_stats() if self.plugin.room_manager else {}, + "messages": self.plugin.message_router.get_message_stats() if self.plugin.message_router else {}, + "storage": self.plugin.storage_manager.get_storage_stats() if self.plugin.storage_manager else {}, + "loadbalancer": self.plugin.load_balancer.get_loadbalancer_stats() if self.plugin.load_balancer else {}, + "monitoring": self.plugin.monitoring_system.get_monitoring_stats() if self.plugin.monitoring_system else {}, + "security": self.plugin.security_manager.get_security_stats() if self.plugin.security_manager else {}, + "api": self.plugin.api_interface.get_api_stats() if self.plugin.api_interface else {} + } + + # 收集性能指标 + if self.plugin.monitoring_system: + self.editor_data["performance_metrics"] = self.plugin.monitoring_system.get_performance_metrics() + + # 收集活动警报 + if self.plugin.monitoring_system: + self.editor_data["active_alerts"] = list(self.plugin.monitoring_system.get_active_alerts().values()) + + # 收集客户端列表 + if self.plugin.client_manager: + self.editor_data["client_list"] = list(self.plugin.client_manager.get_all_clients().values()) + + # 收集房间列表 + if self.plugin.room_manager: + self.editor_data["room_list"] = self.plugin.room_manager.get_all_rooms() + + # 收集服务器配置 + if self.plugin.config_manager: + self.editor_data["server_config"] = self.plugin.config_manager.get_config() + + self.editor_stats["data_points_collected"] += 1 + self.last_data_update = current_time + + except Exception as e: + print(f"✗ 编辑器数据收集失败: {e}") + self.editor_stats["errors"] += 1 + + def _send_editor_updates(self): + """发送编辑器更新""" + try: + # 在实际实现中,这里会通过WebSocket或其他方式发送数据到编辑器前端 + # 此处简化处理,仅触发回调 + + self._trigger_editor_callback("data_updated", { + "data": self.editor_data, + "timestamp": time.time() + }) + + self.editor_stats["updates_sent"] += 1 + + except Exception as e: + print(f"✗ 编辑器更新发送失败: {e}") + self.editor_stats["errors"] += 1 + + def handle_ui_interaction(self, interaction_data: Dict[str, Any]): + """ + 处理UI交互 + + Args: + interaction_data: 交互数据 + """ + try: + self.editor_stats["ui_interactions"] += 1 + self.last_ui_interaction = time.time() + + # 触发UI交互回调 + self._trigger_editor_callback("ui_interaction", { + "interaction": interaction_data, + "timestamp": self.last_ui_interaction + }) + + # 处理特定交互类型 + interaction_type = interaction_data.get("type") + if interaction_type == "tab_change": + self.editor_state["selected_tab"] = interaction_data.get("tab_name", "overview") + elif interaction_type == "config_update": + self._handle_config_update(interaction_data.get("config_data", {})) + elif interaction_type == "client_action": + self._handle_client_action(interaction_data.get("action_data", {})) + elif interaction_type == "room_action": + self._handle_room_action(interaction_data.get("action_data", {})) + + except Exception as e: + print(f"✗ UI交互处理失败: {e}") + self.editor_stats["errors"] += 1 + + def _handle_config_update(self, config_data: Dict[str, Any]): + """ + 处理配置更新 + + Args: + config_data: 配置数据 + """ + try: + if not self.plugin.config_manager: + return + + # 应用配置更新 + for section, section_data in config_data.items(): + if isinstance(section_data, dict): + self.plugin.config_manager.update_config_section(section, section_data) + + print("✓ 配置已更新") + + except Exception as e: + print(f"✗ 配置更新处理失败: {e}") + self.editor_stats["errors"] += 1 + + def _handle_client_action(self, action_data: Dict[str, Any]): + """ + 处理客户端操作 + + Args: + action_data: 操作数据 + """ + try: + if not self.plugin.client_manager: + return + + action = action_data.get("action") + client_id = action_data.get("client_id") + + if action == "kick": + self.plugin.client_manager.disconnect_client(client_id, "kicked_by_admin") + print(f"✓ 客户端已踢出: {client_id}") + elif action == "ban": + client_data = self.plugin.client_manager.get_client(client_id) + if client_data: + client_address = client_data.get("address") + if client_address: + self.plugin.security_manager._block_ip(client_address[0], 3600) # 阻止1小时 + print(f"✓ 客户端IP已阻止: {client_address[0]}") + + except Exception as e: + print(f"✗ 客户端操作处理失败: {e}") + self.editor_stats["errors"] += 1 + + def _handle_room_action(self, action_data: Dict[str, Any]): + """ + 处理房间操作 + + Args: + action_data: 操作数据 + """ + try: + if not self.plugin.room_manager: + return + + action = action_data.get("action") + room_id = action_data.get("room_id") + + if action == "destroy": + self.plugin.room_manager.destroy_room(room_id) + print(f"✓ 房间已销毁: {room_id}") + + except Exception as e: + print(f"✗ 房间操作处理失败: {e}") + self.editor_stats["errors"] += 1 + + def export_data(self, export_type: str = "json") -> Optional[str]: + """ + 导出数据 + + Args: + export_type: 导出类型 (json, csv等) + + Returns: + 导出的数据字符串或None + """ + try: + self.editor_stats["export_operations"] += 1 + + if export_type == "json": + export_data = { + "export_time": time.time(), + "data": self.editor_data, + "stats": self.editor_stats + } + return json.dumps(export_data, ensure_ascii=False, indent=2) + else: + print(f"✗ 不支持的导出类型: {export_type}") + return None + + except Exception as e: + print(f"✗ 数据导出失败: {e}") + self.editor_stats["errors"] += 1 + return None + + def import_data(self, import_data: str, import_type: str = "json") -> bool: + """ + 导入数据 + + Args: + import_data: 导入的数据 + import_type: 导入类型 (json, csv等) + + Returns: + 是否导入成功 + """ + try: + self.editor_stats["import_operations"] += 1 + + if import_type == "json": + data = json.loads(import_data) + # 在实际实现中,这里会处理导入的数据 + print("✓ 数据导入完成") + return True + else: + print(f"✗ 不支持的导入类型: {import_type}") + return False + + except Exception as e: + print(f"✗ 数据导入失败: {e}") + self.editor_stats["errors"] += 1 + return False + + def get_editor_state(self) -> Dict[str, Any]: + """ + 获取编辑器状态 + + Returns: + 编辑器状态字典 + """ + return self.editor_state.copy() + + def get_editor_data(self) -> Dict[str, Any]: + """ + 获取编辑器数据 + + Returns: + 编辑器数据字典 + """ + return self.editor_data.copy() + + def get_editor_stats(self) -> Dict[str, Any]: + """ + 获取编辑器统计信息 + + Returns: + 编辑器统计字典 + """ + return { + "state": self.editor_state.copy(), + "stats": self.editor_stats.copy(), + "config": self.editor_config.copy() + } + + def reset_stats(self): + """重置编辑器统计信息""" + try: + self.editor_stats = { + "updates_sent": 0, + "data_points_collected": 0, + "ui_interactions": 0, + "export_operations": 0, + "import_operations": 0, + "errors": 0 + } + print("✓ 编辑器统计信息已重置") + except Exception as e: + print(f"✗ 编辑器统计信息重置失败: {e}") + + def set_editor_config(self, config: Dict[str, Any]) -> bool: + """ + 设置编辑器配置 + + Args: + config: 编辑器配置字典 + + Returns: + 是否设置成功 + """ + try: + self.editor_config.update(config) + print(f"✓ 编辑器配置已更新: {self.editor_config}") + return True + except Exception as e: + print(f"✗ 编辑器配置设置失败: {e}") + return False + + def get_editor_config(self) -> Dict[str, Any]: + """ + 获取编辑器配置 + + Returns: + 编辑器配置字典 + """ + return self.editor_config.copy() + + def _trigger_editor_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发编辑器回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.editor_callbacks: + for callback in self.editor_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 编辑器回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 编辑器回调触发失败: {e}") + + def register_editor_callback(self, callback_type: str, callback: callable): + """ + 注册编辑器回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.editor_callbacks: + self.editor_callbacks[callback_type].append(callback) + print(f"✓ 编辑器回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 编辑器回调注册失败: {e}") + + def unregister_editor_callback(self, callback_type: str, callback: callable): + """ + 注销编辑器回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.editor_callbacks: + if callback in self.editor_callbacks[callback_type]: + self.editor_callbacks[callback_type].remove(callback) + print(f"✓ 编辑器回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 编辑器回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/server_architecture/loadbalancer/load_balancer.py b/plugins/user/server_architecture/loadbalancer/load_balancer.py index c2189bbf..5e86874f 100644 --- a/plugins/user/server_architecture/loadbalancer/load_balancer.py +++ b/plugins/user/server_architecture/loadbalancer/load_balancer.py @@ -1,793 +1,793 @@ -""" -负载均衡器模块 -负责在多个服务器实例间分配负载 -""" - -import time -import threading -import random -import hashlib -from typing import Dict, Any, List, Optional - -class LoadBalancer: - """ - 负载均衡器 - 负责在多个服务器实例间分配负载 - """ - - def __init__(self, plugin): - """ - 初始化负载均衡器 - - Args: - plugin: 服务器架构插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 负载均衡配置 - self.loadbalancer_config = { - "enable_load_balancing": True, - "balancing_algorithm": "round_robin", # round_robin, least_connections, weighted_round_robin, ip_hash - "health_check_interval": 30.0, - "enable_health_check": True, - "max_retry_attempts": 3, - "fallback_strategy": "random", # random, next_available - "enable_session_stickiness": False, - "session_timeout": 3600, # 1小时 - "enable_auto_scaling": False, - "min_servers": 1, - "max_servers": 10 - } - - # 服务器实例列表 - self.server_instances = {} - - # 负载均衡状态 - self.loadbalancer_state = { - "current_instance_index": 0, - "session_map": {}, # 客户端IP到服务器实例的映射 - "last_health_check": 0.0, - "total_requests": 0, - "successful_requests": 0, - "failed_requests": 0 - } - - # 负载均衡统计 - self.loadbalancer_stats = { - "instances_added": 0, - "instances_removed": 0, - "health_checks": 0, - "failed_health_checks": 0, - "requests_routed": 0, - "errors": 0 - } - - # 线程锁 - self.lb_lock = threading.RLock() - - # 回调函数 - self.loadbalancer_callbacks = { - "instance_added": [], - "instance_removed": [], - "health_check_passed": [], - "health_check_failed": [], - "request_routed": [], - "load_balancer_error": [] - } - - # 时间戳记录 - self.last_health_check = 0.0 - self.last_request = 0.0 - - print("✓ 负载均衡器已创建") - - def initialize(self) -> bool: - """ - 初始化负载均衡器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化负载均衡器...") - - # 添加本地服务器实例 - self.add_server_instance("localhost", { - "host": "127.0.0.1", - "port": self.plugin.server_manager.server_config["port"] if self.plugin.server_manager else 8080, - "weight": 1, - "max_connections": 1000, - "current_connections": 0, - "status": "active", - "last_health_check": time.time(), - "health_status": "healthy" - }) - - self.initialized = True - print("✓ 负载均衡器初始化完成") - return True - - except Exception as e: - print(f"✗ 负载均衡器初始化失败: {e}") - self.loadbalancer_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用负载均衡器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 负载均衡器未初始化") - return False - - self.enabled = True - print("✓ 负载均衡器已启用") - return True - - except Exception as e: - print(f"✗ 负载均衡器启用失败: {e}") - self.loadbalancer_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用负载均衡器""" - try: - self.enabled = False - - # 清理会话映射 - with self.lb_lock: - self.loadbalancer_state["session_map"].clear() - - print("✓ 负载均衡器已禁用") - - except Exception as e: - print(f"✗ 负载均衡器禁用失败: {e}") - self.loadbalancer_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理负载均衡器资源""" - try: - # 禁用负载均衡器 - if self.enabled: - self.disable() - - # 清理回调 - self.loadbalancer_callbacks.clear() - - self.initialized = False - print("✓ 负载均衡器资源已清理") - - except Exception as e: - print(f"✗ 负载均衡器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新负载均衡器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 定期执行健康检查 - if (self.loadbalancer_config["enable_health_check"] and - current_time - self.last_health_check > self.loadbalancer_config["health_check_interval"]): - self._perform_health_check() - self.last_health_check = current_time - - # 清理过期会话 - if self.loadbalancer_config["enable_session_stickiness"]: - self._cleanup_expired_sessions(current_time) - - except Exception as e: - print(f"✗ 负载均衡器更新失败: {e}") - self.loadbalancer_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def _perform_health_check(self): - """执行健康检查""" - try: - with self.lb_lock: - for instance_id, instance_data in self.server_instances.items(): - try: - # 执行健康检查(简化实现) - is_healthy = self._check_instance_health(instance_data) - - # 更新实例健康状态 - instance_data["last_health_check"] = time.time() - instance_data["health_status"] = "healthy" if is_healthy else "unhealthy" - - self.loadbalancer_stats["health_checks"] += 1 - - # 触发健康检查回调 - if is_healthy: - self._trigger_loadbalancer_callback("health_check_passed", { - "instance_id": instance_id, - "timestamp": time.time() - }) - else: - self.loadbalancer_stats["failed_health_checks"] += 1 - self._trigger_loadbalancer_callback("health_check_failed", { - "instance_id": instance_id, - "timestamp": time.time() - }) - - except Exception as e: - print(f"✗ 实例健康检查失败: {instance_id} - {e}") - self.loadbalancer_stats["errors"] += 1 - - except Exception as e: - print(f"✗ 健康检查执行失败: {e}") - self.loadbalancer_stats["errors"] += 1 - - def _check_instance_health(self, instance_data: Dict[str, Any]) -> bool: - """ - 检查实例健康状态 - - Args: - instance_data: 实例数据 - - Returns: - 实例是否健康 - """ - try: - # 简化实现:检查实例状态和最后检查时间 - if instance_data["status"] != "active": - return False - - # 检查连接数是否超过限制 - if instance_data["current_connections"] > instance_data["max_connections"]: - return False - - # 检查最后健康检查时间 - if time.time() - instance_data["last_health_check"] > self.loadbalancer_config["health_check_interval"] * 2: - return False - - return True - - except Exception as e: - print(f"✗ 实例健康状态检查失败: {e}") - self.loadbalancer_stats["errors"] += 1 - return False - - def _cleanup_expired_sessions(self, current_time: float): - """清理过期会话""" - try: - expired_sessions = [] - with self.lb_lock: - for client_ip, session_data in self.loadbalancer_state["session_map"].items(): - if current_time - session_data["timestamp"] > self.loadbalancer_config["session_timeout"]: - expired_sessions.append(client_ip) - - for client_ip in expired_sessions: - with self.lb_lock: - del self.loadbalancer_state["session_map"][client_ip] - - except Exception as e: - print(f"✗ 过期会话清理失败: {e}") - self.loadbalancer_stats["errors"] += 1 - - def add_server_instance(self, instance_id: str, instance_data: Dict[str, Any]) -> bool: - """ - 添加服务器实例 - - Args: - instance_id: 实例ID - instance_data: 实例数据 - - Returns: - 是否添加成功 - """ - try: - if not self.enabled: - return False - - with self.lb_lock: - self.server_instances[instance_id] = instance_data - - self.loadbalancer_stats["instances_added"] += 1 - - # 触发实例添加回调 - self._trigger_loadbalancer_callback("instance_added", { - "instance_id": instance_id, - "instance_data": instance_data, - "timestamp": time.time() - }) - - print(f"✓ 服务器实例已添加: {instance_id}") - return True - - except Exception as e: - print(f"✗ 添加服务器实例失败: {e}") - self.loadbalancer_stats["errors"] += 1 - return False - - def remove_server_instance(self, instance_id: str) -> bool: - """ - 移除服务器实例 - - Args: - instance_id: 实例ID - - Returns: - 是否移除成功 - """ - try: - if not self.enabled: - return False - - with self.lb_lock: - if instance_id in self.server_instances: - del self.server_instances[instance_id] - - # 移除相关的会话映射 - clients_to_remove = [] - for client_ip, session_data in self.loadbalancer_state["session_map"].items(): - if session_data["instance_id"] == instance_id: - clients_to_remove.append(client_ip) - - for client_ip in clients_to_remove: - del self.loadbalancer_state["session_map"][client_ip] - - self.loadbalancer_stats["instances_removed"] += 1 - - # 触发实例移除回调 - self._trigger_loadbalancer_callback("instance_removed", { - "instance_id": instance_id, - "timestamp": time.time() - }) - - print(f"✓ 服务器实例已移除: {instance_id}") - return True - - except Exception as e: - print(f"✗ 移除服务器实例失败: {e}") - self.loadbalancer_stats["errors"] += 1 - return False - - def get_server_instance(self, client_ip: str = None) -> Optional[Dict[str, Any]]: - """ - 获取服务器实例(根据负载均衡算法) - - Args: - client_ip: 客户端IP(用于会话粘性) - - Returns: - 服务器实例数据或None - """ - try: - if not self.enabled or not self.loadbalancer_config["enable_load_balancing"]: - # 如果未启用负载均衡,返回本地实例 - with self.lb_lock: - if "localhost" in self.server_instances: - return self.server_instances["localhost"].copy() - elif self.server_instances: - # 返回第一个实例 - first_instance = next(iter(self.server_instances.values())) - return first_instance.copy() - else: - return None - - # 根据算法选择实例 - algorithm = self.loadbalancer_config["balancing_algorithm"] - - with self.lb_lock: - active_instances = { - k: v for k, v in self.server_instances.items() - if v["status"] == "active" and v["health_status"] == "healthy" - } - - if not active_instances: - return None - - if algorithm == "round_robin": - return self._round_robin_selection(active_instances) - elif algorithm == "least_connections": - return self._least_connections_selection(active_instances) - elif algorithm == "weighted_round_robin": - return self._weighted_round_robin_selection(active_instances) - elif algorithm == "ip_hash" and client_ip: - return self._ip_hash_selection(active_instances, client_ip) - else: - # 默认使用轮询 - return self._round_robin_selection(active_instances) - - except Exception as e: - print(f"✗ 获取服务器实例失败: {e}") - self.loadbalancer_stats["errors"] += 1 - return None - - def _round_robin_selection(self, active_instances: Dict[str, Dict[str, Any]]) -> Optional[Dict[str, Any]]: - """ - 轮询选择算法 - - Args: - active_instances: 活跃实例字典 - - Returns: - 选中的实例数据 - """ - try: - instance_ids = list(active_instances.keys()) - if not instance_ids: - return None - - # 选择下一个实例 - selected_index = self.loadbalancer_state["current_instance_index"] % len(instance_ids) - selected_instance_id = instance_ids[selected_index] - - # 更新索引 - self.loadbalancer_state["current_instance_index"] = (selected_index + 1) % len(instance_ids) - - return active_instances[selected_instance_id].copy() - - except Exception as e: - print(f"✗ 轮询选择失败: {e}") - self.loadbalancer_stats["errors"] += 1 - return None - - def _least_connections_selection(self, active_instances: Dict[str, Dict[str, Any]]) -> Optional[Dict[str, Any]]: - """ - 最少连接选择算法 - - Args: - active_instances: 活跃实例字典 - - Returns: - 选中的实例数据 - """ - try: - if not active_instances: - return None - - # 选择连接数最少的实例 - selected_instance_id = min( - active_instances.keys(), - key=lambda k: active_instances[k]["current_connections"] - ) - - return active_instances[selected_instance_id].copy() - - except Exception as e: - print(f"✗ 最少连接选择失败: {e}") - self.loadbalancer_stats["errors"] += 1 - return None - - def _weighted_round_robin_selection(self, active_instances: Dict[str, Dict[str, Any]]) -> Optional[Dict[str, Any]]: - """ - 加权轮询选择算法 - - Args: - active_instances: 活跃实例字典 - - Returns: - 选中的实例数据 - """ - try: - if not active_instances: - return None - - # 创建加权实例列表 - weighted_instances = [] - for instance_id, instance_data in active_instances.items(): - weight = instance_data.get("weight", 1) - for _ in range(weight): - weighted_instances.append(instance_id) - - if not weighted_instances: - return None - - # 选择实例 - selected_index = self.loadbalancer_state["current_instance_index"] % len(weighted_instances) - selected_instance_id = weighted_instances[selected_index] - - # 更新索引 - self.loadbalancer_state["current_instance_index"] = (selected_index + 1) % len(weighted_instances) - - return active_instances[selected_instance_id].copy() - - except Exception as e: - print(f"✗ 加权轮询选择失败: {e}") - self.loadbalancer_stats["errors"] += 1 - return None - - def _ip_hash_selection(self, active_instances: Dict[str, Dict[str, Any]], client_ip: str) -> Optional[Dict[str, Any]]: - """ - IP哈希选择算法 - - Args: - active_instances: 活跃实例字典 - client_ip: 客户端IP - - Returns: - 选中的实例数据 - """ - try: - if not active_instances or not client_ip: - return None - - # 检查会话粘性 - if self.loadbalancer_config["enable_session_stickiness"]: - with self.lb_lock: - if client_ip in self.loadbalancer_state["session_map"]: - session_data = self.loadbalancer_state["session_map"][client_ip] - if time.time() - session_data["timestamp"] <= self.loadbalancer_config["session_timeout"]: - instance_id = session_data["instance_id"] - if instance_id in active_instances: - return active_instances[instance_id].copy() - - # 使用IP哈希选择实例 - instance_ids = list(active_instances.keys()) - hash_value = int(hashlib.md5(client_ip.encode()).hexdigest(), 16) - selected_index = hash_value % len(instance_ids) - selected_instance_id = instance_ids[selected_index] - - # 更新会话映射 - if self.loadbalancer_config["enable_session_stickiness"]: - with self.lb_lock: - self.loadbalancer_state["session_map"][client_ip] = { - "instance_id": selected_instance_id, - "timestamp": time.time() - } - - return active_instances[selected_instance_id].copy() - - except Exception as e: - print(f"✗ IP哈希选择失败: {e}") - self.loadbalancer_stats["errors"] += 1 - return None - - def route_request(self, client_ip: str = None) -> Optional[Dict[str, Any]]: - """ - 路由请求到合适的服务器实例 - - Args: - client_ip: 客户端IP - - Returns: - 服务器实例数据或None - """ - try: - if not self.enabled: - return None - - # 获取服务器实例 - instance_data = self.get_server_instance(client_ip) - if not instance_data: - return None - - self.loadbalancer_stats["requests_routed"] += 1 - self.loadbalancer_state["total_requests"] += 1 - - # 增加实例连接数 - with self.lb_lock: - instance_id = instance_data.get("instance_id") - if instance_id and instance_id in self.server_instances: - self.server_instances[instance_id]["current_connections"] += 1 - - # 触发请求路由回调 - self._trigger_loadbalancer_callback("request_routed", { - "instance_id": instance_data.get("instance_id"), - "client_ip": client_ip, - "timestamp": time.time() - }) - - self.last_request = time.time() - return instance_data - - except Exception as e: - print(f"✗ 请求路由失败: {e}") - self.loadbalancer_stats["errors"] += 1 - self.loadbalancer_state["failed_requests"] += 1 - return None - - def release_instance_connection(self, instance_id: str): - """ - 释放实例连接 - - Args: - instance_id: 实例ID - """ - try: - with self.lb_lock: - if instance_id in self.server_instances: - if self.server_instances[instance_id]["current_connections"] > 0: - self.server_instances[instance_id]["current_connections"] -= 1 - - except Exception as e: - print(f"✗ 释放实例连接失败: {e}") - self.loadbalancer_stats["errors"] += 1 - - def get_instance_stats(self, instance_id: str) -> Optional[Dict[str, Any]]: - """ - 获取实例统计信息 - - Args: - instance_id: 实例ID - - Returns: - 实例统计信息或None - """ - try: - with self.lb_lock: - if instance_id in self.server_instances: - return self.server_instances[instance_id].copy() - else: - return None - - except Exception as e: - print(f"✗ 获取实例统计信息失败: {e}") - self.loadbalancer_stats["errors"] += 1 - return None - - def get_all_instances(self) -> Dict[str, Dict[str, Any]]: - """ - 获取所有实例信息 - - Returns: - 所有实例信息字典 - """ - try: - with self.lb_lock: - return {k: v.copy() for k, v in self.server_instances.items()} - except Exception as e: - print(f"✗ 获取所有实例信息失败: {e}") - self.loadbalancer_stats["errors"] += 1 - return {} - - def update_instance_status(self, instance_id: str, status: str) -> bool: - """ - 更新实例状态 - - Args: - instance_id: 实例ID - status: 状态(active, inactive, maintenance) - - Returns: - 是否更新成功 - """ - try: - with self.lb_lock: - if instance_id in self.server_instances: - self.server_instances[instance_id]["status"] = status - return True - else: - return False - - except Exception as e: - print(f"✗ 更新实例状态失败: {e}") - self.loadbalancer_stats["errors"] += 1 - return False - - def get_loadbalancer_stats(self) -> Dict[str, Any]: - """ - 获取负载均衡器统计信息 - - Returns: - 负载均衡器统计字典 - """ - return { - "state": self.loadbalancer_state.copy(), - "stats": self.loadbalancer_stats.copy(), - "config": self.loadbalancer_config.copy(), - "instances_count": len(self.server_instances) - } - - def reset_stats(self): - """重置负载均衡器统计信息""" - try: - self.loadbalancer_stats = { - "instances_added": 0, - "instances_removed": 0, - "health_checks": 0, - "failed_health_checks": 0, - "requests_routed": 0, - "errors": 0 - } - - self.loadbalancer_state["total_requests"] = 0 - self.loadbalancer_state["successful_requests"] = 0 - self.loadbalancer_state["failed_requests"] = 0 - - print("✓ 负载均衡器统计信息已重置") - except Exception as e: - print(f"✗ 负载均衡器统计信息重置失败: {e}") - - def set_loadbalancer_config(self, config: Dict[str, Any]) -> bool: - """ - 设置负载均衡器配置 - - Args: - config: 负载均衡器配置字典 - - Returns: - 是否设置成功 - """ - try: - self.loadbalancer_config.update(config) - print(f"✓ 负载均衡器配置已更新: {self.loadbalancer_config}") - return True - except Exception as e: - print(f"✗ 负载均衡器配置设置失败: {e}") - return False - - def get_loadbalancer_config(self) -> Dict[str, Any]: - """ - 获取负载均衡器配置 - - Returns: - 负载均衡器配置字典 - """ - return self.loadbalancer_config.copy() - - def _trigger_loadbalancer_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发负载均衡器回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.loadbalancer_callbacks: - for callback in self.loadbalancer_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 负载均衡器回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 负载均衡器回调触发失败: {e}") - - def register_loadbalancer_callback(self, callback_type: str, callback: callable): - """ - 注册负载均衡器回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.loadbalancer_callbacks: - self.loadbalancer_callbacks[callback_type].append(callback) - print(f"✓ 负载均衡器回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 负载均衡器回调注册失败: {e}") - - def unregister_loadbalancer_callback(self, callback_type: str, callback: callable): - """ - 注销负载均衡器回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.loadbalancer_callbacks: - if callback in self.loadbalancer_callbacks[callback_type]: - self.loadbalancer_callbacks[callback_type].remove(callback) - print(f"✓ 负载均衡器回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +负载均衡器模块 +负责在多个服务器实例间分配负载 +""" + +import time +import threading +import random +import hashlib +from typing import Dict, Any, List, Optional + +class LoadBalancer: + """ + 负载均衡器 + 负责在多个服务器实例间分配负载 + """ + + def __init__(self, plugin): + """ + 初始化负载均衡器 + + Args: + plugin: 服务器架构插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 负载均衡配置 + self.loadbalancer_config = { + "enable_load_balancing": True, + "balancing_algorithm": "round_robin", # round_robin, least_connections, weighted_round_robin, ip_hash + "health_check_interval": 30.0, + "enable_health_check": True, + "max_retry_attempts": 3, + "fallback_strategy": "random", # random, next_available + "enable_session_stickiness": False, + "session_timeout": 3600, # 1小时 + "enable_auto_scaling": False, + "min_servers": 1, + "max_servers": 10 + } + + # 服务器实例列表 + self.server_instances = {} + + # 负载均衡状态 + self.loadbalancer_state = { + "current_instance_index": 0, + "session_map": {}, # 客户端IP到服务器实例的映射 + "last_health_check": 0.0, + "total_requests": 0, + "successful_requests": 0, + "failed_requests": 0 + } + + # 负载均衡统计 + self.loadbalancer_stats = { + "instances_added": 0, + "instances_removed": 0, + "health_checks": 0, + "failed_health_checks": 0, + "requests_routed": 0, + "errors": 0 + } + + # 线程锁 + self.lb_lock = threading.RLock() + + # 回调函数 + self.loadbalancer_callbacks = { + "instance_added": [], + "instance_removed": [], + "health_check_passed": [], + "health_check_failed": [], + "request_routed": [], + "load_balancer_error": [] + } + + # 时间戳记录 + self.last_health_check = 0.0 + self.last_request = 0.0 + + print("✓ 负载均衡器已创建") + + def initialize(self) -> bool: + """ + 初始化负载均衡器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化负载均衡器...") + + # 添加本地服务器实例 + self.add_server_instance("localhost", { + "host": "127.0.0.1", + "port": self.plugin.server_manager.server_config["port"] if self.plugin.server_manager else 8080, + "weight": 1, + "max_connections": 1000, + "current_connections": 0, + "status": "active", + "last_health_check": time.time(), + "health_status": "healthy" + }) + + self.initialized = True + print("✓ 负载均衡器初始化完成") + return True + + except Exception as e: + print(f"✗ 负载均衡器初始化失败: {e}") + self.loadbalancer_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用负载均衡器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 负载均衡器未初始化") + return False + + self.enabled = True + print("✓ 负载均衡器已启用") + return True + + except Exception as e: + print(f"✗ 负载均衡器启用失败: {e}") + self.loadbalancer_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用负载均衡器""" + try: + self.enabled = False + + # 清理会话映射 + with self.lb_lock: + self.loadbalancer_state["session_map"].clear() + + print("✓ 负载均衡器已禁用") + + except Exception as e: + print(f"✗ 负载均衡器禁用失败: {e}") + self.loadbalancer_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理负载均衡器资源""" + try: + # 禁用负载均衡器 + if self.enabled: + self.disable() + + # 清理回调 + self.loadbalancer_callbacks.clear() + + self.initialized = False + print("✓ 负载均衡器资源已清理") + + except Exception as e: + print(f"✗ 负载均衡器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新负载均衡器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 定期执行健康检查 + if (self.loadbalancer_config["enable_health_check"] and + current_time - self.last_health_check > self.loadbalancer_config["health_check_interval"]): + self._perform_health_check() + self.last_health_check = current_time + + # 清理过期会话 + if self.loadbalancer_config["enable_session_stickiness"]: + self._cleanup_expired_sessions(current_time) + + except Exception as e: + print(f"✗ 负载均衡器更新失败: {e}") + self.loadbalancer_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def _perform_health_check(self): + """执行健康检查""" + try: + with self.lb_lock: + for instance_id, instance_data in self.server_instances.items(): + try: + # 执行健康检查(简化实现) + is_healthy = self._check_instance_health(instance_data) + + # 更新实例健康状态 + instance_data["last_health_check"] = time.time() + instance_data["health_status"] = "healthy" if is_healthy else "unhealthy" + + self.loadbalancer_stats["health_checks"] += 1 + + # 触发健康检查回调 + if is_healthy: + self._trigger_loadbalancer_callback("health_check_passed", { + "instance_id": instance_id, + "timestamp": time.time() + }) + else: + self.loadbalancer_stats["failed_health_checks"] += 1 + self._trigger_loadbalancer_callback("health_check_failed", { + "instance_id": instance_id, + "timestamp": time.time() + }) + + except Exception as e: + print(f"✗ 实例健康检查失败: {instance_id} - {e}") + self.loadbalancer_stats["errors"] += 1 + + except Exception as e: + print(f"✗ 健康检查执行失败: {e}") + self.loadbalancer_stats["errors"] += 1 + + def _check_instance_health(self, instance_data: Dict[str, Any]) -> bool: + """ + 检查实例健康状态 + + Args: + instance_data: 实例数据 + + Returns: + 实例是否健康 + """ + try: + # 简化实现:检查实例状态和最后检查时间 + if instance_data["status"] != "active": + return False + + # 检查连接数是否超过限制 + if instance_data["current_connections"] > instance_data["max_connections"]: + return False + + # 检查最后健康检查时间 + if time.time() - instance_data["last_health_check"] > self.loadbalancer_config["health_check_interval"] * 2: + return False + + return True + + except Exception as e: + print(f"✗ 实例健康状态检查失败: {e}") + self.loadbalancer_stats["errors"] += 1 + return False + + def _cleanup_expired_sessions(self, current_time: float): + """清理过期会话""" + try: + expired_sessions = [] + with self.lb_lock: + for client_ip, session_data in self.loadbalancer_state["session_map"].items(): + if current_time - session_data["timestamp"] > self.loadbalancer_config["session_timeout"]: + expired_sessions.append(client_ip) + + for client_ip in expired_sessions: + with self.lb_lock: + del self.loadbalancer_state["session_map"][client_ip] + + except Exception as e: + print(f"✗ 过期会话清理失败: {e}") + self.loadbalancer_stats["errors"] += 1 + + def add_server_instance(self, instance_id: str, instance_data: Dict[str, Any]) -> bool: + """ + 添加服务器实例 + + Args: + instance_id: 实例ID + instance_data: 实例数据 + + Returns: + 是否添加成功 + """ + try: + if not self.enabled: + return False + + with self.lb_lock: + self.server_instances[instance_id] = instance_data + + self.loadbalancer_stats["instances_added"] += 1 + + # 触发实例添加回调 + self._trigger_loadbalancer_callback("instance_added", { + "instance_id": instance_id, + "instance_data": instance_data, + "timestamp": time.time() + }) + + print(f"✓ 服务器实例已添加: {instance_id}") + return True + + except Exception as e: + print(f"✗ 添加服务器实例失败: {e}") + self.loadbalancer_stats["errors"] += 1 + return False + + def remove_server_instance(self, instance_id: str) -> bool: + """ + 移除服务器实例 + + Args: + instance_id: 实例ID + + Returns: + 是否移除成功 + """ + try: + if not self.enabled: + return False + + with self.lb_lock: + if instance_id in self.server_instances: + del self.server_instances[instance_id] + + # 移除相关的会话映射 + clients_to_remove = [] + for client_ip, session_data in self.loadbalancer_state["session_map"].items(): + if session_data["instance_id"] == instance_id: + clients_to_remove.append(client_ip) + + for client_ip in clients_to_remove: + del self.loadbalancer_state["session_map"][client_ip] + + self.loadbalancer_stats["instances_removed"] += 1 + + # 触发实例移除回调 + self._trigger_loadbalancer_callback("instance_removed", { + "instance_id": instance_id, + "timestamp": time.time() + }) + + print(f"✓ 服务器实例已移除: {instance_id}") + return True + + except Exception as e: + print(f"✗ 移除服务器实例失败: {e}") + self.loadbalancer_stats["errors"] += 1 + return False + + def get_server_instance(self, client_ip: str = None) -> Optional[Dict[str, Any]]: + """ + 获取服务器实例(根据负载均衡算法) + + Args: + client_ip: 客户端IP(用于会话粘性) + + Returns: + 服务器实例数据或None + """ + try: + if not self.enabled or not self.loadbalancer_config["enable_load_balancing"]: + # 如果未启用负载均衡,返回本地实例 + with self.lb_lock: + if "localhost" in self.server_instances: + return self.server_instances["localhost"].copy() + elif self.server_instances: + # 返回第一个实例 + first_instance = next(iter(self.server_instances.values())) + return first_instance.copy() + else: + return None + + # 根据算法选择实例 + algorithm = self.loadbalancer_config["balancing_algorithm"] + + with self.lb_lock: + active_instances = { + k: v for k, v in self.server_instances.items() + if v["status"] == "active" and v["health_status"] == "healthy" + } + + if not active_instances: + return None + + if algorithm == "round_robin": + return self._round_robin_selection(active_instances) + elif algorithm == "least_connections": + return self._least_connections_selection(active_instances) + elif algorithm == "weighted_round_robin": + return self._weighted_round_robin_selection(active_instances) + elif algorithm == "ip_hash" and client_ip: + return self._ip_hash_selection(active_instances, client_ip) + else: + # 默认使用轮询 + return self._round_robin_selection(active_instances) + + except Exception as e: + print(f"✗ 获取服务器实例失败: {e}") + self.loadbalancer_stats["errors"] += 1 + return None + + def _round_robin_selection(self, active_instances: Dict[str, Dict[str, Any]]) -> Optional[Dict[str, Any]]: + """ + 轮询选择算法 + + Args: + active_instances: 活跃实例字典 + + Returns: + 选中的实例数据 + """ + try: + instance_ids = list(active_instances.keys()) + if not instance_ids: + return None + + # 选择下一个实例 + selected_index = self.loadbalancer_state["current_instance_index"] % len(instance_ids) + selected_instance_id = instance_ids[selected_index] + + # 更新索引 + self.loadbalancer_state["current_instance_index"] = (selected_index + 1) % len(instance_ids) + + return active_instances[selected_instance_id].copy() + + except Exception as e: + print(f"✗ 轮询选择失败: {e}") + self.loadbalancer_stats["errors"] += 1 + return None + + def _least_connections_selection(self, active_instances: Dict[str, Dict[str, Any]]) -> Optional[Dict[str, Any]]: + """ + 最少连接选择算法 + + Args: + active_instances: 活跃实例字典 + + Returns: + 选中的实例数据 + """ + try: + if not active_instances: + return None + + # 选择连接数最少的实例 + selected_instance_id = min( + active_instances.keys(), + key=lambda k: active_instances[k]["current_connections"] + ) + + return active_instances[selected_instance_id].copy() + + except Exception as e: + print(f"✗ 最少连接选择失败: {e}") + self.loadbalancer_stats["errors"] += 1 + return None + + def _weighted_round_robin_selection(self, active_instances: Dict[str, Dict[str, Any]]) -> Optional[Dict[str, Any]]: + """ + 加权轮询选择算法 + + Args: + active_instances: 活跃实例字典 + + Returns: + 选中的实例数据 + """ + try: + if not active_instances: + return None + + # 创建加权实例列表 + weighted_instances = [] + for instance_id, instance_data in active_instances.items(): + weight = instance_data.get("weight", 1) + for _ in range(weight): + weighted_instances.append(instance_id) + + if not weighted_instances: + return None + + # 选择实例 + selected_index = self.loadbalancer_state["current_instance_index"] % len(weighted_instances) + selected_instance_id = weighted_instances[selected_index] + + # 更新索引 + self.loadbalancer_state["current_instance_index"] = (selected_index + 1) % len(weighted_instances) + + return active_instances[selected_instance_id].copy() + + except Exception as e: + print(f"✗ 加权轮询选择失败: {e}") + self.loadbalancer_stats["errors"] += 1 + return None + + def _ip_hash_selection(self, active_instances: Dict[str, Dict[str, Any]], client_ip: str) -> Optional[Dict[str, Any]]: + """ + IP哈希选择算法 + + Args: + active_instances: 活跃实例字典 + client_ip: 客户端IP + + Returns: + 选中的实例数据 + """ + try: + if not active_instances or not client_ip: + return None + + # 检查会话粘性 + if self.loadbalancer_config["enable_session_stickiness"]: + with self.lb_lock: + if client_ip in self.loadbalancer_state["session_map"]: + session_data = self.loadbalancer_state["session_map"][client_ip] + if time.time() - session_data["timestamp"] <= self.loadbalancer_config["session_timeout"]: + instance_id = session_data["instance_id"] + if instance_id in active_instances: + return active_instances[instance_id].copy() + + # 使用IP哈希选择实例 + instance_ids = list(active_instances.keys()) + hash_value = int(hashlib.md5(client_ip.encode()).hexdigest(), 16) + selected_index = hash_value % len(instance_ids) + selected_instance_id = instance_ids[selected_index] + + # 更新会话映射 + if self.loadbalancer_config["enable_session_stickiness"]: + with self.lb_lock: + self.loadbalancer_state["session_map"][client_ip] = { + "instance_id": selected_instance_id, + "timestamp": time.time() + } + + return active_instances[selected_instance_id].copy() + + except Exception as e: + print(f"✗ IP哈希选择失败: {e}") + self.loadbalancer_stats["errors"] += 1 + return None + + def route_request(self, client_ip: str = None) -> Optional[Dict[str, Any]]: + """ + 路由请求到合适的服务器实例 + + Args: + client_ip: 客户端IP + + Returns: + 服务器实例数据或None + """ + try: + if not self.enabled: + return None + + # 获取服务器实例 + instance_data = self.get_server_instance(client_ip) + if not instance_data: + return None + + self.loadbalancer_stats["requests_routed"] += 1 + self.loadbalancer_state["total_requests"] += 1 + + # 增加实例连接数 + with self.lb_lock: + instance_id = instance_data.get("instance_id") + if instance_id and instance_id in self.server_instances: + self.server_instances[instance_id]["current_connections"] += 1 + + # 触发请求路由回调 + self._trigger_loadbalancer_callback("request_routed", { + "instance_id": instance_data.get("instance_id"), + "client_ip": client_ip, + "timestamp": time.time() + }) + + self.last_request = time.time() + return instance_data + + except Exception as e: + print(f"✗ 请求路由失败: {e}") + self.loadbalancer_stats["errors"] += 1 + self.loadbalancer_state["failed_requests"] += 1 + return None + + def release_instance_connection(self, instance_id: str): + """ + 释放实例连接 + + Args: + instance_id: 实例ID + """ + try: + with self.lb_lock: + if instance_id in self.server_instances: + if self.server_instances[instance_id]["current_connections"] > 0: + self.server_instances[instance_id]["current_connections"] -= 1 + + except Exception as e: + print(f"✗ 释放实例连接失败: {e}") + self.loadbalancer_stats["errors"] += 1 + + def get_instance_stats(self, instance_id: str) -> Optional[Dict[str, Any]]: + """ + 获取实例统计信息 + + Args: + instance_id: 实例ID + + Returns: + 实例统计信息或None + """ + try: + with self.lb_lock: + if instance_id in self.server_instances: + return self.server_instances[instance_id].copy() + else: + return None + + except Exception as e: + print(f"✗ 获取实例统计信息失败: {e}") + self.loadbalancer_stats["errors"] += 1 + return None + + def get_all_instances(self) -> Dict[str, Dict[str, Any]]: + """ + 获取所有实例信息 + + Returns: + 所有实例信息字典 + """ + try: + with self.lb_lock: + return {k: v.copy() for k, v in self.server_instances.items()} + except Exception as e: + print(f"✗ 获取所有实例信息失败: {e}") + self.loadbalancer_stats["errors"] += 1 + return {} + + def update_instance_status(self, instance_id: str, status: str) -> bool: + """ + 更新实例状态 + + Args: + instance_id: 实例ID + status: 状态(active, inactive, maintenance) + + Returns: + 是否更新成功 + """ + try: + with self.lb_lock: + if instance_id in self.server_instances: + self.server_instances[instance_id]["status"] = status + return True + else: + return False + + except Exception as e: + print(f"✗ 更新实例状态失败: {e}") + self.loadbalancer_stats["errors"] += 1 + return False + + def get_loadbalancer_stats(self) -> Dict[str, Any]: + """ + 获取负载均衡器统计信息 + + Returns: + 负载均衡器统计字典 + """ + return { + "state": self.loadbalancer_state.copy(), + "stats": self.loadbalancer_stats.copy(), + "config": self.loadbalancer_config.copy(), + "instances_count": len(self.server_instances) + } + + def reset_stats(self): + """重置负载均衡器统计信息""" + try: + self.loadbalancer_stats = { + "instances_added": 0, + "instances_removed": 0, + "health_checks": 0, + "failed_health_checks": 0, + "requests_routed": 0, + "errors": 0 + } + + self.loadbalancer_state["total_requests"] = 0 + self.loadbalancer_state["successful_requests"] = 0 + self.loadbalancer_state["failed_requests"] = 0 + + print("✓ 负载均衡器统计信息已重置") + except Exception as e: + print(f"✗ 负载均衡器统计信息重置失败: {e}") + + def set_loadbalancer_config(self, config: Dict[str, Any]) -> bool: + """ + 设置负载均衡器配置 + + Args: + config: 负载均衡器配置字典 + + Returns: + 是否设置成功 + """ + try: + self.loadbalancer_config.update(config) + print(f"✓ 负载均衡器配置已更新: {self.loadbalancer_config}") + return True + except Exception as e: + print(f"✗ 负载均衡器配置设置失败: {e}") + return False + + def get_loadbalancer_config(self) -> Dict[str, Any]: + """ + 获取负载均衡器配置 + + Returns: + 负载均衡器配置字典 + """ + return self.loadbalancer_config.copy() + + def _trigger_loadbalancer_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发负载均衡器回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.loadbalancer_callbacks: + for callback in self.loadbalancer_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 负载均衡器回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 负载均衡器回调触发失败: {e}") + + def register_loadbalancer_callback(self, callback_type: str, callback: callable): + """ + 注册负载均衡器回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.loadbalancer_callbacks: + self.loadbalancer_callbacks[callback_type].append(callback) + print(f"✓ 负载均衡器回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 负载均衡器回调注册失败: {e}") + + def unregister_loadbalancer_callback(self, callback_type: str, callback: callable): + """ + 注销负载均衡器回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.loadbalancer_callbacks: + if callback in self.loadbalancer_callbacks[callback_type]: + self.loadbalancer_callbacks[callback_type].remove(callback) + print(f"✓ 负载均衡器回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 负载均衡器回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/server_architecture/messaging/message_router.py b/plugins/user/server_architecture/messaging/message_router.py index 6cccc708..77b846b4 100644 --- a/plugins/user/server_architecture/messaging/message_router.py +++ b/plugins/user/server_architecture/messaging/message_router.py @@ -1,832 +1,832 @@ -""" -消息路由系统模块 -负责处理客户端和服务器之间的消息传递 -""" - -import time -import threading -import json -import uuid -from typing import Dict, Any, List, Optional, Callable - -class MessageRouter: - """ - 消息路由器 - 负责处理客户端和服务器之间的消息传递 - """ - - def __init__(self, plugin): - """ - 初始化消息路由器 - - Args: - plugin: 服务器架构插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 消息配置 - self.message_config = { - "enable_compression": True, - "enable_encryption": False, - "max_message_size": 65536, - "enable_message_queue": True, - "max_queue_size": 10000, - "enable_broadcast": True, - "enable_message_filtering": True, - "enable_message_logging": True, - "log_level": "INFO" - } - - # 消息处理器映射 - self.message_handlers = {} - - # 消息队列 - self.message_queue = [] - self.message_queue_lock = threading.RLock() - - # 消息统计 - self.message_stats = { - "messages_received": 0, - "messages_sent": 0, - "messages_routed": 0, - "messages_dropped": 0, - "broadcast_messages": 0, - "handler_errors": 0, - "bytes_received": 0, - "bytes_sent": 0, - "errors": 0 - } - - # 回调函数 - self.message_callbacks = { - "message_received": [], - "message_sent": [], - "message_routed": [], - "message_error": [], - "broadcast_sent": [] - } - - # 时间戳记录 - self.last_message_time = 0.0 - self.last_queue_process = 0.0 - - print("✓ 消息路由器已创建") - - def initialize(self) -> bool: - """ - 初始化消息路由器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化消息路由器...") - - # 注册默认消息处理器 - self._register_default_handlers() - - self.initialized = True - print("✓ 消息路由器初始化完成") - return True - - except Exception as e: - print(f"✗ 消息路由器初始化失败: {e}") - self.message_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def _register_default_handlers(self): - """注册默认消息处理器""" - try: - self.register_message_handler("ping", self._handle_ping) - self.register_message_handler("pong", self._handle_pong) - self.register_message_handler("heartbeat", self._handle_heartbeat) - self.register_message_handler("auth", self._handle_auth) - self.register_message_handler("chat", self._handle_chat) - self.register_message_handler("join_room", self._handle_join_room) - self.register_message_handler("leave_room", self._handle_leave_room) - self.register_message_handler("sync_data", self._handle_sync_data) - - print("✓ 默认消息处理器已注册") - except Exception as e: - print(f"✗ 默认消息处理器注册失败: {e}") - - def enable(self) -> bool: - """ - 启用消息路由器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 消息路由器未初始化") - return False - - self.enabled = True - print("✓ 消息路由器已启用") - return True - - except Exception as e: - print(f"✗ 消息路由器启用失败: {e}") - self.message_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用消息路由器""" - try: - self.enabled = False - - # 清空消息队列 - with self.message_queue_lock: - self.message_queue.clear() - - print("✓ 消息路由器已禁用") - - except Exception as e: - print(f"✗ 消息路由器禁用失败: {e}") - self.message_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理消息路由器资源""" - try: - # 禁用路由器 - if self.enabled: - self.disable() - - # 清理回调和处理器 - self.message_callbacks.clear() - self.message_handlers.clear() - - self.initialized = False - print("✓ 消息路由器资源已清理") - - except Exception as e: - print(f"✗ 消息路由器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新消息路由器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.last_message_time = current_time - - # 处理消息队列 - if self.message_config["enable_message_queue"]: - self._process_message_queue() - - except Exception as e: - print(f"✗ 消息路由器更新失败: {e}") - self.message_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def _process_message_queue(self): - """处理消息队列""" - try: - current_time = time.time() - if current_time - self.last_queue_process < 0.01: # 限制处理频率 - return - - processed_messages = 0 - max_messages_per_update = 100 # 每次更新最多处理100条消息 - - with self.message_queue_lock: - while self.message_queue and processed_messages < max_messages_per_update: - try: - message_data = self.message_queue.pop(0) - self._route_message_internal( - message_data["message"], - message_data["client_address"], - message_data["client_socket"], - message_data["client_id"] - ) - processed_messages += 1 - except Exception as e: - print(f"✗ 队列消息处理失败: {e}") - self.message_stats["errors"] += 1 - processed_messages += 1 # 避免无限循环 - - self.last_queue_process = current_time - - except Exception as e: - print(f"✗ 消息队列处理失败: {e}") - self.message_stats["errors"] += 1 - - def route_message(self, message: bytes, client_address: tuple, client_socket, client_id: str = None): - """ - 路由消息 - - Args: - message: 消息数据 - client_address: 客户端地址 - client_socket: 客户端套接字 - client_id: 客户端ID(可选) - """ - try: - if not self.enabled: - return - - self.message_stats["messages_received"] += 1 - self.message_stats["bytes_received"] += len(message) - - # 触发消息接收回调 - self._trigger_message_callback("message_received", { - "message_size": len(message), - "client_address": client_address, - "client_id": client_id, - "timestamp": time.time() - }) - - # 检查消息大小 - if len(message) > self.message_config["max_message_size"]: - print(f"✗ 消息过大,已丢弃: {len(message)} bytes") - self.message_stats["messages_dropped"] += 1 - return - - # 如果启用队列,将消息添加到队列中 - if self.message_config["enable_message_queue"]: - with self.message_queue_lock: - if len(self.message_queue) < self.message_config["max_queue_size"]: - self.message_queue.append({ - "message": message, - "client_address": client_address, - "client_socket": client_socket, - "client_id": client_id - }) - else: - print("✗ 消息队列已满,丢弃消息") - self.message_stats["messages_dropped"] += 1 - else: - # 直接处理消息 - self._route_message_internal(message, client_address, client_socket, client_id) - - except Exception as e: - print(f"✗ 消息路由失败: {e}") - self.message_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def _route_message_internal(self, message: bytes, client_address: tuple, client_socket, client_id: str = None): - """ - 内部消息路由处理 - - Args: - message: 消息数据 - client_address: 客户端地址 - client_socket: 客户端套接字 - client_id: 客户端ID(可选) - """ - try: - # 解析消息 - try: - message_str = message.decode('utf-8') - message_data = json.loads(message_str) - except Exception as e: - print(f"✗ 消息解析失败: {e}") - self.message_stats["errors"] += 1 - return - - # 获取消息类型 - message_type = message_data.get("type", "unknown") - - # 检查消息过滤 - if self.message_config["enable_message_filtering"]: - if not self._is_message_allowed(message_type, client_id): - print(f"✗ 消息被过滤: {message_type}") - return - - # 查找消息处理器 - handler = self.message_handlers.get(message_type) - if handler: - try: - # 调用消息处理器 - handler(message_data, client_address, client_socket, client_id) - self.message_stats["messages_routed"] += 1 - - # 触发消息路由回调 - self._trigger_message_callback("message_routed", { - "message_type": message_type, - "client_address": client_address, - "client_id": client_id, - "timestamp": time.time() - }) - - except Exception as e: - print(f"✗ 消息处理器执行失败: {message_type} - {e}") - self.message_stats["handler_errors"] += 1 - - # 触发消息错误回调 - self._trigger_message_callback("message_error", { - "message_type": message_type, - "error": str(e), - "client_address": client_address, - "client_id": client_id, - "timestamp": time.time() - }) - else: - print(f"✗ 未找到消息处理器: {message_type}") - self.message_stats["errors"] += 1 - - except Exception as e: - print(f"✗ 内部消息路由处理失败: {e}") - self.message_stats["errors"] += 1 - - def _is_message_allowed(self, message_type: str, client_id: str = None) -> bool: - """ - 检查消息是否被允许 - - Args: - message_type: 消息类型 - client_id: 客户端ID(可选) - - Returns: - 消息是否被允许 - """ - try: - # 检查客户端是否已认证(某些消息类型不需要认证) - if message_type not in ["auth", "ping", "pong"]: - if not client_id or not self.plugin.client_manager.is_client_authenticated(client_id): - return False - - return True - - except Exception as e: - print(f"✗ 消息允许检查失败: {e}") - self.message_stats["errors"] += 1 - return False - - def register_message_handler(self, message_type: str, handler: Callable) -> bool: - """ - 注册消息处理器 - - Args: - message_type: 消息类型 - handler: 处理器函数 - - Returns: - 是否注册成功 - """ - try: - self.message_handlers[message_type] = handler - print(f"✓ 消息处理器已注册: {message_type}") - return True - except Exception as e: - print(f"✗ 消息处理器注册失败: {e}") - return False - - def unregister_message_handler(self, message_type: str) -> bool: - """ - 注销消息处理器 - - Args: - message_type: 消息类型 - - Returns: - 是否注销成功 - """ - try: - if message_type in self.message_handlers: - del self.message_handlers[message_type] - print(f"✓ 消息处理器已注销: {message_type}") - return True - else: - print(f"✗ 消息处理器不存在: {message_type}") - return False - except Exception as e: - print(f"✗ 消息处理器注销失败: {e}") - return False - - def send_message(self, message_data: Dict[str, Any], client_socket, client_id: str = None) -> bool: - """ - 发送消息到客户端 - - Args: - message_data: 消息数据 - client_socket: 客户端套接字 - client_id: 客户端ID(可选) - - Returns: - 是否发送成功 - """ - try: - if not self.enabled: - return False - - # 序列化消息 - try: - message_str = json.dumps(message_data, ensure_ascii=False) - message_bytes = message_str.encode('utf-8') - except Exception as e: - print(f"✗ 消息序列化失败: {e}") - self.message_stats["errors"] += 1 - return False - - # 检查消息大小 - if len(message_bytes) > self.message_config["max_message_size"]: - print(f"✗ 消息过大,无法发送: {len(message_bytes)} bytes") - self.message_stats["messages_dropped"] += 1 - return False - - # 发送消息 - try: - client_socket.send(message_bytes) - except Exception as e: - print(f"✗ 消息发送失败: {e}") - self.message_stats["errors"] += 1 - return False - - self.message_stats["messages_sent"] += 1 - self.message_stats["bytes_sent"] += len(message_bytes) - - # 更新客户端统计 - if client_id: - client_data = self.plugin.client_manager.get_client(client_id) - if client_data: - client_data["messages_sent"] += 1 - client_data["bytes_sent"] += len(message_bytes) - - # 触发消息发送回调 - self._trigger_message_callback("message_sent", { - "message_type": message_data.get("type", "unknown"), - "message_size": len(message_bytes), - "client_id": client_id, - "timestamp": time.time() - }) - - return True - - except Exception as e: - print(f"✗ 发送消息失败: {e}") - self.message_stats["errors"] += 1 - return False - - def broadcast_message(self, message_data: Dict[str, Any], exclude_client_id: str = None) -> bool: - """ - 广播消息到所有客户端 - - Args: - message_data: 消息数据 - exclude_client_id: 要排除的客户端ID(可选) - - Returns: - 是否广播成功 - """ - try: - if not self.enabled or not self.message_config["enable_broadcast"]: - return False - - # 序列化消息 - try: - message_str = json.dumps(message_data, ensure_ascii=False) - message_bytes = message_str.encode('utf-8') - except Exception as e: - print(f"✗ 广播消息序列化失败: {e}") - self.message_stats["errors"] += 1 - return False - - # 检查消息大小 - if len(message_bytes) > self.message_config["max_message_size"]: - print(f"✗ 广播消息过大,无法发送: {len(message_bytes)} bytes") - self.message_stats["messages_dropped"] += 1 - return False - - # 获取所有客户端 - clients = self.plugin.client_manager.get_all_clients() - success_count = 0 - - # 发送消息到每个客户端 - for client_id, client_data in clients.items(): - # 跳过排除的客户端 - if client_id == exclude_client_id: - continue - - try: - client_data["socket"].send(message_bytes) - client_data["messages_sent"] += 1 - client_data["bytes_sent"] += len(message_bytes) - success_count += 1 - except Exception as e: - print(f"✗ 广播消息发送失败: {client_id} - {e}") - self.message_stats["errors"] += 1 - - self.message_stats["broadcast_messages"] += 1 - self.message_stats["messages_sent"] += success_count - self.message_stats["bytes_sent"] += len(message_bytes) * success_count - - # 触发广播发送回调 - self._trigger_message_callback("broadcast_sent", { - "message_type": message_data.get("type", "unknown"), - "message_size": len(message_bytes), - "recipient_count": success_count, - "timestamp": time.time() - }) - - return success_count > 0 - - except Exception as e: - print(f"✗ 广播消息失败: {e}") - self.message_stats["errors"] += 1 - return False - - # 默认消息处理器 - def _handle_ping(self, message_data: Dict[str, Any], client_address: tuple, client_socket, client_id: str = None): - """处理ping消息""" - try: - # 回复pong消息 - pong_message = { - "type": "pong", - "timestamp": time.time(), - "ping_id": message_data.get("ping_id", str(uuid.uuid4())) - } - - self.send_message(pong_message, client_socket, client_id) - - # 更新客户端心跳 - if client_id: - self.plugin.client_manager.update_client_heartbeat(client_id) - - except Exception as e: - print(f"✗ ping消息处理失败: {e}") - self.message_stats["errors"] += 1 - - def _handle_pong(self, message_data: Dict[str, Any], client_address: tuple, client_socket, client_id: str = None): - """处理pong消息""" - try: - # 可以在这里处理延迟计算等逻辑 - pass - except Exception as e: - print(f"✗ pong消息处理失败: {e}") - self.message_stats["errors"] += 1 - - def _handle_heartbeat(self, message_data: Dict[str, Any], client_address: tuple, client_socket, client_id: str = None): - """处理心跳消息""" - try: - # 更新客户端心跳 - if client_id: - self.plugin.client_manager.update_client_heartbeat(client_id) - except Exception as e: - print(f"✗ 心跳消息处理失败: {e}") - self.message_stats["errors"] += 1 - - def _handle_auth(self, message_data: Dict[str, Any], client_address: tuple, client_socket, client_id: str = None): - """处理认证消息""" - try: - if not client_id: - print("✗ 认证消息缺少客户端ID") - return - - # 使用客户端管理器进行认证 - if self.plugin.client_manager: - auth_success = self.plugin.client_manager.authenticate_client(client_id, message_data) - if auth_success: - # 发送认证成功消息 - success_message = { - "type": "auth_result", - "success": True, - "message": "认证成功" - } - self.send_message(success_message, client_socket, client_id) - else: - # 发送认证失败消息 - fail_message = { - "type": "auth_result", - "success": False, - "message": "认证失败" - } - self.send_message(fail_message, client_socket, client_id) - except Exception as e: - print(f"✗ 认证消息处理失败: {e}") - self.message_stats["errors"] += 1 - - def _handle_chat(self, message_data: Dict[str, Any], client_address: tuple, client_socket, client_id: str = None): - """处理聊天消息""" - try: - # 广播聊天消息到所有客户端 - chat_message = { - "type": "chat", - "sender": message_data.get("sender", "unknown"), - "message": message_data.get("message", ""), - "timestamp": time.time() - } - - self.broadcast_message(chat_message, exclude_client_id=client_id) - except Exception as e: - print(f"✗ 聊天消息处理失败: {e}") - self.message_stats["errors"] += 1 - - def _handle_join_room(self, message_data: Dict[str, Any], client_address: tuple, client_socket, client_id: str = None): - """处理加入房间消息""" - try: - room_id = message_data.get("room_id") - if not room_id: - print("✗ 加入房间消息缺少房间ID") - return - - # 使用房间管理器处理加入房间请求 - if self.plugin.room_manager: - join_success = self.plugin.room_manager.add_client_to_room(room_id, client_id) - if join_success: - # 发送加入成功消息 - success_message = { - "type": "join_room_result", - "success": True, - "room_id": room_id, - "message": "成功加入房间" - } - self.send_message(success_message, client_socket, client_id) - - # 通知房间内其他客户端 - notification = { - "type": "client_joined_room", - "client_id": client_id, - "room_id": room_id - } - self.plugin.room_manager.broadcast_to_room(room_id, notification, exclude_client_id=client_id) - else: - # 发送加入失败消息 - fail_message = { - "type": "join_room_result", - "success": False, - "room_id": room_id, - "message": "加入房间失败" - } - self.send_message(fail_message, client_socket, client_id) - except Exception as e: - print(f"✗ 加入房间消息处理失败: {e}") - self.message_stats["errors"] += 1 - - def _handle_leave_room(self, message_data: Dict[str, Any], client_address: tuple, client_socket, client_id: str = None): - """处理离开房间消息""" - try: - room_id = message_data.get("room_id") - if not room_id: - print("✗ 离开房间消息缺少房间ID") - return - - # 使用房间管理器处理离开房间请求 - if self.plugin.room_manager: - leave_success = self.plugin.room_manager.remove_client_from_room(room_id, client_id) - if leave_success: - # 发送离开成功消息 - success_message = { - "type": "leave_room_result", - "success": True, - "room_id": room_id, - "message": "成功离开房间" - } - self.send_message(success_message, client_socket, client_id) - - # 通知房间内其他客户端 - notification = { - "type": "client_left_room", - "client_id": client_id, - "room_id": room_id - } - self.plugin.room_manager.broadcast_to_room(room_id, notification, exclude_client_id=client_id) - except Exception as e: - print(f"✗ 离开房间消息处理失败: {e}") - self.message_stats["errors"] += 1 - - def _handle_sync_data(self, message_data: Dict[str, Any], client_address: tuple, client_socket, client_id: str = None): - """处理同步数据消息""" - try: - # 将同步数据广播到房间内的其他客户端 - room_id = message_data.get("room_id") - if room_id and self.plugin.room_manager: - # 移除房间ID,避免重复处理 - sync_data = message_data.copy() - sync_data.pop("room_id", None) - - self.plugin.room_manager.broadcast_to_room(room_id, sync_data, exclude_client_id=client_id) - except Exception as e: - print(f"✗ 同步数据消息处理失败: {e}") - self.message_stats["errors"] += 1 - - def get_message_stats(self) -> Dict[str, Any]: - """ - 获取消息统计信息 - - Returns: - 消息统计字典 - """ - return { - "stats": self.message_stats.copy(), - "config": self.message_config.copy(), - "handlers_count": len(self.message_handlers), - "queue_size": len(self.message_queue) - } - - def reset_stats(self): - """重置消息统计信息""" - try: - self.message_stats = { - "messages_received": 0, - "messages_sent": 0, - "messages_routed": 0, - "messages_dropped": 0, - "broadcast_messages": 0, - "handler_errors": 0, - "bytes_received": 0, - "bytes_sent": 0, - "errors": 0 - } - print("✓ 消息统计信息已重置") - except Exception as e: - print(f"✗ 消息统计信息重置失败: {e}") - - def set_message_config(self, config: Dict[str, Any]) -> bool: - """ - 设置消息配置 - - Args: - config: 消息配置字典 - - Returns: - 是否设置成功 - """ - try: - self.message_config.update(config) - print(f"✓ 消息配置已更新: {self.message_config}") - return True - except Exception as e: - print(f"✗ 消息配置设置失败: {e}") - return False - - def get_message_config(self) -> Dict[str, Any]: - """ - 获取消息配置 - - Returns: - 消息配置字典 - """ - return self.message_config.copy() - - def _trigger_message_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发消息回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.message_callbacks: - for callback in self.message_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 消息回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 消息回调触发失败: {e}") - - def register_message_callback(self, callback_type: str, callback: callable): - """ - 注册消息回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.message_callbacks: - self.message_callbacks[callback_type].append(callback) - print(f"✓ 消息回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 消息回调注册失败: {e}") - - def unregister_message_callback(self, callback_type: str, callback: callable): - """ - 注销消息回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.message_callbacks: - if callback in self.message_callbacks[callback_type]: - self.message_callbacks[callback_type].remove(callback) - print(f"✓ 消息回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +消息路由系统模块 +负责处理客户端和服务器之间的消息传递 +""" + +import time +import threading +import json +import uuid +from typing import Dict, Any, List, Optional, Callable + +class MessageRouter: + """ + 消息路由器 + 负责处理客户端和服务器之间的消息传递 + """ + + def __init__(self, plugin): + """ + 初始化消息路由器 + + Args: + plugin: 服务器架构插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 消息配置 + self.message_config = { + "enable_compression": True, + "enable_encryption": False, + "max_message_size": 65536, + "enable_message_queue": True, + "max_queue_size": 10000, + "enable_broadcast": True, + "enable_message_filtering": True, + "enable_message_logging": True, + "log_level": "INFO" + } + + # 消息处理器映射 + self.message_handlers = {} + + # 消息队列 + self.message_queue = [] + self.message_queue_lock = threading.RLock() + + # 消息统计 + self.message_stats = { + "messages_received": 0, + "messages_sent": 0, + "messages_routed": 0, + "messages_dropped": 0, + "broadcast_messages": 0, + "handler_errors": 0, + "bytes_received": 0, + "bytes_sent": 0, + "errors": 0 + } + + # 回调函数 + self.message_callbacks = { + "message_received": [], + "message_sent": [], + "message_routed": [], + "message_error": [], + "broadcast_sent": [] + } + + # 时间戳记录 + self.last_message_time = 0.0 + self.last_queue_process = 0.0 + + print("✓ 消息路由器已创建") + + def initialize(self) -> bool: + """ + 初始化消息路由器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化消息路由器...") + + # 注册默认消息处理器 + self._register_default_handlers() + + self.initialized = True + print("✓ 消息路由器初始化完成") + return True + + except Exception as e: + print(f"✗ 消息路由器初始化失败: {e}") + self.message_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def _register_default_handlers(self): + """注册默认消息处理器""" + try: + self.register_message_handler("ping", self._handle_ping) + self.register_message_handler("pong", self._handle_pong) + self.register_message_handler("heartbeat", self._handle_heartbeat) + self.register_message_handler("auth", self._handle_auth) + self.register_message_handler("chat", self._handle_chat) + self.register_message_handler("join_room", self._handle_join_room) + self.register_message_handler("leave_room", self._handle_leave_room) + self.register_message_handler("sync_data", self._handle_sync_data) + + print("✓ 默认消息处理器已注册") + except Exception as e: + print(f"✗ 默认消息处理器注册失败: {e}") + + def enable(self) -> bool: + """ + 启用消息路由器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 消息路由器未初始化") + return False + + self.enabled = True + print("✓ 消息路由器已启用") + return True + + except Exception as e: + print(f"✗ 消息路由器启用失败: {e}") + self.message_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用消息路由器""" + try: + self.enabled = False + + # 清空消息队列 + with self.message_queue_lock: + self.message_queue.clear() + + print("✓ 消息路由器已禁用") + + except Exception as e: + print(f"✗ 消息路由器禁用失败: {e}") + self.message_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理消息路由器资源""" + try: + # 禁用路由器 + if self.enabled: + self.disable() + + # 清理回调和处理器 + self.message_callbacks.clear() + self.message_handlers.clear() + + self.initialized = False + print("✓ 消息路由器资源已清理") + + except Exception as e: + print(f"✗ 消息路由器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新消息路由器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.last_message_time = current_time + + # 处理消息队列 + if self.message_config["enable_message_queue"]: + self._process_message_queue() + + except Exception as e: + print(f"✗ 消息路由器更新失败: {e}") + self.message_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def _process_message_queue(self): + """处理消息队列""" + try: + current_time = time.time() + if current_time - self.last_queue_process < 0.01: # 限制处理频率 + return + + processed_messages = 0 + max_messages_per_update = 100 # 每次更新最多处理100条消息 + + with self.message_queue_lock: + while self.message_queue and processed_messages < max_messages_per_update: + try: + message_data = self.message_queue.pop(0) + self._route_message_internal( + message_data["message"], + message_data["client_address"], + message_data["client_socket"], + message_data["client_id"] + ) + processed_messages += 1 + except Exception as e: + print(f"✗ 队列消息处理失败: {e}") + self.message_stats["errors"] += 1 + processed_messages += 1 # 避免无限循环 + + self.last_queue_process = current_time + + except Exception as e: + print(f"✗ 消息队列处理失败: {e}") + self.message_stats["errors"] += 1 + + def route_message(self, message: bytes, client_address: tuple, client_socket, client_id: str = None): + """ + 路由消息 + + Args: + message: 消息数据 + client_address: 客户端地址 + client_socket: 客户端套接字 + client_id: 客户端ID(可选) + """ + try: + if not self.enabled: + return + + self.message_stats["messages_received"] += 1 + self.message_stats["bytes_received"] += len(message) + + # 触发消息接收回调 + self._trigger_message_callback("message_received", { + "message_size": len(message), + "client_address": client_address, + "client_id": client_id, + "timestamp": time.time() + }) + + # 检查消息大小 + if len(message) > self.message_config["max_message_size"]: + print(f"✗ 消息过大,已丢弃: {len(message)} bytes") + self.message_stats["messages_dropped"] += 1 + return + + # 如果启用队列,将消息添加到队列中 + if self.message_config["enable_message_queue"]: + with self.message_queue_lock: + if len(self.message_queue) < self.message_config["max_queue_size"]: + self.message_queue.append({ + "message": message, + "client_address": client_address, + "client_socket": client_socket, + "client_id": client_id + }) + else: + print("✗ 消息队列已满,丢弃消息") + self.message_stats["messages_dropped"] += 1 + else: + # 直接处理消息 + self._route_message_internal(message, client_address, client_socket, client_id) + + except Exception as e: + print(f"✗ 消息路由失败: {e}") + self.message_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def _route_message_internal(self, message: bytes, client_address: tuple, client_socket, client_id: str = None): + """ + 内部消息路由处理 + + Args: + message: 消息数据 + client_address: 客户端地址 + client_socket: 客户端套接字 + client_id: 客户端ID(可选) + """ + try: + # 解析消息 + try: + message_str = message.decode('utf-8') + message_data = json.loads(message_str) + except Exception as e: + print(f"✗ 消息解析失败: {e}") + self.message_stats["errors"] += 1 + return + + # 获取消息类型 + message_type = message_data.get("type", "unknown") + + # 检查消息过滤 + if self.message_config["enable_message_filtering"]: + if not self._is_message_allowed(message_type, client_id): + print(f"✗ 消息被过滤: {message_type}") + return + + # 查找消息处理器 + handler = self.message_handlers.get(message_type) + if handler: + try: + # 调用消息处理器 + handler(message_data, client_address, client_socket, client_id) + self.message_stats["messages_routed"] += 1 + + # 触发消息路由回调 + self._trigger_message_callback("message_routed", { + "message_type": message_type, + "client_address": client_address, + "client_id": client_id, + "timestamp": time.time() + }) + + except Exception as e: + print(f"✗ 消息处理器执行失败: {message_type} - {e}") + self.message_stats["handler_errors"] += 1 + + # 触发消息错误回调 + self._trigger_message_callback("message_error", { + "message_type": message_type, + "error": str(e), + "client_address": client_address, + "client_id": client_id, + "timestamp": time.time() + }) + else: + print(f"✗ 未找到消息处理器: {message_type}") + self.message_stats["errors"] += 1 + + except Exception as e: + print(f"✗ 内部消息路由处理失败: {e}") + self.message_stats["errors"] += 1 + + def _is_message_allowed(self, message_type: str, client_id: str = None) -> bool: + """ + 检查消息是否被允许 + + Args: + message_type: 消息类型 + client_id: 客户端ID(可选) + + Returns: + 消息是否被允许 + """ + try: + # 检查客户端是否已认证(某些消息类型不需要认证) + if message_type not in ["auth", "ping", "pong"]: + if not client_id or not self.plugin.client_manager.is_client_authenticated(client_id): + return False + + return True + + except Exception as e: + print(f"✗ 消息允许检查失败: {e}") + self.message_stats["errors"] += 1 + return False + + def register_message_handler(self, message_type: str, handler: Callable) -> bool: + """ + 注册消息处理器 + + Args: + message_type: 消息类型 + handler: 处理器函数 + + Returns: + 是否注册成功 + """ + try: + self.message_handlers[message_type] = handler + print(f"✓ 消息处理器已注册: {message_type}") + return True + except Exception as e: + print(f"✗ 消息处理器注册失败: {e}") + return False + + def unregister_message_handler(self, message_type: str) -> bool: + """ + 注销消息处理器 + + Args: + message_type: 消息类型 + + Returns: + 是否注销成功 + """ + try: + if message_type in self.message_handlers: + del self.message_handlers[message_type] + print(f"✓ 消息处理器已注销: {message_type}") + return True + else: + print(f"✗ 消息处理器不存在: {message_type}") + return False + except Exception as e: + print(f"✗ 消息处理器注销失败: {e}") + return False + + def send_message(self, message_data: Dict[str, Any], client_socket, client_id: str = None) -> bool: + """ + 发送消息到客户端 + + Args: + message_data: 消息数据 + client_socket: 客户端套接字 + client_id: 客户端ID(可选) + + Returns: + 是否发送成功 + """ + try: + if not self.enabled: + return False + + # 序列化消息 + try: + message_str = json.dumps(message_data, ensure_ascii=False) + message_bytes = message_str.encode('utf-8') + except Exception as e: + print(f"✗ 消息序列化失败: {e}") + self.message_stats["errors"] += 1 + return False + + # 检查消息大小 + if len(message_bytes) > self.message_config["max_message_size"]: + print(f"✗ 消息过大,无法发送: {len(message_bytes)} bytes") + self.message_stats["messages_dropped"] += 1 + return False + + # 发送消息 + try: + client_socket.send(message_bytes) + except Exception as e: + print(f"✗ 消息发送失败: {e}") + self.message_stats["errors"] += 1 + return False + + self.message_stats["messages_sent"] += 1 + self.message_stats["bytes_sent"] += len(message_bytes) + + # 更新客户端统计 + if client_id: + client_data = self.plugin.client_manager.get_client(client_id) + if client_data: + client_data["messages_sent"] += 1 + client_data["bytes_sent"] += len(message_bytes) + + # 触发消息发送回调 + self._trigger_message_callback("message_sent", { + "message_type": message_data.get("type", "unknown"), + "message_size": len(message_bytes), + "client_id": client_id, + "timestamp": time.time() + }) + + return True + + except Exception as e: + print(f"✗ 发送消息失败: {e}") + self.message_stats["errors"] += 1 + return False + + def broadcast_message(self, message_data: Dict[str, Any], exclude_client_id: str = None) -> bool: + """ + 广播消息到所有客户端 + + Args: + message_data: 消息数据 + exclude_client_id: 要排除的客户端ID(可选) + + Returns: + 是否广播成功 + """ + try: + if not self.enabled or not self.message_config["enable_broadcast"]: + return False + + # 序列化消息 + try: + message_str = json.dumps(message_data, ensure_ascii=False) + message_bytes = message_str.encode('utf-8') + except Exception as e: + print(f"✗ 广播消息序列化失败: {e}") + self.message_stats["errors"] += 1 + return False + + # 检查消息大小 + if len(message_bytes) > self.message_config["max_message_size"]: + print(f"✗ 广播消息过大,无法发送: {len(message_bytes)} bytes") + self.message_stats["messages_dropped"] += 1 + return False + + # 获取所有客户端 + clients = self.plugin.client_manager.get_all_clients() + success_count = 0 + + # 发送消息到每个客户端 + for client_id, client_data in clients.items(): + # 跳过排除的客户端 + if client_id == exclude_client_id: + continue + + try: + client_data["socket"].send(message_bytes) + client_data["messages_sent"] += 1 + client_data["bytes_sent"] += len(message_bytes) + success_count += 1 + except Exception as e: + print(f"✗ 广播消息发送失败: {client_id} - {e}") + self.message_stats["errors"] += 1 + + self.message_stats["broadcast_messages"] += 1 + self.message_stats["messages_sent"] += success_count + self.message_stats["bytes_sent"] += len(message_bytes) * success_count + + # 触发广播发送回调 + self._trigger_message_callback("broadcast_sent", { + "message_type": message_data.get("type", "unknown"), + "message_size": len(message_bytes), + "recipient_count": success_count, + "timestamp": time.time() + }) + + return success_count > 0 + + except Exception as e: + print(f"✗ 广播消息失败: {e}") + self.message_stats["errors"] += 1 + return False + + # 默认消息处理器 + def _handle_ping(self, message_data: Dict[str, Any], client_address: tuple, client_socket, client_id: str = None): + """处理ping消息""" + try: + # 回复pong消息 + pong_message = { + "type": "pong", + "timestamp": time.time(), + "ping_id": message_data.get("ping_id", str(uuid.uuid4())) + } + + self.send_message(pong_message, client_socket, client_id) + + # 更新客户端心跳 + if client_id: + self.plugin.client_manager.update_client_heartbeat(client_id) + + except Exception as e: + print(f"✗ ping消息处理失败: {e}") + self.message_stats["errors"] += 1 + + def _handle_pong(self, message_data: Dict[str, Any], client_address: tuple, client_socket, client_id: str = None): + """处理pong消息""" + try: + # 可以在这里处理延迟计算等逻辑 + pass + except Exception as e: + print(f"✗ pong消息处理失败: {e}") + self.message_stats["errors"] += 1 + + def _handle_heartbeat(self, message_data: Dict[str, Any], client_address: tuple, client_socket, client_id: str = None): + """处理心跳消息""" + try: + # 更新客户端心跳 + if client_id: + self.plugin.client_manager.update_client_heartbeat(client_id) + except Exception as e: + print(f"✗ 心跳消息处理失败: {e}") + self.message_stats["errors"] += 1 + + def _handle_auth(self, message_data: Dict[str, Any], client_address: tuple, client_socket, client_id: str = None): + """处理认证消息""" + try: + if not client_id: + print("✗ 认证消息缺少客户端ID") + return + + # 使用客户端管理器进行认证 + if self.plugin.client_manager: + auth_success = self.plugin.client_manager.authenticate_client(client_id, message_data) + if auth_success: + # 发送认证成功消息 + success_message = { + "type": "auth_result", + "success": True, + "message": "认证成功" + } + self.send_message(success_message, client_socket, client_id) + else: + # 发送认证失败消息 + fail_message = { + "type": "auth_result", + "success": False, + "message": "认证失败" + } + self.send_message(fail_message, client_socket, client_id) + except Exception as e: + print(f"✗ 认证消息处理失败: {e}") + self.message_stats["errors"] += 1 + + def _handle_chat(self, message_data: Dict[str, Any], client_address: tuple, client_socket, client_id: str = None): + """处理聊天消息""" + try: + # 广播聊天消息到所有客户端 + chat_message = { + "type": "chat", + "sender": message_data.get("sender", "unknown"), + "message": message_data.get("message", ""), + "timestamp": time.time() + } + + self.broadcast_message(chat_message, exclude_client_id=client_id) + except Exception as e: + print(f"✗ 聊天消息处理失败: {e}") + self.message_stats["errors"] += 1 + + def _handle_join_room(self, message_data: Dict[str, Any], client_address: tuple, client_socket, client_id: str = None): + """处理加入房间消息""" + try: + room_id = message_data.get("room_id") + if not room_id: + print("✗ 加入房间消息缺少房间ID") + return + + # 使用房间管理器处理加入房间请求 + if self.plugin.room_manager: + join_success = self.plugin.room_manager.add_client_to_room(room_id, client_id) + if join_success: + # 发送加入成功消息 + success_message = { + "type": "join_room_result", + "success": True, + "room_id": room_id, + "message": "成功加入房间" + } + self.send_message(success_message, client_socket, client_id) + + # 通知房间内其他客户端 + notification = { + "type": "client_joined_room", + "client_id": client_id, + "room_id": room_id + } + self.plugin.room_manager.broadcast_to_room(room_id, notification, exclude_client_id=client_id) + else: + # 发送加入失败消息 + fail_message = { + "type": "join_room_result", + "success": False, + "room_id": room_id, + "message": "加入房间失败" + } + self.send_message(fail_message, client_socket, client_id) + except Exception as e: + print(f"✗ 加入房间消息处理失败: {e}") + self.message_stats["errors"] += 1 + + def _handle_leave_room(self, message_data: Dict[str, Any], client_address: tuple, client_socket, client_id: str = None): + """处理离开房间消息""" + try: + room_id = message_data.get("room_id") + if not room_id: + print("✗ 离开房间消息缺少房间ID") + return + + # 使用房间管理器处理离开房间请求 + if self.plugin.room_manager: + leave_success = self.plugin.room_manager.remove_client_from_room(room_id, client_id) + if leave_success: + # 发送离开成功消息 + success_message = { + "type": "leave_room_result", + "success": True, + "room_id": room_id, + "message": "成功离开房间" + } + self.send_message(success_message, client_socket, client_id) + + # 通知房间内其他客户端 + notification = { + "type": "client_left_room", + "client_id": client_id, + "room_id": room_id + } + self.plugin.room_manager.broadcast_to_room(room_id, notification, exclude_client_id=client_id) + except Exception as e: + print(f"✗ 离开房间消息处理失败: {e}") + self.message_stats["errors"] += 1 + + def _handle_sync_data(self, message_data: Dict[str, Any], client_address: tuple, client_socket, client_id: str = None): + """处理同步数据消息""" + try: + # 将同步数据广播到房间内的其他客户端 + room_id = message_data.get("room_id") + if room_id and self.plugin.room_manager: + # 移除房间ID,避免重复处理 + sync_data = message_data.copy() + sync_data.pop("room_id", None) + + self.plugin.room_manager.broadcast_to_room(room_id, sync_data, exclude_client_id=client_id) + except Exception as e: + print(f"✗ 同步数据消息处理失败: {e}") + self.message_stats["errors"] += 1 + + def get_message_stats(self) -> Dict[str, Any]: + """ + 获取消息统计信息 + + Returns: + 消息统计字典 + """ + return { + "stats": self.message_stats.copy(), + "config": self.message_config.copy(), + "handlers_count": len(self.message_handlers), + "queue_size": len(self.message_queue) + } + + def reset_stats(self): + """重置消息统计信息""" + try: + self.message_stats = { + "messages_received": 0, + "messages_sent": 0, + "messages_routed": 0, + "messages_dropped": 0, + "broadcast_messages": 0, + "handler_errors": 0, + "bytes_received": 0, + "bytes_sent": 0, + "errors": 0 + } + print("✓ 消息统计信息已重置") + except Exception as e: + print(f"✗ 消息统计信息重置失败: {e}") + + def set_message_config(self, config: Dict[str, Any]) -> bool: + """ + 设置消息配置 + + Args: + config: 消息配置字典 + + Returns: + 是否设置成功 + """ + try: + self.message_config.update(config) + print(f"✓ 消息配置已更新: {self.message_config}") + return True + except Exception as e: + print(f"✗ 消息配置设置失败: {e}") + return False + + def get_message_config(self) -> Dict[str, Any]: + """ + 获取消息配置 + + Returns: + 消息配置字典 + """ + return self.message_config.copy() + + def _trigger_message_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发消息回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.message_callbacks: + for callback in self.message_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 消息回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 消息回调触发失败: {e}") + + def register_message_callback(self, callback_type: str, callback: callable): + """ + 注册消息回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.message_callbacks: + self.message_callbacks[callback_type].append(callback) + print(f"✓ 消息回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 消息回调注册失败: {e}") + + def unregister_message_callback(self, callback_type: str, callback: callable): + """ + 注销消息回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.message_callbacks: + if callback in self.message_callbacks[callback_type]: + self.message_callbacks[callback_type].remove(callback) + print(f"✓ 消息回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 消息回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/server_architecture/monitoring/monitoring_system.py b/plugins/user/server_architecture/monitoring/monitoring_system.py index 909bf4a8..1b6c6095 100644 --- a/plugins/user/server_architecture/monitoring/monitoring_system.py +++ b/plugins/user/server_architecture/monitoring/monitoring_system.py @@ -1,823 +1,823 @@ -""" -监控和日志系统模块 -负责实时监控服务器性能和记录日志 -""" - -import time -import threading -import json -import os -from typing import Dict, Any, List, Optional -from collections import deque - -class MonitoringSystem: - """ - 监控系统 - 负责实时监控服务器性能和记录日志 - """ - - def __init__(self, plugin): - """ - 初始化监控系统 - - Args: - plugin: 服务器架构插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 监控配置 - self.monitoring_config = { - "enable_monitoring": True, - "monitoring_interval": 5.0, - "log_level": "INFO", - "enable_file_logging": True, - "log_file_path": "/home/hello/EG/plugins/user/server_architecture/logs/server_monitor.log", - "max_log_file_size": 10485760, # 10MB - "enable_performance_monitoring": True, - "enable_alerts": True, - "alert_thresholds": { - "cpu_usage": 80.0, - "memory_usage": 85.0, - "disk_usage": 90.0, - "network_latency": 100.0, - "error_rate": 5.0 - }, - "enable_metrics_collection": True, - "metrics_retention_period": 3600, # 1小时 - "enable_realtime_dashboard": True - } - - # 性能指标存储 - self.performance_metrics = { - "cpu_usage": deque(maxlen=1000), - "memory_usage": deque(maxlen=1000), - "disk_usage": deque(maxlen=1000), - "network_in": deque(maxlen=1000), - "network_out": deque(maxlen=1000), - "active_connections": deque(maxlen=1000), - "requests_per_second": deque(maxlen=1000), - "response_time": deque(maxlen=1000), - "error_rate": deque(maxlen=1000) - } - - # 监控状态 - self.monitoring_state = { - "last_monitoring_update": 0.0, - "last_performance_check": 0.0, - "total_logs": 0, - "total_alerts": 0, - "active_alerts": 0 - } - - # 警报系统 - self.active_alerts = {} - self.alert_history = deque(maxlen=1000) - - # 日志文件 - self.log_file = None - self.log_file_size = 0 - - # 监控统计 - self.monitoring_stats = { - "metrics_collected": 0, - "logs_written": 0, - "alerts_generated": 0, - "alerts_resolved": 0, - "performance_checks": 0, - "errors": 0 - } - - # 线程锁 - self.monitoring_lock = threading.RLock() - - # 回调函数 - self.monitoring_callbacks = { - "metric_collected": [], - "alert_triggered": [], - "alert_resolved": [], - "log_entry": [], - "performance_degraded": [] - } - - # 监控线程 - self.monitoring_thread = None - self.monitoring_thread_running = False - - # 时间戳记录 - self.last_log_write = 0.0 - self.last_metric_collection = 0.0 - - print("✓ 监控系统已创建") - - def initialize(self) -> bool: - """ - 初始化监控系统 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化监控系统...") - - # 创建日志目录 - log_dir = os.path.dirname(self.monitoring_config["log_file_path"]) - if not os.path.exists(log_dir): - os.makedirs(log_dir) - - # 初始化日志文件 - if self.monitoring_config["enable_file_logging"]: - self._initialize_log_file() - - # 启动监控线程 - self._start_monitoring_thread() - - self.initialized = True - print("✓ 监控系统初始化完成") - return True - - except Exception as e: - print(f"✗ 监控系统初始化失败: {e}") - self.monitoring_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def _initialize_log_file(self): - """初始化日志文件""" - try: - # 检查文件是否存在 - if os.path.exists(self.monitoring_config["log_file_path"]): - self.log_file_size = os.path.getsize(self.monitoring_config["log_file_path"]) - - # 打开日志文件 - self.log_file = open(self.monitoring_config["log_file_path"], "a", encoding="utf-8") - print(f"✓ 日志文件已初始化: {self.monitoring_config['log_file_path']}") - - except Exception as e: - print(f"✗ 日志文件初始化失败: {e}") - self.monitoring_stats["errors"] += 1 - - def _start_monitoring_thread(self): - """启动监控线程""" - try: - self.monitoring_thread_running = True - self.monitoring_thread = threading.Thread(target=self._monitoring_loop, daemon=True) - self.monitoring_thread.start() - print("✓ 监控线程已启动") - except Exception as e: - print(f"✗ 监控线程启动失败: {e}") - self.monitoring_stats["errors"] += 1 - - def _monitoring_loop(self): - """监控线程循环""" - try: - while self.monitoring_thread_running: - try: - if self.enabled and self.monitoring_config["enable_monitoring"]: - current_time = time.time() - - # 收集性能指标 - if current_time - self.last_metric_collection >= self.monitoring_config["monitoring_interval"]: - self._collect_performance_metrics() - self.last_metric_collection = current_time - - # 检查警报 - self._check_alerts() - - # 检查日志文件大小 - self._check_log_file_size() - - # 短暂休眠 - time.sleep(1.0) - - except Exception as e: - print(f"✗ 监控循环错误: {e}") - self.monitoring_stats["errors"] += 1 - time.sleep(5.0) # 出错时延长休眠 - - except Exception as e: - print(f"✗ 监控线程失败: {e}") - self.monitoring_stats["errors"] += 1 - - def enable(self) -> bool: - """ - 启用监控系统 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 监控系统未初始化") - return False - - self.enabled = True - print("✓ 监控系统已启用") - return True - - except Exception as e: - print(f"✗ 监控系统启用失败: {e}") - self.monitoring_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用监控系统""" - try: - self.enabled = False - - # 停止监控线程 - if self.monitoring_thread_running: - self.monitoring_thread_running = False - if self.monitoring_thread and self.monitoring_thread.is_alive(): - self.monitoring_thread.join(timeout=5.0) - - # 关闭日志文件 - if self.log_file: - self.log_file.close() - self.log_file = None - - print("✓ 监控系统已禁用") - - except Exception as e: - print(f"✗ 监控系统禁用失败: {e}") - self.monitoring_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理监控系统资源""" - try: - # 禁用监控系统 - if self.enabled: - self.disable() - - # 清理回调 - self.monitoring_callbacks.clear() - - self.initialized = False - print("✓ 监控系统资源已清理") - - except Exception as e: - print(f"✗ 监控系统资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新监控系统状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.monitoring_state["last_monitoring_update"] = current_time - - except Exception as e: - print(f"✗ 监控系统更新失败: {e}") - self.monitoring_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def _collect_performance_metrics(self): - """收集性能指标""" - try: - current_time = time.time() - metrics = {} - - # 收集系统资源使用情况 - try: - import psutil - - # CPU使用率 - cpu_percent = psutil.cpu_percent(interval=0.1) - metrics["cpu_usage"] = cpu_percent - self.performance_metrics["cpu_usage"].append((current_time, cpu_percent)) - - # 内存使用率 - memory_info = psutil.virtual_memory() - memory_percent = memory_info.percent - metrics["memory_usage"] = memory_percent - self.performance_metrics["memory_usage"].append((current_time, memory_percent)) - - # 磁盘使用率 - disk_info = psutil.disk_usage("/") - disk_percent = (disk_info.used / disk_info.total) * 100 - metrics["disk_usage"] = disk_percent - self.performance_metrics["disk_usage"].append((current_time, disk_percent)) - - # 网络使用情况 - net_io = psutil.net_io_counters() - metrics["network_in"] = net_io.bytes_recv - metrics["network_out"] = net_io.bytes_sent - self.performance_metrics["network_in"].append((current_time, net_io.bytes_recv)) - self.performance_metrics["network_out"].append((current_time, net_io.bytes_sent)) - - except ImportError: - # 如果没有psutil,使用简化的指标 - metrics["cpu_usage"] = 0.0 - metrics["memory_usage"] = 0.0 - metrics["disk_usage"] = 0.0 - metrics["network_in"] = 0 - metrics["network_out"] = 0 - - # 收集服务器指标 - if self.plugin.server_manager: - server_stats = self.plugin.server_manager.get_server_stats() - server_state = server_stats.get("state", {}) - - # 活跃连接数 - active_connections = server_state.get("current_connections", 0) - metrics["active_connections"] = active_connections - self.performance_metrics["active_connections"].append((current_time, active_connections)) - - # 数据传输量 - bytes_sent = server_state.get("bytes_sent", 0) - bytes_received = server_state.get("bytes_received", 0) - metrics["bytes_sent"] = bytes_sent - metrics["bytes_received"] = bytes_received - - # 收集客户端指标 - if self.plugin.client_manager: - client_stats = self.plugin.client_manager.get_client_stats() - client_count = client_stats.get("current_clients", 0) - metrics["client_count"] = client_count - - # 收集消息指标 - if self.plugin.message_router: - message_stats = self.plugin.message_router.get_message_stats() - message_state = message_stats.get("stats", {}) - - messages_received = message_state.get("messages_received", 0) - messages_sent = message_state.get("messages_sent", 0) - metrics["messages_received"] = messages_received - metrics["messages_sent"] = messages_sent - - # 计算每秒请求数 - if hasattr(self, '_last_messages_received'): - rps = (messages_received - self._last_messages_received) / self.monitoring_config["monitoring_interval"] - metrics["requests_per_second"] = max(0, rps) - self.performance_metrics["requests_per_second"].append((current_time, metrics["requests_per_second"])) - else: - metrics["requests_per_second"] = 0 - self.performance_metrics["requests_per_second"].append((current_time, 0)) - - self._last_messages_received = messages_received - - # 收集房间指标 - if self.plugin.room_manager: - room_stats = self.plugin.room_manager.get_room_stats() - active_rooms = room_stats.get("current_rooms", 0) - metrics["active_rooms"] = active_rooms - - # 更新统计 - self.monitoring_stats["metrics_collected"] += 1 - self.last_metric_collection = current_time - - # 触发指标收集回调 - self._trigger_monitoring_callback("metric_collected", { - "metrics": metrics, - "timestamp": current_time - }) - - # 记录日志 - if self._should_log("DEBUG"): - self._write_log("DEBUG", f"性能指标收集: {metrics}") - - except Exception as e: - print(f"✗ 性能指标收集失败: {e}") - self.monitoring_stats["errors"] += 1 - - def _check_alerts(self): - """检查警报条件""" - try: - if not self.monitoring_config["enable_alerts"]: - return - - current_time = time.time() - thresholds = self.monitoring_config["alert_thresholds"] - - # 检查CPU使用率 - if self.performance_metrics["cpu_usage"]: - latest_cpu = self.performance_metrics["cpu_usage"][-1][1] - if latest_cpu > thresholds["cpu_usage"]: - self._trigger_alert("high_cpu_usage", f"CPU使用率过高: {latest_cpu:.2f}%", current_time) - - # 检查内存使用率 - if self.performance_metrics["memory_usage"]: - latest_memory = self.performance_metrics["memory_usage"][-1][1] - if latest_memory > thresholds["memory_usage"]: - self._trigger_alert("high_memory_usage", f"内存使用率过高: {latest_memory:.2f}%", current_time) - - # 检查错误率 - if self.performance_metrics["error_rate"]: - latest_error_rate = self.performance_metrics["error_rate"][-1][1] - if latest_error_rate > thresholds["error_rate"]: - self._trigger_alert("high_error_rate", f"错误率过高: {latest_error_rate:.2f}%", current_time) - - # 检查活跃连接数 - if self.performance_metrics["active_connections"]: - latest_connections = self.performance_metrics["active_connections"][-1][1] - if self.plugin.server_manager: - max_connections = self.plugin.server_manager.server_config.get("max_connections", 1000) - if latest_connections > max_connections * 0.9: # 90%阈值 - self._trigger_alert("high_connections", f"连接数接近上限: {latest_connections}/{max_connections}", current_time) - - except Exception as e: - print(f"✗ 警报检查失败: {e}") - self.monitoring_stats["errors"] += 1 - - def _trigger_alert(self, alert_type: str, message: str, timestamp: float = None): - """ - 触发警报 - - Args: - alert_type: 警报类型 - message: 警报消息 - timestamp: 时间戳 - """ - try: - if timestamp is None: - timestamp = time.time() - - alert_id = f"{alert_type}_{int(timestamp)}" - - # 检查是否已存在相同类型的活动警报 - with self.monitoring_lock: - if alert_type in self.active_alerts: - # 更新现有警报 - self.active_alerts[alert_type]["count"] += 1 - self.active_alerts[alert_type]["last_triggered"] = timestamp - else: - # 创建新警报 - self.active_alerts[alert_type] = { - "id": alert_id, - "type": alert_type, - "message": message, - "first_triggered": timestamp, - "last_triggered": timestamp, - "count": 1, - "resolved": False - } - - self.monitoring_state["active_alerts"] += 1 - self.monitoring_stats["alerts_generated"] += 1 - - # 添加到警报历史 - self.alert_history.append({ - "id": alert_id, - "type": alert_type, - "message": message, - "timestamp": timestamp, - "resolved": False - }) - - # 触发警报回调 - self._trigger_monitoring_callback("alert_triggered", { - "alert_id": alert_id, - "alert_type": alert_type, - "message": message, - "timestamp": timestamp - }) - - # 记录日志 - self._write_log("WARNING", f"警报触发 [{alert_type}]: {message}") - - except Exception as e: - print(f"✗ 警报触发失败: {e}") - self.monitoring_stats["errors"] += 1 - - def resolve_alert(self, alert_type: str): - """ - 解决警报 - - Args: - alert_type: 警报类型 - """ - try: - with self.monitoring_lock: - if alert_type in self.active_alerts: - alert_data = self.active_alerts[alert_type] - alert_data["resolved"] = True - alert_data["resolved_time"] = time.time() - - self.monitoring_state["active_alerts"] -= 1 - self.monitoring_stats["alerts_resolved"] += 1 - - # 触发警报解决回调 - self._trigger_monitoring_callback("alert_resolved", { - "alert_type": alert_type, - "alert_data": alert_data - }) - - # 从活动警报中移除 - del self.active_alerts[alert_type] - - # 记录日志 - self._write_log("INFO", f"警报已解决: {alert_type}") - - except Exception as e: - print(f"✗ 警报解决失败: {e}") - self.monitoring_stats["errors"] += 1 - - def _check_log_file_size(self): - """检查日志文件大小""" - try: - if not self.monitoring_config["enable_file_logging"] or not self.log_file: - return - - # 检查文件大小 - if os.path.exists(self.monitoring_config["log_file_path"]): - current_size = os.path.getsize(self.monitoring_config["log_file_path"]) - if current_size > self.monitoring_config["max_log_file_size"]: - # 轮转日志文件 - self._rotate_log_file() - - except Exception as e: - print(f"✗ 日志文件大小检查失败: {e}") - self.monitoring_stats["errors"] += 1 - - def _rotate_log_file(self): - """轮转日志文件""" - try: - if self.log_file: - self.log_file.close() - - # 重命名当前日志文件 - timestamp = int(time.time()) - rotated_filename = f"{self.monitoring_config['log_file_path']}.{timestamp}" - os.rename(self.monitoring_config["log_file_path"], rotated_filename) - - # 重新初始化日志文件 - self._initialize_log_file() - - print(f"✓ 日志文件已轮转: {rotated_filename}") - - except Exception as e: - print(f"✗ 日志文件轮转失败: {e}") - self.monitoring_stats["errors"] += 1 - - def _write_log(self, level: str, message: str, extra_data: Dict[str, Any] = None): - """ - 写入日志 - - Args: - level: 日志级别 - message: 日志消息 - extra_data: 额外数据 - """ - try: - if not self._should_log(level): - return - - timestamp = time.time() - log_entry = { - "timestamp": timestamp, - "level": level, - "message": message, - "extra_data": extra_data - } - - # 控制台输出 - print(f"[{level}] {message}") - - # 文件日志 - if self.monitoring_config["enable_file_logging"] and self.log_file: - try: - log_line = json.dumps(log_entry, ensure_ascii=False) - self.log_file.write(log_line + "\n") - self.log_file.flush() - - self.log_file_size += len(log_line) + 1 - self.monitoring_stats["logs_written"] += 1 - self.monitoring_state["total_logs"] += 1 - - except Exception as e: - print(f"✗ 文件日志写入失败: {e}") - - # 触发日志条目回调 - self._trigger_monitoring_callback("log_entry", log_entry) - - self.last_log_write = timestamp - - except Exception as e: - print(f"✗ 日志写入失败: {e}") - self.monitoring_stats["errors"] += 1 - - def _should_log(self, level: str) -> bool: - """ - 检查是否应该记录指定级别的日志 - - Args: - level: 日志级别 - - Returns: - 是否应该记录日志 - """ - try: - log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] - current_level = self.monitoring_config["log_level"] - - if current_level not in log_levels: - return True # 默认记录所有日志 - - if level not in log_levels: - return True # 默认记录未知级别的日志 - - current_index = log_levels.index(current_level) - level_index = log_levels.index(level) - - # 只记录等于或高于当前级别的日志 - return level_index >= current_index - - except Exception as e: - print(f"✗ 日志级别检查失败: {e}") - return True - - def get_performance_metrics(self, metric_name: str = None, limit: int = 100) -> Any: - """ - 获取性能指标 - - Args: - metric_name: 指标名称(可选) - limit: 限制返回的数量 - - Returns: - 性能指标数据 - """ - try: - with self.monitoring_lock: - if metric_name: - if metric_name in self.performance_metrics: - metrics = list(self.performance_metrics[metric_name]) - return metrics[-limit:] if len(metrics) > limit else metrics - else: - return [] - else: - # 返回所有指标 - result = {} - for name, metrics in self.performance_metrics.items(): - metric_list = list(metrics) - result[name] = metric_list[-limit:] if len(metric_list) > limit else metric_list - return result - - except Exception as e: - print(f"✗ 获取性能指标失败: {e}") - self.monitoring_stats["errors"] += 1 - return {} - - def get_active_alerts(self) -> Dict[str, Any]: - """ - 获取活动警报 - - Returns: - 活动警报字典 - """ - try: - with self.monitoring_lock: - return self.active_alerts.copy() - except Exception as e: - print(f"✗ 获取活动警报失败: {e}") - self.monitoring_stats["errors"] += 1 - return {} - - def get_alert_history(self, limit: int = 100) -> List[Dict[str, Any]]: - """ - 获取警报历史 - - Args: - limit: 限制返回的数量 - - Returns: - 警报历史列表 - """ - try: - with self.monitoring_lock: - history = list(self.alert_history) - return history[-limit:] if len(history) > limit else history - except Exception as e: - print(f"✗ 获取警报历史失败: {e}") - self.monitoring_stats["errors"] += 1 - return [] - - def get_monitoring_stats(self) -> Dict[str, Any]: - """ - 获取监控统计信息 - - Returns: - 监控统计字典 - """ - return { - "state": self.monitoring_state.copy(), - "stats": self.monitoring_stats.copy(), - "config": self.monitoring_config.copy(), - "active_alerts_count": len(self.active_alerts), - "alert_history_count": len(self.alert_history) - } - - def reset_stats(self): - """重置监控统计信息""" - try: - self.monitoring_stats = { - "metrics_collected": 0, - "logs_written": 0, - "alerts_generated": 0, - "alerts_resolved": 0, - "performance_checks": 0, - "errors": 0 - } - - self.monitoring_state["total_logs"] = 0 - self.monitoring_state["total_alerts"] = 0 - self.monitoring_state["active_alerts"] = 0 - - print("✓ 监控统计信息已重置") - except Exception as e: - print(f"✗ 监控统计信息重置失败: {e}") - - def set_monitoring_config(self, config: Dict[str, Any]) -> bool: - """ - 设置监控配置 - - Args: - config: 监控配置字典 - - Returns: - 是否设置成功 - """ - try: - self.monitoring_config.update(config) - print(f"✓ 监控配置已更新: {self.monitoring_config}") - return True - except Exception as e: - print(f"✗ 监控配置设置失败: {e}") - return False - - def get_monitoring_config(self) -> Dict[str, Any]: - """ - 获取监控配置 - - Returns: - 监控配置字典 - """ - return self.monitoring_config.copy() - - def _trigger_monitoring_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发监控回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.monitoring_callbacks: - for callback in self.monitoring_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 监控回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 监控回调触发失败: {e}") - - def register_monitoring_callback(self, callback_type: str, callback: callable): - """ - 注册监控回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.monitoring_callbacks: - self.monitoring_callbacks[callback_type].append(callback) - print(f"✓ 监控回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 监控回调注册失败: {e}") - - def unregister_monitoring_callback(self, callback_type: str, callback: callable): - """ - 注销监控回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.monitoring_callbacks: - if callback in self.monitoring_callbacks[callback_type]: - self.monitoring_callbacks[callback_type].remove(callback) - print(f"✓ 监控回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +监控和日志系统模块 +负责实时监控服务器性能和记录日志 +""" + +import time +import threading +import json +import os +from typing import Dict, Any, List, Optional +from collections import deque + +class MonitoringSystem: + """ + 监控系统 + 负责实时监控服务器性能和记录日志 + """ + + def __init__(self, plugin): + """ + 初始化监控系统 + + Args: + plugin: 服务器架构插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 监控配置 + self.monitoring_config = { + "enable_monitoring": True, + "monitoring_interval": 5.0, + "log_level": "INFO", + "enable_file_logging": True, + "log_file_path": "/home/hello/EG/plugins/user/server_architecture/logs/server_monitor.log", + "max_log_file_size": 10485760, # 10MB + "enable_performance_monitoring": True, + "enable_alerts": True, + "alert_thresholds": { + "cpu_usage": 80.0, + "memory_usage": 85.0, + "disk_usage": 90.0, + "network_latency": 100.0, + "error_rate": 5.0 + }, + "enable_metrics_collection": True, + "metrics_retention_period": 3600, # 1小时 + "enable_realtime_dashboard": True + } + + # 性能指标存储 + self.performance_metrics = { + "cpu_usage": deque(maxlen=1000), + "memory_usage": deque(maxlen=1000), + "disk_usage": deque(maxlen=1000), + "network_in": deque(maxlen=1000), + "network_out": deque(maxlen=1000), + "active_connections": deque(maxlen=1000), + "requests_per_second": deque(maxlen=1000), + "response_time": deque(maxlen=1000), + "error_rate": deque(maxlen=1000) + } + + # 监控状态 + self.monitoring_state = { + "last_monitoring_update": 0.0, + "last_performance_check": 0.0, + "total_logs": 0, + "total_alerts": 0, + "active_alerts": 0 + } + + # 警报系统 + self.active_alerts = {} + self.alert_history = deque(maxlen=1000) + + # 日志文件 + self.log_file = None + self.log_file_size = 0 + + # 监控统计 + self.monitoring_stats = { + "metrics_collected": 0, + "logs_written": 0, + "alerts_generated": 0, + "alerts_resolved": 0, + "performance_checks": 0, + "errors": 0 + } + + # 线程锁 + self.monitoring_lock = threading.RLock() + + # 回调函数 + self.monitoring_callbacks = { + "metric_collected": [], + "alert_triggered": [], + "alert_resolved": [], + "log_entry": [], + "performance_degraded": [] + } + + # 监控线程 + self.monitoring_thread = None + self.monitoring_thread_running = False + + # 时间戳记录 + self.last_log_write = 0.0 + self.last_metric_collection = 0.0 + + print("✓ 监控系统已创建") + + def initialize(self) -> bool: + """ + 初始化监控系统 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化监控系统...") + + # 创建日志目录 + log_dir = os.path.dirname(self.monitoring_config["log_file_path"]) + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + # 初始化日志文件 + if self.monitoring_config["enable_file_logging"]: + self._initialize_log_file() + + # 启动监控线程 + self._start_monitoring_thread() + + self.initialized = True + print("✓ 监控系统初始化完成") + return True + + except Exception as e: + print(f"✗ 监控系统初始化失败: {e}") + self.monitoring_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def _initialize_log_file(self): + """初始化日志文件""" + try: + # 检查文件是否存在 + if os.path.exists(self.monitoring_config["log_file_path"]): + self.log_file_size = os.path.getsize(self.monitoring_config["log_file_path"]) + + # 打开日志文件 + self.log_file = open(self.monitoring_config["log_file_path"], "a", encoding="utf-8") + print(f"✓ 日志文件已初始化: {self.monitoring_config['log_file_path']}") + + except Exception as e: + print(f"✗ 日志文件初始化失败: {e}") + self.monitoring_stats["errors"] += 1 + + def _start_monitoring_thread(self): + """启动监控线程""" + try: + self.monitoring_thread_running = True + self.monitoring_thread = threading.Thread(target=self._monitoring_loop, daemon=True) + self.monitoring_thread.start() + print("✓ 监控线程已启动") + except Exception as e: + print(f"✗ 监控线程启动失败: {e}") + self.monitoring_stats["errors"] += 1 + + def _monitoring_loop(self): + """监控线程循环""" + try: + while self.monitoring_thread_running: + try: + if self.enabled and self.monitoring_config["enable_monitoring"]: + current_time = time.time() + + # 收集性能指标 + if current_time - self.last_metric_collection >= self.monitoring_config["monitoring_interval"]: + self._collect_performance_metrics() + self.last_metric_collection = current_time + + # 检查警报 + self._check_alerts() + + # 检查日志文件大小 + self._check_log_file_size() + + # 短暂休眠 + time.sleep(1.0) + + except Exception as e: + print(f"✗ 监控循环错误: {e}") + self.monitoring_stats["errors"] += 1 + time.sleep(5.0) # 出错时延长休眠 + + except Exception as e: + print(f"✗ 监控线程失败: {e}") + self.monitoring_stats["errors"] += 1 + + def enable(self) -> bool: + """ + 启用监控系统 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 监控系统未初始化") + return False + + self.enabled = True + print("✓ 监控系统已启用") + return True + + except Exception as e: + print(f"✗ 监控系统启用失败: {e}") + self.monitoring_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用监控系统""" + try: + self.enabled = False + + # 停止监控线程 + if self.monitoring_thread_running: + self.monitoring_thread_running = False + if self.monitoring_thread and self.monitoring_thread.is_alive(): + self.monitoring_thread.join(timeout=5.0) + + # 关闭日志文件 + if self.log_file: + self.log_file.close() + self.log_file = None + + print("✓ 监控系统已禁用") + + except Exception as e: + print(f"✗ 监控系统禁用失败: {e}") + self.monitoring_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理监控系统资源""" + try: + # 禁用监控系统 + if self.enabled: + self.disable() + + # 清理回调 + self.monitoring_callbacks.clear() + + self.initialized = False + print("✓ 监控系统资源已清理") + + except Exception as e: + print(f"✗ 监控系统资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新监控系统状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.monitoring_state["last_monitoring_update"] = current_time + + except Exception as e: + print(f"✗ 监控系统更新失败: {e}") + self.monitoring_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def _collect_performance_metrics(self): + """收集性能指标""" + try: + current_time = time.time() + metrics = {} + + # 收集系统资源使用情况 + try: + import psutil + + # CPU使用率 + cpu_percent = psutil.cpu_percent(interval=0.1) + metrics["cpu_usage"] = cpu_percent + self.performance_metrics["cpu_usage"].append((current_time, cpu_percent)) + + # 内存使用率 + memory_info = psutil.virtual_memory() + memory_percent = memory_info.percent + metrics["memory_usage"] = memory_percent + self.performance_metrics["memory_usage"].append((current_time, memory_percent)) + + # 磁盘使用率 + disk_info = psutil.disk_usage("/") + disk_percent = (disk_info.used / disk_info.total) * 100 + metrics["disk_usage"] = disk_percent + self.performance_metrics["disk_usage"].append((current_time, disk_percent)) + + # 网络使用情况 + net_io = psutil.net_io_counters() + metrics["network_in"] = net_io.bytes_recv + metrics["network_out"] = net_io.bytes_sent + self.performance_metrics["network_in"].append((current_time, net_io.bytes_recv)) + self.performance_metrics["network_out"].append((current_time, net_io.bytes_sent)) + + except ImportError: + # 如果没有psutil,使用简化的指标 + metrics["cpu_usage"] = 0.0 + metrics["memory_usage"] = 0.0 + metrics["disk_usage"] = 0.0 + metrics["network_in"] = 0 + metrics["network_out"] = 0 + + # 收集服务器指标 + if self.plugin.server_manager: + server_stats = self.plugin.server_manager.get_server_stats() + server_state = server_stats.get("state", {}) + + # 活跃连接数 + active_connections = server_state.get("current_connections", 0) + metrics["active_connections"] = active_connections + self.performance_metrics["active_connections"].append((current_time, active_connections)) + + # 数据传输量 + bytes_sent = server_state.get("bytes_sent", 0) + bytes_received = server_state.get("bytes_received", 0) + metrics["bytes_sent"] = bytes_sent + metrics["bytes_received"] = bytes_received + + # 收集客户端指标 + if self.plugin.client_manager: + client_stats = self.plugin.client_manager.get_client_stats() + client_count = client_stats.get("current_clients", 0) + metrics["client_count"] = client_count + + # 收集消息指标 + if self.plugin.message_router: + message_stats = self.plugin.message_router.get_message_stats() + message_state = message_stats.get("stats", {}) + + messages_received = message_state.get("messages_received", 0) + messages_sent = message_state.get("messages_sent", 0) + metrics["messages_received"] = messages_received + metrics["messages_sent"] = messages_sent + + # 计算每秒请求数 + if hasattr(self, '_last_messages_received'): + rps = (messages_received - self._last_messages_received) / self.monitoring_config["monitoring_interval"] + metrics["requests_per_second"] = max(0, rps) + self.performance_metrics["requests_per_second"].append((current_time, metrics["requests_per_second"])) + else: + metrics["requests_per_second"] = 0 + self.performance_metrics["requests_per_second"].append((current_time, 0)) + + self._last_messages_received = messages_received + + # 收集房间指标 + if self.plugin.room_manager: + room_stats = self.plugin.room_manager.get_room_stats() + active_rooms = room_stats.get("current_rooms", 0) + metrics["active_rooms"] = active_rooms + + # 更新统计 + self.monitoring_stats["metrics_collected"] += 1 + self.last_metric_collection = current_time + + # 触发指标收集回调 + self._trigger_monitoring_callback("metric_collected", { + "metrics": metrics, + "timestamp": current_time + }) + + # 记录日志 + if self._should_log("DEBUG"): + self._write_log("DEBUG", f"性能指标收集: {metrics}") + + except Exception as e: + print(f"✗ 性能指标收集失败: {e}") + self.monitoring_stats["errors"] += 1 + + def _check_alerts(self): + """检查警报条件""" + try: + if not self.monitoring_config["enable_alerts"]: + return + + current_time = time.time() + thresholds = self.monitoring_config["alert_thresholds"] + + # 检查CPU使用率 + if self.performance_metrics["cpu_usage"]: + latest_cpu = self.performance_metrics["cpu_usage"][-1][1] + if latest_cpu > thresholds["cpu_usage"]: + self._trigger_alert("high_cpu_usage", f"CPU使用率过高: {latest_cpu:.2f}%", current_time) + + # 检查内存使用率 + if self.performance_metrics["memory_usage"]: + latest_memory = self.performance_metrics["memory_usage"][-1][1] + if latest_memory > thresholds["memory_usage"]: + self._trigger_alert("high_memory_usage", f"内存使用率过高: {latest_memory:.2f}%", current_time) + + # 检查错误率 + if self.performance_metrics["error_rate"]: + latest_error_rate = self.performance_metrics["error_rate"][-1][1] + if latest_error_rate > thresholds["error_rate"]: + self._trigger_alert("high_error_rate", f"错误率过高: {latest_error_rate:.2f}%", current_time) + + # 检查活跃连接数 + if self.performance_metrics["active_connections"]: + latest_connections = self.performance_metrics["active_connections"][-1][1] + if self.plugin.server_manager: + max_connections = self.plugin.server_manager.server_config.get("max_connections", 1000) + if latest_connections > max_connections * 0.9: # 90%阈值 + self._trigger_alert("high_connections", f"连接数接近上限: {latest_connections}/{max_connections}", current_time) + + except Exception as e: + print(f"✗ 警报检查失败: {e}") + self.monitoring_stats["errors"] += 1 + + def _trigger_alert(self, alert_type: str, message: str, timestamp: float = None): + """ + 触发警报 + + Args: + alert_type: 警报类型 + message: 警报消息 + timestamp: 时间戳 + """ + try: + if timestamp is None: + timestamp = time.time() + + alert_id = f"{alert_type}_{int(timestamp)}" + + # 检查是否已存在相同类型的活动警报 + with self.monitoring_lock: + if alert_type in self.active_alerts: + # 更新现有警报 + self.active_alerts[alert_type]["count"] += 1 + self.active_alerts[alert_type]["last_triggered"] = timestamp + else: + # 创建新警报 + self.active_alerts[alert_type] = { + "id": alert_id, + "type": alert_type, + "message": message, + "first_triggered": timestamp, + "last_triggered": timestamp, + "count": 1, + "resolved": False + } + + self.monitoring_state["active_alerts"] += 1 + self.monitoring_stats["alerts_generated"] += 1 + + # 添加到警报历史 + self.alert_history.append({ + "id": alert_id, + "type": alert_type, + "message": message, + "timestamp": timestamp, + "resolved": False + }) + + # 触发警报回调 + self._trigger_monitoring_callback("alert_triggered", { + "alert_id": alert_id, + "alert_type": alert_type, + "message": message, + "timestamp": timestamp + }) + + # 记录日志 + self._write_log("WARNING", f"警报触发 [{alert_type}]: {message}") + + except Exception as e: + print(f"✗ 警报触发失败: {e}") + self.monitoring_stats["errors"] += 1 + + def resolve_alert(self, alert_type: str): + """ + 解决警报 + + Args: + alert_type: 警报类型 + """ + try: + with self.monitoring_lock: + if alert_type in self.active_alerts: + alert_data = self.active_alerts[alert_type] + alert_data["resolved"] = True + alert_data["resolved_time"] = time.time() + + self.monitoring_state["active_alerts"] -= 1 + self.monitoring_stats["alerts_resolved"] += 1 + + # 触发警报解决回调 + self._trigger_monitoring_callback("alert_resolved", { + "alert_type": alert_type, + "alert_data": alert_data + }) + + # 从活动警报中移除 + del self.active_alerts[alert_type] + + # 记录日志 + self._write_log("INFO", f"警报已解决: {alert_type}") + + except Exception as e: + print(f"✗ 警报解决失败: {e}") + self.monitoring_stats["errors"] += 1 + + def _check_log_file_size(self): + """检查日志文件大小""" + try: + if not self.monitoring_config["enable_file_logging"] or not self.log_file: + return + + # 检查文件大小 + if os.path.exists(self.monitoring_config["log_file_path"]): + current_size = os.path.getsize(self.monitoring_config["log_file_path"]) + if current_size > self.monitoring_config["max_log_file_size"]: + # 轮转日志文件 + self._rotate_log_file() + + except Exception as e: + print(f"✗ 日志文件大小检查失败: {e}") + self.monitoring_stats["errors"] += 1 + + def _rotate_log_file(self): + """轮转日志文件""" + try: + if self.log_file: + self.log_file.close() + + # 重命名当前日志文件 + timestamp = int(time.time()) + rotated_filename = f"{self.monitoring_config['log_file_path']}.{timestamp}" + os.rename(self.monitoring_config["log_file_path"], rotated_filename) + + # 重新初始化日志文件 + self._initialize_log_file() + + print(f"✓ 日志文件已轮转: {rotated_filename}") + + except Exception as e: + print(f"✗ 日志文件轮转失败: {e}") + self.monitoring_stats["errors"] += 1 + + def _write_log(self, level: str, message: str, extra_data: Dict[str, Any] = None): + """ + 写入日志 + + Args: + level: 日志级别 + message: 日志消息 + extra_data: 额外数据 + """ + try: + if not self._should_log(level): + return + + timestamp = time.time() + log_entry = { + "timestamp": timestamp, + "level": level, + "message": message, + "extra_data": extra_data + } + + # 控制台输出 + print(f"[{level}] {message}") + + # 文件日志 + if self.monitoring_config["enable_file_logging"] and self.log_file: + try: + log_line = json.dumps(log_entry, ensure_ascii=False) + self.log_file.write(log_line + "\n") + self.log_file.flush() + + self.log_file_size += len(log_line) + 1 + self.monitoring_stats["logs_written"] += 1 + self.monitoring_state["total_logs"] += 1 + + except Exception as e: + print(f"✗ 文件日志写入失败: {e}") + + # 触发日志条目回调 + self._trigger_monitoring_callback("log_entry", log_entry) + + self.last_log_write = timestamp + + except Exception as e: + print(f"✗ 日志写入失败: {e}") + self.monitoring_stats["errors"] += 1 + + def _should_log(self, level: str) -> bool: + """ + 检查是否应该记录指定级别的日志 + + Args: + level: 日志级别 + + Returns: + 是否应该记录日志 + """ + try: + log_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + current_level = self.monitoring_config["log_level"] + + if current_level not in log_levels: + return True # 默认记录所有日志 + + if level not in log_levels: + return True # 默认记录未知级别的日志 + + current_index = log_levels.index(current_level) + level_index = log_levels.index(level) + + # 只记录等于或高于当前级别的日志 + return level_index >= current_index + + except Exception as e: + print(f"✗ 日志级别检查失败: {e}") + return True + + def get_performance_metrics(self, metric_name: str = None, limit: int = 100) -> Any: + """ + 获取性能指标 + + Args: + metric_name: 指标名称(可选) + limit: 限制返回的数量 + + Returns: + 性能指标数据 + """ + try: + with self.monitoring_lock: + if metric_name: + if metric_name in self.performance_metrics: + metrics = list(self.performance_metrics[metric_name]) + return metrics[-limit:] if len(metrics) > limit else metrics + else: + return [] + else: + # 返回所有指标 + result = {} + for name, metrics in self.performance_metrics.items(): + metric_list = list(metrics) + result[name] = metric_list[-limit:] if len(metric_list) > limit else metric_list + return result + + except Exception as e: + print(f"✗ 获取性能指标失败: {e}") + self.monitoring_stats["errors"] += 1 + return {} + + def get_active_alerts(self) -> Dict[str, Any]: + """ + 获取活动警报 + + Returns: + 活动警报字典 + """ + try: + with self.monitoring_lock: + return self.active_alerts.copy() + except Exception as e: + print(f"✗ 获取活动警报失败: {e}") + self.monitoring_stats["errors"] += 1 + return {} + + def get_alert_history(self, limit: int = 100) -> List[Dict[str, Any]]: + """ + 获取警报历史 + + Args: + limit: 限制返回的数量 + + Returns: + 警报历史列表 + """ + try: + with self.monitoring_lock: + history = list(self.alert_history) + return history[-limit:] if len(history) > limit else history + except Exception as e: + print(f"✗ 获取警报历史失败: {e}") + self.monitoring_stats["errors"] += 1 + return [] + + def get_monitoring_stats(self) -> Dict[str, Any]: + """ + 获取监控统计信息 + + Returns: + 监控统计字典 + """ + return { + "state": self.monitoring_state.copy(), + "stats": self.monitoring_stats.copy(), + "config": self.monitoring_config.copy(), + "active_alerts_count": len(self.active_alerts), + "alert_history_count": len(self.alert_history) + } + + def reset_stats(self): + """重置监控统计信息""" + try: + self.monitoring_stats = { + "metrics_collected": 0, + "logs_written": 0, + "alerts_generated": 0, + "alerts_resolved": 0, + "performance_checks": 0, + "errors": 0 + } + + self.monitoring_state["total_logs"] = 0 + self.monitoring_state["total_alerts"] = 0 + self.monitoring_state["active_alerts"] = 0 + + print("✓ 监控统计信息已重置") + except Exception as e: + print(f"✗ 监控统计信息重置失败: {e}") + + def set_monitoring_config(self, config: Dict[str, Any]) -> bool: + """ + 设置监控配置 + + Args: + config: 监控配置字典 + + Returns: + 是否设置成功 + """ + try: + self.monitoring_config.update(config) + print(f"✓ 监控配置已更新: {self.monitoring_config}") + return True + except Exception as e: + print(f"✗ 监控配置设置失败: {e}") + return False + + def get_monitoring_config(self) -> Dict[str, Any]: + """ + 获取监控配置 + + Returns: + 监控配置字典 + """ + return self.monitoring_config.copy() + + def _trigger_monitoring_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发监控回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.monitoring_callbacks: + for callback in self.monitoring_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 监控回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 监控回调触发失败: {e}") + + def register_monitoring_callback(self, callback_type: str, callback: callable): + """ + 注册监控回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.monitoring_callbacks: + self.monitoring_callbacks[callback_type].append(callback) + print(f"✓ 监控回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 监控回调注册失败: {e}") + + def unregister_monitoring_callback(self, callback_type: str, callback: callable): + """ + 注销监控回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.monitoring_callbacks: + if callback in self.monitoring_callbacks[callback_type]: + self.monitoring_callbacks[callback_type].remove(callback) + print(f"✓ 监控回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 监控回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/server_architecture/plugin.py b/plugins/user/server_architecture/plugin.py index 47f1350d..980ba268 100644 --- a/plugins/user/server_architecture/plugin.py +++ b/plugins/user/server_architecture/plugin.py @@ -1,602 +1,602 @@ -""" -服务器架构插件 -为EG引擎提供完整的服务器架构支持 -""" - -import time -from typing import Dict, Any, List, Optional -import threading - -class ServerArchitecturePlugin: - """ - 服务器架构插件主类 - 整合所有服务器功能模块 - """ - - def __init__(self, engine): - """ - 初始化服务器架构插件 - - Args: - engine: EG引擎实例 - """ - self.engine = engine - self.enabled = False - self.initialized = False - - # 插件元数据 - self.plugin_info = { - "name": "Server Architecture Plugin", - "version": "1.0.0", - "author": "EG Development Team", - "description": "为EG引擎提供完整的服务器架构支持" - } - - # 核心模块 - self.server_manager = None - self.client_manager = None - self.message_router = None - self.room_manager = None - self.storage_manager = None - self.load_balancer = None - self.monitoring_system = None - self.config_manager = None - self.security_manager = None - self.api_interface = None - self.editor_interface = None - - # 插件状态 - self.plugin_state = { - "startup_time": 0.0, - "last_update": 0.0, - "total_clients": 0, - "total_rooms": 0, - "messages_processed": 0, - "bytes_transferred": 0 - } - - # 插件配置 - self.plugin_config = { - "enable_monitoring": True, - "enable_logging": True, - "max_clients": 1000, - "server_port": 8080, - "enable_ssl": False, - "log_level": "INFO" - } - - # 统计信息 - self.stats = { - "initialization_count": 0, - "enable_count": 0, - "disable_count": 0, - "error_count": 0 - } - - # 回调函数 - self.callbacks = { - "plugin_initialized": [], - "plugin_enabled": [], - "plugin_disabled": [], - "error_occurred": [] - } - - print("✓ 服务器架构插件已创建") - - def initialize(self) -> bool: - """ - 初始化插件 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化服务器架构插件...") - - # 初始化各个模块 - self._initialize_modules() - - self.initialized = True - self.stats["initialization_count"] += 1 - self.plugin_state["startup_time"] = time.time() - - # 触发初始化完成回调 - self._trigger_callback("plugin_initialized", { - "timestamp": self.plugin_state["startup_time"] - }) - - print("✓ 服务器架构插件初始化完成") - return True - - except Exception as e: - print(f"✗ 服务器架构插件初始化失败: {e}") - self.stats["error_count"] += 1 - import traceback - traceback.print_exc() - return False - - def _initialize_modules(self): - """初始化各个功能模块""" - try: - # 初始化配置管理器 - from .config.config_manager import ConfigManager - self.config_manager = ConfigManager(self) - self.config_manager.initialize() - - # 初始化安全管理系统 - from .security.security_manager import SecurityManager - self.security_manager = SecurityManager(self) - self.security_manager.initialize() - - # 初始化存储管理器 - from .storage.storage_manager import StorageManager - self.storage_manager = StorageManager(self) - self.storage_manager.initialize() - - # 初始化客户端管理器 - from .clients.client_manager import ClientManager - self.client_manager = ClientManager(self) - self.client_manager.initialize() - - # 初始化消息路由系统 - from .messaging.message_router import MessageRouter - self.message_router = MessageRouter(self) - self.message_router.initialize() - - # 初始化房间管理器 - from .rooms.room_manager import RoomManager - self.room_manager = RoomManager(self) - self.room_manager.initialize() - - # 初始化服务器管理器 - from .core.server_manager import ServerManager - self.server_manager = ServerManager(self) - self.server_manager.initialize() - - # 初始化负载均衡器 - from .loadbalancer.load_balancer import LoadBalancer - self.load_balancer = LoadBalancer(self) - self.load_balancer.initialize() - - # 初始化监控系统 - if self.plugin_config["enable_monitoring"]: - from .monitoring.monitoring_system import MonitoringSystem - self.monitoring_system = MonitoringSystem(self) - self.monitoring_system.initialize() - - # 初始化API接口 - from .api.api_interface import APIInterface - self.api_interface = APIInterface(self) - self.api_interface.initialize() - - # 初始化编辑器接口 - from .editor.editor_interface import EditorInterface - self.editor_interface = EditorInterface(self) - self.editor_interface.initialize() - - print("✓ 所有模块初始化完成") - - except Exception as e: - print(f"✗ 模块初始化失败: {e}") - raise - - def enable(self) -> bool: - """ - 启用插件 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 插件未初始化") - return False - - print("正在启用服务器架构插件...") - - # 启用各个模块 - self._enable_modules() - - self.enabled = True - self.stats["enable_count"] += 1 - - # 触发启用完成回调 - self._trigger_callback("plugin_enabled", { - "timestamp": time.time() - }) - - print("✓ 服务器架构插件已启用") - return True - - except Exception as e: - print(f"✗ 服务器架构插件启用失败: {e}") - self.stats["error_count"] += 1 - import traceback - traceback.print_exc() - return False - - def _enable_modules(self): - """启用各个功能模块""" - try: - # 按依赖顺序启用模块 - if self.config_manager: - self.config_manager.enable() - - if self.security_manager: - self.security_manager.enable() - - if self.storage_manager: - self.storage_manager.enable() - - if self.client_manager: - self.client_manager.enable() - - if self.message_router: - self.message_router.enable() - - if self.room_manager: - self.room_manager.enable() - - if self.load_balancer: - self.load_balancer.enable() - - if self.monitoring_system: - self.monitoring_system.enable() - - if self.api_interface: - self.api_interface.enable() - - if self.editor_interface: - self.editor_interface.enable() - - # 最后启用服务器管理器 - if self.server_manager: - self.server_manager.enable() - - print("✓ 所有模块已启用") - - except Exception as e: - print(f"✗ 模块启用失败: {e}") - raise - - def disable(self): - """禁用插件""" - try: - if not self.enabled: - return - - print("正在禁用服务器架构插件...") - - # 禁用各个模块 - self._disable_modules() - - self.enabled = False - self.stats["disable_count"] += 1 - - # 触发禁用完成回调 - self._trigger_callback("plugin_disabled", { - "timestamp": time.time() - }) - - print("✓ 服务器架构插件已禁用") - - except Exception as e: - print(f"✗ 服务器架构插件禁用失败: {e}") - self.stats["error_count"] += 1 - import traceback - traceback.print_exc() - - def _disable_modules(self): - """禁用各个功能模块""" - try: - # 按依赖顺序禁用模块(与启用顺序相反) - if self.server_manager: - self.server_manager.disable() - - if self.editor_interface: - self.editor_interface.disable() - - if self.api_interface: - self.api_interface.disable() - - if self.monitoring_system: - self.monitoring_system.disable() - - if self.load_balancer: - self.load_balancer.disable() - - if self.room_manager: - self.room_manager.disable() - - if self.message_router: - self.message_router.disable() - - if self.client_manager: - self.client_manager.disable() - - if self.storage_manager: - self.storage_manager.disable() - - if self.security_manager: - self.security_manager.disable() - - if self.config_manager: - self.config_manager.disable() - - print("✓ 所有模块已禁用") - - except Exception as e: - print(f"✗ 模块禁用失败: {e}") - raise - - def finalize(self): - """清理插件资源""" - try: - print("正在清理服务器架构插件资源...") - - # 禁用插件 - if self.enabled: - self.disable() - - # 清理各个模块 - self._finalize_modules() - - # 清理回调 - self.callbacks.clear() - - self.initialized = False - print("✓ 服务器架构插件资源已清理") - - except Exception as e: - print(f"✗ 服务器架构插件资源清理失败: {e}") - import traceback - traceback.print_exc() - - def _finalize_modules(self): - """清理各个功能模块""" - try: - # 按依赖顺序清理模块(与启用顺序相反) - modules = [ - self.server_manager, - self.editor_interface, - self.api_interface, - self.monitoring_system, - self.load_balancer, - self.room_manager, - self.message_router, - self.client_manager, - self.storage_manager, - self.security_manager, - self.config_manager - ] - - for module in modules: - if module and hasattr(module, 'finalize'): - try: - module.finalize() - except Exception as e: - print(f"✗ 模块清理失败: {module.__class__.__name__} - {e}") - - except Exception as e: - print(f"✗ 模块资源清理失败: {e}") - - def update(self, dt: float): - """ - 更新插件状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.plugin_state["last_update"] = current_time - - # 更新各个模块 - self._update_modules(dt) - - except Exception as e: - print(f"✗ 服务器架构插件更新失败: {e}") - self.stats["error_count"] += 1 - import traceback - traceback.print_exc() - - def _update_modules(self, dt: float): - """更新各个功能模块""" - try: - # 更新顺序考虑依赖关系 - if self.monitoring_system: - self.monitoring_system.update(dt) - - if self.server_manager: - self.server_manager.update(dt) - - if self.client_manager: - self.client_manager.update(dt) - - if self.message_router: - self.message_router.update(dt) - - if self.room_manager: - self.room_manager.update(dt) - - if self.load_balancer: - self.load_balancer.update(dt) - - if self.storage_manager: - self.storage_manager.update(dt) - - if self.security_manager: - self.security_manager.update(dt) - - if self.api_interface: - self.api_interface.update(dt) - - if self.editor_interface: - self.editor_interface.update(dt) - - except Exception as e: - print(f"✗ 模块更新失败: {e}") - raise - - def get_stats(self) -> Dict[str, Any]: - """ - 获取插件统计信息 - - Returns: - 统计信息字典 - """ - try: - # 收集各模块统计信息 - module_stats = {} - - if self.server_manager: - module_stats["server"] = self.server_manager.get_server_stats() - - if self.client_manager: - module_stats["clients"] = self.client_manager.get_client_stats() - - if self.message_router: - module_stats["messages"] = self.message_router.get_message_stats() - - if self.room_manager: - module_stats["rooms"] = self.room_manager.get_room_stats() - - if self.storage_manager: - module_stats["storage"] = self.storage_manager.get_storage_stats() - - if self.monitoring_system: - module_stats["monitoring"] = self.monitoring_system.get_monitoring_stats() - - # 合并插件统计 - stats = { - "plugin": self.stats, - "state": self.plugin_state, - "modules": module_stats - } - - return stats - - except Exception as e: - print(f"✗ 统计信息获取失败: {e}") - return {} - - def reset_stats(self): - """重置统计信息""" - try: - self.stats = { - "initialization_count": 0, - "enable_count": 0, - "disable_count": 0, - "error_count": 0 - } - - # 重置各模块统计 - modules = [ - self.server_manager, - self.client_manager, - self.message_router, - self.room_manager, - self.storage_manager, - self.monitoring_system - ] - - for module in modules: - if module and hasattr(module, 'reset_stats'): - module.reset_stats() - - print("✓ 统计信息已重置") - - except Exception as e: - print(f"✗ 统计信息重置失败: {e}") - - def _trigger_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发回调函数 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.callbacks: - for callback in self.callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 回调触发失败: {e}") - - def register_callback(self, callback_type: str, callback: callable): - """ - 注册回调函数 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.callbacks: - self.callbacks[callback_type].append(callback) - print(f"✓ 回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 回调注册失败: {e}") - - def unregister_callback(self, callback_type: str, callback: callable): - """ - 注销回调函数 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.callbacks: - if callback in self.callbacks[callback_type]: - self.callbacks[callback_type].remove(callback) - print(f"✓ 回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 回调注销失败: {e}") - - def get_info(self) -> Dict[str, Any]: - """ - 获取插件信息 - - Returns: - 插件信息字典 - """ - return self.plugin_info.copy() - - def get_config(self) -> Dict[str, Any]: - """ - 获取插件配置 - - Returns: - 插件配置字典 - """ - return self.plugin_config.copy() - - def set_config(self, config: Dict[str, Any]) -> bool: - """ - 设置插件配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.plugin_config.update(config) - print(f"✓ 插件配置已更新: {self.plugin_config}") - return True - except Exception as e: - print(f"✗ 插件配置设置失败: {e}") +""" +服务器架构插件 +为EG引擎提供完整的服务器架构支持 +""" + +import time +from typing import Dict, Any, List, Optional +import threading + +class ServerArchitecturePlugin: + """ + 服务器架构插件主类 + 整合所有服务器功能模块 + """ + + def __init__(self, engine): + """ + 初始化服务器架构插件 + + Args: + engine: EG引擎实例 + """ + self.engine = engine + self.enabled = False + self.initialized = False + + # 插件元数据 + self.plugin_info = { + "name": "Server Architecture Plugin", + "version": "1.0.0", + "author": "EG Development Team", + "description": "为EG引擎提供完整的服务器架构支持" + } + + # 核心模块 + self.server_manager = None + self.client_manager = None + self.message_router = None + self.room_manager = None + self.storage_manager = None + self.load_balancer = None + self.monitoring_system = None + self.config_manager = None + self.security_manager = None + self.api_interface = None + self.editor_interface = None + + # 插件状态 + self.plugin_state = { + "startup_time": 0.0, + "last_update": 0.0, + "total_clients": 0, + "total_rooms": 0, + "messages_processed": 0, + "bytes_transferred": 0 + } + + # 插件配置 + self.plugin_config = { + "enable_monitoring": True, + "enable_logging": True, + "max_clients": 1000, + "server_port": 8080, + "enable_ssl": False, + "log_level": "INFO" + } + + # 统计信息 + self.stats = { + "initialization_count": 0, + "enable_count": 0, + "disable_count": 0, + "error_count": 0 + } + + # 回调函数 + self.callbacks = { + "plugin_initialized": [], + "plugin_enabled": [], + "plugin_disabled": [], + "error_occurred": [] + } + + print("✓ 服务器架构插件已创建") + + def initialize(self) -> bool: + """ + 初始化插件 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化服务器架构插件...") + + # 初始化各个模块 + self._initialize_modules() + + self.initialized = True + self.stats["initialization_count"] += 1 + self.plugin_state["startup_time"] = time.time() + + # 触发初始化完成回调 + self._trigger_callback("plugin_initialized", { + "timestamp": self.plugin_state["startup_time"] + }) + + print("✓ 服务器架构插件初始化完成") + return True + + except Exception as e: + print(f"✗ 服务器架构插件初始化失败: {e}") + self.stats["error_count"] += 1 + import traceback + traceback.print_exc() + return False + + def _initialize_modules(self): + """初始化各个功能模块""" + try: + # 初始化配置管理器 + from .config.config_manager import ConfigManager + self.config_manager = ConfigManager(self) + self.config_manager.initialize() + + # 初始化安全管理系统 + from .security.security_manager import SecurityManager + self.security_manager = SecurityManager(self) + self.security_manager.initialize() + + # 初始化存储管理器 + from .storage.storage_manager import StorageManager + self.storage_manager = StorageManager(self) + self.storage_manager.initialize() + + # 初始化客户端管理器 + from .clients.client_manager import ClientManager + self.client_manager = ClientManager(self) + self.client_manager.initialize() + + # 初始化消息路由系统 + from .messaging.message_router import MessageRouter + self.message_router = MessageRouter(self) + self.message_router.initialize() + + # 初始化房间管理器 + from .rooms.room_manager import RoomManager + self.room_manager = RoomManager(self) + self.room_manager.initialize() + + # 初始化服务器管理器 + from .core.server_manager import ServerManager + self.server_manager = ServerManager(self) + self.server_manager.initialize() + + # 初始化负载均衡器 + from .loadbalancer.load_balancer import LoadBalancer + self.load_balancer = LoadBalancer(self) + self.load_balancer.initialize() + + # 初始化监控系统 + if self.plugin_config["enable_monitoring"]: + from .monitoring.monitoring_system import MonitoringSystem + self.monitoring_system = MonitoringSystem(self) + self.monitoring_system.initialize() + + # 初始化API接口 + from .api.api_interface import APIInterface + self.api_interface = APIInterface(self) + self.api_interface.initialize() + + # 初始化编辑器接口 + from .editor.editor_interface import EditorInterface + self.editor_interface = EditorInterface(self) + self.editor_interface.initialize() + + print("✓ 所有模块初始化完成") + + except Exception as e: + print(f"✗ 模块初始化失败: {e}") + raise + + def enable(self) -> bool: + """ + 启用插件 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 插件未初始化") + return False + + print("正在启用服务器架构插件...") + + # 启用各个模块 + self._enable_modules() + + self.enabled = True + self.stats["enable_count"] += 1 + + # 触发启用完成回调 + self._trigger_callback("plugin_enabled", { + "timestamp": time.time() + }) + + print("✓ 服务器架构插件已启用") + return True + + except Exception as e: + print(f"✗ 服务器架构插件启用失败: {e}") + self.stats["error_count"] += 1 + import traceback + traceback.print_exc() + return False + + def _enable_modules(self): + """启用各个功能模块""" + try: + # 按依赖顺序启用模块 + if self.config_manager: + self.config_manager.enable() + + if self.security_manager: + self.security_manager.enable() + + if self.storage_manager: + self.storage_manager.enable() + + if self.client_manager: + self.client_manager.enable() + + if self.message_router: + self.message_router.enable() + + if self.room_manager: + self.room_manager.enable() + + if self.load_balancer: + self.load_balancer.enable() + + if self.monitoring_system: + self.monitoring_system.enable() + + if self.api_interface: + self.api_interface.enable() + + if self.editor_interface: + self.editor_interface.enable() + + # 最后启用服务器管理器 + if self.server_manager: + self.server_manager.enable() + + print("✓ 所有模块已启用") + + except Exception as e: + print(f"✗ 模块启用失败: {e}") + raise + + def disable(self): + """禁用插件""" + try: + if not self.enabled: + return + + print("正在禁用服务器架构插件...") + + # 禁用各个模块 + self._disable_modules() + + self.enabled = False + self.stats["disable_count"] += 1 + + # 触发禁用完成回调 + self._trigger_callback("plugin_disabled", { + "timestamp": time.time() + }) + + print("✓ 服务器架构插件已禁用") + + except Exception as e: + print(f"✗ 服务器架构插件禁用失败: {e}") + self.stats["error_count"] += 1 + import traceback + traceback.print_exc() + + def _disable_modules(self): + """禁用各个功能模块""" + try: + # 按依赖顺序禁用模块(与启用顺序相反) + if self.server_manager: + self.server_manager.disable() + + if self.editor_interface: + self.editor_interface.disable() + + if self.api_interface: + self.api_interface.disable() + + if self.monitoring_system: + self.monitoring_system.disable() + + if self.load_balancer: + self.load_balancer.disable() + + if self.room_manager: + self.room_manager.disable() + + if self.message_router: + self.message_router.disable() + + if self.client_manager: + self.client_manager.disable() + + if self.storage_manager: + self.storage_manager.disable() + + if self.security_manager: + self.security_manager.disable() + + if self.config_manager: + self.config_manager.disable() + + print("✓ 所有模块已禁用") + + except Exception as e: + print(f"✗ 模块禁用失败: {e}") + raise + + def finalize(self): + """清理插件资源""" + try: + print("正在清理服务器架构插件资源...") + + # 禁用插件 + if self.enabled: + self.disable() + + # 清理各个模块 + self._finalize_modules() + + # 清理回调 + self.callbacks.clear() + + self.initialized = False + print("✓ 服务器架构插件资源已清理") + + except Exception as e: + print(f"✗ 服务器架构插件资源清理失败: {e}") + import traceback + traceback.print_exc() + + def _finalize_modules(self): + """清理各个功能模块""" + try: + # 按依赖顺序清理模块(与启用顺序相反) + modules = [ + self.server_manager, + self.editor_interface, + self.api_interface, + self.monitoring_system, + self.load_balancer, + self.room_manager, + self.message_router, + self.client_manager, + self.storage_manager, + self.security_manager, + self.config_manager + ] + + for module in modules: + if module and hasattr(module, 'finalize'): + try: + module.finalize() + except Exception as e: + print(f"✗ 模块清理失败: {module.__class__.__name__} - {e}") + + except Exception as e: + print(f"✗ 模块资源清理失败: {e}") + + def update(self, dt: float): + """ + 更新插件状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.plugin_state["last_update"] = current_time + + # 更新各个模块 + self._update_modules(dt) + + except Exception as e: + print(f"✗ 服务器架构插件更新失败: {e}") + self.stats["error_count"] += 1 + import traceback + traceback.print_exc() + + def _update_modules(self, dt: float): + """更新各个功能模块""" + try: + # 更新顺序考虑依赖关系 + if self.monitoring_system: + self.monitoring_system.update(dt) + + if self.server_manager: + self.server_manager.update(dt) + + if self.client_manager: + self.client_manager.update(dt) + + if self.message_router: + self.message_router.update(dt) + + if self.room_manager: + self.room_manager.update(dt) + + if self.load_balancer: + self.load_balancer.update(dt) + + if self.storage_manager: + self.storage_manager.update(dt) + + if self.security_manager: + self.security_manager.update(dt) + + if self.api_interface: + self.api_interface.update(dt) + + if self.editor_interface: + self.editor_interface.update(dt) + + except Exception as e: + print(f"✗ 模块更新失败: {e}") + raise + + def get_stats(self) -> Dict[str, Any]: + """ + 获取插件统计信息 + + Returns: + 统计信息字典 + """ + try: + # 收集各模块统计信息 + module_stats = {} + + if self.server_manager: + module_stats["server"] = self.server_manager.get_server_stats() + + if self.client_manager: + module_stats["clients"] = self.client_manager.get_client_stats() + + if self.message_router: + module_stats["messages"] = self.message_router.get_message_stats() + + if self.room_manager: + module_stats["rooms"] = self.room_manager.get_room_stats() + + if self.storage_manager: + module_stats["storage"] = self.storage_manager.get_storage_stats() + + if self.monitoring_system: + module_stats["monitoring"] = self.monitoring_system.get_monitoring_stats() + + # 合并插件统计 + stats = { + "plugin": self.stats, + "state": self.plugin_state, + "modules": module_stats + } + + return stats + + except Exception as e: + print(f"✗ 统计信息获取失败: {e}") + return {} + + def reset_stats(self): + """重置统计信息""" + try: + self.stats = { + "initialization_count": 0, + "enable_count": 0, + "disable_count": 0, + "error_count": 0 + } + + # 重置各模块统计 + modules = [ + self.server_manager, + self.client_manager, + self.message_router, + self.room_manager, + self.storage_manager, + self.monitoring_system + ] + + for module in modules: + if module and hasattr(module, 'reset_stats'): + module.reset_stats() + + print("✓ 统计信息已重置") + + except Exception as e: + print(f"✗ 统计信息重置失败: {e}") + + def _trigger_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发回调函数 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.callbacks: + for callback in self.callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 回调触发失败: {e}") + + def register_callback(self, callback_type: str, callback: callable): + """ + 注册回调函数 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.callbacks: + self.callbacks[callback_type].append(callback) + print(f"✓ 回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 回调注册失败: {e}") + + def unregister_callback(self, callback_type: str, callback: callable): + """ + 注销回调函数 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.callbacks: + if callback in self.callbacks[callback_type]: + self.callbacks[callback_type].remove(callback) + print(f"✓ 回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 回调注销失败: {e}") + + def get_info(self) -> Dict[str, Any]: + """ + 获取插件信息 + + Returns: + 插件信息字典 + """ + return self.plugin_info.copy() + + def get_config(self) -> Dict[str, Any]: + """ + 获取插件配置 + + Returns: + 插件配置字典 + """ + return self.plugin_config.copy() + + def set_config(self, config: Dict[str, Any]) -> bool: + """ + 设置插件配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.plugin_config.update(config) + print(f"✓ 插件配置已更新: {self.plugin_config}") + return True + except Exception as e: + print(f"✗ 插件配置设置失败: {e}") return False \ No newline at end of file diff --git a/plugins/user/server_architecture/rooms/room_manager.py b/plugins/user/server_architecture/rooms/room_manager.py index 168cc97d..d379cbf7 100644 --- a/plugins/user/server_architecture/rooms/room_manager.py +++ b/plugins/user/server_architecture/rooms/room_manager.py @@ -1,879 +1,879 @@ -""" -房间管理器模块 -负责管理多个游戏房间或场景实例 -""" - -import time -import threading -import uuid -from typing import Dict, Any, List, Optional - -class RoomManager: - """ - 房间管理器 - 负责管理多个游戏房间或场景实例 - """ - - def __init__(self, plugin): - """ - 初始化房间管理器 - - Args: - plugin: 服务器架构插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 房间配置 - self.room_config = { - "max_rooms": 100, - "max_clients_per_room": 50, - "enable_room_persistence": False, - "room_timeout": 3600, # 1小时无活动房间自动关闭 - "enable_room_passwords": True, - "enable_room_visibility": True, - "default_room_settings": { - "max_clients": 10, - "is_public": True, - "allow_spectators": True, - "enable_voice_chat": True - } - } - - # 房间存储 - self.rooms = {} # 房间字典 - self.client_rooms = {} # 客户端所在房间映射 - - # 房间统计 - self.room_stats = { - "total_rooms": 0, - "active_rooms": 0, - "clients_in_rooms": 0, - "rooms_created": 0, - "rooms_destroyed": 0, - "clients_joined": 0, - "clients_left": 0, - "errors": 0 - } - - # 线程锁 - self.rooms_lock = threading.RLock() - - # 回调函数 - self.room_callbacks = { - "room_created": [], - "room_destroyed": [], - "client_joined": [], - "client_left": [], - "room_updated": [] - } - - # 时间戳记录 - self.last_cleanup = 0.0 - self.last_room_check = 0.0 - - print("✓ 房间管理器已创建") - - def initialize(self) -> bool: - """ - 初始化房间管理器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化房间管理器...") - - self.initialized = True - print("✓ 房间管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 房间管理器初始化失败: {e}") - self.room_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用房间管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 房间管理器未初始化") - return False - - self.enabled = True - print("✓ 房间管理器已启用") - return True - - except Exception as e: - print(f"✗ 房间管理器启用失败: {e}") - self.room_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用房间管理器""" - try: - self.enabled = False - - # 关闭所有房间 - self.destroy_all_rooms() - - print("✓ 房间管理器已禁用") - - except Exception as e: - print(f"✗ 房间管理器禁用失败: {e}") - self.room_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理房间管理器资源""" - try: - # 禁用管理器 - if self.enabled: - self.disable() - - # 清理回调 - self.room_callbacks.clear() - - self.initialized = False - print("✓ 房间管理器资源已清理") - - except Exception as e: - print(f"✗ 房间管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新房间管理器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 定期清理过期房间 - if current_time - self.last_cleanup > 60.0: # 每分钟清理一次 - self._cleanup_expired_rooms(current_time) - self.last_cleanup = current_time - - except Exception as e: - print(f"✗ 房间管理器更新失败: {e}") - self.room_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def _cleanup_expired_rooms(self, current_time: float): - """清理过期房间""" - try: - expired_rooms = [] - with self.rooms_lock: - for room_id, room_data in self.rooms.items(): - # 检查房间是否超时 - if current_time - room_data["last_activity"] > self.room_config["room_timeout"]: - # 检查房间是否为空 - if len(room_data["clients"]) == 0: - expired_rooms.append(room_id) - - # 销毁过期房间 - for room_id in expired_rooms: - self.destroy_room(room_id) - - except Exception as e: - print(f"✗ 过期房间清理失败: {e}") - self.room_stats["errors"] += 1 - - def create_room(self, room_name: str = None, room_settings: Dict[str, Any] = None, - room_password: str = None) -> Optional[str]: - """ - 创建房间 - - Args: - room_name: 房间名称 - room_settings: 房间设置 - room_password: 房间密码 - - Returns: - 房间ID或None - """ - try: - if not self.enabled: - return None - - # 检查房间数量限制 - with self.rooms_lock: - if len(self.rooms) >= self.room_config["max_rooms"]: - print("✗ 房间数量已达上限") - return None - - # 生成房间ID - room_id = str(uuid.uuid4()) - - # 使用默认设置或传入设置 - settings = self.room_config["default_room_settings"].copy() - if room_settings: - settings.update(room_settings) - - # 创建房间数据 - room_data = { - "room_id": room_id, - "room_name": room_name or f"房间-{room_id[:8]}", - "settings": settings, - "password": room_password, - "clients": {}, # 客户端ID到客户端数据的映射 - "host_client_id": None, - "created_time": time.time(), - "last_activity": time.time(), - "room_state": {}, # 房间状态数据 - "custom_data": {} # 自定义房间数据 - } - - # 添加到房间列表 - with self.rooms_lock: - self.rooms[room_id] = room_data - - self.room_stats["total_rooms"] += 1 - self.room_stats["active_rooms"] += 1 - self.room_stats["rooms_created"] += 1 - - # 触发房间创建回调 - self._trigger_room_callback("room_created", { - "room_id": room_id, - "room_name": room_data["room_name"], - "settings": settings, - "timestamp": room_data["created_time"] - }) - - print(f"✓ 房间已创建: {room_id} ({room_data['room_name']})") - return room_id - - except Exception as e: - print(f"✗ 房间创建失败: {e}") - self.room_stats["errors"] += 1 - return None - - def destroy_room(self, room_id: str) -> bool: - """ - 销毁房间 - - Args: - room_id: 房间ID - - Returns: - 是否销毁成功 - """ - try: - if not self.enabled: - return False - - # 检查房间是否存在 - with self.rooms_lock: - if room_id not in self.rooms: - print(f"✗ 房间不存在: {room_id}") - return False - - room_data = self.rooms[room_id] - - # 通知房间内所有客户端 - for client_id in list(room_data["clients"].keys()): - self._notify_client_room_destroyed(client_id, room_id) - - # 移除房间 - del self.rooms[room_id] - - self.room_stats["active_rooms"] -= 1 - self.room_stats["rooms_destroyed"] += 1 - - # 触发房间销毁回调 - self._trigger_room_callback("room_destroyed", { - "room_id": room_id, - "room_name": room_data["room_name"], - "timestamp": time.time() - }) - - print(f"✓ 房间已销毁: {room_id}") - return True - - except Exception as e: - print(f"✗ 房间销毁失败: {e}") - self.room_stats["errors"] += 1 - return False - - def destroy_all_rooms(self): - """销毁所有房间""" - try: - with self.rooms_lock: - room_ids = list(self.rooms.keys()) - - for room_id in room_ids: - self.destroy_room(room_id) - - except Exception as e: - print(f"✗ 销毁所有房间失败: {e}") - self.room_stats["errors"] += 1 - - def add_client_to_room(self, room_id: str, client_id: str, client_data: Dict[str, Any] = None) -> bool: - """ - 添加客户端到房间 - - Args: - room_id: 房间ID - client_id: 客户端ID - client_data: 客户端数据 - - Returns: - 是否添加成功 - """ - try: - if not self.enabled: - return False - - # 检查客户端是否已认证 - if not self.plugin.client_manager.is_client_authenticated(client_id): - print(f"✗ 客户端未认证: {client_id}") - return False - - # 检查房间是否存在 - with self.rooms_lock: - if room_id not in self.rooms: - print(f"✗ 房间不存在: {room_id}") - return False - - room_data = self.rooms[room_id] - - # 检查房间是否已满 - if len(room_data["clients"]) >= room_data["settings"]["max_clients"]: - print(f"✗ 房间已满: {room_id}") - return False - - # 检查房间密码 - if room_data["password"] and not self._verify_room_password(room_id, client_data): - print(f"✗ 房间密码错误: {room_id}") - return False - - # 添加客户端到房间 - client_info = client_data or {} - client_info["join_time"] = time.time() - client_info["last_activity"] = time.time() - - room_data["clients"][client_id] = client_info - room_data["last_activity"] = time.time() - - # 设置房间主机(如果是第一个客户端) - if room_data["host_client_id"] is None: - room_data["host_client_id"] = client_id - - # 更新客户端房间映射 - self.client_rooms[client_id] = room_id - - self.room_stats["clients_in_rooms"] += 1 - self.room_stats["clients_joined"] += 1 - - # 触发客户端加入回调 - self._trigger_room_callback("client_joined", { - "room_id": room_id, - "room_name": room_data["room_name"], - "client_id": client_id, - "timestamp": time.time() - }) - - print(f"✓ 客户端已加入房间: {client_id} -> {room_id}") - return True - - except Exception as e: - print(f"✗ 添加客户端到房间失败: {e}") - self.room_stats["errors"] += 1 - return False - - def remove_client_from_room(self, room_id: str, client_id: str) -> bool: - """ - 从房间移除客户端 - - Args: - room_id: 房间ID - client_id: 客户端ID - - Returns: - 是否移除成功 - """ - try: - if not self.enabled: - return False - - # 检查房间是否存在 - with self.rooms_lock: - if room_id not in self.rooms: - print(f"✗ 房间不存在: {room_id}") - return False - - room_data = self.rooms[room_id] - - # 检查客户端是否在房间中 - if client_id not in room_data["clients"]: - print(f"✗ 客户端不在房间中: {client_id}") - return False - - # 移除客户端 - del room_data["clients"][client_id] - room_data["last_activity"] = time.time() - - # 更新主机(如果移除的是主机) - if room_data["host_client_id"] == client_id: - if room_data["clients"]: - # 选择第一个客户端作为新主机 - room_data["host_client_id"] = next(iter(room_data["clients"])) - else: - room_data["host_client_id"] = None - - # 移除客户端房间映射 - if client_id in self.client_rooms: - del self.client_rooms[client_id] - - self.room_stats["clients_in_rooms"] -= 1 - self.room_stats["clients_left"] += 1 - - # 触发客户端离开回调 - self._trigger_room_callback("client_left", { - "room_id": room_id, - "room_name": room_data["room_name"], - "client_id": client_id, - "timestamp": time.time() - }) - - # 如果房间为空,销毁房间 - if len(room_data["clients"]) == 0: - self.destroy_room(room_id) - - print(f"✓ 客户端已离开房间: {client_id} <- {room_id}") - return True - - except Exception as e: - print(f"✗ 从房间移除客户端失败: {e}") - self.room_stats["errors"] += 1 - return False - - def _verify_room_password(self, room_id: str, client_data: Dict[str, Any]) -> bool: - """ - 验证房间密码 - - Args: - room_id: 房间ID - client_data: 客户端数据 - - Returns: - 密码是否正确 - """ - try: - if not self.room_config["enable_room_passwords"]: - return True - - with self.rooms_lock: - if room_id not in self.rooms: - return False - - room_data = self.rooms[room_id] - if not room_data["password"]: - return True - - client_password = client_data.get("password") if client_data else None - return client_password == room_data["password"] - - except Exception as e: - print(f"✗ 房间密码验证失败: {e}") - self.room_stats["errors"] += 1 - return False - - def _notify_client_room_destroyed(self, client_id: str, room_id: str): - """ - 通知客户端房间被销毁 - - Args: - client_id: 客户端ID - room_id: 房间ID - """ - try: - # 从客户端房间映射中移除 - if client_id in self.client_rooms: - del self.client_rooms[client_id] - - # 发送房间销毁通知 - if self.plugin.message_router: - destroy_message = { - "type": "room_destroyed", - "room_id": room_id, - "timestamp": time.time() - } - - client_data = self.plugin.client_manager.get_client(client_id) - if client_data and "socket" in client_data: - self.plugin.message_router.send_message( - destroy_message, - client_data["socket"], - client_id - ) - - except Exception as e: - print(f"✗ 通知客户端房间销毁失败: {e}") - self.room_stats["errors"] += 1 - - def get_room_info(self, room_id: str) -> Optional[Dict[str, Any]]: - """ - 获取房间信息 - - Args: - room_id: 房间ID - - Returns: - 房间信息或None - """ - try: - with self.rooms_lock: - if room_id in self.rooms: - room_data = self.rooms[room_id] - # 返回不包含敏感信息的房间信息 - safe_room_data = { - "room_id": room_data["room_id"], - "room_name": room_data["room_name"], - "settings": room_data["settings"], - "client_count": len(room_data["clients"]), - "has_password": bool(room_data["password"]), - "host_client_id": room_data["host_client_id"], - "created_time": room_data["created_time"], - "last_activity": room_data["last_activity"] - } - return safe_room_data - else: - return None - - except Exception as e: - print(f"✗ 获取房间信息失败: {e}") - self.room_stats["errors"] += 1 - return None - - def get_all_rooms(self) -> List[Dict[str, Any]]: - """ - 获取所有房间信息 - - Returns: - 房间信息列表 - """ - try: - rooms_info = [] - with self.rooms_lock: - for room_id, room_data in self.rooms.items(): - # 只返回公开房间或用户所在的房间 - if room_data["settings"].get("is_public", True): - safe_room_data = { - "room_id": room_data["room_id"], - "room_name": room_data["room_name"], - "settings": room_data["settings"], - "client_count": len(room_data["clients"]), - "has_password": bool(room_data["password"]), - "host_client_id": room_data["host_client_id"], - "created_time": room_data["created_time"], - "last_activity": room_data["last_activity"] - } - rooms_info.append(safe_room_data) - - return rooms_info - - except Exception as e: - print(f"✗ 获取所有房间信息失败: {e}") - self.room_stats["errors"] += 1 - return [] - - def get_client_room(self, client_id: str) -> Optional[str]: - """ - 获取客户端所在房间 - - Args: - client_id: 客户端ID - - Returns: - 房间ID或None - """ - try: - return self.client_rooms.get(client_id) - except Exception as e: - print(f"✗ 获取客户端房间失败: {e}") - self.room_stats["errors"] += 1 - return None - - def get_room_clients(self, room_id: str) -> List[str]: - """ - 获取房间内所有客户端 - - Args: - room_id: 房间ID - - Returns: - 客户端ID列表 - """ - try: - with self.rooms_lock: - if room_id in self.rooms: - return list(self.rooms[room_id]["clients"].keys()) - else: - return [] - - except Exception as e: - print(f"✗ 获取房间客户端失败: {e}") - self.room_stats["errors"] += 1 - return [] - - def update_room_state(self, room_id: str, state_data: Dict[str, Any]) -> bool: - """ - 更新房间状态 - - Args: - room_id: 房间ID - state_data: 状态数据 - - Returns: - 是否更新成功 - """ - try: - with self.rooms_lock: - if room_id not in self.rooms: - return False - - self.rooms[room_id]["room_state"].update(state_data) - self.rooms[room_id]["last_activity"] = time.time() - - # 触发房间更新回调 - self._trigger_room_callback("room_updated", { - "room_id": room_id, - "state_data": state_data, - "timestamp": time.time() - }) - - return True - - except Exception as e: - print(f"✗ 更新房间状态失败: {e}") - self.room_stats["errors"] += 1 - return False - - def broadcast_to_room(self, room_id: str, message_data: Dict[str, Any], - exclude_client_id: str = None) -> bool: - """ - 向房间内所有客户端广播消息 - - Args: - room_id: 房间ID - message_data: 消息数据 - exclude_client_id: 要排除的客户端ID - - Returns: - 是否广播成功 - """ - try: - if not self.enabled: - return False - - with self.rooms_lock: - if room_id not in self.rooms: - return False - - room_data = self.rooms[room_id] - success_count = 0 - - # 向房间内每个客户端发送消息 - for client_id in room_data["clients"]: - # 跳过排除的客户端 - if client_id == exclude_client_id: - continue - - # 获取客户端数据 - client_data = self.plugin.client_manager.get_client(client_id) - if client_data and "socket" in client_data: - # 发送消息 - if self.plugin.message_router: - send_success = self.plugin.message_router.send_message( - message_data, - client_data["socket"], - client_id - ) - if send_success: - success_count += 1 - - return success_count > 0 - - except Exception as e: - print(f"✗ 房间广播失败: {e}") - self.room_stats["errors"] += 1 - return False - - def set_room_custom_data(self, room_id: str, key: str, value: Any) -> bool: - """ - 设置房间自定义数据 - - Args: - room_id: 房间ID - key: 数据键 - value: 数据值 - - Returns: - 是否设置成功 - """ - try: - with self.rooms_lock: - if room_id not in self.rooms: - return False - - self.rooms[room_id]["custom_data"][key] = value - return True - - except Exception as e: - print(f"✗ 设置房间自定义数据失败: {e}") - self.room_stats["errors"] += 1 - return False - - def get_room_custom_data(self, room_id: str, key: str, default: Any = None) -> Any: - """ - 获取房间自定义数据 - - Args: - room_id: 房间ID - key: 数据键 - default: 默认值 - - Returns: - 数据值或默认值 - """ - try: - with self.rooms_lock: - if room_id not in self.rooms: - return default - - return self.rooms[room_id]["custom_data"].get(key, default) - - except Exception as e: - print(f"✗ 获取房间自定义数据失败: {e}") - self.room_stats["errors"] += 1 - return default - - def get_room_stats(self) -> Dict[str, Any]: - """ - 获取房间统计信息 - - Returns: - 房间统计字典 - """ - return { - "stats": self.room_stats.copy(), - "config": self.room_config.copy(), - "current_rooms": len(self.rooms), - "current_clients_in_rooms": len(self.client_rooms) - } - - def reset_stats(self): - """重置房间统计信息""" - try: - self.room_stats = { - "total_rooms": 0, - "active_rooms": 0, - "clients_in_rooms": 0, - "rooms_created": 0, - "rooms_destroyed": 0, - "clients_joined": 0, - "clients_left": 0, - "errors": 0 - } - print("✓ 房间统计信息已重置") - except Exception as e: - print(f"✗ 房间统计信息重置失败: {e}") - - def set_room_config(self, config: Dict[str, Any]) -> bool: - """ - 设置房间配置 - - Args: - config: 房间配置字典 - - Returns: - 是否设置成功 - """ - try: - self.room_config.update(config) - print(f"✓ 房间配置已更新: {self.room_config}") - return True - except Exception as e: - print(f"✗ 房间配置设置失败: {e}") - return False - - def get_room_config(self) -> Dict[str, Any]: - """ - 获取房间配置 - - Returns: - 房间配置字典 - """ - return self.room_config.copy() - - def _trigger_room_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发房间回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.room_callbacks: - for callback in self.room_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 房间回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 房间回调触发失败: {e}") - - def register_room_callback(self, callback_type: str, callback: callable): - """ - 注册房间回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.room_callbacks: - self.room_callbacks[callback_type].append(callback) - print(f"✓ 房间回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 房间回调注册失败: {e}") - - def unregister_room_callback(self, callback_type: str, callback: callable): - """ - 注销房间回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.room_callbacks: - if callback in self.room_callbacks[callback_type]: - self.room_callbacks[callback_type].remove(callback) - print(f"✓ 房间回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +房间管理器模块 +负责管理多个游戏房间或场景实例 +""" + +import time +import threading +import uuid +from typing import Dict, Any, List, Optional + +class RoomManager: + """ + 房间管理器 + 负责管理多个游戏房间或场景实例 + """ + + def __init__(self, plugin): + """ + 初始化房间管理器 + + Args: + plugin: 服务器架构插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 房间配置 + self.room_config = { + "max_rooms": 100, + "max_clients_per_room": 50, + "enable_room_persistence": False, + "room_timeout": 3600, # 1小时无活动房间自动关闭 + "enable_room_passwords": True, + "enable_room_visibility": True, + "default_room_settings": { + "max_clients": 10, + "is_public": True, + "allow_spectators": True, + "enable_voice_chat": True + } + } + + # 房间存储 + self.rooms = {} # 房间字典 + self.client_rooms = {} # 客户端所在房间映射 + + # 房间统计 + self.room_stats = { + "total_rooms": 0, + "active_rooms": 0, + "clients_in_rooms": 0, + "rooms_created": 0, + "rooms_destroyed": 0, + "clients_joined": 0, + "clients_left": 0, + "errors": 0 + } + + # 线程锁 + self.rooms_lock = threading.RLock() + + # 回调函数 + self.room_callbacks = { + "room_created": [], + "room_destroyed": [], + "client_joined": [], + "client_left": [], + "room_updated": [] + } + + # 时间戳记录 + self.last_cleanup = 0.0 + self.last_room_check = 0.0 + + print("✓ 房间管理器已创建") + + def initialize(self) -> bool: + """ + 初始化房间管理器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化房间管理器...") + + self.initialized = True + print("✓ 房间管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 房间管理器初始化失败: {e}") + self.room_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用房间管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 房间管理器未初始化") + return False + + self.enabled = True + print("✓ 房间管理器已启用") + return True + + except Exception as e: + print(f"✗ 房间管理器启用失败: {e}") + self.room_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用房间管理器""" + try: + self.enabled = False + + # 关闭所有房间 + self.destroy_all_rooms() + + print("✓ 房间管理器已禁用") + + except Exception as e: + print(f"✗ 房间管理器禁用失败: {e}") + self.room_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理房间管理器资源""" + try: + # 禁用管理器 + if self.enabled: + self.disable() + + # 清理回调 + self.room_callbacks.clear() + + self.initialized = False + print("✓ 房间管理器资源已清理") + + except Exception as e: + print(f"✗ 房间管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新房间管理器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 定期清理过期房间 + if current_time - self.last_cleanup > 60.0: # 每分钟清理一次 + self._cleanup_expired_rooms(current_time) + self.last_cleanup = current_time + + except Exception as e: + print(f"✗ 房间管理器更新失败: {e}") + self.room_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def _cleanup_expired_rooms(self, current_time: float): + """清理过期房间""" + try: + expired_rooms = [] + with self.rooms_lock: + for room_id, room_data in self.rooms.items(): + # 检查房间是否超时 + if current_time - room_data["last_activity"] > self.room_config["room_timeout"]: + # 检查房间是否为空 + if len(room_data["clients"]) == 0: + expired_rooms.append(room_id) + + # 销毁过期房间 + for room_id in expired_rooms: + self.destroy_room(room_id) + + except Exception as e: + print(f"✗ 过期房间清理失败: {e}") + self.room_stats["errors"] += 1 + + def create_room(self, room_name: str = None, room_settings: Dict[str, Any] = None, + room_password: str = None) -> Optional[str]: + """ + 创建房间 + + Args: + room_name: 房间名称 + room_settings: 房间设置 + room_password: 房间密码 + + Returns: + 房间ID或None + """ + try: + if not self.enabled: + return None + + # 检查房间数量限制 + with self.rooms_lock: + if len(self.rooms) >= self.room_config["max_rooms"]: + print("✗ 房间数量已达上限") + return None + + # 生成房间ID + room_id = str(uuid.uuid4()) + + # 使用默认设置或传入设置 + settings = self.room_config["default_room_settings"].copy() + if room_settings: + settings.update(room_settings) + + # 创建房间数据 + room_data = { + "room_id": room_id, + "room_name": room_name or f"房间-{room_id[:8]}", + "settings": settings, + "password": room_password, + "clients": {}, # 客户端ID到客户端数据的映射 + "host_client_id": None, + "created_time": time.time(), + "last_activity": time.time(), + "room_state": {}, # 房间状态数据 + "custom_data": {} # 自定义房间数据 + } + + # 添加到房间列表 + with self.rooms_lock: + self.rooms[room_id] = room_data + + self.room_stats["total_rooms"] += 1 + self.room_stats["active_rooms"] += 1 + self.room_stats["rooms_created"] += 1 + + # 触发房间创建回调 + self._trigger_room_callback("room_created", { + "room_id": room_id, + "room_name": room_data["room_name"], + "settings": settings, + "timestamp": room_data["created_time"] + }) + + print(f"✓ 房间已创建: {room_id} ({room_data['room_name']})") + return room_id + + except Exception as e: + print(f"✗ 房间创建失败: {e}") + self.room_stats["errors"] += 1 + return None + + def destroy_room(self, room_id: str) -> bool: + """ + 销毁房间 + + Args: + room_id: 房间ID + + Returns: + 是否销毁成功 + """ + try: + if not self.enabled: + return False + + # 检查房间是否存在 + with self.rooms_lock: + if room_id not in self.rooms: + print(f"✗ 房间不存在: {room_id}") + return False + + room_data = self.rooms[room_id] + + # 通知房间内所有客户端 + for client_id in list(room_data["clients"].keys()): + self._notify_client_room_destroyed(client_id, room_id) + + # 移除房间 + del self.rooms[room_id] + + self.room_stats["active_rooms"] -= 1 + self.room_stats["rooms_destroyed"] += 1 + + # 触发房间销毁回调 + self._trigger_room_callback("room_destroyed", { + "room_id": room_id, + "room_name": room_data["room_name"], + "timestamp": time.time() + }) + + print(f"✓ 房间已销毁: {room_id}") + return True + + except Exception as e: + print(f"✗ 房间销毁失败: {e}") + self.room_stats["errors"] += 1 + return False + + def destroy_all_rooms(self): + """销毁所有房间""" + try: + with self.rooms_lock: + room_ids = list(self.rooms.keys()) + + for room_id in room_ids: + self.destroy_room(room_id) + + except Exception as e: + print(f"✗ 销毁所有房间失败: {e}") + self.room_stats["errors"] += 1 + + def add_client_to_room(self, room_id: str, client_id: str, client_data: Dict[str, Any] = None) -> bool: + """ + 添加客户端到房间 + + Args: + room_id: 房间ID + client_id: 客户端ID + client_data: 客户端数据 + + Returns: + 是否添加成功 + """ + try: + if not self.enabled: + return False + + # 检查客户端是否已认证 + if not self.plugin.client_manager.is_client_authenticated(client_id): + print(f"✗ 客户端未认证: {client_id}") + return False + + # 检查房间是否存在 + with self.rooms_lock: + if room_id not in self.rooms: + print(f"✗ 房间不存在: {room_id}") + return False + + room_data = self.rooms[room_id] + + # 检查房间是否已满 + if len(room_data["clients"]) >= room_data["settings"]["max_clients"]: + print(f"✗ 房间已满: {room_id}") + return False + + # 检查房间密码 + if room_data["password"] and not self._verify_room_password(room_id, client_data): + print(f"✗ 房间密码错误: {room_id}") + return False + + # 添加客户端到房间 + client_info = client_data or {} + client_info["join_time"] = time.time() + client_info["last_activity"] = time.time() + + room_data["clients"][client_id] = client_info + room_data["last_activity"] = time.time() + + # 设置房间主机(如果是第一个客户端) + if room_data["host_client_id"] is None: + room_data["host_client_id"] = client_id + + # 更新客户端房间映射 + self.client_rooms[client_id] = room_id + + self.room_stats["clients_in_rooms"] += 1 + self.room_stats["clients_joined"] += 1 + + # 触发客户端加入回调 + self._trigger_room_callback("client_joined", { + "room_id": room_id, + "room_name": room_data["room_name"], + "client_id": client_id, + "timestamp": time.time() + }) + + print(f"✓ 客户端已加入房间: {client_id} -> {room_id}") + return True + + except Exception as e: + print(f"✗ 添加客户端到房间失败: {e}") + self.room_stats["errors"] += 1 + return False + + def remove_client_from_room(self, room_id: str, client_id: str) -> bool: + """ + 从房间移除客户端 + + Args: + room_id: 房间ID + client_id: 客户端ID + + Returns: + 是否移除成功 + """ + try: + if not self.enabled: + return False + + # 检查房间是否存在 + with self.rooms_lock: + if room_id not in self.rooms: + print(f"✗ 房间不存在: {room_id}") + return False + + room_data = self.rooms[room_id] + + # 检查客户端是否在房间中 + if client_id not in room_data["clients"]: + print(f"✗ 客户端不在房间中: {client_id}") + return False + + # 移除客户端 + del room_data["clients"][client_id] + room_data["last_activity"] = time.time() + + # 更新主机(如果移除的是主机) + if room_data["host_client_id"] == client_id: + if room_data["clients"]: + # 选择第一个客户端作为新主机 + room_data["host_client_id"] = next(iter(room_data["clients"])) + else: + room_data["host_client_id"] = None + + # 移除客户端房间映射 + if client_id in self.client_rooms: + del self.client_rooms[client_id] + + self.room_stats["clients_in_rooms"] -= 1 + self.room_stats["clients_left"] += 1 + + # 触发客户端离开回调 + self._trigger_room_callback("client_left", { + "room_id": room_id, + "room_name": room_data["room_name"], + "client_id": client_id, + "timestamp": time.time() + }) + + # 如果房间为空,销毁房间 + if len(room_data["clients"]) == 0: + self.destroy_room(room_id) + + print(f"✓ 客户端已离开房间: {client_id} <- {room_id}") + return True + + except Exception as e: + print(f"✗ 从房间移除客户端失败: {e}") + self.room_stats["errors"] += 1 + return False + + def _verify_room_password(self, room_id: str, client_data: Dict[str, Any]) -> bool: + """ + 验证房间密码 + + Args: + room_id: 房间ID + client_data: 客户端数据 + + Returns: + 密码是否正确 + """ + try: + if not self.room_config["enable_room_passwords"]: + return True + + with self.rooms_lock: + if room_id not in self.rooms: + return False + + room_data = self.rooms[room_id] + if not room_data["password"]: + return True + + client_password = client_data.get("password") if client_data else None + return client_password == room_data["password"] + + except Exception as e: + print(f"✗ 房间密码验证失败: {e}") + self.room_stats["errors"] += 1 + return False + + def _notify_client_room_destroyed(self, client_id: str, room_id: str): + """ + 通知客户端房间被销毁 + + Args: + client_id: 客户端ID + room_id: 房间ID + """ + try: + # 从客户端房间映射中移除 + if client_id in self.client_rooms: + del self.client_rooms[client_id] + + # 发送房间销毁通知 + if self.plugin.message_router: + destroy_message = { + "type": "room_destroyed", + "room_id": room_id, + "timestamp": time.time() + } + + client_data = self.plugin.client_manager.get_client(client_id) + if client_data and "socket" in client_data: + self.plugin.message_router.send_message( + destroy_message, + client_data["socket"], + client_id + ) + + except Exception as e: + print(f"✗ 通知客户端房间销毁失败: {e}") + self.room_stats["errors"] += 1 + + def get_room_info(self, room_id: str) -> Optional[Dict[str, Any]]: + """ + 获取房间信息 + + Args: + room_id: 房间ID + + Returns: + 房间信息或None + """ + try: + with self.rooms_lock: + if room_id in self.rooms: + room_data = self.rooms[room_id] + # 返回不包含敏感信息的房间信息 + safe_room_data = { + "room_id": room_data["room_id"], + "room_name": room_data["room_name"], + "settings": room_data["settings"], + "client_count": len(room_data["clients"]), + "has_password": bool(room_data["password"]), + "host_client_id": room_data["host_client_id"], + "created_time": room_data["created_time"], + "last_activity": room_data["last_activity"] + } + return safe_room_data + else: + return None + + except Exception as e: + print(f"✗ 获取房间信息失败: {e}") + self.room_stats["errors"] += 1 + return None + + def get_all_rooms(self) -> List[Dict[str, Any]]: + """ + 获取所有房间信息 + + Returns: + 房间信息列表 + """ + try: + rooms_info = [] + with self.rooms_lock: + for room_id, room_data in self.rooms.items(): + # 只返回公开房间或用户所在的房间 + if room_data["settings"].get("is_public", True): + safe_room_data = { + "room_id": room_data["room_id"], + "room_name": room_data["room_name"], + "settings": room_data["settings"], + "client_count": len(room_data["clients"]), + "has_password": bool(room_data["password"]), + "host_client_id": room_data["host_client_id"], + "created_time": room_data["created_time"], + "last_activity": room_data["last_activity"] + } + rooms_info.append(safe_room_data) + + return rooms_info + + except Exception as e: + print(f"✗ 获取所有房间信息失败: {e}") + self.room_stats["errors"] += 1 + return [] + + def get_client_room(self, client_id: str) -> Optional[str]: + """ + 获取客户端所在房间 + + Args: + client_id: 客户端ID + + Returns: + 房间ID或None + """ + try: + return self.client_rooms.get(client_id) + except Exception as e: + print(f"✗ 获取客户端房间失败: {e}") + self.room_stats["errors"] += 1 + return None + + def get_room_clients(self, room_id: str) -> List[str]: + """ + 获取房间内所有客户端 + + Args: + room_id: 房间ID + + Returns: + 客户端ID列表 + """ + try: + with self.rooms_lock: + if room_id in self.rooms: + return list(self.rooms[room_id]["clients"].keys()) + else: + return [] + + except Exception as e: + print(f"✗ 获取房间客户端失败: {e}") + self.room_stats["errors"] += 1 + return [] + + def update_room_state(self, room_id: str, state_data: Dict[str, Any]) -> bool: + """ + 更新房间状态 + + Args: + room_id: 房间ID + state_data: 状态数据 + + Returns: + 是否更新成功 + """ + try: + with self.rooms_lock: + if room_id not in self.rooms: + return False + + self.rooms[room_id]["room_state"].update(state_data) + self.rooms[room_id]["last_activity"] = time.time() + + # 触发房间更新回调 + self._trigger_room_callback("room_updated", { + "room_id": room_id, + "state_data": state_data, + "timestamp": time.time() + }) + + return True + + except Exception as e: + print(f"✗ 更新房间状态失败: {e}") + self.room_stats["errors"] += 1 + return False + + def broadcast_to_room(self, room_id: str, message_data: Dict[str, Any], + exclude_client_id: str = None) -> bool: + """ + 向房间内所有客户端广播消息 + + Args: + room_id: 房间ID + message_data: 消息数据 + exclude_client_id: 要排除的客户端ID + + Returns: + 是否广播成功 + """ + try: + if not self.enabled: + return False + + with self.rooms_lock: + if room_id not in self.rooms: + return False + + room_data = self.rooms[room_id] + success_count = 0 + + # 向房间内每个客户端发送消息 + for client_id in room_data["clients"]: + # 跳过排除的客户端 + if client_id == exclude_client_id: + continue + + # 获取客户端数据 + client_data = self.plugin.client_manager.get_client(client_id) + if client_data and "socket" in client_data: + # 发送消息 + if self.plugin.message_router: + send_success = self.plugin.message_router.send_message( + message_data, + client_data["socket"], + client_id + ) + if send_success: + success_count += 1 + + return success_count > 0 + + except Exception as e: + print(f"✗ 房间广播失败: {e}") + self.room_stats["errors"] += 1 + return False + + def set_room_custom_data(self, room_id: str, key: str, value: Any) -> bool: + """ + 设置房间自定义数据 + + Args: + room_id: 房间ID + key: 数据键 + value: 数据值 + + Returns: + 是否设置成功 + """ + try: + with self.rooms_lock: + if room_id not in self.rooms: + return False + + self.rooms[room_id]["custom_data"][key] = value + return True + + except Exception as e: + print(f"✗ 设置房间自定义数据失败: {e}") + self.room_stats["errors"] += 1 + return False + + def get_room_custom_data(self, room_id: str, key: str, default: Any = None) -> Any: + """ + 获取房间自定义数据 + + Args: + room_id: 房间ID + key: 数据键 + default: 默认值 + + Returns: + 数据值或默认值 + """ + try: + with self.rooms_lock: + if room_id not in self.rooms: + return default + + return self.rooms[room_id]["custom_data"].get(key, default) + + except Exception as e: + print(f"✗ 获取房间自定义数据失败: {e}") + self.room_stats["errors"] += 1 + return default + + def get_room_stats(self) -> Dict[str, Any]: + """ + 获取房间统计信息 + + Returns: + 房间统计字典 + """ + return { + "stats": self.room_stats.copy(), + "config": self.room_config.copy(), + "current_rooms": len(self.rooms), + "current_clients_in_rooms": len(self.client_rooms) + } + + def reset_stats(self): + """重置房间统计信息""" + try: + self.room_stats = { + "total_rooms": 0, + "active_rooms": 0, + "clients_in_rooms": 0, + "rooms_created": 0, + "rooms_destroyed": 0, + "clients_joined": 0, + "clients_left": 0, + "errors": 0 + } + print("✓ 房间统计信息已重置") + except Exception as e: + print(f"✗ 房间统计信息重置失败: {e}") + + def set_room_config(self, config: Dict[str, Any]) -> bool: + """ + 设置房间配置 + + Args: + config: 房间配置字典 + + Returns: + 是否设置成功 + """ + try: + self.room_config.update(config) + print(f"✓ 房间配置已更新: {self.room_config}") + return True + except Exception as e: + print(f"✗ 房间配置设置失败: {e}") + return False + + def get_room_config(self) -> Dict[str, Any]: + """ + 获取房间配置 + + Returns: + 房间配置字典 + """ + return self.room_config.copy() + + def _trigger_room_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发房间回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.room_callbacks: + for callback in self.room_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 房间回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 房间回调触发失败: {e}") + + def register_room_callback(self, callback_type: str, callback: callable): + """ + 注册房间回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.room_callbacks: + self.room_callbacks[callback_type].append(callback) + print(f"✓ 房间回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 房间回调注册失败: {e}") + + def unregister_room_callback(self, callback_type: str, callback: callable): + """ + 注销房间回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.room_callbacks: + if callback in self.room_callbacks[callback_type]: + self.room_callbacks[callback_type].remove(callback) + print(f"✓ 房间回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 房间回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/server_architecture/security/security_manager.py b/plugins/user/server_architecture/security/security_manager.py index ce2bd5fb..eda4ac5d 100644 --- a/plugins/user/server_architecture/security/security_manager.py +++ b/plugins/user/server_architecture/security/security_manager.py @@ -1,892 +1,892 @@ -""" -安全管理系统模块 -负责处理认证、授权和防止攻击 -""" - -import time -import hashlib -import secrets -import hmac -from typing import Dict, Any, List, Optional - -class SecurityManager: - """ - 安全管理器 - 负责处理认证、授权和防止攻击 - """ - - def __init__(self, plugin): - """ - 初始化安全管理器 - - Args: - plugin: 服务器架构插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 安全配置 - self.security_config = { - "enable_authentication": True, - "enable_encryption": False, - "enable_message_authentication": True, - "enable_rate_limiting": True, - "enable_ddos_protection": True, - "enable_session_management": True, - "session_timeout": 3600, # 1小时 - "max_login_attempts": 5, - "login_lockout_duration": 900, # 15分钟 - "enable_ip_filtering": True, - "enable_captcha": False, - "captcha_threshold": 10, - "password_min_length": 8, - "password_require_complexity": True, - "enable_two_factor_auth": False, - "token_lifetime": 86400, # 24小时 - "enable_audit_logging": True - } - - # 安全状态 - self.security_state = { - "active_sessions": {}, - "blocked_ips": {}, - "login_attempts": {}, - "security_events": [], - "last_security_check": 0.0 - } - - # 安全统计 - self.security_stats = { - "successful_authentications": 0, - "failed_authentications": 0, - "blocked_connections": 0, - "security_events": 0, - "active_sessions": 0, - "errors": 0 - } - - # 密钥管理 - self.secret_key = self._generate_secret_key() - self.session_keys = {} - - # 回调函数 - self.security_callbacks = { - "authentication_success": [], - "authentication_failed": [], - "session_created": [], - "session_expired": [], - "security_violation": [], - "ip_blocked": [] - } - - # 时间戳记录 - self.last_security_event = 0.0 - self.last_session_cleanup = 0.0 - - print("✓ 安全管理器已创建") - - def initialize(self) -> bool: - """ - 初始化安全管理器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化安全管理器...") - - # 生成密钥 - self.secret_key = self._generate_secret_key() - - self.initialized = True - print("✓ 安全管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 安全管理器初始化失败: {e}") - self.security_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def _generate_secret_key(self) -> str: - """ - 生成密钥 - - Returns: - 生成的密钥 - """ - try: - return secrets.token_hex(32) - except Exception as e: - print(f"✗ 密钥生成失败: {e}") - # 使用默认密钥(仅用于开发环境) - return "default_secret_key_for_development_purposes_only" - - def enable(self) -> bool: - """ - 启用安全管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 安全管理器未初始化") - return False - - self.enabled = True - print("✓ 安全管理器已启用") - return True - - except Exception as e: - print(f"✗ 安全管理器启用失败: {e}") - self.security_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用安全管理器""" - try: - self.enabled = False - - # 清理会话 - self.security_state["active_sessions"].clear() - self.session_keys.clear() - - print("✓ 安全管理器已禁用") - - except Exception as e: - print(f"✗ 安全管理器禁用失败: {e}") - self.security_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理安全管理器资源""" - try: - # 禁用安全管理器 - if self.enabled: - self.disable() - - # 清理回调 - self.security_callbacks.clear() - - self.initialized = False - print("✓ 安全管理器资源已清理") - - except Exception as e: - print(f"✗ 安全管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新安全管理器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 定期清理过期会话 - if current_time - self.last_session_cleanup > 300: # 每5分钟清理一次 - self._cleanup_expired_sessions(current_time) - self.last_session_cleanup = current_time - - # 定期清理登录尝试记录 - self._cleanup_login_attempts(current_time) - - self.security_state["last_security_check"] = current_time - - except Exception as e: - print(f"✗ 安全管理器更新失败: {e}") - self.security_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def _cleanup_expired_sessions(self, current_time: float): - """清理过期会话""" - try: - expired_sessions = [] - for session_id, session_data in self.security_state["active_sessions"].items(): - if current_time - session_data["login_time"] > self.security_config["session_timeout"]: - expired_sessions.append(session_id) - - for session_id in expired_sessions: - del self.security_state["active_sessions"][session_id] - if session_id in self.session_keys: - del self.session_keys[session_id] - - self.security_stats["active_sessions"] -= 1 - - # 触发会话过期回调 - self._trigger_security_callback("session_expired", { - "session_id": session_id, - "timestamp": current_time - }) - - except Exception as e: - print(f"✗ 过期会话清理失败: {e}") - self.security_stats["errors"] += 1 - - def _cleanup_login_attempts(self, current_time: float): - """清理登录尝试记录""" - try: - expired_attempts = [] - for ip_address, attempt_data in self.security_state["login_attempts"].items(): - if current_time - attempt_data["last_attempt"] > self.security_config["login_lockout_duration"]: - expired_attempts.append(ip_address) - - for ip_address in expired_attempts: - del self.security_state["login_attempts"][ip_address] - - except Exception as e: - print(f"✗ 登录尝试记录清理失败: {e}") - self.security_stats["errors"] += 1 - - def authenticate_client(self, client_id: str, auth_data: Dict[str, Any], client_address: tuple) -> bool: - """ - 认证客户端 - - Args: - client_id: 客户端ID - auth_data: 认证数据 - client_address: 客户端地址 - - Returns: - 是否认证成功 - """ - try: - if not self.enabled or not self.security_config["enable_authentication"]: - # 如果未启用认证,直接通过 - return True - - client_ip = client_address[0] if isinstance(client_address, tuple) else str(client_address) - - # 检查IP是否被阻止 - if self._is_ip_blocked(client_ip): - self.security_stats["blocked_connections"] += 1 - return False - - # 检查登录尝试次数 - if self._is_login_blocked(client_ip): - self.security_stats["blocked_connections"] += 1 - return False - - # 执行认证 - auth_result = self._perform_authentication(client_id, auth_data, client_address) - - if auth_result: - self.security_stats["successful_authentications"] += 1 - - # 触发认证成功回调 - self._trigger_security_callback("authentication_success", { - "client_id": client_id, - "client_address": client_address, - "timestamp": time.time() - }) - - # 记录安全事件 - self._log_security_event("authentication_success", client_id, client_address) - - else: - self.security_stats["failed_authentications"] += 1 - - # 记录失败的登录尝试 - self._record_failed_login(client_ip) - - # 触发认证失败回调 - self._trigger_security_callback("authentication_failed", { - "client_id": client_id, - "client_address": client_address, - "timestamp": time.time() - }) - - # 记录安全事件 - self._log_security_event("authentication_failed", client_id, client_address) - - return auth_result - - except Exception as e: - print(f"✗ 客户端认证失败: {e}") - self.security_stats["errors"] += 1 - return False - - def _is_ip_blocked(self, client_ip: str) -> bool: - """ - 检查IP是否被阻止 - - Args: - client_ip: 客户端IP - - Returns: - IP是否被阻止 - """ - try: - if not self.security_config["enable_ip_filtering"]: - return False - - if client_ip in self.security_state["blocked_ips"]: - block_data = self.security_state["blocked_ips"][client_ip] - if time.time() - block_data["block_time"] < block_data["duration"]: - return True - else: - # 阻止时间已过期,移除 - del self.security_state["blocked_ips"][client_ip] - - return False - - except Exception as e: - print(f"✗ IP阻止检查失败: {e}") - self.security_stats["errors"] += 1 - return False - - def _is_login_blocked(self, client_ip: str) -> bool: - """ - 检查登录是否被阻止 - - Args: - client_ip: 客户端IP - - Returns: - 登录是否被阻止 - """ - try: - if client_ip in self.security_state["login_attempts"]: - attempts = self.security_state["login_attempts"][client_ip] - if (attempts["count"] >= self.security_config["max_login_attempts"] and - time.time() - attempts["last_attempt"] < self.security_config["login_lockout_duration"]): - return True - - return False - - except Exception as e: - print(f"✗ 登录阻止检查失败: {e}") - self.security_stats["errors"] += 1 - return False - - def _record_failed_login(self, client_ip: str): - """ - 记录失败的登录尝试 - - Args: - client_ip: 客户端IP - """ - try: - current_time = time.time() - - if client_ip not in self.security_state["login_attempts"]: - self.security_state["login_attempts"][client_ip] = { - "count": 1, - "last_attempt": current_time - } - else: - self.security_state["login_attempts"][client_ip]["count"] += 1 - self.security_state["login_attempts"][client_ip]["last_attempt"] = current_time - - # 如果超过最大尝试次数,阻止IP - if (self.security_state["login_attempts"][client_ip]["count"] >= - self.security_config["max_login_attempts"]): - self._block_ip(client_ip, self.security_config["login_lockout_duration"]) - - except Exception as e: - print(f"✗ 失败登录记录失败: {e}") - self.security_stats["errors"] += 1 - - def _perform_authentication(self, client_id: str, auth_data: Dict[str, Any], client_address: tuple) -> bool: - """ - 执行认证 - - Args: - client_id: 客户端ID - auth_data: 认证数据 - client_address: 客户端地址 - - Returns: - 是否认证成功 - """ - try: - # 检查认证类型 - auth_type = auth_data.get("auth_type", "token") - - if auth_type == "token": - return self._authenticate_with_token(client_id, auth_data, client_address) - elif auth_type == "password": - return self._authenticate_with_password(client_id, auth_data, client_address) - elif auth_type == "session": - return self._authenticate_with_session(client_id, auth_data, client_address) - else: - # 默认认证方式 - return self._authenticate_with_token(client_id, auth_data, client_address) - - except Exception as e: - print(f"✗ 认证执行失败: {e}") - self.security_stats["errors"] += 1 - return False - - def _authenticate_with_token(self, client_id: str, auth_data: Dict[str, Any], client_address: tuple) -> bool: - """ - 使用令牌认证 - - Args: - client_id: 客户端ID - auth_data: 认证数据 - client_address: 客户端地址 - - Returns: - 是否认证成功 - """ - try: - token = auth_data.get("token") - if not token: - return False - - # 验证令牌(简化实现) - # 在实际应用中,这里应该连接到用户数据库验证令牌 - if token == "valid_test_token": - # 创建会话 - self._create_session(client_id, auth_data, client_address) - return True - else: - return False - - except Exception as e: - print(f"✗ 令牌认证失败: {e}") - self.security_stats["errors"] += 1 - return False - - def _authenticate_with_password(self, client_id: str, auth_data: Dict[str, Any], client_address: tuple) -> bool: - """ - 使用密码认证 - - Args: - client_id: 客户端ID - auth_data: 认证数据 - client_address: 客户端地址 - - Returns: - 是否认证成功 - """ - try: - username = auth_data.get("username") - password = auth_data.get("password") - - if not username or not password: - return False - - # 验证密码(简化实现) - # 在实际应用中,这里应该连接到用户数据库验证用户名和密码 - if username == "test_user" and password == "test_password": - # 创建会话 - self._create_session(client_id, auth_data, client_address) - return True - else: - return False - - except Exception as e: - print(f"✗ 密码认证失败: {e}") - self.security_stats["errors"] += 1 - return False - - def _authenticate_with_session(self, client_id: str, auth_data: Dict[str, Any], client_address: tuple) -> bool: - """ - 使用会话认证 - - Args: - client_id: 客户端ID - auth_data: 认证数据 - client_address: 客户端地址 - - Returns: - 是否认证成功 - """ - try: - session_id = auth_data.get("session_id") - if not session_id: - return False - - # 检查会话是否存在且有效 - if session_id in self.security_state["active_sessions"]: - session_data = self.security_state["active_sessions"][session_id] - if time.time() - session_data["login_time"] < self.security_config["session_timeout"]: - # 更新会话最后活动时间 - session_data["last_activity"] = time.time() - return True - - return False - - except Exception as e: - print(f"✗ 会话认证失败: {e}") - self.security_stats["errors"] += 1 - return False - - def _create_session(self, client_id: str, auth_data: Dict[str, Any], client_address: tuple): - """ - 创建会话 - - Args: - client_id: 客户端ID - auth_data: 认证数据 - client_address: 客户端地址 - """ - try: - session_id = self._generate_session_id() - - session_data = { - "client_id": client_id, - "client_address": client_address, - "login_time": time.time(), - "last_activity": time.time(), - "user_id": auth_data.get("user_id", ""), - "username": auth_data.get("username", ""), - "permissions": auth_data.get("permissions", []) - } - - self.security_state["active_sessions"][session_id] = session_data - self.security_stats["active_sessions"] += 1 - - # 生成会话密钥 - if self.security_config["enable_encryption"]: - self.session_keys[session_id] = self._generate_session_key() - - # 触发会话创建回调 - self._trigger_security_callback("session_created", { - "session_id": session_id, - "client_id": client_id, - "client_address": client_address, - "timestamp": time.time() - }) - - except Exception as e: - print(f"✗ 会话创建失败: {e}") - self.security_stats["errors"] += 1 - - def _generate_session_id(self) -> str: - """ - 生成会话ID - - Returns: - 生成的会话ID - """ - try: - return secrets.token_urlsafe(32) - except Exception as e: - print(f"✗ 会话ID生成失败: {e}") - self.security_stats["errors"] += 1 - return f"session_{int(time.time() * 1000000)}" - - def _generate_session_key(self) -> str: - """ - 生成会话密钥 - - Returns: - 生成的会话密钥 - """ - try: - return secrets.token_hex(16) - except Exception as e: - print(f"✗ 会话密钥生成失败: {e}") - self.security_stats["errors"] += 1 - return f"key_{int(time.time() * 1000000)}" - - def validate_token(self, token: str) -> bool: - """ - 验证令牌 - - Args: - token: 令牌 - - Returns: - 令牌是否有效 - """ - try: - if not token: - return False - - # 简化实现:检查令牌格式 - # 在实际应用中,这里应该连接到令牌数据库验证令牌 - return len(token) > 10 and token.isalnum() - - except Exception as e: - print(f"✗ 令牌验证失败: {e}") - self.security_stats["errors"] += 1 - return False - - def hash_password(self, password: str, salt: str = None) -> Dict[str, str]: - """ - 哈希密码 - - Args: - password: 密码 - salt: 盐值(可选) - - Returns: - 包含哈希值和盐值的字典 - """ - try: - if salt is None: - salt = secrets.token_hex(16) - - # 使用PBKDF2哈希密码 - password_hash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt.encode('utf-8'), 100000) - return { - "hash": password_hash.hex(), - "salt": salt - } - - except Exception as e: - print(f"✗ 密码哈希失败: {e}") - self.security_stats["errors"] += 1 - return {} - - def verify_password(self, password: str, password_hash: str, salt: str) -> bool: - """ - 验证密码 - - Args: - password: 密码 - password_hash: 密码哈希值 - salt: 盐值 - - Returns: - 密码是否正确 - """ - try: - # 重新哈希密码 - new_hash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt.encode('utf-8'), 100000) - return hmac.compare_digest(new_hash.hex(), password_hash) - - except Exception as e: - print(f"✗ 密码验证失败: {e}") - self.security_stats["errors"] += 1 - return False - - def _block_ip(self, client_ip: str, duration: float): - """ - 阻止IP地址 - - Args: - client_ip: 客户端IP - duration: 阻止持续时间(秒) - """ - try: - self.security_state["blocked_ips"][client_ip] = { - "block_time": time.time(), - "duration": duration - } - - # 触发IP阻止回调 - self._trigger_security_callback("ip_blocked", { - "ip_address": client_ip, - "duration": duration, - "timestamp": time.time() - }) - - # 记录安全事件 - self._log_security_event("ip_blocked", None, client_ip) - - print(f"✓ IP地址已被阻止: {client_ip} (持续 {duration} 秒)") - - except Exception as e: - print(f"✗ IP阻止失败: {e}") - self.security_stats["errors"] += 1 - - def unblock_ip(self, client_ip: str) -> bool: - """ - 解除IP阻止 - - Args: - client_ip: 客户端IP - - Returns: - 是否解除成功 - """ - try: - if client_ip in self.security_state["blocked_ips"]: - del self.security_state["blocked_ips"][client_ip] - print(f"✓ IP地址阻止已解除: {client_ip}") - return True - else: - print(f"✗ IP地址未被阻止: {client_ip}") - return False - - except Exception as e: - print(f"✗ IP阻止解除失败: {e}") - self.security_stats["errors"] += 1 - return False - - def _log_security_event(self, event_type: str, client_id: str = None, client_address=None): - """ - 记录安全事件 - - Args: - event_type: 事件类型 - client_id: 客户端ID(可选) - client_address: 客户端地址(可选) - """ - try: - if not self.security_config["enable_audit_logging"]: - return - - event = { - "event_type": event_type, - "client_id": client_id, - "client_address": client_address, - "timestamp": time.time() - } - - self.security_state["security_events"].append(event) - self.security_stats["security_events"] += 1 - - # 保持事件列表大小 - if len(self.security_state["security_events"]) > 1000: - self.security_state["security_events"] = self.security_state["security_events"][-1000:] - - self.last_security_event = time.time() - - except Exception as e: - print(f"✗ 安全事件记录失败: {e}") - self.security_stats["errors"] += 1 - - def get_active_sessions(self) -> Dict[str, Any]: - """ - 获取活跃会话 - - Returns: - 活跃会话字典 - """ - try: - return self.security_state["active_sessions"].copy() - except Exception as e: - print(f"✗ 获取活跃会话失败: {e}") - self.security_stats["errors"] += 1 - return {} - - def get_blocked_ips(self) -> Dict[str, Any]: - """ - 获取被阻止的IP地址 - - Returns: - 被阻止的IP地址字典 - """ - try: - return self.security_state["blocked_ips"].copy() - except Exception as e: - print(f"✗ 获取被阻止的IP地址失败: {e}") - self.security_stats["errors"] += 1 - return {} - - def get_security_stats(self) -> Dict[str, Any]: - """ - 获取安全统计信息 - - Returns: - 安全统计字典 - """ - return { - "state": self.security_state.copy(), - "stats": self.security_stats.copy(), - "config": self.security_config.copy() - } - - def reset_stats(self): - """重置安全统计信息""" - try: - self.security_stats = { - "successful_authentications": 0, - "failed_authentications": 0, - "blocked_connections": 0, - "security_events": 0, - "active_sessions": 0, - "errors": 0 - } - print("✓ 安全统计信息已重置") - except Exception as e: - print(f"✗ 安全统计信息重置失败: {e}") - - def set_security_config(self, config: Dict[str, Any]) -> bool: - """ - 设置安全配置 - - Args: - config: 安全配置字典 - - Returns: - 是否设置成功 - """ - try: - self.security_config.update(config) - print(f"✓ 安全配置已更新: {self.security_config}") - return True - except Exception as e: - print(f"✗ 安全配置设置失败: {e}") - return False - - def get_security_config(self) -> Dict[str, Any]: - """ - 获取安全配置 - - Returns: - 安全配置字典 - """ - return self.security_config.copy() - - def _trigger_security_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发安全回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.security_callbacks: - for callback in self.security_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 安全回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 安全回调触发失败: {e}") - - def register_security_callback(self, callback_type: str, callback: callable): - """ - 注册安全回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.security_callbacks: - self.security_callbacks[callback_type].append(callback) - print(f"✓ 安全回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 安全回调注册失败: {e}") - - def unregister_security_callback(self, callback_type: str, callback: callable): - """ - 注销安全回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.security_callbacks: - if callback in self.security_callbacks[callback_type]: - self.security_callbacks[callback_type].remove(callback) - print(f"✓ 安全回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: +""" +安全管理系统模块 +负责处理认证、授权和防止攻击 +""" + +import time +import hashlib +import secrets +import hmac +from typing import Dict, Any, List, Optional + +class SecurityManager: + """ + 安全管理器 + 负责处理认证、授权和防止攻击 + """ + + def __init__(self, plugin): + """ + 初始化安全管理器 + + Args: + plugin: 服务器架构插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 安全配置 + self.security_config = { + "enable_authentication": True, + "enable_encryption": False, + "enable_message_authentication": True, + "enable_rate_limiting": True, + "enable_ddos_protection": True, + "enable_session_management": True, + "session_timeout": 3600, # 1小时 + "max_login_attempts": 5, + "login_lockout_duration": 900, # 15分钟 + "enable_ip_filtering": True, + "enable_captcha": False, + "captcha_threshold": 10, + "password_min_length": 8, + "password_require_complexity": True, + "enable_two_factor_auth": False, + "token_lifetime": 86400, # 24小时 + "enable_audit_logging": True + } + + # 安全状态 + self.security_state = { + "active_sessions": {}, + "blocked_ips": {}, + "login_attempts": {}, + "security_events": [], + "last_security_check": 0.0 + } + + # 安全统计 + self.security_stats = { + "successful_authentications": 0, + "failed_authentications": 0, + "blocked_connections": 0, + "security_events": 0, + "active_sessions": 0, + "errors": 0 + } + + # 密钥管理 + self.secret_key = self._generate_secret_key() + self.session_keys = {} + + # 回调函数 + self.security_callbacks = { + "authentication_success": [], + "authentication_failed": [], + "session_created": [], + "session_expired": [], + "security_violation": [], + "ip_blocked": [] + } + + # 时间戳记录 + self.last_security_event = 0.0 + self.last_session_cleanup = 0.0 + + print("✓ 安全管理器已创建") + + def initialize(self) -> bool: + """ + 初始化安全管理器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化安全管理器...") + + # 生成密钥 + self.secret_key = self._generate_secret_key() + + self.initialized = True + print("✓ 安全管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 安全管理器初始化失败: {e}") + self.security_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def _generate_secret_key(self) -> str: + """ + 生成密钥 + + Returns: + 生成的密钥 + """ + try: + return secrets.token_hex(32) + except Exception as e: + print(f"✗ 密钥生成失败: {e}") + # 使用默认密钥(仅用于开发环境) + return "default_secret_key_for_development_purposes_only" + + def enable(self) -> bool: + """ + 启用安全管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 安全管理器未初始化") + return False + + self.enabled = True + print("✓ 安全管理器已启用") + return True + + except Exception as e: + print(f"✗ 安全管理器启用失败: {e}") + self.security_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用安全管理器""" + try: + self.enabled = False + + # 清理会话 + self.security_state["active_sessions"].clear() + self.session_keys.clear() + + print("✓ 安全管理器已禁用") + + except Exception as e: + print(f"✗ 安全管理器禁用失败: {e}") + self.security_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理安全管理器资源""" + try: + # 禁用安全管理器 + if self.enabled: + self.disable() + + # 清理回调 + self.security_callbacks.clear() + + self.initialized = False + print("✓ 安全管理器资源已清理") + + except Exception as e: + print(f"✗ 安全管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新安全管理器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 定期清理过期会话 + if current_time - self.last_session_cleanup > 300: # 每5分钟清理一次 + self._cleanup_expired_sessions(current_time) + self.last_session_cleanup = current_time + + # 定期清理登录尝试记录 + self._cleanup_login_attempts(current_time) + + self.security_state["last_security_check"] = current_time + + except Exception as e: + print(f"✗ 安全管理器更新失败: {e}") + self.security_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def _cleanup_expired_sessions(self, current_time: float): + """清理过期会话""" + try: + expired_sessions = [] + for session_id, session_data in self.security_state["active_sessions"].items(): + if current_time - session_data["login_time"] > self.security_config["session_timeout"]: + expired_sessions.append(session_id) + + for session_id in expired_sessions: + del self.security_state["active_sessions"][session_id] + if session_id in self.session_keys: + del self.session_keys[session_id] + + self.security_stats["active_sessions"] -= 1 + + # 触发会话过期回调 + self._trigger_security_callback("session_expired", { + "session_id": session_id, + "timestamp": current_time + }) + + except Exception as e: + print(f"✗ 过期会话清理失败: {e}") + self.security_stats["errors"] += 1 + + def _cleanup_login_attempts(self, current_time: float): + """清理登录尝试记录""" + try: + expired_attempts = [] + for ip_address, attempt_data in self.security_state["login_attempts"].items(): + if current_time - attempt_data["last_attempt"] > self.security_config["login_lockout_duration"]: + expired_attempts.append(ip_address) + + for ip_address in expired_attempts: + del self.security_state["login_attempts"][ip_address] + + except Exception as e: + print(f"✗ 登录尝试记录清理失败: {e}") + self.security_stats["errors"] += 1 + + def authenticate_client(self, client_id: str, auth_data: Dict[str, Any], client_address: tuple) -> bool: + """ + 认证客户端 + + Args: + client_id: 客户端ID + auth_data: 认证数据 + client_address: 客户端地址 + + Returns: + 是否认证成功 + """ + try: + if not self.enabled or not self.security_config["enable_authentication"]: + # 如果未启用认证,直接通过 + return True + + client_ip = client_address[0] if isinstance(client_address, tuple) else str(client_address) + + # 检查IP是否被阻止 + if self._is_ip_blocked(client_ip): + self.security_stats["blocked_connections"] += 1 + return False + + # 检查登录尝试次数 + if self._is_login_blocked(client_ip): + self.security_stats["blocked_connections"] += 1 + return False + + # 执行认证 + auth_result = self._perform_authentication(client_id, auth_data, client_address) + + if auth_result: + self.security_stats["successful_authentications"] += 1 + + # 触发认证成功回调 + self._trigger_security_callback("authentication_success", { + "client_id": client_id, + "client_address": client_address, + "timestamp": time.time() + }) + + # 记录安全事件 + self._log_security_event("authentication_success", client_id, client_address) + + else: + self.security_stats["failed_authentications"] += 1 + + # 记录失败的登录尝试 + self._record_failed_login(client_ip) + + # 触发认证失败回调 + self._trigger_security_callback("authentication_failed", { + "client_id": client_id, + "client_address": client_address, + "timestamp": time.time() + }) + + # 记录安全事件 + self._log_security_event("authentication_failed", client_id, client_address) + + return auth_result + + except Exception as e: + print(f"✗ 客户端认证失败: {e}") + self.security_stats["errors"] += 1 + return False + + def _is_ip_blocked(self, client_ip: str) -> bool: + """ + 检查IP是否被阻止 + + Args: + client_ip: 客户端IP + + Returns: + IP是否被阻止 + """ + try: + if not self.security_config["enable_ip_filtering"]: + return False + + if client_ip in self.security_state["blocked_ips"]: + block_data = self.security_state["blocked_ips"][client_ip] + if time.time() - block_data["block_time"] < block_data["duration"]: + return True + else: + # 阻止时间已过期,移除 + del self.security_state["blocked_ips"][client_ip] + + return False + + except Exception as e: + print(f"✗ IP阻止检查失败: {e}") + self.security_stats["errors"] += 1 + return False + + def _is_login_blocked(self, client_ip: str) -> bool: + """ + 检查登录是否被阻止 + + Args: + client_ip: 客户端IP + + Returns: + 登录是否被阻止 + """ + try: + if client_ip in self.security_state["login_attempts"]: + attempts = self.security_state["login_attempts"][client_ip] + if (attempts["count"] >= self.security_config["max_login_attempts"] and + time.time() - attempts["last_attempt"] < self.security_config["login_lockout_duration"]): + return True + + return False + + except Exception as e: + print(f"✗ 登录阻止检查失败: {e}") + self.security_stats["errors"] += 1 + return False + + def _record_failed_login(self, client_ip: str): + """ + 记录失败的登录尝试 + + Args: + client_ip: 客户端IP + """ + try: + current_time = time.time() + + if client_ip not in self.security_state["login_attempts"]: + self.security_state["login_attempts"][client_ip] = { + "count": 1, + "last_attempt": current_time + } + else: + self.security_state["login_attempts"][client_ip]["count"] += 1 + self.security_state["login_attempts"][client_ip]["last_attempt"] = current_time + + # 如果超过最大尝试次数,阻止IP + if (self.security_state["login_attempts"][client_ip]["count"] >= + self.security_config["max_login_attempts"]): + self._block_ip(client_ip, self.security_config["login_lockout_duration"]) + + except Exception as e: + print(f"✗ 失败登录记录失败: {e}") + self.security_stats["errors"] += 1 + + def _perform_authentication(self, client_id: str, auth_data: Dict[str, Any], client_address: tuple) -> bool: + """ + 执行认证 + + Args: + client_id: 客户端ID + auth_data: 认证数据 + client_address: 客户端地址 + + Returns: + 是否认证成功 + """ + try: + # 检查认证类型 + auth_type = auth_data.get("auth_type", "token") + + if auth_type == "token": + return self._authenticate_with_token(client_id, auth_data, client_address) + elif auth_type == "password": + return self._authenticate_with_password(client_id, auth_data, client_address) + elif auth_type == "session": + return self._authenticate_with_session(client_id, auth_data, client_address) + else: + # 默认认证方式 + return self._authenticate_with_token(client_id, auth_data, client_address) + + except Exception as e: + print(f"✗ 认证执行失败: {e}") + self.security_stats["errors"] += 1 + return False + + def _authenticate_with_token(self, client_id: str, auth_data: Dict[str, Any], client_address: tuple) -> bool: + """ + 使用令牌认证 + + Args: + client_id: 客户端ID + auth_data: 认证数据 + client_address: 客户端地址 + + Returns: + 是否认证成功 + """ + try: + token = auth_data.get("token") + if not token: + return False + + # 验证令牌(简化实现) + # 在实际应用中,这里应该连接到用户数据库验证令牌 + if token == "valid_test_token": + # 创建会话 + self._create_session(client_id, auth_data, client_address) + return True + else: + return False + + except Exception as e: + print(f"✗ 令牌认证失败: {e}") + self.security_stats["errors"] += 1 + return False + + def _authenticate_with_password(self, client_id: str, auth_data: Dict[str, Any], client_address: tuple) -> bool: + """ + 使用密码认证 + + Args: + client_id: 客户端ID + auth_data: 认证数据 + client_address: 客户端地址 + + Returns: + 是否认证成功 + """ + try: + username = auth_data.get("username") + password = auth_data.get("password") + + if not username or not password: + return False + + # 验证密码(简化实现) + # 在实际应用中,这里应该连接到用户数据库验证用户名和密码 + if username == "test_user" and password == "test_password": + # 创建会话 + self._create_session(client_id, auth_data, client_address) + return True + else: + return False + + except Exception as e: + print(f"✗ 密码认证失败: {e}") + self.security_stats["errors"] += 1 + return False + + def _authenticate_with_session(self, client_id: str, auth_data: Dict[str, Any], client_address: tuple) -> bool: + """ + 使用会话认证 + + Args: + client_id: 客户端ID + auth_data: 认证数据 + client_address: 客户端地址 + + Returns: + 是否认证成功 + """ + try: + session_id = auth_data.get("session_id") + if not session_id: + return False + + # 检查会话是否存在且有效 + if session_id in self.security_state["active_sessions"]: + session_data = self.security_state["active_sessions"][session_id] + if time.time() - session_data["login_time"] < self.security_config["session_timeout"]: + # 更新会话最后活动时间 + session_data["last_activity"] = time.time() + return True + + return False + + except Exception as e: + print(f"✗ 会话认证失败: {e}") + self.security_stats["errors"] += 1 + return False + + def _create_session(self, client_id: str, auth_data: Dict[str, Any], client_address: tuple): + """ + 创建会话 + + Args: + client_id: 客户端ID + auth_data: 认证数据 + client_address: 客户端地址 + """ + try: + session_id = self._generate_session_id() + + session_data = { + "client_id": client_id, + "client_address": client_address, + "login_time": time.time(), + "last_activity": time.time(), + "user_id": auth_data.get("user_id", ""), + "username": auth_data.get("username", ""), + "permissions": auth_data.get("permissions", []) + } + + self.security_state["active_sessions"][session_id] = session_data + self.security_stats["active_sessions"] += 1 + + # 生成会话密钥 + if self.security_config["enable_encryption"]: + self.session_keys[session_id] = self._generate_session_key() + + # 触发会话创建回调 + self._trigger_security_callback("session_created", { + "session_id": session_id, + "client_id": client_id, + "client_address": client_address, + "timestamp": time.time() + }) + + except Exception as e: + print(f"✗ 会话创建失败: {e}") + self.security_stats["errors"] += 1 + + def _generate_session_id(self) -> str: + """ + 生成会话ID + + Returns: + 生成的会话ID + """ + try: + return secrets.token_urlsafe(32) + except Exception as e: + print(f"✗ 会话ID生成失败: {e}") + self.security_stats["errors"] += 1 + return f"session_{int(time.time() * 1000000)}" + + def _generate_session_key(self) -> str: + """ + 生成会话密钥 + + Returns: + 生成的会话密钥 + """ + try: + return secrets.token_hex(16) + except Exception as e: + print(f"✗ 会话密钥生成失败: {e}") + self.security_stats["errors"] += 1 + return f"key_{int(time.time() * 1000000)}" + + def validate_token(self, token: str) -> bool: + """ + 验证令牌 + + Args: + token: 令牌 + + Returns: + 令牌是否有效 + """ + try: + if not token: + return False + + # 简化实现:检查令牌格式 + # 在实际应用中,这里应该连接到令牌数据库验证令牌 + return len(token) > 10 and token.isalnum() + + except Exception as e: + print(f"✗ 令牌验证失败: {e}") + self.security_stats["errors"] += 1 + return False + + def hash_password(self, password: str, salt: str = None) -> Dict[str, str]: + """ + 哈希密码 + + Args: + password: 密码 + salt: 盐值(可选) + + Returns: + 包含哈希值和盐值的字典 + """ + try: + if salt is None: + salt = secrets.token_hex(16) + + # 使用PBKDF2哈希密码 + password_hash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt.encode('utf-8'), 100000) + return { + "hash": password_hash.hex(), + "salt": salt + } + + except Exception as e: + print(f"✗ 密码哈希失败: {e}") + self.security_stats["errors"] += 1 + return {} + + def verify_password(self, password: str, password_hash: str, salt: str) -> bool: + """ + 验证密码 + + Args: + password: 密码 + password_hash: 密码哈希值 + salt: 盐值 + + Returns: + 密码是否正确 + """ + try: + # 重新哈希密码 + new_hash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt.encode('utf-8'), 100000) + return hmac.compare_digest(new_hash.hex(), password_hash) + + except Exception as e: + print(f"✗ 密码验证失败: {e}") + self.security_stats["errors"] += 1 + return False + + def _block_ip(self, client_ip: str, duration: float): + """ + 阻止IP地址 + + Args: + client_ip: 客户端IP + duration: 阻止持续时间(秒) + """ + try: + self.security_state["blocked_ips"][client_ip] = { + "block_time": time.time(), + "duration": duration + } + + # 触发IP阻止回调 + self._trigger_security_callback("ip_blocked", { + "ip_address": client_ip, + "duration": duration, + "timestamp": time.time() + }) + + # 记录安全事件 + self._log_security_event("ip_blocked", None, client_ip) + + print(f"✓ IP地址已被阻止: {client_ip} (持续 {duration} 秒)") + + except Exception as e: + print(f"✗ IP阻止失败: {e}") + self.security_stats["errors"] += 1 + + def unblock_ip(self, client_ip: str) -> bool: + """ + 解除IP阻止 + + Args: + client_ip: 客户端IP + + Returns: + 是否解除成功 + """ + try: + if client_ip in self.security_state["blocked_ips"]: + del self.security_state["blocked_ips"][client_ip] + print(f"✓ IP地址阻止已解除: {client_ip}") + return True + else: + print(f"✗ IP地址未被阻止: {client_ip}") + return False + + except Exception as e: + print(f"✗ IP阻止解除失败: {e}") + self.security_stats["errors"] += 1 + return False + + def _log_security_event(self, event_type: str, client_id: str = None, client_address=None): + """ + 记录安全事件 + + Args: + event_type: 事件类型 + client_id: 客户端ID(可选) + client_address: 客户端地址(可选) + """ + try: + if not self.security_config["enable_audit_logging"]: + return + + event = { + "event_type": event_type, + "client_id": client_id, + "client_address": client_address, + "timestamp": time.time() + } + + self.security_state["security_events"].append(event) + self.security_stats["security_events"] += 1 + + # 保持事件列表大小 + if len(self.security_state["security_events"]) > 1000: + self.security_state["security_events"] = self.security_state["security_events"][-1000:] + + self.last_security_event = time.time() + + except Exception as e: + print(f"✗ 安全事件记录失败: {e}") + self.security_stats["errors"] += 1 + + def get_active_sessions(self) -> Dict[str, Any]: + """ + 获取活跃会话 + + Returns: + 活跃会话字典 + """ + try: + return self.security_state["active_sessions"].copy() + except Exception as e: + print(f"✗ 获取活跃会话失败: {e}") + self.security_stats["errors"] += 1 + return {} + + def get_blocked_ips(self) -> Dict[str, Any]: + """ + 获取被阻止的IP地址 + + Returns: + 被阻止的IP地址字典 + """ + try: + return self.security_state["blocked_ips"].copy() + except Exception as e: + print(f"✗ 获取被阻止的IP地址失败: {e}") + self.security_stats["errors"] += 1 + return {} + + def get_security_stats(self) -> Dict[str, Any]: + """ + 获取安全统计信息 + + Returns: + 安全统计字典 + """ + return { + "state": self.security_state.copy(), + "stats": self.security_stats.copy(), + "config": self.security_config.copy() + } + + def reset_stats(self): + """重置安全统计信息""" + try: + self.security_stats = { + "successful_authentications": 0, + "failed_authentications": 0, + "blocked_connections": 0, + "security_events": 0, + "active_sessions": 0, + "errors": 0 + } + print("✓ 安全统计信息已重置") + except Exception as e: + print(f"✗ 安全统计信息重置失败: {e}") + + def set_security_config(self, config: Dict[str, Any]) -> bool: + """ + 设置安全配置 + + Args: + config: 安全配置字典 + + Returns: + 是否设置成功 + """ + try: + self.security_config.update(config) + print(f"✓ 安全配置已更新: {self.security_config}") + return True + except Exception as e: + print(f"✗ 安全配置设置失败: {e}") + return False + + def get_security_config(self) -> Dict[str, Any]: + """ + 获取安全配置 + + Returns: + 安全配置字典 + """ + return self.security_config.copy() + + def _trigger_security_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发安全回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.security_callbacks: + for callback in self.security_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 安全回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 安全回调触发失败: {e}") + + def register_security_callback(self, callback_type: str, callback: callable): + """ + 注册安全回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.security_callbacks: + self.security_callbacks[callback_type].append(callback) + print(f"✓ 安全回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 安全回调注册失败: {e}") + + def unregister_security_callback(self, callback_type: str, callback: callable): + """ + 注销安全回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.security_callbacks: + if callback in self.security_callbacks[callback_type]: + self.security_callbacks[callback_type].remove(callback) + print(f"✓ 安全回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: print(f"✗ 安全回调注销失败: {e}") \ No newline at end of file diff --git a/plugins/user/server_architecture/storage/storage_manager.py b/plugins/user/server_architecture/storage/storage_manager.py index f36a9bd0..7e3022b5 100644 --- a/plugins/user/server_architecture/storage/storage_manager.py +++ b/plugins/user/server_architecture/storage/storage_manager.py @@ -1,1185 +1,1185 @@ -""" -数据存储系统模块 -负责持久化存储用户数据和游戏状态 -""" - -import time -import threading -import json -import os -import sqlite3 -from typing import Dict, Any, List, Optional - -class StorageManager: - """ - 数据存储管理器 - 负责持久化存储用户数据和游戏状态 - """ - - def __init__(self, plugin): - """ - 初始化数据存储管理器 - - Args: - plugin: 服务器架构插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 存储配置 - self.storage_config = { - "database_path": "/home/hello/EG/plugins/user/server_architecture/data/server_database.db", - "enable_caching": True, - "cache_size": 1000, - "enable_compression": True, - "backup_interval": 3600, # 1小时 - "enable_auto_backup": True, - "max_backups": 10, - "enable_encryption": False, - "enable_async_writes": True - } - - # 数据库连接 - self.db_connection = None - - # 缓存管理 - self.data_cache = {} - self.cache_lock = threading.RLock() - - # 存储统计 - self.storage_stats = { - "reads": 0, - "writes": 0, - "cache_hits": 0, - "cache_misses": 0, - "backups_created": 0, - "errors": 0, - "bytes_read": 0, - "bytes_written": 0 - } - - # 异步写入队列 - self.write_queue = [] - self.write_queue_lock = threading.RLock() - self.write_thread = None - self.write_thread_running = False - - # 回调函数 - self.storage_callbacks = { - "data_saved": [], - "data_loaded": [], - "backup_created": [], - "storage_error": [] - } - - # 时间戳记录 - self.last_backup = 0.0 - self.last_write = 0.0 - self.last_read = 0.0 - - print("✓ 数据存储管理器已创建") - - def initialize(self) -> bool: - """ - 初始化数据存储管理器 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化数据存储管理器...") - - # 创建数据目录 - data_dir = os.path.dirname(self.storage_config["database_path"]) - if not os.path.exists(data_dir): - os.makedirs(data_dir) - - # 初始化数据库 - self._initialize_database() - - # 启动异步写入线程 - if self.storage_config["enable_async_writes"]: - self._start_write_thread() - - self.initialized = True - print("✓ 数据存储管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 数据存储管理器初始化失败: {e}") - self.storage_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def _initialize_database(self): - """初始化数据库""" - try: - # 连接到SQLite数据库 - self.db_connection = sqlite3.connect( - self.storage_config["database_path"], - check_same_thread=False - ) - - # 创建表 - cursor = self.db_connection.cursor() - - # 用户表 - cursor.execute(''' - CREATE TABLE IF NOT EXISTS users ( - user_id TEXT PRIMARY KEY, - username TEXT UNIQUE NOT NULL, - email TEXT, - password_hash TEXT, - created_time REAL, - last_login REAL, - user_data TEXT - ) - ''') - - # 房间表 - cursor.execute(''' - CREATE TABLE IF NOT EXISTS rooms ( - room_id TEXT PRIMARY KEY, - room_name TEXT NOT NULL, - host_user_id TEXT, - created_time REAL, - room_settings TEXT, - room_state TEXT - ) - ''') - - # 游戏状态表 - cursor.execute(''' - CREATE TABLE IF NOT EXISTS game_states ( - state_id TEXT PRIMARY KEY, - room_id TEXT, - state_data TEXT, - timestamp REAL, - FOREIGN KEY (room_id) REFERENCES rooms (room_id) - ) - ''') - - # 用户房间关系表 - cursor.execute(''' - CREATE TABLE IF NOT EXISTS user_rooms ( - user_id TEXT, - room_id TEXT, - join_time REAL, - PRIMARY KEY (user_id, room_id), - FOREIGN KEY (user_id) REFERENCES users (user_id), - FOREIGN KEY (room_id) REFERENCES rooms (room_id) - ) - ''') - - # 系统配置表 - cursor.execute(''' - CREATE TABLE IF NOT EXISTS system_config ( - config_key TEXT PRIMARY KEY, - config_value TEXT, - updated_time REAL - ) - ''') - - # 日志表 - cursor.execute(''' - CREATE TABLE IF NOT EXISTS logs ( - log_id INTEGER PRIMARY KEY AUTOINCREMENT, - log_level TEXT, - log_message TEXT, - timestamp REAL, - user_id TEXT - ) - ''') - - # 提交更改 - self.db_connection.commit() - - print("✓ 数据库初始化完成") - - except Exception as e: - print(f"✗ 数据库初始化失败: {e}") - self.storage_stats["errors"] += 1 - raise - - def _start_write_thread(self): - """启动异步写入线程""" - try: - self.write_thread_running = True - self.write_thread = threading.Thread(target=self._write_thread_loop, daemon=True) - self.write_thread.start() - print("✓ 异步写入线程已启动") - except Exception as e: - print(f"✗ 异步写入线程启动失败: {e}") - self.storage_stats["errors"] += 1 - - def _write_thread_loop(self): - """异步写入线程循环""" - try: - while self.write_thread_running: - try: - # 处理写入队列 - self._process_write_queue() - - # 短暂休眠 - time.sleep(0.1) - - except Exception as e: - print(f"✗ 异步写入循环错误: {e}") - self.storage_stats["errors"] += 1 - time.sleep(1.0) # 出错时延长休眠 - - except Exception as e: - print(f"✗ 异步写入线程失败: {e}") - self.storage_stats["errors"] += 1 - - def _process_write_queue(self): - """处理写入队列""" - try: - if not self.write_queue: - return - - with self.write_queue_lock: - write_items = self.write_queue[:] - self.write_queue.clear() - - # 执行写入操作 - for item in write_items: - try: - operation = item["operation"] - if operation == "save_user": - self._save_user_internal(item["user_id"], item["user_data"]) - elif operation == "save_room": - self._save_room_internal(item["room_id"], item["room_data"]) - elif operation == "save_game_state": - self._save_game_state_internal(item["state_id"], item["state_data"]) - - except Exception as e: - print(f"✗ 队列写入操作失败: {e}") - self.storage_stats["errors"] += 1 - - except Exception as e: - print(f"✗ 写入队列处理失败: {e}") - self.storage_stats["errors"] += 1 - - def enable(self) -> bool: - """ - 启用数据存储管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 数据存储管理器未初始化") - return False - - self.enabled = True - print("✓ 数据存储管理器已启用") - return True - - except Exception as e: - print(f"✗ 数据存储管理器启用失败: {e}") - self.storage_stats["errors"] += 1 - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用数据存储管理器""" - try: - self.enabled = False - - # 停止异步写入线程 - if self.write_thread_running: - self.write_thread_running = False - if self.write_thread and self.write_thread.is_alive(): - self.write_thread.join(timeout=5.0) - - # 处理剩余的写入队列 - self._process_write_queue() - - # 关闭数据库连接 - if self.db_connection: - self.db_connection.close() - self.db_connection = None - - print("✓ 数据存储管理器已禁用") - - except Exception as e: - print(f"✗ 数据存储管理器禁用失败: {e}") - self.storage_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def finalize(self): - """清理数据存储管理器资源""" - try: - # 禁用管理器 - if self.enabled: - self.disable() - - # 清理缓存和回调 - with self.cache_lock: - self.data_cache.clear() - self.storage_callbacks.clear() - - self.initialized = False - print("✓ 数据存储管理器资源已清理") - - except Exception as e: - print(f"✗ 数据存储管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新数据存储管理器状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 定期创建备份 - if (self.storage_config["enable_auto_backup"] and - current_time - self.last_backup > self.storage_config["backup_interval"]): - self._create_backup() - self.last_backup = current_time - - except Exception as e: - print(f"✗ 数据存储管理器更新失败: {e}") - self.storage_stats["errors"] += 1 - import traceback - traceback.print_exc() - - def _create_backup(self): - """创建数据库备份""" - try: - if not self.db_connection: - return - - # 创建备份文件名 - backup_dir = os.path.dirname(self.storage_config["database_path"]) - backup_filename = f"backup_{int(time.time())}.db" - backup_path = os.path.join(backup_dir, backup_filename) - - # 复制数据库文件 - import shutil - shutil.copy2(self.storage_config["database_path"], backup_path) - - self.storage_stats["backups_created"] += 1 - - # 清理旧备份 - self._cleanup_old_backups() - - # 触发备份创建回调 - self._trigger_storage_callback("backup_created", { - "backup_path": backup_path, - "timestamp": time.time() - }) - - print(f"✓ 数据库备份已创建: {backup_path}") - - except Exception as e: - print(f"✗ 数据库备份创建失败: {e}") - self.storage_stats["errors"] += 1 - - def _cleanup_old_backups(self): - """清理旧备份""" - try: - backup_dir = os.path.dirname(self.storage_config["database_path"]) - backup_files = [] - - # 查找备份文件 - for filename in os.listdir(backup_dir): - if filename.startswith("backup_") and filename.endswith(".db"): - filepath = os.path.join(backup_dir, filename) - backup_files.append((filepath, os.path.getmtime(filepath))) - - # 按修改时间排序 - backup_files.sort(key=lambda x: x[1], reverse=True) - - # 删除多余的备份 - for filepath, _ in backup_files[self.storage_config["max_backups"]:]: - try: - os.remove(filepath) - print(f"✓ 旧备份已删除: {filepath}") - except Exception as e: - print(f"✗ 旧备份删除失败: {filepath} - {e}") - - except Exception as e: - print(f"✗ 旧备份清理失败: {e}") - self.storage_stats["errors"] += 1 - - def save_user(self, user_id: str, user_data: Dict[str, Any]) -> bool: - """ - 保存用户数据 - - Args: - user_id: 用户ID - user_data: 用户数据 - - Returns: - 是否保存成功 - """ - try: - if not self.enabled: - return False - - # 更新缓存 - if self.storage_config["enable_caching"]: - with self.cache_lock: - self.data_cache[f"user_{user_id}"] = { - "data": user_data.copy(), - "timestamp": time.time() - } - - # 保持缓存大小 - if len(self.data_cache) > self.storage_config["cache_size"]: - # 移除最旧的缓存项 - oldest_key = min(self.data_cache.keys(), - key=lambda k: self.data_cache[k]["timestamp"]) - del self.data_cache[oldest_key] - - # 异步写入 - if self.storage_config["enable_async_writes"]: - with self.write_queue_lock: - self.write_queue.append({ - "operation": "save_user", - "user_id": user_id, - "user_data": user_data - }) - return True - else: - # 同步写入 - return self._save_user_internal(user_id, user_data) - - except Exception as e: - print(f"✗ 保存用户数据失败: {e}") - self.storage_stats["errors"] += 1 - return False - - def _save_user_internal(self, user_id: str, user_data: Dict[str, Any]) -> bool: - """ - 内部保存用户数据 - - Args: - user_id: 用户ID - user_data: 用户数据 - - Returns: - 是否保存成功 - """ - try: - if not self.db_connection: - return False - - cursor = self.db_connection.cursor() - - # 序列化用户数据 - user_data_json = json.dumps(user_data, ensure_ascii=False) - - # 插入或更新用户数据 - cursor.execute(''' - INSERT OR REPLACE INTO users - (user_id, username, email, password_hash, created_time, last_login, user_data) - VALUES (?, ?, ?, ?, ?, ?, ?) - ''', ( - user_id, - user_data.get("username", ""), - user_data.get("email", ""), - user_data.get("password_hash", ""), - user_data.get("created_time", time.time()), - user_data.get("last_login", 0), - user_data_json - )) - - self.db_connection.commit() - self.storage_stats["writes"] += 1 - self.last_write = time.time() - - # 触发数据保存回调 - self._trigger_storage_callback("data_saved", { - "data_type": "user", - "user_id": user_id, - "timestamp": self.last_write - }) - - return True - - except Exception as e: - print(f"✗ 内部保存用户数据失败: {e}") - self.storage_stats["errors"] += 1 - return False - - def load_user(self, user_id: str) -> Optional[Dict[str, Any]]: - """ - 加载用户数据 - - Args: - user_id: 用户ID - - Returns: - 用户数据或None - """ - try: - if not self.enabled: - return None - - # 检查缓存 - if self.storage_config["enable_caching"]: - cache_key = f"user_{user_id}" - with self.cache_lock: - if cache_key in self.data_cache: - self.storage_stats["cache_hits"] += 1 - cached_data = self.data_cache[cache_key] - return cached_data["data"].copy() - else: - self.storage_stats["cache_misses"] += 1 - - # 从数据库加载 - if not self.db_connection: - return None - - cursor = self.db_connection.cursor() - cursor.execute('SELECT user_data FROM users WHERE user_id = ?', (user_id,)) - result = cursor.fetchone() - - if result: - user_data = json.loads(result[0]) - self.storage_stats["reads"] += 1 - self.last_read = time.time() - - # 更新缓存 - if self.storage_config["enable_caching"]: - with self.cache_lock: - self.data_cache[f"user_{user_id}"] = { - "data": user_data, - "timestamp": self.last_read - } - - # 触发数据加载回调 - self._trigger_storage_callback("data_loaded", { - "data_type": "user", - "user_id": user_id, - "timestamp": self.last_read - }) - - return user_data - else: - return None - - except Exception as e: - print(f"✗ 加载用户数据失败: {e}") - self.storage_stats["errors"] += 1 - return None - - def save_room(self, room_id: str, room_data: Dict[str, Any]) -> bool: - """ - 保存房间数据 - - Args: - room_id: 房间ID - room_data: 房间数据 - - Returns: - 是否保存成功 - """ - try: - if not self.enabled: - return False - - # 更新缓存 - if self.storage_config["enable_caching"]: - with self.cache_lock: - self.data_cache[f"room_{room_id}"] = { - "data": room_data.copy(), - "timestamp": time.time() - } - - # 异步写入 - if self.storage_config["enable_async_writes"]: - with self.write_queue_lock: - self.write_queue.append({ - "operation": "save_room", - "room_id": room_id, - "room_data": room_data - }) - return True - else: - # 同步写入 - return self._save_room_internal(room_id, room_data) - - except Exception as e: - print(f"✗ 保存房间数据失败: {e}") - self.storage_stats["errors"] += 1 - return False - - def _save_room_internal(self, room_id: str, room_data: Dict[str, Any]) -> bool: - """ - 内部保存房间数据 - - Args: - room_id: 房间ID - room_data: 房间数据 - - Returns: - 是否保存成功 - """ - try: - if not self.db_connection: - return False - - cursor = self.db_connection.cursor() - - # 序列化房间数据 - room_settings_json = json.dumps(room_data.get("settings", {}), ensure_ascii=False) - room_state_json = json.dumps(room_data.get("state", {}), ensure_ascii=False) - - # 插入或更新房间数据 - cursor.execute(''' - INSERT OR REPLACE INTO rooms - (room_id, room_name, host_user_id, created_time, room_settings, room_state) - VALUES (?, ?, ?, ?, ?, ?) - ''', ( - room_id, - room_data.get("room_name", ""), - room_data.get("host_user_id"), - room_data.get("created_time", time.time()), - room_settings_json, - room_state_json - )) - - self.db_connection.commit() - self.storage_stats["writes"] += 1 - self.last_write = time.time() - - # 触发数据保存回调 - self._trigger_storage_callback("data_saved", { - "data_type": "room", - "room_id": room_id, - "timestamp": self.last_write - }) - - return True - - except Exception as e: - print(f"✗ 内部保存房间数据失败: {e}") - self.storage_stats["errors"] += 1 - return False - - def load_room(self, room_id: str) -> Optional[Dict[str, Any]]: - """ - 加载房间数据 - - Args: - room_id: 房间ID - - Returns: - 房间数据或None - """ - try: - if not self.enabled: - return None - - # 检查缓存 - if self.storage_config["enable_caching"]: - cache_key = f"room_{room_id}" - with self.cache_lock: - if cache_key in self.data_cache: - self.storage_stats["cache_hits"] += 1 - cached_data = self.data_cache[cache_key] - return cached_data["data"].copy() - else: - self.storage_stats["cache_misses"] += 1 - - # 从数据库加载 - if not self.db_connection: - return None - - cursor = self.db_connection.cursor() - cursor.execute(''' - SELECT room_name, host_user_id, created_time, room_settings, room_state - FROM rooms WHERE room_id = ? - ''', (room_id,)) - result = cursor.fetchone() - - if result: - room_data = { - "room_id": room_id, - "room_name": result[0], - "host_user_id": result[1], - "created_time": result[2], - "settings": json.loads(result[3]) if result[3] else {}, - "state": json.loads(result[4]) if result[4] else {} - } - - self.storage_stats["reads"] += 1 - self.last_read = time.time() - - # 更新缓存 - if self.storage_config["enable_caching"]: - with self.cache_lock: - self.data_cache[f"room_{room_id}"] = { - "data": room_data, - "timestamp": self.last_read - } - - # 触发数据加载回调 - self._trigger_storage_callback("data_loaded", { - "data_type": "room", - "room_id": room_id, - "timestamp": self.last_read - }) - - return room_data - else: - return None - - except Exception as e: - print(f"✗ 加载房间数据失败: {e}") - self.storage_stats["errors"] += 1 - return None - - def save_game_state(self, state_id: str, state_data: Dict[str, Any]) -> bool: - """ - 保存游戏状态数据 - - Args: - state_id: 状态ID - state_data: 游戏状态数据 - - Returns: - 是否保存成功 - """ - try: - if not self.enabled: - return False - - # 异步写入 - if self.storage_config["enable_async_writes"]: - with self.write_queue_lock: - self.write_queue.append({ - "operation": "save_game_state", - "state_id": state_id, - "state_data": state_data - }) - return True - else: - # 同步写入 - return self._save_game_state_internal(state_id, state_data) - - except Exception as e: - print(f"✗ 保存游戏状态数据失败: {e}") - self.storage_stats["errors"] += 1 - return False - - def _save_game_state_internal(self, state_id: str, state_data: Dict[str, Any]) -> bool: - """ - 内部保存游戏状态数据 - - Args: - state_id: 状态ID - state_data: 游戏状态数据 - - Returns: - 是否保存成功 - """ - try: - if not self.db_connection: - return False - - cursor = self.db_connection.cursor() - - # 序列化游戏状态数据 - state_data_json = json.dumps(state_data, ensure_ascii=False) - - # 插入或更新游戏状态数据 - cursor.execute(''' - INSERT OR REPLACE INTO game_states - (state_id, room_id, state_data, timestamp) - VALUES (?, ?, ?, ?) - ''', ( - state_id, - state_data.get("room_id"), - state_data_json, - state_data.get("timestamp", time.time()) - )) - - self.db_connection.commit() - self.storage_stats["writes"] += 1 - self.last_write = time.time() - - # 触发数据保存回调 - self._trigger_storage_callback("data_saved", { - "data_type": "game_state", - "state_id": state_id, - "timestamp": self.last_write - }) - - return True - - except Exception as e: - print(f"✗ 内部保存游戏状态数据失败: {e}") - self.storage_stats["errors"] += 1 - return False - - def load_game_state(self, state_id: str) -> Optional[Dict[str, Any]]: - """ - 加载游戏状态数据 - - Args: - state_id: 状态ID - - Returns: - 游戏状态数据或None - """ - try: - if not self.enabled: - return None - - # 从数据库加载 - if not self.db_connection: - return None - - cursor = self.db_connection.cursor() - cursor.execute('SELECT room_id, state_data, timestamp FROM game_states WHERE state_id = ?', (state_id,)) - result = cursor.fetchone() - - if result: - state_data = { - "state_id": state_id, - "room_id": result[0], - "data": json.loads(result[1]) if result[1] else {}, - "timestamp": result[2] - } - - self.storage_stats["reads"] += 1 - self.last_read = time.time() - - # 触发数据加载回调 - self._trigger_storage_callback("data_loaded", { - "data_type": "game_state", - "state_id": state_id, - "timestamp": self.last_read - }) - - return state_data - else: - return None - - except Exception as e: - print(f"✗ 加载游戏状态数据失败: {e}") - self.storage_stats["errors"] += 1 - return None - - def get_user_rooms(self, user_id: str) -> List[str]: - """ - 获取用户所在的房间列表 - - Args: - user_id: 用户ID - - Returns: - 房间ID列表 - """ - try: - if not self.db_connection: - return [] - - cursor = self.db_connection.cursor() - cursor.execute('SELECT room_id FROM user_rooms WHERE user_id = ?', (user_id,)) - results = cursor.fetchall() - - return [row[0] for row in results] - - except Exception as e: - print(f"✗ 获取用户房间列表失败: {e}") - self.storage_stats["errors"] += 1 - return [] - - def add_user_to_room(self, user_id: str, room_id: str) -> bool: - """ - 添加用户到房间 - - Args: - user_id: 用户ID - room_id: 房间ID - - Returns: - 是否添加成功 - """ - try: - if not self.db_connection: - return False - - cursor = self.db_connection.cursor() - cursor.execute(''' - INSERT OR IGNORE INTO user_rooms (user_id, room_id, join_time) - VALUES (?, ?, ?) - ''', (user_id, room_id, time.time())) - - self.db_connection.commit() - return True - - except Exception as e: - print(f"✗ 添加用户到房间失败: {e}") - self.storage_stats["errors"] += 1 - return False - - def remove_user_from_room(self, user_id: str, room_id: str) -> bool: - """ - 从房间移除用户 - - Args: - user_id: 用户ID - room_id: 房间ID - - Returns: - 是否移除成功 - """ - try: - if not self.db_connection: - return False - - cursor = self.db_connection.cursor() - cursor.execute('DELETE FROM user_rooms WHERE user_id = ? AND room_id = ?', (user_id, room_id)) - - self.db_connection.commit() - return True - - except Exception as e: - print(f"✗ 从房间移除用户失败: {e}") - self.storage_stats["errors"] += 1 - return False - - def save_system_config(self, config_key: str, config_value: Any) -> bool: - """ - 保存系统配置 - - Args: - config_key: 配置键 - config_value: 配置值 - - Returns: - 是否保存成功 - """ - try: - if not self.db_connection: - return False - - cursor = self.db_connection.cursor() - config_value_str = json.dumps(config_value, ensure_ascii=False) - - cursor.execute(''' - INSERT OR REPLACE INTO system_config (config_key, config_value, updated_time) - VALUES (?, ?, ?) - ''', (config_key, config_value_str, time.time())) - - self.db_connection.commit() - self.storage_stats["writes"] += 1 - return True - - except Exception as e: - print(f"✗ 保存系统配置失败: {e}") - self.storage_stats["errors"] += 1 - return False - - def load_system_config(self, config_key: str, default_value: Any = None) -> Any: - """ - 加载系统配置 - - Args: - config_key: 配置键 - default_value: 默认值 - - Returns: - 配置值或默认值 - """ - try: - if not self.db_connection: - return default_value - - cursor = self.db_connection.cursor() - cursor.execute('SELECT config_value FROM system_config WHERE config_key = ?', (config_key,)) - result = cursor.fetchone() - - if result: - return json.loads(result[0]) - else: - return default_value - - except Exception as e: - print(f"✗ 加载系统配置失败: {e}") - self.storage_stats["errors"] += 1 - return default_value - - def add_log(self, log_level: str, log_message: str, user_id: str = None) -> bool: - """ - 添加日志 - - Args: - log_level: 日志级别 - log_message: 日志消息 - user_id: 用户ID(可选) - - Returns: - 是否添加成功 - """ - try: - if not self.db_connection: - return False - - cursor = self.db_connection.cursor() - cursor.execute(''' - INSERT INTO logs (log_level, log_message, timestamp, user_id) - VALUES (?, ?, ?, ?) - ''', (log_level, log_message, time.time(), user_id)) - - self.db_connection.commit() - return True - - except Exception as e: - print(f"✗ 添加日志失败: {e}") - self.storage_stats["errors"] += 1 - return False - - def get_storage_stats(self) -> Dict[str, Any]: - """ - 获取存储统计信息 - - Returns: - 存储统计字典 - """ - return { - "stats": self.storage_stats.copy(), - "config": self.storage_config.copy(), - "cache_size": len(self.data_cache), - "write_queue_size": len(self.write_queue) - } - - def reset_stats(self): - """重置存储统计信息""" - try: - self.storage_stats = { - "reads": 0, - "writes": 0, - "cache_hits": 0, - "cache_misses": 0, - "backups_created": 0, - "errors": 0, - "bytes_read": 0, - "bytes_written": 0 - } - print("✓ 存储统计信息已重置") - except Exception as e: - print(f"✗ 存储统计信息重置失败: {e}") - - def set_storage_config(self, config: Dict[str, Any]) -> bool: - """ - 设置存储配置 - - Args: - config: 存储配置字典 - - Returns: - 是否设置成功 - """ - try: - old_async = self.storage_config["enable_async_writes"] - new_async = config.get("enable_async_writes", old_async) - - # 如果异步写入设置发生变化,处理线程 - if old_async != new_async: - if new_async and not self.write_thread_running: - self._start_write_thread() - elif not new_async and self.write_thread_running: - self.write_thread_running = False - if self.write_thread and self.write_thread.is_alive(): - self.write_thread.join(timeout=2.0) - - self.storage_config.update(config) - print(f"✓ 存储配置已更新: {self.storage_config}") - return True - except Exception as e: - print(f"✗ 存储配置设置失败: {e}") - return False - - def get_storage_config(self) -> Dict[str, Any]: - """ - 获取存储配置 - - Returns: - 存储配置字典 - """ - return self.storage_config.copy() - - def clear_cache(self): - """清空缓存""" - try: - with self.cache_lock: - self.data_cache.clear() - print("✓ 存储缓存已清空") - except Exception as e: - print(f"✗ 存储缓存清空失败: {e}") - self.storage_stats["errors"] += 1 - - def _trigger_storage_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发存储回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.storage_callbacks: - for callback in self.storage_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 存储回调执行失败: {callback_type} - {e}") - except Exception as e: - print(f"✗ 存储回调触发失败: {e}") - - def register_storage_callback(self, callback_type: str, callback: callable): - """ - 注册存储回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.storage_callbacks: - self.storage_callbacks[callback_type].append(callback) - print(f"✓ 存储回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 存储回调注册失败: {e}") - - def unregister_storage_callback(self, callback_type: str, callback: callable): - """ - 注销存储回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.storage_callbacks: - if callback in self.storage_callbacks[callback_type]: - self.storage_callbacks[callback_type].remove(callback) - print(f"✓ 存储回调已注销: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 存储回调注销失败: {e}") - +""" +数据存储系统模块 +负责持久化存储用户数据和游戏状态 +""" + +import time +import threading +import json +import os +import sqlite3 +from typing import Dict, Any, List, Optional + +class StorageManager: + """ + 数据存储管理器 + 负责持久化存储用户数据和游戏状态 + """ + + def __init__(self, plugin): + """ + 初始化数据存储管理器 + + Args: + plugin: 服务器架构插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 存储配置 + self.storage_config = { + "database_path": "/home/hello/EG/plugins/user/server_architecture/data/server_database.db", + "enable_caching": True, + "cache_size": 1000, + "enable_compression": True, + "backup_interval": 3600, # 1小时 + "enable_auto_backup": True, + "max_backups": 10, + "enable_encryption": False, + "enable_async_writes": True + } + + # 数据库连接 + self.db_connection = None + + # 缓存管理 + self.data_cache = {} + self.cache_lock = threading.RLock() + + # 存储统计 + self.storage_stats = { + "reads": 0, + "writes": 0, + "cache_hits": 0, + "cache_misses": 0, + "backups_created": 0, + "errors": 0, + "bytes_read": 0, + "bytes_written": 0 + } + + # 异步写入队列 + self.write_queue = [] + self.write_queue_lock = threading.RLock() + self.write_thread = None + self.write_thread_running = False + + # 回调函数 + self.storage_callbacks = { + "data_saved": [], + "data_loaded": [], + "backup_created": [], + "storage_error": [] + } + + # 时间戳记录 + self.last_backup = 0.0 + self.last_write = 0.0 + self.last_read = 0.0 + + print("✓ 数据存储管理器已创建") + + def initialize(self) -> bool: + """ + 初始化数据存储管理器 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化数据存储管理器...") + + # 创建数据目录 + data_dir = os.path.dirname(self.storage_config["database_path"]) + if not os.path.exists(data_dir): + os.makedirs(data_dir) + + # 初始化数据库 + self._initialize_database() + + # 启动异步写入线程 + if self.storage_config["enable_async_writes"]: + self._start_write_thread() + + self.initialized = True + print("✓ 数据存储管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 数据存储管理器初始化失败: {e}") + self.storage_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def _initialize_database(self): + """初始化数据库""" + try: + # 连接到SQLite数据库 + self.db_connection = sqlite3.connect( + self.storage_config["database_path"], + check_same_thread=False + ) + + # 创建表 + cursor = self.db_connection.cursor() + + # 用户表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS users ( + user_id TEXT PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + email TEXT, + password_hash TEXT, + created_time REAL, + last_login REAL, + user_data TEXT + ) + ''') + + # 房间表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS rooms ( + room_id TEXT PRIMARY KEY, + room_name TEXT NOT NULL, + host_user_id TEXT, + created_time REAL, + room_settings TEXT, + room_state TEXT + ) + ''') + + # 游戏状态表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS game_states ( + state_id TEXT PRIMARY KEY, + room_id TEXT, + state_data TEXT, + timestamp REAL, + FOREIGN KEY (room_id) REFERENCES rooms (room_id) + ) + ''') + + # 用户房间关系表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS user_rooms ( + user_id TEXT, + room_id TEXT, + join_time REAL, + PRIMARY KEY (user_id, room_id), + FOREIGN KEY (user_id) REFERENCES users (user_id), + FOREIGN KEY (room_id) REFERENCES rooms (room_id) + ) + ''') + + # 系统配置表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS system_config ( + config_key TEXT PRIMARY KEY, + config_value TEXT, + updated_time REAL + ) + ''') + + # 日志表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS logs ( + log_id INTEGER PRIMARY KEY AUTOINCREMENT, + log_level TEXT, + log_message TEXT, + timestamp REAL, + user_id TEXT + ) + ''') + + # 提交更改 + self.db_connection.commit() + + print("✓ 数据库初始化完成") + + except Exception as e: + print(f"✗ 数据库初始化失败: {e}") + self.storage_stats["errors"] += 1 + raise + + def _start_write_thread(self): + """启动异步写入线程""" + try: + self.write_thread_running = True + self.write_thread = threading.Thread(target=self._write_thread_loop, daemon=True) + self.write_thread.start() + print("✓ 异步写入线程已启动") + except Exception as e: + print(f"✗ 异步写入线程启动失败: {e}") + self.storage_stats["errors"] += 1 + + def _write_thread_loop(self): + """异步写入线程循环""" + try: + while self.write_thread_running: + try: + # 处理写入队列 + self._process_write_queue() + + # 短暂休眠 + time.sleep(0.1) + + except Exception as e: + print(f"✗ 异步写入循环错误: {e}") + self.storage_stats["errors"] += 1 + time.sleep(1.0) # 出错时延长休眠 + + except Exception as e: + print(f"✗ 异步写入线程失败: {e}") + self.storage_stats["errors"] += 1 + + def _process_write_queue(self): + """处理写入队列""" + try: + if not self.write_queue: + return + + with self.write_queue_lock: + write_items = self.write_queue[:] + self.write_queue.clear() + + # 执行写入操作 + for item in write_items: + try: + operation = item["operation"] + if operation == "save_user": + self._save_user_internal(item["user_id"], item["user_data"]) + elif operation == "save_room": + self._save_room_internal(item["room_id"], item["room_data"]) + elif operation == "save_game_state": + self._save_game_state_internal(item["state_id"], item["state_data"]) + + except Exception as e: + print(f"✗ 队列写入操作失败: {e}") + self.storage_stats["errors"] += 1 + + except Exception as e: + print(f"✗ 写入队列处理失败: {e}") + self.storage_stats["errors"] += 1 + + def enable(self) -> bool: + """ + 启用数据存储管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 数据存储管理器未初始化") + return False + + self.enabled = True + print("✓ 数据存储管理器已启用") + return True + + except Exception as e: + print(f"✗ 数据存储管理器启用失败: {e}") + self.storage_stats["errors"] += 1 + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用数据存储管理器""" + try: + self.enabled = False + + # 停止异步写入线程 + if self.write_thread_running: + self.write_thread_running = False + if self.write_thread and self.write_thread.is_alive(): + self.write_thread.join(timeout=5.0) + + # 处理剩余的写入队列 + self._process_write_queue() + + # 关闭数据库连接 + if self.db_connection: + self.db_connection.close() + self.db_connection = None + + print("✓ 数据存储管理器已禁用") + + except Exception as e: + print(f"✗ 数据存储管理器禁用失败: {e}") + self.storage_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def finalize(self): + """清理数据存储管理器资源""" + try: + # 禁用管理器 + if self.enabled: + self.disable() + + # 清理缓存和回调 + with self.cache_lock: + self.data_cache.clear() + self.storage_callbacks.clear() + + self.initialized = False + print("✓ 数据存储管理器资源已清理") + + except Exception as e: + print(f"✗ 数据存储管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新数据存储管理器状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 定期创建备份 + if (self.storage_config["enable_auto_backup"] and + current_time - self.last_backup > self.storage_config["backup_interval"]): + self._create_backup() + self.last_backup = current_time + + except Exception as e: + print(f"✗ 数据存储管理器更新失败: {e}") + self.storage_stats["errors"] += 1 + import traceback + traceback.print_exc() + + def _create_backup(self): + """创建数据库备份""" + try: + if not self.db_connection: + return + + # 创建备份文件名 + backup_dir = os.path.dirname(self.storage_config["database_path"]) + backup_filename = f"backup_{int(time.time())}.db" + backup_path = os.path.join(backup_dir, backup_filename) + + # 复制数据库文件 + import shutil + shutil.copy2(self.storage_config["database_path"], backup_path) + + self.storage_stats["backups_created"] += 1 + + # 清理旧备份 + self._cleanup_old_backups() + + # 触发备份创建回调 + self._trigger_storage_callback("backup_created", { + "backup_path": backup_path, + "timestamp": time.time() + }) + + print(f"✓ 数据库备份已创建: {backup_path}") + + except Exception as e: + print(f"✗ 数据库备份创建失败: {e}") + self.storage_stats["errors"] += 1 + + def _cleanup_old_backups(self): + """清理旧备份""" + try: + backup_dir = os.path.dirname(self.storage_config["database_path"]) + backup_files = [] + + # 查找备份文件 + for filename in os.listdir(backup_dir): + if filename.startswith("backup_") and filename.endswith(".db"): + filepath = os.path.join(backup_dir, filename) + backup_files.append((filepath, os.path.getmtime(filepath))) + + # 按修改时间排序 + backup_files.sort(key=lambda x: x[1], reverse=True) + + # 删除多余的备份 + for filepath, _ in backup_files[self.storage_config["max_backups"]:]: + try: + os.remove(filepath) + print(f"✓ 旧备份已删除: {filepath}") + except Exception as e: + print(f"✗ 旧备份删除失败: {filepath} - {e}") + + except Exception as e: + print(f"✗ 旧备份清理失败: {e}") + self.storage_stats["errors"] += 1 + + def save_user(self, user_id: str, user_data: Dict[str, Any]) -> bool: + """ + 保存用户数据 + + Args: + user_id: 用户ID + user_data: 用户数据 + + Returns: + 是否保存成功 + """ + try: + if not self.enabled: + return False + + # 更新缓存 + if self.storage_config["enable_caching"]: + with self.cache_lock: + self.data_cache[f"user_{user_id}"] = { + "data": user_data.copy(), + "timestamp": time.time() + } + + # 保持缓存大小 + if len(self.data_cache) > self.storage_config["cache_size"]: + # 移除最旧的缓存项 + oldest_key = min(self.data_cache.keys(), + key=lambda k: self.data_cache[k]["timestamp"]) + del self.data_cache[oldest_key] + + # 异步写入 + if self.storage_config["enable_async_writes"]: + with self.write_queue_lock: + self.write_queue.append({ + "operation": "save_user", + "user_id": user_id, + "user_data": user_data + }) + return True + else: + # 同步写入 + return self._save_user_internal(user_id, user_data) + + except Exception as e: + print(f"✗ 保存用户数据失败: {e}") + self.storage_stats["errors"] += 1 + return False + + def _save_user_internal(self, user_id: str, user_data: Dict[str, Any]) -> bool: + """ + 内部保存用户数据 + + Args: + user_id: 用户ID + user_data: 用户数据 + + Returns: + 是否保存成功 + """ + try: + if not self.db_connection: + return False + + cursor = self.db_connection.cursor() + + # 序列化用户数据 + user_data_json = json.dumps(user_data, ensure_ascii=False) + + # 插入或更新用户数据 + cursor.execute(''' + INSERT OR REPLACE INTO users + (user_id, username, email, password_hash, created_time, last_login, user_data) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', ( + user_id, + user_data.get("username", ""), + user_data.get("email", ""), + user_data.get("password_hash", ""), + user_data.get("created_time", time.time()), + user_data.get("last_login", 0), + user_data_json + )) + + self.db_connection.commit() + self.storage_stats["writes"] += 1 + self.last_write = time.time() + + # 触发数据保存回调 + self._trigger_storage_callback("data_saved", { + "data_type": "user", + "user_id": user_id, + "timestamp": self.last_write + }) + + return True + + except Exception as e: + print(f"✗ 内部保存用户数据失败: {e}") + self.storage_stats["errors"] += 1 + return False + + def load_user(self, user_id: str) -> Optional[Dict[str, Any]]: + """ + 加载用户数据 + + Args: + user_id: 用户ID + + Returns: + 用户数据或None + """ + try: + if not self.enabled: + return None + + # 检查缓存 + if self.storage_config["enable_caching"]: + cache_key = f"user_{user_id}" + with self.cache_lock: + if cache_key in self.data_cache: + self.storage_stats["cache_hits"] += 1 + cached_data = self.data_cache[cache_key] + return cached_data["data"].copy() + else: + self.storage_stats["cache_misses"] += 1 + + # 从数据库加载 + if not self.db_connection: + return None + + cursor = self.db_connection.cursor() + cursor.execute('SELECT user_data FROM users WHERE user_id = ?', (user_id,)) + result = cursor.fetchone() + + if result: + user_data = json.loads(result[0]) + self.storage_stats["reads"] += 1 + self.last_read = time.time() + + # 更新缓存 + if self.storage_config["enable_caching"]: + with self.cache_lock: + self.data_cache[f"user_{user_id}"] = { + "data": user_data, + "timestamp": self.last_read + } + + # 触发数据加载回调 + self._trigger_storage_callback("data_loaded", { + "data_type": "user", + "user_id": user_id, + "timestamp": self.last_read + }) + + return user_data + else: + return None + + except Exception as e: + print(f"✗ 加载用户数据失败: {e}") + self.storage_stats["errors"] += 1 + return None + + def save_room(self, room_id: str, room_data: Dict[str, Any]) -> bool: + """ + 保存房间数据 + + Args: + room_id: 房间ID + room_data: 房间数据 + + Returns: + 是否保存成功 + """ + try: + if not self.enabled: + return False + + # 更新缓存 + if self.storage_config["enable_caching"]: + with self.cache_lock: + self.data_cache[f"room_{room_id}"] = { + "data": room_data.copy(), + "timestamp": time.time() + } + + # 异步写入 + if self.storage_config["enable_async_writes"]: + with self.write_queue_lock: + self.write_queue.append({ + "operation": "save_room", + "room_id": room_id, + "room_data": room_data + }) + return True + else: + # 同步写入 + return self._save_room_internal(room_id, room_data) + + except Exception as e: + print(f"✗ 保存房间数据失败: {e}") + self.storage_stats["errors"] += 1 + return False + + def _save_room_internal(self, room_id: str, room_data: Dict[str, Any]) -> bool: + """ + 内部保存房间数据 + + Args: + room_id: 房间ID + room_data: 房间数据 + + Returns: + 是否保存成功 + """ + try: + if not self.db_connection: + return False + + cursor = self.db_connection.cursor() + + # 序列化房间数据 + room_settings_json = json.dumps(room_data.get("settings", {}), ensure_ascii=False) + room_state_json = json.dumps(room_data.get("state", {}), ensure_ascii=False) + + # 插入或更新房间数据 + cursor.execute(''' + INSERT OR REPLACE INTO rooms + (room_id, room_name, host_user_id, created_time, room_settings, room_state) + VALUES (?, ?, ?, ?, ?, ?) + ''', ( + room_id, + room_data.get("room_name", ""), + room_data.get("host_user_id"), + room_data.get("created_time", time.time()), + room_settings_json, + room_state_json + )) + + self.db_connection.commit() + self.storage_stats["writes"] += 1 + self.last_write = time.time() + + # 触发数据保存回调 + self._trigger_storage_callback("data_saved", { + "data_type": "room", + "room_id": room_id, + "timestamp": self.last_write + }) + + return True + + except Exception as e: + print(f"✗ 内部保存房间数据失败: {e}") + self.storage_stats["errors"] += 1 + return False + + def load_room(self, room_id: str) -> Optional[Dict[str, Any]]: + """ + 加载房间数据 + + Args: + room_id: 房间ID + + Returns: + 房间数据或None + """ + try: + if not self.enabled: + return None + + # 检查缓存 + if self.storage_config["enable_caching"]: + cache_key = f"room_{room_id}" + with self.cache_lock: + if cache_key in self.data_cache: + self.storage_stats["cache_hits"] += 1 + cached_data = self.data_cache[cache_key] + return cached_data["data"].copy() + else: + self.storage_stats["cache_misses"] += 1 + + # 从数据库加载 + if not self.db_connection: + return None + + cursor = self.db_connection.cursor() + cursor.execute(''' + SELECT room_name, host_user_id, created_time, room_settings, room_state + FROM rooms WHERE room_id = ? + ''', (room_id,)) + result = cursor.fetchone() + + if result: + room_data = { + "room_id": room_id, + "room_name": result[0], + "host_user_id": result[1], + "created_time": result[2], + "settings": json.loads(result[3]) if result[3] else {}, + "state": json.loads(result[4]) if result[4] else {} + } + + self.storage_stats["reads"] += 1 + self.last_read = time.time() + + # 更新缓存 + if self.storage_config["enable_caching"]: + with self.cache_lock: + self.data_cache[f"room_{room_id}"] = { + "data": room_data, + "timestamp": self.last_read + } + + # 触发数据加载回调 + self._trigger_storage_callback("data_loaded", { + "data_type": "room", + "room_id": room_id, + "timestamp": self.last_read + }) + + return room_data + else: + return None + + except Exception as e: + print(f"✗ 加载房间数据失败: {e}") + self.storage_stats["errors"] += 1 + return None + + def save_game_state(self, state_id: str, state_data: Dict[str, Any]) -> bool: + """ + 保存游戏状态数据 + + Args: + state_id: 状态ID + state_data: 游戏状态数据 + + Returns: + 是否保存成功 + """ + try: + if not self.enabled: + return False + + # 异步写入 + if self.storage_config["enable_async_writes"]: + with self.write_queue_lock: + self.write_queue.append({ + "operation": "save_game_state", + "state_id": state_id, + "state_data": state_data + }) + return True + else: + # 同步写入 + return self._save_game_state_internal(state_id, state_data) + + except Exception as e: + print(f"✗ 保存游戏状态数据失败: {e}") + self.storage_stats["errors"] += 1 + return False + + def _save_game_state_internal(self, state_id: str, state_data: Dict[str, Any]) -> bool: + """ + 内部保存游戏状态数据 + + Args: + state_id: 状态ID + state_data: 游戏状态数据 + + Returns: + 是否保存成功 + """ + try: + if not self.db_connection: + return False + + cursor = self.db_connection.cursor() + + # 序列化游戏状态数据 + state_data_json = json.dumps(state_data, ensure_ascii=False) + + # 插入或更新游戏状态数据 + cursor.execute(''' + INSERT OR REPLACE INTO game_states + (state_id, room_id, state_data, timestamp) + VALUES (?, ?, ?, ?) + ''', ( + state_id, + state_data.get("room_id"), + state_data_json, + state_data.get("timestamp", time.time()) + )) + + self.db_connection.commit() + self.storage_stats["writes"] += 1 + self.last_write = time.time() + + # 触发数据保存回调 + self._trigger_storage_callback("data_saved", { + "data_type": "game_state", + "state_id": state_id, + "timestamp": self.last_write + }) + + return True + + except Exception as e: + print(f"✗ 内部保存游戏状态数据失败: {e}") + self.storage_stats["errors"] += 1 + return False + + def load_game_state(self, state_id: str) -> Optional[Dict[str, Any]]: + """ + 加载游戏状态数据 + + Args: + state_id: 状态ID + + Returns: + 游戏状态数据或None + """ + try: + if not self.enabled: + return None + + # 从数据库加载 + if not self.db_connection: + return None + + cursor = self.db_connection.cursor() + cursor.execute('SELECT room_id, state_data, timestamp FROM game_states WHERE state_id = ?', (state_id,)) + result = cursor.fetchone() + + if result: + state_data = { + "state_id": state_id, + "room_id": result[0], + "data": json.loads(result[1]) if result[1] else {}, + "timestamp": result[2] + } + + self.storage_stats["reads"] += 1 + self.last_read = time.time() + + # 触发数据加载回调 + self._trigger_storage_callback("data_loaded", { + "data_type": "game_state", + "state_id": state_id, + "timestamp": self.last_read + }) + + return state_data + else: + return None + + except Exception as e: + print(f"✗ 加载游戏状态数据失败: {e}") + self.storage_stats["errors"] += 1 + return None + + def get_user_rooms(self, user_id: str) -> List[str]: + """ + 获取用户所在的房间列表 + + Args: + user_id: 用户ID + + Returns: + 房间ID列表 + """ + try: + if not self.db_connection: + return [] + + cursor = self.db_connection.cursor() + cursor.execute('SELECT room_id FROM user_rooms WHERE user_id = ?', (user_id,)) + results = cursor.fetchall() + + return [row[0] for row in results] + + except Exception as e: + print(f"✗ 获取用户房间列表失败: {e}") + self.storage_stats["errors"] += 1 + return [] + + def add_user_to_room(self, user_id: str, room_id: str) -> bool: + """ + 添加用户到房间 + + Args: + user_id: 用户ID + room_id: 房间ID + + Returns: + 是否添加成功 + """ + try: + if not self.db_connection: + return False + + cursor = self.db_connection.cursor() + cursor.execute(''' + INSERT OR IGNORE INTO user_rooms (user_id, room_id, join_time) + VALUES (?, ?, ?) + ''', (user_id, room_id, time.time())) + + self.db_connection.commit() + return True + + except Exception as e: + print(f"✗ 添加用户到房间失败: {e}") + self.storage_stats["errors"] += 1 + return False + + def remove_user_from_room(self, user_id: str, room_id: str) -> bool: + """ + 从房间移除用户 + + Args: + user_id: 用户ID + room_id: 房间ID + + Returns: + 是否移除成功 + """ + try: + if not self.db_connection: + return False + + cursor = self.db_connection.cursor() + cursor.execute('DELETE FROM user_rooms WHERE user_id = ? AND room_id = ?', (user_id, room_id)) + + self.db_connection.commit() + return True + + except Exception as e: + print(f"✗ 从房间移除用户失败: {e}") + self.storage_stats["errors"] += 1 + return False + + def save_system_config(self, config_key: str, config_value: Any) -> bool: + """ + 保存系统配置 + + Args: + config_key: 配置键 + config_value: 配置值 + + Returns: + 是否保存成功 + """ + try: + if not self.db_connection: + return False + + cursor = self.db_connection.cursor() + config_value_str = json.dumps(config_value, ensure_ascii=False) + + cursor.execute(''' + INSERT OR REPLACE INTO system_config (config_key, config_value, updated_time) + VALUES (?, ?, ?) + ''', (config_key, config_value_str, time.time())) + + self.db_connection.commit() + self.storage_stats["writes"] += 1 + return True + + except Exception as e: + print(f"✗ 保存系统配置失败: {e}") + self.storage_stats["errors"] += 1 + return False + + def load_system_config(self, config_key: str, default_value: Any = None) -> Any: + """ + 加载系统配置 + + Args: + config_key: 配置键 + default_value: 默认值 + + Returns: + 配置值或默认值 + """ + try: + if not self.db_connection: + return default_value + + cursor = self.db_connection.cursor() + cursor.execute('SELECT config_value FROM system_config WHERE config_key = ?', (config_key,)) + result = cursor.fetchone() + + if result: + return json.loads(result[0]) + else: + return default_value + + except Exception as e: + print(f"✗ 加载系统配置失败: {e}") + self.storage_stats["errors"] += 1 + return default_value + + def add_log(self, log_level: str, log_message: str, user_id: str = None) -> bool: + """ + 添加日志 + + Args: + log_level: 日志级别 + log_message: 日志消息 + user_id: 用户ID(可选) + + Returns: + 是否添加成功 + """ + try: + if not self.db_connection: + return False + + cursor = self.db_connection.cursor() + cursor.execute(''' + INSERT INTO logs (log_level, log_message, timestamp, user_id) + VALUES (?, ?, ?, ?) + ''', (log_level, log_message, time.time(), user_id)) + + self.db_connection.commit() + return True + + except Exception as e: + print(f"✗ 添加日志失败: {e}") + self.storage_stats["errors"] += 1 + return False + + def get_storage_stats(self) -> Dict[str, Any]: + """ + 获取存储统计信息 + + Returns: + 存储统计字典 + """ + return { + "stats": self.storage_stats.copy(), + "config": self.storage_config.copy(), + "cache_size": len(self.data_cache), + "write_queue_size": len(self.write_queue) + } + + def reset_stats(self): + """重置存储统计信息""" + try: + self.storage_stats = { + "reads": 0, + "writes": 0, + "cache_hits": 0, + "cache_misses": 0, + "backups_created": 0, + "errors": 0, + "bytes_read": 0, + "bytes_written": 0 + } + print("✓ 存储统计信息已重置") + except Exception as e: + print(f"✗ 存储统计信息重置失败: {e}") + + def set_storage_config(self, config: Dict[str, Any]) -> bool: + """ + 设置存储配置 + + Args: + config: 存储配置字典 + + Returns: + 是否设置成功 + """ + try: + old_async = self.storage_config["enable_async_writes"] + new_async = config.get("enable_async_writes", old_async) + + # 如果异步写入设置发生变化,处理线程 + if old_async != new_async: + if new_async and not self.write_thread_running: + self._start_write_thread() + elif not new_async and self.write_thread_running: + self.write_thread_running = False + if self.write_thread and self.write_thread.is_alive(): + self.write_thread.join(timeout=2.0) + + self.storage_config.update(config) + print(f"✓ 存储配置已更新: {self.storage_config}") + return True + except Exception as e: + print(f"✗ 存储配置设置失败: {e}") + return False + + def get_storage_config(self) -> Dict[str, Any]: + """ + 获取存储配置 + + Returns: + 存储配置字典 + """ + return self.storage_config.copy() + + def clear_cache(self): + """清空缓存""" + try: + with self.cache_lock: + self.data_cache.clear() + print("✓ 存储缓存已清空") + except Exception as e: + print(f"✗ 存储缓存清空失败: {e}") + self.storage_stats["errors"] += 1 + + def _trigger_storage_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发存储回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.storage_callbacks: + for callback in self.storage_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 存储回调执行失败: {callback_type} - {e}") + except Exception as e: + print(f"✗ 存储回调触发失败: {e}") + + def register_storage_callback(self, callback_type: str, callback: callable): + """ + 注册存储回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.storage_callbacks: + self.storage_callbacks[callback_type].append(callback) + print(f"✓ 存储回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 存储回调注册失败: {e}") + + def unregister_storage_callback(self, callback_type: str, callback: callable): + """ + 注销存储回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.storage_callbacks: + if callback in self.storage_callbacks[callback_type]: + self.storage_callbacks[callback_type].remove(callback) + print(f"✓ 存储回调已注销: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 存储回调注销失败: {e}") + diff --git a/plugins/user/server_architecture/utils/server_utils.py b/plugins/user/server_architecture/utils/server_utils.py index e11c3e5d..47c2d99c 100644 --- a/plugins/user/server_architecture/utils/server_utils.py +++ b/plugins/user/server_architecture/utils/server_utils.py @@ -1,334 +1,334 @@ -""" -服务器架构插件工具类模块 -提供实用工具函数 -""" - -import time -import json -import hashlib -import base64 -from typing import Dict, Any, List, Optional - -class ServerUtils: - """ - 服务器工具类 - 提供实用工具函数 - """ - - @staticmethod - def generate_unique_id() -> str: - """ - 生成唯一ID - - Returns: - 唯一ID字符串 - """ - return hashlib.sha256(f"{time.time()}_{id(object())}".encode()).hexdigest()[:16] - - @staticmethod - def validate_json(data: str) -> bool: - """ - 验证JSON数据 - - Args: - data: JSON字符串 - - Returns: - 是否为有效JSON - """ - try: - json.loads(data) - return True - except: - return False - - @staticmethod - def format_bytes(bytes_count: int) -> str: - """ - 格式化字节数 - - Args: - bytes_count: 字节数 - - Returns: - 格式化后的字符串 - """ - for unit in ['B', 'KB', 'MB', 'GB', 'TB']: - if bytes_count < 1024.0: - return f"{bytes_count:.2f} {unit}" - bytes_count /= 1024.0 - return f"{bytes_count:.2f} PB" - - @staticmethod - def calculate_percentage(part: float, total: float) -> float: - """ - 计算百分比 - - Args: - part: 部分值 - total: 总值 - - Returns: - 百分比 - """ - if total == 0: - return 0.0 - return (part / total) * 100 - - @staticmethod - def sanitize_input(input_str: str) -> str: - """ - 清理输入字符串 - - Args: - input_str: 输入字符串 - - Returns: - 清理后的字符串 - """ - # 移除潜在的危险字符 - dangerous_chars = ['<', '>', '&', '"', "'", '\\', '/', '..'] - sanitized = input_str - for char in dangerous_chars: - sanitized = sanitized.replace(char, '') - return sanitized.strip() - - @staticmethod - def validate_ip_address(ip: str) -> bool: - """ - 验证IP地址格式 - - Args: - ip: IP地址字符串 - - Returns: - 是否为有效IP地址 - """ - parts = ip.split('.') - if len(parts) != 4: - return False - for part in parts: - try: - num = int(part) - if num < 0 or num > 255: - return False - except: - return False - return True - - @staticmethod - def mask_sensitive_data(data: Dict[str, Any], sensitive_keys: List[str]) -> Dict[str, Any]: - """ - 遮蔽敏感数据 - - Args: - data: 数据字典 - sensitive_keys: 敏感键列表 - - Returns: - 遮蔽后的数据字典 - """ - masked_data = data.copy() - for key in sensitive_keys: - if key in masked_data: - masked_data[key] = "***MASKED***" - return masked_data - - @staticmethod - def calculate_uptime(start_time: float) -> str: - """ - 计算运行时间 - - Args: - start_time: 开始时间戳 - - Returns: - 格式化的运行时间字符串 - """ - if start_time == 0: - return "0s" - - uptime_seconds = time.time() - start_time - days = int(uptime_seconds // 86400) - hours = int((uptime_seconds % 86400) // 3600) - minutes = int((uptime_seconds % 3600) // 60) - seconds = int(uptime_seconds % 60) - - parts = [] - if days > 0: - parts.append(f"{days}d") - if hours > 0: - parts.append(f"{hours}h") - if minutes > 0: - parts.append(f"{minutes}m") - if seconds > 0 or not parts: - parts.append(f"{seconds}s") - - return " ".join(parts) - - @staticmethod - def encode_base64(data: str) -> str: - """ - Base64编码 - - Args: - data: 要编码的数据 - - Returns: - 编码后的字符串 - """ - return base64.b64encode(data.encode('utf-8')).decode('utf-8') - - @staticmethod - def decode_base64(encoded_data: str) -> str: - """ - Base64解码 - - Args: - encoded_data: 要解码的数据 - - Returns: - 解码后的字符串 - """ - return base64.b64decode(encoded_data.encode('utf-8')).decode('utf-8') - - @staticmethod - def get_system_info() -> Dict[str, Any]: - """ - 获取系统信息 - - Returns: - 系统信息字典 - """ - try: - import platform - import psutil - - # CPU信息 - cpu_percent = psutil.cpu_percent(interval=1) - - # 内存信息 - memory = psutil.virtual_memory() - - # 磁盘信息 - disk = psutil.disk_usage('/') - - return { - "platform": platform.system(), - "platform_version": platform.version(), - "architecture": platform.machine(), - "cpu_count": psutil.cpu_count(), - "cpu_percent": cpu_percent, - "memory_total": memory.total, - "memory_available": memory.available, - "memory_percent": memory.percent, - "disk_total": disk.total, - "disk_used": disk.used, - "disk_free": disk.free, - "disk_percent": (disk.used / disk.total) * 100 if disk.total > 0 else 0 - } - except: - # 如果无法获取系统信息,返回基本数据 - return { - "platform": "Unknown", - "platform_version": "Unknown", - "architecture": "Unknown", - "cpu_count": 0, - "cpu_percent": 0.0, - "memory_total": 0, - "memory_available": 0, - "memory_percent": 0.0, - "disk_total": 0, - "disk_used": 0, - "disk_free": 0, - "disk_percent": 0.0 - } - - @staticmethod - def create_response(success: bool, message: str, data: Any = None) -> Dict[str, Any]: - """ - 创建标准响应 - - Args: - success: 是否成功 - message: 消息 - data: 数据 - - Returns: - 标准响应字典 - """ - response = { - "success": success, - "message": message, - "timestamp": time.time() - } - - if data is not None: - response["data"] = data - - return response - - @staticmethod - def merge_dicts(dict1: Dict[str, Any], dict2: Dict[str, Any]) -> Dict[str, Any]: - """ - 合并两个字典(递归) - - Args: - dict1: 第一个字典 - dict2: 第二个字典 - - Returns: - 合并后的字典 - """ - result = dict1.copy() - - for key, value in dict2.items(): - if key in result and isinstance(result[key], dict) and isinstance(value, dict): - result[key] = ServerUtils.merge_dicts(result[key], value) - else: - result[key] = value - - return result - - @staticmethod - def get_nested_value(data: Dict[str, Any], path: str, default: Any = None) -> Any: - """ - 获取嵌套字典中的值 - - Args: - data: 数据字典 - path: 路径(用.分隔) - default: 默认值 - - Returns: - 获取到的值或默认值 - """ - keys = path.split('.') - current = data - - try: - for key in keys: - current = current[key] - return current - except (KeyError, TypeError): - return default - - @staticmethod - def set_nested_value(data: Dict[str, Any], path: str, value: Any) -> None: - """ - 设置嵌套字典中的值 - - Args: - data: 数据字典 - path: 路径(用.分隔) - value: 要设置的值 - """ - keys = path.split('.') - current = data - - for key in keys[:-1]: - if key not in current: - current[key] = {} - current = current[key] - +""" +服务器架构插件工具类模块 +提供实用工具函数 +""" + +import time +import json +import hashlib +import base64 +from typing import Dict, Any, List, Optional + +class ServerUtils: + """ + 服务器工具类 + 提供实用工具函数 + """ + + @staticmethod + def generate_unique_id() -> str: + """ + 生成唯一ID + + Returns: + 唯一ID字符串 + """ + return hashlib.sha256(f"{time.time()}_{id(object())}".encode()).hexdigest()[:16] + + @staticmethod + def validate_json(data: str) -> bool: + """ + 验证JSON数据 + + Args: + data: JSON字符串 + + Returns: + 是否为有效JSON + """ + try: + json.loads(data) + return True + except: + return False + + @staticmethod + def format_bytes(bytes_count: int) -> str: + """ + 格式化字节数 + + Args: + bytes_count: 字节数 + + Returns: + 格式化后的字符串 + """ + for unit in ['B', 'KB', 'MB', 'GB', 'TB']: + if bytes_count < 1024.0: + return f"{bytes_count:.2f} {unit}" + bytes_count /= 1024.0 + return f"{bytes_count:.2f} PB" + + @staticmethod + def calculate_percentage(part: float, total: float) -> float: + """ + 计算百分比 + + Args: + part: 部分值 + total: 总值 + + Returns: + 百分比 + """ + if total == 0: + return 0.0 + return (part / total) * 100 + + @staticmethod + def sanitize_input(input_str: str) -> str: + """ + 清理输入字符串 + + Args: + input_str: 输入字符串 + + Returns: + 清理后的字符串 + """ + # 移除潜在的危险字符 + dangerous_chars = ['<', '>', '&', '"', "'", '\\', '/', '..'] + sanitized = input_str + for char in dangerous_chars: + sanitized = sanitized.replace(char, '') + return sanitized.strip() + + @staticmethod + def validate_ip_address(ip: str) -> bool: + """ + 验证IP地址格式 + + Args: + ip: IP地址字符串 + + Returns: + 是否为有效IP地址 + """ + parts = ip.split('.') + if len(parts) != 4: + return False + for part in parts: + try: + num = int(part) + if num < 0 or num > 255: + return False + except: + return False + return True + + @staticmethod + def mask_sensitive_data(data: Dict[str, Any], sensitive_keys: List[str]) -> Dict[str, Any]: + """ + 遮蔽敏感数据 + + Args: + data: 数据字典 + sensitive_keys: 敏感键列表 + + Returns: + 遮蔽后的数据字典 + """ + masked_data = data.copy() + for key in sensitive_keys: + if key in masked_data: + masked_data[key] = "***MASKED***" + return masked_data + + @staticmethod + def calculate_uptime(start_time: float) -> str: + """ + 计算运行时间 + + Args: + start_time: 开始时间戳 + + Returns: + 格式化的运行时间字符串 + """ + if start_time == 0: + return "0s" + + uptime_seconds = time.time() - start_time + days = int(uptime_seconds // 86400) + hours = int((uptime_seconds % 86400) // 3600) + minutes = int((uptime_seconds % 3600) // 60) + seconds = int(uptime_seconds % 60) + + parts = [] + if days > 0: + parts.append(f"{days}d") + if hours > 0: + parts.append(f"{hours}h") + if minutes > 0: + parts.append(f"{minutes}m") + if seconds > 0 or not parts: + parts.append(f"{seconds}s") + + return " ".join(parts) + + @staticmethod + def encode_base64(data: str) -> str: + """ + Base64编码 + + Args: + data: 要编码的数据 + + Returns: + 编码后的字符串 + """ + return base64.b64encode(data.encode('utf-8')).decode('utf-8') + + @staticmethod + def decode_base64(encoded_data: str) -> str: + """ + Base64解码 + + Args: + encoded_data: 要解码的数据 + + Returns: + 解码后的字符串 + """ + return base64.b64decode(encoded_data.encode('utf-8')).decode('utf-8') + + @staticmethod + def get_system_info() -> Dict[str, Any]: + """ + 获取系统信息 + + Returns: + 系统信息字典 + """ + try: + import platform + import psutil + + # CPU信息 + cpu_percent = psutil.cpu_percent(interval=1) + + # 内存信息 + memory = psutil.virtual_memory() + + # 磁盘信息 + disk = psutil.disk_usage('/') + + return { + "platform": platform.system(), + "platform_version": platform.version(), + "architecture": platform.machine(), + "cpu_count": psutil.cpu_count(), + "cpu_percent": cpu_percent, + "memory_total": memory.total, + "memory_available": memory.available, + "memory_percent": memory.percent, + "disk_total": disk.total, + "disk_used": disk.used, + "disk_free": disk.free, + "disk_percent": (disk.used / disk.total) * 100 if disk.total > 0 else 0 + } + except: + # 如果无法获取系统信息,返回基本数据 + return { + "platform": "Unknown", + "platform_version": "Unknown", + "architecture": "Unknown", + "cpu_count": 0, + "cpu_percent": 0.0, + "memory_total": 0, + "memory_available": 0, + "memory_percent": 0.0, + "disk_total": 0, + "disk_used": 0, + "disk_free": 0, + "disk_percent": 0.0 + } + + @staticmethod + def create_response(success: bool, message: str, data: Any = None) -> Dict[str, Any]: + """ + 创建标准响应 + + Args: + success: 是否成功 + message: 消息 + data: 数据 + + Returns: + 标准响应字典 + """ + response = { + "success": success, + "message": message, + "timestamp": time.time() + } + + if data is not None: + response["data"] = data + + return response + + @staticmethod + def merge_dicts(dict1: Dict[str, Any], dict2: Dict[str, Any]) -> Dict[str, Any]: + """ + 合并两个字典(递归) + + Args: + dict1: 第一个字典 + dict2: 第二个字典 + + Returns: + 合并后的字典 + """ + result = dict1.copy() + + for key, value in dict2.items(): + if key in result and isinstance(result[key], dict) and isinstance(value, dict): + result[key] = ServerUtils.merge_dicts(result[key], value) + else: + result[key] = value + + return result + + @staticmethod + def get_nested_value(data: Dict[str, Any], path: str, default: Any = None) -> Any: + """ + 获取嵌套字典中的值 + + Args: + data: 数据字典 + path: 路径(用.分隔) + default: 默认值 + + Returns: + 获取到的值或默认值 + """ + keys = path.split('.') + current = data + + try: + for key in keys: + current = current[key] + return current + except (KeyError, TypeError): + return default + + @staticmethod + def set_nested_value(data: Dict[str, Any], path: str, value: Any) -> None: + """ + 设置嵌套字典中的值 + + Args: + data: 数据字典 + path: 路径(用.分隔) + value: 要设置的值 + """ + keys = path.split('.') + current = data + + for key in keys[:-1]: + if key not in current: + current[key] = {} + current = current[key] + current[keys[-1]] = value \ No newline at end of file diff --git a/plugins/user/skeletal_animation_system/README.md b/plugins/user/skeletal_animation_system/README.md index 3a1ba688..a09ec6ac 100644 --- a/plugins/user/skeletal_animation_system/README.md +++ b/plugins/user/skeletal_animation_system/README.md @@ -1,274 +1,274 @@ -# 骨骼动画系统插件 - -## 概述 - -骨骼动画系统插件为EG项目提供完整的骨骼动画支持。该插件基于Panda3D的Actor系统,支持模型加载、动画播放、速度控制、动画混合等高级功能。 - -## 功能特性 - -1. **Actor模型加载** - 支持加载带有骨骼动画的3D模型 -2. **动画播放控制** - 播放、停止、暂停、恢复动画 -3. **速度调节** - 可调节动画播放速度 -4. **动画混合** - 支持多个动画之间的平滑混合 -5. **序列播放** - 支持动画序列的连续播放 -6. **事件驱动** - 通过事件系统控制动画播放 -7. **GUI控制面板** - 提供可视化控制界面 -8. **多Actor管理** - 支持同时管理多个动画角色 -9. **动画信息查询** - 提供详细的动画状态信息 - -## 系统架构 - -插件采用模块化设计,包含以下核心组件: - -1. **SkeletalAnimationManager** - 核心动画管理器,负责Actor和动画的管理 -2. **AnimationEventManager** - 事件管理器,处理事件驱动的动画控制 -3. **SkeletalAnimationGUI** - GUI界面,提供可视化控制面板 - -## 安装和启用 - -### 启用插件 - -在主程序中启用插件: - -```python -# 启用插件系统 -world.plugin_manager.enable_plugin_system() - -# 启用骨骼动画插件 -world.plugin_manager.enable_plugin("skeletal_animation_system") -``` - -## 使用方法 - -### 基本使用 - -```python -# 获取插件实例 -skeletal_plugin = world.plugin_manager.get_plugin("skeletal_animation_system") -animation_manager = skeletal_plugin.get_animation_manager() - -# 加载Actor模型 -actor, actor_id = animation_manager.load_actor( - model_path="path/to/model.egg", - anim_dict={ - "walk": "path/to/walk.egg", - "run": "path/to/run.egg" - }, - actor_id="my_hero", # 可选,自动生成如果未提供 - position=(0, 0, 0), # 可选 - scale=1.0 # 可选 -) - -# 播放动画 -if actor: - animation_manager.play_animation(actor_id, "walk", loop=True) -``` - -### 高级使用 - -```python -# 动画混合 -blend = animation_manager.blend_animations( - actor_id, - "walk", - "run", - blend_time=2.0, - loop=True -) - -# 创建动画序列 -sequence = animation_manager.create_animation_sequence( - actor_id, - ["walk", "run", "jump"], - loop=True -) - -# 获取动画信息 -anim_info = animation_manager.get_animation_info(actor_id, "walk") -actor_info = animation_manager.get_actor_info(actor_id) -``` - -### 事件驱动控制 - -插件注册了以下事件: - -- `skeletal.load_actor` - 加载Actor模型 -- `skeletal.play_animation` - 播放动画 -- `skeletal.stop_animation` - 停止动画 -- `skeletal.set_animation_speed` - 设置动画速度 -- `skeletal.pause_animation` - 暂停动画 -- `skeletal.resume_animation` - 恢复动画 - -使用方法: - -```python -# 通过事件加载Actor -world.send("skeletal.load_actor", [ - "models/character.egg", - {"walk": "anim/walk.egg"}, - "hero", - (0, 0, 0), - 1.0 -]) - -# 通过事件播放动画 -world.send("skeletal.play_animation", ["hero", "walk", True, 1.0]) -``` - -## API参考 - -### SkeletalAnimationManager 类 - -#### load_actor(model_path, anim_dict=None, actor_id=None, position=None, scale=None) -加载Actor模型 - -**参数:** -- `model_path` (str): 模型文件路径 -- `anim_dict` (dict): 动画文件路径字典,格式 {'anim_name': 'anim_path'} -- `actor_id` (str): Actor标识符,如果为None则自动生成 -- `position` (tuple): 位置坐标 (x, y, z) -- `scale` (float or tuple): 缩放比例 - -**返回:** -- Actor对象和actor_id - -#### unload_actor(actor_id) -卸载Actor - -**参数:** -- `actor_id` (str): 要卸载的Actor标识符 - -#### play_animation(actor_id, anim_name, loop=False, play_rate=1.0, start_frame=None, end_frame=None) -播放动画 - -**参数:** -- `actor_id` (str): Actor标识符 -- `anim_name` (str): 动画名称 -- `loop` (bool): 是否循环播放 -- `play_rate` (float): 播放速度 -- `start_frame` (int): 起始帧 -- `end_frame` (int): 结束帧 - -#### stop_animation(actor_id, anim_name=None) -停止动画 - -**参数:** -- `actor_id` (str): Actor标识符 -- `anim_name` (str): 动画名称,如果为None则停止所有动画 - -#### set_animation_speed(actor_id, anim_name, speed) -设置动画播放速度 - -**参数:** -- `actor_id` (str): Actor标识符 -- `anim_name` (str): 动画名称 -- `speed` (float): 播放速度 - -#### pause_animation(actor_id, anim_name=None) -暂停动画 - -**参数:** -- `actor_id` (str): Actor标识符 -- `anim_name` (str): 动画名称,如果为None则暂停所有动画 - -#### resume_animation(actor_id, anim_name=None) -恢复动画播放 - -**参数:** -- `actor_id` (str): Actor标识符 -- `anim_name` (str): 动画名称,如果为None则恢复所有动画 - -#### blend_animations(actor_id, from_anim, to_anim, blend_time=1.0, loop=True) -混合两个动画 - -**参数:** -- `actor_id` (str): Actor标识符 -- `from_anim` (str): 起始动画名称 -- `to_anim` (str): 目标动画名称 -- `blend_time` (float): 混合时间 -- `loop` (bool): 是否循环混合 - -#### create_animation_sequence(actor_id, anim_list, loop=False) -创建动画序列 - -**参数:** -- `actor_id` (str): Actor标识符 -- `anim_list` (list): 动画名称列表 -- `loop` (bool): 是否循环播放序列 - -#### get_actor_animations(actor_id) -获取Actor的所有动画名称 - -**参数:** -- `actor_id` (str): Actor标识符 - -**返回:** -- 动画名称列表 - -#### get_animation_info(actor_id, anim_name) -获取动画信息 - -**参数:** -- `actor_id` (str): Actor标识符 -- `anim_name` (str): 动画名称 - -**返回:** -- 动画信息字典 - -#### get_actor_info(actor_id) -获取Actor详细信息 - -**参数:** -- `actor_id` (str): Actor标识符 - -**返回:** -- Actor信息字典 - -## 支持的文件格式 - -- 模型格式: .egg, .bam, .fbx, .gltf, .glb等Panda3D支持的格式 -- 动画格式: .egg, .bam等包含动画数据的格式 - -## 配置选项 - -插件支持以下配置选项(config.json): - -- `default_play_rate`: 默认播放速度 -- `default_loop`: 默认循环播放设置 -- `enable_gui`: 是否启用GUI控制面板 -- `enable_events`: 是否启用事件系统 -- `max_actors`: 最大Actor数量 -- `animation_cache_size`: 动画缓存大小 -- `blend_default_time`: 默认混合时间 -- `debug_mode`: 调试模式 - -## 注意事项 - -1. 确保模型文件包含正确的骨骼信息 -2. 动画文件需要与模型骨骼结构匹配 -3. 插件会在禁用时自动清理所有加载的Actor -4. 使用完毕后应调用unload_actor释放资源 -5. 大量Actor可能影响性能,注意优化 -6. 某些格式(如FBX)可能需要额外的转换处理 - -## 故障排除 - -常见问题及解决方案: - -1. **模型加载失败**: 检查文件路径和格式支持 -2. **动画无法播放**: 确认动画名称正确且与模型匹配 -3. **性能问题**: 减少同时播放的动画数量,及时卸载不需要的Actor -4. **事件无响应**: 确认插件已正确启用且事件系统工作正常 - -## 扩展开发 - -插件采用模块化设计,可以轻松扩展: - -1. 继承SkeletalAnimationManager添加新功能 -2. 扩展AnimationEventManager处理自定义事件 -3. 修改SkeletalAnimationGUI创建自定义界面 -4. 添加新的动画控制算法 - -## 示例代码 - +# 骨骼动画系统插件 + +## 概述 + +骨骼动画系统插件为EG项目提供完整的骨骼动画支持。该插件基于Panda3D的Actor系统,支持模型加载、动画播放、速度控制、动画混合等高级功能。 + +## 功能特性 + +1. **Actor模型加载** - 支持加载带有骨骼动画的3D模型 +2. **动画播放控制** - 播放、停止、暂停、恢复动画 +3. **速度调节** - 可调节动画播放速度 +4. **动画混合** - 支持多个动画之间的平滑混合 +5. **序列播放** - 支持动画序列的连续播放 +6. **事件驱动** - 通过事件系统控制动画播放 +7. **GUI控制面板** - 提供可视化控制界面 +8. **多Actor管理** - 支持同时管理多个动画角色 +9. **动画信息查询** - 提供详细的动画状态信息 + +## 系统架构 + +插件采用模块化设计,包含以下核心组件: + +1. **SkeletalAnimationManager** - 核心动画管理器,负责Actor和动画的管理 +2. **AnimationEventManager** - 事件管理器,处理事件驱动的动画控制 +3. **SkeletalAnimationGUI** - GUI界面,提供可视化控制面板 + +## 安装和启用 + +### 启用插件 + +在主程序中启用插件: + +```python +# 启用插件系统 +world.plugin_manager.enable_plugin_system() + +# 启用骨骼动画插件 +world.plugin_manager.enable_plugin("skeletal_animation_system") +``` + +## 使用方法 + +### 基本使用 + +```python +# 获取插件实例 +skeletal_plugin = world.plugin_manager.get_plugin("skeletal_animation_system") +animation_manager = skeletal_plugin.get_animation_manager() + +# 加载Actor模型 +actor, actor_id = animation_manager.load_actor( + model_path="path/to/model.egg", + anim_dict={ + "walk": "path/to/walk.egg", + "run": "path/to/run.egg" + }, + actor_id="my_hero", # 可选,自动生成如果未提供 + position=(0, 0, 0), # 可选 + scale=1.0 # 可选 +) + +# 播放动画 +if actor: + animation_manager.play_animation(actor_id, "walk", loop=True) +``` + +### 高级使用 + +```python +# 动画混合 +blend = animation_manager.blend_animations( + actor_id, + "walk", + "run", + blend_time=2.0, + loop=True +) + +# 创建动画序列 +sequence = animation_manager.create_animation_sequence( + actor_id, + ["walk", "run", "jump"], + loop=True +) + +# 获取动画信息 +anim_info = animation_manager.get_animation_info(actor_id, "walk") +actor_info = animation_manager.get_actor_info(actor_id) +``` + +### 事件驱动控制 + +插件注册了以下事件: + +- `skeletal.load_actor` - 加载Actor模型 +- `skeletal.play_animation` - 播放动画 +- `skeletal.stop_animation` - 停止动画 +- `skeletal.set_animation_speed` - 设置动画速度 +- `skeletal.pause_animation` - 暂停动画 +- `skeletal.resume_animation` - 恢复动画 + +使用方法: + +```python +# 通过事件加载Actor +world.send("skeletal.load_actor", [ + "models/character.egg", + {"walk": "anim/walk.egg"}, + "hero", + (0, 0, 0), + 1.0 +]) + +# 通过事件播放动画 +world.send("skeletal.play_animation", ["hero", "walk", True, 1.0]) +``` + +## API参考 + +### SkeletalAnimationManager 类 + +#### load_actor(model_path, anim_dict=None, actor_id=None, position=None, scale=None) +加载Actor模型 + +**参数:** +- `model_path` (str): 模型文件路径 +- `anim_dict` (dict): 动画文件路径字典,格式 {'anim_name': 'anim_path'} +- `actor_id` (str): Actor标识符,如果为None则自动生成 +- `position` (tuple): 位置坐标 (x, y, z) +- `scale` (float or tuple): 缩放比例 + +**返回:** +- Actor对象和actor_id + +#### unload_actor(actor_id) +卸载Actor + +**参数:** +- `actor_id` (str): 要卸载的Actor标识符 + +#### play_animation(actor_id, anim_name, loop=False, play_rate=1.0, start_frame=None, end_frame=None) +播放动画 + +**参数:** +- `actor_id` (str): Actor标识符 +- `anim_name` (str): 动画名称 +- `loop` (bool): 是否循环播放 +- `play_rate` (float): 播放速度 +- `start_frame` (int): 起始帧 +- `end_frame` (int): 结束帧 + +#### stop_animation(actor_id, anim_name=None) +停止动画 + +**参数:** +- `actor_id` (str): Actor标识符 +- `anim_name` (str): 动画名称,如果为None则停止所有动画 + +#### set_animation_speed(actor_id, anim_name, speed) +设置动画播放速度 + +**参数:** +- `actor_id` (str): Actor标识符 +- `anim_name` (str): 动画名称 +- `speed` (float): 播放速度 + +#### pause_animation(actor_id, anim_name=None) +暂停动画 + +**参数:** +- `actor_id` (str): Actor标识符 +- `anim_name` (str): 动画名称,如果为None则暂停所有动画 + +#### resume_animation(actor_id, anim_name=None) +恢复动画播放 + +**参数:** +- `actor_id` (str): Actor标识符 +- `anim_name` (str): 动画名称,如果为None则恢复所有动画 + +#### blend_animations(actor_id, from_anim, to_anim, blend_time=1.0, loop=True) +混合两个动画 + +**参数:** +- `actor_id` (str): Actor标识符 +- `from_anim` (str): 起始动画名称 +- `to_anim` (str): 目标动画名称 +- `blend_time` (float): 混合时间 +- `loop` (bool): 是否循环混合 + +#### create_animation_sequence(actor_id, anim_list, loop=False) +创建动画序列 + +**参数:** +- `actor_id` (str): Actor标识符 +- `anim_list` (list): 动画名称列表 +- `loop` (bool): 是否循环播放序列 + +#### get_actor_animations(actor_id) +获取Actor的所有动画名称 + +**参数:** +- `actor_id` (str): Actor标识符 + +**返回:** +- 动画名称列表 + +#### get_animation_info(actor_id, anim_name) +获取动画信息 + +**参数:** +- `actor_id` (str): Actor标识符 +- `anim_name` (str): 动画名称 + +**返回:** +- 动画信息字典 + +#### get_actor_info(actor_id) +获取Actor详细信息 + +**参数:** +- `actor_id` (str): Actor标识符 + +**返回:** +- Actor信息字典 + +## 支持的文件格式 + +- 模型格式: .egg, .bam, .fbx, .gltf, .glb等Panda3D支持的格式 +- 动画格式: .egg, .bam等包含动画数据的格式 + +## 配置选项 + +插件支持以下配置选项(config.json): + +- `default_play_rate`: 默认播放速度 +- `default_loop`: 默认循环播放设置 +- `enable_gui`: 是否启用GUI控制面板 +- `enable_events`: 是否启用事件系统 +- `max_actors`: 最大Actor数量 +- `animation_cache_size`: 动画缓存大小 +- `blend_default_time`: 默认混合时间 +- `debug_mode`: 调试模式 + +## 注意事项 + +1. 确保模型文件包含正确的骨骼信息 +2. 动画文件需要与模型骨骼结构匹配 +3. 插件会在禁用时自动清理所有加载的Actor +4. 使用完毕后应调用unload_actor释放资源 +5. 大量Actor可能影响性能,注意优化 +6. 某些格式(如FBX)可能需要额外的转换处理 + +## 故障排除 + +常见问题及解决方案: + +1. **模型加载失败**: 检查文件路径和格式支持 +2. **动画无法播放**: 确认动画名称正确且与模型匹配 +3. **性能问题**: 减少同时播放的动画数量,及时卸载不需要的Actor +4. **事件无响应**: 确认插件已正确启用且事件系统工作正常 + +## 扩展开发 + +插件采用模块化设计,可以轻松扩展: + +1. 继承SkeletalAnimationManager添加新功能 +2. 扩展AnimationEventManager处理自定义事件 +3. 修改SkeletalAnimationGUI创建自定义界面 +4. 添加新的动画控制算法 + +## 示例代码 + 查看 `examples.py` 文件获取详细的使用示例。 \ No newline at end of file diff --git a/plugins/user/skeletal_animation_system/advanced_features.py b/plugins/user/skeletal_animation_system/advanced_features.py index 66d08e3f..c4e735c4 100644 --- a/plugins/user/skeletal_animation_system/advanced_features.py +++ b/plugins/user/skeletal_animation_system/advanced_features.py @@ -1,588 +1,588 @@ -""" -骨骼动画系统插件 - 高级功能扩展 -提供动画状态机、IK系统、动画曲线编辑等高级功能 -""" - -import math -from panda3d.core import * -from direct.actor.Actor import Actor -from direct.interval.IntervalGlobal import * - - -class AnimationStateMachine: - """ - 动画状态机 - 管理复杂的动画状态转换和混合 - """ - - def __init__(self, actor): - self.actor = actor - self.states = {} # 状态字典 {state_name: state_data} - self.current_state = None - self.transitions = {} # 状态转换 {from_state: {to_state: condition_func}} - self.state_callbacks = {} # 状态回调 {state_name: {event: callback_func}} - - def add_state(self, state_name, animations, loop=True, play_rate=1.0, blend_time=0.3): - """ - 添加动画状态 - :param state_name: 状态名称 - :param animations: 动画列表或单个动画名称 - :param loop: 是否循环 - :param play_rate: 播放速度 - :param blend_time: 状态切换混合时间 - """ - if isinstance(animations, str): - animations = [animations] - - self.states[state_name] = { - 'animations': animations, - 'loop': loop, - 'play_rate': play_rate, - 'blend_time': blend_time, - 'active_interval': None - } - - print(f"添加动画状态: {state_name}") - - def remove_state(self, state_name): - """ - 移除动画状态 - :param state_name: 状态名称 - """ - if state_name in self.states: - # 如果是当前状态,先停止 - if self.current_state == state_name: - self.stop_current_state() - del self.states[state_name] - print(f"移除动画状态: {state_name}") - - def set_transition(self, from_state, to_state, condition_func=None): - """ - 设置状态转换条件 - :param from_state: 起始状态 - :param to_state: 目标状态 - :param condition_func: 转换条件函数,返回True时执行转换 - """ - if from_state not in self.transitions: - self.transitions[from_state] = {} - self.transitions[from_state][to_state] = condition_func - print(f"设置状态转换: {from_state} -> {to_state}") - - def set_state_callback(self, state_name, event, callback_func): - """ - 设置状态回调 - :param state_name: 状态名称 - :param event: 事件类型 ('enter', 'exit', 'update') - :param callback_func: 回调函数 - """ - if state_name not in self.state_callbacks: - self.state_callbacks[state_name] = {} - self.state_callbacks[state_name][event] = callback_func - - def enter_state(self, state_name): - """ - 进入指定状态 - :param state_name: 状态名称 - """ - if state_name not in self.states: - print(f"错误: 状态 {state_name} 不存在") - return False - - # 退出当前状态 - if self.current_state: - self.exit_current_state() - - # 进入新状态 - state_data = self.states[state_name] - - # 创建动画序列 - intervals = [] - for anim_name in state_data['animations']: - if anim_name in self.actor.getAnimNames(): - interval = self.actor.actorInterval( - anim_name, - loop=state_data['loop'], - playRate=state_data['play_rate'] - ) - intervals.append(interval) - - if intervals: - if len(intervals) == 1: - state_data['active_interval'] = intervals[0] - else: - state_data['active_interval'] = Sequence(*intervals) - - # 启动动画 - state_data['active_interval'].start() - - self.current_state = state_name - - # 调用进入回调 - if state_name in self.state_callbacks and 'enter' in self.state_callbacks[state_name]: - self.state_callbacks[state_name]['enter']() - - print(f"进入动画状态: {state_name}") - return True - - def exit_current_state(self): - """ - 退出当前状态 - """ - if not self.current_state: - return - - state_data = self.states[self.current_state] - - # 停止当前动画 - if state_data['active_interval']: - state_data['active_interval'].finish() - state_data['active_interval'] = None - - # 调用退出回调 - if self.current_state in self.state_callbacks and 'exit' in self.state_callbacks[self.current_state]: - self.state_callbacks[self.current_state]['exit']() - - print(f"退出动画状态: {self.current_state}") - self.current_state = None - - def stop_current_state(self): - """ - 停止当前状态 - """ - if not self.current_state: - return - - state_data = self.states[self.current_state] - - # 停止当前动画 - if state_data['active_interval']: - state_data['active_interval'].finish() - state_data['active_interval'] = None - - def update(self, dt): - """ - 更新状态机 - :param dt: 时间增量 - """ - if not self.current_state: - return - - # 调用更新回调 - if self.current_state in self.state_callbacks and 'update' in self.state_callbacks[self.current_state]: - self.state_callbacks[self.current_state]['update'](dt) - - # 检查状态转换 - if self.current_state in self.transitions: - for to_state, condition_func in self.transitions[self.current_state].items(): - if condition_func is None or condition_func(): - self.enter_state(to_state) - break - - def get_current_state(self): - """ - 获取当前状态 - :return: 当前状态名称 - """ - return self.current_state - - def get_available_states(self): - """ - 获取所有可用状态 - :return: 状态名称列表 - """ - return list(self.states.keys()) - - -class InverseKinematicsSystem: - """ - 逆向运动学系统 - 实现骨骼的IK控制 - """ - - def __init__(self, actor): - self.actor = actor - self.ik_chains = {} # IK链 {chain_name: chain_data} - self.targets = {} # IK目标 {target_name: target_node} - - def add_ik_chain(self, chain_name, end_effector, pole_vector=None, iterations=10): - """ - 添加IK链 - :param chain_name: 链名称 - :param end_effector: 终端效应器(骨骼名称) - :param pole_vector: 极向量(用于控制弯曲方向) - :param iterations: 迭代次数 - """ - # 查找骨骼 - effector_joint = self.actor.exposeJoint(None, "modelRoot", end_effector) - if not effector_joint: - print(f"错误: 未找到骨骼 {end_effector}") - return - - self.ik_chains[chain_name] = { - 'end_effector': end_effector, - 'effector_joint': effector_joint, - 'pole_vector': pole_vector, - 'iterations': iterations, - 'enabled': True - } - - print(f"添加IK链: {chain_name} -> {end_effector}") - - def remove_ik_chain(self, chain_name): - """ - 移除IK链 - :param chain_name: 链名称 - """ - if chain_name in self.ik_chains: - del self.ik_chains[chain_name] - print(f"移除IK链: {chain_name}") - - def set_ik_target(self, chain_name, target_pos): - """ - 设置IK目标位置 - :param chain_name: 链名称 - :param target_pos: 目标位置 (x, y, z) - """ - if chain_name not in self.ik_chains: - print(f"错误: IK链 {chain_name} 不存在") - return - - chain_data = self.ik_chains[chain_name] - if not chain_data['enabled']: - return - - # 简化的IK实现(CCD算法) - effector_name = chain_data['end_effector'] - iterations = chain_data['iterations'] - - # 获取当前终端效应器位置 - effector_joint = chain_data['effector_joint'] - current_pos = effector_joint.getPos(self.actor) - target_pos = Point3(*target_pos) - - # 计算距离 - distance = (target_pos - current_pos).length() - if distance < 0.01: # 阈值 - return - - # CCD算法迭代 - for _ in range(iterations): - # 从终端向根部迭代调整骨骼 - # 这里简化处理,实际应该遍历整条骨骼链 - # 为了示例,我们只调整终端效应器朝向 - direction = target_pos - current_pos - direction.normalize() - - # 简化的朝向调整 - effector_joint.lookAt(target_pos) - - # 更新位置 - current_pos = effector_joint.getPos(self.actor) - distance = (target_pos - current_pos).length() - if distance < 0.01: - break - - def enable_ik_chain(self, chain_name, enabled=True): - """ - 启用/禁用IK链 - :param chain_name: 链名称 - :param enabled: 是否启用 - """ - if chain_name in self.ik_chains: - self.ik_chains[chain_name]['enabled'] = enabled - status = "启用" if enabled else "禁用" - print(f"{status}IK链: {chain_name}") - - def batch_set_targets(self, targets_dict): - """ - 批量设置IK目标 - :param targets_dict: {chain_name: target_pos} - """ - for chain_name, target_pos in targets_dict.items(): - self.set_ik_target(chain_name, target_pos) - - -class AnimationCurveEditor: - """ - 动画曲线编辑器 - 支持对动画属性进行曲线编辑和自定义 - """ - - def __init__(self, actor): - self.actor = actor - self.curves = {} # 曲线字典 {curve_name: curve_data} - self.curve_targets = {} # 曲线目标 {target_name: target_data} - - def create_curve(self, curve_name, keyframes, interpolation='linear'): - """ - 创建动画曲线 - :param curve_name: 曲线名称 - :param keyframes: 关键帧列表 [(time, value), ...] - :param interpolation: 插值方式 ('linear', 'smooth', 'step') - """ - self.curves[curve_name] = { - 'keyframes': sorted(keyframes, key=lambda x: x[0]), - 'interpolation': interpolation, - 'current_value': 0.0 - } - - print(f"创建动画曲线: {curve_name}") - - def remove_curve(self, curve_name): - """ - 移除动画曲线 - :param curve_name: 曲线名称 - """ - if curve_name in self.curves: - del self.curves[curve_name] - print(f"移除动画曲线: {curve_name}") - - def add_curve_target(self, target_name, curve_name, property_path, transform_type='scale'): - """ - 添加曲线目标 - :param target_name: 目标名称 - :param curve_name: 曲线名称 - :param property_path: 属性路径(如骨骼名称) - :param transform_type: 变换类型 ('scale', 'position', 'rotation') - """ - if curve_name not in self.curves: - print(f"错误: 曲线 {curve_name} 不存在") - return - - self.curve_targets[target_name] = { - 'curve_name': curve_name, - 'property_path': property_path, - 'transform_type': transform_type - } - - print(f"添加曲线目标: {target_name} -> {curve_name}") - - def evaluate_curve(self, curve_name, time): - """ - 计算曲线在指定时间的值 - :param curve_name: 曲线名称 - :param time: 时间 - :return: 曲线值 - """ - if curve_name not in self.curves: - return 0.0 - - curve_data = self.curves[curve_name] - keyframes = curve_data['keyframes'] - - if not keyframes: - return 0.0 - - # 边界情况 - if time <= keyframes[0][0]: - return keyframes[0][1] - if time >= keyframes[-1][0]: - return keyframes[-1][1] - - # 查找相邻关键帧 - for i in range(len(keyframes) - 1): - if keyframes[i][0] <= time <= keyframes[i + 1][0]: - t1, v1 = keyframes[i] - t2, v2 = keyframes[i + 1] - - # 根据插值方式计算值 - if curve_data['interpolation'] == 'step': - return v1 - elif curve_data['interpolation'] == 'smooth': - # 平滑插值(三次 Hermite) - t = (time - t1) / (t2 - t1) - t2_sq = t * t - t3_sq = t2_sq * t - h00 = 2 * t3_sq - 3 * t2_sq + 1 - h10 = t3_sq - 2 * t2_sq + t - h01 = -2 * t3_sq + 3 * t2_sq - h11 = t3_sq - t2_sq - return h00 * v1 + h10 * (t2 - t1) + h01 * v2 + h11 * (t2 - t1) - else: # linear - t = (time - t1) / (t2 - t1) - return v1 + (v2 - v1) * t - - return 0.0 - - def update_curves(self, time): - """ - 更新所有曲线 - :param time: 当前时间 - """ - for target_name, target_data in self.curve_targets.items(): - curve_name = target_data['curve_name'] - property_path = target_data['property_path'] - transform_type = target_data['transform_type'] - - # 计算曲线值 - value = self.evaluate_curve(curve_name, time) - - # 应用到目标 - self.apply_curve_value(property_path, transform_type, value) - - # 更新曲线数据 - if curve_name in self.curves: - self.curves[curve_name]['current_value'] = value - - def apply_curve_value(self, property_path, transform_type, value): - """ - 应用曲线值到目标属性 - :param property_path: 属性路径 - :param transform_type: 变换类型 - :param value: 值 - """ - # 查找目标骨骼 - joint = self.actor.exposeJoint(None, "modelRoot", property_path) - if not joint: - return - - # 根据变换类型应用值 - if transform_type == 'scale': - joint.setScale(value, value, value) - elif transform_type == 'position': - current_pos = joint.getPos() - if property_path.endswith('.x'): - joint.setPos(value, current_pos.y, current_pos.z) - elif property_path.endswith('.y'): - joint.setPos(current_pos.x, value, current_pos.z) - elif property_path.endswith('.z'): - joint.setPos(current_pos.x, current_pos.y, value) - elif transform_type == 'rotation': - # 简化处理,实际应该分解为具体的旋转轴 - joint.setHpr(value, value, value) - - -class AdvancedAnimationManager: - """ - 高级动画管理器 - 集成状态机、IK系统和曲线编辑器 - """ - - def __init__(self, actor): - self.actor = actor - self.state_machine = AnimationStateMachine(actor) - self.ik_system = InverseKinematicsSystem(actor) - self.curve_editor = AnimationCurveEditor(actor) - self.enabled_systems = { - 'state_machine': True, - 'ik_system': True, - 'curve_editor': True - } - - def update(self, dt, time): - """ - 更新所有高级动画系统 - :param dt: 时间增量 - :param time: 当前时间 - """ - if self.enabled_systems['state_machine']: - self.state_machine.update(dt) - - if self.enabled_systems['curve_editor']: - self.curve_editor.update_curves(time) - - def enable_system(self, system_name, enabled=True): - """ - 启用/禁用子系统 - :param system_name: 系统名称 ('state_machine', 'ik_system', 'curve_editor') - :param enabled: 是否启用 - """ - if system_name in self.enabled_systems: - self.enabled_systems[system_name] = enabled - status = "启用" if enabled else "禁用" - print(f"{status}动画子系统: {system_name}") - - -# 使用示例和测试代码 -def example_state_machine_usage(actor): - """ - 状态机使用示例 - """ - print("=== 动画状态机使用示例 ===") - - # 创建状态机 - state_machine = AnimationStateMachine(actor) - - # 添加状态 - state_machine.add_state("idle", "idle_anim", loop=True) - state_machine.add_state("walk", "walk_anim", loop=True) - state_machine.add_state("run", "run_anim", loop=True) - state_machine.add_state("jump", ["jump_start", "jump_loop", "jump_end"], loop=False) - - # 设置状态转换 - state_machine.set_transition("idle", "walk") - state_machine.set_transition("walk", "run") - state_machine.set_transition("run", "idle") - - # 进入初始状态 - state_machine.enter_state("idle") - - print("状态机示例设置完成") - return state_machine - - -def example_ik_system_usage(actor): - """ - IK系统使用示例 - """ - print("=== 逆向运动学系统使用示例 ===") - - # 创建IK系统 - ik_system = InverseKinematicsSystem(actor) - - # 添加IK链 - ik_system.add_ik_chain("right_arm", "right_hand") - ik_system.add_ik_chain("left_arm", "left_hand") - ik_system.add_ik_chain("right_leg", "right_foot") - ik_system.add_ik_chain("left_leg", "left_foot") - - # 设置IK目标 - ik_system.set_ik_target("right_arm", (1, 0, 1)) - ik_system.set_ik_target("left_arm", (-1, 0, 1)) - - print("IK系统示例设置完成") - return ik_system - - -def example_curve_editor_usage(actor): - """ - 曲线编辑器使用示例 - """ - print("=== 动画曲线编辑器使用示例 ===") - - # 创建曲线编辑器 - curve_editor = AnimationCurveEditor(actor) - - # 创建曲线 - scale_curve_frames = [(0, 1.0), (1, 1.5), (2, 1.0), (3, 0.8), (4, 1.0)] - curve_editor.create_curve("bounce_scale", scale_curve_frames, 'smooth') - - position_curve_frames = [(0, 0), (1, 0.5), (2, 0), (3, -0.3), (4, 0)] - curve_editor.create_curve("bounce_position", position_curve_frames, 'smooth') - - # 添加曲线目标 - curve_editor.add_curve_target("character_scale", "bounce_scale", "character", "scale") - curve_editor.add_curve_target("character_height", "bounce_position", "character", "position") - - print("曲线编辑器示例设置完成") - return curve_editor - - -def run_advanced_examples(actor): - """ - 运行所有高级功能示例 - """ - print("开始运行高级动画功能示例...") - - state_machine = example_state_machine_usage(actor) - ik_system = example_ik_system_usage(actor) - curve_editor = example_curve_editor_usage(actor) - - print("所有高级功能示例运行完成!") - - return state_machine, ik_system, curve_editor - - -if __name__ == "__main__": - # 这里可以添加更多测试代码 +""" +骨骼动画系统插件 - 高级功能扩展 +提供动画状态机、IK系统、动画曲线编辑等高级功能 +""" + +import math +from panda3d.core import * +from direct.actor.Actor import Actor +from direct.interval.IntervalGlobal import * + + +class AnimationStateMachine: + """ + 动画状态机 + 管理复杂的动画状态转换和混合 + """ + + def __init__(self, actor): + self.actor = actor + self.states = {} # 状态字典 {state_name: state_data} + self.current_state = None + self.transitions = {} # 状态转换 {from_state: {to_state: condition_func}} + self.state_callbacks = {} # 状态回调 {state_name: {event: callback_func}} + + def add_state(self, state_name, animations, loop=True, play_rate=1.0, blend_time=0.3): + """ + 添加动画状态 + :param state_name: 状态名称 + :param animations: 动画列表或单个动画名称 + :param loop: 是否循环 + :param play_rate: 播放速度 + :param blend_time: 状态切换混合时间 + """ + if isinstance(animations, str): + animations = [animations] + + self.states[state_name] = { + 'animations': animations, + 'loop': loop, + 'play_rate': play_rate, + 'blend_time': blend_time, + 'active_interval': None + } + + print(f"添加动画状态: {state_name}") + + def remove_state(self, state_name): + """ + 移除动画状态 + :param state_name: 状态名称 + """ + if state_name in self.states: + # 如果是当前状态,先停止 + if self.current_state == state_name: + self.stop_current_state() + del self.states[state_name] + print(f"移除动画状态: {state_name}") + + def set_transition(self, from_state, to_state, condition_func=None): + """ + 设置状态转换条件 + :param from_state: 起始状态 + :param to_state: 目标状态 + :param condition_func: 转换条件函数,返回True时执行转换 + """ + if from_state not in self.transitions: + self.transitions[from_state] = {} + self.transitions[from_state][to_state] = condition_func + print(f"设置状态转换: {from_state} -> {to_state}") + + def set_state_callback(self, state_name, event, callback_func): + """ + 设置状态回调 + :param state_name: 状态名称 + :param event: 事件类型 ('enter', 'exit', 'update') + :param callback_func: 回调函数 + """ + if state_name not in self.state_callbacks: + self.state_callbacks[state_name] = {} + self.state_callbacks[state_name][event] = callback_func + + def enter_state(self, state_name): + """ + 进入指定状态 + :param state_name: 状态名称 + """ + if state_name not in self.states: + print(f"错误: 状态 {state_name} 不存在") + return False + + # 退出当前状态 + if self.current_state: + self.exit_current_state() + + # 进入新状态 + state_data = self.states[state_name] + + # 创建动画序列 + intervals = [] + for anim_name in state_data['animations']: + if anim_name in self.actor.getAnimNames(): + interval = self.actor.actorInterval( + anim_name, + loop=state_data['loop'], + playRate=state_data['play_rate'] + ) + intervals.append(interval) + + if intervals: + if len(intervals) == 1: + state_data['active_interval'] = intervals[0] + else: + state_data['active_interval'] = Sequence(*intervals) + + # 启动动画 + state_data['active_interval'].start() + + self.current_state = state_name + + # 调用进入回调 + if state_name in self.state_callbacks and 'enter' in self.state_callbacks[state_name]: + self.state_callbacks[state_name]['enter']() + + print(f"进入动画状态: {state_name}") + return True + + def exit_current_state(self): + """ + 退出当前状态 + """ + if not self.current_state: + return + + state_data = self.states[self.current_state] + + # 停止当前动画 + if state_data['active_interval']: + state_data['active_interval'].finish() + state_data['active_interval'] = None + + # 调用退出回调 + if self.current_state in self.state_callbacks and 'exit' in self.state_callbacks[self.current_state]: + self.state_callbacks[self.current_state]['exit']() + + print(f"退出动画状态: {self.current_state}") + self.current_state = None + + def stop_current_state(self): + """ + 停止当前状态 + """ + if not self.current_state: + return + + state_data = self.states[self.current_state] + + # 停止当前动画 + if state_data['active_interval']: + state_data['active_interval'].finish() + state_data['active_interval'] = None + + def update(self, dt): + """ + 更新状态机 + :param dt: 时间增量 + """ + if not self.current_state: + return + + # 调用更新回调 + if self.current_state in self.state_callbacks and 'update' in self.state_callbacks[self.current_state]: + self.state_callbacks[self.current_state]['update'](dt) + + # 检查状态转换 + if self.current_state in self.transitions: + for to_state, condition_func in self.transitions[self.current_state].items(): + if condition_func is None or condition_func(): + self.enter_state(to_state) + break + + def get_current_state(self): + """ + 获取当前状态 + :return: 当前状态名称 + """ + return self.current_state + + def get_available_states(self): + """ + 获取所有可用状态 + :return: 状态名称列表 + """ + return list(self.states.keys()) + + +class InverseKinematicsSystem: + """ + 逆向运动学系统 + 实现骨骼的IK控制 + """ + + def __init__(self, actor): + self.actor = actor + self.ik_chains = {} # IK链 {chain_name: chain_data} + self.targets = {} # IK目标 {target_name: target_node} + + def add_ik_chain(self, chain_name, end_effector, pole_vector=None, iterations=10): + """ + 添加IK链 + :param chain_name: 链名称 + :param end_effector: 终端效应器(骨骼名称) + :param pole_vector: 极向量(用于控制弯曲方向) + :param iterations: 迭代次数 + """ + # 查找骨骼 + effector_joint = self.actor.exposeJoint(None, "modelRoot", end_effector) + if not effector_joint: + print(f"错误: 未找到骨骼 {end_effector}") + return + + self.ik_chains[chain_name] = { + 'end_effector': end_effector, + 'effector_joint': effector_joint, + 'pole_vector': pole_vector, + 'iterations': iterations, + 'enabled': True + } + + print(f"添加IK链: {chain_name} -> {end_effector}") + + def remove_ik_chain(self, chain_name): + """ + 移除IK链 + :param chain_name: 链名称 + """ + if chain_name in self.ik_chains: + del self.ik_chains[chain_name] + print(f"移除IK链: {chain_name}") + + def set_ik_target(self, chain_name, target_pos): + """ + 设置IK目标位置 + :param chain_name: 链名称 + :param target_pos: 目标位置 (x, y, z) + """ + if chain_name not in self.ik_chains: + print(f"错误: IK链 {chain_name} 不存在") + return + + chain_data = self.ik_chains[chain_name] + if not chain_data['enabled']: + return + + # 简化的IK实现(CCD算法) + effector_name = chain_data['end_effector'] + iterations = chain_data['iterations'] + + # 获取当前终端效应器位置 + effector_joint = chain_data['effector_joint'] + current_pos = effector_joint.getPos(self.actor) + target_pos = Point3(*target_pos) + + # 计算距离 + distance = (target_pos - current_pos).length() + if distance < 0.01: # 阈值 + return + + # CCD算法迭代 + for _ in range(iterations): + # 从终端向根部迭代调整骨骼 + # 这里简化处理,实际应该遍历整条骨骼链 + # 为了示例,我们只调整终端效应器朝向 + direction = target_pos - current_pos + direction.normalize() + + # 简化的朝向调整 + effector_joint.lookAt(target_pos) + + # 更新位置 + current_pos = effector_joint.getPos(self.actor) + distance = (target_pos - current_pos).length() + if distance < 0.01: + break + + def enable_ik_chain(self, chain_name, enabled=True): + """ + 启用/禁用IK链 + :param chain_name: 链名称 + :param enabled: 是否启用 + """ + if chain_name in self.ik_chains: + self.ik_chains[chain_name]['enabled'] = enabled + status = "启用" if enabled else "禁用" + print(f"{status}IK链: {chain_name}") + + def batch_set_targets(self, targets_dict): + """ + 批量设置IK目标 + :param targets_dict: {chain_name: target_pos} + """ + for chain_name, target_pos in targets_dict.items(): + self.set_ik_target(chain_name, target_pos) + + +class AnimationCurveEditor: + """ + 动画曲线编辑器 + 支持对动画属性进行曲线编辑和自定义 + """ + + def __init__(self, actor): + self.actor = actor + self.curves = {} # 曲线字典 {curve_name: curve_data} + self.curve_targets = {} # 曲线目标 {target_name: target_data} + + def create_curve(self, curve_name, keyframes, interpolation='linear'): + """ + 创建动画曲线 + :param curve_name: 曲线名称 + :param keyframes: 关键帧列表 [(time, value), ...] + :param interpolation: 插值方式 ('linear', 'smooth', 'step') + """ + self.curves[curve_name] = { + 'keyframes': sorted(keyframes, key=lambda x: x[0]), + 'interpolation': interpolation, + 'current_value': 0.0 + } + + print(f"创建动画曲线: {curve_name}") + + def remove_curve(self, curve_name): + """ + 移除动画曲线 + :param curve_name: 曲线名称 + """ + if curve_name in self.curves: + del self.curves[curve_name] + print(f"移除动画曲线: {curve_name}") + + def add_curve_target(self, target_name, curve_name, property_path, transform_type='scale'): + """ + 添加曲线目标 + :param target_name: 目标名称 + :param curve_name: 曲线名称 + :param property_path: 属性路径(如骨骼名称) + :param transform_type: 变换类型 ('scale', 'position', 'rotation') + """ + if curve_name not in self.curves: + print(f"错误: 曲线 {curve_name} 不存在") + return + + self.curve_targets[target_name] = { + 'curve_name': curve_name, + 'property_path': property_path, + 'transform_type': transform_type + } + + print(f"添加曲线目标: {target_name} -> {curve_name}") + + def evaluate_curve(self, curve_name, time): + """ + 计算曲线在指定时间的值 + :param curve_name: 曲线名称 + :param time: 时间 + :return: 曲线值 + """ + if curve_name not in self.curves: + return 0.0 + + curve_data = self.curves[curve_name] + keyframes = curve_data['keyframes'] + + if not keyframes: + return 0.0 + + # 边界情况 + if time <= keyframes[0][0]: + return keyframes[0][1] + if time >= keyframes[-1][0]: + return keyframes[-1][1] + + # 查找相邻关键帧 + for i in range(len(keyframes) - 1): + if keyframes[i][0] <= time <= keyframes[i + 1][0]: + t1, v1 = keyframes[i] + t2, v2 = keyframes[i + 1] + + # 根据插值方式计算值 + if curve_data['interpolation'] == 'step': + return v1 + elif curve_data['interpolation'] == 'smooth': + # 平滑插值(三次 Hermite) + t = (time - t1) / (t2 - t1) + t2_sq = t * t + t3_sq = t2_sq * t + h00 = 2 * t3_sq - 3 * t2_sq + 1 + h10 = t3_sq - 2 * t2_sq + t + h01 = -2 * t3_sq + 3 * t2_sq + h11 = t3_sq - t2_sq + return h00 * v1 + h10 * (t2 - t1) + h01 * v2 + h11 * (t2 - t1) + else: # linear + t = (time - t1) / (t2 - t1) + return v1 + (v2 - v1) * t + + return 0.0 + + def update_curves(self, time): + """ + 更新所有曲线 + :param time: 当前时间 + """ + for target_name, target_data in self.curve_targets.items(): + curve_name = target_data['curve_name'] + property_path = target_data['property_path'] + transform_type = target_data['transform_type'] + + # 计算曲线值 + value = self.evaluate_curve(curve_name, time) + + # 应用到目标 + self.apply_curve_value(property_path, transform_type, value) + + # 更新曲线数据 + if curve_name in self.curves: + self.curves[curve_name]['current_value'] = value + + def apply_curve_value(self, property_path, transform_type, value): + """ + 应用曲线值到目标属性 + :param property_path: 属性路径 + :param transform_type: 变换类型 + :param value: 值 + """ + # 查找目标骨骼 + joint = self.actor.exposeJoint(None, "modelRoot", property_path) + if not joint: + return + + # 根据变换类型应用值 + if transform_type == 'scale': + joint.setScale(value, value, value) + elif transform_type == 'position': + current_pos = joint.getPos() + if property_path.endswith('.x'): + joint.setPos(value, current_pos.y, current_pos.z) + elif property_path.endswith('.y'): + joint.setPos(current_pos.x, value, current_pos.z) + elif property_path.endswith('.z'): + joint.setPos(current_pos.x, current_pos.y, value) + elif transform_type == 'rotation': + # 简化处理,实际应该分解为具体的旋转轴 + joint.setHpr(value, value, value) + + +class AdvancedAnimationManager: + """ + 高级动画管理器 + 集成状态机、IK系统和曲线编辑器 + """ + + def __init__(self, actor): + self.actor = actor + self.state_machine = AnimationStateMachine(actor) + self.ik_system = InverseKinematicsSystem(actor) + self.curve_editor = AnimationCurveEditor(actor) + self.enabled_systems = { + 'state_machine': True, + 'ik_system': True, + 'curve_editor': True + } + + def update(self, dt, time): + """ + 更新所有高级动画系统 + :param dt: 时间增量 + :param time: 当前时间 + """ + if self.enabled_systems['state_machine']: + self.state_machine.update(dt) + + if self.enabled_systems['curve_editor']: + self.curve_editor.update_curves(time) + + def enable_system(self, system_name, enabled=True): + """ + 启用/禁用子系统 + :param system_name: 系统名称 ('state_machine', 'ik_system', 'curve_editor') + :param enabled: 是否启用 + """ + if system_name in self.enabled_systems: + self.enabled_systems[system_name] = enabled + status = "启用" if enabled else "禁用" + print(f"{status}动画子系统: {system_name}") + + +# 使用示例和测试代码 +def example_state_machine_usage(actor): + """ + 状态机使用示例 + """ + print("=== 动画状态机使用示例 ===") + + # 创建状态机 + state_machine = AnimationStateMachine(actor) + + # 添加状态 + state_machine.add_state("idle", "idle_anim", loop=True) + state_machine.add_state("walk", "walk_anim", loop=True) + state_machine.add_state("run", "run_anim", loop=True) + state_machine.add_state("jump", ["jump_start", "jump_loop", "jump_end"], loop=False) + + # 设置状态转换 + state_machine.set_transition("idle", "walk") + state_machine.set_transition("walk", "run") + state_machine.set_transition("run", "idle") + + # 进入初始状态 + state_machine.enter_state("idle") + + print("状态机示例设置完成") + return state_machine + + +def example_ik_system_usage(actor): + """ + IK系统使用示例 + """ + print("=== 逆向运动学系统使用示例 ===") + + # 创建IK系统 + ik_system = InverseKinematicsSystem(actor) + + # 添加IK链 + ik_system.add_ik_chain("right_arm", "right_hand") + ik_system.add_ik_chain("left_arm", "left_hand") + ik_system.add_ik_chain("right_leg", "right_foot") + ik_system.add_ik_chain("left_leg", "left_foot") + + # 设置IK目标 + ik_system.set_ik_target("right_arm", (1, 0, 1)) + ik_system.set_ik_target("left_arm", (-1, 0, 1)) + + print("IK系统示例设置完成") + return ik_system + + +def example_curve_editor_usage(actor): + """ + 曲线编辑器使用示例 + """ + print("=== 动画曲线编辑器使用示例 ===") + + # 创建曲线编辑器 + curve_editor = AnimationCurveEditor(actor) + + # 创建曲线 + scale_curve_frames = [(0, 1.0), (1, 1.5), (2, 1.0), (3, 0.8), (4, 1.0)] + curve_editor.create_curve("bounce_scale", scale_curve_frames, 'smooth') + + position_curve_frames = [(0, 0), (1, 0.5), (2, 0), (3, -0.3), (4, 0)] + curve_editor.create_curve("bounce_position", position_curve_frames, 'smooth') + + # 添加曲线目标 + curve_editor.add_curve_target("character_scale", "bounce_scale", "character", "scale") + curve_editor.add_curve_target("character_height", "bounce_position", "character", "position") + + print("曲线编辑器示例设置完成") + return curve_editor + + +def run_advanced_examples(actor): + """ + 运行所有高级功能示例 + """ + print("开始运行高级动画功能示例...") + + state_machine = example_state_machine_usage(actor) + ik_system = example_ik_system_usage(actor) + curve_editor = example_curve_editor_usage(actor) + + print("所有高级功能示例运行完成!") + + return state_machine, ik_system, curve_editor + + +if __name__ == "__main__": + # 这里可以添加更多测试代码 print("高级动画功能模块加载完成") \ No newline at end of file diff --git a/plugins/user/skeletal_animation_system/animation_optimization.py b/plugins/user/skeletal_animation_system/animation_optimization.py index 2f1d9216..9468f121 100644 --- a/plugins/user/skeletal_animation_system/animation_optimization.py +++ b/plugins/user/skeletal_animation_system/animation_optimization.py @@ -1,600 +1,600 @@ -""" -骨骼动画系统插件 - 动画优化和压缩模块 -提供动画压缩、优化、缓存等功能 -""" - -from panda3d.core import * -from direct.actor.Actor import Actor -import math -import json - - -class AnimationCompression: - """ - 动画压缩系统 - 支持关键帧压缩、插值优化等技术 - """ - - def __init__(self): - self.compression_settings = { - 'position_threshold': 0.01, # 位置阈值 - 'rotation_threshold': 0.1, # 旋转阈值(度) - 'scale_threshold': 0.01, # 缩放阈值 - 'enable_quantization': True, # 是否启用量化 - 'quantization_bits': 16 # 量化位数 - } - - def compress_animation(self, actor, anim_name, output_path=None): - """ - 压缩指定动画 - :param actor: Actor对象 - :param anim_name: 动画名称 - :param output_path: 输出路径 - :return: 压缩后的动画数据 - """ - if anim_name not in actor.getAnimNames(): - print(f"错误: 动画 {anim_name} 不存在") - return None - - print(f"开始压缩动画: {anim_name}") - - # 获取动画控制 - anim_control = actor.getAnimControl(anim_name) - if not anim_control: - print(f"错误: 无法获取动画控制 {anim_name}") - return None - - # 获取动画数据 - num_frames = anim_control.getNumFrames() - frame_rate = anim_control.getFrameRate() - - print(f" 动画信息: {num_frames} 帧, {frame_rate} FPS") - - # 压缩数据结构 - compressed_data = { - 'name': anim_name, - 'frame_rate': frame_rate, - 'duration': num_frames / frame_rate if frame_rate > 0 else 0, - 'channels': {} - } - - # 获取骨骼列表 - channels = self._extract_animation_channels(actor, anim_name, num_frames) - - # 压缩每个通道 - for bone_name, channel_data in channels.items(): - compressed_channel = self._compress_channel(channel_data) - compressed_data['channels'][bone_name] = compressed_channel - - # 保存压缩数据 - if output_path: - self._save_compressed_animation(compressed_data, output_path) - - compression_ratio = self._calculate_compression_ratio(channels, compressed_data) - print(f" 压缩完成,压缩比: {compression_ratio:.2f}:1") - - return compressed_data - - def _extract_animation_channels(self, actor, anim_name, num_frames): - """ - 提取动画通道数据 - :param actor: Actor对象 - :param anim_name: 动画名称 - :param num_frames: 帧数 - :return: 通道数据字典 - """ - channels = {} - - # 获取所有骨骼 - bundle = actor.getPartBundleDict().get('modelRoot') - if not bundle: - return channels - - # 遍历骨骼 - for i in range(bundle.getNumChildren()): - child = bundle.getChild(i) - if hasattr(child, 'getName'): - bone_name = child.getName() - channels[bone_name] = self._extract_bone_channel(actor, bone_name, anim_name, num_frames) - - return channels - - def _extract_bone_channel(self, actor, bone_name, anim_name, num_frames): - """ - 提取单个骨骼的动画通道 - :param actor: Actor对象 - :param bone_name: 骨骼名称 - :param anim_name: 动画名称 - :param num_frames: 帧数 - :return: 骨骼动画数据 - """ - channel_data = { - 'positions': [], - 'rotations': [], - 'scales': [] - } - - # 为每一帧提取数据 - for frame in range(num_frames): - # 设置到指定帧 - actor.pose(anim_name, frame) - - # 获取骨骼变换 - joint = actor.exposeJoint(None, "modelRoot", bone_name) - if joint: - pos = joint.getPos() - hpr = joint.getHpr() - scale = joint.getScale() - - channel_data['positions'].append((pos.x, pos.y, pos.z)) - channel_data['rotations'].append((hpr.x, hpr.y, hpr.z)) - channel_data['scales'].append((scale.x, scale.y, scale.z)) - - return channel_data - - def _compress_channel(self, channel_data): - """ - 压缩单个通道 - :param channel_data: 通道数据 - :return: 压缩后的通道数据 - """ - compressed_channel = { - 'positions': self._compress_keyframes(channel_data['positions'], 'position'), - 'rotations': self._compress_keyframes(channel_data['rotations'], 'rotation'), - 'scales': self._compress_keyframes(channel_data['scales'], 'scale') - } - - return compressed_channel - - def _compress_keyframes(self, keyframes, data_type): - """ - 压缩关键帧数据 - :param keyframes: 关键帧数据 - :param data_type: 数据类型 ('position', 'rotation', 'scale') - :return: 压缩后的关键帧 - """ - if not keyframes: - return [] - - # 根据数据类型获取阈值 - if data_type == 'position': - threshold = self.compression_settings['position_threshold'] - elif data_type == 'rotation': - threshold = self.compression_settings['rotation_threshold'] - else: # scale - threshold = self.compression_settings['scale_threshold'] - - compressed_keyframes = [] - compressed_keyframes.append((0, keyframes[0])) # 第一帧总是保留 - - # 关键帧简化算法 - last_keyframe = keyframes[0] - for i in range(1, len(keyframes)): - current_keyframe = keyframes[i] - - # 检查是否需要保留这一帧 - if self._needs_keyframe(last_keyframe, current_keyframe, threshold, data_type): - compressed_keyframes.append((i, current_keyframe)) - last_keyframe = current_keyframe - - # 总是保留最后一帧 - if len(keyframes) > 1 and (len(compressed_keyframes) == 0 or compressed_keyframes[-1][0] != len(keyframes) - 1): - compressed_keyframes.append((len(keyframes) - 1, keyframes[-1])) - - # 应用量化 - if self.compression_settings['enable_quantization']: - compressed_keyframes = self._quantize_keyframes(compressed_keyframes, data_type) - - return compressed_keyframes - - def _needs_keyframe(self, last_frame, current_frame, threshold, data_type): - """ - 判断是否需要保留关键帧 - :param last_frame: 上一帧数据 - :param current_frame: 当前帧数据 - :param threshold: 阈值 - :param data_type: 数据类型 - :return: 是否需要保留 - """ - if data_type == 'rotation': - # 旋转使用角度差 - diff_x = abs(current_frame[0] - last_frame[0]) - diff_y = abs(current_frame[1] - last_frame[1]) - diff_z = abs(current_frame[2] - last_frame[2]) - return diff_x > threshold or diff_y > threshold or diff_z > threshold - else: - # 位置和缩放使用欧几里得距离 - diff_x = abs(current_frame[0] - last_frame[0]) - diff_y = abs(current_frame[1] - last_frame[1]) - diff_z = abs(current_frame[2] - last_frame[2]) - distance = math.sqrt(diff_x**2 + diff_y**2 + diff_z**2) - return distance > threshold - - def _quantize_keyframes(self, keyframes, data_type): - """ - 量化关键帧数据 - :param keyframes: 关键帧数据 - :param data_type: 数据类型 - :return: 量化后的关键帧 - """ - bits = self.compression_settings['quantization_bits'] - max_value = 2**(bits - 1) - 1 - min_value = -2**(bits - 1) - - quantized_keyframes = [] - for frame_index, frame_data in keyframes: - quantized_data = [] - for value in frame_data: - # 简化量化过程 - quantized_value = max(min_value, min(max_value, int(value * (max_value / 10.0)))) - quantized_data.append(quantized_value) - quantized_keyframes.append((frame_index, tuple(quantized_data))) - - return quantized_keyframes - - def _calculate_compression_ratio(self, original_data, compressed_data): - """ - 计算压缩比 - :param original_data: 原始数据 - :param compressed_data: 压缩数据 - :return: 压缩比 - """ - # 简化的压缩比计算 - original_size = sum(len(channel['positions']) + len(channel['rotations']) + len(channel['scales']) - for channel in original_data.values()) - compressed_size = sum(len(channel['positions']) + len(channel['rotations']) + len(channel['scales']) - for channel in compressed_data['channels'].values()) - - if compressed_size == 0: - return 1.0 - - return original_size / compressed_size - - def _save_compressed_animation(self, compressed_data, output_path): - """ - 保存压缩动画数据 - :param compressed_data: 压缩数据 - :param output_path: 输出路径 - """ - try: - with open(output_path, 'w') as f: - json.dump(compressed_data, f, indent=2) - print(f"压缩动画已保存到: {output_path}") - except Exception as e: - print(f"保存压缩动画失败: {e}") - - -class AnimationCaching: - """ - 动画缓存系统 - 提供动画数据缓存以提高性能 - """ - - def __init__(self, max_cache_size=100): - self.max_cache_size = max_cache_size - self.cache = {} # {anim_key: anim_data} - self.access_order = [] # 访问顺序,用于LRU - self.cache_stats = { - 'hits': 0, - 'misses': 0, - 'evictions': 0 - } - - def get_cached_animation(self, actor_id, anim_name): - """ - 获取缓存的动画数据 - :param actor_id: Actor ID - :param anim_name: 动画名称 - :return: 动画数据或None - """ - cache_key = f"{actor_id}:{anim_name}" - - if cache_key in self.cache: - # 缓存命中 - self.cache_stats['hits'] += 1 - - # 更新访问顺序 - if cache_key in self.access_order: - self.access_order.remove(cache_key) - self.access_order.append(cache_key) - - print(f"动画缓存命中: {cache_key}") - return self.cache[cache_key] - else: - # 缓存未命中 - self.cache_stats['misses'] += 1 - print(f"动画缓存未命中: {cache_key}") - return None - - def cache_animation(self, actor_id, anim_name, anim_data): - """ - 缓存动画数据 - :param actor_id: Actor ID - :param anim_name: 动画名称 - :param anim_data: 动画数据 - """ - cache_key = f"{actor_id}:{anim_name}" - - # 如果缓存已满,移除最久未使用的项 - if len(self.cache) >= self.max_cache_size: - if self.access_order: - oldest_key = self.access_order.pop(0) - del self.cache[oldest_key] - self.cache_stats['evictions'] += 1 - print(f"动画缓存已满,移除最旧项: {oldest_key}") - - # 添加新项 - self.cache[cache_key] = anim_data - self.access_order.append(cache_key) - - print(f"动画已缓存: {cache_key}") - - def invalidate_cache(self, actor_id=None, anim_name=None): - """ - 使缓存失效 - :param actor_id: Actor ID,如果为None则匹配所有 - :param anim_name: 动画名称,如果为None则匹配所有 - """ - if actor_id is None and anim_name is None: - # 清空所有缓存 - self.cache.clear() - self.access_order.clear() - print("所有动画缓存已清空") - return - - # 构建匹配模式 - if actor_id and anim_name: - pattern = f"{actor_id}:{anim_name}" - keys_to_remove = [k for k in self.cache.keys() if k == pattern] - elif actor_id: - pattern = f"{actor_id}:" - keys_to_remove = [k for k in self.cache.keys() if k.startswith(pattern)] - else: # anim_name only - keys_to_remove = [k for k in self.cache.keys() if k.endswith(f":{anim_name}")] - - # 移除匹配项 - for key in keys_to_remove: - if key in self.cache: - del self.cache[key] - if key in self.access_order: - self.access_order.remove(key) - - print(f"已使 {len(keys_to_remove)} 项缓存失效") - - def get_cache_stats(self): - """ - 获取缓存统计信息 - :return: 统计信息字典 - """ - total_requests = self.cache_stats['hits'] + self.cache_stats['misses'] - hit_rate = self.cache_stats['hits'] / total_requests if total_requests > 0 else 0 - - return { - 'cache_size': len(self.cache), - 'max_size': self.max_cache_size, - 'hits': self.cache_stats['hits'], - 'misses': self.cache_stats['misses'], - 'evictions': self.cache_stats['evictions'], - 'hit_rate': hit_rate, - 'total_requests': total_requests - } - - def clear_stats(self): - """ - 清空统计信息 - """ - self.cache_stats = { - 'hits': 0, - 'misses': 0, - 'evictions': 0 - } - - -class AnimationLOD: - """ - 动画细节层次系统 - 根据距离和重要性调整动画质量 - """ - - def __init__(self, world): - self.world = world - self.lod_settings = { - 'distance_lod': True, - 'distance_thresholds': [10, 20, 50], # 距离阈值 - 'lod_levels': [0, 1, 2, 3], # LOD级别 - 'performance_lod': True, - 'fps_threshold': 30, # FPS阈值 - 'automatic_lod': True - } - self.actor_lod_levels = {} # {actor_id: lod_level} - self.performance_monitor = { - 'last_fps': 60, - 'frame_times': [] - } - - def update_lod_levels(self, camera_pos): - """ - 更新所有Actor的LOD级别 - :param camera_pos: 摄像机位置 - """ - if not self.lod_settings['automatic_lod']: - return - - # 获取所有Actor - actors = self.world.render.findAllMatches("**/+ActorNode") - - for actor_np in actors: - actor = actor_np.node() - if not isinstance(actor, Actor): - continue - - actor_id = actor_np.getName() - - # 计算距离LOD - distance_lod = 0 - if self.lod_settings['distance_lod']: - distance = (actor_np.getPos() - camera_pos).length() - thresholds = self.lod_settings['distance_thresholds'] - - for i, threshold in enumerate(thresholds): - if distance > threshold: - distance_lod = i + 1 - - # 计算性能LOD - performance_lod = 0 - if self.lod_settings['performance_lod']: - if self.performance_monitor['last_fps'] < self.lod_settings['fps_threshold']: - performance_lod = 1 - - # 确定最终LOD级别 - final_lod = max(distance_lod, performance_lod) - self.actor_lod_levels[actor_id] = final_lod - - # 应用LOD设置 - self._apply_lod_settings(actor, actor_id, final_lod) - - def _apply_lod_settings(self, actor, actor_id, lod_level): - """ - 应用LOD设置到Actor - :param actor: Actor对象 - :param actor_id: Actor ID - :param lod_level: LOD级别 - """ - # LOD级别说明: - # 0 - 最高质量 (全帧率,全骨骼) - # 1 - 中等质量 (半帧率,全骨骼) - # 2 - 低质量 (1/4帧率,简化骨骼) - # 3 - 最低质量 (1/10帧率,极简骨骼) - - if lod_level == 0: - # 最高质量 - 不做调整 - pass - elif lod_level == 1: - # 中等质量 - 降低播放速度 - for anim_name in actor.getAnimNames(): - current_rate = actor.getPlayRate(anim_name) - actor.setPlayRate(current_rate * 0.5, anim_name) - elif lod_level == 2: - # 低质量 - 进一步降低播放速度 - for anim_name in actor.getAnimNames(): - current_rate = actor.getPlayRate(anim_name) - actor.setPlayRate(current_rate * 0.25, anim_name) - elif lod_level == 3: - # 最低质量 - 大幅降低播放速度 - for anim_name in actor.getAnimNames(): - current_rate = actor.getPlayRate(anim_name) - actor.setPlayRate(current_rate * 0.1, anim_name) - - print(f"Actor {actor_id} LOD级别设置为: {lod_level}") - - def set_actor_importance(self, actor_id, importance): - """ - 设置Actor重要性(影响LOD计算) - :param actor_id: Actor ID - :param importance: 重要性 (0.0 - 1.0) - """ - # 重要性高的Actor会获得更好的LOD级别 - # 这里可以实现更复杂的逻辑 - pass - - def update_performance_stats(self, current_fps, frame_time): - """ - 更新性能统计 - :param current_fps: 当前FPS - :param frame_time: 帧时间 - """ - self.performance_monitor['last_fps'] = current_fps - self.performance_monitor['frame_times'].append(frame_time) - - # 保持最近30帧的统计数据 - if len(self.performance_monitor['frame_times']) > 30: - self.performance_monitor['frame_times'] = self.performance_monitor['frame_times'][-30:] - - def get_lod_stats(self): - """ - 获取LOD统计信息 - :return: 统计信息 - """ - lod_counts = {} - for lod_level in self.actor_lod_levels.values(): - if lod_level not in lod_counts: - lod_counts[lod_level] = 0 - lod_counts[lod_level] += 1 - - return { - 'actor_lod_distribution': lod_counts, - 'current_fps': self.performance_monitor['last_fps'], - 'average_frame_time': sum(self.performance_monitor['frame_times']) / len(self.performance_monitor['frame_times']) - if self.performance_monitor['frame_times'] else 0 - } - - -# 使用示例 -def example_compression_usage(actor): - """ - 动画压缩使用示例 - """ - print("=== 动画压缩使用示例 ===") - - compressor = AnimationCompression() - - # 压缩所有动画 - for anim_name in actor.getAnimNames(): - print(f"压缩动画: {anim_name}") - compressed_data = compressor.compress_animation(actor, anim_name) - if compressed_data: - print(f" 压缩成功: {len(compressed_data['channels'])} 个骨骼通道") - - print("动画压缩示例完成") - - -def example_caching_usage(world): - """ - 动画缓存使用示例 - """ - print("=== 动画缓存使用示例 ===") - - cache = AnimationCaching(max_cache_size=50) - - # 模拟缓存一些动画数据 - dummy_data = {'frames': 100, 'channels': 20} - cache.cache_animation("actor_1", "walk", dummy_data) - cache.cache_animation("actor_1", "run", dummy_data) - cache.cache_animation("actor_2", "idle", dummy_data) - - # 尝试获取缓存数据 - data = cache.get_cached_animation("actor_1", "walk") - if data: - print(" 成功获取缓存数据") - - # 查看缓存统计 - stats = cache.get_cache_stats() - print(f" 缓存统计: {stats}") - - print("动画缓存示例完成") - - -def example_lod_usage(world): - """ - 动画LOD使用示例 - """ - print("=== 动画LOD使用示例 ===") - - lod_system = AnimationLOD(world) - - # 模拟更新LOD级别 - camera_pos = Point3(0, 0, 0) - lod_system.update_lod_levels(camera_pos) - - # 更新性能统计 - lod_system.update_performance_stats(45, 0.022) # 45 FPS, 22ms per frame - - # 获取LOD统计 - stats = lod_system.get_lod_stats() - print(f" LOD统计: {stats}") - - print("动画LOD示例完成") - - -if __name__ == "__main__": +""" +骨骼动画系统插件 - 动画优化和压缩模块 +提供动画压缩、优化、缓存等功能 +""" + +from panda3d.core import * +from direct.actor.Actor import Actor +import math +import json + + +class AnimationCompression: + """ + 动画压缩系统 + 支持关键帧压缩、插值优化等技术 + """ + + def __init__(self): + self.compression_settings = { + 'position_threshold': 0.01, # 位置阈值 + 'rotation_threshold': 0.1, # 旋转阈值(度) + 'scale_threshold': 0.01, # 缩放阈值 + 'enable_quantization': True, # 是否启用量化 + 'quantization_bits': 16 # 量化位数 + } + + def compress_animation(self, actor, anim_name, output_path=None): + """ + 压缩指定动画 + :param actor: Actor对象 + :param anim_name: 动画名称 + :param output_path: 输出路径 + :return: 压缩后的动画数据 + """ + if anim_name not in actor.getAnimNames(): + print(f"错误: 动画 {anim_name} 不存在") + return None + + print(f"开始压缩动画: {anim_name}") + + # 获取动画控制 + anim_control = actor.getAnimControl(anim_name) + if not anim_control: + print(f"错误: 无法获取动画控制 {anim_name}") + return None + + # 获取动画数据 + num_frames = anim_control.getNumFrames() + frame_rate = anim_control.getFrameRate() + + print(f" 动画信息: {num_frames} 帧, {frame_rate} FPS") + + # 压缩数据结构 + compressed_data = { + 'name': anim_name, + 'frame_rate': frame_rate, + 'duration': num_frames / frame_rate if frame_rate > 0 else 0, + 'channels': {} + } + + # 获取骨骼列表 + channels = self._extract_animation_channels(actor, anim_name, num_frames) + + # 压缩每个通道 + for bone_name, channel_data in channels.items(): + compressed_channel = self._compress_channel(channel_data) + compressed_data['channels'][bone_name] = compressed_channel + + # 保存压缩数据 + if output_path: + self._save_compressed_animation(compressed_data, output_path) + + compression_ratio = self._calculate_compression_ratio(channels, compressed_data) + print(f" 压缩完成,压缩比: {compression_ratio:.2f}:1") + + return compressed_data + + def _extract_animation_channels(self, actor, anim_name, num_frames): + """ + 提取动画通道数据 + :param actor: Actor对象 + :param anim_name: 动画名称 + :param num_frames: 帧数 + :return: 通道数据字典 + """ + channels = {} + + # 获取所有骨骼 + bundle = actor.getPartBundleDict().get('modelRoot') + if not bundle: + return channels + + # 遍历骨骼 + for i in range(bundle.getNumChildren()): + child = bundle.getChild(i) + if hasattr(child, 'getName'): + bone_name = child.getName() + channels[bone_name] = self._extract_bone_channel(actor, bone_name, anim_name, num_frames) + + return channels + + def _extract_bone_channel(self, actor, bone_name, anim_name, num_frames): + """ + 提取单个骨骼的动画通道 + :param actor: Actor对象 + :param bone_name: 骨骼名称 + :param anim_name: 动画名称 + :param num_frames: 帧数 + :return: 骨骼动画数据 + """ + channel_data = { + 'positions': [], + 'rotations': [], + 'scales': [] + } + + # 为每一帧提取数据 + for frame in range(num_frames): + # 设置到指定帧 + actor.pose(anim_name, frame) + + # 获取骨骼变换 + joint = actor.exposeJoint(None, "modelRoot", bone_name) + if joint: + pos = joint.getPos() + hpr = joint.getHpr() + scale = joint.getScale() + + channel_data['positions'].append((pos.x, pos.y, pos.z)) + channel_data['rotations'].append((hpr.x, hpr.y, hpr.z)) + channel_data['scales'].append((scale.x, scale.y, scale.z)) + + return channel_data + + def _compress_channel(self, channel_data): + """ + 压缩单个通道 + :param channel_data: 通道数据 + :return: 压缩后的通道数据 + """ + compressed_channel = { + 'positions': self._compress_keyframes(channel_data['positions'], 'position'), + 'rotations': self._compress_keyframes(channel_data['rotations'], 'rotation'), + 'scales': self._compress_keyframes(channel_data['scales'], 'scale') + } + + return compressed_channel + + def _compress_keyframes(self, keyframes, data_type): + """ + 压缩关键帧数据 + :param keyframes: 关键帧数据 + :param data_type: 数据类型 ('position', 'rotation', 'scale') + :return: 压缩后的关键帧 + """ + if not keyframes: + return [] + + # 根据数据类型获取阈值 + if data_type == 'position': + threshold = self.compression_settings['position_threshold'] + elif data_type == 'rotation': + threshold = self.compression_settings['rotation_threshold'] + else: # scale + threshold = self.compression_settings['scale_threshold'] + + compressed_keyframes = [] + compressed_keyframes.append((0, keyframes[0])) # 第一帧总是保留 + + # 关键帧简化算法 + last_keyframe = keyframes[0] + for i in range(1, len(keyframes)): + current_keyframe = keyframes[i] + + # 检查是否需要保留这一帧 + if self._needs_keyframe(last_keyframe, current_keyframe, threshold, data_type): + compressed_keyframes.append((i, current_keyframe)) + last_keyframe = current_keyframe + + # 总是保留最后一帧 + if len(keyframes) > 1 and (len(compressed_keyframes) == 0 or compressed_keyframes[-1][0] != len(keyframes) - 1): + compressed_keyframes.append((len(keyframes) - 1, keyframes[-1])) + + # 应用量化 + if self.compression_settings['enable_quantization']: + compressed_keyframes = self._quantize_keyframes(compressed_keyframes, data_type) + + return compressed_keyframes + + def _needs_keyframe(self, last_frame, current_frame, threshold, data_type): + """ + 判断是否需要保留关键帧 + :param last_frame: 上一帧数据 + :param current_frame: 当前帧数据 + :param threshold: 阈值 + :param data_type: 数据类型 + :return: 是否需要保留 + """ + if data_type == 'rotation': + # 旋转使用角度差 + diff_x = abs(current_frame[0] - last_frame[0]) + diff_y = abs(current_frame[1] - last_frame[1]) + diff_z = abs(current_frame[2] - last_frame[2]) + return diff_x > threshold or diff_y > threshold or diff_z > threshold + else: + # 位置和缩放使用欧几里得距离 + diff_x = abs(current_frame[0] - last_frame[0]) + diff_y = abs(current_frame[1] - last_frame[1]) + diff_z = abs(current_frame[2] - last_frame[2]) + distance = math.sqrt(diff_x**2 + diff_y**2 + diff_z**2) + return distance > threshold + + def _quantize_keyframes(self, keyframes, data_type): + """ + 量化关键帧数据 + :param keyframes: 关键帧数据 + :param data_type: 数据类型 + :return: 量化后的关键帧 + """ + bits = self.compression_settings['quantization_bits'] + max_value = 2**(bits - 1) - 1 + min_value = -2**(bits - 1) + + quantized_keyframes = [] + for frame_index, frame_data in keyframes: + quantized_data = [] + for value in frame_data: + # 简化量化过程 + quantized_value = max(min_value, min(max_value, int(value * (max_value / 10.0)))) + quantized_data.append(quantized_value) + quantized_keyframes.append((frame_index, tuple(quantized_data))) + + return quantized_keyframes + + def _calculate_compression_ratio(self, original_data, compressed_data): + """ + 计算压缩比 + :param original_data: 原始数据 + :param compressed_data: 压缩数据 + :return: 压缩比 + """ + # 简化的压缩比计算 + original_size = sum(len(channel['positions']) + len(channel['rotations']) + len(channel['scales']) + for channel in original_data.values()) + compressed_size = sum(len(channel['positions']) + len(channel['rotations']) + len(channel['scales']) + for channel in compressed_data['channels'].values()) + + if compressed_size == 0: + return 1.0 + + return original_size / compressed_size + + def _save_compressed_animation(self, compressed_data, output_path): + """ + 保存压缩动画数据 + :param compressed_data: 压缩数据 + :param output_path: 输出路径 + """ + try: + with open(output_path, 'w') as f: + json.dump(compressed_data, f, indent=2) + print(f"压缩动画已保存到: {output_path}") + except Exception as e: + print(f"保存压缩动画失败: {e}") + + +class AnimationCaching: + """ + 动画缓存系统 + 提供动画数据缓存以提高性能 + """ + + def __init__(self, max_cache_size=100): + self.max_cache_size = max_cache_size + self.cache = {} # {anim_key: anim_data} + self.access_order = [] # 访问顺序,用于LRU + self.cache_stats = { + 'hits': 0, + 'misses': 0, + 'evictions': 0 + } + + def get_cached_animation(self, actor_id, anim_name): + """ + 获取缓存的动画数据 + :param actor_id: Actor ID + :param anim_name: 动画名称 + :return: 动画数据或None + """ + cache_key = f"{actor_id}:{anim_name}" + + if cache_key in self.cache: + # 缓存命中 + self.cache_stats['hits'] += 1 + + # 更新访问顺序 + if cache_key in self.access_order: + self.access_order.remove(cache_key) + self.access_order.append(cache_key) + + print(f"动画缓存命中: {cache_key}") + return self.cache[cache_key] + else: + # 缓存未命中 + self.cache_stats['misses'] += 1 + print(f"动画缓存未命中: {cache_key}") + return None + + def cache_animation(self, actor_id, anim_name, anim_data): + """ + 缓存动画数据 + :param actor_id: Actor ID + :param anim_name: 动画名称 + :param anim_data: 动画数据 + """ + cache_key = f"{actor_id}:{anim_name}" + + # 如果缓存已满,移除最久未使用的项 + if len(self.cache) >= self.max_cache_size: + if self.access_order: + oldest_key = self.access_order.pop(0) + del self.cache[oldest_key] + self.cache_stats['evictions'] += 1 + print(f"动画缓存已满,移除最旧项: {oldest_key}") + + # 添加新项 + self.cache[cache_key] = anim_data + self.access_order.append(cache_key) + + print(f"动画已缓存: {cache_key}") + + def invalidate_cache(self, actor_id=None, anim_name=None): + """ + 使缓存失效 + :param actor_id: Actor ID,如果为None则匹配所有 + :param anim_name: 动画名称,如果为None则匹配所有 + """ + if actor_id is None and anim_name is None: + # 清空所有缓存 + self.cache.clear() + self.access_order.clear() + print("所有动画缓存已清空") + return + + # 构建匹配模式 + if actor_id and anim_name: + pattern = f"{actor_id}:{anim_name}" + keys_to_remove = [k for k in self.cache.keys() if k == pattern] + elif actor_id: + pattern = f"{actor_id}:" + keys_to_remove = [k for k in self.cache.keys() if k.startswith(pattern)] + else: # anim_name only + keys_to_remove = [k for k in self.cache.keys() if k.endswith(f":{anim_name}")] + + # 移除匹配项 + for key in keys_to_remove: + if key in self.cache: + del self.cache[key] + if key in self.access_order: + self.access_order.remove(key) + + print(f"已使 {len(keys_to_remove)} 项缓存失效") + + def get_cache_stats(self): + """ + 获取缓存统计信息 + :return: 统计信息字典 + """ + total_requests = self.cache_stats['hits'] + self.cache_stats['misses'] + hit_rate = self.cache_stats['hits'] / total_requests if total_requests > 0 else 0 + + return { + 'cache_size': len(self.cache), + 'max_size': self.max_cache_size, + 'hits': self.cache_stats['hits'], + 'misses': self.cache_stats['misses'], + 'evictions': self.cache_stats['evictions'], + 'hit_rate': hit_rate, + 'total_requests': total_requests + } + + def clear_stats(self): + """ + 清空统计信息 + """ + self.cache_stats = { + 'hits': 0, + 'misses': 0, + 'evictions': 0 + } + + +class AnimationLOD: + """ + 动画细节层次系统 + 根据距离和重要性调整动画质量 + """ + + def __init__(self, world): + self.world = world + self.lod_settings = { + 'distance_lod': True, + 'distance_thresholds': [10, 20, 50], # 距离阈值 + 'lod_levels': [0, 1, 2, 3], # LOD级别 + 'performance_lod': True, + 'fps_threshold': 30, # FPS阈值 + 'automatic_lod': True + } + self.actor_lod_levels = {} # {actor_id: lod_level} + self.performance_monitor = { + 'last_fps': 60, + 'frame_times': [] + } + + def update_lod_levels(self, camera_pos): + """ + 更新所有Actor的LOD级别 + :param camera_pos: 摄像机位置 + """ + if not self.lod_settings['automatic_lod']: + return + + # 获取所有Actor + actors = self.world.render.findAllMatches("**/+ActorNode") + + for actor_np in actors: + actor = actor_np.node() + if not isinstance(actor, Actor): + continue + + actor_id = actor_np.getName() + + # 计算距离LOD + distance_lod = 0 + if self.lod_settings['distance_lod']: + distance = (actor_np.getPos() - camera_pos).length() + thresholds = self.lod_settings['distance_thresholds'] + + for i, threshold in enumerate(thresholds): + if distance > threshold: + distance_lod = i + 1 + + # 计算性能LOD + performance_lod = 0 + if self.lod_settings['performance_lod']: + if self.performance_monitor['last_fps'] < self.lod_settings['fps_threshold']: + performance_lod = 1 + + # 确定最终LOD级别 + final_lod = max(distance_lod, performance_lod) + self.actor_lod_levels[actor_id] = final_lod + + # 应用LOD设置 + self._apply_lod_settings(actor, actor_id, final_lod) + + def _apply_lod_settings(self, actor, actor_id, lod_level): + """ + 应用LOD设置到Actor + :param actor: Actor对象 + :param actor_id: Actor ID + :param lod_level: LOD级别 + """ + # LOD级别说明: + # 0 - 最高质量 (全帧率,全骨骼) + # 1 - 中等质量 (半帧率,全骨骼) + # 2 - 低质量 (1/4帧率,简化骨骼) + # 3 - 最低质量 (1/10帧率,极简骨骼) + + if lod_level == 0: + # 最高质量 - 不做调整 + pass + elif lod_level == 1: + # 中等质量 - 降低播放速度 + for anim_name in actor.getAnimNames(): + current_rate = actor.getPlayRate(anim_name) + actor.setPlayRate(current_rate * 0.5, anim_name) + elif lod_level == 2: + # 低质量 - 进一步降低播放速度 + for anim_name in actor.getAnimNames(): + current_rate = actor.getPlayRate(anim_name) + actor.setPlayRate(current_rate * 0.25, anim_name) + elif lod_level == 3: + # 最低质量 - 大幅降低播放速度 + for anim_name in actor.getAnimNames(): + current_rate = actor.getPlayRate(anim_name) + actor.setPlayRate(current_rate * 0.1, anim_name) + + print(f"Actor {actor_id} LOD级别设置为: {lod_level}") + + def set_actor_importance(self, actor_id, importance): + """ + 设置Actor重要性(影响LOD计算) + :param actor_id: Actor ID + :param importance: 重要性 (0.0 - 1.0) + """ + # 重要性高的Actor会获得更好的LOD级别 + # 这里可以实现更复杂的逻辑 + pass + + def update_performance_stats(self, current_fps, frame_time): + """ + 更新性能统计 + :param current_fps: 当前FPS + :param frame_time: 帧时间 + """ + self.performance_monitor['last_fps'] = current_fps + self.performance_monitor['frame_times'].append(frame_time) + + # 保持最近30帧的统计数据 + if len(self.performance_monitor['frame_times']) > 30: + self.performance_monitor['frame_times'] = self.performance_monitor['frame_times'][-30:] + + def get_lod_stats(self): + """ + 获取LOD统计信息 + :return: 统计信息 + """ + lod_counts = {} + for lod_level in self.actor_lod_levels.values(): + if lod_level not in lod_counts: + lod_counts[lod_level] = 0 + lod_counts[lod_level] += 1 + + return { + 'actor_lod_distribution': lod_counts, + 'current_fps': self.performance_monitor['last_fps'], + 'average_frame_time': sum(self.performance_monitor['frame_times']) / len(self.performance_monitor['frame_times']) + if self.performance_monitor['frame_times'] else 0 + } + + +# 使用示例 +def example_compression_usage(actor): + """ + 动画压缩使用示例 + """ + print("=== 动画压缩使用示例 ===") + + compressor = AnimationCompression() + + # 压缩所有动画 + for anim_name in actor.getAnimNames(): + print(f"压缩动画: {anim_name}") + compressed_data = compressor.compress_animation(actor, anim_name) + if compressed_data: + print(f" 压缩成功: {len(compressed_data['channels'])} 个骨骼通道") + + print("动画压缩示例完成") + + +def example_caching_usage(world): + """ + 动画缓存使用示例 + """ + print("=== 动画缓存使用示例 ===") + + cache = AnimationCaching(max_cache_size=50) + + # 模拟缓存一些动画数据 + dummy_data = {'frames': 100, 'channels': 20} + cache.cache_animation("actor_1", "walk", dummy_data) + cache.cache_animation("actor_1", "run", dummy_data) + cache.cache_animation("actor_2", "idle", dummy_data) + + # 尝试获取缓存数据 + data = cache.get_cached_animation("actor_1", "walk") + if data: + print(" 成功获取缓存数据") + + # 查看缓存统计 + stats = cache.get_cache_stats() + print(f" 缓存统计: {stats}") + + print("动画缓存示例完成") + + +def example_lod_usage(world): + """ + 动画LOD使用示例 + """ + print("=== 动画LOD使用示例 ===") + + lod_system = AnimationLOD(world) + + # 模拟更新LOD级别 + camera_pos = Point3(0, 0, 0) + lod_system.update_lod_levels(camera_pos) + + # 更新性能统计 + lod_system.update_performance_stats(45, 0.022) # 45 FPS, 22ms per frame + + # 获取LOD统计 + stats = lod_system.get_lod_stats() + print(f" LOD统计: {stats}") + + print("动画LOD示例完成") + + +if __name__ == "__main__": print("动画优化和压缩模块加载完成") \ No newline at end of file diff --git a/plugins/user/skeletal_animation_system/config.json b/plugins/user/skeletal_animation_system/config.json index 5d2668da..633d7863 100644 --- a/plugins/user/skeletal_animation_system/config.json +++ b/plugins/user/skeletal_animation_system/config.json @@ -1,32 +1,32 @@ -{ - "name": "skeletal_animation_system", - "version": "1.0.0", - "author": "EG Team", - "description": "骨骼动画系统,支持模型动画加载、播放控制、混合等高级功能", - "dependencies": [ - "core" - ], - "enabled": true, - "autoload": false, - "config": { - "default_play_rate": 1.0, - "default_loop": false, - "enable_gui": true, - "enable_events": true, - "max_actors": 100, - "animation_cache_size": 50, - "blend_default_time": 1.0, - "debug_mode": false - }, - "supported_formats": [ - ".egg", - ".bam", - ".fbx", - ".gltf", - ".glb" - ], - "animation_extensions": [ - ".egg", - ".bam" - ] +{ + "name": "skeletal_animation_system", + "version": "1.0.0", + "author": "EG Team", + "description": "骨骼动画系统,支持模型动画加载、播放控制、混合等高级功能", + "dependencies": [ + "core" + ], + "enabled": true, + "autoload": false, + "config": { + "default_play_rate": 1.0, + "default_loop": false, + "enable_gui": true, + "enable_events": true, + "max_actors": 100, + "animation_cache_size": 50, + "blend_default_time": 1.0, + "debug_mode": false + }, + "supported_formats": [ + ".egg", + ".bam", + ".fbx", + ".gltf", + ".glb" + ], + "animation_extensions": [ + ".egg", + ".bam" + ] } \ No newline at end of file diff --git a/plugins/user/skeletal_animation_system/examples.py b/plugins/user/skeletal_animation_system/examples.py index 1a429af6..0316ca28 100644 --- a/plugins/user/skeletal_animation_system/examples.py +++ b/plugins/user/skeletal_animation_system/examples.py @@ -1,381 +1,381 @@ -""" -骨骼动画系统插件使用示例 -提供各种使用场景的代码示例 -""" - -def example_basic_usage(world): - """ - 基本使用示例 - """ - print("=== 骨骼动画插件基本使用示例 ===") - - # 获取插件实例 - skeletal_plugin = world.plugin_manager.get_plugin("skeletal_animation_system") - if not skeletal_plugin: - print("错误: 骨骼动画插件未加载") - return - - animation_manager = skeletal_plugin.get_animation_manager() - - # 1. 加载简单的Actor - print("1. 加载Actor...") - actor, actor_id = animation_manager.load_actor( - model_path="models/characters/robot.egg", - position=(0, 0, 0), - scale=1.0 - ) - - if actor: - print(f" 成功加载Actor: {actor_id}") - - # 2. 获取可用动画 - animations = animation_manager.get_actor_animations(actor_id) - print(f"2. 可用动画: {animations}") - - # 3. 播放动画 - if animations: - print("3. 播放第一个动画...") - animation_manager.play_animation( - actor_id=actor_id, - anim_name=animations[0], - loop=True, - play_rate=1.0 - ) - - # 4. 获取动画信息 - if animations: - info = animation_manager.get_animation_info(actor_id, animations[0]) - print(f"4. 动画信息: {info}") - - print("=== 基本使用示例结束 ===\n") - - -def example_advanced_usage(world): - """ - 高级使用示例 - """ - print("=== 骨骼动画插件高级使用示例 ===") - - # 获取插件实例 - skeletal_plugin = world.plugin_manager.get_plugin("skeletal_animation_system") - if not skeletal_plugin: - print("错误: 骨骼动画插件未加载") - return - - animation_manager = skeletal_plugin.get_animation_manager() - - # 1. 加载带多个动画的Actor - print("1. 加载带多个动画的Actor...") - anim_dict = { - "walk": "models/anim/walk.egg", - "run": "models/anim/run.egg", - "jump": "models/anim/jump.egg", - "idle": "models/anim/idle.egg" - } - - actor, actor_id = animation_manager.load_actor( - model_path="models/characters/hero.egg", - anim_dict=anim_dict, - position=(5, 0, 0), - scale=1.2 - ) - - if actor: - print(f" 成功加载Actor: {actor_id}") - - # 2. 播放循环动画 - print("2. 播放循环idle动画...") - animation_manager.play_animation(actor_id, "idle", loop=True) - - # 3. 3秒后切换到walk动画 - def switch_to_walk(): - print("3. 切换到walk动画...") - animation_manager.stop_animation(actor_id, "idle") - animation_manager.play_animation(actor_id, "walk", loop=True) - - # 使用Panda3D的任务系统在3秒后执行 - def schedule_task(task): - if task.time >= 3.0: - switch_to_walk() - return task.done - return task.cont - - world.taskMgr.doMethodLater(3.0, schedule_task, "switch_animation_task") - - # 4. 创建动画序列 - print("4. 创建动画序列...") - sequence = animation_manager.create_animation_sequence( - actor_id, - ["walk", "run", "jump"], - loop=True - ) - - # 5. 动画混合示例 - print("5. 创建动画混合...") - blend = animation_manager.blend_animations( - actor_id, - "walk", - "run", - blend_time=2.0, - loop=True - ) - - print("=== 高级使用示例结束 ===\n") - - -def example_event_driven_usage(world): - """ - 事件驱动使用示例 - """ - print("=== 骨骼动画插件事件驱动使用示例 ===") - - # 1. 通过事件加载Actor - print("1. 通过事件加载Actor...") - world.acceptOnce("example_load_done", lambda: print("Actor加载完成")) - world.send("skeletal.load_actor", [ - "models/characters/npc.egg", - {"greet": "models/anim/greet.egg"}, - "npc_actor", - (10, 0, 0), - 1.0 - ]) - - # 2. 通过事件播放动画 - def play_greeting(): - print("2. 通过事件播放动画...") - world.send("skeletal.play_animation", ["npc_actor", "greet", False, 1.0]) - - # 3秒后播放动画 - def schedule_greeting(task): - if task.time >= 3.0: - play_greeting() - return task.done - return task.cont - - world.taskMgr.doMethodLater(3.0, schedule_greeting, "play_greeting_task") - - print("=== 事件驱动使用示例结束 ===\n") - - -def example_multiple_actors(world): - """ - 多Actor管理示例 - """ - print("=== 骨骼动画插件多Actor管理示例 ===") - - # 获取插件实例 - skeletal_plugin = world.plugin_manager.get_plugin("skeletal_animation_system") - if not skeletal_plugin: - print("错误: 骨骼动画插件未加载") - return - - animation_manager = skeletal_plugin.get_animation_manager() - - # 1. 创建多个Actor - print("1. 创建多个Actor...") - actors_data = [ - { - "model": "models/characters/warrior.egg", - "anims": {"walk": "models/anim/warrior_walk.egg", "attack": "models/anim/warrior_attack.egg"}, - "position": (-5, -5, 0), - "scale": 1.0, - "id": "warrior" - }, - { - "model": "models/characters/mage.egg", - "anims": {"walk": "models/anim/mage_walk.egg", "cast": "models/anim/mage_cast.egg"}, - "position": (5, -5, 0), - "scale": 1.0, - "id": "mage" - }, - { - "model": "models/characters/archer.egg", - "anims": {"walk": "models/anim/archer_walk.egg", "shoot": "models/anim/archer_shoot.egg"}, - "position": (0, -10, 0), - "scale": 1.0, - "id": "archer" - } - ] - - created_actors = [] - for actor_data in actors_data: - actor, actor_id = animation_manager.load_actor( - model_path=actor_data["model"], - anim_dict=actor_data["anims"], - actor_id=actor_data["id"], - position=actor_data["position"], - scale=actor_data["scale"] - ) - - if actor: - created_actors.append(actor_id) - print(f" 创建Actor: {actor_id}") - - # 2. 统一控制所有Actor - print("2. 统一控制所有Actor...") - - def start_all_walk(): - for actor_id in created_actors: - animations = animation_manager.get_actor_animations(actor_id) - if "walk" in animations: - animation_manager.play_animation(actor_id, "walk", loop=True) - - def stop_all_animations(): - for actor_id in created_actors: - animation_manager.stop_all_animations(actor_id) - - # 3秒后开始行走,再过3秒后停止 - def control_all_actors(task): - if task.time >= 3.0 and task.time < 3.1: - start_all_walk() - print(" 所有角色开始行走") - elif task.time >= 6.0 and task.time < 6.1: - stop_all_animations() - print(" 所有角色停止动画") - return task.done - return task.cont - - world.taskMgr.add(control_all_actors, "control_all_actors_task") - - print("=== 多Actor管理示例结束 ===\n") - - -def example_animation_manipulation(world): - """ - 动画操作示例 - """ - print("=== 骨骼动画插件动画操作示例 ===") - - # 获取插件实例 - skeletal_plugin = world.plugin_manager.get_plugin("skeletal_animation_system") - if not skeletal_plugin: - print("错误: 骨骼动画插件未加载") - return - - animation_manager = skeletal_plugin.get_animation_manager() - - # 1. 加载Actor用于演示 - print("1. 加载Actor用于演示...") - anim_dict = { - "walk": "models/anim/walk.egg", - "run": "models/anim/run.egg", - "jump": "models/anim/jump.egg" - } - - actor, actor_id = animation_manager.load_actor( - model_path="models/characters/demo_character.egg", - anim_dict=anim_dict, - position=(0, 10, 0), - scale=1.0 - ) - - if actor: - # 2. 演示播放速度控制 - print("2. 演示播放速度控制...") - animation_manager.play_animation(actor_id, "walk", loop=True, play_rate=0.5) - print(" 以0.5倍速度播放walk动画") - - # 3秒后调整速度 - def change_speed(task): - if task.time >= 3.0 and task.time < 3.1: - animation_manager.set_animation_speed(actor_id, "walk", 2.0) - print(" 调整为2.0倍速度播放walk动画") - return task.done - return task.cont - - world.taskMgr.doMethodLater(3.0, change_speed, "change_speed_task") - - # 3. 演示暂停和恢复 - print("3. 演示暂停和恢复...") - - def pause_resume_demo(task): - if task.time >= 6.0 and task.time < 6.1: - animation_manager.pause_animation(actor_id) - print(" 暂停动画") - elif task.time >= 8.0 and task.time < 8.1: - animation_manager.resume_animation(actor_id) - print(" 恢复动画") - return task.done - return task.cont - - world.taskMgr.doMethodLater(6.0, pause_resume_demo, "pause_resume_task") - - # 4. 演示动画信息获取 - print("4. 演示动画信息获取...") - - def show_animation_info(task): - if task.time >= 10.0 and task.time < 10.1: - info = animation_manager.get_actor_info(actor_id) - print(f" Actor信息: {info}") - return task.done - return task.cont - - world.taskMgr.doMethodLater(10.0, show_animation_info, "show_info_task") - - print("=== 动画操作示例结束 ===\n") - - -def run_all_examples(world): - """ - 运行所有示例 - """ - print("开始运行骨骼动画插件所有示例...") - - example_basic_usage(world) - example_advanced_usage(world) - example_event_driven_usage(world) - example_multiple_actors(world) - example_animation_manipulation(world) - - print("所有示例运行完成!") - - -# 使用说明和API参考 -USAGE_GUIDE = """ -骨骼动画插件使用说明 -================== - -1. 基本概念 ----------- -- Actor: 带有骨骼的3D模型对象 -- Animation: 应用于Actor的动画序列 -- Manager: 管理所有Actor和动画的中心控制器 - -2. 主要功能 ----------- -- Actor加载和管理 -- 动画播放、暂停、停止控制 -- 动画速度调节 -- 动画混合和序列播放 -- 事件驱动的动画控制 -- 多Actor统一管理 - -3. 核心API ----------- -SkeletalAnimationManager: - - load_actor(): 加载Actor模型 - - unload_actor(): 卸载Actor - - play_animation(): 播放动画 - - stop_animation(): 停止动画 - - set_animation_speed(): 设置动画速度 - - pause_animation(): 暂停动画 - - resume_animation(): 恢复动画 - - blend_animations(): 混合动画 - - create_animation_sequence(): 创建动画序列 - -4. 事件系统 ----------- -- skeletal.load_actor: 加载Actor -- skeletal.play_animation: 播放动画 -- skeletal.stop_animation: 停止动画 -- skeletal.set_animation_speed: 设置动画速度 -- skeletal.pause_animation: 暂停动画 -- skeletal.resume_animation: 恢复动画 - -5. 使用示例 ----------- -参见examples.py文件中的各种使用示例 -""" - -if __name__ == "__main__": +""" +骨骼动画系统插件使用示例 +提供各种使用场景的代码示例 +""" + +def example_basic_usage(world): + """ + 基本使用示例 + """ + print("=== 骨骼动画插件基本使用示例 ===") + + # 获取插件实例 + skeletal_plugin = world.plugin_manager.get_plugin("skeletal_animation_system") + if not skeletal_plugin: + print("错误: 骨骼动画插件未加载") + return + + animation_manager = skeletal_plugin.get_animation_manager() + + # 1. 加载简单的Actor + print("1. 加载Actor...") + actor, actor_id = animation_manager.load_actor( + model_path="models/characters/robot.egg", + position=(0, 0, 0), + scale=1.0 + ) + + if actor: + print(f" 成功加载Actor: {actor_id}") + + # 2. 获取可用动画 + animations = animation_manager.get_actor_animations(actor_id) + print(f"2. 可用动画: {animations}") + + # 3. 播放动画 + if animations: + print("3. 播放第一个动画...") + animation_manager.play_animation( + actor_id=actor_id, + anim_name=animations[0], + loop=True, + play_rate=1.0 + ) + + # 4. 获取动画信息 + if animations: + info = animation_manager.get_animation_info(actor_id, animations[0]) + print(f"4. 动画信息: {info}") + + print("=== 基本使用示例结束 ===\n") + + +def example_advanced_usage(world): + """ + 高级使用示例 + """ + print("=== 骨骼动画插件高级使用示例 ===") + + # 获取插件实例 + skeletal_plugin = world.plugin_manager.get_plugin("skeletal_animation_system") + if not skeletal_plugin: + print("错误: 骨骼动画插件未加载") + return + + animation_manager = skeletal_plugin.get_animation_manager() + + # 1. 加载带多个动画的Actor + print("1. 加载带多个动画的Actor...") + anim_dict = { + "walk": "models/anim/walk.egg", + "run": "models/anim/run.egg", + "jump": "models/anim/jump.egg", + "idle": "models/anim/idle.egg" + } + + actor, actor_id = animation_manager.load_actor( + model_path="models/characters/hero.egg", + anim_dict=anim_dict, + position=(5, 0, 0), + scale=1.2 + ) + + if actor: + print(f" 成功加载Actor: {actor_id}") + + # 2. 播放循环动画 + print("2. 播放循环idle动画...") + animation_manager.play_animation(actor_id, "idle", loop=True) + + # 3. 3秒后切换到walk动画 + def switch_to_walk(): + print("3. 切换到walk动画...") + animation_manager.stop_animation(actor_id, "idle") + animation_manager.play_animation(actor_id, "walk", loop=True) + + # 使用Panda3D的任务系统在3秒后执行 + def schedule_task(task): + if task.time >= 3.0: + switch_to_walk() + return task.done + return task.cont + + world.taskMgr.doMethodLater(3.0, schedule_task, "switch_animation_task") + + # 4. 创建动画序列 + print("4. 创建动画序列...") + sequence = animation_manager.create_animation_sequence( + actor_id, + ["walk", "run", "jump"], + loop=True + ) + + # 5. 动画混合示例 + print("5. 创建动画混合...") + blend = animation_manager.blend_animations( + actor_id, + "walk", + "run", + blend_time=2.0, + loop=True + ) + + print("=== 高级使用示例结束 ===\n") + + +def example_event_driven_usage(world): + """ + 事件驱动使用示例 + """ + print("=== 骨骼动画插件事件驱动使用示例 ===") + + # 1. 通过事件加载Actor + print("1. 通过事件加载Actor...") + world.acceptOnce("example_load_done", lambda: print("Actor加载完成")) + world.send("skeletal.load_actor", [ + "models/characters/npc.egg", + {"greet": "models/anim/greet.egg"}, + "npc_actor", + (10, 0, 0), + 1.0 + ]) + + # 2. 通过事件播放动画 + def play_greeting(): + print("2. 通过事件播放动画...") + world.send("skeletal.play_animation", ["npc_actor", "greet", False, 1.0]) + + # 3秒后播放动画 + def schedule_greeting(task): + if task.time >= 3.0: + play_greeting() + return task.done + return task.cont + + world.taskMgr.doMethodLater(3.0, schedule_greeting, "play_greeting_task") + + print("=== 事件驱动使用示例结束 ===\n") + + +def example_multiple_actors(world): + """ + 多Actor管理示例 + """ + print("=== 骨骼动画插件多Actor管理示例 ===") + + # 获取插件实例 + skeletal_plugin = world.plugin_manager.get_plugin("skeletal_animation_system") + if not skeletal_plugin: + print("错误: 骨骼动画插件未加载") + return + + animation_manager = skeletal_plugin.get_animation_manager() + + # 1. 创建多个Actor + print("1. 创建多个Actor...") + actors_data = [ + { + "model": "models/characters/warrior.egg", + "anims": {"walk": "models/anim/warrior_walk.egg", "attack": "models/anim/warrior_attack.egg"}, + "position": (-5, -5, 0), + "scale": 1.0, + "id": "warrior" + }, + { + "model": "models/characters/mage.egg", + "anims": {"walk": "models/anim/mage_walk.egg", "cast": "models/anim/mage_cast.egg"}, + "position": (5, -5, 0), + "scale": 1.0, + "id": "mage" + }, + { + "model": "models/characters/archer.egg", + "anims": {"walk": "models/anim/archer_walk.egg", "shoot": "models/anim/archer_shoot.egg"}, + "position": (0, -10, 0), + "scale": 1.0, + "id": "archer" + } + ] + + created_actors = [] + for actor_data in actors_data: + actor, actor_id = animation_manager.load_actor( + model_path=actor_data["model"], + anim_dict=actor_data["anims"], + actor_id=actor_data["id"], + position=actor_data["position"], + scale=actor_data["scale"] + ) + + if actor: + created_actors.append(actor_id) + print(f" 创建Actor: {actor_id}") + + # 2. 统一控制所有Actor + print("2. 统一控制所有Actor...") + + def start_all_walk(): + for actor_id in created_actors: + animations = animation_manager.get_actor_animations(actor_id) + if "walk" in animations: + animation_manager.play_animation(actor_id, "walk", loop=True) + + def stop_all_animations(): + for actor_id in created_actors: + animation_manager.stop_all_animations(actor_id) + + # 3秒后开始行走,再过3秒后停止 + def control_all_actors(task): + if task.time >= 3.0 and task.time < 3.1: + start_all_walk() + print(" 所有角色开始行走") + elif task.time >= 6.0 and task.time < 6.1: + stop_all_animations() + print(" 所有角色停止动画") + return task.done + return task.cont + + world.taskMgr.add(control_all_actors, "control_all_actors_task") + + print("=== 多Actor管理示例结束 ===\n") + + +def example_animation_manipulation(world): + """ + 动画操作示例 + """ + print("=== 骨骼动画插件动画操作示例 ===") + + # 获取插件实例 + skeletal_plugin = world.plugin_manager.get_plugin("skeletal_animation_system") + if not skeletal_plugin: + print("错误: 骨骼动画插件未加载") + return + + animation_manager = skeletal_plugin.get_animation_manager() + + # 1. 加载Actor用于演示 + print("1. 加载Actor用于演示...") + anim_dict = { + "walk": "models/anim/walk.egg", + "run": "models/anim/run.egg", + "jump": "models/anim/jump.egg" + } + + actor, actor_id = animation_manager.load_actor( + model_path="models/characters/demo_character.egg", + anim_dict=anim_dict, + position=(0, 10, 0), + scale=1.0 + ) + + if actor: + # 2. 演示播放速度控制 + print("2. 演示播放速度控制...") + animation_manager.play_animation(actor_id, "walk", loop=True, play_rate=0.5) + print(" 以0.5倍速度播放walk动画") + + # 3秒后调整速度 + def change_speed(task): + if task.time >= 3.0 and task.time < 3.1: + animation_manager.set_animation_speed(actor_id, "walk", 2.0) + print(" 调整为2.0倍速度播放walk动画") + return task.done + return task.cont + + world.taskMgr.doMethodLater(3.0, change_speed, "change_speed_task") + + # 3. 演示暂停和恢复 + print("3. 演示暂停和恢复...") + + def pause_resume_demo(task): + if task.time >= 6.0 and task.time < 6.1: + animation_manager.pause_animation(actor_id) + print(" 暂停动画") + elif task.time >= 8.0 and task.time < 8.1: + animation_manager.resume_animation(actor_id) + print(" 恢复动画") + return task.done + return task.cont + + world.taskMgr.doMethodLater(6.0, pause_resume_demo, "pause_resume_task") + + # 4. 演示动画信息获取 + print("4. 演示动画信息获取...") + + def show_animation_info(task): + if task.time >= 10.0 and task.time < 10.1: + info = animation_manager.get_actor_info(actor_id) + print(f" Actor信息: {info}") + return task.done + return task.cont + + world.taskMgr.doMethodLater(10.0, show_animation_info, "show_info_task") + + print("=== 动画操作示例结束 ===\n") + + +def run_all_examples(world): + """ + 运行所有示例 + """ + print("开始运行骨骼动画插件所有示例...") + + example_basic_usage(world) + example_advanced_usage(world) + example_event_driven_usage(world) + example_multiple_actors(world) + example_animation_manipulation(world) + + print("所有示例运行完成!") + + +# 使用说明和API参考 +USAGE_GUIDE = """ +骨骼动画插件使用说明 +================== + +1. 基本概念 +---------- +- Actor: 带有骨骼的3D模型对象 +- Animation: 应用于Actor的动画序列 +- Manager: 管理所有Actor和动画的中心控制器 + +2. 主要功能 +---------- +- Actor加载和管理 +- 动画播放、暂停、停止控制 +- 动画速度调节 +- 动画混合和序列播放 +- 事件驱动的动画控制 +- 多Actor统一管理 + +3. 核心API +---------- +SkeletalAnimationManager: + - load_actor(): 加载Actor模型 + - unload_actor(): 卸载Actor + - play_animation(): 播放动画 + - stop_animation(): 停止动画 + - set_animation_speed(): 设置动画速度 + - pause_animation(): 暂停动画 + - resume_animation(): 恢复动画 + - blend_animations(): 混合动画 + - create_animation_sequence(): 创建动画序列 + +4. 事件系统 +---------- +- skeletal.load_actor: 加载Actor +- skeletal.play_animation: 播放动画 +- skeletal.stop_animation: 停止动画 +- skeletal.set_animation_speed: 设置动画速度 +- skeletal.pause_animation: 暂停动画 +- skeletal.resume_animation: 恢复动画 + +5. 使用示例 +---------- +参见examples.py文件中的各种使用示例 +""" + +if __name__ == "__main__": print(USAGE_GUIDE) \ No newline at end of file diff --git a/plugins/user/skeletal_animation_system/network_sync.py b/plugins/user/skeletal_animation_system/network_sync.py index 8f670c95..476282d7 100644 --- a/plugins/user/skeletal_animation_system/network_sync.py +++ b/plugins/user/skeletal_animation_system/network_sync.py @@ -1,684 +1,684 @@ -""" -骨骼动画系统插件 - 网络同步模块 -提供动画状态网络同步、多人协作等功能 -""" - -from panda3d.core import * -from direct.distributed.ClockDelta import * -from direct.showbase.DirectObject import DirectObject -import json -import hashlib - - -class AnimationNetworkSync(DirectObject): - """ - 动画网络同步系统 - 支持多人环境中动画状态的同步 - """ - - def __init__(self, world): - self.world = world - self.network_settings = { - 'enable_sync': True, - 'sync_interval': 0.1, # 同步间隔(秒) - 'interpolation_mode': 'linear', # 插值模式 - 'extrapolation_limit': 0.5, # 外推限制(秒) - 'compression_enabled': True, - 'delta_compression': True - } - - # 网络状态 - self.is_server = False - self.is_client = False - self.connected = False - self.network_actor_states = {} # {actor_id: state_data} - self.local_actor_states = {} # {actor_id: state_data} - self.actor_sync_callbacks = {} # {actor_id: callback_func} - - # 同步定时器 - self.sync_task = None - self.last_sync_time = 0 - - def enable_server_mode(self): - """ - 启用服务器模式 - """ - self.is_server = True - self.is_client = False - self.connected = True - print("动画网络同步系统 - 服务器模式已启用") - - def enable_client_mode(self): - """ - 启用客户端模式 - """ - self.is_server = False - self.is_client = True - self.connected = True - print("动画网络同步系统 - 客户端模式已启用") - - def disable_network_sync(self): - """ - 禁用网络同步 - """ - self.is_server = False - self.is_client = False - self.connected = False - print("动画网络同步系统已禁用") - - def register_actor_for_sync(self, actor_id, actor, sync_callback=None): - """ - 注册Actor以进行网络同步 - :param actor_id: Actor ID - :param actor: Actor对象 - :param sync_callback: 同步回调函数 - """ - self.local_actor_states[actor_id] = { - 'actor': actor, - 'last_state': self._capture_actor_state(actor), - 'last_update': globalClock.getRealTime(), - 'sync_callback': sync_callback - } - - if sync_callback: - self.actor_sync_callbacks[actor_id] = sync_callback - - print(f"Actor {actor_id} 已注册网络同步") - - def unregister_actor_from_sync(self, actor_id): - """ - 取消Actor的网络同步注册 - :param actor_id: Actor ID - """ - if actor_id in self.local_actor_states: - del self.local_actor_states[actor_id] - if actor_id in self.actor_sync_callbacks: - del self.actor_sync_callbacks[actor_id] - if actor_id in self.network_actor_states: - del self.network_actor_states[actor_id] - - print(f"Actor {actor_id} 已取消网络同步注册") - - def _capture_actor_state(self, actor): - """ - 捕获Actor当前状态 - :param actor: Actor对象 - :return: 状态数据 - """ - state = { - 'position': actor.getPos().toTuple(), - 'rotation': actor.getHpr().toTuple(), - 'scale': actor.getScale().toTuple(), - 'active_animations': {}, - 'timestamp': globalClock.getRealTime() - } - - # 捕获活动动画状态 - for anim_name in actor.getAnimNames(): - control = actor.getAnimControl(anim_name) - if control and control.isPlaying(): - state['active_animations'][anim_name] = { - 'frame': actor.getCurrentFrame(anim_name), - 'play_rate': actor.getPlayRate(anim_name), - 'is_looping': control.getLoop() - } - - return state - - def _apply_actor_state(self, actor, state_data): - """ - 应用Actor状态 - :param actor: Actor对象 - :param state_data: 状态数据 - """ - # 应用变换 - if 'position' in state_data: - actor.setPos(*state_data['position']) - if 'rotation' in state_data: - actor.setHpr(*state_data['rotation']) - if 'scale' in state_data: - actor.setScale(*state_data['scale']) - - # 应用动画状态 - if 'active_animations' in state_data: - for anim_name, anim_state in state_data['active_animations'].items(): - if anim_name in actor.getAnimNames(): - # 停止当前动画 - actor.stop(anim_name) - - # 设置到指定帧 - if 'frame' in anim_state: - actor.pose(anim_name, anim_state['frame']) - - # 设置播放速度 - if 'play_rate' in anim_state: - actor.setPlayRate(anim_state['play_rate'], anim_name) - - # 调用自定义同步回调 - actor_id = actor.getName() # 简化获取ID的方法 - if actor_id in self.actor_sync_callbacks: - self.actor_sync_callbacks[actor_id](actor, state_data) - - def send_actor_state(self, actor_id): - """ - 发送Actor状态(服务器模式) - :param actor_id: Actor ID - """ - if not self.is_server or not self.connected: - return - - if actor_id not in self.local_actor_states: - return - - actor_state = self.local_actor_states[actor_id] - actor = actor_state['actor'] - current_state = self._capture_actor_state(actor) - - # 检查状态是否发生变化 - if self._has_state_changed(actor_state['last_state'], current_state): - # 发送状态更新 - self._broadcast_actor_state(actor_id, current_state) - - # 更新最后状态 - actor_state['last_state'] = current_state - actor_state['last_update'] = globalClock.getRealTime() - - def _has_state_changed(self, old_state, new_state, threshold=0.001): - """ - 检查状态是否发生变化 - :param old_state: 旧状态 - :param new_state: 新状态 - :param threshold: 变化阈值 - :return: 是否发生变化 - """ - # 检查位置变化 - if 'position' in old_state and 'position' in new_state: - old_pos = Vec3(*old_state['position']) - new_pos = Vec3(*new_state['position']) - if (old_pos - new_pos).length() > threshold: - return True - - # 检查旋转变化 - if 'rotation' in old_state and 'rotation' in new_state: - old_hpr = Vec3(*old_state['rotation']) - new_hpr = Vec3(*new_state['rotation']) - if (old_hpr - new_hpr).length() > threshold: - return True - - # 检查动画状态变化 - if 'active_animations' in old_state and 'active_animations' in new_state: - if old_state['active_animations'] != new_state['active_animations']: - return True - - return False - - def _broadcast_actor_state(self, actor_id, state_data): - """ - 广播Actor状态 - :param actor_id: Actor ID - :param state_data: 状态数据 - """ - # 在实际实现中,这里会通过网络发送数据 - # 为简化,我们直接更新网络状态 - self.network_actor_states[actor_id] = state_data - print(f"广播Actor {actor_id} 状态更新") - - def receive_actor_state(self, actor_id, state_data): - """ - 接收Actor状态(客户端模式) - :param actor_id: Actor ID - :param state_data: 状态数据 - """ - if not self.is_client or not self.connected: - return - - # 存储接收到的状态 - self.network_actor_states[actor_id] = { - 'state': state_data, - 'receive_time': globalClock.getRealTime(), - 'interpolated': False - } - - print(f"接收Actor {actor_id} 状态更新") - - def update_network_sync(self, task): - """ - 更新网络同步 - """ - current_time = globalClock.getRealTime() - - # 检查同步间隔 - if current_time - self.last_sync_time < self.network_settings['sync_interval']: - return task.cont - - self.last_sync_time = current_time - - if self.is_server: - # 服务器:发送本地Actor状态 - for actor_id in list(self.local_actor_states.keys()): - self.send_actor_state(actor_id) - - elif self.is_client: - # 客户端:应用接收到的状态 - self._apply_network_states() - - return task.cont - - def _apply_network_states(self): - """ - 应用网络状态 - """ - current_time = globalClock.getRealTime() - - for actor_id, state_info in self.network_actor_states.items(): - state_data = state_info['state'] - receive_time = state_info['receive_time'] - - # 查找对应的Actor - actor_np = self.world.render.find(f"**/{actor_id}") - if actor_np and actor_np.node(): - actor = actor_np.node() - if isinstance(actor, Actor): - # 时间补偿插值 - if self.network_settings['interpolation_mode'] == 'linear': - # 简单线性插值 - self._apply_actor_state(actor, state_data) - else: - # 直接应用状态 - self._apply_actor_state(actor, state_data) - - def start_sync_task(self): - """ - 启动同步任务 - """ - if self.sync_task is None: - self.sync_task = self.world.taskMgr.add( - self.update_network_sync, - "animationNetworkSyncTask", - sort=40 # 确保在网络更新之后执行 - ) - print("动画网络同步任务已启动") - - def stop_sync_task(self): - """ - 停止同步任务 - """ - if self.sync_task: - self.world.taskMgr.remove(self.sync_task) - self.sync_task = None - print("动画网络同步任务已停止") - - def get_network_stats(self): - """ - 获取网络同步统计信息 - :return: 统计信息 - """ - return { - 'is_server': self.is_server, - 'is_client': self.is_client, - 'connected': self.connected, - 'local_actors': len(self.local_actor_states), - 'network_actors': len(self.network_actor_states), - 'sync_enabled': self.network_settings['enable_sync'], - 'sync_interval': self.network_settings['sync_interval'] - } - - -class DistributedAnimationManager: - """ - 分布式动画管理器 - 管理网络环境中的动画同步 - """ - - def __init__(self, world): - self.world = world - self.network_sync = AnimationNetworkSync(world) - self.distributed_actors = {} # {actor_id: distributed_data} - self.master_slave_relations = {} # {slave_actor_id: master_actor_id} - - def enable_distributed_mode(self, mode='server'): - """ - 启用分布式模式 - :param mode: 模式 ('server' 或 'client') - """ - if mode == 'server': - self.network_sync.enable_server_mode() - else: - self.network_sync.enable_client_mode() - - self.network_sync.start_sync_task() - print(f"分布式动画管理器已启用 ({mode} 模式)") - - def disable_distributed_mode(self): - """ - 禁用分布式模式 - """ - self.network_sync.stop_sync_task() - self.network_sync.disable_network_sync() - print("分布式动画管理器已禁用") - - def create_distributed_actor(self, actor_id, model_path, anim_dict=None): - """ - 创建分布式Actor - :param actor_id: Actor ID - :param model_path: 模型路径 - :param anim_dict: 动画字典 - :return: Actor对象 - """ - from direct.actor.Actor import Actor - - # 创建Actor - if anim_dict: - actor = Actor(model_path, anim_dict) - else: - actor = Actor(model_path) - - # 添加到场景 - actor.reparentTo(self.world.render) - actor.setName(actor_id) - - # 注册网络同步 - self.network_sync.register_actor_for_sync(actor_id, actor) - - # 存储分布式数据 - self.distributed_actors[actor_id] = { - 'actor': actor, - 'model_path': model_path, - 'anim_dict': anim_dict, - 'is_distributed': True - } - - print(f"分布式Actor已创建: {actor_id}") - return actor - - def set_master_slave_relation(self, master_id, slave_id): - """ - 设置主从关系 - :param master_id: 主Actor ID - :param slave_id: 从Actor ID - """ - if master_id in self.distributed_actors and slave_id in self.distributed_actors: - self.master_slave_relations[slave_id] = master_id - print(f"设置主从关系: {master_id} -> {slave_id}") - - def sync_actor_animation(self, actor_id, anim_name, play=True, loop=False, play_rate=1.0): - """ - 同步Actor动画 - :param actor_id: Actor ID - :param anim_name: 动画名称 - :param play: 是否播放 - :param loop: 是否循环 - :param play_rate: 播放速度 - """ - if actor_id not in self.distributed_actors: - return - - actor_data = self.distributed_actors[actor_id] - actor = actor_data['actor'] - - if play: - actor.play(anim_name) - if loop: - actor.loop(anim_name, restart=False) - actor.setPlayRate(play_rate, anim_name) - else: - actor.stop(anim_name) - - print(f"同步Actor {actor_id} 动画: {anim_name}") - - def get_distributed_stats(self): - """ - 获取分布式统计信息 - :return: 统计信息 - """ - network_stats = self.network_sync.get_network_stats() - return { - 'network_stats': network_stats, - 'distributed_actors': len(self.distributed_actors), - 'master_slave_relations': len(self.master_slave_relations) - } - - -class AnimationStateSerializer: - """ - 动画状态序列化器 - 支持动画状态的序列化和反序列化 - """ - - def __init__(self): - self.serialization_settings = { - 'compress_data': True, - 'include_bone_transforms': True, - 'include_animation_state': True, - 'include_custom_properties': True - } - - def serialize_actor_state(self, actor, include_animations=True): - """ - 序列化Actor状态 - :param actor: Actor对象 - :param include_animations: 是否包含动画状态 - :return: 序列化数据 - """ - state_data = { - 'name': actor.getName() if hasattr(actor, 'getName') else 'Unknown', - 'position': actor.getPos().toTuple(), - 'rotation': actor.getHpr().toTuple(), - 'scale': actor.getScale().toTuple(), - 'timestamp': globalClock.getRealTime() - } - - if include_animations: - state_data['animations'] = self._serialize_animation_state(actor) - - if self.serialization_settings['include_bone_transforms']: - state_data['bone_transforms'] = self._serialize_bone_transforms(actor) - - # 计算数据校验和 - state_data['checksum'] = self._calculate_checksum(state_data) - - return state_data - - def _serialize_animation_state(self, actor): - """ - 序列化动画状态 - :param actor: Actor对象 - :return: 动画状态数据 - """ - anim_state = { - 'current_animations': [], - 'animation_rates': {}, - 'animation_frames': {} - } - - for anim_name in actor.getAnimNames(): - control = actor.getAnimControl(anim_name) - if control and control.isPlaying(): - anim_state['current_animations'].append(anim_name) - anim_state['animation_rates'][anim_name] = actor.getPlayRate(anim_name) - anim_state['animation_frames'][anim_name] = actor.getCurrentFrame(anim_name) - - return anim_state - - def _serialize_bone_transforms(self, actor): - """ - 序列化骨骼变换 - :param actor: Actor对象 - :return: 骨骼变换数据 - """ - bone_transforms = {} - - # 获取骨骼信息 - bundle_dict = actor.getPartBundleDict() - for bundle_name, bundle in bundle_dict.items(): - bone_transforms[bundle_name] = self._extract_bundle_transforms(bundle) - - return bone_transforms - - def _extract_bundle_transforms(self, bundle): - """ - 提取绑定变换 - :param bundle: PartBundle对象 - :return: 变换数据 - """ - transforms = {} - - # 遍历子节点 - for i in range(bundle.getNumChildren()): - child = bundle.getChild(i) - if hasattr(child, 'getName'): - bone_name = child.getName() - # 注意:这里简化处理,实际项目中需要更复杂的骨骼数据提取 - transforms[bone_name] = { - 'transform': 'dummy_transform_data' - } - - return transforms - - def _calculate_checksum(self, data): - """ - 计算数据校验和 - :param data: 数据 - :return: 校验和 - """ - # 简化实现,实际项目中应使用更安全的哈希算法 - data_str = json.dumps(data, sort_keys=True) - return hashlib.md5(data_str.encode()).hexdigest() - - def deserialize_actor_state(self, actor, state_data): - """ - 反序列化Actor状态 - :param actor: Actor对象 - :param state_data: 状态数据 - """ - # 验证校验和 - if 'checksum' in state_data: - expected_checksum = state_data['checksum'] - data_to_check = state_data.copy() - del data_to_check['checksum'] - actual_checksum = self._calculate_checksum(data_to_check) - - if expected_checksum != actual_checksum: - print("警告: 数据校验和不匹配") - - # 应用变换 - if 'position' in state_data: - actor.setPos(*state_data['position']) - if 'rotation' in state_data: - actor.setHpr(*state_data['rotation']) - if 'scale' in state_data: - actor.setScale(*state_data['scale']) - - # 应用动画状态 - if 'animations' in state_data: - self._deserialize_animation_state(actor, state_data['animations']) - - def _deserialize_animation_state(self, actor, anim_state): - """ - 反序列化动画状态 - :param actor: Actor对象 - :param anim_state: 动画状态数据 - """ - # 停止所有当前动画 - actor.stop() - - # 播放指定动画 - for anim_name in anim_state.get('current_animations', []): - if anim_name in actor.getAnimNames(): - frame = anim_state.get('animation_frames', {}).get(anim_name, 0) - rate = anim_state.get('animation_rates', {}).get(anim_name, 1.0) - - actor.pose(anim_name, frame) - actor.setPlayRate(rate, anim_name) - - def save_state_to_file(self, actor, file_path, include_animations=True): - """ - 保存状态到文件 - :param actor: Actor对象 - :param file_path: 文件路径 - :param include_animations: 是否包含动画状态 - """ - state_data = self.serialize_actor_state(actor, include_animations) - - try: - with open(file_path, 'w') as f: - json.dump(state_data, f, indent=2) - print(f"Actor状态已保存到: {file_path}") - except Exception as e: - print(f"保存状态失败: {e}") - - def load_state_from_file(self, actor, file_path): - """ - 从文件加载状态 - :param actor: Actor对象 - :param file_path: 文件路径 - """ - try: - with open(file_path, 'r') as f: - state_data = json.load(f) - self.deserialize_actor_state(actor, state_data) - print(f"Actor状态已从 {file_path} 加载") - except Exception as e: - print(f"加载状态失败: {e}") - - -# 使用示例 -def example_network_sync_usage(world): - """ - 网络同步使用示例 - """ - print("=== 动画网络同步使用示例 ===") - - # 创建网络同步系统 - network_sync = AnimationNetworkSync(world) - - # 模拟服务器模式 - network_sync.enable_server_mode() - - # 获取网络统计 - stats = network_sync.get_network_stats() - print(f"网络统计: {stats}") - - print("网络同步示例完成") - - -def example_distributed_usage(world): - """ - 分布式动画使用示例 - """ - print("=== 分布式动画使用示例 ===") - - # 创建分布式管理器 - dist_manager = DistributedAnimationManager(world) - - # 启用服务器模式 - dist_manager.enable_distributed_mode('server') - - # 获取分布式统计 - stats = dist_manager.get_distributed_stats() - print(f"分布式统计: {stats}") - - print("分布式动画示例完成") - - -def example_serialization_usage(actor): - """ - 状态序列化使用示例 - """ - print("=== 动画状态序列化使用示例 ===") - - # 创建序列化器 - serializer = AnimationStateSerializer() - - # 序列化Actor状态 - state_data = serializer.serialize_actor_state(actor) - print(f"序列化状态大小: {len(json.dumps(state_data))} 字符") - - # 保存到文件 - serializer.save_state_to_file(actor, "actor_state.json") - - print("状态序列化示例完成") - - -if __name__ == "__main__": +""" +骨骼动画系统插件 - 网络同步模块 +提供动画状态网络同步、多人协作等功能 +""" + +from panda3d.core import * +from direct.distributed.ClockDelta import * +from direct.showbase.DirectObject import DirectObject +import json +import hashlib + + +class AnimationNetworkSync(DirectObject): + """ + 动画网络同步系统 + 支持多人环境中动画状态的同步 + """ + + def __init__(self, world): + self.world = world + self.network_settings = { + 'enable_sync': True, + 'sync_interval': 0.1, # 同步间隔(秒) + 'interpolation_mode': 'linear', # 插值模式 + 'extrapolation_limit': 0.5, # 外推限制(秒) + 'compression_enabled': True, + 'delta_compression': True + } + + # 网络状态 + self.is_server = False + self.is_client = False + self.connected = False + self.network_actor_states = {} # {actor_id: state_data} + self.local_actor_states = {} # {actor_id: state_data} + self.actor_sync_callbacks = {} # {actor_id: callback_func} + + # 同步定时器 + self.sync_task = None + self.last_sync_time = 0 + + def enable_server_mode(self): + """ + 启用服务器模式 + """ + self.is_server = True + self.is_client = False + self.connected = True + print("动画网络同步系统 - 服务器模式已启用") + + def enable_client_mode(self): + """ + 启用客户端模式 + """ + self.is_server = False + self.is_client = True + self.connected = True + print("动画网络同步系统 - 客户端模式已启用") + + def disable_network_sync(self): + """ + 禁用网络同步 + """ + self.is_server = False + self.is_client = False + self.connected = False + print("动画网络同步系统已禁用") + + def register_actor_for_sync(self, actor_id, actor, sync_callback=None): + """ + 注册Actor以进行网络同步 + :param actor_id: Actor ID + :param actor: Actor对象 + :param sync_callback: 同步回调函数 + """ + self.local_actor_states[actor_id] = { + 'actor': actor, + 'last_state': self._capture_actor_state(actor), + 'last_update': globalClock.getRealTime(), + 'sync_callback': sync_callback + } + + if sync_callback: + self.actor_sync_callbacks[actor_id] = sync_callback + + print(f"Actor {actor_id} 已注册网络同步") + + def unregister_actor_from_sync(self, actor_id): + """ + 取消Actor的网络同步注册 + :param actor_id: Actor ID + """ + if actor_id in self.local_actor_states: + del self.local_actor_states[actor_id] + if actor_id in self.actor_sync_callbacks: + del self.actor_sync_callbacks[actor_id] + if actor_id in self.network_actor_states: + del self.network_actor_states[actor_id] + + print(f"Actor {actor_id} 已取消网络同步注册") + + def _capture_actor_state(self, actor): + """ + 捕获Actor当前状态 + :param actor: Actor对象 + :return: 状态数据 + """ + state = { + 'position': actor.getPos().toTuple(), + 'rotation': actor.getHpr().toTuple(), + 'scale': actor.getScale().toTuple(), + 'active_animations': {}, + 'timestamp': globalClock.getRealTime() + } + + # 捕获活动动画状态 + for anim_name in actor.getAnimNames(): + control = actor.getAnimControl(anim_name) + if control and control.isPlaying(): + state['active_animations'][anim_name] = { + 'frame': actor.getCurrentFrame(anim_name), + 'play_rate': actor.getPlayRate(anim_name), + 'is_looping': control.getLoop() + } + + return state + + def _apply_actor_state(self, actor, state_data): + """ + 应用Actor状态 + :param actor: Actor对象 + :param state_data: 状态数据 + """ + # 应用变换 + if 'position' in state_data: + actor.setPos(*state_data['position']) + if 'rotation' in state_data: + actor.setHpr(*state_data['rotation']) + if 'scale' in state_data: + actor.setScale(*state_data['scale']) + + # 应用动画状态 + if 'active_animations' in state_data: + for anim_name, anim_state in state_data['active_animations'].items(): + if anim_name in actor.getAnimNames(): + # 停止当前动画 + actor.stop(anim_name) + + # 设置到指定帧 + if 'frame' in anim_state: + actor.pose(anim_name, anim_state['frame']) + + # 设置播放速度 + if 'play_rate' in anim_state: + actor.setPlayRate(anim_state['play_rate'], anim_name) + + # 调用自定义同步回调 + actor_id = actor.getName() # 简化获取ID的方法 + if actor_id in self.actor_sync_callbacks: + self.actor_sync_callbacks[actor_id](actor, state_data) + + def send_actor_state(self, actor_id): + """ + 发送Actor状态(服务器模式) + :param actor_id: Actor ID + """ + if not self.is_server or not self.connected: + return + + if actor_id not in self.local_actor_states: + return + + actor_state = self.local_actor_states[actor_id] + actor = actor_state['actor'] + current_state = self._capture_actor_state(actor) + + # 检查状态是否发生变化 + if self._has_state_changed(actor_state['last_state'], current_state): + # 发送状态更新 + self._broadcast_actor_state(actor_id, current_state) + + # 更新最后状态 + actor_state['last_state'] = current_state + actor_state['last_update'] = globalClock.getRealTime() + + def _has_state_changed(self, old_state, new_state, threshold=0.001): + """ + 检查状态是否发生变化 + :param old_state: 旧状态 + :param new_state: 新状态 + :param threshold: 变化阈值 + :return: 是否发生变化 + """ + # 检查位置变化 + if 'position' in old_state and 'position' in new_state: + old_pos = Vec3(*old_state['position']) + new_pos = Vec3(*new_state['position']) + if (old_pos - new_pos).length() > threshold: + return True + + # 检查旋转变化 + if 'rotation' in old_state and 'rotation' in new_state: + old_hpr = Vec3(*old_state['rotation']) + new_hpr = Vec3(*new_state['rotation']) + if (old_hpr - new_hpr).length() > threshold: + return True + + # 检查动画状态变化 + if 'active_animations' in old_state and 'active_animations' in new_state: + if old_state['active_animations'] != new_state['active_animations']: + return True + + return False + + def _broadcast_actor_state(self, actor_id, state_data): + """ + 广播Actor状态 + :param actor_id: Actor ID + :param state_data: 状态数据 + """ + # 在实际实现中,这里会通过网络发送数据 + # 为简化,我们直接更新网络状态 + self.network_actor_states[actor_id] = state_data + print(f"广播Actor {actor_id} 状态更新") + + def receive_actor_state(self, actor_id, state_data): + """ + 接收Actor状态(客户端模式) + :param actor_id: Actor ID + :param state_data: 状态数据 + """ + if not self.is_client or not self.connected: + return + + # 存储接收到的状态 + self.network_actor_states[actor_id] = { + 'state': state_data, + 'receive_time': globalClock.getRealTime(), + 'interpolated': False + } + + print(f"接收Actor {actor_id} 状态更新") + + def update_network_sync(self, task): + """ + 更新网络同步 + """ + current_time = globalClock.getRealTime() + + # 检查同步间隔 + if current_time - self.last_sync_time < self.network_settings['sync_interval']: + return task.cont + + self.last_sync_time = current_time + + if self.is_server: + # 服务器:发送本地Actor状态 + for actor_id in list(self.local_actor_states.keys()): + self.send_actor_state(actor_id) + + elif self.is_client: + # 客户端:应用接收到的状态 + self._apply_network_states() + + return task.cont + + def _apply_network_states(self): + """ + 应用网络状态 + """ + current_time = globalClock.getRealTime() + + for actor_id, state_info in self.network_actor_states.items(): + state_data = state_info['state'] + receive_time = state_info['receive_time'] + + # 查找对应的Actor + actor_np = self.world.render.find(f"**/{actor_id}") + if actor_np and actor_np.node(): + actor = actor_np.node() + if isinstance(actor, Actor): + # 时间补偿插值 + if self.network_settings['interpolation_mode'] == 'linear': + # 简单线性插值 + self._apply_actor_state(actor, state_data) + else: + # 直接应用状态 + self._apply_actor_state(actor, state_data) + + def start_sync_task(self): + """ + 启动同步任务 + """ + if self.sync_task is None: + self.sync_task = self.world.taskMgr.add( + self.update_network_sync, + "animationNetworkSyncTask", + sort=40 # 确保在网络更新之后执行 + ) + print("动画网络同步任务已启动") + + def stop_sync_task(self): + """ + 停止同步任务 + """ + if self.sync_task: + self.world.taskMgr.remove(self.sync_task) + self.sync_task = None + print("动画网络同步任务已停止") + + def get_network_stats(self): + """ + 获取网络同步统计信息 + :return: 统计信息 + """ + return { + 'is_server': self.is_server, + 'is_client': self.is_client, + 'connected': self.connected, + 'local_actors': len(self.local_actor_states), + 'network_actors': len(self.network_actor_states), + 'sync_enabled': self.network_settings['enable_sync'], + 'sync_interval': self.network_settings['sync_interval'] + } + + +class DistributedAnimationManager: + """ + 分布式动画管理器 + 管理网络环境中的动画同步 + """ + + def __init__(self, world): + self.world = world + self.network_sync = AnimationNetworkSync(world) + self.distributed_actors = {} # {actor_id: distributed_data} + self.master_slave_relations = {} # {slave_actor_id: master_actor_id} + + def enable_distributed_mode(self, mode='server'): + """ + 启用分布式模式 + :param mode: 模式 ('server' 或 'client') + """ + if mode == 'server': + self.network_sync.enable_server_mode() + else: + self.network_sync.enable_client_mode() + + self.network_sync.start_sync_task() + print(f"分布式动画管理器已启用 ({mode} 模式)") + + def disable_distributed_mode(self): + """ + 禁用分布式模式 + """ + self.network_sync.stop_sync_task() + self.network_sync.disable_network_sync() + print("分布式动画管理器已禁用") + + def create_distributed_actor(self, actor_id, model_path, anim_dict=None): + """ + 创建分布式Actor + :param actor_id: Actor ID + :param model_path: 模型路径 + :param anim_dict: 动画字典 + :return: Actor对象 + """ + from direct.actor.Actor import Actor + + # 创建Actor + if anim_dict: + actor = Actor(model_path, anim_dict) + else: + actor = Actor(model_path) + + # 添加到场景 + actor.reparentTo(self.world.render) + actor.setName(actor_id) + + # 注册网络同步 + self.network_sync.register_actor_for_sync(actor_id, actor) + + # 存储分布式数据 + self.distributed_actors[actor_id] = { + 'actor': actor, + 'model_path': model_path, + 'anim_dict': anim_dict, + 'is_distributed': True + } + + print(f"分布式Actor已创建: {actor_id}") + return actor + + def set_master_slave_relation(self, master_id, slave_id): + """ + 设置主从关系 + :param master_id: 主Actor ID + :param slave_id: 从Actor ID + """ + if master_id in self.distributed_actors and slave_id in self.distributed_actors: + self.master_slave_relations[slave_id] = master_id + print(f"设置主从关系: {master_id} -> {slave_id}") + + def sync_actor_animation(self, actor_id, anim_name, play=True, loop=False, play_rate=1.0): + """ + 同步Actor动画 + :param actor_id: Actor ID + :param anim_name: 动画名称 + :param play: 是否播放 + :param loop: 是否循环 + :param play_rate: 播放速度 + """ + if actor_id not in self.distributed_actors: + return + + actor_data = self.distributed_actors[actor_id] + actor = actor_data['actor'] + + if play: + actor.play(anim_name) + if loop: + actor.loop(anim_name, restart=False) + actor.setPlayRate(play_rate, anim_name) + else: + actor.stop(anim_name) + + print(f"同步Actor {actor_id} 动画: {anim_name}") + + def get_distributed_stats(self): + """ + 获取分布式统计信息 + :return: 统计信息 + """ + network_stats = self.network_sync.get_network_stats() + return { + 'network_stats': network_stats, + 'distributed_actors': len(self.distributed_actors), + 'master_slave_relations': len(self.master_slave_relations) + } + + +class AnimationStateSerializer: + """ + 动画状态序列化器 + 支持动画状态的序列化和反序列化 + """ + + def __init__(self): + self.serialization_settings = { + 'compress_data': True, + 'include_bone_transforms': True, + 'include_animation_state': True, + 'include_custom_properties': True + } + + def serialize_actor_state(self, actor, include_animations=True): + """ + 序列化Actor状态 + :param actor: Actor对象 + :param include_animations: 是否包含动画状态 + :return: 序列化数据 + """ + state_data = { + 'name': actor.getName() if hasattr(actor, 'getName') else 'Unknown', + 'position': actor.getPos().toTuple(), + 'rotation': actor.getHpr().toTuple(), + 'scale': actor.getScale().toTuple(), + 'timestamp': globalClock.getRealTime() + } + + if include_animations: + state_data['animations'] = self._serialize_animation_state(actor) + + if self.serialization_settings['include_bone_transforms']: + state_data['bone_transforms'] = self._serialize_bone_transforms(actor) + + # 计算数据校验和 + state_data['checksum'] = self._calculate_checksum(state_data) + + return state_data + + def _serialize_animation_state(self, actor): + """ + 序列化动画状态 + :param actor: Actor对象 + :return: 动画状态数据 + """ + anim_state = { + 'current_animations': [], + 'animation_rates': {}, + 'animation_frames': {} + } + + for anim_name in actor.getAnimNames(): + control = actor.getAnimControl(anim_name) + if control and control.isPlaying(): + anim_state['current_animations'].append(anim_name) + anim_state['animation_rates'][anim_name] = actor.getPlayRate(anim_name) + anim_state['animation_frames'][anim_name] = actor.getCurrentFrame(anim_name) + + return anim_state + + def _serialize_bone_transforms(self, actor): + """ + 序列化骨骼变换 + :param actor: Actor对象 + :return: 骨骼变换数据 + """ + bone_transforms = {} + + # 获取骨骼信息 + bundle_dict = actor.getPartBundleDict() + for bundle_name, bundle in bundle_dict.items(): + bone_transforms[bundle_name] = self._extract_bundle_transforms(bundle) + + return bone_transforms + + def _extract_bundle_transforms(self, bundle): + """ + 提取绑定变换 + :param bundle: PartBundle对象 + :return: 变换数据 + """ + transforms = {} + + # 遍历子节点 + for i in range(bundle.getNumChildren()): + child = bundle.getChild(i) + if hasattr(child, 'getName'): + bone_name = child.getName() + # 注意:这里简化处理,实际项目中需要更复杂的骨骼数据提取 + transforms[bone_name] = { + 'transform': 'dummy_transform_data' + } + + return transforms + + def _calculate_checksum(self, data): + """ + 计算数据校验和 + :param data: 数据 + :return: 校验和 + """ + # 简化实现,实际项目中应使用更安全的哈希算法 + data_str = json.dumps(data, sort_keys=True) + return hashlib.md5(data_str.encode()).hexdigest() + + def deserialize_actor_state(self, actor, state_data): + """ + 反序列化Actor状态 + :param actor: Actor对象 + :param state_data: 状态数据 + """ + # 验证校验和 + if 'checksum' in state_data: + expected_checksum = state_data['checksum'] + data_to_check = state_data.copy() + del data_to_check['checksum'] + actual_checksum = self._calculate_checksum(data_to_check) + + if expected_checksum != actual_checksum: + print("警告: 数据校验和不匹配") + + # 应用变换 + if 'position' in state_data: + actor.setPos(*state_data['position']) + if 'rotation' in state_data: + actor.setHpr(*state_data['rotation']) + if 'scale' in state_data: + actor.setScale(*state_data['scale']) + + # 应用动画状态 + if 'animations' in state_data: + self._deserialize_animation_state(actor, state_data['animations']) + + def _deserialize_animation_state(self, actor, anim_state): + """ + 反序列化动画状态 + :param actor: Actor对象 + :param anim_state: 动画状态数据 + """ + # 停止所有当前动画 + actor.stop() + + # 播放指定动画 + for anim_name in anim_state.get('current_animations', []): + if anim_name in actor.getAnimNames(): + frame = anim_state.get('animation_frames', {}).get(anim_name, 0) + rate = anim_state.get('animation_rates', {}).get(anim_name, 1.0) + + actor.pose(anim_name, frame) + actor.setPlayRate(rate, anim_name) + + def save_state_to_file(self, actor, file_path, include_animations=True): + """ + 保存状态到文件 + :param actor: Actor对象 + :param file_path: 文件路径 + :param include_animations: 是否包含动画状态 + """ + state_data = self.serialize_actor_state(actor, include_animations) + + try: + with open(file_path, 'w') as f: + json.dump(state_data, f, indent=2) + print(f"Actor状态已保存到: {file_path}") + except Exception as e: + print(f"保存状态失败: {e}") + + def load_state_from_file(self, actor, file_path): + """ + 从文件加载状态 + :param actor: Actor对象 + :param file_path: 文件路径 + """ + try: + with open(file_path, 'r') as f: + state_data = json.load(f) + self.deserialize_actor_state(actor, state_data) + print(f"Actor状态已从 {file_path} 加载") + except Exception as e: + print(f"加载状态失败: {e}") + + +# 使用示例 +def example_network_sync_usage(world): + """ + 网络同步使用示例 + """ + print("=== 动画网络同步使用示例 ===") + + # 创建网络同步系统 + network_sync = AnimationNetworkSync(world) + + # 模拟服务器模式 + network_sync.enable_server_mode() + + # 获取网络统计 + stats = network_sync.get_network_stats() + print(f"网络统计: {stats}") + + print("网络同步示例完成") + + +def example_distributed_usage(world): + """ + 分布式动画使用示例 + """ + print("=== 分布式动画使用示例 ===") + + # 创建分布式管理器 + dist_manager = DistributedAnimationManager(world) + + # 启用服务器模式 + dist_manager.enable_distributed_mode('server') + + # 获取分布式统计 + stats = dist_manager.get_distributed_stats() + print(f"分布式统计: {stats}") + + print("分布式动画示例完成") + + +def example_serialization_usage(actor): + """ + 状态序列化使用示例 + """ + print("=== 动画状态序列化使用示例 ===") + + # 创建序列化器 + serializer = AnimationStateSerializer() + + # 序列化Actor状态 + state_data = serializer.serialize_actor_state(actor) + print(f"序列化状态大小: {len(json.dumps(state_data))} 字符") + + # 保存到文件 + serializer.save_state_to_file(actor, "actor_state.json") + + print("状态序列化示例完成") + + +if __name__ == "__main__": print("动画网络同步模块加载完成") \ No newline at end of file diff --git a/plugins/user/skeletal_animation_system/plugin.py b/plugins/user/skeletal_animation_system/plugin.py index aa515532..36a8669e 100644 --- a/plugins/user/skeletal_animation_system/plugin.py +++ b/plugins/user/skeletal_animation_system/plugin.py @@ -1,1224 +1,1224 @@ -""" -骨骼动画系统插件 -提供完整的骨骼动画支持,包括加载、播放、控制、混合等高级功能 -""" - -from plugins.plugin_manager import BasePlugin -from direct.actor.Actor import Actor -from direct.interval.IntervalGlobal import * -from panda3d.core import Filename, ConfigVariableBool -from panda3d.core import NodePath, Vec3, Point3 -import os -import json -import math - -# 导入高级功能模块 -from .advanced_features import AdvancedAnimationManager -from .animation_optimization import AnimationCompression, AnimationCaching, AnimationLOD -from .network_sync import DistributedAnimationManager, AnimationStateSerializer - - -class SkeletalAnimationManager: - """ - 骨骼动画管理器类 - 负责管理场景中所有的骨骼动画Actor - """ - - def __init__(self, world): - self.world = world - self.actors = {} # 存储所有Actor {actor_id: actor_object} - self.actor_configs = {} # 存储Actor配置 {actor_id: config_dict} - self.active_animations = {} # 存储活动动画 {actor_id: {anim_name: control_object}} - self.animation_intervals = {} # 存储动画间隔 {actor_id: {interval_name: interval_object}} - self.blend_intervals = {} # 存储混合动画 {actor_id: {blend_name: blend_object}} - - # 高级功能管理器 - self.advanced_managers = {} # {actor_id: AdvancedAnimationManager} - self.compression_system = AnimationCompression() - self.caching_system = AnimationCaching() - self.lod_system = AnimationLOD(world) - self.distributed_manager = DistributedAnimationManager(world) - self.serializer = AnimationStateSerializer() - - def load_actor(self, model_path, anim_dict=None, actor_id=None, position=None, scale=None): - """ - 加载Actor模型 - :param model_path: 模型文件路径 - :param anim_dict: 动画字典,格式 {'anim_name': 'anim_path'} - :param actor_id: Actor标识符,如果为None则自动生成 - :param position: 位置坐标 (x, y, z) - :param scale: 缩放比例 (x, y, z) 或单个数值 - :return: Actor对象和actor_id - """ - try: - if not os.path.exists(model_path): - print(f"错误: 模型文件不存在: {model_path}") - return None, None - - if actor_id is None: - actor_id = f"actor_{len(self.actors)}" - - # 检查缓存 - cached_actor = self.caching_system.get_cached_animation(actor_id, "model") - if cached_actor: - actor = cached_actor - else: - # 创建Actor - if anim_dict: - # 验证动画文件存在性 - validated_anims = {} - for anim_name, anim_path in anim_dict.items(): - if os.path.exists(anim_path): - validated_anims[anim_name] = anim_path - else: - print(f"警告: 动画文件不存在: {anim_path}") - - actor = Actor(model_path, validated_anims) - else: - actor = Actor(model_path) - - # 缓存Actor - self.caching_system.cache_animation(actor_id, "model", actor) - - # 设置位置和缩放 - if position: - actor.setPos(*position) - if scale: - if isinstance(scale, (int, float)): - actor.setScale(scale) - else: - actor.setScale(*scale) - - # 将Actor添加到场景中 - actor.reparentTo(self.world.render) - actor.setName(actor_id) - - # 存储Actor和配置 - self.actors[actor_id] = actor - self.actor_configs[actor_id] = { - 'model_path': model_path, - 'anim_dict': anim_dict, - 'position': position, - 'scale': scale - } - - # 创建高级动画管理器 - self.advanced_managers[actor_id] = AdvancedAnimationManager(actor) - - print(f"成功加载Actor: {actor_id}") - print(f" 模型: {model_path}") - print(f" 位置: {position}") - print(f" 缩放: {scale}") - print(f" 可用动画: {actor.getAnimNames()}") - - return actor, actor_id - - except Exception as e: - print(f"加载Actor失败: {e}") - import traceback - traceback.print_exc() - return None, None - - def unload_actor(self, actor_id): - """ - 卸载Actor - :param actor_id: 要卸载的Actor标识符 - """ - if actor_id in self.actors: - # 停止所有活动动画 - self.stop_all_animations(actor_id) - - # 清理混合动画 - if actor_id in self.blend_intervals: - for blend in self.blend_intervals[actor_id].values(): - if blend: - blend.finish() - del self.blend_intervals[actor_id] - - # 清理动画间隔 - if actor_id in self.animation_intervals: - for interval in self.animation_intervals[actor_id].values(): - if interval: - interval.finish() - del self.animation_intervals[actor_id] - - # 清理高级动画管理器 - if actor_id in self.advanced_managers: - del self.advanced_managers[actor_id] - - # 清理Actor - actor = self.actors[actor_id] - actor.cleanup() - del self.actors[actor_id] - - # 清理配置 - if actor_id in self.actor_configs: - del self.actor_configs[actor_id] - - # 清理活动动画记录 - if actor_id in self.active_animations: - del self.active_animations[actor_id] - - # 使缓存失效 - self.caching_system.invalidate_cache(actor_id) - - print(f"已卸载Actor: {actor_id}") - - def get_actor(self, actor_id): - """ - 获取Actor对象 - :param actor_id: Actor标识符 - :return: Actor对象或None - """ - return self.actors.get(actor_id) - - def get_actor_animations(self, actor_id): - """ - 获取Actor的所有动画名称 - :param actor_id: Actor标识符 - :return: 动画名称列表 - """ - if actor_id in self.actors: - return self.actors[actor_id].getAnimNames() - return [] - - def play_animation(self, actor_id, anim_name, loop=False, play_rate=1.0, start_frame=None, end_frame=None): - """ - 播放动画 - :param actor_id: Actor标识符 - :param anim_name: 动画名称 - :param loop: 是否循环播放 - :param play_rate: 播放速度 - :param start_frame: 起始帧 - :param end_frame: 结束帧 - """ - if actor_id not in self.actors: - print(f"错误: Actor {actor_id} 不存在") - return False - - actor = self.actors[actor_id] - - # 检查动画是否存在 - if anim_name not in actor.getAnimNames(): - print(f"错误: 动画 {anim_name} 不存在于Actor {actor_id} 中") - print(f" 可用动画: {actor.getAnimNames()}") - return False - - try: - # 设置播放速度 - actor.setPlayRate(play_rate, anim_name) - - # 播放动画 - if loop: - if start_frame is not None and end_frame is not None: - actor.loop(anim_name, restart=False, fromFrame=start_frame, toFrame=end_frame) - else: - actor.loop(anim_name, restart=False) - else: - if start_frame is not None and end_frame is not None: - actor.play(anim_name, fromFrame=start_frame, toFrame=end_frame) - else: - actor.play(anim_name) - - # 记录活动动画 - if actor_id not in self.active_animations: - self.active_animations[actor_id] = {} - self.active_animations[actor_id][anim_name] = actor.getAnimControl(anim_name) - - print(f"播放动画: {anim_name} (Actor: {actor_id}, 循环: {loop}, 速度: {play_rate}x)") - return True - - except Exception as e: - print(f"播放动画失败: {e}") - import traceback - traceback.print_exc() - return False - - def stop_animation(self, actor_id, anim_name=None): - """ - 停止动画 - :param actor_id: Actor标识符 - :param anim_name: 动画名称,如果为None则停止所有动画 - """ - if actor_id not in self.actors: - print(f"错误: Actor {actor_id} 不存在") - return - - actor = self.actors[actor_id] - - try: - if anim_name: - # 停止指定动画 - if anim_name in actor.getAnimNames(): - actor.stop(anim_name) - # 从活动动画中移除 - if actor_id in self.active_animations and anim_name in self.active_animations[actor_id]: - del self.active_animations[actor_id][anim_name] - print(f"停止动画: {anim_name} (Actor: {actor_id})") - else: - print(f"警告: 动画 {anim_name} 不存在于Actor {actor_id} 中") - else: - # 停止所有动画 - actor.stop() - # 清空活动动画记录 - if actor_id in self.active_animations: - self.active_animations[actor_id].clear() - print(f"停止所有动画 (Actor: {actor_id})") - - except Exception as e: - print(f"停止动画失败: {e}") - import traceback - traceback.print_exc() - - def stop_all_animations(self, actor_id): - """ - 停止指定Actor的所有动画 - :param actor_id: Actor标识符 - """ - self.stop_animation(actor_id) - - def set_animation_speed(self, actor_id, anim_name, speed): - """ - 设置动画播放速度 - :param actor_id: Actor标识符 - :param anim_name: 动画名称 - :param speed: 播放速度 - """ - if actor_id not in self.actors: - print(f"错误: Actor {actor_id} 不存在") - return - - actor = self.actors[actor_id] - - try: - if anim_name in actor.getAnimNames(): - actor.setPlayRate(speed, anim_name) - print(f"设置动画速度: {anim_name} 为 {speed}x (Actor: {actor_id})") - else: - print(f"警告: 动画 {anim_name} 不存在于Actor {actor_id} 中") - - except Exception as e: - print(f"设置动画速度失败: {e}") - import traceback - traceback.print_exc() - - def pause_animation(self, actor_id, anim_name=None): - """ - 暂停动画 - :param actor_id: Actor标识符 - :param anim_name: 动画名称,如果为None则暂停所有动画 - """ - if actor_id not in self.actors: - print(f"错误: Actor {actor_id} 不存在") - return - - actor = self.actors[actor_id] - - try: - if anim_name: - if anim_name in actor.getAnimNames(): - control = actor.getAnimControl(anim_name) - if control and control.isPlaying(): - control.setPlayRate(0) # 通过设置播放速度为0来模拟暂停 - print(f"暂停动画: {anim_name} (Actor: {actor_id})") - else: - print(f"警告: 动画 {anim_name} 不存在于Actor {actor_id} 中") - else: - # 暂停所有活动动画 - if actor_id in self.active_animations: - for anim_name, control in self.active_animations[actor_id].items(): - if control and control.isPlaying(): - control.setPlayRate(0) - print(f"暂停所有动画 (Actor: {actor_id})") - - except Exception as e: - print(f"暂停动画失败: {e}") - import traceback - traceback.print_exc() - - def resume_animation(self, actor_id, anim_name=None): - """ - 恢复动画播放 - :param actor_id: Actor标识符 - :param anim_name: 动画名称,如果为None则恢复所有动画 - """ - if actor_id not in self.actors: - print(f"错误: Actor {actor_id} 不存在") - return - - actor = self.actors[actor_id] - - try: - if anim_name: - if anim_name in actor.getAnimNames(): - control = actor.getAnimControl(anim_name) - if control and control.getPlayRate() == 0: - control.setPlayRate(1.0) # 恢复正常播放速度 - print(f"恢复动画: {anim_name} (Actor: {actor_id})") - else: - print(f"警告: 动画 {anim_name} 不存在于Actor {actor_id} 中") - else: - # 恢复所有暂停的动画 - if actor_id in self.active_animations: - for anim_name, control in self.active_animations[actor_id].items(): - if control and control.getPlayRate() == 0: - control.setPlayRate(1.0) - print(f"恢复所有动画 (Actor: {actor_id})") - - except Exception as e: - print(f"恢复动画失败: {e}") - import traceback - traceback.print_exc() - - def get_animation_info(self, actor_id, anim_name): - """ - 获取动画信息 - :param actor_id: Actor标识符 - :param anim_name: 动画名称 - :return: 动画信息字典 - """ - if actor_id not in self.actors: - print(f"错误: Actor {actor_id} 不存在") - return None - - actor = self.actors[actor_id] - - if anim_name not in actor.getAnimNames(): - print(f"错误: 动画 {anim_name} 不存在于Actor {actor_id} 中") - return None - - try: - control = actor.getAnimControl(anim_name) - info = { - 'name': anim_name, - 'frame_rate': actor.getFrameRate(anim_name), - 'num_frames': actor.getNumFrames(anim_name), - 'play_rate': actor.getPlayRate(anim_name), - 'is_playing': control.isPlaying() if control else False, - 'is_paused': control.getPlayRate() == 0 if control else False, - 'current_frame': actor.getCurrentFrame(anim_name) if control else 0 - } - return info - - except Exception as e: - print(f"获取动画信息失败: {e}") - import traceback - traceback.print_exc() - return None - - def blend_animations(self, actor_id, from_anim, to_anim, blend_time=1.0, loop=True): - """ - 混合两个动画 - :param actor_id: Actor标识符 - :param from_anim: 起始动画名称 - :param to_anim: 目标动画名称 - :param blend_time: 混合时间 - :param loop: 是否循环混合 - """ - if actor_id not in self.actors: - print(f"错误: Actor {actor_id} 不存在") - return None - - actor = self.actors[actor_id] - - # 检查动画是否存在 - if from_anim not in actor.getAnimNames(): - print(f"错误: 起始动画 {from_anim} 不存在于Actor {actor_id} 中") - return None - - if to_anim not in actor.getAnimNames(): - print(f"错误: 目标动画 {to_anim} 不存在于Actor {actor_id} 中") - return None - - try: - # 创建动画混合 - blend_name = f"{from_anim}_to_{to_anim}" - blend = actor.actorInterval(from_anim, endTime=actor.getDuration(from_anim)) \ - + actor.actorInterval(to_anim, loop=loop) - - # 存储混合动画 - if actor_id not in self.blend_intervals: - self.blend_intervals[actor_id] = {} - self.blend_intervals[actor_id][blend_name] = blend - - # 播放混合动画 - blend.start() - - print(f"开始动画混合: {from_anim} -> {to_anim} (Actor: {actor_id}, 时间: {blend_time}s)") - return blend - - except Exception as e: - print(f"动画混合失败: {e}") - import traceback - traceback.print_exc() - return None - - def stop_blend(self, actor_id, blend_name=None): - """ - 停止动画混合 - :param actor_id: Actor标识符 - :param blend_name: 混合动画名称,如果为None则停止所有混合 - """ - if actor_id not in self.blend_intervals: - return - - try: - if blend_name: - if blend_name in self.blend_intervals[actor_id]: - blend = self.blend_intervals[actor_id][blend_name] - if blend: - blend.finish() - del self.blend_intervals[actor_id][blend_name] - print(f"停止动画混合: {blend_name} (Actor: {actor_id})") - else: - # 停止所有混合 - for blend in self.blend_intervals[actor_id].values(): - if blend: - blend.finish() - self.blend_intervals[actor_id].clear() - print(f"停止所有动画混合 (Actor: {actor_id})") - - except Exception as e: - print(f"停止动画混合失败: {e}") - import traceback - traceback.print_exc() - - def create_animation_sequence(self, actor_id, anim_list, loop=False): - """ - 创建动画序列 - :param actor_id: Actor标识符 - :param anim_list: 动画名称列表 - :param loop: 是否循环播放序列 - :return: 序列间隔对象 - """ - if actor_id not in self.actors: - print(f"错误: Actor {actor_id} 不存在") - return None - - actor = self.actors[actor_id] - - try: - # 验证动画存在性 - for anim_name in anim_list: - if anim_name not in actor.getAnimNames(): - print(f"错误: 动画 {anim_name} 不存在于Actor {actor_id} 中") - return None - - # 创建动画序列 - sequence = Sequence() - for anim_name in anim_list: - interval = actor.actorInterval(anim_name) - sequence.append(interval) - - # 存储序列 - sequence_name = f"seq_{'_'.join(anim_list)}" - if actor_id not in self.animation_intervals: - self.animation_intervals[actor_id] = {} - self.animation_intervals[actor_id][sequence_name] = sequence - - # 设置循环 - if loop: - sequence.loop() - else: - sequence.start() - - print(f"创建动画序列: {sequence_name} (Actor: {actor_id})") - return sequence - - except Exception as e: - print(f"创建动画序列失败: {e}") - import traceback - traceback.print_exc() - return None - - def stop_animation_sequence(self, actor_id, sequence_name=None): - """ - 停止动画序列 - :param actor_id: Actor标识符 - :param sequence_name: 序列名称,如果为None则停止所有序列 - """ - if actor_id not in self.animation_intervals: - return - - try: - if sequence_name: - if sequence_name in self.animation_intervals[actor_id]: - sequence = self.animation_intervals[actor_id][sequence_name] - if sequence: - sequence.finish() - del self.animation_intervals[actor_id][sequence_name] - print(f"停止动画序列: {sequence_name} (Actor: {actor_id})") - else: - # 停止所有序列 - for sequence in self.animation_intervals[actor_id].values(): - if sequence: - sequence.finish() - self.animation_intervals[actor_id].clear() - print(f"停止所有动画序列 (Actor: {actor_id})") - - except Exception as e: - print(f"停止动画序列失败: {e}") - import traceback - traceback.print_exc() - - def get_actor_info(self, actor_id): - """ - 获取Actor详细信息 - :param actor_id: Actor标识符 - :return: Actor信息字典 - """ - if actor_id not in self.actors: - print(f"错误: Actor {actor_id} 不存在") - return None - - actor = self.actors[actor_id] - config = self.actor_configs.get(actor_id, {}) - - try: - info = { - 'id': actor_id, - 'model_path': config.get('model_path'), - 'position': actor.getPos().toTuple(), - 'scale': actor.getScale().toTuple(), - 'available_animations': actor.getAnimNames(), - 'active_animations': list(self.active_animations.get(actor_id, {}).keys()), - 'has_character': actor.hasCharacter(), - 'num_parts': actor.getNumParts(), - 'bounds': { - 'min': actor.getTightBounds()[0].toTuple() if actor.getTightBounds() else None, - 'max': actor.getTightBounds()[1].toTuple() if actor.getTightBounds() else None - } - } - return info - - except Exception as e: - print(f"获取Actor信息失败: {e}") - import traceback - traceback.print_exc() - return None - - def set_actor_position(self, actor_id, x, y, z): - """ - 设置Actor位置 - :param actor_id: Actor标识符 - :param x: X坐标 - :param y: Y坐标 - :param z: Z坐标 - """ - if actor_id in self.actors: - self.actors[actor_id].setPos(x, y, z) - # 更新配置 - if actor_id in self.actor_configs: - self.actor_configs[actor_id]['position'] = (x, y, z) - print(f"设置Actor {actor_id} 位置: ({x}, {y}, {z})") - - def set_actor_scale(self, actor_id, scale): - """ - 设置Actor缩放 - :param actor_id: Actor标识符 - :param scale: 缩放值 (数值或(x, y, z)元组) - """ - if actor_id in self.actors: - if isinstance(scale, (int, float)): - self.actors[actor_id].setScale(scale) - else: - self.actors[actor_id].setScale(*scale) - # 更新配置 - if actor_id in self.actor_configs: - self.actor_configs[actor_id]['scale'] = scale - print(f"设置Actor {actor_id} 缩放: {scale}") - - def get_advanced_manager(self, actor_id): - """ - 获取指定Actor的高级动画管理器 - :param actor_id: Actor标识符 - :return: AdvancedAnimationManager实例 - """ - return self.advanced_managers.get(actor_id) - - def compress_animation(self, actor_id, anim_name, output_path=None): - """ - 压缩指定动画 - :param actor_id: Actor标识符 - :param anim_name: 动画名称 - :param output_path: 输出路径 - """ - if actor_id in self.actors: - actor = self.actors[actor_id] - return self.compression_system.compress_animation(actor, anim_name, output_path) - return None - - def get_cache_stats(self): - """ - 获取缓存统计信息 - :return: 缓存统计 - """ - return self.caching_system.get_cache_stats() - - def update_lod(self, camera_pos): - """ - 更新LOD系统 - :param camera_pos: 摄像机位置 - """ - self.lod_system.update_lod_levels(camera_pos) - - def get_lod_stats(self): - """ - 获取LOD统计信息 - :return: LOD统计 - """ - return self.lod_system.get_lod_stats() - - def enable_distributed_mode(self, mode='server'): - """ - 启用分布式模式 - :param mode: 模式 ('server' 或 'client') - """ - self.distributed_manager.enable_distributed_mode(mode) - - def create_distributed_actor(self, actor_id, model_path, anim_dict=None): - """ - 创建分布式Actor - :param actor_id: Actor ID - :param model_path: 模型路径 - :param anim_dict: 动画字典 - :return: Actor对象 - """ - return self.distributed_manager.create_distributed_actor(actor_id, model_path, anim_dict) - - def serialize_actor_state(self, actor_id, file_path=None): - """ - 序列化Actor状态 - :param actor_id: Actor ID - :param file_path: 文件路径 - """ - if actor_id in self.actors: - actor = self.actors[actor_id] - if file_path: - self.serializer.save_state_to_file(actor, file_path) - else: - return self.serializer.serialize_actor_state(actor) - return None - - -class AnimationEventManager: - """ - 动画事件管理器 - 处理动画事件和回调 - """ - - def __init__(self, world, animation_manager): - self.world = world - self.animation_manager = animation_manager - self.event_callbacks = {} # 存储事件回调 {event_name: [callback_list]} - - def register_event_callback(self, event_name, callback): - """ - 注册事件回调 - :param event_name: 事件名称 - :param callback: 回调函数 - """ - if event_name not in self.event_callbacks: - self.event_callbacks[event_name] = [] - self.event_callbacks[event_name].append(callback) - print(f"注册事件回调: {event_name}") - - def unregister_event_callback(self, event_name, callback): - """ - 注销事件回调 - :param event_name: 事件名称 - :param callback: 回调函数 - """ - if event_name in self.event_callbacks: - if callback in self.event_callbacks[event_name]: - self.event_callbacks[event_name].remove(callback) - print(f"注销事件回调: {event_name}") - - def trigger_event(self, event_name, *args, **kwargs): - """ - 触发事件 - :param event_name: 事件名称 - :param args: 位置参数 - :param kwargs: 关键字参数 - """ - if event_name in self.event_callbacks: - for callback in self.event_callbacks[event_name]: - try: - callback(*args, **kwargs) - except Exception as e: - print(f"执行事件回调失败 {event_name}: {e}") - import traceback - traceback.print_exc() - - def setup_default_events(self): - """ - 设置默认事件处理器 - """ - # 注册基本动画控制事件 - self.world.accept("skeletal.load_actor", self._handle_load_actor) - self.world.accept("skeletal.play_animation", self._handle_play_animation) - self.world.accept("skeletal.stop_animation", self._handle_stop_animation) - self.world.accept("skeletal.set_animation_speed", self._handle_set_animation_speed) - self.world.accept("skeletal.pause_animation", self._handle_pause_animation) - self.world.accept("skeletal.resume_animation", self._handle_resume_animation) - print("已设置默认动画事件处理器") - - def _handle_load_actor(self, model_path, anim_dict=None, actor_id=None, position=None, scale=None): - """处理加载Actor事件""" - self.animation_manager.load_actor(model_path, anim_dict, actor_id, position, scale) - - def _handle_play_animation(self, actor_id, anim_name, loop=False, play_rate=1.0): - """处理播放动画事件""" - self.animation_manager.play_animation(actor_id, anim_name, loop, play_rate) - - def _handle_stop_animation(self, actor_id, anim_name=None): - """处理停止动画事件""" - self.animation_manager.stop_animation(actor_id, anim_name) - - def _handle_set_animation_speed(self, actor_id, anim_name, speed): - """处理设置动画速度事件""" - self.animation_manager.set_animation_speed(actor_id, anim_name, speed) - - def _handle_pause_animation(self, actor_id, anim_name=None): - """处理暂停动画事件""" - self.animation_manager.pause_animation(actor_id, anim_name) - - def _handle_resume_animation(self, actor_id, anim_name=None): - """处理恢复动画事件""" - self.animation_manager.resume_animation(actor_id, anim_name) - - -class SkeletalAnimationGUI: - """ - 骨骼动画GUI界面 - 提供可视化控制界面 - """ - - def __init__(self, world, animation_manager, event_manager): - self.world = world - self.animation_manager = animation_manager - self.event_manager = event_manager - self.panel = None - self.actor_list_elements = {} # 存储Actor列表元素 - self.animation_list_elements = {} # 存储动画列表元素 - - def create_control_panel(self): - """ - 创建控制面板 - """ - if not hasattr(self.world, 'gui_manager') or not self.world.gui_manager: - print("警告: GUI管理器不可用,无法创建控制面板") - return - - try: - # 创建主面板 - self.panel = self.world.gui_manager.createGUIPanel( - pos=(0.7, 0, 0.5), - size=(0.25, 0.4), - title="骨骼动画控制面板" - ) - - # 创建Actor控制区域 - actor_label = self.world.gui_manager.createGUIText( - parent=self.panel, - text="Actor列表:", - pos=(0.02, 0, 0.35), - scale=0.04 - ) - - # 创建Actor列表框 - self.actor_listbox = self.world.gui_manager.createGUIScrollableFrame( - parent=self.panel, - pos=(0.02, 0, 0.15), - size=(0.22, 0.18) - ) - - # 创建动画控制区域 - anim_label = self.world.gui_manager.createGUIText( - parent=self.panel, - text="动画控制:", - pos=(0.02, 0, 0.1), - scale=0.04 - ) - - # 创建控制按钮 - button_y = 0.02 - button_spacing = 0.06 - - self.play_button = self.world.gui_manager.createGUIButton( - parent=self.panel, - pos=(0.03, 0, button_y), - size=0.05, - text="播放" - ) - self.world.accept("skeletal_gui_play", self._on_play_clicked) - - self.stop_button = self.world.gui_manager.createGUIButton( - parent=self.panel, - pos=(0.09, 0, button_y), - size=0.05, - text="停止" - ) - self.world.accept("skeletal_gui_stop", self._on_stop_clicked) - - self.pause_button = self.world.gui_manager.createGUIButton( - parent=self.panel, - pos=(0.15, 0, button_y), - size=0.05, - text="暂停" - ) - self.world.accept("skeletal_gui_pause", self._on_pause_clicked) - - # 创建速度控制 - speed_label = self.world.gui_manager.createGUIText( - parent=self.panel, - text="速度:", - pos=(0.02, 0, button_y - button_spacing), - scale=0.03 - ) - - self.speed_slider = self.world.gui_manager.createGUISlider( - parent=self.panel, - pos=(0.08, 0, button_y - button_spacing), - size=0.15, - min_value=0.0, - max_value=3.0, - initial_value=1.0 - ) - - print("骨骼动画控制面板创建成功") - - except Exception as e: - print(f"创建控制面板失败: {e}") - import traceback - traceback.print_exc() - - def update_actor_list(self): - """ - 更新Actor列表 - """ - if not hasattr(self, 'actor_listbox') or not self.actor_listbox: - return - - try: - # 清除现有元素 - for element in self.actor_list_elements.values(): - self.world.gui_manager.deleteGUIElement(element) - self.actor_list_elements.clear() - - # 添加Actor列表项 - actors = self.animation_manager.actors - y_offset = 0.15 - y_spacing = 0.03 - - for i, (actor_id, actor) in enumerate(actors.items()): - # 创建Actor列表项 - actor_button = self.world.gui_manager.createGUIButton( - parent=self.actor_listbox, - pos=(0.01, 0, y_offset - i * y_spacing), - size=0.04, - text=actor_id - ) - - # 注册点击事件 - self.world.accept(f"skeletal_select_actor_{actor_id}", - lambda aid=actor_id: self._on_actor_selected(aid)) - - self.actor_list_elements[actor_id] = actor_button - - except Exception as e: - print(f"更新Actor列表失败: {e}") - import traceback - traceback.print_exc() - - def _on_actor_selected(self, actor_id): - """ - 当Actor被选中时的处理函数 - :param actor_id: 选中的Actor ID - """ - print(f"选中Actor: {actor_id}") - # 更新动画列表 - self.update_animation_list(actor_id) - - def update_animation_list(self, actor_id): - """ - 更新动画列表 - :param actor_id: Actor ID - """ - if not hasattr(self, 'actor_listbox') or not self.actor_listbox: - return - - try: - # 清除现有元素 - for element in self.animation_list_elements.values(): - self.world.gui_manager.deleteGUIElement(element) - self.animation_list_elements.clear() - - # 获取Actor动画列表 - animations = self.animation_manager.get_actor_animations(actor_id) - y_offset = -0.05 - y_spacing = 0.03 - - for i, anim_name in enumerate(animations): - # 创建动画列表项 - anim_button = self.world.gui_manager.createGUIButton( - parent=self.actor_listbox, - pos=(0.01, 0, y_offset - i * y_spacing), - size=0.04, - text=anim_name - ) - - # 注册点击事件 - self.world.accept(f"skeletal_select_anim_{actor_id}_{anim_name}", - lambda aid=actor_id, an=anim_name: self._on_animation_selected(aid, an)) - - self.animation_list_elements[f"{actor_id}_{anim_name}"] = anim_button - - except Exception as e: - print(f"更新动画列表失败: {e}") - import traceback - traceback.print_exc() - - def _on_animation_selected(self, actor_id, anim_name): - """ - 当动画被选中时的处理函数 - :param actor_id: Actor ID - :param anim_name: 动画名称 - """ - print(f"选中动画: {anim_name} (Actor: {actor_id})") - - def _on_play_clicked(self): - """ - 当播放按钮被点击时的处理函数 - """ - print("播放按钮被点击") - # 这里应该实现实际的播放逻辑 - - def _on_stop_clicked(self): - """ - 当停止按钮被点击时的处理函数 - """ - print("停止按钮被点击") - # 这里应该实现实际的停止逻辑 - - def _on_pause_clicked(self): - """ - 当暂停按钮被点击时的处理函数 - """ - print("暂停按钮被点击") - # 这里应该实现实际的暂停逻辑 - - def destroy(self): - """ - 销毁GUI界面 - """ - try: - if self.panel: - self.world.gui_manager.deleteGUIElement(self.panel) - self.panel = None - print("骨骼动画GUI界面已销毁") - except Exception as e: - print(f"销毁GUI界面失败: {e}") - - -class Plugin(BasePlugin): - """ - 骨骼动画系统插件 - 提供完整的骨骼动画支持,包括加载、播放、控制、混合等高级功能 - """ - - def __init__(self, plugin_manager, name): - super().__init__(plugin_manager, name) - self.config = { - "version": "1.0.0", - "author": "EG Team", - "description": "骨骼动画系统,支持模型动画加载、播放控制、混合等高级功能" - } - self.animation_manager = None - self.event_manager = None - self.gui = None - self.is_enabled = False - self.update_task = None - - def initialize(self) -> bool: - """ - 初始化插件 - """ - print(f"初始化骨骼动画插件: {self.name}") - - try: - # 创建动画管理器 - self.animation_manager = SkeletalAnimationManager(self.plugin_manager.world) - - # 创建事件管理器 - self.event_manager = AnimationEventManager(self.plugin_manager.world, self.animation_manager) - - # 创建GUI - self.gui = SkeletalAnimationGUI(self.plugin_manager.world, - self.animation_manager, - self.event_manager) - - print(f"骨骼动画插件初始化成功: {self.name}") - return True - - except Exception as e: - print(f"骨骼动画插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - """ - if not super().enable(): - return False - - if self.is_enabled: - print(f"骨骼动画插件已启用: {self.name}") - return True - - print(f"启用骨骼动画插件: {self.name}") - - try: - # 设置默认事件处理器 - self.event_manager.setup_default_events() - - # 创建GUI控制面板 - self.gui.create_control_panel() - - # 启动更新任务 - self.update_task = self.plugin_manager.world.taskMgr.add( - self._update_task, - f"skeletal_animation_update_{self.name}", - sort=30 - ) - - # 标记为已启用 - self.is_enabled = True - - print(f"骨骼动画插件启用成功: {self.name}") - return True - - except Exception as e: - print(f"骨骼动画插件启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self) -> bool: - """ - 禁用插件 - """ - if not super().disable(): - return False - - if not self.is_enabled: - print(f"骨骼动画插件已禁用: {self.name}") - return True - - print(f"禁用骨骼动画插件: {self.name}") - - try: - # 停止更新任务 - if self.update_task: - self.plugin_manager.world.taskMgr.remove(self.update_task) - self.update_task = None - - # 移除事件处理器 - self.plugin_manager.world.ignore("skeletal.load_actor") - self.plugin_manager.world.ignore("skeletal.play_animation") - self.plugin_manager.world.ignore("skeletal.stop_animation") - self.plugin_manager.world.ignore("skeletal.set_animation_speed") - self.plugin_manager.world.ignore("skeletal.pause_animation") - self.plugin_manager.world.ignore("skeletal.resume_animation") - - # 销毁GUI - self.gui.destroy() - - # 标记为已禁用 - self.is_enabled = False - - print(f"骨骼动画插件禁用成功: {self.name}") - return True - - except Exception as e: - print(f"骨骼动画插件禁用失败: {e}") - import traceback - traceback.print_exc() - return False - - def finalize(self): - """ - 清理插件资源 - """ - print(f"清理骨骼动画插件资源: {self.name}") - - try: - # 停止更新任务 - if self.update_task: - self.plugin_manager.world.taskMgr.remove(self.update_task) - self.update_task = None - - # 停止所有动画 - for actor_id in list(self.animation_manager.actors.keys()): - self.animation_manager.unload_actor(actor_id) - - # 清理事件处理器 - self.event_manager = None - - # 清理GUI - self.gui = None - - # 清理动画管理器 - self.animation_manager = None - - print(f"骨骼动画插件资源清理完成: {self.name}") - - except Exception as e: - print(f"清理骨骼动画插件资源失败: {e}") - import traceback - traceback.print_exc() - - def _update_task(self, task): - """ - 插件更新任务 - """ - try: - # 获取摄像机位置用于LOD计算 - if hasattr(self.plugin_manager.world, 'camera'): - camera_pos = self.plugin_manager.world.camera.getPos() - self.animation_manager.update_lod(camera_pos) - - # 更新高级动画系统 - dt = globalClock.getDt() - time = globalClock.getRealTime() - - for actor_id, advanced_manager in self.animation_manager.advanced_managers.items(): - advanced_manager.update(dt, time) - - except Exception as e: - print(f"插件更新任务出错: {e}") - - return task.cont - - def get_animation_manager(self): - """ - 获取动画管理器 - :return: SkeletalAnimationManager实例 - """ - return self.animation_manager - - def get_event_manager(self): - """ - 获取事件管理器 - :return: AnimationEventManager实例 - """ - return self.event_manager - - def get_gui(self): - """ - 获取GUI界面 - :return: SkeletalAnimationGUI实例 - """ +""" +骨骼动画系统插件 +提供完整的骨骼动画支持,包括加载、播放、控制、混合等高级功能 +""" + +from plugins.plugin_manager import BasePlugin +from direct.actor.Actor import Actor +from direct.interval.IntervalGlobal import * +from panda3d.core import Filename, ConfigVariableBool +from panda3d.core import NodePath, Vec3, Point3 +import os +import json +import math + +# 导入高级功能模块 +from .advanced_features import AdvancedAnimationManager +from .animation_optimization import AnimationCompression, AnimationCaching, AnimationLOD +from .network_sync import DistributedAnimationManager, AnimationStateSerializer + + +class SkeletalAnimationManager: + """ + 骨骼动画管理器类 + 负责管理场景中所有的骨骼动画Actor + """ + + def __init__(self, world): + self.world = world + self.actors = {} # 存储所有Actor {actor_id: actor_object} + self.actor_configs = {} # 存储Actor配置 {actor_id: config_dict} + self.active_animations = {} # 存储活动动画 {actor_id: {anim_name: control_object}} + self.animation_intervals = {} # 存储动画间隔 {actor_id: {interval_name: interval_object}} + self.blend_intervals = {} # 存储混合动画 {actor_id: {blend_name: blend_object}} + + # 高级功能管理器 + self.advanced_managers = {} # {actor_id: AdvancedAnimationManager} + self.compression_system = AnimationCompression() + self.caching_system = AnimationCaching() + self.lod_system = AnimationLOD(world) + self.distributed_manager = DistributedAnimationManager(world) + self.serializer = AnimationStateSerializer() + + def load_actor(self, model_path, anim_dict=None, actor_id=None, position=None, scale=None): + """ + 加载Actor模型 + :param model_path: 模型文件路径 + :param anim_dict: 动画字典,格式 {'anim_name': 'anim_path'} + :param actor_id: Actor标识符,如果为None则自动生成 + :param position: 位置坐标 (x, y, z) + :param scale: 缩放比例 (x, y, z) 或单个数值 + :return: Actor对象和actor_id + """ + try: + if not os.path.exists(model_path): + print(f"错误: 模型文件不存在: {model_path}") + return None, None + + if actor_id is None: + actor_id = f"actor_{len(self.actors)}" + + # 检查缓存 + cached_actor = self.caching_system.get_cached_animation(actor_id, "model") + if cached_actor: + actor = cached_actor + else: + # 创建Actor + if anim_dict: + # 验证动画文件存在性 + validated_anims = {} + for anim_name, anim_path in anim_dict.items(): + if os.path.exists(anim_path): + validated_anims[anim_name] = anim_path + else: + print(f"警告: 动画文件不存在: {anim_path}") + + actor = Actor(model_path, validated_anims) + else: + actor = Actor(model_path) + + # 缓存Actor + self.caching_system.cache_animation(actor_id, "model", actor) + + # 设置位置和缩放 + if position: + actor.setPos(*position) + if scale: + if isinstance(scale, (int, float)): + actor.setScale(scale) + else: + actor.setScale(*scale) + + # 将Actor添加到场景中 + actor.reparentTo(self.world.render) + actor.setName(actor_id) + + # 存储Actor和配置 + self.actors[actor_id] = actor + self.actor_configs[actor_id] = { + 'model_path': model_path, + 'anim_dict': anim_dict, + 'position': position, + 'scale': scale + } + + # 创建高级动画管理器 + self.advanced_managers[actor_id] = AdvancedAnimationManager(actor) + + print(f"成功加载Actor: {actor_id}") + print(f" 模型: {model_path}") + print(f" 位置: {position}") + print(f" 缩放: {scale}") + print(f" 可用动画: {actor.getAnimNames()}") + + return actor, actor_id + + except Exception as e: + print(f"加载Actor失败: {e}") + import traceback + traceback.print_exc() + return None, None + + def unload_actor(self, actor_id): + """ + 卸载Actor + :param actor_id: 要卸载的Actor标识符 + """ + if actor_id in self.actors: + # 停止所有活动动画 + self.stop_all_animations(actor_id) + + # 清理混合动画 + if actor_id in self.blend_intervals: + for blend in self.blend_intervals[actor_id].values(): + if blend: + blend.finish() + del self.blend_intervals[actor_id] + + # 清理动画间隔 + if actor_id in self.animation_intervals: + for interval in self.animation_intervals[actor_id].values(): + if interval: + interval.finish() + del self.animation_intervals[actor_id] + + # 清理高级动画管理器 + if actor_id in self.advanced_managers: + del self.advanced_managers[actor_id] + + # 清理Actor + actor = self.actors[actor_id] + actor.cleanup() + del self.actors[actor_id] + + # 清理配置 + if actor_id in self.actor_configs: + del self.actor_configs[actor_id] + + # 清理活动动画记录 + if actor_id in self.active_animations: + del self.active_animations[actor_id] + + # 使缓存失效 + self.caching_system.invalidate_cache(actor_id) + + print(f"已卸载Actor: {actor_id}") + + def get_actor(self, actor_id): + """ + 获取Actor对象 + :param actor_id: Actor标识符 + :return: Actor对象或None + """ + return self.actors.get(actor_id) + + def get_actor_animations(self, actor_id): + """ + 获取Actor的所有动画名称 + :param actor_id: Actor标识符 + :return: 动画名称列表 + """ + if actor_id in self.actors: + return self.actors[actor_id].getAnimNames() + return [] + + def play_animation(self, actor_id, anim_name, loop=False, play_rate=1.0, start_frame=None, end_frame=None): + """ + 播放动画 + :param actor_id: Actor标识符 + :param anim_name: 动画名称 + :param loop: 是否循环播放 + :param play_rate: 播放速度 + :param start_frame: 起始帧 + :param end_frame: 结束帧 + """ + if actor_id not in self.actors: + print(f"错误: Actor {actor_id} 不存在") + return False + + actor = self.actors[actor_id] + + # 检查动画是否存在 + if anim_name not in actor.getAnimNames(): + print(f"错误: 动画 {anim_name} 不存在于Actor {actor_id} 中") + print(f" 可用动画: {actor.getAnimNames()}") + return False + + try: + # 设置播放速度 + actor.setPlayRate(play_rate, anim_name) + + # 播放动画 + if loop: + if start_frame is not None and end_frame is not None: + actor.loop(anim_name, restart=False, fromFrame=start_frame, toFrame=end_frame) + else: + actor.loop(anim_name, restart=False) + else: + if start_frame is not None and end_frame is not None: + actor.play(anim_name, fromFrame=start_frame, toFrame=end_frame) + else: + actor.play(anim_name) + + # 记录活动动画 + if actor_id not in self.active_animations: + self.active_animations[actor_id] = {} + self.active_animations[actor_id][anim_name] = actor.getAnimControl(anim_name) + + print(f"播放动画: {anim_name} (Actor: {actor_id}, 循环: {loop}, 速度: {play_rate}x)") + return True + + except Exception as e: + print(f"播放动画失败: {e}") + import traceback + traceback.print_exc() + return False + + def stop_animation(self, actor_id, anim_name=None): + """ + 停止动画 + :param actor_id: Actor标识符 + :param anim_name: 动画名称,如果为None则停止所有动画 + """ + if actor_id not in self.actors: + print(f"错误: Actor {actor_id} 不存在") + return + + actor = self.actors[actor_id] + + try: + if anim_name: + # 停止指定动画 + if anim_name in actor.getAnimNames(): + actor.stop(anim_name) + # 从活动动画中移除 + if actor_id in self.active_animations and anim_name in self.active_animations[actor_id]: + del self.active_animations[actor_id][anim_name] + print(f"停止动画: {anim_name} (Actor: {actor_id})") + else: + print(f"警告: 动画 {anim_name} 不存在于Actor {actor_id} 中") + else: + # 停止所有动画 + actor.stop() + # 清空活动动画记录 + if actor_id in self.active_animations: + self.active_animations[actor_id].clear() + print(f"停止所有动画 (Actor: {actor_id})") + + except Exception as e: + print(f"停止动画失败: {e}") + import traceback + traceback.print_exc() + + def stop_all_animations(self, actor_id): + """ + 停止指定Actor的所有动画 + :param actor_id: Actor标识符 + """ + self.stop_animation(actor_id) + + def set_animation_speed(self, actor_id, anim_name, speed): + """ + 设置动画播放速度 + :param actor_id: Actor标识符 + :param anim_name: 动画名称 + :param speed: 播放速度 + """ + if actor_id not in self.actors: + print(f"错误: Actor {actor_id} 不存在") + return + + actor = self.actors[actor_id] + + try: + if anim_name in actor.getAnimNames(): + actor.setPlayRate(speed, anim_name) + print(f"设置动画速度: {anim_name} 为 {speed}x (Actor: {actor_id})") + else: + print(f"警告: 动画 {anim_name} 不存在于Actor {actor_id} 中") + + except Exception as e: + print(f"设置动画速度失败: {e}") + import traceback + traceback.print_exc() + + def pause_animation(self, actor_id, anim_name=None): + """ + 暂停动画 + :param actor_id: Actor标识符 + :param anim_name: 动画名称,如果为None则暂停所有动画 + """ + if actor_id not in self.actors: + print(f"错误: Actor {actor_id} 不存在") + return + + actor = self.actors[actor_id] + + try: + if anim_name: + if anim_name in actor.getAnimNames(): + control = actor.getAnimControl(anim_name) + if control and control.isPlaying(): + control.setPlayRate(0) # 通过设置播放速度为0来模拟暂停 + print(f"暂停动画: {anim_name} (Actor: {actor_id})") + else: + print(f"警告: 动画 {anim_name} 不存在于Actor {actor_id} 中") + else: + # 暂停所有活动动画 + if actor_id in self.active_animations: + for anim_name, control in self.active_animations[actor_id].items(): + if control and control.isPlaying(): + control.setPlayRate(0) + print(f"暂停所有动画 (Actor: {actor_id})") + + except Exception as e: + print(f"暂停动画失败: {e}") + import traceback + traceback.print_exc() + + def resume_animation(self, actor_id, anim_name=None): + """ + 恢复动画播放 + :param actor_id: Actor标识符 + :param anim_name: 动画名称,如果为None则恢复所有动画 + """ + if actor_id not in self.actors: + print(f"错误: Actor {actor_id} 不存在") + return + + actor = self.actors[actor_id] + + try: + if anim_name: + if anim_name in actor.getAnimNames(): + control = actor.getAnimControl(anim_name) + if control and control.getPlayRate() == 0: + control.setPlayRate(1.0) # 恢复正常播放速度 + print(f"恢复动画: {anim_name} (Actor: {actor_id})") + else: + print(f"警告: 动画 {anim_name} 不存在于Actor {actor_id} 中") + else: + # 恢复所有暂停的动画 + if actor_id in self.active_animations: + for anim_name, control in self.active_animations[actor_id].items(): + if control and control.getPlayRate() == 0: + control.setPlayRate(1.0) + print(f"恢复所有动画 (Actor: {actor_id})") + + except Exception as e: + print(f"恢复动画失败: {e}") + import traceback + traceback.print_exc() + + def get_animation_info(self, actor_id, anim_name): + """ + 获取动画信息 + :param actor_id: Actor标识符 + :param anim_name: 动画名称 + :return: 动画信息字典 + """ + if actor_id not in self.actors: + print(f"错误: Actor {actor_id} 不存在") + return None + + actor = self.actors[actor_id] + + if anim_name not in actor.getAnimNames(): + print(f"错误: 动画 {anim_name} 不存在于Actor {actor_id} 中") + return None + + try: + control = actor.getAnimControl(anim_name) + info = { + 'name': anim_name, + 'frame_rate': actor.getFrameRate(anim_name), + 'num_frames': actor.getNumFrames(anim_name), + 'play_rate': actor.getPlayRate(anim_name), + 'is_playing': control.isPlaying() if control else False, + 'is_paused': control.getPlayRate() == 0 if control else False, + 'current_frame': actor.getCurrentFrame(anim_name) if control else 0 + } + return info + + except Exception as e: + print(f"获取动画信息失败: {e}") + import traceback + traceback.print_exc() + return None + + def blend_animations(self, actor_id, from_anim, to_anim, blend_time=1.0, loop=True): + """ + 混合两个动画 + :param actor_id: Actor标识符 + :param from_anim: 起始动画名称 + :param to_anim: 目标动画名称 + :param blend_time: 混合时间 + :param loop: 是否循环混合 + """ + if actor_id not in self.actors: + print(f"错误: Actor {actor_id} 不存在") + return None + + actor = self.actors[actor_id] + + # 检查动画是否存在 + if from_anim not in actor.getAnimNames(): + print(f"错误: 起始动画 {from_anim} 不存在于Actor {actor_id} 中") + return None + + if to_anim not in actor.getAnimNames(): + print(f"错误: 目标动画 {to_anim} 不存在于Actor {actor_id} 中") + return None + + try: + # 创建动画混合 + blend_name = f"{from_anim}_to_{to_anim}" + blend = actor.actorInterval(from_anim, endTime=actor.getDuration(from_anim)) \ + + actor.actorInterval(to_anim, loop=loop) + + # 存储混合动画 + if actor_id not in self.blend_intervals: + self.blend_intervals[actor_id] = {} + self.blend_intervals[actor_id][blend_name] = blend + + # 播放混合动画 + blend.start() + + print(f"开始动画混合: {from_anim} -> {to_anim} (Actor: {actor_id}, 时间: {blend_time}s)") + return blend + + except Exception as e: + print(f"动画混合失败: {e}") + import traceback + traceback.print_exc() + return None + + def stop_blend(self, actor_id, blend_name=None): + """ + 停止动画混合 + :param actor_id: Actor标识符 + :param blend_name: 混合动画名称,如果为None则停止所有混合 + """ + if actor_id not in self.blend_intervals: + return + + try: + if blend_name: + if blend_name in self.blend_intervals[actor_id]: + blend = self.blend_intervals[actor_id][blend_name] + if blend: + blend.finish() + del self.blend_intervals[actor_id][blend_name] + print(f"停止动画混合: {blend_name} (Actor: {actor_id})") + else: + # 停止所有混合 + for blend in self.blend_intervals[actor_id].values(): + if blend: + blend.finish() + self.blend_intervals[actor_id].clear() + print(f"停止所有动画混合 (Actor: {actor_id})") + + except Exception as e: + print(f"停止动画混合失败: {e}") + import traceback + traceback.print_exc() + + def create_animation_sequence(self, actor_id, anim_list, loop=False): + """ + 创建动画序列 + :param actor_id: Actor标识符 + :param anim_list: 动画名称列表 + :param loop: 是否循环播放序列 + :return: 序列间隔对象 + """ + if actor_id not in self.actors: + print(f"错误: Actor {actor_id} 不存在") + return None + + actor = self.actors[actor_id] + + try: + # 验证动画存在性 + for anim_name in anim_list: + if anim_name not in actor.getAnimNames(): + print(f"错误: 动画 {anim_name} 不存在于Actor {actor_id} 中") + return None + + # 创建动画序列 + sequence = Sequence() + for anim_name in anim_list: + interval = actor.actorInterval(anim_name) + sequence.append(interval) + + # 存储序列 + sequence_name = f"seq_{'_'.join(anim_list)}" + if actor_id not in self.animation_intervals: + self.animation_intervals[actor_id] = {} + self.animation_intervals[actor_id][sequence_name] = sequence + + # 设置循环 + if loop: + sequence.loop() + else: + sequence.start() + + print(f"创建动画序列: {sequence_name} (Actor: {actor_id})") + return sequence + + except Exception as e: + print(f"创建动画序列失败: {e}") + import traceback + traceback.print_exc() + return None + + def stop_animation_sequence(self, actor_id, sequence_name=None): + """ + 停止动画序列 + :param actor_id: Actor标识符 + :param sequence_name: 序列名称,如果为None则停止所有序列 + """ + if actor_id not in self.animation_intervals: + return + + try: + if sequence_name: + if sequence_name in self.animation_intervals[actor_id]: + sequence = self.animation_intervals[actor_id][sequence_name] + if sequence: + sequence.finish() + del self.animation_intervals[actor_id][sequence_name] + print(f"停止动画序列: {sequence_name} (Actor: {actor_id})") + else: + # 停止所有序列 + for sequence in self.animation_intervals[actor_id].values(): + if sequence: + sequence.finish() + self.animation_intervals[actor_id].clear() + print(f"停止所有动画序列 (Actor: {actor_id})") + + except Exception as e: + print(f"停止动画序列失败: {e}") + import traceback + traceback.print_exc() + + def get_actor_info(self, actor_id): + """ + 获取Actor详细信息 + :param actor_id: Actor标识符 + :return: Actor信息字典 + """ + if actor_id not in self.actors: + print(f"错误: Actor {actor_id} 不存在") + return None + + actor = self.actors[actor_id] + config = self.actor_configs.get(actor_id, {}) + + try: + info = { + 'id': actor_id, + 'model_path': config.get('model_path'), + 'position': actor.getPos().toTuple(), + 'scale': actor.getScale().toTuple(), + 'available_animations': actor.getAnimNames(), + 'active_animations': list(self.active_animations.get(actor_id, {}).keys()), + 'has_character': actor.hasCharacter(), + 'num_parts': actor.getNumParts(), + 'bounds': { + 'min': actor.getTightBounds()[0].toTuple() if actor.getTightBounds() else None, + 'max': actor.getTightBounds()[1].toTuple() if actor.getTightBounds() else None + } + } + return info + + except Exception as e: + print(f"获取Actor信息失败: {e}") + import traceback + traceback.print_exc() + return None + + def set_actor_position(self, actor_id, x, y, z): + """ + 设置Actor位置 + :param actor_id: Actor标识符 + :param x: X坐标 + :param y: Y坐标 + :param z: Z坐标 + """ + if actor_id in self.actors: + self.actors[actor_id].setPos(x, y, z) + # 更新配置 + if actor_id in self.actor_configs: + self.actor_configs[actor_id]['position'] = (x, y, z) + print(f"设置Actor {actor_id} 位置: ({x}, {y}, {z})") + + def set_actor_scale(self, actor_id, scale): + """ + 设置Actor缩放 + :param actor_id: Actor标识符 + :param scale: 缩放值 (数值或(x, y, z)元组) + """ + if actor_id in self.actors: + if isinstance(scale, (int, float)): + self.actors[actor_id].setScale(scale) + else: + self.actors[actor_id].setScale(*scale) + # 更新配置 + if actor_id in self.actor_configs: + self.actor_configs[actor_id]['scale'] = scale + print(f"设置Actor {actor_id} 缩放: {scale}") + + def get_advanced_manager(self, actor_id): + """ + 获取指定Actor的高级动画管理器 + :param actor_id: Actor标识符 + :return: AdvancedAnimationManager实例 + """ + return self.advanced_managers.get(actor_id) + + def compress_animation(self, actor_id, anim_name, output_path=None): + """ + 压缩指定动画 + :param actor_id: Actor标识符 + :param anim_name: 动画名称 + :param output_path: 输出路径 + """ + if actor_id in self.actors: + actor = self.actors[actor_id] + return self.compression_system.compress_animation(actor, anim_name, output_path) + return None + + def get_cache_stats(self): + """ + 获取缓存统计信息 + :return: 缓存统计 + """ + return self.caching_system.get_cache_stats() + + def update_lod(self, camera_pos): + """ + 更新LOD系统 + :param camera_pos: 摄像机位置 + """ + self.lod_system.update_lod_levels(camera_pos) + + def get_lod_stats(self): + """ + 获取LOD统计信息 + :return: LOD统计 + """ + return self.lod_system.get_lod_stats() + + def enable_distributed_mode(self, mode='server'): + """ + 启用分布式模式 + :param mode: 模式 ('server' 或 'client') + """ + self.distributed_manager.enable_distributed_mode(mode) + + def create_distributed_actor(self, actor_id, model_path, anim_dict=None): + """ + 创建分布式Actor + :param actor_id: Actor ID + :param model_path: 模型路径 + :param anim_dict: 动画字典 + :return: Actor对象 + """ + return self.distributed_manager.create_distributed_actor(actor_id, model_path, anim_dict) + + def serialize_actor_state(self, actor_id, file_path=None): + """ + 序列化Actor状态 + :param actor_id: Actor ID + :param file_path: 文件路径 + """ + if actor_id in self.actors: + actor = self.actors[actor_id] + if file_path: + self.serializer.save_state_to_file(actor, file_path) + else: + return self.serializer.serialize_actor_state(actor) + return None + + +class AnimationEventManager: + """ + 动画事件管理器 + 处理动画事件和回调 + """ + + def __init__(self, world, animation_manager): + self.world = world + self.animation_manager = animation_manager + self.event_callbacks = {} # 存储事件回调 {event_name: [callback_list]} + + def register_event_callback(self, event_name, callback): + """ + 注册事件回调 + :param event_name: 事件名称 + :param callback: 回调函数 + """ + if event_name not in self.event_callbacks: + self.event_callbacks[event_name] = [] + self.event_callbacks[event_name].append(callback) + print(f"注册事件回调: {event_name}") + + def unregister_event_callback(self, event_name, callback): + """ + 注销事件回调 + :param event_name: 事件名称 + :param callback: 回调函数 + """ + if event_name in self.event_callbacks: + if callback in self.event_callbacks[event_name]: + self.event_callbacks[event_name].remove(callback) + print(f"注销事件回调: {event_name}") + + def trigger_event(self, event_name, *args, **kwargs): + """ + 触发事件 + :param event_name: 事件名称 + :param args: 位置参数 + :param kwargs: 关键字参数 + """ + if event_name in self.event_callbacks: + for callback in self.event_callbacks[event_name]: + try: + callback(*args, **kwargs) + except Exception as e: + print(f"执行事件回调失败 {event_name}: {e}") + import traceback + traceback.print_exc() + + def setup_default_events(self): + """ + 设置默认事件处理器 + """ + # 注册基本动画控制事件 + self.world.accept("skeletal.load_actor", self._handle_load_actor) + self.world.accept("skeletal.play_animation", self._handle_play_animation) + self.world.accept("skeletal.stop_animation", self._handle_stop_animation) + self.world.accept("skeletal.set_animation_speed", self._handle_set_animation_speed) + self.world.accept("skeletal.pause_animation", self._handle_pause_animation) + self.world.accept("skeletal.resume_animation", self._handle_resume_animation) + print("已设置默认动画事件处理器") + + def _handle_load_actor(self, model_path, anim_dict=None, actor_id=None, position=None, scale=None): + """处理加载Actor事件""" + self.animation_manager.load_actor(model_path, anim_dict, actor_id, position, scale) + + def _handle_play_animation(self, actor_id, anim_name, loop=False, play_rate=1.0): + """处理播放动画事件""" + self.animation_manager.play_animation(actor_id, anim_name, loop, play_rate) + + def _handle_stop_animation(self, actor_id, anim_name=None): + """处理停止动画事件""" + self.animation_manager.stop_animation(actor_id, anim_name) + + def _handle_set_animation_speed(self, actor_id, anim_name, speed): + """处理设置动画速度事件""" + self.animation_manager.set_animation_speed(actor_id, anim_name, speed) + + def _handle_pause_animation(self, actor_id, anim_name=None): + """处理暂停动画事件""" + self.animation_manager.pause_animation(actor_id, anim_name) + + def _handle_resume_animation(self, actor_id, anim_name=None): + """处理恢复动画事件""" + self.animation_manager.resume_animation(actor_id, anim_name) + + +class SkeletalAnimationGUI: + """ + 骨骼动画GUI界面 + 提供可视化控制界面 + """ + + def __init__(self, world, animation_manager, event_manager): + self.world = world + self.animation_manager = animation_manager + self.event_manager = event_manager + self.panel = None + self.actor_list_elements = {} # 存储Actor列表元素 + self.animation_list_elements = {} # 存储动画列表元素 + + def create_control_panel(self): + """ + 创建控制面板 + """ + if not hasattr(self.world, 'gui_manager') or not self.world.gui_manager: + print("警告: GUI管理器不可用,无法创建控制面板") + return + + try: + # 创建主面板 + self.panel = self.world.gui_manager.createGUIPanel( + pos=(0.7, 0, 0.5), + size=(0.25, 0.4), + title="骨骼动画控制面板" + ) + + # 创建Actor控制区域 + actor_label = self.world.gui_manager.createGUIText( + parent=self.panel, + text="Actor列表:", + pos=(0.02, 0, 0.35), + scale=0.04 + ) + + # 创建Actor列表框 + self.actor_listbox = self.world.gui_manager.createGUIScrollableFrame( + parent=self.panel, + pos=(0.02, 0, 0.15), + size=(0.22, 0.18) + ) + + # 创建动画控制区域 + anim_label = self.world.gui_manager.createGUIText( + parent=self.panel, + text="动画控制:", + pos=(0.02, 0, 0.1), + scale=0.04 + ) + + # 创建控制按钮 + button_y = 0.02 + button_spacing = 0.06 + + self.play_button = self.world.gui_manager.createGUIButton( + parent=self.panel, + pos=(0.03, 0, button_y), + size=0.05, + text="播放" + ) + self.world.accept("skeletal_gui_play", self._on_play_clicked) + + self.stop_button = self.world.gui_manager.createGUIButton( + parent=self.panel, + pos=(0.09, 0, button_y), + size=0.05, + text="停止" + ) + self.world.accept("skeletal_gui_stop", self._on_stop_clicked) + + self.pause_button = self.world.gui_manager.createGUIButton( + parent=self.panel, + pos=(0.15, 0, button_y), + size=0.05, + text="暂停" + ) + self.world.accept("skeletal_gui_pause", self._on_pause_clicked) + + # 创建速度控制 + speed_label = self.world.gui_manager.createGUIText( + parent=self.panel, + text="速度:", + pos=(0.02, 0, button_y - button_spacing), + scale=0.03 + ) + + self.speed_slider = self.world.gui_manager.createGUISlider( + parent=self.panel, + pos=(0.08, 0, button_y - button_spacing), + size=0.15, + min_value=0.0, + max_value=3.0, + initial_value=1.0 + ) + + print("骨骼动画控制面板创建成功") + + except Exception as e: + print(f"创建控制面板失败: {e}") + import traceback + traceback.print_exc() + + def update_actor_list(self): + """ + 更新Actor列表 + """ + if not hasattr(self, 'actor_listbox') or not self.actor_listbox: + return + + try: + # 清除现有元素 + for element in self.actor_list_elements.values(): + self.world.gui_manager.deleteGUIElement(element) + self.actor_list_elements.clear() + + # 添加Actor列表项 + actors = self.animation_manager.actors + y_offset = 0.15 + y_spacing = 0.03 + + for i, (actor_id, actor) in enumerate(actors.items()): + # 创建Actor列表项 + actor_button = self.world.gui_manager.createGUIButton( + parent=self.actor_listbox, + pos=(0.01, 0, y_offset - i * y_spacing), + size=0.04, + text=actor_id + ) + + # 注册点击事件 + self.world.accept(f"skeletal_select_actor_{actor_id}", + lambda aid=actor_id: self._on_actor_selected(aid)) + + self.actor_list_elements[actor_id] = actor_button + + except Exception as e: + print(f"更新Actor列表失败: {e}") + import traceback + traceback.print_exc() + + def _on_actor_selected(self, actor_id): + """ + 当Actor被选中时的处理函数 + :param actor_id: 选中的Actor ID + """ + print(f"选中Actor: {actor_id}") + # 更新动画列表 + self.update_animation_list(actor_id) + + def update_animation_list(self, actor_id): + """ + 更新动画列表 + :param actor_id: Actor ID + """ + if not hasattr(self, 'actor_listbox') or not self.actor_listbox: + return + + try: + # 清除现有元素 + for element in self.animation_list_elements.values(): + self.world.gui_manager.deleteGUIElement(element) + self.animation_list_elements.clear() + + # 获取Actor动画列表 + animations = self.animation_manager.get_actor_animations(actor_id) + y_offset = -0.05 + y_spacing = 0.03 + + for i, anim_name in enumerate(animations): + # 创建动画列表项 + anim_button = self.world.gui_manager.createGUIButton( + parent=self.actor_listbox, + pos=(0.01, 0, y_offset - i * y_spacing), + size=0.04, + text=anim_name + ) + + # 注册点击事件 + self.world.accept(f"skeletal_select_anim_{actor_id}_{anim_name}", + lambda aid=actor_id, an=anim_name: self._on_animation_selected(aid, an)) + + self.animation_list_elements[f"{actor_id}_{anim_name}"] = anim_button + + except Exception as e: + print(f"更新动画列表失败: {e}") + import traceback + traceback.print_exc() + + def _on_animation_selected(self, actor_id, anim_name): + """ + 当动画被选中时的处理函数 + :param actor_id: Actor ID + :param anim_name: 动画名称 + """ + print(f"选中动画: {anim_name} (Actor: {actor_id})") + + def _on_play_clicked(self): + """ + 当播放按钮被点击时的处理函数 + """ + print("播放按钮被点击") + # 这里应该实现实际的播放逻辑 + + def _on_stop_clicked(self): + """ + 当停止按钮被点击时的处理函数 + """ + print("停止按钮被点击") + # 这里应该实现实际的停止逻辑 + + def _on_pause_clicked(self): + """ + 当暂停按钮被点击时的处理函数 + """ + print("暂停按钮被点击") + # 这里应该实现实际的暂停逻辑 + + def destroy(self): + """ + 销毁GUI界面 + """ + try: + if self.panel: + self.world.gui_manager.deleteGUIElement(self.panel) + self.panel = None + print("骨骼动画GUI界面已销毁") + except Exception as e: + print(f"销毁GUI界面失败: {e}") + + +class Plugin(BasePlugin): + """ + 骨骼动画系统插件 + 提供完整的骨骼动画支持,包括加载、播放、控制、混合等高级功能 + """ + + def __init__(self, plugin_manager, name): + super().__init__(plugin_manager, name) + self.config = { + "version": "1.0.0", + "author": "EG Team", + "description": "骨骼动画系统,支持模型动画加载、播放控制、混合等高级功能" + } + self.animation_manager = None + self.event_manager = None + self.gui = None + self.is_enabled = False + self.update_task = None + + def initialize(self) -> bool: + """ + 初始化插件 + """ + print(f"初始化骨骼动画插件: {self.name}") + + try: + # 创建动画管理器 + self.animation_manager = SkeletalAnimationManager(self.plugin_manager.world) + + # 创建事件管理器 + self.event_manager = AnimationEventManager(self.plugin_manager.world, self.animation_manager) + + # 创建GUI + self.gui = SkeletalAnimationGUI(self.plugin_manager.world, + self.animation_manager, + self.event_manager) + + print(f"骨骼动画插件初始化成功: {self.name}") + return True + + except Exception as e: + print(f"骨骼动画插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + """ + if not super().enable(): + return False + + if self.is_enabled: + print(f"骨骼动画插件已启用: {self.name}") + return True + + print(f"启用骨骼动画插件: {self.name}") + + try: + # 设置默认事件处理器 + self.event_manager.setup_default_events() + + # 创建GUI控制面板 + self.gui.create_control_panel() + + # 启动更新任务 + self.update_task = self.plugin_manager.world.taskMgr.add( + self._update_task, + f"skeletal_animation_update_{self.name}", + sort=30 + ) + + # 标记为已启用 + self.is_enabled = True + + print(f"骨骼动画插件启用成功: {self.name}") + return True + + except Exception as e: + print(f"骨骼动画插件启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self) -> bool: + """ + 禁用插件 + """ + if not super().disable(): + return False + + if not self.is_enabled: + print(f"骨骼动画插件已禁用: {self.name}") + return True + + print(f"禁用骨骼动画插件: {self.name}") + + try: + # 停止更新任务 + if self.update_task: + self.plugin_manager.world.taskMgr.remove(self.update_task) + self.update_task = None + + # 移除事件处理器 + self.plugin_manager.world.ignore("skeletal.load_actor") + self.plugin_manager.world.ignore("skeletal.play_animation") + self.plugin_manager.world.ignore("skeletal.stop_animation") + self.plugin_manager.world.ignore("skeletal.set_animation_speed") + self.plugin_manager.world.ignore("skeletal.pause_animation") + self.plugin_manager.world.ignore("skeletal.resume_animation") + + # 销毁GUI + self.gui.destroy() + + # 标记为已禁用 + self.is_enabled = False + + print(f"骨骼动画插件禁用成功: {self.name}") + return True + + except Exception as e: + print(f"骨骼动画插件禁用失败: {e}") + import traceback + traceback.print_exc() + return False + + def finalize(self): + """ + 清理插件资源 + """ + print(f"清理骨骼动画插件资源: {self.name}") + + try: + # 停止更新任务 + if self.update_task: + self.plugin_manager.world.taskMgr.remove(self.update_task) + self.update_task = None + + # 停止所有动画 + for actor_id in list(self.animation_manager.actors.keys()): + self.animation_manager.unload_actor(actor_id) + + # 清理事件处理器 + self.event_manager = None + + # 清理GUI + self.gui = None + + # 清理动画管理器 + self.animation_manager = None + + print(f"骨骼动画插件资源清理完成: {self.name}") + + except Exception as e: + print(f"清理骨骼动画插件资源失败: {e}") + import traceback + traceback.print_exc() + + def _update_task(self, task): + """ + 插件更新任务 + """ + try: + # 获取摄像机位置用于LOD计算 + if hasattr(self.plugin_manager.world, 'camera'): + camera_pos = self.plugin_manager.world.camera.getPos() + self.animation_manager.update_lod(camera_pos) + + # 更新高级动画系统 + dt = globalClock.getDt() + time = globalClock.getRealTime() + + for actor_id, advanced_manager in self.animation_manager.advanced_managers.items(): + advanced_manager.update(dt, time) + + except Exception as e: + print(f"插件更新任务出错: {e}") + + return task.cont + + def get_animation_manager(self): + """ + 获取动画管理器 + :return: SkeletalAnimationManager实例 + """ + return self.animation_manager + + def get_event_manager(self): + """ + 获取事件管理器 + :return: AnimationEventManager实例 + """ + return self.event_manager + + def get_gui(self): + """ + 获取GUI界面 + :return: SkeletalAnimationGUI实例 + """ return self.gui \ No newline at end of file diff --git a/plugins/user/soft_body_cloth_physics/README.md b/plugins/user/soft_body_cloth_physics/README.md index 24de9b86..9d99d3d1 100644 --- a/plugins/user/soft_body_cloth_physics/README.md +++ b/plugins/user/soft_body_cloth_physics/README.md @@ -1,228 +1,228 @@ -# 软体物理和布料模拟插件使用指南 - -## 简介 - -软体物理和布料模拟插件为iFlow引擎提供了真实的布料和软体物理模拟功能。该插件基于Bullet物理引擎的软体物理模块,支持多种布料材质和软体类型。 - -## 功能特性 - -1. **布料模拟**: - - 支持多种预定义布料材质(棉布、丝绸、皮革等) - - 可自定义布料物理参数 - - 支持固定点设置 - - 实时风力影响 - -2. **软体物理**: - - 支持多种软体材质(橡胶、果冻、肌肉组织等) - - 多种形状支持(球体、盒体、绳索等) - - 可自定义物理参数 - -3. **调试可视化**: - - 网格可视化 - - 固定点显示 - - 力和速度向量可视化 - -## 安装和启用 - -### 启用插件 - -在iFlow项目中启用插件: - -```python -# 在主程序中启用插件 -world = MyWorld() # 或您的世界对象 - -# 启用插件系统 -world.plugin_manager.enable_plugin_system() - -# 加载并启用软体物理和布料模拟插件 -cloth_plugin = world.plugin_manager.load_plugin("soft_body_cloth_physics") -if cloth_plugin: - world.plugin_manager.enable_plugin("soft_body_cloth_physics") -``` - -## 基本使用 - -### 创建布料对象 - -```python -# 获取布料管理器 -cloth_manager = world.plugins['soft_body_cloth_physics'].cloth_manager - -# 启用布料管理器 -cloth_manager.enable() - -# 创建布料片 -from panda3d.core import NodePath -parent_node = NodePath("cloth_parent") - -# 创建一个2x2米的棉布片,分辨率为10x10 -cloth_info = cloth_manager.create_cloth_patch( - parent_node, - width=2.0, - height=2.0, - resolution_x=10, - resolution_y=10, - params={'material': 'cotton'} -) - -# 添加固定点(固定四个角) -if cloth_info: - cloth_manager.add_anchor(cloth_info, 0) # 左下角 - cloth_manager.add_anchor(cloth_info, 9) # 右下角 - cloth_manager.add_anchor(cloth_info, 90) # 左上角 - cloth_manager.add_anchor(cloth_info, 99) # 右上角 -``` - -### 创建软体对象 - -```python -# 获取软体管理器 -soft_manager = world.plugins['soft_body_cloth_physics'].soft_body_manager - -# 启用软体管理器 -soft_manager.enable() - -# 创建球形软体 -from panda3d.core import Point3 -parent_node = NodePath("soft_body_parent") - -# 创建一个橡胶球 -soft_info = soft_manager.create_soft_body_sphere( - parent_node, - radius=0.5, - resolution=8, - params={'material': 'rubber'} -) - -# 创建绳索软体 -rope_info = soft_manager.create_soft_body_rope( - parent_node, - point_a=Point3(0, 0, 5), - point_b=Point3(0, 0, 0), - resolution=20, - params={'material': 'rubber'} -) -``` - -### 使用预定义材质 - -```python -# 获取材质管理器 -from plugins.user.soft_body_cloth_physics.materials.cloth_materials import ClothMaterialManager -material_manager = ClothMaterialManager() - -# 获取棉布材质参数 -cotton_params = material_manager.get_material("cotton").get_physical_parameters() - -# 创建使用特定材质的布料 -cloth_info = cloth_manager.create_cloth_patch( - parent_node, - width=2.0, - height=2.0, - resolution_x=10, - resolution_y=10, - params=cotton_params -) -``` - -### 调试可视化 - -```python -# 启用调试可视化 -debug_manager = world.plugins['soft_body_cloth_physics'].debug_manager -debug_manager.enable() - -# 设置可视化选项 -debug_manager.cloth_visualizer.set_visibility_flags( - mesh=True, # 显示网格 - anchors=True, # 显示固定点 - forces=False, # 不显示力 - velocities=False # 不显示速度 -) -``` - -## 高级功能 - -### 自定义材质 - -```python -# 创建自定义布料材质 -custom_material = material_manager.create_custom_material( - "CustomElastic", - "custom", - physical_params={ - 'density': 0.4, - 'linear_stiffness': 0.9, - 'angular_stiffness': 0.8, - 'volume_stiffness': 0.7, - 'friction': 0.5, - 'restitution': 0.3 - }, - visual_params={ - 'color': Vec4(1.0, 0.5, 0.5, 1.0), - 'roughness': 0.6 - } -) - -# 注册自定义材质 -material_manager.register_material(custom_material) -``` - -### 实时参数调整 - -```python -# 动态调整布料参数 -if cloth_info: - cloth_manager.update_cloth_params(cloth_info, { - 'linear_stiffness': 0.9, # 增加线性刚度 - 'friction': 0.8 # 增加摩擦力 - }) -``` - -### 应用外力 - -```python -# 对软体应用力 -from panda3d.core import Vec3 -if soft_info: - soft_manager.apply_force_to_soft_body( - soft_info['soft_body'], - Vec3(0, 0, 100) # 向上的力 - ) -``` - -## 配置文件 - -插件的配置存储在 `config/cloth_settings.json` 文件中,可以调整以下参数: - -- 物理参数(重力、空气密度等) -- 默认布料和软体参数 -- 调试可视化选项 - -## 性能优化建议 - -1. **降低分辨率**:对于远距离或不重要的布料,使用较低的分辨率 -2. **限制对象数量**:软体物理计算开销较大,避免场景中同时存在过多软体对象 -3. **使用LOD**:根据距离动态调整布料分辨率 -4. **合理设置参数**:避免过于 stiff 的参数设置,可能导致仿真不稳定 - -## 故障排除 - -### 常见问题 - -1. **布料穿模**: - - 增加布料的体积保持系数 - - 调整时间步长参数 - -2. **性能问题**: - - 降低布料分辨率 - - 减少同时模拟的软体对象数量 - -3. **对象不响应物理**: - - 确保插件已正确启用 - - 检查物理世界是否正常工作 - -### API参考 - +# 软体物理和布料模拟插件使用指南 + +## 简介 + +软体物理和布料模拟插件为iFlow引擎提供了真实的布料和软体物理模拟功能。该插件基于Bullet物理引擎的软体物理模块,支持多种布料材质和软体类型。 + +## 功能特性 + +1. **布料模拟**: + - 支持多种预定义布料材质(棉布、丝绸、皮革等) + - 可自定义布料物理参数 + - 支持固定点设置 + - 实时风力影响 + +2. **软体物理**: + - 支持多种软体材质(橡胶、果冻、肌肉组织等) + - 多种形状支持(球体、盒体、绳索等) + - 可自定义物理参数 + +3. **调试可视化**: + - 网格可视化 + - 固定点显示 + - 力和速度向量可视化 + +## 安装和启用 + +### 启用插件 + +在iFlow项目中启用插件: + +```python +# 在主程序中启用插件 +world = MyWorld() # 或您的世界对象 + +# 启用插件系统 +world.plugin_manager.enable_plugin_system() + +# 加载并启用软体物理和布料模拟插件 +cloth_plugin = world.plugin_manager.load_plugin("soft_body_cloth_physics") +if cloth_plugin: + world.plugin_manager.enable_plugin("soft_body_cloth_physics") +``` + +## 基本使用 + +### 创建布料对象 + +```python +# 获取布料管理器 +cloth_manager = world.plugins['soft_body_cloth_physics'].cloth_manager + +# 启用布料管理器 +cloth_manager.enable() + +# 创建布料片 +from panda3d.core import NodePath +parent_node = NodePath("cloth_parent") + +# 创建一个2x2米的棉布片,分辨率为10x10 +cloth_info = cloth_manager.create_cloth_patch( + parent_node, + width=2.0, + height=2.0, + resolution_x=10, + resolution_y=10, + params={'material': 'cotton'} +) + +# 添加固定点(固定四个角) +if cloth_info: + cloth_manager.add_anchor(cloth_info, 0) # 左下角 + cloth_manager.add_anchor(cloth_info, 9) # 右下角 + cloth_manager.add_anchor(cloth_info, 90) # 左上角 + cloth_manager.add_anchor(cloth_info, 99) # 右上角 +``` + +### 创建软体对象 + +```python +# 获取软体管理器 +soft_manager = world.plugins['soft_body_cloth_physics'].soft_body_manager + +# 启用软体管理器 +soft_manager.enable() + +# 创建球形软体 +from panda3d.core import Point3 +parent_node = NodePath("soft_body_parent") + +# 创建一个橡胶球 +soft_info = soft_manager.create_soft_body_sphere( + parent_node, + radius=0.5, + resolution=8, + params={'material': 'rubber'} +) + +# 创建绳索软体 +rope_info = soft_manager.create_soft_body_rope( + parent_node, + point_a=Point3(0, 0, 5), + point_b=Point3(0, 0, 0), + resolution=20, + params={'material': 'rubber'} +) +``` + +### 使用预定义材质 + +```python +# 获取材质管理器 +from plugins.user.soft_body_cloth_physics.materials.cloth_materials import ClothMaterialManager +material_manager = ClothMaterialManager() + +# 获取棉布材质参数 +cotton_params = material_manager.get_material("cotton").get_physical_parameters() + +# 创建使用特定材质的布料 +cloth_info = cloth_manager.create_cloth_patch( + parent_node, + width=2.0, + height=2.0, + resolution_x=10, + resolution_y=10, + params=cotton_params +) +``` + +### 调试可视化 + +```python +# 启用调试可视化 +debug_manager = world.plugins['soft_body_cloth_physics'].debug_manager +debug_manager.enable() + +# 设置可视化选项 +debug_manager.cloth_visualizer.set_visibility_flags( + mesh=True, # 显示网格 + anchors=True, # 显示固定点 + forces=False, # 不显示力 + velocities=False # 不显示速度 +) +``` + +## 高级功能 + +### 自定义材质 + +```python +# 创建自定义布料材质 +custom_material = material_manager.create_custom_material( + "CustomElastic", + "custom", + physical_params={ + 'density': 0.4, + 'linear_stiffness': 0.9, + 'angular_stiffness': 0.8, + 'volume_stiffness': 0.7, + 'friction': 0.5, + 'restitution': 0.3 + }, + visual_params={ + 'color': Vec4(1.0, 0.5, 0.5, 1.0), + 'roughness': 0.6 + } +) + +# 注册自定义材质 +material_manager.register_material(custom_material) +``` + +### 实时参数调整 + +```python +# 动态调整布料参数 +if cloth_info: + cloth_manager.update_cloth_params(cloth_info, { + 'linear_stiffness': 0.9, # 增加线性刚度 + 'friction': 0.8 # 增加摩擦力 + }) +``` + +### 应用外力 + +```python +# 对软体应用力 +from panda3d.core import Vec3 +if soft_info: + soft_manager.apply_force_to_soft_body( + soft_info['soft_body'], + Vec3(0, 0, 100) # 向上的力 + ) +``` + +## 配置文件 + +插件的配置存储在 `config/cloth_settings.json` 文件中,可以调整以下参数: + +- 物理参数(重力、空气密度等) +- 默认布料和软体参数 +- 调试可视化选项 + +## 性能优化建议 + +1. **降低分辨率**:对于远距离或不重要的布料,使用较低的分辨率 +2. **限制对象数量**:软体物理计算开销较大,避免场景中同时存在过多软体对象 +3. **使用LOD**:根据距离动态调整布料分辨率 +4. **合理设置参数**:避免过于 stiff 的参数设置,可能导致仿真不稳定 + +## 故障排除 + +### 常见问题 + +1. **布料穿模**: + - 增加布料的体积保持系数 + - 调整时间步长参数 + +2. **性能问题**: + - 降低布料分辨率 + - 减少同时模拟的软体对象数量 + +3. **对象不响应物理**: + - 确保插件已正确启用 + - 检查物理世界是否正常工作 + +### API参考 + 详细API文档请参考插件源代码中的类和方法注释。 \ No newline at end of file diff --git a/plugins/user/soft_body_cloth_physics/__init__.py b/plugins/user/soft_body_cloth_physics/__init__.py index 32e16114..99cfc955 100644 --- a/plugins/user/soft_body_cloth_physics/__init__.py +++ b/plugins/user/soft_body_cloth_physics/__init__.py @@ -1,24 +1,24 @@ -""" -软体物理和布料模拟插件初始化文件 -""" - -# 导入主要的类和函数 -from .plugin import Plugin -from .core.cloth_manager import ClothManager -from .core.soft_body_manager import SoftBodyManager -from .core.cloth_physics import ClothPhysics -from .core.soft_body_physics import SoftBodyPhysics -from .materials.cloth_materials import ClothMaterialManager -from .utils.mesh_generator import MeshGenerator -from .debug.cloth_debug import PhysicsDebugManager - -__all__ = [ - 'Plugin', - 'ClothManager', - 'SoftBodyManager', - 'ClothPhysics', - 'SoftBodyPhysics', - 'ClothMaterialManager', - 'MeshGenerator', - 'PhysicsDebugManager' +""" +软体物理和布料模拟插件初始化文件 +""" + +# 导入主要的类和函数 +from .plugin import Plugin +from .core.cloth_manager import ClothManager +from .core.soft_body_manager import SoftBodyManager +from .core.cloth_physics import ClothPhysics +from .core.soft_body_physics import SoftBodyPhysics +from .materials.cloth_materials import ClothMaterialManager +from .utils.mesh_generator import MeshGenerator +from .debug.cloth_debug import PhysicsDebugManager + +__all__ = [ + 'Plugin', + 'ClothManager', + 'SoftBodyManager', + 'ClothPhysics', + 'SoftBodyPhysics', + 'ClothMaterialManager', + 'MeshGenerator', + 'PhysicsDebugManager' ] \ No newline at end of file diff --git a/plugins/user/soft_body_cloth_physics/config/cloth_settings.json b/plugins/user/soft_body_cloth_physics/config/cloth_settings.json index e6eaebac..e29c0f4a 100644 --- a/plugins/user/soft_body_cloth_physics/config/cloth_settings.json +++ b/plugins/user/soft_body_cloth_physics/config/cloth_settings.json @@ -1,94 +1,94 @@ -{ - "plugin": { - "name": "SoftBodyClothPhysics", - "version": "1.0.0", - "author": "iFlow Team", - "description": "软体物理和布料模拟插件" - }, - "physics": { - "gravity": [0, 0, -9.81], - "air_density": 1.225, - "water_density": 1000.0, - "default_cloth_density": 0.5, - "default_soft_body_density": 1.0 - }, - "cloth": { - "default_resolution_x": 10, - "default_resolution_y": 10, - "default_width": 2.0, - "default_height": 2.0, - "materials": { - "cotton": { - "density": 0.6, - "linear_stiffness": 0.7, - "angular_stiffness": 0.6, - "volume_stiffness": 0.4, - "friction": 0.6, - "restitution": 0.1, - "pressure_coefficient": 0.0, - "drag_coefficient": 0.02, - "lift_coefficient": 0.01 - }, - "silk": { - "density": 0.3, - "linear_stiffness": 0.5, - "angular_stiffness": 0.4, - "volume_stiffness": 0.2, - "friction": 0.2, - "restitution": 0.3, - "pressure_coefficient": 0.0, - "drag_coefficient": 0.05, - "lift_coefficient": 0.03 - }, - "leather": { - "density": 0.8, - "linear_stiffness": 0.9, - "angular_stiffness": 0.8, - "volume_stiffness": 0.6, - "friction": 0.7, - "restitution": 0.1, - "pressure_coefficient": 0.0, - "drag_coefficient": 0.01, - "lift_coefficient": 0.005 - } - } - }, - "soft_body": { - "default_resolution": 8, - "default_radius": 1.0, - "materials": { - "rubber": { - "density": 1.2, - "linear_stiffness": 0.9, - "angular_stiffness": 0.8, - "volume_stiffness": 0.7, - "friction": 0.8, - "restitution": 0.6, - "pressure_coefficient": 0.0, - "drag_coefficient": 0.01, - "lift_coefficient": 0.005 - }, - "jelly": { - "density": 1.1, - "linear_stiffness": 0.6, - "angular_stiffness": 0.5, - "volume_stiffness": 0.8, - "friction": 0.7, - "restitution": 0.2, - "pressure_coefficient": 0.0, - "drag_coefficient": 0.02, - "lift_coefficient": 0.01 - } - } - }, - "debug": { - "enable_visualization": false, - "show_cloth_mesh": true, - "show_cloth_anchors": true, - "show_cloth_forces": false, - "show_cloth_velocities": false, - "show_soft_body_mesh": true, - "show_soft_body_forces": false, - "show_soft_body_velocities": false - } +{ + "plugin": { + "name": "SoftBodyClothPhysics", + "version": "1.0.0", + "author": "iFlow Team", + "description": "软体物理和布料模拟插件" + }, + "physics": { + "gravity": [0, 0, -9.81], + "air_density": 1.225, + "water_density": 1000.0, + "default_cloth_density": 0.5, + "default_soft_body_density": 1.0 + }, + "cloth": { + "default_resolution_x": 10, + "default_resolution_y": 10, + "default_width": 2.0, + "default_height": 2.0, + "materials": { + "cotton": { + "density": 0.6, + "linear_stiffness": 0.7, + "angular_stiffness": 0.6, + "volume_stiffness": 0.4, + "friction": 0.6, + "restitution": 0.1, + "pressure_coefficient": 0.0, + "drag_coefficient": 0.02, + "lift_coefficient": 0.01 + }, + "silk": { + "density": 0.3, + "linear_stiffness": 0.5, + "angular_stiffness": 0.4, + "volume_stiffness": 0.2, + "friction": 0.2, + "restitution": 0.3, + "pressure_coefficient": 0.0, + "drag_coefficient": 0.05, + "lift_coefficient": 0.03 + }, + "leather": { + "density": 0.8, + "linear_stiffness": 0.9, + "angular_stiffness": 0.8, + "volume_stiffness": 0.6, + "friction": 0.7, + "restitution": 0.1, + "pressure_coefficient": 0.0, + "drag_coefficient": 0.01, + "lift_coefficient": 0.005 + } + } + }, + "soft_body": { + "default_resolution": 8, + "default_radius": 1.0, + "materials": { + "rubber": { + "density": 1.2, + "linear_stiffness": 0.9, + "angular_stiffness": 0.8, + "volume_stiffness": 0.7, + "friction": 0.8, + "restitution": 0.6, + "pressure_coefficient": 0.0, + "drag_coefficient": 0.01, + "lift_coefficient": 0.005 + }, + "jelly": { + "density": 1.1, + "linear_stiffness": 0.6, + "angular_stiffness": 0.5, + "volume_stiffness": 0.8, + "friction": 0.7, + "restitution": 0.2, + "pressure_coefficient": 0.0, + "drag_coefficient": 0.02, + "lift_coefficient": 0.01 + } + } + }, + "debug": { + "enable_visualization": false, + "show_cloth_mesh": true, + "show_cloth_anchors": true, + "show_cloth_forces": false, + "show_cloth_velocities": false, + "show_soft_body_mesh": true, + "show_soft_body_forces": false, + "show_soft_body_velocities": false + } } \ No newline at end of file diff --git a/plugins/user/soft_body_cloth_physics/core/cloth_manager.py b/plugins/user/soft_body_cloth_physics/core/cloth_manager.py index b347f046..c905da0f 100644 --- a/plugins/user/soft_body_cloth_physics/core/cloth_manager.py +++ b/plugins/user/soft_body_cloth_physics/core/cloth_manager.py @@ -1,882 +1,882 @@ -""" -布料管理器 -负责布料对象的创建、管理和更新 -包含完整的布料物理模拟功能 -""" - -from panda3d.core import Vec3, Point3, GeomVertexReader, GeomVertexFormat -from panda3d.core import GeomVertexData, Geom, GeomNode, NodePath -from panda3d.bullet import BulletSoftBodyNode, BulletSoftBodyWorldInfo -import math -from typing import List, Dict, Optional, Any, Tuple -import numpy as np - - -class ClothManager: - """ - 布料管理器 - 管理场景中的所有布料对象,提供完整的布料物理模拟功能 - """ - - # 布料类型常量 - CLOTH_TYPE_PLANE = "plane" - CLOTH_TYPE_PATCH = "patch" - CLOTH_TYPE_TUBE = "tube" - CLOTH_TYPE_ROPE = "rope" - - # 撕裂模式 - TEAR_MODE_NONE = "none" - TEAR_MODE_VERTEX = "vertex" - TEAR_MODE_EDGE = "edge" - - def __init__(self, world): - self.world = world - self.cloth_objects: List[Dict[str, Any]] = [] - self.enabled = False - self.gravity = Vec3(0, 0, -9.81) - - # 布料默认参数 - self.default_params = { - 'density': 0.5, - 'linear_stiffness': 0.8, - 'angular_stiffness': 0.8, - 'volume_stiffness': 0.5, - 'friction': 0.5, - 'restitution': 0.2, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.01, - 'lift_coefficient': 0.01, - 'anchored': False, - 'tear_mode': 'none', - 'tear_threshold': 100.0, - 'tear_resistance': 50.0, - 'bending_constraints': True, - 'clusters': 0, - 'cluster_hardness': 1.0, - 'position_iteration': 2, - 'drift_iteration': 2, - 'cluster_iteration': 2, - 'velocity_iteration': 2 - } - - # 风力参数 - self.wind_enabled = False - self.wind_velocity = Vec3(0, 0, 0) - self.wind_turbulence = 0.0 - self.wind_frequency = 1.0 - - # 性能参数 - self.max_cloth_objects = 100 - self.cloth_update_frequency = 60 # Hz - - def enable(self): - """ - 启用布料管理器 - """ - self.enabled = True - print("✓ 布料管理器已启用") - - def disable(self): - """ - 禁用布料管理器 - """ - self.enabled = False - print("✓ 布料管理器已禁用") - - def set_gravity(self, gravity: Vec3): - """ - 设置重力 - - Args: - gravity: 重力向量 - """ - self.gravity = gravity - - def set_wind_parameters(self, enabled: bool = True, velocity: Vec3 = None, - turbulence: float = 0.0, frequency: float = 1.0): - """ - 设置风力参数 - - Args: - enabled: 是否启用风力 - velocity: 风速向量 - turbulence: 湍流强度 - frequency: 风力变化频率 - """ - self.wind_enabled = enabled - if velocity: - self.wind_velocity = velocity - self.wind_turbulence = turbulence - self.wind_frequency = frequency - - def create_cloth_from_mesh(self, node_path, params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: - """ - 从网格创建布料对象 - - Args: - node_path: 网格节点路径 - params: 布料参数 - - Returns: - 布料对象信息字典 - """ - if not self.enabled: - print("⚠ 布料管理器未启用") - return None - - if len(self.cloth_objects) >= self.max_cloth_objects: - print("⚠ 达到最大布料对象数量限制") - return None - - try: - # 合并参数 - cloth_params = self.default_params.copy() - if params: - cloth_params.update(params) - - # 创建软体节点 - cloth_name = f'cloth_{len(self.cloth_objects)}' - soft_body_node = BulletSoftBodyNode(cloth_name) - - # 设置布料参数 - self._apply_cloth_parameters(soft_body_node, cloth_params) - - # 从网格创建布料 - vertices, indices = self._extract_mesh_data(node_path) - if not vertices or not indices: - print("⚠ 无法从网格提取顶点数据") - return None - - # 添加顶点 - for vertex in vertices: - soft_body_node.appendNode(vertex, 1.0) - - # 添加面 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) - - # 添加链接(边) - self._add_links_from_indices(soft_body_node, indices) - - # 添加弯曲约束 - if cloth_params.get('bending_constraints', True): - self._add_bending_constraints(soft_body_node) - - # 创建布料节点路径 - cloth_np = node_path.attachNewNode(soft_body_node) - cloth_np.setPos(node_path.getPos()) - cloth_np.setHpr(node_path.getHpr()) - - # 添加到物理世界 - physics_world = self._get_physics_world() - if physics_world: - physics_world.attachSoftBody(soft_body_node) - - # 保存布料信息 - cloth_info = { - 'node_path': cloth_np, - 'soft_body': soft_body_node, - 'original_node': node_path, - 'params': cloth_params, - 'type': 'cloth', - 'anchors': [], # 固定点列表 - 'tears': [], # 撕裂记录 - 'vertices': vertices, - 'indices': indices, - 'creation_time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0, - 'last_update_time': 0.0, - 'performance_stats': { - 'update_count': 0, - 'tear_count': 0, - 'collision_count': 0 - } - } - - self.cloth_objects.append(cloth_info) - - print(f"✓ 创建布料对象: {cloth_name} (顶点数: {len(vertices)}, 面数: {len(indices)//3})") - return cloth_info - - except Exception as e: - print(f"✗ 创建布料对象失败: {e}") - import traceback - traceback.print_exc() - return None - - def create_cloth_patch(self, node_path, width: float, height: float, - resolution_x: int, resolution_y: int, - params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: - """ - 创建布料片 - - Args: - node_path: 父节点路径 - width: 布料宽度 - height: 布料高度 - resolution_x: X方向分辨率 - resolution_y: Y方向分辨率 - params: 布料参数 - - Returns: - 布料对象信息字典 - """ - if not self.enabled: - print("⚠ 布料管理器未启用") - return None - - if len(self.cloth_objects) >= self.max_cloth_objects: - print("⚠ 达到最大布料对象数量限制") - return None - - try: - # 合并参数 - cloth_params = self.default_params.copy() - if params: - cloth_params.update(params) - - # 创建软体节点 - cloth_name = f'cloth_patch_{len(self.cloth_objects)}' - soft_body_node = BulletSoftBodyNode(cloth_name) - - # 设置布料参数 - self._apply_cloth_parameters(soft_body_node, cloth_params) - - # 生成布料片顶点 - vertices = [] - indices = [] - half_width = width / 2.0 - half_height = height / 2.0 - - # 生成顶点 - for j in range(resolution_y + 1): - for i in range(resolution_x + 1): - x = (i / resolution_x) * width - half_width - y = (j / resolution_y) * height - half_height - z = 0.0 - vertices.append(Point3(x, y, z)) - - # 生成索引 - for j in range(resolution_y): - for i in range(resolution_x): - v0 = j * (resolution_x + 1) + i - v1 = v0 + 1 - v2 = (j + 1) * (resolution_x + 1) + i - v3 = v2 + 1 - - indices.extend([v0, v1, v2, v1, v3, v2]) - - # 添加顶点到软体 - for vertex in vertices: - soft_body_node.appendNode(vertex, 1.0) - - # 添加面 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) - - # 添加链接 - self._add_links_from_indices(soft_body_node, indices) - - # 添加弯曲约束 - if cloth_params.get('bending_constraints', True): - self._add_bending_constraints(soft_body_node) - - # 创建布料节点路径 - cloth_np = node_path.attachNewNode(soft_body_node) - - # 添加到物理世界 - physics_world = self._get_physics_world() - if physics_world: - physics_world.attachSoftBody(soft_body_node) - - # 保存布料信息 - cloth_info = { - 'node_path': cloth_np, - 'soft_body': soft_body_node, - 'original_node': node_path, - 'params': cloth_params, - 'type': 'cloth_patch', - 'width': width, - 'height': height, - 'resolution_x': resolution_x, - 'resolution_y': resolution_y, - 'anchors': [], - 'tears': [], - 'vertices': vertices, - 'indices': indices, - 'creation_time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0, - 'last_update_time': 0.0, - 'performance_stats': { - 'update_count': 0, - 'tear_count': 0, - 'collision_count': 0 - } - } - - self.cloth_objects.append(cloth_info) - - print(f"✓ 创建布料片: {cloth_name} ({resolution_x+1}x{resolution_y+1} 顶点)") - return cloth_info - - except Exception as e: - print(f"✗ 创建布料片失败: {e}") - import traceback - traceback.print_exc() - return None - - def create_cloth_tube(self, node_path, radius: float, height: float, - resolution: int = 16, - params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: - """ - 创建管状布料 - - Args: - node_path: 父节点路径 - radius: 管道半径 - height: 管道高度 - resolution: 分辨率 - params: 布料参数 - - Returns: - 布料对象信息字典 - """ - if not self.enabled: - print("⚠ 布料管理器未启用") - return None - - if len(self.cloth_objects) >= self.max_cloth_objects: - print("⚠ 达到最大布料对象数量限制") - return None - - try: - # 合并参数 - cloth_params = self.default_params.copy() - if params: - cloth_params.update(params) - - # 创建软体节点 - cloth_name = f'cloth_tube_{len(self.cloth_objects)}' - soft_body_node = BulletSoftBodyNode(cloth_name) - - # 设置布料参数 - self._apply_cloth_parameters(soft_body_node, cloth_params) - - # 生成管状布料顶点 - vertices = [] - indices = [] - half_height = height / 2.0 - - # 生成顶点 - for j in range(2): # 顶部和底部 - z = half_height if j == 0 else -half_height - for i in range(resolution): - angle = (i / resolution) * 2 * math.pi - x = math.cos(angle) * radius - y = math.sin(angle) * radius - vertices.append(Point3(x, y, z)) - - # 生成侧面索引 - for i in range(resolution): - next_i = (i + 1) % resolution - # 上面 - indices.extend([i, next_i, i + resolution]) - indices.extend([next_i, next_i + resolution, i + resolution]) - - # 添加顶点到软体 - for vertex in vertices: - soft_body_node.appendNode(vertex, 1.0) - - # 添加面 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) - - # 添加链接 - self._add_links_from_indices(soft_body_node, indices) - - # 添加弯曲约束 - if cloth_params.get('bending_constraints', True): - self._add_bending_constraints(soft_body_node) - - # 创建布料节点路径 - cloth_np = node_path.attachNewNode(soft_body_node) - - # 添加到物理世界 - physics_world = self._get_physics_world() - if physics_world: - physics_world.attachSoftBody(soft_body_node) - - # 保存布料信息 - cloth_info = { - 'node_path': cloth_np, - 'soft_body': soft_body_node, - 'original_node': node_path, - 'params': cloth_params, - 'type': 'cloth_tube', - 'radius': radius, - 'height': height, - 'resolution': resolution, - 'anchors': [], - 'tears': [], - 'vertices': vertices, - 'indices': indices, - 'creation_time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0, - 'last_update_time': 0.0, - 'performance_stats': { - 'update_count': 0, - 'tear_count': 0, - 'collision_count': 0 - } - } - - self.cloth_objects.append(cloth_info) - - print(f"✓ 创建管状布料: {cloth_name} (半径: {radius}, 高度: {height})") - return cloth_info - - except Exception as e: - print(f"✗ 创建管状布料失败: {e}") - import traceback - traceback.print_exc() - return None - - def add_anchor(self, cloth_info: Dict[str, Any], vertex_index: int, - anchor_node=None, disable_collision: bool = True) -> bool: - """ - 为布料添加固定点 - - Args: - cloth_info: 布料对象信息 - vertex_index: 顶点索引 - anchor_node: 固定点节点(如果为None,则固定在当前位置) - disable_collision: 是否禁用与固定点的碰撞 - - Returns: - 是否成功添加固定点 - """ - try: - soft_body = cloth_info['soft_body'] - - if anchor_node: - # 固定到指定节点 - soft_body.appendAnchor(vertex_index, anchor_node, disable_collision) - else: - # 固定在当前位置 - soft_body.appendAnchor(vertex_index, None, disable_collision) - - # 记录固定点 - cloth_info['anchors'].append({ - 'vertex_index': vertex_index, - 'anchor_node': anchor_node, - 'disable_collision': disable_collision, - 'position': soft_body.get_nodes()[vertex_index].get_position() - }) - - print(f"✓ 为布料添加固定点: 顶点 {vertex_index}") - return True - - except Exception as e: - print(f"✗ 添加固定点失败: {e}") - return False - - def remove_anchor(self, cloth_info: Dict[str, Any], vertex_index: int) -> bool: - """ - 移除布料固定点 - - Args: - cloth_info: 布料对象信息 - vertex_index: 顶点索引 - - Returns: - 是否成功移除固定点 - """ - try: - anchors_to_remove = [] - for i, anchor in enumerate(cloth_info['anchors']): - if anchor['vertex_index'] == vertex_index: - anchors_to_remove.append(i) - - # 从后往前删除,避免索引变化 - for i in reversed(anchors_to_remove): - cloth_info['anchors'].pop(i) - - print(f"✓ 移除布料固定点: 顶点 {vertex_index}") - return True - - except Exception as e: - print(f"✗ 移除固定点失败: {e}") - return False - - def tear_cloth_at_point(self, cloth_info: Dict[str, Any], point: Point3, - radius: float = 0.5) -> bool: - """ - 在指定点撕裂布料 - - Args: - cloth_info: 布料对象信息 - point: 撕裂点 - radius: 撕裂半径 - - Returns: - 是否成功撕裂 - """ - try: - soft_body = cloth_info['soft_body'] - nodes = soft_body.get_nodes() - - # 查找附近的顶点 - tear_indices = [] - for i, node in enumerate(nodes): - node_pos = node.get_position() - distance = (node_pos - point).length() - if distance <= radius: - tear_indices.append(i) - - if not tear_indices: - print("⚠ 没有找到可撕裂的顶点") - return False - - # 执行撕裂 - tear_count = 0 - for index in tear_indices: - # 在实际实现中,这里会执行撕裂操作 - # BulletSoftBodyNode目前不直接支持撕裂,需要通过其他方式实现 - tear_count += 1 - - # 记录撕裂 - cloth_info['tears'].append({ - 'point': point, - 'radius': radius, - 'indices': tear_indices, - 'time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0 - }) - - cloth_info['performance_stats']['tear_count'] += tear_count - print(f"✓ 撕裂布料: {tear_count} 个顶点") - return True - - except Exception as e: - print(f"✗ 撕裂布料失败: {e}") - return False - - def remove_cloth(self, cloth_info: Dict[str, Any]) -> bool: - """ - 移除布料对象 - - Args: - cloth_info: 布料对象信息 - - Returns: - 是否成功移除 - """ - try: - if cloth_info not in self.cloth_objects: - print("⚠ 布料对象不存在") - return False - - # 从物理世界移除 - physics_world = self._get_physics_world() - if physics_world: - physics_world.removeSoftBody(cloth_info['soft_body']) - - # 移除节点 - if cloth_info['node_path'] and not cloth_info['node_path'].isEmpty(): - cloth_info['node_path'].removeNode() - - # 从列表移除 - self.cloth_objects.remove(cloth_info) - - print("✓ 移除布料对象") - return True - - except Exception as e: - print(f"✗ 移除布料对象失败: {e}") - return False - - def update_cloth_params(self, cloth_info: Dict[str, Any], params: Dict[str, Any]) -> bool: - """ - 更新布料参数 - - Args: - cloth_info: 布料对象信息 - params: 新参数 - - Returns: - 是否成功更新 - """ - try: - # 更新参数 - cloth_info['params'].update(params) - - # 应用参数到软体节点 - soft_body = cloth_info['soft_body'] - self._apply_cloth_parameters(soft_body, cloth_info['params']) - - print("✓ 更新布料参数") - return True - - except Exception as e: - print(f"✗ 更新布料参数失败: {e}") - return False - - def update_all_cloths(self, dt: float): - """ - 更新所有布料对象 - - Args: - dt: 时间增量 - """ - if not self.enabled: - return - - current_time = self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0 - - for cloth_info in self.cloth_objects: - # 检查更新频率 - if current_time - cloth_info['last_update_time'] < 1.0 / self.cloth_update_frequency: - continue - - # 更新性能统计 - cloth_info['performance_stats']['update_count'] += 1 - cloth_info['last_update_time'] = current_time - - # 应用风力 - if self.wind_enabled: - self._apply_wind_force(cloth_info) - - # 检查撕裂 - self._check_tear_conditions(cloth_info) - - def get_cloth_stats(self) -> Dict[str, Any]: - """ - 获取布料统计信息 - - Returns: - 统计信息字典 - """ - total_vertices = 0 - total_faces = 0 - total_anchors = 0 - total_tears = 0 - - for cloth_info in self.cloth_objects: - total_vertices += len(cloth_info.get('vertices', [])) - total_faces += len(cloth_info.get('indices', [])) // 3 - total_anchors += len(cloth_info.get('anchors', [])) - total_tears += cloth_info['performance_stats'].get('tear_count', 0) - - return { - 'total_cloth_objects': len(self.cloth_objects), - 'total_vertices': total_vertices, - 'total_faces': total_faces, - 'total_anchors': total_anchors, - 'total_tears': total_tears, - 'enabled': self.enabled, - 'wind_enabled': self.wind_enabled - } - - def cleanup(self): - """ - 清理所有布料对象 - """ - for cloth_info in self.cloth_objects[:]: - self.remove_cloth(cloth_info) - self.cloth_objects.clear() - print("✓ 布料管理器已清理") - - def _apply_cloth_parameters(self, soft_body: BulletSoftBodyNode, params: Dict[str, Any]): - """ - 应用布料参数到软体节点 - - Args: - soft_body: 软体节点 - params: 参数字典 - """ - cfg = soft_body.get_cfg() - - # 物理参数 - if 'linear_stiffness' in params: - cfg.set_kDF(params['linear_stiffness']) - if 'angular_stiffness' in params: - cfg.set_kDG(params['angular_stiffness']) - if 'volume_stiffness' in params: - cfg.set_kVC(params['volume_stiffness']) - if 'pressure_coefficient' in params: - cfg.set_kPR(params['pressure_coefficient']) - if 'drag_coefficient' in params: - cfg.set_kDG(params['drag_coefficient']) - if 'lift_coefficient' in params: - cfg.set_kLF(params['lift_coefficient']) - if 'cluster_hardness' in params: - cfg.set_kCHR(params['cluster_hardness']) - if 'kinetic_hardness' in params: - cfg.set_kKHR(params['kinetic_hardness']) - if 'soft_hardness' in params: - cfg.set_kSHR(params['soft_hardness']) - if 'anchor_hardness' in params: - cfg.set_kAHR(params['anchor_hardness']) - - # 迭代次数 - if 'position_iteration' in params: - cfg.set_piterations(params['position_iteration']) - if 'drift_iteration' in params: - cfg.set_diterations(params['drift_iteration']) - if 'cluster_iteration' in params: - cfg.set_citerations(params['cluster_iteration']) - if 'velocity_iteration' in params: - cfg.set_viterations(params['velocity_iteration']) - - # 材质属性 - if 'friction' in params: - soft_body.setFriction(params['friction']) - if 'restitution' in params: - soft_body.setRestitution(params['restitution']) - if 'density' in params: - soft_body.setTotalMass(params['density']) - - # 集群 - if 'clusters' in params and params['clusters'] > 0: - soft_body.generateClusters(params['clusters']) - - def _extract_mesh_data(self, node_path) -> Tuple[List[Point3], List[int]]: - """ - 从节点路径提取网格数据 - - Args: - node_path: 节点路径 - - Returns: - (顶点列表, 索引列表) - """ - try: - vertices = [] - indices = [] - - # 查找几何节点 - geom_nodes = node_path.findAllMatches('**/+GeomNode') - for geom_node_path in geom_nodes: - geom_node = geom_node_path.node() - for i in range(geom_node.getNumGeoms()): - geom = geom_node.getGeom(i) - vdata = geom.getVertexData() - - # 提取顶点 - vertex_reader = GeomVertexReader(vdata, 'vertex') - while not vertex_reader.isAtEnd(): - vertices.append(vertex_reader.getData3()) - - # 提取索引 - for prim in geom.getPrimitives(): - index_reader = prim.getIndexIterator() - while not index_reader.isAtEnd(): - indices.append(index_reader.getData()) - index_reader.next() - - return vertices, indices - - except Exception as e: - print(f"⚠ 提取网格数据失败: {e}") - return [], [] - - def _add_links_from_indices(self, soft_body: BulletSoftBodyNode, indices: List[int]): - """ - 根据索引添加链接 - - Args: - soft_body: 软体节点 - indices: 索引列表 - """ - try: - # 创建边集合以避免重复 - edges = set() - - # 从三角形面提取边 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - # 三角形的三条边 - edges.add(tuple(sorted([indices[i], indices[i+1]]))) - edges.add(tuple(sorted([indices[i+1], indices[i+2]]))) - edges.add(tuple(sorted([indices[i+2], indices[i]]))) - - # 添加链接 - for edge in edges: - soft_body.appendLink(edge[0], edge[1]) - - except Exception as e: - print(f"⚠ 添加链接失败: {e}") - - def _add_bending_constraints(self, soft_body: BulletSoftBodyNode): - """ - 添加弯曲约束 - - Args: - soft_body: 软体节点 - """ - try: - # 在实际实现中,这里会添加弯曲约束 - # BulletSoftBodyNode目前通过generateBendingConstraints方法添加 - soft_body.generateBendingConstraints(2) - - except Exception as e: - print(f"⚠ 添加弯曲约束失败: {e}") - - def _apply_wind_force(self, cloth_info: Dict[str, Any]): - """ - 对布料应用风力 - - Args: - cloth_info: 布料对象信息 - """ - try: - soft_body = cloth_info['soft_body'] - - # 在实际实现中,这里会应用风力 - # BulletSoftBodyWorldInfo可以设置风力参数 - # 这里简化处理 - pass - - except Exception as e: - print(f"⚠ 应用风力失败: {e}") - - def _check_tear_conditions(self, cloth_info: Dict[str, Any]): - """ - 检查撕裂条件 - - Args: - cloth_info: 布料对象信息 - """ - try: - tear_mode = cloth_info['params'].get('tear_mode', 'none') - if tear_mode == 'none': - return - - tear_threshold = cloth_info['params'].get('tear_threshold', 100.0) - # 在实际实现中,这里会检查撕裂条件 - # 由于BulletSoftBodyNode目前不直接支持撕裂,这里简化处理 - - except Exception as e: - print(f"⚠ 检查撕裂条件失败: {e}") - - def _get_physics_world(self): - """ - 获取物理世界对象 - - Returns: - 物理世界对象或None - """ - try: - # 尝试从世界对象获取物理世界 - if hasattr(self.world, 'physics_world'): - return self.world.physics_world.bullet_world - elif hasattr(self.world, 'bullet_world'): - return self.world.bullet_world - else: - # 查找物理插件 - physics_plugin = self.world.plugins.get('rigid_body_physics') - if physics_plugin and hasattr(physics_plugin, 'physics_world'): - return physics_plugin.physics_world.bullet_world - except Exception as e: - print(f"⚠ 获取物理世界失败: {e}") - +""" +布料管理器 +负责布料对象的创建、管理和更新 +包含完整的布料物理模拟功能 +""" + +from panda3d.core import Vec3, Point3, GeomVertexReader, GeomVertexFormat +from panda3d.core import GeomVertexData, Geom, GeomNode, NodePath +from panda3d.bullet import BulletSoftBodyNode, BulletSoftBodyWorldInfo +import math +from typing import List, Dict, Optional, Any, Tuple +import numpy as np + + +class ClothManager: + """ + 布料管理器 + 管理场景中的所有布料对象,提供完整的布料物理模拟功能 + """ + + # 布料类型常量 + CLOTH_TYPE_PLANE = "plane" + CLOTH_TYPE_PATCH = "patch" + CLOTH_TYPE_TUBE = "tube" + CLOTH_TYPE_ROPE = "rope" + + # 撕裂模式 + TEAR_MODE_NONE = "none" + TEAR_MODE_VERTEX = "vertex" + TEAR_MODE_EDGE = "edge" + + def __init__(self, world): + self.world = world + self.cloth_objects: List[Dict[str, Any]] = [] + self.enabled = False + self.gravity = Vec3(0, 0, -9.81) + + # 布料默认参数 + self.default_params = { + 'density': 0.5, + 'linear_stiffness': 0.8, + 'angular_stiffness': 0.8, + 'volume_stiffness': 0.5, + 'friction': 0.5, + 'restitution': 0.2, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.01, + 'lift_coefficient': 0.01, + 'anchored': False, + 'tear_mode': 'none', + 'tear_threshold': 100.0, + 'tear_resistance': 50.0, + 'bending_constraints': True, + 'clusters': 0, + 'cluster_hardness': 1.0, + 'position_iteration': 2, + 'drift_iteration': 2, + 'cluster_iteration': 2, + 'velocity_iteration': 2 + } + + # 风力参数 + self.wind_enabled = False + self.wind_velocity = Vec3(0, 0, 0) + self.wind_turbulence = 0.0 + self.wind_frequency = 1.0 + + # 性能参数 + self.max_cloth_objects = 100 + self.cloth_update_frequency = 60 # Hz + + def enable(self): + """ + 启用布料管理器 + """ + self.enabled = True + print("✓ 布料管理器已启用") + + def disable(self): + """ + 禁用布料管理器 + """ + self.enabled = False + print("✓ 布料管理器已禁用") + + def set_gravity(self, gravity: Vec3): + """ + 设置重力 + + Args: + gravity: 重力向量 + """ + self.gravity = gravity + + def set_wind_parameters(self, enabled: bool = True, velocity: Vec3 = None, + turbulence: float = 0.0, frequency: float = 1.0): + """ + 设置风力参数 + + Args: + enabled: 是否启用风力 + velocity: 风速向量 + turbulence: 湍流强度 + frequency: 风力变化频率 + """ + self.wind_enabled = enabled + if velocity: + self.wind_velocity = velocity + self.wind_turbulence = turbulence + self.wind_frequency = frequency + + def create_cloth_from_mesh(self, node_path, params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: + """ + 从网格创建布料对象 + + Args: + node_path: 网格节点路径 + params: 布料参数 + + Returns: + 布料对象信息字典 + """ + if not self.enabled: + print("⚠ 布料管理器未启用") + return None + + if len(self.cloth_objects) >= self.max_cloth_objects: + print("⚠ 达到最大布料对象数量限制") + return None + + try: + # 合并参数 + cloth_params = self.default_params.copy() + if params: + cloth_params.update(params) + + # 创建软体节点 + cloth_name = f'cloth_{len(self.cloth_objects)}' + soft_body_node = BulletSoftBodyNode(cloth_name) + + # 设置布料参数 + self._apply_cloth_parameters(soft_body_node, cloth_params) + + # 从网格创建布料 + vertices, indices = self._extract_mesh_data(node_path) + if not vertices or not indices: + print("⚠ 无法从网格提取顶点数据") + return None + + # 添加顶点 + for vertex in vertices: + soft_body_node.appendNode(vertex, 1.0) + + # 添加面 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) + + # 添加链接(边) + self._add_links_from_indices(soft_body_node, indices) + + # 添加弯曲约束 + if cloth_params.get('bending_constraints', True): + self._add_bending_constraints(soft_body_node) + + # 创建布料节点路径 + cloth_np = node_path.attachNewNode(soft_body_node) + cloth_np.setPos(node_path.getPos()) + cloth_np.setHpr(node_path.getHpr()) + + # 添加到物理世界 + physics_world = self._get_physics_world() + if physics_world: + physics_world.attachSoftBody(soft_body_node) + + # 保存布料信息 + cloth_info = { + 'node_path': cloth_np, + 'soft_body': soft_body_node, + 'original_node': node_path, + 'params': cloth_params, + 'type': 'cloth', + 'anchors': [], # 固定点列表 + 'tears': [], # 撕裂记录 + 'vertices': vertices, + 'indices': indices, + 'creation_time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0, + 'last_update_time': 0.0, + 'performance_stats': { + 'update_count': 0, + 'tear_count': 0, + 'collision_count': 0 + } + } + + self.cloth_objects.append(cloth_info) + + print(f"✓ 创建布料对象: {cloth_name} (顶点数: {len(vertices)}, 面数: {len(indices)//3})") + return cloth_info + + except Exception as e: + print(f"✗ 创建布料对象失败: {e}") + import traceback + traceback.print_exc() + return None + + def create_cloth_patch(self, node_path, width: float, height: float, + resolution_x: int, resolution_y: int, + params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: + """ + 创建布料片 + + Args: + node_path: 父节点路径 + width: 布料宽度 + height: 布料高度 + resolution_x: X方向分辨率 + resolution_y: Y方向分辨率 + params: 布料参数 + + Returns: + 布料对象信息字典 + """ + if not self.enabled: + print("⚠ 布料管理器未启用") + return None + + if len(self.cloth_objects) >= self.max_cloth_objects: + print("⚠ 达到最大布料对象数量限制") + return None + + try: + # 合并参数 + cloth_params = self.default_params.copy() + if params: + cloth_params.update(params) + + # 创建软体节点 + cloth_name = f'cloth_patch_{len(self.cloth_objects)}' + soft_body_node = BulletSoftBodyNode(cloth_name) + + # 设置布料参数 + self._apply_cloth_parameters(soft_body_node, cloth_params) + + # 生成布料片顶点 + vertices = [] + indices = [] + half_width = width / 2.0 + half_height = height / 2.0 + + # 生成顶点 + for j in range(resolution_y + 1): + for i in range(resolution_x + 1): + x = (i / resolution_x) * width - half_width + y = (j / resolution_y) * height - half_height + z = 0.0 + vertices.append(Point3(x, y, z)) + + # 生成索引 + for j in range(resolution_y): + for i in range(resolution_x): + v0 = j * (resolution_x + 1) + i + v1 = v0 + 1 + v2 = (j + 1) * (resolution_x + 1) + i + v3 = v2 + 1 + + indices.extend([v0, v1, v2, v1, v3, v2]) + + # 添加顶点到软体 + for vertex in vertices: + soft_body_node.appendNode(vertex, 1.0) + + # 添加面 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) + + # 添加链接 + self._add_links_from_indices(soft_body_node, indices) + + # 添加弯曲约束 + if cloth_params.get('bending_constraints', True): + self._add_bending_constraints(soft_body_node) + + # 创建布料节点路径 + cloth_np = node_path.attachNewNode(soft_body_node) + + # 添加到物理世界 + physics_world = self._get_physics_world() + if physics_world: + physics_world.attachSoftBody(soft_body_node) + + # 保存布料信息 + cloth_info = { + 'node_path': cloth_np, + 'soft_body': soft_body_node, + 'original_node': node_path, + 'params': cloth_params, + 'type': 'cloth_patch', + 'width': width, + 'height': height, + 'resolution_x': resolution_x, + 'resolution_y': resolution_y, + 'anchors': [], + 'tears': [], + 'vertices': vertices, + 'indices': indices, + 'creation_time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0, + 'last_update_time': 0.0, + 'performance_stats': { + 'update_count': 0, + 'tear_count': 0, + 'collision_count': 0 + } + } + + self.cloth_objects.append(cloth_info) + + print(f"✓ 创建布料片: {cloth_name} ({resolution_x+1}x{resolution_y+1} 顶点)") + return cloth_info + + except Exception as e: + print(f"✗ 创建布料片失败: {e}") + import traceback + traceback.print_exc() + return None + + def create_cloth_tube(self, node_path, radius: float, height: float, + resolution: int = 16, + params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: + """ + 创建管状布料 + + Args: + node_path: 父节点路径 + radius: 管道半径 + height: 管道高度 + resolution: 分辨率 + params: 布料参数 + + Returns: + 布料对象信息字典 + """ + if not self.enabled: + print("⚠ 布料管理器未启用") + return None + + if len(self.cloth_objects) >= self.max_cloth_objects: + print("⚠ 达到最大布料对象数量限制") + return None + + try: + # 合并参数 + cloth_params = self.default_params.copy() + if params: + cloth_params.update(params) + + # 创建软体节点 + cloth_name = f'cloth_tube_{len(self.cloth_objects)}' + soft_body_node = BulletSoftBodyNode(cloth_name) + + # 设置布料参数 + self._apply_cloth_parameters(soft_body_node, cloth_params) + + # 生成管状布料顶点 + vertices = [] + indices = [] + half_height = height / 2.0 + + # 生成顶点 + for j in range(2): # 顶部和底部 + z = half_height if j == 0 else -half_height + for i in range(resolution): + angle = (i / resolution) * 2 * math.pi + x = math.cos(angle) * radius + y = math.sin(angle) * radius + vertices.append(Point3(x, y, z)) + + # 生成侧面索引 + for i in range(resolution): + next_i = (i + 1) % resolution + # 上面 + indices.extend([i, next_i, i + resolution]) + indices.extend([next_i, next_i + resolution, i + resolution]) + + # 添加顶点到软体 + for vertex in vertices: + soft_body_node.appendNode(vertex, 1.0) + + # 添加面 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) + + # 添加链接 + self._add_links_from_indices(soft_body_node, indices) + + # 添加弯曲约束 + if cloth_params.get('bending_constraints', True): + self._add_bending_constraints(soft_body_node) + + # 创建布料节点路径 + cloth_np = node_path.attachNewNode(soft_body_node) + + # 添加到物理世界 + physics_world = self._get_physics_world() + if physics_world: + physics_world.attachSoftBody(soft_body_node) + + # 保存布料信息 + cloth_info = { + 'node_path': cloth_np, + 'soft_body': soft_body_node, + 'original_node': node_path, + 'params': cloth_params, + 'type': 'cloth_tube', + 'radius': radius, + 'height': height, + 'resolution': resolution, + 'anchors': [], + 'tears': [], + 'vertices': vertices, + 'indices': indices, + 'creation_time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0, + 'last_update_time': 0.0, + 'performance_stats': { + 'update_count': 0, + 'tear_count': 0, + 'collision_count': 0 + } + } + + self.cloth_objects.append(cloth_info) + + print(f"✓ 创建管状布料: {cloth_name} (半径: {radius}, 高度: {height})") + return cloth_info + + except Exception as e: + print(f"✗ 创建管状布料失败: {e}") + import traceback + traceback.print_exc() + return None + + def add_anchor(self, cloth_info: Dict[str, Any], vertex_index: int, + anchor_node=None, disable_collision: bool = True) -> bool: + """ + 为布料添加固定点 + + Args: + cloth_info: 布料对象信息 + vertex_index: 顶点索引 + anchor_node: 固定点节点(如果为None,则固定在当前位置) + disable_collision: 是否禁用与固定点的碰撞 + + Returns: + 是否成功添加固定点 + """ + try: + soft_body = cloth_info['soft_body'] + + if anchor_node: + # 固定到指定节点 + soft_body.appendAnchor(vertex_index, anchor_node, disable_collision) + else: + # 固定在当前位置 + soft_body.appendAnchor(vertex_index, None, disable_collision) + + # 记录固定点 + cloth_info['anchors'].append({ + 'vertex_index': vertex_index, + 'anchor_node': anchor_node, + 'disable_collision': disable_collision, + 'position': soft_body.get_nodes()[vertex_index].get_position() + }) + + print(f"✓ 为布料添加固定点: 顶点 {vertex_index}") + return True + + except Exception as e: + print(f"✗ 添加固定点失败: {e}") + return False + + def remove_anchor(self, cloth_info: Dict[str, Any], vertex_index: int) -> bool: + """ + 移除布料固定点 + + Args: + cloth_info: 布料对象信息 + vertex_index: 顶点索引 + + Returns: + 是否成功移除固定点 + """ + try: + anchors_to_remove = [] + for i, anchor in enumerate(cloth_info['anchors']): + if anchor['vertex_index'] == vertex_index: + anchors_to_remove.append(i) + + # 从后往前删除,避免索引变化 + for i in reversed(anchors_to_remove): + cloth_info['anchors'].pop(i) + + print(f"✓ 移除布料固定点: 顶点 {vertex_index}") + return True + + except Exception as e: + print(f"✗ 移除固定点失败: {e}") + return False + + def tear_cloth_at_point(self, cloth_info: Dict[str, Any], point: Point3, + radius: float = 0.5) -> bool: + """ + 在指定点撕裂布料 + + Args: + cloth_info: 布料对象信息 + point: 撕裂点 + radius: 撕裂半径 + + Returns: + 是否成功撕裂 + """ + try: + soft_body = cloth_info['soft_body'] + nodes = soft_body.get_nodes() + + # 查找附近的顶点 + tear_indices = [] + for i, node in enumerate(nodes): + node_pos = node.get_position() + distance = (node_pos - point).length() + if distance <= radius: + tear_indices.append(i) + + if not tear_indices: + print("⚠ 没有找到可撕裂的顶点") + return False + + # 执行撕裂 + tear_count = 0 + for index in tear_indices: + # 在实际实现中,这里会执行撕裂操作 + # BulletSoftBodyNode目前不直接支持撕裂,需要通过其他方式实现 + tear_count += 1 + + # 记录撕裂 + cloth_info['tears'].append({ + 'point': point, + 'radius': radius, + 'indices': tear_indices, + 'time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0 + }) + + cloth_info['performance_stats']['tear_count'] += tear_count + print(f"✓ 撕裂布料: {tear_count} 个顶点") + return True + + except Exception as e: + print(f"✗ 撕裂布料失败: {e}") + return False + + def remove_cloth(self, cloth_info: Dict[str, Any]) -> bool: + """ + 移除布料对象 + + Args: + cloth_info: 布料对象信息 + + Returns: + 是否成功移除 + """ + try: + if cloth_info not in self.cloth_objects: + print("⚠ 布料对象不存在") + return False + + # 从物理世界移除 + physics_world = self._get_physics_world() + if physics_world: + physics_world.removeSoftBody(cloth_info['soft_body']) + + # 移除节点 + if cloth_info['node_path'] and not cloth_info['node_path'].isEmpty(): + cloth_info['node_path'].removeNode() + + # 从列表移除 + self.cloth_objects.remove(cloth_info) + + print("✓ 移除布料对象") + return True + + except Exception as e: + print(f"✗ 移除布料对象失败: {e}") + return False + + def update_cloth_params(self, cloth_info: Dict[str, Any], params: Dict[str, Any]) -> bool: + """ + 更新布料参数 + + Args: + cloth_info: 布料对象信息 + params: 新参数 + + Returns: + 是否成功更新 + """ + try: + # 更新参数 + cloth_info['params'].update(params) + + # 应用参数到软体节点 + soft_body = cloth_info['soft_body'] + self._apply_cloth_parameters(soft_body, cloth_info['params']) + + print("✓ 更新布料参数") + return True + + except Exception as e: + print(f"✗ 更新布料参数失败: {e}") + return False + + def update_all_cloths(self, dt: float): + """ + 更新所有布料对象 + + Args: + dt: 时间增量 + """ + if not self.enabled: + return + + current_time = self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0 + + for cloth_info in self.cloth_objects: + # 检查更新频率 + if current_time - cloth_info['last_update_time'] < 1.0 / self.cloth_update_frequency: + continue + + # 更新性能统计 + cloth_info['performance_stats']['update_count'] += 1 + cloth_info['last_update_time'] = current_time + + # 应用风力 + if self.wind_enabled: + self._apply_wind_force(cloth_info) + + # 检查撕裂 + self._check_tear_conditions(cloth_info) + + def get_cloth_stats(self) -> Dict[str, Any]: + """ + 获取布料统计信息 + + Returns: + 统计信息字典 + """ + total_vertices = 0 + total_faces = 0 + total_anchors = 0 + total_tears = 0 + + for cloth_info in self.cloth_objects: + total_vertices += len(cloth_info.get('vertices', [])) + total_faces += len(cloth_info.get('indices', [])) // 3 + total_anchors += len(cloth_info.get('anchors', [])) + total_tears += cloth_info['performance_stats'].get('tear_count', 0) + + return { + 'total_cloth_objects': len(self.cloth_objects), + 'total_vertices': total_vertices, + 'total_faces': total_faces, + 'total_anchors': total_anchors, + 'total_tears': total_tears, + 'enabled': self.enabled, + 'wind_enabled': self.wind_enabled + } + + def cleanup(self): + """ + 清理所有布料对象 + """ + for cloth_info in self.cloth_objects[:]: + self.remove_cloth(cloth_info) + self.cloth_objects.clear() + print("✓ 布料管理器已清理") + + def _apply_cloth_parameters(self, soft_body: BulletSoftBodyNode, params: Dict[str, Any]): + """ + 应用布料参数到软体节点 + + Args: + soft_body: 软体节点 + params: 参数字典 + """ + cfg = soft_body.get_cfg() + + # 物理参数 + if 'linear_stiffness' in params: + cfg.set_kDF(params['linear_stiffness']) + if 'angular_stiffness' in params: + cfg.set_kDG(params['angular_stiffness']) + if 'volume_stiffness' in params: + cfg.set_kVC(params['volume_stiffness']) + if 'pressure_coefficient' in params: + cfg.set_kPR(params['pressure_coefficient']) + if 'drag_coefficient' in params: + cfg.set_kDG(params['drag_coefficient']) + if 'lift_coefficient' in params: + cfg.set_kLF(params['lift_coefficient']) + if 'cluster_hardness' in params: + cfg.set_kCHR(params['cluster_hardness']) + if 'kinetic_hardness' in params: + cfg.set_kKHR(params['kinetic_hardness']) + if 'soft_hardness' in params: + cfg.set_kSHR(params['soft_hardness']) + if 'anchor_hardness' in params: + cfg.set_kAHR(params['anchor_hardness']) + + # 迭代次数 + if 'position_iteration' in params: + cfg.set_piterations(params['position_iteration']) + if 'drift_iteration' in params: + cfg.set_diterations(params['drift_iteration']) + if 'cluster_iteration' in params: + cfg.set_citerations(params['cluster_iteration']) + if 'velocity_iteration' in params: + cfg.set_viterations(params['velocity_iteration']) + + # 材质属性 + if 'friction' in params: + soft_body.setFriction(params['friction']) + if 'restitution' in params: + soft_body.setRestitution(params['restitution']) + if 'density' in params: + soft_body.setTotalMass(params['density']) + + # 集群 + if 'clusters' in params and params['clusters'] > 0: + soft_body.generateClusters(params['clusters']) + + def _extract_mesh_data(self, node_path) -> Tuple[List[Point3], List[int]]: + """ + 从节点路径提取网格数据 + + Args: + node_path: 节点路径 + + Returns: + (顶点列表, 索引列表) + """ + try: + vertices = [] + indices = [] + + # 查找几何节点 + geom_nodes = node_path.findAllMatches('**/+GeomNode') + for geom_node_path in geom_nodes: + geom_node = geom_node_path.node() + for i in range(geom_node.getNumGeoms()): + geom = geom_node.getGeom(i) + vdata = geom.getVertexData() + + # 提取顶点 + vertex_reader = GeomVertexReader(vdata, 'vertex') + while not vertex_reader.isAtEnd(): + vertices.append(vertex_reader.getData3()) + + # 提取索引 + for prim in geom.getPrimitives(): + index_reader = prim.getIndexIterator() + while not index_reader.isAtEnd(): + indices.append(index_reader.getData()) + index_reader.next() + + return vertices, indices + + except Exception as e: + print(f"⚠ 提取网格数据失败: {e}") + return [], [] + + def _add_links_from_indices(self, soft_body: BulletSoftBodyNode, indices: List[int]): + """ + 根据索引添加链接 + + Args: + soft_body: 软体节点 + indices: 索引列表 + """ + try: + # 创建边集合以避免重复 + edges = set() + + # 从三角形面提取边 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + # 三角形的三条边 + edges.add(tuple(sorted([indices[i], indices[i+1]]))) + edges.add(tuple(sorted([indices[i+1], indices[i+2]]))) + edges.add(tuple(sorted([indices[i+2], indices[i]]))) + + # 添加链接 + for edge in edges: + soft_body.appendLink(edge[0], edge[1]) + + except Exception as e: + print(f"⚠ 添加链接失败: {e}") + + def _add_bending_constraints(self, soft_body: BulletSoftBodyNode): + """ + 添加弯曲约束 + + Args: + soft_body: 软体节点 + """ + try: + # 在实际实现中,这里会添加弯曲约束 + # BulletSoftBodyNode目前通过generateBendingConstraints方法添加 + soft_body.generateBendingConstraints(2) + + except Exception as e: + print(f"⚠ 添加弯曲约束失败: {e}") + + def _apply_wind_force(self, cloth_info: Dict[str, Any]): + """ + 对布料应用风力 + + Args: + cloth_info: 布料对象信息 + """ + try: + soft_body = cloth_info['soft_body'] + + # 在实际实现中,这里会应用风力 + # BulletSoftBodyWorldInfo可以设置风力参数 + # 这里简化处理 + pass + + except Exception as e: + print(f"⚠ 应用风力失败: {e}") + + def _check_tear_conditions(self, cloth_info: Dict[str, Any]): + """ + 检查撕裂条件 + + Args: + cloth_info: 布料对象信息 + """ + try: + tear_mode = cloth_info['params'].get('tear_mode', 'none') + if tear_mode == 'none': + return + + tear_threshold = cloth_info['params'].get('tear_threshold', 100.0) + # 在实际实现中,这里会检查撕裂条件 + # 由于BulletSoftBodyNode目前不直接支持撕裂,这里简化处理 + + except Exception as e: + print(f"⚠ 检查撕裂条件失败: {e}") + + def _get_physics_world(self): + """ + 获取物理世界对象 + + Returns: + 物理世界对象或None + """ + try: + # 尝试从世界对象获取物理世界 + if hasattr(self.world, 'physics_world'): + return self.world.physics_world.bullet_world + elif hasattr(self.world, 'bullet_world'): + return self.world.bullet_world + else: + # 查找物理插件 + physics_plugin = self.world.plugins.get('rigid_body_physics') + if physics_plugin and hasattr(physics_plugin, 'physics_world'): + return physics_plugin.physics_world.bullet_world + except Exception as e: + print(f"⚠ 获取物理世界失败: {e}") + return None \ No newline at end of file diff --git a/plugins/user/soft_body_cloth_physics/core/cloth_physics.py b/plugins/user/soft_body_cloth_physics/core/cloth_physics.py index 8893c071..8e91917e 100644 --- a/plugins/user/soft_body_cloth_physics/core/cloth_physics.py +++ b/plugins/user/soft_body_cloth_physics/core/cloth_physics.py @@ -1,991 +1,991 @@ -""" -布料物理实现 -提供布料物理模拟的具体实现 -包含完整的布料物理计算和仿真功能 -""" - -from panda3d.core import Vec3, Point3, GeomVertexReader, GeomVertexFormat -from panda3d.core import GeomVertexData, Geom, GeomNode, NodePath -from panda3d.bullet import BulletSoftBodyNode, BulletSoftBodyWorldInfo -import math -from typing import List, Dict, Optional, Any, Tuple -import numpy as np - - -class ClothPhysics: - """ - 布料物理实现类 - 处理布料的具体物理模拟,包括弹性、塑性、撕裂等复杂物理效应 - """ - - # 布料材质类型 - CLOTH_TYPE_COTTON = "cotton" - CLOTH_TYPE_SILK = "silk" - CLOTH_TYPE_LEATHER = "leather" - CLOTH_TYPE_RUBBER = "rubber" - CLOTH_TYPE_DENIM = "denim" - CLOTH_TYPE_LINEN = "linen" - CLOTH_TYPE_WOOL = "wool" - CLOTH_TYPE_SYNTHETIC = "synthetic" - CLOTH_TYPE_MESH = "mesh" - CLOTH_TYPE_CANVAS = "canvas" - - # 布料行为模式 - BEHAVIOR_DEFAULT = "default" - BEHAVIOR_STIFF = "stiff" - BEHAVIOR_FLEXIBLE = "flexible" - BEHAVIOR_ELASTIC = "elastic" - - # 撕裂模式 - TEAR_MODE_NONE = "none" - TEAR_MODE_VERTEX = "vertex" - TEAR_MODE_EDGE = "edge" - TEAR_MODE_FACE = "face" - - def __init__(self, world): - self.world = world - self.soft_body_world_info = None - self._init_soft_body_world() - - # 预定义的布料材质参数 - self.cloth_materials = { - self.CLOTH_TYPE_COTTON: { - 'density': 0.6, - 'linear_stiffness': 0.7, - 'angular_stiffness': 0.6, - 'volume_stiffness': 0.4, - 'friction': 0.6, - 'restitution': 0.1, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.02, - 'lift_coefficient': 0.01, - 'bend_stiffness': 0.5, - 'damping': 0.01, - 'plastic_threshold': 0.1, - 'plastic_stiffness': 0.5, - 'tear_threshold': 100.0, - 'tear_resistance': 50.0, - 'anisotropic_friction': [0.6, 0.6, 0.6], - 'dynamic_friction': 0.6, - 'static_friction': 0.7, - 'rolling_friction': 0.1, - 'spinning_friction': 0.05 - }, - self.CLOTH_TYPE_SILK: { - 'density': 0.3, - 'linear_stiffness': 0.5, - 'angular_stiffness': 0.4, - 'volume_stiffness': 0.2, - 'friction': 0.2, - 'restitution': 0.3, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.05, - 'lift_coefficient': 0.03, - 'bend_stiffness': 0.3, - 'damping': 0.005, - 'plastic_threshold': 0.05, - 'plastic_stiffness': 0.3, - 'tear_threshold': 80.0, - 'tear_resistance': 30.0, - 'anisotropic_friction': [0.2, 0.2, 0.2], - 'dynamic_friction': 0.2, - 'static_friction': 0.3, - 'rolling_friction': 0.05, - 'spinning_friction': 0.02 - }, - self.CLOTH_TYPE_LEATHER: { - 'density': 0.8, - 'linear_stiffness': 0.9, - 'angular_stiffness': 0.8, - 'volume_stiffness': 0.6, - 'friction': 0.7, - 'restitution': 0.1, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.01, - 'lift_coefficient': 0.005, - 'bend_stiffness': 0.8, - 'damping': 0.015, - 'plastic_threshold': 0.2, - 'plastic_stiffness': 0.7, - 'tear_threshold': 150.0, - 'tear_resistance': 80.0, - 'anisotropic_friction': [0.7, 0.7, 0.7], - 'dynamic_friction': 0.7, - 'static_friction': 0.8, - 'rolling_friction': 0.15, - 'spinning_friction': 0.08 - }, - self.CLOTH_TYPE_RUBBER: { - 'density': 1.2, - 'linear_stiffness': 0.95, - 'angular_stiffness': 0.9, - 'volume_stiffness': 0.8, - 'friction': 0.8, - 'restitution': 0.4, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.005, - 'lift_coefficient': 0.002, - 'bend_stiffness': 0.9, - 'damping': 0.02, - 'plastic_threshold': 0.3, - 'plastic_stiffness': 0.8, - 'tear_threshold': 200.0, - 'tear_resistance': 100.0, - 'anisotropic_friction': [0.8, 0.8, 0.8], - 'dynamic_friction': 0.8, - 'static_friction': 0.9, - 'rolling_friction': 0.2, - 'spinning_friction': 0.1 - }, - self.CLOTH_TYPE_DENIM: { - 'density': 0.7, - 'linear_stiffness': 0.8, - 'angular_stiffness': 0.7, - 'volume_stiffness': 0.5, - 'friction': 0.65, - 'restitution': 0.15, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.015, - 'lift_coefficient': 0.008, - 'bend_stiffness': 0.6, - 'damping': 0.012, - 'plastic_threshold': 0.15, - 'plastic_stiffness': 0.6, - 'tear_threshold': 120.0, - 'tear_resistance': 60.0, - 'anisotropic_friction': [0.65, 0.65, 0.65], - 'dynamic_friction': 0.65, - 'static_friction': 0.75, - 'rolling_friction': 0.12, - 'spinning_friction': 0.06 - }, - self.CLOTH_TYPE_LINEN: { - 'density': 0.5, - 'linear_stiffness': 0.6, - 'angular_stiffness': 0.5, - 'volume_stiffness': 0.3, - 'friction': 0.55, - 'restitution': 0.12, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.025, - 'lift_coefficient': 0.012, - 'bend_stiffness': 0.4, - 'damping': 0.008, - 'plastic_threshold': 0.12, - 'plastic_stiffness': 0.4, - 'tear_threshold': 90.0, - 'tear_resistance': 40.0, - 'anisotropic_friction': [0.55, 0.55, 0.55], - 'dynamic_friction': 0.55, - 'static_friction': 0.65, - 'rolling_friction': 0.08, - 'spinning_friction': 0.04 - }, - self.CLOTH_TYPE_WOOL: { - 'density': 0.65, - 'linear_stiffness': 0.65, - 'angular_stiffness': 0.55, - 'volume_stiffness': 0.35, - 'friction': 0.6, - 'restitution': 0.13, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.018, - 'lift_coefficient': 0.009, - 'bend_stiffness': 0.45, - 'damping': 0.01, - 'plastic_threshold': 0.13, - 'plastic_stiffness': 0.45, - 'tear_threshold': 100.0, - 'tear_resistance': 50.0, - 'anisotropic_friction': [0.6, 0.6, 0.6], - 'dynamic_friction': 0.6, - 'static_friction': 0.7, - 'rolling_friction': 0.1, - 'spinning_friction': 0.05 - }, - self.CLOTH_TYPE_SYNTHETIC: { - 'density': 0.4, - 'linear_stiffness': 0.75, - 'angular_stiffness': 0.65, - 'volume_stiffness': 0.45, - 'friction': 0.5, - 'restitution': 0.2, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.012, - 'lift_coefficient': 0.007, - 'bend_stiffness': 0.55, - 'damping': 0.006, - 'plastic_threshold': 0.18, - 'plastic_stiffness': 0.55, - 'tear_threshold': 110.0, - 'tear_resistance': 55.0, - 'anisotropic_friction': [0.5, 0.5, 0.5], - 'dynamic_friction': 0.5, - 'static_friction': 0.6, - 'rolling_friction': 0.07, - 'spinning_friction': 0.03 - }, - self.CLOTH_TYPE_MESH: { - 'density': 0.2, - 'linear_stiffness': 0.4, - 'angular_stiffness': 0.3, - 'volume_stiffness': 0.1, - 'friction': 0.3, - 'restitution': 0.05, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.04, - 'lift_coefficient': 0.02, - 'bend_stiffness': 0.2, - 'damping': 0.003, - 'plastic_threshold': 0.08, - 'plastic_stiffness': 0.2, - 'tear_threshold': 70.0, - 'tear_resistance': 25.0, - 'anisotropic_friction': [0.3, 0.3, 0.3], - 'dynamic_friction': 0.3, - 'static_friction': 0.4, - 'rolling_friction': 0.03, - 'spinning_friction': 0.01 - }, - self.CLOTH_TYPE_CANVAS: { - 'density': 0.75, - 'linear_stiffness': 0.85, - 'angular_stiffness': 0.75, - 'volume_stiffness': 0.55, - 'friction': 0.68, - 'restitution': 0.14, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.013, - 'lift_coefficient': 0.006, - 'bend_stiffness': 0.65, - 'damping': 0.014, - 'plastic_threshold': 0.18, - 'plastic_stiffness': 0.65, - 'tear_threshold': 130.0, - 'tear_resistance': 65.0, - 'anisotropic_friction': [0.68, 0.68, 0.68], - 'dynamic_friction': 0.68, - 'static_friction': 0.78, - 'rolling_friction': 0.14, - 'spinning_friction': 0.07 - } - } - - # 风力参数 - self.wind_enabled = False - self.wind_velocity = Vec3(0, 0, 0) - self.wind_turbulence = 0.0 - self.wind_frequency = 1.0 - self.wind_phase = 0.0 - - # 性能优化参数 - self.max_simulation_steps = 10 - self.time_step = 1.0/60.0 - self.sub_steps = 4 - - def _init_soft_body_world(self): - """ - 初始化软体世界信息 - """ - try: - # 获取物理世界中的软体世界信息 - physics_world = self._get_physics_world() - if physics_world and hasattr(physics_world, 'soft_body_world_info'): - self.soft_body_world_info = physics_world.soft_body_world_info - else: - # 创建新的软体世界信息 - self.soft_body_world_info = BulletSoftBodyWorldInfo() - self.soft_body_world_info.setGravity(Vec3(0, 0, -9.81)) - self.soft_body_world_info.setAirDensity(1.225) - self.soft_body_world_info.setWaterDensity(1000.0) - self.soft_body_world_info.setWaterOffset(0.0) - self.soft_body_world_info.setWaterNormal(Vec3(0, 0, 1)) - - print("✓ 软体世界初始化完成") - - except Exception as e: - print(f"✗ 软体世界初始化失败: {e}") - - def set_wind_parameters(self, enabled: bool = True, velocity: Vec3 = None, - turbulence: float = 0.0, frequency: float = 1.0): - """ - 设置风力参数 - - Args: - enabled: 是否启用风力 - velocity: 风速向量 - turbulence: 湍流强度 - frequency: 风力变化频率 - """ - self.wind_enabled = enabled - if velocity: - self.wind_velocity = velocity - self.wind_turbulence = turbulence - self.wind_frequency = frequency - - def update_wind(self, dt: float): - """ - 更新风力参数(随时间变化) - - Args: - dt: 时间增量 - """ - if not self.wind_enabled: - return - - # 更新风力相位 - self.wind_phase += dt * self.wind_frequency * 2 * math.pi - - # 应用湍流效果 - if self.wind_turbulence > 0: - turbulence_factor = 1.0 + math.sin(self.wind_phase) * self.wind_turbulence - current_wind = self.wind_velocity * turbulence_factor - - # 更新软体世界信息 - if self.soft_body_world_info: - # 在实际实现中,这里会更新软体世界的风力参数 - pass - - def create_cloth_with_material(self, node_path, cloth_type: str, - width: float, height: float, - resolution_x: int, resolution_y: int, - behavior_mode: str = BEHAVIOR_DEFAULT) -> Optional[BulletSoftBodyNode]: - """ - 使用指定材质创建布料 - - Args: - node_path: 父节点路径 - cloth_type: 布料类型 - width: 布料宽度 - height: 布料高度 - resolution_x: X方向分辨率 - resolution_y: Y方向分辨率 - behavior_mode: 行为模式 - - Returns: - BulletSoftBodyNode对象或None - """ - try: - # 获取材质参数 - if cloth_type not in self.cloth_materials: - print(f"⚠ 未知布料类型: {cloth_type}, 使用默认参数") - params = self.cloth_materials[self.CLOTH_TYPE_COTTON] - else: - params = self.cloth_materials[cloth_type] - - # 根据行为模式调整参数 - adjusted_params = self._adjust_parameters_for_behavior(params, behavior_mode) - - # 创建软体节点 - cloth_name = f'cloth_{cloth_type}_{len(getattr(self, "_cloth_counter", [0]))}' - soft_body_node = BulletSoftBodyNode(cloth_name) - - # 设置布料参数 - self._apply_cloth_parameters(soft_body_node, adjusted_params) - - # 生成布料网格顶点 - vertices = self._generate_cloth_vertices(width, height, resolution_x, resolution_y) - - # 添加顶点到软体 - for vertex in vertices: - soft_body_node.appendNode(vertex, 1.0) - - # 生成面索引 - indices = self._generate_cloth_indices(resolution_x, resolution_y) - - # 添加面 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) - - # 添加链接 - self._add_links_from_indices(soft_body_node, indices) - - # 添加弯曲约束 - self._add_bending_constraints(soft_body_node, adjusted_params) - - # 创建布料节点路径 - cloth_np = node_path.attachNewNode(soft_body_node) - - # 添加到物理世界 - physics_world = self._get_physics_world() - if physics_world: - physics_world.attachSoftBody(soft_body_node) - - print(f"✓ 创建{cloth_type}布料: {cloth_name} ({resolution_x+1}x{resolution_y+1} 顶点)") - return soft_body_node - - except Exception as e: - print(f"✗ 创建{cloth_type}布料失败: {e}") - import traceback - traceback.print_exc() - return None - - def create_cloth_from_mesh(self, node_path, cloth_type: str, - params: Dict[str, Any] = None) -> Optional[BulletSoftBodyNode]: - """ - 从网格创建布料 - - Args: - node_path: 网格节点路径 - cloth_type: 布料类型 - params: 额外参数 - - Returns: - BulletSoftBodyNode对象或None - """ - try: - # 获取材质参数 - if cloth_type not in self.cloth_materials: - print(f"⚠ 未知布料类型: {cloth_type}, 使用默认参数") - base_params = self.cloth_materials[self.CLOTH_TYPE_COTTON] - else: - base_params = self.cloth_materials[cloth_type] - - # 合并自定义参数 - if params: - base_params.update(params) - - # 创建软体节点 - cloth_name = f'cloth_mesh_{cloth_type}_{len(getattr(self, "_cloth_counter", [0]))}' - soft_body_node = BulletSoftBodyNode(cloth_name) - - # 设置布料参数 - self._apply_cloth_parameters(soft_body_node, base_params) - - # 从网格提取数据 - vertices, indices = self._extract_mesh_data(node_path) - if not vertices or not indices: - print("⚠ 无法从网格提取顶点数据") - return None - - # 添加顶点到软体 - for vertex in vertices: - soft_body_node.appendNode(vertex, 1.0) - - # 添加面 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) - - # 添加链接 - self._add_links_from_indices(soft_body_node, indices) - - # 添加弯曲约束 - self._add_bending_constraints(soft_body_node, base_params) - - # 创建布料节点路径 - cloth_np = node_path.attachNewNode(soft_body_node) - cloth_np.setPos(node_path.getPos()) - cloth_np.setHpr(node_path.getHpr()) - - # 添加到物理世界 - physics_world = self._get_physics_world() - if physics_world: - physics_world.attachSoftBody(soft_body_node) - - print(f"✓ 从网格创建{cloth_type}布料: {cloth_name} (顶点数: {len(vertices)})") - return soft_body_node - - except Exception as e: - print(f"✗ 从网格创建{cloth_type}布料失败: {e}") - import traceback - traceback.print_exc() - return None - - def apply_wind_force(self, soft_body: BulletSoftBodyNode, wind_vector: Vec3 = None): - """ - 对软体应用风力 - - Args: - soft_body: 软体节点 - wind_vector: 风力向量(如果为None,则使用全局风力参数) - """ - try: - # 使用指定风力向量或全局风力参数 - if wind_vector is None: - wind_vector = self.wind_velocity - - # 在实际实现中,这里会应用风力到软体 - # BulletSoftBodyNode有addVelocity方法可以用于此目的 - # 通过软体世界信息设置风力参数 - if self.soft_body_world_info: - # 这里会通过软体世界信息应用风力 - pass - - print(f"✓ 对软体应用风力: {wind_vector}") - - except Exception as e: - print(f"✗ 应用风力失败: {e}") - - def apply_force_to_region(self, soft_body: BulletSoftBodyNode, - force: Vec3, region_center: Point3, - radius: float) -> bool: - """ - 对布料的特定区域应用力 - - Args: - soft_body: 软体节点 - force: 力向量 - region_center: 区域中心点 - radius: 区域半径 - - Returns: - 是否成功应用力 - """ - try: - # 获取软体节点 - nodes = soft_body.get_nodes() - - # 查找受影响的节点 - affected_nodes = [] - for i, node in enumerate(nodes): - node_pos = node.get_position() - distance = (node_pos - region_center).length() - if distance <= radius: - affected_nodes.append((i, node, distance)) - - # 应用力到受影响的节点 - force_magnitude = force.length() - for node_index, node, distance in affected_nodes: - # 根据距离衰减力的大小 - decay_factor = 1.0 - (distance / radius) - if decay_factor > 0: - applied_force = force * decay_factor - # 在实际实现中,这里会应用力到节点 - # node.add_velocity(applied_force) - - print(f"✓ 对布料区域应用力: {len(affected_nodes)} 个节点受影响") - return True - - except Exception as e: - print(f"✗ 应用力到区域失败: {e}") - return False - - def simulate_tear(self, soft_body: BulletSoftBodyNode, - tear_point: Point3, radius: float) -> bool: - """ - 模拟布料撕裂 - - Args: - soft_body: 软体节点 - tear_point: 撕裂点 - radius: 撕裂半径 - - Returns: - 是否成功模拟撕裂 - """ - try: - # 获取软体节点 - nodes = soft_body.get_nodes() - - # 查找撕裂区域内的节点 - tear_nodes = [] - for i, node in enumerate(nodes): - node_pos = node.get_position() - distance = (node_pos - tear_point).length() - if distance <= radius: - tear_nodes.append((i, node, distance)) - - if not tear_nodes: - print("⚠ 没有找到可撕裂的节点") - return False - - # 在实际实现中,这里会执行撕裂操作 - # BulletSoftBodyNode目前不直接支持撕裂,需要通过其他方式实现 - tear_count = len(tear_nodes) - - print(f"✓ 模拟布料撕裂: {tear_count} 个节点受影响") - return True - - except Exception as e: - print(f"✗ 模拟撕裂失败: {e}") - return False - - def set_cloth_parameters(self, soft_body: BulletSoftBodyNode, params: Dict[str, Any]): - """ - 设置布料参数 - - Args: - soft_body: 软体节点 - params: 参数字典 - """ - try: - self._apply_cloth_parameters(soft_body, params) - print("✓ 设置布料参数完成") - - except Exception as e: - print(f"✗ 设置布料参数失败: {e}") - - def get_material_parameters(self, cloth_type: str) -> Optional[Dict[str, Any]]: - """ - 获取指定材质的参数 - - Args: - cloth_type: 布料类型 - - Returns: - 参数字典或None - """ - return self.cloth_materials.get(cloth_type) - - def get_all_material_types(self) -> List[str]: - """ - 获取所有可用的布料材质类型 - - Returns: - 材质类型列表 - """ - return list(self.cloth_materials.keys()) - - def update_soft_body_world_info(self, air_density: float = None, - water_density: float = None, - water_offset: float = None, - water_normal: Vec3 = None, - gravity: Vec3 = None): - """ - 更新软体世界信息 - - Args: - air_density: 空气密度 - water_density: 水密度 - water_offset: 水偏移 - water_normal: 水法线 - gravity: 重力向量 - """ - try: - if not self.soft_body_world_info: - print("⚠ 软体世界信息未初始化") - return - - if air_density is not None: - self.soft_body_world_info.setAirDensity(air_density) - if water_density is not None: - self.soft_body_world_info.setWaterDensity(water_density) - if water_offset is not None: - self.soft_body_world_info.setWaterOffset(water_offset) - if water_normal is not None: - self.soft_body_world_info.setWaterNormal(water_normal) - if gravity is not None: - self.soft_body_world_info.setGravity(gravity) - - print("✓ 更新软体世界信息完成") - - except Exception as e: - print(f"✗ 更新软体世界信息失败: {e}") - - def calculate_cloth_properties(self, vertices: List[Point3], indices: List[int]) -> Dict[str, Any]: - """ - 计算布料属性 - - Args: - vertices: 顶点列表 - indices: 索引列表 - - Returns: - 属性字典 - """ - try: - properties = { - 'vertex_count': len(vertices), - 'face_count': len(indices) // 3, - 'edge_count': 0, - 'surface_area': 0.0, - 'volume': 0.0, - 'center_of_mass': Point3(0, 0, 0), - 'bounding_box': { - 'min': Point3(float('inf'), float('inf'), float('inf')), - 'max': Point3(float('-inf'), float('-inf'), float('-inf')) - } - } - - if not vertices: - return properties - - # 计算包围盒 - for vertex in vertices: - properties['bounding_box']['min'].set( - min(properties['bounding_box']['min'][0], vertex[0]), - min(properties['bounding_box']['min'][1], vertex[1]), - min(properties['bounding_box']['min'][2], vertex[2]) - ) - properties['bounding_box']['max'].set( - max(properties['bounding_box']['max'][0], vertex[0]), - max(properties['bounding_box']['max'][1], vertex[1]), - max(properties['bounding_box']['max'][2], vertex[2]) - ) - - # 计算质心 - center = Point3(0, 0, 0) - for vertex in vertices: - center += vertex - center /= len(vertices) - properties['center_of_mass'] = center - - # 计算表面积 - total_area = 0.0 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - v0 = vertices[indices[i]] - v1 = vertices[indices[i+1]] - v2 = vertices[indices[i+2]] - # 计算三角形面积 - edge1 = v1 - v0 - edge2 = v2 - v0 - cross = edge1.cross(edge2) - area = cross.length() * 0.5 - total_area += area - - properties['surface_area'] = total_area - - return properties - - except Exception as e: - print(f"✗ 计算布料属性失败: {e}") - return {} - - def _apply_cloth_parameters(self, soft_body: BulletSoftBodyNode, params: Dict[str, Any]): - """ - 应用布料参数到软体节点 - - Args: - soft_body: 软体节点 - params: 参数字典 - """ - cfg = soft_body.get_cfg() - - # 物理参数 - if 'linear_stiffness' in params: - cfg.set_kDF(params['linear_stiffness']) - if 'angular_stiffness' in params: - cfg.set_kDG(params['angular_stiffness']) - if 'volume_stiffness' in params: - cfg.set_kVC(params['volume_stiffness']) - if 'pressure_coefficient' in params: - cfg.set_kPR(params['pressure_coefficient']) - if 'drag_coefficient' in params: - cfg.set_kDG(params['drag_coefficient']) - if 'lift_coefficient' in params: - cfg.set_kLF(params['lift_coefficient']) - if 'bend_stiffness' in params: - cfg.set_kDF(params['bend_stiffness']) # 弯曲刚度 - if 'damping' in params: - cfg.set_dft_damping(params['damping']) - if 'plastic_threshold' in params: - cfg.set_kMT(params['plastic_threshold']) # 塑性阈值 - if 'plastic_stiffness' in params: - cfg.set_kDP(params['plastic_stiffness']) # 塑性刚度 - - # 材质属性 - if 'friction' in params: - soft_body.setFriction(params['friction']) - if 'restitution' in params: - soft_body.setRestitution(params['restitution']) - if 'density' in params: - soft_body.setTotalMass(params['density']) - if 'anisotropic_friction' in params: - friction_vec = params['anisotropic_friction'] - if len(friction_vec) >= 3: - soft_body.setAnisotropicFriction( - Vec3(friction_vec[0], friction_vec[1], friction_vec[2]) - ) - - # 高级摩擦参数 - if 'dynamic_friction' in params: - # 在实际实现中,这里会设置动态摩擦系数 - pass - if 'static_friction' in params: - # 在实际实现中,这里会设置静态摩擦系数 - pass - if 'rolling_friction' in params: - # 在实际实现中,这里会设置滚动摩擦系数 - pass - if 'spinning_friction' in params: - # 在实际实现中,这里会设置自旋摩擦系数 - pass - - def _adjust_parameters_for_behavior(self, params: Dict[str, Any], behavior_mode: str) -> Dict[str, Any]: - """ - 根据行为模式调整参数 - - Args: - params: 原始参数 - behavior_mode: 行为模式 - - Returns: - 调整后的参数 - """ - adjusted_params = params.copy() - - if behavior_mode == self.BEHAVIOR_STIFF: - adjusted_params['linear_stiffness'] *= 1.5 - adjusted_params['angular_stiffness'] *= 1.5 - adjusted_params['volume_stiffness'] *= 1.5 - adjusted_params['bend_stiffness'] *= 1.5 - elif behavior_mode == self.BEHAVIOR_FLEXIBLE: - adjusted_params['linear_stiffness'] *= 0.7 - adjusted_params['angular_stiffness'] *= 0.7 - adjusted_params['volume_stiffness'] *= 0.7 - adjusted_params['bend_stiffness'] *= 0.7 - elif behavior_mode == self.BEHAVIOR_ELASTIC: - adjusted_params['linear_stiffness'] *= 1.2 - adjusted_params['restitution'] = min(1.0, adjusted_params['restitution'] * 1.5) - - return adjusted_params - - def _generate_cloth_vertices(self, width: float, height: float, - resolution_x: int, resolution_y: int) -> List[Point3]: - """ - 生成布料顶点 - - Args: - width: 宽度 - height: 高度 - resolution_x: X分辨率 - resolution_y: Y分辨率 - - Returns: - 顶点列表 - """ - vertices = [] - half_width = width / 2.0 - half_height = height / 2.0 - - for j in range(resolution_y + 1): - for i in range(resolution_x + 1): - x = (i / resolution_x) * width - half_width - y = (j / resolution_y) * height - half_height - z = 0.0 - vertices.append(Point3(x, y, z)) - - return vertices - - def _generate_cloth_indices(self, resolution_x: int, resolution_y: int) -> List[int]: - """ - 生成布料索引 - - Args: - resolution_x: X分辨率 - resolution_y: Y分辨率 - - Returns: - 索引列表 - """ - indices = [] - - for j in range(resolution_y): - for i in range(resolution_x): - v0 = j * (resolution_x + 1) + i - v1 = v0 + 1 - v2 = (j + 1) * (resolution_x + 1) + i - v3 = v2 + 1 - - indices.extend([v0, v1, v2, v1, v3, v2]) - - return indices - - def _add_links_from_indices(self, soft_body: BulletSoftBodyNode, indices: List[int]): - """ - 根据索引添加链接 - - Args: - soft_body: 软体节点 - indices: 索引列表 - """ - try: - # 创建边集合以避免重复 - edges = set() - - # 从三角形面提取边 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - # 三角形的三条边 - edges.add(tuple(sorted([indices[i], indices[i+1]]))) - edges.add(tuple(sorted([indices[i+1], indices[i+2]]))) - edges.add(tuple(sorted([indices[i+2], indices[i]]))) - - # 添加链接 - for edge in edges: - soft_body.appendLink(edge[0], edge[1]) - - except Exception as e: - print(f"⚠ 添加链接失败: {e}") - - def _add_bending_constraints(self, soft_body: BulletSoftBodyNode, params: Dict[str, Any]): - """ - 添加弯曲约束 - - Args: - soft_body: 软体节点 - params: 参数字典 - """ - try: - # 添加弯曲约束 - # 这个参数控制弯曲约束的生成步长 - soft_body.generateBendingConstraints(2) - - except Exception as e: - print(f"⚠ 添加弯曲约束失败: {e}") - - def _extract_mesh_data(self, node_path) -> Tuple[List[Point3], List[int]]: - """ - 从节点路径提取网格数据 - - Args: - node_path: 节点路径 - - Returns: - (顶点列表, 索引列表) - """ - try: - vertices = [] - indices = [] - - # 查找几何节点 - geom_nodes = node_path.findAllMatches('**/+GeomNode') - for geom_node_path in geom_nodes: - geom_node = geom_node_path.node() - for i in range(geom_node.getNumGeoms()): - geom = geom_node.getGeom(i) - vdata = geom.getVertexData() - - # 提取顶点 - vertex_reader = GeomVertexReader(vdata, 'vertex') - while not vertex_reader.isAtEnd(): - vertices.append(vertex_reader.getData3()) - - # 提取索引 - for prim in geom.getPrimitives(): - index_reader = prim.getIndexIterator() - while not index_reader.isAtEnd(): - indices.append(index_reader.getData()) - index_reader.next() - - return vertices, indices - - except Exception as e: - print(f"⚠ 提取网格数据失败: {e}") - return [], [] - - def _get_physics_world(self): - """ - 获取物理世界对象 - - Returns: - 物理世界对象或None - """ - try: - # 尝试从世界对象获取物理世界 - if hasattr(self.world, 'physics_world'): - return self.world.physics_world.bullet_world - elif hasattr(self.world, 'bullet_world'): - return self.world.bullet_world - else: - # 查找物理插件 - physics_plugin = self.world.plugins.get('rigid_body_physics') - if physics_plugin and hasattr(physics_plugin, 'physics_world'): - return physics_plugin.physics_world.bullet_world - except Exception as e: - print(f"⚠ 获取物理世界失败: {e}") - +""" +布料物理实现 +提供布料物理模拟的具体实现 +包含完整的布料物理计算和仿真功能 +""" + +from panda3d.core import Vec3, Point3, GeomVertexReader, GeomVertexFormat +from panda3d.core import GeomVertexData, Geom, GeomNode, NodePath +from panda3d.bullet import BulletSoftBodyNode, BulletSoftBodyWorldInfo +import math +from typing import List, Dict, Optional, Any, Tuple +import numpy as np + + +class ClothPhysics: + """ + 布料物理实现类 + 处理布料的具体物理模拟,包括弹性、塑性、撕裂等复杂物理效应 + """ + + # 布料材质类型 + CLOTH_TYPE_COTTON = "cotton" + CLOTH_TYPE_SILK = "silk" + CLOTH_TYPE_LEATHER = "leather" + CLOTH_TYPE_RUBBER = "rubber" + CLOTH_TYPE_DENIM = "denim" + CLOTH_TYPE_LINEN = "linen" + CLOTH_TYPE_WOOL = "wool" + CLOTH_TYPE_SYNTHETIC = "synthetic" + CLOTH_TYPE_MESH = "mesh" + CLOTH_TYPE_CANVAS = "canvas" + + # 布料行为模式 + BEHAVIOR_DEFAULT = "default" + BEHAVIOR_STIFF = "stiff" + BEHAVIOR_FLEXIBLE = "flexible" + BEHAVIOR_ELASTIC = "elastic" + + # 撕裂模式 + TEAR_MODE_NONE = "none" + TEAR_MODE_VERTEX = "vertex" + TEAR_MODE_EDGE = "edge" + TEAR_MODE_FACE = "face" + + def __init__(self, world): + self.world = world + self.soft_body_world_info = None + self._init_soft_body_world() + + # 预定义的布料材质参数 + self.cloth_materials = { + self.CLOTH_TYPE_COTTON: { + 'density': 0.6, + 'linear_stiffness': 0.7, + 'angular_stiffness': 0.6, + 'volume_stiffness': 0.4, + 'friction': 0.6, + 'restitution': 0.1, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.02, + 'lift_coefficient': 0.01, + 'bend_stiffness': 0.5, + 'damping': 0.01, + 'plastic_threshold': 0.1, + 'plastic_stiffness': 0.5, + 'tear_threshold': 100.0, + 'tear_resistance': 50.0, + 'anisotropic_friction': [0.6, 0.6, 0.6], + 'dynamic_friction': 0.6, + 'static_friction': 0.7, + 'rolling_friction': 0.1, + 'spinning_friction': 0.05 + }, + self.CLOTH_TYPE_SILK: { + 'density': 0.3, + 'linear_stiffness': 0.5, + 'angular_stiffness': 0.4, + 'volume_stiffness': 0.2, + 'friction': 0.2, + 'restitution': 0.3, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.05, + 'lift_coefficient': 0.03, + 'bend_stiffness': 0.3, + 'damping': 0.005, + 'plastic_threshold': 0.05, + 'plastic_stiffness': 0.3, + 'tear_threshold': 80.0, + 'tear_resistance': 30.0, + 'anisotropic_friction': [0.2, 0.2, 0.2], + 'dynamic_friction': 0.2, + 'static_friction': 0.3, + 'rolling_friction': 0.05, + 'spinning_friction': 0.02 + }, + self.CLOTH_TYPE_LEATHER: { + 'density': 0.8, + 'linear_stiffness': 0.9, + 'angular_stiffness': 0.8, + 'volume_stiffness': 0.6, + 'friction': 0.7, + 'restitution': 0.1, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.01, + 'lift_coefficient': 0.005, + 'bend_stiffness': 0.8, + 'damping': 0.015, + 'plastic_threshold': 0.2, + 'plastic_stiffness': 0.7, + 'tear_threshold': 150.0, + 'tear_resistance': 80.0, + 'anisotropic_friction': [0.7, 0.7, 0.7], + 'dynamic_friction': 0.7, + 'static_friction': 0.8, + 'rolling_friction': 0.15, + 'spinning_friction': 0.08 + }, + self.CLOTH_TYPE_RUBBER: { + 'density': 1.2, + 'linear_stiffness': 0.95, + 'angular_stiffness': 0.9, + 'volume_stiffness': 0.8, + 'friction': 0.8, + 'restitution': 0.4, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.005, + 'lift_coefficient': 0.002, + 'bend_stiffness': 0.9, + 'damping': 0.02, + 'plastic_threshold': 0.3, + 'plastic_stiffness': 0.8, + 'tear_threshold': 200.0, + 'tear_resistance': 100.0, + 'anisotropic_friction': [0.8, 0.8, 0.8], + 'dynamic_friction': 0.8, + 'static_friction': 0.9, + 'rolling_friction': 0.2, + 'spinning_friction': 0.1 + }, + self.CLOTH_TYPE_DENIM: { + 'density': 0.7, + 'linear_stiffness': 0.8, + 'angular_stiffness': 0.7, + 'volume_stiffness': 0.5, + 'friction': 0.65, + 'restitution': 0.15, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.015, + 'lift_coefficient': 0.008, + 'bend_stiffness': 0.6, + 'damping': 0.012, + 'plastic_threshold': 0.15, + 'plastic_stiffness': 0.6, + 'tear_threshold': 120.0, + 'tear_resistance': 60.0, + 'anisotropic_friction': [0.65, 0.65, 0.65], + 'dynamic_friction': 0.65, + 'static_friction': 0.75, + 'rolling_friction': 0.12, + 'spinning_friction': 0.06 + }, + self.CLOTH_TYPE_LINEN: { + 'density': 0.5, + 'linear_stiffness': 0.6, + 'angular_stiffness': 0.5, + 'volume_stiffness': 0.3, + 'friction': 0.55, + 'restitution': 0.12, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.025, + 'lift_coefficient': 0.012, + 'bend_stiffness': 0.4, + 'damping': 0.008, + 'plastic_threshold': 0.12, + 'plastic_stiffness': 0.4, + 'tear_threshold': 90.0, + 'tear_resistance': 40.0, + 'anisotropic_friction': [0.55, 0.55, 0.55], + 'dynamic_friction': 0.55, + 'static_friction': 0.65, + 'rolling_friction': 0.08, + 'spinning_friction': 0.04 + }, + self.CLOTH_TYPE_WOOL: { + 'density': 0.65, + 'linear_stiffness': 0.65, + 'angular_stiffness': 0.55, + 'volume_stiffness': 0.35, + 'friction': 0.6, + 'restitution': 0.13, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.018, + 'lift_coefficient': 0.009, + 'bend_stiffness': 0.45, + 'damping': 0.01, + 'plastic_threshold': 0.13, + 'plastic_stiffness': 0.45, + 'tear_threshold': 100.0, + 'tear_resistance': 50.0, + 'anisotropic_friction': [0.6, 0.6, 0.6], + 'dynamic_friction': 0.6, + 'static_friction': 0.7, + 'rolling_friction': 0.1, + 'spinning_friction': 0.05 + }, + self.CLOTH_TYPE_SYNTHETIC: { + 'density': 0.4, + 'linear_stiffness': 0.75, + 'angular_stiffness': 0.65, + 'volume_stiffness': 0.45, + 'friction': 0.5, + 'restitution': 0.2, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.012, + 'lift_coefficient': 0.007, + 'bend_stiffness': 0.55, + 'damping': 0.006, + 'plastic_threshold': 0.18, + 'plastic_stiffness': 0.55, + 'tear_threshold': 110.0, + 'tear_resistance': 55.0, + 'anisotropic_friction': [0.5, 0.5, 0.5], + 'dynamic_friction': 0.5, + 'static_friction': 0.6, + 'rolling_friction': 0.07, + 'spinning_friction': 0.03 + }, + self.CLOTH_TYPE_MESH: { + 'density': 0.2, + 'linear_stiffness': 0.4, + 'angular_stiffness': 0.3, + 'volume_stiffness': 0.1, + 'friction': 0.3, + 'restitution': 0.05, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.04, + 'lift_coefficient': 0.02, + 'bend_stiffness': 0.2, + 'damping': 0.003, + 'plastic_threshold': 0.08, + 'plastic_stiffness': 0.2, + 'tear_threshold': 70.0, + 'tear_resistance': 25.0, + 'anisotropic_friction': [0.3, 0.3, 0.3], + 'dynamic_friction': 0.3, + 'static_friction': 0.4, + 'rolling_friction': 0.03, + 'spinning_friction': 0.01 + }, + self.CLOTH_TYPE_CANVAS: { + 'density': 0.75, + 'linear_stiffness': 0.85, + 'angular_stiffness': 0.75, + 'volume_stiffness': 0.55, + 'friction': 0.68, + 'restitution': 0.14, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.013, + 'lift_coefficient': 0.006, + 'bend_stiffness': 0.65, + 'damping': 0.014, + 'plastic_threshold': 0.18, + 'plastic_stiffness': 0.65, + 'tear_threshold': 130.0, + 'tear_resistance': 65.0, + 'anisotropic_friction': [0.68, 0.68, 0.68], + 'dynamic_friction': 0.68, + 'static_friction': 0.78, + 'rolling_friction': 0.14, + 'spinning_friction': 0.07 + } + } + + # 风力参数 + self.wind_enabled = False + self.wind_velocity = Vec3(0, 0, 0) + self.wind_turbulence = 0.0 + self.wind_frequency = 1.0 + self.wind_phase = 0.0 + + # 性能优化参数 + self.max_simulation_steps = 10 + self.time_step = 1.0/60.0 + self.sub_steps = 4 + + def _init_soft_body_world(self): + """ + 初始化软体世界信息 + """ + try: + # 获取物理世界中的软体世界信息 + physics_world = self._get_physics_world() + if physics_world and hasattr(physics_world, 'soft_body_world_info'): + self.soft_body_world_info = physics_world.soft_body_world_info + else: + # 创建新的软体世界信息 + self.soft_body_world_info = BulletSoftBodyWorldInfo() + self.soft_body_world_info.setGravity(Vec3(0, 0, -9.81)) + self.soft_body_world_info.setAirDensity(1.225) + self.soft_body_world_info.setWaterDensity(1000.0) + self.soft_body_world_info.setWaterOffset(0.0) + self.soft_body_world_info.setWaterNormal(Vec3(0, 0, 1)) + + print("✓ 软体世界初始化完成") + + except Exception as e: + print(f"✗ 软体世界初始化失败: {e}") + + def set_wind_parameters(self, enabled: bool = True, velocity: Vec3 = None, + turbulence: float = 0.0, frequency: float = 1.0): + """ + 设置风力参数 + + Args: + enabled: 是否启用风力 + velocity: 风速向量 + turbulence: 湍流强度 + frequency: 风力变化频率 + """ + self.wind_enabled = enabled + if velocity: + self.wind_velocity = velocity + self.wind_turbulence = turbulence + self.wind_frequency = frequency + + def update_wind(self, dt: float): + """ + 更新风力参数(随时间变化) + + Args: + dt: 时间增量 + """ + if not self.wind_enabled: + return + + # 更新风力相位 + self.wind_phase += dt * self.wind_frequency * 2 * math.pi + + # 应用湍流效果 + if self.wind_turbulence > 0: + turbulence_factor = 1.0 + math.sin(self.wind_phase) * self.wind_turbulence + current_wind = self.wind_velocity * turbulence_factor + + # 更新软体世界信息 + if self.soft_body_world_info: + # 在实际实现中,这里会更新软体世界的风力参数 + pass + + def create_cloth_with_material(self, node_path, cloth_type: str, + width: float, height: float, + resolution_x: int, resolution_y: int, + behavior_mode: str = BEHAVIOR_DEFAULT) -> Optional[BulletSoftBodyNode]: + """ + 使用指定材质创建布料 + + Args: + node_path: 父节点路径 + cloth_type: 布料类型 + width: 布料宽度 + height: 布料高度 + resolution_x: X方向分辨率 + resolution_y: Y方向分辨率 + behavior_mode: 行为模式 + + Returns: + BulletSoftBodyNode对象或None + """ + try: + # 获取材质参数 + if cloth_type not in self.cloth_materials: + print(f"⚠ 未知布料类型: {cloth_type}, 使用默认参数") + params = self.cloth_materials[self.CLOTH_TYPE_COTTON] + else: + params = self.cloth_materials[cloth_type] + + # 根据行为模式调整参数 + adjusted_params = self._adjust_parameters_for_behavior(params, behavior_mode) + + # 创建软体节点 + cloth_name = f'cloth_{cloth_type}_{len(getattr(self, "_cloth_counter", [0]))}' + soft_body_node = BulletSoftBodyNode(cloth_name) + + # 设置布料参数 + self._apply_cloth_parameters(soft_body_node, adjusted_params) + + # 生成布料网格顶点 + vertices = self._generate_cloth_vertices(width, height, resolution_x, resolution_y) + + # 添加顶点到软体 + for vertex in vertices: + soft_body_node.appendNode(vertex, 1.0) + + # 生成面索引 + indices = self._generate_cloth_indices(resolution_x, resolution_y) + + # 添加面 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) + + # 添加链接 + self._add_links_from_indices(soft_body_node, indices) + + # 添加弯曲约束 + self._add_bending_constraints(soft_body_node, adjusted_params) + + # 创建布料节点路径 + cloth_np = node_path.attachNewNode(soft_body_node) + + # 添加到物理世界 + physics_world = self._get_physics_world() + if physics_world: + physics_world.attachSoftBody(soft_body_node) + + print(f"✓ 创建{cloth_type}布料: {cloth_name} ({resolution_x+1}x{resolution_y+1} 顶点)") + return soft_body_node + + except Exception as e: + print(f"✗ 创建{cloth_type}布料失败: {e}") + import traceback + traceback.print_exc() + return None + + def create_cloth_from_mesh(self, node_path, cloth_type: str, + params: Dict[str, Any] = None) -> Optional[BulletSoftBodyNode]: + """ + 从网格创建布料 + + Args: + node_path: 网格节点路径 + cloth_type: 布料类型 + params: 额外参数 + + Returns: + BulletSoftBodyNode对象或None + """ + try: + # 获取材质参数 + if cloth_type not in self.cloth_materials: + print(f"⚠ 未知布料类型: {cloth_type}, 使用默认参数") + base_params = self.cloth_materials[self.CLOTH_TYPE_COTTON] + else: + base_params = self.cloth_materials[cloth_type] + + # 合并自定义参数 + if params: + base_params.update(params) + + # 创建软体节点 + cloth_name = f'cloth_mesh_{cloth_type}_{len(getattr(self, "_cloth_counter", [0]))}' + soft_body_node = BulletSoftBodyNode(cloth_name) + + # 设置布料参数 + self._apply_cloth_parameters(soft_body_node, base_params) + + # 从网格提取数据 + vertices, indices = self._extract_mesh_data(node_path) + if not vertices or not indices: + print("⚠ 无法从网格提取顶点数据") + return None + + # 添加顶点到软体 + for vertex in vertices: + soft_body_node.appendNode(vertex, 1.0) + + # 添加面 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) + + # 添加链接 + self._add_links_from_indices(soft_body_node, indices) + + # 添加弯曲约束 + self._add_bending_constraints(soft_body_node, base_params) + + # 创建布料节点路径 + cloth_np = node_path.attachNewNode(soft_body_node) + cloth_np.setPos(node_path.getPos()) + cloth_np.setHpr(node_path.getHpr()) + + # 添加到物理世界 + physics_world = self._get_physics_world() + if physics_world: + physics_world.attachSoftBody(soft_body_node) + + print(f"✓ 从网格创建{cloth_type}布料: {cloth_name} (顶点数: {len(vertices)})") + return soft_body_node + + except Exception as e: + print(f"✗ 从网格创建{cloth_type}布料失败: {e}") + import traceback + traceback.print_exc() + return None + + def apply_wind_force(self, soft_body: BulletSoftBodyNode, wind_vector: Vec3 = None): + """ + 对软体应用风力 + + Args: + soft_body: 软体节点 + wind_vector: 风力向量(如果为None,则使用全局风力参数) + """ + try: + # 使用指定风力向量或全局风力参数 + if wind_vector is None: + wind_vector = self.wind_velocity + + # 在实际实现中,这里会应用风力到软体 + # BulletSoftBodyNode有addVelocity方法可以用于此目的 + # 通过软体世界信息设置风力参数 + if self.soft_body_world_info: + # 这里会通过软体世界信息应用风力 + pass + + print(f"✓ 对软体应用风力: {wind_vector}") + + except Exception as e: + print(f"✗ 应用风力失败: {e}") + + def apply_force_to_region(self, soft_body: BulletSoftBodyNode, + force: Vec3, region_center: Point3, + radius: float) -> bool: + """ + 对布料的特定区域应用力 + + Args: + soft_body: 软体节点 + force: 力向量 + region_center: 区域中心点 + radius: 区域半径 + + Returns: + 是否成功应用力 + """ + try: + # 获取软体节点 + nodes = soft_body.get_nodes() + + # 查找受影响的节点 + affected_nodes = [] + for i, node in enumerate(nodes): + node_pos = node.get_position() + distance = (node_pos - region_center).length() + if distance <= radius: + affected_nodes.append((i, node, distance)) + + # 应用力到受影响的节点 + force_magnitude = force.length() + for node_index, node, distance in affected_nodes: + # 根据距离衰减力的大小 + decay_factor = 1.0 - (distance / radius) + if decay_factor > 0: + applied_force = force * decay_factor + # 在实际实现中,这里会应用力到节点 + # node.add_velocity(applied_force) + + print(f"✓ 对布料区域应用力: {len(affected_nodes)} 个节点受影响") + return True + + except Exception as e: + print(f"✗ 应用力到区域失败: {e}") + return False + + def simulate_tear(self, soft_body: BulletSoftBodyNode, + tear_point: Point3, radius: float) -> bool: + """ + 模拟布料撕裂 + + Args: + soft_body: 软体节点 + tear_point: 撕裂点 + radius: 撕裂半径 + + Returns: + 是否成功模拟撕裂 + """ + try: + # 获取软体节点 + nodes = soft_body.get_nodes() + + # 查找撕裂区域内的节点 + tear_nodes = [] + for i, node in enumerate(nodes): + node_pos = node.get_position() + distance = (node_pos - tear_point).length() + if distance <= radius: + tear_nodes.append((i, node, distance)) + + if not tear_nodes: + print("⚠ 没有找到可撕裂的节点") + return False + + # 在实际实现中,这里会执行撕裂操作 + # BulletSoftBodyNode目前不直接支持撕裂,需要通过其他方式实现 + tear_count = len(tear_nodes) + + print(f"✓ 模拟布料撕裂: {tear_count} 个节点受影响") + return True + + except Exception as e: + print(f"✗ 模拟撕裂失败: {e}") + return False + + def set_cloth_parameters(self, soft_body: BulletSoftBodyNode, params: Dict[str, Any]): + """ + 设置布料参数 + + Args: + soft_body: 软体节点 + params: 参数字典 + """ + try: + self._apply_cloth_parameters(soft_body, params) + print("✓ 设置布料参数完成") + + except Exception as e: + print(f"✗ 设置布料参数失败: {e}") + + def get_material_parameters(self, cloth_type: str) -> Optional[Dict[str, Any]]: + """ + 获取指定材质的参数 + + Args: + cloth_type: 布料类型 + + Returns: + 参数字典或None + """ + return self.cloth_materials.get(cloth_type) + + def get_all_material_types(self) -> List[str]: + """ + 获取所有可用的布料材质类型 + + Returns: + 材质类型列表 + """ + return list(self.cloth_materials.keys()) + + def update_soft_body_world_info(self, air_density: float = None, + water_density: float = None, + water_offset: float = None, + water_normal: Vec3 = None, + gravity: Vec3 = None): + """ + 更新软体世界信息 + + Args: + air_density: 空气密度 + water_density: 水密度 + water_offset: 水偏移 + water_normal: 水法线 + gravity: 重力向量 + """ + try: + if not self.soft_body_world_info: + print("⚠ 软体世界信息未初始化") + return + + if air_density is not None: + self.soft_body_world_info.setAirDensity(air_density) + if water_density is not None: + self.soft_body_world_info.setWaterDensity(water_density) + if water_offset is not None: + self.soft_body_world_info.setWaterOffset(water_offset) + if water_normal is not None: + self.soft_body_world_info.setWaterNormal(water_normal) + if gravity is not None: + self.soft_body_world_info.setGravity(gravity) + + print("✓ 更新软体世界信息完成") + + except Exception as e: + print(f"✗ 更新软体世界信息失败: {e}") + + def calculate_cloth_properties(self, vertices: List[Point3], indices: List[int]) -> Dict[str, Any]: + """ + 计算布料属性 + + Args: + vertices: 顶点列表 + indices: 索引列表 + + Returns: + 属性字典 + """ + try: + properties = { + 'vertex_count': len(vertices), + 'face_count': len(indices) // 3, + 'edge_count': 0, + 'surface_area': 0.0, + 'volume': 0.0, + 'center_of_mass': Point3(0, 0, 0), + 'bounding_box': { + 'min': Point3(float('inf'), float('inf'), float('inf')), + 'max': Point3(float('-inf'), float('-inf'), float('-inf')) + } + } + + if not vertices: + return properties + + # 计算包围盒 + for vertex in vertices: + properties['bounding_box']['min'].set( + min(properties['bounding_box']['min'][0], vertex[0]), + min(properties['bounding_box']['min'][1], vertex[1]), + min(properties['bounding_box']['min'][2], vertex[2]) + ) + properties['bounding_box']['max'].set( + max(properties['bounding_box']['max'][0], vertex[0]), + max(properties['bounding_box']['max'][1], vertex[1]), + max(properties['bounding_box']['max'][2], vertex[2]) + ) + + # 计算质心 + center = Point3(0, 0, 0) + for vertex in vertices: + center += vertex + center /= len(vertices) + properties['center_of_mass'] = center + + # 计算表面积 + total_area = 0.0 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + v0 = vertices[indices[i]] + v1 = vertices[indices[i+1]] + v2 = vertices[indices[i+2]] + # 计算三角形面积 + edge1 = v1 - v0 + edge2 = v2 - v0 + cross = edge1.cross(edge2) + area = cross.length() * 0.5 + total_area += area + + properties['surface_area'] = total_area + + return properties + + except Exception as e: + print(f"✗ 计算布料属性失败: {e}") + return {} + + def _apply_cloth_parameters(self, soft_body: BulletSoftBodyNode, params: Dict[str, Any]): + """ + 应用布料参数到软体节点 + + Args: + soft_body: 软体节点 + params: 参数字典 + """ + cfg = soft_body.get_cfg() + + # 物理参数 + if 'linear_stiffness' in params: + cfg.set_kDF(params['linear_stiffness']) + if 'angular_stiffness' in params: + cfg.set_kDG(params['angular_stiffness']) + if 'volume_stiffness' in params: + cfg.set_kVC(params['volume_stiffness']) + if 'pressure_coefficient' in params: + cfg.set_kPR(params['pressure_coefficient']) + if 'drag_coefficient' in params: + cfg.set_kDG(params['drag_coefficient']) + if 'lift_coefficient' in params: + cfg.set_kLF(params['lift_coefficient']) + if 'bend_stiffness' in params: + cfg.set_kDF(params['bend_stiffness']) # 弯曲刚度 + if 'damping' in params: + cfg.set_dft_damping(params['damping']) + if 'plastic_threshold' in params: + cfg.set_kMT(params['plastic_threshold']) # 塑性阈值 + if 'plastic_stiffness' in params: + cfg.set_kDP(params['plastic_stiffness']) # 塑性刚度 + + # 材质属性 + if 'friction' in params: + soft_body.setFriction(params['friction']) + if 'restitution' in params: + soft_body.setRestitution(params['restitution']) + if 'density' in params: + soft_body.setTotalMass(params['density']) + if 'anisotropic_friction' in params: + friction_vec = params['anisotropic_friction'] + if len(friction_vec) >= 3: + soft_body.setAnisotropicFriction( + Vec3(friction_vec[0], friction_vec[1], friction_vec[2]) + ) + + # 高级摩擦参数 + if 'dynamic_friction' in params: + # 在实际实现中,这里会设置动态摩擦系数 + pass + if 'static_friction' in params: + # 在实际实现中,这里会设置静态摩擦系数 + pass + if 'rolling_friction' in params: + # 在实际实现中,这里会设置滚动摩擦系数 + pass + if 'spinning_friction' in params: + # 在实际实现中,这里会设置自旋摩擦系数 + pass + + def _adjust_parameters_for_behavior(self, params: Dict[str, Any], behavior_mode: str) -> Dict[str, Any]: + """ + 根据行为模式调整参数 + + Args: + params: 原始参数 + behavior_mode: 行为模式 + + Returns: + 调整后的参数 + """ + adjusted_params = params.copy() + + if behavior_mode == self.BEHAVIOR_STIFF: + adjusted_params['linear_stiffness'] *= 1.5 + adjusted_params['angular_stiffness'] *= 1.5 + adjusted_params['volume_stiffness'] *= 1.5 + adjusted_params['bend_stiffness'] *= 1.5 + elif behavior_mode == self.BEHAVIOR_FLEXIBLE: + adjusted_params['linear_stiffness'] *= 0.7 + adjusted_params['angular_stiffness'] *= 0.7 + adjusted_params['volume_stiffness'] *= 0.7 + adjusted_params['bend_stiffness'] *= 0.7 + elif behavior_mode == self.BEHAVIOR_ELASTIC: + adjusted_params['linear_stiffness'] *= 1.2 + adjusted_params['restitution'] = min(1.0, adjusted_params['restitution'] * 1.5) + + return adjusted_params + + def _generate_cloth_vertices(self, width: float, height: float, + resolution_x: int, resolution_y: int) -> List[Point3]: + """ + 生成布料顶点 + + Args: + width: 宽度 + height: 高度 + resolution_x: X分辨率 + resolution_y: Y分辨率 + + Returns: + 顶点列表 + """ + vertices = [] + half_width = width / 2.0 + half_height = height / 2.0 + + for j in range(resolution_y + 1): + for i in range(resolution_x + 1): + x = (i / resolution_x) * width - half_width + y = (j / resolution_y) * height - half_height + z = 0.0 + vertices.append(Point3(x, y, z)) + + return vertices + + def _generate_cloth_indices(self, resolution_x: int, resolution_y: int) -> List[int]: + """ + 生成布料索引 + + Args: + resolution_x: X分辨率 + resolution_y: Y分辨率 + + Returns: + 索引列表 + """ + indices = [] + + for j in range(resolution_y): + for i in range(resolution_x): + v0 = j * (resolution_x + 1) + i + v1 = v0 + 1 + v2 = (j + 1) * (resolution_x + 1) + i + v3 = v2 + 1 + + indices.extend([v0, v1, v2, v1, v3, v2]) + + return indices + + def _add_links_from_indices(self, soft_body: BulletSoftBodyNode, indices: List[int]): + """ + 根据索引添加链接 + + Args: + soft_body: 软体节点 + indices: 索引列表 + """ + try: + # 创建边集合以避免重复 + edges = set() + + # 从三角形面提取边 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + # 三角形的三条边 + edges.add(tuple(sorted([indices[i], indices[i+1]]))) + edges.add(tuple(sorted([indices[i+1], indices[i+2]]))) + edges.add(tuple(sorted([indices[i+2], indices[i]]))) + + # 添加链接 + for edge in edges: + soft_body.appendLink(edge[0], edge[1]) + + except Exception as e: + print(f"⚠ 添加链接失败: {e}") + + def _add_bending_constraints(self, soft_body: BulletSoftBodyNode, params: Dict[str, Any]): + """ + 添加弯曲约束 + + Args: + soft_body: 软体节点 + params: 参数字典 + """ + try: + # 添加弯曲约束 + # 这个参数控制弯曲约束的生成步长 + soft_body.generateBendingConstraints(2) + + except Exception as e: + print(f"⚠ 添加弯曲约束失败: {e}") + + def _extract_mesh_data(self, node_path) -> Tuple[List[Point3], List[int]]: + """ + 从节点路径提取网格数据 + + Args: + node_path: 节点路径 + + Returns: + (顶点列表, 索引列表) + """ + try: + vertices = [] + indices = [] + + # 查找几何节点 + geom_nodes = node_path.findAllMatches('**/+GeomNode') + for geom_node_path in geom_nodes: + geom_node = geom_node_path.node() + for i in range(geom_node.getNumGeoms()): + geom = geom_node.getGeom(i) + vdata = geom.getVertexData() + + # 提取顶点 + vertex_reader = GeomVertexReader(vdata, 'vertex') + while not vertex_reader.isAtEnd(): + vertices.append(vertex_reader.getData3()) + + # 提取索引 + for prim in geom.getPrimitives(): + index_reader = prim.getIndexIterator() + while not index_reader.isAtEnd(): + indices.append(index_reader.getData()) + index_reader.next() + + return vertices, indices + + except Exception as e: + print(f"⚠ 提取网格数据失败: {e}") + return [], [] + + def _get_physics_world(self): + """ + 获取物理世界对象 + + Returns: + 物理世界对象或None + """ + try: + # 尝试从世界对象获取物理世界 + if hasattr(self.world, 'physics_world'): + return self.world.physics_world.bullet_world + elif hasattr(self.world, 'bullet_world'): + return self.world.bullet_world + else: + # 查找物理插件 + physics_plugin = self.world.plugins.get('rigid_body_physics') + if physics_plugin and hasattr(physics_plugin, 'physics_world'): + return physics_plugin.physics_world.bullet_world + except Exception as e: + print(f"⚠ 获取物理世界失败: {e}") + return None \ No newline at end of file diff --git a/plugins/user/soft_body_cloth_physics/core/soft_body_manager.py b/plugins/user/soft_body_cloth_physics/core/soft_body_manager.py index 87b24e3c..145f1280 100644 --- a/plugins/user/soft_body_cloth_physics/core/soft_body_manager.py +++ b/plugins/user/soft_body_cloth_physics/core/soft_body_manager.py @@ -1,1557 +1,1557 @@ -""" -软体管理器 -负责软体对象的创建、管理和更新 -包含完整的软体物理模拟功能 -""" - -from panda3d.core import Vec3, Point3, GeomVertexReader, GeomVertexFormat -from panda3d.core import GeomVertexData, Geom, GeomNode, NodePath -from panda3d.bullet import BulletSoftBodyNode, BulletSoftBodyWorldInfo -import math -from typing import List, Dict, Optional, Any, Tuple -import numpy as np - - -class SoftBodyManager: - """ - 软体管理器 - 管理场景中的所有软体对象,提供完整的软体物理模拟功能 - """ - - # 软体类型常量 - SOFT_TYPE_SPHERE = "sphere" - SOFT_TYPE_BOX = "box" - SOFT_TYPE_CAPSULE = "capsule" - SOFT_TYPE_CYLINDER = "cylinder" - SOFT_TYPE_MESH = "mesh" - SOFT_TYPE_ROPE = "rope" - SOFT_TYPE_PATCH = "patch" - - # 软体行为模式 - BEHAVIOR_DEFAULT = "default" - BEHAVIOR_RIGID = "rigid" - BEHAVIOR_SOFT = "soft" - BEHAVIOR_FLUID = "fluid" - - # 撕裂和碰撞模式 - TEAR_MODE_NONE = "none" - TEAR_MODE_VERTEX = "vertex" - TEAR_MODE_EDGE = "edge" - COLLISION_MODE_NONE = "none" - COLLISION_MODE_SOFT = "soft" - COLLISION_MODE_RIGID = "rigid" - - def __init__(self, world): - self.world = world - self.soft_body_objects: List[Dict[str, Any]] = [] - self.enabled = False - self.gravity = Vec3(0, 0, -9.81) - - # 软体默认参数 - self.default_params = { - 'density': 1.0, - 'linear_stiffness': 0.8, - 'angular_stiffness': 0.8, - 'volume_stiffness': 0.8, - 'friction': 0.5, - 'restitution': 0.2, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.01, - 'lift_coefficient': 0.01, - 'soft_type': 'generic', - 'behavior_mode': 'default', - 'tear_mode': 'none', - 'tear_threshold': 100.0, - 'tear_resistance': 50.0, - 'collision_mode': 'soft', - 'bending_constraints': True, - 'clusters': 0, - 'cluster_hardness': 1.0, - 'position_iteration': 2, - 'drift_iteration': 2, - 'cluster_iteration': 2, - 'velocity_iteration': 2, - 'mass_distribution': 'uniform', # uniform, proportional, custom - 'pressure': 0.0, - 'volume_conservation': 0.0, - 'damping': 0.01, - 'self_collision': True - } - - # 性能参数 - self.max_soft_body_objects = 100 - self.soft_body_update_frequency = 60 # Hz - self.performance_stats = { - 'total_updates': 0, - 'collision_count': 0, - 'tear_count': 0, - 'memory_usage': 0 - } - - def enable(self): - """ - 启用软体管理器 - """ - self.enabled = True - print("✓ 软体管理器已启用") - - def disable(self): - """ - 禁用软体管理器 - """ - self.enabled = False - print("✓ 软体管理器已禁用") - - def set_gravity(self, gravity: Vec3): - """ - 设置重力 - - Args: - gravity: 重力向量 - """ - self.gravity = gravity - - def create_soft_body_from_mesh(self, node_path, params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: - """ - 从网格创建软体对象 - - Args: - node_path: 网格节点路径 - params: 软体参数 - - Returns: - 软体对象信息字典 - """ - if not self.enabled: - print("⚠ 软体管理器未启用") - return None - - if len(self.soft_body_objects) >= self.max_soft_body_objects: - print("⚠ 达到最大软体对象数量限制") - return None - - try: - # 合并参数 - soft_params = self.default_params.copy() - if params: - soft_params.update(params) - - # 创建软体节点 - soft_name = f'soft_body_{len(self.soft_body_objects)}' - soft_body_node = BulletSoftBodyNode(soft_name) - - # 设置软体参数 - self._apply_soft_body_parameters(soft_body_node, soft_params) - - # 从网格提取数据 - vertices, indices = self._extract_mesh_data(node_path) - if not vertices or not indices: - print("⚠ 无法从网格提取顶点数据") - return None - - # 添加顶点 - for vertex in vertices: - soft_body_node.appendNode(vertex, 1.0) - - # 添加面 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) - - # 添加链接 - self._add_links_from_indices(soft_body_node, indices) - - # 添加弯曲约束 - if soft_params.get('bending_constraints', True): - self._add_bending_constraints(soft_body_node) - - # 创建软体节点路径 - soft_np = node_path.attachNewNode(soft_body_node) - soft_np.setPos(node_path.getPos()) - soft_np.setHpr(node_path.getHpr()) - - # 添加到物理世界 - physics_world = self._get_physics_world() - if physics_world: - physics_world.attachSoftBody(soft_body_node) - - # 计算质量分布 - mass_distribution = self._calculate_mass_distribution(vertices, soft_params) - - # 保存软体信息 - soft_info = { - 'node_path': soft_np, - 'soft_body': soft_body_node, - 'original_node': node_path, - 'params': soft_params, - 'type': 'soft_mesh', - 'behavior_mode': soft_params.get('behavior_mode', 'default'), - 'vertices': vertices, - 'indices': indices, - 'mass_distribution': mass_distribution, - 'tears': [], - 'collisions': [], - 'constraints': [], - 'creation_time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0, - 'last_update_time': 0.0, - 'performance_stats': { - 'update_count': 0, - 'collision_count': 0, - 'tear_count': 0, - 'memory_usage': 0 - } - } - - self.soft_body_objects.append(soft_info) - - print(f"✓ 创建软体对象: {soft_name} (顶点数: {len(vertices)}, 面数: {len(indices)//3})") - return soft_info - - except Exception as e: - print(f"✗ 创建软体对象失败: {e}") - import traceback - traceback.print_exc() - return None - - def create_soft_body_sphere(self, node_path, radius: float, - resolution: int = 8, - params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: - """ - 创建球形软体 - - Args: - node_path: 父节点路径 - radius: 球体半径 - resolution: 分辨率 - params: 软体参数 - - Returns: - 软体对象信息字典 - """ - if not self.enabled: - print("⚠ 软体管理器未启用") - return None - - if len(self.soft_body_objects) >= self.max_soft_body_objects: - print("⚠ 达到最大软体对象数量限制") - return None - - try: - # 合并参数 - soft_params = self.default_params.copy() - if params: - soft_params.update(params) - - # 创建软体节点 - soft_name = f'soft_sphere_{len(self.soft_body_objects)}' - soft_body_node = BulletSoftBodyNode(soft_name) - - # 设置软体参数 - self._apply_soft_body_parameters(soft_body_node, soft_params) - - # 生成球形网格 - vertices, indices = self._generate_sphere_mesh(radius, resolution) - - # 添加顶点 - for vertex in vertices: - soft_body_node.appendNode(vertex, 1.0) - - # 添加面 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) - - # 添加链接 - self._add_links_from_indices(soft_body_node, indices) - - # 添加弯曲约束 - if soft_params.get('bending_constraints', True): - self._add_bending_constraints(soft_body_node) - - # 创建软体节点路径 - soft_np = node_path.attachNewNode(soft_body_node) - - # 添加到物理世界 - physics_world = self._get_physics_world() - if physics_world: - physics_world.attachSoftBody(soft_body_node) - - # 计算质量分布 - mass_distribution = self._calculate_mass_distribution(vertices, soft_params) - - # 保存软体信息 - soft_info = { - 'node_path': soft_np, - 'soft_body': soft_body_node, - 'original_node': node_path, - 'params': soft_params, - 'type': 'soft_sphere', - 'radius': radius, - 'resolution': resolution, - 'behavior_mode': soft_params.get('behavior_mode', 'default'), - 'vertices': vertices, - 'indices': indices, - 'mass_distribution': mass_distribution, - 'tears': [], - 'collisions': [], - 'constraints': [], - 'creation_time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0, - 'last_update_time': 0.0, - 'performance_stats': { - 'update_count': 0, - 'collision_count': 0, - 'tear_count': 0, - 'memory_usage': 0 - } - } - - self.soft_body_objects.append(soft_info) - - print(f"✓ 创建球形软体: {soft_name} (半径: {radius}, 分辨率: {resolution})") - return soft_info - - except Exception as e: - print(f"✗ 创建球形软体失败: {e}") - import traceback - traceback.print_exc() - return None - - def create_soft_body_box(self, node_path, width: float, height: float, depth: float, - resolution: int = 4, - params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: - """ - 创建盒形软体 - - Args: - node_path: 父节点路径 - width: 宽度 - height: 高度 - depth: 深度 - resolution: 分辨率 - params: 软体参数 - - Returns: - 软体对象信息字典 - """ - if not self.enabled: - print("⚠ 软体管理器未启用") - return None - - if len(self.soft_body_objects) >= self.max_soft_body_objects: - print("⚠ 达到最大软体对象数量限制") - return None - - try: - # 合并参数 - soft_params = self.default_params.copy() - if params: - soft_params.update(params) - - # 创建软体节点 - soft_name = f'soft_box_{len(self.soft_body_objects)}' - soft_body_node = BulletSoftBodyNode(soft_name) - - # 设置软体参数 - self._apply_soft_body_parameters(soft_body_node, soft_params) - - # 生成盒形网格 - vertices, indices = self._generate_box_mesh(width, height, depth, resolution) - - # 添加顶点 - for vertex in vertices: - soft_body_node.appendNode(vertex, 1.0) - - # 添加面 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) - - # 添加链接 - self._add_links_from_indices(soft_body_node, indices) - - # 添加弯曲约束 - if soft_params.get('bending_constraints', True): - self._add_bending_constraints(soft_body_node) - - # 创建软体节点路径 - soft_np = node_path.attachNewNode(soft_body_node) - - # 添加到物理世界 - physics_world = self._get_physics_world() - if physics_world: - physics_world.attachSoftBody(soft_body_node) - - # 计算质量分布 - mass_distribution = self._calculate_mass_distribution(vertices, soft_params) - - # 保存软体信息 - soft_info = { - 'node_path': soft_np, - 'soft_body': soft_body_node, - 'original_node': node_path, - 'params': soft_params, - 'type': 'soft_box', - 'width': width, - 'height': height, - 'depth': depth, - 'resolution': resolution, - 'behavior_mode': soft_params.get('behavior_mode', 'default'), - 'vertices': vertices, - 'indices': indices, - 'mass_distribution': mass_distribution, - 'tears': [], - 'collisions': [], - 'constraints': [], - 'creation_time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0, - 'last_update_time': 0.0, - 'performance_stats': { - 'update_count': 0, - 'collision_count': 0, - 'tear_count': 0, - 'memory_usage': 0 - } - } - - self.soft_body_objects.append(soft_info) - - print(f"✓ 创建盒形软体: {soft_name} ({width}x{height}x{depth})") - return soft_info - - except Exception as e: - print(f"✗ 创建盒形软体失败: {e}") - import traceback - traceback.print_exc() - return None - - def create_soft_body_capsule(self, node_path, radius: float, height: float, - resolution: int = 8, - params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: - """ - 创建胶囊形软体 - - Args: - node_path: 父节点路径 - radius: 半径 - height: 高度 - resolution: 分辨率 - params: 软体参数 - - Returns: - 软体对象信息字典 - """ - if not self.enabled: - print("⚠ 软体管理器未启用") - return None - - if len(self.soft_body_objects) >= self.max_soft_body_objects: - print("⚠ 达到最大软体对象数量限制") - return None - - try: - # 合并参数 - soft_params = self.default_params.copy() - if params: - soft_params.update(params) - - # 创建软体节点 - soft_name = f'soft_capsule_{len(self.soft_body_objects)}' - soft_body_node = BulletSoftBodyNode(soft_name) - - # 设置软体参数 - self._apply_soft_body_parameters(soft_body_node, soft_params) - - # 生成胶囊形网格 - vertices, indices = self._generate_capsule_mesh(radius, height, resolution) - - # 添加顶点 - for vertex in vertices: - soft_body_node.appendNode(vertex, 1.0) - - # 添加面 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) - - # 添加链接 - self._add_links_from_indices(soft_body_node, indices) - - # 添加弯曲约束 - if soft_params.get('bending_constraints', True): - self._add_bending_constraints(soft_body_node) - - # 创建软体节点路径 - soft_np = node_path.attachNewNode(soft_body_node) - - # 添加到物理世界 - physics_world = self._get_physics_world() - if physics_world: - physics_world.attachSoftBody(soft_body_node) - - # 计算质量分布 - mass_distribution = self._calculate_mass_distribution(vertices, soft_params) - - # 保存软体信息 - soft_info = { - 'node_path': soft_np, - 'soft_body': soft_body_node, - 'original_node': node_path, - 'params': soft_params, - 'type': 'soft_capsule', - 'radius': radius, - 'height': height, - 'resolution': resolution, - 'behavior_mode': soft_params.get('behavior_mode', 'default'), - 'vertices': vertices, - 'indices': indices, - 'mass_distribution': mass_distribution, - 'tears': [], - 'collisions': [], - 'constraints': [], - 'creation_time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0, - 'last_update_time': 0.0, - 'performance_stats': { - 'update_count': 0, - 'collision_count': 0, - 'tear_count': 0, - 'memory_usage': 0 - } - } - - self.soft_body_objects.append(soft_info) - - print(f"✓ 创建胶囊形软体: {soft_name} (半径: {radius}, 高度: {height})") - return soft_info - - except Exception as e: - print(f"✗ 创建胶囊形软体失败: {e}") - import traceback - traceback.print_exc() - return None - - def create_soft_body_cylinder(self, node_path, radius: float, height: float, - resolution: int = 8, - params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: - """ - 创建圆柱形软体 - - Args: - node_path: 父节点路径 - radius: 半径 - height: 高度 - resolution: 分辨率 - params: 软体参数 - - Returns: - 软体对象信息字典 - """ - if not self.enabled: - print("⚠ 软体管理器未启用") - return None - - if len(self.soft_body_objects) >= self.max_soft_body_objects: - print("⚠ 达到最大软体对象数量限制") - return None - - try: - # 合并参数 - soft_params = self.default_params.copy() - if params: - soft_params.update(params) - - # 创建软体节点 - soft_name = f'soft_cylinder_{len(self.soft_body_objects)}' - soft_body_node = BulletSoftBodyNode(soft_name) - - # 设置软体参数 - self._apply_soft_body_parameters(soft_body_node, soft_params) - - # 生成圆柱形网格 - vertices, indices = self._generate_cylinder_mesh(radius, height, resolution) - - # 添加顶点 - for vertex in vertices: - soft_body_node.appendNode(vertex, 1.0) - - # 添加面 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) - - # 添加链接 - self._add_links_from_indices(soft_body_node, indices) - - # 添加弯曲约束 - if soft_params.get('bending_constraints', True): - self._add_bending_constraints(soft_body_node) - - # 创建软体节点路径 - soft_np = node_path.attachNewNode(soft_body_node) - - # 添加到物理世界 - physics_world = self._get_physics_world() - if physics_world: - physics_world.attachSoftBody(soft_body_node) - - # 计算质量分布 - mass_distribution = self._calculate_mass_distribution(vertices, soft_params) - - # 保存软体信息 - soft_info = { - 'node_path': soft_np, - 'soft_body': soft_body_node, - 'original_node': node_path, - 'params': soft_params, - 'type': 'soft_cylinder', - 'radius': radius, - 'height': height, - 'resolution': resolution, - 'behavior_mode': soft_params.get('behavior_mode', 'default'), - 'vertices': vertices, - 'indices': indices, - 'mass_distribution': mass_distribution, - 'tears': [], - 'collisions': [], - 'constraints': [], - 'creation_time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0, - 'last_update_time': 0.0, - 'performance_stats': { - 'update_count': 0, - 'collision_count': 0, - 'tear_count': 0, - 'memory_usage': 0 - } - } - - self.soft_body_objects.append(soft_info) - - print(f"✓ 创建圆柱形软体: {soft_name} (半径: {radius}, 高度: {height})") - return soft_info - - except Exception as e: - print(f"✗ 创建圆柱形软体失败: {e}") - import traceback - traceback.print_exc() - return None - - def create_soft_body_rope(self, node_path, point_a: Point3, point_b: Point3, - resolution: int = 10, - params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: - """ - 创建绳索软体 - - Args: - node_path: 父节点路径 - point_a: 起点 - point_b: 终点 - resolution: 分辨率 - params: 软体参数 - - Returns: - 软体对象信息字典 - """ - if not self.enabled: - print("⚠ 软体管理器未启用") - return None - - if len(self.soft_body_objects) >= self.max_soft_body_objects: - print("⚠ 达到最大软体对象数量限制") - return None - - try: - # 合并参数 - soft_params = self.default_params.copy() - if params: - soft_params.update(params) - - # 创建软体节点 - soft_name = f'soft_rope_{len(self.soft_body_objects)}' - soft_body_node = BulletSoftBodyNode(soft_name) - - # 设置软体参数 - self._apply_soft_body_parameters(soft_body_node, soft_params) - - # 生成绳索顶点 - vertices = self._generate_rope_vertices(point_a, point_b, resolution) - indices = self._generate_rope_indices(resolution) - - # 添加顶点 - for vertex in vertices: - soft_body_node.appendNode(vertex, 1.0) - - # 添加链接 - for i in range(len(indices) - 1): - soft_body_node.appendLink(indices[i], indices[i+1]) - - # 创建软体节点路径 - soft_np = node_path.attachNewNode(soft_body_node) - - # 添加到物理世界 - physics_world = self._get_physics_world() - if physics_world: - physics_world.attachSoftBody(soft_body_node) - - # 计算质量分布 - mass_distribution = self._calculate_mass_distribution(vertices, soft_params) - - # 保存软体信息 - soft_info = { - 'node_path': soft_np, - 'soft_body': soft_body_node, - 'original_node': node_path, - 'params': soft_params, - 'type': 'soft_rope', - 'point_a': point_a, - 'point_b': point_b, - 'resolution': resolution, - 'behavior_mode': soft_params.get('behavior_mode', 'default'), - 'vertices': vertices, - 'indices': indices, - 'mass_distribution': mass_distribution, - 'tears': [], - 'collisions': [], - 'constraints': [], - 'creation_time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0, - 'last_update_time': 0.0, - 'performance_stats': { - 'update_count': 0, - 'collision_count': 0, - 'tear_count': 0, - 'memory_usage': 0 - } - } - - self.soft_body_objects.append(soft_info) - - print(f"✓ 创建绳索软体: {soft_name} (长度: {(point_b - point_a).length()})") - return soft_info - - except Exception as e: - print(f"✗ 创建绳索软体失败: {e}") - import traceback - traceback.print_exc() - return None - - def create_soft_body_patch(self, node_path, width: float, height: float, - resolution_x: int, resolution_y: int, - params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: - """ - 创建布料片软体 - - Args: - node_path: 父节点路径 - width: 宽度 - height: 高度 - resolution_x: X分辨率 - resolution_y: Y分辨率 - params: 软体参数 - - Returns: - 软体对象信息字典 - """ - if not self.enabled: - print("⚠ 软体管理器未启用") - return None - - if len(self.soft_body_objects) >= self.max_soft_body_objects: - print("⚠ 达到最大软体对象数量限制") - return None - - try: - # 合并参数 - soft_params = self.default_params.copy() - if params: - soft_params.update(params) - - # 创建软体节点 - soft_name = f'soft_patch_{len(self.soft_body_objects)}' - soft_body_node = BulletSoftBodyNode(soft_name) - - # 设置软体参数 - self._apply_soft_body_parameters(soft_body_node, soft_params) - - # 生成布料片顶点 - vertices, indices = self._generate_patch_mesh(width, height, resolution_x, resolution_y) - - # 添加顶点 - for vertex in vertices: - soft_body_node.appendNode(vertex, 1.0) - - # 添加面 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) - - # 添加链接 - self._add_links_from_indices(soft_body_node, indices) - - # 添加弯曲约束 - if soft_params.get('bending_constraints', True): - self._add_bending_constraints(soft_body_node) - - # 创建软体节点路径 - soft_np = node_path.attachNewNode(soft_body_node) - - # 添加到物理世界 - physics_world = self._get_physics_world() - if physics_world: - physics_world.attachSoftBody(soft_body_node) - - # 计算质量分布 - mass_distribution = self._calculate_mass_distribution(vertices, soft_params) - - # 保存软体信息 - soft_info = { - 'node_path': soft_np, - 'soft_body': soft_body_node, - 'original_node': node_path, - 'params': soft_params, - 'type': 'soft_patch', - 'width': width, - 'height': height, - 'resolution_x': resolution_x, - 'resolution_y': resolution_y, - 'behavior_mode': soft_params.get('behavior_mode', 'default'), - 'vertices': vertices, - 'indices': indices, - 'mass_distribution': mass_distribution, - 'tears': [], - 'collisions': [], - 'constraints': [], - 'creation_time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0, - 'last_update_time': 0.0, - 'performance_stats': { - 'update_count': 0, - 'collision_count': 0, - 'tear_count': 0, - 'memory_usage': 0 - } - } - - self.soft_body_objects.append(soft_info) - - print(f"✓ 创建布料片软体: {soft_name} ({resolution_x+1}x{resolution_y+1} 顶点)") - return soft_info - - except Exception as e: - print(f"✗ 创建布料片软体失败: {e}") - import traceback - traceback.print_exc() - return None - - def add_constraint(self, soft_info1: Dict[str, Any], soft_info2: Dict[str, Any], - constraint_type: str = "link", - params: Dict[str, Any] = None) -> bool: - """ - 在两个软体之间添加约束 - - Args: - soft_info1: 第一个软体信息 - soft_info2: 第二个软体信息 - constraint_type: 约束类型 - params: 约束参数 - - Returns: - 是否成功添加约束 - """ - try: - # 在实际实现中,这里会添加软体间的约束 - # BulletSoftBodyNode提供多种约束类型 - print(f"✓ 在软体间添加{constraint_type}约束") - return True - - except Exception as e: - print(f"✗ 添加约束失败: {e}") - return False - - def apply_force_to_soft_body(self, soft_info: Dict[str, Any], force: Vec3, - position: Point3 = None) -> bool: - """ - 对软体应用力 - - Args: - soft_info: 软体对象信息 - force: 力向量 - position: 应力位置(如果为None,则应用到质心) - - Returns: - 是否成功应用力 - """ - try: - soft_body = soft_info['soft_body'] - - # 在实际实现中,这里会应用力到软体 - # BulletSoftBodyNode有多种力应用方法 - print(f"✓ 对软体应用力: {force}") - return True - - except Exception as e: - print(f"✗ 应用力失败: {e}") - return False - - def remove_soft_body(self, soft_info: Dict[str, Any]) -> bool: - """ - 移除软体对象 - - Args: - soft_info: 软体对象信息 - - Returns: - 是否成功移除 - """ - try: - if soft_info not in self.soft_body_objects: - print("⚠ 软体对象不存在") - return False - - # 从物理世界移除 - physics_world = self._get_physics_world() - if physics_world: - physics_world.removeSoftBody(soft_info['soft_body']) - - # 移除节点 - if soft_info['node_path'] and not soft_info['node_path'].isEmpty(): - soft_info['node_path'].removeNode() - - # 从列表移除 - self.soft_body_objects.remove(soft_info) - - print("✓ 移除软体对象") - return True - - except Exception as e: - print(f"✗ 移除软体对象失败: {e}") - return False - - def update_soft_body_params(self, soft_info: Dict[str, Any], params: Dict[str, Any]) -> bool: - """ - 更新软体参数 - - Args: - soft_info: 软体对象信息 - params: 新参数 - - Returns: - 是否成功更新 - """ - try: - # 更新参数 - soft_info['params'].update(params) - - # 应用参数到软体节点 - soft_body = soft_info['soft_body'] - self._apply_soft_body_parameters(soft_body, soft_info['params']) - - print("✓ 更新软体参数") - return True - - except Exception as e: - print(f"✗ 更新软体参数失败: {e}") - return False - - def update_all_soft_bodies(self, dt: float): - """ - 更新所有软体对象 - - Args: - dt: 时间增量 - """ - if not self.enabled: - return - - current_time = self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0 - - for soft_info in self.soft_body_objects: - # 检查更新频率 - if current_time - soft_info['last_update_time'] < 1.0 / self.soft_body_update_frequency: - continue - - # 更新性能统计 - soft_info['performance_stats']['update_count'] += 1 - soft_info['last_update_time'] = current_time - - # 检查撕裂 - self._check_tear_conditions(soft_info) - - # 更新碰撞统计 - self._update_collision_stats(soft_info) - - # 更新整体性能统计 - self.performance_stats['total_updates'] += len(self.soft_body_objects) - - def get_soft_body_stats(self) -> Dict[str, Any]: - """ - 获取软体统计信息 - - Returns: - 统计信息字典 - """ - total_vertices = 0 - total_faces = 0 - total_collisions = 0 - total_tears = 0 - - for soft_info in self.soft_body_objects: - total_vertices += len(soft_info.get('vertices', [])) - total_faces += len(soft_info.get('indices', [])) // 3 - total_collisions += soft_info['performance_stats'].get('collision_count', 0) - total_tears += soft_info['performance_stats'].get('tear_count', 0) - - return { - 'total_soft_body_objects': len(self.soft_body_objects), - 'total_vertices': total_vertices, - 'total_faces': total_faces, - 'total_collisions': total_collisions, - 'total_tears': total_tears, - 'enabled': self.enabled, - 'max_objects': self.max_soft_body_objects, - 'update_frequency': self.soft_body_update_frequency, - 'performance_stats': self.performance_stats - } - - def cleanup(self): - """ - 清理所有软体对象 - """ - for soft_info in self.soft_body_objects[:]: - self.remove_soft_body(soft_info) - self.soft_body_objects.clear() - print("✓ 软体管理器已清理") - - def _apply_soft_body_parameters(self, soft_body: BulletSoftBodyNode, params: Dict[str, Any]): - """ - 应用软体参数到软体节点 - - Args: - soft_body: 软体节点 - params: 参数字典 - """ - cfg = soft_body.get_cfg() - - # 物理参数 - if 'linear_stiffness' in params: - cfg.set_kDF(params['linear_stiffness']) - if 'angular_stiffness' in params: - cfg.set_kDG(params['angular_stiffness']) - if 'volume_stiffness' in params: - cfg.set_kVC(params['volume_stiffness']) - if 'pressure_coefficient' in params: - cfg.set_kPR(params['pressure_coefficient']) - if 'drag_coefficient' in params: - cfg.set_kDG(params['drag_coefficient']) - if 'lift_coefficient' in params: - cfg.set_kLF(params['lift_coefficient']) - if 'cluster_hardness' in params: - cfg.set_kCHR(params['cluster_hardness']) - if 'kinetic_hardness' in params: - cfg.set_kKHR(params['kinetic_hardness']) - if 'soft_hardness' in params: - cfg.set_kSHR(params['soft_hardness']) - if 'anchor_hardness' in params: - cfg.set_kAHR(params['anchor_hardness']) - if 'pressure' in params: - cfg.set_kPR(params['pressure']) - if 'volume_conservation' in params: - cfg.set_kVC(params['volume_conservation']) - if 'damping' in params: - cfg.set_dft_damping(params['damping']) - - # 迭代次数 - if 'position_iteration' in params: - cfg.set_piterations(params['position_iteration']) - if 'drift_iteration' in params: - cfg.set_diterations(params['drift_iteration']) - if 'cluster_iteration' in params: - cfg.set_citerations(params['cluster_iteration']) - if 'velocity_iteration' in params: - cfg.set_viterations(params['velocity_iteration']) - - # 材质属性 - if 'friction' in params: - soft_body.setFriction(params['friction']) - if 'restitution' in params: - soft_body.setRestitution(params['restitution']) - if 'density' in params: - # 设置总质量(基于密度和体积估算) - soft_body.setTotalMass(params['density']) - - # 集群 - if 'clusters' in params and params['clusters'] > 0: - soft_body.generateClusters(params['clusters']) - - # 自碰撞 - if 'self_collision' in params: - soft_body.setSelfCollision(params['self_collision']) - - def _extract_mesh_data(self, node_path) -> Tuple[List[Point3], List[int]]: - """ - 从节点路径提取网格数据 - - Args: - node_path: 节点路径 - - Returns: - (顶点列表, 索引列表) - """ - try: - vertices = [] - indices = [] - - # 查找几何节点 - geom_nodes = node_path.findAllMatches('**/+GeomNode') - for geom_node_path in geom_nodes: - geom_node = geom_node_path.node() - for i in range(geom_node.getNumGeoms()): - geom = geom_node.getGeom(i) - vdata = geom.getVertexData() - - # 提取顶点 - vertex_reader = GeomVertexReader(vdata, 'vertex') - while not vertex_reader.isAtEnd(): - vertices.append(vertex_reader.getData3()) - - # 提取索引 - for prim in geom.getPrimitives(): - index_reader = prim.getIndexIterator() - while not index_reader.isAtEnd(): - indices.append(index_reader.getData()) - index_reader.next() - - return vertices, indices - - except Exception as e: - print(f"⚠ 提取网格数据失败: {e}") - return [], [] - - def _generate_sphere_mesh(self, radius: float, resolution: int) -> Tuple[List[Point3], List[int]]: - """ - 生成球形网格 - - Args: - radius: 半径 - resolution: 分辨率 - - Returns: - (顶点列表, 索引列表) - """ - vertices = [] - indices = [] - - # 生成球面顶点 - for i in range(resolution + 1): - phi = (i / resolution) * math.pi # 纬度角 - for j in range(resolution * 2 + 1): - theta = (j / (resolution * 2)) * 2 * math.pi # 经度角 - - x = radius * math.sin(phi) * math.cos(theta) - y = radius * math.sin(phi) * math.sin(theta) - z = radius * math.cos(phi) - - vertices.append(Point3(x, y, z)) - - # 生成面索引 - for i in range(resolution): - for j in range(resolution * 2): - v0 = i * (resolution * 2 + 1) + j - v1 = v0 + 1 - v2 = (i + 1) * (resolution * 2 + 1) + j - v3 = v2 + 1 - - if i < resolution - 1: - indices.extend([v0, v1, v2]) - indices.extend([v1, v3, v2]) - - return vertices, indices - - def _generate_box_mesh(self, width: float, height: float, depth: float, resolution: int) -> Tuple[List[Point3], List[int]]: - """ - 生成盒形网格 - - Args: - width: 宽度 - height: 高度 - depth: 深度 - resolution: 分辨率 - - Returns: - (顶点列表, 索引列表) - """ - vertices = [] - indices = [] - - # 半尺寸 - hw = width / 2.0 - hh = height / 2.0 - hd = depth / 2.0 - - # 生成六个面的顶点 - # 正X面 - for i in range(resolution + 1): - for j in range(resolution + 1): - x = hw - y = (i / resolution) * depth - hd - z = (j / resolution) * height - hh - vertices.append(Point3(x, y, z)) - - # 负X面 - start_idx = len(vertices) - for i in range(resolution + 1): - for j in range(resolution + 1): - x = -hw - y = (i / resolution) * depth - hd - z = (j / resolution) * height - hh - vertices.append(Point3(x, y, z)) - - # 正Y面 - start_idx = len(vertices) - for i in range(resolution + 1): - for j in range(resolution + 1): - x = (i / resolution) * width - hw - y = hd - z = (j / resolution) * height - hh - vertices.append(Point3(x, y, z)) - - # 负Y面 - start_idx = len(vertices) - for i in range(resolution + 1): - for j in range(resolution + 1): - x = (i / resolution) * width - hw - y = -hd - z = (j / resolution) * height - hh - vertices.append(Point3(x, y, z)) - - # 正Z面 - start_idx = len(vertices) - for i in range(resolution + 1): - for j in range(resolution + 1): - x = (i / resolution) * width - hw - y = (j / resolution) * depth - hd - z = hh - vertices.append(Point3(x, y, z)) - - # 负Z面 - start_idx = len(vertices) - for i in range(resolution + 1): - for j in range(resolution + 1): - x = (i / resolution) * width - hw - y = (j / resolution) * depth - hd - z = -hh - vertices.append(Point3(x, y, z)) - - # 生成面索引(简化处理) - # 这里仅为示例,实际实现需要更复杂的索引生成逻辑 - for face in range(6): - for i in range(resolution): - for j in range(resolution): - v0 = face * (resolution + 1) * (resolution + 1) + i * (resolution + 1) + j - v1 = v0 + 1 - v2 = face * (resolution + 1) * (resolution + 1) + (i + 1) * (resolution + 1) + j - v3 = v2 + 1 - - indices.extend([v0, v1, v2, v1, v3, v2]) - - return vertices, indices - - def _generate_capsule_mesh(self, radius: float, height: float, resolution: int) -> Tuple[List[Point3], List[int]]: - """ - 生成胶囊形网格 - - Args: - radius: 半径 - height: 高度 - resolution: 分辨率 - - Returns: - (顶点列表, 索引列表) - """ - vertices = [] - indices = [] - - # 生成胶囊形状顶点 - half_height = height / 2.0 - - # 上半球 - for i in range(resolution // 2 + 1): - phi = (i / (resolution // 2)) * (math.pi / 2) # 0到π/2 - for j in range(resolution * 2 + 1): - theta = (j / (resolution * 2)) * 2 * math.pi - - x = radius * math.sin(phi) * math.cos(theta) - y = radius * math.sin(phi) * math.sin(theta) - z = radius * math.cos(phi) + half_height - - vertices.append(Point3(x, y, z)) - - # 圆柱部分 - for i in range(resolution + 1): - for j in range(resolution * 2 + 1): - theta = (j / (resolution * 2)) * 2 * math.pi - y_offset = (i / resolution) * height - half_height - - x = radius * math.cos(theta) - y = radius * math.sin(theta) - z = y_offset - - vertices.append(Point3(x, y, z)) - - # 下半球 - start_idx = len(vertices) - for i in range(resolution // 2 + 1): - phi = math.pi / 2 + (i / (resolution // 2)) * (math.pi / 2) # π/2到π - for j in range(resolution * 2 + 1): - theta = (j / (resolution * 2)) * 2 * math.pi - - x = radius * math.sin(phi) * math.cos(theta) - y = radius * math.sin(phi) * math.sin(theta) - z = radius * math.cos(phi) - half_height - - vertices.append(Point3(x, y, z)) - - return vertices, indices - - def _generate_cylinder_mesh(self, radius: float, height: float, resolution: int) -> Tuple[List[Point3], List[int]]: - """ - 生成圆柱形网格 - - Args: - radius: 半径 - height: 高度 - resolution: 分辨率 - - Returns: - (顶点列表, 索引列表) - """ - vertices = [] - indices = [] - - # 半高度 - half_height = height / 2.0 - - # 侧面顶点 - for i in range(2): # 顶部到底部 - z = half_height if i == 0 else -half_height - for j in range(resolution * 2 + 1): - theta = (j / (resolution * 2)) * 2 * math.pi - x = radius * math.cos(theta) - y = radius * math.sin(theta) - vertices.append(Point3(x, y, z)) - - # 圆柱面索引 - for i in range(resolution * 2): - indices.extend([i, i + 1, i + resolution * 2 + 1]) - indices.extend([i + 1, i + resolution * 2 + 2, i + resolution * 2 + 1]) - - return vertices, indices - - def _generate_rope_vertices(self, point_a: Point3, point_b: Point3, resolution: int) -> List[Point3]: - """ - 生成绳索顶点 - - Args: - point_a: 起点 - point_b: 终点 - resolution: 分辨率 - - Returns: - 顶点列表 - """ - vertices = [] - - # 在两点之间生成顶点 - for i in range(resolution): - t = i / (resolution - 1) if resolution > 1 else 0 - pos = point_a + (point_b - point_a) * t - vertices.append(pos) - - return vertices - - def _generate_rope_indices(self, resolution: int) -> List[int]: - """ - 生成绳索索引 - - Args: - resolution: 分辨率 - - Returns: - 索引列表 - """ - return list(range(resolution)) - - def _generate_patch_mesh(self, width: float, height: float, resolution_x: int, resolution_y: int) -> Tuple[List[Point3], List[int]]: - """ - 生成布料片网格 - - Args: - width: 宽度 - height: 高度 - resolution_x: X分辨率 - resolution_y: Y分辨率 - - Returns: - (顶点列表, 索引列表) - """ - vertices = [] - indices = [] - - # 半宽高 - hw = width / 2.0 - hh = height / 2.0 - - # 生成顶点 - for j in range(resolution_y + 1): - for i in range(resolution_x + 1): - x = (i / resolution_x) * width - hw - y = (j / resolution_y) * height - hh - z = 0.0 - vertices.append(Point3(x, y, z)) - - # 生成面索引 - for j in range(resolution_y): - for i in range(resolution_x): - v0 = j * (resolution_x + 1) + i - v1 = v0 + 1 - v2 = (j + 1) * (resolution_x + 1) + i - v3 = v2 + 1 - - indices.extend([v0, v1, v2, v1, v3, v2]) - - return vertices, indices - - def _add_links_from_indices(self, soft_body: BulletSoftBodyNode, indices: List[int]): - """ - 根据索引添加链接 - - Args: - soft_body: 软体节点 - indices: 索引列表 - """ - try: - # 创建边集合以避免重复 - edges = set() - - # 从三角形面提取边 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - # 三角形的三条边 - edges.add(tuple(sorted([indices[i], indices[i+1]]))) - edges.add(tuple(sorted([indices[i+1], indices[i+2]]))) - edges.add(tuple(sorted([indices[i+2], indices[i]]))) - - # 添加链接 - for edge in edges: - soft_body.appendLink(edge[0], edge[1]) - - except Exception as e: - print(f"⚠ 添加链接失败: {e}") - - def _add_bending_constraints(self, soft_body: BulletSoftBodyNode): - """ - 添加弯曲约束 - - Args: - soft_body: 软体节点 - """ - try: - # 添加弯曲约束 - # 这个参数控制弯曲约束的生成步长 - soft_body.generateBendingConstraints(2) - - except Exception as e: - print(f"⚠ 添加弯曲约束失败: {e}") - - def _calculate_mass_distribution(self, vertices: List[Point3], params: Dict[str, Any]) -> Dict[str, Any]: - """ - 计算质量分布 - - Args: - vertices: 顶点列表 - params: 参数字典 - - Returns: - 质量分布信息 - """ - mass_distribution = { - 'type': params.get('mass_distribution', 'uniform'), - 'total_mass': 0.0, - 'center_of_mass': Point3(0, 0, 0), - 'mass_per_vertex': [] - } - - if not vertices: - return mass_distribution - - # 计算总质量(基于密度和估算的体积) - if mass_distribution['type'] == 'uniform': - # 统一质量分布 - density = params.get('density', 1.0) - estimated_volume = len(vertices) * 0.01 # 简单估算 - total_mass = density * estimated_volume - mass_per_vertex = total_mass / len(vertices) if len(vertices) > 0 else 0.0 - - mass_distribution['total_mass'] = total_mass - mass_distribution['mass_per_vertex'] = [mass_per_vertex] * len(vertices) - - # 计算质心 - center = Point3(0, 0, 0) - for vertex in vertices: - center += vertex - center /= len(vertices) - mass_distribution['center_of_mass'] = center - - return mass_distribution - - def _check_tear_conditions(self, soft_info: Dict[str, Any]): - """ - 检查撕裂条件 - - Args: - soft_info: 软体对象信息 - """ - try: - tear_mode = soft_info['params'].get('tear_mode', 'none') - if tear_mode == 'none': - return - - tear_threshold = soft_info['params'].get('tear_threshold', 100.0) - # 在实际实现中,这里会检查撕裂条件 - # 由于BulletSoftBodyNode目前不直接支持撕裂,这里简化处理 - - except Exception as e: - print(f"⚠ 检查撕裂条件失败: {e}") - - def _update_collision_stats(self, soft_info: Dict[str, Any]): - """ - 更新碰撞统计 - - Args: - soft_info: 软体对象信息 - """ - try: - # 在实际实现中,这里会更新碰撞统计 - # 简化处理 - pass - - except Exception as e: - print(f"⚠ 更新碰撞统计失败: {e}") - - def _get_physics_world(self): - """ - 获取物理世界对象 - - Returns: - 物理世界对象或None - """ - try: - # 尝试从世界对象获取物理世界 - if hasattr(self.world, 'physics_world'): - return self.world.physics_world.bullet_world - elif hasattr(self.world, 'bullet_world'): - return self.world.bullet_world - else: - # 查找物理插件 - physics_plugin = self.world.plugins.get('rigid_body_physics') - if physics_plugin and hasattr(physics_plugin, 'physics_world'): - return physics_plugin.physics_world.bullet_world - except Exception as e: - print(f"⚠ 获取物理世界失败: {e}") - +""" +软体管理器 +负责软体对象的创建、管理和更新 +包含完整的软体物理模拟功能 +""" + +from panda3d.core import Vec3, Point3, GeomVertexReader, GeomVertexFormat +from panda3d.core import GeomVertexData, Geom, GeomNode, NodePath +from panda3d.bullet import BulletSoftBodyNode, BulletSoftBodyWorldInfo +import math +from typing import List, Dict, Optional, Any, Tuple +import numpy as np + + +class SoftBodyManager: + """ + 软体管理器 + 管理场景中的所有软体对象,提供完整的软体物理模拟功能 + """ + + # 软体类型常量 + SOFT_TYPE_SPHERE = "sphere" + SOFT_TYPE_BOX = "box" + SOFT_TYPE_CAPSULE = "capsule" + SOFT_TYPE_CYLINDER = "cylinder" + SOFT_TYPE_MESH = "mesh" + SOFT_TYPE_ROPE = "rope" + SOFT_TYPE_PATCH = "patch" + + # 软体行为模式 + BEHAVIOR_DEFAULT = "default" + BEHAVIOR_RIGID = "rigid" + BEHAVIOR_SOFT = "soft" + BEHAVIOR_FLUID = "fluid" + + # 撕裂和碰撞模式 + TEAR_MODE_NONE = "none" + TEAR_MODE_VERTEX = "vertex" + TEAR_MODE_EDGE = "edge" + COLLISION_MODE_NONE = "none" + COLLISION_MODE_SOFT = "soft" + COLLISION_MODE_RIGID = "rigid" + + def __init__(self, world): + self.world = world + self.soft_body_objects: List[Dict[str, Any]] = [] + self.enabled = False + self.gravity = Vec3(0, 0, -9.81) + + # 软体默认参数 + self.default_params = { + 'density': 1.0, + 'linear_stiffness': 0.8, + 'angular_stiffness': 0.8, + 'volume_stiffness': 0.8, + 'friction': 0.5, + 'restitution': 0.2, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.01, + 'lift_coefficient': 0.01, + 'soft_type': 'generic', + 'behavior_mode': 'default', + 'tear_mode': 'none', + 'tear_threshold': 100.0, + 'tear_resistance': 50.0, + 'collision_mode': 'soft', + 'bending_constraints': True, + 'clusters': 0, + 'cluster_hardness': 1.0, + 'position_iteration': 2, + 'drift_iteration': 2, + 'cluster_iteration': 2, + 'velocity_iteration': 2, + 'mass_distribution': 'uniform', # uniform, proportional, custom + 'pressure': 0.0, + 'volume_conservation': 0.0, + 'damping': 0.01, + 'self_collision': True + } + + # 性能参数 + self.max_soft_body_objects = 100 + self.soft_body_update_frequency = 60 # Hz + self.performance_stats = { + 'total_updates': 0, + 'collision_count': 0, + 'tear_count': 0, + 'memory_usage': 0 + } + + def enable(self): + """ + 启用软体管理器 + """ + self.enabled = True + print("✓ 软体管理器已启用") + + def disable(self): + """ + 禁用软体管理器 + """ + self.enabled = False + print("✓ 软体管理器已禁用") + + def set_gravity(self, gravity: Vec3): + """ + 设置重力 + + Args: + gravity: 重力向量 + """ + self.gravity = gravity + + def create_soft_body_from_mesh(self, node_path, params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: + """ + 从网格创建软体对象 + + Args: + node_path: 网格节点路径 + params: 软体参数 + + Returns: + 软体对象信息字典 + """ + if not self.enabled: + print("⚠ 软体管理器未启用") + return None + + if len(self.soft_body_objects) >= self.max_soft_body_objects: + print("⚠ 达到最大软体对象数量限制") + return None + + try: + # 合并参数 + soft_params = self.default_params.copy() + if params: + soft_params.update(params) + + # 创建软体节点 + soft_name = f'soft_body_{len(self.soft_body_objects)}' + soft_body_node = BulletSoftBodyNode(soft_name) + + # 设置软体参数 + self._apply_soft_body_parameters(soft_body_node, soft_params) + + # 从网格提取数据 + vertices, indices = self._extract_mesh_data(node_path) + if not vertices or not indices: + print("⚠ 无法从网格提取顶点数据") + return None + + # 添加顶点 + for vertex in vertices: + soft_body_node.appendNode(vertex, 1.0) + + # 添加面 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) + + # 添加链接 + self._add_links_from_indices(soft_body_node, indices) + + # 添加弯曲约束 + if soft_params.get('bending_constraints', True): + self._add_bending_constraints(soft_body_node) + + # 创建软体节点路径 + soft_np = node_path.attachNewNode(soft_body_node) + soft_np.setPos(node_path.getPos()) + soft_np.setHpr(node_path.getHpr()) + + # 添加到物理世界 + physics_world = self._get_physics_world() + if physics_world: + physics_world.attachSoftBody(soft_body_node) + + # 计算质量分布 + mass_distribution = self._calculate_mass_distribution(vertices, soft_params) + + # 保存软体信息 + soft_info = { + 'node_path': soft_np, + 'soft_body': soft_body_node, + 'original_node': node_path, + 'params': soft_params, + 'type': 'soft_mesh', + 'behavior_mode': soft_params.get('behavior_mode', 'default'), + 'vertices': vertices, + 'indices': indices, + 'mass_distribution': mass_distribution, + 'tears': [], + 'collisions': [], + 'constraints': [], + 'creation_time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0, + 'last_update_time': 0.0, + 'performance_stats': { + 'update_count': 0, + 'collision_count': 0, + 'tear_count': 0, + 'memory_usage': 0 + } + } + + self.soft_body_objects.append(soft_info) + + print(f"✓ 创建软体对象: {soft_name} (顶点数: {len(vertices)}, 面数: {len(indices)//3})") + return soft_info + + except Exception as e: + print(f"✗ 创建软体对象失败: {e}") + import traceback + traceback.print_exc() + return None + + def create_soft_body_sphere(self, node_path, radius: float, + resolution: int = 8, + params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: + """ + 创建球形软体 + + Args: + node_path: 父节点路径 + radius: 球体半径 + resolution: 分辨率 + params: 软体参数 + + Returns: + 软体对象信息字典 + """ + if not self.enabled: + print("⚠ 软体管理器未启用") + return None + + if len(self.soft_body_objects) >= self.max_soft_body_objects: + print("⚠ 达到最大软体对象数量限制") + return None + + try: + # 合并参数 + soft_params = self.default_params.copy() + if params: + soft_params.update(params) + + # 创建软体节点 + soft_name = f'soft_sphere_{len(self.soft_body_objects)}' + soft_body_node = BulletSoftBodyNode(soft_name) + + # 设置软体参数 + self._apply_soft_body_parameters(soft_body_node, soft_params) + + # 生成球形网格 + vertices, indices = self._generate_sphere_mesh(radius, resolution) + + # 添加顶点 + for vertex in vertices: + soft_body_node.appendNode(vertex, 1.0) + + # 添加面 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) + + # 添加链接 + self._add_links_from_indices(soft_body_node, indices) + + # 添加弯曲约束 + if soft_params.get('bending_constraints', True): + self._add_bending_constraints(soft_body_node) + + # 创建软体节点路径 + soft_np = node_path.attachNewNode(soft_body_node) + + # 添加到物理世界 + physics_world = self._get_physics_world() + if physics_world: + physics_world.attachSoftBody(soft_body_node) + + # 计算质量分布 + mass_distribution = self._calculate_mass_distribution(vertices, soft_params) + + # 保存软体信息 + soft_info = { + 'node_path': soft_np, + 'soft_body': soft_body_node, + 'original_node': node_path, + 'params': soft_params, + 'type': 'soft_sphere', + 'radius': radius, + 'resolution': resolution, + 'behavior_mode': soft_params.get('behavior_mode', 'default'), + 'vertices': vertices, + 'indices': indices, + 'mass_distribution': mass_distribution, + 'tears': [], + 'collisions': [], + 'constraints': [], + 'creation_time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0, + 'last_update_time': 0.0, + 'performance_stats': { + 'update_count': 0, + 'collision_count': 0, + 'tear_count': 0, + 'memory_usage': 0 + } + } + + self.soft_body_objects.append(soft_info) + + print(f"✓ 创建球形软体: {soft_name} (半径: {radius}, 分辨率: {resolution})") + return soft_info + + except Exception as e: + print(f"✗ 创建球形软体失败: {e}") + import traceback + traceback.print_exc() + return None + + def create_soft_body_box(self, node_path, width: float, height: float, depth: float, + resolution: int = 4, + params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: + """ + 创建盒形软体 + + Args: + node_path: 父节点路径 + width: 宽度 + height: 高度 + depth: 深度 + resolution: 分辨率 + params: 软体参数 + + Returns: + 软体对象信息字典 + """ + if not self.enabled: + print("⚠ 软体管理器未启用") + return None + + if len(self.soft_body_objects) >= self.max_soft_body_objects: + print("⚠ 达到最大软体对象数量限制") + return None + + try: + # 合并参数 + soft_params = self.default_params.copy() + if params: + soft_params.update(params) + + # 创建软体节点 + soft_name = f'soft_box_{len(self.soft_body_objects)}' + soft_body_node = BulletSoftBodyNode(soft_name) + + # 设置软体参数 + self._apply_soft_body_parameters(soft_body_node, soft_params) + + # 生成盒形网格 + vertices, indices = self._generate_box_mesh(width, height, depth, resolution) + + # 添加顶点 + for vertex in vertices: + soft_body_node.appendNode(vertex, 1.0) + + # 添加面 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) + + # 添加链接 + self._add_links_from_indices(soft_body_node, indices) + + # 添加弯曲约束 + if soft_params.get('bending_constraints', True): + self._add_bending_constraints(soft_body_node) + + # 创建软体节点路径 + soft_np = node_path.attachNewNode(soft_body_node) + + # 添加到物理世界 + physics_world = self._get_physics_world() + if physics_world: + physics_world.attachSoftBody(soft_body_node) + + # 计算质量分布 + mass_distribution = self._calculate_mass_distribution(vertices, soft_params) + + # 保存软体信息 + soft_info = { + 'node_path': soft_np, + 'soft_body': soft_body_node, + 'original_node': node_path, + 'params': soft_params, + 'type': 'soft_box', + 'width': width, + 'height': height, + 'depth': depth, + 'resolution': resolution, + 'behavior_mode': soft_params.get('behavior_mode', 'default'), + 'vertices': vertices, + 'indices': indices, + 'mass_distribution': mass_distribution, + 'tears': [], + 'collisions': [], + 'constraints': [], + 'creation_time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0, + 'last_update_time': 0.0, + 'performance_stats': { + 'update_count': 0, + 'collision_count': 0, + 'tear_count': 0, + 'memory_usage': 0 + } + } + + self.soft_body_objects.append(soft_info) + + print(f"✓ 创建盒形软体: {soft_name} ({width}x{height}x{depth})") + return soft_info + + except Exception as e: + print(f"✗ 创建盒形软体失败: {e}") + import traceback + traceback.print_exc() + return None + + def create_soft_body_capsule(self, node_path, radius: float, height: float, + resolution: int = 8, + params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: + """ + 创建胶囊形软体 + + Args: + node_path: 父节点路径 + radius: 半径 + height: 高度 + resolution: 分辨率 + params: 软体参数 + + Returns: + 软体对象信息字典 + """ + if not self.enabled: + print("⚠ 软体管理器未启用") + return None + + if len(self.soft_body_objects) >= self.max_soft_body_objects: + print("⚠ 达到最大软体对象数量限制") + return None + + try: + # 合并参数 + soft_params = self.default_params.copy() + if params: + soft_params.update(params) + + # 创建软体节点 + soft_name = f'soft_capsule_{len(self.soft_body_objects)}' + soft_body_node = BulletSoftBodyNode(soft_name) + + # 设置软体参数 + self._apply_soft_body_parameters(soft_body_node, soft_params) + + # 生成胶囊形网格 + vertices, indices = self._generate_capsule_mesh(radius, height, resolution) + + # 添加顶点 + for vertex in vertices: + soft_body_node.appendNode(vertex, 1.0) + + # 添加面 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) + + # 添加链接 + self._add_links_from_indices(soft_body_node, indices) + + # 添加弯曲约束 + if soft_params.get('bending_constraints', True): + self._add_bending_constraints(soft_body_node) + + # 创建软体节点路径 + soft_np = node_path.attachNewNode(soft_body_node) + + # 添加到物理世界 + physics_world = self._get_physics_world() + if physics_world: + physics_world.attachSoftBody(soft_body_node) + + # 计算质量分布 + mass_distribution = self._calculate_mass_distribution(vertices, soft_params) + + # 保存软体信息 + soft_info = { + 'node_path': soft_np, + 'soft_body': soft_body_node, + 'original_node': node_path, + 'params': soft_params, + 'type': 'soft_capsule', + 'radius': radius, + 'height': height, + 'resolution': resolution, + 'behavior_mode': soft_params.get('behavior_mode', 'default'), + 'vertices': vertices, + 'indices': indices, + 'mass_distribution': mass_distribution, + 'tears': [], + 'collisions': [], + 'constraints': [], + 'creation_time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0, + 'last_update_time': 0.0, + 'performance_stats': { + 'update_count': 0, + 'collision_count': 0, + 'tear_count': 0, + 'memory_usage': 0 + } + } + + self.soft_body_objects.append(soft_info) + + print(f"✓ 创建胶囊形软体: {soft_name} (半径: {radius}, 高度: {height})") + return soft_info + + except Exception as e: + print(f"✗ 创建胶囊形软体失败: {e}") + import traceback + traceback.print_exc() + return None + + def create_soft_body_cylinder(self, node_path, radius: float, height: float, + resolution: int = 8, + params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: + """ + 创建圆柱形软体 + + Args: + node_path: 父节点路径 + radius: 半径 + height: 高度 + resolution: 分辨率 + params: 软体参数 + + Returns: + 软体对象信息字典 + """ + if not self.enabled: + print("⚠ 软体管理器未启用") + return None + + if len(self.soft_body_objects) >= self.max_soft_body_objects: + print("⚠ 达到最大软体对象数量限制") + return None + + try: + # 合并参数 + soft_params = self.default_params.copy() + if params: + soft_params.update(params) + + # 创建软体节点 + soft_name = f'soft_cylinder_{len(self.soft_body_objects)}' + soft_body_node = BulletSoftBodyNode(soft_name) + + # 设置软体参数 + self._apply_soft_body_parameters(soft_body_node, soft_params) + + # 生成圆柱形网格 + vertices, indices = self._generate_cylinder_mesh(radius, height, resolution) + + # 添加顶点 + for vertex in vertices: + soft_body_node.appendNode(vertex, 1.0) + + # 添加面 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) + + # 添加链接 + self._add_links_from_indices(soft_body_node, indices) + + # 添加弯曲约束 + if soft_params.get('bending_constraints', True): + self._add_bending_constraints(soft_body_node) + + # 创建软体节点路径 + soft_np = node_path.attachNewNode(soft_body_node) + + # 添加到物理世界 + physics_world = self._get_physics_world() + if physics_world: + physics_world.attachSoftBody(soft_body_node) + + # 计算质量分布 + mass_distribution = self._calculate_mass_distribution(vertices, soft_params) + + # 保存软体信息 + soft_info = { + 'node_path': soft_np, + 'soft_body': soft_body_node, + 'original_node': node_path, + 'params': soft_params, + 'type': 'soft_cylinder', + 'radius': radius, + 'height': height, + 'resolution': resolution, + 'behavior_mode': soft_params.get('behavior_mode', 'default'), + 'vertices': vertices, + 'indices': indices, + 'mass_distribution': mass_distribution, + 'tears': [], + 'collisions': [], + 'constraints': [], + 'creation_time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0, + 'last_update_time': 0.0, + 'performance_stats': { + 'update_count': 0, + 'collision_count': 0, + 'tear_count': 0, + 'memory_usage': 0 + } + } + + self.soft_body_objects.append(soft_info) + + print(f"✓ 创建圆柱形软体: {soft_name} (半径: {radius}, 高度: {height})") + return soft_info + + except Exception as e: + print(f"✗ 创建圆柱形软体失败: {e}") + import traceback + traceback.print_exc() + return None + + def create_soft_body_rope(self, node_path, point_a: Point3, point_b: Point3, + resolution: int = 10, + params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: + """ + 创建绳索软体 + + Args: + node_path: 父节点路径 + point_a: 起点 + point_b: 终点 + resolution: 分辨率 + params: 软体参数 + + Returns: + 软体对象信息字典 + """ + if not self.enabled: + print("⚠ 软体管理器未启用") + return None + + if len(self.soft_body_objects) >= self.max_soft_body_objects: + print("⚠ 达到最大软体对象数量限制") + return None + + try: + # 合并参数 + soft_params = self.default_params.copy() + if params: + soft_params.update(params) + + # 创建软体节点 + soft_name = f'soft_rope_{len(self.soft_body_objects)}' + soft_body_node = BulletSoftBodyNode(soft_name) + + # 设置软体参数 + self._apply_soft_body_parameters(soft_body_node, soft_params) + + # 生成绳索顶点 + vertices = self._generate_rope_vertices(point_a, point_b, resolution) + indices = self._generate_rope_indices(resolution) + + # 添加顶点 + for vertex in vertices: + soft_body_node.appendNode(vertex, 1.0) + + # 添加链接 + for i in range(len(indices) - 1): + soft_body_node.appendLink(indices[i], indices[i+1]) + + # 创建软体节点路径 + soft_np = node_path.attachNewNode(soft_body_node) + + # 添加到物理世界 + physics_world = self._get_physics_world() + if physics_world: + physics_world.attachSoftBody(soft_body_node) + + # 计算质量分布 + mass_distribution = self._calculate_mass_distribution(vertices, soft_params) + + # 保存软体信息 + soft_info = { + 'node_path': soft_np, + 'soft_body': soft_body_node, + 'original_node': node_path, + 'params': soft_params, + 'type': 'soft_rope', + 'point_a': point_a, + 'point_b': point_b, + 'resolution': resolution, + 'behavior_mode': soft_params.get('behavior_mode', 'default'), + 'vertices': vertices, + 'indices': indices, + 'mass_distribution': mass_distribution, + 'tears': [], + 'collisions': [], + 'constraints': [], + 'creation_time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0, + 'last_update_time': 0.0, + 'performance_stats': { + 'update_count': 0, + 'collision_count': 0, + 'tear_count': 0, + 'memory_usage': 0 + } + } + + self.soft_body_objects.append(soft_info) + + print(f"✓ 创建绳索软体: {soft_name} (长度: {(point_b - point_a).length()})") + return soft_info + + except Exception as e: + print(f"✗ 创建绳索软体失败: {e}") + import traceback + traceback.print_exc() + return None + + def create_soft_body_patch(self, node_path, width: float, height: float, + resolution_x: int, resolution_y: int, + params: Dict[str, Any] = None) -> Optional[Dict[str, Any]]: + """ + 创建布料片软体 + + Args: + node_path: 父节点路径 + width: 宽度 + height: 高度 + resolution_x: X分辨率 + resolution_y: Y分辨率 + params: 软体参数 + + Returns: + 软体对象信息字典 + """ + if not self.enabled: + print("⚠ 软体管理器未启用") + return None + + if len(self.soft_body_objects) >= self.max_soft_body_objects: + print("⚠ 达到最大软体对象数量限制") + return None + + try: + # 合并参数 + soft_params = self.default_params.copy() + if params: + soft_params.update(params) + + # 创建软体节点 + soft_name = f'soft_patch_{len(self.soft_body_objects)}' + soft_body_node = BulletSoftBodyNode(soft_name) + + # 设置软体参数 + self._apply_soft_body_parameters(soft_body_node, soft_params) + + # 生成布料片顶点 + vertices, indices = self._generate_patch_mesh(width, height, resolution_x, resolution_y) + + # 添加顶点 + for vertex in vertices: + soft_body_node.appendNode(vertex, 1.0) + + # 添加面 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) + + # 添加链接 + self._add_links_from_indices(soft_body_node, indices) + + # 添加弯曲约束 + if soft_params.get('bending_constraints', True): + self._add_bending_constraints(soft_body_node) + + # 创建软体节点路径 + soft_np = node_path.attachNewNode(soft_body_node) + + # 添加到物理世界 + physics_world = self._get_physics_world() + if physics_world: + physics_world.attachSoftBody(soft_body_node) + + # 计算质量分布 + mass_distribution = self._calculate_mass_distribution(vertices, soft_params) + + # 保存软体信息 + soft_info = { + 'node_path': soft_np, + 'soft_body': soft_body_node, + 'original_node': node_path, + 'params': soft_params, + 'type': 'soft_patch', + 'width': width, + 'height': height, + 'resolution_x': resolution_x, + 'resolution_y': resolution_y, + 'behavior_mode': soft_params.get('behavior_mode', 'default'), + 'vertices': vertices, + 'indices': indices, + 'mass_distribution': mass_distribution, + 'tears': [], + 'collisions': [], + 'constraints': [], + 'creation_time': self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0, + 'last_update_time': 0.0, + 'performance_stats': { + 'update_count': 0, + 'collision_count': 0, + 'tear_count': 0, + 'memory_usage': 0 + } + } + + self.soft_body_objects.append(soft_info) + + print(f"✓ 创建布料片软体: {soft_name} ({resolution_x+1}x{resolution_y+1} 顶点)") + return soft_info + + except Exception as e: + print(f"✗ 创建布料片软体失败: {e}") + import traceback + traceback.print_exc() + return None + + def add_constraint(self, soft_info1: Dict[str, Any], soft_info2: Dict[str, Any], + constraint_type: str = "link", + params: Dict[str, Any] = None) -> bool: + """ + 在两个软体之间添加约束 + + Args: + soft_info1: 第一个软体信息 + soft_info2: 第二个软体信息 + constraint_type: 约束类型 + params: 约束参数 + + Returns: + 是否成功添加约束 + """ + try: + # 在实际实现中,这里会添加软体间的约束 + # BulletSoftBodyNode提供多种约束类型 + print(f"✓ 在软体间添加{constraint_type}约束") + return True + + except Exception as e: + print(f"✗ 添加约束失败: {e}") + return False + + def apply_force_to_soft_body(self, soft_info: Dict[str, Any], force: Vec3, + position: Point3 = None) -> bool: + """ + 对软体应用力 + + Args: + soft_info: 软体对象信息 + force: 力向量 + position: 应力位置(如果为None,则应用到质心) + + Returns: + 是否成功应用力 + """ + try: + soft_body = soft_info['soft_body'] + + # 在实际实现中,这里会应用力到软体 + # BulletSoftBodyNode有多种力应用方法 + print(f"✓ 对软体应用力: {force}") + return True + + except Exception as e: + print(f"✗ 应用力失败: {e}") + return False + + def remove_soft_body(self, soft_info: Dict[str, Any]) -> bool: + """ + 移除软体对象 + + Args: + soft_info: 软体对象信息 + + Returns: + 是否成功移除 + """ + try: + if soft_info not in self.soft_body_objects: + print("⚠ 软体对象不存在") + return False + + # 从物理世界移除 + physics_world = self._get_physics_world() + if physics_world: + physics_world.removeSoftBody(soft_info['soft_body']) + + # 移除节点 + if soft_info['node_path'] and not soft_info['node_path'].isEmpty(): + soft_info['node_path'].removeNode() + + # 从列表移除 + self.soft_body_objects.remove(soft_info) + + print("✓ 移除软体对象") + return True + + except Exception as e: + print(f"✗ 移除软体对象失败: {e}") + return False + + def update_soft_body_params(self, soft_info: Dict[str, Any], params: Dict[str, Any]) -> bool: + """ + 更新软体参数 + + Args: + soft_info: 软体对象信息 + params: 新参数 + + Returns: + 是否成功更新 + """ + try: + # 更新参数 + soft_info['params'].update(params) + + # 应用参数到软体节点 + soft_body = soft_info['soft_body'] + self._apply_soft_body_parameters(soft_body, soft_info['params']) + + print("✓ 更新软体参数") + return True + + except Exception as e: + print(f"✗ 更新软体参数失败: {e}") + return False + + def update_all_soft_bodies(self, dt: float): + """ + 更新所有软体对象 + + Args: + dt: 时间增量 + """ + if not self.enabled: + return + + current_time = self.world.globalClock.getFrameTime() if hasattr(self.world, 'globalClock') else 0.0 + + for soft_info in self.soft_body_objects: + # 检查更新频率 + if current_time - soft_info['last_update_time'] < 1.0 / self.soft_body_update_frequency: + continue + + # 更新性能统计 + soft_info['performance_stats']['update_count'] += 1 + soft_info['last_update_time'] = current_time + + # 检查撕裂 + self._check_tear_conditions(soft_info) + + # 更新碰撞统计 + self._update_collision_stats(soft_info) + + # 更新整体性能统计 + self.performance_stats['total_updates'] += len(self.soft_body_objects) + + def get_soft_body_stats(self) -> Dict[str, Any]: + """ + 获取软体统计信息 + + Returns: + 统计信息字典 + """ + total_vertices = 0 + total_faces = 0 + total_collisions = 0 + total_tears = 0 + + for soft_info in self.soft_body_objects: + total_vertices += len(soft_info.get('vertices', [])) + total_faces += len(soft_info.get('indices', [])) // 3 + total_collisions += soft_info['performance_stats'].get('collision_count', 0) + total_tears += soft_info['performance_stats'].get('tear_count', 0) + + return { + 'total_soft_body_objects': len(self.soft_body_objects), + 'total_vertices': total_vertices, + 'total_faces': total_faces, + 'total_collisions': total_collisions, + 'total_tears': total_tears, + 'enabled': self.enabled, + 'max_objects': self.max_soft_body_objects, + 'update_frequency': self.soft_body_update_frequency, + 'performance_stats': self.performance_stats + } + + def cleanup(self): + """ + 清理所有软体对象 + """ + for soft_info in self.soft_body_objects[:]: + self.remove_soft_body(soft_info) + self.soft_body_objects.clear() + print("✓ 软体管理器已清理") + + def _apply_soft_body_parameters(self, soft_body: BulletSoftBodyNode, params: Dict[str, Any]): + """ + 应用软体参数到软体节点 + + Args: + soft_body: 软体节点 + params: 参数字典 + """ + cfg = soft_body.get_cfg() + + # 物理参数 + if 'linear_stiffness' in params: + cfg.set_kDF(params['linear_stiffness']) + if 'angular_stiffness' in params: + cfg.set_kDG(params['angular_stiffness']) + if 'volume_stiffness' in params: + cfg.set_kVC(params['volume_stiffness']) + if 'pressure_coefficient' in params: + cfg.set_kPR(params['pressure_coefficient']) + if 'drag_coefficient' in params: + cfg.set_kDG(params['drag_coefficient']) + if 'lift_coefficient' in params: + cfg.set_kLF(params['lift_coefficient']) + if 'cluster_hardness' in params: + cfg.set_kCHR(params['cluster_hardness']) + if 'kinetic_hardness' in params: + cfg.set_kKHR(params['kinetic_hardness']) + if 'soft_hardness' in params: + cfg.set_kSHR(params['soft_hardness']) + if 'anchor_hardness' in params: + cfg.set_kAHR(params['anchor_hardness']) + if 'pressure' in params: + cfg.set_kPR(params['pressure']) + if 'volume_conservation' in params: + cfg.set_kVC(params['volume_conservation']) + if 'damping' in params: + cfg.set_dft_damping(params['damping']) + + # 迭代次数 + if 'position_iteration' in params: + cfg.set_piterations(params['position_iteration']) + if 'drift_iteration' in params: + cfg.set_diterations(params['drift_iteration']) + if 'cluster_iteration' in params: + cfg.set_citerations(params['cluster_iteration']) + if 'velocity_iteration' in params: + cfg.set_viterations(params['velocity_iteration']) + + # 材质属性 + if 'friction' in params: + soft_body.setFriction(params['friction']) + if 'restitution' in params: + soft_body.setRestitution(params['restitution']) + if 'density' in params: + # 设置总质量(基于密度和体积估算) + soft_body.setTotalMass(params['density']) + + # 集群 + if 'clusters' in params and params['clusters'] > 0: + soft_body.generateClusters(params['clusters']) + + # 自碰撞 + if 'self_collision' in params: + soft_body.setSelfCollision(params['self_collision']) + + def _extract_mesh_data(self, node_path) -> Tuple[List[Point3], List[int]]: + """ + 从节点路径提取网格数据 + + Args: + node_path: 节点路径 + + Returns: + (顶点列表, 索引列表) + """ + try: + vertices = [] + indices = [] + + # 查找几何节点 + geom_nodes = node_path.findAllMatches('**/+GeomNode') + for geom_node_path in geom_nodes: + geom_node = geom_node_path.node() + for i in range(geom_node.getNumGeoms()): + geom = geom_node.getGeom(i) + vdata = geom.getVertexData() + + # 提取顶点 + vertex_reader = GeomVertexReader(vdata, 'vertex') + while not vertex_reader.isAtEnd(): + vertices.append(vertex_reader.getData3()) + + # 提取索引 + for prim in geom.getPrimitives(): + index_reader = prim.getIndexIterator() + while not index_reader.isAtEnd(): + indices.append(index_reader.getData()) + index_reader.next() + + return vertices, indices + + except Exception as e: + print(f"⚠ 提取网格数据失败: {e}") + return [], [] + + def _generate_sphere_mesh(self, radius: float, resolution: int) -> Tuple[List[Point3], List[int]]: + """ + 生成球形网格 + + Args: + radius: 半径 + resolution: 分辨率 + + Returns: + (顶点列表, 索引列表) + """ + vertices = [] + indices = [] + + # 生成球面顶点 + for i in range(resolution + 1): + phi = (i / resolution) * math.pi # 纬度角 + for j in range(resolution * 2 + 1): + theta = (j / (resolution * 2)) * 2 * math.pi # 经度角 + + x = radius * math.sin(phi) * math.cos(theta) + y = radius * math.sin(phi) * math.sin(theta) + z = radius * math.cos(phi) + + vertices.append(Point3(x, y, z)) + + # 生成面索引 + for i in range(resolution): + for j in range(resolution * 2): + v0 = i * (resolution * 2 + 1) + j + v1 = v0 + 1 + v2 = (i + 1) * (resolution * 2 + 1) + j + v3 = v2 + 1 + + if i < resolution - 1: + indices.extend([v0, v1, v2]) + indices.extend([v1, v3, v2]) + + return vertices, indices + + def _generate_box_mesh(self, width: float, height: float, depth: float, resolution: int) -> Tuple[List[Point3], List[int]]: + """ + 生成盒形网格 + + Args: + width: 宽度 + height: 高度 + depth: 深度 + resolution: 分辨率 + + Returns: + (顶点列表, 索引列表) + """ + vertices = [] + indices = [] + + # 半尺寸 + hw = width / 2.0 + hh = height / 2.0 + hd = depth / 2.0 + + # 生成六个面的顶点 + # 正X面 + for i in range(resolution + 1): + for j in range(resolution + 1): + x = hw + y = (i / resolution) * depth - hd + z = (j / resolution) * height - hh + vertices.append(Point3(x, y, z)) + + # 负X面 + start_idx = len(vertices) + for i in range(resolution + 1): + for j in range(resolution + 1): + x = -hw + y = (i / resolution) * depth - hd + z = (j / resolution) * height - hh + vertices.append(Point3(x, y, z)) + + # 正Y面 + start_idx = len(vertices) + for i in range(resolution + 1): + for j in range(resolution + 1): + x = (i / resolution) * width - hw + y = hd + z = (j / resolution) * height - hh + vertices.append(Point3(x, y, z)) + + # 负Y面 + start_idx = len(vertices) + for i in range(resolution + 1): + for j in range(resolution + 1): + x = (i / resolution) * width - hw + y = -hd + z = (j / resolution) * height - hh + vertices.append(Point3(x, y, z)) + + # 正Z面 + start_idx = len(vertices) + for i in range(resolution + 1): + for j in range(resolution + 1): + x = (i / resolution) * width - hw + y = (j / resolution) * depth - hd + z = hh + vertices.append(Point3(x, y, z)) + + # 负Z面 + start_idx = len(vertices) + for i in range(resolution + 1): + for j in range(resolution + 1): + x = (i / resolution) * width - hw + y = (j / resolution) * depth - hd + z = -hh + vertices.append(Point3(x, y, z)) + + # 生成面索引(简化处理) + # 这里仅为示例,实际实现需要更复杂的索引生成逻辑 + for face in range(6): + for i in range(resolution): + for j in range(resolution): + v0 = face * (resolution + 1) * (resolution + 1) + i * (resolution + 1) + j + v1 = v0 + 1 + v2 = face * (resolution + 1) * (resolution + 1) + (i + 1) * (resolution + 1) + j + v3 = v2 + 1 + + indices.extend([v0, v1, v2, v1, v3, v2]) + + return vertices, indices + + def _generate_capsule_mesh(self, radius: float, height: float, resolution: int) -> Tuple[List[Point3], List[int]]: + """ + 生成胶囊形网格 + + Args: + radius: 半径 + height: 高度 + resolution: 分辨率 + + Returns: + (顶点列表, 索引列表) + """ + vertices = [] + indices = [] + + # 生成胶囊形状顶点 + half_height = height / 2.0 + + # 上半球 + for i in range(resolution // 2 + 1): + phi = (i / (resolution // 2)) * (math.pi / 2) # 0到π/2 + for j in range(resolution * 2 + 1): + theta = (j / (resolution * 2)) * 2 * math.pi + + x = radius * math.sin(phi) * math.cos(theta) + y = radius * math.sin(phi) * math.sin(theta) + z = radius * math.cos(phi) + half_height + + vertices.append(Point3(x, y, z)) + + # 圆柱部分 + for i in range(resolution + 1): + for j in range(resolution * 2 + 1): + theta = (j / (resolution * 2)) * 2 * math.pi + y_offset = (i / resolution) * height - half_height + + x = radius * math.cos(theta) + y = radius * math.sin(theta) + z = y_offset + + vertices.append(Point3(x, y, z)) + + # 下半球 + start_idx = len(vertices) + for i in range(resolution // 2 + 1): + phi = math.pi / 2 + (i / (resolution // 2)) * (math.pi / 2) # π/2到π + for j in range(resolution * 2 + 1): + theta = (j / (resolution * 2)) * 2 * math.pi + + x = radius * math.sin(phi) * math.cos(theta) + y = radius * math.sin(phi) * math.sin(theta) + z = radius * math.cos(phi) - half_height + + vertices.append(Point3(x, y, z)) + + return vertices, indices + + def _generate_cylinder_mesh(self, radius: float, height: float, resolution: int) -> Tuple[List[Point3], List[int]]: + """ + 生成圆柱形网格 + + Args: + radius: 半径 + height: 高度 + resolution: 分辨率 + + Returns: + (顶点列表, 索引列表) + """ + vertices = [] + indices = [] + + # 半高度 + half_height = height / 2.0 + + # 侧面顶点 + for i in range(2): # 顶部到底部 + z = half_height if i == 0 else -half_height + for j in range(resolution * 2 + 1): + theta = (j / (resolution * 2)) * 2 * math.pi + x = radius * math.cos(theta) + y = radius * math.sin(theta) + vertices.append(Point3(x, y, z)) + + # 圆柱面索引 + for i in range(resolution * 2): + indices.extend([i, i + 1, i + resolution * 2 + 1]) + indices.extend([i + 1, i + resolution * 2 + 2, i + resolution * 2 + 1]) + + return vertices, indices + + def _generate_rope_vertices(self, point_a: Point3, point_b: Point3, resolution: int) -> List[Point3]: + """ + 生成绳索顶点 + + Args: + point_a: 起点 + point_b: 终点 + resolution: 分辨率 + + Returns: + 顶点列表 + """ + vertices = [] + + # 在两点之间生成顶点 + for i in range(resolution): + t = i / (resolution - 1) if resolution > 1 else 0 + pos = point_a + (point_b - point_a) * t + vertices.append(pos) + + return vertices + + def _generate_rope_indices(self, resolution: int) -> List[int]: + """ + 生成绳索索引 + + Args: + resolution: 分辨率 + + Returns: + 索引列表 + """ + return list(range(resolution)) + + def _generate_patch_mesh(self, width: float, height: float, resolution_x: int, resolution_y: int) -> Tuple[List[Point3], List[int]]: + """ + 生成布料片网格 + + Args: + width: 宽度 + height: 高度 + resolution_x: X分辨率 + resolution_y: Y分辨率 + + Returns: + (顶点列表, 索引列表) + """ + vertices = [] + indices = [] + + # 半宽高 + hw = width / 2.0 + hh = height / 2.0 + + # 生成顶点 + for j in range(resolution_y + 1): + for i in range(resolution_x + 1): + x = (i / resolution_x) * width - hw + y = (j / resolution_y) * height - hh + z = 0.0 + vertices.append(Point3(x, y, z)) + + # 生成面索引 + for j in range(resolution_y): + for i in range(resolution_x): + v0 = j * (resolution_x + 1) + i + v1 = v0 + 1 + v2 = (j + 1) * (resolution_x + 1) + i + v3 = v2 + 1 + + indices.extend([v0, v1, v2, v1, v3, v2]) + + return vertices, indices + + def _add_links_from_indices(self, soft_body: BulletSoftBodyNode, indices: List[int]): + """ + 根据索引添加链接 + + Args: + soft_body: 软体节点 + indices: 索引列表 + """ + try: + # 创建边集合以避免重复 + edges = set() + + # 从三角形面提取边 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + # 三角形的三条边 + edges.add(tuple(sorted([indices[i], indices[i+1]]))) + edges.add(tuple(sorted([indices[i+1], indices[i+2]]))) + edges.add(tuple(sorted([indices[i+2], indices[i]]))) + + # 添加链接 + for edge in edges: + soft_body.appendLink(edge[0], edge[1]) + + except Exception as e: + print(f"⚠ 添加链接失败: {e}") + + def _add_bending_constraints(self, soft_body: BulletSoftBodyNode): + """ + 添加弯曲约束 + + Args: + soft_body: 软体节点 + """ + try: + # 添加弯曲约束 + # 这个参数控制弯曲约束的生成步长 + soft_body.generateBendingConstraints(2) + + except Exception as e: + print(f"⚠ 添加弯曲约束失败: {e}") + + def _calculate_mass_distribution(self, vertices: List[Point3], params: Dict[str, Any]) -> Dict[str, Any]: + """ + 计算质量分布 + + Args: + vertices: 顶点列表 + params: 参数字典 + + Returns: + 质量分布信息 + """ + mass_distribution = { + 'type': params.get('mass_distribution', 'uniform'), + 'total_mass': 0.0, + 'center_of_mass': Point3(0, 0, 0), + 'mass_per_vertex': [] + } + + if not vertices: + return mass_distribution + + # 计算总质量(基于密度和估算的体积) + if mass_distribution['type'] == 'uniform': + # 统一质量分布 + density = params.get('density', 1.0) + estimated_volume = len(vertices) * 0.01 # 简单估算 + total_mass = density * estimated_volume + mass_per_vertex = total_mass / len(vertices) if len(vertices) > 0 else 0.0 + + mass_distribution['total_mass'] = total_mass + mass_distribution['mass_per_vertex'] = [mass_per_vertex] * len(vertices) + + # 计算质心 + center = Point3(0, 0, 0) + for vertex in vertices: + center += vertex + center /= len(vertices) + mass_distribution['center_of_mass'] = center + + return mass_distribution + + def _check_tear_conditions(self, soft_info: Dict[str, Any]): + """ + 检查撕裂条件 + + Args: + soft_info: 软体对象信息 + """ + try: + tear_mode = soft_info['params'].get('tear_mode', 'none') + if tear_mode == 'none': + return + + tear_threshold = soft_info['params'].get('tear_threshold', 100.0) + # 在实际实现中,这里会检查撕裂条件 + # 由于BulletSoftBodyNode目前不直接支持撕裂,这里简化处理 + + except Exception as e: + print(f"⚠ 检查撕裂条件失败: {e}") + + def _update_collision_stats(self, soft_info: Dict[str, Any]): + """ + 更新碰撞统计 + + Args: + soft_info: 软体对象信息 + """ + try: + # 在实际实现中,这里会更新碰撞统计 + # 简化处理 + pass + + except Exception as e: + print(f"⚠ 更新碰撞统计失败: {e}") + + def _get_physics_world(self): + """ + 获取物理世界对象 + + Returns: + 物理世界对象或None + """ + try: + # 尝试从世界对象获取物理世界 + if hasattr(self.world, 'physics_world'): + return self.world.physics_world.bullet_world + elif hasattr(self.world, 'bullet_world'): + return self.world.bullet_world + else: + # 查找物理插件 + physics_plugin = self.world.plugins.get('rigid_body_physics') + if physics_plugin and hasattr(physics_plugin, 'physics_world'): + return physics_plugin.physics_world.bullet_world + except Exception as e: + print(f"⚠ 获取物理世界失败: {e}") + return None \ No newline at end of file diff --git a/plugins/user/soft_body_cloth_physics/core/soft_body_physics.py b/plugins/user/soft_body_cloth_physics/core/soft_body_physics.py index 7201008f..f78621d5 100644 --- a/plugins/user/soft_body_cloth_physics/core/soft_body_physics.py +++ b/plugins/user/soft_body_cloth_physics/core/soft_body_physics.py @@ -1,1347 +1,1347 @@ -""" -软体物理实现 -提供软体物理模拟的具体实现 -包含完整的软体物理计算和仿真功能 -""" - -from panda3d.core import Vec3, Point3, GeomVertexReader, GeomVertexFormat -from panda3d.core import GeomVertexData, Geom, GeomNode, NodePath -from panda3d.bullet import BulletSoftBodyNode, BulletSoftBodyWorldInfo -import math -from typing import List, Dict, Optional, Any, Tuple -import numpy as np - - -class SoftBodyPhysics: - """ - 软体物理实现类 - 处理软体的具体物理模拟,包括弹性力学、粘弹性行为、流体交互等复杂物理效应 - """ - - # 软体材质类型 - SOFT_TYPE_RUBBER = "rubber" - SOFT_TYPE_JELLY = "jelly" - SOFT_TYPE_MUSCLE = "muscle" - SOFT_TYPE_FOAM = "foam" - SOFT_TYPE_GEL = "gel" - SOFT_TYPE_SILICONE = "silicone" - SOFT_TYPE_BALLOON = "balloon" - SOFT_TYPE_BUBBLE = "bubble" - SOFT_TYPE_TISSUE = "tissue" - SOFT_TYPE_CARTILAGE = "cartilage" - - # 软体行为模式 - BEHAVIOR_DEFAULT = "default" - BEHAVIOR_RIGID = "rigid" - BEHAVIOR_SOFT = "soft" - BEHAVIOR_FLUID = "fluid" - BEHAVIOR_ELASTIC = "elastic" - - # 约束类型 - CONSTRAINT_FIXED = "fixed" - CONSTRAINT_P2P = "p2p" # 点对点 - CONSTRAINT_HINGE = "hinge" # 铰链 - CONSTRAINT_SLIDER = "slider" # 滑动 - CONSTRAINT_CONETWIST = "conetwist" # 锥形扭转 - CONSTRAINT_DOF = "dof" # 6自由度 - - def __init__(self, world): - self.world = world - self.soft_body_world_info = None - self._init_soft_body_world() - - # 预定义的软体材质参数 - self.soft_materials = { - self.SOFT_TYPE_RUBBER: { - 'density': 1.2, - 'linear_stiffness': 0.9, - 'angular_stiffness': 0.8, - 'volume_stiffness': 0.7, - 'friction': 0.8, - 'restitution': 0.6, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.01, - 'lift_coefficient': 0.005, - 'bend_stiffness': 0.8, - 'damping': 0.02, - 'plastic_threshold': 0.3, - 'plastic_stiffness': 0.8, - 'tear_threshold': 200.0, - 'tear_resistance': 100.0, - 'anisotropic_friction': [0.8, 0.8, 0.8], - 'dynamic_friction': 0.8, - 'static_friction': 0.9, - 'rolling_friction': 0.2, - 'spinning_friction': 0.1, - 'viscosity': 0.01, - 'compressibility': 0.1, - 'shear_modulus': 0.8, - 'bulk_modulus': 1.2, - 'youngs_modulus': 1.0, - 'poisson_ratio': 0.4 - }, - self.SOFT_TYPE_JELLY: { - 'density': 1.1, - 'linear_stiffness': 0.6, - 'angular_stiffness': 0.5, - 'volume_stiffness': 0.8, - 'friction': 0.7, - 'restitution': 0.2, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.02, - 'lift_coefficient': 0.01, - 'bend_stiffness': 0.4, - 'damping': 0.015, - 'plastic_threshold': 0.1, - 'plastic_stiffness': 0.3, - 'tear_threshold': 80.0, - 'tear_resistance': 40.0, - 'anisotropic_friction': [0.7, 0.7, 0.7], - 'dynamic_friction': 0.7, - 'static_friction': 0.8, - 'rolling_friction': 0.15, - 'spinning_friction': 0.07, - 'viscosity': 0.03, - 'compressibility': 0.2, - 'shear_modulus': 0.4, - 'bulk_modulus': 0.9, - 'youngs_modulus': 0.6, - 'poisson_ratio': 0.35 - }, - self.SOFT_TYPE_MUSCLE: { - 'density': 1.05, - 'linear_stiffness': 0.8, - 'angular_stiffness': 0.7, - 'volume_stiffness': 0.6, - 'friction': 0.6, - 'restitution': 0.3, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.015, - 'lift_coefficient': 0.008, - 'bend_stiffness': 0.5, - 'damping': 0.012, - 'plastic_threshold': 0.15, - 'plastic_stiffness': 0.5, - 'tear_threshold': 120.0, - 'tear_resistance': 60.0, - 'anisotropic_friction': [0.6, 0.6, 0.6], - 'dynamic_friction': 0.6, - 'static_friction': 0.7, - 'rolling_friction': 0.12, - 'spinning_friction': 0.06, - 'viscosity': 0.02, - 'compressibility': 0.15, - 'shear_modulus': 0.7, - 'bulk_modulus': 1.1, - 'youngs_modulus': 0.85, - 'poisson_ratio': 0.42 - }, - self.SOFT_TYPE_FOAM: { - 'density': 0.3, - 'linear_stiffness': 0.4, - 'angular_stiffness': 0.3, - 'volume_stiffness': 0.5, - 'friction': 0.5, - 'restitution': 0.1, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.03, - 'lift_coefficient': 0.02, - 'bend_stiffness': 0.25, - 'damping': 0.008, - 'plastic_threshold': 0.05, - 'plastic_stiffness': 0.2, - 'tear_threshold': 60.0, - 'tear_resistance': 25.0, - 'anisotropic_friction': [0.5, 0.5, 0.5], - 'dynamic_friction': 0.5, - 'static_friction': 0.6, - 'rolling_friction': 0.05, - 'spinning_friction': 0.02, - 'viscosity': 0.05, - 'compressibility': 0.3, - 'shear_modulus': 0.2, - 'bulk_modulus': 0.7, - 'youngs_modulus': 0.4, - 'poisson_ratio': 0.3 - }, - self.SOFT_TYPE_GEL: { - 'density': 1.3, - 'linear_stiffness': 0.7, - 'angular_stiffness': 0.6, - 'volume_stiffness': 0.9, - 'friction': 0.75, - 'restitution': 0.25, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.012, - 'lift_coefficient': 0.006, - 'bend_stiffness': 0.55, - 'damping': 0.018, - 'plastic_threshold': 0.12, - 'plastic_stiffness': 0.45, - 'tear_threshold': 110.0, - 'tear_resistance': 55.0, - 'anisotropic_friction': [0.75, 0.75, 0.75], - 'dynamic_friction': 0.75, - 'static_friction': 0.85, - 'rolling_friction': 0.14, - 'spinning_friction': 0.07, - 'viscosity': 0.025, - 'compressibility': 0.12, - 'shear_modulus': 0.6, - 'bulk_modulus': 0.95, - 'youngs_modulus': 0.7, - 'poisson_ratio': 0.38 - }, - self.SOFT_TYPE_SILICONE: { - 'density': 1.15, - 'linear_stiffness': 0.75, - 'angular_stiffness': 0.65, - 'volume_stiffness': 0.75, - 'friction': 0.8, - 'restitution': 0.4, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.008, - 'lift_coefficient': 0.004, - 'bend_stiffness': 0.6, - 'damping': 0.01, - 'plastic_threshold': 0.2, - 'plastic_stiffness': 0.6, - 'tear_threshold': 140.0, - 'tear_resistance': 70.0, - 'anisotropic_friction': [0.8, 0.8, 0.8], - 'dynamic_friction': 0.8, - 'static_friction': 0.9, - 'rolling_friction': 0.16, - 'spinning_friction': 0.08, - 'viscosity': 0.015, - 'compressibility': 0.18, - 'shear_modulus': 0.65, - 'bulk_modulus': 1.0, - 'youngs_modulus': 0.8, - 'poisson_ratio': 0.41 - }, - self.SOFT_TYPE_BALLOON: { - 'density': 0.1, - 'linear_stiffness': 0.85, - 'angular_stiffness': 0.8, - 'volume_stiffness': 0.9, - 'friction': 0.3, - 'restitution': 0.8, - 'pressure_coefficient': 0.5, - 'drag_coefficient': 0.05, - 'lift_coefficient': 0.025, - 'bend_stiffness': 0.7, - 'damping': 0.005, - 'plastic_threshold': 0.25, - 'plastic_stiffness': 0.7, - 'tear_threshold': 40.0, - 'tear_resistance': 15.0, - 'anisotropic_friction': [0.3, 0.3, 0.3], - 'dynamic_friction': 0.3, - 'static_friction': 0.4, - 'rolling_friction': 0.03, - 'spinning_friction': 0.01, - 'viscosity': 0.005, - 'compressibility': 0.05, - 'shear_modulus': 0.2, - 'bulk_modulus': 0.5, - 'youngs_modulus': 0.3, - 'poisson_ratio': 0.45 - }, - self.SOFT_TYPE_BUBBLE: { - 'density': 0.01, - 'linear_stiffness': 0.9, - 'angular_stiffness': 0.85, - 'volume_stiffness': 0.95, - 'friction': 0.1, - 'restitution': 0.9, - 'pressure_coefficient': 0.8, - 'drag_coefficient': 0.04, - 'lift_coefficient': 0.03, - 'bend_stiffness': 0.75, - 'damping': 0.003, - 'plastic_threshold': 0.1, - 'plastic_stiffness': 0.5, - 'tear_threshold': 20.0, - 'tear_resistance': 5.0, - 'anisotropic_friction': [0.1, 0.1, 0.1], - 'dynamic_friction': 0.1, - 'static_friction': 0.2, - 'rolling_friction': 0.01, - 'spinning_friction': 0.005, - 'viscosity': 0.002, - 'compressibility': 0.02, - 'shear_modulus': 0.1, - 'bulk_modulus': 0.2, - 'youngs_modulus': 0.15, - 'poisson_ratio': 0.47 - }, - self.SOFT_TYPE_TISSUE: { - 'density': 1.05, - 'linear_stiffness': 0.65, - 'angular_stiffness': 0.55, - 'volume_stiffness': 0.65, - 'friction': 0.55, - 'restitution': 0.25, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.018, - 'lift_coefficient': 0.009, - 'bend_stiffness': 0.45, - 'damping': 0.014, - 'plastic_threshold': 0.1, - 'plastic_stiffness': 0.35, - 'tear_threshold': 90.0, - 'tear_resistance': 45.0, - 'anisotropic_friction': [0.55, 0.55, 0.55], - 'dynamic_friction': 0.55, - 'static_friction': 0.65, - 'rolling_friction': 0.1, - 'spinning_friction': 0.05, - 'viscosity': 0.025, - 'compressibility': 0.25, - 'shear_modulus': 0.5, - 'bulk_modulus': 0.8, - 'youngs_modulus': 0.6, - 'poisson_ratio': 0.37 - }, - self.SOFT_TYPE_CARTILAGE: { - 'density': 1.1, - 'linear_stiffness': 0.75, - 'angular_stiffness': 0.7, - 'volume_stiffness': 0.7, - 'friction': 0.65, - 'restitution': 0.35, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.01, - 'lift_coefficient': 0.005, - 'bend_stiffness': 0.6, - 'damping': 0.016, - 'plastic_threshold': 0.18, - 'plastic_stiffness': 0.55, - 'tear_threshold': 130.0, - 'tear_resistance': 65.0, - 'anisotropic_friction': [0.65, 0.65, 0.65], - 'dynamic_friction': 0.65, - 'static_friction': 0.75, - 'rolling_friction': 0.13, - 'spinning_friction': 0.065, - 'viscosity': 0.022, - 'compressibility': 0.18, - 'shear_modulus': 0.65, - 'bulk_modulus': 0.95, - 'youngs_modulus': 0.75, - 'poisson_ratio': 0.39 - } - } - - # 流体交互参数 - self.fluid_interaction = { - 'enabled': False, - 'viscosity': 0.01, - 'buoyancy': 1.0, - 'flow_direction': Vec3(0, 0, 0), - 'flow_strength': 0.0 - } - - # 性能优化参数 - self.max_simulation_steps = 10 - self.time_step = 1.0/60.0 - self.sub_steps = 4 - - def _init_soft_body_world(self): - """ - 初始化软体世界信息 - """ - try: - # 获取物理世界中的软体世界信息 - physics_world = self._get_physics_world() - if physics_world and hasattr(physics_world, 'soft_body_world_info'): - self.soft_body_world_info = physics_world.soft_body_world_info - else: - # 创建新的软体世界信息 - self.soft_body_world_info = BulletSoftBodyWorldInfo() - self.soft_body_world_info.setGravity(Vec3(0, 0, -9.81)) - self.soft_body_world_info.setAirDensity(1.225) - self.soft_body_world_info.setWaterDensity(1000.0) - self.soft_body_world_info.setWaterOffset(0.0) - self.soft_body_world_info.setWaterNormal(Vec3(0, 0, 1)) - - print("✓ 软体世界初始化完成") - - except Exception as e: - print(f"✗ 软体世界初始化失败: {e}") - - def set_fluid_interaction(self, enabled: bool = True, viscosity: float = 0.01, - buoyancy: float = 1.0, flow_direction: Vec3 = None, - flow_strength: float = 0.0): - """ - 设置流体交互参数 - - Args: - enabled: 是否启用流体交互 - viscosity: 粘度 - buoyancy: 浮力 - flow_direction: 流动方向 - flow_strength: 流动强度 - """ - self.fluid_interaction['enabled'] = enabled - self.fluid_interaction['viscosity'] = viscosity - self.fluid_interaction['buoyancy'] = buoyancy - if flow_direction: - self.fluid_interaction['flow_direction'] = flow_direction - self.fluid_interaction['flow_strength'] = flow_strength - - def create_soft_body_with_material(self, node_path, soft_type: str, - shape_type: str = "sphere", - behavior_mode: str = BEHAVIOR_DEFAULT, - **kwargs) -> Optional[BulletSoftBodyNode]: - """ - 使用指定材质创建软体 - - Args: - node_path: 父节点路径 - soft_type: 软体类型 - shape_type: 形状类型 (sphere, box, rope, mesh) - behavior_mode: 行为模式 - **kwargs: 形状参数 - - Returns: - BulletSoftBodyNode对象或None - """ - try: - # 获取材质参数 - if soft_type not in self.soft_materials: - print(f"⚠ 未知软体类型: {soft_type}, 使用默认参数") - base_params = self.soft_materials[self.SOFT_TYPE_RUBBER] - else: - base_params = self.soft_materials[soft_type] - - # 根据行为模式调整参数 - params = self._adjust_parameters_for_behavior(base_params, behavior_mode) - - # 创建软体节点 - soft_name = f'soft_{soft_type}_{shape_type}_{len(getattr(self, "_soft_counter", [0]))}' - soft_body_node = BulletSoftBodyNode(soft_name) - - # 设置软体参数 - self._apply_soft_body_parameters(soft_body_node, params) - - # 根据形状类型创建软体 - if shape_type == "sphere": - radius = kwargs.get('radius', 1.0) - resolution = kwargs.get('resolution', 8) - self._create_sphere_shape(soft_body_node, radius, resolution) - print(f"✓ 创建球形软体: 半径={radius}, 分辨率={resolution}") - elif shape_type == "box": - width = kwargs.get('width', 1.0) - height = kwargs.get('height', 1.0) - depth = kwargs.get('depth', 1.0) - resolution = kwargs.get('resolution', 4) - self._create_box_shape(soft_body_node, width, height, depth, resolution) - print(f"✓ 创建盒形软体: 宽={width}, 高={height}, 深={depth}, 分辨率={resolution}") - elif shape_type == "rope": - point_a = kwargs.get('point_a', Point3(0, 0, 0)) - point_b = kwargs.get('point_b', Point3(0, 0, 1)) - resolution = kwargs.get('resolution', 10) - self._create_rope_shape(soft_body_node, point_a, point_b, resolution) - print(f"✓ 创建绳索软体: 从{point_a}到{point_b}, 分辨率={resolution}") - elif shape_type == "mesh": - mesh_node = kwargs.get('mesh_node') - if mesh_node: - self._create_mesh_shape(soft_body_node, mesh_node) - print(f"✓ 从网格创建软体") - else: - print("✗ 网格节点参数缺失") - return None - elif shape_type == "capsule": - radius = kwargs.get('radius', 1.0) - height = kwargs.get('height', 2.0) - resolution = kwargs.get('resolution', 8) - self._create_capsule_shape(soft_body_node, radius, height, resolution) - print(f"✓ 创建胶囊软体: 半径={radius}, 高度={height}, 分辨率={resolution}") - elif shape_type == "cylinder": - radius = kwargs.get('radius', 1.0) - height = kwargs.get('height', 2.0) - resolution = kwargs.get('resolution', 8) - self._create_cylinder_shape(soft_body_node, radius, height, resolution) - print(f"✓ 创建圆柱软体: 半径={radius}, 高度={height}, 分辨率={resolution}") - - # 创建软体节点路径 - soft_np = node_path.attachNewNode(soft_body_node) - - # 添加到物理世界 - physics_world = self._get_physics_world() - if physics_world: - physics_world.attachSoftBody(soft_body_node) - - print(f"✓ 创建{soft_type}软体: {soft_name}") - return soft_body_node - - except Exception as e: - print(f"✗ 创建{soft_type}软体失败: {e}") - import traceback - traceback.print_exc() - return None - - def create_soft_body_from_mesh(self, node_path, soft_type: str, - behavior_mode: str = BEHAVIOR_DEFAULT, - params: Dict[str, Any] = None) -> Optional[BulletSoftBodyNode]: - """ - 从网格创建软体 - - Args: - node_path: 网格节点路径 - soft_type: 软体类型 - behavior_mode: 行为模式 - params: 额外参数 - - Returns: - BulletSoftBodyNode对象或None - """ - try: - # 获取材质参数 - if soft_type not in self.soft_materials: - print(f"⚠ 未知软体类型: {soft_type}, 使用默认参数") - base_params = self.soft_materials[self.SOFT_TYPE_RUBBER] - else: - base_params = self.soft_materials[soft_type] - - # 合并自定义参数 - if params: - base_params.update(params) - - # 根据行为模式调整参数 - params = self._adjust_parameters_for_behavior(base_params, behavior_mode) - - # 创建软体节点 - soft_name = f'soft_mesh_{soft_type}_{len(getattr(self, "_soft_counter", [0]))}' - soft_body_node = BulletSoftBodyNode(soft_name) - - # 设置软体参数 - self._apply_soft_body_parameters(soft_body_node, params) - - # 从网格提取数据 - vertices, indices = self._extract_mesh_data(node_path) - if not vertices or not indices: - print("⚠ 无法从网格提取顶点数据") - return None - - # 添加顶点到软体 - for vertex in vertices: - soft_body_node.appendNode(vertex, 1.0) - - # 添加面 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) - - # 添加链接 - self._add_links_from_indices(soft_body_node, indices) - - # 添加弯曲约束 - self._add_bending_constraints(soft_body_node, params) - - # 创建软体节点路径 - soft_np = node_path.attachNewNode(soft_body_node) - soft_np.setPos(node_path.getPos()) - soft_np.setHpr(node_path.getHpr()) - - # 添加到物理世界 - physics_world = self._get_physics_world() - if physics_world: - physics_world.attachSoftBody(soft_body_node) - - print(f"✓ 从网格创建{soft_type}软体: {soft_name} (顶点数: {len(vertices)})") - return soft_body_node - - except Exception as e: - print(f"✗ 从网格创建{soft_type}软体失败: {e}") - import traceback - traceback.print_exc() - return None - - def apply_force_to_soft_body(self, soft_body: BulletSoftBodyNode, force: Vec3, - position: Point3 = None, mode: str = "add"): - """ - 对软体应用力 - - Args: - soft_body: 软体节点 - force: 力向量 - position: 应力位置(如果为None,则应用到质心) - mode: 应力模式 ("add", "set", "impulse") - """ - try: - # 在实际实现中,这里会应用力到软体 - # 根据模式应用力 - if mode == "add": - # 添加力 - pass - elif mode == "set": - # 设置力 - pass - elif mode == "impulse": - # 应用冲量 - pass - - print(f"✓ 对软体应用{mode}力: {force}") - - except Exception as e: - print(f"✗ 应用力失败: {e}") - - def apply_force_to_region(self, soft_body: BulletSoftBodyNode, - force: Vec3, region_center: Point3, - radius: float, mode: str = "add") -> bool: - """ - 对软体的特定区域应用力 - - Args: - soft_body: 软体节点 - force: 力向量 - region_center: 区域中心点 - radius: 区域半径 - mode: 应力模式 - - Returns: - 是否成功应用力 - """ - try: - # 获取软体节点 - nodes = soft_body.get_nodes() - - # 查找受影响的节点 - affected_nodes = [] - for i, node in enumerate(nodes): - node_pos = node.get_position() - distance = (node_pos - region_center).length() - if distance <= radius: - affected_nodes.append((i, node, distance)) - - # 应用力到受影响的节点 - force_magnitude = force.length() - for node_index, node, distance in affected_nodes: - # 根据距离衰减力的大小 - decay_factor = 1.0 - (distance / radius) - if decay_factor > 0: - applied_force = force * decay_factor - # 在实际实现中,这里会应用力到节点 - # node.add_velocity(applied_force) - - print(f"✓ 对软体区域应用力: {len(affected_nodes)} 个节点受影响") - return True - - except Exception as e: - print(f"✗ 应用力到区域失败: {e}") - return False - - def simulate_deformation(self, soft_body: BulletSoftBodyNode, - force: Vec3, duration: float) -> bool: - """ - 模拟软体变形 - - Args: - soft_body: 软体节点 - force: 变形力 - duration: 持续时间 - - Returns: - 是否成功模拟变形 - """ - try: - # 在实际实现中,这里会模拟软体在持续力作用下的变形 - # 通过逐步施加力并更新软体状态来模拟变形过程 - - # 计算子步长 - sub_steps = max(1, int(duration / self.time_step)) - sub_force = force / sub_steps - - for i in range(sub_steps): - # 应用力到软体 - self.apply_force_to_soft_body(soft_body, sub_force) - - print(f"✓ 模拟软体变形: 持续时间 {duration}s") - return True - - except Exception as e: - print(f"✗ 模拟变形失败: {e}") - return False - - def set_soft_body_parameters(self, soft_body: BulletSoftBodyNode, params: Dict[str, Any]): - """ - 设置软体参数 - - Args: - soft_body: 软体节点 - params: 参数字典 - """ - try: - self._apply_soft_body_parameters(soft_body, params) - print("✓ 设置软体参数完成") - - except Exception as e: - print(f"✗ 设置软体参数失败: {e}") - - def get_material_parameters(self, soft_type: str) -> Optional[Dict[str, Any]]: - """ - 获取指定材质的参数 - - Args: - soft_type: 软体类型 - - Returns: - 参数字典或None - """ - return self.soft_materials.get(soft_type) - - def get_all_material_types(self) -> List[str]: - """ - 获取所有可用的软体材质类型 - - Returns: - 材质类型列表 - """ - return list(self.soft_materials.keys()) - - def add_constraint(self, soft_body1: BulletSoftBodyNode, soft_body2: BulletSoftBodyNode, - constraint_type: str = CONSTRAINT_P2P, params: Dict[str, Any] = None) -> bool: - """ - 在两个软体之间添加约束 - - Args: - soft_body1: 第一个软体 - soft_body2: 第二个软体 - constraint_type: 约束类型 - params: 约束参数 - - Returns: - 是否成功添加约束 - """ - try: - # 在实际实现中,这里会添加软体间的约束 - # Bullet提供多种约束类型 - - print(f"✓ 添加{constraint_type}约束") - return True - - except Exception as e: - print(f"✗ 添加约束失败: {e}") - return False - - def update_soft_body_world_info(self, air_density: float = None, - water_density: float = None, - water_offset: float = None, - water_normal: Vec3 = None, - gravity: Vec3 = None): - """ - 更新软体世界信息 - - Args: - air_density: 空气密度 - water_density: 水密度 - water_offset: 水偏移 - water_normal: 水法线 - gravity: 重力向量 - """ - try: - if not self.soft_body_world_info: - print("⚠ 软体世界信息未初始化") - return - - if air_density is not None: - self.soft_body_world_info.setAirDensity(air_density) - if water_density is not None: - self.soft_body_world_info.setWaterDensity(water_density) - if water_offset is not None: - self.soft_body_world_info.setWaterOffset(water_offset) - if water_normal is not None: - self.soft_body_world_info.setWaterNormal(water_normal) - if gravity is not None: - self.soft_body_world_info.setGravity(gravity) - - print("✓ 更新软体世界信息完成") - - except Exception as e: - print(f"✗ 更新软体世界信息失败: {e}") - - def calculate_soft_body_properties(self, vertices: List[Point3], indices: List[int], - params: Dict[str, Any]) -> Dict[str, Any]: - """ - 计算软体属性 - - Args: - vertices: 顶点列表 - indices: 索引列表 - params: 参数字典 - - Returns: - 属性字典 - """ - try: - properties = { - 'vertex_count': len(vertices), - 'face_count': len(indices) // 3, - 'edge_count': 0, - 'volume': 0.0, - 'surface_area': 0.0, - 'center_of_mass': Point3(0, 0, 0), - 'mass': 0.0, - 'elasticity': params.get('youngs_modulus', 1.0), - 'viscosity': params.get('viscosity', 0.01), - 'compressibility': params.get('compressibility', 0.1), - 'bounding_box': { - 'min': Point3(float('inf'), float('inf'), float('inf')), - 'max': Point3(float('-inf'), float('-inf'), float('-inf')) - } - } - - if not vertices: - return properties - - # 计算包围盒 - for vertex in vertices: - properties['bounding_box']['min'].set( - min(properties['bounding_box']['min'][0], vertex[0]), - min(properties['bounding_box']['min'][1], vertex[1]), - min(properties['bounding_box']['min'][2], vertex[2]) - ) - properties['bounding_box']['max'].set( - max(properties['bounding_box']['max'][0], vertex[0]), - max(properties['bounding_box']['max'][1], vertex[1]), - max(properties['bounding_box']['max'][2], vertex[2]) - ) - - # 计算质心 - center = Point3(0, 0, 0) - for vertex in vertices: - center += vertex - center /= len(vertices) - properties['center_of_mass'] = center - - # 计算体积和表面积 - total_area = 0.0 - total_volume = 0.0 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - v0 = vertices[indices[i]] - v1 = vertices[indices[i+1]] - v2 = vertices[indices[i+2]] - # 计算三角形面积 - edge1 = v1 - v0 - edge2 = v2 - v0 - cross = edge1.cross(edge2) - area = cross.length() * 0.5 - total_area += area - - # 估算体积(简化计算) - # 实际体积需要更复杂的算法 - # 这里使用一个简化估算 - volume_contribution = area * abs(cross.length()) / 6.0 - total_volume += volume_contribution - - properties['surface_area'] = total_area - properties['volume'] = total_volume - - # 计算质量(基于密度和体积) - density = params.get('density', 1.0) - properties['mass'] = density * total_volume - - return properties - - except Exception as e: - print(f"✗ 计算软体属性失败: {e}") - return {} - - def _apply_soft_body_parameters(self, soft_body: BulletSoftBodyNode, params: Dict[str, Any]): - """ - 应用软体参数到软体节点 - - Args: - soft_body: 软体节点 - params: 参数字典 - """ - cfg = soft_body.get_cfg() - - # 物理参数 - if 'linear_stiffness' in params: - cfg.set_kDF(params['linear_stiffness']) - if 'angular_stiffness' in params: - cfg.set_kDG(params['angular_stiffness']) - if 'volume_stiffness' in params: - cfg.set_kVC(params['volume_stiffness']) - if 'pressure_coefficient' in params: - cfg.set_kPR(params['pressure_coefficient']) - if 'drag_coefficient' in params: - cfg.set_kDG(params['drag_coefficient']) - if 'lift_coefficient' in params: - cfg.set_kLF(params['lift_coefficient']) - if 'bend_stiffness' in params: - # 弯曲刚度 - 用作线性刚度的补充 - cfg.set_kDF(params['bend_stiffness'] * 0.5) - if 'damping' in params: - cfg.set_dft_damping(params['damping']) - if 'plastic_threshold' in params: - cfg.set_kMT(params['plastic_threshold']) - if 'plastic_stiffness' in params: - cfg.set_kDP(params['plastic_stiffness']) - - # 材质属性 - if 'friction' in params: - soft_body.setFriction(params['friction']) - if 'restitution' in params: - soft_body.setRestitution(params['restitution']) - if 'density' in params: - soft_body.setTotalMass(params['density']) - if 'anisotropic_friction' in params: - friction_vec = params['anisotropic_friction'] - if len(friction_vec) >= 3: - soft_body.setAnisotropicFriction( - Vec3(friction_vec[0], friction_vec[1], friction_vec[2]) - ) - - # 高级材质参数 - if 'viscosity' in params: - # 在实际实现中,这里会影响软体的粘性行为 - pass - if 'compressibility' in params: - # 在实际实现中,这里会影响软体的压缩性 - pass - if 'shear_modulus' in params: - # 剪切模量 - pass - if 'bulk_modulus' in params: - # 体积模量 - pass - if 'youngs_modulus' in params: - # 杨氏模量 - pass - if 'poisson_ratio' in params: - # 泊松比 - pass - - # 高级摩擦参数 - if 'dynamic_friction' in params: - # 动态摩擦系数 - pass - if 'static_friction' in params: - # 静态摩擦系数 - pass - if 'rolling_friction' in params: - # 滚动摩擦系数 - pass - if 'spinning_friction' in params: - # 自旋摩擦系数 - pass - - def _adjust_parameters_for_behavior(self, params: Dict[str, Any], behavior_mode: str) -> Dict[str, Any]: - """ - 根据行为模式调整参数 - - Args: - params: 原始参数 - behavior_mode: 行为模式 - - Returns: - 调整后的参数 - """ - adjusted_params = params.copy() - - if behavior_mode == self.BEHAVIOR_RIGID: - adjusted_params['linear_stiffness'] = min(1.0, adjusted_params['linear_stiffness'] * 2.0) - adjusted_params['angular_stiffness'] = min(1.0, adjusted_params['angular_stiffness'] * 2.0) - adjusted_params['volume_stiffness'] = min(1.0, adjusted_params['volume_stiffness'] * 2.0) - adjusted_params['damping'] = max(0.001, adjusted_params['damping'] * 0.5) - elif behavior_mode == self.BEHAVIOR_SOFT: - adjusted_params['linear_stiffness'] = max(0.1, adjusted_params['linear_stiffness'] * 0.5) - adjusted_params['angular_stiffness'] = max(0.1, adjusted_params['angular_stiffness'] * 0.5) - adjusted_params['volume_stiffness'] = max(0.1, adjusted_params['volume_stiffness'] * 0.5) - adjusted_params['damping'] = min(0.1, adjusted_params['damping'] * 2.0) - elif behavior_mode == self.BEHAVIOR_FLUID: - adjusted_params['linear_stiffness'] = max(0.05, adjusted_params['linear_stiffness'] * 0.1) - adjusted_params['angular_stiffness'] = max(0.05, adjusted_params['angular_stiffness'] * 0.1) - adjusted_params['volume_stiffness'] = max(0.05, adjusted_params['volume_stiffness'] * 0.1) - adjusted_params['damping'] = min(0.2, adjusted_params['damping'] * 5.0) - adjusted_params['viscosity'] = min(1.0, adjusted_params['viscosity'] * 10.0) - elif behavior_mode == self.BEHAVIOR_ELASTIC: - adjusted_params['linear_stiffness'] = min(1.0, adjusted_params['linear_stiffness'] * 1.5) - adjusted_params['restitution'] = min(1.0, adjusted_params['restitution'] * 1.5) - adjusted_params['damping'] = max(0.001, adjusted_params['damping'] * 0.7) - - return adjusted_params - - def _create_sphere_shape(self, soft_body: BulletSoftBodyNode, radius: float, resolution: int): - """ - 创建球形软体 - - Args: - soft_body: 软体节点 - radius: 半径 - resolution: 分辨率 - """ - # 生成球面顶点 - vertices = [] - for i in range(resolution + 1): - phi = (i / resolution) * math.pi # 纬度角 - for j in range(resolution * 2 + 1): - theta = (j / (resolution * 2)) * 2 * math.pi # 经度角 - - x = radius * math.sin(phi) * math.cos(theta) - y = radius * math.sin(phi) * math.sin(theta) - z = radius * math.cos(phi) - - vertices.append(Point3(x, y, z)) - - # 添加顶点 - for vertex in vertices: - soft_body.appendNode(vertex, 1.0) - - # 生成面索引 - indices = [] - for i in range(resolution): - for j in range(resolution * 2): - v0 = i * (resolution * 2 + 1) + j - v1 = v0 + 1 - v2 = (i + 1) * (resolution * 2 + 1) + j - v3 = v2 + 1 - - if i < resolution - 1: - indices.extend([v0, v1, v2]) - indices.extend([v1, v3, v2]) - - # 添加面 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - soft_body.appendFace(indices[i], indices[i+1], indices[i+2]) - - # 添加链接 - self._add_links_from_indices(soft_body, indices) - - # 添加弯曲约束 - self._add_bending_constraints(soft_body, {}) - - def _create_box_shape(self, soft_body: BulletSoftBodyNode, width: float, height: float, depth: float, resolution: int): - """ - 创建盒形软体 - - Args: - soft_body: 软体节点 - width: 宽度 - height: 高度 - depth: 深度 - resolution: 分辨率 - """ - # 实现盒形软体创建 - # 这里是简化版本,实际实现需要更详细的网格生成 - hw = width / 2.0 - hh = height / 2.0 - hd = depth / 2.0 - - # 生成八个角点 - vertices = [ - Point3(-hw, -hd, -hh), # 0 - Point3(hw, -hd, -hh), # 1 - Point3(hw, hd, -hh), # 2 - Point3(-hw, hd, -hh), # 3 - Point3(-hw, -hd, hh), # 4 - Point3(hw, -hd, hh), # 5 - Point3(hw, hd, hh), # 6 - Point3(-hw, hd, hh) # 7 - ] - - # 添加顶点 - for vertex in vertices: - soft_body.appendNode(vertex, 1.0) - - # 添加面(简化的立方体面) - faces = [ - [0, 1, 2, 3], # 底面 - [4, 7, 6, 5], # 顶面 - [0, 4, 5, 1], # 前面 - [2, 6, 7, 3], # 后面 - [0, 3, 7, 4], # 左面 - [1, 5, 6, 2] # 右面 - ] - - # 添加面 - for face in faces: - soft_body.appendFace(face[0], face[1], face[2]) - soft_body.appendFace(face[0], face[2], face[3]) - - # 添加链接 - indices = [] - for face in faces: - indices.extend(face) - self._add_links_from_indices(soft_body, indices) - - # 添加弯曲约束 - self._add_bending_constraints(soft_body, {}) - - def _create_rope_shape(self, soft_body: BulletSoftBodyNode, point_a: Point3, point_b: Point3, resolution: int): - """ - 创建绳索软体 - - Args: - soft_body: 软体节点 - point_a: 起点 - point_b: 终点 - resolution: 分辨率 - """ - # 生成绳索顶点 - vertices = [] - for i in range(resolution): - t = i / (resolution - 1) if resolution > 1 else 0 - pos = point_a + (point_b - point_a) * t - vertices.append(pos) - - # 添加顶点 - for vertex in vertices: - soft_body.appendNode(vertex, 1.0) - - # 添加链接 - for i in range(len(vertices) - 1): - soft_body.appendLink(i, i + 1) - - def _create_capsule_shape(self, soft_body: BulletSoftBodyNode, radius: float, height: float, resolution: int): - """ - 创建胶囊形软体 - - Args: - soft_body: 软体节点 - radius: 半径 - height: 高度 - resolution: 分辨率 - """ - # 实现胶囊形软体创建 - # 胶囊是圆柱体加上两个半球 - half_height = height / 2.0 - - vertices = [] - - # 生成上半球 - for i in range(resolution // 2 + 1): - phi = (i / (resolution // 2)) * (math.pi / 2) # 0到π/2 - for j in range(resolution * 2 + 1): - theta = (j / (resolution * 2)) * 2 * math.pi - x = radius * math.sin(phi) * math.cos(theta) - y = radius * math.sin(phi) * math.sin(theta) - z = radius * math.cos(phi) + half_height - vertices.append(Point3(x, y, z)) - - # 生成圆柱部分 - for i in range(resolution + 1): - for j in range(resolution * 2 + 1): - theta = (j / (resolution * 2)) * 2 * math.pi - y_offset = (i / resolution) * height - half_height - x = radius * math.cos(theta) - y = radius * math.sin(theta) - z = y_offset - vertices.append(Point3(x, y, z)) - - # 生成下半球 - for i in range(resolution // 2 + 1): - phi = math.pi / 2 + (i / (resolution // 2)) * (math.pi / 2) # π/2到π - for j in range(resolution * 2 + 1): - theta = (j / (resolution * 2)) * 2 * math.pi - x = radius * math.sin(phi) * math.cos(theta) - y = radius * math.sin(phi) * math.sin(theta) - z = radius * math.cos(phi) - half_height - vertices.append(Point3(x, y, z)) - - # 添加顶点 - for vertex in vertices: - soft_body.appendNode(vertex, 1.0) - - # 这里需要生成适当的面索引 - # 为简化,我们使用一个基本的索引生成方法 - indices = [] - # 生成面的索引是复杂的,这里我们使用一个简化的处理方式 - # 实际实现需要更复杂的索引生成算法 - - # 添加链接 - # 这里也需要根据胶囊的几何结构添加适当的链接 - # 为简化,我们连接相邻的顶点 - for i in range(len(vertices) - 1): - soft_body.appendLink(i, i + 1) - - # 添加弯曲约束 - self._add_bending_constraints(soft_body, {}) - - def _create_cylinder_shape(self, soft_body: BulletSoftBodyNode, radius: float, height: float, resolution: int): - """ - 创建圆柱形软体 - - Args: - soft_body: 软体节点 - radius: 半径 - height: 高度 - resolution: 分辨率 - """ - # 实现圆柱形软体创建 - half_height = height / 2.0 - - vertices = [] - - # 生成圆柱面 - for i in range(2): # 顶部到底部 - z = half_height if i == 0 else -half_height - for j in range(resolution * 2 + 1): - theta = (j / (resolution * 2)) * 2 * math.pi - x = radius * math.cos(theta) - y = radius * math.sin(theta) - vertices.append(Point3(x, y, z)) - - # 添加顶点 - for vertex in vertices: - soft_body.appendNode(vertex, 1.0) - - # 生成面索引 - indices = [] - for i in range(resolution * 2): - indices.extend([i, i + 1, i + resolution * 2 + 1]) - indices.extend([i + 1, i + resolution * 2 + 2, i + resolution * 2 + 1]) - - # 添加面 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - soft_body.appendFace(indices[i], indices[i+1], indices[i+2]) - - # 添加链接 - self._add_links_from_indices(soft_body, indices) - - # 添加弯曲约束 - self._add_bending_constraints(soft_body, {}) - - def _create_mesh_shape(self, soft_body: BulletSoftBodyNode, mesh_node: NodePath): - """ - 从网格创建软体 - - Args: - soft_body: 软体节点 - mesh_node: 网格节点 - """ - # 从网格节点提取顶点和面 - vertices, indices = self._extract_mesh_data(mesh_node) - - if not vertices or not indices: - print("⚠ 无法从网格提取数据") - return - - # 添加顶点 - for vertex in vertices: - soft_body.appendNode(vertex, 1.0) - - # 添加面 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - soft_body.appendFace(indices[i], indices[i+1], indices[i+2]) - - # 添加链接 - self._add_links_from_indices(soft_body, indices) - - # 添加弯曲约束 - self._add_bending_constraints(soft_body, {}) - - def _add_links_from_indices(self, soft_body: BulletSoftBodyNode, indices: List[int]): - """ - 根据索引添加链接 - - Args: - soft_body: 软体节点 - indices: 索引列表 - """ - try: - # 创建边集合以避免重复 - edges = set() - - # 从三角形面提取边 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - # 三角形的三条边 - edges.add(tuple(sorted([indices[i], indices[i+1]]))) - edges.add(tuple(sorted([indices[i+1], indices[i+2]]))) - edges.add(tuple(sorted([indices[i+2], indices[i]]))) - - # 添加链接 - for edge in edges: - soft_body.appendLink(edge[0], edge[1]) - - except Exception as e: - print(f"⚠ 添加链接失败: {e}") - - def _add_bending_constraints(self, soft_body: BulletSoftBodyNode, params: Dict[str, Any]): - """ - 添加弯曲约束 - - Args: - soft_body: 软体节点 - params: 参数字典 - """ - try: - # 添加弯曲约束 - # 这个参数控制弯曲约束的生成步长 - soft_body.generateBendingConstraints(2) - - except Exception as e: - print(f"⚠ 添加弯曲约束失败: {e}") - - def _extract_mesh_data(self, node_path) -> Tuple[List[Point3], List[int]]: - """ - 从节点路径提取网格数据 - - Args: - node_path: 节点路径 - - Returns: - (顶点列表, 索引列表) - """ - try: - vertices = [] - indices = [] - - # 查找几何节点 - geom_nodes = node_path.findAllMatches('**/+GeomNode') - for geom_node_path in geom_nodes: - geom_node = geom_node_path.node() - for i in range(geom_node.getNumGeoms()): - geom = geom_node.getGeom(i) - vdata = geom.getVertexData() - - # 提取顶点 - vertex_reader = GeomVertexReader(vdata, 'vertex') - while not vertex_reader.isAtEnd(): - vertices.append(vertex_reader.getData3()) - - # 提取索引 - for prim in geom.getPrimitives(): - index_reader = prim.getIndexIterator() - while not index_reader.isAtEnd(): - indices.append(index_reader.getData()) - index_reader.next() - - return vertices, indices - - except Exception as e: - print(f"⚠ 提取网格数据失败: {e}") - return [], [] - - def _get_physics_world(self): - """ - 获取物理世界对象 - - Returns: - 物理世界对象或None - """ - try: - # 尝试从世界对象获取物理世界 - if hasattr(self.world, 'physics_world'): - return self.world.physics_world.bullet_world - elif hasattr(self.world, 'bullet_world'): - return self.world.bullet_world - else: - # 查找物理插件 - physics_plugin = self.world.plugins.get('rigid_body_physics') - if physics_plugin and hasattr(physics_plugin, 'physics_world'): - return physics_plugin.physics_world.bullet_world - except Exception as e: - print(f"⚠ 获取物理世界失败: {e}") - +""" +软体物理实现 +提供软体物理模拟的具体实现 +包含完整的软体物理计算和仿真功能 +""" + +from panda3d.core import Vec3, Point3, GeomVertexReader, GeomVertexFormat +from panda3d.core import GeomVertexData, Geom, GeomNode, NodePath +from panda3d.bullet import BulletSoftBodyNode, BulletSoftBodyWorldInfo +import math +from typing import List, Dict, Optional, Any, Tuple +import numpy as np + + +class SoftBodyPhysics: + """ + 软体物理实现类 + 处理软体的具体物理模拟,包括弹性力学、粘弹性行为、流体交互等复杂物理效应 + """ + + # 软体材质类型 + SOFT_TYPE_RUBBER = "rubber" + SOFT_TYPE_JELLY = "jelly" + SOFT_TYPE_MUSCLE = "muscle" + SOFT_TYPE_FOAM = "foam" + SOFT_TYPE_GEL = "gel" + SOFT_TYPE_SILICONE = "silicone" + SOFT_TYPE_BALLOON = "balloon" + SOFT_TYPE_BUBBLE = "bubble" + SOFT_TYPE_TISSUE = "tissue" + SOFT_TYPE_CARTILAGE = "cartilage" + + # 软体行为模式 + BEHAVIOR_DEFAULT = "default" + BEHAVIOR_RIGID = "rigid" + BEHAVIOR_SOFT = "soft" + BEHAVIOR_FLUID = "fluid" + BEHAVIOR_ELASTIC = "elastic" + + # 约束类型 + CONSTRAINT_FIXED = "fixed" + CONSTRAINT_P2P = "p2p" # 点对点 + CONSTRAINT_HINGE = "hinge" # 铰链 + CONSTRAINT_SLIDER = "slider" # 滑动 + CONSTRAINT_CONETWIST = "conetwist" # 锥形扭转 + CONSTRAINT_DOF = "dof" # 6自由度 + + def __init__(self, world): + self.world = world + self.soft_body_world_info = None + self._init_soft_body_world() + + # 预定义的软体材质参数 + self.soft_materials = { + self.SOFT_TYPE_RUBBER: { + 'density': 1.2, + 'linear_stiffness': 0.9, + 'angular_stiffness': 0.8, + 'volume_stiffness': 0.7, + 'friction': 0.8, + 'restitution': 0.6, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.01, + 'lift_coefficient': 0.005, + 'bend_stiffness': 0.8, + 'damping': 0.02, + 'plastic_threshold': 0.3, + 'plastic_stiffness': 0.8, + 'tear_threshold': 200.0, + 'tear_resistance': 100.0, + 'anisotropic_friction': [0.8, 0.8, 0.8], + 'dynamic_friction': 0.8, + 'static_friction': 0.9, + 'rolling_friction': 0.2, + 'spinning_friction': 0.1, + 'viscosity': 0.01, + 'compressibility': 0.1, + 'shear_modulus': 0.8, + 'bulk_modulus': 1.2, + 'youngs_modulus': 1.0, + 'poisson_ratio': 0.4 + }, + self.SOFT_TYPE_JELLY: { + 'density': 1.1, + 'linear_stiffness': 0.6, + 'angular_stiffness': 0.5, + 'volume_stiffness': 0.8, + 'friction': 0.7, + 'restitution': 0.2, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.02, + 'lift_coefficient': 0.01, + 'bend_stiffness': 0.4, + 'damping': 0.015, + 'plastic_threshold': 0.1, + 'plastic_stiffness': 0.3, + 'tear_threshold': 80.0, + 'tear_resistance': 40.0, + 'anisotropic_friction': [0.7, 0.7, 0.7], + 'dynamic_friction': 0.7, + 'static_friction': 0.8, + 'rolling_friction': 0.15, + 'spinning_friction': 0.07, + 'viscosity': 0.03, + 'compressibility': 0.2, + 'shear_modulus': 0.4, + 'bulk_modulus': 0.9, + 'youngs_modulus': 0.6, + 'poisson_ratio': 0.35 + }, + self.SOFT_TYPE_MUSCLE: { + 'density': 1.05, + 'linear_stiffness': 0.8, + 'angular_stiffness': 0.7, + 'volume_stiffness': 0.6, + 'friction': 0.6, + 'restitution': 0.3, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.015, + 'lift_coefficient': 0.008, + 'bend_stiffness': 0.5, + 'damping': 0.012, + 'plastic_threshold': 0.15, + 'plastic_stiffness': 0.5, + 'tear_threshold': 120.0, + 'tear_resistance': 60.0, + 'anisotropic_friction': [0.6, 0.6, 0.6], + 'dynamic_friction': 0.6, + 'static_friction': 0.7, + 'rolling_friction': 0.12, + 'spinning_friction': 0.06, + 'viscosity': 0.02, + 'compressibility': 0.15, + 'shear_modulus': 0.7, + 'bulk_modulus': 1.1, + 'youngs_modulus': 0.85, + 'poisson_ratio': 0.42 + }, + self.SOFT_TYPE_FOAM: { + 'density': 0.3, + 'linear_stiffness': 0.4, + 'angular_stiffness': 0.3, + 'volume_stiffness': 0.5, + 'friction': 0.5, + 'restitution': 0.1, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.03, + 'lift_coefficient': 0.02, + 'bend_stiffness': 0.25, + 'damping': 0.008, + 'plastic_threshold': 0.05, + 'plastic_stiffness': 0.2, + 'tear_threshold': 60.0, + 'tear_resistance': 25.0, + 'anisotropic_friction': [0.5, 0.5, 0.5], + 'dynamic_friction': 0.5, + 'static_friction': 0.6, + 'rolling_friction': 0.05, + 'spinning_friction': 0.02, + 'viscosity': 0.05, + 'compressibility': 0.3, + 'shear_modulus': 0.2, + 'bulk_modulus': 0.7, + 'youngs_modulus': 0.4, + 'poisson_ratio': 0.3 + }, + self.SOFT_TYPE_GEL: { + 'density': 1.3, + 'linear_stiffness': 0.7, + 'angular_stiffness': 0.6, + 'volume_stiffness': 0.9, + 'friction': 0.75, + 'restitution': 0.25, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.012, + 'lift_coefficient': 0.006, + 'bend_stiffness': 0.55, + 'damping': 0.018, + 'plastic_threshold': 0.12, + 'plastic_stiffness': 0.45, + 'tear_threshold': 110.0, + 'tear_resistance': 55.0, + 'anisotropic_friction': [0.75, 0.75, 0.75], + 'dynamic_friction': 0.75, + 'static_friction': 0.85, + 'rolling_friction': 0.14, + 'spinning_friction': 0.07, + 'viscosity': 0.025, + 'compressibility': 0.12, + 'shear_modulus': 0.6, + 'bulk_modulus': 0.95, + 'youngs_modulus': 0.7, + 'poisson_ratio': 0.38 + }, + self.SOFT_TYPE_SILICONE: { + 'density': 1.15, + 'linear_stiffness': 0.75, + 'angular_stiffness': 0.65, + 'volume_stiffness': 0.75, + 'friction': 0.8, + 'restitution': 0.4, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.008, + 'lift_coefficient': 0.004, + 'bend_stiffness': 0.6, + 'damping': 0.01, + 'plastic_threshold': 0.2, + 'plastic_stiffness': 0.6, + 'tear_threshold': 140.0, + 'tear_resistance': 70.0, + 'anisotropic_friction': [0.8, 0.8, 0.8], + 'dynamic_friction': 0.8, + 'static_friction': 0.9, + 'rolling_friction': 0.16, + 'spinning_friction': 0.08, + 'viscosity': 0.015, + 'compressibility': 0.18, + 'shear_modulus': 0.65, + 'bulk_modulus': 1.0, + 'youngs_modulus': 0.8, + 'poisson_ratio': 0.41 + }, + self.SOFT_TYPE_BALLOON: { + 'density': 0.1, + 'linear_stiffness': 0.85, + 'angular_stiffness': 0.8, + 'volume_stiffness': 0.9, + 'friction': 0.3, + 'restitution': 0.8, + 'pressure_coefficient': 0.5, + 'drag_coefficient': 0.05, + 'lift_coefficient': 0.025, + 'bend_stiffness': 0.7, + 'damping': 0.005, + 'plastic_threshold': 0.25, + 'plastic_stiffness': 0.7, + 'tear_threshold': 40.0, + 'tear_resistance': 15.0, + 'anisotropic_friction': [0.3, 0.3, 0.3], + 'dynamic_friction': 0.3, + 'static_friction': 0.4, + 'rolling_friction': 0.03, + 'spinning_friction': 0.01, + 'viscosity': 0.005, + 'compressibility': 0.05, + 'shear_modulus': 0.2, + 'bulk_modulus': 0.5, + 'youngs_modulus': 0.3, + 'poisson_ratio': 0.45 + }, + self.SOFT_TYPE_BUBBLE: { + 'density': 0.01, + 'linear_stiffness': 0.9, + 'angular_stiffness': 0.85, + 'volume_stiffness': 0.95, + 'friction': 0.1, + 'restitution': 0.9, + 'pressure_coefficient': 0.8, + 'drag_coefficient': 0.04, + 'lift_coefficient': 0.03, + 'bend_stiffness': 0.75, + 'damping': 0.003, + 'plastic_threshold': 0.1, + 'plastic_stiffness': 0.5, + 'tear_threshold': 20.0, + 'tear_resistance': 5.0, + 'anisotropic_friction': [0.1, 0.1, 0.1], + 'dynamic_friction': 0.1, + 'static_friction': 0.2, + 'rolling_friction': 0.01, + 'spinning_friction': 0.005, + 'viscosity': 0.002, + 'compressibility': 0.02, + 'shear_modulus': 0.1, + 'bulk_modulus': 0.2, + 'youngs_modulus': 0.15, + 'poisson_ratio': 0.47 + }, + self.SOFT_TYPE_TISSUE: { + 'density': 1.05, + 'linear_stiffness': 0.65, + 'angular_stiffness': 0.55, + 'volume_stiffness': 0.65, + 'friction': 0.55, + 'restitution': 0.25, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.018, + 'lift_coefficient': 0.009, + 'bend_stiffness': 0.45, + 'damping': 0.014, + 'plastic_threshold': 0.1, + 'plastic_stiffness': 0.35, + 'tear_threshold': 90.0, + 'tear_resistance': 45.0, + 'anisotropic_friction': [0.55, 0.55, 0.55], + 'dynamic_friction': 0.55, + 'static_friction': 0.65, + 'rolling_friction': 0.1, + 'spinning_friction': 0.05, + 'viscosity': 0.025, + 'compressibility': 0.25, + 'shear_modulus': 0.5, + 'bulk_modulus': 0.8, + 'youngs_modulus': 0.6, + 'poisson_ratio': 0.37 + }, + self.SOFT_TYPE_CARTILAGE: { + 'density': 1.1, + 'linear_stiffness': 0.75, + 'angular_stiffness': 0.7, + 'volume_stiffness': 0.7, + 'friction': 0.65, + 'restitution': 0.35, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.01, + 'lift_coefficient': 0.005, + 'bend_stiffness': 0.6, + 'damping': 0.016, + 'plastic_threshold': 0.18, + 'plastic_stiffness': 0.55, + 'tear_threshold': 130.0, + 'tear_resistance': 65.0, + 'anisotropic_friction': [0.65, 0.65, 0.65], + 'dynamic_friction': 0.65, + 'static_friction': 0.75, + 'rolling_friction': 0.13, + 'spinning_friction': 0.065, + 'viscosity': 0.022, + 'compressibility': 0.18, + 'shear_modulus': 0.65, + 'bulk_modulus': 0.95, + 'youngs_modulus': 0.75, + 'poisson_ratio': 0.39 + } + } + + # 流体交互参数 + self.fluid_interaction = { + 'enabled': False, + 'viscosity': 0.01, + 'buoyancy': 1.0, + 'flow_direction': Vec3(0, 0, 0), + 'flow_strength': 0.0 + } + + # 性能优化参数 + self.max_simulation_steps = 10 + self.time_step = 1.0/60.0 + self.sub_steps = 4 + + def _init_soft_body_world(self): + """ + 初始化软体世界信息 + """ + try: + # 获取物理世界中的软体世界信息 + physics_world = self._get_physics_world() + if physics_world and hasattr(physics_world, 'soft_body_world_info'): + self.soft_body_world_info = physics_world.soft_body_world_info + else: + # 创建新的软体世界信息 + self.soft_body_world_info = BulletSoftBodyWorldInfo() + self.soft_body_world_info.setGravity(Vec3(0, 0, -9.81)) + self.soft_body_world_info.setAirDensity(1.225) + self.soft_body_world_info.setWaterDensity(1000.0) + self.soft_body_world_info.setWaterOffset(0.0) + self.soft_body_world_info.setWaterNormal(Vec3(0, 0, 1)) + + print("✓ 软体世界初始化完成") + + except Exception as e: + print(f"✗ 软体世界初始化失败: {e}") + + def set_fluid_interaction(self, enabled: bool = True, viscosity: float = 0.01, + buoyancy: float = 1.0, flow_direction: Vec3 = None, + flow_strength: float = 0.0): + """ + 设置流体交互参数 + + Args: + enabled: 是否启用流体交互 + viscosity: 粘度 + buoyancy: 浮力 + flow_direction: 流动方向 + flow_strength: 流动强度 + """ + self.fluid_interaction['enabled'] = enabled + self.fluid_interaction['viscosity'] = viscosity + self.fluid_interaction['buoyancy'] = buoyancy + if flow_direction: + self.fluid_interaction['flow_direction'] = flow_direction + self.fluid_interaction['flow_strength'] = flow_strength + + def create_soft_body_with_material(self, node_path, soft_type: str, + shape_type: str = "sphere", + behavior_mode: str = BEHAVIOR_DEFAULT, + **kwargs) -> Optional[BulletSoftBodyNode]: + """ + 使用指定材质创建软体 + + Args: + node_path: 父节点路径 + soft_type: 软体类型 + shape_type: 形状类型 (sphere, box, rope, mesh) + behavior_mode: 行为模式 + **kwargs: 形状参数 + + Returns: + BulletSoftBodyNode对象或None + """ + try: + # 获取材质参数 + if soft_type not in self.soft_materials: + print(f"⚠ 未知软体类型: {soft_type}, 使用默认参数") + base_params = self.soft_materials[self.SOFT_TYPE_RUBBER] + else: + base_params = self.soft_materials[soft_type] + + # 根据行为模式调整参数 + params = self._adjust_parameters_for_behavior(base_params, behavior_mode) + + # 创建软体节点 + soft_name = f'soft_{soft_type}_{shape_type}_{len(getattr(self, "_soft_counter", [0]))}' + soft_body_node = BulletSoftBodyNode(soft_name) + + # 设置软体参数 + self._apply_soft_body_parameters(soft_body_node, params) + + # 根据形状类型创建软体 + if shape_type == "sphere": + radius = kwargs.get('radius', 1.0) + resolution = kwargs.get('resolution', 8) + self._create_sphere_shape(soft_body_node, radius, resolution) + print(f"✓ 创建球形软体: 半径={radius}, 分辨率={resolution}") + elif shape_type == "box": + width = kwargs.get('width', 1.0) + height = kwargs.get('height', 1.0) + depth = kwargs.get('depth', 1.0) + resolution = kwargs.get('resolution', 4) + self._create_box_shape(soft_body_node, width, height, depth, resolution) + print(f"✓ 创建盒形软体: 宽={width}, 高={height}, 深={depth}, 分辨率={resolution}") + elif shape_type == "rope": + point_a = kwargs.get('point_a', Point3(0, 0, 0)) + point_b = kwargs.get('point_b', Point3(0, 0, 1)) + resolution = kwargs.get('resolution', 10) + self._create_rope_shape(soft_body_node, point_a, point_b, resolution) + print(f"✓ 创建绳索软体: 从{point_a}到{point_b}, 分辨率={resolution}") + elif shape_type == "mesh": + mesh_node = kwargs.get('mesh_node') + if mesh_node: + self._create_mesh_shape(soft_body_node, mesh_node) + print(f"✓ 从网格创建软体") + else: + print("✗ 网格节点参数缺失") + return None + elif shape_type == "capsule": + radius = kwargs.get('radius', 1.0) + height = kwargs.get('height', 2.0) + resolution = kwargs.get('resolution', 8) + self._create_capsule_shape(soft_body_node, radius, height, resolution) + print(f"✓ 创建胶囊软体: 半径={radius}, 高度={height}, 分辨率={resolution}") + elif shape_type == "cylinder": + radius = kwargs.get('radius', 1.0) + height = kwargs.get('height', 2.0) + resolution = kwargs.get('resolution', 8) + self._create_cylinder_shape(soft_body_node, radius, height, resolution) + print(f"✓ 创建圆柱软体: 半径={radius}, 高度={height}, 分辨率={resolution}") + + # 创建软体节点路径 + soft_np = node_path.attachNewNode(soft_body_node) + + # 添加到物理世界 + physics_world = self._get_physics_world() + if physics_world: + physics_world.attachSoftBody(soft_body_node) + + print(f"✓ 创建{soft_type}软体: {soft_name}") + return soft_body_node + + except Exception as e: + print(f"✗ 创建{soft_type}软体失败: {e}") + import traceback + traceback.print_exc() + return None + + def create_soft_body_from_mesh(self, node_path, soft_type: str, + behavior_mode: str = BEHAVIOR_DEFAULT, + params: Dict[str, Any] = None) -> Optional[BulletSoftBodyNode]: + """ + 从网格创建软体 + + Args: + node_path: 网格节点路径 + soft_type: 软体类型 + behavior_mode: 行为模式 + params: 额外参数 + + Returns: + BulletSoftBodyNode对象或None + """ + try: + # 获取材质参数 + if soft_type not in self.soft_materials: + print(f"⚠ 未知软体类型: {soft_type}, 使用默认参数") + base_params = self.soft_materials[self.SOFT_TYPE_RUBBER] + else: + base_params = self.soft_materials[soft_type] + + # 合并自定义参数 + if params: + base_params.update(params) + + # 根据行为模式调整参数 + params = self._adjust_parameters_for_behavior(base_params, behavior_mode) + + # 创建软体节点 + soft_name = f'soft_mesh_{soft_type}_{len(getattr(self, "_soft_counter", [0]))}' + soft_body_node = BulletSoftBodyNode(soft_name) + + # 设置软体参数 + self._apply_soft_body_parameters(soft_body_node, params) + + # 从网格提取数据 + vertices, indices = self._extract_mesh_data(node_path) + if not vertices or not indices: + print("⚠ 无法从网格提取顶点数据") + return None + + # 添加顶点到软体 + for vertex in vertices: + soft_body_node.appendNode(vertex, 1.0) + + # 添加面 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + soft_body_node.appendFace(indices[i], indices[i+1], indices[i+2]) + + # 添加链接 + self._add_links_from_indices(soft_body_node, indices) + + # 添加弯曲约束 + self._add_bending_constraints(soft_body_node, params) + + # 创建软体节点路径 + soft_np = node_path.attachNewNode(soft_body_node) + soft_np.setPos(node_path.getPos()) + soft_np.setHpr(node_path.getHpr()) + + # 添加到物理世界 + physics_world = self._get_physics_world() + if physics_world: + physics_world.attachSoftBody(soft_body_node) + + print(f"✓ 从网格创建{soft_type}软体: {soft_name} (顶点数: {len(vertices)})") + return soft_body_node + + except Exception as e: + print(f"✗ 从网格创建{soft_type}软体失败: {e}") + import traceback + traceback.print_exc() + return None + + def apply_force_to_soft_body(self, soft_body: BulletSoftBodyNode, force: Vec3, + position: Point3 = None, mode: str = "add"): + """ + 对软体应用力 + + Args: + soft_body: 软体节点 + force: 力向量 + position: 应力位置(如果为None,则应用到质心) + mode: 应力模式 ("add", "set", "impulse") + """ + try: + # 在实际实现中,这里会应用力到软体 + # 根据模式应用力 + if mode == "add": + # 添加力 + pass + elif mode == "set": + # 设置力 + pass + elif mode == "impulse": + # 应用冲量 + pass + + print(f"✓ 对软体应用{mode}力: {force}") + + except Exception as e: + print(f"✗ 应用力失败: {e}") + + def apply_force_to_region(self, soft_body: BulletSoftBodyNode, + force: Vec3, region_center: Point3, + radius: float, mode: str = "add") -> bool: + """ + 对软体的特定区域应用力 + + Args: + soft_body: 软体节点 + force: 力向量 + region_center: 区域中心点 + radius: 区域半径 + mode: 应力模式 + + Returns: + 是否成功应用力 + """ + try: + # 获取软体节点 + nodes = soft_body.get_nodes() + + # 查找受影响的节点 + affected_nodes = [] + for i, node in enumerate(nodes): + node_pos = node.get_position() + distance = (node_pos - region_center).length() + if distance <= radius: + affected_nodes.append((i, node, distance)) + + # 应用力到受影响的节点 + force_magnitude = force.length() + for node_index, node, distance in affected_nodes: + # 根据距离衰减力的大小 + decay_factor = 1.0 - (distance / radius) + if decay_factor > 0: + applied_force = force * decay_factor + # 在实际实现中,这里会应用力到节点 + # node.add_velocity(applied_force) + + print(f"✓ 对软体区域应用力: {len(affected_nodes)} 个节点受影响") + return True + + except Exception as e: + print(f"✗ 应用力到区域失败: {e}") + return False + + def simulate_deformation(self, soft_body: BulletSoftBodyNode, + force: Vec3, duration: float) -> bool: + """ + 模拟软体变形 + + Args: + soft_body: 软体节点 + force: 变形力 + duration: 持续时间 + + Returns: + 是否成功模拟变形 + """ + try: + # 在实际实现中,这里会模拟软体在持续力作用下的变形 + # 通过逐步施加力并更新软体状态来模拟变形过程 + + # 计算子步长 + sub_steps = max(1, int(duration / self.time_step)) + sub_force = force / sub_steps + + for i in range(sub_steps): + # 应用力到软体 + self.apply_force_to_soft_body(soft_body, sub_force) + + print(f"✓ 模拟软体变形: 持续时间 {duration}s") + return True + + except Exception as e: + print(f"✗ 模拟变形失败: {e}") + return False + + def set_soft_body_parameters(self, soft_body: BulletSoftBodyNode, params: Dict[str, Any]): + """ + 设置软体参数 + + Args: + soft_body: 软体节点 + params: 参数字典 + """ + try: + self._apply_soft_body_parameters(soft_body, params) + print("✓ 设置软体参数完成") + + except Exception as e: + print(f"✗ 设置软体参数失败: {e}") + + def get_material_parameters(self, soft_type: str) -> Optional[Dict[str, Any]]: + """ + 获取指定材质的参数 + + Args: + soft_type: 软体类型 + + Returns: + 参数字典或None + """ + return self.soft_materials.get(soft_type) + + def get_all_material_types(self) -> List[str]: + """ + 获取所有可用的软体材质类型 + + Returns: + 材质类型列表 + """ + return list(self.soft_materials.keys()) + + def add_constraint(self, soft_body1: BulletSoftBodyNode, soft_body2: BulletSoftBodyNode, + constraint_type: str = CONSTRAINT_P2P, params: Dict[str, Any] = None) -> bool: + """ + 在两个软体之间添加约束 + + Args: + soft_body1: 第一个软体 + soft_body2: 第二个软体 + constraint_type: 约束类型 + params: 约束参数 + + Returns: + 是否成功添加约束 + """ + try: + # 在实际实现中,这里会添加软体间的约束 + # Bullet提供多种约束类型 + + print(f"✓ 添加{constraint_type}约束") + return True + + except Exception as e: + print(f"✗ 添加约束失败: {e}") + return False + + def update_soft_body_world_info(self, air_density: float = None, + water_density: float = None, + water_offset: float = None, + water_normal: Vec3 = None, + gravity: Vec3 = None): + """ + 更新软体世界信息 + + Args: + air_density: 空气密度 + water_density: 水密度 + water_offset: 水偏移 + water_normal: 水法线 + gravity: 重力向量 + """ + try: + if not self.soft_body_world_info: + print("⚠ 软体世界信息未初始化") + return + + if air_density is not None: + self.soft_body_world_info.setAirDensity(air_density) + if water_density is not None: + self.soft_body_world_info.setWaterDensity(water_density) + if water_offset is not None: + self.soft_body_world_info.setWaterOffset(water_offset) + if water_normal is not None: + self.soft_body_world_info.setWaterNormal(water_normal) + if gravity is not None: + self.soft_body_world_info.setGravity(gravity) + + print("✓ 更新软体世界信息完成") + + except Exception as e: + print(f"✗ 更新软体世界信息失败: {e}") + + def calculate_soft_body_properties(self, vertices: List[Point3], indices: List[int], + params: Dict[str, Any]) -> Dict[str, Any]: + """ + 计算软体属性 + + Args: + vertices: 顶点列表 + indices: 索引列表 + params: 参数字典 + + Returns: + 属性字典 + """ + try: + properties = { + 'vertex_count': len(vertices), + 'face_count': len(indices) // 3, + 'edge_count': 0, + 'volume': 0.0, + 'surface_area': 0.0, + 'center_of_mass': Point3(0, 0, 0), + 'mass': 0.0, + 'elasticity': params.get('youngs_modulus', 1.0), + 'viscosity': params.get('viscosity', 0.01), + 'compressibility': params.get('compressibility', 0.1), + 'bounding_box': { + 'min': Point3(float('inf'), float('inf'), float('inf')), + 'max': Point3(float('-inf'), float('-inf'), float('-inf')) + } + } + + if not vertices: + return properties + + # 计算包围盒 + for vertex in vertices: + properties['bounding_box']['min'].set( + min(properties['bounding_box']['min'][0], vertex[0]), + min(properties['bounding_box']['min'][1], vertex[1]), + min(properties['bounding_box']['min'][2], vertex[2]) + ) + properties['bounding_box']['max'].set( + max(properties['bounding_box']['max'][0], vertex[0]), + max(properties['bounding_box']['max'][1], vertex[1]), + max(properties['bounding_box']['max'][2], vertex[2]) + ) + + # 计算质心 + center = Point3(0, 0, 0) + for vertex in vertices: + center += vertex + center /= len(vertices) + properties['center_of_mass'] = center + + # 计算体积和表面积 + total_area = 0.0 + total_volume = 0.0 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + v0 = vertices[indices[i]] + v1 = vertices[indices[i+1]] + v2 = vertices[indices[i+2]] + # 计算三角形面积 + edge1 = v1 - v0 + edge2 = v2 - v0 + cross = edge1.cross(edge2) + area = cross.length() * 0.5 + total_area += area + + # 估算体积(简化计算) + # 实际体积需要更复杂的算法 + # 这里使用一个简化估算 + volume_contribution = area * abs(cross.length()) / 6.0 + total_volume += volume_contribution + + properties['surface_area'] = total_area + properties['volume'] = total_volume + + # 计算质量(基于密度和体积) + density = params.get('density', 1.0) + properties['mass'] = density * total_volume + + return properties + + except Exception as e: + print(f"✗ 计算软体属性失败: {e}") + return {} + + def _apply_soft_body_parameters(self, soft_body: BulletSoftBodyNode, params: Dict[str, Any]): + """ + 应用软体参数到软体节点 + + Args: + soft_body: 软体节点 + params: 参数字典 + """ + cfg = soft_body.get_cfg() + + # 物理参数 + if 'linear_stiffness' in params: + cfg.set_kDF(params['linear_stiffness']) + if 'angular_stiffness' in params: + cfg.set_kDG(params['angular_stiffness']) + if 'volume_stiffness' in params: + cfg.set_kVC(params['volume_stiffness']) + if 'pressure_coefficient' in params: + cfg.set_kPR(params['pressure_coefficient']) + if 'drag_coefficient' in params: + cfg.set_kDG(params['drag_coefficient']) + if 'lift_coefficient' in params: + cfg.set_kLF(params['lift_coefficient']) + if 'bend_stiffness' in params: + # 弯曲刚度 - 用作线性刚度的补充 + cfg.set_kDF(params['bend_stiffness'] * 0.5) + if 'damping' in params: + cfg.set_dft_damping(params['damping']) + if 'plastic_threshold' in params: + cfg.set_kMT(params['plastic_threshold']) + if 'plastic_stiffness' in params: + cfg.set_kDP(params['plastic_stiffness']) + + # 材质属性 + if 'friction' in params: + soft_body.setFriction(params['friction']) + if 'restitution' in params: + soft_body.setRestitution(params['restitution']) + if 'density' in params: + soft_body.setTotalMass(params['density']) + if 'anisotropic_friction' in params: + friction_vec = params['anisotropic_friction'] + if len(friction_vec) >= 3: + soft_body.setAnisotropicFriction( + Vec3(friction_vec[0], friction_vec[1], friction_vec[2]) + ) + + # 高级材质参数 + if 'viscosity' in params: + # 在实际实现中,这里会影响软体的粘性行为 + pass + if 'compressibility' in params: + # 在实际实现中,这里会影响软体的压缩性 + pass + if 'shear_modulus' in params: + # 剪切模量 + pass + if 'bulk_modulus' in params: + # 体积模量 + pass + if 'youngs_modulus' in params: + # 杨氏模量 + pass + if 'poisson_ratio' in params: + # 泊松比 + pass + + # 高级摩擦参数 + if 'dynamic_friction' in params: + # 动态摩擦系数 + pass + if 'static_friction' in params: + # 静态摩擦系数 + pass + if 'rolling_friction' in params: + # 滚动摩擦系数 + pass + if 'spinning_friction' in params: + # 自旋摩擦系数 + pass + + def _adjust_parameters_for_behavior(self, params: Dict[str, Any], behavior_mode: str) -> Dict[str, Any]: + """ + 根据行为模式调整参数 + + Args: + params: 原始参数 + behavior_mode: 行为模式 + + Returns: + 调整后的参数 + """ + adjusted_params = params.copy() + + if behavior_mode == self.BEHAVIOR_RIGID: + adjusted_params['linear_stiffness'] = min(1.0, adjusted_params['linear_stiffness'] * 2.0) + adjusted_params['angular_stiffness'] = min(1.0, adjusted_params['angular_stiffness'] * 2.0) + adjusted_params['volume_stiffness'] = min(1.0, adjusted_params['volume_stiffness'] * 2.0) + adjusted_params['damping'] = max(0.001, adjusted_params['damping'] * 0.5) + elif behavior_mode == self.BEHAVIOR_SOFT: + adjusted_params['linear_stiffness'] = max(0.1, adjusted_params['linear_stiffness'] * 0.5) + adjusted_params['angular_stiffness'] = max(0.1, adjusted_params['angular_stiffness'] * 0.5) + adjusted_params['volume_stiffness'] = max(0.1, adjusted_params['volume_stiffness'] * 0.5) + adjusted_params['damping'] = min(0.1, adjusted_params['damping'] * 2.0) + elif behavior_mode == self.BEHAVIOR_FLUID: + adjusted_params['linear_stiffness'] = max(0.05, adjusted_params['linear_stiffness'] * 0.1) + adjusted_params['angular_stiffness'] = max(0.05, adjusted_params['angular_stiffness'] * 0.1) + adjusted_params['volume_stiffness'] = max(0.05, adjusted_params['volume_stiffness'] * 0.1) + adjusted_params['damping'] = min(0.2, adjusted_params['damping'] * 5.0) + adjusted_params['viscosity'] = min(1.0, adjusted_params['viscosity'] * 10.0) + elif behavior_mode == self.BEHAVIOR_ELASTIC: + adjusted_params['linear_stiffness'] = min(1.0, adjusted_params['linear_stiffness'] * 1.5) + adjusted_params['restitution'] = min(1.0, adjusted_params['restitution'] * 1.5) + adjusted_params['damping'] = max(0.001, adjusted_params['damping'] * 0.7) + + return adjusted_params + + def _create_sphere_shape(self, soft_body: BulletSoftBodyNode, radius: float, resolution: int): + """ + 创建球形软体 + + Args: + soft_body: 软体节点 + radius: 半径 + resolution: 分辨率 + """ + # 生成球面顶点 + vertices = [] + for i in range(resolution + 1): + phi = (i / resolution) * math.pi # 纬度角 + for j in range(resolution * 2 + 1): + theta = (j / (resolution * 2)) * 2 * math.pi # 经度角 + + x = radius * math.sin(phi) * math.cos(theta) + y = radius * math.sin(phi) * math.sin(theta) + z = radius * math.cos(phi) + + vertices.append(Point3(x, y, z)) + + # 添加顶点 + for vertex in vertices: + soft_body.appendNode(vertex, 1.0) + + # 生成面索引 + indices = [] + for i in range(resolution): + for j in range(resolution * 2): + v0 = i * (resolution * 2 + 1) + j + v1 = v0 + 1 + v2 = (i + 1) * (resolution * 2 + 1) + j + v3 = v2 + 1 + + if i < resolution - 1: + indices.extend([v0, v1, v2]) + indices.extend([v1, v3, v2]) + + # 添加面 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + soft_body.appendFace(indices[i], indices[i+1], indices[i+2]) + + # 添加链接 + self._add_links_from_indices(soft_body, indices) + + # 添加弯曲约束 + self._add_bending_constraints(soft_body, {}) + + def _create_box_shape(self, soft_body: BulletSoftBodyNode, width: float, height: float, depth: float, resolution: int): + """ + 创建盒形软体 + + Args: + soft_body: 软体节点 + width: 宽度 + height: 高度 + depth: 深度 + resolution: 分辨率 + """ + # 实现盒形软体创建 + # 这里是简化版本,实际实现需要更详细的网格生成 + hw = width / 2.0 + hh = height / 2.0 + hd = depth / 2.0 + + # 生成八个角点 + vertices = [ + Point3(-hw, -hd, -hh), # 0 + Point3(hw, -hd, -hh), # 1 + Point3(hw, hd, -hh), # 2 + Point3(-hw, hd, -hh), # 3 + Point3(-hw, -hd, hh), # 4 + Point3(hw, -hd, hh), # 5 + Point3(hw, hd, hh), # 6 + Point3(-hw, hd, hh) # 7 + ] + + # 添加顶点 + for vertex in vertices: + soft_body.appendNode(vertex, 1.0) + + # 添加面(简化的立方体面) + faces = [ + [0, 1, 2, 3], # 底面 + [4, 7, 6, 5], # 顶面 + [0, 4, 5, 1], # 前面 + [2, 6, 7, 3], # 后面 + [0, 3, 7, 4], # 左面 + [1, 5, 6, 2] # 右面 + ] + + # 添加面 + for face in faces: + soft_body.appendFace(face[0], face[1], face[2]) + soft_body.appendFace(face[0], face[2], face[3]) + + # 添加链接 + indices = [] + for face in faces: + indices.extend(face) + self._add_links_from_indices(soft_body, indices) + + # 添加弯曲约束 + self._add_bending_constraints(soft_body, {}) + + def _create_rope_shape(self, soft_body: BulletSoftBodyNode, point_a: Point3, point_b: Point3, resolution: int): + """ + 创建绳索软体 + + Args: + soft_body: 软体节点 + point_a: 起点 + point_b: 终点 + resolution: 分辨率 + """ + # 生成绳索顶点 + vertices = [] + for i in range(resolution): + t = i / (resolution - 1) if resolution > 1 else 0 + pos = point_a + (point_b - point_a) * t + vertices.append(pos) + + # 添加顶点 + for vertex in vertices: + soft_body.appendNode(vertex, 1.0) + + # 添加链接 + for i in range(len(vertices) - 1): + soft_body.appendLink(i, i + 1) + + def _create_capsule_shape(self, soft_body: BulletSoftBodyNode, radius: float, height: float, resolution: int): + """ + 创建胶囊形软体 + + Args: + soft_body: 软体节点 + radius: 半径 + height: 高度 + resolution: 分辨率 + """ + # 实现胶囊形软体创建 + # 胶囊是圆柱体加上两个半球 + half_height = height / 2.0 + + vertices = [] + + # 生成上半球 + for i in range(resolution // 2 + 1): + phi = (i / (resolution // 2)) * (math.pi / 2) # 0到π/2 + for j in range(resolution * 2 + 1): + theta = (j / (resolution * 2)) * 2 * math.pi + x = radius * math.sin(phi) * math.cos(theta) + y = radius * math.sin(phi) * math.sin(theta) + z = radius * math.cos(phi) + half_height + vertices.append(Point3(x, y, z)) + + # 生成圆柱部分 + for i in range(resolution + 1): + for j in range(resolution * 2 + 1): + theta = (j / (resolution * 2)) * 2 * math.pi + y_offset = (i / resolution) * height - half_height + x = radius * math.cos(theta) + y = radius * math.sin(theta) + z = y_offset + vertices.append(Point3(x, y, z)) + + # 生成下半球 + for i in range(resolution // 2 + 1): + phi = math.pi / 2 + (i / (resolution // 2)) * (math.pi / 2) # π/2到π + for j in range(resolution * 2 + 1): + theta = (j / (resolution * 2)) * 2 * math.pi + x = radius * math.sin(phi) * math.cos(theta) + y = radius * math.sin(phi) * math.sin(theta) + z = radius * math.cos(phi) - half_height + vertices.append(Point3(x, y, z)) + + # 添加顶点 + for vertex in vertices: + soft_body.appendNode(vertex, 1.0) + + # 这里需要生成适当的面索引 + # 为简化,我们使用一个基本的索引生成方法 + indices = [] + # 生成面的索引是复杂的,这里我们使用一个简化的处理方式 + # 实际实现需要更复杂的索引生成算法 + + # 添加链接 + # 这里也需要根据胶囊的几何结构添加适当的链接 + # 为简化,我们连接相邻的顶点 + for i in range(len(vertices) - 1): + soft_body.appendLink(i, i + 1) + + # 添加弯曲约束 + self._add_bending_constraints(soft_body, {}) + + def _create_cylinder_shape(self, soft_body: BulletSoftBodyNode, radius: float, height: float, resolution: int): + """ + 创建圆柱形软体 + + Args: + soft_body: 软体节点 + radius: 半径 + height: 高度 + resolution: 分辨率 + """ + # 实现圆柱形软体创建 + half_height = height / 2.0 + + vertices = [] + + # 生成圆柱面 + for i in range(2): # 顶部到底部 + z = half_height if i == 0 else -half_height + for j in range(resolution * 2 + 1): + theta = (j / (resolution * 2)) * 2 * math.pi + x = radius * math.cos(theta) + y = radius * math.sin(theta) + vertices.append(Point3(x, y, z)) + + # 添加顶点 + for vertex in vertices: + soft_body.appendNode(vertex, 1.0) + + # 生成面索引 + indices = [] + for i in range(resolution * 2): + indices.extend([i, i + 1, i + resolution * 2 + 1]) + indices.extend([i + 1, i + resolution * 2 + 2, i + resolution * 2 + 1]) + + # 添加面 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + soft_body.appendFace(indices[i], indices[i+1], indices[i+2]) + + # 添加链接 + self._add_links_from_indices(soft_body, indices) + + # 添加弯曲约束 + self._add_bending_constraints(soft_body, {}) + + def _create_mesh_shape(self, soft_body: BulletSoftBodyNode, mesh_node: NodePath): + """ + 从网格创建软体 + + Args: + soft_body: 软体节点 + mesh_node: 网格节点 + """ + # 从网格节点提取顶点和面 + vertices, indices = self._extract_mesh_data(mesh_node) + + if not vertices or not indices: + print("⚠ 无法从网格提取数据") + return + + # 添加顶点 + for vertex in vertices: + soft_body.appendNode(vertex, 1.0) + + # 添加面 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + soft_body.appendFace(indices[i], indices[i+1], indices[i+2]) + + # 添加链接 + self._add_links_from_indices(soft_body, indices) + + # 添加弯曲约束 + self._add_bending_constraints(soft_body, {}) + + def _add_links_from_indices(self, soft_body: BulletSoftBodyNode, indices: List[int]): + """ + 根据索引添加链接 + + Args: + soft_body: 软体节点 + indices: 索引列表 + """ + try: + # 创建边集合以避免重复 + edges = set() + + # 从三角形面提取边 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + # 三角形的三条边 + edges.add(tuple(sorted([indices[i], indices[i+1]]))) + edges.add(tuple(sorted([indices[i+1], indices[i+2]]))) + edges.add(tuple(sorted([indices[i+2], indices[i]]))) + + # 添加链接 + for edge in edges: + soft_body.appendLink(edge[0], edge[1]) + + except Exception as e: + print(f"⚠ 添加链接失败: {e}") + + def _add_bending_constraints(self, soft_body: BulletSoftBodyNode, params: Dict[str, Any]): + """ + 添加弯曲约束 + + Args: + soft_body: 软体节点 + params: 参数字典 + """ + try: + # 添加弯曲约束 + # 这个参数控制弯曲约束的生成步长 + soft_body.generateBendingConstraints(2) + + except Exception as e: + print(f"⚠ 添加弯曲约束失败: {e}") + + def _extract_mesh_data(self, node_path) -> Tuple[List[Point3], List[int]]: + """ + 从节点路径提取网格数据 + + Args: + node_path: 节点路径 + + Returns: + (顶点列表, 索引列表) + """ + try: + vertices = [] + indices = [] + + # 查找几何节点 + geom_nodes = node_path.findAllMatches('**/+GeomNode') + for geom_node_path in geom_nodes: + geom_node = geom_node_path.node() + for i in range(geom_node.getNumGeoms()): + geom = geom_node.getGeom(i) + vdata = geom.getVertexData() + + # 提取顶点 + vertex_reader = GeomVertexReader(vdata, 'vertex') + while not vertex_reader.isAtEnd(): + vertices.append(vertex_reader.getData3()) + + # 提取索引 + for prim in geom.getPrimitives(): + index_reader = prim.getIndexIterator() + while not index_reader.isAtEnd(): + indices.append(index_reader.getData()) + index_reader.next() + + return vertices, indices + + except Exception as e: + print(f"⚠ 提取网格数据失败: {e}") + return [], [] + + def _get_physics_world(self): + """ + 获取物理世界对象 + + Returns: + 物理世界对象或None + """ + try: + # 尝试从世界对象获取物理世界 + if hasattr(self.world, 'physics_world'): + return self.world.physics_world.bullet_world + elif hasattr(self.world, 'bullet_world'): + return self.world.bullet_world + else: + # 查找物理插件 + physics_plugin = self.world.plugins.get('rigid_body_physics') + if physics_plugin and hasattr(physics_plugin, 'physics_world'): + return physics_plugin.physics_world.bullet_world + except Exception as e: + print(f"⚠ 获取物理世界失败: {e}") + return None \ No newline at end of file diff --git a/plugins/user/soft_body_cloth_physics/debug/cloth_debug.py b/plugins/user/soft_body_cloth_physics/debug/cloth_debug.py index 8e1a52a7..83a78217 100644 --- a/plugins/user/soft_body_cloth_physics/debug/cloth_debug.py +++ b/plugins/user/soft_body_cloth_physics/debug/cloth_debug.py @@ -1,1390 +1,1390 @@ -""" -调试可视化工具 -提供软体和布料物理模拟的调试可视化功能 -包含完整的可视化和性能监控功能 -""" - -from panda3d.core import NodePath, LineSegs, Vec3, Vec4, Point3 -from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter -from panda3d.core import GeomLines, Geom, GeomNode, TextNode -from panda3d.core import TransparencyAttrib -from typing import List, Dict, Any, Optional, Tuple -import math - - -class ClothDebugVisualizer: - """ - 布料调试可视化器 - 提供布料物理模拟的可视化调试功能,包括网格、固定点、力、速度等 - """ - - # 可视化模式 - MODE_WIREFRAME = "wireframe" - MODE_POINTS = "points" - MODE_SOLID = "solid" - - # 颜色主题 - COLOR_THEME_DEFAULT = "default" - COLOR_THEME_HEATMAP = "heatmap" - COLOR_THEME_STRESS = "stress" - - def __init__(self, render_node: NodePath): - """ - 初始化调试可视化器 - - Args: - render_node: 渲染节点 - """ - self.render_node = render_node - self.debug_root = render_node.attachNewNode("cloth_debug_visualizer") - self.enabled = False - - # 可视化标志 - self.show_flags = { - 'mesh': True, - 'anchors': True, - 'forces': False, - 'velocities': False, - 'normals': False, - 'tears': True, - 'collisions': False, - 'bounding_box': False, - 'stress': False, - 'strain': False - } - - # 可视化参数 - self.visualization_params = { - 'mesh_color': Vec4(0.0, 1.0, 0.0, 0.7), # 绿色半透明 - 'anchor_color': Vec4(1.0, 0.0, 0.0, 1.0), # 红色 - 'force_color': Vec4(1.0, 1.0, 0.0, 1.0), # 黄色 - 'velocity_color': Vec4(0.0, 0.0, 1.0, 1.0), # 蓝色 - 'normal_color': Vec4(1.0, 0.5, 0.0, 1.0), # 橙色 - 'tear_color': Vec4(1.0, 0.0, 1.0, 1.0), # 紫色 - 'collision_color': Vec4(1.0, 0.5, 0.0, 1.0), # 橙色 - 'line_thickness': 2.0, - 'point_size': 5.0, - 'vector_scale': 1.0, - 'transparency': 0.7 - } - - # 性能监控 - self.performance_stats = { - 'last_update_time': 0.0, - 'update_count': 0, - 'rendered_objects': 0, - 'total_vertices': 0, - 'fps': 0.0 - } - - # 可视化模式 - self.visualization_mode = self.MODE_WIREFRAME - self.color_theme = self.COLOR_THEME_DEFAULT - - # 统计信息显示 - self.stats_node = None - self.show_stats = False - - def enable(self): - """ - 启用调试可视化 - """ - self.enabled = True - self.debug_root.show() - print("✓ 布料调试可视化已启用") - - def disable(self): - """ - 禁用调试可视化 - """ - self.enabled = False - self.debug_root.hide() - print("✓ 布料调试可视化已禁用") - - def set_visibility_flags(self, **kwargs): - """ - 设置可视化标志 - - Args: - **kwargs: 可视化标志键值对 - """ - for key, value in kwargs.items(): - if key in self.show_flags: - self.show_flags[key] = value - - def set_visualization_params(self, **kwargs): - """ - 设置可视化参数 - - Args: - **kwargs: 可视化参数键值对 - """ - for key, value in kwargs.items(): - if key in self.visualization_params: - self.visualization_params[key] = value - - def set_visualization_mode(self, mode: str): - """ - 设置可视化模式 - - Args: - mode: 可视化模式 - """ - if mode in [self.MODE_WIREFRAME, self.MODE_POINTS, self.MODE_SOLID]: - self.visualization_mode = mode - - def set_color_theme(self, theme: str): - """ - 设置颜色主题 - - Args: - theme: 颜色主题 - """ - if theme in [self.COLOR_THEME_DEFAULT, self.COLOR_THEME_HEATMAP, self.COLOR_THEME_STRESS]: - self.color_theme = theme - - def enable_statistics(self, show: bool = True): - """ - 启用统计信息显示 - - Args: - show: 是否显示统计信息 - """ - self.show_stats = show - if show and not self.stats_node: - self._create_stats_display() - elif not show and self.stats_node: - self.stats_node.removeNode() - self.stats_node = None - - def visualize_cloth_mesh(self, cloth_info: Dict[str, Any]): - """ - 可视化布料网格 - - Args: - cloth_info: 布料对象信息 - """ - if not self.enabled or not self.show_flags['mesh']: - return - - try: - # 清除之前的可视化 - self._clear_cloth_visualization(cloth_info, "mesh") - - # 获取软体节点 - soft_body = cloth_info.get('soft_body') - if not soft_body: - return - - # 获取节点信息 - nodes = soft_body.get_nodes() - links = soft_body.get_links() - - if not nodes or not links: - return - - # 创建线条段 - lines = LineSegs() - lines.setThickness(self.visualization_params['line_thickness']) - lines.setColor(self.visualization_params['mesh_color']) - - # 根据颜色主题设置颜色 - base_color = self.visualization_params['mesh_color'] - - # 绘制链接线 - for link in links: - node_a = nodes[link.get_node_a()] - node_b = nodes[link.get_node_b()] - - pos_a = node_a.get_position() - pos_b = node_b.get_position() - - # 根据模式调整颜色 - if self.color_theme == self.COLOR_THEME_STRESS: - # 根据应力调整颜色 - stress = link.get_bbending() # 简化处理 - color_factor = min(1.0, stress * 2.0) - color = Vec4( - base_color[0] + color_factor * (1.0 - base_color[0]), - base_color[1] * (1.0 - color_factor), - base_color[2] * (1.0 - color_factor), - base_color[3] - ) - lines.setColor(color) - elif self.color_theme == self.COLOR_THEME_HEATMAP: - # 根据位置高度调整颜色 - height_factor = (pos_a[2] + pos_b[2]) / 2.0 - normalized_height = max(0.0, min(1.0, (height_factor + 10.0) / 20.0)) - lines.setColor(Vec4(normalized_height, 1.0 - normalized_height, 0.5, base_color[3])) - else: - lines.setColor(base_color) - - lines.moveTo(pos_a) - lines.drawTo(pos_b) - - # 创建可视化节点 - mesh_geom = lines.create() - mesh_node = self.debug_root.attachNewNode(mesh_geom) - mesh_node.setName(f"cloth_mesh_debug_{id(cloth_info)}") - cloth_info['_debug_mesh_node'] = mesh_node - - # 应用透明度 - mesh_node.setTransparency(TransparencyAttrib.MAlpha) - - self.performance_stats['total_vertices'] += len(nodes) - - except Exception as e: - print(f"⚠ 可视化布料网格失败: {e}") - - def visualize_anchors(self, cloth_info: Dict[str, Any]): - """ - 可视化固定点 - - Args: - cloth_info: 布料对象信息 - """ - if not self.enabled or not self.show_flags['anchors']: - return - - try: - # 清除之前的可视化 - self._clear_cloth_visualization(cloth_info, "anchors") - - # 获取固定点信息 - anchors = cloth_info.get('anchors', []) - if not anchors: - return - - # 创建点表示固定点 - lines = LineSegs() - lines.setThickness(self.visualization_params['point_size']) - lines.setColor(self.visualization_params['anchor_color']) - - # 绘制固定点 - for anchor in anchors: - vertex_index = anchor.get('vertex_index', 0) - - # 获取软体节点 - soft_body = cloth_info.get('soft_body') - if soft_body: - nodes = soft_body.get_nodes() - if vertex_index < len(nodes): - pos = nodes[vertex_index].get_position() - lines.moveTo(pos) - lines.drawTo(pos) # 绘制点 - - # 创建可视化节点 - anchor_geom = lines.create() - anchor_node = self.debug_root.attachNewNode(anchor_geom) - anchor_node.setName(f"cloth_anchors_debug_{id(cloth_info)}") - cloth_info['_debug_anchor_node'] = anchor_node - - except Exception as e: - print(f"⚠ 可视化固定点失败: {e}") - - def visualize_forces(self, cloth_info: Dict[str, Any]): - """ - 可视化力 - - Args: - cloth_info: 布料对象信息 - """ - if not self.enabled or not self.show_flags['forces']: - return - - try: - # 清除之前的可视化 - self._clear_cloth_visualization(cloth_info, "forces") - - # 获取软体节点 - soft_body = cloth_info.get('soft_body') - if not soft_body: - return - - # 获取节点信息 - nodes = soft_body.get_nodes() - if not nodes: - return - - # 创建力向量可视化 - lines = LineSegs() - lines.setThickness(1.0) - lines.setColor(self.visualization_params['force_color']) - - # 绘制力向量 - scale = self.visualization_params['vector_scale'] * 0.1 - for i, node in enumerate(nodes): - pos = node.get_position() - # 简化处理:使用速度作为力的近似 - velocity = node.get_velocity() - force_vector = velocity * scale - - if force_vector.length() > 0.01: # 只显示显著的力 - lines.moveTo(pos) - lines.drawTo(pos + force_vector) - - # 绘制箭头 - self._draw_arrow_head(lines, pos, pos + force_vector, 0.1) - - # 创建可视化节点 - force_geom = lines.create() - force_node = self.debug_root.attachNewNode(force_geom) - force_node.setName(f"cloth_forces_debug_{id(cloth_info)}") - cloth_info['_debug_force_node'] = force_node - - except Exception as e: - print(f"⚠ 可视化布料力失败: {e}") - - def visualize_velocities(self, cloth_info: Dict[str, Any]): - """ - 可视化速度 - - Args: - cloth_info: 布料对象信息 - """ - if not self.enabled or not self.show_flags['velocities']: - return - - try: - # 清除之前的可视化 - self._clear_cloth_visualization(cloth_info, "velocities") - - # 获取软体节点 - soft_body = cloth_info.get('soft_body') - if not soft_body: - return - - # 获取节点信息 - nodes = soft_body.get_nodes() - if not nodes: - return - - # 创建速度向量可视化 - lines = LineSegs() - lines.setThickness(1.0) - lines.setColor(self.visualization_params['velocity_color']) - - # 绘制速度向量 - scale = self.visualization_params['vector_scale'] * 0.05 - for i, node in enumerate(nodes): - pos = node.get_position() - velocity = node.get_velocity() - velocity_vector = velocity * scale - - if velocity_vector.length() > 0.005: # 只显示显著的速度 - lines.moveTo(pos) - lines.drawTo(pos + velocity_vector) - - # 绘制箭头 - self._draw_arrow_head(lines, pos, pos + velocity_vector, 0.05) - - # 创建可视化节点 - velocity_geom = lines.create() - velocity_node = self.debug_root.attachNewNode(velocity_geom) - velocity_node.setName(f"cloth_velocities_debug_{id(cloth_info)}") - cloth_info['_debug_velocity_node'] = velocity_node - - except Exception as e: - print(f"⚠ 可视化布料速度失败: {e}") - - def visualize_normals(self, cloth_info: Dict[str, Any]): - """ - 可视化法线 - - Args: - cloth_info: 布料对象信息 - """ - if not self.enabled or not self.show_flags['normals']: - return - - try: - # 清除之前的可视化 - self._clear_cloth_visualization(cloth_info, "normals") - - # 获取软体节点 - soft_body = cloth_info.get('soft_body') - if not soft_body: - return - - # 获取面信息 - faces = soft_body.get_faces() - if not faces: - return - - # 创建法线可视化 - lines = LineSegs() - lines.setThickness(1.0) - lines.setColor(self.visualization_params['normal_color']) - - # 绘制面法线 - scale = self.visualization_params['vector_scale'] * 0.2 - for face in faces: - # 简化处理:计算面的中心和法线 - # 实际实现需要更精确的计算 - pass - - # 创建可视化节点 - normal_geom = lines.create() - normal_node = self.debug_root.attachNewNode(normal_geom) - normal_node.setName(f"cloth_normals_debug_{id(cloth_info)}") - cloth_info['_debug_normal_node'] = normal_node - - except Exception as e: - print(f"⚠ 可视化布料法线失败: {e}") - - def visualize_tears(self, cloth_info: Dict[str, Any]): - """ - 可视化撕裂 - - Args: - cloth_info: 布料对象信息 - """ - if not self.enabled or not self.show_flags['tears']: - return - - try: - # 清除之前的可视化 - self._clear_cloth_visualization(cloth_info, "tears") - - # 获取撕裂信息 - tears = cloth_info.get('tears', []) - if not tears: - return - - # 创建撕裂可视化 - lines = LineSegs() - lines.setThickness(self.visualization_params['line_thickness'] * 2) - lines.setColor(self.visualization_params['tear_color']) - - # 绘制撕裂线 - for tear_info in tears: - tear_point = tear_info.get('point', Point3(0, 0, 0)) - tear_radius = tear_info.get('radius', 0.1) - - # 绘制撕裂圆 - self._draw_circle(lines, tear_point, tear_radius, 16) - - # 创建可视化节点 - tear_geom = lines.create() - tear_node = self.debug_root.attachNewNode(tear_geom) - tear_node.setName(f"cloth_tears_debug_{id(cloth_info)}") - cloth_info['_debug_tear_node'] = tear_node - - except Exception as e: - print(f"⚠ 可视化布料撕裂失败: {e}") - - def visualize_collisions(self, cloth_info: Dict[str, Any]): - """ - 可视化碰撞 - - Args: - cloth_info: 布料对象信息 - """ - if not self.enabled or not self.show_flags['collisions']: - return - - try: - # 清除之前的可视化 - self._clear_cloth_visualization(cloth_info, "collisions") - - # 获取碰撞信息 - collisions = cloth_info.get('collisions', []) - if not collisions: - return - - # 创建碰撞可视化 - lines = LineSegs() - lines.setThickness(self.visualization_params['point_size']) - lines.setColor(self.visualization_params['collision_color']) - - # 绘制碰撞点 - for collision_info in collisions: - collision_point = collision_info.get('point', Point3(0, 0, 0)) - lines.moveTo(collision_point) - lines.drawTo(collision_point) - - # 创建可视化节点 - collision_geom = lines.create() - collision_node = self.debug_root.attachNewNode(collision_geom) - collision_node.setName(f"cloth_collisions_debug_{id(cloth_info)}") - cloth_info['_debug_collision_node'] = collision_node - - except Exception as e: - print(f"⚠ 可视化布料碰撞失败: {e}") - - def visualize_bounding_box(self, cloth_info: Dict[str, Any]): - """ - 可视化包围盒 - - Args: - cloth_info: 布料对象信息 - """ - if not self.enabled or not self.show_flags['bounding_box']: - return - - try: - # 清除之前的可视化 - self._clear_cloth_visualization(cloth_info, "bounding_box") - - # 获取软体节点 - soft_body = cloth_info.get('soft_body') - if not soft_body: - return - - # 获取节点信息 - nodes = soft_body.get_nodes() - if not nodes: - return - - # 计算包围盒 - min_pos = Point3(float('inf'), float('inf'), float('inf')) - max_pos = Point3(float('-inf'), float('-inf'), float('-inf')) - - for node in nodes: - pos = node.get_position() - min_pos.set( - min(min_pos[0], pos[0]), - min(min_pos[1], pos[1]), - min(min_pos[2], pos[2]) - ) - max_pos.set( - max(max_pos[0], pos[0]), - max(max_pos[1], pos[1]), - max(max_pos[2], pos[2]) - ) - - # 创建包围盒可视化 - lines = LineSegs() - lines.setThickness(1.0) - lines.setColor(Vec4(0.5, 0.5, 1.0, 0.5)) # 蓝色半透明 - - # 绘制包围盒 - self._draw_box(lines, min_pos, max_pos) - - # 创建可视化节点 - bbox_geom = lines.create() - bbox_node = self.debug_root.attachNewNode(bbox_geom) - bbox_node.setName(f"cloth_bbox_debug_{id(cloth_info)}") - cloth_info['_debug_bbox_node'] = bbox_node - - except Exception as e: - print(f"⚠ 可视化布料包围盒失败: {e}") - - def update_visualization(self, cloth_info: Dict[str, Any]): - """ - 更新布料可视化 - - Args: - cloth_info: 布料对象信息 - """ - if not self.enabled: - return - - # 更新各种可视化 - if self.show_flags['mesh']: - self.visualize_cloth_mesh(cloth_info) - if self.show_flags['anchors']: - self.visualize_anchors(cloth_info) - if self.show_flags['forces']: - self.visualize_forces(cloth_info) - if self.show_flags['velocities']: - self.visualize_velocities(cloth_info) - if self.show_flags['normals']: - self.visualize_normals(cloth_info) - if self.show_flags['tears']: - self.visualize_tears(cloth_info) - if self.show_flags['collisions']: - self.visualize_collisions(cloth_info) - if self.show_flags['bounding_box']: - self.visualize_bounding_box(cloth_info) - - # 更新性能统计 - self.performance_stats['update_count'] += 1 - - def clear_cloth_visualization(self, cloth_info: Dict[str, Any]): - """ - 清除布料可视化 - - Args: - cloth_info: 布料对象信息 - """ - self._clear_cloth_visualization(cloth_info, "all") - - def _clear_cloth_visualization(self, cloth_info: Dict[str, Any], - visualization_type: str): - """ - 清除布料可视化(内部方法) - - Args: - cloth_info: 布料对象信息 - visualization_type: 可视化类型 - """ - try: - visualization_map = { - "mesh": '_debug_mesh_node', - "anchors": '_debug_anchor_node', - "forces": '_debug_force_node', - "velocities": '_debug_velocity_node', - "normals": '_debug_normal_node', - "tears": '_debug_tear_node', - "collisions": '_debug_collision_node', - "bounding_box": '_debug_bbox_node' - } - - if visualization_type == "all": - # 清除所有可视化 - for key in visualization_map.values(): - node = cloth_info.get(key) - if node and not node.isEmpty(): - node.removeNode() - if key in cloth_info: - del cloth_info[key] - else: - # 清除特定可视化 - key = visualization_map.get(visualization_type) - if key: - node = cloth_info.get(key) - if node and not node.isEmpty(): - node.removeNode() - if key in cloth_info: - del cloth_info[key] - - except Exception as e: - print(f"⚠ 清除布料可视化失败: {e}") - - def _draw_arrow_head(self, lines: LineSegs, start: Point3, end: Point3, size: float): - """ - 绘制箭头头部 - - Args: - lines: 线段对象 - start: 起点 - end: 终点 - size: 箭头大小 - """ - direction = end - start - length = direction.length() - if length == 0: - return - - direction.normalize() - - # 计算垂直向量 - if abs(direction.dot(Vec3(0, 0, 1))) < 0.999: - up_vector = Vec3(0, 0, 1) - else: - up_vector = Vec3(1, 0, 0) - - right_vector = direction.cross(up_vector) - right_vector.normalize() - up_vector = right_vector.cross(direction) - up_vector.normalize() - - # 绘制箭头 - arrow_point1 = end - direction * size + right_vector * size * 0.5 - arrow_point2 = end - direction * size - right_vector * size * 0.5 - - lines.moveTo(end) - lines.drawTo(arrow_point1) - lines.moveTo(end) - lines.drawTo(arrow_point2) - - def _draw_circle(self, lines: LineSegs, center: Point3, radius: float, segments: int): - """ - 绘制圆形 - - Args: - lines: 线段对象 - center: 圆心 - radius: 半径 - segments: 分段数 - """ - for i in range(segments): - angle1 = (i / segments) * 2 * math.pi - angle2 = ((i + 1) / segments) * 2 * math.pi - - point1 = center + Point3(math.cos(angle1) * radius, math.sin(angle1) * radius, 0) - point2 = center + Point3(math.cos(angle2) * radius, math.sin(angle2) * radius, 0) - - lines.moveTo(point1) - lines.drawTo(point2) - - def _draw_box(self, lines: LineSegs, min_pos: Point3, max_pos: Point3): - """ - 绘制包围盒 - - Args: - lines: 线段对象 - min_pos: 最小点 - max_pos: 最大点 - """ - # 8个顶点 - vertices = [ - Point3(min_pos[0], min_pos[1], min_pos[2]), # 0 - Point3(max_pos[0], min_pos[1], min_pos[2]), # 1 - Point3(max_pos[0], max_pos[1], min_pos[2]), # 2 - Point3(min_pos[0], max_pos[1], min_pos[2]), # 3 - Point3(min_pos[0], min_pos[1], max_pos[2]), # 4 - Point3(max_pos[0], min_pos[1], max_pos[2]), # 5 - Point3(max_pos[0], max_pos[1], max_pos[2]), # 6 - Point3(min_pos[0], max_pos[1], max_pos[2]) # 7 - ] - - # 12条边 - edges = [ - (0, 1), (1, 2), (2, 3), (3, 0), # 底面 - (4, 5), (5, 6), (6, 7), (7, 4), # 顶面 - (0, 4), (1, 5), (2, 6), (3, 7) # 垂直边 - ] - - for edge in edges: - lines.moveTo(vertices[edge[0]]) - lines.drawTo(vertices[edge[1]]) - - def _create_stats_display(self): - """ - 创建统计信息显示 - """ - # 创建文本节点 - text_node = TextNode('cloth_debug_stats') - text_node.setText("Cloth Debug Stats") - text_node.setAlign(TextNode.ALeft) - text_node.setTextColor(1, 1, 0, 1) # 黄色 - - # 创建节点路径 - self.stats_node = self.debug_root.attachNewNode(text_node) - self.stats_node.setScale(0.1) - self.stats_node.setPos(-2, 0, 2) # 屏幕左上角 - - def update_stats_display(self): - """ - 更新统计信息显示 - """ - if self.stats_node and self.show_stats: - text = f"Cloth Debug Stats\n" - text += f"Objects: {self.performance_stats['rendered_objects']}\n" - text += f"Vertices: {self.performance_stats['total_vertices']}\n" - text += f"Updates: {self.performance_stats['update_count']}\n" - text += f"FPS: {self.performance_stats['fps']:.1f}" - - self.stats_node.node().setText(text) - - def cleanup(self): - """ - 清理所有调试可视化 - """ - if not self.debug_root.isEmpty(): - self.debug_root.removeNode() - if self.stats_node: - self.stats_node.removeNode() - self.stats_node = None - print("✓ 布料调试可视化已清理") - - -class SoftBodyDebugVisualizer: - """ - 软体调试可视化器 - 提供软体物理模拟的可视化调试功能 - """ - - def __init__(self, render_node: NodePath): - """ - 初始化调试可视化器 - - Args: - render_node: 渲染节点 - """ - self.render_node = render_node - self.debug_root = render_node.attachNewNode("soft_body_debug_visualizer") - self.enabled = False - - # 可视化标志 - self.show_flags = { - 'mesh': True, - 'forces': False, - 'velocities': False, - 'normals': False, - 'collisions': False, - 'bounding_box': False, - 'mass_distribution': False, - 'constraints': False - } - - # 可视化参数 - self.visualization_params = { - 'mesh_color': Vec4(1.0, 0.0, 0.0, 0.7), # 红色半透明 - 'force_color': Vec4(1.0, 1.0, 0.0, 1.0), # 黄色 - 'velocity_color': Vec4(0.0, 0.0, 1.0, 1.0), # 蓝色 - 'normal_color': Vec4(1.0, 0.5, 0.0, 1.0), # 橙色 - 'collision_color': Vec4(1.0, 0.5, 0.0, 1.0), # 橙色 - 'constraint_color': Vec4(0.5, 0.0, 1.0, 1.0), # 紫色 - 'line_thickness': 2.0, - 'point_size': 5.0, - 'vector_scale': 1.0, - 'transparency': 0.7 - } - - # 性能监控 - self.performance_stats = { - 'last_update_time': 0.0, - 'update_count': 0, - 'rendered_objects': 0, - 'total_vertices': 0, - 'fps': 0.0 - } - - # 统计信息显示 - self.stats_node = None - self.show_stats = False - - def enable(self): - """ - 启用调试可视化 - """ - self.enabled = True - self.debug_root.show() - print("✓ 软体调试可视化已启用") - - def disable(self): - """ - 禁用调试可视化 - """ - self.enabled = False - self.debug_root.hide() - print("✓ 软体调试可视化已禁用") - - def set_visibility_flags(self, **kwargs): - """ - 设置可视化标志 - - Args: - **kwargs: 可视化标志键值对 - """ - for key, value in kwargs.items(): - if key in self.show_flags: - self.show_flags[key] = value - - def set_visualization_params(self, **kwargs): - """ - 设置可视化参数 - - Args: - **kwargs: 可视化参数键值对 - """ - for key, value in kwargs.items(): - if key in self.visualization_params: - self.visualization_params[key] = value - - def enable_statistics(self, show: bool = True): - """ - 启用统计信息显示 - - Args: - show: 是否显示统计信息 - """ - self.show_stats = show - if show and not self.stats_node: - self._create_stats_display() - elif not show and self.stats_node: - self.stats_node.removeNode() - self.stats_node = None - - def visualize_soft_body_mesh(self, soft_info: Dict[str, Any]): - """ - 可视化软体网格 - - Args: - soft_info: 软体对象信息 - """ - if not self.enabled or not self.show_flags['mesh']: - return - - try: - # 清除之前的可视化 - self._clear_soft_body_visualization(soft_info, "mesh") - - # 获取软体节点 - soft_body = soft_info.get('soft_body') - if not soft_body: - return - - # 获取节点信息 - nodes = soft_body.get_nodes() - links = soft_body.get_links() - - if not nodes or not links: - return - - # 创建线条段 - lines = LineSegs() - lines.setThickness(self.visualization_params['line_thickness']) - lines.setColor(self.visualization_params['mesh_color']) - - # 绘制链接线 - for link in links: - node_a = nodes[link.get_node_a()] - node_b = nodes[link.get_node_b()] - - pos_a = node_a.get_position() - pos_b = node_b.get_position() - - lines.moveTo(pos_a) - lines.drawTo(pos_b) - - # 创建可视化节点 - mesh_geom = lines.create() - mesh_node = self.debug_root.attachNewNode(mesh_geom) - mesh_node.setName(f"soft_body_mesh_debug_{id(soft_info)}") - soft_info['_debug_mesh_node'] = mesh_node - - # 应用透明度 - mesh_node.setTransparency(TransparencyAttrib.MAlpha) - - self.performance_stats['total_vertices'] += len(nodes) - - except Exception as e: - print(f"⚠ 可视化软体网格失败: {e}") - - def visualize_soft_body_forces(self, soft_info: Dict[str, Any]): - """ - 可视化软体力 - - Args: - soft_info: 软体对象信息 - """ - if not self.enabled or not self.show_flags['forces']: - return - - try: - # 清除之前的可视化 - self._clear_soft_body_visualization(soft_info, "forces") - - # 获取软体节点 - soft_body = soft_info.get('soft_body') - if not soft_body: - return - - # 获取节点信息 - nodes = soft_body.get_nodes() - if not nodes: - return - - # 创建力向量可视化 - lines = LineSegs() - lines.setThickness(1.0) - lines.setColor(self.visualization_params['force_color']) - - # 绘制力向量 - scale = self.visualization_params['vector_scale'] * 0.1 - for i, node in enumerate(nodes): - pos = node.get_position() - # 简化处理:使用速度作为力的近似 - velocity = node.get_velocity() - force_vector = velocity * scale - - if force_vector.length() > 0.01: # 只显示显著的力 - lines.moveTo(pos) - lines.drawTo(pos + force_vector) - - # 绘制箭头 - self._draw_arrow_head(lines, pos, pos + force_vector, 0.1) - - # 创建可视化节点 - force_geom = lines.create() - force_node = self.debug_root.attachNewNode(force_geom) - force_node.setName(f"soft_body_forces_debug_{id(soft_info)}") - soft_info['_debug_force_node'] = force_node - - except Exception as e: - print(f"⚠ 可视化软体力失败: {e}") - - def visualize_soft_body_velocities(self, soft_info: Dict[str, Any]): - """ - 可视化软体速度 - - Args: - soft_info: 软体对象信息 - """ - if not self.enabled or not self.show_flags['velocities']: - return - - try: - # 清除之前的可视化 - self._clear_soft_body_visualization(soft_info, "velocities") - - # 获取软体节点 - soft_body = soft_info.get('soft_body') - if not soft_body: - return - - # 获取节点信息 - nodes = soft_body.get_nodes() - if not nodes: - return - - # 创建速度向量可视化 - lines = LineSegs() - lines.setThickness(1.0) - lines.setColor(self.visualization_params['velocity_color']) - - # 绘制速度向量 - scale = self.visualization_params['vector_scale'] * 0.05 - for i, node in enumerate(nodes): - pos = node.get_position() - velocity = node.get_velocity() - velocity_vector = velocity * scale - - if velocity_vector.length() > 0.005: # 只显示显著的速度 - lines.moveTo(pos) - lines.drawTo(pos + velocity_vector) - - # 绘制箭头 - self._draw_arrow_head(lines, pos, pos + velocity_vector, 0.05) - - # 创建可视化节点 - velocity_geom = lines.create() - velocity_node = self.debug_root.attachNewNode(velocity_geom) - velocity_node.setName(f"soft_body_velocities_debug_{id(soft_info)}") - soft_info['_debug_velocity_node'] = velocity_node - - except Exception as e: - print(f"⚠ 可视化软体速度失败: {e}") - - def visualize_constraints(self, soft_info: Dict[str, Any]): - """ - 可视化约束 - - Args: - soft_info: 软体对象信息 - """ - if not self.enabled or not self.show_flags['constraints']: - return - - try: - # 清除之前的可视化 - self._clear_soft_body_visualization(soft_info, "constraints") - - # 获取约束信息 - constraints = soft_info.get('constraints', []) - if not constraints: - return - - # 创建约束可视化 - lines = LineSegs() - lines.setThickness(self.visualization_params['line_thickness']) - lines.setColor(self.visualization_params['constraint_color']) - - # 绘制约束线 - for constraint_info in constraints: - # 简化处理 - pass - - # 创建可视化节点 - constraint_geom = lines.create() - constraint_node = self.debug_root.attachNewNode(constraint_geom) - constraint_node.setName(f"soft_body_constraints_debug_{id(soft_info)}") - soft_info['_debug_constraint_node'] = constraint_node - - except Exception as e: - print(f"⚠ 可视化软体约束失败: {e}") - - def update_visualization(self, soft_info: Dict[str, Any]): - """ - 更新软体可视化 - - Args: - soft_info: 软体对象信息 - """ - if not self.enabled: - return - - # 更新各种可视化 - if self.show_flags['mesh']: - self.visualize_soft_body_mesh(soft_info) - if self.show_flags['forces']: - self.visualize_soft_body_forces(soft_info) - if self.show_flags['velocities']: - self.visualize_soft_body_velocities(soft_info) - if self.show_flags['constraints']: - self.visualize_constraints(soft_info) - - # 更新性能统计 - self.performance_stats['update_count'] += 1 - - def clear_soft_body_visualization(self, soft_info: Dict[str, Any]): - """ - 清除软体可视化 - - Args: - soft_info: 软体对象信息 - """ - self._clear_soft_body_visualization(soft_info, "all") - - def _clear_soft_body_visualization(self, soft_info: Dict[str, Any], - visualization_type: str): - """ - 清除软体可视化(内部方法) - - Args: - soft_info: 软体对象信息 - visualization_type: 可视化类型 - """ - try: - visualization_map = { - "mesh": '_debug_mesh_node', - "forces": '_debug_force_node', - "velocities": '_debug_velocity_node', - "constraints": '_debug_constraint_node' - } - - if visualization_type == "all": - # 清除所有可视化 - for key in visualization_map.values(): - node = soft_info.get(key) - if node and not node.isEmpty(): - node.removeNode() - if key in soft_info: - del soft_info[key] - else: - # 清除特定可视化 - key = visualization_map.get(visualization_type) - if key: - node = soft_info.get(key) - if node and not node.isEmpty(): - node.removeNode() - if key in soft_info: - del soft_info[key] - - except Exception as e: - print(f"⚠ 清除软体可视化失败: {e}") - - def _draw_arrow_head(self, lines: LineSegs, start: Point3, end: Point3, size: float): - """ - 绘制箭头头部 - - Args: - lines: 线段对象 - start: 起点 - end: 终点 - size: 箭头大小 - """ - direction = end - start - length = direction.length() - if length == 0: - return - - direction.normalize() - - # 计算垂直向量 - if abs(direction.dot(Vec3(0, 0, 1))) < 0.999: - up_vector = Vec3(0, 0, 1) - else: - up_vector = Vec3(1, 0, 0) - - right_vector = direction.cross(up_vector) - right_vector.normalize() - up_vector = right_vector.cross(direction) - up_vector.normalize() - - # 绘制箭头 - arrow_point1 = end - direction * size + right_vector * size * 0.5 - arrow_point2 = end - direction * size - right_vector * size * 0.5 - - lines.moveTo(end) - lines.drawTo(arrow_point1) - lines.moveTo(end) - lines.drawTo(arrow_point2) - - def _create_stats_display(self): - """ - 创建统计信息显示 - """ - # 创建文本节点 - text_node = TextNode('soft_body_debug_stats') - text_node.setText("Soft Body Debug Stats") - text_node.setAlign(TextNode.ALeft) - text_node.setTextColor(1, 0, 0, 1) # 红色 - - # 创建节点路径 - self.stats_node = self.debug_root.attachNewNode(text_node) - self.stats_node.setScale(0.1) - self.stats_node.setPos(-2, 0, 1) # 屏幕左上角 - - def update_stats_display(self): - """ - 更新统计信息显示 - """ - if self.stats_node and self.show_stats: - text = f"Soft Body Debug Stats\n" - text += f"Objects: {self.performance_stats['rendered_objects']}\n" - text += f"Vertices: {self.performance_stats['total_vertices']}\n" - text += f"Updates: {self.performance_stats['update_count']}\n" - text += f"FPS: {self.performance_stats['fps']:.1f}" - - self.stats_node.node().setText(text) - - def cleanup(self): - """ - 清理所有调试可视化 - """ - if not self.debug_root.isEmpty(): - self.debug_root.removeNode() - if self.stats_node: - self.stats_node.removeNode() - self.stats_node = None - print("✓ 软体调试可视化已清理") - - -class PhysicsDebugManager: - """ - 物理调试管理器 - 统一管理布料和软体的调试可视化 - """ - - def __init__(self, render_node: NodePath): - """ - 初始化物理调试管理器 - - Args: - render_node: 渲染节点 - """ - self.render_node = render_node - self.cloth_visualizer = ClothDebugVisualizer(render_node) - self.soft_body_visualizer = SoftBodyDebugVisualizer(render_node) - self.enabled = False - - # 全局设置 - self.global_settings = { - 'show_statistics': False, - 'update_frequency': 1, # 每帧更新 - 'auto_cleanup': True - } - - def enable(self): - """ - 启用物理调试 - """ - self.enabled = True - self.cloth_visualizer.enable() - self.soft_body_visualizer.enable() - print("✓ 物理调试已启用") - - def disable(self): - """ - 禁用物理调试 - """ - self.enabled = False - self.cloth_visualizer.disable() - self.soft_body_visualizer.disable() - print("✓ 物理调试已禁用") - - def set_global_settings(self, **kwargs): - """ - 设置全局调试设置 - - Args: - **kwargs: 设置键值对 - """ - for key, value in kwargs.items(): - if key in self.global_settings: - self.global_settings[key] = value - - def set_cloth_visibility(self, **kwargs): - """ - 设置布料可视化标志 - - Args: - **kwargs: 可视化标志键值对 - """ - self.cloth_visualizer.set_visibility_flags(**kwargs) - - def set_soft_body_visibility(self, **kwargs): - """ - 设置软体可视化标志 - - Args: - **kwargs: 可视化标志键值对 - """ - self.soft_body_visualizer.set_visibility_flags(**kwargs) - - def set_cloth_params(self, **kwargs): - """ - 设置布料可视化参数 - - Args: - **kwargs: 参数键值对 - """ - self.cloth_visualizer.set_visualization_params(**kwargs) - - def set_soft_body_params(self, **kwargs): - """ - 设置软体可视化参数 - - Args: - **kwargs: 参数键值对 - """ - self.soft_body_visualizer.set_visualization_params(**kwargs) - - def update_all_visualizations(self, cloth_objects: List[Dict[str, Any]], - soft_body_objects: List[Dict[str, Any]]): - """ - 更新所有可视化 - - Args: - cloth_objects: 布料对象列表 - soft_body_objects: 软体对象列表 - """ - if not self.enabled: - return - - # 更新布料可视化 - for cloth_info in cloth_objects: - self.cloth_visualizer.update_visualization(cloth_info) - - # 更新软体可视化 - for soft_info in soft_body_objects: - self.soft_body_visualizer.update_visualization(soft_info) - - # 更新统计显示 - if self.global_settings['show_statistics']: - self.cloth_visualizer.update_stats_display() - self.soft_body_visualizer.update_stats_display() - - def clear_all_visualizations(self, cloth_objects: List[Dict[str, Any]], - soft_body_objects: List[Dict[str, Any]]): - """ - 清除所有可视化 - - Args: - cloth_objects: 布料对象列表 - soft_body_objects: 软体对象列表 - """ - # 清除布料可视化 - for cloth_info in cloth_objects: - self.cloth_visualizer.clear_cloth_visualization(cloth_info) - - # 清除软体可视化 - for soft_info in soft_body_objects: - self.soft_body_visualizer.clear_soft_body_visualization(soft_info) - - def cleanup(self): - """ - 清理所有调试可视化 - """ - self.cloth_visualizer.cleanup() - self.soft_body_visualizer.cleanup() - print("✓ 物理调试已清理") - - def get_debug_stats(self) -> Dict[str, Any]: - """ - 获取调试统计信息 - - Returns: - 统计信息字典 - """ - return { - 'cloth_stats': self.cloth_visualizer.performance_stats, - 'soft_body_stats': self.soft_body_visualizer.performance_stats, - 'global_settings': self.global_settings, - 'enabled': self.enabled +""" +调试可视化工具 +提供软体和布料物理模拟的调试可视化功能 +包含完整的可视化和性能监控功能 +""" + +from panda3d.core import NodePath, LineSegs, Vec3, Vec4, Point3 +from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter +from panda3d.core import GeomLines, Geom, GeomNode, TextNode +from panda3d.core import TransparencyAttrib +from typing import List, Dict, Any, Optional, Tuple +import math + + +class ClothDebugVisualizer: + """ + 布料调试可视化器 + 提供布料物理模拟的可视化调试功能,包括网格、固定点、力、速度等 + """ + + # 可视化模式 + MODE_WIREFRAME = "wireframe" + MODE_POINTS = "points" + MODE_SOLID = "solid" + + # 颜色主题 + COLOR_THEME_DEFAULT = "default" + COLOR_THEME_HEATMAP = "heatmap" + COLOR_THEME_STRESS = "stress" + + def __init__(self, render_node: NodePath): + """ + 初始化调试可视化器 + + Args: + render_node: 渲染节点 + """ + self.render_node = render_node + self.debug_root = render_node.attachNewNode("cloth_debug_visualizer") + self.enabled = False + + # 可视化标志 + self.show_flags = { + 'mesh': True, + 'anchors': True, + 'forces': False, + 'velocities': False, + 'normals': False, + 'tears': True, + 'collisions': False, + 'bounding_box': False, + 'stress': False, + 'strain': False + } + + # 可视化参数 + self.visualization_params = { + 'mesh_color': Vec4(0.0, 1.0, 0.0, 0.7), # 绿色半透明 + 'anchor_color': Vec4(1.0, 0.0, 0.0, 1.0), # 红色 + 'force_color': Vec4(1.0, 1.0, 0.0, 1.0), # 黄色 + 'velocity_color': Vec4(0.0, 0.0, 1.0, 1.0), # 蓝色 + 'normal_color': Vec4(1.0, 0.5, 0.0, 1.0), # 橙色 + 'tear_color': Vec4(1.0, 0.0, 1.0, 1.0), # 紫色 + 'collision_color': Vec4(1.0, 0.5, 0.0, 1.0), # 橙色 + 'line_thickness': 2.0, + 'point_size': 5.0, + 'vector_scale': 1.0, + 'transparency': 0.7 + } + + # 性能监控 + self.performance_stats = { + 'last_update_time': 0.0, + 'update_count': 0, + 'rendered_objects': 0, + 'total_vertices': 0, + 'fps': 0.0 + } + + # 可视化模式 + self.visualization_mode = self.MODE_WIREFRAME + self.color_theme = self.COLOR_THEME_DEFAULT + + # 统计信息显示 + self.stats_node = None + self.show_stats = False + + def enable(self): + """ + 启用调试可视化 + """ + self.enabled = True + self.debug_root.show() + print("✓ 布料调试可视化已启用") + + def disable(self): + """ + 禁用调试可视化 + """ + self.enabled = False + self.debug_root.hide() + print("✓ 布料调试可视化已禁用") + + def set_visibility_flags(self, **kwargs): + """ + 设置可视化标志 + + Args: + **kwargs: 可视化标志键值对 + """ + for key, value in kwargs.items(): + if key in self.show_flags: + self.show_flags[key] = value + + def set_visualization_params(self, **kwargs): + """ + 设置可视化参数 + + Args: + **kwargs: 可视化参数键值对 + """ + for key, value in kwargs.items(): + if key in self.visualization_params: + self.visualization_params[key] = value + + def set_visualization_mode(self, mode: str): + """ + 设置可视化模式 + + Args: + mode: 可视化模式 + """ + if mode in [self.MODE_WIREFRAME, self.MODE_POINTS, self.MODE_SOLID]: + self.visualization_mode = mode + + def set_color_theme(self, theme: str): + """ + 设置颜色主题 + + Args: + theme: 颜色主题 + """ + if theme in [self.COLOR_THEME_DEFAULT, self.COLOR_THEME_HEATMAP, self.COLOR_THEME_STRESS]: + self.color_theme = theme + + def enable_statistics(self, show: bool = True): + """ + 启用统计信息显示 + + Args: + show: 是否显示统计信息 + """ + self.show_stats = show + if show and not self.stats_node: + self._create_stats_display() + elif not show and self.stats_node: + self.stats_node.removeNode() + self.stats_node = None + + def visualize_cloth_mesh(self, cloth_info: Dict[str, Any]): + """ + 可视化布料网格 + + Args: + cloth_info: 布料对象信息 + """ + if not self.enabled or not self.show_flags['mesh']: + return + + try: + # 清除之前的可视化 + self._clear_cloth_visualization(cloth_info, "mesh") + + # 获取软体节点 + soft_body = cloth_info.get('soft_body') + if not soft_body: + return + + # 获取节点信息 + nodes = soft_body.get_nodes() + links = soft_body.get_links() + + if not nodes or not links: + return + + # 创建线条段 + lines = LineSegs() + lines.setThickness(self.visualization_params['line_thickness']) + lines.setColor(self.visualization_params['mesh_color']) + + # 根据颜色主题设置颜色 + base_color = self.visualization_params['mesh_color'] + + # 绘制链接线 + for link in links: + node_a = nodes[link.get_node_a()] + node_b = nodes[link.get_node_b()] + + pos_a = node_a.get_position() + pos_b = node_b.get_position() + + # 根据模式调整颜色 + if self.color_theme == self.COLOR_THEME_STRESS: + # 根据应力调整颜色 + stress = link.get_bbending() # 简化处理 + color_factor = min(1.0, stress * 2.0) + color = Vec4( + base_color[0] + color_factor * (1.0 - base_color[0]), + base_color[1] * (1.0 - color_factor), + base_color[2] * (1.0 - color_factor), + base_color[3] + ) + lines.setColor(color) + elif self.color_theme == self.COLOR_THEME_HEATMAP: + # 根据位置高度调整颜色 + height_factor = (pos_a[2] + pos_b[2]) / 2.0 + normalized_height = max(0.0, min(1.0, (height_factor + 10.0) / 20.0)) + lines.setColor(Vec4(normalized_height, 1.0 - normalized_height, 0.5, base_color[3])) + else: + lines.setColor(base_color) + + lines.moveTo(pos_a) + lines.drawTo(pos_b) + + # 创建可视化节点 + mesh_geom = lines.create() + mesh_node = self.debug_root.attachNewNode(mesh_geom) + mesh_node.setName(f"cloth_mesh_debug_{id(cloth_info)}") + cloth_info['_debug_mesh_node'] = mesh_node + + # 应用透明度 + mesh_node.setTransparency(TransparencyAttrib.MAlpha) + + self.performance_stats['total_vertices'] += len(nodes) + + except Exception as e: + print(f"⚠ 可视化布料网格失败: {e}") + + def visualize_anchors(self, cloth_info: Dict[str, Any]): + """ + 可视化固定点 + + Args: + cloth_info: 布料对象信息 + """ + if not self.enabled or not self.show_flags['anchors']: + return + + try: + # 清除之前的可视化 + self._clear_cloth_visualization(cloth_info, "anchors") + + # 获取固定点信息 + anchors = cloth_info.get('anchors', []) + if not anchors: + return + + # 创建点表示固定点 + lines = LineSegs() + lines.setThickness(self.visualization_params['point_size']) + lines.setColor(self.visualization_params['anchor_color']) + + # 绘制固定点 + for anchor in anchors: + vertex_index = anchor.get('vertex_index', 0) + + # 获取软体节点 + soft_body = cloth_info.get('soft_body') + if soft_body: + nodes = soft_body.get_nodes() + if vertex_index < len(nodes): + pos = nodes[vertex_index].get_position() + lines.moveTo(pos) + lines.drawTo(pos) # 绘制点 + + # 创建可视化节点 + anchor_geom = lines.create() + anchor_node = self.debug_root.attachNewNode(anchor_geom) + anchor_node.setName(f"cloth_anchors_debug_{id(cloth_info)}") + cloth_info['_debug_anchor_node'] = anchor_node + + except Exception as e: + print(f"⚠ 可视化固定点失败: {e}") + + def visualize_forces(self, cloth_info: Dict[str, Any]): + """ + 可视化力 + + Args: + cloth_info: 布料对象信息 + """ + if not self.enabled or not self.show_flags['forces']: + return + + try: + # 清除之前的可视化 + self._clear_cloth_visualization(cloth_info, "forces") + + # 获取软体节点 + soft_body = cloth_info.get('soft_body') + if not soft_body: + return + + # 获取节点信息 + nodes = soft_body.get_nodes() + if not nodes: + return + + # 创建力向量可视化 + lines = LineSegs() + lines.setThickness(1.0) + lines.setColor(self.visualization_params['force_color']) + + # 绘制力向量 + scale = self.visualization_params['vector_scale'] * 0.1 + for i, node in enumerate(nodes): + pos = node.get_position() + # 简化处理:使用速度作为力的近似 + velocity = node.get_velocity() + force_vector = velocity * scale + + if force_vector.length() > 0.01: # 只显示显著的力 + lines.moveTo(pos) + lines.drawTo(pos + force_vector) + + # 绘制箭头 + self._draw_arrow_head(lines, pos, pos + force_vector, 0.1) + + # 创建可视化节点 + force_geom = lines.create() + force_node = self.debug_root.attachNewNode(force_geom) + force_node.setName(f"cloth_forces_debug_{id(cloth_info)}") + cloth_info['_debug_force_node'] = force_node + + except Exception as e: + print(f"⚠ 可视化布料力失败: {e}") + + def visualize_velocities(self, cloth_info: Dict[str, Any]): + """ + 可视化速度 + + Args: + cloth_info: 布料对象信息 + """ + if not self.enabled or not self.show_flags['velocities']: + return + + try: + # 清除之前的可视化 + self._clear_cloth_visualization(cloth_info, "velocities") + + # 获取软体节点 + soft_body = cloth_info.get('soft_body') + if not soft_body: + return + + # 获取节点信息 + nodes = soft_body.get_nodes() + if not nodes: + return + + # 创建速度向量可视化 + lines = LineSegs() + lines.setThickness(1.0) + lines.setColor(self.visualization_params['velocity_color']) + + # 绘制速度向量 + scale = self.visualization_params['vector_scale'] * 0.05 + for i, node in enumerate(nodes): + pos = node.get_position() + velocity = node.get_velocity() + velocity_vector = velocity * scale + + if velocity_vector.length() > 0.005: # 只显示显著的速度 + lines.moveTo(pos) + lines.drawTo(pos + velocity_vector) + + # 绘制箭头 + self._draw_arrow_head(lines, pos, pos + velocity_vector, 0.05) + + # 创建可视化节点 + velocity_geom = lines.create() + velocity_node = self.debug_root.attachNewNode(velocity_geom) + velocity_node.setName(f"cloth_velocities_debug_{id(cloth_info)}") + cloth_info['_debug_velocity_node'] = velocity_node + + except Exception as e: + print(f"⚠ 可视化布料速度失败: {e}") + + def visualize_normals(self, cloth_info: Dict[str, Any]): + """ + 可视化法线 + + Args: + cloth_info: 布料对象信息 + """ + if not self.enabled or not self.show_flags['normals']: + return + + try: + # 清除之前的可视化 + self._clear_cloth_visualization(cloth_info, "normals") + + # 获取软体节点 + soft_body = cloth_info.get('soft_body') + if not soft_body: + return + + # 获取面信息 + faces = soft_body.get_faces() + if not faces: + return + + # 创建法线可视化 + lines = LineSegs() + lines.setThickness(1.0) + lines.setColor(self.visualization_params['normal_color']) + + # 绘制面法线 + scale = self.visualization_params['vector_scale'] * 0.2 + for face in faces: + # 简化处理:计算面的中心和法线 + # 实际实现需要更精确的计算 + pass + + # 创建可视化节点 + normal_geom = lines.create() + normal_node = self.debug_root.attachNewNode(normal_geom) + normal_node.setName(f"cloth_normals_debug_{id(cloth_info)}") + cloth_info['_debug_normal_node'] = normal_node + + except Exception as e: + print(f"⚠ 可视化布料法线失败: {e}") + + def visualize_tears(self, cloth_info: Dict[str, Any]): + """ + 可视化撕裂 + + Args: + cloth_info: 布料对象信息 + """ + if not self.enabled or not self.show_flags['tears']: + return + + try: + # 清除之前的可视化 + self._clear_cloth_visualization(cloth_info, "tears") + + # 获取撕裂信息 + tears = cloth_info.get('tears', []) + if not tears: + return + + # 创建撕裂可视化 + lines = LineSegs() + lines.setThickness(self.visualization_params['line_thickness'] * 2) + lines.setColor(self.visualization_params['tear_color']) + + # 绘制撕裂线 + for tear_info in tears: + tear_point = tear_info.get('point', Point3(0, 0, 0)) + tear_radius = tear_info.get('radius', 0.1) + + # 绘制撕裂圆 + self._draw_circle(lines, tear_point, tear_radius, 16) + + # 创建可视化节点 + tear_geom = lines.create() + tear_node = self.debug_root.attachNewNode(tear_geom) + tear_node.setName(f"cloth_tears_debug_{id(cloth_info)}") + cloth_info['_debug_tear_node'] = tear_node + + except Exception as e: + print(f"⚠ 可视化布料撕裂失败: {e}") + + def visualize_collisions(self, cloth_info: Dict[str, Any]): + """ + 可视化碰撞 + + Args: + cloth_info: 布料对象信息 + """ + if not self.enabled or not self.show_flags['collisions']: + return + + try: + # 清除之前的可视化 + self._clear_cloth_visualization(cloth_info, "collisions") + + # 获取碰撞信息 + collisions = cloth_info.get('collisions', []) + if not collisions: + return + + # 创建碰撞可视化 + lines = LineSegs() + lines.setThickness(self.visualization_params['point_size']) + lines.setColor(self.visualization_params['collision_color']) + + # 绘制碰撞点 + for collision_info in collisions: + collision_point = collision_info.get('point', Point3(0, 0, 0)) + lines.moveTo(collision_point) + lines.drawTo(collision_point) + + # 创建可视化节点 + collision_geom = lines.create() + collision_node = self.debug_root.attachNewNode(collision_geom) + collision_node.setName(f"cloth_collisions_debug_{id(cloth_info)}") + cloth_info['_debug_collision_node'] = collision_node + + except Exception as e: + print(f"⚠ 可视化布料碰撞失败: {e}") + + def visualize_bounding_box(self, cloth_info: Dict[str, Any]): + """ + 可视化包围盒 + + Args: + cloth_info: 布料对象信息 + """ + if not self.enabled or not self.show_flags['bounding_box']: + return + + try: + # 清除之前的可视化 + self._clear_cloth_visualization(cloth_info, "bounding_box") + + # 获取软体节点 + soft_body = cloth_info.get('soft_body') + if not soft_body: + return + + # 获取节点信息 + nodes = soft_body.get_nodes() + if not nodes: + return + + # 计算包围盒 + min_pos = Point3(float('inf'), float('inf'), float('inf')) + max_pos = Point3(float('-inf'), float('-inf'), float('-inf')) + + for node in nodes: + pos = node.get_position() + min_pos.set( + min(min_pos[0], pos[0]), + min(min_pos[1], pos[1]), + min(min_pos[2], pos[2]) + ) + max_pos.set( + max(max_pos[0], pos[0]), + max(max_pos[1], pos[1]), + max(max_pos[2], pos[2]) + ) + + # 创建包围盒可视化 + lines = LineSegs() + lines.setThickness(1.0) + lines.setColor(Vec4(0.5, 0.5, 1.0, 0.5)) # 蓝色半透明 + + # 绘制包围盒 + self._draw_box(lines, min_pos, max_pos) + + # 创建可视化节点 + bbox_geom = lines.create() + bbox_node = self.debug_root.attachNewNode(bbox_geom) + bbox_node.setName(f"cloth_bbox_debug_{id(cloth_info)}") + cloth_info['_debug_bbox_node'] = bbox_node + + except Exception as e: + print(f"⚠ 可视化布料包围盒失败: {e}") + + def update_visualization(self, cloth_info: Dict[str, Any]): + """ + 更新布料可视化 + + Args: + cloth_info: 布料对象信息 + """ + if not self.enabled: + return + + # 更新各种可视化 + if self.show_flags['mesh']: + self.visualize_cloth_mesh(cloth_info) + if self.show_flags['anchors']: + self.visualize_anchors(cloth_info) + if self.show_flags['forces']: + self.visualize_forces(cloth_info) + if self.show_flags['velocities']: + self.visualize_velocities(cloth_info) + if self.show_flags['normals']: + self.visualize_normals(cloth_info) + if self.show_flags['tears']: + self.visualize_tears(cloth_info) + if self.show_flags['collisions']: + self.visualize_collisions(cloth_info) + if self.show_flags['bounding_box']: + self.visualize_bounding_box(cloth_info) + + # 更新性能统计 + self.performance_stats['update_count'] += 1 + + def clear_cloth_visualization(self, cloth_info: Dict[str, Any]): + """ + 清除布料可视化 + + Args: + cloth_info: 布料对象信息 + """ + self._clear_cloth_visualization(cloth_info, "all") + + def _clear_cloth_visualization(self, cloth_info: Dict[str, Any], + visualization_type: str): + """ + 清除布料可视化(内部方法) + + Args: + cloth_info: 布料对象信息 + visualization_type: 可视化类型 + """ + try: + visualization_map = { + "mesh": '_debug_mesh_node', + "anchors": '_debug_anchor_node', + "forces": '_debug_force_node', + "velocities": '_debug_velocity_node', + "normals": '_debug_normal_node', + "tears": '_debug_tear_node', + "collisions": '_debug_collision_node', + "bounding_box": '_debug_bbox_node' + } + + if visualization_type == "all": + # 清除所有可视化 + for key in visualization_map.values(): + node = cloth_info.get(key) + if node and not node.isEmpty(): + node.removeNode() + if key in cloth_info: + del cloth_info[key] + else: + # 清除特定可视化 + key = visualization_map.get(visualization_type) + if key: + node = cloth_info.get(key) + if node and not node.isEmpty(): + node.removeNode() + if key in cloth_info: + del cloth_info[key] + + except Exception as e: + print(f"⚠ 清除布料可视化失败: {e}") + + def _draw_arrow_head(self, lines: LineSegs, start: Point3, end: Point3, size: float): + """ + 绘制箭头头部 + + Args: + lines: 线段对象 + start: 起点 + end: 终点 + size: 箭头大小 + """ + direction = end - start + length = direction.length() + if length == 0: + return + + direction.normalize() + + # 计算垂直向量 + if abs(direction.dot(Vec3(0, 0, 1))) < 0.999: + up_vector = Vec3(0, 0, 1) + else: + up_vector = Vec3(1, 0, 0) + + right_vector = direction.cross(up_vector) + right_vector.normalize() + up_vector = right_vector.cross(direction) + up_vector.normalize() + + # 绘制箭头 + arrow_point1 = end - direction * size + right_vector * size * 0.5 + arrow_point2 = end - direction * size - right_vector * size * 0.5 + + lines.moveTo(end) + lines.drawTo(arrow_point1) + lines.moveTo(end) + lines.drawTo(arrow_point2) + + def _draw_circle(self, lines: LineSegs, center: Point3, radius: float, segments: int): + """ + 绘制圆形 + + Args: + lines: 线段对象 + center: 圆心 + radius: 半径 + segments: 分段数 + """ + for i in range(segments): + angle1 = (i / segments) * 2 * math.pi + angle2 = ((i + 1) / segments) * 2 * math.pi + + point1 = center + Point3(math.cos(angle1) * radius, math.sin(angle1) * radius, 0) + point2 = center + Point3(math.cos(angle2) * radius, math.sin(angle2) * radius, 0) + + lines.moveTo(point1) + lines.drawTo(point2) + + def _draw_box(self, lines: LineSegs, min_pos: Point3, max_pos: Point3): + """ + 绘制包围盒 + + Args: + lines: 线段对象 + min_pos: 最小点 + max_pos: 最大点 + """ + # 8个顶点 + vertices = [ + Point3(min_pos[0], min_pos[1], min_pos[2]), # 0 + Point3(max_pos[0], min_pos[1], min_pos[2]), # 1 + Point3(max_pos[0], max_pos[1], min_pos[2]), # 2 + Point3(min_pos[0], max_pos[1], min_pos[2]), # 3 + Point3(min_pos[0], min_pos[1], max_pos[2]), # 4 + Point3(max_pos[0], min_pos[1], max_pos[2]), # 5 + Point3(max_pos[0], max_pos[1], max_pos[2]), # 6 + Point3(min_pos[0], max_pos[1], max_pos[2]) # 7 + ] + + # 12条边 + edges = [ + (0, 1), (1, 2), (2, 3), (3, 0), # 底面 + (4, 5), (5, 6), (6, 7), (7, 4), # 顶面 + (0, 4), (1, 5), (2, 6), (3, 7) # 垂直边 + ] + + for edge in edges: + lines.moveTo(vertices[edge[0]]) + lines.drawTo(vertices[edge[1]]) + + def _create_stats_display(self): + """ + 创建统计信息显示 + """ + # 创建文本节点 + text_node = TextNode('cloth_debug_stats') + text_node.setText("Cloth Debug Stats") + text_node.setAlign(TextNode.ALeft) + text_node.setTextColor(1, 1, 0, 1) # 黄色 + + # 创建节点路径 + self.stats_node = self.debug_root.attachNewNode(text_node) + self.stats_node.setScale(0.1) + self.stats_node.setPos(-2, 0, 2) # 屏幕左上角 + + def update_stats_display(self): + """ + 更新统计信息显示 + """ + if self.stats_node and self.show_stats: + text = f"Cloth Debug Stats\n" + text += f"Objects: {self.performance_stats['rendered_objects']}\n" + text += f"Vertices: {self.performance_stats['total_vertices']}\n" + text += f"Updates: {self.performance_stats['update_count']}\n" + text += f"FPS: {self.performance_stats['fps']:.1f}" + + self.stats_node.node().setText(text) + + def cleanup(self): + """ + 清理所有调试可视化 + """ + if not self.debug_root.isEmpty(): + self.debug_root.removeNode() + if self.stats_node: + self.stats_node.removeNode() + self.stats_node = None + print("✓ 布料调试可视化已清理") + + +class SoftBodyDebugVisualizer: + """ + 软体调试可视化器 + 提供软体物理模拟的可视化调试功能 + """ + + def __init__(self, render_node: NodePath): + """ + 初始化调试可视化器 + + Args: + render_node: 渲染节点 + """ + self.render_node = render_node + self.debug_root = render_node.attachNewNode("soft_body_debug_visualizer") + self.enabled = False + + # 可视化标志 + self.show_flags = { + 'mesh': True, + 'forces': False, + 'velocities': False, + 'normals': False, + 'collisions': False, + 'bounding_box': False, + 'mass_distribution': False, + 'constraints': False + } + + # 可视化参数 + self.visualization_params = { + 'mesh_color': Vec4(1.0, 0.0, 0.0, 0.7), # 红色半透明 + 'force_color': Vec4(1.0, 1.0, 0.0, 1.0), # 黄色 + 'velocity_color': Vec4(0.0, 0.0, 1.0, 1.0), # 蓝色 + 'normal_color': Vec4(1.0, 0.5, 0.0, 1.0), # 橙色 + 'collision_color': Vec4(1.0, 0.5, 0.0, 1.0), # 橙色 + 'constraint_color': Vec4(0.5, 0.0, 1.0, 1.0), # 紫色 + 'line_thickness': 2.0, + 'point_size': 5.0, + 'vector_scale': 1.0, + 'transparency': 0.7 + } + + # 性能监控 + self.performance_stats = { + 'last_update_time': 0.0, + 'update_count': 0, + 'rendered_objects': 0, + 'total_vertices': 0, + 'fps': 0.0 + } + + # 统计信息显示 + self.stats_node = None + self.show_stats = False + + def enable(self): + """ + 启用调试可视化 + """ + self.enabled = True + self.debug_root.show() + print("✓ 软体调试可视化已启用") + + def disable(self): + """ + 禁用调试可视化 + """ + self.enabled = False + self.debug_root.hide() + print("✓ 软体调试可视化已禁用") + + def set_visibility_flags(self, **kwargs): + """ + 设置可视化标志 + + Args: + **kwargs: 可视化标志键值对 + """ + for key, value in kwargs.items(): + if key in self.show_flags: + self.show_flags[key] = value + + def set_visualization_params(self, **kwargs): + """ + 设置可视化参数 + + Args: + **kwargs: 可视化参数键值对 + """ + for key, value in kwargs.items(): + if key in self.visualization_params: + self.visualization_params[key] = value + + def enable_statistics(self, show: bool = True): + """ + 启用统计信息显示 + + Args: + show: 是否显示统计信息 + """ + self.show_stats = show + if show and not self.stats_node: + self._create_stats_display() + elif not show and self.stats_node: + self.stats_node.removeNode() + self.stats_node = None + + def visualize_soft_body_mesh(self, soft_info: Dict[str, Any]): + """ + 可视化软体网格 + + Args: + soft_info: 软体对象信息 + """ + if not self.enabled or not self.show_flags['mesh']: + return + + try: + # 清除之前的可视化 + self._clear_soft_body_visualization(soft_info, "mesh") + + # 获取软体节点 + soft_body = soft_info.get('soft_body') + if not soft_body: + return + + # 获取节点信息 + nodes = soft_body.get_nodes() + links = soft_body.get_links() + + if not nodes or not links: + return + + # 创建线条段 + lines = LineSegs() + lines.setThickness(self.visualization_params['line_thickness']) + lines.setColor(self.visualization_params['mesh_color']) + + # 绘制链接线 + for link in links: + node_a = nodes[link.get_node_a()] + node_b = nodes[link.get_node_b()] + + pos_a = node_a.get_position() + pos_b = node_b.get_position() + + lines.moveTo(pos_a) + lines.drawTo(pos_b) + + # 创建可视化节点 + mesh_geom = lines.create() + mesh_node = self.debug_root.attachNewNode(mesh_geom) + mesh_node.setName(f"soft_body_mesh_debug_{id(soft_info)}") + soft_info['_debug_mesh_node'] = mesh_node + + # 应用透明度 + mesh_node.setTransparency(TransparencyAttrib.MAlpha) + + self.performance_stats['total_vertices'] += len(nodes) + + except Exception as e: + print(f"⚠ 可视化软体网格失败: {e}") + + def visualize_soft_body_forces(self, soft_info: Dict[str, Any]): + """ + 可视化软体力 + + Args: + soft_info: 软体对象信息 + """ + if not self.enabled or not self.show_flags['forces']: + return + + try: + # 清除之前的可视化 + self._clear_soft_body_visualization(soft_info, "forces") + + # 获取软体节点 + soft_body = soft_info.get('soft_body') + if not soft_body: + return + + # 获取节点信息 + nodes = soft_body.get_nodes() + if not nodes: + return + + # 创建力向量可视化 + lines = LineSegs() + lines.setThickness(1.0) + lines.setColor(self.visualization_params['force_color']) + + # 绘制力向量 + scale = self.visualization_params['vector_scale'] * 0.1 + for i, node in enumerate(nodes): + pos = node.get_position() + # 简化处理:使用速度作为力的近似 + velocity = node.get_velocity() + force_vector = velocity * scale + + if force_vector.length() > 0.01: # 只显示显著的力 + lines.moveTo(pos) + lines.drawTo(pos + force_vector) + + # 绘制箭头 + self._draw_arrow_head(lines, pos, pos + force_vector, 0.1) + + # 创建可视化节点 + force_geom = lines.create() + force_node = self.debug_root.attachNewNode(force_geom) + force_node.setName(f"soft_body_forces_debug_{id(soft_info)}") + soft_info['_debug_force_node'] = force_node + + except Exception as e: + print(f"⚠ 可视化软体力失败: {e}") + + def visualize_soft_body_velocities(self, soft_info: Dict[str, Any]): + """ + 可视化软体速度 + + Args: + soft_info: 软体对象信息 + """ + if not self.enabled or not self.show_flags['velocities']: + return + + try: + # 清除之前的可视化 + self._clear_soft_body_visualization(soft_info, "velocities") + + # 获取软体节点 + soft_body = soft_info.get('soft_body') + if not soft_body: + return + + # 获取节点信息 + nodes = soft_body.get_nodes() + if not nodes: + return + + # 创建速度向量可视化 + lines = LineSegs() + lines.setThickness(1.0) + lines.setColor(self.visualization_params['velocity_color']) + + # 绘制速度向量 + scale = self.visualization_params['vector_scale'] * 0.05 + for i, node in enumerate(nodes): + pos = node.get_position() + velocity = node.get_velocity() + velocity_vector = velocity * scale + + if velocity_vector.length() > 0.005: # 只显示显著的速度 + lines.moveTo(pos) + lines.drawTo(pos + velocity_vector) + + # 绘制箭头 + self._draw_arrow_head(lines, pos, pos + velocity_vector, 0.05) + + # 创建可视化节点 + velocity_geom = lines.create() + velocity_node = self.debug_root.attachNewNode(velocity_geom) + velocity_node.setName(f"soft_body_velocities_debug_{id(soft_info)}") + soft_info['_debug_velocity_node'] = velocity_node + + except Exception as e: + print(f"⚠ 可视化软体速度失败: {e}") + + def visualize_constraints(self, soft_info: Dict[str, Any]): + """ + 可视化约束 + + Args: + soft_info: 软体对象信息 + """ + if not self.enabled or not self.show_flags['constraints']: + return + + try: + # 清除之前的可视化 + self._clear_soft_body_visualization(soft_info, "constraints") + + # 获取约束信息 + constraints = soft_info.get('constraints', []) + if not constraints: + return + + # 创建约束可视化 + lines = LineSegs() + lines.setThickness(self.visualization_params['line_thickness']) + lines.setColor(self.visualization_params['constraint_color']) + + # 绘制约束线 + for constraint_info in constraints: + # 简化处理 + pass + + # 创建可视化节点 + constraint_geom = lines.create() + constraint_node = self.debug_root.attachNewNode(constraint_geom) + constraint_node.setName(f"soft_body_constraints_debug_{id(soft_info)}") + soft_info['_debug_constraint_node'] = constraint_node + + except Exception as e: + print(f"⚠ 可视化软体约束失败: {e}") + + def update_visualization(self, soft_info: Dict[str, Any]): + """ + 更新软体可视化 + + Args: + soft_info: 软体对象信息 + """ + if not self.enabled: + return + + # 更新各种可视化 + if self.show_flags['mesh']: + self.visualize_soft_body_mesh(soft_info) + if self.show_flags['forces']: + self.visualize_soft_body_forces(soft_info) + if self.show_flags['velocities']: + self.visualize_soft_body_velocities(soft_info) + if self.show_flags['constraints']: + self.visualize_constraints(soft_info) + + # 更新性能统计 + self.performance_stats['update_count'] += 1 + + def clear_soft_body_visualization(self, soft_info: Dict[str, Any]): + """ + 清除软体可视化 + + Args: + soft_info: 软体对象信息 + """ + self._clear_soft_body_visualization(soft_info, "all") + + def _clear_soft_body_visualization(self, soft_info: Dict[str, Any], + visualization_type: str): + """ + 清除软体可视化(内部方法) + + Args: + soft_info: 软体对象信息 + visualization_type: 可视化类型 + """ + try: + visualization_map = { + "mesh": '_debug_mesh_node', + "forces": '_debug_force_node', + "velocities": '_debug_velocity_node', + "constraints": '_debug_constraint_node' + } + + if visualization_type == "all": + # 清除所有可视化 + for key in visualization_map.values(): + node = soft_info.get(key) + if node and not node.isEmpty(): + node.removeNode() + if key in soft_info: + del soft_info[key] + else: + # 清除特定可视化 + key = visualization_map.get(visualization_type) + if key: + node = soft_info.get(key) + if node and not node.isEmpty(): + node.removeNode() + if key in soft_info: + del soft_info[key] + + except Exception as e: + print(f"⚠ 清除软体可视化失败: {e}") + + def _draw_arrow_head(self, lines: LineSegs, start: Point3, end: Point3, size: float): + """ + 绘制箭头头部 + + Args: + lines: 线段对象 + start: 起点 + end: 终点 + size: 箭头大小 + """ + direction = end - start + length = direction.length() + if length == 0: + return + + direction.normalize() + + # 计算垂直向量 + if abs(direction.dot(Vec3(0, 0, 1))) < 0.999: + up_vector = Vec3(0, 0, 1) + else: + up_vector = Vec3(1, 0, 0) + + right_vector = direction.cross(up_vector) + right_vector.normalize() + up_vector = right_vector.cross(direction) + up_vector.normalize() + + # 绘制箭头 + arrow_point1 = end - direction * size + right_vector * size * 0.5 + arrow_point2 = end - direction * size - right_vector * size * 0.5 + + lines.moveTo(end) + lines.drawTo(arrow_point1) + lines.moveTo(end) + lines.drawTo(arrow_point2) + + def _create_stats_display(self): + """ + 创建统计信息显示 + """ + # 创建文本节点 + text_node = TextNode('soft_body_debug_stats') + text_node.setText("Soft Body Debug Stats") + text_node.setAlign(TextNode.ALeft) + text_node.setTextColor(1, 0, 0, 1) # 红色 + + # 创建节点路径 + self.stats_node = self.debug_root.attachNewNode(text_node) + self.stats_node.setScale(0.1) + self.stats_node.setPos(-2, 0, 1) # 屏幕左上角 + + def update_stats_display(self): + """ + 更新统计信息显示 + """ + if self.stats_node and self.show_stats: + text = f"Soft Body Debug Stats\n" + text += f"Objects: {self.performance_stats['rendered_objects']}\n" + text += f"Vertices: {self.performance_stats['total_vertices']}\n" + text += f"Updates: {self.performance_stats['update_count']}\n" + text += f"FPS: {self.performance_stats['fps']:.1f}" + + self.stats_node.node().setText(text) + + def cleanup(self): + """ + 清理所有调试可视化 + """ + if not self.debug_root.isEmpty(): + self.debug_root.removeNode() + if self.stats_node: + self.stats_node.removeNode() + self.stats_node = None + print("✓ 软体调试可视化已清理") + + +class PhysicsDebugManager: + """ + 物理调试管理器 + 统一管理布料和软体的调试可视化 + """ + + def __init__(self, render_node: NodePath): + """ + 初始化物理调试管理器 + + Args: + render_node: 渲染节点 + """ + self.render_node = render_node + self.cloth_visualizer = ClothDebugVisualizer(render_node) + self.soft_body_visualizer = SoftBodyDebugVisualizer(render_node) + self.enabled = False + + # 全局设置 + self.global_settings = { + 'show_statistics': False, + 'update_frequency': 1, # 每帧更新 + 'auto_cleanup': True + } + + def enable(self): + """ + 启用物理调试 + """ + self.enabled = True + self.cloth_visualizer.enable() + self.soft_body_visualizer.enable() + print("✓ 物理调试已启用") + + def disable(self): + """ + 禁用物理调试 + """ + self.enabled = False + self.cloth_visualizer.disable() + self.soft_body_visualizer.disable() + print("✓ 物理调试已禁用") + + def set_global_settings(self, **kwargs): + """ + 设置全局调试设置 + + Args: + **kwargs: 设置键值对 + """ + for key, value in kwargs.items(): + if key in self.global_settings: + self.global_settings[key] = value + + def set_cloth_visibility(self, **kwargs): + """ + 设置布料可视化标志 + + Args: + **kwargs: 可视化标志键值对 + """ + self.cloth_visualizer.set_visibility_flags(**kwargs) + + def set_soft_body_visibility(self, **kwargs): + """ + 设置软体可视化标志 + + Args: + **kwargs: 可视化标志键值对 + """ + self.soft_body_visualizer.set_visibility_flags(**kwargs) + + def set_cloth_params(self, **kwargs): + """ + 设置布料可视化参数 + + Args: + **kwargs: 参数键值对 + """ + self.cloth_visualizer.set_visualization_params(**kwargs) + + def set_soft_body_params(self, **kwargs): + """ + 设置软体可视化参数 + + Args: + **kwargs: 参数键值对 + """ + self.soft_body_visualizer.set_visualization_params(**kwargs) + + def update_all_visualizations(self, cloth_objects: List[Dict[str, Any]], + soft_body_objects: List[Dict[str, Any]]): + """ + 更新所有可视化 + + Args: + cloth_objects: 布料对象列表 + soft_body_objects: 软体对象列表 + """ + if not self.enabled: + return + + # 更新布料可视化 + for cloth_info in cloth_objects: + self.cloth_visualizer.update_visualization(cloth_info) + + # 更新软体可视化 + for soft_info in soft_body_objects: + self.soft_body_visualizer.update_visualization(soft_info) + + # 更新统计显示 + if self.global_settings['show_statistics']: + self.cloth_visualizer.update_stats_display() + self.soft_body_visualizer.update_stats_display() + + def clear_all_visualizations(self, cloth_objects: List[Dict[str, Any]], + soft_body_objects: List[Dict[str, Any]]): + """ + 清除所有可视化 + + Args: + cloth_objects: 布料对象列表 + soft_body_objects: 软体对象列表 + """ + # 清除布料可视化 + for cloth_info in cloth_objects: + self.cloth_visualizer.clear_cloth_visualization(cloth_info) + + # 清除软体可视化 + for soft_info in soft_body_objects: + self.soft_body_visualizer.clear_soft_body_visualization(soft_info) + + def cleanup(self): + """ + 清理所有调试可视化 + """ + self.cloth_visualizer.cleanup() + self.soft_body_visualizer.cleanup() + print("✓ 物理调试已清理") + + def get_debug_stats(self) -> Dict[str, Any]: + """ + 获取调试统计信息 + + Returns: + 统计信息字典 + """ + return { + 'cloth_stats': self.cloth_visualizer.performance_stats, + 'soft_body_stats': self.soft_body_visualizer.performance_stats, + 'global_settings': self.global_settings, + 'enabled': self.enabled } \ No newline at end of file diff --git a/plugins/user/soft_body_cloth_physics/materials/cloth_materials.py b/plugins/user/soft_body_cloth_physics/materials/cloth_materials.py index 8a2efde2..c7fe1092 100644 --- a/plugins/user/soft_body_cloth_physics/materials/cloth_materials.py +++ b/plugins/user/soft_body_cloth_physics/materials/cloth_materials.py @@ -1,1909 +1,1909 @@ -""" -布料材质系统 -提供布料材质的定义和管理 -包含完整的物理和视觉属性定义 -""" - -from panda3d.core import Vec4, Vec3 -from typing import Dict, Any, List, Optional -import math - - -class ClothMaterial: - """ - 布料材质类 - 定义布料的物理和视觉属性,包含完整的材料科学参数 - """ - - # 预定义的布料材质类型 - TYPE_COTTON = "cotton" - TYPE_SILK = "silk" - TYPE_LEATHER = "leather" - TYPE_RUBBER = "rubber" - TYPE_DENIM = "denim" - TYPE_LINEN = "linen" - TYPE_WOOL = "wool" - TYPE_SYNTHETIC = "synthetic" - TYPE_MESH = "mesh" - TYPE_CANVAS = "canvas" - TYPE_NYLON = "nylon" - TYPE_POLYESTER = "polyester" - TYPE_SATIN = "satin" - TYPE_TERRY = "terry" - TYPE_FLEECE = "fleece" - TYPE_NEOPRENE = "neoprene" - TYPE_SPANDEX = "spandex" - TYPE_TWEED = "tweed" - TYPE_TULLE = "tulle" - TYPE_VELVET = "velvet" - TYPE_SUEDE = "suede" - - # 材质行为特性 - BEHAVIOR_DEFAULT = "default" - BEHAVIOR_STIFF = "stiff" - BEHAVIOR_FLEXIBLE = "flexible" - BEHAVIOR_ELASTIC = "elastic" - BEHAVIOR_HEAVY = "heavy" - BEHAVIOR_LIGHT = "light" - - def __init__(self, name: str, material_type: str = TYPE_COTTON, - physical_params: Dict[str, float] = None, - visual_params: Dict[str, Any] = None, - advanced_params: Dict[str, Any] = None): - """ - 初始化布料材质 - - Args: - name: 材质名称 - material_type: 材质类型 - physical_params: 物理参数字典 - visual_params: 视觉参数字典 - advanced_params: 高级参数字典 - """ - self.name = name - self.material_type = material_type - self.behavior_mode = self.BEHAVIOR_DEFAULT - - # 默认物理参数(基础物理属性) - self.default_physical_params = { - 'density': 0.6, # 密度 (kg/m³) - 'linear_stiffness': 0.7, # 线性刚度 - 'angular_stiffness': 0.6, # 角度刚度 - 'volume_stiffness': 0.4, # 体积保持刚度 - 'friction': 0.6, # 摩擦系数 - 'restitution': 0.1, # 恢复系数 - 'pressure_coefficient': 0.0, # 压力系数 - 'drag_coefficient': 0.02, # 阻力系数 - 'lift_coefficient': 0.01, # 升力系数 - 'bend_stiffness': 0.5, # 弯曲刚度 - 'damping': 0.01, # 阻尼系数 - 'plastic_threshold': 0.1, # 塑性阈值 - 'plastic_stiffness': 0.5, # 塑性刚度 - 'tear_threshold': 100.0, # 撕裂阈值 - 'tear_resistance': 50.0, # 撕裂阻力 - 'anisotropic_friction': [0.6, 0.6, 0.6], # 各向异性摩擦 - 'dynamic_friction': 0.6, # 动态摩擦系数 - 'static_friction': 0.7, # 静态摩擦系数 - 'rolling_friction': 0.1, # 滚动摩擦系数 - 'spinning_friction': 0.05, # 自旋摩擦系数 - 'viscosity': 0.01, # 粘度 - 'compressibility': 0.1, # 可压缩性 - 'shear_modulus': 0.7, # 剪切模量 - 'bulk_modulus': 1.2, # 体积模量 - 'youngs_modulus': 1.0, # 杨氏模量 - 'poisson_ratio': 0.4 # 泊松比 - } - - # 默认视觉参数 - self.default_visual_params = { - 'color': Vec4(0.8, 0.8, 0.8, 1.0), # 颜色 - 'roughness': 0.7, # 粗糙度 - 'metallic': 0.0, # 金属度 - 'specular': 0.2, # 镜面反射 - 'texture_scale': 1.0, # 纹理缩放 - 'normal_strength': 1.0, # 法线强度 - 'emissive': Vec4(0, 0, 0, 0), # 自发光 - 'opacity': 1.0, # 不透明度 - 'transmission': 0.0, # 透射 - 'anisotropy': 0.0, # 各向异性 - 'clearcoat': 0.0, # 清漆层 - 'clearcoat_roughness': 0.0, # 清漆粗糙度 - 'sheen': 0.0, # 光泽 - 'sheen_roughness': 0.0, # 光泽粗糙度 - 'subsurface': 0.0, # 次表面散射 - 'subsurface_color': Vec4(1, 1, 1, 1), # 次表面颜色 - 'thickness': 1.0 # 厚度 - } - - # 默认高级参数 - self.default_advanced_params = { - 'temperature_sensitivity': 0.0, # 温度敏感性 - 'humidity_sensitivity': 0.0, # 湿度敏感性 - 'aging_factor': 0.0, # 老化因子 - 'stretch_recovery': 0.8, # 拉伸恢复 - 'compression_recovery': 0.8, # 压缩恢复 - 'fatigue_resistance': 0.9, # 疲劳抗性 - 'abrasion_resistance': 0.8, # 耐磨性 - 'chemical_resistance': 0.7, # 化学抗性 - 'uv_resistance': 0.6, # 紫外线抗性 - 'flame_resistance': 0.5, # 阻燃性 - 'water_absorption': 0.1, # 吸水性 - 'air_permeability': 0.5, # 透气性 - 'thermal_conductivity': 0.1, # 热导率 - 'electrical_conductivity': 0.0, # 电导率 - 'magnetic_permeability': 1.0, # 磁导率 - 'radiation_absorption': 0.1, # 辐射吸收 - 'biodegradability': 0.3, # 生物降解性 - 'recyclability': 0.8 # 可回收性 - } - - # 合并参数 - self.physical_params = self.default_physical_params.copy() - if physical_params: - self.physical_params.update(physical_params) - - self.visual_params = self.default_visual_params.copy() - if visual_params: - self.visual_params.update(visual_params) - - self.advanced_params = self.default_advanced_params.copy() - if advanced_params: - self.advanced_params.update(advanced_params) - - # 材质状态 - self.temperature = 20.0 # 摄氏度 - self.humidity = 50.0 # 相对湿度百分比 - self.age = 0.0 # 使用年限 - self.wear_level = 0.0 # 磨损程度 (0-1) - - def get_physical_parameters(self) -> Dict[str, float]: - """ - 获取物理参数(考虑环境和老化影响) - - Returns: - 物理参数字典 - """ - # 应用环境和老化效应 - adjusted_params = self.physical_params.copy() - - # 温度影响 - temp_sensitivity = self.advanced_params.get('temperature_sensitivity', 0.0) - temp_factor = 1.0 + (self.temperature - 20.0) * temp_sensitivity * 0.01 - adjusted_params['linear_stiffness'] *= temp_factor - adjusted_params['angular_stiffness'] *= temp_factor - - # 湿度影响 - humidity_sensitivity = self.advanced_params.get('humidity_sensitivity', 0.0) - humidity_factor = 1.0 + (self.humidity - 50.0) * humidity_sensitivity * 0.01 - adjusted_params['density'] *= humidity_factor - - # 老化影响 - aging_factor = self.advanced_params.get('aging_factor', 0.0) - aging_multiplier = 1.0 - self.age * aging_factor * 0.01 - adjusted_params['linear_stiffness'] *= aging_multiplier - adjusted_params['restitution'] *= aging_multiplier - - # 磨损影响 - adjusted_params['friction'] *= (1.0 - self.wear_level * 0.2) - adjusted_params['tear_resistance'] *= (1.0 - self.wear_level * 0.5) - - return adjusted_params - - def get_visual_parameters(self) -> Dict[str, Any]: - """ - 获取视觉参数(考虑磨损和老化影响) - - Returns: - 视觉参数字典 - """ - adjusted_params = self.visual_params.copy() - - # 磨损影响颜色和粗糙度 - if self.wear_level > 0: - # 颜色变暗 - color = adjusted_params['color'] - darkening = 1.0 - self.wear_level * 0.2 - adjusted_params['color'] = Vec4( - color[0] * darkening, - color[1] * darkening, - color[2] * darkening, - color[3] - ) - - # 增加粗糙度 - adjusted_params['roughness'] = min(1.0, - adjusted_params['roughness'] + self.wear_level * 0.3) - - # 老化影响 - aging_darkening = self.age * 0.05 - if aging_darkening > 0: - color = adjusted_params['color'] - adjusted_params['color'] = Vec4( - max(0.0, color[0] - aging_darkening), - max(0.0, color[1] - aging_darkening), - max(0.0, color[2] - aging_darkening), - color[3] - ) - - return adjusted_params - - def get_advanced_parameters(self) -> Dict[str, Any]: - """ - 获取高级参数 - - Returns: - 高级参数字典 - """ - return self.advanced_params.copy() - - def update_physical_parameter(self, param_name: str, value: float) -> bool: - """ - 更新物理参数 - - Args: - param_name: 参数名称 - value: 参数值 - - Returns: - 是否更新成功 - """ - if param_name in self.physical_params: - self.physical_params[param_name] = value - return True - return False - - def update_visual_parameter(self, param_name: str, value) -> bool: - """ - 更新视觉参数 - - Args: - param_name: 参数名称 - value: 参数值 - - Returns: - 是否更新成功 - """ - if param_name in self.visual_params: - self.visual_params[param_name] = value - return True - return False - - def update_advanced_parameter(self, param_name: str, value) -> bool: - """ - 更新高级参数 - - Args: - param_name: 参数名称 - value: 参数值 - - Returns: - 是否更新成功 - """ - if param_name in self.advanced_params: - self.advanced_params[param_name] = value - return True - return False - - def set_environment_conditions(self, temperature: float = None, - humidity: float = None): - """ - 设置环境条件 - - Args: - temperature: 温度 (摄氏度) - humidity: 湿度 (相对湿度百分比) - """ - if temperature is not None: - self.temperature = temperature - if humidity is not None: - self.humidity = max(0.0, min(100.0, humidity)) - - def simulate_aging(self, years: float): - """ - 模拟材料老化 - - Args: - years: 老化年数 - """ - self.age += years - self.age = max(0.0, self.age) - - def simulate_wear(self, wear_amount: float): - """ - 模拟材料磨损 - - Args: - wear_amount: 磨损量 (0-1) - """ - self.wear_level += wear_amount - self.wear_level = max(0.0, min(1.0, self.wear_level)) - - def set_behavior_mode(self, behavior_mode: str): - """ - 设置行为模式 - - Args: - behavior_mode: 行为模式 - """ - self.behavior_mode = behavior_mode - - def get_material_properties_summary(self) -> Dict[str, Any]: - """ - 获取材料属性摘要 - - Returns: - 属性摘要字典 - """ - return { - 'name': self.name, - 'type': self.material_type, - 'behavior_mode': self.behavior_mode, - 'environment': { - 'temperature': self.temperature, - 'humidity': self.humidity - }, - 'condition': { - 'age': self.age, - 'wear_level': self.wear_level - }, - 'physical_properties': { - 'density': self.physical_params['density'], - 'stiffness': self.physical_params['linear_stiffness'], - 'elasticity': self.physical_params['youngs_modulus'], - 'friction': self.physical_params['friction'], - 'tear_resistance': self.physical_params['tear_resistance'] - }, - 'visual_properties': { - 'color': self.visual_params['color'], - 'roughness': self.visual_params['roughness'], - 'metallic': self.visual_params['metallic'] - }, - 'advanced_properties': { - 'durability': self.advanced_params['fatigue_resistance'], - 'chemical_resistance': self.advanced_params['chemical_resistance'], - 'uv_resistance': self.advanced_params['uv_resistance'] - } - } - - -class ClothMaterialManager: - """ - 布料材质管理器 - 管理和提供预定义的布料材质,支持材质混合和自定义 - """ - - def __init__(self): - self.materials: Dict[str, ClothMaterial] = {} - self.material_combinations: Dict[str, Dict[str, Any]] = {} - self._init_default_materials() - - def _init_default_materials(self): - """ - 初始化默认布料材质 - """ - # 棉布 - 天然纤维,透气性好 - cotton = ClothMaterial( - "Cotton", - ClothMaterial.TYPE_COTTON, - { - 'density': 0.6, - 'linear_stiffness': 0.7, - 'angular_stiffness': 0.6, - 'volume_stiffness': 0.4, - 'friction': 0.6, - 'restitution': 0.1, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.02, - 'lift_coefficient': 0.01, - 'bend_stiffness': 0.5, - 'damping': 0.01, - 'plastic_threshold': 0.1, - 'plastic_stiffness': 0.5, - 'tear_threshold': 100.0, - 'tear_resistance': 50.0, - 'anisotropic_friction': [0.6, 0.6, 0.6], - 'dynamic_friction': 0.6, - 'static_friction': 0.7, - 'rolling_friction': 0.1, - 'spinning_friction': 0.05, - 'viscosity': 0.01, - 'compressibility': 0.1, - 'shear_modulus': 0.7, - 'bulk_modulus': 1.2, - 'youngs_modulus': 1.0, - 'poisson_ratio': 0.4 - }, - { - 'color': Vec4(0.9, 0.9, 0.85, 1.0), - 'roughness': 0.8, - 'metallic': 0.0, - 'specular': 0.1, - 'texture_scale': 1.0, - 'normal_strength': 0.8 - }, - { - 'temperature_sensitivity': 0.1, - 'humidity_sensitivity': 0.3, - 'aging_factor': 0.2, - 'stretch_recovery': 0.8, - 'compression_recovery': 0.8, - 'fatigue_resistance': 0.7, - 'abrasion_resistance': 0.6, - 'chemical_resistance': 0.5, - 'uv_resistance': 0.4, - 'flame_resistance': 0.6, - 'water_absorption': 0.8, - 'air_permeability': 0.9, - 'thermal_conductivity': 0.1, - 'biodegradability': 0.9, - 'recyclability': 0.8 - } - ) - - # 丝绸 - 光滑、柔软、有光泽 - silk = ClothMaterial( - "Silk", - ClothMaterial.TYPE_SILK, - { - 'density': 0.3, - 'linear_stiffness': 0.5, - 'angular_stiffness': 0.4, - 'volume_stiffness': 0.2, - 'friction': 0.2, - 'restitution': 0.3, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.05, - 'lift_coefficient': 0.03, - 'bend_stiffness': 0.3, - 'damping': 0.005, - 'plastic_threshold': 0.05, - 'plastic_stiffness': 0.3, - 'tear_threshold': 80.0, - 'tear_resistance': 30.0, - 'anisotropic_friction': [0.2, 0.2, 0.2], - 'dynamic_friction': 0.2, - 'static_friction': 0.3, - 'rolling_friction': 0.05, - 'spinning_friction': 0.02, - 'viscosity': 0.005, - 'compressibility': 0.05, - 'shear_modulus': 0.4, - 'bulk_modulus': 0.9, - 'youngs_modulus': 0.6, - 'poisson_ratio': 0.35 - }, - { - 'color': Vec4(0.95, 0.95, 0.9, 1.0), - 'roughness': 0.3, - 'metallic': 0.1, - 'specular': 0.4, - 'texture_scale': 0.5, - 'normal_strength': 0.5, - 'sheen': 0.8, - 'sheen_roughness': 0.2 - }, - { - 'temperature_sensitivity': 0.05, - 'humidity_sensitivity': 0.2, - 'aging_factor': 0.1, - 'stretch_recovery': 0.9, - 'compression_recovery': 0.9, - 'fatigue_resistance': 0.6, - 'abrasion_resistance': 0.4, - 'chemical_resistance': 0.3, - 'uv_resistance': 0.2, - 'flame_resistance': 0.7, - 'water_absorption': 0.6, - 'air_permeability': 0.7, - 'thermal_conductivity': 0.05, - 'biodegradability': 0.8, - 'recyclability': 0.7 - } - ) - - # 皮革 - 坚韧、耐用 - leather = ClothMaterial( - "Leather", - ClothMaterial.TYPE_LEATHER, - { - 'density': 0.8, - 'linear_stiffness': 0.9, - 'angular_stiffness': 0.8, - 'volume_stiffness': 0.6, - 'friction': 0.7, - 'restitution': 0.1, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.01, - 'lift_coefficient': 0.005, - 'bend_stiffness': 0.8, - 'damping': 0.015, - 'plastic_threshold': 0.2, - 'plastic_stiffness': 0.7, - 'tear_threshold': 150.0, - 'tear_resistance': 80.0, - 'anisotropic_friction': [0.7, 0.7, 0.7], - 'dynamic_friction': 0.7, - 'static_friction': 0.8, - 'rolling_friction': 0.15, - 'spinning_friction': 0.08, - 'viscosity': 0.015, - 'compressibility': 0.15, - 'shear_modulus': 0.8, - 'bulk_modulus': 1.1, - 'youngs_modulus': 0.85, - 'poisson_ratio': 0.42 - }, - { - 'color': Vec4(0.8, 0.6, 0.4, 1.0), - 'roughness': 0.6, - 'metallic': 0.0, - 'specular': 0.2, - 'texture_scale': 2.0, - 'normal_strength': 1.2 - }, - { - 'temperature_sensitivity': 0.02, - 'humidity_sensitivity': 0.1, - 'aging_factor': 0.05, - 'stretch_recovery': 0.7, - 'compression_recovery': 0.7, - 'fatigue_resistance': 0.9, - 'abrasion_resistance': 0.9, - 'chemical_resistance': 0.8, - 'uv_resistance': 0.7, - 'flame_resistance': 0.8, - 'water_absorption': 0.3, - 'air_permeability': 0.2, - 'thermal_conductivity': 0.15, - 'biodegradability': 0.4, - 'recyclability': 0.6 - } - ) - - # 橡胶 - 弹性、防水 - rubber = ClothMaterial( - "Rubber", - ClothMaterial.TYPE_RUBBER, - { - 'density': 1.2, - 'linear_stiffness': 0.95, - 'angular_stiffness': 0.9, - 'volume_stiffness': 0.8, - 'friction': 0.8, - 'restitution': 0.4, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.005, - 'lift_coefficient': 0.002, - 'bend_stiffness': 0.9, - 'damping': 0.02, - 'plastic_threshold': 0.3, - 'plastic_stiffness': 0.8, - 'tear_threshold': 200.0, - 'tear_resistance': 100.0, - 'anisotropic_friction': [0.8, 0.8, 0.8], - 'dynamic_friction': 0.8, - 'static_friction': 0.9, - 'rolling_friction': 0.2, - 'spinning_friction': 0.1, - 'viscosity': 0.02, - 'compressibility': 0.2, - 'shear_modulus': 0.9, - 'bulk_modulus': 1.5, - 'youngs_modulus': 1.2, - 'poisson_ratio': 0.45 - }, - { - 'color': Vec4(0.3, 0.3, 0.3, 1.0), - 'roughness': 0.9, - 'metallic': 0.0, - 'specular': 0.1, - 'texture_scale': 0.3, - 'normal_strength': 0.3 - }, - { - 'temperature_sensitivity': 0.15, - 'humidity_sensitivity': 0.05, - 'aging_factor': 0.3, - 'stretch_recovery': 0.95, - 'compression_recovery': 0.95, - 'fatigue_resistance': 0.8, - 'abrasion_resistance': 0.7, - 'chemical_resistance': 0.9, - 'uv_resistance': 0.6, - 'flame_resistance': 0.9, - 'water_absorption': 0.1, - 'air_permeability': 0.0, - 'thermal_conductivity': 0.2, - 'biodegradability': 0.2, - 'recyclability': 0.5 - } - ) - - # 牛仔布 - 坚韧、耐磨 - denim = ClothMaterial( - "Denim", - ClothMaterial.TYPE_DENIM, - { - 'density': 0.7, - 'linear_stiffness': 0.8, - 'angular_stiffness': 0.7, - 'volume_stiffness': 0.5, - 'friction': 0.65, - 'restitution': 0.15, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.015, - 'lift_coefficient': 0.008, - 'bend_stiffness': 0.6, - 'damping': 0.012, - 'plastic_threshold': 0.15, - 'plastic_stiffness': 0.6, - 'tear_threshold': 120.0, - 'tear_resistance': 60.0, - 'anisotropic_friction': [0.65, 0.65, 0.65], - 'dynamic_friction': 0.65, - 'static_friction': 0.75, - 'rolling_friction': 0.12, - 'spinning_friction': 0.06, - 'viscosity': 0.012, - 'compressibility': 0.12, - 'shear_modulus': 0.65, - 'bulk_modulus': 1.0, - 'youngs_modulus': 0.75, - 'poisson_ratio': 0.38 - }, - { - 'color': Vec4(0.4, 0.5, 0.7, 1.0), - 'roughness': 0.7, - 'metallic': 0.0, - 'specular': 0.15, - 'texture_scale': 1.5, - 'normal_strength': 1.0 - }, - { - 'temperature_sensitivity': 0.08, - 'humidity_sensitivity': 0.15, - 'aging_factor': 0.1, - 'stretch_recovery': 0.75, - 'compression_recovery': 0.75, - 'fatigue_resistance': 0.8, - 'abrasion_resistance': 0.8, - 'chemical_resistance': 0.6, - 'uv_resistance': 0.5, - 'flame_resistance': 0.7, - 'water_absorption': 0.4, - 'air_permeability': 0.6, - 'thermal_conductivity': 0.12, - 'biodegradability': 0.6, - 'recyclability': 0.7 - } - ) - - # 亚麻 - 天然、透气 - linen = ClothMaterial( - "Linen", - ClothMaterial.TYPE_LINEN, - { - 'density': 0.5, - 'linear_stiffness': 0.6, - 'angular_stiffness': 0.5, - 'volume_stiffness': 0.3, - 'friction': 0.55, - 'restitution': 0.12, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.025, - 'lift_coefficient': 0.012, - 'bend_stiffness': 0.4, - 'damping': 0.008, - 'plastic_threshold': 0.12, - 'plastic_stiffness': 0.4, - 'tear_threshold': 90.0, - 'tear_resistance': 40.0, - 'anisotropic_friction': [0.55, 0.55, 0.55], - 'dynamic_friction': 0.55, - 'static_friction': 0.65, - 'rolling_friction': 0.08, - 'spinning_friction': 0.04, - 'viscosity': 0.008, - 'compressibility': 0.08, - 'shear_modulus': 0.5, - 'bulk_modulus': 0.8, - 'youngs_modulus': 0.6, - 'poisson_ratio': 0.35 - }, - { - 'color': Vec4(0.9, 0.85, 0.75, 1.0), - 'roughness': 0.85, - 'metallic': 0.0, - 'specular': 0.12, - 'texture_scale': 1.2, - 'normal_strength': 0.9 - }, - { - 'temperature_sensitivity': 0.12, - 'humidity_sensitivity': 0.25, - 'aging_factor': 0.15, - 'stretch_recovery': 0.7, - 'compression_recovery': 0.7, - 'fatigue_resistance': 0.6, - 'abrasion_resistance': 0.5, - 'chemical_resistance': 0.4, - 'uv_resistance': 0.3, - 'flame_resistance': 0.8, - 'water_absorption': 0.7, - 'air_permeability': 0.95, - 'thermal_conductivity': 0.08, - 'biodegradability': 0.95, - 'recyclability': 0.85 - } - ) - - # 羊毛 - 保暖、柔软 - wool = ClothMaterial( - "Wool", - ClothMaterial.TYPE_WOOL, - { - 'density': 0.65, - 'linear_stiffness': 0.65, - 'angular_stiffness': 0.55, - 'volume_stiffness': 0.35, - 'friction': 0.6, - 'restitution': 0.13, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.018, - 'lift_coefficient': 0.009, - 'bend_stiffness': 0.45, - 'damping': 0.01, - 'plastic_threshold': 0.13, - 'plastic_stiffness': 0.45, - 'tear_threshold': 100.0, - 'tear_resistance': 50.0, - 'anisotropic_friction': [0.6, 0.6, 0.6], - 'dynamic_friction': 0.6, - 'static_friction': 0.7, - 'rolling_friction': 0.1, - 'spinning_friction': 0.05, - 'viscosity': 0.01, - 'compressibility': 0.18, - 'shear_modulus': 0.55, - 'bulk_modulus': 0.9, - 'youngs_modulus': 0.65, - 'poisson_ratio': 0.37 - }, - { - 'color': Vec4(0.85, 0.8, 0.7, 1.0), - 'roughness': 0.75, - 'metallic': 0.0, - 'specular': 0.13, - 'texture_scale': 1.8, - 'normal_strength': 1.1, - 'subsurface': 0.3, - 'subsurface_color': Vec4(0.9, 0.8, 0.7, 1.0) - }, - { - 'temperature_sensitivity': 0.05, - 'humidity_sensitivity': 0.4, - 'aging_factor': 0.12, - 'stretch_recovery': 0.8, - 'compression_recovery': 0.8, - 'fatigue_resistance': 0.7, - 'abrasion_resistance': 0.6, - 'chemical_resistance': 0.5, - 'uv_resistance': 0.4, - 'flame_resistance': 0.9, - 'water_absorption': 0.6, - 'air_permeability': 0.7, - 'thermal_conductivity': 0.05, - 'biodegradability': 0.85, - 'recyclability': 0.75 - } - ) - - # 合成纤维 - 多功能性 - synthetic = ClothMaterial( - "Synthetic", - ClothMaterial.TYPE_SYNTHETIC, - { - 'density': 0.4, - 'linear_stiffness': 0.75, - 'angular_stiffness': 0.65, - 'volume_stiffness': 0.45, - 'friction': 0.5, - 'restitution': 0.2, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.012, - 'lift_coefficient': 0.007, - 'bend_stiffness': 0.55, - 'damping': 0.006, - 'plastic_threshold': 0.18, - 'plastic_stiffness': 0.55, - 'tear_threshold': 110.0, - 'tear_resistance': 55.0, - 'anisotropic_friction': [0.5, 0.5, 0.5], - 'dynamic_friction': 0.5, - 'static_friction': 0.6, - 'rolling_friction': 0.07, - 'spinning_friction': 0.03, - 'viscosity': 0.006, - 'compressibility': 0.1, - 'shear_modulus': 0.55, - 'bulk_modulus': 0.95, - 'youngs_modulus': 0.7, - 'poisson_ratio': 0.39 - }, - { - 'color': Vec4(0.7, 0.7, 0.7, 1.0), - 'roughness': 0.5, - 'metallic': 0.05, - 'specular': 0.25, - 'texture_scale': 1.0, - 'normal_strength': 0.7 - }, - { - 'temperature_sensitivity': 0.08, - 'humidity_sensitivity': 0.1, - 'aging_factor': 0.08, - 'stretch_recovery': 0.85, - 'compression_recovery': 0.85, - 'fatigue_resistance': 0.85, - 'abrasion_resistance': 0.75, - 'chemical_resistance': 0.85, - 'uv_resistance': 0.75, - 'flame_resistance': 0.6, - 'water_absorption': 0.2, - 'air_permeability': 0.6, - 'thermal_conductivity': 0.15, - 'biodegradability': 0.3, - 'recyclability': 0.9 - } - ) - - # 网眼布 - 透气、轻质 - mesh = ClothMaterial( - "Mesh", - ClothMaterial.TYPE_MESH, - { - 'density': 0.2, - 'linear_stiffness': 0.4, - 'angular_stiffness': 0.3, - 'volume_stiffness': 0.1, - 'friction': 0.3, - 'restitution': 0.05, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.04, - 'lift_coefficient': 0.02, - 'bend_stiffness': 0.2, - 'damping': 0.003, - 'plastic_threshold': 0.08, - 'plastic_stiffness': 0.2, - 'tear_threshold': 70.0, - 'tear_resistance': 25.0, - 'anisotropic_friction': [0.3, 0.3, 0.3], - 'dynamic_friction': 0.3, - 'static_friction': 0.4, - 'rolling_friction': 0.03, - 'spinning_friction': 0.01, - 'viscosity': 0.003, - 'compressibility': 0.05, - 'shear_modulus': 0.2, - 'bulk_modulus': 0.5, - 'youngs_modulus': 0.3, - 'poisson_ratio': 0.3 - }, - { - 'color': Vec4(0.6, 0.6, 0.6, 0.7), - 'roughness': 0.6, - 'metallic': 0.1, - 'specular': 0.2, - 'texture_scale': 3.0, - 'normal_strength': 0.4, - 'opacity': 0.7 - }, - { - 'temperature_sensitivity': 0.03, - 'humidity_sensitivity': 0.05, - 'aging_factor': 0.03, - 'stretch_recovery': 0.9, - 'compression_recovery': 0.9, - 'fatigue_resistance': 0.5, - 'abrasion_resistance': 0.3, - 'chemical_resistance': 0.7, - 'uv_resistance': 0.6, - 'flame_resistance': 0.8, - 'water_absorption': 0.1, - 'air_permeability': 1.0, - 'thermal_conductivity': 0.2, - 'biodegradability': 0.4, - 'recyclability': 0.95 - } - ) - - # 帆布 - 坚固、耐用 - canvas = ClothMaterial( - "Canvas", - ClothMaterial.TYPE_CANVAS, - { - 'density': 0.75, - 'linear_stiffness': 0.85, - 'angular_stiffness': 0.75, - 'volume_stiffness': 0.55, - 'friction': 0.68, - 'restitution': 0.14, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.013, - 'lift_coefficient': 0.006, - 'bend_stiffness': 0.65, - 'damping': 0.014, - 'plastic_threshold': 0.18, - 'plastic_stiffness': 0.65, - 'tear_threshold': 130.0, - 'tear_resistance': 65.0, - 'anisotropic_friction': [0.68, 0.68, 0.68], - 'dynamic_friction': 0.68, - 'static_friction': 0.78, - 'rolling_friction': 0.14, - 'spinning_friction': 0.07, - 'viscosity': 0.014, - 'compressibility': 0.14, - 'shear_modulus': 0.65, - 'bulk_modulus': 1.05, - 'youngs_modulus': 0.8, - 'poisson_ratio': 0.4 - }, - { - 'color': Vec4(0.8, 0.75, 0.65, 1.0), - 'roughness': 0.78, - 'metallic': 0.0, - 'specular': 0.14, - 'texture_scale': 2.5, - 'normal_strength': 1.3 - }, - { - 'temperature_sensitivity': 0.06, - 'humidity_sensitivity': 0.2, - 'aging_factor': 0.08, - 'stretch_recovery': 0.78, - 'compression_recovery': 0.78, - 'fatigue_resistance': 0.85, - 'abrasion_resistance': 0.85, - 'chemical_resistance': 0.7, - 'uv_resistance': 0.6, - 'flame_resistance': 0.75, - 'water_absorption': 0.35, - 'air_permeability': 0.5, - 'thermal_conductivity': 0.13, - 'biodegradability': 0.5, - 'recyclability': 0.7 - } - ) - - # 尼龙 - 强韧、轻质 - nylon = ClothMaterial( - "Nylon", - ClothMaterial.TYPE_NYLON, - { - 'density': 0.35, - 'linear_stiffness': 0.8, - 'angular_stiffness': 0.7, - 'volume_stiffness': 0.5, - 'friction': 0.4, - 'restitution': 0.25, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.01, - 'lift_coefficient': 0.005, - 'bend_stiffness': 0.6, - 'damping': 0.007, - 'plastic_threshold': 0.15, - 'plastic_stiffness': 0.6, - 'tear_threshold': 140.0, - 'tear_resistance': 70.0, - 'anisotropic_friction': [0.4, 0.4, 0.4], - 'dynamic_friction': 0.4, - 'static_friction': 0.5, - 'rolling_friction': 0.08, - 'spinning_friction': 0.04, - 'viscosity': 0.007, - 'compressibility': 0.08, - 'shear_modulus': 0.6, - 'bulk_modulus': 1.0, - 'youngs_modulus': 0.85, - 'poisson_ratio': 0.4 - }, - { - 'color': Vec4(0.95, 0.95, 0.95, 1.0), - 'roughness': 0.4, - 'metallic': 0.0, - 'specular': 0.3, - 'texture_scale': 1.0, - 'normal_strength': 0.6 - }, - { - 'temperature_sensitivity': 0.1, - 'humidity_sensitivity': 0.15, - 'aging_factor': 0.1, - 'stretch_recovery': 0.92, - 'compression_recovery': 0.92, - 'fatigue_resistance': 0.88, - 'abrasion_resistance': 0.8, - 'chemical_resistance': 0.8, - 'uv_resistance': 0.5, - 'flame_resistance': 0.65, - 'water_absorption': 0.25, - 'air_permeability': 0.65, - 'thermal_conductivity': 0.18, - 'biodegradability': 0.25, - 'recyclability': 0.85 - } - ) - - # 聚酯纤维 - 耐用、抗皱 - polyester = ClothMaterial( - "Polyester", - ClothMaterial.TYPE_POLYESTER, - { - 'density': 0.45, - 'linear_stiffness': 0.7, - 'angular_stiffness': 0.6, - 'volume_stiffness': 0.4, - 'friction': 0.45, - 'restitution': 0.18, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.015, - 'lift_coefficient': 0.008, - 'bend_stiffness': 0.5, - 'damping': 0.009, - 'plastic_threshold': 0.12, - 'plastic_stiffness': 0.5, - 'tear_threshold': 110.0, - 'tear_resistance': 55.0, - 'anisotropic_friction': [0.45, 0.45, 0.45], - 'dynamic_friction': 0.45, - 'static_friction': 0.55, - 'rolling_friction': 0.09, - 'spinning_friction': 0.045, - 'viscosity': 0.009, - 'compressibility': 0.1, - 'shear_modulus': 0.55, - 'bulk_modulus': 0.95, - 'youngs_modulus': 0.75, - 'poisson_ratio': 0.38 - }, - { - 'color': Vec4(0.8, 0.8, 0.8, 1.0), - 'roughness': 0.55, - 'metallic': 0.0, - 'specular': 0.2, - 'texture_scale': 1.0, - 'normal_strength': 0.7 - }, - { - 'temperature_sensitivity': 0.07, - 'humidity_sensitivity': 0.08, - 'aging_factor': 0.06, - 'stretch_recovery': 0.88, - 'compression_recovery': 0.88, - 'fatigue_resistance': 0.82, - 'abrasion_resistance': 0.75, - 'chemical_resistance': 0.85, - 'uv_resistance': 0.65, - 'flame_resistance': 0.7, - 'water_absorption': 0.15, - 'air_permeability': 0.55, - 'thermal_conductivity': 0.14, - 'biodegradability': 0.15, - 'recyclability': 0.9 - } - ) - - # 缎子 - 光滑、有光泽 - satin = ClothMaterial( - "Satin", - ClothMaterial.TYPE_SATIN, - { - 'density': 0.25, - 'linear_stiffness': 0.45, - 'angular_stiffness': 0.35, - 'volume_stiffness': 0.25, - 'friction': 0.15, - 'restitution': 0.28, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.03, - 'lift_coefficient': 0.015, - 'bend_stiffness': 0.3, - 'damping': 0.004, - 'plastic_threshold': 0.08, - 'plastic_stiffness': 0.3, - 'tear_threshold': 75.0, - 'tear_resistance': 35.0, - 'anisotropic_friction': [0.15, 0.15, 0.15], - 'dynamic_friction': 0.15, - 'static_friction': 0.25, - 'rolling_friction': 0.04, - 'spinning_friction': 0.02, - 'viscosity': 0.004, - 'compressibility': 0.06, - 'shear_modulus': 0.35, - 'bulk_modulus': 0.7, - 'youngs_modulus': 0.5, - 'poisson_ratio': 0.33 - }, - { - 'color': Vec4(0.98, 0.98, 0.98, 1.0), - 'roughness': 0.1, - 'metallic': 0.2, - 'specular': 0.6, - 'texture_scale': 0.8, - 'normal_strength': 0.3, - 'sheen': 0.9, - 'sheen_roughness': 0.1 - }, - { - 'temperature_sensitivity': 0.04, - 'humidity_sensitivity': 0.12, - 'aging_factor': 0.08, - 'stretch_recovery': 0.85, - 'compression_recovery': 0.85, - 'fatigue_resistance': 0.65, - 'abrasion_resistance': 0.45, - 'chemical_resistance': 0.6, - 'uv_resistance': 0.3, - 'flame_resistance': 0.75, - 'water_absorption': 0.3, - 'air_permeability': 0.4, - 'thermal_conductivity': 0.07, - 'biodegradability': 0.5, - 'recyclability': 0.75 - } - ) - - # 毛圈织物 - 吸水、柔软 - terry = ClothMaterial( - "Terry", - ClothMaterial.TYPE_TERRY, - { - 'density': 0.55, - 'linear_stiffness': 0.55, - 'angular_stiffness': 0.45, - 'volume_stiffness': 0.3, - 'friction': 0.5, - 'restitution': 0.1, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.02, - 'lift_coefficient': 0.01, - 'bend_stiffness': 0.35, - 'damping': 0.012, - 'plastic_threshold': 0.1, - 'plastic_stiffness': 0.35, - 'tear_threshold': 95.0, - 'tear_resistance': 45.0, - 'anisotropic_friction': [0.5, 0.5, 0.5], - 'dynamic_friction': 0.5, - 'static_friction': 0.6, - 'rolling_friction': 0.1, - 'spinning_friction': 0.05, - 'viscosity': 0.012, - 'compressibility': 0.15, - 'shear_modulus': 0.45, - 'bulk_modulus': 0.85, - 'youngs_modulus': 0.6, - 'poisson_ratio': 0.36 - }, - { - 'color': Vec4(0.92, 0.92, 0.92, 1.0), - 'roughness': 0.8, - 'metallic': 0.0, - 'specular': 0.15, - 'texture_scale': 2.0, - 'normal_strength': 1.5, - 'subsurface': 0.2, - 'subsurface_color': Vec4(0.95, 0.95, 0.95, 1.0) - }, - { - 'temperature_sensitivity': 0.09, - 'humidity_sensitivity': 0.35, - 'aging_factor': 0.13, - 'stretch_recovery': 0.75, - 'compression_recovery': 0.75, - 'fatigue_resistance': 0.65, - 'abrasion_resistance': 0.55, - 'chemical_resistance': 0.5, - 'uv_resistance': 0.45, - 'flame_resistance': 0.8, - 'water_absorption': 0.9, - 'air_permeability': 0.75, - 'thermal_conductivity': 0.1, - 'biodegradability': 0.7, - 'recyclability': 0.8 - } - ) - - # 抓绒 - 保暖、柔软 - fleece = ClothMaterial( - "Fleece", - ClothMaterial.TYPE_FLEECE, - { - 'density': 0.3, - 'linear_stiffness': 0.4, - 'angular_stiffness': 0.3, - 'volume_stiffness': 0.2, - 'friction': 0.4, - 'restitution': 0.08, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.025, - 'lift_coefficient': 0.012, - 'bend_stiffness': 0.25, - 'damping': 0.008, - 'plastic_threshold': 0.07, - 'plastic_stiffness': 0.25, - 'tear_threshold': 80.0, - 'tear_resistance': 35.0, - 'anisotropic_friction': [0.4, 0.4, 0.4], - 'dynamic_friction': 0.4, - 'static_friction': 0.5, - 'rolling_friction': 0.08, - 'spinning_friction': 0.04, - 'viscosity': 0.008, - 'compressibility': 0.2, - 'shear_modulus': 0.3, - 'bulk_modulus': 0.6, - 'youngs_modulus': 0.4, - 'poisson_ratio': 0.32 - }, - { - 'color': Vec4(0.88, 0.88, 0.88, 1.0), - 'roughness': 0.85, - 'metallic': 0.0, - 'specular': 0.1, - 'texture_scale': 1.5, - 'normal_strength': 1.8, - 'subsurface': 0.4, - 'subsurface_color': Vec4(0.9, 0.9, 0.9, 1.0) - }, - { - 'temperature_sensitivity': 0.06, - 'humidity_sensitivity': 0.25, - 'aging_factor': 0.1, - 'stretch_recovery': 0.8, - 'compression_recovery': 0.8, - 'fatigue_resistance': 0.6, - 'abrasion_resistance': 0.5, - 'chemical_resistance': 0.6, - 'uv_resistance': 0.5, - 'flame_resistance': 0.85, - 'water_absorption': 0.5, - 'air_permeability': 0.65, - 'thermal_conductivity': 0.04, - 'biodegradability': 0.6, - 'recyclability': 0.85 - } - ) - - # 氯丁橡胶 - 防水、保温 - neoprene = ClothMaterial( - "Neoprene", - ClothMaterial.TYPE_NEOPRENE, - { - 'density': 0.9, - 'linear_stiffness': 0.85, - 'angular_stiffness': 0.75, - 'volume_stiffness': 0.65, - 'friction': 0.6, - 'restitution': 0.22, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.008, - 'lift_coefficient': 0.003, - 'bend_stiffness': 0.7, - 'damping': 0.018, - 'plastic_threshold': 0.25, - 'plastic_stiffness': 0.7, - 'tear_threshold': 160.0, - 'tear_resistance': 85.0, - 'anisotropic_friction': [0.6, 0.6, 0.6], - 'dynamic_friction': 0.6, - 'static_friction': 0.7, - 'rolling_friction': 0.16, - 'spinning_friction': 0.09, - 'viscosity': 0.018, - 'compressibility': 0.18, - 'shear_modulus': 0.75, - 'bulk_modulus': 1.2, - 'youngs_modulus': 0.9, - 'poisson_ratio': 0.43 - }, - { - 'color': Vec4(0.2, 0.2, 0.2, 1.0), - 'roughness': 0.7, - 'metallic': 0.0, - 'specular': 0.25, - 'texture_scale': 0.5, - 'normal_strength': 0.8 - }, - { - 'temperature_sensitivity': 0.12, - 'humidity_sensitivity': 0.03, - 'aging_factor': 0.15, - 'stretch_recovery': 0.9, - 'compression_recovery': 0.9, - 'fatigue_resistance': 0.85, - 'abrasion_resistance': 0.75, - 'chemical_resistance': 0.95, - 'uv_resistance': 0.7, - 'flame_resistance': 0.8, - 'water_absorption': 0.05, - 'air_permeability': 0.0, - 'thermal_conductivity': 0.12, - 'biodegradability': 0.1, - 'recyclability': 0.6 - } - ) - - # 氨纶 - 高弹性 - spandex = ClothMaterial( - "Spandex", - ClothMaterial.TYPE_SPANDEX, - { - 'density': 0.2, - 'linear_stiffness': 0.3, - 'angular_stiffness': 0.25, - 'volume_stiffness': 0.15, - 'friction': 0.35, - 'restitution': 0.35, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.018, - 'lift_coefficient': 0.009, - 'bend_stiffness': 0.2, - 'damping': 0.005, - 'plastic_threshold': 0.05, - 'plastic_stiffness': 0.2, - 'tear_threshold': 60.0, - 'tear_resistance': 20.0, - 'anisotropic_friction': [0.35, 0.35, 0.35], - 'dynamic_friction': 0.35, - 'static_friction': 0.45, - 'rolling_friction': 0.06, - 'spinning_friction': 0.03, - 'viscosity': 0.005, - 'compressibility': 0.07, - 'shear_modulus': 0.25, - 'bulk_modulus': 0.55, - 'youngs_modulus': 0.35, - 'poisson_ratio': 0.31 - }, - { - 'color': Vec4(0.95, 0.95, 0.95, 1.0), - 'roughness': 0.45, - 'metallic': 0.0, - 'specular': 0.35, - 'texture_scale': 0.8, - 'normal_strength': 0.4 - }, - { - 'temperature_sensitivity': 0.08, - 'humidity_sensitivity': 0.07, - 'aging_factor': 0.05, - 'stretch_recovery': 0.98, - 'compression_recovery': 0.98, - 'fatigue_resistance': 0.75, - 'abrasion_resistance': 0.4, - 'chemical_resistance': 0.7, - 'uv_resistance': 0.4, - 'flame_resistance': 0.6, - 'water_absorption': 0.1, - 'air_permeability': 0.5, - 'thermal_conductivity': 0.16, - 'biodegradability': 0.2, - 'recyclability': 0.7 - } - ) - - # 粗花呢 - 厚重、有纹理 - tweed = ClothMaterial( - "Tweed", - ClothMaterial.TYPE_TWEED, - { - 'density': 0.7, - 'linear_stiffness': 0.75, - 'angular_stiffness': 0.65, - 'volume_stiffness': 0.45, - 'friction': 0.62, - 'restitution': 0.12, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.014, - 'lift_coefficient': 0.007, - 'bend_stiffness': 0.55, - 'damping': 0.013, - 'plastic_threshold': 0.14, - 'plastic_stiffness': 0.55, - 'tear_threshold': 115.0, - 'tear_resistance': 58.0, - 'anisotropic_friction': [0.62, 0.62, 0.62], - 'dynamic_friction': 0.62, - 'static_friction': 0.72, - 'rolling_friction': 0.13, - 'spinning_friction': 0.065, - 'viscosity': 0.013, - 'compressibility': 0.13, - 'shear_modulus': 0.58, - 'bulk_modulus': 0.98, - 'youngs_modulus': 0.72, - 'poisson_ratio': 0.39 - }, - { - 'color': Vec4(0.75, 0.7, 0.6, 1.0), - 'roughness': 0.72, - 'metallic': 0.0, - 'specular': 0.16, - 'texture_scale': 3.0, - 'normal_strength': 1.4 - }, - { - 'temperature_sensitivity': 0.07, - 'humidity_sensitivity': 0.18, - 'aging_factor': 0.09, - 'stretch_recovery': 0.72, - 'compression_recovery': 0.72, - 'fatigue_resistance': 0.78, - 'abrasion_resistance': 0.72, - 'chemical_resistance': 0.65, - 'uv_resistance': 0.55, - 'flame_resistance': 0.72, - 'water_absorption': 0.32, - 'air_permeability': 0.58, - 'thermal_conductivity': 0.11, - 'biodegradability': 0.55, - 'recyclability': 0.72 - } - ) - - # 薄纱 - 轻盈、透明 - tulle = ClothMaterial( - "Tulle", - ClothMaterial.TYPE_TULLE, - { - 'density': 0.1, - 'linear_stiffness': 0.25, - 'angular_stiffness': 0.2, - 'volume_stiffness': 0.1, - 'friction': 0.2, - 'restitution': 0.15, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.035, - 'lift_coefficient': 0.018, - 'bend_stiffness': 0.15, - 'damping': 0.003, - 'plastic_threshold': 0.04, - 'plastic_stiffness': 0.15, - 'tear_threshold': 40.0, - 'tear_resistance': 15.0, - 'anisotropic_friction': [0.2, 0.2, 0.2], - 'dynamic_friction': 0.2, - 'static_friction': 0.3, - 'rolling_friction': 0.03, - 'spinning_friction': 0.015, - 'viscosity': 0.003, - 'compressibility': 0.03, - 'shear_modulus': 0.15, - 'bulk_modulus': 0.3, - 'youngs_modulus': 0.2, - 'poisson_ratio': 0.3 - }, - { - 'color': Vec4(1.0, 1.0, 1.0, 0.3), - 'roughness': 0.5, - 'metallic': 0.0, - 'specular': 0.4, - 'texture_scale': 4.0, - 'normal_strength': 0.2, - 'opacity': 0.3 - }, - { - 'temperature_sensitivity': 0.02, - 'humidity_sensitivity': 0.04, - 'aging_factor': 0.02, - 'stretch_recovery': 0.95, - 'compression_recovery': 0.95, - 'fatigue_resistance': 0.4, - 'abrasion_resistance': 0.2, - 'chemical_resistance': 0.5, - 'uv_resistance': 0.4, - 'flame_resistance': 0.9, - 'water_absorption': 0.05, - 'air_permeability': 1.0, - 'thermal_conductivity': 0.25, - 'biodegradability': 0.6, - 'recyclability': 0.98 - } - ) - - # 天鹅绒 - 柔软、有光泽 - velvet = ClothMaterial( - "Velvet", - ClothMaterial.TYPE_VELVET, - { - 'density': 0.6, - 'linear_stiffness': 0.5, - 'angular_stiffness': 0.4, - 'volume_stiffness': 0.3, - 'friction': 0.45, - 'restitution': 0.1, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.022, - 'lift_coefficient': 0.011, - 'bend_stiffness': 0.35, - 'damping': 0.011, - 'plastic_threshold': 0.09, - 'plastic_stiffness': 0.35, - 'tear_threshold': 85.0, - 'tear_resistance': 40.0, - 'anisotropic_friction': [0.45, 0.45, 0.45], - 'dynamic_friction': 0.45, - 'static_friction': 0.55, - 'rolling_friction': 0.09, - 'spinning_friction': 0.045, - 'viscosity': 0.011, - 'compressibility': 0.11, - 'shear_modulus': 0.4, - 'bulk_modulus': 0.75, - 'youngs_modulus': 0.55, - 'poisson_ratio': 0.35 - }, - { - 'color': Vec4(0.85, 0.8, 0.75, 1.0), - 'roughness': 0.2, - 'metallic': 0.3, - 'specular': 0.5, - 'texture_scale': 1.2, - 'normal_strength': 2.0, - 'sheen': 0.7, - 'sheen_roughness': 0.15 - }, - { - 'temperature_sensitivity': 0.06, - 'humidity_sensitivity': 0.22, - 'aging_factor': 0.11, - 'stretch_recovery': 0.82, - 'compression_recovery': 0.82, - 'fatigue_resistance': 0.68, - 'abrasion_resistance': 0.52, - 'chemical_resistance': 0.58, - 'uv_resistance': 0.48, - 'flame_resistance': 0.78, - 'water_absorption': 0.42, - 'air_permeability': 0.62, - 'thermal_conductivity': 0.09, - 'biodegradability': 0.62, - 'recyclability': 0.78 - } - ) - - # 翻毛皮 - 柔软、有质感 - suede = ClothMaterial( - "Suede", - ClothMaterial.TYPE_SUEDE, - { - 'density': 0.75, - 'linear_stiffness': 0.65, - 'angular_stiffness': 0.55, - 'volume_stiffness': 0.4, - 'friction': 0.55, - 'restitution': 0.08, - 'pressure_coefficient': 0.0, - 'drag_coefficient': 0.016, - 'lift_coefficient': 0.008, - 'bend_stiffness': 0.45, - 'damping': 0.015, - 'plastic_threshold': 0.11, - 'plastic_stiffness': 0.45, - 'tear_threshold': 105.0, - 'tear_resistance': 52.0, - 'anisotropic_friction': [0.55, 0.55, 0.55], - 'dynamic_friction': 0.55, - 'static_friction': 0.65, - 'rolling_friction': 0.11, - 'spinning_friction': 0.055, - 'viscosity': 0.015, - 'compressibility': 0.12, - 'shear_modulus': 0.48, - 'bulk_modulus': 0.9, - 'youngs_modulus': 0.68, - 'poisson_ratio': 0.37 - }, - { - 'color': Vec4(0.7, 0.65, 0.6, 1.0), - 'roughness': 0.85, - 'metallic': 0.0, - 'specular': 0.18, - 'texture_scale': 2.2, - 'normal_strength': 1.6 - }, - { - 'temperature_sensitivity': 0.05, - 'humidity_sensitivity': 0.16, - 'aging_factor': 0.07, - 'stretch_recovery': 0.76, - 'compression_recovery': 0.76, - 'fatigue_resistance': 0.72, - 'abrasion_resistance': 0.62, - 'chemical_resistance': 0.68, - 'uv_resistance': 0.58, - 'flame_resistance': 0.76, - 'water_absorption': 0.38, - 'air_permeability': 0.52, - 'thermal_conductivity': 0.13, - 'biodegradability': 0.48, - 'recyclability': 0.68 - } - ) - - # 添加到材质字典 - self.materials = { - ClothMaterial.TYPE_COTTON: cotton, - ClothMaterial.TYPE_SILK: silk, - ClothMaterial.TYPE_LEATHER: leather, - ClothMaterial.TYPE_RUBBER: rubber, - ClothMaterial.TYPE_DENIM: denim, - ClothMaterial.TYPE_LINEN: linen, - ClothMaterial.TYPE_WOOL: wool, - ClothMaterial.TYPE_SYNTHETIC: synthetic, - ClothMaterial.TYPE_MESH: mesh, - ClothMaterial.TYPE_CANVAS: canvas, - ClothMaterial.TYPE_NYLON: nylon, - ClothMaterial.TYPE_POLYESTER: polyester, - ClothMaterial.TYPE_SATIN: satin, - ClothMaterial.TYPE_TERRY: terry, - ClothMaterial.TYPE_FLEECE: fleece, - ClothMaterial.TYPE_NEOPRENE: neoprene, - ClothMaterial.TYPE_SPANDEX: spandex, - ClothMaterial.TYPE_TWEED: tweed, - ClothMaterial.TYPE_TULLE: tulle, - ClothMaterial.TYPE_VELVET: velvet, - ClothMaterial.TYPE_SUEDE: suede - } - - def get_material(self, material_type: str) -> Optional[ClothMaterial]: - """ - 获取指定类型的布料材质 - - Args: - material_type: 材质类型 - - Returns: - 布料材质对象或None - """ - return self.materials.get(material_type) - - def get_all_material_types(self) -> List[str]: - """ - 获取所有材质类型 - - Returns: - 材质类型列表 - """ - return list(self.materials.keys()) - - def get_material_names(self) -> List[str]: - """ - 获取所有材质名称 - - Returns: - 材质名称列表 - """ - return [material.name for material in self.materials.values()] - - def create_custom_material(self, name: str, material_type: str, - physical_params: Dict[str, float] = None, - visual_params: Dict[str, Any] = None, - advanced_params: Dict[str, Any] = None) -> ClothMaterial: - """ - 创建自定义布料材质 - - Args: - name: 材质名称 - material_type: 材质类型 - physical_params: 物理参数 - visual_params: 视觉参数 - advanced_params: 高级参数 - - Returns: - 布料材质对象 - """ - material = ClothMaterial(name, material_type, physical_params, visual_params, advanced_params) - return material - - def register_material(self, material: ClothMaterial) -> bool: - """ - 注册布料材质 - - Args: - material: 布料材质对象 - - Returns: - 是否注册成功 - """ - if material.material_type: - self.materials[material.material_type] = material - return True - return False - - def create_material_combination(self, name: str, materials: List[str], - ratios: List[float]) -> ClothMaterial: - """ - 创建混合材质 - - Args: - name: 混合材质名称 - materials: 材质类型列表 - ratios: 比例列表 - - Returns: - 混合布料材质对象 - """ - if len(materials) != len(ratios): - raise ValueError("材质数量和比例数量必须相同") - - # 归一化比例 - total_ratio = sum(ratios) - if total_ratio == 0: - raise ValueError("比例总和不能为0") - normalized_ratios = [r / total_ratio for r in ratios] - - # 获取所有材质 - material_objects = [] - for material_type in materials: - material_obj = self.get_material(material_type) - if material_obj is None: - raise ValueError(f"未知材质类型: {material_type}") - material_objects.append(material_obj) - - # 混合物理参数 - combined_physical = {} - combined_visual = {} - combined_advanced = {} - - # 物理参数混合 - for param_name in material_objects[0].physical_params.keys(): - weighted_sum = 0.0 - for i, material_obj in enumerate(material_objects): - weighted_sum += material_obj.physical_params[param_name] * normalized_ratios[i] - combined_physical[param_name] = weighted_sum - - # 视觉参数混合 - for param_name in material_objects[0].visual_params.keys(): - if param_name == 'color': - # 颜色混合 - mixed_color = Vec4(0, 0, 0, 0) - for i, material_obj in enumerate(material_objects): - color = material_obj.visual_params[param_name] - mixed_color += color * normalized_ratios[i] - combined_visual[param_name] = mixed_color - else: - # 其他视觉参数混合 - weighted_sum = 0.0 - for i, material_obj in enumerate(material_objects): - weighted_sum += material_obj.visual_params[param_name] * normalized_ratios[i] - combined_visual[param_name] = weighted_sum - - # 高级参数混合 - for param_name in material_objects[0].advanced_params.keys(): - weighted_sum = 0.0 - for i, material_obj in enumerate(material_objects): - weighted_sum += material_obj.advanced_params[param_name] * normalized_ratios[i] - combined_advanced[param_name] = weighted_sum - - # 创建混合材质 - combined_material = ClothMaterial( - name, - f"combined_{name}", - combined_physical, - combined_visual, - combined_advanced - ) - - # 保存组合信息 - self.material_combinations[name] = { - 'materials': materials, - 'ratios': normalized_ratios, - 'material_object': combined_material - } - - return combined_material - - def get_material_combination(self, name: str) -> Optional[ClothMaterial]: - """ - 获取混合材质 - - Args: - name: 混合材质名称 - - Returns: - 混合布料材质对象或None - """ - combination = self.material_combinations.get(name) - if combination: - return combination['material_object'] - return None - - def get_material_properties_database(self) -> Dict[str, Dict[str, Any]]: - """ - 获取材质属性数据库 - - Returns: - 材质属性字典 - """ - database = {} - for material_type, material in self.materials.items(): - database[material_type] = material.get_material_properties_summary() - return database - - def find_similar_materials(self, reference_material: ClothMaterial, - max_results: int = 5) -> List[Tuple[str, float]]: - """ - 查找相似材质 - - Args: - reference_material: 参考材质 - max_results: 最大结果数量 - - Returns: - 相似材质列表 (材质类型, 相似度) - """ - reference_props = reference_material.get_material_properties_summary() - similarities = [] - - for material_type, material in self.materials.items(): - if material_type == reference_material.material_type: - continue # 跳过自身 - - material_props = material.get_material_properties_summary() - - # 计算相似度 (简化版本) - similarity = 0.0 - total_weight = 0.0 - - # 物理属性相似度 - ref_phys = reference_props['physical_properties'] - mat_phys = material_props['physical_properties'] - - for key in ref_phys.keys(): - if key in mat_phys: - ref_val = ref_phys[key] - mat_val = mat_phys[key] - # 归一化差异 (假设值在合理范围内) - diff = abs(ref_val - mat_val) / max(1.0, ref_val + mat_val) - similarity += (1.0 - diff) * 0.4 - total_weight += 0.4 - - # 视觉属性相似度 - ref_vis = reference_props['visual_properties'] - mat_vis = material_props['visual_properties'] - - # 颜色相似度 - if 'color' in ref_vis and 'color' in mat_vis: - ref_color = ref_vis['color'] - mat_color = mat_vis['color'] - color_diff = ( - (ref_color[0] - mat_color[0]) ** 2 + - (ref_color[1] - mat_color[1]) ** 2 + - (ref_color[2] - mat_color[2]) ** 2 - ) ** 0.5 - similarity += (1.0 - color_diff) * 0.3 - total_weight += 0.3 - - # 粗糙度相似度 - if 'roughness' in ref_vis and 'roughness' in mat_vis: - roughness_diff = abs(ref_vis['roughness'] - mat_vis['roughness']) - similarity += (1.0 - roughness_diff) * 0.2 - total_weight += 0.2 - - # 金属度相似度 - if 'metallic' in ref_vis and 'metallic' in mat_vis: - metallic_diff = abs(ref_vis['metallic'] - mat_vis['metallic']) - similarity += (1.0 - metallic_diff) * 0.1 - total_weight += 0.1 - - if total_weight > 0: - normalized_similarity = similarity / total_weight - similarities.append((material_type, normalized_similarity)) - - # 按相似度排序 - similarities.sort(key=lambda x: x[1], reverse=True) +""" +布料材质系统 +提供布料材质的定义和管理 +包含完整的物理和视觉属性定义 +""" + +from panda3d.core import Vec4, Vec3 +from typing import Dict, Any, List, Optional +import math + + +class ClothMaterial: + """ + 布料材质类 + 定义布料的物理和视觉属性,包含完整的材料科学参数 + """ + + # 预定义的布料材质类型 + TYPE_COTTON = "cotton" + TYPE_SILK = "silk" + TYPE_LEATHER = "leather" + TYPE_RUBBER = "rubber" + TYPE_DENIM = "denim" + TYPE_LINEN = "linen" + TYPE_WOOL = "wool" + TYPE_SYNTHETIC = "synthetic" + TYPE_MESH = "mesh" + TYPE_CANVAS = "canvas" + TYPE_NYLON = "nylon" + TYPE_POLYESTER = "polyester" + TYPE_SATIN = "satin" + TYPE_TERRY = "terry" + TYPE_FLEECE = "fleece" + TYPE_NEOPRENE = "neoprene" + TYPE_SPANDEX = "spandex" + TYPE_TWEED = "tweed" + TYPE_TULLE = "tulle" + TYPE_VELVET = "velvet" + TYPE_SUEDE = "suede" + + # 材质行为特性 + BEHAVIOR_DEFAULT = "default" + BEHAVIOR_STIFF = "stiff" + BEHAVIOR_FLEXIBLE = "flexible" + BEHAVIOR_ELASTIC = "elastic" + BEHAVIOR_HEAVY = "heavy" + BEHAVIOR_LIGHT = "light" + + def __init__(self, name: str, material_type: str = TYPE_COTTON, + physical_params: Dict[str, float] = None, + visual_params: Dict[str, Any] = None, + advanced_params: Dict[str, Any] = None): + """ + 初始化布料材质 + + Args: + name: 材质名称 + material_type: 材质类型 + physical_params: 物理参数字典 + visual_params: 视觉参数字典 + advanced_params: 高级参数字典 + """ + self.name = name + self.material_type = material_type + self.behavior_mode = self.BEHAVIOR_DEFAULT + + # 默认物理参数(基础物理属性) + self.default_physical_params = { + 'density': 0.6, # 密度 (kg/m³) + 'linear_stiffness': 0.7, # 线性刚度 + 'angular_stiffness': 0.6, # 角度刚度 + 'volume_stiffness': 0.4, # 体积保持刚度 + 'friction': 0.6, # 摩擦系数 + 'restitution': 0.1, # 恢复系数 + 'pressure_coefficient': 0.0, # 压力系数 + 'drag_coefficient': 0.02, # 阻力系数 + 'lift_coefficient': 0.01, # 升力系数 + 'bend_stiffness': 0.5, # 弯曲刚度 + 'damping': 0.01, # 阻尼系数 + 'plastic_threshold': 0.1, # 塑性阈值 + 'plastic_stiffness': 0.5, # 塑性刚度 + 'tear_threshold': 100.0, # 撕裂阈值 + 'tear_resistance': 50.0, # 撕裂阻力 + 'anisotropic_friction': [0.6, 0.6, 0.6], # 各向异性摩擦 + 'dynamic_friction': 0.6, # 动态摩擦系数 + 'static_friction': 0.7, # 静态摩擦系数 + 'rolling_friction': 0.1, # 滚动摩擦系数 + 'spinning_friction': 0.05, # 自旋摩擦系数 + 'viscosity': 0.01, # 粘度 + 'compressibility': 0.1, # 可压缩性 + 'shear_modulus': 0.7, # 剪切模量 + 'bulk_modulus': 1.2, # 体积模量 + 'youngs_modulus': 1.0, # 杨氏模量 + 'poisson_ratio': 0.4 # 泊松比 + } + + # 默认视觉参数 + self.default_visual_params = { + 'color': Vec4(0.8, 0.8, 0.8, 1.0), # 颜色 + 'roughness': 0.7, # 粗糙度 + 'metallic': 0.0, # 金属度 + 'specular': 0.2, # 镜面反射 + 'texture_scale': 1.0, # 纹理缩放 + 'normal_strength': 1.0, # 法线强度 + 'emissive': Vec4(0, 0, 0, 0), # 自发光 + 'opacity': 1.0, # 不透明度 + 'transmission': 0.0, # 透射 + 'anisotropy': 0.0, # 各向异性 + 'clearcoat': 0.0, # 清漆层 + 'clearcoat_roughness': 0.0, # 清漆粗糙度 + 'sheen': 0.0, # 光泽 + 'sheen_roughness': 0.0, # 光泽粗糙度 + 'subsurface': 0.0, # 次表面散射 + 'subsurface_color': Vec4(1, 1, 1, 1), # 次表面颜色 + 'thickness': 1.0 # 厚度 + } + + # 默认高级参数 + self.default_advanced_params = { + 'temperature_sensitivity': 0.0, # 温度敏感性 + 'humidity_sensitivity': 0.0, # 湿度敏感性 + 'aging_factor': 0.0, # 老化因子 + 'stretch_recovery': 0.8, # 拉伸恢复 + 'compression_recovery': 0.8, # 压缩恢复 + 'fatigue_resistance': 0.9, # 疲劳抗性 + 'abrasion_resistance': 0.8, # 耐磨性 + 'chemical_resistance': 0.7, # 化学抗性 + 'uv_resistance': 0.6, # 紫外线抗性 + 'flame_resistance': 0.5, # 阻燃性 + 'water_absorption': 0.1, # 吸水性 + 'air_permeability': 0.5, # 透气性 + 'thermal_conductivity': 0.1, # 热导率 + 'electrical_conductivity': 0.0, # 电导率 + 'magnetic_permeability': 1.0, # 磁导率 + 'radiation_absorption': 0.1, # 辐射吸收 + 'biodegradability': 0.3, # 生物降解性 + 'recyclability': 0.8 # 可回收性 + } + + # 合并参数 + self.physical_params = self.default_physical_params.copy() + if physical_params: + self.physical_params.update(physical_params) + + self.visual_params = self.default_visual_params.copy() + if visual_params: + self.visual_params.update(visual_params) + + self.advanced_params = self.default_advanced_params.copy() + if advanced_params: + self.advanced_params.update(advanced_params) + + # 材质状态 + self.temperature = 20.0 # 摄氏度 + self.humidity = 50.0 # 相对湿度百分比 + self.age = 0.0 # 使用年限 + self.wear_level = 0.0 # 磨损程度 (0-1) + + def get_physical_parameters(self) -> Dict[str, float]: + """ + 获取物理参数(考虑环境和老化影响) + + Returns: + 物理参数字典 + """ + # 应用环境和老化效应 + adjusted_params = self.physical_params.copy() + + # 温度影响 + temp_sensitivity = self.advanced_params.get('temperature_sensitivity', 0.0) + temp_factor = 1.0 + (self.temperature - 20.0) * temp_sensitivity * 0.01 + adjusted_params['linear_stiffness'] *= temp_factor + adjusted_params['angular_stiffness'] *= temp_factor + + # 湿度影响 + humidity_sensitivity = self.advanced_params.get('humidity_sensitivity', 0.0) + humidity_factor = 1.0 + (self.humidity - 50.0) * humidity_sensitivity * 0.01 + adjusted_params['density'] *= humidity_factor + + # 老化影响 + aging_factor = self.advanced_params.get('aging_factor', 0.0) + aging_multiplier = 1.0 - self.age * aging_factor * 0.01 + adjusted_params['linear_stiffness'] *= aging_multiplier + adjusted_params['restitution'] *= aging_multiplier + + # 磨损影响 + adjusted_params['friction'] *= (1.0 - self.wear_level * 0.2) + adjusted_params['tear_resistance'] *= (1.0 - self.wear_level * 0.5) + + return adjusted_params + + def get_visual_parameters(self) -> Dict[str, Any]: + """ + 获取视觉参数(考虑磨损和老化影响) + + Returns: + 视觉参数字典 + """ + adjusted_params = self.visual_params.copy() + + # 磨损影响颜色和粗糙度 + if self.wear_level > 0: + # 颜色变暗 + color = adjusted_params['color'] + darkening = 1.0 - self.wear_level * 0.2 + adjusted_params['color'] = Vec4( + color[0] * darkening, + color[1] * darkening, + color[2] * darkening, + color[3] + ) + + # 增加粗糙度 + adjusted_params['roughness'] = min(1.0, + adjusted_params['roughness'] + self.wear_level * 0.3) + + # 老化影响 + aging_darkening = self.age * 0.05 + if aging_darkening > 0: + color = adjusted_params['color'] + adjusted_params['color'] = Vec4( + max(0.0, color[0] - aging_darkening), + max(0.0, color[1] - aging_darkening), + max(0.0, color[2] - aging_darkening), + color[3] + ) + + return adjusted_params + + def get_advanced_parameters(self) -> Dict[str, Any]: + """ + 获取高级参数 + + Returns: + 高级参数字典 + """ + return self.advanced_params.copy() + + def update_physical_parameter(self, param_name: str, value: float) -> bool: + """ + 更新物理参数 + + Args: + param_name: 参数名称 + value: 参数值 + + Returns: + 是否更新成功 + """ + if param_name in self.physical_params: + self.physical_params[param_name] = value + return True + return False + + def update_visual_parameter(self, param_name: str, value) -> bool: + """ + 更新视觉参数 + + Args: + param_name: 参数名称 + value: 参数值 + + Returns: + 是否更新成功 + """ + if param_name in self.visual_params: + self.visual_params[param_name] = value + return True + return False + + def update_advanced_parameter(self, param_name: str, value) -> bool: + """ + 更新高级参数 + + Args: + param_name: 参数名称 + value: 参数值 + + Returns: + 是否更新成功 + """ + if param_name in self.advanced_params: + self.advanced_params[param_name] = value + return True + return False + + def set_environment_conditions(self, temperature: float = None, + humidity: float = None): + """ + 设置环境条件 + + Args: + temperature: 温度 (摄氏度) + humidity: 湿度 (相对湿度百分比) + """ + if temperature is not None: + self.temperature = temperature + if humidity is not None: + self.humidity = max(0.0, min(100.0, humidity)) + + def simulate_aging(self, years: float): + """ + 模拟材料老化 + + Args: + years: 老化年数 + """ + self.age += years + self.age = max(0.0, self.age) + + def simulate_wear(self, wear_amount: float): + """ + 模拟材料磨损 + + Args: + wear_amount: 磨损量 (0-1) + """ + self.wear_level += wear_amount + self.wear_level = max(0.0, min(1.0, self.wear_level)) + + def set_behavior_mode(self, behavior_mode: str): + """ + 设置行为模式 + + Args: + behavior_mode: 行为模式 + """ + self.behavior_mode = behavior_mode + + def get_material_properties_summary(self) -> Dict[str, Any]: + """ + 获取材料属性摘要 + + Returns: + 属性摘要字典 + """ + return { + 'name': self.name, + 'type': self.material_type, + 'behavior_mode': self.behavior_mode, + 'environment': { + 'temperature': self.temperature, + 'humidity': self.humidity + }, + 'condition': { + 'age': self.age, + 'wear_level': self.wear_level + }, + 'physical_properties': { + 'density': self.physical_params['density'], + 'stiffness': self.physical_params['linear_stiffness'], + 'elasticity': self.physical_params['youngs_modulus'], + 'friction': self.physical_params['friction'], + 'tear_resistance': self.physical_params['tear_resistance'] + }, + 'visual_properties': { + 'color': self.visual_params['color'], + 'roughness': self.visual_params['roughness'], + 'metallic': self.visual_params['metallic'] + }, + 'advanced_properties': { + 'durability': self.advanced_params['fatigue_resistance'], + 'chemical_resistance': self.advanced_params['chemical_resistance'], + 'uv_resistance': self.advanced_params['uv_resistance'] + } + } + + +class ClothMaterialManager: + """ + 布料材质管理器 + 管理和提供预定义的布料材质,支持材质混合和自定义 + """ + + def __init__(self): + self.materials: Dict[str, ClothMaterial] = {} + self.material_combinations: Dict[str, Dict[str, Any]] = {} + self._init_default_materials() + + def _init_default_materials(self): + """ + 初始化默认布料材质 + """ + # 棉布 - 天然纤维,透气性好 + cotton = ClothMaterial( + "Cotton", + ClothMaterial.TYPE_COTTON, + { + 'density': 0.6, + 'linear_stiffness': 0.7, + 'angular_stiffness': 0.6, + 'volume_stiffness': 0.4, + 'friction': 0.6, + 'restitution': 0.1, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.02, + 'lift_coefficient': 0.01, + 'bend_stiffness': 0.5, + 'damping': 0.01, + 'plastic_threshold': 0.1, + 'plastic_stiffness': 0.5, + 'tear_threshold': 100.0, + 'tear_resistance': 50.0, + 'anisotropic_friction': [0.6, 0.6, 0.6], + 'dynamic_friction': 0.6, + 'static_friction': 0.7, + 'rolling_friction': 0.1, + 'spinning_friction': 0.05, + 'viscosity': 0.01, + 'compressibility': 0.1, + 'shear_modulus': 0.7, + 'bulk_modulus': 1.2, + 'youngs_modulus': 1.0, + 'poisson_ratio': 0.4 + }, + { + 'color': Vec4(0.9, 0.9, 0.85, 1.0), + 'roughness': 0.8, + 'metallic': 0.0, + 'specular': 0.1, + 'texture_scale': 1.0, + 'normal_strength': 0.8 + }, + { + 'temperature_sensitivity': 0.1, + 'humidity_sensitivity': 0.3, + 'aging_factor': 0.2, + 'stretch_recovery': 0.8, + 'compression_recovery': 0.8, + 'fatigue_resistance': 0.7, + 'abrasion_resistance': 0.6, + 'chemical_resistance': 0.5, + 'uv_resistance': 0.4, + 'flame_resistance': 0.6, + 'water_absorption': 0.8, + 'air_permeability': 0.9, + 'thermal_conductivity': 0.1, + 'biodegradability': 0.9, + 'recyclability': 0.8 + } + ) + + # 丝绸 - 光滑、柔软、有光泽 + silk = ClothMaterial( + "Silk", + ClothMaterial.TYPE_SILK, + { + 'density': 0.3, + 'linear_stiffness': 0.5, + 'angular_stiffness': 0.4, + 'volume_stiffness': 0.2, + 'friction': 0.2, + 'restitution': 0.3, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.05, + 'lift_coefficient': 0.03, + 'bend_stiffness': 0.3, + 'damping': 0.005, + 'plastic_threshold': 0.05, + 'plastic_stiffness': 0.3, + 'tear_threshold': 80.0, + 'tear_resistance': 30.0, + 'anisotropic_friction': [0.2, 0.2, 0.2], + 'dynamic_friction': 0.2, + 'static_friction': 0.3, + 'rolling_friction': 0.05, + 'spinning_friction': 0.02, + 'viscosity': 0.005, + 'compressibility': 0.05, + 'shear_modulus': 0.4, + 'bulk_modulus': 0.9, + 'youngs_modulus': 0.6, + 'poisson_ratio': 0.35 + }, + { + 'color': Vec4(0.95, 0.95, 0.9, 1.0), + 'roughness': 0.3, + 'metallic': 0.1, + 'specular': 0.4, + 'texture_scale': 0.5, + 'normal_strength': 0.5, + 'sheen': 0.8, + 'sheen_roughness': 0.2 + }, + { + 'temperature_sensitivity': 0.05, + 'humidity_sensitivity': 0.2, + 'aging_factor': 0.1, + 'stretch_recovery': 0.9, + 'compression_recovery': 0.9, + 'fatigue_resistance': 0.6, + 'abrasion_resistance': 0.4, + 'chemical_resistance': 0.3, + 'uv_resistance': 0.2, + 'flame_resistance': 0.7, + 'water_absorption': 0.6, + 'air_permeability': 0.7, + 'thermal_conductivity': 0.05, + 'biodegradability': 0.8, + 'recyclability': 0.7 + } + ) + + # 皮革 - 坚韧、耐用 + leather = ClothMaterial( + "Leather", + ClothMaterial.TYPE_LEATHER, + { + 'density': 0.8, + 'linear_stiffness': 0.9, + 'angular_stiffness': 0.8, + 'volume_stiffness': 0.6, + 'friction': 0.7, + 'restitution': 0.1, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.01, + 'lift_coefficient': 0.005, + 'bend_stiffness': 0.8, + 'damping': 0.015, + 'plastic_threshold': 0.2, + 'plastic_stiffness': 0.7, + 'tear_threshold': 150.0, + 'tear_resistance': 80.0, + 'anisotropic_friction': [0.7, 0.7, 0.7], + 'dynamic_friction': 0.7, + 'static_friction': 0.8, + 'rolling_friction': 0.15, + 'spinning_friction': 0.08, + 'viscosity': 0.015, + 'compressibility': 0.15, + 'shear_modulus': 0.8, + 'bulk_modulus': 1.1, + 'youngs_modulus': 0.85, + 'poisson_ratio': 0.42 + }, + { + 'color': Vec4(0.8, 0.6, 0.4, 1.0), + 'roughness': 0.6, + 'metallic': 0.0, + 'specular': 0.2, + 'texture_scale': 2.0, + 'normal_strength': 1.2 + }, + { + 'temperature_sensitivity': 0.02, + 'humidity_sensitivity': 0.1, + 'aging_factor': 0.05, + 'stretch_recovery': 0.7, + 'compression_recovery': 0.7, + 'fatigue_resistance': 0.9, + 'abrasion_resistance': 0.9, + 'chemical_resistance': 0.8, + 'uv_resistance': 0.7, + 'flame_resistance': 0.8, + 'water_absorption': 0.3, + 'air_permeability': 0.2, + 'thermal_conductivity': 0.15, + 'biodegradability': 0.4, + 'recyclability': 0.6 + } + ) + + # 橡胶 - 弹性、防水 + rubber = ClothMaterial( + "Rubber", + ClothMaterial.TYPE_RUBBER, + { + 'density': 1.2, + 'linear_stiffness': 0.95, + 'angular_stiffness': 0.9, + 'volume_stiffness': 0.8, + 'friction': 0.8, + 'restitution': 0.4, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.005, + 'lift_coefficient': 0.002, + 'bend_stiffness': 0.9, + 'damping': 0.02, + 'plastic_threshold': 0.3, + 'plastic_stiffness': 0.8, + 'tear_threshold': 200.0, + 'tear_resistance': 100.0, + 'anisotropic_friction': [0.8, 0.8, 0.8], + 'dynamic_friction': 0.8, + 'static_friction': 0.9, + 'rolling_friction': 0.2, + 'spinning_friction': 0.1, + 'viscosity': 0.02, + 'compressibility': 0.2, + 'shear_modulus': 0.9, + 'bulk_modulus': 1.5, + 'youngs_modulus': 1.2, + 'poisson_ratio': 0.45 + }, + { + 'color': Vec4(0.3, 0.3, 0.3, 1.0), + 'roughness': 0.9, + 'metallic': 0.0, + 'specular': 0.1, + 'texture_scale': 0.3, + 'normal_strength': 0.3 + }, + { + 'temperature_sensitivity': 0.15, + 'humidity_sensitivity': 0.05, + 'aging_factor': 0.3, + 'stretch_recovery': 0.95, + 'compression_recovery': 0.95, + 'fatigue_resistance': 0.8, + 'abrasion_resistance': 0.7, + 'chemical_resistance': 0.9, + 'uv_resistance': 0.6, + 'flame_resistance': 0.9, + 'water_absorption': 0.1, + 'air_permeability': 0.0, + 'thermal_conductivity': 0.2, + 'biodegradability': 0.2, + 'recyclability': 0.5 + } + ) + + # 牛仔布 - 坚韧、耐磨 + denim = ClothMaterial( + "Denim", + ClothMaterial.TYPE_DENIM, + { + 'density': 0.7, + 'linear_stiffness': 0.8, + 'angular_stiffness': 0.7, + 'volume_stiffness': 0.5, + 'friction': 0.65, + 'restitution': 0.15, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.015, + 'lift_coefficient': 0.008, + 'bend_stiffness': 0.6, + 'damping': 0.012, + 'plastic_threshold': 0.15, + 'plastic_stiffness': 0.6, + 'tear_threshold': 120.0, + 'tear_resistance': 60.0, + 'anisotropic_friction': [0.65, 0.65, 0.65], + 'dynamic_friction': 0.65, + 'static_friction': 0.75, + 'rolling_friction': 0.12, + 'spinning_friction': 0.06, + 'viscosity': 0.012, + 'compressibility': 0.12, + 'shear_modulus': 0.65, + 'bulk_modulus': 1.0, + 'youngs_modulus': 0.75, + 'poisson_ratio': 0.38 + }, + { + 'color': Vec4(0.4, 0.5, 0.7, 1.0), + 'roughness': 0.7, + 'metallic': 0.0, + 'specular': 0.15, + 'texture_scale': 1.5, + 'normal_strength': 1.0 + }, + { + 'temperature_sensitivity': 0.08, + 'humidity_sensitivity': 0.15, + 'aging_factor': 0.1, + 'stretch_recovery': 0.75, + 'compression_recovery': 0.75, + 'fatigue_resistance': 0.8, + 'abrasion_resistance': 0.8, + 'chemical_resistance': 0.6, + 'uv_resistance': 0.5, + 'flame_resistance': 0.7, + 'water_absorption': 0.4, + 'air_permeability': 0.6, + 'thermal_conductivity': 0.12, + 'biodegradability': 0.6, + 'recyclability': 0.7 + } + ) + + # 亚麻 - 天然、透气 + linen = ClothMaterial( + "Linen", + ClothMaterial.TYPE_LINEN, + { + 'density': 0.5, + 'linear_stiffness': 0.6, + 'angular_stiffness': 0.5, + 'volume_stiffness': 0.3, + 'friction': 0.55, + 'restitution': 0.12, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.025, + 'lift_coefficient': 0.012, + 'bend_stiffness': 0.4, + 'damping': 0.008, + 'plastic_threshold': 0.12, + 'plastic_stiffness': 0.4, + 'tear_threshold': 90.0, + 'tear_resistance': 40.0, + 'anisotropic_friction': [0.55, 0.55, 0.55], + 'dynamic_friction': 0.55, + 'static_friction': 0.65, + 'rolling_friction': 0.08, + 'spinning_friction': 0.04, + 'viscosity': 0.008, + 'compressibility': 0.08, + 'shear_modulus': 0.5, + 'bulk_modulus': 0.8, + 'youngs_modulus': 0.6, + 'poisson_ratio': 0.35 + }, + { + 'color': Vec4(0.9, 0.85, 0.75, 1.0), + 'roughness': 0.85, + 'metallic': 0.0, + 'specular': 0.12, + 'texture_scale': 1.2, + 'normal_strength': 0.9 + }, + { + 'temperature_sensitivity': 0.12, + 'humidity_sensitivity': 0.25, + 'aging_factor': 0.15, + 'stretch_recovery': 0.7, + 'compression_recovery': 0.7, + 'fatigue_resistance': 0.6, + 'abrasion_resistance': 0.5, + 'chemical_resistance': 0.4, + 'uv_resistance': 0.3, + 'flame_resistance': 0.8, + 'water_absorption': 0.7, + 'air_permeability': 0.95, + 'thermal_conductivity': 0.08, + 'biodegradability': 0.95, + 'recyclability': 0.85 + } + ) + + # 羊毛 - 保暖、柔软 + wool = ClothMaterial( + "Wool", + ClothMaterial.TYPE_WOOL, + { + 'density': 0.65, + 'linear_stiffness': 0.65, + 'angular_stiffness': 0.55, + 'volume_stiffness': 0.35, + 'friction': 0.6, + 'restitution': 0.13, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.018, + 'lift_coefficient': 0.009, + 'bend_stiffness': 0.45, + 'damping': 0.01, + 'plastic_threshold': 0.13, + 'plastic_stiffness': 0.45, + 'tear_threshold': 100.0, + 'tear_resistance': 50.0, + 'anisotropic_friction': [0.6, 0.6, 0.6], + 'dynamic_friction': 0.6, + 'static_friction': 0.7, + 'rolling_friction': 0.1, + 'spinning_friction': 0.05, + 'viscosity': 0.01, + 'compressibility': 0.18, + 'shear_modulus': 0.55, + 'bulk_modulus': 0.9, + 'youngs_modulus': 0.65, + 'poisson_ratio': 0.37 + }, + { + 'color': Vec4(0.85, 0.8, 0.7, 1.0), + 'roughness': 0.75, + 'metallic': 0.0, + 'specular': 0.13, + 'texture_scale': 1.8, + 'normal_strength': 1.1, + 'subsurface': 0.3, + 'subsurface_color': Vec4(0.9, 0.8, 0.7, 1.0) + }, + { + 'temperature_sensitivity': 0.05, + 'humidity_sensitivity': 0.4, + 'aging_factor': 0.12, + 'stretch_recovery': 0.8, + 'compression_recovery': 0.8, + 'fatigue_resistance': 0.7, + 'abrasion_resistance': 0.6, + 'chemical_resistance': 0.5, + 'uv_resistance': 0.4, + 'flame_resistance': 0.9, + 'water_absorption': 0.6, + 'air_permeability': 0.7, + 'thermal_conductivity': 0.05, + 'biodegradability': 0.85, + 'recyclability': 0.75 + } + ) + + # 合成纤维 - 多功能性 + synthetic = ClothMaterial( + "Synthetic", + ClothMaterial.TYPE_SYNTHETIC, + { + 'density': 0.4, + 'linear_stiffness': 0.75, + 'angular_stiffness': 0.65, + 'volume_stiffness': 0.45, + 'friction': 0.5, + 'restitution': 0.2, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.012, + 'lift_coefficient': 0.007, + 'bend_stiffness': 0.55, + 'damping': 0.006, + 'plastic_threshold': 0.18, + 'plastic_stiffness': 0.55, + 'tear_threshold': 110.0, + 'tear_resistance': 55.0, + 'anisotropic_friction': [0.5, 0.5, 0.5], + 'dynamic_friction': 0.5, + 'static_friction': 0.6, + 'rolling_friction': 0.07, + 'spinning_friction': 0.03, + 'viscosity': 0.006, + 'compressibility': 0.1, + 'shear_modulus': 0.55, + 'bulk_modulus': 0.95, + 'youngs_modulus': 0.7, + 'poisson_ratio': 0.39 + }, + { + 'color': Vec4(0.7, 0.7, 0.7, 1.0), + 'roughness': 0.5, + 'metallic': 0.05, + 'specular': 0.25, + 'texture_scale': 1.0, + 'normal_strength': 0.7 + }, + { + 'temperature_sensitivity': 0.08, + 'humidity_sensitivity': 0.1, + 'aging_factor': 0.08, + 'stretch_recovery': 0.85, + 'compression_recovery': 0.85, + 'fatigue_resistance': 0.85, + 'abrasion_resistance': 0.75, + 'chemical_resistance': 0.85, + 'uv_resistance': 0.75, + 'flame_resistance': 0.6, + 'water_absorption': 0.2, + 'air_permeability': 0.6, + 'thermal_conductivity': 0.15, + 'biodegradability': 0.3, + 'recyclability': 0.9 + } + ) + + # 网眼布 - 透气、轻质 + mesh = ClothMaterial( + "Mesh", + ClothMaterial.TYPE_MESH, + { + 'density': 0.2, + 'linear_stiffness': 0.4, + 'angular_stiffness': 0.3, + 'volume_stiffness': 0.1, + 'friction': 0.3, + 'restitution': 0.05, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.04, + 'lift_coefficient': 0.02, + 'bend_stiffness': 0.2, + 'damping': 0.003, + 'plastic_threshold': 0.08, + 'plastic_stiffness': 0.2, + 'tear_threshold': 70.0, + 'tear_resistance': 25.0, + 'anisotropic_friction': [0.3, 0.3, 0.3], + 'dynamic_friction': 0.3, + 'static_friction': 0.4, + 'rolling_friction': 0.03, + 'spinning_friction': 0.01, + 'viscosity': 0.003, + 'compressibility': 0.05, + 'shear_modulus': 0.2, + 'bulk_modulus': 0.5, + 'youngs_modulus': 0.3, + 'poisson_ratio': 0.3 + }, + { + 'color': Vec4(0.6, 0.6, 0.6, 0.7), + 'roughness': 0.6, + 'metallic': 0.1, + 'specular': 0.2, + 'texture_scale': 3.0, + 'normal_strength': 0.4, + 'opacity': 0.7 + }, + { + 'temperature_sensitivity': 0.03, + 'humidity_sensitivity': 0.05, + 'aging_factor': 0.03, + 'stretch_recovery': 0.9, + 'compression_recovery': 0.9, + 'fatigue_resistance': 0.5, + 'abrasion_resistance': 0.3, + 'chemical_resistance': 0.7, + 'uv_resistance': 0.6, + 'flame_resistance': 0.8, + 'water_absorption': 0.1, + 'air_permeability': 1.0, + 'thermal_conductivity': 0.2, + 'biodegradability': 0.4, + 'recyclability': 0.95 + } + ) + + # 帆布 - 坚固、耐用 + canvas = ClothMaterial( + "Canvas", + ClothMaterial.TYPE_CANVAS, + { + 'density': 0.75, + 'linear_stiffness': 0.85, + 'angular_stiffness': 0.75, + 'volume_stiffness': 0.55, + 'friction': 0.68, + 'restitution': 0.14, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.013, + 'lift_coefficient': 0.006, + 'bend_stiffness': 0.65, + 'damping': 0.014, + 'plastic_threshold': 0.18, + 'plastic_stiffness': 0.65, + 'tear_threshold': 130.0, + 'tear_resistance': 65.0, + 'anisotropic_friction': [0.68, 0.68, 0.68], + 'dynamic_friction': 0.68, + 'static_friction': 0.78, + 'rolling_friction': 0.14, + 'spinning_friction': 0.07, + 'viscosity': 0.014, + 'compressibility': 0.14, + 'shear_modulus': 0.65, + 'bulk_modulus': 1.05, + 'youngs_modulus': 0.8, + 'poisson_ratio': 0.4 + }, + { + 'color': Vec4(0.8, 0.75, 0.65, 1.0), + 'roughness': 0.78, + 'metallic': 0.0, + 'specular': 0.14, + 'texture_scale': 2.5, + 'normal_strength': 1.3 + }, + { + 'temperature_sensitivity': 0.06, + 'humidity_sensitivity': 0.2, + 'aging_factor': 0.08, + 'stretch_recovery': 0.78, + 'compression_recovery': 0.78, + 'fatigue_resistance': 0.85, + 'abrasion_resistance': 0.85, + 'chemical_resistance': 0.7, + 'uv_resistance': 0.6, + 'flame_resistance': 0.75, + 'water_absorption': 0.35, + 'air_permeability': 0.5, + 'thermal_conductivity': 0.13, + 'biodegradability': 0.5, + 'recyclability': 0.7 + } + ) + + # 尼龙 - 强韧、轻质 + nylon = ClothMaterial( + "Nylon", + ClothMaterial.TYPE_NYLON, + { + 'density': 0.35, + 'linear_stiffness': 0.8, + 'angular_stiffness': 0.7, + 'volume_stiffness': 0.5, + 'friction': 0.4, + 'restitution': 0.25, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.01, + 'lift_coefficient': 0.005, + 'bend_stiffness': 0.6, + 'damping': 0.007, + 'plastic_threshold': 0.15, + 'plastic_stiffness': 0.6, + 'tear_threshold': 140.0, + 'tear_resistance': 70.0, + 'anisotropic_friction': [0.4, 0.4, 0.4], + 'dynamic_friction': 0.4, + 'static_friction': 0.5, + 'rolling_friction': 0.08, + 'spinning_friction': 0.04, + 'viscosity': 0.007, + 'compressibility': 0.08, + 'shear_modulus': 0.6, + 'bulk_modulus': 1.0, + 'youngs_modulus': 0.85, + 'poisson_ratio': 0.4 + }, + { + 'color': Vec4(0.95, 0.95, 0.95, 1.0), + 'roughness': 0.4, + 'metallic': 0.0, + 'specular': 0.3, + 'texture_scale': 1.0, + 'normal_strength': 0.6 + }, + { + 'temperature_sensitivity': 0.1, + 'humidity_sensitivity': 0.15, + 'aging_factor': 0.1, + 'stretch_recovery': 0.92, + 'compression_recovery': 0.92, + 'fatigue_resistance': 0.88, + 'abrasion_resistance': 0.8, + 'chemical_resistance': 0.8, + 'uv_resistance': 0.5, + 'flame_resistance': 0.65, + 'water_absorption': 0.25, + 'air_permeability': 0.65, + 'thermal_conductivity': 0.18, + 'biodegradability': 0.25, + 'recyclability': 0.85 + } + ) + + # 聚酯纤维 - 耐用、抗皱 + polyester = ClothMaterial( + "Polyester", + ClothMaterial.TYPE_POLYESTER, + { + 'density': 0.45, + 'linear_stiffness': 0.7, + 'angular_stiffness': 0.6, + 'volume_stiffness': 0.4, + 'friction': 0.45, + 'restitution': 0.18, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.015, + 'lift_coefficient': 0.008, + 'bend_stiffness': 0.5, + 'damping': 0.009, + 'plastic_threshold': 0.12, + 'plastic_stiffness': 0.5, + 'tear_threshold': 110.0, + 'tear_resistance': 55.0, + 'anisotropic_friction': [0.45, 0.45, 0.45], + 'dynamic_friction': 0.45, + 'static_friction': 0.55, + 'rolling_friction': 0.09, + 'spinning_friction': 0.045, + 'viscosity': 0.009, + 'compressibility': 0.1, + 'shear_modulus': 0.55, + 'bulk_modulus': 0.95, + 'youngs_modulus': 0.75, + 'poisson_ratio': 0.38 + }, + { + 'color': Vec4(0.8, 0.8, 0.8, 1.0), + 'roughness': 0.55, + 'metallic': 0.0, + 'specular': 0.2, + 'texture_scale': 1.0, + 'normal_strength': 0.7 + }, + { + 'temperature_sensitivity': 0.07, + 'humidity_sensitivity': 0.08, + 'aging_factor': 0.06, + 'stretch_recovery': 0.88, + 'compression_recovery': 0.88, + 'fatigue_resistance': 0.82, + 'abrasion_resistance': 0.75, + 'chemical_resistance': 0.85, + 'uv_resistance': 0.65, + 'flame_resistance': 0.7, + 'water_absorption': 0.15, + 'air_permeability': 0.55, + 'thermal_conductivity': 0.14, + 'biodegradability': 0.15, + 'recyclability': 0.9 + } + ) + + # 缎子 - 光滑、有光泽 + satin = ClothMaterial( + "Satin", + ClothMaterial.TYPE_SATIN, + { + 'density': 0.25, + 'linear_stiffness': 0.45, + 'angular_stiffness': 0.35, + 'volume_stiffness': 0.25, + 'friction': 0.15, + 'restitution': 0.28, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.03, + 'lift_coefficient': 0.015, + 'bend_stiffness': 0.3, + 'damping': 0.004, + 'plastic_threshold': 0.08, + 'plastic_stiffness': 0.3, + 'tear_threshold': 75.0, + 'tear_resistance': 35.0, + 'anisotropic_friction': [0.15, 0.15, 0.15], + 'dynamic_friction': 0.15, + 'static_friction': 0.25, + 'rolling_friction': 0.04, + 'spinning_friction': 0.02, + 'viscosity': 0.004, + 'compressibility': 0.06, + 'shear_modulus': 0.35, + 'bulk_modulus': 0.7, + 'youngs_modulus': 0.5, + 'poisson_ratio': 0.33 + }, + { + 'color': Vec4(0.98, 0.98, 0.98, 1.0), + 'roughness': 0.1, + 'metallic': 0.2, + 'specular': 0.6, + 'texture_scale': 0.8, + 'normal_strength': 0.3, + 'sheen': 0.9, + 'sheen_roughness': 0.1 + }, + { + 'temperature_sensitivity': 0.04, + 'humidity_sensitivity': 0.12, + 'aging_factor': 0.08, + 'stretch_recovery': 0.85, + 'compression_recovery': 0.85, + 'fatigue_resistance': 0.65, + 'abrasion_resistance': 0.45, + 'chemical_resistance': 0.6, + 'uv_resistance': 0.3, + 'flame_resistance': 0.75, + 'water_absorption': 0.3, + 'air_permeability': 0.4, + 'thermal_conductivity': 0.07, + 'biodegradability': 0.5, + 'recyclability': 0.75 + } + ) + + # 毛圈织物 - 吸水、柔软 + terry = ClothMaterial( + "Terry", + ClothMaterial.TYPE_TERRY, + { + 'density': 0.55, + 'linear_stiffness': 0.55, + 'angular_stiffness': 0.45, + 'volume_stiffness': 0.3, + 'friction': 0.5, + 'restitution': 0.1, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.02, + 'lift_coefficient': 0.01, + 'bend_stiffness': 0.35, + 'damping': 0.012, + 'plastic_threshold': 0.1, + 'plastic_stiffness': 0.35, + 'tear_threshold': 95.0, + 'tear_resistance': 45.0, + 'anisotropic_friction': [0.5, 0.5, 0.5], + 'dynamic_friction': 0.5, + 'static_friction': 0.6, + 'rolling_friction': 0.1, + 'spinning_friction': 0.05, + 'viscosity': 0.012, + 'compressibility': 0.15, + 'shear_modulus': 0.45, + 'bulk_modulus': 0.85, + 'youngs_modulus': 0.6, + 'poisson_ratio': 0.36 + }, + { + 'color': Vec4(0.92, 0.92, 0.92, 1.0), + 'roughness': 0.8, + 'metallic': 0.0, + 'specular': 0.15, + 'texture_scale': 2.0, + 'normal_strength': 1.5, + 'subsurface': 0.2, + 'subsurface_color': Vec4(0.95, 0.95, 0.95, 1.0) + }, + { + 'temperature_sensitivity': 0.09, + 'humidity_sensitivity': 0.35, + 'aging_factor': 0.13, + 'stretch_recovery': 0.75, + 'compression_recovery': 0.75, + 'fatigue_resistance': 0.65, + 'abrasion_resistance': 0.55, + 'chemical_resistance': 0.5, + 'uv_resistance': 0.45, + 'flame_resistance': 0.8, + 'water_absorption': 0.9, + 'air_permeability': 0.75, + 'thermal_conductivity': 0.1, + 'biodegradability': 0.7, + 'recyclability': 0.8 + } + ) + + # 抓绒 - 保暖、柔软 + fleece = ClothMaterial( + "Fleece", + ClothMaterial.TYPE_FLEECE, + { + 'density': 0.3, + 'linear_stiffness': 0.4, + 'angular_stiffness': 0.3, + 'volume_stiffness': 0.2, + 'friction': 0.4, + 'restitution': 0.08, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.025, + 'lift_coefficient': 0.012, + 'bend_stiffness': 0.25, + 'damping': 0.008, + 'plastic_threshold': 0.07, + 'plastic_stiffness': 0.25, + 'tear_threshold': 80.0, + 'tear_resistance': 35.0, + 'anisotropic_friction': [0.4, 0.4, 0.4], + 'dynamic_friction': 0.4, + 'static_friction': 0.5, + 'rolling_friction': 0.08, + 'spinning_friction': 0.04, + 'viscosity': 0.008, + 'compressibility': 0.2, + 'shear_modulus': 0.3, + 'bulk_modulus': 0.6, + 'youngs_modulus': 0.4, + 'poisson_ratio': 0.32 + }, + { + 'color': Vec4(0.88, 0.88, 0.88, 1.0), + 'roughness': 0.85, + 'metallic': 0.0, + 'specular': 0.1, + 'texture_scale': 1.5, + 'normal_strength': 1.8, + 'subsurface': 0.4, + 'subsurface_color': Vec4(0.9, 0.9, 0.9, 1.0) + }, + { + 'temperature_sensitivity': 0.06, + 'humidity_sensitivity': 0.25, + 'aging_factor': 0.1, + 'stretch_recovery': 0.8, + 'compression_recovery': 0.8, + 'fatigue_resistance': 0.6, + 'abrasion_resistance': 0.5, + 'chemical_resistance': 0.6, + 'uv_resistance': 0.5, + 'flame_resistance': 0.85, + 'water_absorption': 0.5, + 'air_permeability': 0.65, + 'thermal_conductivity': 0.04, + 'biodegradability': 0.6, + 'recyclability': 0.85 + } + ) + + # 氯丁橡胶 - 防水、保温 + neoprene = ClothMaterial( + "Neoprene", + ClothMaterial.TYPE_NEOPRENE, + { + 'density': 0.9, + 'linear_stiffness': 0.85, + 'angular_stiffness': 0.75, + 'volume_stiffness': 0.65, + 'friction': 0.6, + 'restitution': 0.22, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.008, + 'lift_coefficient': 0.003, + 'bend_stiffness': 0.7, + 'damping': 0.018, + 'plastic_threshold': 0.25, + 'plastic_stiffness': 0.7, + 'tear_threshold': 160.0, + 'tear_resistance': 85.0, + 'anisotropic_friction': [0.6, 0.6, 0.6], + 'dynamic_friction': 0.6, + 'static_friction': 0.7, + 'rolling_friction': 0.16, + 'spinning_friction': 0.09, + 'viscosity': 0.018, + 'compressibility': 0.18, + 'shear_modulus': 0.75, + 'bulk_modulus': 1.2, + 'youngs_modulus': 0.9, + 'poisson_ratio': 0.43 + }, + { + 'color': Vec4(0.2, 0.2, 0.2, 1.0), + 'roughness': 0.7, + 'metallic': 0.0, + 'specular': 0.25, + 'texture_scale': 0.5, + 'normal_strength': 0.8 + }, + { + 'temperature_sensitivity': 0.12, + 'humidity_sensitivity': 0.03, + 'aging_factor': 0.15, + 'stretch_recovery': 0.9, + 'compression_recovery': 0.9, + 'fatigue_resistance': 0.85, + 'abrasion_resistance': 0.75, + 'chemical_resistance': 0.95, + 'uv_resistance': 0.7, + 'flame_resistance': 0.8, + 'water_absorption': 0.05, + 'air_permeability': 0.0, + 'thermal_conductivity': 0.12, + 'biodegradability': 0.1, + 'recyclability': 0.6 + } + ) + + # 氨纶 - 高弹性 + spandex = ClothMaterial( + "Spandex", + ClothMaterial.TYPE_SPANDEX, + { + 'density': 0.2, + 'linear_stiffness': 0.3, + 'angular_stiffness': 0.25, + 'volume_stiffness': 0.15, + 'friction': 0.35, + 'restitution': 0.35, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.018, + 'lift_coefficient': 0.009, + 'bend_stiffness': 0.2, + 'damping': 0.005, + 'plastic_threshold': 0.05, + 'plastic_stiffness': 0.2, + 'tear_threshold': 60.0, + 'tear_resistance': 20.0, + 'anisotropic_friction': [0.35, 0.35, 0.35], + 'dynamic_friction': 0.35, + 'static_friction': 0.45, + 'rolling_friction': 0.06, + 'spinning_friction': 0.03, + 'viscosity': 0.005, + 'compressibility': 0.07, + 'shear_modulus': 0.25, + 'bulk_modulus': 0.55, + 'youngs_modulus': 0.35, + 'poisson_ratio': 0.31 + }, + { + 'color': Vec4(0.95, 0.95, 0.95, 1.0), + 'roughness': 0.45, + 'metallic': 0.0, + 'specular': 0.35, + 'texture_scale': 0.8, + 'normal_strength': 0.4 + }, + { + 'temperature_sensitivity': 0.08, + 'humidity_sensitivity': 0.07, + 'aging_factor': 0.05, + 'stretch_recovery': 0.98, + 'compression_recovery': 0.98, + 'fatigue_resistance': 0.75, + 'abrasion_resistance': 0.4, + 'chemical_resistance': 0.7, + 'uv_resistance': 0.4, + 'flame_resistance': 0.6, + 'water_absorption': 0.1, + 'air_permeability': 0.5, + 'thermal_conductivity': 0.16, + 'biodegradability': 0.2, + 'recyclability': 0.7 + } + ) + + # 粗花呢 - 厚重、有纹理 + tweed = ClothMaterial( + "Tweed", + ClothMaterial.TYPE_TWEED, + { + 'density': 0.7, + 'linear_stiffness': 0.75, + 'angular_stiffness': 0.65, + 'volume_stiffness': 0.45, + 'friction': 0.62, + 'restitution': 0.12, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.014, + 'lift_coefficient': 0.007, + 'bend_stiffness': 0.55, + 'damping': 0.013, + 'plastic_threshold': 0.14, + 'plastic_stiffness': 0.55, + 'tear_threshold': 115.0, + 'tear_resistance': 58.0, + 'anisotropic_friction': [0.62, 0.62, 0.62], + 'dynamic_friction': 0.62, + 'static_friction': 0.72, + 'rolling_friction': 0.13, + 'spinning_friction': 0.065, + 'viscosity': 0.013, + 'compressibility': 0.13, + 'shear_modulus': 0.58, + 'bulk_modulus': 0.98, + 'youngs_modulus': 0.72, + 'poisson_ratio': 0.39 + }, + { + 'color': Vec4(0.75, 0.7, 0.6, 1.0), + 'roughness': 0.72, + 'metallic': 0.0, + 'specular': 0.16, + 'texture_scale': 3.0, + 'normal_strength': 1.4 + }, + { + 'temperature_sensitivity': 0.07, + 'humidity_sensitivity': 0.18, + 'aging_factor': 0.09, + 'stretch_recovery': 0.72, + 'compression_recovery': 0.72, + 'fatigue_resistance': 0.78, + 'abrasion_resistance': 0.72, + 'chemical_resistance': 0.65, + 'uv_resistance': 0.55, + 'flame_resistance': 0.72, + 'water_absorption': 0.32, + 'air_permeability': 0.58, + 'thermal_conductivity': 0.11, + 'biodegradability': 0.55, + 'recyclability': 0.72 + } + ) + + # 薄纱 - 轻盈、透明 + tulle = ClothMaterial( + "Tulle", + ClothMaterial.TYPE_TULLE, + { + 'density': 0.1, + 'linear_stiffness': 0.25, + 'angular_stiffness': 0.2, + 'volume_stiffness': 0.1, + 'friction': 0.2, + 'restitution': 0.15, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.035, + 'lift_coefficient': 0.018, + 'bend_stiffness': 0.15, + 'damping': 0.003, + 'plastic_threshold': 0.04, + 'plastic_stiffness': 0.15, + 'tear_threshold': 40.0, + 'tear_resistance': 15.0, + 'anisotropic_friction': [0.2, 0.2, 0.2], + 'dynamic_friction': 0.2, + 'static_friction': 0.3, + 'rolling_friction': 0.03, + 'spinning_friction': 0.015, + 'viscosity': 0.003, + 'compressibility': 0.03, + 'shear_modulus': 0.15, + 'bulk_modulus': 0.3, + 'youngs_modulus': 0.2, + 'poisson_ratio': 0.3 + }, + { + 'color': Vec4(1.0, 1.0, 1.0, 0.3), + 'roughness': 0.5, + 'metallic': 0.0, + 'specular': 0.4, + 'texture_scale': 4.0, + 'normal_strength': 0.2, + 'opacity': 0.3 + }, + { + 'temperature_sensitivity': 0.02, + 'humidity_sensitivity': 0.04, + 'aging_factor': 0.02, + 'stretch_recovery': 0.95, + 'compression_recovery': 0.95, + 'fatigue_resistance': 0.4, + 'abrasion_resistance': 0.2, + 'chemical_resistance': 0.5, + 'uv_resistance': 0.4, + 'flame_resistance': 0.9, + 'water_absorption': 0.05, + 'air_permeability': 1.0, + 'thermal_conductivity': 0.25, + 'biodegradability': 0.6, + 'recyclability': 0.98 + } + ) + + # 天鹅绒 - 柔软、有光泽 + velvet = ClothMaterial( + "Velvet", + ClothMaterial.TYPE_VELVET, + { + 'density': 0.6, + 'linear_stiffness': 0.5, + 'angular_stiffness': 0.4, + 'volume_stiffness': 0.3, + 'friction': 0.45, + 'restitution': 0.1, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.022, + 'lift_coefficient': 0.011, + 'bend_stiffness': 0.35, + 'damping': 0.011, + 'plastic_threshold': 0.09, + 'plastic_stiffness': 0.35, + 'tear_threshold': 85.0, + 'tear_resistance': 40.0, + 'anisotropic_friction': [0.45, 0.45, 0.45], + 'dynamic_friction': 0.45, + 'static_friction': 0.55, + 'rolling_friction': 0.09, + 'spinning_friction': 0.045, + 'viscosity': 0.011, + 'compressibility': 0.11, + 'shear_modulus': 0.4, + 'bulk_modulus': 0.75, + 'youngs_modulus': 0.55, + 'poisson_ratio': 0.35 + }, + { + 'color': Vec4(0.85, 0.8, 0.75, 1.0), + 'roughness': 0.2, + 'metallic': 0.3, + 'specular': 0.5, + 'texture_scale': 1.2, + 'normal_strength': 2.0, + 'sheen': 0.7, + 'sheen_roughness': 0.15 + }, + { + 'temperature_sensitivity': 0.06, + 'humidity_sensitivity': 0.22, + 'aging_factor': 0.11, + 'stretch_recovery': 0.82, + 'compression_recovery': 0.82, + 'fatigue_resistance': 0.68, + 'abrasion_resistance': 0.52, + 'chemical_resistance': 0.58, + 'uv_resistance': 0.48, + 'flame_resistance': 0.78, + 'water_absorption': 0.42, + 'air_permeability': 0.62, + 'thermal_conductivity': 0.09, + 'biodegradability': 0.62, + 'recyclability': 0.78 + } + ) + + # 翻毛皮 - 柔软、有质感 + suede = ClothMaterial( + "Suede", + ClothMaterial.TYPE_SUEDE, + { + 'density': 0.75, + 'linear_stiffness': 0.65, + 'angular_stiffness': 0.55, + 'volume_stiffness': 0.4, + 'friction': 0.55, + 'restitution': 0.08, + 'pressure_coefficient': 0.0, + 'drag_coefficient': 0.016, + 'lift_coefficient': 0.008, + 'bend_stiffness': 0.45, + 'damping': 0.015, + 'plastic_threshold': 0.11, + 'plastic_stiffness': 0.45, + 'tear_threshold': 105.0, + 'tear_resistance': 52.0, + 'anisotropic_friction': [0.55, 0.55, 0.55], + 'dynamic_friction': 0.55, + 'static_friction': 0.65, + 'rolling_friction': 0.11, + 'spinning_friction': 0.055, + 'viscosity': 0.015, + 'compressibility': 0.12, + 'shear_modulus': 0.48, + 'bulk_modulus': 0.9, + 'youngs_modulus': 0.68, + 'poisson_ratio': 0.37 + }, + { + 'color': Vec4(0.7, 0.65, 0.6, 1.0), + 'roughness': 0.85, + 'metallic': 0.0, + 'specular': 0.18, + 'texture_scale': 2.2, + 'normal_strength': 1.6 + }, + { + 'temperature_sensitivity': 0.05, + 'humidity_sensitivity': 0.16, + 'aging_factor': 0.07, + 'stretch_recovery': 0.76, + 'compression_recovery': 0.76, + 'fatigue_resistance': 0.72, + 'abrasion_resistance': 0.62, + 'chemical_resistance': 0.68, + 'uv_resistance': 0.58, + 'flame_resistance': 0.76, + 'water_absorption': 0.38, + 'air_permeability': 0.52, + 'thermal_conductivity': 0.13, + 'biodegradability': 0.48, + 'recyclability': 0.68 + } + ) + + # 添加到材质字典 + self.materials = { + ClothMaterial.TYPE_COTTON: cotton, + ClothMaterial.TYPE_SILK: silk, + ClothMaterial.TYPE_LEATHER: leather, + ClothMaterial.TYPE_RUBBER: rubber, + ClothMaterial.TYPE_DENIM: denim, + ClothMaterial.TYPE_LINEN: linen, + ClothMaterial.TYPE_WOOL: wool, + ClothMaterial.TYPE_SYNTHETIC: synthetic, + ClothMaterial.TYPE_MESH: mesh, + ClothMaterial.TYPE_CANVAS: canvas, + ClothMaterial.TYPE_NYLON: nylon, + ClothMaterial.TYPE_POLYESTER: polyester, + ClothMaterial.TYPE_SATIN: satin, + ClothMaterial.TYPE_TERRY: terry, + ClothMaterial.TYPE_FLEECE: fleece, + ClothMaterial.TYPE_NEOPRENE: neoprene, + ClothMaterial.TYPE_SPANDEX: spandex, + ClothMaterial.TYPE_TWEED: tweed, + ClothMaterial.TYPE_TULLE: tulle, + ClothMaterial.TYPE_VELVET: velvet, + ClothMaterial.TYPE_SUEDE: suede + } + + def get_material(self, material_type: str) -> Optional[ClothMaterial]: + """ + 获取指定类型的布料材质 + + Args: + material_type: 材质类型 + + Returns: + 布料材质对象或None + """ + return self.materials.get(material_type) + + def get_all_material_types(self) -> List[str]: + """ + 获取所有材质类型 + + Returns: + 材质类型列表 + """ + return list(self.materials.keys()) + + def get_material_names(self) -> List[str]: + """ + 获取所有材质名称 + + Returns: + 材质名称列表 + """ + return [material.name for material in self.materials.values()] + + def create_custom_material(self, name: str, material_type: str, + physical_params: Dict[str, float] = None, + visual_params: Dict[str, Any] = None, + advanced_params: Dict[str, Any] = None) -> ClothMaterial: + """ + 创建自定义布料材质 + + Args: + name: 材质名称 + material_type: 材质类型 + physical_params: 物理参数 + visual_params: 视觉参数 + advanced_params: 高级参数 + + Returns: + 布料材质对象 + """ + material = ClothMaterial(name, material_type, physical_params, visual_params, advanced_params) + return material + + def register_material(self, material: ClothMaterial) -> bool: + """ + 注册布料材质 + + Args: + material: 布料材质对象 + + Returns: + 是否注册成功 + """ + if material.material_type: + self.materials[material.material_type] = material + return True + return False + + def create_material_combination(self, name: str, materials: List[str], + ratios: List[float]) -> ClothMaterial: + """ + 创建混合材质 + + Args: + name: 混合材质名称 + materials: 材质类型列表 + ratios: 比例列表 + + Returns: + 混合布料材质对象 + """ + if len(materials) != len(ratios): + raise ValueError("材质数量和比例数量必须相同") + + # 归一化比例 + total_ratio = sum(ratios) + if total_ratio == 0: + raise ValueError("比例总和不能为0") + normalized_ratios = [r / total_ratio for r in ratios] + + # 获取所有材质 + material_objects = [] + for material_type in materials: + material_obj = self.get_material(material_type) + if material_obj is None: + raise ValueError(f"未知材质类型: {material_type}") + material_objects.append(material_obj) + + # 混合物理参数 + combined_physical = {} + combined_visual = {} + combined_advanced = {} + + # 物理参数混合 + for param_name in material_objects[0].physical_params.keys(): + weighted_sum = 0.0 + for i, material_obj in enumerate(material_objects): + weighted_sum += material_obj.physical_params[param_name] * normalized_ratios[i] + combined_physical[param_name] = weighted_sum + + # 视觉参数混合 + for param_name in material_objects[0].visual_params.keys(): + if param_name == 'color': + # 颜色混合 + mixed_color = Vec4(0, 0, 0, 0) + for i, material_obj in enumerate(material_objects): + color = material_obj.visual_params[param_name] + mixed_color += color * normalized_ratios[i] + combined_visual[param_name] = mixed_color + else: + # 其他视觉参数混合 + weighted_sum = 0.0 + for i, material_obj in enumerate(material_objects): + weighted_sum += material_obj.visual_params[param_name] * normalized_ratios[i] + combined_visual[param_name] = weighted_sum + + # 高级参数混合 + for param_name in material_objects[0].advanced_params.keys(): + weighted_sum = 0.0 + for i, material_obj in enumerate(material_objects): + weighted_sum += material_obj.advanced_params[param_name] * normalized_ratios[i] + combined_advanced[param_name] = weighted_sum + + # 创建混合材质 + combined_material = ClothMaterial( + name, + f"combined_{name}", + combined_physical, + combined_visual, + combined_advanced + ) + + # 保存组合信息 + self.material_combinations[name] = { + 'materials': materials, + 'ratios': normalized_ratios, + 'material_object': combined_material + } + + return combined_material + + def get_material_combination(self, name: str) -> Optional[ClothMaterial]: + """ + 获取混合材质 + + Args: + name: 混合材质名称 + + Returns: + 混合布料材质对象或None + """ + combination = self.material_combinations.get(name) + if combination: + return combination['material_object'] + return None + + def get_material_properties_database(self) -> Dict[str, Dict[str, Any]]: + """ + 获取材质属性数据库 + + Returns: + 材质属性字典 + """ + database = {} + for material_type, material in self.materials.items(): + database[material_type] = material.get_material_properties_summary() + return database + + def find_similar_materials(self, reference_material: ClothMaterial, + max_results: int = 5) -> List[Tuple[str, float]]: + """ + 查找相似材质 + + Args: + reference_material: 参考材质 + max_results: 最大结果数量 + + Returns: + 相似材质列表 (材质类型, 相似度) + """ + reference_props = reference_material.get_material_properties_summary() + similarities = [] + + for material_type, material in self.materials.items(): + if material_type == reference_material.material_type: + continue # 跳过自身 + + material_props = material.get_material_properties_summary() + + # 计算相似度 (简化版本) + similarity = 0.0 + total_weight = 0.0 + + # 物理属性相似度 + ref_phys = reference_props['physical_properties'] + mat_phys = material_props['physical_properties'] + + for key in ref_phys.keys(): + if key in mat_phys: + ref_val = ref_phys[key] + mat_val = mat_phys[key] + # 归一化差异 (假设值在合理范围内) + diff = abs(ref_val - mat_val) / max(1.0, ref_val + mat_val) + similarity += (1.0 - diff) * 0.4 + total_weight += 0.4 + + # 视觉属性相似度 + ref_vis = reference_props['visual_properties'] + mat_vis = material_props['visual_properties'] + + # 颜色相似度 + if 'color' in ref_vis and 'color' in mat_vis: + ref_color = ref_vis['color'] + mat_color = mat_vis['color'] + color_diff = ( + (ref_color[0] - mat_color[0]) ** 2 + + (ref_color[1] - mat_color[1]) ** 2 + + (ref_color[2] - mat_color[2]) ** 2 + ) ** 0.5 + similarity += (1.0 - color_diff) * 0.3 + total_weight += 0.3 + + # 粗糙度相似度 + if 'roughness' in ref_vis and 'roughness' in mat_vis: + roughness_diff = abs(ref_vis['roughness'] - mat_vis['roughness']) + similarity += (1.0 - roughness_diff) * 0.2 + total_weight += 0.2 + + # 金属度相似度 + if 'metallic' in ref_vis and 'metallic' in mat_vis: + metallic_diff = abs(ref_vis['metallic'] - mat_vis['metallic']) + similarity += (1.0 - metallic_diff) * 0.1 + total_weight += 0.1 + + if total_weight > 0: + normalized_similarity = similarity / total_weight + similarities.append((material_type, normalized_similarity)) + + # 按相似度排序 + similarities.sort(key=lambda x: x[1], reverse=True) return similarities[:max_results] \ No newline at end of file diff --git a/plugins/user/soft_body_cloth_physics/plugin.py b/plugins/user/soft_body_cloth_physics/plugin.py index 3223dc6e..1d450bba 100644 --- a/plugins/user/soft_body_cloth_physics/plugin.py +++ b/plugins/user/soft_body_cloth_physics/plugin.py @@ -1,511 +1,511 @@ -""" -软体物理和布料模拟插件主入口 -提供完整的软体物理和布料模拟功能 -""" - -from panda3d.core import Vec3 -from plugins.plugin_interface_spec import BasePlugin -from .core.cloth_manager import ClothManager -from .core.soft_body_manager import SoftBodyManager -from .core.cloth_physics import ClothPhysics -from .core.soft_body_physics import SoftBodyPhysics -from .materials.cloth_materials import ClothMaterialManager -from .utils.mesh_generator import MeshGenerator -from .debug.cloth_debug import PhysicsDebugManager -import json -import os - - -class Plugin(BasePlugin): - """ - 软体物理和布料模拟插件 - 提供布料模拟和软体物理功能,支持多种材质、形状和物理效应 - """ - - def __init__(self, plugin_manager, name: str = "soft_body_cloth_physics"): - super().__init__(plugin_manager, name) - - # 核心管理器 - self.cloth_manager = None - self.soft_body_manager = None - self.cloth_physics = None - self.soft_body_physics = None - self.material_manager = None - self.mesh_generator = None - self.debug_manager = None - - # 配置 - self.config = { - 'gravity': Vec3(0, 0, -9.81), - 'time_step': 1.0/60.0, - 'sub_steps': 4, - 'default_cloth_density': 0.5, - 'default_soft_body_density': 1.0, - 'enable_debug_visualization': False, - 'debug_flags': { - 'show_mesh': True, - 'show_anchors': True, - 'show_forces': False, - 'show_velocities': False, - 'show_normals': False, - 'show_tears': True, - 'show_collisions': False, - 'show_bounding_box': False - }, - 'performance': { - 'max_cloth_objects': 50, - 'max_soft_body_objects': 50, - 'update_frequency': 60, - 'sub_steps': 4 - }, - 'physics': { - 'air_density': 1.225, - 'water_density': 1000.0, - 'water_offset': 0.0, - 'water_normal': Vec3(0, 0, 1) - }, - 'wind': { - 'enabled': False, - 'velocity': Vec3(0, 0, 0), - 'turbulence': 0.0, - 'frequency': 1.0 - } - } - - # 状态 - self.initialized = False - self.performance_stats = { - 'total_cloth_objects': 0, - 'total_soft_body_objects': 0, - 'simulation_time': 0.0, - 'update_count': 0 - } - - def initialize(self) -> bool: - """ - 初始化插件 - - Returns: - 是否初始化成功 - """ - try: - print(f"正在初始化 {self.name} 插件...") - - # 获取世界对象 - world = self.plugin_manager.world - - # 初始化所有管理器 - self.cloth_manager = ClothManager(world) - self.soft_body_manager = SoftBodyManager(world) - self.cloth_physics = ClothPhysics(world) - self.soft_body_physics = SoftBodyPhysics(world) - self.material_manager = ClothMaterialManager() - self.mesh_generator = MeshGenerator() - self.debug_manager = PhysicsDebugManager(world.render) - - # 加载配置 - self._load_config() - - # 应用配置 - self._apply_config() - - # 启用调试可视化(如果配置中启用) - if self.config['enable_debug_visualization']: - self.debug_manager.enable() - # 设置调试标志 - for flag, value in self.config['debug_flags'].items(): - if flag == 'show_mesh': - self.debug_manager.set_cloth_visibility(mesh=value) - self.debug_manager.set_soft_body_visibility(mesh=value) - elif flag == 'show_anchors': - self.debug_manager.set_cloth_visibility(anchors=value) - elif flag == 'show_forces': - self.debug_manager.set_cloth_visibility(forces=value) - self.debug_manager.set_soft_body_visibility(forces=value) - elif flag == 'show_velocities': - self.debug_manager.set_cloth_visibility(velocities=value) - self.debug_manager.set_soft_body_visibility(velocities=value) - elif flag == 'show_normals': - self.debug_manager.set_cloth_visibility(normals=value) - elif flag == 'show_tears': - self.debug_manager.set_cloth_visibility(tears=value) - elif flag == 'show_collisions': - self.debug_manager.set_cloth_visibility(collisions=value) - self.debug_manager.set_soft_body_visibility(collisions=value) - elif flag == 'show_bounding_box': - self.debug_manager.set_cloth_visibility(bounding_box=value) - - # 设置性能参数 - self.cloth_manager.max_cloth_objects = self.config['performance']['max_cloth_objects'] - self.cloth_manager.cloth_update_frequency = self.config['performance']['update_frequency'] - self.soft_body_manager.max_soft_body_objects = self.config['performance']['max_soft_body_objects'] - self.soft_body_manager.soft_body_update_frequency = self.config['performance']['update_frequency'] - - # 设置风力参数 - self._apply_wind_config() - - self.initialized = True - print(f"✓ {self.name} 插件初始化成功") - return True - - except Exception as e: - print(f"✗ {self.name} 插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("⚠ 插件未初始化,无法启用") - return False - - # 启用所有管理器 - if self.cloth_manager: - self.cloth_manager.enable() - if self.soft_body_manager: - self.soft_body_manager.enable() - if self.cloth_physics: - # 无需特殊启用 - pass - if self.soft_body_physics: - # 无需特殊启用 - pass - if self.debug_manager: - self.debug_manager.enable() - - self.enabled = True - print(f"✓ {self.name} 插件已启用") - return True - - except Exception as e: - print(f"✗ {self.name} 插件启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self) -> bool: - """ - 禁用插件 - - Returns: - 是否禁用成功 - """ - try: - # 禁用所有管理器 - if self.cloth_manager: - self.cloth_manager.disable() - if self.soft_body_manager: - self.soft_body_manager.disable() - if self.debug_manager: - self.debug_manager.disable() - - self.enabled = False - print(f"✓ {self.name} 插件已禁用") - return True - - except Exception as e: - print(f"✗ {self.name} 插件禁用失败: {e}") - import traceback - traceback.print_exc() - return False - - def finalize(self): - """ - 清理插件资源 - """ - try: - # 清理所有管理器 - if self.cloth_manager: - self.cloth_manager.cleanup() - self.cloth_manager = None - if self.soft_body_manager: - self.soft_body_manager.cleanup() - self.soft_body_manager = None - if self.cloth_physics: - self.cloth_physics = None - if self.soft_body_physics: - self.soft_body_physics = None - if self.material_manager: - self.material_manager = None - if self.mesh_generator: - self.mesh_generator = None - if self.debug_manager: - self.debug_manager.cleanup() - self.debug_manager = None - - print(f"✓ {self.name} 插件资源已清理") - - except Exception as e: - print(f"✗ {self.name} 插件清理失败: {e}") - import traceback - traceback.print_exc() - - def get_info(self): - """ - 获取插件信息 - - Returns: - 插件信息字典 - """ - info = super().get_info() - info.update({ - "name": "SoftBodyClothPhysics", - "version": "1.0.0", - "author": "iFlow Team", - "description": "提供软体物理和布料模拟功能的插件", - "features": [ - "布料物理模拟", - "软体物理模拟", - "多种布料材质类型", - "参数实时调节", - "调试可视化", - "风力模拟", - "撕裂和碰撞检测", - "高级网格生成" - ], - "dependencies": [ - "panda3d", - "panda3d.bullet" - ], - "capabilities": { - "cloth_types": self.material_manager.get_all_material_types() if self.material_manager else [], - "mesh_generation": [ - "cloth_patch", - "sphere", - "box", - "capsule", - "cylinder", - "rope", - "torus", - "custom" - ], - "debug_visualization": [ - "mesh", - "anchors", - "forces", - "velocities", - "normals", - "tears", - "collisions", - "bounding_box" - ] - } - }) - return info - - def update(self, dt: float): - """ - 更新插件(每帧调用) - - Args: - dt: 时间增量 - """ - if not self.enabled or not self.initialized: - return - - # 更新所有管理器 - if self.cloth_manager: - self.cloth_manager.update_all_cloths(dt) - if self.soft_body_manager: - self.soft_body_manager.update_all_soft_bodies(dt) - if self.cloth_physics: - self.cloth_physics.update_wind(dt) - if self.debug_manager: - # 更新可视化 - if self.cloth_manager: - self.debug_manager.update_all_visualizations( - self.cloth_manager.cloth_objects, - self.soft_body_manager.soft_body_objects if self.soft_body_manager else [] - ) - - # 更新性能统计 - self.performance_stats['update_count'] += 1 - if self.cloth_manager: - self.performance_stats['total_cloth_objects'] = len(self.cloth_manager.cloth_objects) - if self.soft_body_manager: - self.performance_stats['total_soft_body_objects'] = len(self.soft_body_manager.soft_body_objects) - - def get_performance_stats(self) -> dict: - """ - 获取性能统计信息 - - Returns: - 性能统计信息字典 - """ - stats = self.performance_stats.copy() - - # 添加管理器统计 - if self.cloth_manager: - cloth_stats = self.cloth_manager.get_cloth_stats() - stats.update(cloth_stats) - - if self.soft_body_manager: - soft_stats = self.soft_body_manager.get_soft_body_stats() - stats.update(soft_stats) - - if self.debug_manager: - debug_stats = self.debug_manager.get_debug_stats() - stats['debug_stats'] = debug_stats - - return stats - - def create_cloth(self, parent_node, cloth_type: str, - width: float, height: float, - resolution_x: int, resolution_y: int, - params: dict = None, - behavior_mode: str = None) -> dict: - """ - 创建布料对象 - - Args: - parent_node: 父节点 - cloth_type: 布料类型 - width: 宽度 - height: 高度 - resolution_x: X分辨率 - resolution_y: Y分辨率 - params: 参数 - behavior_mode: 行为模式 - - Returns: - 布料对象信息 - """ - if not self.initialized or not self.cloth_manager: - return None - - return self.cloth_manager.create_cloth_patch( - parent_node, width, height, resolution_x, resolution_y, params - ) - - def create_soft_body(self, parent_node, soft_type: str, - shape_type: str = "sphere", - **kwargs) -> dict: - """ - 创建软体对象 - - Args: - parent_node: 父节点 - soft_type: 软体类型 - shape_type: 形状类型 - **kwargs: 形状参数 - - Returns: - 软体对象信息 - """ - if not self.initialized or not self.soft_body_manager: - return None - - return self.soft_body_manager.create_soft_body_from_mesh( - parent_node, soft_type, shape_type, **kwargs - ) - - def set_config(self, key: str, value): - """ - 设置配置参数 - - Args: - key: 配置键 - value: 配置值 - """ - if key in self.config: - self.config[key] = value - if key == 'gravity': - self._apply_config() - elif key == 'enable_debug_visualization': - if value and self.debug_manager: - self.debug_manager.enable() - elif not value and self.debug_manager: - self.debug_manager.disable() - - def get_config(self, key: str = None): - """ - 获取配置参数 - - Args: - key: 配置键(如果为None则返回整个配置) - - Returns: - 配置值或整个配置字典 - """ - if key: - return self.config.get(key) - return self.config.copy() - - def _load_config(self): - """ - 从配置文件加载配置 - """ - try: - config_path = os.path.join( - os.path.dirname(__file__), - "config", - "cloth_settings.json" - ) - - if os.path.exists(config_path): - with open(config_path, 'r', encoding='utf-8') as f: - file_config = json.load(f) - - # 合并配置 - self._merge_config(self.config, file_config.get('plugin', {})) - - except Exception as e: - print(f"⚠ 加载配置文件失败: {e}") - - def _merge_config(self, target: dict, source: dict): - """ - 合并配置字典 - - Args: - target: 目标字典 - source: 源字典 - """ - for key, value in source.items(): - if isinstance(value, dict) and key in target and isinstance(target[key], dict): - self._merge_config(target[key], value) - else: - target[key] = value - - def _apply_config(self): - """ - 应用配置设置 - """ - if self.cloth_manager: - self.cloth_manager.set_gravity(self.config['gravity']) - if self.soft_body_manager: - self.soft_body_manager.set_gravity(self.config['gravity']) - if self.cloth_physics: - self.cloth_physics.update_soft_body_world_info( - air_density=self.config['physics']['air_density'], - water_density=self.config['physics']['water_density'], - water_offset=self.config['physics']['water_offset'], - water_normal=self.config['physics']['water_normal'], - gravity=self.config['gravity'] - ) - if self.soft_body_physics: - self.soft_body_physics.update_soft_body_world_info( - air_density=self.config['physics']['air_density'], - water_density=self.config['physics']['water_density'], - water_offset=self.config['physics']['water_offset'], - water_normal=self.config['physics']['water_normal'], - gravity=self.config['gravity'] - ) - - def _apply_wind_config(self): - """ - 应用风力配置 - """ - if self.cloth_physics and self.config['wind']['enabled']: - self.cloth_physics.set_wind_parameters( - enabled=self.config['wind']['enabled'], - velocity=self.config['wind']['velocity'], - turbulence=self.config['wind']['turbulence'], - frequency=self.config['wind']['frequency'] +""" +软体物理和布料模拟插件主入口 +提供完整的软体物理和布料模拟功能 +""" + +from panda3d.core import Vec3 +from plugins.plugin_interface_spec import BasePlugin +from .core.cloth_manager import ClothManager +from .core.soft_body_manager import SoftBodyManager +from .core.cloth_physics import ClothPhysics +from .core.soft_body_physics import SoftBodyPhysics +from .materials.cloth_materials import ClothMaterialManager +from .utils.mesh_generator import MeshGenerator +from .debug.cloth_debug import PhysicsDebugManager +import json +import os + + +class Plugin(BasePlugin): + """ + 软体物理和布料模拟插件 + 提供布料模拟和软体物理功能,支持多种材质、形状和物理效应 + """ + + def __init__(self, plugin_manager, name: str = "soft_body_cloth_physics"): + super().__init__(plugin_manager, name) + + # 核心管理器 + self.cloth_manager = None + self.soft_body_manager = None + self.cloth_physics = None + self.soft_body_physics = None + self.material_manager = None + self.mesh_generator = None + self.debug_manager = None + + # 配置 + self.config = { + 'gravity': Vec3(0, 0, -9.81), + 'time_step': 1.0/60.0, + 'sub_steps': 4, + 'default_cloth_density': 0.5, + 'default_soft_body_density': 1.0, + 'enable_debug_visualization': False, + 'debug_flags': { + 'show_mesh': True, + 'show_anchors': True, + 'show_forces': False, + 'show_velocities': False, + 'show_normals': False, + 'show_tears': True, + 'show_collisions': False, + 'show_bounding_box': False + }, + 'performance': { + 'max_cloth_objects': 50, + 'max_soft_body_objects': 50, + 'update_frequency': 60, + 'sub_steps': 4 + }, + 'physics': { + 'air_density': 1.225, + 'water_density': 1000.0, + 'water_offset': 0.0, + 'water_normal': Vec3(0, 0, 1) + }, + 'wind': { + 'enabled': False, + 'velocity': Vec3(0, 0, 0), + 'turbulence': 0.0, + 'frequency': 1.0 + } + } + + # 状态 + self.initialized = False + self.performance_stats = { + 'total_cloth_objects': 0, + 'total_soft_body_objects': 0, + 'simulation_time': 0.0, + 'update_count': 0 + } + + def initialize(self) -> bool: + """ + 初始化插件 + + Returns: + 是否初始化成功 + """ + try: + print(f"正在初始化 {self.name} 插件...") + + # 获取世界对象 + world = self.plugin_manager.world + + # 初始化所有管理器 + self.cloth_manager = ClothManager(world) + self.soft_body_manager = SoftBodyManager(world) + self.cloth_physics = ClothPhysics(world) + self.soft_body_physics = SoftBodyPhysics(world) + self.material_manager = ClothMaterialManager() + self.mesh_generator = MeshGenerator() + self.debug_manager = PhysicsDebugManager(world.render) + + # 加载配置 + self._load_config() + + # 应用配置 + self._apply_config() + + # 启用调试可视化(如果配置中启用) + if self.config['enable_debug_visualization']: + self.debug_manager.enable() + # 设置调试标志 + for flag, value in self.config['debug_flags'].items(): + if flag == 'show_mesh': + self.debug_manager.set_cloth_visibility(mesh=value) + self.debug_manager.set_soft_body_visibility(mesh=value) + elif flag == 'show_anchors': + self.debug_manager.set_cloth_visibility(anchors=value) + elif flag == 'show_forces': + self.debug_manager.set_cloth_visibility(forces=value) + self.debug_manager.set_soft_body_visibility(forces=value) + elif flag == 'show_velocities': + self.debug_manager.set_cloth_visibility(velocities=value) + self.debug_manager.set_soft_body_visibility(velocities=value) + elif flag == 'show_normals': + self.debug_manager.set_cloth_visibility(normals=value) + elif flag == 'show_tears': + self.debug_manager.set_cloth_visibility(tears=value) + elif flag == 'show_collisions': + self.debug_manager.set_cloth_visibility(collisions=value) + self.debug_manager.set_soft_body_visibility(collisions=value) + elif flag == 'show_bounding_box': + self.debug_manager.set_cloth_visibility(bounding_box=value) + + # 设置性能参数 + self.cloth_manager.max_cloth_objects = self.config['performance']['max_cloth_objects'] + self.cloth_manager.cloth_update_frequency = self.config['performance']['update_frequency'] + self.soft_body_manager.max_soft_body_objects = self.config['performance']['max_soft_body_objects'] + self.soft_body_manager.soft_body_update_frequency = self.config['performance']['update_frequency'] + + # 设置风力参数 + self._apply_wind_config() + + self.initialized = True + print(f"✓ {self.name} 插件初始化成功") + return True + + except Exception as e: + print(f"✗ {self.name} 插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("⚠ 插件未初始化,无法启用") + return False + + # 启用所有管理器 + if self.cloth_manager: + self.cloth_manager.enable() + if self.soft_body_manager: + self.soft_body_manager.enable() + if self.cloth_physics: + # 无需特殊启用 + pass + if self.soft_body_physics: + # 无需特殊启用 + pass + if self.debug_manager: + self.debug_manager.enable() + + self.enabled = True + print(f"✓ {self.name} 插件已启用") + return True + + except Exception as e: + print(f"✗ {self.name} 插件启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self) -> bool: + """ + 禁用插件 + + Returns: + 是否禁用成功 + """ + try: + # 禁用所有管理器 + if self.cloth_manager: + self.cloth_manager.disable() + if self.soft_body_manager: + self.soft_body_manager.disable() + if self.debug_manager: + self.debug_manager.disable() + + self.enabled = False + print(f"✓ {self.name} 插件已禁用") + return True + + except Exception as e: + print(f"✗ {self.name} 插件禁用失败: {e}") + import traceback + traceback.print_exc() + return False + + def finalize(self): + """ + 清理插件资源 + """ + try: + # 清理所有管理器 + if self.cloth_manager: + self.cloth_manager.cleanup() + self.cloth_manager = None + if self.soft_body_manager: + self.soft_body_manager.cleanup() + self.soft_body_manager = None + if self.cloth_physics: + self.cloth_physics = None + if self.soft_body_physics: + self.soft_body_physics = None + if self.material_manager: + self.material_manager = None + if self.mesh_generator: + self.mesh_generator = None + if self.debug_manager: + self.debug_manager.cleanup() + self.debug_manager = None + + print(f"✓ {self.name} 插件资源已清理") + + except Exception as e: + print(f"✗ {self.name} 插件清理失败: {e}") + import traceback + traceback.print_exc() + + def get_info(self): + """ + 获取插件信息 + + Returns: + 插件信息字典 + """ + info = super().get_info() + info.update({ + "name": "SoftBodyClothPhysics", + "version": "1.0.0", + "author": "iFlow Team", + "description": "提供软体物理和布料模拟功能的插件", + "features": [ + "布料物理模拟", + "软体物理模拟", + "多种布料材质类型", + "参数实时调节", + "调试可视化", + "风力模拟", + "撕裂和碰撞检测", + "高级网格生成" + ], + "dependencies": [ + "panda3d", + "panda3d.bullet" + ], + "capabilities": { + "cloth_types": self.material_manager.get_all_material_types() if self.material_manager else [], + "mesh_generation": [ + "cloth_patch", + "sphere", + "box", + "capsule", + "cylinder", + "rope", + "torus", + "custom" + ], + "debug_visualization": [ + "mesh", + "anchors", + "forces", + "velocities", + "normals", + "tears", + "collisions", + "bounding_box" + ] + } + }) + return info + + def update(self, dt: float): + """ + 更新插件(每帧调用) + + Args: + dt: 时间增量 + """ + if not self.enabled or not self.initialized: + return + + # 更新所有管理器 + if self.cloth_manager: + self.cloth_manager.update_all_cloths(dt) + if self.soft_body_manager: + self.soft_body_manager.update_all_soft_bodies(dt) + if self.cloth_physics: + self.cloth_physics.update_wind(dt) + if self.debug_manager: + # 更新可视化 + if self.cloth_manager: + self.debug_manager.update_all_visualizations( + self.cloth_manager.cloth_objects, + self.soft_body_manager.soft_body_objects if self.soft_body_manager else [] + ) + + # 更新性能统计 + self.performance_stats['update_count'] += 1 + if self.cloth_manager: + self.performance_stats['total_cloth_objects'] = len(self.cloth_manager.cloth_objects) + if self.soft_body_manager: + self.performance_stats['total_soft_body_objects'] = len(self.soft_body_manager.soft_body_objects) + + def get_performance_stats(self) -> dict: + """ + 获取性能统计信息 + + Returns: + 性能统计信息字典 + """ + stats = self.performance_stats.copy() + + # 添加管理器统计 + if self.cloth_manager: + cloth_stats = self.cloth_manager.get_cloth_stats() + stats.update(cloth_stats) + + if self.soft_body_manager: + soft_stats = self.soft_body_manager.get_soft_body_stats() + stats.update(soft_stats) + + if self.debug_manager: + debug_stats = self.debug_manager.get_debug_stats() + stats['debug_stats'] = debug_stats + + return stats + + def create_cloth(self, parent_node, cloth_type: str, + width: float, height: float, + resolution_x: int, resolution_y: int, + params: dict = None, + behavior_mode: str = None) -> dict: + """ + 创建布料对象 + + Args: + parent_node: 父节点 + cloth_type: 布料类型 + width: 宽度 + height: 高度 + resolution_x: X分辨率 + resolution_y: Y分辨率 + params: 参数 + behavior_mode: 行为模式 + + Returns: + 布料对象信息 + """ + if not self.initialized or not self.cloth_manager: + return None + + return self.cloth_manager.create_cloth_patch( + parent_node, width, height, resolution_x, resolution_y, params + ) + + def create_soft_body(self, parent_node, soft_type: str, + shape_type: str = "sphere", + **kwargs) -> dict: + """ + 创建软体对象 + + Args: + parent_node: 父节点 + soft_type: 软体类型 + shape_type: 形状类型 + **kwargs: 形状参数 + + Returns: + 软体对象信息 + """ + if not self.initialized or not self.soft_body_manager: + return None + + return self.soft_body_manager.create_soft_body_from_mesh( + parent_node, soft_type, shape_type, **kwargs + ) + + def set_config(self, key: str, value): + """ + 设置配置参数 + + Args: + key: 配置键 + value: 配置值 + """ + if key in self.config: + self.config[key] = value + if key == 'gravity': + self._apply_config() + elif key == 'enable_debug_visualization': + if value and self.debug_manager: + self.debug_manager.enable() + elif not value and self.debug_manager: + self.debug_manager.disable() + + def get_config(self, key: str = None): + """ + 获取配置参数 + + Args: + key: 配置键(如果为None则返回整个配置) + + Returns: + 配置值或整个配置字典 + """ + if key: + return self.config.get(key) + return self.config.copy() + + def _load_config(self): + """ + 从配置文件加载配置 + """ + try: + config_path = os.path.join( + os.path.dirname(__file__), + "config", + "cloth_settings.json" + ) + + if os.path.exists(config_path): + with open(config_path, 'r', encoding='utf-8') as f: + file_config = json.load(f) + + # 合并配置 + self._merge_config(self.config, file_config.get('plugin', {})) + + except Exception as e: + print(f"⚠ 加载配置文件失败: {e}") + + def _merge_config(self, target: dict, source: dict): + """ + 合并配置字典 + + Args: + target: 目标字典 + source: 源字典 + """ + for key, value in source.items(): + if isinstance(value, dict) and key in target and isinstance(target[key], dict): + self._merge_config(target[key], value) + else: + target[key] = value + + def _apply_config(self): + """ + 应用配置设置 + """ + if self.cloth_manager: + self.cloth_manager.set_gravity(self.config['gravity']) + if self.soft_body_manager: + self.soft_body_manager.set_gravity(self.config['gravity']) + if self.cloth_physics: + self.cloth_physics.update_soft_body_world_info( + air_density=self.config['physics']['air_density'], + water_density=self.config['physics']['water_density'], + water_offset=self.config['physics']['water_offset'], + water_normal=self.config['physics']['water_normal'], + gravity=self.config['gravity'] + ) + if self.soft_body_physics: + self.soft_body_physics.update_soft_body_world_info( + air_density=self.config['physics']['air_density'], + water_density=self.config['physics']['water_density'], + water_offset=self.config['physics']['water_offset'], + water_normal=self.config['physics']['water_normal'], + gravity=self.config['gravity'] + ) + + def _apply_wind_config(self): + """ + 应用风力配置 + """ + if self.cloth_physics and self.config['wind']['enabled']: + self.cloth_physics.set_wind_parameters( + enabled=self.config['wind']['enabled'], + velocity=self.config['wind']['velocity'], + turbulence=self.config['wind']['turbulence'], + frequency=self.config['wind']['frequency'] ) \ No newline at end of file diff --git a/plugins/user/soft_body_cloth_physics/test_plugin.py b/plugins/user/soft_body_cloth_physics/test_plugin.py index 05251cc9..be03435b 100644 --- a/plugins/user/soft_body_cloth_physics/test_plugin.py +++ b/plugins/user/soft_body_cloth_physics/test_plugin.py @@ -1,218 +1,218 @@ -""" -软体物理和布料模拟插件测试脚本 -用于测试插件的基本功能 -""" - -import sys -import os - -# 添加项目根目录到Python路径 -project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) -sys.path.insert(0, project_root) - -def test_plugin_structure(): - """ - 测试插件结构是否正确 - """ - print("开始测试软体物理和布料模拟插件结构...") - - # 测试导入插件模块 - try: - from plugins.user.soft_body_cloth_physics.plugin import Plugin - print("✓ 插件主类导入成功") - except ImportError as e: - print(f"✗ 插件主类导入失败: {e}") - return False - - # 测试导入核心模块 - try: - from plugins.user.soft_body_cloth_physics.core.cloth_manager import ClothManager - from plugins.user.soft_body_cloth_physics.core.soft_body_manager import SoftBodyManager - print("✓ 核心管理器导入成功") - except ImportError as e: - print(f"✗ 核心管理器导入失败: {e}") - return False - - # 测试导入物理模块 - try: - from plugins.user.soft_body_cloth_physics.core.cloth_physics import ClothPhysics - from plugins.user.soft_body_cloth_physics.core.soft_body_physics import SoftBodyPhysics - print("✓ 物理核心类导入成功") - except ImportError as e: - print(f"✗ 物理核心类导入失败: {e}") - return False - - # 测试导入材质系统 - try: - from plugins.user.soft_body_cloth_physics.materials.cloth_materials import ClothMaterialManager - print("✓ 材质系统导入成功") - except ImportError as e: - print(f"✗ 材质系统导入失败: {e}") - return False - - # 测试导入工具模块 - try: - from plugins.user.soft_body_cloth_physics.utils.mesh_generator import MeshGenerator - print("✓ 网格生成工具导入成功") - except ImportError as e: - print(f"✗ 网格生成工具导入失败: {e}") - return False - - # 测试导入调试模块 - try: - from plugins.user.soft_body_cloth_physics.debug.cloth_debug import PhysicsDebugManager - print("✓ 调试工具导入成功") - except ImportError as e: - print(f"✗ 调试工具导入失败: {e}") - return False - - # 测试配置文件是否存在 - config_path = os.path.join( - project_root, - "plugins", - "user", - "soft_body_cloth_physics", - "config", - "cloth_settings.json" - ) - - if os.path.exists(config_path): - print("✓ 配置文件存在") - else: - print("✗ 配置文件不存在") - return False - - print("插件结构测试通过!") - return True - -def test_plugin_functionality(): - """ - 测试插件基本功能 - """ - print("\n开始测试插件基本功能...") - - # 创建一个模拟的世界对象 - class MockWorld: - def __init__(self): - self.plugins = {} - self.world = self # 添加world属性以避免错误 - - try: - # 测试插件初始化 - from plugins.user.soft_body_cloth_physics.plugin import Plugin - - mock_world = MockWorld() - plugin = Plugin(mock_world, "soft_body_cloth_physics") - - if plugin.initialize(): - print("✓ 插件初始化成功") - else: - print("✗ 插件初始化失败") - return False - - # 测试启用插件 - if plugin.enable(): - print("✓ 插件启用成功") - else: - print("✗ 插件启用失败") - return False - - # 测试获取插件信息 - info = plugin.get_info() - if info and "name" in info and info["name"] == "SoftBodyClothPhysics": - print("✓ 插件信息获取成功") - else: - print("✗ 插件信息获取失败") - return False - - # 测试禁用插件 - if plugin.disable(): - print("✓ 插件禁用成功") - else: - print("✗ 插件禁用失败") - return False - - # 测试清理插件 - plugin.finalize() - print("✓ 插件清理成功") - - print("插件功能测试通过!") - return True - - except Exception as e: - print(f"✗ 插件功能测试失败: {e}") - import traceback - traceback.print_exc() - return False - -def test_cloth_materials(): - """ - 测试布料材质系统 - """ - print("\n开始测试布料材质系统...") - - try: - from plugins.user.soft_body_cloth_physics.materials.cloth_materials import ClothMaterialManager - - # 创建材质管理器 - material_manager = ClothMaterialManager() - - # 测试获取材质 - cotton = material_manager.get_material("cotton") - if cotton: - print("✓ 棉布材质获取成功") - else: - print("✗ 棉布材质获取失败") - return False - - # 测试获取所有材质类型 - material_types = material_manager.get_all_material_types() - if material_types: - print(f"✓ 材质类型获取成功: {len(material_types)} 种") - else: - print("✗ 材质类型获取失败") - return False - - print("布料材质系统测试通过!") - return True - - except Exception as e: - print(f"✗ 布料材质系统测试失败: {e}") - import traceback - traceback.print_exc() - return False - -def main(): - """ - 主测试函数 - """ - print("软体物理和布料模拟插件测试") - print("=" * 40) - - # 运行所有测试 - tests = [ - test_plugin_structure, - test_plugin_functionality, - test_cloth_materials - ] - - passed = 0 - total = len(tests) - - for test in tests: - if test(): - passed += 1 - print() - - print("=" * 40) - print(f"测试结果: {passed}/{total} 通过") - - if passed == total: - print("所有测试通过! 插件已准备好使用。") - return True - else: - print("部分测试失败,请检查插件实现。") - return False - -if __name__ == "__main__": +""" +软体物理和布料模拟插件测试脚本 +用于测试插件的基本功能 +""" + +import sys +import os + +# 添加项目根目录到Python路径 +project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +sys.path.insert(0, project_root) + +def test_plugin_structure(): + """ + 测试插件结构是否正确 + """ + print("开始测试软体物理和布料模拟插件结构...") + + # 测试导入插件模块 + try: + from plugins.user.soft_body_cloth_physics.plugin import Plugin + print("✓ 插件主类导入成功") + except ImportError as e: + print(f"✗ 插件主类导入失败: {e}") + return False + + # 测试导入核心模块 + try: + from plugins.user.soft_body_cloth_physics.core.cloth_manager import ClothManager + from plugins.user.soft_body_cloth_physics.core.soft_body_manager import SoftBodyManager + print("✓ 核心管理器导入成功") + except ImportError as e: + print(f"✗ 核心管理器导入失败: {e}") + return False + + # 测试导入物理模块 + try: + from plugins.user.soft_body_cloth_physics.core.cloth_physics import ClothPhysics + from plugins.user.soft_body_cloth_physics.core.soft_body_physics import SoftBodyPhysics + print("✓ 物理核心类导入成功") + except ImportError as e: + print(f"✗ 物理核心类导入失败: {e}") + return False + + # 测试导入材质系统 + try: + from plugins.user.soft_body_cloth_physics.materials.cloth_materials import ClothMaterialManager + print("✓ 材质系统导入成功") + except ImportError as e: + print(f"✗ 材质系统导入失败: {e}") + return False + + # 测试导入工具模块 + try: + from plugins.user.soft_body_cloth_physics.utils.mesh_generator import MeshGenerator + print("✓ 网格生成工具导入成功") + except ImportError as e: + print(f"✗ 网格生成工具导入失败: {e}") + return False + + # 测试导入调试模块 + try: + from plugins.user.soft_body_cloth_physics.debug.cloth_debug import PhysicsDebugManager + print("✓ 调试工具导入成功") + except ImportError as e: + print(f"✗ 调试工具导入失败: {e}") + return False + + # 测试配置文件是否存在 + config_path = os.path.join( + project_root, + "plugins", + "user", + "soft_body_cloth_physics", + "config", + "cloth_settings.json" + ) + + if os.path.exists(config_path): + print("✓ 配置文件存在") + else: + print("✗ 配置文件不存在") + return False + + print("插件结构测试通过!") + return True + +def test_plugin_functionality(): + """ + 测试插件基本功能 + """ + print("\n开始测试插件基本功能...") + + # 创建一个模拟的世界对象 + class MockWorld: + def __init__(self): + self.plugins = {} + self.world = self # 添加world属性以避免错误 + + try: + # 测试插件初始化 + from plugins.user.soft_body_cloth_physics.plugin import Plugin + + mock_world = MockWorld() + plugin = Plugin(mock_world, "soft_body_cloth_physics") + + if plugin.initialize(): + print("✓ 插件初始化成功") + else: + print("✗ 插件初始化失败") + return False + + # 测试启用插件 + if plugin.enable(): + print("✓ 插件启用成功") + else: + print("✗ 插件启用失败") + return False + + # 测试获取插件信息 + info = plugin.get_info() + if info and "name" in info and info["name"] == "SoftBodyClothPhysics": + print("✓ 插件信息获取成功") + else: + print("✗ 插件信息获取失败") + return False + + # 测试禁用插件 + if plugin.disable(): + print("✓ 插件禁用成功") + else: + print("✗ 插件禁用失败") + return False + + # 测试清理插件 + plugin.finalize() + print("✓ 插件清理成功") + + print("插件功能测试通过!") + return True + + except Exception as e: + print(f"✗ 插件功能测试失败: {e}") + import traceback + traceback.print_exc() + return False + +def test_cloth_materials(): + """ + 测试布料材质系统 + """ + print("\n开始测试布料材质系统...") + + try: + from plugins.user.soft_body_cloth_physics.materials.cloth_materials import ClothMaterialManager + + # 创建材质管理器 + material_manager = ClothMaterialManager() + + # 测试获取材质 + cotton = material_manager.get_material("cotton") + if cotton: + print("✓ 棉布材质获取成功") + else: + print("✗ 棉布材质获取失败") + return False + + # 测试获取所有材质类型 + material_types = material_manager.get_all_material_types() + if material_types: + print(f"✓ 材质类型获取成功: {len(material_types)} 种") + else: + print("✗ 材质类型获取失败") + return False + + print("布料材质系统测试通过!") + return True + + except Exception as e: + print(f"✗ 布料材质系统测试失败: {e}") + import traceback + traceback.print_exc() + return False + +def main(): + """ + 主测试函数 + """ + print("软体物理和布料模拟插件测试") + print("=" * 40) + + # 运行所有测试 + tests = [ + test_plugin_structure, + test_plugin_functionality, + test_cloth_materials + ] + + passed = 0 + total = len(tests) + + for test in tests: + if test(): + passed += 1 + print() + + print("=" * 40) + print(f"测试结果: {passed}/{total} 通过") + + if passed == total: + print("所有测试通过! 插件已准备好使用。") + return True + else: + print("部分测试失败,请检查插件实现。") + return False + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/plugins/user/soft_body_cloth_physics/utils/mesh_generator.py b/plugins/user/soft_body_cloth_physics/utils/mesh_generator.py index e768c06f..36193c99 100644 --- a/plugins/user/soft_body_cloth_physics/utils/mesh_generator.py +++ b/plugins/user/soft_body_cloth_physics/utils/mesh_generator.py @@ -1,1360 +1,1360 @@ -""" -网格生成工具 -提供用于生成布料和软体物理模拟所需网格的工具函数 -包含高级网格生成和优化功能 -""" - -from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter -from panda3d.core import GeomTriangles, Geom, GeomNode, NodePath -from panda3d.core import Vec3, Point3, Vec4 -import math -from typing import List, Tuple, Optional, Dict, Any -import numpy as np - - -class MeshGenerator: - """ - 网格生成器 - 提供各种网格生成函数,支持高级几何体和优化 - """ - - # 网格类型常量 - TYPE_CLOTH = "cloth" - TYPE_SPHERE = "sphere" - TYPE_BOX = "box" - TYPE_CAPSULE = "capsule" - TYPE_CYLINDER = "cylinder" - TYPE_ROPE = "rope" - TYPE_TORUS = "torus" - TYPE_PLANE = "plane" - TYPE_CUSTOM = "custom" - - # 优化级别 - OPTIMIZATION_NONE = "none" - OPTIMIZATION_LOW = "low" - OPTIMIZATION_MEDIUM = "medium" - OPTIMIZATION_HIGH = "high" - - @staticmethod - def create_cloth_patch(width: float, height: float, - resolution_x: int, resolution_y: int, - parent_node: NodePath = None, - texture_coords: bool = True, - normals: bool = True, - optimization: str = OPTIMIZATION_NONE, - uv_scale: float = 1.0, - deformation_pattern: str = "flat") -> NodePath: - """ - 创建布料片网格 - - Args: - width: 布料宽度 - height: 布料高度 - resolution_x: X方向分辨率 - resolution_y: Y方向分辨率 - parent_node: 父节点(可选) - texture_coords: 是否生成纹理坐标 - normals: 是否生成法线 - optimization: 优化级别 - uv_scale: UV缩放因子 - deformation_pattern: 变形模式 ("flat", "wave", "sine", "custom") - - Returns: - 包含布料网格的节点路径 - """ - # 确定顶点格式 - if texture_coords and normals: - format = GeomVertexFormat.getV3n3t2() - elif normals: - format = GeomVertexFormat.getV3n3() - elif texture_coords: - format = GeomVertexFormat.getV3t2() - else: - format = GeomVertexFormat.getV3() - - # 创建顶点数据 - vdata = GeomVertexData('cloth_patch', format, Geom.UHDynamic) - - # 计算顶点数量 - num_vertices = (resolution_x + 1) * (resolution_y + 1) - vdata.setNumRows(num_vertices) - - # 创建顶点写入器 - vertex_writer = GeomVertexWriter(vdata, 'vertex') - if normals: - normal_writer = GeomVertexWriter(vdata, 'normal') - if texture_coords: - texcoord_writer = GeomVertexWriter(vdata, 'texcoord') - - # 生成顶点 - half_width = width / 2.0 - half_height = height / 2.0 - - vertices = [] - for j in range(resolution_y + 1): - for i in range(resolution_x + 1): - # 计算基础顶点位置 - x = (i / resolution_x) * width - half_width - y = (j / resolution_y) * height - half_height - - # 根据变形模式调整Z坐标 - z = 0.0 - if deformation_pattern == "wave": - z = math.sin(x * 2) * 0.1 + math.cos(y * 2) * 0.1 - elif deformation_pattern == "sine": - z = math.sin(math.sqrt(x*x + y*y) * 3) * 0.05 - elif deformation_pattern == "custom": - # 自定义变形模式可以在这里扩展 - z = 0.0 - - # 写入顶点数据 - vertex_writer.addData3(x, y, z) - - if normals: - # 计算法线(对于变形表面) - if deformation_pattern == "flat": - normal_writer.addData3(0, 0, 1) - else: - # 对于变形表面,计算近似法线 - # 这里使用简化的法线计算 - normal_writer.addData3(0, 0, 1) - - if texture_coords: - u = (i / resolution_x) * uv_scale - v = (j / resolution_y) * uv_scale - texcoord_writer.addData2(u, v) - - vertices.append(Point3(x, y, z)) - - # 创建三角形索引 - prim = GeomTriangles(Geom.UHDynamic) - - # 根据优化级别调整索引生成 - if optimization == MeshGenerator.OPTIMIZATION_HIGH: - # 高优化:使用三角形条带 - for j in range(resolution_y): - if j % 2 == 0: - # 从左到右 - for i in range(resolution_x): - v0 = j * (resolution_x + 1) + i - v1 = v0 + 1 - v2 = (j + 1) * (resolution_x + 1) + i - v3 = v2 + 1 - - prim.addVertices(v0, v1, v2) - prim.addVertices(v1, v3, v2) - else: - # 从右到左(优化三角形条带) - for i in range(resolution_x - 1, -1, -1): - v0 = j * (resolution_x + 1) + i - v1 = v0 + 1 - v2 = (j + 1) * (resolution_x + 1) + i - v3 = v2 + 1 - - prim.addVertices(v1, v0, v3) - prim.addVertices(v0, v2, v3) - else: - # 标准索引生成 - for j in range(resolution_y): - for i in range(resolution_x): - v0 = j * (resolution_x + 1) + i - v1 = v0 + 1 - v2 = (j + 1) * (resolution_x + 1) + i - v3 = v2 + 1 - - prim.addVertices(v0, v1, v2) - prim.addVertices(v1, v3, v2) - - prim.closePrimitive() - - # 创建几何体 - geom = Geom(vdata) - geom.addPrimitive(prim) - - # 应用优化 - if optimization == MeshGenerator.OPTIMIZATION_MEDIUM: - geom.decompose() - elif optimization == MeshGenerator.OPTIMIZATION_HIGH: - geom.decompose() - geom.unify(0, True) - - # 创建节点 - node = GeomNode('cloth_patch_geom') - node.addGeom(geom) - - # 创建节点路径 - if parent_node: - np = parent_node.attachNewNode(node) - else: - np = NodePath(node) - - return np - - @staticmethod - def create_rope(start_point: Point3, end_point: Point3, - resolution: int, radius: float = 0.1, - parent_node: NodePath = None, - texture_coords: bool = False, - normals: bool = True, - optimization: str = OPTIMIZATION_NONE, - twist: float = 0.0) -> NodePath: - """ - 创建绳索网格 - - Args: - start_point: 起点 - end_point: 终点 - resolution: 分辨率 - radius: 绳索半径 - parent_node: 父节点(可选) - texture_coords: 是否生成纹理坐标 - normals: 是否生成法线 - optimization: 优化级别 - twist: 扭曲角度(弧度) - - Returns: - 包含绳索网格的节点路径 - """ - # 确定顶点格式 - if texture_coords and normals: - format = GeomVertexFormat.getV3n3t2() - elif normals: - format = GeomVertexFormat.getV3n3() - elif texture_coords: - format = GeomVertexFormat.getV3t2() - else: - format = GeomVertexFormat.getV3() - - # 计算绳索方向和长度 - direction = end_point - start_point - length = direction.length() - if length == 0: - length = 0.001 # 避免除零 - direction.normalize() - - # 创建顶点数据 - vdata = GeomVertexData('rope', format, Geom.UHDynamic) - - # 计算顶点数量 - num_vertices = (resolution + 1) * (resolution + 1) - vdata.setNumRows(num_vertices) - - # 创建顶点写入器 - vertex_writer = GeomVertexWriter(vdata, 'vertex') - if normals: - normal_writer = GeomVertexWriter(vdata, 'normal') - if texture_coords: - texcoord_writer = GeomVertexWriter(vdata, 'texcoord') - - # 生成顶点 - # 创建垂直于绳索方向的向量 - if abs(direction.dot(Vec3(0, 0, 1))) < 0.999: - up_vector = Vec3(0, 0, 1) - else: - up_vector = Vec3(1, 0, 0) - - right_vector = direction.cross(up_vector) - right_vector.normalize() - up_vector = right_vector.cross(direction) - up_vector.normalize() - - # 生成圆柱体顶点 - vertices = [] - for i in range(resolution + 1): - # 沿绳索长度方向的位置 - t = i / resolution - center_point = start_point + (end_point - start_point) * t - - # 应用扭曲 - twist_angle = twist * t - - for j in range(resolution + 1): - angle = (j / resolution) * 2 * math.pi + twist_angle - - # 计算圆周上的点 - offset_x = math.cos(angle) * radius - offset_y = math.sin(angle) * radius - - # 计算世界坐标 - world_offset = right_vector * offset_x + up_vector * offset_y - pos = center_point + world_offset - - # 写入顶点数据 - vertex_writer.addData3(pos) - - if normals: - normal_writer.addData3(world_offset.normalized()) - - if texture_coords: - u = j / resolution - v = t - texcoord_writer.addData2(u, v) - - vertices.append(pos) - - # 创建三角形索引 - prim = GeomTriangles(Geom.UHDynamic) - - for i in range(resolution): - for j in range(resolution): - # 计算四个顶点索引 - v0 = i * (resolution + 1) + j - v1 = v0 + 1 - v2 = (i + 1) * (resolution + 1) + j - v3 = v2 + 1 - - # 添加两个三角形 - prim.addVertices(v0, v1, v2) - prim.addVertices(v1, v3, v2) - - prim.closePrimitive() - - # 创建几何体 - geom = Geom(vdata) - geom.addPrimitive(prim) - - # 应用优化 - if optimization != MeshGenerator.OPTIMIZATION_NONE: - geom.decompose() - - # 创建节点 - node = GeomNode('rope_geom') - node.addGeom(geom) - - # 创建节点路径 - if parent_node: - np = parent_node.attachNewNode(node) - else: - np = NodePath(node) - - return np - - @staticmethod - def create_soft_sphere(radius: float, resolution: int = 8, - parent_node: NodePath = None, - texture_coords: bool = True, - normals: bool = True, - optimization: str = OPTIMIZATION_NONE, - hemisphere: bool = False, - uv_mapping: str = "spherical") -> NodePath: - """ - 创建软球网格 - - Args: - radius: 球体半径 - resolution: 分辨率 - parent_node: 父节点(可选) - texture_coords: 是否生成纹理坐标 - normals: 是否生成法线 - optimization: 优化级别 - hemisphere: 是否创建半球 - uv_mapping: UV映射类型 ("spherical", "cylindrical", "planar") - - Returns: - 包含球体网格的节点路径 - """ - # 确定顶点格式 - if texture_coords and normals: - format = GeomVertexFormat.getV3n3t2() - elif normals: - format = GeomVertexFormat.getV3n3() - elif texture_coords: - format = GeomVertexFormat.getV3t2() - else: - format = GeomVertexFormat.getV3() - - # 创建顶点数据 - vdata = GeomVertexData('soft_sphere', format, Geom.UHDynamic) - - # 生成球体顶点 - vertices = [] - texcoords = [] - normals_list = [] - - # 确定纬度范围 - lat_start = 0 if hemisphere else 0 - lat_end = resolution if hemisphere else resolution - - # 生成顶点 - for i in range(lat_start, lat_end + 1): - phi = (i / resolution) * math.pi # 纬度角 - - for j in range(resolution * 2 + 1): - theta = (j / (resolution * 2)) * 2 * math.pi # 经度角 - - # 球面坐标转笛卡尔坐标 - x = radius * math.sin(phi) * math.cos(theta) - y = radius * math.sin(phi) * math.sin(theta) - z = radius * math.cos(phi) - - vertices.append(Point3(x, y, z)) - normals_list.append(Vec3(x, y, z).normalized()) - - # UV坐标计算 - if uv_mapping == "spherical": - u = j / (resolution * 2) - v = i / resolution - elif uv_mapping == "cylindrical": - u = j / (resolution * 2) - v = (z + radius) / (2 * radius) - else: # planar - u = (x + radius) / (2 * radius) - v = (y + radius) / (2 * radius) - - texcoords.append((u, v)) - - # 添加极点(如果需要) - if not hemisphere: - # 北极点 - vertices.append(Point3(0, 0, radius)) - normals_list.append(Vec3(0, 0, 1)) - texcoords.append((0.5, 0.0)) - - # 南极点 - vertices.append(Point3(0, 0, -radius)) - normals_list.append(Vec3(0, 0, -1)) - texcoords.append((0.5, 1.0)) - - # 设置顶点数量 - num_vertices = len(vertices) - vdata.setNumRows(num_vertices) - - # 创建顶点写入器 - vertex_writer = GeomVertexWriter(vdata, 'vertex') - if normals: - normal_writer = GeomVertexWriter(vdata, 'normal') - if texture_coords: - texcoord_writer = GeomVertexWriter(vdata, 'texcoord') - - # 写入顶点数据 - for i, vertex in enumerate(vertices): - vertex_writer.addData3(vertex) - if normals: - normal_writer.addData3(normals_list[i]) - if texture_coords: - texcoord_writer.addData2(texcoords[i][0], texcoords[i][1]) - - # 创建三角形索引 - prim = GeomTriangles(Geom.UHDynamic) - - # 生成面索引 - for i in range(lat_start, lat_end): - for j in range(resolution * 2): - # 计算四个顶点索引 - v0 = i * (resolution * 2 + 1) + j - v1 = v0 + 1 - v2 = (i + 1) * (resolution * 2 + 1) + j - v3 = v2 + 1 - - # 添加两个三角形 - if i > lat_start or not hemisphere: # 不是北极点 - prim.addVertices(v0, v1, v2) - if i < lat_end - 1 or not hemisphere: # 不是南极点 - prim.addVertices(v1, v3, v2) - - prim.closePrimitive() - - # 创建几何体 - geom = Geom(vdata) - geom.addPrimitive(prim) - - # 应用优化 - if optimization != MeshGenerator.OPTIMIZATION_NONE: - geom.decompose() - - # 创建节点 - node = GeomNode('soft_sphere_geom') - node.addGeom(geom) - - # 创建节点路径 - if parent_node: - np = parent_node.attachNewNode(node) - else: - np = NodePath(node) - - return np - - @staticmethod - def create_soft_box(width: float, height: float, depth: float, - resolution_x: int = 1, resolution_y: int = 1, resolution_z: int = 1, - parent_node: NodePath = None, - texture_coords: bool = True, - normals: bool = True, - optimization: str = OPTIMIZATION_NONE, - rounded_corners: bool = False, - corner_radius: float = 0.1) -> NodePath: - """ - 创建软盒网格 - - Args: - width: 宽度 - height: 高度 - depth: 深度 - resolution_x: X方向分辨率 - resolution_y: Y方向分辨率 - resolution_z: Z方向分辨率 - parent_node: 父节点(可选) - texture_coords: 是否生成纹理坐标 - normals: 是否生成法线 - optimization: 优化级别 - rounded_corners: 是否创建圆角 - corner_radius: 圆角半径 - - Returns: - 包含盒体网格的节点路径 - """ - # 确定顶点格式 - if texture_coords and normals: - format = GeomVertexFormat.getV3n3t2() - elif normals: - format = GeomVertexFormat.getV3n3() - elif texture_coords: - format = GeomVertexFormat.getV3t2() - else: - format = GeomVertexFormat.getV3() - - # 创建顶点数据 - vdata = GeomVertexData('soft_box', format, Geom.UHDynamic) - - # 半尺寸 - half_w = width / 2.0 - half_h = height / 2.0 - half_d = depth / 2.0 - - # 生成顶点 - vertices = [] - normals_list = [] - texcoords = [] - - if not rounded_corners: - # 标准盒体顶点 - box_vertices = [ - Point3(-half_w, -half_d, -half_h), # 0 - Point3(half_w, -half_d, -half_h), # 1 - Point3(half_w, half_d, -half_h), # 2 - Point3(-half_w, half_d, -half_h), # 3 - Point3(-half_w, -half_d, half_h), # 4 - Point3(half_w, -half_d, half_h), # 5 - Point3(half_w, half_d, half_h), # 6 - Point3(-half_w, half_d, half_h) # 7 - ] - - # 面的顶点索引(每面4个顶点) - faces = [ - [0, 1, 2, 3], # 底面 (Z-) - [4, 7, 6, 5], # 顶面 (Z+) - [0, 4, 5, 1], # 前面 (Y-) - [2, 6, 7, 3], # 后面 (Y+) - [0, 3, 7, 4], # 左面 (X-) - [1, 5, 6, 2] # 右面 (X+) - ] - - # 法线向量 - face_normals = [ - Vec3(0, 0, -1), # 底面 - Vec3(0, 0, 1), # 顶面 - Vec3(0, -1, 0), # 前面 - Vec3(0, 1, 0), # 后面 - Vec3(-1, 0, 0), # 左面 - Vec3(1, 0, 0) # 右面 - ] - - # UV坐标 - face_uvs = [ - [(0, 0), (1, 0), (1, 1), (0, 1)], # 底面 - [(0, 0), (1, 0), (1, 1), (0, 1)], # 顶面 - [(0, 0), (1, 0), (1, 1), (0, 1)], # 前面 - [(0, 0), (1, 0), (1, 1), (0, 1)], # 后面 - [(0, 0), (1, 0), (1, 1), (0, 1)], # 左面 - [(0, 0), (1, 0), (1, 1), (0, 1)] # 右面 - ] - - # 生成顶点(每个面的顶点独立,以便正确计算法线) - for face_index, face in enumerate(faces): - face_normal = face_normals[face_index] - face_uv = face_uvs[face_index] - for vertex_index, uv_coord in zip(face, face_uv): - vertices.append(box_vertices[vertex_index]) - normals_list.append(face_normal) - texcoords.append(uv_coord) - else: - # 圆角盒体需要更复杂的网格生成 - # 这里实现一个简化的版本 - vertices, normals_list, texcoords = MeshGenerator._generate_rounded_box_vertices( - half_w, half_h, half_d, corner_radius, - resolution_x, resolution_y, resolution_z - ) - - # 设置顶点数量 - num_vertices = len(vertices) - vdata.setNumRows(num_vertices) - - # 创建顶点写入器 - vertex_writer = GeomVertexWriter(vdata, 'vertex') - if normals: - normal_writer = GeomVertexWriter(vdata, 'normal') - if texture_coords: - texcoord_writer = GeomVertexWriter(vdata, 'texcoord') - - # 写入顶点数据 - for i, vertex in enumerate(vertices): - vertex_writer.addData3(vertex) - if normals: - normal_writer.addData3(normals_list[i]) - if texture_coords: - texcoord_writer.addData2(texcoords[i][0], texcoords[i][1]) - - # 创建三角形索引 - prim = GeomTriangles(Geom.UHDynamic) - - if not rounded_corners: - # 每个面生成两个三角形 - for face_index in range(6): - base_index = face_index * 4 - # 第一个三角形 - prim.addVertices(base_index, base_index + 1, base_index + 2) - # 第二个三角形 - prim.addVertices(base_index, base_index + 2, base_index + 3) - else: - # 圆角盒体的索引生成 - # 这里需要更复杂的索引生成逻辑 - # 简化处理:按面分组生成索引 - vertices_per_face = len(vertices) // 6 - for face_index in range(6): - base_index = face_index * vertices_per_face - # 简单的三角剖分 - for i in range(vertices_per_face - 2): - prim.addVertices(base_index, base_index + i + 1, base_index + i + 2) - - prim.closePrimitive() - - # 创建几何体 - geom = Geom(vdata) - geom.addPrimitive(prim) - - # 应用优化 - if optimization != MeshGenerator.OPTIMIZATION_NONE: - geom.decompose() - - # 创建节点 - node = GeomNode('soft_box_geom') - node.addGeom(geom) - - # 创建节点路径 - if parent_node: - np = parent_node.attachNewNode(node) - else: - np = NodePath(node) - - return np - - @staticmethod - def create_capsule(radius: float, height: float, - resolution: int = 8, - parent_node: NodePath = None, - texture_coords: bool = True, - normals: bool = True, - optimization: str = OPTIMIZATION_NONE) -> NodePath: - """ - 创建胶囊网格 - - Args: - radius: 半径 - height: 高度 - resolution: 分辨率 - parent_node: 父节点(可选) - texture_coords: 是否生成纹理坐标 - normals: 是否生成法线 - optimization: 优化级别 - - Returns: - 包含胶囊网格的节点路径 - """ - # 确定顶点格式 - if texture_coords and normals: - format = GeomVertexFormat.getV3n3t2() - elif normals: - format = GeomVertexFormat.getV3n3() - elif texture_coords: - format = GeomVertexFormat.getV3t2() - else: - format = GeomVertexFormat.getV3() - - # 创建顶点数据 - vdata = GeomVertexData('capsule', format, Geom.UHDynamic) - - # 生成胶囊顶点 - vertices = [] - normals_list = [] - texcoords = [] - - half_height = height / 2.0 - - # 生成上半球 - for i in range(resolution // 2 + 1): - phi = (i / (resolution // 2)) * (math.pi / 2) # 0到π/2 - for j in range(resolution * 2 + 1): - theta = (j / (resolution * 2)) * 2 * math.pi - - x = radius * math.sin(phi) * math.cos(theta) - y = radius * math.sin(phi) * math.sin(theta) - z = radius * math.cos(phi) + half_height - - vertices.append(Point3(x, y, z)) - normals_list.append(Vec3(x, y, z - half_height).normalized()) - texcoords.append((j / (resolution * 2), i / (resolution // 2))) - - # 生成圆柱部分 - cylinder_vertices_start = len(vertices) - for i in range(resolution + 1): - for j in range(resolution * 2 + 1): - theta = (j / (resolution * 2)) * 2 * math.pi - z_offset = (i / resolution) * height - half_height - - x = radius * math.cos(theta) - y = radius * math.sin(theta) - z = z_offset - - vertices.append(Point3(x, y, z)) - normals_list.append(Vec3(x, y, 0).normalized()) - texcoords.append((j / (resolution * 2), 0.5 + (i / resolution) * 0.5)) - - # 生成下半球 - sphere_vertices_start = len(vertices) - for i in range(resolution // 2 + 1): - phi = math.pi / 2 + (i / (resolution // 2)) * (math.pi / 2) # π/2到π - for j in range(resolution * 2 + 1): - theta = (j / (resolution * 2)) * 2 * math.pi - - x = radius * math.sin(phi) * math.cos(theta) - y = radius * math.sin(phi) * math.sin(theta) - z = radius * math.cos(phi) - half_height - - vertices.append(Point3(x, y, z)) - normals_list.append(Vec3(x, y, z + half_height).normalized()) - texcoords.append((j / (resolution * 2), 1.0 - i / (resolution // 2))) - - # 设置顶点数量 - num_vertices = len(vertices) - vdata.setNumRows(num_vertices) - - # 创建顶点写入器 - vertex_writer = GeomVertexWriter(vdata, 'vertex') - if normals: - normal_writer = GeomVertexWriter(vdata, 'normal') - if texture_coords: - texcoord_writer = GeomVertexWriter(vdata, 'texcoord') - - # 写入顶点数据 - for i, vertex in enumerate(vertices): - vertex_writer.addData3(vertex) - if normals: - normal_writer.addData3(normals_list[i]) - if texture_coords: - texcoord_writer.addData2(texcoords[i][0], texcoords[i][1]) - - # 创建三角形索引 - prim = GeomTriangles(Geom.UHDynamic) - - # 上半球面索引 - for i in range(resolution // 2): - for j in range(resolution * 2): - v0 = i * (resolution * 2 + 1) + j - v1 = v0 + 1 - v2 = (i + 1) * (resolution * 2 + 1) + j - v3 = v2 + 1 - - prim.addVertices(v0, v1, v2) - prim.addVertices(v1, v3, v2) - - # 圆柱面索引 - for i in range(resolution): - for j in range(resolution * 2): - v0 = cylinder_vertices_start + i * (resolution * 2 + 1) + j - v1 = v0 + 1 - v2 = cylinder_vertices_start + (i + 1) * (resolution * 2 + 1) + j - v3 = v2 + 1 - - prim.addVertices(v0, v1, v2) - prim.addVertices(v1, v3, v2) - - # 下半球面索引 - for i in range(resolution // 2): - for j in range(resolution * 2): - v0 = sphere_vertices_start + i * (resolution * 2 + 1) + j - v1 = v0 + 1 - v2 = sphere_vertices_start + (i + 1) * (resolution * 2 + 1) + j - v3 = v2 + 1 - - prim.addVertices(v0, v1, v2) - prim.addVertices(v1, v3, v2) - - prim.closePrimitive() - - # 创建几何体 - geom = Geom(vdata) - geom.addPrimitive(prim) - - # 应用优化 - if optimization != MeshGenerator.OPTIMIZATION_NONE: - geom.decompose() - - # 创建节点 - node = GeomNode('capsule_geom') - node.addGeom(geom) - - # 创建节点路径 - if parent_node: - np = parent_node.attachNewNode(node) - else: - np = NodePath(node) - - return np - - @staticmethod - def create_cylinder(radius: float, height: float, - resolution: int = 8, - parent_node: NodePath = None, - texture_coords: bool = True, - normals: bool = True, - optimization: str = OPTIMIZATION_NONE, - capped: bool = True) -> NodePath: - """ - 创建圆柱网格 - - Args: - radius: 半径 - height: 高度 - resolution: 分辨率 - parent_node: 父节点(可选) - texture_coords: 是否生成纹理坐标 - normals: 是否生成法线 - optimization: 优化级别 - capped: 是否包含顶盖 - - Returns: - 包含圆柱网格的节点路径 - """ - # 确定顶点格式 - if texture_coords and normals: - format = GeomVertexFormat.getV3n3t2() - elif normals: - format = GeomVertexFormat.getV3n3() - elif texture_coords: - format = GeomVertexFormat.getV3t2() - else: - format = GeomVertexFormat.getV3() - - # 创建顶点数据 - vdata = GeomVertexData('cylinder', format, Geom.UHDynamic) - - # 生成圆柱顶点 - vertices = [] - normals_list = [] - texcoords = [] - - half_height = height / 2.0 - - if capped: - # 底面中心点 - vertices.append(Point3(0, 0, -half_height)) - normals_list.append(Vec3(0, 0, -1)) - texcoords.append((0.5, 0.5)) - - # 顶面中心点 - vertices.append(Point3(0, 0, half_height)) - normals_list.append(Vec3(0, 0, 1)) - texcoords.append((0.5, 0.5)) - - # 生成侧面顶点 - for i in range(2): # 底面和顶面 - z = -half_height if i == 0 else half_height - z_normal = -1 if i == 0 else 1 - - for j in range(resolution + 1): - theta = (j / resolution) * 2 * math.pi - x = radius * math.cos(theta) - y = radius * math.sin(theta) - - vertices.append(Point3(x, y, z)) - normals_list.append(Vec3(x, y, 0).normalized()) - texcoords.append((j / resolution, i)) - - # 设置顶点数量 - num_vertices = len(vertices) - vdata.setNumRows(num_vertices) - - # 创建顶点写入器 - vertex_writer = GeomVertexWriter(vdata, 'vertex') - if normals: - normal_writer = GeomVertexWriter(vdata, 'normal') - if texture_coords: - texcoord_writer = GeomVertexWriter(vdata, 'texcoord') - - # 写入顶点数据 - for i, vertex in enumerate(vertices): - vertex_writer.addData3(vertex) - if normals: - normal_writer.addData3(normals_list[i]) - if texture_coords: - texcoord_writer.addData2(texcoords[i][0], texcoords[i][1]) - - # 创建三角形索引 - prim = GeomTriangles(Geom.UHDynamic) - - # 侧面索引 - for j in range(resolution): - # 底面点索引 - bottom_center = 0 if capped else None - top_center = 1 if capped else None - bottom_start = 2 if capped else 0 - top_start = bottom_start + resolution + 1 - - # 侧面三角形 - v0 = bottom_start + j - v1 = bottom_start + j + 1 - v2 = top_start + j - v3 = top_start + j + 1 - - prim.addVertices(v0, v1, v2) - prim.addVertices(v1, v3, v2) - - # 底面三角形 - if capped: - prim.addVertices(bottom_center, v0, v1) - # 顶面三角形 - prim.addVertices(top_center, v3, v2) - - prim.closePrimitive() - - # 创建几何体 - geom = Geom(vdata) - geom.addPrimitive(prim) - - # 应用优化 - if optimization != MeshGenerator.OPTIMIZATION_NONE: - geom.decompose() - - # 创建节点 - node = GeomNode('cylinder_geom') - node.addGeom(geom) - - # 创建节点路径 - if parent_node: - np = parent_node.attachNewNode(node) - else: - np = NodePath(node) - - return np - - @staticmethod - def create_torus(inner_radius: float, outer_radius: float, - resolution_u: int = 16, resolution_v: int = 8, - parent_node: NodePath = None, - texture_coords: bool = True, - normals: bool = True, - optimization: str = OPTIMIZATION_NONE) -> NodePath: - """ - 创建环面网格 - - Args: - inner_radius: 内半径 - outer_radius: 外半径 - resolution_u: U方向分辨率 - resolution_v: V方向分辨率 - parent_node: 父节点(可选) - texture_coords: 是否生成纹理坐标 - normals: 是否生成法线 - optimization: 优化级别 - - Returns: - 包含环面网格的节点路径 - """ - # 确定顶点格式 - if texture_coords and normals: - format = GeomVertexFormat.getV3n3t2() - elif normals: - format = GeomVertexFormat.getV3n3() - elif texture_coords: - format = GeomVertexFormat.getV3t2() - else: - format = GeomVertexFormat.getV3() - - # 创建顶点数据 - vdata = GeomVertexData('torus', format, Geom.UHDynamic) - - # 生成环面顶点 - vertices = [] - normals_list = [] - texcoords = [] - - # 生成顶点 - for i in range(resolution_u + 1): - u = (i / resolution_u) * 2 * math.pi - - for j in range(resolution_v + 1): - v = (j / resolution_v) * 2 * math.pi - - # 环面参数方程 - x = (outer_radius + inner_radius * math.cos(v)) * math.cos(u) - y = (outer_radius + inner_radius * math.cos(v)) * math.sin(u) - z = inner_radius * math.sin(v) - - vertices.append(Point3(x, y, z)) - - # 计算法线 - # 这是一个简化的法线计算 - normal = Vec3( - math.cos(u) * math.cos(v), - math.sin(u) * math.cos(v), - math.sin(v) - ) - normals_list.append(normal.normalized()) - - # UV坐标 - texcoords.append((i / resolution_u, j / resolution_v)) - - # 设置顶点数量 - num_vertices = len(vertices) - vdata.setNumRows(num_vertices) - - # 创建顶点写入器 - vertex_writer = GeomVertexWriter(vdata, 'vertex') - if normals: - normal_writer = GeomVertexWriter(vdata, 'normal') - if texture_coords: - texcoord_writer = GeomVertexWriter(vdata, 'texcoord') - - # 写入顶点数据 - for i, vertex in enumerate(vertices): - vertex_writer.addData3(vertex) - if normals: - normal_writer.addData3(normals_list[i]) - if texture_coords: - texcoord_writer.addData2(texcoords[i][0], texcoords[i][1]) - - # 创建三角形索引 - prim = GeomTriangles(Geom.UHDynamic) - - # 生成面索引 - for i in range(resolution_u): - for j in range(resolution_v): - # 计算四个顶点索引 - v0 = i * (resolution_v + 1) + j - v1 = v0 + 1 - v2 = (i + 1) * (resolution_v + 1) + j - v3 = v2 + 1 - - # 添加两个三角形 - prim.addVertices(v0, v1, v2) - prim.addVertices(v1, v3, v2) - - prim.closePrimitive() - - # 创建几何体 - geom = Geom(vdata) - geom.addPrimitive(prim) - - # 应用优化 - if optimization != MeshGenerator.OPTIMIZATION_NONE: - geom.decompose() - - # 创建节点 - node = GeomNode('torus_geom') - node.addGeom(geom) - - # 创建节点路径 - if parent_node: - np = parent_node.attachNewNode(node) - else: - np = NodePath(node) - - return np - - @staticmethod - def create_plane(width: float, height: float, - resolution_x: int, resolution_y: int, - parent_node: NodePath = None, - texture_coords: bool = True, - normals: bool = True, - optimization: str = OPTIMIZATION_NONE) -> NodePath: - """ - 创建平面网格 - - Args: - width: 宽度 - height: 高度 - resolution_x: X方向分辨率 - resolution_y: Y方向分辨率 - parent_node: 父节点(可选) - texture_coords: 是否生成纹理坐标 - normals: 是否生成法线 - optimization: 优化级别 - - Returns: - 包含平面网格的节点路径 - """ - return MeshGenerator.create_cloth_patch( - width, height, resolution_x, resolution_y, - parent_node, texture_coords, normals, optimization - ) - - @staticmethod - def create_custom_mesh(vertices: List[Point3], - indices: List[int], - normals: List[Vec3] = None, - texcoords: List[Tuple[float, float]] = None, - parent_node: NodePath = None, - optimization: str = OPTIMIZATION_NONE) -> NodePath: - """ - 创建自定义网格 - - Args: - vertices: 顶点列表 - indices: 索引列表 - normals: 法线列表(可选) - texcoords: 纹理坐标列表(可选) - parent_node: 父节点(可选) - optimization: 优化级别 - - Returns: - 包含自定义网格的节点路径 - """ - # 确定顶点格式 - has_normals = normals is not None and len(normals) == len(vertices) - has_texcoords = texcoords is not None and len(texcoords) == len(vertices) - - if has_normals and has_texcoords: - format = GeomVertexFormat.getV3n3t2() - elif has_normals: - format = GeomVertexFormat.getV3n3() - elif has_texcoords: - format = GeomVertexFormat.getV3t2() - else: - format = GeomVertexFormat.getV3() - - # 创建顶点数据 - vdata = GeomVertexData('custom_mesh', format, Geom.UHDynamic) - - # 设置顶点数量 - num_vertices = len(vertices) - vdata.setNumRows(num_vertices) - - # 创建顶点写入器 - vertex_writer = GeomVertexWriter(vdata, 'vertex') - if has_normals: - normal_writer = GeomVertexWriter(vdata, 'normal') - if has_texcoords: - texcoord_writer = GeomVertexWriter(vdata, 'texcoord') - - # 写入顶点数据 - for i, vertex in enumerate(vertices): - vertex_writer.addData3(vertex) - if has_normals: - normal_writer.addData3(normals[i]) - if has_texcoords: - texcoord_writer.addData2(texcoords[i][0], texcoords[i][1]) - - # 创建三角形索引 - prim = GeomTriangles(Geom.UHDynamic) - - # 添加索引 - for i in range(0, len(indices), 3): - if i + 2 < len(indices): - prim.addVertices(indices[i], indices[i+1], indices[i+2]) - - prim.closePrimitive() - - # 创建几何体 - geom = Geom(vdata) - geom.addPrimitive(prim) - - # 应用优化 - if optimization != MeshGenerator.OPTIMIZATION_NONE: - geom.decompose() - - # 创建节点 - node = GeomNode('custom_mesh_geom') - node.addGeom(geom) - - # 创建节点路径 - if parent_node: - np = parent_node.attachNewNode(node) - else: - np = NodePath(node) - - return np - - @staticmethod - def _generate_rounded_box_vertices(half_w: float, half_h: float, half_d: float, - corner_radius: float, - resolution_x: int, resolution_y: int, resolution_z: int) -> Tuple[List[Point3], List[Vec3], List[Tuple[float, float]]]: - """ - 生成圆角盒体的顶点 - - Args: - half_w: 半宽度 - half_h: 半高度 - half_d: 半深度 - corner_radius: 圆角半径 - resolution_x: X方向分辨率 - resolution_y: Y方向分辨率 - resolution_z: Z方向分辨率 - - Returns: - (顶点列表, 法线列表, 纹理坐标列表) - """ - vertices = [] - normals = [] - texcoords = [] - - # 这里实现一个简化的圆角盒体顶点生成 - # 实际实现需要更复杂的几何计算 - - # 六个面的顶点生成(简化版本) - for face in range(6): - # 每个面生成resolution_x * resolution_y个顶点 - for i in range(resolution_x + 1): - for j in range(resolution_y + 1): - # 根据面的方向计算顶点位置 - if face == 0: # 前面 (Z-) - x = (i / resolution_x) * 2 * half_w - half_w - y = (j / resolution_y) * 2 * half_h - half_h - z = -half_d - elif face == 1: # 后面 (Z+) - x = (i / resolution_x) * 2 * half_w - half_w - y = (j / resolution_y) * 2 * half_h - half_h - z = half_d - elif face == 2: # 左面 (X-) - x = -half_w - y = (i / resolution_x) * 2 * half_h - half_h - z = (j / resolution_y) * 2 * half_d - half_d - elif face == 3: # 右面 (X+) - x = half_w - y = (i / resolution_x) * 2 * half_h - half_h - z = (j / resolution_y) * 2 * half_d - half_d - elif face == 4: # 底面 (Y-) - x = (i / resolution_x) * 2 * half_w - half_w - y = -half_h - z = (j / resolution_y) * 2 * half_d - half_d - else: # 顶面 (Y+) - x = (i / resolution_x) * 2 * half_w - half_w - y = half_h - z = (j / resolution_y) * 2 * half_d - half_d - - vertices.append(Point3(x, y, z)) - # 简化的法线计算 - normal = Vec3(0, 0, 1) # 需要根据面的方向调整 - normals.append(normal) - # 简化的纹理坐标 - texcoords.append((i / resolution_x, j / resolution_y)) - - return vertices, normals, texcoords - - @staticmethod - def get_mesh_statistics(node_path: NodePath) -> Dict[str, Any]: - """ - 获取网格统计信息 - - Args: - node_path: 网格节点路径 - - Returns: - 统计信息字典 - """ - stats = { - 'vertex_count': 0, - 'face_count': 0, - 'triangle_count': 0, - 'edge_count': 0, - 'bounding_box': { - 'min': Point3(float('inf'), float('inf'), float('inf')), - 'max': Point3(float('-inf'), float('-inf'), float('-inf')) - }, - 'surface_area': 0.0, - 'volume': 0.0 - } - - # 查找几何节点 - geom_nodes = node_path.findAllMatches('**/+GeomNode') - for geom_node_path in geom_nodes: - geom_node = geom_node_path.node() - for i in range(geom_node.getNumGeoms()): - geom = geom_node.getGeom(i) - vdata = geom.getVertexData() - - # 顶点数量 - stats['vertex_count'] += vdata.getNumRows() - - # 更新包围盒 - vertex_reader = GeomVertexReader(vdata, 'vertex') - while not vertex_reader.isAtEnd(): - vertex = vertex_reader.getData3() - stats['bounding_box']['min'].set( - min(stats['bounding_box']['min'][0], vertex[0]), - min(stats['bounding_box']['min'][1], vertex[1]), - min(stats['bounding_box']['min'][2], vertex[2]) - ) - stats['bounding_box']['max'].set( - max(stats['bounding_box']['max'][0], vertex[0]), - max(stats['bounding_box']['max'][1], vertex[1]), - max(stats['bounding_box']['max'][2], vertex[2]) - ) - - # 面和三角形数量 - for prim in geom.getPrimitives(): - if isinstance(prim, GeomTriangles): - stats['triangle_count'] += prim.getNumFaces() - stats['face_count'] += prim.getNumFaces() - else: - stats['face_count'] += prim.getNumFaces() - - # 计算表面积和体积的简化版本 - bbox = stats['bounding_box'] - width = bbox['max'][0] - bbox['min'][0] - height = bbox['max'][1] - bbox['min'][1] - depth = bbox['max'][2] - bbox['min'][2] - stats['surface_area'] = 2 * (width * height + width * depth + height * depth) - stats['volume'] = width * height * depth - - return stats - - @staticmethod - def optimize_mesh(node_path: NodePath, - target_vertex_count: int = None, - preserve_normals: bool = True, - preserve_texcoords: bool = True) -> NodePath: - """ - 优化网格 - - Args: - node_path: 网格节点路径 - target_vertex_count: 目标顶点数量 - preserve_normals: 是否保持法线 - preserve_texcoords: 是否保持纹理坐标 - - Returns: - 优化后的网格节点路径 - """ - # 这里应该实现网格优化算法 - # 简化版本:返回原始节点 - print("⚠ 网格优化功能需要更复杂的实现") - return node_path - - @staticmethod - def generate_collision_mesh(source_mesh: NodePath, - type: str = "convex") -> NodePath: - """ - 生成碰撞网格 - - Args: - source_mesh: 源网格 - type: 碰撞网格类型 ("convex", "triangle", "sphere", "box", "capsule") - - Returns: - 碰撞网格节点路径 - """ - # 这里应该实现碰撞网格生成算法 - # 简化版本:返回原始节点的副本 - print("⚠ 碰撞网格生成功能需要更复杂的实现") +""" +网格生成工具 +提供用于生成布料和软体物理模拟所需网格的工具函数 +包含高级网格生成和优化功能 +""" + +from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter +from panda3d.core import GeomTriangles, Geom, GeomNode, NodePath +from panda3d.core import Vec3, Point3, Vec4 +import math +from typing import List, Tuple, Optional, Dict, Any +import numpy as np + + +class MeshGenerator: + """ + 网格生成器 + 提供各种网格生成函数,支持高级几何体和优化 + """ + + # 网格类型常量 + TYPE_CLOTH = "cloth" + TYPE_SPHERE = "sphere" + TYPE_BOX = "box" + TYPE_CAPSULE = "capsule" + TYPE_CYLINDER = "cylinder" + TYPE_ROPE = "rope" + TYPE_TORUS = "torus" + TYPE_PLANE = "plane" + TYPE_CUSTOM = "custom" + + # 优化级别 + OPTIMIZATION_NONE = "none" + OPTIMIZATION_LOW = "low" + OPTIMIZATION_MEDIUM = "medium" + OPTIMIZATION_HIGH = "high" + + @staticmethod + def create_cloth_patch(width: float, height: float, + resolution_x: int, resolution_y: int, + parent_node: NodePath = None, + texture_coords: bool = True, + normals: bool = True, + optimization: str = OPTIMIZATION_NONE, + uv_scale: float = 1.0, + deformation_pattern: str = "flat") -> NodePath: + """ + 创建布料片网格 + + Args: + width: 布料宽度 + height: 布料高度 + resolution_x: X方向分辨率 + resolution_y: Y方向分辨率 + parent_node: 父节点(可选) + texture_coords: 是否生成纹理坐标 + normals: 是否生成法线 + optimization: 优化级别 + uv_scale: UV缩放因子 + deformation_pattern: 变形模式 ("flat", "wave", "sine", "custom") + + Returns: + 包含布料网格的节点路径 + """ + # 确定顶点格式 + if texture_coords and normals: + format = GeomVertexFormat.getV3n3t2() + elif normals: + format = GeomVertexFormat.getV3n3() + elif texture_coords: + format = GeomVertexFormat.getV3t2() + else: + format = GeomVertexFormat.getV3() + + # 创建顶点数据 + vdata = GeomVertexData('cloth_patch', format, Geom.UHDynamic) + + # 计算顶点数量 + num_vertices = (resolution_x + 1) * (resolution_y + 1) + vdata.setNumRows(num_vertices) + + # 创建顶点写入器 + vertex_writer = GeomVertexWriter(vdata, 'vertex') + if normals: + normal_writer = GeomVertexWriter(vdata, 'normal') + if texture_coords: + texcoord_writer = GeomVertexWriter(vdata, 'texcoord') + + # 生成顶点 + half_width = width / 2.0 + half_height = height / 2.0 + + vertices = [] + for j in range(resolution_y + 1): + for i in range(resolution_x + 1): + # 计算基础顶点位置 + x = (i / resolution_x) * width - half_width + y = (j / resolution_y) * height - half_height + + # 根据变形模式调整Z坐标 + z = 0.0 + if deformation_pattern == "wave": + z = math.sin(x * 2) * 0.1 + math.cos(y * 2) * 0.1 + elif deformation_pattern == "sine": + z = math.sin(math.sqrt(x*x + y*y) * 3) * 0.05 + elif deformation_pattern == "custom": + # 自定义变形模式可以在这里扩展 + z = 0.0 + + # 写入顶点数据 + vertex_writer.addData3(x, y, z) + + if normals: + # 计算法线(对于变形表面) + if deformation_pattern == "flat": + normal_writer.addData3(0, 0, 1) + else: + # 对于变形表面,计算近似法线 + # 这里使用简化的法线计算 + normal_writer.addData3(0, 0, 1) + + if texture_coords: + u = (i / resolution_x) * uv_scale + v = (j / resolution_y) * uv_scale + texcoord_writer.addData2(u, v) + + vertices.append(Point3(x, y, z)) + + # 创建三角形索引 + prim = GeomTriangles(Geom.UHDynamic) + + # 根据优化级别调整索引生成 + if optimization == MeshGenerator.OPTIMIZATION_HIGH: + # 高优化:使用三角形条带 + for j in range(resolution_y): + if j % 2 == 0: + # 从左到右 + for i in range(resolution_x): + v0 = j * (resolution_x + 1) + i + v1 = v0 + 1 + v2 = (j + 1) * (resolution_x + 1) + i + v3 = v2 + 1 + + prim.addVertices(v0, v1, v2) + prim.addVertices(v1, v3, v2) + else: + # 从右到左(优化三角形条带) + for i in range(resolution_x - 1, -1, -1): + v0 = j * (resolution_x + 1) + i + v1 = v0 + 1 + v2 = (j + 1) * (resolution_x + 1) + i + v3 = v2 + 1 + + prim.addVertices(v1, v0, v3) + prim.addVertices(v0, v2, v3) + else: + # 标准索引生成 + for j in range(resolution_y): + for i in range(resolution_x): + v0 = j * (resolution_x + 1) + i + v1 = v0 + 1 + v2 = (j + 1) * (resolution_x + 1) + i + v3 = v2 + 1 + + prim.addVertices(v0, v1, v2) + prim.addVertices(v1, v3, v2) + + prim.closePrimitive() + + # 创建几何体 + geom = Geom(vdata) + geom.addPrimitive(prim) + + # 应用优化 + if optimization == MeshGenerator.OPTIMIZATION_MEDIUM: + geom.decompose() + elif optimization == MeshGenerator.OPTIMIZATION_HIGH: + geom.decompose() + geom.unify(0, True) + + # 创建节点 + node = GeomNode('cloth_patch_geom') + node.addGeom(geom) + + # 创建节点路径 + if parent_node: + np = parent_node.attachNewNode(node) + else: + np = NodePath(node) + + return np + + @staticmethod + def create_rope(start_point: Point3, end_point: Point3, + resolution: int, radius: float = 0.1, + parent_node: NodePath = None, + texture_coords: bool = False, + normals: bool = True, + optimization: str = OPTIMIZATION_NONE, + twist: float = 0.0) -> NodePath: + """ + 创建绳索网格 + + Args: + start_point: 起点 + end_point: 终点 + resolution: 分辨率 + radius: 绳索半径 + parent_node: 父节点(可选) + texture_coords: 是否生成纹理坐标 + normals: 是否生成法线 + optimization: 优化级别 + twist: 扭曲角度(弧度) + + Returns: + 包含绳索网格的节点路径 + """ + # 确定顶点格式 + if texture_coords and normals: + format = GeomVertexFormat.getV3n3t2() + elif normals: + format = GeomVertexFormat.getV3n3() + elif texture_coords: + format = GeomVertexFormat.getV3t2() + else: + format = GeomVertexFormat.getV3() + + # 计算绳索方向和长度 + direction = end_point - start_point + length = direction.length() + if length == 0: + length = 0.001 # 避免除零 + direction.normalize() + + # 创建顶点数据 + vdata = GeomVertexData('rope', format, Geom.UHDynamic) + + # 计算顶点数量 + num_vertices = (resolution + 1) * (resolution + 1) + vdata.setNumRows(num_vertices) + + # 创建顶点写入器 + vertex_writer = GeomVertexWriter(vdata, 'vertex') + if normals: + normal_writer = GeomVertexWriter(vdata, 'normal') + if texture_coords: + texcoord_writer = GeomVertexWriter(vdata, 'texcoord') + + # 生成顶点 + # 创建垂直于绳索方向的向量 + if abs(direction.dot(Vec3(0, 0, 1))) < 0.999: + up_vector = Vec3(0, 0, 1) + else: + up_vector = Vec3(1, 0, 0) + + right_vector = direction.cross(up_vector) + right_vector.normalize() + up_vector = right_vector.cross(direction) + up_vector.normalize() + + # 生成圆柱体顶点 + vertices = [] + for i in range(resolution + 1): + # 沿绳索长度方向的位置 + t = i / resolution + center_point = start_point + (end_point - start_point) * t + + # 应用扭曲 + twist_angle = twist * t + + for j in range(resolution + 1): + angle = (j / resolution) * 2 * math.pi + twist_angle + + # 计算圆周上的点 + offset_x = math.cos(angle) * radius + offset_y = math.sin(angle) * radius + + # 计算世界坐标 + world_offset = right_vector * offset_x + up_vector * offset_y + pos = center_point + world_offset + + # 写入顶点数据 + vertex_writer.addData3(pos) + + if normals: + normal_writer.addData3(world_offset.normalized()) + + if texture_coords: + u = j / resolution + v = t + texcoord_writer.addData2(u, v) + + vertices.append(pos) + + # 创建三角形索引 + prim = GeomTriangles(Geom.UHDynamic) + + for i in range(resolution): + for j in range(resolution): + # 计算四个顶点索引 + v0 = i * (resolution + 1) + j + v1 = v0 + 1 + v2 = (i + 1) * (resolution + 1) + j + v3 = v2 + 1 + + # 添加两个三角形 + prim.addVertices(v0, v1, v2) + prim.addVertices(v1, v3, v2) + + prim.closePrimitive() + + # 创建几何体 + geom = Geom(vdata) + geom.addPrimitive(prim) + + # 应用优化 + if optimization != MeshGenerator.OPTIMIZATION_NONE: + geom.decompose() + + # 创建节点 + node = GeomNode('rope_geom') + node.addGeom(geom) + + # 创建节点路径 + if parent_node: + np = parent_node.attachNewNode(node) + else: + np = NodePath(node) + + return np + + @staticmethod + def create_soft_sphere(radius: float, resolution: int = 8, + parent_node: NodePath = None, + texture_coords: bool = True, + normals: bool = True, + optimization: str = OPTIMIZATION_NONE, + hemisphere: bool = False, + uv_mapping: str = "spherical") -> NodePath: + """ + 创建软球网格 + + Args: + radius: 球体半径 + resolution: 分辨率 + parent_node: 父节点(可选) + texture_coords: 是否生成纹理坐标 + normals: 是否生成法线 + optimization: 优化级别 + hemisphere: 是否创建半球 + uv_mapping: UV映射类型 ("spherical", "cylindrical", "planar") + + Returns: + 包含球体网格的节点路径 + """ + # 确定顶点格式 + if texture_coords and normals: + format = GeomVertexFormat.getV3n3t2() + elif normals: + format = GeomVertexFormat.getV3n3() + elif texture_coords: + format = GeomVertexFormat.getV3t2() + else: + format = GeomVertexFormat.getV3() + + # 创建顶点数据 + vdata = GeomVertexData('soft_sphere', format, Geom.UHDynamic) + + # 生成球体顶点 + vertices = [] + texcoords = [] + normals_list = [] + + # 确定纬度范围 + lat_start = 0 if hemisphere else 0 + lat_end = resolution if hemisphere else resolution + + # 生成顶点 + for i in range(lat_start, lat_end + 1): + phi = (i / resolution) * math.pi # 纬度角 + + for j in range(resolution * 2 + 1): + theta = (j / (resolution * 2)) * 2 * math.pi # 经度角 + + # 球面坐标转笛卡尔坐标 + x = radius * math.sin(phi) * math.cos(theta) + y = radius * math.sin(phi) * math.sin(theta) + z = radius * math.cos(phi) + + vertices.append(Point3(x, y, z)) + normals_list.append(Vec3(x, y, z).normalized()) + + # UV坐标计算 + if uv_mapping == "spherical": + u = j / (resolution * 2) + v = i / resolution + elif uv_mapping == "cylindrical": + u = j / (resolution * 2) + v = (z + radius) / (2 * radius) + else: # planar + u = (x + radius) / (2 * radius) + v = (y + radius) / (2 * radius) + + texcoords.append((u, v)) + + # 添加极点(如果需要) + if not hemisphere: + # 北极点 + vertices.append(Point3(0, 0, radius)) + normals_list.append(Vec3(0, 0, 1)) + texcoords.append((0.5, 0.0)) + + # 南极点 + vertices.append(Point3(0, 0, -radius)) + normals_list.append(Vec3(0, 0, -1)) + texcoords.append((0.5, 1.0)) + + # 设置顶点数量 + num_vertices = len(vertices) + vdata.setNumRows(num_vertices) + + # 创建顶点写入器 + vertex_writer = GeomVertexWriter(vdata, 'vertex') + if normals: + normal_writer = GeomVertexWriter(vdata, 'normal') + if texture_coords: + texcoord_writer = GeomVertexWriter(vdata, 'texcoord') + + # 写入顶点数据 + for i, vertex in enumerate(vertices): + vertex_writer.addData3(vertex) + if normals: + normal_writer.addData3(normals_list[i]) + if texture_coords: + texcoord_writer.addData2(texcoords[i][0], texcoords[i][1]) + + # 创建三角形索引 + prim = GeomTriangles(Geom.UHDynamic) + + # 生成面索引 + for i in range(lat_start, lat_end): + for j in range(resolution * 2): + # 计算四个顶点索引 + v0 = i * (resolution * 2 + 1) + j + v1 = v0 + 1 + v2 = (i + 1) * (resolution * 2 + 1) + j + v3 = v2 + 1 + + # 添加两个三角形 + if i > lat_start or not hemisphere: # 不是北极点 + prim.addVertices(v0, v1, v2) + if i < lat_end - 1 or not hemisphere: # 不是南极点 + prim.addVertices(v1, v3, v2) + + prim.closePrimitive() + + # 创建几何体 + geom = Geom(vdata) + geom.addPrimitive(prim) + + # 应用优化 + if optimization != MeshGenerator.OPTIMIZATION_NONE: + geom.decompose() + + # 创建节点 + node = GeomNode('soft_sphere_geom') + node.addGeom(geom) + + # 创建节点路径 + if parent_node: + np = parent_node.attachNewNode(node) + else: + np = NodePath(node) + + return np + + @staticmethod + def create_soft_box(width: float, height: float, depth: float, + resolution_x: int = 1, resolution_y: int = 1, resolution_z: int = 1, + parent_node: NodePath = None, + texture_coords: bool = True, + normals: bool = True, + optimization: str = OPTIMIZATION_NONE, + rounded_corners: bool = False, + corner_radius: float = 0.1) -> NodePath: + """ + 创建软盒网格 + + Args: + width: 宽度 + height: 高度 + depth: 深度 + resolution_x: X方向分辨率 + resolution_y: Y方向分辨率 + resolution_z: Z方向分辨率 + parent_node: 父节点(可选) + texture_coords: 是否生成纹理坐标 + normals: 是否生成法线 + optimization: 优化级别 + rounded_corners: 是否创建圆角 + corner_radius: 圆角半径 + + Returns: + 包含盒体网格的节点路径 + """ + # 确定顶点格式 + if texture_coords and normals: + format = GeomVertexFormat.getV3n3t2() + elif normals: + format = GeomVertexFormat.getV3n3() + elif texture_coords: + format = GeomVertexFormat.getV3t2() + else: + format = GeomVertexFormat.getV3() + + # 创建顶点数据 + vdata = GeomVertexData('soft_box', format, Geom.UHDynamic) + + # 半尺寸 + half_w = width / 2.0 + half_h = height / 2.0 + half_d = depth / 2.0 + + # 生成顶点 + vertices = [] + normals_list = [] + texcoords = [] + + if not rounded_corners: + # 标准盒体顶点 + box_vertices = [ + Point3(-half_w, -half_d, -half_h), # 0 + Point3(half_w, -half_d, -half_h), # 1 + Point3(half_w, half_d, -half_h), # 2 + Point3(-half_w, half_d, -half_h), # 3 + Point3(-half_w, -half_d, half_h), # 4 + Point3(half_w, -half_d, half_h), # 5 + Point3(half_w, half_d, half_h), # 6 + Point3(-half_w, half_d, half_h) # 7 + ] + + # 面的顶点索引(每面4个顶点) + faces = [ + [0, 1, 2, 3], # 底面 (Z-) + [4, 7, 6, 5], # 顶面 (Z+) + [0, 4, 5, 1], # 前面 (Y-) + [2, 6, 7, 3], # 后面 (Y+) + [0, 3, 7, 4], # 左面 (X-) + [1, 5, 6, 2] # 右面 (X+) + ] + + # 法线向量 + face_normals = [ + Vec3(0, 0, -1), # 底面 + Vec3(0, 0, 1), # 顶面 + Vec3(0, -1, 0), # 前面 + Vec3(0, 1, 0), # 后面 + Vec3(-1, 0, 0), # 左面 + Vec3(1, 0, 0) # 右面 + ] + + # UV坐标 + face_uvs = [ + [(0, 0), (1, 0), (1, 1), (0, 1)], # 底面 + [(0, 0), (1, 0), (1, 1), (0, 1)], # 顶面 + [(0, 0), (1, 0), (1, 1), (0, 1)], # 前面 + [(0, 0), (1, 0), (1, 1), (0, 1)], # 后面 + [(0, 0), (1, 0), (1, 1), (0, 1)], # 左面 + [(0, 0), (1, 0), (1, 1), (0, 1)] # 右面 + ] + + # 生成顶点(每个面的顶点独立,以便正确计算法线) + for face_index, face in enumerate(faces): + face_normal = face_normals[face_index] + face_uv = face_uvs[face_index] + for vertex_index, uv_coord in zip(face, face_uv): + vertices.append(box_vertices[vertex_index]) + normals_list.append(face_normal) + texcoords.append(uv_coord) + else: + # 圆角盒体需要更复杂的网格生成 + # 这里实现一个简化的版本 + vertices, normals_list, texcoords = MeshGenerator._generate_rounded_box_vertices( + half_w, half_h, half_d, corner_radius, + resolution_x, resolution_y, resolution_z + ) + + # 设置顶点数量 + num_vertices = len(vertices) + vdata.setNumRows(num_vertices) + + # 创建顶点写入器 + vertex_writer = GeomVertexWriter(vdata, 'vertex') + if normals: + normal_writer = GeomVertexWriter(vdata, 'normal') + if texture_coords: + texcoord_writer = GeomVertexWriter(vdata, 'texcoord') + + # 写入顶点数据 + for i, vertex in enumerate(vertices): + vertex_writer.addData3(vertex) + if normals: + normal_writer.addData3(normals_list[i]) + if texture_coords: + texcoord_writer.addData2(texcoords[i][0], texcoords[i][1]) + + # 创建三角形索引 + prim = GeomTriangles(Geom.UHDynamic) + + if not rounded_corners: + # 每个面生成两个三角形 + for face_index in range(6): + base_index = face_index * 4 + # 第一个三角形 + prim.addVertices(base_index, base_index + 1, base_index + 2) + # 第二个三角形 + prim.addVertices(base_index, base_index + 2, base_index + 3) + else: + # 圆角盒体的索引生成 + # 这里需要更复杂的索引生成逻辑 + # 简化处理:按面分组生成索引 + vertices_per_face = len(vertices) // 6 + for face_index in range(6): + base_index = face_index * vertices_per_face + # 简单的三角剖分 + for i in range(vertices_per_face - 2): + prim.addVertices(base_index, base_index + i + 1, base_index + i + 2) + + prim.closePrimitive() + + # 创建几何体 + geom = Geom(vdata) + geom.addPrimitive(prim) + + # 应用优化 + if optimization != MeshGenerator.OPTIMIZATION_NONE: + geom.decompose() + + # 创建节点 + node = GeomNode('soft_box_geom') + node.addGeom(geom) + + # 创建节点路径 + if parent_node: + np = parent_node.attachNewNode(node) + else: + np = NodePath(node) + + return np + + @staticmethod + def create_capsule(radius: float, height: float, + resolution: int = 8, + parent_node: NodePath = None, + texture_coords: bool = True, + normals: bool = True, + optimization: str = OPTIMIZATION_NONE) -> NodePath: + """ + 创建胶囊网格 + + Args: + radius: 半径 + height: 高度 + resolution: 分辨率 + parent_node: 父节点(可选) + texture_coords: 是否生成纹理坐标 + normals: 是否生成法线 + optimization: 优化级别 + + Returns: + 包含胶囊网格的节点路径 + """ + # 确定顶点格式 + if texture_coords and normals: + format = GeomVertexFormat.getV3n3t2() + elif normals: + format = GeomVertexFormat.getV3n3() + elif texture_coords: + format = GeomVertexFormat.getV3t2() + else: + format = GeomVertexFormat.getV3() + + # 创建顶点数据 + vdata = GeomVertexData('capsule', format, Geom.UHDynamic) + + # 生成胶囊顶点 + vertices = [] + normals_list = [] + texcoords = [] + + half_height = height / 2.0 + + # 生成上半球 + for i in range(resolution // 2 + 1): + phi = (i / (resolution // 2)) * (math.pi / 2) # 0到π/2 + for j in range(resolution * 2 + 1): + theta = (j / (resolution * 2)) * 2 * math.pi + + x = radius * math.sin(phi) * math.cos(theta) + y = radius * math.sin(phi) * math.sin(theta) + z = radius * math.cos(phi) + half_height + + vertices.append(Point3(x, y, z)) + normals_list.append(Vec3(x, y, z - half_height).normalized()) + texcoords.append((j / (resolution * 2), i / (resolution // 2))) + + # 生成圆柱部分 + cylinder_vertices_start = len(vertices) + for i in range(resolution + 1): + for j in range(resolution * 2 + 1): + theta = (j / (resolution * 2)) * 2 * math.pi + z_offset = (i / resolution) * height - half_height + + x = radius * math.cos(theta) + y = radius * math.sin(theta) + z = z_offset + + vertices.append(Point3(x, y, z)) + normals_list.append(Vec3(x, y, 0).normalized()) + texcoords.append((j / (resolution * 2), 0.5 + (i / resolution) * 0.5)) + + # 生成下半球 + sphere_vertices_start = len(vertices) + for i in range(resolution // 2 + 1): + phi = math.pi / 2 + (i / (resolution // 2)) * (math.pi / 2) # π/2到π + for j in range(resolution * 2 + 1): + theta = (j / (resolution * 2)) * 2 * math.pi + + x = radius * math.sin(phi) * math.cos(theta) + y = radius * math.sin(phi) * math.sin(theta) + z = radius * math.cos(phi) - half_height + + vertices.append(Point3(x, y, z)) + normals_list.append(Vec3(x, y, z + half_height).normalized()) + texcoords.append((j / (resolution * 2), 1.0 - i / (resolution // 2))) + + # 设置顶点数量 + num_vertices = len(vertices) + vdata.setNumRows(num_vertices) + + # 创建顶点写入器 + vertex_writer = GeomVertexWriter(vdata, 'vertex') + if normals: + normal_writer = GeomVertexWriter(vdata, 'normal') + if texture_coords: + texcoord_writer = GeomVertexWriter(vdata, 'texcoord') + + # 写入顶点数据 + for i, vertex in enumerate(vertices): + vertex_writer.addData3(vertex) + if normals: + normal_writer.addData3(normals_list[i]) + if texture_coords: + texcoord_writer.addData2(texcoords[i][0], texcoords[i][1]) + + # 创建三角形索引 + prim = GeomTriangles(Geom.UHDynamic) + + # 上半球面索引 + for i in range(resolution // 2): + for j in range(resolution * 2): + v0 = i * (resolution * 2 + 1) + j + v1 = v0 + 1 + v2 = (i + 1) * (resolution * 2 + 1) + j + v3 = v2 + 1 + + prim.addVertices(v0, v1, v2) + prim.addVertices(v1, v3, v2) + + # 圆柱面索引 + for i in range(resolution): + for j in range(resolution * 2): + v0 = cylinder_vertices_start + i * (resolution * 2 + 1) + j + v1 = v0 + 1 + v2 = cylinder_vertices_start + (i + 1) * (resolution * 2 + 1) + j + v3 = v2 + 1 + + prim.addVertices(v0, v1, v2) + prim.addVertices(v1, v3, v2) + + # 下半球面索引 + for i in range(resolution // 2): + for j in range(resolution * 2): + v0 = sphere_vertices_start + i * (resolution * 2 + 1) + j + v1 = v0 + 1 + v2 = sphere_vertices_start + (i + 1) * (resolution * 2 + 1) + j + v3 = v2 + 1 + + prim.addVertices(v0, v1, v2) + prim.addVertices(v1, v3, v2) + + prim.closePrimitive() + + # 创建几何体 + geom = Geom(vdata) + geom.addPrimitive(prim) + + # 应用优化 + if optimization != MeshGenerator.OPTIMIZATION_NONE: + geom.decompose() + + # 创建节点 + node = GeomNode('capsule_geom') + node.addGeom(geom) + + # 创建节点路径 + if parent_node: + np = parent_node.attachNewNode(node) + else: + np = NodePath(node) + + return np + + @staticmethod + def create_cylinder(radius: float, height: float, + resolution: int = 8, + parent_node: NodePath = None, + texture_coords: bool = True, + normals: bool = True, + optimization: str = OPTIMIZATION_NONE, + capped: bool = True) -> NodePath: + """ + 创建圆柱网格 + + Args: + radius: 半径 + height: 高度 + resolution: 分辨率 + parent_node: 父节点(可选) + texture_coords: 是否生成纹理坐标 + normals: 是否生成法线 + optimization: 优化级别 + capped: 是否包含顶盖 + + Returns: + 包含圆柱网格的节点路径 + """ + # 确定顶点格式 + if texture_coords and normals: + format = GeomVertexFormat.getV3n3t2() + elif normals: + format = GeomVertexFormat.getV3n3() + elif texture_coords: + format = GeomVertexFormat.getV3t2() + else: + format = GeomVertexFormat.getV3() + + # 创建顶点数据 + vdata = GeomVertexData('cylinder', format, Geom.UHDynamic) + + # 生成圆柱顶点 + vertices = [] + normals_list = [] + texcoords = [] + + half_height = height / 2.0 + + if capped: + # 底面中心点 + vertices.append(Point3(0, 0, -half_height)) + normals_list.append(Vec3(0, 0, -1)) + texcoords.append((0.5, 0.5)) + + # 顶面中心点 + vertices.append(Point3(0, 0, half_height)) + normals_list.append(Vec3(0, 0, 1)) + texcoords.append((0.5, 0.5)) + + # 生成侧面顶点 + for i in range(2): # 底面和顶面 + z = -half_height if i == 0 else half_height + z_normal = -1 if i == 0 else 1 + + for j in range(resolution + 1): + theta = (j / resolution) * 2 * math.pi + x = radius * math.cos(theta) + y = radius * math.sin(theta) + + vertices.append(Point3(x, y, z)) + normals_list.append(Vec3(x, y, 0).normalized()) + texcoords.append((j / resolution, i)) + + # 设置顶点数量 + num_vertices = len(vertices) + vdata.setNumRows(num_vertices) + + # 创建顶点写入器 + vertex_writer = GeomVertexWriter(vdata, 'vertex') + if normals: + normal_writer = GeomVertexWriter(vdata, 'normal') + if texture_coords: + texcoord_writer = GeomVertexWriter(vdata, 'texcoord') + + # 写入顶点数据 + for i, vertex in enumerate(vertices): + vertex_writer.addData3(vertex) + if normals: + normal_writer.addData3(normals_list[i]) + if texture_coords: + texcoord_writer.addData2(texcoords[i][0], texcoords[i][1]) + + # 创建三角形索引 + prim = GeomTriangles(Geom.UHDynamic) + + # 侧面索引 + for j in range(resolution): + # 底面点索引 + bottom_center = 0 if capped else None + top_center = 1 if capped else None + bottom_start = 2 if capped else 0 + top_start = bottom_start + resolution + 1 + + # 侧面三角形 + v0 = bottom_start + j + v1 = bottom_start + j + 1 + v2 = top_start + j + v3 = top_start + j + 1 + + prim.addVertices(v0, v1, v2) + prim.addVertices(v1, v3, v2) + + # 底面三角形 + if capped: + prim.addVertices(bottom_center, v0, v1) + # 顶面三角形 + prim.addVertices(top_center, v3, v2) + + prim.closePrimitive() + + # 创建几何体 + geom = Geom(vdata) + geom.addPrimitive(prim) + + # 应用优化 + if optimization != MeshGenerator.OPTIMIZATION_NONE: + geom.decompose() + + # 创建节点 + node = GeomNode('cylinder_geom') + node.addGeom(geom) + + # 创建节点路径 + if parent_node: + np = parent_node.attachNewNode(node) + else: + np = NodePath(node) + + return np + + @staticmethod + def create_torus(inner_radius: float, outer_radius: float, + resolution_u: int = 16, resolution_v: int = 8, + parent_node: NodePath = None, + texture_coords: bool = True, + normals: bool = True, + optimization: str = OPTIMIZATION_NONE) -> NodePath: + """ + 创建环面网格 + + Args: + inner_radius: 内半径 + outer_radius: 外半径 + resolution_u: U方向分辨率 + resolution_v: V方向分辨率 + parent_node: 父节点(可选) + texture_coords: 是否生成纹理坐标 + normals: 是否生成法线 + optimization: 优化级别 + + Returns: + 包含环面网格的节点路径 + """ + # 确定顶点格式 + if texture_coords and normals: + format = GeomVertexFormat.getV3n3t2() + elif normals: + format = GeomVertexFormat.getV3n3() + elif texture_coords: + format = GeomVertexFormat.getV3t2() + else: + format = GeomVertexFormat.getV3() + + # 创建顶点数据 + vdata = GeomVertexData('torus', format, Geom.UHDynamic) + + # 生成环面顶点 + vertices = [] + normals_list = [] + texcoords = [] + + # 生成顶点 + for i in range(resolution_u + 1): + u = (i / resolution_u) * 2 * math.pi + + for j in range(resolution_v + 1): + v = (j / resolution_v) * 2 * math.pi + + # 环面参数方程 + x = (outer_radius + inner_radius * math.cos(v)) * math.cos(u) + y = (outer_radius + inner_radius * math.cos(v)) * math.sin(u) + z = inner_radius * math.sin(v) + + vertices.append(Point3(x, y, z)) + + # 计算法线 + # 这是一个简化的法线计算 + normal = Vec3( + math.cos(u) * math.cos(v), + math.sin(u) * math.cos(v), + math.sin(v) + ) + normals_list.append(normal.normalized()) + + # UV坐标 + texcoords.append((i / resolution_u, j / resolution_v)) + + # 设置顶点数量 + num_vertices = len(vertices) + vdata.setNumRows(num_vertices) + + # 创建顶点写入器 + vertex_writer = GeomVertexWriter(vdata, 'vertex') + if normals: + normal_writer = GeomVertexWriter(vdata, 'normal') + if texture_coords: + texcoord_writer = GeomVertexWriter(vdata, 'texcoord') + + # 写入顶点数据 + for i, vertex in enumerate(vertices): + vertex_writer.addData3(vertex) + if normals: + normal_writer.addData3(normals_list[i]) + if texture_coords: + texcoord_writer.addData2(texcoords[i][0], texcoords[i][1]) + + # 创建三角形索引 + prim = GeomTriangles(Geom.UHDynamic) + + # 生成面索引 + for i in range(resolution_u): + for j in range(resolution_v): + # 计算四个顶点索引 + v0 = i * (resolution_v + 1) + j + v1 = v0 + 1 + v2 = (i + 1) * (resolution_v + 1) + j + v3 = v2 + 1 + + # 添加两个三角形 + prim.addVertices(v0, v1, v2) + prim.addVertices(v1, v3, v2) + + prim.closePrimitive() + + # 创建几何体 + geom = Geom(vdata) + geom.addPrimitive(prim) + + # 应用优化 + if optimization != MeshGenerator.OPTIMIZATION_NONE: + geom.decompose() + + # 创建节点 + node = GeomNode('torus_geom') + node.addGeom(geom) + + # 创建节点路径 + if parent_node: + np = parent_node.attachNewNode(node) + else: + np = NodePath(node) + + return np + + @staticmethod + def create_plane(width: float, height: float, + resolution_x: int, resolution_y: int, + parent_node: NodePath = None, + texture_coords: bool = True, + normals: bool = True, + optimization: str = OPTIMIZATION_NONE) -> NodePath: + """ + 创建平面网格 + + Args: + width: 宽度 + height: 高度 + resolution_x: X方向分辨率 + resolution_y: Y方向分辨率 + parent_node: 父节点(可选) + texture_coords: 是否生成纹理坐标 + normals: 是否生成法线 + optimization: 优化级别 + + Returns: + 包含平面网格的节点路径 + """ + return MeshGenerator.create_cloth_patch( + width, height, resolution_x, resolution_y, + parent_node, texture_coords, normals, optimization + ) + + @staticmethod + def create_custom_mesh(vertices: List[Point3], + indices: List[int], + normals: List[Vec3] = None, + texcoords: List[Tuple[float, float]] = None, + parent_node: NodePath = None, + optimization: str = OPTIMIZATION_NONE) -> NodePath: + """ + 创建自定义网格 + + Args: + vertices: 顶点列表 + indices: 索引列表 + normals: 法线列表(可选) + texcoords: 纹理坐标列表(可选) + parent_node: 父节点(可选) + optimization: 优化级别 + + Returns: + 包含自定义网格的节点路径 + """ + # 确定顶点格式 + has_normals = normals is not None and len(normals) == len(vertices) + has_texcoords = texcoords is not None and len(texcoords) == len(vertices) + + if has_normals and has_texcoords: + format = GeomVertexFormat.getV3n3t2() + elif has_normals: + format = GeomVertexFormat.getV3n3() + elif has_texcoords: + format = GeomVertexFormat.getV3t2() + else: + format = GeomVertexFormat.getV3() + + # 创建顶点数据 + vdata = GeomVertexData('custom_mesh', format, Geom.UHDynamic) + + # 设置顶点数量 + num_vertices = len(vertices) + vdata.setNumRows(num_vertices) + + # 创建顶点写入器 + vertex_writer = GeomVertexWriter(vdata, 'vertex') + if has_normals: + normal_writer = GeomVertexWriter(vdata, 'normal') + if has_texcoords: + texcoord_writer = GeomVertexWriter(vdata, 'texcoord') + + # 写入顶点数据 + for i, vertex in enumerate(vertices): + vertex_writer.addData3(vertex) + if has_normals: + normal_writer.addData3(normals[i]) + if has_texcoords: + texcoord_writer.addData2(texcoords[i][0], texcoords[i][1]) + + # 创建三角形索引 + prim = GeomTriangles(Geom.UHDynamic) + + # 添加索引 + for i in range(0, len(indices), 3): + if i + 2 < len(indices): + prim.addVertices(indices[i], indices[i+1], indices[i+2]) + + prim.closePrimitive() + + # 创建几何体 + geom = Geom(vdata) + geom.addPrimitive(prim) + + # 应用优化 + if optimization != MeshGenerator.OPTIMIZATION_NONE: + geom.decompose() + + # 创建节点 + node = GeomNode('custom_mesh_geom') + node.addGeom(geom) + + # 创建节点路径 + if parent_node: + np = parent_node.attachNewNode(node) + else: + np = NodePath(node) + + return np + + @staticmethod + def _generate_rounded_box_vertices(half_w: float, half_h: float, half_d: float, + corner_radius: float, + resolution_x: int, resolution_y: int, resolution_z: int) -> Tuple[List[Point3], List[Vec3], List[Tuple[float, float]]]: + """ + 生成圆角盒体的顶点 + + Args: + half_w: 半宽度 + half_h: 半高度 + half_d: 半深度 + corner_radius: 圆角半径 + resolution_x: X方向分辨率 + resolution_y: Y方向分辨率 + resolution_z: Z方向分辨率 + + Returns: + (顶点列表, 法线列表, 纹理坐标列表) + """ + vertices = [] + normals = [] + texcoords = [] + + # 这里实现一个简化的圆角盒体顶点生成 + # 实际实现需要更复杂的几何计算 + + # 六个面的顶点生成(简化版本) + for face in range(6): + # 每个面生成resolution_x * resolution_y个顶点 + for i in range(resolution_x + 1): + for j in range(resolution_y + 1): + # 根据面的方向计算顶点位置 + if face == 0: # 前面 (Z-) + x = (i / resolution_x) * 2 * half_w - half_w + y = (j / resolution_y) * 2 * half_h - half_h + z = -half_d + elif face == 1: # 后面 (Z+) + x = (i / resolution_x) * 2 * half_w - half_w + y = (j / resolution_y) * 2 * half_h - half_h + z = half_d + elif face == 2: # 左面 (X-) + x = -half_w + y = (i / resolution_x) * 2 * half_h - half_h + z = (j / resolution_y) * 2 * half_d - half_d + elif face == 3: # 右面 (X+) + x = half_w + y = (i / resolution_x) * 2 * half_h - half_h + z = (j / resolution_y) * 2 * half_d - half_d + elif face == 4: # 底面 (Y-) + x = (i / resolution_x) * 2 * half_w - half_w + y = -half_h + z = (j / resolution_y) * 2 * half_d - half_d + else: # 顶面 (Y+) + x = (i / resolution_x) * 2 * half_w - half_w + y = half_h + z = (j / resolution_y) * 2 * half_d - half_d + + vertices.append(Point3(x, y, z)) + # 简化的法线计算 + normal = Vec3(0, 0, 1) # 需要根据面的方向调整 + normals.append(normal) + # 简化的纹理坐标 + texcoords.append((i / resolution_x, j / resolution_y)) + + return vertices, normals, texcoords + + @staticmethod + def get_mesh_statistics(node_path: NodePath) -> Dict[str, Any]: + """ + 获取网格统计信息 + + Args: + node_path: 网格节点路径 + + Returns: + 统计信息字典 + """ + stats = { + 'vertex_count': 0, + 'face_count': 0, + 'triangle_count': 0, + 'edge_count': 0, + 'bounding_box': { + 'min': Point3(float('inf'), float('inf'), float('inf')), + 'max': Point3(float('-inf'), float('-inf'), float('-inf')) + }, + 'surface_area': 0.0, + 'volume': 0.0 + } + + # 查找几何节点 + geom_nodes = node_path.findAllMatches('**/+GeomNode') + for geom_node_path in geom_nodes: + geom_node = geom_node_path.node() + for i in range(geom_node.getNumGeoms()): + geom = geom_node.getGeom(i) + vdata = geom.getVertexData() + + # 顶点数量 + stats['vertex_count'] += vdata.getNumRows() + + # 更新包围盒 + vertex_reader = GeomVertexReader(vdata, 'vertex') + while not vertex_reader.isAtEnd(): + vertex = vertex_reader.getData3() + stats['bounding_box']['min'].set( + min(stats['bounding_box']['min'][0], vertex[0]), + min(stats['bounding_box']['min'][1], vertex[1]), + min(stats['bounding_box']['min'][2], vertex[2]) + ) + stats['bounding_box']['max'].set( + max(stats['bounding_box']['max'][0], vertex[0]), + max(stats['bounding_box']['max'][1], vertex[1]), + max(stats['bounding_box']['max'][2], vertex[2]) + ) + + # 面和三角形数量 + for prim in geom.getPrimitives(): + if isinstance(prim, GeomTriangles): + stats['triangle_count'] += prim.getNumFaces() + stats['face_count'] += prim.getNumFaces() + else: + stats['face_count'] += prim.getNumFaces() + + # 计算表面积和体积的简化版本 + bbox = stats['bounding_box'] + width = bbox['max'][0] - bbox['min'][0] + height = bbox['max'][1] - bbox['min'][1] + depth = bbox['max'][2] - bbox['min'][2] + stats['surface_area'] = 2 * (width * height + width * depth + height * depth) + stats['volume'] = width * height * depth + + return stats + + @staticmethod + def optimize_mesh(node_path: NodePath, + target_vertex_count: int = None, + preserve_normals: bool = True, + preserve_texcoords: bool = True) -> NodePath: + """ + 优化网格 + + Args: + node_path: 网格节点路径 + target_vertex_count: 目标顶点数量 + preserve_normals: 是否保持法线 + preserve_texcoords: 是否保持纹理坐标 + + Returns: + 优化后的网格节点路径 + """ + # 这里应该实现网格优化算法 + # 简化版本:返回原始节点 + print("⚠ 网格优化功能需要更复杂的实现") + return node_path + + @staticmethod + def generate_collision_mesh(source_mesh: NodePath, + type: str = "convex") -> NodePath: + """ + 生成碰撞网格 + + Args: + source_mesh: 源网格 + type: 碰撞网格类型 ("convex", "triangle", "sphere", "box", "capsule") + + Returns: + 碰撞网格节点路径 + """ + # 这里应该实现碰撞网格生成算法 + # 简化版本:返回原始节点的副本 + print("⚠ 碰撞网格生成功能需要更复杂的实现") return source_mesh.copyTo(source_mesh.getParent()) \ No newline at end of file diff --git a/plugins/user/spatial_audio/README.md b/plugins/user/spatial_audio/README.md index f9b07ec6..1cd49b2c 100644 --- a/plugins/user/spatial_audio/README.md +++ b/plugins/user/spatial_audio/README.md @@ -1,410 +1,410 @@ -# 3D空间音效插件 - -一个功能完整的3D空间音效系统插件,为EG引擎提供基于Panda3D的3D音频处理功能,支持空间音效、环境音效和音频效果处理。 - -## 功能特性 - -### 核心功能 -- **3D音频定位**: 基于听者位置和方向的实时3D音频定位 -- **2D音频支持**: 支持传统的2D音频播放 -- **音频源管理**: 完整的音频源创建、播放、暂停、停止和删除功能 -- **环境音频**: 区域化的环境音效系统 -- **音频效果**: 支持多种音频效果处理(混响、回声、滤波等) -- **音频缓存**: 智能音频文件缓存系统,提高加载性能 -- **淡入淡出**: 支持音频的淡入淡出效果 - -### 音频处理 -- **空间化处理**: 基于距离和方向的音量衰减和声道平衡 -- **多普勒效应**: 根据音源和听者的相对运动计算频率变化 -- **HRTF支持**: 头部相关传递函数实现更真实的3D音效 -- **环境混响**: 根据房间尺寸和材质自动计算混响参数 -- **音量控制**: 精确的音量控制和淡入淡出效果 -- **距离衰减**: 可配置的最小/最大距离和衰减因子 -- **音频优先级**: 音频资源管理和优先级控制 - -### 音频效果 -- **混响效果**: 模拟不同环境的混响效果 -- **回声效果**: 创建回声和延迟效果 -- **滤波效果**: 低通、高通和带通滤波器 -- **失真效果**: 音频失真和过载效果 -- **压缩效果**: 动态范围压缩 -- **均衡器**: 多频段均衡器 -- **合声/镶边**: 调制效果 -- **效果链**: 支持多个效果的组合应用 - -### 环境系统 -- **音频区域**: 定义具有特定音频属性的3D区域 -- **环境音效**: 区域内的背景环境音效 -- **区域属性**: 可配置的区域音频参数 -- **动态切换**: 听者在不同区域间移动时的音频切换 -- **过渡效果**: 区域间平滑的音频过渡 -- **遮挡效果**: 音频遮挡和 occlusion 处理 -- **回调系统**: 区域进入/离开/内部状态回调 - -### 编辑功能 -- **可视化编辑**: 图形化界面编辑音频场景 -- **多种工具**: 选择、添加音效、添加区域等工具 -- **场景保存/加载**: 音频场景的序列化和反序列化 -- **网格对齐**: 网格对齐功能提高编辑精度 -- **实时预览**: 编辑过程中的实时音频预览 - -### 性能优化 -- **资源管理**: 智能的音频资源加载和卸载 -- **内存优化**: 音频缓冲区管理和内存复用 -- **批量处理**: 支持大量音频源的同时处理 -- **LOD支持**: 根据距离调整音频处理质量 -- **对象池**: 音频对象池减少GC压力 - -## 使用方法 - -### 快捷键操作 -- **F1**: 切换音频调试模式 -- **F2**: 切换空间音频 -- **F3**: 播放测试音效 -- **F4**: 停止所有音频 -- **F5**: 显示音频统计信息 -- **F6**: 切换环境音频 -- **F7**: 应用混响效果 -- **F8**: 应用回声效果 -- **F9**: 创建测试区域 -- **F10**: 切换多普勒效应 -- **F11**: 切换HRTF -- **F12**: 显示详细统计信息 - -### GUI界面操作 -插件会在屏幕左上角创建控制面板,右上角创建编辑器面板,可以通过按钮直接操作音频功能。 - -### 编程接口 -```python -# 获取插件实例 -audio_plugin = world.plugin_manager.get_plugin("spatial_audio") - -# 创建3D音效 -sound_id = audio_plugin.create_3d_sound( - "sounds/footsteps.wav", - (10, 0, 5), # 位置 - False, # 循环 - 0.8, # 音量 - 1.0, # 最小距离 - 100.0, # 最大距离 - 1.0 # 衰减因子 -) - -# 创建2D音效 -music_id = audio_plugin.create_2d_sound("sounds/background_music.mp3", True, 0.6) - -# 播放音效(支持淡入) -audio_plugin.play_sound(sound_id, fade_in_time=1.0) - -# 设置音源位置 -audio_plugin.set_sound_position(sound_id, (15, 0, 8)) - -# 设置音量 -audio_plugin.set_sound_volume(sound_id, 0.5) - -# 检查音效状态 -if audio_plugin.is_sound_playing(sound_id): - print("音效正在播放") - -# 创建音频效果 -reverb_effect_id = audio_plugin.create_audio_effect("reverb", { - "reverb_time": 2.0, - "early_reflections": 0.3, - "late_reverb": 0.7 -}) - -# 应用效果到音效 -audio_plugin.apply_effect_to_sound(sound_id, reverb_effect_id) - -# 创建环境区域 -audio_plugin.create_environment_zone("cave", (0, 0, 0), 20.0, { - "reverb": 0.8, - "absorption": 0.3, - "ambient_volume": 0.4 -}) - -# 为区域添加环境音效 -audio_plugin.environment_audio_manager.add_ambient_sound_to_zone( - "cave", "sounds/cave_ambience.wav", 0.4, True) - -# 设置听者位置 -audio_plugin.set_listener_position((5, 0, 5)) - -# 设置听者方向 -audio_plugin.set_listener_orientation((0, 0, 0)) - -# 停止音效(支持淡出) -audio_plugin.stop_sound(sound_id, fade_out_time=1.0) - -# 删除音效 -audio_plugin.delete_sound(sound_id) - -# 使用预设效果 -cathedral_reverb = audio_plugin.create_preset_effect("cathedral_reverb") -audio_plugin.apply_effect_to_sound(sound_id, cathedral_reverb) - -# 设置主音量 -audio_plugin.set_master_volume(0.8) - -# 绑定音效到对象 -audio_plugin.attach_sound_to_object(sound_id, character_node) - -# 添加区域回调 -def on_enter_zone(zone_id): - print(f"进入区域: {zone_id}") - -audio_plugin.add_zone_enter_callback("cave", on_enter_zone) -``` - -## 核心组件 - -### 音频源管理器 -负责音频源的创建、播放和控制。 - -主要功能: -- 3D和2D音效创建 -- 音频播放控制(播放、暂停、停止) -- 音源位置和属性设置 -- 音频状态查询 -- 资源管理和缓存 -- 淡入淡出效果 - -### 音频效果处理器 -处理各种音频效果的应用和管理。 - -主要功能: -- 多种音频效果创建 -- 效果参数配置 -- 效果应用到音源 -- 效果启用/禁用控制 -- 效果链管理 -- 预设效果系统 - -### 环境音频管理器 -管理环境音效和区域音频效果。 - -主要功能: -- 音频区域创建和管理 -- 环境音效播放 -- 区域属性配置 -- 区域间音频切换 -- 动态环境效果 -- 区域状态回调 - -### 音频编辑器 -提供可视化编辑音频场景的功能。 - -主要功能: -- 图形化界面编辑 -- 多种编辑工具 -- 场景保存和加载 -- 实时预览 -- 网格对齐 - -## 音频组件 - -### 音频源组件 -为游戏对象添加音频播放功能。 - -主要功能: -- 音频文件加载 -- 3D空间化处理 -- 音频播放控制 -- 对象绑定 - -### 音频听者组件 -设置音频听者位置和方向。 - -主要功能: -- 听者位置设置 -- 听者方向设置 -- 对象绑定 - -### 音频效果组件 -为音频源应用效果。 - -主要功能: -- 音频效果创建 -- 效果参数配置 -- 效果应用和移除 - -## 音频工具 - -### 距离计算 -计算两点间距离用于音量衰减计算。 - -### 音量衰减 -基于距离的音量衰减计算。 - -### 声道平衡 -根据音源相对位置计算左右声道平衡。 - -### 多普勒效应 -计算音源和听者相对运动引起的频率变化。 - -### HRTF处理 -头部相关传递函数实现更真实的3D音效。 - -## 性能优化 - -### 资源管理 -- **智能加载**: 根据需要动态加载音频资源 -- **内存复用**: 音频缓冲区复用减少内存分配 -- **自动卸载**: 不使用的音频资源自动卸载 -- **缓存系统**: 音频文件智能缓存 - -### 处理优化 -- **批量更新**: 批量处理音频源更新 -- **LOD系统**: 根据距离调整处理质量 -- **效果优化**: 根据重要性调整效果处理 - -### 内存优化 -- **对象池**: 音频对象池减少GC压力 -- **流式加载**: 大型音频文件流式加载 -- **压缩存储**: 音频数据压缩存储 - -## 扩展开发 - -### 添加新的音频效果 -1. 在[effects](file:///home/hello/EG/plugins/user/spatial_audio/effects)目录中创建新的效果处理器 -2. 实现效果处理逻辑 -3. 在[AudioEffectProcessor](file:///home/hello/EG/plugins/user/spatial_audio/effects/audio_effect_processor.py#L11-L94)中注册新效果 - -### 添加新的音频组件 -1. 在[components](file:///home/hello/EG/plugins/user/spatial_audio/components)目录中创建新的组件类 -2. 继承AudioComponent基类 -3. 实现组件功能 - -### 添加新的工具函数 -1. 在[utils](file:///home/hello/EG/plugins/user/spatial_audio/utils)目录中创建新的工具模块 -2. 实现工具函数 -3. 在需要的地方使用工具函数 - -## 故障排除 - -### 音频无法播放 -1. 检查音频文件路径是否正确 -2. 确认音频格式是否支持 -3. 检查音频系统是否正确初始化 -4. 验证音频文件是否损坏 - -### 3D音效不工作 -1. 确认听者位置是否正确设置 -2. 检查音源位置是否正确设置 -3. 验证音频系统是否支持3D音效 -4. 检查音频格式是否支持3D处理 - -### 性能问题 -1. 使用音频统计功能分析性能瓶颈 -2. 减少同时播放的音频数量 -3. 降低音频处理质量 -4. 使用LOD系统优化远处音频 - -### 环境音频问题 -1. 检查区域定义是否正确 -2. 确认听者位置是否在区域内 -3. 验证环境音效文件是否正确加载 -4. 检查区域属性设置 - -## API参考 - -### Plugin类主要方法 - -#### 初始化和控制 -- `initialize()` - 初始化插件 -- `enable()` - 启用插件 -- `disable()` - 禁用插件 -- `finalize()` - 清理插件资源 -- `update(dt)` - 更新插件状态 - -#### 音频源操作 -- `create_3d_sound(filepath, position, loop, volume, min_distance, max_distance, rolloff_factor)` - 创建3D音效 -- `create_2d_sound(filepath, loop, volume)` - 创建2D音效 -- `play_sound(sound_id, fade_in_time)` - 播放音效 -- `stop_sound(sound_id, fade_out_time)` - 停止音效 -- `pause_sound(sound_id)` - 暂停音效 -- `resume_sound(sound_id)` - 恢复音效 -- `set_sound_position(sound_id, position)` - 设置音源位置 -- `set_sound_volume(sound_id, volume)` - 设置音源音量 -- `set_sound_loop(sound_id, loop)` - 设置音源循环模式 -- `is_sound_playing(sound_id)` - 检查音效是否正在播放 -- `get_sound_duration(sound_id)` - 获取音效时长 -- `get_sound_position(sound_id)` - 获取音源位置 -- `delete_sound(sound_id)` - 删除音效 -- `stop_all_sounds()` - 停止所有音频 -- `attach_sound_to_object(sound_id, obj)` - 绑定音效到对象 -- `detach_sound_from_object(sound_id)` - 解绑音效从对象 - -#### 音频效果操作 -- `create_audio_effect(effect_type, parameters)` - 创建音频效果 -- `apply_effect_to_sound(sound_id, effect_id)` - 应用效果到音效 -- `remove_effect_from_sound(sound_id, effect_id)` - 从音效移除效果 -- `create_preset_effect(preset_name)` - 创建预设效果 -- `enable_audio_effects(enable)` - 启用/禁用音频效果处理 - -#### 听者操作 -- `set_listener_position(position)` - 设置听者位置 -- `set_listener_orientation(orientation)` - 设置听者方向 -- `get_listener_velocity()` - 获取听者速度 - -#### 环境音频操作 -- `create_environment_zone(zone_id, position, radius, properties)` - 创建环境区域 -- `set_zone_property(zone_id, property_name, value)` - 设置区域属性 -- `add_zone_enter_callback(zone_id, callback)` - 添加区域进入回调 -- `add_zone_exit_callback(zone_id, callback)` - 添加区域离开回调 - -#### 系统控制 -- `set_master_volume(volume)` - 设置主音量 -- `get_master_volume()` - 获取主音量 -- `set_audio_quality(quality)` - 设置音频质量 -- `set_doppler_factor(factor)` - 设置多普勒效应因子 -- `set_environment_occlusion(enable)` - 启用/禁用环境遮挡效果 - -#### 统计和信息 -- `get_stats()` - 获取插件统计信息 -- `get_cached_sound_files()` - 获取已缓存的音频文件列表 -- `clear_sound_cache()` - 清空音频文件缓存 - -## 音频格式支持 - -支持的音频格式: -- **WAV**: 未压缩的PCM音频 -- **OGG**: Ogg Vorbis压缩音频 -- **MP3**: MPEG音频层III - -## 性能调优建议 - -### 音频源管理 -1. 合理控制同时播放的音频数量 -2. 及时删除不需要的音频源 -3. 使用对象池管理音频对象 -4. 预加载常用的音频资源 -5. 合理设置音频缓存大小 - -### 效果处理 -1. 根据重要性选择效果 -2. 限制同时应用的效果数量 -3. 使用简化的效果处理远处音频 -4. 及时移除不需要的效果 - -### 环境音频 -1. 合理设置区域大小 -2. 控制区域内环境音效数量 -3. 使用LOD系统优化远处区域 -4. 及时清理不需要的区域 - -### 内存管理 -1. 定期清理音频缓存 -2. 及时释放不需要的音频资源 -3. 使用流式加载大型音频文件 -4. 监控内存使用情况 - -## 版本信息 - -- 版本: 1.0.0 -- 作者: EG Team -- 发布日期: 2024年 - -## 许可证 - +# 3D空间音效插件 + +一个功能完整的3D空间音效系统插件,为EG引擎提供基于Panda3D的3D音频处理功能,支持空间音效、环境音效和音频效果处理。 + +## 功能特性 + +### 核心功能 +- **3D音频定位**: 基于听者位置和方向的实时3D音频定位 +- **2D音频支持**: 支持传统的2D音频播放 +- **音频源管理**: 完整的音频源创建、播放、暂停、停止和删除功能 +- **环境音频**: 区域化的环境音效系统 +- **音频效果**: 支持多种音频效果处理(混响、回声、滤波等) +- **音频缓存**: 智能音频文件缓存系统,提高加载性能 +- **淡入淡出**: 支持音频的淡入淡出效果 + +### 音频处理 +- **空间化处理**: 基于距离和方向的音量衰减和声道平衡 +- **多普勒效应**: 根据音源和听者的相对运动计算频率变化 +- **HRTF支持**: 头部相关传递函数实现更真实的3D音效 +- **环境混响**: 根据房间尺寸和材质自动计算混响参数 +- **音量控制**: 精确的音量控制和淡入淡出效果 +- **距离衰减**: 可配置的最小/最大距离和衰减因子 +- **音频优先级**: 音频资源管理和优先级控制 + +### 音频效果 +- **混响效果**: 模拟不同环境的混响效果 +- **回声效果**: 创建回声和延迟效果 +- **滤波效果**: 低通、高通和带通滤波器 +- **失真效果**: 音频失真和过载效果 +- **压缩效果**: 动态范围压缩 +- **均衡器**: 多频段均衡器 +- **合声/镶边**: 调制效果 +- **效果链**: 支持多个效果的组合应用 + +### 环境系统 +- **音频区域**: 定义具有特定音频属性的3D区域 +- **环境音效**: 区域内的背景环境音效 +- **区域属性**: 可配置的区域音频参数 +- **动态切换**: 听者在不同区域间移动时的音频切换 +- **过渡效果**: 区域间平滑的音频过渡 +- **遮挡效果**: 音频遮挡和 occlusion 处理 +- **回调系统**: 区域进入/离开/内部状态回调 + +### 编辑功能 +- **可视化编辑**: 图形化界面编辑音频场景 +- **多种工具**: 选择、添加音效、添加区域等工具 +- **场景保存/加载**: 音频场景的序列化和反序列化 +- **网格对齐**: 网格对齐功能提高编辑精度 +- **实时预览**: 编辑过程中的实时音频预览 + +### 性能优化 +- **资源管理**: 智能的音频资源加载和卸载 +- **内存优化**: 音频缓冲区管理和内存复用 +- **批量处理**: 支持大量音频源的同时处理 +- **LOD支持**: 根据距离调整音频处理质量 +- **对象池**: 音频对象池减少GC压力 + +## 使用方法 + +### 快捷键操作 +- **F1**: 切换音频调试模式 +- **F2**: 切换空间音频 +- **F3**: 播放测试音效 +- **F4**: 停止所有音频 +- **F5**: 显示音频统计信息 +- **F6**: 切换环境音频 +- **F7**: 应用混响效果 +- **F8**: 应用回声效果 +- **F9**: 创建测试区域 +- **F10**: 切换多普勒效应 +- **F11**: 切换HRTF +- **F12**: 显示详细统计信息 + +### GUI界面操作 +插件会在屏幕左上角创建控制面板,右上角创建编辑器面板,可以通过按钮直接操作音频功能。 + +### 编程接口 +```python +# 获取插件实例 +audio_plugin = world.plugin_manager.get_plugin("spatial_audio") + +# 创建3D音效 +sound_id = audio_plugin.create_3d_sound( + "sounds/footsteps.wav", + (10, 0, 5), # 位置 + False, # 循环 + 0.8, # 音量 + 1.0, # 最小距离 + 100.0, # 最大距离 + 1.0 # 衰减因子 +) + +# 创建2D音效 +music_id = audio_plugin.create_2d_sound("sounds/background_music.mp3", True, 0.6) + +# 播放音效(支持淡入) +audio_plugin.play_sound(sound_id, fade_in_time=1.0) + +# 设置音源位置 +audio_plugin.set_sound_position(sound_id, (15, 0, 8)) + +# 设置音量 +audio_plugin.set_sound_volume(sound_id, 0.5) + +# 检查音效状态 +if audio_plugin.is_sound_playing(sound_id): + print("音效正在播放") + +# 创建音频效果 +reverb_effect_id = audio_plugin.create_audio_effect("reverb", { + "reverb_time": 2.0, + "early_reflections": 0.3, + "late_reverb": 0.7 +}) + +# 应用效果到音效 +audio_plugin.apply_effect_to_sound(sound_id, reverb_effect_id) + +# 创建环境区域 +audio_plugin.create_environment_zone("cave", (0, 0, 0), 20.0, { + "reverb": 0.8, + "absorption": 0.3, + "ambient_volume": 0.4 +}) + +# 为区域添加环境音效 +audio_plugin.environment_audio_manager.add_ambient_sound_to_zone( + "cave", "sounds/cave_ambience.wav", 0.4, True) + +# 设置听者位置 +audio_plugin.set_listener_position((5, 0, 5)) + +# 设置听者方向 +audio_plugin.set_listener_orientation((0, 0, 0)) + +# 停止音效(支持淡出) +audio_plugin.stop_sound(sound_id, fade_out_time=1.0) + +# 删除音效 +audio_plugin.delete_sound(sound_id) + +# 使用预设效果 +cathedral_reverb = audio_plugin.create_preset_effect("cathedral_reverb") +audio_plugin.apply_effect_to_sound(sound_id, cathedral_reverb) + +# 设置主音量 +audio_plugin.set_master_volume(0.8) + +# 绑定音效到对象 +audio_plugin.attach_sound_to_object(sound_id, character_node) + +# 添加区域回调 +def on_enter_zone(zone_id): + print(f"进入区域: {zone_id}") + +audio_plugin.add_zone_enter_callback("cave", on_enter_zone) +``` + +## 核心组件 + +### 音频源管理器 +负责音频源的创建、播放和控制。 + +主要功能: +- 3D和2D音效创建 +- 音频播放控制(播放、暂停、停止) +- 音源位置和属性设置 +- 音频状态查询 +- 资源管理和缓存 +- 淡入淡出效果 + +### 音频效果处理器 +处理各种音频效果的应用和管理。 + +主要功能: +- 多种音频效果创建 +- 效果参数配置 +- 效果应用到音源 +- 效果启用/禁用控制 +- 效果链管理 +- 预设效果系统 + +### 环境音频管理器 +管理环境音效和区域音频效果。 + +主要功能: +- 音频区域创建和管理 +- 环境音效播放 +- 区域属性配置 +- 区域间音频切换 +- 动态环境效果 +- 区域状态回调 + +### 音频编辑器 +提供可视化编辑音频场景的功能。 + +主要功能: +- 图形化界面编辑 +- 多种编辑工具 +- 场景保存和加载 +- 实时预览 +- 网格对齐 + +## 音频组件 + +### 音频源组件 +为游戏对象添加音频播放功能。 + +主要功能: +- 音频文件加载 +- 3D空间化处理 +- 音频播放控制 +- 对象绑定 + +### 音频听者组件 +设置音频听者位置和方向。 + +主要功能: +- 听者位置设置 +- 听者方向设置 +- 对象绑定 + +### 音频效果组件 +为音频源应用效果。 + +主要功能: +- 音频效果创建 +- 效果参数配置 +- 效果应用和移除 + +## 音频工具 + +### 距离计算 +计算两点间距离用于音量衰减计算。 + +### 音量衰减 +基于距离的音量衰减计算。 + +### 声道平衡 +根据音源相对位置计算左右声道平衡。 + +### 多普勒效应 +计算音源和听者相对运动引起的频率变化。 + +### HRTF处理 +头部相关传递函数实现更真实的3D音效。 + +## 性能优化 + +### 资源管理 +- **智能加载**: 根据需要动态加载音频资源 +- **内存复用**: 音频缓冲区复用减少内存分配 +- **自动卸载**: 不使用的音频资源自动卸载 +- **缓存系统**: 音频文件智能缓存 + +### 处理优化 +- **批量更新**: 批量处理音频源更新 +- **LOD系统**: 根据距离调整处理质量 +- **效果优化**: 根据重要性调整效果处理 + +### 内存优化 +- **对象池**: 音频对象池减少GC压力 +- **流式加载**: 大型音频文件流式加载 +- **压缩存储**: 音频数据压缩存储 + +## 扩展开发 + +### 添加新的音频效果 +1. 在[effects](file:///home/hello/EG/plugins/user/spatial_audio/effects)目录中创建新的效果处理器 +2. 实现效果处理逻辑 +3. 在[AudioEffectProcessor](file:///home/hello/EG/plugins/user/spatial_audio/effects/audio_effect_processor.py#L11-L94)中注册新效果 + +### 添加新的音频组件 +1. 在[components](file:///home/hello/EG/plugins/user/spatial_audio/components)目录中创建新的组件类 +2. 继承AudioComponent基类 +3. 实现组件功能 + +### 添加新的工具函数 +1. 在[utils](file:///home/hello/EG/plugins/user/spatial_audio/utils)目录中创建新的工具模块 +2. 实现工具函数 +3. 在需要的地方使用工具函数 + +## 故障排除 + +### 音频无法播放 +1. 检查音频文件路径是否正确 +2. 确认音频格式是否支持 +3. 检查音频系统是否正确初始化 +4. 验证音频文件是否损坏 + +### 3D音效不工作 +1. 确认听者位置是否正确设置 +2. 检查音源位置是否正确设置 +3. 验证音频系统是否支持3D音效 +4. 检查音频格式是否支持3D处理 + +### 性能问题 +1. 使用音频统计功能分析性能瓶颈 +2. 减少同时播放的音频数量 +3. 降低音频处理质量 +4. 使用LOD系统优化远处音频 + +### 环境音频问题 +1. 检查区域定义是否正确 +2. 确认听者位置是否在区域内 +3. 验证环境音效文件是否正确加载 +4. 检查区域属性设置 + +## API参考 + +### Plugin类主要方法 + +#### 初始化和控制 +- `initialize()` - 初始化插件 +- `enable()` - 启用插件 +- `disable()` - 禁用插件 +- `finalize()` - 清理插件资源 +- `update(dt)` - 更新插件状态 + +#### 音频源操作 +- `create_3d_sound(filepath, position, loop, volume, min_distance, max_distance, rolloff_factor)` - 创建3D音效 +- `create_2d_sound(filepath, loop, volume)` - 创建2D音效 +- `play_sound(sound_id, fade_in_time)` - 播放音效 +- `stop_sound(sound_id, fade_out_time)` - 停止音效 +- `pause_sound(sound_id)` - 暂停音效 +- `resume_sound(sound_id)` - 恢复音效 +- `set_sound_position(sound_id, position)` - 设置音源位置 +- `set_sound_volume(sound_id, volume)` - 设置音源音量 +- `set_sound_loop(sound_id, loop)` - 设置音源循环模式 +- `is_sound_playing(sound_id)` - 检查音效是否正在播放 +- `get_sound_duration(sound_id)` - 获取音效时长 +- `get_sound_position(sound_id)` - 获取音源位置 +- `delete_sound(sound_id)` - 删除音效 +- `stop_all_sounds()` - 停止所有音频 +- `attach_sound_to_object(sound_id, obj)` - 绑定音效到对象 +- `detach_sound_from_object(sound_id)` - 解绑音效从对象 + +#### 音频效果操作 +- `create_audio_effect(effect_type, parameters)` - 创建音频效果 +- `apply_effect_to_sound(sound_id, effect_id)` - 应用效果到音效 +- `remove_effect_from_sound(sound_id, effect_id)` - 从音效移除效果 +- `create_preset_effect(preset_name)` - 创建预设效果 +- `enable_audio_effects(enable)` - 启用/禁用音频效果处理 + +#### 听者操作 +- `set_listener_position(position)` - 设置听者位置 +- `set_listener_orientation(orientation)` - 设置听者方向 +- `get_listener_velocity()` - 获取听者速度 + +#### 环境音频操作 +- `create_environment_zone(zone_id, position, radius, properties)` - 创建环境区域 +- `set_zone_property(zone_id, property_name, value)` - 设置区域属性 +- `add_zone_enter_callback(zone_id, callback)` - 添加区域进入回调 +- `add_zone_exit_callback(zone_id, callback)` - 添加区域离开回调 + +#### 系统控制 +- `set_master_volume(volume)` - 设置主音量 +- `get_master_volume()` - 获取主音量 +- `set_audio_quality(quality)` - 设置音频质量 +- `set_doppler_factor(factor)` - 设置多普勒效应因子 +- `set_environment_occlusion(enable)` - 启用/禁用环境遮挡效果 + +#### 统计和信息 +- `get_stats()` - 获取插件统计信息 +- `get_cached_sound_files()` - 获取已缓存的音频文件列表 +- `clear_sound_cache()` - 清空音频文件缓存 + +## 音频格式支持 + +支持的音频格式: +- **WAV**: 未压缩的PCM音频 +- **OGG**: Ogg Vorbis压缩音频 +- **MP3**: MPEG音频层III + +## 性能调优建议 + +### 音频源管理 +1. 合理控制同时播放的音频数量 +2. 及时删除不需要的音频源 +3. 使用对象池管理音频对象 +4. 预加载常用的音频资源 +5. 合理设置音频缓存大小 + +### 效果处理 +1. 根据重要性选择效果 +2. 限制同时应用的效果数量 +3. 使用简化的效果处理远处音频 +4. 及时移除不需要的效果 + +### 环境音频 +1. 合理设置区域大小 +2. 控制区域内环境音效数量 +3. 使用LOD系统优化远处区域 +4. 及时清理不需要的区域 + +### 内存管理 +1. 定期清理音频缓存 +2. 及时释放不需要的音频资源 +3. 使用流式加载大型音频文件 +4. 监控内存使用情况 + +## 版本信息 + +- 版本: 1.0.0 +- 作者: EG Team +- 发布日期: 2024年 + +## 许可证 + 本插件遵循EG引擎许可证协议。 \ No newline at end of file diff --git a/plugins/user/spatial_audio/components/audio_component.py b/plugins/user/spatial_audio/components/audio_component.py index 4307f208..4d1e98ce 100644 --- a/plugins/user/spatial_audio/components/audio_component.py +++ b/plugins/user/spatial_audio/components/audio_component.py @@ -1,459 +1,459 @@ -""" -音频组件基类 -提供音频组件的基础功能和接口 -""" - -from abc import ABC, abstractmethod -from typing import Dict, Any, Optional -import uuid - -class AudioComponent(ABC): - """ - 音频组件基类 - 提供音频组件的基础功能和接口 - """ - - def __init__(self, plugin, name: str = ""): - """ - 初始化音频组件 - - Args: - plugin: 3D音频插件实例 - name: 组件名称 - """ - self.plugin = plugin - self.name = name if name else f"AudioComponent_{uuid.uuid4()}" - self.enabled = True - self.properties: Dict[str, Any] = {} - - @abstractmethod - def initialize(self) -> bool: - """ - 初始化组件 - - Returns: - 是否初始化成功 - """ - pass - - @abstractmethod - def cleanup(self): - """清理组件资源""" - pass - - @abstractmethod - def update(self, dt: float): - """ - 更新组件状态 - - Args: - dt: 时间增量 - """ - pass - - def enable(self): - """启用组件""" - self.enabled = True - - def disable(self): - """禁用组件""" - self.enabled = False - - def is_enabled(self) -> bool: - """ - 检查组件是否启用 - - Returns: - 是否启用 - """ - return self.enabled - - def set_property(self, name: str, value: Any) -> bool: - """ - 设置属性 - - Args: - name: 属性名 - value: 属性值 - - Returns: - 是否设置成功 - """ - try: - self.properties[name] = value - return True - except Exception: - return False - - def get_property(self, name: str, default: Any = None) -> Any: - """ - 获取属性 - - Args: - name: 属性名 - default: 默认值 - - Returns: - 属性值 - """ - return self.properties.get(name, default) - - def remove_property(self, name: str) -> bool: - """ - 移除属性 - - Args: - name: 属性名 - - Returns: - 是否移除成功 - """ - if name in self.properties: - del self.properties[name] - return True - return False - - def get_all_properties(self) -> Dict[str, Any]: - """ - 获取所有属性 - - Returns: - 属性字典 - """ - return self.properties.copy() - - -class AudioSourceComponent(AudioComponent): - """ - 音频源组件 - 用于为游戏对象添加音频播放功能 - """ - - def __init__(self, plugin, name: str = ""): - super().__init__(plugin, name) - self.sound_id: Optional[str] = None - self.spatial = False - self.loop = False - self.volume = 1.0 - self.attached_object = None - - def initialize(self) -> bool: - """初始化音频源组件""" - # 音频源组件在需要时才创建音效 - return True - - def cleanup(self): - """清理音频源组件""" - if self.sound_id: - self.plugin.audio_source_manager.delete_sound(self.sound_id) - self.sound_id = None - - def update(self, dt: float): - """更新音频源组件""" - if not self.enabled: - return - - # 如果附加到对象,同步位置 - if self.attached_object and self.sound_id and self.spatial: - pos = self.attached_object.getPos() - self.plugin.audio_source_manager.set_sound_position( - self.sound_id, (pos.x, pos.y, pos.z)) - - def load_sound(self, filepath: str, spatial: bool = False, - loop: bool = False, volume: float = 1.0) -> bool: - """ - 加载音频文件 - - Args: - filepath: 音频文件路径 - spatial: 是否为3D音效 - loop: 是否循环播放 - volume: 音量 - - Returns: - 是否加载成功 - """ - try: - # 删除现有音效 - if self.sound_id: - self.plugin.audio_source_manager.delete_sound(self.sound_id) - - # 创建新音效 - if spatial: - pos = (0, 0, 0) - if self.attached_object: - obj_pos = self.attached_object.getPos() - pos = (obj_pos.x, obj_pos.y, obj_pos.z) - self.sound_id = self.plugin.audio_source_manager.create_3d_sound( - filepath, pos, loop, volume) - else: - self.sound_id = self.plugin.audio_source_manager.create_2d_sound( - filepath, loop, volume) - - if self.sound_id: - self.spatial = spatial - self.loop = loop - self.volume = volume - return True - else: - return False - - except Exception as e: - print(f"✗ 加载音频文件失败: {e}") - return False - - def play(self) -> bool: - """ - 播放音效 - - Returns: - 是否播放成功 - """ - if self.sound_id: - return self.plugin.audio_source_manager.play_sound(self.sound_id) - return False - - def stop(self) -> bool: - """ - 停止音效 - - Returns: - 是否停止成功 - """ - if self.sound_id: - return self.plugin.audio_source_manager.stop_sound(self.sound_id) - return False - - def pause(self) -> bool: - """ - 暂停音效 - - Returns: - 是否暂停成功 - """ - if self.sound_id: - return self.plugin.audio_source_manager.pause_sound(self.sound_id) - return False - - def resume(self) -> bool: - """ - 恢复音效 - - Returns: - 是否恢复成功 - """ - if self.sound_id: - return self.plugin.audio_source_manager.resume_sound(self.sound_id) - return False - - def is_playing(self) -> bool: - """ - 检查音效是否正在播放 - - Returns: - 是否正在播放 - """ - if self.sound_id: - return self.plugin.audio_source_manager.is_sound_playing(self.sound_id) - return False - - def set_volume(self, volume: float) -> bool: - """ - 设置音量 - - Args: - volume: 音量 (0.0-1.0) - - Returns: - 是否设置成功 - """ - if self.sound_id: - success = self.plugin.audio_source_manager.set_sound_volume(self.sound_id, volume) - if success: - self.volume = volume - return success - return False - - def set_loop(self, loop: bool) -> bool: - """ - 设置循环模式 - - Args: - loop: 是否循环 - - Returns: - 是否设置成功 - """ - if self.sound_id: - success = self.plugin.audio_source_manager.set_sound_loop(self.sound_id, loop) - if success: - self.loop = loop - return success - return False - - def attach_to_object(self, obj): - """ - 附加到游戏对象 - - Args: - obj: 游戏对象 - """ - self.attached_object = obj - - def detach_from_object(self): - """从游戏对象分离""" - self.attached_object = None - - -class AudioListenerComponent(AudioComponent): - """ - 音频听者组件 - 用于设置音频听者位置和方向 - """ - - def __init__(self, plugin, name: str = ""): - super().__init__(plugin, name) - self.attached_object = None - - def initialize(self) -> bool: - """初始化音频听者组件""" - return True - - def cleanup(self): - """清理音频听者组件""" - pass - - def update(self, dt: float): - """更新音频听者组件""" - if not self.enabled: - return - - # 如果附加到对象,同步位置和方向 - if self.attached_object: - pos = self.attached_object.getPos() - hpr = self.attached_object.getHpr() - self.plugin.set_listener_position((pos.x, pos.y, pos.z)) - self.plugin.set_listener_orientation((hpr.x, hpr.y, hpr.z)) - - def attach_to_object(self, obj): - """ - 附加到游戏对象 - - Args: - obj: 游戏对象 - """ - self.attached_object = obj - - def detach_from_object(self): - """从游戏对象分离""" - self.attached_object = None - - -class AudioEffectComponent(AudioComponent): - """ - 音频效果组件 - 用于为音频源应用效果 - """ - - def __init__(self, plugin, name: str = ""): - super().__init__(plugin, name) - self.effect_id: Optional[str] = None - self.effect_type = "" - self.target_sound_id: Optional[str] = None - - def initialize(self) -> bool: - """初始化音频效果组件""" - return True - - def cleanup(self): - """清理音频效果组件""" - if self.effect_id: - self.plugin.audio_effect_processor.delete_effect(self.effect_id) - self.effect_id = None - - def update(self, dt: float): - """更新音频效果组件""" - if not self.enabled: - return - - def create_effect(self, effect_type: str, parameters: Dict[str, Any]) -> bool: - """ - 创建音频效果 - - Args: - effect_type: 效果类型 - parameters: 效果参数 - - Returns: - 是否创建成功 - """ - try: - # 删除现有效果 - if self.effect_id: - self.plugin.audio_effect_processor.delete_effect(self.effect_id) - - # 创建新效果 - self.effect_id = self.plugin.audio_effect_processor.create_effect( - effect_type, parameters) - - if self.effect_id: - self.effect_type = effect_type - return True - else: - return False - - except Exception as e: - print(f"✗ 创建音频效果失败: {e}") - return False - - def apply_to_sound(self, sound_id: str) -> bool: - """ - 应用效果到音效 - - Args: - sound_id: 音源ID - - Returns: - 是否应用成功 - """ - if self.effect_id: - success = self.plugin.audio_effect_processor.apply_effect_to_sound( - sound_id, self.effect_id) - if success: - self.target_sound_id = sound_id - return success - return False - - def remove_from_sound(self, sound_id: str) -> bool: - """ - 从音效移除效果 - - Args: - sound_id: 音源ID - - Returns: - 是否移除成功 - """ - if self.effect_id: - success = self.plugin.audio_effect_processor.remove_effect_from_sound( - sound_id, self.effect_id) - if success and self.target_sound_id == sound_id: - self.target_sound_id = None - return success - return False - - def set_parameter(self, parameter: str, value: Any) -> bool: - """ - 设置效果参数 - - Args: - parameter: 参数名 - value: 参数值 - - Returns: - 是否设置成功 - """ - if self.effect_id: - return self.plugin.audio_effect_processor.set_effect_parameter( - self.effect_id, parameter, value) +""" +音频组件基类 +提供音频组件的基础功能和接口 +""" + +from abc import ABC, abstractmethod +from typing import Dict, Any, Optional +import uuid + +class AudioComponent(ABC): + """ + 音频组件基类 + 提供音频组件的基础功能和接口 + """ + + def __init__(self, plugin, name: str = ""): + """ + 初始化音频组件 + + Args: + plugin: 3D音频插件实例 + name: 组件名称 + """ + self.plugin = plugin + self.name = name if name else f"AudioComponent_{uuid.uuid4()}" + self.enabled = True + self.properties: Dict[str, Any] = {} + + @abstractmethod + def initialize(self) -> bool: + """ + 初始化组件 + + Returns: + 是否初始化成功 + """ + pass + + @abstractmethod + def cleanup(self): + """清理组件资源""" + pass + + @abstractmethod + def update(self, dt: float): + """ + 更新组件状态 + + Args: + dt: 时间增量 + """ + pass + + def enable(self): + """启用组件""" + self.enabled = True + + def disable(self): + """禁用组件""" + self.enabled = False + + def is_enabled(self) -> bool: + """ + 检查组件是否启用 + + Returns: + 是否启用 + """ + return self.enabled + + def set_property(self, name: str, value: Any) -> bool: + """ + 设置属性 + + Args: + name: 属性名 + value: 属性值 + + Returns: + 是否设置成功 + """ + try: + self.properties[name] = value + return True + except Exception: + return False + + def get_property(self, name: str, default: Any = None) -> Any: + """ + 获取属性 + + Args: + name: 属性名 + default: 默认值 + + Returns: + 属性值 + """ + return self.properties.get(name, default) + + def remove_property(self, name: str) -> bool: + """ + 移除属性 + + Args: + name: 属性名 + + Returns: + 是否移除成功 + """ + if name in self.properties: + del self.properties[name] + return True + return False + + def get_all_properties(self) -> Dict[str, Any]: + """ + 获取所有属性 + + Returns: + 属性字典 + """ + return self.properties.copy() + + +class AudioSourceComponent(AudioComponent): + """ + 音频源组件 + 用于为游戏对象添加音频播放功能 + """ + + def __init__(self, plugin, name: str = ""): + super().__init__(plugin, name) + self.sound_id: Optional[str] = None + self.spatial = False + self.loop = False + self.volume = 1.0 + self.attached_object = None + + def initialize(self) -> bool: + """初始化音频源组件""" + # 音频源组件在需要时才创建音效 + return True + + def cleanup(self): + """清理音频源组件""" + if self.sound_id: + self.plugin.audio_source_manager.delete_sound(self.sound_id) + self.sound_id = None + + def update(self, dt: float): + """更新音频源组件""" + if not self.enabled: + return + + # 如果附加到对象,同步位置 + if self.attached_object and self.sound_id and self.spatial: + pos = self.attached_object.getPos() + self.plugin.audio_source_manager.set_sound_position( + self.sound_id, (pos.x, pos.y, pos.z)) + + def load_sound(self, filepath: str, spatial: bool = False, + loop: bool = False, volume: float = 1.0) -> bool: + """ + 加载音频文件 + + Args: + filepath: 音频文件路径 + spatial: 是否为3D音效 + loop: 是否循环播放 + volume: 音量 + + Returns: + 是否加载成功 + """ + try: + # 删除现有音效 + if self.sound_id: + self.plugin.audio_source_manager.delete_sound(self.sound_id) + + # 创建新音效 + if spatial: + pos = (0, 0, 0) + if self.attached_object: + obj_pos = self.attached_object.getPos() + pos = (obj_pos.x, obj_pos.y, obj_pos.z) + self.sound_id = self.plugin.audio_source_manager.create_3d_sound( + filepath, pos, loop, volume) + else: + self.sound_id = self.plugin.audio_source_manager.create_2d_sound( + filepath, loop, volume) + + if self.sound_id: + self.spatial = spatial + self.loop = loop + self.volume = volume + return True + else: + return False + + except Exception as e: + print(f"✗ 加载音频文件失败: {e}") + return False + + def play(self) -> bool: + """ + 播放音效 + + Returns: + 是否播放成功 + """ + if self.sound_id: + return self.plugin.audio_source_manager.play_sound(self.sound_id) + return False + + def stop(self) -> bool: + """ + 停止音效 + + Returns: + 是否停止成功 + """ + if self.sound_id: + return self.plugin.audio_source_manager.stop_sound(self.sound_id) + return False + + def pause(self) -> bool: + """ + 暂停音效 + + Returns: + 是否暂停成功 + """ + if self.sound_id: + return self.plugin.audio_source_manager.pause_sound(self.sound_id) + return False + + def resume(self) -> bool: + """ + 恢复音效 + + Returns: + 是否恢复成功 + """ + if self.sound_id: + return self.plugin.audio_source_manager.resume_sound(self.sound_id) + return False + + def is_playing(self) -> bool: + """ + 检查音效是否正在播放 + + Returns: + 是否正在播放 + """ + if self.sound_id: + return self.plugin.audio_source_manager.is_sound_playing(self.sound_id) + return False + + def set_volume(self, volume: float) -> bool: + """ + 设置音量 + + Args: + volume: 音量 (0.0-1.0) + + Returns: + 是否设置成功 + """ + if self.sound_id: + success = self.plugin.audio_source_manager.set_sound_volume(self.sound_id, volume) + if success: + self.volume = volume + return success + return False + + def set_loop(self, loop: bool) -> bool: + """ + 设置循环模式 + + Args: + loop: 是否循环 + + Returns: + 是否设置成功 + """ + if self.sound_id: + success = self.plugin.audio_source_manager.set_sound_loop(self.sound_id, loop) + if success: + self.loop = loop + return success + return False + + def attach_to_object(self, obj): + """ + 附加到游戏对象 + + Args: + obj: 游戏对象 + """ + self.attached_object = obj + + def detach_from_object(self): + """从游戏对象分离""" + self.attached_object = None + + +class AudioListenerComponent(AudioComponent): + """ + 音频听者组件 + 用于设置音频听者位置和方向 + """ + + def __init__(self, plugin, name: str = ""): + super().__init__(plugin, name) + self.attached_object = None + + def initialize(self) -> bool: + """初始化音频听者组件""" + return True + + def cleanup(self): + """清理音频听者组件""" + pass + + def update(self, dt: float): + """更新音频听者组件""" + if not self.enabled: + return + + # 如果附加到对象,同步位置和方向 + if self.attached_object: + pos = self.attached_object.getPos() + hpr = self.attached_object.getHpr() + self.plugin.set_listener_position((pos.x, pos.y, pos.z)) + self.plugin.set_listener_orientation((hpr.x, hpr.y, hpr.z)) + + def attach_to_object(self, obj): + """ + 附加到游戏对象 + + Args: + obj: 游戏对象 + """ + self.attached_object = obj + + def detach_from_object(self): + """从游戏对象分离""" + self.attached_object = None + + +class AudioEffectComponent(AudioComponent): + """ + 音频效果组件 + 用于为音频源应用效果 + """ + + def __init__(self, plugin, name: str = ""): + super().__init__(plugin, name) + self.effect_id: Optional[str] = None + self.effect_type = "" + self.target_sound_id: Optional[str] = None + + def initialize(self) -> bool: + """初始化音频效果组件""" + return True + + def cleanup(self): + """清理音频效果组件""" + if self.effect_id: + self.plugin.audio_effect_processor.delete_effect(self.effect_id) + self.effect_id = None + + def update(self, dt: float): + """更新音频效果组件""" + if not self.enabled: + return + + def create_effect(self, effect_type: str, parameters: Dict[str, Any]) -> bool: + """ + 创建音频效果 + + Args: + effect_type: 效果类型 + parameters: 效果参数 + + Returns: + 是否创建成功 + """ + try: + # 删除现有效果 + if self.effect_id: + self.plugin.audio_effect_processor.delete_effect(self.effect_id) + + # 创建新效果 + self.effect_id = self.plugin.audio_effect_processor.create_effect( + effect_type, parameters) + + if self.effect_id: + self.effect_type = effect_type + return True + else: + return False + + except Exception as e: + print(f"✗ 创建音频效果失败: {e}") + return False + + def apply_to_sound(self, sound_id: str) -> bool: + """ + 应用效果到音效 + + Args: + sound_id: 音源ID + + Returns: + 是否应用成功 + """ + if self.effect_id: + success = self.plugin.audio_effect_processor.apply_effect_to_sound( + sound_id, self.effect_id) + if success: + self.target_sound_id = sound_id + return success + return False + + def remove_from_sound(self, sound_id: str) -> bool: + """ + 从音效移除效果 + + Args: + sound_id: 音源ID + + Returns: + 是否移除成功 + """ + if self.effect_id: + success = self.plugin.audio_effect_processor.remove_effect_from_sound( + sound_id, self.effect_id) + if success and self.target_sound_id == sound_id: + self.target_sound_id = None + return success + return False + + def set_parameter(self, parameter: str, value: Any) -> bool: + """ + 设置效果参数 + + Args: + parameter: 参数名 + value: 参数值 + + Returns: + 是否设置成功 + """ + if self.effect_id: + return self.plugin.audio_effect_processor.set_effect_parameter( + self.effect_id, parameter, value) return False \ No newline at end of file diff --git a/plugins/user/spatial_audio/core/audio_source_manager.py b/plugins/user/spatial_audio/core/audio_source_manager.py index 86a8df44..1b33e206 100644 --- a/plugins/user/spatial_audio/core/audio_source_manager.py +++ b/plugins/user/spatial_audio/core/audio_source_manager.py @@ -1,528 +1,528 @@ -""" -音频源管理器 -管理3D空间音效的音频源创建、播放和控制 -""" - -import uuid -from typing import Dict, Optional, Any, Tuple -from panda3d.core import AudioSound, NodePath -import os - -class AudioSourceManager: - """ - 音频源管理器 - 管理3D空间音效的音频源创建、播放和控制 - """ - - def __init__(self, plugin): - """ - 初始化音频源管理器 - - Args: - plugin: 3D音频插件实例 - """ - self.plugin = plugin - self.audio_sources: Dict[str, Dict[str, Any]] = {} - self.stats = { - 'total_created': 0, - 'currently_playing': 0, - 'total_played': 0 - } - - def create_3d_sound(self, filepath: str, position: Tuple[float, float, float] = (0, 0, 0), - loop: bool = False, volume: float = 1.0) -> Optional[str]: - """ - 创建3D音效 - - Args: - filepath: 音频文件路径 - position: 音源位置 (x, y, z) - loop: 是否循环播放 - volume: 音量 (0.0-1.0) - - Returns: - 音源ID,如果创建失败则返回None - """ - try: - # 检查文件是否存在 - if not os.path.exists(filepath): - print(f"✗ 音频文件不存在: {filepath}") - return None - - # 创建音源节点 - sound_node = NodePath(f"sound_{uuid.uuid4()}") - sound_node.reparentTo(self.plugin.plugin_manager.world.render) - sound_node.setPos(position[0], position[1], position[2]) - - # 加载音频文件 - audio_manager = self.plugin.audio_manager - sound = audio_manager.loadSfx(filepath) - - if not sound: - print(f"✗ 无法加载音频文件: {filepath}") - sound_node.removeNode() - return None - - # 设置音频属性 - sound.setLoop(loop) - sound.setVolume(volume) - - # 将音源附加到节点 - sound_node.setSound(sound) - - # 生成唯一ID - sound_id = str(uuid.uuid4()) - - # 存储音源信息 - self.audio_sources[sound_id] = { - 'sound': sound, - 'node': sound_node, - 'type': '3d', - 'loop': loop, - 'volume': volume, - 'position': position, - 'filepath': filepath - } - - # 更新统计信息 - self.stats['total_created'] += 1 - - print(f"✓ 3D音效创建成功: {sound_id} ({filepath})") - return sound_id - - except Exception as e: - print(f"✗ 创建3D音效失败: {e}") - return None - - def create_2d_sound(self, filepath: str, loop: bool = False, volume: float = 1.0) -> Optional[str]: - """ - 创建2D音效 - - Args: - filepath: 音频文件路径 - loop: 是否循环播放 - volume: 音量 (0.0-1.0) - - Returns: - 音源ID,如果创建失败则返回None - """ - try: - # 检查文件是否存在 - if not os.path.exists(filepath): - print(f"✗ 音频文件不存在: {filepath}") - return None - - # 加载音频文件 - audio_manager = self.plugin.audio_manager - sound = audio_manager.loadSfx(filepath) - - if not sound: - print(f"✗ 无法加载音频文件: {filepath}") - return None - - # 设置音频属性 - sound.setLoop(loop) - sound.setVolume(volume) - - # 生成唯一ID - sound_id = str(uuid.uuid4()) - - # 存储音源信息 - self.audio_sources[sound_id] = { - 'sound': sound, - 'node': None, - 'type': '2d', - 'loop': loop, - 'volume': volume, - 'position': (0, 0, 0), - 'filepath': filepath - } - - # 更新统计信息 - self.stats['total_created'] += 1 - - print(f"✓ 2D音效创建成功: {sound_id} ({filepath})") - return sound_id - - except Exception as e: - print(f"✗ 创建2D音效失败: {e}") - return None - - def play_sound(self, sound_id: str) -> bool: - """ - 播放音效 - - Args: - sound_id: 音源ID - - Returns: - 是否播放成功 - """ - if sound_id not in self.audio_sources: - print(f"✗ 音源不存在: {sound_id}") - return False - - try: - sound_info = self.audio_sources[sound_id] - sound = sound_info['sound'] - - # 检查音效状态 - if sound.status() == AudioSound.PLAYING: - print(f"⚠ 音效已在播放中: {sound_id}") - return True - - # 播放音效 - sound.play() - - # 更新统计信息 - self.stats['currently_playing'] += 1 - self.stats['total_played'] += 1 - - print(f"✓ 音效开始播放: {sound_id}") - return True - - except Exception as e: - print(f"✗ 播放音效失败: {e}") - return False - - def stop_sound(self, sound_id: str) -> bool: - """ - 停止音效 - - Args: - sound_id: 音源ID - - Returns: - 是否停止成功 - """ - if sound_id not in self.audio_sources: - print(f"✗ 音源不存在: {sound_id}") - return False - - try: - sound_info = self.audio_sources[sound_id] - sound = sound_info['sound'] - - # 停止音效 - sound.stop() - - # 更新统计信息 - if sound.status() == AudioSound.PLAYING: - self.stats['currently_playing'] -= 1 - - print(f"✓ 音效已停止: {sound_id}") - return True - - except Exception as e: - print(f"✗ 停止音效失败: {e}") - return False - - def pause_sound(self, sound_id: str) -> bool: - """ - 暂停音效 - - Args: - sound_id: 音源ID - - Returns: - 是否暂停成功 - """ - if sound_id not in self.audio_sources: - print(f"✗ 音源不存在: {sound_id}") - return False - - try: - sound_info = self.audio_sources[sound_id] - sound = sound_info['sound'] - - # 暂停音效 - sound.setActive(False) - - # 更新统计信息 - if sound.status() == AudioSound.PLAYING: - self.stats['currently_playing'] -= 1 - - print(f"✓ 音效已暂停: {sound_id}") - return True - - except Exception as e: - print(f"✗ 暂停音效失败: {e}") - return False - - def resume_sound(self, sound_id: str) -> bool: - """ - 恢复音效 - - Args: - sound_id: 音源ID - - Returns: - 是否恢复成功 - """ - if sound_id not in self.audio_sources: - print(f"✗ 音源不存在: {sound_id}") - return False - - try: - sound_info = self.audio_sources[sound_id] - sound = sound_info['sound'] - - # 恢复音效 - sound.setActive(True) - - # 更新统计信息 - if sound.status() != AudioSound.PLAYING: - self.stats['currently_playing'] += 1 - - print(f"✓ 音效已恢复: {sound_id}") - return True - - except Exception as e: - print(f"✗ 恢复音效失败: {e}") - return False - - def set_sound_position(self, sound_id: str, position: Tuple[float, float, float]) -> bool: - """ - 设置音源位置 - - Args: - sound_id: 音源ID - position: 位置 (x, y, z) - - Returns: - 是否设置成功 - """ - if sound_id not in self.audio_sources: - print(f"✗ 音源不存在: {sound_id}") - return False - - try: - sound_info = self.audio_sources[sound_id] - - # 检查是否为3D音效 - if sound_info['type'] != '3d': - print(f"✗ 只有3D音效可以设置位置: {sound_id}") - return False - - node = sound_info['node'] - node.setPos(position[0], position[1], position[2]) - sound_info['position'] = position - - print(f"✓ 音源位置已设置: {sound_id} -> {position}") - return True - - except Exception as e: - print(f"✗ 设置音源位置失败: {e}") - return False - - def set_sound_volume(self, sound_id: str, volume: float) -> bool: - """ - 设置音源音量 - - Args: - sound_id: 音源ID - volume: 音量 (0.0-1.0) - - Returns: - 是否设置成功 - """ - if sound_id not in self.audio_sources: - print(f"✗ 音源不存在: {sound_id}") - return False - - try: - sound_info = self.audio_sources[sound_id] - sound = sound_info['sound'] - - # 限制音量范围 - volume = max(0.0, min(1.0, volume)) - - sound.setVolume(volume) - sound_info['volume'] = volume - - print(f"✓ 音源音量已设置: {sound_id} -> {volume}") - return True - - except Exception as e: - print(f"✗ 设置音源音量失败: {e}") - return False - - def set_sound_loop(self, sound_id: str, loop: bool) -> bool: - """ - 设置音源循环模式 - - Args: - sound_id: 音源ID - loop: 是否循环 - - Returns: - 是否设置成功 - """ - if sound_id not in self.audio_sources: - print(f"✗ 音源不存在: {sound_id}") - return False - - try: - sound_info = self.audio_sources[sound_id] - sound = sound_info['sound'] - - sound.setLoop(loop) - sound_info['loop'] = loop - - print(f"✓ 音源循环模式已设置: {sound_id} -> {'循环' if loop else '单次'}") - return True - - except Exception as e: - print(f"✗ 设置音源循环模式失败: {e}") - return False - - def is_sound_playing(self, sound_id: str) -> bool: - """ - 检查音效是否正在播放 - - Args: - sound_id: 音源ID - - Returns: - 是否正在播放 - """ - if sound_id not in self.audio_sources: - return False - - try: - sound_info = self.audio_sources[sound_id] - sound = sound_info['sound'] - return sound.status() == AudioSound.PLAYING - except Exception: - return False - - def get_sound_duration(self, sound_id: str) -> float: - """ - 获取音效时长 - - Args: - sound_id: 音源ID - - Returns: - 音效时长(秒) - """ - if sound_id not in self.audio_sources: - return 0.0 - - try: - sound_info = self.audio_sources[sound_id] - sound = sound_info['sound'] - return sound.length() - except Exception: - return 0.0 - - def get_sound_position(self, sound_id: str) -> Tuple[float, float, float]: - """ - 获取音源位置 - - Args: - sound_id: 音源ID - - Returns: - 音源位置 (x, y, z) - """ - if sound_id not in self.audio_sources: - return (0, 0, 0) - - try: - sound_info = self.audio_sources[sound_id] - return sound_info['position'] - except Exception: - return (0, 0, 0) - - def delete_sound(self, sound_id: str) -> bool: - """ - 删除音效 - - Args: - sound_id: 音源ID - - Returns: - 是否删除成功 - """ - if sound_id not in self.audio_sources: - print(f"✗ 音源不存在: {sound_id}") - return False - - try: - sound_info = self.audio_sources[sound_id] - - # 停止播放 - if self.is_sound_playing(sound_id): - self.stop_sound(sound_id) - - # 清理资源 - sound = sound_info['sound'] - node = sound_info['node'] - - if node: - node.removeNode() - - # 从管理器中移除 - del self.audio_sources[sound_id] - - print(f"✓ 音效已删除: {sound_id}") - return True - - except Exception as e: - print(f"✗ 删除音效失败: {e}") - return False - - def stop_all_sounds(self): - """停止所有音频""" - sound_ids = list(self.audio_sources.keys()) - for sound_id in sound_ids: - self.stop_sound(sound_id) - print("✓ 所有音频已停止") - - def cleanup(self): - """清理所有资源""" - self.stop_all_sounds() - self.audio_sources.clear() - self.stats = { - 'total_created': 0, - 'currently_playing': 0, - 'total_played': 0 - } - print("✓ 音频源管理器资源已清理") - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def update(self, dt: float): - """ - 更新音频源状态 - - Args: - dt: 时间增量 - """ - # 检查已完成播放的音效 - finished_sounds = [] - for sound_id, sound_info in self.audio_sources.items(): - sound = sound_info['sound'] - if sound.status() != AudioSound.PLAYING and sound_info['loop'] == False: - # 检查是否是从播放状态变为停止状态 - if hasattr(sound_info, '_was_playing') and sound_info['_was_playing']: - self.stats['currently_playing'] -= 1 - finished_sounds.append(sound_id) - # 更新播放状态 - sound_info['_was_playing'] = (sound.status() == AudioSound.PLAYING) - - # 清理已完成的非循环音效 - for sound_id in finished_sounds: - if sound_id in self.audio_sources: - sound_info = self.audio_sources[sound_id] - if not sound_info['loop']: - self.stats['currently_playing'] -= 1 - +""" +音频源管理器 +管理3D空间音效的音频源创建、播放和控制 +""" + +import uuid +from typing import Dict, Optional, Any, Tuple +from panda3d.core import AudioSound, NodePath +import os + +class AudioSourceManager: + """ + 音频源管理器 + 管理3D空间音效的音频源创建、播放和控制 + """ + + def __init__(self, plugin): + """ + 初始化音频源管理器 + + Args: + plugin: 3D音频插件实例 + """ + self.plugin = plugin + self.audio_sources: Dict[str, Dict[str, Any]] = {} + self.stats = { + 'total_created': 0, + 'currently_playing': 0, + 'total_played': 0 + } + + def create_3d_sound(self, filepath: str, position: Tuple[float, float, float] = (0, 0, 0), + loop: bool = False, volume: float = 1.0) -> Optional[str]: + """ + 创建3D音效 + + Args: + filepath: 音频文件路径 + position: 音源位置 (x, y, z) + loop: 是否循环播放 + volume: 音量 (0.0-1.0) + + Returns: + 音源ID,如果创建失败则返回None + """ + try: + # 检查文件是否存在 + if not os.path.exists(filepath): + print(f"✗ 音频文件不存在: {filepath}") + return None + + # 创建音源节点 + sound_node = NodePath(f"sound_{uuid.uuid4()}") + sound_node.reparentTo(self.plugin.plugin_manager.world.render) + sound_node.setPos(position[0], position[1], position[2]) + + # 加载音频文件 + audio_manager = self.plugin.audio_manager + sound = audio_manager.loadSfx(filepath) + + if not sound: + print(f"✗ 无法加载音频文件: {filepath}") + sound_node.removeNode() + return None + + # 设置音频属性 + sound.setLoop(loop) + sound.setVolume(volume) + + # 将音源附加到节点 + sound_node.setSound(sound) + + # 生成唯一ID + sound_id = str(uuid.uuid4()) + + # 存储音源信息 + self.audio_sources[sound_id] = { + 'sound': sound, + 'node': sound_node, + 'type': '3d', + 'loop': loop, + 'volume': volume, + 'position': position, + 'filepath': filepath + } + + # 更新统计信息 + self.stats['total_created'] += 1 + + print(f"✓ 3D音效创建成功: {sound_id} ({filepath})") + return sound_id + + except Exception as e: + print(f"✗ 创建3D音效失败: {e}") + return None + + def create_2d_sound(self, filepath: str, loop: bool = False, volume: float = 1.0) -> Optional[str]: + """ + 创建2D音效 + + Args: + filepath: 音频文件路径 + loop: 是否循环播放 + volume: 音量 (0.0-1.0) + + Returns: + 音源ID,如果创建失败则返回None + """ + try: + # 检查文件是否存在 + if not os.path.exists(filepath): + print(f"✗ 音频文件不存在: {filepath}") + return None + + # 加载音频文件 + audio_manager = self.plugin.audio_manager + sound = audio_manager.loadSfx(filepath) + + if not sound: + print(f"✗ 无法加载音频文件: {filepath}") + return None + + # 设置音频属性 + sound.setLoop(loop) + sound.setVolume(volume) + + # 生成唯一ID + sound_id = str(uuid.uuid4()) + + # 存储音源信息 + self.audio_sources[sound_id] = { + 'sound': sound, + 'node': None, + 'type': '2d', + 'loop': loop, + 'volume': volume, + 'position': (0, 0, 0), + 'filepath': filepath + } + + # 更新统计信息 + self.stats['total_created'] += 1 + + print(f"✓ 2D音效创建成功: {sound_id} ({filepath})") + return sound_id + + except Exception as e: + print(f"✗ 创建2D音效失败: {e}") + return None + + def play_sound(self, sound_id: str) -> bool: + """ + 播放音效 + + Args: + sound_id: 音源ID + + Returns: + 是否播放成功 + """ + if sound_id not in self.audio_sources: + print(f"✗ 音源不存在: {sound_id}") + return False + + try: + sound_info = self.audio_sources[sound_id] + sound = sound_info['sound'] + + # 检查音效状态 + if sound.status() == AudioSound.PLAYING: + print(f"⚠ 音效已在播放中: {sound_id}") + return True + + # 播放音效 + sound.play() + + # 更新统计信息 + self.stats['currently_playing'] += 1 + self.stats['total_played'] += 1 + + print(f"✓ 音效开始播放: {sound_id}") + return True + + except Exception as e: + print(f"✗ 播放音效失败: {e}") + return False + + def stop_sound(self, sound_id: str) -> bool: + """ + 停止音效 + + Args: + sound_id: 音源ID + + Returns: + 是否停止成功 + """ + if sound_id not in self.audio_sources: + print(f"✗ 音源不存在: {sound_id}") + return False + + try: + sound_info = self.audio_sources[sound_id] + sound = sound_info['sound'] + + # 停止音效 + sound.stop() + + # 更新统计信息 + if sound.status() == AudioSound.PLAYING: + self.stats['currently_playing'] -= 1 + + print(f"✓ 音效已停止: {sound_id}") + return True + + except Exception as e: + print(f"✗ 停止音效失败: {e}") + return False + + def pause_sound(self, sound_id: str) -> bool: + """ + 暂停音效 + + Args: + sound_id: 音源ID + + Returns: + 是否暂停成功 + """ + if sound_id not in self.audio_sources: + print(f"✗ 音源不存在: {sound_id}") + return False + + try: + sound_info = self.audio_sources[sound_id] + sound = sound_info['sound'] + + # 暂停音效 + sound.setActive(False) + + # 更新统计信息 + if sound.status() == AudioSound.PLAYING: + self.stats['currently_playing'] -= 1 + + print(f"✓ 音效已暂停: {sound_id}") + return True + + except Exception as e: + print(f"✗ 暂停音效失败: {e}") + return False + + def resume_sound(self, sound_id: str) -> bool: + """ + 恢复音效 + + Args: + sound_id: 音源ID + + Returns: + 是否恢复成功 + """ + if sound_id not in self.audio_sources: + print(f"✗ 音源不存在: {sound_id}") + return False + + try: + sound_info = self.audio_sources[sound_id] + sound = sound_info['sound'] + + # 恢复音效 + sound.setActive(True) + + # 更新统计信息 + if sound.status() != AudioSound.PLAYING: + self.stats['currently_playing'] += 1 + + print(f"✓ 音效已恢复: {sound_id}") + return True + + except Exception as e: + print(f"✗ 恢复音效失败: {e}") + return False + + def set_sound_position(self, sound_id: str, position: Tuple[float, float, float]) -> bool: + """ + 设置音源位置 + + Args: + sound_id: 音源ID + position: 位置 (x, y, z) + + Returns: + 是否设置成功 + """ + if sound_id not in self.audio_sources: + print(f"✗ 音源不存在: {sound_id}") + return False + + try: + sound_info = self.audio_sources[sound_id] + + # 检查是否为3D音效 + if sound_info['type'] != '3d': + print(f"✗ 只有3D音效可以设置位置: {sound_id}") + return False + + node = sound_info['node'] + node.setPos(position[0], position[1], position[2]) + sound_info['position'] = position + + print(f"✓ 音源位置已设置: {sound_id} -> {position}") + return True + + except Exception as e: + print(f"✗ 设置音源位置失败: {e}") + return False + + def set_sound_volume(self, sound_id: str, volume: float) -> bool: + """ + 设置音源音量 + + Args: + sound_id: 音源ID + volume: 音量 (0.0-1.0) + + Returns: + 是否设置成功 + """ + if sound_id not in self.audio_sources: + print(f"✗ 音源不存在: {sound_id}") + return False + + try: + sound_info = self.audio_sources[sound_id] + sound = sound_info['sound'] + + # 限制音量范围 + volume = max(0.0, min(1.0, volume)) + + sound.setVolume(volume) + sound_info['volume'] = volume + + print(f"✓ 音源音量已设置: {sound_id} -> {volume}") + return True + + except Exception as e: + print(f"✗ 设置音源音量失败: {e}") + return False + + def set_sound_loop(self, sound_id: str, loop: bool) -> bool: + """ + 设置音源循环模式 + + Args: + sound_id: 音源ID + loop: 是否循环 + + Returns: + 是否设置成功 + """ + if sound_id not in self.audio_sources: + print(f"✗ 音源不存在: {sound_id}") + return False + + try: + sound_info = self.audio_sources[sound_id] + sound = sound_info['sound'] + + sound.setLoop(loop) + sound_info['loop'] = loop + + print(f"✓ 音源循环模式已设置: {sound_id} -> {'循环' if loop else '单次'}") + return True + + except Exception as e: + print(f"✗ 设置音源循环模式失败: {e}") + return False + + def is_sound_playing(self, sound_id: str) -> bool: + """ + 检查音效是否正在播放 + + Args: + sound_id: 音源ID + + Returns: + 是否正在播放 + """ + if sound_id not in self.audio_sources: + return False + + try: + sound_info = self.audio_sources[sound_id] + sound = sound_info['sound'] + return sound.status() == AudioSound.PLAYING + except Exception: + return False + + def get_sound_duration(self, sound_id: str) -> float: + """ + 获取音效时长 + + Args: + sound_id: 音源ID + + Returns: + 音效时长(秒) + """ + if sound_id not in self.audio_sources: + return 0.0 + + try: + sound_info = self.audio_sources[sound_id] + sound = sound_info['sound'] + return sound.length() + except Exception: + return 0.0 + + def get_sound_position(self, sound_id: str) -> Tuple[float, float, float]: + """ + 获取音源位置 + + Args: + sound_id: 音源ID + + Returns: + 音源位置 (x, y, z) + """ + if sound_id not in self.audio_sources: + return (0, 0, 0) + + try: + sound_info = self.audio_sources[sound_id] + return sound_info['position'] + except Exception: + return (0, 0, 0) + + def delete_sound(self, sound_id: str) -> bool: + """ + 删除音效 + + Args: + sound_id: 音源ID + + Returns: + 是否删除成功 + """ + if sound_id not in self.audio_sources: + print(f"✗ 音源不存在: {sound_id}") + return False + + try: + sound_info = self.audio_sources[sound_id] + + # 停止播放 + if self.is_sound_playing(sound_id): + self.stop_sound(sound_id) + + # 清理资源 + sound = sound_info['sound'] + node = sound_info['node'] + + if node: + node.removeNode() + + # 从管理器中移除 + del self.audio_sources[sound_id] + + print(f"✓ 音效已删除: {sound_id}") + return True + + except Exception as e: + print(f"✗ 删除音效失败: {e}") + return False + + def stop_all_sounds(self): + """停止所有音频""" + sound_ids = list(self.audio_sources.keys()) + for sound_id in sound_ids: + self.stop_sound(sound_id) + print("✓ 所有音频已停止") + + def cleanup(self): + """清理所有资源""" + self.stop_all_sounds() + self.audio_sources.clear() + self.stats = { + 'total_created': 0, + 'currently_playing': 0, + 'total_played': 0 + } + print("✓ 音频源管理器资源已清理") + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def update(self, dt: float): + """ + 更新音频源状态 + + Args: + dt: 时间增量 + """ + # 检查已完成播放的音效 + finished_sounds = [] + for sound_id, sound_info in self.audio_sources.items(): + sound = sound_info['sound'] + if sound.status() != AudioSound.PLAYING and sound_info['loop'] == False: + # 检查是否是从播放状态变为停止状态 + if hasattr(sound_info, '_was_playing') and sound_info['_was_playing']: + self.stats['currently_playing'] -= 1 + finished_sounds.append(sound_id) + # 更新播放状态 + sound_info['_was_playing'] = (sound.status() == AudioSound.PLAYING) + + # 清理已完成的非循环音效 + for sound_id in finished_sounds: + if sound_id in self.audio_sources: + sound_info = self.audio_sources[sound_id] + if not sound_info['loop']: + self.stats['currently_playing'] -= 1 + diff --git a/plugins/user/spatial_audio/core/environment_audio.py b/plugins/user/spatial_audio/core/environment_audio.py index deab3322..f312e616 100644 --- a/plugins/user/spatial_audio/core/environment_audio.py +++ b/plugins/user/spatial_audio/core/environment_audio.py @@ -1,498 +1,498 @@ -""" -环境音频管理器 -管理环境音效和区域音频效果 -""" - -import math -from typing import Dict, List, Any, Tuple, Optional -from panda3d.core import Vec3 - -class EnvironmentAudioManager: - """ - 环境音频管理器 - 管理环境音效和区域音频效果 - """ - - def __init__(self, plugin): - """ - 初始化环境音频管理器 - - Args: - plugin: 3D音频插件实例 - """ - self.plugin = plugin - self.zones: Dict[str, Dict[str, Any]] = {} - self.ambient_sounds: Dict[str, str] = {} # 环境音效 - self.enabled = True - self.listener_position = (0, 0, 0) - self.stats = { - 'zones_created': 0, - 'active_zones': 0, - 'ambient_sounds': 0 - } - - def create_zone(self, zone_id: str, position: Tuple[float, float, float], - radius: float, properties: Dict[str, Any]) -> bool: - """ - 创建环境区域 - - Args: - zone_id: 区域ID - position: 位置 (x, y, z) - radius: 半径 - properties: 区域属性 - - Returns: - 是否创建成功 - """ - try: - # 检查区域ID是否已存在 - if zone_id in self.zones: - print(f"✗ 区域ID已存在: {zone_id}") - return False - - # 创建区域 - zone = { - 'id': zone_id, - 'position': position, - 'radius': radius, - 'properties': properties, - 'active': True, - 'ambient_sound': None - } - - # 存储区域 - self.zones[zone_id] = zone - - # 更新统计信息 - self.stats['zones_created'] += 1 - self.stats['active_zones'] += 1 - - print(f"✓ 环境区域创建成功: {zone_id}") - return True - - except Exception as e: - print(f"✗ 创建环境区域失败: {e}") - return False - - def delete_zone(self, zone_id: str) -> bool: - """ - 删除环境区域 - - Args: - zone_id: 区域ID - - Returns: - 是否删除成功 - """ - if zone_id not in self.zones: - print(f"✗ 区域不存在: {zone_id}") - return False - - try: - # 停止区域内的环境音效 - zone = self.zones[zone_id] - if zone['ambient_sound']: - ambient_sound_id = zone['ambient_sound'] - self.plugin.audio_source_manager.stop_sound(ambient_sound_id) - self.plugin.audio_source_manager.delete_sound(ambient_sound_id) - - # 删除区域 - del self.zones[zone_id] - - # 更新统计信息 - self.stats['active_zones'] -= 1 - - print(f"✓ 环境区域已删除: {zone_id}") - return True - - except Exception as e: - print(f"✗ 删除环境区域失败: {e}") - return False - - def set_zone_property(self, zone_id: str, property_name: str, value: Any) -> bool: - """ - 设置区域属性 - - Args: - zone_id: 区域ID - property_name: 属性名 - value: 属性值 - - Returns: - 是否设置成功 - """ - if zone_id not in self.zones: - print(f"✗ 区域不存在: {zone_id}") - return False - - try: - self.zones[zone_id]['properties'][property_name] = value - print(f"✓ 区域属性已设置: {zone_id}.{property_name} = {value}") - return True - - except Exception as e: - print(f"✗ 设置区域属性失败: {e}") - return False - - def get_zone_property(self, zone_id: str, property_name: str) -> Any: - """ - 获取区域属性 - - Args: - zone_id: 区域ID - property_name: 属性名 - - Returns: - 属性值 - """ - if zone_id not in self.zones: - return None - - return self.zones[zone_id]['properties'].get(property_name, None) - - def set_zone_position(self, zone_id: str, position: Tuple[float, float, float]) -> bool: - """ - 设置区域位置 - - Args: - zone_id: 区域ID - position: 位置 (x, y, z) - - Returns: - 是否设置成功 - """ - if zone_id not in self.zones: - print(f"✗ 区域不存在: {zone_id}") - return False - - try: - self.zones[zone_id]['position'] = position - print(f"✓ 区域位置已设置: {zone_id} -> {position}") - return True - - except Exception as e: - print(f"✗ 设置区域位置失败: {e}") - return False - - def set_zone_radius(self, zone_id: str, radius: float) -> bool: - """ - 设置区域半径 - - Args: - zone_id: 区域ID - radius: 半径 - - Returns: - 是否设置成功 - """ - if zone_id not in self.zones: - print(f"✗ 区域不存在: {zone_id}") - return False - - try: - self.zones[zone_id]['radius'] = max(0.1, radius) # 最小半径0.1 - print(f"✓ 区域半径已设置: {zone_id} -> {radius}") - return True - - except Exception as e: - print(f"✗ 设置区域半径失败: {e}") - return False - - def enable_zone(self, zone_id: str) -> bool: - """ - 启用区域 - - Args: - zone_id: 区域ID - - Returns: - 是否启用成功 - """ - if zone_id not in self.zones: - print(f"✗ 区域不存在: {zone_id}") - return False - - try: - self.zones[zone_id]['active'] = True - self.stats['active_zones'] += 1 - print(f"✓ 区域已启用: {zone_id}") - return True - - except Exception as e: - print(f"✗ 启用区域失败: {e}") - return False - - def disable_zone(self, zone_id: str) -> bool: - """ - 禁用区域 - - Args: - zone_id: 区域ID - - Returns: - 是否禁用成功 - """ - if zone_id not in self.zones: - print(f"✗ 区域不存在: {zone_id}") - return False - - try: - self.zones[zone_id]['active'] = False - self.stats['active_zones'] -= 1 - print(f"✓ 区域已禁用: {zone_id}") - return True - - except Exception as e: - print(f"✗ 禁用区域失败: {e}") - return False - - def add_ambient_sound_to_zone(self, zone_id: str, filepath: str, - volume: float = 1.0, loop: bool = True) -> bool: - """ - 为区域添加环境音效 - - Args: - zone_id: 区域ID - filepath: 音频文件路径 - volume: 音量 - loop: 是否循环 - - Returns: - 是否添加成功 - """ - if zone_id not in self.zones: - print(f"✗ 区域不存在: {zone_id}") - return False - - try: - # 创建3D音效 - zone = self.zones[zone_id] - position = zone['position'] - sound_id = self.plugin.audio_source_manager.create_3d_sound( - filepath, position, loop, volume) - - if not sound_id: - print(f"✗ 无法创建环境音效: {filepath}") - return False - - # 播放音效 - self.plugin.audio_source_manager.play_sound(sound_id) - - # 关联音效到区域 - zone['ambient_sound'] = sound_id - - # 更新统计信息 - self.stats['ambient_sounds'] += 1 - - print(f"✓ 环境音效已添加到区域: {zone_id} -> {filepath}") - return True - - except Exception as e: - print(f"✗ 添加环境音效到区域失败: {e}") - return False - - def remove_ambient_sound_from_zone(self, zone_id: str) -> bool: - """ - 从区域移除环境音效 - - Args: - zone_id: 区域ID - - Returns: - 是否移除成功 - """ - if zone_id not in self.zones: - print(f"✗ 区域不存在: {zone_id}") - return False - - try: - zone = self.zones[zone_id] - if zone['ambient_sound']: - # 停止并删除音效 - sound_id = zone['ambient_sound'] - self.plugin.audio_source_manager.stop_sound(sound_id) - self.plugin.audio_source_manager.delete_sound(sound_id) - - # 清除关联 - zone['ambient_sound'] = None - - # 更新统计信息 - self.stats['ambient_sounds'] -= 1 - - print(f"✓ 环境音效已从区域移除: {zone_id}") - return True - else: - print(f"⚠ 区域没有环境音效: {zone_id}") - return True - - except Exception as e: - print(f"✗ 从区域移除环境音效失败: {e}") - return False - - def set_ambient_sound_volume(self, zone_id: str, volume: float) -> bool: - """ - 设置区域环境音效音量 - - Args: - zone_id: 区域ID - volume: 音量 (0.0-1.0) - - Returns: - 是否设置成功 - """ - if zone_id not in self.zones: - print(f"✗ 区域不存在: {zone_id}") - return False - - try: - zone = self.zones[zone_id] - if zone['ambient_sound']: - sound_id = zone['ambient_sound'] - return self.plugin.audio_source_manager.set_sound_volume(sound_id, volume) - else: - print(f"⚠ 区域没有环境音效: {zone_id}") - return False - - except Exception as e: - print(f"✗ 设置环境音效音量失败: {e}") - return False - - def _calculate_distance(self, pos1: Tuple[float, float, float], - pos2: Tuple[float, float, float]) -> float: - """ - 计算两点间距离 - - Args: - pos1: 第一个点 - pos2: 第二个点 - - Returns: - 距离 - """ - dx = pos1[0] - pos2[0] - dy = pos1[1] - pos2[1] - dz = pos1[2] - pos2[2] - return math.sqrt(dx*dx + dy*dy + dz*dz) - - def _is_point_in_zone(self, point: Tuple[float, float, float], - zone_id: str) -> bool: - """ - 检查点是否在区域内 - - Args: - point: 检查点 - zone_id: 区域ID - - Returns: - 是否在区域内 - """ - if zone_id not in self.zones: - return False - - zone = self.zones[zone_id] - zone_position = zone['position'] - zone_radius = zone['radius'] - - distance = self._calculate_distance(point, zone_position) - return distance <= zone_radius - - def update(self, dt: float): - """ - 更新环境音频状态 - - Args: - dt: 时间增量 - """ - if not self.enabled: - return - - # 更新听者位置 - if self.plugin.listener: - listener_pos = self.plugin.listener.getPos() - self.listener_position = (listener_pos.x, listener_pos.y, listener_pos.z) - - # 更新各区域的环境音效 - for zone_id, zone in self.zones.items(): - if not zone['active']: - continue - - # 检查听者是否在区域内 - is_in_zone = self._is_point_in_zone(self.listener_position, zone_id) - - # 更新环境音效状态 - if zone['ambient_sound']: - sound_id = zone['ambient_sound'] - if is_in_zone: - # 在区域内,确保音效播放 - if not self.plugin.audio_source_manager.is_sound_playing(sound_id): - self.plugin.audio_source_manager.play_sound(sound_id) - else: - # 不在区域内,停止音效 - if self.plugin.audio_source_manager.is_sound_playing(sound_id): - self.plugin.audio_source_manager.stop_sound(sound_id) - - def toggle_enabled(self): - """切换启用状态""" - self.enabled = not self.enabled - print(f"✓ 环境音频管理器已{'启用' if self.enabled else '禁用'}") - - def get_zone_info(self, zone_id: str) -> Dict[str, Any]: - """ - 获取区域信息 - - Args: - zone_id: 区域ID - - Returns: - 区域信息字典 - """ - if zone_id not in self.zones: - return {} - - zone = self.zones[zone_id] - return { - 'id': zone['id'], - 'position': zone['position'], - 'radius': zone['radius'], - 'active': zone['active'], - 'has_ambient_sound': zone['ambient_sound'] is not None - } - - def get_all_zones_info(self) -> List[Dict[str, Any]]: - """ - 获取所有区域信息 - - Returns: - 区域信息列表 - """ - zones_info = [] - for zone_id in self.zones: - zones_info.append(self.get_zone_info(zone_id)) - return zones_info - - def cleanup(self): - """清理所有资源""" - # 删除所有区域 - zone_ids = list(self.zones.keys()) - for zone_id in zone_ids: - self.delete_zone(zone_id) - - self.zones.clear() - self.ambient_sounds.clear() - self.stats = { - 'zones_created': 0, - 'active_zones': 0, - 'ambient_sounds': 0 - } - print("✓ 环境音频管理器资源已清理") - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - +""" +环境音频管理器 +管理环境音效和区域音频效果 +""" + +import math +from typing import Dict, List, Any, Tuple, Optional +from panda3d.core import Vec3 + +class EnvironmentAudioManager: + """ + 环境音频管理器 + 管理环境音效和区域音频效果 + """ + + def __init__(self, plugin): + """ + 初始化环境音频管理器 + + Args: + plugin: 3D音频插件实例 + """ + self.plugin = plugin + self.zones: Dict[str, Dict[str, Any]] = {} + self.ambient_sounds: Dict[str, str] = {} # 环境音效 + self.enabled = True + self.listener_position = (0, 0, 0) + self.stats = { + 'zones_created': 0, + 'active_zones': 0, + 'ambient_sounds': 0 + } + + def create_zone(self, zone_id: str, position: Tuple[float, float, float], + radius: float, properties: Dict[str, Any]) -> bool: + """ + 创建环境区域 + + Args: + zone_id: 区域ID + position: 位置 (x, y, z) + radius: 半径 + properties: 区域属性 + + Returns: + 是否创建成功 + """ + try: + # 检查区域ID是否已存在 + if zone_id in self.zones: + print(f"✗ 区域ID已存在: {zone_id}") + return False + + # 创建区域 + zone = { + 'id': zone_id, + 'position': position, + 'radius': radius, + 'properties': properties, + 'active': True, + 'ambient_sound': None + } + + # 存储区域 + self.zones[zone_id] = zone + + # 更新统计信息 + self.stats['zones_created'] += 1 + self.stats['active_zones'] += 1 + + print(f"✓ 环境区域创建成功: {zone_id}") + return True + + except Exception as e: + print(f"✗ 创建环境区域失败: {e}") + return False + + def delete_zone(self, zone_id: str) -> bool: + """ + 删除环境区域 + + Args: + zone_id: 区域ID + + Returns: + 是否删除成功 + """ + if zone_id not in self.zones: + print(f"✗ 区域不存在: {zone_id}") + return False + + try: + # 停止区域内的环境音效 + zone = self.zones[zone_id] + if zone['ambient_sound']: + ambient_sound_id = zone['ambient_sound'] + self.plugin.audio_source_manager.stop_sound(ambient_sound_id) + self.plugin.audio_source_manager.delete_sound(ambient_sound_id) + + # 删除区域 + del self.zones[zone_id] + + # 更新统计信息 + self.stats['active_zones'] -= 1 + + print(f"✓ 环境区域已删除: {zone_id}") + return True + + except Exception as e: + print(f"✗ 删除环境区域失败: {e}") + return False + + def set_zone_property(self, zone_id: str, property_name: str, value: Any) -> bool: + """ + 设置区域属性 + + Args: + zone_id: 区域ID + property_name: 属性名 + value: 属性值 + + Returns: + 是否设置成功 + """ + if zone_id not in self.zones: + print(f"✗ 区域不存在: {zone_id}") + return False + + try: + self.zones[zone_id]['properties'][property_name] = value + print(f"✓ 区域属性已设置: {zone_id}.{property_name} = {value}") + return True + + except Exception as e: + print(f"✗ 设置区域属性失败: {e}") + return False + + def get_zone_property(self, zone_id: str, property_name: str) -> Any: + """ + 获取区域属性 + + Args: + zone_id: 区域ID + property_name: 属性名 + + Returns: + 属性值 + """ + if zone_id not in self.zones: + return None + + return self.zones[zone_id]['properties'].get(property_name, None) + + def set_zone_position(self, zone_id: str, position: Tuple[float, float, float]) -> bool: + """ + 设置区域位置 + + Args: + zone_id: 区域ID + position: 位置 (x, y, z) + + Returns: + 是否设置成功 + """ + if zone_id not in self.zones: + print(f"✗ 区域不存在: {zone_id}") + return False + + try: + self.zones[zone_id]['position'] = position + print(f"✓ 区域位置已设置: {zone_id} -> {position}") + return True + + except Exception as e: + print(f"✗ 设置区域位置失败: {e}") + return False + + def set_zone_radius(self, zone_id: str, radius: float) -> bool: + """ + 设置区域半径 + + Args: + zone_id: 区域ID + radius: 半径 + + Returns: + 是否设置成功 + """ + if zone_id not in self.zones: + print(f"✗ 区域不存在: {zone_id}") + return False + + try: + self.zones[zone_id]['radius'] = max(0.1, radius) # 最小半径0.1 + print(f"✓ 区域半径已设置: {zone_id} -> {radius}") + return True + + except Exception as e: + print(f"✗ 设置区域半径失败: {e}") + return False + + def enable_zone(self, zone_id: str) -> bool: + """ + 启用区域 + + Args: + zone_id: 区域ID + + Returns: + 是否启用成功 + """ + if zone_id not in self.zones: + print(f"✗ 区域不存在: {zone_id}") + return False + + try: + self.zones[zone_id]['active'] = True + self.stats['active_zones'] += 1 + print(f"✓ 区域已启用: {zone_id}") + return True + + except Exception as e: + print(f"✗ 启用区域失败: {e}") + return False + + def disable_zone(self, zone_id: str) -> bool: + """ + 禁用区域 + + Args: + zone_id: 区域ID + + Returns: + 是否禁用成功 + """ + if zone_id not in self.zones: + print(f"✗ 区域不存在: {zone_id}") + return False + + try: + self.zones[zone_id]['active'] = False + self.stats['active_zones'] -= 1 + print(f"✓ 区域已禁用: {zone_id}") + return True + + except Exception as e: + print(f"✗ 禁用区域失败: {e}") + return False + + def add_ambient_sound_to_zone(self, zone_id: str, filepath: str, + volume: float = 1.0, loop: bool = True) -> bool: + """ + 为区域添加环境音效 + + Args: + zone_id: 区域ID + filepath: 音频文件路径 + volume: 音量 + loop: 是否循环 + + Returns: + 是否添加成功 + """ + if zone_id not in self.zones: + print(f"✗ 区域不存在: {zone_id}") + return False + + try: + # 创建3D音效 + zone = self.zones[zone_id] + position = zone['position'] + sound_id = self.plugin.audio_source_manager.create_3d_sound( + filepath, position, loop, volume) + + if not sound_id: + print(f"✗ 无法创建环境音效: {filepath}") + return False + + # 播放音效 + self.plugin.audio_source_manager.play_sound(sound_id) + + # 关联音效到区域 + zone['ambient_sound'] = sound_id + + # 更新统计信息 + self.stats['ambient_sounds'] += 1 + + print(f"✓ 环境音效已添加到区域: {zone_id} -> {filepath}") + return True + + except Exception as e: + print(f"✗ 添加环境音效到区域失败: {e}") + return False + + def remove_ambient_sound_from_zone(self, zone_id: str) -> bool: + """ + 从区域移除环境音效 + + Args: + zone_id: 区域ID + + Returns: + 是否移除成功 + """ + if zone_id not in self.zones: + print(f"✗ 区域不存在: {zone_id}") + return False + + try: + zone = self.zones[zone_id] + if zone['ambient_sound']: + # 停止并删除音效 + sound_id = zone['ambient_sound'] + self.plugin.audio_source_manager.stop_sound(sound_id) + self.plugin.audio_source_manager.delete_sound(sound_id) + + # 清除关联 + zone['ambient_sound'] = None + + # 更新统计信息 + self.stats['ambient_sounds'] -= 1 + + print(f"✓ 环境音效已从区域移除: {zone_id}") + return True + else: + print(f"⚠ 区域没有环境音效: {zone_id}") + return True + + except Exception as e: + print(f"✗ 从区域移除环境音效失败: {e}") + return False + + def set_ambient_sound_volume(self, zone_id: str, volume: float) -> bool: + """ + 设置区域环境音效音量 + + Args: + zone_id: 区域ID + volume: 音量 (0.0-1.0) + + Returns: + 是否设置成功 + """ + if zone_id not in self.zones: + print(f"✗ 区域不存在: {zone_id}") + return False + + try: + zone = self.zones[zone_id] + if zone['ambient_sound']: + sound_id = zone['ambient_sound'] + return self.plugin.audio_source_manager.set_sound_volume(sound_id, volume) + else: + print(f"⚠ 区域没有环境音效: {zone_id}") + return False + + except Exception as e: + print(f"✗ 设置环境音效音量失败: {e}") + return False + + def _calculate_distance(self, pos1: Tuple[float, float, float], + pos2: Tuple[float, float, float]) -> float: + """ + 计算两点间距离 + + Args: + pos1: 第一个点 + pos2: 第二个点 + + Returns: + 距离 + """ + dx = pos1[0] - pos2[0] + dy = pos1[1] - pos2[1] + dz = pos1[2] - pos2[2] + return math.sqrt(dx*dx + dy*dy + dz*dz) + + def _is_point_in_zone(self, point: Tuple[float, float, float], + zone_id: str) -> bool: + """ + 检查点是否在区域内 + + Args: + point: 检查点 + zone_id: 区域ID + + Returns: + 是否在区域内 + """ + if zone_id not in self.zones: + return False + + zone = self.zones[zone_id] + zone_position = zone['position'] + zone_radius = zone['radius'] + + distance = self._calculate_distance(point, zone_position) + return distance <= zone_radius + + def update(self, dt: float): + """ + 更新环境音频状态 + + Args: + dt: 时间增量 + """ + if not self.enabled: + return + + # 更新听者位置 + if self.plugin.listener: + listener_pos = self.plugin.listener.getPos() + self.listener_position = (listener_pos.x, listener_pos.y, listener_pos.z) + + # 更新各区域的环境音效 + for zone_id, zone in self.zones.items(): + if not zone['active']: + continue + + # 检查听者是否在区域内 + is_in_zone = self._is_point_in_zone(self.listener_position, zone_id) + + # 更新环境音效状态 + if zone['ambient_sound']: + sound_id = zone['ambient_sound'] + if is_in_zone: + # 在区域内,确保音效播放 + if not self.plugin.audio_source_manager.is_sound_playing(sound_id): + self.plugin.audio_source_manager.play_sound(sound_id) + else: + # 不在区域内,停止音效 + if self.plugin.audio_source_manager.is_sound_playing(sound_id): + self.plugin.audio_source_manager.stop_sound(sound_id) + + def toggle_enabled(self): + """切换启用状态""" + self.enabled = not self.enabled + print(f"✓ 环境音频管理器已{'启用' if self.enabled else '禁用'}") + + def get_zone_info(self, zone_id: str) -> Dict[str, Any]: + """ + 获取区域信息 + + Args: + zone_id: 区域ID + + Returns: + 区域信息字典 + """ + if zone_id not in self.zones: + return {} + + zone = self.zones[zone_id] + return { + 'id': zone['id'], + 'position': zone['position'], + 'radius': zone['radius'], + 'active': zone['active'], + 'has_ambient_sound': zone['ambient_sound'] is not None + } + + def get_all_zones_info(self) -> List[Dict[str, Any]]: + """ + 获取所有区域信息 + + Returns: + 区域信息列表 + """ + zones_info = [] + for zone_id in self.zones: + zones_info.append(self.get_zone_info(zone_id)) + return zones_info + + def cleanup(self): + """清理所有资源""" + # 删除所有区域 + zone_ids = list(self.zones.keys()) + for zone_id in zone_ids: + self.delete_zone(zone_id) + + self.zones.clear() + self.ambient_sounds.clear() + self.stats = { + 'zones_created': 0, + 'active_zones': 0, + 'ambient_sounds': 0 + } + print("✓ 环境音频管理器资源已清理") + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + diff --git a/plugins/user/spatial_audio/editor/audio_editor.py b/plugins/user/spatial_audio/editor/audio_editor.py index a5757d02..8fc1d478 100644 --- a/plugins/user/spatial_audio/editor/audio_editor.py +++ b/plugins/user/spatial_audio/editor/audio_editor.py @@ -1,552 +1,552 @@ -""" -音频编辑器 -提供可视化编辑音频场景和参数的功能 -""" - -from typing import Dict, List, Any, Optional -from panda3d.core import NodePath, Vec3 - -class AudioEditor: - """ - 音频编辑器 - 提供可视化编辑音频场景和参数的功能 - """ - - def __init__(self, plugin, world): - """ - 初始化音频编辑器 - - Args: - plugin: 3D音频插件实例 - world: 3D世界实例 - """ - self.plugin = plugin - self.world = world - self.visible = False - self.editor_node = None - self.zone_visualizations = {} - self.sound_visualizations = {} - self.selected_item = None - - # 编辑器状态 - self.edit_mode = False - self.selected_tool = "select" # select, add_sound, add_zone - self.grid_size = 5.0 - self.snap_to_grid = True - - # GUI元素 - self.gui_elements = [] - self.tool_buttons = {} - - def toggle_visibility(self): - """切换编辑器可见性""" - if self.visible: - self.hide() - else: - self.show() - - def show(self): - """显示编辑器""" - if self.visible: - return - - self.visible = True - self._create_editor_visualization() - self._create_editor_gui() - print("✓ 音频编辑器已显示") - - def hide(self): - """隐藏编辑器""" - if not self.visible: - return - - self.visible = False - self._cleanup_editor_visualization() - self._cleanup_editor_gui() - print("✓ 音频编辑器已隐藏") - - def _create_editor_visualization(self): - """创建编辑器可视化元素""" - try: - if not self.world.render: - return - - # 创建编辑器根节点 - self.editor_node = self.world.render.attachNewNode("audio_editor") - - # 可视化现有的音频区域 - self._visualize_zones() - - # 可视化现有的音频源 - self._visualize_sounds() - - except Exception as e: - print(f"⚠ 编辑器可视化创建失败: {e}") - - def _cleanup_editor_visualization(self): - """清理编辑器可视化元素""" - try: - # 清理区域可视化 - for zone_node in self.zone_visualizations.values(): - zone_node.removeNode() - self.zone_visualizations.clear() - - # 清理音源可视化 - for sound_node in self.sound_visualizations.values(): - sound_node.removeNode() - self.sound_visualizations.clear() - - # 清理编辑器根节点 - if self.editor_node: - self.editor_node.removeNode() - self.editor_node = None - - except Exception as e: - print(f"⚠ 编辑器可视化清理失败: {e}") - - def _visualize_zones(self): - """可视化音频区域""" - if not self.editor_node: - return - - # 获取所有区域信息 - zones_info = self.plugin.environment_audio_manager.get_all_zones_info() - - for zone_info in zones_info: - zone_id = zone_info['id'] - position = zone_info['position'] - radius = zone_info['radius'] - - # 创建区域可视化节点 - zone_node = self.editor_node.attachNewNode(f"zone_{zone_id}") - zone_node.setPos(position[0], position[1], position[2]) - - # 这里应该创建一个球体或其他可视化表示 - # 为简化起见,我们只是存储节点 - self.zone_visualizations[zone_id] = zone_node - - def _visualize_sounds(self): - """可视化音频源""" - if not self.editor_node: - return - - # 获取所有音频源 - for sound_id, sound_info in self.plugin.audio_source_manager.audio_sources.items(): - position = sound_info['position'] - - # 创建音源可视化节点 - sound_node = self.editor_node.attachNewNode(f"sound_{sound_id}") - sound_node.setPos(position[0], position[1], position[2]) - - # 这里应该创建一个图标或其他可视化表示 - # 为简化起见,我们只是存储节点 - self.sound_visualizations[sound_id] = sound_node - - def _create_editor_gui(self): - """创建编辑器GUI""" - try: - if not hasattr(self.world, 'gui_manager') or not self.world.gui_manager: - print("⚠ GUI管理器不可用,无法创建编辑器界面") - return - - gui_manager = self.world.gui_manager - - # 创建编辑器面板标题 - panel = gui_manager.createGUIButton( - pos=(0.7, 0, 0.95), - text="音频编辑器", - size=0.06 - ) - self.gui_elements.append(panel) - - # 创建工具按钮 - tools = [ - ("选择", "select"), - ("添加音效", "add_sound"), - ("添加区域", "add_zone") - ] - - for i, (name, tool) in enumerate(tools): - button = gui_manager.createGUIButton( - pos=(0.7, 0, 0.88 - i * 0.06), - text=name, - size=0.04 - ) - self.gui_elements.append(button) - self.tool_buttons[tool] = button - self.world.accept(f"audio_tool_{tool}", - lambda t=tool: self.set_edit_tool(t)) - - # 创建编辑功能按钮 - functions = [ - ("网格对齐", self.toggle_grid_snap), - ("播放测试音", self.play_test_sound), - ("环境音效", self.toggle_environment_audio), - ("保存场景", self.save_audio_scene), - ("加载场景", self.load_audio_scene) - ] - - for i, (name, callback) in enumerate(functions): - button = gui_manager.createGUIButton( - pos=(0.82, 0, 0.88 - i * 0.06), - text=name, - size=0.04 - ) - self.gui_elements.append(button) - self.world.accept(f"audio_func_{name}", callback) - - except Exception as e: - print(f"⚠ 编辑器GUI创建失败: {e}") - - def _cleanup_editor_gui(self): - """清理编辑器GUI""" - try: - if hasattr(self.world, 'gui_manager') and self.world.gui_manager: - gui_manager = self.world.gui_manager - - for element in self.gui_elements: - gui_manager.deleteGUIElement(element) - - # 清理事件监听 - tools = ["select", "add_sound", "add_zone"] - for tool in tools: - self.world.ignore(f"audio_tool_{tool}") - - functions = [ - "网格对齐", "播放测试音", "环境音效", - "保存场景", "加载场景" - ] - for func in functions: - self.world.ignore(f"audio_func_{func}") - - self.gui_elements.clear() - self.tool_buttons.clear() - - except Exception as e: - print(f"⚠ 编辑器GUI清理失败: {e}") - - def set_edit_tool(self, tool: str): - """设置编辑工具""" - self.selected_tool = tool - print(f"✓ 切换到 {tool} 工具") - - # 更新按钮状态 - for tool_name, button in self.tool_buttons.items(): - if tool_name == tool: - # 高亮选中工具 - button.setColor(0.5, 1.0, 0.5, 1.0) # 绿色 - else: - # 恢复默认颜色 - button.setColor(1.0, 1.0, 1.0, 1.0) # 白色 - - def toggle_grid_snap(self): - """切换网格对齐""" - self.snap_to_grid = not self.snap_to_grid - print(f"✓ 网格对齐已{'启用' if self.snap_to_grid else '禁用'}") - - def play_test_sound(self): - """播放测试音效""" - print("✓ 播放测试音效") - - def toggle_environment_audio(self): - """切换环境音频""" - self.plugin.environment_audio_manager.toggle_enabled() - print("✓ 环境音频切换") - - def save_audio_scene(self): - """保存音频场景""" - print("✓ 保存音频场景") - - def load_audio_scene(self): - """加载音频场景""" - print("✓ 加载音频场景") - - def handle_mouse_click(self, mouse_x: float, mouse_y: float): - """处理鼠标点击事件""" - if not self.edit_mode: - return - - # 将屏幕坐标转换为世界坐标 - world_pos = self._screen_to_world(mouse_x, mouse_y) - if not world_pos: - return - - # 根据当前工具执行操作 - if self.selected_tool == "add_sound": - self._add_sound_at_position(world_pos) - elif self.selected_tool == "add_zone": - self._add_zone_at_position(world_pos) - elif self.selected_tool == "select": - self._select_item_at_position(world_pos) - - def _screen_to_world(self, screen_x: float, screen_y: float) -> Optional[Vec3]: - """ - 将屏幕坐标转换为世界坐标 - - Args: - screen_x: 屏幕X坐标 (-1 到 1) - screen_y: 屏幕Y坐标 (-1 到 1) - - Returns: - 世界坐标,如果转换失败则返回None - """ - # 这是一个简化的实现 - # 实际应用中需要使用摄像机的射线检测 - - # 简单地将屏幕坐标映射到XZ平面 - x = screen_x * 20 # 假设场景大小为40x40 - z = screen_y * 20 - y = 0 # 在地面高度 - - # 网格对齐 - if self.snap_to_grid: - x = round(x / self.grid_size) * self.grid_size - z = round(z / self.grid_size) * self.grid_size - - return Vec3(x, y, z) - - def _add_sound_at_position(self, position: Vec3): - """ - 在指定位置添加音效 - - Args: - position: 位置 - """ - print(f"✓ 在位置 {position} 添加音效") - # 这里应该打开一个对话框让用户选择音频文件等参数 - - def _add_zone_at_position(self, position: Vec3): - """ - 在指定位置添加区域 - - Args: - position: 位置 - """ - print(f"✓ 在位置 {position} 添加音频区域") - # 这里应该打开一个对话框让用户设置区域参数 - - def _select_item_at_position(self, position: Vec3): - """ - 选择指定位置的项目 - - Args: - position: 位置 - """ - print(f"✓ 选择位置 {position} 的项目") - # 这里应该实现项目选择逻辑 - - def toggle_edit_mode(self): - """切换编辑模式""" - self.edit_mode = not self.edit_mode - if self.edit_mode: - print("✓ 进入音频编辑模式") - else: - print("✓ 退出音频编辑模式") - - def update(self, dt: float): - """更新编辑器状态""" - # 处理键盘输入 - if self.world.mouseWatcherNode.hasMouse(): - mouse_pos = self.world.mouseWatcherNode.getMouse() - - # 检查鼠标按键 - from panda3d.core import MouseWatcher - if self.world.mouseWatcherNode.isButtonDown(MouseWatcher.getGlobalPointer()): - self.handle_mouse_click(mouse_pos.x, mouse_pos.y) - - # 检查键盘快捷键 - from panda3d.core import KeyboardButton - if self.world.mouseWatcherNode.isButtonDown(KeyboardButton.asciiKey('e')): - self.toggle_edit_mode() - - def cleanup(self): - """清理编辑器资源""" - self.hide() - self.plugin = None - self.world = None - self.selected_item = None - - -class AudioSceneSerializer: - """ - 音频场景序列化器 - 处理音频场景的保存和加载 - """ - - def __init__(self, plugin): - """ - 初始化序列化器 - - Args: - plugin: 3D音频插件实例 - """ - self.plugin = plugin - - def save_scene(self, filepath: str) -> bool: - """ - 保存音频场景 - - Args: - filepath: 文件路径 - - Returns: - 是否保存成功 - """ - try: - import json - - # 收集场景数据 - scene_data = { - 'version': '1.0', - 'zones': self._collect_zones_data(), - 'sounds': self._collect_sounds_data(), - 'effects': self._collect_effects_data() - } - - # 保存到文件 - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(scene_data, f, indent=2, ensure_ascii=False) - - print(f"✓ 音频场景已保存到: {filepath}") - return True - - except Exception as e: - print(f"✗ 保存音频场景失败: {e}") - return False - - def load_scene(self, filepath: str) -> bool: - """ - 加载音频场景 - - Args: - filepath: 文件路径 - - Returns: - 是否加载成功 - """ - try: - import json - - # 从文件加载 - with open(filepath, 'r', encoding='utf-8') as f: - scene_data = json.load(f) - - # 应用场景数据 - self._apply_zones_data(scene_data.get('zones', [])) - self._apply_sounds_data(scene_data.get('sounds', [])) - self._apply_effects_data(scene_data.get('effects', [])) - - print(f"✓ 音频场景已从 {filepath} 加载") - return True - - except Exception as e: - print(f"✗ 加载音频场景失败: {e}") - return False - - def _collect_zones_data(self) -> List[Dict[str, Any]]: - """收集区域数据""" - zones_data = [] - zones_info = self.plugin.environment_audio_manager.get_all_zones_info() - - for zone_info in zones_info: - zone_id = zone_info['id'] - # 获取区域的完整信息 - zone = self.plugin.environment_audio_manager.zones[zone_id] - - zone_data = { - 'id': zone_id, - 'position': zone['position'], - 'radius': zone['radius'], - 'properties': zone['properties'], - 'active': zone['active'] - } - zones_data.append(zone_data) - - return zones_data - - def _collect_sounds_data(self) -> List[Dict[str, Any]]: - """收集音效数据""" - sounds_data = [] - - for sound_id, sound_info in self.plugin.audio_source_manager.audio_sources.items(): - sound_data = { - 'id': sound_id, - 'filepath': sound_info['filepath'], - 'type': sound_info['type'], - 'position': sound_info['position'], - 'loop': sound_info['loop'], - 'volume': sound_info['volume'] - } - sounds_data.append(sound_data) - - return sounds_data - - def _collect_effects_data(self) -> List[Dict[str, Any]]: - """收集效果数据""" - effects_data = [] - - for effect_id, effect_info in self.plugin.audio_effect_processor.effects.items(): - effect_data = { - 'id': effect_id, - 'type': effect_info['type'], - 'parameters': effect_info['parameters'], - 'active': effect_info['active'] - } - effects_data.append(effect_data) - - return effects_data - - def _apply_zones_data(self, zones_data: List[Dict[str, Any]]): - """应用区域数据""" - for zone_data in zones_data: - zone_id = zone_data['id'] - position = zone_data['position'] - radius = zone_data['radius'] - properties = zone_data['properties'] - - # 创建区域 - self.plugin.environment_audio_manager.create_zone( - zone_id, position, radius, properties) - - # 设置活动状态 - if not zone_data.get('active', True): - self.plugin.environment_audio_manager.disable_zone(zone_id) - - def _apply_sounds_data(self, sounds_data: List[Dict[str, Any]]): - """应用音效数据""" - for sound_data in sounds_data: - sound_id = sound_data['id'] - filepath = sound_data['filepath'] - sound_type = sound_data['type'] - position = sound_data['position'] - loop = sound_data['loop'] - volume = sound_data['volume'] - - # 创建音效 - if sound_type == '3d': - new_sound_id = self.plugin.audio_source_manager.create_3d_sound( - filepath, position, loop, volume) - else: - new_sound_id = self.plugin.audio_source_manager.create_2d_sound( - filepath, loop, volume) - - # 注意:由于ID可能已更改,需要处理关联关系 - if new_sound_id: - # 播放音效 - self.plugin.audio_source_manager.play_sound(new_sound_id) - - def _apply_effects_data(self, effects_data: List[Dict[str, Any]]): - """应用效果数据""" - for effect_data in effects_data: - effect_id = effect_data['id'] - effect_type = effect_data['type'] - parameters = effect_data['parameters'] - - # 创建效果 - new_effect_id = self.plugin.audio_effect_processor.create_effect( - effect_type, parameters) - - # 设置活动状态 - if not effect_data.get('active', True): - if new_effect_id: +""" +音频编辑器 +提供可视化编辑音频场景和参数的功能 +""" + +from typing import Dict, List, Any, Optional +from panda3d.core import NodePath, Vec3 + +class AudioEditor: + """ + 音频编辑器 + 提供可视化编辑音频场景和参数的功能 + """ + + def __init__(self, plugin, world): + """ + 初始化音频编辑器 + + Args: + plugin: 3D音频插件实例 + world: 3D世界实例 + """ + self.plugin = plugin + self.world = world + self.visible = False + self.editor_node = None + self.zone_visualizations = {} + self.sound_visualizations = {} + self.selected_item = None + + # 编辑器状态 + self.edit_mode = False + self.selected_tool = "select" # select, add_sound, add_zone + self.grid_size = 5.0 + self.snap_to_grid = True + + # GUI元素 + self.gui_elements = [] + self.tool_buttons = {} + + def toggle_visibility(self): + """切换编辑器可见性""" + if self.visible: + self.hide() + else: + self.show() + + def show(self): + """显示编辑器""" + if self.visible: + return + + self.visible = True + self._create_editor_visualization() + self._create_editor_gui() + print("✓ 音频编辑器已显示") + + def hide(self): + """隐藏编辑器""" + if not self.visible: + return + + self.visible = False + self._cleanup_editor_visualization() + self._cleanup_editor_gui() + print("✓ 音频编辑器已隐藏") + + def _create_editor_visualization(self): + """创建编辑器可视化元素""" + try: + if not self.world.render: + return + + # 创建编辑器根节点 + self.editor_node = self.world.render.attachNewNode("audio_editor") + + # 可视化现有的音频区域 + self._visualize_zones() + + # 可视化现有的音频源 + self._visualize_sounds() + + except Exception as e: + print(f"⚠ 编辑器可视化创建失败: {e}") + + def _cleanup_editor_visualization(self): + """清理编辑器可视化元素""" + try: + # 清理区域可视化 + for zone_node in self.zone_visualizations.values(): + zone_node.removeNode() + self.zone_visualizations.clear() + + # 清理音源可视化 + for sound_node in self.sound_visualizations.values(): + sound_node.removeNode() + self.sound_visualizations.clear() + + # 清理编辑器根节点 + if self.editor_node: + self.editor_node.removeNode() + self.editor_node = None + + except Exception as e: + print(f"⚠ 编辑器可视化清理失败: {e}") + + def _visualize_zones(self): + """可视化音频区域""" + if not self.editor_node: + return + + # 获取所有区域信息 + zones_info = self.plugin.environment_audio_manager.get_all_zones_info() + + for zone_info in zones_info: + zone_id = zone_info['id'] + position = zone_info['position'] + radius = zone_info['radius'] + + # 创建区域可视化节点 + zone_node = self.editor_node.attachNewNode(f"zone_{zone_id}") + zone_node.setPos(position[0], position[1], position[2]) + + # 这里应该创建一个球体或其他可视化表示 + # 为简化起见,我们只是存储节点 + self.zone_visualizations[zone_id] = zone_node + + def _visualize_sounds(self): + """可视化音频源""" + if not self.editor_node: + return + + # 获取所有音频源 + for sound_id, sound_info in self.plugin.audio_source_manager.audio_sources.items(): + position = sound_info['position'] + + # 创建音源可视化节点 + sound_node = self.editor_node.attachNewNode(f"sound_{sound_id}") + sound_node.setPos(position[0], position[1], position[2]) + + # 这里应该创建一个图标或其他可视化表示 + # 为简化起见,我们只是存储节点 + self.sound_visualizations[sound_id] = sound_node + + def _create_editor_gui(self): + """创建编辑器GUI""" + try: + if not hasattr(self.world, 'gui_manager') or not self.world.gui_manager: + print("⚠ GUI管理器不可用,无法创建编辑器界面") + return + + gui_manager = self.world.gui_manager + + # 创建编辑器面板标题 + panel = gui_manager.createGUIButton( + pos=(0.7, 0, 0.95), + text="音频编辑器", + size=0.06 + ) + self.gui_elements.append(panel) + + # 创建工具按钮 + tools = [ + ("选择", "select"), + ("添加音效", "add_sound"), + ("添加区域", "add_zone") + ] + + for i, (name, tool) in enumerate(tools): + button = gui_manager.createGUIButton( + pos=(0.7, 0, 0.88 - i * 0.06), + text=name, + size=0.04 + ) + self.gui_elements.append(button) + self.tool_buttons[tool] = button + self.world.accept(f"audio_tool_{tool}", + lambda t=tool: self.set_edit_tool(t)) + + # 创建编辑功能按钮 + functions = [ + ("网格对齐", self.toggle_grid_snap), + ("播放测试音", self.play_test_sound), + ("环境音效", self.toggle_environment_audio), + ("保存场景", self.save_audio_scene), + ("加载场景", self.load_audio_scene) + ] + + for i, (name, callback) in enumerate(functions): + button = gui_manager.createGUIButton( + pos=(0.82, 0, 0.88 - i * 0.06), + text=name, + size=0.04 + ) + self.gui_elements.append(button) + self.world.accept(f"audio_func_{name}", callback) + + except Exception as e: + print(f"⚠ 编辑器GUI创建失败: {e}") + + def _cleanup_editor_gui(self): + """清理编辑器GUI""" + try: + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + gui_manager = self.world.gui_manager + + for element in self.gui_elements: + gui_manager.deleteGUIElement(element) + + # 清理事件监听 + tools = ["select", "add_sound", "add_zone"] + for tool in tools: + self.world.ignore(f"audio_tool_{tool}") + + functions = [ + "网格对齐", "播放测试音", "环境音效", + "保存场景", "加载场景" + ] + for func in functions: + self.world.ignore(f"audio_func_{func}") + + self.gui_elements.clear() + self.tool_buttons.clear() + + except Exception as e: + print(f"⚠ 编辑器GUI清理失败: {e}") + + def set_edit_tool(self, tool: str): + """设置编辑工具""" + self.selected_tool = tool + print(f"✓ 切换到 {tool} 工具") + + # 更新按钮状态 + for tool_name, button in self.tool_buttons.items(): + if tool_name == tool: + # 高亮选中工具 + button.setColor(0.5, 1.0, 0.5, 1.0) # 绿色 + else: + # 恢复默认颜色 + button.setColor(1.0, 1.0, 1.0, 1.0) # 白色 + + def toggle_grid_snap(self): + """切换网格对齐""" + self.snap_to_grid = not self.snap_to_grid + print(f"✓ 网格对齐已{'启用' if self.snap_to_grid else '禁用'}") + + def play_test_sound(self): + """播放测试音效""" + print("✓ 播放测试音效") + + def toggle_environment_audio(self): + """切换环境音频""" + self.plugin.environment_audio_manager.toggle_enabled() + print("✓ 环境音频切换") + + def save_audio_scene(self): + """保存音频场景""" + print("✓ 保存音频场景") + + def load_audio_scene(self): + """加载音频场景""" + print("✓ 加载音频场景") + + def handle_mouse_click(self, mouse_x: float, mouse_y: float): + """处理鼠标点击事件""" + if not self.edit_mode: + return + + # 将屏幕坐标转换为世界坐标 + world_pos = self._screen_to_world(mouse_x, mouse_y) + if not world_pos: + return + + # 根据当前工具执行操作 + if self.selected_tool == "add_sound": + self._add_sound_at_position(world_pos) + elif self.selected_tool == "add_zone": + self._add_zone_at_position(world_pos) + elif self.selected_tool == "select": + self._select_item_at_position(world_pos) + + def _screen_to_world(self, screen_x: float, screen_y: float) -> Optional[Vec3]: + """ + 将屏幕坐标转换为世界坐标 + + Args: + screen_x: 屏幕X坐标 (-1 到 1) + screen_y: 屏幕Y坐标 (-1 到 1) + + Returns: + 世界坐标,如果转换失败则返回None + """ + # 这是一个简化的实现 + # 实际应用中需要使用摄像机的射线检测 + + # 简单地将屏幕坐标映射到XZ平面 + x = screen_x * 20 # 假设场景大小为40x40 + z = screen_y * 20 + y = 0 # 在地面高度 + + # 网格对齐 + if self.snap_to_grid: + x = round(x / self.grid_size) * self.grid_size + z = round(z / self.grid_size) * self.grid_size + + return Vec3(x, y, z) + + def _add_sound_at_position(self, position: Vec3): + """ + 在指定位置添加音效 + + Args: + position: 位置 + """ + print(f"✓ 在位置 {position} 添加音效") + # 这里应该打开一个对话框让用户选择音频文件等参数 + + def _add_zone_at_position(self, position: Vec3): + """ + 在指定位置添加区域 + + Args: + position: 位置 + """ + print(f"✓ 在位置 {position} 添加音频区域") + # 这里应该打开一个对话框让用户设置区域参数 + + def _select_item_at_position(self, position: Vec3): + """ + 选择指定位置的项目 + + Args: + position: 位置 + """ + print(f"✓ 选择位置 {position} 的项目") + # 这里应该实现项目选择逻辑 + + def toggle_edit_mode(self): + """切换编辑模式""" + self.edit_mode = not self.edit_mode + if self.edit_mode: + print("✓ 进入音频编辑模式") + else: + print("✓ 退出音频编辑模式") + + def update(self, dt: float): + """更新编辑器状态""" + # 处理键盘输入 + if self.world.mouseWatcherNode.hasMouse(): + mouse_pos = self.world.mouseWatcherNode.getMouse() + + # 检查鼠标按键 + from panda3d.core import MouseWatcher + if self.world.mouseWatcherNode.isButtonDown(MouseWatcher.getGlobalPointer()): + self.handle_mouse_click(mouse_pos.x, mouse_pos.y) + + # 检查键盘快捷键 + from panda3d.core import KeyboardButton + if self.world.mouseWatcherNode.isButtonDown(KeyboardButton.asciiKey('e')): + self.toggle_edit_mode() + + def cleanup(self): + """清理编辑器资源""" + self.hide() + self.plugin = None + self.world = None + self.selected_item = None + + +class AudioSceneSerializer: + """ + 音频场景序列化器 + 处理音频场景的保存和加载 + """ + + def __init__(self, plugin): + """ + 初始化序列化器 + + Args: + plugin: 3D音频插件实例 + """ + self.plugin = plugin + + def save_scene(self, filepath: str) -> bool: + """ + 保存音频场景 + + Args: + filepath: 文件路径 + + Returns: + 是否保存成功 + """ + try: + import json + + # 收集场景数据 + scene_data = { + 'version': '1.0', + 'zones': self._collect_zones_data(), + 'sounds': self._collect_sounds_data(), + 'effects': self._collect_effects_data() + } + + # 保存到文件 + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(scene_data, f, indent=2, ensure_ascii=False) + + print(f"✓ 音频场景已保存到: {filepath}") + return True + + except Exception as e: + print(f"✗ 保存音频场景失败: {e}") + return False + + def load_scene(self, filepath: str) -> bool: + """ + 加载音频场景 + + Args: + filepath: 文件路径 + + Returns: + 是否加载成功 + """ + try: + import json + + # 从文件加载 + with open(filepath, 'r', encoding='utf-8') as f: + scene_data = json.load(f) + + # 应用场景数据 + self._apply_zones_data(scene_data.get('zones', [])) + self._apply_sounds_data(scene_data.get('sounds', [])) + self._apply_effects_data(scene_data.get('effects', [])) + + print(f"✓ 音频场景已从 {filepath} 加载") + return True + + except Exception as e: + print(f"✗ 加载音频场景失败: {e}") + return False + + def _collect_zones_data(self) -> List[Dict[str, Any]]: + """收集区域数据""" + zones_data = [] + zones_info = self.plugin.environment_audio_manager.get_all_zones_info() + + for zone_info in zones_info: + zone_id = zone_info['id'] + # 获取区域的完整信息 + zone = self.plugin.environment_audio_manager.zones[zone_id] + + zone_data = { + 'id': zone_id, + 'position': zone['position'], + 'radius': zone['radius'], + 'properties': zone['properties'], + 'active': zone['active'] + } + zones_data.append(zone_data) + + return zones_data + + def _collect_sounds_data(self) -> List[Dict[str, Any]]: + """收集音效数据""" + sounds_data = [] + + for sound_id, sound_info in self.plugin.audio_source_manager.audio_sources.items(): + sound_data = { + 'id': sound_id, + 'filepath': sound_info['filepath'], + 'type': sound_info['type'], + 'position': sound_info['position'], + 'loop': sound_info['loop'], + 'volume': sound_info['volume'] + } + sounds_data.append(sound_data) + + return sounds_data + + def _collect_effects_data(self) -> List[Dict[str, Any]]: + """收集效果数据""" + effects_data = [] + + for effect_id, effect_info in self.plugin.audio_effect_processor.effects.items(): + effect_data = { + 'id': effect_id, + 'type': effect_info['type'], + 'parameters': effect_info['parameters'], + 'active': effect_info['active'] + } + effects_data.append(effect_data) + + return effects_data + + def _apply_zones_data(self, zones_data: List[Dict[str, Any]]): + """应用区域数据""" + for zone_data in zones_data: + zone_id = zone_data['id'] + position = zone_data['position'] + radius = zone_data['radius'] + properties = zone_data['properties'] + + # 创建区域 + self.plugin.environment_audio_manager.create_zone( + zone_id, position, radius, properties) + + # 设置活动状态 + if not zone_data.get('active', True): + self.plugin.environment_audio_manager.disable_zone(zone_id) + + def _apply_sounds_data(self, sounds_data: List[Dict[str, Any]]): + """应用音效数据""" + for sound_data in sounds_data: + sound_id = sound_data['id'] + filepath = sound_data['filepath'] + sound_type = sound_data['type'] + position = sound_data['position'] + loop = sound_data['loop'] + volume = sound_data['volume'] + + # 创建音效 + if sound_type == '3d': + new_sound_id = self.plugin.audio_source_manager.create_3d_sound( + filepath, position, loop, volume) + else: + new_sound_id = self.plugin.audio_source_manager.create_2d_sound( + filepath, loop, volume) + + # 注意:由于ID可能已更改,需要处理关联关系 + if new_sound_id: + # 播放音效 + self.plugin.audio_source_manager.play_sound(new_sound_id) + + def _apply_effects_data(self, effects_data: List[Dict[str, Any]]): + """应用效果数据""" + for effect_data in effects_data: + effect_id = effect_data['id'] + effect_type = effect_data['type'] + parameters = effect_data['parameters'] + + # 创建效果 + new_effect_id = self.plugin.audio_effect_processor.create_effect( + effect_type, parameters) + + # 设置活动状态 + if not effect_data.get('active', True): + if new_effect_id: self.plugin.audio_effect_processor.disable_effect(new_effect_id) \ No newline at end of file diff --git a/plugins/user/spatial_audio/effects/audio_effect_processor.py b/plugins/user/spatial_audio/effects/audio_effect_processor.py index 839ad7f8..06f75e84 100644 --- a/plugins/user/spatial_audio/effects/audio_effect_processor.py +++ b/plugins/user/spatial_audio/effects/audio_effect_processor.py @@ -1,460 +1,460 @@ -""" -音频效果处理器 -处理各种音频效果,如混响、回声、滤波等 -""" - -import uuid -from typing import Dict, List, Any, Optional -import math - -class AudioEffectProcessor: - """ - 音频效果处理器 - 处理各种音频效果,如混响、回声、滤波等 - """ - - def __init__(self, plugin): - """ - 初始化音频效果处理器 - - Args: - plugin: 3D音频插件实例 - """ - self.plugin = plugin - self.effects: Dict[str, Dict[str, Any]] = {} - self.sound_effects: Dict[str, List[str]] = {} # 音效与效果的关联 - self.stats = { - 'total_effects': 0, - 'active_effects': 0, - 'effects_applied': 0 - } - - def create_effect(self, effect_type: str, parameters: Dict[str, Any]) -> str: - """ - 创建音频效果 - - Args: - effect_type: 效果类型 ('reverb', 'echo', 'lowpass', 'highpass', 'distortion') - parameters: 效果参数 - - Returns: - 效果ID - """ - try: - # 生成唯一ID - effect_id = str(uuid.uuid4()) - - # 创建效果对象 - effect = { - 'id': effect_id, - 'type': effect_type, - 'parameters': parameters.copy(), - 'active': True - } - - # 存储效果 - self.effects[effect_id] = effect - - # 更新统计信息 - self.stats['total_effects'] += 1 - self.stats['active_effects'] += 1 - - print(f"✓ 音频效果创建成功: {effect_id} ({effect_type})") - return effect_id - - except Exception as e: - print(f"✗ 创建音频效果失败: {e}") - return "" - - def delete_effect(self, effect_id: str) -> bool: - """ - 删除音频效果 - - Args: - effect_id: 效果ID - - Returns: - 是否删除成功 - """ - if effect_id not in self.effects: - print(f"✗ 效果不存在: {effect_id}") - return False - - try: - # 从所有音效中移除该效果 - for sound_id, effect_list in self.sound_effects.items(): - if effect_id in effect_list: - effect_list.remove(effect_id) - - # 删除效果 - del self.effects[effect_id] - - # 更新统计信息 - self.stats['active_effects'] -= 1 - - print(f"✓ 音频效果已删除: {effect_id}") - return True - - except Exception as e: - print(f"✗ 删除音频效果失败: {e}") - return False - - def apply_effect_to_sound(self, sound_id: str, effect_id: str) -> bool: - """ - 应用效果到音效 - - Args: - sound_id: 音源ID - effect_id: 效果ID - - Returns: - 是否应用成功 - """ - # 检查音源和效果是否存在 - if sound_id not in self.plugin.audio_source_manager.audio_sources: - print(f"✗ 音源不存在: {sound_id}") - return False - - if effect_id not in self.effects: - print(f"✗ 效果不存在: {effect_id}") - return False - - try: - # 初始化音效的效果列表 - if sound_id not in self.sound_effects: - self.sound_effects[sound_id] = [] - - # 添加效果到音效 - if effect_id not in self.sound_effects[sound_id]: - self.sound_effects[sound_id].append(effect_id) - - # 更新统计信息 - self.stats['effects_applied'] += 1 - - effect_type = self.effects[effect_id]['type'] - print(f"✓ 效果已应用到音效: {effect_id} ({effect_type}) -> {sound_id}") - return True - else: - print(f"⚠ 效果已应用于该音效: {effect_id} -> {sound_id}") - return True - - except Exception as e: - print(f"✗ 应用效果到音效失败: {e}") - return False - - def remove_effect_from_sound(self, sound_id: str, effect_id: str) -> bool: - """ - 从音效移除效果 - - Args: - sound_id: 音源ID - effect_id: 效果ID - - Returns: - 是否移除成功 - """ - # 检查音源和效果是否存在 - if sound_id not in self.plugin.audio_source_manager.audio_sources: - print(f"✗ 音源不存在: {sound_id}") - return False - - if effect_id not in self.effects: - print(f"✗ 效果不存在: {effect_id}") - return False - - try: - # 检查效果是否已应用于该音效 - if sound_id in self.sound_effects and effect_id in self.sound_effects[sound_id]: - self.sound_effects[sound_id].remove(effect_id) - - # 更新统计信息 - self.stats['effects_applied'] -= 1 - - effect_type = self.effects[effect_id]['type'] - print(f"✓ 效果已从音效移除: {effect_id} ({effect_type}) -> {sound_id}") - return True - else: - print(f"⚠ 效果未应用于该音效: {effect_id} -> {sound_id}") - return True - - except Exception as e: - print(f"✗ 从音效移除效果失败: {e}") - return False - - def set_effect_parameter(self, effect_id: str, parameter: str, value: Any) -> bool: - """ - 设置效果参数 - - Args: - effect_id: 效果ID - parameter: 参数名 - value: 参数值 - - Returns: - 是否设置成功 - """ - if effect_id not in self.effects: - print(f"✗ 效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['parameters'][parameter] = value - print(f"✓ 效果参数已设置: {effect_id}.{parameter} = {value}") - return True - - except Exception as e: - print(f"✗ 设置效果参数失败: {e}") - return False - - def get_effect_parameters(self, effect_id: str) -> Dict[str, Any]: - """ - 获取效果参数 - - Args: - effect_id: 效果ID - - Returns: - 参数字典 - """ - if effect_id not in self.effects: - return {} - - return self.effects[effect_id]['parameters'].copy() - - def enable_effect(self, effect_id: str) -> bool: - """ - 启用效果 - - Args: - effect_id: 效果ID - - Returns: - 是否启用成功 - """ - if effect_id not in self.effects: - print(f"✗ 效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['active'] = True - self.stats['active_effects'] += 1 - print(f"✓ 效果已启用: {effect_id}") - return True - - except Exception as e: - print(f"✗ 启用效果失败: {e}") - return False - - def disable_effect(self, effect_id: str) -> bool: - """ - 禁用效果 - - Args: - effect_id: 效果ID - - Returns: - 是否禁用成功 - """ - if effect_id not in self.effects: - print(f"✗ 效果不存在: {effect_id}") - return False - - try: - self.effects[effect_id]['active'] = False - self.stats['active_effects'] -= 1 - print(f"✓ 效果已禁用: {effect_id}") - return True - - except Exception as e: - print(f"✗ 禁用效果失败: {e}") - return False - - def get_effect_type(self, effect_id: str) -> str: - """ - 获取效果类型 - - Args: - effect_id: 效果ID - - Returns: - 效果类型 - """ - if effect_id not in self.effects: - return "" - - return self.effects[effect_id]['type'] - - def get_sound_effects(self, sound_id: str) -> List[str]: - """ - 获取音效应用的效果列表 - - Args: - sound_id: 音源ID - - Returns: - 效果ID列表 - """ - return self.sound_effects.get(sound_id, []).copy() - - def process_audio_with_effects(self, sound_id: str, audio_data: Any) -> Any: - """ - 使用效果处理音频数据 - - Args: - sound_id: 音源ID - audio_data: 音频数据 - - Returns: - 处理后的音频数据 - """ - # 这是一个简化的实现 - # 实际应用中需要根据效果类型对音频数据进行处理 - - if sound_id not in self.sound_effects: - return audio_data - - # 获取应用到该音效的所有效果 - effect_ids = self.sound_effects[sound_id] - - # 按顺序应用效果 - processed_data = audio_data - for effect_id in effect_ids: - if effect_id in self.effects and self.effects[effect_id]['active']: - effect = self.effects[effect_id] - processed_data = self._apply_single_effect(processed_data, effect) - - return processed_data - - def _apply_single_effect(self, audio_data: Any, effect: Dict[str, Any]) -> Any: - """ - 应用单个效果到音频数据 - - Args: - audio_data: 音频数据 - effect: 效果配置 - - Returns: - 处理后的音频数据 - """ - # 这是一个简化的实现 - # 实际应用中需要根据效果类型对音频数据进行处理 - effect_type = effect['type'] - parameters = effect['parameters'] - - # 根据效果类型进行处理 - if effect_type == 'reverb': - # 混响效果处理 - processed_data = self._apply_reverb(audio_data, parameters) - elif effect_type == 'echo': - # 回声效果处理 - processed_data = self._apply_echo(audio_data, parameters) - elif effect_type == 'lowpass': - # 低通滤波效果处理 - processed_data = self._apply_lowpass_filter(audio_data, parameters) - elif effect_type == 'highpass': - # 高通滤波效果处理 - processed_data = self._apply_highpass_filter(audio_data, parameters) - elif effect_type == 'distortion': - # 失真效果处理 - processed_data = self._apply_distortion(audio_data, parameters) - else: - # 未知效果类型,不处理 - processed_data = audio_data - - return processed_data - - def _apply_reverb(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: - """ - 应用混响效果 - - Args: - audio_data: 音频数据 - parameters: 效果参数 - - Returns: - 处理后的音频数据 - """ - # 这是一个简化的实现 - # 实际应用中需要对音频数据进行混响处理 - return audio_data - - def _apply_echo(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: - """ - 应用回声效果 - - Args: - audio_data: 音频数据 - parameters: 效果参数 - - Returns: - 处理后的音频数据 - """ - # 这是一个简化的实现 - # 实际应用中需要对音频数据进行回声处理 - return audio_data - - def _apply_lowpass_filter(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: - """ - 应用低通滤波效果 - - Args: - audio_data: 音频数据 - parameters: 效果参数 - - Returns: - 处理后的音频数据 - """ - # 这是一个简化的实现 - # 实际应用中需要对音频数据进行低通滤波处理 - return audio_data - - def _apply_highpass_filter(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: - """ - 应用高通滤波效果 - - Args: - audio_data: 音频数据 - parameters: 效果参数 - - Returns: - 处理后的音频数据 - """ - # 这是一个简化的实现 - # 实际应用中需要对音频数据进行高通滤波处理 - return audio_data - - def _apply_distortion(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: - """ - 应用失真效果 - - Args: - audio_data: 音频数据 - parameters: 效果参数 - - Returns: - 处理后的音频数据 - """ - # 这是一个简化的实现 - # 实际应用中需要对音频数据进行失真处理 - return audio_data - - def cleanup(self): - """清理所有资源""" - self.effects.clear() - self.sound_effects.clear() - self.stats = { - 'total_effects': 0, - 'active_effects': 0, - 'effects_applied': 0 - } - print("✓ 音频效果处理器资源已清理") - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - +""" +音频效果处理器 +处理各种音频效果,如混响、回声、滤波等 +""" + +import uuid +from typing import Dict, List, Any, Optional +import math + +class AudioEffectProcessor: + """ + 音频效果处理器 + 处理各种音频效果,如混响、回声、滤波等 + """ + + def __init__(self, plugin): + """ + 初始化音频效果处理器 + + Args: + plugin: 3D音频插件实例 + """ + self.plugin = plugin + self.effects: Dict[str, Dict[str, Any]] = {} + self.sound_effects: Dict[str, List[str]] = {} # 音效与效果的关联 + self.stats = { + 'total_effects': 0, + 'active_effects': 0, + 'effects_applied': 0 + } + + def create_effect(self, effect_type: str, parameters: Dict[str, Any]) -> str: + """ + 创建音频效果 + + Args: + effect_type: 效果类型 ('reverb', 'echo', 'lowpass', 'highpass', 'distortion') + parameters: 效果参数 + + Returns: + 效果ID + """ + try: + # 生成唯一ID + effect_id = str(uuid.uuid4()) + + # 创建效果对象 + effect = { + 'id': effect_id, + 'type': effect_type, + 'parameters': parameters.copy(), + 'active': True + } + + # 存储效果 + self.effects[effect_id] = effect + + # 更新统计信息 + self.stats['total_effects'] += 1 + self.stats['active_effects'] += 1 + + print(f"✓ 音频效果创建成功: {effect_id} ({effect_type})") + return effect_id + + except Exception as e: + print(f"✗ 创建音频效果失败: {e}") + return "" + + def delete_effect(self, effect_id: str) -> bool: + """ + 删除音频效果 + + Args: + effect_id: 效果ID + + Returns: + 是否删除成功 + """ + if effect_id not in self.effects: + print(f"✗ 效果不存在: {effect_id}") + return False + + try: + # 从所有音效中移除该效果 + for sound_id, effect_list in self.sound_effects.items(): + if effect_id in effect_list: + effect_list.remove(effect_id) + + # 删除效果 + del self.effects[effect_id] + + # 更新统计信息 + self.stats['active_effects'] -= 1 + + print(f"✓ 音频效果已删除: {effect_id}") + return True + + except Exception as e: + print(f"✗ 删除音频效果失败: {e}") + return False + + def apply_effect_to_sound(self, sound_id: str, effect_id: str) -> bool: + """ + 应用效果到音效 + + Args: + sound_id: 音源ID + effect_id: 效果ID + + Returns: + 是否应用成功 + """ + # 检查音源和效果是否存在 + if sound_id not in self.plugin.audio_source_manager.audio_sources: + print(f"✗ 音源不存在: {sound_id}") + return False + + if effect_id not in self.effects: + print(f"✗ 效果不存在: {effect_id}") + return False + + try: + # 初始化音效的效果列表 + if sound_id not in self.sound_effects: + self.sound_effects[sound_id] = [] + + # 添加效果到音效 + if effect_id not in self.sound_effects[sound_id]: + self.sound_effects[sound_id].append(effect_id) + + # 更新统计信息 + self.stats['effects_applied'] += 1 + + effect_type = self.effects[effect_id]['type'] + print(f"✓ 效果已应用到音效: {effect_id} ({effect_type}) -> {sound_id}") + return True + else: + print(f"⚠ 效果已应用于该音效: {effect_id} -> {sound_id}") + return True + + except Exception as e: + print(f"✗ 应用效果到音效失败: {e}") + return False + + def remove_effect_from_sound(self, sound_id: str, effect_id: str) -> bool: + """ + 从音效移除效果 + + Args: + sound_id: 音源ID + effect_id: 效果ID + + Returns: + 是否移除成功 + """ + # 检查音源和效果是否存在 + if sound_id not in self.plugin.audio_source_manager.audio_sources: + print(f"✗ 音源不存在: {sound_id}") + return False + + if effect_id not in self.effects: + print(f"✗ 效果不存在: {effect_id}") + return False + + try: + # 检查效果是否已应用于该音效 + if sound_id in self.sound_effects and effect_id in self.sound_effects[sound_id]: + self.sound_effects[sound_id].remove(effect_id) + + # 更新统计信息 + self.stats['effects_applied'] -= 1 + + effect_type = self.effects[effect_id]['type'] + print(f"✓ 效果已从音效移除: {effect_id} ({effect_type}) -> {sound_id}") + return True + else: + print(f"⚠ 效果未应用于该音效: {effect_id} -> {sound_id}") + return True + + except Exception as e: + print(f"✗ 从音效移除效果失败: {e}") + return False + + def set_effect_parameter(self, effect_id: str, parameter: str, value: Any) -> bool: + """ + 设置效果参数 + + Args: + effect_id: 效果ID + parameter: 参数名 + value: 参数值 + + Returns: + 是否设置成功 + """ + if effect_id not in self.effects: + print(f"✗ 效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['parameters'][parameter] = value + print(f"✓ 效果参数已设置: {effect_id}.{parameter} = {value}") + return True + + except Exception as e: + print(f"✗ 设置效果参数失败: {e}") + return False + + def get_effect_parameters(self, effect_id: str) -> Dict[str, Any]: + """ + 获取效果参数 + + Args: + effect_id: 效果ID + + Returns: + 参数字典 + """ + if effect_id not in self.effects: + return {} + + return self.effects[effect_id]['parameters'].copy() + + def enable_effect(self, effect_id: str) -> bool: + """ + 启用效果 + + Args: + effect_id: 效果ID + + Returns: + 是否启用成功 + """ + if effect_id not in self.effects: + print(f"✗ 效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['active'] = True + self.stats['active_effects'] += 1 + print(f"✓ 效果已启用: {effect_id}") + return True + + except Exception as e: + print(f"✗ 启用效果失败: {e}") + return False + + def disable_effect(self, effect_id: str) -> bool: + """ + 禁用效果 + + Args: + effect_id: 效果ID + + Returns: + 是否禁用成功 + """ + if effect_id not in self.effects: + print(f"✗ 效果不存在: {effect_id}") + return False + + try: + self.effects[effect_id]['active'] = False + self.stats['active_effects'] -= 1 + print(f"✓ 效果已禁用: {effect_id}") + return True + + except Exception as e: + print(f"✗ 禁用效果失败: {e}") + return False + + def get_effect_type(self, effect_id: str) -> str: + """ + 获取效果类型 + + Args: + effect_id: 效果ID + + Returns: + 效果类型 + """ + if effect_id not in self.effects: + return "" + + return self.effects[effect_id]['type'] + + def get_sound_effects(self, sound_id: str) -> List[str]: + """ + 获取音效应用的效果列表 + + Args: + sound_id: 音源ID + + Returns: + 效果ID列表 + """ + return self.sound_effects.get(sound_id, []).copy() + + def process_audio_with_effects(self, sound_id: str, audio_data: Any) -> Any: + """ + 使用效果处理音频数据 + + Args: + sound_id: 音源ID + audio_data: 音频数据 + + Returns: + 处理后的音频数据 + """ + # 这是一个简化的实现 + # 实际应用中需要根据效果类型对音频数据进行处理 + + if sound_id not in self.sound_effects: + return audio_data + + # 获取应用到该音效的所有效果 + effect_ids = self.sound_effects[sound_id] + + # 按顺序应用效果 + processed_data = audio_data + for effect_id in effect_ids: + if effect_id in self.effects and self.effects[effect_id]['active']: + effect = self.effects[effect_id] + processed_data = self._apply_single_effect(processed_data, effect) + + return processed_data + + def _apply_single_effect(self, audio_data: Any, effect: Dict[str, Any]) -> Any: + """ + 应用单个效果到音频数据 + + Args: + audio_data: 音频数据 + effect: 效果配置 + + Returns: + 处理后的音频数据 + """ + # 这是一个简化的实现 + # 实际应用中需要根据效果类型对音频数据进行处理 + effect_type = effect['type'] + parameters = effect['parameters'] + + # 根据效果类型进行处理 + if effect_type == 'reverb': + # 混响效果处理 + processed_data = self._apply_reverb(audio_data, parameters) + elif effect_type == 'echo': + # 回声效果处理 + processed_data = self._apply_echo(audio_data, parameters) + elif effect_type == 'lowpass': + # 低通滤波效果处理 + processed_data = self._apply_lowpass_filter(audio_data, parameters) + elif effect_type == 'highpass': + # 高通滤波效果处理 + processed_data = self._apply_highpass_filter(audio_data, parameters) + elif effect_type == 'distortion': + # 失真效果处理 + processed_data = self._apply_distortion(audio_data, parameters) + else: + # 未知效果类型,不处理 + processed_data = audio_data + + return processed_data + + def _apply_reverb(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: + """ + 应用混响效果 + + Args: + audio_data: 音频数据 + parameters: 效果参数 + + Returns: + 处理后的音频数据 + """ + # 这是一个简化的实现 + # 实际应用中需要对音频数据进行混响处理 + return audio_data + + def _apply_echo(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: + """ + 应用回声效果 + + Args: + audio_data: 音频数据 + parameters: 效果参数 + + Returns: + 处理后的音频数据 + """ + # 这是一个简化的实现 + # 实际应用中需要对音频数据进行回声处理 + return audio_data + + def _apply_lowpass_filter(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: + """ + 应用低通滤波效果 + + Args: + audio_data: 音频数据 + parameters: 效果参数 + + Returns: + 处理后的音频数据 + """ + # 这是一个简化的实现 + # 实际应用中需要对音频数据进行低通滤波处理 + return audio_data + + def _apply_highpass_filter(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: + """ + 应用高通滤波效果 + + Args: + audio_data: 音频数据 + parameters: 效果参数 + + Returns: + 处理后的音频数据 + """ + # 这是一个简化的实现 + # 实际应用中需要对音频数据进行高通滤波处理 + return audio_data + + def _apply_distortion(self, audio_data: Any, parameters: Dict[str, Any]) -> Any: + """ + 应用失真效果 + + Args: + audio_data: 音频数据 + parameters: 效果参数 + + Returns: + 处理后的音频数据 + """ + # 这是一个简化的实现 + # 实际应用中需要对音频数据进行失真处理 + return audio_data + + def cleanup(self): + """清理所有资源""" + self.effects.clear() + self.sound_effects.clear() + self.stats = { + 'total_effects': 0, + 'active_effects': 0, + 'effects_applied': 0 + } + print("✓ 音频效果处理器资源已清理") + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + diff --git a/plugins/user/spatial_audio/plugin.py b/plugins/user/spatial_audio/plugin.py index a9035ac5..01356646 100644 --- a/plugins/user/spatial_audio/plugin.py +++ b/plugins/user/spatial_audio/plugin.py @@ -1,648 +1,648 @@ -""" -3D空间音效插件主文件 -提供基于Panda3D的3D音频处理功能,支持空间音效、环境音效和音频效果处理 -""" - -import math -from typing import Dict, List, Optional, Any -from plugins.plugin_manager import BasePlugin -from panda3d.core import AudioSound, NodePath -from direct.showbase.AudioManager import AudioManager - -class Plugin(BasePlugin): - """ - 3D空间音效插件 - 提供基于Panda3D的3D音频处理功能 - """ - - def __init__(self, plugin_manager, name): - super().__init__(plugin_manager, name) - self.config = { - "version": "1.0.0", - "author": "EG Team", - "description": "3D空间音效插件,支持3D定位音频、环境音效和音频效果处理" - } - - # 核心组件 - self.audio_manager = None - self.sound_sources = {} # 音频源管理 - self.listener = None # 听者节点 - self.audio_effects = {} # 音频效果管理 - self.environment_zones = {} # 环境区域管理 - - # GUI元素 - self.gui_elements = [] - - # 事件处理器 - self.event_handlers = {} - - # 插件状态 - self.is_initialized = False - self.is_enabled = False - - # 性能监控 - self.performance_stats = { - 'active_sounds': 0, - 'total_sounds': 0, - 'effects_applied': 0 - } - - def initialize(self) -> bool: - """ - 初始化插件 - """ - try: - if self.is_initialized: - return True - - print(f"初始化3D空间音效插件: {self.name}") - - # 初始化音频管理器 - self.audio_manager = self.plugin_manager.world.audio_manager - - # 初始化听者 - self.listener = self.plugin_manager.world.camera - - # 初始化音频源管理器 - from .core.audio_source_manager import AudioSourceManager - self.audio_source_manager = AudioSourceManager(self) - - # 初始化音频效果处理器 - from .effects.audio_effect_processor import AudioEffectProcessor - self.audio_effect_processor = AudioEffectProcessor(self) - - # 初始化环境音频管理器 - from .core.environment_audio import EnvironmentAudioManager - self.environment_audio_manager = EnvironmentAudioManager(self) - - self.is_initialized = True - print("✓ 3D空间音效插件初始化完成") - return True - - except Exception as e: - print(f"✗ 3D空间音效插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - """ - if not super().enable(): - return False - - if self.is_enabled: - return True - - try: - print(f"启用3D空间音效插件: {self.name}") - - # 注册事件处理器 - self._register_event_handlers() - - # 创建GUI界面 - self._create_gui() - - self.is_enabled = True - print("✓ 3D空间音效插件启用成功") - return True - - except Exception as e: - print(f"✗ 3D空间音效插件启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self) -> bool: - """ - 禁用插件 - """ - if not super().disable(): - return False - - if not self.is_enabled: - return True - - try: - print(f"禁用3D空间音效插件: {self.name}") - - # 清理GUI界面 - self._cleanup_gui() - - # 移除事件处理器 - self._unregister_event_handlers() - - # 停止所有音频 - self.stop_all_sounds() - - self.is_enabled = False - print("✓ 3D空间音效插件禁用成功") - return True - - except Exception as e: - print(f"✗ 3D空间音效插件禁用失败: {e}") - import traceback - traceback.print_exc() - return False - - def finalize(self): - """ - 清理插件资源 - """ - print(f"清理3D空间音效插件资源: {self.name}") - - # 停止所有音频 - self.stop_all_sounds() - - # 清理所有组件 - if self.audio_source_manager: - self.audio_source_manager.cleanup() - self.audio_source_manager = None - - if self.audio_effect_processor: - self.audio_effect_processor.cleanup() - self.audio_effect_processor = None - - if self.environment_audio_manager: - self.environment_audio_manager.cleanup() - self.environment_audio_manager = None - - self.audio_manager = None - self.listener = None - self.sound_sources.clear() - self.audio_effects.clear() - self.environment_zones.clear() - - self.is_initialized = False - self.is_enabled = False - - def _register_event_handlers(self): - """ - 注册事件处理器 - """ - world = self.plugin_manager.world - - # 3D音频系统控制快捷键 - self.event_handlers['f1'] = self.toggle_audio_debug - self.event_handlers['f2'] = self.toggle_spatial_audio - self.event_handlers['f3'] = self.play_test_sound - self.event_handlers['f4'] = self.stop_all_sounds - self.event_handlers['f5'] = self.show_audio_stats - self.event_handlers['f6'] = self.toggle_environment_audio - self.event_handlers['f7'] = self.apply_reverb_effect - self.event_handlers['f8'] = self.apply_echo_effect - - # 注册事件 - for event, handler in self.event_handlers.items(): - world.accept(event, handler) - world.accept(event.upper(), handler) # 同时注册大写事件 - - def _unregister_event_handlers(self): - """ - 移除事件处理器 - """ - world = self.plugin_manager.world - - for event in self.event_handlers.keys(): - world.ignore(event) - world.ignore(event.upper()) - - self.event_handlers.clear() - - def _create_gui(self): - """ - 创建GUI界面 - """ - try: - world = self.plugin_manager.world - - # 检查是否有GUI管理器 - if not hasattr(world, 'gui_manager') or not world.gui_manager: - print("⚠ GUI管理器不可用,跳过GUI创建") - return - - gui_manager = world.gui_manager - - # 创建3D音频控制面板 - panel_button = gui_manager.createGUIButton( - pos=(0.02, 0, 0.35), - text="3D音频", - size=0.06 - ) - self.gui_elements.append(panel_button) - - # 创建功能按钮 - functions = [ - ("音频调试 [F1]", self.toggle_audio_debug), - ("空间音频 [F2]", self.toggle_spatial_audio), - ("测试音效 [F3]", self.play_test_sound), - ("停止音频 [F4]", self.stop_all_sounds), - ("音频统计 [F5]", self.show_audio_stats), - ("环境音频 [F6]", self.toggle_environment_audio), - ("混响效果 [F7]", self.apply_reverb_effect), - ("回声效果 [F8]", self.apply_echo_effect) - ] - - for i, (name, callback) in enumerate(functions): - button = gui_manager.createGUIButton( - pos=(0.02, 0, 0.28 - i * 0.055), - text=name, - size=0.045 - ) - self.gui_elements.append(button) - world.accept(f"audio_function_{name}", callback) - - except Exception as e: - print(f"⚠ GUI创建失败: {e}") - - def _cleanup_gui(self): - """ - 清理GUI界面 - """ - try: - world = self.plugin_manager.world - - if hasattr(world, 'gui_manager') and world.gui_manager: - gui_manager = world.gui_manager - - # 删除所有GUI元素 - for element in self.gui_elements: - gui_manager.deleteGUIElement(element) - - self.gui_elements.clear() - - except Exception as e: - print(f"⚠ GUI清理失败: {e}") - - # 事件处理方法 - def toggle_audio_debug(self): - """切换音频调试模式""" - print("✓ 音频调试模式切换") - - def toggle_spatial_audio(self): - """切换空间音频""" - print("✓ 空间音频切换") - - def play_test_sound(self): - """播放测试音效""" - print("✓ 播放测试音效") - - def stop_all_sounds(self): - """停止所有音频""" - if self.audio_source_manager: - self.audio_source_manager.stop_all_sounds() - print("✓ 停止所有音频") - - def show_audio_stats(self): - """显示音频统计信息""" - stats = self.get_stats() - print("=== 3D音频统计信息 ===") - for key, value in stats.items(): - print(f" {key}: {value}") - - def toggle_environment_audio(self): - """切换环境音频""" - if self.environment_audio_manager: - self.environment_audio_manager.toggle_enabled() - print("✓ 环境音频切换") - - def apply_reverb_effect(self): - """应用混响效果""" - print("✓ 应用混响效果") - - def apply_echo_effect(self): - """应用回声效果""" - print("✓ 应用回声效果") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取插件统计信息 - - Returns: - 统计信息字典 - """ - stats = self.performance_stats.copy() - if self.audio_source_manager: - stats.update(self.audio_source_manager.get_stats()) - if self.audio_effect_processor: - stats.update(self.audio_effect_processor.get_stats()) - if self.environment_audio_manager: - stats.update(self.environment_audio_manager.get_stats()) - return stats - - # 公共接口方法 - def create_3d_sound(self, filepath: str, position: tuple = (0, 0, 0), - loop: bool = False, volume: float = 1.0) -> Optional[str]: - """ - 创建3D音效 - - Args: - filepath: 音频文件路径 - position: 音源位置 (x, y, z) - loop: 是否循环播放 - volume: 音量 (0.0-1.0) - - Returns: - 音源ID,如果创建失败则返回None - """ - if self.audio_source_manager: - return self.audio_source_manager.create_3d_sound(filepath, position, loop, volume) - return None - - def create_2d_sound(self, filepath: str, loop: bool = False, volume: float = 1.0) -> Optional[str]: - """ - 创建2D音效 - - Args: - filepath: 音频文件路径 - loop: 是否循环播放 - volume: 音量 (0.0-1.0) - - Returns: - 音源ID,如果创建失败则返回None - """ - if self.audio_source_manager: - return self.audio_source_manager.create_2d_sound(filepath, loop, volume) - return None - - def play_sound(self, sound_id: str) -> bool: - """ - 播放音效 - - Args: - sound_id: 音源ID - - Returns: - 是否播放成功 - """ - if self.audio_source_manager: - return self.audio_source_manager.play_sound(sound_id) - return False - - def stop_sound(self, sound_id: str) -> bool: - """ - 停止音效 - - Args: - sound_id: 音源ID - - Returns: - 是否停止成功 - """ - if self.audio_source_manager: - return self.audio_source_manager.stop_sound(sound_id) - return False - - def pause_sound(self, sound_id: str) -> bool: - """ - 暂停音效 - - Args: - sound_id: 音源ID - - Returns: - 是否暂停成功 - """ - if self.audio_source_manager: - return self.audio_source_manager.pause_sound(sound_id) - return False - - def resume_sound(self, sound_id: str) -> bool: - """ - 恢复音效 - - Args: - sound_id: 音源ID - - Returns: - 是否恢复成功 - """ - if self.audio_source_manager: - return self.audio_source_manager.resume_sound(sound_id) - return False - - def set_sound_position(self, sound_id: str, position: tuple) -> bool: - """ - 设置音源位置 - - Args: - sound_id: 音源ID - position: 位置 (x, y, z) - - Returns: - 是否设置成功 - """ - if self.audio_source_manager: - return self.audio_source_manager.set_sound_position(sound_id, position) - return False - - def set_sound_volume(self, sound_id: str, volume: float) -> bool: - """ - 设置音源音量 - - Args: - sound_id: 音源ID - volume: 音量 (0.0-1.0) - - Returns: - 是否设置成功 - """ - if self.audio_source_manager: - return self.audio_source_manager.set_sound_volume(sound_id, volume) - return False - - def set_sound_loop(self, sound_id: str, loop: bool) -> bool: - """ - 设置音源循环模式 - - Args: - sound_id: 音源ID - loop: 是否循环 - - Returns: - 是否设置成功 - """ - if self.audio_source_manager: - return self.audio_source_manager.set_sound_loop(sound_id, loop) - return False - - def is_sound_playing(self, sound_id: str) -> bool: - """ - 检查音效是否正在播放 - - Args: - sound_id: 音源ID - - Returns: - 是否正在播放 - """ - if self.audio_source_manager: - return self.audio_source_manager.is_sound_playing(sound_id) - return False - - def get_sound_duration(self, sound_id: str) -> float: - """ - 获取音效时长 - - Args: - sound_id: 音源ID - - Returns: - 音效时长(秒) - """ - if self.audio_source_manager: - return self.audio_source_manager.get_sound_duration(sound_id) - return 0.0 - - def get_sound_position(self, sound_id: str) -> tuple: - """ - 获取音源位置 - - Args: - sound_id: 音源ID - - Returns: - 音源位置 (x, y, z) - """ - if self.audio_source_manager: - return self.audio_source_manager.get_sound_position(sound_id) - return (0, 0, 0) - - def delete_sound(self, sound_id: str) -> bool: - """ - 删除音效 - - Args: - sound_id: 音源ID - - Returns: - 是否删除成功 - """ - if self.audio_source_manager: - return self.audio_source_manager.delete_sound(sound_id) - return False - - def create_audio_effect(self, effect_type: str, parameters: Dict[str, Any]) -> str: - """ - 创建音频效果 - - Args: - effect_type: 效果类型 - parameters: 效果参数 - - Returns: - 效果ID - """ - if self.audio_effect_processor: - return self.audio_effect_processor.create_effect(effect_type, parameters) - return "" - - def apply_effect_to_sound(self, sound_id: str, effect_id: str) -> bool: - """ - 应用效果到音效 - - Args: - sound_id: 音源ID - effect_id: 效果ID - - Returns: - 是否应用成功 - """ - if self.audio_effect_processor: - return self.audio_effect_processor.apply_effect_to_sound(sound_id, effect_id) - return False - - def remove_effect_from_sound(self, sound_id: str, effect_id: str) -> bool: - """ - 从音效移除效果 - - Args: - sound_id: 音源ID - effect_id: 效果ID - - Returns: - 是否移除成功 - """ - if self.audio_effect_processor: - return self.audio_effect_processor.remove_effect_from_sound(sound_id, effect_id) - return False - - def set_listener_position(self, position: tuple) -> bool: - """ - 设置听者位置 - - Args: - position: 位置 (x, y, z) - - Returns: - 是否设置成功 - """ - if self.listener: - self.listener.setPos(position[0], position[1], position[2]) - return True - return False - - def set_listener_orientation(self, orientation: tuple) -> bool: - """ - 设置听者方向 - - Args: - orientation: 方向 (heading, pitch, roll) - - Returns: - 是否设置成功 - """ - if self.listener: - self.listener.setHpr(orientation[0], orientation[1], orientation[2]) - return True - return False - - def create_environment_zone(self, zone_id: str, position: tuple, - radius: float, properties: Dict[str, Any]) -> bool: - """ - 创建环境区域 - - Args: - zone_id: 区域ID - position: 位置 (x, y, z) - radius: 半径 - properties: 区域属性 - - Returns: - 是否创建成功 - """ - if self.environment_audio_manager: - return self.environment_audio_manager.create_zone(zone_id, position, radius, properties) - return False - - def set_zone_property(self, zone_id: str, property_name: str, value: Any) -> bool: - """ - 设置区域属性 - - Args: - zone_id: 区域ID - property_name: 属性名 - value: 属性值 - - Returns: - 是否设置成功 - """ - if self.environment_audio_manager: - return self.environment_audio_manager.set_zone_property(zone_id, property_name, value) - return False - - def update(self, dt: float): - """ - 更新插件状态 - - Args: - dt: 时间增量 - """ - # 更新音频源管理器 - if self.audio_source_manager: - self.audio_source_manager.update(dt) - - # 更新环境音频管理器 - if self.environment_audio_manager: +""" +3D空间音效插件主文件 +提供基于Panda3D的3D音频处理功能,支持空间音效、环境音效和音频效果处理 +""" + +import math +from typing import Dict, List, Optional, Any +from plugins.plugin_manager import BasePlugin +from panda3d.core import AudioSound, NodePath +from direct.showbase.AudioManager import AudioManager + +class Plugin(BasePlugin): + """ + 3D空间音效插件 + 提供基于Panda3D的3D音频处理功能 + """ + + def __init__(self, plugin_manager, name): + super().__init__(plugin_manager, name) + self.config = { + "version": "1.0.0", + "author": "EG Team", + "description": "3D空间音效插件,支持3D定位音频、环境音效和音频效果处理" + } + + # 核心组件 + self.audio_manager = None + self.sound_sources = {} # 音频源管理 + self.listener = None # 听者节点 + self.audio_effects = {} # 音频效果管理 + self.environment_zones = {} # 环境区域管理 + + # GUI元素 + self.gui_elements = [] + + # 事件处理器 + self.event_handlers = {} + + # 插件状态 + self.is_initialized = False + self.is_enabled = False + + # 性能监控 + self.performance_stats = { + 'active_sounds': 0, + 'total_sounds': 0, + 'effects_applied': 0 + } + + def initialize(self) -> bool: + """ + 初始化插件 + """ + try: + if self.is_initialized: + return True + + print(f"初始化3D空间音效插件: {self.name}") + + # 初始化音频管理器 + self.audio_manager = self.plugin_manager.world.audio_manager + + # 初始化听者 + self.listener = self.plugin_manager.world.camera + + # 初始化音频源管理器 + from .core.audio_source_manager import AudioSourceManager + self.audio_source_manager = AudioSourceManager(self) + + # 初始化音频效果处理器 + from .effects.audio_effect_processor import AudioEffectProcessor + self.audio_effect_processor = AudioEffectProcessor(self) + + # 初始化环境音频管理器 + from .core.environment_audio import EnvironmentAudioManager + self.environment_audio_manager = EnvironmentAudioManager(self) + + self.is_initialized = True + print("✓ 3D空间音效插件初始化完成") + return True + + except Exception as e: + print(f"✗ 3D空间音效插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + """ + if not super().enable(): + return False + + if self.is_enabled: + return True + + try: + print(f"启用3D空间音效插件: {self.name}") + + # 注册事件处理器 + self._register_event_handlers() + + # 创建GUI界面 + self._create_gui() + + self.is_enabled = True + print("✓ 3D空间音效插件启用成功") + return True + + except Exception as e: + print(f"✗ 3D空间音效插件启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self) -> bool: + """ + 禁用插件 + """ + if not super().disable(): + return False + + if not self.is_enabled: + return True + + try: + print(f"禁用3D空间音效插件: {self.name}") + + # 清理GUI界面 + self._cleanup_gui() + + # 移除事件处理器 + self._unregister_event_handlers() + + # 停止所有音频 + self.stop_all_sounds() + + self.is_enabled = False + print("✓ 3D空间音效插件禁用成功") + return True + + except Exception as e: + print(f"✗ 3D空间音效插件禁用失败: {e}") + import traceback + traceback.print_exc() + return False + + def finalize(self): + """ + 清理插件资源 + """ + print(f"清理3D空间音效插件资源: {self.name}") + + # 停止所有音频 + self.stop_all_sounds() + + # 清理所有组件 + if self.audio_source_manager: + self.audio_source_manager.cleanup() + self.audio_source_manager = None + + if self.audio_effect_processor: + self.audio_effect_processor.cleanup() + self.audio_effect_processor = None + + if self.environment_audio_manager: + self.environment_audio_manager.cleanup() + self.environment_audio_manager = None + + self.audio_manager = None + self.listener = None + self.sound_sources.clear() + self.audio_effects.clear() + self.environment_zones.clear() + + self.is_initialized = False + self.is_enabled = False + + def _register_event_handlers(self): + """ + 注册事件处理器 + """ + world = self.plugin_manager.world + + # 3D音频系统控制快捷键 + self.event_handlers['f1'] = self.toggle_audio_debug + self.event_handlers['f2'] = self.toggle_spatial_audio + self.event_handlers['f3'] = self.play_test_sound + self.event_handlers['f4'] = self.stop_all_sounds + self.event_handlers['f5'] = self.show_audio_stats + self.event_handlers['f6'] = self.toggle_environment_audio + self.event_handlers['f7'] = self.apply_reverb_effect + self.event_handlers['f8'] = self.apply_echo_effect + + # 注册事件 + for event, handler in self.event_handlers.items(): + world.accept(event, handler) + world.accept(event.upper(), handler) # 同时注册大写事件 + + def _unregister_event_handlers(self): + """ + 移除事件处理器 + """ + world = self.plugin_manager.world + + for event in self.event_handlers.keys(): + world.ignore(event) + world.ignore(event.upper()) + + self.event_handlers.clear() + + def _create_gui(self): + """ + 创建GUI界面 + """ + try: + world = self.plugin_manager.world + + # 检查是否有GUI管理器 + if not hasattr(world, 'gui_manager') or not world.gui_manager: + print("⚠ GUI管理器不可用,跳过GUI创建") + return + + gui_manager = world.gui_manager + + # 创建3D音频控制面板 + panel_button = gui_manager.createGUIButton( + pos=(0.02, 0, 0.35), + text="3D音频", + size=0.06 + ) + self.gui_elements.append(panel_button) + + # 创建功能按钮 + functions = [ + ("音频调试 [F1]", self.toggle_audio_debug), + ("空间音频 [F2]", self.toggle_spatial_audio), + ("测试音效 [F3]", self.play_test_sound), + ("停止音频 [F4]", self.stop_all_sounds), + ("音频统计 [F5]", self.show_audio_stats), + ("环境音频 [F6]", self.toggle_environment_audio), + ("混响效果 [F7]", self.apply_reverb_effect), + ("回声效果 [F8]", self.apply_echo_effect) + ] + + for i, (name, callback) in enumerate(functions): + button = gui_manager.createGUIButton( + pos=(0.02, 0, 0.28 - i * 0.055), + text=name, + size=0.045 + ) + self.gui_elements.append(button) + world.accept(f"audio_function_{name}", callback) + + except Exception as e: + print(f"⚠ GUI创建失败: {e}") + + def _cleanup_gui(self): + """ + 清理GUI界面 + """ + try: + world = self.plugin_manager.world + + if hasattr(world, 'gui_manager') and world.gui_manager: + gui_manager = world.gui_manager + + # 删除所有GUI元素 + for element in self.gui_elements: + gui_manager.deleteGUIElement(element) + + self.gui_elements.clear() + + except Exception as e: + print(f"⚠ GUI清理失败: {e}") + + # 事件处理方法 + def toggle_audio_debug(self): + """切换音频调试模式""" + print("✓ 音频调试模式切换") + + def toggle_spatial_audio(self): + """切换空间音频""" + print("✓ 空间音频切换") + + def play_test_sound(self): + """播放测试音效""" + print("✓ 播放测试音效") + + def stop_all_sounds(self): + """停止所有音频""" + if self.audio_source_manager: + self.audio_source_manager.stop_all_sounds() + print("✓ 停止所有音频") + + def show_audio_stats(self): + """显示音频统计信息""" + stats = self.get_stats() + print("=== 3D音频统计信息 ===") + for key, value in stats.items(): + print(f" {key}: {value}") + + def toggle_environment_audio(self): + """切换环境音频""" + if self.environment_audio_manager: + self.environment_audio_manager.toggle_enabled() + print("✓ 环境音频切换") + + def apply_reverb_effect(self): + """应用混响效果""" + print("✓ 应用混响效果") + + def apply_echo_effect(self): + """应用回声效果""" + print("✓ 应用回声效果") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取插件统计信息 + + Returns: + 统计信息字典 + """ + stats = self.performance_stats.copy() + if self.audio_source_manager: + stats.update(self.audio_source_manager.get_stats()) + if self.audio_effect_processor: + stats.update(self.audio_effect_processor.get_stats()) + if self.environment_audio_manager: + stats.update(self.environment_audio_manager.get_stats()) + return stats + + # 公共接口方法 + def create_3d_sound(self, filepath: str, position: tuple = (0, 0, 0), + loop: bool = False, volume: float = 1.0) -> Optional[str]: + """ + 创建3D音效 + + Args: + filepath: 音频文件路径 + position: 音源位置 (x, y, z) + loop: 是否循环播放 + volume: 音量 (0.0-1.0) + + Returns: + 音源ID,如果创建失败则返回None + """ + if self.audio_source_manager: + return self.audio_source_manager.create_3d_sound(filepath, position, loop, volume) + return None + + def create_2d_sound(self, filepath: str, loop: bool = False, volume: float = 1.0) -> Optional[str]: + """ + 创建2D音效 + + Args: + filepath: 音频文件路径 + loop: 是否循环播放 + volume: 音量 (0.0-1.0) + + Returns: + 音源ID,如果创建失败则返回None + """ + if self.audio_source_manager: + return self.audio_source_manager.create_2d_sound(filepath, loop, volume) + return None + + def play_sound(self, sound_id: str) -> bool: + """ + 播放音效 + + Args: + sound_id: 音源ID + + Returns: + 是否播放成功 + """ + if self.audio_source_manager: + return self.audio_source_manager.play_sound(sound_id) + return False + + def stop_sound(self, sound_id: str) -> bool: + """ + 停止音效 + + Args: + sound_id: 音源ID + + Returns: + 是否停止成功 + """ + if self.audio_source_manager: + return self.audio_source_manager.stop_sound(sound_id) + return False + + def pause_sound(self, sound_id: str) -> bool: + """ + 暂停音效 + + Args: + sound_id: 音源ID + + Returns: + 是否暂停成功 + """ + if self.audio_source_manager: + return self.audio_source_manager.pause_sound(sound_id) + return False + + def resume_sound(self, sound_id: str) -> bool: + """ + 恢复音效 + + Args: + sound_id: 音源ID + + Returns: + 是否恢复成功 + """ + if self.audio_source_manager: + return self.audio_source_manager.resume_sound(sound_id) + return False + + def set_sound_position(self, sound_id: str, position: tuple) -> bool: + """ + 设置音源位置 + + Args: + sound_id: 音源ID + position: 位置 (x, y, z) + + Returns: + 是否设置成功 + """ + if self.audio_source_manager: + return self.audio_source_manager.set_sound_position(sound_id, position) + return False + + def set_sound_volume(self, sound_id: str, volume: float) -> bool: + """ + 设置音源音量 + + Args: + sound_id: 音源ID + volume: 音量 (0.0-1.0) + + Returns: + 是否设置成功 + """ + if self.audio_source_manager: + return self.audio_source_manager.set_sound_volume(sound_id, volume) + return False + + def set_sound_loop(self, sound_id: str, loop: bool) -> bool: + """ + 设置音源循环模式 + + Args: + sound_id: 音源ID + loop: 是否循环 + + Returns: + 是否设置成功 + """ + if self.audio_source_manager: + return self.audio_source_manager.set_sound_loop(sound_id, loop) + return False + + def is_sound_playing(self, sound_id: str) -> bool: + """ + 检查音效是否正在播放 + + Args: + sound_id: 音源ID + + Returns: + 是否正在播放 + """ + if self.audio_source_manager: + return self.audio_source_manager.is_sound_playing(sound_id) + return False + + def get_sound_duration(self, sound_id: str) -> float: + """ + 获取音效时长 + + Args: + sound_id: 音源ID + + Returns: + 音效时长(秒) + """ + if self.audio_source_manager: + return self.audio_source_manager.get_sound_duration(sound_id) + return 0.0 + + def get_sound_position(self, sound_id: str) -> tuple: + """ + 获取音源位置 + + Args: + sound_id: 音源ID + + Returns: + 音源位置 (x, y, z) + """ + if self.audio_source_manager: + return self.audio_source_manager.get_sound_position(sound_id) + return (0, 0, 0) + + def delete_sound(self, sound_id: str) -> bool: + """ + 删除音效 + + Args: + sound_id: 音源ID + + Returns: + 是否删除成功 + """ + if self.audio_source_manager: + return self.audio_source_manager.delete_sound(sound_id) + return False + + def create_audio_effect(self, effect_type: str, parameters: Dict[str, Any]) -> str: + """ + 创建音频效果 + + Args: + effect_type: 效果类型 + parameters: 效果参数 + + Returns: + 效果ID + """ + if self.audio_effect_processor: + return self.audio_effect_processor.create_effect(effect_type, parameters) + return "" + + def apply_effect_to_sound(self, sound_id: str, effect_id: str) -> bool: + """ + 应用效果到音效 + + Args: + sound_id: 音源ID + effect_id: 效果ID + + Returns: + 是否应用成功 + """ + if self.audio_effect_processor: + return self.audio_effect_processor.apply_effect_to_sound(sound_id, effect_id) + return False + + def remove_effect_from_sound(self, sound_id: str, effect_id: str) -> bool: + """ + 从音效移除效果 + + Args: + sound_id: 音源ID + effect_id: 效果ID + + Returns: + 是否移除成功 + """ + if self.audio_effect_processor: + return self.audio_effect_processor.remove_effect_from_sound(sound_id, effect_id) + return False + + def set_listener_position(self, position: tuple) -> bool: + """ + 设置听者位置 + + Args: + position: 位置 (x, y, z) + + Returns: + 是否设置成功 + """ + if self.listener: + self.listener.setPos(position[0], position[1], position[2]) + return True + return False + + def set_listener_orientation(self, orientation: tuple) -> bool: + """ + 设置听者方向 + + Args: + orientation: 方向 (heading, pitch, roll) + + Returns: + 是否设置成功 + """ + if self.listener: + self.listener.setHpr(orientation[0], orientation[1], orientation[2]) + return True + return False + + def create_environment_zone(self, zone_id: str, position: tuple, + radius: float, properties: Dict[str, Any]) -> bool: + """ + 创建环境区域 + + Args: + zone_id: 区域ID + position: 位置 (x, y, z) + radius: 半径 + properties: 区域属性 + + Returns: + 是否创建成功 + """ + if self.environment_audio_manager: + return self.environment_audio_manager.create_zone(zone_id, position, radius, properties) + return False + + def set_zone_property(self, zone_id: str, property_name: str, value: Any) -> bool: + """ + 设置区域属性 + + Args: + zone_id: 区域ID + property_name: 属性名 + value: 属性值 + + Returns: + 是否设置成功 + """ + if self.environment_audio_manager: + return self.environment_audio_manager.set_zone_property(zone_id, property_name, value) + return False + + def update(self, dt: float): + """ + 更新插件状态 + + Args: + dt: 时间增量 + """ + # 更新音频源管理器 + if self.audio_source_manager: + self.audio_source_manager.update(dt) + + # 更新环境音频管理器 + if self.environment_audio_manager: self.environment_audio_manager.update(dt) \ No newline at end of file diff --git a/plugins/user/spatial_audio/utils/audio_utils.py b/plugins/user/spatial_audio/utils/audio_utils.py index 7b2bba16..40ffc4ba 100644 --- a/plugins/user/spatial_audio/utils/audio_utils.py +++ b/plugins/user/spatial_audio/utils/audio_utils.py @@ -1,329 +1,329 @@ -""" -音频工具类 -提供音频处理相关的实用工具函数 -""" - -import math -from typing import Tuple, List, Dict, Any -from panda3d.core import Vec3 - -def calculate_distance(pos1: Tuple[float, float, float], - pos2: Tuple[float, float, float]) -> float: - """ - 计算两点间距离 - - Args: - pos1: 第一个点 (x, y, z) - pos2: 第二个点 (x, y, z) - - Returns: - 距离 - """ - dx = pos1[0] - pos2[0] - dy = pos1[1] - pos2[1] - dz = pos1[2] - pos2[2] - return math.sqrt(dx*dx + dy*dy + dz*dz) - - -def calculate_volume_attenuation(distance: float, max_distance: float = 50.0, - min_volume: float = 0.0, max_volume: float = 1.0) -> float: - """ - 计算基于距离的音量衰减 - - Args: - distance: 距离 - max_distance: 最大距离(在此距离外音量为最小值) - min_volume: 最小音量 - max_volume: 最大音量 - - Returns: - 计算后的音量 - """ - if distance >= max_distance: - return min_volume - - # 线性衰减 - attenuation = 1.0 - (distance / max_distance) - volume = min_volume + (max_volume - min_volume) * attenuation - return max(min_volume, min(max_volume, volume)) - - -def calculate_pan(position: Tuple[float, float, float], - listener_position: Tuple[float, float, float]) -> float: - """ - 计算立体声平移(左/右声道平衡) - - Args: - position: 音源位置 - listener_position: 听者位置 - - Returns: - 平移值 (-1.0 到 1.0,-1为左,1为右) - """ - # 计算相对位置 - rel_x = position[0] - listener_position[0] - rel_z = position[2] - listener_position[2] - - # 计算角度 - angle = math.atan2(rel_x, rel_z) - - # 转换为平移值 - pan = math.sin(angle) - return max(-1.0, min(1.0, pan)) - - -def interpolate_positions(start_pos: Tuple[float, float, float], - end_pos: Tuple[float, float, float], - steps: int) -> List[Tuple[float, float, float]]: - """ - 在两点间插值生成位置列表 - - Args: - start_pos: 起始位置 - end_pos: 结束位置 - steps: 插值步数 - - Returns: - 插值位置列表 - """ - if steps <= 1: - return [start_pos, end_pos] - - positions = [] - for i in range(steps): - t = i / (steps - 1) - x = start_pos[0] + (end_pos[0] - start_pos[0]) * t - y = start_pos[1] + (end_pos[1] - start_pos[1]) * t - z = start_pos[2] + (end_pos[2] - start_pos[2]) * t - positions.append((x, y, z)) - - return positions - - -def validate_audio_file(filepath: str) -> Dict[str, Any]: - """ - 验证音频文件 - - Args: - filepath: 音频文件路径 - - Returns: - 验证结果字典 - """ - import os - - result = { - 'valid': False, - 'exists': False, - 'file_size': 0, - 'extension': '', - 'supported': False - } - - # 检查文件是否存在 - if not os.path.exists(filepath): - result['error'] = '文件不存在' - return result - - result['exists'] = True - - # 获取文件信息 - result['file_size'] = os.path.getsize(filepath) - _, extension = os.path.splitext(filepath) - result['extension'] = extension.lower() - - # 检查是否为支持的格式 - supported_formats = ['.wav', '.ogg', '.mp3'] - result['supported'] = result['extension'] in supported_formats - - # 验证通过 - if result['supported'] and result['file_size'] > 0: - result['valid'] = True - - return result - - -def generate_test_tone(frequency: float = 440.0, duration: float = 1.0, - sample_rate: int = 44100) -> bytes: - """ - 生成测试音调(正弦波) - - Args: - frequency: 频率 (Hz) - duration: 持续时间 (秒) - sample_rate: 采样率 - - Returns: - 音频数据字节 - """ - import math - import struct - - # 生成正弦波数据 - num_samples = int(duration * sample_rate) - audio_data = bytearray() - - for i in range(num_samples): - # 生成正弦波样本 - t = i / sample_rate - sample = math.sin(2 * math.pi * frequency * t) - - # 转换为16位整数 - sample_int = int(sample * 32767) - audio_data.extend(struct.pack(' bytes: - """ - 将多声道音频转换为单声道 - - Args: - audio_data: 音频数据 - channels: 声道数 - - Returns: - 单声道音频数据 - """ - if channels == 1: - return audio_data - - # 假设16位音频数据 - sample_size = 2 - frame_size = sample_size * channels - - mono_data = bytearray() - for i in range(0, len(audio_data), frame_size): - # 取第一个声道的数据 - mono_data.extend(audio_data[i:i+sample_size]) - - return bytes(mono_data) - - -def apply_volume_multiplier(audio_data: bytes, volume: float) -> bytes: - """ - 应用音量乘数到音频数据 - - Args: - audio_data: 音频数据 - volume: 音量乘数 (0.0-1.0) - - Returns: - 处理后的音频数据 - """ - import struct - - if volume == 1.0: - return audio_data - - # 假设16位音频数据 - sample_count = len(audio_data) // 2 - processed_data = bytearray() - - for i in range(sample_count): - # 解包16位样本 - sample = struct.unpack_from(' float: - """ - 计算多普勒频移 - - Args: - source_velocity: 音源速度向量 - listener_velocity: 听者速度向量 - sound_speed: 声速 (米/秒) - - Returns: - 频率比率 (播放频率/原始频率) - """ - # 计算相对速度 - rel_velocity_x = source_velocity[0] - listener_velocity[0] - rel_velocity_z = source_velocity[2] - listener_velocity[2] - - # 计算速度大小 - source_speed = math.sqrt(rel_velocity_x**2 + rel_velocity_z**2) - - # 简化的多普勒效应计算 - if sound_speed > source_speed: - ratio = (sound_speed - source_speed) / sound_speed - return max(0.5, min(2.0, ratio)) # 限制在合理范围内 - else: - return 0.5 # 音源速度接近或超过声速时的处理 - - -def create_hrtf_filter(angle: float, elevation: float) -> Dict[str, List[float]]: - """ - 创建HRTF(头部相关传递函数)滤波器 - - Args: - angle: 水平角度 (度) - elevation: 垂直角度 (度) - - Returns: - 左右声道滤波器系数 - """ - # 这是一个简化的HRTF实现 - # 实际应用中会使用真实的HRTF测量数据 - - # 简单的左右声道延迟差异 - angle_rad = math.radians(angle) - delay_diff = math.sin(angle_rad) * 0.001 # 最大1毫秒延迟差异 - - # 简单的左右声道幅度差异 - left_amp = 1.0 - max(0, math.sin(angle_rad)) * 0.3 - right_amp = 1.0 - max(0, -math.sin(angle_rad)) * 0.3 - - return { - 'left': [left_amp, 0.0, 0.0], - 'right': [right_amp, 0.0, delay_diff] - } - - -def calculate_reverb_parameters(room_size: Tuple[float, float, float], - absorption: float = 0.5) -> Dict[str, float]: - """ - 根据房间尺寸计算混响参数 - - Args: - room_size: 房间尺寸 (长, 宽, 高) - absorption: 吸收系数 (0.0-1.0) - - Returns: - 混响参数字典 - """ - length, width, height = room_size - - # 计算房间体积 - volume = length * width * height - - # 计算表面积 - surface_area = 2 * (length*width + length*height + width*height) - - # 简化的混响时间计算(Sabine公式) - reverb_time = 0.161 * volume / (surface_area * absorption) - - # 限制在合理范围内 - reverb_time = max(0.1, min(10.0, reverb_time)) - - return { - 'reverb_time': reverb_time, - 'early_reflections': reverb_time * 0.1, - 'late_reverb': reverb_time * 0.9, - 'density': min(1.0, volume / 1000.0), - 'diffusion': 1.0 - absorption +""" +音频工具类 +提供音频处理相关的实用工具函数 +""" + +import math +from typing import Tuple, List, Dict, Any +from panda3d.core import Vec3 + +def calculate_distance(pos1: Tuple[float, float, float], + pos2: Tuple[float, float, float]) -> float: + """ + 计算两点间距离 + + Args: + pos1: 第一个点 (x, y, z) + pos2: 第二个点 (x, y, z) + + Returns: + 距离 + """ + dx = pos1[0] - pos2[0] + dy = pos1[1] - pos2[1] + dz = pos1[2] - pos2[2] + return math.sqrt(dx*dx + dy*dy + dz*dz) + + +def calculate_volume_attenuation(distance: float, max_distance: float = 50.0, + min_volume: float = 0.0, max_volume: float = 1.0) -> float: + """ + 计算基于距离的音量衰减 + + Args: + distance: 距离 + max_distance: 最大距离(在此距离外音量为最小值) + min_volume: 最小音量 + max_volume: 最大音量 + + Returns: + 计算后的音量 + """ + if distance >= max_distance: + return min_volume + + # 线性衰减 + attenuation = 1.0 - (distance / max_distance) + volume = min_volume + (max_volume - min_volume) * attenuation + return max(min_volume, min(max_volume, volume)) + + +def calculate_pan(position: Tuple[float, float, float], + listener_position: Tuple[float, float, float]) -> float: + """ + 计算立体声平移(左/右声道平衡) + + Args: + position: 音源位置 + listener_position: 听者位置 + + Returns: + 平移值 (-1.0 到 1.0,-1为左,1为右) + """ + # 计算相对位置 + rel_x = position[0] - listener_position[0] + rel_z = position[2] - listener_position[2] + + # 计算角度 + angle = math.atan2(rel_x, rel_z) + + # 转换为平移值 + pan = math.sin(angle) + return max(-1.0, min(1.0, pan)) + + +def interpolate_positions(start_pos: Tuple[float, float, float], + end_pos: Tuple[float, float, float], + steps: int) -> List[Tuple[float, float, float]]: + """ + 在两点间插值生成位置列表 + + Args: + start_pos: 起始位置 + end_pos: 结束位置 + steps: 插值步数 + + Returns: + 插值位置列表 + """ + if steps <= 1: + return [start_pos, end_pos] + + positions = [] + for i in range(steps): + t = i / (steps - 1) + x = start_pos[0] + (end_pos[0] - start_pos[0]) * t + y = start_pos[1] + (end_pos[1] - start_pos[1]) * t + z = start_pos[2] + (end_pos[2] - start_pos[2]) * t + positions.append((x, y, z)) + + return positions + + +def validate_audio_file(filepath: str) -> Dict[str, Any]: + """ + 验证音频文件 + + Args: + filepath: 音频文件路径 + + Returns: + 验证结果字典 + """ + import os + + result = { + 'valid': False, + 'exists': False, + 'file_size': 0, + 'extension': '', + 'supported': False + } + + # 检查文件是否存在 + if not os.path.exists(filepath): + result['error'] = '文件不存在' + return result + + result['exists'] = True + + # 获取文件信息 + result['file_size'] = os.path.getsize(filepath) + _, extension = os.path.splitext(filepath) + result['extension'] = extension.lower() + + # 检查是否为支持的格式 + supported_formats = ['.wav', '.ogg', '.mp3'] + result['supported'] = result['extension'] in supported_formats + + # 验证通过 + if result['supported'] and result['file_size'] > 0: + result['valid'] = True + + return result + + +def generate_test_tone(frequency: float = 440.0, duration: float = 1.0, + sample_rate: int = 44100) -> bytes: + """ + 生成测试音调(正弦波) + + Args: + frequency: 频率 (Hz) + duration: 持续时间 (秒) + sample_rate: 采样率 + + Returns: + 音频数据字节 + """ + import math + import struct + + # 生成正弦波数据 + num_samples = int(duration * sample_rate) + audio_data = bytearray() + + for i in range(num_samples): + # 生成正弦波样本 + t = i / sample_rate + sample = math.sin(2 * math.pi * frequency * t) + + # 转换为16位整数 + sample_int = int(sample * 32767) + audio_data.extend(struct.pack(' bytes: + """ + 将多声道音频转换为单声道 + + Args: + audio_data: 音频数据 + channels: 声道数 + + Returns: + 单声道音频数据 + """ + if channels == 1: + return audio_data + + # 假设16位音频数据 + sample_size = 2 + frame_size = sample_size * channels + + mono_data = bytearray() + for i in range(0, len(audio_data), frame_size): + # 取第一个声道的数据 + mono_data.extend(audio_data[i:i+sample_size]) + + return bytes(mono_data) + + +def apply_volume_multiplier(audio_data: bytes, volume: float) -> bytes: + """ + 应用音量乘数到音频数据 + + Args: + audio_data: 音频数据 + volume: 音量乘数 (0.0-1.0) + + Returns: + 处理后的音频数据 + """ + import struct + + if volume == 1.0: + return audio_data + + # 假设16位音频数据 + sample_count = len(audio_data) // 2 + processed_data = bytearray() + + for i in range(sample_count): + # 解包16位样本 + sample = struct.unpack_from(' float: + """ + 计算多普勒频移 + + Args: + source_velocity: 音源速度向量 + listener_velocity: 听者速度向量 + sound_speed: 声速 (米/秒) + + Returns: + 频率比率 (播放频率/原始频率) + """ + # 计算相对速度 + rel_velocity_x = source_velocity[0] - listener_velocity[0] + rel_velocity_z = source_velocity[2] - listener_velocity[2] + + # 计算速度大小 + source_speed = math.sqrt(rel_velocity_x**2 + rel_velocity_z**2) + + # 简化的多普勒效应计算 + if sound_speed > source_speed: + ratio = (sound_speed - source_speed) / sound_speed + return max(0.5, min(2.0, ratio)) # 限制在合理范围内 + else: + return 0.5 # 音源速度接近或超过声速时的处理 + + +def create_hrtf_filter(angle: float, elevation: float) -> Dict[str, List[float]]: + """ + 创建HRTF(头部相关传递函数)滤波器 + + Args: + angle: 水平角度 (度) + elevation: 垂直角度 (度) + + Returns: + 左右声道滤波器系数 + """ + # 这是一个简化的HRTF实现 + # 实际应用中会使用真实的HRTF测量数据 + + # 简单的左右声道延迟差异 + angle_rad = math.radians(angle) + delay_diff = math.sin(angle_rad) * 0.001 # 最大1毫秒延迟差异 + + # 简单的左右声道幅度差异 + left_amp = 1.0 - max(0, math.sin(angle_rad)) * 0.3 + right_amp = 1.0 - max(0, -math.sin(angle_rad)) * 0.3 + + return { + 'left': [left_amp, 0.0, 0.0], + 'right': [right_amp, 0.0, delay_diff] + } + + +def calculate_reverb_parameters(room_size: Tuple[float, float, float], + absorption: float = 0.5) -> Dict[str, float]: + """ + 根据房间尺寸计算混响参数 + + Args: + room_size: 房间尺寸 (长, 宽, 高) + absorption: 吸收系数 (0.0-1.0) + + Returns: + 混响参数字典 + """ + length, width, height = room_size + + # 计算房间体积 + volume = length * width * height + + # 计算表面积 + surface_area = 2 * (length*width + length*height + width*height) + + # 简化的混响时间计算(Sabine公式) + reverb_time = 0.161 * volume / (surface_area * absorption) + + # 限制在合理范围内 + reverb_time = max(0.1, min(10.0, reverb_time)) + + return { + 'reverb_time': reverb_time, + 'early_reflections': reverb_time * 0.1, + 'late_reverb': reverb_time * 0.9, + 'density': min(1.0, volume / 1000.0), + 'diffusion': 1.0 - absorption } \ No newline at end of file diff --git a/plugins/user/speech_recognition_synthesis/README.md b/plugins/user/speech_recognition_synthesis/README.md index 07d2395f..ea265864 100644 --- a/plugins/user/speech_recognition_synthesis/README.md +++ b/plugins/user/speech_recognition_synthesis/README.md @@ -1,431 +1,431 @@ -# 语音识别和合成插件 - -一个功能完整的语音识别和文本转语音合成插件,为EG引擎提供实时语音处理功能,支持多种语言、语音和音频效果处理。 - -## 功能特性 - -### 核心功能 -- **语音识别**: 实时语音转文本功能 -- **文本转语音**: 高质量文本转语音合成 -- **音频处理**: 专业的音频效果处理 -- **多语言支持**: 支持多种语言的识别和合成 -- **多语音支持**: 提供多种语音选择 -- **实时处理**: 低延迟的实时语音处理 -- **音频文件操作**: 支持音频文件的读写和处理 - -### 语音识别功能 -- **实时语音识别**: 持续监听并识别语音 -- **多语言支持**: 支持中文、英文、日文等多种语言 -- **语音活动检测**: 自动检测语音活动开始和结束 -- **静音检测**: 智能识别静音段并分割语音 -- **自定义词汇**: 支持添加专业词汇和术语 -- **识别模式**: 支持命令模式、听写模式和搜索模式 -- **标点符号识别**: 自动添加标点符号 -- **脏话过滤**: 可选的脏话过滤功能 -- **备选结果**: 支持返回多个备选识别结果 -- **单词时间戳**: 提供单词级别的精确时间戳 -- **说话人分离**: 识别不同说话人(需支持) -- **上下文偏向**: 根据上下文提高识别准确率 -- **增强模型**: 支持使用增强识别模型 - -### 文本转语音功能 -- **高质量合成**: 专业级语音合成质量 -- **多语言支持**: 支持多种语言的语音合成 -- **多语音选择**: 提供多种男声、女声和中性声音 -- **情感语音**: 支持高兴、悲伤、愤怒等情感语音 -- **语音定制**: 可调节语速、音调、音量等参数 -- **SSML支持**: 支持语音合成标记语言 -- **音素支持**: 支持音素级别的精确控制 -- **语音克隆**: 支持基于样本的语音克隆(需支持) -- **语音混合**: 支持多个语音的混合使用 -- **语音转换**: 支持语音风格转换 -- **神经网络语音**: 支持使用神经网络生成的高质量语音 -- **语音个性**: 支持友好、专业、休闲等语音个性 -- **自定义词典**: 支持添加自定义发音词典 - -### 音频处理功能 -- **音频格式支持**: WAV、MP3等常见音频格式 -- **采样率转换**: 支持不同采样率之间的转换 -- **音频效果**: 20+种专业音频效果 - - 混响(Reverb) - - 合唱(Chorus) - - 镶边(Flanger) - - 失真(Distortion) - - 压缩器(Compressor) - - 限制器(Limiter) - - 扩展器(Expander) - - 噪声门(Noise Gate) - - 去齿音器(De-esser) - - 均衡器(EQ) - - 颤音(Vibrato) - - 音高移位(Pitch Shift) - - 时间拉伸(Time Stretch) -- **音频分析**: 音频级别检测、静音检测等 -- **音频编辑**: 淡入淡出、音频拼接、分割等 -- **滤波器**: 高通、低通等滤波器 -- **动态处理**: 压缩、限制、扩展等动态范围处理 - -### 性能优化 -- **低延迟处理**: 优化的实时处理算法 -- **内存管理**: 智能的内存使用和垃圾回收 -- **多线程支持**: 支持后台处理,不影响主线程 -- **资源缓存**: 缓存常用资源,提高处理速度 -- **批量处理**: 支持批量音频处理任务 - -## 使用方法 - -### 快捷键操作 -- **F1**: 切换语音识别开关 -- **F2**: 开始/停止语音监听 -- **F3**: 测试文本转语音 -- **F4**: 停止当前语音播放 -- **F5**: 增加语音音量 -- **F6**: 降低语音音量 -- **F7**: 提高语音语速 -- **F8**: 降低语音语速 -- **F9**: 显示语音处理统计信息 -- **F10**: 切换语音情感 -- **F11**: 显示详细统计信息 -- **F12**: 重置语音系统 - -### GUI界面操作 -插件会在屏幕右上角创建控制面板,可以通过按钮直接操作语音识别和合成功能。 - -### 编程接口 - -```python -# 获取插件实例 -speech_plugin = world.plugin_manager.get_plugin("speech_recognition_synthesis") - -# 开始语音监听 -speech_plugin.start_listening() - -# 停止语音监听 -speech_plugin.stop_listening() - -# 合成语音 -speech_plugin.synthesize_speech("你好,世界!") - -# 设置语音识别回调 -def on_speech_recognized(text): - print(f"识别到语音: {text}") - -speech_plugin.set_speech_recognized_callback(on_speech_recognized) - -# 设置语音合成回调 -def on_speech_synthesized(audio_data, text): - print(f"合成语音: {text}") - -speech_plugin.set_speech_synthesis_callback(on_speech_synthesized) - -# 设置语言 -speech_plugin.set_language('zh-CN') # 中文 -speech_plugin.set_language('en-US') # 英文 - -# 设置语音 -speech_plugin.set_voice('male1') -speech_plugin.set_voice('female2') - -# 设置音量、语速、音调 -speech_plugin.set_volume(0.8) # 80%音量 -speech_plugin.set_speed(1.2) # 1.2倍语速 -speech_plugin.set_pitch(1.1) # 1.1倍音调 - -# 获取支持的语言和语音 -languages = speech_plugin.get_available_languages() -voices = speech_plugin.get_available_voices() - -# 检查语音状态 -is_speaking = speech_plugin.is_speaking() -is_listening = speech_plugin.is_listening - -# 停止语音播放 -speech_plugin.stop_speaking() - -# 获取统计信息 -stats = speech_plugin.get_stats() -``` - -## 核心组件 - -### 语音管理器 (SpeechManager) -负责管理语音识别和合成的核心功能,包括音频设备管理、音频数据处理等。 - -主要功能: -- 音频设备管理 -- 音频数据缓冲和处理 -- 录音和播放控制 -- 音频预处理和后处理 -- 音频级别检测和静音检测 - -### 语音识别器 (SpeechRecognizer) -负责将语音转换为文本,支持多种语言和识别引擎。 - -主要功能: -- 实时语音监听 -- 语音活动检测 -- 语音识别处理 -- 静音段检测和分割 -- 多语言支持 -- 自定义词汇管理 - -### 语音合成器 (SpeechSynthesizer) -负责将文本转换为语音,支持多种语音和语言。 - -主要功能: -- 文本到语音合成 -- 多语音支持 -- 参数调节(语速、音调、音量) -- 音频效果处理 -- SSML标记支持 -- 音频播放控制 - -### 音频工具类 (AudioUtils) -提供音频处理和文件操作功能。 - -主要功能: -- 音频文件读写(WAV格式) -- 音频格式转换 -- 采样率转换 -- 音频效果处理 -- 音频分析和编辑 -- 音频信息获取 - -## 支持的语言 - -### 语音识别支持的语言 -- 简体中文 (zh-CN) -- 繁体中文 (zh-TW) -- 英语(美国) (en-US) -- 英语(英国) (en-GB) -- 日语 (ja-JP) -- 韩语 (ko-KR) -- 法语 (fr-FR) -- 德语 (de-DE) -- 西班牙语 (es-ES) -- 俄语 (ru-RU) - -### 语音合成支持的语言 -- 简体中文 (zh-CN) -- 繁体中文 (zh-TW) -- 英语(美国) (en-US) -- 英语(英国) (en-GB) -- 日语 (ja-JP) -- 韩语 (ko-KR) -- 法语 (fr-FR) -- 德语 (de-DE) -- 西班牙语 (es-ES) -- 俄语 (ru-RU) - -## 支持的语音 - -### 男声 -- male1: 标准男声 -- male2: 低沉男声 -- male3: 明亮男声 - -### 女声 -- female1: 标准女声 -- female2: 温柔女声 -- female3: 明亮女声 - -### 中性声音 -- neutral1: 中性声音 -- neutral2: 机器人声音 - -### 情感语音 -- happy: 高兴 -- sad: 悲伤 -- angry: 愤怒 -- excited: 兴奋 - -## 音频效果 - -插件提供丰富的音频效果处理功能: - -### 时间域效果 -- **混响 (Reverb)**: 模拟不同空间环境的混响效果 -- **合唱 (Chorus)**: 增加声音的厚度和立体感 -- **镶边 (Flanger)**: 创建独特的扫频效果 -- **颤音 (Vibrato)**: 音高周期性变化效果 -- **失真 (Distortion)**: 添加过载和失真效果 - -### 动态范围处理 -- **压缩器 (Compressor)**: 控制音频动态范围 -- **限制器 (Limiter)**: 防止音频削波 -- **扩展器 (Expander)**: 扩展动态范围 -- **噪声门 (Noise Gate)**: 过滤背景噪声 -- **去齿音器 (De-esser)**: 减少语音中的刺耳音 - -### 滤波器效果 -- **高通滤波器**: 过滤低频噪声 -- **低通滤波器**: 过滤高频噪声 -- **均衡器**: 多频段频率调节 - -### 音高和时间处理 -- **音高移位**: 改变音频音高而不改变时长 -- **时间拉伸**: 改变音频时长而不改变音高 - -## 性能调优 - -### 实时处理优化 -1. **缓冲区管理**: 使用高效的音频缓冲区管理机制 -2. **多线程处理**: 关键处理任务在后台线程执行 -3. **内存优化**: 智能的内存分配和回收 -4. **算法优化**: 使用优化的数字信号处理算法 - -### 资源管理 -1. **智能预加载**: 根据使用情况预加载语音资源 -2. **动态卸载**: 自动卸载长时间未使用的资源 -3. **缓存机制**: 缓存常用处理结果和音频数据 - -### 质量与性能平衡 -1. **可调节质量**: 支持调节处理质量等级 -2. **自适应处理**: 根据系统负载动态调整处理复杂度 -3. **批量处理**: 支持批量处理任务以提高效率 - -## 扩展开发 - -### 添加新的语言支持 -1. 在语音识别器和合成器中添加新的语言代码 -2. 集成相应的语言识别和合成模型 -3. 更新语言列表和相关配置 - -### 添加新的语音 -1. 在语音合成器中注册新的语音名称 -2. 集成相应的语音合成模型或样本 -3. 实现语音参数配置 - -### 添加新的音频效果 -1. 在音频工具类中实现新的效果算法 -2. 添加效果参数验证和处理 -3. 实现效果的应用接口 - -### 添加新的识别模式 -1. 在语音识别器中实现新的识别模式 -2. 调整语音活动检测参数 -3. 优化特定模式下的处理流程 - -## 故障排除 - -### 语音识别问题 -1. **无法识别语音**: 检查麦克风是否正常工作 -2. **识别准确率低**: 检查语言设置是否正确,添加自定义词汇 -3. **识别延迟高**: 检查系统资源使用情况,降低处理质量设置 -4. **无法开始监听**: 检查插件是否已正确启用 - -### 语音合成问题 -1. **无法播放语音**: 检查扬声器或耳机是否正常工作 -2. **语音质量差**: 检查采样率设置,更新语音合成模型 -3. **合成速度慢**: 检查系统资源使用情况,使用较低质量设置 -4. **没有声音输出**: 检查音量设置,确认音频设备正常 - -### 音频处理问题 -1. **音频效果不明显**: 检查效果参数设置 -2. **音频失真**: 检查增益设置,避免削波 -3. **处理延迟高**: 优化处理算法,使用更高效的数据结构 -4. **内存占用高**: 检查缓冲区大小,及时释放不用资源 - -### 性能问题 -1. **CPU使用率高**: 使用性能分析工具定位瓶颈 -2. **内存泄漏**: 检查资源释放,使用内存分析工具 -3. **响应缓慢**: 优化算法,减少不必要的计算 - -## API参考 - -### Plugin类主要方法 - -#### 初始化和控制 -- `initialize()` - 初始化插件 -- `enable()` - 启用插件 -- `disable()` - 禁用插件 -- `finalize()` - 清理插件资源 -- `update(dt)` - 更新插件状态 - -#### 语音识别控制 -- `start_listening()` - 开始语音监听 -- `stop_listening()` - 停止语音监听 -- `set_language(language)` - 设置识别语言 -- `get_available_languages()` - 获取支持的语言列表 - -#### 语音合成控制 -- `synthesize_speech(text, callback)` - 合成语音 -- `stop_speaking()` - 停止语音播放 -- `set_voice(voice)` - 设置语音 -- `set_volume(volume)` - 设置音量 -- `set_speed(speed)` - 设置语速 -- `set_pitch(pitch)` - 设置音调 -- `get_available_voices()` - 获取支持的语音列表 -- `is_speaking()` - 检查是否正在播放语音 - -#### 回调函数设置 -- `set_speech_recognized_callback(callback)` - 设置识别回调 -- `set_speech_synthesis_callback(callback)` - 设置合成回调 -- `set_error_callback(callback)` - 设置错误回调 - -#### 统计和信息 -- `get_stats()` - 获取插件统计信息 - -### 核心组件类 - -#### SpeechManager -- `start_recording()` - 开始录音 -- `stop_recording()` - 停止录音 -- `start_playback(audio_data)` - 开始播放 -- `stop_playback()` - 停止播放 -- `set_sample_rate(sample_rate)` - 设置采样率 -- `get_audio_chunk()` - 获取音频数据块 - -#### SpeechRecognizer -- `start_listening()` - 开始监听 -- `stop_listening()` - 停止监听 -- `set_language(language)` - 设置语言 -- `set_silence_threshold(threshold)` - 设置静音阈值 -- `get_available_languages()` - 获取支持的语言 - -#### SpeechSynthesizer -- `synthesize(text, callback)` - 合成语音 -- `stop_speaking()` - 停止播放 -- `set_voice(voice)` - 设置语音 -- `set_volume(volume)` - 设置音量 -- `set_speed(speed)` - 设置语速 -- `set_pitch(pitch)` - 设置音调 -- `get_available_voices()` - 获取支持的语音 - -#### AudioUtils -- `save_wav_file(filename, audio_data, sample_rate)` - 保存WAV文件 -- `load_wav_file(filename)` - 加载WAV文件 -- `apply_reverb(audio_data, room_size, damping, wet_level)` - 应用混响 -- `apply_chorus(audio_data, rate, depth, mix)` - 应用合唱 -- `resample_audio(audio_data, from_rate, to_rate)` - 重采样音频 -- `normalize_audio(audio_data)` - 归一化音频 - -## 音频格式支持 - -支持的音频格式: -- **WAV**: 未压缩的PCM音频 -- **MP3**: MPEG音频层III(需要额外库支持) -- **PCM**: 原始脉冲编码调制数据 - -## 系统要求 - -### 硬件要求 -- **处理器**: 双核2.0GHz或更高 -- **内存**: 4GB RAM或更高 -- **音频设备**: 支持录音和播放的音频设备 -- **存储空间**: 100MB可用空间用于安装 - -### 软件要求 -- **操作系统**: Windows 7/8/10, macOS 10.12+, Ubuntu 18.04+ -- **Python**: 3.7或更高版本 -- **依赖库**: numpy, scipy等科学计算库 - -## 版本信息 - -- 版本: 1.0.0 -- 作者: EG Team -- 发布日期: 2024年 - -## 许可证 - +# 语音识别和合成插件 + +一个功能完整的语音识别和文本转语音合成插件,为EG引擎提供实时语音处理功能,支持多种语言、语音和音频效果处理。 + +## 功能特性 + +### 核心功能 +- **语音识别**: 实时语音转文本功能 +- **文本转语音**: 高质量文本转语音合成 +- **音频处理**: 专业的音频效果处理 +- **多语言支持**: 支持多种语言的识别和合成 +- **多语音支持**: 提供多种语音选择 +- **实时处理**: 低延迟的实时语音处理 +- **音频文件操作**: 支持音频文件的读写和处理 + +### 语音识别功能 +- **实时语音识别**: 持续监听并识别语音 +- **多语言支持**: 支持中文、英文、日文等多种语言 +- **语音活动检测**: 自动检测语音活动开始和结束 +- **静音检测**: 智能识别静音段并分割语音 +- **自定义词汇**: 支持添加专业词汇和术语 +- **识别模式**: 支持命令模式、听写模式和搜索模式 +- **标点符号识别**: 自动添加标点符号 +- **脏话过滤**: 可选的脏话过滤功能 +- **备选结果**: 支持返回多个备选识别结果 +- **单词时间戳**: 提供单词级别的精确时间戳 +- **说话人分离**: 识别不同说话人(需支持) +- **上下文偏向**: 根据上下文提高识别准确率 +- **增强模型**: 支持使用增强识别模型 + +### 文本转语音功能 +- **高质量合成**: 专业级语音合成质量 +- **多语言支持**: 支持多种语言的语音合成 +- **多语音选择**: 提供多种男声、女声和中性声音 +- **情感语音**: 支持高兴、悲伤、愤怒等情感语音 +- **语音定制**: 可调节语速、音调、音量等参数 +- **SSML支持**: 支持语音合成标记语言 +- **音素支持**: 支持音素级别的精确控制 +- **语音克隆**: 支持基于样本的语音克隆(需支持) +- **语音混合**: 支持多个语音的混合使用 +- **语音转换**: 支持语音风格转换 +- **神经网络语音**: 支持使用神经网络生成的高质量语音 +- **语音个性**: 支持友好、专业、休闲等语音个性 +- **自定义词典**: 支持添加自定义发音词典 + +### 音频处理功能 +- **音频格式支持**: WAV、MP3等常见音频格式 +- **采样率转换**: 支持不同采样率之间的转换 +- **音频效果**: 20+种专业音频效果 + - 混响(Reverb) + - 合唱(Chorus) + - 镶边(Flanger) + - 失真(Distortion) + - 压缩器(Compressor) + - 限制器(Limiter) + - 扩展器(Expander) + - 噪声门(Noise Gate) + - 去齿音器(De-esser) + - 均衡器(EQ) + - 颤音(Vibrato) + - 音高移位(Pitch Shift) + - 时间拉伸(Time Stretch) +- **音频分析**: 音频级别检测、静音检测等 +- **音频编辑**: 淡入淡出、音频拼接、分割等 +- **滤波器**: 高通、低通等滤波器 +- **动态处理**: 压缩、限制、扩展等动态范围处理 + +### 性能优化 +- **低延迟处理**: 优化的实时处理算法 +- **内存管理**: 智能的内存使用和垃圾回收 +- **多线程支持**: 支持后台处理,不影响主线程 +- **资源缓存**: 缓存常用资源,提高处理速度 +- **批量处理**: 支持批量音频处理任务 + +## 使用方法 + +### 快捷键操作 +- **F1**: 切换语音识别开关 +- **F2**: 开始/停止语音监听 +- **F3**: 测试文本转语音 +- **F4**: 停止当前语音播放 +- **F5**: 增加语音音量 +- **F6**: 降低语音音量 +- **F7**: 提高语音语速 +- **F8**: 降低语音语速 +- **F9**: 显示语音处理统计信息 +- **F10**: 切换语音情感 +- **F11**: 显示详细统计信息 +- **F12**: 重置语音系统 + +### GUI界面操作 +插件会在屏幕右上角创建控制面板,可以通过按钮直接操作语音识别和合成功能。 + +### 编程接口 + +```python +# 获取插件实例 +speech_plugin = world.plugin_manager.get_plugin("speech_recognition_synthesis") + +# 开始语音监听 +speech_plugin.start_listening() + +# 停止语音监听 +speech_plugin.stop_listening() + +# 合成语音 +speech_plugin.synthesize_speech("你好,世界!") + +# 设置语音识别回调 +def on_speech_recognized(text): + print(f"识别到语音: {text}") + +speech_plugin.set_speech_recognized_callback(on_speech_recognized) + +# 设置语音合成回调 +def on_speech_synthesized(audio_data, text): + print(f"合成语音: {text}") + +speech_plugin.set_speech_synthesis_callback(on_speech_synthesized) + +# 设置语言 +speech_plugin.set_language('zh-CN') # 中文 +speech_plugin.set_language('en-US') # 英文 + +# 设置语音 +speech_plugin.set_voice('male1') +speech_plugin.set_voice('female2') + +# 设置音量、语速、音调 +speech_plugin.set_volume(0.8) # 80%音量 +speech_plugin.set_speed(1.2) # 1.2倍语速 +speech_plugin.set_pitch(1.1) # 1.1倍音调 + +# 获取支持的语言和语音 +languages = speech_plugin.get_available_languages() +voices = speech_plugin.get_available_voices() + +# 检查语音状态 +is_speaking = speech_plugin.is_speaking() +is_listening = speech_plugin.is_listening + +# 停止语音播放 +speech_plugin.stop_speaking() + +# 获取统计信息 +stats = speech_plugin.get_stats() +``` + +## 核心组件 + +### 语音管理器 (SpeechManager) +负责管理语音识别和合成的核心功能,包括音频设备管理、音频数据处理等。 + +主要功能: +- 音频设备管理 +- 音频数据缓冲和处理 +- 录音和播放控制 +- 音频预处理和后处理 +- 音频级别检测和静音检测 + +### 语音识别器 (SpeechRecognizer) +负责将语音转换为文本,支持多种语言和识别引擎。 + +主要功能: +- 实时语音监听 +- 语音活动检测 +- 语音识别处理 +- 静音段检测和分割 +- 多语言支持 +- 自定义词汇管理 + +### 语音合成器 (SpeechSynthesizer) +负责将文本转换为语音,支持多种语音和语言。 + +主要功能: +- 文本到语音合成 +- 多语音支持 +- 参数调节(语速、音调、音量) +- 音频效果处理 +- SSML标记支持 +- 音频播放控制 + +### 音频工具类 (AudioUtils) +提供音频处理和文件操作功能。 + +主要功能: +- 音频文件读写(WAV格式) +- 音频格式转换 +- 采样率转换 +- 音频效果处理 +- 音频分析和编辑 +- 音频信息获取 + +## 支持的语言 + +### 语音识别支持的语言 +- 简体中文 (zh-CN) +- 繁体中文 (zh-TW) +- 英语(美国) (en-US) +- 英语(英国) (en-GB) +- 日语 (ja-JP) +- 韩语 (ko-KR) +- 法语 (fr-FR) +- 德语 (de-DE) +- 西班牙语 (es-ES) +- 俄语 (ru-RU) + +### 语音合成支持的语言 +- 简体中文 (zh-CN) +- 繁体中文 (zh-TW) +- 英语(美国) (en-US) +- 英语(英国) (en-GB) +- 日语 (ja-JP) +- 韩语 (ko-KR) +- 法语 (fr-FR) +- 德语 (de-DE) +- 西班牙语 (es-ES) +- 俄语 (ru-RU) + +## 支持的语音 + +### 男声 +- male1: 标准男声 +- male2: 低沉男声 +- male3: 明亮男声 + +### 女声 +- female1: 标准女声 +- female2: 温柔女声 +- female3: 明亮女声 + +### 中性声音 +- neutral1: 中性声音 +- neutral2: 机器人声音 + +### 情感语音 +- happy: 高兴 +- sad: 悲伤 +- angry: 愤怒 +- excited: 兴奋 + +## 音频效果 + +插件提供丰富的音频效果处理功能: + +### 时间域效果 +- **混响 (Reverb)**: 模拟不同空间环境的混响效果 +- **合唱 (Chorus)**: 增加声音的厚度和立体感 +- **镶边 (Flanger)**: 创建独特的扫频效果 +- **颤音 (Vibrato)**: 音高周期性变化效果 +- **失真 (Distortion)**: 添加过载和失真效果 + +### 动态范围处理 +- **压缩器 (Compressor)**: 控制音频动态范围 +- **限制器 (Limiter)**: 防止音频削波 +- **扩展器 (Expander)**: 扩展动态范围 +- **噪声门 (Noise Gate)**: 过滤背景噪声 +- **去齿音器 (De-esser)**: 减少语音中的刺耳音 + +### 滤波器效果 +- **高通滤波器**: 过滤低频噪声 +- **低通滤波器**: 过滤高频噪声 +- **均衡器**: 多频段频率调节 + +### 音高和时间处理 +- **音高移位**: 改变音频音高而不改变时长 +- **时间拉伸**: 改变音频时长而不改变音高 + +## 性能调优 + +### 实时处理优化 +1. **缓冲区管理**: 使用高效的音频缓冲区管理机制 +2. **多线程处理**: 关键处理任务在后台线程执行 +3. **内存优化**: 智能的内存分配和回收 +4. **算法优化**: 使用优化的数字信号处理算法 + +### 资源管理 +1. **智能预加载**: 根据使用情况预加载语音资源 +2. **动态卸载**: 自动卸载长时间未使用的资源 +3. **缓存机制**: 缓存常用处理结果和音频数据 + +### 质量与性能平衡 +1. **可调节质量**: 支持调节处理质量等级 +2. **自适应处理**: 根据系统负载动态调整处理复杂度 +3. **批量处理**: 支持批量处理任务以提高效率 + +## 扩展开发 + +### 添加新的语言支持 +1. 在语音识别器和合成器中添加新的语言代码 +2. 集成相应的语言识别和合成模型 +3. 更新语言列表和相关配置 + +### 添加新的语音 +1. 在语音合成器中注册新的语音名称 +2. 集成相应的语音合成模型或样本 +3. 实现语音参数配置 + +### 添加新的音频效果 +1. 在音频工具类中实现新的效果算法 +2. 添加效果参数验证和处理 +3. 实现效果的应用接口 + +### 添加新的识别模式 +1. 在语音识别器中实现新的识别模式 +2. 调整语音活动检测参数 +3. 优化特定模式下的处理流程 + +## 故障排除 + +### 语音识别问题 +1. **无法识别语音**: 检查麦克风是否正常工作 +2. **识别准确率低**: 检查语言设置是否正确,添加自定义词汇 +3. **识别延迟高**: 检查系统资源使用情况,降低处理质量设置 +4. **无法开始监听**: 检查插件是否已正确启用 + +### 语音合成问题 +1. **无法播放语音**: 检查扬声器或耳机是否正常工作 +2. **语音质量差**: 检查采样率设置,更新语音合成模型 +3. **合成速度慢**: 检查系统资源使用情况,使用较低质量设置 +4. **没有声音输出**: 检查音量设置,确认音频设备正常 + +### 音频处理问题 +1. **音频效果不明显**: 检查效果参数设置 +2. **音频失真**: 检查增益设置,避免削波 +3. **处理延迟高**: 优化处理算法,使用更高效的数据结构 +4. **内存占用高**: 检查缓冲区大小,及时释放不用资源 + +### 性能问题 +1. **CPU使用率高**: 使用性能分析工具定位瓶颈 +2. **内存泄漏**: 检查资源释放,使用内存分析工具 +3. **响应缓慢**: 优化算法,减少不必要的计算 + +## API参考 + +### Plugin类主要方法 + +#### 初始化和控制 +- `initialize()` - 初始化插件 +- `enable()` - 启用插件 +- `disable()` - 禁用插件 +- `finalize()` - 清理插件资源 +- `update(dt)` - 更新插件状态 + +#### 语音识别控制 +- `start_listening()` - 开始语音监听 +- `stop_listening()` - 停止语音监听 +- `set_language(language)` - 设置识别语言 +- `get_available_languages()` - 获取支持的语言列表 + +#### 语音合成控制 +- `synthesize_speech(text, callback)` - 合成语音 +- `stop_speaking()` - 停止语音播放 +- `set_voice(voice)` - 设置语音 +- `set_volume(volume)` - 设置音量 +- `set_speed(speed)` - 设置语速 +- `set_pitch(pitch)` - 设置音调 +- `get_available_voices()` - 获取支持的语音列表 +- `is_speaking()` - 检查是否正在播放语音 + +#### 回调函数设置 +- `set_speech_recognized_callback(callback)` - 设置识别回调 +- `set_speech_synthesis_callback(callback)` - 设置合成回调 +- `set_error_callback(callback)` - 设置错误回调 + +#### 统计和信息 +- `get_stats()` - 获取插件统计信息 + +### 核心组件类 + +#### SpeechManager +- `start_recording()` - 开始录音 +- `stop_recording()` - 停止录音 +- `start_playback(audio_data)` - 开始播放 +- `stop_playback()` - 停止播放 +- `set_sample_rate(sample_rate)` - 设置采样率 +- `get_audio_chunk()` - 获取音频数据块 + +#### SpeechRecognizer +- `start_listening()` - 开始监听 +- `stop_listening()` - 停止监听 +- `set_language(language)` - 设置语言 +- `set_silence_threshold(threshold)` - 设置静音阈值 +- `get_available_languages()` - 获取支持的语言 + +#### SpeechSynthesizer +- `synthesize(text, callback)` - 合成语音 +- `stop_speaking()` - 停止播放 +- `set_voice(voice)` - 设置语音 +- `set_volume(volume)` - 设置音量 +- `set_speed(speed)` - 设置语速 +- `set_pitch(pitch)` - 设置音调 +- `get_available_voices()` - 获取支持的语音 + +#### AudioUtils +- `save_wav_file(filename, audio_data, sample_rate)` - 保存WAV文件 +- `load_wav_file(filename)` - 加载WAV文件 +- `apply_reverb(audio_data, room_size, damping, wet_level)` - 应用混响 +- `apply_chorus(audio_data, rate, depth, mix)` - 应用合唱 +- `resample_audio(audio_data, from_rate, to_rate)` - 重采样音频 +- `normalize_audio(audio_data)` - 归一化音频 + +## 音频格式支持 + +支持的音频格式: +- **WAV**: 未压缩的PCM音频 +- **MP3**: MPEG音频层III(需要额外库支持) +- **PCM**: 原始脉冲编码调制数据 + +## 系统要求 + +### 硬件要求 +- **处理器**: 双核2.0GHz或更高 +- **内存**: 4GB RAM或更高 +- **音频设备**: 支持录音和播放的音频设备 +- **存储空间**: 100MB可用空间用于安装 + +### 软件要求 +- **操作系统**: Windows 7/8/10, macOS 10.12+, Ubuntu 18.04+ +- **Python**: 3.7或更高版本 +- **依赖库**: numpy, scipy等科学计算库 + +## 版本信息 + +- 版本: 1.0.0 +- 作者: EG Team +- 发布日期: 2024年 + +## 许可证 + 本插件遵循EG引擎许可证协议。 \ No newline at end of file diff --git a/plugins/user/speech_recognition_synthesis/core/speech_manager.py b/plugins/user/speech_recognition_synthesis/core/speech_manager.py index 133ec36f..865911d6 100644 --- a/plugins/user/speech_recognition_synthesis/core/speech_manager.py +++ b/plugins/user/speech_recognition_synthesis/core/speech_manager.py @@ -1,1542 +1,1542 @@ -""" -语音管理器 -负责管理语音识别和合成的核心功能 -""" - -import time -import threading -import numpy as np -from typing import Dict, Any, List -from queue import Queue, Empty -import pyaudio -import wave -import struct - -class SpeechManager: - """ - 语音管理器 - 负责管理语音识别和合成的核心功能,包括音频设备管理、音频数据处理等 - """ - - def __init__(self, plugin): - """ - 初始化语音管理器 - - Args: - plugin: 语音识别和合成插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 音频配置 - self.sample_rate = plugin.config.get('sample_rate', 16000) - self.channels = plugin.config.get('channels', 1) - self.buffer_size = plugin.config.get('buffer_size', 1024) - self.format = pyaudio.paInt16 # 16位音频格式 - - # 音频设备 - self.pyaudio_instance = None - self.input_stream = None - self.output_stream = None - - # 音频缓冲区 - self.audio_buffer = Queue(maxsize=1000) # 音频数据缓冲区 - self.processed_buffer = Queue(maxsize=1000) # 处理后数据缓冲区 - - # 音频处理状态 - self.is_recording = False - self.is_playing = False - self.recording_thread = None - self.playback_thread = None - - # 统计信息 - self.stats = { - 'audio_buffers_processed': 0, - 'audio_buffers_dropped': 0, - 'recording_time': 0.0, - 'playback_time': 0.0, - 'peak_audio_level': 0.0, - 'total_samples_recorded': 0, - 'total_samples_played': 0 - } - - # 音频处理参数 - self.audio_params = { - 'noise_threshold': 0.01, - 'silence_threshold': 0.005, - 'silence_duration': 1.0, - 'min_voice_duration': 0.5, - 'pre_emphasis': 0.97, # 预加重系数 - 'frame_length': 0.025, # 帧长度(25ms) - 'frame_stride': 0.01, # 帧步长(10ms) - } - - # 音频处理缓冲区 - self.voice_buffer = [] # 语音片段缓冲区 - self.silence_counter = 0 - self.voice_activity_counter = 0 - self.is_voice_detected = False - - print("✓ 语音管理器已创建") - - def initialize(self) -> bool: - """ - 初始化语音管理器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化PyAudio - self.pyaudio_instance = pyaudio.PyAudio() - - # 验证音频设备 - input_device_count = self.pyaudio_instance.get_device_count() - if input_device_count == 0: - print("⚠ 未检测到音频输入设备") - - print(f"✓ 检测到 {input_device_count} 个音频设备") - - # 初始化音频设备 - print("✓ 音频设备初始化完成") - - self.initialized = True - print("✓ 语音管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 语音管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用语音管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 语音管理器未初始化") - return False - - self.enabled = True - print("✓ 语音管理器已启用") - return True - - except Exception as e: - print(f"✗ 语音管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用语音管理器""" - try: - self.enabled = False - self.stop_recording() - self.stop_playback() - print("✓ 语音管理器已禁用") - - except Exception as e: - print(f"✗ 语音管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理语音管理器资源""" - try: - self.disable() - - # 关闭音频流 - if self.input_stream: - self.input_stream.stop_stream() - self.input_stream.close() - self.input_stream = None - - if self.output_stream: - self.output_stream.stop_stream() - self.output_stream.close() - self.output_stream = None - - # 终止PyAudio - if self.pyaudio_instance: - self.pyaudio_instance.terminate() - self.pyaudio_instance = None - - self.initialized = False - print("✓ 语音管理器资源已清理") - - except Exception as e: - print(f"✗ 语音管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新语音管理器状态 - - Args: - dt: 时间增量 - """ - # 这里可以处理定期更新的任务 - pass - - def start_recording(self) -> bool: - """ - 开始录音 - - Returns: - 是否开始成功 - """ - try: - if not self.enabled: - print("✗ 语音管理器未启用") - return False - - if self.is_recording: - print("⚠ 已经在录音中") - return True - - # 打开音频输入流 - self.input_stream = self.pyaudio_instance.open( - format=self.format, - channels=self.channels, - rate=self.sample_rate, - input=True, - frames_per_buffer=self.buffer_size - ) - - # 启动录音线程 - self.is_recording = True - self.recording_thread = threading.Thread(target=self._recording_worker, daemon=True) - self.recording_thread.start() - - print("✓ 开始录音") - return True - - except Exception as e: - print(f"✗ 开始录音失败: {e}") - import traceback - traceback.print_exc() - return False - - def stop_recording(self): - """停止录音""" - try: - self.is_recording = False - - # 关闭音频输入流 - if self.input_stream: - self.input_stream.stop_stream() - self.input_stream.close() - self.input_stream = None - - if self.recording_thread and self.recording_thread.is_alive(): - self.recording_thread.join(timeout=1.0) - print("✓ 停止录音") - - except Exception as e: - print(f"✗ 停止录音失败: {e}") - import traceback - traceback.print_exc() - - def start_playback(self, audio_data: bytes) -> bool: - """ - 开始播放音频 - - Args: - audio_data: 音频数据 - - Returns: - 是否开始成功 - """ - try: - if not self.enabled: - print("✗ 语音管理器未启用") - return False - - if self.is_playing: - print("⚠ 已经在播放中") - return True - - # 打开音频输出流 - self.output_stream = self.pyaudio_instance.open( - format=self.format, - channels=self.channels, - rate=self.sample_rate, - output=True, - frames_per_buffer=self.buffer_size - ) - - # 启动播放线程 - self.is_playing = True - self.playback_thread = threading.Thread( - target=self._playback_worker, - args=(audio_data,), - daemon=True - ) - self.playback_thread.start() - - print("✓ 开始播放音频") - return True - - except Exception as e: - print(f"✗ 开始播放音频失败: {e}") - import traceback - traceback.print_exc() - return False - - def stop_playback(self): - """停止播放音频""" - try: - self.is_playing = False - - # 关闭音频输出流 - if self.output_stream: - self.output_stream.stop_stream() - self.output_stream.close() - self.output_stream = None - - if self.playback_thread and self.playback_thread.is_alive(): - self.playback_thread.join(timeout=1.0) - print("✓ 停止播放音频") - - except Exception as e: - print(f"✗ 停止播放音频失败: {e}") - import traceback - traceback.print_exc() - - def _recording_worker(self): - """录音工作线程""" - try: - recording_start_time = time.time() - frames_recorded = 0 - - # 录音循环 - while self.is_recording and self.enabled: - try: - # 从音频输入流读取数据 - audio_data = self.input_stream.read(self.buffer_size, exception_on_overflow=False) - - # 转换为numpy数组进行处理 - audio_array = np.frombuffer(audio_data, dtype=np.int16) - audio_float = audio_array.astype(np.float32) / 32768.0 - - # 计算音频级别 - audio_level = np.mean(np.abs(audio_float)) if len(audio_float) > 0 else 0.0 - self.stats['peak_audio_level'] = max(self.stats['peak_audio_level'], audio_level) - - # 应用预处理 - processed_audio = self._preprocess_audio(audio_float) - - # 将音频数据放入缓冲区 - try: - self.audio_buffer.put(processed_audio, block=False) - frames_recorded += len(audio_float) - except: - self.stats['audio_buffers_dropped'] += 1 - - # 更新统计信息 - self.stats['total_samples_recorded'] += len(audio_float) - - except Exception as e: - print(f"⚠ 录音过程中出现错误: {e}") - continue - - self.stats['recording_time'] += time.time() - recording_start_time - self.stats['total_samples_recorded'] = frames_recorded - - except Exception as e: - print(f"✗ 录音工作线程错误: {e}") - import traceback - traceback.print_exc() - - def _playback_worker(self, audio_data: bytes): - """播放工作线程""" - try: - playback_start_time = time.time() - bytes_played = 0 - - # 将音频数据分块播放 - chunk_size = self.buffer_size * 2 # 16位音频,每个样本2字节 - for i in range(0, len(audio_data), chunk_size): - if not self.is_playing: - break - - chunk = audio_data[i:i + chunk_size] - - try: - # 写入音频输出流 - self.output_stream.write(chunk) - bytes_played += len(chunk) - except Exception as e: - print(f"⚠ 播放过程中出现错误: {e}") - continue - - self.stats['playback_time'] += time.time() - playback_start_time - self.stats['total_samples_played'] = bytes_played // 2 # 16位音频 - self.is_playing = False - - except Exception as e: - print(f"✗ 播放工作线程错误: {e}") - import traceback - traceback.print_exc() - self.is_playing = False - - def _preprocess_audio(self, audio_data: np.ndarray) -> np.ndarray: - """ - 预处理音频数据 - - Args: - audio_data: 原始音频数据 - - Returns: - 预处理后的音频数据 - """ - try: - if len(audio_data) == 0: - return audio_data - - # 预加重 (Pre-emphasis) - pre_emphasis = self.audio_params['pre_emphasis'] - if pre_emphasis > 0: - # 应用预加重滤波器: y[t] = x[t] - a * x[t-1] - emphasized = np.zeros_like(audio_data) - emphasized[0] = audio_data[0] - emphasized[1:] = audio_data[1:] - pre_emphasis * audio_data[:-1] - audio_data = emphasized - - # 归一化 - max_val = np.max(np.abs(audio_data)) - if max_val > 0: - audio_data = audio_data / max_val - - return audio_data - - except Exception as e: - print(f"✗ 音频预处理失败: {e}") - return audio_data - - def get_audio_chunk(self) -> np.ndarray: - """ - 获取音频数据块 - - Returns: - 音频数据块 - """ - try: - return self.audio_buffer.get(block=False) - except Empty: - return np.zeros(self.buffer_size, dtype=np.float32) - except Exception as e: - print(f"✗ 获取音频数据块失败: {e}") - return np.zeros(self.buffer_size, dtype=np.float32) - - def put_processed_audio(self, audio_data: np.ndarray): - """ - 放入处理后的音频数据 - - Args: - audio_data: 处理后的音频数据 - """ - try: - self.processed_buffer.put(audio_data, block=False) - self.stats['audio_buffers_processed'] += 1 - except: - self.stats['audio_buffers_dropped'] += 1 - - def get_processed_audio(self) -> np.ndarray: - """ - 获取处理后的音频数据 - - Returns: - 处理后的音频数据 - """ - try: - return self.processed_buffer.get(block=False) - except Empty: - return np.zeros(self.buffer_size, dtype=np.float32) - except Exception as e: - print(f"✗ 获取处理后音频数据失败: {e}") - return np.zeros(self.buffer_size, dtype=np.float32) - - def set_sample_rate(self, sample_rate: int): - """ - 设置采样率 - - Args: - sample_rate: 采样率 (Hz) - """ - old_sample_rate = self.sample_rate - self.sample_rate = sample_rate - print(f"✓ 采样率已从 {old_sample_rate} 更新为: {sample_rate} Hz") - - def set_buffer_size(self, buffer_size: int): - """ - 设置缓冲区大小 - - Args: - buffer_size: 缓冲区大小 - """ - self.buffer_size = buffer_size - print(f"✓ 缓冲区大小设置为: {buffer_size}") - - def set_noise_threshold(self, threshold: float): - """ - 设置噪声阈值 - - Args: - threshold: 噪声阈值 - """ - self.audio_params['noise_threshold'] = max(0.0, min(1.0, threshold)) - print(f"✓ 噪声阈值设置为: {threshold}") - - def set_silence_threshold(self, threshold: float): - """ - 设置静音阈值 - - Args: - threshold: 静音阈值 - """ - self.audio_params['silence_threshold'] = max(0.0, min(1.0, threshold)) - print(f"✓ 静音阈值设置为: {threshold}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def is_audio_silent(self, audio_data: np.ndarray) -> bool: - """ - 检查音频是否静音 - - Args: - audio_data: 音频数据 - - Returns: - 是否静音 - """ - try: - if len(audio_data) == 0: - return True - audio_level = np.mean(np.abs(audio_data)) - return audio_level < self.audio_params['silence_threshold'] - except Exception as e: - print(f"✗ 检查音频静音状态失败: {e}") - return True - - def is_audio_noisy(self, audio_data: np.ndarray) -> bool: - """ - 检查音频是否有噪声 - - Args: - audio_data: 音频数据 - - Returns: - 是否有噪声 - """ - try: - if len(audio_data) == 0: - return False - audio_level = np.mean(np.abs(audio_data)) - return audio_level > self.audio_params['noise_threshold'] - except Exception as e: - print(f"✗ 检查音频噪声状态失败: {e}") - return False - - def apply_preprocessing(self, audio_data: np.ndarray) -> np.ndarray: - """ - 应用音频预处理 - - Args: - audio_data: 原始音频数据 - - Returns: - 预处理后的音频数据 - """ - try: - # 预加重 - pre_emphasis = self.audio_params['pre_emphasis'] - if pre_emphasis > 0 and len(audio_data) > 1: - emphasized = np.zeros_like(audio_data) - emphasized[0] = audio_data[0] - emphasized[1:] = audio_data[1:] - pre_emphasis * audio_data[:-1] - audio_data = emphasized - - # 归一化 - max_val = np.max(np.abs(audio_data)) - if max_val > 0: - audio_data = audio_data / max_val - - return audio_data - except Exception as e: - print(f"✗ 音频预处理失败: {e}") - return audio_data - - def apply_postprocessing(self, audio_data: np.ndarray) -> np.ndarray: - """ - 应用音频后处理 - - Args: - audio_data: 处理后的音频数据 - - Returns: - 后处理后的音频数据 - """ - try: - # 限幅处理防止削波 - audio_data = np.clip(audio_data, -0.99, 0.99) - - # 转换为16位整数格式 - audio_int16 = (audio_data * 32767).astype(np.int16) - - return audio_int16 - except Exception as e: - print(f"✗ 音频后处理失败: {e}") - # 返回原始数据的16位版本 - try: - return (audio_data * 32767).astype(np.int16) - except: - return np.zeros(len(audio_data), dtype=np.int16) - - def detect_voice_activity(self, audio_data: np.ndarray) -> bool: - """ - 检测语音活动 - - Args: - audio_data: 音频数据 - - Returns: - 是否检测到语音活动 - """ - try: - # 检查音频级别是否超过噪声阈值 - if len(audio_data) == 0: - return False - - audio_level = np.mean(np.abs(audio_data)) - return audio_level > self.audio_params['noise_threshold'] - except Exception as e: - print(f"✗ 语音活动检测失败: {e}") - return False - - def calculate_audio_level(self, audio_data: np.ndarray) -> float: - """ - 计算音频级别 - - Args: - audio_data: 音频数据 - - Returns: - 音频级别 (0.0-1.0) - """ - try: - if len(audio_data) == 0: - return 0.0 - return float(np.mean(np.abs(audio_data))) - except Exception as e: - print(f"✗ 计算音频级别失败: {e}") - return 0.0 - - def convert_sample_rate(self, audio_data: np.ndarray, from_rate: int, to_rate: int) -> np.ndarray: - """ - 转换音频采样率 - - Args: - audio_data: 音频数据 - from_rate: 原始采样率 - to_rate: 目标采样率 - - Returns: - 转换后的音频数据 - """ - try: - if from_rate == to_rate: - return audio_data - - # 计算采样率转换比例 - ratio = to_rate / from_rate - new_length = int(len(audio_data) * ratio) - - if new_length <= 0: - return np.array([], dtype=np.float32) - - # 使用线性插值进行重采样 - indices = np.linspace(0, len(audio_data) - 1, new_length) - resampled_data = np.interp(indices, np.arange(len(audio_data)), audio_data) - - return resampled_data.astype(np.float32) - except Exception as e: - print(f"✗ 采样率转换失败: {e}") - return audio_data - - def apply_gain(self, audio_data: np.ndarray, gain: float) -> np.ndarray: - """ - 应用增益到音频数据 - - Args: - audio_data: 音频数据 - gain: 增益值 - - Returns: - 应用增益后的音频数据 - """ - try: - amplified_data = audio_data * gain - # 防止削波 - return np.clip(amplified_data, -1.0, 1.0) - except Exception as e: - print(f"✗ 应用增益失败: {e}") - return audio_data - - def mix_audio(self, audio1: np.ndarray, audio2: np.ndarray) -> np.ndarray: - """ - 混合两个音频信号 - - Args: - audio1: 第一个音频信号 - audio2: 第二个音频信号 - - Returns: - 混合后的音频信号 - """ - try: - # 确保两个音频信号长度相同 - max_length = max(len(audio1), len(audio2)) - if len(audio1) < max_length: - audio1 = np.pad(audio1, (0, max_length - len(audio1))) - if len(audio2) < max_length: - audio2 = np.pad(audio2, (0, max_length - len(audio2))) - - # 混合音频(简单平均) - mixed_audio = (audio1 + audio2) / 2.0 - - # 防止削波 - return np.clip(mixed_audio, -1.0, 1.0) - except Exception as e: - print(f"✗ 音频混合失败: {e}") - # 返回较长的音频信号 - return audio1 if len(audio1) >= len(audio2) else audio2 - - def apply_window_function(self, audio_data: np.ndarray, window_type: str = 'hann') -> np.ndarray: - """ - 应用窗函数到音频数据 - - Args: - audio_data: 音频数据 - window_type: 窗函数类型 ('hann', 'hamming', 'blackman') - - Returns: - 应用窗函数后的音频数据 - """ - try: - if len(audio_data) == 0: - return audio_data - - # 根据类型生成窗函数 - if window_type == 'hann': - window = np.hanning(len(audio_data)) - elif window_type == 'hamming': - window = np.hamming(len(audio_data)) - elif window_type == 'blackman': - window = np.blackman(len(audio_data)) - else: - # 默认使用汉宁窗 - window = np.hanning(len(audio_data)) - - # 应用窗函数 - windowed_data = audio_data * window - - return windowed_data - except Exception as e: - print(f"✗ 应用窗函数失败: {e}") - return audio_data - - def calculate_spectral_features(self, audio_data: np.ndarray) -> Dict[str, Any]: - """ - 计算音频的频谱特征 - - Args: - audio_data: 音频数据 - - Returns: - 频谱特征字典 - """ - try: - if len(audio_data) == 0: - return {} - - # 计算FFT - fft_data = np.fft.fft(audio_data) - magnitude = np.abs(fft_data) - - # 计算基本频谱特征 - spectral_centroid = np.sum(magnitude * np.arange(len(magnitude))) / np.sum(magnitude) - spectral_rolloff = np.sum(magnitude[:int(len(magnitude) * 0.85)]) - spectral_flux = np.sum(np.diff(magnitude) ** 2) - zero_crossing_rate = np.sum(np.diff(np.sign(audio_data)) != 0) / len(audio_data) - - return { - 'spectral_centroid': spectral_centroid, - 'spectral_rolloff': spectral_rolloff, - 'spectral_flux': spectral_flux, - 'zero_crossing_rate': zero_crossing_rate, - 'rms_energy': np.sqrt(np.mean(audio_data ** 2)), - 'peak_amplitude': np.max(np.abs(audio_data)) - } - except Exception as e: - print(f"✗ 计算频谱特征失败: {e}") - return {} - - def apply_spectral_filter(self, audio_data: np.ndarray, filter_type: str, cutoff_freq: float) -> np.ndarray: - """ - 应用频谱滤波器 - - Args: - audio_data: 音频数据 - filter_type: 滤波器类型 ('lowpass', 'highpass', 'bandpass') - cutoff_freq: 截止频率 (Hz) - - Returns: - 滤波后的音频数据 - """ - try: - if len(audio_data) == 0: - return audio_data - - # 简化的频域滤波实现 - fft_data = np.fft.fft(audio_data) - freqs = np.fft.fftfreq(len(audio_data), 1/self.sample_rate) - - # 创建滤波器掩码 - if filter_type == 'lowpass': - mask = np.abs(freqs) <= cutoff_freq - elif filter_type == 'highpass': - mask = np.abs(freqs) >= cutoff_freq - elif filter_type == 'bandpass': - # 简化的带通滤波器 (以cutoff_freq为中心频率) - bandwidth = cutoff_freq * 0.2 # 20%带宽 - mask = (np.abs(freqs) >= (cutoff_freq - bandwidth)) & (np.abs(freqs) <= (cutoff_freq + bandwidth)) - else: - # 默认不滤波 - return audio_data - - # 应用滤波器 - filtered_fft = fft_data * mask - filtered_audio = np.real(np.fft.ifft(filtered_fft)) - - return filtered_audio - except Exception as e: - print(f"✗ 应用频谱滤波器失败: {e}") - return audio_data - - def segment_audio(self, audio_data: np.ndarray, segment_length: int) -> List[np.ndarray]: - """ - 将音频数据分割成段 - - Args: - audio_data: 音频数据 - segment_length: 每段长度 - - Returns: - 音频段列表 - """ - try: - segments = [] - for i in range(0, len(audio_data), segment_length): - segment = audio_data[i:i + segment_length] - segments.append(segment) - return segments - except Exception as e: - print(f"✗ 音频分割失败: {e}") - return [audio_data] - - def apply_dynamic_range_compression(self, audio_data: np.ndarray, threshold: float = -20.0, - ratio: float = 4.0, attack: float = 0.01, release: float = 0.1) -> np.ndarray: - """ - 应用动态范围压缩 - - Args: - audio_data: 音频数据 - threshold: 阈值 (dB) - ratio: 压缩比 - attack: 启动时间 (秒) - release: 释放时间 (秒) - - Returns: - 压缩后的音频数据 - """ - try: - if len(audio_data) == 0: - return audio_data - - # 转换阈值为线性值 - threshold_linear = 10 ** (threshold / 20.0) - - # 计算包络 - envelope = np.zeros_like(audio_data) - gain = np.ones_like(audio_data) - - # 简化的压缩器实现 - current_gain = 1.0 - for i in range(len(audio_data)): - # 计算瞬时幅度 - instant_amp = abs(audio_data[i]) - - # 更新包络 (简化实现) - if instant_amp > envelope[i-1] if i > 0 else 0: - # 启动阶段 - envelope[i] = instant_amp - else: - # 释放阶段 - envelope[i] = envelope[i-1] * 0.99 if i > 0 else instant_amp - - # 计算增益缩减 - if envelope[i] > threshold_linear: - gain_db = 20 * np.log10(envelope[i] / threshold_linear) - reduction_db = gain_db - gain_db / ratio - target_gain = 10 ** (-reduction_db / 20.0) - else: - target_gain = 1.0 - - # 应用启动和释放时间 - if target_gain < current_gain: - # 启动 - current_gain = current_gain + (target_gain - current_gain) * (1 - np.exp(-1.0 / (self.sample_rate * attack))) - else: - # 释放 - current_gain = current_gain + (target_gain - current_gain) * (1 - np.exp(-1.0 / (self.sample_rate * release))) - - gain[i] = current_gain - - # 应用增益 - compressed_audio = audio_data * gain - - return compressed_audio - except Exception as e: - print(f"✗ 动态范围压缩失败: {e}") - return audio_data - - def apply_noise_reduction(self, audio_data: np.ndarray, noise_factor: float = 0.05) -> np.ndarray: - """ - 应用噪声抑制 - - Args: - audio_data: 音频数据 - noise_factor: 噪声因子 (0.0-1.0) - - Returns: - 噪声抑制后的音频数据 - """ - try: - if len(audio_data) == 0: - return audio_data - - # 简化的噪声抑制实现 - # 使用谱减法的简化版本 - fft_data = np.fft.fft(audio_data) - magnitude = np.abs(fft_data) - phase = np.angle(fft_data) - - # 估计噪声水平 (使用前10%的数据) - noise_estimate = np.mean(magnitude[:max(1, len(magnitude) // 10)]) - - # 应用噪声抑制 - suppression = np.maximum(magnitude - noise_factor * noise_estimate, 0) - suppressed_fft = suppression * np.exp(1j * phase) - - # 逆变换 - suppressed_audio = np.real(np.fft.ifft(suppressed_fft)) - - return suppressed_audio - except Exception as e: - print(f"✗ 噪声抑制失败: {e}") - return audio_data - - def apply_echo_effect(self, audio_data: np.ndarray, delay: float = 0.25, decay: float = 0.5) -> np.ndarray: - """ - 应用回声效果 - - Args: - audio_data: 音频数据 - delay: 延迟时间 (秒) - decay: 衰减系数 (0.0-1.0) - - Returns: - 添加回声效果的音频数据 - """ - try: - if len(audio_data) == 0: - return audio_data - - # 计算延迟样本数 - delay_samples = int(delay * self.sample_rate) - - # 创建输出数组 - echo_audio = np.zeros(len(audio_data) + delay_samples) - echo_audio[:len(audio_data)] = audio_data - - # 应用回声 - for i in range(len(audio_data)): - if i + delay_samples < len(echo_audio): - echo_audio[i + delay_samples] += audio_data[i] * decay - - # 裁剪到原始长度 - return echo_audio[:len(audio_data)] - except Exception as e: - print(f"✗ 回声效果应用失败: {e}") - return audio_data - - def apply_vocal_enhancement(self, audio_data: np.ndarray) -> np.ndarray: - """ - 应用人声增强效果 - - Args: - audio_data: 音频数据 - - Returns: - 人声增强后的音频数据 - """ - try: - if len(audio_data) == 0: - return audio_data - - # 简化的人声增强实现 - # 1. 应用预加重 - pre_emphasized = self._preprocess_audio(audio_data) - - # 2. 应用中频提升滤波器 (简化实现) - # 人声主要集中在300Hz-3400Hz范围内 - mid_freq_boosted = self.apply_spectral_filter(pre_emphasized, 'bandpass', 1500) # 中心频率约1500Hz - - # 3. 应用动态范围压缩 - compressed = self.apply_dynamic_range_compression(mid_freq_boosted, threshold=-25.0, ratio=3.0) - - # 4. 混合原始信号和增强信号 - enhanced_audio = (pre_emphasized + compressed) / 2.0 - - return enhanced_audio - except Exception as e: - print(f"✗ 人声增强失败: {e}") - return audio_data - - def save_audio_to_wav(self, filename: str, audio_data: np.ndarray, sample_rate: int = None) -> bool: - """ - 保存音频数据为WAV文件 - - Args: - filename: 文件名 - audio_data: 音频数据 - sample_rate: 采样率 - - Returns: - 是否保存成功 - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - # 确保目录存在 - import os - os.makedirs(os.path.dirname(filename) if os.path.dirname(filename) else '.', exist_ok=True) - - # 转换为16位整数 - if audio_data.dtype != np.int16: - audio_int16 = np.clip(audio_data, -1.0, 1.0) - audio_int16 = (audio_int16 * 32767).astype(np.int16) - else: - audio_int16 = audio_data - - # 保存为WAV文件 - with wave.open(filename, 'w') as wav_file: - wav_file.setnchannels(self.channels) - wav_file.setsampwidth(2) # 16位 - wav_file.setframerate(sample_rate) - wav_file.writeframes(audio_int16.tobytes()) - - print(f"✓ 音频已保存为WAV文件: {filename}") - return True - except Exception as e: - print(f"✗ 保存WAV文件失败: {e}") - import traceback - traceback.print_exc() - return False - - def load_audio_from_wav(self, filename: str) -> np.ndarray: - """ - 从WAV文件加载音频数据 - - Args: - filename: 文件名 - - Returns: - 音频数据 - """ - try: - with wave.open(filename, 'r') as wav_file: - # 获取音频参数 - channels = wav_file.getnchannels() - sample_width = wav_file.getsampwidth() - frame_rate = wav_file.getframerate() - frames = wav_file.getnframes() - - # 读取音频数据 - raw_audio = wav_file.readframes(frames) - - # 转换为numpy数组 - if sample_width == 1: - # 8位音频 - audio_data = np.frombuffer(raw_audio, dtype=np.uint8) - audio_data = (audio_data.astype(np.float32) - 128) / 128.0 - elif sample_width == 2: - # 16位音频 - audio_data = np.frombuffer(raw_audio, dtype=np.int16) - audio_data = audio_data.astype(np.float32) / 32768.0 - elif sample_width == 4: - # 32位音频 - audio_data = np.frombuffer(raw_audio, dtype=np.int32) - audio_data = audio_data.astype(np.float32) / 2147483648.0 - else: - raise ValueError(f"不支持的采样宽度: {sample_width}") - - # 如果采样率不同,进行重采样 - if frame_rate != self.sample_rate: - audio_data = self.convert_sample_rate(audio_data, frame_rate, self.sample_rate) - - print(f"✓ 音频已从WAV文件加载: {filename}") - return audio_data - - except Exception as e: - print(f"✗ 从WAV文件加载音频失败: {e}") - import traceback - traceback.print_exc() - return np.array([], dtype=np.float32) - - def apply_auto_tuning(self, audio_data: np.ndarray, pitch_shift: float = 0.0) -> np.ndarray: - """ - 应用自动调音效果 - - Args: - audio_data: 音频数据 - pitch_shift: 音高偏移 (半音数) - - Returns: - 调音后的音频数据 - """ - try: - if len(audio_data) == 0: - return audio_data - - # 简化的自动调音实现 - # 实际的自动调音算法非常复杂,这里只实现音高移位 - - # 计算音高移位因子 - pitch_factor = 2 ** (pitch_shift / 12.0) - - if pitch_factor == 1.0: - return audio_data - - # 简化的音高移位(线性插值) - output_length = int(len(audio_data) / pitch_factor) - if output_length <= 0: - return np.array([], dtype=np.float32) - - processed = np.zeros(output_length) - - # 重采样 - for i in range(output_length): - # 计算源位置 - src_pos = i * pitch_factor - src_index = int(src_pos) - frac = src_pos - src_index - - # 线性插值 - if src_index < len(audio_data) - 1: - processed[i] = audio_data[src_index] * (1 - frac) + \ - audio_data[src_index + 1] * frac - else: - processed[i] = audio_data[min(src_index, len(audio_data) - 1)] - - return processed - except Exception as e: - print(f"✗ 自动调音失败: {e}") - return audio_data - - def apply_formant_filter(self, audio_data: np.ndarray, formant_freq: float = 1000.0, - formant_bandwidth: float = 100.0) -> np.ndarray: - """ - 应用共振峰滤波器 - - Args: - audio_data: 音频数据 - formant_freq: 共振峰频率 (Hz) - formant_bandwidth: 共振峰带宽 (Hz) - - Returns: - 共振峰滤波后的音频数据 - """ - try: - if len(audio_data) == 0: - return audio_data - - # 简化的共振峰滤波器实现 - # 使用带通滤波器模拟共振峰 - - # 计算滤波器参数 - w0 = 2 * np.pi * formant_freq / self.sample_rate - alpha = np.sin(w0) * np.sinh(np.log(2) / 2 * formant_bandwidth * w0 / np.sin(w0)) - - # 计算滤波器系数 - b0 = alpha - b1 = 0 - b2 = -alpha - a0 = 1 + alpha - a1 = -2 * np.cos(w0) - a2 = 1 - alpha - - # 归一化系数 - b0, b1, b2 = b0/a0, b1/a0, b2/a0 - a1, a2 = a1/a0, a2/a0 - - # 应用滤波器 (直接II型结构) - filtered_audio = np.zeros_like(audio_data) - for n in range(len(audio_data)): - filtered_audio[n] = b0 * audio_data[n] + b1 * (audio_data[n-1] if n > 0 else 0) + \ - b2 * (audio_data[n-2] if n > 1 else 0) - \ - a1 * (filtered_audio[n-1] if n > 0 else 0) - \ - a2 * (filtered_audio[n-2] if n > 1 else 0) - - return filtered_audio - except Exception as e: - print(f"✗ 共振峰滤波失败: {e}") - return audio_data - - def apply_harmonic_enhancement(self, audio_data: np.ndarray, harmonics: List[int] = [2, 3]) -> np.ndarray: - """ - 应用谐波增强效果 - - Args: - audio_data: 音频数据 - harmonics: 要增强的谐波倍数列表 - - Returns: - 谐波增强后的音频数据 - """ - try: - if len(audio_data) == 0: - return audio_data - - # 简化的谐波增强实现 - enhanced_audio = audio_data.copy() - - # 对每个谐波倍数进行处理 - for harmonic in harmonics: - # 音高移位到谐波频率 - harmonic_audio = self.apply_auto_tuning(audio_data, 12 * np.log2(harmonic)) - - # 混合到原始音频 (较低的增益) - enhanced_audio += harmonic_audio * (0.3 / len(harmonics)) - - # 归一化防止削波 - max_val = np.max(np.abs(enhanced_audio)) - if max_val > 1.0: - enhanced_audio = enhanced_audio / max_val - - return enhanced_audio - except Exception as e: - print(f"✗ 谐波增强失败: {e}") - return audio_data - - def apply_spectral_subtraction(self, audio_data: np.ndarray, noise_factor: float = 0.1) -> np.ndarray: - """ - 应用谱减法降噪 - - Args: - audio_data: 音频数据 - noise_factor: 噪声估计因子 - - Returns: - 降噪后的音频数据 - """ - try: - if len(audio_data) == 0: - return audio_data - - # 简化的谱减法实现 - # 1. 计算短时傅里叶变换 - frame_length = int(self.sample_rate * 0.025) # 25ms帧 - frame_step = int(self.sample_rate * 0.01) # 10ms步长 - frames = [] - - # 分帧 - for i in range(0, len(audio_data) - frame_length, frame_step): - frame = audio_data[i:i + frame_length] - # 应用窗函数 - windowed_frame = self.apply_window_function(frame, 'hann') - frames.append(windowed_frame) - - if not frames: - return audio_data - - # 2. 估计噪声谱 (使用前几帧) - noise_frames = min(5, len(frames)) - noise_spectrum = np.mean([np.abs(np.fft.fft(frames[i])) for i in range(noise_frames)], axis=0) - - # 3. 对每帧进行谱减 - processed_frames = [] - for frame in frames: - # 计算帧的频谱 - frame_spectrum = np.fft.fft(frame) - magnitude = np.abs(frame_spectrum) - phase = np.angle(frame_spectrum) - - # 谱减法 - subtracted_magnitude = np.maximum(magnitude - noise_factor * noise_spectrum, 0) - - # 重构频谱 - reconstructed_spectrum = subtracted_magnitude * np.exp(1j * phase) - - # 逆变换得到时域信号 - processed_frame = np.real(np.fft.ifft(reconstructed_spectrum)) - processed_frames.append(processed_frame) - - # 4. 重叠相加合成 - output_length = len(audio_data) - output_signal = np.zeros(output_length) - overlap_count = np.zeros(output_length) - - for i, frame in enumerate(processed_frames): - start_idx = i * frame_step - end_idx = min(start_idx + frame_length, output_length) - output_signal[start_idx:end_idx] += frame[:end_idx-start_idx] - overlap_count[start_idx:end_idx] += 1 - - # 归一化重叠区域 - output_signal = np.divide(output_signal, overlap_count, - out=np.zeros_like(output_signal), where=overlap_count!=0) - - return output_signal - except Exception as e: - print(f"✗ 谱减法降噪失败: {e}") - return audio_data - - def apply_adaptive_filter(self, audio_data: np.ndarray, reference_signal: np.ndarray = None) -> np.ndarray: - """ - 应用自适应滤波器(用于回声消除等) - - Args: - audio_data: 主音频信号 - reference_signal: 参考信号(如远端信号) - - Returns: - 滤波后的音频数据 - """ - try: - if len(audio_data) == 0: - return audio_data - - # 如果没有提供参考信号,使用延迟版本的主信号作为参考 - if reference_signal is None: - delay_samples = int(0.05 * self.sample_rate) # 50ms延迟 - reference_signal = np.concatenate([np.zeros(delay_samples), audio_data[:-delay_samples]]) - - # 确保参考信号长度与主信号相同 - if len(reference_signal) != len(audio_data): - min_length = min(len(reference_signal), len(audio_data)) - audio_data = audio_data[:min_length] - reference_signal = reference_signal[:min_length] - - # 简化的LMS自适应滤波器 - filter_length = 64 - step_size = 0.01 - weights = np.zeros(filter_length) - filtered_audio = np.zeros_like(audio_data) - - # 自适应滤波处理 - for n in range(filter_length, len(audio_data)): - # 获取输入向量 - input_vector = reference_signal[n-filter_length:n][::-1] # 反转 - - # 滤波器输出 - output = np.dot(weights, input_vector) - - # 误差信号 - error = audio_data[n] - output - - # 更新权重 (LMS算法) - weights += step_size * error * input_vector - - # 保存滤波后的信号 - filtered_audio[n] = error - - return filtered_audio - except Exception as e: - print(f"✗ 自适应滤波失败: {e}") - return audio_data - - def apply_voice_activity_detection(self, audio_data: np.ndarray) -> np.ndarray: - """ - 应用语音活动检测,只保留语音段 - - Args: - audio_data: 音频数据 - - Returns: - 仅包含语音段的音频数据 - """ - try: - if len(audio_data) == 0: - return audio_data - - # 分帧处理 - frame_length = int(self.sample_rate * self.audio_params['frame_length']) - frame_stride = int(self.sample_rate * self.audio_params['frame_stride']) - - voiced_frames = [] - - # 处理每帧 - for i in range(0, len(audio_data) - frame_length, frame_stride): - frame = audio_data[i:i + frame_length] - - # 计算帧能量 - frame_energy = np.sum(frame ** 2) / len(frame) - - # 计算过零率 - zero_crossings = np.sum(np.diff(np.sign(frame)) != 0) / len(frame) - - # 简单的VAD决策 - # 语音通常具有较高的能量和适中的过零率 - energy_threshold = self.audio_params['noise_threshold'] ** 2 - if frame_energy > energy_threshold and 0.01 < zero_crossings < 0.5: - voiced_frames.append(frame) - - # 合并语音帧 - if voiced_frames: - return np.concatenate(voiced_frames) - else: - return np.array([], dtype=np.float32) - - except Exception as e: - print(f"✗ 语音活动检测失败: {e}") - return audio_data - - def apply_speech_enhancement(self, audio_data: np.ndarray) -> np.ndarray: - """ - 应用综合语音增强处理 - - Args: - audio_data: 音频数据 - - Returns: - 增强后的音频数据 - """ - try: - if len(audio_data) == 0: - return audio_data - - # 1. 预处理 - processed = self._preprocess_audio(audio_data) - - # 2. 噪声抑制 - processed = self.apply_spectral_subtraction(processed, noise_factor=0.1) - - # 3. 语音活动检测 - processed = self.apply_voice_activity_detection(processed) - - # 4. 共振峰增强 - processed = self.apply_formant_filter(processed, formant_freq=1000.0, formant_bandwidth=100.0) - - # 5. 动态范围压缩 - processed = self.apply_dynamic_range_compression(processed, threshold=-25.0, ratio=3.0) - - # 6. 人声增强 - processed = self.apply_vocal_enhancement(processed) - - return processed - except Exception as e: - print(f"✗ 语音增强失败: {e}") - return audio_data - - def calculate_mfcc(self, audio_data: np.ndarray, num_mfcc: int = 13) -> np.ndarray: - """ - 计算MFCC特征(梅尔频率倒谱系数) - - Args: - audio_data: 音频数据 - num_mfcc: MFCC系数数量 - - Returns: - MFCC特征矩阵 - """ - try: - if len(audio_data) == 0: - return np.array([]) - - # 这是一个简化的MFCC实现 - # 实际应用中会使用专门的库如librosa - - # 1. 预加重 - pre_emphasized = self._preprocess_audio(audio_data) - - # 2. 分帧和加窗 - frame_length = int(self.sample_rate * 0.025) - frame_stride = int(self.sample_rate * 0.01) - - # 3. 计算功率谱 - # (简化实现,实际会更复杂) - - # 返回简化的特征 - return np.random.rand(num_mfcc, max(1, len(audio_data) // frame_stride)) - except Exception as e: - print(f"✗ MFCC计算失败: {e}") - return np.array([]) - - def apply_beamforming(self, audio_data: np.ndarray, angle: float = 0.0) -> np.ndarray: - """ - 应用波束成形(针对多麦克风阵列) - - Args: - audio_data: 音频数据 - angle: 目标方向角度(弧度) - - Returns: - 波束成形后的音频数据 - """ - try: - # 这是一个简化的波束成形实现 - # 实际实现需要多通道音频数据和麦克风阵列几何信息 - - # 对于单通道音频,简单返回原始数据 - return audio_data - except Exception as e: - print(f"✗ 波束成形失败: {e}") - return audio_data - - def apply_speech_separation(self, audio_data: np.ndarray, num_speakers: int = 2) -> List[np.ndarray]: - """ - 应用语音分离(将混合语音分离为不同说话人) - - Args: - audio_data: 混合音频数据 - num_speakers: 说话人数量 - - Returns: - 分离后的音频数据列表 - """ - try: - # 这是一个简化的语音分离实现 - # 实际实现会非常复杂,需要深度学习模型 - - # 简单地返回相同音频的多个副本 - return [audio_data.copy() for _ in range(num_speakers)] - except Exception as e: - print(f"✗ 语音分离失败: {e}") +""" +语音管理器 +负责管理语音识别和合成的核心功能 +""" + +import time +import threading +import numpy as np +from typing import Dict, Any, List +from queue import Queue, Empty +import pyaudio +import wave +import struct + +class SpeechManager: + """ + 语音管理器 + 负责管理语音识别和合成的核心功能,包括音频设备管理、音频数据处理等 + """ + + def __init__(self, plugin): + """ + 初始化语音管理器 + + Args: + plugin: 语音识别和合成插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 音频配置 + self.sample_rate = plugin.config.get('sample_rate', 16000) + self.channels = plugin.config.get('channels', 1) + self.buffer_size = plugin.config.get('buffer_size', 1024) + self.format = pyaudio.paInt16 # 16位音频格式 + + # 音频设备 + self.pyaudio_instance = None + self.input_stream = None + self.output_stream = None + + # 音频缓冲区 + self.audio_buffer = Queue(maxsize=1000) # 音频数据缓冲区 + self.processed_buffer = Queue(maxsize=1000) # 处理后数据缓冲区 + + # 音频处理状态 + self.is_recording = False + self.is_playing = False + self.recording_thread = None + self.playback_thread = None + + # 统计信息 + self.stats = { + 'audio_buffers_processed': 0, + 'audio_buffers_dropped': 0, + 'recording_time': 0.0, + 'playback_time': 0.0, + 'peak_audio_level': 0.0, + 'total_samples_recorded': 0, + 'total_samples_played': 0 + } + + # 音频处理参数 + self.audio_params = { + 'noise_threshold': 0.01, + 'silence_threshold': 0.005, + 'silence_duration': 1.0, + 'min_voice_duration': 0.5, + 'pre_emphasis': 0.97, # 预加重系数 + 'frame_length': 0.025, # 帧长度(25ms) + 'frame_stride': 0.01, # 帧步长(10ms) + } + + # 音频处理缓冲区 + self.voice_buffer = [] # 语音片段缓冲区 + self.silence_counter = 0 + self.voice_activity_counter = 0 + self.is_voice_detected = False + + print("✓ 语音管理器已创建") + + def initialize(self) -> bool: + """ + 初始化语音管理器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化PyAudio + self.pyaudio_instance = pyaudio.PyAudio() + + # 验证音频设备 + input_device_count = self.pyaudio_instance.get_device_count() + if input_device_count == 0: + print("⚠ 未检测到音频输入设备") + + print(f"✓ 检测到 {input_device_count} 个音频设备") + + # 初始化音频设备 + print("✓ 音频设备初始化完成") + + self.initialized = True + print("✓ 语音管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 语音管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用语音管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 语音管理器未初始化") + return False + + self.enabled = True + print("✓ 语音管理器已启用") + return True + + except Exception as e: + print(f"✗ 语音管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用语音管理器""" + try: + self.enabled = False + self.stop_recording() + self.stop_playback() + print("✓ 语音管理器已禁用") + + except Exception as e: + print(f"✗ 语音管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理语音管理器资源""" + try: + self.disable() + + # 关闭音频流 + if self.input_stream: + self.input_stream.stop_stream() + self.input_stream.close() + self.input_stream = None + + if self.output_stream: + self.output_stream.stop_stream() + self.output_stream.close() + self.output_stream = None + + # 终止PyAudio + if self.pyaudio_instance: + self.pyaudio_instance.terminate() + self.pyaudio_instance = None + + self.initialized = False + print("✓ 语音管理器资源已清理") + + except Exception as e: + print(f"✗ 语音管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新语音管理器状态 + + Args: + dt: 时间增量 + """ + # 这里可以处理定期更新的任务 + pass + + def start_recording(self) -> bool: + """ + 开始录音 + + Returns: + 是否开始成功 + """ + try: + if not self.enabled: + print("✗ 语音管理器未启用") + return False + + if self.is_recording: + print("⚠ 已经在录音中") + return True + + # 打开音频输入流 + self.input_stream = self.pyaudio_instance.open( + format=self.format, + channels=self.channels, + rate=self.sample_rate, + input=True, + frames_per_buffer=self.buffer_size + ) + + # 启动录音线程 + self.is_recording = True + self.recording_thread = threading.Thread(target=self._recording_worker, daemon=True) + self.recording_thread.start() + + print("✓ 开始录音") + return True + + except Exception as e: + print(f"✗ 开始录音失败: {e}") + import traceback + traceback.print_exc() + return False + + def stop_recording(self): + """停止录音""" + try: + self.is_recording = False + + # 关闭音频输入流 + if self.input_stream: + self.input_stream.stop_stream() + self.input_stream.close() + self.input_stream = None + + if self.recording_thread and self.recording_thread.is_alive(): + self.recording_thread.join(timeout=1.0) + print("✓ 停止录音") + + except Exception as e: + print(f"✗ 停止录音失败: {e}") + import traceback + traceback.print_exc() + + def start_playback(self, audio_data: bytes) -> bool: + """ + 开始播放音频 + + Args: + audio_data: 音频数据 + + Returns: + 是否开始成功 + """ + try: + if not self.enabled: + print("✗ 语音管理器未启用") + return False + + if self.is_playing: + print("⚠ 已经在播放中") + return True + + # 打开音频输出流 + self.output_stream = self.pyaudio_instance.open( + format=self.format, + channels=self.channels, + rate=self.sample_rate, + output=True, + frames_per_buffer=self.buffer_size + ) + + # 启动播放线程 + self.is_playing = True + self.playback_thread = threading.Thread( + target=self._playback_worker, + args=(audio_data,), + daemon=True + ) + self.playback_thread.start() + + print("✓ 开始播放音频") + return True + + except Exception as e: + print(f"✗ 开始播放音频失败: {e}") + import traceback + traceback.print_exc() + return False + + def stop_playback(self): + """停止播放音频""" + try: + self.is_playing = False + + # 关闭音频输出流 + if self.output_stream: + self.output_stream.stop_stream() + self.output_stream.close() + self.output_stream = None + + if self.playback_thread and self.playback_thread.is_alive(): + self.playback_thread.join(timeout=1.0) + print("✓ 停止播放音频") + + except Exception as e: + print(f"✗ 停止播放音频失败: {e}") + import traceback + traceback.print_exc() + + def _recording_worker(self): + """录音工作线程""" + try: + recording_start_time = time.time() + frames_recorded = 0 + + # 录音循环 + while self.is_recording and self.enabled: + try: + # 从音频输入流读取数据 + audio_data = self.input_stream.read(self.buffer_size, exception_on_overflow=False) + + # 转换为numpy数组进行处理 + audio_array = np.frombuffer(audio_data, dtype=np.int16) + audio_float = audio_array.astype(np.float32) / 32768.0 + + # 计算音频级别 + audio_level = np.mean(np.abs(audio_float)) if len(audio_float) > 0 else 0.0 + self.stats['peak_audio_level'] = max(self.stats['peak_audio_level'], audio_level) + + # 应用预处理 + processed_audio = self._preprocess_audio(audio_float) + + # 将音频数据放入缓冲区 + try: + self.audio_buffer.put(processed_audio, block=False) + frames_recorded += len(audio_float) + except: + self.stats['audio_buffers_dropped'] += 1 + + # 更新统计信息 + self.stats['total_samples_recorded'] += len(audio_float) + + except Exception as e: + print(f"⚠ 录音过程中出现错误: {e}") + continue + + self.stats['recording_time'] += time.time() - recording_start_time + self.stats['total_samples_recorded'] = frames_recorded + + except Exception as e: + print(f"✗ 录音工作线程错误: {e}") + import traceback + traceback.print_exc() + + def _playback_worker(self, audio_data: bytes): + """播放工作线程""" + try: + playback_start_time = time.time() + bytes_played = 0 + + # 将音频数据分块播放 + chunk_size = self.buffer_size * 2 # 16位音频,每个样本2字节 + for i in range(0, len(audio_data), chunk_size): + if not self.is_playing: + break + + chunk = audio_data[i:i + chunk_size] + + try: + # 写入音频输出流 + self.output_stream.write(chunk) + bytes_played += len(chunk) + except Exception as e: + print(f"⚠ 播放过程中出现错误: {e}") + continue + + self.stats['playback_time'] += time.time() - playback_start_time + self.stats['total_samples_played'] = bytes_played // 2 # 16位音频 + self.is_playing = False + + except Exception as e: + print(f"✗ 播放工作线程错误: {e}") + import traceback + traceback.print_exc() + self.is_playing = False + + def _preprocess_audio(self, audio_data: np.ndarray) -> np.ndarray: + """ + 预处理音频数据 + + Args: + audio_data: 原始音频数据 + + Returns: + 预处理后的音频数据 + """ + try: + if len(audio_data) == 0: + return audio_data + + # 预加重 (Pre-emphasis) + pre_emphasis = self.audio_params['pre_emphasis'] + if pre_emphasis > 0: + # 应用预加重滤波器: y[t] = x[t] - a * x[t-1] + emphasized = np.zeros_like(audio_data) + emphasized[0] = audio_data[0] + emphasized[1:] = audio_data[1:] - pre_emphasis * audio_data[:-1] + audio_data = emphasized + + # 归一化 + max_val = np.max(np.abs(audio_data)) + if max_val > 0: + audio_data = audio_data / max_val + + return audio_data + + except Exception as e: + print(f"✗ 音频预处理失败: {e}") + return audio_data + + def get_audio_chunk(self) -> np.ndarray: + """ + 获取音频数据块 + + Returns: + 音频数据块 + """ + try: + return self.audio_buffer.get(block=False) + except Empty: + return np.zeros(self.buffer_size, dtype=np.float32) + except Exception as e: + print(f"✗ 获取音频数据块失败: {e}") + return np.zeros(self.buffer_size, dtype=np.float32) + + def put_processed_audio(self, audio_data: np.ndarray): + """ + 放入处理后的音频数据 + + Args: + audio_data: 处理后的音频数据 + """ + try: + self.processed_buffer.put(audio_data, block=False) + self.stats['audio_buffers_processed'] += 1 + except: + self.stats['audio_buffers_dropped'] += 1 + + def get_processed_audio(self) -> np.ndarray: + """ + 获取处理后的音频数据 + + Returns: + 处理后的音频数据 + """ + try: + return self.processed_buffer.get(block=False) + except Empty: + return np.zeros(self.buffer_size, dtype=np.float32) + except Exception as e: + print(f"✗ 获取处理后音频数据失败: {e}") + return np.zeros(self.buffer_size, dtype=np.float32) + + def set_sample_rate(self, sample_rate: int): + """ + 设置采样率 + + Args: + sample_rate: 采样率 (Hz) + """ + old_sample_rate = self.sample_rate + self.sample_rate = sample_rate + print(f"✓ 采样率已从 {old_sample_rate} 更新为: {sample_rate} Hz") + + def set_buffer_size(self, buffer_size: int): + """ + 设置缓冲区大小 + + Args: + buffer_size: 缓冲区大小 + """ + self.buffer_size = buffer_size + print(f"✓ 缓冲区大小设置为: {buffer_size}") + + def set_noise_threshold(self, threshold: float): + """ + 设置噪声阈值 + + Args: + threshold: 噪声阈值 + """ + self.audio_params['noise_threshold'] = max(0.0, min(1.0, threshold)) + print(f"✓ 噪声阈值设置为: {threshold}") + + def set_silence_threshold(self, threshold: float): + """ + 设置静音阈值 + + Args: + threshold: 静音阈值 + """ + self.audio_params['silence_threshold'] = max(0.0, min(1.0, threshold)) + print(f"✓ 静音阈值设置为: {threshold}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def is_audio_silent(self, audio_data: np.ndarray) -> bool: + """ + 检查音频是否静音 + + Args: + audio_data: 音频数据 + + Returns: + 是否静音 + """ + try: + if len(audio_data) == 0: + return True + audio_level = np.mean(np.abs(audio_data)) + return audio_level < self.audio_params['silence_threshold'] + except Exception as e: + print(f"✗ 检查音频静音状态失败: {e}") + return True + + def is_audio_noisy(self, audio_data: np.ndarray) -> bool: + """ + 检查音频是否有噪声 + + Args: + audio_data: 音频数据 + + Returns: + 是否有噪声 + """ + try: + if len(audio_data) == 0: + return False + audio_level = np.mean(np.abs(audio_data)) + return audio_level > self.audio_params['noise_threshold'] + except Exception as e: + print(f"✗ 检查音频噪声状态失败: {e}") + return False + + def apply_preprocessing(self, audio_data: np.ndarray) -> np.ndarray: + """ + 应用音频预处理 + + Args: + audio_data: 原始音频数据 + + Returns: + 预处理后的音频数据 + """ + try: + # 预加重 + pre_emphasis = self.audio_params['pre_emphasis'] + if pre_emphasis > 0 and len(audio_data) > 1: + emphasized = np.zeros_like(audio_data) + emphasized[0] = audio_data[0] + emphasized[1:] = audio_data[1:] - pre_emphasis * audio_data[:-1] + audio_data = emphasized + + # 归一化 + max_val = np.max(np.abs(audio_data)) + if max_val > 0: + audio_data = audio_data / max_val + + return audio_data + except Exception as e: + print(f"✗ 音频预处理失败: {e}") + return audio_data + + def apply_postprocessing(self, audio_data: np.ndarray) -> np.ndarray: + """ + 应用音频后处理 + + Args: + audio_data: 处理后的音频数据 + + Returns: + 后处理后的音频数据 + """ + try: + # 限幅处理防止削波 + audio_data = np.clip(audio_data, -0.99, 0.99) + + # 转换为16位整数格式 + audio_int16 = (audio_data * 32767).astype(np.int16) + + return audio_int16 + except Exception as e: + print(f"✗ 音频后处理失败: {e}") + # 返回原始数据的16位版本 + try: + return (audio_data * 32767).astype(np.int16) + except: + return np.zeros(len(audio_data), dtype=np.int16) + + def detect_voice_activity(self, audio_data: np.ndarray) -> bool: + """ + 检测语音活动 + + Args: + audio_data: 音频数据 + + Returns: + 是否检测到语音活动 + """ + try: + # 检查音频级别是否超过噪声阈值 + if len(audio_data) == 0: + return False + + audio_level = np.mean(np.abs(audio_data)) + return audio_level > self.audio_params['noise_threshold'] + except Exception as e: + print(f"✗ 语音活动检测失败: {e}") + return False + + def calculate_audio_level(self, audio_data: np.ndarray) -> float: + """ + 计算音频级别 + + Args: + audio_data: 音频数据 + + Returns: + 音频级别 (0.0-1.0) + """ + try: + if len(audio_data) == 0: + return 0.0 + return float(np.mean(np.abs(audio_data))) + except Exception as e: + print(f"✗ 计算音频级别失败: {e}") + return 0.0 + + def convert_sample_rate(self, audio_data: np.ndarray, from_rate: int, to_rate: int) -> np.ndarray: + """ + 转换音频采样率 + + Args: + audio_data: 音频数据 + from_rate: 原始采样率 + to_rate: 目标采样率 + + Returns: + 转换后的音频数据 + """ + try: + if from_rate == to_rate: + return audio_data + + # 计算采样率转换比例 + ratio = to_rate / from_rate + new_length = int(len(audio_data) * ratio) + + if new_length <= 0: + return np.array([], dtype=np.float32) + + # 使用线性插值进行重采样 + indices = np.linspace(0, len(audio_data) - 1, new_length) + resampled_data = np.interp(indices, np.arange(len(audio_data)), audio_data) + + return resampled_data.astype(np.float32) + except Exception as e: + print(f"✗ 采样率转换失败: {e}") + return audio_data + + def apply_gain(self, audio_data: np.ndarray, gain: float) -> np.ndarray: + """ + 应用增益到音频数据 + + Args: + audio_data: 音频数据 + gain: 增益值 + + Returns: + 应用增益后的音频数据 + """ + try: + amplified_data = audio_data * gain + # 防止削波 + return np.clip(amplified_data, -1.0, 1.0) + except Exception as e: + print(f"✗ 应用增益失败: {e}") + return audio_data + + def mix_audio(self, audio1: np.ndarray, audio2: np.ndarray) -> np.ndarray: + """ + 混合两个音频信号 + + Args: + audio1: 第一个音频信号 + audio2: 第二个音频信号 + + Returns: + 混合后的音频信号 + """ + try: + # 确保两个音频信号长度相同 + max_length = max(len(audio1), len(audio2)) + if len(audio1) < max_length: + audio1 = np.pad(audio1, (0, max_length - len(audio1))) + if len(audio2) < max_length: + audio2 = np.pad(audio2, (0, max_length - len(audio2))) + + # 混合音频(简单平均) + mixed_audio = (audio1 + audio2) / 2.0 + + # 防止削波 + return np.clip(mixed_audio, -1.0, 1.0) + except Exception as e: + print(f"✗ 音频混合失败: {e}") + # 返回较长的音频信号 + return audio1 if len(audio1) >= len(audio2) else audio2 + + def apply_window_function(self, audio_data: np.ndarray, window_type: str = 'hann') -> np.ndarray: + """ + 应用窗函数到音频数据 + + Args: + audio_data: 音频数据 + window_type: 窗函数类型 ('hann', 'hamming', 'blackman') + + Returns: + 应用窗函数后的音频数据 + """ + try: + if len(audio_data) == 0: + return audio_data + + # 根据类型生成窗函数 + if window_type == 'hann': + window = np.hanning(len(audio_data)) + elif window_type == 'hamming': + window = np.hamming(len(audio_data)) + elif window_type == 'blackman': + window = np.blackman(len(audio_data)) + else: + # 默认使用汉宁窗 + window = np.hanning(len(audio_data)) + + # 应用窗函数 + windowed_data = audio_data * window + + return windowed_data + except Exception as e: + print(f"✗ 应用窗函数失败: {e}") + return audio_data + + def calculate_spectral_features(self, audio_data: np.ndarray) -> Dict[str, Any]: + """ + 计算音频的频谱特征 + + Args: + audio_data: 音频数据 + + Returns: + 频谱特征字典 + """ + try: + if len(audio_data) == 0: + return {} + + # 计算FFT + fft_data = np.fft.fft(audio_data) + magnitude = np.abs(fft_data) + + # 计算基本频谱特征 + spectral_centroid = np.sum(magnitude * np.arange(len(magnitude))) / np.sum(magnitude) + spectral_rolloff = np.sum(magnitude[:int(len(magnitude) * 0.85)]) + spectral_flux = np.sum(np.diff(magnitude) ** 2) + zero_crossing_rate = np.sum(np.diff(np.sign(audio_data)) != 0) / len(audio_data) + + return { + 'spectral_centroid': spectral_centroid, + 'spectral_rolloff': spectral_rolloff, + 'spectral_flux': spectral_flux, + 'zero_crossing_rate': zero_crossing_rate, + 'rms_energy': np.sqrt(np.mean(audio_data ** 2)), + 'peak_amplitude': np.max(np.abs(audio_data)) + } + except Exception as e: + print(f"✗ 计算频谱特征失败: {e}") + return {} + + def apply_spectral_filter(self, audio_data: np.ndarray, filter_type: str, cutoff_freq: float) -> np.ndarray: + """ + 应用频谱滤波器 + + Args: + audio_data: 音频数据 + filter_type: 滤波器类型 ('lowpass', 'highpass', 'bandpass') + cutoff_freq: 截止频率 (Hz) + + Returns: + 滤波后的音频数据 + """ + try: + if len(audio_data) == 0: + return audio_data + + # 简化的频域滤波实现 + fft_data = np.fft.fft(audio_data) + freqs = np.fft.fftfreq(len(audio_data), 1/self.sample_rate) + + # 创建滤波器掩码 + if filter_type == 'lowpass': + mask = np.abs(freqs) <= cutoff_freq + elif filter_type == 'highpass': + mask = np.abs(freqs) >= cutoff_freq + elif filter_type == 'bandpass': + # 简化的带通滤波器 (以cutoff_freq为中心频率) + bandwidth = cutoff_freq * 0.2 # 20%带宽 + mask = (np.abs(freqs) >= (cutoff_freq - bandwidth)) & (np.abs(freqs) <= (cutoff_freq + bandwidth)) + else: + # 默认不滤波 + return audio_data + + # 应用滤波器 + filtered_fft = fft_data * mask + filtered_audio = np.real(np.fft.ifft(filtered_fft)) + + return filtered_audio + except Exception as e: + print(f"✗ 应用频谱滤波器失败: {e}") + return audio_data + + def segment_audio(self, audio_data: np.ndarray, segment_length: int) -> List[np.ndarray]: + """ + 将音频数据分割成段 + + Args: + audio_data: 音频数据 + segment_length: 每段长度 + + Returns: + 音频段列表 + """ + try: + segments = [] + for i in range(0, len(audio_data), segment_length): + segment = audio_data[i:i + segment_length] + segments.append(segment) + return segments + except Exception as e: + print(f"✗ 音频分割失败: {e}") + return [audio_data] + + def apply_dynamic_range_compression(self, audio_data: np.ndarray, threshold: float = -20.0, + ratio: float = 4.0, attack: float = 0.01, release: float = 0.1) -> np.ndarray: + """ + 应用动态范围压缩 + + Args: + audio_data: 音频数据 + threshold: 阈值 (dB) + ratio: 压缩比 + attack: 启动时间 (秒) + release: 释放时间 (秒) + + Returns: + 压缩后的音频数据 + """ + try: + if len(audio_data) == 0: + return audio_data + + # 转换阈值为线性值 + threshold_linear = 10 ** (threshold / 20.0) + + # 计算包络 + envelope = np.zeros_like(audio_data) + gain = np.ones_like(audio_data) + + # 简化的压缩器实现 + current_gain = 1.0 + for i in range(len(audio_data)): + # 计算瞬时幅度 + instant_amp = abs(audio_data[i]) + + # 更新包络 (简化实现) + if instant_amp > envelope[i-1] if i > 0 else 0: + # 启动阶段 + envelope[i] = instant_amp + else: + # 释放阶段 + envelope[i] = envelope[i-1] * 0.99 if i > 0 else instant_amp + + # 计算增益缩减 + if envelope[i] > threshold_linear: + gain_db = 20 * np.log10(envelope[i] / threshold_linear) + reduction_db = gain_db - gain_db / ratio + target_gain = 10 ** (-reduction_db / 20.0) + else: + target_gain = 1.0 + + # 应用启动和释放时间 + if target_gain < current_gain: + # 启动 + current_gain = current_gain + (target_gain - current_gain) * (1 - np.exp(-1.0 / (self.sample_rate * attack))) + else: + # 释放 + current_gain = current_gain + (target_gain - current_gain) * (1 - np.exp(-1.0 / (self.sample_rate * release))) + + gain[i] = current_gain + + # 应用增益 + compressed_audio = audio_data * gain + + return compressed_audio + except Exception as e: + print(f"✗ 动态范围压缩失败: {e}") + return audio_data + + def apply_noise_reduction(self, audio_data: np.ndarray, noise_factor: float = 0.05) -> np.ndarray: + """ + 应用噪声抑制 + + Args: + audio_data: 音频数据 + noise_factor: 噪声因子 (0.0-1.0) + + Returns: + 噪声抑制后的音频数据 + """ + try: + if len(audio_data) == 0: + return audio_data + + # 简化的噪声抑制实现 + # 使用谱减法的简化版本 + fft_data = np.fft.fft(audio_data) + magnitude = np.abs(fft_data) + phase = np.angle(fft_data) + + # 估计噪声水平 (使用前10%的数据) + noise_estimate = np.mean(magnitude[:max(1, len(magnitude) // 10)]) + + # 应用噪声抑制 + suppression = np.maximum(magnitude - noise_factor * noise_estimate, 0) + suppressed_fft = suppression * np.exp(1j * phase) + + # 逆变换 + suppressed_audio = np.real(np.fft.ifft(suppressed_fft)) + + return suppressed_audio + except Exception as e: + print(f"✗ 噪声抑制失败: {e}") + return audio_data + + def apply_echo_effect(self, audio_data: np.ndarray, delay: float = 0.25, decay: float = 0.5) -> np.ndarray: + """ + 应用回声效果 + + Args: + audio_data: 音频数据 + delay: 延迟时间 (秒) + decay: 衰减系数 (0.0-1.0) + + Returns: + 添加回声效果的音频数据 + """ + try: + if len(audio_data) == 0: + return audio_data + + # 计算延迟样本数 + delay_samples = int(delay * self.sample_rate) + + # 创建输出数组 + echo_audio = np.zeros(len(audio_data) + delay_samples) + echo_audio[:len(audio_data)] = audio_data + + # 应用回声 + for i in range(len(audio_data)): + if i + delay_samples < len(echo_audio): + echo_audio[i + delay_samples] += audio_data[i] * decay + + # 裁剪到原始长度 + return echo_audio[:len(audio_data)] + except Exception as e: + print(f"✗ 回声效果应用失败: {e}") + return audio_data + + def apply_vocal_enhancement(self, audio_data: np.ndarray) -> np.ndarray: + """ + 应用人声增强效果 + + Args: + audio_data: 音频数据 + + Returns: + 人声增强后的音频数据 + """ + try: + if len(audio_data) == 0: + return audio_data + + # 简化的人声增强实现 + # 1. 应用预加重 + pre_emphasized = self._preprocess_audio(audio_data) + + # 2. 应用中频提升滤波器 (简化实现) + # 人声主要集中在300Hz-3400Hz范围内 + mid_freq_boosted = self.apply_spectral_filter(pre_emphasized, 'bandpass', 1500) # 中心频率约1500Hz + + # 3. 应用动态范围压缩 + compressed = self.apply_dynamic_range_compression(mid_freq_boosted, threshold=-25.0, ratio=3.0) + + # 4. 混合原始信号和增强信号 + enhanced_audio = (pre_emphasized + compressed) / 2.0 + + return enhanced_audio + except Exception as e: + print(f"✗ 人声增强失败: {e}") + return audio_data + + def save_audio_to_wav(self, filename: str, audio_data: np.ndarray, sample_rate: int = None) -> bool: + """ + 保存音频数据为WAV文件 + + Args: + filename: 文件名 + audio_data: 音频数据 + sample_rate: 采样率 + + Returns: + 是否保存成功 + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + # 确保目录存在 + import os + os.makedirs(os.path.dirname(filename) if os.path.dirname(filename) else '.', exist_ok=True) + + # 转换为16位整数 + if audio_data.dtype != np.int16: + audio_int16 = np.clip(audio_data, -1.0, 1.0) + audio_int16 = (audio_int16 * 32767).astype(np.int16) + else: + audio_int16 = audio_data + + # 保存为WAV文件 + with wave.open(filename, 'w') as wav_file: + wav_file.setnchannels(self.channels) + wav_file.setsampwidth(2) # 16位 + wav_file.setframerate(sample_rate) + wav_file.writeframes(audio_int16.tobytes()) + + print(f"✓ 音频已保存为WAV文件: {filename}") + return True + except Exception as e: + print(f"✗ 保存WAV文件失败: {e}") + import traceback + traceback.print_exc() + return False + + def load_audio_from_wav(self, filename: str) -> np.ndarray: + """ + 从WAV文件加载音频数据 + + Args: + filename: 文件名 + + Returns: + 音频数据 + """ + try: + with wave.open(filename, 'r') as wav_file: + # 获取音频参数 + channels = wav_file.getnchannels() + sample_width = wav_file.getsampwidth() + frame_rate = wav_file.getframerate() + frames = wav_file.getnframes() + + # 读取音频数据 + raw_audio = wav_file.readframes(frames) + + # 转换为numpy数组 + if sample_width == 1: + # 8位音频 + audio_data = np.frombuffer(raw_audio, dtype=np.uint8) + audio_data = (audio_data.astype(np.float32) - 128) / 128.0 + elif sample_width == 2: + # 16位音频 + audio_data = np.frombuffer(raw_audio, dtype=np.int16) + audio_data = audio_data.astype(np.float32) / 32768.0 + elif sample_width == 4: + # 32位音频 + audio_data = np.frombuffer(raw_audio, dtype=np.int32) + audio_data = audio_data.astype(np.float32) / 2147483648.0 + else: + raise ValueError(f"不支持的采样宽度: {sample_width}") + + # 如果采样率不同,进行重采样 + if frame_rate != self.sample_rate: + audio_data = self.convert_sample_rate(audio_data, frame_rate, self.sample_rate) + + print(f"✓ 音频已从WAV文件加载: {filename}") + return audio_data + + except Exception as e: + print(f"✗ 从WAV文件加载音频失败: {e}") + import traceback + traceback.print_exc() + return np.array([], dtype=np.float32) + + def apply_auto_tuning(self, audio_data: np.ndarray, pitch_shift: float = 0.0) -> np.ndarray: + """ + 应用自动调音效果 + + Args: + audio_data: 音频数据 + pitch_shift: 音高偏移 (半音数) + + Returns: + 调音后的音频数据 + """ + try: + if len(audio_data) == 0: + return audio_data + + # 简化的自动调音实现 + # 实际的自动调音算法非常复杂,这里只实现音高移位 + + # 计算音高移位因子 + pitch_factor = 2 ** (pitch_shift / 12.0) + + if pitch_factor == 1.0: + return audio_data + + # 简化的音高移位(线性插值) + output_length = int(len(audio_data) / pitch_factor) + if output_length <= 0: + return np.array([], dtype=np.float32) + + processed = np.zeros(output_length) + + # 重采样 + for i in range(output_length): + # 计算源位置 + src_pos = i * pitch_factor + src_index = int(src_pos) + frac = src_pos - src_index + + # 线性插值 + if src_index < len(audio_data) - 1: + processed[i] = audio_data[src_index] * (1 - frac) + \ + audio_data[src_index + 1] * frac + else: + processed[i] = audio_data[min(src_index, len(audio_data) - 1)] + + return processed + except Exception as e: + print(f"✗ 自动调音失败: {e}") + return audio_data + + def apply_formant_filter(self, audio_data: np.ndarray, formant_freq: float = 1000.0, + formant_bandwidth: float = 100.0) -> np.ndarray: + """ + 应用共振峰滤波器 + + Args: + audio_data: 音频数据 + formant_freq: 共振峰频率 (Hz) + formant_bandwidth: 共振峰带宽 (Hz) + + Returns: + 共振峰滤波后的音频数据 + """ + try: + if len(audio_data) == 0: + return audio_data + + # 简化的共振峰滤波器实现 + # 使用带通滤波器模拟共振峰 + + # 计算滤波器参数 + w0 = 2 * np.pi * formant_freq / self.sample_rate + alpha = np.sin(w0) * np.sinh(np.log(2) / 2 * formant_bandwidth * w0 / np.sin(w0)) + + # 计算滤波器系数 + b0 = alpha + b1 = 0 + b2 = -alpha + a0 = 1 + alpha + a1 = -2 * np.cos(w0) + a2 = 1 - alpha + + # 归一化系数 + b0, b1, b2 = b0/a0, b1/a0, b2/a0 + a1, a2 = a1/a0, a2/a0 + + # 应用滤波器 (直接II型结构) + filtered_audio = np.zeros_like(audio_data) + for n in range(len(audio_data)): + filtered_audio[n] = b0 * audio_data[n] + b1 * (audio_data[n-1] if n > 0 else 0) + \ + b2 * (audio_data[n-2] if n > 1 else 0) - \ + a1 * (filtered_audio[n-1] if n > 0 else 0) - \ + a2 * (filtered_audio[n-2] if n > 1 else 0) + + return filtered_audio + except Exception as e: + print(f"✗ 共振峰滤波失败: {e}") + return audio_data + + def apply_harmonic_enhancement(self, audio_data: np.ndarray, harmonics: List[int] = [2, 3]) -> np.ndarray: + """ + 应用谐波增强效果 + + Args: + audio_data: 音频数据 + harmonics: 要增强的谐波倍数列表 + + Returns: + 谐波增强后的音频数据 + """ + try: + if len(audio_data) == 0: + return audio_data + + # 简化的谐波增强实现 + enhanced_audio = audio_data.copy() + + # 对每个谐波倍数进行处理 + for harmonic in harmonics: + # 音高移位到谐波频率 + harmonic_audio = self.apply_auto_tuning(audio_data, 12 * np.log2(harmonic)) + + # 混合到原始音频 (较低的增益) + enhanced_audio += harmonic_audio * (0.3 / len(harmonics)) + + # 归一化防止削波 + max_val = np.max(np.abs(enhanced_audio)) + if max_val > 1.0: + enhanced_audio = enhanced_audio / max_val + + return enhanced_audio + except Exception as e: + print(f"✗ 谐波增强失败: {e}") + return audio_data + + def apply_spectral_subtraction(self, audio_data: np.ndarray, noise_factor: float = 0.1) -> np.ndarray: + """ + 应用谱减法降噪 + + Args: + audio_data: 音频数据 + noise_factor: 噪声估计因子 + + Returns: + 降噪后的音频数据 + """ + try: + if len(audio_data) == 0: + return audio_data + + # 简化的谱减法实现 + # 1. 计算短时傅里叶变换 + frame_length = int(self.sample_rate * 0.025) # 25ms帧 + frame_step = int(self.sample_rate * 0.01) # 10ms步长 + frames = [] + + # 分帧 + for i in range(0, len(audio_data) - frame_length, frame_step): + frame = audio_data[i:i + frame_length] + # 应用窗函数 + windowed_frame = self.apply_window_function(frame, 'hann') + frames.append(windowed_frame) + + if not frames: + return audio_data + + # 2. 估计噪声谱 (使用前几帧) + noise_frames = min(5, len(frames)) + noise_spectrum = np.mean([np.abs(np.fft.fft(frames[i])) for i in range(noise_frames)], axis=0) + + # 3. 对每帧进行谱减 + processed_frames = [] + for frame in frames: + # 计算帧的频谱 + frame_spectrum = np.fft.fft(frame) + magnitude = np.abs(frame_spectrum) + phase = np.angle(frame_spectrum) + + # 谱减法 + subtracted_magnitude = np.maximum(magnitude - noise_factor * noise_spectrum, 0) + + # 重构频谱 + reconstructed_spectrum = subtracted_magnitude * np.exp(1j * phase) + + # 逆变换得到时域信号 + processed_frame = np.real(np.fft.ifft(reconstructed_spectrum)) + processed_frames.append(processed_frame) + + # 4. 重叠相加合成 + output_length = len(audio_data) + output_signal = np.zeros(output_length) + overlap_count = np.zeros(output_length) + + for i, frame in enumerate(processed_frames): + start_idx = i * frame_step + end_idx = min(start_idx + frame_length, output_length) + output_signal[start_idx:end_idx] += frame[:end_idx-start_idx] + overlap_count[start_idx:end_idx] += 1 + + # 归一化重叠区域 + output_signal = np.divide(output_signal, overlap_count, + out=np.zeros_like(output_signal), where=overlap_count!=0) + + return output_signal + except Exception as e: + print(f"✗ 谱减法降噪失败: {e}") + return audio_data + + def apply_adaptive_filter(self, audio_data: np.ndarray, reference_signal: np.ndarray = None) -> np.ndarray: + """ + 应用自适应滤波器(用于回声消除等) + + Args: + audio_data: 主音频信号 + reference_signal: 参考信号(如远端信号) + + Returns: + 滤波后的音频数据 + """ + try: + if len(audio_data) == 0: + return audio_data + + # 如果没有提供参考信号,使用延迟版本的主信号作为参考 + if reference_signal is None: + delay_samples = int(0.05 * self.sample_rate) # 50ms延迟 + reference_signal = np.concatenate([np.zeros(delay_samples), audio_data[:-delay_samples]]) + + # 确保参考信号长度与主信号相同 + if len(reference_signal) != len(audio_data): + min_length = min(len(reference_signal), len(audio_data)) + audio_data = audio_data[:min_length] + reference_signal = reference_signal[:min_length] + + # 简化的LMS自适应滤波器 + filter_length = 64 + step_size = 0.01 + weights = np.zeros(filter_length) + filtered_audio = np.zeros_like(audio_data) + + # 自适应滤波处理 + for n in range(filter_length, len(audio_data)): + # 获取输入向量 + input_vector = reference_signal[n-filter_length:n][::-1] # 反转 + + # 滤波器输出 + output = np.dot(weights, input_vector) + + # 误差信号 + error = audio_data[n] - output + + # 更新权重 (LMS算法) + weights += step_size * error * input_vector + + # 保存滤波后的信号 + filtered_audio[n] = error + + return filtered_audio + except Exception as e: + print(f"✗ 自适应滤波失败: {e}") + return audio_data + + def apply_voice_activity_detection(self, audio_data: np.ndarray) -> np.ndarray: + """ + 应用语音活动检测,只保留语音段 + + Args: + audio_data: 音频数据 + + Returns: + 仅包含语音段的音频数据 + """ + try: + if len(audio_data) == 0: + return audio_data + + # 分帧处理 + frame_length = int(self.sample_rate * self.audio_params['frame_length']) + frame_stride = int(self.sample_rate * self.audio_params['frame_stride']) + + voiced_frames = [] + + # 处理每帧 + for i in range(0, len(audio_data) - frame_length, frame_stride): + frame = audio_data[i:i + frame_length] + + # 计算帧能量 + frame_energy = np.sum(frame ** 2) / len(frame) + + # 计算过零率 + zero_crossings = np.sum(np.diff(np.sign(frame)) != 0) / len(frame) + + # 简单的VAD决策 + # 语音通常具有较高的能量和适中的过零率 + energy_threshold = self.audio_params['noise_threshold'] ** 2 + if frame_energy > energy_threshold and 0.01 < zero_crossings < 0.5: + voiced_frames.append(frame) + + # 合并语音帧 + if voiced_frames: + return np.concatenate(voiced_frames) + else: + return np.array([], dtype=np.float32) + + except Exception as e: + print(f"✗ 语音活动检测失败: {e}") + return audio_data + + def apply_speech_enhancement(self, audio_data: np.ndarray) -> np.ndarray: + """ + 应用综合语音增强处理 + + Args: + audio_data: 音频数据 + + Returns: + 增强后的音频数据 + """ + try: + if len(audio_data) == 0: + return audio_data + + # 1. 预处理 + processed = self._preprocess_audio(audio_data) + + # 2. 噪声抑制 + processed = self.apply_spectral_subtraction(processed, noise_factor=0.1) + + # 3. 语音活动检测 + processed = self.apply_voice_activity_detection(processed) + + # 4. 共振峰增强 + processed = self.apply_formant_filter(processed, formant_freq=1000.0, formant_bandwidth=100.0) + + # 5. 动态范围压缩 + processed = self.apply_dynamic_range_compression(processed, threshold=-25.0, ratio=3.0) + + # 6. 人声增强 + processed = self.apply_vocal_enhancement(processed) + + return processed + except Exception as e: + print(f"✗ 语音增强失败: {e}") + return audio_data + + def calculate_mfcc(self, audio_data: np.ndarray, num_mfcc: int = 13) -> np.ndarray: + """ + 计算MFCC特征(梅尔频率倒谱系数) + + Args: + audio_data: 音频数据 + num_mfcc: MFCC系数数量 + + Returns: + MFCC特征矩阵 + """ + try: + if len(audio_data) == 0: + return np.array([]) + + # 这是一个简化的MFCC实现 + # 实际应用中会使用专门的库如librosa + + # 1. 预加重 + pre_emphasized = self._preprocess_audio(audio_data) + + # 2. 分帧和加窗 + frame_length = int(self.sample_rate * 0.025) + frame_stride = int(self.sample_rate * 0.01) + + # 3. 计算功率谱 + # (简化实现,实际会更复杂) + + # 返回简化的特征 + return np.random.rand(num_mfcc, max(1, len(audio_data) // frame_stride)) + except Exception as e: + print(f"✗ MFCC计算失败: {e}") + return np.array([]) + + def apply_beamforming(self, audio_data: np.ndarray, angle: float = 0.0) -> np.ndarray: + """ + 应用波束成形(针对多麦克风阵列) + + Args: + audio_data: 音频数据 + angle: 目标方向角度(弧度) + + Returns: + 波束成形后的音频数据 + """ + try: + # 这是一个简化的波束成形实现 + # 实际实现需要多通道音频数据和麦克风阵列几何信息 + + # 对于单通道音频,简单返回原始数据 + return audio_data + except Exception as e: + print(f"✗ 波束成形失败: {e}") + return audio_data + + def apply_speech_separation(self, audio_data: np.ndarray, num_speakers: int = 2) -> List[np.ndarray]: + """ + 应用语音分离(将混合语音分离为不同说话人) + + Args: + audio_data: 混合音频数据 + num_speakers: 说话人数量 + + Returns: + 分离后的音频数据列表 + """ + try: + # 这是一个简化的语音分离实现 + # 实际实现会非常复杂,需要深度学习模型 + + # 简单地返回相同音频的多个副本 + return [audio_data.copy() for _ in range(num_speakers)] + except Exception as e: + print(f"✗ 语音分离失败: {e}") return [audio_data] \ No newline at end of file diff --git a/plugins/user/speech_recognition_synthesis/plugin.py b/plugins/user/speech_recognition_synthesis/plugin.py index b3477e8f..5c7b8754 100644 --- a/plugins/user/speech_recognition_synthesis/plugin.py +++ b/plugins/user/speech_recognition_synthesis/plugin.py @@ -1,522 +1,522 @@ -""" -语音识别和合成插件 -为EG引擎提供语音识别和文本转语音功能 -""" - -import sys -import os -import time -import threading -from typing import Dict, Any, Callable, List - -# 添加项目根目录到Python路径 -sys.path.append(os.path.dirname(os.path.abspath(__file__))) - -from core.speech_manager import SpeechManager -from recognition.speech_recognizer import SpeechRecognizer -from synthesis.speech_synthesizer import SpeechSynthesizer -from utils.audio_utils import AudioUtils - -class SpeechRecognitionSynthesisPlugin: - """ - 语音识别和合成插件主类 - 提供完整的语音处理功能,包括语音识别和文本转语音合成 - """ - - def __init__(self, world): - """ - 初始化语音识别和合成插件 - - Args: - world: EG引擎世界对象 - """ - self.world = world - self.plugin_name = "speech_recognition_synthesis" - self.version = "1.0.0" - self.author = "EG Team" - - # 插件状态 - self.enabled = False - self.initialized = False - self.is_listening = False - - # 核心组件 - self.speech_manager = None - self.speech_recognizer = None - self.speech_synthesizer = None - self.audio_utils = None - - # 配置参数 - self.config = { - 'sample_rate': 16000, - 'channels': 1, - 'buffer_size': 1024, - 'language': 'zh-CN', - 'voice': 'default', - 'volume': 1.0, - 'speed': 1.0, - 'pitch': 1.0 - } - - # 回调函数 - self.speech_recognized_callback = None - self.speech_synthesis_callback = None - self.error_callback = None - - # 统计信息 - self.stats = { - 'recognized_count': 0, - 'synthesized_count': 0, - 'recognition_errors': 0, - 'synthesis_errors': 0, - 'processing_time': 0.0 - } - - # 线程管理 - self.recognition_thread = None - self.synthesis_thread = None - self.running = False - - print(f"✓ 语音识别和合成插件 v{self.version} 已创建") - - def initialize(self) -> bool: - """ - 初始化插件 - - Returns: - 是否初始化成功 - """ - try: - # 初始化核心组件 - self.speech_manager = SpeechManager(self) - self.speech_recognizer = SpeechRecognizer(self) - self.speech_synthesizer = SpeechSynthesizer(self) - self.audio_utils = AudioUtils(self) - - # 初始化各组件 - if not self.speech_manager.initialize(): - print("✗ 语音管理器初始化失败") - return False - - if not self.speech_recognizer.initialize(): - print("✗ 语音识别器初始化失败") - return False - - if not self.speech_synthesizer.initialize(): - print("✗ 语音合成器初始化失败") - return False - - self.initialized = True - print("✓ 语音识别和合成插件初始化完成") - return True - - except Exception as e: - print(f"✗ 插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 插件未初始化") - return False - - # 启用各组件 - if not self.speech_manager.enable(): - print("✗ 语音管理器启用失败") - return False - - if not self.speech_recognizer.enable(): - print("✗ 语音识别器启用失败") - return False - - if not self.speech_synthesizer.enable(): - print("✗ 语音合成器启用失败") - return False - - self.enabled = True - self.running = True - - # 启动识别线程 - self.recognition_thread = threading.Thread(target=self._recognition_worker, daemon=True) - self.recognition_thread.start() - - print("✓ 语音识别和合成插件已启用") - return True - - except Exception as e: - print(f"✗ 插件启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用插件""" - try: - self.running = False - self.enabled = False - - # 禁用各组件 - if self.speech_synthesizer: - self.speech_synthesizer.disable() - - if self.speech_recognizer: - self.speech_recognizer.disable() - - if self.speech_manager: - self.speech_manager.disable() - - print("✓ 语音识别和合成插件已禁用") - - except Exception as e: - print(f"✗ 插件禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理插件资源""" - try: - self.disable() - - # 清理各组件 - if self.speech_synthesizer: - self.speech_synthesizer.finalize() - self.speech_synthesizer = None - - if self.speech_recognizer: - self.speech_recognizer.finalize() - self.speech_recognizer = None - - if self.speech_manager: - self.speech_manager.finalize() - self.speech_manager = None - - if self.audio_utils: - self.audio_utils = None - - self.initialized = False - print("✓ 语音识别和合成插件资源已清理") - - except Exception as e: - print(f"✗ 插件资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新插件状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新各组件 - if self.speech_manager: - self.speech_manager.update(dt) - - if self.speech_recognizer: - self.speech_recognizer.update(dt) - - if self.speech_synthesizer: - self.speech_synthesizer.update(dt) - - except Exception as e: - print(f"✗ 插件更新失败: {e}") - if self.error_callback: - self.error_callback(f"插件更新失败: {e}") - - def _recognition_worker(self): - """语音识别工作线程""" - try: - while self.running and self.enabled: - if self.is_listening and self.speech_recognizer: - # 执行语音识别 - self.speech_recognizer.process_audio() - time.sleep(0.01) # 10ms延迟 - - except Exception as e: - print(f"✗ 语音识别工作线程错误: {e}") - if self.error_callback: - self.error_callback(f"语音识别工作线程错误: {e}") - - def start_listening(self) -> bool: - """ - 开始语音监听 - - Returns: - 是否开始成功 - """ - try: - if not self.enabled: - print("✗ 插件未启用") - return False - - if self.speech_recognizer: - result = self.speech_recognizer.start_listening() - if result: - self.is_listening = True - print("✓ 开始语音监听") - return result - return False - - except Exception as e: - print(f"✗ 开始语音监听失败: {e}") - if self.error_callback: - self.error_callback(f"开始语音监听失败: {e}") - return False - - def stop_listening(self): - """停止语音监听""" - try: - if self.speech_recognizer: - self.speech_recognizer.stop_listening() - - self.is_listening = False - print("✓ 停止语音监听") - - except Exception as e: - print(f"✗ 停止语音监听失败: {e}") - if self.error_callback: - self.error_callback(f"停止语音监听失败: {e}") - - def synthesize_speech(self, text: str, callback: Callable = None) -> bool: - """ - 合成语音 - - Args: - text: 要合成的文本 - callback: 合成完成回调函数 - - Returns: - 是否开始合成 - """ - try: - if not self.enabled: - print("✗ 插件未启用") - return False - - if self.speech_synthesizer: - result = self.speech_synthesizer.synthesize(text, callback) - if result: - self.stats['synthesized_count'] += 1 - print(f"✓ 开始合成语音: {text[:50]}...") - return result - return False - - except Exception as e: - print(f"✗ 语音合成失败: {e}") - self.stats['synthesis_errors'] += 1 - if self.error_callback: - self.error_callback(f"语音合成失败: {e}") - return False - - def set_speech_recognized_callback(self, callback: Callable): - """ - 设置语音识别回调函数 - - Args: - callback: 回调函数 - """ - self.speech_recognized_callback = callback - - def set_speech_synthesis_callback(self, callback: Callable): - """ - 设置语音合成回调函数 - - Args: - callback: 回调函数 - """ - self.speech_synthesis_callback = callback - - def set_error_callback(self, callback: Callable): - """ - 设置错误回调函数 - - Args: - callback: 回调函数 - """ - self.error_callback = callback - - def set_language(self, language: str): - """ - 设置语言 - - Args: - language: 语言代码 (如 'zh-CN', 'en-US') - """ - self.config['language'] = language - if self.speech_recognizer: - self.speech_recognizer.set_language(language) - if self.speech_synthesizer: - self.speech_synthesizer.set_language(language) - print(f"✓ 语言设置为: {language}") - - def set_voice(self, voice: str): - """ - 设置语音 - - Args: - voice: 语音名称 - """ - self.config['voice'] = voice - if self.speech_synthesizer: - self.speech_synthesizer.set_voice(voice) - print(f"✓ 语音设置为: {voice}") - - def set_volume(self, volume: float): - """ - 设置音量 - - Args: - volume: 音量 (0.0-1.0) - """ - self.config['volume'] = max(0.0, min(1.0, volume)) - if self.speech_synthesizer: - self.speech_synthesizer.set_volume(volume) - print(f"✓ 音量设置为: {volume}") - - def set_speed(self, speed: float): - """ - 设置语速 - - Args: - speed: 语速 (0.5-2.0) - """ - self.config['speed'] = max(0.5, min(2.0, speed)) - if self.speech_synthesizer: - self.speech_synthesizer.set_speed(speed) - print(f"✓ 语速设置为: {speed}") - - def set_pitch(self, pitch: float): - """ - 设置音调 - - Args: - pitch: 音调 (0.5-2.0) - """ - self.config['pitch'] = max(0.5, min(2.0, pitch)) - if self.speech_synthesizer: - self.speech_synthesizer.set_pitch(pitch) - print(f"✓ 音调设置为: {pitch}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - # 合并各组件的统计信息 - stats = self.stats.copy() - - if self.speech_manager: - stats.update(self.speech_manager.get_stats()) - - if self.speech_recognizer: - stats.update(self.speech_recognizer.get_stats()) - - if self.speech_synthesizer: - stats.update(self.speech_synthesizer.get_stats()) - - return stats - - def is_speaking(self) -> bool: - """ - 检查是否正在播放语音 - - Returns: - 是否正在播放语音 - """ - if self.speech_synthesizer: - return self.speech_synthesizer.is_speaking() - return False - - def stop_speaking(self): - """停止播放语音""" - if self.speech_synthesizer: - self.speech_synthesizer.stop_speaking() - print("✓ 停止播放语音") - - def get_available_voices(self) -> List[str]: - """ - 获取可用语音列表 - - Returns: - 语音名称列表 - """ - if self.speech_synthesizer: - return self.speech_synthesizer.get_available_voices() - return [] - - def get_available_languages(self) -> List[str]: - """ - 获取可用语言列表 - - Returns: - 语言代码列表 - """ - if self.speech_recognizer and self.speech_synthesizer: - recognizer_langs = self.speech_recognizer.get_available_languages() - synthesizer_langs = self.speech_synthesizer.get_available_languages() - # 返回两个列表的交集 - return list(set(recognizer_langs) & set(synthesizer_langs)) - elif self.speech_recognizer: - return self.speech_recognizer.get_available_languages() - elif self.speech_synthesizer: - return self.speech_synthesizer.get_available_languages() - return [] - - def save_audio_file(self, filename: str, audio_data: bytes) -> bool: - """ - 保存音频文件 - - Args: - filename: 文件名 - audio_data: 音频数据 - - Returns: - 是否保存成功 - """ - if self.audio_utils: - return self.audio_utils.save_audio_file(filename, audio_data) - return False - - def load_audio_file(self, filename: str) -> bytes: - """ - 加载音频文件 - - Args: - filename: 文件名 - - Returns: - 音频数据 - """ - if self.audio_utils: - return self.audio_utils.load_audio_file(filename) - return b'' - -# 插件注册信息 -def get_plugin_info(): - """获取插件信息""" - return { - 'name': 'speech_recognition_synthesis', - 'version': '1.0.0', - 'author': 'EG Team', - 'description': '语音识别和合成插件,提供语音识别和文本转语音功能', - 'dependencies': [] - } - -# 创建插件实例的工厂函数 -def create_plugin(world): - """创建插件实例""" +""" +语音识别和合成插件 +为EG引擎提供语音识别和文本转语音功能 +""" + +import sys +import os +import time +import threading +from typing import Dict, Any, Callable, List + +# 添加项目根目录到Python路径 +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from core.speech_manager import SpeechManager +from recognition.speech_recognizer import SpeechRecognizer +from synthesis.speech_synthesizer import SpeechSynthesizer +from utils.audio_utils import AudioUtils + +class SpeechRecognitionSynthesisPlugin: + """ + 语音识别和合成插件主类 + 提供完整的语音处理功能,包括语音识别和文本转语音合成 + """ + + def __init__(self, world): + """ + 初始化语音识别和合成插件 + + Args: + world: EG引擎世界对象 + """ + self.world = world + self.plugin_name = "speech_recognition_synthesis" + self.version = "1.0.0" + self.author = "EG Team" + + # 插件状态 + self.enabled = False + self.initialized = False + self.is_listening = False + + # 核心组件 + self.speech_manager = None + self.speech_recognizer = None + self.speech_synthesizer = None + self.audio_utils = None + + # 配置参数 + self.config = { + 'sample_rate': 16000, + 'channels': 1, + 'buffer_size': 1024, + 'language': 'zh-CN', + 'voice': 'default', + 'volume': 1.0, + 'speed': 1.0, + 'pitch': 1.0 + } + + # 回调函数 + self.speech_recognized_callback = None + self.speech_synthesis_callback = None + self.error_callback = None + + # 统计信息 + self.stats = { + 'recognized_count': 0, + 'synthesized_count': 0, + 'recognition_errors': 0, + 'synthesis_errors': 0, + 'processing_time': 0.0 + } + + # 线程管理 + self.recognition_thread = None + self.synthesis_thread = None + self.running = False + + print(f"✓ 语音识别和合成插件 v{self.version} 已创建") + + def initialize(self) -> bool: + """ + 初始化插件 + + Returns: + 是否初始化成功 + """ + try: + # 初始化核心组件 + self.speech_manager = SpeechManager(self) + self.speech_recognizer = SpeechRecognizer(self) + self.speech_synthesizer = SpeechSynthesizer(self) + self.audio_utils = AudioUtils(self) + + # 初始化各组件 + if not self.speech_manager.initialize(): + print("✗ 语音管理器初始化失败") + return False + + if not self.speech_recognizer.initialize(): + print("✗ 语音识别器初始化失败") + return False + + if not self.speech_synthesizer.initialize(): + print("✗ 语音合成器初始化失败") + return False + + self.initialized = True + print("✓ 语音识别和合成插件初始化完成") + return True + + except Exception as e: + print(f"✗ 插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 插件未初始化") + return False + + # 启用各组件 + if not self.speech_manager.enable(): + print("✗ 语音管理器启用失败") + return False + + if not self.speech_recognizer.enable(): + print("✗ 语音识别器启用失败") + return False + + if not self.speech_synthesizer.enable(): + print("✗ 语音合成器启用失败") + return False + + self.enabled = True + self.running = True + + # 启动识别线程 + self.recognition_thread = threading.Thread(target=self._recognition_worker, daemon=True) + self.recognition_thread.start() + + print("✓ 语音识别和合成插件已启用") + return True + + except Exception as e: + print(f"✗ 插件启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用插件""" + try: + self.running = False + self.enabled = False + + # 禁用各组件 + if self.speech_synthesizer: + self.speech_synthesizer.disable() + + if self.speech_recognizer: + self.speech_recognizer.disable() + + if self.speech_manager: + self.speech_manager.disable() + + print("✓ 语音识别和合成插件已禁用") + + except Exception as e: + print(f"✗ 插件禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理插件资源""" + try: + self.disable() + + # 清理各组件 + if self.speech_synthesizer: + self.speech_synthesizer.finalize() + self.speech_synthesizer = None + + if self.speech_recognizer: + self.speech_recognizer.finalize() + self.speech_recognizer = None + + if self.speech_manager: + self.speech_manager.finalize() + self.speech_manager = None + + if self.audio_utils: + self.audio_utils = None + + self.initialized = False + print("✓ 语音识别和合成插件资源已清理") + + except Exception as e: + print(f"✗ 插件资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新插件状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新各组件 + if self.speech_manager: + self.speech_manager.update(dt) + + if self.speech_recognizer: + self.speech_recognizer.update(dt) + + if self.speech_synthesizer: + self.speech_synthesizer.update(dt) + + except Exception as e: + print(f"✗ 插件更新失败: {e}") + if self.error_callback: + self.error_callback(f"插件更新失败: {e}") + + def _recognition_worker(self): + """语音识别工作线程""" + try: + while self.running and self.enabled: + if self.is_listening and self.speech_recognizer: + # 执行语音识别 + self.speech_recognizer.process_audio() + time.sleep(0.01) # 10ms延迟 + + except Exception as e: + print(f"✗ 语音识别工作线程错误: {e}") + if self.error_callback: + self.error_callback(f"语音识别工作线程错误: {e}") + + def start_listening(self) -> bool: + """ + 开始语音监听 + + Returns: + 是否开始成功 + """ + try: + if not self.enabled: + print("✗ 插件未启用") + return False + + if self.speech_recognizer: + result = self.speech_recognizer.start_listening() + if result: + self.is_listening = True + print("✓ 开始语音监听") + return result + return False + + except Exception as e: + print(f"✗ 开始语音监听失败: {e}") + if self.error_callback: + self.error_callback(f"开始语音监听失败: {e}") + return False + + def stop_listening(self): + """停止语音监听""" + try: + if self.speech_recognizer: + self.speech_recognizer.stop_listening() + + self.is_listening = False + print("✓ 停止语音监听") + + except Exception as e: + print(f"✗ 停止语音监听失败: {e}") + if self.error_callback: + self.error_callback(f"停止语音监听失败: {e}") + + def synthesize_speech(self, text: str, callback: Callable = None) -> bool: + """ + 合成语音 + + Args: + text: 要合成的文本 + callback: 合成完成回调函数 + + Returns: + 是否开始合成 + """ + try: + if not self.enabled: + print("✗ 插件未启用") + return False + + if self.speech_synthesizer: + result = self.speech_synthesizer.synthesize(text, callback) + if result: + self.stats['synthesized_count'] += 1 + print(f"✓ 开始合成语音: {text[:50]}...") + return result + return False + + except Exception as e: + print(f"✗ 语音合成失败: {e}") + self.stats['synthesis_errors'] += 1 + if self.error_callback: + self.error_callback(f"语音合成失败: {e}") + return False + + def set_speech_recognized_callback(self, callback: Callable): + """ + 设置语音识别回调函数 + + Args: + callback: 回调函数 + """ + self.speech_recognized_callback = callback + + def set_speech_synthesis_callback(self, callback: Callable): + """ + 设置语音合成回调函数 + + Args: + callback: 回调函数 + """ + self.speech_synthesis_callback = callback + + def set_error_callback(self, callback: Callable): + """ + 设置错误回调函数 + + Args: + callback: 回调函数 + """ + self.error_callback = callback + + def set_language(self, language: str): + """ + 设置语言 + + Args: + language: 语言代码 (如 'zh-CN', 'en-US') + """ + self.config['language'] = language + if self.speech_recognizer: + self.speech_recognizer.set_language(language) + if self.speech_synthesizer: + self.speech_synthesizer.set_language(language) + print(f"✓ 语言设置为: {language}") + + def set_voice(self, voice: str): + """ + 设置语音 + + Args: + voice: 语音名称 + """ + self.config['voice'] = voice + if self.speech_synthesizer: + self.speech_synthesizer.set_voice(voice) + print(f"✓ 语音设置为: {voice}") + + def set_volume(self, volume: float): + """ + 设置音量 + + Args: + volume: 音量 (0.0-1.0) + """ + self.config['volume'] = max(0.0, min(1.0, volume)) + if self.speech_synthesizer: + self.speech_synthesizer.set_volume(volume) + print(f"✓ 音量设置为: {volume}") + + def set_speed(self, speed: float): + """ + 设置语速 + + Args: + speed: 语速 (0.5-2.0) + """ + self.config['speed'] = max(0.5, min(2.0, speed)) + if self.speech_synthesizer: + self.speech_synthesizer.set_speed(speed) + print(f"✓ 语速设置为: {speed}") + + def set_pitch(self, pitch: float): + """ + 设置音调 + + Args: + pitch: 音调 (0.5-2.0) + """ + self.config['pitch'] = max(0.5, min(2.0, pitch)) + if self.speech_synthesizer: + self.speech_synthesizer.set_pitch(pitch) + print(f"✓ 音调设置为: {pitch}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + # 合并各组件的统计信息 + stats = self.stats.copy() + + if self.speech_manager: + stats.update(self.speech_manager.get_stats()) + + if self.speech_recognizer: + stats.update(self.speech_recognizer.get_stats()) + + if self.speech_synthesizer: + stats.update(self.speech_synthesizer.get_stats()) + + return stats + + def is_speaking(self) -> bool: + """ + 检查是否正在播放语音 + + Returns: + 是否正在播放语音 + """ + if self.speech_synthesizer: + return self.speech_synthesizer.is_speaking() + return False + + def stop_speaking(self): + """停止播放语音""" + if self.speech_synthesizer: + self.speech_synthesizer.stop_speaking() + print("✓ 停止播放语音") + + def get_available_voices(self) -> List[str]: + """ + 获取可用语音列表 + + Returns: + 语音名称列表 + """ + if self.speech_synthesizer: + return self.speech_synthesizer.get_available_voices() + return [] + + def get_available_languages(self) -> List[str]: + """ + 获取可用语言列表 + + Returns: + 语言代码列表 + """ + if self.speech_recognizer and self.speech_synthesizer: + recognizer_langs = self.speech_recognizer.get_available_languages() + synthesizer_langs = self.speech_synthesizer.get_available_languages() + # 返回两个列表的交集 + return list(set(recognizer_langs) & set(synthesizer_langs)) + elif self.speech_recognizer: + return self.speech_recognizer.get_available_languages() + elif self.speech_synthesizer: + return self.speech_synthesizer.get_available_languages() + return [] + + def save_audio_file(self, filename: str, audio_data: bytes) -> bool: + """ + 保存音频文件 + + Args: + filename: 文件名 + audio_data: 音频数据 + + Returns: + 是否保存成功 + """ + if self.audio_utils: + return self.audio_utils.save_audio_file(filename, audio_data) + return False + + def load_audio_file(self, filename: str) -> bytes: + """ + 加载音频文件 + + Args: + filename: 文件名 + + Returns: + 音频数据 + """ + if self.audio_utils: + return self.audio_utils.load_audio_file(filename) + return b'' + +# 插件注册信息 +def get_plugin_info(): + """获取插件信息""" + return { + 'name': 'speech_recognition_synthesis', + 'version': '1.0.0', + 'author': 'EG Team', + 'description': '语音识别和合成插件,提供语音识别和文本转语音功能', + 'dependencies': [] + } + +# 创建插件实例的工厂函数 +def create_plugin(world): + """创建插件实例""" return SpeechRecognitionSynthesisPlugin(world) \ No newline at end of file diff --git a/plugins/user/speech_recognition_synthesis/recognition/speech_recognizer.py b/plugins/user/speech_recognition_synthesis/recognition/speech_recognizer.py index 3fec718f..2f78c516 100644 --- a/plugins/user/speech_recognition_synthesis/recognition/speech_recognizer.py +++ b/plugins/user/speech_recognition_synthesis/recognition/speech_recognizer.py @@ -1,1772 +1,1772 @@ -""" -语音识别器 -负责将语音转换为文本 -""" - -import time -import numpy as np -from typing import Dict, Any, List, Callable -import threading -import json -import re - -class SpeechRecognizer: - """ - 语音识别器 - 负责将语音转换为文本,支持多种语言和识别引擎 - """ - - def __init__(self, plugin): - """ - 初始化语音识别器 - - Args: - plugin: 语音识别和合成插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - self.is_listening = False - self.is_processing = False - - # 识别配置 - self.language = plugin.config.get('language', 'zh-CN') - self.sample_rate = plugin.config.get('sample_rate', 16000) - self.channels = plugin.config.get('channels', 1) - - # 识别状态 - self.recognition_thread = None - self.audio_buffer = [] - self.silence_counter = 0 - self.voice_activity_counter = 0 - self.is_voice_detected = False - self.voice_start_time = 0.0 - self.last_voice_time = 0.0 - - # 识别参数 - self.recognition_params = { - 'silence_threshold': 0.005, - 'voice_threshold': 0.01, - 'silence_duration': 1.0, # 秒 - 'min_voice_duration': 0.3, # 秒 - 'max_recording_duration': 30.0, # 秒 - 'confidence_threshold': 0.7, # 置信度阈值 - 'enable_punctuation': True, # 启用标点符号 - 'enable_profanity_filter': True, # 启用脏话过滤 - 'max_alternatives': 3, # 最大备选结果数 - 'enable_word_timing': True, # 启用单词时间戳 - 'enable_speaker_diarization': False, # 启用说话人分离 - 'min_speaker_count': 1, # 最少说话人数 - 'max_speaker_count': 6, # 最多说话人数 - 'enable_contextual_biasing': True, # 启用上下文偏向 - 'contextual_phrases': [], # 上下文短语 - 'model_variant': 'default', # 模型变体 - 'enable_enhanced_models': True, # 启用增强模型 - 'recognition_timeout': 60.0, # 识别超时时间 - 'enable_real_time_feedback': True, # 启用实时反馈 - 'profanity_replace_char': '*', # 脏话替换字符 - 'enable_audio_preprocessing': True, # 启用音频预处理 - 'noise_reduction_level': 0.3, # 噪声消除级别 - } - - # 统计信息 - self.stats = { - 'recognitions_processed': 0, - 'recognition_errors': 0, - 'total_recognition_time': 0.0, - 'average_recognition_time': 0.0, - 'voice_segments_detected': 0, - 'total_voice_duration': 0.0, - 'words_per_minute': 0.0, - 'confidence_average': 0.0, - 'recognition_accuracy': 0.0 - } - - # 回调函数 - self.recognition_callback = None - self.partial_result_callback = None - self.error_callback = None - self.voice_activity_callback = None - - # 识别结果缓存 - self.recognition_cache = {} - self.cache_size = 100 - - # 识别模式 - self.recognition_mode = 'dictation' # 'command', 'dictation', 'search' - - # 自定义词汇表 - self.custom_lexicon = {} # 词汇到发音的映射 - - # 说话人信息 - self.speaker_info = {} - - # 模拟的识别结果数据库 - self.simulated_database = { - 'zh-CN': { - '你好世界': 0.95, - '今天天气怎么样': 0.92, - '我想测试语音识别功能': 0.90, - '这是一个语音识别测试': 0.88, - '语音识别插件工作正常': 0.91, - '请说出您要识别的内容': 0.89, - '语音识别技术非常有用': 0.87, - '我可以识别多种语言': 0.85, - '识别准确率很高': 0.86, - '支持实时语音识别': 0.84, - '语音命令控制': 0.93, - '打开游戏设置': 0.94, - '保存当前进度': 0.92, - '退出应用程序': 0.90, - '增加音量': 0.88, - '降低音量': 0.87, - '暂停游戏': 0.91, - '继续游戏': 0.90, - '重新开始': 0.89, - '显示地图': 0.88 - }, - 'en-US': { - 'Hello world': 0.95, - 'How is the weather today': 0.92, - 'I want to test speech recognition': 0.90, - 'This is a speech recognition test': 0.88, - 'Speech recognition plugin works fine': 0.91, - 'Please speak what you want to recognize': 0.89, - 'Speech recognition technology is very useful': 0.87, - 'I can recognize multiple languages': 0.85, - 'Recognition accuracy is high': 0.86, - 'Support real-time speech recognition': 0.84, - 'Voice command control': 0.93, - 'Open game settings': 0.94, - 'Save current progress': 0.92, - 'Exit application': 0.90, - 'Increase volume': 0.88, - 'Decrease volume': 0.87, - 'Pause game': 0.91, - 'Resume game': 0.90, - 'Restart': 0.89, - 'Show map': 0.88 - } - } - - # 专业术语词汇表 - self.technical_terms = { - 'GPU': 'G P U', - 'CPU': 'C P U', - 'RAM': 'R A M', - 'API': 'A P I', - 'SDK': 'S D K', - 'FPS': 'F P S', - 'UI': 'U I', - 'UX': 'U X' - } - - # 脏话过滤词典 - self.profanity_words = { - 'zh-CN': ['脏话1', '脏话2', '脏话3'], - 'en-US': ['profanity1', 'profanity2', 'profanity3'] - } - - print("✓ 语音识别器已创建") - - def initialize(self) -> bool: - """ - 初始化语音识别器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化识别引擎(这里简化处理,实际项目中需要加载相应的识别模型或库) - print("✓ 语音识别引擎初始化完成") - - self.initialized = True - print("✓ 语音识别器初始化完成") - return True - - except Exception as e: - print(f"✗ 语音识别器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用语音识别器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 语音识别器未初始化") - return False - - self.enabled = True - print("✓ 语音识别器已启用") - return True - - except Exception as e: - print(f"✗ 语音识别器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用语音识别器""" - try: - self.stop_listening() - self.enabled = False - print("✓ 语音识别器已禁用") - - except Exception as e: - print(f"✗ 语音识别器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理语音识别器资源""" - try: - self.disable() - self.initialized = False - print("✓ 语音识别器资源已清理") - - except Exception as e: - print(f"✗ 语音识别器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新语音识别器状态 - - Args: - dt: 时间增量 - """ - # 这里可以处理定期更新的任务 - pass - - def start_listening(self) -> bool: - """ - 开始语音监听 - - Returns: - 是否开始成功 - """ - try: - if not self.enabled: - print("✗ 语音识别器未启用") - return False - - if self.is_listening: - print("⚠ 已经在监听中") - return True - - self.is_listening = True - self.audio_buffer = [] - self.silence_counter = 0 - self.voice_activity_counter = 0 - self.is_voice_detected = False - self.voice_start_time = 0.0 - self.last_voice_time = 0.0 - - # 启动语音识别线程 - self.recognition_thread = threading.Thread(target=self._recognition_worker, daemon=True) - self.recognition_thread.start() - - # 启动录音 - if self.plugin.speech_manager: - self.plugin.speech_manager.start_recording() - - print("✓ 开始语音监听") - return True - - except Exception as e: - print(f"✗ 开始语音监听失败: {e}") - import traceback - traceback.print_exc() - return False - - def stop_listening(self): - """停止语音监听""" - try: - self.is_listening = False - - if self.recognition_thread and self.recognition_thread.is_alive(): - self.recognition_thread.join(timeout=1.0) - - # 停止录音 - if self.plugin.speech_manager: - self.plugin.speech_manager.stop_recording() - - print("✓ 停止语音监听") - - except Exception as e: - print(f"✗ 停止语音监听失败: {e}") - import traceback - traceback.print_exc() - - def process_audio(self): - """处理音频数据""" - try: - if not self.is_listening or not self.enabled: - return - - # 从语音管理器获取音频数据 - if self.plugin.speech_manager: - audio_chunk = self.plugin.speech_manager.get_audio_chunk() - - # 可选的音频预处理 - if self.recognition_params['enable_audio_preprocessing']: - audio_chunk = self.plugin.speech_manager.apply_speech_enhancement(audio_chunk) - - # 检测语音活动 - is_voice = self._detect_voice_activity(audio_chunk) - - current_time = time.time() - - if is_voice: - # 检测到语音 - if not self.is_voice_detected: - self.is_voice_detected = True - self.voice_start_time = current_time - self.voice_activity_counter = 1 - self.silence_counter = 0 - print("✓ 检测到语音活动开始") - - # 调用语音活动回调 - if self.voice_activity_callback: - self.voice_activity_callback(True) - else: - self.voice_activity_counter += 1 - self.last_voice_time = current_time - - else: - # 检测到静音 - if self.is_voice_detected: - self.silence_counter += 1 - - # 检查是否超过静音时长阈值 - silence_duration = self.silence_counter * len(audio_chunk) / self.sample_rate - if silence_duration >= self.recognition_params['silence_duration']: - # 语音活动结束 - voice_duration = current_time - self.voice_start_time - if voice_duration >= self.recognition_params['min_voice_duration']: - print(f"✓ 检测到语音活动结束,时长: {voice_duration:.2f}秒") - self._process_voice_segment() - else: - print("⚠ 语音片段太短,忽略") - - # 重置状态 - self.is_voice_detected = False - self.voice_activity_counter = 0 - self.silence_counter = 0 - - # 调用语音活动回调 - if self.voice_activity_callback: - self.voice_activity_callback(False) - - # 将音频数据添加到缓冲区 - self.audio_buffer.append(audio_chunk) - - # 检查是否超过最大录音时长 - total_samples = sum(len(chunk) for chunk in self.audio_buffer) - recording_time = total_samples / self.sample_rate - - if recording_time >= self.recognition_params['max_recording_duration']: - print("⚠ 达到最大录音时长,强制处理") - self._process_voice_segment() - - except Exception as e: - print(f"✗ 处理音频数据失败: {e}") - import traceback - traceback.print_exc() - - def _recognition_worker(self): - """语音识别工作线程""" - try: - while self.is_listening and self.enabled: - self.process_audio() - time.sleep(0.01) # 10ms延迟 - - except Exception as e: - print(f"✗ 语音识别工作线程错误: {e}") - import traceback - traceback.print_exc() - - def _detect_voice_activity(self, audio_chunk: np.ndarray) -> bool: - """ - 检测语音活动 - - Args: - audio_chunk: 音频数据块 - - Returns: - 是否检测到语音活动 - """ - try: - if len(audio_chunk) == 0: - return False - - # 计算音频级别 - audio_level = np.mean(np.abs(audio_chunk)) - - # 判断是否超过语音阈值 - return audio_level > self.recognition_params['voice_threshold'] - except Exception as e: - print(f"✗ 语音活动检测失败: {e}") - return False - - def _process_voice_segment(self): - """处理语音片段""" - try: - if not self.audio_buffer: - return - - # 检查语音时长是否足够 - total_samples = sum(len(chunk) for chunk in self.audio_buffer) - voice_duration = total_samples / self.sample_rate - - if voice_duration >= self.recognition_params['min_voice_duration']: - # 执行语音识别 - recognized_result = self._perform_recognition() - - if recognized_result and 'text' in recognized_result and recognized_result['text']: - self.stats['recognitions_processed'] += 1 - self.stats['voice_segments_detected'] += 1 - self.stats['total_voice_duration'] += voice_duration - - # 更新平均识别时间 - if self.stats['recognitions_processed'] > 0: - self.stats['average_recognition_time'] = ( - self.stats['total_recognition_time'] / self.stats['recognitions_processed'] - ) - - # 更新平均置信度 - if 'confidence' in recognized_result: - old_avg = self.stats['confidence_average'] - n = self.stats['recognitions_processed'] - self.stats['confidence_average'] = (old_avg * (n - 1) + recognized_result['confidence']) / n - - # 调用回调函数 - if self.plugin.speech_recognized_callback: - self.plugin.speech_recognized_callback(recognized_result['text']) - elif self.recognition_callback: - self.recognition_callback(recognized_result['text']) - - print(f"✓ 识别结果: {recognized_result['text']} (置信度: {recognized_result.get('confidence', 0):.2f})") - - # 缓存识别结果 - self._cache_recognition_result(recognized_result['text'], recognized_result) - - # 重置状态 - self.audio_buffer = [] - self.is_voice_detected = False - self.silence_counter = 0 - self.voice_activity_counter = 0 - self.voice_start_time = 0.0 - self.last_voice_time = 0.0 - - except Exception as e: - print(f"✗ 处理语音片段失败: {e}") - self.stats['recognition_errors'] += 1 - import traceback - traceback.print_exc() - - def _perform_recognition(self) -> Dict[str, Any]: - """ - 执行语音识别 - - Returns: - 识别结果字典 - """ - try: - recognition_start_time = time.time() - - # 合并音频缓冲区 - if not self.audio_buffer: - return {} - - audio_data = np.concatenate(self.audio_buffer) - - # 在实际项目中,这里应该调用真正的语音识别引擎 - # 例如:Google Speech-to-Text, Microsoft Speech Services, 或本地模型 - - # 模拟识别过程 - time.sleep(0.1) # 模拟识别延迟 - - # 生成模拟结果 - result = self._generate_simulated_result(audio_data) - - # 更新统计信息 - recognition_time = time.time() - recognition_start_time - self.stats['total_recognition_time'] += recognition_time - - return result - - except Exception as e: - print(f"✗ 语音识别失败: {e}") - self.stats['recognition_errors'] += 1 - import traceback - traceback.print_exc() - return {} - - def _generate_simulated_result(self, audio_data: np.ndarray) -> Dict[str, Any]: - """ - 生成模拟识别结果 - - Args: - audio_data: 音频数据 - - Returns: - 识别结果字典 - """ - try: - # 根据语言选择词汇表 - language_vocab = self.simulated_database.get(self.language, self.simulated_database['zh-CN']) - - # 计算音频特征以选择合适的短语 - audio_energy = np.sum(audio_data ** 2) / len(audio_data) - audio_duration = len(audio_data) / self.sample_rate - - # 根据音频特征选择短语 - candidates = [] - for phrase, base_confidence in language_vocab.items(): - # 根据音频时长调整置信度 - duration_match = 1.0 - abs(len(phrase) * 0.1 - audio_duration) / max(len(phrase) * 0.1, audio_duration) - energy_match = min(1.0, audio_energy * 1000) # 调整能量匹配 - - adjusted_confidence = base_confidence * duration_match * energy_match - candidates.append((phrase, adjusted_confidence)) - - # 选择置信度最高的结果 - if candidates: - candidates.sort(key=lambda x: x[1], reverse=True) - best_phrase, confidence = candidates[0] - - # 应用后处理 - processed_phrase = self._post_process_result(best_phrase) - - result = { - 'text': processed_phrase, - 'confidence': confidence, - 'language': self.language, - 'duration': audio_duration, - 'alternatives': [] - } - - # 添加备选结果 - for phrase, conf in candidates[1:self.recognition_params['max_alternatives']]: - result['alternatives'].append({ - 'text': self._post_process_result(phrase), - 'confidence': conf - }) - - return result - else: - return { - 'text': '', - 'confidence': 0.0, - 'language': self.language, - 'duration': audio_duration - } - - except Exception as e: - print(f"✗ 生成模拟识别结果失败: {e}") - return { - 'text': '', - 'confidence': 0.0, - 'language': self.language, - 'duration': len(audio_data) / self.sample_rate if audio_data is not None else 0 - } - - def _post_process_result(self, text: str) -> str: - """ - 后处理识别结果 - - Args: - text: 原始识别文本 - - Returns: - 处理后的文本 - """ - try: - processed_text = text - - # 应用自定义词汇表 - for word, pronunciation in self.custom_lexicon.items(): - processed_text = processed_text.replace(word, word) - - # 应用专业术语 - for term, expansion in self.technical_terms.items(): - processed_text = processed_text.replace(term, term) - - # 应用脏话过滤 - if self.recognition_params['enable_profanity_filter']: - profanity_list = self.profanity_words.get(self.language, []) - replace_char = self.recognition_params['profanity_replace_char'] - for profanity in profanity_list: - processed_text = processed_text.replace(profanity, replace_char * len(profanity)) - - # 应用上下文偏向短语 - for phrase in self.recognition_params['contextual_phrases']: - # 这里可以实现更复杂的上下文匹配逻辑 - pass - - return processed_text - - except Exception as e: - print(f"✗ 后处理识别结果失败: {e}") - return text - - def _cache_recognition_result(self, text: str, result: Dict[str, Any]): - """ - 缓存识别结果 - - Args: - text: 识别文本 - result: 识别结果 - """ - try: - # 简单的LRU缓存实现 - if len(self.recognition_cache) >= self.cache_size: - # 移除最旧的条目 - oldest_key = next(iter(self.recognition_cache)) - del self.recognition_cache[oldest_key] - - self.recognition_cache[text] = { - 'result': result, - 'timestamp': time.time() - } - except Exception as e: - print(f"✗ 缓存识别结果失败: {e}") - - def set_language(self, language: str): - """ - 设置识别语言 - - Args: - language: 语言代码 (如 'zh-CN', 'en-US') - """ - self.language = language - print(f"✓ 识别语言设置为: {language}") - - def set_recognition_callback(self, callback: Callable): - """ - 设置识别回调函数 - - Args: - callback: 回调函数 - """ - self.recognition_callback = callback - - def set_partial_result_callback(self, callback: Callable): - """ - 设置部分结果回调函数 - - Args: - callback: 回调函数 - """ - self.partial_result_callback = callback - - def set_error_callback(self, callback: Callable): - """ - 设置错误回调函数 - - Args: - callback: 回调函数 - """ - self.error_callback = callback - - def set_voice_activity_callback(self, callback: Callable): - """ - 设置语音活动回调函数 - - Args: - callback: 回调函数 - """ - self.voice_activity_callback = callback - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - # 计算每分钟单词数 - if self.stats['total_voice_duration'] > 0: - self.stats['words_per_minute'] = ( - self.stats['recognitions_processed'] * 10 / # 假设每次识别约10个单词 - (self.stats['total_voice_duration'] / 60.0) - ) - - return self.stats.copy() - - def get_available_languages(self) -> List[str]: - """ - 获取支持的语言列表 - - Returns: - 语言代码列表 - """ - # 模拟支持的语言列表 - return [ - 'zh-CN', # 简体中文 - 'zh-TW', # 繁体中文 - 'en-US', # 英语(美国) - 'en-GB', # 英语(英国) - 'ja-JP', # 日语 - 'ko-KR', # 韩语 - 'fr-FR', # 法语 - 'de-DE', # 德语 - 'es-ES', # 西班牙语 - 'ru-RU', # 俄语 - 'ar-SA', # 阿拉伯语 - 'pt-BR', # 葡萄牙语(巴西) - 'it-IT', # 意大利语 - 'nl-NL', # 荷兰语 - 'pl-PL', # 波兰语 - 'th-TH', # 泰语 - 'vi-VN', # 越南语 - 'tr-TR', # 土耳其语 - 'cs-CZ', # 捷克语 - 'uk-UA' # 乌克兰语 - ] - - def set_silence_threshold(self, threshold: float): - """ - 设置静音阈值 - - Args: - threshold: 静音阈值 - """ - self.recognition_params['silence_threshold'] = max(0.0, min(1.0, threshold)) - print(f"✓ 静音阈值设置为: {threshold}") - - def set_voice_threshold(self, threshold: float): - """ - 设置语音阈值 - - Args: - threshold: 语音阈值 - """ - self.recognition_params['voice_threshold'] = max(0.0, min(1.0, threshold)) - print(f"✓ 语音阈值设置为: {threshold}") - - def set_silence_duration(self, duration: float): - """ - 设置静音检测时长 - - Args: - duration: 静音检测时长(秒) - """ - self.recognition_params['silence_duration'] = max(0.1, duration) - print(f"✓ 静音检测时长设置为: {duration} 秒") - - def set_min_voice_duration(self, duration: float): - """ - 设置最短语音时长 - - Args: - duration: 最短语音时长(秒) - """ - self.recognition_params['min_voice_duration'] = max(0.1, duration) - print(f"✓ 最短语音时长设置为: {duration} 秒") - - def set_max_recording_duration(self, duration: float): - """ - 设置最大录音时长 - - Args: - duration: 最大录音时长(秒) - """ - self.recognition_params['max_recording_duration'] = max(1.0, duration) - print(f"✓ 最大录音时长设置为: {duration} 秒") - - def is_voice_detected(self) -> bool: - """ - 检查是否检测到语音 - - Returns: - 是否检测到语音 - """ - return self.is_voice_detected - - def get_current_buffer_duration(self) -> float: - """ - 获取当前缓冲区音频时长 - - Returns: - 音频时长(秒) - """ - try: - total_samples = sum(len(chunk) for chunk in self.audio_buffer) - return total_samples / self.sample_rate - except Exception as e: - print(f"✗ 计算缓冲区时长失败: {e}") - return 0.0 - - def clear_audio_buffer(self): - """清空音频缓冲区""" - self.audio_buffer = [] - self.silence_counter = 0 - self.voice_activity_counter = 0 - self.is_voice_detected = False - self.voice_start_time = 0.0 - self.last_voice_time = 0.0 - print("✓ 音频缓冲区已清空") - - def add_custom_vocabulary(self, words: List[str]): - """ - 添加自定义词汇表 - - Args: - words: 词汇列表 - """ - try: - for word in words: - # 简单处理,实际应用中可能需要提供发音信息 - self.custom_lexicon[word] = word.upper() - print(f"✓ 添加自定义词汇: {words}") - except Exception as e: - print(f"✗ 添加自定义词汇失败: {e}") - - def set_recognition_mode(self, mode: str): - """ - 设置识别模式 - - Args: - mode: 识别模式 ('command', 'dictation', 'search') - """ - valid_modes = ['command', 'dictation', 'search'] - if mode in valid_modes: - self.recognition_mode = mode - print(f"✓ 识别模式设置为: {mode}") - else: - print(f"✗ 无效的识别模式: {mode}") - - def enable_punctuation(self, enable: bool = True): - """ - 启用/禁用标点符号识别 - - Args: - enable: 是否启用标点符号识别 - """ - self.recognition_params['enable_punctuation'] = enable - state = "启用" if enable else "禁用" - print(f"✓ 标点符号识别已{state}") - - def enable_profanity_filter(self, enable: bool = True): - """ - 启用/禁用脏话过滤 - - Args: - enable: 是否启用脏话过滤 - """ - self.recognition_params['enable_profanity_filter'] = enable - state = "启用" if enable else "禁用" - print(f"✓ 脏话过滤已{state}") - - def set_alternative_count(self, count: int): - """ - 设置备选结果数量 - - Args: - count: 备选结果数量 - """ - count = max(0, min(30, count)) # 限制在0-30之间 - self.recognition_params['max_alternatives'] = count - print(f"✓ 备选结果数量设置为: {count}") - - def enable_word_timing(self, enable: bool = True): - """ - 启用/禁用单词时间戳 - - Args: - enable: 是否启用单词时间戳 - """ - self.recognition_params['enable_word_timing'] = enable - state = "启用" if enable else "禁用" - print(f"✓ 单词时间戳已{state}") - - def set_max_alternatives(self, max_alternatives: int): - """ - 设置最大备选结果数 - - Args: - max_alternatives: 最大备选结果数 - """ - self.recognition_params['max_alternatives'] = max(1, max_alternatives) - print(f"✓ 最大备选结果数设置为: {max_alternatives}") - - def enable_speaker_diarization(self, enable: bool = True): - """ - 启用/禁用说话人分离 - - Args: - enable: 是否启用说话人分离 - """ - self.recognition_params['enable_speaker_diarization'] = enable - state = "启用" if enable else "禁用" - print(f"✓ 说话人分离已{state}") - - def set_speaker_count(self, min_speakers: int, max_speakers: int): - """ - 设置说话人数量范围 - - Args: - min_speakers: 最少说话人数 - max_speakers: 最多说话人数 - """ - min_speakers = max(1, min_speakers) - max_speakers = max(min_speakers, max_speakers) - self.recognition_params['min_speaker_count'] = min_speakers - self.recognition_params['max_speaker_count'] = max_speakers - print(f"✓ 说话人数量范围设置为: {min_speakers}-{max_speakers}") - - def enable_audio_preprocessing(self, enable: bool = True): - """ - 启用/禁用音频预处理 - - Args: - enable: 是否启用音频预处理 - """ - self.recognition_params['enable_audio_preprocessing'] = enable - state = "启用" if enable else "禁用" - print(f"✓ 音频预处理已{state}") - - def set_noise_reduction_level(self, level: float): - """ - 设置噪声消除级别 - - Args: - level: 噪声消除级别 (0.0-1.0) - """ - level = max(0.0, min(1.0, level)) - self.recognition_params['noise_reduction_level'] = level - print(f"✓ 噪声消除级别设置为: {level}") - - def enable_real_time_feedback(self, enable: bool = True): - """ - 启用/禁用实时反馈 - - Args: - enable: 是否启用实时反馈 - """ - self.recognition_params['enable_real_time_feedback'] = enable - state = "启用" if enable else "禁用" - print(f"✓ 实时反馈已{state}") - - def set_confidence_threshold(self, threshold: float): - """ - 设置置信度阈值 - - Args: - threshold: 置信度阈值 (0.0-1.0) - """ - threshold = max(0.0, min(1.0, threshold)) - self.recognition_params['confidence_threshold'] = threshold - print(f"✓ 置信度阈值设置为: {threshold}") - - def enable_contextual_biasing(self, enable: bool = True): - """ - 启用/禁用上下文偏向 - - Args: - enable: 是否启用上下文偏向 - """ - self.recognition_params['enable_contextual_biasing'] = enable - state = "启用" if enable else "禁用" - print(f"✓ 上下文偏向已{state}") - - def add_contextual_phrases(self, phrases: List[str]): - """ - 添加上下文短语 - - Args: - phrases: 短语列表 - """ - try: - self.recognition_params['contextual_phrases'].extend(phrases) - print(f"✓ 添加上下文短语: {phrases}") - except Exception as e: - print(f"✗ 添加上下文短语失败: {e}") - - def set_model_variant(self, variant: str): - """ - 设置模型变体 - - Args: - variant: 模型变体 ('default', 'command_and_search', 'phone_call', 'video') - """ - valid_variants = ['default', 'command_and_search', 'phone_call', 'video'] - if variant in valid_variants: - self.recognition_params['model_variant'] = variant - print(f"✓ 模型变体设置为: {variant}") - else: - print(f"✗ 无效的模型变体: {variant}") - - def enable_enhanced_models(self, enable: bool = True): - """ - 启用/禁用增强模型 - - Args: - enable: 是否启用增强模型 - """ - self.recognition_params['enable_enhanced_models'] = enable - state = "启用" if enable else "禁用" - print(f"✓ 增强模型已{state}") - - def set_recognition_timeout(self, timeout: float): - """ - 设置识别超时时间 - - Args: - timeout: 超时时间(秒) - """ - timeout = max(0.1, timeout) - self.recognition_params['recognition_timeout'] = timeout - print(f"✓ 识别超时时间设置为: {timeout} 秒") - - def set_profanity_replace_char(self, char: str): - """ - 设置脏话替换字符 - - Args: - char: 替换字符 - """ - self.recognition_params['profanity_replace_char'] = char - print(f"✓ 脏话替换字符设置为: {char}") - - def get_cached_results(self) -> Dict[str, Any]: - """ - 获取缓存的识别结果 - - Returns: - 缓存结果字典 - """ - return self.recognition_cache.copy() - - def clear_cache(self): - """清空识别结果缓存""" - self.recognition_cache.clear() - print("✓ 识别结果缓存已清空") - - def set_cache_size(self, size: int): - """ - 设置缓存大小 - - Args: - size: 缓存大小 - """ - self.cache_size = max(1, size) - print(f"✓ 缓存大小设置为: {size}") - - def export_stats(self, filename: str) -> bool: - """ - 导出统计信息到文件 - - Args: - filename: 文件名 - - Returns: - 是否导出成功 - """ - try: - stats_data = { - 'timestamp': time.time(), - 'stats': self.get_stats(), - 'params': self.recognition_params, - 'language': self.language, - 'mode': self.recognition_mode - } - - with open(filename, 'w', encoding='utf-8') as f: - json.dump(stats_data, f, ensure_ascii=False, indent=2) - - print(f"✓ 统计信息已导出到: {filename}") - return True - except Exception as e: - print(f"✗ 导出统计信息失败: {e}") - return False - - def import_custom_lexicon(self, filename: str) -> bool: - """ - 从文件导入自定义词汇表 - - Args: - filename: 文件名 - - Returns: - 是否导入成功 - """ - try: - with open(filename, 'r', encoding='utf-8') as f: - lexicon_data = json.load(f) - self.custom_lexicon.update(lexicon_data) - - print(f"✓ 自定义词汇表已从 {filename} 导入") - return True - except Exception as e: - print(f"✗ 导入自定义词汇表失败: {e}") - return False - - def export_custom_lexicon(self, filename: str) -> bool: - """ - 导出自定义词汇表到文件 - - Args: - filename: 文件名 - - Returns: - 是否导出成功 - """ - try: - with open(filename, 'w', encoding='utf-8') as f: - json.dump(self.custom_lexicon, f, ensure_ascii=False, indent=2) - - print(f"✓ 自定义词汇表已导出到: {filename}") - return True - except Exception as e: - print(f"✗ 导出自定义词汇表失败: {e}") - return False - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'recognitions_processed': 0, - 'recognition_errors': 0, - 'total_recognition_time': 0.0, - 'average_recognition_time': 0.0, - 'voice_segments_detected': 0, - 'total_voice_duration': 0.0, - 'words_per_minute': 0.0, - 'confidence_average': 0.0, - 'recognition_accuracy': 0.0 - } - print("✓ 统计信息已重置") - - def set_speaker_info(self, speaker_id: str, info: Dict[str, Any]): - """ - 设置说话人信息 - - Args: - speaker_id: 说话人ID - info: 说话人信息 - """ - self.speaker_info[speaker_id] = info - print(f"✓ 说话人 {speaker_id} 信息已设置") - - def get_speaker_info(self, speaker_id: str) -> Dict[str, Any]: - """ - 获取说话人信息 - - Args: - speaker_id: 说话人ID - - Returns: - 说话人信息 - """ - return self.speaker_info.get(speaker_id, {}) - - def remove_speaker_info(self, speaker_id: str): - """ - 移除说话人信息 - - Args: - speaker_id: 说话人ID - """ - if speaker_id in self.speaker_info: - del self.speaker_info[speaker_id] - print(f"✓ 说话人 {speaker_id} 信息已移除") - - def enable_continuous_recognition(self, enable: bool = True): - """ - 启用/禁用连续识别 - - Args: - enable: 是否启用连续识别 - """ - # 连续识别是默认行为,这里只是记录设置 - state = "启用" if enable else "禁用" - print(f"✓ 连续识别已{state}") - - def set_adaptive_sensitivity(self, enable: bool = True): - """ - 启用/禁用自适应灵敏度 - - Args: - enable: 是否启用自适应灵敏度 - """ - # 在实际实现中,这会根据环境噪声自动调整阈值 - state = "启用" if enable else "禁用" - print(f"✓ 自适应灵敏度已{state}") - - def enable_dialect_support(self, enable: bool = True): - """ - 启用/禁用方言支持 - - Args: - enable: 是否启用方言支持 - """ - state = "启用" if enable else "禁用" - print(f"✓ 方言支持已{state}") - - def set_dialect_preference(self, dialects: List[str]): - """ - 设置方言偏好 - - Args: - dialects: 方言列表 - """ - print(f"✓ 方言偏好设置为: {dialects}") - - def enable_accent_adaptation(self, enable: bool = True): - """ - 启用/禁用口音适应 - - Args: - enable: 是否启用口音适应 - """ - state = "启用" if enable else "禁用" - print(f"✓ 口音适应已{state}") - - def add_accent_sample(self, audio_data: np.ndarray, accent_type: str): - """ - 添加口音样本用于适应 - - Args: - audio_data: 音频数据 - accent_type: 口音类型 - """ - print(f"✓ 已添加 {accent_type} 口音样本") - - def enable_domain_optimization(self, enable: bool = True): - """ - 启用/禁用领域优化 - - Args: - enable: 是否启用领域优化 - """ - state = "启用" if enable else "禁用" - print(f"✓ 领域优化已{state}") - - def set_domain_context(self, domain: str): - """ - 设置领域上下文 - - Args: - domain: 领域类型 ('medical', 'legal', 'technical', 'general') - """ - valid_domains = ['medical', 'legal', 'technical', 'general'] - if domain in valid_domains: - print(f"✓ 领域上下文设置为: {domain}") - else: - print(f"✗ 无效的领域类型: {domain}") - - def enable_low_latency_mode(self, enable: bool = True): - """ - 启用/禁用低延迟模式 - - Args: - enable: 是否启用低延迟模式 - """ - state = "启用" if enable else "禁用" - print(f"✓ 低延迟模式已{state}") - - def set_latency_preference(self, preference: str): - """ - 设置延迟偏好 - - Args: - preference: 延迟偏好 ('accuracy', 'speed', 'balanced') - """ - valid_preferences = ['accuracy', 'speed', 'balanced'] - if preference in valid_preferences: - print(f"✓ 延迟偏好设置为: {preference}") - else: - print(f"✗ 无效的延迟偏好: {preference}") - - def enable_offline_recognition(self, enable: bool = True): - """ - 启用/禁用离线识别 - - Args: - enable: 是否启用离线识别 - """ - state = "启用" if enable else "禁用" - print(f"✓ 离线识别已{state}") - - def update_offline_model(self, model_path: str): - """ - 更新离线识别模型 - - Args: - model_path: 模型路径 - """ - print(f"✓ 离线识别模型更新为: {model_path}") - - def set_recognition_sensitivity(self, sensitivity: float): - """ - 设置识别灵敏度 - - Args: - sensitivity: 灵敏度 (0.0-1.0) - """ - sensitivity = max(0.0, min(1.0, sensitivity)) - # 调整语音和静音阈值 - self.recognition_params['voice_threshold'] = 0.005 + sensitivity * 0.02 - self.recognition_params['silence_threshold'] = 0.001 + sensitivity * 0.01 - print(f"✓ 识别灵敏度设置为: {sensitivity}") - - def enable_auto_punctuation(self, enable: bool = True): - """ - 启用/禁用自动标点符号 - - Args: - enable: 是否启用自动标点符号 - """ - self.recognition_params['enable_punctuation'] = enable - state = "启用" if enable else "禁用" - print(f"✓ 自动标点符号已{state}") - - def set_punctuation_style(self, style: str): - """ - 设置标点符号风格 - - Args: - style: 标点符号风格 ('casual', 'formal', 'technical') - """ - valid_styles = ['casual', 'formal', 'technical'] - if style in valid_styles: - print(f"✓ 标点符号风格设置为: {style}") - else: - print(f"✗ 无效的标点符号风格: {style}") - - def enable_emotion_detection(self, enable: bool = True): - """ - 启用/禁用情感检测 - - Args: - enable: 是否启用情感检测 - """ - state = "启用" if enable else "禁用" - print(f"✓ 情感检测已{state}") - - def get_emotion_analysis(self, text: str) -> Dict[str, float]: - """ - 获取文本情感分析 - - Args: - text: 文本内容 - - Returns: - 情感分析结果 - """ - # 模拟情感分析结果 - return { - 'positive': 0.6, - 'negative': 0.2, - 'neutral': 0.2, - 'confidence': 0.8 - } - - def enable_intent_recognition(self, enable: bool = True): - """ - 启用/禁用意图识别 - - Args: - enable: 是否启用意图识别 - """ - state = "启用" if enable else "禁用" - print(f"✓ 意图识别已{state}") - - def set_intent_context(self, context: str): - """ - 设置意图识别上下文 - - Args: - context: 上下文类型 - """ - print(f"✓ 意图识别上下文设置为: {context}") - - def recognize_with_context(self, audio_data: np.ndarray, context: str = None) -> Dict[str, Any]: - """ - 带上下文的语音识别 - - Args: - audio_data: 音频数据 - context: 上下文信息 - - Returns: - 识别结果 - """ - try: - # 执行识别 - result = self._perform_recognition() - - # 添加上下文信息 - if context: - result['context'] = context - - return result - except Exception as e: - print(f"✗ 带上下文的语音识别失败: {e}") - return {} - - def batch_recognize(self, audio_segments: List[np.ndarray]) -> List[Dict[str, Any]]: - """ - 批量语音识别 - - Args: - audio_segments: 音频片段列表 - - Returns: - 识别结果列表 - """ - try: - results = [] - for i, segment in enumerate(audio_segments): - print(f"✓ 处理音频片段 {i+1}/{len(audio_segments)}") - # 临时替换音频缓冲区 - original_buffer = self.audio_buffer - self.audio_buffer = [segment] - - result = self._perform_recognition() - results.append(result) - - # 恢复音频缓冲区 - self.audio_buffer = original_buffer - - return results - except Exception as e: - print(f"✗ 批量语音识别失败: {e}") - return [] - - def recognize_with_feedback(self, audio_data: np.ndarray, - feedback_callback: Callable[[str], None] = None) -> Dict[str, Any]: - """ - 带反馈的语音识别 - - Args: - audio_data: 音频数据 - feedback_callback: 反馈回调函数 - - Returns: - 识别结果 - """ - try: - # 模拟实时反馈过程 - if feedback_callback and self.recognition_params['enable_real_time_feedback']: - feedback_callback("开始识别...") - time.sleep(0.05) # 模拟处理时间 - feedback_callback("分析音频特征...") - time.sleep(0.05) - feedback_callback("匹配语言模型...") - time.sleep(0.05) - feedback_callback("生成识别结果...") - - result = self._perform_recognition() - - if feedback_callback: - feedback_callback(f"识别完成: {result.get('text', '')}") - - return result - except Exception as e: - print(f"✗ 带反馈的语音识别失败: {e}") - return {} - - def set_recognition_profile(self, profile: str): - """ - 设置识别配置文件 - - Args: - profile: 配置文件 ('fast', 'accurate', 'balanced') - """ - valid_profiles = ['fast', 'accurate', 'balanced'] - if profile in valid_profiles: - # 根据配置文件调整参数 - if profile == 'fast': - self.recognition_params['enable_enhanced_models'] = False - self.recognition_params['max_alternatives'] = 1 - self.recognition_params['enable_word_timing'] = False - elif profile == 'accurate': - self.recognition_params['enable_enhanced_models'] = True - self.recognition_params['max_alternatives'] = 5 - self.recognition_params['enable_word_timing'] = True - elif profile == 'balanced': - self.recognition_params['enable_enhanced_models'] = True - self.recognition_params['max_alternatives'] = 3 - self.recognition_params['enable_word_timing'] = True - - print(f"✓ 识别配置文件设置为: {profile}") - else: - print(f"✗ 无效的配置文件: {profile}") - - def enable_multilingual_recognition(self, enable: bool = True): - """ - 启用/禁用多语言识别 - - Args: - enable: 是否启用多语言识别 - """ - state = "启用" if enable else "禁用" - print(f"✓ 多语言识别已{state}") - - def set_multilingual_languages(self, languages: List[str]): - """ - 设置多语言识别的语言列表 - - Args: - languages: 语言列表 - """ - print(f"✓ 多语言识别语言设置为: {languages}") - - def recognize_multilingual(self, audio_data: np.ndarray) -> Dict[str, Any]: - """ - 多语言语音识别 - - Args: - audio_data: 音频数据 - - Returns: - 识别结果 - """ - try: - # 执行识别 - result = self._perform_recognition() - - # 添加多语言支持信息 - result['multilingual'] = True - result['detected_languages'] = [self.language] - - return result - except Exception as e: - print(f"✗ 多语言语音识别失败: {e}") - return {} - - def enable_voice_profile_adaptation(self, enable: bool = True): - """ - 启用/禁用语音配置文件适应 - - Args: - enable: 是否启用语音配置文件适应 - """ - state = "启用" if enable else "禁用" - print(f"✓ 语音配置文件适应已{state}") - - def create_voice_profile(self, profile_name: str, audio_samples: List[np.ndarray]) -> bool: - """ - 创建语音配置文件 - - Args: - profile_name: 配置文件名称 - audio_samples: 音频样本列表 - - Returns: - 是否创建成功 - """ - try: - print(f"✓ 语音配置文件 '{profile_name}' 已创建") - return True - except Exception as e: - print(f"✗ 创建语音配置文件失败: {e}") - return False - - def set_active_voice_profile(self, profile_name: str): - """ - 设置活动语音配置文件 - - Args: - profile_name: 配置文件名称 - """ - print(f"✓ 活动语音配置文件设置为: {profile_name}") - - def enable_custom_acoustic_model(self, enable: bool = True): - """ - 启用/禁用自定义声学模型 - - Args: - enable: 是否启用自定义声学模型 - """ - state = "启用" if enable else "禁用" - print(f"✓ 自定义声学模型已{state}") - - def load_custom_acoustic_model(self, model_path: str): - """ - 加载自定义声学模型 - - Args: - model_path: 模型路径 - """ - print(f"✓ 自定义声学模型已加载: {model_path}") - - def enable_custom_language_model(self, enable: bool = True): - """ - 启用/禁用自定义语言模型 - - Args: - enable: 是否启用自定义语言模型 - """ - state = "启用" if enable else "禁用" - print(f"✓ 自定义语言模型已{state}") - - def load_custom_language_model(self, model_path: str): - """ - 加载自定义语言模型 - - Args: - model_path: 模型路径 - """ - print(f"✓ 自定义语言模型已加载: {model_path}") - - def set_recognition_priority(self, priority: str): - """ - 设置识别优先级 - - Args: - priority: 优先级 ('realtime', 'quality', 'balanced') - """ - valid_priorities = ['realtime', 'quality', 'balanced'] - if priority in valid_priorities: - print(f"✓ 识别优先级设置为: {priority}") - else: - print(f"✗ 无效的优先级: {priority}") - - def enable_confidence_scoring(self, enable: bool = True): - """ - 启用/禁用置信度评分 - - Args: - enable: 是否启用置信度评分 - """ - state = "启用" if enable else "禁用" - print(f"✓ 置信度评分已{state}") - - def set_confidence_scoring_mode(self, mode: str): - """ - 设置置信度评分模式 - - Args: - mode: 评分模式 ('basic', 'detailed', 'comprehensive') - """ - valid_modes = ['basic', 'detailed', 'comprehensive'] - if mode in valid_modes: - print(f"✓ 置信度评分模式设置为: {mode}") - else: - print(f"✗ 无效的评分模式: {mode}") - - def enable_lattice_generation(self, enable: bool = True): - """ - 启用/禁用格子生成 - - Args: - enable: 是否启用格子生成 - """ - state = "启用" if enable else "禁用" - print(f"✓ 格子生成已{state}") - - def get_recognition_lattice(self) -> Dict[str, Any]: - """ - 获取识别格子 - - Returns: - 识别格子数据 - """ - # 模拟格子数据 - return { - 'nodes': [], - 'edges': [], - 'timestamp': time.time() - } - - def enable_audio_segmentation(self, enable: bool = True): - """ - 启用/禁用音频分割 - - Args: - enable: 是否启用音频分割 - """ - state = "启用" if enable else "禁用" - print(f"✓ 音频分割已{state}") - - def set_segmentation_strategy(self, strategy: str): - """ - 设置分割策略 - - Args: - strategy: 分割策略 ('silence', 'content', 'hybrid') - """ - valid_strategies = ['silence', 'content', 'hybrid'] - if strategy in valid_strategies: - print(f"✓ 分割策略设置为: {strategy}") - else: - print(f"✗ 无效的分割策略: {strategy}") - - def recognize_with_correction(self, audio_data: np.ndarray, - correction_history: List[str] = None) -> Dict[str, Any]: - """ - 带纠错的语音识别 - - Args: - audio_data: 音频数据 - correction_history: 纠正历史 - - Returns: - 识别结果 - """ - try: - result = self._perform_recognition() - - # 应用纠正历史 - if correction_history: - result['correction_history'] = correction_history - - return result - except Exception as e: - print(f"✗ 带纠错的语音识别失败: {e}") - return {} - - def enable_robust_recognition(self, enable: bool = True): - """ - 启用/禁用鲁棒识别 - - Args: - enable: 是否启用鲁棒识别 - """ - state = "启用" if enable else "禁用" - print(f"✓ 鲁棒识别已{state}") - - def set_robustness_level(self, level: float): - """ - 设置鲁棒性级别 - - Args: - level: 鲁棒性级别 (0.0-1.0) - """ - level = max(0.0, min(1.0, level)) - print(f"✓ 鲁棒性级别设置为: {level}") - - def enable_adaptive_noise_cancellation(self, enable: bool = True): - """ - 启用/禁用自适应噪声消除 - - Args: - enable: 是否启用自适应噪声消除 - """ - state = "启用" if enable else "禁用" - print(f"✓ 自适应噪声消除已{state}") - - def set_noise_cancellation_profile(self, profile: str): - """ - 设置噪声消除配置文件 - - Args: - profile: 配置文件 ('light', 'medium', 'heavy', 'adaptive') - """ - valid_profiles = ['light', 'medium', 'heavy', 'adaptive'] - if profile in valid_profiles: - print(f"✓ 噪声消除配置文件设置为: {profile}") - else: +""" +语音识别器 +负责将语音转换为文本 +""" + +import time +import numpy as np +from typing import Dict, Any, List, Callable +import threading +import json +import re + +class SpeechRecognizer: + """ + 语音识别器 + 负责将语音转换为文本,支持多种语言和识别引擎 + """ + + def __init__(self, plugin): + """ + 初始化语音识别器 + + Args: + plugin: 语音识别和合成插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + self.is_listening = False + self.is_processing = False + + # 识别配置 + self.language = plugin.config.get('language', 'zh-CN') + self.sample_rate = plugin.config.get('sample_rate', 16000) + self.channels = plugin.config.get('channels', 1) + + # 识别状态 + self.recognition_thread = None + self.audio_buffer = [] + self.silence_counter = 0 + self.voice_activity_counter = 0 + self.is_voice_detected = False + self.voice_start_time = 0.0 + self.last_voice_time = 0.0 + + # 识别参数 + self.recognition_params = { + 'silence_threshold': 0.005, + 'voice_threshold': 0.01, + 'silence_duration': 1.0, # 秒 + 'min_voice_duration': 0.3, # 秒 + 'max_recording_duration': 30.0, # 秒 + 'confidence_threshold': 0.7, # 置信度阈值 + 'enable_punctuation': True, # 启用标点符号 + 'enable_profanity_filter': True, # 启用脏话过滤 + 'max_alternatives': 3, # 最大备选结果数 + 'enable_word_timing': True, # 启用单词时间戳 + 'enable_speaker_diarization': False, # 启用说话人分离 + 'min_speaker_count': 1, # 最少说话人数 + 'max_speaker_count': 6, # 最多说话人数 + 'enable_contextual_biasing': True, # 启用上下文偏向 + 'contextual_phrases': [], # 上下文短语 + 'model_variant': 'default', # 模型变体 + 'enable_enhanced_models': True, # 启用增强模型 + 'recognition_timeout': 60.0, # 识别超时时间 + 'enable_real_time_feedback': True, # 启用实时反馈 + 'profanity_replace_char': '*', # 脏话替换字符 + 'enable_audio_preprocessing': True, # 启用音频预处理 + 'noise_reduction_level': 0.3, # 噪声消除级别 + } + + # 统计信息 + self.stats = { + 'recognitions_processed': 0, + 'recognition_errors': 0, + 'total_recognition_time': 0.0, + 'average_recognition_time': 0.0, + 'voice_segments_detected': 0, + 'total_voice_duration': 0.0, + 'words_per_minute': 0.0, + 'confidence_average': 0.0, + 'recognition_accuracy': 0.0 + } + + # 回调函数 + self.recognition_callback = None + self.partial_result_callback = None + self.error_callback = None + self.voice_activity_callback = None + + # 识别结果缓存 + self.recognition_cache = {} + self.cache_size = 100 + + # 识别模式 + self.recognition_mode = 'dictation' # 'command', 'dictation', 'search' + + # 自定义词汇表 + self.custom_lexicon = {} # 词汇到发音的映射 + + # 说话人信息 + self.speaker_info = {} + + # 模拟的识别结果数据库 + self.simulated_database = { + 'zh-CN': { + '你好世界': 0.95, + '今天天气怎么样': 0.92, + '我想测试语音识别功能': 0.90, + '这是一个语音识别测试': 0.88, + '语音识别插件工作正常': 0.91, + '请说出您要识别的内容': 0.89, + '语音识别技术非常有用': 0.87, + '我可以识别多种语言': 0.85, + '识别准确率很高': 0.86, + '支持实时语音识别': 0.84, + '语音命令控制': 0.93, + '打开游戏设置': 0.94, + '保存当前进度': 0.92, + '退出应用程序': 0.90, + '增加音量': 0.88, + '降低音量': 0.87, + '暂停游戏': 0.91, + '继续游戏': 0.90, + '重新开始': 0.89, + '显示地图': 0.88 + }, + 'en-US': { + 'Hello world': 0.95, + 'How is the weather today': 0.92, + 'I want to test speech recognition': 0.90, + 'This is a speech recognition test': 0.88, + 'Speech recognition plugin works fine': 0.91, + 'Please speak what you want to recognize': 0.89, + 'Speech recognition technology is very useful': 0.87, + 'I can recognize multiple languages': 0.85, + 'Recognition accuracy is high': 0.86, + 'Support real-time speech recognition': 0.84, + 'Voice command control': 0.93, + 'Open game settings': 0.94, + 'Save current progress': 0.92, + 'Exit application': 0.90, + 'Increase volume': 0.88, + 'Decrease volume': 0.87, + 'Pause game': 0.91, + 'Resume game': 0.90, + 'Restart': 0.89, + 'Show map': 0.88 + } + } + + # 专业术语词汇表 + self.technical_terms = { + 'GPU': 'G P U', + 'CPU': 'C P U', + 'RAM': 'R A M', + 'API': 'A P I', + 'SDK': 'S D K', + 'FPS': 'F P S', + 'UI': 'U I', + 'UX': 'U X' + } + + # 脏话过滤词典 + self.profanity_words = { + 'zh-CN': ['脏话1', '脏话2', '脏话3'], + 'en-US': ['profanity1', 'profanity2', 'profanity3'] + } + + print("✓ 语音识别器已创建") + + def initialize(self) -> bool: + """ + 初始化语音识别器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化识别引擎(这里简化处理,实际项目中需要加载相应的识别模型或库) + print("✓ 语音识别引擎初始化完成") + + self.initialized = True + print("✓ 语音识别器初始化完成") + return True + + except Exception as e: + print(f"✗ 语音识别器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用语音识别器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 语音识别器未初始化") + return False + + self.enabled = True + print("✓ 语音识别器已启用") + return True + + except Exception as e: + print(f"✗ 语音识别器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用语音识别器""" + try: + self.stop_listening() + self.enabled = False + print("✓ 语音识别器已禁用") + + except Exception as e: + print(f"✗ 语音识别器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理语音识别器资源""" + try: + self.disable() + self.initialized = False + print("✓ 语音识别器资源已清理") + + except Exception as e: + print(f"✗ 语音识别器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新语音识别器状态 + + Args: + dt: 时间增量 + """ + # 这里可以处理定期更新的任务 + pass + + def start_listening(self) -> bool: + """ + 开始语音监听 + + Returns: + 是否开始成功 + """ + try: + if not self.enabled: + print("✗ 语音识别器未启用") + return False + + if self.is_listening: + print("⚠ 已经在监听中") + return True + + self.is_listening = True + self.audio_buffer = [] + self.silence_counter = 0 + self.voice_activity_counter = 0 + self.is_voice_detected = False + self.voice_start_time = 0.0 + self.last_voice_time = 0.0 + + # 启动语音识别线程 + self.recognition_thread = threading.Thread(target=self._recognition_worker, daemon=True) + self.recognition_thread.start() + + # 启动录音 + if self.plugin.speech_manager: + self.plugin.speech_manager.start_recording() + + print("✓ 开始语音监听") + return True + + except Exception as e: + print(f"✗ 开始语音监听失败: {e}") + import traceback + traceback.print_exc() + return False + + def stop_listening(self): + """停止语音监听""" + try: + self.is_listening = False + + if self.recognition_thread and self.recognition_thread.is_alive(): + self.recognition_thread.join(timeout=1.0) + + # 停止录音 + if self.plugin.speech_manager: + self.plugin.speech_manager.stop_recording() + + print("✓ 停止语音监听") + + except Exception as e: + print(f"✗ 停止语音监听失败: {e}") + import traceback + traceback.print_exc() + + def process_audio(self): + """处理音频数据""" + try: + if not self.is_listening or not self.enabled: + return + + # 从语音管理器获取音频数据 + if self.plugin.speech_manager: + audio_chunk = self.plugin.speech_manager.get_audio_chunk() + + # 可选的音频预处理 + if self.recognition_params['enable_audio_preprocessing']: + audio_chunk = self.plugin.speech_manager.apply_speech_enhancement(audio_chunk) + + # 检测语音活动 + is_voice = self._detect_voice_activity(audio_chunk) + + current_time = time.time() + + if is_voice: + # 检测到语音 + if not self.is_voice_detected: + self.is_voice_detected = True + self.voice_start_time = current_time + self.voice_activity_counter = 1 + self.silence_counter = 0 + print("✓ 检测到语音活动开始") + + # 调用语音活动回调 + if self.voice_activity_callback: + self.voice_activity_callback(True) + else: + self.voice_activity_counter += 1 + self.last_voice_time = current_time + + else: + # 检测到静音 + if self.is_voice_detected: + self.silence_counter += 1 + + # 检查是否超过静音时长阈值 + silence_duration = self.silence_counter * len(audio_chunk) / self.sample_rate + if silence_duration >= self.recognition_params['silence_duration']: + # 语音活动结束 + voice_duration = current_time - self.voice_start_time + if voice_duration >= self.recognition_params['min_voice_duration']: + print(f"✓ 检测到语音活动结束,时长: {voice_duration:.2f}秒") + self._process_voice_segment() + else: + print("⚠ 语音片段太短,忽略") + + # 重置状态 + self.is_voice_detected = False + self.voice_activity_counter = 0 + self.silence_counter = 0 + + # 调用语音活动回调 + if self.voice_activity_callback: + self.voice_activity_callback(False) + + # 将音频数据添加到缓冲区 + self.audio_buffer.append(audio_chunk) + + # 检查是否超过最大录音时长 + total_samples = sum(len(chunk) for chunk in self.audio_buffer) + recording_time = total_samples / self.sample_rate + + if recording_time >= self.recognition_params['max_recording_duration']: + print("⚠ 达到最大录音时长,强制处理") + self._process_voice_segment() + + except Exception as e: + print(f"✗ 处理音频数据失败: {e}") + import traceback + traceback.print_exc() + + def _recognition_worker(self): + """语音识别工作线程""" + try: + while self.is_listening and self.enabled: + self.process_audio() + time.sleep(0.01) # 10ms延迟 + + except Exception as e: + print(f"✗ 语音识别工作线程错误: {e}") + import traceback + traceback.print_exc() + + def _detect_voice_activity(self, audio_chunk: np.ndarray) -> bool: + """ + 检测语音活动 + + Args: + audio_chunk: 音频数据块 + + Returns: + 是否检测到语音活动 + """ + try: + if len(audio_chunk) == 0: + return False + + # 计算音频级别 + audio_level = np.mean(np.abs(audio_chunk)) + + # 判断是否超过语音阈值 + return audio_level > self.recognition_params['voice_threshold'] + except Exception as e: + print(f"✗ 语音活动检测失败: {e}") + return False + + def _process_voice_segment(self): + """处理语音片段""" + try: + if not self.audio_buffer: + return + + # 检查语音时长是否足够 + total_samples = sum(len(chunk) for chunk in self.audio_buffer) + voice_duration = total_samples / self.sample_rate + + if voice_duration >= self.recognition_params['min_voice_duration']: + # 执行语音识别 + recognized_result = self._perform_recognition() + + if recognized_result and 'text' in recognized_result and recognized_result['text']: + self.stats['recognitions_processed'] += 1 + self.stats['voice_segments_detected'] += 1 + self.stats['total_voice_duration'] += voice_duration + + # 更新平均识别时间 + if self.stats['recognitions_processed'] > 0: + self.stats['average_recognition_time'] = ( + self.stats['total_recognition_time'] / self.stats['recognitions_processed'] + ) + + # 更新平均置信度 + if 'confidence' in recognized_result: + old_avg = self.stats['confidence_average'] + n = self.stats['recognitions_processed'] + self.stats['confidence_average'] = (old_avg * (n - 1) + recognized_result['confidence']) / n + + # 调用回调函数 + if self.plugin.speech_recognized_callback: + self.plugin.speech_recognized_callback(recognized_result['text']) + elif self.recognition_callback: + self.recognition_callback(recognized_result['text']) + + print(f"✓ 识别结果: {recognized_result['text']} (置信度: {recognized_result.get('confidence', 0):.2f})") + + # 缓存识别结果 + self._cache_recognition_result(recognized_result['text'], recognized_result) + + # 重置状态 + self.audio_buffer = [] + self.is_voice_detected = False + self.silence_counter = 0 + self.voice_activity_counter = 0 + self.voice_start_time = 0.0 + self.last_voice_time = 0.0 + + except Exception as e: + print(f"✗ 处理语音片段失败: {e}") + self.stats['recognition_errors'] += 1 + import traceback + traceback.print_exc() + + def _perform_recognition(self) -> Dict[str, Any]: + """ + 执行语音识别 + + Returns: + 识别结果字典 + """ + try: + recognition_start_time = time.time() + + # 合并音频缓冲区 + if not self.audio_buffer: + return {} + + audio_data = np.concatenate(self.audio_buffer) + + # 在实际项目中,这里应该调用真正的语音识别引擎 + # 例如:Google Speech-to-Text, Microsoft Speech Services, 或本地模型 + + # 模拟识别过程 + time.sleep(0.1) # 模拟识别延迟 + + # 生成模拟结果 + result = self._generate_simulated_result(audio_data) + + # 更新统计信息 + recognition_time = time.time() - recognition_start_time + self.stats['total_recognition_time'] += recognition_time + + return result + + except Exception as e: + print(f"✗ 语音识别失败: {e}") + self.stats['recognition_errors'] += 1 + import traceback + traceback.print_exc() + return {} + + def _generate_simulated_result(self, audio_data: np.ndarray) -> Dict[str, Any]: + """ + 生成模拟识别结果 + + Args: + audio_data: 音频数据 + + Returns: + 识别结果字典 + """ + try: + # 根据语言选择词汇表 + language_vocab = self.simulated_database.get(self.language, self.simulated_database['zh-CN']) + + # 计算音频特征以选择合适的短语 + audio_energy = np.sum(audio_data ** 2) / len(audio_data) + audio_duration = len(audio_data) / self.sample_rate + + # 根据音频特征选择短语 + candidates = [] + for phrase, base_confidence in language_vocab.items(): + # 根据音频时长调整置信度 + duration_match = 1.0 - abs(len(phrase) * 0.1 - audio_duration) / max(len(phrase) * 0.1, audio_duration) + energy_match = min(1.0, audio_energy * 1000) # 调整能量匹配 + + adjusted_confidence = base_confidence * duration_match * energy_match + candidates.append((phrase, adjusted_confidence)) + + # 选择置信度最高的结果 + if candidates: + candidates.sort(key=lambda x: x[1], reverse=True) + best_phrase, confidence = candidates[0] + + # 应用后处理 + processed_phrase = self._post_process_result(best_phrase) + + result = { + 'text': processed_phrase, + 'confidence': confidence, + 'language': self.language, + 'duration': audio_duration, + 'alternatives': [] + } + + # 添加备选结果 + for phrase, conf in candidates[1:self.recognition_params['max_alternatives']]: + result['alternatives'].append({ + 'text': self._post_process_result(phrase), + 'confidence': conf + }) + + return result + else: + return { + 'text': '', + 'confidence': 0.0, + 'language': self.language, + 'duration': audio_duration + } + + except Exception as e: + print(f"✗ 生成模拟识别结果失败: {e}") + return { + 'text': '', + 'confidence': 0.0, + 'language': self.language, + 'duration': len(audio_data) / self.sample_rate if audio_data is not None else 0 + } + + def _post_process_result(self, text: str) -> str: + """ + 后处理识别结果 + + Args: + text: 原始识别文本 + + Returns: + 处理后的文本 + """ + try: + processed_text = text + + # 应用自定义词汇表 + for word, pronunciation in self.custom_lexicon.items(): + processed_text = processed_text.replace(word, word) + + # 应用专业术语 + for term, expansion in self.technical_terms.items(): + processed_text = processed_text.replace(term, term) + + # 应用脏话过滤 + if self.recognition_params['enable_profanity_filter']: + profanity_list = self.profanity_words.get(self.language, []) + replace_char = self.recognition_params['profanity_replace_char'] + for profanity in profanity_list: + processed_text = processed_text.replace(profanity, replace_char * len(profanity)) + + # 应用上下文偏向短语 + for phrase in self.recognition_params['contextual_phrases']: + # 这里可以实现更复杂的上下文匹配逻辑 + pass + + return processed_text + + except Exception as e: + print(f"✗ 后处理识别结果失败: {e}") + return text + + def _cache_recognition_result(self, text: str, result: Dict[str, Any]): + """ + 缓存识别结果 + + Args: + text: 识别文本 + result: 识别结果 + """ + try: + # 简单的LRU缓存实现 + if len(self.recognition_cache) >= self.cache_size: + # 移除最旧的条目 + oldest_key = next(iter(self.recognition_cache)) + del self.recognition_cache[oldest_key] + + self.recognition_cache[text] = { + 'result': result, + 'timestamp': time.time() + } + except Exception as e: + print(f"✗ 缓存识别结果失败: {e}") + + def set_language(self, language: str): + """ + 设置识别语言 + + Args: + language: 语言代码 (如 'zh-CN', 'en-US') + """ + self.language = language + print(f"✓ 识别语言设置为: {language}") + + def set_recognition_callback(self, callback: Callable): + """ + 设置识别回调函数 + + Args: + callback: 回调函数 + """ + self.recognition_callback = callback + + def set_partial_result_callback(self, callback: Callable): + """ + 设置部分结果回调函数 + + Args: + callback: 回调函数 + """ + self.partial_result_callback = callback + + def set_error_callback(self, callback: Callable): + """ + 设置错误回调函数 + + Args: + callback: 回调函数 + """ + self.error_callback = callback + + def set_voice_activity_callback(self, callback: Callable): + """ + 设置语音活动回调函数 + + Args: + callback: 回调函数 + """ + self.voice_activity_callback = callback + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + # 计算每分钟单词数 + if self.stats['total_voice_duration'] > 0: + self.stats['words_per_minute'] = ( + self.stats['recognitions_processed'] * 10 / # 假设每次识别约10个单词 + (self.stats['total_voice_duration'] / 60.0) + ) + + return self.stats.copy() + + def get_available_languages(self) -> List[str]: + """ + 获取支持的语言列表 + + Returns: + 语言代码列表 + """ + # 模拟支持的语言列表 + return [ + 'zh-CN', # 简体中文 + 'zh-TW', # 繁体中文 + 'en-US', # 英语(美国) + 'en-GB', # 英语(英国) + 'ja-JP', # 日语 + 'ko-KR', # 韩语 + 'fr-FR', # 法语 + 'de-DE', # 德语 + 'es-ES', # 西班牙语 + 'ru-RU', # 俄语 + 'ar-SA', # 阿拉伯语 + 'pt-BR', # 葡萄牙语(巴西) + 'it-IT', # 意大利语 + 'nl-NL', # 荷兰语 + 'pl-PL', # 波兰语 + 'th-TH', # 泰语 + 'vi-VN', # 越南语 + 'tr-TR', # 土耳其语 + 'cs-CZ', # 捷克语 + 'uk-UA' # 乌克兰语 + ] + + def set_silence_threshold(self, threshold: float): + """ + 设置静音阈值 + + Args: + threshold: 静音阈值 + """ + self.recognition_params['silence_threshold'] = max(0.0, min(1.0, threshold)) + print(f"✓ 静音阈值设置为: {threshold}") + + def set_voice_threshold(self, threshold: float): + """ + 设置语音阈值 + + Args: + threshold: 语音阈值 + """ + self.recognition_params['voice_threshold'] = max(0.0, min(1.0, threshold)) + print(f"✓ 语音阈值设置为: {threshold}") + + def set_silence_duration(self, duration: float): + """ + 设置静音检测时长 + + Args: + duration: 静音检测时长(秒) + """ + self.recognition_params['silence_duration'] = max(0.1, duration) + print(f"✓ 静音检测时长设置为: {duration} 秒") + + def set_min_voice_duration(self, duration: float): + """ + 设置最短语音时长 + + Args: + duration: 最短语音时长(秒) + """ + self.recognition_params['min_voice_duration'] = max(0.1, duration) + print(f"✓ 最短语音时长设置为: {duration} 秒") + + def set_max_recording_duration(self, duration: float): + """ + 设置最大录音时长 + + Args: + duration: 最大录音时长(秒) + """ + self.recognition_params['max_recording_duration'] = max(1.0, duration) + print(f"✓ 最大录音时长设置为: {duration} 秒") + + def is_voice_detected(self) -> bool: + """ + 检查是否检测到语音 + + Returns: + 是否检测到语音 + """ + return self.is_voice_detected + + def get_current_buffer_duration(self) -> float: + """ + 获取当前缓冲区音频时长 + + Returns: + 音频时长(秒) + """ + try: + total_samples = sum(len(chunk) for chunk in self.audio_buffer) + return total_samples / self.sample_rate + except Exception as e: + print(f"✗ 计算缓冲区时长失败: {e}") + return 0.0 + + def clear_audio_buffer(self): + """清空音频缓冲区""" + self.audio_buffer = [] + self.silence_counter = 0 + self.voice_activity_counter = 0 + self.is_voice_detected = False + self.voice_start_time = 0.0 + self.last_voice_time = 0.0 + print("✓ 音频缓冲区已清空") + + def add_custom_vocabulary(self, words: List[str]): + """ + 添加自定义词汇表 + + Args: + words: 词汇列表 + """ + try: + for word in words: + # 简单处理,实际应用中可能需要提供发音信息 + self.custom_lexicon[word] = word.upper() + print(f"✓ 添加自定义词汇: {words}") + except Exception as e: + print(f"✗ 添加自定义词汇失败: {e}") + + def set_recognition_mode(self, mode: str): + """ + 设置识别模式 + + Args: + mode: 识别模式 ('command', 'dictation', 'search') + """ + valid_modes = ['command', 'dictation', 'search'] + if mode in valid_modes: + self.recognition_mode = mode + print(f"✓ 识别模式设置为: {mode}") + else: + print(f"✗ 无效的识别模式: {mode}") + + def enable_punctuation(self, enable: bool = True): + """ + 启用/禁用标点符号识别 + + Args: + enable: 是否启用标点符号识别 + """ + self.recognition_params['enable_punctuation'] = enable + state = "启用" if enable else "禁用" + print(f"✓ 标点符号识别已{state}") + + def enable_profanity_filter(self, enable: bool = True): + """ + 启用/禁用脏话过滤 + + Args: + enable: 是否启用脏话过滤 + """ + self.recognition_params['enable_profanity_filter'] = enable + state = "启用" if enable else "禁用" + print(f"✓ 脏话过滤已{state}") + + def set_alternative_count(self, count: int): + """ + 设置备选结果数量 + + Args: + count: 备选结果数量 + """ + count = max(0, min(30, count)) # 限制在0-30之间 + self.recognition_params['max_alternatives'] = count + print(f"✓ 备选结果数量设置为: {count}") + + def enable_word_timing(self, enable: bool = True): + """ + 启用/禁用单词时间戳 + + Args: + enable: 是否启用单词时间戳 + """ + self.recognition_params['enable_word_timing'] = enable + state = "启用" if enable else "禁用" + print(f"✓ 单词时间戳已{state}") + + def set_max_alternatives(self, max_alternatives: int): + """ + 设置最大备选结果数 + + Args: + max_alternatives: 最大备选结果数 + """ + self.recognition_params['max_alternatives'] = max(1, max_alternatives) + print(f"✓ 最大备选结果数设置为: {max_alternatives}") + + def enable_speaker_diarization(self, enable: bool = True): + """ + 启用/禁用说话人分离 + + Args: + enable: 是否启用说话人分离 + """ + self.recognition_params['enable_speaker_diarization'] = enable + state = "启用" if enable else "禁用" + print(f"✓ 说话人分离已{state}") + + def set_speaker_count(self, min_speakers: int, max_speakers: int): + """ + 设置说话人数量范围 + + Args: + min_speakers: 最少说话人数 + max_speakers: 最多说话人数 + """ + min_speakers = max(1, min_speakers) + max_speakers = max(min_speakers, max_speakers) + self.recognition_params['min_speaker_count'] = min_speakers + self.recognition_params['max_speaker_count'] = max_speakers + print(f"✓ 说话人数量范围设置为: {min_speakers}-{max_speakers}") + + def enable_audio_preprocessing(self, enable: bool = True): + """ + 启用/禁用音频预处理 + + Args: + enable: 是否启用音频预处理 + """ + self.recognition_params['enable_audio_preprocessing'] = enable + state = "启用" if enable else "禁用" + print(f"✓ 音频预处理已{state}") + + def set_noise_reduction_level(self, level: float): + """ + 设置噪声消除级别 + + Args: + level: 噪声消除级别 (0.0-1.0) + """ + level = max(0.0, min(1.0, level)) + self.recognition_params['noise_reduction_level'] = level + print(f"✓ 噪声消除级别设置为: {level}") + + def enable_real_time_feedback(self, enable: bool = True): + """ + 启用/禁用实时反馈 + + Args: + enable: 是否启用实时反馈 + """ + self.recognition_params['enable_real_time_feedback'] = enable + state = "启用" if enable else "禁用" + print(f"✓ 实时反馈已{state}") + + def set_confidence_threshold(self, threshold: float): + """ + 设置置信度阈值 + + Args: + threshold: 置信度阈值 (0.0-1.0) + """ + threshold = max(0.0, min(1.0, threshold)) + self.recognition_params['confidence_threshold'] = threshold + print(f"✓ 置信度阈值设置为: {threshold}") + + def enable_contextual_biasing(self, enable: bool = True): + """ + 启用/禁用上下文偏向 + + Args: + enable: 是否启用上下文偏向 + """ + self.recognition_params['enable_contextual_biasing'] = enable + state = "启用" if enable else "禁用" + print(f"✓ 上下文偏向已{state}") + + def add_contextual_phrases(self, phrases: List[str]): + """ + 添加上下文短语 + + Args: + phrases: 短语列表 + """ + try: + self.recognition_params['contextual_phrases'].extend(phrases) + print(f"✓ 添加上下文短语: {phrases}") + except Exception as e: + print(f"✗ 添加上下文短语失败: {e}") + + def set_model_variant(self, variant: str): + """ + 设置模型变体 + + Args: + variant: 模型变体 ('default', 'command_and_search', 'phone_call', 'video') + """ + valid_variants = ['default', 'command_and_search', 'phone_call', 'video'] + if variant in valid_variants: + self.recognition_params['model_variant'] = variant + print(f"✓ 模型变体设置为: {variant}") + else: + print(f"✗ 无效的模型变体: {variant}") + + def enable_enhanced_models(self, enable: bool = True): + """ + 启用/禁用增强模型 + + Args: + enable: 是否启用增强模型 + """ + self.recognition_params['enable_enhanced_models'] = enable + state = "启用" if enable else "禁用" + print(f"✓ 增强模型已{state}") + + def set_recognition_timeout(self, timeout: float): + """ + 设置识别超时时间 + + Args: + timeout: 超时时间(秒) + """ + timeout = max(0.1, timeout) + self.recognition_params['recognition_timeout'] = timeout + print(f"✓ 识别超时时间设置为: {timeout} 秒") + + def set_profanity_replace_char(self, char: str): + """ + 设置脏话替换字符 + + Args: + char: 替换字符 + """ + self.recognition_params['profanity_replace_char'] = char + print(f"✓ 脏话替换字符设置为: {char}") + + def get_cached_results(self) -> Dict[str, Any]: + """ + 获取缓存的识别结果 + + Returns: + 缓存结果字典 + """ + return self.recognition_cache.copy() + + def clear_cache(self): + """清空识别结果缓存""" + self.recognition_cache.clear() + print("✓ 识别结果缓存已清空") + + def set_cache_size(self, size: int): + """ + 设置缓存大小 + + Args: + size: 缓存大小 + """ + self.cache_size = max(1, size) + print(f"✓ 缓存大小设置为: {size}") + + def export_stats(self, filename: str) -> bool: + """ + 导出统计信息到文件 + + Args: + filename: 文件名 + + Returns: + 是否导出成功 + """ + try: + stats_data = { + 'timestamp': time.time(), + 'stats': self.get_stats(), + 'params': self.recognition_params, + 'language': self.language, + 'mode': self.recognition_mode + } + + with open(filename, 'w', encoding='utf-8') as f: + json.dump(stats_data, f, ensure_ascii=False, indent=2) + + print(f"✓ 统计信息已导出到: {filename}") + return True + except Exception as e: + print(f"✗ 导出统计信息失败: {e}") + return False + + def import_custom_lexicon(self, filename: str) -> bool: + """ + 从文件导入自定义词汇表 + + Args: + filename: 文件名 + + Returns: + 是否导入成功 + """ + try: + with open(filename, 'r', encoding='utf-8') as f: + lexicon_data = json.load(f) + self.custom_lexicon.update(lexicon_data) + + print(f"✓ 自定义词汇表已从 {filename} 导入") + return True + except Exception as e: + print(f"✗ 导入自定义词汇表失败: {e}") + return False + + def export_custom_lexicon(self, filename: str) -> bool: + """ + 导出自定义词汇表到文件 + + Args: + filename: 文件名 + + Returns: + 是否导出成功 + """ + try: + with open(filename, 'w', encoding='utf-8') as f: + json.dump(self.custom_lexicon, f, ensure_ascii=False, indent=2) + + print(f"✓ 自定义词汇表已导出到: {filename}") + return True + except Exception as e: + print(f"✗ 导出自定义词汇表失败: {e}") + return False + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'recognitions_processed': 0, + 'recognition_errors': 0, + 'total_recognition_time': 0.0, + 'average_recognition_time': 0.0, + 'voice_segments_detected': 0, + 'total_voice_duration': 0.0, + 'words_per_minute': 0.0, + 'confidence_average': 0.0, + 'recognition_accuracy': 0.0 + } + print("✓ 统计信息已重置") + + def set_speaker_info(self, speaker_id: str, info: Dict[str, Any]): + """ + 设置说话人信息 + + Args: + speaker_id: 说话人ID + info: 说话人信息 + """ + self.speaker_info[speaker_id] = info + print(f"✓ 说话人 {speaker_id} 信息已设置") + + def get_speaker_info(self, speaker_id: str) -> Dict[str, Any]: + """ + 获取说话人信息 + + Args: + speaker_id: 说话人ID + + Returns: + 说话人信息 + """ + return self.speaker_info.get(speaker_id, {}) + + def remove_speaker_info(self, speaker_id: str): + """ + 移除说话人信息 + + Args: + speaker_id: 说话人ID + """ + if speaker_id in self.speaker_info: + del self.speaker_info[speaker_id] + print(f"✓ 说话人 {speaker_id} 信息已移除") + + def enable_continuous_recognition(self, enable: bool = True): + """ + 启用/禁用连续识别 + + Args: + enable: 是否启用连续识别 + """ + # 连续识别是默认行为,这里只是记录设置 + state = "启用" if enable else "禁用" + print(f"✓ 连续识别已{state}") + + def set_adaptive_sensitivity(self, enable: bool = True): + """ + 启用/禁用自适应灵敏度 + + Args: + enable: 是否启用自适应灵敏度 + """ + # 在实际实现中,这会根据环境噪声自动调整阈值 + state = "启用" if enable else "禁用" + print(f"✓ 自适应灵敏度已{state}") + + def enable_dialect_support(self, enable: bool = True): + """ + 启用/禁用方言支持 + + Args: + enable: 是否启用方言支持 + """ + state = "启用" if enable else "禁用" + print(f"✓ 方言支持已{state}") + + def set_dialect_preference(self, dialects: List[str]): + """ + 设置方言偏好 + + Args: + dialects: 方言列表 + """ + print(f"✓ 方言偏好设置为: {dialects}") + + def enable_accent_adaptation(self, enable: bool = True): + """ + 启用/禁用口音适应 + + Args: + enable: 是否启用口音适应 + """ + state = "启用" if enable else "禁用" + print(f"✓ 口音适应已{state}") + + def add_accent_sample(self, audio_data: np.ndarray, accent_type: str): + """ + 添加口音样本用于适应 + + Args: + audio_data: 音频数据 + accent_type: 口音类型 + """ + print(f"✓ 已添加 {accent_type} 口音样本") + + def enable_domain_optimization(self, enable: bool = True): + """ + 启用/禁用领域优化 + + Args: + enable: 是否启用领域优化 + """ + state = "启用" if enable else "禁用" + print(f"✓ 领域优化已{state}") + + def set_domain_context(self, domain: str): + """ + 设置领域上下文 + + Args: + domain: 领域类型 ('medical', 'legal', 'technical', 'general') + """ + valid_domains = ['medical', 'legal', 'technical', 'general'] + if domain in valid_domains: + print(f"✓ 领域上下文设置为: {domain}") + else: + print(f"✗ 无效的领域类型: {domain}") + + def enable_low_latency_mode(self, enable: bool = True): + """ + 启用/禁用低延迟模式 + + Args: + enable: 是否启用低延迟模式 + """ + state = "启用" if enable else "禁用" + print(f"✓ 低延迟模式已{state}") + + def set_latency_preference(self, preference: str): + """ + 设置延迟偏好 + + Args: + preference: 延迟偏好 ('accuracy', 'speed', 'balanced') + """ + valid_preferences = ['accuracy', 'speed', 'balanced'] + if preference in valid_preferences: + print(f"✓ 延迟偏好设置为: {preference}") + else: + print(f"✗ 无效的延迟偏好: {preference}") + + def enable_offline_recognition(self, enable: bool = True): + """ + 启用/禁用离线识别 + + Args: + enable: 是否启用离线识别 + """ + state = "启用" if enable else "禁用" + print(f"✓ 离线识别已{state}") + + def update_offline_model(self, model_path: str): + """ + 更新离线识别模型 + + Args: + model_path: 模型路径 + """ + print(f"✓ 离线识别模型更新为: {model_path}") + + def set_recognition_sensitivity(self, sensitivity: float): + """ + 设置识别灵敏度 + + Args: + sensitivity: 灵敏度 (0.0-1.0) + """ + sensitivity = max(0.0, min(1.0, sensitivity)) + # 调整语音和静音阈值 + self.recognition_params['voice_threshold'] = 0.005 + sensitivity * 0.02 + self.recognition_params['silence_threshold'] = 0.001 + sensitivity * 0.01 + print(f"✓ 识别灵敏度设置为: {sensitivity}") + + def enable_auto_punctuation(self, enable: bool = True): + """ + 启用/禁用自动标点符号 + + Args: + enable: 是否启用自动标点符号 + """ + self.recognition_params['enable_punctuation'] = enable + state = "启用" if enable else "禁用" + print(f"✓ 自动标点符号已{state}") + + def set_punctuation_style(self, style: str): + """ + 设置标点符号风格 + + Args: + style: 标点符号风格 ('casual', 'formal', 'technical') + """ + valid_styles = ['casual', 'formal', 'technical'] + if style in valid_styles: + print(f"✓ 标点符号风格设置为: {style}") + else: + print(f"✗ 无效的标点符号风格: {style}") + + def enable_emotion_detection(self, enable: bool = True): + """ + 启用/禁用情感检测 + + Args: + enable: 是否启用情感检测 + """ + state = "启用" if enable else "禁用" + print(f"✓ 情感检测已{state}") + + def get_emotion_analysis(self, text: str) -> Dict[str, float]: + """ + 获取文本情感分析 + + Args: + text: 文本内容 + + Returns: + 情感分析结果 + """ + # 模拟情感分析结果 + return { + 'positive': 0.6, + 'negative': 0.2, + 'neutral': 0.2, + 'confidence': 0.8 + } + + def enable_intent_recognition(self, enable: bool = True): + """ + 启用/禁用意图识别 + + Args: + enable: 是否启用意图识别 + """ + state = "启用" if enable else "禁用" + print(f"✓ 意图识别已{state}") + + def set_intent_context(self, context: str): + """ + 设置意图识别上下文 + + Args: + context: 上下文类型 + """ + print(f"✓ 意图识别上下文设置为: {context}") + + def recognize_with_context(self, audio_data: np.ndarray, context: str = None) -> Dict[str, Any]: + """ + 带上下文的语音识别 + + Args: + audio_data: 音频数据 + context: 上下文信息 + + Returns: + 识别结果 + """ + try: + # 执行识别 + result = self._perform_recognition() + + # 添加上下文信息 + if context: + result['context'] = context + + return result + except Exception as e: + print(f"✗ 带上下文的语音识别失败: {e}") + return {} + + def batch_recognize(self, audio_segments: List[np.ndarray]) -> List[Dict[str, Any]]: + """ + 批量语音识别 + + Args: + audio_segments: 音频片段列表 + + Returns: + 识别结果列表 + """ + try: + results = [] + for i, segment in enumerate(audio_segments): + print(f"✓ 处理音频片段 {i+1}/{len(audio_segments)}") + # 临时替换音频缓冲区 + original_buffer = self.audio_buffer + self.audio_buffer = [segment] + + result = self._perform_recognition() + results.append(result) + + # 恢复音频缓冲区 + self.audio_buffer = original_buffer + + return results + except Exception as e: + print(f"✗ 批量语音识别失败: {e}") + return [] + + def recognize_with_feedback(self, audio_data: np.ndarray, + feedback_callback: Callable[[str], None] = None) -> Dict[str, Any]: + """ + 带反馈的语音识别 + + Args: + audio_data: 音频数据 + feedback_callback: 反馈回调函数 + + Returns: + 识别结果 + """ + try: + # 模拟实时反馈过程 + if feedback_callback and self.recognition_params['enable_real_time_feedback']: + feedback_callback("开始识别...") + time.sleep(0.05) # 模拟处理时间 + feedback_callback("分析音频特征...") + time.sleep(0.05) + feedback_callback("匹配语言模型...") + time.sleep(0.05) + feedback_callback("生成识别结果...") + + result = self._perform_recognition() + + if feedback_callback: + feedback_callback(f"识别完成: {result.get('text', '')}") + + return result + except Exception as e: + print(f"✗ 带反馈的语音识别失败: {e}") + return {} + + def set_recognition_profile(self, profile: str): + """ + 设置识别配置文件 + + Args: + profile: 配置文件 ('fast', 'accurate', 'balanced') + """ + valid_profiles = ['fast', 'accurate', 'balanced'] + if profile in valid_profiles: + # 根据配置文件调整参数 + if profile == 'fast': + self.recognition_params['enable_enhanced_models'] = False + self.recognition_params['max_alternatives'] = 1 + self.recognition_params['enable_word_timing'] = False + elif profile == 'accurate': + self.recognition_params['enable_enhanced_models'] = True + self.recognition_params['max_alternatives'] = 5 + self.recognition_params['enable_word_timing'] = True + elif profile == 'balanced': + self.recognition_params['enable_enhanced_models'] = True + self.recognition_params['max_alternatives'] = 3 + self.recognition_params['enable_word_timing'] = True + + print(f"✓ 识别配置文件设置为: {profile}") + else: + print(f"✗ 无效的配置文件: {profile}") + + def enable_multilingual_recognition(self, enable: bool = True): + """ + 启用/禁用多语言识别 + + Args: + enable: 是否启用多语言识别 + """ + state = "启用" if enable else "禁用" + print(f"✓ 多语言识别已{state}") + + def set_multilingual_languages(self, languages: List[str]): + """ + 设置多语言识别的语言列表 + + Args: + languages: 语言列表 + """ + print(f"✓ 多语言识别语言设置为: {languages}") + + def recognize_multilingual(self, audio_data: np.ndarray) -> Dict[str, Any]: + """ + 多语言语音识别 + + Args: + audio_data: 音频数据 + + Returns: + 识别结果 + """ + try: + # 执行识别 + result = self._perform_recognition() + + # 添加多语言支持信息 + result['multilingual'] = True + result['detected_languages'] = [self.language] + + return result + except Exception as e: + print(f"✗ 多语言语音识别失败: {e}") + return {} + + def enable_voice_profile_adaptation(self, enable: bool = True): + """ + 启用/禁用语音配置文件适应 + + Args: + enable: 是否启用语音配置文件适应 + """ + state = "启用" if enable else "禁用" + print(f"✓ 语音配置文件适应已{state}") + + def create_voice_profile(self, profile_name: str, audio_samples: List[np.ndarray]) -> bool: + """ + 创建语音配置文件 + + Args: + profile_name: 配置文件名称 + audio_samples: 音频样本列表 + + Returns: + 是否创建成功 + """ + try: + print(f"✓ 语音配置文件 '{profile_name}' 已创建") + return True + except Exception as e: + print(f"✗ 创建语音配置文件失败: {e}") + return False + + def set_active_voice_profile(self, profile_name: str): + """ + 设置活动语音配置文件 + + Args: + profile_name: 配置文件名称 + """ + print(f"✓ 活动语音配置文件设置为: {profile_name}") + + def enable_custom_acoustic_model(self, enable: bool = True): + """ + 启用/禁用自定义声学模型 + + Args: + enable: 是否启用自定义声学模型 + """ + state = "启用" if enable else "禁用" + print(f"✓ 自定义声学模型已{state}") + + def load_custom_acoustic_model(self, model_path: str): + """ + 加载自定义声学模型 + + Args: + model_path: 模型路径 + """ + print(f"✓ 自定义声学模型已加载: {model_path}") + + def enable_custom_language_model(self, enable: bool = True): + """ + 启用/禁用自定义语言模型 + + Args: + enable: 是否启用自定义语言模型 + """ + state = "启用" if enable else "禁用" + print(f"✓ 自定义语言模型已{state}") + + def load_custom_language_model(self, model_path: str): + """ + 加载自定义语言模型 + + Args: + model_path: 模型路径 + """ + print(f"✓ 自定义语言模型已加载: {model_path}") + + def set_recognition_priority(self, priority: str): + """ + 设置识别优先级 + + Args: + priority: 优先级 ('realtime', 'quality', 'balanced') + """ + valid_priorities = ['realtime', 'quality', 'balanced'] + if priority in valid_priorities: + print(f"✓ 识别优先级设置为: {priority}") + else: + print(f"✗ 无效的优先级: {priority}") + + def enable_confidence_scoring(self, enable: bool = True): + """ + 启用/禁用置信度评分 + + Args: + enable: 是否启用置信度评分 + """ + state = "启用" if enable else "禁用" + print(f"✓ 置信度评分已{state}") + + def set_confidence_scoring_mode(self, mode: str): + """ + 设置置信度评分模式 + + Args: + mode: 评分模式 ('basic', 'detailed', 'comprehensive') + """ + valid_modes = ['basic', 'detailed', 'comprehensive'] + if mode in valid_modes: + print(f"✓ 置信度评分模式设置为: {mode}") + else: + print(f"✗ 无效的评分模式: {mode}") + + def enable_lattice_generation(self, enable: bool = True): + """ + 启用/禁用格子生成 + + Args: + enable: 是否启用格子生成 + """ + state = "启用" if enable else "禁用" + print(f"✓ 格子生成已{state}") + + def get_recognition_lattice(self) -> Dict[str, Any]: + """ + 获取识别格子 + + Returns: + 识别格子数据 + """ + # 模拟格子数据 + return { + 'nodes': [], + 'edges': [], + 'timestamp': time.time() + } + + def enable_audio_segmentation(self, enable: bool = True): + """ + 启用/禁用音频分割 + + Args: + enable: 是否启用音频分割 + """ + state = "启用" if enable else "禁用" + print(f"✓ 音频分割已{state}") + + def set_segmentation_strategy(self, strategy: str): + """ + 设置分割策略 + + Args: + strategy: 分割策略 ('silence', 'content', 'hybrid') + """ + valid_strategies = ['silence', 'content', 'hybrid'] + if strategy in valid_strategies: + print(f"✓ 分割策略设置为: {strategy}") + else: + print(f"✗ 无效的分割策略: {strategy}") + + def recognize_with_correction(self, audio_data: np.ndarray, + correction_history: List[str] = None) -> Dict[str, Any]: + """ + 带纠错的语音识别 + + Args: + audio_data: 音频数据 + correction_history: 纠正历史 + + Returns: + 识别结果 + """ + try: + result = self._perform_recognition() + + # 应用纠正历史 + if correction_history: + result['correction_history'] = correction_history + + return result + except Exception as e: + print(f"✗ 带纠错的语音识别失败: {e}") + return {} + + def enable_robust_recognition(self, enable: bool = True): + """ + 启用/禁用鲁棒识别 + + Args: + enable: 是否启用鲁棒识别 + """ + state = "启用" if enable else "禁用" + print(f"✓ 鲁棒识别已{state}") + + def set_robustness_level(self, level: float): + """ + 设置鲁棒性级别 + + Args: + level: 鲁棒性级别 (0.0-1.0) + """ + level = max(0.0, min(1.0, level)) + print(f"✓ 鲁棒性级别设置为: {level}") + + def enable_adaptive_noise_cancellation(self, enable: bool = True): + """ + 启用/禁用自适应噪声消除 + + Args: + enable: 是否启用自适应噪声消除 + """ + state = "启用" if enable else "禁用" + print(f"✓ 自适应噪声消除已{state}") + + def set_noise_cancellation_profile(self, profile: str): + """ + 设置噪声消除配置文件 + + Args: + profile: 配置文件 ('light', 'medium', 'heavy', 'adaptive') + """ + valid_profiles = ['light', 'medium', 'heavy', 'adaptive'] + if profile in valid_profiles: + print(f"✓ 噪声消除配置文件设置为: {profile}") + else: print(f"✗ 无效的配置文件: {profile}") \ No newline at end of file diff --git a/plugins/user/speech_recognition_synthesis/synthesis/speech_synthesizer.py b/plugins/user/speech_recognition_synthesis/synthesis/speech_synthesizer.py index 21d353e3..568be890 100644 --- a/plugins/user/speech_recognition_synthesis/synthesis/speech_synthesizer.py +++ b/plugins/user/speech_recognition_synthesis/synthesis/speech_synthesizer.py @@ -1,1043 +1,1043 @@ -""" -语音合成器 -负责将文本转换为语音 -""" - -import time -import numpy as np -from typing import Dict, Any, List, Callable -import threading -from queue import Queue, Empty - -class SpeechSynthesizer: - """ - 语音合成器 - 负责将文本转换为语音,支持多种语音和语言 - """ - - def __init__(self, plugin): - """ - 初始化语音合成器 - - Args: - plugin: 语音识别和合成插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - self.is_speaking = False - - # 合成配置 - self.language = plugin.config.get('language', 'zh-CN') - self.voice = plugin.config.get('voice', 'default') - self.volume = plugin.config.get('volume', 1.0) - self.speed = plugin.config.get('speed', 1.0) - self.pitch = plugin.config.get('pitch', 1.0) - self.sample_rate = plugin.config.get('sample_rate', 16000) - - # 合成状态 - self.synthesis_thread = None - self.playback_thread = None - self.synthesis_queue = Queue() - self.is_playing = False - self.current_audio_data = None - - # 语音参数 - self.voice_params = { - 'male_voices': ['male1', 'male2', 'male3'], - 'female_voices': ['female1', 'female2', 'female3'], - 'neutral_voices': ['neutral1', 'neutral2'], - 'emotional_voices': ['happy', 'sad', 'angry', 'excited'] - } - - # 合成参数 - self.synthesis_params = { - 'ssml_enabled': False, - 'phoneme_enabled': False, - 'word_break_ms': 250, - 'sentence_break_ms': 500 - } - - # 统计信息 - self.stats = { - 'synthesizations_processed': 0, - 'synthesis_errors': 0, - 'total_synthesis_time': 0.0, - 'average_synthesis_time': 0.0, - 'total_audio_duration': 0.0 - } - - # 回调函数 - self.synthesis_callback = None - self.playback_finished_callback = None - - # 音素字典(简化版) - self.phoneme_dict = { - # 中文拼音到音素的映射(简化) - 'a': [600], 'o': [500], 'e': [400], 'i': [300], 'u': [200], 'v': [250], - 'b': [150], 'p': [160], 'm': [140], 'f': [130], 'd': [120], 't': [110], - 'n': [100], 'l': [90], 'g': [80], 'k': [70], 'h': [60], 'j': [50], - 'q': [40], 'x': [30], 'zh': [25], 'ch': [20], 'sh': [15], 'r': [10], - 'z': [9], 'c': [8], 's': [7], 'y': [6], 'w': [5] - } - - print("✓ 语音合成器已创建") - - def initialize(self) -> bool: - """ - 初始化语音合成器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化合成引擎(这里简化处理,实际项目中需要加载相应的合成模型或库) - print("✓ 语音合成引擎初始化完成") - - self.initialized = True - print("✓ 语音合成器初始化完成") - return True - - except Exception as e: - print(f"✗ 语音合成器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用语音合成器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 语音合成器未初始化") - return False - - self.enabled = True - - # 启动合成线程 - self.synthesis_thread = threading.Thread(target=self._synthesis_worker, daemon=True) - self.synthesis_thread.start() - - print("✓ 语音合成器已启用") - return True - - except Exception as e: - print(f"✗ 语音合成器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用语音合成器""" - try: - self.stop_speaking() - self.enabled = False - - # 清空合成队列 - while not self.synthesis_queue.empty(): - try: - self.synthesis_queue.get_nowait() - except Empty: - break - - print("✓ 语音合成器已禁用") - - except Exception as e: - print(f"✗ 语音合成器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理语音合成器资源""" - try: - self.disable() - self.initialized = False - print("✓ 语音合成器资源已清理") - - except Exception as e: - print(f"✗ 语音合成器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新语音合成器状态 - - Args: - dt: 时间增量 - """ - # 这里可以处理定期更新的任务 - pass - - def synthesize(self, text: str, callback: Callable = None) -> bool: - """ - 合成语音 - - Args: - text: 要合成的文本 - callback: 合成完成回调函数 - - Returns: - 是否开始合成 - """ - try: - if not self.enabled: - print("✗ 语音合成器未启用") - return False - - # 将合成任务加入队列 - task = { - 'text': text, - 'callback': callback or self.synthesis_callback, - 'timestamp': time.time() - } - - self.synthesis_queue.put(task) - print(f"✓ 语音合成任务已加入队列: {text[:50]}...") - return True - - except Exception as e: - print(f"✗ 语音合成任务添加失败: {e}") - import traceback - traceback.print_exc() - return False - - def _synthesis_worker(self): - """语音合成工作线程""" - try: - while self.enabled: - try: - # 从队列获取合成任务 - task = self.synthesis_queue.get(timeout=0.1) - text = task['text'] - callback = task['callback'] - - # 执行语音合成 - audio_data = self._perform_synthesis(text) - - if audio_data is not None: - self.stats['synthesizations_processed'] += 1 - - # 调用回调函数 - if callback: - callback(audio_data, text) - - # 播放音频 - self._play_audio(audio_data) - - except Empty: - continue - except Exception as e: - print(f"✗ 语音合成工作线程错误: {e}") - self.stats['synthesis_errors'] += 1 - import traceback - traceback.print_exc() - - except Exception as e: - print(f"✗ 语音合成工作线程终止: {e}") - import traceback - traceback.print_exc() - - def _perform_synthesis(self, text: str) -> np.ndarray: - """ - 执行语音合成 - - Args: - text: 要合成的文本 - - Returns: - 合成的音频数据 - """ - try: - synthesis_start_time = time.time() - - # 在实际项目中,这里应该调用真正的语音合成引擎 - # 例如:Google Text-to-Speech, Microsoft Speech Services, 或本地模型 - - # 模拟合成过程 - time.sleep(0.05) # 模拟合成延迟 - - # 生成模拟音频数据 - audio_data = self._generate_speech_audio(text) - - # 更新统计信息 - synthesis_time = time.time() - synthesis_start_time - self.stats['total_synthesis_time'] += synthesis_time - - if self.stats['synthesizations_processed'] > 0: - self.stats['average_synthesis_time'] = ( - self.stats['total_synthesis_time'] / self.stats['synthesizations_processed'] - ) - - # 估算音频时长 - audio_duration = len(audio_data) / self.sample_rate - self.stats['total_audio_duration'] += audio_duration - - return audio_data - - except Exception as e: - print(f"✗ 语音合成失败: {e}") - self.stats['synthesis_errors'] += 1 - import traceback - traceback.print_exc() - return None - - def _generate_speech_audio(self, text: str) -> np.ndarray: - """ - 生成语音音频数据 - - Args: - text: 文本内容 - - Returns: - 音频数据 - """ - try: - # 简化的文本到语音转换 - # 在实际实现中,这会使用复杂的语音合成算法 - - # 计算音频长度(基于文本长度和语速) - base_duration = len(text) * 0.1 # 每个字符约0.1秒 - adjusted_duration = base_duration / self.speed - - # 生成音频样本数 - num_samples = int(adjusted_duration * self.sample_rate) - if num_samples <= 0: - return np.array([], dtype=np.float32) - - # 生成时间轴 - t = np.linspace(0, adjusted_duration, num_samples, False) - - # 生成基础音频信号(多个正弦波的组合) - audio_signal = np.zeros(num_samples) - - # 添加基频(根据音调调整) - base_freq = 100 + (self.pitch - 1.0) * 50 # 基频100-150Hz - audio_signal += 0.3 * np.sin(2 * np.pi * base_freq * t) - - # 添加谐波 - for harmonic in range(2, 6): - freq = base_freq * harmonic - amplitude = 0.3 / harmonic - audio_signal += amplitude * np.sin(2 * np.pi * freq * t) - - # 添加语音特征(基于文本) - # 这里简化处理,实际应该基于音素 - for i, char in enumerate(text[:100]): # 限制处理长度 - if char.isalpha(): - # 根据字符生成不同的频率 - char_freq = 200 + (ord(char.lower()) - ord('a')) * 20 - start_idx = int(i * len(t) / len(text)) - end_idx = min(len(t), int((i + 1) * len(t) / len(text))) - char_t = t[start_idx:end_idx] - audio_signal[start_idx:end_idx] += 0.2 * np.sin(2 * np.pi * char_freq * char_t) - - # 应用音量 - audio_signal *= self.volume - - # 应用包络(模拟语音的起音和释音) - envelope = np.ones_like(audio_signal) - attack_time = min(1000, len(audio_signal) // 10) # 前10%时间 - release_time = min(1000, len(audio_signal) // 10) # 后10%时间 - - if len(envelope) > attack_time: - envelope[:attack_time] = np.linspace(0, 1, attack_time) - if len(envelope) > release_time: - envelope[-release_time:] = np.linspace(1, 0, release_time) - - audio_signal *= envelope - - # 确保数值在合法范围内 - audio_signal = np.clip(audio_signal, -1.0, 1.0) - - return audio_signal.astype(np.float32) - - except Exception as e: - print(f"✗ 生成语音音频失败: {e}") - return np.zeros(0, dtype=np.float32) - - def _play_audio(self, audio_data: np.ndarray): - """ - 播放音频数据 - - Args: - audio_data: 音频数据 - """ - try: - if self.is_playing: - print("⚠ 音频正在播放中,等待完成") - return - - self.is_playing = True - self.current_audio_data = audio_data - - # 启动播放线程 - self.playback_thread = threading.Thread( - target=self._playback_worker, - args=(audio_data,), - daemon=True - ) - self.playback_thread.start() - - except Exception as e: - print(f"✗ 音频播放启动失败: {e}") - self.is_playing = False - import traceback - traceback.print_exc() - - def _playback_worker(self, audio_data: np.ndarray): - """音频播放工作线程""" - try: - # 模拟音频播放 - audio_duration = len(audio_data) / self.sample_rate - time.sleep(audio_duration) # 模拟播放时间 - - # 播放完成 - self.is_playing = False - self.current_audio_data = None - - # 调用播放完成回调 - if self.playback_finished_callback: - self.playback_finished_callback() - - print("✓ 音频播放完成") - - except Exception as e: - print(f"✗ 音频播放失败: {e}") - self.is_playing = False - self.current_audio_data = None - import traceback - traceback.print_exc() - - def stop_speaking(self): - """停止播放语音""" - try: - self.is_playing = False - self.current_audio_data = None - - if self.playback_thread and self.playback_thread.is_alive(): - # 在实际实现中,可能需要更优雅地停止播放线程 - pass - - print("✓ 停止语音播放") - - except Exception as e: - print(f"✗ 停止语音播放失败: {e}") - import traceback - traceback.print_exc() - - def set_language(self, language: str): - """ - 设置合成语言 - - Args: - language: 语言代码 (如 'zh-CN', 'en-US') - """ - self.language = language - print(f"✓ 合成语言设置为: {language}") - - def set_voice(self, voice: str): - """ - 设置语音 - - Args: - voice: 语音名称 - """ - self.voice = voice - print(f"✓ 语音设置为: {voice}") - - def set_volume(self, volume: float): - """ - 设置音量 - - Args: - volume: 音量 (0.0-1.0) - """ - self.volume = max(0.0, min(1.0, volume)) - print(f"✓ 音量设置为: {volume}") - - def set_speed(self, speed: float): - """ - 设置语速 - - Args: - speed: 语速 (0.5-2.0) - """ - self.speed = max(0.5, min(2.0, speed)) - print(f"✓ 语速设置为: {speed}") - - def set_pitch(self, pitch: float): - """ - 设置音调 - - Args: - pitch: 音调 (0.5-2.0) - """ - self.pitch = max(0.5, min(2.0, pitch)) - print(f"✓ 音调设置为: {pitch}") - - def set_synthesis_callback(self, callback: Callable): - """ - 设置合成回调函数 - - Args: - callback: 回调函数 - """ - self.synthesis_callback = callback - - def set_playback_finished_callback(self, callback: Callable): - """ - 设置播放完成回调函数 - - Args: - callback: 回调函数 - """ - self.playback_finished_callback = callback - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def get_available_voices(self) -> List[str]: - """ - 获取支持的语音列表 - - Returns: - 语音名称列表 - """ - voices = [] - for voice_list in self.voice_params.values(): - voices.extend(voice_list) - return voices - - def get_available_languages(self) -> List[str]: - """ - 获取支持的语言列表 - - Returns: - 语言代码列表 - """ - # 模拟支持的语言列表 - return [ - 'zh-CN', # 简体中文 - 'zh-TW', # 繁体中文 - 'en-US', # 英语(美国) - 'en-GB', # 英语(英国) - 'ja-JP', # 日语 - 'ko-KR', # 韩语 - 'fr-FR', # 法语 - 'de-DE', # 德语 - 'es-ES', # 西班牙语 - 'ru-RU' # 俄语 - ] - - def is_speaking(self) -> bool: - """ - 检查是否正在播放语音 - - Returns: - 是否正在播放语音 - """ - return self.is_playing - - def get_current_audio_data(self) -> np.ndarray: - """ - 获取当前播放的音频数据 - - Returns: - 音频数据 - """ - return self.current_audio_data if self.current_audio_data is not None else np.array([]) - - def set_ssml_enabled(self, enabled: bool): - """ - 启用/禁用SSML支持 - - Args: - enabled: 是否启用SSML - """ - self.synthesis_params['ssml_enabled'] = enabled - state = "启用" if enabled else "禁用" - print(f"✓ SSML支持已{state}") - - def set_phoneme_enabled(self, enabled: bool): - """ - 启用/禁用音素支持 - - Args: - enabled: 是否启用音素 - """ - self.synthesis_params['phoneme_enabled'] = enabled - state = "启用" if enabled else "禁用" - print(f"✓ 音素支持已{state}") - - def set_word_break(self, ms: int): - """ - 设置单词间断时间 - - Args: - ms: 间断时间(毫秒) - """ - self.synthesis_params['word_break_ms'] = max(0, ms) - print(f"✓ 单词间断时间设置为: {ms} 毫秒") - - def set_sentence_break(self, ms: int): - """ - 设置句子间断时间 - - Args: - ms: 间断时间(毫秒) - """ - self.synthesis_params['sentence_break_ms'] = max(0, ms) - print(f"✓ 句子间断时间设置为: {ms} 毫秒") - - def set_emotion(self, emotion: str): - """ - 设置语音情感 - - Args: - emotion: 情感类型 ('neutral', 'happy', 'sad', 'angry', 'excited') - """ - valid_emotions = ['neutral', 'happy', 'sad', 'angry', 'excited'] - if emotion in valid_emotions: - print(f"✓ 语音情感设置为: {emotion}") - else: - print(f"✗ 无效的语音情感: {emotion}") - - def set_voice_style(self, style: str): - """ - 设置语音风格 - - Args: - style: 风格类型 ('normal', 'cheerful', 'sad', 'angry', 'fearful', 'gentle', 'lyrical') - """ - valid_styles = ['normal', 'cheerful', 'sad', 'angry', 'fearful', 'gentle', 'lyrical'] - if style in valid_styles: - print(f"✓ 语音风格设置为: {style}") - else: - print(f"✗ 无效的语音风格: {style}") - - def set_voice_age(self, age: int): - """ - 设置语音年龄 - - Args: - age: 年龄 (10-80) - """ - age = max(10, min(80, age)) - print(f"✓ 语音年龄设置为: {age} 岁") - - def set_voice_gender(self, gender: str): - """ - 设置语音性别 - - Args: - gender: 性别 ('male', 'female', 'neutral') - """ - valid_genders = ['male', 'female', 'neutral'] - if gender in valid_genders: - print(f"✓ 语音性别设置为: {gender}") - else: - print(f"✗ 无效的语音性别: {gender}") - - def set_sample_rate(self, sample_rate: int): - """ - 设置采样率 - - Args: - sample_rate: 采样率 (Hz) - """ - self.sample_rate = sample_rate - print(f"✓ 采样率设置为: {sample_rate} Hz") - - def set_prosody(self, rate: float = None, volume: float = None, pitch: float = None): - """ - 设置韵律参数 - - Args: - rate: 语速 (0.5-2.0) - volume: 音量 (0.0-1.0) - pitch: 音调 (0.5-2.0) - """ - if rate is not None: - self.set_speed(rate) - if volume is not None: - self.set_volume(volume) - if pitch is not None: - self.set_pitch(pitch) - print("✓ 韵律参数已设置") - - def set_audio_format(self, format: str): - """ - 设置音频格式 - - Args: - format: 音频格式 ('wav', 'mp3', 'pcm') - """ - valid_formats = ['wav', 'mp3', 'pcm'] - if format in valid_formats: - print(f"✓ 音频格式设置为: {format}") - else: - print(f"✗ 无效的音频格式: {format}") - - def set_output_gain(self, gain: float): - """ - 设置输出增益 - - Args: - gain: 增益值 (0.0-10.0) - """ - gain = max(0.0, min(10.0, gain)) - print(f"✓ 输出增益设置为: {gain}") - - def enable_audio_effects(self, enable: bool = True): - """ - 启用/禁用音频效果 - - Args: - enable: 是否启用音频效果 - """ - state = "启用" if enable else "禁用" - print(f"✓ 音频效果已{state}") - - def set_audio_effect(self, effect: str, intensity: float): - """ - 设置音频效果 - - Args: - effect: 效果类型 ('reverb', 'chorus', 'echo') - intensity: 效果强度 (0.0-1.0) - """ - valid_effects = ['reverb', 'chorus', 'echo'] - if effect in valid_effects: - intensity = max(0.0, min(1.0, intensity)) - print(f"✓ 音频效果 {effect} 强度设置为: {intensity}") - else: - print(f"✗ 无效的音频效果: {effect}") - - def set_speaking_rate_mode(self, mode: str): - """ - 设置语速模式 - - Args: - mode: 模式 ('default', 'x-slow', 'slow', 'medium', 'fast', 'x-fast') - """ - valid_modes = ['default', 'x-slow', 'slow', 'medium', 'fast', 'x-fast'] - if mode in valid_modes: - print(f"✓ 语速模式设置为: {mode}") - else: - print(f"✗ 无效的语速模式: {mode}") - - def set_voice_role(self, role: str): - """ - 设置语音角色 - - Args: - role: 角色类型 ('default', 'young', 'old', 'senior', 'teen', 'child') - """ - valid_roles = ['default', 'young', 'old', 'senior', 'teen', 'child'] - if role in valid_roles: - print(f"✓ 语音角色设置为: {role}") - else: - print(f"✗ 无效的语音角色: {role}") - - def enable_custom_lexicon(self, enable: bool = True): - """ - 启用/禁用自定义词典 - - Args: - enable: 是否启用自定义词典 - """ - state = "启用" if enable else "禁用" - print(f"✓ 自定义词典已{state}") - - def add_lexicon_entry(self, word: str, pronunciation: str): - """ - 添加词典条目 - - Args: - word: 单词 - pronunciation: 发音 - """ - print(f"✓ 添加词典条目: {word} -> {pronunciation}") - - def set_voice_mapping(self, text_type: str, voice: str): - """ - 设置文本类型对应的语音 - - Args: - text_type: 文本类型 ('narration', 'dialogue', 'question', 'exclamation') - voice: 语音名称 - """ - valid_types = ['narration', 'dialogue', 'question', 'exclamation'] - if text_type in valid_types: - print(f"✓ 文本类型 {text_type} 的语音设置为: {voice}") - else: - print(f"✗ 无效的文本类型: {text_type}") - - def enable_voice_profile(self, enable: bool = True): - """ - 启用/禁用语音配置文件 - - Args: - enable: 是否启用语音配置文件 - """ - state = "启用" if enable else "禁用" - print(f"✓ 语音配置文件已{state}") - - def set_voice_profile(self, profile: str): - """ - 设置语音配置文件 - - Args: - profile: 配置文件名称 - """ - print(f"✓ 语音配置文件设置为: {profile}") - - def enable_neural_voices(self, enable: bool = True): - """ - 启用/禁用神经网络语音 - - Args: - enable: 是否启用神经网络语音 - """ - state = "启用" if enable else "禁用" - print(f"✓ 神经网络语音已{state}") - - def set_voice_quality(self, quality: str): - """ - 设置语音质量 - - Args: - quality: 质量等级 ('low', 'medium', 'high') - """ - valid_qualities = ['low', 'medium', 'high'] - if quality in valid_qualities: - print(f"✓ 语音质量设置为: {quality}") - else: - print(f"✗ 无效的语音质量: {quality}") - - def set_voice_personality(self, personality: str): - """ - 设置语音个性 - - Args: - personality: 个性类型 ('friendly', 'professional', 'casual', 'warm') - """ - valid_personalities = ['friendly', 'professional', 'casual', 'warm'] - if personality in valid_personalities: - print(f"✓ 语音个性设置为: {personality}") - else: - print(f"✗ 无效的语音个性: {personality}") - - def enable_emotional_tts(self, enable: bool = True): - """ - 启用/禁用情感语音合成 - - Args: - enable: 是否启用情感语音合成 - """ - state = "启用" if enable else "禁用" - print(f"✓ 情感语音合成已{state}") - - def set_emotional_intensity(self, intensity: float): - """ - 设置情感强度 - - Args: - intensity: 情感强度 (0.0-1.0) - """ - intensity = max(0.0, min(1.0, intensity)) - print(f"✓ 情感强度设置为: {intensity}") - - def synthesize_with_callback(self, text: str, callback: Callable) -> bool: - """ - 合成语音并指定回调函数 - - Args: - text: 要合成的文本 - callback: 回调函数 - - Returns: - 是否开始合成 - """ - return self.synthesize(text, callback) - - def synthesize_to_file(self, text: str, filename: str) -> bool: - """ - 合成语音并保存到文件 - - Args: - text: 要合成的文本 - filename: 文件名 - - Returns: - 是否合成成功 - """ - try: - audio_data = self._perform_synthesis(text) - if audio_data is not None and self.plugin.audio_utils: - # 保存为WAV文件 - success = self.plugin.audio_utils.save_wav_file(filename, audio_data, self.sample_rate) - if success: - print(f"✓ 语音已保存到文件: {filename}") - return success - return False - except Exception as e: - print(f"✗ 语音合成并保存文件失败: {e}") - import traceback - traceback.print_exc() - return False - - def synthesize_with_markers(self, text: str) -> Dict[str, Any]: - """ - 合成语音并返回标记信息 - - Args: - text: 要合成的文本 - - Returns: - 包含音频数据和标记信息的字典 - """ - try: - audio_data = self._perform_synthesis(text) - if audio_data is not None: - # 生成简单的标记信息(实际实现中会更复杂) - markers = { - 'word_timings': [], # 单词时间戳 - 'phoneme_timings': [], # 音素时间戳 - 'sentence_breaks': [] # 句子断点 - } - - # 简化处理,为每个字符生成一个时间戳 - duration_per_char = len(audio_data) / len(text) if text else 0 - for i, char in enumerate(text): - markers['word_timings'].append({ - 'word': char, - 'start_time': i * duration_per_char / self.sample_rate, - 'end_time': (i + 1) * duration_per_char / self.sample_rate - }) - - return { - 'audio_data': audio_data, - 'markers': markers, - 'text': text - } - return {} - except Exception as e: - print(f"✗ 带标记的语音合成失败: {e}") - import traceback - traceback.print_exc() - return {} - - def set_voice_customization(self, parameter: str, value: Any): - """ - 设置语音自定义参数 - - Args: - parameter: 参数名称 - value: 参数值 - """ - print(f"✓ 语音自定义参数 {parameter} 设置为: {value}") - - def get_voice_characteristics(self) -> Dict[str, Any]: - """ - 获取当前语音特征 - - Returns: - 语音特征字典 - """ - return { - 'language': self.language, - 'voice': self.voice, - 'volume': self.volume, - 'speed': self.speed, - 'pitch': self.pitch, - 'sample_rate': self.sample_rate - } - - def clone_voice(self, name: str, reference_audio: bytes) -> bool: - """ - 克隆语音(基于参考音频) - - Args: - name: 新语音名称 - reference_audio: 参考音频数据 - - Returns: - 是否克隆成功 - """ - try: - print(f"✓ 语音克隆 '{name}' 已创建(模拟)") - return True - except Exception as e: - print(f"✗ 语音克隆失败: {e}") - return False - - def delete_custom_voice(self, name: str) -> bool: - """ - 删除自定义语音 - - Args: - name: 语音名称 - - Returns: - 是否删除成功 - """ - try: - print(f"✓ 自定义语音 '{name}' 已删除(模拟)") - return True - except Exception as e: - print(f"✗ 删除自定义语音失败: {e}") - return False - - def list_custom_voices(self) -> List[str]: - """ - 列出所有自定义语音 - - Returns: - 自定义语音列表 - """ - # 模拟返回一些自定义语音 - return ['custom_voice_1', 'custom_voice_2'] - - def set_voice_blending(self, voices: List[str], weights: List[float]): - """ - 设置语音混合 - - Args: - voices: 语音名称列表 - weights: 对应权重列表 - """ - if len(voices) == len(weights): - print(f"✓ 语音混合已设置: {voices} with weights {weights}") - else: - print("✗ 语音名称和权重数量不匹配") - - def enable_voice_conversion(self, enable: bool = True): - """ - 启用/禁用语音转换 - - Args: - enable: 是否启用语音转换 - """ - state = "启用" if enable else "禁用" - print(f"✓ 语音转换已{state}") - - def set_voice_conversion_target(self, target_voice: str): - """ - 设置语音转换目标 - - Args: - target_voice: 目标语音 - """ +""" +语音合成器 +负责将文本转换为语音 +""" + +import time +import numpy as np +from typing import Dict, Any, List, Callable +import threading +from queue import Queue, Empty + +class SpeechSynthesizer: + """ + 语音合成器 + 负责将文本转换为语音,支持多种语音和语言 + """ + + def __init__(self, plugin): + """ + 初始化语音合成器 + + Args: + plugin: 语音识别和合成插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + self.is_speaking = False + + # 合成配置 + self.language = plugin.config.get('language', 'zh-CN') + self.voice = plugin.config.get('voice', 'default') + self.volume = plugin.config.get('volume', 1.0) + self.speed = plugin.config.get('speed', 1.0) + self.pitch = plugin.config.get('pitch', 1.0) + self.sample_rate = plugin.config.get('sample_rate', 16000) + + # 合成状态 + self.synthesis_thread = None + self.playback_thread = None + self.synthesis_queue = Queue() + self.is_playing = False + self.current_audio_data = None + + # 语音参数 + self.voice_params = { + 'male_voices': ['male1', 'male2', 'male3'], + 'female_voices': ['female1', 'female2', 'female3'], + 'neutral_voices': ['neutral1', 'neutral2'], + 'emotional_voices': ['happy', 'sad', 'angry', 'excited'] + } + + # 合成参数 + self.synthesis_params = { + 'ssml_enabled': False, + 'phoneme_enabled': False, + 'word_break_ms': 250, + 'sentence_break_ms': 500 + } + + # 统计信息 + self.stats = { + 'synthesizations_processed': 0, + 'synthesis_errors': 0, + 'total_synthesis_time': 0.0, + 'average_synthesis_time': 0.0, + 'total_audio_duration': 0.0 + } + + # 回调函数 + self.synthesis_callback = None + self.playback_finished_callback = None + + # 音素字典(简化版) + self.phoneme_dict = { + # 中文拼音到音素的映射(简化) + 'a': [600], 'o': [500], 'e': [400], 'i': [300], 'u': [200], 'v': [250], + 'b': [150], 'p': [160], 'm': [140], 'f': [130], 'd': [120], 't': [110], + 'n': [100], 'l': [90], 'g': [80], 'k': [70], 'h': [60], 'j': [50], + 'q': [40], 'x': [30], 'zh': [25], 'ch': [20], 'sh': [15], 'r': [10], + 'z': [9], 'c': [8], 's': [7], 'y': [6], 'w': [5] + } + + print("✓ 语音合成器已创建") + + def initialize(self) -> bool: + """ + 初始化语音合成器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化合成引擎(这里简化处理,实际项目中需要加载相应的合成模型或库) + print("✓ 语音合成引擎初始化完成") + + self.initialized = True + print("✓ 语音合成器初始化完成") + return True + + except Exception as e: + print(f"✗ 语音合成器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用语音合成器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 语音合成器未初始化") + return False + + self.enabled = True + + # 启动合成线程 + self.synthesis_thread = threading.Thread(target=self._synthesis_worker, daemon=True) + self.synthesis_thread.start() + + print("✓ 语音合成器已启用") + return True + + except Exception as e: + print(f"✗ 语音合成器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用语音合成器""" + try: + self.stop_speaking() + self.enabled = False + + # 清空合成队列 + while not self.synthesis_queue.empty(): + try: + self.synthesis_queue.get_nowait() + except Empty: + break + + print("✓ 语音合成器已禁用") + + except Exception as e: + print(f"✗ 语音合成器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理语音合成器资源""" + try: + self.disable() + self.initialized = False + print("✓ 语音合成器资源已清理") + + except Exception as e: + print(f"✗ 语音合成器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新语音合成器状态 + + Args: + dt: 时间增量 + """ + # 这里可以处理定期更新的任务 + pass + + def synthesize(self, text: str, callback: Callable = None) -> bool: + """ + 合成语音 + + Args: + text: 要合成的文本 + callback: 合成完成回调函数 + + Returns: + 是否开始合成 + """ + try: + if not self.enabled: + print("✗ 语音合成器未启用") + return False + + # 将合成任务加入队列 + task = { + 'text': text, + 'callback': callback or self.synthesis_callback, + 'timestamp': time.time() + } + + self.synthesis_queue.put(task) + print(f"✓ 语音合成任务已加入队列: {text[:50]}...") + return True + + except Exception as e: + print(f"✗ 语音合成任务添加失败: {e}") + import traceback + traceback.print_exc() + return False + + def _synthesis_worker(self): + """语音合成工作线程""" + try: + while self.enabled: + try: + # 从队列获取合成任务 + task = self.synthesis_queue.get(timeout=0.1) + text = task['text'] + callback = task['callback'] + + # 执行语音合成 + audio_data = self._perform_synthesis(text) + + if audio_data is not None: + self.stats['synthesizations_processed'] += 1 + + # 调用回调函数 + if callback: + callback(audio_data, text) + + # 播放音频 + self._play_audio(audio_data) + + except Empty: + continue + except Exception as e: + print(f"✗ 语音合成工作线程错误: {e}") + self.stats['synthesis_errors'] += 1 + import traceback + traceback.print_exc() + + except Exception as e: + print(f"✗ 语音合成工作线程终止: {e}") + import traceback + traceback.print_exc() + + def _perform_synthesis(self, text: str) -> np.ndarray: + """ + 执行语音合成 + + Args: + text: 要合成的文本 + + Returns: + 合成的音频数据 + """ + try: + synthesis_start_time = time.time() + + # 在实际项目中,这里应该调用真正的语音合成引擎 + # 例如:Google Text-to-Speech, Microsoft Speech Services, 或本地模型 + + # 模拟合成过程 + time.sleep(0.05) # 模拟合成延迟 + + # 生成模拟音频数据 + audio_data = self._generate_speech_audio(text) + + # 更新统计信息 + synthesis_time = time.time() - synthesis_start_time + self.stats['total_synthesis_time'] += synthesis_time + + if self.stats['synthesizations_processed'] > 0: + self.stats['average_synthesis_time'] = ( + self.stats['total_synthesis_time'] / self.stats['synthesizations_processed'] + ) + + # 估算音频时长 + audio_duration = len(audio_data) / self.sample_rate + self.stats['total_audio_duration'] += audio_duration + + return audio_data + + except Exception as e: + print(f"✗ 语音合成失败: {e}") + self.stats['synthesis_errors'] += 1 + import traceback + traceback.print_exc() + return None + + def _generate_speech_audio(self, text: str) -> np.ndarray: + """ + 生成语音音频数据 + + Args: + text: 文本内容 + + Returns: + 音频数据 + """ + try: + # 简化的文本到语音转换 + # 在实际实现中,这会使用复杂的语音合成算法 + + # 计算音频长度(基于文本长度和语速) + base_duration = len(text) * 0.1 # 每个字符约0.1秒 + adjusted_duration = base_duration / self.speed + + # 生成音频样本数 + num_samples = int(adjusted_duration * self.sample_rate) + if num_samples <= 0: + return np.array([], dtype=np.float32) + + # 生成时间轴 + t = np.linspace(0, adjusted_duration, num_samples, False) + + # 生成基础音频信号(多个正弦波的组合) + audio_signal = np.zeros(num_samples) + + # 添加基频(根据音调调整) + base_freq = 100 + (self.pitch - 1.0) * 50 # 基频100-150Hz + audio_signal += 0.3 * np.sin(2 * np.pi * base_freq * t) + + # 添加谐波 + for harmonic in range(2, 6): + freq = base_freq * harmonic + amplitude = 0.3 / harmonic + audio_signal += amplitude * np.sin(2 * np.pi * freq * t) + + # 添加语音特征(基于文本) + # 这里简化处理,实际应该基于音素 + for i, char in enumerate(text[:100]): # 限制处理长度 + if char.isalpha(): + # 根据字符生成不同的频率 + char_freq = 200 + (ord(char.lower()) - ord('a')) * 20 + start_idx = int(i * len(t) / len(text)) + end_idx = min(len(t), int((i + 1) * len(t) / len(text))) + char_t = t[start_idx:end_idx] + audio_signal[start_idx:end_idx] += 0.2 * np.sin(2 * np.pi * char_freq * char_t) + + # 应用音量 + audio_signal *= self.volume + + # 应用包络(模拟语音的起音和释音) + envelope = np.ones_like(audio_signal) + attack_time = min(1000, len(audio_signal) // 10) # 前10%时间 + release_time = min(1000, len(audio_signal) // 10) # 后10%时间 + + if len(envelope) > attack_time: + envelope[:attack_time] = np.linspace(0, 1, attack_time) + if len(envelope) > release_time: + envelope[-release_time:] = np.linspace(1, 0, release_time) + + audio_signal *= envelope + + # 确保数值在合法范围内 + audio_signal = np.clip(audio_signal, -1.0, 1.0) + + return audio_signal.astype(np.float32) + + except Exception as e: + print(f"✗ 生成语音音频失败: {e}") + return np.zeros(0, dtype=np.float32) + + def _play_audio(self, audio_data: np.ndarray): + """ + 播放音频数据 + + Args: + audio_data: 音频数据 + """ + try: + if self.is_playing: + print("⚠ 音频正在播放中,等待完成") + return + + self.is_playing = True + self.current_audio_data = audio_data + + # 启动播放线程 + self.playback_thread = threading.Thread( + target=self._playback_worker, + args=(audio_data,), + daemon=True + ) + self.playback_thread.start() + + except Exception as e: + print(f"✗ 音频播放启动失败: {e}") + self.is_playing = False + import traceback + traceback.print_exc() + + def _playback_worker(self, audio_data: np.ndarray): + """音频播放工作线程""" + try: + # 模拟音频播放 + audio_duration = len(audio_data) / self.sample_rate + time.sleep(audio_duration) # 模拟播放时间 + + # 播放完成 + self.is_playing = False + self.current_audio_data = None + + # 调用播放完成回调 + if self.playback_finished_callback: + self.playback_finished_callback() + + print("✓ 音频播放完成") + + except Exception as e: + print(f"✗ 音频播放失败: {e}") + self.is_playing = False + self.current_audio_data = None + import traceback + traceback.print_exc() + + def stop_speaking(self): + """停止播放语音""" + try: + self.is_playing = False + self.current_audio_data = None + + if self.playback_thread and self.playback_thread.is_alive(): + # 在实际实现中,可能需要更优雅地停止播放线程 + pass + + print("✓ 停止语音播放") + + except Exception as e: + print(f"✗ 停止语音播放失败: {e}") + import traceback + traceback.print_exc() + + def set_language(self, language: str): + """ + 设置合成语言 + + Args: + language: 语言代码 (如 'zh-CN', 'en-US') + """ + self.language = language + print(f"✓ 合成语言设置为: {language}") + + def set_voice(self, voice: str): + """ + 设置语音 + + Args: + voice: 语音名称 + """ + self.voice = voice + print(f"✓ 语音设置为: {voice}") + + def set_volume(self, volume: float): + """ + 设置音量 + + Args: + volume: 音量 (0.0-1.0) + """ + self.volume = max(0.0, min(1.0, volume)) + print(f"✓ 音量设置为: {volume}") + + def set_speed(self, speed: float): + """ + 设置语速 + + Args: + speed: 语速 (0.5-2.0) + """ + self.speed = max(0.5, min(2.0, speed)) + print(f"✓ 语速设置为: {speed}") + + def set_pitch(self, pitch: float): + """ + 设置音调 + + Args: + pitch: 音调 (0.5-2.0) + """ + self.pitch = max(0.5, min(2.0, pitch)) + print(f"✓ 音调设置为: {pitch}") + + def set_synthesis_callback(self, callback: Callable): + """ + 设置合成回调函数 + + Args: + callback: 回调函数 + """ + self.synthesis_callback = callback + + def set_playback_finished_callback(self, callback: Callable): + """ + 设置播放完成回调函数 + + Args: + callback: 回调函数 + """ + self.playback_finished_callback = callback + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def get_available_voices(self) -> List[str]: + """ + 获取支持的语音列表 + + Returns: + 语音名称列表 + """ + voices = [] + for voice_list in self.voice_params.values(): + voices.extend(voice_list) + return voices + + def get_available_languages(self) -> List[str]: + """ + 获取支持的语言列表 + + Returns: + 语言代码列表 + """ + # 模拟支持的语言列表 + return [ + 'zh-CN', # 简体中文 + 'zh-TW', # 繁体中文 + 'en-US', # 英语(美国) + 'en-GB', # 英语(英国) + 'ja-JP', # 日语 + 'ko-KR', # 韩语 + 'fr-FR', # 法语 + 'de-DE', # 德语 + 'es-ES', # 西班牙语 + 'ru-RU' # 俄语 + ] + + def is_speaking(self) -> bool: + """ + 检查是否正在播放语音 + + Returns: + 是否正在播放语音 + """ + return self.is_playing + + def get_current_audio_data(self) -> np.ndarray: + """ + 获取当前播放的音频数据 + + Returns: + 音频数据 + """ + return self.current_audio_data if self.current_audio_data is not None else np.array([]) + + def set_ssml_enabled(self, enabled: bool): + """ + 启用/禁用SSML支持 + + Args: + enabled: 是否启用SSML + """ + self.synthesis_params['ssml_enabled'] = enabled + state = "启用" if enabled else "禁用" + print(f"✓ SSML支持已{state}") + + def set_phoneme_enabled(self, enabled: bool): + """ + 启用/禁用音素支持 + + Args: + enabled: 是否启用音素 + """ + self.synthesis_params['phoneme_enabled'] = enabled + state = "启用" if enabled else "禁用" + print(f"✓ 音素支持已{state}") + + def set_word_break(self, ms: int): + """ + 设置单词间断时间 + + Args: + ms: 间断时间(毫秒) + """ + self.synthesis_params['word_break_ms'] = max(0, ms) + print(f"✓ 单词间断时间设置为: {ms} 毫秒") + + def set_sentence_break(self, ms: int): + """ + 设置句子间断时间 + + Args: + ms: 间断时间(毫秒) + """ + self.synthesis_params['sentence_break_ms'] = max(0, ms) + print(f"✓ 句子间断时间设置为: {ms} 毫秒") + + def set_emotion(self, emotion: str): + """ + 设置语音情感 + + Args: + emotion: 情感类型 ('neutral', 'happy', 'sad', 'angry', 'excited') + """ + valid_emotions = ['neutral', 'happy', 'sad', 'angry', 'excited'] + if emotion in valid_emotions: + print(f"✓ 语音情感设置为: {emotion}") + else: + print(f"✗ 无效的语音情感: {emotion}") + + def set_voice_style(self, style: str): + """ + 设置语音风格 + + Args: + style: 风格类型 ('normal', 'cheerful', 'sad', 'angry', 'fearful', 'gentle', 'lyrical') + """ + valid_styles = ['normal', 'cheerful', 'sad', 'angry', 'fearful', 'gentle', 'lyrical'] + if style in valid_styles: + print(f"✓ 语音风格设置为: {style}") + else: + print(f"✗ 无效的语音风格: {style}") + + def set_voice_age(self, age: int): + """ + 设置语音年龄 + + Args: + age: 年龄 (10-80) + """ + age = max(10, min(80, age)) + print(f"✓ 语音年龄设置为: {age} 岁") + + def set_voice_gender(self, gender: str): + """ + 设置语音性别 + + Args: + gender: 性别 ('male', 'female', 'neutral') + """ + valid_genders = ['male', 'female', 'neutral'] + if gender in valid_genders: + print(f"✓ 语音性别设置为: {gender}") + else: + print(f"✗ 无效的语音性别: {gender}") + + def set_sample_rate(self, sample_rate: int): + """ + 设置采样率 + + Args: + sample_rate: 采样率 (Hz) + """ + self.sample_rate = sample_rate + print(f"✓ 采样率设置为: {sample_rate} Hz") + + def set_prosody(self, rate: float = None, volume: float = None, pitch: float = None): + """ + 设置韵律参数 + + Args: + rate: 语速 (0.5-2.0) + volume: 音量 (0.0-1.0) + pitch: 音调 (0.5-2.0) + """ + if rate is not None: + self.set_speed(rate) + if volume is not None: + self.set_volume(volume) + if pitch is not None: + self.set_pitch(pitch) + print("✓ 韵律参数已设置") + + def set_audio_format(self, format: str): + """ + 设置音频格式 + + Args: + format: 音频格式 ('wav', 'mp3', 'pcm') + """ + valid_formats = ['wav', 'mp3', 'pcm'] + if format in valid_formats: + print(f"✓ 音频格式设置为: {format}") + else: + print(f"✗ 无效的音频格式: {format}") + + def set_output_gain(self, gain: float): + """ + 设置输出增益 + + Args: + gain: 增益值 (0.0-10.0) + """ + gain = max(0.0, min(10.0, gain)) + print(f"✓ 输出增益设置为: {gain}") + + def enable_audio_effects(self, enable: bool = True): + """ + 启用/禁用音频效果 + + Args: + enable: 是否启用音频效果 + """ + state = "启用" if enable else "禁用" + print(f"✓ 音频效果已{state}") + + def set_audio_effect(self, effect: str, intensity: float): + """ + 设置音频效果 + + Args: + effect: 效果类型 ('reverb', 'chorus', 'echo') + intensity: 效果强度 (0.0-1.0) + """ + valid_effects = ['reverb', 'chorus', 'echo'] + if effect in valid_effects: + intensity = max(0.0, min(1.0, intensity)) + print(f"✓ 音频效果 {effect} 强度设置为: {intensity}") + else: + print(f"✗ 无效的音频效果: {effect}") + + def set_speaking_rate_mode(self, mode: str): + """ + 设置语速模式 + + Args: + mode: 模式 ('default', 'x-slow', 'slow', 'medium', 'fast', 'x-fast') + """ + valid_modes = ['default', 'x-slow', 'slow', 'medium', 'fast', 'x-fast'] + if mode in valid_modes: + print(f"✓ 语速模式设置为: {mode}") + else: + print(f"✗ 无效的语速模式: {mode}") + + def set_voice_role(self, role: str): + """ + 设置语音角色 + + Args: + role: 角色类型 ('default', 'young', 'old', 'senior', 'teen', 'child') + """ + valid_roles = ['default', 'young', 'old', 'senior', 'teen', 'child'] + if role in valid_roles: + print(f"✓ 语音角色设置为: {role}") + else: + print(f"✗ 无效的语音角色: {role}") + + def enable_custom_lexicon(self, enable: bool = True): + """ + 启用/禁用自定义词典 + + Args: + enable: 是否启用自定义词典 + """ + state = "启用" if enable else "禁用" + print(f"✓ 自定义词典已{state}") + + def add_lexicon_entry(self, word: str, pronunciation: str): + """ + 添加词典条目 + + Args: + word: 单词 + pronunciation: 发音 + """ + print(f"✓ 添加词典条目: {word} -> {pronunciation}") + + def set_voice_mapping(self, text_type: str, voice: str): + """ + 设置文本类型对应的语音 + + Args: + text_type: 文本类型 ('narration', 'dialogue', 'question', 'exclamation') + voice: 语音名称 + """ + valid_types = ['narration', 'dialogue', 'question', 'exclamation'] + if text_type in valid_types: + print(f"✓ 文本类型 {text_type} 的语音设置为: {voice}") + else: + print(f"✗ 无效的文本类型: {text_type}") + + def enable_voice_profile(self, enable: bool = True): + """ + 启用/禁用语音配置文件 + + Args: + enable: 是否启用语音配置文件 + """ + state = "启用" if enable else "禁用" + print(f"✓ 语音配置文件已{state}") + + def set_voice_profile(self, profile: str): + """ + 设置语音配置文件 + + Args: + profile: 配置文件名称 + """ + print(f"✓ 语音配置文件设置为: {profile}") + + def enable_neural_voices(self, enable: bool = True): + """ + 启用/禁用神经网络语音 + + Args: + enable: 是否启用神经网络语音 + """ + state = "启用" if enable else "禁用" + print(f"✓ 神经网络语音已{state}") + + def set_voice_quality(self, quality: str): + """ + 设置语音质量 + + Args: + quality: 质量等级 ('low', 'medium', 'high') + """ + valid_qualities = ['low', 'medium', 'high'] + if quality in valid_qualities: + print(f"✓ 语音质量设置为: {quality}") + else: + print(f"✗ 无效的语音质量: {quality}") + + def set_voice_personality(self, personality: str): + """ + 设置语音个性 + + Args: + personality: 个性类型 ('friendly', 'professional', 'casual', 'warm') + """ + valid_personalities = ['friendly', 'professional', 'casual', 'warm'] + if personality in valid_personalities: + print(f"✓ 语音个性设置为: {personality}") + else: + print(f"✗ 无效的语音个性: {personality}") + + def enable_emotional_tts(self, enable: bool = True): + """ + 启用/禁用情感语音合成 + + Args: + enable: 是否启用情感语音合成 + """ + state = "启用" if enable else "禁用" + print(f"✓ 情感语音合成已{state}") + + def set_emotional_intensity(self, intensity: float): + """ + 设置情感强度 + + Args: + intensity: 情感强度 (0.0-1.0) + """ + intensity = max(0.0, min(1.0, intensity)) + print(f"✓ 情感强度设置为: {intensity}") + + def synthesize_with_callback(self, text: str, callback: Callable) -> bool: + """ + 合成语音并指定回调函数 + + Args: + text: 要合成的文本 + callback: 回调函数 + + Returns: + 是否开始合成 + """ + return self.synthesize(text, callback) + + def synthesize_to_file(self, text: str, filename: str) -> bool: + """ + 合成语音并保存到文件 + + Args: + text: 要合成的文本 + filename: 文件名 + + Returns: + 是否合成成功 + """ + try: + audio_data = self._perform_synthesis(text) + if audio_data is not None and self.plugin.audio_utils: + # 保存为WAV文件 + success = self.plugin.audio_utils.save_wav_file(filename, audio_data, self.sample_rate) + if success: + print(f"✓ 语音已保存到文件: {filename}") + return success + return False + except Exception as e: + print(f"✗ 语音合成并保存文件失败: {e}") + import traceback + traceback.print_exc() + return False + + def synthesize_with_markers(self, text: str) -> Dict[str, Any]: + """ + 合成语音并返回标记信息 + + Args: + text: 要合成的文本 + + Returns: + 包含音频数据和标记信息的字典 + """ + try: + audio_data = self._perform_synthesis(text) + if audio_data is not None: + # 生成简单的标记信息(实际实现中会更复杂) + markers = { + 'word_timings': [], # 单词时间戳 + 'phoneme_timings': [], # 音素时间戳 + 'sentence_breaks': [] # 句子断点 + } + + # 简化处理,为每个字符生成一个时间戳 + duration_per_char = len(audio_data) / len(text) if text else 0 + for i, char in enumerate(text): + markers['word_timings'].append({ + 'word': char, + 'start_time': i * duration_per_char / self.sample_rate, + 'end_time': (i + 1) * duration_per_char / self.sample_rate + }) + + return { + 'audio_data': audio_data, + 'markers': markers, + 'text': text + } + return {} + except Exception as e: + print(f"✗ 带标记的语音合成失败: {e}") + import traceback + traceback.print_exc() + return {} + + def set_voice_customization(self, parameter: str, value: Any): + """ + 设置语音自定义参数 + + Args: + parameter: 参数名称 + value: 参数值 + """ + print(f"✓ 语音自定义参数 {parameter} 设置为: {value}") + + def get_voice_characteristics(self) -> Dict[str, Any]: + """ + 获取当前语音特征 + + Returns: + 语音特征字典 + """ + return { + 'language': self.language, + 'voice': self.voice, + 'volume': self.volume, + 'speed': self.speed, + 'pitch': self.pitch, + 'sample_rate': self.sample_rate + } + + def clone_voice(self, name: str, reference_audio: bytes) -> bool: + """ + 克隆语音(基于参考音频) + + Args: + name: 新语音名称 + reference_audio: 参考音频数据 + + Returns: + 是否克隆成功 + """ + try: + print(f"✓ 语音克隆 '{name}' 已创建(模拟)") + return True + except Exception as e: + print(f"✗ 语音克隆失败: {e}") + return False + + def delete_custom_voice(self, name: str) -> bool: + """ + 删除自定义语音 + + Args: + name: 语音名称 + + Returns: + 是否删除成功 + """ + try: + print(f"✓ 自定义语音 '{name}' 已删除(模拟)") + return True + except Exception as e: + print(f"✗ 删除自定义语音失败: {e}") + return False + + def list_custom_voices(self) -> List[str]: + """ + 列出所有自定义语音 + + Returns: + 自定义语音列表 + """ + # 模拟返回一些自定义语音 + return ['custom_voice_1', 'custom_voice_2'] + + def set_voice_blending(self, voices: List[str], weights: List[float]): + """ + 设置语音混合 + + Args: + voices: 语音名称列表 + weights: 对应权重列表 + """ + if len(voices) == len(weights): + print(f"✓ 语音混合已设置: {voices} with weights {weights}") + else: + print("✗ 语音名称和权重数量不匹配") + + def enable_voice_conversion(self, enable: bool = True): + """ + 启用/禁用语音转换 + + Args: + enable: 是否启用语音转换 + """ + state = "启用" if enable else "禁用" + print(f"✓ 语音转换已{state}") + + def set_voice_conversion_target(self, target_voice: str): + """ + 设置语音转换目标 + + Args: + target_voice: 目标语音 + """ print(f"✓ 语音转换目标设置为: {target_voice}") \ No newline at end of file diff --git a/plugins/user/speech_recognition_synthesis/utils/audio_utils.py b/plugins/user/speech_recognition_synthesis/utils/audio_utils.py index d40a65e1..675b9e3b 100644 --- a/plugins/user/speech_recognition_synthesis/utils/audio_utils.py +++ b/plugins/user/speech_recognition_synthesis/utils/audio_utils.py @@ -1,1442 +1,1442 @@ -""" -音频工具类 -提供音频处理和文件操作功能 -""" - -import numpy as np -import wave -import struct -from typing import Dict, Any, List -import os - -class AudioUtils: - """ - 音频工具类 - 提供音频处理和文件操作功能 - """ - - def __init__(self, plugin): - """ - 初始化音频工具类 - - Args: - plugin: 语音识别和合成插件实例 - """ - self.plugin = plugin - self.sample_rate = plugin.config.get('sample_rate', 16000) - self.channels = plugin.config.get('channels', 1) - self.sample_width = 2 # 16位音频 - - print("✓ 音频工具类已创建") - - def save_audio_file(self, filename: str, audio_data: bytes) -> bool: - """ - 保存音频文件 - - Args: - filename: 文件名 - audio_data: 音频数据 - - Returns: - 是否保存成功 - """ - try: - # 确保目录存在 - os.makedirs(os.path.dirname(filename) if os.path.dirname(filename) else '.', exist_ok=True) - - # 保存音频数据到文件 - with open(filename, 'wb') as f: - f.write(audio_data) - - print(f"✓ 音频文件已保存: {filename}") - return True - - except Exception as e: - print(f"✗ 保存音频文件失败: {e}") - import traceback - traceback.print_exc() - return False - - def load_audio_file(self, filename: str) -> bytes: - """ - 加载音频文件 - - Args: - filename: 文件名 - - Returns: - 音频数据 - """ - try: - with open(filename, 'rb') as f: - audio_data = f.read() - print(f"✓ 音频文件已加载: {filename}") - return audio_data - - except Exception as e: - print(f"✗ 加载音频文件失败: {e}") - import traceback - traceback.print_exc() - return b'' - - def save_wav_file(self, filename: str, audio_data: np.ndarray, sample_rate: int = None) -> bool: - """ - 保存WAV音频文件 - - Args: - filename: 文件名 - audio_data: 音频数据(numpy数组) - sample_rate: 采样率 - - Returns: - 是否保存成功 - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - # 确保目录存在 - os.makedirs(os.path.dirname(filename) if os.path.dirname(filename) else '.', exist_ok=True) - - # 确保音频数据在合法范围内 - audio_data = np.clip(audio_data, -1.0, 1.0) - - # 转换为16位整数 - audio_int16 = (audio_data * 32767).astype(np.int16) - - # 保存为WAV文件 - with wave.open(filename, 'w') as wav_file: - wav_file.setnchannels(self.channels) - wav_file.setsampwidth(self.sample_width) # 16位 = 2字节 - wav_file.setframerate(sample_rate) - wav_file.writeframes(audio_int16.tobytes()) - - print(f"✓ WAV文件已保存: {filename}") - return True - - except Exception as e: - print(f"✗ 保存WAV文件失败: {e}") - import traceback - traceback.print_exc() - return False - - def load_wav_file(self, filename: str) -> Dict[str, Any]: - """ - 加载WAV音频文件 - - Args: - filename: 文件名 - - Returns: - 包含音频数据和参数的字典 - """ - try: - with wave.open(filename, 'r') as wav_file: - # 获取音频参数 - channels = wav_file.getnchannels() - sample_width = wav_file.getsampwidth() - sample_rate = wav_file.getframerate() - frames = wav_file.getnframes() - - # 读取音频数据 - audio_data = wav_file.readframes(frames) - - # 转换为numpy数组 - if sample_width == 1: - # 8位音频 - audio_array = np.frombuffer(audio_data, dtype=np.uint8) - audio_array = (audio_array.astype(np.float32) - 128) / 128.0 - elif sample_width == 2: - # 16位音频 - audio_array = np.frombuffer(audio_data, dtype=np.int16) - audio_array = audio_array.astype(np.float32) / 32768.0 - elif sample_width == 4: - # 32位音频 - audio_array = np.frombuffer(audio_data, dtype=np.int32) - audio_array = audio_array.astype(np.float32) / 2147483648.0 - else: - print(f"✗ 不支持的采样宽度: {sample_width}") - return {} - - print(f"✓ WAV文件已加载: {filename}") - return { - 'audio_data': audio_array, - 'sample_rate': sample_rate, - 'channels': channels, - 'sample_width': sample_width, - 'frames': frames - } - - except Exception as e: - print(f"✗ 加载WAV文件失败: {e}") - import traceback - traceback.print_exc() - return {} - - def convert_audio_format(self, audio_data: np.ndarray, from_format: str, to_format: str) -> np.ndarray: - """ - 转换音频格式 - - Args: - audio_data: 音频数据 - from_format: 源格式 - to_format: 目标格式 - - Returns: - 转换后的音频数据 - """ - try: - # 这里简化处理,实际项目中可能需要更复杂的格式转换 - print(f"✓ 音频格式已转换: {from_format} -> {to_format}") - return audio_data - except Exception as e: - print(f"✗ 音频格式转换失败: {e}") - return audio_data - - def resample_audio(self, audio_data: np.ndarray, from_rate: int, to_rate: int) -> np.ndarray: - """ - 重采样音频数据 - - Args: - audio_data: 音频数据 - from_rate: 源采样率 - to_rate: 目标采样率 - - Returns: - 重采样后的音频数据 - """ - try: - if from_rate == to_rate: - return audio_data - - # 计算重采样比例 - ratio = to_rate / from_rate - new_length = int(len(audio_data) * ratio) - - # 简单的线性插值(实际项目中应该使用更高质量的重采样算法) - if new_length > 0: - indices = np.linspace(0, len(audio_data) - 1, new_length) - resampled_data = np.interp(indices, np.arange(len(audio_data)), audio_data) - print(f"✓ 音频已重采样: {from_rate}Hz -> {to_rate}Hz") - return resampled_data - else: - return np.array([], dtype=np.float32) - - except Exception as e: - print(f"✗ 音频重采样失败: {e}") - return audio_data - - def apply_gain(self, audio_data: np.ndarray, gain_db: float) -> np.ndarray: - """ - 应用增益到音频数据 - - Args: - audio_data: 音频数据 - gain_db: 增益值(分贝) - - Returns: - 应用增益后的音频数据 - """ - try: - # 将分贝转换为线性增益 - gain_linear = 10 ** (gain_db / 20.0) - amplified_data = audio_data * gain_linear - - # 防止削波 - amplified_data = np.clip(amplified_data, -1.0, 1.0) - - print(f"✓ 音频增益已应用: {gain_db}dB") - return amplified_data - - except Exception as e: - print(f"✗ 应用音频增益失败: {e}") - return audio_data - - def normalize_audio(self, audio_data: np.ndarray) -> np.ndarray: - """ - 归一化音频数据 - - Args: - audio_data: 音频数据 - - Returns: - 归一化后的音频数据 - """ - try: - if len(audio_data) == 0: - return audio_data - - # 计算最大幅度 - max_amplitude = np.max(np.abs(audio_data)) - - if max_amplitude > 0: - normalized_data = audio_data / max_amplitude - print("✓ 音频已归一化") - return normalized_data - else: - return audio_data - - except Exception as e: - print(f"✗ 音频归一化失败: {e}") - return audio_data - - def apply_fade_in(self, audio_data: np.ndarray, fade_duration: float, sample_rate: int = None) -> np.ndarray: - """ - 应用淡入效果 - - Args: - audio_data: 音频数据 - fade_duration: 淡入时长(秒) - sample_rate: 采样率 - - Returns: - 应用淡入效果后的音频数据 - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - fade_samples = int(fade_duration * sample_rate) - if fade_samples <= 0 or fade_samples >= len(audio_data): - return audio_data - - # 创建淡入曲线 - fade_curve = np.linspace(0, 1, fade_samples) - - # 应用淡入效果 - faded_data = audio_data.copy() - faded_data[:fade_samples] *= fade_curve - - print(f"✓ 淡入效果已应用: {fade_duration}秒") - return faded_data - - except Exception as e: - print(f"✗ 应用淡入效果失败: {e}") - return audio_data - - def apply_fade_out(self, audio_data: np.ndarray, fade_duration: float, sample_rate: int = None) -> np.ndarray: - """ - 应用淡出效果 - - Args: - audio_data: 音频数据 - fade_duration: 淡出时长(秒) - sample_rate: 采样率 - - Returns: - 应用淡出效果后的音频数据 - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - fade_samples = int(fade_duration * sample_rate) - if fade_samples <= 0 or fade_samples >= len(audio_data): - return audio_data - - # 创建淡出曲线 - fade_curve = np.linspace(1, 0, fade_samples) - - # 应用淡出效果 - faded_data = audio_data.copy() - faded_data[-fade_samples:] *= fade_curve - - print(f"✓ 淡出效果已应用: {fade_duration}秒") - return faded_data - - except Exception as e: - print(f"✗ 应用淡出效果失败: {e}") - return audio_data - - def concatenate_audio(self, audio_segments: List[np.ndarray]) -> np.ndarray: - """ - 连接多个音频片段 - - Args: - audio_segments: 音频片段列表 - - Returns: - 连接后的音频数据 - """ - try: - if not audio_segments: - return np.array([], dtype=np.float32) - - # 连接所有音频片段 - concatenated = np.concatenate(audio_segments) - print(f"✓ {len(audio_segments)}个音频片段已连接") - return concatenated - - except Exception as e: - print(f"✗ 连接音频片段失败: {e}") - return np.array([], dtype=np.float32) - - def split_audio(self, audio_data: np.ndarray, segment_duration: float, sample_rate: int = None) -> List[np.ndarray]: - """ - 分割音频数据为多个片段 - - Args: - audio_data: 音频数据 - segment_duration: 片段时长(秒) - sample_rate: 采样率 - - Returns: - 音频片段列表 - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - segment_samples = int(segment_duration * sample_rate) - if segment_samples <= 0: - return [audio_data] - - # 分割音频数据 - segments = [] - for i in range(0, len(audio_data), segment_samples): - segment = audio_data[i:i + segment_samples] - segments.append(segment) - - print(f"✓ 音频已分割为{len(segments)}个片段") - return segments - - except Exception as e: - print(f"✗ 分割音频失败: {e}") - return [audio_data] - - def calculate_audio_level(self, audio_data: np.ndarray) -> float: - """ - 计算音频级别(RMS) - - Args: - audio_data: 音频数据 - - Returns: - 音频级别 (0.0-1.0) - """ - try: - if len(audio_data) == 0: - return 0.0 - - # 计算RMS值 - rms = np.sqrt(np.mean(audio_data ** 2)) - return float(rms) - - except Exception as e: - print(f"✗ 计算音频级别失败: {e}") - return 0.0 - - def detect_silence(self, audio_data: np.ndarray, threshold: float = 0.01, - min_silence_duration: float = 0.1, sample_rate: int = None) -> List[Dict[str, float]]: - """ - 检测音频中的静音段 - - Args: - audio_data: 音频数据 - threshold: 静音阈值 - min_silence_duration: 最小静音时长(秒) - sample_rate: 采样率 - - Returns: - 静音段列表 [{'start': start_time, 'end': end_time}] - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - min_silence_samples = int(min_silence_duration * sample_rate) - silence_segments = [] - - # 简化的静音检测 - is_silent = np.abs(audio_data) < threshold - in_silence = False - silence_start = 0 - - for i in range(len(is_silent)): - if is_silent[i] and not in_silence: - # 开始静音段 - in_silence = True - silence_start = i - elif not is_silent[i] and in_silence: - # 结束静音段 - in_silence = False - silence_duration = i - silence_start - if silence_duration >= min_silence_samples: - silence_segments.append({ - 'start': silence_start / sample_rate, - 'end': i / sample_rate - }) - - # 处理最后一个静音段 - if in_silence: - silence_duration = len(audio_data) - silence_start - if silence_duration >= min_silence_samples: - silence_segments.append({ - 'start': silence_start / sample_rate, - 'end': len(audio_data) / sample_rate - }) - - print(f"✓ 检测到{len(silence_segments)}个静音段") - return silence_segments - - except Exception as e: - print(f"✗ 检测静音段失败: {e}") - return [] - - def remove_silence(self, audio_data: np.ndarray, threshold: float = 0.01, - min_silence_duration: float = 0.1, sample_rate: int = None) -> np.ndarray: - """ - 移除音频中的静音段 - - Args: - audio_data: 音频数据 - threshold: 静音阈值 - min_silence_duration: 最小静音时长(秒) - sample_rate: 采样率 - - Returns: - 移除静音后的音频数据 - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - # 检测静音段 - silence_segments = self.detect_silence(audio_data, threshold, min_silence_duration, sample_rate) - - if not silence_segments: - return audio_data - - # 移除静音段 - cleaned_audio = audio_data.copy() - for segment in reversed(silence_segments): # 从后往前删除,避免索引变化 - start_sample = int(segment['start'] * sample_rate) - end_sample = int(segment['end'] * sample_rate) - cleaned_audio = np.concatenate([ - cleaned_audio[:start_sample], - cleaned_audio[end_sample:] - ]) - - print(f"✓ 已移除{len(silence_segments)}个静音段") - return cleaned_audio - - except Exception as e: - print(f"✗ 移除静音段失败: {e}") - return audio_data - - def apply_high_pass_filter(self, audio_data: np.ndarray, cutoff_freq: float, - sample_rate: int = None) -> np.ndarray: - """ - 应用高通滤波器 - - Args: - audio_data: 音频数据 - cutoff_freq: 截止频率(Hz) - sample_rate: 采样率 - - Returns: - 滤波后的音频数据 - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - # 简化的高通滤波实现 - # 实际项目中应该使用更专业的滤波器设计 - rc = 1.0 / (2 * np.pi * cutoff_freq) - dt = 1.0 / sample_rate - alpha = rc / (rc + dt) - - filtered_data = np.zeros_like(audio_data) - for i in range(1, len(audio_data)): - filtered_data[i] = alpha * (filtered_data[i-1] + audio_data[i] - audio_data[i-1]) - - print(f"✓ 高通滤波器已应用: {cutoff_freq}Hz") - return filtered_data - - except Exception as e: - print(f"✗ 应用高通滤波器失败: {e}") - return audio_data - - def apply_low_pass_filter(self, audio_data: np.ndarray, cutoff_freq: float, - sample_rate: int = None) -> np.ndarray: - """ - 应用低通滤波器 - - Args: - audio_data: 音频数据 - cutoff_freq: 截止频率(Hz) - sample_rate: 采样率 - - Returns: - 滤波后的音频数据 - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - # 简化的低通滤波实现 - rc = 1.0 / (2 * np.pi * cutoff_freq) - dt = 1.0 / sample_rate - alpha = dt / (rc + dt) - - filtered_data = np.zeros_like(audio_data) - filtered_data[0] = audio_data[0] - for i in range(1, len(audio_data)): - filtered_data[i] = alpha * audio_data[i] + (1 - alpha) * filtered_data[i-1] - - print(f"✓ 低通滤波器已应用: {cutoff_freq}Hz") - return filtered_data - - except Exception as e: - print(f"✗ 应用低通滤波器失败: {e}") - return audio_data - - def generate_sine_wave(self, frequency: float, duration: float, - amplitude: float = 1.0, sample_rate: int = None) -> np.ndarray: - """ - 生成正弦波音频 - - Args: - frequency: 频率(Hz) - duration: 时长(秒) - amplitude: 振幅 (0.0-1.0) - sample_rate: 采样率 - - Returns: - 正弦波音频数据 - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - # 生成时间轴 - t = np.linspace(0, duration, int(sample_rate * duration), False) - - # 生成正弦波 - sine_wave = amplitude * np.sin(2 * np.pi * frequency * t) - - print(f"✓ 正弦波已生成: {frequency}Hz, {duration}秒") - return sine_wave - - except Exception as e: - print(f"✗ 生成正弦波失败: {e}") - return np.array([], dtype=np.float32) - - def generate_white_noise(self, duration: float, amplitude: float = 0.1, - sample_rate: int = None) -> np.ndarray: - """ - 生成白噪声音频 - - Args: - duration: 时长(秒) - amplitude: 振幅 (0.0-1.0) - sample_rate: 采样率 - - Returns: - 白噪声音频数据 - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - # 生成样本数 - num_samples = int(sample_rate * duration) - - # 生成白噪声 - white_noise = amplitude * (np.random.random(num_samples) * 2 - 1) - - print(f"✓ 白噪声已生成: {duration}秒") - return white_noise - - except Exception as e: - print(f"✗ 生成白噪声失败: {e}") - return np.array([], dtype=np.float32) - - def mix_audio_signals(self, audio1: np.ndarray, audio2: np.ndarray, - ratio: float = 0.5) -> np.ndarray: - """ - 混合两个音频信号 - - Args: - audio1: 第一个音频信号 - audio2: 第二个音频信号 - ratio: 混合比例 (0.0-1.0, 0表示全audio1,1表示全audio2) - - Returns: - 混合后的音频信号 - """ - try: - # 确保两个音频信号长度相同 - max_length = max(len(audio1), len(audio2)) - if len(audio1) < max_length: - audio1 = np.pad(audio1, (0, max_length - len(audio1))) - if len(audio2) < max_length: - audio2 = np.pad(audio2, (0, max_length - len(audio2))) - - # 混合音频 - mixed_audio = (1 - ratio) * audio1 + ratio * audio2 - - # 防止削波 - mixed_audio = np.clip(mixed_audio, -1.0, 1.0) - - print("✓ 音频信号已混合") - return mixed_audio - - except Exception as e: - print(f"✗ 混合音频信号失败: {e}") - # 返回较长的音频信号 - return audio1 if len(audio1) >= len(audio2) else audio2 - - def calculate_audio_duration(self, audio_data: np.ndarray, sample_rate: int = None) -> float: - """ - 计算音频时长 - - Args: - audio_data: 音频数据 - sample_rate: 采样率 - - Returns: - 音频时长(秒) - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - if len(audio_data) == 0: - return 0.0 - - duration = len(audio_data) / sample_rate - return duration - - except Exception as e: - print(f"✗ 计算音频时长失败: {e}") - return 0.0 - - def convert_to_mono(self, audio_data: np.ndarray, channels: int = None) -> np.ndarray: - """ - 转换为单声道音频 - - Args: - audio_data: 音频数据 - channels: 声道数 - - Returns: - 单声道音频数据 - """ - try: - if channels is None: - channels = self.channels - - if channels == 1: - return audio_data - - # 假设音频数据是交错存储的 - if len(audio_data) % channels != 0: - print("✗ 音频数据长度与声道数不匹配") - return audio_data - - # 重新整形并计算平均值 - reshaped = audio_data.reshape(-1, channels) - mono_audio = np.mean(reshaped, axis=1) - - print("✓ 音频已转换为单声道") - return mono_audio - - except Exception as e: - print(f"✗ 转换为单声道失败: {e}") - return audio_data - - def convert_to_stereo(self, audio_data: np.ndarray) -> np.ndarray: - """ - 转换为立体声音频(复制单声道到两个声道) - - Args: - audio_data: 单声道音频数据 - - Returns: - 立体声音频数据 - """ - try: - # 将单声道数据复制到两个声道 - stereo_audio = np.tile(audio_data.reshape(-1, 1), (1, 2)) - stereo_audio = stereo_audio.flatten() - - print("✓ 音频已转换为立体声") - return stereo_audio - - except Exception as e: - print(f"✗ 转换为立体声失败: {e}") - return audio_data - - def apply_compressor(self, audio_data: np.ndarray, threshold: float = -20.0, - ratio: float = 4.0, attack: float = 0.01, release: float = 0.1) -> np.ndarray: - """ - 应用压缩器效果 - - Args: - audio_data: 音频数据 - threshold: 阈值(dB) - ratio: 压缩比 - attack: 启动时间(秒) - release: 释放时间(秒) - - Returns: - 压缩后的音频数据 - """ - try: - # 简化的压缩器实现 - threshold_linear = 10 ** (threshold / 20.0) - - # 计算增益缩减 - gain_reduction = np.zeros_like(audio_data) - envelope = 0.0 - - for i in range(len(audio_data)): - # 计算包络 - envelope = max(abs(audio_data[i]), envelope * 0.99) - - # 计算增益缩减 - if envelope > threshold_linear: - gain_db = 20 * np.log10(envelope / threshold_linear) - reduction_db = gain_db - gain_db / ratio - gain_reduction[i] = 10 ** (-reduction_db / 20.0) - else: - gain_reduction[i] = 1.0 - - # 应用增益缩减 - compressed_audio = audio_data * gain_reduction - - print("✓ 压缩器效果已应用") - return compressed_audio - - except Exception as e: - print(f"✗ 应用压缩器效果失败: {e}") - return audio_data - - def apply_limiter(self, audio_data: np.ndarray, threshold: float = -0.1) -> np.ndarray: - """ - 应用限制器效果 - - Args: - audio_data: 音频数据 - threshold: 阈值(dB,相对于满量程) - - Returns: - 限制后的音频数据 - """ - try: - # 转换阈值为线性值 - threshold_linear = 10 ** (threshold / 20.0) - - # 应用限制器 - limited_audio = np.clip(audio_data, -threshold_linear, threshold_linear) - - print(f"✓ 限制器效果已应用: {threshold}dB") - return limited_audio - - except Exception as e: - print(f"✗ 应用限制器效果失败: {e}") - return audio_data - - def apply_expander(self, audio_data: np.ndarray, threshold: float = -40.0, - ratio: float = 2.0, attack: float = 0.01, release: float = 0.1) -> np.ndarray: - """ - 应用扩展器效果 - - Args: - audio_data: 音频数据 - threshold: 阈值(dB) - ratio: 扩展比 - attack: 启动时间(秒) - release: 释放时间(秒) - - Returns: - 扩展后的音频数据 - """ - try: - # 简化的扩展器实现 - threshold_linear = 10 ** (threshold / 20.0) - - # 计算增益扩展 - gain_expansion = np.zeros_like(audio_data) - envelope = 0.0 - - for i in range(len(audio_data)): - # 计算包络 - envelope = max(abs(audio_data[i]), envelope * 0.99) - - # 计算增益扩展 - if envelope < threshold_linear: - gain_db = 20 * np.log10(envelope / threshold_linear) - expansion_db = gain_db * (1 - 1/ratio) - gain_expansion[i] = 10 ** (expansion_db / 20.0) - else: - gain_expansion[i] = 1.0 - - # 应用增益扩展 - expanded_audio = audio_data * gain_expansion - - print("✓ 扩展器效果已应用") - return expanded_audio - - except Exception as e: - print(f"✗ 应用扩展器效果失败: {e}") - return audio_data - - def apply_deesser(self, audio_data: np.ndarray, threshold: float = -30.0, - frequency: float = 5000.0, sample_rate: int = None) -> np.ndarray: - """ - 应用去齿音器效果 - - Args: - audio_data: 音频数据 - threshold: 阈值(dB) - frequency: 处理频率(Hz) - sample_rate: 采样率 - - Returns: - 处理后的音频数据 - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - # 简化的去齿音器实现 - # 实际实现中会使用更复杂的频域处理 - - # 应用高通滤波器突出高频 - filtered_audio = self.apply_high_pass_filter(audio_data, frequency, sample_rate) - - # 检测高频能量 - threshold_linear = 10 ** (threshold / 20.0) - high_energy = np.abs(filtered_audio) - - # 计算减少因子 - reduction_factor = np.ones_like(high_energy) - reduction_factor[high_energy > threshold_linear] = threshold_linear / high_energy[high_energy > threshold_linear] - - # 应用减少因子 - deessed_audio = audio_data * reduction_factor - - print(f"✓ 去齿音器效果已应用: {frequency}Hz") - return deessed_audio - - except Exception as e: - print(f"✗ 应用去齿音器效果失败: {e}") - return audio_data - - def apply_noise_gate(self, audio_data: np.ndarray, threshold: float = -40.0, - attack: float = 0.001, release: float = 0.01, sample_rate: int = None) -> np.ndarray: - """ - 应用噪声门效果 - - Args: - audio_data: 音频数据 - threshold: 阈值(dB) - attack: 启动时间(秒) - release: 释放时间(秒) - sample_rate: 采样率 - - Returns: - 处理后的音频数据 - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - threshold_linear = 10 ** (threshold / 20.0) - - # 计算包络 - envelope = np.abs(audio_data) - - # 简化的噪声门实现 - gate_open = envelope > threshold_linear - smoothed_gate = np.zeros_like(gate_open, dtype=np.float32) - - # 应用启动和释放时间 - attack_coeff = np.exp(-1.0 / (sample_rate * attack)) - release_coeff = np.exp(-1.0 / (sample_rate * release)) - - gate_state = 0.0 - for i in range(len(gate_open)): - if gate_open[i]: - gate_state = attack_coeff * gate_state + (1 - attack_coeff) - else: - gate_state = release_coeff * gate_state - - smoothed_gate[i] = gate_state - - # 应用噪声门 - gated_audio = audio_data * smoothed_gate - - print("✓ 噪声门效果已应用") - return gated_audio - - except Exception as e: - print(f"✗ 应用噪声门效果失败: {e}") - return audio_data - - def apply_eq_filter(self, audio_data: np.ndarray, frequencies: List[float], - gains: List[float], sample_rate: int = None) -> np.ndarray: - """ - 应用均衡器滤波器 - - Args: - audio_data: 音频数据 - frequencies: 频率列表(Hz) - gains: 增益列表(dB) - sample_rate: 采样率 - - Returns: - 滤波后的音频数据 - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - # 简化的均衡器实现 - eq_audio = audio_data.copy() - - # 对每个频段应用滤波 - for freq, gain in zip(frequencies, gains): - if gain > 0: - # 应用低通或高通滤波器增强特定频率 - if freq < sample_rate / 4: - eq_audio = self.apply_low_pass_filter(eq_audio, freq * 2, sample_rate) - elif gain < 0: - # 应用滤波器减弱特定频率 - if freq > 100: - eq_audio = self.apply_high_pass_filter(eq_audio, freq / 2, sample_rate) - - # 应用总体增益 - total_gain = 10 ** (sum(gains) / len(gains) / 20.0) - eq_audio = eq_audio * total_gain - eq_audio = np.clip(eq_audio, -1.0, 1.0) - - print(f"✓ 均衡器效果已应用: {len(frequencies)}个频段") - return eq_audio - - except Exception as e: - print(f"✗ 应用均衡器效果失败: {e}") - return audio_data - - def apply_reverb(self, audio_data: np.ndarray, room_size: float = 0.5, - damping: float = 0.5, wet_level: float = 0.33, - sample_rate: int = None) -> np.ndarray: - """ - 应用混响效果 - - Args: - audio_data: 音频数据 - room_size: 房间大小 (0.0-1.0) - damping: 阻尼 (0.0-1.0) - wet_level: 湿信号级别 (0.0-1.0) - sample_rate: 采样率 - - Returns: - 添加混响后的音频数据 - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - # 简化的混响实现 - # 创建延迟线 - max_delay = int(sample_rate * 0.1) # 最大100ms延迟 - delay_line = np.zeros(max_delay) - delay_index = 0 - - # 混响参数 - delay_times = [int(sample_rate * 0.013), int(sample_rate * 0.023), - int(sample_rate * 0.037), int(sample_rate * 0.043)] - decay_factors = [0.7, 0.6, 0.5, 0.4] - - # 应用混响 - reverb_audio = np.zeros_like(audio_data) - - for i in range(len(audio_data)): - input_sample = audio_data[i] - - # 从延迟线读取 - delayed_samples = [] - for j, delay_time in enumerate(delay_times): - read_index = (delay_index - delay_time) % max_delay - delayed_samples.append(delay_line[read_index] * decay_factors[j]) - - # 计算反馈 - feedback = sum(delayed_samples) * damping - - # 写入延迟线 - delay_line[delay_index] = input_sample + feedback - - # 更新延迟索引 - delay_index = (delay_index + 1) % max_delay - - # 输出混合信号 - dry_sample = input_sample * (1 - wet_level) - wet_sample = sum(delayed_samples) * wet_level - reverb_audio[i] = dry_sample + wet_sample - - # 防止削波 - reverb_audio = np.clip(reverb_audio, -1.0, 1.0) - - print("✓ 混响效果已应用") - return reverb_audio - - except Exception as e: - print(f"✗ 应用混响效果失败: {e}") - return audio_data - - def apply_chorus(self, audio_data: np.ndarray, rate: float = 1.0, - depth: float = 0.5, mix: float = 0.5, - sample_rate: int = None) -> np.ndarray: - """ - 应用合唱效果 - - Args: - audio_data: 音频数据 - rate: 速率(Hz) - depth: 深度 (0.0-1.0) - mix: 混合比例 (0.0-1.0) - sample_rate: 采样率 - - Returns: - 添加合唱效果后的音频数据 - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - # 创建延迟缓冲区 - delay_buffer = np.zeros(int(sample_rate * 0.05)) # 50ms缓冲区 - write_index = 0 - - # 合唱参数 - lfo_phase = 0.0 - delay_base = 0.005 # 5ms基础延迟 - - # 应用合唱效果 - chorus_audio = np.zeros_like(audio_data) - - for i in range(len(audio_data)): - # 更新LFO相位 - lfo_phase += 2 * np.pi * rate / sample_rate - if lfo_phase > 2 * np.pi: - lfo_phase -= 2 * np.pi - - # 计算延迟时间 - lfo_value = np.sin(lfo_phase) - delay_time = delay_base + depth * 0.01 * lfo_value - delay_samples = delay_time * sample_rate - - # 写入当前样本 - delay_buffer[write_index] = audio_data[i] - - # 计算读取位置 - read_index = write_index - int(delay_samples) - if read_index < 0: - read_index += len(delay_buffer) - - # 线性插值获取延迟样本 - frac = delay_samples - int(delay_samples) - delayed_sample = (delay_buffer[read_index] * (1 - frac) + - delay_buffer[(read_index + 1) % len(delay_buffer)] * frac) - - # 混合干湿信号 - chorus_audio[i] = audio_data[i] * (1 - mix) + delayed_sample * mix - - # 更新写入索引 - write_index = (write_index + 1) % len(delay_buffer) - - print("✓ 合唱效果已应用") - return chorus_audio - - except Exception as e: - print(f"✗ 应用合唱效果失败: {e}") - return audio_data - - def apply_flanger(self, audio_data: np.ndarray, rate: float = 0.5, - depth: float = 0.002, feedback: float = 0.3, - mix: float = 0.5, sample_rate: int = None) -> np.ndarray: - """ - 应用镶边效果 - - Args: - audio_data: 音频数据 - rate: 速率(Hz) - depth: 深度(秒) - feedback: 反馈 (0.0-1.0) - mix: 混合比例 (0.0-1.0) - sample_rate: 采样率 - - Returns: - 添加镶边效果后的音频数据 - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - # 创建延迟缓冲区 - max_delay = int(sample_rate * 0.01) # 10ms最大延迟 - delay_buffer = np.zeros(max_delay) - write_index = 0 - last_output = 0.0 - - # LFO参数 - lfo_phase = 0.0 - - # 应用镶边效果 - flanger_audio = np.zeros_like(audio_data) - - for i in range(len(audio_data)): - # 更新LFO相位 - lfo_phase += 2 * np.pi * rate / sample_rate - if lfo_phase > 2 * np.pi: - lfo_phase -= 2 * np.pi - - # 计算延迟时间 - lfo_value = np.sin(lfo_phase) - delay_time = depth * lfo_value - delay_samples = abs(delay_time) * sample_rate - - # 写入当前样本(带反馈) - delay_buffer[write_index] = audio_data[i] + last_output * feedback - - # 计算读取位置 - read_index = write_index - int(delay_samples) - if read_index < 0: - read_index += len(delay_buffer) - - # 线性插值获取延迟样本 - frac = delay_samples - int(delay_samples) - delayed_sample = (delay_buffer[read_index] * (1 - frac) + - delay_buffer[(read_index + 1) % len(delay_buffer)] * frac) - - # 混合干湿信号 - flanger_audio[i] = audio_data[i] * (1 - mix) + delayed_sample * mix - last_output = flanger_audio[i] - - # 更新写入索引 - write_index = (write_index + 1) % len(delay_buffer) - - print("✓ 镶边效果已应用") - return flanger_audio - - except Exception as e: - print(f"✗ 应用镶边效果失败: {e}") - return audio_data - - def apply_distortion(self, audio_data: np.ndarray, drive: float = 0.5, - tone: float = 0.5) -> np.ndarray: - """ - 应用失真效果 - - Args: - audio_data: 音频数据 - drive: 驱动 (0.0-1.0) - tone: 音调 (0.0-1.0) - - Returns: - 添加失真效果后的音频数据 - """ - try: - # 应用前置增益 - drive_factor = 1.0 + drive * 9.0 # 1x 到 10x 增益 - processed = audio_data * drive_factor - - # 应用软削波失真 - processed = np.tanh(processed) - - # 应用色调控制(简单的高通滤波) - if tone != 0.5: - # 简单的一阶高通滤波器 - rc = 1.0 / (2 * np.pi * (200 + tone * 2000)) # 200Hz 到 2200Hz - dt = 1.0 / self.sample_rate - alpha = rc / (rc + dt) - - for i in range(1, len(processed)): - processed[i] = alpha * (processed[i] + processed[i-1] - processed[i-1]) - - print("✓ 失真效果已应用") - return processed - - except Exception as e: - print(f"✗ 应用失真效果失败: {e}") - return audio_data - - def apply_pitch_shift(self, audio_data: np.ndarray, semitones: float = 0.0, - sample_rate: int = None) -> np.ndarray: - """ - 应用音高移位效果 - - Args: - audio_data: 音频数据 - semitones: 半音数(正数升调,负数降调) - sample_rate: 采样率 - - Returns: - 音高移位后的音频数据 - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - # 计算音高移位因子 - pitch_factor = 2 ** (semitones / 12.0) - - if pitch_factor == 1.0: - return audio_data - - # 简化的音高移位(线性插值) - output_length = int(len(audio_data) / pitch_factor) - if output_length <= 0: - return np.array([], dtype=np.float32) - - processed = np.zeros(output_length) - - # 重采样 - for i in range(output_length): - # 计算源位置 - src_pos = i * pitch_factor - src_index = int(src_pos) - frac = src_pos - src_index - - # 线性插值 - if src_index < len(audio_data) - 1: - processed[i] = audio_data[src_index] * (1 - frac) + \ - audio_data[src_index + 1] * frac - else: - processed[i] = audio_data[min(src_index, len(audio_data) - 1)] - - print(f"✓ 音高移位效果已应用: {semitones}半音") - return processed - - except Exception as e: - print(f"✗ 应用音高移位效果失败: {e}") - return audio_data - - def apply_time_stretch(self, audio_data: np.ndarray, factor: float = 1.0, - sample_rate: int = None) -> np.ndarray: - """ - 应用时间拉伸效果(不改变音高) - - Args: - audio_data: 音频数据 - factor: 拉伸因子(>1.0变慢,<1.0变快) - sample_rate: 采样率 - - Returns: - 时间拉伸后的音频数据 - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - if factor == 1.0: - return audio_data - - # 简化的时间拉伸实现 - new_length = int(len(audio_data) * factor) - if new_length <= 0: - return np.array([], dtype=np.float32) - - # 使用线性插值进行时间拉伸 - indices = np.linspace(0, len(audio_data) - 1, new_length) - stretched_audio = np.interp(indices, np.arange(len(audio_data)), audio_data) - - print(f"✓ 时间拉伸效果已应用: {factor}x") - return stretched_audio - - except Exception as e: - print(f"✗ 应用时间拉伸效果失败: {e}") - return audio_data - - def apply_vibrato(self, audio_data: np.ndarray, rate: float = 5.0, - depth: float = 0.5, sample_rate: int = None) -> np.ndarray: - """ - 应用颤音效果 - - Args: - audio_data: 音频数据 - rate: 速率(Hz) - depth: 深度 (0.0-1.0) - sample_rate: 采样率 - - Returns: - 添加颤音效果后的音频数据 - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - # 使用音高移位实现颤音 - lfo_phase = 0.0 - vibrato_audio = np.zeros_like(audio_data) - - for i in range(len(audio_data)): - # 更新LFO相位 - lfo_phase += 2 * np.pi * rate / sample_rate - if lfo_phase > 2 * np.pi: - lfo_phase -= 2 * np.pi - - # 计算音高变化 - lfo_value = np.sin(lfo_phase) - semitones = depth * 2 * lfo_value # ±2半音变化 - - # 简化处理:对整个信号应用平均音高变化 - # 实际实现中会使用更复杂的逐样本处理 - if i == 0: # 只计算一次作为示例 - vibrato_audio = self.apply_pitch_shift(audio_data, semitones, sample_rate) - - if len(vibrato_audio) == 0: - vibrato_audio = audio_data # 出错时返回原音频 - - print("✓ 颤音效果已应用") - return vibrato_audio - - except Exception as e: - print(f"✗ 应用颤音效果失败: {e}") - return audio_data - - def get_audio_info(self, audio_data: np.ndarray, sample_rate: int = None) -> Dict[str, Any]: - """ - 获取音频信息 - - Args: - audio_data: 音频数据 - sample_rate: 采样率 - - Returns: - 音频信息字典 - """ - try: - if sample_rate is None: - sample_rate = self.sample_rate - - if len(audio_data) == 0: - return { - 'duration': 0.0, - 'samples': 0, - 'sample_rate': sample_rate, - 'channels': self.channels, - 'peak_level': 0.0, - 'rms_level': 0.0 - } - - duration = len(audio_data) / sample_rate - peak_level = float(np.max(np.abs(audio_data))) - rms_level = float(np.sqrt(np.mean(audio_data ** 2))) - - return { - 'duration': duration, - 'samples': len(audio_data), - 'sample_rate': sample_rate, - 'channels': self.channels, - 'peak_level': peak_level, - 'rms_level': rms_level - } - - except Exception as e: - print(f"✗ 获取音频信息失败: {e}") +""" +音频工具类 +提供音频处理和文件操作功能 +""" + +import numpy as np +import wave +import struct +from typing import Dict, Any, List +import os + +class AudioUtils: + """ + 音频工具类 + 提供音频处理和文件操作功能 + """ + + def __init__(self, plugin): + """ + 初始化音频工具类 + + Args: + plugin: 语音识别和合成插件实例 + """ + self.plugin = plugin + self.sample_rate = plugin.config.get('sample_rate', 16000) + self.channels = plugin.config.get('channels', 1) + self.sample_width = 2 # 16位音频 + + print("✓ 音频工具类已创建") + + def save_audio_file(self, filename: str, audio_data: bytes) -> bool: + """ + 保存音频文件 + + Args: + filename: 文件名 + audio_data: 音频数据 + + Returns: + 是否保存成功 + """ + try: + # 确保目录存在 + os.makedirs(os.path.dirname(filename) if os.path.dirname(filename) else '.', exist_ok=True) + + # 保存音频数据到文件 + with open(filename, 'wb') as f: + f.write(audio_data) + + print(f"✓ 音频文件已保存: {filename}") + return True + + except Exception as e: + print(f"✗ 保存音频文件失败: {e}") + import traceback + traceback.print_exc() + return False + + def load_audio_file(self, filename: str) -> bytes: + """ + 加载音频文件 + + Args: + filename: 文件名 + + Returns: + 音频数据 + """ + try: + with open(filename, 'rb') as f: + audio_data = f.read() + print(f"✓ 音频文件已加载: {filename}") + return audio_data + + except Exception as e: + print(f"✗ 加载音频文件失败: {e}") + import traceback + traceback.print_exc() + return b'' + + def save_wav_file(self, filename: str, audio_data: np.ndarray, sample_rate: int = None) -> bool: + """ + 保存WAV音频文件 + + Args: + filename: 文件名 + audio_data: 音频数据(numpy数组) + sample_rate: 采样率 + + Returns: + 是否保存成功 + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + # 确保目录存在 + os.makedirs(os.path.dirname(filename) if os.path.dirname(filename) else '.', exist_ok=True) + + # 确保音频数据在合法范围内 + audio_data = np.clip(audio_data, -1.0, 1.0) + + # 转换为16位整数 + audio_int16 = (audio_data * 32767).astype(np.int16) + + # 保存为WAV文件 + with wave.open(filename, 'w') as wav_file: + wav_file.setnchannels(self.channels) + wav_file.setsampwidth(self.sample_width) # 16位 = 2字节 + wav_file.setframerate(sample_rate) + wav_file.writeframes(audio_int16.tobytes()) + + print(f"✓ WAV文件已保存: {filename}") + return True + + except Exception as e: + print(f"✗ 保存WAV文件失败: {e}") + import traceback + traceback.print_exc() + return False + + def load_wav_file(self, filename: str) -> Dict[str, Any]: + """ + 加载WAV音频文件 + + Args: + filename: 文件名 + + Returns: + 包含音频数据和参数的字典 + """ + try: + with wave.open(filename, 'r') as wav_file: + # 获取音频参数 + channels = wav_file.getnchannels() + sample_width = wav_file.getsampwidth() + sample_rate = wav_file.getframerate() + frames = wav_file.getnframes() + + # 读取音频数据 + audio_data = wav_file.readframes(frames) + + # 转换为numpy数组 + if sample_width == 1: + # 8位音频 + audio_array = np.frombuffer(audio_data, dtype=np.uint8) + audio_array = (audio_array.astype(np.float32) - 128) / 128.0 + elif sample_width == 2: + # 16位音频 + audio_array = np.frombuffer(audio_data, dtype=np.int16) + audio_array = audio_array.astype(np.float32) / 32768.0 + elif sample_width == 4: + # 32位音频 + audio_array = np.frombuffer(audio_data, dtype=np.int32) + audio_array = audio_array.astype(np.float32) / 2147483648.0 + else: + print(f"✗ 不支持的采样宽度: {sample_width}") + return {} + + print(f"✓ WAV文件已加载: {filename}") + return { + 'audio_data': audio_array, + 'sample_rate': sample_rate, + 'channels': channels, + 'sample_width': sample_width, + 'frames': frames + } + + except Exception as e: + print(f"✗ 加载WAV文件失败: {e}") + import traceback + traceback.print_exc() + return {} + + def convert_audio_format(self, audio_data: np.ndarray, from_format: str, to_format: str) -> np.ndarray: + """ + 转换音频格式 + + Args: + audio_data: 音频数据 + from_format: 源格式 + to_format: 目标格式 + + Returns: + 转换后的音频数据 + """ + try: + # 这里简化处理,实际项目中可能需要更复杂的格式转换 + print(f"✓ 音频格式已转换: {from_format} -> {to_format}") + return audio_data + except Exception as e: + print(f"✗ 音频格式转换失败: {e}") + return audio_data + + def resample_audio(self, audio_data: np.ndarray, from_rate: int, to_rate: int) -> np.ndarray: + """ + 重采样音频数据 + + Args: + audio_data: 音频数据 + from_rate: 源采样率 + to_rate: 目标采样率 + + Returns: + 重采样后的音频数据 + """ + try: + if from_rate == to_rate: + return audio_data + + # 计算重采样比例 + ratio = to_rate / from_rate + new_length = int(len(audio_data) * ratio) + + # 简单的线性插值(实际项目中应该使用更高质量的重采样算法) + if new_length > 0: + indices = np.linspace(0, len(audio_data) - 1, new_length) + resampled_data = np.interp(indices, np.arange(len(audio_data)), audio_data) + print(f"✓ 音频已重采样: {from_rate}Hz -> {to_rate}Hz") + return resampled_data + else: + return np.array([], dtype=np.float32) + + except Exception as e: + print(f"✗ 音频重采样失败: {e}") + return audio_data + + def apply_gain(self, audio_data: np.ndarray, gain_db: float) -> np.ndarray: + """ + 应用增益到音频数据 + + Args: + audio_data: 音频数据 + gain_db: 增益值(分贝) + + Returns: + 应用增益后的音频数据 + """ + try: + # 将分贝转换为线性增益 + gain_linear = 10 ** (gain_db / 20.0) + amplified_data = audio_data * gain_linear + + # 防止削波 + amplified_data = np.clip(amplified_data, -1.0, 1.0) + + print(f"✓ 音频增益已应用: {gain_db}dB") + return amplified_data + + except Exception as e: + print(f"✗ 应用音频增益失败: {e}") + return audio_data + + def normalize_audio(self, audio_data: np.ndarray) -> np.ndarray: + """ + 归一化音频数据 + + Args: + audio_data: 音频数据 + + Returns: + 归一化后的音频数据 + """ + try: + if len(audio_data) == 0: + return audio_data + + # 计算最大幅度 + max_amplitude = np.max(np.abs(audio_data)) + + if max_amplitude > 0: + normalized_data = audio_data / max_amplitude + print("✓ 音频已归一化") + return normalized_data + else: + return audio_data + + except Exception as e: + print(f"✗ 音频归一化失败: {e}") + return audio_data + + def apply_fade_in(self, audio_data: np.ndarray, fade_duration: float, sample_rate: int = None) -> np.ndarray: + """ + 应用淡入效果 + + Args: + audio_data: 音频数据 + fade_duration: 淡入时长(秒) + sample_rate: 采样率 + + Returns: + 应用淡入效果后的音频数据 + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + fade_samples = int(fade_duration * sample_rate) + if fade_samples <= 0 or fade_samples >= len(audio_data): + return audio_data + + # 创建淡入曲线 + fade_curve = np.linspace(0, 1, fade_samples) + + # 应用淡入效果 + faded_data = audio_data.copy() + faded_data[:fade_samples] *= fade_curve + + print(f"✓ 淡入效果已应用: {fade_duration}秒") + return faded_data + + except Exception as e: + print(f"✗ 应用淡入效果失败: {e}") + return audio_data + + def apply_fade_out(self, audio_data: np.ndarray, fade_duration: float, sample_rate: int = None) -> np.ndarray: + """ + 应用淡出效果 + + Args: + audio_data: 音频数据 + fade_duration: 淡出时长(秒) + sample_rate: 采样率 + + Returns: + 应用淡出效果后的音频数据 + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + fade_samples = int(fade_duration * sample_rate) + if fade_samples <= 0 or fade_samples >= len(audio_data): + return audio_data + + # 创建淡出曲线 + fade_curve = np.linspace(1, 0, fade_samples) + + # 应用淡出效果 + faded_data = audio_data.copy() + faded_data[-fade_samples:] *= fade_curve + + print(f"✓ 淡出效果已应用: {fade_duration}秒") + return faded_data + + except Exception as e: + print(f"✗ 应用淡出效果失败: {e}") + return audio_data + + def concatenate_audio(self, audio_segments: List[np.ndarray]) -> np.ndarray: + """ + 连接多个音频片段 + + Args: + audio_segments: 音频片段列表 + + Returns: + 连接后的音频数据 + """ + try: + if not audio_segments: + return np.array([], dtype=np.float32) + + # 连接所有音频片段 + concatenated = np.concatenate(audio_segments) + print(f"✓ {len(audio_segments)}个音频片段已连接") + return concatenated + + except Exception as e: + print(f"✗ 连接音频片段失败: {e}") + return np.array([], dtype=np.float32) + + def split_audio(self, audio_data: np.ndarray, segment_duration: float, sample_rate: int = None) -> List[np.ndarray]: + """ + 分割音频数据为多个片段 + + Args: + audio_data: 音频数据 + segment_duration: 片段时长(秒) + sample_rate: 采样率 + + Returns: + 音频片段列表 + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + segment_samples = int(segment_duration * sample_rate) + if segment_samples <= 0: + return [audio_data] + + # 分割音频数据 + segments = [] + for i in range(0, len(audio_data), segment_samples): + segment = audio_data[i:i + segment_samples] + segments.append(segment) + + print(f"✓ 音频已分割为{len(segments)}个片段") + return segments + + except Exception as e: + print(f"✗ 分割音频失败: {e}") + return [audio_data] + + def calculate_audio_level(self, audio_data: np.ndarray) -> float: + """ + 计算音频级别(RMS) + + Args: + audio_data: 音频数据 + + Returns: + 音频级别 (0.0-1.0) + """ + try: + if len(audio_data) == 0: + return 0.0 + + # 计算RMS值 + rms = np.sqrt(np.mean(audio_data ** 2)) + return float(rms) + + except Exception as e: + print(f"✗ 计算音频级别失败: {e}") + return 0.0 + + def detect_silence(self, audio_data: np.ndarray, threshold: float = 0.01, + min_silence_duration: float = 0.1, sample_rate: int = None) -> List[Dict[str, float]]: + """ + 检测音频中的静音段 + + Args: + audio_data: 音频数据 + threshold: 静音阈值 + min_silence_duration: 最小静音时长(秒) + sample_rate: 采样率 + + Returns: + 静音段列表 [{'start': start_time, 'end': end_time}] + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + min_silence_samples = int(min_silence_duration * sample_rate) + silence_segments = [] + + # 简化的静音检测 + is_silent = np.abs(audio_data) < threshold + in_silence = False + silence_start = 0 + + for i in range(len(is_silent)): + if is_silent[i] and not in_silence: + # 开始静音段 + in_silence = True + silence_start = i + elif not is_silent[i] and in_silence: + # 结束静音段 + in_silence = False + silence_duration = i - silence_start + if silence_duration >= min_silence_samples: + silence_segments.append({ + 'start': silence_start / sample_rate, + 'end': i / sample_rate + }) + + # 处理最后一个静音段 + if in_silence: + silence_duration = len(audio_data) - silence_start + if silence_duration >= min_silence_samples: + silence_segments.append({ + 'start': silence_start / sample_rate, + 'end': len(audio_data) / sample_rate + }) + + print(f"✓ 检测到{len(silence_segments)}个静音段") + return silence_segments + + except Exception as e: + print(f"✗ 检测静音段失败: {e}") + return [] + + def remove_silence(self, audio_data: np.ndarray, threshold: float = 0.01, + min_silence_duration: float = 0.1, sample_rate: int = None) -> np.ndarray: + """ + 移除音频中的静音段 + + Args: + audio_data: 音频数据 + threshold: 静音阈值 + min_silence_duration: 最小静音时长(秒) + sample_rate: 采样率 + + Returns: + 移除静音后的音频数据 + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + # 检测静音段 + silence_segments = self.detect_silence(audio_data, threshold, min_silence_duration, sample_rate) + + if not silence_segments: + return audio_data + + # 移除静音段 + cleaned_audio = audio_data.copy() + for segment in reversed(silence_segments): # 从后往前删除,避免索引变化 + start_sample = int(segment['start'] * sample_rate) + end_sample = int(segment['end'] * sample_rate) + cleaned_audio = np.concatenate([ + cleaned_audio[:start_sample], + cleaned_audio[end_sample:] + ]) + + print(f"✓ 已移除{len(silence_segments)}个静音段") + return cleaned_audio + + except Exception as e: + print(f"✗ 移除静音段失败: {e}") + return audio_data + + def apply_high_pass_filter(self, audio_data: np.ndarray, cutoff_freq: float, + sample_rate: int = None) -> np.ndarray: + """ + 应用高通滤波器 + + Args: + audio_data: 音频数据 + cutoff_freq: 截止频率(Hz) + sample_rate: 采样率 + + Returns: + 滤波后的音频数据 + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + # 简化的高通滤波实现 + # 实际项目中应该使用更专业的滤波器设计 + rc = 1.0 / (2 * np.pi * cutoff_freq) + dt = 1.0 / sample_rate + alpha = rc / (rc + dt) + + filtered_data = np.zeros_like(audio_data) + for i in range(1, len(audio_data)): + filtered_data[i] = alpha * (filtered_data[i-1] + audio_data[i] - audio_data[i-1]) + + print(f"✓ 高通滤波器已应用: {cutoff_freq}Hz") + return filtered_data + + except Exception as e: + print(f"✗ 应用高通滤波器失败: {e}") + return audio_data + + def apply_low_pass_filter(self, audio_data: np.ndarray, cutoff_freq: float, + sample_rate: int = None) -> np.ndarray: + """ + 应用低通滤波器 + + Args: + audio_data: 音频数据 + cutoff_freq: 截止频率(Hz) + sample_rate: 采样率 + + Returns: + 滤波后的音频数据 + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + # 简化的低通滤波实现 + rc = 1.0 / (2 * np.pi * cutoff_freq) + dt = 1.0 / sample_rate + alpha = dt / (rc + dt) + + filtered_data = np.zeros_like(audio_data) + filtered_data[0] = audio_data[0] + for i in range(1, len(audio_data)): + filtered_data[i] = alpha * audio_data[i] + (1 - alpha) * filtered_data[i-1] + + print(f"✓ 低通滤波器已应用: {cutoff_freq}Hz") + return filtered_data + + except Exception as e: + print(f"✗ 应用低通滤波器失败: {e}") + return audio_data + + def generate_sine_wave(self, frequency: float, duration: float, + amplitude: float = 1.0, sample_rate: int = None) -> np.ndarray: + """ + 生成正弦波音频 + + Args: + frequency: 频率(Hz) + duration: 时长(秒) + amplitude: 振幅 (0.0-1.0) + sample_rate: 采样率 + + Returns: + 正弦波音频数据 + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + # 生成时间轴 + t = np.linspace(0, duration, int(sample_rate * duration), False) + + # 生成正弦波 + sine_wave = amplitude * np.sin(2 * np.pi * frequency * t) + + print(f"✓ 正弦波已生成: {frequency}Hz, {duration}秒") + return sine_wave + + except Exception as e: + print(f"✗ 生成正弦波失败: {e}") + return np.array([], dtype=np.float32) + + def generate_white_noise(self, duration: float, amplitude: float = 0.1, + sample_rate: int = None) -> np.ndarray: + """ + 生成白噪声音频 + + Args: + duration: 时长(秒) + amplitude: 振幅 (0.0-1.0) + sample_rate: 采样率 + + Returns: + 白噪声音频数据 + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + # 生成样本数 + num_samples = int(sample_rate * duration) + + # 生成白噪声 + white_noise = amplitude * (np.random.random(num_samples) * 2 - 1) + + print(f"✓ 白噪声已生成: {duration}秒") + return white_noise + + except Exception as e: + print(f"✗ 生成白噪声失败: {e}") + return np.array([], dtype=np.float32) + + def mix_audio_signals(self, audio1: np.ndarray, audio2: np.ndarray, + ratio: float = 0.5) -> np.ndarray: + """ + 混合两个音频信号 + + Args: + audio1: 第一个音频信号 + audio2: 第二个音频信号 + ratio: 混合比例 (0.0-1.0, 0表示全audio1,1表示全audio2) + + Returns: + 混合后的音频信号 + """ + try: + # 确保两个音频信号长度相同 + max_length = max(len(audio1), len(audio2)) + if len(audio1) < max_length: + audio1 = np.pad(audio1, (0, max_length - len(audio1))) + if len(audio2) < max_length: + audio2 = np.pad(audio2, (0, max_length - len(audio2))) + + # 混合音频 + mixed_audio = (1 - ratio) * audio1 + ratio * audio2 + + # 防止削波 + mixed_audio = np.clip(mixed_audio, -1.0, 1.0) + + print("✓ 音频信号已混合") + return mixed_audio + + except Exception as e: + print(f"✗ 混合音频信号失败: {e}") + # 返回较长的音频信号 + return audio1 if len(audio1) >= len(audio2) else audio2 + + def calculate_audio_duration(self, audio_data: np.ndarray, sample_rate: int = None) -> float: + """ + 计算音频时长 + + Args: + audio_data: 音频数据 + sample_rate: 采样率 + + Returns: + 音频时长(秒) + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + if len(audio_data) == 0: + return 0.0 + + duration = len(audio_data) / sample_rate + return duration + + except Exception as e: + print(f"✗ 计算音频时长失败: {e}") + return 0.0 + + def convert_to_mono(self, audio_data: np.ndarray, channels: int = None) -> np.ndarray: + """ + 转换为单声道音频 + + Args: + audio_data: 音频数据 + channels: 声道数 + + Returns: + 单声道音频数据 + """ + try: + if channels is None: + channels = self.channels + + if channels == 1: + return audio_data + + # 假设音频数据是交错存储的 + if len(audio_data) % channels != 0: + print("✗ 音频数据长度与声道数不匹配") + return audio_data + + # 重新整形并计算平均值 + reshaped = audio_data.reshape(-1, channels) + mono_audio = np.mean(reshaped, axis=1) + + print("✓ 音频已转换为单声道") + return mono_audio + + except Exception as e: + print(f"✗ 转换为单声道失败: {e}") + return audio_data + + def convert_to_stereo(self, audio_data: np.ndarray) -> np.ndarray: + """ + 转换为立体声音频(复制单声道到两个声道) + + Args: + audio_data: 单声道音频数据 + + Returns: + 立体声音频数据 + """ + try: + # 将单声道数据复制到两个声道 + stereo_audio = np.tile(audio_data.reshape(-1, 1), (1, 2)) + stereo_audio = stereo_audio.flatten() + + print("✓ 音频已转换为立体声") + return stereo_audio + + except Exception as e: + print(f"✗ 转换为立体声失败: {e}") + return audio_data + + def apply_compressor(self, audio_data: np.ndarray, threshold: float = -20.0, + ratio: float = 4.0, attack: float = 0.01, release: float = 0.1) -> np.ndarray: + """ + 应用压缩器效果 + + Args: + audio_data: 音频数据 + threshold: 阈值(dB) + ratio: 压缩比 + attack: 启动时间(秒) + release: 释放时间(秒) + + Returns: + 压缩后的音频数据 + """ + try: + # 简化的压缩器实现 + threshold_linear = 10 ** (threshold / 20.0) + + # 计算增益缩减 + gain_reduction = np.zeros_like(audio_data) + envelope = 0.0 + + for i in range(len(audio_data)): + # 计算包络 + envelope = max(abs(audio_data[i]), envelope * 0.99) + + # 计算增益缩减 + if envelope > threshold_linear: + gain_db = 20 * np.log10(envelope / threshold_linear) + reduction_db = gain_db - gain_db / ratio + gain_reduction[i] = 10 ** (-reduction_db / 20.0) + else: + gain_reduction[i] = 1.0 + + # 应用增益缩减 + compressed_audio = audio_data * gain_reduction + + print("✓ 压缩器效果已应用") + return compressed_audio + + except Exception as e: + print(f"✗ 应用压缩器效果失败: {e}") + return audio_data + + def apply_limiter(self, audio_data: np.ndarray, threshold: float = -0.1) -> np.ndarray: + """ + 应用限制器效果 + + Args: + audio_data: 音频数据 + threshold: 阈值(dB,相对于满量程) + + Returns: + 限制后的音频数据 + """ + try: + # 转换阈值为线性值 + threshold_linear = 10 ** (threshold / 20.0) + + # 应用限制器 + limited_audio = np.clip(audio_data, -threshold_linear, threshold_linear) + + print(f"✓ 限制器效果已应用: {threshold}dB") + return limited_audio + + except Exception as e: + print(f"✗ 应用限制器效果失败: {e}") + return audio_data + + def apply_expander(self, audio_data: np.ndarray, threshold: float = -40.0, + ratio: float = 2.0, attack: float = 0.01, release: float = 0.1) -> np.ndarray: + """ + 应用扩展器效果 + + Args: + audio_data: 音频数据 + threshold: 阈值(dB) + ratio: 扩展比 + attack: 启动时间(秒) + release: 释放时间(秒) + + Returns: + 扩展后的音频数据 + """ + try: + # 简化的扩展器实现 + threshold_linear = 10 ** (threshold / 20.0) + + # 计算增益扩展 + gain_expansion = np.zeros_like(audio_data) + envelope = 0.0 + + for i in range(len(audio_data)): + # 计算包络 + envelope = max(abs(audio_data[i]), envelope * 0.99) + + # 计算增益扩展 + if envelope < threshold_linear: + gain_db = 20 * np.log10(envelope / threshold_linear) + expansion_db = gain_db * (1 - 1/ratio) + gain_expansion[i] = 10 ** (expansion_db / 20.0) + else: + gain_expansion[i] = 1.0 + + # 应用增益扩展 + expanded_audio = audio_data * gain_expansion + + print("✓ 扩展器效果已应用") + return expanded_audio + + except Exception as e: + print(f"✗ 应用扩展器效果失败: {e}") + return audio_data + + def apply_deesser(self, audio_data: np.ndarray, threshold: float = -30.0, + frequency: float = 5000.0, sample_rate: int = None) -> np.ndarray: + """ + 应用去齿音器效果 + + Args: + audio_data: 音频数据 + threshold: 阈值(dB) + frequency: 处理频率(Hz) + sample_rate: 采样率 + + Returns: + 处理后的音频数据 + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + # 简化的去齿音器实现 + # 实际实现中会使用更复杂的频域处理 + + # 应用高通滤波器突出高频 + filtered_audio = self.apply_high_pass_filter(audio_data, frequency, sample_rate) + + # 检测高频能量 + threshold_linear = 10 ** (threshold / 20.0) + high_energy = np.abs(filtered_audio) + + # 计算减少因子 + reduction_factor = np.ones_like(high_energy) + reduction_factor[high_energy > threshold_linear] = threshold_linear / high_energy[high_energy > threshold_linear] + + # 应用减少因子 + deessed_audio = audio_data * reduction_factor + + print(f"✓ 去齿音器效果已应用: {frequency}Hz") + return deessed_audio + + except Exception as e: + print(f"✗ 应用去齿音器效果失败: {e}") + return audio_data + + def apply_noise_gate(self, audio_data: np.ndarray, threshold: float = -40.0, + attack: float = 0.001, release: float = 0.01, sample_rate: int = None) -> np.ndarray: + """ + 应用噪声门效果 + + Args: + audio_data: 音频数据 + threshold: 阈值(dB) + attack: 启动时间(秒) + release: 释放时间(秒) + sample_rate: 采样率 + + Returns: + 处理后的音频数据 + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + threshold_linear = 10 ** (threshold / 20.0) + + # 计算包络 + envelope = np.abs(audio_data) + + # 简化的噪声门实现 + gate_open = envelope > threshold_linear + smoothed_gate = np.zeros_like(gate_open, dtype=np.float32) + + # 应用启动和释放时间 + attack_coeff = np.exp(-1.0 / (sample_rate * attack)) + release_coeff = np.exp(-1.0 / (sample_rate * release)) + + gate_state = 0.0 + for i in range(len(gate_open)): + if gate_open[i]: + gate_state = attack_coeff * gate_state + (1 - attack_coeff) + else: + gate_state = release_coeff * gate_state + + smoothed_gate[i] = gate_state + + # 应用噪声门 + gated_audio = audio_data * smoothed_gate + + print("✓ 噪声门效果已应用") + return gated_audio + + except Exception as e: + print(f"✗ 应用噪声门效果失败: {e}") + return audio_data + + def apply_eq_filter(self, audio_data: np.ndarray, frequencies: List[float], + gains: List[float], sample_rate: int = None) -> np.ndarray: + """ + 应用均衡器滤波器 + + Args: + audio_data: 音频数据 + frequencies: 频率列表(Hz) + gains: 增益列表(dB) + sample_rate: 采样率 + + Returns: + 滤波后的音频数据 + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + # 简化的均衡器实现 + eq_audio = audio_data.copy() + + # 对每个频段应用滤波 + for freq, gain in zip(frequencies, gains): + if gain > 0: + # 应用低通或高通滤波器增强特定频率 + if freq < sample_rate / 4: + eq_audio = self.apply_low_pass_filter(eq_audio, freq * 2, sample_rate) + elif gain < 0: + # 应用滤波器减弱特定频率 + if freq > 100: + eq_audio = self.apply_high_pass_filter(eq_audio, freq / 2, sample_rate) + + # 应用总体增益 + total_gain = 10 ** (sum(gains) / len(gains) / 20.0) + eq_audio = eq_audio * total_gain + eq_audio = np.clip(eq_audio, -1.0, 1.0) + + print(f"✓ 均衡器效果已应用: {len(frequencies)}个频段") + return eq_audio + + except Exception as e: + print(f"✗ 应用均衡器效果失败: {e}") + return audio_data + + def apply_reverb(self, audio_data: np.ndarray, room_size: float = 0.5, + damping: float = 0.5, wet_level: float = 0.33, + sample_rate: int = None) -> np.ndarray: + """ + 应用混响效果 + + Args: + audio_data: 音频数据 + room_size: 房间大小 (0.0-1.0) + damping: 阻尼 (0.0-1.0) + wet_level: 湿信号级别 (0.0-1.0) + sample_rate: 采样率 + + Returns: + 添加混响后的音频数据 + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + # 简化的混响实现 + # 创建延迟线 + max_delay = int(sample_rate * 0.1) # 最大100ms延迟 + delay_line = np.zeros(max_delay) + delay_index = 0 + + # 混响参数 + delay_times = [int(sample_rate * 0.013), int(sample_rate * 0.023), + int(sample_rate * 0.037), int(sample_rate * 0.043)] + decay_factors = [0.7, 0.6, 0.5, 0.4] + + # 应用混响 + reverb_audio = np.zeros_like(audio_data) + + for i in range(len(audio_data)): + input_sample = audio_data[i] + + # 从延迟线读取 + delayed_samples = [] + for j, delay_time in enumerate(delay_times): + read_index = (delay_index - delay_time) % max_delay + delayed_samples.append(delay_line[read_index] * decay_factors[j]) + + # 计算反馈 + feedback = sum(delayed_samples) * damping + + # 写入延迟线 + delay_line[delay_index] = input_sample + feedback + + # 更新延迟索引 + delay_index = (delay_index + 1) % max_delay + + # 输出混合信号 + dry_sample = input_sample * (1 - wet_level) + wet_sample = sum(delayed_samples) * wet_level + reverb_audio[i] = dry_sample + wet_sample + + # 防止削波 + reverb_audio = np.clip(reverb_audio, -1.0, 1.0) + + print("✓ 混响效果已应用") + return reverb_audio + + except Exception as e: + print(f"✗ 应用混响效果失败: {e}") + return audio_data + + def apply_chorus(self, audio_data: np.ndarray, rate: float = 1.0, + depth: float = 0.5, mix: float = 0.5, + sample_rate: int = None) -> np.ndarray: + """ + 应用合唱效果 + + Args: + audio_data: 音频数据 + rate: 速率(Hz) + depth: 深度 (0.0-1.0) + mix: 混合比例 (0.0-1.0) + sample_rate: 采样率 + + Returns: + 添加合唱效果后的音频数据 + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + # 创建延迟缓冲区 + delay_buffer = np.zeros(int(sample_rate * 0.05)) # 50ms缓冲区 + write_index = 0 + + # 合唱参数 + lfo_phase = 0.0 + delay_base = 0.005 # 5ms基础延迟 + + # 应用合唱效果 + chorus_audio = np.zeros_like(audio_data) + + for i in range(len(audio_data)): + # 更新LFO相位 + lfo_phase += 2 * np.pi * rate / sample_rate + if lfo_phase > 2 * np.pi: + lfo_phase -= 2 * np.pi + + # 计算延迟时间 + lfo_value = np.sin(lfo_phase) + delay_time = delay_base + depth * 0.01 * lfo_value + delay_samples = delay_time * sample_rate + + # 写入当前样本 + delay_buffer[write_index] = audio_data[i] + + # 计算读取位置 + read_index = write_index - int(delay_samples) + if read_index < 0: + read_index += len(delay_buffer) + + # 线性插值获取延迟样本 + frac = delay_samples - int(delay_samples) + delayed_sample = (delay_buffer[read_index] * (1 - frac) + + delay_buffer[(read_index + 1) % len(delay_buffer)] * frac) + + # 混合干湿信号 + chorus_audio[i] = audio_data[i] * (1 - mix) + delayed_sample * mix + + # 更新写入索引 + write_index = (write_index + 1) % len(delay_buffer) + + print("✓ 合唱效果已应用") + return chorus_audio + + except Exception as e: + print(f"✗ 应用合唱效果失败: {e}") + return audio_data + + def apply_flanger(self, audio_data: np.ndarray, rate: float = 0.5, + depth: float = 0.002, feedback: float = 0.3, + mix: float = 0.5, sample_rate: int = None) -> np.ndarray: + """ + 应用镶边效果 + + Args: + audio_data: 音频数据 + rate: 速率(Hz) + depth: 深度(秒) + feedback: 反馈 (0.0-1.0) + mix: 混合比例 (0.0-1.0) + sample_rate: 采样率 + + Returns: + 添加镶边效果后的音频数据 + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + # 创建延迟缓冲区 + max_delay = int(sample_rate * 0.01) # 10ms最大延迟 + delay_buffer = np.zeros(max_delay) + write_index = 0 + last_output = 0.0 + + # LFO参数 + lfo_phase = 0.0 + + # 应用镶边效果 + flanger_audio = np.zeros_like(audio_data) + + for i in range(len(audio_data)): + # 更新LFO相位 + lfo_phase += 2 * np.pi * rate / sample_rate + if lfo_phase > 2 * np.pi: + lfo_phase -= 2 * np.pi + + # 计算延迟时间 + lfo_value = np.sin(lfo_phase) + delay_time = depth * lfo_value + delay_samples = abs(delay_time) * sample_rate + + # 写入当前样本(带反馈) + delay_buffer[write_index] = audio_data[i] + last_output * feedback + + # 计算读取位置 + read_index = write_index - int(delay_samples) + if read_index < 0: + read_index += len(delay_buffer) + + # 线性插值获取延迟样本 + frac = delay_samples - int(delay_samples) + delayed_sample = (delay_buffer[read_index] * (1 - frac) + + delay_buffer[(read_index + 1) % len(delay_buffer)] * frac) + + # 混合干湿信号 + flanger_audio[i] = audio_data[i] * (1 - mix) + delayed_sample * mix + last_output = flanger_audio[i] + + # 更新写入索引 + write_index = (write_index + 1) % len(delay_buffer) + + print("✓ 镶边效果已应用") + return flanger_audio + + except Exception as e: + print(f"✗ 应用镶边效果失败: {e}") + return audio_data + + def apply_distortion(self, audio_data: np.ndarray, drive: float = 0.5, + tone: float = 0.5) -> np.ndarray: + """ + 应用失真效果 + + Args: + audio_data: 音频数据 + drive: 驱动 (0.0-1.0) + tone: 音调 (0.0-1.0) + + Returns: + 添加失真效果后的音频数据 + """ + try: + # 应用前置增益 + drive_factor = 1.0 + drive * 9.0 # 1x 到 10x 增益 + processed = audio_data * drive_factor + + # 应用软削波失真 + processed = np.tanh(processed) + + # 应用色调控制(简单的高通滤波) + if tone != 0.5: + # 简单的一阶高通滤波器 + rc = 1.0 / (2 * np.pi * (200 + tone * 2000)) # 200Hz 到 2200Hz + dt = 1.0 / self.sample_rate + alpha = rc / (rc + dt) + + for i in range(1, len(processed)): + processed[i] = alpha * (processed[i] + processed[i-1] - processed[i-1]) + + print("✓ 失真效果已应用") + return processed + + except Exception as e: + print(f"✗ 应用失真效果失败: {e}") + return audio_data + + def apply_pitch_shift(self, audio_data: np.ndarray, semitones: float = 0.0, + sample_rate: int = None) -> np.ndarray: + """ + 应用音高移位效果 + + Args: + audio_data: 音频数据 + semitones: 半音数(正数升调,负数降调) + sample_rate: 采样率 + + Returns: + 音高移位后的音频数据 + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + # 计算音高移位因子 + pitch_factor = 2 ** (semitones / 12.0) + + if pitch_factor == 1.0: + return audio_data + + # 简化的音高移位(线性插值) + output_length = int(len(audio_data) / pitch_factor) + if output_length <= 0: + return np.array([], dtype=np.float32) + + processed = np.zeros(output_length) + + # 重采样 + for i in range(output_length): + # 计算源位置 + src_pos = i * pitch_factor + src_index = int(src_pos) + frac = src_pos - src_index + + # 线性插值 + if src_index < len(audio_data) - 1: + processed[i] = audio_data[src_index] * (1 - frac) + \ + audio_data[src_index + 1] * frac + else: + processed[i] = audio_data[min(src_index, len(audio_data) - 1)] + + print(f"✓ 音高移位效果已应用: {semitones}半音") + return processed + + except Exception as e: + print(f"✗ 应用音高移位效果失败: {e}") + return audio_data + + def apply_time_stretch(self, audio_data: np.ndarray, factor: float = 1.0, + sample_rate: int = None) -> np.ndarray: + """ + 应用时间拉伸效果(不改变音高) + + Args: + audio_data: 音频数据 + factor: 拉伸因子(>1.0变慢,<1.0变快) + sample_rate: 采样率 + + Returns: + 时间拉伸后的音频数据 + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + if factor == 1.0: + return audio_data + + # 简化的时间拉伸实现 + new_length = int(len(audio_data) * factor) + if new_length <= 0: + return np.array([], dtype=np.float32) + + # 使用线性插值进行时间拉伸 + indices = np.linspace(0, len(audio_data) - 1, new_length) + stretched_audio = np.interp(indices, np.arange(len(audio_data)), audio_data) + + print(f"✓ 时间拉伸效果已应用: {factor}x") + return stretched_audio + + except Exception as e: + print(f"✗ 应用时间拉伸效果失败: {e}") + return audio_data + + def apply_vibrato(self, audio_data: np.ndarray, rate: float = 5.0, + depth: float = 0.5, sample_rate: int = None) -> np.ndarray: + """ + 应用颤音效果 + + Args: + audio_data: 音频数据 + rate: 速率(Hz) + depth: 深度 (0.0-1.0) + sample_rate: 采样率 + + Returns: + 添加颤音效果后的音频数据 + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + # 使用音高移位实现颤音 + lfo_phase = 0.0 + vibrato_audio = np.zeros_like(audio_data) + + for i in range(len(audio_data)): + # 更新LFO相位 + lfo_phase += 2 * np.pi * rate / sample_rate + if lfo_phase > 2 * np.pi: + lfo_phase -= 2 * np.pi + + # 计算音高变化 + lfo_value = np.sin(lfo_phase) + semitones = depth * 2 * lfo_value # ±2半音变化 + + # 简化处理:对整个信号应用平均音高变化 + # 实际实现中会使用更复杂的逐样本处理 + if i == 0: # 只计算一次作为示例 + vibrato_audio = self.apply_pitch_shift(audio_data, semitones, sample_rate) + + if len(vibrato_audio) == 0: + vibrato_audio = audio_data # 出错时返回原音频 + + print("✓ 颤音效果已应用") + return vibrato_audio + + except Exception as e: + print(f"✗ 应用颤音效果失败: {e}") + return audio_data + + def get_audio_info(self, audio_data: np.ndarray, sample_rate: int = None) -> Dict[str, Any]: + """ + 获取音频信息 + + Args: + audio_data: 音频数据 + sample_rate: 采样率 + + Returns: + 音频信息字典 + """ + try: + if sample_rate is None: + sample_rate = self.sample_rate + + if len(audio_data) == 0: + return { + 'duration': 0.0, + 'samples': 0, + 'sample_rate': sample_rate, + 'channels': self.channels, + 'peak_level': 0.0, + 'rms_level': 0.0 + } + + duration = len(audio_data) / sample_rate + peak_level = float(np.max(np.abs(audio_data))) + rms_level = float(np.sqrt(np.mean(audio_data ** 2))) + + return { + 'duration': duration, + 'samples': len(audio_data), + 'sample_rate': sample_rate, + 'channels': self.channels, + 'peak_level': peak_level, + 'rms_level': rms_level + } + + except Exception as e: + print(f"✗ 获取音频信息失败: {e}") return {} \ No newline at end of file diff --git a/plugins/user/state_machine/README.md b/plugins/user/state_machine/README.md index bb2f9d61..71aa8702 100644 --- a/plugins/user/state_machine/README.md +++ b/plugins/user/state_machine/README.md @@ -1,86 +1,86 @@ -# 状态机插件使用文档 - -## 简介 - -状态机插件提供了一套完整的有限状态机系统,用于管理对象的不同状态和状态之间的转换。状态机是一种常用的编程模式,特别适用于管理游戏角色的行为状态。 - -## 核心概念 - -### 状态(State) - -状态表示对象在特定时刻所处的状况。每个状态都有三个主要方法: -- `enter(context)`:进入状态时调用 -- `execute(context)`:执行状态逻辑时调用 -- `exit(context)`:退出状态时调用 - -### 状态机(StateMachine) - -状态机负责管理当前状态和状态转换。主要功能包括: -- 状态切换 -- 状态更新 -- 状态历史记录 - -### 状态转换(Transition) - -状态转换定义了从一个状态到另一个状态的条件和规则。 - -## 使用方法 - -### 1. 加载和启用插件 - -```python -# 在主程序中加载插件 -plugin_manager.load_plugin("state_machine") -plugin_manager.enable_plugin("state_machine") - -# 获取插件实例 -sm_plugin = plugin_manager.get_plugin("state_machine") -``` - -### 2. 创建状态 - -```python -from plugins.user.state_machine import State - -class IdleState(State): - def enter(self, context): - print("进入空闲状态") - - def execute(self, context): - print("执行空闲状态逻辑") - # 返回下一个状态,如果为None则保持当前状态 - return None - - def exit(self, context): - print("退出空闲状态") -``` - -### 3. 创建状态机 - -```python -# 创建状态机 -state_machine = sm_plugin.create_state_machine("ai_state_machine") - -# 创建上下文 -context = {"data": {}} - -# 设置状态机上下文 -state_machine.set_context(context) - -# 创建状态实例 -idle_state = IdleState("Idle") - -# 切换到初始状态 -state_machine.change_state(idle_state) -``` - -### 4. 更新状态机 - -```python -# 更新状态机 -state_machine.update() -``` - -## 示例 - +# 状态机插件使用文档 + +## 简介 + +状态机插件提供了一套完整的有限状态机系统,用于管理对象的不同状态和状态之间的转换。状态机是一种常用的编程模式,特别适用于管理游戏角色的行为状态。 + +## 核心概念 + +### 状态(State) + +状态表示对象在特定时刻所处的状况。每个状态都有三个主要方法: +- `enter(context)`:进入状态时调用 +- `execute(context)`:执行状态逻辑时调用 +- `exit(context)`:退出状态时调用 + +### 状态机(StateMachine) + +状态机负责管理当前状态和状态转换。主要功能包括: +- 状态切换 +- 状态更新 +- 状态历史记录 + +### 状态转换(Transition) + +状态转换定义了从一个状态到另一个状态的条件和规则。 + +## 使用方法 + +### 1. 加载和启用插件 + +```python +# 在主程序中加载插件 +plugin_manager.load_plugin("state_machine") +plugin_manager.enable_plugin("state_machine") + +# 获取插件实例 +sm_plugin = plugin_manager.get_plugin("state_machine") +``` + +### 2. 创建状态 + +```python +from plugins.user.state_machine import State + +class IdleState(State): + def enter(self, context): + print("进入空闲状态") + + def execute(self, context): + print("执行空闲状态逻辑") + # 返回下一个状态,如果为None则保持当前状态 + return None + + def exit(self, context): + print("退出空闲状态") +``` + +### 3. 创建状态机 + +```python +# 创建状态机 +state_machine = sm_plugin.create_state_machine("ai_state_machine") + +# 创建上下文 +context = {"data": {}} + +# 设置状态机上下文 +state_machine.set_context(context) + +# 创建状态实例 +idle_state = IdleState("Idle") + +# 切换到初始状态 +state_machine.change_state(idle_state) +``` + +### 4. 更新状态机 + +```python +# 更新状态机 +state_machine.update() +``` + +## 示例 + 查看 `examples/ai_example.py` 文件了解完整的使用示例。 \ No newline at end of file diff --git a/plugins/user/state_machine/__init__.py b/plugins/user/state_machine/__init__.py index bb3049b2..e5cbdacc 100644 --- a/plugins/user/state_machine/__init__.py +++ b/plugins/user/state_machine/__init__.py @@ -1,14 +1,14 @@ -""" -状态机插件__init__.py -""" - -# 导入主要的类和函数,方便外部使用 -from .plugin import Plugin -from .core.state_machine import ( - State, StateMachine, StateContext, - StateTransition, HierarchicalState, - StateType, StateMachineManager -) - -# 定义插件类,保持与plugin.py中的一致 +""" +状态机插件__init__.py +""" + +# 导入主要的类和函数,方便外部使用 +from .plugin import Plugin +from .core.state_machine import ( + State, StateMachine, StateContext, + StateTransition, HierarchicalState, + StateType, StateMachineManager +) + +# 定义插件类,保持与plugin.py中的一致 Plugin = Plugin \ No newline at end of file diff --git a/plugins/user/state_machine/core/state_machine.py b/plugins/user/state_machine/core/state_machine.py index ba1b15ba..62f9447d 100644 --- a/plugins/user/state_machine/core/state_machine.py +++ b/plugins/user/state_machine/core/state_machine.py @@ -1,606 +1,606 @@ -""" -状态机核心组件 -定义状态机的基本结构和执行逻辑 -包含完整的状态管理、转换和上下文系统 -""" - -import time -import threading -from abc import ABC, abstractmethod -from typing import Dict, List, Optional, Any, Callable, Union -from enum import Enum -from dataclasses import dataclass, field -from datetime import datetime - -class StateType(Enum): - """状态类型枚举""" - IDLE = "idle" - ACTIVE = "active" - SUSPENDED = "suspended" - TERMINATED = "terminated" - -@dataclass -class StateContext: - """ - 状态上下文数据类 - 存储状态相关的数据和信息 - """ - data: Dict[str, Any] = field(default_factory=dict) - state_machine: Optional['StateMachine'] = None - timestamp: datetime = field(default_factory=datetime.now) - execution_count: int = 0 - - def get(self, key: str, default: Any = None) -> Any: - """获取数据""" - return self.data.get(key, default) - - def set(self, key: str, value: Any) -> None: - """设置数据""" - self.data[key] = value - - def has(self, key: str) -> bool: - """检查是否存在键""" - return key in self.data - - def remove(self, key: str) -> bool: - """移除数据""" - if key in self.data: - del self.data[key] - return True - return False - - def clear(self) -> None: - """清空数据""" - self.data.clear() - - def update(self, data_dict: Dict[str, Any]) -> None: - """批量更新数据""" - self.data.update(data_dict) - - def copy(self) -> 'StateContext': - """创建上下文副本""" - return StateContext( - data=self.data.copy(), - state_machine=self.state_machine, - timestamp=self.timestamp, - execution_count=self.execution_count - ) - -class StateTransition: - """ - 状态转换类 - 定义状态之间的转换条件和动作 - """ - - def __init__(self, from_state: str, to_state: str, - condition: Optional[Callable[[StateContext], bool]] = None, - action: Optional[Callable[[StateContext], None]] = None, - priority: int = 0): - self.from_state = from_state - self.to_state = to_state - self.condition = condition - self.action = action - self.priority = priority # 优先级,数字越大优先级越高 - self.transition_count = 0 # 转换次数 - - def can_transition(self, context: StateContext) -> bool: - """检查是否可以转换""" - if self.condition: - try: - return self.condition(context) - except Exception as e: - print(f"转换条件检查出错: {e}") - return False - return True - - def execute_action(self, context: StateContext) -> None: - """执行转换动作""" - if self.action: - try: - self.action(context) - except Exception as e: - print(f"转换动作执行出错: {e}") - - def increment_count(self) -> int: - """增加转换计数并返回新计数""" - self.transition_count += 1 - return self.transition_count - -class State(ABC): - """ - 状态基类 - """ - - def __init__(self, name: str = "State", state_type: StateType = StateType.ACTIVE): - self.name = name - self.state_type = state_type - self.enter_time: Optional[datetime] = None - self.exit_time: Optional[datetime] = None - self.execution_count: int = 0 - self.total_execution_time: float = 0.0 - self.transitions: List[StateTransition] = [] - - def add_transition(self, transition: StateTransition) -> None: - """添加状态转换""" - self.transitions.append(transition) - - def add_transition_simple(self, to_state: str, - condition: Optional[Callable[[StateContext], bool]] = None, - action: Optional[Callable[[StateContext], None]] = None, - priority: int = 0) -> None: - """添加简单状态转换""" - transition = StateTransition(self.name, to_state, condition, action, priority) - self.transitions.append(transition) - - def get_transitions(self) -> List[StateTransition]: - """获取状态转换列表""" - return self.transitions.copy() - - def clear_transitions(self) -> None: - """清空状态转换""" - self.transitions.clear() - - def get_transition_to(self, target_state: str) -> Optional[StateTransition]: - """获取到指定状态的转换""" - for transition in self.transitions: - if transition.to_state == target_state: - return transition - return None - - def get_available_transitions(self, context: StateContext) -> List[StateTransition]: - """获取可用的转换列表(按优先级排序)""" - available = [] - for transition in self.transitions: - if transition.can_transition(context): - available.append(transition) - # 按优先级排序 - available.sort(key=lambda t: t.priority, reverse=True) - return available - - @abstractmethod - def enter(self, context: StateContext) -> None: - """ - 进入状态时调用 - :param context: 状态上下文 - """ - self.enter_time = datetime.now() - self.execution_count = 0 - pass - - @abstractmethod - def execute(self, context: StateContext) -> Optional[str]: - """ - 执行状态逻辑 - :param context: 状态上下文 - :return: 下一个状态名称,如果为None则保持当前状态 - """ - start_time = time.time() - self.execution_count += 1 - - # 检查自动转换 - next_state = self._check_auto_transitions(context) - if next_state: - return next_state - - self.total_execution_time += time.time() - start_time - return None - - @abstractmethod - def exit(self, context: StateContext) -> None: - """ - 退出状态时调用 - :param context: 状态上下文 - """ - self.exit_time = datetime.now() - pass - - def _check_auto_transitions(self, context: StateContext) -> Optional[str]: - """检查自动转换""" - for transition in self.transitions: - if transition.can_transition(context): - transition.increment_count() - transition.execute_action(context) - return transition.to_state - return None - - def get_statistics(self) -> Dict[str, Any]: - """获取状态统计信息""" - duration = 0.0 - if self.enter_time and self.exit_time: - duration = (self.exit_time - self.enter_time).total_seconds() - elif self.enter_time: - duration = (datetime.now() - self.enter_time).total_seconds() - - return { - 'name': self.name, - 'type': self.state_type.value, - 'execution_count': self.execution_count, - 'total_execution_time': self.total_execution_time, - 'current_duration': duration, - 'transitions_count': len(self.transitions), - 'enter_time': self.enter_time.isoformat() if self.enter_time else None, - 'exit_time': self.exit_time.isoformat() if self.exit_time else None - } - -class HierarchicalState(State): - """ - 分层状态类 - 支持子状态的状态实现 - """ - - def __init__(self, name: str = "HierarchicalState", - state_type: StateType = StateType.ACTIVE): - super().__init__(name, state_type) - self.sub_states: Dict[str, State] = {} - self.current_sub_state: Optional[State] = None - self.default_sub_state: Optional[str] = None - - def add_sub_state(self, state: State) -> None: - """添加子状态""" - self.sub_states[state.name] = state - - def remove_sub_state(self, state_name: str) -> bool: - """移除子状态""" - if state_name in self.sub_states: - del self.sub_states[state_name] - return True - return False - - def get_sub_state(self, state_name: str) -> Optional[State]: - """获取子状态""" - return self.sub_states.get(state_name) - - def set_default_sub_state(self, state_name: str) -> bool: - """设置默认子状态""" - if state_name in self.sub_states: - self.default_sub_state = state_name - return True - return False - - def enter_sub_state(self, context: StateContext, sub_state_name: str) -> bool: - """进入子状态""" - if sub_state_name in self.sub_states: - if self.current_sub_state: - self.current_sub_state.exit(context) - self.current_sub_state = self.sub_states[sub_state_name] - self.current_sub_state.enter(context) - return True - return False - - def exit_sub_state(self, context: StateContext) -> bool: - """退出当前子状态""" - if self.current_sub_state: - self.current_sub_state.exit(context) - self.current_sub_state = None - return True - return False - - def execute(self, context: StateContext) -> Optional[str]: - """执行状态逻辑(包含子状态)""" - # 首先执行父状态逻辑 - result = super().execute(context) - if result: - return result - - # 然后执行子状态逻辑 - if self.current_sub_state: - sub_result = self.current_sub_state.execute(context) - if sub_result: - return sub_result - - return None - -class StateMachine: - """ - 状态机类 - """ - - def __init__(self, name: str): - self.name = name - self.current_state: Optional[State] = None - self.previous_state: Optional[State] = None - self.context: Optional[StateContext] = None - self.states: Dict[str, State] = {} - self.is_running: bool = False - self.start_time: Optional[datetime] = None - self.last_update_time: Optional[datetime] = None - self.update_count: int = 0 - self.lock = threading.RLock() # 线程安全锁 - self.state_change_callbacks: List[Callable[[State, State], None]] = [] - self.custom_functions: Dict[str, Callable] = {} - - def add_state(self, state: State) -> None: - """添加状态""" - with self.lock: - self.states[state.name] = state - - def remove_state(self, state_name: str) -> bool: - """移除状态""" - with self.lock: - if state_name in self.states: - del self.states[state_name] - return True - return False - - def get_state(self, state_name: str) -> Optional[State]: - """获取状态""" - with self.lock: - return self.states.get(state_name) - - def set_context(self, context: StateContext) -> None: - """设置状态上下文""" - with self.lock: - self.context = context - if context: - context.state_machine = self - - def change_state(self, new_state: Union[State, str]) -> bool: - """切换状态""" - with self.lock: - # 解析状态对象 - if isinstance(new_state, str): - if new_state not in self.states: - print(f"状态 '{new_state}' 不存在") - return False - new_state = self.states[new_state] - - # 如果已经是当前状态,直接返回 - if new_state == self.current_state: - return True - - # 退出当前状态 - if self.current_state: - try: - self.current_state.exit(self.context) - except Exception as e: - print(f"退出状态 '{self.current_state.name}' 时出错: {e}") - - # 保存前一个状态 - self.previous_state = self.current_state - - # 进入新状态 - self.current_state = new_state - if self.current_state: - try: - self.current_state.enter(self.context) - except Exception as e: - print(f"进入状态 '{self.current_state.name}' 时出错: {e}") - return False - - # 触发状态变更回调 - self._trigger_state_change_callbacks(self.previous_state, self.current_state) - - return True - - def revert_to_previous_state(self) -> bool: - """恢复到之前的状态""" - with self.lock: - if self.previous_state: - return self.change_state(self.previous_state) - return False - - def update(self) -> bool: - """更新状态机""" - with self.lock: - self.update_count += 1 - self.last_update_time = datetime.now() - - if not self.current_state or not self.context: - return False - - try: - # 执行当前状态逻辑 - next_state_name = self.current_state.execute(self.context) - - # 如果有下一个状态,切换状态 - if next_state_name and next_state_name in self.states: - next_state = self.states[next_state_name] - if next_state != self.current_state: - return self.change_state(next_state) - - except Exception as e: - print(f"状态机更新出错: {e}") - import traceback - traceback.print_exc() - return False - - return True - - def start(self) -> bool: - """启动状态机""" - with self.lock: - if self.is_running: - return True - - self.is_running = True - self.start_time = datetime.now() - self.update_count = 0 - return True - - def stop(self) -> bool: - """停止状态机""" - with self.lock: - if not self.is_running: - return True - - # 退出当前状态 - if self.current_state: - try: - self.current_state.exit(self.context) - except Exception as e: - print(f"退出状态时出错: {e}") - - self.is_running = False - return True - - def get_current_state(self) -> Optional[State]: - """获取当前状态""" - with self.lock: - return self.current_state - - def get_previous_state(self) -> Optional[State]: - """获取之前的状态""" - with self.lock: - return self.previous_state - - def get_current_state_name(self) -> Optional[str]: - """获取当前状态名称""" - with self.lock: - return self.current_state.name if self.current_state else None - - def is_in_state(self, state_name: str) -> bool: - """检查是否处于指定状态""" - with self.lock: - return self.current_state and self.current_state.name == state_name - - def add_state_change_callback(self, callback: Callable[[State, State], None]) -> None: - """添加状态变更回调""" - with self.lock: - self.state_change_callbacks.append(callback) - - def remove_state_change_callback(self, callback: Callable[[State, State], None]) -> bool: - """移除状态变更回调""" - with self.lock: - if callback in self.state_change_callbacks: - self.state_change_callbacks.remove(callback) - return True - return False - - def _trigger_state_change_callbacks(self, old_state: Optional[State], new_state: Optional[State]) -> None: - """触发状态变更回调""" - for callback in self.state_change_callbacks: - try: - callback(old_state, new_state) - except Exception as e: - print(f"状态变更回调执行出错: {e}") - - def add_custom_function(self, name: str, func: Callable) -> None: - """添加自定义函数""" - with self.lock: - self.custom_functions[name] = func - - def get_custom_function(self, name: str) -> Optional[Callable]: - """获取自定义函数""" - with self.lock: - return self.custom_functions.get(name) - - def get_statistics(self) -> Dict[str, Any]: - """获取状态机统计信息""" - with self.lock: - duration = 0.0 - if self.start_time: - end_time = self.last_update_time or datetime.now() - duration = (end_time - self.start_time).total_seconds() - - current_state_stats = None - if self.current_state: - current_state_stats = self.current_state.get_statistics() - - return { - 'name': self.name, - 'is_running': self.is_running, - 'start_time': self.start_time.isoformat() if self.start_time else None, - 'last_update_time': self.last_update_time.isoformat() if self.last_update_time else None, - 'update_count': self.update_count, - 'total_duration': duration, - 'states_count': len(self.states), - 'current_state': current_state_stats, - 'previous_state': self.previous_state.name if self.previous_state else None - } - -class StateMachineManager: - """ - 状态机管理器 - 用于管理多个状态机的生命周期 - """ - - def __init__(self): - self.state_machines: Dict[str, StateMachine] = {} - self.active_machines: List[str] = [] - self.lock = threading.RLock() - - def add_machine(self, name: str, machine: StateMachine) -> bool: - """添加状态机""" - with self.lock: - if name in self.state_machines: - return False - self.state_machines[name] = machine - return True - - def remove_machine(self, name: str) -> bool: - """移除状态机""" - with self.lock: - if name in self.state_machines: - machine = self.state_machines[name] - if machine.is_running: - machine.stop() - del self.state_machines[name] - if name in self.active_machines: - self.active_machines.remove(name) - return True - return False - - def get_machine(self, name: str) -> Optional[StateMachine]: - """获取状态机""" - with self.lock: - return self.state_machines.get(name) - - def activate_machine(self, name: str) -> bool: - """激活状态机""" - with self.lock: - if name in self.state_machines and name not in self.active_machines: - self.active_machines.append(name) - return self.state_machines[name].start() - return False - - def deactivate_machine(self, name: str) -> bool: - """停用状态机""" - with self.lock: - if name in self.active_machines: - self.active_machines.remove(name) - machine = self.state_machines.get(name) - if machine: - return machine.stop() - return False - - def update_all(self) -> Dict[str, bool]: - """更新所有激活的状态机""" - with self.lock: - results = {} - for machine_name in self.active_machines.copy(): - machine = self.state_machines.get(machine_name) - if machine: - results[machine_name] = machine.update() - return results - - def get_machine_stats(self, name: str) -> Optional[Dict[str, Any]]: - """获取特定状态机的统计信息""" - with self.lock: - machine = self.get_machine(name) - if machine: - return machine.get_statistics() - return None - - def get_all_stats(self) -> Dict[str, Dict[str, Any]]: - """获取所有状态机的统计信息""" - with self.lock: - stats = {} - for name, machine in self.state_machines.items(): - stats[name] = machine.get_statistics() - return stats - - def start_all(self) -> Dict[str, bool]: - """启动所有状态机""" - with self.lock: - results = {} - for name, machine in self.state_machines.items(): - results[name] = machine.start() - return results - - def stop_all(self) -> Dict[str, bool]: - """停止所有状态机""" - with self.lock: - results = {} - for name, machine in self.state_machines.items(): - results[name] = machine.stop() +""" +状态机核心组件 +定义状态机的基本结构和执行逻辑 +包含完整的状态管理、转换和上下文系统 +""" + +import time +import threading +from abc import ABC, abstractmethod +from typing import Dict, List, Optional, Any, Callable, Union +from enum import Enum +from dataclasses import dataclass, field +from datetime import datetime + +class StateType(Enum): + """状态类型枚举""" + IDLE = "idle" + ACTIVE = "active" + SUSPENDED = "suspended" + TERMINATED = "terminated" + +@dataclass +class StateContext: + """ + 状态上下文数据类 + 存储状态相关的数据和信息 + """ + data: Dict[str, Any] = field(default_factory=dict) + state_machine: Optional['StateMachine'] = None + timestamp: datetime = field(default_factory=datetime.now) + execution_count: int = 0 + + def get(self, key: str, default: Any = None) -> Any: + """获取数据""" + return self.data.get(key, default) + + def set(self, key: str, value: Any) -> None: + """设置数据""" + self.data[key] = value + + def has(self, key: str) -> bool: + """检查是否存在键""" + return key in self.data + + def remove(self, key: str) -> bool: + """移除数据""" + if key in self.data: + del self.data[key] + return True + return False + + def clear(self) -> None: + """清空数据""" + self.data.clear() + + def update(self, data_dict: Dict[str, Any]) -> None: + """批量更新数据""" + self.data.update(data_dict) + + def copy(self) -> 'StateContext': + """创建上下文副本""" + return StateContext( + data=self.data.copy(), + state_machine=self.state_machine, + timestamp=self.timestamp, + execution_count=self.execution_count + ) + +class StateTransition: + """ + 状态转换类 + 定义状态之间的转换条件和动作 + """ + + def __init__(self, from_state: str, to_state: str, + condition: Optional[Callable[[StateContext], bool]] = None, + action: Optional[Callable[[StateContext], None]] = None, + priority: int = 0): + self.from_state = from_state + self.to_state = to_state + self.condition = condition + self.action = action + self.priority = priority # 优先级,数字越大优先级越高 + self.transition_count = 0 # 转换次数 + + def can_transition(self, context: StateContext) -> bool: + """检查是否可以转换""" + if self.condition: + try: + return self.condition(context) + except Exception as e: + print(f"转换条件检查出错: {e}") + return False + return True + + def execute_action(self, context: StateContext) -> None: + """执行转换动作""" + if self.action: + try: + self.action(context) + except Exception as e: + print(f"转换动作执行出错: {e}") + + def increment_count(self) -> int: + """增加转换计数并返回新计数""" + self.transition_count += 1 + return self.transition_count + +class State(ABC): + """ + 状态基类 + """ + + def __init__(self, name: str = "State", state_type: StateType = StateType.ACTIVE): + self.name = name + self.state_type = state_type + self.enter_time: Optional[datetime] = None + self.exit_time: Optional[datetime] = None + self.execution_count: int = 0 + self.total_execution_time: float = 0.0 + self.transitions: List[StateTransition] = [] + + def add_transition(self, transition: StateTransition) -> None: + """添加状态转换""" + self.transitions.append(transition) + + def add_transition_simple(self, to_state: str, + condition: Optional[Callable[[StateContext], bool]] = None, + action: Optional[Callable[[StateContext], None]] = None, + priority: int = 0) -> None: + """添加简单状态转换""" + transition = StateTransition(self.name, to_state, condition, action, priority) + self.transitions.append(transition) + + def get_transitions(self) -> List[StateTransition]: + """获取状态转换列表""" + return self.transitions.copy() + + def clear_transitions(self) -> None: + """清空状态转换""" + self.transitions.clear() + + def get_transition_to(self, target_state: str) -> Optional[StateTransition]: + """获取到指定状态的转换""" + for transition in self.transitions: + if transition.to_state == target_state: + return transition + return None + + def get_available_transitions(self, context: StateContext) -> List[StateTransition]: + """获取可用的转换列表(按优先级排序)""" + available = [] + for transition in self.transitions: + if transition.can_transition(context): + available.append(transition) + # 按优先级排序 + available.sort(key=lambda t: t.priority, reverse=True) + return available + + @abstractmethod + def enter(self, context: StateContext) -> None: + """ + 进入状态时调用 + :param context: 状态上下文 + """ + self.enter_time = datetime.now() + self.execution_count = 0 + pass + + @abstractmethod + def execute(self, context: StateContext) -> Optional[str]: + """ + 执行状态逻辑 + :param context: 状态上下文 + :return: 下一个状态名称,如果为None则保持当前状态 + """ + start_time = time.time() + self.execution_count += 1 + + # 检查自动转换 + next_state = self._check_auto_transitions(context) + if next_state: + return next_state + + self.total_execution_time += time.time() - start_time + return None + + @abstractmethod + def exit(self, context: StateContext) -> None: + """ + 退出状态时调用 + :param context: 状态上下文 + """ + self.exit_time = datetime.now() + pass + + def _check_auto_transitions(self, context: StateContext) -> Optional[str]: + """检查自动转换""" + for transition in self.transitions: + if transition.can_transition(context): + transition.increment_count() + transition.execute_action(context) + return transition.to_state + return None + + def get_statistics(self) -> Dict[str, Any]: + """获取状态统计信息""" + duration = 0.0 + if self.enter_time and self.exit_time: + duration = (self.exit_time - self.enter_time).total_seconds() + elif self.enter_time: + duration = (datetime.now() - self.enter_time).total_seconds() + + return { + 'name': self.name, + 'type': self.state_type.value, + 'execution_count': self.execution_count, + 'total_execution_time': self.total_execution_time, + 'current_duration': duration, + 'transitions_count': len(self.transitions), + 'enter_time': self.enter_time.isoformat() if self.enter_time else None, + 'exit_time': self.exit_time.isoformat() if self.exit_time else None + } + +class HierarchicalState(State): + """ + 分层状态类 + 支持子状态的状态实现 + """ + + def __init__(self, name: str = "HierarchicalState", + state_type: StateType = StateType.ACTIVE): + super().__init__(name, state_type) + self.sub_states: Dict[str, State] = {} + self.current_sub_state: Optional[State] = None + self.default_sub_state: Optional[str] = None + + def add_sub_state(self, state: State) -> None: + """添加子状态""" + self.sub_states[state.name] = state + + def remove_sub_state(self, state_name: str) -> bool: + """移除子状态""" + if state_name in self.sub_states: + del self.sub_states[state_name] + return True + return False + + def get_sub_state(self, state_name: str) -> Optional[State]: + """获取子状态""" + return self.sub_states.get(state_name) + + def set_default_sub_state(self, state_name: str) -> bool: + """设置默认子状态""" + if state_name in self.sub_states: + self.default_sub_state = state_name + return True + return False + + def enter_sub_state(self, context: StateContext, sub_state_name: str) -> bool: + """进入子状态""" + if sub_state_name in self.sub_states: + if self.current_sub_state: + self.current_sub_state.exit(context) + self.current_sub_state = self.sub_states[sub_state_name] + self.current_sub_state.enter(context) + return True + return False + + def exit_sub_state(self, context: StateContext) -> bool: + """退出当前子状态""" + if self.current_sub_state: + self.current_sub_state.exit(context) + self.current_sub_state = None + return True + return False + + def execute(self, context: StateContext) -> Optional[str]: + """执行状态逻辑(包含子状态)""" + # 首先执行父状态逻辑 + result = super().execute(context) + if result: + return result + + # 然后执行子状态逻辑 + if self.current_sub_state: + sub_result = self.current_sub_state.execute(context) + if sub_result: + return sub_result + + return None + +class StateMachine: + """ + 状态机类 + """ + + def __init__(self, name: str): + self.name = name + self.current_state: Optional[State] = None + self.previous_state: Optional[State] = None + self.context: Optional[StateContext] = None + self.states: Dict[str, State] = {} + self.is_running: bool = False + self.start_time: Optional[datetime] = None + self.last_update_time: Optional[datetime] = None + self.update_count: int = 0 + self.lock = threading.RLock() # 线程安全锁 + self.state_change_callbacks: List[Callable[[State, State], None]] = [] + self.custom_functions: Dict[str, Callable] = {} + + def add_state(self, state: State) -> None: + """添加状态""" + with self.lock: + self.states[state.name] = state + + def remove_state(self, state_name: str) -> bool: + """移除状态""" + with self.lock: + if state_name in self.states: + del self.states[state_name] + return True + return False + + def get_state(self, state_name: str) -> Optional[State]: + """获取状态""" + with self.lock: + return self.states.get(state_name) + + def set_context(self, context: StateContext) -> None: + """设置状态上下文""" + with self.lock: + self.context = context + if context: + context.state_machine = self + + def change_state(self, new_state: Union[State, str]) -> bool: + """切换状态""" + with self.lock: + # 解析状态对象 + if isinstance(new_state, str): + if new_state not in self.states: + print(f"状态 '{new_state}' 不存在") + return False + new_state = self.states[new_state] + + # 如果已经是当前状态,直接返回 + if new_state == self.current_state: + return True + + # 退出当前状态 + if self.current_state: + try: + self.current_state.exit(self.context) + except Exception as e: + print(f"退出状态 '{self.current_state.name}' 时出错: {e}") + + # 保存前一个状态 + self.previous_state = self.current_state + + # 进入新状态 + self.current_state = new_state + if self.current_state: + try: + self.current_state.enter(self.context) + except Exception as e: + print(f"进入状态 '{self.current_state.name}' 时出错: {e}") + return False + + # 触发状态变更回调 + self._trigger_state_change_callbacks(self.previous_state, self.current_state) + + return True + + def revert_to_previous_state(self) -> bool: + """恢复到之前的状态""" + with self.lock: + if self.previous_state: + return self.change_state(self.previous_state) + return False + + def update(self) -> bool: + """更新状态机""" + with self.lock: + self.update_count += 1 + self.last_update_time = datetime.now() + + if not self.current_state or not self.context: + return False + + try: + # 执行当前状态逻辑 + next_state_name = self.current_state.execute(self.context) + + # 如果有下一个状态,切换状态 + if next_state_name and next_state_name in self.states: + next_state = self.states[next_state_name] + if next_state != self.current_state: + return self.change_state(next_state) + + except Exception as e: + print(f"状态机更新出错: {e}") + import traceback + traceback.print_exc() + return False + + return True + + def start(self) -> bool: + """启动状态机""" + with self.lock: + if self.is_running: + return True + + self.is_running = True + self.start_time = datetime.now() + self.update_count = 0 + return True + + def stop(self) -> bool: + """停止状态机""" + with self.lock: + if not self.is_running: + return True + + # 退出当前状态 + if self.current_state: + try: + self.current_state.exit(self.context) + except Exception as e: + print(f"退出状态时出错: {e}") + + self.is_running = False + return True + + def get_current_state(self) -> Optional[State]: + """获取当前状态""" + with self.lock: + return self.current_state + + def get_previous_state(self) -> Optional[State]: + """获取之前的状态""" + with self.lock: + return self.previous_state + + def get_current_state_name(self) -> Optional[str]: + """获取当前状态名称""" + with self.lock: + return self.current_state.name if self.current_state else None + + def is_in_state(self, state_name: str) -> bool: + """检查是否处于指定状态""" + with self.lock: + return self.current_state and self.current_state.name == state_name + + def add_state_change_callback(self, callback: Callable[[State, State], None]) -> None: + """添加状态变更回调""" + with self.lock: + self.state_change_callbacks.append(callback) + + def remove_state_change_callback(self, callback: Callable[[State, State], None]) -> bool: + """移除状态变更回调""" + with self.lock: + if callback in self.state_change_callbacks: + self.state_change_callbacks.remove(callback) + return True + return False + + def _trigger_state_change_callbacks(self, old_state: Optional[State], new_state: Optional[State]) -> None: + """触发状态变更回调""" + for callback in self.state_change_callbacks: + try: + callback(old_state, new_state) + except Exception as e: + print(f"状态变更回调执行出错: {e}") + + def add_custom_function(self, name: str, func: Callable) -> None: + """添加自定义函数""" + with self.lock: + self.custom_functions[name] = func + + def get_custom_function(self, name: str) -> Optional[Callable]: + """获取自定义函数""" + with self.lock: + return self.custom_functions.get(name) + + def get_statistics(self) -> Dict[str, Any]: + """获取状态机统计信息""" + with self.lock: + duration = 0.0 + if self.start_time: + end_time = self.last_update_time or datetime.now() + duration = (end_time - self.start_time).total_seconds() + + current_state_stats = None + if self.current_state: + current_state_stats = self.current_state.get_statistics() + + return { + 'name': self.name, + 'is_running': self.is_running, + 'start_time': self.start_time.isoformat() if self.start_time else None, + 'last_update_time': self.last_update_time.isoformat() if self.last_update_time else None, + 'update_count': self.update_count, + 'total_duration': duration, + 'states_count': len(self.states), + 'current_state': current_state_stats, + 'previous_state': self.previous_state.name if self.previous_state else None + } + +class StateMachineManager: + """ + 状态机管理器 + 用于管理多个状态机的生命周期 + """ + + def __init__(self): + self.state_machines: Dict[str, StateMachine] = {} + self.active_machines: List[str] = [] + self.lock = threading.RLock() + + def add_machine(self, name: str, machine: StateMachine) -> bool: + """添加状态机""" + with self.lock: + if name in self.state_machines: + return False + self.state_machines[name] = machine + return True + + def remove_machine(self, name: str) -> bool: + """移除状态机""" + with self.lock: + if name in self.state_machines: + machine = self.state_machines[name] + if machine.is_running: + machine.stop() + del self.state_machines[name] + if name in self.active_machines: + self.active_machines.remove(name) + return True + return False + + def get_machine(self, name: str) -> Optional[StateMachine]: + """获取状态机""" + with self.lock: + return self.state_machines.get(name) + + def activate_machine(self, name: str) -> bool: + """激活状态机""" + with self.lock: + if name in self.state_machines and name not in self.active_machines: + self.active_machines.append(name) + return self.state_machines[name].start() + return False + + def deactivate_machine(self, name: str) -> bool: + """停用状态机""" + with self.lock: + if name in self.active_machines: + self.active_machines.remove(name) + machine = self.state_machines.get(name) + if machine: + return machine.stop() + return False + + def update_all(self) -> Dict[str, bool]: + """更新所有激活的状态机""" + with self.lock: + results = {} + for machine_name in self.active_machines.copy(): + machine = self.state_machines.get(machine_name) + if machine: + results[machine_name] = machine.update() + return results + + def get_machine_stats(self, name: str) -> Optional[Dict[str, Any]]: + """获取特定状态机的统计信息""" + with self.lock: + machine = self.get_machine(name) + if machine: + return machine.get_statistics() + return None + + def get_all_stats(self) -> Dict[str, Dict[str, Any]]: + """获取所有状态机的统计信息""" + with self.lock: + stats = {} + for name, machine in self.state_machines.items(): + stats[name] = machine.get_statistics() + return stats + + def start_all(self) -> Dict[str, bool]: + """启动所有状态机""" + with self.lock: + results = {} + for name, machine in self.state_machines.items(): + results[name] = machine.start() + return results + + def stop_all(self) -> Dict[str, bool]: + """停止所有状态机""" + with self.lock: + results = {} + for name, machine in self.state_machines.items(): + results[name] = machine.stop() return results \ No newline at end of file diff --git a/plugins/user/state_machine/examples/ai_example.py b/plugins/user/state_machine/examples/ai_example.py index e9232183..3122aab0 100644 --- a/plugins/user/state_machine/examples/ai_example.py +++ b/plugins/user/state_machine/examples/ai_example.py @@ -1,161 +1,161 @@ -""" -状态机示例 -演示如何使用状态机插件创建和运行状态机 -""" - -from plugins.user.state_machine.core.state_machine import State - -class IdleState(State): - """空闲状态""" - - def enter(self, context): - print("进入空闲状态") - if context: - context.set_data("state", "idle") - - def execute(self, context): - print("执行空闲状态逻辑") - # 检查是否需要切换到巡逻状态 - if context and context.get_data("need_patrol", False): - return context.states["patrol"] - return None - - def exit(self, context): - print("退出空闲状态") - -class PatrolState(State): - """巡逻状态""" - - def enter(self, context): - print("进入巡逻状态") - if context: - context.set_data("state", "patrol") - - def execute(self, context): - print("执行巡逻状态逻辑") - # 更新巡逻计数 - patrol_count = context.get_data("patrol_count", 0) if context else 0 - if context: - context.set_data("patrol_count", patrol_count + 1) - - # 检查是否需要切换到战斗状态 - if context and context.get_data("need_combat", False): - return context.states["combat"] - # 检查是否巡逻次数足够,需要切换到休息状态 - elif context and context.get_data("patrol_count", 0) >= 3: - return context.states["rest"] - return None - - def exit(self, context): - print("退出巡逻状态") - -class CombatState(State): - """战斗状态""" - - def enter(self, context): - print("进入战斗状态") - if context: - context.set_data("state", "combat") - - def execute(self, context): - print("执行战斗状态逻辑") - # 检查是否需要切换到空闲状态 - if context and context.get_data("need_idle", False): - return context.states["idle"] - return None - - def exit(self, context): - print("退出战斗状态") - -class RestState(State): - """休息状态""" - - def enter(self, context): - print("进入休息状态") - if context: - context.set_data("state", "rest") - - def execute(self, context): - print("执行休息状态逻辑") - # 检查是否需要切换到空闲状态 - if context and context.get_data("need_idle", False): - return context.states["idle"] - return None - - def exit(self, context): - print("退出休息状态") - -class SimpleContext: - """简单的上下文类""" - - def __init__(self): - self.data = {} - self.states = { - "idle": IdleState(), - "patrol": PatrolState(), - "combat": CombatState(), - "rest": RestState() - } - - def get_data(self, key, default=None): - return self.data.get(key, default) - - def set_data(self, key, value): - self.data[key] = value - - def get_state(self, name): - return self.states.get(name) - -def create_ai_state_machine(plugin): - """ - 创建AI状态机示例 - """ - # 创建状态机 - state_machine = plugin.create_state_machine("ai_state_machine") - - # 创建上下文 - context = SimpleContext() - - # 设置状态机上下文 - state_machine.set_context(context) - - # 初始状态设置为空闲 - context.set_data("need_patrol", True) - context.set_data("need_combat", False) - context.set_data("need_idle", False) - context.set_data("patrol_count", 0) - - # 切换到初始状态 - state_machine.change_state(context.states["idle"]) - - return state_machine, context - -def run_ai_example(plugin): - """ - 运行AI状态机示例 - """ - print("=== 状态机示例:AI状态管理 ===") - - # 创建状态机和上下文 - state_machine, context = create_ai_state_machine(plugin) - - # 运行状态机 - for i in range(8): - print(f"\n--- 第 {i+1} 次更新 ---") - print(f"当前状态: {state_machine.get_current_state().name}") - - # 更新状态机 - state_machine.update() - - # 模拟一些条件变化 - if i == 2: - context.set_data("need_combat", True) - print("设置需要战斗") - elif i == 4: - context.set_data("need_combat", False) - context.set_data("need_idle", True) - print("设置需要空闲") - elif i == 6: - context.set_data("need_idle", False) - context.set_data("need_patrol", True) +""" +状态机示例 +演示如何使用状态机插件创建和运行状态机 +""" + +from plugins.user.state_machine.core.state_machine import State + +class IdleState(State): + """空闲状态""" + + def enter(self, context): + print("进入空闲状态") + if context: + context.set_data("state", "idle") + + def execute(self, context): + print("执行空闲状态逻辑") + # 检查是否需要切换到巡逻状态 + if context and context.get_data("need_patrol", False): + return context.states["patrol"] + return None + + def exit(self, context): + print("退出空闲状态") + +class PatrolState(State): + """巡逻状态""" + + def enter(self, context): + print("进入巡逻状态") + if context: + context.set_data("state", "patrol") + + def execute(self, context): + print("执行巡逻状态逻辑") + # 更新巡逻计数 + patrol_count = context.get_data("patrol_count", 0) if context else 0 + if context: + context.set_data("patrol_count", patrol_count + 1) + + # 检查是否需要切换到战斗状态 + if context and context.get_data("need_combat", False): + return context.states["combat"] + # 检查是否巡逻次数足够,需要切换到休息状态 + elif context and context.get_data("patrol_count", 0) >= 3: + return context.states["rest"] + return None + + def exit(self, context): + print("退出巡逻状态") + +class CombatState(State): + """战斗状态""" + + def enter(self, context): + print("进入战斗状态") + if context: + context.set_data("state", "combat") + + def execute(self, context): + print("执行战斗状态逻辑") + # 检查是否需要切换到空闲状态 + if context and context.get_data("need_idle", False): + return context.states["idle"] + return None + + def exit(self, context): + print("退出战斗状态") + +class RestState(State): + """休息状态""" + + def enter(self, context): + print("进入休息状态") + if context: + context.set_data("state", "rest") + + def execute(self, context): + print("执行休息状态逻辑") + # 检查是否需要切换到空闲状态 + if context and context.get_data("need_idle", False): + return context.states["idle"] + return None + + def exit(self, context): + print("退出休息状态") + +class SimpleContext: + """简单的上下文类""" + + def __init__(self): + self.data = {} + self.states = { + "idle": IdleState(), + "patrol": PatrolState(), + "combat": CombatState(), + "rest": RestState() + } + + def get_data(self, key, default=None): + return self.data.get(key, default) + + def set_data(self, key, value): + self.data[key] = value + + def get_state(self, name): + return self.states.get(name) + +def create_ai_state_machine(plugin): + """ + 创建AI状态机示例 + """ + # 创建状态机 + state_machine = plugin.create_state_machine("ai_state_machine") + + # 创建上下文 + context = SimpleContext() + + # 设置状态机上下文 + state_machine.set_context(context) + + # 初始状态设置为空闲 + context.set_data("need_patrol", True) + context.set_data("need_combat", False) + context.set_data("need_idle", False) + context.set_data("patrol_count", 0) + + # 切换到初始状态 + state_machine.change_state(context.states["idle"]) + + return state_machine, context + +def run_ai_example(plugin): + """ + 运行AI状态机示例 + """ + print("=== 状态机示例:AI状态管理 ===") + + # 创建状态机和上下文 + state_machine, context = create_ai_state_machine(plugin) + + # 运行状态机 + for i in range(8): + print(f"\n--- 第 {i+1} 次更新 ---") + print(f"当前状态: {state_machine.get_current_state().name}") + + # 更新状态机 + state_machine.update() + + # 模拟一些条件变化 + if i == 2: + context.set_data("need_combat", True) + print("设置需要战斗") + elif i == 4: + context.set_data("need_combat", False) + context.set_data("need_idle", True) + print("设置需要空闲") + elif i == 6: + context.set_data("need_idle", False) + context.set_data("need_patrol", True) print("设置需要巡逻") \ No newline at end of file diff --git a/plugins/user/state_machine/plugin.py b/plugins/user/state_machine/plugin.py index 499bad25..6413b023 100644 --- a/plugins/user/state_machine/plugin.py +++ b/plugins/user/state_machine/plugin.py @@ -1,190 +1,190 @@ -""" -状态机插件 -提供完整的有限状态机功能用于管理对象状态 -包含状态管理、转换系统、上下文管理等完整功能 -""" - -from plugins.plugin_manager import BasePlugin -from typing import Dict, Optional, Any - -class Plugin(BasePlugin): - """ - 状态机插件 - 提供完整的有限状态机功能用于管理对象状态 - """ - - def __init__(self, plugin_manager, name): - super().__init__(plugin_manager, name) - self.config = { - "version": "1.0.0", - "author": "EG Team", - "description": "状态机插件,用于管理对象状态,包含完整的状态管理、转换系统和上下文管理" - } - self.state_machines = {} - self.machine_manager = None - self.is_initialized = False - - def initialize(self) -> bool: - """ - 初始化插件 - """ - if self.is_initialized: - return True - - try: - # 导入核心模块 - from plugins.user.state_machine.core.state_machine import StateMachineManager - - # 创建管理器实例 - self.machine_manager = StateMachineManager() - - print(f"初始化状态机插件: {self.name}") - self.is_initialized = True - return True - - except Exception as e: - print(f"状态机插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - """ - if not super().enable(): - return False - - if not self.is_initialized: - if not self.initialize(): - return False - - print(f"启用状态机插件: {self.name}") - return True - - def disable(self) -> bool: - """ - 禁用插件 - """ - if not super().disable(): - return False - - # 停止所有激活的状态机 - if self.machine_manager: - self.machine_manager.active_machines.clear() - - print(f"禁用状态机插件: {self.name}") - return True - - def finalize(self) -> None: - """ - 清理插件资源 - """ - print(f"清理状态机插件资源: {self.name}") - - try: - # 停止所有激活的状态机 - if self.machine_manager: - # 停止所有状态机 - for machine in self.machine_manager.state_machines.values(): - if machine.is_running: - machine.stop() - # 清空激活列表 - self.machine_manager.active_machines.clear() - - # 清理资源 - self.state_machines.clear() - self.machine_manager = None - self.is_initialized = False - - print(f"状态机插件资源清理完成: {self.name}") - - except Exception as e: - print(f"清理状态机插件资源失败: {e}") - import traceback - traceback.print_exc() - - def create_state_machine(self, name: str) -> Optional['StateMachine']: - """ - 创建状态机 - """ - try: - from plugins.user.state_machine.core.state_machine import StateMachine - machine = StateMachine(name) - self.state_machines[name] = machine - - # 添加到管理器 - if self.machine_manager: - self.machine_manager.add_machine(name, machine) - - return machine - except Exception as e: - print(f"创建状态机失败: {e}") - return None - - def get_state_machine(self, name: str) -> Optional['StateMachine']: - """ - 获取状态机实例 - """ - return self.state_machines.get(name) - - def remove_state_machine(self, name: str) -> bool: - """ - 移除状态机 - """ - if name in self.state_machines: - # 从管理器移除 - if self.machine_manager: - self.machine_manager.remove_machine(name) - del self.state_machines[name] - return True - return False - - def get_machine_manager(self) -> Optional['StateMachineManager']: - """ - 获取状态机管理器 - """ - return self.machine_manager - - def update_all_machines(self) -> Dict[str, bool]: - """ - 更新所有激活的状态机 - """ - if self.machine_manager: - return self.machine_manager.update_all() - return {} - - def get_machine_statistics(self, name: str) -> Optional[Dict[str, Any]]: - """ - 获取状态机统计信息 - """ - if self.machine_manager: - return self.machine_manager.get_machine_stats(name) - return None - - def get_all_statistics(self) -> Dict[str, Dict[str, Any]]: - """ - 获取所有状态机统计信息 - """ - if self.machine_manager: - return self.machine_manager.get_all_stats() - return {} - - def start_all_machines(self) -> Dict[str, bool]: - """ - 启动所有状态机 - """ - if self.machine_manager: - return self.machine_manager.start_all() - return {} - - def stop_all_machines(self) -> Dict[str, bool]: - """ - 停止所有状态机 - """ - if self.machine_manager: - return self.machine_manager.stop_all() - return {} - -# 兼容性接口 +""" +状态机插件 +提供完整的有限状态机功能用于管理对象状态 +包含状态管理、转换系统、上下文管理等完整功能 +""" + +from plugins.plugin_manager import BasePlugin +from typing import Dict, Optional, Any + +class Plugin(BasePlugin): + """ + 状态机插件 + 提供完整的有限状态机功能用于管理对象状态 + """ + + def __init__(self, plugin_manager, name): + super().__init__(plugin_manager, name) + self.config = { + "version": "1.0.0", + "author": "EG Team", + "description": "状态机插件,用于管理对象状态,包含完整的状态管理、转换系统和上下文管理" + } + self.state_machines = {} + self.machine_manager = None + self.is_initialized = False + + def initialize(self) -> bool: + """ + 初始化插件 + """ + if self.is_initialized: + return True + + try: + # 导入核心模块 + from plugins.user.state_machine.core.state_machine import StateMachineManager + + # 创建管理器实例 + self.machine_manager = StateMachineManager() + + print(f"初始化状态机插件: {self.name}") + self.is_initialized = True + return True + + except Exception as e: + print(f"状态机插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + """ + if not super().enable(): + return False + + if not self.is_initialized: + if not self.initialize(): + return False + + print(f"启用状态机插件: {self.name}") + return True + + def disable(self) -> bool: + """ + 禁用插件 + """ + if not super().disable(): + return False + + # 停止所有激活的状态机 + if self.machine_manager: + self.machine_manager.active_machines.clear() + + print(f"禁用状态机插件: {self.name}") + return True + + def finalize(self) -> None: + """ + 清理插件资源 + """ + print(f"清理状态机插件资源: {self.name}") + + try: + # 停止所有激活的状态机 + if self.machine_manager: + # 停止所有状态机 + for machine in self.machine_manager.state_machines.values(): + if machine.is_running: + machine.stop() + # 清空激活列表 + self.machine_manager.active_machines.clear() + + # 清理资源 + self.state_machines.clear() + self.machine_manager = None + self.is_initialized = False + + print(f"状态机插件资源清理完成: {self.name}") + + except Exception as e: + print(f"清理状态机插件资源失败: {e}") + import traceback + traceback.print_exc() + + def create_state_machine(self, name: str) -> Optional['StateMachine']: + """ + 创建状态机 + """ + try: + from plugins.user.state_machine.core.state_machine import StateMachine + machine = StateMachine(name) + self.state_machines[name] = machine + + # 添加到管理器 + if self.machine_manager: + self.machine_manager.add_machine(name, machine) + + return machine + except Exception as e: + print(f"创建状态机失败: {e}") + return None + + def get_state_machine(self, name: str) -> Optional['StateMachine']: + """ + 获取状态机实例 + """ + return self.state_machines.get(name) + + def remove_state_machine(self, name: str) -> bool: + """ + 移除状态机 + """ + if name in self.state_machines: + # 从管理器移除 + if self.machine_manager: + self.machine_manager.remove_machine(name) + del self.state_machines[name] + return True + return False + + def get_machine_manager(self) -> Optional['StateMachineManager']: + """ + 获取状态机管理器 + """ + return self.machine_manager + + def update_all_machines(self) -> Dict[str, bool]: + """ + 更新所有激活的状态机 + """ + if self.machine_manager: + return self.machine_manager.update_all() + return {} + + def get_machine_statistics(self, name: str) -> Optional[Dict[str, Any]]: + """ + 获取状态机统计信息 + """ + if self.machine_manager: + return self.machine_manager.get_machine_stats(name) + return None + + def get_all_statistics(self) -> Dict[str, Dict[str, Any]]: + """ + 获取所有状态机统计信息 + """ + if self.machine_manager: + return self.machine_manager.get_all_stats() + return {} + + def start_all_machines(self) -> Dict[str, bool]: + """ + 启动所有状态机 + """ + if self.machine_manager: + return self.machine_manager.start_all() + return {} + + def stop_all_machines(self) -> Dict[str, bool]: + """ + 停止所有状态机 + """ + if self.machine_manager: + return self.machine_manager.stop_all() + return {} + +# 兼容性接口 Plugin = Plugin \ No newline at end of file diff --git a/plugins/user/state_machine/transitions/transition.py b/plugins/user/state_machine/transitions/transition.py index 7a970c3f..251a4a21 100644 --- a/plugins/user/state_machine/transitions/transition.py +++ b/plugins/user/state_machine/transitions/transition.py @@ -1,87 +1,87 @@ -""" -状态转换类 -定义状态之间的转换条件和规则 -""" - -class Transition: - """ - 状态转换类 - """ - - def __init__(self, to_state, condition_func=None): - self.to_state = to_state # 目标状态 - self.condition_func = condition_func # 转换条件函数 - - def is_triggered(self, context): - """ - 检查转换条件是否满足 - :param context: 状态上下文 - :return: 是否满足转换条件 - """ - if self.condition_func: - try: - return self.condition_func(context) - except Exception as e: - print(f"转换条件检查出错: {e}") - return False - else: - # 如果没有提供条件函数,默认满足转换条件 - return True - -class StateWithTransitions: - """ - 带有转换条件的状态类 - 扩展基础状态类,添加状态转换功能 - """ - - def __init__(self, name="StateWithTransitions"): - self.name = name - self.transitions = [] # 状态转换列表 - - def add_transition(self, transition): - """添加状态转换""" - self.transitions.append(transition) - - def remove_transition(self, transition): - """移除状态转换""" - if transition in self.transitions: - self.transitions.remove(transition) - - def get_transitions(self): - """获取状态转换列表""" - return self.transitions - - def check_transitions(self, context): - """ - 检查所有转换条件,返回满足条件的目标状态 - :param context: 状态上下文 - :return: 目标状态,如果没有满足条件的转换则返回None - """ - for transition in self.transitions: - if transition.is_triggered(context): - return transition.to_state - return None - - def enter(self, context): - """ - 进入状态时调用 - :param context: 状态上下文 - """ - pass - - def execute(self, context): - """ - 执行状态逻辑 - :param context: 状态上下文 - :return: 下一个状态,如果为None则保持当前状态 - """ - # 检查状态转换 - next_state = self.check_transitions(context) - return next_state - - def exit(self, context): - """ - 退出状态时调用 - :param context: 状态上下文 - """ +""" +状态转换类 +定义状态之间的转换条件和规则 +""" + +class Transition: + """ + 状态转换类 + """ + + def __init__(self, to_state, condition_func=None): + self.to_state = to_state # 目标状态 + self.condition_func = condition_func # 转换条件函数 + + def is_triggered(self, context): + """ + 检查转换条件是否满足 + :param context: 状态上下文 + :return: 是否满足转换条件 + """ + if self.condition_func: + try: + return self.condition_func(context) + except Exception as e: + print(f"转换条件检查出错: {e}") + return False + else: + # 如果没有提供条件函数,默认满足转换条件 + return True + +class StateWithTransitions: + """ + 带有转换条件的状态类 + 扩展基础状态类,添加状态转换功能 + """ + + def __init__(self, name="StateWithTransitions"): + self.name = name + self.transitions = [] # 状态转换列表 + + def add_transition(self, transition): + """添加状态转换""" + self.transitions.append(transition) + + def remove_transition(self, transition): + """移除状态转换""" + if transition in self.transitions: + self.transitions.remove(transition) + + def get_transitions(self): + """获取状态转换列表""" + return self.transitions + + def check_transitions(self, context): + """ + 检查所有转换条件,返回满足条件的目标状态 + :param context: 状态上下文 + :return: 目标状态,如果没有满足条件的转换则返回None + """ + for transition in self.transitions: + if transition.is_triggered(context): + return transition.to_state + return None + + def enter(self, context): + """ + 进入状态时调用 + :param context: 状态上下文 + """ + pass + + def execute(self, context): + """ + 执行状态逻辑 + :param context: 状态上下文 + :return: 下一个状态,如果为None则保持当前状态 + """ + # 检查状态转换 + next_state = self.check_transitions(context) + return next_state + + def exit(self, context): + """ + 退出状态时调用 + :param context: 状态上下文 + """ pass \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/README.md b/plugins/user/swarm_intelligence/README.md index 6e745607..2ca34161 100644 --- a/plugins/user/swarm_intelligence/README.md +++ b/plugins/user/swarm_intelligence/README.md @@ -1,202 +1,202 @@ -# 群体智能插件 - 高级功能说明 - -## 概述 - -本插件实现了一个完整的群体智能系统,基于Boids算法并扩展了多种高级行为和优化功能。 - -## 核心功能 - -### 1. 高级Boids算法 -- **聚集行为**:个体向邻居群集中心移动 -- **分离行为**:个体避免与邻居过于靠近 -- **对齐行为**:个体与邻居的移动方向保持一致 -- **加权邻居计算**:距离越近的邻居对行为影响越大 -- **个人空间**:考虑个体的个人空间需求 - -### 2. 扩展行为 -- **避障行为**:避开场景中的障碍物 -- **寻求行为**:向目标位置移动 -- **逃离行为**:远离目标位置 -- **游走行为**:产生自然的随机移动 -- **边界限制**:将成员保持在指定区域内 -- **路径跟随**:沿着预定义路径移动 -- **队形保持**:维持特定的群体队形(V字形、直线、圆形、楔形、球形) -- **躲避捕食者**:远离捕食者 -- **到达行为**:接近目标时减速 - -### 3. 群体间交互 -- **合作交互**:群体之间相互协作,共享资源或信息 -- **竞争交互**:群体之间争夺资源或领域 -- **捕食者-猎物交互**:一个群体捕食另一个群体 -- **中性交互**:群体之间无直接相互作用 -- **动态交互**:交互关系随时间变化 - -### 4. 环境感知和响应机制 -- **环境传感器**:每个群体成员都配备环境传感器,可感知障碍物、其他群体、捕食者、资源点和危险区域 -- **传感器参数**:可调节传感器范围和精度 -- **环境响应**:根据感知到的环境信息调整行为(避障、避敌、寻资源、避危险) -- **天气适应**:适应风向、温度等天气条件 -- **时间效应**:环境随时间变化(昼夜、季节) -- **环境动态**:资源恢复、天气变化等环境动态效果 - -### 5. 学习和适应能力 -- **经验学习**:基于经验优化行为策略 -- **环境适应**:根据环境条件调整行为参数 -- **强化学习**:使用Q-learning算法优化决策 -- **探索与利用**:平衡探索新策略和利用已知策略 -- **参数自适应**:自动调整行为参数以适应环境 - -### 5. 物理模拟 -- **重力模拟**:真实的重力效果 -- **空气阻力**:考虑空气阻力对移动的影响 -- **浮力效果**:适用于水下生物模拟 -- **质量属性**:不同成员可以有不同的质量 - -### 6. 性能优化 -- **空间分区**:使用网格划分优化邻居查找 -- **邻居搜索优化**:减少不必要的距离计算 -- **并行计算**:支持多线程计算加速 -- **多群体管理**:高效管理多个群体 - -## 配置参数 - -### 基本Boids参数 -- `cohesion_weight`:聚集权重 (0.0-10.0) -- `separation_weight`:分离权重 (0.0-10.0) -- `alignment_weight`:对齐权重 (0.0-10.0) -- `perception_radius`:感知半径 (0.1-100.0) - -### 移动参数 -- `max_speed`:最大速度 (0.1-50.0) -- `max_acceleration`:最大加速度 (0.01-10.0) -- `max_force`:最大转向力 (0.01-5.0) - -### 高级行为参数 -- `personal_space`:个人空间半径 (0.1-20.0) -- `obstacle_radius`:障碍物影响半径 (0.1-50.0) -- `seek_weight`:寻求力权重 (0.0-10.0) -- `flee_weight`:逃离力权重 (0.0-10.0) -- `wander_weight`:游走力权重 (0.0-5.0) -- `boundary_weight`:边界力权重 (0.0-10.0) -- `path_weight`:路径跟随权重 (0.0-10.0) -- `formation_weight`:队形保持权重 (0.0-10.0) -- `predator_avoid_weight`:捕食者躲避权重 (0.0-10.0) -- `interaction_weight`:群体间交互权重 (0.0-5.0) - -### 环境感知参数 -- `sensor_range`:传感器范围 (1.0-50.0) -- `sensor_accuracy`:传感器精度 (0.1-1.0) -- `obstacle_response_weight`:障碍物响应权重 (0.0-5.0) -- `predator_response_weight`:捕食者响应权重 (0.0-5.0) -- `resource_seek_weight`:资源寻求权重 (0.0-2.0) -- `hazard_avoid_weight`:危险回避权重 (0.0-5.0) -- `time_flow_rate`:时间流逝速度 (0.01-2.0) -- `gravity_enabled`:重力开关 (布尔值) -- `air_resistance_enabled`:空气阻力开关 (布尔值) -- `buoyancy_enabled`:浮力开关 (布尔值) -- `fluid_level`:流体水平面高度 (-100.0-100.0) - -### 性能参数 -- `spatial_partitioning`:空间分区优化 (布尔值) -- `neighbor_search_optimization`:邻居搜索优化 (布尔值) -- `parallel_computation`:并行计算 (布尔值) -- `update_rate`:更新频率 (1-240 FPS) - -## 使用示例 - -### 基本使用 -```python -# 创建群体管理器 -swarm_manager = SwarmManager(world) - -# 设置参数 -swarm_manager.config.set("member_count", 100) -swarm_manager.config.set("cohesion_weight", 1.2) -swarm_manager.config.set("separation_weight", 1.8) - -# 创建群体 -swarm_manager.create_example_swarm() -``` - -### 高级使用 -```python -# 启用物理效果 -swarm_manager.config.set("gravity_enabled", True) -swarm_manager.config.set("air_resistance_enabled", True) - -# 启用性能优化 -swarm_manager.config.set("spatial_partitioning", True) -swarm_manager.config.set("neighbor_search_optimization", True) - -# 添加障碍物 -swarm_manager.create_obstacle(Vec3(10, 10, 5), 3.0) - -# 设置目标 -swarm_manager.set_target(Vec3(20, 20, 10)) -swarm_manager.config.set("seek_enabled", True) -``` - -## API参考 - -### SwarmManager类 -- `create_example_swarm()`:创建示例群体 -- `create_multiple_swarms(swarm_configs)`:创建多个群体 -- `create_obstacle(position, radius)`:创建障碍物 -- `create_predator(position)`:创建捕食者 -- `set_target(target)`:设置群体目标位置 -- `set_path(path)`:设置群体移动路径 -- `add_member()`:添加一个新的群体成员 -- `remove_member()`:移除一个群体成员 -- `update_swarm_type(swarm_type)`:更新群体类型 -- `get_stats()`:获取群体统计信息 -- `toggle_pause()`:切换暂停状态 -- `cleanup()`:清理资源 -- `reset()`:重置群体 -- `get_swarm_by_id(swarm_id)`:根据ID获取群体 -- `get_swarm_by_type(swarm_type)`:根据类型获取群体 - -### Config类 -- `get(key, default=None)`:获取配置值 -- `set(key, value)`:设置配置值 -- `reset_to_default()`:重置为默认配置 -- `load()`:从文件加载配置 -- `save()`:保存配置到文件 -- `export_config(file_path)`:导出配置到指定文件 -- `import_config(file_path)`:从指定文件导入配置 - -### InteractionManager类 -- `add_interaction(swarm1, swarm2, interaction_type, strength)`:添加群体间交互关系 -- `remove_interaction(interaction)`:移除群体间交互关系 -- `update_interactions(swarms)`:更新所有群体间交互 -- `get_interaction_stats()`:获取交互统计信息 -- `clear_interactions()`:清空所有交互关系 - -## 性能优化建议 - -1. **启用空间分区**:对于大规模群体(>100成员),启用空间分区可以显著提高性能 -2. **调整感知半径**:较小的感知半径可以减少计算量 -3. **使用邻居搜索优化**:减少不必要的距离计算 -4. **启用并行计算**:对于大规模群体(>50成员),启用并行计算可以利用多核CPU提高性能 -5. **合理设置更新频率**:根据需求调整更新频率,在性能和流畅度之间找到平衡 - -## 扩展开发 - -插件设计为模块化结构,可以轻松扩展新功能: - -1. **自定义行为**:继承`AdvancedBoidsAlgorithm`类并添加新的行为方法 -2. **新的队形类型**:在`calculate_flock_formation`方法中添加新的队形计算逻辑 -3. **新的物理效果**:扩展`PhysicsSimulator`类添加新的物理模拟 -4. **新的优化算法**:改进`SpatialPartition`类实现更高效的邻居查找 -5. **自定义并行策略**:修改`update_swarm_parallel`方法实现自定义的并行计算策略 - -## 故障排除 - -### 常见问题 -1. **性能问题**:检查是否启用了空间分区和邻居搜索优化 -2. **群体行为异常**:检查权重参数是否设置合理 -3. **物理效果不明显**:确保启用了相应的物理效果开关 - -### 调试建议 -1. 使用状态信息标签页监控群体统计信息 -2. 查看性能信息了解计算开销 +# 群体智能插件 - 高级功能说明 + +## 概述 + +本插件实现了一个完整的群体智能系统,基于Boids算法并扩展了多种高级行为和优化功能。 + +## 核心功能 + +### 1. 高级Boids算法 +- **聚集行为**:个体向邻居群集中心移动 +- **分离行为**:个体避免与邻居过于靠近 +- **对齐行为**:个体与邻居的移动方向保持一致 +- **加权邻居计算**:距离越近的邻居对行为影响越大 +- **个人空间**:考虑个体的个人空间需求 + +### 2. 扩展行为 +- **避障行为**:避开场景中的障碍物 +- **寻求行为**:向目标位置移动 +- **逃离行为**:远离目标位置 +- **游走行为**:产生自然的随机移动 +- **边界限制**:将成员保持在指定区域内 +- **路径跟随**:沿着预定义路径移动 +- **队形保持**:维持特定的群体队形(V字形、直线、圆形、楔形、球形) +- **躲避捕食者**:远离捕食者 +- **到达行为**:接近目标时减速 + +### 3. 群体间交互 +- **合作交互**:群体之间相互协作,共享资源或信息 +- **竞争交互**:群体之间争夺资源或领域 +- **捕食者-猎物交互**:一个群体捕食另一个群体 +- **中性交互**:群体之间无直接相互作用 +- **动态交互**:交互关系随时间变化 + +### 4. 环境感知和响应机制 +- **环境传感器**:每个群体成员都配备环境传感器,可感知障碍物、其他群体、捕食者、资源点和危险区域 +- **传感器参数**:可调节传感器范围和精度 +- **环境响应**:根据感知到的环境信息调整行为(避障、避敌、寻资源、避危险) +- **天气适应**:适应风向、温度等天气条件 +- **时间效应**:环境随时间变化(昼夜、季节) +- **环境动态**:资源恢复、天气变化等环境动态效果 + +### 5. 学习和适应能力 +- **经验学习**:基于经验优化行为策略 +- **环境适应**:根据环境条件调整行为参数 +- **强化学习**:使用Q-learning算法优化决策 +- **探索与利用**:平衡探索新策略和利用已知策略 +- **参数自适应**:自动调整行为参数以适应环境 + +### 5. 物理模拟 +- **重力模拟**:真实的重力效果 +- **空气阻力**:考虑空气阻力对移动的影响 +- **浮力效果**:适用于水下生物模拟 +- **质量属性**:不同成员可以有不同的质量 + +### 6. 性能优化 +- **空间分区**:使用网格划分优化邻居查找 +- **邻居搜索优化**:减少不必要的距离计算 +- **并行计算**:支持多线程计算加速 +- **多群体管理**:高效管理多个群体 + +## 配置参数 + +### 基本Boids参数 +- `cohesion_weight`:聚集权重 (0.0-10.0) +- `separation_weight`:分离权重 (0.0-10.0) +- `alignment_weight`:对齐权重 (0.0-10.0) +- `perception_radius`:感知半径 (0.1-100.0) + +### 移动参数 +- `max_speed`:最大速度 (0.1-50.0) +- `max_acceleration`:最大加速度 (0.01-10.0) +- `max_force`:最大转向力 (0.01-5.0) + +### 高级行为参数 +- `personal_space`:个人空间半径 (0.1-20.0) +- `obstacle_radius`:障碍物影响半径 (0.1-50.0) +- `seek_weight`:寻求力权重 (0.0-10.0) +- `flee_weight`:逃离力权重 (0.0-10.0) +- `wander_weight`:游走力权重 (0.0-5.0) +- `boundary_weight`:边界力权重 (0.0-10.0) +- `path_weight`:路径跟随权重 (0.0-10.0) +- `formation_weight`:队形保持权重 (0.0-10.0) +- `predator_avoid_weight`:捕食者躲避权重 (0.0-10.0) +- `interaction_weight`:群体间交互权重 (0.0-5.0) + +### 环境感知参数 +- `sensor_range`:传感器范围 (1.0-50.0) +- `sensor_accuracy`:传感器精度 (0.1-1.0) +- `obstacle_response_weight`:障碍物响应权重 (0.0-5.0) +- `predator_response_weight`:捕食者响应权重 (0.0-5.0) +- `resource_seek_weight`:资源寻求权重 (0.0-2.0) +- `hazard_avoid_weight`:危险回避权重 (0.0-5.0) +- `time_flow_rate`:时间流逝速度 (0.01-2.0) +- `gravity_enabled`:重力开关 (布尔值) +- `air_resistance_enabled`:空气阻力开关 (布尔值) +- `buoyancy_enabled`:浮力开关 (布尔值) +- `fluid_level`:流体水平面高度 (-100.0-100.0) + +### 性能参数 +- `spatial_partitioning`:空间分区优化 (布尔值) +- `neighbor_search_optimization`:邻居搜索优化 (布尔值) +- `parallel_computation`:并行计算 (布尔值) +- `update_rate`:更新频率 (1-240 FPS) + +## 使用示例 + +### 基本使用 +```python +# 创建群体管理器 +swarm_manager = SwarmManager(world) + +# 设置参数 +swarm_manager.config.set("member_count", 100) +swarm_manager.config.set("cohesion_weight", 1.2) +swarm_manager.config.set("separation_weight", 1.8) + +# 创建群体 +swarm_manager.create_example_swarm() +``` + +### 高级使用 +```python +# 启用物理效果 +swarm_manager.config.set("gravity_enabled", True) +swarm_manager.config.set("air_resistance_enabled", True) + +# 启用性能优化 +swarm_manager.config.set("spatial_partitioning", True) +swarm_manager.config.set("neighbor_search_optimization", True) + +# 添加障碍物 +swarm_manager.create_obstacle(Vec3(10, 10, 5), 3.0) + +# 设置目标 +swarm_manager.set_target(Vec3(20, 20, 10)) +swarm_manager.config.set("seek_enabled", True) +``` + +## API参考 + +### SwarmManager类 +- `create_example_swarm()`:创建示例群体 +- `create_multiple_swarms(swarm_configs)`:创建多个群体 +- `create_obstacle(position, radius)`:创建障碍物 +- `create_predator(position)`:创建捕食者 +- `set_target(target)`:设置群体目标位置 +- `set_path(path)`:设置群体移动路径 +- `add_member()`:添加一个新的群体成员 +- `remove_member()`:移除一个群体成员 +- `update_swarm_type(swarm_type)`:更新群体类型 +- `get_stats()`:获取群体统计信息 +- `toggle_pause()`:切换暂停状态 +- `cleanup()`:清理资源 +- `reset()`:重置群体 +- `get_swarm_by_id(swarm_id)`:根据ID获取群体 +- `get_swarm_by_type(swarm_type)`:根据类型获取群体 + +### Config类 +- `get(key, default=None)`:获取配置值 +- `set(key, value)`:设置配置值 +- `reset_to_default()`:重置为默认配置 +- `load()`:从文件加载配置 +- `save()`:保存配置到文件 +- `export_config(file_path)`:导出配置到指定文件 +- `import_config(file_path)`:从指定文件导入配置 + +### InteractionManager类 +- `add_interaction(swarm1, swarm2, interaction_type, strength)`:添加群体间交互关系 +- `remove_interaction(interaction)`:移除群体间交互关系 +- `update_interactions(swarms)`:更新所有群体间交互 +- `get_interaction_stats()`:获取交互统计信息 +- `clear_interactions()`:清空所有交互关系 + +## 性能优化建议 + +1. **启用空间分区**:对于大规模群体(>100成员),启用空间分区可以显著提高性能 +2. **调整感知半径**:较小的感知半径可以减少计算量 +3. **使用邻居搜索优化**:减少不必要的距离计算 +4. **启用并行计算**:对于大规模群体(>50成员),启用并行计算可以利用多核CPU提高性能 +5. **合理设置更新频率**:根据需求调整更新频率,在性能和流畅度之间找到平衡 + +## 扩展开发 + +插件设计为模块化结构,可以轻松扩展新功能: + +1. **自定义行为**:继承`AdvancedBoidsAlgorithm`类并添加新的行为方法 +2. **新的队形类型**:在`calculate_flock_formation`方法中添加新的队形计算逻辑 +3. **新的物理效果**:扩展`PhysicsSimulator`类添加新的物理模拟 +4. **新的优化算法**:改进`SpatialPartition`类实现更高效的邻居查找 +5. **自定义并行策略**:修改`update_swarm_parallel`方法实现自定义的并行计算策略 + +## 故障排除 + +### 常见问题 +1. **性能问题**:检查是否启用了空间分区和邻居搜索优化 +2. **群体行为异常**:检查权重参数是否设置合理 +3. **物理效果不明显**:确保启用了相应的物理效果开关 + +### 调试建议 +1. 使用状态信息标签页监控群体统计信息 +2. 查看性能信息了解计算开销 3. 逐步调整参数观察行为变化 \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/__init__.py b/plugins/user/swarm_intelligence/__init__.py index 64c07bf0..ad4ad711 100644 --- a/plugins/user/swarm_intelligence/__init__.py +++ b/plugins/user/swarm_intelligence/__init__.py @@ -1,684 +1,684 @@ -""" -群体智能插件完整实现 -包含插件主类、群体管理器、Boids算法、配置管理、GUI界面和示例 -""" - -# plugin.py -plugin_code = ''' -""" -群体智能插件 -实现Boids群体智能算法 -""" - -from plugins.plugin_manager import BasePlugin -from .swarm_manager import SwarmManager - -class Plugin(BasePlugin): - """ - 群体智能插件 - """ - - def __init__(self, plugin_manager, name): - super().__init__(plugin_manager, name) - # 设置插件配置 - self.config = { - "version": "1.0.0", - "author": "AI Assistant", - "description": "实现Boids群体智能算法的插件" - } - self.swarm_manager = None - - def initialize(self) -> bool: - """ - 初始化插件 - """ - print(f"初始化插件: {self.name}") - # 在这里执行插件初始化逻辑 - return True - - def enable(self) -> bool: - """ - 启用插件 - """ - if not super().enable(): - return False - - print(f"启用插件: {self.name}") - - # 初始化群体管理器 - self.swarm_manager = SwarmManager(self.plugin_manager.world) - - # 创建示例群体 - self.swarm_manager.create_example_swarm() - - # 注册事件处理器 - self.plugin_manager.world.accept("f1", self._on_f1_pressed) - - return True - - def disable(self) -> bool: - """ - 禁用插件 - """ - if not super().disable(): - return False - - print(f"禁用插件: {self.name}") - - # 清理群体 - if self.swarm_manager: - self.swarm_manager.cleanup() - - # 移除事件处理器 - self.plugin_manager.world.ignore("f1") - - return True - - def finalize(self): - """ - 清理插件资源 - """ - print(f"清理插件资源: {self.name}") - # 在这里执行最终清理逻辑 - - def get_info(self) -> dict: - """ - 获取插件信息 - """ - info = super().get_info() - info.update({ - "version": self.config.get("version", "1.0.0"), - "author": self.config.get("author", "Unknown"), - "description": self.config.get("description", "Base plugin class") - }) - return info - - def _on_f1_pressed(self): - """ - F1按键处理函数 - """ - print("群体智能插件: F1按键被按下") - if self.swarm_manager: - self.swarm_manager.toggle_pause() -''' - -# swarm_manager.py -swarm_manager_code = ''' -""" -群体管理器 -负责创建和管理群体成员 -""" - -from panda3d.core import Vec3, Point3 -import random - -class SwarmManager: - """ - 群体管理器 - 负责创建和管理群体成员 - """ - - def __init__(self, world): - self.world = world - self.members = [] - self.paused = False - self.task = None - - # 群体参数 - self.cohesion_weight = 1.0 - self.separation_weight = 1.5 - self.alignment_weight = 1.0 - self.max_speed = 5.0 - self.perception_radius = 10.0 - - # 启动更新任务 - self.task = self.world.taskMgr.add(self._update_task, "swarm_update_task") - - def create_example_swarm(self): - """ - 创建示例群体 - """ - # 创建20个群体成员 - for i in range(20): - # 随机位置 - pos = Vec3( - random.uniform(-20, 20), - random.uniform(-20, 20), - random.uniform(5, 15) - ) - - # 创建简单的球体代表群体成员 - member = self.world.loader.loadModel("models/misc/sphere") - member.reparentTo(self.world.render) - member.setPos(pos) - member.setScale(0.5) - - # 随机初始速度 - velocity = Vec3( - random.uniform(-1, 1), - random.uniform(-1, 1), - random.uniform(-1, 1) - ) - - # 添加到群体成员列表 - self.members.append({ - 'node': member, - 'position': pos, - 'velocity': velocity - }) - - def _update_task(self, task): - """ - 更新任务 - """ - if not self.paused: - self._update_swarm() - return task.cont - - def _update_swarm(self): - """ - 更新群体行为 - """ - for i, member in enumerate(self.members): - # 计算邻居 - neighbors = self._find_neighbors(member, i) - - # 计算三个基本力 - cohesion_force = self._calculate_cohesion(member, neighbors) - separation_force = self._calculate_separation(member, neighbors) - alignment_force = self._calculate_alignment(member, neighbors) - - # 应用权重 - cohesion_force *= self.cohesion_weight - separation_force *= self.separation_weight - alignment_force *= self.alignment_weight - - # 计算总加速度 - acceleration = cohesion_force + separation_force + alignment_force - - # 更新速度和位置 - member['velocity'] += acceleration - # 限制最大速度 - if member['velocity'].length() > self.max_speed: - member['velocity'].normalize() - member['velocity'] *= self.max_speed - - member['position'] += member['velocity'] - member['node'].setPos(member['position']) - - def _find_neighbors(self, member, index): - """ - 查找邻居 - """ - neighbors = [] - for i, other in enumerate(self.members): - if i != index: # 排除自己 - distance = (member['position'] - other['position']).length() - if distance < self.perception_radius: - neighbors.append(other) - return neighbors - - def _calculate_cohesion(self, member, neighbors): - """ - 计算聚集力 - """ - if not neighbors: - return Vec3(0, 0, 0) - - # 计算邻居的中心位置 - center = Vec3(0, 0, 0) - for neighbor in neighbors: - center += neighbor['position'] - center /= len(neighbors) - - # 返回指向中心的向量 - return (center - member['position']).normalized() - - def _calculate_separation(self, member, neighbors): - """ - 计算分离力 - """ - if not neighbors: - return Vec3(0, 0, 0) - - separation = Vec3(0, 0, 0) - for neighbor in neighbors: - diff = member['position'] - neighbor['position'] - distance = diff.length() - if distance > 0: # 避免除零 - # 距离越近,分离力越大 - separation += diff.normalized() / distance - - if len(neighbors) > 0: - separation /= len(neighbors) - - return separation.normalized() if separation.length() > 0 else separation - - def _calculate_alignment(self, member, neighbors): - """ - 计算对齐力 - """ - if not neighbors: - return Vec3(0, 0, 0) - - # 计算邻居的平均速度方向 - avg_velocity = Vec3(0, 0, 0) - for neighbor in neighbors: - avg_velocity += neighbor['velocity'] - avg_velocity /= len(neighbors) - - return avg_velocity.normalized() - - def toggle_pause(self): - """ - 切换暂停状态 - """ - self.paused = not self.paused - print(f"群体模拟{'已暂停' if self.paused else '已恢复'}") - - def cleanup(self): - """ - 清理资源 - """ - # 移除任务 - if self.task: - self.world.taskMgr.remove(self.task) - - # 移除所有群体成员 - for member in self.members: - member['node'].removeNode() - - self.members.clear() -''' - -# config.py -config_code = ''' -""" -群体智能插件配置 -""" - -class Config: - """ - 配置管理类 - """ - - def __init__(self): - # 默认配置 - self.default_settings = { - "cohesion_weight": 1.0, - "separation_weight": 1.5, - "alignment_weight": 1.0, - "max_speed": 5.0, - "perception_radius": 10.0, - "member_count": 20 - } - - # 当前配置 - self.settings = self.default_settings.copy() - - def get(self, key, default=None): - """ - 获取配置值 - """ - return self.settings.get(key, default) - - def set(self, key, value): - """ - 设置配置值 - """ - self.settings[key] = value - - def reset_to_default(self): - """ - 重置为默认配置 - """ - self.settings = self.default_settings.copy() - - def load_from_dict(self, config_dict): - """ - 从字典加载配置 - """ - self.settings.update(config_dict) - - def to_dict(self): - """ - 导出配置为字典 - """ - return self.settings.copy() -''' - -# boids_algorithm.py -boids_algorithm_code = ''' -""" -Boids算法实现 -""" - -from panda3d.core import Vec3 - -class BoidsAlgorithm: - """ - Boids算法实现类 - """ - - def __init__(self, config): - self.config = config - - def calculate_cohesion(self, member, neighbors): - """ - 计算聚集力 - """ - if not neighbors: - return Vec3(0, 0, 0) - - # 计算邻居的中心位置 - center = Vec3(0, 0, 0) - for neighbor in neighbors: - center += neighbor['position'] - center /= len(neighbors) - - # 返回指向中心的向量 - return (center - member['position']).normalized() - - def calculate_separation(self, member, neighbors): - """ - 计算分离力 - """ - if not neighbors: - return Vec3(0, 0, 0) - - separation = Vec3(0, 0, 0) - for neighbor in neighbors: - diff = member['position'] - neighbor['position'] - distance = diff.length() - if distance > 0: # 避免除零 - # 距离越近,分离力越大 - separation += diff.normalized() / distance - - if len(neighbors) > 0: - separation /= len(neighbors) - - return separation.normalized() if separation.length() > 0 else separation - - def calculate_alignment(self, member, neighbors): - """ - 计算对齐力 - """ - if not neighbors: - return Vec3(0, 0, 0) - - # 计算邻居的平均速度方向 - avg_velocity = Vec3(0, 0, 0) - for neighbor in neighbors: - avg_velocity += neighbor['velocity'] - avg_velocity /= len(neighbors) - - return avg_velocity.normalized() - - def calculate_obstacle_avoidance(self, member, obstacles): - """ - 计算避障力 - """ - # 简单实现,实际应用中可以更复杂 - avoidance = Vec3(0, 0, 0) - for obstacle in obstacles: - diff = member['position'] - obstacle['position'] - distance = diff.length() - if distance < obstacle['radius']: - avoidance += diff.normalized() / distance - - return avoidance.normalized() if avoidance.length() > 0 else avoidance - - def update_member(self, member, neighbors, obstacles=[]): - """ - 更新群体成员状态 - """ - # 计算三个基本力 - cohesion_force = self.calculate_cohesion(member, neighbors) - separation_force = self.calculate_separation(member, neighbors) - alignment_force = self.calculate_alignment(member, neighbors) - - # 计算避障力 - obstacle_force = self.calculate_obstacle_avoidance(member, obstacles) - - # 应用权重 - cohesion_force *= self.config.get("cohesion_weight", 1.0) - separation_force *= self.config.get("separation_weight", 1.5) - alignment_force *= self.config.get("alignment_weight", 1.0) - obstacle_force *= self.config.get("obstacle_weight", 2.0) - - # 计算总加速度 - acceleration = cohesion_force + separation_force + alignment_force + obstacle_force - - # 更新速度和位置 - member['velocity'] += acceleration - # 限制最大速度 - max_speed = self.config.get("max_speed", 5.0) - if member['velocity'].length() > max_speed: - member['velocity'].normalize() - member['velocity'] *= max_speed - - member['position'] += member['velocity'] -''' - -# gui.py -gui_code = ''' -""" -群体智能插件GUI界面 -""" - -from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QSlider, QLabel, QGroupBox -from PyQt5.QtCore import Qt - -class SwarmGUI(QWidget): - """ - 群体智能插件GUI界面 - """ - - def __init__(self, swarm_manager, config): - super().__init__() - self.swarm_manager = swarm_manager - self.config = config - self.init_ui() - - def init_ui(self): - """ - 初始化UI界面 - """ - self.setWindowTitle('群体智能控制面板') - self.setGeometry(100, 100, 300, 400) - - layout = QVBoxLayout() - - # 控制按钮组 - control_group = QGroupBox("控制") - control_layout = QHBoxLayout() - - self.pause_button = QPushButton("暂停/继续") - self.pause_button.clicked.connect(self.toggle_pause) - control_layout.addWidget(self.pause_button) - - reset_button = QPushButton("重置") - reset_button.clicked.connect(self.reset_swarm) - control_layout.addWidget(reset_button) - - control_group.setLayout(control_layout) - layout.addWidget(control_group) - - # 参数调节组 - params_group = QGroupBox("参数调节") - params_layout = QVBoxLayout() - - # 聚集权重 - cohesion_layout = QHBoxLayout() - cohesion_layout.addWidget(QLabel("聚集权重:")) - self.cohesion_slider = QSlider(Qt.Horizontal) - self.cohesion_slider.setRange(0, 50) - self.cohesion_slider.setValue(int(self.config.get("cohesion_weight", 1.0) * 10)) - self.cohesion_slider.valueChanged.connect(self.update_cohesion_weight) - cohesion_layout.addWidget(self.cohesion_slider) - - # 分离权重 - separation_layout = QHBoxLayout() - separation_layout.addWidget(QLabel("分离权重:")) - self.separation_slider = QSlider(Qt.Horizontal) - self.separation_slider.setRange(0, 50) - self.separation_slider.setValue(int(self.config.get("separation_weight", 1.5) * 10)) - self.separation_slider.valueChanged.connect(self.update_separation_weight) - separation_layout.addWidget(self.separation_slider) - - # 对齐权重 - alignment_layout = QHBoxLayout() - alignment_layout.addWidget(QLabel("对齐权重:")) - self.alignment_slider = QSlider(Qt.Horizontal) - self.alignment_slider.setRange(0, 50) - self.alignment_slider.setValue(int(self.config.get("alignment_weight", 1.0) * 10)) - self.alignment_slider.valueChanged.connect(self.update_alignment_weight) - alignment_layout.addWidget(self.alignment_slider) - - # 最大速度 - speed_layout = QHBoxLayout() - speed_layout.addWidget(QLabel("最大速度:")) - self.speed_slider = QSlider(Qt.Horizontal) - self.speed_slider.setRange(1, 20) - self.speed_slider.setValue(int(self.config.get("max_speed", 5.0))) - self.speed_slider.valueChanged.connect(self.update_max_speed) - speed_layout.addWidget(self.speed_slider) - - # 感知半径 - radius_layout = QHBoxLayout() - radius_layout.addWidget(QLabel("感知半径:")) - self.radius_slider = QSlider(Qt.Horizontal) - self.radius_slider.setRange(1, 50) - self.radius_slider.setValue(int(self.config.get("perception_radius", 10.0))) - self.radius_slider.valueChanged.connect(self.update_perception_radius) - radius_layout.addWidget(self.radius_slider) - - params_group.setLayout(params_layout) - layout.addWidget(params_group) - - # 状态信息组 - status_group = QGroupBox("状态信息") - status_layout = QVBoxLayout() - self.status_label = QLabel("群体成员数量: 0") - status_layout.addWidget(self.status_label) - status_group.setLayout(status_layout) - layout.addWidget(status_group) - - self.setLayout(layout) - - def toggle_pause(self): - """ - 切换暂停状态 - """ - if self.swarm_manager: - self.swarm_manager.toggle_pause() - - def reset_swarm(self): - """ - 重置群体 - """ - if self.swarm_manager: - self.swarm_manager.cleanup() - self.swarm_manager.create_example_swarm() - self.update_status() - - def update_cohesion_weight(self, value): - """ - 更新聚集权重 - """ - weight = value / 10.0 - self.config.set("cohesion_weight", weight) - if self.swarm_manager: - self.swarm_manager.cohesion_weight = weight - - def update_separation_weight(self, value): - """ - 更新分离权重 - """ - weight = value / 10.0 - self.config.set("separation_weight", weight) - if self.swarm_manager: - self.swarm_manager.separation_weight = weight - - def update_alignment_weight(self, value): - """ - 更新对齐权重 - """ - weight = value / 10.0 - self.config.set("alignment_weight", weight) - if self.swarm_manager: - self.swarm_manager.alignment_weight = weight - - def update_max_speed(self, value): - """ - 更新最大速度 - """ - self.config.set("max_speed", float(value)) - if self.swarm_manager: - self.swarm_manager.max_speed = float(value) - - def update_perception_radius(self, value): - """ - 更新感知半径 - """ - self.config.set("perception_radius", float(value)) - if self.swarm_manager: - self.swarm_manager.perception_radius = float(value) - - def update_status(self): - """ - 更新状态信息 - """ - if self.swarm_manager: - count = len(self.swarm_manager.members) - self.status_label.setText(f"群体成员数量: {count}") -''' - -# __init__.py -init_code = ''' -""" -群体智能插件初始化 -""" - -from .plugin import Plugin -from .swarm_manager import SwarmManager -from .boids_algorithm import BoidsAlgorithm -from .config import Config -from .gui import SwarmGUI - -__all__ = ['Plugin', 'SwarmManager', 'BoidsAlgorithm', 'Config', 'SwarmGUI'] -''' - -# Create the plugin directory and files -import os -plugin_dir = "/home/hello/EG/plugins/user/swarm_intelligence" -os.makedirs(plugin_dir, exist_ok=True) - -# Write the files -with open(os.path.join(plugin_dir, "plugin.py"), "w", encoding="utf-8") as f: - f.write(plugin_code) - -with open(os.path.join(plugin_dir, "swarm_manager.py"), "w", encoding="utf-8") as f: - f.write(swarm_manager_code) - -with open(os.path.join(plugin_dir, "config.py"), "w", encoding="utf-8") as f: - f.write(config_code) - -with open(os.path.join(plugin_dir, "boids_algorithm.py"), "w", encoding="utf-8") as f: - f.write(boids_algorithm_code) - -with open(os.path.join(plugin_dir, "gui.py"), "w", encoding="utf-8") as f: - f.write(gui_code) - -with open(os.path.join(plugin_dir, "__init__.py"), "w", encoding="utf-8") as f: - f.write(init_code) - -print("群体智能插件已成功创建!") -print("插件包含以下功能:") -print("1. Boids群体智能算法实现") -print("2. 可调节的群体行为参数") -print("3. 可视化控制面板") -print("4. 实时参数调节") -print("5. 暂停/继续控制") -print("6. 示例场景") +""" +群体智能插件完整实现 +包含插件主类、群体管理器、Boids算法、配置管理、GUI界面和示例 +""" + +# plugin.py +plugin_code = ''' +""" +群体智能插件 +实现Boids群体智能算法 +""" + +from plugins.plugin_manager import BasePlugin +from .swarm_manager import SwarmManager + +class Plugin(BasePlugin): + """ + 群体智能插件 + """ + + def __init__(self, plugin_manager, name): + super().__init__(plugin_manager, name) + # 设置插件配置 + self.config = { + "version": "1.0.0", + "author": "AI Assistant", + "description": "实现Boids群体智能算法的插件" + } + self.swarm_manager = None + + def initialize(self) -> bool: + """ + 初始化插件 + """ + print(f"初始化插件: {self.name}") + # 在这里执行插件初始化逻辑 + return True + + def enable(self) -> bool: + """ + 启用插件 + """ + if not super().enable(): + return False + + print(f"启用插件: {self.name}") + + # 初始化群体管理器 + self.swarm_manager = SwarmManager(self.plugin_manager.world) + + # 创建示例群体 + self.swarm_manager.create_example_swarm() + + # 注册事件处理器 + self.plugin_manager.world.accept("f1", self._on_f1_pressed) + + return True + + def disable(self) -> bool: + """ + 禁用插件 + """ + if not super().disable(): + return False + + print(f"禁用插件: {self.name}") + + # 清理群体 + if self.swarm_manager: + self.swarm_manager.cleanup() + + # 移除事件处理器 + self.plugin_manager.world.ignore("f1") + + return True + + def finalize(self): + """ + 清理插件资源 + """ + print(f"清理插件资源: {self.name}") + # 在这里执行最终清理逻辑 + + def get_info(self) -> dict: + """ + 获取插件信息 + """ + info = super().get_info() + info.update({ + "version": self.config.get("version", "1.0.0"), + "author": self.config.get("author", "Unknown"), + "description": self.config.get("description", "Base plugin class") + }) + return info + + def _on_f1_pressed(self): + """ + F1按键处理函数 + """ + print("群体智能插件: F1按键被按下") + if self.swarm_manager: + self.swarm_manager.toggle_pause() +''' + +# swarm_manager.py +swarm_manager_code = ''' +""" +群体管理器 +负责创建和管理群体成员 +""" + +from panda3d.core import Vec3, Point3 +import random + +class SwarmManager: + """ + 群体管理器 + 负责创建和管理群体成员 + """ + + def __init__(self, world): + self.world = world + self.members = [] + self.paused = False + self.task = None + + # 群体参数 + self.cohesion_weight = 1.0 + self.separation_weight = 1.5 + self.alignment_weight = 1.0 + self.max_speed = 5.0 + self.perception_radius = 10.0 + + # 启动更新任务 + self.task = self.world.taskMgr.add(self._update_task, "swarm_update_task") + + def create_example_swarm(self): + """ + 创建示例群体 + """ + # 创建20个群体成员 + for i in range(20): + # 随机位置 + pos = Vec3( + random.uniform(-20, 20), + random.uniform(-20, 20), + random.uniform(5, 15) + ) + + # 创建简单的球体代表群体成员 + member = self.world.loader.loadModel("models/misc/sphere") + member.reparentTo(self.world.render) + member.setPos(pos) + member.setScale(0.5) + + # 随机初始速度 + velocity = Vec3( + random.uniform(-1, 1), + random.uniform(-1, 1), + random.uniform(-1, 1) + ) + + # 添加到群体成员列表 + self.members.append({ + 'node': member, + 'position': pos, + 'velocity': velocity + }) + + def _update_task(self, task): + """ + 更新任务 + """ + if not self.paused: + self._update_swarm() + return task.cont + + def _update_swarm(self): + """ + 更新群体行为 + """ + for i, member in enumerate(self.members): + # 计算邻居 + neighbors = self._find_neighbors(member, i) + + # 计算三个基本力 + cohesion_force = self._calculate_cohesion(member, neighbors) + separation_force = self._calculate_separation(member, neighbors) + alignment_force = self._calculate_alignment(member, neighbors) + + # 应用权重 + cohesion_force *= self.cohesion_weight + separation_force *= self.separation_weight + alignment_force *= self.alignment_weight + + # 计算总加速度 + acceleration = cohesion_force + separation_force + alignment_force + + # 更新速度和位置 + member['velocity'] += acceleration + # 限制最大速度 + if member['velocity'].length() > self.max_speed: + member['velocity'].normalize() + member['velocity'] *= self.max_speed + + member['position'] += member['velocity'] + member['node'].setPos(member['position']) + + def _find_neighbors(self, member, index): + """ + 查找邻居 + """ + neighbors = [] + for i, other in enumerate(self.members): + if i != index: # 排除自己 + distance = (member['position'] - other['position']).length() + if distance < self.perception_radius: + neighbors.append(other) + return neighbors + + def _calculate_cohesion(self, member, neighbors): + """ + 计算聚集力 + """ + if not neighbors: + return Vec3(0, 0, 0) + + # 计算邻居的中心位置 + center = Vec3(0, 0, 0) + for neighbor in neighbors: + center += neighbor['position'] + center /= len(neighbors) + + # 返回指向中心的向量 + return (center - member['position']).normalized() + + def _calculate_separation(self, member, neighbors): + """ + 计算分离力 + """ + if not neighbors: + return Vec3(0, 0, 0) + + separation = Vec3(0, 0, 0) + for neighbor in neighbors: + diff = member['position'] - neighbor['position'] + distance = diff.length() + if distance > 0: # 避免除零 + # 距离越近,分离力越大 + separation += diff.normalized() / distance + + if len(neighbors) > 0: + separation /= len(neighbors) + + return separation.normalized() if separation.length() > 0 else separation + + def _calculate_alignment(self, member, neighbors): + """ + 计算对齐力 + """ + if not neighbors: + return Vec3(0, 0, 0) + + # 计算邻居的平均速度方向 + avg_velocity = Vec3(0, 0, 0) + for neighbor in neighbors: + avg_velocity += neighbor['velocity'] + avg_velocity /= len(neighbors) + + return avg_velocity.normalized() + + def toggle_pause(self): + """ + 切换暂停状态 + """ + self.paused = not self.paused + print(f"群体模拟{'已暂停' if self.paused else '已恢复'}") + + def cleanup(self): + """ + 清理资源 + """ + # 移除任务 + if self.task: + self.world.taskMgr.remove(self.task) + + # 移除所有群体成员 + for member in self.members: + member['node'].removeNode() + + self.members.clear() +''' + +# config.py +config_code = ''' +""" +群体智能插件配置 +""" + +class Config: + """ + 配置管理类 + """ + + def __init__(self): + # 默认配置 + self.default_settings = { + "cohesion_weight": 1.0, + "separation_weight": 1.5, + "alignment_weight": 1.0, + "max_speed": 5.0, + "perception_radius": 10.0, + "member_count": 20 + } + + # 当前配置 + self.settings = self.default_settings.copy() + + def get(self, key, default=None): + """ + 获取配置值 + """ + return self.settings.get(key, default) + + def set(self, key, value): + """ + 设置配置值 + """ + self.settings[key] = value + + def reset_to_default(self): + """ + 重置为默认配置 + """ + self.settings = self.default_settings.copy() + + def load_from_dict(self, config_dict): + """ + 从字典加载配置 + """ + self.settings.update(config_dict) + + def to_dict(self): + """ + 导出配置为字典 + """ + return self.settings.copy() +''' + +# boids_algorithm.py +boids_algorithm_code = ''' +""" +Boids算法实现 +""" + +from panda3d.core import Vec3 + +class BoidsAlgorithm: + """ + Boids算法实现类 + """ + + def __init__(self, config): + self.config = config + + def calculate_cohesion(self, member, neighbors): + """ + 计算聚集力 + """ + if not neighbors: + return Vec3(0, 0, 0) + + # 计算邻居的中心位置 + center = Vec3(0, 0, 0) + for neighbor in neighbors: + center += neighbor['position'] + center /= len(neighbors) + + # 返回指向中心的向量 + return (center - member['position']).normalized() + + def calculate_separation(self, member, neighbors): + """ + 计算分离力 + """ + if not neighbors: + return Vec3(0, 0, 0) + + separation = Vec3(0, 0, 0) + for neighbor in neighbors: + diff = member['position'] - neighbor['position'] + distance = diff.length() + if distance > 0: # 避免除零 + # 距离越近,分离力越大 + separation += diff.normalized() / distance + + if len(neighbors) > 0: + separation /= len(neighbors) + + return separation.normalized() if separation.length() > 0 else separation + + def calculate_alignment(self, member, neighbors): + """ + 计算对齐力 + """ + if not neighbors: + return Vec3(0, 0, 0) + + # 计算邻居的平均速度方向 + avg_velocity = Vec3(0, 0, 0) + for neighbor in neighbors: + avg_velocity += neighbor['velocity'] + avg_velocity /= len(neighbors) + + return avg_velocity.normalized() + + def calculate_obstacle_avoidance(self, member, obstacles): + """ + 计算避障力 + """ + # 简单实现,实际应用中可以更复杂 + avoidance = Vec3(0, 0, 0) + for obstacle in obstacles: + diff = member['position'] - obstacle['position'] + distance = diff.length() + if distance < obstacle['radius']: + avoidance += diff.normalized() / distance + + return avoidance.normalized() if avoidance.length() > 0 else avoidance + + def update_member(self, member, neighbors, obstacles=[]): + """ + 更新群体成员状态 + """ + # 计算三个基本力 + cohesion_force = self.calculate_cohesion(member, neighbors) + separation_force = self.calculate_separation(member, neighbors) + alignment_force = self.calculate_alignment(member, neighbors) + + # 计算避障力 + obstacle_force = self.calculate_obstacle_avoidance(member, obstacles) + + # 应用权重 + cohesion_force *= self.config.get("cohesion_weight", 1.0) + separation_force *= self.config.get("separation_weight", 1.5) + alignment_force *= self.config.get("alignment_weight", 1.0) + obstacle_force *= self.config.get("obstacle_weight", 2.0) + + # 计算总加速度 + acceleration = cohesion_force + separation_force + alignment_force + obstacle_force + + # 更新速度和位置 + member['velocity'] += acceleration + # 限制最大速度 + max_speed = self.config.get("max_speed", 5.0) + if member['velocity'].length() > max_speed: + member['velocity'].normalize() + member['velocity'] *= max_speed + + member['position'] += member['velocity'] +''' + +# gui.py +gui_code = ''' +""" +群体智能插件GUI界面 +""" + +from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QSlider, QLabel, QGroupBox +from PyQt5.QtCore import Qt + +class SwarmGUI(QWidget): + """ + 群体智能插件GUI界面 + """ + + def __init__(self, swarm_manager, config): + super().__init__() + self.swarm_manager = swarm_manager + self.config = config + self.init_ui() + + def init_ui(self): + """ + 初始化UI界面 + """ + self.setWindowTitle('群体智能控制面板') + self.setGeometry(100, 100, 300, 400) + + layout = QVBoxLayout() + + # 控制按钮组 + control_group = QGroupBox("控制") + control_layout = QHBoxLayout() + + self.pause_button = QPushButton("暂停/继续") + self.pause_button.clicked.connect(self.toggle_pause) + control_layout.addWidget(self.pause_button) + + reset_button = QPushButton("重置") + reset_button.clicked.connect(self.reset_swarm) + control_layout.addWidget(reset_button) + + control_group.setLayout(control_layout) + layout.addWidget(control_group) + + # 参数调节组 + params_group = QGroupBox("参数调节") + params_layout = QVBoxLayout() + + # 聚集权重 + cohesion_layout = QHBoxLayout() + cohesion_layout.addWidget(QLabel("聚集权重:")) + self.cohesion_slider = QSlider(Qt.Horizontal) + self.cohesion_slider.setRange(0, 50) + self.cohesion_slider.setValue(int(self.config.get("cohesion_weight", 1.0) * 10)) + self.cohesion_slider.valueChanged.connect(self.update_cohesion_weight) + cohesion_layout.addWidget(self.cohesion_slider) + + # 分离权重 + separation_layout = QHBoxLayout() + separation_layout.addWidget(QLabel("分离权重:")) + self.separation_slider = QSlider(Qt.Horizontal) + self.separation_slider.setRange(0, 50) + self.separation_slider.setValue(int(self.config.get("separation_weight", 1.5) * 10)) + self.separation_slider.valueChanged.connect(self.update_separation_weight) + separation_layout.addWidget(self.separation_slider) + + # 对齐权重 + alignment_layout = QHBoxLayout() + alignment_layout.addWidget(QLabel("对齐权重:")) + self.alignment_slider = QSlider(Qt.Horizontal) + self.alignment_slider.setRange(0, 50) + self.alignment_slider.setValue(int(self.config.get("alignment_weight", 1.0) * 10)) + self.alignment_slider.valueChanged.connect(self.update_alignment_weight) + alignment_layout.addWidget(self.alignment_slider) + + # 最大速度 + speed_layout = QHBoxLayout() + speed_layout.addWidget(QLabel("最大速度:")) + self.speed_slider = QSlider(Qt.Horizontal) + self.speed_slider.setRange(1, 20) + self.speed_slider.setValue(int(self.config.get("max_speed", 5.0))) + self.speed_slider.valueChanged.connect(self.update_max_speed) + speed_layout.addWidget(self.speed_slider) + + # 感知半径 + radius_layout = QHBoxLayout() + radius_layout.addWidget(QLabel("感知半径:")) + self.radius_slider = QSlider(Qt.Horizontal) + self.radius_slider.setRange(1, 50) + self.radius_slider.setValue(int(self.config.get("perception_radius", 10.0))) + self.radius_slider.valueChanged.connect(self.update_perception_radius) + radius_layout.addWidget(self.radius_slider) + + params_group.setLayout(params_layout) + layout.addWidget(params_group) + + # 状态信息组 + status_group = QGroupBox("状态信息") + status_layout = QVBoxLayout() + self.status_label = QLabel("群体成员数量: 0") + status_layout.addWidget(self.status_label) + status_group.setLayout(status_layout) + layout.addWidget(status_group) + + self.setLayout(layout) + + def toggle_pause(self): + """ + 切换暂停状态 + """ + if self.swarm_manager: + self.swarm_manager.toggle_pause() + + def reset_swarm(self): + """ + 重置群体 + """ + if self.swarm_manager: + self.swarm_manager.cleanup() + self.swarm_manager.create_example_swarm() + self.update_status() + + def update_cohesion_weight(self, value): + """ + 更新聚集权重 + """ + weight = value / 10.0 + self.config.set("cohesion_weight", weight) + if self.swarm_manager: + self.swarm_manager.cohesion_weight = weight + + def update_separation_weight(self, value): + """ + 更新分离权重 + """ + weight = value / 10.0 + self.config.set("separation_weight", weight) + if self.swarm_manager: + self.swarm_manager.separation_weight = weight + + def update_alignment_weight(self, value): + """ + 更新对齐权重 + """ + weight = value / 10.0 + self.config.set("alignment_weight", weight) + if self.swarm_manager: + self.swarm_manager.alignment_weight = weight + + def update_max_speed(self, value): + """ + 更新最大速度 + """ + self.config.set("max_speed", float(value)) + if self.swarm_manager: + self.swarm_manager.max_speed = float(value) + + def update_perception_radius(self, value): + """ + 更新感知半径 + """ + self.config.set("perception_radius", float(value)) + if self.swarm_manager: + self.swarm_manager.perception_radius = float(value) + + def update_status(self): + """ + 更新状态信息 + """ + if self.swarm_manager: + count = len(self.swarm_manager.members) + self.status_label.setText(f"群体成员数量: {count}") +''' + +# __init__.py +init_code = ''' +""" +群体智能插件初始化 +""" + +from .plugin import Plugin +from .swarm_manager import SwarmManager +from .boids_algorithm import BoidsAlgorithm +from .config import Config +from .gui import SwarmGUI + +__all__ = ['Plugin', 'SwarmManager', 'BoidsAlgorithm', 'Config', 'SwarmGUI'] +''' + +# Create the plugin directory and files +import os +plugin_dir = "/home/hello/EG/plugins/user/swarm_intelligence" +os.makedirs(plugin_dir, exist_ok=True) + +# Write the files +with open(os.path.join(plugin_dir, "plugin.py"), "w", encoding="utf-8") as f: + f.write(plugin_code) + +with open(os.path.join(plugin_dir, "swarm_manager.py"), "w", encoding="utf-8") as f: + f.write(swarm_manager_code) + +with open(os.path.join(plugin_dir, "config.py"), "w", encoding="utf-8") as f: + f.write(config_code) + +with open(os.path.join(plugin_dir, "boids_algorithm.py"), "w", encoding="utf-8") as f: + f.write(boids_algorithm_code) + +with open(os.path.join(plugin_dir, "gui.py"), "w", encoding="utf-8") as f: + f.write(gui_code) + +with open(os.path.join(plugin_dir, "__init__.py"), "w", encoding="utf-8") as f: + f.write(init_code) + +print("群体智能插件已成功创建!") +print("插件包含以下功能:") +print("1. Boids群体智能算法实现") +print("2. 可调节的群体行为参数") +print("3. 可视化控制面板") +print("4. 实时参数调节") +print("5. 暂停/继续控制") +print("6. 示例场景") print("7. 完整的插件生命周期管理") \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/advanced_boids.py b/plugins/user/swarm_intelligence/advanced_boids.py index 936645d8..eea30f69 100644 --- a/plugins/user/swarm_intelligence/advanced_boids.py +++ b/plugins/user/swarm_intelligence/advanced_boids.py @@ -1,908 +1,908 @@ -""" -高级Boids算法实现 -这是一个经过优化和扩展的Boids算法实现,包含空间分区优化、并行计算和高级行为规则 -""" - -from panda3d.core import Vec3, Point3 -import random -import math -from typing import List, Dict, Tuple, Optional -import concurrent.futures -import threading -from multiprocessing import Pool, cpu_count -import numpy as np - -class SpatialPartition: - """ - 空间分区类,用于优化邻居查找算法 - 将3D空间划分为网格,每个网格格子存储位于其中的群体成员 - """ - - def __init__(self, bounds: Dict[str, float], cell_size: float): - """ - 初始化空间分区 - - :param bounds: 边界信息 {'min_x': float, 'max_x': float, 'min_y': float, 'max_y': float, 'min_z': float, 'max_z': float} - :param cell_size: 网格格子大小 - """ - self.bounds = bounds - self.cell_size = cell_size - - # 计算网格尺寸 - self.grid_width = int((bounds['max_x'] - bounds['min_x']) / cell_size) + 1 - self.grid_height = int((bounds['max_y'] - bounds['min_y']) / cell_size) + 1 - self.grid_depth = int((bounds['max_z'] - bounds['min_z']) / cell_size) + 1 - - # 创建网格 - self.grid = {} - - # 邻居格子偏移量 - self.neighbor_offsets = [ - (-1, -1, -1), (-1, -1, 0), (-1, -1, 1), - (-1, 0, -1), (-1, 0, 0), (-1, 0, 1), - (-1, 1, -1), (-1, 1, 0), (-1, 1, 1), - (0, -1, -1), (0, -1, 0), (0, -1, 1), - (0, 0, -1), (0, 0, 0), (0, 0, 1), - (0, 1, -1), (0, 1, 0), (0, 1, 1), - (1, -1, -1), (1, -1, 0), (1, -1, 1), - (1, 0, -1), (1, 0, 0), (1, 0, 1), - (1, 1, -1), (1, 1, 0), (1, 1, 1) - ] - - def get_grid_coords(self, position: Vec3) -> Tuple[int, int, int]: - """ - 获取位置对应的网格坐标 - - :param position: 3D位置 - :return: (x, y, z) 网格坐标 - """ - grid_x = int((position.x - self.bounds['min_x']) / self.cell_size) - grid_y = int((position.y - self.bounds['min_y']) / self.cell_size) - grid_z = int((position.z - self.bounds['min_z']) / self.cell_size) - - # 确保坐标在有效范围内 - grid_x = max(0, min(grid_x, self.grid_width - 1)) - grid_y = max(0, min(grid_y, self.grid_height - 1)) - grid_z = max(0, min(grid_z, self.grid_depth - 1)) - - return (grid_x, grid_y, grid_z) - - def add_member(self, member: Dict, position: Vec3): - """ - 将成员添加到网格中 - - :param member: 成员对象 - :param position: 成员位置 - """ - grid_coords = self.get_grid_coords(position) - grid_key = f"{grid_coords[0]}_{grid_coords[1]}_{grid_coords[2]}" - - if grid_key not in self.grid: - self.grid[grid_key] = [] - self.grid[grid_key].append(member) - - def remove_member(self, member: Dict, position: Vec3): - """ - 从网格中移除成员 - - :param member: 成员对象 - :param position: 成员位置 - """ - grid_coords = self.get_grid_coords(position) - grid_key = f"{grid_coords[0]}_{grid_coords[1]}_{grid_coords[2]}" - - if grid_key in self.grid: - if member in self.grid[grid_key]: - self.grid[grid_key].remove(member) - - def update_member(self, member: Dict, old_position: Vec3, new_position: Vec3): - """ - 更新成员在网格中的位置 - - :param member: 成员对象 - :param old_position: 原位置 - :param new_position: 新位置 - """ - # 从旧位置移除 - self.remove_member(member, old_position) - # 添加到新位置 - self.add_member(member, new_position) - - def get_neighbors(self, position: Vec3, radius: float) -> List[Dict]: - """ - 获取指定位置周围的邻居 - - :param position: 中心位置 - :param radius: 搜索半径 - :return: 邻居列表 - """ - neighbors = [] - - # 计算需要检查的网格格子范围 - cells_to_check = int(radius / self.cell_size) + 1 - - center_coords = self.get_grid_coords(position) - - # 检查中心格子及其邻居 - for offset in self.neighbor_offsets: - grid_x = center_coords[0] + offset[0] - grid_y = center_coords[1] + offset[1] - grid_z = center_coords[2] + offset[2] - - # 检查格子是否在有效范围内 - if (0 <= grid_x < self.grid_width and - 0 <= grid_y < self.grid_height and - 0 <= grid_z < self.grid_depth): - - grid_key = f"{grid_x}_{grid_y}_{grid_z}" - if grid_key in self.grid: - # 检查格子中的每个成员是否在半径范围内 - for member in self.grid[grid_key]: - distance = (position - member['position']).length() - if distance <= radius and member not in neighbors: - neighbors.append(member) - - return neighbors - - def clear(self): - """ - 清空网格 - """ - self.grid.clear() - -class PhysicsSimulator: - """ - 物理模拟器,提供更真实的物理行为 - """ - - def __init__(self): - self.gravity = Vec3(0, 0, -9.81) # 重力加速度 - self.air_resistance = 0.01 # 空气阻力系数 - self.fluid_density = 1.225 # 空气密度 (kg/m^3) - - def apply_gravity(self, member: Dict, dt: float): - """ - 应用重力 - """ - if 'mass' in member: - mass = member['mass'] - else: - mass = 1.0 # 默认质量 - - gravity_force = self.gravity * mass - member['velocity'] += gravity_force * dt / mass - - def apply_air_resistance(self, member: Dict, dt: float): - """ - 应用空气阻力 - """ - velocity = member['velocity'] - speed = velocity.length() - - if speed > 0: - # 空气阻力与速度平方成正比 - drag_force_magnitude = 0.5 * self.fluid_density * speed * speed * 0.01 # 简化的阻力系数 - drag_force = -velocity.normalized() * drag_force_magnitude - - # 应用阻力 - member['velocity'] += drag_force * dt - - def apply_buoyancy(self, member: Dict, fluid_level: float, dt: float): - """ - 应用浮力(适用于模拟水中生物) - """ - if member['position'].z < fluid_level: - # 计算浮力(简化模型) - buoyancy_force = Vec3(0, 0, 0.5) # 向上的浮力 - member['velocity'] += buoyancy_force * dt - - def update_physics(self, member: Dict, dt: float = 1.0/60.0): - """ - 更新物理状态 - """ - # 应用重力 - self.apply_gravity(member, dt) - - # 应用空气阻力 - self.apply_air_resistance(member, dt) - -class AdvancedBoidsAlgorithm: - """ - 高级Boids算法实现类 - 包含空间分区优化、物理模拟和高级行为规则 - """ - - def __init__(self, config): - self.config = config - - # 初始化空间分区系统 - bounds = { - 'min_x': -100, 'max_x': 100, - 'min_y': -100, 'max_y': 100, - 'min_z': -10, 'max_z': 100 - } - cell_size = self.config.get('perception_radius', 10.0) # 使用感知半径作为网格大小 - self.spatial_partition = SpatialPartition(bounds, cell_size) - - # 初始化物理模拟器 - self.physics_simulator = PhysicsSimulator() - - # 用于记录性能统计 - self.stats = { - 'neighbor_lookups': 0, - 'total_neighbors_found': 0, - 'algorithm_calls': 0 - } - - def calculate_cohesion(self, member: Dict, neighbors: List[Dict]) -> Vec3: - """ - 计算聚集力 - 个体向邻居群集中心移动 - """ - if not neighbors: - return Vec3(0, 0, 0) - - # 计算邻居的中心位置 - center = Vec3(0, 0, 0) - for neighbor in neighbors: - center += neighbor['position'] - center /= len(neighbors) - - # 返回指向中心的向量 - return (center - member['position']).normalized() - - def calculate_separation(self, member: Dict, neighbors: List[Dict]) -> Vec3: - """ - 计算分离力 - 个体避免与邻居过于靠近 - """ - if not neighbors: - return Vec3(0, 0, 0) - - separation = Vec3(0, 0, 0) - for neighbor in neighbors: - diff = member['position'] - neighbor['position'] - distance = diff.length() - if distance > 0: # 避免除零 - # 距离越近,分离力越大 - separation += diff.normalized() / distance - - if len(neighbors) > 0: - separation /= len(neighbors) - - return separation.normalized() if separation.length() > 0 else separation - - def calculate_alignment(self, member: Dict, neighbors: List[Dict]) -> Vec3: - """ - 计算对齐力 - 个体与邻居的移动方向保持一致 - """ - if not neighbors: - return Vec3(0, 0, 0) - - # 计算邻居的平均速度方向 - avg_velocity = Vec3(0, 0, 0) - for neighbor in neighbors: - avg_velocity += neighbor['velocity'] - avg_velocity /= len(neighbors) - - return avg_velocity.normalized() - - def calculate_cohesion_with_weighted_neighbors(self, member: Dict, neighbors: List[Dict]) -> Vec3: - """ - 计算加权聚集力 - 距离越近的邻居对聚集力的影响越大 - """ - if not neighbors: - return Vec3(0, 0, 0) - - weighted_center = Vec3(0, 0, 0) - total_weight = 0.0 - - for neighbor in neighbors: - distance = (member['position'] - neighbor['position']).length() - if distance > 0: - # 距离越近,权重越大 - weight = 1.0 / distance - weighted_center += neighbor['position'] * weight - total_weight += weight - - if total_weight > 0: - weighted_center /= total_weight - return (weighted_center - member['position']).normalized() - else: - return Vec3(0, 0, 0) - - def calculate_separation_with_personal_space(self, member: Dict, neighbors: List[Dict]) -> Vec3: - """ - 计算分离力,考虑个人空间 - """ - if not neighbors: - return Vec3(0, 0, 0) - - separation = Vec3(0, 0, 0) - personal_space = self.config.get("personal_space", 2.0) # 个人空间半径 - - for neighbor in neighbors: - diff = member['position'] - neighbor['position'] - distance = diff.length() - - if distance > 0 and distance < personal_space: - # 仅在邻居进入个人空间时才产生分离力 - separation += diff.normalized() * (personal_space - distance) / personal_space - - return separation.normalized() if separation.length() > 0 else separation - - def calculate_obstacle_avoidance(self, member: Dict, obstacles: List[Dict]) -> Vec3: - """ - 计算避障力 - 避开场景中的障碍物 - """ - if not obstacles: - return Vec3(0, 0, 0) - - avoidance = Vec3(0, 0, 0) - obstacle_radius = self.config.get("obstacle_radius", 5.0) - - for obstacle in obstacles: - diff = member['position'] - obstacle['position'] - distance = diff.length() - - if distance < obstacle_radius + obstacle.get('radius', 1.0): - # 距离越近,避障力越大 - force_magnitude = max(0, (obstacle_radius + obstacle.get('radius', 1.0)) - distance) - avoidance += diff.normalized() * force_magnitude - - return avoidance.normalized() if avoidance.length() > 0 else avoidance - - def calculate_seek(self, member: Dict, target: Vec3) -> Vec3: - """ - 计算寻求力 - 向目标位置移动 - """ - desired = target - member['position'] - distance = desired.length() - - if distance > 0: - desired.normalize() - # 如果启用了达到行为,当接近目标时减速 - if self.config.get("arrival_enabled", False): - slow_radius = self.config.get("arrival_slow_radius", 5.0) - if distance < slow_radius: - desired *= self.config.get("max_speed", 5.0) * (distance / slow_radius) - else: - desired *= self.config.get("max_speed", 5.0) - else: - desired *= self.config.get("max_speed", 5.0) - - # 计算转向力 - steer = desired - member['velocity'] - # 限制转向力 - max_force = self.config.get("max_force", 0.5) - if steer.length() > max_force: - steer.normalize() - steer *= max_force - - return steer - return Vec3(0, 0, 0) - - def calculate_flee(self, member: Dict, target: Vec3) -> Vec3: - """ - 计算逃离力 - 远离目标位置 - """ - desired = member['position'] - target - distance = desired.length() - - if distance > 0: - desired.normalize() - desired *= self.config.get("max_speed", 5.0) - - # 计算转向力 - steer = desired - member['velocity'] - # 限制转向力 - max_force = self.config.get("max_force", 0.5) - if steer.length() > max_force: - steer.normalize() - steer *= max_force - - return steer - return Vec3(0, 0, 0) - - def calculate_wander(self, member: Dict) -> Vec3: - """ - 计算游走力 - 产生自然的随机移动 - """ - # 获取游走参数 - wander_radius = self.config.get("wander_radius", 5.0) - wander_distance = self.config.get("wander_distance", 10.0) - wander_jitter = self.config.get("wander_jitter", 1.0) - - # 如果成员没有游走目标,创建一个 - if 'wander_target' not in member: - member['wander_target'] = Vec3( - random.uniform(-1, 1), - random.uniform(-1, 1), - random.uniform(-1, 1) - ).normalized() * wander_radius - - # 添加随机扰动 - jitter = Vec3( - random.uniform(-1, 1), - random.uniform(-1, 1), - random.uniform(-1, 1) - ).normalized() * wander_jitter - - member['wander_target'] += jitter - member['wander_target'].normalize() - member['wander_target'] *= wander_radius - - # 计算游走目标在前方的距离 - target = member['wander_target'] + Vec3(0, 0, 0) # 相对于成员前方的点 - target *= wander_distance - - # 转换到世界坐标系 - # 这里简化处理,实际应用中需要考虑成员的朝向 - desired = target - member['position'] - if desired.length() > 0: - desired.normalize() - desired *= self.config.get("max_speed", 5.0) - - # 计算转向力 - steer = desired - member['velocity'] - # 限制转向力 - max_force = self.config.get("max_force", 0.5) - if steer.length() > max_force: - steer.normalize() - steer *= max_force - - return steer - return Vec3(0, 0, 0) - - def calculate_boundaries(self, member: Dict, bounds: Dict[str, float]) -> Vec3: - """ - 计算边界力 - 将成员保持在指定区域内 - """ - if not bounds: - return Vec3(0, 0, 0) - - # 获取边界参数 - min_x, max_x = bounds['min_x'], bounds['max_x'] - min_y, max_y = bounds['min_y'], bounds['max_y'] - min_z, max_z = bounds['min_z'], bounds['max_z'] - - # 边界缓冲距离 - buffer = self.config.get("boundary_buffer", 5.0) - - # 计算边界力 - bound_force = Vec3(0, 0, 0) - - if member['position'].x < min_x + buffer: - bound_force.x = 1.0 - elif member['position'].x > max_x - buffer: - bound_force.x = -1.0 - - if member['position'].y < min_y + buffer: - bound_force.y = 1.0 - elif member['position'].y > max_y - buffer: - bound_force.y = -1.0 - - if member['position'].z < min_z + buffer: - bound_force.z = 1.0 - elif member['position'].z > max_z - buffer: - bound_force.z = -1.0 - - # 应用边界权重 - bound_force *= self.config.get("boundary_weight", 2.0) - - return bound_force - - def calculate_follow_path(self, member: Dict, path: List[Vec3]) -> Vec3: - """ - 计算路径跟随力 - 沿着预定义路径移动 - """ - if not path or len(path) < 2: - return Vec3(0, 0, 0) - - # 获取路径参数 - path_radius = self.config.get("path_radius", 3.0) - - # 找到最近的路径点 - nearest_index = 0 - nearest_distance = float('inf') - - for i, point in enumerate(path): - distance = (member['position'] - point).length() - if distance < nearest_distance: - nearest_distance = distance - nearest_index = i - - # 预测位置 - prediction = member['velocity'].normalized() - prediction *= self.config.get("prediction_distance", 10.0) - predict_pos = member['position'] + prediction - - # 找到预测位置最近的路径点 - predict_nearest_index = 0 - predict_nearest_distance = float('inf') - - for i, point in enumerate(path): - distance = (predict_pos - point).length() - if distance < predict_nearest_distance: - predict_nearest_distance = distance - predict_nearest_index = i - - # 如果偏离路径太远,直接寻求最近的路径点 - if nearest_distance > path_radius: - target = path[nearest_index] - return self.calculate_seek(member, target) - - # 否则寻求预测位置前方的路径点 - if predict_nearest_index < len(path) - 1: - target = path[predict_nearest_index + 1] - return self.calculate_seek(member, target) - - return Vec3(0, 0, 0) - - def calculate_flock_formation(self, member: Dict, neighbors: List[Dict], formation_type: str) -> Vec3: - """ - 计算队形保持力 - 维持特定的群体队形 - """ - if not neighbors: - return Vec3(0, 0, 0) - - # 根据队形类型计算期望位置 - target_position = Vec3(0, 0, 0) - - if formation_type == "V": - # V字形队形 - # 简化实现,实际应用中需要更复杂的计算 - if 'index' in member: - index = member['index'] - angle = math.radians(index * 15) # 每个成员间隔15度 - distance = 5.0 # 固定间距 - target_position = Vec3( - math.cos(angle) * distance, - math.sin(angle) * distance, - 0 - ) - elif formation_type == "line": - # 直线队形 - if 'index' in member: - index = member['index'] - spacing = 3.0 - target_position = Vec3(0, index * spacing, 0) - elif formation_type == "circle": - # 圆形队形 - if 'index' in member: - index = member['index'] - total = len(neighbors) + 1 - angle = 2 * math.pi * index / total - radius = 10.0 - target_position = Vec3( - math.cos(angle) * radius, - math.sin(angle) * radius, - 0 - ) - elif formation_type == "sphere": - # 球形队形 - if 'index' in member: - index = member['index'] - total = len(neighbors) + 1 - # 使用斐波那契螺旋法在球面上分布点 - phi = math.pi * (3 - math.sqrt(5)) # 金角 - y = 1 - (index / float(total - 1)) * 2 # y: 1 to -1 - radius = math.sqrt(1 - y * y) # 半径在y处 - theta = phi * index # 金角旋转 - x = math.cos(theta) * radius - z = math.sin(theta) * radius - target_position = Vec3(x * 10, y * 10, z * 10) # 缩放到合适大小 - - # 计算到达期望位置的力 - if target_position.length() > 0: - desired = target_position - member['position'] - if desired.length() > 0: - desired.normalize() - desired *= self.config.get("max_speed", 5.0) - - # 计算转向力 - steer = desired - member['velocity'] - # 应用队形权重 - steer *= self.config.get("formation_weight", 1.0) - # 限制转向力 - max_force = self.config.get("max_force", 0.5) - if steer.length() > max_force: - steer.normalize() - steer *= max_force - - return steer - return Vec3(0, 0, 0) - - def calculate_avoid_predator(self, member: Dict, predators: List[Dict]) -> Vec3: - """ - 计算躲避捕食者力 - 远离捕食者 - """ - if not predators: - return Vec3(0, 0, 0) - - avoidance = Vec3(0, 0, 0) - predator_radius = self.config.get("predator_radius", 15.0) - - for predator in predators: - diff = member['position'] - predator['position'] - distance = diff.length() - if distance < predator_radius: - # 距离越近,躲避力越大 - force = diff.normalized() / (distance / predator_radius) - avoidance += force - - # 应用捕食者躲避权重 - avoidance *= self.config.get("predator_avoid_weight", 3.0) - - return avoidance.normalized() if avoidance.length() > 0 else avoidance - - def calculate_cohesion_with_predator_awareness(self, member: Dict, neighbors: List[Dict], predators: List[Dict]) -> Vec3: - """ - 计算受捕食者影响的聚集力 - 在有捕食者时,成员会更紧密地聚集 - """ - if not neighbors: - return Vec3(0, 0, 0) - - # 检查是否有捕食者在附近 - has_nearby_predator = False - for predator in predators: - if (member['position'] - predator['position']).length() < self.config.get("predator_radius", 15.0): - has_nearby_predator = True - break - - # 如果有捕食者在附近,增加聚集权重 - target_factor = 1.0 - if has_nearby_predator: - target_factor = 2.0 - - # 计算邻居的中心位置 - center = Vec3(0, 0, 0) - for neighbor in neighbors: - center += neighbor['position'] - center /= len(neighbors) - - # 返回指向中心的向量 - return (center - member['position']).normalized() * target_factor - - def calculate_formation_with_obstacle_avoidance(self, member: Dict, neighbors: List[Dict], formation_type: str, obstacles: List[Dict]) -> Vec3: - """ - 计算考虑障碍物的队形保持力 - 在保持队形的同时避开障碍物 - """ - # 首先计算队形力 - formation_force = self.calculate_flock_formation(member, neighbors, formation_type) - - # 然后计算避障力 - obstacle_force = self.calculate_obstacle_avoidance(member, obstacles) - - # 结合两种力量 - combined_force = formation_force + obstacle_force * 1.5 # 避障力权重稍大 - - return combined_force.normalized() * formation_force.length() - - def update_member(self, member: Dict, neighbors: List[Dict], obstacles: List[Dict] = [], - target: Vec3 = None, bounds: Dict[str, float] = None, - path: List[Vec3] = None, predators: List[Dict] = [], - interaction_forces: Dict = None, environment_force: Vec3 = None): - """ - 更新群体成员状态 - 计算所有作用力并更新成员的位置和速度 - """ - # 记录统计信息 - self.stats['algorithm_calls'] += 1 - self.stats['total_neighbors_found'] += len(neighbors) - - # 计算基本Boids力 - cohesion_force = self.calculate_cohesion_with_weighted_neighbors(member, neighbors) - separation_force = self.calculate_separation_with_personal_space(member, neighbors) - alignment_force = self.calculate_alignment(member, neighbors) - - # 计算扩展力 - obstacle_force = self.calculate_obstacle_avoidance(member, obstacles) - bound_force = self.calculate_boundaries(member, bounds) - predator_force = self.calculate_avoid_predator(member, predators) - - # 计算目标导向力 - seek_force = Vec3(0, 0, 0) - if target: - seek_force = self.calculate_seek(member, target) - - # 计算路径跟随力 - path_force = Vec3(0, 0, 0) - if path: - path_force = self.calculate_follow_path(member, path) - - # 计算游走力 - wander_force = Vec3(0, 0, 0) - if self.config.get("wander_enabled", False): - wander_force = self.calculate_wander(member) - - # 计算受捕食者影响的聚集力 - predator_aware_cohesion = self.calculate_cohesion_with_predator_awareness(member, neighbors, predators) - - # 计算群体间交互力 - interaction_force = Vec3(0, 0, 0) - if interaction_forces: - # 合作力 - if 'cooperative_force' in interaction_forces: - interaction_force += interaction_forces['cooperative_force'] - # 竞争力 - if 'competition_force' in interaction_forces: - interaction_force += interaction_forces['competition_force'] - # 捕食力 - if 'predation_force' in interaction_forces: - interaction_force += interaction_forces['predation_force'] - # 逃避力 - if 'escape_force' in interaction_forces: - interaction_force += interaction_forces['escape_force'] - - # 应用权重 - cohesion_force *= self.config.get("cohesion_weight", 1.0) - separation_force *= self.config.get("separation_weight", 1.5) - alignment_force *= self.config.get("alignment_weight", 1.0) - obstacle_force *= self.config.get("obstacle_weight", 2.0) - seek_force *= self.config.get("seek_weight", 1.0) - bound_force *= self.config.get("boundary_weight", 2.0) - path_force *= self.config.get("path_weight", 1.0) - wander_force *= self.config.get("wander_weight", 0.5) - predator_force *= self.config.get("predator_avoid_weight", 3.0) - predator_aware_cohesion *= self.config.get("cohesion_weight", 1.0) * 0.5 # 给予一半权重 - interaction_force *= self.config.get("interaction_weight", 1.0) # 交互力权重 - - # 计算总加速度 - acceleration = ( - cohesion_force + - separation_force + - alignment_force + - obstacle_force + - seek_force + - bound_force + - path_force + - wander_force + - predator_force + - predator_aware_cohesion + - interaction_force - ) - - # 添加环境力 - if environment_force: - acceleration += environment_force * 0.1 # 环境力权重 - - # 应用加速度限制 - max_acceleration = self.config.get("max_acceleration", 1.0) - if acceleration.length() > max_acceleration: - acceleration.normalize() - acceleration *= max_acceleration - - # 应用物理模拟 - dt = 1.0 / 60.0 # 时间步长 - self.physics_simulator.update_physics(member, dt) - - # 更新速度和位置 - member['velocity'] += acceleration - - # 限制最大速度 - max_speed = self.config.get("max_speed", 5.0) - if member['velocity'].length() > max_speed: - member['velocity'].normalize() - member['velocity'] *= max_speed - - member['position'] += member['velocity'] - - # 更新成员的朝向(使其面向移动方向) - if member['velocity'].length() > 0 and 'node' in member: - # 简化实现,实际应用中可能需要更复杂的朝向计算 - member['node'].lookAt(member['position'] + member['velocity']) - - def update_swarm_parallel(self, members: List[Dict], all_members: List[Dict], - obstacles: List[Dict] = [], target: Vec3 = None, - bounds: Dict[str, float] = None, path: List[Vec3] = None, - predators: List[Dict] = [], interaction_forces: Dict = None, - environment_forces: Dict = None): - """ - 并行更新群体成员状态 - 使用多线程或多进程来并行计算每个成员的更新 - """ - if not self.config.get("spatial_partitioning", False): - # 如果没有使用空间分区,则无法高效并行计算邻居 - # 回退到串行计算 - for member in members: - neighbors = self._find_neighbors_serial(member, all_members) - member_interactions = interaction_forces.get(id(member), {}) if interaction_forces else {} - member_environment_force = environment_forces.get(id(member), Vec3(0, 0, 0)) if environment_forces else Vec3(0, 0, 0) - self.update_member(member, neighbors, obstacles, target, bounds, path, predators, member_interactions, member_environment_force) - return - - # 如果启用了空间分区,则可以使用并行计算 - use_threading = True # 对于I/O密集型任务,使用线程更好 - - # 如果需要处理大量成员,可以使用多线程 - if use_threading: - # 使用ThreadPoolExecutor进行并行计算 - num_threads = min(len(members), cpu_count()) - - # 创建线程池 - with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor: - # 提交任务 - futures = [] - for member in members: - # 获取邻居(这一步可能也需要优化) - neighbors = self.spatial_partition.get_neighbors(member['position'], - self.config.get("perception_radius", 10.0)) - member_interactions = interaction_forces.get(id(member), {}) if interaction_forces else {} - member_environment_force = environment_forces.get(id(member), Vec3(0, 0, 0)) if environment_forces else Vec3(0, 0, 0) - future = executor.submit(self._update_member_task, member, neighbors, obstacles, - target, bounds, path, predators, member_interactions, member_environment_force) - futures.append(future) - - # 等待所有任务完成 - for future in concurrent.futures.as_completed(futures): - # 获取结果(如果需要的话) - result = future.result() - else: - # 使用多进程进行并行计算 - num_processes = min(len(members), cpu_count()) - - # 准备参数 - tasks = [] - for member in members: - neighbors = self.spatial_partition.get_neighbors(member['position'], - self.config.get("perception_radius", 10.0)) - member_interactions = interaction_forces.get(id(member), {}) if interaction_forces else {} - member_environment_force = environment_forces.get(id(member), Vec3(0, 0, 0)) if environment_forces else Vec3(0, 0, 0) - task_args = (member, neighbors, obstacles, target, bounds, path, predators, member_interactions, member_environment_force) - tasks.append(task_args) - - # 使用多进程池 - with Pool(processes=num_processes) as pool: - pool.starmap(self._update_member_task, tasks) - - def _update_member_task(self, member: Dict, neighbors: List[Dict], obstacles: List[Dict], - target: Vec3, bounds: Dict[str, float], path: List[Vec3], - predators: List[Dict], interaction_forces: Dict = None, - environment_force: Vec3 = None): - """ - 用于并行任务的成员更新方法 - """ - self.update_member(member, neighbors, obstacles, target, bounds, path, predators, interaction_forces, environment_force) - - def _find_neighbors_serial(self, member: Dict, all_members: List[Dict]) -> List[Dict]: - """ - 串行查找邻居(当不使用空间分区时) - """ - neighbors = [] - perception_radius = self.config.get("perception_radius", 10.0) - - for other in all_members: - if other != member: # 排除自己 - distance = (member['position'] - other['position']).length() - if distance < perception_radius: - neighbors.append(other) - return neighbors - - def get_stats(self) -> Dict[str, int]: - """ - 获取性能统计信息 - """ - return self.stats.copy() - - def clear_stats(self): - """ - 清空性能统计信息 - """ - self.stats = { - 'neighbor_lookups': 0, - 'total_neighbors_found': 0, - 'algorithm_calls': 0 +""" +高级Boids算法实现 +这是一个经过优化和扩展的Boids算法实现,包含空间分区优化、并行计算和高级行为规则 +""" + +from panda3d.core import Vec3, Point3 +import random +import math +from typing import List, Dict, Tuple, Optional +import concurrent.futures +import threading +from multiprocessing import Pool, cpu_count +import numpy as np + +class SpatialPartition: + """ + 空间分区类,用于优化邻居查找算法 + 将3D空间划分为网格,每个网格格子存储位于其中的群体成员 + """ + + def __init__(self, bounds: Dict[str, float], cell_size: float): + """ + 初始化空间分区 + + :param bounds: 边界信息 {'min_x': float, 'max_x': float, 'min_y': float, 'max_y': float, 'min_z': float, 'max_z': float} + :param cell_size: 网格格子大小 + """ + self.bounds = bounds + self.cell_size = cell_size + + # 计算网格尺寸 + self.grid_width = int((bounds['max_x'] - bounds['min_x']) / cell_size) + 1 + self.grid_height = int((bounds['max_y'] - bounds['min_y']) / cell_size) + 1 + self.grid_depth = int((bounds['max_z'] - bounds['min_z']) / cell_size) + 1 + + # 创建网格 + self.grid = {} + + # 邻居格子偏移量 + self.neighbor_offsets = [ + (-1, -1, -1), (-1, -1, 0), (-1, -1, 1), + (-1, 0, -1), (-1, 0, 0), (-1, 0, 1), + (-1, 1, -1), (-1, 1, 0), (-1, 1, 1), + (0, -1, -1), (0, -1, 0), (0, -1, 1), + (0, 0, -1), (0, 0, 0), (0, 0, 1), + (0, 1, -1), (0, 1, 0), (0, 1, 1), + (1, -1, -1), (1, -1, 0), (1, -1, 1), + (1, 0, -1), (1, 0, 0), (1, 0, 1), + (1, 1, -1), (1, 1, 0), (1, 1, 1) + ] + + def get_grid_coords(self, position: Vec3) -> Tuple[int, int, int]: + """ + 获取位置对应的网格坐标 + + :param position: 3D位置 + :return: (x, y, z) 网格坐标 + """ + grid_x = int((position.x - self.bounds['min_x']) / self.cell_size) + grid_y = int((position.y - self.bounds['min_y']) / self.cell_size) + grid_z = int((position.z - self.bounds['min_z']) / self.cell_size) + + # 确保坐标在有效范围内 + grid_x = max(0, min(grid_x, self.grid_width - 1)) + grid_y = max(0, min(grid_y, self.grid_height - 1)) + grid_z = max(0, min(grid_z, self.grid_depth - 1)) + + return (grid_x, grid_y, grid_z) + + def add_member(self, member: Dict, position: Vec3): + """ + 将成员添加到网格中 + + :param member: 成员对象 + :param position: 成员位置 + """ + grid_coords = self.get_grid_coords(position) + grid_key = f"{grid_coords[0]}_{grid_coords[1]}_{grid_coords[2]}" + + if grid_key not in self.grid: + self.grid[grid_key] = [] + self.grid[grid_key].append(member) + + def remove_member(self, member: Dict, position: Vec3): + """ + 从网格中移除成员 + + :param member: 成员对象 + :param position: 成员位置 + """ + grid_coords = self.get_grid_coords(position) + grid_key = f"{grid_coords[0]}_{grid_coords[1]}_{grid_coords[2]}" + + if grid_key in self.grid: + if member in self.grid[grid_key]: + self.grid[grid_key].remove(member) + + def update_member(self, member: Dict, old_position: Vec3, new_position: Vec3): + """ + 更新成员在网格中的位置 + + :param member: 成员对象 + :param old_position: 原位置 + :param new_position: 新位置 + """ + # 从旧位置移除 + self.remove_member(member, old_position) + # 添加到新位置 + self.add_member(member, new_position) + + def get_neighbors(self, position: Vec3, radius: float) -> List[Dict]: + """ + 获取指定位置周围的邻居 + + :param position: 中心位置 + :param radius: 搜索半径 + :return: 邻居列表 + """ + neighbors = [] + + # 计算需要检查的网格格子范围 + cells_to_check = int(radius / self.cell_size) + 1 + + center_coords = self.get_grid_coords(position) + + # 检查中心格子及其邻居 + for offset in self.neighbor_offsets: + grid_x = center_coords[0] + offset[0] + grid_y = center_coords[1] + offset[1] + grid_z = center_coords[2] + offset[2] + + # 检查格子是否在有效范围内 + if (0 <= grid_x < self.grid_width and + 0 <= grid_y < self.grid_height and + 0 <= grid_z < self.grid_depth): + + grid_key = f"{grid_x}_{grid_y}_{grid_z}" + if grid_key in self.grid: + # 检查格子中的每个成员是否在半径范围内 + for member in self.grid[grid_key]: + distance = (position - member['position']).length() + if distance <= radius and member not in neighbors: + neighbors.append(member) + + return neighbors + + def clear(self): + """ + 清空网格 + """ + self.grid.clear() + +class PhysicsSimulator: + """ + 物理模拟器,提供更真实的物理行为 + """ + + def __init__(self): + self.gravity = Vec3(0, 0, -9.81) # 重力加速度 + self.air_resistance = 0.01 # 空气阻力系数 + self.fluid_density = 1.225 # 空气密度 (kg/m^3) + + def apply_gravity(self, member: Dict, dt: float): + """ + 应用重力 + """ + if 'mass' in member: + mass = member['mass'] + else: + mass = 1.0 # 默认质量 + + gravity_force = self.gravity * mass + member['velocity'] += gravity_force * dt / mass + + def apply_air_resistance(self, member: Dict, dt: float): + """ + 应用空气阻力 + """ + velocity = member['velocity'] + speed = velocity.length() + + if speed > 0: + # 空气阻力与速度平方成正比 + drag_force_magnitude = 0.5 * self.fluid_density * speed * speed * 0.01 # 简化的阻力系数 + drag_force = -velocity.normalized() * drag_force_magnitude + + # 应用阻力 + member['velocity'] += drag_force * dt + + def apply_buoyancy(self, member: Dict, fluid_level: float, dt: float): + """ + 应用浮力(适用于模拟水中生物) + """ + if member['position'].z < fluid_level: + # 计算浮力(简化模型) + buoyancy_force = Vec3(0, 0, 0.5) # 向上的浮力 + member['velocity'] += buoyancy_force * dt + + def update_physics(self, member: Dict, dt: float = 1.0/60.0): + """ + 更新物理状态 + """ + # 应用重力 + self.apply_gravity(member, dt) + + # 应用空气阻力 + self.apply_air_resistance(member, dt) + +class AdvancedBoidsAlgorithm: + """ + 高级Boids算法实现类 + 包含空间分区优化、物理模拟和高级行为规则 + """ + + def __init__(self, config): + self.config = config + + # 初始化空间分区系统 + bounds = { + 'min_x': -100, 'max_x': 100, + 'min_y': -100, 'max_y': 100, + 'min_z': -10, 'max_z': 100 + } + cell_size = self.config.get('perception_radius', 10.0) # 使用感知半径作为网格大小 + self.spatial_partition = SpatialPartition(bounds, cell_size) + + # 初始化物理模拟器 + self.physics_simulator = PhysicsSimulator() + + # 用于记录性能统计 + self.stats = { + 'neighbor_lookups': 0, + 'total_neighbors_found': 0, + 'algorithm_calls': 0 + } + + def calculate_cohesion(self, member: Dict, neighbors: List[Dict]) -> Vec3: + """ + 计算聚集力 + 个体向邻居群集中心移动 + """ + if not neighbors: + return Vec3(0, 0, 0) + + # 计算邻居的中心位置 + center = Vec3(0, 0, 0) + for neighbor in neighbors: + center += neighbor['position'] + center /= len(neighbors) + + # 返回指向中心的向量 + return (center - member['position']).normalized() + + def calculate_separation(self, member: Dict, neighbors: List[Dict]) -> Vec3: + """ + 计算分离力 + 个体避免与邻居过于靠近 + """ + if not neighbors: + return Vec3(0, 0, 0) + + separation = Vec3(0, 0, 0) + for neighbor in neighbors: + diff = member['position'] - neighbor['position'] + distance = diff.length() + if distance > 0: # 避免除零 + # 距离越近,分离力越大 + separation += diff.normalized() / distance + + if len(neighbors) > 0: + separation /= len(neighbors) + + return separation.normalized() if separation.length() > 0 else separation + + def calculate_alignment(self, member: Dict, neighbors: List[Dict]) -> Vec3: + """ + 计算对齐力 + 个体与邻居的移动方向保持一致 + """ + if not neighbors: + return Vec3(0, 0, 0) + + # 计算邻居的平均速度方向 + avg_velocity = Vec3(0, 0, 0) + for neighbor in neighbors: + avg_velocity += neighbor['velocity'] + avg_velocity /= len(neighbors) + + return avg_velocity.normalized() + + def calculate_cohesion_with_weighted_neighbors(self, member: Dict, neighbors: List[Dict]) -> Vec3: + """ + 计算加权聚集力 + 距离越近的邻居对聚集力的影响越大 + """ + if not neighbors: + return Vec3(0, 0, 0) + + weighted_center = Vec3(0, 0, 0) + total_weight = 0.0 + + for neighbor in neighbors: + distance = (member['position'] - neighbor['position']).length() + if distance > 0: + # 距离越近,权重越大 + weight = 1.0 / distance + weighted_center += neighbor['position'] * weight + total_weight += weight + + if total_weight > 0: + weighted_center /= total_weight + return (weighted_center - member['position']).normalized() + else: + return Vec3(0, 0, 0) + + def calculate_separation_with_personal_space(self, member: Dict, neighbors: List[Dict]) -> Vec3: + """ + 计算分离力,考虑个人空间 + """ + if not neighbors: + return Vec3(0, 0, 0) + + separation = Vec3(0, 0, 0) + personal_space = self.config.get("personal_space", 2.0) # 个人空间半径 + + for neighbor in neighbors: + diff = member['position'] - neighbor['position'] + distance = diff.length() + + if distance > 0 and distance < personal_space: + # 仅在邻居进入个人空间时才产生分离力 + separation += diff.normalized() * (personal_space - distance) / personal_space + + return separation.normalized() if separation.length() > 0 else separation + + def calculate_obstacle_avoidance(self, member: Dict, obstacles: List[Dict]) -> Vec3: + """ + 计算避障力 + 避开场景中的障碍物 + """ + if not obstacles: + return Vec3(0, 0, 0) + + avoidance = Vec3(0, 0, 0) + obstacle_radius = self.config.get("obstacle_radius", 5.0) + + for obstacle in obstacles: + diff = member['position'] - obstacle['position'] + distance = diff.length() + + if distance < obstacle_radius + obstacle.get('radius', 1.0): + # 距离越近,避障力越大 + force_magnitude = max(0, (obstacle_radius + obstacle.get('radius', 1.0)) - distance) + avoidance += diff.normalized() * force_magnitude + + return avoidance.normalized() if avoidance.length() > 0 else avoidance + + def calculate_seek(self, member: Dict, target: Vec3) -> Vec3: + """ + 计算寻求力 + 向目标位置移动 + """ + desired = target - member['position'] + distance = desired.length() + + if distance > 0: + desired.normalize() + # 如果启用了达到行为,当接近目标时减速 + if self.config.get("arrival_enabled", False): + slow_radius = self.config.get("arrival_slow_radius", 5.0) + if distance < slow_radius: + desired *= self.config.get("max_speed", 5.0) * (distance / slow_radius) + else: + desired *= self.config.get("max_speed", 5.0) + else: + desired *= self.config.get("max_speed", 5.0) + + # 计算转向力 + steer = desired - member['velocity'] + # 限制转向力 + max_force = self.config.get("max_force", 0.5) + if steer.length() > max_force: + steer.normalize() + steer *= max_force + + return steer + return Vec3(0, 0, 0) + + def calculate_flee(self, member: Dict, target: Vec3) -> Vec3: + """ + 计算逃离力 + 远离目标位置 + """ + desired = member['position'] - target + distance = desired.length() + + if distance > 0: + desired.normalize() + desired *= self.config.get("max_speed", 5.0) + + # 计算转向力 + steer = desired - member['velocity'] + # 限制转向力 + max_force = self.config.get("max_force", 0.5) + if steer.length() > max_force: + steer.normalize() + steer *= max_force + + return steer + return Vec3(0, 0, 0) + + def calculate_wander(self, member: Dict) -> Vec3: + """ + 计算游走力 + 产生自然的随机移动 + """ + # 获取游走参数 + wander_radius = self.config.get("wander_radius", 5.0) + wander_distance = self.config.get("wander_distance", 10.0) + wander_jitter = self.config.get("wander_jitter", 1.0) + + # 如果成员没有游走目标,创建一个 + if 'wander_target' not in member: + member['wander_target'] = Vec3( + random.uniform(-1, 1), + random.uniform(-1, 1), + random.uniform(-1, 1) + ).normalized() * wander_radius + + # 添加随机扰动 + jitter = Vec3( + random.uniform(-1, 1), + random.uniform(-1, 1), + random.uniform(-1, 1) + ).normalized() * wander_jitter + + member['wander_target'] += jitter + member['wander_target'].normalize() + member['wander_target'] *= wander_radius + + # 计算游走目标在前方的距离 + target = member['wander_target'] + Vec3(0, 0, 0) # 相对于成员前方的点 + target *= wander_distance + + # 转换到世界坐标系 + # 这里简化处理,实际应用中需要考虑成员的朝向 + desired = target - member['position'] + if desired.length() > 0: + desired.normalize() + desired *= self.config.get("max_speed", 5.0) + + # 计算转向力 + steer = desired - member['velocity'] + # 限制转向力 + max_force = self.config.get("max_force", 0.5) + if steer.length() > max_force: + steer.normalize() + steer *= max_force + + return steer + return Vec3(0, 0, 0) + + def calculate_boundaries(self, member: Dict, bounds: Dict[str, float]) -> Vec3: + """ + 计算边界力 + 将成员保持在指定区域内 + """ + if not bounds: + return Vec3(0, 0, 0) + + # 获取边界参数 + min_x, max_x = bounds['min_x'], bounds['max_x'] + min_y, max_y = bounds['min_y'], bounds['max_y'] + min_z, max_z = bounds['min_z'], bounds['max_z'] + + # 边界缓冲距离 + buffer = self.config.get("boundary_buffer", 5.0) + + # 计算边界力 + bound_force = Vec3(0, 0, 0) + + if member['position'].x < min_x + buffer: + bound_force.x = 1.0 + elif member['position'].x > max_x - buffer: + bound_force.x = -1.0 + + if member['position'].y < min_y + buffer: + bound_force.y = 1.0 + elif member['position'].y > max_y - buffer: + bound_force.y = -1.0 + + if member['position'].z < min_z + buffer: + bound_force.z = 1.0 + elif member['position'].z > max_z - buffer: + bound_force.z = -1.0 + + # 应用边界权重 + bound_force *= self.config.get("boundary_weight", 2.0) + + return bound_force + + def calculate_follow_path(self, member: Dict, path: List[Vec3]) -> Vec3: + """ + 计算路径跟随力 + 沿着预定义路径移动 + """ + if not path or len(path) < 2: + return Vec3(0, 0, 0) + + # 获取路径参数 + path_radius = self.config.get("path_radius", 3.0) + + # 找到最近的路径点 + nearest_index = 0 + nearest_distance = float('inf') + + for i, point in enumerate(path): + distance = (member['position'] - point).length() + if distance < nearest_distance: + nearest_distance = distance + nearest_index = i + + # 预测位置 + prediction = member['velocity'].normalized() + prediction *= self.config.get("prediction_distance", 10.0) + predict_pos = member['position'] + prediction + + # 找到预测位置最近的路径点 + predict_nearest_index = 0 + predict_nearest_distance = float('inf') + + for i, point in enumerate(path): + distance = (predict_pos - point).length() + if distance < predict_nearest_distance: + predict_nearest_distance = distance + predict_nearest_index = i + + # 如果偏离路径太远,直接寻求最近的路径点 + if nearest_distance > path_radius: + target = path[nearest_index] + return self.calculate_seek(member, target) + + # 否则寻求预测位置前方的路径点 + if predict_nearest_index < len(path) - 1: + target = path[predict_nearest_index + 1] + return self.calculate_seek(member, target) + + return Vec3(0, 0, 0) + + def calculate_flock_formation(self, member: Dict, neighbors: List[Dict], formation_type: str) -> Vec3: + """ + 计算队形保持力 + 维持特定的群体队形 + """ + if not neighbors: + return Vec3(0, 0, 0) + + # 根据队形类型计算期望位置 + target_position = Vec3(0, 0, 0) + + if formation_type == "V": + # V字形队形 + # 简化实现,实际应用中需要更复杂的计算 + if 'index' in member: + index = member['index'] + angle = math.radians(index * 15) # 每个成员间隔15度 + distance = 5.0 # 固定间距 + target_position = Vec3( + math.cos(angle) * distance, + math.sin(angle) * distance, + 0 + ) + elif formation_type == "line": + # 直线队形 + if 'index' in member: + index = member['index'] + spacing = 3.0 + target_position = Vec3(0, index * spacing, 0) + elif formation_type == "circle": + # 圆形队形 + if 'index' in member: + index = member['index'] + total = len(neighbors) + 1 + angle = 2 * math.pi * index / total + radius = 10.0 + target_position = Vec3( + math.cos(angle) * radius, + math.sin(angle) * radius, + 0 + ) + elif formation_type == "sphere": + # 球形队形 + if 'index' in member: + index = member['index'] + total = len(neighbors) + 1 + # 使用斐波那契螺旋法在球面上分布点 + phi = math.pi * (3 - math.sqrt(5)) # 金角 + y = 1 - (index / float(total - 1)) * 2 # y: 1 to -1 + radius = math.sqrt(1 - y * y) # 半径在y处 + theta = phi * index # 金角旋转 + x = math.cos(theta) * radius + z = math.sin(theta) * radius + target_position = Vec3(x * 10, y * 10, z * 10) # 缩放到合适大小 + + # 计算到达期望位置的力 + if target_position.length() > 0: + desired = target_position - member['position'] + if desired.length() > 0: + desired.normalize() + desired *= self.config.get("max_speed", 5.0) + + # 计算转向力 + steer = desired - member['velocity'] + # 应用队形权重 + steer *= self.config.get("formation_weight", 1.0) + # 限制转向力 + max_force = self.config.get("max_force", 0.5) + if steer.length() > max_force: + steer.normalize() + steer *= max_force + + return steer + return Vec3(0, 0, 0) + + def calculate_avoid_predator(self, member: Dict, predators: List[Dict]) -> Vec3: + """ + 计算躲避捕食者力 + 远离捕食者 + """ + if not predators: + return Vec3(0, 0, 0) + + avoidance = Vec3(0, 0, 0) + predator_radius = self.config.get("predator_radius", 15.0) + + for predator in predators: + diff = member['position'] - predator['position'] + distance = diff.length() + if distance < predator_radius: + # 距离越近,躲避力越大 + force = diff.normalized() / (distance / predator_radius) + avoidance += force + + # 应用捕食者躲避权重 + avoidance *= self.config.get("predator_avoid_weight", 3.0) + + return avoidance.normalized() if avoidance.length() > 0 else avoidance + + def calculate_cohesion_with_predator_awareness(self, member: Dict, neighbors: List[Dict], predators: List[Dict]) -> Vec3: + """ + 计算受捕食者影响的聚集力 + 在有捕食者时,成员会更紧密地聚集 + """ + if not neighbors: + return Vec3(0, 0, 0) + + # 检查是否有捕食者在附近 + has_nearby_predator = False + for predator in predators: + if (member['position'] - predator['position']).length() < self.config.get("predator_radius", 15.0): + has_nearby_predator = True + break + + # 如果有捕食者在附近,增加聚集权重 + target_factor = 1.0 + if has_nearby_predator: + target_factor = 2.0 + + # 计算邻居的中心位置 + center = Vec3(0, 0, 0) + for neighbor in neighbors: + center += neighbor['position'] + center /= len(neighbors) + + # 返回指向中心的向量 + return (center - member['position']).normalized() * target_factor + + def calculate_formation_with_obstacle_avoidance(self, member: Dict, neighbors: List[Dict], formation_type: str, obstacles: List[Dict]) -> Vec3: + """ + 计算考虑障碍物的队形保持力 + 在保持队形的同时避开障碍物 + """ + # 首先计算队形力 + formation_force = self.calculate_flock_formation(member, neighbors, formation_type) + + # 然后计算避障力 + obstacle_force = self.calculate_obstacle_avoidance(member, obstacles) + + # 结合两种力量 + combined_force = formation_force + obstacle_force * 1.5 # 避障力权重稍大 + + return combined_force.normalized() * formation_force.length() + + def update_member(self, member: Dict, neighbors: List[Dict], obstacles: List[Dict] = [], + target: Vec3 = None, bounds: Dict[str, float] = None, + path: List[Vec3] = None, predators: List[Dict] = [], + interaction_forces: Dict = None, environment_force: Vec3 = None): + """ + 更新群体成员状态 + 计算所有作用力并更新成员的位置和速度 + """ + # 记录统计信息 + self.stats['algorithm_calls'] += 1 + self.stats['total_neighbors_found'] += len(neighbors) + + # 计算基本Boids力 + cohesion_force = self.calculate_cohesion_with_weighted_neighbors(member, neighbors) + separation_force = self.calculate_separation_with_personal_space(member, neighbors) + alignment_force = self.calculate_alignment(member, neighbors) + + # 计算扩展力 + obstacle_force = self.calculate_obstacle_avoidance(member, obstacles) + bound_force = self.calculate_boundaries(member, bounds) + predator_force = self.calculate_avoid_predator(member, predators) + + # 计算目标导向力 + seek_force = Vec3(0, 0, 0) + if target: + seek_force = self.calculate_seek(member, target) + + # 计算路径跟随力 + path_force = Vec3(0, 0, 0) + if path: + path_force = self.calculate_follow_path(member, path) + + # 计算游走力 + wander_force = Vec3(0, 0, 0) + if self.config.get("wander_enabled", False): + wander_force = self.calculate_wander(member) + + # 计算受捕食者影响的聚集力 + predator_aware_cohesion = self.calculate_cohesion_with_predator_awareness(member, neighbors, predators) + + # 计算群体间交互力 + interaction_force = Vec3(0, 0, 0) + if interaction_forces: + # 合作力 + if 'cooperative_force' in interaction_forces: + interaction_force += interaction_forces['cooperative_force'] + # 竞争力 + if 'competition_force' in interaction_forces: + interaction_force += interaction_forces['competition_force'] + # 捕食力 + if 'predation_force' in interaction_forces: + interaction_force += interaction_forces['predation_force'] + # 逃避力 + if 'escape_force' in interaction_forces: + interaction_force += interaction_forces['escape_force'] + + # 应用权重 + cohesion_force *= self.config.get("cohesion_weight", 1.0) + separation_force *= self.config.get("separation_weight", 1.5) + alignment_force *= self.config.get("alignment_weight", 1.0) + obstacle_force *= self.config.get("obstacle_weight", 2.0) + seek_force *= self.config.get("seek_weight", 1.0) + bound_force *= self.config.get("boundary_weight", 2.0) + path_force *= self.config.get("path_weight", 1.0) + wander_force *= self.config.get("wander_weight", 0.5) + predator_force *= self.config.get("predator_avoid_weight", 3.0) + predator_aware_cohesion *= self.config.get("cohesion_weight", 1.0) * 0.5 # 给予一半权重 + interaction_force *= self.config.get("interaction_weight", 1.0) # 交互力权重 + + # 计算总加速度 + acceleration = ( + cohesion_force + + separation_force + + alignment_force + + obstacle_force + + seek_force + + bound_force + + path_force + + wander_force + + predator_force + + predator_aware_cohesion + + interaction_force + ) + + # 添加环境力 + if environment_force: + acceleration += environment_force * 0.1 # 环境力权重 + + # 应用加速度限制 + max_acceleration = self.config.get("max_acceleration", 1.0) + if acceleration.length() > max_acceleration: + acceleration.normalize() + acceleration *= max_acceleration + + # 应用物理模拟 + dt = 1.0 / 60.0 # 时间步长 + self.physics_simulator.update_physics(member, dt) + + # 更新速度和位置 + member['velocity'] += acceleration + + # 限制最大速度 + max_speed = self.config.get("max_speed", 5.0) + if member['velocity'].length() > max_speed: + member['velocity'].normalize() + member['velocity'] *= max_speed + + member['position'] += member['velocity'] + + # 更新成员的朝向(使其面向移动方向) + if member['velocity'].length() > 0 and 'node' in member: + # 简化实现,实际应用中可能需要更复杂的朝向计算 + member['node'].lookAt(member['position'] + member['velocity']) + + def update_swarm_parallel(self, members: List[Dict], all_members: List[Dict], + obstacles: List[Dict] = [], target: Vec3 = None, + bounds: Dict[str, float] = None, path: List[Vec3] = None, + predators: List[Dict] = [], interaction_forces: Dict = None, + environment_forces: Dict = None): + """ + 并行更新群体成员状态 + 使用多线程或多进程来并行计算每个成员的更新 + """ + if not self.config.get("spatial_partitioning", False): + # 如果没有使用空间分区,则无法高效并行计算邻居 + # 回退到串行计算 + for member in members: + neighbors = self._find_neighbors_serial(member, all_members) + member_interactions = interaction_forces.get(id(member), {}) if interaction_forces else {} + member_environment_force = environment_forces.get(id(member), Vec3(0, 0, 0)) if environment_forces else Vec3(0, 0, 0) + self.update_member(member, neighbors, obstacles, target, bounds, path, predators, member_interactions, member_environment_force) + return + + # 如果启用了空间分区,则可以使用并行计算 + use_threading = True # 对于I/O密集型任务,使用线程更好 + + # 如果需要处理大量成员,可以使用多线程 + if use_threading: + # 使用ThreadPoolExecutor进行并行计算 + num_threads = min(len(members), cpu_count()) + + # 创建线程池 + with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor: + # 提交任务 + futures = [] + for member in members: + # 获取邻居(这一步可能也需要优化) + neighbors = self.spatial_partition.get_neighbors(member['position'], + self.config.get("perception_radius", 10.0)) + member_interactions = interaction_forces.get(id(member), {}) if interaction_forces else {} + member_environment_force = environment_forces.get(id(member), Vec3(0, 0, 0)) if environment_forces else Vec3(0, 0, 0) + future = executor.submit(self._update_member_task, member, neighbors, obstacles, + target, bounds, path, predators, member_interactions, member_environment_force) + futures.append(future) + + # 等待所有任务完成 + for future in concurrent.futures.as_completed(futures): + # 获取结果(如果需要的话) + result = future.result() + else: + # 使用多进程进行并行计算 + num_processes = min(len(members), cpu_count()) + + # 准备参数 + tasks = [] + for member in members: + neighbors = self.spatial_partition.get_neighbors(member['position'], + self.config.get("perception_radius", 10.0)) + member_interactions = interaction_forces.get(id(member), {}) if interaction_forces else {} + member_environment_force = environment_forces.get(id(member), Vec3(0, 0, 0)) if environment_forces else Vec3(0, 0, 0) + task_args = (member, neighbors, obstacles, target, bounds, path, predators, member_interactions, member_environment_force) + tasks.append(task_args) + + # 使用多进程池 + with Pool(processes=num_processes) as pool: + pool.starmap(self._update_member_task, tasks) + + def _update_member_task(self, member: Dict, neighbors: List[Dict], obstacles: List[Dict], + target: Vec3, bounds: Dict[str, float], path: List[Vec3], + predators: List[Dict], interaction_forces: Dict = None, + environment_force: Vec3 = None): + """ + 用于并行任务的成员更新方法 + """ + self.update_member(member, neighbors, obstacles, target, bounds, path, predators, interaction_forces, environment_force) + + def _find_neighbors_serial(self, member: Dict, all_members: List[Dict]) -> List[Dict]: + """ + 串行查找邻居(当不使用空间分区时) + """ + neighbors = [] + perception_radius = self.config.get("perception_radius", 10.0) + + for other in all_members: + if other != member: # 排除自己 + distance = (member['position'] - other['position']).length() + if distance < perception_radius: + neighbors.append(other) + return neighbors + + def get_stats(self) -> Dict[str, int]: + """ + 获取性能统计信息 + """ + return self.stats.copy() + + def clear_stats(self): + """ + 清空性能统计信息 + """ + self.stats = { + 'neighbor_lookups': 0, + 'total_neighbors_found': 0, + 'algorithm_calls': 0 } \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/boids_algorithm.py b/plugins/user/swarm_intelligence/boids_algorithm.py index c1304fad..4f5dc6b6 100644 --- a/plugins/user/swarm_intelligence/boids_algorithm.py +++ b/plugins/user/swarm_intelligence/boids_algorithm.py @@ -1,656 +1,656 @@ -""" -Boids算法实现 -这是一个经过优化和扩展的Boids算法实现,包含空间分区优化和高级行为规则 -""" - -from panda3d.core import Vec3, Point3 -import random -import math -from typing import List, Dict, Tuple, Optional - -class SpatialPartition: - """ - 空间分区类,用于优化邻居查找算法 - 将3D空间划分为网格,每个网格格子存储位于其中的群体成员 - """ - - def __init__(self, bounds: Dict[str, float], cell_size: float): - """ - 初始化空间分区 - - :param bounds: 边界信息 {'min_x': float, 'max_x': float, 'min_y': float, 'max_y': float, 'min_z': float, 'max_z': float} - :param cell_size: 网格格子大小 - """ - self.bounds = bounds - self.cell_size = cell_size - - # 计算网格尺寸 - self.grid_width = int((bounds['max_x'] - bounds['min_x']) / cell_size) + 1 - self.grid_height = int((bounds['max_y'] - bounds['min_y']) / cell_size) + 1 - self.grid_depth = int((bounds['max_z'] - bounds['min_z']) / cell_size) + 1 - - # 创建网格 - self.grid = {} - - # 邻居格子偏移量 - self.neighbor_offsets = [ - (-1, -1, -1), (-1, -1, 0), (-1, -1, 1), - (-1, 0, -1), (-1, 0, 0), (-1, 0, 1), - (-1, 1, -1), (-1, 1, 0), (-1, 1, 1), - (0, -1, -1), (0, -1, 0), (0, -1, 1), - (0, 0, -1), (0, 0, 0), (0, 0, 1), - (0, 1, -1), (0, 1, 0), (0, 1, 1), - (1, -1, -1), (1, -1, 0), (1, -1, 1), - (1, 0, -1), (1, 0, 0), (1, 0, 1), - (1, 1, -1), (1, 1, 0), (1, 1, 1) - ] - - def get_grid_coords(self, position: Vec3) -> Tuple[int, int, int]: - """ - 获取位置对应的网格坐标 - - :param position: 3D位置 - :return: (x, y, z) 网格坐标 - """ - grid_x = int((position.x - self.bounds['min_x']) / self.cell_size) - grid_y = int((position.y - self.bounds['min_y']) / self.cell_size) - grid_z = int((position.z - self.bounds['min_z']) / self.cell_size) - - # 确保坐标在有效范围内 - grid_x = max(0, min(grid_x, self.grid_width - 1)) - grid_y = max(0, min(grid_y, self.grid_height - 1)) - grid_z = max(0, min(grid_z, self.grid_depth - 1)) - - return (grid_x, grid_y, grid_z) - - def add_member(self, member: Dict, position: Vec3): - """ - 将成员添加到网格中 - - :param member: 成员对象 - :param position: 成员位置 - """ - grid_coords = self.get_grid_coords(position) - grid_key = f"{grid_coords[0]}_{grid_coords[1]}_{grid_coords[2]}" - - if grid_key not in self.grid: - self.grid[grid_key] = [] - self.grid[grid_key].append(member) - - def remove_member(self, member: Dict, position: Vec3): - """ - 从网格中移除成员 - - :param member: 成员对象 - :param position: 成员位置 - """ - grid_coords = self.get_grid_coords(position) - grid_key = f"{grid_coords[0]}_{grid_coords[1]}_{grid_coords[2]}" - - if grid_key in self.grid: - if member in self.grid[grid_key]: - self.grid[grid_key].remove(member) - - def update_member(self, member: Dict, old_position: Vec3, new_position: Vec3): - """ - 更新成员在网格中的位置 - - :param member: 成员对象 - :param old_position: 原位置 - :param new_position: 新位置 - """ - # 从旧位置移除 - self.remove_member(member, old_position) - # 添加到新位置 - self.add_member(member, new_position) - - def get_neighbors(self, position: Vec3, radius: float) -> List[Dict]: - """ - 获取指定位置周围的邻居 - - :param position: 中心位置 - :param radius: 搜索半径 - :return: 邻居列表 - """ - neighbors = [] - - # 计算需要检查的网格格子范围 - cells_to_check = int(radius / self.cell_size) + 1 - - center_coords = self.get_grid_coords(position) - - # 检查中心格子及其邻居 - for offset in self.neighbor_offsets: - grid_x = center_coords[0] + offset[0] - grid_y = center_coords[1] + offset[1] - grid_z = center_coords[2] + offset[2] - - # 检查格子是否在有效范围内 - if (0 <= grid_x < self.grid_width and - 0 <= grid_y < self.grid_height and - 0 <= grid_z < self.grid_depth): - - grid_key = f"{grid_x}_{grid_y}_{grid_z}" - if grid_key in self.grid: - # 检查格子中的每个成员是否在半径范围内 - for member in self.grid[grid_key]: - distance = (position - member['position']).length() - if distance <= radius and member not in neighbors: - neighbors.append(member) - - return neighbors - - def clear(self): - """ - 清空网格 - """ - self.grid.clear() - -class PhysicsSimulator: - """ - 物理模拟器,提供更真实的物理行为 - """ - - def __init__(self): - self.gravity = Vec3(0, 0, -9.81) # 重力加速度 - self.air_resistance = 0.01 # 空气阻力系数 - self.fluid_density = 1.225 # 空气密度 (kg/m^3) - - def apply_gravity(self, member: Dict, dt: float): - """ - 应用重力 - """ - if 'mass' in member: - mass = member['mass'] - else: - mass = 1.0 # 默认质量 - - gravity_force = self.gravity * mass - member['velocity'] += gravity_force * dt / mass - - def apply_air_resistance(self, member: Dict, dt: float): - """ - 应用空气阻力 - """ - velocity = member['velocity'] - speed = velocity.length() - - if speed > 0: - # 空气阻力与速度平方成正比 - drag_force_magnitude = 0.5 * self.fluid_density * speed * speed * 0.01 # 简化的阻力系数 - drag_force = -velocity.normalized() * drag_force_magnitude - - # 应用阻力 - member['velocity'] += drag_force * dt - - def apply_buoyancy(self, member: Dict, fluid_level: float, dt: float): - """ - 应用浮力(适用于模拟水中生物) - """ - if member['position'].z < fluid_level: - # 计算浮力(简化模型) - buoyancy_force = Vec3(0, 0, 0.5) # 向上的浮力 - member['velocity'] += buoyancy_force * dt - - def update_physics(self, member: Dict, dt: float = 1.0/60.0): - """ - 更新物理状态 - """ - # 应用重力 - self.apply_gravity(member, dt) - - # 应用空气阻力 - self.apply_air_resistance(member, dt) - -class AdvancedBoidsAlgorithm: - """ - 高级Boids算法实现类 - 包含空间分区优化、物理模拟和高级行为规则 - """ - - def __init__(self, config): - self.config = config - - # 初始化空间分区系统 - bounds = { - 'min_x': -100, 'max_x': 100, - 'min_y': -100, 'max_y': 100, - 'min_z': -10, 'max_z': 100 - } - cell_size = self.config.get('perception_radius', 10.0) # 使用感知半径作为网格大小 - self.spatial_partition = SpatialPartition(bounds, cell_size) - - # 初始化物理模拟器 - self.physics_simulator = PhysicsSimulator() - - # 用于记录性能统计 - self.stats = { - 'neighbor_lookups': 0, - 'total_neighbors_found': 0, - 'algorithm_calls': 0 - } - - def calculate_cohesion(self, member, neighbors): - """ - 计算聚集力 - 个体向邻居群集中心移动 - """ - if not neighbors: - return Vec3(0, 0, 0) - - # 计算邻居的中心位置 - center = Vec3(0, 0, 0) - for neighbor in neighbors: - center += neighbor['position'] - center /= len(neighbors) - - # 返回指向中心的向量 - return (center - member['position']).normalized() - - def calculate_separation(self, member, neighbors): - """ - 计算分离力 - 个体避免与邻居过于靠近 - """ - if not neighbors: - return Vec3(0, 0, 0) - - separation = Vec3(0, 0, 0) - for neighbor in neighbors: - diff = member['position'] - neighbor['position'] - distance = diff.length() - if distance > 0: # 避免除零 - # 距离越近,分离力越大 - separation += diff.normalized() / distance - - if len(neighbors) > 0: - separation /= len(neighbors) - - return separation.normalized() if separation.length() > 0 else separation - - def calculate_alignment(self, member, neighbors): - """ - 计算对齐力 - 个体与邻居的移动方向保持一致 - """ - if not neighbors: - return Vec3(0, 0, 0) - - # 计算邻居的平均速度方向 - avg_velocity = Vec3(0, 0, 0) - for neighbor in neighbors: - avg_velocity += neighbor['velocity'] - avg_velocity /= len(neighbors) - - return avg_velocity.normalized() - - def calculate_obstacle_avoidance(self, member, obstacles): - """ - 计算避障力 - 避开场景中的障碍物 - """ - if not obstacles: - return Vec3(0, 0, 0) - - avoidance = Vec3(0, 0, 0) - for obstacle in obstacles: - diff = member['position'] - obstacle['position'] - distance = diff.length() - if distance < obstacle['radius']: - avoidance += diff.normalized() / distance - - return avoidance.normalized() if avoidance.length() > 0 else avoidance - - def calculate_seek(self, member, target): - """ - 计算寻求力 - 向目标位置移动 - """ - desired = target - member['position'] - distance = desired.length() - - if distance > 0: - desired.normalize() - # 如果启用了达到行为,当接近目标时减速 - if self.config.get("arrival_enabled", False): - slow_radius = self.config.get("arrival_slow_radius", 5.0) - if distance < slow_radius: - desired *= self.config.get("max_speed", 5.0) * (distance / slow_radius) - else: - desired *= self.config.get("max_speed", 5.0) - else: - desired *= self.config.get("max_speed", 5.0) - - # 计算转向力 - steer = desired - member['velocity'] - # 限制转向力 - max_force = self.config.get("max_force", 0.5) - if steer.length() > max_force: - steer.normalize() - steer *= max_force - - return steer - return Vec3(0, 0, 0) - - def calculate_flee(self, member, target): - """ - 计算逃离力 - 远离目标位置 - """ - desired = member['position'] - target - distance = desired.length() - - if distance > 0: - desired.normalize() - desired *= self.config.get("max_speed", 5.0) - - # 计算转向力 - steer = desired - member['velocity'] - # 限制转向力 - max_force = self.config.get("max_force", 0.5) - if steer.length() > max_force: - steer.normalize() - steer *= max_force - - return steer - return Vec3(0, 0, 0) - - def calculate_wander(self, member): - """ - 计算游走力 - 产生自然的随机移动 - """ - # 获取游走参数 - wander_radius = self.config.get("wander_radius", 5.0) - wander_distance = self.config.get("wander_distance", 10.0) - wander_jitter = self.config.get("wander_jitter", 1.0) - - # 如果成员没有游走目标,创建一个 - if 'wander_target' not in member: - member['wander_target'] = Vec3( - random.uniform(-1, 1), - random.uniform(-1, 1), - random.uniform(-1, 1) - ).normalized() * wander_radius - - # 添加随机扰动 - jitter = Vec3( - random.uniform(-1, 1), - random.uniform(-1, 1), - random.uniform(-1, 1) - ).normalized() * wander_jitter - - member['wander_target'] += jitter - member['wander_target'].normalize() - member['wander_target'] *= wander_radius - - # 计算游走目标在前方的距离 - target = member['wander_target'] + Vec3(0, 0, 0) # 相对于成员前方的点 - target *= wander_distance - - # 转换到世界坐标系 - # 这里简化处理,实际应用中需要考虑成员的朝向 - desired = target - member['position'] - if desired.length() > 0: - desired.normalize() - desired *= self.config.get("max_speed", 5.0) - - # 计算转向力 - steer = desired - member['velocity'] - # 限制转向力 - max_force = self.config.get("max_force", 0.5) - if steer.length() > max_force: - steer.normalize() - steer *= max_force - - return steer - return Vec3(0, 0, 0) - - def calculate_boundaries(self, member, bounds): - """ - 计算边界力 - 将成员保持在指定区域内 - """ - if not bounds: - return Vec3(0, 0, 0) - - # 获取边界参数 - min_x, max_x = bounds['min_x'], bounds['max_x'] - min_y, max_y = bounds['min_y'], bounds['max_y'] - min_z, max_z = bounds['min_z'], bounds['max_z'] - - # 边界缓冲距离 - buffer = self.config.get("boundary_buffer", 5.0) - - # 计算边界力 - bound_force = Vec3(0, 0, 0) - - if member['position'].x < min_x + buffer: - bound_force.x = 1.0 - elif member['position'].x > max_x - buffer: - bound_force.x = -1.0 - - if member['position'].y < min_y + buffer: - bound_force.y = 1.0 - elif member['position'].y > max_y - buffer: - bound_force.y = -1.0 - - if member['position'].z < min_z + buffer: - bound_force.z = 1.0 - elif member['position'].z > max_z - buffer: - bound_force.z = -1.0 - - # 应用边界权重 - bound_force *= self.config.get("boundary_weight", 2.0) - - return bound_force - - def calculate_follow_path(self, member, path): - """ - 计算路径跟随力 - 沿着预定义路径移动 - """ - if not path or len(path) < 2: - return Vec3(0, 0, 0) - - # 获取路径参数 - path_radius = self.config.get("path_radius", 3.0) - - # 找到最近的路径点 - nearest_index = 0 - nearest_distance = float('inf') - - for i, point in enumerate(path): - distance = (member['position'] - point).length() - if distance < nearest_distance: - nearest_distance = distance - nearest_index = i - - # 预测位置 - prediction = member['velocity'].normalized() - prediction *= self.config.get("prediction_distance", 10.0) - predict_pos = member['position'] + prediction - - # 找到预测位置最近的路径点 - predict_nearest_index = 0 - predict_nearest_distance = float('inf') - - for i, point in enumerate(path): - distance = (predict_pos - point).length() - if distance < predict_nearest_distance: - predict_nearest_distance = distance - predict_nearest_index = i - - # 如果偏离路径太远,直接寻求最近的路径点 - if nearest_distance > path_radius: - target = path[nearest_index] - return self.calculate_seek(member, target) - - # 否则寻求预测位置前方的路径点 - if predict_nearest_index < len(path) - 1: - target = path[predict_nearest_index + 1] - return self.calculate_seek(member, target) - - return Vec3(0, 0, 0) - - def calculate_flock_formation(self, member, neighbors, formation_type): - """ - 计算队形保持力 - 维持特定的群体队形 - """ - if not neighbors: - return Vec3(0, 0, 0) - - # 根据队形类型计算期望位置 - target_position = Vec3(0, 0, 0) - - if formation_type == "V": - # V字形队形 - # 简化实现,实际应用中需要更复杂的计算 - if 'index' in member: - index = member['index'] - angle = math.radians(index * 15) # 每个成员间隔15度 - distance = 5.0 # 固定间距 - target_position = Vec3( - math.cos(angle) * distance, - math.sin(angle) * distance, - 0 - ) - elif formation_type == "line": - # 直线队形 - if 'index' in member: - index = member['index'] - spacing = 3.0 - target_position = Vec3(0, index * spacing, 0) - elif formation_type == "circle": - # 圆形队形 - if 'index' in member: - index = member['index'] - total = len(neighbors) + 1 - angle = 2 * math.pi * index / total - radius = 10.0 - target_position = Vec3( - math.cos(angle) * radius, - math.sin(angle) * radius, - 0 - ) - - # 计算到达期望位置的力 - if target_position.length() > 0: - desired = target_position - member['position'] - if desired.length() > 0: - desired.normalize() - desired *= self.config.get("max_speed", 5.0) - - # 计算转向力 - steer = desired - member['velocity'] - # 应用队形权重 - steer *= self.config.get("formation_weight", 1.0) - # 限制转向力 - max_force = self.config.get("max_force", 0.5) - if steer.length() > max_force: - steer.normalize() - steer *= max_force - - return steer - return Vec3(0, 0, 0) - - def calculate_avoid_predator(self, member, predators): - """ - 计算躲避捕食者力 - 远离捕食者 - """ - if not predators: - return Vec3(0, 0, 0) - - avoidance = Vec3(0, 0, 0) - predator_radius = self.config.get("predator_radius", 15.0) - - for predator in predators: - diff = member['position'] - predator['position'] - distance = diff.length() - if distance < predator_radius: - # 距离越近,躲避力越大 - force = diff.normalized() / (distance / predator_radius) - avoidance += force - - # 应用捕食者躲避权重 - avoidance *= self.config.get("predator_avoid_weight", 3.0) - - return avoidance.normalized() if avoidance.length() > 0 else avoidance - - def update_member(self, member, neighbors, obstacles=[], target=None, bounds=None, path=None, predators=[]): - """ - 更新群体成员状态 - 计算所有作用力并更新成员的位置和速度 - """ - # 计算基本Boids力 - cohesion_force = self.calculate_cohesion(member, neighbors) - separation_force = self.calculate_separation(member, neighbors) - alignment_force = self.calculate_alignment(member, neighbors) - - # 计算扩展力 - obstacle_force = self.calculate_obstacle_avoidance(member, obstacles) - bound_force = self.calculate_boundaries(member, bounds) - predator_force = self.calculate_avoid_predator(member, predators) - - # 计算目标导向力 - seek_force = Vec3(0, 0, 0) - if target: - seek_force = self.calculate_seek(member, target) - - # 计算路径跟随力 - path_force = Vec3(0, 0, 0) - if path: - path_force = self.calculate_follow_path(member, path) - - # 计算游走力 - wander_force = Vec3(0, 0, 0) - if self.config.get("wander_enabled", False): - wander_force = self.calculate_wander(member) - - # 应用权重 - cohesion_force *= self.config.get("cohesion_weight", 1.0) - separation_force *= self.config.get("separation_weight", 1.5) - alignment_force *= self.config.get("alignment_weight", 1.0) - obstacle_force *= self.config.get("obstacle_weight", 2.0) - seek_force *= self.config.get("seek_weight", 1.0) - bound_force *= self.config.get("boundary_weight", 2.0) - path_force *= self.config.get("path_weight", 1.0) - wander_force *= self.config.get("wander_weight", 0.5) - predator_force *= self.config.get("predator_avoid_weight", 3.0) - - # 计算总加速度 - acceleration = ( - cohesion_force + - separation_force + - alignment_force + - obstacle_force + - seek_force + - bound_force + - path_force + - wander_force + - predator_force - ) - - # 应用加速度限制 - max_acceleration = self.config.get("max_acceleration", 1.0) - if acceleration.length() > max_acceleration: - acceleration.normalize() - acceleration *= max_acceleration - - # 更新速度和位置 - member['velocity'] += acceleration - - # 限制最大速度 - max_speed = self.config.get("max_speed", 5.0) - if member['velocity'].length() > max_speed: - member['velocity'].normalize() - member['velocity'] *= max_speed - - member['position'] += member['velocity'] - - # 更新成员的朝向(使其面向移动方向) - if member['velocity'].length() > 0 and 'node' in member: - # 简化实现,实际应用中可能需要更复杂的朝向计算 +""" +Boids算法实现 +这是一个经过优化和扩展的Boids算法实现,包含空间分区优化和高级行为规则 +""" + +from panda3d.core import Vec3, Point3 +import random +import math +from typing import List, Dict, Tuple, Optional + +class SpatialPartition: + """ + 空间分区类,用于优化邻居查找算法 + 将3D空间划分为网格,每个网格格子存储位于其中的群体成员 + """ + + def __init__(self, bounds: Dict[str, float], cell_size: float): + """ + 初始化空间分区 + + :param bounds: 边界信息 {'min_x': float, 'max_x': float, 'min_y': float, 'max_y': float, 'min_z': float, 'max_z': float} + :param cell_size: 网格格子大小 + """ + self.bounds = bounds + self.cell_size = cell_size + + # 计算网格尺寸 + self.grid_width = int((bounds['max_x'] - bounds['min_x']) / cell_size) + 1 + self.grid_height = int((bounds['max_y'] - bounds['min_y']) / cell_size) + 1 + self.grid_depth = int((bounds['max_z'] - bounds['min_z']) / cell_size) + 1 + + # 创建网格 + self.grid = {} + + # 邻居格子偏移量 + self.neighbor_offsets = [ + (-1, -1, -1), (-1, -1, 0), (-1, -1, 1), + (-1, 0, -1), (-1, 0, 0), (-1, 0, 1), + (-1, 1, -1), (-1, 1, 0), (-1, 1, 1), + (0, -1, -1), (0, -1, 0), (0, -1, 1), + (0, 0, -1), (0, 0, 0), (0, 0, 1), + (0, 1, -1), (0, 1, 0), (0, 1, 1), + (1, -1, -1), (1, -1, 0), (1, -1, 1), + (1, 0, -1), (1, 0, 0), (1, 0, 1), + (1, 1, -1), (1, 1, 0), (1, 1, 1) + ] + + def get_grid_coords(self, position: Vec3) -> Tuple[int, int, int]: + """ + 获取位置对应的网格坐标 + + :param position: 3D位置 + :return: (x, y, z) 网格坐标 + """ + grid_x = int((position.x - self.bounds['min_x']) / self.cell_size) + grid_y = int((position.y - self.bounds['min_y']) / self.cell_size) + grid_z = int((position.z - self.bounds['min_z']) / self.cell_size) + + # 确保坐标在有效范围内 + grid_x = max(0, min(grid_x, self.grid_width - 1)) + grid_y = max(0, min(grid_y, self.grid_height - 1)) + grid_z = max(0, min(grid_z, self.grid_depth - 1)) + + return (grid_x, grid_y, grid_z) + + def add_member(self, member: Dict, position: Vec3): + """ + 将成员添加到网格中 + + :param member: 成员对象 + :param position: 成员位置 + """ + grid_coords = self.get_grid_coords(position) + grid_key = f"{grid_coords[0]}_{grid_coords[1]}_{grid_coords[2]}" + + if grid_key not in self.grid: + self.grid[grid_key] = [] + self.grid[grid_key].append(member) + + def remove_member(self, member: Dict, position: Vec3): + """ + 从网格中移除成员 + + :param member: 成员对象 + :param position: 成员位置 + """ + grid_coords = self.get_grid_coords(position) + grid_key = f"{grid_coords[0]}_{grid_coords[1]}_{grid_coords[2]}" + + if grid_key in self.grid: + if member in self.grid[grid_key]: + self.grid[grid_key].remove(member) + + def update_member(self, member: Dict, old_position: Vec3, new_position: Vec3): + """ + 更新成员在网格中的位置 + + :param member: 成员对象 + :param old_position: 原位置 + :param new_position: 新位置 + """ + # 从旧位置移除 + self.remove_member(member, old_position) + # 添加到新位置 + self.add_member(member, new_position) + + def get_neighbors(self, position: Vec3, radius: float) -> List[Dict]: + """ + 获取指定位置周围的邻居 + + :param position: 中心位置 + :param radius: 搜索半径 + :return: 邻居列表 + """ + neighbors = [] + + # 计算需要检查的网格格子范围 + cells_to_check = int(radius / self.cell_size) + 1 + + center_coords = self.get_grid_coords(position) + + # 检查中心格子及其邻居 + for offset in self.neighbor_offsets: + grid_x = center_coords[0] + offset[0] + grid_y = center_coords[1] + offset[1] + grid_z = center_coords[2] + offset[2] + + # 检查格子是否在有效范围内 + if (0 <= grid_x < self.grid_width and + 0 <= grid_y < self.grid_height and + 0 <= grid_z < self.grid_depth): + + grid_key = f"{grid_x}_{grid_y}_{grid_z}" + if grid_key in self.grid: + # 检查格子中的每个成员是否在半径范围内 + for member in self.grid[grid_key]: + distance = (position - member['position']).length() + if distance <= radius and member not in neighbors: + neighbors.append(member) + + return neighbors + + def clear(self): + """ + 清空网格 + """ + self.grid.clear() + +class PhysicsSimulator: + """ + 物理模拟器,提供更真实的物理行为 + """ + + def __init__(self): + self.gravity = Vec3(0, 0, -9.81) # 重力加速度 + self.air_resistance = 0.01 # 空气阻力系数 + self.fluid_density = 1.225 # 空气密度 (kg/m^3) + + def apply_gravity(self, member: Dict, dt: float): + """ + 应用重力 + """ + if 'mass' in member: + mass = member['mass'] + else: + mass = 1.0 # 默认质量 + + gravity_force = self.gravity * mass + member['velocity'] += gravity_force * dt / mass + + def apply_air_resistance(self, member: Dict, dt: float): + """ + 应用空气阻力 + """ + velocity = member['velocity'] + speed = velocity.length() + + if speed > 0: + # 空气阻力与速度平方成正比 + drag_force_magnitude = 0.5 * self.fluid_density * speed * speed * 0.01 # 简化的阻力系数 + drag_force = -velocity.normalized() * drag_force_magnitude + + # 应用阻力 + member['velocity'] += drag_force * dt + + def apply_buoyancy(self, member: Dict, fluid_level: float, dt: float): + """ + 应用浮力(适用于模拟水中生物) + """ + if member['position'].z < fluid_level: + # 计算浮力(简化模型) + buoyancy_force = Vec3(0, 0, 0.5) # 向上的浮力 + member['velocity'] += buoyancy_force * dt + + def update_physics(self, member: Dict, dt: float = 1.0/60.0): + """ + 更新物理状态 + """ + # 应用重力 + self.apply_gravity(member, dt) + + # 应用空气阻力 + self.apply_air_resistance(member, dt) + +class AdvancedBoidsAlgorithm: + """ + 高级Boids算法实现类 + 包含空间分区优化、物理模拟和高级行为规则 + """ + + def __init__(self, config): + self.config = config + + # 初始化空间分区系统 + bounds = { + 'min_x': -100, 'max_x': 100, + 'min_y': -100, 'max_y': 100, + 'min_z': -10, 'max_z': 100 + } + cell_size = self.config.get('perception_radius', 10.0) # 使用感知半径作为网格大小 + self.spatial_partition = SpatialPartition(bounds, cell_size) + + # 初始化物理模拟器 + self.physics_simulator = PhysicsSimulator() + + # 用于记录性能统计 + self.stats = { + 'neighbor_lookups': 0, + 'total_neighbors_found': 0, + 'algorithm_calls': 0 + } + + def calculate_cohesion(self, member, neighbors): + """ + 计算聚集力 + 个体向邻居群集中心移动 + """ + if not neighbors: + return Vec3(0, 0, 0) + + # 计算邻居的中心位置 + center = Vec3(0, 0, 0) + for neighbor in neighbors: + center += neighbor['position'] + center /= len(neighbors) + + # 返回指向中心的向量 + return (center - member['position']).normalized() + + def calculate_separation(self, member, neighbors): + """ + 计算分离力 + 个体避免与邻居过于靠近 + """ + if not neighbors: + return Vec3(0, 0, 0) + + separation = Vec3(0, 0, 0) + for neighbor in neighbors: + diff = member['position'] - neighbor['position'] + distance = diff.length() + if distance > 0: # 避免除零 + # 距离越近,分离力越大 + separation += diff.normalized() / distance + + if len(neighbors) > 0: + separation /= len(neighbors) + + return separation.normalized() if separation.length() > 0 else separation + + def calculate_alignment(self, member, neighbors): + """ + 计算对齐力 + 个体与邻居的移动方向保持一致 + """ + if not neighbors: + return Vec3(0, 0, 0) + + # 计算邻居的平均速度方向 + avg_velocity = Vec3(0, 0, 0) + for neighbor in neighbors: + avg_velocity += neighbor['velocity'] + avg_velocity /= len(neighbors) + + return avg_velocity.normalized() + + def calculate_obstacle_avoidance(self, member, obstacles): + """ + 计算避障力 + 避开场景中的障碍物 + """ + if not obstacles: + return Vec3(0, 0, 0) + + avoidance = Vec3(0, 0, 0) + for obstacle in obstacles: + diff = member['position'] - obstacle['position'] + distance = diff.length() + if distance < obstacle['radius']: + avoidance += diff.normalized() / distance + + return avoidance.normalized() if avoidance.length() > 0 else avoidance + + def calculate_seek(self, member, target): + """ + 计算寻求力 + 向目标位置移动 + """ + desired = target - member['position'] + distance = desired.length() + + if distance > 0: + desired.normalize() + # 如果启用了达到行为,当接近目标时减速 + if self.config.get("arrival_enabled", False): + slow_radius = self.config.get("arrival_slow_radius", 5.0) + if distance < slow_radius: + desired *= self.config.get("max_speed", 5.0) * (distance / slow_radius) + else: + desired *= self.config.get("max_speed", 5.0) + else: + desired *= self.config.get("max_speed", 5.0) + + # 计算转向力 + steer = desired - member['velocity'] + # 限制转向力 + max_force = self.config.get("max_force", 0.5) + if steer.length() > max_force: + steer.normalize() + steer *= max_force + + return steer + return Vec3(0, 0, 0) + + def calculate_flee(self, member, target): + """ + 计算逃离力 + 远离目标位置 + """ + desired = member['position'] - target + distance = desired.length() + + if distance > 0: + desired.normalize() + desired *= self.config.get("max_speed", 5.0) + + # 计算转向力 + steer = desired - member['velocity'] + # 限制转向力 + max_force = self.config.get("max_force", 0.5) + if steer.length() > max_force: + steer.normalize() + steer *= max_force + + return steer + return Vec3(0, 0, 0) + + def calculate_wander(self, member): + """ + 计算游走力 + 产生自然的随机移动 + """ + # 获取游走参数 + wander_radius = self.config.get("wander_radius", 5.0) + wander_distance = self.config.get("wander_distance", 10.0) + wander_jitter = self.config.get("wander_jitter", 1.0) + + # 如果成员没有游走目标,创建一个 + if 'wander_target' not in member: + member['wander_target'] = Vec3( + random.uniform(-1, 1), + random.uniform(-1, 1), + random.uniform(-1, 1) + ).normalized() * wander_radius + + # 添加随机扰动 + jitter = Vec3( + random.uniform(-1, 1), + random.uniform(-1, 1), + random.uniform(-1, 1) + ).normalized() * wander_jitter + + member['wander_target'] += jitter + member['wander_target'].normalize() + member['wander_target'] *= wander_radius + + # 计算游走目标在前方的距离 + target = member['wander_target'] + Vec3(0, 0, 0) # 相对于成员前方的点 + target *= wander_distance + + # 转换到世界坐标系 + # 这里简化处理,实际应用中需要考虑成员的朝向 + desired = target - member['position'] + if desired.length() > 0: + desired.normalize() + desired *= self.config.get("max_speed", 5.0) + + # 计算转向力 + steer = desired - member['velocity'] + # 限制转向力 + max_force = self.config.get("max_force", 0.5) + if steer.length() > max_force: + steer.normalize() + steer *= max_force + + return steer + return Vec3(0, 0, 0) + + def calculate_boundaries(self, member, bounds): + """ + 计算边界力 + 将成员保持在指定区域内 + """ + if not bounds: + return Vec3(0, 0, 0) + + # 获取边界参数 + min_x, max_x = bounds['min_x'], bounds['max_x'] + min_y, max_y = bounds['min_y'], bounds['max_y'] + min_z, max_z = bounds['min_z'], bounds['max_z'] + + # 边界缓冲距离 + buffer = self.config.get("boundary_buffer", 5.0) + + # 计算边界力 + bound_force = Vec3(0, 0, 0) + + if member['position'].x < min_x + buffer: + bound_force.x = 1.0 + elif member['position'].x > max_x - buffer: + bound_force.x = -1.0 + + if member['position'].y < min_y + buffer: + bound_force.y = 1.0 + elif member['position'].y > max_y - buffer: + bound_force.y = -1.0 + + if member['position'].z < min_z + buffer: + bound_force.z = 1.0 + elif member['position'].z > max_z - buffer: + bound_force.z = -1.0 + + # 应用边界权重 + bound_force *= self.config.get("boundary_weight", 2.0) + + return bound_force + + def calculate_follow_path(self, member, path): + """ + 计算路径跟随力 + 沿着预定义路径移动 + """ + if not path or len(path) < 2: + return Vec3(0, 0, 0) + + # 获取路径参数 + path_radius = self.config.get("path_radius", 3.0) + + # 找到最近的路径点 + nearest_index = 0 + nearest_distance = float('inf') + + for i, point in enumerate(path): + distance = (member['position'] - point).length() + if distance < nearest_distance: + nearest_distance = distance + nearest_index = i + + # 预测位置 + prediction = member['velocity'].normalized() + prediction *= self.config.get("prediction_distance", 10.0) + predict_pos = member['position'] + prediction + + # 找到预测位置最近的路径点 + predict_nearest_index = 0 + predict_nearest_distance = float('inf') + + for i, point in enumerate(path): + distance = (predict_pos - point).length() + if distance < predict_nearest_distance: + predict_nearest_distance = distance + predict_nearest_index = i + + # 如果偏离路径太远,直接寻求最近的路径点 + if nearest_distance > path_radius: + target = path[nearest_index] + return self.calculate_seek(member, target) + + # 否则寻求预测位置前方的路径点 + if predict_nearest_index < len(path) - 1: + target = path[predict_nearest_index + 1] + return self.calculate_seek(member, target) + + return Vec3(0, 0, 0) + + def calculate_flock_formation(self, member, neighbors, formation_type): + """ + 计算队形保持力 + 维持特定的群体队形 + """ + if not neighbors: + return Vec3(0, 0, 0) + + # 根据队形类型计算期望位置 + target_position = Vec3(0, 0, 0) + + if formation_type == "V": + # V字形队形 + # 简化实现,实际应用中需要更复杂的计算 + if 'index' in member: + index = member['index'] + angle = math.radians(index * 15) # 每个成员间隔15度 + distance = 5.0 # 固定间距 + target_position = Vec3( + math.cos(angle) * distance, + math.sin(angle) * distance, + 0 + ) + elif formation_type == "line": + # 直线队形 + if 'index' in member: + index = member['index'] + spacing = 3.0 + target_position = Vec3(0, index * spacing, 0) + elif formation_type == "circle": + # 圆形队形 + if 'index' in member: + index = member['index'] + total = len(neighbors) + 1 + angle = 2 * math.pi * index / total + radius = 10.0 + target_position = Vec3( + math.cos(angle) * radius, + math.sin(angle) * radius, + 0 + ) + + # 计算到达期望位置的力 + if target_position.length() > 0: + desired = target_position - member['position'] + if desired.length() > 0: + desired.normalize() + desired *= self.config.get("max_speed", 5.0) + + # 计算转向力 + steer = desired - member['velocity'] + # 应用队形权重 + steer *= self.config.get("formation_weight", 1.0) + # 限制转向力 + max_force = self.config.get("max_force", 0.5) + if steer.length() > max_force: + steer.normalize() + steer *= max_force + + return steer + return Vec3(0, 0, 0) + + def calculate_avoid_predator(self, member, predators): + """ + 计算躲避捕食者力 + 远离捕食者 + """ + if not predators: + return Vec3(0, 0, 0) + + avoidance = Vec3(0, 0, 0) + predator_radius = self.config.get("predator_radius", 15.0) + + for predator in predators: + diff = member['position'] - predator['position'] + distance = diff.length() + if distance < predator_radius: + # 距离越近,躲避力越大 + force = diff.normalized() / (distance / predator_radius) + avoidance += force + + # 应用捕食者躲避权重 + avoidance *= self.config.get("predator_avoid_weight", 3.0) + + return avoidance.normalized() if avoidance.length() > 0 else avoidance + + def update_member(self, member, neighbors, obstacles=[], target=None, bounds=None, path=None, predators=[]): + """ + 更新群体成员状态 + 计算所有作用力并更新成员的位置和速度 + """ + # 计算基本Boids力 + cohesion_force = self.calculate_cohesion(member, neighbors) + separation_force = self.calculate_separation(member, neighbors) + alignment_force = self.calculate_alignment(member, neighbors) + + # 计算扩展力 + obstacle_force = self.calculate_obstacle_avoidance(member, obstacles) + bound_force = self.calculate_boundaries(member, bounds) + predator_force = self.calculate_avoid_predator(member, predators) + + # 计算目标导向力 + seek_force = Vec3(0, 0, 0) + if target: + seek_force = self.calculate_seek(member, target) + + # 计算路径跟随力 + path_force = Vec3(0, 0, 0) + if path: + path_force = self.calculate_follow_path(member, path) + + # 计算游走力 + wander_force = Vec3(0, 0, 0) + if self.config.get("wander_enabled", False): + wander_force = self.calculate_wander(member) + + # 应用权重 + cohesion_force *= self.config.get("cohesion_weight", 1.0) + separation_force *= self.config.get("separation_weight", 1.5) + alignment_force *= self.config.get("alignment_weight", 1.0) + obstacle_force *= self.config.get("obstacle_weight", 2.0) + seek_force *= self.config.get("seek_weight", 1.0) + bound_force *= self.config.get("boundary_weight", 2.0) + path_force *= self.config.get("path_weight", 1.0) + wander_force *= self.config.get("wander_weight", 0.5) + predator_force *= self.config.get("predator_avoid_weight", 3.0) + + # 计算总加速度 + acceleration = ( + cohesion_force + + separation_force + + alignment_force + + obstacle_force + + seek_force + + bound_force + + path_force + + wander_force + + predator_force + ) + + # 应用加速度限制 + max_acceleration = self.config.get("max_acceleration", 1.0) + if acceleration.length() > max_acceleration: + acceleration.normalize() + acceleration *= max_acceleration + + # 更新速度和位置 + member['velocity'] += acceleration + + # 限制最大速度 + max_speed = self.config.get("max_speed", 5.0) + if member['velocity'].length() > max_speed: + member['velocity'].normalize() + member['velocity'] *= max_speed + + member['position'] += member['velocity'] + + # 更新成员的朝向(使其面向移动方向) + if member['velocity'].length() > 0 and 'node' in member: + # 简化实现,实际应用中可能需要更复杂的朝向计算 member['node'].lookAt(member['position'] + member['velocity']) \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/config.py b/plugins/user/swarm_intelligence/config.py index 7a11bd43..2b4eaf6a 100644 --- a/plugins/user/swarm_intelligence/config.py +++ b/plugins/user/swarm_intelligence/config.py @@ -1,351 +1,351 @@ -""" -群体智能插件配置 -提供完整的配置管理功能,支持多种数据类型和持久化存储 -""" - -import json -import os -from pathlib import Path - -class Config: - """ - 配置管理类 - 支持配置的加载、保存、验证和管理 - """ - - def __init__(self, config_file=None): - # 配置文件路径 - self.config_file = config_file or self._get_default_config_path() - - # 默认配置 - self.default_settings = { - # 基本Boids参数 - "cohesion_weight": 1.0, - "separation_weight": 1.5, - "alignment_weight": 1.0, - "perception_radius": 10.0, - - # 移动参数 - "max_speed": 5.0, - "max_acceleration": 1.0, - "max_force": 0.5, - - # 群体参数 - "member_count": 20, - "swarm_type": "鸟类", - - # 行为开关 - "seek_enabled": False, - "obstacle_avoidance_enabled": True, - "wander_enabled": False, - "boundary_enabled": True, - "predator_avoid_enabled": False, - "arrival_enabled": False, - "formation_enabled": False, - "path_following_enabled": False, - "interaction_enabled": True, # 群体间交互开关 - "learning_enabled": False, # 学习能力开关 - "environment_awareness_enabled": True, # 环境感知开关 - - # 高级行为参数 - "personal_space": 2.0, # 个人空间半径 - "obstacle_radius": 5.0, # 障碍物影响半径 - "seek_weight": 1.0, # 寻求力权重 - "flee_weight": 1.0, # 逃离力权重 - "wander_weight": 0.5, # 游走力权重 - "wander_radius": 5.0, # 游走半径 - "wander_distance": 10.0, # 游走距离 - "wander_jitter": 1.0, # 游走抖动 - "boundary_weight": 2.0, # 边界力权重 - "boundary_buffer": 5.0, # 边界缓冲区 - "path_radius": 3.0, # 路径影响半径 - "path_weight": 1.0, # 路径跟随权重 - "prediction_distance": 10.0, # 预测距离 - "formation_weight": 1.0, # 队形保持权重 - "predator_avoid_weight": 3.0, # 捕食者躲避权重 - "predator_radius": 15.0, # 捕食者影响半径 - "arrival_slow_radius": 5.0, # 到达减速半径 - "interaction_weight": 1.0, # 群体间交互权重 - - # 学习参数 - "learning_rate": 0.1, # 学习率 - "exploration_rate": 0.3, # 探索率 - "memory_capacity": 1000, # 记忆容量 - - # 环境感知参数 - "sensor_range": 15.0, # 传感器范围 - "sensor_accuracy": 0.9, # 传感器精度 - "obstacle_response_weight": 2.0, # 障碍物响应权重 - "predator_response_weight": 3.0, # 捕食者响应权重 - "resource_seek_weight": 0.5, # 资源寻求权重 - "hazard_avoid_weight": 3.0, # 危险回避权重 - "time_flow_rate": 0.1, # 时间流逝速度 - - # 物理参数 - "gravity_enabled": True, # 重力开关 - "air_resistance_enabled": True, # 空气阻力开关 - "buoyancy_enabled": False, # 浮力开关 - "fluid_level": 0.0, # 流体水平面高度 - - # 性能参数 - "update_rate": 60, # FPS - "spatial_partitioning": True, # 空间分区优化 - "neighbor_search_optimization": True, # 邻居搜索优化 - "parallel_computation": True, # 并行计算 - } - - # 当前配置 - self.settings = self.default_settings.copy() - - # 配置验证规则 - self.validation_rules = { - # 基本Boids参数 - "cohesion_weight": (float, 0.0, 10.0), - "separation_weight": (float, 0.0, 10.0), - "alignment_weight": (float, 0.0, 10.0), - "perception_radius": (float, 0.1, 100.0), - - # 移动参数 - "max_speed": (float, 0.1, 50.0), - "max_acceleration": (float, 0.01, 10.0), - "max_force": (float, 0.01, 5.0), - - # 群体参数 - "member_count": (int, 1, 1000), - "swarm_type": (str, ["鸟类", "鱼类", "昆虫", "自定义"]), - - # 行为开关 - "seek_enabled": (bool, None, None), - "obstacle_avoidance_enabled": (bool, None, None), - "wander_enabled": (bool, None, None), - "boundary_enabled": (bool, None, None), - "predator_avoid_enabled": (bool, None, None), - "arrival_enabled": (bool, None, None), - "formation_enabled": (bool, None, None), - "path_following_enabled": (bool, None, None), - "interaction_enabled": (bool, None, None), - "learning_enabled": (bool, None, None), - "environment_awareness_enabled": (bool, None, None), - - # 高级行为参数 - "personal_space": (float, 0.1, 20.0), - "obstacle_radius": (float, 0.1, 50.0), - "seek_weight": (float, 0.0, 10.0), - "flee_weight": (float, 0.0, 10.0), - "wander_weight": (float, 0.0, 5.0), - "wander_radius": (float, 0.1, 50.0), - "wander_distance": (float, 0.1, 100.0), - "wander_jitter": (float, 0.0, 5.0), - "boundary_weight": (float, 0.0, 10.0), - "boundary_buffer": (float, 0.1, 50.0), - "path_radius": (float, 0.1, 50.0), - "path_weight": (float, 0.0, 10.0), - "prediction_distance": (float, 0.1, 100.0), - "formation_weight": (float, 0.0, 10.0), - "predator_avoid_weight": (float, 0.0, 10.0), - "predator_radius": (float, 1.0, 100.0), - "arrival_slow_radius": (float, 0.1, 50.0), - "interaction_weight": (float, 0.0, 5.0), - - # 学习参数 - "learning_rate": (float, 0.01, 1.0), - "exploration_rate": (float, 0.0, 1.0), - "memory_capacity": (int, 100, 10000), - - # 环境感知参数 - "sensor_range": (float, 1.0, 50.0), - "sensor_accuracy": (float, 0.1, 1.0), - "obstacle_response_weight": (float, 0.0, 5.0), - "predator_response_weight": (float, 0.0, 5.0), - "resource_seek_weight": (float, 0.0, 2.0), - "hazard_avoid_weight": (float, 0.0, 5.0), - "time_flow_rate": (float, 0.01, 2.0), - - # 物理参数 - "gravity_enabled": (bool, None, None), - "air_resistance_enabled": (bool, None, None), - "buoyancy_enabled": (bool, None, None), - "fluid_level": (float, -100.0, 100.0), - - # 性能参数 - "update_rate": (int, 1, 240), - "spatial_partitioning": (bool, None, None), - "parallel_computation": (bool, None, None), - - # 队形类型 - "formation_type": (str, ["无", "V字形", "直线", "圆形", "楔形", "球形"]), - } - - # 加载配置 - self.load() - - def _get_default_config_path(self): - """ - 获取默认配置文件路径 - """ - # 获取插件目录 - plugin_dir = Path(__file__).parent - return plugin_dir / "config.json" - - def get(self, key, default=None): - """ - 获取配置值 - """ - return self.settings.get(key, default) - - def set(self, key, value): - """ - 设置配置值 - """ - # 验证配置值 - if self._validate_setting(key, value): - self.settings[key] = value - return True - else: - print(f"配置值验证失败: {key} = {value}") - return False - - def _validate_setting(self, key, value): - """ - 验证配置值是否有效 - """ - if key not in self.validation_rules: - return True # 没有验证规则的配置项直接通过 - - rule = self.validation_rules[key] - value_type, min_val, max_val = rule - - # 类型检查 - if not isinstance(value, value_type): - return False - - # 范围检查 - if value_type in (int, float) and min_val is not None and max_val is not None: - if value < min_val or value > max_val: - return False - - # 枚举检查 - if value_type == str and isinstance(min_val, list): - if value not in min_val: - return False - - return True - - def reset_to_default(self): - """ - 重置为默认配置 - """ - self.settings = self.default_settings.copy() - self.save() - - def load_from_dict(self, config_dict): - """ - 从字典加载配置 - """ - for key, value in config_dict.items(): - self.set(key, value) - - def to_dict(self): - """ - 导出配置为字典 - """ - return self.settings.copy() - - def load(self): - """ - 从文件加载配置 - """ - try: - if os.path.exists(self.config_file): - with open(self.config_file, 'r', encoding='utf-8') as f: - config_data = json.load(f) - self.load_from_dict(config_data) - print(f"配置已从 {self.config_file} 加载") - else: - print("配置文件不存在,使用默认配置") - except Exception as e: - print(f"加载配置文件时出错: {e}") - print("使用默认配置") - - def save(self): - """ - 保存配置到文件 - """ - try: - # 确保目录存在 - os.makedirs(os.path.dirname(self.config_file), exist_ok=True) - - # 保存配置 - with open(self.config_file, 'w', encoding='utf-8') as f: - json.dump(self.settings, f, ensure_ascii=False, indent=4) - print(f"配置已保存到 {self.config_file}") - return True - except Exception as e: - print(f"保存配置文件时出错: {e}") - return False - - def get_all_settings(self): - """ - 获取所有配置项 - """ - return self.settings.copy() - - def update_setting(self, key, value): - """ - 更新单个配置项并保存 - """ - if self.set(key, value): - return self.save() - return False - - def export_config(self, file_path): - """ - 导出配置到指定文件 - """ - try: - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(self.settings, f, ensure_ascii=False, indent=4) - print(f"配置已导出到 {file_path}") - return True - except Exception as e: - print(f"导出配置时出错: {e}") - return False - - def import_config(self, file_path): - """ - 从指定文件导入配置 - """ - try: - with open(file_path, 'r', encoding='utf-8') as f: - config_data = json.load(f) - self.load_from_dict(config_data) - self.save() - print(f"配置已从 {file_path} 导入") - return True - except Exception as e: - print(f"导入配置时出错: {e}") - return False - - def get_setting_info(self, key): - """ - 获取配置项信息 - """ - if key in self.validation_rules: - rule = self.validation_rules[key] - return { - "type": rule[0].__name__, - "min": rule[1], - "max": rule[2], - "default": self.default_settings.get(key) - } - return None - - def validate_all_settings(self): - """ - 验证所有配置项 - """ - invalid_settings = [] - for key, value in self.settings.items(): - if not self._validate_setting(key, value): - invalid_settings.append(key) +""" +群体智能插件配置 +提供完整的配置管理功能,支持多种数据类型和持久化存储 +""" + +import json +import os +from pathlib import Path + +class Config: + """ + 配置管理类 + 支持配置的加载、保存、验证和管理 + """ + + def __init__(self, config_file=None): + # 配置文件路径 + self.config_file = config_file or self._get_default_config_path() + + # 默认配置 + self.default_settings = { + # 基本Boids参数 + "cohesion_weight": 1.0, + "separation_weight": 1.5, + "alignment_weight": 1.0, + "perception_radius": 10.0, + + # 移动参数 + "max_speed": 5.0, + "max_acceleration": 1.0, + "max_force": 0.5, + + # 群体参数 + "member_count": 20, + "swarm_type": "鸟类", + + # 行为开关 + "seek_enabled": False, + "obstacle_avoidance_enabled": True, + "wander_enabled": False, + "boundary_enabled": True, + "predator_avoid_enabled": False, + "arrival_enabled": False, + "formation_enabled": False, + "path_following_enabled": False, + "interaction_enabled": True, # 群体间交互开关 + "learning_enabled": False, # 学习能力开关 + "environment_awareness_enabled": True, # 环境感知开关 + + # 高级行为参数 + "personal_space": 2.0, # 个人空间半径 + "obstacle_radius": 5.0, # 障碍物影响半径 + "seek_weight": 1.0, # 寻求力权重 + "flee_weight": 1.0, # 逃离力权重 + "wander_weight": 0.5, # 游走力权重 + "wander_radius": 5.0, # 游走半径 + "wander_distance": 10.0, # 游走距离 + "wander_jitter": 1.0, # 游走抖动 + "boundary_weight": 2.0, # 边界力权重 + "boundary_buffer": 5.0, # 边界缓冲区 + "path_radius": 3.0, # 路径影响半径 + "path_weight": 1.0, # 路径跟随权重 + "prediction_distance": 10.0, # 预测距离 + "formation_weight": 1.0, # 队形保持权重 + "predator_avoid_weight": 3.0, # 捕食者躲避权重 + "predator_radius": 15.0, # 捕食者影响半径 + "arrival_slow_radius": 5.0, # 到达减速半径 + "interaction_weight": 1.0, # 群体间交互权重 + + # 学习参数 + "learning_rate": 0.1, # 学习率 + "exploration_rate": 0.3, # 探索率 + "memory_capacity": 1000, # 记忆容量 + + # 环境感知参数 + "sensor_range": 15.0, # 传感器范围 + "sensor_accuracy": 0.9, # 传感器精度 + "obstacle_response_weight": 2.0, # 障碍物响应权重 + "predator_response_weight": 3.0, # 捕食者响应权重 + "resource_seek_weight": 0.5, # 资源寻求权重 + "hazard_avoid_weight": 3.0, # 危险回避权重 + "time_flow_rate": 0.1, # 时间流逝速度 + + # 物理参数 + "gravity_enabled": True, # 重力开关 + "air_resistance_enabled": True, # 空气阻力开关 + "buoyancy_enabled": False, # 浮力开关 + "fluid_level": 0.0, # 流体水平面高度 + + # 性能参数 + "update_rate": 60, # FPS + "spatial_partitioning": True, # 空间分区优化 + "neighbor_search_optimization": True, # 邻居搜索优化 + "parallel_computation": True, # 并行计算 + } + + # 当前配置 + self.settings = self.default_settings.copy() + + # 配置验证规则 + self.validation_rules = { + # 基本Boids参数 + "cohesion_weight": (float, 0.0, 10.0), + "separation_weight": (float, 0.0, 10.0), + "alignment_weight": (float, 0.0, 10.0), + "perception_radius": (float, 0.1, 100.0), + + # 移动参数 + "max_speed": (float, 0.1, 50.0), + "max_acceleration": (float, 0.01, 10.0), + "max_force": (float, 0.01, 5.0), + + # 群体参数 + "member_count": (int, 1, 1000), + "swarm_type": (str, ["鸟类", "鱼类", "昆虫", "自定义"]), + + # 行为开关 + "seek_enabled": (bool, None, None), + "obstacle_avoidance_enabled": (bool, None, None), + "wander_enabled": (bool, None, None), + "boundary_enabled": (bool, None, None), + "predator_avoid_enabled": (bool, None, None), + "arrival_enabled": (bool, None, None), + "formation_enabled": (bool, None, None), + "path_following_enabled": (bool, None, None), + "interaction_enabled": (bool, None, None), + "learning_enabled": (bool, None, None), + "environment_awareness_enabled": (bool, None, None), + + # 高级行为参数 + "personal_space": (float, 0.1, 20.0), + "obstacle_radius": (float, 0.1, 50.0), + "seek_weight": (float, 0.0, 10.0), + "flee_weight": (float, 0.0, 10.0), + "wander_weight": (float, 0.0, 5.0), + "wander_radius": (float, 0.1, 50.0), + "wander_distance": (float, 0.1, 100.0), + "wander_jitter": (float, 0.0, 5.0), + "boundary_weight": (float, 0.0, 10.0), + "boundary_buffer": (float, 0.1, 50.0), + "path_radius": (float, 0.1, 50.0), + "path_weight": (float, 0.0, 10.0), + "prediction_distance": (float, 0.1, 100.0), + "formation_weight": (float, 0.0, 10.0), + "predator_avoid_weight": (float, 0.0, 10.0), + "predator_radius": (float, 1.0, 100.0), + "arrival_slow_radius": (float, 0.1, 50.0), + "interaction_weight": (float, 0.0, 5.0), + + # 学习参数 + "learning_rate": (float, 0.01, 1.0), + "exploration_rate": (float, 0.0, 1.0), + "memory_capacity": (int, 100, 10000), + + # 环境感知参数 + "sensor_range": (float, 1.0, 50.0), + "sensor_accuracy": (float, 0.1, 1.0), + "obstacle_response_weight": (float, 0.0, 5.0), + "predator_response_weight": (float, 0.0, 5.0), + "resource_seek_weight": (float, 0.0, 2.0), + "hazard_avoid_weight": (float, 0.0, 5.0), + "time_flow_rate": (float, 0.01, 2.0), + + # 物理参数 + "gravity_enabled": (bool, None, None), + "air_resistance_enabled": (bool, None, None), + "buoyancy_enabled": (bool, None, None), + "fluid_level": (float, -100.0, 100.0), + + # 性能参数 + "update_rate": (int, 1, 240), + "spatial_partitioning": (bool, None, None), + "parallel_computation": (bool, None, None), + + # 队形类型 + "formation_type": (str, ["无", "V字形", "直线", "圆形", "楔形", "球形"]), + } + + # 加载配置 + self.load() + + def _get_default_config_path(self): + """ + 获取默认配置文件路径 + """ + # 获取插件目录 + plugin_dir = Path(__file__).parent + return plugin_dir / "config.json" + + def get(self, key, default=None): + """ + 获取配置值 + """ + return self.settings.get(key, default) + + def set(self, key, value): + """ + 设置配置值 + """ + # 验证配置值 + if self._validate_setting(key, value): + self.settings[key] = value + return True + else: + print(f"配置值验证失败: {key} = {value}") + return False + + def _validate_setting(self, key, value): + """ + 验证配置值是否有效 + """ + if key not in self.validation_rules: + return True # 没有验证规则的配置项直接通过 + + rule = self.validation_rules[key] + value_type, min_val, max_val = rule + + # 类型检查 + if not isinstance(value, value_type): + return False + + # 范围检查 + if value_type in (int, float) and min_val is not None and max_val is not None: + if value < min_val or value > max_val: + return False + + # 枚举检查 + if value_type == str and isinstance(min_val, list): + if value not in min_val: + return False + + return True + + def reset_to_default(self): + """ + 重置为默认配置 + """ + self.settings = self.default_settings.copy() + self.save() + + def load_from_dict(self, config_dict): + """ + 从字典加载配置 + """ + for key, value in config_dict.items(): + self.set(key, value) + + def to_dict(self): + """ + 导出配置为字典 + """ + return self.settings.copy() + + def load(self): + """ + 从文件加载配置 + """ + try: + if os.path.exists(self.config_file): + with open(self.config_file, 'r', encoding='utf-8') as f: + config_data = json.load(f) + self.load_from_dict(config_data) + print(f"配置已从 {self.config_file} 加载") + else: + print("配置文件不存在,使用默认配置") + except Exception as e: + print(f"加载配置文件时出错: {e}") + print("使用默认配置") + + def save(self): + """ + 保存配置到文件 + """ + try: + # 确保目录存在 + os.makedirs(os.path.dirname(self.config_file), exist_ok=True) + + # 保存配置 + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump(self.settings, f, ensure_ascii=False, indent=4) + print(f"配置已保存到 {self.config_file}") + return True + except Exception as e: + print(f"保存配置文件时出错: {e}") + return False + + def get_all_settings(self): + """ + 获取所有配置项 + """ + return self.settings.copy() + + def update_setting(self, key, value): + """ + 更新单个配置项并保存 + """ + if self.set(key, value): + return self.save() + return False + + def export_config(self, file_path): + """ + 导出配置到指定文件 + """ + try: + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(self.settings, f, ensure_ascii=False, indent=4) + print(f"配置已导出到 {file_path}") + return True + except Exception as e: + print(f"导出配置时出错: {e}") + return False + + def import_config(self, file_path): + """ + 从指定文件导入配置 + """ + try: + with open(file_path, 'r', encoding='utf-8') as f: + config_data = json.load(f) + self.load_from_dict(config_data) + self.save() + print(f"配置已从 {file_path} 导入") + return True + except Exception as e: + print(f"导入配置时出错: {e}") + return False + + def get_setting_info(self, key): + """ + 获取配置项信息 + """ + if key in self.validation_rules: + rule = self.validation_rules[key] + return { + "type": rule[0].__name__, + "min": rule[1], + "max": rule[2], + "default": self.default_settings.get(key) + } + return None + + def validate_all_settings(self): + """ + 验证所有配置项 + """ + invalid_settings = [] + for key, value in self.settings.items(): + if not self._validate_setting(key, value): + invalid_settings.append(key) return invalid_settings \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/environment_manager.py b/plugins/user/swarm_intelligence/environment_manager.py index 8f3d0659..bdb68f61 100644 --- a/plugins/user/swarm_intelligence/environment_manager.py +++ b/plugins/user/swarm_intelligence/environment_manager.py @@ -1,481 +1,481 @@ -""" -环境感知和响应管理器 -提供群体对环境的感知能力和响应机制 -""" - -from panda3d.core import Vec3 -import random -import math -from typing import Dict, List, Tuple, Optional, Any - - -class EnvironmentSensor: - """ - 环境传感器类 - 模拟群体成员的环境感知能力 - """ - - def __init__(self, sensor_range: float = 15.0, sensor_accuracy: float = 0.9): - """ - 初始化传感器 - - :param sensor_range: 传感器感知范围 - :param sensor_accuracy: 传感器精度 (0.0-1.0) - """ - self.sensor_range = sensor_range - self.sensor_accuracy = sensor_accuracy - self.sensor_noise = 1.0 - sensor_accuracy # 传感器噪声水平 - - def sense_environment(self, position: Vec3, environment_data: Dict[str, Any]) -> Dict[str, Any]: - """ - 感知环境 - 返回在指定位置感知到的环境信息 - - :param position: 感知位置 - :param environment_data: 环境数据 - :return: 感知到的环境信息 - """ - sensed_data = {} - - # 感知障碍物 - sensed_data['obstacles'] = self._sense_obstacles(position, environment_data.get('obstacles', [])) - - # 感知其他群体 - sensed_data['other_swarms'] = self._sense_other_swarms(position, environment_data.get('swarms', [])) - - # 感知捕食者 - sensed_data['predators'] = self._sense_predators(position, environment_data.get('predators', [])) - - # 感知天气/环境条件 - sensed_data['weather'] = self._sense_weather(position, environment_data.get('weather', {})) - - # 感知资源点 - sensed_data['resources'] = self._sense_resources(position, environment_data.get('resources', [])) - - # 感知危险区域 - sensed_data['hazards'] = self._sense_hazards(position, environment_data.get('hazards', [])) - - # 添加传感器噪声 - sensed_data = self._add_sensor_noise(sensed_data) - - return sensed_data - - def _sense_obstacles(self, position: Vec3, obstacles: List[Dict]) -> List[Dict]: - """ - 感知障碍物 - """ - sensed_obstacles = [] - for obstacle in obstacles: - distance = (position - obstacle['position']).length() - if distance <= self.sensor_range: - # 计算感知误差 - error_factor = random.uniform(1 - self.sensor_noise, 1 + self.sensor_noise) - sensed_obstacle = obstacle.copy() - sensed_obstacle['distance'] = distance * error_factor - sensed_obstacle['direction'] = (obstacle['position'] - position).normalized() - sensed_obstacles.append(sensed_obstacle) - - return sensed_obstacles - - def _sense_other_swarms(self, position: Vec3, swarms: List[Dict]) -> List[Dict]: - """ - 感知其他群体 - """ - sensed_swarms = [] - for swarm in swarms: - # 计算群体中心位置 - if 'members' in swarm and len(swarm['members']) > 0: - center = Vec3(0, 0, 0) - for member in swarm['members']: - center += member['position'] - center /= len(swarm['members']) - - distance = (position - center).length() - if distance <= self.sensor_range: - error_factor = random.uniform(1 - self.sensor_noise, 1 + self.sensor_noise) - sensed_swarm = { - 'id': swarm['id'], - 'type': swarm['type'], - 'distance': distance * error_factor, - 'direction': (center - position).normalized(), - 'size': len(swarm['members']) - } - sensed_swarms.append(sensed_swarm) - - return sensed_swarms - - def _sense_predators(self, position: Vec3, predators: List[Dict]) -> List[Dict]: - """ - 感知捕食者 - """ - sensed_predators = [] - for predator in predators: - distance = (position - predator['position']).length() - if distance <= self.sensor_range: - error_factor = random.uniform(1 - self.sensor_noise, 1 + self.sensor_noise) - sensed_predator = { - 'position': predator['position'], - 'distance': distance * error_factor, - 'direction': (predator['position'] - position).normalized(), - 'velocity': predator.get('velocity', Vec3(0, 0, 0)) - } - sensed_predators.append(sensed_predator) - - return sensed_predators - - def _sense_weather(self, position: Vec3, weather: Dict) -> Dict: - """ - 感知天气/环境条件 - """ - # 天气信息通常对整个区域是相同的,但可能有局部变化 - sensed_weather = weather.copy() - - # 添加局部变化 - if 'wind' in sensed_weather: - wind = sensed_weather['wind'] - # 添加传感器噪声 - noise_x = random.uniform(-self.sensor_noise, self.sensor_noise) * wind['direction'].x - noise_y = random.uniform(-self.sensor_noise, self.sensor_noise) * wind['direction'].y - sensed_weather['wind']['direction'] = Vec3( - wind['direction'].x + noise_x, - wind['direction'].y + noise_y, - wind['direction'].z - ).normalized() - - return sensed_weather - - def _sense_resources(self, position: Vec3, resources: List[Dict]) -> List[Dict]: - """ - 感知资源点 - """ - sensed_resources = [] - for resource in resources: - distance = (position - resource['position']).length() - if distance <= self.sensor_range: - error_factor = random.uniform(1 - self.sensor_noise, 1 + self.sensor_noise) - sensed_resource = resource.copy() - sensed_resource['distance'] = distance * error_factor - sensed_resource['direction'] = (resource['position'] - position).normalized() - sensed_resources.append(sensed_resource) - - return sensed_resources - - def _sense_hazards(self, position: Vec3, hazards: List[Dict]) -> List[Dict]: - """ - 感知危险区域 - """ - sensed_hazards = [] - for hazard in hazards: - distance = (position - hazard['position']).length() - if distance <= self.sensor_range: - error_factor = random.uniform(1 - self.sensor_noise, 1 + self.sensor_noise) - sensed_hazard = hazard.copy() - sensed_hazard['distance'] = distance * error_factor - sensed_hazard['direction'] = (hazard['position'] - position).normalized() - sensed_hazards.append(sensed_hazard) - - return sensed_hazards - - def _add_sensor_noise(self, sensed_data: Dict) -> Dict: - """ - 为感知数据添加噪声 - """ - # 在实际应用中,可以对某些数值添加噪声 - return sensed_data - - -class EnvironmentalResponseSystem: - """ - 环境响应系统 - 根据感知到的环境信息决定群体的响应行为 - """ - - def __init__(self, config): - self.config = config - - def calculate_response(self, member: Dict, sensed_data: Dict, - current_velocity: Vec3, current_position: Vec3) -> Vec3: - """ - 根据感知数据计算响应力 - """ - response_force = Vec3(0, 0, 0) - - # 避开障碍物 - response_force += self._avoid_obstacles(sensed_data['obstacles'], current_position, current_velocity) - - # 应对捕食者 - response_force += self._respond_to_predators(sensed_data['predators'], current_position) - - # 寻找资源 - response_force += self._seek_resources(sensed_data['resources'], current_position) - - # 避开危险区域 - response_force += self._avoid_hazards(sensed_data['hazards'], current_position) - - # 适应天气条件 - response_force += self._adapt_to_weather(sensed_data['weather'], current_position, current_velocity) - - # 处理其他群体 - response_force += self._respond_to_other_swarms(sensed_data['other_swarms'], current_position) - - return response_force - - def _avoid_obstacles(self, obstacles: List[Dict], position: Vec3, velocity: Vec3) -> Vec3: - """ - 避开障碍物 - """ - avoidance_force = Vec3(0, 0, 0) - - for obstacle in obstacles: - distance = obstacle['distance'] - if distance < 5.0: # 在近距离内才需要避障 - # 计算避障方向(远离障碍物) - direction = obstacle['direction'] * -1 - # 距离越近,避障力越大 - magnitude = max(0, 5.0 - distance) / 5.0 - avoidance_force += direction * magnitude - - return avoidance_force * self.config.get("obstacle_response_weight", 2.0) - - def _respond_to_predators(self, predators: List[Dict], position: Vec3) -> Vec3: - """ - 响应捕食者 - """ - flee_force = Vec3(0, 0, 0) - - for predator in predators: - distance = predator['distance'] - if distance < self.config.get("predator_radius", 15.0): - # 逃离捕食者方向 - direction = predator['direction'] * -1 - # 距离越近,逃离力越大 - magnitude = max(0, (self.config.get("predator_radius", 15.0) - distance) / self.config.get("predator_radius", 15.0)) - flee_force += direction * magnitude - - return flee_force * self.config.get("predator_response_weight", 3.0) - - def _seek_resources(self, resources: List[Dict], position: Vec3) -> Vec3: - """ - 寻找资源 - """ - if not resources: - return Vec3(0, 0, 0) - - # 寻找最近的资源 - nearest_resource = min(resources, key=lambda r: r['distance']) - distance = nearest_resource['distance'] - - if distance < 20.0: # 在一定范围内才寻求资源 - direction = nearest_resource['direction'] - # 距离越远,寻求力越大,当接近时减小 - magnitude = max(0.1, 1.0 - distance/20.0) - seek_force = direction * magnitude - return seek_force * self.config.get("resource_seek_weight", 0.5) # 使用配置中的权重 - - return Vec3(0, 0, 0) - - def _avoid_hazards(self, hazards: List[Dict], position: Vec3) -> Vec3: - """ - 避开危险区域 - """ - avoidance_force = Vec3(0, 0, 0) - - for hazard in hazards: - distance = hazard['distance'] - hazard_radius = hazard.get('radius', 5.0) - if distance < hazard_radius * 2: # 在危险区域两倍半径内 - direction = hazard['direction'] * -1 # 远离危险 - magnitude = max(0, (hazard_radius * 2 - distance) / (hazard_radius * 2)) - avoidance_force += direction * magnitude - - return avoidance_force * self.config.get("hazard_avoid_weight", 3.0) # 使用配置中的权重 - - def _adapt_to_weather(self, weather: Dict, position: Vec3, velocity: Vec3) -> Vec3: - """ - 适应天气条件 - """ - if not weather or 'wind' not in weather: - return Vec3(0, 0, 0) - - wind = weather['wind'] - wind_force = wind['direction'] * wind['strength'] - - # 根据成员类型调整对天气的响应 - member_type = position # 位置参数用于占位 - adaptation_factor = 1.0 - - # 实际应用中,可以根据成员类型调整响应 - if hasattr(self, 'member_type'): - if self.member_type == "鸟类": - adaptation_factor = 0.7 # 鸟类受风影响较小 - elif self.member_type == "昆虫": - adaptation_factor = 1.5 # 昆虫受风影响较大 - - return wind_force * adaptation_factor - - def _respond_to_other_swarms(self, other_swarms: List[Dict], position: Vec3) -> Vec3: - """ - 响应其他群体 - 根据群体类型和关系决定响应 - """ - response_force = Vec3(0, 0, 0) - - for swarm in other_swarms: - distance = swarm['distance'] - swarm_type = swarm['type'] - - if distance < 10.0: # 在近距离内响应 - direction = swarm['direction'] - - # 根据群体类型决定响应 - if swarm_type == "predator": # 假设这是捕食者群体 - # 逃离 - response_force += direction * -1 * max(0, (10.0 - distance) / 10.0) * 2.0 - elif swarm_type == "same": # 同类群体 - # 可能靠近或远离,取决于密度 - response_force += direction * max(0, (distance - 5.0) / 5.0) * 0.5 - else: # 其他类型 - # 中性响应 - response_force += direction * 0.1 - - return response_force - - -class EnvironmentManager: - """ - 环境管理器 - 管理整个环境的状态和变化 - """ - - def __init__(self, config): - self.config = config - self.sensors = {} - self.response_system = EnvironmentalResponseSystem(config) - self.environment_state = { - 'obstacles': [], - 'swarms': [], - 'predators': [], - 'weather': {}, - 'resources': [], - 'hazards': [], - 'time_of_day': 12.0, # 24小时制时间 - 'season': 'spring' - } - self.weather_effects = { - 'wind': {'direction': Vec3(0, 1, 0), 'strength': 0.2}, - 'temperature': 20.0, - 'precipitation': 0.0 - } - - def add_sensor(self, member_id: str, sensor_range: float = None, sensor_accuracy: float = None): - """ - 为成员添加传感器 - """ - if sensor_range is None: - sensor_range = self.config.get("sensor_range", 15.0) - if sensor_accuracy is None: - sensor_accuracy = self.config.get("sensor_accuracy", 0.9) - - self.sensors[member_id] = EnvironmentSensor(sensor_range, sensor_accuracy) - - def update_environment_state(self, obstacles: List[Dict], swarms: List[Dict], - predators: List[Dict], resources: List[Dict], - hazards: List[Dict]): - """ - 更新环境状态 - """ - self.environment_state['obstacles'] = obstacles - self.environment_state['swarms'] = swarms - self.environment_state['predators'] = predators - self.environment_state['resources'] = resources - self.environment_state['hazards'] = hazards - - # 更新天气效果(简化模拟) - self._update_weather_effects() - - def _update_weather_effects(self): - """ - 更新天气效果(随时间变化) - """ - # 简单的天气变化模拟 - time_factor = math.sin(self.environment_state['time_of_day'] * math.pi / 12) # 0-24小时周期 - self.weather_effects['wind']['strength'] = 0.2 + abs(time_factor) * 0.3 - - # 温度变化 - self.weather_effects['temperature'] = 15 + time_factor * 10 - - # 更新环境状态 - self.environment_state['weather'] = self.weather_effects.copy() - - def get_environmental_response(self, member: Dict, position: Vec3, velocity: Vec3) -> Vec3: - """ - 获取成员的环境响应力 - """ - member_id = id(member) - - # 如果成员没有传感器,添加一个默认传感器 - if member_id not in self.sensors: - self.add_sensor(member_id) - - # 使用传感器感知环境 - sensed_data = self.sensors[member_id].sense_environment(position, self.environment_state) - - # 计算环境响应 - response_force = self.response_system.calculate_response( - member, sensed_data, velocity, position - ) - - return response_force - - def advance_time(self, dt: float): - """ - 推进环境时间 - """ - self.environment_state['time_of_day'] += dt * 0.1 # 时间流逝速度调整 - if self.environment_state['time_of_day'] >= 24.0: - self.environment_state['time_of_day'] -= 24.0 - - def add_resource(self, position: Vec3, resource_type: str = "food", amount: float = 100.0): - """ - 添加资源点 - """ - resource = { - 'position': position, - 'type': resource_type, - 'amount': amount, - 'radius': 3.0 - } - self.environment_state['resources'].append(resource) - return resource - - def add_hazard(self, position: Vec3, hazard_type: str = "toxic", radius: float = 5.0): - """ - 添加危险区域 - """ - hazard = { - 'position': position, - 'type': hazard_type, - 'radius': radius - } - self.environment_state['hazards'].append(hazard) - return hazard - - def get_environment_info(self) -> Dict: - """ - 获取环境信息 - """ - return self.environment_state.copy() - - def update_environment_dynamics(self): - """ - 更新环境动态变化 - 如资源消耗、天气变化等 - """ - # 更新资源(简化模拟资源消耗) - for i, resource in enumerate(self.environment_state['resources']): - if resource['amount'] > 0: - # 模拟资源随时间缓慢恢复 - resource['amount'] = min(resource['amount'] + 0.01, 100.0) - - # 更新天气 +""" +环境感知和响应管理器 +提供群体对环境的感知能力和响应机制 +""" + +from panda3d.core import Vec3 +import random +import math +from typing import Dict, List, Tuple, Optional, Any + + +class EnvironmentSensor: + """ + 环境传感器类 + 模拟群体成员的环境感知能力 + """ + + def __init__(self, sensor_range: float = 15.0, sensor_accuracy: float = 0.9): + """ + 初始化传感器 + + :param sensor_range: 传感器感知范围 + :param sensor_accuracy: 传感器精度 (0.0-1.0) + """ + self.sensor_range = sensor_range + self.sensor_accuracy = sensor_accuracy + self.sensor_noise = 1.0 - sensor_accuracy # 传感器噪声水平 + + def sense_environment(self, position: Vec3, environment_data: Dict[str, Any]) -> Dict[str, Any]: + """ + 感知环境 + 返回在指定位置感知到的环境信息 + + :param position: 感知位置 + :param environment_data: 环境数据 + :return: 感知到的环境信息 + """ + sensed_data = {} + + # 感知障碍物 + sensed_data['obstacles'] = self._sense_obstacles(position, environment_data.get('obstacles', [])) + + # 感知其他群体 + sensed_data['other_swarms'] = self._sense_other_swarms(position, environment_data.get('swarms', [])) + + # 感知捕食者 + sensed_data['predators'] = self._sense_predators(position, environment_data.get('predators', [])) + + # 感知天气/环境条件 + sensed_data['weather'] = self._sense_weather(position, environment_data.get('weather', {})) + + # 感知资源点 + sensed_data['resources'] = self._sense_resources(position, environment_data.get('resources', [])) + + # 感知危险区域 + sensed_data['hazards'] = self._sense_hazards(position, environment_data.get('hazards', [])) + + # 添加传感器噪声 + sensed_data = self._add_sensor_noise(sensed_data) + + return sensed_data + + def _sense_obstacles(self, position: Vec3, obstacles: List[Dict]) -> List[Dict]: + """ + 感知障碍物 + """ + sensed_obstacles = [] + for obstacle in obstacles: + distance = (position - obstacle['position']).length() + if distance <= self.sensor_range: + # 计算感知误差 + error_factor = random.uniform(1 - self.sensor_noise, 1 + self.sensor_noise) + sensed_obstacle = obstacle.copy() + sensed_obstacle['distance'] = distance * error_factor + sensed_obstacle['direction'] = (obstacle['position'] - position).normalized() + sensed_obstacles.append(sensed_obstacle) + + return sensed_obstacles + + def _sense_other_swarms(self, position: Vec3, swarms: List[Dict]) -> List[Dict]: + """ + 感知其他群体 + """ + sensed_swarms = [] + for swarm in swarms: + # 计算群体中心位置 + if 'members' in swarm and len(swarm['members']) > 0: + center = Vec3(0, 0, 0) + for member in swarm['members']: + center += member['position'] + center /= len(swarm['members']) + + distance = (position - center).length() + if distance <= self.sensor_range: + error_factor = random.uniform(1 - self.sensor_noise, 1 + self.sensor_noise) + sensed_swarm = { + 'id': swarm['id'], + 'type': swarm['type'], + 'distance': distance * error_factor, + 'direction': (center - position).normalized(), + 'size': len(swarm['members']) + } + sensed_swarms.append(sensed_swarm) + + return sensed_swarms + + def _sense_predators(self, position: Vec3, predators: List[Dict]) -> List[Dict]: + """ + 感知捕食者 + """ + sensed_predators = [] + for predator in predators: + distance = (position - predator['position']).length() + if distance <= self.sensor_range: + error_factor = random.uniform(1 - self.sensor_noise, 1 + self.sensor_noise) + sensed_predator = { + 'position': predator['position'], + 'distance': distance * error_factor, + 'direction': (predator['position'] - position).normalized(), + 'velocity': predator.get('velocity', Vec3(0, 0, 0)) + } + sensed_predators.append(sensed_predator) + + return sensed_predators + + def _sense_weather(self, position: Vec3, weather: Dict) -> Dict: + """ + 感知天气/环境条件 + """ + # 天气信息通常对整个区域是相同的,但可能有局部变化 + sensed_weather = weather.copy() + + # 添加局部变化 + if 'wind' in sensed_weather: + wind = sensed_weather['wind'] + # 添加传感器噪声 + noise_x = random.uniform(-self.sensor_noise, self.sensor_noise) * wind['direction'].x + noise_y = random.uniform(-self.sensor_noise, self.sensor_noise) * wind['direction'].y + sensed_weather['wind']['direction'] = Vec3( + wind['direction'].x + noise_x, + wind['direction'].y + noise_y, + wind['direction'].z + ).normalized() + + return sensed_weather + + def _sense_resources(self, position: Vec3, resources: List[Dict]) -> List[Dict]: + """ + 感知资源点 + """ + sensed_resources = [] + for resource in resources: + distance = (position - resource['position']).length() + if distance <= self.sensor_range: + error_factor = random.uniform(1 - self.sensor_noise, 1 + self.sensor_noise) + sensed_resource = resource.copy() + sensed_resource['distance'] = distance * error_factor + sensed_resource['direction'] = (resource['position'] - position).normalized() + sensed_resources.append(sensed_resource) + + return sensed_resources + + def _sense_hazards(self, position: Vec3, hazards: List[Dict]) -> List[Dict]: + """ + 感知危险区域 + """ + sensed_hazards = [] + for hazard in hazards: + distance = (position - hazard['position']).length() + if distance <= self.sensor_range: + error_factor = random.uniform(1 - self.sensor_noise, 1 + self.sensor_noise) + sensed_hazard = hazard.copy() + sensed_hazard['distance'] = distance * error_factor + sensed_hazard['direction'] = (hazard['position'] - position).normalized() + sensed_hazards.append(sensed_hazard) + + return sensed_hazards + + def _add_sensor_noise(self, sensed_data: Dict) -> Dict: + """ + 为感知数据添加噪声 + """ + # 在实际应用中,可以对某些数值添加噪声 + return sensed_data + + +class EnvironmentalResponseSystem: + """ + 环境响应系统 + 根据感知到的环境信息决定群体的响应行为 + """ + + def __init__(self, config): + self.config = config + + def calculate_response(self, member: Dict, sensed_data: Dict, + current_velocity: Vec3, current_position: Vec3) -> Vec3: + """ + 根据感知数据计算响应力 + """ + response_force = Vec3(0, 0, 0) + + # 避开障碍物 + response_force += self._avoid_obstacles(sensed_data['obstacles'], current_position, current_velocity) + + # 应对捕食者 + response_force += self._respond_to_predators(sensed_data['predators'], current_position) + + # 寻找资源 + response_force += self._seek_resources(sensed_data['resources'], current_position) + + # 避开危险区域 + response_force += self._avoid_hazards(sensed_data['hazards'], current_position) + + # 适应天气条件 + response_force += self._adapt_to_weather(sensed_data['weather'], current_position, current_velocity) + + # 处理其他群体 + response_force += self._respond_to_other_swarms(sensed_data['other_swarms'], current_position) + + return response_force + + def _avoid_obstacles(self, obstacles: List[Dict], position: Vec3, velocity: Vec3) -> Vec3: + """ + 避开障碍物 + """ + avoidance_force = Vec3(0, 0, 0) + + for obstacle in obstacles: + distance = obstacle['distance'] + if distance < 5.0: # 在近距离内才需要避障 + # 计算避障方向(远离障碍物) + direction = obstacle['direction'] * -1 + # 距离越近,避障力越大 + magnitude = max(0, 5.0 - distance) / 5.0 + avoidance_force += direction * magnitude + + return avoidance_force * self.config.get("obstacle_response_weight", 2.0) + + def _respond_to_predators(self, predators: List[Dict], position: Vec3) -> Vec3: + """ + 响应捕食者 + """ + flee_force = Vec3(0, 0, 0) + + for predator in predators: + distance = predator['distance'] + if distance < self.config.get("predator_radius", 15.0): + # 逃离捕食者方向 + direction = predator['direction'] * -1 + # 距离越近,逃离力越大 + magnitude = max(0, (self.config.get("predator_radius", 15.0) - distance) / self.config.get("predator_radius", 15.0)) + flee_force += direction * magnitude + + return flee_force * self.config.get("predator_response_weight", 3.0) + + def _seek_resources(self, resources: List[Dict], position: Vec3) -> Vec3: + """ + 寻找资源 + """ + if not resources: + return Vec3(0, 0, 0) + + # 寻找最近的资源 + nearest_resource = min(resources, key=lambda r: r['distance']) + distance = nearest_resource['distance'] + + if distance < 20.0: # 在一定范围内才寻求资源 + direction = nearest_resource['direction'] + # 距离越远,寻求力越大,当接近时减小 + magnitude = max(0.1, 1.0 - distance/20.0) + seek_force = direction * magnitude + return seek_force * self.config.get("resource_seek_weight", 0.5) # 使用配置中的权重 + + return Vec3(0, 0, 0) + + def _avoid_hazards(self, hazards: List[Dict], position: Vec3) -> Vec3: + """ + 避开危险区域 + """ + avoidance_force = Vec3(0, 0, 0) + + for hazard in hazards: + distance = hazard['distance'] + hazard_radius = hazard.get('radius', 5.0) + if distance < hazard_radius * 2: # 在危险区域两倍半径内 + direction = hazard['direction'] * -1 # 远离危险 + magnitude = max(0, (hazard_radius * 2 - distance) / (hazard_radius * 2)) + avoidance_force += direction * magnitude + + return avoidance_force * self.config.get("hazard_avoid_weight", 3.0) # 使用配置中的权重 + + def _adapt_to_weather(self, weather: Dict, position: Vec3, velocity: Vec3) -> Vec3: + """ + 适应天气条件 + """ + if not weather or 'wind' not in weather: + return Vec3(0, 0, 0) + + wind = weather['wind'] + wind_force = wind['direction'] * wind['strength'] + + # 根据成员类型调整对天气的响应 + member_type = position # 位置参数用于占位 + adaptation_factor = 1.0 + + # 实际应用中,可以根据成员类型调整响应 + if hasattr(self, 'member_type'): + if self.member_type == "鸟类": + adaptation_factor = 0.7 # 鸟类受风影响较小 + elif self.member_type == "昆虫": + adaptation_factor = 1.5 # 昆虫受风影响较大 + + return wind_force * adaptation_factor + + def _respond_to_other_swarms(self, other_swarms: List[Dict], position: Vec3) -> Vec3: + """ + 响应其他群体 + 根据群体类型和关系决定响应 + """ + response_force = Vec3(0, 0, 0) + + for swarm in other_swarms: + distance = swarm['distance'] + swarm_type = swarm['type'] + + if distance < 10.0: # 在近距离内响应 + direction = swarm['direction'] + + # 根据群体类型决定响应 + if swarm_type == "predator": # 假设这是捕食者群体 + # 逃离 + response_force += direction * -1 * max(0, (10.0 - distance) / 10.0) * 2.0 + elif swarm_type == "same": # 同类群体 + # 可能靠近或远离,取决于密度 + response_force += direction * max(0, (distance - 5.0) / 5.0) * 0.5 + else: # 其他类型 + # 中性响应 + response_force += direction * 0.1 + + return response_force + + +class EnvironmentManager: + """ + 环境管理器 + 管理整个环境的状态和变化 + """ + + def __init__(self, config): + self.config = config + self.sensors = {} + self.response_system = EnvironmentalResponseSystem(config) + self.environment_state = { + 'obstacles': [], + 'swarms': [], + 'predators': [], + 'weather': {}, + 'resources': [], + 'hazards': [], + 'time_of_day': 12.0, # 24小时制时间 + 'season': 'spring' + } + self.weather_effects = { + 'wind': {'direction': Vec3(0, 1, 0), 'strength': 0.2}, + 'temperature': 20.0, + 'precipitation': 0.0 + } + + def add_sensor(self, member_id: str, sensor_range: float = None, sensor_accuracy: float = None): + """ + 为成员添加传感器 + """ + if sensor_range is None: + sensor_range = self.config.get("sensor_range", 15.0) + if sensor_accuracy is None: + sensor_accuracy = self.config.get("sensor_accuracy", 0.9) + + self.sensors[member_id] = EnvironmentSensor(sensor_range, sensor_accuracy) + + def update_environment_state(self, obstacles: List[Dict], swarms: List[Dict], + predators: List[Dict], resources: List[Dict], + hazards: List[Dict]): + """ + 更新环境状态 + """ + self.environment_state['obstacles'] = obstacles + self.environment_state['swarms'] = swarms + self.environment_state['predators'] = predators + self.environment_state['resources'] = resources + self.environment_state['hazards'] = hazards + + # 更新天气效果(简化模拟) + self._update_weather_effects() + + def _update_weather_effects(self): + """ + 更新天气效果(随时间变化) + """ + # 简单的天气变化模拟 + time_factor = math.sin(self.environment_state['time_of_day'] * math.pi / 12) # 0-24小时周期 + self.weather_effects['wind']['strength'] = 0.2 + abs(time_factor) * 0.3 + + # 温度变化 + self.weather_effects['temperature'] = 15 + time_factor * 10 + + # 更新环境状态 + self.environment_state['weather'] = self.weather_effects.copy() + + def get_environmental_response(self, member: Dict, position: Vec3, velocity: Vec3) -> Vec3: + """ + 获取成员的环境响应力 + """ + member_id = id(member) + + # 如果成员没有传感器,添加一个默认传感器 + if member_id not in self.sensors: + self.add_sensor(member_id) + + # 使用传感器感知环境 + sensed_data = self.sensors[member_id].sense_environment(position, self.environment_state) + + # 计算环境响应 + response_force = self.response_system.calculate_response( + member, sensed_data, velocity, position + ) + + return response_force + + def advance_time(self, dt: float): + """ + 推进环境时间 + """ + self.environment_state['time_of_day'] += dt * 0.1 # 时间流逝速度调整 + if self.environment_state['time_of_day'] >= 24.0: + self.environment_state['time_of_day'] -= 24.0 + + def add_resource(self, position: Vec3, resource_type: str = "food", amount: float = 100.0): + """ + 添加资源点 + """ + resource = { + 'position': position, + 'type': resource_type, + 'amount': amount, + 'radius': 3.0 + } + self.environment_state['resources'].append(resource) + return resource + + def add_hazard(self, position: Vec3, hazard_type: str = "toxic", radius: float = 5.0): + """ + 添加危险区域 + """ + hazard = { + 'position': position, + 'type': hazard_type, + 'radius': radius + } + self.environment_state['hazards'].append(hazard) + return hazard + + def get_environment_info(self) -> Dict: + """ + 获取环境信息 + """ + return self.environment_state.copy() + + def update_environment_dynamics(self): + """ + 更新环境动态变化 + 如资源消耗、天气变化等 + """ + # 更新资源(简化模拟资源消耗) + for i, resource in enumerate(self.environment_state['resources']): + if resource['amount'] > 0: + # 模拟资源随时间缓慢恢复 + resource['amount'] = min(resource['amount'] + 0.01, 100.0) + + # 更新天气 self._update_weather_effects() \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/examples/advanced_example.py b/plugins/user/swarm_intelligence/examples/advanced_example.py index a8f10ea9..740014c8 100644 --- a/plugins/user/swarm_intelligence/examples/advanced_example.py +++ b/plugins/user/swarm_intelligence/examples/advanced_example.py @@ -1,183 +1,183 @@ -""" -高级群体智能示例 -演示使用高级Boids算法和优化功能 -""" - -from panda3d.core import Vec3 -import random -import time - -def create_optimized_swarm(swarm_manager): - """ - 创建优化的群体 - 使用空间分区和高级行为 - """ - print("创建优化的群体...") - - # 配置高级参数 - swarm_manager.config.set("member_count", 200) # 增加群体规模 - swarm_manager.config.set("cohesion_weight", 1.2) - swarm_manager.config.set("separation_weight", 1.8) - swarm_manager.config.set("alignment_weight", 1.0) - swarm_manager.config.set("perception_radius", 15.0) - swarm_manager.config.set("max_speed", 8.0) - - # 启用物理效果 - swarm_manager.config.set("gravity_enabled", True) - swarm_manager.config.set("air_resistance_enabled", True) - - # 启用空间分区优化 - swarm_manager.config.set("spatial_partitioning", True) - swarm_manager.config.set("neighbor_search_optimization", True) - - # 重新创建群体 - swarm_manager.reset() - - print(f"优化群体创建完成,包含{len(swarm_manager.members)}个成员") - -def create_complex_obstacles(swarm_manager, count=20): - """ - 创建复杂障碍物环境 - """ - print(f"创建{count}个复杂障碍物...") - - for i in range(count): - # 随机位置 - position = Vec3( - random.uniform(-40, 40), - random.uniform(-40, 40), - random.uniform(5, 25) - ) - # 随机半径 - radius = random.uniform(1, 8) - - # 创建障碍物 - swarm_manager.create_obstacle(position, radius) - - print(f"复杂障碍物创建完成") - -def create_hierarchical_flock(swarm_manager): - """ - 创建分层群体结构 - 包含领导者和跟随者 - """ - print("创建分层群体结构...") - - # 标记前10%的成员为领导者 - leader_count = max(1, len(swarm_manager.members) // 10) - - for i, member in enumerate(swarm_manager.members): - if i < leader_count: - member['is_leader'] = True - # 设置领导者颜色 - if 'node' in member: - member['node'].setColor(0, 1, 0, 1) # 绿色 - else: - member['is_leader'] = False - # 设置跟随者颜色 - if 'node' in member: - member['node'].setColor(1, 1, 1, 1) # 白色 - - # 调整领导者参数 - swarm_manager.config.set("leader_cohesion_weight", 2.0) - swarm_manager.config.set("leader_separation_weight", 1.0) - - print(f"分层群体结构创建完成,{leader_count}个领导者") - -def create_dynamic_environment(swarm_manager): - """ - 创建动态环境 - 包括移动障碍物和变化的目标 - """ - print("创建动态环境...") - - # 添加移动障碍物 - moving_obstacle = { - 'position': Vec3(0, 0, 15), - 'radius': 5.0, - 'velocity': Vec3(2, 1, 0), - 'bounds': {'min_x': -30, 'max_x': 30, 'min_y': -30, 'max_y': 30} - } - - swarm_manager.moving_obstacles = [moving_obstacle] - - # 设置移动目标 - swarm_manager.set_target(Vec3(20, 20, 15)) - swarm_manager.config.set("seek_enabled", True) - - print("动态环境创建完成") - -def update_dynamic_environment(swarm_manager, dt): - """ - 更新动态环境 - 移动障碍物和变化目标 - """ - # 更新移动障碍物 - if hasattr(swarm_manager, 'moving_obstacles'): - for obstacle in swarm_manager.moving_obstacles: - # 移动障碍物 - obstacle['position'] += obstacle['velocity'] * dt - - # 边界反弹 - bounds = obstacle['bounds'] - if obstacle['position'].x < bounds['min_x'] or obstacle['position'].x > bounds['max_x']: - obstacle['velocity'].x *= -1 - if obstacle['position'].y < bounds['min_y'] or obstacle['position'].y > bounds['max_y']: - obstacle['velocity'].y *= -1 - - # 保持在边界内 - obstacle['position'].x = max(bounds['min_x'], min(bounds['max_x'], obstacle['position'].x)) - obstacle['position'].y = max(bounds['min_y'], min(bounds['max_y'], obstacle['position'].y)) - - # 定期改变目标位置 - if not hasattr(swarm_manager, 'last_target_change'): - swarm_manager.last_target_change = time.time() - - if time.time() - swarm_manager.last_target_change > 10.0: # 每10秒改变一次目标 - new_target = Vec3( - random.uniform(-30, 30), - random.uniform(-30, 30), - random.uniform(10, 25) - ) - swarm_manager.set_target(new_target) - swarm_manager.last_target_change = time.time() - -def create_performance_test(swarm_manager): - """ - 创建性能测试场景 - """ - print("创建性能测试场景...") - - # 创建大规模群体 - swarm_manager.config.set("member_count", 500) - swarm_manager.reset() - - # 添加大量障碍物 - create_complex_obstacles(swarm_manager, 50) - - # 启用所有优化 - swarm_manager.config.set("spatial_partitioning", True) - swarm_manager.config.set("neighbor_search_optimization", True) - swarm_manager.config.set("gravity_enabled", True) - swarm_manager.config.set("air_resistance_enabled", True) - - print("性能测试场景创建完成") - -def run_advanced_example(swarm_manager): - """ - 运行高级示例 - """ - print("=== 高级群体智能示例 ===") - - # 创建优化群体 - create_optimized_swarm(swarm_manager) - - # 创建复杂环境 - create_complex_obstacles(swarm_manager, 10) - create_hierarchical_flock(swarm_manager) - create_dynamic_environment(swarm_manager) - - print("高级示例设置完成") - print("观察群体的复杂行为和优化性能") - +""" +高级群体智能示例 +演示使用高级Boids算法和优化功能 +""" + +from panda3d.core import Vec3 +import random +import time + +def create_optimized_swarm(swarm_manager): + """ + 创建优化的群体 + 使用空间分区和高级行为 + """ + print("创建优化的群体...") + + # 配置高级参数 + swarm_manager.config.set("member_count", 200) # 增加群体规模 + swarm_manager.config.set("cohesion_weight", 1.2) + swarm_manager.config.set("separation_weight", 1.8) + swarm_manager.config.set("alignment_weight", 1.0) + swarm_manager.config.set("perception_radius", 15.0) + swarm_manager.config.set("max_speed", 8.0) + + # 启用物理效果 + swarm_manager.config.set("gravity_enabled", True) + swarm_manager.config.set("air_resistance_enabled", True) + + # 启用空间分区优化 + swarm_manager.config.set("spatial_partitioning", True) + swarm_manager.config.set("neighbor_search_optimization", True) + + # 重新创建群体 + swarm_manager.reset() + + print(f"优化群体创建完成,包含{len(swarm_manager.members)}个成员") + +def create_complex_obstacles(swarm_manager, count=20): + """ + 创建复杂障碍物环境 + """ + print(f"创建{count}个复杂障碍物...") + + for i in range(count): + # 随机位置 + position = Vec3( + random.uniform(-40, 40), + random.uniform(-40, 40), + random.uniform(5, 25) + ) + # 随机半径 + radius = random.uniform(1, 8) + + # 创建障碍物 + swarm_manager.create_obstacle(position, radius) + + print(f"复杂障碍物创建完成") + +def create_hierarchical_flock(swarm_manager): + """ + 创建分层群体结构 + 包含领导者和跟随者 + """ + print("创建分层群体结构...") + + # 标记前10%的成员为领导者 + leader_count = max(1, len(swarm_manager.members) // 10) + + for i, member in enumerate(swarm_manager.members): + if i < leader_count: + member['is_leader'] = True + # 设置领导者颜色 + if 'node' in member: + member['node'].setColor(0, 1, 0, 1) # 绿色 + else: + member['is_leader'] = False + # 设置跟随者颜色 + if 'node' in member: + member['node'].setColor(1, 1, 1, 1) # 白色 + + # 调整领导者参数 + swarm_manager.config.set("leader_cohesion_weight", 2.0) + swarm_manager.config.set("leader_separation_weight", 1.0) + + print(f"分层群体结构创建完成,{leader_count}个领导者") + +def create_dynamic_environment(swarm_manager): + """ + 创建动态环境 + 包括移动障碍物和变化的目标 + """ + print("创建动态环境...") + + # 添加移动障碍物 + moving_obstacle = { + 'position': Vec3(0, 0, 15), + 'radius': 5.0, + 'velocity': Vec3(2, 1, 0), + 'bounds': {'min_x': -30, 'max_x': 30, 'min_y': -30, 'max_y': 30} + } + + swarm_manager.moving_obstacles = [moving_obstacle] + + # 设置移动目标 + swarm_manager.set_target(Vec3(20, 20, 15)) + swarm_manager.config.set("seek_enabled", True) + + print("动态环境创建完成") + +def update_dynamic_environment(swarm_manager, dt): + """ + 更新动态环境 + 移动障碍物和变化目标 + """ + # 更新移动障碍物 + if hasattr(swarm_manager, 'moving_obstacles'): + for obstacle in swarm_manager.moving_obstacles: + # 移动障碍物 + obstacle['position'] += obstacle['velocity'] * dt + + # 边界反弹 + bounds = obstacle['bounds'] + if obstacle['position'].x < bounds['min_x'] or obstacle['position'].x > bounds['max_x']: + obstacle['velocity'].x *= -1 + if obstacle['position'].y < bounds['min_y'] or obstacle['position'].y > bounds['max_y']: + obstacle['velocity'].y *= -1 + + # 保持在边界内 + obstacle['position'].x = max(bounds['min_x'], min(bounds['max_x'], obstacle['position'].x)) + obstacle['position'].y = max(bounds['min_y'], min(bounds['max_y'], obstacle['position'].y)) + + # 定期改变目标位置 + if not hasattr(swarm_manager, 'last_target_change'): + swarm_manager.last_target_change = time.time() + + if time.time() - swarm_manager.last_target_change > 10.0: # 每10秒改变一次目标 + new_target = Vec3( + random.uniform(-30, 30), + random.uniform(-30, 30), + random.uniform(10, 25) + ) + swarm_manager.set_target(new_target) + swarm_manager.last_target_change = time.time() + +def create_performance_test(swarm_manager): + """ + 创建性能测试场景 + """ + print("创建性能测试场景...") + + # 创建大规模群体 + swarm_manager.config.set("member_count", 500) + swarm_manager.reset() + + # 添加大量障碍物 + create_complex_obstacles(swarm_manager, 50) + + # 启用所有优化 + swarm_manager.config.set("spatial_partitioning", True) + swarm_manager.config.set("neighbor_search_optimization", True) + swarm_manager.config.set("gravity_enabled", True) + swarm_manager.config.set("air_resistance_enabled", True) + + print("性能测试场景创建完成") + +def run_advanced_example(swarm_manager): + """ + 运行高级示例 + """ + print("=== 高级群体智能示例 ===") + + # 创建优化群体 + create_optimized_swarm(swarm_manager) + + # 创建复杂环境 + create_complex_obstacles(swarm_manager, 10) + create_hierarchical_flock(swarm_manager) + create_dynamic_environment(swarm_manager) + + print("高级示例设置完成") + print("观察群体的复杂行为和优化性能") + return update_dynamic_environment \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/examples/advanced_swarm.py b/plugins/user/swarm_intelligence/examples/advanced_swarm.py index 05e8d428..b25fb746 100644 --- a/plugins/user/swarm_intelligence/examples/advanced_swarm.py +++ b/plugins/user/swarm_intelligence/examples/advanced_swarm.py @@ -1,252 +1,252 @@ -""" -高级群体智能示例 -演示复杂的群体行为和交互 -""" - -from panda3d.core import Vec3 -import random - -def create_bird_flock(swarm_manager): - """ - 创建鸟类群体 - """ - print("创建鸟类群体...") - - # 设置群体类型 - swarm_manager.update_swarm_type("鸟类") - - # 配置参数 - swarm_manager.config.set("member_count", 50) - swarm_manager.config.set("cohesion_weight", 1.2) - swarm_manager.config.set("separation_weight", 1.8) - swarm_manager.config.set("alignment_weight", 1.0) - swarm_manager.config.set("perception_radius", 15.0) - swarm_manager.config.set("max_speed", 8.0) - - # 重新创建群体 - swarm_manager.reset() - - print("鸟类群体创建完成,包含50个成员") - -def create_fish_school(swarm_manager): - """ - 创建鱼群 - """ - print("创建鱼群...") - - # 设置群体类型 - swarm_manager.update_swarm_type("鱼类") - - # 配置参数 - swarm_manager.config.set("member_count", 30) - swarm_manager.config.set("cohesion_weight", 1.5) - swarm_manager.config.set("separation_weight", 2.0) - swarm_manager.config.set("alignment_weight", 1.2) - swarm_manager.config.set("perception_radius", 8.0) - swarm_manager.config.set("max_speed", 6.0) - - # 重新创建群体 - swarm_manager.reset() - - print("鱼群创建完成,包含30个成员") - -def create_insect_swarm(swarm_manager): - """ - 创建昆虫群体 - """ - print("创建昆虫群体...") - - # 设置群体类型 - swarm_manager.update_swarm_type("昆虫") - - # 启用游走行为 - swarm_manager.config.set("wander_enabled", True) - swarm_manager.config.set("wander_weight", 0.8) - - # 配置参数 - swarm_manager.config.set("member_count", 100) - swarm_manager.config.set("cohesion_weight", 0.8) - swarm_manager.config.set("separation_weight", 1.2) - swarm_manager.config.set("alignment_weight", 0.6) - swarm_manager.config.set("perception_radius", 5.0) - swarm_manager.config.set("max_speed", 10.0) - - # 重新创建群体 - swarm_manager.reset() - - print("昆虫群体创建完成,包含100个成员") - -def add_obstacles(swarm_manager, count=5): - """ - 添加障碍物 - """ - print(f"添加{count}个障碍物...") - - for i in range(count): - # 随机位置 - position = Vec3( - random.uniform(-30, 30), - random.uniform(-30, 30), - random.uniform(5, 20) - ) - # 随机半径 - radius = random.uniform(2, 5) - - # 创建障碍物 - swarm_manager.create_obstacle(position, radius) - - print(f"障碍物添加完成") - -def add_predators(swarm_manager, count=2): - """ - 添加捕食者 - """ - print(f"添加{count}个捕食者...") - - for i in range(count): - # 随机位置 - position = Vec3( - random.uniform(-40, 40), - random.uniform(-40, 40), - random.uniform(5, 25) - ) - - # 创建捕食者 - swarm_manager.create_predator(position) - - print(f"捕食者添加完成") - -def create_v_formation(swarm_manager): - """ - 创建V字形队形 - """ - print("创建V字形队形...") - - # 启用队形保持 - swarm_manager.config.set("formation_enabled", True) - swarm_manager.config.set("formation_type", "V字形") - swarm_manager.config.set("formation_weight", 2.0) - - # 设置目标 - target = Vec3(0, 50, 15) - swarm_manager.set_target(target) - swarm_manager.config.set("seek_enabled", True) - swarm_manager.config.set("seek_weight", 1.5) - - print("V字形队形创建完成") - -def create_circular_formation(swarm_manager): - """ - 创建圆形队形 - """ - print("创建圆形队形...") - - # 启用队形保持 - swarm_manager.config.set("formation_enabled", True) - swarm_manager.config.set("formation_type", "圆形") - swarm_manager.config.set("formation_weight", 1.5) - - print("圆形队形创建完成") - -def create_path_following(swarm_manager): - """ - 创建路径跟随场景 - """ - print("创建路径跟随场景...") - - # 定义路径点 - path = [ - Vec3(-30, -30, 10), - Vec3(0, -40, 15), - Vec3(30, -30, 10), - Vec3(40, 0, 20), - Vec3(30, 30, 10), - Vec3(0, 40, 15), - Vec3(-30, 30, 10), - Vec3(-40, 0, 20), - ] - - # 设置路径 - swarm_manager.set_path(path) - - # 启用路径跟随 - swarm_manager.config.set("path_following_enabled", True) - swarm_manager.config.set("path_weight", 2.0) - swarm_manager.config.set("prediction_distance", 15.0) - - print("路径跟随场景创建完成") - -def create_hunting_scenario(swarm_manager): - """ - 创建捕猎场景 - """ - print("创建捕猎场景...") - - # 创建小规模鸟类群体 - swarm_manager.update_swarm_type("鸟类") - swarm_manager.config.set("member_count", 20) - swarm_manager.reset() - - # 添加捕食者 - predator_pos = Vec3(-20, -20, 15) - swarm_manager.create_predator(predator_pos) - - # 启用捕食者躲避 - swarm_manager.config.set("predator_avoid_enabled", True) - swarm_manager.config.set("predator_avoid_weight", 4.0) - swarm_manager.config.set("predator_radius", 20.0) - - print("捕猎场景创建完成") - -def create_obstacle_avoidance_scenario(swarm_manager): - """ - 创建避障场景 - """ - print("创建避障场景...") - - # 创建中等规模群体 - swarm_manager.config.set("member_count", 30) - swarm_manager.reset() - - # 添加多个障碍物 - obstacles = [ - (Vec3(-10, 0, 10), 5), - (Vec3(10, 0, 15), 4), - (Vec3(0, 10, 12), 6), - (Vec3(0, -10, 8), 3), - ] - - for pos, radius in obstacles: - swarm_manager.create_obstacle(pos, radius) - - # 设置移动目标 - target = Vec3(0, 30, 15) - swarm_manager.set_target(target) - swarm_manager.config.set("seek_enabled", True) - - # 启用避障 - swarm_manager.config.set("obstacle_avoidance_enabled", True) - swarm_manager.config.set("obstacle_weight", 3.0) - - print("避障场景创建完成") - -def run_advanced_example(swarm_manager): - """ - 运行高级示例 - """ - print("=== 群体智能高级示例 ===") - - # 创建鸟类群体 - create_bird_flock(swarm_manager) - - # 添加障碍物 - add_obstacles(swarm_manager, 3) - - # 添加捕食者 - add_predators(swarm_manager, 1) - - # 启用多种行为 - swarm_manager.config.set("wander_enabled", True) - swarm_manager.config.set("boundary_enabled", True) - +""" +高级群体智能示例 +演示复杂的群体行为和交互 +""" + +from panda3d.core import Vec3 +import random + +def create_bird_flock(swarm_manager): + """ + 创建鸟类群体 + """ + print("创建鸟类群体...") + + # 设置群体类型 + swarm_manager.update_swarm_type("鸟类") + + # 配置参数 + swarm_manager.config.set("member_count", 50) + swarm_manager.config.set("cohesion_weight", 1.2) + swarm_manager.config.set("separation_weight", 1.8) + swarm_manager.config.set("alignment_weight", 1.0) + swarm_manager.config.set("perception_radius", 15.0) + swarm_manager.config.set("max_speed", 8.0) + + # 重新创建群体 + swarm_manager.reset() + + print("鸟类群体创建完成,包含50个成员") + +def create_fish_school(swarm_manager): + """ + 创建鱼群 + """ + print("创建鱼群...") + + # 设置群体类型 + swarm_manager.update_swarm_type("鱼类") + + # 配置参数 + swarm_manager.config.set("member_count", 30) + swarm_manager.config.set("cohesion_weight", 1.5) + swarm_manager.config.set("separation_weight", 2.0) + swarm_manager.config.set("alignment_weight", 1.2) + swarm_manager.config.set("perception_radius", 8.0) + swarm_manager.config.set("max_speed", 6.0) + + # 重新创建群体 + swarm_manager.reset() + + print("鱼群创建完成,包含30个成员") + +def create_insect_swarm(swarm_manager): + """ + 创建昆虫群体 + """ + print("创建昆虫群体...") + + # 设置群体类型 + swarm_manager.update_swarm_type("昆虫") + + # 启用游走行为 + swarm_manager.config.set("wander_enabled", True) + swarm_manager.config.set("wander_weight", 0.8) + + # 配置参数 + swarm_manager.config.set("member_count", 100) + swarm_manager.config.set("cohesion_weight", 0.8) + swarm_manager.config.set("separation_weight", 1.2) + swarm_manager.config.set("alignment_weight", 0.6) + swarm_manager.config.set("perception_radius", 5.0) + swarm_manager.config.set("max_speed", 10.0) + + # 重新创建群体 + swarm_manager.reset() + + print("昆虫群体创建完成,包含100个成员") + +def add_obstacles(swarm_manager, count=5): + """ + 添加障碍物 + """ + print(f"添加{count}个障碍物...") + + for i in range(count): + # 随机位置 + position = Vec3( + random.uniform(-30, 30), + random.uniform(-30, 30), + random.uniform(5, 20) + ) + # 随机半径 + radius = random.uniform(2, 5) + + # 创建障碍物 + swarm_manager.create_obstacle(position, radius) + + print(f"障碍物添加完成") + +def add_predators(swarm_manager, count=2): + """ + 添加捕食者 + """ + print(f"添加{count}个捕食者...") + + for i in range(count): + # 随机位置 + position = Vec3( + random.uniform(-40, 40), + random.uniform(-40, 40), + random.uniform(5, 25) + ) + + # 创建捕食者 + swarm_manager.create_predator(position) + + print(f"捕食者添加完成") + +def create_v_formation(swarm_manager): + """ + 创建V字形队形 + """ + print("创建V字形队形...") + + # 启用队形保持 + swarm_manager.config.set("formation_enabled", True) + swarm_manager.config.set("formation_type", "V字形") + swarm_manager.config.set("formation_weight", 2.0) + + # 设置目标 + target = Vec3(0, 50, 15) + swarm_manager.set_target(target) + swarm_manager.config.set("seek_enabled", True) + swarm_manager.config.set("seek_weight", 1.5) + + print("V字形队形创建完成") + +def create_circular_formation(swarm_manager): + """ + 创建圆形队形 + """ + print("创建圆形队形...") + + # 启用队形保持 + swarm_manager.config.set("formation_enabled", True) + swarm_manager.config.set("formation_type", "圆形") + swarm_manager.config.set("formation_weight", 1.5) + + print("圆形队形创建完成") + +def create_path_following(swarm_manager): + """ + 创建路径跟随场景 + """ + print("创建路径跟随场景...") + + # 定义路径点 + path = [ + Vec3(-30, -30, 10), + Vec3(0, -40, 15), + Vec3(30, -30, 10), + Vec3(40, 0, 20), + Vec3(30, 30, 10), + Vec3(0, 40, 15), + Vec3(-30, 30, 10), + Vec3(-40, 0, 20), + ] + + # 设置路径 + swarm_manager.set_path(path) + + # 启用路径跟随 + swarm_manager.config.set("path_following_enabled", True) + swarm_manager.config.set("path_weight", 2.0) + swarm_manager.config.set("prediction_distance", 15.0) + + print("路径跟随场景创建完成") + +def create_hunting_scenario(swarm_manager): + """ + 创建捕猎场景 + """ + print("创建捕猎场景...") + + # 创建小规模鸟类群体 + swarm_manager.update_swarm_type("鸟类") + swarm_manager.config.set("member_count", 20) + swarm_manager.reset() + + # 添加捕食者 + predator_pos = Vec3(-20, -20, 15) + swarm_manager.create_predator(predator_pos) + + # 启用捕食者躲避 + swarm_manager.config.set("predator_avoid_enabled", True) + swarm_manager.config.set("predator_avoid_weight", 4.0) + swarm_manager.config.set("predator_radius", 20.0) + + print("捕猎场景创建完成") + +def create_obstacle_avoidance_scenario(swarm_manager): + """ + 创建避障场景 + """ + print("创建避障场景...") + + # 创建中等规模群体 + swarm_manager.config.set("member_count", 30) + swarm_manager.reset() + + # 添加多个障碍物 + obstacles = [ + (Vec3(-10, 0, 10), 5), + (Vec3(10, 0, 15), 4), + (Vec3(0, 10, 12), 6), + (Vec3(0, -10, 8), 3), + ] + + for pos, radius in obstacles: + swarm_manager.create_obstacle(pos, radius) + + # 设置移动目标 + target = Vec3(0, 30, 15) + swarm_manager.set_target(target) + swarm_manager.config.set("seek_enabled", True) + + # 启用避障 + swarm_manager.config.set("obstacle_avoidance_enabled", True) + swarm_manager.config.set("obstacle_weight", 3.0) + + print("避障场景创建完成") + +def run_advanced_example(swarm_manager): + """ + 运行高级示例 + """ + print("=== 群体智能高级示例 ===") + + # 创建鸟类群体 + create_bird_flock(swarm_manager) + + # 添加障碍物 + add_obstacles(swarm_manager, 3) + + # 添加捕食者 + add_predators(swarm_manager, 1) + + # 启用多种行为 + swarm_manager.config.set("wander_enabled", True) + swarm_manager.config.set("boundary_enabled", True) + print("高级示例设置完成,观察群体的复杂行为") \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/examples/basic_flock.py b/plugins/user/swarm_intelligence/examples/basic_flock.py index 87182d64..9e7c62d7 100644 --- a/plugins/user/swarm_intelligence/examples/basic_flock.py +++ b/plugins/user/swarm_intelligence/examples/basic_flock.py @@ -1,28 +1,28 @@ -""" -群体智能插件使用示例 -""" - -def basic_example(): - """ - 基础使用示例 - """ - print("群体智能插件基础使用示例") - print("1. 启用插件后,将自动创建一个包含20个成员的群体") - print("2. 群体成员会根据Boids算法进行移动") - print("3. 按F1键可以暂停/继续群体模拟") - print("4. 通过控制面板可以调节群体行为参数") - -def advanced_example(): - """ - 高级使用示例 - """ - print("群体智能插件高级使用示例") - print("1. 可以通过配置文件自定义群体参数") - print("2. 支持动态添加/删除群体成员") - print("3. 可以与场景中的其他对象进行交互") - print("4. 支持自定义群体行为规则") - -if __name__ == "__main__": - basic_example() - print() +""" +群体智能插件使用示例 +""" + +def basic_example(): + """ + 基础使用示例 + """ + print("群体智能插件基础使用示例") + print("1. 启用插件后,将自动创建一个包含20个成员的群体") + print("2. 群体成员会根据Boids算法进行移动") + print("3. 按F1键可以暂停/继续群体模拟") + print("4. 通过控制面板可以调节群体行为参数") + +def advanced_example(): + """ + 高级使用示例 + """ + print("群体智能插件高级使用示例") + print("1. 可以通过配置文件自定义群体参数") + print("2. 支持动态添加/删除群体成员") + print("3. 可以与场景中的其他对象进行交互") + print("4. 支持自定义群体行为规则") + +if __name__ == "__main__": + basic_example() + print() advanced_example() \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/examples/custom_behavior.py b/plugins/user/swarm_intelligence/examples/custom_behavior.py index 6a03eb8b..5696d0b4 100644 --- a/plugins/user/swarm_intelligence/examples/custom_behavior.py +++ b/plugins/user/swarm_intelligence/examples/custom_behavior.py @@ -1,249 +1,249 @@ -""" -自定义行为示例 -演示如何创建自定义的群体行为 -""" - -from panda3d.core import Vec3 -import math - -class CustomBehaviorExample: - """ - 自定义行为示例类 - """ - - def __init__(self, swarm_manager): - self.swarm_manager = swarm_manager - - def create_leadership_behavior(self, leader_count=3): - """ - 创建领导行为 - 指定几个成员作为领导者,其他成员跟随领导者 - """ - print(f"创建领导行为,设置{leader_count}个领导者...") - - # 重置群体 - self.swarm_manager.reset() - - # 标记领导者 - for i, member in enumerate(self.swarm_manager.members): - if i < leader_count: - member['is_leader'] = True - # 设置领导者颜色(实际应用中可能需要修改材质) - if 'node' in member: - member['node'].setColor(0, 1, 0, 1) # 绿色表示领导者 - else: - member['is_leader'] = False - if 'node' in member: - member['node'].setColor(1, 1, 1, 1) # 白色表示跟随者 - - # 配置参数 - self.swarm_manager.config.set("cohesion_weight", 1.0) - self.swarm_manager.config.set("separation_weight", 1.5) - self.swarm_manager.config.set("alignment_weight", 0.8) - - print("领导行为创建完成") - - def create_flocking_with_leaders(self, member): - """ - 为跟随者成员实现跟随领导者的行为 - """ - # 查找领导者邻居 - leaders = [m for m in self.swarm_manager.members if m.get('is_leader', False)] - - if not leaders: - return Vec3(0, 0, 0) - - # 计算到领导者的平均方向 - direction = Vec3(0, 0, 0) - for leader in leaders: - if leader != member: # 排除自己 - dir_to_leader = leader['position'] - member['position'] - distance = dir_to_leader.length() - if distance > 0: - # 距离越近,跟随力越小(避免碰撞) - direction += dir_to_leader.normalized() / (distance / 10.0) - - return direction.normalized() - - def create_attraction_behavior(self, attractor_position, attraction_strength=1.0): - """ - 创建吸引力行为 - 向指定位置吸引群体成员 - """ - print(f"创建吸引力行为,吸引点: {attractor_position}") - - # 设置吸引点 - self.attractor_position = attractor_position - self.attraction_strength = attraction_strength - - # 启用吸引行为 - self.swarm_manager.config.set("attraction_enabled", True) - - print("吸引力行为创建完成") - - def calculate_attraction_force(self, member): - """ - 计算吸引力 - """ - if not hasattr(self, 'attractor_position'): - return Vec3(0, 0, 0) - - direction = self.attractor_position - member['position'] - distance = direction.length() - - if distance > 0: - # 归一化方向并应用强度 - force = direction.normalized() * self.attraction_strength - # 距离越近,力量越小 - force *= 1.0 / (distance / 10.0) - return force - - return Vec3(0, 0, 0) - - def create_repulsion_behavior(self, repulsor_position, repulsion_strength=1.0, radius=20.0): - """ - 创建排斥力行为 - 从指定位置推开群体成员 - """ - print(f"创建排斥力行为,排斥点: {repulsor_position}") - - # 设置排斥点 - self.repulsor_position = repulsor_position - self.repulsion_strength = repulsion_strength - self.repulsion_radius = radius - - # 启用排斥行为 - self.swarm_manager.config.set("repulsion_enabled", True) - - print("排斥力行为创建完成") - - def calculate_repulsion_force(self, member): - """ - 计算排斥力 - """ - if not hasattr(self, 'repulsor_position'): - return Vec3(0, 0, 0) - - direction = member['position'] - self.repulsor_position - distance = direction.length() - - # 只在一定范围内施加排斥力 - if 0 < distance < self.repulsion_radius: - # 归一化方向并应用强度 - force = direction.normalized() * self.repulsion_strength - # 距离越近,力量越大 - force *= (self.repulsion_radius - distance) / self.repulsion_radius - return force - - return Vec3(0, 0, 0) - - def create_sine_wave_behavior(self, amplitude=5.0, frequency=0.5): - """ - 创建正弦波行为 - 群体成员沿正弦波路径移动 - """ - print("创建正弦波行为...") - - self.sine_amplitude = amplitude - self.sine_frequency = frequency - self.sine_offset = 0.0 - - # 启用正弦波行为 - self.swarm_manager.config.set("sine_wave_enabled", True) - - print("正弦波行为创建完成") - - def update_sine_wave_behavior(self, dt): - """ - 更新正弦波行为 - """ - if hasattr(self, 'sine_offset'): - self.sine_offset += dt * self.sine_frequency - - def calculate_sine_wave_force(self, member): - """ - 计算正弦波力 - """ - if not hasattr(self, 'sine_offset'): - return Vec3(0, 0, 0) - - # 基于成员的Y坐标计算期望的Z坐标 - expected_z = math.sin(member['position'].y * 0.1 + self.sine_offset) * self.sine_amplitude - - # 计算向期望位置的力 - desired_z = expected_z - member['position'].z - force = Vec3(0, 0, desired_z) - - return force.normalized() * 0.5 # 应用适当的力量 - - def create_formation_behavior(self, formation_type="custom"): - """ - 创建自定义队形行为 - """ - print(f"创建自定义队形行为: {formation_type}") - - self.formation_type = formation_type - - # 启用队形行为 - self.swarm_manager.config.set("custom_formation_enabled", True) - - print("自定义队形行为创建完成") - - def calculate_formation_force(self, member): - """ - 计算队形力 - """ - if not hasattr(self, 'formation_type'): - return Vec3(0, 0, 0) - - # 根据队形类型计算期望位置 - target_position = Vec3(0, 0, 0) - - if self.formation_type == "spiral": - # 螺旋队形 - if 'index' in member: - index = member['index'] - angle = index * 0.5 - radius = 5.0 + index * 0.2 - target_position = Vec3( - math.cos(angle) * radius, - math.sin(angle) * radius, - index * 0.5 - ) - elif self.formation_type == "grid": - # 网格队形 - if 'index' in member: - index = member['index'] - grid_size = int(math.sqrt(len(self.swarm_manager.members))) - x = (index % grid_size) - grid_size/2 - y = (index // grid_size) - grid_size/2 - target_position = Vec3(x * 3, y * 3, 0) - - # 计算向期望位置的力 - if target_position.length() > 0: - direction = target_position - member['position'] - return direction.normalized() * 0.8 - - return Vec3(0, 0, 0) - -def run_custom_behavior_example(swarm_manager): - """ - 运行自定义行为示例 - """ - print("=== 自定义行为示例 ===") - - # 创建自定义行为示例 - custom_behavior = CustomBehaviorExample(swarm_manager) - - # 创建领导行为 - custom_behavior.create_leadership_behavior(2) - - # 创建吸引力行为 - attractor_pos = Vec3(20, 20, 15) - custom_behavior.create_attraction_behavior(attractor_pos, 1.5) - - # 创建排斥力行为 - repulsor_pos = Vec3(-10, -10, 10) - custom_behavior.create_repulsion_behavior(repulsor_pos, 2.0, 15.0) - +""" +自定义行为示例 +演示如何创建自定义的群体行为 +""" + +from panda3d.core import Vec3 +import math + +class CustomBehaviorExample: + """ + 自定义行为示例类 + """ + + def __init__(self, swarm_manager): + self.swarm_manager = swarm_manager + + def create_leadership_behavior(self, leader_count=3): + """ + 创建领导行为 + 指定几个成员作为领导者,其他成员跟随领导者 + """ + print(f"创建领导行为,设置{leader_count}个领导者...") + + # 重置群体 + self.swarm_manager.reset() + + # 标记领导者 + for i, member in enumerate(self.swarm_manager.members): + if i < leader_count: + member['is_leader'] = True + # 设置领导者颜色(实际应用中可能需要修改材质) + if 'node' in member: + member['node'].setColor(0, 1, 0, 1) # 绿色表示领导者 + else: + member['is_leader'] = False + if 'node' in member: + member['node'].setColor(1, 1, 1, 1) # 白色表示跟随者 + + # 配置参数 + self.swarm_manager.config.set("cohesion_weight", 1.0) + self.swarm_manager.config.set("separation_weight", 1.5) + self.swarm_manager.config.set("alignment_weight", 0.8) + + print("领导行为创建完成") + + def create_flocking_with_leaders(self, member): + """ + 为跟随者成员实现跟随领导者的行为 + """ + # 查找领导者邻居 + leaders = [m for m in self.swarm_manager.members if m.get('is_leader', False)] + + if not leaders: + return Vec3(0, 0, 0) + + # 计算到领导者的平均方向 + direction = Vec3(0, 0, 0) + for leader in leaders: + if leader != member: # 排除自己 + dir_to_leader = leader['position'] - member['position'] + distance = dir_to_leader.length() + if distance > 0: + # 距离越近,跟随力越小(避免碰撞) + direction += dir_to_leader.normalized() / (distance / 10.0) + + return direction.normalized() + + def create_attraction_behavior(self, attractor_position, attraction_strength=1.0): + """ + 创建吸引力行为 + 向指定位置吸引群体成员 + """ + print(f"创建吸引力行为,吸引点: {attractor_position}") + + # 设置吸引点 + self.attractor_position = attractor_position + self.attraction_strength = attraction_strength + + # 启用吸引行为 + self.swarm_manager.config.set("attraction_enabled", True) + + print("吸引力行为创建完成") + + def calculate_attraction_force(self, member): + """ + 计算吸引力 + """ + if not hasattr(self, 'attractor_position'): + return Vec3(0, 0, 0) + + direction = self.attractor_position - member['position'] + distance = direction.length() + + if distance > 0: + # 归一化方向并应用强度 + force = direction.normalized() * self.attraction_strength + # 距离越近,力量越小 + force *= 1.0 / (distance / 10.0) + return force + + return Vec3(0, 0, 0) + + def create_repulsion_behavior(self, repulsor_position, repulsion_strength=1.0, radius=20.0): + """ + 创建排斥力行为 + 从指定位置推开群体成员 + """ + print(f"创建排斥力行为,排斥点: {repulsor_position}") + + # 设置排斥点 + self.repulsor_position = repulsor_position + self.repulsion_strength = repulsion_strength + self.repulsion_radius = radius + + # 启用排斥行为 + self.swarm_manager.config.set("repulsion_enabled", True) + + print("排斥力行为创建完成") + + def calculate_repulsion_force(self, member): + """ + 计算排斥力 + """ + if not hasattr(self, 'repulsor_position'): + return Vec3(0, 0, 0) + + direction = member['position'] - self.repulsor_position + distance = direction.length() + + # 只在一定范围内施加排斥力 + if 0 < distance < self.repulsion_radius: + # 归一化方向并应用强度 + force = direction.normalized() * self.repulsion_strength + # 距离越近,力量越大 + force *= (self.repulsion_radius - distance) / self.repulsion_radius + return force + + return Vec3(0, 0, 0) + + def create_sine_wave_behavior(self, amplitude=5.0, frequency=0.5): + """ + 创建正弦波行为 + 群体成员沿正弦波路径移动 + """ + print("创建正弦波行为...") + + self.sine_amplitude = amplitude + self.sine_frequency = frequency + self.sine_offset = 0.0 + + # 启用正弦波行为 + self.swarm_manager.config.set("sine_wave_enabled", True) + + print("正弦波行为创建完成") + + def update_sine_wave_behavior(self, dt): + """ + 更新正弦波行为 + """ + if hasattr(self, 'sine_offset'): + self.sine_offset += dt * self.sine_frequency + + def calculate_sine_wave_force(self, member): + """ + 计算正弦波力 + """ + if not hasattr(self, 'sine_offset'): + return Vec3(0, 0, 0) + + # 基于成员的Y坐标计算期望的Z坐标 + expected_z = math.sin(member['position'].y * 0.1 + self.sine_offset) * self.sine_amplitude + + # 计算向期望位置的力 + desired_z = expected_z - member['position'].z + force = Vec3(0, 0, desired_z) + + return force.normalized() * 0.5 # 应用适当的力量 + + def create_formation_behavior(self, formation_type="custom"): + """ + 创建自定义队形行为 + """ + print(f"创建自定义队形行为: {formation_type}") + + self.formation_type = formation_type + + # 启用队形行为 + self.swarm_manager.config.set("custom_formation_enabled", True) + + print("自定义队形行为创建完成") + + def calculate_formation_force(self, member): + """ + 计算队形力 + """ + if not hasattr(self, 'formation_type'): + return Vec3(0, 0, 0) + + # 根据队形类型计算期望位置 + target_position = Vec3(0, 0, 0) + + if self.formation_type == "spiral": + # 螺旋队形 + if 'index' in member: + index = member['index'] + angle = index * 0.5 + radius = 5.0 + index * 0.2 + target_position = Vec3( + math.cos(angle) * radius, + math.sin(angle) * radius, + index * 0.5 + ) + elif self.formation_type == "grid": + # 网格队形 + if 'index' in member: + index = member['index'] + grid_size = int(math.sqrt(len(self.swarm_manager.members))) + x = (index % grid_size) - grid_size/2 + y = (index // grid_size) - grid_size/2 + target_position = Vec3(x * 3, y * 3, 0) + + # 计算向期望位置的力 + if target_position.length() > 0: + direction = target_position - member['position'] + return direction.normalized() * 0.8 + + return Vec3(0, 0, 0) + +def run_custom_behavior_example(swarm_manager): + """ + 运行自定义行为示例 + """ + print("=== 自定义行为示例 ===") + + # 创建自定义行为示例 + custom_behavior = CustomBehaviorExample(swarm_manager) + + # 创建领导行为 + custom_behavior.create_leadership_behavior(2) + + # 创建吸引力行为 + attractor_pos = Vec3(20, 20, 15) + custom_behavior.create_attraction_behavior(attractor_pos, 1.5) + + # 创建排斥力行为 + repulsor_pos = Vec3(-10, -10, 10) + custom_behavior.create_repulsion_behavior(repulsor_pos, 2.0, 15.0) + print("自定义行为示例设置完成") \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/examples/environment_demo.py b/plugins/user/swarm_intelligence/examples/environment_demo.py index 710084f0..f2055999 100644 --- a/plugins/user/swarm_intelligence/examples/environment_demo.py +++ b/plugins/user/swarm_intelligence/examples/environment_demo.py @@ -1,125 +1,125 @@ -""" -环境感知示例 -演示如何使用环境感知和响应机制 -""" - -from panda3d.core import Vec3 -import sys -import os - -# 添加插件路径 -plugin_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.path.insert(0, plugin_dir) - -from swarm_manager import SwarmManager -from config import Config - -def create_environment_demo(world): - """ - 创建环境感知演示 - """ - print("创建环境感知演示...") - - # 创建群体管理器 - swarm_manager = SwarmManager(world) - - # 配置环境感知参数 - config = swarm_manager.config - - # 启用环境感知 - config.set("environment_awareness_enabled", True) - - # 设置传感器参数 - config.set("sensor_range", 20.0) # 增加传感器范围 - config.set("sensor_accuracy", 0.95) # 提高传感器精度 - - # 设置环境响应权重 - config.set("obstacle_response_weight", 2.5) - config.set("predator_response_weight", 3.5) - config.set("resource_seek_weight", 0.8) - config.set("hazard_avoid_weight", 4.0) - - # 创建示例群体 - swarm_manager.create_example_swarm("鸟类", 30) - - # 添加环境元素 - # 1. 添加障碍物 - swarm_manager.create_obstacle(Vec3(10, 10, 5), 3.0) - swarm_manager.create_obstacle(Vec3(-15, 5, 8), 2.5) - swarm_manager.create_obstacle(Vec3(0, -20, 3), 4.0) - - # 2. 添加捕食者 - predator = swarm_manager.create_predator(Vec3(25, 0, 10)) - - # 3. 添加资源点 - if hasattr(swarm_manager, 'environment_manager'): - swarm_manager.environment_manager.add_resource(Vec3(-10, 15, 5), "食物", 100.0) - swarm_manager.environment_manager.add_resource(Vec3(20, -10, 7), "水源", 80.0) - - # 4. 添加危险区域 - if hasattr(swarm_manager, 'environment_manager'): - swarm_manager.environment_manager.add_hazard(Vec3(5, 0, 5), "有毒气体", 6.0) - swarm_manager.environment_manager.add_hazard(Vec3(-25, -15, 3), "辐射区", 8.0) - - # 设置目标点(让群体向目标移动,观察环境响应) - swarm_manager.set_target(Vec3(30, 30, 15)) - config.set("seek_enabled", True) - - print("环境感知演示创建完成") - print(f"- 群体成员数量: {len(swarm_manager.members)}") - print(f"- 障碍物数量: {len(swarm_manager.obstacles)}") - print(f"- 捕食者数量: {len(swarm_manager.predators)}") - - if hasattr(swarm_manager, 'environment_manager'): - env_info = swarm_manager.environment_manager.get_environment_info() - print(f"- 资源点数量: {len(env_info.get('resources', []))}") - print(f"- 危险区域数量: {len(env_info.get('hazards', []))}") - - return swarm_manager - -def update_environment_demo(swarm_manager, dt): - """ - 更新环境感知演示 - """ - # 更新环境时间 - if hasattr(swarm_manager, 'environment_manager'): - swarm_manager.environment_manager.advance_time(dt) - swarm_manager.environment_manager.update_environment_dynamics() - - # 可以在这里添加动态环境变化 - # 例如:移动障碍物、改变天气等 - -def demo_environment_awareness_features(): - """ - 演示环境感知特性的使用方法 - """ - print("=== 环境感知特性演示 ===") - - # 1. 传感器配置 - print("1. 传感器配置:") - print(" - sensor_range: 传感器的感知范围") - print(" - sensor_accuracy: 传感器的精度 (0.0-1.0)") - - # 2. 环境响应权重 - print("2. 环境响应权重:") - print(" - obstacle_response_weight: 避障响应权重") - print(" - predator_response_weight: 躲避捕食者响应权重") - print(" - resource_seek_weight: 寻找资源响应权重") - print(" - hazard_avoid_weight: 避开危险区域响应权重") - - # 3. 环境元素 - print("3. 环境元素:") - print(" - obstacles: 障碍物") - print(" - predators: 捕食者") - print(" - resources: 资源点") - print(" - hazards: 危险区域") - - # 4. 环境动态 - print("4. 环境动态:") - print(" - time_of_day: 时间变化") - print(" - weather: 天气变化") - print(" - resource regeneration: 资源恢复") - -if __name__ == "__main__": - # 如果直接运行此脚本,显示特性说明 +""" +环境感知示例 +演示如何使用环境感知和响应机制 +""" + +from panda3d.core import Vec3 +import sys +import os + +# 添加插件路径 +plugin_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, plugin_dir) + +from swarm_manager import SwarmManager +from config import Config + +def create_environment_demo(world): + """ + 创建环境感知演示 + """ + print("创建环境感知演示...") + + # 创建群体管理器 + swarm_manager = SwarmManager(world) + + # 配置环境感知参数 + config = swarm_manager.config + + # 启用环境感知 + config.set("environment_awareness_enabled", True) + + # 设置传感器参数 + config.set("sensor_range", 20.0) # 增加传感器范围 + config.set("sensor_accuracy", 0.95) # 提高传感器精度 + + # 设置环境响应权重 + config.set("obstacle_response_weight", 2.5) + config.set("predator_response_weight", 3.5) + config.set("resource_seek_weight", 0.8) + config.set("hazard_avoid_weight", 4.0) + + # 创建示例群体 + swarm_manager.create_example_swarm("鸟类", 30) + + # 添加环境元素 + # 1. 添加障碍物 + swarm_manager.create_obstacle(Vec3(10, 10, 5), 3.0) + swarm_manager.create_obstacle(Vec3(-15, 5, 8), 2.5) + swarm_manager.create_obstacle(Vec3(0, -20, 3), 4.0) + + # 2. 添加捕食者 + predator = swarm_manager.create_predator(Vec3(25, 0, 10)) + + # 3. 添加资源点 + if hasattr(swarm_manager, 'environment_manager'): + swarm_manager.environment_manager.add_resource(Vec3(-10, 15, 5), "食物", 100.0) + swarm_manager.environment_manager.add_resource(Vec3(20, -10, 7), "水源", 80.0) + + # 4. 添加危险区域 + if hasattr(swarm_manager, 'environment_manager'): + swarm_manager.environment_manager.add_hazard(Vec3(5, 0, 5), "有毒气体", 6.0) + swarm_manager.environment_manager.add_hazard(Vec3(-25, -15, 3), "辐射区", 8.0) + + # 设置目标点(让群体向目标移动,观察环境响应) + swarm_manager.set_target(Vec3(30, 30, 15)) + config.set("seek_enabled", True) + + print("环境感知演示创建完成") + print(f"- 群体成员数量: {len(swarm_manager.members)}") + print(f"- 障碍物数量: {len(swarm_manager.obstacles)}") + print(f"- 捕食者数量: {len(swarm_manager.predators)}") + + if hasattr(swarm_manager, 'environment_manager'): + env_info = swarm_manager.environment_manager.get_environment_info() + print(f"- 资源点数量: {len(env_info.get('resources', []))}") + print(f"- 危险区域数量: {len(env_info.get('hazards', []))}") + + return swarm_manager + +def update_environment_demo(swarm_manager, dt): + """ + 更新环境感知演示 + """ + # 更新环境时间 + if hasattr(swarm_manager, 'environment_manager'): + swarm_manager.environment_manager.advance_time(dt) + swarm_manager.environment_manager.update_environment_dynamics() + + # 可以在这里添加动态环境变化 + # 例如:移动障碍物、改变天气等 + +def demo_environment_awareness_features(): + """ + 演示环境感知特性的使用方法 + """ + print("=== 环境感知特性演示 ===") + + # 1. 传感器配置 + print("1. 传感器配置:") + print(" - sensor_range: 传感器的感知范围") + print(" - sensor_accuracy: 传感器的精度 (0.0-1.0)") + + # 2. 环境响应权重 + print("2. 环境响应权重:") + print(" - obstacle_response_weight: 避障响应权重") + print(" - predator_response_weight: 躲避捕食者响应权重") + print(" - resource_seek_weight: 寻找资源响应权重") + print(" - hazard_avoid_weight: 避开危险区域响应权重") + + # 3. 环境元素 + print("3. 环境元素:") + print(" - obstacles: 障碍物") + print(" - predators: 捕食者") + print(" - resources: 资源点") + print(" - hazards: 危险区域") + + # 4. 环境动态 + print("4. 环境动态:") + print(" - time_of_day: 时间变化") + print(" - weather: 天气变化") + print(" - resource regeneration: 资源恢复") + +if __name__ == "__main__": + # 如果直接运行此脚本,显示特性说明 demo_environment_awareness_features() \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/examples/example_scenarios.py b/plugins/user/swarm_intelligence/examples/example_scenarios.py index 80e924ef..50d176f7 100644 --- a/plugins/user/swarm_intelligence/examples/example_scenarios.py +++ b/plugins/user/swarm_intelligence/examples/example_scenarios.py @@ -1,104 +1,104 @@ -""" -群体智能插件完整示例 -演示各种群体行为和配置选项 -""" - -def basic_example(): - """ - 基础使用示例 - """ - print("=== 群体智能插件基础示例 ===") - print("1. 启用插件后,将自动创建一个包含20个成员的群体") - print("2. 群体成员会根据Boids算法进行移动") - print("3. 按F1键可以暂停/继续群体模拟") - print("4. 通过控制面板可以调节群体行为参数") - print() - -def advanced_example(): - """ - 高级使用示例 - """ - print("=== 群体智能插件高级示例 ===") - print("1. 可以通过配置文件自定义群体参数") - print("2. 支持动态添加/删除群体成员") - print("3. 可以与场景中的其他对象进行交互") - print("4. 支持自定义群体行为规则") - print() - -def scenario_examples(): - """ - 场景示例说明 - """ - print("=== 场景示例 ===") - print("1. 鸟类群体:模拟鸟类飞行行为") - print("2. 鱼群:模拟水下鱼群行为") - print("3. 昆虫群体:模拟昆虫飞行行为") - print("4. V字形队形:模拟候鸟迁徙队形") - print("5. 圆形队形:模拟环形群体行为") - print("6. 路径跟随:群体沿着预设路径移动") - print("7. 捕猎场景:群体躲避捕食者") - print("8. 避障场景:群体避开障碍物") - print() - -def configuration_examples(): - """ - 配置示例说明 - """ - print("=== 配置示例 ===") - print("1. cohesion_weight: 聚集权重,控制群体成员向中心聚集的趋势") - print("2. separation_weight: 分离权重,控制群体成员避免碰撞的趋势") - print("3. alignment_weight: 对齐权重,控制群体成员速度方向对齐的趋势") - print("4. max_speed: 最大速度,限制群体成员的移动速度") - print("5. perception_radius: 感知半径,决定群体成员能感知到邻居的距离") - print("6. obstacle_weight: 避障权重,控制避开障碍物的力量") - print("7. wander_weight: 游走权重,控制随机移动的力量") - print("8. formation_weight: 队形权重,控制保持队形的力量") - print() - -def usage_guide(): - """ - 使用指南 - """ - print("=== 使用指南 ===") - print("1. 在主程序中启用群体智能插件") - print("2. 通过控制面板调节参数观察不同效果") - print("3. 尝试不同的群体类型和行为组合") - print("4. 使用示例函数快速创建特定场景") - print("5. 通过配置管理功能保存和加载参数设置") - print() - -def api_examples(): - """ - API使用示例 - """ - print("=== API使用示例 ===") - print("# 创建群体管理器") - print("swarm_manager = SwarmManager(world)") - print() - print("# 设置群体参数") - print("swarm_manager.config.set('cohesion_weight', 1.5)") - print("swarm_manager.config.set('separation_weight', 2.0)") - print() - print("# 创建示例群体") - print("swarm_manager.create_example_swarm()") - print() - print("# 添加障碍物") - print("swarm_manager.create_obstacle(Vec3(10, 10, 5), 3.0)") - print() - print("# 添加捕食者") - print("swarm_manager.create_predator(Vec3(-10, -10, 8))") - print() - print("# 设置目标") - print("swarm_manager.set_target(Vec3(20, 20, 10))") - print() - print("# 切换暂停状态") - print("swarm_manager.toggle_pause()") - print() - -if __name__ == "__main__": - basic_example() - advanced_example() - scenario_examples() - configuration_examples() - usage_guide() +""" +群体智能插件完整示例 +演示各种群体行为和配置选项 +""" + +def basic_example(): + """ + 基础使用示例 + """ + print("=== 群体智能插件基础示例 ===") + print("1. 启用插件后,将自动创建一个包含20个成员的群体") + print("2. 群体成员会根据Boids算法进行移动") + print("3. 按F1键可以暂停/继续群体模拟") + print("4. 通过控制面板可以调节群体行为参数") + print() + +def advanced_example(): + """ + 高级使用示例 + """ + print("=== 群体智能插件高级示例 ===") + print("1. 可以通过配置文件自定义群体参数") + print("2. 支持动态添加/删除群体成员") + print("3. 可以与场景中的其他对象进行交互") + print("4. 支持自定义群体行为规则") + print() + +def scenario_examples(): + """ + 场景示例说明 + """ + print("=== 场景示例 ===") + print("1. 鸟类群体:模拟鸟类飞行行为") + print("2. 鱼群:模拟水下鱼群行为") + print("3. 昆虫群体:模拟昆虫飞行行为") + print("4. V字形队形:模拟候鸟迁徙队形") + print("5. 圆形队形:模拟环形群体行为") + print("6. 路径跟随:群体沿着预设路径移动") + print("7. 捕猎场景:群体躲避捕食者") + print("8. 避障场景:群体避开障碍物") + print() + +def configuration_examples(): + """ + 配置示例说明 + """ + print("=== 配置示例 ===") + print("1. cohesion_weight: 聚集权重,控制群体成员向中心聚集的趋势") + print("2. separation_weight: 分离权重,控制群体成员避免碰撞的趋势") + print("3. alignment_weight: 对齐权重,控制群体成员速度方向对齐的趋势") + print("4. max_speed: 最大速度,限制群体成员的移动速度") + print("5. perception_radius: 感知半径,决定群体成员能感知到邻居的距离") + print("6. obstacle_weight: 避障权重,控制避开障碍物的力量") + print("7. wander_weight: 游走权重,控制随机移动的力量") + print("8. formation_weight: 队形权重,控制保持队形的力量") + print() + +def usage_guide(): + """ + 使用指南 + """ + print("=== 使用指南 ===") + print("1. 在主程序中启用群体智能插件") + print("2. 通过控制面板调节参数观察不同效果") + print("3. 尝试不同的群体类型和行为组合") + print("4. 使用示例函数快速创建特定场景") + print("5. 通过配置管理功能保存和加载参数设置") + print() + +def api_examples(): + """ + API使用示例 + """ + print("=== API使用示例 ===") + print("# 创建群体管理器") + print("swarm_manager = SwarmManager(world)") + print() + print("# 设置群体参数") + print("swarm_manager.config.set('cohesion_weight', 1.5)") + print("swarm_manager.config.set('separation_weight', 2.0)") + print() + print("# 创建示例群体") + print("swarm_manager.create_example_swarm()") + print() + print("# 添加障碍物") + print("swarm_manager.create_obstacle(Vec3(10, 10, 5), 3.0)") + print() + print("# 添加捕食者") + print("swarm_manager.create_predator(Vec3(-10, -10, 8))") + print() + print("# 设置目标") + print("swarm_manager.set_target(Vec3(20, 20, 10))") + print() + print("# 切换暂停状态") + print("swarm_manager.toggle_pause()") + print() + +if __name__ == "__main__": + basic_example() + advanced_example() + scenario_examples() + configuration_examples() + usage_guide() api_examples() \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/examples/full_interaction_demo.py b/plugins/user/swarm_intelligence/examples/full_interaction_demo.py index 994cb568..5cea9235 100644 --- a/plugins/user/swarm_intelligence/examples/full_interaction_demo.py +++ b/plugins/user/swarm_intelligence/examples/full_interaction_demo.py @@ -1,286 +1,286 @@ -""" -群体间交互完整示例 -演示如何使用群体间交互功能创建复杂的生态系统 -""" - -from panda3d.core import Vec3 -import random - -def create_ecosystem_demo(swarm_manager): - """ - 创建生态系统演示 - 包含多个群体和复杂的交互关系 - """ - print("创建生态系统演示...") - - # 清理现有群体 - swarm_manager.cleanup() - swarm_manager.swarms.clear() - swarm_manager.swarm_id_counter = 0 - - # 创建生产者群体(植物) - print("创建生产者群体...") - producer_swarm = swarm_manager.create_example_swarm( - swarm_type="昆虫", - member_count=150, - position_offset=Vec3(0, 0, 2) - ) - - # 设置生产者群体为绿色 - for member in producer_swarm['members']: - if 'node' in member: - member['node'].setColor(0, 1, 0, 1) # 绿色 - - # 创建初级消费者群体(食草动物) - print("创建初级消费者群体...") - primary_consumer_swarm = swarm_manager.create_example_swarm( - swarm_type="鱼类", - member_count=60, - position_offset=Vec3(-10, -10, 8) - ) - - # 设置初级消费者群体为蓝色 - for member in primary_consumer_swarm['members']: - if 'node' in member: - member['node'].setColor(0, 0, 1, 1) # 蓝色 - - # 创建次级消费者群体(食肉动物) - print("创建次级消费者群体...") - secondary_consumer_swarm = swarm_manager.create_example_swarm( - swarm_type="鸟类", - member_count=20, - position_offset=Vec3(20, 20, 15) - ) - - # 设置次级消费者群体为红色 - for member in secondary_consumer_swarm['members']: - if 'node' in member: - member['node'].setColor(1, 0, 0, 1) # 红色 - - # 创建分解者群体 - print("创建分解者群体...") - decomposer_swarm = swarm_manager.create_example_swarm( - swarm_type="昆虫", - member_count=80, - position_offset=Vec3(15, -15, 3) - ) - - # 设置分解者群体为棕色 - for member in decomposer_swarm['members']: - if 'node' in member: - member['node'].setColor(0.5, 0.3, 0, 1) # 棕色 - - # 添加环境元素 - print("添加环境元素...") - # 添加水源 - swarm_manager.create_obstacle(Vec3(-20, -20, 1), 8.0) # 湖泊 - # 添加食物源 - swarm_manager.create_obstacle(Vec3(10, 10, 4), 3.0) # 果树林 - # 添加栖息地 - swarm_manager.create_obstacle(Vec3(-15, 15, 6), 5.0) # 森林 - - # 设置交互关系 - print("设置交互关系...") - # 初级消费者吃生产者(竞争关系) - swarm_manager.interaction_manager.add_interaction( - primary_consumer_swarm, - producer_swarm, - 'competitive', - 1.2 - ) - - # 次级消费者捕食初级消费者(捕食者-猎物关系) - swarm_manager.interaction_manager.add_interaction( - secondary_consumer_swarm, - primary_consumer_swarm, - 'predator_prey', - 1.5 - ) - - # 分解者与生产者合作(分解有机物,提供养分) - swarm_manager.interaction_manager.add_interaction( - decomposer_swarm, - producer_swarm, - 'cooperative', - 0.8 - ) - - # 分解者与初级消费者竞争(争夺有机物资源) - swarm_manager.interaction_manager.add_interaction( - decomposer_swarm, - primary_consumer_swarm, - 'competitive', - 0.6 - ) - - # 更新配置 - swarm_manager.config.set("interaction_enabled", True) - swarm_manager.config.set("interaction_weight", 1.0) - swarm_manager.config.set("cohesion_weight", 0.8) - swarm_manager.config.set("separation_weight", 1.2) - swarm_manager.config.set("alignment_weight", 0.6) - swarm_manager.config.set("max_speed", 6.0) - swarm_manager.config.set("perception_radius", 12.0) - - print("生态系统演示创建完成") - print(f"共创建了 {len(swarm_manager.swarms)} 个群体") - print("观察生态系统中不同群体之间的复杂交互行为") - -def create_battle_demo(swarm_manager): - """ - 创建群体对战演示 - 两个敌对群体之间的竞争 - """ - print("创建群体对战演示...") - - # 清理现有群体 - swarm_manager.cleanup() - swarm_manager.swarms.clear() - swarm_manager.swarm_id_counter = 0 - - # 创建红方群体 - print("创建红方群体...") - red_swarm = swarm_manager.create_example_swarm( - swarm_type="鸟类", - member_count=50, - position_offset=Vec3(-25, 0, 15) - ) - - # 设置红方群体为红色 - for member in red_swarm['members']: - if 'node' in member: - member['node'].setColor(1, 0, 0, 1) # 红色 - - # 创建蓝方群体 - print("创建蓝方群体...") - blue_swarm = swarm_manager.create_example_swarm( - swarm_type="鱼类", - member_count=50, - position_offset=Vec3(25, 0, 10) - ) - - # 设置蓝方群体为蓝色 - for member in blue_swarm['members']: - if 'node' in member: - member['node'].setColor(0, 0, 1, 1) # 蓝色 - - # 在中间添加一个资源点 - resource_position = Vec3(0, 0, 10) - swarm_manager.create_obstacle(resource_position, 4.0) - - # 设置敌对关系 - swarm_manager.interaction_manager.add_interaction( - red_swarm, - blue_swarm, - 'competitive', - 1.8 - ) - - # 更新配置 - swarm_manager.config.set("interaction_enabled", True) - swarm_manager.config.set("interaction_weight", 1.5) - swarm_manager.config.set("cohesion_weight", 1.2) - swarm_manager.config.set("separation_weight", 1.0) - swarm_manager.config.set("alignment_weight", 0.8) - swarm_manager.config.set("max_speed", 8.0) - swarm_manager.config.set("perception_radius", 15.0) - - print("群体对战演示创建完成") - print("观察两个敌对群体如何争夺中心资源") - -def create_symbiosis_demo(swarm_manager): - """ - 创建共生关系演示 - 两个群体相互依赖,共同生存 - """ - print("创建共生关系演示...") - - # 清理现有群体 - swarm_manager.cleanup() - swarm_manager.swarms.clear() - swarm_manager.swarm_id_counter = 0 - - # 创建植物群体 - print("创建植物群体...") - plant_swarm = swarm_manager.create_example_swarm( - swarm_type="昆虫", - member_count=100, - position_offset=Vec3(0, 0, 3) - ) - - # 设置植物群体为绿色 - for member in plant_swarm['members']: - if 'node' in member: - member['node'].setColor(0, 1, 0, 1) # 绿色 - - # 创建传粉者群体 - print("创建传粉者群体...") - pollinator_swarm = swarm_manager.create_example_swarm( - swarm_type="鸟类", - member_count=30, - position_offset=Vec3(10, 10, 12) - ) - - # 设置传粉者群体为黄色 - for member in pollinator_swarm['members']: - if 'node' in member: - member['node'].setColor(1, 1, 0, 1) # 黄色 - - # 设置共生关系 - swarm_manager.interaction_manager.add_interaction( - plant_swarm, - pollinator_swarm, - 'cooperative', - 1.5 - ) - - # 添加多个花丛 - for i in range(5): - angle = i * 2 * 3.14159 / 5 - x = 20 * math.cos(angle) - y = 20 * math.sin(angle) - swarm_manager.create_obstacle(Vec3(x, y, 5), 3.0) - - # 更新配置 - swarm_manager.config.set("interaction_enabled", True) - swarm_manager.config.set("interaction_weight", 1.2) - swarm_manager.config.set("cohesion_weight", 1.0) - swarm_manager.config.set("separation_weight", 1.3) - swarm_manager.config.set("alignment_weight", 0.7) - swarm_manager.config.set("max_speed", 5.0) - swarm_manager.config.set("perception_radius", 10.0) - swarm_manager.config.set("wander_enabled", True) - swarm_manager.config.set("wander_weight", 0.8) - - print("共生关系演示创建完成") - print("观察植物和传粉者之间的互利共生关系") - -def run_interaction_demo(swarm_manager, demo_type="ecosystem"): - """ - 运行群体间交互演示 - - :param swarm_manager: 群体管理器 - :param demo_type: 演示类型 ("ecosystem", "battle", "symbiosis") - """ - import math - - print("=== 群体间交互演示 ===") - - if demo_type == "ecosystem": - create_ecosystem_demo(swarm_manager) - elif demo_type == "battle": - create_battle_demo(swarm_manager) - elif demo_type == "symbiosis": - create_symbiosis_demo(swarm_manager) - else: - print(f"未知的演示类型: {demo_type}") - return - - # 启用交互功能 - swarm_manager.config.set("interaction_enabled", True) - - print(f"\n{demo_type} 演示已启动") - print("使用控制面板可以调整交互参数") - print("观察不同群体之间的复杂交互行为") - +""" +群体间交互完整示例 +演示如何使用群体间交互功能创建复杂的生态系统 +""" + +from panda3d.core import Vec3 +import random + +def create_ecosystem_demo(swarm_manager): + """ + 创建生态系统演示 + 包含多个群体和复杂的交互关系 + """ + print("创建生态系统演示...") + + # 清理现有群体 + swarm_manager.cleanup() + swarm_manager.swarms.clear() + swarm_manager.swarm_id_counter = 0 + + # 创建生产者群体(植物) + print("创建生产者群体...") + producer_swarm = swarm_manager.create_example_swarm( + swarm_type="昆虫", + member_count=150, + position_offset=Vec3(0, 0, 2) + ) + + # 设置生产者群体为绿色 + for member in producer_swarm['members']: + if 'node' in member: + member['node'].setColor(0, 1, 0, 1) # 绿色 + + # 创建初级消费者群体(食草动物) + print("创建初级消费者群体...") + primary_consumer_swarm = swarm_manager.create_example_swarm( + swarm_type="鱼类", + member_count=60, + position_offset=Vec3(-10, -10, 8) + ) + + # 设置初级消费者群体为蓝色 + for member in primary_consumer_swarm['members']: + if 'node' in member: + member['node'].setColor(0, 0, 1, 1) # 蓝色 + + # 创建次级消费者群体(食肉动物) + print("创建次级消费者群体...") + secondary_consumer_swarm = swarm_manager.create_example_swarm( + swarm_type="鸟类", + member_count=20, + position_offset=Vec3(20, 20, 15) + ) + + # 设置次级消费者群体为红色 + for member in secondary_consumer_swarm['members']: + if 'node' in member: + member['node'].setColor(1, 0, 0, 1) # 红色 + + # 创建分解者群体 + print("创建分解者群体...") + decomposer_swarm = swarm_manager.create_example_swarm( + swarm_type="昆虫", + member_count=80, + position_offset=Vec3(15, -15, 3) + ) + + # 设置分解者群体为棕色 + for member in decomposer_swarm['members']: + if 'node' in member: + member['node'].setColor(0.5, 0.3, 0, 1) # 棕色 + + # 添加环境元素 + print("添加环境元素...") + # 添加水源 + swarm_manager.create_obstacle(Vec3(-20, -20, 1), 8.0) # 湖泊 + # 添加食物源 + swarm_manager.create_obstacle(Vec3(10, 10, 4), 3.0) # 果树林 + # 添加栖息地 + swarm_manager.create_obstacle(Vec3(-15, 15, 6), 5.0) # 森林 + + # 设置交互关系 + print("设置交互关系...") + # 初级消费者吃生产者(竞争关系) + swarm_manager.interaction_manager.add_interaction( + primary_consumer_swarm, + producer_swarm, + 'competitive', + 1.2 + ) + + # 次级消费者捕食初级消费者(捕食者-猎物关系) + swarm_manager.interaction_manager.add_interaction( + secondary_consumer_swarm, + primary_consumer_swarm, + 'predator_prey', + 1.5 + ) + + # 分解者与生产者合作(分解有机物,提供养分) + swarm_manager.interaction_manager.add_interaction( + decomposer_swarm, + producer_swarm, + 'cooperative', + 0.8 + ) + + # 分解者与初级消费者竞争(争夺有机物资源) + swarm_manager.interaction_manager.add_interaction( + decomposer_swarm, + primary_consumer_swarm, + 'competitive', + 0.6 + ) + + # 更新配置 + swarm_manager.config.set("interaction_enabled", True) + swarm_manager.config.set("interaction_weight", 1.0) + swarm_manager.config.set("cohesion_weight", 0.8) + swarm_manager.config.set("separation_weight", 1.2) + swarm_manager.config.set("alignment_weight", 0.6) + swarm_manager.config.set("max_speed", 6.0) + swarm_manager.config.set("perception_radius", 12.0) + + print("生态系统演示创建完成") + print(f"共创建了 {len(swarm_manager.swarms)} 个群体") + print("观察生态系统中不同群体之间的复杂交互行为") + +def create_battle_demo(swarm_manager): + """ + 创建群体对战演示 + 两个敌对群体之间的竞争 + """ + print("创建群体对战演示...") + + # 清理现有群体 + swarm_manager.cleanup() + swarm_manager.swarms.clear() + swarm_manager.swarm_id_counter = 0 + + # 创建红方群体 + print("创建红方群体...") + red_swarm = swarm_manager.create_example_swarm( + swarm_type="鸟类", + member_count=50, + position_offset=Vec3(-25, 0, 15) + ) + + # 设置红方群体为红色 + for member in red_swarm['members']: + if 'node' in member: + member['node'].setColor(1, 0, 0, 1) # 红色 + + # 创建蓝方群体 + print("创建蓝方群体...") + blue_swarm = swarm_manager.create_example_swarm( + swarm_type="鱼类", + member_count=50, + position_offset=Vec3(25, 0, 10) + ) + + # 设置蓝方群体为蓝色 + for member in blue_swarm['members']: + if 'node' in member: + member['node'].setColor(0, 0, 1, 1) # 蓝色 + + # 在中间添加一个资源点 + resource_position = Vec3(0, 0, 10) + swarm_manager.create_obstacle(resource_position, 4.0) + + # 设置敌对关系 + swarm_manager.interaction_manager.add_interaction( + red_swarm, + blue_swarm, + 'competitive', + 1.8 + ) + + # 更新配置 + swarm_manager.config.set("interaction_enabled", True) + swarm_manager.config.set("interaction_weight", 1.5) + swarm_manager.config.set("cohesion_weight", 1.2) + swarm_manager.config.set("separation_weight", 1.0) + swarm_manager.config.set("alignment_weight", 0.8) + swarm_manager.config.set("max_speed", 8.0) + swarm_manager.config.set("perception_radius", 15.0) + + print("群体对战演示创建完成") + print("观察两个敌对群体如何争夺中心资源") + +def create_symbiosis_demo(swarm_manager): + """ + 创建共生关系演示 + 两个群体相互依赖,共同生存 + """ + print("创建共生关系演示...") + + # 清理现有群体 + swarm_manager.cleanup() + swarm_manager.swarms.clear() + swarm_manager.swarm_id_counter = 0 + + # 创建植物群体 + print("创建植物群体...") + plant_swarm = swarm_manager.create_example_swarm( + swarm_type="昆虫", + member_count=100, + position_offset=Vec3(0, 0, 3) + ) + + # 设置植物群体为绿色 + for member in plant_swarm['members']: + if 'node' in member: + member['node'].setColor(0, 1, 0, 1) # 绿色 + + # 创建传粉者群体 + print("创建传粉者群体...") + pollinator_swarm = swarm_manager.create_example_swarm( + swarm_type="鸟类", + member_count=30, + position_offset=Vec3(10, 10, 12) + ) + + # 设置传粉者群体为黄色 + for member in pollinator_swarm['members']: + if 'node' in member: + member['node'].setColor(1, 1, 0, 1) # 黄色 + + # 设置共生关系 + swarm_manager.interaction_manager.add_interaction( + plant_swarm, + pollinator_swarm, + 'cooperative', + 1.5 + ) + + # 添加多个花丛 + for i in range(5): + angle = i * 2 * 3.14159 / 5 + x = 20 * math.cos(angle) + y = 20 * math.sin(angle) + swarm_manager.create_obstacle(Vec3(x, y, 5), 3.0) + + # 更新配置 + swarm_manager.config.set("interaction_enabled", True) + swarm_manager.config.set("interaction_weight", 1.2) + swarm_manager.config.set("cohesion_weight", 1.0) + swarm_manager.config.set("separation_weight", 1.3) + swarm_manager.config.set("alignment_weight", 0.7) + swarm_manager.config.set("max_speed", 5.0) + swarm_manager.config.set("perception_radius", 10.0) + swarm_manager.config.set("wander_enabled", True) + swarm_manager.config.set("wander_weight", 0.8) + + print("共生关系演示创建完成") + print("观察植物和传粉者之间的互利共生关系") + +def run_interaction_demo(swarm_manager, demo_type="ecosystem"): + """ + 运行群体间交互演示 + + :param swarm_manager: 群体管理器 + :param demo_type: 演示类型 ("ecosystem", "battle", "symbiosis") + """ + import math + + print("=== 群体间交互演示 ===") + + if demo_type == "ecosystem": + create_ecosystem_demo(swarm_manager) + elif demo_type == "battle": + create_battle_demo(swarm_manager) + elif demo_type == "symbiosis": + create_symbiosis_demo(swarm_manager) + else: + print(f"未知的演示类型: {demo_type}") + return + + # 启用交互功能 + swarm_manager.config.set("interaction_enabled", True) + + print(f"\n{demo_type} 演示已启动") + print("使用控制面板可以调整交互参数") + print("观察不同群体之间的复杂交互行为") + return demo_type \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/examples/interaction_example.py b/plugins/user/swarm_intelligence/examples/interaction_example.py index c397b4c5..6c389224 100644 --- a/plugins/user/swarm_intelligence/examples/interaction_example.py +++ b/plugins/user/swarm_intelligence/examples/interaction_example.py @@ -1,289 +1,289 @@ -""" -群体间交互示例 -演示不同群体之间的交互行为 -""" - -from panda3d.core import Vec3 - -def create_cooperative_swarms(swarm_manager): - """ - 创建合作群体 - 两个群体相互合作,共同完成任务 - """ - print("创建合作群体...") - - # 创建两个不同类型的群体 - swarm_configs = [ - { - 'type': '鸟类', - 'count': 30, - 'position_offset': Vec3(-20, -20, 10) - }, - { - 'type': '昆虫', - 'count': 50, - 'position_offset': Vec3(20, 20, 5) - } - ] - - swarm_manager.create_multiple_swarms(swarm_configs) - - # 添加合作交互关系 - if len(swarm_manager.swarms) >= 2: - swarm_manager.interaction_manager.add_interaction( - swarm_manager.swarms[0], - swarm_manager.swarms[1], - 'cooperative', - 0.8 - ) - - print("合作群体创建完成") - -def create_competitive_swarms(swarm_manager): - """ - 创建竞争群体 - 两个群体争夺同一资源 - """ - print("创建竞争群体...") - - # 创建两个相同类型的群体 - swarm_configs = [ - { - 'type': '鱼类', - 'count': 25, - 'position_offset': Vec3(-15, 0, 8) - }, - { - 'type': '鱼类', - 'count': 25, - 'position_offset': Vec3(15, 0, 8) - } - ] - - swarm_manager.create_multiple_swarms(swarm_configs) - - # 添加竞争交互关系 - if len(swarm_manager.swarms) >= 2: - swarm_manager.interaction_manager.add_interaction( - swarm_manager.swarms[0], - swarm_manager.swarms[1], - 'competitive', - 1.0 - ) - - # 在中心添加一个资源点 - resource_position = Vec3(0, 0, 10) - swarm_manager.create_obstacle(resource_position, 3.0) - - print("竞争群体创建完成") - -def create_predator_prey_swarms(swarm_manager): - """ - 创建捕食者-猎物群体 - 一个群体捕食另一个群体 - """ - print("创建捕食者-猎物群体...") - - # 创建捕食者和猎物群体 - swarm_configs = [ - { - 'type': '鸟类', # 捕食者 - 'count': 10, - 'position_offset': Vec3(-25, -25, 15) - }, - { - 'type': '昆虫', # 猎物 - 'count': 80, - 'position_offset': Vec3(0, 0, 5) - } - ] - - swarm_manager.create_multiple_swarms(swarm_configs) - - # 添加捕食者-猎物交互关系 - if len(swarm_manager.swarms) >= 2: - swarm_manager.interaction_manager.add_interaction( - swarm_manager.swarms[0], # 捕食者 - swarm_manager.swarms[1], # 猎物 - 'predator_prey', - 1.2 - ) - - print("捕食者-猎物群体创建完成") - -def create_complex_ecosystem(swarm_manager): - """ - 创建复杂生态系统 - 包含多个群体和多种交互关系 - """ - print("创建复杂生态系统...") - - # 创建多个群体 - swarm_configs = [ - { - 'type': '鸟类', # 猎食者 - 'count': 15, - 'position_offset': Vec3(-30, -30, 20) - }, - { - 'type': '鱼类', # 中级消费者 - 'count': 40, - 'position_offset': Vec3(0, -20, 10) - }, - { - 'type': '昆虫', # 初级消费者 - 'count': 100, - 'position_offset': Vec3(20, 10, 5) - }, - { - 'type': '鸟类', # 分解者协作者 - 'count': 20, - 'position_offset': Vec3(30, 30, 15) - } - ] - - swarm_manager.create_multiple_swarms(swarm_configs) - - # 添加复杂的交互关系 - if len(swarm_manager.swarms) >= 4: - # 鸟类捕食鱼类 - swarm_manager.interaction_manager.add_interaction( - swarm_manager.swarms[0], - swarm_manager.swarms[1], - 'predator_prey', - 1.0 - ) - - # 鱼类捕食昆虫 - swarm_manager.interaction_manager.add_interaction( - swarm_manager.swarms[1], - swarm_manager.swarms[2], - 'predator_prey', - 0.8 - ) - - # 鸟类与分解者协作者合作 - swarm_manager.interaction_manager.add_interaction( - swarm_manager.swarms[0], - swarm_manager.swarms[3], - 'cooperative', - 0.6 - ) - - # 昆虫与分解者协作者竞争资源 - swarm_manager.interaction_manager.add_interaction( - swarm_manager.swarms[2], - swarm_manager.swarms[3], - 'competitive', - 0.7 - ) - - # 添加环境元素 - # 食物源 - swarm_manager.create_obstacle(Vec3(10, 0, 8), 4.0) - swarm_manager.create_obstacle(Vec3(-10, 20, 6), 3.0) - - # 障碍物 - swarm_manager.create_obstacle(Vec3(-15, -10, 12), 5.0) - swarm_manager.create_obstacle(Vec3(25, -15, 10), 6.0) - - print("复杂生态系统创建完成") - -def create_dynamic_interactions(swarm_manager): - """ - 创建动态交互场景 - 交互关系随时间变化 - """ - print("创建动态交互场景...") - - # 创建两个群体 - swarm_configs = [ - { - 'type': '鸟类', - 'count': 30, - 'position_offset': Vec3(-20, 0, 15) - }, - { - 'type': '鱼类', - 'count': 30, - 'position_offset': Vec3(20, 0, 10) - } - ] - - swarm_manager.create_multiple_swarms(swarm_configs) - - # 初始为合作关系 - if len(swarm_manager.swarms) >= 2: - swarm_manager.interaction_manager.add_interaction( - swarm_manager.swarms[0], - swarm_manager.swarms[1], - 'cooperative', - 0.8 - ) - - # 设置动态交互变化 - swarm_manager.dynamic_interactions = { - 'time_points': [0, 30, 60, 90], # 时间点(秒) - 'interactions': [ - ('cooperative', 0.8), - ('competitive', 1.0), - ('predator_prey', 1.2), # 第一个群体捕食第二个群体 - ('cooperative', 0.6) - ] - } - - print("动态交互场景创建完成") - -def update_dynamic_interactions(swarm_manager, elapsed_time): - """ - 更新动态交互关系 - """ - if hasattr(swarm_manager, 'dynamic_interactions'): - time_points = swarm_manager.dynamic_interactions['time_points'] - interactions = swarm_manager.dynamic_interactions['interactions'] - - # 找到当前时间点 - current_index = 0 - for i, time_point in enumerate(time_points): - if elapsed_time >= time_point: - current_index = i - else: - break - - if current_index < len(interactions): - interaction_type, strength = interactions[current_index] - - # 更新交互关系 - if len(swarm_manager.swarms) >= 2: - # 清除现有交互 - swarm_manager.interaction_manager.clear_interactions() - - # 添加新交互 - swarm_manager.interaction_manager.add_interaction( - swarm_manager.swarms[0], - swarm_manager.swarms[1], - interaction_type, - strength - ) - - print(f"交互关系更新为: {interaction_type} (强度: {strength})") - -def run_interaction_example(swarm_manager): - """ - 运行交互示例 - """ - print("=== 群体间交互示例 ===") - - # 创建复杂生态系统 - create_complex_ecosystem(swarm_manager) - - # 启用相关配置 - swarm_manager.config.set("interaction_weight", 1.0) - swarm_manager.config.set("cohesion_weight", 1.0) - swarm_manager.config.set("separation_weight", 1.5) - swarm_manager.config.set("alignment_weight", 1.0) - - print("交互示例设置完成") - print("观察不同群体之间的复杂交互行为") - +""" +群体间交互示例 +演示不同群体之间的交互行为 +""" + +from panda3d.core import Vec3 + +def create_cooperative_swarms(swarm_manager): + """ + 创建合作群体 + 两个群体相互合作,共同完成任务 + """ + print("创建合作群体...") + + # 创建两个不同类型的群体 + swarm_configs = [ + { + 'type': '鸟类', + 'count': 30, + 'position_offset': Vec3(-20, -20, 10) + }, + { + 'type': '昆虫', + 'count': 50, + 'position_offset': Vec3(20, 20, 5) + } + ] + + swarm_manager.create_multiple_swarms(swarm_configs) + + # 添加合作交互关系 + if len(swarm_manager.swarms) >= 2: + swarm_manager.interaction_manager.add_interaction( + swarm_manager.swarms[0], + swarm_manager.swarms[1], + 'cooperative', + 0.8 + ) + + print("合作群体创建完成") + +def create_competitive_swarms(swarm_manager): + """ + 创建竞争群体 + 两个群体争夺同一资源 + """ + print("创建竞争群体...") + + # 创建两个相同类型的群体 + swarm_configs = [ + { + 'type': '鱼类', + 'count': 25, + 'position_offset': Vec3(-15, 0, 8) + }, + { + 'type': '鱼类', + 'count': 25, + 'position_offset': Vec3(15, 0, 8) + } + ] + + swarm_manager.create_multiple_swarms(swarm_configs) + + # 添加竞争交互关系 + if len(swarm_manager.swarms) >= 2: + swarm_manager.interaction_manager.add_interaction( + swarm_manager.swarms[0], + swarm_manager.swarms[1], + 'competitive', + 1.0 + ) + + # 在中心添加一个资源点 + resource_position = Vec3(0, 0, 10) + swarm_manager.create_obstacle(resource_position, 3.0) + + print("竞争群体创建完成") + +def create_predator_prey_swarms(swarm_manager): + """ + 创建捕食者-猎物群体 + 一个群体捕食另一个群体 + """ + print("创建捕食者-猎物群体...") + + # 创建捕食者和猎物群体 + swarm_configs = [ + { + 'type': '鸟类', # 捕食者 + 'count': 10, + 'position_offset': Vec3(-25, -25, 15) + }, + { + 'type': '昆虫', # 猎物 + 'count': 80, + 'position_offset': Vec3(0, 0, 5) + } + ] + + swarm_manager.create_multiple_swarms(swarm_configs) + + # 添加捕食者-猎物交互关系 + if len(swarm_manager.swarms) >= 2: + swarm_manager.interaction_manager.add_interaction( + swarm_manager.swarms[0], # 捕食者 + swarm_manager.swarms[1], # 猎物 + 'predator_prey', + 1.2 + ) + + print("捕食者-猎物群体创建完成") + +def create_complex_ecosystem(swarm_manager): + """ + 创建复杂生态系统 + 包含多个群体和多种交互关系 + """ + print("创建复杂生态系统...") + + # 创建多个群体 + swarm_configs = [ + { + 'type': '鸟类', # 猎食者 + 'count': 15, + 'position_offset': Vec3(-30, -30, 20) + }, + { + 'type': '鱼类', # 中级消费者 + 'count': 40, + 'position_offset': Vec3(0, -20, 10) + }, + { + 'type': '昆虫', # 初级消费者 + 'count': 100, + 'position_offset': Vec3(20, 10, 5) + }, + { + 'type': '鸟类', # 分解者协作者 + 'count': 20, + 'position_offset': Vec3(30, 30, 15) + } + ] + + swarm_manager.create_multiple_swarms(swarm_configs) + + # 添加复杂的交互关系 + if len(swarm_manager.swarms) >= 4: + # 鸟类捕食鱼类 + swarm_manager.interaction_manager.add_interaction( + swarm_manager.swarms[0], + swarm_manager.swarms[1], + 'predator_prey', + 1.0 + ) + + # 鱼类捕食昆虫 + swarm_manager.interaction_manager.add_interaction( + swarm_manager.swarms[1], + swarm_manager.swarms[2], + 'predator_prey', + 0.8 + ) + + # 鸟类与分解者协作者合作 + swarm_manager.interaction_manager.add_interaction( + swarm_manager.swarms[0], + swarm_manager.swarms[3], + 'cooperative', + 0.6 + ) + + # 昆虫与分解者协作者竞争资源 + swarm_manager.interaction_manager.add_interaction( + swarm_manager.swarms[2], + swarm_manager.swarms[3], + 'competitive', + 0.7 + ) + + # 添加环境元素 + # 食物源 + swarm_manager.create_obstacle(Vec3(10, 0, 8), 4.0) + swarm_manager.create_obstacle(Vec3(-10, 20, 6), 3.0) + + # 障碍物 + swarm_manager.create_obstacle(Vec3(-15, -10, 12), 5.0) + swarm_manager.create_obstacle(Vec3(25, -15, 10), 6.0) + + print("复杂生态系统创建完成") + +def create_dynamic_interactions(swarm_manager): + """ + 创建动态交互场景 + 交互关系随时间变化 + """ + print("创建动态交互场景...") + + # 创建两个群体 + swarm_configs = [ + { + 'type': '鸟类', + 'count': 30, + 'position_offset': Vec3(-20, 0, 15) + }, + { + 'type': '鱼类', + 'count': 30, + 'position_offset': Vec3(20, 0, 10) + } + ] + + swarm_manager.create_multiple_swarms(swarm_configs) + + # 初始为合作关系 + if len(swarm_manager.swarms) >= 2: + swarm_manager.interaction_manager.add_interaction( + swarm_manager.swarms[0], + swarm_manager.swarms[1], + 'cooperative', + 0.8 + ) + + # 设置动态交互变化 + swarm_manager.dynamic_interactions = { + 'time_points': [0, 30, 60, 90], # 时间点(秒) + 'interactions': [ + ('cooperative', 0.8), + ('competitive', 1.0), + ('predator_prey', 1.2), # 第一个群体捕食第二个群体 + ('cooperative', 0.6) + ] + } + + print("动态交互场景创建完成") + +def update_dynamic_interactions(swarm_manager, elapsed_time): + """ + 更新动态交互关系 + """ + if hasattr(swarm_manager, 'dynamic_interactions'): + time_points = swarm_manager.dynamic_interactions['time_points'] + interactions = swarm_manager.dynamic_interactions['interactions'] + + # 找到当前时间点 + current_index = 0 + for i, time_point in enumerate(time_points): + if elapsed_time >= time_point: + current_index = i + else: + break + + if current_index < len(interactions): + interaction_type, strength = interactions[current_index] + + # 更新交互关系 + if len(swarm_manager.swarms) >= 2: + # 清除现有交互 + swarm_manager.interaction_manager.clear_interactions() + + # 添加新交互 + swarm_manager.interaction_manager.add_interaction( + swarm_manager.swarms[0], + swarm_manager.swarms[1], + interaction_type, + strength + ) + + print(f"交互关系更新为: {interaction_type} (强度: {strength})") + +def run_interaction_example(swarm_manager): + """ + 运行交互示例 + """ + print("=== 群体间交互示例 ===") + + # 创建复杂生态系统 + create_complex_ecosystem(swarm_manager) + + # 启用相关配置 + swarm_manager.config.set("interaction_weight", 1.0) + swarm_manager.config.set("cohesion_weight", 1.0) + swarm_manager.config.set("separation_weight", 1.5) + swarm_manager.config.set("alignment_weight", 1.0) + + print("交互示例设置完成") + print("观察不同群体之间的复杂交互行为") + return update_dynamic_interactions \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/examples/learning_demo.py b/plugins/user/swarm_intelligence/examples/learning_demo.py index 603e9841..bb2c0fd1 100644 --- a/plugins/user/swarm_intelligence/examples/learning_demo.py +++ b/plugins/user/swarm_intelligence/examples/learning_demo.py @@ -1,317 +1,317 @@ -""" -群体学习能力示例 -演示群体的学习和适应能力 -""" - -from panda3d.core import Vec3 -import random - -def create_learning_demo(swarm_manager): - """ - 创建学习能力演示 - 群体学习如何在复杂环境中生存和适应 - """ - print("创建学习能力演示...") - - # 清理现有群体 - swarm_manager.cleanup() - swarm_manager.swarms.clear() - swarm_manager.swarm_id_counter = 0 - - # 创建学习型群体 - print("创建学习型群体...") - learning_swarm = swarm_manager.create_example_swarm( - swarm_type="鸟类", - member_count=40, - position_offset=Vec3(0, 0, 10) - ) - - # 设置学习型群体为紫色 - for member in learning_swarm['members']: - if 'node' in member: - member['node'].setColor(0.5, 0, 0.5, 1) # 紫色 - - # 创建复杂环境 - print("创建复杂环境...") - - # 添加多个障碍物 - obstacles = [ - (Vec3(-15, -15, 8), 4.0), # 大障碍物 - (Vec3(15, 15, 12), 3.0), # 中等障碍物 - (Vec3(-10, 10, 6), 2.0), # 小障碍物 - (Vec3(10, -10, 10), 3.5), # 中等障碍物 - (Vec3(0, 0, 15), 2.5), # 中心障碍物 - ] - - for position, radius in obstacles: - swarm_manager.create_obstacle(position, radius) - - # 添加移动障碍物 - moving_obstacle = { - 'position': Vec3(-20, 0, 10), - 'radius': 3.0, - 'velocity': Vec3(2, 1, 0), - 'bounds': {'min_x': -25, 'max_x': 25, 'min_y': -25, 'max_y': 25} - } - - if not hasattr(swarm_manager, 'moving_obstacles'): - swarm_manager.moving_obstacles = [] - swarm_manager.moving_obstacles.append(moving_obstacle) - - # 添加资源点 - resources = [ - (Vec3(-25, -25, 5), 2.0), # 资源点1 - (Vec3(25, 25, 8), 2.5), # 资源点2 - (Vec3(-25, 25, 6), 1.5), # 资源点3 - ] - - for position, radius in resources: - swarm_manager.create_obstacle(position, radius) - - # 配置学习参数 - print("配置学习参数...") - swarm_manager.config.set("learning_enabled", True) - swarm_manager.config.set("learning_rate", 0.1) - swarm_manager.config.set("exploration_rate", 0.3) - swarm_manager.config.set("memory_capacity", 500) - - # 调整Boids参数以促进学习 - swarm_manager.config.set("cohesion_weight", 0.8) - swarm_manager.config.set("separation_weight", 1.2) - swarm_manager.config.set("alignment_weight", 0.6) - swarm_manager.config.set("max_speed", 6.0) - swarm_manager.config.set("perception_radius", 12.0) - - # 启用避障和边界限制 - swarm_manager.config.set("obstacle_avoidance_enabled", True) - swarm_manager.config.set("boundary_enabled", True) - swarm_manager.config.set("boundary_weight", 2.0) - - print("学习能力演示创建完成") - print("观察群体如何通过学习适应复杂环境") - print("群体将学会避开障碍物并寻找资源") - -def create_adaptation_demo(swarm_manager): - """ - 创建环境适应演示 - 群体学习适应不同的环境条件 - """ - print("创建环境适应演示...") - - # 清理现有群体 - swarm_manager.cleanup() - swarm_manager.swarms.clear() - swarm_manager.swarm_id_counter = 0 - - # 创建适应型群体 - print("创建适应型群体...") - adaptive_swarm = swarm_manager.create_example_swarm( - swarm_type="鱼类", - member_count=60, - position_offset=Vec3(0, 0, 8) - ) - - # 设置适应型群体为蓝色 - for member in adaptive_swarm['members']: - if 'node' in member: - member['node'].setColor(0, 0, 1, 1) # 蓝色 - - # 创建变化的环境 - print("创建变化环境...") - - # 添加多个区域 - # 安全区 - safe_zone = swarm_manager.create_obstacle(Vec3(0, 0, 5), 8.0) - - # 危险区 - danger_zone = swarm_manager.create_obstacle(Vec3(15, 15, 10), 5.0) - - # 资源丰富区 - resource_zone = swarm_manager.create_obstacle(Vec3(-15, -15, 6), 6.0) - - # 配置学习参数 - print("配置适应参数...") - swarm_manager.config.set("learning_enabled", True) - swarm_manager.config.set("learning_rate", 0.15) - swarm_manager.config.set("exploration_rate", 0.25) - swarm_manager.config.set("memory_capacity", 800) - - # 设置环境变化 - swarm_manager.environment_changes = { - 'time_points': [0, 60, 120, 180], # 时间点(秒) - 'conditions': [ - {'threat_level': 0.2, 'resource_abundance': 0.8}, # 安全且资源丰富 - {'threat_level': 0.7, 'resource_abundance': 0.3}, # 危险且资源稀缺 - {'threat_level': 0.4, 'resource_abundance': 0.6}, # 中等条件 - {'threat_level': 0.1, 'resource_abundance': 0.9}, # 安全且资源丰富 - ] - } - - # 调整参数以促进适应 - swarm_manager.config.set("cohesion_weight", 1.0) - swarm_manager.config.set("separation_weight", 1.3) - swarm_manager.config.set("alignment_weight", 0.7) - swarm_manager.config.set("max_speed", 5.0) - swarm_manager.config.set("perception_radius", 10.0) - - # 启用相关行为 - swarm_manager.config.set("obstacle_avoidance_enabled", True) - swarm_manager.config.set("boundary_enabled", True) - swarm_manager.config.set("wander_enabled", True) - swarm_manager.config.set("wander_weight", 0.5) - - print("环境适应演示创建完成") - print("观察群体如何适应变化的环境条件") - print("群体将学会在不同条件下调整行为策略") - -def create_multi_task_learning_demo(swarm_manager): - """ - 创建多任务学习演示 - 群体学习执行多个任务 - """ - print("创建多任务学习演示...") - - # 清理现有群体 - swarm_manager.cleanup() - swarm_manager.swarms.clear() - swarm_manager.swarm_id_counter = 0 - - # 创建多任务群体 - print("创建多任务群体...") - multi_task_swarm = swarm_manager.create_example_swarm( - swarm_type="昆虫", - member_count=100, - position_offset=Vec3(0, 0, 3) - ) - - # 设置多任务群体为绿色 - for member in multi_task_swarm['members']: - if 'node' in member: - member['node'].setColor(0, 1, 0, 1) # 绿色 - - # 创建多个任务区域 - print("创建任务区域...") - - # 搜寻任务区域 - search_area = swarm_manager.create_obstacle(Vec3(-20, 0, 4), 5.0) - - # 运输任务区域 - transport_area = swarm_manager.create_obstacle(Vec3(20, 0, 4), 4.0) - - # 建造任务区域 - build_area = swarm_manager.create_obstacle(Vec3(0, 20, 5), 6.0) - - # 防御任务区域 - defense_area = swarm_manager.create_obstacle(Vec3(0, -20, 3), 5.0) - - # 配置学习参数 - print("配置多任务学习参数...") - swarm_manager.config.set("learning_enabled", True) - swarm_manager.config.set("learning_rate", 0.12) - swarm_manager.config.set("exploration_rate", 0.35) - swarm_manager.config.set("memory_capacity", 1000) - - # 设置任务轮换 - swarm_manager.task_rotation = { - 'time_intervals': 45, # 每45秒切换任务 - 'tasks': ['search', 'transport', 'build', 'defense'], - 'current_task_index': 0 - } - - # 调整参数以适应多任务 - swarm_manager.config.set("cohesion_weight", 0.9) - swarm_manager.config.set("separation_weight", 1.1) - swarm_manager.config.set("alignment_weight", 0.8) - swarm_manager.config.set("max_speed", 7.0) - swarm_manager.config.set("perception_radius", 15.0) - - # 启用相关行为 - swarm_manager.config.set("obstacle_avoidance_enabled", True) - swarm_manager.config.set("boundary_enabled", True) - swarm_manager.config.set("wander_enabled", True) - swarm_manager.config.set("wander_weight", 0.7) - swarm_manager.config.set("path_following_enabled", True) - - print("多任务学习演示创建完成") - print("观察群体如何学习执行不同的任务") - print("群体将学会在不同任务间切换并优化策略") - -def update_dynamic_environment(swarm_manager, elapsed_time): - """ - 更新动态环境 - """ - # 更新移动障碍物 - if hasattr(swarm_manager, 'moving_obstacles'): - for obstacle in swarm_manager.moving_obstacles: - # 移动障碍物 - obstacle['position'] += obstacle['velocity'] * 0.1 # 时间步长 - - # 边界反弹 - bounds = obstacle['bounds'] - if obstacle['position'].x < bounds['min_x'] or obstacle['position'].x > bounds['max_x']: - obstacle['velocity'].x *= -1 - if obstacle['position'].y < bounds['min_y'] or obstacle['position'].y > bounds['max_y']: - obstacle['velocity'].y *= -1 - - # 保持在边界内 - obstacle['position'].x = max(bounds['min_x'], min(bounds['max_x'], obstacle['position'].x)) - obstacle['position'].y = max(bounds['min_y'], min(bounds['max_y'], obstacle['position'].y)) - - # 更新环境条件变化 - if hasattr(swarm_manager, 'environment_changes'): - time_points = swarm_manager.environment_changes['time_points'] - conditions = swarm_manager.environment_changes['conditions'] - - # 找到当前时间点 - current_index = 0 - for i, time_point in enumerate(time_points): - if elapsed_time >= time_point: - current_index = i - else: - break - - if current_index < len(conditions): - condition = conditions[current_index] - - # 这里可以更新环境可视化或其他相关参数 - print(f"环境条件更新: 威胁等级 {condition['threat_level']}, 资源丰富度 {condition['resource_abundance']}") - - # 更新任务轮换 - if hasattr(swarm_manager, 'task_rotation'): - interval = swarm_manager.task_rotation['time_intervals'] - tasks = swarm_manager.task_rotation['tasks'] - - # 计算当前任务索引 - current_task_index = int(elapsed_time // interval) % len(tasks) - - if current_task_index != swarm_manager.task_rotation['current_task_index']: - swarm_manager.task_rotation['current_task_index'] = current_task_index - print(f"任务切换到: {tasks[current_task_index]}") - -def run_learning_demo(swarm_manager, demo_type="basic"): - """ - 运行学习能力演示 - - :param swarm_manager: 群体管理器 - :param demo_type: 演示类型 ("basic", "adaptation", "multi_task") - """ - print("=== 群体学习能力演示 ===") - - if demo_type == "basic": - create_learning_demo(swarm_manager) - elif demo_type == "adaptation": - create_adaptation_demo(swarm_manager) - elif demo_type == "multi_task": - create_multi_task_learning_demo(swarm_manager) - else: - print(f"未知的演示类型: {demo_type}") - return - - # 启用学习功能 - swarm_manager.config.set("learning_enabled", True) - - print(f"\n{demo_type} 学习演示已启动") - print("使用学习控制面板可以调整学习参数") - print("观察群体如何通过学习改善行为表现") - +""" +群体学习能力示例 +演示群体的学习和适应能力 +""" + +from panda3d.core import Vec3 +import random + +def create_learning_demo(swarm_manager): + """ + 创建学习能力演示 + 群体学习如何在复杂环境中生存和适应 + """ + print("创建学习能力演示...") + + # 清理现有群体 + swarm_manager.cleanup() + swarm_manager.swarms.clear() + swarm_manager.swarm_id_counter = 0 + + # 创建学习型群体 + print("创建学习型群体...") + learning_swarm = swarm_manager.create_example_swarm( + swarm_type="鸟类", + member_count=40, + position_offset=Vec3(0, 0, 10) + ) + + # 设置学习型群体为紫色 + for member in learning_swarm['members']: + if 'node' in member: + member['node'].setColor(0.5, 0, 0.5, 1) # 紫色 + + # 创建复杂环境 + print("创建复杂环境...") + + # 添加多个障碍物 + obstacles = [ + (Vec3(-15, -15, 8), 4.0), # 大障碍物 + (Vec3(15, 15, 12), 3.0), # 中等障碍物 + (Vec3(-10, 10, 6), 2.0), # 小障碍物 + (Vec3(10, -10, 10), 3.5), # 中等障碍物 + (Vec3(0, 0, 15), 2.5), # 中心障碍物 + ] + + for position, radius in obstacles: + swarm_manager.create_obstacle(position, radius) + + # 添加移动障碍物 + moving_obstacle = { + 'position': Vec3(-20, 0, 10), + 'radius': 3.0, + 'velocity': Vec3(2, 1, 0), + 'bounds': {'min_x': -25, 'max_x': 25, 'min_y': -25, 'max_y': 25} + } + + if not hasattr(swarm_manager, 'moving_obstacles'): + swarm_manager.moving_obstacles = [] + swarm_manager.moving_obstacles.append(moving_obstacle) + + # 添加资源点 + resources = [ + (Vec3(-25, -25, 5), 2.0), # 资源点1 + (Vec3(25, 25, 8), 2.5), # 资源点2 + (Vec3(-25, 25, 6), 1.5), # 资源点3 + ] + + for position, radius in resources: + swarm_manager.create_obstacle(position, radius) + + # 配置学习参数 + print("配置学习参数...") + swarm_manager.config.set("learning_enabled", True) + swarm_manager.config.set("learning_rate", 0.1) + swarm_manager.config.set("exploration_rate", 0.3) + swarm_manager.config.set("memory_capacity", 500) + + # 调整Boids参数以促进学习 + swarm_manager.config.set("cohesion_weight", 0.8) + swarm_manager.config.set("separation_weight", 1.2) + swarm_manager.config.set("alignment_weight", 0.6) + swarm_manager.config.set("max_speed", 6.0) + swarm_manager.config.set("perception_radius", 12.0) + + # 启用避障和边界限制 + swarm_manager.config.set("obstacle_avoidance_enabled", True) + swarm_manager.config.set("boundary_enabled", True) + swarm_manager.config.set("boundary_weight", 2.0) + + print("学习能力演示创建完成") + print("观察群体如何通过学习适应复杂环境") + print("群体将学会避开障碍物并寻找资源") + +def create_adaptation_demo(swarm_manager): + """ + 创建环境适应演示 + 群体学习适应不同的环境条件 + """ + print("创建环境适应演示...") + + # 清理现有群体 + swarm_manager.cleanup() + swarm_manager.swarms.clear() + swarm_manager.swarm_id_counter = 0 + + # 创建适应型群体 + print("创建适应型群体...") + adaptive_swarm = swarm_manager.create_example_swarm( + swarm_type="鱼类", + member_count=60, + position_offset=Vec3(0, 0, 8) + ) + + # 设置适应型群体为蓝色 + for member in adaptive_swarm['members']: + if 'node' in member: + member['node'].setColor(0, 0, 1, 1) # 蓝色 + + # 创建变化的环境 + print("创建变化环境...") + + # 添加多个区域 + # 安全区 + safe_zone = swarm_manager.create_obstacle(Vec3(0, 0, 5), 8.0) + + # 危险区 + danger_zone = swarm_manager.create_obstacle(Vec3(15, 15, 10), 5.0) + + # 资源丰富区 + resource_zone = swarm_manager.create_obstacle(Vec3(-15, -15, 6), 6.0) + + # 配置学习参数 + print("配置适应参数...") + swarm_manager.config.set("learning_enabled", True) + swarm_manager.config.set("learning_rate", 0.15) + swarm_manager.config.set("exploration_rate", 0.25) + swarm_manager.config.set("memory_capacity", 800) + + # 设置环境变化 + swarm_manager.environment_changes = { + 'time_points': [0, 60, 120, 180], # 时间点(秒) + 'conditions': [ + {'threat_level': 0.2, 'resource_abundance': 0.8}, # 安全且资源丰富 + {'threat_level': 0.7, 'resource_abundance': 0.3}, # 危险且资源稀缺 + {'threat_level': 0.4, 'resource_abundance': 0.6}, # 中等条件 + {'threat_level': 0.1, 'resource_abundance': 0.9}, # 安全且资源丰富 + ] + } + + # 调整参数以促进适应 + swarm_manager.config.set("cohesion_weight", 1.0) + swarm_manager.config.set("separation_weight", 1.3) + swarm_manager.config.set("alignment_weight", 0.7) + swarm_manager.config.set("max_speed", 5.0) + swarm_manager.config.set("perception_radius", 10.0) + + # 启用相关行为 + swarm_manager.config.set("obstacle_avoidance_enabled", True) + swarm_manager.config.set("boundary_enabled", True) + swarm_manager.config.set("wander_enabled", True) + swarm_manager.config.set("wander_weight", 0.5) + + print("环境适应演示创建完成") + print("观察群体如何适应变化的环境条件") + print("群体将学会在不同条件下调整行为策略") + +def create_multi_task_learning_demo(swarm_manager): + """ + 创建多任务学习演示 + 群体学习执行多个任务 + """ + print("创建多任务学习演示...") + + # 清理现有群体 + swarm_manager.cleanup() + swarm_manager.swarms.clear() + swarm_manager.swarm_id_counter = 0 + + # 创建多任务群体 + print("创建多任务群体...") + multi_task_swarm = swarm_manager.create_example_swarm( + swarm_type="昆虫", + member_count=100, + position_offset=Vec3(0, 0, 3) + ) + + # 设置多任务群体为绿色 + for member in multi_task_swarm['members']: + if 'node' in member: + member['node'].setColor(0, 1, 0, 1) # 绿色 + + # 创建多个任务区域 + print("创建任务区域...") + + # 搜寻任务区域 + search_area = swarm_manager.create_obstacle(Vec3(-20, 0, 4), 5.0) + + # 运输任务区域 + transport_area = swarm_manager.create_obstacle(Vec3(20, 0, 4), 4.0) + + # 建造任务区域 + build_area = swarm_manager.create_obstacle(Vec3(0, 20, 5), 6.0) + + # 防御任务区域 + defense_area = swarm_manager.create_obstacle(Vec3(0, -20, 3), 5.0) + + # 配置学习参数 + print("配置多任务学习参数...") + swarm_manager.config.set("learning_enabled", True) + swarm_manager.config.set("learning_rate", 0.12) + swarm_manager.config.set("exploration_rate", 0.35) + swarm_manager.config.set("memory_capacity", 1000) + + # 设置任务轮换 + swarm_manager.task_rotation = { + 'time_intervals': 45, # 每45秒切换任务 + 'tasks': ['search', 'transport', 'build', 'defense'], + 'current_task_index': 0 + } + + # 调整参数以适应多任务 + swarm_manager.config.set("cohesion_weight", 0.9) + swarm_manager.config.set("separation_weight", 1.1) + swarm_manager.config.set("alignment_weight", 0.8) + swarm_manager.config.set("max_speed", 7.0) + swarm_manager.config.set("perception_radius", 15.0) + + # 启用相关行为 + swarm_manager.config.set("obstacle_avoidance_enabled", True) + swarm_manager.config.set("boundary_enabled", True) + swarm_manager.config.set("wander_enabled", True) + swarm_manager.config.set("wander_weight", 0.7) + swarm_manager.config.set("path_following_enabled", True) + + print("多任务学习演示创建完成") + print("观察群体如何学习执行不同的任务") + print("群体将学会在不同任务间切换并优化策略") + +def update_dynamic_environment(swarm_manager, elapsed_time): + """ + 更新动态环境 + """ + # 更新移动障碍物 + if hasattr(swarm_manager, 'moving_obstacles'): + for obstacle in swarm_manager.moving_obstacles: + # 移动障碍物 + obstacle['position'] += obstacle['velocity'] * 0.1 # 时间步长 + + # 边界反弹 + bounds = obstacle['bounds'] + if obstacle['position'].x < bounds['min_x'] or obstacle['position'].x > bounds['max_x']: + obstacle['velocity'].x *= -1 + if obstacle['position'].y < bounds['min_y'] or obstacle['position'].y > bounds['max_y']: + obstacle['velocity'].y *= -1 + + # 保持在边界内 + obstacle['position'].x = max(bounds['min_x'], min(bounds['max_x'], obstacle['position'].x)) + obstacle['position'].y = max(bounds['min_y'], min(bounds['max_y'], obstacle['position'].y)) + + # 更新环境条件变化 + if hasattr(swarm_manager, 'environment_changes'): + time_points = swarm_manager.environment_changes['time_points'] + conditions = swarm_manager.environment_changes['conditions'] + + # 找到当前时间点 + current_index = 0 + for i, time_point in enumerate(time_points): + if elapsed_time >= time_point: + current_index = i + else: + break + + if current_index < len(conditions): + condition = conditions[current_index] + + # 这里可以更新环境可视化或其他相关参数 + print(f"环境条件更新: 威胁等级 {condition['threat_level']}, 资源丰富度 {condition['resource_abundance']}") + + # 更新任务轮换 + if hasattr(swarm_manager, 'task_rotation'): + interval = swarm_manager.task_rotation['time_intervals'] + tasks = swarm_manager.task_rotation['tasks'] + + # 计算当前任务索引 + current_task_index = int(elapsed_time // interval) % len(tasks) + + if current_task_index != swarm_manager.task_rotation['current_task_index']: + swarm_manager.task_rotation['current_task_index'] = current_task_index + print(f"任务切换到: {tasks[current_task_index]}") + +def run_learning_demo(swarm_manager, demo_type="basic"): + """ + 运行学习能力演示 + + :param swarm_manager: 群体管理器 + :param demo_type: 演示类型 ("basic", "adaptation", "multi_task") + """ + print("=== 群体学习能力演示 ===") + + if demo_type == "basic": + create_learning_demo(swarm_manager) + elif demo_type == "adaptation": + create_adaptation_demo(swarm_manager) + elif demo_type == "multi_task": + create_multi_task_learning_demo(swarm_manager) + else: + print(f"未知的演示类型: {demo_type}") + return + + # 启用学习功能 + swarm_manager.config.set("learning_enabled", True) + + print(f"\n{demo_type} 学习演示已启动") + print("使用学习控制面板可以调整学习参数") + print("观察群体如何通过学习改善行为表现") + return update_dynamic_environment \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/examples/parallel_performance_test.py b/plugins/user/swarm_intelligence/examples/parallel_performance_test.py index e3be1b70..e57e67b9 100644 --- a/plugins/user/swarm_intelligence/examples/parallel_performance_test.py +++ b/plugins/user/swarm_intelligence/examples/parallel_performance_test.py @@ -1,155 +1,155 @@ -""" -并行计算性能测试 -测试并行计算对群体智能模拟性能的影响 -""" - -import time -from panda3d.core import Vec3 - -def performance_comparison_test(swarm_manager, member_counts=[100, 200, 500]): - """ - 性能对比测试 - 比较串行和并行计算的性能差异 - """ - print("=== 并行计算性能对比测试 ===") - - # 保存原始设置 - original_count = swarm_manager.config.get("member_count", 20) - original_parallel = swarm_manager.config.get("parallel_computation", True) - - for count in member_counts: - print(f"\n测试群体规模: {count} 个成员") - - # 设置群体数量 - swarm_manager.config.set("member_count", count) - swarm_manager.reset() - - # 测试串行计算性能 - print(" 测试串行计算...") - swarm_manager.config.set("parallel_computation", False) - serial_time = run_performance_test(swarm_manager, 3.0) - serial_fps = 3.0 / serial_time if serial_time > 0 else 0 - print(f" 串行计算FPS: {serial_fps:.2f}") - - # 测试并行计算性能 - print(" 测试并行计算...") - swarm_manager.config.set("parallel_computation", True) - parallel_time = run_performance_test(swarm_manager, 3.0) - parallel_fps = 3.0 / parallel_time if parallel_time > 0 else 0 - print(f" 并行计算FPS: {parallel_fps:.2f}") - - # 计算性能提升 - if serial_fps > 0: - speedup = parallel_fps / serial_fps - print(f" 性能提升: {speedup:.2f}x") - - # 恢复原始设置 - swarm_manager.config.set("member_count", original_count) - swarm_manager.config.set("parallel_computation", original_parallel) - swarm_manager.reset() - - print("\n性能对比测试完成") - -def run_performance_test(swarm_manager, duration): - """ - 运行性能测试 - """ - start_time = time.time() - frame_count = 0 - - while time.time() - start_time < duration: - swarm_manager._update_swarm() - frame_count += 1 - - return time.time() - start_time - -def scalability_test(swarm_manager): - """ - 可扩展性测试 - 测试不同CPU核心数对并行计算性能的影响 - """ - print("\n=== 可扩展性测试 ===") - - # 设置大规模群体 - original_count = swarm_manager.config.get("member_count", 20) - swarm_manager.config.set("member_count", 1000) - swarm_manager.reset() - - # 测试不同并行度 - parallel_settings = [1, 2, 4, 8] - - for num_threads in parallel_settings: - print(f"\n测试并行度: {num_threads} 线程") - - # 这里我们模拟不同的并行度设置 - # 实际应用中可能需要调整线程池大小 - test_time = run_performance_test(swarm_manager, 2.0) - test_fps = 2.0 / test_time if test_time > 0 else 0 - print(f" FPS: {test_fps:.2f}") - - # 恢复原始设置 - swarm_manager.config.set("member_count", original_count) - swarm_manager.reset() - - print("\n可扩展性测试完成") - -def memory_usage_test(swarm_manager): - """ - 内存使用测试 - 测试并行计算对内存使用的影响 - """ - print("\n=== 内存使用测试 ===") - - # 设置中等规模群体 - original_count = swarm_manager.config.get("member_count", 20) - swarm_manager.config.set("member_count", 500) - swarm_manager.reset() - - # 获取初始内存使用 - import psutil - import os - process = psutil.Process(os.getpid()) - initial_memory = process.memory_info().rss / 1024 / 1024 # MB - - print(f"初始内存使用: {initial_memory:.2f} MB") - - # 测试串行计算内存使用 - swarm_manager.config.set("parallel_computation", False) - run_performance_test(swarm_manager, 1.0) - serial_memory = process.memory_info().rss / 1024 / 1024 # MB - print(f"串行计算内存使用: {serial_memory:.2f} MB") - - # 测试并行计算内存使用 - swarm_manager.config.set("parallel_computation", True) - run_performance_test(swarm_manager, 1.0) - parallel_memory = process.memory_info().rss / 1024 / 1024 # MB - print(f"并行计算内存使用: {parallel_memory:.2f} MB") - - # 计算内存开销 - serial_overhead = serial_memory - initial_memory - parallel_overhead = parallel_memory - initial_memory - print(f"串行计算内存开销: {serial_overhead:.2f} MB") - print(f"并行计算内存开销: {parallel_overhead:.2f} MB") - - # 恢复原始设置 - swarm_manager.config.set("member_count", original_count) - swarm_manager.reset() - - print("\n内存使用测试完成") - -def run_all_parallel_tests(swarm_manager): - """ - 运行所有并行计算测试 - """ - print("开始运行所有并行计算测试...") - - # 性能对比测试 - performance_comparison_test(swarm_manager) - - # 可扩展性测试 - scalability_test(swarm_manager) - - # 内存使用测试 - memory_usage_test(swarm_manager) - +""" +并行计算性能测试 +测试并行计算对群体智能模拟性能的影响 +""" + +import time +from panda3d.core import Vec3 + +def performance_comparison_test(swarm_manager, member_counts=[100, 200, 500]): + """ + 性能对比测试 + 比较串行和并行计算的性能差异 + """ + print("=== 并行计算性能对比测试 ===") + + # 保存原始设置 + original_count = swarm_manager.config.get("member_count", 20) + original_parallel = swarm_manager.config.get("parallel_computation", True) + + for count in member_counts: + print(f"\n测试群体规模: {count} 个成员") + + # 设置群体数量 + swarm_manager.config.set("member_count", count) + swarm_manager.reset() + + # 测试串行计算性能 + print(" 测试串行计算...") + swarm_manager.config.set("parallel_computation", False) + serial_time = run_performance_test(swarm_manager, 3.0) + serial_fps = 3.0 / serial_time if serial_time > 0 else 0 + print(f" 串行计算FPS: {serial_fps:.2f}") + + # 测试并行计算性能 + print(" 测试并行计算...") + swarm_manager.config.set("parallel_computation", True) + parallel_time = run_performance_test(swarm_manager, 3.0) + parallel_fps = 3.0 / parallel_time if parallel_time > 0 else 0 + print(f" 并行计算FPS: {parallel_fps:.2f}") + + # 计算性能提升 + if serial_fps > 0: + speedup = parallel_fps / serial_fps + print(f" 性能提升: {speedup:.2f}x") + + # 恢复原始设置 + swarm_manager.config.set("member_count", original_count) + swarm_manager.config.set("parallel_computation", original_parallel) + swarm_manager.reset() + + print("\n性能对比测试完成") + +def run_performance_test(swarm_manager, duration): + """ + 运行性能测试 + """ + start_time = time.time() + frame_count = 0 + + while time.time() - start_time < duration: + swarm_manager._update_swarm() + frame_count += 1 + + return time.time() - start_time + +def scalability_test(swarm_manager): + """ + 可扩展性测试 + 测试不同CPU核心数对并行计算性能的影响 + """ + print("\n=== 可扩展性测试 ===") + + # 设置大规模群体 + original_count = swarm_manager.config.get("member_count", 20) + swarm_manager.config.set("member_count", 1000) + swarm_manager.reset() + + # 测试不同并行度 + parallel_settings = [1, 2, 4, 8] + + for num_threads in parallel_settings: + print(f"\n测试并行度: {num_threads} 线程") + + # 这里我们模拟不同的并行度设置 + # 实际应用中可能需要调整线程池大小 + test_time = run_performance_test(swarm_manager, 2.0) + test_fps = 2.0 / test_time if test_time > 0 else 0 + print(f" FPS: {test_fps:.2f}") + + # 恢复原始设置 + swarm_manager.config.set("member_count", original_count) + swarm_manager.reset() + + print("\n可扩展性测试完成") + +def memory_usage_test(swarm_manager): + """ + 内存使用测试 + 测试并行计算对内存使用的影响 + """ + print("\n=== 内存使用测试 ===") + + # 设置中等规模群体 + original_count = swarm_manager.config.get("member_count", 20) + swarm_manager.config.set("member_count", 500) + swarm_manager.reset() + + # 获取初始内存使用 + import psutil + import os + process = psutil.Process(os.getpid()) + initial_memory = process.memory_info().rss / 1024 / 1024 # MB + + print(f"初始内存使用: {initial_memory:.2f} MB") + + # 测试串行计算内存使用 + swarm_manager.config.set("parallel_computation", False) + run_performance_test(swarm_manager, 1.0) + serial_memory = process.memory_info().rss / 1024 / 1024 # MB + print(f"串行计算内存使用: {serial_memory:.2f} MB") + + # 测试并行计算内存使用 + swarm_manager.config.set("parallel_computation", True) + run_performance_test(swarm_manager, 1.0) + parallel_memory = process.memory_info().rss / 1024 / 1024 # MB + print(f"并行计算内存使用: {parallel_memory:.2f} MB") + + # 计算内存开销 + serial_overhead = serial_memory - initial_memory + parallel_overhead = parallel_memory - initial_memory + print(f"串行计算内存开销: {serial_overhead:.2f} MB") + print(f"并行计算内存开销: {parallel_overhead:.2f} MB") + + # 恢复原始设置 + swarm_manager.config.set("member_count", original_count) + swarm_manager.reset() + + print("\n内存使用测试完成") + +def run_all_parallel_tests(swarm_manager): + """ + 运行所有并行计算测试 + """ + print("开始运行所有并行计算测试...") + + # 性能对比测试 + performance_comparison_test(swarm_manager) + + # 可扩展性测试 + scalability_test(swarm_manager) + + # 内存使用测试 + memory_usage_test(swarm_manager) + print("\n所有并行计算测试完成!") \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/examples/performance_test.py b/plugins/user/swarm_intelligence/examples/performance_test.py index 10643092..29cd6d43 100644 --- a/plugins/user/swarm_intelligence/examples/performance_test.py +++ b/plugins/user/swarm_intelligence/examples/performance_test.py @@ -1,172 +1,172 @@ -""" -性能测试示例 -测试群体智能插件的性能表现 -""" - -import time -from panda3d.core import Vec3 - -def performance_test(swarm_manager, member_counts=[50, 100, 200, 500]): - """ - 性能测试函数 - 测试不同群体规模下的性能表现 - """ - print("=== 群体智能插件性能测试 ===") - - # 保存原始设置 - original_count = swarm_manager.config.get("member_count", 20) - - for count in member_counts: - print(f"\n测试群体规模: {count} 个成员") - - # 设置群体数量 - swarm_manager.config.set("member_count", count) - swarm_manager.reset() - - # 运行测试 - test_duration = 5.0 # 测试5秒 - start_time = time.time() - frame_count = 0 - - # 简单的性能测试循环 - while time.time() - start_time < test_duration: - swarm_manager._update_swarm() - frame_count += 1 - - # 计算FPS - elapsed_time = time.time() - start_time - fps = frame_count / elapsed_time - - print(f" 帧数: {frame_count}") - print(f" 耗时: {elapsed_time:.2f} 秒") - print(f" 平均FPS: {fps:.2f}") - - # 显示群体统计信息 - stats = swarm_manager.get_stats() - print(f" 群体统计: {stats}") - - # 恢复原始设置 - swarm_manager.config.set("member_count", original_count) - swarm_manager.reset() - - print("\n性能测试完成") - -def behavior_performance_test(swarm_manager): - """ - 行为性能测试 - 测试不同行为对性能的影响 - """ - print("\n=== 行为性能测试 ===") - - # 基准测试(仅基本Boids行为) - print("\n基准测试(仅基本Boids行为):") - swarm_manager.config.set("obstacle_avoidance_enabled", False) - swarm_manager.config.set("wander_enabled", False) - swarm_manager.config.set("predator_avoid_enabled", False) - swarm_manager.config.set("formation_enabled", False) - swarm_manager.config.set("path_following_enabled", False) - - baseline_duration = run_behavior_test(swarm_manager, 3.0) - baseline_fps = 3.0 / baseline_duration if baseline_duration > 0 else 0 - print(f" 基准FPS: {baseline_fps:.2f}") - - # 避障行为测试 - print("\n避障行为测试:") - swarm_manager.config.set("obstacle_avoidance_enabled", True) - # 添加一些障碍物 - for i in range(5): - pos = Vec3(i*10-20, 0, 10) - swarm_manager.create_obstacle(pos, 3.0) - - obstacle_duration = run_behavior_test(swarm_manager, 3.0) - obstacle_fps = 3.0 / obstacle_duration if obstacle_duration > 0 else 0 - print(f" 避障FPS: {obstacle_fps:.2f}") - print(f" 性能下降: {((baseline_fps - obstacle_fps) / baseline_fps * 100):.1f}%") - - # 清理障碍物 - swarm_manager.obstacles.clear() - - # 游走行为测试 - print("\n游走行为测试:") - swarm_manager.config.set("obstacle_avoidance_enabled", False) - swarm_manager.config.set("wander_enabled", True) - - wander_duration = run_behavior_test(swarm_manager, 3.0) - wander_fps = 3.0 / wander_duration if wander_duration > 0 else 0 - print(f" 游走FPS: {wander_fps:.2f}") - print(f" 性能下降: {((baseline_fps - wander_fps) / baseline_fps * 100):.1f}%") - - # 恢复默认设置 - swarm_manager.config.set("wander_enabled", False) - - print("\n行为性能测试完成") - -def run_behavior_test(swarm_manager, duration): - """ - 运行行为测试 - """ - start_time = time.time() - frame_count = 0 - - while time.time() - start_time < duration: - swarm_manager._update_swarm() - frame_count += 1 - - return time.time() - start_time - -def stress_test(swarm_manager): - """ - 压力测试 - 测试极端条件下的性能表现 - """ - print("\n=== 压力测试 ===") - - # 极大群体规模 - print("\n极大群体规模测试 (1000个成员):") - original_count = swarm_manager.config.get("member_count", 20) - swarm_manager.config.set("member_count", 1000) - swarm_manager.reset() - - duration = run_behavior_test(swarm_manager, 5.0) - fps = 5.0 / duration if duration > 0 else 0 - print(f" FPS: {fps:.2f}") - - # 恢复原始设置 - swarm_manager.config.set("member_count", original_count) - swarm_manager.reset() - - # 大量障碍物 - print("\n大量障碍物测试 (100个障碍物):") - for i in range(100): - pos = Vec3( - (i % 10) * 10 - 45, - (i // 10) * 10 - 45, - 10 - ) - swarm_manager.create_obstacle(pos, 2.0) - - duration = run_behavior_test(swarm_manager, 3.0) - fps = 3.0 / duration if duration > 0 else 0 - print(f" FPS: {fps:.2f}") - - # 清理障碍物 - swarm_manager.obstacles.clear() - - print("\n压力测试完成") - -def run_all_performance_tests(swarm_manager): - """ - 运行所有性能测试 - """ - print("开始运行所有性能测试...") - - # 基本性能测试 - performance_test(swarm_manager) - - # 行为性能测试 - behavior_performance_test(swarm_manager) - - # 压力测试 - stress_test(swarm_manager) - +""" +性能测试示例 +测试群体智能插件的性能表现 +""" + +import time +from panda3d.core import Vec3 + +def performance_test(swarm_manager, member_counts=[50, 100, 200, 500]): + """ + 性能测试函数 + 测试不同群体规模下的性能表现 + """ + print("=== 群体智能插件性能测试 ===") + + # 保存原始设置 + original_count = swarm_manager.config.get("member_count", 20) + + for count in member_counts: + print(f"\n测试群体规模: {count} 个成员") + + # 设置群体数量 + swarm_manager.config.set("member_count", count) + swarm_manager.reset() + + # 运行测试 + test_duration = 5.0 # 测试5秒 + start_time = time.time() + frame_count = 0 + + # 简单的性能测试循环 + while time.time() - start_time < test_duration: + swarm_manager._update_swarm() + frame_count += 1 + + # 计算FPS + elapsed_time = time.time() - start_time + fps = frame_count / elapsed_time + + print(f" 帧数: {frame_count}") + print(f" 耗时: {elapsed_time:.2f} 秒") + print(f" 平均FPS: {fps:.2f}") + + # 显示群体统计信息 + stats = swarm_manager.get_stats() + print(f" 群体统计: {stats}") + + # 恢复原始设置 + swarm_manager.config.set("member_count", original_count) + swarm_manager.reset() + + print("\n性能测试完成") + +def behavior_performance_test(swarm_manager): + """ + 行为性能测试 + 测试不同行为对性能的影响 + """ + print("\n=== 行为性能测试 ===") + + # 基准测试(仅基本Boids行为) + print("\n基准测试(仅基本Boids行为):") + swarm_manager.config.set("obstacle_avoidance_enabled", False) + swarm_manager.config.set("wander_enabled", False) + swarm_manager.config.set("predator_avoid_enabled", False) + swarm_manager.config.set("formation_enabled", False) + swarm_manager.config.set("path_following_enabled", False) + + baseline_duration = run_behavior_test(swarm_manager, 3.0) + baseline_fps = 3.0 / baseline_duration if baseline_duration > 0 else 0 + print(f" 基准FPS: {baseline_fps:.2f}") + + # 避障行为测试 + print("\n避障行为测试:") + swarm_manager.config.set("obstacle_avoidance_enabled", True) + # 添加一些障碍物 + for i in range(5): + pos = Vec3(i*10-20, 0, 10) + swarm_manager.create_obstacle(pos, 3.0) + + obstacle_duration = run_behavior_test(swarm_manager, 3.0) + obstacle_fps = 3.0 / obstacle_duration if obstacle_duration > 0 else 0 + print(f" 避障FPS: {obstacle_fps:.2f}") + print(f" 性能下降: {((baseline_fps - obstacle_fps) / baseline_fps * 100):.1f}%") + + # 清理障碍物 + swarm_manager.obstacles.clear() + + # 游走行为测试 + print("\n游走行为测试:") + swarm_manager.config.set("obstacle_avoidance_enabled", False) + swarm_manager.config.set("wander_enabled", True) + + wander_duration = run_behavior_test(swarm_manager, 3.0) + wander_fps = 3.0 / wander_duration if wander_duration > 0 else 0 + print(f" 游走FPS: {wander_fps:.2f}") + print(f" 性能下降: {((baseline_fps - wander_fps) / baseline_fps * 100):.1f}%") + + # 恢复默认设置 + swarm_manager.config.set("wander_enabled", False) + + print("\n行为性能测试完成") + +def run_behavior_test(swarm_manager, duration): + """ + 运行行为测试 + """ + start_time = time.time() + frame_count = 0 + + while time.time() - start_time < duration: + swarm_manager._update_swarm() + frame_count += 1 + + return time.time() - start_time + +def stress_test(swarm_manager): + """ + 压力测试 + 测试极端条件下的性能表现 + """ + print("\n=== 压力测试 ===") + + # 极大群体规模 + print("\n极大群体规模测试 (1000个成员):") + original_count = swarm_manager.config.get("member_count", 20) + swarm_manager.config.set("member_count", 1000) + swarm_manager.reset() + + duration = run_behavior_test(swarm_manager, 5.0) + fps = 5.0 / duration if duration > 0 else 0 + print(f" FPS: {fps:.2f}") + + # 恢复原始设置 + swarm_manager.config.set("member_count", original_count) + swarm_manager.reset() + + # 大量障碍物 + print("\n大量障碍物测试 (100个障碍物):") + for i in range(100): + pos = Vec3( + (i % 10) * 10 - 45, + (i // 10) * 10 - 45, + 10 + ) + swarm_manager.create_obstacle(pos, 2.0) + + duration = run_behavior_test(swarm_manager, 3.0) + fps = 3.0 / duration if duration > 0 else 0 + print(f" FPS: {fps:.2f}") + + # 清理障碍物 + swarm_manager.obstacles.clear() + + print("\n压力测试完成") + +def run_all_performance_tests(swarm_manager): + """ + 运行所有性能测试 + """ + print("开始运行所有性能测试...") + + # 基本性能测试 + performance_test(swarm_manager) + + # 行为性能测试 + behavior_performance_test(swarm_manager) + + # 压力测试 + stress_test(swarm_manager) + print("\n所有性能测试完成!") \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/gui.py b/plugins/user/swarm_intelligence/gui.py index b0ecb03f..f9063286 100644 --- a/plugins/user/swarm_intelligence/gui.py +++ b/plugins/user/swarm_intelligence/gui.py @@ -1,1124 +1,1124 @@ -""" -群体智能插件GUI界面 -""" - -from PyQt5.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QSlider, QLabel, - QGroupBox, QCheckBox, QComboBox, QSpinBox, QDoubleSpinBox, QTabWidget, - QFormLayout, QTextEdit -) -from PyQt5.QtCore import Qt, QTimer - -class SwarmGUI(QWidget): - """ - 群体智能插件GUI界面 - 提供完整的控制面板用于调节群体行为参数 - """ - - def __init__(self, swarm_manager, config): - super().__init__() - self.swarm_manager = swarm_manager - self.config = config - self.init_ui() - # 启动定时器更新状态信息 - self.timer = QTimer(self) - self.timer.timeout.connect(self.update_status) - self.timer.start(1000) # 每秒更新一次 - - def init_ui(self): - """ - 初始化UI界面 - """ - self.setWindowTitle('群体智能控制面板') - self.setGeometry(100, 100, 400, 600) - - # 创建标签页 - tab_widget = QTabWidget() - - # 基本控制标签页 - basic_tab = self.create_basic_tab() - tab_widget.addTab(basic_tab, "基本控制") - - # Boids参数标签页 - boids_tab = self.create_boids_tab() - tab_widget.addTab(boids_tab, "Boids参数") - - # 扩展行为标签页 - behavior_tab = self.create_behavior_tab() - tab_widget.addTab(behavior_tab, "扩展行为") - - # 队形控制标签页 - formation_tab = self.create_formation_tab() - tab_widget.addTab(formation_tab, "队形控制") - - # 学习控制标签页 - learning_tab = self.create_learning_tab() - tab_widget.addTab(learning_tab, "学习控制") - - # 环境感知标签页 - environment_tab = self.create_environment_tab() - tab_widget.addTab(environment_tab, "环境感知") - - # 状态信息标签页 - status_tab = self.create_status_tab() - tab_widget.addTab(status_tab, "状态信息") - - # 主布局 - main_layout = QVBoxLayout() - main_layout.addWidget(tab_widget) - self.setLayout(main_layout) - - def create_environment_tab(self): - """ - 创建环境感知标签页 - """ - widget = QWidget() - layout = QVBoxLayout() - - # 环境感知开关 - awareness_group = QGroupBox("环境感知控制") - awareness_layout = QVBoxLayout() - - self.environment_awareness_checkbox = QCheckBox("启用环境感知") - self.environment_awareness_checkbox.setChecked(self.config.get("environment_awareness_enabled", True)) - self.environment_awareness_checkbox.stateChanged.connect(self.toggle_environment_awareness) - awareness_layout.addWidget(self.environment_awareness_checkbox) - - awareness_group.setLayout(awareness_layout) - layout.addWidget(awareness_group) - - # 传感器参数 - sensor_group = QGroupBox("传感器参数") - sensor_layout = QFormLayout() - - # 传感器范围 - self.sensor_range_spinbox = QDoubleSpinBox() - self.sensor_range_spinbox.setRange(1.0, 50.0) - self.sensor_range_spinbox.setSingleStep(1.0) - self.sensor_range_spinbox.setValue(self.config.get("sensor_range", 15.0)) - self.sensor_range_spinbox.valueChanged.connect(self.update_sensor_range) - sensor_layout.addRow("传感器范围:", self.sensor_range_spinbox) - - # 传感器精度 - self.sensor_accuracy_spinbox = QDoubleSpinBox() - self.sensor_accuracy_spinbox.setRange(0.1, 1.0) - self.sensor_accuracy_spinbox.setSingleStep(0.05) - self.sensor_accuracy_spinbox.setValue(self.config.get("sensor_accuracy", 0.9)) - self.sensor_accuracy_spinbox.valueChanged.connect(self.update_sensor_accuracy) - sensor_layout.addRow("传感器精度:", self.sensor_accuracy_spinbox) - - sensor_group.setLayout(sensor_layout) - layout.addWidget(sensor_group) - - # 环境响应参数 - response_group = QGroupBox("环境响应参数") - response_layout = QFormLayout() - - # 障碍物响应权重 - self.obstacle_response_weight_spinbox = QDoubleSpinBox() - self.obstacle_response_weight_spinbox.setRange(0.0, 5.0) - self.obstacle_response_weight_spinbox.setSingleStep(0.1) - self.obstacle_response_weight_spinbox.setValue(self.config.get("obstacle_response_weight", 2.0)) - self.obstacle_response_weight_spinbox.valueChanged.connect(self.update_obstacle_response_weight) - response_layout.addRow("障碍物响应权重:", self.obstacle_response_weight_spinbox) - - # 捕食者响应权重 - self.predator_response_weight_spinbox = QDoubleSpinBox() - self.predator_response_weight_spinbox.setRange(0.0, 5.0) - self.predator_response_weight_spinbox.setSingleStep(0.1) - self.predator_response_weight_spinbox.setValue(self.config.get("predator_response_weight", 3.0)) - self.predator_response_weight_spinbox.valueChanged.connect(self.update_predator_response_weight) - response_layout.addRow("捕食者响应权重:", self.predator_response_weight_spinbox) - - # 资源寻求权重 - self.resource_seek_weight_spinbox = QDoubleSpinBox() - self.resource_seek_weight_spinbox.setRange(0.0, 2.0) - self.resource_seek_weight_spinbox.setSingleStep(0.05) - self.resource_seek_weight_spinbox.setValue(self.config.get("resource_seek_weight", 0.5)) - self.resource_seek_weight_spinbox.valueChanged.connect(self.update_resource_seek_weight) - response_layout.addRow("资源寻求权重:", self.resource_seek_weight_spinbox) - - # 危险回避权重 - self.hazard_avoid_weight_spinbox = QDoubleSpinBox() - self.hazard_avoid_weight_spinbox.setRange(0.0, 5.0) - self.hazard_avoid_weight_spinbox.setSingleStep(0.1) - self.hazard_avoid_weight_spinbox.setValue(self.config.get("hazard_avoid_weight", 3.0)) - self.hazard_avoid_weight_spinbox.valueChanged.connect(self.update_hazard_avoid_weight) - response_layout.addRow("危险回避权重:", self.hazard_avoid_weight_spinbox) - - response_group.setLayout(response_layout) - layout.addWidget(response_group) - - # 环境动态参数 - dynamics_group = QGroupBox("环境动态") - dynamics_layout = QFormLayout() - - # 时间流逝速度 - self.time_flow_rate_spinbox = QDoubleSpinBox() - self.time_flow_rate_spinbox.setRange(0.01, 2.0) - self.time_flow_rate_spinbox.setSingleStep(0.05) - self.time_flow_rate_spinbox.setValue(self.config.get("time_flow_rate", 0.1)) - self.time_flow_rate_spinbox.valueChanged.connect(self.update_time_flow_rate) - dynamics_layout.addRow("时间流逝速度:", self.time_flow_rate_spinbox) - - dynamics_group.setLayout(dynamics_layout) - layout.addWidget(dynamics_group) - - layout.addStretch() - widget.setLayout(layout) - return widget - - def create_basic_tab(self): - """ - 创建基本控制标签页 - """ - widget = QWidget() - layout = QVBoxLayout() - - # 控制按钮组 - control_group = QGroupBox("基本控制") - control_layout = QVBoxLayout() - - # 控制按钮 - button_layout = QHBoxLayout() - self.pause_button = QPushButton("暂停/继续") - self.pause_button.clicked.connect(self.toggle_pause) - button_layout.addWidget(self.pause_button) - - reset_button = QPushButton("重置群体") - reset_button.clicked.connect(self.reset_swarm) - button_layout.addWidget(reset_button) - - control_layout.addLayout(button_layout) - - # 群体数量控制 - member_layout = QHBoxLayout() - member_layout.addWidget(QLabel("群体数量:")) - self.member_count_spinbox = QSpinBox() - self.member_count_spinbox.setRange(1, 1000) - self.member_count_spinbox.setValue(self.config.get("member_count", 20)) - self.member_count_spinbox.valueChanged.connect(self.update_member_count) - member_layout.addWidget(self.member_count_spinbox) - - control_layout.addLayout(member_layout) - - control_group.setLayout(control_layout) - layout.addWidget(control_group) - - # 群体类型选择 - type_group = QGroupBox("群体类型") - type_layout = QVBoxLayout() - - self.type_combo = QComboBox() - self.type_combo.addItems(["鸟类", "鱼类", "昆虫", "自定义"]) - self.type_combo.setCurrentText(self.config.get("swarm_type", "鸟类")) - self.type_combo.currentTextChanged.connect(self.update_swarm_type) - type_layout.addWidget(self.type_combo) - - type_group.setLayout(type_layout) - layout.addWidget(type_group) - - # 目标控制 - target_group = QGroupBox("目标控制") - target_layout = QVBoxLayout() - - self.seek_target_checkbox = QCheckBox("启用目标寻求") - self.seek_target_checkbox.setChecked(self.config.get("seek_enabled", False)) - self.seek_target_checkbox.stateChanged.connect(self.toggle_seek_target) - target_layout.addWidget(self.seek_target_checkbox) - - target_group.setLayout(target_layout) - layout.addWidget(target_group) - - layout.addStretch() - widget.setLayout(layout) - return widget - - def create_boids_tab(self): - """ - 创建Boids参数标签页 - """ - widget = QWidget() - layout = QVBoxLayout() - - # 基本Boids参数 - boids_group = QGroupBox("基本Boids参数") - boids_layout = QFormLayout() - - # 聚集权重 - self.cohesion_spinbox = QDoubleSpinBox() - self.cohesion_spinbox.setRange(0.0, 10.0) - self.cohesion_spinbox.setSingleStep(0.1) - self.cohesion_spinbox.setValue(self.config.get("cohesion_weight", 1.0)) - self.cohesion_spinbox.valueChanged.connect(self.update_cohesion_weight) - boids_layout.addRow("聚集权重:", self.cohesion_spinbox) - - # 分离权重 - self.separation_spinbox = QDoubleSpinBox() - self.separation_spinbox.setRange(0.0, 10.0) - self.separation_spinbox.setSingleStep(0.1) - self.separation_spinbox.setValue(self.config.get("separation_weight", 1.5)) - self.separation_spinbox.valueChanged.connect(self.update_separation_weight) - boids_layout.addRow("分离权重:", self.separation_spinbox) - - # 对齐权重 - self.alignment_spinbox = QDoubleSpinBox() - self.alignment_spinbox.setRange(0.0, 10.0) - self.alignment_spinbox.setSingleStep(0.1) - self.alignment_spinbox.setValue(self.config.get("alignment_weight", 1.0)) - self.alignment_spinbox.valueChanged.connect(self.update_alignment_weight) - boids_layout.addRow("对齐权重:", self.alignment_spinbox) - - # 感知半径 - self.perception_spinbox = QDoubleSpinBox() - self.perception_spinbox.setRange(0.1, 100.0) - self.perception_spinbox.setSingleStep(0.5) - self.perception_spinbox.setValue(self.config.get("perception_radius", 10.0)) - self.perception_spinbox.valueChanged.connect(self.update_perception_radius) - boids_layout.addRow("感知半径:", self.perception_spinbox) - - boids_group.setLayout(boids_layout) - layout.addWidget(boids_group) - - # 移动参数 - movement_group = QGroupBox("移动参数") - movement_layout = QFormLayout() - - # 最大速度 - self.max_speed_spinbox = QDoubleSpinBox() - self.max_speed_spinbox.setRange(0.1, 50.0) - self.max_speed_spinbox.setSingleStep(0.5) - self.max_speed_spinbox.setValue(self.config.get("max_speed", 5.0)) - self.max_speed_spinbox.valueChanged.connect(self.update_max_speed) - movement_layout.addRow("最大速度:", self.max_speed_spinbox) - - # 最大加速度 - self.max_acceleration_spinbox = QDoubleSpinBox() - self.max_acceleration_spinbox.setRange(0.01, 10.0) - self.max_acceleration_spinbox.setSingleStep(0.05) - self.max_acceleration_spinbox.setValue(self.config.get("max_acceleration", 1.0)) - self.max_acceleration_spinbox.valueChanged.connect(self.update_max_acceleration) - movement_layout.addRow("最大加速度:", self.max_acceleration_spinbox) - - # 最大转向力 - self.max_force_spinbox = QDoubleSpinBox() - self.max_force_spinbox.setRange(0.01, 5.0) - self.max_force_spinbox.setSingleStep(0.05) - self.max_force_spinbox.setValue(self.config.get("max_force", 0.5)) - self.max_force_spinbox.valueChanged.connect(self.update_max_force) - movement_layout.addRow("最大转向力:", self.max_force_spinbox) - - movement_group.setLayout(movement_layout) - layout.addWidget(movement_group) - - # 边界参数 - boundary_group = QGroupBox("边界参数") - boundary_layout = QFormLayout() - - self.boundary_checkbox = QCheckBox("启用边界限制") - self.boundary_checkbox.setChecked(self.config.get("boundary_enabled", True)) - self.boundary_checkbox.stateChanged.connect(self.toggle_boundary) - boundary_layout.addRow("", self.boundary_checkbox) - - self.boundary_weight_spinbox = QDoubleSpinBox() - self.boundary_weight_spinbox.setRange(0.0, 10.0) - self.boundary_weight_spinbox.setSingleStep(0.1) - self.boundary_weight_spinbox.setValue(self.config.get("boundary_weight", 2.0)) - self.boundary_weight_spinbox.valueChanged.connect(self.update_boundary_weight) - boundary_layout.addRow("边界权重:", self.boundary_weight_spinbox) - - self.boundary_buffer_spinbox = QDoubleSpinBox() - self.boundary_buffer_spinbox.setRange(0.1, 50.0) - self.boundary_buffer_spinbox.setSingleStep(0.5) - self.boundary_buffer_spinbox.setValue(self.config.get("boundary_buffer", 5.0)) - self.boundary_buffer_spinbox.valueChanged.connect(self.update_boundary_buffer) - boundary_layout.addRow("边界缓冲:", self.boundary_buffer_spinbox) - - boundary_group.setLayout(boundary_layout) - layout.addWidget(boundary_group) - - layout.addStretch() - widget.setLayout(layout) - return widget - - def create_behavior_tab(self): - """ - 创建扩展行为标签页 - """ - widget = QWidget() - layout = QVBoxLayout() - - # 避障行为 - obstacle_group = QGroupBox("避障行为") - obstacle_layout = QFormLayout() - - self.obstacle_checkbox = QCheckBox("启用避障") - self.obstacle_checkbox.setChecked(self.config.get("obstacle_avoidance_enabled", True)) - self.obstacle_checkbox.stateChanged.connect(self.toggle_obstacle_avoidance) - obstacle_layout.addRow("", self.obstacle_checkbox) - - self.obstacle_weight_spinbox = QDoubleSpinBox() - self.obstacle_weight_spinbox.setRange(0.0, 10.0) - self.obstacle_weight_spinbox.setSingleStep(0.1) - self.obstacle_weight_spinbox.setValue(self.config.get("obstacle_weight", 2.0)) - self.obstacle_weight_spinbox.valueChanged.connect(self.update_obstacle_weight) - obstacle_layout.addRow("避障权重:", self.obstacle_weight_spinbox) - - obstacle_group.setLayout(obstacle_layout) - layout.addWidget(obstacle_group) - - # 游走行为 - wander_group = QGroupBox("游走行为") - wander_layout = QFormLayout() - - self.wander_checkbox = QCheckBox("启用游走") - self.wander_checkbox.setChecked(self.config.get("wander_enabled", False)) - self.wander_checkbox.stateChanged.connect(self.toggle_wander) - wander_layout.addRow("", self.wander_checkbox) - - self.wander_weight_spinbox = QDoubleSpinBox() - self.wander_weight_spinbox.setRange(0.0, 5.0) - self.wander_weight_spinbox.setSingleStep(0.05) - self.wander_weight_spinbox.setValue(self.config.get("wander_weight", 0.5)) - self.wander_weight_spinbox.valueChanged.connect(self.update_wander_weight) - wander_layout.addRow("游走权重:", self.wander_weight_spinbox) - - self.wander_radius_spinbox = QDoubleSpinBox() - self.wander_radius_spinbox.setRange(0.1, 50.0) - self.wander_radius_spinbox.setSingleStep(0.5) - self.wander_radius_spinbox.setValue(self.config.get("wander_radius", 5.0)) - self.wander_radius_spinbox.valueChanged.connect(self.update_wander_radius) - wander_layout.addRow("游走半径:", self.wander_radius_spinbox) - - self.wander_jitter_spinbox = QDoubleSpinBox() - self.wander_jitter_spinbox.setRange(0.0, 5.0) - self.wander_jitter_spinbox.setSingleStep(0.05) - self.wander_jitter_spinbox.setValue(self.config.get("wander_jitter", 1.0)) - self.wander_jitter_spinbox.valueChanged.connect(self.update_wander_jitter) - wander_layout.addRow("游走抖动:", self.wander_jitter_spinbox) - - wander_group.setLayout(wander_layout) - layout.addWidget(wander_group) - - # 躲避捕食者行为 - predator_group = QGroupBox("躲避捕食者") - predator_layout = QFormLayout() - - self.predator_checkbox = QCheckBox("启用躲避捕食者") - self.predator_checkbox.setChecked(self.config.get("predator_avoid_enabled", False)) - self.predator_checkbox.stateChanged.connect(self.toggle_predator_avoidance) - predator_layout.addRow("", self.predator_checkbox) - - self.predator_weight_spinbox = QDoubleSpinBox() - self.predator_weight_spinbox.setRange(0.0, 10.0) - self.predator_weight_spinbox.setSingleStep(0.1) - self.predator_weight_spinbox.setValue(self.config.get("predator_avoid_weight", 3.0)) - self.predator_weight_spinbox.valueChanged.connect(self.update_predator_weight) - predator_layout.addRow("躲避权重:", self.predator_weight_spinbox) - - self.predator_radius_spinbox = QDoubleSpinBox() - self.predator_radius_spinbox.setRange(1.0, 100.0) - self.predator_radius_spinbox.setSingleStep(1.0) - self.predator_radius_spinbox.setValue(self.config.get("predator_radius", 15.0)) - self.predator_radius_spinbox.valueChanged.connect(self.update_predator_radius) - predator_layout.addRow("感知半径:", self.predator_radius_spinbox) - - predator_group.setLayout(predator_layout) - layout.addWidget(predator_group) - - # 到达行为 - arrival_group = QGroupBox("到达行为") - arrival_layout = QFormLayout() - - self.arrival_checkbox = QCheckBox("启用到达行为") - self.arrival_checkbox.setChecked(self.config.get("arrival_enabled", False)) - self.arrival_checkbox.stateChanged.connect(self.toggle_arrival) - arrival_layout.addRow("", self.arrival_checkbox) - - self.arrival_slow_radius_spinbox = QDoubleSpinBox() - self.arrival_slow_radius_spinbox.setRange(0.1, 50.0) - self.arrival_slow_radius_spinbox.setSingleStep(0.5) - self.arrival_slow_radius_spinbox.setValue(self.config.get("arrival_slow_radius", 5.0)) - self.arrival_slow_radius_spinbox.valueChanged.connect(self.update_arrival_slow_radius) - arrival_layout.addRow("减速半径:", self.arrival_slow_radius_spinbox) - - arrival_group.setLayout(arrival_layout) - layout.addWidget(arrival_group) - - layout.addStretch() - widget.setLayout(layout) - return widget - - def create_formation_tab(self): - """ - 创建队形控制标签页 - """ - widget = QWidget() - layout = QVBoxLayout() - - # 队形控制 - formation_group = QGroupBox("队形控制") - formation_layout = QFormLayout() - - self.formation_checkbox = QCheckBox("启用队形保持") - self.formation_checkbox.setChecked(self.config.get("formation_enabled", False)) - self.formation_checkbox.stateChanged.connect(self.toggle_formation) - formation_layout.addRow("", self.formation_checkbox) - - self.formation_combo = QComboBox() - self.formation_combo.addItems(["无", "V字形", "直线", "圆形", "楔形", "球形"]) - self.formation_combo.setCurrentText(self.config.get("formation_type", "无")) - self.formation_combo.currentTextChanged.connect(self.update_formation_type) - formation_layout.addRow("队形类型:", self.formation_combo) - - self.formation_weight_spinbox = QDoubleSpinBox() - self.formation_weight_spinbox.setRange(0.0, 10.0) - self.formation_weight_spinbox.setSingleStep(0.1) - self.formation_weight_spinbox.setValue(self.config.get("formation_weight", 1.0)) - self.formation_weight_spinbox.valueChanged.connect(self.update_formation_weight) - formation_layout.addRow("队形权重:", self.formation_weight_spinbox) - - formation_group.setLayout(formation_layout) - layout.addWidget(formation_group) - - # 高级队形参数 - advanced_formation_group = QGroupBox("高级队形参数") - advanced_formation_layout = QFormLayout() - - self.personal_space_spinbox = QDoubleSpinBox() - self.personal_space_spinbox.setRange(0.1, 20.0) - self.personal_space_spinbox.setSingleStep(0.1) - self.personal_space_spinbox.setValue(self.config.get("personal_space", 2.0)) - self.personal_space_spinbox.valueChanged.connect(self.update_personal_space) - advanced_formation_layout.addRow("个人空间:", self.personal_space_spinbox) - - advanced_formation_group.setLayout(advanced_formation_layout) - layout.addWidget(advanced_formation_group) - - # 路径跟随 - path_group = QGroupBox("路径跟随") - path_layout = QFormLayout() - - self.path_checkbox = QCheckBox("启用路径跟随") - self.path_checkbox.setChecked(self.config.get("path_following_enabled", False)) - self.path_checkbox.stateChanged.connect(self.toggle_path_following) - path_layout.addRow("", self.path_checkbox) - - self.path_weight_spinbox = QDoubleSpinBox() - self.path_weight_spinbox.setRange(0.0, 10.0) - self.path_weight_spinbox.setSingleStep(0.1) - self.path_weight_spinbox.setValue(self.config.get("path_weight", 1.0)) - self.path_weight_spinbox.valueChanged.connect(self.update_path_weight) - path_layout.addRow("路径权重:", self.path_weight_spinbox) - - self.path_radius_spinbox = QDoubleSpinBox() - self.path_radius_spinbox.setRange(0.1, 50.0) - self.path_radius_spinbox.setSingleStep(0.5) - self.path_radius_spinbox.setValue(self.config.get("path_radius", 3.0)) - self.path_radius_spinbox.valueChanged.connect(self.update_path_radius) - path_layout.addRow("路径半径:", self.path_radius_spinbox) - - path_group.setLayout(path_layout) - layout.addWidget(path_group) - - # 预测参数 - prediction_group = QGroupBox("预测参数") - prediction_layout = QFormLayout() - - self.prediction_distance_spinbox = QDoubleSpinBox() - self.prediction_distance_spinbox.setRange(0.1, 100.0) - self.prediction_distance_spinbox.setSingleStep(1.0) - self.prediction_distance_spinbox.setValue(self.config.get("prediction_distance", 10.0)) - self.prediction_distance_spinbox.valueChanged.connect(self.update_prediction_distance) - prediction_layout.addRow("预测距离:", self.prediction_distance_spinbox) - - prediction_group.setLayout(prediction_layout) - layout.addWidget(prediction_group) - - # 物理参数 - physics_group = QGroupBox("物理参数") - physics_layout = QFormLayout() - - self.gravity_checkbox = QCheckBox("启用重力") - self.gravity_checkbox.setChecked(self.config.get("gravity_enabled", True)) - self.gravity_checkbox.stateChanged.connect(self.toggle_gravity) - physics_layout.addRow("", self.gravity_checkbox) - - self.air_resistance_checkbox = QCheckBox("启用空气阻力") - self.air_resistance_checkbox.setChecked(self.config.get("air_resistance_enabled", True)) - self.air_resistance_checkbox.stateChanged.connect(self.toggle_air_resistance) - physics_layout.addRow("", self.air_resistance_checkbox) - - self.buoyancy_checkbox = QCheckBox("启用浮力") - self.buoyancy_checkbox.setChecked(self.config.get("buoyancy_enabled", False)) - self.buoyancy_checkbox.stateChanged.connect(self.toggle_buoyancy) - physics_layout.addRow("", self.buoyancy_checkbox) - - self.fluid_level_spinbox = QDoubleSpinBox() - self.fluid_level_spinbox.setRange(-100.0, 100.0) - self.fluid_level_spinbox.setSingleStep(1.0) - self.fluid_level_spinbox.setValue(self.config.get("fluid_level", 0.0)) - self.fluid_level_spinbox.valueChanged.connect(self.update_fluid_level) - physics_layout.addRow("流体水平面:", self.fluid_level_spinbox) - - physics_group.setLayout(physics_layout) - layout.addWidget(physics_group) - - # 性能优化参数 - optimization_group = QGroupBox("性能优化") - optimization_layout = QFormLayout() - - self.spatial_partitioning_checkbox = QCheckBox("启用空间分区") - self.spatial_partitioning_checkbox.setChecked(self.config.get("spatial_partitioning", True)) - self.spatial_partitioning_checkbox.stateChanged.connect(self.toggle_spatial_partitioning) - optimization_layout.addRow("", self.spatial_partitioning_checkbox) - - self.neighbor_optimization_checkbox = QCheckBox("启用邻居优化") - self.neighbor_optimization_checkbox.setChecked(self.config.get("neighbor_search_optimization", True)) - self.neighbor_optimization_checkbox.stateChanged.connect(self.toggle_neighbor_optimization) - optimization_layout.addRow("", self.neighbor_optimization_checkbox) - - self.parallel_computation_checkbox = QCheckBox("启用并行计算") - self.parallel_computation_checkbox.setChecked(self.config.get("parallel_computation", True)) - self.parallel_computation_checkbox.stateChanged.connect(self.toggle_parallel_computation) - optimization_layout.addRow("", self.parallel_computation_checkbox) - - optimization_group.setLayout(optimization_layout) - layout.addWidget(optimization_group) - - layout.addStretch() - widget.setLayout(layout) - return widget - - def create_status_tab(self): - """ - 创建状态信息标签页 - """ - widget = QWidget() - layout = QVBoxLayout() - - # 群体状态 - status_group = QGroupBox("群体状态") - status_layout = QFormLayout() - - self.member_count_label = QLabel("0") - status_layout.addRow("成员数量:", self.member_count_label) - - self.paused_label = QLabel("否") - status_layout.addRow("是否暂停:", self.paused_label) - - self.fps_label = QLabel("0") - status_layout.addRow("帧率:", self.fps_label) - - status_group.setLayout(status_layout) - layout.addWidget(status_group) - - # 性能信息 - performance_group = QGroupBox("性能信息") - performance_layout = QVBoxLayout() - - self.performance_text = QTextEdit() - self.performance_text.setReadOnly(True) - self.performance_text.setMaximumHeight(200) - performance_layout.addWidget(self.performance_text) - - performance_group.setLayout(performance_layout) - layout.addWidget(performance_group) - - # 日志信息 - log_group = QGroupBox("日志信息") - log_layout = QVBoxLayout() - - self.log_text = QTextEdit() - self.log_text.setReadOnly(True) - log_layout.addWidget(self.log_text) - - log_group.setLayout(log_layout) - layout.addWidget(log_group) - - layout.addStretch() - widget.setLayout(layout) - return widget - - def toggle_pause(self): - """ - 切换暂停状态 - """ - if self.swarm_manager: - self.swarm_manager.toggle_pause() - - def reset_swarm(self): - """ - 重置群体 - """ - if self.swarm_manager: - self.swarm_manager.cleanup() - self.swarm_manager.create_example_swarm() - self.update_status() - - def update_member_count(self, value): - """ - 更新群体数量 - """ - self.config.set("member_count", value) - # 如果当前群体数量与设置不符,重新创建群体 - if self.swarm_manager and len(self.swarm_manager.members) != value: - self.swarm_manager.cleanup() - self.swarm_manager.create_example_swarm() - - def update_swarm_type(self, value): - """ - 更新群体类型 - """ - self.config.set("swarm_type", value) - if self.swarm_manager: - self.swarm_manager.swarm_type = value - - def toggle_seek_target(self, state): - """ - 切换目标寻求 - """ - enabled = state == Qt.Checked - self.config.set("seek_enabled", enabled) - - def update_cohesion_weight(self, value): - """ - 更新聚集权重 - """ - self.config.set("cohesion_weight", value) - if self.swarm_manager: - self.swarm_manager.cohesion_weight = value - - def update_separation_weight(self, value): - """ - 更新分离权重 - """ - self.config.set("separation_weight", value) - if self.swarm_manager: - self.swarm_manager.separation_weight = value - - def update_alignment_weight(self, value): - """ - 更新对齐权重 - """ - self.config.set("alignment_weight", value) - if self.swarm_manager: - self.swarm_manager.alignment_weight = value - - def update_perception_radius(self, value): - """ - 更新感知半径 - """ - self.config.set("perception_radius", value) - if self.swarm_manager: - self.swarm_manager.perception_radius = value - - def update_max_speed(self, value): - """ - 更新最大速度 - """ - self.config.set("max_speed", value) - if self.swarm_manager: - self.swarm_manager.max_speed = value - - def update_max_acceleration(self, value): - """ - 更新最大加速度 - """ - self.config.set("max_acceleration", value) - - def update_max_force(self, value): - """ - 更新最大转向力 - """ - self.config.set("max_force", value) - - def toggle_boundary(self, state): - """ - 切换边界限制 - """ - enabled = state == Qt.Checked - self.config.set("boundary_enabled", enabled) - - def update_boundary_weight(self, value): - """ - 更新边界权重 - """ - self.config.set("boundary_weight", value) - - def update_boundary_buffer(self, value): - """ - 更新边界缓冲 - """ - self.config.set("boundary_buffer", value) - - def toggle_obstacle_avoidance(self, state): - """ - 切换避障行为 - """ - enabled = state == Qt.Checked - self.config.set("obstacle_avoidance_enabled", enabled) - - def update_obstacle_weight(self, value): - """ - 更新避障权重 - """ - self.config.set("obstacle_weight", value) - - def toggle_wander(self, state): - """ - 切换游走行为 - """ - enabled = state == Qt.Checked - self.config.set("wander_enabled", enabled) - - def update_wander_weight(self, value): - """ - 更新游走权重 - """ - self.config.set("wander_weight", value) - - def update_wander_radius(self, value): - """ - 更新游走半径 - """ - self.config.set("wander_radius", value) - - def update_wander_jitter(self, value): - """ - 更新游走抖动 - """ - self.config.set("wander_jitter", value) - - def toggle_predator_avoidance(self, state): - """ - 切换躲避捕食者行为 - """ - enabled = state == Qt.Checked - self.config.set("predator_avoid_enabled", enabled) - - def update_predator_weight(self, value): - """ - 更新躲避捕食者权重 - """ - self.config.set("predator_avoid_weight", value) - - def update_predator_radius(self, value): - """ - 更新捕食者感知半径 - """ - self.config.set("predator_radius", value) - - def toggle_arrival(self, state): - """ - 切换到达行为 - """ - enabled = state == Qt.Checked - self.config.set("arrival_enabled", enabled) - - def update_arrival_slow_radius(self, value): - """ - 更新到达减速半径 - """ - self.config.set("arrival_slow_radius", value) - - def toggle_formation(self, state): - """ - 切换队形保持 - """ - enabled = state == Qt.Checked - self.config.set("formation_enabled", enabled) - - def update_formation_type(self, value): - """ - 更新队形类型 - """ - self.config.set("formation_type", value) - - def update_formation_weight(self, value): - """ - 更新队形权重 - """ - self.config.set("formation_weight", value) - - def toggle_path_following(self, state): - """ - 切换路径跟随 - """ - enabled = state == Qt.Checked - self.config.set("path_following_enabled", enabled) - - def update_path_weight(self, value): - """ - 更新路径权重 - """ - self.config.set("path_weight", value) - - def update_path_radius(self, value): - """ - 更新路径半径 - """ - self.config.set("path_radius", value) - - def update_prediction_distance(self, value): - """ - 更新预测距离 - """ - self.config.set("prediction_distance", value) - - def update_personal_space(self, value): - """ - 更新个人空间 - """ - self.config.set("personal_space", value) - - def toggle_gravity(self, state): - """ - 切换重力 - """ - enabled = state == Qt.Checked - self.config.set("gravity_enabled", enabled) - - def toggle_air_resistance(self, state): - """ - 切换空气阻力 - """ - enabled = state == Qt.Checked - self.config.set("air_resistance_enabled", enabled) - - def toggle_buoyancy(self, state): - """ - 切换浮力 - """ - enabled = state == Qt.Checked - self.config.set("buoyancy_enabled", enabled) - - def update_fluid_level(self, value): - """ - 更新流体水平面 - """ - self.config.set("fluid_level", value) - - def toggle_spatial_partitioning(self, state): - """ - 切换空间分区 - """ - enabled = state == Qt.Checked - self.config.set("spatial_partitioning", enabled) - - def toggle_neighbor_optimization(self, state): - """ - 切换邻居优化 - """ - enabled = state == Qt.Checked - self.config.set("neighbor_search_optimization", enabled) - - def toggle_parallel_computation(self, state): - """ - 切换并行计算 - """ - enabled = state == Qt.Checked - self.config.set("parallel_computation", enabled) - - def toggle_interaction(self, state): - """ - 切换群体间交互 - """ - enabled = state == Qt.Checked - self.config.set("interaction_enabled", enabled) - - def toggle_learning(self, state): - """ - 切换学习能力 - """ - enabled = state == Qt.Checked - self.config.set("learning_enabled", enabled) - - def toggle_environment_awareness(self, state): - """ - 切换环境感知 - """ - enabled = state == Qt.Checked - self.config.set("environment_awareness_enabled", enabled) - - def update_sensor_range(self, value): - """ - 更新传感器范围 - """ - self.config.set("sensor_range", value) - - def update_sensor_accuracy(self, value): - """ - 更新传感器精度 - """ - self.config.set("sensor_accuracy", value) - - def update_obstacle_response_weight(self, value): - """ - 更新障碍物响应权重 - """ - self.config.set("obstacle_response_weight", value) - - def update_predator_response_weight(self, value): - """ - 更新捕食者响应权重 - """ - self.config.set("predator_response_weight", value) - - def update_resource_seek_weight(self, value): - """ - 更新资源寻求权重 - """ - self.config.set("resource_seek_weight", value) - - def update_hazard_avoid_weight(self, value): - """ - 更新危险回避权重 - """ - self.config.set("hazard_avoid_weight", value) - - def update_time_flow_rate(self, value): - """ - 更新时间流逝速度 - """ - self.config.set("time_flow_rate", value) - - def update_learning_rate(self, value): - """ - 更新学习率 - """ - self.config.set("learning_rate", value) - if hasattr(self.swarm_manager, 'learning_manager'): - self.swarm_manager.learning_manager.learning_rate = value - - def update_exploration_rate(self, value): - """ - 更新探索率 - """ - self.config.set("exploration_rate", value) - if hasattr(self.swarm_manager, 'learning_manager'): - self.swarm_manager.learning_manager.exploration_rate = value - - def update_memory_capacity(self, value): - """ - 更新记忆容量 - """ - self.config.set("memory_capacity", value) - if hasattr(self.swarm_manager, 'learning_manager'): - self.swarm_manager.learning_manager.memory_capacity = value - - def clear_learning_memory(self): - """ - 清空学习记忆 - """ - if hasattr(self.swarm_manager, 'learning_manager'): - self.swarm_manager.learning_manager.clear_memory() - - def create_learning_tab(self): - """ - 创建学习控制标签页 - """ - widget = QWidget() - layout = QVBoxLayout() - - # 学习开关 - learning_group = QGroupBox("学习控制") - learning_layout = QVBoxLayout() - - self.learning_checkbox = QCheckBox("启用学习能力") - self.learning_checkbox.setChecked(self.config.get("learning_enabled", False)) - self.learning_checkbox.stateChanged.connect(self.toggle_learning) - learning_layout.addWidget(self.learning_checkbox) - - clear_memory_button = QPushButton("清空学习记忆") - clear_memory_button.clicked.connect(self.clear_learning_memory) - learning_layout.addWidget(clear_memory_button) - - learning_group.setLayout(learning_layout) - layout.addWidget(learning_group) - - # 学习参数 - params_group = QGroupBox("学习参数") - params_layout = QFormLayout() - - # 学习率 - self.learning_rate_spinbox = QDoubleSpinBox() - self.learning_rate_spinbox.setRange(0.01, 1.0) - self.learning_rate_spinbox.setSingleStep(0.01) - self.learning_rate_spinbox.setValue(self.config.get("learning_rate", 0.1)) - self.learning_rate_spinbox.valueChanged.connect(self.update_learning_rate) - params_layout.addRow("学习率:", self.learning_rate_spinbox) - - # 探索率 - self.exploration_rate_spinbox = QDoubleSpinBox() - self.exploration_rate_spinbox.setRange(0.0, 1.0) - self.exploration_rate_spinbox.setSingleStep(0.05) - self.exploration_rate_spinbox.setValue(self.config.get("exploration_rate", 0.3)) - self.exploration_rate_spinbox.valueChanged.connect(self.update_exploration_rate) - params_layout.addRow("探索率:", self.exploration_rate_spinbox) - - # 记忆容量 - self.memory_capacity_spinbox = QSpinBox() - self.memory_capacity_spinbox.setRange(100, 10000) - self.memory_capacity_spinbox.setSingleStep(100) - self.memory_capacity_spinbox.setValue(self.config.get("memory_capacity", 1000)) - self.memory_capacity_spinbox.valueChanged.connect(self.update_memory_capacity) - params_layout.addRow("记忆容量:", self.memory_capacity_spinbox) - - params_group.setLayout(params_layout) - layout.addWidget(params_group) - - # 学习统计信息 - stats_group = QGroupBox("学习统计") - stats_layout = QFormLayout() - - self.experience_count_label = QLabel("0") - stats_layout.addRow("经验数量:", self.experience_count_label) - - self.policy_count_label = QLabel("0") - stats_layout.addRow("策略数量:", self.policy_count_label) - - self.exploration_rate_label = QLabel("0.0") - stats_layout.addRow("当前探索率:", self.exploration_rate_label) - - stats_group.setLayout(stats_layout) - layout.addWidget(stats_group) - - layout.addStretch() - widget.setLayout(layout) - return widget - - def update_status(self): - """ - 更新状态信息 - """ - if self.swarm_manager: - count = len(self.swarm_manager.members) - self.member_count_label.setText(str(count)) - self.paused_label.setText("是" if self.swarm_manager.paused else "否") - - # 更新性能信息 - performance_info = f"成员数量: {count}\n" - performance_info += f"是否暂停: {'是' if self.swarm_manager.paused else '否'}\n" - performance_info += f"感知半径: {self.config.get('perception_radius', 10.0)}\n" - performance_info += f"最大速度: {self.config.get('max_speed', 5.0)}\n" - performance_info += f"成员数量: {self.config.get('member_count', 20)}\n" - performance_info += f"并行计算: {'是' if self.config.get('parallel_computation', True) else '否'}\n" - - # 添加学习统计信息 - if hasattr(self.swarm_manager, 'learning_manager') and self.config.get("learning_enabled", False): - learning_stats = self.swarm_manager.learning_manager.get_learning_stats() - performance_info += f"经验数量: {learning_stats['experience_count']}\n" - performance_info += f"策略数量: {learning_stats['policy_count']}\n" - performance_info += f"探索率: {learning_stats['exploration_rate']:.2f}\n" - performance_info += f"学习率: {learning_stats['learning_rate']:.2f}\n" - - # 更新学习统计标签 - if hasattr(self, 'experience_count_label'): - self.experience_count_label.setText(str(learning_stats['experience_count'])) - if hasattr(self, 'policy_count_label'): - self.policy_count_label.setText(str(learning_stats['policy_count'])) - if hasattr(self, 'exploration_rate_label'): - self.exploration_rate_label.setText(f"{learning_stats['exploration_rate']:.2f}") - +""" +群体智能插件GUI界面 +""" + +from PyQt5.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QSlider, QLabel, + QGroupBox, QCheckBox, QComboBox, QSpinBox, QDoubleSpinBox, QTabWidget, + QFormLayout, QTextEdit +) +from PyQt5.QtCore import Qt, QTimer + +class SwarmGUI(QWidget): + """ + 群体智能插件GUI界面 + 提供完整的控制面板用于调节群体行为参数 + """ + + def __init__(self, swarm_manager, config): + super().__init__() + self.swarm_manager = swarm_manager + self.config = config + self.init_ui() + # 启动定时器更新状态信息 + self.timer = QTimer(self) + self.timer.timeout.connect(self.update_status) + self.timer.start(1000) # 每秒更新一次 + + def init_ui(self): + """ + 初始化UI界面 + """ + self.setWindowTitle('群体智能控制面板') + self.setGeometry(100, 100, 400, 600) + + # 创建标签页 + tab_widget = QTabWidget() + + # 基本控制标签页 + basic_tab = self.create_basic_tab() + tab_widget.addTab(basic_tab, "基本控制") + + # Boids参数标签页 + boids_tab = self.create_boids_tab() + tab_widget.addTab(boids_tab, "Boids参数") + + # 扩展行为标签页 + behavior_tab = self.create_behavior_tab() + tab_widget.addTab(behavior_tab, "扩展行为") + + # 队形控制标签页 + formation_tab = self.create_formation_tab() + tab_widget.addTab(formation_tab, "队形控制") + + # 学习控制标签页 + learning_tab = self.create_learning_tab() + tab_widget.addTab(learning_tab, "学习控制") + + # 环境感知标签页 + environment_tab = self.create_environment_tab() + tab_widget.addTab(environment_tab, "环境感知") + + # 状态信息标签页 + status_tab = self.create_status_tab() + tab_widget.addTab(status_tab, "状态信息") + + # 主布局 + main_layout = QVBoxLayout() + main_layout.addWidget(tab_widget) + self.setLayout(main_layout) + + def create_environment_tab(self): + """ + 创建环境感知标签页 + """ + widget = QWidget() + layout = QVBoxLayout() + + # 环境感知开关 + awareness_group = QGroupBox("环境感知控制") + awareness_layout = QVBoxLayout() + + self.environment_awareness_checkbox = QCheckBox("启用环境感知") + self.environment_awareness_checkbox.setChecked(self.config.get("environment_awareness_enabled", True)) + self.environment_awareness_checkbox.stateChanged.connect(self.toggle_environment_awareness) + awareness_layout.addWidget(self.environment_awareness_checkbox) + + awareness_group.setLayout(awareness_layout) + layout.addWidget(awareness_group) + + # 传感器参数 + sensor_group = QGroupBox("传感器参数") + sensor_layout = QFormLayout() + + # 传感器范围 + self.sensor_range_spinbox = QDoubleSpinBox() + self.sensor_range_spinbox.setRange(1.0, 50.0) + self.sensor_range_spinbox.setSingleStep(1.0) + self.sensor_range_spinbox.setValue(self.config.get("sensor_range", 15.0)) + self.sensor_range_spinbox.valueChanged.connect(self.update_sensor_range) + sensor_layout.addRow("传感器范围:", self.sensor_range_spinbox) + + # 传感器精度 + self.sensor_accuracy_spinbox = QDoubleSpinBox() + self.sensor_accuracy_spinbox.setRange(0.1, 1.0) + self.sensor_accuracy_spinbox.setSingleStep(0.05) + self.sensor_accuracy_spinbox.setValue(self.config.get("sensor_accuracy", 0.9)) + self.sensor_accuracy_spinbox.valueChanged.connect(self.update_sensor_accuracy) + sensor_layout.addRow("传感器精度:", self.sensor_accuracy_spinbox) + + sensor_group.setLayout(sensor_layout) + layout.addWidget(sensor_group) + + # 环境响应参数 + response_group = QGroupBox("环境响应参数") + response_layout = QFormLayout() + + # 障碍物响应权重 + self.obstacle_response_weight_spinbox = QDoubleSpinBox() + self.obstacle_response_weight_spinbox.setRange(0.0, 5.0) + self.obstacle_response_weight_spinbox.setSingleStep(0.1) + self.obstacle_response_weight_spinbox.setValue(self.config.get("obstacle_response_weight", 2.0)) + self.obstacle_response_weight_spinbox.valueChanged.connect(self.update_obstacle_response_weight) + response_layout.addRow("障碍物响应权重:", self.obstacle_response_weight_spinbox) + + # 捕食者响应权重 + self.predator_response_weight_spinbox = QDoubleSpinBox() + self.predator_response_weight_spinbox.setRange(0.0, 5.0) + self.predator_response_weight_spinbox.setSingleStep(0.1) + self.predator_response_weight_spinbox.setValue(self.config.get("predator_response_weight", 3.0)) + self.predator_response_weight_spinbox.valueChanged.connect(self.update_predator_response_weight) + response_layout.addRow("捕食者响应权重:", self.predator_response_weight_spinbox) + + # 资源寻求权重 + self.resource_seek_weight_spinbox = QDoubleSpinBox() + self.resource_seek_weight_spinbox.setRange(0.0, 2.0) + self.resource_seek_weight_spinbox.setSingleStep(0.05) + self.resource_seek_weight_spinbox.setValue(self.config.get("resource_seek_weight", 0.5)) + self.resource_seek_weight_spinbox.valueChanged.connect(self.update_resource_seek_weight) + response_layout.addRow("资源寻求权重:", self.resource_seek_weight_spinbox) + + # 危险回避权重 + self.hazard_avoid_weight_spinbox = QDoubleSpinBox() + self.hazard_avoid_weight_spinbox.setRange(0.0, 5.0) + self.hazard_avoid_weight_spinbox.setSingleStep(0.1) + self.hazard_avoid_weight_spinbox.setValue(self.config.get("hazard_avoid_weight", 3.0)) + self.hazard_avoid_weight_spinbox.valueChanged.connect(self.update_hazard_avoid_weight) + response_layout.addRow("危险回避权重:", self.hazard_avoid_weight_spinbox) + + response_group.setLayout(response_layout) + layout.addWidget(response_group) + + # 环境动态参数 + dynamics_group = QGroupBox("环境动态") + dynamics_layout = QFormLayout() + + # 时间流逝速度 + self.time_flow_rate_spinbox = QDoubleSpinBox() + self.time_flow_rate_spinbox.setRange(0.01, 2.0) + self.time_flow_rate_spinbox.setSingleStep(0.05) + self.time_flow_rate_spinbox.setValue(self.config.get("time_flow_rate", 0.1)) + self.time_flow_rate_spinbox.valueChanged.connect(self.update_time_flow_rate) + dynamics_layout.addRow("时间流逝速度:", self.time_flow_rate_spinbox) + + dynamics_group.setLayout(dynamics_layout) + layout.addWidget(dynamics_group) + + layout.addStretch() + widget.setLayout(layout) + return widget + + def create_basic_tab(self): + """ + 创建基本控制标签页 + """ + widget = QWidget() + layout = QVBoxLayout() + + # 控制按钮组 + control_group = QGroupBox("基本控制") + control_layout = QVBoxLayout() + + # 控制按钮 + button_layout = QHBoxLayout() + self.pause_button = QPushButton("暂停/继续") + self.pause_button.clicked.connect(self.toggle_pause) + button_layout.addWidget(self.pause_button) + + reset_button = QPushButton("重置群体") + reset_button.clicked.connect(self.reset_swarm) + button_layout.addWidget(reset_button) + + control_layout.addLayout(button_layout) + + # 群体数量控制 + member_layout = QHBoxLayout() + member_layout.addWidget(QLabel("群体数量:")) + self.member_count_spinbox = QSpinBox() + self.member_count_spinbox.setRange(1, 1000) + self.member_count_spinbox.setValue(self.config.get("member_count", 20)) + self.member_count_spinbox.valueChanged.connect(self.update_member_count) + member_layout.addWidget(self.member_count_spinbox) + + control_layout.addLayout(member_layout) + + control_group.setLayout(control_layout) + layout.addWidget(control_group) + + # 群体类型选择 + type_group = QGroupBox("群体类型") + type_layout = QVBoxLayout() + + self.type_combo = QComboBox() + self.type_combo.addItems(["鸟类", "鱼类", "昆虫", "自定义"]) + self.type_combo.setCurrentText(self.config.get("swarm_type", "鸟类")) + self.type_combo.currentTextChanged.connect(self.update_swarm_type) + type_layout.addWidget(self.type_combo) + + type_group.setLayout(type_layout) + layout.addWidget(type_group) + + # 目标控制 + target_group = QGroupBox("目标控制") + target_layout = QVBoxLayout() + + self.seek_target_checkbox = QCheckBox("启用目标寻求") + self.seek_target_checkbox.setChecked(self.config.get("seek_enabled", False)) + self.seek_target_checkbox.stateChanged.connect(self.toggle_seek_target) + target_layout.addWidget(self.seek_target_checkbox) + + target_group.setLayout(target_layout) + layout.addWidget(target_group) + + layout.addStretch() + widget.setLayout(layout) + return widget + + def create_boids_tab(self): + """ + 创建Boids参数标签页 + """ + widget = QWidget() + layout = QVBoxLayout() + + # 基本Boids参数 + boids_group = QGroupBox("基本Boids参数") + boids_layout = QFormLayout() + + # 聚集权重 + self.cohesion_spinbox = QDoubleSpinBox() + self.cohesion_spinbox.setRange(0.0, 10.0) + self.cohesion_spinbox.setSingleStep(0.1) + self.cohesion_spinbox.setValue(self.config.get("cohesion_weight", 1.0)) + self.cohesion_spinbox.valueChanged.connect(self.update_cohesion_weight) + boids_layout.addRow("聚集权重:", self.cohesion_spinbox) + + # 分离权重 + self.separation_spinbox = QDoubleSpinBox() + self.separation_spinbox.setRange(0.0, 10.0) + self.separation_spinbox.setSingleStep(0.1) + self.separation_spinbox.setValue(self.config.get("separation_weight", 1.5)) + self.separation_spinbox.valueChanged.connect(self.update_separation_weight) + boids_layout.addRow("分离权重:", self.separation_spinbox) + + # 对齐权重 + self.alignment_spinbox = QDoubleSpinBox() + self.alignment_spinbox.setRange(0.0, 10.0) + self.alignment_spinbox.setSingleStep(0.1) + self.alignment_spinbox.setValue(self.config.get("alignment_weight", 1.0)) + self.alignment_spinbox.valueChanged.connect(self.update_alignment_weight) + boids_layout.addRow("对齐权重:", self.alignment_spinbox) + + # 感知半径 + self.perception_spinbox = QDoubleSpinBox() + self.perception_spinbox.setRange(0.1, 100.0) + self.perception_spinbox.setSingleStep(0.5) + self.perception_spinbox.setValue(self.config.get("perception_radius", 10.0)) + self.perception_spinbox.valueChanged.connect(self.update_perception_radius) + boids_layout.addRow("感知半径:", self.perception_spinbox) + + boids_group.setLayout(boids_layout) + layout.addWidget(boids_group) + + # 移动参数 + movement_group = QGroupBox("移动参数") + movement_layout = QFormLayout() + + # 最大速度 + self.max_speed_spinbox = QDoubleSpinBox() + self.max_speed_spinbox.setRange(0.1, 50.0) + self.max_speed_spinbox.setSingleStep(0.5) + self.max_speed_spinbox.setValue(self.config.get("max_speed", 5.0)) + self.max_speed_spinbox.valueChanged.connect(self.update_max_speed) + movement_layout.addRow("最大速度:", self.max_speed_spinbox) + + # 最大加速度 + self.max_acceleration_spinbox = QDoubleSpinBox() + self.max_acceleration_spinbox.setRange(0.01, 10.0) + self.max_acceleration_spinbox.setSingleStep(0.05) + self.max_acceleration_spinbox.setValue(self.config.get("max_acceleration", 1.0)) + self.max_acceleration_spinbox.valueChanged.connect(self.update_max_acceleration) + movement_layout.addRow("最大加速度:", self.max_acceleration_spinbox) + + # 最大转向力 + self.max_force_spinbox = QDoubleSpinBox() + self.max_force_spinbox.setRange(0.01, 5.0) + self.max_force_spinbox.setSingleStep(0.05) + self.max_force_spinbox.setValue(self.config.get("max_force", 0.5)) + self.max_force_spinbox.valueChanged.connect(self.update_max_force) + movement_layout.addRow("最大转向力:", self.max_force_spinbox) + + movement_group.setLayout(movement_layout) + layout.addWidget(movement_group) + + # 边界参数 + boundary_group = QGroupBox("边界参数") + boundary_layout = QFormLayout() + + self.boundary_checkbox = QCheckBox("启用边界限制") + self.boundary_checkbox.setChecked(self.config.get("boundary_enabled", True)) + self.boundary_checkbox.stateChanged.connect(self.toggle_boundary) + boundary_layout.addRow("", self.boundary_checkbox) + + self.boundary_weight_spinbox = QDoubleSpinBox() + self.boundary_weight_spinbox.setRange(0.0, 10.0) + self.boundary_weight_spinbox.setSingleStep(0.1) + self.boundary_weight_spinbox.setValue(self.config.get("boundary_weight", 2.0)) + self.boundary_weight_spinbox.valueChanged.connect(self.update_boundary_weight) + boundary_layout.addRow("边界权重:", self.boundary_weight_spinbox) + + self.boundary_buffer_spinbox = QDoubleSpinBox() + self.boundary_buffer_spinbox.setRange(0.1, 50.0) + self.boundary_buffer_spinbox.setSingleStep(0.5) + self.boundary_buffer_spinbox.setValue(self.config.get("boundary_buffer", 5.0)) + self.boundary_buffer_spinbox.valueChanged.connect(self.update_boundary_buffer) + boundary_layout.addRow("边界缓冲:", self.boundary_buffer_spinbox) + + boundary_group.setLayout(boundary_layout) + layout.addWidget(boundary_group) + + layout.addStretch() + widget.setLayout(layout) + return widget + + def create_behavior_tab(self): + """ + 创建扩展行为标签页 + """ + widget = QWidget() + layout = QVBoxLayout() + + # 避障行为 + obstacle_group = QGroupBox("避障行为") + obstacle_layout = QFormLayout() + + self.obstacle_checkbox = QCheckBox("启用避障") + self.obstacle_checkbox.setChecked(self.config.get("obstacle_avoidance_enabled", True)) + self.obstacle_checkbox.stateChanged.connect(self.toggle_obstacle_avoidance) + obstacle_layout.addRow("", self.obstacle_checkbox) + + self.obstacle_weight_spinbox = QDoubleSpinBox() + self.obstacle_weight_spinbox.setRange(0.0, 10.0) + self.obstacle_weight_spinbox.setSingleStep(0.1) + self.obstacle_weight_spinbox.setValue(self.config.get("obstacle_weight", 2.0)) + self.obstacle_weight_spinbox.valueChanged.connect(self.update_obstacle_weight) + obstacle_layout.addRow("避障权重:", self.obstacle_weight_spinbox) + + obstacle_group.setLayout(obstacle_layout) + layout.addWidget(obstacle_group) + + # 游走行为 + wander_group = QGroupBox("游走行为") + wander_layout = QFormLayout() + + self.wander_checkbox = QCheckBox("启用游走") + self.wander_checkbox.setChecked(self.config.get("wander_enabled", False)) + self.wander_checkbox.stateChanged.connect(self.toggle_wander) + wander_layout.addRow("", self.wander_checkbox) + + self.wander_weight_spinbox = QDoubleSpinBox() + self.wander_weight_spinbox.setRange(0.0, 5.0) + self.wander_weight_spinbox.setSingleStep(0.05) + self.wander_weight_spinbox.setValue(self.config.get("wander_weight", 0.5)) + self.wander_weight_spinbox.valueChanged.connect(self.update_wander_weight) + wander_layout.addRow("游走权重:", self.wander_weight_spinbox) + + self.wander_radius_spinbox = QDoubleSpinBox() + self.wander_radius_spinbox.setRange(0.1, 50.0) + self.wander_radius_spinbox.setSingleStep(0.5) + self.wander_radius_spinbox.setValue(self.config.get("wander_radius", 5.0)) + self.wander_radius_spinbox.valueChanged.connect(self.update_wander_radius) + wander_layout.addRow("游走半径:", self.wander_radius_spinbox) + + self.wander_jitter_spinbox = QDoubleSpinBox() + self.wander_jitter_spinbox.setRange(0.0, 5.0) + self.wander_jitter_spinbox.setSingleStep(0.05) + self.wander_jitter_spinbox.setValue(self.config.get("wander_jitter", 1.0)) + self.wander_jitter_spinbox.valueChanged.connect(self.update_wander_jitter) + wander_layout.addRow("游走抖动:", self.wander_jitter_spinbox) + + wander_group.setLayout(wander_layout) + layout.addWidget(wander_group) + + # 躲避捕食者行为 + predator_group = QGroupBox("躲避捕食者") + predator_layout = QFormLayout() + + self.predator_checkbox = QCheckBox("启用躲避捕食者") + self.predator_checkbox.setChecked(self.config.get("predator_avoid_enabled", False)) + self.predator_checkbox.stateChanged.connect(self.toggle_predator_avoidance) + predator_layout.addRow("", self.predator_checkbox) + + self.predator_weight_spinbox = QDoubleSpinBox() + self.predator_weight_spinbox.setRange(0.0, 10.0) + self.predator_weight_spinbox.setSingleStep(0.1) + self.predator_weight_spinbox.setValue(self.config.get("predator_avoid_weight", 3.0)) + self.predator_weight_spinbox.valueChanged.connect(self.update_predator_weight) + predator_layout.addRow("躲避权重:", self.predator_weight_spinbox) + + self.predator_radius_spinbox = QDoubleSpinBox() + self.predator_radius_spinbox.setRange(1.0, 100.0) + self.predator_radius_spinbox.setSingleStep(1.0) + self.predator_radius_spinbox.setValue(self.config.get("predator_radius", 15.0)) + self.predator_radius_spinbox.valueChanged.connect(self.update_predator_radius) + predator_layout.addRow("感知半径:", self.predator_radius_spinbox) + + predator_group.setLayout(predator_layout) + layout.addWidget(predator_group) + + # 到达行为 + arrival_group = QGroupBox("到达行为") + arrival_layout = QFormLayout() + + self.arrival_checkbox = QCheckBox("启用到达行为") + self.arrival_checkbox.setChecked(self.config.get("arrival_enabled", False)) + self.arrival_checkbox.stateChanged.connect(self.toggle_arrival) + arrival_layout.addRow("", self.arrival_checkbox) + + self.arrival_slow_radius_spinbox = QDoubleSpinBox() + self.arrival_slow_radius_spinbox.setRange(0.1, 50.0) + self.arrival_slow_radius_spinbox.setSingleStep(0.5) + self.arrival_slow_radius_spinbox.setValue(self.config.get("arrival_slow_radius", 5.0)) + self.arrival_slow_radius_spinbox.valueChanged.connect(self.update_arrival_slow_radius) + arrival_layout.addRow("减速半径:", self.arrival_slow_radius_spinbox) + + arrival_group.setLayout(arrival_layout) + layout.addWidget(arrival_group) + + layout.addStretch() + widget.setLayout(layout) + return widget + + def create_formation_tab(self): + """ + 创建队形控制标签页 + """ + widget = QWidget() + layout = QVBoxLayout() + + # 队形控制 + formation_group = QGroupBox("队形控制") + formation_layout = QFormLayout() + + self.formation_checkbox = QCheckBox("启用队形保持") + self.formation_checkbox.setChecked(self.config.get("formation_enabled", False)) + self.formation_checkbox.stateChanged.connect(self.toggle_formation) + formation_layout.addRow("", self.formation_checkbox) + + self.formation_combo = QComboBox() + self.formation_combo.addItems(["无", "V字形", "直线", "圆形", "楔形", "球形"]) + self.formation_combo.setCurrentText(self.config.get("formation_type", "无")) + self.formation_combo.currentTextChanged.connect(self.update_formation_type) + formation_layout.addRow("队形类型:", self.formation_combo) + + self.formation_weight_spinbox = QDoubleSpinBox() + self.formation_weight_spinbox.setRange(0.0, 10.0) + self.formation_weight_spinbox.setSingleStep(0.1) + self.formation_weight_spinbox.setValue(self.config.get("formation_weight", 1.0)) + self.formation_weight_spinbox.valueChanged.connect(self.update_formation_weight) + formation_layout.addRow("队形权重:", self.formation_weight_spinbox) + + formation_group.setLayout(formation_layout) + layout.addWidget(formation_group) + + # 高级队形参数 + advanced_formation_group = QGroupBox("高级队形参数") + advanced_formation_layout = QFormLayout() + + self.personal_space_spinbox = QDoubleSpinBox() + self.personal_space_spinbox.setRange(0.1, 20.0) + self.personal_space_spinbox.setSingleStep(0.1) + self.personal_space_spinbox.setValue(self.config.get("personal_space", 2.0)) + self.personal_space_spinbox.valueChanged.connect(self.update_personal_space) + advanced_formation_layout.addRow("个人空间:", self.personal_space_spinbox) + + advanced_formation_group.setLayout(advanced_formation_layout) + layout.addWidget(advanced_formation_group) + + # 路径跟随 + path_group = QGroupBox("路径跟随") + path_layout = QFormLayout() + + self.path_checkbox = QCheckBox("启用路径跟随") + self.path_checkbox.setChecked(self.config.get("path_following_enabled", False)) + self.path_checkbox.stateChanged.connect(self.toggle_path_following) + path_layout.addRow("", self.path_checkbox) + + self.path_weight_spinbox = QDoubleSpinBox() + self.path_weight_spinbox.setRange(0.0, 10.0) + self.path_weight_spinbox.setSingleStep(0.1) + self.path_weight_spinbox.setValue(self.config.get("path_weight", 1.0)) + self.path_weight_spinbox.valueChanged.connect(self.update_path_weight) + path_layout.addRow("路径权重:", self.path_weight_spinbox) + + self.path_radius_spinbox = QDoubleSpinBox() + self.path_radius_spinbox.setRange(0.1, 50.0) + self.path_radius_spinbox.setSingleStep(0.5) + self.path_radius_spinbox.setValue(self.config.get("path_radius", 3.0)) + self.path_radius_spinbox.valueChanged.connect(self.update_path_radius) + path_layout.addRow("路径半径:", self.path_radius_spinbox) + + path_group.setLayout(path_layout) + layout.addWidget(path_group) + + # 预测参数 + prediction_group = QGroupBox("预测参数") + prediction_layout = QFormLayout() + + self.prediction_distance_spinbox = QDoubleSpinBox() + self.prediction_distance_spinbox.setRange(0.1, 100.0) + self.prediction_distance_spinbox.setSingleStep(1.0) + self.prediction_distance_spinbox.setValue(self.config.get("prediction_distance", 10.0)) + self.prediction_distance_spinbox.valueChanged.connect(self.update_prediction_distance) + prediction_layout.addRow("预测距离:", self.prediction_distance_spinbox) + + prediction_group.setLayout(prediction_layout) + layout.addWidget(prediction_group) + + # 物理参数 + physics_group = QGroupBox("物理参数") + physics_layout = QFormLayout() + + self.gravity_checkbox = QCheckBox("启用重力") + self.gravity_checkbox.setChecked(self.config.get("gravity_enabled", True)) + self.gravity_checkbox.stateChanged.connect(self.toggle_gravity) + physics_layout.addRow("", self.gravity_checkbox) + + self.air_resistance_checkbox = QCheckBox("启用空气阻力") + self.air_resistance_checkbox.setChecked(self.config.get("air_resistance_enabled", True)) + self.air_resistance_checkbox.stateChanged.connect(self.toggle_air_resistance) + physics_layout.addRow("", self.air_resistance_checkbox) + + self.buoyancy_checkbox = QCheckBox("启用浮力") + self.buoyancy_checkbox.setChecked(self.config.get("buoyancy_enabled", False)) + self.buoyancy_checkbox.stateChanged.connect(self.toggle_buoyancy) + physics_layout.addRow("", self.buoyancy_checkbox) + + self.fluid_level_spinbox = QDoubleSpinBox() + self.fluid_level_spinbox.setRange(-100.0, 100.0) + self.fluid_level_spinbox.setSingleStep(1.0) + self.fluid_level_spinbox.setValue(self.config.get("fluid_level", 0.0)) + self.fluid_level_spinbox.valueChanged.connect(self.update_fluid_level) + physics_layout.addRow("流体水平面:", self.fluid_level_spinbox) + + physics_group.setLayout(physics_layout) + layout.addWidget(physics_group) + + # 性能优化参数 + optimization_group = QGroupBox("性能优化") + optimization_layout = QFormLayout() + + self.spatial_partitioning_checkbox = QCheckBox("启用空间分区") + self.spatial_partitioning_checkbox.setChecked(self.config.get("spatial_partitioning", True)) + self.spatial_partitioning_checkbox.stateChanged.connect(self.toggle_spatial_partitioning) + optimization_layout.addRow("", self.spatial_partitioning_checkbox) + + self.neighbor_optimization_checkbox = QCheckBox("启用邻居优化") + self.neighbor_optimization_checkbox.setChecked(self.config.get("neighbor_search_optimization", True)) + self.neighbor_optimization_checkbox.stateChanged.connect(self.toggle_neighbor_optimization) + optimization_layout.addRow("", self.neighbor_optimization_checkbox) + + self.parallel_computation_checkbox = QCheckBox("启用并行计算") + self.parallel_computation_checkbox.setChecked(self.config.get("parallel_computation", True)) + self.parallel_computation_checkbox.stateChanged.connect(self.toggle_parallel_computation) + optimization_layout.addRow("", self.parallel_computation_checkbox) + + optimization_group.setLayout(optimization_layout) + layout.addWidget(optimization_group) + + layout.addStretch() + widget.setLayout(layout) + return widget + + def create_status_tab(self): + """ + 创建状态信息标签页 + """ + widget = QWidget() + layout = QVBoxLayout() + + # 群体状态 + status_group = QGroupBox("群体状态") + status_layout = QFormLayout() + + self.member_count_label = QLabel("0") + status_layout.addRow("成员数量:", self.member_count_label) + + self.paused_label = QLabel("否") + status_layout.addRow("是否暂停:", self.paused_label) + + self.fps_label = QLabel("0") + status_layout.addRow("帧率:", self.fps_label) + + status_group.setLayout(status_layout) + layout.addWidget(status_group) + + # 性能信息 + performance_group = QGroupBox("性能信息") + performance_layout = QVBoxLayout() + + self.performance_text = QTextEdit() + self.performance_text.setReadOnly(True) + self.performance_text.setMaximumHeight(200) + performance_layout.addWidget(self.performance_text) + + performance_group.setLayout(performance_layout) + layout.addWidget(performance_group) + + # 日志信息 + log_group = QGroupBox("日志信息") + log_layout = QVBoxLayout() + + self.log_text = QTextEdit() + self.log_text.setReadOnly(True) + log_layout.addWidget(self.log_text) + + log_group.setLayout(log_layout) + layout.addWidget(log_group) + + layout.addStretch() + widget.setLayout(layout) + return widget + + def toggle_pause(self): + """ + 切换暂停状态 + """ + if self.swarm_manager: + self.swarm_manager.toggle_pause() + + def reset_swarm(self): + """ + 重置群体 + """ + if self.swarm_manager: + self.swarm_manager.cleanup() + self.swarm_manager.create_example_swarm() + self.update_status() + + def update_member_count(self, value): + """ + 更新群体数量 + """ + self.config.set("member_count", value) + # 如果当前群体数量与设置不符,重新创建群体 + if self.swarm_manager and len(self.swarm_manager.members) != value: + self.swarm_manager.cleanup() + self.swarm_manager.create_example_swarm() + + def update_swarm_type(self, value): + """ + 更新群体类型 + """ + self.config.set("swarm_type", value) + if self.swarm_manager: + self.swarm_manager.swarm_type = value + + def toggle_seek_target(self, state): + """ + 切换目标寻求 + """ + enabled = state == Qt.Checked + self.config.set("seek_enabled", enabled) + + def update_cohesion_weight(self, value): + """ + 更新聚集权重 + """ + self.config.set("cohesion_weight", value) + if self.swarm_manager: + self.swarm_manager.cohesion_weight = value + + def update_separation_weight(self, value): + """ + 更新分离权重 + """ + self.config.set("separation_weight", value) + if self.swarm_manager: + self.swarm_manager.separation_weight = value + + def update_alignment_weight(self, value): + """ + 更新对齐权重 + """ + self.config.set("alignment_weight", value) + if self.swarm_manager: + self.swarm_manager.alignment_weight = value + + def update_perception_radius(self, value): + """ + 更新感知半径 + """ + self.config.set("perception_radius", value) + if self.swarm_manager: + self.swarm_manager.perception_radius = value + + def update_max_speed(self, value): + """ + 更新最大速度 + """ + self.config.set("max_speed", value) + if self.swarm_manager: + self.swarm_manager.max_speed = value + + def update_max_acceleration(self, value): + """ + 更新最大加速度 + """ + self.config.set("max_acceleration", value) + + def update_max_force(self, value): + """ + 更新最大转向力 + """ + self.config.set("max_force", value) + + def toggle_boundary(self, state): + """ + 切换边界限制 + """ + enabled = state == Qt.Checked + self.config.set("boundary_enabled", enabled) + + def update_boundary_weight(self, value): + """ + 更新边界权重 + """ + self.config.set("boundary_weight", value) + + def update_boundary_buffer(self, value): + """ + 更新边界缓冲 + """ + self.config.set("boundary_buffer", value) + + def toggle_obstacle_avoidance(self, state): + """ + 切换避障行为 + """ + enabled = state == Qt.Checked + self.config.set("obstacle_avoidance_enabled", enabled) + + def update_obstacle_weight(self, value): + """ + 更新避障权重 + """ + self.config.set("obstacle_weight", value) + + def toggle_wander(self, state): + """ + 切换游走行为 + """ + enabled = state == Qt.Checked + self.config.set("wander_enabled", enabled) + + def update_wander_weight(self, value): + """ + 更新游走权重 + """ + self.config.set("wander_weight", value) + + def update_wander_radius(self, value): + """ + 更新游走半径 + """ + self.config.set("wander_radius", value) + + def update_wander_jitter(self, value): + """ + 更新游走抖动 + """ + self.config.set("wander_jitter", value) + + def toggle_predator_avoidance(self, state): + """ + 切换躲避捕食者行为 + """ + enabled = state == Qt.Checked + self.config.set("predator_avoid_enabled", enabled) + + def update_predator_weight(self, value): + """ + 更新躲避捕食者权重 + """ + self.config.set("predator_avoid_weight", value) + + def update_predator_radius(self, value): + """ + 更新捕食者感知半径 + """ + self.config.set("predator_radius", value) + + def toggle_arrival(self, state): + """ + 切换到达行为 + """ + enabled = state == Qt.Checked + self.config.set("arrival_enabled", enabled) + + def update_arrival_slow_radius(self, value): + """ + 更新到达减速半径 + """ + self.config.set("arrival_slow_radius", value) + + def toggle_formation(self, state): + """ + 切换队形保持 + """ + enabled = state == Qt.Checked + self.config.set("formation_enabled", enabled) + + def update_formation_type(self, value): + """ + 更新队形类型 + """ + self.config.set("formation_type", value) + + def update_formation_weight(self, value): + """ + 更新队形权重 + """ + self.config.set("formation_weight", value) + + def toggle_path_following(self, state): + """ + 切换路径跟随 + """ + enabled = state == Qt.Checked + self.config.set("path_following_enabled", enabled) + + def update_path_weight(self, value): + """ + 更新路径权重 + """ + self.config.set("path_weight", value) + + def update_path_radius(self, value): + """ + 更新路径半径 + """ + self.config.set("path_radius", value) + + def update_prediction_distance(self, value): + """ + 更新预测距离 + """ + self.config.set("prediction_distance", value) + + def update_personal_space(self, value): + """ + 更新个人空间 + """ + self.config.set("personal_space", value) + + def toggle_gravity(self, state): + """ + 切换重力 + """ + enabled = state == Qt.Checked + self.config.set("gravity_enabled", enabled) + + def toggle_air_resistance(self, state): + """ + 切换空气阻力 + """ + enabled = state == Qt.Checked + self.config.set("air_resistance_enabled", enabled) + + def toggle_buoyancy(self, state): + """ + 切换浮力 + """ + enabled = state == Qt.Checked + self.config.set("buoyancy_enabled", enabled) + + def update_fluid_level(self, value): + """ + 更新流体水平面 + """ + self.config.set("fluid_level", value) + + def toggle_spatial_partitioning(self, state): + """ + 切换空间分区 + """ + enabled = state == Qt.Checked + self.config.set("spatial_partitioning", enabled) + + def toggle_neighbor_optimization(self, state): + """ + 切换邻居优化 + """ + enabled = state == Qt.Checked + self.config.set("neighbor_search_optimization", enabled) + + def toggle_parallel_computation(self, state): + """ + 切换并行计算 + """ + enabled = state == Qt.Checked + self.config.set("parallel_computation", enabled) + + def toggle_interaction(self, state): + """ + 切换群体间交互 + """ + enabled = state == Qt.Checked + self.config.set("interaction_enabled", enabled) + + def toggle_learning(self, state): + """ + 切换学习能力 + """ + enabled = state == Qt.Checked + self.config.set("learning_enabled", enabled) + + def toggle_environment_awareness(self, state): + """ + 切换环境感知 + """ + enabled = state == Qt.Checked + self.config.set("environment_awareness_enabled", enabled) + + def update_sensor_range(self, value): + """ + 更新传感器范围 + """ + self.config.set("sensor_range", value) + + def update_sensor_accuracy(self, value): + """ + 更新传感器精度 + """ + self.config.set("sensor_accuracy", value) + + def update_obstacle_response_weight(self, value): + """ + 更新障碍物响应权重 + """ + self.config.set("obstacle_response_weight", value) + + def update_predator_response_weight(self, value): + """ + 更新捕食者响应权重 + """ + self.config.set("predator_response_weight", value) + + def update_resource_seek_weight(self, value): + """ + 更新资源寻求权重 + """ + self.config.set("resource_seek_weight", value) + + def update_hazard_avoid_weight(self, value): + """ + 更新危险回避权重 + """ + self.config.set("hazard_avoid_weight", value) + + def update_time_flow_rate(self, value): + """ + 更新时间流逝速度 + """ + self.config.set("time_flow_rate", value) + + def update_learning_rate(self, value): + """ + 更新学习率 + """ + self.config.set("learning_rate", value) + if hasattr(self.swarm_manager, 'learning_manager'): + self.swarm_manager.learning_manager.learning_rate = value + + def update_exploration_rate(self, value): + """ + 更新探索率 + """ + self.config.set("exploration_rate", value) + if hasattr(self.swarm_manager, 'learning_manager'): + self.swarm_manager.learning_manager.exploration_rate = value + + def update_memory_capacity(self, value): + """ + 更新记忆容量 + """ + self.config.set("memory_capacity", value) + if hasattr(self.swarm_manager, 'learning_manager'): + self.swarm_manager.learning_manager.memory_capacity = value + + def clear_learning_memory(self): + """ + 清空学习记忆 + """ + if hasattr(self.swarm_manager, 'learning_manager'): + self.swarm_manager.learning_manager.clear_memory() + + def create_learning_tab(self): + """ + 创建学习控制标签页 + """ + widget = QWidget() + layout = QVBoxLayout() + + # 学习开关 + learning_group = QGroupBox("学习控制") + learning_layout = QVBoxLayout() + + self.learning_checkbox = QCheckBox("启用学习能力") + self.learning_checkbox.setChecked(self.config.get("learning_enabled", False)) + self.learning_checkbox.stateChanged.connect(self.toggle_learning) + learning_layout.addWidget(self.learning_checkbox) + + clear_memory_button = QPushButton("清空学习记忆") + clear_memory_button.clicked.connect(self.clear_learning_memory) + learning_layout.addWidget(clear_memory_button) + + learning_group.setLayout(learning_layout) + layout.addWidget(learning_group) + + # 学习参数 + params_group = QGroupBox("学习参数") + params_layout = QFormLayout() + + # 学习率 + self.learning_rate_spinbox = QDoubleSpinBox() + self.learning_rate_spinbox.setRange(0.01, 1.0) + self.learning_rate_spinbox.setSingleStep(0.01) + self.learning_rate_spinbox.setValue(self.config.get("learning_rate", 0.1)) + self.learning_rate_spinbox.valueChanged.connect(self.update_learning_rate) + params_layout.addRow("学习率:", self.learning_rate_spinbox) + + # 探索率 + self.exploration_rate_spinbox = QDoubleSpinBox() + self.exploration_rate_spinbox.setRange(0.0, 1.0) + self.exploration_rate_spinbox.setSingleStep(0.05) + self.exploration_rate_spinbox.setValue(self.config.get("exploration_rate", 0.3)) + self.exploration_rate_spinbox.valueChanged.connect(self.update_exploration_rate) + params_layout.addRow("探索率:", self.exploration_rate_spinbox) + + # 记忆容量 + self.memory_capacity_spinbox = QSpinBox() + self.memory_capacity_spinbox.setRange(100, 10000) + self.memory_capacity_spinbox.setSingleStep(100) + self.memory_capacity_spinbox.setValue(self.config.get("memory_capacity", 1000)) + self.memory_capacity_spinbox.valueChanged.connect(self.update_memory_capacity) + params_layout.addRow("记忆容量:", self.memory_capacity_spinbox) + + params_group.setLayout(params_layout) + layout.addWidget(params_group) + + # 学习统计信息 + stats_group = QGroupBox("学习统计") + stats_layout = QFormLayout() + + self.experience_count_label = QLabel("0") + stats_layout.addRow("经验数量:", self.experience_count_label) + + self.policy_count_label = QLabel("0") + stats_layout.addRow("策略数量:", self.policy_count_label) + + self.exploration_rate_label = QLabel("0.0") + stats_layout.addRow("当前探索率:", self.exploration_rate_label) + + stats_group.setLayout(stats_layout) + layout.addWidget(stats_group) + + layout.addStretch() + widget.setLayout(layout) + return widget + + def update_status(self): + """ + 更新状态信息 + """ + if self.swarm_manager: + count = len(self.swarm_manager.members) + self.member_count_label.setText(str(count)) + self.paused_label.setText("是" if self.swarm_manager.paused else "否") + + # 更新性能信息 + performance_info = f"成员数量: {count}\n" + performance_info += f"是否暂停: {'是' if self.swarm_manager.paused else '否'}\n" + performance_info += f"感知半径: {self.config.get('perception_radius', 10.0)}\n" + performance_info += f"最大速度: {self.config.get('max_speed', 5.0)}\n" + performance_info += f"成员数量: {self.config.get('member_count', 20)}\n" + performance_info += f"并行计算: {'是' if self.config.get('parallel_computation', True) else '否'}\n" + + # 添加学习统计信息 + if hasattr(self.swarm_manager, 'learning_manager') and self.config.get("learning_enabled", False): + learning_stats = self.swarm_manager.learning_manager.get_learning_stats() + performance_info += f"经验数量: {learning_stats['experience_count']}\n" + performance_info += f"策略数量: {learning_stats['policy_count']}\n" + performance_info += f"探索率: {learning_stats['exploration_rate']:.2f}\n" + performance_info += f"学习率: {learning_stats['learning_rate']:.2f}\n" + + # 更新学习统计标签 + if hasattr(self, 'experience_count_label'): + self.experience_count_label.setText(str(learning_stats['experience_count'])) + if hasattr(self, 'policy_count_label'): + self.policy_count_label.setText(str(learning_stats['policy_count'])) + if hasattr(self, 'exploration_rate_label'): + self.exploration_rate_label.setText(f"{learning_stats['exploration_rate']:.2f}") + self.performance_text.setPlainText(performance_info) \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/interaction_manager.py b/plugins/user/swarm_intelligence/interaction_manager.py index 7bd412a1..cf9353a9 100644 --- a/plugins/user/swarm_intelligence/interaction_manager.py +++ b/plugins/user/swarm_intelligence/interaction_manager.py @@ -1,265 +1,265 @@ -""" -群体间交互管理器 -管理不同群体之间的交互行为 -""" - -from panda3d.core import Vec3 -import random -from typing import List, Dict - -class InteractionManager: - """ - 群体间交互管理器 - 负责处理不同群体之间的交互行为,如竞争、合作、捕食等 - """ - - def __init__(self, config): - self.config = config - self.interactions = [] # 存储群体间交互关系 - self.interaction_history = [] # 交互历史记录 - - def add_interaction(self, swarm1: Dict, swarm2: Dict, interaction_type: str, strength: float = 1.0): - """ - 添加群体间交互关系 - - :param swarm1: 第一个群体 - :param swarm2: 第二个群体 - :param interaction_type: 交互类型 ('cooperative', 'competitive', 'predator_prey', 'neutral') - :param strength: 交互强度 (0.0-1.0) - """ - interaction = { - 'swarm1': swarm1, - 'swarm2': swarm2, - 'type': interaction_type, - 'strength': strength, - 'active': True - } - self.interactions.append(interaction) - - def remove_interaction(self, interaction): - """ - 移除群体间交互关系 - """ - if interaction in self.interactions: - self.interactions.remove(interaction) - - def update_interactions(self, swarms: List[Dict]): - """ - 更新所有群体间交互 - - :param swarms: 所有群体列表 - """ - for interaction in self.interactions: - if interaction['active']: - self._process_interaction(interaction, swarms) - - def _process_interaction(self, interaction: Dict, swarms: List[Dict]): - """ - 处理单个群体间交互 - - :param interaction: 交互关系 - :param swarms: 所有群体列表 - """ - swarm1 = interaction['swarm1'] - swarm2 = interaction['swarm2'] - interaction_type = interaction['type'] - strength = interaction['strength'] - - if interaction_type == 'cooperative': - self._process_cooperative_interaction(swarm1, swarm2, strength) - elif interaction_type == 'competitive': - self._process_competitive_interaction(swarm1, swarm2, strength) - elif interaction_type == 'predator_prey': - self._process_predator_prey_interaction(swarm1, swarm2, strength) - elif interaction_type == 'neutral': - # 中性交互,不产生影响 - pass - - def _process_cooperative_interaction(self, swarm1: Dict, swarm2: Dict, strength: float): - """ - 处理合作交互 - 群体之间相互帮助,共享资源或信息 - - :param swarm1: 第一个群体 - :param swarm2: 第二个群体 - :param strength: 交互强度 - """ - # 合作行为:群体成员倾向于向另一个群体的中心移动 - if swarm1.members and swarm2.members: - # 计算群体中心 - center1 = self._calculate_swarm_center(swarm1.members) - center2 = self._calculate_swarm_center(swarm2.members) - - # 对两个群体的成员施加合作力 - for member in swarm1.members: - # 向第二个群体中心移动的趋势 - coop_force = (center2 - member['position']).normalized() * strength * 0.5 - member['cooperative_force'] = coop_force - - for member in swarm2.members: - # 向第一个群体中心移动的趋势 - coop_force = (center1 - member['position']).normalized() * strength * 0.5 - member['cooperative_force'] = coop_force - - # 记录交互历史 - self._record_interaction_history(swarm1, swarm2, 'cooperative', strength) - - def _process_competitive_interaction(self, swarm1: Dict, swarm2: Dict, strength: float): - """ - 处理竞争交互 - 群体之间争夺资源或领域 - - :param swarm1: 第一个群体 - :param swarm2: 第二个群体 - :param strength: 交互强度 - """ - # 竞争行为:群体成员倾向于远离另一个群体 - if swarm1.members and swarm2.members: - # 计算群体中心 - center1 = self._calculate_swarm_center(swarm1.members) - center2 = self._calculate_swarm_center(swarm2.members) - - # 对两个群体的成员施加竞争排斥力 - for member in swarm1.members: - # 远离第二个群体中心 - competition_force = (member['position'] - center2).normalized() * strength * 0.7 - member['competition_force'] = competition_force - - for member in swarm2.members: - # 远离第一个群体中心 - competition_force = (member['position'] - center1).normalized() * strength * 0.7 - member['competition_force'] = competition_force - - # 记录交互历史 - self._record_interaction_history(swarm1, swarm2, 'competitive', strength) - - def _process_predator_prey_interaction(self, predator_swarm: Dict, prey_swarm: Dict, strength: float): - """ - 处理捕食者-猎物交互 - 一个群体捕食另一个群体 - - :param predator_swarm: 捕食者群体 - :param prey_swarm: 猎物群体 - :param strength: 交互强度 - """ - if predator_swarm.members and prey_swarm.members: - # 捕食者行为:寻找最近的猎物 - for predator in predator_swarm.members: - nearest_prey = self._find_nearest_member(predator, prey_swarm.members) - if nearest_prey: - # 向猎物移动 - predation_force = (nearest_prey['position'] - predator['position']).normalized() * strength - predator['predation_force'] = predation_force - - # 猎物行为:逃离最近的捕食者 - for prey in prey_swarm.members: - nearest_predator = self._find_nearest_member(prey, predator_swarm.members) - if nearest_predator: - # 远离捕食者 - escape_force = (prey['position'] - nearest_predator['position']).normalized() * strength * 1.2 - prey['escape_force'] = escape_force - - # 记录交互历史 - self._record_interaction_history(predator_swarm, prey_swarm, 'predator_prey', strength) - - def _calculate_swarm_center(self, members: List[Dict]) -> Vec3: - """ - 计算群体中心位置 - - :param members: 群体成员列表 - :return: 群体中心位置 - """ - if not members: - return Vec3(0, 0, 0) - - center = Vec3(0, 0, 0) - for member in members: - center += member['position'] - center /= len(members) - return center - - def _find_nearest_member(self, member: Dict, other_members: List[Dict]) -> Dict: - """ - 查找最近的成员 - - :param member: 参考成员 - :param other_members: 其他成员列表 - :return: 最近的成员 - """ - if not other_members: - return None - - nearest = None - min_distance = float('inf') - - for other in other_members: - if other != member: # 排除自己 - distance = (member['position'] - other['position']).length() - if distance < min_distance: - min_distance = distance - nearest = other - - return nearest - - def _record_interaction_history(self, swarm1: Dict, swarm2: Dict, interaction_type: str, strength: float): - """ - 记录交互历史 - - :param swarm1: 第一个群体 - :param swarm2: 第二个群体 - :param interaction_type: 交互类型 - :param strength: 交互强度 - """ - record = { - 'swarm1_id': id(swarm1), - 'swarm2_id': id(swarm2), - 'type': interaction_type, - 'strength': strength, - 'timestamp': self._get_current_time() - } - self.interaction_history.append(record) - - # 限制历史记录数量 - if len(self.interaction_history) > 1000: - self.interaction_history = self.interaction_history[-1000:] - - def _get_current_time(self): - """ - 获取当前时间戳 - """ - import time - return time.time() - - def get_interaction_stats(self) -> Dict: - """ - 获取交互统计信息 - - :return: 交互统计信息 - """ - stats = { - 'total_interactions': len(self.interactions), - 'active_interactions': len([i for i in self.interactions if i['active']]), - 'cooperative': len([i for i in self.interactions if i['type'] == 'cooperative']), - 'competitive': len([i for i in self.interactions if i['type'] == 'competitive']), - 'predator_prey': len([i for i in self.interactions if i['type'] == 'predator_prey']), - 'neutral': len([i for i in self.interactions if i['type'] == 'neutral']), - 'history_count': len(self.interaction_history) - } - return stats - - def clear_interactions(self): - """ - 清空所有交互关系 - """ - self.interactions.clear() - self.interaction_history.clear() - - def get_interactions_for_swarm(self, swarm: Dict) -> List[Dict]: - """ - 获取指定群体的所有交互关系 - - :param swarm: 群体对象 - :return: 交互关系列表 - """ - return [i for i in self.interactions +""" +群体间交互管理器 +管理不同群体之间的交互行为 +""" + +from panda3d.core import Vec3 +import random +from typing import List, Dict + +class InteractionManager: + """ + 群体间交互管理器 + 负责处理不同群体之间的交互行为,如竞争、合作、捕食等 + """ + + def __init__(self, config): + self.config = config + self.interactions = [] # 存储群体间交互关系 + self.interaction_history = [] # 交互历史记录 + + def add_interaction(self, swarm1: Dict, swarm2: Dict, interaction_type: str, strength: float = 1.0): + """ + 添加群体间交互关系 + + :param swarm1: 第一个群体 + :param swarm2: 第二个群体 + :param interaction_type: 交互类型 ('cooperative', 'competitive', 'predator_prey', 'neutral') + :param strength: 交互强度 (0.0-1.0) + """ + interaction = { + 'swarm1': swarm1, + 'swarm2': swarm2, + 'type': interaction_type, + 'strength': strength, + 'active': True + } + self.interactions.append(interaction) + + def remove_interaction(self, interaction): + """ + 移除群体间交互关系 + """ + if interaction in self.interactions: + self.interactions.remove(interaction) + + def update_interactions(self, swarms: List[Dict]): + """ + 更新所有群体间交互 + + :param swarms: 所有群体列表 + """ + for interaction in self.interactions: + if interaction['active']: + self._process_interaction(interaction, swarms) + + def _process_interaction(self, interaction: Dict, swarms: List[Dict]): + """ + 处理单个群体间交互 + + :param interaction: 交互关系 + :param swarms: 所有群体列表 + """ + swarm1 = interaction['swarm1'] + swarm2 = interaction['swarm2'] + interaction_type = interaction['type'] + strength = interaction['strength'] + + if interaction_type == 'cooperative': + self._process_cooperative_interaction(swarm1, swarm2, strength) + elif interaction_type == 'competitive': + self._process_competitive_interaction(swarm1, swarm2, strength) + elif interaction_type == 'predator_prey': + self._process_predator_prey_interaction(swarm1, swarm2, strength) + elif interaction_type == 'neutral': + # 中性交互,不产生影响 + pass + + def _process_cooperative_interaction(self, swarm1: Dict, swarm2: Dict, strength: float): + """ + 处理合作交互 + 群体之间相互帮助,共享资源或信息 + + :param swarm1: 第一个群体 + :param swarm2: 第二个群体 + :param strength: 交互强度 + """ + # 合作行为:群体成员倾向于向另一个群体的中心移动 + if swarm1.members and swarm2.members: + # 计算群体中心 + center1 = self._calculate_swarm_center(swarm1.members) + center2 = self._calculate_swarm_center(swarm2.members) + + # 对两个群体的成员施加合作力 + for member in swarm1.members: + # 向第二个群体中心移动的趋势 + coop_force = (center2 - member['position']).normalized() * strength * 0.5 + member['cooperative_force'] = coop_force + + for member in swarm2.members: + # 向第一个群体中心移动的趋势 + coop_force = (center1 - member['position']).normalized() * strength * 0.5 + member['cooperative_force'] = coop_force + + # 记录交互历史 + self._record_interaction_history(swarm1, swarm2, 'cooperative', strength) + + def _process_competitive_interaction(self, swarm1: Dict, swarm2: Dict, strength: float): + """ + 处理竞争交互 + 群体之间争夺资源或领域 + + :param swarm1: 第一个群体 + :param swarm2: 第二个群体 + :param strength: 交互强度 + """ + # 竞争行为:群体成员倾向于远离另一个群体 + if swarm1.members and swarm2.members: + # 计算群体中心 + center1 = self._calculate_swarm_center(swarm1.members) + center2 = self._calculate_swarm_center(swarm2.members) + + # 对两个群体的成员施加竞争排斥力 + for member in swarm1.members: + # 远离第二个群体中心 + competition_force = (member['position'] - center2).normalized() * strength * 0.7 + member['competition_force'] = competition_force + + for member in swarm2.members: + # 远离第一个群体中心 + competition_force = (member['position'] - center1).normalized() * strength * 0.7 + member['competition_force'] = competition_force + + # 记录交互历史 + self._record_interaction_history(swarm1, swarm2, 'competitive', strength) + + def _process_predator_prey_interaction(self, predator_swarm: Dict, prey_swarm: Dict, strength: float): + """ + 处理捕食者-猎物交互 + 一个群体捕食另一个群体 + + :param predator_swarm: 捕食者群体 + :param prey_swarm: 猎物群体 + :param strength: 交互强度 + """ + if predator_swarm.members and prey_swarm.members: + # 捕食者行为:寻找最近的猎物 + for predator in predator_swarm.members: + nearest_prey = self._find_nearest_member(predator, prey_swarm.members) + if nearest_prey: + # 向猎物移动 + predation_force = (nearest_prey['position'] - predator['position']).normalized() * strength + predator['predation_force'] = predation_force + + # 猎物行为:逃离最近的捕食者 + for prey in prey_swarm.members: + nearest_predator = self._find_nearest_member(prey, predator_swarm.members) + if nearest_predator: + # 远离捕食者 + escape_force = (prey['position'] - nearest_predator['position']).normalized() * strength * 1.2 + prey['escape_force'] = escape_force + + # 记录交互历史 + self._record_interaction_history(predator_swarm, prey_swarm, 'predator_prey', strength) + + def _calculate_swarm_center(self, members: List[Dict]) -> Vec3: + """ + 计算群体中心位置 + + :param members: 群体成员列表 + :return: 群体中心位置 + """ + if not members: + return Vec3(0, 0, 0) + + center = Vec3(0, 0, 0) + for member in members: + center += member['position'] + center /= len(members) + return center + + def _find_nearest_member(self, member: Dict, other_members: List[Dict]) -> Dict: + """ + 查找最近的成员 + + :param member: 参考成员 + :param other_members: 其他成员列表 + :return: 最近的成员 + """ + if not other_members: + return None + + nearest = None + min_distance = float('inf') + + for other in other_members: + if other != member: # 排除自己 + distance = (member['position'] - other['position']).length() + if distance < min_distance: + min_distance = distance + nearest = other + + return nearest + + def _record_interaction_history(self, swarm1: Dict, swarm2: Dict, interaction_type: str, strength: float): + """ + 记录交互历史 + + :param swarm1: 第一个群体 + :param swarm2: 第二个群体 + :param interaction_type: 交互类型 + :param strength: 交互强度 + """ + record = { + 'swarm1_id': id(swarm1), + 'swarm2_id': id(swarm2), + 'type': interaction_type, + 'strength': strength, + 'timestamp': self._get_current_time() + } + self.interaction_history.append(record) + + # 限制历史记录数量 + if len(self.interaction_history) > 1000: + self.interaction_history = self.interaction_history[-1000:] + + def _get_current_time(self): + """ + 获取当前时间戳 + """ + import time + return time.time() + + def get_interaction_stats(self) -> Dict: + """ + 获取交互统计信息 + + :return: 交互统计信息 + """ + stats = { + 'total_interactions': len(self.interactions), + 'active_interactions': len([i for i in self.interactions if i['active']]), + 'cooperative': len([i for i in self.interactions if i['type'] == 'cooperative']), + 'competitive': len([i for i in self.interactions if i['type'] == 'competitive']), + 'predator_prey': len([i for i in self.interactions if i['type'] == 'predator_prey']), + 'neutral': len([i for i in self.interactions if i['type'] == 'neutral']), + 'history_count': len(self.interaction_history) + } + return stats + + def clear_interactions(self): + """ + 清空所有交互关系 + """ + self.interactions.clear() + self.interaction_history.clear() + + def get_interactions_for_swarm(self, swarm: Dict) -> List[Dict]: + """ + 获取指定群体的所有交互关系 + + :param swarm: 群体对象 + :return: 交互关系列表 + """ + return [i for i in self.interactions if i['swarm1'] == swarm or i['swarm2'] == swarm] \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/learning_manager.py b/plugins/user/swarm_intelligence/learning_manager.py index 6ff593e3..fcbb8e24 100644 --- a/plugins/user/swarm_intelligence/learning_manager.py +++ b/plugins/user/swarm_intelligence/learning_manager.py @@ -1,417 +1,417 @@ -""" -群体学习和适应管理器 -实现群体的学习和适应能力 -""" - -import random -import math -from typing import List, Dict, Any -from panda3d.core import Vec3 - -class LearningManager: - """ - 学习管理器 - 负责实现群体的学习和适应能力,包括经验积累、行为优化和环境适应 - """ - - def __init__(self, config): - self.config = config - - # 学习参数 - self.learning_rate = 0.1 # 学习率 - self.exploration_rate = 0.3 # 探索率 - self.memory_capacity = 1000 # 记忆容量 - - # 经验记忆 - self.experience_memory = [] - - # 行为策略 - self.behavior_policies = {} - - # 环境模型 - self.environment_model = {} - - # 适应性参数 - self.adaptation_factors = { - 'speed': 1.0, - 'cohesion': 1.0, - 'separation': 1.0, - 'alignment': 1.0 - } - - def add_experience(self, state: Dict, action: str, reward: float, next_state: Dict): - """ - 添加经验到记忆中 - - :param state: 当前状态 - :param action: 执行的动作 - :param reward: 获得的奖励 - :param next_state: 下一个状态 - """ - experience = { - 'state': state, - 'action': action, - 'reward': reward, - 'next_state': next_state, - 'timestamp': self._get_current_time() - } - - self.experience_memory.append(experience) - - # 限制记忆容量 - if len(self.experience_memory) > self.memory_capacity: - self.experience_memory = self.experience_memory[-self.memory_capacity:] - - def update_behavior_policies(self): - """ - 更新行为策略 - 基于经验记忆优化行为策略 - """ - if len(self.experience_memory) < 10: - return - - # 简单的Q-learning更新 - for experience in self.experience_memory[-100:]: # 只使用最近的经验 - state = experience['state'] - action = experience['action'] - reward = experience['reward'] - next_state = experience['next_state'] - - # 获取当前状态-动作对的Q值 - state_key = self._state_to_key(state) - if state_key not in self.behavior_policies: - self.behavior_policies[state_key] = {} - - if action not in self.behavior_policies[state_key]: - self.behavior_policies[state_key][action] = 0.0 - - # 获取下一个状态的最大Q值 - next_state_key = self._state_to_key(next_state) - max_next_q = 0.0 - if next_state_key in self.behavior_policies: - if self.behavior_policies[next_state_key]: - max_next_q = max(self.behavior_policies[next_state_key].values()) - - # 更新Q值 - current_q = self.behavior_policies[state_key][action] - new_q = current_q + self.learning_rate * (reward + 0.9 * max_next_q - current_q) - self.behavior_policies[state_key][action] = new_q - - def select_action(self, state: Dict, available_actions: List[str]) -> str: - """ - 根据当前状态选择动作 - - :param state: 当前状态 - :param available_actions: 可用动作列表 - :return: 选择的动作 - """ - if not available_actions: - return None - - # epsilon-贪婪策略 - if random.random() < self.exploration_rate: - # 探索:随机选择动作 - return random.choice(available_actions) - else: - # 利用:选择最佳动作 - state_key = self._state_to_key(state) - if state_key in self.behavior_policies: - # 选择Q值最高的动作 - action_q_values = { - action: self.behavior_policies[state_key].get(action, 0.0) - for action in available_actions - } - return max(action_q_values, key=action_q_values.get) - else: - # 如果没有经验,随机选择 - return random.choice(available_actions) - - def adapt_to_environment(self, environment_state: Dict): - """ - 根据环境状态调整行为参数 - - :param environment_state: 环境状态 - """ - # 根据环境密度调整行为参数 - if 'density' in environment_state: - density = environment_state['density'] - - # 高密度环境下增加分离权重,减少聚集权重 - if density > 0.7: - self.adaptation_factors['separation'] = min(2.0, self.adaptation_factors['separation'] + 0.1) - self.adaptation_factors['cohesion'] = max(0.5, self.adaptation_factors['cohesion'] - 0.1) - # 低密度环境下增加聚集权重,减少分离权重 - elif density < 0.3: - self.adaptation_factors['cohesion'] = min(2.0, self.adaptation_factors['cohesion'] + 0.1) - self.adaptation_factors['separation'] = max(0.5, self.adaptation_factors['separation'] - 0.1) - - # 根据威胁水平调整速度 - if 'threat_level' in environment_state: - threat_level = environment_state['threat_level'] - - # 高威胁环境下增加速度 - if threat_level > 0.7: - self.adaptation_factors['speed'] = min(2.0, self.adaptation_factors['speed'] + 0.2) - # 低威胁环境下恢复正常速度 - elif threat_level < 0.3: - self.adaptation_factors['speed'] = max(0.8, self.adaptation_factors['speed'] - 0.1) - - # 根据资源丰富度调整行为 - if 'resource_abundance' in environment_state: - resource_abundance = environment_state['resource_abundance'] - - # 资源丰富时减少移动,增加聚集 - if resource_abundance > 0.7: - self.adaptation_factors['speed'] = max(0.7, self.adaptation_factors['speed'] - 0.1) - self.adaptation_factors['cohesion'] = min(1.5, self.adaptation_factors['cohesion'] + 0.1) - # 资源稀缺时增加移动,减少聚集 - elif resource_abundance < 0.3: - self.adaptation_factors['speed'] = min(1.3, self.adaptation_factors['speed'] + 0.1) - self.adaptation_factors['cohesion'] = max(0.7, self.adaptation_factors['cohesion'] - 0.1) - - def get_adapted_parameters(self) -> Dict[str, float]: - """ - 获取适应后的参数 - - :return: 适应后的参数字典 - """ - return self.adaptation_factors.copy() - - def update_environment_model(self, observation: Dict): - """ - 更新环境模型 - - :param observation: 环境观察 - """ - # 简单的环境模型更新 - for key, value in observation.items(): - if key not in self.environment_model: - self.environment_model[key] = value - else: - # 平滑更新 - self.environment_model[key] = 0.9 * self.environment_model[key] + 0.1 * value - - def predict_environment_change(self, time_steps: int = 1) -> Dict[str, Any]: - """ - 预测环境变化 - - :param time_steps: 预测的时间步数 - :return: 预测的环境状态 - """ - prediction = {} - for key, value in self.environment_model.items(): - # 简单的线性预测 - prediction[key] = value # 在没有趋势信息的情况下保持不变 - - return prediction - - def calculate_reward(self, member: Dict, neighbors: List[Dict], - obstacles: List[Dict], environment_state: Dict) -> float: - """ - 计算奖励值 - - :param member: 群体成员 - :param neighbors: 邻居列表 - :param obstacles: 障碍物列表 - :param environment_state: 环境状态 - :return: 奖励值 - """ - reward = 0.0 - - # 奖励安全距离(避免碰撞) - safe_distance_reward = self._calculate_safe_distance_reward(member, neighbors, obstacles) - reward += safe_distance_reward - - # 奖励群体凝聚力 - cohesion_reward = self._calculate_cohesion_reward(member, neighbors) - reward += cohesion_reward - - # 奖励环境适应性 - adaptation_reward = self._calculate_adaptation_reward(member, environment_state) - reward += adaptation_reward - - # 惩罚过度探索 - exploration_penalty = self._calculate_exploration_penalty(member) - reward += exploration_penalty - - return reward - - def _calculate_safe_distance_reward(self, member: Dict, neighbors: List[Dict], - obstacles: List[Dict]) -> float: - """ - 计算安全距离奖励 - - :param member: 群体成员 - :param neighbors: 邻居列表 - :param obstacles: 障碍物列表 - :return: 安全距离奖励 - """ - reward = 0.0 - min_safe_distance = 2.0 - - # 检查与邻居的安全距离 - for neighbor in neighbors: - distance = (member['position'] - neighbor['position']).length() - if distance < min_safe_distance: - reward -= (min_safe_distance - distance) * 2.0 # 惩罚过近距离 - - # 检查与障碍物的安全距离 - for obstacle in obstacles: - distance = (member['position'] - obstacle['position']).length() - obstacle.get('radius', 1.0) - if distance < min_safe_distance: - reward -= (min_safe_distance - distance) * 3.0 # 惩罚过近距离 - - return reward - - def _calculate_cohesion_reward(self, member: Dict, neighbors: List[Dict]) -> float: - """ - 计算群体凝聚力奖励 - - :param member: 群体成员 - :param neighbors: 邻居列表 - :return: 凝聚力奖励 - """ - if not neighbors: - return 0.0 - - # 计算与邻居的平均距离 - total_distance = 0.0 - for neighbor in neighbors: - distance = (member['position'] - neighbor['position']).length() - total_distance += distance - - avg_distance = total_distance / len(neighbors) - - # 理想距离范围 - ideal_min_distance = 5.0 - ideal_max_distance = 15.0 - - # 在理想范围内给予奖励 - if ideal_min_distance <= avg_distance <= ideal_max_distance: - return 1.0 - else: - # 距离理想范围越远,奖励越少 - distance_to_ideal = min( - abs(avg_distance - ideal_min_distance), - abs(avg_distance - ideal_max_distance) - ) - return max(0.0, 1.0 - distance_to_ideal * 0.1) - - def _calculate_adaptation_reward(self, member: Dict, environment_state: Dict) -> float: - """ - 计算环境适应性奖励 - - :param member: 群体成员 - :param environment_state: 环境状态 - :return: 适应性奖励 - """ - reward = 0.0 - - # 根据环境条件给予奖励 - if 'resource_abundance' in environment_state: - resource_abundance = environment_state['resource_abundance'] - # 资源丰富时给予奖励 - reward += resource_abundance * 0.5 - - if 'threat_level' in environment_state: - threat_level = environment_state['threat_level'] - # 威胁低时给予奖励 - reward += (1.0 - threat_level) * 0.3 - - return reward - - def _calculate_exploration_penalty(self, member: Dict) -> float: - """ - 计算探索惩罚 - - :param member: 群体成员 - :return: 探索惩罚 - """ - # 如果成员移动到边界附近,给予惩罚 - bounds = { - 'min_x': -50, 'max_x': 50, - 'min_y': -50, 'max_y': 50, - 'min_z': 0, 'max_z': 30 - } - - penalty = 0.0 - buffer = 5.0 - - if (member['position'].x < bounds['min_x'] + buffer or - member['position'].x > bounds['max_x'] - buffer or - member['position'].y < bounds['min_y'] + buffer or - member['position'].y > bounds['max_y'] - buffer or - member['position'].z < bounds['min_z'] + buffer or - member['position'].z > bounds['max_z'] - buffer): - penalty -= 1.0 - - return penalty - - def _state_to_key(self, state: Dict) -> str: - """ - 将状态转换为键值 - - :param state: 状态字典 - :return: 状态键值 - """ - # 简单的键值生成方法 - key_parts = [] - for key in sorted(state.keys()): - if isinstance(state[key], (int, float)): - # 数值型状态,四舍五入到小数点后一位 - key_parts.append(f"{key}:{state[key]:.1f}") - else: - key_parts.append(f"{key}:{state[key]}") - - return "|".join(key_parts) - - def _get_current_time(self): - """ - 获取当前时间戳 - """ - import time - return time.time() - - def get_learning_stats(self) -> Dict: - """ - 获取学习统计信息 - - :return: 学习统计信息 - """ - return { - 'experience_count': len(self.experience_memory), - 'policy_count': len(self.behavior_policies), - 'exploration_rate': self.exploration_rate, - 'learning_rate': self.learning_rate, - 'adaptation_factors': self.adaptation_factors.copy() - } - - def adjust_learning_parameters(self, performance: float): - """ - 根据性能调整学习参数 - - :param performance: 当前性能指标 - """ - # 根据性能调整探索率 - if performance > 0.8: - # 性能良好时减少探索 - self.exploration_rate = max(0.1, self.exploration_rate - 0.05) - elif performance < 0.3: - # 性能较差时增加探索 - self.exploration_rate = min(0.8, self.exploration_rate + 0.1) - - # 根据性能调整学习率 - if performance > 0.9: - # 性能很好时降低学习率以稳定行为 - self.learning_rate = max(0.05, self.learning_rate * 0.9) - elif performance < 0.2: - # 性能很差时提高学习率以快速学习 - self.learning_rate = min(0.5, self.learning_rate * 1.1) - - def clear_memory(self): - """ - 清空记忆 - """ - self.experience_memory.clear() - self.behavior_policies.clear() +""" +群体学习和适应管理器 +实现群体的学习和适应能力 +""" + +import random +import math +from typing import List, Dict, Any +from panda3d.core import Vec3 + +class LearningManager: + """ + 学习管理器 + 负责实现群体的学习和适应能力,包括经验积累、行为优化和环境适应 + """ + + def __init__(self, config): + self.config = config + + # 学习参数 + self.learning_rate = 0.1 # 学习率 + self.exploration_rate = 0.3 # 探索率 + self.memory_capacity = 1000 # 记忆容量 + + # 经验记忆 + self.experience_memory = [] + + # 行为策略 + self.behavior_policies = {} + + # 环境模型 + self.environment_model = {} + + # 适应性参数 + self.adaptation_factors = { + 'speed': 1.0, + 'cohesion': 1.0, + 'separation': 1.0, + 'alignment': 1.0 + } + + def add_experience(self, state: Dict, action: str, reward: float, next_state: Dict): + """ + 添加经验到记忆中 + + :param state: 当前状态 + :param action: 执行的动作 + :param reward: 获得的奖励 + :param next_state: 下一个状态 + """ + experience = { + 'state': state, + 'action': action, + 'reward': reward, + 'next_state': next_state, + 'timestamp': self._get_current_time() + } + + self.experience_memory.append(experience) + + # 限制记忆容量 + if len(self.experience_memory) > self.memory_capacity: + self.experience_memory = self.experience_memory[-self.memory_capacity:] + + def update_behavior_policies(self): + """ + 更新行为策略 + 基于经验记忆优化行为策略 + """ + if len(self.experience_memory) < 10: + return + + # 简单的Q-learning更新 + for experience in self.experience_memory[-100:]: # 只使用最近的经验 + state = experience['state'] + action = experience['action'] + reward = experience['reward'] + next_state = experience['next_state'] + + # 获取当前状态-动作对的Q值 + state_key = self._state_to_key(state) + if state_key not in self.behavior_policies: + self.behavior_policies[state_key] = {} + + if action not in self.behavior_policies[state_key]: + self.behavior_policies[state_key][action] = 0.0 + + # 获取下一个状态的最大Q值 + next_state_key = self._state_to_key(next_state) + max_next_q = 0.0 + if next_state_key in self.behavior_policies: + if self.behavior_policies[next_state_key]: + max_next_q = max(self.behavior_policies[next_state_key].values()) + + # 更新Q值 + current_q = self.behavior_policies[state_key][action] + new_q = current_q + self.learning_rate * (reward + 0.9 * max_next_q - current_q) + self.behavior_policies[state_key][action] = new_q + + def select_action(self, state: Dict, available_actions: List[str]) -> str: + """ + 根据当前状态选择动作 + + :param state: 当前状态 + :param available_actions: 可用动作列表 + :return: 选择的动作 + """ + if not available_actions: + return None + + # epsilon-贪婪策略 + if random.random() < self.exploration_rate: + # 探索:随机选择动作 + return random.choice(available_actions) + else: + # 利用:选择最佳动作 + state_key = self._state_to_key(state) + if state_key in self.behavior_policies: + # 选择Q值最高的动作 + action_q_values = { + action: self.behavior_policies[state_key].get(action, 0.0) + for action in available_actions + } + return max(action_q_values, key=action_q_values.get) + else: + # 如果没有经验,随机选择 + return random.choice(available_actions) + + def adapt_to_environment(self, environment_state: Dict): + """ + 根据环境状态调整行为参数 + + :param environment_state: 环境状态 + """ + # 根据环境密度调整行为参数 + if 'density' in environment_state: + density = environment_state['density'] + + # 高密度环境下增加分离权重,减少聚集权重 + if density > 0.7: + self.adaptation_factors['separation'] = min(2.0, self.adaptation_factors['separation'] + 0.1) + self.adaptation_factors['cohesion'] = max(0.5, self.adaptation_factors['cohesion'] - 0.1) + # 低密度环境下增加聚集权重,减少分离权重 + elif density < 0.3: + self.adaptation_factors['cohesion'] = min(2.0, self.adaptation_factors['cohesion'] + 0.1) + self.adaptation_factors['separation'] = max(0.5, self.adaptation_factors['separation'] - 0.1) + + # 根据威胁水平调整速度 + if 'threat_level' in environment_state: + threat_level = environment_state['threat_level'] + + # 高威胁环境下增加速度 + if threat_level > 0.7: + self.adaptation_factors['speed'] = min(2.0, self.adaptation_factors['speed'] + 0.2) + # 低威胁环境下恢复正常速度 + elif threat_level < 0.3: + self.adaptation_factors['speed'] = max(0.8, self.adaptation_factors['speed'] - 0.1) + + # 根据资源丰富度调整行为 + if 'resource_abundance' in environment_state: + resource_abundance = environment_state['resource_abundance'] + + # 资源丰富时减少移动,增加聚集 + if resource_abundance > 0.7: + self.adaptation_factors['speed'] = max(0.7, self.adaptation_factors['speed'] - 0.1) + self.adaptation_factors['cohesion'] = min(1.5, self.adaptation_factors['cohesion'] + 0.1) + # 资源稀缺时增加移动,减少聚集 + elif resource_abundance < 0.3: + self.adaptation_factors['speed'] = min(1.3, self.adaptation_factors['speed'] + 0.1) + self.adaptation_factors['cohesion'] = max(0.7, self.adaptation_factors['cohesion'] - 0.1) + + def get_adapted_parameters(self) -> Dict[str, float]: + """ + 获取适应后的参数 + + :return: 适应后的参数字典 + """ + return self.adaptation_factors.copy() + + def update_environment_model(self, observation: Dict): + """ + 更新环境模型 + + :param observation: 环境观察 + """ + # 简单的环境模型更新 + for key, value in observation.items(): + if key not in self.environment_model: + self.environment_model[key] = value + else: + # 平滑更新 + self.environment_model[key] = 0.9 * self.environment_model[key] + 0.1 * value + + def predict_environment_change(self, time_steps: int = 1) -> Dict[str, Any]: + """ + 预测环境变化 + + :param time_steps: 预测的时间步数 + :return: 预测的环境状态 + """ + prediction = {} + for key, value in self.environment_model.items(): + # 简单的线性预测 + prediction[key] = value # 在没有趋势信息的情况下保持不变 + + return prediction + + def calculate_reward(self, member: Dict, neighbors: List[Dict], + obstacles: List[Dict], environment_state: Dict) -> float: + """ + 计算奖励值 + + :param member: 群体成员 + :param neighbors: 邻居列表 + :param obstacles: 障碍物列表 + :param environment_state: 环境状态 + :return: 奖励值 + """ + reward = 0.0 + + # 奖励安全距离(避免碰撞) + safe_distance_reward = self._calculate_safe_distance_reward(member, neighbors, obstacles) + reward += safe_distance_reward + + # 奖励群体凝聚力 + cohesion_reward = self._calculate_cohesion_reward(member, neighbors) + reward += cohesion_reward + + # 奖励环境适应性 + adaptation_reward = self._calculate_adaptation_reward(member, environment_state) + reward += adaptation_reward + + # 惩罚过度探索 + exploration_penalty = self._calculate_exploration_penalty(member) + reward += exploration_penalty + + return reward + + def _calculate_safe_distance_reward(self, member: Dict, neighbors: List[Dict], + obstacles: List[Dict]) -> float: + """ + 计算安全距离奖励 + + :param member: 群体成员 + :param neighbors: 邻居列表 + :param obstacles: 障碍物列表 + :return: 安全距离奖励 + """ + reward = 0.0 + min_safe_distance = 2.0 + + # 检查与邻居的安全距离 + for neighbor in neighbors: + distance = (member['position'] - neighbor['position']).length() + if distance < min_safe_distance: + reward -= (min_safe_distance - distance) * 2.0 # 惩罚过近距离 + + # 检查与障碍物的安全距离 + for obstacle in obstacles: + distance = (member['position'] - obstacle['position']).length() - obstacle.get('radius', 1.0) + if distance < min_safe_distance: + reward -= (min_safe_distance - distance) * 3.0 # 惩罚过近距离 + + return reward + + def _calculate_cohesion_reward(self, member: Dict, neighbors: List[Dict]) -> float: + """ + 计算群体凝聚力奖励 + + :param member: 群体成员 + :param neighbors: 邻居列表 + :return: 凝聚力奖励 + """ + if not neighbors: + return 0.0 + + # 计算与邻居的平均距离 + total_distance = 0.0 + for neighbor in neighbors: + distance = (member['position'] - neighbor['position']).length() + total_distance += distance + + avg_distance = total_distance / len(neighbors) + + # 理想距离范围 + ideal_min_distance = 5.0 + ideal_max_distance = 15.0 + + # 在理想范围内给予奖励 + if ideal_min_distance <= avg_distance <= ideal_max_distance: + return 1.0 + else: + # 距离理想范围越远,奖励越少 + distance_to_ideal = min( + abs(avg_distance - ideal_min_distance), + abs(avg_distance - ideal_max_distance) + ) + return max(0.0, 1.0 - distance_to_ideal * 0.1) + + def _calculate_adaptation_reward(self, member: Dict, environment_state: Dict) -> float: + """ + 计算环境适应性奖励 + + :param member: 群体成员 + :param environment_state: 环境状态 + :return: 适应性奖励 + """ + reward = 0.0 + + # 根据环境条件给予奖励 + if 'resource_abundance' in environment_state: + resource_abundance = environment_state['resource_abundance'] + # 资源丰富时给予奖励 + reward += resource_abundance * 0.5 + + if 'threat_level' in environment_state: + threat_level = environment_state['threat_level'] + # 威胁低时给予奖励 + reward += (1.0 - threat_level) * 0.3 + + return reward + + def _calculate_exploration_penalty(self, member: Dict) -> float: + """ + 计算探索惩罚 + + :param member: 群体成员 + :return: 探索惩罚 + """ + # 如果成员移动到边界附近,给予惩罚 + bounds = { + 'min_x': -50, 'max_x': 50, + 'min_y': -50, 'max_y': 50, + 'min_z': 0, 'max_z': 30 + } + + penalty = 0.0 + buffer = 5.0 + + if (member['position'].x < bounds['min_x'] + buffer or + member['position'].x > bounds['max_x'] - buffer or + member['position'].y < bounds['min_y'] + buffer or + member['position'].y > bounds['max_y'] - buffer or + member['position'].z < bounds['min_z'] + buffer or + member['position'].z > bounds['max_z'] - buffer): + penalty -= 1.0 + + return penalty + + def _state_to_key(self, state: Dict) -> str: + """ + 将状态转换为键值 + + :param state: 状态字典 + :return: 状态键值 + """ + # 简单的键值生成方法 + key_parts = [] + for key in sorted(state.keys()): + if isinstance(state[key], (int, float)): + # 数值型状态,四舍五入到小数点后一位 + key_parts.append(f"{key}:{state[key]:.1f}") + else: + key_parts.append(f"{key}:{state[key]}") + + return "|".join(key_parts) + + def _get_current_time(self): + """ + 获取当前时间戳 + """ + import time + return time.time() + + def get_learning_stats(self) -> Dict: + """ + 获取学习统计信息 + + :return: 学习统计信息 + """ + return { + 'experience_count': len(self.experience_memory), + 'policy_count': len(self.behavior_policies), + 'exploration_rate': self.exploration_rate, + 'learning_rate': self.learning_rate, + 'adaptation_factors': self.adaptation_factors.copy() + } + + def adjust_learning_parameters(self, performance: float): + """ + 根据性能调整学习参数 + + :param performance: 当前性能指标 + """ + # 根据性能调整探索率 + if performance > 0.8: + # 性能良好时减少探索 + self.exploration_rate = max(0.1, self.exploration_rate - 0.05) + elif performance < 0.3: + # 性能较差时增加探索 + self.exploration_rate = min(0.8, self.exploration_rate + 0.1) + + # 根据性能调整学习率 + if performance > 0.9: + # 性能很好时降低学习率以稳定行为 + self.learning_rate = max(0.05, self.learning_rate * 0.9) + elif performance < 0.2: + # 性能很差时提高学习率以快速学习 + self.learning_rate = min(0.5, self.learning_rate * 1.1) + + def clear_memory(self): + """ + 清空记忆 + """ + self.experience_memory.clear() + self.behavior_policies.clear() self.environment_model.clear() \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/plugin.py b/plugins/user/swarm_intelligence/plugin.py index 5924daf7..9fc560d9 100644 --- a/plugins/user/swarm_intelligence/plugin.py +++ b/plugins/user/swarm_intelligence/plugin.py @@ -1,95 +1,95 @@ -""" -群体智能插件 -实现Boids群体智能算法 -""" - -from plugins.plugin_manager import BasePlugin -from .swarm_manager import SwarmManager - -class Plugin(BasePlugin): - """ - 群体智能插件 - """ - - def __init__(self, plugin_manager, name): - super().__init__(plugin_manager, name) - # 设置插件配置 - self.config = { - "version": "1.0.0", - "author": "AI Assistant", - "description": "实现Boids群体智能算法的插件" - } - self.swarm_manager = None - - def initialize(self) -> bool: - """ - 初始化插件 - """ - print(f"初始化插件: {self.name}") - # 在这里执行插件初始化逻辑 - return True - - def enable(self) -> bool: - """ - 启用插件 - """ - if not super().enable(): - return False - - print(f"启用插件: {self.name}") - - # 初始化群体管理器 - self.swarm_manager = SwarmManager(self.plugin_manager.world) - - # 创建示例群体 - self.swarm_manager.create_example_swarm() - - # 注册事件处理器 - self.plugin_manager.world.accept("f1", self._on_f1_pressed) - - return True - - def disable(self) -> bool: - """ - 禁用插件 - """ - if not super().disable(): - return False - - print(f"禁用插件: {self.name}") - - # 清理群体 - if self.swarm_manager: - self.swarm_manager.cleanup() - - # 移除事件处理器 - self.plugin_manager.world.ignore("f1") - - return True - - def finalize(self): - """ - 清理插件资源 - """ - print(f"清理插件资源: {self.name}") - # 在这里执行最终清理逻辑 - - def get_info(self) -> dict: - """ - 获取插件信息 - """ - info = super().get_info() - info.update({ - "version": self.config.get("version", "1.0.0"), - "author": self.config.get("author", "Unknown"), - "description": self.config.get("description", "Base plugin class") - }) - return info - - def _on_f1_pressed(self): - """ - F1按键处理函数 - """ - print("群体智能插件: F1按键被按下") - if self.swarm_manager: +""" +群体智能插件 +实现Boids群体智能算法 +""" + +from plugins.plugin_manager import BasePlugin +from .swarm_manager import SwarmManager + +class Plugin(BasePlugin): + """ + 群体智能插件 + """ + + def __init__(self, plugin_manager, name): + super().__init__(plugin_manager, name) + # 设置插件配置 + self.config = { + "version": "1.0.0", + "author": "AI Assistant", + "description": "实现Boids群体智能算法的插件" + } + self.swarm_manager = None + + def initialize(self) -> bool: + """ + 初始化插件 + """ + print(f"初始化插件: {self.name}") + # 在这里执行插件初始化逻辑 + return True + + def enable(self) -> bool: + """ + 启用插件 + """ + if not super().enable(): + return False + + print(f"启用插件: {self.name}") + + # 初始化群体管理器 + self.swarm_manager = SwarmManager(self.plugin_manager.world) + + # 创建示例群体 + self.swarm_manager.create_example_swarm() + + # 注册事件处理器 + self.plugin_manager.world.accept("f1", self._on_f1_pressed) + + return True + + def disable(self) -> bool: + """ + 禁用插件 + """ + if not super().disable(): + return False + + print(f"禁用插件: {self.name}") + + # 清理群体 + if self.swarm_manager: + self.swarm_manager.cleanup() + + # 移除事件处理器 + self.plugin_manager.world.ignore("f1") + + return True + + def finalize(self): + """ + 清理插件资源 + """ + print(f"清理插件资源: {self.name}") + # 在这里执行最终清理逻辑 + + def get_info(self) -> dict: + """ + 获取插件信息 + """ + info = super().get_info() + info.update({ + "version": self.config.get("version", "1.0.0"), + "author": self.config.get("author", "Unknown"), + "description": self.config.get("description", "Base plugin class") + }) + return info + + def _on_f1_pressed(self): + """ + F1按键处理函数 + """ + print("群体智能插件: F1按键被按下") + if self.swarm_manager: self.swarm_manager.toggle_pause() \ No newline at end of file diff --git a/plugins/user/swarm_intelligence/swarm_manager.py b/plugins/user/swarm_intelligence/swarm_manager.py index 9d6f0d0f..3885e387 100644 --- a/plugins/user/swarm_intelligence/swarm_manager.py +++ b/plugins/user/swarm_intelligence/swarm_manager.py @@ -1,732 +1,732 @@ -""" -群体管理器 -负责创建和管理群体成员 -""" - -from panda3d.core import Vec3, Point3, LVector3f -import random -import math -import time - -class SwarmManager: - """ - 群体管理器 - 负责创建和管理群体成员,实现完整的群体智能行为 - """ - - def __init__(self, world): - self.world = world - self.members = [] - self.obstacles = [] - self.predators = [] - self.paused = False - self.task = None - self.swarm_type = "鸟类" - self.target = None - self.path = [] - self.bounds = { - 'min_x': -50, 'max_x': 50, - 'min_y': -50, 'max_y': 50, - 'min_z': 0, 'max_z': 30 - } - - # 多群体支持 - self.swarms = [] # 存储多个群体 - self.swarm_id_counter = 0 - - # 性能统计 - self.frame_count = 0 - self.last_update_time = time.time() - self.fps = 0 - - # 群体参数 - self.cohesion_weight = 1.0 - self.separation_weight = 1.5 - self.alignment_weight = 1.0 - self.max_speed = 5.0 - self.perception_radius = 10.0 - self.member_count = 20 - - # 初始化高级Boids算法 - from .advanced_boids import AdvancedBoidsAlgorithm - from .config import Config - from .interaction_manager import InteractionManager - from .learning_manager import LearningManager - from .environment_manager import EnvironmentManager - self.config = Config() - self.boids_algorithm = AdvancedBoidsAlgorithm(self.config) - self.interaction_manager = InteractionManager(self.config) - self.learning_manager = LearningManager(self.config) - self.environment_manager = EnvironmentManager(self.config) - - # 启动更新任务 - self.task = self.world.taskMgr.add(self._update_task, "swarm_update_task") - - def create_example_swarm(self, swarm_type=None, member_count=None, position_offset=Vec3(0, 0, 0)): - """ - 创建示例群体 - 根据群体类型创建不同类型的群体成员 - """ - # 如果是创建新的独立群体,不清理现有群体 - if swarm_type is None and member_count is None: - # 清理现有群体 - self.cleanup() - - # 获取群体类型和数量 - swarm_type = swarm_type or self.swarm_type - count = member_count or self.config.get("member_count", self.member_count) - - # 创建指定数量的群体成员 - new_members = [] - for i in range(count): - # 根据群体类型设置随机位置范围,并应用位置偏移 - if swarm_type == "鸟类": - pos = Vec3( - random.uniform(-20, 20), - random.uniform(-20, 20), - random.uniform(5, 25) - ) + position_offset - elif swarm_type == "鱼类": - pos = Vec3( - random.uniform(-15, 15), - random.uniform(-15, 15), - random.uniform(2, 15) - ) + position_offset - elif swarm_type == "昆虫": - pos = Vec3( - random.uniform(-10, 10), - random.uniform(-10, 10), - random.uniform(1, 10) - ) + position_offset - else: # 自定义 - pos = Vec3( - random.uniform(self.bounds['min_x'], self.bounds['max_x']), - random.uniform(self.bounds['min_y'], self.bounds['min_y']), - random.uniform(self.bounds['min_z'], self.bounds['max_z']) - ) + position_offset - - # 根据群体类型加载不同的模型 - if swarm_type == "鸟类": - member_model = "models/misc/sphere" # 实际应用中应使用鸟类模型 - scale = 0.5 - elif swarm_type == "鱼类": - member_model = "models/misc/sphere" # 实际应用中应使用鱼类模型 - scale = 0.3 - elif swarm_type == "昆虫": - member_model = "models/misc/sphere" # 实际应用中应使用昆虫模型 - scale = 0.2 - else: - member_model = "models/misc/sphere" - scale = 0.5 - - # 加载模型 - try: - member_node = self.world.loader.loadModel(member_model) - except: - # 如果模型加载失败,使用默认球体 - member_node = self.world.loader.loadModel("models/misc/sphere") - - member_node.reparentTo(self.world.render) - member_node.setPos(pos) - member_node.setScale(scale) - - # 随机初始速度 - velocity = Vec3( - random.uniform(-1, 1), - random.uniform(-1, 1), - random.uniform(-1, 1) - ) - - # 添加到群体成员列表 - member = { - 'node': member_node, - 'position': pos, - 'velocity': velocity, - 'index': i, # 成员索引,用于队形计算 - 'type': swarm_type, # 成员类型 - 'swarm_id': self.swarm_id_counter # 群体ID - } - new_members.append(member) - - # 为成员添加环境传感器 - if hasattr(self, 'environment_manager'): - self.environment_manager.add_sensor(id(member)) - - # 如果是创建新的独立群体 - if swarm_type is not None or member_count is not None: - # 创建新的群体对象 - swarm = { - 'id': self.swarm_id_counter, - 'type': swarm_type, - 'members': new_members, - 'position_offset': position_offset - } - self.swarms.append(swarm) - self.swarm_id_counter += 1 - - # 将新成员添加到主成员列表 - self.members.extend(new_members) - - print(f"已创建包含{len(new_members)}个成员的{swarm_type}群体 (ID: {swarm['id']})") - return swarm - else: - # 更新主成员列表 - self.members = new_members - print(f"已创建包含{len(self.members)}个成员的{swarm_type}群体") - - def create_multiple_swarms(self, swarm_configs: List[Dict]): - """ - 创建多个群体 - :param swarm_configs: 群体配置列表,每个配置包含type, count, position_offset - """ - self.cleanup() # 清理现有群体 - self.swarms.clear() # 清理群体列表 - self.swarm_id_counter = 0 # 重置ID计数器 - - for config in swarm_configs: - swarm_type = config.get('type', '鸟类') - member_count = config.get('count', 20) - position_offset = config.get('position_offset', Vec3(0, 0, 0)) - - self.create_example_swarm(swarm_type, member_count, position_offset) - - print(f"已创建{len(self.swarms)}个群体") - - def get_swarm_by_id(self, swarm_id: int): - """ - 根据ID获取群体 - """ - for swarm in self.swarms: - if swarm['id'] == swarm_id: - return swarm - return None - - def get_swarm_by_type(self, swarm_type: str): - """ - 根据类型获取群体 - """ - return [swarm for swarm in self.swarms if swarm['type'] == swarm_type] - - def create_obstacle(self, position, radius): - """ - 创建障碍物 - """ - obstacle = { - 'position': position, - 'radius': radius - } - self.obstacles.append(obstacle) - return obstacle - - def create_predator(self, position): - """ - 创建捕食者 - """ - # 创建捕食者模型 - try: - predator_node = self.world.loader.loadModel("models/misc/sphere") # 实际应用中应使用捕食者模型 - except: - predator_node = self.world.loader.loadModel("models/misc/sphere") - - predator_node.reparentTo(self.world.render) - predator_node.setPos(position) - predator_node.setScale(1.0) - predator_node.setColor(1, 0, 0, 1) # 红色表示捕食者 - - # 创建捕食者对象 - predator = { - 'node': predator_node, - 'position': position, - 'velocity': Vec3(0, 0, 0), - 'target_index': 0 # 目标群体成员索引 - } - - self.predators.append(predator) - return predator - - def set_target(self, target): - """ - 设置群体目标位置 - """ - self.target = target - - def set_path(self, path): - """ - 设置群体移动路径 - """ - self.path = path - - def _update_task(self, task): - """ - 更新任务 - """ - if not self.paused: - self._update_swarm() - - # 更新性能统计 - self.frame_count += 1 - current_time = time.time() - if current_time - self.last_update_time >= 1.0: - self.fps = self.frame_count - self.frame_count = 0 - self.last_update_time = current_time - - return task.cont - - def _update_swarm(self): - """ - 更新群体行为 - """ - # 更新捕食者行为(如果存在) - self._update_predators() - - # 更新群体间交互 - interaction_forces = {} - if hasattr(self, 'interaction_manager'): - # 为每个成员计算交互力 - for member in self.members: - interaction_forces[id(member)] = {} - - # 应用合作力 - if 'cooperative_force' in member: - interaction_forces[id(member)]['cooperative_force'] = member['cooperative_force'] - del member['cooperative_force'] - - # 应用竞争力 - if 'competition_force' in member: - interaction_forces[id(member)]['competition_force'] = member['competition_force'] - del member['competition_force'] - - # 应用捕食力 - if 'predation_force' in member: - interaction_forces[id(member)]['predation_force'] = member['predation_force'] - del member['predation_force'] - - # 应用逃避力 - if 'escape_force' in member: - interaction_forces[id(member)]['escape_force'] = member['escape_force'] - del member['escape_force'] - - # 更新环境状态 - if hasattr(self, 'environment_manager'): - self.environment_manager.update_environment_state( - self.obstacles, self.swarms, self.predators, - getattr(self, 'resources', []), getattr(self, 'hazards', []) - ) - - # 更新学习和适应 - if hasattr(self, 'learning_manager') and self.config.get("learning_enabled", False): - # 计算环境状态 - environment_state = self._calculate_environment_state() - - # 更新环境模型 - self.learning_manager.update_environment_model(environment_state) - - # 适应环境 - self.learning_manager.adapt_to_environment(environment_state) - - # 获取适应后的参数 - adapted_params = self.learning_manager.get_adapted_parameters() - - # 更新Boids算法参数 - self._update_boids_parameters(adapted_params) - - # 检查是否启用并行计算 - if self.config.get("parallel_computation", False) and len(self.members) > 50: - # 使用并行计算更新群体成员 - self._update_swarm_parallel() - else: - # 使用串行计算更新群体成员 - for i, member in enumerate(self.members): - # 计算邻居 - neighbors = self._find_neighbors(member, i) - - # 获取扩展参数 - obstacles = self.obstacles if self.config.get("obstacle_avoidance_enabled", True) else [] - bounds = self.bounds if self.config.get("boundary_enabled", True) else None - predators = self.predators if self.config.get("predator_avoid_enabled", False) else [] - target = self.target if self.config.get("seek_enabled", False) else None - path = self.path if self.config.get("path_following_enabled", False) else None - interactions = interaction_forces.get(id(member), {}) if interaction_forces else {} - - # 计算奖励(如果启用学习) - if hasattr(self, 'learning_manager') and self.config.get("learning_enabled", False): - reward = self.learning_manager.calculate_reward(member, neighbors, obstacles, environment_state) - - # 创建状态表示 - state = { - 'neighbor_count': len(neighbors), - 'obstacle_distance': self._calculate_min_obstacle_distance(member), - 'boundary_distance': self._calculate_boundary_distance(member), - 'velocity': member['velocity'].length(), - 'position_x': member['position'].x, - 'position_y': member['position'].y, - 'position_z': member['position'].z - } - - # 添加经验(简化) - # 在实际应用中,这里需要更复杂的动作表示 - action = "maintain" # 简化动作 - next_state = state.copy() # 简化下一状态 - - self.learning_manager.add_experience(state, action, reward, next_state) - - # 获取环境响应力 - environment_force = Vec3(0, 0, 0) - if (hasattr(self, 'environment_manager') and - self.config.get("environment_awareness_enabled", True)): - environment_force = self.environment_manager.get_environmental_response( - member, member['position'], member['velocity'] - ) - - # 使用Boids算法更新成员状态,添加环境响应力 - self.boids_algorithm.update_member( - member, neighbors, obstacles, target, bounds, path, predators, interactions - ) - - # 应用环境响应力 - if self.config.get("environment_awareness_enabled", True): - member['velocity'] += environment_force * 0.1 # 环境力权重 - - # 更新成员节点位置 - member['node'].setPos(member['position']) - - # 更新成员朝向(面向移动方向) - if member['velocity'].length() > 0: - # 创建一个向前的向量 - forward = member['velocity'].normalized() - # 设置节点朝向 - member['node'].lookAt(member['position'] + forward) - - # 更新行为策略(定期更新) - if hasattr(self, 'learning_manager') and self.config.get("learning_enabled", False): - if self.frame_count % 60 == 0: # 每秒更新一次(假设60FPS) - self.learning_manager.update_behavior_policies() - - def _update_swarm_parallel(self): - """ - 使用并行计算更新群体行为 - """ - # 获取扩展参数 - obstacles = self.obstacles if self.config.get("obstacle_avoidance_enabled", True) else [] - bounds = self.bounds if self.config.get("boundary_enabled", True) else None - predators = self.predators if self.config.get("predator_avoid_enabled", False) else [] - target = self.target if self.config.get("seek_enabled", False) else None - path = self.path if self.config.get("path_following_enabled", False) else None - - # 准备交互力 - interaction_forces = {} - if hasattr(self, 'interaction_manager'): - # 为每个成员计算交互力 - for member in self.members: - interaction_forces[id(member)] = {} - - # 应用合作力 - if 'cooperative_force' in member: - interaction_forces[id(member)]['cooperative_force'] = member['cooperative_force'] - del member['cooperative_force'] - - # 应用竞争力 - if 'competition_force' in member: - interaction_forces[id(member)]['competition_force'] = member['competition_force'] - del member['competition_force'] - - # 应用捕食力 - if 'predation_force' in member: - interaction_forces[id(member)]['predation_force'] = member['predation_force'] - del member['predation_force'] - - # 应用逃避力 - if 'escape_force' in member: - interaction_forces[id(member)]['escape_force'] = member['escape_force'] - del member['escape_force'] - - # 准备环境力参数 - environment_forces = {} - if (hasattr(self, 'environment_manager') and - self.config.get("environment_awareness_enabled", True)): - for member in self.members: - environment_forces[id(member)] = self.environment_manager.get_environmental_response( - member, member['position'], member['velocity'] - ) - else: - # 如果环境感知被禁用,设置为零力 - for member in self.members: - environment_forces[id(member)] = Vec3(0, 0, 0) - - # 使用并行算法更新群体成员 - self.boids_algorithm.update_swarm_parallel( - self.members, self.members, obstacles, target, bounds, path, predators, interaction_forces, environment_forces - ) - - # 更新所有成员的节点位置和朝向 - for member in self.members: - member['node'].setPos(member['position']) - - # 更新成员朝向(面向移动方向) - if member['velocity'].length() > 0: - # 创建一个向前的向量 - forward = member['velocity'].normalized() - # 设置节点朝向 - member['node'].lookAt(member['position'] + forward) - - def _update_predators(self): - """ - 更新捕食者行为 - """ - for predator in self.predators: - if self.members: - # 简单的捕食者行为:寻找最近的群体成员 - nearest_member = None - nearest_distance = float('inf') - - for member in self.members: - distance = (predator['position'] - member['position']).length() - if distance < nearest_distance: - nearest_distance = distance - nearest_member = member - - if nearest_member: - # 向最近的成员移动 - direction = (nearest_member['position'] - predator['position']).normalized() - predator['velocity'] = direction * 3.0 # 捕食者速度 - predator['position'] += predator['velocity'] - predator['node'].setPos(predator['position']) - - def _calculate_environment_state(self): - """ - 计算环境状态 - - :return: 环境状态字典 - """ - if not self.members: - return {} - - # 计算群体密度 - member_count = len(self.members) - total_area = (self.bounds['max_x'] - self.bounds['min_x']) * (self.bounds['max_y'] - self.bounds['min_y']) - density = member_count / total_area if total_area > 0 else 0 - - # 计算平均速度 - total_speed = sum(member['velocity'].length() for member in self.members) - avg_speed = total_speed / member_count if member_count > 0 else 0 - - # 计算与障碍物的平均距离 - total_obstacle_distance = 0 - obstacle_count = 0 - for member in self.members: - min_distance = float('inf') - for obstacle in self.obstacles: - distance = (member['position'] - obstacle['position']).length() - obstacle.get('radius', 1.0) - min_distance = min(min_distance, distance) - if min_distance != float('inf'): - total_obstacle_distance += min_distance - obstacle_count += 1 - - avg_obstacle_distance = total_obstacle_distance / obstacle_count if obstacle_count > 0 else 10.0 - - # 威胁水平(基于与障碍物的距离) - threat_level = max(0.0, min(1.0, 1.0 - avg_obstacle_distance / 20.0)) - - # 资源丰富度(简化为群体密度的反比) - resource_abundance = max(0.0, min(1.0, 1.0 - density * 10.0)) - - return { - 'density': density, - 'avg_speed': avg_speed, - 'avg_obstacle_distance': avg_obstacle_distance, - 'threat_level': threat_level, - 'resource_abundance': resource_abundance, - 'member_count': member_count - } - - def _calculate_min_obstacle_distance(self, member): - """ - 计算成员与最近障碍物的距离 - - :param member: 群体成员 - :return: 最小距离 - """ - if not self.obstacles: - return float('inf') - - min_distance = float('inf') - for obstacle in self.obstacles: - distance = (member['position'] - obstacle['position']).length() - obstacle.get('radius', 1.0) - min_distance = min(min_distance, distance) - - return min_distance - - def _calculate_boundary_distance(self, member): - """ - 计算成员与边界的距离 - - :param member: 群体成员 - :return: 最小边界距离 - """ - min_distance = float('inf') - - # 计算到各边界的距离 - distances = [ - member['position'].x - self.bounds['min_x'], - self.bounds['max_x'] - member['position'].x, - member['position'].y - self.bounds['min_y'], - self.bounds['max_y'] - member['position'].y, - member['position'].z - self.bounds['min_z'], - self.bounds['max_z'] - member['position'].z - ] - - return min(distances) if distances else float('inf') - - def _update_boids_parameters(self, adapted_params): - """ - 更新Boids算法参数 - - :param adapted_params: 适应后的参数 - """ - # 更新速度 - current_max_speed = self.config.get("max_speed", 5.0) - self.config.set("max_speed", current_max_speed * adapted_params['speed']) - - # 更新行为权重 - current_cohesion = self.config.get("cohesion_weight", 1.0) - self.config.set("cohesion_weight", current_cohesion * adapted_params['cohesion']) - - current_separation = self.config.get("separation_weight", 1.5) - self.config.set("separation_weight", current_separation * adapted_params['separation']) - - current_alignment = self.config.get("alignment_weight", 1.0) - self.config.set("alignment_weight", current_alignment * adapted_params['alignment']) - - def _find_neighbors(self, member, index): - """ - 查找邻居 - 使用空间分区优化邻居查找(简化实现) - """ - neighbors = [] - perception_radius = self.config.get("perception_radius", self.perception_radius) - - for i, other in enumerate(self.members): - if i != index: # 排除自己 - distance = (member['position'] - other['position']).length() - if distance < perception_radius: - neighbors.append(other) - return neighbors - - def add_member(self): - """ - 添加一个新的群体成员 - """ - # 随机位置(靠近现有群体) - if self.members: - # 在现有群体附近创建新成员 - existing_member = random.choice(self.members) - pos = Vec3( - existing_member['position'].x + random.uniform(-5, 5), - existing_member['position'].y + random.uniform(-5, 5), - existing_member['position'].z + random.uniform(-2, 2) - ) - else: - # 如果没有现有成员,在随机位置创建 - pos = Vec3( - random.uniform(-20, 20), - random.uniform(-20, 20), - random.uniform(5, 15) - ) - - # 加载模型 - try: - member_node = self.world.loader.loadModel("models/misc/sphere") - except: - member_node = self.world.loader.loadModel("models/misc/sphere") - - member_node.reparentTo(self.world.render) - member_node.setPos(pos) - member_node.setScale(0.5) - - # 随机初始速度 - velocity = Vec3( - random.uniform(-1, 1), - random.uniform(-1, 1), - random.uniform(-1, 1) - ) - - # 添加到群体成员列表 - member = { - 'node': member_node, - 'position': pos, - 'velocity': velocity, - 'index': len(self.members), # 成员索引 - 'type': self.swarm_type - } - - self.members.append(member) - self.config.set("member_count", len(self.members)) - return member - - def remove_member(self): - """ - 移除一个群体成员 - """ - if self.members: - # 移除随机成员 - member = self.members.pop() - member['node'].removeNode() - self.config.set("member_count", len(self.members)) - return member - return None - - def update_swarm_type(self, swarm_type): - """ - 更新群体类型 - """ - self.swarm_type = swarm_type - # 重新创建群体以应用新的类型 - self.cleanup() - self.create_example_swarm() - - def get_stats(self): - """ - 获取群体统计信息 - """ - return { - 'member_count': len(self.members), - 'paused': self.paused, - 'fps': self.fps, - 'swarm_type': self.swarm_type - } - - def toggle_pause(self): - """ - 切换暂停状态 - """ - self.paused = not self.paused - print(f"群体模拟{'已暂停' if self.paused else '已恢复'}") - - def cleanup(self): - """ - 清理资源 - """ - # 移除任务 - if self.task: - self.world.taskMgr.remove(self.task) - - # 移除所有群体成员 - for member in self.members: - member['node'].removeNode() - - # 移除所有捕食者 - for predator in self.predators: - predator['node'].removeNode() - - # 清空列表 - self.members.clear() - self.obstacles.clear() - self.predators.clear() - - def reset(self): - """ - 重置群体 - """ - self.cleanup() +""" +群体管理器 +负责创建和管理群体成员 +""" + +from panda3d.core import Vec3, Point3, LVector3f +import random +import math +import time + +class SwarmManager: + """ + 群体管理器 + 负责创建和管理群体成员,实现完整的群体智能行为 + """ + + def __init__(self, world): + self.world = world + self.members = [] + self.obstacles = [] + self.predators = [] + self.paused = False + self.task = None + self.swarm_type = "鸟类" + self.target = None + self.path = [] + self.bounds = { + 'min_x': -50, 'max_x': 50, + 'min_y': -50, 'max_y': 50, + 'min_z': 0, 'max_z': 30 + } + + # 多群体支持 + self.swarms = [] # 存储多个群体 + self.swarm_id_counter = 0 + + # 性能统计 + self.frame_count = 0 + self.last_update_time = time.time() + self.fps = 0 + + # 群体参数 + self.cohesion_weight = 1.0 + self.separation_weight = 1.5 + self.alignment_weight = 1.0 + self.max_speed = 5.0 + self.perception_radius = 10.0 + self.member_count = 20 + + # 初始化高级Boids算法 + from .advanced_boids import AdvancedBoidsAlgorithm + from .config import Config + from .interaction_manager import InteractionManager + from .learning_manager import LearningManager + from .environment_manager import EnvironmentManager + self.config = Config() + self.boids_algorithm = AdvancedBoidsAlgorithm(self.config) + self.interaction_manager = InteractionManager(self.config) + self.learning_manager = LearningManager(self.config) + self.environment_manager = EnvironmentManager(self.config) + + # 启动更新任务 + self.task = self.world.taskMgr.add(self._update_task, "swarm_update_task") + + def create_example_swarm(self, swarm_type=None, member_count=None, position_offset=Vec3(0, 0, 0)): + """ + 创建示例群体 + 根据群体类型创建不同类型的群体成员 + """ + # 如果是创建新的独立群体,不清理现有群体 + if swarm_type is None and member_count is None: + # 清理现有群体 + self.cleanup() + + # 获取群体类型和数量 + swarm_type = swarm_type or self.swarm_type + count = member_count or self.config.get("member_count", self.member_count) + + # 创建指定数量的群体成员 + new_members = [] + for i in range(count): + # 根据群体类型设置随机位置范围,并应用位置偏移 + if swarm_type == "鸟类": + pos = Vec3( + random.uniform(-20, 20), + random.uniform(-20, 20), + random.uniform(5, 25) + ) + position_offset + elif swarm_type == "鱼类": + pos = Vec3( + random.uniform(-15, 15), + random.uniform(-15, 15), + random.uniform(2, 15) + ) + position_offset + elif swarm_type == "昆虫": + pos = Vec3( + random.uniform(-10, 10), + random.uniform(-10, 10), + random.uniform(1, 10) + ) + position_offset + else: # 自定义 + pos = Vec3( + random.uniform(self.bounds['min_x'], self.bounds['max_x']), + random.uniform(self.bounds['min_y'], self.bounds['min_y']), + random.uniform(self.bounds['min_z'], self.bounds['max_z']) + ) + position_offset + + # 根据群体类型加载不同的模型 + if swarm_type == "鸟类": + member_model = "models/misc/sphere" # 实际应用中应使用鸟类模型 + scale = 0.5 + elif swarm_type == "鱼类": + member_model = "models/misc/sphere" # 实际应用中应使用鱼类模型 + scale = 0.3 + elif swarm_type == "昆虫": + member_model = "models/misc/sphere" # 实际应用中应使用昆虫模型 + scale = 0.2 + else: + member_model = "models/misc/sphere" + scale = 0.5 + + # 加载模型 + try: + member_node = self.world.loader.loadModel(member_model) + except: + # 如果模型加载失败,使用默认球体 + member_node = self.world.loader.loadModel("models/misc/sphere") + + member_node.reparentTo(self.world.render) + member_node.setPos(pos) + member_node.setScale(scale) + + # 随机初始速度 + velocity = Vec3( + random.uniform(-1, 1), + random.uniform(-1, 1), + random.uniform(-1, 1) + ) + + # 添加到群体成员列表 + member = { + 'node': member_node, + 'position': pos, + 'velocity': velocity, + 'index': i, # 成员索引,用于队形计算 + 'type': swarm_type, # 成员类型 + 'swarm_id': self.swarm_id_counter # 群体ID + } + new_members.append(member) + + # 为成员添加环境传感器 + if hasattr(self, 'environment_manager'): + self.environment_manager.add_sensor(id(member)) + + # 如果是创建新的独立群体 + if swarm_type is not None or member_count is not None: + # 创建新的群体对象 + swarm = { + 'id': self.swarm_id_counter, + 'type': swarm_type, + 'members': new_members, + 'position_offset': position_offset + } + self.swarms.append(swarm) + self.swarm_id_counter += 1 + + # 将新成员添加到主成员列表 + self.members.extend(new_members) + + print(f"已创建包含{len(new_members)}个成员的{swarm_type}群体 (ID: {swarm['id']})") + return swarm + else: + # 更新主成员列表 + self.members = new_members + print(f"已创建包含{len(self.members)}个成员的{swarm_type}群体") + + def create_multiple_swarms(self, swarm_configs: List[Dict]): + """ + 创建多个群体 + :param swarm_configs: 群体配置列表,每个配置包含type, count, position_offset + """ + self.cleanup() # 清理现有群体 + self.swarms.clear() # 清理群体列表 + self.swarm_id_counter = 0 # 重置ID计数器 + + for config in swarm_configs: + swarm_type = config.get('type', '鸟类') + member_count = config.get('count', 20) + position_offset = config.get('position_offset', Vec3(0, 0, 0)) + + self.create_example_swarm(swarm_type, member_count, position_offset) + + print(f"已创建{len(self.swarms)}个群体") + + def get_swarm_by_id(self, swarm_id: int): + """ + 根据ID获取群体 + """ + for swarm in self.swarms: + if swarm['id'] == swarm_id: + return swarm + return None + + def get_swarm_by_type(self, swarm_type: str): + """ + 根据类型获取群体 + """ + return [swarm for swarm in self.swarms if swarm['type'] == swarm_type] + + def create_obstacle(self, position, radius): + """ + 创建障碍物 + """ + obstacle = { + 'position': position, + 'radius': radius + } + self.obstacles.append(obstacle) + return obstacle + + def create_predator(self, position): + """ + 创建捕食者 + """ + # 创建捕食者模型 + try: + predator_node = self.world.loader.loadModel("models/misc/sphere") # 实际应用中应使用捕食者模型 + except: + predator_node = self.world.loader.loadModel("models/misc/sphere") + + predator_node.reparentTo(self.world.render) + predator_node.setPos(position) + predator_node.setScale(1.0) + predator_node.setColor(1, 0, 0, 1) # 红色表示捕食者 + + # 创建捕食者对象 + predator = { + 'node': predator_node, + 'position': position, + 'velocity': Vec3(0, 0, 0), + 'target_index': 0 # 目标群体成员索引 + } + + self.predators.append(predator) + return predator + + def set_target(self, target): + """ + 设置群体目标位置 + """ + self.target = target + + def set_path(self, path): + """ + 设置群体移动路径 + """ + self.path = path + + def _update_task(self, task): + """ + 更新任务 + """ + if not self.paused: + self._update_swarm() + + # 更新性能统计 + self.frame_count += 1 + current_time = time.time() + if current_time - self.last_update_time >= 1.0: + self.fps = self.frame_count + self.frame_count = 0 + self.last_update_time = current_time + + return task.cont + + def _update_swarm(self): + """ + 更新群体行为 + """ + # 更新捕食者行为(如果存在) + self._update_predators() + + # 更新群体间交互 + interaction_forces = {} + if hasattr(self, 'interaction_manager'): + # 为每个成员计算交互力 + for member in self.members: + interaction_forces[id(member)] = {} + + # 应用合作力 + if 'cooperative_force' in member: + interaction_forces[id(member)]['cooperative_force'] = member['cooperative_force'] + del member['cooperative_force'] + + # 应用竞争力 + if 'competition_force' in member: + interaction_forces[id(member)]['competition_force'] = member['competition_force'] + del member['competition_force'] + + # 应用捕食力 + if 'predation_force' in member: + interaction_forces[id(member)]['predation_force'] = member['predation_force'] + del member['predation_force'] + + # 应用逃避力 + if 'escape_force' in member: + interaction_forces[id(member)]['escape_force'] = member['escape_force'] + del member['escape_force'] + + # 更新环境状态 + if hasattr(self, 'environment_manager'): + self.environment_manager.update_environment_state( + self.obstacles, self.swarms, self.predators, + getattr(self, 'resources', []), getattr(self, 'hazards', []) + ) + + # 更新学习和适应 + if hasattr(self, 'learning_manager') and self.config.get("learning_enabled", False): + # 计算环境状态 + environment_state = self._calculate_environment_state() + + # 更新环境模型 + self.learning_manager.update_environment_model(environment_state) + + # 适应环境 + self.learning_manager.adapt_to_environment(environment_state) + + # 获取适应后的参数 + adapted_params = self.learning_manager.get_adapted_parameters() + + # 更新Boids算法参数 + self._update_boids_parameters(adapted_params) + + # 检查是否启用并行计算 + if self.config.get("parallel_computation", False) and len(self.members) > 50: + # 使用并行计算更新群体成员 + self._update_swarm_parallel() + else: + # 使用串行计算更新群体成员 + for i, member in enumerate(self.members): + # 计算邻居 + neighbors = self._find_neighbors(member, i) + + # 获取扩展参数 + obstacles = self.obstacles if self.config.get("obstacle_avoidance_enabled", True) else [] + bounds = self.bounds if self.config.get("boundary_enabled", True) else None + predators = self.predators if self.config.get("predator_avoid_enabled", False) else [] + target = self.target if self.config.get("seek_enabled", False) else None + path = self.path if self.config.get("path_following_enabled", False) else None + interactions = interaction_forces.get(id(member), {}) if interaction_forces else {} + + # 计算奖励(如果启用学习) + if hasattr(self, 'learning_manager') and self.config.get("learning_enabled", False): + reward = self.learning_manager.calculate_reward(member, neighbors, obstacles, environment_state) + + # 创建状态表示 + state = { + 'neighbor_count': len(neighbors), + 'obstacle_distance': self._calculate_min_obstacle_distance(member), + 'boundary_distance': self._calculate_boundary_distance(member), + 'velocity': member['velocity'].length(), + 'position_x': member['position'].x, + 'position_y': member['position'].y, + 'position_z': member['position'].z + } + + # 添加经验(简化) + # 在实际应用中,这里需要更复杂的动作表示 + action = "maintain" # 简化动作 + next_state = state.copy() # 简化下一状态 + + self.learning_manager.add_experience(state, action, reward, next_state) + + # 获取环境响应力 + environment_force = Vec3(0, 0, 0) + if (hasattr(self, 'environment_manager') and + self.config.get("environment_awareness_enabled", True)): + environment_force = self.environment_manager.get_environmental_response( + member, member['position'], member['velocity'] + ) + + # 使用Boids算法更新成员状态,添加环境响应力 + self.boids_algorithm.update_member( + member, neighbors, obstacles, target, bounds, path, predators, interactions + ) + + # 应用环境响应力 + if self.config.get("environment_awareness_enabled", True): + member['velocity'] += environment_force * 0.1 # 环境力权重 + + # 更新成员节点位置 + member['node'].setPos(member['position']) + + # 更新成员朝向(面向移动方向) + if member['velocity'].length() > 0: + # 创建一个向前的向量 + forward = member['velocity'].normalized() + # 设置节点朝向 + member['node'].lookAt(member['position'] + forward) + + # 更新行为策略(定期更新) + if hasattr(self, 'learning_manager') and self.config.get("learning_enabled", False): + if self.frame_count % 60 == 0: # 每秒更新一次(假设60FPS) + self.learning_manager.update_behavior_policies() + + def _update_swarm_parallel(self): + """ + 使用并行计算更新群体行为 + """ + # 获取扩展参数 + obstacles = self.obstacles if self.config.get("obstacle_avoidance_enabled", True) else [] + bounds = self.bounds if self.config.get("boundary_enabled", True) else None + predators = self.predators if self.config.get("predator_avoid_enabled", False) else [] + target = self.target if self.config.get("seek_enabled", False) else None + path = self.path if self.config.get("path_following_enabled", False) else None + + # 准备交互力 + interaction_forces = {} + if hasattr(self, 'interaction_manager'): + # 为每个成员计算交互力 + for member in self.members: + interaction_forces[id(member)] = {} + + # 应用合作力 + if 'cooperative_force' in member: + interaction_forces[id(member)]['cooperative_force'] = member['cooperative_force'] + del member['cooperative_force'] + + # 应用竞争力 + if 'competition_force' in member: + interaction_forces[id(member)]['competition_force'] = member['competition_force'] + del member['competition_force'] + + # 应用捕食力 + if 'predation_force' in member: + interaction_forces[id(member)]['predation_force'] = member['predation_force'] + del member['predation_force'] + + # 应用逃避力 + if 'escape_force' in member: + interaction_forces[id(member)]['escape_force'] = member['escape_force'] + del member['escape_force'] + + # 准备环境力参数 + environment_forces = {} + if (hasattr(self, 'environment_manager') and + self.config.get("environment_awareness_enabled", True)): + for member in self.members: + environment_forces[id(member)] = self.environment_manager.get_environmental_response( + member, member['position'], member['velocity'] + ) + else: + # 如果环境感知被禁用,设置为零力 + for member in self.members: + environment_forces[id(member)] = Vec3(0, 0, 0) + + # 使用并行算法更新群体成员 + self.boids_algorithm.update_swarm_parallel( + self.members, self.members, obstacles, target, bounds, path, predators, interaction_forces, environment_forces + ) + + # 更新所有成员的节点位置和朝向 + for member in self.members: + member['node'].setPos(member['position']) + + # 更新成员朝向(面向移动方向) + if member['velocity'].length() > 0: + # 创建一个向前的向量 + forward = member['velocity'].normalized() + # 设置节点朝向 + member['node'].lookAt(member['position'] + forward) + + def _update_predators(self): + """ + 更新捕食者行为 + """ + for predator in self.predators: + if self.members: + # 简单的捕食者行为:寻找最近的群体成员 + nearest_member = None + nearest_distance = float('inf') + + for member in self.members: + distance = (predator['position'] - member['position']).length() + if distance < nearest_distance: + nearest_distance = distance + nearest_member = member + + if nearest_member: + # 向最近的成员移动 + direction = (nearest_member['position'] - predator['position']).normalized() + predator['velocity'] = direction * 3.0 # 捕食者速度 + predator['position'] += predator['velocity'] + predator['node'].setPos(predator['position']) + + def _calculate_environment_state(self): + """ + 计算环境状态 + + :return: 环境状态字典 + """ + if not self.members: + return {} + + # 计算群体密度 + member_count = len(self.members) + total_area = (self.bounds['max_x'] - self.bounds['min_x']) * (self.bounds['max_y'] - self.bounds['min_y']) + density = member_count / total_area if total_area > 0 else 0 + + # 计算平均速度 + total_speed = sum(member['velocity'].length() for member in self.members) + avg_speed = total_speed / member_count if member_count > 0 else 0 + + # 计算与障碍物的平均距离 + total_obstacle_distance = 0 + obstacle_count = 0 + for member in self.members: + min_distance = float('inf') + for obstacle in self.obstacles: + distance = (member['position'] - obstacle['position']).length() - obstacle.get('radius', 1.0) + min_distance = min(min_distance, distance) + if min_distance != float('inf'): + total_obstacle_distance += min_distance + obstacle_count += 1 + + avg_obstacle_distance = total_obstacle_distance / obstacle_count if obstacle_count > 0 else 10.0 + + # 威胁水平(基于与障碍物的距离) + threat_level = max(0.0, min(1.0, 1.0 - avg_obstacle_distance / 20.0)) + + # 资源丰富度(简化为群体密度的反比) + resource_abundance = max(0.0, min(1.0, 1.0 - density * 10.0)) + + return { + 'density': density, + 'avg_speed': avg_speed, + 'avg_obstacle_distance': avg_obstacle_distance, + 'threat_level': threat_level, + 'resource_abundance': resource_abundance, + 'member_count': member_count + } + + def _calculate_min_obstacle_distance(self, member): + """ + 计算成员与最近障碍物的距离 + + :param member: 群体成员 + :return: 最小距离 + """ + if not self.obstacles: + return float('inf') + + min_distance = float('inf') + for obstacle in self.obstacles: + distance = (member['position'] - obstacle['position']).length() - obstacle.get('radius', 1.0) + min_distance = min(min_distance, distance) + + return min_distance + + def _calculate_boundary_distance(self, member): + """ + 计算成员与边界的距离 + + :param member: 群体成员 + :return: 最小边界距离 + """ + min_distance = float('inf') + + # 计算到各边界的距离 + distances = [ + member['position'].x - self.bounds['min_x'], + self.bounds['max_x'] - member['position'].x, + member['position'].y - self.bounds['min_y'], + self.bounds['max_y'] - member['position'].y, + member['position'].z - self.bounds['min_z'], + self.bounds['max_z'] - member['position'].z + ] + + return min(distances) if distances else float('inf') + + def _update_boids_parameters(self, adapted_params): + """ + 更新Boids算法参数 + + :param adapted_params: 适应后的参数 + """ + # 更新速度 + current_max_speed = self.config.get("max_speed", 5.0) + self.config.set("max_speed", current_max_speed * adapted_params['speed']) + + # 更新行为权重 + current_cohesion = self.config.get("cohesion_weight", 1.0) + self.config.set("cohesion_weight", current_cohesion * adapted_params['cohesion']) + + current_separation = self.config.get("separation_weight", 1.5) + self.config.set("separation_weight", current_separation * adapted_params['separation']) + + current_alignment = self.config.get("alignment_weight", 1.0) + self.config.set("alignment_weight", current_alignment * adapted_params['alignment']) + + def _find_neighbors(self, member, index): + """ + 查找邻居 + 使用空间分区优化邻居查找(简化实现) + """ + neighbors = [] + perception_radius = self.config.get("perception_radius", self.perception_radius) + + for i, other in enumerate(self.members): + if i != index: # 排除自己 + distance = (member['position'] - other['position']).length() + if distance < perception_radius: + neighbors.append(other) + return neighbors + + def add_member(self): + """ + 添加一个新的群体成员 + """ + # 随机位置(靠近现有群体) + if self.members: + # 在现有群体附近创建新成员 + existing_member = random.choice(self.members) + pos = Vec3( + existing_member['position'].x + random.uniform(-5, 5), + existing_member['position'].y + random.uniform(-5, 5), + existing_member['position'].z + random.uniform(-2, 2) + ) + else: + # 如果没有现有成员,在随机位置创建 + pos = Vec3( + random.uniform(-20, 20), + random.uniform(-20, 20), + random.uniform(5, 15) + ) + + # 加载模型 + try: + member_node = self.world.loader.loadModel("models/misc/sphere") + except: + member_node = self.world.loader.loadModel("models/misc/sphere") + + member_node.reparentTo(self.world.render) + member_node.setPos(pos) + member_node.setScale(0.5) + + # 随机初始速度 + velocity = Vec3( + random.uniform(-1, 1), + random.uniform(-1, 1), + random.uniform(-1, 1) + ) + + # 添加到群体成员列表 + member = { + 'node': member_node, + 'position': pos, + 'velocity': velocity, + 'index': len(self.members), # 成员索引 + 'type': self.swarm_type + } + + self.members.append(member) + self.config.set("member_count", len(self.members)) + return member + + def remove_member(self): + """ + 移除一个群体成员 + """ + if self.members: + # 移除随机成员 + member = self.members.pop() + member['node'].removeNode() + self.config.set("member_count", len(self.members)) + return member + return None + + def update_swarm_type(self, swarm_type): + """ + 更新群体类型 + """ + self.swarm_type = swarm_type + # 重新创建群体以应用新的类型 + self.cleanup() + self.create_example_swarm() + + def get_stats(self): + """ + 获取群体统计信息 + """ + return { + 'member_count': len(self.members), + 'paused': self.paused, + 'fps': self.fps, + 'swarm_type': self.swarm_type + } + + def toggle_pause(self): + """ + 切换暂停状态 + """ + self.paused = not self.paused + print(f"群体模拟{'已暂停' if self.paused else '已恢复'}") + + def cleanup(self): + """ + 清理资源 + """ + # 移除任务 + if self.task: + self.world.taskMgr.remove(self.task) + + # 移除所有群体成员 + for member in self.members: + member['node'].removeNode() + + # 移除所有捕食者 + for predator in self.predators: + predator['node'].removeNode() + + # 清空列表 + self.members.clear() + self.obstacles.clear() + self.predators.clear() + + def reset(self): + """ + 重置群体 + """ + self.cleanup() self.create_example_swarm() \ No newline at end of file diff --git a/plugins/user/terrain_editor/__init__.py b/plugins/user/terrain_editor/__init__.py index 8065a9f5..a6c0efa6 100644 --- a/plugins/user/terrain_editor/__init__.py +++ b/plugins/user/terrain_editor/__init__.py @@ -1,3 +1,3 @@ -""" -地形编辑器插件 +""" +地形编辑器插件 """ \ No newline at end of file diff --git a/plugins/user/terrain_editor/animation/__init__.py b/plugins/user/terrain_editor/animation/__init__.py index 9cd3ce75..b8100399 100644 --- a/plugins/user/terrain_editor/animation/__init__.py +++ b/plugins/user/terrain_editor/animation/__init__.py @@ -1,3 +1,3 @@ -""" -地形动画系统初始化文件 +""" +地形动画系统初始化文件 """ \ No newline at end of file diff --git a/plugins/user/terrain_editor/animation/animation_system.py b/plugins/user/terrain_editor/animation/animation_system.py index 0e936918..e4ef9ff1 100644 --- a/plugins/user/terrain_editor/animation/animation_system.py +++ b/plugins/user/terrain_editor/animation/animation_system.py @@ -1,1551 +1,1551 @@ -""" -地形动画和变形系统 -提供完整的地形动画、变形、morphing、顶点动画、骨骼动画等功能 -""" - -from panda3d.core import NodePath, Vec3, Point3, Mat4, LVector3f -from panda3d.core import Shader, ShaderAttrib, TransformState -from panda3d.core import Texture, TextureStage, PNMImage -from panda3d.core import LerpFunctionInterval, LerpPosInterval, LerpHprInterval -from panda3d.core import ClockObject, deg2Rad, rad2Deg -import math -import random -import json -import os -import numpy as np -from collections import deque - -class TerrainAnimationSystem: - """ - 地形动画和变形系统类 - 提供地形动画、变形、morphing、顶点动画、骨骼动画等功能 - """ - - def __init__(self, world): - self.world = world - self.terrain_animations = {} # 存储地形动画 - self.deformation_effects = {} # 存储变形效果 - self.morph_targets = {} # 存储morph目标 - self.vertex_animations = {} # 存储顶点动画 - self.skeletal_animations = {} # 存储骨骼动画 - self.animation_intervals = [] # 存储动画间隔 - self.animation_enabled = True - self.animation_speed = 1.0 # 动画速度倍数 - - # 性能监控 - self.performance_stats = { - 'last_update_time': 0.0, - 'update_count': 0, - 'avg_update_time': 0.0 - } - - # 初始化动画系统 - self._init_animation_system() - - def _init_animation_system(self): - """ - 初始化动画系统(增强版) - """ - try: - # 初始化动画缓存 - self.animation_cache = {} - - # 初始化变形缓存 - self.deformation_cache = {} - - # 初始化时间管理器 - self.time_manager = { - 'global_time': 0.0, - 'animation_time_scale': 1.0, - 'paused': False - } - - print("地形动画系统初始化完成") - - except Exception as e: - print(f"初始化动画系统时出错: {e}") - - def create_terrain_animation(self, terrain_info, animation_name, keyframes, - loop=False, speed=1.0, easing='linear'): - """ - 创建地形动画(增强版) - terrain_info: 地形信息 - animation_name: 动画名称 - keyframes: 关键帧数据 - loop: 是否循环 - speed: 动画速度 - easing: 缓动函数 - """ - try: - # 验证关键帧数据 - if not keyframes or len(keyframes) < 1: - print("关键帧数据无效") - return None - - # 排序关键帧 - sorted_keyframes = sorted(keyframes, key=lambda kf: kf['time']) - - # 创建动画信息 - animation_info = { - 'name': animation_name, - 'terrain_info': terrain_info, - 'keyframes': sorted_keyframes, - 'duration': sorted_keyframes[-1]['time'] if sorted_keyframes else 0.0, - 'loop': loop, - 'speed': speed, - 'easing': easing, - 'playing': False, - 'paused': False, - 'current_time': 0.0, - 'start_time': 0.0, - 'blend_weight': 1.0, - 'blend_mode': 'override', # override, add, multiply - 'animation_type': 'transform', # transform, vertex, material - 'creation_time': self.world.globalClock.getFrameTime() - } - - # 保存动画 - terrain_node = terrain_info['node'] - if terrain_node not in self.terrain_animations: - self.terrain_animations[terrain_node] = {} - self.terrain_animations[terrain_node][animation_name] = animation_info - - # 缓存动画数据以提高性能 - self._cache_animation_data(animation_info) - - print(f"创建地形动画: {animation_name} (持续时间: {animation_info['duration']}秒)") - return animation_info - - except Exception as e: - print(f"创建地形动画时出错: {e}") - return None - - def _cache_animation_data(self, animation_info): - """ - 缓存动画数据以提高性能 - """ - try: - cache_key = f"{id(animation_info['terrain_info'])}_{animation_info['name']}" - - # 预计算插值数据 - cached_data = { - 'position_curves': [], - 'rotation_curves': [], - 'scale_curves': [], - 'computed_times': [] - } - - keyframes = animation_info['keyframes'] - for i in range(len(keyframes) - 1): - kf1 = keyframes[i] - kf2 = keyframes[i + 1] - - # 计算时间段内的插值曲线 - if 'position' in kf1 and 'position' in kf2: - pos1 = Vec3(*kf1['position']) - pos2 = Vec3(*kf2['position']) - cached_data['position_curves'].append((pos1, pos2, kf1['time'], kf2['time'])) - - if 'rotation' in kf1 and 'rotation' in kf2: - rot1 = Vec3(*kf1['rotation']) - rot2 = Vec3(*kf2['rotation']) - cached_data['rotation_curves'].append((rot1, rot2, kf1['time'], kf2['time'])) - - if 'scale' in kf1 and 'scale' in kf2: - scale1 = Vec3(*kf1['scale']) - scale2 = Vec3(*kf2['scale']) - cached_data['scale_curves'].append((scale1, scale2, kf1['time'], kf2['time'])) - - self.animation_cache[cache_key] = cached_data - - except Exception as e: - print(f"缓存动画数据时出错: {e}") - - def play_terrain_animation(self, terrain_info, animation_name, loop=None, - fade_time=0.0, blend_weight=1.0): - """ - 播放地形动画(增强版) - """ - try: - terrain_node = terrain_info['node'] - if terrain_node not in self.terrain_animations: - print(f"地形没有动画: {animation_name}") - return False - - if animation_name not in self.terrain_animations[terrain_node]: - print(f"动画不存在: {animation_name}") - return False - - # 获取动画信息 - animation_info = self.terrain_animations[terrain_node][animation_name] - - # 更新动画属性 - if loop is not None: - animation_info['loop'] = loop - animation_info['playing'] = True - animation_info['paused'] = False - animation_info['start_time'] = self.world.globalClock.getFrameTime() - animation_info['current_time'] = 0.0 - animation_info['blend_weight'] = blend_weight - - # 处理淡入效果 - if fade_time > 0: - self._start_animation_fade_in(animation_info, fade_time) - - print(f"播放地形动画: {animation_name}") - return True - - except Exception as e: - print(f"播放地形动画时出错: {e}") - return False - - def _start_animation_fade_in(self, animation_info, fade_time): - """ - 开始动画淡入效果 - """ - try: - # 创建淡入定时器 - animation_info['fade_in'] = { - 'duration': fade_time, - 'start_time': self.world.globalClock.getFrameTime(), - 'active': True - } - - except Exception as e: - print(f"开始动画淡入时出错: {e}") - - def stop_terrain_animation(self, terrain_info, animation_name, fade_time=0.0): - """ - 停止地形动画(增强版) - """ - try: - terrain_node = terrain_info['node'] - if (terrain_node in self.terrain_animations and - animation_name in self.terrain_animations[terrain_node]): - animation_info = self.terrain_animations[terrain_node][animation_name] - - # 处理淡出效果 - if fade_time > 0: - self._start_animation_fade_out(animation_info, fade_time) - else: - animation_info['playing'] = False - animation_info['paused'] = False - - print(f"停止地形动画: {animation_name}") - return True - - except Exception as e: - print(f"停止地形动画时出错: {e}") - return False - - def _start_animation_fade_out(self, animation_info, fade_time): - """ - 开始动画淡出效果 - """ - try: - # 创建淡出定时器 - animation_info['fade_out'] = { - 'duration': fade_time, - 'start_time': self.world.globalClock.getFrameTime(), - 'active': True - } - - except Exception as e: - print(f"开始动画淡出时出错: {e}") - - def pause_terrain_animation(self, terrain_info, animation_name): - """ - 暂停地形动画 - """ - try: - terrain_node = terrain_info['node'] - if (terrain_node in self.terrain_animations and - animation_name in self.terrain_animations[terrain_node]): - animation_info = self.terrain_animations[terrain_node][animation_name] - animation_info['paused'] = True - print(f"暂停地形动画: {animation_name}") - return True - - except Exception as e: - print(f"暂停地形动画时出错: {e}") - return False - - def resume_terrain_animation(self, terrain_info, animation_name): - """ - 恢复地形动画 - """ - try: - terrain_node = terrain_info['node'] - if (terrain_node in self.terrain_animations and - animation_name in self.terrain_animations[terrain_node]): - animation_info = self.terrain_animations[terrain_node][animation_name] - animation_info['paused'] = False - print(f"恢复地形动画: {animation_name}") - return True - - except Exception as e: - print(f"恢复地形动画时出错: {e}") - return False - - def update_terrain_animations(self, time_delta): - """ - 更新地形动画(增强版) - """ - try: - if not self.animation_enabled or self.time_manager['paused']: - return - - # 性能监控开始 - start_time = self.world.globalClock.getRealTime() - - # 应用时间缩放 - scaled_time_delta = time_delta * self.time_manager['animation_time_scale'] * self.animation_speed - - current_time = self.world.globalClock.getFrameTime() - self.time_manager['global_time'] += scaled_time_delta - - # 更新每个地形的动画 - for terrain_node, animations in self.terrain_animations.items(): - for animation_name, animation_info in animations.items(): - if animation_info['playing'] and not animation_info['paused']: - # 更新当前时间 - animation_info['current_time'] += scaled_time_delta * animation_info['speed'] - - # 处理淡入淡出 - self._update_animation_fade(animation_info, current_time) - - # 检查是否需要循环 - if animation_info['current_time'] > animation_info['duration']: - if animation_info['loop']: - animation_info['current_time'] = animation_info['current_time'] % animation_info['duration'] - else: - animation_info['playing'] = False - continue - - # 应用动画 - self._apply_terrain_animation(animation_info, scaled_time_delta) - - # 更新性能统计 - end_time = self.world.globalClock.getRealTime() - update_time = end_time - start_time - self.performance_stats['last_update_time'] = update_time - self.performance_stats['update_count'] += 1 - - # 计算平均更新时间 - if self.performance_stats['update_count'] > 1: - alpha = 0.1 - self.performance_stats['avg_update_time'] = ( - alpha * update_time + - (1 - alpha) * self.performance_stats['avg_update_time'] - ) - else: - self.performance_stats['avg_update_time'] = update_time - - except Exception as e: - print(f"更新地形动画时出错: {e}") - - def _update_animation_fade(self, animation_info, current_time): - """ - 更新动画淡入淡出效果 - """ - try: - # 处理淡入 - if 'fade_in' in animation_info and animation_info['fade_in']['active']: - fade_in = animation_info['fade_in'] - elapsed = current_time - fade_in['start_time'] - if elapsed >= fade_in['duration']: - animation_info['blend_weight'] = 1.0 - animation_info['fade_in']['active'] = False - del animation_info['fade_in'] - else: - animation_info['blend_weight'] = elapsed / fade_in['duration'] - - # 处理淡出 - if 'fade_out' in animation_info and animation_info['fade_out']['active']: - fade_out = animation_info['fade_out'] - elapsed = current_time - fade_out['start_time'] - if elapsed >= fade_out['duration']: - animation_info['playing'] = False - animation_info['fade_out']['active'] = False - del animation_info['fade_out'] - else: - animation_info['blend_weight'] = 1.0 - (elapsed / fade_out['duration']) - - except Exception as e: - print(f"更新动画淡入淡出时出错: {e}") - - def _apply_terrain_animation(self, animation_info, time_delta): - """ - 应用地形动画(增强版) - """ - try: - current_time = animation_info['current_time'] - keyframes = animation_info['keyframes'] - blend_weight = animation_info['blend_weight'] - - if len(keyframes) < 1: - return - - # 如果只有一个关键帧,直接应用 - if len(keyframes) == 1: - kf = keyframes[0] - self._apply_keyframe_transform(animation_info['terrain_info'], kf, blend_weight) - return - - # 找到当前时间对应的两个关键帧 - prev_kf = keyframes[0] - next_kf = keyframes[-1] - - for i in range(len(keyframes) - 1): - if keyframes[i]['time'] <= current_time <= keyframes[i + 1]['time']: - prev_kf = keyframes[i] - next_kf = keyframes[i + 1] - break - - # 计算插值因子 - time_diff = next_kf['time'] - prev_kf['time'] - if time_diff > 0: - t = (current_time - prev_kf['time']) / time_diff - - # 应用缓动函数 - t = self._apply_easing(t, animation_info['easing']) - else: - t = 0.0 - - # 插值计算变换 - self._interpolate_and_apply_transform( - animation_info['terrain_info'], - prev_kf, next_kf, t, blend_weight - ) - - except Exception as e: - print(f"应用地形动画时出错: {e}") - - def _apply_easing(self, t, easing_type): - """ - 应用缓动函数 - """ - try: - if easing_type == 'linear': - return t - elif easing_type == 'ease_in': - return t * t - elif easing_type == 'ease_out': - return 1 - (1 - t) * (1 - t) - elif easing_type == 'ease_in_out': - return 3 * t * t - 2 * t * t * t - elif easing_type == 'bounce': - if t < 1/2.75: - return 7.5625 * t * t - elif t < 2/2.75: - t -= 1.5/2.75 - return 7.5625 * t * t + 0.75 - elif t < 2.5/2.75: - t -= 2.25/2.75 - return 7.5625 * t * t + 0.9375 - else: - t -= 2.625/2.75 - return 7.5625 * t * t + 0.984375 - else: - return t - - except Exception as e: - print(f"应用缓动函数时出错: {e}") - return t - - def _apply_keyframe_transform(self, terrain_info, keyframe, blend_weight): - """ - 应用单个关键帧变换 - """ - try: - terrain_node = terrain_info['node'] - - # 应用位置变换 - if 'position' in keyframe: - target_pos = Vec3(*keyframe['position']) - if blend_weight < 1.0: - current_pos = terrain_node.getPos() - final_pos = current_pos + (target_pos - current_pos) * blend_weight - else: - final_pos = target_pos - terrain_node.setPos(final_pos) - - # 应用旋转变换 - if 'rotation' in keyframe: - target_rot = Vec3(*keyframe['rotation']) - if blend_weight < 1.0: - current_rot = terrain_node.getHpr() - final_rot = current_rot + (target_rot - current_rot) * blend_weight - else: - final_rot = target_rot - terrain_node.setHpr(final_rot) - - # 应用缩放变换 - if 'scale' in keyframe: - target_scale = Vec3(*keyframe['scale']) - if blend_weight < 1.0: - current_scale = terrain_node.getScale() - final_scale = current_scale + (target_scale - current_scale) * blend_weight - else: - final_scale = target_scale - terrain_node.setScale(final_scale) - - except Exception as e: - print(f"应用关键帧变换时出错: {e}") - - def _interpolate_and_apply_transform(self, terrain_info, prev_kf, next_kf, t, blend_weight): - """ - 插值并应用变换 - """ - try: - terrain_node = terrain_info['node'] - - # 插值位置 - if 'position' in prev_kf and 'position' in next_kf: - prev_pos = Vec3(*prev_kf['position']) - next_pos = Vec3(*next_kf['position']) - interpolated_pos = prev_pos + (next_pos - prev_pos) * t - - if blend_weight < 1.0: - current_pos = terrain_node.getPos() - final_pos = current_pos + (interpolated_pos - current_pos) * blend_weight - else: - final_pos = interpolated_pos - - terrain_node.setPos(final_pos) - - # 插值旋转 - if 'rotation' in prev_kf and 'rotation' in next_kf: - prev_rot = Vec3(*prev_kf['rotation']) - next_rot = Vec3(*next_kf['rotation']) - interpolated_rot = prev_rot + (next_rot - prev_rot) * t - - if blend_weight < 1.0: - current_rot = terrain_node.getHpr() - final_rot = current_rot + (interpolated_rot - current_rot) * blend_weight - else: - final_rot = interpolated_rot - - terrain_node.setHpr(final_rot) - - # 插值缩放 - if 'scale' in prev_kf and 'scale' in next_kf: - prev_scale = Vec3(*prev_kf['scale']) - next_scale = Vec3(*next_kf['scale']) - interpolated_scale = prev_scale + (next_scale - prev_scale) * t - - if blend_weight < 1.0: - current_scale = terrain_node.getScale() - final_scale = current_scale + (interpolated_scale - current_scale) * blend_weight - else: - final_scale = interpolated_scale - - terrain_node.setScale(final_scale) - - except Exception as e: - print(f"插值并应用变换时出错: {e}") - - def create_height_deformation(self, terrain_info, deformation_name, - center_pos, radius, strength, duration=2.0, - easing='ease_out', falloff='linear'): - """ - 创建高度变形效果(增强版) - """ - try: - # 创建变形效果信息 - deformation_info = { - 'name': deformation_name, - 'terrain_info': terrain_info, - 'center_pos': Vec3(center_pos), - 'radius': radius, - 'strength': strength, - 'duration': duration, - 'easing': easing, - 'falloff': falloff, - 'start_time': self.world.globalClock.getFrameTime(), - 'active': True, - 'progress': 0.0, - 'type': 'height', - 'creation_time': self.world.globalClock.getFrameTime() - } - - # 保存变形效果 - terrain_node = terrain_info['node'] - if terrain_node not in self.deformation_effects: - self.deformation_effects[terrain_node] = [] - self.deformation_effects[terrain_node].append(deformation_info) - - print(f"创建高度变形效果: {deformation_name}") - return deformation_info - - except Exception as e: - print(f"创建高度变形效果时出错: {e}") - return None - - def create_noise_deformation(self, terrain_info, deformation_name, - scale=1.0, strength=0.1, duration=5.0, - octaves=3, persistence=0.5, lacunarity=2.0): - """ - 创建噪声变形效果 - """ - try: - # 创建噪声变形效果信息 - deformation_info = { - 'name': deformation_name, - 'terrain_info': terrain_info, - 'scale': scale, - 'strength': strength, - 'duration': duration, - 'octaves': octaves, - 'persistence': persistence, - 'lacunarity': lacunarity, - 'start_time': self.world.globalClock.getFrameTime(), - 'active': True, - 'progress': 0.0, - 'type': 'noise', - 'seed': random.randint(0, 10000), - 'creation_time': self.world.globalClock.getFrameTime() - } - - # 保存变形效果 - terrain_node = terrain_info['node'] - if terrain_node not in self.deformation_effects: - self.deformation_effects[terrain_node] = [] - self.deformation_effects[terrain_node].append(deformation_info) - - print(f"创建噪声变形效果: {deformation_name}") - return deformation_info - - except Exception as e: - print(f"创建噪声变形效果时出错: {e}") - return None - - def create_wave_deformation(self, terrain_info, deformation_name, - amplitude=0.1, wavelength=10.0, speed=1.0, - direction=Vec3(1, 0, 0), duration=float('inf')): - """ - 创建波浪变形效果 - """ - try: - # 创建波浪变形效果信息 - deformation_info = { - 'name': deformation_name, - 'terrain_info': terrain_info, - 'amplitude': amplitude, - 'wavelength': wavelength, - 'speed': speed, - 'direction': direction.normalized(), - 'duration': duration, - 'start_time': self.world.globalClock.getFrameTime(), - 'active': True, - 'progress': 0.0, - 'type': 'wave', - 'creation_time': self.world.globalClock.getFrameTime() - } - - # 保存变形效果 - terrain_node = terrain_info['node'] - if terrain_node not in self.deformation_effects: - self.deformation_effects[terrain_node] = [] - self.deformation_effects[terrain_node].append(deformation_info) - - print(f"创建波浪变形效果: {deformation_name}") - return deformation_info - - except Exception as e: - print(f"创建波浪变形效果时出错: {e}") - return None - - def update_deformation_effects(self, time_delta): - """ - 更新变形效果(增强版) - """ - try: - if not self.animation_enabled: - return - - current_time = self.world.globalClock.getFrameTime() - scaled_time_delta = time_delta * self.animation_speed - - # 更新每个地形的变形效果 - for terrain_node, deformations in self.deformation_effects.items(): - for deformation_info in deformations[:]: # 使用切片复制列表 - if deformation_info['active']: - # 更新进度 - elapsed_time = current_time - deformation_info['start_time'] - progress = min(1.0, elapsed_time / deformation_info['duration']) if deformation_info['duration'] > 0 else 0.0 - deformation_info['progress'] = progress - - # 检查是否超时 - if deformation_info['duration'] > 0 and elapsed_time > deformation_info['duration']: - deformation_info['active'] = False - continue - - # 根据变形类型应用效果 - if deformation_info['type'] == 'height': - self._apply_height_deformation(deformation_info, progress, scaled_time_delta) - elif deformation_info['type'] == 'noise': - self._apply_noise_deformation(deformation_info, progress, scaled_time_delta) - elif deformation_info['type'] == 'wave': - self._apply_wave_deformation(deformation_info, progress, scaled_time_delta) - - # 更新地形网格 - if deformation_info['active']: - self._update_terrain_mesh(deformation_info['terrain_info']) - - except Exception as e: - print(f"更新变形效果时出错: {e}") - - def _apply_height_deformation(self, deformation_info, progress, time_delta): - """ - 应用高度变形效果(增强版) - """ - try: - terrain_info = deformation_info['terrain_info'] - heightfield = terrain_info['heightfield'] - - if not heightfield: - return - - terrain_node = terrain_info['node'] - terrain_pos = terrain_node.getPos() - terrain_scale = terrain_node.getScale() - - # 获取高度图尺寸 - width = heightfield.getXSize() - height = heightfield.getYSize() - - # 计算中心点在高度图中的位置 - center_offset = (width - 1) / 2 - center_x = int((deformation_info['center_pos'].getX() - terrain_pos.getX()) / terrain_scale.getX() + center_offset) - center_y = int((deformation_info['center_pos'].getY() - terrain_pos.getY()) / terrain_scale.getY() + center_offset) - - # 计算变形半径(在高度图坐标系中) - radius_pixels = int(deformation_info['radius'] / max(terrain_scale.getX(), terrain_scale.getY())) - - # 计算变形强度(应用缓动和进度) - eased_progress = self._apply_easing(progress, deformation_info['easing']) - current_strength = deformation_info['strength'] * eased_progress - - # 应用变形 - modified = False - for dx in range(-radius_pixels, radius_pixels + 1): - for dy in range(-radius_pixels, radius_pixels + 1): - # 计算距离 - distance = math.sqrt(dx*dx + dy*dy) - if distance <= radius_pixels: - # 计算衰减因子 - if deformation_info['falloff'] == 'linear': - weight = 1.0 - (distance / radius_pixels) - elif deformation_info['falloff'] == 'smooth': - weight = math.cos((distance / radius_pixels) * math.pi / 2) - elif deformation_info['falloff'] == 'quadratic': - weight = 1.0 - (distance / radius_pixels) ** 2 - else: - weight = 1.0 - (distance / radius_pixels) - - # 计算目标点 - target_x = center_x + dx - target_y = center_y + dy - - # 检查边界 - if 0 <= target_x < width and 0 <= target_y < height: - # 获取当前高度 - current_height = heightfield.getRed(target_x, target_y) - - # 应用变形 - new_height = current_height + current_strength * weight * 0.01 - new_height = max(0.0, min(1.0, new_height)) - - # 设置新高度 - heightfield.setRed(target_x, target_y, new_height) - heightfield.setGreen(target_x, target_y, new_height) - heightfield.setBlue(target_x, target_y, new_height) - modified = True - - # 标记需要更新网格 - if modified: - terrain_info['_mesh_dirty'] = True - - except Exception as e: - print(f"应用高度变形效果时出错: {e}") - - def _apply_noise_deformation(self, deformation_info, progress, time_delta): - """ - 应用噪声变形效果 - """ - try: - terrain_info = deformation_info['terrain_info'] - heightfield = terrain_info['heightfield'] - - if not heightfield: - return - - # 获取高度图尺寸 - width = heightfield.getXSize() - height_value = heightfield.getYSize() - - # 计算噪声参数 - scale = deformation_info['scale'] - strength = deformation_info['strength'] * progress - seed = deformation_info['seed'] - - # 应用噪声变形 - modified = False - for y in range(height_value): - for x in range(width): - # 生成噪声值 - nx = x / width * scale - ny = y / height_value * scale - - # 简单的噪声实现(实际项目中可以使用更复杂的噪声算法) - noise_value = self._fbm_noise(nx, ny, seed, - deformation_info['octaves'], - deformation_info['persistence'], - deformation_info['lacunarity']) - - # 获取当前高度 - current_height = heightfield.getRed(x, y) - - # 应用噪声变形 - new_height = current_height + noise_value * strength - new_height = max(0.0, min(1.0, new_height)) - - # 设置新高度 - heightfield.setRed(x, y, new_height) - heightfield.setGreen(x, y, new_height) - heightfield.setBlue(x, y, new_height) - modified = True - - # 标记需要更新网格 - if modified: - terrain_info['_mesh_dirty'] = True - - except Exception as e: - print(f"应用噪声变形效果时出错: {e}") - - def _fbm_noise(self, x, y, seed, octaves, persistence, lacunarity): - """ - 分形布朗运动噪声 - """ - try: - value = 0.0 - amplitude = 1.0 - frequency = 1.0 - max_value = 0.0 - - for i in range(octaves): - # 简化的噪声函数(实际项目中可以使用Perlin噪声或Simplex噪声) - sample_x = (x * frequency + seed) % 1000 - sample_y = (y * frequency + seed) % 1000 - noise = math.sin(sample_x) * math.cos(sample_y) - - value += noise * amplitude - max_value += amplitude - - amplitude *= persistence - frequency *= lacunarity - - return value / max_value if max_value > 0 else 0.0 - - except Exception as e: - print(f"计算FMB噪声时出错: {e}") - return 0.0 - - def _apply_wave_deformation(self, deformation_info, progress, time_delta): - """ - 应用波浪变形效果 - """ - try: - terrain_info = deformation_info['terrain_info'] - heightfield = terrain_info['heightfield'] - - if not heightfield: - return - - # 获取高度图尺寸 - width = heightfield.getXSize() - height_value = heightfield.getYSize() - - # 计算波浪参数 - amplitude = deformation_info['amplitude'] - wavelength = deformation_info['wavelength'] - speed = deformation_info['speed'] - direction = deformation_info['direction'] - elapsed_time = self.world.globalClock.getFrameTime() - deformation_info['start_time'] - - # 应用波浪变形 - modified = False - for y in range(height_value): - for x in range(width): - # 计算波浪值 - projection = x * direction.getX() + y * direction.getY() - wave_value = math.sin((projection / wavelength + elapsed_time * speed) * 2 * math.pi) * amplitude - - # 获取当前高度 - current_height = heightfield.getRed(x, y) - - # 应用波浪变形 - new_height = current_height + wave_value - new_height = max(0.0, min(1.0, new_height)) - - # 设置新高度 - heightfield.setRed(x, y, new_height) - heightfield.setGreen(x, y, new_height) - heightfield.setBlue(x, y, new_height) - modified = True - - # 标记需要更新网格 - if modified: - terrain_info['_mesh_dirty'] = True - - except Exception as e: - print(f"应用波浪变形效果时出错: {e}") - - def _update_terrain_mesh(self, terrain_info): - """ - 更新地形网格 - """ - try: - if terrain_info.get('_mesh_dirty', False): - heightfield = terrain_info['heightfield'] - if not heightfield: - return - - terrain = terrain_info['terrain'] - terrain.setHeightfield(heightfield) - terrain.generate() - - # 重新创建碰撞体 - self._recreate_collision(terrain_info['node']) - - # 清除脏标记 - terrain_info['_mesh_dirty'] = False - - except Exception as e: - print(f"更新地形网格时出错: {e}") - - def _recreate_collision(self, terrain_node): - """ - 重新创建碰撞体(增强版) - """ - try: - from panda3d.core import BitMask32 - - # 移除旧的碰撞体 - for child in terrain_node.getChildren(): - if child.getName().startswith("terrain_collision_"): - child.removeNode() - - # 设置碰撞掩码 - terrain_node.setCollideMask(BitMask32.bit(2)) - - # 为子节点设置碰撞掩码 - for child in terrain_node.getChildren(): - child.setCollideMask(BitMask32.bit(2)) - - except Exception as e: - print(f"重新创建碰撞体时出错: {e}") - - def create_morph_target(self, terrain_info, target_name, target_heightfield, - weights=None, interpolation='linear'): - """ - 创建morph目标(增强版) - """ - try: - # 验证目标高度图 - if not target_heightfield: - print("无效的目标高度图") - return None - - # 创建morph目标信息 - morph_target = { - 'name': target_name, - 'terrain_info': terrain_info, - 'heightfield': target_heightfield, - 'weights': weights if weights is not None else {}, - 'interpolation': interpolation, - 'creation_time': self.world.globalClock.getFrameTime() - } - - # 保存morph目标 - terrain_node = terrain_info['node'] - if terrain_node not in self.morph_targets: - self.morph_targets[terrain_node] = {} - self.morph_targets[terrain_node][target_name] = morph_target - - print(f"创建morph目标: {target_name}") - return morph_target - - except Exception as e: - print(f"创建morph目标时出错: {e}") - return None - - def apply_morph_target(self, terrain_info, target_name, weight=1.0, - blend_mode='linear', preserve_volume=False): - """ - 应用morph目标(增强版) - """ - try: - terrain_node = terrain_info['node'] - if (terrain_node not in self.morph_targets or - target_name not in self.morph_targets[terrain_node]): - print(f"Morph目标不存在: {target_name}") - return False - - morph_target = self.morph_targets[terrain_node][target_name] - source_heightfield = terrain_info['heightfield'] - target_heightfield = morph_target['heightfield'] - - if not source_heightfield or not target_heightfield: - return False - - # 验证高度图尺寸 - if (source_heightfield.getXSize() != target_heightfield.getXSize() or - source_heightfield.getYSize() != target_heightfield.getYSize()): - print("源和目标高度图尺寸不匹配") - return False - - # 获取高度图尺寸 - width = source_heightfield.getXSize() - height_value = source_heightfield.getYSize() - - # 应用morph混合 - for y in range(height_value): - for x in range(width): - source_height = source_heightfield.getRed(x, y) - target_height = target_heightfield.getRed(x, y) - - # 根据混合模式计算混合高度 - if blend_mode == 'linear': - blended_height = source_height * (1.0 - weight) + target_height * weight - elif blend_mode == 'ease_in': - t = weight * weight - blended_height = source_height * (1.0 - t) + target_height * t - elif blend_mode == 'ease_out': - t = 1.0 - (1.0 - weight) * (1.0 - weight) - blended_height = source_height * (1.0 - t) + target_height * t - else: - blended_height = source_height * (1.0 - weight) + target_height * weight - - # 体积保持(简化实现) - if preserve_volume: - # 这里可以实现更复杂的体积保持算法 - pass - - # 限制高度范围 - blended_height = max(0.0, min(1.0, blended_height)) - - source_heightfield.setRed(x, y, blended_height) - source_heightfield.setGreen(x, y, blended_height) - source_heightfield.setBlue(x, y, blended_height) - - # 标记需要更新网格 - terrain_info['_mesh_dirty'] = True - - print(f"应用morph目标: {target_name} (权重: {weight})") - return True - - except Exception as e: - print(f"应用morph目标时出错: {e}") - return False - - def create_vertex_animation(self, terrain_info, vertex_indices, - target_positions, duration=2.0, - easing='linear', loop=False): - """ - 创建顶点动画(增强版) - """ - try: - # 创建顶点动画信息 - vertex_animation = { - 'terrain_info': terrain_info, - 'vertex_indices': vertex_indices, - 'target_positions': target_positions, - 'duration': duration, - 'easing': easing, - 'loop': loop, - 'start_time': self.world.globalClock.getFrameTime(), - 'active': True, - 'current_time': 0.0, - 'blend_weight': 1.0, - 'creation_time': self.world.globalClock.getFrameTime() - } - - # 验证数据 - if len(vertex_indices) != len(target_positions): - print("顶点索引和目标位置数量不匹配") - return None - - # 保存顶点动画 - terrain_node = terrain_info['node'] - if terrain_node not in self.vertex_animations: - self.vertex_animations[terrain_node] = [] - self.vertex_animations[terrain_node].append(vertex_animation) - - print("创建顶点动画") - return vertex_animation - - except Exception as e: - print(f"创建顶点动画时出错: {e}") - return None - - def update_vertex_animations(self, time_delta): - """ - 更新顶点动画(增强版) - """ - try: - if not self.animation_enabled: - return - - current_time = self.world.globalClock.getFrameTime() - scaled_time_delta = time_delta * self.animation_speed - - # 更新所有地形的顶点动画 - for terrain_node, animations in self.vertex_animations.items(): - for vertex_anim in animations[:]: # 使用切片复制 - if vertex_anim['active']: - # 更新当前时间 - vertex_anim['current_time'] += scaled_time_delta - - # 检查是否需要循环 - if vertex_anim['current_time'] > vertex_anim['duration']: - if vertex_anim['loop']: - vertex_anim['current_time'] = vertex_anim['current_time'] % vertex_anim['duration'] - else: - vertex_anim['active'] = False - continue - - # 计算进度 - progress = vertex_anim['current_time'] / vertex_anim['duration'] - eased_progress = self._apply_easing(progress, vertex_anim['easing']) - - # 应用顶点动画 - self._apply_vertex_animation(vertex_anim, eased_progress) - - except Exception as e: - print(f"更新顶点动画时出错: {e}") - - def _apply_vertex_animation(self, vertex_anim, progress): - """ - 应用顶点动画 - """ - try: - # 这里可以实现顶点的插值动画 - # 由于GeoMipTerrain的限制,这个功能需要特殊处理 - # 实际项目中可能需要直接操作顶点缓冲区 - pass - - except Exception as e: - print(f"应用顶点动画时出错: {e}") - - def create_skeletal_animation(self, terrain_info, bone_hierarchy, - keyframes, loop=False, speed=1.0): - """ - 创建骨骼动画 - """ - try: - # 创建骨骼动画信息 - skeletal_animation = { - 'terrain_info': terrain_info, - 'bone_hierarchy': bone_hierarchy, - 'keyframes': keyframes, - 'loop': loop, - 'speed': speed, - 'start_time': self.world.globalClock.getFrameTime(), - 'active': True, - 'current_time': 0.0, - 'creation_time': self.world.globalClock.getFrameTime() - } - - # 保存骨骼动画 - terrain_node = terrain_info['node'] - if terrain_node not in self.skeletal_animations: - self.skeletal_animations[terrain_node] = [] - self.skeletal_animations[terrain_node].append(skeletal_animation) - - print("创建骨骼动画") - return skeletal_animation - - except Exception as e: - print(f"创建骨骼动画时出错: {e}") - return None - - def update_skeletal_animations(self, time_delta): - """ - 更新骨骼动画 - """ - try: - if not self.animation_enabled: - return - - current_time = self.world.globalClock.getFrameTime() - scaled_time_delta = time_delta * self.animation_speed - - # 更新所有地形的骨骼动画 - for terrain_node, animations in self.skeletal_animations.items(): - for skeletal_anim in animations: - if skeletal_anim['active']: - # 更新当前时间 - skeletal_anim['current_time'] += scaled_time_delta * skeletal_anim['speed'] - - # 检查是否需要循环 - if skeletal_anim['current_time'] > self._get_animation_duration(skeletal_anim['keyframes']): - if skeletal_anim['loop']: - skeletal_anim['current_time'] = 0.0 - else: - skeletal_anim['active'] = False - continue - - # 应用骨骼动画 - self._apply_skeletal_animation(skeletal_anim, skeletal_anim['current_time']) - - except Exception as e: - print(f"更新骨骼动画时出错: {e}") - - def _get_animation_duration(self, keyframes): - """ - 获取动画持续时间 - """ - try: - if not keyframes: - return 0.0 - return max([kf['time'] for kf in keyframes]) - except Exception as e: - print(f"获取动画持续时间时出错: {e}") - return 0.0 - - def _apply_skeletal_animation(self, skeletal_anim, current_time): - """ - 应用骨骼动画 - """ - try: - # 这里可以实现骨骼动画的插值和应用 - # 实际项目中需要处理骨骼层次结构和蒙皮 - pass - - except Exception as e: - print(f"应用骨骼动画时出错: {e}") - - def set_animation_enabled(self, enabled): - """ - 启用或禁用动画系统 - """ - self.animation_enabled = enabled - print(f"地形动画系统已{'启用' if enabled else '禁用'}") - - def set_animation_speed(self, speed): - """ - 设置动画速度 - """ - self.animation_speed = max(0.0, speed) - print(f"动画速度设置为: {speed}x") - - def pause_all_animations(self): - """ - 暂停所有动画 - """ - self.time_manager['paused'] = True - print("所有动画已暂停") - - def resume_all_animations(self): - """ - 恢复所有动画 - """ - self.time_manager['paused'] = False - print("所有动画已恢复") - - def get_animation_stats(self): - """ - 获取动画统计信息(增强版) - """ - stats = { - 'enabled': self.animation_enabled, - 'animation_speed': self.animation_speed, - 'paused': self.time_manager['paused'], - 'global_time': self.time_manager['global_time'], - 'terrain_animations': 0, - 'deformation_effects': 0, - 'morph_targets': 0, - 'vertex_animations': 0, - 'skeletal_animations': 0, - 'active_animations': 0, - 'performance': { - 'last_update_time': self.performance_stats['last_update_time'], - 'avg_update_time': self.performance_stats['avg_update_time'], - 'update_count': self.performance_stats['update_count'] - } - } - - # 统计动画数量 - for terrain_node, animations in self.terrain_animations.items(): - stats['terrain_animations'] += len(animations) - for animation in animations.values(): - if animation['playing']: - stats['active_animations'] += 1 - - # 统计变形效果 - for terrain_node, deformations in self.deformation_effects.items(): - stats['deformation_effects'] += len(deformations) - - # 统计morph目标 - for terrain_node, morphs in self.morph_targets.items(): - stats['morph_targets'] += len(morphs) - - # 统计顶点动画 - for terrain_node, animations in self.vertex_animations.items(): - stats['vertex_animations'] += len(animations) - - # 统计骨骼动画 - for terrain_node, animations in self.skeletal_animations.items(): - stats['skeletal_animations'] += len(animations) - - return stats - - def save_animation_data(self, output_path): - """ - 保存动画数据(增强版) - """ - try: - # 收集动画数据 - animation_data = { - 'terrain_animations': {}, - 'morph_targets': {}, - 'settings': { - 'animation_speed': self.animation_speed, - 'time_scale': self.time_manager['animation_time_scale'] - } - } - - # 保存地形动画信息 - for terrain_node, animations in self.terrain_animations.items(): - node_name = terrain_node.getName() if not terrain_node.isEmpty() else "unknown" - animation_data['terrain_animations'][node_name] = {} - - for anim_name, anim_info in animations.items(): - animation_data['terrain_animations'][node_name][anim_name] = { - 'name': anim_info['name'], - 'duration': anim_info['duration'], - 'loop': anim_info['loop'], - 'speed': anim_info['speed'], - 'easing': anim_info['easing'], - 'keyframes': anim_info['keyframes'] - } - - # 保存morph目标信息 - for terrain_node, morphs in self.morph_targets.items(): - node_name = terrain_node.getName() if not terrain_node.isEmpty() else "unknown" - animation_data['morph_targets'][node_name] = {} - - for morph_name, morph_info in morphs.items(): - animation_data['morph_targets'][node_name][morph_name] = { - 'name': morph_info['name'], - 'interpolation': morph_info['interpolation'] - # 注意:高度图数据可能太大,不适合保存在JSON中 - } - - # 确保输出目录存在 - output_dir = os.path.dirname(output_path) - if not os.path.exists(output_dir): - os.makedirs(output_dir) - - # 写入JSON文件 - with open(output_path, 'w', encoding='utf-8') as f: - json.dump(animation_data, f, indent=2, ensure_ascii=False) - - print(f"动画数据保存成功: {output_path}") - return True - - except Exception as e: - print(f"保存动画数据时出错: {e}") - return False - - def load_animation_data(self, input_path): - """ - 加载动画数据(增强版) - """ - try: - if not os.path.exists(input_path): - print(f"动画数据文件不存在: {input_path}") - return False - - # 读取JSON文件 - with open(input_path, 'r', encoding='utf-8') as f: - animation_data = json.load(f) - - # 恢复设置 - if 'settings' in animation_data: - settings = animation_data['settings'] - if 'animation_speed' in settings: - self.animation_speed = settings['animation_speed'] - if 'time_scale' in settings: - self.time_manager['animation_time_scale'] = settings['time_scale'] - - # 加载地形动画 - # 注意:实际加载需要在地形创建后进行 - self._pending_animations = animation_data.get('terrain_animations', {}) - self._pending_morph_targets = animation_data.get('morph_targets', {}) - - print(f"动画数据加载成功: {input_path}") - return True - - except Exception as e: - print(f"加载动画数据时出错: {e}") - return False - - def apply_pending_animations(self, terrain_manager): - """ - 应用待处理的动画数据 - """ - try: - if hasattr(self, '_pending_animations'): - # 应用动画数据 - delattr(self, '_pending_animations') - - if hasattr(self, '_pending_morph_targets'): - # 应用morph目标数据 - delattr(self, '_pending_morph_targets') - - except Exception as e: - print(f"应用待处理动画数据时出错: {e}") - - def clear_all_animations(self): - """ - 清除所有动画(增强版) - """ - try: - # 停止所有动画 - for terrain_node, animations in self.terrain_animations.items(): - for animation_info in animations.values(): - animation_info['playing'] = False - - # 清空数据结构 - self.terrain_animations = {} - self.deformation_effects = {} - self.morph_targets = {} - self.vertex_animations = {} - self.skeletal_animations = {} - - # 清除挂起的数据 - if hasattr(self, '_pending_animations'): - delattr(self, '_pending_animations') - if hasattr(self, '_pending_morph_targets'): - delattr(self, '_pending_morph_targets') - - # 重置时间管理器 - self.time_manager['global_time'] = 0.0 - self.time_manager['paused'] = False - - # 重置性能统计 - self.performance_stats['update_count'] = 0 - self.performance_stats['avg_update_time'] = 0.0 - - print("清除所有动画") - return True - - except Exception as e: - print(f"清除所有动画时出错: {e}") - return False - - def create_explosion_deformation(self, terrain_info, center_pos, radius, strength, - duration=1.0, easing='ease_out'): - """ - 创建爆炸变形效果(增强版) - """ - try: - # 创建爆炸变形效果 - explosion_info = { - 'terrain_info': terrain_info, - 'center_pos': Vec3(center_pos), - 'radius': radius, - 'strength': strength, - 'duration': duration, - 'easing': easing, - 'start_time': self.world.globalClock.getFrameTime(), - 'active': True, - 'progress': 0.0, - 'type': 'explosion', - 'creation_time': self.world.globalClock.getFrameTime() - } - - # 保存爆炸效果 - terrain_node = terrain_info['node'] - if terrain_node not in self.deformation_effects: - self.deformation_effects[terrain_node] = [] - self.deformation_effects[terrain_node].append(explosion_info) - - print("创建爆炸变形效果") - return explosion_info - - except Exception as e: - print(f"创建爆炸变形效果时出错: {e}") - return None - - def create_ripple_effect(self, terrain_info, center_pos, radius, strength, - speed=1.0, duration=3.0): - """ - 创建涟漪效果 - """ - try: - # 创建涟漪效果信息 - ripple_info = { - 'terrain_info': terrain_info, - 'center_pos': Vec3(center_pos), - 'radius': radius, - 'strength': strength, - 'speed': speed, - 'duration': duration, - 'start_time': self.world.globalClock.getFrameTime(), - 'active': True, - 'progress': 0.0, - 'type': 'ripple', - 'creation_time': self.world.globalClock.getFrameTime() - } - - # 保存涟漪效果 - terrain_node = terrain_info['node'] - if terrain_node not in self.deformation_effects: - self.deformation_effects[terrain_node] = [] - self.deformation_effects[terrain_node].append(ripple_info) - - print("创建涟漪效果") - return ripple_info - - except Exception as e: - print(f"创建涟漪效果时出错: {e}") - return None - - def create_terrain_crack(self, terrain_info, start_pos, end_pos, width, depth, - duration=2.0, easing='ease_in'): - """ - 创建地形裂缝效果 - """ - try: - # 创建裂缝效果信息 - crack_info = { - 'terrain_info': terrain_info, - 'start_pos': Vec3(start_pos), - 'end_pos': Vec3(end_pos), - 'width': width, - 'depth': depth, - 'duration': duration, - 'easing': easing, - 'start_time': self.world.globalClock.getFrameTime(), - 'active': True, - 'progress': 0.0, - 'type': 'crack', - 'creation_time': self.world.globalClock.getFrameTime() - } - - # 保存裂缝效果 - terrain_node = terrain_info['node'] - if terrain_node not in self.deformation_effects: - self.deformation_effects[terrain_node] = [] - self.deformation_effects[terrain_node].append(crack_info) - - print("创建地形裂缝效果") - return crack_info - - except Exception as e: - print(f"创建地形裂缝效果时出错: {e}") +""" +地形动画和变形系统 +提供完整的地形动画、变形、morphing、顶点动画、骨骼动画等功能 +""" + +from panda3d.core import NodePath, Vec3, Point3, Mat4, LVector3f +from panda3d.core import Shader, ShaderAttrib, TransformState +from panda3d.core import Texture, TextureStage, PNMImage +from panda3d.core import LerpFunctionInterval, LerpPosInterval, LerpHprInterval +from panda3d.core import ClockObject, deg2Rad, rad2Deg +import math +import random +import json +import os +import numpy as np +from collections import deque + +class TerrainAnimationSystem: + """ + 地形动画和变形系统类 + 提供地形动画、变形、morphing、顶点动画、骨骼动画等功能 + """ + + def __init__(self, world): + self.world = world + self.terrain_animations = {} # 存储地形动画 + self.deformation_effects = {} # 存储变形效果 + self.morph_targets = {} # 存储morph目标 + self.vertex_animations = {} # 存储顶点动画 + self.skeletal_animations = {} # 存储骨骼动画 + self.animation_intervals = [] # 存储动画间隔 + self.animation_enabled = True + self.animation_speed = 1.0 # 动画速度倍数 + + # 性能监控 + self.performance_stats = { + 'last_update_time': 0.0, + 'update_count': 0, + 'avg_update_time': 0.0 + } + + # 初始化动画系统 + self._init_animation_system() + + def _init_animation_system(self): + """ + 初始化动画系统(增强版) + """ + try: + # 初始化动画缓存 + self.animation_cache = {} + + # 初始化变形缓存 + self.deformation_cache = {} + + # 初始化时间管理器 + self.time_manager = { + 'global_time': 0.0, + 'animation_time_scale': 1.0, + 'paused': False + } + + print("地形动画系统初始化完成") + + except Exception as e: + print(f"初始化动画系统时出错: {e}") + + def create_terrain_animation(self, terrain_info, animation_name, keyframes, + loop=False, speed=1.0, easing='linear'): + """ + 创建地形动画(增强版) + terrain_info: 地形信息 + animation_name: 动画名称 + keyframes: 关键帧数据 + loop: 是否循环 + speed: 动画速度 + easing: 缓动函数 + """ + try: + # 验证关键帧数据 + if not keyframes or len(keyframes) < 1: + print("关键帧数据无效") + return None + + # 排序关键帧 + sorted_keyframes = sorted(keyframes, key=lambda kf: kf['time']) + + # 创建动画信息 + animation_info = { + 'name': animation_name, + 'terrain_info': terrain_info, + 'keyframes': sorted_keyframes, + 'duration': sorted_keyframes[-1]['time'] if sorted_keyframes else 0.0, + 'loop': loop, + 'speed': speed, + 'easing': easing, + 'playing': False, + 'paused': False, + 'current_time': 0.0, + 'start_time': 0.0, + 'blend_weight': 1.0, + 'blend_mode': 'override', # override, add, multiply + 'animation_type': 'transform', # transform, vertex, material + 'creation_time': self.world.globalClock.getFrameTime() + } + + # 保存动画 + terrain_node = terrain_info['node'] + if terrain_node not in self.terrain_animations: + self.terrain_animations[terrain_node] = {} + self.terrain_animations[terrain_node][animation_name] = animation_info + + # 缓存动画数据以提高性能 + self._cache_animation_data(animation_info) + + print(f"创建地形动画: {animation_name} (持续时间: {animation_info['duration']}秒)") + return animation_info + + except Exception as e: + print(f"创建地形动画时出错: {e}") + return None + + def _cache_animation_data(self, animation_info): + """ + 缓存动画数据以提高性能 + """ + try: + cache_key = f"{id(animation_info['terrain_info'])}_{animation_info['name']}" + + # 预计算插值数据 + cached_data = { + 'position_curves': [], + 'rotation_curves': [], + 'scale_curves': [], + 'computed_times': [] + } + + keyframes = animation_info['keyframes'] + for i in range(len(keyframes) - 1): + kf1 = keyframes[i] + kf2 = keyframes[i + 1] + + # 计算时间段内的插值曲线 + if 'position' in kf1 and 'position' in kf2: + pos1 = Vec3(*kf1['position']) + pos2 = Vec3(*kf2['position']) + cached_data['position_curves'].append((pos1, pos2, kf1['time'], kf2['time'])) + + if 'rotation' in kf1 and 'rotation' in kf2: + rot1 = Vec3(*kf1['rotation']) + rot2 = Vec3(*kf2['rotation']) + cached_data['rotation_curves'].append((rot1, rot2, kf1['time'], kf2['time'])) + + if 'scale' in kf1 and 'scale' in kf2: + scale1 = Vec3(*kf1['scale']) + scale2 = Vec3(*kf2['scale']) + cached_data['scale_curves'].append((scale1, scale2, kf1['time'], kf2['time'])) + + self.animation_cache[cache_key] = cached_data + + except Exception as e: + print(f"缓存动画数据时出错: {e}") + + def play_terrain_animation(self, terrain_info, animation_name, loop=None, + fade_time=0.0, blend_weight=1.0): + """ + 播放地形动画(增强版) + """ + try: + terrain_node = terrain_info['node'] + if terrain_node not in self.terrain_animations: + print(f"地形没有动画: {animation_name}") + return False + + if animation_name not in self.terrain_animations[terrain_node]: + print(f"动画不存在: {animation_name}") + return False + + # 获取动画信息 + animation_info = self.terrain_animations[terrain_node][animation_name] + + # 更新动画属性 + if loop is not None: + animation_info['loop'] = loop + animation_info['playing'] = True + animation_info['paused'] = False + animation_info['start_time'] = self.world.globalClock.getFrameTime() + animation_info['current_time'] = 0.0 + animation_info['blend_weight'] = blend_weight + + # 处理淡入效果 + if fade_time > 0: + self._start_animation_fade_in(animation_info, fade_time) + + print(f"播放地形动画: {animation_name}") + return True + + except Exception as e: + print(f"播放地形动画时出错: {e}") + return False + + def _start_animation_fade_in(self, animation_info, fade_time): + """ + 开始动画淡入效果 + """ + try: + # 创建淡入定时器 + animation_info['fade_in'] = { + 'duration': fade_time, + 'start_time': self.world.globalClock.getFrameTime(), + 'active': True + } + + except Exception as e: + print(f"开始动画淡入时出错: {e}") + + def stop_terrain_animation(self, terrain_info, animation_name, fade_time=0.0): + """ + 停止地形动画(增强版) + """ + try: + terrain_node = terrain_info['node'] + if (terrain_node in self.terrain_animations and + animation_name in self.terrain_animations[terrain_node]): + animation_info = self.terrain_animations[terrain_node][animation_name] + + # 处理淡出效果 + if fade_time > 0: + self._start_animation_fade_out(animation_info, fade_time) + else: + animation_info['playing'] = False + animation_info['paused'] = False + + print(f"停止地形动画: {animation_name}") + return True + + except Exception as e: + print(f"停止地形动画时出错: {e}") + return False + + def _start_animation_fade_out(self, animation_info, fade_time): + """ + 开始动画淡出效果 + """ + try: + # 创建淡出定时器 + animation_info['fade_out'] = { + 'duration': fade_time, + 'start_time': self.world.globalClock.getFrameTime(), + 'active': True + } + + except Exception as e: + print(f"开始动画淡出时出错: {e}") + + def pause_terrain_animation(self, terrain_info, animation_name): + """ + 暂停地形动画 + """ + try: + terrain_node = terrain_info['node'] + if (terrain_node in self.terrain_animations and + animation_name in self.terrain_animations[terrain_node]): + animation_info = self.terrain_animations[terrain_node][animation_name] + animation_info['paused'] = True + print(f"暂停地形动画: {animation_name}") + return True + + except Exception as e: + print(f"暂停地形动画时出错: {e}") + return False + + def resume_terrain_animation(self, terrain_info, animation_name): + """ + 恢复地形动画 + """ + try: + terrain_node = terrain_info['node'] + if (terrain_node in self.terrain_animations and + animation_name in self.terrain_animations[terrain_node]): + animation_info = self.terrain_animations[terrain_node][animation_name] + animation_info['paused'] = False + print(f"恢复地形动画: {animation_name}") + return True + + except Exception as e: + print(f"恢复地形动画时出错: {e}") + return False + + def update_terrain_animations(self, time_delta): + """ + 更新地形动画(增强版) + """ + try: + if not self.animation_enabled or self.time_manager['paused']: + return + + # 性能监控开始 + start_time = self.world.globalClock.getRealTime() + + # 应用时间缩放 + scaled_time_delta = time_delta * self.time_manager['animation_time_scale'] * self.animation_speed + + current_time = self.world.globalClock.getFrameTime() + self.time_manager['global_time'] += scaled_time_delta + + # 更新每个地形的动画 + for terrain_node, animations in self.terrain_animations.items(): + for animation_name, animation_info in animations.items(): + if animation_info['playing'] and not animation_info['paused']: + # 更新当前时间 + animation_info['current_time'] += scaled_time_delta * animation_info['speed'] + + # 处理淡入淡出 + self._update_animation_fade(animation_info, current_time) + + # 检查是否需要循环 + if animation_info['current_time'] > animation_info['duration']: + if animation_info['loop']: + animation_info['current_time'] = animation_info['current_time'] % animation_info['duration'] + else: + animation_info['playing'] = False + continue + + # 应用动画 + self._apply_terrain_animation(animation_info, scaled_time_delta) + + # 更新性能统计 + end_time = self.world.globalClock.getRealTime() + update_time = end_time - start_time + self.performance_stats['last_update_time'] = update_time + self.performance_stats['update_count'] += 1 + + # 计算平均更新时间 + if self.performance_stats['update_count'] > 1: + alpha = 0.1 + self.performance_stats['avg_update_time'] = ( + alpha * update_time + + (1 - alpha) * self.performance_stats['avg_update_time'] + ) + else: + self.performance_stats['avg_update_time'] = update_time + + except Exception as e: + print(f"更新地形动画时出错: {e}") + + def _update_animation_fade(self, animation_info, current_time): + """ + 更新动画淡入淡出效果 + """ + try: + # 处理淡入 + if 'fade_in' in animation_info and animation_info['fade_in']['active']: + fade_in = animation_info['fade_in'] + elapsed = current_time - fade_in['start_time'] + if elapsed >= fade_in['duration']: + animation_info['blend_weight'] = 1.0 + animation_info['fade_in']['active'] = False + del animation_info['fade_in'] + else: + animation_info['blend_weight'] = elapsed / fade_in['duration'] + + # 处理淡出 + if 'fade_out' in animation_info and animation_info['fade_out']['active']: + fade_out = animation_info['fade_out'] + elapsed = current_time - fade_out['start_time'] + if elapsed >= fade_out['duration']: + animation_info['playing'] = False + animation_info['fade_out']['active'] = False + del animation_info['fade_out'] + else: + animation_info['blend_weight'] = 1.0 - (elapsed / fade_out['duration']) + + except Exception as e: + print(f"更新动画淡入淡出时出错: {e}") + + def _apply_terrain_animation(self, animation_info, time_delta): + """ + 应用地形动画(增强版) + """ + try: + current_time = animation_info['current_time'] + keyframes = animation_info['keyframes'] + blend_weight = animation_info['blend_weight'] + + if len(keyframes) < 1: + return + + # 如果只有一个关键帧,直接应用 + if len(keyframes) == 1: + kf = keyframes[0] + self._apply_keyframe_transform(animation_info['terrain_info'], kf, blend_weight) + return + + # 找到当前时间对应的两个关键帧 + prev_kf = keyframes[0] + next_kf = keyframes[-1] + + for i in range(len(keyframes) - 1): + if keyframes[i]['time'] <= current_time <= keyframes[i + 1]['time']: + prev_kf = keyframes[i] + next_kf = keyframes[i + 1] + break + + # 计算插值因子 + time_diff = next_kf['time'] - prev_kf['time'] + if time_diff > 0: + t = (current_time - prev_kf['time']) / time_diff + + # 应用缓动函数 + t = self._apply_easing(t, animation_info['easing']) + else: + t = 0.0 + + # 插值计算变换 + self._interpolate_and_apply_transform( + animation_info['terrain_info'], + prev_kf, next_kf, t, blend_weight + ) + + except Exception as e: + print(f"应用地形动画时出错: {e}") + + def _apply_easing(self, t, easing_type): + """ + 应用缓动函数 + """ + try: + if easing_type == 'linear': + return t + elif easing_type == 'ease_in': + return t * t + elif easing_type == 'ease_out': + return 1 - (1 - t) * (1 - t) + elif easing_type == 'ease_in_out': + return 3 * t * t - 2 * t * t * t + elif easing_type == 'bounce': + if t < 1/2.75: + return 7.5625 * t * t + elif t < 2/2.75: + t -= 1.5/2.75 + return 7.5625 * t * t + 0.75 + elif t < 2.5/2.75: + t -= 2.25/2.75 + return 7.5625 * t * t + 0.9375 + else: + t -= 2.625/2.75 + return 7.5625 * t * t + 0.984375 + else: + return t + + except Exception as e: + print(f"应用缓动函数时出错: {e}") + return t + + def _apply_keyframe_transform(self, terrain_info, keyframe, blend_weight): + """ + 应用单个关键帧变换 + """ + try: + terrain_node = terrain_info['node'] + + # 应用位置变换 + if 'position' in keyframe: + target_pos = Vec3(*keyframe['position']) + if blend_weight < 1.0: + current_pos = terrain_node.getPos() + final_pos = current_pos + (target_pos - current_pos) * blend_weight + else: + final_pos = target_pos + terrain_node.setPos(final_pos) + + # 应用旋转变换 + if 'rotation' in keyframe: + target_rot = Vec3(*keyframe['rotation']) + if blend_weight < 1.0: + current_rot = terrain_node.getHpr() + final_rot = current_rot + (target_rot - current_rot) * blend_weight + else: + final_rot = target_rot + terrain_node.setHpr(final_rot) + + # 应用缩放变换 + if 'scale' in keyframe: + target_scale = Vec3(*keyframe['scale']) + if blend_weight < 1.0: + current_scale = terrain_node.getScale() + final_scale = current_scale + (target_scale - current_scale) * blend_weight + else: + final_scale = target_scale + terrain_node.setScale(final_scale) + + except Exception as e: + print(f"应用关键帧变换时出错: {e}") + + def _interpolate_and_apply_transform(self, terrain_info, prev_kf, next_kf, t, blend_weight): + """ + 插值并应用变换 + """ + try: + terrain_node = terrain_info['node'] + + # 插值位置 + if 'position' in prev_kf and 'position' in next_kf: + prev_pos = Vec3(*prev_kf['position']) + next_pos = Vec3(*next_kf['position']) + interpolated_pos = prev_pos + (next_pos - prev_pos) * t + + if blend_weight < 1.0: + current_pos = terrain_node.getPos() + final_pos = current_pos + (interpolated_pos - current_pos) * blend_weight + else: + final_pos = interpolated_pos + + terrain_node.setPos(final_pos) + + # 插值旋转 + if 'rotation' in prev_kf and 'rotation' in next_kf: + prev_rot = Vec3(*prev_kf['rotation']) + next_rot = Vec3(*next_kf['rotation']) + interpolated_rot = prev_rot + (next_rot - prev_rot) * t + + if blend_weight < 1.0: + current_rot = terrain_node.getHpr() + final_rot = current_rot + (interpolated_rot - current_rot) * blend_weight + else: + final_rot = interpolated_rot + + terrain_node.setHpr(final_rot) + + # 插值缩放 + if 'scale' in prev_kf and 'scale' in next_kf: + prev_scale = Vec3(*prev_kf['scale']) + next_scale = Vec3(*next_kf['scale']) + interpolated_scale = prev_scale + (next_scale - prev_scale) * t + + if blend_weight < 1.0: + current_scale = terrain_node.getScale() + final_scale = current_scale + (interpolated_scale - current_scale) * blend_weight + else: + final_scale = interpolated_scale + + terrain_node.setScale(final_scale) + + except Exception as e: + print(f"插值并应用变换时出错: {e}") + + def create_height_deformation(self, terrain_info, deformation_name, + center_pos, radius, strength, duration=2.0, + easing='ease_out', falloff='linear'): + """ + 创建高度变形效果(增强版) + """ + try: + # 创建变形效果信息 + deformation_info = { + 'name': deformation_name, + 'terrain_info': terrain_info, + 'center_pos': Vec3(center_pos), + 'radius': radius, + 'strength': strength, + 'duration': duration, + 'easing': easing, + 'falloff': falloff, + 'start_time': self.world.globalClock.getFrameTime(), + 'active': True, + 'progress': 0.0, + 'type': 'height', + 'creation_time': self.world.globalClock.getFrameTime() + } + + # 保存变形效果 + terrain_node = terrain_info['node'] + if terrain_node not in self.deformation_effects: + self.deformation_effects[terrain_node] = [] + self.deformation_effects[terrain_node].append(deformation_info) + + print(f"创建高度变形效果: {deformation_name}") + return deformation_info + + except Exception as e: + print(f"创建高度变形效果时出错: {e}") + return None + + def create_noise_deformation(self, terrain_info, deformation_name, + scale=1.0, strength=0.1, duration=5.0, + octaves=3, persistence=0.5, lacunarity=2.0): + """ + 创建噪声变形效果 + """ + try: + # 创建噪声变形效果信息 + deformation_info = { + 'name': deformation_name, + 'terrain_info': terrain_info, + 'scale': scale, + 'strength': strength, + 'duration': duration, + 'octaves': octaves, + 'persistence': persistence, + 'lacunarity': lacunarity, + 'start_time': self.world.globalClock.getFrameTime(), + 'active': True, + 'progress': 0.0, + 'type': 'noise', + 'seed': random.randint(0, 10000), + 'creation_time': self.world.globalClock.getFrameTime() + } + + # 保存变形效果 + terrain_node = terrain_info['node'] + if terrain_node not in self.deformation_effects: + self.deformation_effects[terrain_node] = [] + self.deformation_effects[terrain_node].append(deformation_info) + + print(f"创建噪声变形效果: {deformation_name}") + return deformation_info + + except Exception as e: + print(f"创建噪声变形效果时出错: {e}") + return None + + def create_wave_deformation(self, terrain_info, deformation_name, + amplitude=0.1, wavelength=10.0, speed=1.0, + direction=Vec3(1, 0, 0), duration=float('inf')): + """ + 创建波浪变形效果 + """ + try: + # 创建波浪变形效果信息 + deformation_info = { + 'name': deformation_name, + 'terrain_info': terrain_info, + 'amplitude': amplitude, + 'wavelength': wavelength, + 'speed': speed, + 'direction': direction.normalized(), + 'duration': duration, + 'start_time': self.world.globalClock.getFrameTime(), + 'active': True, + 'progress': 0.0, + 'type': 'wave', + 'creation_time': self.world.globalClock.getFrameTime() + } + + # 保存变形效果 + terrain_node = terrain_info['node'] + if terrain_node not in self.deformation_effects: + self.deformation_effects[terrain_node] = [] + self.deformation_effects[terrain_node].append(deformation_info) + + print(f"创建波浪变形效果: {deformation_name}") + return deformation_info + + except Exception as e: + print(f"创建波浪变形效果时出错: {e}") + return None + + def update_deformation_effects(self, time_delta): + """ + 更新变形效果(增强版) + """ + try: + if not self.animation_enabled: + return + + current_time = self.world.globalClock.getFrameTime() + scaled_time_delta = time_delta * self.animation_speed + + # 更新每个地形的变形效果 + for terrain_node, deformations in self.deformation_effects.items(): + for deformation_info in deformations[:]: # 使用切片复制列表 + if deformation_info['active']: + # 更新进度 + elapsed_time = current_time - deformation_info['start_time'] + progress = min(1.0, elapsed_time / deformation_info['duration']) if deformation_info['duration'] > 0 else 0.0 + deformation_info['progress'] = progress + + # 检查是否超时 + if deformation_info['duration'] > 0 and elapsed_time > deformation_info['duration']: + deformation_info['active'] = False + continue + + # 根据变形类型应用效果 + if deformation_info['type'] == 'height': + self._apply_height_deformation(deformation_info, progress, scaled_time_delta) + elif deformation_info['type'] == 'noise': + self._apply_noise_deformation(deformation_info, progress, scaled_time_delta) + elif deformation_info['type'] == 'wave': + self._apply_wave_deformation(deformation_info, progress, scaled_time_delta) + + # 更新地形网格 + if deformation_info['active']: + self._update_terrain_mesh(deformation_info['terrain_info']) + + except Exception as e: + print(f"更新变形效果时出错: {e}") + + def _apply_height_deformation(self, deformation_info, progress, time_delta): + """ + 应用高度变形效果(增强版) + """ + try: + terrain_info = deformation_info['terrain_info'] + heightfield = terrain_info['heightfield'] + + if not heightfield: + return + + terrain_node = terrain_info['node'] + terrain_pos = terrain_node.getPos() + terrain_scale = terrain_node.getScale() + + # 获取高度图尺寸 + width = heightfield.getXSize() + height = heightfield.getYSize() + + # 计算中心点在高度图中的位置 + center_offset = (width - 1) / 2 + center_x = int((deformation_info['center_pos'].getX() - terrain_pos.getX()) / terrain_scale.getX() + center_offset) + center_y = int((deformation_info['center_pos'].getY() - terrain_pos.getY()) / terrain_scale.getY() + center_offset) + + # 计算变形半径(在高度图坐标系中) + radius_pixels = int(deformation_info['radius'] / max(terrain_scale.getX(), terrain_scale.getY())) + + # 计算变形强度(应用缓动和进度) + eased_progress = self._apply_easing(progress, deformation_info['easing']) + current_strength = deformation_info['strength'] * eased_progress + + # 应用变形 + modified = False + for dx in range(-radius_pixels, radius_pixels + 1): + for dy in range(-radius_pixels, radius_pixels + 1): + # 计算距离 + distance = math.sqrt(dx*dx + dy*dy) + if distance <= radius_pixels: + # 计算衰减因子 + if deformation_info['falloff'] == 'linear': + weight = 1.0 - (distance / radius_pixels) + elif deformation_info['falloff'] == 'smooth': + weight = math.cos((distance / radius_pixels) * math.pi / 2) + elif deformation_info['falloff'] == 'quadratic': + weight = 1.0 - (distance / radius_pixels) ** 2 + else: + weight = 1.0 - (distance / radius_pixels) + + # 计算目标点 + target_x = center_x + dx + target_y = center_y + dy + + # 检查边界 + if 0 <= target_x < width and 0 <= target_y < height: + # 获取当前高度 + current_height = heightfield.getRed(target_x, target_y) + + # 应用变形 + new_height = current_height + current_strength * weight * 0.01 + new_height = max(0.0, min(1.0, new_height)) + + # 设置新高度 + heightfield.setRed(target_x, target_y, new_height) + heightfield.setGreen(target_x, target_y, new_height) + heightfield.setBlue(target_x, target_y, new_height) + modified = True + + # 标记需要更新网格 + if modified: + terrain_info['_mesh_dirty'] = True + + except Exception as e: + print(f"应用高度变形效果时出错: {e}") + + def _apply_noise_deformation(self, deformation_info, progress, time_delta): + """ + 应用噪声变形效果 + """ + try: + terrain_info = deformation_info['terrain_info'] + heightfield = terrain_info['heightfield'] + + if not heightfield: + return + + # 获取高度图尺寸 + width = heightfield.getXSize() + height_value = heightfield.getYSize() + + # 计算噪声参数 + scale = deformation_info['scale'] + strength = deformation_info['strength'] * progress + seed = deformation_info['seed'] + + # 应用噪声变形 + modified = False + for y in range(height_value): + for x in range(width): + # 生成噪声值 + nx = x / width * scale + ny = y / height_value * scale + + # 简单的噪声实现(实际项目中可以使用更复杂的噪声算法) + noise_value = self._fbm_noise(nx, ny, seed, + deformation_info['octaves'], + deformation_info['persistence'], + deformation_info['lacunarity']) + + # 获取当前高度 + current_height = heightfield.getRed(x, y) + + # 应用噪声变形 + new_height = current_height + noise_value * strength + new_height = max(0.0, min(1.0, new_height)) + + # 设置新高度 + heightfield.setRed(x, y, new_height) + heightfield.setGreen(x, y, new_height) + heightfield.setBlue(x, y, new_height) + modified = True + + # 标记需要更新网格 + if modified: + terrain_info['_mesh_dirty'] = True + + except Exception as e: + print(f"应用噪声变形效果时出错: {e}") + + def _fbm_noise(self, x, y, seed, octaves, persistence, lacunarity): + """ + 分形布朗运动噪声 + """ + try: + value = 0.0 + amplitude = 1.0 + frequency = 1.0 + max_value = 0.0 + + for i in range(octaves): + # 简化的噪声函数(实际项目中可以使用Perlin噪声或Simplex噪声) + sample_x = (x * frequency + seed) % 1000 + sample_y = (y * frequency + seed) % 1000 + noise = math.sin(sample_x) * math.cos(sample_y) + + value += noise * amplitude + max_value += amplitude + + amplitude *= persistence + frequency *= lacunarity + + return value / max_value if max_value > 0 else 0.0 + + except Exception as e: + print(f"计算FMB噪声时出错: {e}") + return 0.0 + + def _apply_wave_deformation(self, deformation_info, progress, time_delta): + """ + 应用波浪变形效果 + """ + try: + terrain_info = deformation_info['terrain_info'] + heightfield = terrain_info['heightfield'] + + if not heightfield: + return + + # 获取高度图尺寸 + width = heightfield.getXSize() + height_value = heightfield.getYSize() + + # 计算波浪参数 + amplitude = deformation_info['amplitude'] + wavelength = deformation_info['wavelength'] + speed = deformation_info['speed'] + direction = deformation_info['direction'] + elapsed_time = self.world.globalClock.getFrameTime() - deformation_info['start_time'] + + # 应用波浪变形 + modified = False + for y in range(height_value): + for x in range(width): + # 计算波浪值 + projection = x * direction.getX() + y * direction.getY() + wave_value = math.sin((projection / wavelength + elapsed_time * speed) * 2 * math.pi) * amplitude + + # 获取当前高度 + current_height = heightfield.getRed(x, y) + + # 应用波浪变形 + new_height = current_height + wave_value + new_height = max(0.0, min(1.0, new_height)) + + # 设置新高度 + heightfield.setRed(x, y, new_height) + heightfield.setGreen(x, y, new_height) + heightfield.setBlue(x, y, new_height) + modified = True + + # 标记需要更新网格 + if modified: + terrain_info['_mesh_dirty'] = True + + except Exception as e: + print(f"应用波浪变形效果时出错: {e}") + + def _update_terrain_mesh(self, terrain_info): + """ + 更新地形网格 + """ + try: + if terrain_info.get('_mesh_dirty', False): + heightfield = terrain_info['heightfield'] + if not heightfield: + return + + terrain = terrain_info['terrain'] + terrain.setHeightfield(heightfield) + terrain.generate() + + # 重新创建碰撞体 + self._recreate_collision(terrain_info['node']) + + # 清除脏标记 + terrain_info['_mesh_dirty'] = False + + except Exception as e: + print(f"更新地形网格时出错: {e}") + + def _recreate_collision(self, terrain_node): + """ + 重新创建碰撞体(增强版) + """ + try: + from panda3d.core import BitMask32 + + # 移除旧的碰撞体 + for child in terrain_node.getChildren(): + if child.getName().startswith("terrain_collision_"): + child.removeNode() + + # 设置碰撞掩码 + terrain_node.setCollideMask(BitMask32.bit(2)) + + # 为子节点设置碰撞掩码 + for child in terrain_node.getChildren(): + child.setCollideMask(BitMask32.bit(2)) + + except Exception as e: + print(f"重新创建碰撞体时出错: {e}") + + def create_morph_target(self, terrain_info, target_name, target_heightfield, + weights=None, interpolation='linear'): + """ + 创建morph目标(增强版) + """ + try: + # 验证目标高度图 + if not target_heightfield: + print("无效的目标高度图") + return None + + # 创建morph目标信息 + morph_target = { + 'name': target_name, + 'terrain_info': terrain_info, + 'heightfield': target_heightfield, + 'weights': weights if weights is not None else {}, + 'interpolation': interpolation, + 'creation_time': self.world.globalClock.getFrameTime() + } + + # 保存morph目标 + terrain_node = terrain_info['node'] + if terrain_node not in self.morph_targets: + self.morph_targets[terrain_node] = {} + self.morph_targets[terrain_node][target_name] = morph_target + + print(f"创建morph目标: {target_name}") + return morph_target + + except Exception as e: + print(f"创建morph目标时出错: {e}") + return None + + def apply_morph_target(self, terrain_info, target_name, weight=1.0, + blend_mode='linear', preserve_volume=False): + """ + 应用morph目标(增强版) + """ + try: + terrain_node = terrain_info['node'] + if (terrain_node not in self.morph_targets or + target_name not in self.morph_targets[terrain_node]): + print(f"Morph目标不存在: {target_name}") + return False + + morph_target = self.morph_targets[terrain_node][target_name] + source_heightfield = terrain_info['heightfield'] + target_heightfield = morph_target['heightfield'] + + if not source_heightfield or not target_heightfield: + return False + + # 验证高度图尺寸 + if (source_heightfield.getXSize() != target_heightfield.getXSize() or + source_heightfield.getYSize() != target_heightfield.getYSize()): + print("源和目标高度图尺寸不匹配") + return False + + # 获取高度图尺寸 + width = source_heightfield.getXSize() + height_value = source_heightfield.getYSize() + + # 应用morph混合 + for y in range(height_value): + for x in range(width): + source_height = source_heightfield.getRed(x, y) + target_height = target_heightfield.getRed(x, y) + + # 根据混合模式计算混合高度 + if blend_mode == 'linear': + blended_height = source_height * (1.0 - weight) + target_height * weight + elif blend_mode == 'ease_in': + t = weight * weight + blended_height = source_height * (1.0 - t) + target_height * t + elif blend_mode == 'ease_out': + t = 1.0 - (1.0 - weight) * (1.0 - weight) + blended_height = source_height * (1.0 - t) + target_height * t + else: + blended_height = source_height * (1.0 - weight) + target_height * weight + + # 体积保持(简化实现) + if preserve_volume: + # 这里可以实现更复杂的体积保持算法 + pass + + # 限制高度范围 + blended_height = max(0.0, min(1.0, blended_height)) + + source_heightfield.setRed(x, y, blended_height) + source_heightfield.setGreen(x, y, blended_height) + source_heightfield.setBlue(x, y, blended_height) + + # 标记需要更新网格 + terrain_info['_mesh_dirty'] = True + + print(f"应用morph目标: {target_name} (权重: {weight})") + return True + + except Exception as e: + print(f"应用morph目标时出错: {e}") + return False + + def create_vertex_animation(self, terrain_info, vertex_indices, + target_positions, duration=2.0, + easing='linear', loop=False): + """ + 创建顶点动画(增强版) + """ + try: + # 创建顶点动画信息 + vertex_animation = { + 'terrain_info': terrain_info, + 'vertex_indices': vertex_indices, + 'target_positions': target_positions, + 'duration': duration, + 'easing': easing, + 'loop': loop, + 'start_time': self.world.globalClock.getFrameTime(), + 'active': True, + 'current_time': 0.0, + 'blend_weight': 1.0, + 'creation_time': self.world.globalClock.getFrameTime() + } + + # 验证数据 + if len(vertex_indices) != len(target_positions): + print("顶点索引和目标位置数量不匹配") + return None + + # 保存顶点动画 + terrain_node = terrain_info['node'] + if terrain_node not in self.vertex_animations: + self.vertex_animations[terrain_node] = [] + self.vertex_animations[terrain_node].append(vertex_animation) + + print("创建顶点动画") + return vertex_animation + + except Exception as e: + print(f"创建顶点动画时出错: {e}") + return None + + def update_vertex_animations(self, time_delta): + """ + 更新顶点动画(增强版) + """ + try: + if not self.animation_enabled: + return + + current_time = self.world.globalClock.getFrameTime() + scaled_time_delta = time_delta * self.animation_speed + + # 更新所有地形的顶点动画 + for terrain_node, animations in self.vertex_animations.items(): + for vertex_anim in animations[:]: # 使用切片复制 + if vertex_anim['active']: + # 更新当前时间 + vertex_anim['current_time'] += scaled_time_delta + + # 检查是否需要循环 + if vertex_anim['current_time'] > vertex_anim['duration']: + if vertex_anim['loop']: + vertex_anim['current_time'] = vertex_anim['current_time'] % vertex_anim['duration'] + else: + vertex_anim['active'] = False + continue + + # 计算进度 + progress = vertex_anim['current_time'] / vertex_anim['duration'] + eased_progress = self._apply_easing(progress, vertex_anim['easing']) + + # 应用顶点动画 + self._apply_vertex_animation(vertex_anim, eased_progress) + + except Exception as e: + print(f"更新顶点动画时出错: {e}") + + def _apply_vertex_animation(self, vertex_anim, progress): + """ + 应用顶点动画 + """ + try: + # 这里可以实现顶点的插值动画 + # 由于GeoMipTerrain的限制,这个功能需要特殊处理 + # 实际项目中可能需要直接操作顶点缓冲区 + pass + + except Exception as e: + print(f"应用顶点动画时出错: {e}") + + def create_skeletal_animation(self, terrain_info, bone_hierarchy, + keyframes, loop=False, speed=1.0): + """ + 创建骨骼动画 + """ + try: + # 创建骨骼动画信息 + skeletal_animation = { + 'terrain_info': terrain_info, + 'bone_hierarchy': bone_hierarchy, + 'keyframes': keyframes, + 'loop': loop, + 'speed': speed, + 'start_time': self.world.globalClock.getFrameTime(), + 'active': True, + 'current_time': 0.0, + 'creation_time': self.world.globalClock.getFrameTime() + } + + # 保存骨骼动画 + terrain_node = terrain_info['node'] + if terrain_node not in self.skeletal_animations: + self.skeletal_animations[terrain_node] = [] + self.skeletal_animations[terrain_node].append(skeletal_animation) + + print("创建骨骼动画") + return skeletal_animation + + except Exception as e: + print(f"创建骨骼动画时出错: {e}") + return None + + def update_skeletal_animations(self, time_delta): + """ + 更新骨骼动画 + """ + try: + if not self.animation_enabled: + return + + current_time = self.world.globalClock.getFrameTime() + scaled_time_delta = time_delta * self.animation_speed + + # 更新所有地形的骨骼动画 + for terrain_node, animations in self.skeletal_animations.items(): + for skeletal_anim in animations: + if skeletal_anim['active']: + # 更新当前时间 + skeletal_anim['current_time'] += scaled_time_delta * skeletal_anim['speed'] + + # 检查是否需要循环 + if skeletal_anim['current_time'] > self._get_animation_duration(skeletal_anim['keyframes']): + if skeletal_anim['loop']: + skeletal_anim['current_time'] = 0.0 + else: + skeletal_anim['active'] = False + continue + + # 应用骨骼动画 + self._apply_skeletal_animation(skeletal_anim, skeletal_anim['current_time']) + + except Exception as e: + print(f"更新骨骼动画时出错: {e}") + + def _get_animation_duration(self, keyframes): + """ + 获取动画持续时间 + """ + try: + if not keyframes: + return 0.0 + return max([kf['time'] for kf in keyframes]) + except Exception as e: + print(f"获取动画持续时间时出错: {e}") + return 0.0 + + def _apply_skeletal_animation(self, skeletal_anim, current_time): + """ + 应用骨骼动画 + """ + try: + # 这里可以实现骨骼动画的插值和应用 + # 实际项目中需要处理骨骼层次结构和蒙皮 + pass + + except Exception as e: + print(f"应用骨骼动画时出错: {e}") + + def set_animation_enabled(self, enabled): + """ + 启用或禁用动画系统 + """ + self.animation_enabled = enabled + print(f"地形动画系统已{'启用' if enabled else '禁用'}") + + def set_animation_speed(self, speed): + """ + 设置动画速度 + """ + self.animation_speed = max(0.0, speed) + print(f"动画速度设置为: {speed}x") + + def pause_all_animations(self): + """ + 暂停所有动画 + """ + self.time_manager['paused'] = True + print("所有动画已暂停") + + def resume_all_animations(self): + """ + 恢复所有动画 + """ + self.time_manager['paused'] = False + print("所有动画已恢复") + + def get_animation_stats(self): + """ + 获取动画统计信息(增强版) + """ + stats = { + 'enabled': self.animation_enabled, + 'animation_speed': self.animation_speed, + 'paused': self.time_manager['paused'], + 'global_time': self.time_manager['global_time'], + 'terrain_animations': 0, + 'deformation_effects': 0, + 'morph_targets': 0, + 'vertex_animations': 0, + 'skeletal_animations': 0, + 'active_animations': 0, + 'performance': { + 'last_update_time': self.performance_stats['last_update_time'], + 'avg_update_time': self.performance_stats['avg_update_time'], + 'update_count': self.performance_stats['update_count'] + } + } + + # 统计动画数量 + for terrain_node, animations in self.terrain_animations.items(): + stats['terrain_animations'] += len(animations) + for animation in animations.values(): + if animation['playing']: + stats['active_animations'] += 1 + + # 统计变形效果 + for terrain_node, deformations in self.deformation_effects.items(): + stats['deformation_effects'] += len(deformations) + + # 统计morph目标 + for terrain_node, morphs in self.morph_targets.items(): + stats['morph_targets'] += len(morphs) + + # 统计顶点动画 + for terrain_node, animations in self.vertex_animations.items(): + stats['vertex_animations'] += len(animations) + + # 统计骨骼动画 + for terrain_node, animations in self.skeletal_animations.items(): + stats['skeletal_animations'] += len(animations) + + return stats + + def save_animation_data(self, output_path): + """ + 保存动画数据(增强版) + """ + try: + # 收集动画数据 + animation_data = { + 'terrain_animations': {}, + 'morph_targets': {}, + 'settings': { + 'animation_speed': self.animation_speed, + 'time_scale': self.time_manager['animation_time_scale'] + } + } + + # 保存地形动画信息 + for terrain_node, animations in self.terrain_animations.items(): + node_name = terrain_node.getName() if not terrain_node.isEmpty() else "unknown" + animation_data['terrain_animations'][node_name] = {} + + for anim_name, anim_info in animations.items(): + animation_data['terrain_animations'][node_name][anim_name] = { + 'name': anim_info['name'], + 'duration': anim_info['duration'], + 'loop': anim_info['loop'], + 'speed': anim_info['speed'], + 'easing': anim_info['easing'], + 'keyframes': anim_info['keyframes'] + } + + # 保存morph目标信息 + for terrain_node, morphs in self.morph_targets.items(): + node_name = terrain_node.getName() if not terrain_node.isEmpty() else "unknown" + animation_data['morph_targets'][node_name] = {} + + for morph_name, morph_info in morphs.items(): + animation_data['morph_targets'][node_name][morph_name] = { + 'name': morph_info['name'], + 'interpolation': morph_info['interpolation'] + # 注意:高度图数据可能太大,不适合保存在JSON中 + } + + # 确保输出目录存在 + output_dir = os.path.dirname(output_path) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + # 写入JSON文件 + with open(output_path, 'w', encoding='utf-8') as f: + json.dump(animation_data, f, indent=2, ensure_ascii=False) + + print(f"动画数据保存成功: {output_path}") + return True + + except Exception as e: + print(f"保存动画数据时出错: {e}") + return False + + def load_animation_data(self, input_path): + """ + 加载动画数据(增强版) + """ + try: + if not os.path.exists(input_path): + print(f"动画数据文件不存在: {input_path}") + return False + + # 读取JSON文件 + with open(input_path, 'r', encoding='utf-8') as f: + animation_data = json.load(f) + + # 恢复设置 + if 'settings' in animation_data: + settings = animation_data['settings'] + if 'animation_speed' in settings: + self.animation_speed = settings['animation_speed'] + if 'time_scale' in settings: + self.time_manager['animation_time_scale'] = settings['time_scale'] + + # 加载地形动画 + # 注意:实际加载需要在地形创建后进行 + self._pending_animations = animation_data.get('terrain_animations', {}) + self._pending_morph_targets = animation_data.get('morph_targets', {}) + + print(f"动画数据加载成功: {input_path}") + return True + + except Exception as e: + print(f"加载动画数据时出错: {e}") + return False + + def apply_pending_animations(self, terrain_manager): + """ + 应用待处理的动画数据 + """ + try: + if hasattr(self, '_pending_animations'): + # 应用动画数据 + delattr(self, '_pending_animations') + + if hasattr(self, '_pending_morph_targets'): + # 应用morph目标数据 + delattr(self, '_pending_morph_targets') + + except Exception as e: + print(f"应用待处理动画数据时出错: {e}") + + def clear_all_animations(self): + """ + 清除所有动画(增强版) + """ + try: + # 停止所有动画 + for terrain_node, animations in self.terrain_animations.items(): + for animation_info in animations.values(): + animation_info['playing'] = False + + # 清空数据结构 + self.terrain_animations = {} + self.deformation_effects = {} + self.morph_targets = {} + self.vertex_animations = {} + self.skeletal_animations = {} + + # 清除挂起的数据 + if hasattr(self, '_pending_animations'): + delattr(self, '_pending_animations') + if hasattr(self, '_pending_morph_targets'): + delattr(self, '_pending_morph_targets') + + # 重置时间管理器 + self.time_manager['global_time'] = 0.0 + self.time_manager['paused'] = False + + # 重置性能统计 + self.performance_stats['update_count'] = 0 + self.performance_stats['avg_update_time'] = 0.0 + + print("清除所有动画") + return True + + except Exception as e: + print(f"清除所有动画时出错: {e}") + return False + + def create_explosion_deformation(self, terrain_info, center_pos, radius, strength, + duration=1.0, easing='ease_out'): + """ + 创建爆炸变形效果(增强版) + """ + try: + # 创建爆炸变形效果 + explosion_info = { + 'terrain_info': terrain_info, + 'center_pos': Vec3(center_pos), + 'radius': radius, + 'strength': strength, + 'duration': duration, + 'easing': easing, + 'start_time': self.world.globalClock.getFrameTime(), + 'active': True, + 'progress': 0.0, + 'type': 'explosion', + 'creation_time': self.world.globalClock.getFrameTime() + } + + # 保存爆炸效果 + terrain_node = terrain_info['node'] + if terrain_node not in self.deformation_effects: + self.deformation_effects[terrain_node] = [] + self.deformation_effects[terrain_node].append(explosion_info) + + print("创建爆炸变形效果") + return explosion_info + + except Exception as e: + print(f"创建爆炸变形效果时出错: {e}") + return None + + def create_ripple_effect(self, terrain_info, center_pos, radius, strength, + speed=1.0, duration=3.0): + """ + 创建涟漪效果 + """ + try: + # 创建涟漪效果信息 + ripple_info = { + 'terrain_info': terrain_info, + 'center_pos': Vec3(center_pos), + 'radius': radius, + 'strength': strength, + 'speed': speed, + 'duration': duration, + 'start_time': self.world.globalClock.getFrameTime(), + 'active': True, + 'progress': 0.0, + 'type': 'ripple', + 'creation_time': self.world.globalClock.getFrameTime() + } + + # 保存涟漪效果 + terrain_node = terrain_info['node'] + if terrain_node not in self.deformation_effects: + self.deformation_effects[terrain_node] = [] + self.deformation_effects[terrain_node].append(ripple_info) + + print("创建涟漪效果") + return ripple_info + + except Exception as e: + print(f"创建涟漪效果时出错: {e}") + return None + + def create_terrain_crack(self, terrain_info, start_pos, end_pos, width, depth, + duration=2.0, easing='ease_in'): + """ + 创建地形裂缝效果 + """ + try: + # 创建裂缝效果信息 + crack_info = { + 'terrain_info': terrain_info, + 'start_pos': Vec3(start_pos), + 'end_pos': Vec3(end_pos), + 'width': width, + 'depth': depth, + 'duration': duration, + 'easing': easing, + 'start_time': self.world.globalClock.getFrameTime(), + 'active': True, + 'progress': 0.0, + 'type': 'crack', + 'creation_time': self.world.globalClock.getFrameTime() + } + + # 保存裂缝效果 + terrain_node = terrain_info['node'] + if terrain_node not in self.deformation_effects: + self.deformation_effects[terrain_node] = [] + self.deformation_effects[terrain_node].append(crack_info) + + print("创建地形裂缝效果") + return crack_info + + except Exception as e: + print(f"创建地形裂缝效果时出错: {e}") return None \ No newline at end of file diff --git a/plugins/user/terrain_editor/environment/__init__.py b/plugins/user/terrain_editor/environment/__init__.py index 6fa4b889..6e7c4391 100644 --- a/plugins/user/terrain_editor/environment/__init__.py +++ b/plugins/user/terrain_editor/environment/__init__.py @@ -1,3 +1,3 @@ -""" -地形环境系统初始化文件 +""" +地形环境系统初始化文件 """ \ No newline at end of file diff --git a/plugins/user/terrain_editor/environment/environment_system.py b/plugins/user/terrain_editor/environment/environment_system.py index d1f70899..528ff274 100644 --- a/plugins/user/terrain_editor/environment/environment_system.py +++ b/plugins/user/terrain_editor/environment/environment_system.py @@ -1,1558 +1,1558 @@ -""" -地形天气和环境系统 -提供完整的天气模拟、环境效果、光照控制、动态天空等功能 -""" - -from panda3d.core import NodePath, Vec3, Point3, Vec4, LVector3f -from panda3d.core import AmbientLight, DirectionalLight, PointLight, Spotlight -from panda3d.core import Fog, Shader, ShaderAttrib, PerspectiveLens -from panda3d.core import Texture, TextureStage, CardMaker -from panda3d.core import PNMImage, Filename, ClockObject -from panda3d.core import deg2Rad, rad2Deg -import math -import random -import json -import os -from collections import deque - -class EnvironmentSystem: - """ - 地形天气和环境系统类 - 提供完整的天气模拟、环境效果、光照控制、动态天空等功能 - """ - - def __init__(self, world): - self.world = world - self.weather_conditions = {} # 存储天气条件 - self.environment_effects = {} # 存储环境效果 - self.lighting_system = {} # 存储光照系统 - self.particle_systems = [] # 存储粒子系统 - self.weather_enabled = True - self.current_weather = 'sunny' - self.time_of_day = 12.0 # 24小时制 - self.current_season = 'summer' - self.weather_transition = None # 天气过渡状态 - self.sky_dome = None # 天空穹顶 - self.cloud_layer = None # 云层 - self.wind_vector = Vec3(1, 0, 0) # 风向量 - self.wind_strength = 0.1 # 风力强度 - - # 性能监控 - self.performance_stats = { - 'last_update_time': 0.0, - 'update_count': 0, - 'avg_update_time': 0.0 - } - - # 初始化环境系统 - self._init_environment_system() - - def _init_environment_system(self): - """ - 初始化环境系统 - """ - try: - # 定义默认天气条件 - self._define_default_weather() - - # 初始化光照系统 - self._init_lighting_system() - - # 初始化环境效果 - self._init_environment_effects() - - # 创建动态天空 - self.create_dynamic_sky('dome') - - print("环境系统初始化完成") - - except Exception as e: - print(f"初始化环境系统时出错: {e}") - - def _define_default_weather(self): - """ - 定义默认天气条件(增强版) - """ - self.weather_conditions = { - 'sunny': { - 'name': '晴天', - 'ambient_light': (0.3, 0.3, 0.3, 1.0), - 'directional_light': (0.8, 0.8, 0.8, 1.0), - 'light_color_temp': 5500, # 色温(K) - 'fog_color': (0.5, 0.5, 0.5, 1.0), - 'fog_density': 0.0, - 'particle_effects': [], - 'wind_strength': 0.1, - 'temperature': 25.0, - 'humidity': 30.0, - 'visibility': 20000.0, # 能见度(米) - 'cloud_coverage': 0.1, # 云覆盖率 - 'precipitation': 0.0, # 降水量 - 'sky_tint': (1.0, 1.0, 1.0, 1.0), # 天空着色 - 'sun_intensity': 1.0, - 'moon_intensity': 0.0 - }, - 'cloudy': { - 'name': '多云', - 'ambient_light': (0.2, 0.2, 0.2, 1.0), - 'directional_light': (0.5, 0.5, 0.5, 1.0), - 'light_color_temp': 4500, - 'fog_color': (0.7, 0.7, 0.7, 1.0), - 'fog_density': 0.001, - 'particle_effects': ['clouds'], - 'wind_strength': 0.3, - 'temperature': 20.0, - 'humidity': 60.0, - 'visibility': 5000.0, - 'cloud_coverage': 0.7, - 'precipitation': 0.0, - 'sky_tint': (0.8, 0.8, 0.8, 1.0), - 'sun_intensity': 0.6, - 'moon_intensity': 0.1 - }, - 'rainy': { - 'name': '雨天', - 'ambient_light': (0.1, 0.1, 0.1, 1.0), - 'directional_light': (0.3, 0.3, 0.3, 1.0), - 'light_color_temp': 6500, - 'fog_color': (0.8, 0.8, 0.8, 1.0), - 'fog_density': 0.002, - 'particle_effects': ['rain', 'clouds'], - 'wind_strength': 0.5, - 'temperature': 15.0, - 'humidity': 90.0, - 'visibility': 1000.0, - 'cloud_coverage': 0.9, - 'precipitation': 5.0, - 'sky_tint': (0.7, 0.7, 0.7, 1.0), - 'sun_intensity': 0.3, - 'moon_intensity': 0.2 - }, - 'stormy': { - 'name': '暴风雨', - 'ambient_light': (0.05, 0.05, 0.05, 1.0), - 'directional_light': (0.2, 0.2, 0.2, 1.0), - 'light_color_temp': 7000, - 'fog_color': (0.9, 0.9, 0.9, 1.0), - 'fog_density': 0.003, - 'particle_effects': ['rain', 'lightning', 'clouds'], - 'wind_strength': 1.0, - 'temperature': 10.0, - 'humidity': 95.0, - 'visibility': 500.0, - 'cloud_coverage': 1.0, - 'precipitation': 20.0, - 'sky_tint': (0.5, 0.5, 0.5, 1.0), - 'sun_intensity': 0.1, - 'moon_intensity': 0.3 - }, - 'snowy': { - 'name': '雪天', - 'ambient_light': (0.2, 0.2, 0.2, 1.0), - 'directional_light': (0.4, 0.4, 0.4, 1.0), - 'light_color_temp': 5000, - 'fog_color': (0.9, 0.9, 0.9, 1.0), - 'fog_density': 0.001, - 'particle_effects': ['snow', 'clouds'], - 'wind_strength': 0.4, - 'temperature': -5.0, - 'humidity': 80.0, - 'visibility': 2000.0, - 'cloud_coverage': 0.8, - 'precipitation': 3.0, - 'sky_tint': (0.9, 0.9, 1.0, 1.0), - 'sun_intensity': 0.4, - 'moon_intensity': 0.4 - }, - 'foggy': { - 'name': '雾天', - 'ambient_light': (0.15, 0.15, 0.15, 1.0), - 'directional_light': (0.2, 0.2, 0.2, 1.0), - 'light_color_temp': 3000, - 'fog_color': (0.95, 0.95, 0.95, 1.0), - 'fog_density': 0.005, - 'particle_effects': ['mist'], - 'wind_strength': 0.2, - 'temperature': 12.0, - 'humidity': 98.0, - 'visibility': 200.0, - 'cloud_coverage': 0.3, - 'precipitation': 0.1, - 'sky_tint': (0.9, 0.9, 0.9, 1.0), - 'sun_intensity': 0.2, - 'moon_intensity': 0.1 - } - } - - def _init_lighting_system(self): - """ - 初始化光照系统(增强版) - """ - try: - # 创建环境光 - ambient_light = AmbientLight('ambient_light') - ambient_light.setColor(Vec4(0.3, 0.3, 0.3, 1.0)) - self.ambient_light_np = self.world.render.attachNewNode(ambient_light) - self.world.render.setLight(self.ambient_light_np) - - # 创建方向光(太阳光) - directional_light = DirectionalLight('directional_light') - directional_light.setColor(Vec4(0.8, 0.8, 0.8, 1.0)) - directional_light.setShadowCaster(True, 2048, 2048) - directional_light.setCameraMask(0x00000001) # 设置相机掩码 - self.directional_light_np = self.world.render.attachNewNode(directional_light) - self.directional_light_np.setHpr(45, -45, 0) - self.world.render.setLight(self.directional_light_np) - - # 创建月光 - moon_light = DirectionalLight('moon_light') - moon_light.setColor(Vec4(0.1, 0.1, 0.2, 1.0)) - self.moon_light_np = self.world.render.attachNewNode(moon_light) - self.moon_light_np.setHpr(225, -30, 0) - # 初始时禁用月光 - self.world.render.clearLight(self.moon_light_np) - - # 保存光照节点 - self.lighting_system = { - 'ambient': { - 'node': ambient_light, - 'nodepath': self.ambient_light_np - }, - 'directional': { - 'node': directional_light, - 'nodepath': self.directional_light_np - }, - 'moon': { - 'node': moon_light, - 'nodepath': self.moon_light_np, - 'enabled': False - } - } - - except Exception as e: - print(f"初始化光照系统时出错: {e}") - - def _init_environment_effects(self): - """ - 初始化环境效果(增强版) - """ - try: - # 初始化雾效 - self.fog = Fog('environment_fog') - self.fog.setColor(0.5, 0.5, 0.5, 1.0) - self.fog.setExpDensity(0.0) - self.world.render.setFog(self.fog) - - # 初始化风效 - self.wind_vector = Vec3(1, 0, 0) - self.wind_strength = 0.1 - - # 初始化粒子系统容器 - self.particle_containers = { - 'rain': [], - 'snow': [], - 'clouds': [], - 'lightning': [], - 'mist': [] - } - - except Exception as e: - print(f"初始化环境效果时出错: {e}") - - def set_weather(self, weather_type, transition_time=5.0): - """ - 设置天气(增强版) - """ - try: - if weather_type not in self.weather_conditions: - print(f"未知的天气类型: {weather_type}") - return False - - # 如果已经有过渡效果,取消它 - if self.weather_transition: - self.weather_transition = None - - # 如果需要平滑过渡 - if transition_time > 0 and self.current_weather != weather_type: - self.weather_transition = { - 'from': self.current_weather, - 'to': weather_type, - 'start_time': self.world.globalClock.getFrameTime(), - 'duration': transition_time, - 'progress': 0.0 - } - print(f"开始天气过渡: {self.current_weather} -> {weather_type}") - else: - # 直接切换天气 - self._apply_weather_immediately(weather_type) - - return True - - except Exception as e: - print(f"设置天气时出错: {e}") - return False - - def _apply_weather_immediately(self, weather_type): - """ - 立即应用天气 - """ - try: - weather = self.weather_conditions[weather_type] - self.current_weather = weather_type - - # 更新环境光 - if 'ambient' in self.lighting_system: - light_node = self.lighting_system['ambient']['node'] - light_node.setColor(Vec4(*weather['ambient_light'])) - - # 更新方向光 - if 'directional' in self.lighting_system: - light_node = self.lighting_system['directional']['node'] - light_node.setColor(Vec4(*weather['directional_light'])) - - # 更新雾效 - fog_color = weather['fog_color'] - self.fog.setColor(fog_color[0], fog_color[1], fog_color[2], fog_color[3]) - self.fog.setExpDensity(weather['fog_density']) - - # 更新风效 - self.wind_strength = weather['wind_strength'] - - # 更新粒子效果 - self._update_particle_effects(weather['particle_effects']) - - # 应用天气着色器 - self._apply_weather_shader(weather_type) - - # 更新天空 - self._update_sky_for_weather(weather_type) - - print(f"设置天气为: {weather['name']}") - - except Exception as e: - print(f"立即应用天气时出错: {e}") - - def _update_particle_effects(self, effects_list): - """ - 更新粒子效果(增强版) - """ - try: - # 清除现有粒子效果 - self._clear_particle_effects() - - # 创建新的粒子效果 - for effect in effects_list: - if effect == 'rain': - self._create_rain_effect() - elif effect == 'snow': - self._create_snow_effect() - elif effect == 'clouds': - self._create_cloud_effect() - elif effect == 'lightning': - self._create_lightning_effect() - elif effect == 'mist': - self._create_mist_effect() - - except Exception as e: - print(f"更新粒子效果时出错: {e}") - - def _clear_particle_effects(self): - """ - 清除粒子效果(增强版) - """ - try: - # 移除所有粒子系统节点 - for effect_type, particles in self.particle_containers.items(): - for particle in particles: - if particle['node'] and not particle['node'].isEmpty(): - particle['node'].removeNode() - self.particle_containers[effect_type] = [] - - # 清空旧的粒子系统列表 - self.particle_systems = [] - - except Exception as e: - print(f"清除粒子效果时出错: {e}") - - def _create_rain_effect(self): - """ - 创建雨天效果(增强版) - """ - try: - # 创建雨滴粒子系统 - rain_particles = [] - - # 创建多个雨滴发射器以提高性能 - for i in range(5): - # 创建雨滴节点 - cm = CardMaker(f'rain_drop_{i}') - cm.setFrame(-0.01, 0.01, -0.1, 0.1) - rain_node = self.world.render.attachNewNode(cm.generate()) - rain_node.setBin("fixed", 40) # 确保在最上层 - rain_node.setDepthWrite(False) - rain_node.setDepthTest(False) - - # 设置雨滴纹理(简化版本) - # 实际项目中应该加载真实的雨滴纹理 - rain_node.setColor(0.5, 0.5, 1.0, 0.7) - - # 创建粒子信息 - particle_info = { - 'type': 'rain', - 'node': rain_node, - 'position': Vec3(random.uniform(-50, 50), random.uniform(-50, 50), 20), - 'velocity': Vec3(random.uniform(-1, 1), random.uniform(-1, 1), -20.0), - 'lifetime': random.uniform(1.0, 3.0), - 'age': 0.0, - 'emitter_id': i - } - - rain_node.setPos(particle_info['position']) - rain_particles.append(particle_info) - - self.particle_containers['rain'].extend(rain_particles) - self.particle_systems.extend(rain_particles) - - print("创建雨天效果") - return rain_particles - - except Exception as e: - print(f"创建雨天效果时出错: {e}") - return [] - - def _create_snow_effect(self): - """ - 创建雪天效果(增强版) - """ - try: - # 创建雪花粒子系统 - snow_particles = [] - - # 创建多个雪花发射器 - for i in range(8): - # 创建雪花节点 - cm = CardMaker(f'snow_flake_{i}') - size = random.uniform(0.05, 0.15) - cm.setFrame(-size, size, -size, size) - snow_node = self.world.render.attachNewNode(cm.generate()) - snow_node.setBin("fixed", 35) - snow_node.setDepthWrite(False) - snow_node.setDepthTest(False) - - # 设置雪花颜色 - snow_node.setColor(1.0, 1.0, 1.0, 0.8) - - # 创建粒子信息 - particle_info = { - 'type': 'snow', - 'node': snow_node, - 'position': Vec3(random.uniform(-100, 100), random.uniform(-100, 100), 30), - 'velocity': Vec3(random.uniform(-2, 2), random.uniform(-2, 2), random.uniform(-5, -2)), - 'lifetime': random.uniform(5.0, 10.0), - 'age': 0.0, - 'rotation_speed': random.uniform(-10, 10), - 'emitter_id': i - } - - snow_node.setPos(particle_info['position']) - snow_particles.append(particle_info) - - self.particle_containers['snow'].extend(snow_particles) - self.particle_systems.extend(snow_particles) - - print("创建雪天效果") - return snow_particles - - except Exception as e: - print(f"创建雪天效果时出错: {e}") - return [] - - def _create_cloud_effect(self): - """ - 创建云效果(增强版) - """ - try: - # 创建云朵粒子系统 - cloud_particles = [] - - # 创建多个云朵 - for i in range(10): - # 创建云朵节点 - cm = CardMaker(f'cloud_{i}') - size = random.uniform(10, 30) - cm.setFrame(-size, size, -size/2, size/2) - cloud_node = self.world.render.attachNewNode(cm.generate()) - cloud_node.setBin("fixed", 30) - cloud_node.setDepthWrite(False) - cloud_node.setDepthTest(False) - - # 设置云朵颜色和透明度 - cloud_node.setColor(1.0, 1.0, 1.0, random.uniform(0.3, 0.7)) - - # 创建粒子信息 - particle_info = { - 'type': 'clouds', - 'node': cloud_node, - 'position': Vec3(random.uniform(-200, 200), random.uniform(-200, 200), random.uniform(50, 80)), - 'velocity': Vec3(random.uniform(-0.5, 0.5), random.uniform(-0.5, 0.5), 0), - 'lifetime': float('inf'), # 永久存在 - 'age': 0.0, - 'scale': size, - 'emitter_id': i - } - - cloud_node.setPos(particle_info['position']) - cloud_particles.append(particle_info) - - self.particle_containers['clouds'].extend(cloud_particles) - self.particle_systems.extend(cloud_particles) - - print("创建云效果") - return cloud_particles - - except Exception as e: - print(f"创建云效果时出错: {e}") - return [] - - def _create_lightning_effect(self): - """ - 创建闪电效果(增强版) - """ - try: - # 创建闪电效果 - lightning_effects = [] - - # 创建闪电节点 - cm = CardMaker('lightning') - cm.setFrame(-100, 100, -100, 100) - lightning_node = self.world.render.attachNewNode(cm.generate()) - lightning_node.setBin("fixed", 50) - lightning_node.setDepthWrite(False) - lightning_node.setDepthTest(False) - - # 设置初始不可见 - lightning_node.hide() - - # 创建闪电信息 - lightning_info = { - 'type': 'lightning', - 'node': lightning_node, - 'position': Vec3(0, 0, 100), - 'lifetime': 0.1, # 闪电持续时间很短 - 'age': 0.0, - 'next_strike_time': random.uniform(5, 15), # 下次闪电时间 - 'intensity': 2.0 - } - - lightning_node.setPos(lightning_info['position']) - lightning_effects.append(lightning_info) - - self.particle_containers['lightning'].extend(lightning_effects) - self.particle_systems.extend(lightning_effects) - - print("创建闪电效果") - return lightning_effects - - except Exception as e: - print(f"创建闪电效果时出错: {e}") - return [] - - def _create_mist_effect(self): - """ - 创建雾气效果(增强版) - """ - try: - # 创建雾气粒子系统 - mist_particles = [] - - # 创建多个雾气发射器 - for i in range(3): - # 创建雾气节点 - cm = CardMaker(f'mist_{i}') - size = random.uniform(20, 50) - cm.setFrame(-size, size, -size/2, size/2) - mist_node = self.world.render.attachNewNode(cm.generate()) - mist_node.setBin("fixed", 25) - mist_node.setDepthWrite(False) - mist_node.setDepthTest(False) - - # 设置雾气颜色 - mist_node.setColor(0.9, 0.9, 0.9, random.uniform(0.2, 0.4)) - - # 创建粒子信息 - particle_info = { - 'type': 'mist', - 'node': mist_node, - 'position': Vec3(random.uniform(-100, 100), random.uniform(-100, 100), random.uniform(0, 5)), - 'velocity': Vec3(random.uniform(-0.2, 0.2), random.uniform(-0.2, 0.2), 0), - 'lifetime': float('inf'), - 'age': 0.0, - 'scale': size, - 'emitter_id': i - } - - mist_node.setPos(particle_info['position']) - mist_particles.append(particle_info) - - self.particle_containers['mist'].extend(mist_particles) - self.particle_systems.extend(mist_particles) - - print("创建雾气效果") - return mist_particles - - except Exception as e: - print(f"创建雾气效果时出错: {e}") - return [] - - def _apply_weather_shader(self, weather_type): - """ - 应用天气着色器(增强版) - """ - try: - # 如果使用RenderPipeline,应用天气效果 - if hasattr(self.world, 'render_pipeline'): - try: - # 根据天气类型选择着色器 - if weather_type == 'rainy': - effect_file = "effects/rain_weather.yaml" - elif weather_type == 'stormy': - effect_file = "effects/storm_weather.yaml" - elif weather_type == 'snowy': - effect_file = "effects/snow_weather.yaml" - elif weather_type == 'foggy': - effect_file = "effects/fog_weather.yaml" - else: - effect_file = "effects/default_weather.yaml" - - self.world.render_pipeline.set_effect( - self.world.render, - effect_file, - { - "weather_type": weather_type, - "wind_strength": self.wind_strength, - "fog_density": self.weather_conditions[weather_type]['fog_density'], - "time_of_day": self.time_of_day - }, - 5 # 排序值 - ) - - except Exception as e: - print(f"应用天气着色器失败: {e}") - - except Exception as e: - print(f"应用天气着色器时出错: {e}") - - def update_environment(self, time_delta): - """ - 更新环境系统(增强版) - """ - try: - if not self.weather_enabled: - return - - # 性能监控开始 - start_time = self.world.globalClock.getRealTime() - - # 更新天气过渡 - self._update_weather_transition(time_delta) - - # 更新粒子效果 - self._update_particles(time_delta) - - # 更新风效 - self._update_wind(time_delta) - - # 更新光照 - self._update_lighting(time_delta) - - # 更新天空 - self._update_sky(time_delta) - - # 更新性能统计 - end_time = self.world.globalClock.getRealTime() - update_time = end_time - start_time - self.performance_stats['last_update_time'] = update_time - self.performance_stats['update_count'] += 1 - - # 计算平均更新时间 - if self.performance_stats['update_count'] > 1: - alpha = 0.1 - self.performance_stats['avg_update_time'] = ( - alpha * update_time + - (1 - alpha) * self.performance_stats['avg_update_time'] - ) - else: - self.performance_stats['avg_update_time'] = update_time - - except Exception as e: - print(f"更新环境系统时出错: {e}") - - def _update_weather_transition(self, time_delta): - """ - 更新天气过渡效果 - """ - try: - if not self.weather_transition: - return - - # 更新过渡进度 - current_time = self.world.globalClock.getFrameTime() - elapsed = current_time - self.weather_transition['start_time'] - progress = min(1.0, elapsed / self.weather_transition['duration']) - - self.weather_transition['progress'] = progress - - # 获取起始和目标天气 - from_weather = self.weather_conditions[self.weather_transition['from']] - to_weather = self.weather_conditions[self.weather_transition['to']] - - # 插值计算当前天气参数 - if progress >= 1.0: - # 过渡完成,应用最终天气 - self._apply_weather_immediately(self.weather_transition['to']) - self.weather_transition = None - else: - # 过渡中,插值计算参数 - self._interpolate_weather_parameters(from_weather, to_weather, progress) - - except Exception as e: - print(f"更新天气过渡时出错: {e}") - - def _interpolate_weather_parameters(self, from_weather, to_weather, progress): - """ - 插值计算天气参数 - """ - try: - # 线性插值函数 - def lerp(a, b, t): - if isinstance(a, (list, tuple)) and len(a) == len(b): - return [a[i] + (b[i] - a[i]) * t for i in range(len(a))] - return a + (b - a) * t - - # 插值环境光 - if 'ambient' in self.lighting_system: - ambient_from = from_weather['ambient_light'] - ambient_to = to_weather['ambient_light'] - ambient_current = lerp(ambient_from, ambient_to, progress) - light_node = self.lighting_system['ambient']['node'] - light_node.setColor(Vec4(*ambient_current)) - - # 插值方向光 - if 'directional' in self.lighting_system: - directional_from = from_weather['directional_light'] - directional_to = to_weather['directional_light'] - directional_current = lerp(directional_from, directional_to, progress) - light_node = self.lighting_system['directional']['node'] - light_node.setColor(Vec4(*directional_current)) - - # 插值雾效 - fog_from = from_weather['fog_color'] - fog_to = to_weather['fog_color'] - fog_color_current = lerp(fog_from, fog_to, progress) - self.fog.setColor(fog_color_current[0], fog_color_current[1], fog_color_current[2], fog_color_current[3]) - - fog_density_from = from_weather['fog_density'] - fog_density_to = to_weather['fog_density'] - fog_density_current = lerp(fog_density_from, fog_density_to, progress) - self.fog.setExpDensity(fog_density_current) - - # 插值风力 - wind_from = from_weather['wind_strength'] - wind_to = to_weather['wind_strength'] - self.wind_strength = lerp(wind_from, wind_to, progress) - - except Exception as e: - print(f"插值天气参数时出错: {e}") - - def _update_particles(self, time_delta): - """ - 更新粒子效果(增强版) - """ - try: - current_time = self.world.globalClock.getFrameTime() - - # 更新所有粒子系统 - for particle in self.particle_systems[:]: # 使用切片复制 - # 更新粒子年龄 - particle['age'] += time_delta - - # 检查生命周期 - if particle['lifetime'] != float('inf') and particle['age'] > particle['lifetime']: - # 移除过期粒子 - if particle['node'] and not particle['node'].isEmpty(): - particle['node'].removeNode() - if particle in self.particle_systems: - self.particle_systems.remove(particle) - continue - - # 根据粒子类型更新 - if particle['type'] == 'rain': - self._update_rain_particle(particle, time_delta, current_time) - elif particle['type'] == 'snow': - self._update_snow_particle(particle, time_delta, current_time) - elif particle['type'] == 'clouds': - self._update_cloud_particle(particle, time_delta, current_time) - elif particle['type'] == 'lightning': - self._update_lightning_particle(particle, time_delta, current_time) - elif particle['type'] == 'mist': - self._update_mist_particle(particle, time_delta, current_time) - - except Exception as e: - print(f"更新粒子效果时出错: {e}") - - def _update_rain_particle(self, particle, time_delta, current_time): - """ - 更新雨滴粒子 - """ - try: - # 更新位置 - particle['position'] += particle['velocity'] * time_delta - particle['node'].setPos(particle['position']) - - # 如果雨滴落到地面以下,重置位置 - if particle['position'].getZ() < 0: - particle['position'].setZ(20) # 重置到高处 - particle['position'].setX(random.uniform(-50, 50)) - particle['position'].setY(random.uniform(-50, 50)) - particle['node'].setPos(particle['position']) - - except Exception as e: - print(f"更新雨滴粒子时出错: {e}") - - def _update_snow_particle(self, particle, time_delta, current_time): - """ - 更新雪花粒子 - """ - try: - # 更新位置 - particle['position'] += particle['velocity'] * time_delta - particle['node'].setPos(particle['position']) - - # 添加摆动效果 - sway = math.sin(current_time * 2 + particle['emitter_id']) * 0.5 - particle['position'].setX(particle['position'].getX() + sway * time_delta) - particle['node'].setPos(particle['position']) - - # 更新旋转 - particle['node'].setHpr(particle['node'].getHpr() + Vec3(0, 0, particle['rotation_speed'] * time_delta)) - - # 如果雪花落到地面以下,重置位置 - if particle['position'].getZ() < 0: - particle['position'].setZ(30) # 重置到高处 - particle['position'].setX(random.uniform(-100, 100)) - particle['position'].setY(random.uniform(-100, 100)) - particle['node'].setPos(particle['position']) - - except Exception as e: - print(f"更新雪花粒子时出错: {e}") - - def _update_cloud_particle(self, particle, time_delta, current_time): - """ - 更新云朵粒子 - """ - try: - # 更新位置 - particle['position'] += particle['velocity'] * time_delta - particle['node'].setPos(particle['position']) - - # 添加缓慢变化效果 - if random.random() < 0.01: # 1%概率改变速度 - particle['velocity'] = Vec3( - random.uniform(-0.5, 0.5), - random.uniform(-0.5, 0.5), - 0 - ) - - # 如果云朵移出范围,重置位置 - if (abs(particle['position'].getX()) > 300 or - abs(particle['position'].getY()) > 300): - particle['position'].setX(random.uniform(-200, 200)) - particle['position'].setY(random.uniform(-200, 200)) - particle['node'].setPos(particle['position']) - - except Exception as e: - print(f"更新云朵粒子时出错: {e}") - - def _update_lightning_particle(self, particle, time_delta, current_time): - """ - 更新闪电粒子 - """ - try: - # 更新计时器 - particle['next_strike_time'] -= time_delta - - # 检查是否触发闪电 - if particle['next_strike_time'] <= 0: - # 显示闪电 - particle['node'].show() - particle['age'] = 0 # 重置年龄 - - # 随机改变闪电位置 - particle['position'] = Vec3( - random.uniform(-100, 100), - random.uniform(-100, 100), - 100 - ) - particle['node'].setPos(particle['position']) - - # 设置下次闪电时间 - particle['next_strike_time'] = random.uniform(5, 15) - - # 增强环境光模拟闪电光照 - if 'ambient' in self.lighting_system: - light_node = self.lighting_system['ambient']['node'] - light_node.setColor(Vec4(0.8, 0.8, 0.8, 1.0)) - else: - # 检查是否需要隐藏闪电 - if particle['age'] > particle['lifetime']: - particle['node'].hide() - # 恢复正常环境光 - current_weather = self.weather_conditions[self.current_weather] - if 'ambient' in self.lighting_system: - light_node = self.lighting_system['ambient']['node'] - light_node.setColor(Vec4(*current_weather['ambient_light'])) - - except Exception as e: - print(f"更新闪电粒子时出错: {e}") - - def _update_mist_particle(self, particle, time_delta, current_time): - """ - 更新雾气粒子 - """ - try: - # 更新位置 - particle['position'] += particle['velocity'] * time_delta - particle['node'].setPos(particle['position']) - - # 缓慢改变透明度 - alpha = particle['node'].getColor().getW() - alpha += random.uniform(-0.01, 0.01) - alpha = max(0.1, min(0.5, alpha)) # 限制在合理范围内 - particle['node'].setColor( - particle['node'].getColor().getX(), - particle['node'].getColor().getY(), - particle['node'].getColor().getZ(), - alpha - ) - - # 如果雾气移出范围,重置位置 - if (abs(particle['position'].getX()) > 200 or - abs(particle['position'].getY()) > 200): - particle['position'].setX(random.uniform(-100, 100)) - particle['position'].setY(random.uniform(-100, 100)) - particle['node'].setPos(particle['position']) - - except Exception as e: - print(f"更新雾气粒子时出错: {e}") - - def _update_wind(self, time_delta): - """ - 更新风效(增强版) - """ - try: - # 可以实现风向和风力的变化 - # 例如随机改变风向或模拟阵风 - current_weather = self.weather_conditions[self.current_weather] - - # 模拟阵风效果 - gust_factor = 1.0 + math.sin(self.world.globalClock.getFrameTime() * 0.5) * 0.3 - self.wind_vector = Vec3( - math.cos(self.world.globalClock.getFrameTime() * 0.1) * gust_factor, - math.sin(self.world.globalClock.getFrameTime() * 0.1) * gust_factor, - 0 - ) * current_weather['wind_strength'] - - except Exception as e: - print(f"更新风效时出错: {e}") - - def _update_lighting(self, time_delta): - """ - 更新光照(增强版) - """ - try: - # 更新时间 - self.time_of_day += time_delta / 3600.0 # 假设1秒对应1小时 - if self.time_of_day >= 24.0: - self.time_of_day -= 24.0 - - # 根据时间调整太阳和月亮位置 - self._update_celestial_positions() - - # 根据时间调整光照颜色 - self._update_lighting_colors() - - except Exception as e: - print(f"更新光照时出错: {e}") - - def _update_celestial_positions(self): - """ - 更新天体位置 - """ - try: - # 计算太阳位置(简化模型) - sun_angle = self.time_of_day / 24.0 * 360.0 - sun_elevation = math.sin(deg2Rad(sun_angle)) * 60.0 # 最大仰角60度 - - if hasattr(self, 'directional_light_np'): - self.directional_light_np.setHpr(sun_angle, -sun_elevation, 0) - - # 计算月亮位置(与太阳相差180度) - moon_angle = (sun_angle + 180.0) % 360.0 - moon_elevation = math.sin(deg2Rad(moon_angle)) * 50.0 - - if hasattr(self, 'moon_light_np'): - self.moon_light_np.setHpr(moon_angle, -moon_elevation, 0) - - # 根据太阳高度控制月亮显示 - if sun_elevation < -10.0: # 太阳在地平线以下10度时显示月亮 - if not self.lighting_system['moon']['enabled']: - self.world.render.setLight(self.moon_light_np) - self.lighting_system['moon']['enabled'] = True - else: - if self.lighting_system['moon']['enabled']: - self.world.render.clearLight(self.moon_light_np) - self.lighting_system['moon']['enabled'] = False - - except Exception as e: - print(f"更新天体位置时出错: {e}") - - def _update_lighting_colors(self): - """ - 更新光照颜色 - """ - try: - # 根据太阳高度调整光照颜色 - sun_elevation = math.sin(deg2Rad(self.time_of_day / 24.0 * 360.0)) * 60.0 - - # 计算日照因子 - day_factor = max(0.0, min(1.0, (sun_elevation + 10.0) / 70.0)) # 从-10度到60度 - - # 获取当前天气 - current_weather = self.weather_conditions[self.current_weather] - - # 插值计算光照颜色 - if 'directional' in self.lighting_system: - day_color = current_weather['directional_light'] - night_color = (0.1, 0.1, 0.2, 1.0) # 月光颜色 - light_color = [ - day_color[i] * day_factor + night_color[i] * (1.0 - day_factor) - for i in range(4) - ] - light_node = self.lighting_system['directional']['node'] - light_node.setColor(Vec4(*light_color)) - - # 更新环境光 - if 'ambient' in self.lighting_system: - day_ambient = current_weather['ambient_light'] - night_ambient = (0.05, 0.05, 0.1, 1.0) - ambient_color = [ - day_ambient[i] * day_factor + night_ambient[i] * (1.0 - day_factor) - for i in range(4) - ] - light_node = self.lighting_system['ambient']['node'] - light_node.setColor(Vec4(*ambient_color)) - - except Exception as e: - print(f"更新光照颜色时出错: {e}") - - def _update_sky(self, time_delta): - """ - 更新天空(增强版) - """ - try: - # 更新天空颜色 - self._update_sky_color() - - # 更新云层 - self._update_cloud_layer(time_delta) - - except Exception as e: - print(f"更新天空时出错: {e}") - - def _update_sky_color(self): - """ - 更新天空颜色 - """ - try: - # 根据时间、天气和季节调整天空颜色 - current_weather = self.weather_conditions[self.current_weather] - sky_tint = current_weather['sky_tint'] - - # 根据太阳高度调整天空颜色 - sun_elevation = math.sin(deg2Rad(self.time_of_day / 24.0 * 360.0)) * 60.0 - day_factor = max(0.0, min(1.0, (sun_elevation + 10.0) / 70.0)) - - # 计算天空颜色 - day_sky = (0.4, 0.6, 1.0, 1.0) # 白天天空蓝 - night_sky = (0.05, 0.05, 0.1, 1.0) # 夜晚天空暗 - - sky_color = [ - (day_sky[i] * sky_tint[i] * day_factor + night_sky[i] * (1.0 - day_factor)) - * current_weather['sun_intensity'] - for i in range(4) - ] - - # 应用天空颜色到天空穹顶 - if self.sky_dome: - self.sky_dome.setColor(Vec4(*sky_color)) - - except Exception as e: - print(f"更新天空颜色时出错: {e}") - - def _update_cloud_layer(self, time_delta): - """ - 更新云层 - """ - # 云层更新已在粒子系统中处理 - pass - - def _update_sky_for_weather(self, weather_type): - """ - 根据天气更新天空 - """ - try: - if self.sky_dome: - current_weather = self.weather_conditions[weather_type] - # 可以根据天气调整天空穹顶的纹理或颜色 - pass - - except Exception as e: - print(f"根据天气更新天空时出错: {e}") - - def set_time_of_day(self, hour, minute=0): - """ - 设置一天中的时间(增强版) - """ - try: - self.time_of_day = hour + minute / 60.0 - - # 立即更新光照和天空 - self._update_celestial_positions() - self._update_lighting_colors() - self._update_sky_color() - - print(f"设置时间为: {hour:02d}:{minute:02d}") - return True - - except Exception as e: - print(f"设置时间时出错: {e}") - return False - - def set_season(self, season): - """ - 设置季节(增强版) - """ - try: - seasons = { - 'spring': {'temperature': 15.0, 'vegetation_color': (0.3, 0.7, 0.3, 1.0)}, - 'summer': {'temperature': 25.0, 'vegetation_color': (0.2, 0.8, 0.2, 1.0)}, - 'autumn': {'temperature': 10.0, 'vegetation_color': (0.8, 0.5, 0.2, 1.0)}, - 'winter': {'temperature': -5.0, 'vegetation_color': (0.6, 0.6, 0.8, 1.0)} - } - - if season in seasons: - self.current_season = season - season_data = seasons[season] - print(f"设置季节为: {season}") - return season_data - - except Exception as e: - print(f"设置季节时出错: {e}") - return None - - def create_dynamic_sky(self, sky_type='dome'): - """ - 创建动态天空(增强版) - """ - try: - # 移除现有的天空 - if self.sky_dome and not self.sky_dome.isEmpty(): - self.sky_dome.removeNode() - - # 创建天空穹顶 - if sky_type == 'dome': - self._create_sky_dome() - elif sky_type == 'box': - self._create_sky_box() - elif sky_type == 'procedural': - self._create_procedural_sky() - - print(f"创建动态天空: {sky_type}") - return True - - except Exception as e: - print(f"创建动态天空时出错: {e}") - return False - - def _create_sky_dome(self): - """ - 创建天空穹顶(增强版) - """ - try: - # 创建天空穹顶几何体 - from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter - from panda3d.core import GeomTriangles, Geom, GeomNode - - # 创建顶点数据 - vformat = GeomVertexFormat.getV3c4() - vdata = GeomVertexData('sky_dome', vformat, Geom.UHStatic) - - # 添加顶点 - vertex = GeomVertexWriter(vdata, 'vertex') - color = GeomVertexWriter(vdata, 'color') - - # 创建半球形穹顶 - radius = 1000.0 - segments = 32 - rings = 16 - - vertices = [] - indices = [] - - # 生成顶点 - for r in range(rings + 1): - for s in range(segments + 1): - # 球面坐标 - theta = math.pi * r / rings # 从0到π - phi = 2 * math.pi * s / segments # 从0到2π - - x = radius * math.sin(theta) * math.cos(phi) - y = radius * math.sin(theta) * math.sin(phi) - z = radius * math.cos(theta) - - vertices.append((x, y, z)) - - # 生成索引 - for r in range(rings): - for s in range(segments): - # 计算四个顶点的索引 - i1 = r * (segments + 1) + s - i2 = r * (segments + 1) + (s + 1) - i3 = (r + 1) * (segments + 1) + s - i4 = (r + 1) * (segments + 1) + (s + 1) - - # 添加两个三角形 - indices.extend([i1, i3, i2]) - indices.extend([i2, i3, i4]) - - # 写入顶点数据 - for i, (x, y, z) in enumerate(vertices): - vertex.addData3(x, y, z) - # 默认天空颜色 - color.addData4(0.4, 0.6, 1.0, 1.0) - - # 创建几何体 - geom = Geom(vdata) - prim = GeomTriangles(Geom.UHStatic) - - # 添加索引 - for idx in indices: - prim.addVertex(idx) - prim.closePrimitive() - - geom.addPrimitive(prim) - - # 创建节点 - node = GeomNode('sky_dome') - node.addGeom(geom) - - # 创建节点路径 - self.sky_dome = self.world.render.attachNewNode(node) - - # 设置渲染属性 - self.sky_dome.setBin("background", 0) - self.sky_dome.setDepthWrite(False) - self.sky_dome.setDepthTest(False) - self.sky_dome.setLightOff() - self.sky_dome.setFogOff() - - # 设置位置在相机中心 - self.sky_dome.setCompass() # 跟随相机旋转但不移动 - - print("创建天空穹顶") - - except Exception as e: - print(f"创建天空穹顶时出错: {e}") - - def _create_sky_box(self): - """ - 创建天空盒 - """ - # 简化实现,实际项目中应该加载立方体贴图 - print("创建天空盒(简化实现)") - pass - - def _create_procedural_sky(self): - """ - 创建程序化天空 - """ - # 简化实现,实际项目中应该使用着色器生成程序化天空 - print("创建程序化天空(简化实现)") - pass - - def simulate_weather_cycle(self, duration=3600.0, cycle_pattern=None): - """ - 模拟天气循环(增强版) - """ - try: - # 创建天气变化序列 - if cycle_pattern is None: - # 默认循环模式 - cycle_pattern = ['sunny', 'cloudy', 'rainy', 'stormy', 'snowy', 'foggy'] - - # 可以实现定时改变天气 - print(f"开始天气循环模拟,持续时间: {duration}秒") - print(f"循环模式: {cycle_pattern}") - return True - - except Exception as e: - print(f"模拟天气循环时出错: {e}") - return False - - def get_environment_stats(self): - """ - 获取环境统计信息(增强版) - """ - stats = { - 'current_weather': self.current_weather, - 'time_of_day': self.time_of_day, - 'current_season': self.current_season, - 'weather_enabled': self.weather_enabled, - 'particle_systems': len(self.particle_systems), - 'fog_density': self.fog.getExpDensity() if self.fog else 0.0, - 'wind_strength': self.wind_strength, - 'wind_vector': (self.wind_vector.getX(), self.wind_vector.getY(), self.wind_vector.getZ()), - 'ambient_light': self.lighting_system['ambient']['node'].getColor() if 'ambient' in self.lighting_system else Vec4(0,0,0,0), - 'directional_light': self.lighting_system['directional']['node'].getColor() if 'directional' in self.lighting_system else Vec4(0,0,0,0), - 'moon_light_enabled': self.lighting_system['moon']['enabled'] if 'moon' in self.lighting_system else False, - 'sky_dome_exists': self.sky_dome is not None and not self.sky_dome.isEmpty(), - 'performance': { - 'last_update_time': self.performance_stats['last_update_time'], - 'avg_update_time': self.performance_stats['avg_update_time'], - 'update_count': self.performance_stats['update_count'] - } - } - - return stats - - def save_environment_data(self, output_path): - """ - 保存环境数据(增强版) - """ - try: - # 收集环境数据 - environment_data = { - 'current_weather': self.current_weather, - 'time_of_day': self.time_of_day, - 'current_season': self.current_season, - 'weather_conditions': self.weather_conditions, - 'lighting': { - 'ambient': list(self.lighting_system['ambient']['node'].getColor()) if 'ambient' in self.lighting_system else [], - 'directional': list(self.lighting_system['directional']['node'].getColor()) if 'directional' in self.lighting_system else [], - 'moon_enabled': self.lighting_system['moon']['enabled'] if 'moon' in self.lighting_system else False - }, - 'fog': { - 'color': list(self.fog.getColor()) if self.fog else [], - 'density': self.fog.getExpDensity() if self.fog else 0.0 - }, - 'wind': { - 'strength': self.wind_strength, - 'vector': [self.wind_vector.getX(), self.wind_vector.getY(), self.wind_vector.getZ()] - } - } - - # 确保输出目录存在 - output_dir = os.path.dirname(output_path) - if not os.path.exists(output_dir): - os.makedirs(output_dir) - - # 写入JSON文件 - with open(output_path, 'w', encoding='utf-8') as f: - json.dump(environment_data, f, indent=2, ensure_ascii=False) - - print(f"环境数据保存成功: {output_path}") - return True - - except Exception as e: - print(f"保存环境数据时出错: {e}") - return False - - def load_environment_data(self, input_path): - """ - 加载环境数据(增强版) - """ - try: - if not os.path.exists(input_path): - print(f"环境数据文件不存在: {input_path}") - return False - - # 读取JSON文件 - with open(input_path, 'r', encoding='utf-8') as f: - environment_data = json.load(f) - - # 恢复时间 - if 'time_of_day' in environment_data: - self.time_of_day = environment_data['time_of_day'] - - # 恢复季节 - if 'current_season' in environment_data: - self.current_season = environment_data['current_season'] - - # 恢复天气条件 - if 'weather_conditions' in environment_data: - self.weather_conditions = environment_data['weather_conditions'] - - # 恢复当前天气 - if 'current_weather' in environment_data: - self.set_weather(environment_data['current_weather'], transition_time=0) - - # 恢复光照 - if 'lighting' in environment_data: - lighting = environment_data['lighting'] - if 'ambient' in lighting and len(lighting['ambient']) == 4: - if 'ambient' in self.lighting_system: - light_node = self.lighting_system['ambient']['node'] - light_node.setColor(Vec4(*lighting['ambient'])) - if 'directional' in lighting and len(lighting['directional']) == 4: - if 'directional' in self.lighting_system: - light_node = self.lighting_system['directional']['node'] - light_node.setColor(Vec4(*lighting['directional'])) - if 'moon_enabled' in lighting: - if lighting['moon_enabled'] and not self.lighting_system['moon']['enabled']: - self.world.render.setLight(self.moon_light_np) - self.lighting_system['moon']['enabled'] = True - elif not lighting['moon_enabled'] and self.lighting_system['moon']['enabled']: - self.world.render.clearLight(self.moon_light_np) - self.lighting_system['moon']['enabled'] = False - - # 恢复雾效 - if 'fog' in environment_data: - fog_data = environment_data['fog'] - if 'color' in fog_data and len(fog_data['color']) == 4: - self.fog.setColor(*fog_data['color']) - if 'density' in fog_data: - self.fog.setExpDensity(fog_data['density']) - - # 恢复风效 - if 'wind' in environment_data: - wind_data = environment_data['wind'] - if 'strength' in wind_data: - self.wind_strength = wind_data['strength'] - if 'vector' in wind_data and len(wind_data['vector']) == 3: - self.wind_vector = Vec3(*wind_data['vector']) - - # 更新光照和天空 - self._update_celestial_positions() - self._update_lighting_colors() - self._update_sky_color() - - print(f"环境数据加载成功: {input_path}") - return True - - except Exception as e: - print(f"加载环境数据时出错: {e}") - return False - - def set_weather_enabled(self, enabled): - """ - 启用或禁用天气系统 - """ - self.weather_enabled = enabled - print(f"天气系统已{'启用' if enabled else '禁用'}") - - def clear_all_environment_effects(self): - """ - 清除所有环境效果(增强版) - """ - try: - # 清除粒子效果 - self._clear_particle_effects() - - # 重置为晴天 - self.set_weather('sunny', transition_time=0) - - # 清除雾效 - if self.fog: - self.fog.setExpDensity(0.0) - - # 重置时间 - self.set_time_of_day(12, 0) - - # 重置季节 - self.set_season('summer') - - print("清除所有环境效果") - return True - - except Exception as e: - print(f"清除所有环境效果时出错: {e}") - return False - - def create_weather_zone(self, position, radius, weather_type, transition_time=2.0): - """ - 创建局部天气区域(增强版) - """ - try: - # 创建局部天气区域 - # 可以实现球形或盒形的局部天气效果 - weather_zone = { - 'position': position, - 'radius': radius, - 'weather_type': weather_type, - 'transition_time': transition_time, - 'node': None, # 可视化节点 - 'creation_time': self.world.globalClock.getFrameTime(), - 'active': True - } - - # 可以创建一个可视化表示 - cm = CardMaker('weather_zone') - cm.setFrame(-radius, radius, -radius, radius) - zone_node = self.world.render.attachNewNode(cm.generate()) - zone_node.setPos(position) - zone_node.setBin("fixed", 10) - zone_node.setDepthWrite(False) - zone_node.setDepthTest(False) - zone_node.setColor(1, 1, 1, 0.2) # 半透明 - zone_node.hide() # 默认隐藏 - - weather_zone['node'] = zone_node - - print(f"创建局部天气区域: {weather_type} at {position}") - return weather_zone - - except Exception as e: - print(f"创建局部天气区域时出错: {e}") - return None - - def add_weather_event(self, event_type, duration, intensity=1.0): - """ - 添加天气事件(如突然的阵雨、雷暴等) - """ - try: - weather_event = { - 'type': event_type, - 'duration': duration, - 'intensity': intensity, - 'start_time': self.world.globalClock.getFrameTime(), - 'active': True - } - - # 根据事件类型应用效果 - if event_type == 'sudden_rain': - # 增加雨天粒子效果 - self._create_rain_effect() - elif event_type == 'thunderstorm': - # 增加闪电和雨天效果 - self._create_lightning_effect() - self._create_rain_effect() - elif event_type == 'gust_of_wind': - # 增加风力 - self.wind_strength *= (1.0 + intensity) - - print(f"添加天气事件: {event_type}, 持续时间: {duration}秒") - return weather_event - - except Exception as e: - print(f"添加天气事件时出错: {e}") +""" +地形天气和环境系统 +提供完整的天气模拟、环境效果、光照控制、动态天空等功能 +""" + +from panda3d.core import NodePath, Vec3, Point3, Vec4, LVector3f +from panda3d.core import AmbientLight, DirectionalLight, PointLight, Spotlight +from panda3d.core import Fog, Shader, ShaderAttrib, PerspectiveLens +from panda3d.core import Texture, TextureStage, CardMaker +from panda3d.core import PNMImage, Filename, ClockObject +from panda3d.core import deg2Rad, rad2Deg +import math +import random +import json +import os +from collections import deque + +class EnvironmentSystem: + """ + 地形天气和环境系统类 + 提供完整的天气模拟、环境效果、光照控制、动态天空等功能 + """ + + def __init__(self, world): + self.world = world + self.weather_conditions = {} # 存储天气条件 + self.environment_effects = {} # 存储环境效果 + self.lighting_system = {} # 存储光照系统 + self.particle_systems = [] # 存储粒子系统 + self.weather_enabled = True + self.current_weather = 'sunny' + self.time_of_day = 12.0 # 24小时制 + self.current_season = 'summer' + self.weather_transition = None # 天气过渡状态 + self.sky_dome = None # 天空穹顶 + self.cloud_layer = None # 云层 + self.wind_vector = Vec3(1, 0, 0) # 风向量 + self.wind_strength = 0.1 # 风力强度 + + # 性能监控 + self.performance_stats = { + 'last_update_time': 0.0, + 'update_count': 0, + 'avg_update_time': 0.0 + } + + # 初始化环境系统 + self._init_environment_system() + + def _init_environment_system(self): + """ + 初始化环境系统 + """ + try: + # 定义默认天气条件 + self._define_default_weather() + + # 初始化光照系统 + self._init_lighting_system() + + # 初始化环境效果 + self._init_environment_effects() + + # 创建动态天空 + self.create_dynamic_sky('dome') + + print("环境系统初始化完成") + + except Exception as e: + print(f"初始化环境系统时出错: {e}") + + def _define_default_weather(self): + """ + 定义默认天气条件(增强版) + """ + self.weather_conditions = { + 'sunny': { + 'name': '晴天', + 'ambient_light': (0.3, 0.3, 0.3, 1.0), + 'directional_light': (0.8, 0.8, 0.8, 1.0), + 'light_color_temp': 5500, # 色温(K) + 'fog_color': (0.5, 0.5, 0.5, 1.0), + 'fog_density': 0.0, + 'particle_effects': [], + 'wind_strength': 0.1, + 'temperature': 25.0, + 'humidity': 30.0, + 'visibility': 20000.0, # 能见度(米) + 'cloud_coverage': 0.1, # 云覆盖率 + 'precipitation': 0.0, # 降水量 + 'sky_tint': (1.0, 1.0, 1.0, 1.0), # 天空着色 + 'sun_intensity': 1.0, + 'moon_intensity': 0.0 + }, + 'cloudy': { + 'name': '多云', + 'ambient_light': (0.2, 0.2, 0.2, 1.0), + 'directional_light': (0.5, 0.5, 0.5, 1.0), + 'light_color_temp': 4500, + 'fog_color': (0.7, 0.7, 0.7, 1.0), + 'fog_density': 0.001, + 'particle_effects': ['clouds'], + 'wind_strength': 0.3, + 'temperature': 20.0, + 'humidity': 60.0, + 'visibility': 5000.0, + 'cloud_coverage': 0.7, + 'precipitation': 0.0, + 'sky_tint': (0.8, 0.8, 0.8, 1.0), + 'sun_intensity': 0.6, + 'moon_intensity': 0.1 + }, + 'rainy': { + 'name': '雨天', + 'ambient_light': (0.1, 0.1, 0.1, 1.0), + 'directional_light': (0.3, 0.3, 0.3, 1.0), + 'light_color_temp': 6500, + 'fog_color': (0.8, 0.8, 0.8, 1.0), + 'fog_density': 0.002, + 'particle_effects': ['rain', 'clouds'], + 'wind_strength': 0.5, + 'temperature': 15.0, + 'humidity': 90.0, + 'visibility': 1000.0, + 'cloud_coverage': 0.9, + 'precipitation': 5.0, + 'sky_tint': (0.7, 0.7, 0.7, 1.0), + 'sun_intensity': 0.3, + 'moon_intensity': 0.2 + }, + 'stormy': { + 'name': '暴风雨', + 'ambient_light': (0.05, 0.05, 0.05, 1.0), + 'directional_light': (0.2, 0.2, 0.2, 1.0), + 'light_color_temp': 7000, + 'fog_color': (0.9, 0.9, 0.9, 1.0), + 'fog_density': 0.003, + 'particle_effects': ['rain', 'lightning', 'clouds'], + 'wind_strength': 1.0, + 'temperature': 10.0, + 'humidity': 95.0, + 'visibility': 500.0, + 'cloud_coverage': 1.0, + 'precipitation': 20.0, + 'sky_tint': (0.5, 0.5, 0.5, 1.0), + 'sun_intensity': 0.1, + 'moon_intensity': 0.3 + }, + 'snowy': { + 'name': '雪天', + 'ambient_light': (0.2, 0.2, 0.2, 1.0), + 'directional_light': (0.4, 0.4, 0.4, 1.0), + 'light_color_temp': 5000, + 'fog_color': (0.9, 0.9, 0.9, 1.0), + 'fog_density': 0.001, + 'particle_effects': ['snow', 'clouds'], + 'wind_strength': 0.4, + 'temperature': -5.0, + 'humidity': 80.0, + 'visibility': 2000.0, + 'cloud_coverage': 0.8, + 'precipitation': 3.0, + 'sky_tint': (0.9, 0.9, 1.0, 1.0), + 'sun_intensity': 0.4, + 'moon_intensity': 0.4 + }, + 'foggy': { + 'name': '雾天', + 'ambient_light': (0.15, 0.15, 0.15, 1.0), + 'directional_light': (0.2, 0.2, 0.2, 1.0), + 'light_color_temp': 3000, + 'fog_color': (0.95, 0.95, 0.95, 1.0), + 'fog_density': 0.005, + 'particle_effects': ['mist'], + 'wind_strength': 0.2, + 'temperature': 12.0, + 'humidity': 98.0, + 'visibility': 200.0, + 'cloud_coverage': 0.3, + 'precipitation': 0.1, + 'sky_tint': (0.9, 0.9, 0.9, 1.0), + 'sun_intensity': 0.2, + 'moon_intensity': 0.1 + } + } + + def _init_lighting_system(self): + """ + 初始化光照系统(增强版) + """ + try: + # 创建环境光 + ambient_light = AmbientLight('ambient_light') + ambient_light.setColor(Vec4(0.3, 0.3, 0.3, 1.0)) + self.ambient_light_np = self.world.render.attachNewNode(ambient_light) + self.world.render.setLight(self.ambient_light_np) + + # 创建方向光(太阳光) + directional_light = DirectionalLight('directional_light') + directional_light.setColor(Vec4(0.8, 0.8, 0.8, 1.0)) + directional_light.setShadowCaster(True, 2048, 2048) + directional_light.setCameraMask(0x00000001) # 设置相机掩码 + self.directional_light_np = self.world.render.attachNewNode(directional_light) + self.directional_light_np.setHpr(45, -45, 0) + self.world.render.setLight(self.directional_light_np) + + # 创建月光 + moon_light = DirectionalLight('moon_light') + moon_light.setColor(Vec4(0.1, 0.1, 0.2, 1.0)) + self.moon_light_np = self.world.render.attachNewNode(moon_light) + self.moon_light_np.setHpr(225, -30, 0) + # 初始时禁用月光 + self.world.render.clearLight(self.moon_light_np) + + # 保存光照节点 + self.lighting_system = { + 'ambient': { + 'node': ambient_light, + 'nodepath': self.ambient_light_np + }, + 'directional': { + 'node': directional_light, + 'nodepath': self.directional_light_np + }, + 'moon': { + 'node': moon_light, + 'nodepath': self.moon_light_np, + 'enabled': False + } + } + + except Exception as e: + print(f"初始化光照系统时出错: {e}") + + def _init_environment_effects(self): + """ + 初始化环境效果(增强版) + """ + try: + # 初始化雾效 + self.fog = Fog('environment_fog') + self.fog.setColor(0.5, 0.5, 0.5, 1.0) + self.fog.setExpDensity(0.0) + self.world.render.setFog(self.fog) + + # 初始化风效 + self.wind_vector = Vec3(1, 0, 0) + self.wind_strength = 0.1 + + # 初始化粒子系统容器 + self.particle_containers = { + 'rain': [], + 'snow': [], + 'clouds': [], + 'lightning': [], + 'mist': [] + } + + except Exception as e: + print(f"初始化环境效果时出错: {e}") + + def set_weather(self, weather_type, transition_time=5.0): + """ + 设置天气(增强版) + """ + try: + if weather_type not in self.weather_conditions: + print(f"未知的天气类型: {weather_type}") + return False + + # 如果已经有过渡效果,取消它 + if self.weather_transition: + self.weather_transition = None + + # 如果需要平滑过渡 + if transition_time > 0 and self.current_weather != weather_type: + self.weather_transition = { + 'from': self.current_weather, + 'to': weather_type, + 'start_time': self.world.globalClock.getFrameTime(), + 'duration': transition_time, + 'progress': 0.0 + } + print(f"开始天气过渡: {self.current_weather} -> {weather_type}") + else: + # 直接切换天气 + self._apply_weather_immediately(weather_type) + + return True + + except Exception as e: + print(f"设置天气时出错: {e}") + return False + + def _apply_weather_immediately(self, weather_type): + """ + 立即应用天气 + """ + try: + weather = self.weather_conditions[weather_type] + self.current_weather = weather_type + + # 更新环境光 + if 'ambient' in self.lighting_system: + light_node = self.lighting_system['ambient']['node'] + light_node.setColor(Vec4(*weather['ambient_light'])) + + # 更新方向光 + if 'directional' in self.lighting_system: + light_node = self.lighting_system['directional']['node'] + light_node.setColor(Vec4(*weather['directional_light'])) + + # 更新雾效 + fog_color = weather['fog_color'] + self.fog.setColor(fog_color[0], fog_color[1], fog_color[2], fog_color[3]) + self.fog.setExpDensity(weather['fog_density']) + + # 更新风效 + self.wind_strength = weather['wind_strength'] + + # 更新粒子效果 + self._update_particle_effects(weather['particle_effects']) + + # 应用天气着色器 + self._apply_weather_shader(weather_type) + + # 更新天空 + self._update_sky_for_weather(weather_type) + + print(f"设置天气为: {weather['name']}") + + except Exception as e: + print(f"立即应用天气时出错: {e}") + + def _update_particle_effects(self, effects_list): + """ + 更新粒子效果(增强版) + """ + try: + # 清除现有粒子效果 + self._clear_particle_effects() + + # 创建新的粒子效果 + for effect in effects_list: + if effect == 'rain': + self._create_rain_effect() + elif effect == 'snow': + self._create_snow_effect() + elif effect == 'clouds': + self._create_cloud_effect() + elif effect == 'lightning': + self._create_lightning_effect() + elif effect == 'mist': + self._create_mist_effect() + + except Exception as e: + print(f"更新粒子效果时出错: {e}") + + def _clear_particle_effects(self): + """ + 清除粒子效果(增强版) + """ + try: + # 移除所有粒子系统节点 + for effect_type, particles in self.particle_containers.items(): + for particle in particles: + if particle['node'] and not particle['node'].isEmpty(): + particle['node'].removeNode() + self.particle_containers[effect_type] = [] + + # 清空旧的粒子系统列表 + self.particle_systems = [] + + except Exception as e: + print(f"清除粒子效果时出错: {e}") + + def _create_rain_effect(self): + """ + 创建雨天效果(增强版) + """ + try: + # 创建雨滴粒子系统 + rain_particles = [] + + # 创建多个雨滴发射器以提高性能 + for i in range(5): + # 创建雨滴节点 + cm = CardMaker(f'rain_drop_{i}') + cm.setFrame(-0.01, 0.01, -0.1, 0.1) + rain_node = self.world.render.attachNewNode(cm.generate()) + rain_node.setBin("fixed", 40) # 确保在最上层 + rain_node.setDepthWrite(False) + rain_node.setDepthTest(False) + + # 设置雨滴纹理(简化版本) + # 实际项目中应该加载真实的雨滴纹理 + rain_node.setColor(0.5, 0.5, 1.0, 0.7) + + # 创建粒子信息 + particle_info = { + 'type': 'rain', + 'node': rain_node, + 'position': Vec3(random.uniform(-50, 50), random.uniform(-50, 50), 20), + 'velocity': Vec3(random.uniform(-1, 1), random.uniform(-1, 1), -20.0), + 'lifetime': random.uniform(1.0, 3.0), + 'age': 0.0, + 'emitter_id': i + } + + rain_node.setPos(particle_info['position']) + rain_particles.append(particle_info) + + self.particle_containers['rain'].extend(rain_particles) + self.particle_systems.extend(rain_particles) + + print("创建雨天效果") + return rain_particles + + except Exception as e: + print(f"创建雨天效果时出错: {e}") + return [] + + def _create_snow_effect(self): + """ + 创建雪天效果(增强版) + """ + try: + # 创建雪花粒子系统 + snow_particles = [] + + # 创建多个雪花发射器 + for i in range(8): + # 创建雪花节点 + cm = CardMaker(f'snow_flake_{i}') + size = random.uniform(0.05, 0.15) + cm.setFrame(-size, size, -size, size) + snow_node = self.world.render.attachNewNode(cm.generate()) + snow_node.setBin("fixed", 35) + snow_node.setDepthWrite(False) + snow_node.setDepthTest(False) + + # 设置雪花颜色 + snow_node.setColor(1.0, 1.0, 1.0, 0.8) + + # 创建粒子信息 + particle_info = { + 'type': 'snow', + 'node': snow_node, + 'position': Vec3(random.uniform(-100, 100), random.uniform(-100, 100), 30), + 'velocity': Vec3(random.uniform(-2, 2), random.uniform(-2, 2), random.uniform(-5, -2)), + 'lifetime': random.uniform(5.0, 10.0), + 'age': 0.0, + 'rotation_speed': random.uniform(-10, 10), + 'emitter_id': i + } + + snow_node.setPos(particle_info['position']) + snow_particles.append(particle_info) + + self.particle_containers['snow'].extend(snow_particles) + self.particle_systems.extend(snow_particles) + + print("创建雪天效果") + return snow_particles + + except Exception as e: + print(f"创建雪天效果时出错: {e}") + return [] + + def _create_cloud_effect(self): + """ + 创建云效果(增强版) + """ + try: + # 创建云朵粒子系统 + cloud_particles = [] + + # 创建多个云朵 + for i in range(10): + # 创建云朵节点 + cm = CardMaker(f'cloud_{i}') + size = random.uniform(10, 30) + cm.setFrame(-size, size, -size/2, size/2) + cloud_node = self.world.render.attachNewNode(cm.generate()) + cloud_node.setBin("fixed", 30) + cloud_node.setDepthWrite(False) + cloud_node.setDepthTest(False) + + # 设置云朵颜色和透明度 + cloud_node.setColor(1.0, 1.0, 1.0, random.uniform(0.3, 0.7)) + + # 创建粒子信息 + particle_info = { + 'type': 'clouds', + 'node': cloud_node, + 'position': Vec3(random.uniform(-200, 200), random.uniform(-200, 200), random.uniform(50, 80)), + 'velocity': Vec3(random.uniform(-0.5, 0.5), random.uniform(-0.5, 0.5), 0), + 'lifetime': float('inf'), # 永久存在 + 'age': 0.0, + 'scale': size, + 'emitter_id': i + } + + cloud_node.setPos(particle_info['position']) + cloud_particles.append(particle_info) + + self.particle_containers['clouds'].extend(cloud_particles) + self.particle_systems.extend(cloud_particles) + + print("创建云效果") + return cloud_particles + + except Exception as e: + print(f"创建云效果时出错: {e}") + return [] + + def _create_lightning_effect(self): + """ + 创建闪电效果(增强版) + """ + try: + # 创建闪电效果 + lightning_effects = [] + + # 创建闪电节点 + cm = CardMaker('lightning') + cm.setFrame(-100, 100, -100, 100) + lightning_node = self.world.render.attachNewNode(cm.generate()) + lightning_node.setBin("fixed", 50) + lightning_node.setDepthWrite(False) + lightning_node.setDepthTest(False) + + # 设置初始不可见 + lightning_node.hide() + + # 创建闪电信息 + lightning_info = { + 'type': 'lightning', + 'node': lightning_node, + 'position': Vec3(0, 0, 100), + 'lifetime': 0.1, # 闪电持续时间很短 + 'age': 0.0, + 'next_strike_time': random.uniform(5, 15), # 下次闪电时间 + 'intensity': 2.0 + } + + lightning_node.setPos(lightning_info['position']) + lightning_effects.append(lightning_info) + + self.particle_containers['lightning'].extend(lightning_effects) + self.particle_systems.extend(lightning_effects) + + print("创建闪电效果") + return lightning_effects + + except Exception as e: + print(f"创建闪电效果时出错: {e}") + return [] + + def _create_mist_effect(self): + """ + 创建雾气效果(增强版) + """ + try: + # 创建雾气粒子系统 + mist_particles = [] + + # 创建多个雾气发射器 + for i in range(3): + # 创建雾气节点 + cm = CardMaker(f'mist_{i}') + size = random.uniform(20, 50) + cm.setFrame(-size, size, -size/2, size/2) + mist_node = self.world.render.attachNewNode(cm.generate()) + mist_node.setBin("fixed", 25) + mist_node.setDepthWrite(False) + mist_node.setDepthTest(False) + + # 设置雾气颜色 + mist_node.setColor(0.9, 0.9, 0.9, random.uniform(0.2, 0.4)) + + # 创建粒子信息 + particle_info = { + 'type': 'mist', + 'node': mist_node, + 'position': Vec3(random.uniform(-100, 100), random.uniform(-100, 100), random.uniform(0, 5)), + 'velocity': Vec3(random.uniform(-0.2, 0.2), random.uniform(-0.2, 0.2), 0), + 'lifetime': float('inf'), + 'age': 0.0, + 'scale': size, + 'emitter_id': i + } + + mist_node.setPos(particle_info['position']) + mist_particles.append(particle_info) + + self.particle_containers['mist'].extend(mist_particles) + self.particle_systems.extend(mist_particles) + + print("创建雾气效果") + return mist_particles + + except Exception as e: + print(f"创建雾气效果时出错: {e}") + return [] + + def _apply_weather_shader(self, weather_type): + """ + 应用天气着色器(增强版) + """ + try: + # 如果使用RenderPipeline,应用天气效果 + if hasattr(self.world, 'render_pipeline'): + try: + # 根据天气类型选择着色器 + if weather_type == 'rainy': + effect_file = "effects/rain_weather.yaml" + elif weather_type == 'stormy': + effect_file = "effects/storm_weather.yaml" + elif weather_type == 'snowy': + effect_file = "effects/snow_weather.yaml" + elif weather_type == 'foggy': + effect_file = "effects/fog_weather.yaml" + else: + effect_file = "effects/default_weather.yaml" + + self.world.render_pipeline.set_effect( + self.world.render, + effect_file, + { + "weather_type": weather_type, + "wind_strength": self.wind_strength, + "fog_density": self.weather_conditions[weather_type]['fog_density'], + "time_of_day": self.time_of_day + }, + 5 # 排序值 + ) + + except Exception as e: + print(f"应用天气着色器失败: {e}") + + except Exception as e: + print(f"应用天气着色器时出错: {e}") + + def update_environment(self, time_delta): + """ + 更新环境系统(增强版) + """ + try: + if not self.weather_enabled: + return + + # 性能监控开始 + start_time = self.world.globalClock.getRealTime() + + # 更新天气过渡 + self._update_weather_transition(time_delta) + + # 更新粒子效果 + self._update_particles(time_delta) + + # 更新风效 + self._update_wind(time_delta) + + # 更新光照 + self._update_lighting(time_delta) + + # 更新天空 + self._update_sky(time_delta) + + # 更新性能统计 + end_time = self.world.globalClock.getRealTime() + update_time = end_time - start_time + self.performance_stats['last_update_time'] = update_time + self.performance_stats['update_count'] += 1 + + # 计算平均更新时间 + if self.performance_stats['update_count'] > 1: + alpha = 0.1 + self.performance_stats['avg_update_time'] = ( + alpha * update_time + + (1 - alpha) * self.performance_stats['avg_update_time'] + ) + else: + self.performance_stats['avg_update_time'] = update_time + + except Exception as e: + print(f"更新环境系统时出错: {e}") + + def _update_weather_transition(self, time_delta): + """ + 更新天气过渡效果 + """ + try: + if not self.weather_transition: + return + + # 更新过渡进度 + current_time = self.world.globalClock.getFrameTime() + elapsed = current_time - self.weather_transition['start_time'] + progress = min(1.0, elapsed / self.weather_transition['duration']) + + self.weather_transition['progress'] = progress + + # 获取起始和目标天气 + from_weather = self.weather_conditions[self.weather_transition['from']] + to_weather = self.weather_conditions[self.weather_transition['to']] + + # 插值计算当前天气参数 + if progress >= 1.0: + # 过渡完成,应用最终天气 + self._apply_weather_immediately(self.weather_transition['to']) + self.weather_transition = None + else: + # 过渡中,插值计算参数 + self._interpolate_weather_parameters(from_weather, to_weather, progress) + + except Exception as e: + print(f"更新天气过渡时出错: {e}") + + def _interpolate_weather_parameters(self, from_weather, to_weather, progress): + """ + 插值计算天气参数 + """ + try: + # 线性插值函数 + def lerp(a, b, t): + if isinstance(a, (list, tuple)) and len(a) == len(b): + return [a[i] + (b[i] - a[i]) * t for i in range(len(a))] + return a + (b - a) * t + + # 插值环境光 + if 'ambient' in self.lighting_system: + ambient_from = from_weather['ambient_light'] + ambient_to = to_weather['ambient_light'] + ambient_current = lerp(ambient_from, ambient_to, progress) + light_node = self.lighting_system['ambient']['node'] + light_node.setColor(Vec4(*ambient_current)) + + # 插值方向光 + if 'directional' in self.lighting_system: + directional_from = from_weather['directional_light'] + directional_to = to_weather['directional_light'] + directional_current = lerp(directional_from, directional_to, progress) + light_node = self.lighting_system['directional']['node'] + light_node.setColor(Vec4(*directional_current)) + + # 插值雾效 + fog_from = from_weather['fog_color'] + fog_to = to_weather['fog_color'] + fog_color_current = lerp(fog_from, fog_to, progress) + self.fog.setColor(fog_color_current[0], fog_color_current[1], fog_color_current[2], fog_color_current[3]) + + fog_density_from = from_weather['fog_density'] + fog_density_to = to_weather['fog_density'] + fog_density_current = lerp(fog_density_from, fog_density_to, progress) + self.fog.setExpDensity(fog_density_current) + + # 插值风力 + wind_from = from_weather['wind_strength'] + wind_to = to_weather['wind_strength'] + self.wind_strength = lerp(wind_from, wind_to, progress) + + except Exception as e: + print(f"插值天气参数时出错: {e}") + + def _update_particles(self, time_delta): + """ + 更新粒子效果(增强版) + """ + try: + current_time = self.world.globalClock.getFrameTime() + + # 更新所有粒子系统 + for particle in self.particle_systems[:]: # 使用切片复制 + # 更新粒子年龄 + particle['age'] += time_delta + + # 检查生命周期 + if particle['lifetime'] != float('inf') and particle['age'] > particle['lifetime']: + # 移除过期粒子 + if particle['node'] and not particle['node'].isEmpty(): + particle['node'].removeNode() + if particle in self.particle_systems: + self.particle_systems.remove(particle) + continue + + # 根据粒子类型更新 + if particle['type'] == 'rain': + self._update_rain_particle(particle, time_delta, current_time) + elif particle['type'] == 'snow': + self._update_snow_particle(particle, time_delta, current_time) + elif particle['type'] == 'clouds': + self._update_cloud_particle(particle, time_delta, current_time) + elif particle['type'] == 'lightning': + self._update_lightning_particle(particle, time_delta, current_time) + elif particle['type'] == 'mist': + self._update_mist_particle(particle, time_delta, current_time) + + except Exception as e: + print(f"更新粒子效果时出错: {e}") + + def _update_rain_particle(self, particle, time_delta, current_time): + """ + 更新雨滴粒子 + """ + try: + # 更新位置 + particle['position'] += particle['velocity'] * time_delta + particle['node'].setPos(particle['position']) + + # 如果雨滴落到地面以下,重置位置 + if particle['position'].getZ() < 0: + particle['position'].setZ(20) # 重置到高处 + particle['position'].setX(random.uniform(-50, 50)) + particle['position'].setY(random.uniform(-50, 50)) + particle['node'].setPos(particle['position']) + + except Exception as e: + print(f"更新雨滴粒子时出错: {e}") + + def _update_snow_particle(self, particle, time_delta, current_time): + """ + 更新雪花粒子 + """ + try: + # 更新位置 + particle['position'] += particle['velocity'] * time_delta + particle['node'].setPos(particle['position']) + + # 添加摆动效果 + sway = math.sin(current_time * 2 + particle['emitter_id']) * 0.5 + particle['position'].setX(particle['position'].getX() + sway * time_delta) + particle['node'].setPos(particle['position']) + + # 更新旋转 + particle['node'].setHpr(particle['node'].getHpr() + Vec3(0, 0, particle['rotation_speed'] * time_delta)) + + # 如果雪花落到地面以下,重置位置 + if particle['position'].getZ() < 0: + particle['position'].setZ(30) # 重置到高处 + particle['position'].setX(random.uniform(-100, 100)) + particle['position'].setY(random.uniform(-100, 100)) + particle['node'].setPos(particle['position']) + + except Exception as e: + print(f"更新雪花粒子时出错: {e}") + + def _update_cloud_particle(self, particle, time_delta, current_time): + """ + 更新云朵粒子 + """ + try: + # 更新位置 + particle['position'] += particle['velocity'] * time_delta + particle['node'].setPos(particle['position']) + + # 添加缓慢变化效果 + if random.random() < 0.01: # 1%概率改变速度 + particle['velocity'] = Vec3( + random.uniform(-0.5, 0.5), + random.uniform(-0.5, 0.5), + 0 + ) + + # 如果云朵移出范围,重置位置 + if (abs(particle['position'].getX()) > 300 or + abs(particle['position'].getY()) > 300): + particle['position'].setX(random.uniform(-200, 200)) + particle['position'].setY(random.uniform(-200, 200)) + particle['node'].setPos(particle['position']) + + except Exception as e: + print(f"更新云朵粒子时出错: {e}") + + def _update_lightning_particle(self, particle, time_delta, current_time): + """ + 更新闪电粒子 + """ + try: + # 更新计时器 + particle['next_strike_time'] -= time_delta + + # 检查是否触发闪电 + if particle['next_strike_time'] <= 0: + # 显示闪电 + particle['node'].show() + particle['age'] = 0 # 重置年龄 + + # 随机改变闪电位置 + particle['position'] = Vec3( + random.uniform(-100, 100), + random.uniform(-100, 100), + 100 + ) + particle['node'].setPos(particle['position']) + + # 设置下次闪电时间 + particle['next_strike_time'] = random.uniform(5, 15) + + # 增强环境光模拟闪电光照 + if 'ambient' in self.lighting_system: + light_node = self.lighting_system['ambient']['node'] + light_node.setColor(Vec4(0.8, 0.8, 0.8, 1.0)) + else: + # 检查是否需要隐藏闪电 + if particle['age'] > particle['lifetime']: + particle['node'].hide() + # 恢复正常环境光 + current_weather = self.weather_conditions[self.current_weather] + if 'ambient' in self.lighting_system: + light_node = self.lighting_system['ambient']['node'] + light_node.setColor(Vec4(*current_weather['ambient_light'])) + + except Exception as e: + print(f"更新闪电粒子时出错: {e}") + + def _update_mist_particle(self, particle, time_delta, current_time): + """ + 更新雾气粒子 + """ + try: + # 更新位置 + particle['position'] += particle['velocity'] * time_delta + particle['node'].setPos(particle['position']) + + # 缓慢改变透明度 + alpha = particle['node'].getColor().getW() + alpha += random.uniform(-0.01, 0.01) + alpha = max(0.1, min(0.5, alpha)) # 限制在合理范围内 + particle['node'].setColor( + particle['node'].getColor().getX(), + particle['node'].getColor().getY(), + particle['node'].getColor().getZ(), + alpha + ) + + # 如果雾气移出范围,重置位置 + if (abs(particle['position'].getX()) > 200 or + abs(particle['position'].getY()) > 200): + particle['position'].setX(random.uniform(-100, 100)) + particle['position'].setY(random.uniform(-100, 100)) + particle['node'].setPos(particle['position']) + + except Exception as e: + print(f"更新雾气粒子时出错: {e}") + + def _update_wind(self, time_delta): + """ + 更新风效(增强版) + """ + try: + # 可以实现风向和风力的变化 + # 例如随机改变风向或模拟阵风 + current_weather = self.weather_conditions[self.current_weather] + + # 模拟阵风效果 + gust_factor = 1.0 + math.sin(self.world.globalClock.getFrameTime() * 0.5) * 0.3 + self.wind_vector = Vec3( + math.cos(self.world.globalClock.getFrameTime() * 0.1) * gust_factor, + math.sin(self.world.globalClock.getFrameTime() * 0.1) * gust_factor, + 0 + ) * current_weather['wind_strength'] + + except Exception as e: + print(f"更新风效时出错: {e}") + + def _update_lighting(self, time_delta): + """ + 更新光照(增强版) + """ + try: + # 更新时间 + self.time_of_day += time_delta / 3600.0 # 假设1秒对应1小时 + if self.time_of_day >= 24.0: + self.time_of_day -= 24.0 + + # 根据时间调整太阳和月亮位置 + self._update_celestial_positions() + + # 根据时间调整光照颜色 + self._update_lighting_colors() + + except Exception as e: + print(f"更新光照时出错: {e}") + + def _update_celestial_positions(self): + """ + 更新天体位置 + """ + try: + # 计算太阳位置(简化模型) + sun_angle = self.time_of_day / 24.0 * 360.0 + sun_elevation = math.sin(deg2Rad(sun_angle)) * 60.0 # 最大仰角60度 + + if hasattr(self, 'directional_light_np'): + self.directional_light_np.setHpr(sun_angle, -sun_elevation, 0) + + # 计算月亮位置(与太阳相差180度) + moon_angle = (sun_angle + 180.0) % 360.0 + moon_elevation = math.sin(deg2Rad(moon_angle)) * 50.0 + + if hasattr(self, 'moon_light_np'): + self.moon_light_np.setHpr(moon_angle, -moon_elevation, 0) + + # 根据太阳高度控制月亮显示 + if sun_elevation < -10.0: # 太阳在地平线以下10度时显示月亮 + if not self.lighting_system['moon']['enabled']: + self.world.render.setLight(self.moon_light_np) + self.lighting_system['moon']['enabled'] = True + else: + if self.lighting_system['moon']['enabled']: + self.world.render.clearLight(self.moon_light_np) + self.lighting_system['moon']['enabled'] = False + + except Exception as e: + print(f"更新天体位置时出错: {e}") + + def _update_lighting_colors(self): + """ + 更新光照颜色 + """ + try: + # 根据太阳高度调整光照颜色 + sun_elevation = math.sin(deg2Rad(self.time_of_day / 24.0 * 360.0)) * 60.0 + + # 计算日照因子 + day_factor = max(0.0, min(1.0, (sun_elevation + 10.0) / 70.0)) # 从-10度到60度 + + # 获取当前天气 + current_weather = self.weather_conditions[self.current_weather] + + # 插值计算光照颜色 + if 'directional' in self.lighting_system: + day_color = current_weather['directional_light'] + night_color = (0.1, 0.1, 0.2, 1.0) # 月光颜色 + light_color = [ + day_color[i] * day_factor + night_color[i] * (1.0 - day_factor) + for i in range(4) + ] + light_node = self.lighting_system['directional']['node'] + light_node.setColor(Vec4(*light_color)) + + # 更新环境光 + if 'ambient' in self.lighting_system: + day_ambient = current_weather['ambient_light'] + night_ambient = (0.05, 0.05, 0.1, 1.0) + ambient_color = [ + day_ambient[i] * day_factor + night_ambient[i] * (1.0 - day_factor) + for i in range(4) + ] + light_node = self.lighting_system['ambient']['node'] + light_node.setColor(Vec4(*ambient_color)) + + except Exception as e: + print(f"更新光照颜色时出错: {e}") + + def _update_sky(self, time_delta): + """ + 更新天空(增强版) + """ + try: + # 更新天空颜色 + self._update_sky_color() + + # 更新云层 + self._update_cloud_layer(time_delta) + + except Exception as e: + print(f"更新天空时出错: {e}") + + def _update_sky_color(self): + """ + 更新天空颜色 + """ + try: + # 根据时间、天气和季节调整天空颜色 + current_weather = self.weather_conditions[self.current_weather] + sky_tint = current_weather['sky_tint'] + + # 根据太阳高度调整天空颜色 + sun_elevation = math.sin(deg2Rad(self.time_of_day / 24.0 * 360.0)) * 60.0 + day_factor = max(0.0, min(1.0, (sun_elevation + 10.0) / 70.0)) + + # 计算天空颜色 + day_sky = (0.4, 0.6, 1.0, 1.0) # 白天天空蓝 + night_sky = (0.05, 0.05, 0.1, 1.0) # 夜晚天空暗 + + sky_color = [ + (day_sky[i] * sky_tint[i] * day_factor + night_sky[i] * (1.0 - day_factor)) + * current_weather['sun_intensity'] + for i in range(4) + ] + + # 应用天空颜色到天空穹顶 + if self.sky_dome: + self.sky_dome.setColor(Vec4(*sky_color)) + + except Exception as e: + print(f"更新天空颜色时出错: {e}") + + def _update_cloud_layer(self, time_delta): + """ + 更新云层 + """ + # 云层更新已在粒子系统中处理 + pass + + def _update_sky_for_weather(self, weather_type): + """ + 根据天气更新天空 + """ + try: + if self.sky_dome: + current_weather = self.weather_conditions[weather_type] + # 可以根据天气调整天空穹顶的纹理或颜色 + pass + + except Exception as e: + print(f"根据天气更新天空时出错: {e}") + + def set_time_of_day(self, hour, minute=0): + """ + 设置一天中的时间(增强版) + """ + try: + self.time_of_day = hour + minute / 60.0 + + # 立即更新光照和天空 + self._update_celestial_positions() + self._update_lighting_colors() + self._update_sky_color() + + print(f"设置时间为: {hour:02d}:{minute:02d}") + return True + + except Exception as e: + print(f"设置时间时出错: {e}") + return False + + def set_season(self, season): + """ + 设置季节(增强版) + """ + try: + seasons = { + 'spring': {'temperature': 15.0, 'vegetation_color': (0.3, 0.7, 0.3, 1.0)}, + 'summer': {'temperature': 25.0, 'vegetation_color': (0.2, 0.8, 0.2, 1.0)}, + 'autumn': {'temperature': 10.0, 'vegetation_color': (0.8, 0.5, 0.2, 1.0)}, + 'winter': {'temperature': -5.0, 'vegetation_color': (0.6, 0.6, 0.8, 1.0)} + } + + if season in seasons: + self.current_season = season + season_data = seasons[season] + print(f"设置季节为: {season}") + return season_data + + except Exception as e: + print(f"设置季节时出错: {e}") + return None + + def create_dynamic_sky(self, sky_type='dome'): + """ + 创建动态天空(增强版) + """ + try: + # 移除现有的天空 + if self.sky_dome and not self.sky_dome.isEmpty(): + self.sky_dome.removeNode() + + # 创建天空穹顶 + if sky_type == 'dome': + self._create_sky_dome() + elif sky_type == 'box': + self._create_sky_box() + elif sky_type == 'procedural': + self._create_procedural_sky() + + print(f"创建动态天空: {sky_type}") + return True + + except Exception as e: + print(f"创建动态天空时出错: {e}") + return False + + def _create_sky_dome(self): + """ + 创建天空穹顶(增强版) + """ + try: + # 创建天空穹顶几何体 + from panda3d.core import GeomVertexFormat, GeomVertexData, GeomVertexWriter + from panda3d.core import GeomTriangles, Geom, GeomNode + + # 创建顶点数据 + vformat = GeomVertexFormat.getV3c4() + vdata = GeomVertexData('sky_dome', vformat, Geom.UHStatic) + + # 添加顶点 + vertex = GeomVertexWriter(vdata, 'vertex') + color = GeomVertexWriter(vdata, 'color') + + # 创建半球形穹顶 + radius = 1000.0 + segments = 32 + rings = 16 + + vertices = [] + indices = [] + + # 生成顶点 + for r in range(rings + 1): + for s in range(segments + 1): + # 球面坐标 + theta = math.pi * r / rings # 从0到π + phi = 2 * math.pi * s / segments # 从0到2π + + x = radius * math.sin(theta) * math.cos(phi) + y = radius * math.sin(theta) * math.sin(phi) + z = radius * math.cos(theta) + + vertices.append((x, y, z)) + + # 生成索引 + for r in range(rings): + for s in range(segments): + # 计算四个顶点的索引 + i1 = r * (segments + 1) + s + i2 = r * (segments + 1) + (s + 1) + i3 = (r + 1) * (segments + 1) + s + i4 = (r + 1) * (segments + 1) + (s + 1) + + # 添加两个三角形 + indices.extend([i1, i3, i2]) + indices.extend([i2, i3, i4]) + + # 写入顶点数据 + for i, (x, y, z) in enumerate(vertices): + vertex.addData3(x, y, z) + # 默认天空颜色 + color.addData4(0.4, 0.6, 1.0, 1.0) + + # 创建几何体 + geom = Geom(vdata) + prim = GeomTriangles(Geom.UHStatic) + + # 添加索引 + for idx in indices: + prim.addVertex(idx) + prim.closePrimitive() + + geom.addPrimitive(prim) + + # 创建节点 + node = GeomNode('sky_dome') + node.addGeom(geom) + + # 创建节点路径 + self.sky_dome = self.world.render.attachNewNode(node) + + # 设置渲染属性 + self.sky_dome.setBin("background", 0) + self.sky_dome.setDepthWrite(False) + self.sky_dome.setDepthTest(False) + self.sky_dome.setLightOff() + self.sky_dome.setFogOff() + + # 设置位置在相机中心 + self.sky_dome.setCompass() # 跟随相机旋转但不移动 + + print("创建天空穹顶") + + except Exception as e: + print(f"创建天空穹顶时出错: {e}") + + def _create_sky_box(self): + """ + 创建天空盒 + """ + # 简化实现,实际项目中应该加载立方体贴图 + print("创建天空盒(简化实现)") + pass + + def _create_procedural_sky(self): + """ + 创建程序化天空 + """ + # 简化实现,实际项目中应该使用着色器生成程序化天空 + print("创建程序化天空(简化实现)") + pass + + def simulate_weather_cycle(self, duration=3600.0, cycle_pattern=None): + """ + 模拟天气循环(增强版) + """ + try: + # 创建天气变化序列 + if cycle_pattern is None: + # 默认循环模式 + cycle_pattern = ['sunny', 'cloudy', 'rainy', 'stormy', 'snowy', 'foggy'] + + # 可以实现定时改变天气 + print(f"开始天气循环模拟,持续时间: {duration}秒") + print(f"循环模式: {cycle_pattern}") + return True + + except Exception as e: + print(f"模拟天气循环时出错: {e}") + return False + + def get_environment_stats(self): + """ + 获取环境统计信息(增强版) + """ + stats = { + 'current_weather': self.current_weather, + 'time_of_day': self.time_of_day, + 'current_season': self.current_season, + 'weather_enabled': self.weather_enabled, + 'particle_systems': len(self.particle_systems), + 'fog_density': self.fog.getExpDensity() if self.fog else 0.0, + 'wind_strength': self.wind_strength, + 'wind_vector': (self.wind_vector.getX(), self.wind_vector.getY(), self.wind_vector.getZ()), + 'ambient_light': self.lighting_system['ambient']['node'].getColor() if 'ambient' in self.lighting_system else Vec4(0,0,0,0), + 'directional_light': self.lighting_system['directional']['node'].getColor() if 'directional' in self.lighting_system else Vec4(0,0,0,0), + 'moon_light_enabled': self.lighting_system['moon']['enabled'] if 'moon' in self.lighting_system else False, + 'sky_dome_exists': self.sky_dome is not None and not self.sky_dome.isEmpty(), + 'performance': { + 'last_update_time': self.performance_stats['last_update_time'], + 'avg_update_time': self.performance_stats['avg_update_time'], + 'update_count': self.performance_stats['update_count'] + } + } + + return stats + + def save_environment_data(self, output_path): + """ + 保存环境数据(增强版) + """ + try: + # 收集环境数据 + environment_data = { + 'current_weather': self.current_weather, + 'time_of_day': self.time_of_day, + 'current_season': self.current_season, + 'weather_conditions': self.weather_conditions, + 'lighting': { + 'ambient': list(self.lighting_system['ambient']['node'].getColor()) if 'ambient' in self.lighting_system else [], + 'directional': list(self.lighting_system['directional']['node'].getColor()) if 'directional' in self.lighting_system else [], + 'moon_enabled': self.lighting_system['moon']['enabled'] if 'moon' in self.lighting_system else False + }, + 'fog': { + 'color': list(self.fog.getColor()) if self.fog else [], + 'density': self.fog.getExpDensity() if self.fog else 0.0 + }, + 'wind': { + 'strength': self.wind_strength, + 'vector': [self.wind_vector.getX(), self.wind_vector.getY(), self.wind_vector.getZ()] + } + } + + # 确保输出目录存在 + output_dir = os.path.dirname(output_path) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + # 写入JSON文件 + with open(output_path, 'w', encoding='utf-8') as f: + json.dump(environment_data, f, indent=2, ensure_ascii=False) + + print(f"环境数据保存成功: {output_path}") + return True + + except Exception as e: + print(f"保存环境数据时出错: {e}") + return False + + def load_environment_data(self, input_path): + """ + 加载环境数据(增强版) + """ + try: + if not os.path.exists(input_path): + print(f"环境数据文件不存在: {input_path}") + return False + + # 读取JSON文件 + with open(input_path, 'r', encoding='utf-8') as f: + environment_data = json.load(f) + + # 恢复时间 + if 'time_of_day' in environment_data: + self.time_of_day = environment_data['time_of_day'] + + # 恢复季节 + if 'current_season' in environment_data: + self.current_season = environment_data['current_season'] + + # 恢复天气条件 + if 'weather_conditions' in environment_data: + self.weather_conditions = environment_data['weather_conditions'] + + # 恢复当前天气 + if 'current_weather' in environment_data: + self.set_weather(environment_data['current_weather'], transition_time=0) + + # 恢复光照 + if 'lighting' in environment_data: + lighting = environment_data['lighting'] + if 'ambient' in lighting and len(lighting['ambient']) == 4: + if 'ambient' in self.lighting_system: + light_node = self.lighting_system['ambient']['node'] + light_node.setColor(Vec4(*lighting['ambient'])) + if 'directional' in lighting and len(lighting['directional']) == 4: + if 'directional' in self.lighting_system: + light_node = self.lighting_system['directional']['node'] + light_node.setColor(Vec4(*lighting['directional'])) + if 'moon_enabled' in lighting: + if lighting['moon_enabled'] and not self.lighting_system['moon']['enabled']: + self.world.render.setLight(self.moon_light_np) + self.lighting_system['moon']['enabled'] = True + elif not lighting['moon_enabled'] and self.lighting_system['moon']['enabled']: + self.world.render.clearLight(self.moon_light_np) + self.lighting_system['moon']['enabled'] = False + + # 恢复雾效 + if 'fog' in environment_data: + fog_data = environment_data['fog'] + if 'color' in fog_data and len(fog_data['color']) == 4: + self.fog.setColor(*fog_data['color']) + if 'density' in fog_data: + self.fog.setExpDensity(fog_data['density']) + + # 恢复风效 + if 'wind' in environment_data: + wind_data = environment_data['wind'] + if 'strength' in wind_data: + self.wind_strength = wind_data['strength'] + if 'vector' in wind_data and len(wind_data['vector']) == 3: + self.wind_vector = Vec3(*wind_data['vector']) + + # 更新光照和天空 + self._update_celestial_positions() + self._update_lighting_colors() + self._update_sky_color() + + print(f"环境数据加载成功: {input_path}") + return True + + except Exception as e: + print(f"加载环境数据时出错: {e}") + return False + + def set_weather_enabled(self, enabled): + """ + 启用或禁用天气系统 + """ + self.weather_enabled = enabled + print(f"天气系统已{'启用' if enabled else '禁用'}") + + def clear_all_environment_effects(self): + """ + 清除所有环境效果(增强版) + """ + try: + # 清除粒子效果 + self._clear_particle_effects() + + # 重置为晴天 + self.set_weather('sunny', transition_time=0) + + # 清除雾效 + if self.fog: + self.fog.setExpDensity(0.0) + + # 重置时间 + self.set_time_of_day(12, 0) + + # 重置季节 + self.set_season('summer') + + print("清除所有环境效果") + return True + + except Exception as e: + print(f"清除所有环境效果时出错: {e}") + return False + + def create_weather_zone(self, position, radius, weather_type, transition_time=2.0): + """ + 创建局部天气区域(增强版) + """ + try: + # 创建局部天气区域 + # 可以实现球形或盒形的局部天气效果 + weather_zone = { + 'position': position, + 'radius': radius, + 'weather_type': weather_type, + 'transition_time': transition_time, + 'node': None, # 可视化节点 + 'creation_time': self.world.globalClock.getFrameTime(), + 'active': True + } + + # 可以创建一个可视化表示 + cm = CardMaker('weather_zone') + cm.setFrame(-radius, radius, -radius, radius) + zone_node = self.world.render.attachNewNode(cm.generate()) + zone_node.setPos(position) + zone_node.setBin("fixed", 10) + zone_node.setDepthWrite(False) + zone_node.setDepthTest(False) + zone_node.setColor(1, 1, 1, 0.2) # 半透明 + zone_node.hide() # 默认隐藏 + + weather_zone['node'] = zone_node + + print(f"创建局部天气区域: {weather_type} at {position}") + return weather_zone + + except Exception as e: + print(f"创建局部天气区域时出错: {e}") + return None + + def add_weather_event(self, event_type, duration, intensity=1.0): + """ + 添加天气事件(如突然的阵雨、雷暴等) + """ + try: + weather_event = { + 'type': event_type, + 'duration': duration, + 'intensity': intensity, + 'start_time': self.world.globalClock.getFrameTime(), + 'active': True + } + + # 根据事件类型应用效果 + if event_type == 'sudden_rain': + # 增加雨天粒子效果 + self._create_rain_effect() + elif event_type == 'thunderstorm': + # 增加闪电和雨天效果 + self._create_lightning_effect() + self._create_rain_effect() + elif event_type == 'gust_of_wind': + # 增加风力 + self.wind_strength *= (1.0 + intensity) + + print(f"添加天气事件: {event_type}, 持续时间: {duration}秒") + return weather_event + + except Exception as e: + print(f"添加天气事件时出错: {e}") return None \ No newline at end of file diff --git a/plugins/user/terrain_editor/io/__init__.py b/plugins/user/terrain_editor/io/__init__.py index 4f84d72e..c72e541f 100644 --- a/plugins/user/terrain_editor/io/__init__.py +++ b/plugins/user/terrain_editor/io/__init__.py @@ -1,3 +1,3 @@ -""" -地形导入导出模块 +""" +地形导入导出模块 """ \ No newline at end of file diff --git a/plugins/user/terrain_editor/io/terrain_io.py b/plugins/user/terrain_editor/io/terrain_io.py index c85ce1d3..f3bc42bf 100644 --- a/plugins/user/terrain_editor/io/terrain_io.py +++ b/plugins/user/terrain_editor/io/terrain_io.py @@ -1,494 +1,494 @@ -""" -地形导入导出模块 -提供完整的地形数据导入导出功能,支持多种格式和批量操作 -""" - -import os -import json -import math -import numpy as np -from datetime import datetime -from panda3d.core import PNMImage, Filename, PNMFileTypeRegistry, PNMFileType - -class TerrainIO: - """ - 地形导入导出类 - 提供完整的地形数据导入导出功能 - """ - - def __init__(self, world): - self.world = world - self.supported_formats = ['.png', '.jpg', '.jpeg', '.bmp', '.tga', '.raw', '.ter'] - - def import_heightmap(self, heightmap_path, scale=(1, 1, 1), resolution=None): - """ - 从高度图导入地形 - 支持多种图像格式的高度图 - """ - if not os.path.exists(heightmap_path): - print(f"高度图文件不存在: {heightmap_path}") - return None - - try: - # 检查文件格式 - file_ext = os.path.splitext(heightmap_path)[1].lower() - if file_ext not in self.supported_formats: - print(f"不支持的高度图格式: {file_ext}") - return None - - # 使用现有的地形管理器创建地形 - if hasattr(self.world, 'terrain_manager'): - terrain = self.world.terrain_manager.createTerrainFromHeightMap(heightmap_path, scale) - return terrain - else: - print("地形管理器不可用") - return None - - except Exception as e: - print(f"导入高度图时出错: {e}") - return None - - def export_heightmap(self, terrain_info, output_path, format='png', bit_depth=8): - """ - 导出地形为高度图 - 支持多种格式和位深度 - """ - if not terrain_info: - print("地形信息无效") - return False - - try: - # 从地形信息中获取高度图数据 - if 'heightfield' in terrain_info: - heightfield = terrain_info['heightfield'] - if heightfield: - # 确保输出目录存在 - output_dir = os.path.dirname(output_path) - if not os.path.exists(output_dir): - os.makedirs(output_dir) - - # 根据位深度调整高度图 - if bit_depth == 16: - # 转换为16位 - temp_image = PNMImage(heightfield.getXSize(), heightfield.getYSize(), 1) # 灰度图 - temp_image.setMaxval(65535) # 16位最大值 - - for x in range(heightfield.getXSize()): - for y in range(heightfield.getYSize()): - # 将0-1范围的值转换为0-65535范围 - value = int(heightfield.getRed(x, y) * 65535) - temp_image.setGray(x, y, value) - - heightfield = temp_image - - # 获取文件扩展名 - file_ext = os.path.splitext(output_path)[1].lower() - if not file_ext: - output_path += f".{format}" - - # 写入高度图文件 - success = heightfield.write(Filename.fromOsSpecific(output_path)) - if success: - print(f"高度图导出成功: {output_path} (位深度: {bit_depth})") - return True - else: - print(f"高度图导出失败: {output_path}") - return False - else: - print("地形中没有高度图数据") - return False - else: - print("地形信息中没有高度图数据") - return False - except Exception as e: - print(f"导出高度图时出错: {e}") - return False - - def import_raw_heightmap(self, raw_path, width, height, bit_depth=8, scale=(1, 1, 1)): - """ - 导入RAW格式的高度图 - """ - try: - if not os.path.exists(raw_path): - print(f"RAW文件不存在: {raw_path}") - return None - - # 读取RAW数据 - with open(raw_path, 'rb') as f: - if bit_depth == 8: - data = np.fromfile(f, dtype=np.uint8) - elif bit_depth == 16: - data = np.fromfile(f, dtype=np.uint16) - else: - print(f"不支持的位深度: {bit_depth}") - return None - - # 检查数据大小 - if len(data) != width * height: - print(f"RAW数据大小不匹配: 期望 {width * height}, 实际 {len(data)}") - return None - - # 创建PNM图像 - heightfield = PNMImage(width, height) - - # 填充高度数据 - if bit_depth == 8: - max_val = 255.0 - else: # 16位 - max_val = 65535.0 - - for y in range(height): - for x in range(width): - # RAW文件通常是行优先存储 - index = y * width + x - value = data[index] / max_val - heightfield.setRed(x, y, value) - heightfield.setGreen(x, y, value) - heightfield.setBlue(x, y, value) - - # 保存到临时文件 - temp_path = f"/tmp/temp_heightmap_{int(datetime.now().timestamp())}.png" - heightfield.write(Filename.fromOsSpecific(temp_path)) - - # 使用现有方法创建地形 - return self.import_heightmap(temp_path, scale) - - except Exception as e: - print(f"导入RAW高度图时出错: {e}") - return None - - def export_raw_heightmap(self, terrain_info, output_path, bit_depth=16): - """ - 导出地形为RAW格式的高度图 - """ - if not terrain_info or 'heightfield' not in terrain_info: - print("地形信息无效或没有高度图数据") - return False - - try: - heightfield = terrain_info['heightfield'] - width = heightfield.getXSize() - height = heightfield.getYSize() - - # 确保输出目录存在 - output_dir = os.path.dirname(output_path) - if not os.path.exists(output_dir): - os.makedirs(output_dir) - - # 创建数据数组 - if bit_depth == 8: - data = np.zeros(width * height, dtype=np.uint8) - max_val = 255.0 - elif bit_depth == 16: - data = np.zeros(width * height, dtype=np.uint16) - max_val = 65535.0 - else: - print(f"不支持的位深度: {bit_depth}") - return False - - # 填充数据 - for y in range(height): - for x in range(width): - value = heightfield.getRed(x, y) * max_val - index = y * width + x - data[index] = int(value) - - # 写入RAW文件 - with open(output_path, 'wb') as f: - data.tofile(f) - - print(f"RAW高度图导出成功: {output_path} ({width}x{height}, {bit_depth}位)") - return True - - except Exception as e: - print(f"导出RAW高度图时出错: {e}") - return False - - def import_terrain_data(self, terrain_data_path): - """ - 导入地形数据文件(JSON格式) - """ - try: - if not os.path.exists(terrain_data_path): - print(f"地形数据文件不存在: {terrain_data_path}") - return None - - with open(terrain_data_path, 'r', encoding='utf-8') as f: - terrain_data = json.load(f) - - # 解析地形数据并创建地形 - terrain_type = terrain_data.get('type', 'heightmap') - - if terrain_type == 'heightmap' and 'heightmap_path' in terrain_data: - heightmap_path = terrain_data['heightmap_path'] - # 检查高度图路径是否为相对路径 - if not os.path.isabs(heightmap_path): - # 转换为绝对路径 - data_dir = os.path.dirname(terrain_data_path) - heightmap_path = os.path.join(data_dir, heightmap_path) - - scale = tuple(terrain_data.get('scale', (1, 1, 1))) - resolution = terrain_data.get('resolution') - - return self.import_heightmap(heightmap_path, scale, resolution) - - elif terrain_type == 'procedural': - # 创建程序化地形 - return self._create_procedural_terrain(terrain_data) - - else: - print(f"不支持的地形类型: {terrain_type}") - return None - - except Exception as e: - print(f"导入地形数据时出错: {e}") - return None - - def export_terrain_data(self, terrain_info, output_path, include_heightmap=True): - """ - 导出地形数据文件(JSON格式) - """ - if not terrain_info: - print("地形信息无效") - return False - - try: - # 收集地形信息 - terrain_data = { - 'name': terrain_info.get('name', f'terrain_{int(datetime.now().timestamp())}'), - 'scale': terrain_info.get('scale', (1, 1, 1)), - 'type': 'heightmap' if terrain_info.get('heightmap') else 'procedural', - 'created_at': datetime.now().isoformat(), - 'resolution': { - 'width': terrain_info['heightfield'].getXSize() if terrain_info.get('heightfield') else 0, - 'height': terrain_info['heightfield'].getYSize() if terrain_info.get('heightfield') else 0 - }, - 'metadata': terrain_info.get('metadata', {}) - } - - # 添加高度图信息 - if terrain_info.get('heightmap'): - heightmap_path = terrain_info['heightmap'] - terrain_data['heightmap_path'] = os.path.basename(heightmap_path) - - # 确保输出目录存在 - output_dir = os.path.dirname(output_path) - if not os.path.exists(output_dir): - os.makedirs(output_dir) - - # 写入地形数据文件 - with open(output_path, 'w', encoding='utf-8') as f: - json.dump(terrain_data, f, indent=2, ensure_ascii=False) - - print(f"地形数据导出成功: {output_path}") - - # 如果需要,也导出高度图 - if include_heightmap and terrain_info.get('heightmap'): - heightmap_output = output_path.replace('.json', '_heightmap.png') - return self.export_heightmap(terrain_info, heightmap_output) - - return True - - except Exception as e: - print(f"导出地形数据时出错: {e}") - return False - - def batch_import(self, import_configs): - """ - 批量导入地形 - import_configs: 导入配置列表,每个配置包含路径和参数 - """ - imported_terrains = [] - - for config in import_configs: - path = config.get('path') - if not path: - continue - - # 根据文件扩展名确定导入方法 - file_ext = os.path.splitext(path)[1].lower() - - if file_ext in ['.json']: - # 导入地形数据文件 - terrain = self.import_terrain_data(path) - elif file_ext in ['.raw']: - # 导入RAW格式 - width = config.get('width', 129) - height = config.get('height', 129) - bit_depth = config.get('bit_depth', 8) - scale = config.get('scale', (1, 1, 1)) - terrain = self.import_raw_heightmap(path, width, height, bit_depth, scale) - else: - # 导入图像格式高度图 - scale = config.get('scale', (1, 1, 1)) - resolution = config.get('resolution') - terrain = self.import_heightmap(path, scale, resolution) - - if terrain: - imported_terrains.append(terrain) - - print(f"批量导入完成,成功导入 {len(imported_terrains)} 个地形") - return imported_terrains - - def batch_export(self, terrain_infos, output_dir, export_format='json', include_heightmap=True): - """ - 批量导出地形 - """ - if not os.path.exists(output_dir): - os.makedirs(output_dir) - - exported_count = 0 - - for i, terrain_info in enumerate(terrain_infos): - if export_format.lower() == 'json': - output_path = os.path.join(output_dir, f"terrain_{i}_data.json") - if self.export_terrain_data(terrain_info, output_path, include_heightmap): - exported_count += 1 - else: - # 导出为高度图 - output_path = os.path.join(output_dir, f"terrain_{i}_heightmap.png") - if self.export_heightmap(terrain_info, output_path): - exported_count += 1 - - print(f"批量导出完成,成功导出 {exported_count} 个地形") - return exported_count - - def _create_procedural_terrain(self, terrain_data): - """ - 创建程序化地形 - """ - try: - if not hasattr(self.world, 'terrain_manager'): - print("地形管理器不可用") - return None - - # 获取参数 - scale = tuple(terrain_data.get('scale', (1, 1, 1))) - resolution = terrain_data.get('resolution', 129) - algorithm = terrain_data.get('algorithm', 'perlin') - - # 创建平面地形作为基础 - terrain = self.world.terrain_manager.createFlatTerrain( - size=(scale[0], scale[1]), - resolution=resolution - ) - - if terrain: - # 应用程序化算法修改高度 - self._apply_procedural_algorithm(terrain, algorithm, terrain_data) - - return terrain - - except Exception as e: - print(f"创建程序化地形时出错: {e}") - return None - - def _apply_procedural_algorithm(self, terrain, algorithm, params): - """ - 应用程序化算法生成地形 - """ - print(f"应用程序化算法: {algorithm}") - # 这里可以实现各种程序化地形生成算法 - # 如Perlin噪声、Voronoi图、分形等 - - def import_terragen_terrain(self, ter_path, scale=(1, 1, 1)): - """ - 导入Terragen格式的地形文件 - """ - try: - if not os.path.exists(ter_path): - print(f"Terragen文件不存在: {ter_path}") - return None - - # 解析Terragen文件格式 - terrain_data = self._parse_terragen_file(ter_path) - if not terrain_data: - return None - - # 创建高度图 - width = terrain_data['width'] - height = terrain_data['height'] - heightfield = PNMImage(width, height) - - # 填充高度数据 - heights = terrain_data['heights'] - for y in range(height): - for x in range(width): - index = y * width + x - if index < len(heights): - # 将高度值归一化到0-1范围 - normalized_height = (heights[index] - terrain_data['min_height']) / (terrain_data['max_height'] - terrain_data['min_height']) - heightfield.setRed(x, y, normalized_height) - heightfield.setGreen(x, y, normalized_height) - heightfield.setBlue(x, y, normalized_height) - - # 保存到临时文件 - temp_path = f"/tmp/terragen_heightmap_{int(datetime.now().timestamp())}.png" - heightfield.write(Filename.fromOsSpecific(temp_path)) - - # 使用现有方法创建地形 - return self.import_heightmap(temp_path, scale) - - except Exception as e: - print(f"导入Terragen地形时出错: {e}") - return None - - def _parse_terragen_file(self, ter_path): - """ - 解析Terragen文件 - """ - try: - with open(ter_path, 'rb') as f: - # 读取文件头 - header = f.read(16) - if header[:8] != b'TERRAGEN': - print("无效的Terragen文件") - return None - - # 解析版本和参数 - version = int.from_bytes(header[8:12], byteorder='little') - width = int.from_bytes(header[12:16], byteorder='little') - - # 读取高度数据 - heights = [] - min_height = float('inf') - max_height = float('-inf') - - while True: - chunk_header = f.read(4) - if not chunk_header: - break - - chunk_type = chunk_header - chunk_size = int.from_bytes(f.read(4), byteorder='little') - - if chunk_type == b'ALTW': - # 高度数据块 - for _ in range(chunk_size // 2): - height = int.from_bytes(f.read(2), byteorder='little', signed=True) - heights.append(height) - min_height = min(min_height, height) - max_height = max(max_height, height) - else: - # 跳过其他块 - f.seek(chunk_size, 1) - - return { - 'width': width, - 'height': width, # Terragen通常是方形 - 'heights': heights, - 'min_height': min_height, - 'max_height': max_height - } - - except Exception as e: - print(f"解析Terragen文件时出错: {e}") - return None - - def export_terrain_mesh(self, terrain_info, output_path, format='obj'): - """ - 导出地形为网格文件(OBJ等格式) - """ - print(f"导出地形网格为 {format} 格式") - # 这里可以实现将地形导出为网格文件的功能 +""" +地形导入导出模块 +提供完整的地形数据导入导出功能,支持多种格式和批量操作 +""" + +import os +import json +import math +import numpy as np +from datetime import datetime +from panda3d.core import PNMImage, Filename, PNMFileTypeRegistry, PNMFileType + +class TerrainIO: + """ + 地形导入导出类 + 提供完整的地形数据导入导出功能 + """ + + def __init__(self, world): + self.world = world + self.supported_formats = ['.png', '.jpg', '.jpeg', '.bmp', '.tga', '.raw', '.ter'] + + def import_heightmap(self, heightmap_path, scale=(1, 1, 1), resolution=None): + """ + 从高度图导入地形 + 支持多种图像格式的高度图 + """ + if not os.path.exists(heightmap_path): + print(f"高度图文件不存在: {heightmap_path}") + return None + + try: + # 检查文件格式 + file_ext = os.path.splitext(heightmap_path)[1].lower() + if file_ext not in self.supported_formats: + print(f"不支持的高度图格式: {file_ext}") + return None + + # 使用现有的地形管理器创建地形 + if hasattr(self.world, 'terrain_manager'): + terrain = self.world.terrain_manager.createTerrainFromHeightMap(heightmap_path, scale) + return terrain + else: + print("地形管理器不可用") + return None + + except Exception as e: + print(f"导入高度图时出错: {e}") + return None + + def export_heightmap(self, terrain_info, output_path, format='png', bit_depth=8): + """ + 导出地形为高度图 + 支持多种格式和位深度 + """ + if not terrain_info: + print("地形信息无效") + return False + + try: + # 从地形信息中获取高度图数据 + if 'heightfield' in terrain_info: + heightfield = terrain_info['heightfield'] + if heightfield: + # 确保输出目录存在 + output_dir = os.path.dirname(output_path) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + # 根据位深度调整高度图 + if bit_depth == 16: + # 转换为16位 + temp_image = PNMImage(heightfield.getXSize(), heightfield.getYSize(), 1) # 灰度图 + temp_image.setMaxval(65535) # 16位最大值 + + for x in range(heightfield.getXSize()): + for y in range(heightfield.getYSize()): + # 将0-1范围的值转换为0-65535范围 + value = int(heightfield.getRed(x, y) * 65535) + temp_image.setGray(x, y, value) + + heightfield = temp_image + + # 获取文件扩展名 + file_ext = os.path.splitext(output_path)[1].lower() + if not file_ext: + output_path += f".{format}" + + # 写入高度图文件 + success = heightfield.write(Filename.fromOsSpecific(output_path)) + if success: + print(f"高度图导出成功: {output_path} (位深度: {bit_depth})") + return True + else: + print(f"高度图导出失败: {output_path}") + return False + else: + print("地形中没有高度图数据") + return False + else: + print("地形信息中没有高度图数据") + return False + except Exception as e: + print(f"导出高度图时出错: {e}") + return False + + def import_raw_heightmap(self, raw_path, width, height, bit_depth=8, scale=(1, 1, 1)): + """ + 导入RAW格式的高度图 + """ + try: + if not os.path.exists(raw_path): + print(f"RAW文件不存在: {raw_path}") + return None + + # 读取RAW数据 + with open(raw_path, 'rb') as f: + if bit_depth == 8: + data = np.fromfile(f, dtype=np.uint8) + elif bit_depth == 16: + data = np.fromfile(f, dtype=np.uint16) + else: + print(f"不支持的位深度: {bit_depth}") + return None + + # 检查数据大小 + if len(data) != width * height: + print(f"RAW数据大小不匹配: 期望 {width * height}, 实际 {len(data)}") + return None + + # 创建PNM图像 + heightfield = PNMImage(width, height) + + # 填充高度数据 + if bit_depth == 8: + max_val = 255.0 + else: # 16位 + max_val = 65535.0 + + for y in range(height): + for x in range(width): + # RAW文件通常是行优先存储 + index = y * width + x + value = data[index] / max_val + heightfield.setRed(x, y, value) + heightfield.setGreen(x, y, value) + heightfield.setBlue(x, y, value) + + # 保存到临时文件 + temp_path = f"/tmp/temp_heightmap_{int(datetime.now().timestamp())}.png" + heightfield.write(Filename.fromOsSpecific(temp_path)) + + # 使用现有方法创建地形 + return self.import_heightmap(temp_path, scale) + + except Exception as e: + print(f"导入RAW高度图时出错: {e}") + return None + + def export_raw_heightmap(self, terrain_info, output_path, bit_depth=16): + """ + 导出地形为RAW格式的高度图 + """ + if not terrain_info or 'heightfield' not in terrain_info: + print("地形信息无效或没有高度图数据") + return False + + try: + heightfield = terrain_info['heightfield'] + width = heightfield.getXSize() + height = heightfield.getYSize() + + # 确保输出目录存在 + output_dir = os.path.dirname(output_path) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + # 创建数据数组 + if bit_depth == 8: + data = np.zeros(width * height, dtype=np.uint8) + max_val = 255.0 + elif bit_depth == 16: + data = np.zeros(width * height, dtype=np.uint16) + max_val = 65535.0 + else: + print(f"不支持的位深度: {bit_depth}") + return False + + # 填充数据 + for y in range(height): + for x in range(width): + value = heightfield.getRed(x, y) * max_val + index = y * width + x + data[index] = int(value) + + # 写入RAW文件 + with open(output_path, 'wb') as f: + data.tofile(f) + + print(f"RAW高度图导出成功: {output_path} ({width}x{height}, {bit_depth}位)") + return True + + except Exception as e: + print(f"导出RAW高度图时出错: {e}") + return False + + def import_terrain_data(self, terrain_data_path): + """ + 导入地形数据文件(JSON格式) + """ + try: + if not os.path.exists(terrain_data_path): + print(f"地形数据文件不存在: {terrain_data_path}") + return None + + with open(terrain_data_path, 'r', encoding='utf-8') as f: + terrain_data = json.load(f) + + # 解析地形数据并创建地形 + terrain_type = terrain_data.get('type', 'heightmap') + + if terrain_type == 'heightmap' and 'heightmap_path' in terrain_data: + heightmap_path = terrain_data['heightmap_path'] + # 检查高度图路径是否为相对路径 + if not os.path.isabs(heightmap_path): + # 转换为绝对路径 + data_dir = os.path.dirname(terrain_data_path) + heightmap_path = os.path.join(data_dir, heightmap_path) + + scale = tuple(terrain_data.get('scale', (1, 1, 1))) + resolution = terrain_data.get('resolution') + + return self.import_heightmap(heightmap_path, scale, resolution) + + elif terrain_type == 'procedural': + # 创建程序化地形 + return self._create_procedural_terrain(terrain_data) + + else: + print(f"不支持的地形类型: {terrain_type}") + return None + + except Exception as e: + print(f"导入地形数据时出错: {e}") + return None + + def export_terrain_data(self, terrain_info, output_path, include_heightmap=True): + """ + 导出地形数据文件(JSON格式) + """ + if not terrain_info: + print("地形信息无效") + return False + + try: + # 收集地形信息 + terrain_data = { + 'name': terrain_info.get('name', f'terrain_{int(datetime.now().timestamp())}'), + 'scale': terrain_info.get('scale', (1, 1, 1)), + 'type': 'heightmap' if terrain_info.get('heightmap') else 'procedural', + 'created_at': datetime.now().isoformat(), + 'resolution': { + 'width': terrain_info['heightfield'].getXSize() if terrain_info.get('heightfield') else 0, + 'height': terrain_info['heightfield'].getYSize() if terrain_info.get('heightfield') else 0 + }, + 'metadata': terrain_info.get('metadata', {}) + } + + # 添加高度图信息 + if terrain_info.get('heightmap'): + heightmap_path = terrain_info['heightmap'] + terrain_data['heightmap_path'] = os.path.basename(heightmap_path) + + # 确保输出目录存在 + output_dir = os.path.dirname(output_path) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + # 写入地形数据文件 + with open(output_path, 'w', encoding='utf-8') as f: + json.dump(terrain_data, f, indent=2, ensure_ascii=False) + + print(f"地形数据导出成功: {output_path}") + + # 如果需要,也导出高度图 + if include_heightmap and terrain_info.get('heightmap'): + heightmap_output = output_path.replace('.json', '_heightmap.png') + return self.export_heightmap(terrain_info, heightmap_output) + + return True + + except Exception as e: + print(f"导出地形数据时出错: {e}") + return False + + def batch_import(self, import_configs): + """ + 批量导入地形 + import_configs: 导入配置列表,每个配置包含路径和参数 + """ + imported_terrains = [] + + for config in import_configs: + path = config.get('path') + if not path: + continue + + # 根据文件扩展名确定导入方法 + file_ext = os.path.splitext(path)[1].lower() + + if file_ext in ['.json']: + # 导入地形数据文件 + terrain = self.import_terrain_data(path) + elif file_ext in ['.raw']: + # 导入RAW格式 + width = config.get('width', 129) + height = config.get('height', 129) + bit_depth = config.get('bit_depth', 8) + scale = config.get('scale', (1, 1, 1)) + terrain = self.import_raw_heightmap(path, width, height, bit_depth, scale) + else: + # 导入图像格式高度图 + scale = config.get('scale', (1, 1, 1)) + resolution = config.get('resolution') + terrain = self.import_heightmap(path, scale, resolution) + + if terrain: + imported_terrains.append(terrain) + + print(f"批量导入完成,成功导入 {len(imported_terrains)} 个地形") + return imported_terrains + + def batch_export(self, terrain_infos, output_dir, export_format='json', include_heightmap=True): + """ + 批量导出地形 + """ + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + exported_count = 0 + + for i, terrain_info in enumerate(terrain_infos): + if export_format.lower() == 'json': + output_path = os.path.join(output_dir, f"terrain_{i}_data.json") + if self.export_terrain_data(terrain_info, output_path, include_heightmap): + exported_count += 1 + else: + # 导出为高度图 + output_path = os.path.join(output_dir, f"terrain_{i}_heightmap.png") + if self.export_heightmap(terrain_info, output_path): + exported_count += 1 + + print(f"批量导出完成,成功导出 {exported_count} 个地形") + return exported_count + + def _create_procedural_terrain(self, terrain_data): + """ + 创建程序化地形 + """ + try: + if not hasattr(self.world, 'terrain_manager'): + print("地形管理器不可用") + return None + + # 获取参数 + scale = tuple(terrain_data.get('scale', (1, 1, 1))) + resolution = terrain_data.get('resolution', 129) + algorithm = terrain_data.get('algorithm', 'perlin') + + # 创建平面地形作为基础 + terrain = self.world.terrain_manager.createFlatTerrain( + size=(scale[0], scale[1]), + resolution=resolution + ) + + if terrain: + # 应用程序化算法修改高度 + self._apply_procedural_algorithm(terrain, algorithm, terrain_data) + + return terrain + + except Exception as e: + print(f"创建程序化地形时出错: {e}") + return None + + def _apply_procedural_algorithm(self, terrain, algorithm, params): + """ + 应用程序化算法生成地形 + """ + print(f"应用程序化算法: {algorithm}") + # 这里可以实现各种程序化地形生成算法 + # 如Perlin噪声、Voronoi图、分形等 + + def import_terragen_terrain(self, ter_path, scale=(1, 1, 1)): + """ + 导入Terragen格式的地形文件 + """ + try: + if not os.path.exists(ter_path): + print(f"Terragen文件不存在: {ter_path}") + return None + + # 解析Terragen文件格式 + terrain_data = self._parse_terragen_file(ter_path) + if not terrain_data: + return None + + # 创建高度图 + width = terrain_data['width'] + height = terrain_data['height'] + heightfield = PNMImage(width, height) + + # 填充高度数据 + heights = terrain_data['heights'] + for y in range(height): + for x in range(width): + index = y * width + x + if index < len(heights): + # 将高度值归一化到0-1范围 + normalized_height = (heights[index] - terrain_data['min_height']) / (terrain_data['max_height'] - terrain_data['min_height']) + heightfield.setRed(x, y, normalized_height) + heightfield.setGreen(x, y, normalized_height) + heightfield.setBlue(x, y, normalized_height) + + # 保存到临时文件 + temp_path = f"/tmp/terragen_heightmap_{int(datetime.now().timestamp())}.png" + heightfield.write(Filename.fromOsSpecific(temp_path)) + + # 使用现有方法创建地形 + return self.import_heightmap(temp_path, scale) + + except Exception as e: + print(f"导入Terragen地形时出错: {e}") + return None + + def _parse_terragen_file(self, ter_path): + """ + 解析Terragen文件 + """ + try: + with open(ter_path, 'rb') as f: + # 读取文件头 + header = f.read(16) + if header[:8] != b'TERRAGEN': + print("无效的Terragen文件") + return None + + # 解析版本和参数 + version = int.from_bytes(header[8:12], byteorder='little') + width = int.from_bytes(header[12:16], byteorder='little') + + # 读取高度数据 + heights = [] + min_height = float('inf') + max_height = float('-inf') + + while True: + chunk_header = f.read(4) + if not chunk_header: + break + + chunk_type = chunk_header + chunk_size = int.from_bytes(f.read(4), byteorder='little') + + if chunk_type == b'ALTW': + # 高度数据块 + for _ in range(chunk_size // 2): + height = int.from_bytes(f.read(2), byteorder='little', signed=True) + heights.append(height) + min_height = min(min_height, height) + max_height = max(max_height, height) + else: + # 跳过其他块 + f.seek(chunk_size, 1) + + return { + 'width': width, + 'height': width, # Terragen通常是方形 + 'heights': heights, + 'min_height': min_height, + 'max_height': max_height + } + + except Exception as e: + print(f"解析Terragen文件时出错: {e}") + return None + + def export_terrain_mesh(self, terrain_info, output_path, format='obj'): + """ + 导出地形为网格文件(OBJ等格式) + """ + print(f"导出地形网格为 {format} 格式") + # 这里可以实现将地形导出为网格文件的功能 return False \ No newline at end of file diff --git a/plugins/user/terrain_editor/materials/__init__.py b/plugins/user/terrain_editor/materials/__init__.py index 1e126ac5..b8609b1e 100644 --- a/plugins/user/terrain_editor/materials/__init__.py +++ b/plugins/user/terrain_editor/materials/__init__.py @@ -1,3 +1,3 @@ -""" -地形材质模块 +""" +地形材质模块 """ \ No newline at end of file diff --git a/plugins/user/terrain_editor/materials/terrain_layer_system.py b/plugins/user/terrain_editor/materials/terrain_layer_system.py index 9dda89a1..e6f69b28 100644 --- a/plugins/user/terrain_editor/materials/terrain_layer_system.py +++ b/plugins/user/terrain_editor/materials/terrain_layer_system.py @@ -1,391 +1,391 @@ -""" -地形图层系统 -提供完整的地形图层管理功能,支持多层纹理混合、Alpha混合等高级功能 -""" - -from panda3d.core import Texture, TextureStage, PNMImage, Filename -import os - -class TerrainLayerSystem: - """ - 地形图层系统类 - 提供完整的地形图层管理功能 - """ - - def __init__(self, world): - self.world = world - self.terrain_layers = {} # 存储地形图层信息 - - def create_layered_terrain(self, terrain_node, layer_configs): - """ - 创建分层地形 - layer_configs: 图层配置列表,每个配置包含纹理路径、混合贴图等信息 - """ - try: - # 清除现有的地形纹理 - terrain_node.clearTexture() - - # 为每个图层创建纹理阶段 - for i, layer_config in enumerate(layer_configs): - # 加载漫反射纹理 - if 'diffuse' in layer_config: - diffuse_texture = self._load_texture(layer_config['diffuse']) - if diffuse_texture: - diffuse_stage = TextureStage(f"diffuse_{i}") - diffuse_stage.setSort(i * 10) # 设置排序 - terrain_node.setTexture(diffuse_stage, diffuse_texture) - - # 加载法线纹理 - if 'normal' in layer_config: - normal_texture = self._load_texture(layer_config['normal']) - if normal_texture: - normal_stage = TextureStage(f"normal_{i}") - normal_stage.setMode(TextureStage.M_normal) # 设置为法线贴图模式 - normal_stage.setSort(i * 10 + 1) - terrain_node.setTexture(normal_stage, normal_texture) - - # 加载高光纹理 - if 'specular' in layer_config: - specular_texture = self._load_texture(layer_config['specular']) - if specular_texture: - specular_stage = TextureStage(f"specular_{i}") - specular_stage.setMode(TextureStage.M_specular) # 设置为高光贴图模式 - specular_stage.setSort(i * 10 + 2) - terrain_node.setTexture(specular_stage, specular_texture) - - # 加载混合纹理(控制图层混合) - if 'blend' in layer_config: - blend_texture = self._load_texture(layer_config['blend']) - if blend_texture: - blend_stage = TextureStage(f"blend_{i}") - blend_stage.setMode(TextureStage.M_blend) # 设置为混合模式 - blend_stage.setSort(i * 10 + 3) - terrain_node.setTexture(blend_stage, blend_texture) - - # 加载Alpha纹理(控制透明度) - if 'alpha' in layer_config: - alpha_texture = self._load_texture(layer_config['alpha']) - if alpha_texture: - alpha_stage = TextureStage(f"alpha_{i}") - alpha_stage.setMode(TextureStage.M_modulate) # 设置为调制模式 - alpha_stage.setSort(i * 10 + 4) - terrain_node.setTexture(alpha_stage, alpha_texture) - - # 保存图层信息 - self.terrain_layers[terrain_node] = layer_configs - - print(f"为地形创建 {len(layer_configs)} 个图层系统") - return True - - except Exception as e: - print(f"创建分层地形时出错: {e}") - return False - - def add_layer(self, terrain_node, layer_config, layer_index=None): - """ - 为地形添加新图层 - """ - try: - if terrain_node not in self.terrain_layers: - # 如果地形还没有图层系统,创建一个新的 - self.terrain_layers[terrain_node] = [] - - layers = self.terrain_layers[terrain_node] - - # 确定插入位置 - if layer_index is None: - layer_index = len(layers) - - # 插入新图层 - layers.insert(layer_index, layer_config) - - # 重新应用所有图层 - return self.create_layered_terrain(terrain_node, layers) - - except Exception as e: - print(f"添加图层时出错: {e}") - return False - - def remove_layer(self, terrain_node, layer_index): - """ - 移除地形的指定图层 - """ - try: - if terrain_node not in self.terrain_layers: - print("地形没有图层系统") - return False - - layers = self.terrain_layers[terrain_node] - - if layer_index < 0 or layer_index >= len(layers): - print("图层索引超出范围") - return False - - # 移除图层 - layers.pop(layer_index) - - # 重新应用所有图层 - return self.create_layered_terrain(terrain_node, layers) - - except Exception as e: - print(f"移除图层时出错: {e}") - return False - - def update_layer(self, terrain_node, layer_index, layer_config): - """ - 更新地形图层 - """ - try: - if terrain_node not in self.terrain_layers: - print("地形没有图层系统") - return False - - layers = self.terrain_layers[terrain_node] - - if layer_index < 0 or layer_index >= len(layers): - print("图层索引超出范围") - return False - - # 更新图层配置 - layers[layer_index] = layer_config - - # 重新应用所有图层 - return self.create_layered_terrain(terrain_node, layers) - - except Exception as e: - print(f"更新图层时出错: {e}") - return False - - def get_layer_info(self, terrain_node, layer_index): - """ - 获取图层信息 - """ - try: - if terrain_node not in self.terrain_layers: - print("地形没有图层系统") - return None - - layers = self.terrain_layers[terrain_node] - - if layer_index < 0 or layer_index >= len(layers): - print("图层索引超出范围") - return None - - return layers[layer_index] - - except Exception as e: - print(f"获取图层信息时出错: {e}") - return None - - def blend_layers(self, terrain_node, blend_map_path): - """ - 使用混合图混合所有图层 - """ - try: - if terrain_node not in self.terrain_layers: - print("地形没有图层系统") - return False - - # 加载混合图 - blend_texture = self._load_texture(blend_map_path) - if not blend_texture: - print("无法加载混合图") - return False - - # 应用混合图到所有图层 - blend_stage = TextureStage("global_blend") - blend_stage.setMode(TextureStage.M_blend) - blend_stage.setSort(1000) # 确保在最后应用 - terrain_node.setTexture(blend_stage, blend_texture) - - print(f"应用全局混合图: {blend_map_path}") - return True - - except Exception as e: - print(f"混合图层时出错: {e}") - return False - - def generate_blend_map(self, terrain_info, layer_weights, output_path): - """ - 生成图层混合图 - layer_weights: 每个图层的权重列表 - """ - try: - heightfield = terrain_info['heightfield'] - if not heightfield: - print("无法获取地形高度图数据") - return False - - width = heightfield.getXSize() - height = heightfield.getYSize() - - # 创建混合图 - blend_image = PNMImage(width, height, 4) # RGBA格式 - - # 根据高度和权重生成混合图 - for y in range(height): - for x in range(width): - height_value = heightfield.getRed(x, y) - - # 根据高度值分配权重 - weights = self._calculate_layer_weights(height_value, layer_weights) - - # 设置RGBA值 - blend_image.setRed(x, y, weights[0] if len(weights) > 0 else 0) - blend_image.setGreen(x, y, weights[1] if len(weights) > 1 else 0) - blend_image.setBlue(x, y, weights[2] if len(weights) > 2 else 0) - blend_image.setAlpha(x, y, weights[3] if len(weights) > 3 else 0) - - # 保存混合图 - output_dir = os.path.dirname(output_path) - if not os.path.exists(output_dir): - os.makedirs(output_dir) - - success = blend_image.write(Filename.fromOsSpecific(output_path)) - if success: - print(f"混合图生成成功: {output_path}") - return True - else: - print(f"混合图生成失败: {output_path}") - return False - - except Exception as e: - print(f"生成混合图时出错: {e}") - return False - - def _calculate_layer_weights(self, height_value, layer_weights): - """ - 根据高度值计算图层权重 - """ - # 简单的高度分区方法 - weights = [0.0] * len(layer_weights) - - if height_value < 0.3: # 低地 - weights[0] = 1.0 - elif height_value < 0.6: # 中等地 - weights[1] = 1.0 - elif height_value < 0.8: # 高地 - weights[2] = 1.0 - else: # 山峰 - weights[3] = 1.0 if len(weights) > 3 else 0 - - return weights - - def _load_texture(self, texture_path): - """ - 加载纹理 - """ - try: - if not os.path.exists(texture_path): - print(f"纹理文件不存在: {texture_path}") - return None - - texture = self.world.loader.loadTexture(texture_path) - if texture: - print(f"纹理加载成功: {texture_path}") - return texture - else: - print(f"纹理加载失败: {texture_path}") - return None - except Exception as e: - print(f"加载纹理时出错: {e}") - return None - - def create_texture_array(self, texture_paths, array_name): - """ - 创建纹理数组 - """ - try: - # 加载所有纹理 - textures = [] - for path in texture_paths: - texture = self._load_texture(path) - if texture: - textures.append(texture) - - if not textures: - print("没有成功加载任何纹理") - return None - - # 创建纹理数组 - # 注意:Panda3D的纹理数组创建比较复杂,这里只是一个概念实现 - print(f"创建纹理数组: {array_name},包含 {len(textures)} 个纹理") - return textures # 临时返回纹理列表 - - except Exception as e: - print(f"创建纹理数组时出错: {e}") - return None - - def apply_texture_array(self, terrain_node, texture_array, array_name): - """ - 应用纹理数组到地形 - """ - try: - if not texture_array: - print("纹理数组为空") - return False - - # 为每个纹理创建纹理阶段 - for i, texture in enumerate(texture_array): - texture_stage = TextureStage(f"{array_name}_{i}") - texture_stage.setSort(i) - terrain_node.setTexture(texture_stage, texture) - - print(f"应用纹理数组: {array_name}") - return True - - except Exception as e: - print(f"应用纹理数组时出错: {e}") - return False - - def save_layer_config(self, terrain_node, config_path): - """ - 保存图层配置到文件 - """ - try: - if terrain_node not in self.terrain_layers: - print("地形没有图层系统") - return False - - import json - - # 保存图层配置 - layers = self.terrain_layers[terrain_node] - - # 确保输出目录存在 - output_dir = os.path.dirname(config_path) - if not os.path.exists(output_dir): - os.makedirs(output_dir) - - with open(config_path, 'w', encoding='utf-8') as f: - json.dump(layers, f, indent=2, ensure_ascii=False) - - print(f"图层配置保存成功: {config_path}") - return True - - except Exception as e: - print(f"保存图层配置时出错: {e}") - return False - - def load_layer_config(self, terrain_node, config_path): - """ - 从文件加载图层配置 - """ - try: - if not os.path.exists(config_path): - print(f"图层配置文件不存在: {config_path}") - return False - - import json - - # 加载图层配置 - with open(config_path, 'r', encoding='utf-8') as f: - layers = json.load(f) - - # 应用图层配置 - self.terrain_layers[terrain_node] = layers - return self.create_layered_terrain(terrain_node, layers) - - except Exception as e: - print(f"加载图层配置时出错: {e}") +""" +地形图层系统 +提供完整的地形图层管理功能,支持多层纹理混合、Alpha混合等高级功能 +""" + +from panda3d.core import Texture, TextureStage, PNMImage, Filename +import os + +class TerrainLayerSystem: + """ + 地形图层系统类 + 提供完整的地形图层管理功能 + """ + + def __init__(self, world): + self.world = world + self.terrain_layers = {} # 存储地形图层信息 + + def create_layered_terrain(self, terrain_node, layer_configs): + """ + 创建分层地形 + layer_configs: 图层配置列表,每个配置包含纹理路径、混合贴图等信息 + """ + try: + # 清除现有的地形纹理 + terrain_node.clearTexture() + + # 为每个图层创建纹理阶段 + for i, layer_config in enumerate(layer_configs): + # 加载漫反射纹理 + if 'diffuse' in layer_config: + diffuse_texture = self._load_texture(layer_config['diffuse']) + if diffuse_texture: + diffuse_stage = TextureStage(f"diffuse_{i}") + diffuse_stage.setSort(i * 10) # 设置排序 + terrain_node.setTexture(diffuse_stage, diffuse_texture) + + # 加载法线纹理 + if 'normal' in layer_config: + normal_texture = self._load_texture(layer_config['normal']) + if normal_texture: + normal_stage = TextureStage(f"normal_{i}") + normal_stage.setMode(TextureStage.M_normal) # 设置为法线贴图模式 + normal_stage.setSort(i * 10 + 1) + terrain_node.setTexture(normal_stage, normal_texture) + + # 加载高光纹理 + if 'specular' in layer_config: + specular_texture = self._load_texture(layer_config['specular']) + if specular_texture: + specular_stage = TextureStage(f"specular_{i}") + specular_stage.setMode(TextureStage.M_specular) # 设置为高光贴图模式 + specular_stage.setSort(i * 10 + 2) + terrain_node.setTexture(specular_stage, specular_texture) + + # 加载混合纹理(控制图层混合) + if 'blend' in layer_config: + blend_texture = self._load_texture(layer_config['blend']) + if blend_texture: + blend_stage = TextureStage(f"blend_{i}") + blend_stage.setMode(TextureStage.M_blend) # 设置为混合模式 + blend_stage.setSort(i * 10 + 3) + terrain_node.setTexture(blend_stage, blend_texture) + + # 加载Alpha纹理(控制透明度) + if 'alpha' in layer_config: + alpha_texture = self._load_texture(layer_config['alpha']) + if alpha_texture: + alpha_stage = TextureStage(f"alpha_{i}") + alpha_stage.setMode(TextureStage.M_modulate) # 设置为调制模式 + alpha_stage.setSort(i * 10 + 4) + terrain_node.setTexture(alpha_stage, alpha_texture) + + # 保存图层信息 + self.terrain_layers[terrain_node] = layer_configs + + print(f"为地形创建 {len(layer_configs)} 个图层系统") + return True + + except Exception as e: + print(f"创建分层地形时出错: {e}") + return False + + def add_layer(self, terrain_node, layer_config, layer_index=None): + """ + 为地形添加新图层 + """ + try: + if terrain_node not in self.terrain_layers: + # 如果地形还没有图层系统,创建一个新的 + self.terrain_layers[terrain_node] = [] + + layers = self.terrain_layers[terrain_node] + + # 确定插入位置 + if layer_index is None: + layer_index = len(layers) + + # 插入新图层 + layers.insert(layer_index, layer_config) + + # 重新应用所有图层 + return self.create_layered_terrain(terrain_node, layers) + + except Exception as e: + print(f"添加图层时出错: {e}") + return False + + def remove_layer(self, terrain_node, layer_index): + """ + 移除地形的指定图层 + """ + try: + if terrain_node not in self.terrain_layers: + print("地形没有图层系统") + return False + + layers = self.terrain_layers[terrain_node] + + if layer_index < 0 or layer_index >= len(layers): + print("图层索引超出范围") + return False + + # 移除图层 + layers.pop(layer_index) + + # 重新应用所有图层 + return self.create_layered_terrain(terrain_node, layers) + + except Exception as e: + print(f"移除图层时出错: {e}") + return False + + def update_layer(self, terrain_node, layer_index, layer_config): + """ + 更新地形图层 + """ + try: + if terrain_node not in self.terrain_layers: + print("地形没有图层系统") + return False + + layers = self.terrain_layers[terrain_node] + + if layer_index < 0 or layer_index >= len(layers): + print("图层索引超出范围") + return False + + # 更新图层配置 + layers[layer_index] = layer_config + + # 重新应用所有图层 + return self.create_layered_terrain(terrain_node, layers) + + except Exception as e: + print(f"更新图层时出错: {e}") + return False + + def get_layer_info(self, terrain_node, layer_index): + """ + 获取图层信息 + """ + try: + if terrain_node not in self.terrain_layers: + print("地形没有图层系统") + return None + + layers = self.terrain_layers[terrain_node] + + if layer_index < 0 or layer_index >= len(layers): + print("图层索引超出范围") + return None + + return layers[layer_index] + + except Exception as e: + print(f"获取图层信息时出错: {e}") + return None + + def blend_layers(self, terrain_node, blend_map_path): + """ + 使用混合图混合所有图层 + """ + try: + if terrain_node not in self.terrain_layers: + print("地形没有图层系统") + return False + + # 加载混合图 + blend_texture = self._load_texture(blend_map_path) + if not blend_texture: + print("无法加载混合图") + return False + + # 应用混合图到所有图层 + blend_stage = TextureStage("global_blend") + blend_stage.setMode(TextureStage.M_blend) + blend_stage.setSort(1000) # 确保在最后应用 + terrain_node.setTexture(blend_stage, blend_texture) + + print(f"应用全局混合图: {blend_map_path}") + return True + + except Exception as e: + print(f"混合图层时出错: {e}") + return False + + def generate_blend_map(self, terrain_info, layer_weights, output_path): + """ + 生成图层混合图 + layer_weights: 每个图层的权重列表 + """ + try: + heightfield = terrain_info['heightfield'] + if not heightfield: + print("无法获取地形高度图数据") + return False + + width = heightfield.getXSize() + height = heightfield.getYSize() + + # 创建混合图 + blend_image = PNMImage(width, height, 4) # RGBA格式 + + # 根据高度和权重生成混合图 + for y in range(height): + for x in range(width): + height_value = heightfield.getRed(x, y) + + # 根据高度值分配权重 + weights = self._calculate_layer_weights(height_value, layer_weights) + + # 设置RGBA值 + blend_image.setRed(x, y, weights[0] if len(weights) > 0 else 0) + blend_image.setGreen(x, y, weights[1] if len(weights) > 1 else 0) + blend_image.setBlue(x, y, weights[2] if len(weights) > 2 else 0) + blend_image.setAlpha(x, y, weights[3] if len(weights) > 3 else 0) + + # 保存混合图 + output_dir = os.path.dirname(output_path) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + success = blend_image.write(Filename.fromOsSpecific(output_path)) + if success: + print(f"混合图生成成功: {output_path}") + return True + else: + print(f"混合图生成失败: {output_path}") + return False + + except Exception as e: + print(f"生成混合图时出错: {e}") + return False + + def _calculate_layer_weights(self, height_value, layer_weights): + """ + 根据高度值计算图层权重 + """ + # 简单的高度分区方法 + weights = [0.0] * len(layer_weights) + + if height_value < 0.3: # 低地 + weights[0] = 1.0 + elif height_value < 0.6: # 中等地 + weights[1] = 1.0 + elif height_value < 0.8: # 高地 + weights[2] = 1.0 + else: # 山峰 + weights[3] = 1.0 if len(weights) > 3 else 0 + + return weights + + def _load_texture(self, texture_path): + """ + 加载纹理 + """ + try: + if not os.path.exists(texture_path): + print(f"纹理文件不存在: {texture_path}") + return None + + texture = self.world.loader.loadTexture(texture_path) + if texture: + print(f"纹理加载成功: {texture_path}") + return texture + else: + print(f"纹理加载失败: {texture_path}") + return None + except Exception as e: + print(f"加载纹理时出错: {e}") + return None + + def create_texture_array(self, texture_paths, array_name): + """ + 创建纹理数组 + """ + try: + # 加载所有纹理 + textures = [] + for path in texture_paths: + texture = self._load_texture(path) + if texture: + textures.append(texture) + + if not textures: + print("没有成功加载任何纹理") + return None + + # 创建纹理数组 + # 注意:Panda3D的纹理数组创建比较复杂,这里只是一个概念实现 + print(f"创建纹理数组: {array_name},包含 {len(textures)} 个纹理") + return textures # 临时返回纹理列表 + + except Exception as e: + print(f"创建纹理数组时出错: {e}") + return None + + def apply_texture_array(self, terrain_node, texture_array, array_name): + """ + 应用纹理数组到地形 + """ + try: + if not texture_array: + print("纹理数组为空") + return False + + # 为每个纹理创建纹理阶段 + for i, texture in enumerate(texture_array): + texture_stage = TextureStage(f"{array_name}_{i}") + texture_stage.setSort(i) + terrain_node.setTexture(texture_stage, texture) + + print(f"应用纹理数组: {array_name}") + return True + + except Exception as e: + print(f"应用纹理数组时出错: {e}") + return False + + def save_layer_config(self, terrain_node, config_path): + """ + 保存图层配置到文件 + """ + try: + if terrain_node not in self.terrain_layers: + print("地形没有图层系统") + return False + + import json + + # 保存图层配置 + layers = self.terrain_layers[terrain_node] + + # 确保输出目录存在 + output_dir = os.path.dirname(config_path) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + with open(config_path, 'w', encoding='utf-8') as f: + json.dump(layers, f, indent=2, ensure_ascii=False) + + print(f"图层配置保存成功: {config_path}") + return True + + except Exception as e: + print(f"保存图层配置时出错: {e}") + return False + + def load_layer_config(self, terrain_node, config_path): + """ + 从文件加载图层配置 + """ + try: + if not os.path.exists(config_path): + print(f"图层配置文件不存在: {config_path}") + return False + + import json + + # 加载图层配置 + with open(config_path, 'r', encoding='utf-8') as f: + layers = json.load(f) + + # 应用图层配置 + self.terrain_layers[terrain_node] = layers + return self.create_layered_terrain(terrain_node, layers) + + except Exception as e: + print(f"加载图层配置时出错: {e}") return False \ No newline at end of file diff --git a/plugins/user/terrain_editor/materials/terrain_materials.py b/plugins/user/terrain_editor/materials/terrain_materials.py index 534296ba..29c27d18 100644 --- a/plugins/user/terrain_editor/materials/terrain_materials.py +++ b/plugins/user/terrain_editor/materials/terrain_materials.py @@ -1,417 +1,417 @@ -""" -地形材质和纹理系统 -提供完整的地形材质和纹理管理功能,支持多层纹理混合、程序化材质生成等高级功能 -""" - -import os -from panda3d.core import Material, Texture, TextureStage, Vec4, PNMImage, Filename -from panda3d.core import GeoMipTerrain, Shader, ShaderAttrib - -class TerrainMaterials: - """ - 地形材质和纹理系统类 - 提供完整的地形材质和纹理管理功能 - """ - - def __init__(self, world): - self.world = world - self.materials = {} # 存储材质 - self.terrain_layers = {} # 存储地形图层信息 - self.texture_atlases = {} # 存储纹理图集 - - def create_material(self, name, ambient=(0.5, 0.5, 0.5, 1.0), - diffuse=(0.8, 0.8, 0.8, 1.0), - specular=(0.2, 0.2, 0.2, 1.0), - shininess=5.0, - emissive=(0.0, 0.0, 0.0, 1.0)): - """ - 创建新材质 - """ - material = Material() - material.setName(name) - material.setAmbient(ambient) - material.setDiffuse(diffuse) - material.setSpecular(specular) - material.setShininess(shininess) - material.setEmission(emissive) - - self.materials[name] = material - print(f"创建材质: {name}") - return material - - def apply_material_to_terrain(self, terrain_node, material_name): - """ - 为地形应用材质 - """ - if material_name in self.materials: - material = self.materials[material_name] - terrain_node.setMaterial(material) - print(f"为地形应用材质: {material_name}") - return True - else: - print(f"材质 {material_name} 不存在") - return False - - def load_texture(self, texture_path): - """ - 加载纹理 - """ - try: - # 检查文件是否存在 - if not os.path.exists(texture_path): - print(f"纹理文件不存在: {texture_path}") - return None - - texture = self.world.loader.loadTexture(texture_path) - if texture: - print(f"纹理加载成功: {texture_path}") - return texture - else: - print(f"纹理加载失败: {texture_path}") - return None - except Exception as e: - print(f"加载纹理时出错: {e}") - return None - - def apply_texture_to_terrain(self, terrain_node, texture_path, texture_stage_name="diffuse"): - """ - 为地形应用纹理 - """ - texture = self.load_texture(texture_path) - if texture: - # 创建纹理阶段 - texture_stage = TextureStage(texture_stage_name) - # 应用纹理到地形 - terrain_node.setTexture(texture_stage, texture) - print(f"为地形应用纹理: {texture_path}") - return True - return False - - def apply_multi_texture_to_terrain(self, terrain_node, texture_paths, blend_factors=None): - """ - 为地形应用多纹理混合 - texture_paths: 纹理路径列表 - blend_factors: 混合因子列表,控制每个纹理的混合权重 - """ - if not texture_paths: - print("纹理路径列表为空") - return False - - # 如果没有提供混合因子,使用默认值 - if blend_factors is None: - blend_factors = [1.0] * len(texture_paths) - - # 确保混合因子数量与纹理数量一致 - if len(blend_factors) != len(texture_paths): - print("混合因子数量与纹理数量不匹配") - return False - - # 应用每个纹理 - for i, (texture_path, blend_factor) in enumerate(zip(texture_paths, blend_factors)): - texture = self.load_texture(texture_path) - if texture: - # 为每个纹理创建不同的纹理阶段 - texture_stage = TextureStage(f"texture_{i}") - texture_stage.setSort(i) # 设置排序 - - # 应用纹理 - terrain_node.setTexture(texture_stage, texture) - - # 如果支持,设置混合因子 - if hasattr(texture_stage, 'setBlendFactor'): - texture_stage.setBlendFactor(blend_factor) - - print(f"为地形应用 {len(texture_paths)} 个多纹理混合") - return True - - def create_terrain_layer_system(self, terrain_node, layer_configs): - """ - 创建地形图层系统 - layer_configs: 图层配置列表,每个配置包含纹理路径、混合贴图等信息 - """ - try: - # 清除现有的地形纹理 - terrain_node.clearTexture() - - # 为每个图层创建纹理阶段 - for i, layer_config in enumerate(layer_configs): - # 加载漫反射纹理 - if 'diffuse' in layer_config: - diffuse_texture = self.load_texture(layer_config['diffuse']) - if diffuse_texture: - diffuse_stage = TextureStage(f"diffuse_{i}") - diffuse_stage.setSort(i * 10) # 设置排序 - terrain_node.setTexture(diffuse_stage, diffuse_texture) - - # 加载法线纹理 - if 'normal' in layer_config: - normal_texture = self.load_texture(layer_config['normal']) - if normal_texture: - normal_stage = TextureStage(f"normal_{i}") - normal_stage.setMode(TextureStage.M_normal) # 设置为法线贴图模式 - normal_stage.setSort(i * 10 + 1) - terrain_node.setTexture(normal_stage, normal_texture) - - # 加载混合纹理(控制图层混合) - if 'blend' in layer_config: - blend_texture = self.load_texture(layer_config['blend']) - if blend_texture: - blend_stage = TextureStage(f"blend_{i}") - blend_stage.setMode(TextureStage.M_blend) # 设置为混合模式 - blend_stage.setSort(i * 10 + 2) - terrain_node.setTexture(blend_stage, blend_texture) - - # 保存图层信息 - self.terrain_layers[terrain_node] = layer_configs - - print(f"为地形创建 {len(layer_configs)} 个图层系统") - return True - - except Exception as e: - print(f"创建地形图层系统时出错: {e}") - return False - - def update_terrain_layer(self, terrain_node, layer_index, layer_config): - """ - 更新地形图层 - """ - if terrain_node not in self.terrain_layers: - print("地形没有图层系统") - return False - - if layer_index >= len(self.terrain_layers[terrain_node]): - print("图层索引超出范围") - return False - - # 更新图层配置 - self.terrain_layers[terrain_node][layer_index] = layer_config - - # 重新应用纹理 - return self.create_terrain_layer_system(terrain_node, self.terrain_layers[terrain_node]) - - def create_procedural_terrain_material(self, terrain_node, - base_color=(0.5, 0.6, 0.3, 1.0), - roughness=0.8, - metallic=0.1, - uv_scale=(1.0, 1.0), - use_normal_map=True): - """ - 创建程序化地形材质 - """ - try: - # 创建基础材质 - material = Material() - material.setName(f"procedural_terrain_{id(terrain_node)}") - material.setBaseColor(base_color) - material.setRoughness(roughness) - material.setMetallic(metallic) - - # 应用材质 - terrain_node.setMaterial(material) - - # 如果使用RenderPipeline,可以应用PBR效果 - if hasattr(self.world, 'render_pipeline'): - try: - effect_options = { - "normal_mapping": use_normal_map, - "parallax_mapping": False, - "uv_scale": uv_scale - } - - self.world.render_pipeline.set_effect( - terrain_node, - "effects/terrain.yaml", # 假设有专门的地形效果文件 - effect_options, - 10 # 排序值 - ) - print("应用PBR地形效果") - except Exception as e: - print(f"应用PBR效果失败: {e}") - - self.materials[material.getName()] = material - print(f"创建程序化地形材质: {material.getName()}") - return material - - except Exception as e: - print(f"创建程序化地形材质时出错: {e}") - return None - - def create_texture_atlas(self, atlas_name, texture_paths, atlas_size=(1024, 1024)): - """ - 创建纹理图集 - """ - try: - # 创建新的PNMImage作为图集 - atlas_image = PNMImage(atlas_size[0], atlas_size[1]) - atlas_image.fill(0, 0, 0) # 填充黑色背景 - - # 计算每个纹理在图集中的位置 - num_textures = len(texture_paths) - grid_size = int(math.ceil(math.sqrt(num_textures))) - tile_width = atlas_size[0] // grid_size - tile_height = atlas_size[1] // grid_size - - # 存储纹理坐标信息 - texture_coords = {} - - # 将每个纹理复制到图集中 - for i, texture_path in enumerate(texture_paths): - if not os.path.exists(texture_path): - print(f"纹理文件不存在: {texture_path}") - continue - - # 加载纹理图像 - texture_image = PNMImage(Filename.fromOsSpecific(texture_path)) - if texture_image.empty(): - print(f"无法加载纹理图像: {texture_path}") - continue - - # 计算在图集中的位置 - row = i // grid_size - col = i % grid_size - x_offset = col * tile_width - y_offset = row * tile_height - - # 将纹理复制到图集 - atlas_image.blendSubImage(texture_image, x_offset, y_offset, 0, 0, - min(tile_width, texture_image.getXSize()), - min(tile_height, texture_image.getYSize())) - - # 计算纹理坐标 - u0 = x_offset / atlas_size[0] - v0 = y_offset / atlas_size[1] - u1 = (x_offset + tile_width) / atlas_size[0] - v1 = (y_offset + tile_height) / atlas_size[1] - - texture_coords[os.path.basename(texture_path)] = (u0, v0, u1, v1) - - # 保存图集到临时文件 - temp_path = f"/tmp/{atlas_name}_atlas.png" - atlas_image.write(Filename.fromOsSpecific(temp_path)) - - # 加载图集纹理 - atlas_texture = self.world.loader.loadTexture(temp_path) - if atlas_texture: - self.texture_atlases[atlas_name] = { - 'texture': atlas_texture, - 'coords': texture_coords, - 'size': atlas_size - } - print(f"创建纹理图集: {atlas_name}") - return atlas_texture - - except Exception as e: - print(f"创建纹理图集时出错: {e}") - return None - - def apply_texture_atlas_to_terrain(self, terrain_node, atlas_name, texture_name): - """ - 为地形应用纹理图集中的特定纹理 - """ - if atlas_name not in self.texture_atlases: - print(f"纹理图集 {atlas_name} 不存在") - return False - - atlas_info = self.texture_atlases[atlas_name] - if texture_name not in atlas_info['coords']: - print(f"纹理 {texture_name} 不在图集中") - return False - - # 应用图集纹理 - texture_stage = TextureStage("atlas_diffuse") - terrain_node.setTexture(texture_stage, atlas_info['texture']) - - # 设置纹理坐标偏移(需要着色器支持) - coords = atlas_info['coords'][texture_name] - print(f"应用纹理图集 {atlas_name} 中的 {texture_name}") - return True - - def update_material_property(self, material_name, property_name, value): - """ - 更新材质属性 - """ - if material_name in self.materials: - material = self.materials[material_name] - setter_name = f'set{property_name.capitalize()}' - if hasattr(material, setter_name): - # 特殊处理某些属性 - if property_name in ['ambient', 'diffuse', 'specular', 'emission'] and isinstance(value, (list, tuple)): - getattr(material, setter_name)(Vec4(*value)) - else: - getattr(material, setter_name)(value) - print(f"更新材质 {material_name} 的属性 {property_name} 为 {value}") - return True - print(f"无法更新材质 {material_name} 的属性 {property_name}") - return False - - def get_material_info(self, material_name): - """ - 获取材质信息 - """ - if material_name in self.materials: - material = self.materials[material_name] - return { - "name": material.getName(), - "ambient": material.getAmbient(), - "diffuse": material.getDiffuse(), - "specular": material.getSpecular(), - "shininess": material.getShininess(), - "emissive": material.getEmission() - } - return None - - def create_splatting_material(self, terrain_node, splat_map_path, texture_paths): - """ - 创建地形分层混合材质(Splating) - splat_map_path: 控制混合的灰度图路径 - texture_paths: 纹理路径列表(通常4个:R、G、B、A通道分别控制) - """ - try: - # 加载混合图 - splat_texture = self.load_texture(splat_map_path) - if not splat_texture: - print("无法加载混合图") - return False - - # 加载纹理图集 - for i, texture_path in enumerate(texture_paths): - texture = self.load_texture(texture_path) - if texture: - texture_stage = TextureStage(f"splat_texture_{i}") - texture_stage.setSort(i) - terrain_node.setTexture(texture_stage, texture) - - # 应用混合图 - splat_stage = TextureStage("splat_map") - splat_stage.setMode(TextureStage.M_blend) - terrain_node.setTexture(splat_stage, splat_texture) - - print(f"创建分层混合材质,使用 {len(texture_paths)} 个纹理") - return True - - except Exception as e: - print(f"创建分层混合材质时出错: {e}") - return False - - def apply_terrain_shader(self, terrain_node, shader_path, shader_params=None): - """ - 为地形应用自定义着色器 - """ - try: - # 加载着色器 - shader = Shader.load(Shader.SL_GLSL, shader_path + ".vert", shader_path + ".frag") - - # 应用着色器 - terrain_node.setShader(shader) - - # 设置着色器参数 - if shader_params: - for param_name, param_value in shader_params.items(): - terrain_node.setShaderInput(param_name, param_value) - - print(f"为地形应用着色器: {shader_path}") - return True - - except Exception as e: - print(f"应用着色器时出错: {e}") +""" +地形材质和纹理系统 +提供完整的地形材质和纹理管理功能,支持多层纹理混合、程序化材质生成等高级功能 +""" + +import os +from panda3d.core import Material, Texture, TextureStage, Vec4, PNMImage, Filename +from panda3d.core import GeoMipTerrain, Shader, ShaderAttrib + +class TerrainMaterials: + """ + 地形材质和纹理系统类 + 提供完整的地形材质和纹理管理功能 + """ + + def __init__(self, world): + self.world = world + self.materials = {} # 存储材质 + self.terrain_layers = {} # 存储地形图层信息 + self.texture_atlases = {} # 存储纹理图集 + + def create_material(self, name, ambient=(0.5, 0.5, 0.5, 1.0), + diffuse=(0.8, 0.8, 0.8, 1.0), + specular=(0.2, 0.2, 0.2, 1.0), + shininess=5.0, + emissive=(0.0, 0.0, 0.0, 1.0)): + """ + 创建新材质 + """ + material = Material() + material.setName(name) + material.setAmbient(ambient) + material.setDiffuse(diffuse) + material.setSpecular(specular) + material.setShininess(shininess) + material.setEmission(emissive) + + self.materials[name] = material + print(f"创建材质: {name}") + return material + + def apply_material_to_terrain(self, terrain_node, material_name): + """ + 为地形应用材质 + """ + if material_name in self.materials: + material = self.materials[material_name] + terrain_node.setMaterial(material) + print(f"为地形应用材质: {material_name}") + return True + else: + print(f"材质 {material_name} 不存在") + return False + + def load_texture(self, texture_path): + """ + 加载纹理 + """ + try: + # 检查文件是否存在 + if not os.path.exists(texture_path): + print(f"纹理文件不存在: {texture_path}") + return None + + texture = self.world.loader.loadTexture(texture_path) + if texture: + print(f"纹理加载成功: {texture_path}") + return texture + else: + print(f"纹理加载失败: {texture_path}") + return None + except Exception as e: + print(f"加载纹理时出错: {e}") + return None + + def apply_texture_to_terrain(self, terrain_node, texture_path, texture_stage_name="diffuse"): + """ + 为地形应用纹理 + """ + texture = self.load_texture(texture_path) + if texture: + # 创建纹理阶段 + texture_stage = TextureStage(texture_stage_name) + # 应用纹理到地形 + terrain_node.setTexture(texture_stage, texture) + print(f"为地形应用纹理: {texture_path}") + return True + return False + + def apply_multi_texture_to_terrain(self, terrain_node, texture_paths, blend_factors=None): + """ + 为地形应用多纹理混合 + texture_paths: 纹理路径列表 + blend_factors: 混合因子列表,控制每个纹理的混合权重 + """ + if not texture_paths: + print("纹理路径列表为空") + return False + + # 如果没有提供混合因子,使用默认值 + if blend_factors is None: + blend_factors = [1.0] * len(texture_paths) + + # 确保混合因子数量与纹理数量一致 + if len(blend_factors) != len(texture_paths): + print("混合因子数量与纹理数量不匹配") + return False + + # 应用每个纹理 + for i, (texture_path, blend_factor) in enumerate(zip(texture_paths, blend_factors)): + texture = self.load_texture(texture_path) + if texture: + # 为每个纹理创建不同的纹理阶段 + texture_stage = TextureStage(f"texture_{i}") + texture_stage.setSort(i) # 设置排序 + + # 应用纹理 + terrain_node.setTexture(texture_stage, texture) + + # 如果支持,设置混合因子 + if hasattr(texture_stage, 'setBlendFactor'): + texture_stage.setBlendFactor(blend_factor) + + print(f"为地形应用 {len(texture_paths)} 个多纹理混合") + return True + + def create_terrain_layer_system(self, terrain_node, layer_configs): + """ + 创建地形图层系统 + layer_configs: 图层配置列表,每个配置包含纹理路径、混合贴图等信息 + """ + try: + # 清除现有的地形纹理 + terrain_node.clearTexture() + + # 为每个图层创建纹理阶段 + for i, layer_config in enumerate(layer_configs): + # 加载漫反射纹理 + if 'diffuse' in layer_config: + diffuse_texture = self.load_texture(layer_config['diffuse']) + if diffuse_texture: + diffuse_stage = TextureStage(f"diffuse_{i}") + diffuse_stage.setSort(i * 10) # 设置排序 + terrain_node.setTexture(diffuse_stage, diffuse_texture) + + # 加载法线纹理 + if 'normal' in layer_config: + normal_texture = self.load_texture(layer_config['normal']) + if normal_texture: + normal_stage = TextureStage(f"normal_{i}") + normal_stage.setMode(TextureStage.M_normal) # 设置为法线贴图模式 + normal_stage.setSort(i * 10 + 1) + terrain_node.setTexture(normal_stage, normal_texture) + + # 加载混合纹理(控制图层混合) + if 'blend' in layer_config: + blend_texture = self.load_texture(layer_config['blend']) + if blend_texture: + blend_stage = TextureStage(f"blend_{i}") + blend_stage.setMode(TextureStage.M_blend) # 设置为混合模式 + blend_stage.setSort(i * 10 + 2) + terrain_node.setTexture(blend_stage, blend_texture) + + # 保存图层信息 + self.terrain_layers[terrain_node] = layer_configs + + print(f"为地形创建 {len(layer_configs)} 个图层系统") + return True + + except Exception as e: + print(f"创建地形图层系统时出错: {e}") + return False + + def update_terrain_layer(self, terrain_node, layer_index, layer_config): + """ + 更新地形图层 + """ + if terrain_node not in self.terrain_layers: + print("地形没有图层系统") + return False + + if layer_index >= len(self.terrain_layers[terrain_node]): + print("图层索引超出范围") + return False + + # 更新图层配置 + self.terrain_layers[terrain_node][layer_index] = layer_config + + # 重新应用纹理 + return self.create_terrain_layer_system(terrain_node, self.terrain_layers[terrain_node]) + + def create_procedural_terrain_material(self, terrain_node, + base_color=(0.5, 0.6, 0.3, 1.0), + roughness=0.8, + metallic=0.1, + uv_scale=(1.0, 1.0), + use_normal_map=True): + """ + 创建程序化地形材质 + """ + try: + # 创建基础材质 + material = Material() + material.setName(f"procedural_terrain_{id(terrain_node)}") + material.setBaseColor(base_color) + material.setRoughness(roughness) + material.setMetallic(metallic) + + # 应用材质 + terrain_node.setMaterial(material) + + # 如果使用RenderPipeline,可以应用PBR效果 + if hasattr(self.world, 'render_pipeline'): + try: + effect_options = { + "normal_mapping": use_normal_map, + "parallax_mapping": False, + "uv_scale": uv_scale + } + + self.world.render_pipeline.set_effect( + terrain_node, + "effects/terrain.yaml", # 假设有专门的地形效果文件 + effect_options, + 10 # 排序值 + ) + print("应用PBR地形效果") + except Exception as e: + print(f"应用PBR效果失败: {e}") + + self.materials[material.getName()] = material + print(f"创建程序化地形材质: {material.getName()}") + return material + + except Exception as e: + print(f"创建程序化地形材质时出错: {e}") + return None + + def create_texture_atlas(self, atlas_name, texture_paths, atlas_size=(1024, 1024)): + """ + 创建纹理图集 + """ + try: + # 创建新的PNMImage作为图集 + atlas_image = PNMImage(atlas_size[0], atlas_size[1]) + atlas_image.fill(0, 0, 0) # 填充黑色背景 + + # 计算每个纹理在图集中的位置 + num_textures = len(texture_paths) + grid_size = int(math.ceil(math.sqrt(num_textures))) + tile_width = atlas_size[0] // grid_size + tile_height = atlas_size[1] // grid_size + + # 存储纹理坐标信息 + texture_coords = {} + + # 将每个纹理复制到图集中 + for i, texture_path in enumerate(texture_paths): + if not os.path.exists(texture_path): + print(f"纹理文件不存在: {texture_path}") + continue + + # 加载纹理图像 + texture_image = PNMImage(Filename.fromOsSpecific(texture_path)) + if texture_image.empty(): + print(f"无法加载纹理图像: {texture_path}") + continue + + # 计算在图集中的位置 + row = i // grid_size + col = i % grid_size + x_offset = col * tile_width + y_offset = row * tile_height + + # 将纹理复制到图集 + atlas_image.blendSubImage(texture_image, x_offset, y_offset, 0, 0, + min(tile_width, texture_image.getXSize()), + min(tile_height, texture_image.getYSize())) + + # 计算纹理坐标 + u0 = x_offset / atlas_size[0] + v0 = y_offset / atlas_size[1] + u1 = (x_offset + tile_width) / atlas_size[0] + v1 = (y_offset + tile_height) / atlas_size[1] + + texture_coords[os.path.basename(texture_path)] = (u0, v0, u1, v1) + + # 保存图集到临时文件 + temp_path = f"/tmp/{atlas_name}_atlas.png" + atlas_image.write(Filename.fromOsSpecific(temp_path)) + + # 加载图集纹理 + atlas_texture = self.world.loader.loadTexture(temp_path) + if atlas_texture: + self.texture_atlases[atlas_name] = { + 'texture': atlas_texture, + 'coords': texture_coords, + 'size': atlas_size + } + print(f"创建纹理图集: {atlas_name}") + return atlas_texture + + except Exception as e: + print(f"创建纹理图集时出错: {e}") + return None + + def apply_texture_atlas_to_terrain(self, terrain_node, atlas_name, texture_name): + """ + 为地形应用纹理图集中的特定纹理 + """ + if atlas_name not in self.texture_atlases: + print(f"纹理图集 {atlas_name} 不存在") + return False + + atlas_info = self.texture_atlases[atlas_name] + if texture_name not in atlas_info['coords']: + print(f"纹理 {texture_name} 不在图集中") + return False + + # 应用图集纹理 + texture_stage = TextureStage("atlas_diffuse") + terrain_node.setTexture(texture_stage, atlas_info['texture']) + + # 设置纹理坐标偏移(需要着色器支持) + coords = atlas_info['coords'][texture_name] + print(f"应用纹理图集 {atlas_name} 中的 {texture_name}") + return True + + def update_material_property(self, material_name, property_name, value): + """ + 更新材质属性 + """ + if material_name in self.materials: + material = self.materials[material_name] + setter_name = f'set{property_name.capitalize()}' + if hasattr(material, setter_name): + # 特殊处理某些属性 + if property_name in ['ambient', 'diffuse', 'specular', 'emission'] and isinstance(value, (list, tuple)): + getattr(material, setter_name)(Vec4(*value)) + else: + getattr(material, setter_name)(value) + print(f"更新材质 {material_name} 的属性 {property_name} 为 {value}") + return True + print(f"无法更新材质 {material_name} 的属性 {property_name}") + return False + + def get_material_info(self, material_name): + """ + 获取材质信息 + """ + if material_name in self.materials: + material = self.materials[material_name] + return { + "name": material.getName(), + "ambient": material.getAmbient(), + "diffuse": material.getDiffuse(), + "specular": material.getSpecular(), + "shininess": material.getShininess(), + "emissive": material.getEmission() + } + return None + + def create_splatting_material(self, terrain_node, splat_map_path, texture_paths): + """ + 创建地形分层混合材质(Splating) + splat_map_path: 控制混合的灰度图路径 + texture_paths: 纹理路径列表(通常4个:R、G、B、A通道分别控制) + """ + try: + # 加载混合图 + splat_texture = self.load_texture(splat_map_path) + if not splat_texture: + print("无法加载混合图") + return False + + # 加载纹理图集 + for i, texture_path in enumerate(texture_paths): + texture = self.load_texture(texture_path) + if texture: + texture_stage = TextureStage(f"splat_texture_{i}") + texture_stage.setSort(i) + terrain_node.setTexture(texture_stage, texture) + + # 应用混合图 + splat_stage = TextureStage("splat_map") + splat_stage.setMode(TextureStage.M_blend) + terrain_node.setTexture(splat_stage, splat_texture) + + print(f"创建分层混合材质,使用 {len(texture_paths)} 个纹理") + return True + + except Exception as e: + print(f"创建分层混合材质时出错: {e}") + return False + + def apply_terrain_shader(self, terrain_node, shader_path, shader_params=None): + """ + 为地形应用自定义着色器 + """ + try: + # 加载着色器 + shader = Shader.load(Shader.SL_GLSL, shader_path + ".vert", shader_path + ".frag") + + # 应用着色器 + terrain_node.setShader(shader) + + # 设置着色器参数 + if shader_params: + for param_name, param_value in shader_params.items(): + terrain_node.setShaderInput(param_name, param_value) + + print(f"为地形应用着色器: {shader_path}") + return True + + except Exception as e: + print(f"应用着色器时出错: {e}") return False \ No newline at end of file diff --git a/plugins/user/terrain_editor/optimization/__init__.py b/plugins/user/terrain_editor/optimization/__init__.py index 6a3b5cf4..e2a1f0a0 100644 --- a/plugins/user/terrain_editor/optimization/__init__.py +++ b/plugins/user/terrain_editor/optimization/__init__.py @@ -1,3 +1,3 @@ -""" -地形优化和LOD模块 +""" +地形优化和LOD模块 """ \ No newline at end of file diff --git a/plugins/user/terrain_editor/optimization/terrain_optimization.py b/plugins/user/terrain_editor/optimization/terrain_optimization.py index e9b6d0c9..3aa4b9fe 100644 --- a/plugins/user/terrain_editor/optimization/terrain_optimization.py +++ b/plugins/user/terrain_editor/optimization/terrain_optimization.py @@ -1,382 +1,382 @@ -""" -地形优化和LOD系统 -提供完整的地形优化功能,包括LOD控制、碰撞优化、渲染优化和性能监控 -""" - -from panda3d.core import BitMask32, GeoMipTerrain -import time -import psutil -import gc - -class TerrainOptimization: - """ - 地形优化和LOD系统类 - 提供完整的地形优化功能 - """ - - def __init__(self, world): - self.world = world - self.lod_enabled = True - self.lod_threshold = 50.0 - self.min_block_size = 16 - self.max_block_size = 64 - self.auto_lod_enabled = True - self.performance_monitoring = True - self.last_optimization_time = 0 - - def enable_lod(self, enabled=True): - """ - 启用或禁用LOD - """ - self.lod_enabled = enabled - print(f"LOD系统已{'启用' if enabled else '禁用'}") - - def set_lod_threshold(self, threshold): - """ - 设置LOD阈值 - """ - self.lod_threshold = max(1.0, min(1000.0, float(threshold))) - print(f"LOD阈值设置为: {self.lod_threshold}") - - def set_auto_lod(self, enabled=True): - """ - 启用或禁用自动LOD调整 - """ - self.auto_lod_enabled = enabled - print(f"自动LOD调整已{'启用' if enabled else '禁用'}") - - def update_terrain_lod(self, terrain_info): - """ - 更新单个地形的LOD - """ - if not self.lod_enabled: - return - - try: - if terrain_info in self.world.terrain_manager.terrains: - terrain = terrain_info['terrain'] - if hasattr(terrain, 'setNearFarThreshold'): - # 设置LOD阈值 - near_threshold = self.lod_threshold - far_threshold = self.lod_threshold * 2 - terrain.setNearFarThreshold(near_threshold, far_threshold) - - # 如果启用自动LOD,根据距离动态调整 - if self.auto_lod_enabled: - distance = self._calculate_terrain_distance(terrain_info) - if distance > 200: - # 远距离时使用更大的块 - block_size = min(self.max_block_size, int(distance / 10)) - else: - # 近距离时使用更小的块 - block_size = max(self.min_block_size, int(64 - distance / 5)) - - self.set_terrain_block_size(terrain_info, block_size) - - except Exception as e: - print(f"更新地形LOD时出错: {e}") - - def update_all_terrains_lod(self): - """ - 更新所有地形的LOD - """ - if not self.lod_enabled: - return - - if hasattr(self.world, 'terrain_manager'): - for terrain_info in self.world.terrain_manager.terrains: - self.update_terrain_lod(terrain_info) - - def optimize_terrain_collision(self, terrain_info): - """ - 优化地形碰撞体 - """ - try: - terrain_node = terrain_info['node'] - if terrain_node: - # 清除现有的碰撞体 - for child in terrain_node.getChildren(): - if child.getName().startswith("terrain_collision_"): - child.removeNode() - - # 设置地形节点的碰撞掩码 - terrain_node.setCollideMask(BitMask32.bit(2)) - - # 为地形的所有子节点也设置碰撞掩码 - for child in terrain_node.getChildren(): - child.setCollideMask(BitMask32.bit(2)) - - # 如果地形很大,可以考虑创建简化的碰撞网格 - if self._should_simplify_collision(terrain_info): - self._create_simplified_collision(terrain_info) - - print("地形碰撞体已优化") - return True - except Exception as e: - print(f"优化地形碰撞体时出错: {e}") - return False - - def _should_simplify_collision(self, terrain_info): - """ - 判断是否需要简化碰撞体 - """ - try: - heightfield = terrain_info.get('heightfield') - if heightfield: - width = heightfield.getXSize() - height = heightfield.getYSize() - # 如果地形分辨率很高,考虑简化碰撞 - return width > 256 or height > 256 - return False - except: - return False - - def _create_simplified_collision(self, terrain_info): - """ - 创建简化的碰撞体 - """ - print("创建简化的地形碰撞体") - # 这里可以实现创建低分辨率碰撞网格的逻辑 - - def optimize_terrain_rendering(self, terrain_info): - """ - 优化地形渲染 - """ - try: - terrain_node = terrain_info['node'] - if terrain_node: - # 如果使用RenderPipeline,应用优化效果 - if hasattr(self.world, 'render_pipeline'): - try: - # 根据距离选择不同的效果 - distance = self._calculate_terrain_distance(terrain_info) - if distance > 100: - # 远距离使用简化效果 - effect_file = "effects/terrain_far.yaml" - else: - # 近距离使用高质量效果 - effect_file = "effects/terrain_near.yaml" - - self.world.render_pipeline.set_effect( - terrain_node, - effect_file, - { - "normal_mapping": distance < 50, # 近距离启用法线贴图 - "parallax_mapping": distance < 30, # 很近距离启用视差贴图 - "lod_rendering": self.lod_enabled, - "distance": distance - }, - 15 # 排序值 - ) - print(f"应用优化的地形渲染效果: {effect_file}") - except Exception as e: - print(f"应用优化渲染效果失败: {e}") - - # 设置渲染优先级 - terrain_node.setBin("background", 0) - terrain_node.setDepthWrite(True) - - return True - except Exception as e: - print(f"优化地形渲染时出错: {e}") - return False - - def get_terrain_performance_stats(self, terrain_info): - """ - 获取地形性能统计信息 - """ - stats = { - 'lod_enabled': self.lod_enabled, - 'lod_threshold': self.lod_threshold, - 'vertices': 0, - 'triangles': 0, - 'memory_usage': 0, - 'block_size': 32, - 'distance': 0 - } - - try: - terrain = terrain_info['terrain'] - if terrain: - # 获取地形几何信息 - if hasattr(terrain, 'getRoot'): - root_node = terrain.getRoot() - if root_node: - # 估算顶点和三角形数量 - heightfield = terrain_info.get('heightfield') - if heightfield: - width = heightfield.getXSize() - height = heightfield.getYSize() - stats['vertices'] = width * height - stats['triangles'] = (width - 1) * (height - 1) * 2 - - # 获取块大小 - if hasattr(terrain, 'getBlockSize'): - stats['block_size'] = terrain.getBlockSize() - - # 计算距离 - stats['distance'] = self._calculate_terrain_distance(terrain_info) - - except Exception as e: - print(f"获取地形性能统计时出错: {e}") - - return stats - - def auto_optimize_terrains(self): - """ - 自动优化所有地形 - """ - if not hasattr(self.world, 'terrain_manager'): - return - - # 检查是否需要优化(避免过于频繁的优化) - current_time = time.time() - if current_time - self.last_optimization_time < 1.0: # 至少间隔1秒 - return - - self.last_optimization_time = current_time - - optimized_count = 0 - for terrain_info in self.world.terrain_manager.terrains: - # 优化碰撞体 - if self.optimize_terrain_collision(terrain_info): - optimized_count += 1 - - # 优化渲染 - if self.optimize_terrain_rendering(terrain_info): - optimized_count += 1 - - # 更新LOD - self.update_terrain_lod(terrain_info) - - # 更新所有地形的LOD - self.update_all_terrains_lod() - - # 执行垃圾回收 - if optimized_count > 0: - gc.collect() - - print(f"自动优化完成,优化了 {optimized_count} 个项目") - - def set_terrain_block_size(self, terrain_info, block_size): - """ - 设置地形块大小(影响LOD效果) - """ - try: - # 限制块大小范围 - block_size = max(self.min_block_size, min(self.max_block_size, int(block_size))) - - terrain = terrain_info['terrain'] - if hasattr(terrain, 'setBlockSize'): - terrain.setBlockSize(block_size) - print(f"地形块大小设置为: {block_size}") - return True - except Exception as e: - print(f"设置地形块大小时出错: {e}") - return False - return False - - def set_block_size_range(self, min_size, max_size): - """ - 设置块大小范围 - """ - self.min_block_size = max(8, min(128, int(min_size))) - self.max_block_size = max(8, min(128, int(max_size))) - print(f"地形块大小范围设置为: {self.min_block_size} - {self.max_block_size}") - - def _calculate_terrain_distance(self, terrain_info): - """ - 计算地形到相机的距离 - """ - try: - terrain_node = terrain_info['node'] - if terrain_node and hasattr(self.world, 'cam'): - terrain_pos = terrain_node.getPos() - camera_pos = self.world.cam.getPos() - distance = (terrain_pos - camera_pos).length() - return distance - return 50.0 # 默认距离 - except: - return 50.0 - - def enable_performance_monitoring(self, enabled=True): - """ - 启用或禁用性能监控 - """ - self.performance_monitoring = enabled - print(f"性能监控已{'启用' if enabled else '禁用'}") - - def get_system_performance_stats(self): - """ - 获取系统性能统计信息 - """ - try: - # 获取内存使用情况 - process = psutil.Process() - memory_info = process.memory_info() - - # 获取CPU使用率 - cpu_percent = process.cpu_percent() - - return { - 'memory_rss': memory_info.rss, - 'memory_vms': memory_info.vms, - 'cpu_percent': cpu_percent, - 'terrain_count': len(self.world.terrain_manager.terrains) if hasattr(self.world, 'terrain_manager') else 0 - } - except Exception as e: - print(f"获取系统性能统计时出错: {e}") - return {} - - def optimize_memory_usage(self): - """ - 优化内存使用 - """ - try: - # 执行垃圾回收 - collected = gc.collect() - print(f"执行垃圾回收,清理了 {collected} 个对象") - - # 优化纹理内存使用 - if hasattr(self.world, 'texture_cache'): - self.world.texture_cache.clear_unused() - - return True - except Exception as e: - print(f"优化内存使用时出错: {e}") - return False - - def set_render_distance(self, distance): - """ - 设置渲染距离 - """ - try: - if hasattr(self.world, 'camLens'): - self.world.camLens.setNearFar(0.1, float(distance)) - print(f"渲染距离设置为: {distance}") - return True - except Exception as e: - print(f"设置渲染距离时出错: {e}") - return False - return False - - def enable_frustum_culling(self, enabled=True): - """ - 启用或禁用视锥体剔除 - """ - try: - if hasattr(self.world, 'terrain_manager'): - for terrain_info in self.world.terrain_manager.terrains: - terrain_node = terrain_info['node'] - if terrain_node: - if enabled: - terrain_node.setAttrib(CullFaceAttrib.makePanda()) - else: - terrain_node.clearAttrib(CullFaceAttrib.getClassType()) - - print(f"视锥体剔除已{'启用' if enabled else '禁用'}") - return True - except Exception as e: - print(f"设置视锥体剔除时出错: {e}") - return False +""" +地形优化和LOD系统 +提供完整的地形优化功能,包括LOD控制、碰撞优化、渲染优化和性能监控 +""" + +from panda3d.core import BitMask32, GeoMipTerrain +import time +import psutil +import gc + +class TerrainOptimization: + """ + 地形优化和LOD系统类 + 提供完整的地形优化功能 + """ + + def __init__(self, world): + self.world = world + self.lod_enabled = True + self.lod_threshold = 50.0 + self.min_block_size = 16 + self.max_block_size = 64 + self.auto_lod_enabled = True + self.performance_monitoring = True + self.last_optimization_time = 0 + + def enable_lod(self, enabled=True): + """ + 启用或禁用LOD + """ + self.lod_enabled = enabled + print(f"LOD系统已{'启用' if enabled else '禁用'}") + + def set_lod_threshold(self, threshold): + """ + 设置LOD阈值 + """ + self.lod_threshold = max(1.0, min(1000.0, float(threshold))) + print(f"LOD阈值设置为: {self.lod_threshold}") + + def set_auto_lod(self, enabled=True): + """ + 启用或禁用自动LOD调整 + """ + self.auto_lod_enabled = enabled + print(f"自动LOD调整已{'启用' if enabled else '禁用'}") + + def update_terrain_lod(self, terrain_info): + """ + 更新单个地形的LOD + """ + if not self.lod_enabled: + return + + try: + if terrain_info in self.world.terrain_manager.terrains: + terrain = terrain_info['terrain'] + if hasattr(terrain, 'setNearFarThreshold'): + # 设置LOD阈值 + near_threshold = self.lod_threshold + far_threshold = self.lod_threshold * 2 + terrain.setNearFarThreshold(near_threshold, far_threshold) + + # 如果启用自动LOD,根据距离动态调整 + if self.auto_lod_enabled: + distance = self._calculate_terrain_distance(terrain_info) + if distance > 200: + # 远距离时使用更大的块 + block_size = min(self.max_block_size, int(distance / 10)) + else: + # 近距离时使用更小的块 + block_size = max(self.min_block_size, int(64 - distance / 5)) + + self.set_terrain_block_size(terrain_info, block_size) + + except Exception as e: + print(f"更新地形LOD时出错: {e}") + + def update_all_terrains_lod(self): + """ + 更新所有地形的LOD + """ + if not self.lod_enabled: + return + + if hasattr(self.world, 'terrain_manager'): + for terrain_info in self.world.terrain_manager.terrains: + self.update_terrain_lod(terrain_info) + + def optimize_terrain_collision(self, terrain_info): + """ + 优化地形碰撞体 + """ + try: + terrain_node = terrain_info['node'] + if terrain_node: + # 清除现有的碰撞体 + for child in terrain_node.getChildren(): + if child.getName().startswith("terrain_collision_"): + child.removeNode() + + # 设置地形节点的碰撞掩码 + terrain_node.setCollideMask(BitMask32.bit(2)) + + # 为地形的所有子节点也设置碰撞掩码 + for child in terrain_node.getChildren(): + child.setCollideMask(BitMask32.bit(2)) + + # 如果地形很大,可以考虑创建简化的碰撞网格 + if self._should_simplify_collision(terrain_info): + self._create_simplified_collision(terrain_info) + + print("地形碰撞体已优化") + return True + except Exception as e: + print(f"优化地形碰撞体时出错: {e}") + return False + + def _should_simplify_collision(self, terrain_info): + """ + 判断是否需要简化碰撞体 + """ + try: + heightfield = terrain_info.get('heightfield') + if heightfield: + width = heightfield.getXSize() + height = heightfield.getYSize() + # 如果地形分辨率很高,考虑简化碰撞 + return width > 256 or height > 256 + return False + except: + return False + + def _create_simplified_collision(self, terrain_info): + """ + 创建简化的碰撞体 + """ + print("创建简化的地形碰撞体") + # 这里可以实现创建低分辨率碰撞网格的逻辑 + + def optimize_terrain_rendering(self, terrain_info): + """ + 优化地形渲染 + """ + try: + terrain_node = terrain_info['node'] + if terrain_node: + # 如果使用RenderPipeline,应用优化效果 + if hasattr(self.world, 'render_pipeline'): + try: + # 根据距离选择不同的效果 + distance = self._calculate_terrain_distance(terrain_info) + if distance > 100: + # 远距离使用简化效果 + effect_file = "effects/terrain_far.yaml" + else: + # 近距离使用高质量效果 + effect_file = "effects/terrain_near.yaml" + + self.world.render_pipeline.set_effect( + terrain_node, + effect_file, + { + "normal_mapping": distance < 50, # 近距离启用法线贴图 + "parallax_mapping": distance < 30, # 很近距离启用视差贴图 + "lod_rendering": self.lod_enabled, + "distance": distance + }, + 15 # 排序值 + ) + print(f"应用优化的地形渲染效果: {effect_file}") + except Exception as e: + print(f"应用优化渲染效果失败: {e}") + + # 设置渲染优先级 + terrain_node.setBin("background", 0) + terrain_node.setDepthWrite(True) + + return True + except Exception as e: + print(f"优化地形渲染时出错: {e}") + return False + + def get_terrain_performance_stats(self, terrain_info): + """ + 获取地形性能统计信息 + """ + stats = { + 'lod_enabled': self.lod_enabled, + 'lod_threshold': self.lod_threshold, + 'vertices': 0, + 'triangles': 0, + 'memory_usage': 0, + 'block_size': 32, + 'distance': 0 + } + + try: + terrain = terrain_info['terrain'] + if terrain: + # 获取地形几何信息 + if hasattr(terrain, 'getRoot'): + root_node = terrain.getRoot() + if root_node: + # 估算顶点和三角形数量 + heightfield = terrain_info.get('heightfield') + if heightfield: + width = heightfield.getXSize() + height = heightfield.getYSize() + stats['vertices'] = width * height + stats['triangles'] = (width - 1) * (height - 1) * 2 + + # 获取块大小 + if hasattr(terrain, 'getBlockSize'): + stats['block_size'] = terrain.getBlockSize() + + # 计算距离 + stats['distance'] = self._calculate_terrain_distance(terrain_info) + + except Exception as e: + print(f"获取地形性能统计时出错: {e}") + + return stats + + def auto_optimize_terrains(self): + """ + 自动优化所有地形 + """ + if not hasattr(self.world, 'terrain_manager'): + return + + # 检查是否需要优化(避免过于频繁的优化) + current_time = time.time() + if current_time - self.last_optimization_time < 1.0: # 至少间隔1秒 + return + + self.last_optimization_time = current_time + + optimized_count = 0 + for terrain_info in self.world.terrain_manager.terrains: + # 优化碰撞体 + if self.optimize_terrain_collision(terrain_info): + optimized_count += 1 + + # 优化渲染 + if self.optimize_terrain_rendering(terrain_info): + optimized_count += 1 + + # 更新LOD + self.update_terrain_lod(terrain_info) + + # 更新所有地形的LOD + self.update_all_terrains_lod() + + # 执行垃圾回收 + if optimized_count > 0: + gc.collect() + + print(f"自动优化完成,优化了 {optimized_count} 个项目") + + def set_terrain_block_size(self, terrain_info, block_size): + """ + 设置地形块大小(影响LOD效果) + """ + try: + # 限制块大小范围 + block_size = max(self.min_block_size, min(self.max_block_size, int(block_size))) + + terrain = terrain_info['terrain'] + if hasattr(terrain, 'setBlockSize'): + terrain.setBlockSize(block_size) + print(f"地形块大小设置为: {block_size}") + return True + except Exception as e: + print(f"设置地形块大小时出错: {e}") + return False + return False + + def set_block_size_range(self, min_size, max_size): + """ + 设置块大小范围 + """ + self.min_block_size = max(8, min(128, int(min_size))) + self.max_block_size = max(8, min(128, int(max_size))) + print(f"地形块大小范围设置为: {self.min_block_size} - {self.max_block_size}") + + def _calculate_terrain_distance(self, terrain_info): + """ + 计算地形到相机的距离 + """ + try: + terrain_node = terrain_info['node'] + if terrain_node and hasattr(self.world, 'cam'): + terrain_pos = terrain_node.getPos() + camera_pos = self.world.cam.getPos() + distance = (terrain_pos - camera_pos).length() + return distance + return 50.0 # 默认距离 + except: + return 50.0 + + def enable_performance_monitoring(self, enabled=True): + """ + 启用或禁用性能监控 + """ + self.performance_monitoring = enabled + print(f"性能监控已{'启用' if enabled else '禁用'}") + + def get_system_performance_stats(self): + """ + 获取系统性能统计信息 + """ + try: + # 获取内存使用情况 + process = psutil.Process() + memory_info = process.memory_info() + + # 获取CPU使用率 + cpu_percent = process.cpu_percent() + + return { + 'memory_rss': memory_info.rss, + 'memory_vms': memory_info.vms, + 'cpu_percent': cpu_percent, + 'terrain_count': len(self.world.terrain_manager.terrains) if hasattr(self.world, 'terrain_manager') else 0 + } + except Exception as e: + print(f"获取系统性能统计时出错: {e}") + return {} + + def optimize_memory_usage(self): + """ + 优化内存使用 + """ + try: + # 执行垃圾回收 + collected = gc.collect() + print(f"执行垃圾回收,清理了 {collected} 个对象") + + # 优化纹理内存使用 + if hasattr(self.world, 'texture_cache'): + self.world.texture_cache.clear_unused() + + return True + except Exception as e: + print(f"优化内存使用时出错: {e}") + return False + + def set_render_distance(self, distance): + """ + 设置渲染距离 + """ + try: + if hasattr(self.world, 'camLens'): + self.world.camLens.setNearFar(0.1, float(distance)) + print(f"渲染距离设置为: {distance}") + return True + except Exception as e: + print(f"设置渲染距离时出错: {e}") + return False + return False + + def enable_frustum_culling(self, enabled=True): + """ + 启用或禁用视锥体剔除 + """ + try: + if hasattr(self.world, 'terrain_manager'): + for terrain_info in self.world.terrain_manager.terrains: + terrain_node = terrain_info['node'] + if terrain_node: + if enabled: + terrain_node.setAttrib(CullFaceAttrib.makePanda()) + else: + terrain_node.clearAttrib(CullFaceAttrib.getClassType()) + + print(f"视锥体剔除已{'启用' if enabled else '禁用'}") + return True + except Exception as e: + print(f"设置视锥体剔除时出错: {e}") + return False return False \ No newline at end of file diff --git a/plugins/user/terrain_editor/paths/__init__.py b/plugins/user/terrain_editor/paths/__init__.py index cb3058d6..884e0240 100644 --- a/plugins/user/terrain_editor/paths/__init__.py +++ b/plugins/user/terrain_editor/paths/__init__.py @@ -1,3 +1,3 @@ -""" -地形路径系统初始化文件 +""" +地形路径系统初始化文件 """ \ No newline at end of file diff --git a/plugins/user/terrain_editor/paths/path_system.py b/plugins/user/terrain_editor/paths/path_system.py index 21d33bef..20ab7892 100644 --- a/plugins/user/terrain_editor/paths/path_system.py +++ b/plugins/user/terrain_editor/paths/path_system.py @@ -1,1141 +1,1141 @@ -""" -地形路径和路网系统 -提供完整的路径创建、编辑、导航、寻路算法和可视化功能 -""" - -from panda3d.core import NodePath, Vec3, Point3, BitMask32 -from panda3d.core import CardMaker, LineSegs -from panda3d.core import Texture, TextureStage -from panda3d.ai import AIPathFinder, AIPath, AICharacter -import math -import json -import os -import heapq - -class PathSystem: - """ - 地形路径和路网系统类 - 提供路径创建、编辑、导航、寻路算法和可视化功能 - """ - - def __init__(self, world): - self.world = world - self.paths = [] # 存储路径 - self.waypoints = [] # 存储路径点 - self.roads = [] # 存储道路 - self.navigation_mesh = None # 导航网格 - self.pathfinding_enabled = True - self.graph = {} # 寻路图 - self.traffic_system = {} # 交通系统 - self.pathfinding_algorithm = 'a*' # 寻路算法 ('a*', 'dijkstra', 'rrt') - - def create_path(self, name, points=None): - """ - 创建路径 - name: 路径名称 - points: 路径点列表 - """ - try: - # 创建路径节点 - path_node = self.world.render.attachNewNode(f"path_{name}") - - # 创建路径信息 - path_info = { - 'name': name, - 'node': path_node, - 'points': points if points else [], - 'waypoints': [], # 路径点对象 - 'segments': [], # 路段信息 - 'properties': { - 'width': 2.0, - 'type': 'generic', # generic, road, trail, river, etc. - 'speed_limit': 50.0, # km/h - 'one_way': False, - 'lanes': 1, - 'difficulty': 1.0, # 通行难度系数 - 'traffic_density': 0.0 # 交通密度 - } - } - - # 如果提供了点,创建路径点 - if points: - self._create_waypoints(path_info, points) - - # 设置标签 - path_node.setTag("path_name", name) - path_node.setTag("is_scene_element", "1") - path_node.setTag("tree_item_type", "PATH_NODE") - - # 添加到路径列表 - self.paths.append(path_info) - - # 更新寻路图 - self._update_pathfinding_graph() - - print(f"创建路径: {name}") - return path_info - - except Exception as e: - print(f"创建路径时出错: {e}") - return None - - def _create_waypoints(self, path_info, points): - """ - 创建路径点 - """ - try: - waypoints = [] - - for i, point in enumerate(points): - # 创建路径点节点 - waypoint_node = path_info['node'].attachNewNode(f"waypoint_{i}") - waypoint_node.setPos(point) - - # 创建路径点信息 - waypoint_info = { - 'node': waypoint_node, - 'position': point, - 'index': i, - 'connections': [], # 连接的其他路径点 - 'properties': { - 'speed_limit': path_info['properties']['speed_limit'], - 'stop_sign': False, - 'traffic_light': False, - 'priority': 1.0, # 路径点优先级 - 'wait_time': 0.0 # 等待时间 - } - } - - waypoints.append(waypoint_info) - self.waypoints.append(waypoint_info) - - path_info['waypoints'] = waypoints - - # 创建路径段 - self._create_path_segments(path_info) - - # 可视化路径 - self._visualize_path(path_info) - - except Exception as e: - print(f"创建路径点时出错: {e}") - - def _create_path_segments(self, path_info): - """ - 创建路径段 - """ - try: - waypoints = path_info['waypoints'] - segments = [] - - for i in range(len(waypoints) - 1): - start_waypoint = waypoints[i] - end_waypoint = waypoints[i + 1] - - # 计算路段信息 - segment_length = (end_waypoint['position'] - start_waypoint['position']).length() - segment_direction = (end_waypoint['position'] - start_waypoint['position']).normalized() - segment_slope = self._calculate_slope(start_waypoint['position'], end_waypoint['position']) - - segment_info = { - 'start': start_waypoint, - 'end': end_waypoint, - 'length': segment_length, - 'direction': segment_direction, - 'slope': segment_slope, - 'difficulty': 1.0 + abs(segment_slope) * 0.1, # 坡度影响通行难度 - 'traffic_flow': 0.0, # 交通流量 - 'last_update': self.world.globalClock.getFrameTime() - } - - segments.append(segment_info) - - # 建立连接关系 - start_waypoint['connections'].append(end_waypoint) - if not path_info['properties']['one_way']: - end_waypoint['connections'].append(start_waypoint) - - path_info['segments'] = segments - - except Exception as e: - print(f"创建路径段时出错: {e}") - - def _calculate_slope(self, start_pos, end_pos): - """ - 计算两点间的坡度 - """ - try: - horizontal_distance = Vec3(end_pos.getX() - start_pos.getX(), - end_pos.getY() - start_pos.getY(), 0).length() - vertical_distance = end_pos.getZ() - start_pos.getZ() - - if horizontal_distance > 0: - slope = math.degrees(math.atan(vertical_distance / horizontal_distance)) - return slope - else: - return 0.0 - - except: - return 0.0 - - def _visualize_path(self, path_info): - """ - 可视化路径 - """ - try: - # 移除现有的可视化节点 - if 'visualization' in path_info: - if path_info['visualization'] and not path_info['visualization'].isEmpty(): - path_info['visualization'].removeNode() - - # 创建线条段 - ls = LineSegs() - ls.setThickness(3.0) - - # 根据路径类型和交通状况设置颜色 - path_type = path_info['properties']['type'] - traffic_density = path_info['properties']['traffic_density'] - - # 基础颜色 - if path_type == 'road': - base_color = Vec3(0.3, 0.3, 0.3) # 灰色 - elif path_type == 'trail': - base_color = Vec3(0.5, 0.3, 0.1) # 棕色 - elif path_type == 'river': - base_color = Vec3(0.0, 0.0, 1.0) # 蓝色 - else: - base_color = Vec3(1.0, 1.0, 0.0) # 黄色 - - # 交通密度影响颜色 - if traffic_density > 0.8: - color = Vec3(1.0, 0.0, 0.0) # 红色 - 拥堵 - elif traffic_density > 0.5: - color = Vec3(1.0, 0.5, 0.0) # 橙色 - 繁忙 - else: - color = base_color # 正常 - - ls.setColor(color[0], color[1], color[2], 1.0) - - # 添加线条点 - for waypoint in path_info['waypoints']: - ls.moveTo(waypoint['position']) - - # 创建可视化节点 - visualization_node = self.world.render.attachNewNode(ls.create()) - visualization_node.reparentTo(path_info['node']) - - # 保存可视化节点 - path_info['visualization'] = visualization_node - - except Exception as e: - print(f"可视化路径时出错: {e}") - - def add_waypoint(self, path_info, position, index=None): - """ - 添加路径点 - """ - try: - waypoints = path_info['waypoints'] - - # 确定插入位置 - if index is None: - index = len(waypoints) - - # 创建路径点节点 - waypoint_node = path_info['node'].attachNewNode(f"waypoint_{index}") - waypoint_node.setPos(position) - - # 创建路径点信息 - waypoint_info = { - 'node': waypoint_node, - 'position': position, - 'index': index, - 'connections': [], - 'properties': { - 'speed_limit': path_info['properties']['speed_limit'], - 'stop_sign': False, - 'traffic_light': False, - 'priority': 1.0, - 'wait_time': 0.0 - } - } - - # 插入到路径点列表 - waypoints.insert(index, waypoint_info) - self.waypoints.append(waypoint_info) - - # 更新索引 - for i, wp in enumerate(waypoints): - wp['index'] = i - - # 重新创建路径段 - self._create_path_segments(path_info) - - # 重新可视化路径 - self._visualize_path(path_info) - - # 更新寻路图 - self._update_pathfinding_graph() - - print(f"添加路径点到路径 {path_info['name']}") - return waypoint_info - - except Exception as e: - print(f"添加路径点时出错: {e}") - return None - - def remove_waypoint(self, path_info, waypoint_info): - """ - 移除路径点 - """ - try: - # 从路径中移除 - if waypoint_info in path_info['waypoints']: - path_info['waypoints'].remove(waypoint_info) - - # 从全局列表中移除 - if waypoint_info in self.waypoints: - self.waypoints.remove(waypoint_info) - - # 移除节点 - if waypoint_info['node'] and not waypoint_info['node'].isEmpty(): - waypoint_info['node'].removeNode() - - # 重新创建路径段 - self._create_path_segments(path_info) - - # 重新可视化路径 - self._visualize_path(path_info) - - # 更新寻路图 - self._update_pathfinding_graph() - - print(f"从路径 {path_info['name']} 移除路径点") - return True - - except Exception as e: - print(f"移除路径点时出错: {e}") - return False - - def create_road(self, name, start_point, end_point, width=5.0, lanes=2): - """ - 创建道路 - """ - try: - # 创建道路节点 - road_node = self.world.render.attachNewNode(f"road_{name}") - - # 计算道路方向和长度 - direction = (end_point - start_point).normalized() - length = (end_point - start_point).length() - - # 创建道路几何体 - cm = CardMaker('road_surface') - cm.setFrame(0, length, -width/2, width/2) - road_surface = road_node.attachNewNode(cm.generate()) - - # 设置道路方向和位置 - road_node.setPos(start_point) - road_node.lookAt(end_point) - - # 创建道路信息 - road_info = { - 'name': name, - 'node': road_node, - 'surface': road_surface, - 'start_point': start_point, - 'end_point': end_point, - 'direction': direction, - 'length': length, - 'width': width, - 'lanes': lanes, - 'properties': { - 'surface_type': 'asphalt', # asphalt, concrete, dirt, etc. - 'speed_limit': 50.0, - 'one_way': False, - 'traffic_lights': [], # 交通灯列表 - 'road_markings': True # 道路标记 - } - } - - # 应用道路材质 - self._setup_road_material(road_info) - - # 设置标签 - road_node.setTag("road_name", name) - road_node.setTag("is_scene_element", "1") - road_node.setTag("tree_item_type", "ROAD_NODE") - - # 添加到道路列表 - self.roads.append(road_info) - - # 创建对应路径 - path_points = [start_point, end_point] - path_info = self.create_path(f"path_for_{name}", path_points) - if path_info: - path_info['properties']['type'] = 'road' - path_info['properties']['width'] = width - path_info['properties']['lanes'] = lanes - road_info['path'] = path_info - - print(f"创建道路: {name}") - return road_info - - except Exception as e: - print(f"创建道路时出错: {e}") - return None - - def _setup_road_material(self, road_info): - """ - 设置道路材质 - """ - try: - surface = road_info['surface'] - - # 创建道路材质 - from panda3d.core import Material - road_material = Material() - road_material.setAmbient((0.3, 0.3, 0.3, 1.0)) - road_material.setDiffuse((0.4, 0.4, 0.4, 1.0)) - road_material.setSpecular((0.1, 0.1, 0.1, 1.0)) - road_material.setShininess(10.0) - - surface.setMaterial(road_material) - - # 如果有纹理系统,应用道路纹理 - # 这里可以集成纹理系统 - - except Exception as e: - print(f"设置道路材质时出错: {e}") - - def generate_navigation_mesh(self, terrain_info): - """ - 生成导航网格 - """ - try: - # 基于地形生成导航网格 - # 包括可行走区域、障碍物识别等 - - # 获取地形信息 - heightfield = terrain_info['heightfield'] - if not heightfield: - print("无法获取地形高度图数据") - return None - - # 创建导航网格数据结构 - nav_mesh = { - 'triangles': [], # 三角形网格 - 'vertices': [], # 顶点 - 'obstacles': [], # 障碍物 - 'walkable_areas': [], # 可行走区域 - 'terrain_info': terrain_info - } - - # 生成三角形网格(简化实现) - width = heightfield.getXSize() - height = heightfield.getYSize() - - # 创建顶点 - for y in range(0, height, 4): # 降低分辨率 - for x in range(0, width, 4): - # 获取高度值 - height_value = heightfield.getRed(x, y) - - # 计算世界坐标 - terrain_node = terrain_info['node'] - terrain_pos = terrain_node.getPos() - terrain_scale = terrain_node.getScale() - center_offset = (width - 1) / 2 - - world_x = (x - center_offset) * terrain_scale.getX() + terrain_pos.getX() - world_y = (y - center_offset) * terrain_scale.getY() + terrain_pos.getY() - world_z = height_value * terrain_scale.getZ() + terrain_pos.getZ() - - nav_mesh['vertices'].append(Point3(world_x, world_y, world_z)) - - # 创建三角形(简化实现) - vertex_count = len(nav_mesh['vertices']) - for i in range(0, vertex_count - 1, 2): - if i + 2 < vertex_count: - nav_mesh['triangles'].append([i, i + 1, i + 2]) - - # 识别可行走区域(基于坡度) - for i, vertex in enumerate(nav_mesh['vertices']): - # 检查周围点的坡度 - if i + 1 < len(nav_mesh['vertices']): - next_vertex = nav_mesh['vertices'][i + 1] - slope = self._calculate_slope(vertex, next_vertex) - if slope < 30.0: # 坡度小于30度认为可行走 - nav_mesh['walkable_areas'].append(i) - - print(f"生成导航网格: {vertex_count} 个顶点, {len(nav_mesh['triangles'])} 个三角形") - self.navigation_mesh = nav_mesh - return nav_mesh - - except Exception as e: - print(f"生成导航网格时出错: {e}") - return None - - def find_path(self, start_pos, end_pos, algorithm=None): - """ - 寻找路径 - """ - try: - if not self.pathfinding_enabled: - print("路径寻找已禁用") - return None - - # 选择寻路算法 - if algorithm is None: - algorithm = self.pathfinding_algorithm - - # 执行寻路 - if algorithm == 'a*': - path = self._a_star_pathfinding(start_pos, end_pos) - elif algorithm == 'dijkstra': - path = self._dijkstra_pathfinding(start_pos, end_pos) - elif algorithm == 'rrt': - path = self._rrt_pathfinding(start_pos, end_pos) - else: - # 默认使用A* - path = self._a_star_pathfinding(start_pos, end_pos) - - if path: - print(f"找到路径: {start_pos} -> {end_pos} (长度: {len(path)} 个点)") - return path - else: - print(f"无法找到路径: {start_pos} -> {end_pos}") - return None - - except Exception as e: - print(f"寻找路径时出错: {e}") - return None - - def _a_star_pathfinding(self, start_pos, end_pos): - """ - A*寻路算法 - """ - try: - # 如果没有寻路图,使用路径点 - if not self.graph or len(self.graph) == 0: - return self._simple_a_star(start_pos, end_pos) - - # A*算法实现 - open_set = [] - closed_set = set() - came_from = {} - g_score = {start_pos: 0} - f_score = {start_pos: self._heuristic(start_pos, end_pos)} - - heapq.heappush(open_set, (f_score[start_pos], start_pos)) - - while open_set: - current = heapq.heappop(open_set)[1] - - if self._is_close(current, end_pos, 1.0): - # 重构路径 - path = [] - while current in came_from: - path.append(current) - current = came_from[current] - path.append(start_pos) - path.reverse() - return path - - closed_set.add(current) - - # 检查邻居 - for neighbor in self._get_neighbors(current): - if neighbor in closed_set: - continue - - tentative_g_score = g_score[current] + (neighbor - current).length() - - if neighbor not in g_score or tentative_g_score < g_score[neighbor]: - came_from[neighbor] = current - g_score[neighbor] = tentative_g_score - f_score[neighbor] = g_score[neighbor] + self._heuristic(neighbor, end_pos) - - if neighbor not in [i[1] for i in open_set]: - heapq.heappush(open_set, (f_score[neighbor], neighbor)) - - return None # 未找到路径 - - except Exception as e: - print(f"A*寻路时出错: {e}") - return None - - def _dijkstra_pathfinding(self, start_pos, end_pos): - """ - Dijkstra寻路算法 - """ - try: - # Dijkstra算法实现 - distances = {start_pos: 0} - previous = {} - unvisited = set() - - # 初始化 - for waypoint in self.waypoints: - pos = waypoint['position'] - distances[pos] = float('inf') - unvisited.add(pos) - distances[start_pos] = 0 - unvisited.add(start_pos) - - while unvisited: - # 找到距离最小的节点 - current = min(unvisited, key=lambda x: distances[x]) - unvisited.remove(current) - - if self._is_close(current, end_pos, 1.0): - # 重构路径 - path = [] - while current in previous: - path.append(current) - current = previous[current] - path.append(start_pos) - path.reverse() - return path - - # 更新邻居距离 - for neighbor in self._get_neighbors(current): - if neighbor in unvisited: - alt = distances[current] + (neighbor - current).length() - if alt < distances[neighbor]: - distances[neighbor] = alt - previous[neighbor] = current - - return None # 未找到路径 - - except Exception as e: - print(f"Dijkstra寻路时出错: {e}") - return None - - def _rrt_pathfinding(self, start_pos, end_pos): - """ - RRT (快速随机树) 寻路算法 - """ - try: - # RRT算法简化实现 - nodes = [start_pos] - parent = {start_pos: None} - max_iterations = 1000 - step_size = 5.0 - - for i in range(max_iterations): - # 随机采样或偏向目标 - if random.random() < 0.1: - random_point = end_pos - else: - # 随机点 - random_point = Point3( - start_pos.getX() + random.uniform(-50, 50), - start_pos.getY() + random.uniform(-50, 50), - start_pos.getZ() - ) - - # 找到最近的节点 - nearest_node = min(nodes, key=lambda x: (x - random_point).length()) - - # 向随机点延伸 - direction = (random_point - nearest_node).normalized() - new_point = nearest_node + direction * step_size - - # 检查是否与障碍物碰撞(简化检查) - if self._is_valid_point(new_point): - nodes.append(new_point) - parent[new_point] = nearest_node - - # 检查是否到达目标附近 - if (new_point - end_pos).length() < step_size: - # 重构路径 - path = [end_pos] - current = new_point - while current is not None: - path.append(current) - current = parent.get(current) - path.reverse() - return path - - return None # 未找到路径 - - except Exception as e: - print(f"RRT寻路时出错: {e}") - return None - - def _simple_a_star(self, start_pos, end_pos): - """ - 简化版A*(基于路径点) - """ - try: - if not self.waypoints: - return [start_pos, end_pos] - - # 找到最近的路径点作为起点和终点 - start_waypoint = min(self.waypoints, key=lambda x: (x['position'] - start_pos).length()) - end_waypoint = min(self.waypoints, key=lambda x: (x['position'] - end_pos).length()) - - # 在路径点间寻路 - waypoint_path = self._waypoint_a_star(start_waypoint, end_waypoint) - if not waypoint_path: - return None - - # 构造完整路径 - path = [start_pos] - for waypoint in waypoint_path: - path.append(waypoint['position']) - path.append(end_pos) - - return path - - except Exception as e: - print(f"简化A*寻路时出错: {e}") - return None - - def _waypoint_a_star(self, start_waypoint, end_waypoint): - """ - 路径点间的A*寻路 - """ - try: - open_set = [] - closed_set = set() - came_from = {} - g_score = {start_waypoint: 0} - f_score = {start_waypoint: self._waypoint_heuristic(start_waypoint, end_waypoint)} - - heapq.heappush(open_set, (f_score[start_waypoint], id(start_waypoint), start_waypoint)) - - while open_set: - current = heapq.heappop(open_set)[2] - - if current == end_waypoint: - # 重构路径 - path = [] - while current in came_from: - path.append(current) - current = came_from[current] - path.reverse() - return path - - closed_set.add(current) - - # 检查连接的路径点 - for neighbor in current['connections']: - if neighbor in closed_set: - continue - - # 计算通行成本 - distance = (neighbor['position'] - current['position']).length() - difficulty = 1.0 # 可以从路径属性获取 - tentative_g_score = g_score[current] + distance * difficulty - - if neighbor not in g_score or tentative_g_score < g_score[neighbor]: - came_from[neighbor] = current - g_score[neighbor] = tentative_g_score - f_score[neighbor] = g_score[neighbor] + self._waypoint_heuristic(neighbor, end_waypoint) - - if neighbor not in [item[2] for item in open_set]: - heapq.heappush(open_set, (f_score[neighbor], id(neighbor), neighbor)) - - return None # 未找到路径 - - except Exception as e: - print(f"路径点A*寻路时出错: {e}") - return None - - def _heuristic(self, pos1, pos2): - """ - 启发式函数(欧几里得距离) - """ - return (pos1 - pos2).length() - - def _waypoint_heuristic(self, waypoint1, waypoint2): - """ - 路径点启发式函数 - """ - return (waypoint1['position'] - waypoint2['position']).length() - - def _is_close(self, pos1, pos2, threshold): - """ - 检查两点是否接近 - """ - return (pos1 - pos2).length() <= threshold - - def _get_neighbors(self, pos): - """ - 获取位置的邻居点 - """ - # 简化实现:返回附近的路径点 - neighbors = [] - for waypoint in self.waypoints: - distance = (waypoint['position'] - pos).length() - if 0.1 < distance < 10.0: # 距离阈值 - neighbors.append(waypoint['position']) - return neighbors - - def _is_valid_point(self, point): - """ - 检查点是否有效(不与障碍物碰撞) - """ - # 简化实现:检查是否在合理高度范围内 - return -10.0 <= point.getZ() <= 100.0 - - def create_ai_character(self, name, position, path=None): - """ - 创建AI角色 - """ - try: - # 创建AI角色 - ai_char = AICharacter(name, position, 50, 0.05, 5) - - # 如果提供了路径,设置路径跟随 - if path: - ai_path = AIPath() - for point in path: - ai_path.addPoint(point) - ai_char.setAiPath(ai_path) - - print(f"创建AI角色: {name}") - return ai_char - - except Exception as e: - print(f"创建AI角色时出错: {e}") - return None - - def _update_pathfinding_graph(self): - """ - 更新寻路图 - """ - try: - # 基于路径点构建寻路图 - self.graph = {} - - for waypoint in self.waypoints: - pos = waypoint['position'] - self.graph[pos] = [] - - # 添加连接的邻居 - for connection in waypoint['connections']: - neighbor_pos = connection['position'] - self.graph[pos].append(neighbor_pos) - - print(f"更新寻路图: {len(self.graph)} 个节点") - - except Exception as e: - print(f"更新寻路图时出错: {e}") - - def update_path_system(self, time_delta): - """ - 更新路径系统 - """ - try: - # 更新交通系统 - self._update_traffic_system(time_delta) - - # 更新路径可视化 - self._update_path_visualization() - - # 更新AI角色 - # 这里可以更新所有AI角色的状态 - - except Exception as e: - print(f"更新路径系统时出错: {e}") - - def _update_traffic_system(self, time_delta): - """ - 更新交通系统 - """ - try: - current_time = self.world.globalClock.getFrameTime() - - # 更新每条路径的交通密度 - for path_info in self.paths: - # 模拟交通流量变化 - if 'traffic_density' in path_info['properties']: - # 简单的交通流量模拟 - base_density = 0.3 - time_factor = (math.sin(current_time * 0.1) + 1.0) * 0.5 - path_info['properties']['traffic_density'] = base_density + time_factor * 0.4 - - except Exception as e: - print(f"更新交通系统时出错: {e}") - - def _update_path_visualization(self): - """ - 更新路径可视化 - """ - try: - # 重新可视化所有路径以反映交通状况 - for path_info in self.paths: - self._visualize_path(path_info) - - except Exception as e: - print(f"更新路径可视化时出错: {e}") - - def set_path_properties(self, path_info, properties): - """ - 设置路径属性 - """ - try: - # 更新路径属性 - path_info['properties'].update(properties) - - # 重新可视化路径 - self._visualize_path(path_info) - - # 更新寻路图(如果影响通行难度) - if 'difficulty' in properties or 'speed_limit' in properties: - self._update_pathfinding_graph() - - print(f"更新路径 {path_info['name']} 属性") - return True - - except Exception as e: - print(f"设置路径属性时出错: {e}") - return False - - def get_path_stats(self): - """ - 获取路径统计信息 - """ - stats = { - 'total_paths': len(self.paths), - 'total_waypoints': len(self.waypoints), - 'total_roads': len(self.roads), - 'path_types': {}, - 'total_length': 0, - 'algorithm': self.pathfinding_algorithm, - 'traffic_stats': { - 'total_density': 0.0, - 'avg_density': 0.0 - } - } - - # 统计路径类型和总长度 - total_density = 0.0 - for path_info in self.paths: - path_type = path_info['properties']['type'] - if path_type in stats['path_types']: - stats['path_types'][path_type] += 1 - else: - stats['path_types'][path_type] = 1 - - # 计算路径长度 - for segment in path_info['segments']: - stats['total_length'] += segment['length'] - - # 累计交通密度 - total_density += path_info['properties'].get('traffic_density', 0.0) - - # 计算平均交通密度 - if len(self.paths) > 0: - stats['traffic_stats']['avg_density'] = total_density / len(self.paths) - stats['traffic_stats']['total_density'] = total_density - - return stats - - def save_path_data(self, output_path): - """ - 保存路径数据 - """ - try: - # 收集路径数据 - path_data = { - 'paths': [], - 'roads': [], - 'settings': { - 'pathfinding_algorithm': self.pathfinding_algorithm, - 'pathfinding_enabled': self.pathfinding_enabled - } - } - - # 保存路径信息 - for path_info in self.paths: - path_dict = { - 'name': path_info['name'], - 'points': [], - 'properties': path_info['properties'] - } - - # 保存路径点 - for waypoint in path_info['waypoints']: - point_dict = { - 'position': [waypoint['position'].x, - waypoint['position'].y, - waypoint['position'].z], - 'properties': waypoint['properties'] - } - path_dict['points'].append(point_dict) - - path_data['paths'].append(path_dict) - - # 保存道路信息 - for road_info in self.roads: - road_dict = { - 'name': road_info['name'], - 'start_point': [road_info['start_point'].x, - road_info['start_point'].y, - road_info['start_point'].z], - 'end_point': [road_info['end_point'].x, - road_info['end_point'].y, - road_info['end_point'].z], - 'width': road_info['width'], - 'lanes': road_info['lanes'], - 'properties': road_info['properties'] - } - path_data['roads'].append(road_dict) - - # 确保输出目录存在 - output_dir = os.path.dirname(output_path) - if not os.path.exists(output_dir): - os.makedirs(output_dir) - - # 写入JSON文件 - with open(output_path, 'w', encoding='utf-8') as f: - json.dump(path_data, f, indent=2, ensure_ascii=False) - - print(f"路径数据保存成功: {output_path}") - return True - - except Exception as e: - print(f"保存路径数据时出错: {e}") - return False - - def load_path_data(self, input_path): - """ - 加载路径数据 - """ - try: - if not os.path.exists(input_path): - print(f"路径数据文件不存在: {input_path}") - return False - - # 读取JSON文件 - with open(input_path, 'r', encoding='utf-8') as f: - path_data = json.load(f) - - # 清除现有路径 - self.clear_all_paths() - - # 恢复设置 - if 'settings' in path_data: - settings = path_data['settings'] - self.pathfinding_algorithm = settings.get('pathfinding_algorithm', 'a*') - self.pathfinding_enabled = settings.get('pathfinding_enabled', True) - - # 创建路径 - for path_dict in path_data['paths']: - points = [] - for point_dict in path_dict['points']: - position = Point3(point_dict['position'][0], - point_dict['position'][1], - point_dict['position'][2]) - points.append(position) - - path_info = self.create_path(path_dict['name'], points) - if path_info: - self.set_path_properties(path_info, path_dict['properties']) - - # 创建道路 - for road_dict in path_data['roads']: - start_point = Point3(road_dict['start_point'][0], - road_dict['start_point'][1], - road_dict['start_point'][2]) - end_point = Point3(road_dict['end_point'][0], - road_dict['end_point'][1], - road_dict['end_point'][2]) - - self.create_road(road_dict['name'], start_point, end_point, - road_dict['width'], road_dict['lanes']) - - print(f"路径数据加载成功: {input_path}") - return True - - except Exception as e: - print(f"加载路径数据时出错: {e}") - return False - - def clear_all_paths(self): - """ - 清除所有路径 - """ - try: - # 移除所有路径节点 - for path_info in self.paths: - if path_info['node'] and not path_info['node'].isEmpty(): - path_info['node'].removeNode() - - # 移除所有道路节点 - for road_info in self.roads: - if road_info['node'] and not road_info['node'].isEmpty(): - road_info['node'].removeNode() - - # 清空数据结构 - self.paths = [] - self.waypoints = [] - self.roads = [] - self.graph = {} - - print("清除所有路径") - return True - - except Exception as e: - print(f"清除所有路径时出错: {e}") - return False - - def paint_path(self, start_point, end_point, path_type='generic', width=2.0): - """ - 绘制路径(笔刷工具) - """ - try: - # 创建路径名称 - path_name = f"painted_path_{len(self.paths)}" - - # 创建路径点 - points = [start_point, end_point] - - # 创建路径 - path_info = self.create_path(path_name, points) - if path_info: - # 设置路径属性 - properties = { - 'type': path_type, - 'width': width - } - self.set_path_properties(path_info, properties) - - print(f"绘制路径: {path_name}") - return path_info - - except Exception as e: - print(f"绘制路径时出错: {e}") - return None - - def set_pathfinding_algorithm(self, algorithm): - """ - 设置寻路算法 - """ - valid_algorithms = ['a*', 'dijkstra', 'rrt'] - if algorithm in valid_algorithms: - self.pathfinding_algorithm = algorithm - print(f"寻路算法设置为: {algorithm}") - return True - else: - print(f"无效的寻路算法: {algorithm}") - return False - - def get_navigation_mesh_stats(self): - """ - 获取导航网格统计信息 - """ - if not self.navigation_mesh: - return {'generated': False} - - return { - 'generated': True, - 'vertices': len(self.navigation_mesh['vertices']), - 'triangles': len(self.navigation_mesh['triangles']), - 'obstacles': len(self.navigation_mesh['obstacles']), - 'walkable_areas': len(self.navigation_mesh['walkable_areas']) +""" +地形路径和路网系统 +提供完整的路径创建、编辑、导航、寻路算法和可视化功能 +""" + +from panda3d.core import NodePath, Vec3, Point3, BitMask32 +from panda3d.core import CardMaker, LineSegs +from panda3d.core import Texture, TextureStage +from panda3d.ai import AIPathFinder, AIPath, AICharacter +import math +import json +import os +import heapq + +class PathSystem: + """ + 地形路径和路网系统类 + 提供路径创建、编辑、导航、寻路算法和可视化功能 + """ + + def __init__(self, world): + self.world = world + self.paths = [] # 存储路径 + self.waypoints = [] # 存储路径点 + self.roads = [] # 存储道路 + self.navigation_mesh = None # 导航网格 + self.pathfinding_enabled = True + self.graph = {} # 寻路图 + self.traffic_system = {} # 交通系统 + self.pathfinding_algorithm = 'a*' # 寻路算法 ('a*', 'dijkstra', 'rrt') + + def create_path(self, name, points=None): + """ + 创建路径 + name: 路径名称 + points: 路径点列表 + """ + try: + # 创建路径节点 + path_node = self.world.render.attachNewNode(f"path_{name}") + + # 创建路径信息 + path_info = { + 'name': name, + 'node': path_node, + 'points': points if points else [], + 'waypoints': [], # 路径点对象 + 'segments': [], # 路段信息 + 'properties': { + 'width': 2.0, + 'type': 'generic', # generic, road, trail, river, etc. + 'speed_limit': 50.0, # km/h + 'one_way': False, + 'lanes': 1, + 'difficulty': 1.0, # 通行难度系数 + 'traffic_density': 0.0 # 交通密度 + } + } + + # 如果提供了点,创建路径点 + if points: + self._create_waypoints(path_info, points) + + # 设置标签 + path_node.setTag("path_name", name) + path_node.setTag("is_scene_element", "1") + path_node.setTag("tree_item_type", "PATH_NODE") + + # 添加到路径列表 + self.paths.append(path_info) + + # 更新寻路图 + self._update_pathfinding_graph() + + print(f"创建路径: {name}") + return path_info + + except Exception as e: + print(f"创建路径时出错: {e}") + return None + + def _create_waypoints(self, path_info, points): + """ + 创建路径点 + """ + try: + waypoints = [] + + for i, point in enumerate(points): + # 创建路径点节点 + waypoint_node = path_info['node'].attachNewNode(f"waypoint_{i}") + waypoint_node.setPos(point) + + # 创建路径点信息 + waypoint_info = { + 'node': waypoint_node, + 'position': point, + 'index': i, + 'connections': [], # 连接的其他路径点 + 'properties': { + 'speed_limit': path_info['properties']['speed_limit'], + 'stop_sign': False, + 'traffic_light': False, + 'priority': 1.0, # 路径点优先级 + 'wait_time': 0.0 # 等待时间 + } + } + + waypoints.append(waypoint_info) + self.waypoints.append(waypoint_info) + + path_info['waypoints'] = waypoints + + # 创建路径段 + self._create_path_segments(path_info) + + # 可视化路径 + self._visualize_path(path_info) + + except Exception as e: + print(f"创建路径点时出错: {e}") + + def _create_path_segments(self, path_info): + """ + 创建路径段 + """ + try: + waypoints = path_info['waypoints'] + segments = [] + + for i in range(len(waypoints) - 1): + start_waypoint = waypoints[i] + end_waypoint = waypoints[i + 1] + + # 计算路段信息 + segment_length = (end_waypoint['position'] - start_waypoint['position']).length() + segment_direction = (end_waypoint['position'] - start_waypoint['position']).normalized() + segment_slope = self._calculate_slope(start_waypoint['position'], end_waypoint['position']) + + segment_info = { + 'start': start_waypoint, + 'end': end_waypoint, + 'length': segment_length, + 'direction': segment_direction, + 'slope': segment_slope, + 'difficulty': 1.0 + abs(segment_slope) * 0.1, # 坡度影响通行难度 + 'traffic_flow': 0.0, # 交通流量 + 'last_update': self.world.globalClock.getFrameTime() + } + + segments.append(segment_info) + + # 建立连接关系 + start_waypoint['connections'].append(end_waypoint) + if not path_info['properties']['one_way']: + end_waypoint['connections'].append(start_waypoint) + + path_info['segments'] = segments + + except Exception as e: + print(f"创建路径段时出错: {e}") + + def _calculate_slope(self, start_pos, end_pos): + """ + 计算两点间的坡度 + """ + try: + horizontal_distance = Vec3(end_pos.getX() - start_pos.getX(), + end_pos.getY() - start_pos.getY(), 0).length() + vertical_distance = end_pos.getZ() - start_pos.getZ() + + if horizontal_distance > 0: + slope = math.degrees(math.atan(vertical_distance / horizontal_distance)) + return slope + else: + return 0.0 + + except: + return 0.0 + + def _visualize_path(self, path_info): + """ + 可视化路径 + """ + try: + # 移除现有的可视化节点 + if 'visualization' in path_info: + if path_info['visualization'] and not path_info['visualization'].isEmpty(): + path_info['visualization'].removeNode() + + # 创建线条段 + ls = LineSegs() + ls.setThickness(3.0) + + # 根据路径类型和交通状况设置颜色 + path_type = path_info['properties']['type'] + traffic_density = path_info['properties']['traffic_density'] + + # 基础颜色 + if path_type == 'road': + base_color = Vec3(0.3, 0.3, 0.3) # 灰色 + elif path_type == 'trail': + base_color = Vec3(0.5, 0.3, 0.1) # 棕色 + elif path_type == 'river': + base_color = Vec3(0.0, 0.0, 1.0) # 蓝色 + else: + base_color = Vec3(1.0, 1.0, 0.0) # 黄色 + + # 交通密度影响颜色 + if traffic_density > 0.8: + color = Vec3(1.0, 0.0, 0.0) # 红色 - 拥堵 + elif traffic_density > 0.5: + color = Vec3(1.0, 0.5, 0.0) # 橙色 - 繁忙 + else: + color = base_color # 正常 + + ls.setColor(color[0], color[1], color[2], 1.0) + + # 添加线条点 + for waypoint in path_info['waypoints']: + ls.moveTo(waypoint['position']) + + # 创建可视化节点 + visualization_node = self.world.render.attachNewNode(ls.create()) + visualization_node.reparentTo(path_info['node']) + + # 保存可视化节点 + path_info['visualization'] = visualization_node + + except Exception as e: + print(f"可视化路径时出错: {e}") + + def add_waypoint(self, path_info, position, index=None): + """ + 添加路径点 + """ + try: + waypoints = path_info['waypoints'] + + # 确定插入位置 + if index is None: + index = len(waypoints) + + # 创建路径点节点 + waypoint_node = path_info['node'].attachNewNode(f"waypoint_{index}") + waypoint_node.setPos(position) + + # 创建路径点信息 + waypoint_info = { + 'node': waypoint_node, + 'position': position, + 'index': index, + 'connections': [], + 'properties': { + 'speed_limit': path_info['properties']['speed_limit'], + 'stop_sign': False, + 'traffic_light': False, + 'priority': 1.0, + 'wait_time': 0.0 + } + } + + # 插入到路径点列表 + waypoints.insert(index, waypoint_info) + self.waypoints.append(waypoint_info) + + # 更新索引 + for i, wp in enumerate(waypoints): + wp['index'] = i + + # 重新创建路径段 + self._create_path_segments(path_info) + + # 重新可视化路径 + self._visualize_path(path_info) + + # 更新寻路图 + self._update_pathfinding_graph() + + print(f"添加路径点到路径 {path_info['name']}") + return waypoint_info + + except Exception as e: + print(f"添加路径点时出错: {e}") + return None + + def remove_waypoint(self, path_info, waypoint_info): + """ + 移除路径点 + """ + try: + # 从路径中移除 + if waypoint_info in path_info['waypoints']: + path_info['waypoints'].remove(waypoint_info) + + # 从全局列表中移除 + if waypoint_info in self.waypoints: + self.waypoints.remove(waypoint_info) + + # 移除节点 + if waypoint_info['node'] and not waypoint_info['node'].isEmpty(): + waypoint_info['node'].removeNode() + + # 重新创建路径段 + self._create_path_segments(path_info) + + # 重新可视化路径 + self._visualize_path(path_info) + + # 更新寻路图 + self._update_pathfinding_graph() + + print(f"从路径 {path_info['name']} 移除路径点") + return True + + except Exception as e: + print(f"移除路径点时出错: {e}") + return False + + def create_road(self, name, start_point, end_point, width=5.0, lanes=2): + """ + 创建道路 + """ + try: + # 创建道路节点 + road_node = self.world.render.attachNewNode(f"road_{name}") + + # 计算道路方向和长度 + direction = (end_point - start_point).normalized() + length = (end_point - start_point).length() + + # 创建道路几何体 + cm = CardMaker('road_surface') + cm.setFrame(0, length, -width/2, width/2) + road_surface = road_node.attachNewNode(cm.generate()) + + # 设置道路方向和位置 + road_node.setPos(start_point) + road_node.lookAt(end_point) + + # 创建道路信息 + road_info = { + 'name': name, + 'node': road_node, + 'surface': road_surface, + 'start_point': start_point, + 'end_point': end_point, + 'direction': direction, + 'length': length, + 'width': width, + 'lanes': lanes, + 'properties': { + 'surface_type': 'asphalt', # asphalt, concrete, dirt, etc. + 'speed_limit': 50.0, + 'one_way': False, + 'traffic_lights': [], # 交通灯列表 + 'road_markings': True # 道路标记 + } + } + + # 应用道路材质 + self._setup_road_material(road_info) + + # 设置标签 + road_node.setTag("road_name", name) + road_node.setTag("is_scene_element", "1") + road_node.setTag("tree_item_type", "ROAD_NODE") + + # 添加到道路列表 + self.roads.append(road_info) + + # 创建对应路径 + path_points = [start_point, end_point] + path_info = self.create_path(f"path_for_{name}", path_points) + if path_info: + path_info['properties']['type'] = 'road' + path_info['properties']['width'] = width + path_info['properties']['lanes'] = lanes + road_info['path'] = path_info + + print(f"创建道路: {name}") + return road_info + + except Exception as e: + print(f"创建道路时出错: {e}") + return None + + def _setup_road_material(self, road_info): + """ + 设置道路材质 + """ + try: + surface = road_info['surface'] + + # 创建道路材质 + from panda3d.core import Material + road_material = Material() + road_material.setAmbient((0.3, 0.3, 0.3, 1.0)) + road_material.setDiffuse((0.4, 0.4, 0.4, 1.0)) + road_material.setSpecular((0.1, 0.1, 0.1, 1.0)) + road_material.setShininess(10.0) + + surface.setMaterial(road_material) + + # 如果有纹理系统,应用道路纹理 + # 这里可以集成纹理系统 + + except Exception as e: + print(f"设置道路材质时出错: {e}") + + def generate_navigation_mesh(self, terrain_info): + """ + 生成导航网格 + """ + try: + # 基于地形生成导航网格 + # 包括可行走区域、障碍物识别等 + + # 获取地形信息 + heightfield = terrain_info['heightfield'] + if not heightfield: + print("无法获取地形高度图数据") + return None + + # 创建导航网格数据结构 + nav_mesh = { + 'triangles': [], # 三角形网格 + 'vertices': [], # 顶点 + 'obstacles': [], # 障碍物 + 'walkable_areas': [], # 可行走区域 + 'terrain_info': terrain_info + } + + # 生成三角形网格(简化实现) + width = heightfield.getXSize() + height = heightfield.getYSize() + + # 创建顶点 + for y in range(0, height, 4): # 降低分辨率 + for x in range(0, width, 4): + # 获取高度值 + height_value = heightfield.getRed(x, y) + + # 计算世界坐标 + terrain_node = terrain_info['node'] + terrain_pos = terrain_node.getPos() + terrain_scale = terrain_node.getScale() + center_offset = (width - 1) / 2 + + world_x = (x - center_offset) * terrain_scale.getX() + terrain_pos.getX() + world_y = (y - center_offset) * terrain_scale.getY() + terrain_pos.getY() + world_z = height_value * terrain_scale.getZ() + terrain_pos.getZ() + + nav_mesh['vertices'].append(Point3(world_x, world_y, world_z)) + + # 创建三角形(简化实现) + vertex_count = len(nav_mesh['vertices']) + for i in range(0, vertex_count - 1, 2): + if i + 2 < vertex_count: + nav_mesh['triangles'].append([i, i + 1, i + 2]) + + # 识别可行走区域(基于坡度) + for i, vertex in enumerate(nav_mesh['vertices']): + # 检查周围点的坡度 + if i + 1 < len(nav_mesh['vertices']): + next_vertex = nav_mesh['vertices'][i + 1] + slope = self._calculate_slope(vertex, next_vertex) + if slope < 30.0: # 坡度小于30度认为可行走 + nav_mesh['walkable_areas'].append(i) + + print(f"生成导航网格: {vertex_count} 个顶点, {len(nav_mesh['triangles'])} 个三角形") + self.navigation_mesh = nav_mesh + return nav_mesh + + except Exception as e: + print(f"生成导航网格时出错: {e}") + return None + + def find_path(self, start_pos, end_pos, algorithm=None): + """ + 寻找路径 + """ + try: + if not self.pathfinding_enabled: + print("路径寻找已禁用") + return None + + # 选择寻路算法 + if algorithm is None: + algorithm = self.pathfinding_algorithm + + # 执行寻路 + if algorithm == 'a*': + path = self._a_star_pathfinding(start_pos, end_pos) + elif algorithm == 'dijkstra': + path = self._dijkstra_pathfinding(start_pos, end_pos) + elif algorithm == 'rrt': + path = self._rrt_pathfinding(start_pos, end_pos) + else: + # 默认使用A* + path = self._a_star_pathfinding(start_pos, end_pos) + + if path: + print(f"找到路径: {start_pos} -> {end_pos} (长度: {len(path)} 个点)") + return path + else: + print(f"无法找到路径: {start_pos} -> {end_pos}") + return None + + except Exception as e: + print(f"寻找路径时出错: {e}") + return None + + def _a_star_pathfinding(self, start_pos, end_pos): + """ + A*寻路算法 + """ + try: + # 如果没有寻路图,使用路径点 + if not self.graph or len(self.graph) == 0: + return self._simple_a_star(start_pos, end_pos) + + # A*算法实现 + open_set = [] + closed_set = set() + came_from = {} + g_score = {start_pos: 0} + f_score = {start_pos: self._heuristic(start_pos, end_pos)} + + heapq.heappush(open_set, (f_score[start_pos], start_pos)) + + while open_set: + current = heapq.heappop(open_set)[1] + + if self._is_close(current, end_pos, 1.0): + # 重构路径 + path = [] + while current in came_from: + path.append(current) + current = came_from[current] + path.append(start_pos) + path.reverse() + return path + + closed_set.add(current) + + # 检查邻居 + for neighbor in self._get_neighbors(current): + if neighbor in closed_set: + continue + + tentative_g_score = g_score[current] + (neighbor - current).length() + + if neighbor not in g_score or tentative_g_score < g_score[neighbor]: + came_from[neighbor] = current + g_score[neighbor] = tentative_g_score + f_score[neighbor] = g_score[neighbor] + self._heuristic(neighbor, end_pos) + + if neighbor not in [i[1] for i in open_set]: + heapq.heappush(open_set, (f_score[neighbor], neighbor)) + + return None # 未找到路径 + + except Exception as e: + print(f"A*寻路时出错: {e}") + return None + + def _dijkstra_pathfinding(self, start_pos, end_pos): + """ + Dijkstra寻路算法 + """ + try: + # Dijkstra算法实现 + distances = {start_pos: 0} + previous = {} + unvisited = set() + + # 初始化 + for waypoint in self.waypoints: + pos = waypoint['position'] + distances[pos] = float('inf') + unvisited.add(pos) + distances[start_pos] = 0 + unvisited.add(start_pos) + + while unvisited: + # 找到距离最小的节点 + current = min(unvisited, key=lambda x: distances[x]) + unvisited.remove(current) + + if self._is_close(current, end_pos, 1.0): + # 重构路径 + path = [] + while current in previous: + path.append(current) + current = previous[current] + path.append(start_pos) + path.reverse() + return path + + # 更新邻居距离 + for neighbor in self._get_neighbors(current): + if neighbor in unvisited: + alt = distances[current] + (neighbor - current).length() + if alt < distances[neighbor]: + distances[neighbor] = alt + previous[neighbor] = current + + return None # 未找到路径 + + except Exception as e: + print(f"Dijkstra寻路时出错: {e}") + return None + + def _rrt_pathfinding(self, start_pos, end_pos): + """ + RRT (快速随机树) 寻路算法 + """ + try: + # RRT算法简化实现 + nodes = [start_pos] + parent = {start_pos: None} + max_iterations = 1000 + step_size = 5.0 + + for i in range(max_iterations): + # 随机采样或偏向目标 + if random.random() < 0.1: + random_point = end_pos + else: + # 随机点 + random_point = Point3( + start_pos.getX() + random.uniform(-50, 50), + start_pos.getY() + random.uniform(-50, 50), + start_pos.getZ() + ) + + # 找到最近的节点 + nearest_node = min(nodes, key=lambda x: (x - random_point).length()) + + # 向随机点延伸 + direction = (random_point - nearest_node).normalized() + new_point = nearest_node + direction * step_size + + # 检查是否与障碍物碰撞(简化检查) + if self._is_valid_point(new_point): + nodes.append(new_point) + parent[new_point] = nearest_node + + # 检查是否到达目标附近 + if (new_point - end_pos).length() < step_size: + # 重构路径 + path = [end_pos] + current = new_point + while current is not None: + path.append(current) + current = parent.get(current) + path.reverse() + return path + + return None # 未找到路径 + + except Exception as e: + print(f"RRT寻路时出错: {e}") + return None + + def _simple_a_star(self, start_pos, end_pos): + """ + 简化版A*(基于路径点) + """ + try: + if not self.waypoints: + return [start_pos, end_pos] + + # 找到最近的路径点作为起点和终点 + start_waypoint = min(self.waypoints, key=lambda x: (x['position'] - start_pos).length()) + end_waypoint = min(self.waypoints, key=lambda x: (x['position'] - end_pos).length()) + + # 在路径点间寻路 + waypoint_path = self._waypoint_a_star(start_waypoint, end_waypoint) + if not waypoint_path: + return None + + # 构造完整路径 + path = [start_pos] + for waypoint in waypoint_path: + path.append(waypoint['position']) + path.append(end_pos) + + return path + + except Exception as e: + print(f"简化A*寻路时出错: {e}") + return None + + def _waypoint_a_star(self, start_waypoint, end_waypoint): + """ + 路径点间的A*寻路 + """ + try: + open_set = [] + closed_set = set() + came_from = {} + g_score = {start_waypoint: 0} + f_score = {start_waypoint: self._waypoint_heuristic(start_waypoint, end_waypoint)} + + heapq.heappush(open_set, (f_score[start_waypoint], id(start_waypoint), start_waypoint)) + + while open_set: + current = heapq.heappop(open_set)[2] + + if current == end_waypoint: + # 重构路径 + path = [] + while current in came_from: + path.append(current) + current = came_from[current] + path.reverse() + return path + + closed_set.add(current) + + # 检查连接的路径点 + for neighbor in current['connections']: + if neighbor in closed_set: + continue + + # 计算通行成本 + distance = (neighbor['position'] - current['position']).length() + difficulty = 1.0 # 可以从路径属性获取 + tentative_g_score = g_score[current] + distance * difficulty + + if neighbor not in g_score or tentative_g_score < g_score[neighbor]: + came_from[neighbor] = current + g_score[neighbor] = tentative_g_score + f_score[neighbor] = g_score[neighbor] + self._waypoint_heuristic(neighbor, end_waypoint) + + if neighbor not in [item[2] for item in open_set]: + heapq.heappush(open_set, (f_score[neighbor], id(neighbor), neighbor)) + + return None # 未找到路径 + + except Exception as e: + print(f"路径点A*寻路时出错: {e}") + return None + + def _heuristic(self, pos1, pos2): + """ + 启发式函数(欧几里得距离) + """ + return (pos1 - pos2).length() + + def _waypoint_heuristic(self, waypoint1, waypoint2): + """ + 路径点启发式函数 + """ + return (waypoint1['position'] - waypoint2['position']).length() + + def _is_close(self, pos1, pos2, threshold): + """ + 检查两点是否接近 + """ + return (pos1 - pos2).length() <= threshold + + def _get_neighbors(self, pos): + """ + 获取位置的邻居点 + """ + # 简化实现:返回附近的路径点 + neighbors = [] + for waypoint in self.waypoints: + distance = (waypoint['position'] - pos).length() + if 0.1 < distance < 10.0: # 距离阈值 + neighbors.append(waypoint['position']) + return neighbors + + def _is_valid_point(self, point): + """ + 检查点是否有效(不与障碍物碰撞) + """ + # 简化实现:检查是否在合理高度范围内 + return -10.0 <= point.getZ() <= 100.0 + + def create_ai_character(self, name, position, path=None): + """ + 创建AI角色 + """ + try: + # 创建AI角色 + ai_char = AICharacter(name, position, 50, 0.05, 5) + + # 如果提供了路径,设置路径跟随 + if path: + ai_path = AIPath() + for point in path: + ai_path.addPoint(point) + ai_char.setAiPath(ai_path) + + print(f"创建AI角色: {name}") + return ai_char + + except Exception as e: + print(f"创建AI角色时出错: {e}") + return None + + def _update_pathfinding_graph(self): + """ + 更新寻路图 + """ + try: + # 基于路径点构建寻路图 + self.graph = {} + + for waypoint in self.waypoints: + pos = waypoint['position'] + self.graph[pos] = [] + + # 添加连接的邻居 + for connection in waypoint['connections']: + neighbor_pos = connection['position'] + self.graph[pos].append(neighbor_pos) + + print(f"更新寻路图: {len(self.graph)} 个节点") + + except Exception as e: + print(f"更新寻路图时出错: {e}") + + def update_path_system(self, time_delta): + """ + 更新路径系统 + """ + try: + # 更新交通系统 + self._update_traffic_system(time_delta) + + # 更新路径可视化 + self._update_path_visualization() + + # 更新AI角色 + # 这里可以更新所有AI角色的状态 + + except Exception as e: + print(f"更新路径系统时出错: {e}") + + def _update_traffic_system(self, time_delta): + """ + 更新交通系统 + """ + try: + current_time = self.world.globalClock.getFrameTime() + + # 更新每条路径的交通密度 + for path_info in self.paths: + # 模拟交通流量变化 + if 'traffic_density' in path_info['properties']: + # 简单的交通流量模拟 + base_density = 0.3 + time_factor = (math.sin(current_time * 0.1) + 1.0) * 0.5 + path_info['properties']['traffic_density'] = base_density + time_factor * 0.4 + + except Exception as e: + print(f"更新交通系统时出错: {e}") + + def _update_path_visualization(self): + """ + 更新路径可视化 + """ + try: + # 重新可视化所有路径以反映交通状况 + for path_info in self.paths: + self._visualize_path(path_info) + + except Exception as e: + print(f"更新路径可视化时出错: {e}") + + def set_path_properties(self, path_info, properties): + """ + 设置路径属性 + """ + try: + # 更新路径属性 + path_info['properties'].update(properties) + + # 重新可视化路径 + self._visualize_path(path_info) + + # 更新寻路图(如果影响通行难度) + if 'difficulty' in properties or 'speed_limit' in properties: + self._update_pathfinding_graph() + + print(f"更新路径 {path_info['name']} 属性") + return True + + except Exception as e: + print(f"设置路径属性时出错: {e}") + return False + + def get_path_stats(self): + """ + 获取路径统计信息 + """ + stats = { + 'total_paths': len(self.paths), + 'total_waypoints': len(self.waypoints), + 'total_roads': len(self.roads), + 'path_types': {}, + 'total_length': 0, + 'algorithm': self.pathfinding_algorithm, + 'traffic_stats': { + 'total_density': 0.0, + 'avg_density': 0.0 + } + } + + # 统计路径类型和总长度 + total_density = 0.0 + for path_info in self.paths: + path_type = path_info['properties']['type'] + if path_type in stats['path_types']: + stats['path_types'][path_type] += 1 + else: + stats['path_types'][path_type] = 1 + + # 计算路径长度 + for segment in path_info['segments']: + stats['total_length'] += segment['length'] + + # 累计交通密度 + total_density += path_info['properties'].get('traffic_density', 0.0) + + # 计算平均交通密度 + if len(self.paths) > 0: + stats['traffic_stats']['avg_density'] = total_density / len(self.paths) + stats['traffic_stats']['total_density'] = total_density + + return stats + + def save_path_data(self, output_path): + """ + 保存路径数据 + """ + try: + # 收集路径数据 + path_data = { + 'paths': [], + 'roads': [], + 'settings': { + 'pathfinding_algorithm': self.pathfinding_algorithm, + 'pathfinding_enabled': self.pathfinding_enabled + } + } + + # 保存路径信息 + for path_info in self.paths: + path_dict = { + 'name': path_info['name'], + 'points': [], + 'properties': path_info['properties'] + } + + # 保存路径点 + for waypoint in path_info['waypoints']: + point_dict = { + 'position': [waypoint['position'].x, + waypoint['position'].y, + waypoint['position'].z], + 'properties': waypoint['properties'] + } + path_dict['points'].append(point_dict) + + path_data['paths'].append(path_dict) + + # 保存道路信息 + for road_info in self.roads: + road_dict = { + 'name': road_info['name'], + 'start_point': [road_info['start_point'].x, + road_info['start_point'].y, + road_info['start_point'].z], + 'end_point': [road_info['end_point'].x, + road_info['end_point'].y, + road_info['end_point'].z], + 'width': road_info['width'], + 'lanes': road_info['lanes'], + 'properties': road_info['properties'] + } + path_data['roads'].append(road_dict) + + # 确保输出目录存在 + output_dir = os.path.dirname(output_path) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + # 写入JSON文件 + with open(output_path, 'w', encoding='utf-8') as f: + json.dump(path_data, f, indent=2, ensure_ascii=False) + + print(f"路径数据保存成功: {output_path}") + return True + + except Exception as e: + print(f"保存路径数据时出错: {e}") + return False + + def load_path_data(self, input_path): + """ + 加载路径数据 + """ + try: + if not os.path.exists(input_path): + print(f"路径数据文件不存在: {input_path}") + return False + + # 读取JSON文件 + with open(input_path, 'r', encoding='utf-8') as f: + path_data = json.load(f) + + # 清除现有路径 + self.clear_all_paths() + + # 恢复设置 + if 'settings' in path_data: + settings = path_data['settings'] + self.pathfinding_algorithm = settings.get('pathfinding_algorithm', 'a*') + self.pathfinding_enabled = settings.get('pathfinding_enabled', True) + + # 创建路径 + for path_dict in path_data['paths']: + points = [] + for point_dict in path_dict['points']: + position = Point3(point_dict['position'][0], + point_dict['position'][1], + point_dict['position'][2]) + points.append(position) + + path_info = self.create_path(path_dict['name'], points) + if path_info: + self.set_path_properties(path_info, path_dict['properties']) + + # 创建道路 + for road_dict in path_data['roads']: + start_point = Point3(road_dict['start_point'][0], + road_dict['start_point'][1], + road_dict['start_point'][2]) + end_point = Point3(road_dict['end_point'][0], + road_dict['end_point'][1], + road_dict['end_point'][2]) + + self.create_road(road_dict['name'], start_point, end_point, + road_dict['width'], road_dict['lanes']) + + print(f"路径数据加载成功: {input_path}") + return True + + except Exception as e: + print(f"加载路径数据时出错: {e}") + return False + + def clear_all_paths(self): + """ + 清除所有路径 + """ + try: + # 移除所有路径节点 + for path_info in self.paths: + if path_info['node'] and not path_info['node'].isEmpty(): + path_info['node'].removeNode() + + # 移除所有道路节点 + for road_info in self.roads: + if road_info['node'] and not road_info['node'].isEmpty(): + road_info['node'].removeNode() + + # 清空数据结构 + self.paths = [] + self.waypoints = [] + self.roads = [] + self.graph = {} + + print("清除所有路径") + return True + + except Exception as e: + print(f"清除所有路径时出错: {e}") + return False + + def paint_path(self, start_point, end_point, path_type='generic', width=2.0): + """ + 绘制路径(笔刷工具) + """ + try: + # 创建路径名称 + path_name = f"painted_path_{len(self.paths)}" + + # 创建路径点 + points = [start_point, end_point] + + # 创建路径 + path_info = self.create_path(path_name, points) + if path_info: + # 设置路径属性 + properties = { + 'type': path_type, + 'width': width + } + self.set_path_properties(path_info, properties) + + print(f"绘制路径: {path_name}") + return path_info + + except Exception as e: + print(f"绘制路径时出错: {e}") + return None + + def set_pathfinding_algorithm(self, algorithm): + """ + 设置寻路算法 + """ + valid_algorithms = ['a*', 'dijkstra', 'rrt'] + if algorithm in valid_algorithms: + self.pathfinding_algorithm = algorithm + print(f"寻路算法设置为: {algorithm}") + return True + else: + print(f"无效的寻路算法: {algorithm}") + return False + + def get_navigation_mesh_stats(self): + """ + 获取导航网格统计信息 + """ + if not self.navigation_mesh: + return {'generated': False} + + return { + 'generated': True, + 'vertices': len(self.navigation_mesh['vertices']), + 'triangles': len(self.navigation_mesh['triangles']), + 'obstacles': len(self.navigation_mesh['obstacles']), + 'walkable_areas': len(self.navigation_mesh['walkable_areas']) } \ No newline at end of file diff --git a/plugins/user/terrain_editor/physics/__init__.py b/plugins/user/terrain_editor/physics/__init__.py index 4e9b7f97..3088895e 100644 --- a/plugins/user/terrain_editor/physics/__init__.py +++ b/plugins/user/terrain_editor/physics/__init__.py @@ -1,3 +1,3 @@ -""" -地形物理系统初始化文件 +""" +地形物理系统初始化文件 """ \ No newline at end of file diff --git a/plugins/user/terrain_editor/physics/physics_system.py b/plugins/user/terrain_editor/physics/physics_system.py index ffe4ddb1..d575f0d1 100644 --- a/plugins/user/terrain_editor/physics/physics_system.py +++ b/plugins/user/terrain_editor/physics/physics_system.py @@ -1,1401 +1,1401 @@ -""" -地形物理属性系统 -提供完整的地形物理属性定义、刚体动力学、碰撞检测、约束系统等功能 -""" - -from panda3d.core import NodePath, Vec3, Point3, BitMask32, TransformState -from panda3d.core import Material, MaterialAttrib, ClockObject -from panda3d.bullet import BulletWorld, BulletRigidBodyNode, BulletPlaneShape -from panda3d.bullet import BulletTriangleMesh, BulletTriangleMeshShape, BulletHeightfieldShape -from panda3d.bullet import BulletConstraint, BulletPoint2PointConstraint, BulletHingeConstraint -from panda3d.bullet import BulletSliderConstraint, BulletConeTwistConstraint, BulletGeneric6DofConstraint -from panda3d.bullet import BulletVehicle, BulletWheel, BulletBoxShape, BulletSphereShape -from panda3d.bullet import BulletSoftBodyNode, BulletSoftBodyConfig -import math -import json -import os -import random -from collections import deque - -class TerrainPhysicsSystem: - """ - 地形物理属性系统类 - 提供完整的刚体动力学、碰撞检测、约束系统、车辆模拟等功能 - """ - - def __init__(self, world): - self.world = world - self.physics_world = None - self.terrain_physics = {} # 存储地形物理属性 - self.materials = {} # 存储物理材质 - self.rigid_bodies = [] # 存储刚体 - self.constraints = [] # 存储约束 - self.vehicles = [] # 存储车辆 - self.particles = [] # 存储粒子系统 - self.soft_bodies = [] # 存储软体 - self.physics_enabled = True - self.substeps = 10 # 物理子步数 - self.internal_time_step = 1.0/60.0 # 内部时间步长 - self.time_accumulator = 0.0 # 时间累积器 - self.max_substeps = 10 # 最大子步数 - - # 物理参数 - self.physics_params = { - 'gravity': Vec3(0, 0, -9.81), - 'air_density': 1.225, # 空气密度 - 'default_friction': 0.5, - 'default_restitution': 0.1, - 'default_rolling_friction': 0.1, - 'default_spinning_friction': 0.1 - } - - # 性能监控 - self.performance_stats = { - 'last_update_time': 0.0, - 'update_count': 0, - 'avg_update_time': 0.0, - 'max_update_time': 0.0 - } - - # 初始化物理世界 - self._init_physics_world() - - def _init_physics_world(self): - """ - 初始化物理世界 - """ - try: - self.physics_world = BulletWorld() - self.physics_world.setGravity(self.physics_params['gravity']) - - # 设置物理世界参数 - self.physics_world.setConstraintSolver(self.physics_world.getConstraintSolver()) - self.physics_world.getDispatchInfo().set_merpFactor(0.1) - self.physics_world.getDispatchInfo().set_misERP(0.1) - - print("物理世界初始化完成") - - except Exception as e: - print(f"初始化物理世界时出错: {e}") - - def define_material(self, name, properties): - """ - 定义物理材质 - properties: 材质属性字典 - """ - try: - material = { - 'name': name, - 'friction': properties.get('friction', self.physics_params['default_friction']), - 'restitution': properties.get('restitution', self.physics_params['default_restitution']), - 'rolling_friction': properties.get('rolling_friction', self.physics_params['default_rolling_friction']), - 'spinning_friction': properties.get('spinning_friction', self.physics_params['default_spinning_friction']), - 'density': properties.get('density', 1000.0), - 'color': properties.get('color', (0.5, 0.5, 0.5, 1.0)), - 'static_friction': properties.get('static_friction', 0.6), - 'dynamic_friction': properties.get('dynamic_friction', 0.4), - 'surface_velocity': properties.get('surface_velocity', Vec3(0, 0, 0)), - 'anisotropic_friction': properties.get('anisotropic_friction', Vec3(1, 1, 1)), - 'contact_cfm': properties.get('contact_cfm', 0.0), - 'contact_erp': properties.get('contact_erp', 0.2) - } - - self.materials[name] = material - print(f"定义物理材质: {name}") - return material - - except Exception as e: - print(f"定义物理材质时出错: {e}") - return None - - def assign_material_to_terrain(self, terrain_info, material_name): - """ - 为地形分配物理材质 - """ - try: - if material_name not in self.materials: - print(f"物理材质 {material_name} 未定义") - return False - - # 保存材质信息 - self.terrain_physics[terrain_info['node']] = { - 'material': material_name, - 'material_properties': self.materials[material_name] - } - - # 创建地形物理碰撞体 - self._create_terrain_collision(terrain_info, material_name) - - print(f"为地形分配物理材质: {material_name}") - return True - - except Exception as e: - print(f"为地形分配物理材质时出错: {e}") - return False - - def _create_terrain_collision(self, terrain_info, material_name): - """ - 创建地形物理碰撞体(增强版) - """ - try: - terrain_node = terrain_info['node'] - heightfield = terrain_info['heightfield'] - - if not heightfield: - print("无法获取地形高度图数据") - return False - - # 移除现有的物理碰撞体 - self._remove_terrain_physics(terrain_node) - - # 创建Bullet高度场形状 - width = heightfield.getXSize() - height = heightfield.getYSize() - - # 提取高度数据 - height_data = [] - for y in range(height): - for x in range(width): - height_value = heightfield.getRed(x, y) - # 转换高度值到物理坐标系 - physical_height = height_value * terrain_node.getScale().getZ() - height_data.append(physical_height) - - # 创建高度场形状 - hfield_shape = BulletHeightfieldShape( - height_data, - width, - height, - terrain_node.getScale().getX(), # X间距 - terrain_node.getScale().getY(), # Y间距 - 1 # up axis (Z轴) - ) - - # 创建刚体节点 - body_name = f'terrain_physics_{len(self.rigid_bodies)}' - body_node = BulletRigidBodyNode(body_name) - body_node.addShape(hfield_shape) - - # 设置材质属性 - material = self.materials[material_name] - body_node.setFriction(material['friction']) - body_node.setRestitution(material['restitution']) - body_node.setRollingFriction(material['rolling_friction']) - body_node.setSpinningFriction(material['spinning_friction']) - body_node.setAnisotropicFriction(material['anisotropic_friction']) - body_node.setContactProcessingThreshold(1e30) - - # 设置为静态刚体(地形通常不移动) - body_node.setMass(0) - - # 创建物理节点 - physics_node = terrain_node.attachNewNode(body_node) - physics_node.setPos(terrain_node.getPos()) - - # 添加到物理世界 - self.physics_world.attachRigidBody(body_node) - - # 保存刚体信息 - body_info = { - 'node': physics_node, - 'body': body_node, - 'shape': hfield_shape, - 'terrain_info': terrain_info, - 'type': 'terrain', - 'material': material_name, - 'creation_time': self.world.globalClock.getFrameTime() - } - - self.rigid_bodies.append(body_info) - - print(f"创建地形物理碰撞体: {material_name} (尺寸: {width}x{height})") - return True - - except Exception as e: - print(f"创建地形物理碰撞体时出错: {e}") - return False - - def _remove_terrain_physics(self, terrain_node): - """ - 移除地形物理碰撞体 - """ - try: - for body_info in self.rigid_bodies[:]: # 使用切片复制列表 - if (body_info.get('type') == 'terrain' and - body_info['terrain_info']['node'] == terrain_node): - # 从物理世界移除 - self.physics_world.removeRigidBody(body_info['body']) - - # 移除节点 - if body_info['node'] and not body_info['node'].isEmpty(): - body_info['node'].removeNode() - - # 从列表移除 - self.rigid_bodies.remove(body_info) - - return True - - except Exception as e: - print(f"移除地形物理碰撞体时出错: {e}") - return False - - def create_rigid_body(self, node, mass=1.0, shape_type='box', material_name=None, - kinematic=False, activation_state='active', collision_group=1, - collision_mask=0xFFFFFFFF): - """ - 创建刚体(增强版) - """ - try: - if not self.physics_enabled: - print("物理系统已禁用") - return None - - # 创建碰撞形状 - shape = self._create_collision_shape(node, shape_type) - if not shape: - return None - - # 创建刚体节点 - body_name = f'rigid_body_{len(self.rigid_bodies)}' - body_node = BulletRigidBodyNode(body_name) - body_node.addShape(shape) - body_node.setMass(mass) - - # 设置碰撞组和掩码 - body_node.setCollideFilterGroup(collision_group) - body_node.setCollideFilterMask(collision_mask) - - # 设置是否为运动学刚体 - if kinematic: - body_node.setKinematic(True) - - # 设置激活状态 - if activation_state == 'inactive': - body_node.setActive(False) - elif activation_state == 'always_active': - body_node.setDeactivationEnabled(False) - - # 设置材质属性 - if material_name and material_name in self.materials: - material = self.materials[material_name] - body_node.setFriction(material['friction']) - body_node.setRestitution(material['restitution']) - body_node.setRollingFriction(material['rolling_friction']) - body_node.setSpinningFriction(material['spinning_friction']) - body_node.setAnisotropicFriction(material.get('anisotropic_friction', Vec3(1, 1, 1))) - body_node.setContactProcessingThreshold(material.get('contact_threshold', 1e30)) - else: - # 使用默认材质属性 - body_node.setFriction(self.physics_params['default_friction']) - body_node.setRestitution(self.physics_params['default_restitution']) - body_node.setRollingFriction(self.physics_params['default_rolling_friction']) - body_node.setSpinningFriction(self.physics_params['default_spinning_friction']) - - # 创建物理节点 - physics_node = node.attachNewNode(body_node) - physics_node.setPos(node.getPos()) - physics_node.setHpr(node.getHpr()) - - # 添加到物理世界 - self.physics_world.attachRigidBody(body_node) - - # 保存刚体信息 - body_info = { - 'node': physics_node, - 'body': body_node, - 'shape': shape, - 'original_node': node, - 'mass': mass, - 'material': material_name, - 'type': 'rigid_body', - 'kinematic': kinematic, - 'activation_state': activation_state, - 'collision_group': collision_group, - 'collision_mask': collision_mask, - 'velocity': Vec3(0, 0, 0), - 'angular_velocity': Vec3(0, 0, 0), - 'forces': [], # 应用力列表 - 'torques': [], # 应用扭矩列表 - 'linear_damping': 0.0, - 'angular_damping': 0.0, - 'linear_factor': Vec3(1, 1, 1), - 'angular_factor': Vec3(1, 1, 1), - 'sleeping_threshold': 0.8, - 'deactivation_time': 0.0, - 'creation_time': self.world.globalClock.getFrameTime(), - 'last_update': self.world.globalClock.getFrameTime(), - 'collision_history': deque(maxlen=10) # 碰撞历史 - } - - # 设置阻尼 - body_node.setLinearDamping(body_info['linear_damping']) - body_node.setAngularDamping(body_info['angular_damping']) - - # 设置因子 - body_node.setLinearFactor(body_info['linear_factor']) - body_node.setAngularFactor(body_info['angular_factor']) - - # 设置睡眠阈值 - body_node.setSleepingThresholds( - body_info['sleeping_threshold'], - body_info['sleeping_threshold'] * 0.1 - ) - - self.rigid_bodies.append(body_info) - - print(f"创建刚体: {node.getName()} (质量: {mass}, 类型: {shape_type})") - return body_info - - except Exception as e: - print(f"创建刚体时出错: {e}") - return None - - def _create_collision_shape(self, node, shape_type): - """ - 创建碰撞形状(增强版) - """ - try: - from panda3d.bullet import BulletBoxShape, BulletSphereShape, BulletCapsuleShape - from panda3d.bullet import BulletConeShape, BulletCylinderShape, BulletConvexHullShape - from panda3d.bullet import BulletTriangleMesh, BulletTriangleMeshShape - - if shape_type == 'box': - # 获取节点包围盒 - bounds = node.getBounds() - min_point = bounds.getMin() - max_point = bounds.getMax() - extents = (max_point - min_point) * 0.5 - shape = BulletBoxShape(extents) - - elif shape_type == 'sphere': - bounds = node.getBounds() - radius = bounds.getRadius() - shape = BulletSphereShape(radius) - - elif shape_type == 'capsule': - bounds = node.getBounds() - radius = bounds.getRadius() - height = bounds.getMax().getZ() - bounds.getMin().getZ() - shape = BulletCapsuleShape(radius, height, 2) # Z-axis - - elif shape_type == 'cone': - bounds = node.getBounds() - radius = bounds.getRadius() - height = bounds.getMax().getZ() - bounds.getMin().getZ() - shape = BulletConeShape(radius, height, 2) # Z-axis - - elif shape_type == 'cylinder': - bounds = node.getBounds() - radius = bounds.getRadius() - height = bounds.getMax().getZ() - bounds.getMin().getZ() - shape = BulletCylinderShape(radius, height, 2) # Z-axis - - elif shape_type == 'convex_hull': - # 创建凸包形状 - mesh = BulletTriangleMesh() - # 这里需要从节点提取几何数据 - # 简化实现,使用包围盒 - bounds = node.getBounds() - min_point = bounds.getMin() - max_point = bounds.getMax() - shape = BulletBoxShape((max_point - min_point) * 0.5) - - elif shape_type == 'mesh': - # 创建三角网格形状(用于静态物体) - mesh = BulletTriangleMesh() - # 这里需要从节点提取几何数据 - # 简化实现,使用包围盒 - bounds = node.getBounds() - min_point = bounds.getMin() - max_point = bounds.getMax() - shape = BulletBoxShape((max_point - min_point) * 0.5) - - else: - # 默认使用包围盒 - bounds = node.getBounds() - min_point = bounds.getMin() - max_point = bounds.getMax() - extents = (max_point - min_point) * 0.5 - shape = BulletBoxShape(extents) - - return shape - - except Exception as e: - print(f"创建碰撞形状时出错: {e}") - return None - - def remove_rigid_body(self, body_info): - """ - 移除刚体 - """ - try: - if body_info in self.rigid_bodies: - # 从物理世界移除 - self.physics_world.removeRigidBody(body_info['body']) - - # 移除节点 - if body_info['node'] and not body_info['node'].isEmpty(): - body_info['node'].removeNode() - - # 如果有原始节点,恢复其位置 - if body_info['original_node'] and not body_info['original_node'].isEmpty(): - body_info['original_node'].show() - - # 从列表移除 - self.rigid_bodies.remove(body_info) - - print("移除刚体") - return True - - except Exception as e: - print(f"移除刚体时出错: {e}") - return False - - def create_constraint(self, body_a, body_b, constraint_type='point', - pivot_a=None, pivot_b=None, axis=None, - enable_collision=False, breaking_threshold=0.0): - """ - 创建约束(增强版) - """ - try: - constraint = None - - if constraint_type == 'point': - # 点对点约束 - if pivot_a is None: - pivot_a = Point3(0, 0, 0) - if pivot_b is None: - pivot_b = Point3(0, 0, 0) - constraint = BulletPoint2PointConstraint( - body_a['body'], body_b['body'], pivot_a, pivot_b) - - elif constraint_type == 'hinge': - # 铰链约束 - if pivot_a is None: - pivot_a = Point3(0, 0, 0) - if pivot_b is None: - pivot_b = Point3(0, 0, 0) - if axis is None: - axis = Vec3(0, 0, 1) - constraint = BulletHingeConstraint( - body_a['body'], body_b['body'], pivot_a, pivot_b, axis, axis) - # 设置限制 - constraint.setLimit(-math.pi/2, math.pi/2) - - elif constraint_type == 'slider': - # 滑动约束 - if pivot_a is None: - pivot_a = Point3(0, 0, 0) - if pivot_b is None: - pivot_b = Point3(0, 0, 0) - if axis is None: - axis = Vec3(0, 0, 1) - constraint = BulletSliderConstraint( - body_a['body'], body_b['body'], TransformState.makePos(pivot_a), - TransformState.makePos(pivot_b), True) - # 设置滑动限制 - constraint.setLowerLinLimit(-5.0) - constraint.setUpperLinLimit(5.0) - - elif constraint_type == 'cone_twist': - # 锥形扭转约束 - if pivot_a is None: - pivot_a = Point3(0, 0, 0) - if pivot_b is None: - pivot_b = Point3(0, 0, 0) - constraint = BulletConeTwistConstraint( - body_a['body'], body_b['body'], TransformState.makePos(pivot_a), - TransformState.makePos(pivot_b)) - # 设置锥形限制 - constraint.setLimit(math.pi/4, math.pi/4, math.pi/2) - - elif constraint_type == 'generic_6dof': - # 通用6自由度约束 - if pivot_a is None: - pivot_a = Point3(0, 0, 0) - if pivot_b is None: - pivot_b = Point3(0, 0, 0) - constraint = BulletGeneric6DofConstraint( - body_a['body'], body_b['body'], TransformState.makePos(pivot_a), - TransformState.makePos(pivot_b), True) - # 设置线性限制 - constraint.setLinearLowerLimit(Vec3(-1, -1, -1)) - constraint.setLinearUpperLimit(Vec3(1, 1, 1)) - # 设置角度限制 - constraint.setAngularLowerLimit(Vec3(-math.pi/4, -math.pi/4, -math.pi/4)) - constraint.setAngularUpperLimit(Vec3(math.pi/4, math.pi/4, math.pi/4)) - - if constraint: - # 设置是否允许碰撞 - constraint.setOverrideEnabled(not enable_collision) - - # 设置断裂阈值 - if breaking_threshold > 0: - constraint.setBreakingImpulseThreshold(breaking_threshold) - - # 添加到物理世界 - self.physics_world.attachConstraint(constraint) - - # 保存约束信息 - constraint_info = { - 'constraint': constraint, - 'body_a': body_a, - 'body_b': body_b, - 'type': constraint_type, - 'enabled': True, - 'breaking_threshold': breaking_threshold, - 'creation_time': self.world.globalClock.getFrameTime() - } - - self.constraints.append(constraint_info) - print(f"创建约束: {constraint_type}") - return constraint_info - - except Exception as e: - print(f"创建约束时出错: {e}") - return None - - def remove_constraint(self, constraint_info): - """ - 移除约束 - """ - try: - if constraint_info in self.constraints: - # 从物理世界移除 - self.physics_world.removeConstraint(constraint_info['constraint']) - - # 从列表移除 - self.constraints.remove(constraint_info) - - print("移除约束") - return True - - except Exception as e: - print(f"移除约束时出错: {e}") - return False - - def create_vehicle(self, chassis_body, wheel_config): - """ - 创建车辆(增强版) - """ - try: - # 创建车辆 - vehicle = BulletVehicle(self.physics_world.getBroadphase(), chassis_body['body']) - vehicle.setCoordinateSystem(0, 1, 2) # Right, Up, Forward - - # 添加车轮 - wheels = [] - for i, wheel_cfg in enumerate(wheel_config): - wheel = vehicle.createWheel() - wheel.setNode(wheel_cfg['node']) - wheel.setChassisConnectionPointCs(wheel_cfg['connection_point']) - wheel.setWheelDirectionCs(wheel_cfg['direction']) - wheel.setWheelAxleCs(wheel_cfg['axle']) - wheel.setWheelRadius(wheel_cfg['radius']) - wheel.setMaxSuspensionTravelCm(wheel_cfg.get('suspension_travel', 500.0)) - wheel.setSuspensionStiffness(wheel_cfg.get('suspension_stiffness', 50.0)) - wheel.setWheelsDampingRelaxation(wheel_cfg.get('damping_relaxation', 2.3)) - wheel.setWheelsDampingCompression(wheel_cfg.get('damping_compression', 4.4)) - wheel.setFrictionSlip(wheel_cfg.get('friction_slip', 100.0)) - wheel.setRollInfluence(wheel_cfg.get('roll_influence', 0.1)) - wheel.setSteering(0.0) - wheel.setEngineForce(0.0) - wheel.setBrake(0.0) - - wheels.append(wheel) - - # 添加到物理世界 - self.physics_world.attachVehicle(vehicle) - - # 保存车辆信息 - vehicle_info = { - 'vehicle': vehicle, - 'chassis': chassis_body, - 'wheels': wheels, - 'wheel_config': wheel_config, - 'speed': 0.0, - 'steering': 0.0, - 'engine_force': 0.0, - 'brake_force': 0.0, - 'max_engine_force': 3000.0, - 'max_brake_force': 100.0, - 'max_steering': math.pi/4, - 'creation_time': self.world.globalClock.getFrameTime() - } - - self.vehicles.append(vehicle_info) - print("创建车辆") - return vehicle_info - - except Exception as e: - print(f"创建车辆时出错: {e}") - return None - - def update_physics(self, time_delta): - """ - 更新物理模拟(增强版) - """ - try: - if not self.physics_enabled or not self.physics_world: - return - - # 性能监控开始 - start_time = self.world.globalClock.getRealTime() - - # 累积时间 - self.time_accumulator += time_delta - - # 限制最大子步数以防止螺旋式死亡 - num_substeps = min( - int(self.time_accumulator / self.internal_time_step), - self.max_substeps - ) - - # 确保至少执行一次更新 - if num_substeps == 0 and self.time_accumulator > 0: - num_substeps = 1 - - # 执行子步长模拟 - for i in range(num_substeps): - # 更新物理世界 - self.physics_world.doPhysics(self.internal_time_step) - - # 更新力和扭矩 - self._update_forces_and_torques(self.internal_time_step) - - # 更新粒子系统 - self._update_particles(self.internal_time_step) - - # 更新软体 - self._update_soft_bodies(self.internal_time_step) - - # 减少累积时间 - if num_substeps > 0: - self.time_accumulator -= num_substeps * self.internal_time_step - - # 更新可视化节点位置 - self._update_visualization() - - # 更新车辆 - self._update_vehicles(time_delta) - - # 更新性能统计 - end_time = self.world.globalClock.getRealTime() - update_time = end_time - start_time - self.performance_stats['last_update_time'] = update_time - self.performance_stats['update_count'] += 1 - - # 计算平均更新时间 - if self.performance_stats['update_count'] > 1: - alpha = 0.1 - self.performance_stats['avg_update_time'] = ( - alpha * update_time + - (1 - alpha) * self.performance_stats['avg_update_time'] - ) - else: - self.performance_stats['avg_update_time'] = update_time - - # 更新最大更新时间 - if update_time > self.performance_stats['max_update_time']: - self.performance_stats['max_update_time'] = update_time - - except Exception as e: - print(f"更新物理模拟时出错: {e}") - - def _update_forces_and_torques(self, time_delta): - """ - 更新力和扭矩 - """ - try: - current_time = self.world.globalClock.getFrameTime() - - for body_info in self.rigid_bodies: - body_node = body_info['body'] - - # 应用重力 - if body_info['mass'] > 0 and not body_node.isKinematic(): - gravity_force = self.physics_params['gravity'] * body_info['mass'] - body_node.applyCentralForce(gravity_force) - - # 应用空气阻力 - velocity = body_node.getLinearVelocity() - air_resistance = -velocity * self.physics_params['air_density'] * 0.1 - body_node.applyCentralForce(air_resistance) - - # 应用自定义力 - for force_info in body_info.get('forces', []): - if force_info['duration'] > 0: - body_node.applyCentralForce(force_info['force']) - force_info['duration'] -= time_delta - else: - body_info['forces'].remove(force_info) - - # 应用自定义扭矩 - for torque_info in body_info.get('torques', []): - if torque_info['duration'] > 0: - body_node.applyTorque(torque_info['torque']) - torque_info['duration'] -= time_delta - else: - body_info['torques'].remove(torque_info) - - # 更新状态 - body_info['velocity'] = body_node.getLinearVelocity() - body_info['angular_velocity'] = body_node.getAngularVelocity() - body_info['last_update'] = current_time - - except Exception as e: - print(f"更新力和扭矩时出错: {e}") - - def _update_visualization(self): - """ - 更新可视化节点 - """ - try: - for body_info in self.rigid_bodies: - if body_info['original_node'] and not body_info['original_node'].isEmpty(): - # 同步物理节点和可视化节点 - body_node = body_info['body'] - original_node = body_info['original_node'] - - # 更新位置和旋转 - original_node.setPos(body_node.getTransform().getPos()) - original_node.setHpr(body_node.getTransform().getHpr()) - - except Exception as e: - print(f"更新可视化时出错: {e}") - - def _update_particles(self, time_delta): - """ - 更新粒子系统 - """ - try: - for particle_info in self.particles[:]: # 使用切片复制 - particle_body = particle_info['body'] - - # 更新粒子生命周期 - particle_info['age'] += time_delta - if particle_info['age'] > particle_info['lifetime']: - self.remove_rigid_body(particle_info) - if particle_info in self.particles: - self.particles.remove(particle_info) - continue - - # 应用粒子力(如重力、风力等) - if particle_info.get('affected_by_gravity', True): - gravity_force = self.physics_params['gravity'] * particle_info['mass'] - particle_body.applyCentralForce(gravity_force) - - # 应用阻尼 - velocity = particle_body.getLinearVelocity() - damping_force = -velocity * particle_info.get('damping', 0.1) - particle_body.applyCentralForce(damping_force) - - except Exception as e: - print(f"更新粒子时出错: {e}") - - def _update_soft_bodies(self, time_delta): - """ - 更新软体 - """ - try: - for soft_body_info in self.soft_bodies: - # 软体由Bullet物理引擎自动更新 - pass - - except Exception as e: - print(f"更新软体时出错: {e}") - - def _update_vehicles(self, time_delta): - """ - 更新车辆 - """ - try: - for vehicle_info in self.vehicles: - vehicle = vehicle_info['vehicle'] - # 车辆更新由BulletVehicle自动处理 - - # 更新车辆状态 - vehicle_info['speed'] = vehicle.getCurrentSpeedKmHour() - - except Exception as e: - print(f"更新车辆时出错: {e}") - - def apply_force(self, body_info, force, duration=0.0, position=None): - """ - 对刚体施加力(增强版) - """ - try: - if duration <= 0: - # 瞬时力 - if position is not None: - body_info['body'].applyForce(force, position) - else: - body_info['body'].applyCentralForce(force) - else: - # 持续力 - force_info = { - 'force': force, - 'duration': duration, - 'position': position - } - body_info.setdefault('forces', []).append(force_info) - - print(f"对刚体施加力: {force}") - return True - - except Exception as e: - print(f"施加力时出错: {e}") - return False - - def apply_torque(self, body_info, torque, duration=0.0): - """ - 对刚体施加扭矩(增强版) - """ - try: - if duration <= 0: - # 瞬时扭矩 - body_info['body'].applyTorque(torque) - else: - # 持续扭矩 - torque_info = { - 'torque': torque, - 'duration': duration - } - body_info.setdefault('torques', []).append(torque_info) - - print(f"对刚体施加扭矩: {torque}") - return True - - except Exception as e: - print(f"施加扭矩时出错: {e}") - return False - - def ray_test(self, from_pos, to_pos): - """ - 射线检测(增强版) - """ - try: - if not self.physics_enabled or not self.physics_world: - return None - - # 执行射线检测 - result = self.physics_world.rayTestClosest(from_pos, to_pos) - - if result.hasHit(): - hit_info = { - 'hit_point': result.getHitPos(), - 'hit_normal': result.getHitNormal(), - 'hit_fraction': result.getHitFraction(), - 'node': result.getNode(), - 'body': result.getRigidBody(), - 'distance': (to_pos - from_pos).length() * result.getHitFraction() - } - return hit_info - - except Exception as e: - print(f"射线检测时出错: {e}") - return None - - def sphere_test(self, position, radius): - """ - 球体检测(增强版) - """ - try: - if not self.physics_enabled or not self.physics_world: - return [] - - # 执行球体检测 - results = self.physics_world.sphereTest(position, radius) - - hit_list = [] - for result in results: - hit_info = { - 'hit_point': result.getHitPos(), - 'hit_normal': result.getHitNormal(), - 'node': result.getNode(), - 'body': result.getRigidBody(), - 'distance': (result.getHitPos() - position).length() - } - hit_list.append(hit_info) - - return hit_list - - except Exception as e: - print(f"球体检测时出错: {e}") - return [] - - def set_gravity(self, gravity_vector): - """ - 设置重力 - """ - try: - if self.physics_world: - self.physics_world.setGravity(gravity_vector) - self.physics_params['gravity'] = gravity_vector - print(f"设置重力: {gravity_vector}") - return True - - except Exception as e: - print(f"设置重力时出错: {e}") - return False - - def set_physics_enabled(self, enabled): - """ - 启用或禁用物理系统 - """ - self.physics_enabled = enabled - print(f"物理系统已{'启用' if enabled else '禁用'}") - - def get_physics_stats(self): - """ - 获取物理统计信息(增强版) - """ - stats = { - 'enabled': self.physics_enabled, - 'rigid_bodies': len(self.rigid_bodies), - 'constraints': len(self.constraints), - 'vehicles': len(self.vehicles), - 'particles': len(self.particles), - 'soft_bodies': len(self.soft_bodies), - 'materials': len(self.materials), - 'gravity': self.physics_params['gravity'], - 'substeps': self.substeps, - 'internal_time_step': self.internal_time_step, - 'time_accumulator': self.time_accumulator, - 'performance': { - 'last_update_time': self.performance_stats['last_update_time'], - 'avg_update_time': self.performance_stats['avg_update_time'], - 'max_update_time': self.performance_stats['max_update_time'], - 'update_count': self.performance_stats['update_count'] - } - } - - # 统计材质分布 - material_stats = {} - for body_info in self.rigid_bodies: - material = body_info.get('material', 'default') - if material in material_stats: - material_stats[material] += 1 - else: - material_stats[material] = 1 - - stats['material_distribution'] = material_stats - - # 统计刚体类型分布 - body_type_stats = {} - for body_info in self.rigid_bodies: - body_type = body_info.get('type', 'unknown') - if body_type in body_type_stats: - body_type_stats[body_type] += 1 - else: - body_type_stats[body_type] = 1 - - stats['body_type_distribution'] = body_type_stats - - # 统计约束类型分布 - constraint_stats = {} - for constraint_info in self.constraints: - constraint_type = constraint_info.get('type', 'unknown') - if constraint_type in constraint_stats: - constraint_stats[constraint_type] += 1 - else: - constraint_stats[constraint_type] = 1 - - stats['constraint_distribution'] = constraint_stats - - return stats - - def save_physics_data(self, output_path): - """ - 保存物理数据(增强版) - """ - try: - # 收集物理数据 - physics_data = { - 'materials': {}, - 'terrain_properties': {}, - 'settings': { - 'gravity': [self.physics_params['gravity'].x, - self.physics_params['gravity'].y, - self.physics_params['gravity'].z], - 'substeps': self.substeps, - 'internal_time_step': self.internal_time_step, - 'air_density': self.physics_params['air_density'] - } - } - - # 保存材质 - for name, material in self.materials.items(): - physics_data['materials'][name] = { - 'friction': material['friction'], - 'restitution': material['restitution'], - 'rolling_friction': material['rolling_friction'], - 'spinning_friction': material['spinning_friction'], - 'density': material['density'], - 'static_friction': material.get('static_friction', 0.6), - 'dynamic_friction': material.get('dynamic_friction', 0.4), - 'contact_cfm': material.get('contact_cfm', 0.0), - 'contact_erp': material.get('contact_erp', 0.2) - } - - # 保存地形物理属性 - for terrain_node, props in self.terrain_physics.items(): - # 这里需要序列化节点引用 - node_name = terrain_node.getName() if not terrain_node.isEmpty() else "unknown" - physics_data['terrain_properties'][node_name] = { - 'material': props['material'] - } - - # 确保输出目录存在 - output_dir = os.path.dirname(output_path) - if not os.path.exists(output_dir): - os.makedirs(output_dir) - - # 写入JSON文件 - with open(output_path, 'w', encoding='utf-8') as f: - json.dump(physics_data, f, indent=2, ensure_ascii=False) - - print(f"物理数据保存成功: {output_path}") - return True - - except Exception as e: - print(f"保存物理数据时出错: {e}") - return False - - def load_physics_data(self, input_path): - """ - 加载物理数据(增强版) - """ - try: - if not os.path.exists(input_path): - print(f"物理数据文件不存在: {input_path}") - return False - - # 读取JSON文件 - with open(input_path, 'r', encoding='utf-8') as f: - physics_data = json.load(f) - - # 恢复设置 - if 'settings' in physics_data: - settings = physics_data['settings'] - if 'gravity' in settings: - gravity = settings['gravity'] - self.physics_params['gravity'] = Vec3(gravity[0], gravity[1], gravity[2]) - self.set_gravity(self.physics_params['gravity']) - if 'substeps' in settings: - self.substeps = settings['substeps'] - if 'internal_time_step' in settings: - self.internal_time_step = settings['internal_time_step'] - if 'air_density' in settings: - self.physics_params['air_density'] = settings['air_density'] - - # 加载材质 - for name, properties in physics_data.get('materials', {}).items(): - self.define_material(name, properties) - - # 应用地形物理属性(需要在地形加载后执行) - self._pending_terrain_properties = physics_data.get('terrain_properties', {}) - - print(f"物理数据加载成功: {input_path}") - return True - - except Exception as e: - print(f"加载物理数据时出错: {e}") - return False - - def apply_pending_terrain_properties(self, terrain_manager): - """ - 应用待处理的地形物理属性 - """ - try: - if not hasattr(self, '_pending_terrain_properties'): - return - - for terrain_info in terrain_manager.terrains: - terrain_node = terrain_info['node'] - node_name = terrain_node.getName() - - if node_name in self._pending_terrain_properties: - props = self._pending_terrain_properties[node_name] - material_name = props['material'] - self.assign_material_to_terrain(terrain_info, material_name) - - # 清除待处理属性 - delattr(self, '_pending_terrain_properties') - - except Exception as e: - print(f"应用待处理地形物理属性时出错: {e}") - - def clear_all_physics(self): - """ - 清除所有物理对象 - """ - try: - # 移除所有约束 - for constraint_info in self.constraints[:]: - self.remove_constraint(constraint_info) - - # 移除所有刚体 - for body_info in self.rigid_bodies[:]: # 使用切片复制列表 - self.remove_rigid_body(body_info) - - # 移除所有车辆 - self.vehicles = [] - - # 移除所有粒子 - self.particles = [] - - # 移除所有软体 - self.soft_bodies = [] - - # 清空数据结构 - self.rigid_bodies = [] - self.constraints = [] - self.terrain_physics = {} - - print("清除所有物理对象") - return True - - except Exception as e: - print(f"清除所有物理对象时出错: {e}") - return False - - def create_debris_effect(self, position, count=10, material_name=None, - velocity_range=(5, 15), size_range=(0.05, 0.2)): - """ - 创建碎片效果(增强版) - """ - try: - debris_bodies = [] - - for i in range(count): - # 创建碎片节点 - from panda3d.core import CardMaker - size = random.uniform(size_range[0], size_range[1]) - cm = CardMaker(f'debris_{i}') - cm.setFrame(-size, size, -size, size) - debris_node = self.world.render.attachNewNode(cm.generate()) - debris_node.setPos(position) - - # 添加随机偏移 - offset = Vec3( - random.uniform(-1, 1), - random.uniform(-1, 1), - random.uniform(0, 2) - ) - debris_node.setPos(position + offset) - - # 创建碎片刚体 - body_info = self.create_rigid_body( - debris_node, - mass=random.uniform(0.1, 1.0), - shape_type='box', - material_name=material_name - ) - - if body_info: - # 应加初始力 - initial_force = Vec3( - random.uniform(-1, 1) * random.uniform(velocity_range[0], velocity_range[1]) * 100, - random.uniform(-1, 1) * random.uniform(velocity_range[0], velocity_range[1]) * 100, - random.uniform(0, 1) * random.uniform(velocity_range[0], velocity_range[1]) * 100 - ) - self.apply_force(body_info, initial_force, duration=0.0) - - # 设置角速度 - body_info['body'].setAngularVelocity(Vec3( - random.uniform(-10, 10), - random.uniform(-10, 10), - random.uniform(-10, 10) - )) - - # 设置生命周期 - body_info['lifetime'] = random.uniform(2.0, 5.0) - body_info['age'] = 0.0 - body_info['affected_by_gravity'] = True - body_info['damping'] = random.uniform(0.01, 0.1) - - debris_bodies.append(body_info) - self.particles.append(body_info) # 添加到粒子系统 - - print(f"创建碎片效果: {count} 个碎片") - return debris_bodies - - except Exception as e: - print(f"创建碎片效果时出错: {e}") - return [] - - def simulate_earthquake(self, intensity=1.0, duration=5.0, epicenter=None): - """ - 模拟地震效果(增强版) - """ - try: - if epicenter is None: - epicenter = Point3(0, 0, 0) - - # 创建地震力 - earthquake_force = Vec3( - random.uniform(-1, 1) * intensity * 1000, - random.uniform(-1, 1) * intensity * 1000, - random.uniform(0, 1) * intensity * 500 - ) - - # 对所有地形相关的刚体施加力 - for body_info in self.rigid_bodies: - if body_info.get('type') == 'terrain': - # 对地形施加震动 - self.apply_force(body_info, earthquake_force, duration=0.1) - - # 对附近的物体施加力 - for body_info in self.rigid_bodies: - if body_info.get('type') == 'rigid_body' and body_info['mass'] > 0: - # 计算距离 - body_pos = body_info['node'].getPos() - distance = (body_pos - epicenter).length() - - # 距离衰减 - if distance < 50.0: # 50米范围内受影响 - force_multiplier = max(0.1, 1.0 - distance / 50.0) - local_force = earthquake_force * force_multiplier - self.apply_force(body_info, local_force, duration=0.1) - - print(f"模拟地震效果: 强度 {intensity}, 持续时间 {duration}秒") - return True - - except Exception as e: - print(f"模拟地震效果时出错: {e}") - return False - - def set_physics_substeps(self, substeps, internal_time_step=None): - """ - 设置物理子步长 - """ - self.substeps = max(1, substeps) - if internal_time_step: - self.internal_time_step = internal_time_step - print(f"物理子步长设置: {self.substeps} 步, 时间步长: {self.internal_time_step}") - - def create_soft_body(self, node, mass=1.0, material_name=None): - """ - 创建软体(实验性) - """ - try: - if not self.physics_enabled: - print("物理系统已禁用") - return None - - # 创建软体节点 - soft_body_name = f'soft_body_{len(self.soft_bodies)}' - soft_body_node = BulletSoftBodyNode(soft_body_name) - - # 设置软体属性 - cfg = soft_body_node.get_cfg() - cfg.set_kVC(0.5) # 体积保持系数 - cfg.set_kDF(0.5) # 动态摩擦系数 - cfg.set_kDP(0.01) # 压力系数 - cfg.set_kCHR(1.0) # 完全保持形状 - cfg.set_kKHR(0.1) # 运动学保持形状 - cfg.set_kSHR(1.0) # 静态保持形状 - cfg.set_kAHR(0.7) # 锚点保持形状 - - # 设置材质属性 - if material_name and material_name in self.materials: - material = self.materials[material_name] - soft_body_node.setFriction(material['friction']) - soft_body_node.setRestitution(material['restitution']) - - # 创建软体节点 - soft_body_np = node.attachNewNode(soft_body_node) - soft_body_np.setPos(node.getPos()) - soft_body_np.setHpr(node.getHpr()) - - # 添加到物理世界 - self.physics_world.attachSoftBody(soft_body_node) - - # 保存软体信息 - soft_body_info = { - 'node': soft_body_np, - 'body': soft_body_node, - 'original_node': node, - 'mass': mass, - 'material': material_name, - 'type': 'soft_body', - 'creation_time': self.world.globalClock.getFrameTime() - } - - self.soft_bodies.append(soft_body_info) - - print(f"创建软体: {node.getName()} (质量: {mass})") - return soft_body_info - - except Exception as e: - print(f"创建软体时出错: {e}") - return None - - def remove_soft_body(self, soft_body_info): - """ - 移除软体 - """ - try: - if soft_body_info in self.soft_bodies: - # 从物理世界移除 - self.physics_world.removeSoftBody(soft_body_info['body']) - - # 移除节点 - if soft_body_info['node'] and not soft_body_info['node'].isEmpty(): - soft_body_info['node'].removeNode() - - # 如果有原始节点,恢复其位置 - if soft_body_info['original_node'] and not soft_body_info['original_node'].isEmpty(): - soft_body_info['original_node'].show() - - # 从列表移除 - self.soft_bodies.remove(soft_body_info) - - print("移除软体") - return True - - except Exception as e: - print(f"移除软体时出错: {e}") - return False - - def set_vehicle_control(self, vehicle_info, steering=0.0, engine_force=0.0, brake_force=0.0): - """ - 设置车辆控制(增强版) - """ - try: - vehicle = vehicle_info['vehicle'] - wheels = vehicle_info['wheels'] - - # 限制控制值 - steering = max(-1.0, min(1.0, steering)) - engine_force = max(-1.0, min(1.0, engine_force)) - brake_force = max(0.0, min(1.0, brake_force)) - - # 应用转向 - max_steering = vehicle_info['max_steering'] - for i, wheel in enumerate(wheels): - # 前轮转向 - if i < 2: # 假设前两个轮子是前轮 - wheel.setSteering(steering * max_steering) - - # 应用引擎力和刹车力 - max_engine_force = vehicle_info['max_engine_force'] - max_brake_force = vehicle_info['max_brake_force'] - - for i, wheel in enumerate(wheels): - # 后轮驱动 - if i >= 2: # 假设后两个轮子是后轮 - wheel.setEngineForce(engine_force * max_engine_force) - wheel.setBrake(brake_force * max_brake_force) - - # 更新车辆信息 - vehicle_info['steering'] = steering - vehicle_info['engine_force'] = engine_force - vehicle_info['brake_force'] = brake_force - - return True - - except Exception as e: - print(f"设置车辆控制时出错: {e}") +""" +地形物理属性系统 +提供完整的地形物理属性定义、刚体动力学、碰撞检测、约束系统等功能 +""" + +from panda3d.core import NodePath, Vec3, Point3, BitMask32, TransformState +from panda3d.core import Material, MaterialAttrib, ClockObject +from panda3d.bullet import BulletWorld, BulletRigidBodyNode, BulletPlaneShape +from panda3d.bullet import BulletTriangleMesh, BulletTriangleMeshShape, BulletHeightfieldShape +from panda3d.bullet import BulletConstraint, BulletPoint2PointConstraint, BulletHingeConstraint +from panda3d.bullet import BulletSliderConstraint, BulletConeTwistConstraint, BulletGeneric6DofConstraint +from panda3d.bullet import BulletVehicle, BulletWheel, BulletBoxShape, BulletSphereShape +from panda3d.bullet import BulletSoftBodyNode, BulletSoftBodyConfig +import math +import json +import os +import random +from collections import deque + +class TerrainPhysicsSystem: + """ + 地形物理属性系统类 + 提供完整的刚体动力学、碰撞检测、约束系统、车辆模拟等功能 + """ + + def __init__(self, world): + self.world = world + self.physics_world = None + self.terrain_physics = {} # 存储地形物理属性 + self.materials = {} # 存储物理材质 + self.rigid_bodies = [] # 存储刚体 + self.constraints = [] # 存储约束 + self.vehicles = [] # 存储车辆 + self.particles = [] # 存储粒子系统 + self.soft_bodies = [] # 存储软体 + self.physics_enabled = True + self.substeps = 10 # 物理子步数 + self.internal_time_step = 1.0/60.0 # 内部时间步长 + self.time_accumulator = 0.0 # 时间累积器 + self.max_substeps = 10 # 最大子步数 + + # 物理参数 + self.physics_params = { + 'gravity': Vec3(0, 0, -9.81), + 'air_density': 1.225, # 空气密度 + 'default_friction': 0.5, + 'default_restitution': 0.1, + 'default_rolling_friction': 0.1, + 'default_spinning_friction': 0.1 + } + + # 性能监控 + self.performance_stats = { + 'last_update_time': 0.0, + 'update_count': 0, + 'avg_update_time': 0.0, + 'max_update_time': 0.0 + } + + # 初始化物理世界 + self._init_physics_world() + + def _init_physics_world(self): + """ + 初始化物理世界 + """ + try: + self.physics_world = BulletWorld() + self.physics_world.setGravity(self.physics_params['gravity']) + + # 设置物理世界参数 + self.physics_world.setConstraintSolver(self.physics_world.getConstraintSolver()) + self.physics_world.getDispatchInfo().set_merpFactor(0.1) + self.physics_world.getDispatchInfo().set_misERP(0.1) + + print("物理世界初始化完成") + + except Exception as e: + print(f"初始化物理世界时出错: {e}") + + def define_material(self, name, properties): + """ + 定义物理材质 + properties: 材质属性字典 + """ + try: + material = { + 'name': name, + 'friction': properties.get('friction', self.physics_params['default_friction']), + 'restitution': properties.get('restitution', self.physics_params['default_restitution']), + 'rolling_friction': properties.get('rolling_friction', self.physics_params['default_rolling_friction']), + 'spinning_friction': properties.get('spinning_friction', self.physics_params['default_spinning_friction']), + 'density': properties.get('density', 1000.0), + 'color': properties.get('color', (0.5, 0.5, 0.5, 1.0)), + 'static_friction': properties.get('static_friction', 0.6), + 'dynamic_friction': properties.get('dynamic_friction', 0.4), + 'surface_velocity': properties.get('surface_velocity', Vec3(0, 0, 0)), + 'anisotropic_friction': properties.get('anisotropic_friction', Vec3(1, 1, 1)), + 'contact_cfm': properties.get('contact_cfm', 0.0), + 'contact_erp': properties.get('contact_erp', 0.2) + } + + self.materials[name] = material + print(f"定义物理材质: {name}") + return material + + except Exception as e: + print(f"定义物理材质时出错: {e}") + return None + + def assign_material_to_terrain(self, terrain_info, material_name): + """ + 为地形分配物理材质 + """ + try: + if material_name not in self.materials: + print(f"物理材质 {material_name} 未定义") + return False + + # 保存材质信息 + self.terrain_physics[terrain_info['node']] = { + 'material': material_name, + 'material_properties': self.materials[material_name] + } + + # 创建地形物理碰撞体 + self._create_terrain_collision(terrain_info, material_name) + + print(f"为地形分配物理材质: {material_name}") + return True + + except Exception as e: + print(f"为地形分配物理材质时出错: {e}") + return False + + def _create_terrain_collision(self, terrain_info, material_name): + """ + 创建地形物理碰撞体(增强版) + """ + try: + terrain_node = terrain_info['node'] + heightfield = terrain_info['heightfield'] + + if not heightfield: + print("无法获取地形高度图数据") + return False + + # 移除现有的物理碰撞体 + self._remove_terrain_physics(terrain_node) + + # 创建Bullet高度场形状 + width = heightfield.getXSize() + height = heightfield.getYSize() + + # 提取高度数据 + height_data = [] + for y in range(height): + for x in range(width): + height_value = heightfield.getRed(x, y) + # 转换高度值到物理坐标系 + physical_height = height_value * terrain_node.getScale().getZ() + height_data.append(physical_height) + + # 创建高度场形状 + hfield_shape = BulletHeightfieldShape( + height_data, + width, + height, + terrain_node.getScale().getX(), # X间距 + terrain_node.getScale().getY(), # Y间距 + 1 # up axis (Z轴) + ) + + # 创建刚体节点 + body_name = f'terrain_physics_{len(self.rigid_bodies)}' + body_node = BulletRigidBodyNode(body_name) + body_node.addShape(hfield_shape) + + # 设置材质属性 + material = self.materials[material_name] + body_node.setFriction(material['friction']) + body_node.setRestitution(material['restitution']) + body_node.setRollingFriction(material['rolling_friction']) + body_node.setSpinningFriction(material['spinning_friction']) + body_node.setAnisotropicFriction(material['anisotropic_friction']) + body_node.setContactProcessingThreshold(1e30) + + # 设置为静态刚体(地形通常不移动) + body_node.setMass(0) + + # 创建物理节点 + physics_node = terrain_node.attachNewNode(body_node) + physics_node.setPos(terrain_node.getPos()) + + # 添加到物理世界 + self.physics_world.attachRigidBody(body_node) + + # 保存刚体信息 + body_info = { + 'node': physics_node, + 'body': body_node, + 'shape': hfield_shape, + 'terrain_info': terrain_info, + 'type': 'terrain', + 'material': material_name, + 'creation_time': self.world.globalClock.getFrameTime() + } + + self.rigid_bodies.append(body_info) + + print(f"创建地形物理碰撞体: {material_name} (尺寸: {width}x{height})") + return True + + except Exception as e: + print(f"创建地形物理碰撞体时出错: {e}") + return False + + def _remove_terrain_physics(self, terrain_node): + """ + 移除地形物理碰撞体 + """ + try: + for body_info in self.rigid_bodies[:]: # 使用切片复制列表 + if (body_info.get('type') == 'terrain' and + body_info['terrain_info']['node'] == terrain_node): + # 从物理世界移除 + self.physics_world.removeRigidBody(body_info['body']) + + # 移除节点 + if body_info['node'] and not body_info['node'].isEmpty(): + body_info['node'].removeNode() + + # 从列表移除 + self.rigid_bodies.remove(body_info) + + return True + + except Exception as e: + print(f"移除地形物理碰撞体时出错: {e}") + return False + + def create_rigid_body(self, node, mass=1.0, shape_type='box', material_name=None, + kinematic=False, activation_state='active', collision_group=1, + collision_mask=0xFFFFFFFF): + """ + 创建刚体(增强版) + """ + try: + if not self.physics_enabled: + print("物理系统已禁用") + return None + + # 创建碰撞形状 + shape = self._create_collision_shape(node, shape_type) + if not shape: + return None + + # 创建刚体节点 + body_name = f'rigid_body_{len(self.rigid_bodies)}' + body_node = BulletRigidBodyNode(body_name) + body_node.addShape(shape) + body_node.setMass(mass) + + # 设置碰撞组和掩码 + body_node.setCollideFilterGroup(collision_group) + body_node.setCollideFilterMask(collision_mask) + + # 设置是否为运动学刚体 + if kinematic: + body_node.setKinematic(True) + + # 设置激活状态 + if activation_state == 'inactive': + body_node.setActive(False) + elif activation_state == 'always_active': + body_node.setDeactivationEnabled(False) + + # 设置材质属性 + if material_name and material_name in self.materials: + material = self.materials[material_name] + body_node.setFriction(material['friction']) + body_node.setRestitution(material['restitution']) + body_node.setRollingFriction(material['rolling_friction']) + body_node.setSpinningFriction(material['spinning_friction']) + body_node.setAnisotropicFriction(material.get('anisotropic_friction', Vec3(1, 1, 1))) + body_node.setContactProcessingThreshold(material.get('contact_threshold', 1e30)) + else: + # 使用默认材质属性 + body_node.setFriction(self.physics_params['default_friction']) + body_node.setRestitution(self.physics_params['default_restitution']) + body_node.setRollingFriction(self.physics_params['default_rolling_friction']) + body_node.setSpinningFriction(self.physics_params['default_spinning_friction']) + + # 创建物理节点 + physics_node = node.attachNewNode(body_node) + physics_node.setPos(node.getPos()) + physics_node.setHpr(node.getHpr()) + + # 添加到物理世界 + self.physics_world.attachRigidBody(body_node) + + # 保存刚体信息 + body_info = { + 'node': physics_node, + 'body': body_node, + 'shape': shape, + 'original_node': node, + 'mass': mass, + 'material': material_name, + 'type': 'rigid_body', + 'kinematic': kinematic, + 'activation_state': activation_state, + 'collision_group': collision_group, + 'collision_mask': collision_mask, + 'velocity': Vec3(0, 0, 0), + 'angular_velocity': Vec3(0, 0, 0), + 'forces': [], # 应用力列表 + 'torques': [], # 应用扭矩列表 + 'linear_damping': 0.0, + 'angular_damping': 0.0, + 'linear_factor': Vec3(1, 1, 1), + 'angular_factor': Vec3(1, 1, 1), + 'sleeping_threshold': 0.8, + 'deactivation_time': 0.0, + 'creation_time': self.world.globalClock.getFrameTime(), + 'last_update': self.world.globalClock.getFrameTime(), + 'collision_history': deque(maxlen=10) # 碰撞历史 + } + + # 设置阻尼 + body_node.setLinearDamping(body_info['linear_damping']) + body_node.setAngularDamping(body_info['angular_damping']) + + # 设置因子 + body_node.setLinearFactor(body_info['linear_factor']) + body_node.setAngularFactor(body_info['angular_factor']) + + # 设置睡眠阈值 + body_node.setSleepingThresholds( + body_info['sleeping_threshold'], + body_info['sleeping_threshold'] * 0.1 + ) + + self.rigid_bodies.append(body_info) + + print(f"创建刚体: {node.getName()} (质量: {mass}, 类型: {shape_type})") + return body_info + + except Exception as e: + print(f"创建刚体时出错: {e}") + return None + + def _create_collision_shape(self, node, shape_type): + """ + 创建碰撞形状(增强版) + """ + try: + from panda3d.bullet import BulletBoxShape, BulletSphereShape, BulletCapsuleShape + from panda3d.bullet import BulletConeShape, BulletCylinderShape, BulletConvexHullShape + from panda3d.bullet import BulletTriangleMesh, BulletTriangleMeshShape + + if shape_type == 'box': + # 获取节点包围盒 + bounds = node.getBounds() + min_point = bounds.getMin() + max_point = bounds.getMax() + extents = (max_point - min_point) * 0.5 + shape = BulletBoxShape(extents) + + elif shape_type == 'sphere': + bounds = node.getBounds() + radius = bounds.getRadius() + shape = BulletSphereShape(radius) + + elif shape_type == 'capsule': + bounds = node.getBounds() + radius = bounds.getRadius() + height = bounds.getMax().getZ() - bounds.getMin().getZ() + shape = BulletCapsuleShape(radius, height, 2) # Z-axis + + elif shape_type == 'cone': + bounds = node.getBounds() + radius = bounds.getRadius() + height = bounds.getMax().getZ() - bounds.getMin().getZ() + shape = BulletConeShape(radius, height, 2) # Z-axis + + elif shape_type == 'cylinder': + bounds = node.getBounds() + radius = bounds.getRadius() + height = bounds.getMax().getZ() - bounds.getMin().getZ() + shape = BulletCylinderShape(radius, height, 2) # Z-axis + + elif shape_type == 'convex_hull': + # 创建凸包形状 + mesh = BulletTriangleMesh() + # 这里需要从节点提取几何数据 + # 简化实现,使用包围盒 + bounds = node.getBounds() + min_point = bounds.getMin() + max_point = bounds.getMax() + shape = BulletBoxShape((max_point - min_point) * 0.5) + + elif shape_type == 'mesh': + # 创建三角网格形状(用于静态物体) + mesh = BulletTriangleMesh() + # 这里需要从节点提取几何数据 + # 简化实现,使用包围盒 + bounds = node.getBounds() + min_point = bounds.getMin() + max_point = bounds.getMax() + shape = BulletBoxShape((max_point - min_point) * 0.5) + + else: + # 默认使用包围盒 + bounds = node.getBounds() + min_point = bounds.getMin() + max_point = bounds.getMax() + extents = (max_point - min_point) * 0.5 + shape = BulletBoxShape(extents) + + return shape + + except Exception as e: + print(f"创建碰撞形状时出错: {e}") + return None + + def remove_rigid_body(self, body_info): + """ + 移除刚体 + """ + try: + if body_info in self.rigid_bodies: + # 从物理世界移除 + self.physics_world.removeRigidBody(body_info['body']) + + # 移除节点 + if body_info['node'] and not body_info['node'].isEmpty(): + body_info['node'].removeNode() + + # 如果有原始节点,恢复其位置 + if body_info['original_node'] and not body_info['original_node'].isEmpty(): + body_info['original_node'].show() + + # 从列表移除 + self.rigid_bodies.remove(body_info) + + print("移除刚体") + return True + + except Exception as e: + print(f"移除刚体时出错: {e}") + return False + + def create_constraint(self, body_a, body_b, constraint_type='point', + pivot_a=None, pivot_b=None, axis=None, + enable_collision=False, breaking_threshold=0.0): + """ + 创建约束(增强版) + """ + try: + constraint = None + + if constraint_type == 'point': + # 点对点约束 + if pivot_a is None: + pivot_a = Point3(0, 0, 0) + if pivot_b is None: + pivot_b = Point3(0, 0, 0) + constraint = BulletPoint2PointConstraint( + body_a['body'], body_b['body'], pivot_a, pivot_b) + + elif constraint_type == 'hinge': + # 铰链约束 + if pivot_a is None: + pivot_a = Point3(0, 0, 0) + if pivot_b is None: + pivot_b = Point3(0, 0, 0) + if axis is None: + axis = Vec3(0, 0, 1) + constraint = BulletHingeConstraint( + body_a['body'], body_b['body'], pivot_a, pivot_b, axis, axis) + # 设置限制 + constraint.setLimit(-math.pi/2, math.pi/2) + + elif constraint_type == 'slider': + # 滑动约束 + if pivot_a is None: + pivot_a = Point3(0, 0, 0) + if pivot_b is None: + pivot_b = Point3(0, 0, 0) + if axis is None: + axis = Vec3(0, 0, 1) + constraint = BulletSliderConstraint( + body_a['body'], body_b['body'], TransformState.makePos(pivot_a), + TransformState.makePos(pivot_b), True) + # 设置滑动限制 + constraint.setLowerLinLimit(-5.0) + constraint.setUpperLinLimit(5.0) + + elif constraint_type == 'cone_twist': + # 锥形扭转约束 + if pivot_a is None: + pivot_a = Point3(0, 0, 0) + if pivot_b is None: + pivot_b = Point3(0, 0, 0) + constraint = BulletConeTwistConstraint( + body_a['body'], body_b['body'], TransformState.makePos(pivot_a), + TransformState.makePos(pivot_b)) + # 设置锥形限制 + constraint.setLimit(math.pi/4, math.pi/4, math.pi/2) + + elif constraint_type == 'generic_6dof': + # 通用6自由度约束 + if pivot_a is None: + pivot_a = Point3(0, 0, 0) + if pivot_b is None: + pivot_b = Point3(0, 0, 0) + constraint = BulletGeneric6DofConstraint( + body_a['body'], body_b['body'], TransformState.makePos(pivot_a), + TransformState.makePos(pivot_b), True) + # 设置线性限制 + constraint.setLinearLowerLimit(Vec3(-1, -1, -1)) + constraint.setLinearUpperLimit(Vec3(1, 1, 1)) + # 设置角度限制 + constraint.setAngularLowerLimit(Vec3(-math.pi/4, -math.pi/4, -math.pi/4)) + constraint.setAngularUpperLimit(Vec3(math.pi/4, math.pi/4, math.pi/4)) + + if constraint: + # 设置是否允许碰撞 + constraint.setOverrideEnabled(not enable_collision) + + # 设置断裂阈值 + if breaking_threshold > 0: + constraint.setBreakingImpulseThreshold(breaking_threshold) + + # 添加到物理世界 + self.physics_world.attachConstraint(constraint) + + # 保存约束信息 + constraint_info = { + 'constraint': constraint, + 'body_a': body_a, + 'body_b': body_b, + 'type': constraint_type, + 'enabled': True, + 'breaking_threshold': breaking_threshold, + 'creation_time': self.world.globalClock.getFrameTime() + } + + self.constraints.append(constraint_info) + print(f"创建约束: {constraint_type}") + return constraint_info + + except Exception as e: + print(f"创建约束时出错: {e}") + return None + + def remove_constraint(self, constraint_info): + """ + 移除约束 + """ + try: + if constraint_info in self.constraints: + # 从物理世界移除 + self.physics_world.removeConstraint(constraint_info['constraint']) + + # 从列表移除 + self.constraints.remove(constraint_info) + + print("移除约束") + return True + + except Exception as e: + print(f"移除约束时出错: {e}") + return False + + def create_vehicle(self, chassis_body, wheel_config): + """ + 创建车辆(增强版) + """ + try: + # 创建车辆 + vehicle = BulletVehicle(self.physics_world.getBroadphase(), chassis_body['body']) + vehicle.setCoordinateSystem(0, 1, 2) # Right, Up, Forward + + # 添加车轮 + wheels = [] + for i, wheel_cfg in enumerate(wheel_config): + wheel = vehicle.createWheel() + wheel.setNode(wheel_cfg['node']) + wheel.setChassisConnectionPointCs(wheel_cfg['connection_point']) + wheel.setWheelDirectionCs(wheel_cfg['direction']) + wheel.setWheelAxleCs(wheel_cfg['axle']) + wheel.setWheelRadius(wheel_cfg['radius']) + wheel.setMaxSuspensionTravelCm(wheel_cfg.get('suspension_travel', 500.0)) + wheel.setSuspensionStiffness(wheel_cfg.get('suspension_stiffness', 50.0)) + wheel.setWheelsDampingRelaxation(wheel_cfg.get('damping_relaxation', 2.3)) + wheel.setWheelsDampingCompression(wheel_cfg.get('damping_compression', 4.4)) + wheel.setFrictionSlip(wheel_cfg.get('friction_slip', 100.0)) + wheel.setRollInfluence(wheel_cfg.get('roll_influence', 0.1)) + wheel.setSteering(0.0) + wheel.setEngineForce(0.0) + wheel.setBrake(0.0) + + wheels.append(wheel) + + # 添加到物理世界 + self.physics_world.attachVehicle(vehicle) + + # 保存车辆信息 + vehicle_info = { + 'vehicle': vehicle, + 'chassis': chassis_body, + 'wheels': wheels, + 'wheel_config': wheel_config, + 'speed': 0.0, + 'steering': 0.0, + 'engine_force': 0.0, + 'brake_force': 0.0, + 'max_engine_force': 3000.0, + 'max_brake_force': 100.0, + 'max_steering': math.pi/4, + 'creation_time': self.world.globalClock.getFrameTime() + } + + self.vehicles.append(vehicle_info) + print("创建车辆") + return vehicle_info + + except Exception as e: + print(f"创建车辆时出错: {e}") + return None + + def update_physics(self, time_delta): + """ + 更新物理模拟(增强版) + """ + try: + if not self.physics_enabled or not self.physics_world: + return + + # 性能监控开始 + start_time = self.world.globalClock.getRealTime() + + # 累积时间 + self.time_accumulator += time_delta + + # 限制最大子步数以防止螺旋式死亡 + num_substeps = min( + int(self.time_accumulator / self.internal_time_step), + self.max_substeps + ) + + # 确保至少执行一次更新 + if num_substeps == 0 and self.time_accumulator > 0: + num_substeps = 1 + + # 执行子步长模拟 + for i in range(num_substeps): + # 更新物理世界 + self.physics_world.doPhysics(self.internal_time_step) + + # 更新力和扭矩 + self._update_forces_and_torques(self.internal_time_step) + + # 更新粒子系统 + self._update_particles(self.internal_time_step) + + # 更新软体 + self._update_soft_bodies(self.internal_time_step) + + # 减少累积时间 + if num_substeps > 0: + self.time_accumulator -= num_substeps * self.internal_time_step + + # 更新可视化节点位置 + self._update_visualization() + + # 更新车辆 + self._update_vehicles(time_delta) + + # 更新性能统计 + end_time = self.world.globalClock.getRealTime() + update_time = end_time - start_time + self.performance_stats['last_update_time'] = update_time + self.performance_stats['update_count'] += 1 + + # 计算平均更新时间 + if self.performance_stats['update_count'] > 1: + alpha = 0.1 + self.performance_stats['avg_update_time'] = ( + alpha * update_time + + (1 - alpha) * self.performance_stats['avg_update_time'] + ) + else: + self.performance_stats['avg_update_time'] = update_time + + # 更新最大更新时间 + if update_time > self.performance_stats['max_update_time']: + self.performance_stats['max_update_time'] = update_time + + except Exception as e: + print(f"更新物理模拟时出错: {e}") + + def _update_forces_and_torques(self, time_delta): + """ + 更新力和扭矩 + """ + try: + current_time = self.world.globalClock.getFrameTime() + + for body_info in self.rigid_bodies: + body_node = body_info['body'] + + # 应用重力 + if body_info['mass'] > 0 and not body_node.isKinematic(): + gravity_force = self.physics_params['gravity'] * body_info['mass'] + body_node.applyCentralForce(gravity_force) + + # 应用空气阻力 + velocity = body_node.getLinearVelocity() + air_resistance = -velocity * self.physics_params['air_density'] * 0.1 + body_node.applyCentralForce(air_resistance) + + # 应用自定义力 + for force_info in body_info.get('forces', []): + if force_info['duration'] > 0: + body_node.applyCentralForce(force_info['force']) + force_info['duration'] -= time_delta + else: + body_info['forces'].remove(force_info) + + # 应用自定义扭矩 + for torque_info in body_info.get('torques', []): + if torque_info['duration'] > 0: + body_node.applyTorque(torque_info['torque']) + torque_info['duration'] -= time_delta + else: + body_info['torques'].remove(torque_info) + + # 更新状态 + body_info['velocity'] = body_node.getLinearVelocity() + body_info['angular_velocity'] = body_node.getAngularVelocity() + body_info['last_update'] = current_time + + except Exception as e: + print(f"更新力和扭矩时出错: {e}") + + def _update_visualization(self): + """ + 更新可视化节点 + """ + try: + for body_info in self.rigid_bodies: + if body_info['original_node'] and not body_info['original_node'].isEmpty(): + # 同步物理节点和可视化节点 + body_node = body_info['body'] + original_node = body_info['original_node'] + + # 更新位置和旋转 + original_node.setPos(body_node.getTransform().getPos()) + original_node.setHpr(body_node.getTransform().getHpr()) + + except Exception as e: + print(f"更新可视化时出错: {e}") + + def _update_particles(self, time_delta): + """ + 更新粒子系统 + """ + try: + for particle_info in self.particles[:]: # 使用切片复制 + particle_body = particle_info['body'] + + # 更新粒子生命周期 + particle_info['age'] += time_delta + if particle_info['age'] > particle_info['lifetime']: + self.remove_rigid_body(particle_info) + if particle_info in self.particles: + self.particles.remove(particle_info) + continue + + # 应用粒子力(如重力、风力等) + if particle_info.get('affected_by_gravity', True): + gravity_force = self.physics_params['gravity'] * particle_info['mass'] + particle_body.applyCentralForce(gravity_force) + + # 应用阻尼 + velocity = particle_body.getLinearVelocity() + damping_force = -velocity * particle_info.get('damping', 0.1) + particle_body.applyCentralForce(damping_force) + + except Exception as e: + print(f"更新粒子时出错: {e}") + + def _update_soft_bodies(self, time_delta): + """ + 更新软体 + """ + try: + for soft_body_info in self.soft_bodies: + # 软体由Bullet物理引擎自动更新 + pass + + except Exception as e: + print(f"更新软体时出错: {e}") + + def _update_vehicles(self, time_delta): + """ + 更新车辆 + """ + try: + for vehicle_info in self.vehicles: + vehicle = vehicle_info['vehicle'] + # 车辆更新由BulletVehicle自动处理 + + # 更新车辆状态 + vehicle_info['speed'] = vehicle.getCurrentSpeedKmHour() + + except Exception as e: + print(f"更新车辆时出错: {e}") + + def apply_force(self, body_info, force, duration=0.0, position=None): + """ + 对刚体施加力(增强版) + """ + try: + if duration <= 0: + # 瞬时力 + if position is not None: + body_info['body'].applyForce(force, position) + else: + body_info['body'].applyCentralForce(force) + else: + # 持续力 + force_info = { + 'force': force, + 'duration': duration, + 'position': position + } + body_info.setdefault('forces', []).append(force_info) + + print(f"对刚体施加力: {force}") + return True + + except Exception as e: + print(f"施加力时出错: {e}") + return False + + def apply_torque(self, body_info, torque, duration=0.0): + """ + 对刚体施加扭矩(增强版) + """ + try: + if duration <= 0: + # 瞬时扭矩 + body_info['body'].applyTorque(torque) + else: + # 持续扭矩 + torque_info = { + 'torque': torque, + 'duration': duration + } + body_info.setdefault('torques', []).append(torque_info) + + print(f"对刚体施加扭矩: {torque}") + return True + + except Exception as e: + print(f"施加扭矩时出错: {e}") + return False + + def ray_test(self, from_pos, to_pos): + """ + 射线检测(增强版) + """ + try: + if not self.physics_enabled or not self.physics_world: + return None + + # 执行射线检测 + result = self.physics_world.rayTestClosest(from_pos, to_pos) + + if result.hasHit(): + hit_info = { + 'hit_point': result.getHitPos(), + 'hit_normal': result.getHitNormal(), + 'hit_fraction': result.getHitFraction(), + 'node': result.getNode(), + 'body': result.getRigidBody(), + 'distance': (to_pos - from_pos).length() * result.getHitFraction() + } + return hit_info + + except Exception as e: + print(f"射线检测时出错: {e}") + return None + + def sphere_test(self, position, radius): + """ + 球体检测(增强版) + """ + try: + if not self.physics_enabled or not self.physics_world: + return [] + + # 执行球体检测 + results = self.physics_world.sphereTest(position, radius) + + hit_list = [] + for result in results: + hit_info = { + 'hit_point': result.getHitPos(), + 'hit_normal': result.getHitNormal(), + 'node': result.getNode(), + 'body': result.getRigidBody(), + 'distance': (result.getHitPos() - position).length() + } + hit_list.append(hit_info) + + return hit_list + + except Exception as e: + print(f"球体检测时出错: {e}") + return [] + + def set_gravity(self, gravity_vector): + """ + 设置重力 + """ + try: + if self.physics_world: + self.physics_world.setGravity(gravity_vector) + self.physics_params['gravity'] = gravity_vector + print(f"设置重力: {gravity_vector}") + return True + + except Exception as e: + print(f"设置重力时出错: {e}") + return False + + def set_physics_enabled(self, enabled): + """ + 启用或禁用物理系统 + """ + self.physics_enabled = enabled + print(f"物理系统已{'启用' if enabled else '禁用'}") + + def get_physics_stats(self): + """ + 获取物理统计信息(增强版) + """ + stats = { + 'enabled': self.physics_enabled, + 'rigid_bodies': len(self.rigid_bodies), + 'constraints': len(self.constraints), + 'vehicles': len(self.vehicles), + 'particles': len(self.particles), + 'soft_bodies': len(self.soft_bodies), + 'materials': len(self.materials), + 'gravity': self.physics_params['gravity'], + 'substeps': self.substeps, + 'internal_time_step': self.internal_time_step, + 'time_accumulator': self.time_accumulator, + 'performance': { + 'last_update_time': self.performance_stats['last_update_time'], + 'avg_update_time': self.performance_stats['avg_update_time'], + 'max_update_time': self.performance_stats['max_update_time'], + 'update_count': self.performance_stats['update_count'] + } + } + + # 统计材质分布 + material_stats = {} + for body_info in self.rigid_bodies: + material = body_info.get('material', 'default') + if material in material_stats: + material_stats[material] += 1 + else: + material_stats[material] = 1 + + stats['material_distribution'] = material_stats + + # 统计刚体类型分布 + body_type_stats = {} + for body_info in self.rigid_bodies: + body_type = body_info.get('type', 'unknown') + if body_type in body_type_stats: + body_type_stats[body_type] += 1 + else: + body_type_stats[body_type] = 1 + + stats['body_type_distribution'] = body_type_stats + + # 统计约束类型分布 + constraint_stats = {} + for constraint_info in self.constraints: + constraint_type = constraint_info.get('type', 'unknown') + if constraint_type in constraint_stats: + constraint_stats[constraint_type] += 1 + else: + constraint_stats[constraint_type] = 1 + + stats['constraint_distribution'] = constraint_stats + + return stats + + def save_physics_data(self, output_path): + """ + 保存物理数据(增强版) + """ + try: + # 收集物理数据 + physics_data = { + 'materials': {}, + 'terrain_properties': {}, + 'settings': { + 'gravity': [self.physics_params['gravity'].x, + self.physics_params['gravity'].y, + self.physics_params['gravity'].z], + 'substeps': self.substeps, + 'internal_time_step': self.internal_time_step, + 'air_density': self.physics_params['air_density'] + } + } + + # 保存材质 + for name, material in self.materials.items(): + physics_data['materials'][name] = { + 'friction': material['friction'], + 'restitution': material['restitution'], + 'rolling_friction': material['rolling_friction'], + 'spinning_friction': material['spinning_friction'], + 'density': material['density'], + 'static_friction': material.get('static_friction', 0.6), + 'dynamic_friction': material.get('dynamic_friction', 0.4), + 'contact_cfm': material.get('contact_cfm', 0.0), + 'contact_erp': material.get('contact_erp', 0.2) + } + + # 保存地形物理属性 + for terrain_node, props in self.terrain_physics.items(): + # 这里需要序列化节点引用 + node_name = terrain_node.getName() if not terrain_node.isEmpty() else "unknown" + physics_data['terrain_properties'][node_name] = { + 'material': props['material'] + } + + # 确保输出目录存在 + output_dir = os.path.dirname(output_path) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + # 写入JSON文件 + with open(output_path, 'w', encoding='utf-8') as f: + json.dump(physics_data, f, indent=2, ensure_ascii=False) + + print(f"物理数据保存成功: {output_path}") + return True + + except Exception as e: + print(f"保存物理数据时出错: {e}") + return False + + def load_physics_data(self, input_path): + """ + 加载物理数据(增强版) + """ + try: + if not os.path.exists(input_path): + print(f"物理数据文件不存在: {input_path}") + return False + + # 读取JSON文件 + with open(input_path, 'r', encoding='utf-8') as f: + physics_data = json.load(f) + + # 恢复设置 + if 'settings' in physics_data: + settings = physics_data['settings'] + if 'gravity' in settings: + gravity = settings['gravity'] + self.physics_params['gravity'] = Vec3(gravity[0], gravity[1], gravity[2]) + self.set_gravity(self.physics_params['gravity']) + if 'substeps' in settings: + self.substeps = settings['substeps'] + if 'internal_time_step' in settings: + self.internal_time_step = settings['internal_time_step'] + if 'air_density' in settings: + self.physics_params['air_density'] = settings['air_density'] + + # 加载材质 + for name, properties in physics_data.get('materials', {}).items(): + self.define_material(name, properties) + + # 应用地形物理属性(需要在地形加载后执行) + self._pending_terrain_properties = physics_data.get('terrain_properties', {}) + + print(f"物理数据加载成功: {input_path}") + return True + + except Exception as e: + print(f"加载物理数据时出错: {e}") + return False + + def apply_pending_terrain_properties(self, terrain_manager): + """ + 应用待处理的地形物理属性 + """ + try: + if not hasattr(self, '_pending_terrain_properties'): + return + + for terrain_info in terrain_manager.terrains: + terrain_node = terrain_info['node'] + node_name = terrain_node.getName() + + if node_name in self._pending_terrain_properties: + props = self._pending_terrain_properties[node_name] + material_name = props['material'] + self.assign_material_to_terrain(terrain_info, material_name) + + # 清除待处理属性 + delattr(self, '_pending_terrain_properties') + + except Exception as e: + print(f"应用待处理地形物理属性时出错: {e}") + + def clear_all_physics(self): + """ + 清除所有物理对象 + """ + try: + # 移除所有约束 + for constraint_info in self.constraints[:]: + self.remove_constraint(constraint_info) + + # 移除所有刚体 + for body_info in self.rigid_bodies[:]: # 使用切片复制列表 + self.remove_rigid_body(body_info) + + # 移除所有车辆 + self.vehicles = [] + + # 移除所有粒子 + self.particles = [] + + # 移除所有软体 + self.soft_bodies = [] + + # 清空数据结构 + self.rigid_bodies = [] + self.constraints = [] + self.terrain_physics = {} + + print("清除所有物理对象") + return True + + except Exception as e: + print(f"清除所有物理对象时出错: {e}") + return False + + def create_debris_effect(self, position, count=10, material_name=None, + velocity_range=(5, 15), size_range=(0.05, 0.2)): + """ + 创建碎片效果(增强版) + """ + try: + debris_bodies = [] + + for i in range(count): + # 创建碎片节点 + from panda3d.core import CardMaker + size = random.uniform(size_range[0], size_range[1]) + cm = CardMaker(f'debris_{i}') + cm.setFrame(-size, size, -size, size) + debris_node = self.world.render.attachNewNode(cm.generate()) + debris_node.setPos(position) + + # 添加随机偏移 + offset = Vec3( + random.uniform(-1, 1), + random.uniform(-1, 1), + random.uniform(0, 2) + ) + debris_node.setPos(position + offset) + + # 创建碎片刚体 + body_info = self.create_rigid_body( + debris_node, + mass=random.uniform(0.1, 1.0), + shape_type='box', + material_name=material_name + ) + + if body_info: + # 应加初始力 + initial_force = Vec3( + random.uniform(-1, 1) * random.uniform(velocity_range[0], velocity_range[1]) * 100, + random.uniform(-1, 1) * random.uniform(velocity_range[0], velocity_range[1]) * 100, + random.uniform(0, 1) * random.uniform(velocity_range[0], velocity_range[1]) * 100 + ) + self.apply_force(body_info, initial_force, duration=0.0) + + # 设置角速度 + body_info['body'].setAngularVelocity(Vec3( + random.uniform(-10, 10), + random.uniform(-10, 10), + random.uniform(-10, 10) + )) + + # 设置生命周期 + body_info['lifetime'] = random.uniform(2.0, 5.0) + body_info['age'] = 0.0 + body_info['affected_by_gravity'] = True + body_info['damping'] = random.uniform(0.01, 0.1) + + debris_bodies.append(body_info) + self.particles.append(body_info) # 添加到粒子系统 + + print(f"创建碎片效果: {count} 个碎片") + return debris_bodies + + except Exception as e: + print(f"创建碎片效果时出错: {e}") + return [] + + def simulate_earthquake(self, intensity=1.0, duration=5.0, epicenter=None): + """ + 模拟地震效果(增强版) + """ + try: + if epicenter is None: + epicenter = Point3(0, 0, 0) + + # 创建地震力 + earthquake_force = Vec3( + random.uniform(-1, 1) * intensity * 1000, + random.uniform(-1, 1) * intensity * 1000, + random.uniform(0, 1) * intensity * 500 + ) + + # 对所有地形相关的刚体施加力 + for body_info in self.rigid_bodies: + if body_info.get('type') == 'terrain': + # 对地形施加震动 + self.apply_force(body_info, earthquake_force, duration=0.1) + + # 对附近的物体施加力 + for body_info in self.rigid_bodies: + if body_info.get('type') == 'rigid_body' and body_info['mass'] > 0: + # 计算距离 + body_pos = body_info['node'].getPos() + distance = (body_pos - epicenter).length() + + # 距离衰减 + if distance < 50.0: # 50米范围内受影响 + force_multiplier = max(0.1, 1.0 - distance / 50.0) + local_force = earthquake_force * force_multiplier + self.apply_force(body_info, local_force, duration=0.1) + + print(f"模拟地震效果: 强度 {intensity}, 持续时间 {duration}秒") + return True + + except Exception as e: + print(f"模拟地震效果时出错: {e}") + return False + + def set_physics_substeps(self, substeps, internal_time_step=None): + """ + 设置物理子步长 + """ + self.substeps = max(1, substeps) + if internal_time_step: + self.internal_time_step = internal_time_step + print(f"物理子步长设置: {self.substeps} 步, 时间步长: {self.internal_time_step}") + + def create_soft_body(self, node, mass=1.0, material_name=None): + """ + 创建软体(实验性) + """ + try: + if not self.physics_enabled: + print("物理系统已禁用") + return None + + # 创建软体节点 + soft_body_name = f'soft_body_{len(self.soft_bodies)}' + soft_body_node = BulletSoftBodyNode(soft_body_name) + + # 设置软体属性 + cfg = soft_body_node.get_cfg() + cfg.set_kVC(0.5) # 体积保持系数 + cfg.set_kDF(0.5) # 动态摩擦系数 + cfg.set_kDP(0.01) # 压力系数 + cfg.set_kCHR(1.0) # 完全保持形状 + cfg.set_kKHR(0.1) # 运动学保持形状 + cfg.set_kSHR(1.0) # 静态保持形状 + cfg.set_kAHR(0.7) # 锚点保持形状 + + # 设置材质属性 + if material_name and material_name in self.materials: + material = self.materials[material_name] + soft_body_node.setFriction(material['friction']) + soft_body_node.setRestitution(material['restitution']) + + # 创建软体节点 + soft_body_np = node.attachNewNode(soft_body_node) + soft_body_np.setPos(node.getPos()) + soft_body_np.setHpr(node.getHpr()) + + # 添加到物理世界 + self.physics_world.attachSoftBody(soft_body_node) + + # 保存软体信息 + soft_body_info = { + 'node': soft_body_np, + 'body': soft_body_node, + 'original_node': node, + 'mass': mass, + 'material': material_name, + 'type': 'soft_body', + 'creation_time': self.world.globalClock.getFrameTime() + } + + self.soft_bodies.append(soft_body_info) + + print(f"创建软体: {node.getName()} (质量: {mass})") + return soft_body_info + + except Exception as e: + print(f"创建软体时出错: {e}") + return None + + def remove_soft_body(self, soft_body_info): + """ + 移除软体 + """ + try: + if soft_body_info in self.soft_bodies: + # 从物理世界移除 + self.physics_world.removeSoftBody(soft_body_info['body']) + + # 移除节点 + if soft_body_info['node'] and not soft_body_info['node'].isEmpty(): + soft_body_info['node'].removeNode() + + # 如果有原始节点,恢复其位置 + if soft_body_info['original_node'] and not soft_body_info['original_node'].isEmpty(): + soft_body_info['original_node'].show() + + # 从列表移除 + self.soft_bodies.remove(soft_body_info) + + print("移除软体") + return True + + except Exception as e: + print(f"移除软体时出错: {e}") + return False + + def set_vehicle_control(self, vehicle_info, steering=0.0, engine_force=0.0, brake_force=0.0): + """ + 设置车辆控制(增强版) + """ + try: + vehicle = vehicle_info['vehicle'] + wheels = vehicle_info['wheels'] + + # 限制控制值 + steering = max(-1.0, min(1.0, steering)) + engine_force = max(-1.0, min(1.0, engine_force)) + brake_force = max(0.0, min(1.0, brake_force)) + + # 应用转向 + max_steering = vehicle_info['max_steering'] + for i, wheel in enumerate(wheels): + # 前轮转向 + if i < 2: # 假设前两个轮子是前轮 + wheel.setSteering(steering * max_steering) + + # 应用引擎力和刹车力 + max_engine_force = vehicle_info['max_engine_force'] + max_brake_force = vehicle_info['max_brake_force'] + + for i, wheel in enumerate(wheels): + # 后轮驱动 + if i >= 2: # 假设后两个轮子是后轮 + wheel.setEngineForce(engine_force * max_engine_force) + wheel.setBrake(brake_force * max_brake_force) + + # 更新车辆信息 + vehicle_info['steering'] = steering + vehicle_info['engine_force'] = engine_force + vehicle_info['brake_force'] = brake_force + + return True + + except Exception as e: + print(f"设置车辆控制时出错: {e}") return False \ No newline at end of file diff --git a/plugins/user/terrain_editor/plugin.py b/plugins/user/terrain_editor/plugin.py index 21916124..06a8793f 100644 --- a/plugins/user/terrain_editor/plugin.py +++ b/plugins/user/terrain_editor/plugin.py @@ -1,133 +1,133 @@ -""" -地形编辑器核心类 -""" - -from plugins.plugin_manager import BasePlugin - -class TerrainEditorPlugin(BasePlugin): - """ - 地形编辑器插件核心类 - """ - - def __init__(self, plugin_manager, name): - super().__init__(plugin_manager, name) - self.config = { - "version": "1.0.0", - "author": "EG Team", - "description": "地形编辑器插件,提供完整的地形创建和编辑功能" - } - self.terrain_editor_ui = None - self.terrain_tools = None - self.terrain_materials = None - self.terrain_io = None - self.terrain_optimization = None - - def initialize(self) -> bool: - """ - 初始化地形编辑器插件 - """ - print(f"初始化地形编辑器插件: {self.name}") - return True - - def enable(self) -> bool: - """ - 启用地形编辑器插件 - """ - if not super().enable(): - return False - - print(f"启用地形编辑器插件: {self.name}") - - # 初始化各个模块 - self._init_modules() - - # 创建UI界面 - self._create_ui() - - # 注册事件处理器 - self._register_events() - - return True - - def disable(self) -> bool: - """ - 禁用地形编辑器插件 - """ - if not super().disable(): - return False - - print(f"禁用地形编辑器插件: {self.name}") - - # 清理UI界面 - self._cleanup_ui() - - # 注销事件处理器 - self._unregister_events() - - return True - - def finalize(self): - """ - 清理地形编辑器插件资源 - """ - print(f"清理地形编辑器插件资源: {self.name}") - - def _init_modules(self): - """ - 初始化各个功能模块 - """ - try: - # 动态导入各个模块 - from .ui.terrain_editor_ui import TerrainEditorUI - from .tools.terrain_tools import TerrainTools - from .materials.terrain_materials import TerrainMaterials - from .io.terrain_io import TerrainIO - from .optimization.terrain_optimization import TerrainOptimization - - # 创建模块实例 - self.terrain_editor_ui = TerrainEditorUI(self.plugin_manager.world) - self.terrain_tools = TerrainTools(self.plugin_manager.world) - self.terrain_materials = TerrainMaterials(self.plugin_manager.world) - self.terrain_io = TerrainIO(self.plugin_manager.world) - self.terrain_optimization = TerrainOptimization(self.plugin_manager.world) - - print("✓ 地形编辑器各功能模块初始化成功") - except Exception as e: - print(f"❌ 地形编辑器模块初始化失败: {e}") - - def _create_ui(self): - """ - 创建UI界面 - """ - if self.terrain_editor_ui: - self.terrain_editor_ui.create_interface() - - def _cleanup_ui(self): - """ - 清理UI界面 - """ - if self.terrain_editor_ui: - self.terrain_editor_ui.cleanup() - - def _register_events(self): - """ - 注册事件处理器 - """ - world = self.plugin_manager.world - # 注册快捷键 - world.accept("f6", self._toggle_terrain_editor) - - def _unregister_events(self): - """ - 注销事件处理器 - """ - world = self.plugin_manager.world - # 注销快捷键 - world.ignore("f6") - - def _toggle_terrain_editor(self): - """ - 切换地形编辑器显示/隐藏 - """ - if self.terrain_editor_ui: +""" +地形编辑器核心类 +""" + +from plugins.plugin_manager import BasePlugin + +class TerrainEditorPlugin(BasePlugin): + """ + 地形编辑器插件核心类 + """ + + def __init__(self, plugin_manager, name): + super().__init__(plugin_manager, name) + self.config = { + "version": "1.0.0", + "author": "EG Team", + "description": "地形编辑器插件,提供完整的地形创建和编辑功能" + } + self.terrain_editor_ui = None + self.terrain_tools = None + self.terrain_materials = None + self.terrain_io = None + self.terrain_optimization = None + + def initialize(self) -> bool: + """ + 初始化地形编辑器插件 + """ + print(f"初始化地形编辑器插件: {self.name}") + return True + + def enable(self) -> bool: + """ + 启用地形编辑器插件 + """ + if not super().enable(): + return False + + print(f"启用地形编辑器插件: {self.name}") + + # 初始化各个模块 + self._init_modules() + + # 创建UI界面 + self._create_ui() + + # 注册事件处理器 + self._register_events() + + return True + + def disable(self) -> bool: + """ + 禁用地形编辑器插件 + """ + if not super().disable(): + return False + + print(f"禁用地形编辑器插件: {self.name}") + + # 清理UI界面 + self._cleanup_ui() + + # 注销事件处理器 + self._unregister_events() + + return True + + def finalize(self): + """ + 清理地形编辑器插件资源 + """ + print(f"清理地形编辑器插件资源: {self.name}") + + def _init_modules(self): + """ + 初始化各个功能模块 + """ + try: + # 动态导入各个模块 + from .ui.terrain_editor_ui import TerrainEditorUI + from .tools.terrain_tools import TerrainTools + from .materials.terrain_materials import TerrainMaterials + from .io.terrain_io import TerrainIO + from .optimization.terrain_optimization import TerrainOptimization + + # 创建模块实例 + self.terrain_editor_ui = TerrainEditorUI(self.plugin_manager.world) + self.terrain_tools = TerrainTools(self.plugin_manager.world) + self.terrain_materials = TerrainMaterials(self.plugin_manager.world) + self.terrain_io = TerrainIO(self.plugin_manager.world) + self.terrain_optimization = TerrainOptimization(self.plugin_manager.world) + + print("✓ 地形编辑器各功能模块初始化成功") + except Exception as e: + print(f"❌ 地形编辑器模块初始化失败: {e}") + + def _create_ui(self): + """ + 创建UI界面 + """ + if self.terrain_editor_ui: + self.terrain_editor_ui.create_interface() + + def _cleanup_ui(self): + """ + 清理UI界面 + """ + if self.terrain_editor_ui: + self.terrain_editor_ui.cleanup() + + def _register_events(self): + """ + 注册事件处理器 + """ + world = self.plugin_manager.world + # 注册快捷键 + world.accept("f6", self._toggle_terrain_editor) + + def _unregister_events(self): + """ + 注销事件处理器 + """ + world = self.plugin_manager.world + # 注销快捷键 + world.ignore("f6") + + def _toggle_terrain_editor(self): + """ + 切换地形编辑器显示/隐藏 + """ + if self.terrain_editor_ui: self.terrain_editor_ui.toggle_visibility() \ No newline at end of file diff --git a/plugins/user/terrain_editor/tools/__init__.py b/plugins/user/terrain_editor/tools/__init__.py index 6949a5b5..daa1d7dc 100644 --- a/plugins/user/terrain_editor/tools/__init__.py +++ b/plugins/user/terrain_editor/tools/__init__.py @@ -1,3 +1,3 @@ -""" -地形编辑工具模块 +""" +地形编辑工具模块 """ \ No newline at end of file diff --git a/plugins/user/terrain_editor/tools/advanced_terrain_tools.py b/plugins/user/terrain_editor/tools/advanced_terrain_tools.py index 054e8596..bb3bb1a5 100644 --- a/plugins/user/terrain_editor/tools/advanced_terrain_tools.py +++ b/plugins/user/terrain_editor/tools/advanced_terrain_tools.py @@ -1,436 +1,436 @@ -""" -高级地形编辑工具 -提供噪声生成、侵蚀模拟、傅里叶地形等高级功能 -""" - -import math -import random -import numpy as np -from panda3d.core import PNMImage - -class AdvancedTerrainTools: - """ - 高级地形编辑工具类 - 提供噪声生成、侵蚀模拟、傅里叶地形等高级功能 - """ - - def __init__(self, world): - self.world = world - - def generate_perlin_noise(self, terrain_info, scale=1.0, octaves=4, persistence=0.5, lacunarity=2.0): - """ - 生成Perlin噪声地形 - """ - try: - terrain_node = terrain_info['node'] - heightfield = terrain_info['heightfield'] - - if not heightfield: - print("无法获取地形高度图数据") - return False - - # 获取高度图尺寸 - width = heightfield.getXSize() - height = heightfield.getYSize() - - # 生成Perlin噪声 - for y in range(height): - for x in range(width): - # 计算噪声坐标 - nx = x / width * scale - ny = y / height * scale - - # 生成分形噪声 - noise_value = self._fractal_noise(nx, ny, octaves, persistence, lacunarity) - - # 将噪声值映射到0-1范围 - normalized_value = (noise_value + 1) / 2 - heightfield.setRed(x, y, normalized_value) - heightfield.setGreen(x, y, normalized_value) - heightfield.setBlue(x, y, normalized_value) - - # 更新地形 - return self._update_terrain(terrain_info) - - except Exception as e: - print(f"生成Perlin噪声时出错: {e}") - return False - - def _fractal_noise(self, x, y, octaves, persistence, lacunarity): - """ - 生成分形噪声 - """ - total = 0.0 - frequency = 1.0 - amplitude = 1.0 - maxValue = 0.0 # 用于归一化 - - for i in range(octaves): - total += self._perlin_noise(x * frequency, y * frequency) * amplitude - maxValue += amplitude - amplitude *= persistence - frequency *= lacunarity - - return total / maxValue - - def _perlin_noise(self, x, y): - """ - 生成Perlin噪声 - """ - # 获取整数部分 - X = int(x) & 255 - Y = int(y) & 255 - - # 获取小数部分 - x -= int(x) - y -= int(y) - - # 计算平滑曲线 - u = self._fade(x) - v = self._fade(y) - - # 计算梯度 - a = self._gradient(X, Y, x, y) - b = self._gradient(X + 1, Y, x - 1, y) - c = self._gradient(X, Y + 1, x, y - 1) - d = self._gradient(X + 1, Y + 1, x - 1, y - 1) - - # 插值 - return self._lerp(v, self._lerp(u, a, b), self._lerp(u, c, d)) - - def _fade(self, t): - """ - 平滑曲线函数 - """ - return t * t * t * (t * (t * 6 - 15) + 10) - - def _lerp(self, t, a, b): - """ - 线性插值 - """ - return a + t * (b - a) - - def _gradient(self, hash_val, x, y): - """ - 梯度函数 - """ - h = hash_val & 15 - u = x if h < 8 else y - v = y if h < 4 else (x if h in [12, 14] else 0) - return (u if (h & 1) == 0 else -u) + (v if (h & 2) == 0 else -v) - - def simulate_hydraulic_erosion(self, terrain_info, iterations=100, rainfall=0.1, solubility=0.01, evaporation=0.01): - """ - 模拟水力侵蚀 - """ - try: - heightfield = terrain_info['heightfield'] - if not heightfield: - print("无法获取地形高度图数据") - return False - - width = heightfield.getXSize() - height = heightfield.getYSize() - - # 初始化水流和沉积物数组 - water = np.zeros((width, height)) - sediment = np.zeros((width, height)) - - # 执行侵蚀迭代 - for iteration in range(iterations): - # 添加降雨 - water += rainfall - - # 计算水流 - for y in range(height): - for x in range(width): - current_height = heightfield.getRed(x, y) + water[x, y] - - # 查找最低邻居 - lowest_height = current_height - lowest_x, lowest_y = x, y - - for dx in [-1, 0, 1]: - for dy in [-1, 0, 1]: - if dx == 0 and dy == 0: - continue - - nx, ny = x + dx, y + dy - if 0 <= nx < width and 0 <= ny < height: - neighbor_height = heightfield.getRed(nx, ny) + water[nx, ny] - if neighbor_height < lowest_height: - lowest_height = neighbor_height - lowest_x, lowest_y = nx, ny - - # 移动水流 - if lowest_x != x or lowest_y != y: - flow_amount = min(water[x, y], (current_height - lowest_height) * 0.5) - water[x, y] -= flow_amount - water[lowest_x, lowest_y] += flow_amount - - # 溶解物质 - dissolve_amount = flow_amount * solubility - sediment[x, y] -= dissolve_amount - sediment[lowest_x, lowest_y] += dissolve_amount - - # 沉积和蒸发 - for y in range(height): - for x in range(width): - # 蒸发 - evaporation_amount = min(water[x, y], evaporation) - water[x, y] -= evaporation_amount - - # 沉积 - if sediment[x, y] > 0: - deposit_amount = min(sediment[x, y], 0.01) - sediment[x, y] -= deposit_amount - new_height = heightfield.getRed(x, y) + deposit_amount - heightfield.setRed(x, y, new_height) - heightfield.setGreen(x, y, new_height) - heightfield.setBlue(x, y, new_height) - - # 更新地形 - return self._update_terrain(terrain_info) - - except Exception as e: - print(f"模拟水力侵蚀时出错: {e}") - return False - - def generate_voronoi_terrain(self, terrain_info, points=100, feature_size=10.0): - """ - 生成Voronoi图地形 - """ - try: - heightfield = terrain_info['heightfield'] - if not heightfield: - print("无法获取地形高度图数据") - return False - - width = heightfield.getXSize() - height = heightfield.getYSize() - - # 生成随机点 - voronoi_points = [] - for i in range(points): - x = random.randint(0, width - 1) - y = random.randint(0, height - 1) - height_value = random.random() - voronoi_points.append((x, y, height_value)) - - # 为每个像素分配最近点的高度 - for y in range(height): - for x in range(width): - min_distance = float('inf') - closest_height = 0.0 - - for px, py, ph in voronoi_points: - distance = math.sqrt((x - px)**2 + (y - py)**2) - if distance < min_distance: - min_distance = distance - closest_height = ph - - # 应用特征大小 - normalized_distance = min(1.0, min_distance / feature_size) - final_height = closest_height * (1.0 - normalized_distance) - - heightfield.setRed(x, y, final_height) - heightfield.setGreen(x, y, final_height) - heightfield.setBlue(x, y, final_height) - - # 更新地形 - return self._update_terrain(terrain_info) - - except Exception as e: - print(f"生成Voronoi地形时出错: {e}") - return False - - def generate_fourier_terrain(self, terrain_info, frequency_count=10): - """ - 生成傅里叶地形 - """ - try: - heightfield = terrain_info['heightfield'] - if not heightfield: - print("无法获取地形高度图数据") - return False - - width = heightfield.getXSize() - height_value = heightfield.getYSize() - - # 初始化高度图 - for y in range(height_value): - for x in range(width): - heightfield.setRed(x, y, 0.5) - heightfield.setGreen(x, y, 0.5) - heightfield.setBlue(x, y, 0.5) - - # 添加不同频率的波 - for i in range(frequency_count): - frequency = 2 ** i - amplitude = 1.0 / frequency - - for y in range(height_value): - for x in range(width): - # 添加正弦波 - wave_x = math.sin(2 * math.pi * x * frequency / width) * amplitude - wave_y = math.sin(2 * math.pi * y * frequency / height_value) * amplitude - current_height = heightfield.getRed(x, y) - new_height = current_height + (wave_x + wave_y) * 0.1 - new_height = max(0.0, min(1.0, new_height)) - - heightfield.setRed(x, y, new_height) - heightfield.setGreen(x, y, new_height) - heightfield.setBlue(x, y, new_height) - - # 更新地形 - return self._update_terrain(terrain_info) - - except Exception as e: - print(f"生成傅里叶地形时出错: {e}") - return False - - def generate_fault_formation(self, terrain_info, iterations=50, displacement=0.1): - """ - 生成断层地形 - """ - try: - heightfield = terrain_info['heightfield'] - if not heightfield: - print("无法获取地形高度图数据") - return False - - width = heightfield.getXSize() - height_value = heightfield.getYSize() - - # 执行断层迭代 - for i in range(iterations): - # 随机生成断层线 - angle = random.random() * 2 * math.pi - center_x = random.randint(0, width - 1) - center_y = random.randint(0, height_value - 1) - - # 计算断层方向向量 - dx = math.cos(angle) - dy = math.sin(angle) - - # 移动断层一侧的点 - for y in range(height_value): - for x in range(width): - # 计算点到断层线的距离和方向 - distance = (x - center_x) * dx + (y - center_y) * dy - - if distance > 0: - # 提升这一侧的高度 - current_height = heightfield.getRed(x, y) - new_height = current_height + displacement * (iterations - i) / iterations - new_height = max(0.0, min(1.0, new_height)) - - heightfield.setRed(x, y, new_height) - heightfield.setGreen(x, y, new_height) - heightfield.setBlue(x, y, new_height) - - # 更新地形 - return self._update_terrain(terrain_info) - - except Exception as e: - print(f"生成断层地形时出错: {e}") - return False - - def generate_thermal_erosion(self, terrain_info, iterations=100, talus_angle=0.1): - """ - 生成热侵蚀地形 - """ - try: - heightfield = terrain_info['heightfield'] - if not heightfield: - print("无法获取地形高度图数据") - return False - - width = heightfield.getXSize() - height_value = heightfield.getYSize() - - # 执行热侵蚀迭代 - for iteration in range(iterations): - # 创建高度变化数组 - height_changes = np.zeros((width, height_value)) - - # 计算每个点的侵蚀 - for y in range(height_value): - for x in range(width): - current_height = heightfield.getRed(x, y) - - # 检查周围点 - for dx in [-1, 0, 1]: - for dy in [-1, 0, 1]: - if dx == 0 and dy == 0: - continue - - nx, ny = x + dx, y + dy - if 0 <= nx < width and 0 <= ny < height_value: - neighbor_height = heightfield.getRed(nx, ny) - height_diff = current_height - neighbor_height - - # 如果高度差超过临界角度,发生滑坡 - if height_diff > talus_angle: - # 移动部分物质到邻居 - move_amount = (height_diff - talus_angle) * 0.1 - height_changes[x, y] -= move_amount - height_changes[nx, ny] += move_amount * 0.5 # 部分沉积 - - # 应用高度变化 - for y in range(height_value): - for x in range(width): - if height_changes[x, y] != 0: - current_height = heightfield.getRed(x, y) - new_height = current_height + height_changes[x, y] - new_height = max(0.0, min(1.0, new_height)) - - heightfield.setRed(x, y, new_height) - heightfield.setGreen(x, y, new_height) - heightfield.setBlue(x, y, new_height) - - # 更新地形 - return self._update_terrain(terrain_info) - - except Exception as e: - print(f"生成热侵蚀地形时出错: {e}") - return False - - def _update_terrain(self, terrain_info): - """ - 更新地形 - """ - try: - terrain = terrain_info['terrain'] - terrain.setHeightfield(terrain_info['heightfield']) - terrain.generate() - - # 重新创建碰撞体 - self._recreate_collision(terrain_info['node']) - - return True - except Exception as e: - print(f"更新地形时出错: {e}") - return False - - def _recreate_collision(self, terrain_node): - """ - 重新创建地形碰撞体 - """ - try: - from panda3d.core import BitMask32 - - # 移除旧的地形碰撞体 - for child in terrain_node.getChildren(): - if child.getName().startswith("terrain_collision_"): - child.removeNode() - - # 设置地形节点的碰撞掩码 - terrain_node.setCollideMask(BitMask32.bit(2)) - - # 为地形的每个子节点也设置碰撞掩码 - for child in terrain_node.getChildren(): - child.setCollideMask(BitMask32.bit(2)) - - except Exception as e: +""" +高级地形编辑工具 +提供噪声生成、侵蚀模拟、傅里叶地形等高级功能 +""" + +import math +import random +import numpy as np +from panda3d.core import PNMImage + +class AdvancedTerrainTools: + """ + 高级地形编辑工具类 + 提供噪声生成、侵蚀模拟、傅里叶地形等高级功能 + """ + + def __init__(self, world): + self.world = world + + def generate_perlin_noise(self, terrain_info, scale=1.0, octaves=4, persistence=0.5, lacunarity=2.0): + """ + 生成Perlin噪声地形 + """ + try: + terrain_node = terrain_info['node'] + heightfield = terrain_info['heightfield'] + + if not heightfield: + print("无法获取地形高度图数据") + return False + + # 获取高度图尺寸 + width = heightfield.getXSize() + height = heightfield.getYSize() + + # 生成Perlin噪声 + for y in range(height): + for x in range(width): + # 计算噪声坐标 + nx = x / width * scale + ny = y / height * scale + + # 生成分形噪声 + noise_value = self._fractal_noise(nx, ny, octaves, persistence, lacunarity) + + # 将噪声值映射到0-1范围 + normalized_value = (noise_value + 1) / 2 + heightfield.setRed(x, y, normalized_value) + heightfield.setGreen(x, y, normalized_value) + heightfield.setBlue(x, y, normalized_value) + + # 更新地形 + return self._update_terrain(terrain_info) + + except Exception as e: + print(f"生成Perlin噪声时出错: {e}") + return False + + def _fractal_noise(self, x, y, octaves, persistence, lacunarity): + """ + 生成分形噪声 + """ + total = 0.0 + frequency = 1.0 + amplitude = 1.0 + maxValue = 0.0 # 用于归一化 + + for i in range(octaves): + total += self._perlin_noise(x * frequency, y * frequency) * amplitude + maxValue += amplitude + amplitude *= persistence + frequency *= lacunarity + + return total / maxValue + + def _perlin_noise(self, x, y): + """ + 生成Perlin噪声 + """ + # 获取整数部分 + X = int(x) & 255 + Y = int(y) & 255 + + # 获取小数部分 + x -= int(x) + y -= int(y) + + # 计算平滑曲线 + u = self._fade(x) + v = self._fade(y) + + # 计算梯度 + a = self._gradient(X, Y, x, y) + b = self._gradient(X + 1, Y, x - 1, y) + c = self._gradient(X, Y + 1, x, y - 1) + d = self._gradient(X + 1, Y + 1, x - 1, y - 1) + + # 插值 + return self._lerp(v, self._lerp(u, a, b), self._lerp(u, c, d)) + + def _fade(self, t): + """ + 平滑曲线函数 + """ + return t * t * t * (t * (t * 6 - 15) + 10) + + def _lerp(self, t, a, b): + """ + 线性插值 + """ + return a + t * (b - a) + + def _gradient(self, hash_val, x, y): + """ + 梯度函数 + """ + h = hash_val & 15 + u = x if h < 8 else y + v = y if h < 4 else (x if h in [12, 14] else 0) + return (u if (h & 1) == 0 else -u) + (v if (h & 2) == 0 else -v) + + def simulate_hydraulic_erosion(self, terrain_info, iterations=100, rainfall=0.1, solubility=0.01, evaporation=0.01): + """ + 模拟水力侵蚀 + """ + try: + heightfield = terrain_info['heightfield'] + if not heightfield: + print("无法获取地形高度图数据") + return False + + width = heightfield.getXSize() + height = heightfield.getYSize() + + # 初始化水流和沉积物数组 + water = np.zeros((width, height)) + sediment = np.zeros((width, height)) + + # 执行侵蚀迭代 + for iteration in range(iterations): + # 添加降雨 + water += rainfall + + # 计算水流 + for y in range(height): + for x in range(width): + current_height = heightfield.getRed(x, y) + water[x, y] + + # 查找最低邻居 + lowest_height = current_height + lowest_x, lowest_y = x, y + + for dx in [-1, 0, 1]: + for dy in [-1, 0, 1]: + if dx == 0 and dy == 0: + continue + + nx, ny = x + dx, y + dy + if 0 <= nx < width and 0 <= ny < height: + neighbor_height = heightfield.getRed(nx, ny) + water[nx, ny] + if neighbor_height < lowest_height: + lowest_height = neighbor_height + lowest_x, lowest_y = nx, ny + + # 移动水流 + if lowest_x != x or lowest_y != y: + flow_amount = min(water[x, y], (current_height - lowest_height) * 0.5) + water[x, y] -= flow_amount + water[lowest_x, lowest_y] += flow_amount + + # 溶解物质 + dissolve_amount = flow_amount * solubility + sediment[x, y] -= dissolve_amount + sediment[lowest_x, lowest_y] += dissolve_amount + + # 沉积和蒸发 + for y in range(height): + for x in range(width): + # 蒸发 + evaporation_amount = min(water[x, y], evaporation) + water[x, y] -= evaporation_amount + + # 沉积 + if sediment[x, y] > 0: + deposit_amount = min(sediment[x, y], 0.01) + sediment[x, y] -= deposit_amount + new_height = heightfield.getRed(x, y) + deposit_amount + heightfield.setRed(x, y, new_height) + heightfield.setGreen(x, y, new_height) + heightfield.setBlue(x, y, new_height) + + # 更新地形 + return self._update_terrain(terrain_info) + + except Exception as e: + print(f"模拟水力侵蚀时出错: {e}") + return False + + def generate_voronoi_terrain(self, terrain_info, points=100, feature_size=10.0): + """ + 生成Voronoi图地形 + """ + try: + heightfield = terrain_info['heightfield'] + if not heightfield: + print("无法获取地形高度图数据") + return False + + width = heightfield.getXSize() + height = heightfield.getYSize() + + # 生成随机点 + voronoi_points = [] + for i in range(points): + x = random.randint(0, width - 1) + y = random.randint(0, height - 1) + height_value = random.random() + voronoi_points.append((x, y, height_value)) + + # 为每个像素分配最近点的高度 + for y in range(height): + for x in range(width): + min_distance = float('inf') + closest_height = 0.0 + + for px, py, ph in voronoi_points: + distance = math.sqrt((x - px)**2 + (y - py)**2) + if distance < min_distance: + min_distance = distance + closest_height = ph + + # 应用特征大小 + normalized_distance = min(1.0, min_distance / feature_size) + final_height = closest_height * (1.0 - normalized_distance) + + heightfield.setRed(x, y, final_height) + heightfield.setGreen(x, y, final_height) + heightfield.setBlue(x, y, final_height) + + # 更新地形 + return self._update_terrain(terrain_info) + + except Exception as e: + print(f"生成Voronoi地形时出错: {e}") + return False + + def generate_fourier_terrain(self, terrain_info, frequency_count=10): + """ + 生成傅里叶地形 + """ + try: + heightfield = terrain_info['heightfield'] + if not heightfield: + print("无法获取地形高度图数据") + return False + + width = heightfield.getXSize() + height_value = heightfield.getYSize() + + # 初始化高度图 + for y in range(height_value): + for x in range(width): + heightfield.setRed(x, y, 0.5) + heightfield.setGreen(x, y, 0.5) + heightfield.setBlue(x, y, 0.5) + + # 添加不同频率的波 + for i in range(frequency_count): + frequency = 2 ** i + amplitude = 1.0 / frequency + + for y in range(height_value): + for x in range(width): + # 添加正弦波 + wave_x = math.sin(2 * math.pi * x * frequency / width) * amplitude + wave_y = math.sin(2 * math.pi * y * frequency / height_value) * amplitude + current_height = heightfield.getRed(x, y) + new_height = current_height + (wave_x + wave_y) * 0.1 + new_height = max(0.0, min(1.0, new_height)) + + heightfield.setRed(x, y, new_height) + heightfield.setGreen(x, y, new_height) + heightfield.setBlue(x, y, new_height) + + # 更新地形 + return self._update_terrain(terrain_info) + + except Exception as e: + print(f"生成傅里叶地形时出错: {e}") + return False + + def generate_fault_formation(self, terrain_info, iterations=50, displacement=0.1): + """ + 生成断层地形 + """ + try: + heightfield = terrain_info['heightfield'] + if not heightfield: + print("无法获取地形高度图数据") + return False + + width = heightfield.getXSize() + height_value = heightfield.getYSize() + + # 执行断层迭代 + for i in range(iterations): + # 随机生成断层线 + angle = random.random() * 2 * math.pi + center_x = random.randint(0, width - 1) + center_y = random.randint(0, height_value - 1) + + # 计算断层方向向量 + dx = math.cos(angle) + dy = math.sin(angle) + + # 移动断层一侧的点 + for y in range(height_value): + for x in range(width): + # 计算点到断层线的距离和方向 + distance = (x - center_x) * dx + (y - center_y) * dy + + if distance > 0: + # 提升这一侧的高度 + current_height = heightfield.getRed(x, y) + new_height = current_height + displacement * (iterations - i) / iterations + new_height = max(0.0, min(1.0, new_height)) + + heightfield.setRed(x, y, new_height) + heightfield.setGreen(x, y, new_height) + heightfield.setBlue(x, y, new_height) + + # 更新地形 + return self._update_terrain(terrain_info) + + except Exception as e: + print(f"生成断层地形时出错: {e}") + return False + + def generate_thermal_erosion(self, terrain_info, iterations=100, talus_angle=0.1): + """ + 生成热侵蚀地形 + """ + try: + heightfield = terrain_info['heightfield'] + if not heightfield: + print("无法获取地形高度图数据") + return False + + width = heightfield.getXSize() + height_value = heightfield.getYSize() + + # 执行热侵蚀迭代 + for iteration in range(iterations): + # 创建高度变化数组 + height_changes = np.zeros((width, height_value)) + + # 计算每个点的侵蚀 + for y in range(height_value): + for x in range(width): + current_height = heightfield.getRed(x, y) + + # 检查周围点 + for dx in [-1, 0, 1]: + for dy in [-1, 0, 1]: + if dx == 0 and dy == 0: + continue + + nx, ny = x + dx, y + dy + if 0 <= nx < width and 0 <= ny < height_value: + neighbor_height = heightfield.getRed(nx, ny) + height_diff = current_height - neighbor_height + + # 如果高度差超过临界角度,发生滑坡 + if height_diff > talus_angle: + # 移动部分物质到邻居 + move_amount = (height_diff - talus_angle) * 0.1 + height_changes[x, y] -= move_amount + height_changes[nx, ny] += move_amount * 0.5 # 部分沉积 + + # 应用高度变化 + for y in range(height_value): + for x in range(width): + if height_changes[x, y] != 0: + current_height = heightfield.getRed(x, y) + new_height = current_height + height_changes[x, y] + new_height = max(0.0, min(1.0, new_height)) + + heightfield.setRed(x, y, new_height) + heightfield.setGreen(x, y, new_height) + heightfield.setBlue(x, y, new_height) + + # 更新地形 + return self._update_terrain(terrain_info) + + except Exception as e: + print(f"生成热侵蚀地形时出错: {e}") + return False + + def _update_terrain(self, terrain_info): + """ + 更新地形 + """ + try: + terrain = terrain_info['terrain'] + terrain.setHeightfield(terrain_info['heightfield']) + terrain.generate() + + # 重新创建碰撞体 + self._recreate_collision(terrain_info['node']) + + return True + except Exception as e: + print(f"更新地形时出错: {e}") + return False + + def _recreate_collision(self, terrain_node): + """ + 重新创建地形碰撞体 + """ + try: + from panda3d.core import BitMask32 + + # 移除旧的地形碰撞体 + for child in terrain_node.getChildren(): + if child.getName().startswith("terrain_collision_"): + child.removeNode() + + # 设置地形节点的碰撞掩码 + terrain_node.setCollideMask(BitMask32.bit(2)) + + # 为地形的每个子节点也设置碰撞掩码 + for child in terrain_node.getChildren(): + child.setCollideMask(BitMask32.bit(2)) + + except Exception as e: print(f"重新创建碰撞体时出错: {e}") \ No newline at end of file diff --git a/plugins/user/terrain_editor/tools/terrain_tools.py b/plugins/user/terrain_editor/tools/terrain_tools.py index 33738097..be31ef46 100644 --- a/plugins/user/terrain_editor/tools/terrain_tools.py +++ b/plugins/user/terrain_editor/tools/terrain_tools.py @@ -1,699 +1,699 @@ -""" -地形编辑工具模块 -""" - -import math -import random -from panda3d.core import PNMImage - -class TerrainTools: - """ - 地形编辑工具类 - 提供完整的地形编辑功能,包括抬高、降低、平坦化、平滑、噪声等工具 - """ - - def __init__(self, world): - self.world = world - self.current_tool = "抬高工具" - self.radius = 10.0 - self.strength = 50.0 - self.operation = "添加" - self.is_editing = False - self.last_edit_pos = None - self.brush_spacing = 1.0 # 画笔间隔,用于连续编辑 - - # 工具参数 - self.smooth_factor = 0.5 - self.noise_scale = 1.0 - self.erosion_factor = 0.1 - self.thermal_erosion_factor = 0.05 - - def set_tool(self, tool_name): - """ - 设置当前工具 - """ - valid_tools = ["抬高工具", "降低工具", "平坦化工具", "平滑工具", "噪声工具", "侵蚀工具", "热侵蚀工具"] - if tool_name in valid_tools: - self.current_tool = tool_name - print(f"设置地形编辑工具: {tool_name}") - return True - else: - print(f"无效的工具名称: {tool_name}") - return False - - def set_radius(self, radius): - """ - 设置工具半径 - """ - self.radius = max(0.1, min(100.0, float(radius))) - - def set_strength(self, strength): - """ - 设置工具强度 - """ - self.strength = max(0.0, min(100.0, float(strength))) - - def set_operation(self, operation): - """ - 设置操作类型 - """ - valid_operations = ["添加", "减去", "设置"] - if operation in valid_operations: - self.operation = operation - else: - print(f"无效的操作类型: {operation}") - - def set_smooth_factor(self, factor): - """ - 设置平滑因子 - """ - self.smooth_factor = max(0.0, min(1.0, float(factor))) - - def set_noise_scale(self, scale): - """ - 设置噪声缩放 - """ - self.noise_scale = max(0.1, min(10.0, float(scale))) - - def set_erosion_factor(self, factor): - """ - 设置侵蚀因子 - """ - self.erosion_factor = max(0.0, min(1.0, float(factor))) - - def set_thermal_erosion_factor(self, factor): - """ - 设置热侵蚀因子 - """ - self.thermal_erosion_factor = max(0.0, min(1.0, float(factor))) - - def set_brush_spacing(self, spacing): - """ - 设置画笔间隔 - """ - self.brush_spacing = max(0.1, min(10.0, float(spacing))) - - def start_editing(self, x, y): - """ - 开始编辑地形 - """ - self.is_editing = True - self.last_edit_pos = (x, y) - self._apply_tool(x, y) - - def update_editing(self, x, y): - """ - 更新编辑过程 - """ - if self.is_editing: - # 检查是否需要应用工具(基于画笔间隔) - if self.last_edit_pos: - distance = math.sqrt((x - self.last_edit_pos[0])**2 + (y - self.last_edit_pos[1])**2) - if distance >= self.brush_spacing: - self._apply_tool(x, y) - self.last_edit_pos = (x, y) - else: - self._apply_tool(x, y) - self.last_edit_pos = (x, y) - - def stop_editing(self): - """ - 停止编辑地形 - """ - self.is_editing = False - self.last_edit_pos = None - - def _apply_tool(self, x, y): - """ - 应用当前工具到指定位置 - """ - # 获取当前选中的地形 - selected_terrain = self._get_selected_terrain() - if not selected_terrain: - # 如果没有选中地形,尝试获取场景中的第一个地形 - selected_terrain = self._get_first_terrain() - if not selected_terrain: - print("没有可用的地形") - return - - # 根据当前工具类型应用不同的操作 - if self.current_tool == "抬高工具": - self._raise_terrain(selected_terrain, x, y) - elif self.current_tool == "降低工具": - self._lower_terrain(selected_terrain, x, y) - elif self.current_tool == "平坦化工具": - self._flatten_terrain(selected_terrain, x, y) - elif self.current_tool == "平滑工具": - self._smooth_terrain(selected_terrain, x, y) - elif self.current_tool == "噪声工具": - self._noise_terrain(selected_terrain, x, y) - elif self.current_tool == "侵蚀工具": - self._erode_terrain(selected_terrain, x, y) - elif self.current_tool == "热侵蚀工具": - self._thermal_erode_terrain(selected_terrain, x, y) - - def _get_selected_terrain(self): - """ - 获取当前选中的地形 - """ - # 如果有选择系统,获取选中的地形 - if hasattr(self.world, 'selection') and self.world.selection.selectedNode: - selected_node = self.world.selection.selectedNode - # 检查选中的节点是否是地形节点 - if selected_node.getTag("tree_item_type") == "TERRAIN_NODE": - # 在地形管理器中查找对应的地形信息 - if hasattr(self.world, 'terrain_manager'): - for terrain_info in self.world.terrain_manager.terrains: - if terrain_info['node'] == selected_node: - return terrain_info - return None - - def _get_first_terrain(self): - """ - 获取场景中的第一个地形 - """ - if hasattr(self.world, 'terrain_manager') and self.world.terrain_manager.terrains: - return self.world.terrain_manager.terrains[0] - return None - - def _raise_terrain(self, terrain_info, x, y): - """ - 抬高地形 - """ - if hasattr(self.world, 'terrain_manager'): - operation = "add" - if self.operation == "减去": - operation = "subtract" - elif self.operation == "设置": - operation = "set" - - self.world.terrain_manager.modifyTerrainHeight( - terrain_info, x, y, self.radius, self.strength, operation - ) - - def _lower_terrain(self, terrain_info, x, y): - """ - 降低地形 - """ - if hasattr(self.world, 'terrain_manager'): - operation = "subtract" - if self.operation == "添加": - operation = "add" - elif self.operation == "设置": - operation = "set" - - self.world.terrain_manager.modifyTerrainHeight( - terrain_info, x, y, self.radius, self.strength, operation - ) - - def _flatten_terrain(self, terrain_info, x, y): - """ - 平坦化地形 - """ - if hasattr(self.world, 'terrain_manager'): - # 平坦化工具使用特殊的"set"操作 - self.world.terrain_manager.modifyTerrainHeight( - terrain_info, x, y, self.radius, self.strength, "set" - ) - - def _smooth_terrain(self, terrain_info, x, y): - """ - 平滑地形 - """ - if not hasattr(self.world, 'terrain_manager'): - return - - try: - terrain_node = terrain_info['node'] - heightfield = terrain_info['heightfield'] - - if not heightfield: - print("无法获取地形高度图数据") - return - - # 获取高度图尺寸 - width = heightfield.getXSize() - height = heightfield.getYSize() - - # 获取地形节点信息 - terrain_pos = terrain_node.getPos() - terrain_scale = terrain_node.getScale() - - # 坐标转换 - relative_x = x - terrain_pos.getX() - relative_y = y - terrain_pos.getY() - scaled_x = relative_x / terrain_scale.getX() - scaled_y = relative_y / terrain_scale.getY() - center_offset = (width - 1) / 2 - center_x = int(scaled_x + center_offset) - center_y = int(scaled_y + center_offset) - - # 检查中心点是否在有效范围内 - if not (0 <= center_x < width and 0 <= center_y < height): - return - - # 计算有效编辑半径 - effective_radius = max(1, int(self.radius / max(terrain_scale.getX(), terrain_scale.getY()))) - effective_radius = min(effective_radius, width // 4, height // 4) - - # 创建临时高度图用于存储平滑结果 - temp_heightfield = PNMImage(width, height) - temp_heightfield.copyFrom(heightfield) - - # 应用平滑算法 - modified = False - for dx in range(-effective_radius, effective_radius + 1): - for dy in range(-effective_radius, effective_radius + 1): - distance = math.sqrt(dx*dx + dy*dy) - if distance <= effective_radius: - target_x = center_x + dx - target_y = center_y + dy - - # 检查边界 - if 0 <= target_x < width and 0 <= target_y < height: - # 计算邻域平均值 - total_height = 0.0 - count = 0 - - # 检查周围的8个邻居 - for nx in range(-1, 2): - for ny in range(-1, 2): - neighbor_x = target_x + nx - neighbor_y = target_y + ny - - # 检查邻居是否在边界内 - if 0 <= neighbor_x < width and 0 <= neighbor_y < height: - total_height += heightfield.getRed(neighbor_x, neighbor_y) - count += 1 - - if count > 0: - average_height = total_height / count - current_height = heightfield.getRed(target_x, target_y) - - # 应用平滑因子 - new_height = current_height * (1 - self.smooth_factor) + average_height * self.smooth_factor - - # 设置新高度到临时高度图 - temp_heightfield.setRed(target_x, target_y, new_height) - temp_heightfield.setGreen(target_x, target_y, new_height) - temp_heightfield.setBlue(target_x, target_y, new_height) - modified = True - - # 如果有修改,则更新地形 - if modified: - # 更新高度图 - terrain_info['heightfield'] = temp_heightfield - - # 重新设置高度图并生成地形 - terrain = terrain_info['terrain'] - terrain.setHeightfield(temp_heightfield) - terrain.generate() - - # 重新创建碰撞体 - self._recreate_collision(terrain_node) - - print(f"✓ 地形已平滑: 位置({x}, {y}), 半径{self.radius}") - - except Exception as e: - print(f"平滑地形时出错: {e}") - - def _noise_terrain(self, terrain_info, x, y): - """ - 添加噪声到地形 - """ - if not hasattr(self.world, 'terrain_manager'): - return - - try: - terrain_node = terrain_info['node'] - heightfield = terrain_info['heightfield'] - - if not heightfield: - print("无法获取地形高度图数据") - return - - # 获取高度图尺寸 - width = heightfield.getXSize() - height = heightfield.getYSize() - - # 获取地形节点信息 - terrain_pos = terrain_node.getPos() - terrain_scale = terrain_node.getScale() - - # 坐标转换 - relative_x = x - terrain_pos.getX() - relative_y = y - terrain_pos.getY() - scaled_x = relative_x / terrain_scale.getX() - scaled_y = relative_y / terrain_scale.getY() - center_offset = (width - 1) / 2 - center_x = int(scaled_x + center_offset) - center_y = int(scaled_y + center_offset) - - # 检查中心点是否在有效范围内 - if not (0 <= center_x < width and 0 <= center_y < height): - return - - # 计算有效编辑半径 - effective_radius = max(1, int(self.radius / max(terrain_scale.getX(), terrain_scale.getY()))) - effective_radius = min(effective_radius, width // 4, height // 4) - - # 应用噪声算法 - modified = False - for dx in range(-effective_radius, effective_radius + 1): - for dy in range(-effective_radius, effective_radius + 1): - distance = math.sqrt(dx*dx + dy*dy) - if distance <= effective_radius: - # 计算影响权重 - weight = 1.0 - (distance / effective_radius) if effective_radius > 0 else 1.0 - - target_x = center_x + dx - target_y = center_y + dy - - # 检查边界 - if 0 <= target_x < width and 0 <= target_y < height: - # 生成噪声值 - noise_value = random.uniform(-1.0, 1.0) * self.noise_scale - - # 获取当前高度 - current_height = heightfield.getRed(target_x, target_y) - - # 应用噪声 - new_height = max(0.0, min(1.0, current_height + noise_value * weight * self.strength * 0.01)) - - # 设置新高度 - heightfield.setRed(target_x, target_y, new_height) - heightfield.setGreen(target_x, target_y, new_height) - heightfield.setBlue(target_x, target_y, new_height) - modified = True - - # 如果有修改,则更新地形 - if modified: - # 重新设置高度图并生成地形 - terrain = terrain_info['terrain'] - terrain.setHeightfield(heightfield) - terrain.generate() - - # 重新创建碰撞体 - self._recreate_collision(terrain_node) - - print(f"✓ 地形已添加噪声: 位置({x}, {y}), 半径{self.radius}") - - except Exception as e: - print(f"添加噪声到地形时出错: {e}") - - def _erode_terrain(self, terrain_info, x, y): - """ - 水力侵蚀地形 - """ - if not hasattr(self.world, 'terrain_manager'): - return - - try: - terrain_node = terrain_info['node'] - heightfield = terrain_info['heightfield'] - - if not heightfield: - print("无法获取地形高度图数据") - return - - # 获取高度图尺寸 - width = heightfield.getXSize() - height = heightfield.getYSize() - - # 获取地形节点信息 - terrain_pos = terrain_node.getPos() - terrain_scale = terrain_node.getScale() - - # 坐标转换 - relative_x = x - terrain_pos.getX() - relative_y = y - terrain_pos.getY() - scaled_x = relative_x / terrain_scale.getX() - scaled_y = relative_y / terrain_scale.getY() - center_offset = (width - 1) / 2 - center_x = int(scaled_x + center_offset) - center_y = int(scaled_y + center_offset) - - # 检查中心点是否在有效范围内 - if not (0 <= center_x < width and 0 <= center_y < height): - return - - # 计算有效编辑半径 - effective_radius = max(1, int(self.radius / max(terrain_scale.getX(), terrain_scale.getY()))) - effective_radius = min(effective_radius, width // 4, height // 4) - - # 应用简单的侵蚀算法 - modified = False - for dx in range(-effective_radius, effective_radius + 1): - for dy in range(-effective_radius, effective_radius + 1): - distance = math.sqrt(dx*dx + dy*dy) - if distance <= effective_radius: - # 计算影响权重 - weight = 1.0 - (distance / effective_radius) if effective_radius > 0 else 1.0 - - target_x = center_x + dx - target_y = center_y + dy - - # 检查边界 - if 0 <= target_x < width and 0 <= target_y < height: - # 简单的侵蚀模型:降低高点,填充低点 - current_height = heightfield.getRed(target_x, target_y) - - # 根据高度确定侵蚀效果 - # 高度越高,侵蚀越强 - erosion_amount = self.erosion_factor * weight * (1.0 - current_height) * self.strength * 0.01 - - # 应用侵蚀 - new_height = max(0.0, min(1.0, current_height - erosion_amount)) - - # 设置新高度 - heightfield.setRed(target_x, target_y, new_height) - heightfield.setGreen(target_x, target_y, new_height) - heightfield.setBlue(target_x, target_y, new_height) - modified = True - - # 如果有修改,则更新地形 - if modified: - # 重新设置高度图并生成地形 - terrain = terrain_info['terrain'] - terrain.setHeightfield(heightfield) - terrain.generate() - - # 重新创建碰撞体 - self._recreate_collision(terrain_node) - - print(f"✓ 地形已应用侵蚀: 位置({x}, {y}), 半径{self.radius}") - - except Exception as e: - print(f"侵蚀地形时出错: {e}") - - def _thermal_erode_terrain(self, terrain_info, x, y): - """ - 热侵蚀地形(模拟重力作用) - """ - if not hasattr(self.world, 'terrain_manager'): - return - - try: - terrain_node = terrain_info['node'] - heightfield = terrain_info['heightfield'] - - if not heightfield: - print("无法获取地形高度图数据") - return - - # 获取高度图尺寸 - width = heightfield.getXSize() - height = heightfield.getYSize() - - # 获取地形节点信息 - terrain_pos = terrain_node.getPos() - terrain_scale = terrain_node.getScale() - - # 坐标转换 - relative_x = x - terrain_pos.getX() - relative_y = y - terrain_pos.getY() - scaled_x = relative_x / terrain_scale.getX() - scaled_y = relative_y / terrain_scale.getY() - center_offset = (width - 1) / 2 - center_x = int(scaled_x + center_offset) - center_y = int(scaled_y + center_offset) - - # 检查中心点是否在有效范围内 - if not (0 <= center_x < width and 0 <= center_y < height): - return - - # 计算有效编辑半径 - effective_radius = max(1, int(self.radius / max(terrain_scale.getX(), terrain_scale.getY()))) - effective_radius = min(effective_radius, width // 4, height // 4) - - # 应用热侵蚀算法 - modified = False - for dx in range(-effective_radius, effective_radius + 1): - for dy in range(-effective_radius, effective_radius + 1): - distance = math.sqrt(dx*dx + dy*dy) - if distance <= effective_radius: - # 计算影响权重 - weight = 1.0 - (distance / effective_radius) if effective_radius > 0 else 1.0 - - target_x = center_x + dx - target_y = center_y + dy - - # 检查边界 - if 0 <= target_x < width and 0 <= target_y < height: - # 检查周围的邻居,找到最低点 - current_height = heightfield.getRed(target_x, target_y) - lowest_neighbor_height = current_height - lowest_neighbor_x = target_x - lowest_neighbor_y = target_y - - # 检查8个邻居 - for nx in range(-1, 2): - for ny in range(-1, 2): - if nx == 0 and ny == 0: - continue # 跳过中心点 - - neighbor_x = target_x + nx - neighbor_y = target_y + ny - - # 检查邻居是否在边界内 - if 0 <= neighbor_x < width and 0 <= neighbor_y < height: - neighbor_height = heightfield.getRed(neighbor_x, neighbor_y) - if neighbor_height < lowest_neighbor_height: - lowest_neighbor_height = neighbor_height - lowest_neighbor_x = neighbor_x - lowest_neighbor_y = neighbor_y - - # 如果有更低的邻居,移动部分物质 - height_diff = current_height - lowest_neighbor_height - if height_diff > 0: - # 移动物质 - move_amount = min(height_diff, self.thermal_erosion_factor * weight * self.strength * 0.01) - - # 降低当前点 - new_height = max(0.0, current_height - move_amount) - heightfield.setRed(target_x, target_y, new_height) - heightfield.setGreen(target_x, target_y, new_height) - heightfield.setBlue(target_x, target_y, new_height) - - # 提高最低邻居点 - neighbor_new_height = min(1.0, lowest_neighbor_height + move_amount * 0.5) - heightfield.setRed(lowest_neighbor_x, lowest_neighbor_y, neighbor_new_height) - heightfield.setGreen(lowest_neighbor_x, lowest_neighbor_y, neighbor_new_height) - heightfield.setBlue(lowest_neighbor_x, lowest_neighbor_y, neighbor_new_height) - - modified = True - - # 如果有修改,则更新地形 - if modified: - # 重新设置高度图并生成地形 - terrain = terrain_info['terrain'] - terrain.setHeightfield(heightfield) - terrain.generate() - - # 重新创建碰撞体 - self._recreate_collision(terrain_node) - - print(f"✓ 地形已应用热侵蚀: 位置({x}, {y}), 半径{self.radius}") - - except Exception as e: - print(f"热侵蚀地形时出错: {e}") - - def _recreate_collision(self, terrain_node): - """ - 重新创建地形碰撞体 - """ - try: - from panda3d.core import BitMask32 - - # 移除旧的地形碰撞体 - for child in terrain_node.getChildren(): - if child.getName().startswith("terrain_collision_"): - child.removeNode() - - # 设置地形节点的碰撞掩码 - terrain_node.setCollideMask(BitMask32.bit(2)) - - # 为地形的每个子节点也设置碰撞掩码 - for child in terrain_node.getChildren(): - child.setCollideMask(BitMask32.bit(2)) - - except Exception as e: - print(f"重新创建碰撞体时出错: {e}") - - def apply_brush(self, terrain_info, x, y, brush_data): - """ - 应用画笔数据到地形 - """ - if not hasattr(self.world, 'terrain_manager'): - return - - try: - terrain_node = terrain_info['node'] - heightfield = terrain_info['heightfield'] - - if not heightfield: - print("无法获取地形高度图数据") - return - - # 获取高度图尺寸 - width = heightfield.getXSize() - height = heightfield.getYSize() - - # 获取地形节点信息 - terrain_pos = terrain_node.getPos() - terrain_scale = terrain_node.getScale() - - # 坐标转换 - relative_x = x - terrain_pos.getX() - relative_y = y - terrain_pos.getY() - scaled_x = relative_x / terrain_scale.getX() - scaled_y = relative_y / terrain_scale.getY() - center_offset = (width - 1) / 2 - center_x = int(scaled_x + center_offset) - center_y = int(scaled_y + center_offset) - - # 检查中心点是否在有效范围内 - if not (0 <= center_x < width and 0 <= center_y < height): - return - - # 应用画笔数据 - modified = False - brush_size = len(brush_data) - brush_radius = brush_size // 2 - - for dx in range(-brush_radius, brush_radius + 1): - for dy in range(-brush_radius, brush_radius + 1): - if 0 <= brush_radius + dx < brush_size and 0 <= brush_radius + dy < brush_size: - brush_value = brush_data[brush_radius + dy][brush_radius + dx] - - target_x = center_x + dx - target_y = center_y + dy - - # 检查边界 - if 0 <= target_x < width and 0 <= target_y < height: - # 获取当前高度 - current_height = heightfield.getRed(target_x, target_y) - - # 应用画笔值 - new_height = max(0.0, min(1.0, current_height + brush_value * self.strength * 0.01)) - - # 设置新高度 - heightfield.setRed(target_x, target_y, new_height) - heightfield.setGreen(target_x, target_y, new_height) - heightfield.setBlue(target_x, target_y, new_height) - modified = True - - # 如果有修改,则更新地形 - if modified: - # 重新设置高度图并生成地形 - terrain = terrain_info['terrain'] - terrain.setHeightfield(heightfield) - terrain.generate() - - # 重新创建碰撞体 - self._recreate_collision(terrain_node) - - print(f"✓ 应用画笔数据到地形: 位置({x}, {y})") - - except Exception as e: +""" +地形编辑工具模块 +""" + +import math +import random +from panda3d.core import PNMImage + +class TerrainTools: + """ + 地形编辑工具类 + 提供完整的地形编辑功能,包括抬高、降低、平坦化、平滑、噪声等工具 + """ + + def __init__(self, world): + self.world = world + self.current_tool = "抬高工具" + self.radius = 10.0 + self.strength = 50.0 + self.operation = "添加" + self.is_editing = False + self.last_edit_pos = None + self.brush_spacing = 1.0 # 画笔间隔,用于连续编辑 + + # 工具参数 + self.smooth_factor = 0.5 + self.noise_scale = 1.0 + self.erosion_factor = 0.1 + self.thermal_erosion_factor = 0.05 + + def set_tool(self, tool_name): + """ + 设置当前工具 + """ + valid_tools = ["抬高工具", "降低工具", "平坦化工具", "平滑工具", "噪声工具", "侵蚀工具", "热侵蚀工具"] + if tool_name in valid_tools: + self.current_tool = tool_name + print(f"设置地形编辑工具: {tool_name}") + return True + else: + print(f"无效的工具名称: {tool_name}") + return False + + def set_radius(self, radius): + """ + 设置工具半径 + """ + self.radius = max(0.1, min(100.0, float(radius))) + + def set_strength(self, strength): + """ + 设置工具强度 + """ + self.strength = max(0.0, min(100.0, float(strength))) + + def set_operation(self, operation): + """ + 设置操作类型 + """ + valid_operations = ["添加", "减去", "设置"] + if operation in valid_operations: + self.operation = operation + else: + print(f"无效的操作类型: {operation}") + + def set_smooth_factor(self, factor): + """ + 设置平滑因子 + """ + self.smooth_factor = max(0.0, min(1.0, float(factor))) + + def set_noise_scale(self, scale): + """ + 设置噪声缩放 + """ + self.noise_scale = max(0.1, min(10.0, float(scale))) + + def set_erosion_factor(self, factor): + """ + 设置侵蚀因子 + """ + self.erosion_factor = max(0.0, min(1.0, float(factor))) + + def set_thermal_erosion_factor(self, factor): + """ + 设置热侵蚀因子 + """ + self.thermal_erosion_factor = max(0.0, min(1.0, float(factor))) + + def set_brush_spacing(self, spacing): + """ + 设置画笔间隔 + """ + self.brush_spacing = max(0.1, min(10.0, float(spacing))) + + def start_editing(self, x, y): + """ + 开始编辑地形 + """ + self.is_editing = True + self.last_edit_pos = (x, y) + self._apply_tool(x, y) + + def update_editing(self, x, y): + """ + 更新编辑过程 + """ + if self.is_editing: + # 检查是否需要应用工具(基于画笔间隔) + if self.last_edit_pos: + distance = math.sqrt((x - self.last_edit_pos[0])**2 + (y - self.last_edit_pos[1])**2) + if distance >= self.brush_spacing: + self._apply_tool(x, y) + self.last_edit_pos = (x, y) + else: + self._apply_tool(x, y) + self.last_edit_pos = (x, y) + + def stop_editing(self): + """ + 停止编辑地形 + """ + self.is_editing = False + self.last_edit_pos = None + + def _apply_tool(self, x, y): + """ + 应用当前工具到指定位置 + """ + # 获取当前选中的地形 + selected_terrain = self._get_selected_terrain() + if not selected_terrain: + # 如果没有选中地形,尝试获取场景中的第一个地形 + selected_terrain = self._get_first_terrain() + if not selected_terrain: + print("没有可用的地形") + return + + # 根据当前工具类型应用不同的操作 + if self.current_tool == "抬高工具": + self._raise_terrain(selected_terrain, x, y) + elif self.current_tool == "降低工具": + self._lower_terrain(selected_terrain, x, y) + elif self.current_tool == "平坦化工具": + self._flatten_terrain(selected_terrain, x, y) + elif self.current_tool == "平滑工具": + self._smooth_terrain(selected_terrain, x, y) + elif self.current_tool == "噪声工具": + self._noise_terrain(selected_terrain, x, y) + elif self.current_tool == "侵蚀工具": + self._erode_terrain(selected_terrain, x, y) + elif self.current_tool == "热侵蚀工具": + self._thermal_erode_terrain(selected_terrain, x, y) + + def _get_selected_terrain(self): + """ + 获取当前选中的地形 + """ + # 如果有选择系统,获取选中的地形 + if hasattr(self.world, 'selection') and self.world.selection.selectedNode: + selected_node = self.world.selection.selectedNode + # 检查选中的节点是否是地形节点 + if selected_node.getTag("tree_item_type") == "TERRAIN_NODE": + # 在地形管理器中查找对应的地形信息 + if hasattr(self.world, 'terrain_manager'): + for terrain_info in self.world.terrain_manager.terrains: + if terrain_info['node'] == selected_node: + return terrain_info + return None + + def _get_first_terrain(self): + """ + 获取场景中的第一个地形 + """ + if hasattr(self.world, 'terrain_manager') and self.world.terrain_manager.terrains: + return self.world.terrain_manager.terrains[0] + return None + + def _raise_terrain(self, terrain_info, x, y): + """ + 抬高地形 + """ + if hasattr(self.world, 'terrain_manager'): + operation = "add" + if self.operation == "减去": + operation = "subtract" + elif self.operation == "设置": + operation = "set" + + self.world.terrain_manager.modifyTerrainHeight( + terrain_info, x, y, self.radius, self.strength, operation + ) + + def _lower_terrain(self, terrain_info, x, y): + """ + 降低地形 + """ + if hasattr(self.world, 'terrain_manager'): + operation = "subtract" + if self.operation == "添加": + operation = "add" + elif self.operation == "设置": + operation = "set" + + self.world.terrain_manager.modifyTerrainHeight( + terrain_info, x, y, self.radius, self.strength, operation + ) + + def _flatten_terrain(self, terrain_info, x, y): + """ + 平坦化地形 + """ + if hasattr(self.world, 'terrain_manager'): + # 平坦化工具使用特殊的"set"操作 + self.world.terrain_manager.modifyTerrainHeight( + terrain_info, x, y, self.radius, self.strength, "set" + ) + + def _smooth_terrain(self, terrain_info, x, y): + """ + 平滑地形 + """ + if not hasattr(self.world, 'terrain_manager'): + return + + try: + terrain_node = terrain_info['node'] + heightfield = terrain_info['heightfield'] + + if not heightfield: + print("无法获取地形高度图数据") + return + + # 获取高度图尺寸 + width = heightfield.getXSize() + height = heightfield.getYSize() + + # 获取地形节点信息 + terrain_pos = terrain_node.getPos() + terrain_scale = terrain_node.getScale() + + # 坐标转换 + relative_x = x - terrain_pos.getX() + relative_y = y - terrain_pos.getY() + scaled_x = relative_x / terrain_scale.getX() + scaled_y = relative_y / terrain_scale.getY() + center_offset = (width - 1) / 2 + center_x = int(scaled_x + center_offset) + center_y = int(scaled_y + center_offset) + + # 检查中心点是否在有效范围内 + if not (0 <= center_x < width and 0 <= center_y < height): + return + + # 计算有效编辑半径 + effective_radius = max(1, int(self.radius / max(terrain_scale.getX(), terrain_scale.getY()))) + effective_radius = min(effective_radius, width // 4, height // 4) + + # 创建临时高度图用于存储平滑结果 + temp_heightfield = PNMImage(width, height) + temp_heightfield.copyFrom(heightfield) + + # 应用平滑算法 + modified = False + for dx in range(-effective_radius, effective_radius + 1): + for dy in range(-effective_radius, effective_radius + 1): + distance = math.sqrt(dx*dx + dy*dy) + if distance <= effective_radius: + target_x = center_x + dx + target_y = center_y + dy + + # 检查边界 + if 0 <= target_x < width and 0 <= target_y < height: + # 计算邻域平均值 + total_height = 0.0 + count = 0 + + # 检查周围的8个邻居 + for nx in range(-1, 2): + for ny in range(-1, 2): + neighbor_x = target_x + nx + neighbor_y = target_y + ny + + # 检查邻居是否在边界内 + if 0 <= neighbor_x < width and 0 <= neighbor_y < height: + total_height += heightfield.getRed(neighbor_x, neighbor_y) + count += 1 + + if count > 0: + average_height = total_height / count + current_height = heightfield.getRed(target_x, target_y) + + # 应用平滑因子 + new_height = current_height * (1 - self.smooth_factor) + average_height * self.smooth_factor + + # 设置新高度到临时高度图 + temp_heightfield.setRed(target_x, target_y, new_height) + temp_heightfield.setGreen(target_x, target_y, new_height) + temp_heightfield.setBlue(target_x, target_y, new_height) + modified = True + + # 如果有修改,则更新地形 + if modified: + # 更新高度图 + terrain_info['heightfield'] = temp_heightfield + + # 重新设置高度图并生成地形 + terrain = terrain_info['terrain'] + terrain.setHeightfield(temp_heightfield) + terrain.generate() + + # 重新创建碰撞体 + self._recreate_collision(terrain_node) + + print(f"✓ 地形已平滑: 位置({x}, {y}), 半径{self.radius}") + + except Exception as e: + print(f"平滑地形时出错: {e}") + + def _noise_terrain(self, terrain_info, x, y): + """ + 添加噪声到地形 + """ + if not hasattr(self.world, 'terrain_manager'): + return + + try: + terrain_node = terrain_info['node'] + heightfield = terrain_info['heightfield'] + + if not heightfield: + print("无法获取地形高度图数据") + return + + # 获取高度图尺寸 + width = heightfield.getXSize() + height = heightfield.getYSize() + + # 获取地形节点信息 + terrain_pos = terrain_node.getPos() + terrain_scale = terrain_node.getScale() + + # 坐标转换 + relative_x = x - terrain_pos.getX() + relative_y = y - terrain_pos.getY() + scaled_x = relative_x / terrain_scale.getX() + scaled_y = relative_y / terrain_scale.getY() + center_offset = (width - 1) / 2 + center_x = int(scaled_x + center_offset) + center_y = int(scaled_y + center_offset) + + # 检查中心点是否在有效范围内 + if not (0 <= center_x < width and 0 <= center_y < height): + return + + # 计算有效编辑半径 + effective_radius = max(1, int(self.radius / max(terrain_scale.getX(), terrain_scale.getY()))) + effective_radius = min(effective_radius, width // 4, height // 4) + + # 应用噪声算法 + modified = False + for dx in range(-effective_radius, effective_radius + 1): + for dy in range(-effective_radius, effective_radius + 1): + distance = math.sqrt(dx*dx + dy*dy) + if distance <= effective_radius: + # 计算影响权重 + weight = 1.0 - (distance / effective_radius) if effective_radius > 0 else 1.0 + + target_x = center_x + dx + target_y = center_y + dy + + # 检查边界 + if 0 <= target_x < width and 0 <= target_y < height: + # 生成噪声值 + noise_value = random.uniform(-1.0, 1.0) * self.noise_scale + + # 获取当前高度 + current_height = heightfield.getRed(target_x, target_y) + + # 应用噪声 + new_height = max(0.0, min(1.0, current_height + noise_value * weight * self.strength * 0.01)) + + # 设置新高度 + heightfield.setRed(target_x, target_y, new_height) + heightfield.setGreen(target_x, target_y, new_height) + heightfield.setBlue(target_x, target_y, new_height) + modified = True + + # 如果有修改,则更新地形 + if modified: + # 重新设置高度图并生成地形 + terrain = terrain_info['terrain'] + terrain.setHeightfield(heightfield) + terrain.generate() + + # 重新创建碰撞体 + self._recreate_collision(terrain_node) + + print(f"✓ 地形已添加噪声: 位置({x}, {y}), 半径{self.radius}") + + except Exception as e: + print(f"添加噪声到地形时出错: {e}") + + def _erode_terrain(self, terrain_info, x, y): + """ + 水力侵蚀地形 + """ + if not hasattr(self.world, 'terrain_manager'): + return + + try: + terrain_node = terrain_info['node'] + heightfield = terrain_info['heightfield'] + + if not heightfield: + print("无法获取地形高度图数据") + return + + # 获取高度图尺寸 + width = heightfield.getXSize() + height = heightfield.getYSize() + + # 获取地形节点信息 + terrain_pos = terrain_node.getPos() + terrain_scale = terrain_node.getScale() + + # 坐标转换 + relative_x = x - terrain_pos.getX() + relative_y = y - terrain_pos.getY() + scaled_x = relative_x / terrain_scale.getX() + scaled_y = relative_y / terrain_scale.getY() + center_offset = (width - 1) / 2 + center_x = int(scaled_x + center_offset) + center_y = int(scaled_y + center_offset) + + # 检查中心点是否在有效范围内 + if not (0 <= center_x < width and 0 <= center_y < height): + return + + # 计算有效编辑半径 + effective_radius = max(1, int(self.radius / max(terrain_scale.getX(), terrain_scale.getY()))) + effective_radius = min(effective_radius, width // 4, height // 4) + + # 应用简单的侵蚀算法 + modified = False + for dx in range(-effective_radius, effective_radius + 1): + for dy in range(-effective_radius, effective_radius + 1): + distance = math.sqrt(dx*dx + dy*dy) + if distance <= effective_radius: + # 计算影响权重 + weight = 1.0 - (distance / effective_radius) if effective_radius > 0 else 1.0 + + target_x = center_x + dx + target_y = center_y + dy + + # 检查边界 + if 0 <= target_x < width and 0 <= target_y < height: + # 简单的侵蚀模型:降低高点,填充低点 + current_height = heightfield.getRed(target_x, target_y) + + # 根据高度确定侵蚀效果 + # 高度越高,侵蚀越强 + erosion_amount = self.erosion_factor * weight * (1.0 - current_height) * self.strength * 0.01 + + # 应用侵蚀 + new_height = max(0.0, min(1.0, current_height - erosion_amount)) + + # 设置新高度 + heightfield.setRed(target_x, target_y, new_height) + heightfield.setGreen(target_x, target_y, new_height) + heightfield.setBlue(target_x, target_y, new_height) + modified = True + + # 如果有修改,则更新地形 + if modified: + # 重新设置高度图并生成地形 + terrain = terrain_info['terrain'] + terrain.setHeightfield(heightfield) + terrain.generate() + + # 重新创建碰撞体 + self._recreate_collision(terrain_node) + + print(f"✓ 地形已应用侵蚀: 位置({x}, {y}), 半径{self.radius}") + + except Exception as e: + print(f"侵蚀地形时出错: {e}") + + def _thermal_erode_terrain(self, terrain_info, x, y): + """ + 热侵蚀地形(模拟重力作用) + """ + if not hasattr(self.world, 'terrain_manager'): + return + + try: + terrain_node = terrain_info['node'] + heightfield = terrain_info['heightfield'] + + if not heightfield: + print("无法获取地形高度图数据") + return + + # 获取高度图尺寸 + width = heightfield.getXSize() + height = heightfield.getYSize() + + # 获取地形节点信息 + terrain_pos = terrain_node.getPos() + terrain_scale = terrain_node.getScale() + + # 坐标转换 + relative_x = x - terrain_pos.getX() + relative_y = y - terrain_pos.getY() + scaled_x = relative_x / terrain_scale.getX() + scaled_y = relative_y / terrain_scale.getY() + center_offset = (width - 1) / 2 + center_x = int(scaled_x + center_offset) + center_y = int(scaled_y + center_offset) + + # 检查中心点是否在有效范围内 + if not (0 <= center_x < width and 0 <= center_y < height): + return + + # 计算有效编辑半径 + effective_radius = max(1, int(self.radius / max(terrain_scale.getX(), terrain_scale.getY()))) + effective_radius = min(effective_radius, width // 4, height // 4) + + # 应用热侵蚀算法 + modified = False + for dx in range(-effective_radius, effective_radius + 1): + for dy in range(-effective_radius, effective_radius + 1): + distance = math.sqrt(dx*dx + dy*dy) + if distance <= effective_radius: + # 计算影响权重 + weight = 1.0 - (distance / effective_radius) if effective_radius > 0 else 1.0 + + target_x = center_x + dx + target_y = center_y + dy + + # 检查边界 + if 0 <= target_x < width and 0 <= target_y < height: + # 检查周围的邻居,找到最低点 + current_height = heightfield.getRed(target_x, target_y) + lowest_neighbor_height = current_height + lowest_neighbor_x = target_x + lowest_neighbor_y = target_y + + # 检查8个邻居 + for nx in range(-1, 2): + for ny in range(-1, 2): + if nx == 0 and ny == 0: + continue # 跳过中心点 + + neighbor_x = target_x + nx + neighbor_y = target_y + ny + + # 检查邻居是否在边界内 + if 0 <= neighbor_x < width and 0 <= neighbor_y < height: + neighbor_height = heightfield.getRed(neighbor_x, neighbor_y) + if neighbor_height < lowest_neighbor_height: + lowest_neighbor_height = neighbor_height + lowest_neighbor_x = neighbor_x + lowest_neighbor_y = neighbor_y + + # 如果有更低的邻居,移动部分物质 + height_diff = current_height - lowest_neighbor_height + if height_diff > 0: + # 移动物质 + move_amount = min(height_diff, self.thermal_erosion_factor * weight * self.strength * 0.01) + + # 降低当前点 + new_height = max(0.0, current_height - move_amount) + heightfield.setRed(target_x, target_y, new_height) + heightfield.setGreen(target_x, target_y, new_height) + heightfield.setBlue(target_x, target_y, new_height) + + # 提高最低邻居点 + neighbor_new_height = min(1.0, lowest_neighbor_height + move_amount * 0.5) + heightfield.setRed(lowest_neighbor_x, lowest_neighbor_y, neighbor_new_height) + heightfield.setGreen(lowest_neighbor_x, lowest_neighbor_y, neighbor_new_height) + heightfield.setBlue(lowest_neighbor_x, lowest_neighbor_y, neighbor_new_height) + + modified = True + + # 如果有修改,则更新地形 + if modified: + # 重新设置高度图并生成地形 + terrain = terrain_info['terrain'] + terrain.setHeightfield(heightfield) + terrain.generate() + + # 重新创建碰撞体 + self._recreate_collision(terrain_node) + + print(f"✓ 地形已应用热侵蚀: 位置({x}, {y}), 半径{self.radius}") + + except Exception as e: + print(f"热侵蚀地形时出错: {e}") + + def _recreate_collision(self, terrain_node): + """ + 重新创建地形碰撞体 + """ + try: + from panda3d.core import BitMask32 + + # 移除旧的地形碰撞体 + for child in terrain_node.getChildren(): + if child.getName().startswith("terrain_collision_"): + child.removeNode() + + # 设置地形节点的碰撞掩码 + terrain_node.setCollideMask(BitMask32.bit(2)) + + # 为地形的每个子节点也设置碰撞掩码 + for child in terrain_node.getChildren(): + child.setCollideMask(BitMask32.bit(2)) + + except Exception as e: + print(f"重新创建碰撞体时出错: {e}") + + def apply_brush(self, terrain_info, x, y, brush_data): + """ + 应用画笔数据到地形 + """ + if not hasattr(self.world, 'terrain_manager'): + return + + try: + terrain_node = terrain_info['node'] + heightfield = terrain_info['heightfield'] + + if not heightfield: + print("无法获取地形高度图数据") + return + + # 获取高度图尺寸 + width = heightfield.getXSize() + height = heightfield.getYSize() + + # 获取地形节点信息 + terrain_pos = terrain_node.getPos() + terrain_scale = terrain_node.getScale() + + # 坐标转换 + relative_x = x - terrain_pos.getX() + relative_y = y - terrain_pos.getY() + scaled_x = relative_x / terrain_scale.getX() + scaled_y = relative_y / terrain_scale.getY() + center_offset = (width - 1) / 2 + center_x = int(scaled_x + center_offset) + center_y = int(scaled_y + center_offset) + + # 检查中心点是否在有效范围内 + if not (0 <= center_x < width and 0 <= center_y < height): + return + + # 应用画笔数据 + modified = False + brush_size = len(brush_data) + brush_radius = brush_size // 2 + + for dx in range(-brush_radius, brush_radius + 1): + for dy in range(-brush_radius, brush_radius + 1): + if 0 <= brush_radius + dx < brush_size and 0 <= brush_radius + dy < brush_size: + brush_value = brush_data[brush_radius + dy][brush_radius + dx] + + target_x = center_x + dx + target_y = center_y + dy + + # 检查边界 + if 0 <= target_x < width and 0 <= target_y < height: + # 获取当前高度 + current_height = heightfield.getRed(target_x, target_y) + + # 应用画笔值 + new_height = max(0.0, min(1.0, current_height + brush_value * self.strength * 0.01)) + + # 设置新高度 + heightfield.setRed(target_x, target_y, new_height) + heightfield.setGreen(target_x, target_y, new_height) + heightfield.setBlue(target_x, target_y, new_height) + modified = True + + # 如果有修改,则更新地形 + if modified: + # 重新设置高度图并生成地形 + terrain = terrain_info['terrain'] + terrain.setHeightfield(heightfield) + terrain.generate() + + # 重新创建碰撞体 + self._recreate_collision(terrain_node) + + print(f"✓ 应用画笔数据到地形: 位置({x}, {y})") + + except Exception as e: print(f"应用画笔数据到地形时出错: {e}") \ No newline at end of file diff --git a/plugins/user/terrain_editor/ui/__init__.py b/plugins/user/terrain_editor/ui/__init__.py index 86cba5cd..b6112cb3 100644 --- a/plugins/user/terrain_editor/ui/__init__.py +++ b/plugins/user/terrain_editor/ui/__init__.py @@ -1,3 +1,3 @@ -""" -地形编辑器UI模块 +""" +地形编辑器UI模块 """ \ No newline at end of file diff --git a/plugins/user/terrain_editor/ui/terrain_editor_ui.py b/plugins/user/terrain_editor/ui/terrain_editor_ui.py index 1b8eeb9f..1ab020c5 100644 --- a/plugins/user/terrain_editor/ui/terrain_editor_ui.py +++ b/plugins/user/terrain_editor/ui/terrain_editor_ui.py @@ -1,395 +1,395 @@ -""" -地形编辑器UI界面 -""" - -from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, - QPushButton, QSlider, QLabel, QComboBox, QGroupBox, - QTabWidget, QScrollArea, QFrame) -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QDoubleValidator - -class TerrainEditorUI: - """ - 地形编辑器UI界面类 - """ - - def __init__(self, world): - self.world = world - self.main_widget = None - self.terrain_tools_panel = None - self.terrain_properties_panel = None - self.material_panel = None - - def create_interface(self): - """ - 创建地形编辑器UI界面 - """ - print("创建地形编辑器UI界面") - - # 创建主窗口部件 - self.main_widget = QWidget() - self.main_widget.setWindowTitle("地形编辑器") - self.main_widget.setGeometry(100, 100, 400, 600) - - # 创建主布局 - main_layout = QVBoxLayout() - - # 创建标签页 - tab_widget = QTabWidget() - - # 工具标签页 - tools_tab = self._create_tools_tab() - tab_widget.addTab(tools_tab, "工具") - - # 属性标签页 - properties_tab = self._create_properties_tab() - tab_widget.addTab(properties_tab, "属性") - - # 材质标签页 - material_tab = self._create_material_tab() - tab_widget.addTab(material_tab, "材质") - - # 添加标签页到主布局 - main_layout.addWidget(tab_widget) - - # 添加应用和取消按钮 - button_layout = QHBoxLayout() - apply_btn = QPushButton("应用") - cancel_btn = QPushButton("取消") - apply_btn.clicked.connect(self._apply_changes) - cancel_btn.clicked.connect(self._cancel_changes) - - button_layout.addWidget(apply_btn) - button_layout.addWidget(cancel_btn) - - main_layout.addLayout(button_layout) - - self.main_widget.setLayout(main_layout) - - # 隐藏界面,等待用户打开 - self.main_widget.hide() - - def _create_tools_tab(self): - """ - 创建工具标签页 - """ - tools_tab = QWidget() - layout = QVBoxLayout() - - # 创建工具选择组 - tools_group = QGroupBox("编辑工具") - tools_layout = QVBoxLayout() - - # 工具选择下拉框 - self.tool_selector = QComboBox() - self.tool_selector.addItems([ - "抬高工具", - "降低工具", - "平坦化工具", - "平滑工具", - "噪声工具" - ]) - self.tool_selector.currentTextChanged.connect(self._on_tool_changed) - - tools_layout.addWidget(QLabel("选择工具:")) - tools_layout.addWidget(self.tool_selector) - - # 工具参数组 - params_group = QGroupBox("工具参数") - params_layout = QGridLayout() - - # 半径参数 - params_layout.addWidget(QLabel("半径:"), 0, 0) - self.radius_slider = QSlider(Qt.Horizontal) - self.radius_slider.setRange(1, 100) - self.radius_slider.setValue(10) - self.radius_slider.valueChanged.connect(self._on_radius_changed) - params_layout.addWidget(self.radius_slider, 0, 1) - - self.radius_label = QLabel("10") - params_layout.addWidget(self.radius_label, 0, 2) - - # 强度参数 - params_layout.addWidget(QLabel("强度:"), 1, 0) - self.strength_slider = QSlider(Qt.Horizontal) - self.strength_slider.setRange(1, 100) - self.strength_slider.setValue(50) - self.strength_slider.valueChanged.connect(self._on_strength_changed) - params_layout.addWidget(self.strength_slider, 1, 1) - - self.strength_label = QLabel("50") - params_layout.addWidget(self.strength_label, 1, 2) - - # 操作类型 - params_layout.addWidget(QLabel("操作:"), 2, 0) - self.operation_selector = QComboBox() - self.operation_selector.addItems(["添加", "减去", "设置"]) - self.operation_selector.currentTextChanged.connect(self._on_operation_changed) - params_layout.addWidget(self.operation_selector, 2, 1, 1, 2) - - params_group.setLayout(params_layout) - - tools_layout.addWidget(params_group) - tools_group.setLayout(tools_layout) - - layout.addWidget(tools_group) - - # 预览设置组 - preview_group = QGroupBox("预览设置") - preview_layout = QVBoxLayout() - - self.preview_checkbox = QPushButton("启用实时预览") - self.preview_checkbox.setCheckable(True) - self.preview_checkbox.setChecked(True) - self.preview_checkbox.clicked.connect(self._on_preview_toggled) - - preview_layout.addWidget(self.preview_checkbox) - preview_group.setLayout(preview_layout) - - layout.addWidget(preview_group) - - tools_tab.setLayout(layout) - return tools_tab - - def _create_properties_tab(self): - """ - 创建属性标签页 - """ - properties_tab = QWidget() - layout = QVBoxLayout() - - # 地形尺寸组 - size_group = QGroupBox("地形尺寸") - size_layout = QGridLayout() - - size_layout.addWidget(QLabel("宽度:"), 0, 0) - self.width_input = self._create_double_input("100.0") - size_layout.addWidget(self.width_input, 0, 1) - - size_layout.addWidget(QLabel("长度:"), 1, 0) - self.length_input = self._create_double_input("100.0") - size_layout.addWidget(self.length_input, 1, 1) - - size_layout.addWidget(QLabel("高度:"), 2, 0) - self.height_input = self._create_double_input("50.0") - size_layout.addWidget(self.height_input, 2, 1) - - size_group.setLayout(size_layout) - - layout.addWidget(size_group) - - # 分辨率组 - resolution_group = QGroupBox("分辨率") - resolution_layout = QGridLayout() - - resolution_layout.addWidget(QLabel("X分辨率:"), 0, 0) - self.x_resolution_input = self._create_double_input("129.0") - resolution_layout.addWidget(self.x_resolution_input, 0, 1) - - resolution_layout.addWidget(QLabel("Y分辨率:"), 1, 0) - self.y_resolution_input = self._create_double_input("129.0") - resolution_layout.addWidget(self.y_resolution_input, 1, 1) - - resolution_group.setLayout(resolution_layout) - - layout.addWidget(resolution_group) - - # LOD设置组 - lod_group = QGroupBox("LOD设置") - lod_layout = QVBoxLayout() - - self.lod_enabled_checkbox = QPushButton("启用LOD") - self.lod_enabled_checkbox.setCheckable(True) - self.lod_enabled_checkbox.setChecked(True) - - lod_layout.addWidget(self.lod_enabled_checkbox) - - # LOD阈值滑块 - lod_layout.addWidget(QLabel("LOD阈值:")) - self.lod_threshold_slider = QSlider(Qt.Horizontal) - self.lod_threshold_slider.setRange(1, 100) - self.lod_threshold_slider.setValue(50) - lod_layout.addWidget(self.lod_threshold_slider) - - lod_group.setLayout(lod_layout) - - layout.addWidget(lod_group) - - properties_tab.setLayout(layout) - return properties_tab - - def _create_material_tab(self): - """ - 创建材质标签页 - """ - material_tab = QWidget() - layout = QVBoxLayout() - - # 材质类型组 - type_group = QGroupBox("材质类型") - type_layout = QVBoxLayout() - - self.material_type_selector = QComboBox() - self.material_type_selector.addItems([ - "单纹理", - "多纹理混合", - "程序化生成" - ]) - self.material_type_selector.currentTextChanged.connect(self._on_material_type_changed) - - type_layout.addWidget(self.material_type_selector) - type_group.setLayout(type_layout) - - layout.addWidget(type_group) - - # 纹理路径组 - texture_group = QGroupBox("纹理设置") - texture_layout = QVBoxLayout() - - texture_layout.addWidget(QLabel("基础纹理:")) - self.base_texture_input = self._create_file_input() - texture_layout.addWidget(self.base_texture_input) - - # 添加纹理按钮 - add_texture_btn = QPushButton("添加纹理") - add_texture_btn.clicked.connect(self._on_add_texture) - texture_layout.addWidget(add_texture_btn) - - texture_group.setLayout(texture_layout) - - layout.addWidget(texture_group) - - # 材质属性组 - material_props_group = QGroupBox("材质属性") - material_props_layout = QGridLayout() - - material_props_layout.addWidget(QLabel("环境光:"), 0, 0) - self.ambient_input = self._create_double_input("0.5") - material_props_layout.addWidget(self.ambient_input, 0, 1) - - material_props_layout.addWidget(QLabel("漫反射:"), 1, 0) - self.diffuse_input = self._create_double_input("0.8") - material_props_layout.addWidget(self.diffuse_input, 1, 1) - - material_props_layout.addWidget(QLabel("镜面反射:"), 2, 0) - self.specular_input = self._create_double_input("0.2") - material_props_layout.addWidget(self.specular_input, 2, 1) - - material_props_layout.addWidget(QLabel("光泽度:"), 3, 0) - self.shininess_input = self._create_double_input("5.0") - material_props_layout.addWidget(self.shininess_input, 3, 1) - - material_props_group.setLayout(material_props_layout) - - layout.addWidget(material_props_group) - - material_tab.setLayout(layout) - return material_tab - - def _create_double_input(self, default_value): - """ - 创建双精度输入框 - """ - from PyQt5.QtWidgets import QLineEdit - input_field = QLineEdit(default_value) - validator = QDoubleValidator() - input_field.setValidator(validator) - return input_field - - def _create_file_input(self): - """ - 创建文件路径输入框 - """ - from PyQt5.QtWidgets import QLineEdit, QHBoxLayout, QPushButton, QFileDialog - widget = QWidget() - layout = QHBoxLayout() - - input_field = QLineEdit() - browse_btn = QPushButton("浏览") - - def browse_file(): - file_path, _ = QFileDialog.getOpenFileName( - None, "选择纹理文件", "", "纹理文件 (*.png *.jpg *.jpeg *.bmp *.tga)" - ) - if file_path: - input_field.setText(file_path) - - browse_btn.clicked.connect(browse_file) - - layout.addWidget(input_field) - layout.addWidget(browse_btn) - widget.setLayout(layout) - - return widget - - def _on_tool_changed(self, text): - """ - 工具选择变化事件处理 - """ - print(f"工具选择变化: {text}") - - def _on_radius_changed(self, value): - """ - 半径变化事件处理 - """ - self.radius_label.setText(str(value)) - - def _on_strength_changed(self, value): - """ - 强度变化事件处理 - """ - self.strength_label.setText(str(value)) - - def _on_operation_changed(self, text): - """ - 操作类型变化事件处理 - """ - print(f"操作类型变化: {text}") - - def _on_preview_toggled(self, checked): - """ - 预览开关事件处理 - """ - print(f"预览开关: {checked}") - - def _on_material_type_changed(self, text): - """ - 材质类型变化事件处理 - """ - print(f"材质类型变化: {text}") - - def _on_add_texture(self): - """ - 添加纹理事件处理 - """ - print("添加纹理") - - def _apply_changes(self): - """ - 应用更改 - """ - print("应用更改") - - def _cancel_changes(self): - """ - 取消更改 - """ - print("取消更改") - - def toggle_visibility(self): - """ - 切换界面可见性 - """ - if self.main_widget: - if self.main_widget.isVisible(): - self.main_widget.hide() - else: - self.main_widget.show() - - def cleanup(self): - """ - 清理UI资源 - """ - if self.main_widget: - self.main_widget.close() +""" +地形编辑器UI界面 +""" + +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, + QPushButton, QSlider, QLabel, QComboBox, QGroupBox, + QTabWidget, QScrollArea, QFrame) +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QDoubleValidator + +class TerrainEditorUI: + """ + 地形编辑器UI界面类 + """ + + def __init__(self, world): + self.world = world + self.main_widget = None + self.terrain_tools_panel = None + self.terrain_properties_panel = None + self.material_panel = None + + def create_interface(self): + """ + 创建地形编辑器UI界面 + """ + print("创建地形编辑器UI界面") + + # 创建主窗口部件 + self.main_widget = QWidget() + self.main_widget.setWindowTitle("地形编辑器") + self.main_widget.setGeometry(100, 100, 400, 600) + + # 创建主布局 + main_layout = QVBoxLayout() + + # 创建标签页 + tab_widget = QTabWidget() + + # 工具标签页 + tools_tab = self._create_tools_tab() + tab_widget.addTab(tools_tab, "工具") + + # 属性标签页 + properties_tab = self._create_properties_tab() + tab_widget.addTab(properties_tab, "属性") + + # 材质标签页 + material_tab = self._create_material_tab() + tab_widget.addTab(material_tab, "材质") + + # 添加标签页到主布局 + main_layout.addWidget(tab_widget) + + # 添加应用和取消按钮 + button_layout = QHBoxLayout() + apply_btn = QPushButton("应用") + cancel_btn = QPushButton("取消") + apply_btn.clicked.connect(self._apply_changes) + cancel_btn.clicked.connect(self._cancel_changes) + + button_layout.addWidget(apply_btn) + button_layout.addWidget(cancel_btn) + + main_layout.addLayout(button_layout) + + self.main_widget.setLayout(main_layout) + + # 隐藏界面,等待用户打开 + self.main_widget.hide() + + def _create_tools_tab(self): + """ + 创建工具标签页 + """ + tools_tab = QWidget() + layout = QVBoxLayout() + + # 创建工具选择组 + tools_group = QGroupBox("编辑工具") + tools_layout = QVBoxLayout() + + # 工具选择下拉框 + self.tool_selector = QComboBox() + self.tool_selector.addItems([ + "抬高工具", + "降低工具", + "平坦化工具", + "平滑工具", + "噪声工具" + ]) + self.tool_selector.currentTextChanged.connect(self._on_tool_changed) + + tools_layout.addWidget(QLabel("选择工具:")) + tools_layout.addWidget(self.tool_selector) + + # 工具参数组 + params_group = QGroupBox("工具参数") + params_layout = QGridLayout() + + # 半径参数 + params_layout.addWidget(QLabel("半径:"), 0, 0) + self.radius_slider = QSlider(Qt.Horizontal) + self.radius_slider.setRange(1, 100) + self.radius_slider.setValue(10) + self.radius_slider.valueChanged.connect(self._on_radius_changed) + params_layout.addWidget(self.radius_slider, 0, 1) + + self.radius_label = QLabel("10") + params_layout.addWidget(self.radius_label, 0, 2) + + # 强度参数 + params_layout.addWidget(QLabel("强度:"), 1, 0) + self.strength_slider = QSlider(Qt.Horizontal) + self.strength_slider.setRange(1, 100) + self.strength_slider.setValue(50) + self.strength_slider.valueChanged.connect(self._on_strength_changed) + params_layout.addWidget(self.strength_slider, 1, 1) + + self.strength_label = QLabel("50") + params_layout.addWidget(self.strength_label, 1, 2) + + # 操作类型 + params_layout.addWidget(QLabel("操作:"), 2, 0) + self.operation_selector = QComboBox() + self.operation_selector.addItems(["添加", "减去", "设置"]) + self.operation_selector.currentTextChanged.connect(self._on_operation_changed) + params_layout.addWidget(self.operation_selector, 2, 1, 1, 2) + + params_group.setLayout(params_layout) + + tools_layout.addWidget(params_group) + tools_group.setLayout(tools_layout) + + layout.addWidget(tools_group) + + # 预览设置组 + preview_group = QGroupBox("预览设置") + preview_layout = QVBoxLayout() + + self.preview_checkbox = QPushButton("启用实时预览") + self.preview_checkbox.setCheckable(True) + self.preview_checkbox.setChecked(True) + self.preview_checkbox.clicked.connect(self._on_preview_toggled) + + preview_layout.addWidget(self.preview_checkbox) + preview_group.setLayout(preview_layout) + + layout.addWidget(preview_group) + + tools_tab.setLayout(layout) + return tools_tab + + def _create_properties_tab(self): + """ + 创建属性标签页 + """ + properties_tab = QWidget() + layout = QVBoxLayout() + + # 地形尺寸组 + size_group = QGroupBox("地形尺寸") + size_layout = QGridLayout() + + size_layout.addWidget(QLabel("宽度:"), 0, 0) + self.width_input = self._create_double_input("100.0") + size_layout.addWidget(self.width_input, 0, 1) + + size_layout.addWidget(QLabel("长度:"), 1, 0) + self.length_input = self._create_double_input("100.0") + size_layout.addWidget(self.length_input, 1, 1) + + size_layout.addWidget(QLabel("高度:"), 2, 0) + self.height_input = self._create_double_input("50.0") + size_layout.addWidget(self.height_input, 2, 1) + + size_group.setLayout(size_layout) + + layout.addWidget(size_group) + + # 分辨率组 + resolution_group = QGroupBox("分辨率") + resolution_layout = QGridLayout() + + resolution_layout.addWidget(QLabel("X分辨率:"), 0, 0) + self.x_resolution_input = self._create_double_input("129.0") + resolution_layout.addWidget(self.x_resolution_input, 0, 1) + + resolution_layout.addWidget(QLabel("Y分辨率:"), 1, 0) + self.y_resolution_input = self._create_double_input("129.0") + resolution_layout.addWidget(self.y_resolution_input, 1, 1) + + resolution_group.setLayout(resolution_layout) + + layout.addWidget(resolution_group) + + # LOD设置组 + lod_group = QGroupBox("LOD设置") + lod_layout = QVBoxLayout() + + self.lod_enabled_checkbox = QPushButton("启用LOD") + self.lod_enabled_checkbox.setCheckable(True) + self.lod_enabled_checkbox.setChecked(True) + + lod_layout.addWidget(self.lod_enabled_checkbox) + + # LOD阈值滑块 + lod_layout.addWidget(QLabel("LOD阈值:")) + self.lod_threshold_slider = QSlider(Qt.Horizontal) + self.lod_threshold_slider.setRange(1, 100) + self.lod_threshold_slider.setValue(50) + lod_layout.addWidget(self.lod_threshold_slider) + + lod_group.setLayout(lod_layout) + + layout.addWidget(lod_group) + + properties_tab.setLayout(layout) + return properties_tab + + def _create_material_tab(self): + """ + 创建材质标签页 + """ + material_tab = QWidget() + layout = QVBoxLayout() + + # 材质类型组 + type_group = QGroupBox("材质类型") + type_layout = QVBoxLayout() + + self.material_type_selector = QComboBox() + self.material_type_selector.addItems([ + "单纹理", + "多纹理混合", + "程序化生成" + ]) + self.material_type_selector.currentTextChanged.connect(self._on_material_type_changed) + + type_layout.addWidget(self.material_type_selector) + type_group.setLayout(type_layout) + + layout.addWidget(type_group) + + # 纹理路径组 + texture_group = QGroupBox("纹理设置") + texture_layout = QVBoxLayout() + + texture_layout.addWidget(QLabel("基础纹理:")) + self.base_texture_input = self._create_file_input() + texture_layout.addWidget(self.base_texture_input) + + # 添加纹理按钮 + add_texture_btn = QPushButton("添加纹理") + add_texture_btn.clicked.connect(self._on_add_texture) + texture_layout.addWidget(add_texture_btn) + + texture_group.setLayout(texture_layout) + + layout.addWidget(texture_group) + + # 材质属性组 + material_props_group = QGroupBox("材质属性") + material_props_layout = QGridLayout() + + material_props_layout.addWidget(QLabel("环境光:"), 0, 0) + self.ambient_input = self._create_double_input("0.5") + material_props_layout.addWidget(self.ambient_input, 0, 1) + + material_props_layout.addWidget(QLabel("漫反射:"), 1, 0) + self.diffuse_input = self._create_double_input("0.8") + material_props_layout.addWidget(self.diffuse_input, 1, 1) + + material_props_layout.addWidget(QLabel("镜面反射:"), 2, 0) + self.specular_input = self._create_double_input("0.2") + material_props_layout.addWidget(self.specular_input, 2, 1) + + material_props_layout.addWidget(QLabel("光泽度:"), 3, 0) + self.shininess_input = self._create_double_input("5.0") + material_props_layout.addWidget(self.shininess_input, 3, 1) + + material_props_group.setLayout(material_props_layout) + + layout.addWidget(material_props_group) + + material_tab.setLayout(layout) + return material_tab + + def _create_double_input(self, default_value): + """ + 创建双精度输入框 + """ + from PyQt5.QtWidgets import QLineEdit + input_field = QLineEdit(default_value) + validator = QDoubleValidator() + input_field.setValidator(validator) + return input_field + + def _create_file_input(self): + """ + 创建文件路径输入框 + """ + from PyQt5.QtWidgets import QLineEdit, QHBoxLayout, QPushButton, QFileDialog + widget = QWidget() + layout = QHBoxLayout() + + input_field = QLineEdit() + browse_btn = QPushButton("浏览") + + def browse_file(): + file_path, _ = QFileDialog.getOpenFileName( + None, "选择纹理文件", "", "纹理文件 (*.png *.jpg *.jpeg *.bmp *.tga)" + ) + if file_path: + input_field.setText(file_path) + + browse_btn.clicked.connect(browse_file) + + layout.addWidget(input_field) + layout.addWidget(browse_btn) + widget.setLayout(layout) + + return widget + + def _on_tool_changed(self, text): + """ + 工具选择变化事件处理 + """ + print(f"工具选择变化: {text}") + + def _on_radius_changed(self, value): + """ + 半径变化事件处理 + """ + self.radius_label.setText(str(value)) + + def _on_strength_changed(self, value): + """ + 强度变化事件处理 + """ + self.strength_label.setText(str(value)) + + def _on_operation_changed(self, text): + """ + 操作类型变化事件处理 + """ + print(f"操作类型变化: {text}") + + def _on_preview_toggled(self, checked): + """ + 预览开关事件处理 + """ + print(f"预览开关: {checked}") + + def _on_material_type_changed(self, text): + """ + 材质类型变化事件处理 + """ + print(f"材质类型变化: {text}") + + def _on_add_texture(self): + """ + 添加纹理事件处理 + """ + print("添加纹理") + + def _apply_changes(self): + """ + 应用更改 + """ + print("应用更改") + + def _cancel_changes(self): + """ + 取消更改 + """ + print("取消更改") + + def toggle_visibility(self): + """ + 切换界面可见性 + """ + if self.main_widget: + if self.main_widget.isVisible(): + self.main_widget.hide() + else: + self.main_widget.show() + + def cleanup(self): + """ + 清理UI资源 + """ + if self.main_widget: + self.main_widget.close() self.main_widget = None \ No newline at end of file diff --git a/plugins/user/terrain_editor/vegetation/__init__.py b/plugins/user/terrain_editor/vegetation/__init__.py index f8a4d468..d7da10b0 100644 --- a/plugins/user/terrain_editor/vegetation/__init__.py +++ b/plugins/user/terrain_editor/vegetation/__init__.py @@ -1,3 +1,3 @@ -""" -地形植被系统初始化文件 +""" +地形植被系统初始化文件 """ \ No newline at end of file diff --git a/plugins/user/terrain_editor/vegetation/vegetation_system.py b/plugins/user/terrain_editor/vegetation/vegetation_system.py index 63f0d370..2ae22d88 100644 --- a/plugins/user/terrain_editor/vegetation/vegetation_system.py +++ b/plugins/user/terrain_editor/vegetation/vegetation_system.py @@ -1,1152 +1,1152 @@ -""" -地形植被系统 -提供完整的植被分布、生长模拟、LOD控制、生态模拟等功能 -""" - -from panda3d.core import NodePath, Vec3, Point3, BitMask32 -from panda3d.core import Texture, TextureStage, Material -import random -import math -import json -import os -import numpy as np - -class VegetationSystem: - """ - 地形植被系统类 - 提供完整的植被分布、生长模拟、LOD控制、生态模拟等功能 - """ - - def __init__(self, world): - self.world = world - self.vegetation_instances = [] # 存储植被实例 - self.vegetation_types = {} # 存储植被类型定义 - self.density_maps = {} # 存储密度图 - self.lod_settings = {} # 存储LOD设置 - self.ecosystem = {} # 存储生态系统信息 - self.growth_simulator = None # 生长模拟器 - self.wind_effect = { # 风力效果参数 - 'strength': 0.5, - 'direction': Vec3(1, 0, 0), - 'speed': 1.0, - 'turbulence': 0.2 - } - - def define_vegetation_type(self, name, model_path, scale_range=(1.0, 1.0), - rotation_range=(0, 360), color_variation=(0.8, 1.2), - growth_params=None, ecosystem_role='general'): - """ - 定义植被类型 - growth_params: 生长参数字典 - ecosystem_role: 生态角色 ('tree', 'shrub', 'grass', 'flower', 'general') - """ - self.vegetation_types[name] = { - 'model_path': model_path, - 'scale_range': scale_range, - 'rotation_range': rotation_range, - 'color_variation': color_variation, - 'instances': [], - 'growth_params': growth_params or { - 'max_height': 10.0, - 'growth_rate': 0.1, - 'lifespan': 100.0, - 'seed_production': 0.5, - 'water_requirement': 0.5, - 'sunlight_requirement': 0.7 - }, - 'ecosystem_role': ecosystem_role, - 'seasonal_variations': { - 'spring': {'color_shift': (0.1, 0.2, 0.0), 'scale_factor': 1.1}, - 'summer': {'color_shift': (0.0, 0.0, 0.0), 'scale_factor': 1.0}, - 'autumn': {'color_shift': (0.3, 0.2, 0.0), 'scale_factor': 0.9}, - 'winter': {'color_shift': (0.1, 0.1, 0.1), 'scale_factor': 0.8} - } - } - print(f"定义植被类型: {name} (生态角色: {ecosystem_role})") - - def load_vegetation_models(self): - """ - 预加载所有植被模型 - """ - for name, veg_type in self.vegetation_types.items(): - try: - model = self.world.loader.loadModel(veg_type['model_path']) - if model: - veg_type['model'] = model - print(f"加载植被模型: {name}") - else: - print(f"无法加载植被模型: {name}") - except Exception as e: - print(f"加载植被模型 {name} 时出错: {e}") - - def generate_vegetation(self, terrain_info, vegetation_config, density=0.1, - distribution_method='random'): - """ - 在地形上生成植被 - vegetation_config: 植被配置字典,指定不同类型植被的分布参数 - distribution_method: 分布方法 ('random', 'clustered', 'ecosystem') - """ - try: - terrain_node = terrain_info['node'] - heightfield = terrain_info['heightfield'] - - if not heightfield: - print("无法获取地形高度图数据") - return False - - width = heightfield.getXSize() - height = heightfield.getYSize() - - # 获取地形节点信息 - terrain_pos = terrain_node.getPos() - terrain_scale = terrain_node.getScale() - - # 根据分布方法选择生成策略 - if distribution_method == 'ecosystem': - return self._generate_ecosystem_vegetation(terrain_info, vegetation_config, density) - elif distribution_method == 'clustered': - return self._generate_clustered_vegetation(terrain_info, vegetation_config, density) - else: - return self._generate_random_vegetation(terrain_info, vegetation_config, density) - - except Exception as e: - print(f"生成植被时出错: {e}") - return False - - def _generate_random_vegetation(self, terrain_info, vegetation_config, density): - """ - 随机生成植被 - """ - try: - terrain_node = terrain_info['node'] - heightfield = terrain_info['heightfield'] - width = heightfield.getXSize() - height = heightfield.getYSize() - terrain_pos = terrain_node.getPos() - terrain_scale = terrain_node.getScale() - - # 为每种植被类型生成实例 - total_instances = 0 - for veg_type_name, veg_params in vegetation_config.items(): - if veg_type_name not in self.vegetation_types: - print(f"未定义的植被类型: {veg_type_name}") - continue - - veg_type = self.vegetation_types[veg_type_name] - - # 获取环境限制条件 - min_height = veg_params.get('min_height', 0.0) - max_height = veg_params.get('max_height', 1.0) - slope_threshold = veg_params.get('slope_threshold', 45.0) - moisture_preference = veg_params.get('moisture_preference', 0.5) - - # 计算植被数量 - total_points = int(width * height * density * veg_params.get('density_factor', 1.0)) - - # 生成植被实例 - instances_created = 0 - attempts = 0 - max_attempts = total_points * 10 # 防止无限循环 - - while instances_created < total_points and attempts < max_attempts: - attempts += 1 - - # 随机选择位置 - x = random.randint(0, width - 1) - y = random.randint(0, height - 1) - - # 获取高度值 - height_value = heightfield.getRed(x, y) - - # 检查高度限制 - if height_value < min_height or height_value > max_height: - continue - - # 检查坡度 - if self._calculate_slope(heightfield, x, y) > slope_threshold: - continue - - # 检查湿度条件(简化模拟) - moisture = self._calculate_moisture(heightfield, x, y) - if abs(moisture - moisture_preference) > 0.3: - continue - - # 计算世界坐标 - center_offset = (width - 1) / 2 - world_x = (x - center_offset) * terrain_scale.getX() + terrain_pos.getX() - world_y = (y - center_offset) * terrain_scale.getY() + terrain_pos.getY() - world_z = height_value * terrain_scale.getZ() + terrain_pos.getZ() - - # 创建植被实例 - if self._create_vegetation_instance(veg_type_name, Point3(world_x, world_y, world_z)): - instances_created += 1 - - total_instances += instances_created - print(f"植被类型 {veg_type_name} 生成了 {instances_created} 个实例") - - print(f"随机生成植被完成,共创建 {total_instances} 个实例") - return True - - except Exception as e: - print(f"随机生成植被时出错: {e}") - return False - - def _generate_clustered_vegetation(self, terrain_info, vegetation_config, density): - """ - 簇状生成植被 - """ - try: - terrain_node = terrain_info['node'] - heightfield = terrain_info['heightfield'] - width = heightfield.getXSize() - height = heightfield.getYSize() - terrain_pos = terrain_node.getPos() - terrain_scale = terrain_node.getScale() - - # 为每种植被类型生成簇 - total_instances = 0 - for veg_type_name, veg_params in vegetation_config.items(): - if veg_type_name not in self.vegetation_types: - continue - - # 计算簇的数量 - cluster_count = int((width * height * density * veg_params.get('density_factor', 1.0)) / 10) - - for _ in range(cluster_count): - # 随机选择簇中心 - center_x = random.randint(0, width - 1) - center_y = random.randint(0, height - 1) - center_height = heightfield.getRed(center_x, center_y) - - # 确定簇大小 - cluster_size = random.randint(3, 10) - - # 在簇内生成植被 - for _ in range(cluster_size): - # 在簇中心附近随机分布 - offset_x = random.randint(-5, 5) - offset_y = random.randint(-5, 5) - x = max(0, min(width - 1, center_x + offset_x)) - y = max(0, min(height - 1, center_y + offset_y)) - - # 计算世界坐标 - center_offset = (width - 1) / 2 - world_x = (x - center_offset) * terrain_scale.getX() + terrain_pos.getX() - world_y = (y - center_offset) * terrain_scale.getY() + terrain_pos.getY() - world_z = heightfield.getRed(x, y) * terrain_scale.getZ() + terrain_pos.getZ() - - # 创建植被实例 - if self._create_vegetation_instance(veg_type_name, Point3(world_x, world_y, world_z)): - total_instances += 1 - - print(f"簇状生成植被完成,共创建 {total_instances} 个实例") - return True - - except Exception as e: - print(f"簇状生成植被时出错: {e}") - return False - - def _generate_ecosystem_vegetation(self, terrain_info, vegetation_config, density): - """ - 生态系统方式生成植被 - """ - try: - terrain_node = terrain_info['node'] - heightfield = terrain_info['heightfield'] - width = heightfield.getXSize() - height = heightfield.getYSize() - terrain_pos = terrain_node.getPos() - terrain_scale = terrain_node.getScale() - - # 创建生态位地图 - niche_map = self._create_niche_map(heightfield) - - # 为每种植被类型根据生态位生成 - total_instances = 0 - for veg_type_name, veg_params in vegetation_config.items(): - if veg_type_name not in self.vegetation_types: - continue - - veg_type = self.vegetation_types[veg_type_name] - ecosystem_role = veg_type['ecosystem_role'] - - # 根据生态角色确定分布偏好 - if ecosystem_role == 'tree': - preferred_niches = ['high_ground', 'flat_area'] - elif ecosystem_role == 'shrub': - preferred_niches = ['slope', 'transition'] - elif ecosystem_role == 'grass': - preferred_niches = ['flat_area', 'low_ground'] - else: - preferred_niches = ['any'] - - # 计算植被数量 - total_points = int(width * height * density * veg_params.get('density_factor', 1.0)) - - # 根据生态位偏好生成植被 - instances_created = 0 - attempts = 0 - max_attempts = total_points * 20 - - while instances_created < total_points and attempts < max_attempts: - attempts += 1 - - # 根据生态位偏好选择位置 - x, y = self._select_ecosystem_position(niche_map, preferred_niches) - if x is None or y is None: - continue - - # 检查环境条件 - height_value = heightfield.getRed(x, y) - slope = self._calculate_slope(heightfield, x, y) - - # 根据植被类型检查条件 - if not self._check_environment_conditions(veg_type_name, height_value, slope): - continue - - # 计算世界坐标 - center_offset = (width - 1) / 2 - world_x = (x - center_offset) * terrain_scale.getX() + terrain_pos.getX() - world_y = (y - center_offset) * terrain_scale.getY() + terrain_pos.getY() - world_z = height_value * terrain_scale.getZ() + terrain_pos.getZ() - - # 创建植被实例 - if self._create_vegetation_instance(veg_type_name, Point3(world_x, world_y, world_z)): - instances_created += 1 - - total_instances += instances_created - print(f"生态系统生成植被类型 {veg_type_name}: {instances_created} 个实例") - - print(f"生态系统生成植被完成,共创建 {total_instances} 个实例") - return True - - except Exception as e: - print(f"生态系统生成植被时出错: {e}") - return False - - def _create_niche_map(self, heightfield): - """ - 创建生态位地图 - """ - try: - width = heightfield.getXSize() - height = heightfield.getYSize() - niche_map = {} - - for x in range(width): - for y in range(height): - # 分析位置特征 - slope = self._calculate_slope(heightfield, x, y) - height_value = heightfield.getRed(x, y) - - # 确定生态位 - if slope > 30: - niche = 'steep_slope' - elif slope > 15: - niche = 'slope' - elif height_value > 0.7: - niche = 'high_ground' - elif height_value < 0.3: - niche = 'low_ground' - else: - niche = 'flat_area' - - # 检查是否为过渡区域 - if self._is_transition_zone(heightfield, x, y): - niche = 'transition' - - niche_map[(x, y)] = niche - - return niche_map - - except Exception as e: - print(f"创建生态位地图时出错: {e}") - return {} - - def _is_transition_zone(self, heightfield, x, y): - """ - 检查是否为过渡区域 - """ - try: - width = heightfield.getXSize() - height = heightfield.getYSize() - - if x < 2 or x >= width - 2 or y < 2 or y >= height - 2: - return False - - # 计算周围点的高度差异 - center_height = heightfield.getRed(x, y) - height_diff = 0 - - for dx in [-1, 0, 1]: - for dy in [-1, 0, 1]: - if dx == 0 and dy == 0: - continue - neighbor_height = heightfield.getRed(x + dx, y + dy) - height_diff += abs(neighbor_height - center_height) - - # 如果高度差异较大,则为过渡区域 - return height_diff > 0.1 - - except: - return False - - def _select_ecosystem_position(self, niche_map, preferred_niches): - """ - 根据生态位偏好选择位置 - """ - try: - if not niche_map: - return None, None - - # 如果偏好'any',随机选择 - if 'any' in preferred_niches: - positions = list(niche_map.keys()) - if positions: - return random.choice(positions) - return None, None - - # 根据偏好选择 - preferred_positions = [] - for pos, niche in niche_map.items(): - if niche in preferred_niches: - preferred_positions.append(pos) - - if preferred_positions: - return random.choice(preferred_positions) - - # 如果没有偏好位置,随机选择 - positions = list(niche_map.keys()) - if positions: - return random.choice(positions) - return None, None - - except: - return None, None - - def _check_environment_conditions(self, veg_type_name, height_value, slope): - """ - 检查环境条件是否适合植被生长 - """ - try: - if veg_type_name not in self.vegetation_types: - return True - - veg_type = self.vegetation_types[veg_type_name] - growth_params = veg_type['growth_params'] - - # 检查高度限制 - max_height = growth_params.get('max_height', 100.0) - if height_value * 100 > max_height: # 假设高度值0-1对应0-100米 - return False - - # 检查坡度限制 - max_slope = growth_params.get('max_slope', 45.0) - if slope > max_slope: - return False - - return True - - except: - return True - - def _calculate_slope(self, heightfield, x, y): - """ - 计算指定点的坡度 - """ - try: - width = heightfield.getXSize() - height = heightfield.getYSize() - - # 获取周围点的高度 - heights = [] - for dx in [-1, 0, 1]: - for dy in [-1, 0, 1]: - nx, ny = x + dx, y + dy - if 0 <= nx < width and 0 <= ny < height: - heights.append(heightfield.getRed(nx, ny)) - else: - heights.append(heightfield.getRed(x, y)) - - # 计算X和Y方向的梯度 - dx = heights[2] - heights[0] # 右侧点 - 左侧点 - dy = heights[6] - heights[0] # 下方点 - 上方点 - - # 计算坡度(角度) - slope = math.degrees(math.atan(math.sqrt(dx*dx + dy*dy))) - return slope - - except: - return 0.0 - - def _calculate_moisture(self, heightfield, x, y): - """ - 计算指定点的湿度(简化模拟) - """ - try: - # 简化的湿度计算:低洼地区湿度较高 - width = heightfield.getXSize() - height = heightfield.getYSize() - - # 计算周围点的平均高度 - total_height = 0 - count = 0 - for dx in [-2, -1, 0, 1, 2]: - for dy in [-2, -1, 0, 1, 2]: - nx, ny = x + dx, y + dy - if 0 <= nx < width and 0 <= ny < height: - total_height += heightfield.getRed(nx, ny) - count += 1 - - if count > 0: - avg_height = total_height / count - current_height = heightfield.getRed(x, y) - # 低洼地区湿度较高 - moisture = max(0.0, min(1.0, 0.5 + (avg_height - current_height) * 2)) - return moisture - - return 0.5 - - except: - return 0.5 - - def _create_vegetation_instance(self, veg_type_name, position): - """ - 创建植被实例 - """ - try: - if veg_type_name not in self.vegetation_types: - return None - - veg_type = self.vegetation_types[veg_type_name] - - # 检查模型是否已加载 - if 'model' not in veg_type: - # 尝试加载模型 - try: - model = self.world.loader.loadModel(veg_type['model_path']) - if model: - veg_type['model'] = model - else: - print(f"无法加载植被模型: {veg_type['model_path']}") - return None - except Exception as e: - print(f"加载植被模型时出错: {e}") - return None - - # 克隆模型创建实例 - instance = veg_type['model'].copyTo(self.world.render) - instance.setPos(position) - - # 应用随机缩放 - scale_min, scale_max = veg_type['scale_range'] - scale = random.uniform(scale_min, scale_max) - instance.setScale(scale) - - # 应用随机旋转 - rot_min, rot_max = veg_type['rotation_range'] - rotation = random.uniform(rot_min, rot_max) - instance.setH(rotation) - - # 应用颜色变化 - color_min, color_max = veg_type['color_variation'] - color_factor = random.uniform(color_min, color_max) - # 应用季节性颜色变化 - season_color = self._get_seasonal_color(veg_type_name) - if season_color: - # 这里可以应用颜色变化到模型材质 - pass - - # 设置标签 - instance.setTag("vegetation_type", veg_type_name) - instance.setTag("is_scene_element", "1") - instance.setTag("tree_item_type", "VEGETATION_NODE") - instance.setTag("creation_time", str(self.world.globalClock.getFrameTime())) - - # 添加碰撞体 - instance.setCollideMask(BitMask32.bit(3)) # 使用第3位作为植被碰撞掩码 - - # 初始化生长状态 - creation_time = self.world.globalClock.getFrameTime() - - # 保存实例信息 - instance_info = { - 'node': instance, - 'type': veg_type_name, - 'position': position, - 'scale': scale, - 'rotation': rotation, - 'creation_time': creation_time, - 'age': 0.0, - 'health': 1.0, - 'growth_stage': 0, # 0=seedling, 1=sapling, 2=mature, 3=elderly - 'last_update': creation_time, - 'wind_offset': Vec3(0, 0, 0) - } - - self.vegetation_instances.append(instance_info) - veg_type['instances'].append(instance_info) - - return instance_info - - except Exception as e: - print(f"创建植被实例时出错: {e}") - return None - - def _get_seasonal_color(self, veg_type_name): - """ - 获取季节性颜色变化 - """ - try: - if veg_type_name in self.vegetation_types: - veg_type = self.vegetation_types[veg_type_name] - # 这里可以根据当前季节返回相应的颜色变化 - # 简化实现,返回默认值 - return veg_type['seasonal_variations'].get('summer', {'color_shift': (0, 0, 0)}) - return None - except: - return None - - def remove_vegetation_instance(self, instance_info): - """ - 移除植被实例 - """ - try: - if instance_info in self.vegetation_instances: - # 从全局列表中移除 - self.vegetation_instances.remove(instance_info) - - # 从类型列表中移除 - veg_type_name = instance_info['type'] - if veg_type_name in self.vegetation_types: - if instance_info in self.vegetation_types[veg_type_name]['instances']: - self.vegetation_types[veg_type_name]['instances'].remove(instance_info) - - # 移除节点 - if instance_info['node'] and not instance_info['node'].isEmpty(): - instance_info['node'].removeNode() - - print("移除植被实例") - return True - - except Exception as e: - print(f"移除植被实例时出错: {e}") - return False - - def clear_all_vegetation(self): - """ - 清除所有植被 - """ - try: - # 移除所有实例节点 - for instance_info in self.vegetation_instances: - if instance_info['node'] and not instance_info['node'].isEmpty(): - instance_info['node'].removeNode() - - # 清空数据结构 - self.vegetation_instances = [] - - # 清空各类型实例列表 - for veg_type in self.vegetation_types.values(): - veg_type['instances'] = [] - - print("清除所有植被") - return True - - except Exception as e: - print(f"清除所有植被时出错: {e}") - return False - - def set_vegetation_lod(self, lod_settings): - """ - 设置植被LOD - lod_settings: LOD设置字典,包含距离阈值和简化策略 - """ - self.lod_settings = lod_settings - print("设置植被LOD") - - def update_vegetation_lod(self, camera_pos): - """ - 更新植被LOD - """ - try: - for instance_info in self.vegetation_instances: - node = instance_info['node'] - if node and not node.isEmpty(): - # 计算到相机的距离 - distance = (node.getPos() - camera_pos).length() - - # 根据距离应用LOD策略 - if distance > self.lod_settings.get('far_distance', 100): - # 远距离时隐藏 - node.hide() - elif distance > self.lod_settings.get('medium_distance', 50): - # 中距离时显示简化版本 - node.show() - self._apply_medium_lod(node) - else: - # 近距离时显示完整版本 - node.show() - self._apply_full_lod(node) - - except Exception as e: - print(f"更新植被LOD时出错: {e}") - - def _apply_medium_lod(self, node): - """ - 应用中距离LOD - """ - try: - # 减少多边形数量的简化版本 - # 这里可以实现模型简化或使用公告板 - # 简化实现:使用缩放来模拟简化效果 - original_scale = node.getScale() - node.setScale(original_scale * 0.8) - except: - pass - - def _apply_full_lod(self, node): - """ - 应用完整LOD - """ - # 恢复完整细节 - pass - - def simulate_growth(self, time_step=1.0): - """ - 模拟植被生长 - """ - try: - current_time = self.world.globalClock.getFrameTime() - - for instance_info in self.vegetation_instances: - node = instance_info['node'] - if node and not node.isEmpty(): - # 获取植被类型信息 - veg_type_name = instance_info['type'] - if veg_type_name not in self.vegetation_types: - continue - - veg_type = self.vegetation_types[veg_type_name] - growth_params = veg_type['growth_params'] - - # 计算生长时间 - elapsed_time = current_time - instance_info['last_update'] - instance_info['last_update'] = current_time - - # 更新年龄 - instance_info['age'] += elapsed_time - - # 计算生长率(受健康状况影响) - growth_rate = growth_params.get('growth_rate', 0.1) * instance_info['health'] - - # 生长模拟 - if instance_info['growth_stage'] == 0: # seedling - # 幼苗阶段:快速增长 - new_scale = node.getScale() * (1.0 + growth_rate * elapsed_time * 0.1) - node.setScale(new_scale) - - # 检查是否进入下一阶段 - if instance_info['age'] > 5.0: # 5秒后进入sapling阶段 - instance_info['growth_stage'] = 1 - - elif instance_info['growth_stage'] == 1: # sapling - # 树苗阶段:中等速度增长 - new_scale = node.getScale() * (1.0 + growth_rate * elapsed_time * 0.05) - node.setScale(new_scale) - - # 检查是否进入下一阶段 - if instance_info['age'] > 20.0: # 20秒后进入mature阶段 - instance_info['growth_stage'] = 2 - - elif instance_info['growth_stage'] == 2: # mature - # 成熟阶段:缓慢增长 - new_scale = node.getScale() * (1.0 + growth_rate * elapsed_time * 0.01) - node.setScale(new_scale) - - # 检查是否进入下一阶段 - lifespan = growth_params.get('lifespan', 100.0) - if instance_info['age'] > lifespan * 0.8: # 80%寿命后进入elderly阶段 - instance_info['growth_stage'] = 3 - - elif instance_info['growth_stage'] == 3: # elderly - # 老年阶段:可能开始衰退 - if random.random() < 0.001: # 小概率开始衰退 - instance_info['health'] -= 0.01 * elapsed_time - if instance_info['health'] < 0: - instance_info['health'] = 0 - - # 缩放可能减小 - health_factor = max(0.5, instance_info['health']) - new_scale = node.getScale() * (1.0 - (1.0 - health_factor) * 0.001 * elapsed_time) - node.setScale(new_scale) - - # 更新实例信息 - instance_info['scale'] = node.getScale() - - except Exception as e: - print(f"模拟植被生长时出错: {e}") - - def apply_wind_effect(self, wind_strength=None, wind_direction=None, wind_speed=None): - """ - 应用风力效果 - """ - try: - # 更新风力参数 - if wind_strength is not None: - self.wind_effect['strength'] = wind_strength - if wind_direction is not None: - self.wind_effect['direction'] = wind_direction - if wind_speed is not None: - self.wind_effect['speed'] = wind_speed - - current_time = self.world.globalClock.getFrameTime() - - for instance_info in self.vegetation_instances: - node = instance_info['node'] - if node and not node.isEmpty(): - # 计算风力影响 - time_factor = current_time * self.wind_effect['speed'] - - # 添加湍流效果 - turbulence = math.sin(time_factor * 3) * self.wind_effect['turbulence'] - - # 计算摆动量 - sway_amount = math.sin(time_factor) * self.wind_effect['strength'] * 10 - sway_amount += turbulence * 5 - - # 根据植被类型调整摆动(大树摆动较小,小草摆动较大) - veg_type_name = instance_info['type'] - if veg_type_name in self.vegetation_types: - veg_type = self.vegetation_types[veg_type_name] - scale_factor = node.getScale().length() # 简化的大尺度因子 - sway_amount *= max(0.1, 1.0 / scale_factor) # 大树摆动小 - - # 应用摆动效果 - original_rotation = instance_info['rotation'] - new_h = original_rotation + sway_amount - node.setH(new_h) - - # 添加轻微的位置偏移 - sway_offset = Vec3( - math.cos(time_factor * 2) * self.wind_effect['strength'] * 0.1, - math.sin(time_factor * 2) * self.wind_effect['strength'] * 0.1, - 0 - ) - instance_info['wind_offset'] = sway_offset - - except Exception as e: - print(f"应用风力效果时出错: {e}") - - def simulate_ecosystem_interactions(self, time_step=1.0): - """ - 模拟生态系统相互作用 - """ - try: - # 模拟植物间的竞争、繁殖、死亡等生态过程 - current_time = self.world.globalClock.getFrameTime() - - # 检查植物健康状况 - for instance_info in self.vegetation_instances[:]: # 使用切片复制避免修改列表时的问题 - node = instance_info['node'] - if not node or node.isEmpty(): - continue - - # 检查年龄相关死亡 - veg_type_name = instance_info['type'] - if veg_type_name in self.vegetation_types: - veg_type = self.vegetation_types[veg_type_name] - lifespan = veg_type['growth_params'].get('lifespan', 100.0) - - if instance_info['age'] > lifespan: - # 植物死亡 - if random.random() < 0.01: # 每帧1%概率死亡 - print(f"植物死亡: {veg_type_name}") - self.remove_vegetation_instance(instance_info) - continue - - # 检查健康状况 - if instance_info['health'] <= 0: - print(f"植物因健康状况不佳死亡: {veg_type_name}") - self.remove_vegetation_instance(instance_info) - continue - - # 模拟繁殖(简化) - if instance_info['growth_stage'] >= 2: # 成熟植物才能繁殖 - growth_params = veg_type['growth_params'] - seed_production = growth_params.get('seed_production', 0.5) - - if random.random() < seed_production * 0.001: # 繁殖概率 - self._attempt_reproduction(instance_info) - - except Exception as e: - print(f"模拟生态系统相互作用时出错: {e}") - - def _attempt_reproduction(self, parent_instance): - """ - 尝试繁殖 - """ - try: - # 在父植物附近生成新植物 - parent_pos = parent_instance['position'] - - # 随机生成在附近位置 - offset_range = 5.0 # 5米范围内 - new_x = parent_pos.getX() + random.uniform(-offset_range, offset_range) - new_y = parent_pos.getY() + random.uniform(-offset_range, offset_range) - new_z = parent_pos.getZ() # 高度保持一致 - - # 创建新植物实例 - veg_type_name = parent_instance['type'] - self._create_vegetation_instance(veg_type_name, Point3(new_x, new_y, new_z)) - - except Exception as e: - print(f"尝试繁殖时出错: {e}") - - def save_vegetation_data(self, terrain_info, output_path): - """ - 保存植被数据 - """ - try: - # 收集植被数据 - vegetation_data = { - 'vegetation_types': {}, - 'instances': [], - 'ecosystem_settings': { - 'wind_effect': self.wind_effect - } - } - - # 保存植被类型定义 - for name, veg_type in self.vegetation_types.items(): - vegetation_data['vegetation_types'][name] = { - 'model_path': veg_type['model_path'], - 'scale_range': veg_type['scale_range'], - 'rotation_range': veg_type['rotation_range'], - 'color_variation': veg_type['color_variation'], - 'growth_params': veg_type['growth_params'], - 'ecosystem_role': veg_type['ecosystem_role'] - } - - # 保存实例信息 - for instance_info in self.vegetation_instances: - instance_data = { - 'type': instance_info['type'], - 'position': [instance_info['position'].x, - instance_info['position'].y, - instance_info['position'].z], - 'scale': [instance_info['scale'].x, - instance_info['scale'].y, - instance_info['scale'].z], - 'rotation': instance_info['rotation'], - 'age': instance_info['age'], - 'health': instance_info['health'], - 'growth_stage': instance_info['growth_stage'], - 'creation_time': instance_info['creation_time'] - } - vegetation_data['instances'].append(instance_data) - - # 确保输出目录存在 - output_dir = os.path.dirname(output_path) - if not os.path.exists(output_dir): - os.makedirs(output_dir) - - # 写入JSON文件 - with open(output_path, 'w', encoding='utf-8') as f: - json.dump(vegetation_data, f, indent=2, ensure_ascii=False) - - print(f"植被数据保存成功: {output_path}") - return True - - except Exception as e: - print(f"保存植被数据时出错: {e}") - return False - - def load_vegetation_data(self, terrain_info, input_path): - """ - 加载植被数据 - """ - try: - if not os.path.exists(input_path): - print(f"植被数据文件不存在: {input_path}") - return False - - # 读取JSON文件 - with open(input_path, 'r', encoding='utf-8') as f: - vegetation_data = json.load(f) - - # 清除现有植被 - self.clear_all_vegetation() - - # 加载植被类型定义 - if 'vegetation_types' in vegetation_data: - for name, veg_def in vegetation_data['vegetation_types'].items(): - self.define_vegetation_type( - name=name, - model_path=veg_def['model_path'], - scale_range=veg_def.get('scale_range', (1.0, 1.0)), - rotation_range=veg_def.get('rotation_range', (0, 360)), - color_variation=veg_def.get('color_variation', (0.8, 1.2)), - growth_params=veg_def.get('growth_params'), - ecosystem_role=veg_def.get('ecosystem_role', 'general') - ) - - # 加载实例 - if 'instances' in vegetation_data: - for instance_data in vegetation_data['instances']: - position = Point3(instance_data['position'][0], - instance_data['position'][1], - instance_data['position'][2]) - - # 创建实例 - instance_info = self._create_vegetation_instance(instance_data['type'], position) - if instance_info: - # 恢复实例状态 - if 'scale' in instance_data: - scale_vec = instance_data['scale'] - if len(scale_vec) == 3: - instance_info['node'].setScale(scale_vec[0], scale_vec[1], scale_vec[2]) - else: - instance_info['node'].setScale(scale_vec[0]) - - if 'age' in instance_data: - instance_info['age'] = instance_data['age'] - if 'health' in instance_data: - instance_info['health'] = instance_data['health'] - if 'growth_stage' in instance_data: - instance_info['growth_stage'] = instance_data['growth_stage'] - if 'creation_time' in instance_data: - instance_info['creation_time'] = instance_data['creation_time'] - - # 恢复生态系统设置 - if 'ecosystem_settings' in vegetation_data: - eco_settings = vegetation_data['ecosystem_settings'] - if 'wind_effect' in eco_settings: - self.wind_effect = eco_settings['wind_effect'] - - print(f"植被数据加载成功: {input_path}") - return True - - except Exception as e: - print(f"加载植被数据时出错: {e}") - return False - - def get_vegetation_stats(self): - """ - 获取植被统计信息 - """ - stats = { - 'total_instances': len(self.vegetation_instances), - 'vegetation_types': {}, - 'ecosystem_stats': { - 'wind_strength': self.wind_effect['strength'], - 'wind_speed': self.wind_effect['speed'] - }, - 'growth_stages': { - 'seedling': 0, - 'sapling': 0, - 'mature': 0, - 'elderly': 0 - } - } - - # 统计植被类型 - for name, veg_type in self.vegetation_types.items(): - stats['vegetation_types'][name] = { - 'count': len(veg_type['instances']), - 'model_path': veg_type['model_path'], - 'ecosystem_role': veg_type['ecosystem_role'] - } - - # 统计生长阶段 - for instance_info in self.vegetation_instances: - stage = instance_info['growth_stage'] - if stage == 0: - stats['growth_stages']['seedling'] += 1 - elif stage == 1: - stats['growth_stages']['sapling'] += 1 - elif stage == 2: - stats['growth_stages']['mature'] += 1 - elif stage == 3: - stats['growth_stages']['elderly'] += 1 - - return stats - - def paint_vegetation(self, terrain_info, brush_position, brush_radius, - vegetation_type, density=1.0): - """ - 绘制植被(笔刷工具) - """ - try: - terrain_node = terrain_info['node'] - heightfield = terrain_info['heightfield'] - - if not heightfield: - print("无法获取地形高度图数据") - return False - - width = heightfield.getXSize() - height = heightfield.getYSize() - - # 获取地形节点信息 - terrain_pos = terrain_node.getPos() - terrain_scale = terrain_node.getScale() - - # 计算笔刷区域 - center_offset = (width - 1) / 2 - brush_x = int((brush_position.getX() - terrain_pos.getX()) / terrain_scale.getX() + center_offset) - brush_y = int((brush_position.getY() - terrain_pos.getY()) / terrain_scale.getY() + center_offset) - - brush_pixels = int(brush_radius / max(terrain_scale.getX(), terrain_scale.getY())) - - # 在笔刷区域内生成植被 - instances_created = 0 - for dx in range(-brush_pixels, brush_pixels + 1): - for dy in range(-brush_pixels, brush_pixels + 1): - # 检查是否在笔刷范围内 - distance = math.sqrt(dx*dx + dy*dy) - if distance <= brush_pixels: - # 计算实际位置 - x = brush_x + dx - y = brush_y + dy - - # 检查边界 - if 0 <= x < width and 0 <= y < height: - # 根据密度决定是否创建 - if random.random() < density: - # 计算世界坐标 - world_x = (x - center_offset) * terrain_scale.getX() + terrain_pos.getX() - world_y = (y - center_offset) * terrain_scale.getY() + terrain_pos.getY() - world_z = heightfield.getRed(x, y) * terrain_scale.getZ() + terrain_pos.getZ() - - # 创建植被实例 - if self._create_vegetation_instance(vegetation_type, Point3(world_x, world_y, world_z)): - instances_created += 1 - - print(f"绘制植被完成,创建了 {instances_created} 个实例") - return True - - except Exception as e: - print(f"绘制植被时出错: {e}") - return False - - def update_all_systems(self, time_delta): - """ - 更新所有植被系统 - """ - try: - # 更新生长模拟 - self.simulate_growth(time_delta) - - # 更新风力效果 - self.apply_wind_effect() - - # 更新生态系统相互作用 - self.simulate_ecosystem_interactions(time_delta) - - # 更新LOD(如果已设置相机位置) - # 这里需要从外部传入相机位置 - # self.update_vegetation_lod(camera_pos) - - except Exception as e: +""" +地形植被系统 +提供完整的植被分布、生长模拟、LOD控制、生态模拟等功能 +""" + +from panda3d.core import NodePath, Vec3, Point3, BitMask32 +from panda3d.core import Texture, TextureStage, Material +import random +import math +import json +import os +import numpy as np + +class VegetationSystem: + """ + 地形植被系统类 + 提供完整的植被分布、生长模拟、LOD控制、生态模拟等功能 + """ + + def __init__(self, world): + self.world = world + self.vegetation_instances = [] # 存储植被实例 + self.vegetation_types = {} # 存储植被类型定义 + self.density_maps = {} # 存储密度图 + self.lod_settings = {} # 存储LOD设置 + self.ecosystem = {} # 存储生态系统信息 + self.growth_simulator = None # 生长模拟器 + self.wind_effect = { # 风力效果参数 + 'strength': 0.5, + 'direction': Vec3(1, 0, 0), + 'speed': 1.0, + 'turbulence': 0.2 + } + + def define_vegetation_type(self, name, model_path, scale_range=(1.0, 1.0), + rotation_range=(0, 360), color_variation=(0.8, 1.2), + growth_params=None, ecosystem_role='general'): + """ + 定义植被类型 + growth_params: 生长参数字典 + ecosystem_role: 生态角色 ('tree', 'shrub', 'grass', 'flower', 'general') + """ + self.vegetation_types[name] = { + 'model_path': model_path, + 'scale_range': scale_range, + 'rotation_range': rotation_range, + 'color_variation': color_variation, + 'instances': [], + 'growth_params': growth_params or { + 'max_height': 10.0, + 'growth_rate': 0.1, + 'lifespan': 100.0, + 'seed_production': 0.5, + 'water_requirement': 0.5, + 'sunlight_requirement': 0.7 + }, + 'ecosystem_role': ecosystem_role, + 'seasonal_variations': { + 'spring': {'color_shift': (0.1, 0.2, 0.0), 'scale_factor': 1.1}, + 'summer': {'color_shift': (0.0, 0.0, 0.0), 'scale_factor': 1.0}, + 'autumn': {'color_shift': (0.3, 0.2, 0.0), 'scale_factor': 0.9}, + 'winter': {'color_shift': (0.1, 0.1, 0.1), 'scale_factor': 0.8} + } + } + print(f"定义植被类型: {name} (生态角色: {ecosystem_role})") + + def load_vegetation_models(self): + """ + 预加载所有植被模型 + """ + for name, veg_type in self.vegetation_types.items(): + try: + model = self.world.loader.loadModel(veg_type['model_path']) + if model: + veg_type['model'] = model + print(f"加载植被模型: {name}") + else: + print(f"无法加载植被模型: {name}") + except Exception as e: + print(f"加载植被模型 {name} 时出错: {e}") + + def generate_vegetation(self, terrain_info, vegetation_config, density=0.1, + distribution_method='random'): + """ + 在地形上生成植被 + vegetation_config: 植被配置字典,指定不同类型植被的分布参数 + distribution_method: 分布方法 ('random', 'clustered', 'ecosystem') + """ + try: + terrain_node = terrain_info['node'] + heightfield = terrain_info['heightfield'] + + if not heightfield: + print("无法获取地形高度图数据") + return False + + width = heightfield.getXSize() + height = heightfield.getYSize() + + # 获取地形节点信息 + terrain_pos = terrain_node.getPos() + terrain_scale = terrain_node.getScale() + + # 根据分布方法选择生成策略 + if distribution_method == 'ecosystem': + return self._generate_ecosystem_vegetation(terrain_info, vegetation_config, density) + elif distribution_method == 'clustered': + return self._generate_clustered_vegetation(terrain_info, vegetation_config, density) + else: + return self._generate_random_vegetation(terrain_info, vegetation_config, density) + + except Exception as e: + print(f"生成植被时出错: {e}") + return False + + def _generate_random_vegetation(self, terrain_info, vegetation_config, density): + """ + 随机生成植被 + """ + try: + terrain_node = terrain_info['node'] + heightfield = terrain_info['heightfield'] + width = heightfield.getXSize() + height = heightfield.getYSize() + terrain_pos = terrain_node.getPos() + terrain_scale = terrain_node.getScale() + + # 为每种植被类型生成实例 + total_instances = 0 + for veg_type_name, veg_params in vegetation_config.items(): + if veg_type_name not in self.vegetation_types: + print(f"未定义的植被类型: {veg_type_name}") + continue + + veg_type = self.vegetation_types[veg_type_name] + + # 获取环境限制条件 + min_height = veg_params.get('min_height', 0.0) + max_height = veg_params.get('max_height', 1.0) + slope_threshold = veg_params.get('slope_threshold', 45.0) + moisture_preference = veg_params.get('moisture_preference', 0.5) + + # 计算植被数量 + total_points = int(width * height * density * veg_params.get('density_factor', 1.0)) + + # 生成植被实例 + instances_created = 0 + attempts = 0 + max_attempts = total_points * 10 # 防止无限循环 + + while instances_created < total_points and attempts < max_attempts: + attempts += 1 + + # 随机选择位置 + x = random.randint(0, width - 1) + y = random.randint(0, height - 1) + + # 获取高度值 + height_value = heightfield.getRed(x, y) + + # 检查高度限制 + if height_value < min_height or height_value > max_height: + continue + + # 检查坡度 + if self._calculate_slope(heightfield, x, y) > slope_threshold: + continue + + # 检查湿度条件(简化模拟) + moisture = self._calculate_moisture(heightfield, x, y) + if abs(moisture - moisture_preference) > 0.3: + continue + + # 计算世界坐标 + center_offset = (width - 1) / 2 + world_x = (x - center_offset) * terrain_scale.getX() + terrain_pos.getX() + world_y = (y - center_offset) * terrain_scale.getY() + terrain_pos.getY() + world_z = height_value * terrain_scale.getZ() + terrain_pos.getZ() + + # 创建植被实例 + if self._create_vegetation_instance(veg_type_name, Point3(world_x, world_y, world_z)): + instances_created += 1 + + total_instances += instances_created + print(f"植被类型 {veg_type_name} 生成了 {instances_created} 个实例") + + print(f"随机生成植被完成,共创建 {total_instances} 个实例") + return True + + except Exception as e: + print(f"随机生成植被时出错: {e}") + return False + + def _generate_clustered_vegetation(self, terrain_info, vegetation_config, density): + """ + 簇状生成植被 + """ + try: + terrain_node = terrain_info['node'] + heightfield = terrain_info['heightfield'] + width = heightfield.getXSize() + height = heightfield.getYSize() + terrain_pos = terrain_node.getPos() + terrain_scale = terrain_node.getScale() + + # 为每种植被类型生成簇 + total_instances = 0 + for veg_type_name, veg_params in vegetation_config.items(): + if veg_type_name not in self.vegetation_types: + continue + + # 计算簇的数量 + cluster_count = int((width * height * density * veg_params.get('density_factor', 1.0)) / 10) + + for _ in range(cluster_count): + # 随机选择簇中心 + center_x = random.randint(0, width - 1) + center_y = random.randint(0, height - 1) + center_height = heightfield.getRed(center_x, center_y) + + # 确定簇大小 + cluster_size = random.randint(3, 10) + + # 在簇内生成植被 + for _ in range(cluster_size): + # 在簇中心附近随机分布 + offset_x = random.randint(-5, 5) + offset_y = random.randint(-5, 5) + x = max(0, min(width - 1, center_x + offset_x)) + y = max(0, min(height - 1, center_y + offset_y)) + + # 计算世界坐标 + center_offset = (width - 1) / 2 + world_x = (x - center_offset) * terrain_scale.getX() + terrain_pos.getX() + world_y = (y - center_offset) * terrain_scale.getY() + terrain_pos.getY() + world_z = heightfield.getRed(x, y) * terrain_scale.getZ() + terrain_pos.getZ() + + # 创建植被实例 + if self._create_vegetation_instance(veg_type_name, Point3(world_x, world_y, world_z)): + total_instances += 1 + + print(f"簇状生成植被完成,共创建 {total_instances} 个实例") + return True + + except Exception as e: + print(f"簇状生成植被时出错: {e}") + return False + + def _generate_ecosystem_vegetation(self, terrain_info, vegetation_config, density): + """ + 生态系统方式生成植被 + """ + try: + terrain_node = terrain_info['node'] + heightfield = terrain_info['heightfield'] + width = heightfield.getXSize() + height = heightfield.getYSize() + terrain_pos = terrain_node.getPos() + terrain_scale = terrain_node.getScale() + + # 创建生态位地图 + niche_map = self._create_niche_map(heightfield) + + # 为每种植被类型根据生态位生成 + total_instances = 0 + for veg_type_name, veg_params in vegetation_config.items(): + if veg_type_name not in self.vegetation_types: + continue + + veg_type = self.vegetation_types[veg_type_name] + ecosystem_role = veg_type['ecosystem_role'] + + # 根据生态角色确定分布偏好 + if ecosystem_role == 'tree': + preferred_niches = ['high_ground', 'flat_area'] + elif ecosystem_role == 'shrub': + preferred_niches = ['slope', 'transition'] + elif ecosystem_role == 'grass': + preferred_niches = ['flat_area', 'low_ground'] + else: + preferred_niches = ['any'] + + # 计算植被数量 + total_points = int(width * height * density * veg_params.get('density_factor', 1.0)) + + # 根据生态位偏好生成植被 + instances_created = 0 + attempts = 0 + max_attempts = total_points * 20 + + while instances_created < total_points and attempts < max_attempts: + attempts += 1 + + # 根据生态位偏好选择位置 + x, y = self._select_ecosystem_position(niche_map, preferred_niches) + if x is None or y is None: + continue + + # 检查环境条件 + height_value = heightfield.getRed(x, y) + slope = self._calculate_slope(heightfield, x, y) + + # 根据植被类型检查条件 + if not self._check_environment_conditions(veg_type_name, height_value, slope): + continue + + # 计算世界坐标 + center_offset = (width - 1) / 2 + world_x = (x - center_offset) * terrain_scale.getX() + terrain_pos.getX() + world_y = (y - center_offset) * terrain_scale.getY() + terrain_pos.getY() + world_z = height_value * terrain_scale.getZ() + terrain_pos.getZ() + + # 创建植被实例 + if self._create_vegetation_instance(veg_type_name, Point3(world_x, world_y, world_z)): + instances_created += 1 + + total_instances += instances_created + print(f"生态系统生成植被类型 {veg_type_name}: {instances_created} 个实例") + + print(f"生态系统生成植被完成,共创建 {total_instances} 个实例") + return True + + except Exception as e: + print(f"生态系统生成植被时出错: {e}") + return False + + def _create_niche_map(self, heightfield): + """ + 创建生态位地图 + """ + try: + width = heightfield.getXSize() + height = heightfield.getYSize() + niche_map = {} + + for x in range(width): + for y in range(height): + # 分析位置特征 + slope = self._calculate_slope(heightfield, x, y) + height_value = heightfield.getRed(x, y) + + # 确定生态位 + if slope > 30: + niche = 'steep_slope' + elif slope > 15: + niche = 'slope' + elif height_value > 0.7: + niche = 'high_ground' + elif height_value < 0.3: + niche = 'low_ground' + else: + niche = 'flat_area' + + # 检查是否为过渡区域 + if self._is_transition_zone(heightfield, x, y): + niche = 'transition' + + niche_map[(x, y)] = niche + + return niche_map + + except Exception as e: + print(f"创建生态位地图时出错: {e}") + return {} + + def _is_transition_zone(self, heightfield, x, y): + """ + 检查是否为过渡区域 + """ + try: + width = heightfield.getXSize() + height = heightfield.getYSize() + + if x < 2 or x >= width - 2 or y < 2 or y >= height - 2: + return False + + # 计算周围点的高度差异 + center_height = heightfield.getRed(x, y) + height_diff = 0 + + for dx in [-1, 0, 1]: + for dy in [-1, 0, 1]: + if dx == 0 and dy == 0: + continue + neighbor_height = heightfield.getRed(x + dx, y + dy) + height_diff += abs(neighbor_height - center_height) + + # 如果高度差异较大,则为过渡区域 + return height_diff > 0.1 + + except: + return False + + def _select_ecosystem_position(self, niche_map, preferred_niches): + """ + 根据生态位偏好选择位置 + """ + try: + if not niche_map: + return None, None + + # 如果偏好'any',随机选择 + if 'any' in preferred_niches: + positions = list(niche_map.keys()) + if positions: + return random.choice(positions) + return None, None + + # 根据偏好选择 + preferred_positions = [] + for pos, niche in niche_map.items(): + if niche in preferred_niches: + preferred_positions.append(pos) + + if preferred_positions: + return random.choice(preferred_positions) + + # 如果没有偏好位置,随机选择 + positions = list(niche_map.keys()) + if positions: + return random.choice(positions) + return None, None + + except: + return None, None + + def _check_environment_conditions(self, veg_type_name, height_value, slope): + """ + 检查环境条件是否适合植被生长 + """ + try: + if veg_type_name not in self.vegetation_types: + return True + + veg_type = self.vegetation_types[veg_type_name] + growth_params = veg_type['growth_params'] + + # 检查高度限制 + max_height = growth_params.get('max_height', 100.0) + if height_value * 100 > max_height: # 假设高度值0-1对应0-100米 + return False + + # 检查坡度限制 + max_slope = growth_params.get('max_slope', 45.0) + if slope > max_slope: + return False + + return True + + except: + return True + + def _calculate_slope(self, heightfield, x, y): + """ + 计算指定点的坡度 + """ + try: + width = heightfield.getXSize() + height = heightfield.getYSize() + + # 获取周围点的高度 + heights = [] + for dx in [-1, 0, 1]: + for dy in [-1, 0, 1]: + nx, ny = x + dx, y + dy + if 0 <= nx < width and 0 <= ny < height: + heights.append(heightfield.getRed(nx, ny)) + else: + heights.append(heightfield.getRed(x, y)) + + # 计算X和Y方向的梯度 + dx = heights[2] - heights[0] # 右侧点 - 左侧点 + dy = heights[6] - heights[0] # 下方点 - 上方点 + + # 计算坡度(角度) + slope = math.degrees(math.atan(math.sqrt(dx*dx + dy*dy))) + return slope + + except: + return 0.0 + + def _calculate_moisture(self, heightfield, x, y): + """ + 计算指定点的湿度(简化模拟) + """ + try: + # 简化的湿度计算:低洼地区湿度较高 + width = heightfield.getXSize() + height = heightfield.getYSize() + + # 计算周围点的平均高度 + total_height = 0 + count = 0 + for dx in [-2, -1, 0, 1, 2]: + for dy in [-2, -1, 0, 1, 2]: + nx, ny = x + dx, y + dy + if 0 <= nx < width and 0 <= ny < height: + total_height += heightfield.getRed(nx, ny) + count += 1 + + if count > 0: + avg_height = total_height / count + current_height = heightfield.getRed(x, y) + # 低洼地区湿度较高 + moisture = max(0.0, min(1.0, 0.5 + (avg_height - current_height) * 2)) + return moisture + + return 0.5 + + except: + return 0.5 + + def _create_vegetation_instance(self, veg_type_name, position): + """ + 创建植被实例 + """ + try: + if veg_type_name not in self.vegetation_types: + return None + + veg_type = self.vegetation_types[veg_type_name] + + # 检查模型是否已加载 + if 'model' not in veg_type: + # 尝试加载模型 + try: + model = self.world.loader.loadModel(veg_type['model_path']) + if model: + veg_type['model'] = model + else: + print(f"无法加载植被模型: {veg_type['model_path']}") + return None + except Exception as e: + print(f"加载植被模型时出错: {e}") + return None + + # 克隆模型创建实例 + instance = veg_type['model'].copyTo(self.world.render) + instance.setPos(position) + + # 应用随机缩放 + scale_min, scale_max = veg_type['scale_range'] + scale = random.uniform(scale_min, scale_max) + instance.setScale(scale) + + # 应用随机旋转 + rot_min, rot_max = veg_type['rotation_range'] + rotation = random.uniform(rot_min, rot_max) + instance.setH(rotation) + + # 应用颜色变化 + color_min, color_max = veg_type['color_variation'] + color_factor = random.uniform(color_min, color_max) + # 应用季节性颜色变化 + season_color = self._get_seasonal_color(veg_type_name) + if season_color: + # 这里可以应用颜色变化到模型材质 + pass + + # 设置标签 + instance.setTag("vegetation_type", veg_type_name) + instance.setTag("is_scene_element", "1") + instance.setTag("tree_item_type", "VEGETATION_NODE") + instance.setTag("creation_time", str(self.world.globalClock.getFrameTime())) + + # 添加碰撞体 + instance.setCollideMask(BitMask32.bit(3)) # 使用第3位作为植被碰撞掩码 + + # 初始化生长状态 + creation_time = self.world.globalClock.getFrameTime() + + # 保存实例信息 + instance_info = { + 'node': instance, + 'type': veg_type_name, + 'position': position, + 'scale': scale, + 'rotation': rotation, + 'creation_time': creation_time, + 'age': 0.0, + 'health': 1.0, + 'growth_stage': 0, # 0=seedling, 1=sapling, 2=mature, 3=elderly + 'last_update': creation_time, + 'wind_offset': Vec3(0, 0, 0) + } + + self.vegetation_instances.append(instance_info) + veg_type['instances'].append(instance_info) + + return instance_info + + except Exception as e: + print(f"创建植被实例时出错: {e}") + return None + + def _get_seasonal_color(self, veg_type_name): + """ + 获取季节性颜色变化 + """ + try: + if veg_type_name in self.vegetation_types: + veg_type = self.vegetation_types[veg_type_name] + # 这里可以根据当前季节返回相应的颜色变化 + # 简化实现,返回默认值 + return veg_type['seasonal_variations'].get('summer', {'color_shift': (0, 0, 0)}) + return None + except: + return None + + def remove_vegetation_instance(self, instance_info): + """ + 移除植被实例 + """ + try: + if instance_info in self.vegetation_instances: + # 从全局列表中移除 + self.vegetation_instances.remove(instance_info) + + # 从类型列表中移除 + veg_type_name = instance_info['type'] + if veg_type_name in self.vegetation_types: + if instance_info in self.vegetation_types[veg_type_name]['instances']: + self.vegetation_types[veg_type_name]['instances'].remove(instance_info) + + # 移除节点 + if instance_info['node'] and not instance_info['node'].isEmpty(): + instance_info['node'].removeNode() + + print("移除植被实例") + return True + + except Exception as e: + print(f"移除植被实例时出错: {e}") + return False + + def clear_all_vegetation(self): + """ + 清除所有植被 + """ + try: + # 移除所有实例节点 + for instance_info in self.vegetation_instances: + if instance_info['node'] and not instance_info['node'].isEmpty(): + instance_info['node'].removeNode() + + # 清空数据结构 + self.vegetation_instances = [] + + # 清空各类型实例列表 + for veg_type in self.vegetation_types.values(): + veg_type['instances'] = [] + + print("清除所有植被") + return True + + except Exception as e: + print(f"清除所有植被时出错: {e}") + return False + + def set_vegetation_lod(self, lod_settings): + """ + 设置植被LOD + lod_settings: LOD设置字典,包含距离阈值和简化策略 + """ + self.lod_settings = lod_settings + print("设置植被LOD") + + def update_vegetation_lod(self, camera_pos): + """ + 更新植被LOD + """ + try: + for instance_info in self.vegetation_instances: + node = instance_info['node'] + if node and not node.isEmpty(): + # 计算到相机的距离 + distance = (node.getPos() - camera_pos).length() + + # 根据距离应用LOD策略 + if distance > self.lod_settings.get('far_distance', 100): + # 远距离时隐藏 + node.hide() + elif distance > self.lod_settings.get('medium_distance', 50): + # 中距离时显示简化版本 + node.show() + self._apply_medium_lod(node) + else: + # 近距离时显示完整版本 + node.show() + self._apply_full_lod(node) + + except Exception as e: + print(f"更新植被LOD时出错: {e}") + + def _apply_medium_lod(self, node): + """ + 应用中距离LOD + """ + try: + # 减少多边形数量的简化版本 + # 这里可以实现模型简化或使用公告板 + # 简化实现:使用缩放来模拟简化效果 + original_scale = node.getScale() + node.setScale(original_scale * 0.8) + except: + pass + + def _apply_full_lod(self, node): + """ + 应用完整LOD + """ + # 恢复完整细节 + pass + + def simulate_growth(self, time_step=1.0): + """ + 模拟植被生长 + """ + try: + current_time = self.world.globalClock.getFrameTime() + + for instance_info in self.vegetation_instances: + node = instance_info['node'] + if node and not node.isEmpty(): + # 获取植被类型信息 + veg_type_name = instance_info['type'] + if veg_type_name not in self.vegetation_types: + continue + + veg_type = self.vegetation_types[veg_type_name] + growth_params = veg_type['growth_params'] + + # 计算生长时间 + elapsed_time = current_time - instance_info['last_update'] + instance_info['last_update'] = current_time + + # 更新年龄 + instance_info['age'] += elapsed_time + + # 计算生长率(受健康状况影响) + growth_rate = growth_params.get('growth_rate', 0.1) * instance_info['health'] + + # 生长模拟 + if instance_info['growth_stage'] == 0: # seedling + # 幼苗阶段:快速增长 + new_scale = node.getScale() * (1.0 + growth_rate * elapsed_time * 0.1) + node.setScale(new_scale) + + # 检查是否进入下一阶段 + if instance_info['age'] > 5.0: # 5秒后进入sapling阶段 + instance_info['growth_stage'] = 1 + + elif instance_info['growth_stage'] == 1: # sapling + # 树苗阶段:中等速度增长 + new_scale = node.getScale() * (1.0 + growth_rate * elapsed_time * 0.05) + node.setScale(new_scale) + + # 检查是否进入下一阶段 + if instance_info['age'] > 20.0: # 20秒后进入mature阶段 + instance_info['growth_stage'] = 2 + + elif instance_info['growth_stage'] == 2: # mature + # 成熟阶段:缓慢增长 + new_scale = node.getScale() * (1.0 + growth_rate * elapsed_time * 0.01) + node.setScale(new_scale) + + # 检查是否进入下一阶段 + lifespan = growth_params.get('lifespan', 100.0) + if instance_info['age'] > lifespan * 0.8: # 80%寿命后进入elderly阶段 + instance_info['growth_stage'] = 3 + + elif instance_info['growth_stage'] == 3: # elderly + # 老年阶段:可能开始衰退 + if random.random() < 0.001: # 小概率开始衰退 + instance_info['health'] -= 0.01 * elapsed_time + if instance_info['health'] < 0: + instance_info['health'] = 0 + + # 缩放可能减小 + health_factor = max(0.5, instance_info['health']) + new_scale = node.getScale() * (1.0 - (1.0 - health_factor) * 0.001 * elapsed_time) + node.setScale(new_scale) + + # 更新实例信息 + instance_info['scale'] = node.getScale() + + except Exception as e: + print(f"模拟植被生长时出错: {e}") + + def apply_wind_effect(self, wind_strength=None, wind_direction=None, wind_speed=None): + """ + 应用风力效果 + """ + try: + # 更新风力参数 + if wind_strength is not None: + self.wind_effect['strength'] = wind_strength + if wind_direction is not None: + self.wind_effect['direction'] = wind_direction + if wind_speed is not None: + self.wind_effect['speed'] = wind_speed + + current_time = self.world.globalClock.getFrameTime() + + for instance_info in self.vegetation_instances: + node = instance_info['node'] + if node and not node.isEmpty(): + # 计算风力影响 + time_factor = current_time * self.wind_effect['speed'] + + # 添加湍流效果 + turbulence = math.sin(time_factor * 3) * self.wind_effect['turbulence'] + + # 计算摆动量 + sway_amount = math.sin(time_factor) * self.wind_effect['strength'] * 10 + sway_amount += turbulence * 5 + + # 根据植被类型调整摆动(大树摆动较小,小草摆动较大) + veg_type_name = instance_info['type'] + if veg_type_name in self.vegetation_types: + veg_type = self.vegetation_types[veg_type_name] + scale_factor = node.getScale().length() # 简化的大尺度因子 + sway_amount *= max(0.1, 1.0 / scale_factor) # 大树摆动小 + + # 应用摆动效果 + original_rotation = instance_info['rotation'] + new_h = original_rotation + sway_amount + node.setH(new_h) + + # 添加轻微的位置偏移 + sway_offset = Vec3( + math.cos(time_factor * 2) * self.wind_effect['strength'] * 0.1, + math.sin(time_factor * 2) * self.wind_effect['strength'] * 0.1, + 0 + ) + instance_info['wind_offset'] = sway_offset + + except Exception as e: + print(f"应用风力效果时出错: {e}") + + def simulate_ecosystem_interactions(self, time_step=1.0): + """ + 模拟生态系统相互作用 + """ + try: + # 模拟植物间的竞争、繁殖、死亡等生态过程 + current_time = self.world.globalClock.getFrameTime() + + # 检查植物健康状况 + for instance_info in self.vegetation_instances[:]: # 使用切片复制避免修改列表时的问题 + node = instance_info['node'] + if not node or node.isEmpty(): + continue + + # 检查年龄相关死亡 + veg_type_name = instance_info['type'] + if veg_type_name in self.vegetation_types: + veg_type = self.vegetation_types[veg_type_name] + lifespan = veg_type['growth_params'].get('lifespan', 100.0) + + if instance_info['age'] > lifespan: + # 植物死亡 + if random.random() < 0.01: # 每帧1%概率死亡 + print(f"植物死亡: {veg_type_name}") + self.remove_vegetation_instance(instance_info) + continue + + # 检查健康状况 + if instance_info['health'] <= 0: + print(f"植物因健康状况不佳死亡: {veg_type_name}") + self.remove_vegetation_instance(instance_info) + continue + + # 模拟繁殖(简化) + if instance_info['growth_stage'] >= 2: # 成熟植物才能繁殖 + growth_params = veg_type['growth_params'] + seed_production = growth_params.get('seed_production', 0.5) + + if random.random() < seed_production * 0.001: # 繁殖概率 + self._attempt_reproduction(instance_info) + + except Exception as e: + print(f"模拟生态系统相互作用时出错: {e}") + + def _attempt_reproduction(self, parent_instance): + """ + 尝试繁殖 + """ + try: + # 在父植物附近生成新植物 + parent_pos = parent_instance['position'] + + # 随机生成在附近位置 + offset_range = 5.0 # 5米范围内 + new_x = parent_pos.getX() + random.uniform(-offset_range, offset_range) + new_y = parent_pos.getY() + random.uniform(-offset_range, offset_range) + new_z = parent_pos.getZ() # 高度保持一致 + + # 创建新植物实例 + veg_type_name = parent_instance['type'] + self._create_vegetation_instance(veg_type_name, Point3(new_x, new_y, new_z)) + + except Exception as e: + print(f"尝试繁殖时出错: {e}") + + def save_vegetation_data(self, terrain_info, output_path): + """ + 保存植被数据 + """ + try: + # 收集植被数据 + vegetation_data = { + 'vegetation_types': {}, + 'instances': [], + 'ecosystem_settings': { + 'wind_effect': self.wind_effect + } + } + + # 保存植被类型定义 + for name, veg_type in self.vegetation_types.items(): + vegetation_data['vegetation_types'][name] = { + 'model_path': veg_type['model_path'], + 'scale_range': veg_type['scale_range'], + 'rotation_range': veg_type['rotation_range'], + 'color_variation': veg_type['color_variation'], + 'growth_params': veg_type['growth_params'], + 'ecosystem_role': veg_type['ecosystem_role'] + } + + # 保存实例信息 + for instance_info in self.vegetation_instances: + instance_data = { + 'type': instance_info['type'], + 'position': [instance_info['position'].x, + instance_info['position'].y, + instance_info['position'].z], + 'scale': [instance_info['scale'].x, + instance_info['scale'].y, + instance_info['scale'].z], + 'rotation': instance_info['rotation'], + 'age': instance_info['age'], + 'health': instance_info['health'], + 'growth_stage': instance_info['growth_stage'], + 'creation_time': instance_info['creation_time'] + } + vegetation_data['instances'].append(instance_data) + + # 确保输出目录存在 + output_dir = os.path.dirname(output_path) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + # 写入JSON文件 + with open(output_path, 'w', encoding='utf-8') as f: + json.dump(vegetation_data, f, indent=2, ensure_ascii=False) + + print(f"植被数据保存成功: {output_path}") + return True + + except Exception as e: + print(f"保存植被数据时出错: {e}") + return False + + def load_vegetation_data(self, terrain_info, input_path): + """ + 加载植被数据 + """ + try: + if not os.path.exists(input_path): + print(f"植被数据文件不存在: {input_path}") + return False + + # 读取JSON文件 + with open(input_path, 'r', encoding='utf-8') as f: + vegetation_data = json.load(f) + + # 清除现有植被 + self.clear_all_vegetation() + + # 加载植被类型定义 + if 'vegetation_types' in vegetation_data: + for name, veg_def in vegetation_data['vegetation_types'].items(): + self.define_vegetation_type( + name=name, + model_path=veg_def['model_path'], + scale_range=veg_def.get('scale_range', (1.0, 1.0)), + rotation_range=veg_def.get('rotation_range', (0, 360)), + color_variation=veg_def.get('color_variation', (0.8, 1.2)), + growth_params=veg_def.get('growth_params'), + ecosystem_role=veg_def.get('ecosystem_role', 'general') + ) + + # 加载实例 + if 'instances' in vegetation_data: + for instance_data in vegetation_data['instances']: + position = Point3(instance_data['position'][0], + instance_data['position'][1], + instance_data['position'][2]) + + # 创建实例 + instance_info = self._create_vegetation_instance(instance_data['type'], position) + if instance_info: + # 恢复实例状态 + if 'scale' in instance_data: + scale_vec = instance_data['scale'] + if len(scale_vec) == 3: + instance_info['node'].setScale(scale_vec[0], scale_vec[1], scale_vec[2]) + else: + instance_info['node'].setScale(scale_vec[0]) + + if 'age' in instance_data: + instance_info['age'] = instance_data['age'] + if 'health' in instance_data: + instance_info['health'] = instance_data['health'] + if 'growth_stage' in instance_data: + instance_info['growth_stage'] = instance_data['growth_stage'] + if 'creation_time' in instance_data: + instance_info['creation_time'] = instance_data['creation_time'] + + # 恢复生态系统设置 + if 'ecosystem_settings' in vegetation_data: + eco_settings = vegetation_data['ecosystem_settings'] + if 'wind_effect' in eco_settings: + self.wind_effect = eco_settings['wind_effect'] + + print(f"植被数据加载成功: {input_path}") + return True + + except Exception as e: + print(f"加载植被数据时出错: {e}") + return False + + def get_vegetation_stats(self): + """ + 获取植被统计信息 + """ + stats = { + 'total_instances': len(self.vegetation_instances), + 'vegetation_types': {}, + 'ecosystem_stats': { + 'wind_strength': self.wind_effect['strength'], + 'wind_speed': self.wind_effect['speed'] + }, + 'growth_stages': { + 'seedling': 0, + 'sapling': 0, + 'mature': 0, + 'elderly': 0 + } + } + + # 统计植被类型 + for name, veg_type in self.vegetation_types.items(): + stats['vegetation_types'][name] = { + 'count': len(veg_type['instances']), + 'model_path': veg_type['model_path'], + 'ecosystem_role': veg_type['ecosystem_role'] + } + + # 统计生长阶段 + for instance_info in self.vegetation_instances: + stage = instance_info['growth_stage'] + if stage == 0: + stats['growth_stages']['seedling'] += 1 + elif stage == 1: + stats['growth_stages']['sapling'] += 1 + elif stage == 2: + stats['growth_stages']['mature'] += 1 + elif stage == 3: + stats['growth_stages']['elderly'] += 1 + + return stats + + def paint_vegetation(self, terrain_info, brush_position, brush_radius, + vegetation_type, density=1.0): + """ + 绘制植被(笔刷工具) + """ + try: + terrain_node = terrain_info['node'] + heightfield = terrain_info['heightfield'] + + if not heightfield: + print("无法获取地形高度图数据") + return False + + width = heightfield.getXSize() + height = heightfield.getYSize() + + # 获取地形节点信息 + terrain_pos = terrain_node.getPos() + terrain_scale = terrain_node.getScale() + + # 计算笔刷区域 + center_offset = (width - 1) / 2 + brush_x = int((brush_position.getX() - terrain_pos.getX()) / terrain_scale.getX() + center_offset) + brush_y = int((brush_position.getY() - terrain_pos.getY()) / terrain_scale.getY() + center_offset) + + brush_pixels = int(brush_radius / max(terrain_scale.getX(), terrain_scale.getY())) + + # 在笔刷区域内生成植被 + instances_created = 0 + for dx in range(-brush_pixels, brush_pixels + 1): + for dy in range(-brush_pixels, brush_pixels + 1): + # 检查是否在笔刷范围内 + distance = math.sqrt(dx*dx + dy*dy) + if distance <= brush_pixels: + # 计算实际位置 + x = brush_x + dx + y = brush_y + dy + + # 检查边界 + if 0 <= x < width and 0 <= y < height: + # 根据密度决定是否创建 + if random.random() < density: + # 计算世界坐标 + world_x = (x - center_offset) * terrain_scale.getX() + terrain_pos.getX() + world_y = (y - center_offset) * terrain_scale.getY() + terrain_pos.getY() + world_z = heightfield.getRed(x, y) * terrain_scale.getZ() + terrain_pos.getZ() + + # 创建植被实例 + if self._create_vegetation_instance(vegetation_type, Point3(world_x, world_y, world_z)): + instances_created += 1 + + print(f"绘制植被完成,创建了 {instances_created} 个实例") + return True + + except Exception as e: + print(f"绘制植被时出错: {e}") + return False + + def update_all_systems(self, time_delta): + """ + 更新所有植被系统 + """ + try: + # 更新生长模拟 + self.simulate_growth(time_delta) + + # 更新风力效果 + self.apply_wind_effect() + + # 更新生态系统相互作用 + self.simulate_ecosystem_interactions(time_delta) + + # 更新LOD(如果已设置相机位置) + # 这里需要从外部传入相机位置 + # self.update_vegetation_lod(camera_pos) + + except Exception as e: print(f"更新植被系统时出错: {e}") \ No newline at end of file diff --git a/plugins/user/terrain_editor/water/__init__.py b/plugins/user/terrain_editor/water/__init__.py index fffbd796..04f60320 100644 --- a/plugins/user/terrain_editor/water/__init__.py +++ b/plugins/user/terrain_editor/water/__init__.py @@ -1,3 +1,3 @@ -""" -地形水体系统初始化文件 +""" +地形水体系统初始化文件 """ \ No newline at end of file diff --git a/plugins/user/terrain_editor/water/water_system.py b/plugins/user/terrain_editor/water/water_system.py index c6e5e399..ab2e854d 100644 --- a/plugins/user/terrain_editor/water/water_system.py +++ b/plugins/user/terrain_editor/water/water_system.py @@ -1,935 +1,935 @@ -""" -地形水体系统 -提供完整的水体模拟、渲染、交互、流体动力学等功能 -""" - -from panda3d.core import NodePath, Vec3, Point3, BitMask32 -from panda3d.core import CardMaker, Plane, PlaneNode -from panda3d.core import Texture, TextureStage, Material -from panda3d.core import Shader, ShaderAttrib, Filename -from panda3d.core import PNMImage -import math -import random -import numpy as np - -class WaterSystem: - """ - 地形水体系统类 - 提供水体创建、流体动力学模拟、高级渲染和交互功能 - """ - - def __init__(self, world): - self.world = world - self.water_bodies = [] # 存储水体实例 - self.wave_parameters = { - 'amplitude': 0.5, - 'frequency': 1.0, - 'speed': 1.0, - 'direction': Vec3(1, 0, 0) - } - self.fluid_parameters = { - 'viscosity': 0.01, # 粘度 - 'density': 1000.0, # 密度 - 'surface_tension': 0.07, # 表面张力 - 'gravity': 9.81 # 重力 - } - self.reflection_enabled = True - self.refraction_enabled = True - self.foam_enabled = True - self.caustics_enabled = True # 焦散效果 - self.flow_enabled = True # 水流效果 - - def create_water_body(self, position, size, depth=5.0, water_type='ocean'): - """ - 创建水体 - position: 水体位置 - size: 水体尺寸 (width, height) - depth: 水体深度 - water_type: 水体类型 ('ocean', 'lake', 'river', 'pool') - """ - try: - # 创建水体节点 - water_node = self.world.render.attachNewNode(f"water_body_{len(self.water_bodies)}") - water_node.setPos(position) - - # 创建水面几何体 - cm = CardMaker('water_surface') - cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2) - surface_node = water_node.attachNewNode(cm.generate()) - surface_node.setP(-90) # 使平面水平 - - # 初始化流体动力学网格 - resolution_x = max(32, int(size[0] / 2)) # 根据水体大小调整分辨率 - resolution_y = max(32, int(size[1] / 2)) - - # 创建高度场用于流体模拟 - height_field = np.zeros((resolution_x, resolution_y)) - - # 设置水体属性 - water_info = { - 'node': water_node, - 'surface_node': surface_node, - 'position': position, - 'size': size, - 'depth': depth, - 'type': water_type, - 'waves': [], - 'objects': [], # 水中物体 - 'height_field': height_field, - 'velocity_field': np.zeros((resolution_x, resolution_y, 2)), # 2D速度场 - 'pressure_field': np.zeros((resolution_x, resolution_y)), # 压力场 - 'resolution_x': resolution_x, - 'resolution_y': resolution_y, - 'last_update_time': self.world.globalClock.getFrameTime(), - 'reflection_camera': None, - 'refraction_camera': None - } - - # 应用水面材质和着色器 - self._setup_water_material(water_info) - - # 设置碰撞体 - self._setup_water_collision(water_info) - - # 设置标签 - water_node.setTag("water_type", water_type) - water_node.setTag("is_scene_element", "1") - water_node.setTag("tree_item_type", "WATER_NODE") - - # 添加到水体列表 - self.water_bodies.append(water_info) - - print(f"创建水体: {water_type} at {position} (分辨率: {resolution_x}x{resolution_y})") - return water_info - - except Exception as e: - print(f"创建水体时出错: {e}") - return None - - def _setup_water_material(self, water_info): - """ - 设置水体材质和高级着色器 - """ - try: - surface_node = water_info['surface_node'] - - # 创建水体材质 - water_material = Material() - water_material.setAmbient((0.1, 0.1, 0.3, 1.0)) # 深蓝色环境光 - water_material.setDiffuse((0.2, 0.2, 0.8, 0.8)) # 半透明蓝色漫反射 - water_material.setSpecular((0.8, 0.8, 0.9, 1.0)) # 白色高光 - water_material.setShininess(100.0) # 高光强度 - water_material.setEmission((0.0, 0.05, 0.1, 1.0)) # 微弱自发光 - - surface_node.setMaterial(water_material) - - # 启用透明度 - surface_node.setTransparency(True) - - # 创建水体法线贴图用于更好的光照效果 - self._create_water_normal_map(water_info) - - # 如果支持RenderPipeline,应用高级水体着色器 - if hasattr(self.world, 'render_pipeline'): - try: - effect_options = { - "normal_mapping": True, - "reflection": self.reflection_enabled, - "refraction": self.refraction_enabled, - "foam": self.foam_enabled, - "caustics": self.caustics_enabled, - "wave_frequency": self.wave_parameters['frequency'], - "wave_amplitude": self.wave_parameters['amplitude'], - "wave_speed": self.wave_parameters['speed'] - } - - self.world.render_pipeline.set_effect( - surface_node, - "effects/water_advanced.yaml", - effect_options, - 20 # 排序值,确保水体在适当层级渲染 - ) - except Exception as e: - print(f"应用高级水体着色器失败: {e}") - # 回退到基本着色器 - self._setup_basic_water_shader(surface_node) - else: - # 使用基本的水体着色器 - self._setup_basic_water_shader(surface_node) - - except Exception as e: - print(f"设置水体材质时出错: {e}") - - def _create_water_normal_map(self, water_info): - """ - 创建水体法线贴图 - """ - try: - # 创建法线贴图用于更好的光照效果 - size_x, size_y = water_info['resolution_x'], water_info['resolution_y'] - - # 创建法线贴图 - normal_map = PNMImage(size_x, size_y, 3) # RGB格式 - normal_map.fill(0.5, 0.5, 1.0) # 初始法线为(0,0,1) - - # 保存法线贴图 - water_info['normal_map'] = normal_map - - # 应用法线贴图到材质 - surface_node = water_info['surface_node'] - if 'normal_map' in water_info: - # 这里可以应用法线贴图到纹理阶段 - pass - - except Exception as e: - print(f"创建水体法线贴图时出错: {e}") - - def _setup_basic_water_shader(self, surface_node): - """ - 设置高级水体着色器(包含流体效果) - """ - try: - # 创建高级水体着色器 - vertex_shader = """ - #version 130 - - uniform float time; - uniform float wave_amplitude; - uniform float wave_frequency; - uniform float wave_speed; - uniform vec2 wave_direction; - - varying vec3 v_normal; - varying vec3 v_world_pos; - varying vec2 v_uv; - - void main() { - vec4 pos = gl_Vertex; - v_uv = gl_MultiTexCoord0.xy; - - // 添加多层波浪效果 - float wave1 = sin(pos.x * wave_frequency + time * wave_speed) * - cos(pos.y * wave_frequency + time * wave_speed) * - wave_amplitude; - - float wave2 = sin(pos.x * wave_frequency * 2.0 + time * wave_speed * 1.5) * - cos(pos.y * wave_frequency * 1.5 + time * wave_speed * 1.2) * - wave_amplitude * 0.5; - - float wave3 = sin(pos.x * wave_frequency * 0.5 + pos.y * wave_frequency * 0.5 + time * wave_speed * 0.8) * - wave_amplitude * 0.3; - - pos.z += wave1 + wave2 + wave3; - - gl_Position = gl_ModelViewProjectionMatrix * pos; - v_normal = gl_NormalMatrix * gl_Normal; - v_world_pos = (gl_ModelViewMatrix * pos).xyz; - } - """ - - fragment_shader = """ - #version 130 - - uniform float time; - uniform sampler2D normal_map; - uniform bool use_foam; - uniform bool use_caustics; - - varying vec3 v_normal; - varying vec3 v_world_pos; - varying vec2 v_uv; - - void main() { - // 基础水体颜色 - vec3 water_color = vec3(0.1, 0.3, 0.6); - - // 添加深度颜色变化 - float depth_factor = clamp(v_world_pos.z * 0.1, 0.0, 1.0); - vec3 deep_color = vec3(0.05, 0.15, 0.3); - water_color = mix(water_color, deep_color, depth_factor); - - // 计算光照 - vec3 light_dir = normalize(vec3(0.5, 0.5, 0.7)); - vec3 normal = normalize(v_normal); - float diffuse = max(dot(normal, light_dir), 0.0); - - // 添加高光 - vec3 view_dir = normalize(-v_world_pos); - vec3 reflect_dir = reflect(-light_dir, normal); - float spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); - - // 泡沫效果 - float foam = 0.0; - if (use_foam) { - foam = sin(v_uv.x * 20.0 + time) * sin(v_uv.y * 20.0 + time) * 0.5 + 0.5; - foam = pow(foam, 8.0); - } - - // 焦散效果 - float caustics = 0.0; - if (use_caustics) { - caustics = sin(v_uv.x * 10.0 + time * 2.0) * sin(v_uv.y * 10.0 + time * 2.0); - caustics = caustics * 0.3 + 0.3; - } - - vec3 result = water_color * (0.3 + 0.7 * diffuse) + vec3(0.8) * spec; - result += vec3(foam * 0.8); - result += vec3(caustics * 0.4); - - // 设置透明度 - float alpha = 0.7 + foam * 0.3; - gl_FragColor = vec4(result, alpha); - } - """ - - # 创建着色器 - shader = Shader.make(Shader.SL_GLSL, vertex_shader, fragment_shader) - if shader: - surface_node.setShader(shader) - - # 设置着色器参数 - surface_node.setShaderInput("wave_amplitude", self.wave_parameters['amplitude']) - surface_node.setShaderInput("wave_frequency", self.wave_parameters['frequency']) - surface_node.setShaderInput("wave_speed", self.wave_parameters['speed']) - surface_node.setShaderInput("wave_direction", self.wave_parameters['direction']) - surface_node.setShaderInput("use_foam", self.foam_enabled) - surface_node.setShaderInput("use_caustics", self.caustics_enabled) - - except Exception as e: - print(f"设置高级水体着色器时出错: {e}") - - def _setup_water_collision(self, water_info): - """ - 设置水体碰撞体 - """ - try: - water_node = water_info['node'] - size = water_info['size'] - - # 创建碰撞平面 - plane = Plane(Vec3(0, 0, 1), Point3(0, 0, 0)) - plane_node = PlaneNode('water_collision', plane) - collision_node = water_node.attachNewNode(plane_node) - - # 设置碰撞掩码 - collision_node.setCollideMask(BitMask32.bit(4)) # 使用第4位作为水体碰撞掩码 - - # 保存碰撞节点 - water_info['collision_node'] = collision_node - - except Exception as e: - print(f"设置水体碰撞体时出错: {e}") - - def update_water_animation(self, time_delta): - """ - 更新水体动画和流体模拟 - """ - try: - current_time = self.world.globalClock.getFrameTime() - - for water_info in self.water_bodies: - surface_node = water_info['surface_node'] - - # 更新着色器时间参数 - if surface_node.hasShader(): - surface_node.setShaderInput("time", current_time) - - # 更新流体动力学 - self._update_fluid_dynamics(water_info, time_delta) - - # 更新波浪效果 - self._update_water_waves(water_info, current_time) - - # 更新反射和折射效果 - self._update_water_reflection(water_info) - - except Exception as e: - print(f"更新水体动画时出错: {e}") - - def _update_fluid_dynamics(self, water_info, time_delta): - """ - 更新流体动力学模拟 - """ - try: - # Navier-Stokes方程的简化实现 - height_field = water_info['height_field'] - velocity_field = water_info['velocity_field'] - pressure_field = water_info['pressure_field'] - resolution_x = water_info['resolution_x'] - resolution_y = water_info['resolution_y'] - - # 获取流体参数 - viscosity = self.fluid_parameters['viscosity'] - gravity = self.fluid_parameters['gravity'] - - # 更新速度场 - for i in range(1, resolution_x - 1): - for j in range(1, resolution_y - 1): - # 粘性扩散 - laplacian_u = (velocity_field[i+1, j, 0] + velocity_field[i-1, j, 0] + - velocity_field[i, j+1, 0] + velocity_field[i, j-1, 0] - - 4 * velocity_field[i, j, 0]) - laplacian_v = (velocity_field[i+1, j, 1] + velocity_field[i-1, j, 1] + - velocity_field[i, j+1, 1] + velocity_field[i, j-1, 1] - - 4 * velocity_field[i, j, 1]) - - # 更新速度 - velocity_field[i, j, 0] += viscosity * laplacian_u * time_delta - velocity_field[i, j, 1] += viscosity * laplacian_v * time_delta - - # 重力影响 - velocity_field[i, j, 1] -= gravity * time_delta - - # 更新高度场 - for i in range(1, resolution_x - 1): - for j in range(1, resolution_y - 1): - # 连续性方程 - divergence = (velocity_field[i+1, j, 0] - velocity_field[i-1, j, 0] + - velocity_field[i, j+1, 1] - velocity_field[i, j-1, 1]) * 0.5 - - # 更新高度 - height_field[i, j] -= divergence * time_delta - - # 应用边界条件 - self._apply_boundary_conditions(water_info) - - # 更新水体表面几何 - self._update_water_surface_geometry(water_info) - - except Exception as e: - print(f"更新流体动力学时出错: {e}") - - def _apply_boundary_conditions(self, water_info): - """ - 应用流体边界条件 - """ - try: - height_field = water_info['height_field'] - velocity_field = water_info['velocity_field'] - resolution_x = water_info['resolution_x'] - resolution_y = water_info['resolution_y'] - - # 固定边界条件:边缘速度为0 - velocity_field[0, :, :] = 0 - velocity_field[resolution_x-1, :, :] = 0 - velocity_field[:, 0, :] = 0 - velocity_field[:, resolution_y-1, :] = 0 - - # 高度场边界反射 - height_field[0, :] = height_field[1, :] - height_field[resolution_x-1, :] = height_field[resolution_x-2, :] - height_field[:, 0] = height_field[:, 1] - height_field[:, resolution_y-1] = height_field[:, resolution_y-2] - - except Exception as e: - print(f"应用边界条件时出错: {e}") - - def _update_water_surface_geometry(self, water_info): - """ - 更新水体表面几何形状 - """ - try: - # 这里可以实现动态更新水体表面顶点位置 - # 基于高度场数据调整顶点Z坐标 - pass - - except Exception as e: - print(f"更新水体表面几何时出错: {e}") - - def _update_water_waves(self, water_info, current_time): - """ - 更新水体波浪(多层波浪系统) - """ - try: - # 生成更复杂的波浪系统 - wave_system = water_info.get('wave_system', []) - - # 如果还没有初始化波浪系统,创建多层波浪 - if not wave_system: - wave_system = [] - # 创建不同频率和振幅的波浪层 - for i in range(5): # 5层波浪 - wave = { - 'frequency': self.wave_parameters['frequency'] * (0.5 + i * 0.5), - 'amplitude': self.wave_parameters['amplitude'] * (1.0 - i * 0.15), - 'speed': self.wave_parameters['speed'] * (0.8 + i * 0.2), - 'direction': Vec3(random.uniform(-1, 1), random.uniform(-1, 1), 0).normalized() - } - wave_system.append(wave) - water_info['wave_system'] = wave_system - - # 更新波浪参数 - water_info['waves'] = wave_system - - except Exception as e: - print(f"更新水体波浪时出错: {e}") - - def _update_water_reflection(self, water_info): - """ - 更新水体反射效果 - """ - try: - if not self.reflection_enabled: - return - - # 这里可以实现反射相机的更新逻辑 - # 包括相机位置的镜像计算等 - pass - - except Exception as e: - print(f"更新水体反射时出错: {e}") - - def set_wave_parameters(self, amplitude=None, frequency=None, speed=None, direction=None): - """ - 设置波浪参数 - """ - if amplitude is not None: - self.wave_parameters['amplitude'] = amplitude - if frequency is not None: - self.wave_parameters['frequency'] = frequency - if speed is not None: - self.wave_parameters['speed'] = speed - if direction is not None: - self.wave_parameters['direction'] = direction - - # 更新所有水体的着色器参数 - for water_info in self.water_bodies: - surface_node = water_info['surface_node'] - if surface_node.hasShader(): - if amplitude is not None: - surface_node.setShaderInput("wave_amplitude", amplitude) - if frequency is not None: - surface_node.setShaderInput("wave_frequency", frequency) - if speed is not None: - surface_node.setShaderInput("wave_speed", speed) - if direction is not None: - surface_node.setShaderInput("wave_direction", direction) - - print(f"更新波浪参数: {self.wave_parameters}") - - def set_fluid_parameters(self, viscosity=None, density=None, surface_tension=None, gravity=None): - """ - 设置流体参数 - """ - if viscosity is not None: - self.fluid_parameters['viscosity'] = viscosity - if density is not None: - self.fluid_parameters['density'] = density - if surface_tension is not None: - self.fluid_parameters['surface_tension'] = surface_tension - if gravity is not None: - self.fluid_parameters['gravity'] = gravity - - print(f"更新流体参数: {self.fluid_parameters}") - - def simulate_water_physics(self, time_delta): - """ - 模拟水体物理效果 - """ - try: - for water_info in self.water_bodies: - # 模拟水流 - self._simulate_water_flow(water_info, time_delta) - - # 模拟水花 - self._simulate_water_splash(water_info, time_delta) - - # 模拟波纹 - self._simulate_water_ripples(water_info, time_delta) - - except Exception as e: - print(f"模拟水体物理时出错: {e}") - - def _simulate_water_flow(self, water_info, time_delta): - """ - 模拟水流效果 - """ - try: - if not self.flow_enabled: - return - - # 模拟水流方向和速度 - # 可以基于地形坡度或外力计算水流 - pass - - except Exception as e: - print(f"模拟水流效果时出错: {e}") - - def _simulate_water_splash(self, water_info, time_delta): - """ - 模拟水花效果 - """ - try: - # 模拟物体落入水中时产生的水花效果 - # 可以创建粒子系统或动态几何体 - pass - - except Exception as e: - print(f"模拟水花效果时出错: {e}") - - def _simulate_water_ripples(self, water_info, time_delta): - """ - 模拟波纹效果 - """ - try: - # 模拟水面波纹传播 - # 基于波动方程的简化实现 - pass - - except Exception as e: - print(f"模拟波纹效果时出错: {e}") - - def add_object_to_water(self, water_info, object_node, buoyancy=1.0): - """ - 将物体添加到水中 - """ - try: - # 计算物体的水中位置 - water_pos = object_node.getPos(water_info['node']) - - object_info = { - 'node': object_node, - 'buoyancy': buoyancy, - 'water_position': water_pos, - 'velocity': Vec3(0, 0, 0), - 'volume': self._calculate_object_volume(object_node), - 'density': 1000.0, # 默认密度 - 'submerged_volume': 0.0, - 'drag_coefficient': 0.5 - } - - water_info['objects'].append(object_info) - print(f"物体已添加到水中: {object_node.getName()}") - return object_info - - except Exception as e: - print(f"添加物体到水中时出错: {e}") - return None - - def _calculate_object_volume(self, object_node): - """ - 计算物体体积(简化实现) - """ - try: - # 获取物体包围盒 - bounds = object_node.getBounds() - if hasattr(bounds, 'getRadius'): - # 简化为球体体积计算 - radius = bounds.getRadius() - volume = (4.0/3.0) * math.pi * (radius ** 3) - return volume - return 1.0 # 默认体积 - except: - return 1.0 - - def update_water_physics(self, time_delta): - """ - 更新水中物体的物理效果 - """ - try: - for water_info in self.water_bodies: - water_height = water_info['position'].getZ() - fluid_density = self.fluid_parameters['density'] - - for object_info in water_info['objects']: - object_node = object_info['node'] - if object_node and not object_node.isEmpty(): - # 获取物体位置 - object_pos = object_node.getPos() - - # 计算浸入水中的体积 - object_bounds = object_node.getBounds() - object_bottom = object_pos.getZ() - object_bounds.getRadius() - object_top = object_pos.getZ() + object_bounds.getRadius() - - if object_bottom < water_height and object_top > water_height: - # 部分浸入水中 - submerged_ratio = (water_height - object_bottom) / (object_top - object_bottom) - submerged_volume = object_info['volume'] * min(1.0, max(0.0, submerged_ratio)) - elif object_top <= water_height: - # 完全浸入水中 - submerged_volume = object_info['volume'] - else: - # 完全在水上 - submerged_volume = 0.0 - - object_info['submerged_volume'] = submerged_volume - - # 计算浮力 - buoyancy_force = Vec3(0, 0, submerged_volume * fluid_density * self.fluid_parameters['gravity']) - - # 应用浮力 - object_info['velocity'] += buoyancy_force * time_delta / object_info['density'] - - # 计算阻力 - drag_force = -object_info['velocity'] * object_info['drag_coefficient'] * submerged_volume - object_info['velocity'] += drag_force * time_delta / object_info['density'] - - # 更新物体位置 - new_pos = object_pos + object_info['velocity'] * time_delta - object_node.setPos(new_pos) - - # 应用阻尼 - object_info['velocity'] *= 0.98 - - except Exception as e: - print(f"更新水中物理时出错: {e}") - - def create_water_foam(self, water_info, position, intensity=1.0): - """ - 创建水花泡沫效果 - """ - try: - # 创建泡沫粒子系统 - print(f"在位置 {position} 创建水花泡沫 (强度: {intensity})") - - # 这里可以实现创建粒子系统来模拟泡沫 - # 包括泡沫的生命周期、扩散、消散等效果 - return True - - except Exception as e: - print(f"创建水花泡沫时出错: {e}") - return False - - def create_water_caustics(self, water_info, light_direction): - """ - 创建焦散效果 - """ - try: - if not self.caustics_enabled: - return False - - # 创建焦散纹理或几何体 - print(f"创建焦散效果 (光照方向: {light_direction})") - - # 这里可以实现焦散效果的生成 - # 包括光线追踪、纹理投影等技术 - return True - - except Exception as e: - print(f"创建焦散效果时出错: {e}") - return False - - def set_water_quality(self, quality_level): - """ - 设置水体质量等级 - """ - try: - # 根据质量等级调整水体效果 - if quality_level == "low": - self.reflection_enabled = False - self.refraction_enabled = False - self.foam_enabled = False - self.caustics_enabled = False - self.flow_enabled = False - elif quality_level == "medium": - self.reflection_enabled = True - self.refraction_enabled = False - self.foam_enabled = True - self.caustics_enabled = False - self.flow_enabled = True - elif quality_level == "high": - self.reflection_enabled = True - self.refraction_enabled = True - self.foam_enabled = True - self.caustics_enabled = True - self.flow_enabled = True - - # 更新所有水体 - for water_info in self.water_bodies: - self._setup_water_material(water_info) - - print(f"水体质量设置为: {quality_level}") - - except Exception as e: - print(f"设置水体质量时出错: {e}") - - def get_water_stats(self): - """ - 获取水体统计信息 - """ - stats = { - 'total_bodies': len(self.water_bodies), - 'total_surface_area': 0, - 'water_types': {}, - 'fluid_stats': self.fluid_parameters, - 'quality_settings': { - 'reflection': self.reflection_enabled, - 'refraction': self.refraction_enabled, - 'foam': self.foam_enabled, - 'caustics': self.caustics_enabled - } - } - - for water_info in self.water_bodies: - # 计算表面积 - area = water_info['size'][0] * water_info['size'][1] - stats['total_surface_area'] += area - - # 统计水体类型 - water_type = water_info['type'] - if water_type in stats['water_types']: - stats['water_types'][water_type] += 1 - else: - stats['water_types'][water_type] = 1 - - # 添加流体分辨率信息 - stats[f"water_body_{len(stats['water_types'])}_resolution"] = ( - water_info['resolution_x'], - water_info['resolution_y'] - ) - - return stats - - def remove_water_body(self, water_info): - """ - 移除水体 - """ - try: - if water_info in self.water_bodies: - # 移除节点 - if water_info['node'] and not water_info['node'].isEmpty(): - water_info['node'].removeNode() - - # 从列表中移除 - self.water_bodies.remove(water_info) - - print("水体已移除") - return True - - except Exception as e: - print(f"移除水体时出错: {e}") - return False - - def clear_all_water(self): - """ - 清除所有水体 - """ - try: - for water_info in self.water_bodies[:]: # 使用切片复制列表 - self.remove_water_body(water_info) - - print("所有水体已清除") - return True - - except Exception as e: - print(f"清除所有水体时出错: {e}") - return False - - def save_water_data(self, output_path): - """ - 保存水体数据 - """ - try: - import json - import os - - # 收集水体数据 - water_data = { - 'bodies': [], - 'wave_parameters': self.wave_parameters, - 'fluid_parameters': self.fluid_parameters, - 'quality_settings': { - 'reflection_enabled': self.reflection_enabled, - 'refraction_enabled': self.refraction_enabled, - 'foam_enabled': self.foam_enabled, - 'caustics_enabled': self.caustics_enabled - } - } - - for water_info in self.water_bodies: - body_data = { - 'position': [water_info['position'].x, - water_info['position'].y, - water_info['position'].z], - 'size': water_info['size'], - 'depth': water_info['depth'], - 'type': water_info['type'], - 'resolution_x': water_info['resolution_x'], - 'resolution_y': water_info['resolution_y'] - } - water_data['bodies'].append(body_data) - - # 确保输出目录存在 - output_dir = os.path.dirname(output_path) - if not os.path.exists(output_dir): - os.makedirs(output_dir) - - # 写入JSON文件 - with open(output_path, 'w', encoding='utf-8') as f: - json.dump(water_data, f, indent=2, ensure_ascii=False) - - print(f"水体数据保存成功: {output_path}") - return True - - except Exception as e: - print(f"保存水体数据时出错: {e}") - return False - - def load_water_data(self, input_path): - """ - 加载水体数据 - """ - try: - import json - import os - - if not os.path.exists(input_path): - print(f"水体数据文件不存在: {input_path}") - return False - - # 读取JSON文件 - with open(input_path, 'r', encoding='utf-8') as f: - water_data = json.load(f) - - # 清除现有水体 - self.clear_all_water() - - # 恢复设置 - if 'wave_parameters' in water_data: - self.wave_parameters = water_data['wave_parameters'] - if 'fluid_parameters' in water_data: - self.fluid_parameters = water_data['fluid_parameters'] - if 'quality_settings' in water_data: - qs = water_data['quality_settings'] - self.reflection_enabled = qs.get('reflection_enabled', True) - self.refraction_enabled = qs.get('refraction_enabled', True) - self.foam_enabled = qs.get('foam_enabled', True) - self.caustics_enabled = qs.get('caustics_enabled', True) - - # 创建水体 - for body_data in water_data['bodies']: - position = Point3(body_data['position'][0], - body_data['position'][1], - body_data['position'][2]) - - self.create_water_body( - position=position, - size=body_data['size'], - depth=body_data['depth'], - water_type=body_data['type'] - ) - - print(f"水体数据加载成功: {input_path}") - return True - - except Exception as e: - print(f"加载水体数据时出错: {e}") - return False - - def update_all_water_systems(self, time_delta): - """ - 更新所有水体系统 - """ - try: - # 更新水体动画 - self.update_water_animation(time_delta) - - # 更新水体物理 - self.update_water_physics(time_delta) - - # 更新流体动力学 - self.simulate_water_physics(time_delta) - - except Exception as e: +""" +地形水体系统 +提供完整的水体模拟、渲染、交互、流体动力学等功能 +""" + +from panda3d.core import NodePath, Vec3, Point3, BitMask32 +from panda3d.core import CardMaker, Plane, PlaneNode +from panda3d.core import Texture, TextureStage, Material +from panda3d.core import Shader, ShaderAttrib, Filename +from panda3d.core import PNMImage +import math +import random +import numpy as np + +class WaterSystem: + """ + 地形水体系统类 + 提供水体创建、流体动力学模拟、高级渲染和交互功能 + """ + + def __init__(self, world): + self.world = world + self.water_bodies = [] # 存储水体实例 + self.wave_parameters = { + 'amplitude': 0.5, + 'frequency': 1.0, + 'speed': 1.0, + 'direction': Vec3(1, 0, 0) + } + self.fluid_parameters = { + 'viscosity': 0.01, # 粘度 + 'density': 1000.0, # 密度 + 'surface_tension': 0.07, # 表面张力 + 'gravity': 9.81 # 重力 + } + self.reflection_enabled = True + self.refraction_enabled = True + self.foam_enabled = True + self.caustics_enabled = True # 焦散效果 + self.flow_enabled = True # 水流效果 + + def create_water_body(self, position, size, depth=5.0, water_type='ocean'): + """ + 创建水体 + position: 水体位置 + size: 水体尺寸 (width, height) + depth: 水体深度 + water_type: 水体类型 ('ocean', 'lake', 'river', 'pool') + """ + try: + # 创建水体节点 + water_node = self.world.render.attachNewNode(f"water_body_{len(self.water_bodies)}") + water_node.setPos(position) + + # 创建水面几何体 + cm = CardMaker('water_surface') + cm.setFrame(-size[0]/2, size[0]/2, -size[1]/2, size[1]/2) + surface_node = water_node.attachNewNode(cm.generate()) + surface_node.setP(-90) # 使平面水平 + + # 初始化流体动力学网格 + resolution_x = max(32, int(size[0] / 2)) # 根据水体大小调整分辨率 + resolution_y = max(32, int(size[1] / 2)) + + # 创建高度场用于流体模拟 + height_field = np.zeros((resolution_x, resolution_y)) + + # 设置水体属性 + water_info = { + 'node': water_node, + 'surface_node': surface_node, + 'position': position, + 'size': size, + 'depth': depth, + 'type': water_type, + 'waves': [], + 'objects': [], # 水中物体 + 'height_field': height_field, + 'velocity_field': np.zeros((resolution_x, resolution_y, 2)), # 2D速度场 + 'pressure_field': np.zeros((resolution_x, resolution_y)), # 压力场 + 'resolution_x': resolution_x, + 'resolution_y': resolution_y, + 'last_update_time': self.world.globalClock.getFrameTime(), + 'reflection_camera': None, + 'refraction_camera': None + } + + # 应用水面材质和着色器 + self._setup_water_material(water_info) + + # 设置碰撞体 + self._setup_water_collision(water_info) + + # 设置标签 + water_node.setTag("water_type", water_type) + water_node.setTag("is_scene_element", "1") + water_node.setTag("tree_item_type", "WATER_NODE") + + # 添加到水体列表 + self.water_bodies.append(water_info) + + print(f"创建水体: {water_type} at {position} (分辨率: {resolution_x}x{resolution_y})") + return water_info + + except Exception as e: + print(f"创建水体时出错: {e}") + return None + + def _setup_water_material(self, water_info): + """ + 设置水体材质和高级着色器 + """ + try: + surface_node = water_info['surface_node'] + + # 创建水体材质 + water_material = Material() + water_material.setAmbient((0.1, 0.1, 0.3, 1.0)) # 深蓝色环境光 + water_material.setDiffuse((0.2, 0.2, 0.8, 0.8)) # 半透明蓝色漫反射 + water_material.setSpecular((0.8, 0.8, 0.9, 1.0)) # 白色高光 + water_material.setShininess(100.0) # 高光强度 + water_material.setEmission((0.0, 0.05, 0.1, 1.0)) # 微弱自发光 + + surface_node.setMaterial(water_material) + + # 启用透明度 + surface_node.setTransparency(True) + + # 创建水体法线贴图用于更好的光照效果 + self._create_water_normal_map(water_info) + + # 如果支持RenderPipeline,应用高级水体着色器 + if hasattr(self.world, 'render_pipeline'): + try: + effect_options = { + "normal_mapping": True, + "reflection": self.reflection_enabled, + "refraction": self.refraction_enabled, + "foam": self.foam_enabled, + "caustics": self.caustics_enabled, + "wave_frequency": self.wave_parameters['frequency'], + "wave_amplitude": self.wave_parameters['amplitude'], + "wave_speed": self.wave_parameters['speed'] + } + + self.world.render_pipeline.set_effect( + surface_node, + "effects/water_advanced.yaml", + effect_options, + 20 # 排序值,确保水体在适当层级渲染 + ) + except Exception as e: + print(f"应用高级水体着色器失败: {e}") + # 回退到基本着色器 + self._setup_basic_water_shader(surface_node) + else: + # 使用基本的水体着色器 + self._setup_basic_water_shader(surface_node) + + except Exception as e: + print(f"设置水体材质时出错: {e}") + + def _create_water_normal_map(self, water_info): + """ + 创建水体法线贴图 + """ + try: + # 创建法线贴图用于更好的光照效果 + size_x, size_y = water_info['resolution_x'], water_info['resolution_y'] + + # 创建法线贴图 + normal_map = PNMImage(size_x, size_y, 3) # RGB格式 + normal_map.fill(0.5, 0.5, 1.0) # 初始法线为(0,0,1) + + # 保存法线贴图 + water_info['normal_map'] = normal_map + + # 应用法线贴图到材质 + surface_node = water_info['surface_node'] + if 'normal_map' in water_info: + # 这里可以应用法线贴图到纹理阶段 + pass + + except Exception as e: + print(f"创建水体法线贴图时出错: {e}") + + def _setup_basic_water_shader(self, surface_node): + """ + 设置高级水体着色器(包含流体效果) + """ + try: + # 创建高级水体着色器 + vertex_shader = """ + #version 130 + + uniform float time; + uniform float wave_amplitude; + uniform float wave_frequency; + uniform float wave_speed; + uniform vec2 wave_direction; + + varying vec3 v_normal; + varying vec3 v_world_pos; + varying vec2 v_uv; + + void main() { + vec4 pos = gl_Vertex; + v_uv = gl_MultiTexCoord0.xy; + + // 添加多层波浪效果 + float wave1 = sin(pos.x * wave_frequency + time * wave_speed) * + cos(pos.y * wave_frequency + time * wave_speed) * + wave_amplitude; + + float wave2 = sin(pos.x * wave_frequency * 2.0 + time * wave_speed * 1.5) * + cos(pos.y * wave_frequency * 1.5 + time * wave_speed * 1.2) * + wave_amplitude * 0.5; + + float wave3 = sin(pos.x * wave_frequency * 0.5 + pos.y * wave_frequency * 0.5 + time * wave_speed * 0.8) * + wave_amplitude * 0.3; + + pos.z += wave1 + wave2 + wave3; + + gl_Position = gl_ModelViewProjectionMatrix * pos; + v_normal = gl_NormalMatrix * gl_Normal; + v_world_pos = (gl_ModelViewMatrix * pos).xyz; + } + """ + + fragment_shader = """ + #version 130 + + uniform float time; + uniform sampler2D normal_map; + uniform bool use_foam; + uniform bool use_caustics; + + varying vec3 v_normal; + varying vec3 v_world_pos; + varying vec2 v_uv; + + void main() { + // 基础水体颜色 + vec3 water_color = vec3(0.1, 0.3, 0.6); + + // 添加深度颜色变化 + float depth_factor = clamp(v_world_pos.z * 0.1, 0.0, 1.0); + vec3 deep_color = vec3(0.05, 0.15, 0.3); + water_color = mix(water_color, deep_color, depth_factor); + + // 计算光照 + vec3 light_dir = normalize(vec3(0.5, 0.5, 0.7)); + vec3 normal = normalize(v_normal); + float diffuse = max(dot(normal, light_dir), 0.0); + + // 添加高光 + vec3 view_dir = normalize(-v_world_pos); + vec3 reflect_dir = reflect(-light_dir, normal); + float spec = pow(max(dot(view_dir, reflect_dir), 0.0), 32.0); + + // 泡沫效果 + float foam = 0.0; + if (use_foam) { + foam = sin(v_uv.x * 20.0 + time) * sin(v_uv.y * 20.0 + time) * 0.5 + 0.5; + foam = pow(foam, 8.0); + } + + // 焦散效果 + float caustics = 0.0; + if (use_caustics) { + caustics = sin(v_uv.x * 10.0 + time * 2.0) * sin(v_uv.y * 10.0 + time * 2.0); + caustics = caustics * 0.3 + 0.3; + } + + vec3 result = water_color * (0.3 + 0.7 * diffuse) + vec3(0.8) * spec; + result += vec3(foam * 0.8); + result += vec3(caustics * 0.4); + + // 设置透明度 + float alpha = 0.7 + foam * 0.3; + gl_FragColor = vec4(result, alpha); + } + """ + + # 创建着色器 + shader = Shader.make(Shader.SL_GLSL, vertex_shader, fragment_shader) + if shader: + surface_node.setShader(shader) + + # 设置着色器参数 + surface_node.setShaderInput("wave_amplitude", self.wave_parameters['amplitude']) + surface_node.setShaderInput("wave_frequency", self.wave_parameters['frequency']) + surface_node.setShaderInput("wave_speed", self.wave_parameters['speed']) + surface_node.setShaderInput("wave_direction", self.wave_parameters['direction']) + surface_node.setShaderInput("use_foam", self.foam_enabled) + surface_node.setShaderInput("use_caustics", self.caustics_enabled) + + except Exception as e: + print(f"设置高级水体着色器时出错: {e}") + + def _setup_water_collision(self, water_info): + """ + 设置水体碰撞体 + """ + try: + water_node = water_info['node'] + size = water_info['size'] + + # 创建碰撞平面 + plane = Plane(Vec3(0, 0, 1), Point3(0, 0, 0)) + plane_node = PlaneNode('water_collision', plane) + collision_node = water_node.attachNewNode(plane_node) + + # 设置碰撞掩码 + collision_node.setCollideMask(BitMask32.bit(4)) # 使用第4位作为水体碰撞掩码 + + # 保存碰撞节点 + water_info['collision_node'] = collision_node + + except Exception as e: + print(f"设置水体碰撞体时出错: {e}") + + def update_water_animation(self, time_delta): + """ + 更新水体动画和流体模拟 + """ + try: + current_time = self.world.globalClock.getFrameTime() + + for water_info in self.water_bodies: + surface_node = water_info['surface_node'] + + # 更新着色器时间参数 + if surface_node.hasShader(): + surface_node.setShaderInput("time", current_time) + + # 更新流体动力学 + self._update_fluid_dynamics(water_info, time_delta) + + # 更新波浪效果 + self._update_water_waves(water_info, current_time) + + # 更新反射和折射效果 + self._update_water_reflection(water_info) + + except Exception as e: + print(f"更新水体动画时出错: {e}") + + def _update_fluid_dynamics(self, water_info, time_delta): + """ + 更新流体动力学模拟 + """ + try: + # Navier-Stokes方程的简化实现 + height_field = water_info['height_field'] + velocity_field = water_info['velocity_field'] + pressure_field = water_info['pressure_field'] + resolution_x = water_info['resolution_x'] + resolution_y = water_info['resolution_y'] + + # 获取流体参数 + viscosity = self.fluid_parameters['viscosity'] + gravity = self.fluid_parameters['gravity'] + + # 更新速度场 + for i in range(1, resolution_x - 1): + for j in range(1, resolution_y - 1): + # 粘性扩散 + laplacian_u = (velocity_field[i+1, j, 0] + velocity_field[i-1, j, 0] + + velocity_field[i, j+1, 0] + velocity_field[i, j-1, 0] - + 4 * velocity_field[i, j, 0]) + laplacian_v = (velocity_field[i+1, j, 1] + velocity_field[i-1, j, 1] + + velocity_field[i, j+1, 1] + velocity_field[i, j-1, 1] - + 4 * velocity_field[i, j, 1]) + + # 更新速度 + velocity_field[i, j, 0] += viscosity * laplacian_u * time_delta + velocity_field[i, j, 1] += viscosity * laplacian_v * time_delta + + # 重力影响 + velocity_field[i, j, 1] -= gravity * time_delta + + # 更新高度场 + for i in range(1, resolution_x - 1): + for j in range(1, resolution_y - 1): + # 连续性方程 + divergence = (velocity_field[i+1, j, 0] - velocity_field[i-1, j, 0] + + velocity_field[i, j+1, 1] - velocity_field[i, j-1, 1]) * 0.5 + + # 更新高度 + height_field[i, j] -= divergence * time_delta + + # 应用边界条件 + self._apply_boundary_conditions(water_info) + + # 更新水体表面几何 + self._update_water_surface_geometry(water_info) + + except Exception as e: + print(f"更新流体动力学时出错: {e}") + + def _apply_boundary_conditions(self, water_info): + """ + 应用流体边界条件 + """ + try: + height_field = water_info['height_field'] + velocity_field = water_info['velocity_field'] + resolution_x = water_info['resolution_x'] + resolution_y = water_info['resolution_y'] + + # 固定边界条件:边缘速度为0 + velocity_field[0, :, :] = 0 + velocity_field[resolution_x-1, :, :] = 0 + velocity_field[:, 0, :] = 0 + velocity_field[:, resolution_y-1, :] = 0 + + # 高度场边界反射 + height_field[0, :] = height_field[1, :] + height_field[resolution_x-1, :] = height_field[resolution_x-2, :] + height_field[:, 0] = height_field[:, 1] + height_field[:, resolution_y-1] = height_field[:, resolution_y-2] + + except Exception as e: + print(f"应用边界条件时出错: {e}") + + def _update_water_surface_geometry(self, water_info): + """ + 更新水体表面几何形状 + """ + try: + # 这里可以实现动态更新水体表面顶点位置 + # 基于高度场数据调整顶点Z坐标 + pass + + except Exception as e: + print(f"更新水体表面几何时出错: {e}") + + def _update_water_waves(self, water_info, current_time): + """ + 更新水体波浪(多层波浪系统) + """ + try: + # 生成更复杂的波浪系统 + wave_system = water_info.get('wave_system', []) + + # 如果还没有初始化波浪系统,创建多层波浪 + if not wave_system: + wave_system = [] + # 创建不同频率和振幅的波浪层 + for i in range(5): # 5层波浪 + wave = { + 'frequency': self.wave_parameters['frequency'] * (0.5 + i * 0.5), + 'amplitude': self.wave_parameters['amplitude'] * (1.0 - i * 0.15), + 'speed': self.wave_parameters['speed'] * (0.8 + i * 0.2), + 'direction': Vec3(random.uniform(-1, 1), random.uniform(-1, 1), 0).normalized() + } + wave_system.append(wave) + water_info['wave_system'] = wave_system + + # 更新波浪参数 + water_info['waves'] = wave_system + + except Exception as e: + print(f"更新水体波浪时出错: {e}") + + def _update_water_reflection(self, water_info): + """ + 更新水体反射效果 + """ + try: + if not self.reflection_enabled: + return + + # 这里可以实现反射相机的更新逻辑 + # 包括相机位置的镜像计算等 + pass + + except Exception as e: + print(f"更新水体反射时出错: {e}") + + def set_wave_parameters(self, amplitude=None, frequency=None, speed=None, direction=None): + """ + 设置波浪参数 + """ + if amplitude is not None: + self.wave_parameters['amplitude'] = amplitude + if frequency is not None: + self.wave_parameters['frequency'] = frequency + if speed is not None: + self.wave_parameters['speed'] = speed + if direction is not None: + self.wave_parameters['direction'] = direction + + # 更新所有水体的着色器参数 + for water_info in self.water_bodies: + surface_node = water_info['surface_node'] + if surface_node.hasShader(): + if amplitude is not None: + surface_node.setShaderInput("wave_amplitude", amplitude) + if frequency is not None: + surface_node.setShaderInput("wave_frequency", frequency) + if speed is not None: + surface_node.setShaderInput("wave_speed", speed) + if direction is not None: + surface_node.setShaderInput("wave_direction", direction) + + print(f"更新波浪参数: {self.wave_parameters}") + + def set_fluid_parameters(self, viscosity=None, density=None, surface_tension=None, gravity=None): + """ + 设置流体参数 + """ + if viscosity is not None: + self.fluid_parameters['viscosity'] = viscosity + if density is not None: + self.fluid_parameters['density'] = density + if surface_tension is not None: + self.fluid_parameters['surface_tension'] = surface_tension + if gravity is not None: + self.fluid_parameters['gravity'] = gravity + + print(f"更新流体参数: {self.fluid_parameters}") + + def simulate_water_physics(self, time_delta): + """ + 模拟水体物理效果 + """ + try: + for water_info in self.water_bodies: + # 模拟水流 + self._simulate_water_flow(water_info, time_delta) + + # 模拟水花 + self._simulate_water_splash(water_info, time_delta) + + # 模拟波纹 + self._simulate_water_ripples(water_info, time_delta) + + except Exception as e: + print(f"模拟水体物理时出错: {e}") + + def _simulate_water_flow(self, water_info, time_delta): + """ + 模拟水流效果 + """ + try: + if not self.flow_enabled: + return + + # 模拟水流方向和速度 + # 可以基于地形坡度或外力计算水流 + pass + + except Exception as e: + print(f"模拟水流效果时出错: {e}") + + def _simulate_water_splash(self, water_info, time_delta): + """ + 模拟水花效果 + """ + try: + # 模拟物体落入水中时产生的水花效果 + # 可以创建粒子系统或动态几何体 + pass + + except Exception as e: + print(f"模拟水花效果时出错: {e}") + + def _simulate_water_ripples(self, water_info, time_delta): + """ + 模拟波纹效果 + """ + try: + # 模拟水面波纹传播 + # 基于波动方程的简化实现 + pass + + except Exception as e: + print(f"模拟波纹效果时出错: {e}") + + def add_object_to_water(self, water_info, object_node, buoyancy=1.0): + """ + 将物体添加到水中 + """ + try: + # 计算物体的水中位置 + water_pos = object_node.getPos(water_info['node']) + + object_info = { + 'node': object_node, + 'buoyancy': buoyancy, + 'water_position': water_pos, + 'velocity': Vec3(0, 0, 0), + 'volume': self._calculate_object_volume(object_node), + 'density': 1000.0, # 默认密度 + 'submerged_volume': 0.0, + 'drag_coefficient': 0.5 + } + + water_info['objects'].append(object_info) + print(f"物体已添加到水中: {object_node.getName()}") + return object_info + + except Exception as e: + print(f"添加物体到水中时出错: {e}") + return None + + def _calculate_object_volume(self, object_node): + """ + 计算物体体积(简化实现) + """ + try: + # 获取物体包围盒 + bounds = object_node.getBounds() + if hasattr(bounds, 'getRadius'): + # 简化为球体体积计算 + radius = bounds.getRadius() + volume = (4.0/3.0) * math.pi * (radius ** 3) + return volume + return 1.0 # 默认体积 + except: + return 1.0 + + def update_water_physics(self, time_delta): + """ + 更新水中物体的物理效果 + """ + try: + for water_info in self.water_bodies: + water_height = water_info['position'].getZ() + fluid_density = self.fluid_parameters['density'] + + for object_info in water_info['objects']: + object_node = object_info['node'] + if object_node and not object_node.isEmpty(): + # 获取物体位置 + object_pos = object_node.getPos() + + # 计算浸入水中的体积 + object_bounds = object_node.getBounds() + object_bottom = object_pos.getZ() - object_bounds.getRadius() + object_top = object_pos.getZ() + object_bounds.getRadius() + + if object_bottom < water_height and object_top > water_height: + # 部分浸入水中 + submerged_ratio = (water_height - object_bottom) / (object_top - object_bottom) + submerged_volume = object_info['volume'] * min(1.0, max(0.0, submerged_ratio)) + elif object_top <= water_height: + # 完全浸入水中 + submerged_volume = object_info['volume'] + else: + # 完全在水上 + submerged_volume = 0.0 + + object_info['submerged_volume'] = submerged_volume + + # 计算浮力 + buoyancy_force = Vec3(0, 0, submerged_volume * fluid_density * self.fluid_parameters['gravity']) + + # 应用浮力 + object_info['velocity'] += buoyancy_force * time_delta / object_info['density'] + + # 计算阻力 + drag_force = -object_info['velocity'] * object_info['drag_coefficient'] * submerged_volume + object_info['velocity'] += drag_force * time_delta / object_info['density'] + + # 更新物体位置 + new_pos = object_pos + object_info['velocity'] * time_delta + object_node.setPos(new_pos) + + # 应用阻尼 + object_info['velocity'] *= 0.98 + + except Exception as e: + print(f"更新水中物理时出错: {e}") + + def create_water_foam(self, water_info, position, intensity=1.0): + """ + 创建水花泡沫效果 + """ + try: + # 创建泡沫粒子系统 + print(f"在位置 {position} 创建水花泡沫 (强度: {intensity})") + + # 这里可以实现创建粒子系统来模拟泡沫 + # 包括泡沫的生命周期、扩散、消散等效果 + return True + + except Exception as e: + print(f"创建水花泡沫时出错: {e}") + return False + + def create_water_caustics(self, water_info, light_direction): + """ + 创建焦散效果 + """ + try: + if not self.caustics_enabled: + return False + + # 创建焦散纹理或几何体 + print(f"创建焦散效果 (光照方向: {light_direction})") + + # 这里可以实现焦散效果的生成 + # 包括光线追踪、纹理投影等技术 + return True + + except Exception as e: + print(f"创建焦散效果时出错: {e}") + return False + + def set_water_quality(self, quality_level): + """ + 设置水体质量等级 + """ + try: + # 根据质量等级调整水体效果 + if quality_level == "low": + self.reflection_enabled = False + self.refraction_enabled = False + self.foam_enabled = False + self.caustics_enabled = False + self.flow_enabled = False + elif quality_level == "medium": + self.reflection_enabled = True + self.refraction_enabled = False + self.foam_enabled = True + self.caustics_enabled = False + self.flow_enabled = True + elif quality_level == "high": + self.reflection_enabled = True + self.refraction_enabled = True + self.foam_enabled = True + self.caustics_enabled = True + self.flow_enabled = True + + # 更新所有水体 + for water_info in self.water_bodies: + self._setup_water_material(water_info) + + print(f"水体质量设置为: {quality_level}") + + except Exception as e: + print(f"设置水体质量时出错: {e}") + + def get_water_stats(self): + """ + 获取水体统计信息 + """ + stats = { + 'total_bodies': len(self.water_bodies), + 'total_surface_area': 0, + 'water_types': {}, + 'fluid_stats': self.fluid_parameters, + 'quality_settings': { + 'reflection': self.reflection_enabled, + 'refraction': self.refraction_enabled, + 'foam': self.foam_enabled, + 'caustics': self.caustics_enabled + } + } + + for water_info in self.water_bodies: + # 计算表面积 + area = water_info['size'][0] * water_info['size'][1] + stats['total_surface_area'] += area + + # 统计水体类型 + water_type = water_info['type'] + if water_type in stats['water_types']: + stats['water_types'][water_type] += 1 + else: + stats['water_types'][water_type] = 1 + + # 添加流体分辨率信息 + stats[f"water_body_{len(stats['water_types'])}_resolution"] = ( + water_info['resolution_x'], + water_info['resolution_y'] + ) + + return stats + + def remove_water_body(self, water_info): + """ + 移除水体 + """ + try: + if water_info in self.water_bodies: + # 移除节点 + if water_info['node'] and not water_info['node'].isEmpty(): + water_info['node'].removeNode() + + # 从列表中移除 + self.water_bodies.remove(water_info) + + print("水体已移除") + return True + + except Exception as e: + print(f"移除水体时出错: {e}") + return False + + def clear_all_water(self): + """ + 清除所有水体 + """ + try: + for water_info in self.water_bodies[:]: # 使用切片复制列表 + self.remove_water_body(water_info) + + print("所有水体已清除") + return True + + except Exception as e: + print(f"清除所有水体时出错: {e}") + return False + + def save_water_data(self, output_path): + """ + 保存水体数据 + """ + try: + import json + import os + + # 收集水体数据 + water_data = { + 'bodies': [], + 'wave_parameters': self.wave_parameters, + 'fluid_parameters': self.fluid_parameters, + 'quality_settings': { + 'reflection_enabled': self.reflection_enabled, + 'refraction_enabled': self.refraction_enabled, + 'foam_enabled': self.foam_enabled, + 'caustics_enabled': self.caustics_enabled + } + } + + for water_info in self.water_bodies: + body_data = { + 'position': [water_info['position'].x, + water_info['position'].y, + water_info['position'].z], + 'size': water_info['size'], + 'depth': water_info['depth'], + 'type': water_info['type'], + 'resolution_x': water_info['resolution_x'], + 'resolution_y': water_info['resolution_y'] + } + water_data['bodies'].append(body_data) + + # 确保输出目录存在 + output_dir = os.path.dirname(output_path) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + # 写入JSON文件 + with open(output_path, 'w', encoding='utf-8') as f: + json.dump(water_data, f, indent=2, ensure_ascii=False) + + print(f"水体数据保存成功: {output_path}") + return True + + except Exception as e: + print(f"保存水体数据时出错: {e}") + return False + + def load_water_data(self, input_path): + """ + 加载水体数据 + """ + try: + import json + import os + + if not os.path.exists(input_path): + print(f"水体数据文件不存在: {input_path}") + return False + + # 读取JSON文件 + with open(input_path, 'r', encoding='utf-8') as f: + water_data = json.load(f) + + # 清除现有水体 + self.clear_all_water() + + # 恢复设置 + if 'wave_parameters' in water_data: + self.wave_parameters = water_data['wave_parameters'] + if 'fluid_parameters' in water_data: + self.fluid_parameters = water_data['fluid_parameters'] + if 'quality_settings' in water_data: + qs = water_data['quality_settings'] + self.reflection_enabled = qs.get('reflection_enabled', True) + self.refraction_enabled = qs.get('refraction_enabled', True) + self.foam_enabled = qs.get('foam_enabled', True) + self.caustics_enabled = qs.get('caustics_enabled', True) + + # 创建水体 + for body_data in water_data['bodies']: + position = Point3(body_data['position'][0], + body_data['position'][1], + body_data['position'][2]) + + self.create_water_body( + position=position, + size=body_data['size'], + depth=body_data['depth'], + water_type=body_data['type'] + ) + + print(f"水体数据加载成功: {input_path}") + return True + + except Exception as e: + print(f"加载水体数据时出错: {e}") + return False + + def update_all_water_systems(self, time_delta): + """ + 更新所有水体系统 + """ + try: + # 更新水体动画 + self.update_water_animation(time_delta) + + # 更新水体物理 + self.update_water_physics(time_delta) + + # 更新流体动力学 + self.simulate_water_physics(time_delta) + + except Exception as e: print(f"更新所有水体系统时出错: {e}") \ No newline at end of file diff --git a/plugins/user/test_plugins.py b/plugins/user/test_plugins.py index c455ea64..7cf80da5 100644 --- a/plugins/user/test_plugins.py +++ b/plugins/user/test_plugins.py @@ -1,217 +1,217 @@ -""" -测试行为树和状态机插件功能 -""" - -import sys -import os - -# 添加项目根目录到Python路径 -sys.path.insert(0, '/home/hello/EG') - -def test_behavior_tree_plugin(): - """ - 测试行为树插件功能 - """ - print("开始测试行为树插件...") - - # 导入插件管理器 - from plugins.plugin_manager import PluginManager - - # 创建一个虚拟的世界对象(用于测试) - class MockWorld: - def __init__(self): - self.accept = lambda event, handler: None - self.ignore = lambda event: None - - # 创建世界实例 - world = MockWorld() - - # 创建插件管理器 - plugin_manager = PluginManager(world) - plugin_manager.enable_plugin_system() - - # 加载行为树插件 - bt_plugin = plugin_manager.load_plugin("behavior_tree") - if not bt_plugin: - print("❌ 无法加载行为树插件") - return False - - # 启用行为树插件 - if not plugin_manager.enable_plugin("behavior_tree"): - print("❌ 无法启用行为树插件") - return False - - print("✅ 行为树插件加载和启用成功") - - # 导入示例并运行 - try: - from plugins.user.behavior_tree.examples.patrol_example import run_patrol_example - run_patrol_example(bt_plugin) - print("✅ 行为树示例运行成功") - except Exception as e: - print(f"❌ 行为树示例运行失败: {e}") - import traceback - traceback.print_exc() - return False - - # 测试基本节点功能 - try: - from plugins.user.behavior_tree import SelectorNode, SequenceNode, ActionNode, ConditionNode - - # 创建一个简单的序列节点 - sequence = SequenceNode("TestSequence") - - # 添加一个条件节点 - condition = ConditionNode("TestCondition", lambda bb: True) - sequence.add_child(condition) - - # 添加一个动作节点 - action = ActionNode("TestAction", lambda bb: print("测试动作执行") or True) - sequence.add_child(action) - - # 创建黑板 - blackboard = bt_plugin.create_blackboard("test_blackboard") - - # 创建行为树 - tree = bt_plugin.create_behavior_tree("test_tree", sequence) - tree.set_blackboard(blackboard) - - # 执行行为树 - result = tree.run() - print(f"✅ 基本节点功能测试成功,执行结果: {result.value}") - - except Exception as e: - print(f"❌ 基本节点功能测试失败: {e}") - import traceback - traceback.print_exc() - return False - - # 禁用和卸载插件 - plugin_manager.disable_plugin("behavior_tree") - plugin_manager.unload_plugin("behavior_tree") - - print("✅ 行为树插件测试完成") - return True - -def test_state_machine_plugin(): - """ - 测试状态机插件功能 - """ - print("开始测试状态机插件...") - - # 导入插件管理器 - from plugins.plugin_manager import PluginManager - - # 创建一个虚拟的世界对象(用于测试) - class MockWorld: - def __init__(self): - self.accept = lambda event, handler: None - self.ignore = lambda event: None - - # 创建世界实例 - world = MockWorld() - - # 创建插件管理器 - plugin_manager = PluginManager(world) - plugin_manager.enable_plugin_system() - - # 加载状态机插件 - sm_plugin = plugin_manager.load_plugin("state_machine") - if not sm_plugin: - print("❌ 无法加载状态机插件") - return False - - # 启用状态机插件 - if not plugin_manager.enable_plugin("state_machine"): - print("❌ 无法启用状态机插件") - return False - - print("✅ 状态机插件加载和启用成功") - - # 导入示例并运行 - try: - from plugins.user.state_machine.examples.ai_example import run_ai_example - run_ai_example(sm_plugin) - print("✅ 状态机示例运行成功") - except Exception as e: - print(f"❌ 状态机示例运行失败: {e}") - import traceback - traceback.print_exc() - return False - - # 测试基本状态功能 - try: - from plugins.user.state_machine import State, StateMachine - - # 创建一个简单的状态 - class TestState(State): - def enter(self, context): - print("进入测试状态") - - def execute(self, context): - print("执行测试状态") - return None # 保持当前状态 - - def exit(self, context): - print("退出测试状态") - - # 创建状态机 - state_machine = sm_plugin.create_state_machine("test_state_machine") - - # 创建上下文(简单字典作为上下文) - context = {"data": {}} - - def get_data(key, default=None): - return context["data"].get(key, default) - - def set_data(key, value): - context["data"][key] = value - - context["get_data"] = get_data - context["set_data"] = set_data - - # 设置状态机上下文 - state_machine.set_context(context) - - # 创建测试状态并切换 - test_state = TestState("TestState") - state_machine.change_state(test_state) - - # 更新状态机 - state_machine.update() - - print("✅ 基本状态功能测试成功") - - except Exception as e: - print(f"❌ 基本状态功能测试失败: {e}") - import traceback - traceback.print_exc() - return False - - # 禁用和卸载插件 - plugin_manager.disable_plugin("state_machine") - plugin_manager.unload_plugin("state_machine") - - print("✅ 状态机插件测试完成") - return True - -if __name__ == "__main__": - print("开始测试行为树和状态机插件...") - - # 测试行为树插件 - bt_success = test_behavior_tree_plugin() - - print("\n" + "="*50 + "\n") - - # 测试状态机插件 - sm_success = test_state_machine_plugin() - - print("\n" + "="*50) - print("测试总结:") - print(f"行为树插件测试: {'✅ 通过' if bt_success else '❌ 失败'}") - print(f"状态机插件测试: {'✅ 通过' if sm_success else '❌ 失败'}") - - if bt_success and sm_success: - print("🎉 所有插件测试通过!") - else: - print("❌ 部分测试失败,请检查插件实现。") +""" +测试行为树和状态机插件功能 +""" + +import sys +import os + +# 添加项目根目录到Python路径 +sys.path.insert(0, '/home/hello/EG') + +def test_behavior_tree_plugin(): + """ + 测试行为树插件功能 + """ + print("开始测试行为树插件...") + + # 导入插件管理器 + from plugins.plugin_manager import PluginManager + + # 创建一个虚拟的世界对象(用于测试) + class MockWorld: + def __init__(self): + self.accept = lambda event, handler: None + self.ignore = lambda event: None + + # 创建世界实例 + world = MockWorld() + + # 创建插件管理器 + plugin_manager = PluginManager(world) + plugin_manager.enable_plugin_system() + + # 加载行为树插件 + bt_plugin = plugin_manager.load_plugin("behavior_tree") + if not bt_plugin: + print("❌ 无法加载行为树插件") + return False + + # 启用行为树插件 + if not plugin_manager.enable_plugin("behavior_tree"): + print("❌ 无法启用行为树插件") + return False + + print("✅ 行为树插件加载和启用成功") + + # 导入示例并运行 + try: + from plugins.user.behavior_tree.examples.patrol_example import run_patrol_example + run_patrol_example(bt_plugin) + print("✅ 行为树示例运行成功") + except Exception as e: + print(f"❌ 行为树示例运行失败: {e}") + import traceback + traceback.print_exc() + return False + + # 测试基本节点功能 + try: + from plugins.user.behavior_tree import SelectorNode, SequenceNode, ActionNode, ConditionNode + + # 创建一个简单的序列节点 + sequence = SequenceNode("TestSequence") + + # 添加一个条件节点 + condition = ConditionNode("TestCondition", lambda bb: True) + sequence.add_child(condition) + + # 添加一个动作节点 + action = ActionNode("TestAction", lambda bb: print("测试动作执行") or True) + sequence.add_child(action) + + # 创建黑板 + blackboard = bt_plugin.create_blackboard("test_blackboard") + + # 创建行为树 + tree = bt_plugin.create_behavior_tree("test_tree", sequence) + tree.set_blackboard(blackboard) + + # 执行行为树 + result = tree.run() + print(f"✅ 基本节点功能测试成功,执行结果: {result.value}") + + except Exception as e: + print(f"❌ 基本节点功能测试失败: {e}") + import traceback + traceback.print_exc() + return False + + # 禁用和卸载插件 + plugin_manager.disable_plugin("behavior_tree") + plugin_manager.unload_plugin("behavior_tree") + + print("✅ 行为树插件测试完成") + return True + +def test_state_machine_plugin(): + """ + 测试状态机插件功能 + """ + print("开始测试状态机插件...") + + # 导入插件管理器 + from plugins.plugin_manager import PluginManager + + # 创建一个虚拟的世界对象(用于测试) + class MockWorld: + def __init__(self): + self.accept = lambda event, handler: None + self.ignore = lambda event: None + + # 创建世界实例 + world = MockWorld() + + # 创建插件管理器 + plugin_manager = PluginManager(world) + plugin_manager.enable_plugin_system() + + # 加载状态机插件 + sm_plugin = plugin_manager.load_plugin("state_machine") + if not sm_plugin: + print("❌ 无法加载状态机插件") + return False + + # 启用状态机插件 + if not plugin_manager.enable_plugin("state_machine"): + print("❌ 无法启用状态机插件") + return False + + print("✅ 状态机插件加载和启用成功") + + # 导入示例并运行 + try: + from plugins.user.state_machine.examples.ai_example import run_ai_example + run_ai_example(sm_plugin) + print("✅ 状态机示例运行成功") + except Exception as e: + print(f"❌ 状态机示例运行失败: {e}") + import traceback + traceback.print_exc() + return False + + # 测试基本状态功能 + try: + from plugins.user.state_machine import State, StateMachine + + # 创建一个简单的状态 + class TestState(State): + def enter(self, context): + print("进入测试状态") + + def execute(self, context): + print("执行测试状态") + return None # 保持当前状态 + + def exit(self, context): + print("退出测试状态") + + # 创建状态机 + state_machine = sm_plugin.create_state_machine("test_state_machine") + + # 创建上下文(简单字典作为上下文) + context = {"data": {}} + + def get_data(key, default=None): + return context["data"].get(key, default) + + def set_data(key, value): + context["data"][key] = value + + context["get_data"] = get_data + context["set_data"] = set_data + + # 设置状态机上下文 + state_machine.set_context(context) + + # 创建测试状态并切换 + test_state = TestState("TestState") + state_machine.change_state(test_state) + + # 更新状态机 + state_machine.update() + + print("✅ 基本状态功能测试成功") + + except Exception as e: + print(f"❌ 基本状态功能测试失败: {e}") + import traceback + traceback.print_exc() + return False + + # 禁用和卸载插件 + plugin_manager.disable_plugin("state_machine") + plugin_manager.unload_plugin("state_machine") + + print("✅ 状态机插件测试完成") + return True + +if __name__ == "__main__": + print("开始测试行为树和状态机插件...") + + # 测试行为树插件 + bt_success = test_behavior_tree_plugin() + + print("\n" + "="*50 + "\n") + + # 测试状态机插件 + sm_success = test_state_machine_plugin() + + print("\n" + "="*50) + print("测试总结:") + print(f"行为树插件测试: {'✅ 通过' if bt_success else '❌ 失败'}") + print(f"状态机插件测试: {'✅ 通过' if sm_success else '❌ 失败'}") + + if bt_success and sm_success: + print("🎉 所有插件测试通过!") + else: + print("❌ 部分测试失败,请检查插件实现。") diff --git a/plugins/user/vegetation_ecosystem/README.md b/plugins/user/vegetation_ecosystem/README.md index 4a387759..a0df56e0 100644 --- a/plugins/user/vegetation_ecosystem/README.md +++ b/plugins/user/vegetation_ecosystem/README.md @@ -1,287 +1,287 @@ -# 植被分布和生态模拟插件 - -## 概述 - -植被分布和生态模拟插件为EG引擎提供了一个完整的生态系统模拟解决方案,包括植被生成、动物行为模拟、环境影响、季节变化和可视化展示等功能。该插件可以帮助开发者创建逼真的自然环境,支持复杂的生态关系和动态变化。 - -## 功能特性 - -### 1. 植被管理系统 -- 多种植被类型支持(草地、灌木、树木、花卉等) -- 基于环境条件的植被分布 -- 植被生长、繁殖和死亡模拟 -- 植被健康度和年龄系统 - -### 2. 动物行为模拟 -- 多种动物种类(兔子、鹿、狼、鸟类等) -- 动物繁殖、死亡和迁徙行为 -- 食物链和捕食者-猎物关系 -- 动物群体行为模拟 - -### 3. 生态系统动力学 -- 能量流动和物质循环模拟 -- 生态位和竞争关系建模 -- 生态系统稳定性分析 -- 生物多样性评估 - -### 4. 环境影响系统 -- 地形对植被分布的影响 -- 气候和季节性变化 -- 土壤质量和水资源影响 -- 微气候效应模拟 - -### 5. 季节变化系统 -- 四季循环和过渡效果 -- 季节性植被变化 -- 动物行为的季节性调整 -- 环境参数的季节性调整 - -### 6. 可视化系统 -- 高性能植被渲染 -- LOD(细节层次)系统 -- 动态光照和阴影 -- 季节性色彩调整 - -### 7. 编辑器工具 -- 可视化编辑界面 -- 参数调整和实时预览 -- 预设管理系统 -- 撤销/重做功能 - -## 安装和使用 - -### 安装 -将插件文件夹复制到EG引擎的plugins目录中: - -``` -cp -r vegetation_ecosystem /path/to/eg/plugins/user/ -``` - -### 初始化插件 -在EG引擎中加载插件: - -```python -from plugins.user.vegetation_ecosystem.plugin import VegetationEcosystemPlugin - -# 创建插件实例 -eco_plugin = VegetationEcosystemPlugin(engine) - -# 初始化插件 -if eco_plugin.initialize(): - # 启用插件 - eco_plugin.enable() -``` - -### 基本使用 - -#### 生成植被 -```python -# 获取植被生成器 -spawner = eco_plugin.vegetation_spawner - -# 在指定区域生成植被 -spawner.spawn_vegetation( - area=((-50, -50), (50, 50)), - density=0.8, - algorithm='environmental' -) -``` - -#### 调整环境参数 -```python -# 获取植被管理器 -vegetation_manager = eco_plugin.vegetation_manager - -# 更新环境因子 -vegetation_manager.update_environment_factors({ - 'temperature': 25.0, - 'humidity': 70.0, - 'soil_fertility': 0.8 -}) -``` - -#### 控制季节变化 -```python -# 获取季节效应系统 -seasonal_effects = eco_plugin.seasonal_effects - -# 快进到夏季 -seasonal_effects.fast_forward_to_season('summer') - -# 或者调整季节持续时间 -seasonal_effects.set_season_duration('spring', 60) # 60天 -``` - -## API参考 - -### 核心模块 - -#### VegetationManager(植被管理器) -- `get_vegetation_types()` - 获取所有植被类型 -- `create_vegetation_instance()` - 创建植被实例 -- `update_environment_factors()` - 更新环境因子 -- `get_suitability_score()` - 计算植被适宜性评分 - -#### AnimalSimulator(动物模拟器) -- `get_animal_species()` - 获取所有动物种类 -- `create_animal_instance()` - 创建动物实例 -- `simulate_feeding()` - 模拟进食过程 -- `get_population_data()` - 获取种群数据 - -#### EcosystemManager(生态系统管理器) -- `add_predator_prey_relationship()` - 添加捕食关系 -- `get_biodiversity_index()` - 获取生物多样性指数 -- `get_system_state()` - 获取系统状态 - -#### VegetationSpawner(植被生成器) -- `spawn_vegetation()` - 生成植被 -- `get_distribution_algorithms()` - 获取分布算法 -- `regenerate_vegetation()` - 重新生成植被 - -#### GrowthSimulator(生长模拟器) -- `set_growth_model()` - 设置生长模型 -- `get_growth_parameters()` - 获取生长参数 -- `simulate_seasonal_growth()` - 模拟季节性生长 - -#### TerrainInfluence(地形影响) -- `calculate_terrain_influence()` - 计算地形影响 -- `get_terrain_suitability_map()` - 获取地形适宜性地图 -- `simulate_terrain_adaptation()` - 模拟地形适应 - -#### SeasonalEffects(季节效应) -- `get_current_season()` - 获取当前季节 -- `start_season_transition()` - 开始季节过渡 -- `get_seasonal_events()` - 获取季节性事件 - -#### EcoVisualizer(生态可视化器) -- `render_vegetation()` - 渲染植被 -- `render_animals()` - 渲染动物 -- `set_render_config()` - 设置渲染配置 -- `capture_screenshot()` - 截取屏幕截图 - -### 编辑器模块 - -#### EcoEditor(生态编辑器) -- `set_edit_mode()` - 设置编辑模式 -- `set_parameter()` - 设置参数 -- `undo()` / `redo()` - 撤销/重做 -- `spawn_vegetation()` - 生成植被 - -#### PresetManager(预设管理器) -- `save_preset()` - 保存预设 -- `apply_preset()` - 应用预设 -- `list_presets()` - 列出预设 -- `search_presets()` - 搜索预设 - -### 工具模块 - -#### EcoUtils(生态系统工具) -- `log_message()` - 记录日志 -- `get_performance_stats()` - 获取性能统计 -- `cache_data()` - 缓存数据 -- `serialize_data()` - 序列化数据 - -## 配置选项 - -### 插件配置 -```python -config = { - 'simulation_speed': 1.0, # 模拟速度 - 'render_distance': 100.0, # 渲染距离 - 'max_instances': 5000, # 最大实例数 - 'season_duration': 90 # 季节持续时间(天) -} -``` - -### 植被配置 -```python -vegetation_config = { - 'density': 1.0, # 植被密度 - 'clumping_factor': 0.7, # 聚集因子 - 'spawn_area': ((-100, -100), (100, 100)) # 生成区域 -} -``` - -### 动物配置 -```python -animal_config = { - 'feeding_efficiency': 0.8, # 进食效率 - 'energy_consumption': 0.1, # 能量消耗率 - 'migration_probability': 0.05 # 迁移概率 -} -``` - -## 性能优化 - -1. **LOD系统** - 根据距离自动调整渲染细节 -2. **对象池** - 重用植被和动物实例 -3. **数据缓存** - 缓存计算结果避免重复计算 -4. **批量处理** - 批量更新和渲染操作 -5. **视锥剔除** - 只渲染视野内的对象 - -## 扩展开发 - -### 添加新的植被类型 -```python -# 在VegetationManager中添加新的植被类型定义 -new_plant = { - 'name': '新植物', - 'description': '植物描述', - 'height_range': (0.5, 2.0), - 'growth_rate': 0.1, - 'preferred_conditions': { - 'temperature': (15, 25), - 'humidity': (60, 80) - } -} -``` - -### 添加新的动物种类 -```python -# 在AnimalSimulator中添加新的动物种类定义 -new_animal = { - 'name': '新动物', - 'diet': 'herbivore', - 'reproduction_rate': 0.1, - 'preferred_food': ['grass', 'flower'] -} -``` - -### 自定义生长模型 -```python -# 在GrowthSimulator中实现新的生长模型 -def custom_growth_function(time, params): - # 自定义生长算法 - return size -``` - -## 故障排除 - -### 常见问题 - -1. **性能问题** - - 降低渲染距离 - - 减少最大实例数 - - 使用更低的LOD级别 - -2. **植被分布不自然** - - 调整环境权重 - - 更改分布算法 - - 修改聚集因子 - -3. **动物行为异常** - - 检查食物链配置 - - 调整行为参数 - - 验证环境适应性 - -### 支持 - -如遇到问题,请查看日志文件或联系开发者支持。 - -## 许可证 - -本插件遵循EG引擎的许可证协议。 - -## 版本历史 - +# 植被分布和生态模拟插件 + +## 概述 + +植被分布和生态模拟插件为EG引擎提供了一个完整的生态系统模拟解决方案,包括植被生成、动物行为模拟、环境影响、季节变化和可视化展示等功能。该插件可以帮助开发者创建逼真的自然环境,支持复杂的生态关系和动态变化。 + +## 功能特性 + +### 1. 植被管理系统 +- 多种植被类型支持(草地、灌木、树木、花卉等) +- 基于环境条件的植被分布 +- 植被生长、繁殖和死亡模拟 +- 植被健康度和年龄系统 + +### 2. 动物行为模拟 +- 多种动物种类(兔子、鹿、狼、鸟类等) +- 动物繁殖、死亡和迁徙行为 +- 食物链和捕食者-猎物关系 +- 动物群体行为模拟 + +### 3. 生态系统动力学 +- 能量流动和物质循环模拟 +- 生态位和竞争关系建模 +- 生态系统稳定性分析 +- 生物多样性评估 + +### 4. 环境影响系统 +- 地形对植被分布的影响 +- 气候和季节性变化 +- 土壤质量和水资源影响 +- 微气候效应模拟 + +### 5. 季节变化系统 +- 四季循环和过渡效果 +- 季节性植被变化 +- 动物行为的季节性调整 +- 环境参数的季节性调整 + +### 6. 可视化系统 +- 高性能植被渲染 +- LOD(细节层次)系统 +- 动态光照和阴影 +- 季节性色彩调整 + +### 7. 编辑器工具 +- 可视化编辑界面 +- 参数调整和实时预览 +- 预设管理系统 +- 撤销/重做功能 + +## 安装和使用 + +### 安装 +将插件文件夹复制到EG引擎的plugins目录中: + +``` +cp -r vegetation_ecosystem /path/to/eg/plugins/user/ +``` + +### 初始化插件 +在EG引擎中加载插件: + +```python +from plugins.user.vegetation_ecosystem.plugin import VegetationEcosystemPlugin + +# 创建插件实例 +eco_plugin = VegetationEcosystemPlugin(engine) + +# 初始化插件 +if eco_plugin.initialize(): + # 启用插件 + eco_plugin.enable() +``` + +### 基本使用 + +#### 生成植被 +```python +# 获取植被生成器 +spawner = eco_plugin.vegetation_spawner + +# 在指定区域生成植被 +spawner.spawn_vegetation( + area=((-50, -50), (50, 50)), + density=0.8, + algorithm='environmental' +) +``` + +#### 调整环境参数 +```python +# 获取植被管理器 +vegetation_manager = eco_plugin.vegetation_manager + +# 更新环境因子 +vegetation_manager.update_environment_factors({ + 'temperature': 25.0, + 'humidity': 70.0, + 'soil_fertility': 0.8 +}) +``` + +#### 控制季节变化 +```python +# 获取季节效应系统 +seasonal_effects = eco_plugin.seasonal_effects + +# 快进到夏季 +seasonal_effects.fast_forward_to_season('summer') + +# 或者调整季节持续时间 +seasonal_effects.set_season_duration('spring', 60) # 60天 +``` + +## API参考 + +### 核心模块 + +#### VegetationManager(植被管理器) +- `get_vegetation_types()` - 获取所有植被类型 +- `create_vegetation_instance()` - 创建植被实例 +- `update_environment_factors()` - 更新环境因子 +- `get_suitability_score()` - 计算植被适宜性评分 + +#### AnimalSimulator(动物模拟器) +- `get_animal_species()` - 获取所有动物种类 +- `create_animal_instance()` - 创建动物实例 +- `simulate_feeding()` - 模拟进食过程 +- `get_population_data()` - 获取种群数据 + +#### EcosystemManager(生态系统管理器) +- `add_predator_prey_relationship()` - 添加捕食关系 +- `get_biodiversity_index()` - 获取生物多样性指数 +- `get_system_state()` - 获取系统状态 + +#### VegetationSpawner(植被生成器) +- `spawn_vegetation()` - 生成植被 +- `get_distribution_algorithms()` - 获取分布算法 +- `regenerate_vegetation()` - 重新生成植被 + +#### GrowthSimulator(生长模拟器) +- `set_growth_model()` - 设置生长模型 +- `get_growth_parameters()` - 获取生长参数 +- `simulate_seasonal_growth()` - 模拟季节性生长 + +#### TerrainInfluence(地形影响) +- `calculate_terrain_influence()` - 计算地形影响 +- `get_terrain_suitability_map()` - 获取地形适宜性地图 +- `simulate_terrain_adaptation()` - 模拟地形适应 + +#### SeasonalEffects(季节效应) +- `get_current_season()` - 获取当前季节 +- `start_season_transition()` - 开始季节过渡 +- `get_seasonal_events()` - 获取季节性事件 + +#### EcoVisualizer(生态可视化器) +- `render_vegetation()` - 渲染植被 +- `render_animals()` - 渲染动物 +- `set_render_config()` - 设置渲染配置 +- `capture_screenshot()` - 截取屏幕截图 + +### 编辑器模块 + +#### EcoEditor(生态编辑器) +- `set_edit_mode()` - 设置编辑模式 +- `set_parameter()` - 设置参数 +- `undo()` / `redo()` - 撤销/重做 +- `spawn_vegetation()` - 生成植被 + +#### PresetManager(预设管理器) +- `save_preset()` - 保存预设 +- `apply_preset()` - 应用预设 +- `list_presets()` - 列出预设 +- `search_presets()` - 搜索预设 + +### 工具模块 + +#### EcoUtils(生态系统工具) +- `log_message()` - 记录日志 +- `get_performance_stats()` - 获取性能统计 +- `cache_data()` - 缓存数据 +- `serialize_data()` - 序列化数据 + +## 配置选项 + +### 插件配置 +```python +config = { + 'simulation_speed': 1.0, # 模拟速度 + 'render_distance': 100.0, # 渲染距离 + 'max_instances': 5000, # 最大实例数 + 'season_duration': 90 # 季节持续时间(天) +} +``` + +### 植被配置 +```python +vegetation_config = { + 'density': 1.0, # 植被密度 + 'clumping_factor': 0.7, # 聚集因子 + 'spawn_area': ((-100, -100), (100, 100)) # 生成区域 +} +``` + +### 动物配置 +```python +animal_config = { + 'feeding_efficiency': 0.8, # 进食效率 + 'energy_consumption': 0.1, # 能量消耗率 + 'migration_probability': 0.05 # 迁移概率 +} +``` + +## 性能优化 + +1. **LOD系统** - 根据距离自动调整渲染细节 +2. **对象池** - 重用植被和动物实例 +3. **数据缓存** - 缓存计算结果避免重复计算 +4. **批量处理** - 批量更新和渲染操作 +5. **视锥剔除** - 只渲染视野内的对象 + +## 扩展开发 + +### 添加新的植被类型 +```python +# 在VegetationManager中添加新的植被类型定义 +new_plant = { + 'name': '新植物', + 'description': '植物描述', + 'height_range': (0.5, 2.0), + 'growth_rate': 0.1, + 'preferred_conditions': { + 'temperature': (15, 25), + 'humidity': (60, 80) + } +} +``` + +### 添加新的动物种类 +```python +# 在AnimalSimulator中添加新的动物种类定义 +new_animal = { + 'name': '新动物', + 'diet': 'herbivore', + 'reproduction_rate': 0.1, + 'preferred_food': ['grass', 'flower'] +} +``` + +### 自定义生长模型 +```python +# 在GrowthSimulator中实现新的生长模型 +def custom_growth_function(time, params): + # 自定义生长算法 + return size +``` + +## 故障排除 + +### 常见问题 + +1. **性能问题** + - 降低渲染距离 + - 减少最大实例数 + - 使用更低的LOD级别 + +2. **植被分布不自然** + - 调整环境权重 + - 更改分布算法 + - 修改聚集因子 + +3. **动物行为异常** + - 检查食物链配置 + - 调整行为参数 + - 验证环境适应性 + +### 支持 + +如遇到问题,请查看日志文件或联系开发者支持。 + +## 许可证 + +本插件遵循EG引擎的许可证协议。 + +## 版本历史 + - v1.0.0 - 初始版本,包含完整的植被和生态系统模拟功能 \ No newline at end of file diff --git a/plugins/user/vegetation_ecosystem/animals/animal_simulator.py b/plugins/user/vegetation_ecosystem/animals/animal_simulator.py index d06366a3..ecd1f79f 100644 --- a/plugins/user/vegetation_ecosystem/animals/animal_simulator.py +++ b/plugins/user/vegetation_ecosystem/animals/animal_simulator.py @@ -1,750 +1,750 @@ -""" -动物模拟器 -负责模拟动物种群行为和与植被的相互作用 -""" - -import time -from typing import Dict, Any, List, Optional -import math -import random - -class AnimalSimulator: - """ - 动物模拟器 - 负责模拟动物种群行为、繁殖、死亡和与植被的相互作用 - """ - - def __init__(self, plugin): - """ - 初始化动物模拟器 - - Args: - plugin: 植被和生态系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 动物种类定义 - self.animal_species = { - 'rabbit': { - 'name': '兔子', - 'description': '草食性小型哺乳动物,优秀的种子传播者', - 'diet': 'herbivore', - 'size': 'small', - 'reproduction_rate': 0.3, # 每天繁殖率 - 'mortality_rate': 0.02, # 每天死亡率 - 'mobility': 0.8, # 移动能力 - 'preferred_food': ['grass', 'flower'], - 'territory_size': 10.0, - 'lifespan': 365, # 天 - 'max_population': 50, - 'pollinator': False, - 'seed_disperser': True - }, - 'deer': { - 'name': '鹿', - 'description': '草食性中型哺乳动物,重要的种子传播者', - 'diet': 'herbivore', - 'size': 'medium', - 'reproduction_rate': 0.05, - 'mortality_rate': 0.01, - 'mobility': 0.9, - 'preferred_food': ['bush', 'tree', 'grass'], - 'territory_size': 50.0, - 'lifespan': 2190, # 6年 - 'max_population': 20, - 'pollinator': False, - 'seed_disperser': True - }, - 'wolf': { - 'name': '狼', - 'description': '肉食性大型哺乳动物,顶级捕食者', - 'diet': 'carnivore', - 'size': 'large', - 'reproduction_rate': 0.02, - 'mortality_rate': 0.005, - 'mobility': 0.95, - 'preferred_food': ['rabbit', 'deer'], - 'territory_size': 100.0, - 'lifespan': 2555, # 7年 - 'max_population': 10, - 'pollinator': False, - 'seed_disperser': False - }, - 'fox': { - 'name': '狐狸', - 'description': '杂食性小型哺乳动物', - 'diet': 'omnivore', - 'size': 'small', - 'reproduction_rate': 0.1, - 'mortality_rate': 0.015, - 'mobility': 0.85, - 'preferred_food': ['rabbit', 'flower', 'berry'], - 'territory_size': 30.0, - 'lifespan': 1460, # 4年 - 'max_population': 15, - 'pollinator': False, - 'seed_disperser': True - }, - 'bird': { - 'name': '鸟类', - 'description': '飞行动物,重要的授粉者和种子传播者', - 'diet': 'omnivore', - 'size': 'small', - 'reproduction_rate': 0.2, - 'mortality_rate': 0.03, - 'mobility': 1.0, - 'preferred_food': ['flower', 'berry', 'insect'], - 'territory_size': 200.0, - 'lifespan': 1095, # 3年 - 'max_population': 30, - 'pollinator': True, - 'seed_disperser': True - }, - 'insect': { - 'name': '昆虫', - 'description': '小型无脊椎动物,主要授粉者', - 'diet': 'omnivore', - 'size': 'tiny', - 'reproduction_rate': 0.5, - 'mortality_rate': 0.1, - 'mobility': 0.7, - 'preferred_food': ['flower', 'fruit', 'decaying_matter'], - 'territory_size': 1.0, - 'lifespan': 30, # 1个月 - 'max_population': 500, - 'pollinator': True, - 'seed_disperser': False - } - } - - # 动物种群 - self.animal_populations = {} - self.population_counter = 0 - - # 动物行为配置 - self.behavior_config = { - 'feeding_efficiency': 0.8, # 进食效率 - 'energy_consumption': 0.1, # 能量消耗率 - 'migration_probability': 0.05, # 迁移概率 - 'social_behavior': 0.7, # 社会行为倾向 - 'fear_factor': 0.6, # 恐惧因子 - 'territory_awareness': 0.8 # 领域意识 - } - - # 环境影响因子 - self.environment_factors = { - 'temperature': 20.0, - 'humidity': 60.0, - 'vegetation_density': 0.5, - 'water_availability': 0.7, - 'predator_presence': 0.2 - } - - # 动物实例 - self.animal_instances = {} - self.instance_counter = 0 - - # 动物状态跟踪 - self.animal_states = { - 'feeding': [], # 正在进食的动物 - 'moving': [], # 正在移动的动物 - 'resting': [], # 正在休息的动物 - 'mating': [] # 正在交配的动物 - } - - # 统计信息 - self.stats = { - 'total_animals': 0, - 'animals_by_species': {}, - 'births': 0, - 'deaths': 0, - 'feedings': 0, - 'movements': 0 - } - - print("✓ 动物模拟器已创建") - - def initialize(self) -> bool: - """ - 初始化动物模拟器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化统计数据 - self.stats['animals_by_species'] = {species: 0 for species in self.animal_species.keys()} - - # 初始化一些动物种群 - self._initialize_populations() - - self.initialized = True - print("✓ 动物模拟器初始化完成") - return True - - except Exception as e: - print(f"✗ 动物模拟器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用动物模拟器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 动物模拟器未初始化") - return False - - self.enabled = True - print("✓ 动物模拟器已启用") - return True - - except Exception as e: - print(f"✗ 动物模拟器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用动物模拟器""" - try: - self.enabled = False - print("✓ 动物模拟器已禁用") - - except Exception as e: - print(f"✗ 动物模拟器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理动物模拟器资源""" - try: - self.disable() - self.animal_populations.clear() - self.animal_instances.clear() - self.initialized = False - print("✓ 动物模拟器资源已清理") - - except Exception as e: - print(f"✗ 动物模拟器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新动物模拟器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新动物种群 - self._update_populations(dt) - - # 更新动物实例 - self._update_animal_instances(dt) - - # 更新动物行为 - self._update_animal_behaviors(dt) - - except Exception as e: - print(f"✗ 动物模拟器更新失败: {e}") - import traceback - traceback.print_exc() - - def _initialize_populations(self): - """初始化动物种群""" - try: - # 为每种动物创建初始种群 - for species_name in self.animal_species.keys(): - population_id = self.population_counter - self.population_counter += 1 - - initial_size = max(2, int(self.animal_species[species_name]['max_population'] * 0.1)) - - population_data = { - 'id': population_id, - 'species': species_name, - 'size': initial_size, - 'health': 0.8, - 'territory_center': (random.uniform(-50, 50), random.uniform(-50, 50)), - 'last_update': time.time(), - 'creation_time': time.time() - } - - self.animal_populations[population_id] = population_data - - # 更新统计数据 - self.stats['animals_by_species'][species_name] += initial_size - self.stats['total_animals'] += initial_size - - print(f"✓ {self.animal_species[species_name]['name']} 种群已初始化,大小: {initial_size}") - - except Exception as e: - print(f"✗ 动物种群初始化失败: {e}") - - def _update_populations(self, dt: float): - """ - 更新动物种群 - - Args: - dt: 时间增量 - """ - try: - populations_to_remove = [] - - for population_id, population in self.animal_populations.items(): - species_name = population['species'] - species_info = self.animal_species[species_name] - - # 计算繁殖和死亡 - current_size = population['size'] - - # 繁殖 - reproduction_rate = species_info['reproduction_rate'] - births = int(current_size * reproduction_rate * dt / 86400) # 转换为天 - - # 死亡 - mortality_rate = species_info['mortality_rate'] - deaths = int(current_size * mortality_rate * dt / 86400) - - # 环境压力影响 - environment_pressure = 1.0 - self._calculate_environment_suitability(species_name) - deaths += int(current_size * environment_pressure * 0.1) - - # 更新种群大小 - new_size = max(0, current_size + births - deaths) - population['size'] = min(new_size, species_info['max_population']) - - # 更新统计数据 - size_change = population['size'] - current_size - self.stats['total_animals'] += size_change - self.stats['animals_by_species'][species_name] += size_change - self.stats['births'] += births - self.stats['deaths'] += deaths - - # 如果种群灭绝,标记移除 - if population['size'] <= 0: - populations_to_remove.append(population_id) - - # 移除灭绝的种群 - for population_id in populations_to_remove: - population = self.animal_populations.pop(population_id) - species_name = population['species'] - print(f"✓ {self.animal_species[species_name]['name']} 种群已灭绝") - self.stats['animals_by_species'][species_name] = 0 - - except Exception as e: - print(f"✗ 动物种群更新失败: {e}") - - def _update_animal_instances(self, dt: float): - """更新动物实例""" - try: - # 更新动物实例状态 - for instance_id, instance in self.animal_instances.items(): - if not instance['alive']: - continue - - # 更新饥饿度 - instance['hunger'] = min(1.0, instance['hunger'] + 0.001 * dt / 3600) - - # 更新能量 - instance['energy'] = max(0.0, instance['energy'] - 0.0005 * dt / 3600) - - # 如果饥饿度太高或能量太低,健康度下降 - if instance['hunger'] > 0.8 or instance['energy'] < 0.2: - self.update_animal_health(instance_id, -0.0001 * dt / 3600) - - except Exception as e: - print(f"✗ 动物实例更新失败: {e}") - - def _update_animal_behaviors(self, dt: float): - """更新动物行为""" - try: - # 在实际实现中,这里会更新动物的行为状态 - # 如决定是否进食、移动、休息或交配 - pass - except Exception as e: - print(f"✗ 动物行为更新失败: {e}") - - def _calculate_environment_suitability(self, species_name: str) -> float: - """ - 计算环境适宜性 - - Args: - species_name: 动物种类名称 - - Returns: - 适宜性评分 (0.0-1.0) - """ - try: - species_info = self.animal_species[species_name] - - # 基于植被密度的适宜性 - vegetation_suitability = self.environment_factors['vegetation_density'] - - # 基于水可用性的适宜性 - water_suitability = self.environment_factors['water_availability'] - - # 基于温度的适宜性 - temperature = self.environment_factors['temperature'] - if species_name in ['rabbit', 'deer']: - # 草食动物适宜温度范围 - temp_suitability = max(0.0, 1.0 - abs(temperature - 20.0) / 20.0) - elif species_name in ['wolf', 'fox']: - # 肉食动物适宜温度范围 - temp_suitability = max(0.0, 1.0 - abs(temperature - 15.0) / 25.0) - else: - temp_suitability = 1.0 - - # 综合评分 - overall_suitability = ( - vegetation_suitability * 0.4 + - water_suitability * 0.3 + - temp_suitability * 0.3 - ) - - return max(0.0, min(1.0, overall_suitability)) - - except Exception as e: - print(f"✗ 环境适宜性计算失败: {e}") - return 0.5 - - def get_animal_species(self) -> List[str]: - """ - 获取所有动物种类 - - Returns: - 动物种类列表 - """ - return list(self.animal_species.keys()) - - def get_animal_info(self, species_name: str) -> Optional[Dict[str, Any]]: - """ - 获取动物种类信息 - - Args: - species_name: 动物种类名称 - - Returns: - 动物信息字典或None - """ - return self.animal_species.get(species_name) - - def create_animal_instance(self, species_name: str, position: tuple, - initial_age: float = 0.0, initial_health: float = 1.0) -> int: - """ - 创建动物实例 - - Args: - species_name: 动物种类名称 - position: 位置坐标 (x, y, z) - initial_age: 初始年龄 - initial_health: 初始健康度 (0.0-1.0) - - Returns: - 动物实例ID - """ - try: - if species_name not in self.animal_species: - print(f"✗ 无效的动物种类: {species_name}") - return -1 - - instance_id = self.instance_counter - self.instance_counter += 1 - - # 获取动物种类信息 - animal_info = self.animal_species[species_name] - - # 创建实例数据 - instance_data = { - 'id': instance_id, - 'species': species_name, - 'position': position, - 'age': initial_age, - 'health': initial_health, - 'alive': True, - 'hunger': 0.0, - 'energy': 1.0, - 'state': 'resting', # 初始状态为休息 - 'destination': None, # 移动目标 - 'creation_time': time.time(), - 'last_update': time.time() - } - - # 存储实例 - self.animal_instances[instance_id] = instance_data - - # 添加到状态跟踪 - self.animal_states['resting'].append(instance_id) - - print(f"✓ 动物实例已创建: {animal_info['name']} (ID: {instance_id})") - return instance_id - - except Exception as e: - print(f"✗ 动物实例创建失败: {e}") - import traceback - traceback.print_exc() - return -1 - - def remove_animal_instance(self, instance_id: int) -> bool: - """ - 移除动物实例 - - Args: - instance_id: 动物实例ID - - Returns: - 是否移除成功 - """ - try: - if instance_id not in self.animal_instances: - print(f"✗ 动物实例不存在: {instance_id}") - return False - - # 从状态跟踪中移除 - for state_list in self.animal_states.values(): - if instance_id in state_list: - state_list.remove(instance_id) - - # 移除实例 - del self.animal_instances[instance_id] - - print(f"✓ 动物实例已移除: ID {instance_id}") - return True - - except Exception as e: - print(f"✗ 动物实例移除失败: {e}") - import traceback - traceback.print_exc() - return False - - def get_animal_instance(self, instance_id: int) -> Optional[Dict[str, Any]]: - """ - 获取动物实例信息 - - Args: - instance_id: 动物实例ID - - Returns: - 动物实例信息或None - """ - return self.animal_instances.get(instance_id) - - def get_all_animal_instances(self) -> Dict[int, Dict[str, Any]]: - """ - 获取所有动物实例 - - Returns: - 所有动物实例字典 - """ - return self.animal_instances.copy() - - def update_environment_factors(self, factors: Dict[str, float]): - """ - 更新环境因子 - - Args: - factors: 环境因子字典 - """ - self.environment_factors.update(factors) - print(f"✓ 环境因子已更新: {factors}") - - def get_environment_factors(self) -> Dict[str, float]: - """ - 获取环境因子 - - Returns: - 环境因子字典 - """ - return self.environment_factors.copy() - - def get_population_data(self) -> Dict[int, Dict[str, Any]]: - """ - 获取种群数据 - - Returns: - 种群数据字典 - """ - return self.animal_populations.copy() - - def set_behavior_config(self, config: Dict[str, float]): - """ - 设置行为配置 - - Args: - config: 行为配置字典 - """ - self.behavior_config.update(config) - print(f"✓ 行为配置已更新: {self.behavior_config}") - - def get_behavior_config(self) -> Dict[str, float]: - """ - 获取行为配置 - - Returns: - 行为配置字典 - """ - return self.behavior_config.copy() - - def simulate_feeding(self, animal_species: str, food_type: str, amount: float) -> float: - """ - 模拟进食过程 - - Args: - animal_species: 动物种类 - food_type: 食物类型 - amount: 食物量 - - Returns: - 实际消耗的食物量 - """ - try: - if animal_species not in self.animal_species: - return 0.0 - - # 获取进食效率 - efficiency = self.behavior_config['feeding_efficiency'] - actual_consumption = amount * efficiency - - # 更新统计信息 - self.stats['feedings'] += 1 - - print(f"✓ {animal_species} 进食了 {actual_consumption:.2f} 单位的 {food_type}") - return actual_consumption - - except Exception as e: - print(f"✗ 进食模拟失败: {e}") - return 0.0 - - def update_animal_health(self, instance_id: int, health_change: float): - """ - 更新动物健康度 - - Args: - instance_id: 动物实例ID - health_change: 健康度变化值 - """ - try: - if instance_id not in self.animal_instances: - return - - instance = self.animal_instances[instance_id] - new_health = max(0.0, min(1.0, instance['health'] + health_change)) - instance['health'] = new_health - - # 如果健康度为0,动物死亡 - if new_health <= 0.0: - instance['alive'] = False - print(f"✓ 动物实例死亡: ID {instance_id}") - - except Exception as e: - print(f"✗ 动物健康度更新失败: {e}") - - def get_animal_states(self) -> Dict[str, List[int]]: - """ - 获取动物状态分布 - - Returns: - 动物状态字典 - """ - # 返回状态副本,避免外部修改 - return {state: ids.copy() for state, ids in self.animal_states.items()} - - def update_animal_state(self, instance_id: int, new_state: str) -> bool: - """ - 更新动物状态 - - Args: - instance_id: 动物实例ID - new_state: 新状态 - - Returns: - 是否更新成功 - """ - try: - if instance_id not in self.animal_instances: - print(f"✗ 动物实例不存在: {instance_id}") - return False - - if new_state not in self.animal_states: - print(f"✗ 无效的动物状态: {new_state}") - return False - - # 从旧状态列表中移除 - for state_list in self.animal_states.values(): - if instance_id in state_list: - state_list.remove(instance_id) - - # 添加到新状态列表 - self.animal_states[new_state].append(instance_id) - - # 更新实例状态 - self.animal_instances[instance_id]['state'] = new_state - - # 更新移动统计 - if new_state == 'moving': - self.stats['movements'] += 1 - - print(f"✓ 动物状态已更新: ID {instance_id} -> {new_state}") - return True - - except Exception as e: - print(f"✗ 动物状态更新失败: {e}") - return False - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'total_animals': 0, - 'animals_by_species': {species: 0 for species in self.animal_species.keys()}, - 'births': 0, - 'deaths': 0, - 'feedings': 0, - 'movements': 0 - } - print("✓ 动物模拟器统计信息已重置") - - def get_species_ecological_role(self, species_name: str) -> Dict[str, bool]: - """ - 获取物种的生态角色 - - Args: - species_name: 物种名称 - - Returns: - 生态角色字典 - """ - try: - if species_name not in self.animal_species: - return {} - - species_info = self.animal_species[species_name] - return { - 'pollinator': species_info.get('pollinator', False), - 'seed_disperser': species_info.get('seed_disperser', False), - 'predator': species_info['diet'] == 'carnivore', - 'herbivore': species_info['diet'] in ['herbivore', 'omnivore'] - } - except Exception as e: - print(f"✗ 生态角色获取失败: {e}") +""" +动物模拟器 +负责模拟动物种群行为和与植被的相互作用 +""" + +import time +from typing import Dict, Any, List, Optional +import math +import random + +class AnimalSimulator: + """ + 动物模拟器 + 负责模拟动物种群行为、繁殖、死亡和与植被的相互作用 + """ + + def __init__(self, plugin): + """ + 初始化动物模拟器 + + Args: + plugin: 植被和生态系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 动物种类定义 + self.animal_species = { + 'rabbit': { + 'name': '兔子', + 'description': '草食性小型哺乳动物,优秀的种子传播者', + 'diet': 'herbivore', + 'size': 'small', + 'reproduction_rate': 0.3, # 每天繁殖率 + 'mortality_rate': 0.02, # 每天死亡率 + 'mobility': 0.8, # 移动能力 + 'preferred_food': ['grass', 'flower'], + 'territory_size': 10.0, + 'lifespan': 365, # 天 + 'max_population': 50, + 'pollinator': False, + 'seed_disperser': True + }, + 'deer': { + 'name': '鹿', + 'description': '草食性中型哺乳动物,重要的种子传播者', + 'diet': 'herbivore', + 'size': 'medium', + 'reproduction_rate': 0.05, + 'mortality_rate': 0.01, + 'mobility': 0.9, + 'preferred_food': ['bush', 'tree', 'grass'], + 'territory_size': 50.0, + 'lifespan': 2190, # 6年 + 'max_population': 20, + 'pollinator': False, + 'seed_disperser': True + }, + 'wolf': { + 'name': '狼', + 'description': '肉食性大型哺乳动物,顶级捕食者', + 'diet': 'carnivore', + 'size': 'large', + 'reproduction_rate': 0.02, + 'mortality_rate': 0.005, + 'mobility': 0.95, + 'preferred_food': ['rabbit', 'deer'], + 'territory_size': 100.0, + 'lifespan': 2555, # 7年 + 'max_population': 10, + 'pollinator': False, + 'seed_disperser': False + }, + 'fox': { + 'name': '狐狸', + 'description': '杂食性小型哺乳动物', + 'diet': 'omnivore', + 'size': 'small', + 'reproduction_rate': 0.1, + 'mortality_rate': 0.015, + 'mobility': 0.85, + 'preferred_food': ['rabbit', 'flower', 'berry'], + 'territory_size': 30.0, + 'lifespan': 1460, # 4年 + 'max_population': 15, + 'pollinator': False, + 'seed_disperser': True + }, + 'bird': { + 'name': '鸟类', + 'description': '飞行动物,重要的授粉者和种子传播者', + 'diet': 'omnivore', + 'size': 'small', + 'reproduction_rate': 0.2, + 'mortality_rate': 0.03, + 'mobility': 1.0, + 'preferred_food': ['flower', 'berry', 'insect'], + 'territory_size': 200.0, + 'lifespan': 1095, # 3年 + 'max_population': 30, + 'pollinator': True, + 'seed_disperser': True + }, + 'insect': { + 'name': '昆虫', + 'description': '小型无脊椎动物,主要授粉者', + 'diet': 'omnivore', + 'size': 'tiny', + 'reproduction_rate': 0.5, + 'mortality_rate': 0.1, + 'mobility': 0.7, + 'preferred_food': ['flower', 'fruit', 'decaying_matter'], + 'territory_size': 1.0, + 'lifespan': 30, # 1个月 + 'max_population': 500, + 'pollinator': True, + 'seed_disperser': False + } + } + + # 动物种群 + self.animal_populations = {} + self.population_counter = 0 + + # 动物行为配置 + self.behavior_config = { + 'feeding_efficiency': 0.8, # 进食效率 + 'energy_consumption': 0.1, # 能量消耗率 + 'migration_probability': 0.05, # 迁移概率 + 'social_behavior': 0.7, # 社会行为倾向 + 'fear_factor': 0.6, # 恐惧因子 + 'territory_awareness': 0.8 # 领域意识 + } + + # 环境影响因子 + self.environment_factors = { + 'temperature': 20.0, + 'humidity': 60.0, + 'vegetation_density': 0.5, + 'water_availability': 0.7, + 'predator_presence': 0.2 + } + + # 动物实例 + self.animal_instances = {} + self.instance_counter = 0 + + # 动物状态跟踪 + self.animal_states = { + 'feeding': [], # 正在进食的动物 + 'moving': [], # 正在移动的动物 + 'resting': [], # 正在休息的动物 + 'mating': [] # 正在交配的动物 + } + + # 统计信息 + self.stats = { + 'total_animals': 0, + 'animals_by_species': {}, + 'births': 0, + 'deaths': 0, + 'feedings': 0, + 'movements': 0 + } + + print("✓ 动物模拟器已创建") + + def initialize(self) -> bool: + """ + 初始化动物模拟器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化统计数据 + self.stats['animals_by_species'] = {species: 0 for species in self.animal_species.keys()} + + # 初始化一些动物种群 + self._initialize_populations() + + self.initialized = True + print("✓ 动物模拟器初始化完成") + return True + + except Exception as e: + print(f"✗ 动物模拟器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用动物模拟器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 动物模拟器未初始化") + return False + + self.enabled = True + print("✓ 动物模拟器已启用") + return True + + except Exception as e: + print(f"✗ 动物模拟器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用动物模拟器""" + try: + self.enabled = False + print("✓ 动物模拟器已禁用") + + except Exception as e: + print(f"✗ 动物模拟器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理动物模拟器资源""" + try: + self.disable() + self.animal_populations.clear() + self.animal_instances.clear() + self.initialized = False + print("✓ 动物模拟器资源已清理") + + except Exception as e: + print(f"✗ 动物模拟器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新动物模拟器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新动物种群 + self._update_populations(dt) + + # 更新动物实例 + self._update_animal_instances(dt) + + # 更新动物行为 + self._update_animal_behaviors(dt) + + except Exception as e: + print(f"✗ 动物模拟器更新失败: {e}") + import traceback + traceback.print_exc() + + def _initialize_populations(self): + """初始化动物种群""" + try: + # 为每种动物创建初始种群 + for species_name in self.animal_species.keys(): + population_id = self.population_counter + self.population_counter += 1 + + initial_size = max(2, int(self.animal_species[species_name]['max_population'] * 0.1)) + + population_data = { + 'id': population_id, + 'species': species_name, + 'size': initial_size, + 'health': 0.8, + 'territory_center': (random.uniform(-50, 50), random.uniform(-50, 50)), + 'last_update': time.time(), + 'creation_time': time.time() + } + + self.animal_populations[population_id] = population_data + + # 更新统计数据 + self.stats['animals_by_species'][species_name] += initial_size + self.stats['total_animals'] += initial_size + + print(f"✓ {self.animal_species[species_name]['name']} 种群已初始化,大小: {initial_size}") + + except Exception as e: + print(f"✗ 动物种群初始化失败: {e}") + + def _update_populations(self, dt: float): + """ + 更新动物种群 + + Args: + dt: 时间增量 + """ + try: + populations_to_remove = [] + + for population_id, population in self.animal_populations.items(): + species_name = population['species'] + species_info = self.animal_species[species_name] + + # 计算繁殖和死亡 + current_size = population['size'] + + # 繁殖 + reproduction_rate = species_info['reproduction_rate'] + births = int(current_size * reproduction_rate * dt / 86400) # 转换为天 + + # 死亡 + mortality_rate = species_info['mortality_rate'] + deaths = int(current_size * mortality_rate * dt / 86400) + + # 环境压力影响 + environment_pressure = 1.0 - self._calculate_environment_suitability(species_name) + deaths += int(current_size * environment_pressure * 0.1) + + # 更新种群大小 + new_size = max(0, current_size + births - deaths) + population['size'] = min(new_size, species_info['max_population']) + + # 更新统计数据 + size_change = population['size'] - current_size + self.stats['total_animals'] += size_change + self.stats['animals_by_species'][species_name] += size_change + self.stats['births'] += births + self.stats['deaths'] += deaths + + # 如果种群灭绝,标记移除 + if population['size'] <= 0: + populations_to_remove.append(population_id) + + # 移除灭绝的种群 + for population_id in populations_to_remove: + population = self.animal_populations.pop(population_id) + species_name = population['species'] + print(f"✓ {self.animal_species[species_name]['name']} 种群已灭绝") + self.stats['animals_by_species'][species_name] = 0 + + except Exception as e: + print(f"✗ 动物种群更新失败: {e}") + + def _update_animal_instances(self, dt: float): + """更新动物实例""" + try: + # 更新动物实例状态 + for instance_id, instance in self.animal_instances.items(): + if not instance['alive']: + continue + + # 更新饥饿度 + instance['hunger'] = min(1.0, instance['hunger'] + 0.001 * dt / 3600) + + # 更新能量 + instance['energy'] = max(0.0, instance['energy'] - 0.0005 * dt / 3600) + + # 如果饥饿度太高或能量太低,健康度下降 + if instance['hunger'] > 0.8 or instance['energy'] < 0.2: + self.update_animal_health(instance_id, -0.0001 * dt / 3600) + + except Exception as e: + print(f"✗ 动物实例更新失败: {e}") + + def _update_animal_behaviors(self, dt: float): + """更新动物行为""" + try: + # 在实际实现中,这里会更新动物的行为状态 + # 如决定是否进食、移动、休息或交配 + pass + except Exception as e: + print(f"✗ 动物行为更新失败: {e}") + + def _calculate_environment_suitability(self, species_name: str) -> float: + """ + 计算环境适宜性 + + Args: + species_name: 动物种类名称 + + Returns: + 适宜性评分 (0.0-1.0) + """ + try: + species_info = self.animal_species[species_name] + + # 基于植被密度的适宜性 + vegetation_suitability = self.environment_factors['vegetation_density'] + + # 基于水可用性的适宜性 + water_suitability = self.environment_factors['water_availability'] + + # 基于温度的适宜性 + temperature = self.environment_factors['temperature'] + if species_name in ['rabbit', 'deer']: + # 草食动物适宜温度范围 + temp_suitability = max(0.0, 1.0 - abs(temperature - 20.0) / 20.0) + elif species_name in ['wolf', 'fox']: + # 肉食动物适宜温度范围 + temp_suitability = max(0.0, 1.0 - abs(temperature - 15.0) / 25.0) + else: + temp_suitability = 1.0 + + # 综合评分 + overall_suitability = ( + vegetation_suitability * 0.4 + + water_suitability * 0.3 + + temp_suitability * 0.3 + ) + + return max(0.0, min(1.0, overall_suitability)) + + except Exception as e: + print(f"✗ 环境适宜性计算失败: {e}") + return 0.5 + + def get_animal_species(self) -> List[str]: + """ + 获取所有动物种类 + + Returns: + 动物种类列表 + """ + return list(self.animal_species.keys()) + + def get_animal_info(self, species_name: str) -> Optional[Dict[str, Any]]: + """ + 获取动物种类信息 + + Args: + species_name: 动物种类名称 + + Returns: + 动物信息字典或None + """ + return self.animal_species.get(species_name) + + def create_animal_instance(self, species_name: str, position: tuple, + initial_age: float = 0.0, initial_health: float = 1.0) -> int: + """ + 创建动物实例 + + Args: + species_name: 动物种类名称 + position: 位置坐标 (x, y, z) + initial_age: 初始年龄 + initial_health: 初始健康度 (0.0-1.0) + + Returns: + 动物实例ID + """ + try: + if species_name not in self.animal_species: + print(f"✗ 无效的动物种类: {species_name}") + return -1 + + instance_id = self.instance_counter + self.instance_counter += 1 + + # 获取动物种类信息 + animal_info = self.animal_species[species_name] + + # 创建实例数据 + instance_data = { + 'id': instance_id, + 'species': species_name, + 'position': position, + 'age': initial_age, + 'health': initial_health, + 'alive': True, + 'hunger': 0.0, + 'energy': 1.0, + 'state': 'resting', # 初始状态为休息 + 'destination': None, # 移动目标 + 'creation_time': time.time(), + 'last_update': time.time() + } + + # 存储实例 + self.animal_instances[instance_id] = instance_data + + # 添加到状态跟踪 + self.animal_states['resting'].append(instance_id) + + print(f"✓ 动物实例已创建: {animal_info['name']} (ID: {instance_id})") + return instance_id + + except Exception as e: + print(f"✗ 动物实例创建失败: {e}") + import traceback + traceback.print_exc() + return -1 + + def remove_animal_instance(self, instance_id: int) -> bool: + """ + 移除动物实例 + + Args: + instance_id: 动物实例ID + + Returns: + 是否移除成功 + """ + try: + if instance_id not in self.animal_instances: + print(f"✗ 动物实例不存在: {instance_id}") + return False + + # 从状态跟踪中移除 + for state_list in self.animal_states.values(): + if instance_id in state_list: + state_list.remove(instance_id) + + # 移除实例 + del self.animal_instances[instance_id] + + print(f"✓ 动物实例已移除: ID {instance_id}") + return True + + except Exception as e: + print(f"✗ 动物实例移除失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_animal_instance(self, instance_id: int) -> Optional[Dict[str, Any]]: + """ + 获取动物实例信息 + + Args: + instance_id: 动物实例ID + + Returns: + 动物实例信息或None + """ + return self.animal_instances.get(instance_id) + + def get_all_animal_instances(self) -> Dict[int, Dict[str, Any]]: + """ + 获取所有动物实例 + + Returns: + 所有动物实例字典 + """ + return self.animal_instances.copy() + + def update_environment_factors(self, factors: Dict[str, float]): + """ + 更新环境因子 + + Args: + factors: 环境因子字典 + """ + self.environment_factors.update(factors) + print(f"✓ 环境因子已更新: {factors}") + + def get_environment_factors(self) -> Dict[str, float]: + """ + 获取环境因子 + + Returns: + 环境因子字典 + """ + return self.environment_factors.copy() + + def get_population_data(self) -> Dict[int, Dict[str, Any]]: + """ + 获取种群数据 + + Returns: + 种群数据字典 + """ + return self.animal_populations.copy() + + def set_behavior_config(self, config: Dict[str, float]): + """ + 设置行为配置 + + Args: + config: 行为配置字典 + """ + self.behavior_config.update(config) + print(f"✓ 行为配置已更新: {self.behavior_config}") + + def get_behavior_config(self) -> Dict[str, float]: + """ + 获取行为配置 + + Returns: + 行为配置字典 + """ + return self.behavior_config.copy() + + def simulate_feeding(self, animal_species: str, food_type: str, amount: float) -> float: + """ + 模拟进食过程 + + Args: + animal_species: 动物种类 + food_type: 食物类型 + amount: 食物量 + + Returns: + 实际消耗的食物量 + """ + try: + if animal_species not in self.animal_species: + return 0.0 + + # 获取进食效率 + efficiency = self.behavior_config['feeding_efficiency'] + actual_consumption = amount * efficiency + + # 更新统计信息 + self.stats['feedings'] += 1 + + print(f"✓ {animal_species} 进食了 {actual_consumption:.2f} 单位的 {food_type}") + return actual_consumption + + except Exception as e: + print(f"✗ 进食模拟失败: {e}") + return 0.0 + + def update_animal_health(self, instance_id: int, health_change: float): + """ + 更新动物健康度 + + Args: + instance_id: 动物实例ID + health_change: 健康度变化值 + """ + try: + if instance_id not in self.animal_instances: + return + + instance = self.animal_instances[instance_id] + new_health = max(0.0, min(1.0, instance['health'] + health_change)) + instance['health'] = new_health + + # 如果健康度为0,动物死亡 + if new_health <= 0.0: + instance['alive'] = False + print(f"✓ 动物实例死亡: ID {instance_id}") + + except Exception as e: + print(f"✗ 动物健康度更新失败: {e}") + + def get_animal_states(self) -> Dict[str, List[int]]: + """ + 获取动物状态分布 + + Returns: + 动物状态字典 + """ + # 返回状态副本,避免外部修改 + return {state: ids.copy() for state, ids in self.animal_states.items()} + + def update_animal_state(self, instance_id: int, new_state: str) -> bool: + """ + 更新动物状态 + + Args: + instance_id: 动物实例ID + new_state: 新状态 + + Returns: + 是否更新成功 + """ + try: + if instance_id not in self.animal_instances: + print(f"✗ 动物实例不存在: {instance_id}") + return False + + if new_state not in self.animal_states: + print(f"✗ 无效的动物状态: {new_state}") + return False + + # 从旧状态列表中移除 + for state_list in self.animal_states.values(): + if instance_id in state_list: + state_list.remove(instance_id) + + # 添加到新状态列表 + self.animal_states[new_state].append(instance_id) + + # 更新实例状态 + self.animal_instances[instance_id]['state'] = new_state + + # 更新移动统计 + if new_state == 'moving': + self.stats['movements'] += 1 + + print(f"✓ 动物状态已更新: ID {instance_id} -> {new_state}") + return True + + except Exception as e: + print(f"✗ 动物状态更新失败: {e}") + return False + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'total_animals': 0, + 'animals_by_species': {species: 0 for species in self.animal_species.keys()}, + 'births': 0, + 'deaths': 0, + 'feedings': 0, + 'movements': 0 + } + print("✓ 动物模拟器统计信息已重置") + + def get_species_ecological_role(self, species_name: str) -> Dict[str, bool]: + """ + 获取物种的生态角色 + + Args: + species_name: 物种名称 + + Returns: + 生态角色字典 + """ + try: + if species_name not in self.animal_species: + return {} + + species_info = self.animal_species[species_name] + return { + 'pollinator': species_info.get('pollinator', False), + 'seed_disperser': species_info.get('seed_disperser', False), + 'predator': species_info['diet'] == 'carnivore', + 'herbivore': species_info['diet'] in ['herbivore', 'omnivore'] + } + except Exception as e: + print(f"✗ 生态角色获取失败: {e}") return {} \ No newline at end of file diff --git a/plugins/user/vegetation_ecosystem/core/ecosystem_manager.py b/plugins/user/vegetation_ecosystem/core/ecosystem_manager.py index 3130c9e1..dc4cf098 100644 --- a/plugins/user/vegetation_ecosystem/core/ecosystem_manager.py +++ b/plugins/user/vegetation_ecosystem/core/ecosystem_manager.py @@ -1,620 +1,620 @@ -""" -生态系统管理器 -负责管理生态系统整体状态和生态关系 -""" - -import time -from typing import Dict, Any, List, Optional, Tuple -import math -import random - -class EcosystemManager: - """ - 生态系统管理器 - 负责管理生态系统整体状态、生态关系和系统平衡 - """ - - def __init__(self, plugin): - """ - 初始化生态系统管理器 - - Args: - plugin: 植被和生态系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 生态关系定义 - self.ecosystem_relationships = { - 'predator_prey': [], # 捕食者-猎物关系 - 'competition': [], # 竞争关系 - 'symbiosis': [], # 共生关系 - 'parasitism': [] # 寄生关系 - } - - # 生态位定义 - self.ecological_niches = { - 'primary_producer': { - 'name': '初级生产者', - 'description': '通过光合作用生产有机物', - 'organisms': ['grass', 'bush', 'tree', 'flower', 'fern', 'cactus', 'moss', 'vine'] - }, - 'primary_consumer': { - 'name': '初级消费者', - 'description': '以植物为食的动物', - 'organisms': [] - }, - 'secondary_consumer': { - 'name': '次级消费者', - 'description': '以初级消费者为食的动物', - 'organisms': [] - }, - 'tertiary_consumer': { - 'name': '三级消费者', - 'description': '以次级消费者为食的动物', - 'organisms': [] - }, - 'decomposer': { - 'name': '分解者', - 'description': '分解有机物的微生物和真菌', - 'organisms': [] - } - } - - # 食物链层级 - self.trophic_levels = { - 1: {'name': '生产者', 'organisms': []}, - 2: {'name': '初级消费者', 'organisms': []}, - 3: {'name': '次级消费者', 'organisms': []}, - 4: {'name': '三级消费者', 'organisms': []}, - 5: {'name': '顶级捕食者', 'organisms': []} - } - - # 生态系统参数 - self.ecosystem_parameters = { - 'biodiversity_index': 0.5, # 生物多样性指数 - 'stability_index': 0.7, # 系统稳定性指数 - 'resilience_index': 0.6, # 系统恢复力指数 - 'productivity_rate': 1.0, # 生产力比率 - 'energy_flow_efficiency': 0.1, # 能量传递效率 - 'carrying_capacity': 1000 # 环境承载能力 - } - - # 生态事件 - self.ecological_events = { - 'natural_disaster': { - 'probability': 0.001, # 每天发生概率 - 'effects': { - 'vegetation_loss': 0.3, - 'animal_death': 0.2, - 'soil_degradation': 0.1 - } - }, - 'disease_outbreak': { - 'probability': 0.005, - 'effects': { - 'population_reduction': 0.2, - 'growth_inhibition': 0.3 - } - }, - 'invasive_species': { - 'probability': 0.002, - 'effects': { - 'native_competition': 0.4, - 'habitat_displacement': 0.3 - } - } - } - - # 系统状态 - self.system_state = { - 'total_biomass': 0.0, - 'species_count': 0, - 'population_balance': 0.0, - 'resource_availability': 1.0, - 'last_event_time': 0.0 - } - - # 时间配置 - self.time_config = { - 'simulation_speed': 1.0, - 'day_length': 24.0, # 小时 - 'current_time': 0.0 - } - - # 统计信息 - self.stats = { - 'total_interactions': 0, - 'events_occurred': 0, - 'species_extinctions': 0, - 'new_species_introductions': 0 - } - - print("✓ 生态系统管理器已创建") - - def initialize(self) -> bool: - """ - 初始化生态系统管理器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 生态系统管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 生态系统管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用生态系统管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 生态系统管理器未初始化") - return False - - self.enabled = True - print("✓ 生态系统管理器已启用") - return True - - except Exception as e: - print(f"✗ 生态系统管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用生态系统管理器""" - try: - self.enabled = False - print("✓ 生态系统管理器已禁用") - - except Exception as e: - print(f"✗ 生态系统管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理生态系统管理器资源""" - try: - self.disable() - self.initialized = False - print("✓ 生态系统管理器资源已清理") - - except Exception as e: - print(f"✗ 生态系统管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新生态系统管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新时间 - self.time_config['current_time'] += dt * self.time_config['simulation_speed'] - - # 检查生态事件 - self._check_ecological_events(dt) - - # 更新系统状态 - self._update_system_state() - - except Exception as e: - print(f"✗ 生态系统管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _check_ecological_events(self, dt: float): - """ - 检查生态事件 - - Args: - dt: 时间增量 - """ - try: - # 计算事件概率(基于时间增量) - for event_name, event_data in self.ecological_events.items(): - probability = event_data['probability'] - adjusted_probability = 1 - math.pow(1 - probability, dt / 3600) # 转换为小时 - - if random.random() < adjusted_probability: - self._trigger_ecological_event(event_name) - - except Exception as e: - print(f"✗ 生态事件检查失败: {e}") - - def _trigger_ecological_event(self, event_name: str): - """ - 触发生态事件 - - Args: - event_name: 事件名称 - """ - try: - if event_name not in self.ecological_events: - return - - event_data = self.ecological_events[event_name] - effects = event_data['effects'] - - print(f"✓ 生态事件发生: {event_name}") - - # 应用事件效果 - if 'vegetation_loss' in effects and self.plugin.vegetation_manager: - # 减少植被数量 - loss_ratio = effects['vegetation_loss'] - veg_instances = self.plugin.vegetation_manager.get_all_vegetation_instances() - instances_to_remove = int(len(veg_instances) * loss_ratio) - - instance_ids = list(veg_instances.keys()) - for i in range(min(instances_to_remove, len(instance_ids))): - instance_id = random.choice(instance_ids) - self.plugin.vegetation_manager.remove_vegetation_instance(instance_id) - if instance_id in instance_ids: - instance_ids.remove(instance_id) - - # 更新统计信息 - self.stats['events_occurred'] += 1 - self.system_state['last_event_time'] = self.time_config['current_time'] - - except Exception as e: - print(f"✗ 生态事件触发失败: {e}") - - def _update_system_state(self): - """更新系统状态""" - try: - # 计算总生物量 - total_biomass = 0.0 - species_count = 0 - - if self.plugin.vegetation_manager: - veg_instances = self.plugin.vegetation_manager.get_all_vegetation_instances() - for instance in veg_instances.values(): - if instance['alive']: - veg_type = instance['type'] - veg_info = self.plugin.vegetation_manager.get_vegetation_info(veg_type) - if veg_info: - # 简单的生物量计算 - avg_height = (veg_info['height_range'][0] + veg_info['height_range'][1]) / 2 - total_biomass += avg_height * instance['health'] * instance['size'] - - species_count = len(veg_instances) - - self.system_state['total_biomass'] = total_biomass - self.system_state['species_count'] = species_count - - except Exception as e: - print(f"✗ 系统状态更新失败: {e}") - - def add_predator_prey_relationship(self, predator: str, prey: str, efficiency: float = 0.1): - """ - 添加捕食者-猎物关系 - - Args: - predator: 捕食者 - prey: 猎物 - efficiency: 捕食效率 - """ - try: - relationship = { - 'predator': predator, - 'prey': prey, - 'efficiency': efficiency, - 'creation_time': time.time() - } - - self.ecosystem_relationships['predator_prey'].append(relationship) - print(f"✓ 捕食者-猎物关系已添加: {predator} -> {prey}") - - except Exception as e: - print(f"✗ 捕食者-猎物关系添加失败: {e}") - - def add_competition_relationship(self, species1: str, species2: str, intensity: float = 0.5): - """ - 添加竞争关系 - - Args: - species1: 物种1 - species2: 物种2 - intensity: 竞争强度 - """ - try: - relationship = { - 'species1': species1, - 'species2': species2, - 'intensity': intensity, - 'creation_time': time.time() - } - - self.ecosystem_relationships['competition'].append(relationship) - print(f"✓ 竞争关系已添加: {species1} <-> {species2}") - - except Exception as e: - print(f"✗ 竞争关系添加失败: {e}") - - def add_symbiosis_relationship(self, species1: str, species2: str, benefit1: float = 0.2, benefit2: float = 0.2): - """ - 添加共生关系 - - Args: - species1: 物种1 - species2: 物种2 - benefit1: 物种1获得的益处 - benefit2: 物种2获得的益处 - """ - try: - relationship = { - 'species1': species1, - 'species2': species2, - 'benefit1': benefit1, - 'benefit2': benefit2, - 'creation_time': time.time() - } - - self.ecosystem_relationships['symbiosis'].append(relationship) - print(f"✓ 共生关系已添加: {species1} <-> {species2}") - - except Exception as e: - print(f"✗ 共生关系添加失败: {e}") - - def get_ecosystem_relationships(self, relationship_type: str = None) -> List[Dict[str, Any]]: - """ - 获取生态系统关系 - - Args: - relationship_type: 关系类型(如果为None,则返回所有关系) - - Returns: - 关系列表 - """ - try: - if relationship_type: - if relationship_type in self.ecosystem_relationships: - return self.ecosystem_relationships[relationship_type].copy() - else: - return [] - else: - all_relationships = [] - for rel_type, relationships in self.ecosystem_relationships.items(): - all_relationships.extend(relationships) - return all_relationships - - except Exception as e: - print(f"✗ 生态系统关系获取失败: {e}") - return [] - - def remove_ecosystem_relationship(self, relationship_type: str, index: int) -> bool: - """ - 移除生态系统关系 - - Args: - relationship_type: 关系类型 - index: 关系索引 - - Returns: - 是否移除成功 - """ - try: - if relationship_type not in self.ecosystem_relationships: - print(f"✗ 无效的关系类型: {relationship_type}") - return False - - if index < 0 or index >= len(self.ecosystem_relationships[relationship_type]): - print(f"✗ 无效的关系索引: {index}") - return False - - removed_relationship = self.ecosystem_relationships[relationship_type].pop(index) - print(f"✓ 生态系统关系已移除: {relationship_type} 关系") - return True - - except Exception as e: - print(f"✗ 生态系统关系移除失败: {e}") - return False - - def add_organism_to_niche(self, niche_name: str, organism: str) -> bool: - """ - 添加生物到生态位 - - Args: - niche_name: 生态位名称 - organism: 生物名称 - - Returns: - 是否添加成功 - """ - try: - if niche_name not in self.ecological_niches: - print(f"✗ 无效的生态位: {niche_name}") - return False - - if organism not in self.ecological_niches[niche_name]['organisms']: - self.ecological_niches[niche_name]['organisms'].append(organism) - print(f"✓ 生物已添加到生态位: {organism} -> {niche_name}") - return True - else: - print(f"✗ 生物已在生态位中: {organism}") - return False - - except Exception as e: - print(f"✗ 生物添加到生态位失败: {e}") - return False - - def get_organisms_in_niche(self, niche_name: str) -> List[str]: - """ - 获取生态位中的生物 - - Args: - niche_name: 生态位名称 - - Returns: - 生物列表 - """ - try: - if niche_name in self.ecological_niches: - return self.ecological_niches[niche_name]['organisms'].copy() - return [] - - except Exception as e: - print(f"✗ 生态位生物获取失败: {e}") - return [] - - def set_ecosystem_parameter(self, parameter: str, value: float): - """ - 设置生态系统参数 - - Args: - parameter: 参数名称 - value: 参数值 - """ - try: - if parameter in self.ecosystem_parameters: - self.ecosystem_parameters[parameter] = value - print(f"✓ 生态系统参数已设置: {parameter} = {value}") - else: - print(f"✗ 无效的生态系统参数: {parameter}") - - except Exception as e: - print(f"✗ 生态系统参数设置失败: {e}") - - def get_ecosystem_parameter(self, parameter: str) -> Optional[float]: - """ - 获取生态系统参数 - - Args: - parameter: 参数名称 - - Returns: - 参数值或None - """ - return self.ecosystem_parameters.get(parameter) - - def get_ecosystem_parameters(self) -> Dict[str, float]: - """ - 获取所有生态系统参数 - - Returns: - 参数字典 - """ - return self.ecosystem_parameters.copy() - - def set_time_config(self, config: Dict[str, float]): - """ - 设置时间配置 - - Args: - config: 时间配置字典 - """ - self.time_config.update(config) - print(f"✓ 时间配置已更新: {self.time_config}") - - def get_time_config(self) -> Dict[str, float]: - """ - 获取时间配置 - - Returns: - 时间配置字典 - """ - return self.time_config.copy() - - def get_system_state(self) -> Dict[str, Any]: - """ - 获取系统状态 - - Returns: - 系统状态字典 - """ - return self.system_state.copy() - - def get_biodiversity_index(self) -> float: - """ - 计算生物多样性指数 - - Returns: - 生物多样性指数 (0.0-1.0) - """ - try: - # 简化的多样性计算 - species_count = self.system_state['species_count'] - if species_count > 0: - # 基于物种数量的简单计算 - normalized_count = min(1.0, species_count / 50.0) # 假设50个物种为高多样性 - return normalized_count - return 0.0 - - except Exception as e: - print(f"✗ 生物多样性指数计算失败: {e}") - return 0.0 - - def get_stability_index(self) -> float: - """ - 计算系统稳定性指数 - - Returns: - 系统稳定性指数 (0.0-1.0) - """ - try: - # 简化的稳定性计算 - time_since_event = self.time_config['current_time'] - self.system_state['last_event_time'] - if time_since_event > 0: - # 基于事件间隔的时间稳定性 - stability = min(1.0, time_since_event / 86400.0) # 一天为完全稳定 - return stability - return 0.5 # 默认中等稳定性 - - except Exception as e: - print(f"✗ 系统稳定性指数计算失败: {e}") - return 0.5 - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'total_interactions': 0, - 'events_occurred': 0, - 'species_extinctions': 0, - 'new_species_introductions': 0 - } - print("✓ 生态系统管理器统计信息已重置") - - def get_ecological_niches(self) -> Dict[str, Dict[str, Any]]: - """ - 获取生态位信息 - - Returns: - 生态位字典 - """ - return self.ecological_niches.copy() - - def get_trophic_levels(self) -> Dict[int, Dict[str, Any]]: - """ - 获取营养级信息 - - Returns: - 营养级字典 - """ +""" +生态系统管理器 +负责管理生态系统整体状态和生态关系 +""" + +import time +from typing import Dict, Any, List, Optional, Tuple +import math +import random + +class EcosystemManager: + """ + 生态系统管理器 + 负责管理生态系统整体状态、生态关系和系统平衡 + """ + + def __init__(self, plugin): + """ + 初始化生态系统管理器 + + Args: + plugin: 植被和生态系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 生态关系定义 + self.ecosystem_relationships = { + 'predator_prey': [], # 捕食者-猎物关系 + 'competition': [], # 竞争关系 + 'symbiosis': [], # 共生关系 + 'parasitism': [] # 寄生关系 + } + + # 生态位定义 + self.ecological_niches = { + 'primary_producer': { + 'name': '初级生产者', + 'description': '通过光合作用生产有机物', + 'organisms': ['grass', 'bush', 'tree', 'flower', 'fern', 'cactus', 'moss', 'vine'] + }, + 'primary_consumer': { + 'name': '初级消费者', + 'description': '以植物为食的动物', + 'organisms': [] + }, + 'secondary_consumer': { + 'name': '次级消费者', + 'description': '以初级消费者为食的动物', + 'organisms': [] + }, + 'tertiary_consumer': { + 'name': '三级消费者', + 'description': '以次级消费者为食的动物', + 'organisms': [] + }, + 'decomposer': { + 'name': '分解者', + 'description': '分解有机物的微生物和真菌', + 'organisms': [] + } + } + + # 食物链层级 + self.trophic_levels = { + 1: {'name': '生产者', 'organisms': []}, + 2: {'name': '初级消费者', 'organisms': []}, + 3: {'name': '次级消费者', 'organisms': []}, + 4: {'name': '三级消费者', 'organisms': []}, + 5: {'name': '顶级捕食者', 'organisms': []} + } + + # 生态系统参数 + self.ecosystem_parameters = { + 'biodiversity_index': 0.5, # 生物多样性指数 + 'stability_index': 0.7, # 系统稳定性指数 + 'resilience_index': 0.6, # 系统恢复力指数 + 'productivity_rate': 1.0, # 生产力比率 + 'energy_flow_efficiency': 0.1, # 能量传递效率 + 'carrying_capacity': 1000 # 环境承载能力 + } + + # 生态事件 + self.ecological_events = { + 'natural_disaster': { + 'probability': 0.001, # 每天发生概率 + 'effects': { + 'vegetation_loss': 0.3, + 'animal_death': 0.2, + 'soil_degradation': 0.1 + } + }, + 'disease_outbreak': { + 'probability': 0.005, + 'effects': { + 'population_reduction': 0.2, + 'growth_inhibition': 0.3 + } + }, + 'invasive_species': { + 'probability': 0.002, + 'effects': { + 'native_competition': 0.4, + 'habitat_displacement': 0.3 + } + } + } + + # 系统状态 + self.system_state = { + 'total_biomass': 0.0, + 'species_count': 0, + 'population_balance': 0.0, + 'resource_availability': 1.0, + 'last_event_time': 0.0 + } + + # 时间配置 + self.time_config = { + 'simulation_speed': 1.0, + 'day_length': 24.0, # 小时 + 'current_time': 0.0 + } + + # 统计信息 + self.stats = { + 'total_interactions': 0, + 'events_occurred': 0, + 'species_extinctions': 0, + 'new_species_introductions': 0 + } + + print("✓ 生态系统管理器已创建") + + def initialize(self) -> bool: + """ + 初始化生态系统管理器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 生态系统管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 生态系统管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用生态系统管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 生态系统管理器未初始化") + return False + + self.enabled = True + print("✓ 生态系统管理器已启用") + return True + + except Exception as e: + print(f"✗ 生态系统管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用生态系统管理器""" + try: + self.enabled = False + print("✓ 生态系统管理器已禁用") + + except Exception as e: + print(f"✗ 生态系统管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理生态系统管理器资源""" + try: + self.disable() + self.initialized = False + print("✓ 生态系统管理器资源已清理") + + except Exception as e: + print(f"✗ 生态系统管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新生态系统管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新时间 + self.time_config['current_time'] += dt * self.time_config['simulation_speed'] + + # 检查生态事件 + self._check_ecological_events(dt) + + # 更新系统状态 + self._update_system_state() + + except Exception as e: + print(f"✗ 生态系统管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _check_ecological_events(self, dt: float): + """ + 检查生态事件 + + Args: + dt: 时间增量 + """ + try: + # 计算事件概率(基于时间增量) + for event_name, event_data in self.ecological_events.items(): + probability = event_data['probability'] + adjusted_probability = 1 - math.pow(1 - probability, dt / 3600) # 转换为小时 + + if random.random() < adjusted_probability: + self._trigger_ecological_event(event_name) + + except Exception as e: + print(f"✗ 生态事件检查失败: {e}") + + def _trigger_ecological_event(self, event_name: str): + """ + 触发生态事件 + + Args: + event_name: 事件名称 + """ + try: + if event_name not in self.ecological_events: + return + + event_data = self.ecological_events[event_name] + effects = event_data['effects'] + + print(f"✓ 生态事件发生: {event_name}") + + # 应用事件效果 + if 'vegetation_loss' in effects and self.plugin.vegetation_manager: + # 减少植被数量 + loss_ratio = effects['vegetation_loss'] + veg_instances = self.plugin.vegetation_manager.get_all_vegetation_instances() + instances_to_remove = int(len(veg_instances) * loss_ratio) + + instance_ids = list(veg_instances.keys()) + for i in range(min(instances_to_remove, len(instance_ids))): + instance_id = random.choice(instance_ids) + self.plugin.vegetation_manager.remove_vegetation_instance(instance_id) + if instance_id in instance_ids: + instance_ids.remove(instance_id) + + # 更新统计信息 + self.stats['events_occurred'] += 1 + self.system_state['last_event_time'] = self.time_config['current_time'] + + except Exception as e: + print(f"✗ 生态事件触发失败: {e}") + + def _update_system_state(self): + """更新系统状态""" + try: + # 计算总生物量 + total_biomass = 0.0 + species_count = 0 + + if self.plugin.vegetation_manager: + veg_instances = self.plugin.vegetation_manager.get_all_vegetation_instances() + for instance in veg_instances.values(): + if instance['alive']: + veg_type = instance['type'] + veg_info = self.plugin.vegetation_manager.get_vegetation_info(veg_type) + if veg_info: + # 简单的生物量计算 + avg_height = (veg_info['height_range'][0] + veg_info['height_range'][1]) / 2 + total_biomass += avg_height * instance['health'] * instance['size'] + + species_count = len(veg_instances) + + self.system_state['total_biomass'] = total_biomass + self.system_state['species_count'] = species_count + + except Exception as e: + print(f"✗ 系统状态更新失败: {e}") + + def add_predator_prey_relationship(self, predator: str, prey: str, efficiency: float = 0.1): + """ + 添加捕食者-猎物关系 + + Args: + predator: 捕食者 + prey: 猎物 + efficiency: 捕食效率 + """ + try: + relationship = { + 'predator': predator, + 'prey': prey, + 'efficiency': efficiency, + 'creation_time': time.time() + } + + self.ecosystem_relationships['predator_prey'].append(relationship) + print(f"✓ 捕食者-猎物关系已添加: {predator} -> {prey}") + + except Exception as e: + print(f"✗ 捕食者-猎物关系添加失败: {e}") + + def add_competition_relationship(self, species1: str, species2: str, intensity: float = 0.5): + """ + 添加竞争关系 + + Args: + species1: 物种1 + species2: 物种2 + intensity: 竞争强度 + """ + try: + relationship = { + 'species1': species1, + 'species2': species2, + 'intensity': intensity, + 'creation_time': time.time() + } + + self.ecosystem_relationships['competition'].append(relationship) + print(f"✓ 竞争关系已添加: {species1} <-> {species2}") + + except Exception as e: + print(f"✗ 竞争关系添加失败: {e}") + + def add_symbiosis_relationship(self, species1: str, species2: str, benefit1: float = 0.2, benefit2: float = 0.2): + """ + 添加共生关系 + + Args: + species1: 物种1 + species2: 物种2 + benefit1: 物种1获得的益处 + benefit2: 物种2获得的益处 + """ + try: + relationship = { + 'species1': species1, + 'species2': species2, + 'benefit1': benefit1, + 'benefit2': benefit2, + 'creation_time': time.time() + } + + self.ecosystem_relationships['symbiosis'].append(relationship) + print(f"✓ 共生关系已添加: {species1} <-> {species2}") + + except Exception as e: + print(f"✗ 共生关系添加失败: {e}") + + def get_ecosystem_relationships(self, relationship_type: str = None) -> List[Dict[str, Any]]: + """ + 获取生态系统关系 + + Args: + relationship_type: 关系类型(如果为None,则返回所有关系) + + Returns: + 关系列表 + """ + try: + if relationship_type: + if relationship_type in self.ecosystem_relationships: + return self.ecosystem_relationships[relationship_type].copy() + else: + return [] + else: + all_relationships = [] + for rel_type, relationships in self.ecosystem_relationships.items(): + all_relationships.extend(relationships) + return all_relationships + + except Exception as e: + print(f"✗ 生态系统关系获取失败: {e}") + return [] + + def remove_ecosystem_relationship(self, relationship_type: str, index: int) -> bool: + """ + 移除生态系统关系 + + Args: + relationship_type: 关系类型 + index: 关系索引 + + Returns: + 是否移除成功 + """ + try: + if relationship_type not in self.ecosystem_relationships: + print(f"✗ 无效的关系类型: {relationship_type}") + return False + + if index < 0 or index >= len(self.ecosystem_relationships[relationship_type]): + print(f"✗ 无效的关系索引: {index}") + return False + + removed_relationship = self.ecosystem_relationships[relationship_type].pop(index) + print(f"✓ 生态系统关系已移除: {relationship_type} 关系") + return True + + except Exception as e: + print(f"✗ 生态系统关系移除失败: {e}") + return False + + def add_organism_to_niche(self, niche_name: str, organism: str) -> bool: + """ + 添加生物到生态位 + + Args: + niche_name: 生态位名称 + organism: 生物名称 + + Returns: + 是否添加成功 + """ + try: + if niche_name not in self.ecological_niches: + print(f"✗ 无效的生态位: {niche_name}") + return False + + if organism not in self.ecological_niches[niche_name]['organisms']: + self.ecological_niches[niche_name]['organisms'].append(organism) + print(f"✓ 生物已添加到生态位: {organism} -> {niche_name}") + return True + else: + print(f"✗ 生物已在生态位中: {organism}") + return False + + except Exception as e: + print(f"✗ 生物添加到生态位失败: {e}") + return False + + def get_organisms_in_niche(self, niche_name: str) -> List[str]: + """ + 获取生态位中的生物 + + Args: + niche_name: 生态位名称 + + Returns: + 生物列表 + """ + try: + if niche_name in self.ecological_niches: + return self.ecological_niches[niche_name]['organisms'].copy() + return [] + + except Exception as e: + print(f"✗ 生态位生物获取失败: {e}") + return [] + + def set_ecosystem_parameter(self, parameter: str, value: float): + """ + 设置生态系统参数 + + Args: + parameter: 参数名称 + value: 参数值 + """ + try: + if parameter in self.ecosystem_parameters: + self.ecosystem_parameters[parameter] = value + print(f"✓ 生态系统参数已设置: {parameter} = {value}") + else: + print(f"✗ 无效的生态系统参数: {parameter}") + + except Exception as e: + print(f"✗ 生态系统参数设置失败: {e}") + + def get_ecosystem_parameter(self, parameter: str) -> Optional[float]: + """ + 获取生态系统参数 + + Args: + parameter: 参数名称 + + Returns: + 参数值或None + """ + return self.ecosystem_parameters.get(parameter) + + def get_ecosystem_parameters(self) -> Dict[str, float]: + """ + 获取所有生态系统参数 + + Returns: + 参数字典 + """ + return self.ecosystem_parameters.copy() + + def set_time_config(self, config: Dict[str, float]): + """ + 设置时间配置 + + Args: + config: 时间配置字典 + """ + self.time_config.update(config) + print(f"✓ 时间配置已更新: {self.time_config}") + + def get_time_config(self) -> Dict[str, float]: + """ + 获取时间配置 + + Returns: + 时间配置字典 + """ + return self.time_config.copy() + + def get_system_state(self) -> Dict[str, Any]: + """ + 获取系统状态 + + Returns: + 系统状态字典 + """ + return self.system_state.copy() + + def get_biodiversity_index(self) -> float: + """ + 计算生物多样性指数 + + Returns: + 生物多样性指数 (0.0-1.0) + """ + try: + # 简化的多样性计算 + species_count = self.system_state['species_count'] + if species_count > 0: + # 基于物种数量的简单计算 + normalized_count = min(1.0, species_count / 50.0) # 假设50个物种为高多样性 + return normalized_count + return 0.0 + + except Exception as e: + print(f"✗ 生物多样性指数计算失败: {e}") + return 0.0 + + def get_stability_index(self) -> float: + """ + 计算系统稳定性指数 + + Returns: + 系统稳定性指数 (0.0-1.0) + """ + try: + # 简化的稳定性计算 + time_since_event = self.time_config['current_time'] - self.system_state['last_event_time'] + if time_since_event > 0: + # 基于事件间隔的时间稳定性 + stability = min(1.0, time_since_event / 86400.0) # 一天为完全稳定 + return stability + return 0.5 # 默认中等稳定性 + + except Exception as e: + print(f"✗ 系统稳定性指数计算失败: {e}") + return 0.5 + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'total_interactions': 0, + 'events_occurred': 0, + 'species_extinctions': 0, + 'new_species_introductions': 0 + } + print("✓ 生态系统管理器统计信息已重置") + + def get_ecological_niches(self) -> Dict[str, Dict[str, Any]]: + """ + 获取生态位信息 + + Returns: + 生态位字典 + """ + return self.ecological_niches.copy() + + def get_trophic_levels(self) -> Dict[int, Dict[str, Any]]: + """ + 获取营养级信息 + + Returns: + 营养级字典 + """ return self.trophic_levels.copy() \ No newline at end of file diff --git a/plugins/user/vegetation_ecosystem/core/vegetation_manager.py b/plugins/user/vegetation_ecosystem/core/vegetation_manager.py index d5ac5421..b5af920a 100644 --- a/plugins/user/vegetation_ecosystem/core/vegetation_manager.py +++ b/plugins/user/vegetation_ecosystem/core/vegetation_manager.py @@ -1,615 +1,615 @@ -""" -植被管理器 -负责管理植被类型、分布和基本属性 -""" - -import time -from typing import Dict, Any, List, Optional -import math -import random - -class VegetationManager: - """ - 植被管理器 - 负责管理植被类型、分布、属性和基本操作 - """ - - def __init__(self, plugin): - """ - 初始化植被管理器 - - Args: - plugin: 植被和生态系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 植被类型定义 - self.vegetation_types = { - 'grass': { - 'name': '草地', - 'description': '低矮的草本植物', - 'height_range': (0.1, 0.5), - 'width_range': (0.1, 0.3), - 'color': (0.2, 0.8, 0.2), - 'growth_rate': 0.1, - 'lifespan': 365, # 天 - 'reproduction_rate': 0.05, - 'preferred_conditions': { - 'temperature': (10, 30), - 'humidity': (40, 80), - 'soil_fertility': (0.3, 1.0) - }, - 'competition_factor': 0.2, - 'resource_consumption': { - 'water': 0.1, - 'nutrients': 0.05 - } - }, - 'bush': { - 'name': '灌木', - 'description': '中等高度的木本植物', - 'height_range': (0.5, 2.0), - 'width_range': (0.5, 1.5), - 'color': (0.3, 0.6, 0.3), - 'growth_rate': 0.05, - 'lifespan': 1095, # 3年 - 'reproduction_rate': 0.02, - 'preferred_conditions': { - 'temperature': (5, 25), - 'humidity': (30, 70), - 'soil_fertility': (0.4, 1.0) - }, - 'competition_factor': 0.5, - 'resource_consumption': { - 'water': 0.3, - 'nutrients': 0.2 - } - }, - 'tree': { - 'name': '树木', - 'description': '高大的木本植物', - 'height_range': (5.0, 30.0), - 'width_range': (2.0, 10.0), - 'color': (0.1, 0.5, 0.1), - 'growth_rate': 0.01, - 'lifespan': 10950, # 30年 - 'reproduction_rate': 0.005, - 'preferred_conditions': { - 'temperature': (0, 30), - 'humidity': (40, 90), - 'soil_fertility': (0.5, 1.0) - }, - 'competition_factor': 0.8, - 'resource_consumption': { - 'water': 1.0, - 'nutrients': 0.5 - } - }, - 'flower': { - 'name': '花卉', - 'description': '开花植物', - 'height_range': (0.1, 1.0), - 'width_range': (0.1, 0.5), - 'color': (0.9, 0.3, 0.8), - 'growth_rate': 0.2, - 'lifespan': 180, # 半年 - 'reproduction_rate': 0.1, - 'preferred_conditions': { - 'temperature': (15, 25), - 'humidity': (50, 80), - 'soil_fertility': (0.6, 1.0) - }, - 'competition_factor': 0.1, - 'resource_consumption': { - 'water': 0.05, - 'nutrients': 0.03 - } - }, - 'fern': { - 'name': '蕨类植物', - 'description': '喜阴湿的古老植物', - 'height_range': (0.2, 1.5), - 'width_range': (0.2, 1.0), - 'color': (0.2, 0.6, 0.2), - 'growth_rate': 0.08, - 'lifespan': 730, # 2年 - 'reproduction_rate': 0.03, - 'preferred_conditions': { - 'temperature': (10, 20), - 'humidity': (70, 100), - 'soil_fertility': (0.4, 0.8) - }, - 'competition_factor': 0.3, - 'resource_consumption': { - 'water': 0.4, - 'nutrients': 0.1 - } - }, - 'cactus': { - 'name': '仙人掌', - 'description': '耐旱的多肉植物', - 'height_range': (0.1, 3.0), - 'width_range': (0.1, 1.0), - 'color': (0.2, 0.5, 0.2), - 'growth_rate': 0.005, - 'lifespan': 5475, # 15年 - 'reproduction_rate': 0.01, - 'preferred_conditions': { - 'temperature': (20, 40), - 'humidity': (10, 30), - 'soil_fertility': (0.1, 0.5) - }, - 'competition_factor': 0.1, - 'resource_consumption': { - 'water': 0.01, - 'nutrients': 0.02 - } - }, - 'moss': { - 'name': '苔藓', - 'description': '低等植物,喜阴湿', - 'height_range': (0.01, 0.1), - 'width_range': (0.05, 0.3), - 'color': (0.3, 0.5, 0.3), - 'growth_rate': 0.15, - 'lifespan': 365, # 1年 - 'reproduction_rate': 0.08, - 'preferred_conditions': { - 'temperature': (5, 20), - 'humidity': (80, 100), - 'soil_fertility': (0.1, 0.6) - }, - 'competition_factor': 0.1, - 'resource_consumption': { - 'water': 0.2, - 'nutrients': 0.01 - } - }, - 'vine': { - 'name': '藤蔓', - 'description': '攀援植物', - 'height_range': (1.0, 10.0), - 'width_range': (0.1, 0.5), - 'color': (0.2, 0.7, 0.2), - 'growth_rate': 0.12, - 'lifespan': 730, # 2年 - 'reproduction_rate': 0.06, - 'preferred_conditions': { - 'temperature': (15, 30), - 'humidity': (60, 90), - 'soil_fertility': (0.3, 0.8) - }, - 'competition_factor': 0.4, - 'resource_consumption': { - 'water': 0.2, - 'nutrients': 0.1 - } - } - } - - # 当前植被实例 - self.vegetation_instances = {} - self.instance_counter = 0 - - # 植被分布配置 - self.distribution_config = { - 'density': 1.0, # 植被密度 - 'clumping_factor': 0.7, # 聚集因子 - 'random_seed': 12345, # 随机种子 - 'spawn_area': ((-100, -100), (100, 100)) # 生成区域 - } - - # 环境影响因子 - self.environment_factors = { - 'temperature': 20.0, - 'humidity': 60.0, - 'soil_fertility': 0.5, - 'light_level': 0.8, - 'water_level': 0.6 - } - - # 统计信息 - self.stats = { - 'total_vegetation': 0, - 'vegetation_by_type': {}, - 'total_growth_cycles': 0, - 'total_reproductions': 0 - } - - print("✓ 植被管理器已创建") - - def initialize(self) -> bool: - """ - 初始化植被管理器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化统计数据 - self.stats['vegetation_by_type'] = {vt: 0 for vt in self.vegetation_types.keys()} - - self.initialized = True - print("✓ 植被管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 植被管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用植被管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 植被管理器未初始化") - return False - - self.enabled = True - print("✓ 植被管理器已启用") - return True - - except Exception as e: - print(f"✗ 植被管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用植被管理器""" - try: - self.enabled = False - print("✓ 植被管理器已禁用") - - except Exception as e: - print(f"✗ 植被管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理植被管理器资源""" - try: - self.disable() - self.vegetation_instances.clear() - self.initialized = False - print("✓ 植被管理器资源已清理") - - except Exception as e: - print(f"✗ 植被管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新植被管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新统计数据 - self.stats['total_growth_cycles'] += 1 - - except Exception as e: - print(f"✗ 植被管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def get_vegetation_types(self) -> List[str]: - """ - 获取所有植被类型 - - Returns: - 植被类型列表 - """ - return list(self.vegetation_types.keys()) - - def get_vegetation_info(self, vegetation_type: str) -> Optional[Dict[str, Any]]: - """ - 获取植被类型信息 - - Args: - vegetation_type: 植被类型 - - Returns: - 植被信息字典或None - """ - return self.vegetation_types.get(vegetation_type) - - def create_vegetation_instance(self, vegetation_type: str, position: tuple, - initial_age: float = 0.0, initial_health: float = 1.0) -> int: - """ - 创建植被实例 - - Args: - vegetation_type: 植被类型 - position: 位置坐标 (x, y, z) - initial_age: 初始年龄 - initial_health: 初始健康度 (0.0-1.0) - - Returns: - 植被实例ID - """ - try: - if vegetation_type not in self.vegetation_types: - print(f"✗ 无效的植被类型: {vegetation_type}") - return -1 - - instance_id = self.instance_counter - self.instance_counter += 1 - - # 获取植被类型信息 - veg_info = self.vegetation_types[vegetation_type] - - # 创建实例数据 - instance_data = { - 'id': instance_id, - 'type': vegetation_type, - 'position': position, - 'age': initial_age, - 'health': initial_health, - 'size': self._calculate_initial_size(vegetation_type, initial_age), - 'alive': True, - 'creation_time': time.time(), - 'last_update': time.time() - } - - # 存储实例 - self.vegetation_instances[instance_id] = instance_data - - # 更新统计数据 - self.stats['total_vegetation'] += 1 - self.stats['vegetation_by_type'][vegetation_type] += 1 - - print(f"✓ 植被实例已创建: {veg_info['name']} (ID: {instance_id})") - return instance_id - - except Exception as e: - print(f"✗ 植被实例创建失败: {e}") - import traceback - traceback.print_exc() - return -1 - - def _calculate_initial_size(self, vegetation_type: str, age: float) -> float: - """ - 计算初始大小 - - Args: - vegetation_type: 植被类型 - age: 年龄 - - Returns: - 大小比例 (0.0-1.0) - """ - try: - veg_info = self.vegetation_types[vegetation_type] - max_age = veg_info['lifespan'] - - # 简单的生长模型 - if max_age > 0: - size_ratio = min(1.0, age / max_age) - return size_ratio - return 0.1 - - except Exception as e: - print(f"✗ 初始大小计算失败: {e}") - return 0.1 - - def remove_vegetation_instance(self, instance_id: int) -> bool: - """ - 移除植被实例 - - Args: - instance_id: 植被实例ID - - Returns: - 是否移除成功 - """ - try: - if instance_id not in self.vegetation_instances: - print(f"✗ 植被实例不存在: {instance_id}") - return False - - instance_data = self.vegetation_instances[instance_id] - vegetation_type = instance_data['type'] - - # 移除实例 - del self.vegetation_instances[instance_id] - - # 更新统计数据 - self.stats['total_vegetation'] -= 1 - self.stats['vegetation_by_type'][vegetation_type] -= 1 - - print(f"✓ 植被实例已移除: ID {instance_id}") - return True - - except Exception as e: - print(f"✗ 植被实例移除失败: {e}") - import traceback - traceback.print_exc() - return False - - def get_vegetation_instance(self, instance_id: int) -> Optional[Dict[str, Any]]: - """ - 获取植被实例信息 - - Args: - instance_id: 植被实例ID - - Returns: - 植被实例信息或None - """ - return self.vegetation_instances.get(instance_id) - - def get_all_vegetation_instances(self) -> Dict[int, Dict[str, Any]]: - """ - 获取所有植被实例 - - Returns: - 所有植被实例字典 - """ - return self.vegetation_instances.copy() - - def update_vegetation_health(self, instance_id: int, health_change: float): - """ - 更新植被健康度 - - Args: - instance_id: 植被实例ID - health_change: 健康度变化值 - """ - try: - if instance_id not in self.vegetation_instances: - return - - instance = self.vegetation_instances[instance_id] - new_health = max(0.0, min(1.0, instance['health'] + health_change)) - instance['health'] = new_health - - # 如果健康度为0,植物死亡 - if new_health <= 0.0: - instance['alive'] = False - print(f"✓ 植被实例死亡: ID {instance_id}") - - except Exception as e: - print(f"✗ 植被健康度更新失败: {e}") - - def update_environment_factors(self, factors: Dict[str, float]): - """ - 更新环境因子 - - Args: - factors: 环境因子字典 - """ - self.environment_factors.update(factors) - print(f"✓ 环境因子已更新: {factors}") - - def get_environment_factors(self) -> Dict[str, float]: - """ - 获取环境因子 - - Returns: - 环境因子字典 - """ - return self.environment_factors.copy() - - def get_suitability_score(self, vegetation_type: str) -> float: - """ - 计算植被类型的适宜性评分 - - Args: - vegetation_type: 植被类型 - - Returns: - 适宜性评分 (0.0-1.0) - """ - try: - if vegetation_type not in self.vegetation_types: - return 0.0 - - veg_info = self.vegetation_types[vegetation_type] - preferred = veg_info['preferred_conditions'] - - # 计算各环境因子的适宜性 - temperature_suitability = self._calculate_factor_suitability( - self.environment_factors['temperature'], - preferred['temperature'] - ) - - humidity_suitability = self._calculate_factor_suitability( - self.environment_factors['humidity'], - preferred['humidity'] - ) - - fertility_suitability = self._calculate_factor_suitability( - self.environment_factors['soil_fertility'] * 100, - (preferred['soil_fertility'][0] * 100, preferred['soil_fertility'][1] * 100) - ) - - # 综合评分 - overall_suitability = ( - temperature_suitability * 0.4 + - humidity_suitability * 0.4 + - fertility_suitability * 0.2 - ) - - return max(0.0, min(1.0, overall_suitability)) - - except Exception as e: - print(f"✗ 适宜性评分计算失败: {e}") - return 0.0 - - def _calculate_factor_suitability(self, current_value: float, preferred_range: tuple) -> float: - """ - 计算单个环境因子的适宜性 - - Args: - current_value: 当前值 - preferred_range: 适宜范围 (min, max) - - Returns: - 适宜性评分 (0.0-1.0) - """ - min_val, max_val = preferred_range - - if min_val <= current_value <= max_val: - return 1.0 - elif current_value < min_val: - # 线性递减到0 - if min_val > 0: - return max(0.0, 1.0 - (min_val - current_value) / min_val) - return 0.0 - else: # current_value > max_val - # 线性递减到0 - if max_val > 0: - return max(0.0, 1.0 - (current_value - max_val) / max_val) - return 0.0 - - def set_distribution_config(self, config: Dict[str, Any]): - """ - 设置分布配置 - - Args: - config: 配置字典 - """ - self.distribution_config.update(config) - print(f"✓ 分布配置已更新: {self.distribution_config}") - - def get_distribution_config(self) -> Dict[str, Any]: - """ - 获取分布配置 - - Returns: - 分布配置字典 - """ - return self.distribution_config.copy() - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'total_vegetation': 0, - 'vegetation_by_type': {vt: 0 for vt in self.vegetation_types.keys()}, - 'total_growth_cycles': 0, - 'total_reproductions': 0 - } +""" +植被管理器 +负责管理植被类型、分布和基本属性 +""" + +import time +from typing import Dict, Any, List, Optional +import math +import random + +class VegetationManager: + """ + 植被管理器 + 负责管理植被类型、分布、属性和基本操作 + """ + + def __init__(self, plugin): + """ + 初始化植被管理器 + + Args: + plugin: 植被和生态系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 植被类型定义 + self.vegetation_types = { + 'grass': { + 'name': '草地', + 'description': '低矮的草本植物', + 'height_range': (0.1, 0.5), + 'width_range': (0.1, 0.3), + 'color': (0.2, 0.8, 0.2), + 'growth_rate': 0.1, + 'lifespan': 365, # 天 + 'reproduction_rate': 0.05, + 'preferred_conditions': { + 'temperature': (10, 30), + 'humidity': (40, 80), + 'soil_fertility': (0.3, 1.0) + }, + 'competition_factor': 0.2, + 'resource_consumption': { + 'water': 0.1, + 'nutrients': 0.05 + } + }, + 'bush': { + 'name': '灌木', + 'description': '中等高度的木本植物', + 'height_range': (0.5, 2.0), + 'width_range': (0.5, 1.5), + 'color': (0.3, 0.6, 0.3), + 'growth_rate': 0.05, + 'lifespan': 1095, # 3年 + 'reproduction_rate': 0.02, + 'preferred_conditions': { + 'temperature': (5, 25), + 'humidity': (30, 70), + 'soil_fertility': (0.4, 1.0) + }, + 'competition_factor': 0.5, + 'resource_consumption': { + 'water': 0.3, + 'nutrients': 0.2 + } + }, + 'tree': { + 'name': '树木', + 'description': '高大的木本植物', + 'height_range': (5.0, 30.0), + 'width_range': (2.0, 10.0), + 'color': (0.1, 0.5, 0.1), + 'growth_rate': 0.01, + 'lifespan': 10950, # 30年 + 'reproduction_rate': 0.005, + 'preferred_conditions': { + 'temperature': (0, 30), + 'humidity': (40, 90), + 'soil_fertility': (0.5, 1.0) + }, + 'competition_factor': 0.8, + 'resource_consumption': { + 'water': 1.0, + 'nutrients': 0.5 + } + }, + 'flower': { + 'name': '花卉', + 'description': '开花植物', + 'height_range': (0.1, 1.0), + 'width_range': (0.1, 0.5), + 'color': (0.9, 0.3, 0.8), + 'growth_rate': 0.2, + 'lifespan': 180, # 半年 + 'reproduction_rate': 0.1, + 'preferred_conditions': { + 'temperature': (15, 25), + 'humidity': (50, 80), + 'soil_fertility': (0.6, 1.0) + }, + 'competition_factor': 0.1, + 'resource_consumption': { + 'water': 0.05, + 'nutrients': 0.03 + } + }, + 'fern': { + 'name': '蕨类植物', + 'description': '喜阴湿的古老植物', + 'height_range': (0.2, 1.5), + 'width_range': (0.2, 1.0), + 'color': (0.2, 0.6, 0.2), + 'growth_rate': 0.08, + 'lifespan': 730, # 2年 + 'reproduction_rate': 0.03, + 'preferred_conditions': { + 'temperature': (10, 20), + 'humidity': (70, 100), + 'soil_fertility': (0.4, 0.8) + }, + 'competition_factor': 0.3, + 'resource_consumption': { + 'water': 0.4, + 'nutrients': 0.1 + } + }, + 'cactus': { + 'name': '仙人掌', + 'description': '耐旱的多肉植物', + 'height_range': (0.1, 3.0), + 'width_range': (0.1, 1.0), + 'color': (0.2, 0.5, 0.2), + 'growth_rate': 0.005, + 'lifespan': 5475, # 15年 + 'reproduction_rate': 0.01, + 'preferred_conditions': { + 'temperature': (20, 40), + 'humidity': (10, 30), + 'soil_fertility': (0.1, 0.5) + }, + 'competition_factor': 0.1, + 'resource_consumption': { + 'water': 0.01, + 'nutrients': 0.02 + } + }, + 'moss': { + 'name': '苔藓', + 'description': '低等植物,喜阴湿', + 'height_range': (0.01, 0.1), + 'width_range': (0.05, 0.3), + 'color': (0.3, 0.5, 0.3), + 'growth_rate': 0.15, + 'lifespan': 365, # 1年 + 'reproduction_rate': 0.08, + 'preferred_conditions': { + 'temperature': (5, 20), + 'humidity': (80, 100), + 'soil_fertility': (0.1, 0.6) + }, + 'competition_factor': 0.1, + 'resource_consumption': { + 'water': 0.2, + 'nutrients': 0.01 + } + }, + 'vine': { + 'name': '藤蔓', + 'description': '攀援植物', + 'height_range': (1.0, 10.0), + 'width_range': (0.1, 0.5), + 'color': (0.2, 0.7, 0.2), + 'growth_rate': 0.12, + 'lifespan': 730, # 2年 + 'reproduction_rate': 0.06, + 'preferred_conditions': { + 'temperature': (15, 30), + 'humidity': (60, 90), + 'soil_fertility': (0.3, 0.8) + }, + 'competition_factor': 0.4, + 'resource_consumption': { + 'water': 0.2, + 'nutrients': 0.1 + } + } + } + + # 当前植被实例 + self.vegetation_instances = {} + self.instance_counter = 0 + + # 植被分布配置 + self.distribution_config = { + 'density': 1.0, # 植被密度 + 'clumping_factor': 0.7, # 聚集因子 + 'random_seed': 12345, # 随机种子 + 'spawn_area': ((-100, -100), (100, 100)) # 生成区域 + } + + # 环境影响因子 + self.environment_factors = { + 'temperature': 20.0, + 'humidity': 60.0, + 'soil_fertility': 0.5, + 'light_level': 0.8, + 'water_level': 0.6 + } + + # 统计信息 + self.stats = { + 'total_vegetation': 0, + 'vegetation_by_type': {}, + 'total_growth_cycles': 0, + 'total_reproductions': 0 + } + + print("✓ 植被管理器已创建") + + def initialize(self) -> bool: + """ + 初始化植被管理器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化统计数据 + self.stats['vegetation_by_type'] = {vt: 0 for vt in self.vegetation_types.keys()} + + self.initialized = True + print("✓ 植被管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 植被管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用植被管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 植被管理器未初始化") + return False + + self.enabled = True + print("✓ 植被管理器已启用") + return True + + except Exception as e: + print(f"✗ 植被管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用植被管理器""" + try: + self.enabled = False + print("✓ 植被管理器已禁用") + + except Exception as e: + print(f"✗ 植被管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理植被管理器资源""" + try: + self.disable() + self.vegetation_instances.clear() + self.initialized = False + print("✓ 植被管理器资源已清理") + + except Exception as e: + print(f"✗ 植被管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新植被管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新统计数据 + self.stats['total_growth_cycles'] += 1 + + except Exception as e: + print(f"✗ 植被管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def get_vegetation_types(self) -> List[str]: + """ + 获取所有植被类型 + + Returns: + 植被类型列表 + """ + return list(self.vegetation_types.keys()) + + def get_vegetation_info(self, vegetation_type: str) -> Optional[Dict[str, Any]]: + """ + 获取植被类型信息 + + Args: + vegetation_type: 植被类型 + + Returns: + 植被信息字典或None + """ + return self.vegetation_types.get(vegetation_type) + + def create_vegetation_instance(self, vegetation_type: str, position: tuple, + initial_age: float = 0.0, initial_health: float = 1.0) -> int: + """ + 创建植被实例 + + Args: + vegetation_type: 植被类型 + position: 位置坐标 (x, y, z) + initial_age: 初始年龄 + initial_health: 初始健康度 (0.0-1.0) + + Returns: + 植被实例ID + """ + try: + if vegetation_type not in self.vegetation_types: + print(f"✗ 无效的植被类型: {vegetation_type}") + return -1 + + instance_id = self.instance_counter + self.instance_counter += 1 + + # 获取植被类型信息 + veg_info = self.vegetation_types[vegetation_type] + + # 创建实例数据 + instance_data = { + 'id': instance_id, + 'type': vegetation_type, + 'position': position, + 'age': initial_age, + 'health': initial_health, + 'size': self._calculate_initial_size(vegetation_type, initial_age), + 'alive': True, + 'creation_time': time.time(), + 'last_update': time.time() + } + + # 存储实例 + self.vegetation_instances[instance_id] = instance_data + + # 更新统计数据 + self.stats['total_vegetation'] += 1 + self.stats['vegetation_by_type'][vegetation_type] += 1 + + print(f"✓ 植被实例已创建: {veg_info['name']} (ID: {instance_id})") + return instance_id + + except Exception as e: + print(f"✗ 植被实例创建失败: {e}") + import traceback + traceback.print_exc() + return -1 + + def _calculate_initial_size(self, vegetation_type: str, age: float) -> float: + """ + 计算初始大小 + + Args: + vegetation_type: 植被类型 + age: 年龄 + + Returns: + 大小比例 (0.0-1.0) + """ + try: + veg_info = self.vegetation_types[vegetation_type] + max_age = veg_info['lifespan'] + + # 简单的生长模型 + if max_age > 0: + size_ratio = min(1.0, age / max_age) + return size_ratio + return 0.1 + + except Exception as e: + print(f"✗ 初始大小计算失败: {e}") + return 0.1 + + def remove_vegetation_instance(self, instance_id: int) -> bool: + """ + 移除植被实例 + + Args: + instance_id: 植被实例ID + + Returns: + 是否移除成功 + """ + try: + if instance_id not in self.vegetation_instances: + print(f"✗ 植被实例不存在: {instance_id}") + return False + + instance_data = self.vegetation_instances[instance_id] + vegetation_type = instance_data['type'] + + # 移除实例 + del self.vegetation_instances[instance_id] + + # 更新统计数据 + self.stats['total_vegetation'] -= 1 + self.stats['vegetation_by_type'][vegetation_type] -= 1 + + print(f"✓ 植被实例已移除: ID {instance_id}") + return True + + except Exception as e: + print(f"✗ 植被实例移除失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_vegetation_instance(self, instance_id: int) -> Optional[Dict[str, Any]]: + """ + 获取植被实例信息 + + Args: + instance_id: 植被实例ID + + Returns: + 植被实例信息或None + """ + return self.vegetation_instances.get(instance_id) + + def get_all_vegetation_instances(self) -> Dict[int, Dict[str, Any]]: + """ + 获取所有植被实例 + + Returns: + 所有植被实例字典 + """ + return self.vegetation_instances.copy() + + def update_vegetation_health(self, instance_id: int, health_change: float): + """ + 更新植被健康度 + + Args: + instance_id: 植被实例ID + health_change: 健康度变化值 + """ + try: + if instance_id not in self.vegetation_instances: + return + + instance = self.vegetation_instances[instance_id] + new_health = max(0.0, min(1.0, instance['health'] + health_change)) + instance['health'] = new_health + + # 如果健康度为0,植物死亡 + if new_health <= 0.0: + instance['alive'] = False + print(f"✓ 植被实例死亡: ID {instance_id}") + + except Exception as e: + print(f"✗ 植被健康度更新失败: {e}") + + def update_environment_factors(self, factors: Dict[str, float]): + """ + 更新环境因子 + + Args: + factors: 环境因子字典 + """ + self.environment_factors.update(factors) + print(f"✓ 环境因子已更新: {factors}") + + def get_environment_factors(self) -> Dict[str, float]: + """ + 获取环境因子 + + Returns: + 环境因子字典 + """ + return self.environment_factors.copy() + + def get_suitability_score(self, vegetation_type: str) -> float: + """ + 计算植被类型的适宜性评分 + + Args: + vegetation_type: 植被类型 + + Returns: + 适宜性评分 (0.0-1.0) + """ + try: + if vegetation_type not in self.vegetation_types: + return 0.0 + + veg_info = self.vegetation_types[vegetation_type] + preferred = veg_info['preferred_conditions'] + + # 计算各环境因子的适宜性 + temperature_suitability = self._calculate_factor_suitability( + self.environment_factors['temperature'], + preferred['temperature'] + ) + + humidity_suitability = self._calculate_factor_suitability( + self.environment_factors['humidity'], + preferred['humidity'] + ) + + fertility_suitability = self._calculate_factor_suitability( + self.environment_factors['soil_fertility'] * 100, + (preferred['soil_fertility'][0] * 100, preferred['soil_fertility'][1] * 100) + ) + + # 综合评分 + overall_suitability = ( + temperature_suitability * 0.4 + + humidity_suitability * 0.4 + + fertility_suitability * 0.2 + ) + + return max(0.0, min(1.0, overall_suitability)) + + except Exception as e: + print(f"✗ 适宜性评分计算失败: {e}") + return 0.0 + + def _calculate_factor_suitability(self, current_value: float, preferred_range: tuple) -> float: + """ + 计算单个环境因子的适宜性 + + Args: + current_value: 当前值 + preferred_range: 适宜范围 (min, max) + + Returns: + 适宜性评分 (0.0-1.0) + """ + min_val, max_val = preferred_range + + if min_val <= current_value <= max_val: + return 1.0 + elif current_value < min_val: + # 线性递减到0 + if min_val > 0: + return max(0.0, 1.0 - (min_val - current_value) / min_val) + return 0.0 + else: # current_value > max_val + # 线性递减到0 + if max_val > 0: + return max(0.0, 1.0 - (current_value - max_val) / max_val) + return 0.0 + + def set_distribution_config(self, config: Dict[str, Any]): + """ + 设置分布配置 + + Args: + config: 配置字典 + """ + self.distribution_config.update(config) + print(f"✓ 分布配置已更新: {self.distribution_config}") + + def get_distribution_config(self) -> Dict[str, Any]: + """ + 获取分布配置 + + Returns: + 分布配置字典 + """ + return self.distribution_config.copy() + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'total_vegetation': 0, + 'vegetation_by_type': {vt: 0 for vt in self.vegetation_types.keys()}, + 'total_growth_cycles': 0, + 'total_reproductions': 0 + } print("✓ 植被管理器统计信息已重置") \ No newline at end of file diff --git a/plugins/user/vegetation_ecosystem/ecosystem/eco_interactions.py b/plugins/user/vegetation_ecosystem/ecosystem/eco_interactions.py index 4c4f9949..410c0b78 100644 --- a/plugins/user/vegetation_ecosystem/ecosystem/eco_interactions.py +++ b/plugins/user/vegetation_ecosystem/ecosystem/eco_interactions.py @@ -1,714 +1,714 @@ -""" -生态系统交互模块 -负责处理植被与动物之间以及生态系统内部的复杂交互 -""" - -import time -from typing import Dict, Any, List, Optional -import math -import random - -class EcoInteractions: - """ - 生态系统交互管理器 - 负责处理植被与动物之间以及生态系统内部的复杂交互 - """ - - def __init__(self, plugin): - """ - 初始化生态系统交互管理器 - - Args: - plugin: 植被和生态系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 交互类型定义 - self.interaction_types = { - 'herbivory': { - 'name': '草食行为', - 'description': '动物食用植物', - 'affects': ['vegetation', 'animal'], - 'rate_factor': 1.0 - }, - 'seed_dispersal': { - 'name': '种子传播', - 'description': '动物帮助植物传播种子', - 'affects': ['vegetation'], - 'rate_factor': 0.5 - }, - 'pollination': { - 'name': '授粉', - 'description': '动物为植物授粉', - 'affects': ['vegetation'], - 'rate_factor': 0.8 - }, - 'habitat_modification': { - 'name': '栖息地改造', - 'description': '动物活动改变环境', - 'affects': ['environment'], - 'rate_factor': 0.3 - }, - 'competition': { - 'name': '竞争', - 'description': '物种间资源竞争', - 'affects': ['vegetation', 'animal'], - 'rate_factor': 0.7 - }, - 'mutualism': { - 'name': '互利共生', - 'description': '物种间互利关系', - 'affects': ['vegetation', 'animal'], - 'rate_factor': 0.6 - } - } - - # 交互参数 - self.interaction_params = { - 'herbivory_rate': 0.05, # 草食率 - 'seed_dispersal_rate': 0.1, # 种子传播率 - 'pollination_rate': 0.15, # 授粉率 - 'competition_intensity': 0.2, # 竞争强度 - 'mutualism_benefit': 0.1 # 互利共生益处 - } - - # 交互效果 - self.interaction_effects = { - 'vegetation_damage': 0.0, # 植被损害 - 'vegetation_growth_boost': 0.0, # 植被生长促进 - 'animal_nutrition': 0.0, # 动物营养 - 'biodiversity_impact': 0.0 # 生物多样性影响 - } - - # 交互历史 - self.interaction_history = [] - self.max_history_size = 1000 - - # 时间配置 - self.time_config = { - 'update_interval': 60.0, # 更新间隔(秒) - 'last_update': 0.0 - } - - # 统计信息 - self.interaction_stats = { - 'total_interactions': 0, - 'herbivory_events': 0, - 'seed_dispersal_events': 0, - 'pollination_events': 0, - 'competition_events': 0, - 'mutualism_events': 0 - } - - print("✓ 生态系统交互管理器已创建") - - def initialize(self) -> bool: - """ - 初始化生态系统交互管理器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 生态系统交互管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 生态系统交互管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用生态系统交互管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 生态系统交互管理器未初始化") - return False - - self.enabled = True - print("✓ 生态系统交互管理器已启用") - return True - - except Exception as e: - print(f"✗ 生态系统交互管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用生态系统交互管理器""" - try: - self.enabled = False - print("✓ 生态系统交互管理器已禁用") - - except Exception as e: - print(f"✗ 生态系统交互管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理生态系统交互管理器资源""" - try: - self.disable() - self.interaction_history.clear() - self.initialized = False - print("✓ 生态系统交互管理器资源已清理") - - except Exception as e: - print(f"✗ 生态系统交互管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新生态系统交互管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新时间 - self.time_config['last_update'] += dt - - # 检查是否需要更新 - if self.time_config['last_update'] >= self.time_config['update_interval']: - # 处理生态系统交互 - self._process_ecosystem_interactions() - - # 重置更新计时器 - self.time_config['last_update'] = 0.0 - - except Exception as e: - print(f"✗ 生态系统交互管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _process_ecosystem_interactions(self): - """处理生态系统交互""" - try: - # 处理草食行为 - self._process_herbivory() - - # 处理种子传播 - self._process_seed_dispersal() - - # 处理授粉 - self._process_pollination() - - # 处理竞争 - self._process_competition() - - # 处理互利共生 - self._process_mutualism() - - # 更新统计信息 - self.interaction_stats['total_interactions'] += 1 - - except Exception as e: - print(f"✗ 生态系统交互处理失败: {e}") - - def _process_herbivory(self): - """处理草食行为""" - try: - if not self.plugin.animal_simulator or not self.plugin.vegetation_manager: - return - - # 获取动物和植被信息 - animal_populations = self.plugin.animal_simulator.get_population_data() - vegetation_instances = self.plugin.vegetation_manager.get_all_vegetation_instances() - - # 遍历动物种群 - for population_id, population_data in animal_populations.items(): - species = population_data['species'] - population_size = population_data['size'] - - # 检查动物是否为草食性 - animal_info = self.plugin.animal_simulator.get_animal_info(species) - if not animal_info or animal_info['diet'] not in ['herbivore', 'omnivore']: - continue - - # 计算草食事件概率 - herbivory_probability = self.interaction_params['herbivory_rate'] * population_size / 100 - - if random.random() < herbivory_probability: - # 选择一个植被实例作为目标 - if vegetation_instances: - target_instance_id = random.choice(list(vegetation_instances.keys())) - target_instance = vegetation_instances[target_instance_id] - - # 检查动物是否偏好这种植被 - preferred_food = animal_info.get('preferred_food', []) - if target_instance['type'] in preferred_food: - damage_factor = 1.5 # 偏好食物造成更多损害 - else: - damage_factor = 1.0 - - # 对植被造成损害 - damage = 0.05 * damage_factor - self.plugin.vegetation_manager.update_vegetation_health(target_instance_id, -damage) - - # 动物获得营养 - nutrition_gain = 0.02 * damage_factor - # 这里可以更新动物状态(如果动物实例系统支持) - - # 记录交互事件 - self._record_interaction_event('herbivory', { - 'animal_species': species, - 'vegetation_type': target_instance['type'], - 'damage': damage, - 'nutrition_gain': nutrition_gain - }) - - # 更新统计信息 - self.interaction_stats['herbivory_events'] += 1 - - except Exception as e: - print(f"✗ 草食行为处理失败: {e}") - - def _process_seed_dispersal(self): - """处理种子传播""" - try: - if not self.plugin.animal_simulator or not self.plugin.vegetation_manager: - return - - # 获取动物和植被信息 - animal_populations = self.plugin.animal_simulator.get_population_data() - vegetation_instances = self.plugin.vegetation_manager.get_all_vegetation_instances() - - # 遍历动物种群 - for population_id, population_data in animal_populations.items(): - species = population_data['species'] - population_size = population_data['size'] - - # 检查动物是否适合传播种子(鸟类、哺乳动物等) - animal_info = self.plugin.animal_simulator.get_animal_info(species) - if not animal_info or animal_info['size'] not in ['small', 'medium', 'large']: - continue - - # 计算种子传播概率 - dispersal_probability = self.interaction_params['seed_dispersal_rate'] * population_size / 100 - - if random.random() < dispersal_probability: - # 选择一个成熟的植被实例作为种子来源 - mature_vegetation = [ - (vid, vdata) for vid, vdata in vegetation_instances.items() - if vdata['alive'] and vdata['size'] > 0.7 - ] - - if mature_vegetation: - source_instance_id, source_instance = random.choice(mature_vegetation) - - # 计算新的生成位置(在动物活动范围内) - animal_territory = animal_info.get('territory_size', 30.0) - source_pos = source_instance['position'] - - angle = random.uniform(0, 2 * math.pi) - distance = random.uniform(0, animal_territory) - - new_x = source_pos[0] + distance * math.cos(angle) - new_z = source_pos[2] + distance * math.sin(angle) - - # 创建新的植被实例 - new_instance_id = self.plugin.vegetation_manager.create_vegetation_instance( - source_instance['type'], - (new_x, source_pos[1], new_z), - initial_age=0.0, - initial_health=0.3 - ) - - if new_instance_id >= 0: - # 记录交互事件 - self._record_interaction_event('seed_dispersal', { - 'animal_species': species, - 'vegetation_type': source_instance['type'], - 'source_position': source_pos, - 'new_position': (new_x, source_pos[1], new_z) - }) - - # 更新统计信息 - self.interaction_stats['seed_dispersal_events'] += 1 - - except Exception as e: - print(f"✗ 种子传播处理失败: {e}") - - def _process_pollination(self): - """处理授粉""" - try: - if not self.plugin.animal_simulator or not self.plugin.vegetation_manager: - return - - # 获取动物和植被信息 - animal_populations = self.plugin.animal_simulator.get_population_data() - vegetation_instances = self.plugin.vegetation_manager.get_all_vegetation_instances() - - # 遍历动物种群 - for population_id, population_data in animal_populations.items(): - species = population_data['species'] - population_size = population_data['size'] - - # 检查动物是否适合授粉(昆虫、鸟类等) - animal_info = self.plugin.animal_simulator.get_animal_info(species) - if not animal_info or 'pollinator' not in animal_info.get('description', ''): - # 特殊处理鸟类和昆虫 - if species not in ['bird', 'insect']: - continue - - # 计算授粉概率 - pollination_probability = self.interaction_params['pollination_rate'] * population_size / 100 - - if random.random() < pollination_probability: - # 选择一个开花的植被实例 - flowering_vegetation = [ - (vid, vdata) for vid, vdata in vegetation_instances.items() - if vdata['alive'] and vdata['type'] in ['flower'] and vdata['health'] > 0.5 - ] - - if flowering_vegetation: - target_instance_id, target_instance = random.choice(flowering_vegetation) - - # 提高植被的繁殖率 - # 注意:这里我们只是模拟效果,实际的繁殖率调整在其他模块中处理 - - # 记录交互事件 - self._record_interaction_event('pollination', { - 'animal_species': species, - 'vegetation_type': target_instance['type'], - 'position': target_instance['position'] - }) - - # 更新统计信息 - self.interaction_stats['pollination_events'] += 1 - - except Exception as e: - print(f"✗ 授粉处理失败: {e}") - - def _process_competition(self): - """处理竞争""" - try: - if not self.plugin.vegetation_manager: - return - - # 获取植被实例 - vegetation_instances = self.plugin.vegetation_manager.get_all_vegetation_instances() - - # 按位置分组植被实例,以模拟局部竞争 - position_groups = {} - group_radius = 5.0 # 竞争半径 - - for instance_id, instance in vegetation_instances.items(): - if not instance['alive']: - continue - - pos = instance['position'] - # 简化的分组方法:按坐标网格分组 - grid_x = int(pos[0] // group_radius) - grid_z = int(pos[2] // group_radius) - group_key = (grid_x, grid_z) - - if group_key not in position_groups: - position_groups[group_key] = [] - position_groups[group_key].append((instance_id, instance)) - - # 处理每个组内的竞争 - for group_instances in position_groups.values(): - if len(group_instances) < 2: - continue # 至少需要两个实例才能产生竞争 - - # 计算竞争强度 - competition_intensity = self.interaction_params['competition_intensity'] - - # 对组内的每个实例应用竞争影响 - for instance_id, instance in group_instances: - veg_type = instance['type'] - veg_info = self.plugin.vegetation_manager.get_vegetation_info(veg_type) - - if not veg_info: - continue - - # 竞争因子影响 - competition_factor = veg_info.get('competition_factor', 0.5) - total_competition = competition_intensity * competition_factor - - # 根据组内其他植物数量调整竞争强度 - other_plants_count = len(group_instances) - 1 - adjusted_competition = total_competition * (other_plants_count / 5.0) # 标准化 - - # 应用竞争影响(减少健康度) - health_impact = -adjusted_competition * 0.01 - self.plugin.vegetation_manager.update_vegetation_health(instance_id, health_impact) - - # 记录交互事件 - self._record_interaction_event('competition', { - 'vegetation_type': veg_type, - 'position': instance['position'], - 'competitors_count': other_plants_count, - 'health_impact': health_impact - }) - - # 更新统计信息 - self.interaction_stats['competition_events'] += 1 - - except Exception as e: - print(f"✗ 竞争处理失败: {e}") - - def _process_mutualism(self): - """处理互利共生""" - try: - if not self.plugin.animal_simulator or not self.plugin.vegetation_manager: - return - - # 获取动物和植被信息 - animal_populations = self.plugin.animal_simulator.get_population_data() - vegetation_instances = self.plugin.vegetation_manager.get_all_vegetation_instances() - - # 定义已知的互利共生关系 - mutualistic_pairs = [ - ('bird', 'tree'), # 鸟类与树木 - ('insect', 'flower'), # 昆虫与花卉 - ] - - # 检查是否存在互利共生关系 - for animal_species, vegetation_type in mutualistic_pairs: - # 查找对应的动物种群 - animal_populations_filtered = { - pid: pdata for pid, pdata in animal_populations.items() - if pdata['species'] == animal_species - } - - if not animal_populations_filtered: - continue - - # 查找对应的植被实例 - vegetation_instances_filtered = { - vid: vdata for vid, vdata in vegetation_instances.items() - if vdata['alive'] and vdata['type'] == vegetation_type - } - - if not vegetation_instances_filtered: - continue - - # 计算互利共生概率 - mutualism_probability = self.interaction_params['mutualism_benefit'] - - if random.random() < mutualism_probability: - # 随机选择一个动物种群和植被实例 - animal_population_id = random.choice(list(animal_populations_filtered.keys())) - animal_population = animal_populations_filtered[animal_population_id] - - vegetation_instance_id = random.choice(list(vegetation_instances_filtered.keys())) - vegetation_instance = vegetation_instances_filtered[vegetation_instance_id] - - # 双方都获得益处 - animal_benefit = 0.01 - vegetation_benefit = 0.005 - - # 更新动物种群健康度(这里简化处理) - # 实际应用中可能需要更新个体动物状态 - - # 更新植被健康度 - self.plugin.vegetation_manager.update_vegetation_health(vegetation_instance_id, vegetation_benefit) - - # 记录交互事件 - self._record_interaction_event('mutualism', { - 'animal_species': animal_species, - 'vegetation_type': vegetation_type, - 'animal_benefit': animal_benefit, - 'vegetation_benefit': vegetation_benefit - }) - - # 更新统计信息 - self.interaction_stats['mutualism_events'] += 1 - - except Exception as e: - print(f"✗ 互利共生处理失败: {e}") - - def _record_interaction_event(self, interaction_type: str, details: Dict[str, Any]): - """ - 记录交互事件 - - Args: - interaction_type: 交互类型 - details: 交互详情 - """ - try: - event_record = { - 'timestamp': time.time(), - 'type': interaction_type, - 'details': details - } - - self.interaction_history.append(event_record) - - # 限制历史记录大小 - if len(self.interaction_history) > self.max_history_size: - self.interaction_history.pop(0) - - except Exception as e: - print(f"✗ 交互事件记录失败: {e}") - - def get_interaction_types(self) -> Dict[str, Dict[str, Any]]: - """ - 获取交互类型定义 - - Returns: - 交互类型字典 - """ - return self.interaction_types.copy() - - def set_interaction_parameter(self, parameter: str, value: float): - """ - 设置交互参数 - - Args: - parameter: 参数名称 - value: 参数值 - """ - try: - if parameter in self.interaction_params: - self.interaction_params[parameter] = max(0.0, min(1.0, value)) - print(f"✓ 交互参数已设置: {parameter} = {value}") - else: - print(f"✗ 无效的交互参数: {parameter}") - - except Exception as e: - print(f"✗ 交互参数设置失败: {e}") - - def get_interaction_parameters(self) -> Dict[str, float]: - """ - 获取交互参数 - - Returns: - 交互参数字典 - """ - return self.interaction_params.copy() - - def get_interaction_stats(self) -> Dict[str, int]: - """ - 获取交互统计信息 - - Returns: - 统计信息字典 - """ - return self.interaction_stats.copy() - - def reset_interaction_stats(self): - """重置交互统计信息""" - try: - self.interaction_stats = { - 'total_interactions': 0, - 'herbivory_events': 0, - 'seed_dispersal_events': 0, - 'pollination_events': 0, - 'competition_events': 0, - 'mutualism_events': 0 - } - print("✓ 交互统计信息已重置") - except Exception as e: - print(f"✗ 交互统计信息重置失败: {e}") - - def get_interaction_history(self, limit: int = 50) -> List[Dict[str, Any]]: - """ - 获取交互历史记录 - - Args: - limit: 返回记录数量限制 - - Returns: - 交互历史记录列表 - """ - try: - # 返回最近的记录 - return self.interaction_history[-limit:].copy() - except Exception as e: - print(f"✗ 交互历史记录获取失败: {e}") - return [] - - def clear_interaction_history(self): - """清空交互历史记录""" - try: - self.interaction_history.clear() - print("✓ 交互历史记录已清空") - except Exception as e: - print(f"✗ 交互历史记录清空失败: {e}") - - def set_time_config(self, config: Dict[str, float]): - """ - 设置时间配置 - - Args: - config: 时间配置字典 - """ - try: - self.time_config.update(config) - print(f"✓ 时间配置已更新: {self.time_config}") - except Exception as e: - print(f"✗ 时间配置更新失败: {e}") - - def get_time_config(self) -> Dict[str, float]: - """ - 获取时间配置 - - Returns: - 时间配置字典 - """ - return self.time_config.copy() - - def calculate_biodiversity_impact(self) -> float: - """ - 计算生物多样性影响 - - Returns: - 生物多样性影响值 (-1.0 到 1.0) - """ - try: - # 简化的生物多样性影响计算 - if not self.plugin.vegetation_manager or not self.plugin.animal_simulator: - return 0.0 - - # 获取植被和动物多样性 - veg_stats = self.plugin.vegetation_manager.get_stats() - animal_stats = self.plugin.animal_simulator.get_stats() - - veg_diversity = len([count for count in veg_stats.get('vegetation_by_type', {}).values() if count > 0]) - animal_diversity = len([count for count in animal_stats.get('animals_by_species', {}).values() if count > 0]) - - # 简单的多样性评分 - total_diversity = veg_diversity + animal_diversity - max_possible_diversity = 15 # 假设最大多样性为15 - - diversity_score = min(1.0, total_diversity / max_possible_diversity) - - # 根据交互类型调整 - positive_interactions = ( - self.interaction_stats['seed_dispersal_events'] + - self.interaction_stats['pollination_events'] + - self.interaction_stats['mutualism_events'] - ) - - negative_interactions = ( - self.interaction_stats['herbivory_events'] + - self.interaction_stats['competition_events'] - ) - - interaction_balance = (positive_interactions - negative_interactions) / max(1, positive_interactions + negative_interactions) - - # 综合影响 - overall_impact = diversity_score * 0.7 + interaction_balance * 0.3 - - return max(-1.0, min(1.0, overall_impact)) - - except Exception as e: - print(f"✗ 生物多样性影响计算失败: {e}") +""" +生态系统交互模块 +负责处理植被与动物之间以及生态系统内部的复杂交互 +""" + +import time +from typing import Dict, Any, List, Optional +import math +import random + +class EcoInteractions: + """ + 生态系统交互管理器 + 负责处理植被与动物之间以及生态系统内部的复杂交互 + """ + + def __init__(self, plugin): + """ + 初始化生态系统交互管理器 + + Args: + plugin: 植被和生态系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 交互类型定义 + self.interaction_types = { + 'herbivory': { + 'name': '草食行为', + 'description': '动物食用植物', + 'affects': ['vegetation', 'animal'], + 'rate_factor': 1.0 + }, + 'seed_dispersal': { + 'name': '种子传播', + 'description': '动物帮助植物传播种子', + 'affects': ['vegetation'], + 'rate_factor': 0.5 + }, + 'pollination': { + 'name': '授粉', + 'description': '动物为植物授粉', + 'affects': ['vegetation'], + 'rate_factor': 0.8 + }, + 'habitat_modification': { + 'name': '栖息地改造', + 'description': '动物活动改变环境', + 'affects': ['environment'], + 'rate_factor': 0.3 + }, + 'competition': { + 'name': '竞争', + 'description': '物种间资源竞争', + 'affects': ['vegetation', 'animal'], + 'rate_factor': 0.7 + }, + 'mutualism': { + 'name': '互利共生', + 'description': '物种间互利关系', + 'affects': ['vegetation', 'animal'], + 'rate_factor': 0.6 + } + } + + # 交互参数 + self.interaction_params = { + 'herbivory_rate': 0.05, # 草食率 + 'seed_dispersal_rate': 0.1, # 种子传播率 + 'pollination_rate': 0.15, # 授粉率 + 'competition_intensity': 0.2, # 竞争强度 + 'mutualism_benefit': 0.1 # 互利共生益处 + } + + # 交互效果 + self.interaction_effects = { + 'vegetation_damage': 0.0, # 植被损害 + 'vegetation_growth_boost': 0.0, # 植被生长促进 + 'animal_nutrition': 0.0, # 动物营养 + 'biodiversity_impact': 0.0 # 生物多样性影响 + } + + # 交互历史 + self.interaction_history = [] + self.max_history_size = 1000 + + # 时间配置 + self.time_config = { + 'update_interval': 60.0, # 更新间隔(秒) + 'last_update': 0.0 + } + + # 统计信息 + self.interaction_stats = { + 'total_interactions': 0, + 'herbivory_events': 0, + 'seed_dispersal_events': 0, + 'pollination_events': 0, + 'competition_events': 0, + 'mutualism_events': 0 + } + + print("✓ 生态系统交互管理器已创建") + + def initialize(self) -> bool: + """ + 初始化生态系统交互管理器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 生态系统交互管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 生态系统交互管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用生态系统交互管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 生态系统交互管理器未初始化") + return False + + self.enabled = True + print("✓ 生态系统交互管理器已启用") + return True + + except Exception as e: + print(f"✗ 生态系统交互管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用生态系统交互管理器""" + try: + self.enabled = False + print("✓ 生态系统交互管理器已禁用") + + except Exception as e: + print(f"✗ 生态系统交互管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理生态系统交互管理器资源""" + try: + self.disable() + self.interaction_history.clear() + self.initialized = False + print("✓ 生态系统交互管理器资源已清理") + + except Exception as e: + print(f"✗ 生态系统交互管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新生态系统交互管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新时间 + self.time_config['last_update'] += dt + + # 检查是否需要更新 + if self.time_config['last_update'] >= self.time_config['update_interval']: + # 处理生态系统交互 + self._process_ecosystem_interactions() + + # 重置更新计时器 + self.time_config['last_update'] = 0.0 + + except Exception as e: + print(f"✗ 生态系统交互管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _process_ecosystem_interactions(self): + """处理生态系统交互""" + try: + # 处理草食行为 + self._process_herbivory() + + # 处理种子传播 + self._process_seed_dispersal() + + # 处理授粉 + self._process_pollination() + + # 处理竞争 + self._process_competition() + + # 处理互利共生 + self._process_mutualism() + + # 更新统计信息 + self.interaction_stats['total_interactions'] += 1 + + except Exception as e: + print(f"✗ 生态系统交互处理失败: {e}") + + def _process_herbivory(self): + """处理草食行为""" + try: + if not self.plugin.animal_simulator or not self.plugin.vegetation_manager: + return + + # 获取动物和植被信息 + animal_populations = self.plugin.animal_simulator.get_population_data() + vegetation_instances = self.plugin.vegetation_manager.get_all_vegetation_instances() + + # 遍历动物种群 + for population_id, population_data in animal_populations.items(): + species = population_data['species'] + population_size = population_data['size'] + + # 检查动物是否为草食性 + animal_info = self.plugin.animal_simulator.get_animal_info(species) + if not animal_info or animal_info['diet'] not in ['herbivore', 'omnivore']: + continue + + # 计算草食事件概率 + herbivory_probability = self.interaction_params['herbivory_rate'] * population_size / 100 + + if random.random() < herbivory_probability: + # 选择一个植被实例作为目标 + if vegetation_instances: + target_instance_id = random.choice(list(vegetation_instances.keys())) + target_instance = vegetation_instances[target_instance_id] + + # 检查动物是否偏好这种植被 + preferred_food = animal_info.get('preferred_food', []) + if target_instance['type'] in preferred_food: + damage_factor = 1.5 # 偏好食物造成更多损害 + else: + damage_factor = 1.0 + + # 对植被造成损害 + damage = 0.05 * damage_factor + self.plugin.vegetation_manager.update_vegetation_health(target_instance_id, -damage) + + # 动物获得营养 + nutrition_gain = 0.02 * damage_factor + # 这里可以更新动物状态(如果动物实例系统支持) + + # 记录交互事件 + self._record_interaction_event('herbivory', { + 'animal_species': species, + 'vegetation_type': target_instance['type'], + 'damage': damage, + 'nutrition_gain': nutrition_gain + }) + + # 更新统计信息 + self.interaction_stats['herbivory_events'] += 1 + + except Exception as e: + print(f"✗ 草食行为处理失败: {e}") + + def _process_seed_dispersal(self): + """处理种子传播""" + try: + if not self.plugin.animal_simulator or not self.plugin.vegetation_manager: + return + + # 获取动物和植被信息 + animal_populations = self.plugin.animal_simulator.get_population_data() + vegetation_instances = self.plugin.vegetation_manager.get_all_vegetation_instances() + + # 遍历动物种群 + for population_id, population_data in animal_populations.items(): + species = population_data['species'] + population_size = population_data['size'] + + # 检查动物是否适合传播种子(鸟类、哺乳动物等) + animal_info = self.plugin.animal_simulator.get_animal_info(species) + if not animal_info or animal_info['size'] not in ['small', 'medium', 'large']: + continue + + # 计算种子传播概率 + dispersal_probability = self.interaction_params['seed_dispersal_rate'] * population_size / 100 + + if random.random() < dispersal_probability: + # 选择一个成熟的植被实例作为种子来源 + mature_vegetation = [ + (vid, vdata) for vid, vdata in vegetation_instances.items() + if vdata['alive'] and vdata['size'] > 0.7 + ] + + if mature_vegetation: + source_instance_id, source_instance = random.choice(mature_vegetation) + + # 计算新的生成位置(在动物活动范围内) + animal_territory = animal_info.get('territory_size', 30.0) + source_pos = source_instance['position'] + + angle = random.uniform(0, 2 * math.pi) + distance = random.uniform(0, animal_territory) + + new_x = source_pos[0] + distance * math.cos(angle) + new_z = source_pos[2] + distance * math.sin(angle) + + # 创建新的植被实例 + new_instance_id = self.plugin.vegetation_manager.create_vegetation_instance( + source_instance['type'], + (new_x, source_pos[1], new_z), + initial_age=0.0, + initial_health=0.3 + ) + + if new_instance_id >= 0: + # 记录交互事件 + self._record_interaction_event('seed_dispersal', { + 'animal_species': species, + 'vegetation_type': source_instance['type'], + 'source_position': source_pos, + 'new_position': (new_x, source_pos[1], new_z) + }) + + # 更新统计信息 + self.interaction_stats['seed_dispersal_events'] += 1 + + except Exception as e: + print(f"✗ 种子传播处理失败: {e}") + + def _process_pollination(self): + """处理授粉""" + try: + if not self.plugin.animal_simulator or not self.plugin.vegetation_manager: + return + + # 获取动物和植被信息 + animal_populations = self.plugin.animal_simulator.get_population_data() + vegetation_instances = self.plugin.vegetation_manager.get_all_vegetation_instances() + + # 遍历动物种群 + for population_id, population_data in animal_populations.items(): + species = population_data['species'] + population_size = population_data['size'] + + # 检查动物是否适合授粉(昆虫、鸟类等) + animal_info = self.plugin.animal_simulator.get_animal_info(species) + if not animal_info or 'pollinator' not in animal_info.get('description', ''): + # 特殊处理鸟类和昆虫 + if species not in ['bird', 'insect']: + continue + + # 计算授粉概率 + pollination_probability = self.interaction_params['pollination_rate'] * population_size / 100 + + if random.random() < pollination_probability: + # 选择一个开花的植被实例 + flowering_vegetation = [ + (vid, vdata) for vid, vdata in vegetation_instances.items() + if vdata['alive'] and vdata['type'] in ['flower'] and vdata['health'] > 0.5 + ] + + if flowering_vegetation: + target_instance_id, target_instance = random.choice(flowering_vegetation) + + # 提高植被的繁殖率 + # 注意:这里我们只是模拟效果,实际的繁殖率调整在其他模块中处理 + + # 记录交互事件 + self._record_interaction_event('pollination', { + 'animal_species': species, + 'vegetation_type': target_instance['type'], + 'position': target_instance['position'] + }) + + # 更新统计信息 + self.interaction_stats['pollination_events'] += 1 + + except Exception as e: + print(f"✗ 授粉处理失败: {e}") + + def _process_competition(self): + """处理竞争""" + try: + if not self.plugin.vegetation_manager: + return + + # 获取植被实例 + vegetation_instances = self.plugin.vegetation_manager.get_all_vegetation_instances() + + # 按位置分组植被实例,以模拟局部竞争 + position_groups = {} + group_radius = 5.0 # 竞争半径 + + for instance_id, instance in vegetation_instances.items(): + if not instance['alive']: + continue + + pos = instance['position'] + # 简化的分组方法:按坐标网格分组 + grid_x = int(pos[0] // group_radius) + grid_z = int(pos[2] // group_radius) + group_key = (grid_x, grid_z) + + if group_key not in position_groups: + position_groups[group_key] = [] + position_groups[group_key].append((instance_id, instance)) + + # 处理每个组内的竞争 + for group_instances in position_groups.values(): + if len(group_instances) < 2: + continue # 至少需要两个实例才能产生竞争 + + # 计算竞争强度 + competition_intensity = self.interaction_params['competition_intensity'] + + # 对组内的每个实例应用竞争影响 + for instance_id, instance in group_instances: + veg_type = instance['type'] + veg_info = self.plugin.vegetation_manager.get_vegetation_info(veg_type) + + if not veg_info: + continue + + # 竞争因子影响 + competition_factor = veg_info.get('competition_factor', 0.5) + total_competition = competition_intensity * competition_factor + + # 根据组内其他植物数量调整竞争强度 + other_plants_count = len(group_instances) - 1 + adjusted_competition = total_competition * (other_plants_count / 5.0) # 标准化 + + # 应用竞争影响(减少健康度) + health_impact = -adjusted_competition * 0.01 + self.plugin.vegetation_manager.update_vegetation_health(instance_id, health_impact) + + # 记录交互事件 + self._record_interaction_event('competition', { + 'vegetation_type': veg_type, + 'position': instance['position'], + 'competitors_count': other_plants_count, + 'health_impact': health_impact + }) + + # 更新统计信息 + self.interaction_stats['competition_events'] += 1 + + except Exception as e: + print(f"✗ 竞争处理失败: {e}") + + def _process_mutualism(self): + """处理互利共生""" + try: + if not self.plugin.animal_simulator or not self.plugin.vegetation_manager: + return + + # 获取动物和植被信息 + animal_populations = self.plugin.animal_simulator.get_population_data() + vegetation_instances = self.plugin.vegetation_manager.get_all_vegetation_instances() + + # 定义已知的互利共生关系 + mutualistic_pairs = [ + ('bird', 'tree'), # 鸟类与树木 + ('insect', 'flower'), # 昆虫与花卉 + ] + + # 检查是否存在互利共生关系 + for animal_species, vegetation_type in mutualistic_pairs: + # 查找对应的动物种群 + animal_populations_filtered = { + pid: pdata for pid, pdata in animal_populations.items() + if pdata['species'] == animal_species + } + + if not animal_populations_filtered: + continue + + # 查找对应的植被实例 + vegetation_instances_filtered = { + vid: vdata for vid, vdata in vegetation_instances.items() + if vdata['alive'] and vdata['type'] == vegetation_type + } + + if not vegetation_instances_filtered: + continue + + # 计算互利共生概率 + mutualism_probability = self.interaction_params['mutualism_benefit'] + + if random.random() < mutualism_probability: + # 随机选择一个动物种群和植被实例 + animal_population_id = random.choice(list(animal_populations_filtered.keys())) + animal_population = animal_populations_filtered[animal_population_id] + + vegetation_instance_id = random.choice(list(vegetation_instances_filtered.keys())) + vegetation_instance = vegetation_instances_filtered[vegetation_instance_id] + + # 双方都获得益处 + animal_benefit = 0.01 + vegetation_benefit = 0.005 + + # 更新动物种群健康度(这里简化处理) + # 实际应用中可能需要更新个体动物状态 + + # 更新植被健康度 + self.plugin.vegetation_manager.update_vegetation_health(vegetation_instance_id, vegetation_benefit) + + # 记录交互事件 + self._record_interaction_event('mutualism', { + 'animal_species': animal_species, + 'vegetation_type': vegetation_type, + 'animal_benefit': animal_benefit, + 'vegetation_benefit': vegetation_benefit + }) + + # 更新统计信息 + self.interaction_stats['mutualism_events'] += 1 + + except Exception as e: + print(f"✗ 互利共生处理失败: {e}") + + def _record_interaction_event(self, interaction_type: str, details: Dict[str, Any]): + """ + 记录交互事件 + + Args: + interaction_type: 交互类型 + details: 交互详情 + """ + try: + event_record = { + 'timestamp': time.time(), + 'type': interaction_type, + 'details': details + } + + self.interaction_history.append(event_record) + + # 限制历史记录大小 + if len(self.interaction_history) > self.max_history_size: + self.interaction_history.pop(0) + + except Exception as e: + print(f"✗ 交互事件记录失败: {e}") + + def get_interaction_types(self) -> Dict[str, Dict[str, Any]]: + """ + 获取交互类型定义 + + Returns: + 交互类型字典 + """ + return self.interaction_types.copy() + + def set_interaction_parameter(self, parameter: str, value: float): + """ + 设置交互参数 + + Args: + parameter: 参数名称 + value: 参数值 + """ + try: + if parameter in self.interaction_params: + self.interaction_params[parameter] = max(0.0, min(1.0, value)) + print(f"✓ 交互参数已设置: {parameter} = {value}") + else: + print(f"✗ 无效的交互参数: {parameter}") + + except Exception as e: + print(f"✗ 交互参数设置失败: {e}") + + def get_interaction_parameters(self) -> Dict[str, float]: + """ + 获取交互参数 + + Returns: + 交互参数字典 + """ + return self.interaction_params.copy() + + def get_interaction_stats(self) -> Dict[str, int]: + """ + 获取交互统计信息 + + Returns: + 统计信息字典 + """ + return self.interaction_stats.copy() + + def reset_interaction_stats(self): + """重置交互统计信息""" + try: + self.interaction_stats = { + 'total_interactions': 0, + 'herbivory_events': 0, + 'seed_dispersal_events': 0, + 'pollination_events': 0, + 'competition_events': 0, + 'mutualism_events': 0 + } + print("✓ 交互统计信息已重置") + except Exception as e: + print(f"✗ 交互统计信息重置失败: {e}") + + def get_interaction_history(self, limit: int = 50) -> List[Dict[str, Any]]: + """ + 获取交互历史记录 + + Args: + limit: 返回记录数量限制 + + Returns: + 交互历史记录列表 + """ + try: + # 返回最近的记录 + return self.interaction_history[-limit:].copy() + except Exception as e: + print(f"✗ 交互历史记录获取失败: {e}") + return [] + + def clear_interaction_history(self): + """清空交互历史记录""" + try: + self.interaction_history.clear() + print("✓ 交互历史记录已清空") + except Exception as e: + print(f"✗ 交互历史记录清空失败: {e}") + + def set_time_config(self, config: Dict[str, float]): + """ + 设置时间配置 + + Args: + config: 时间配置字典 + """ + try: + self.time_config.update(config) + print(f"✓ 时间配置已更新: {self.time_config}") + except Exception as e: + print(f"✗ 时间配置更新失败: {e}") + + def get_time_config(self) -> Dict[str, float]: + """ + 获取时间配置 + + Returns: + 时间配置字典 + """ + return self.time_config.copy() + + def calculate_biodiversity_impact(self) -> float: + """ + 计算生物多样性影响 + + Returns: + 生物多样性影响值 (-1.0 到 1.0) + """ + try: + # 简化的生物多样性影响计算 + if not self.plugin.vegetation_manager or not self.plugin.animal_simulator: + return 0.0 + + # 获取植被和动物多样性 + veg_stats = self.plugin.vegetation_manager.get_stats() + animal_stats = self.plugin.animal_simulator.get_stats() + + veg_diversity = len([count for count in veg_stats.get('vegetation_by_type', {}).values() if count > 0]) + animal_diversity = len([count for count in animal_stats.get('animals_by_species', {}).values() if count > 0]) + + # 简单的多样性评分 + total_diversity = veg_diversity + animal_diversity + max_possible_diversity = 15 # 假设最大多样性为15 + + diversity_score = min(1.0, total_diversity / max_possible_diversity) + + # 根据交互类型调整 + positive_interactions = ( + self.interaction_stats['seed_dispersal_events'] + + self.interaction_stats['pollination_events'] + + self.interaction_stats['mutualism_events'] + ) + + negative_interactions = ( + self.interaction_stats['herbivory_events'] + + self.interaction_stats['competition_events'] + ) + + interaction_balance = (positive_interactions - negative_interactions) / max(1, positive_interactions + negative_interactions) + + # 综合影响 + overall_impact = diversity_score * 0.7 + interaction_balance * 0.3 + + return max(-1.0, min(1.0, overall_impact)) + + except Exception as e: + print(f"✗ 生物多样性影响计算失败: {e}") return 0.0 \ No newline at end of file diff --git a/plugins/user/vegetation_ecosystem/ecosystem/ecosystem_dynamics.py b/plugins/user/vegetation_ecosystem/ecosystem/ecosystem_dynamics.py index 46e06ab2..bad71c61 100644 --- a/plugins/user/vegetation_ecosystem/ecosystem/ecosystem_dynamics.py +++ b/plugins/user/vegetation_ecosystem/ecosystem/ecosystem_dynamics.py @@ -1,608 +1,608 @@ -""" -生态系统动力学 -负责模拟生态系统中的复杂动力学过程 -""" - -import time -from typing import Dict, Any, List, Optional -import math -import random - -class EcosystemDynamics: - """ - 生态系统动力学 - 负责模拟生态系统中的复杂动力学过程,包括能量流动、物质循环等 - """ - - def __init__(self, plugin): - """ - 初始化生态系统动力学 - - Args: - plugin: 植被和生态系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 能量流动模型 - self.energy_flow_model = { - 'primary_production': 1000.0, # 初级生产力 (单位: 能量/天) - 'trophic_efficiency': 0.1, # 营养级效率 (10%定律) - 'respiration_loss': 0.2, # 呼吸损失 - 'decomposition_rate': 0.05, # 分解速率 - 'energy_transfer_loss': 0.9 # 能量传递损失 - } - - # 物质循环模型 - self.nutrient_cycle_model = { - 'carbon_cycle': { - 'photosynthesis_rate': 0.8, # 光合作用速率 - 'respiration_rate': 0.3, # 呼吸速率 - 'decomposition_rate': 0.1, # 分解速率 - 'ocean_absorption': 0.05 # 海洋吸收速率 - }, - 'nitrogen_cycle': { - 'nitrogen_fixation': 0.02, # 固氮作用 - 'nitrification': 0.03, # 硝化作用 - 'denitrification': 0.01, # 反硝化作用 - 'plant_uptake': 0.05 # 植物吸收 - }, - 'phosphorus_cycle': { - 'weathering_rate': 0.01, # 风化速率 - 'plant_uptake': 0.02, # 植物吸收 - 'mineralization': 0.015, # 矿化作用 - 'leaching': 0.005 # 流失速率 - } - } - - # 生态系统反馈机制 - self.feedback_mechanisms = { - 'positive_feedback': { - 'description': '增强系统变化的反馈', - 'examples': ['植被增加导致更多降水', '温度升高加速分解'] - }, - 'negative_feedback': { - 'description': '抑制系统变化的反馈', - 'examples': ['捕食者增加导致猎物减少', '营养缺乏抑制植物生长'] - } - } - - # 生态系统稳定性指标 - self.stability_indicators = { - 'resilience': 0.7, # 恢复力 - 'resistance': 0.6, # 抵抗力 - 'persistence': 0.8, # 持续性 - 'constancy': 0.5, # 恒定性 - 'predictability': 0.4 # 可预测性 - } - - # 系统动态参数 - self.dynamic_parameters = { - 'time_scale': 1.0, # 时间尺度 - 'spatial_scale': 1.0, # 空间尺度 - 'complexity_level': 0.8, # 复杂度水平 - 'stochastic_factor': 0.1 # 随机因子 - } - - # 生态系统服务 - self.ecosystem_services = { - 'provisioning': { - 'food': 0.0, - 'water': 0.0, - 'timber': 0.0, - 'fiber': 0.0 - }, - 'regulating': { - 'climate_regulation': 0.0, - 'water_regulation': 0.0, - 'disease_regulation': 0.0, - 'pollination': 0.0 - }, - 'supporting': { - 'soil_formation': 0.0, - 'nutrient_cycling': 0.0, - 'primary_production': 0.0 - }, - 'cultural': { - 'recreation': 0.0, - 'aesthetic': 0.0, - 'education': 0.0, - 'spiritual': 0.0 - } - } - - # 系统状态 - self.system_state = { - 'total_energy': 10000.0, # 总能量 - 'available_nutrients': 500.0, # 可用营养物质 - 'biodiversity_index': 0.6, # 生物多样性指数 - 'stability_index': 0.7, # 稳定性指数 - 'health_index': 0.8 # 健康指数 - } - - # 动态过程 - self.dynamic_processes = { - 'succession': { - 'active': True, - 'stage': 'mature', # 'pioneer', 'intermediate', 'mature', 'climax' - 'progress': 0.8 - }, - 'disturbance': { - 'frequency': 0.01, # 干扰频率 - 'intensity': 0.2, # 干扰强度 - 'recovery_time': 365.0 # 恢复时间(天) - } - } - - # 统计信息 - self.stats = { - 'energy_flows': 0, - 'nutrient_cycles': 0, - 'feedback_events': 0, - 'stability_changes': 0 - } - - print("✓ 生态系统动力学模块已创建") - - def initialize(self) -> bool: - """ - 初始化生态系统动力学 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 生态系统动力学初始化完成") - return True - - except Exception as e: - print(f"✗ 生态系统动力学初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用生态系统动力学 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 生态系统动力学未初始化") - return False - - self.enabled = True - print("✓ 生态系统动力学已启用") - return True - - except Exception as e: - print(f"✗ 生态系统动力学启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用生态系统动力学""" - try: - self.enabled = False - print("✓ 生态系统动力学已禁用") - - except Exception as e: - print(f"✗ 生态系统动力学禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理生态系统动力学资源""" - try: - self.disable() - self.initialized = False - print("✓ 生态系统动力学资源已清理") - - except Exception as e: - print(f"✗ 生态系统动力学资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新生态系统动力学状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新能量流动 - self._update_energy_flow(dt) - - # 更新物质循环 - self._update_nutrient_cycles(dt) - - # 更新系统状态 - self._update_system_state(dt) - - # 检查反馈机制 - self._check_feedback_mechanisms(dt) - - except Exception as e: - print(f"✗ 生态系统动力学更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_energy_flow(self, dt: float): - """ - 更新能量流动 - - Args: - dt: 时间增量 - """ - try: - # 模拟光合作用产生的初级生产力 - photosynthesis = self.energy_flow_model['primary_production'] * dt / 86400 - - # 计算呼吸损失 - respiration_loss = photosynthesis * self.energy_flow_model['respiration_loss'] - - # 计算净初级生产力 - npp = photosynthesis - respiration_loss - - # 更新系统总能量 - energy_loss = self.system_state['total_energy'] * 0.001 * dt / 86400 # 基础能量流失 - self.system_state['total_energy'] += npp - energy_loss - - # 更新统计信息 - self.stats['energy_flows'] += 1 - - except Exception as e: - print(f"✗ 能量流动更新失败: {e}") - - def _update_nutrient_cycles(self, dt: float): - """ - 更新物质循环 - - Args: - dt: 时间增量 - """ - try: - # 更新碳循环 - carbon_change = ( - self.nutrient_cycle_model['carbon_cycle']['photosynthesis_rate'] - - self.nutrient_cycle_model['carbon_cycle']['respiration_rate'] - - self.nutrient_cycle_model['carbon_cycle']['decomposition_rate'] - ) * dt / 86400 - - # 更新氮循环 - nitrogen_change = ( - self.nutrient_cycle_model['nitrogen_cycle']['nitrogen_fixation'] + - self.nutrient_cycle_model['nitrogen_cycle']['nitrification'] - - self.nutrient_cycle_model['nitrogen_cycle']['denitrification'] - - self.nutrient_cycle_model['nitrogen_cycle']['plant_uptake'] - ) * dt / 86400 - - # 更新磷循环 - phosphorus_change = ( - self.nutrient_cycle_model['phosphorus_cycle']['weathering_rate'] + - self.nutrient_cycle_model['phosphorus_cycle']['mineralization'] - - self.nutrient_cycle_model['phosphorus_cycle']['plant_uptake'] - - self.nutrient_cycle_model['phosphorus_cycle']['leaching'] - ) * dt / 86400 - - # 更新可用营养物质 - nutrient_change = carbon_change + nitrogen_change + phosphorus_change - self.system_state['available_nutrients'] = max(0, self.system_state['available_nutrients'] + nutrient_change) - - # 更新统计信息 - self.stats['nutrient_cycles'] += 1 - - except Exception as e: - print(f"✗ 物质循环更新失败: {e}") - - def _update_system_state(self, dt: float): - """ - 更新系统状态 - - Args: - dt: 时间增量 - """ - try: - # 基于植被和动物数量更新生物多样性指数 - if self.plugin.vegetation_manager and self.plugin.animal_simulator: - veg_count = self.plugin.vegetation_manager.get_stats()['total_vegetation'] - animal_count = self.plugin.animal_simulator.get_stats()['total_animals'] - - diversity_score = min(1.0, (veg_count + animal_count) / 1000.0) - self.system_state['biodiversity_index'] = diversity_score - - # 更新健康指数 - health_factors = [ - self.system_state['biodiversity_index'], - min(1.0, self.system_state['available_nutrients'] / 1000.0), - min(1.0, self.system_state['total_energy'] / 20000.0) - ] - - self.system_state['health_index'] = sum(health_factors) / len(health_factors) - - except Exception as e: - print(f"✗ 系统状态更新失败: {e}") - - def _check_feedback_mechanisms(self, dt: float): - """ - 检查反馈机制 - - Args: - dt: 时间增量 - """ - try: - # 检查是否触发正反馈或负反馈 - feedback_probability = self.dynamic_parameters['stochastic_factor'] - if random.random() < feedback_probability * dt / 3600: # 每小时检查 - feedback_type = random.choice(['positive', 'negative']) - self._trigger_feedback(feedback_type) - - except Exception as e: - print(f"✗ 反馈机制检查失败: {e}") - - def _trigger_feedback(self, feedback_type: str): - """ - 触发反馈机制 - - Args: - feedback_type: 反馈类型 ('positive' 或 'negative') - """ - try: - print(f"✓ {feedback_type.capitalize()} feedback triggered in ecosystem") - - # 根据反馈类型调整系统参数 - if feedback_type == 'positive': - # 增强系统变化 - self.system_state['total_energy'] *= 1.05 - else: - # 抑制系统变化 - self.system_state['total_energy'] *= 0.95 - - # 更新统计信息 - self.stats['feedback_events'] += 1 - - except Exception as e: - print(f"✗ 反馈机制触发失败: {e}") - - def get_energy_flow_model(self) -> Dict[str, float]: - """ - 获取能量流动模型参数 - - Returns: - 能量流动模型参数字典 - """ - return self.energy_flow_model.copy() - - def get_nutrient_cycle_model(self) -> Dict[str, Dict[str, float]]: - """ - 获取物质循环模型参数 - - Returns: - 物质循环模型参数字典 - """ - return self.nutrient_cycle_model.copy() - - def set_energy_flow_parameter(self, parameter: str, value: float): - """ - 设置能量流动参数 - - Args: - parameter: 参数名称 - value: 参数值 - """ - try: - if parameter in self.energy_flow_model: - self.energy_flow_model[parameter] = value - print(f"✓ 能量流动参数已设置: {parameter} = {value}") - else: - print(f"✗ 无效的能量流动参数: {parameter}") - - except Exception as e: - print(f"✗ 能量流动参数设置失败: {e}") - - def set_nutrient_cycle_parameter(self, cycle: str, parameter: str, value: float): - """ - 设置物质循环参数 - - Args: - cycle: 循环类型 ('carbon_cycle', 'nitrogen_cycle', 'phosphorus_cycle') - parameter: 参数名称 - value: 参数值 - """ - try: - if cycle in self.nutrient_cycle_model and parameter in self.nutrient_cycle_model[cycle]: - self.nutrient_cycle_model[cycle][parameter] = value - print(f"✓ 物质循环参数已设置: {cycle}.{parameter} = {value}") - else: - print(f"✗ 无效的物质循环参数: {cycle}.{parameter}") - - except Exception as e: - print(f"✗ 物质循环参数设置失败: {e}") - - def get_system_state(self) -> Dict[str, float]: - """ - 获取系统状态 - - Returns: - 系统状态字典 - """ - return self.system_state.copy() - - def get_stability_indicators(self) -> Dict[str, float]: - """ - 获取稳定性指标 - - Returns: - 稳定性指标字典 - """ - return self.stability_indicators.copy() - - def update_stability_indicator(self, indicator: str, value: float): - """ - 更新稳定性指标 - - Args: - indicator: 指标名称 - value: 指标值 - """ - try: - if indicator in self.stability_indicators: - self.stability_indicators[indicator] = max(0.0, min(1.0, value)) - self.stats['stability_changes'] += 1 - print(f"✓ 稳定性指标已更新: {indicator} = {value}") - else: - print(f"✗ 无效的稳定性指标: {indicator}") - - except Exception as e: - print(f"✗ 稳定性指标更新失败: {e}") - - def get_ecosystem_services(self) -> Dict[str, Dict[str, float]]: - """ - 获取生态系统服务 - - Returns: - 生态系统服务字典 - """ - return self.ecosystem_services.copy() - - def calculate_ecosystem_service_value(self, service_category: str, service: str) -> float: - """ - 计算生态系统服务价值 - - Args: - service_category: 服务类别 - service: 服务名称 - - Returns: - 服务价值 - """ - try: - if service_category in self.ecosystem_services and service in self.ecosystem_services[service_category]: - base_value = self.ecosystem_services[service_category][service] - - # 基于系统健康度调整价值 - health_factor = self.system_state['health_index'] - adjusted_value = base_value * health_factor - - return adjusted_value - return 0.0 - - except Exception as e: - print(f"✗ 生态系统服务价值计算失败: {e}") - return 0.0 - - def get_dynamic_processes(self) -> Dict[str, Dict[str, Any]]: - """ - 获取动态过程信息 - - Returns: - 动态过程字典 - """ - return self.dynamic_processes.copy() - - def set_dynamic_parameter(self, parameter: str, value: float): - """ - 设置动态参数 - - Args: - parameter: 参数名称 - value: 参数值 - """ - try: - if parameter in self.dynamic_parameters: - self.dynamic_parameters[parameter] = value - print(f"✓ 动态参数已设置: {parameter} = {value}") - else: - print(f"✗ 无效的动态参数: {parameter}") - - except Exception as e: - print(f"✗ 动态参数设置失败: {e}") - - def get_dynamic_parameters(self) -> Dict[str, float]: - """ - 获取动态参数 - - Returns: - 动态参数字典 - """ - return self.dynamic_parameters.copy() - - def trigger_disturbance(self, intensity: float = 0.5): - """ - 触发干扰事件 - - Args: - intensity: 干扰强度 (0.0-1.0) - """ - try: - # 减少系统能量和营养物质 - energy_loss = self.system_state['total_energy'] * intensity * 0.3 - nutrient_loss = self.system_state['available_nutrients'] * intensity * 0.2 - - self.system_state['total_energy'] = max(0, self.system_state['total_energy'] - energy_loss) - self.system_state['available_nutrients'] = max(0, self.system_state['available_nutrients'] - nutrient_loss) - - # 降低健康指数 - self.system_state['health_index'] *= (1.0 - intensity * 0.3) - - print(f"✓ 干扰事件已触发,强度: {intensity}") - - except Exception as e: - print(f"✗ 干扰事件触发失败: {e}") - - def get_feedback_mechanisms(self) -> Dict[str, Dict[str, Any]]: - """ - 获取反馈机制信息 - - Returns: - 反馈机制字典 - """ - return self.feedback_mechanisms.copy() - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'energy_flows': 0, - 'nutrient_cycles': 0, - 'feedback_events': 0, - 'stability_changes': 0 - } - print("✓ 生态系统动力学统计信息已重置") - - def calculate_carrying_capacity(self) -> float: - """ - 计算环境承载能力 - - Returns: - 承载能力值 - """ - try: - # 基于可用营养物质和能量计算承载能力 - energy_factor = self.system_state['total_energy'] / 10000.0 - nutrient_factor = self.system_state['available_nutrients'] / 500.0 - health_factor = self.system_state['health_index'] - - carrying_capacity = (energy_factor + nutrient_factor + health_factor) / 3.0 - return max(0.0, min(1.0, carrying_capacity)) - - except Exception as e: - print(f"✗ 承载能力计算失败: {e}") +""" +生态系统动力学 +负责模拟生态系统中的复杂动力学过程 +""" + +import time +from typing import Dict, Any, List, Optional +import math +import random + +class EcosystemDynamics: + """ + 生态系统动力学 + 负责模拟生态系统中的复杂动力学过程,包括能量流动、物质循环等 + """ + + def __init__(self, plugin): + """ + 初始化生态系统动力学 + + Args: + plugin: 植被和生态系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 能量流动模型 + self.energy_flow_model = { + 'primary_production': 1000.0, # 初级生产力 (单位: 能量/天) + 'trophic_efficiency': 0.1, # 营养级效率 (10%定律) + 'respiration_loss': 0.2, # 呼吸损失 + 'decomposition_rate': 0.05, # 分解速率 + 'energy_transfer_loss': 0.9 # 能量传递损失 + } + + # 物质循环模型 + self.nutrient_cycle_model = { + 'carbon_cycle': { + 'photosynthesis_rate': 0.8, # 光合作用速率 + 'respiration_rate': 0.3, # 呼吸速率 + 'decomposition_rate': 0.1, # 分解速率 + 'ocean_absorption': 0.05 # 海洋吸收速率 + }, + 'nitrogen_cycle': { + 'nitrogen_fixation': 0.02, # 固氮作用 + 'nitrification': 0.03, # 硝化作用 + 'denitrification': 0.01, # 反硝化作用 + 'plant_uptake': 0.05 # 植物吸收 + }, + 'phosphorus_cycle': { + 'weathering_rate': 0.01, # 风化速率 + 'plant_uptake': 0.02, # 植物吸收 + 'mineralization': 0.015, # 矿化作用 + 'leaching': 0.005 # 流失速率 + } + } + + # 生态系统反馈机制 + self.feedback_mechanisms = { + 'positive_feedback': { + 'description': '增强系统变化的反馈', + 'examples': ['植被增加导致更多降水', '温度升高加速分解'] + }, + 'negative_feedback': { + 'description': '抑制系统变化的反馈', + 'examples': ['捕食者增加导致猎物减少', '营养缺乏抑制植物生长'] + } + } + + # 生态系统稳定性指标 + self.stability_indicators = { + 'resilience': 0.7, # 恢复力 + 'resistance': 0.6, # 抵抗力 + 'persistence': 0.8, # 持续性 + 'constancy': 0.5, # 恒定性 + 'predictability': 0.4 # 可预测性 + } + + # 系统动态参数 + self.dynamic_parameters = { + 'time_scale': 1.0, # 时间尺度 + 'spatial_scale': 1.0, # 空间尺度 + 'complexity_level': 0.8, # 复杂度水平 + 'stochastic_factor': 0.1 # 随机因子 + } + + # 生态系统服务 + self.ecosystem_services = { + 'provisioning': { + 'food': 0.0, + 'water': 0.0, + 'timber': 0.0, + 'fiber': 0.0 + }, + 'regulating': { + 'climate_regulation': 0.0, + 'water_regulation': 0.0, + 'disease_regulation': 0.0, + 'pollination': 0.0 + }, + 'supporting': { + 'soil_formation': 0.0, + 'nutrient_cycling': 0.0, + 'primary_production': 0.0 + }, + 'cultural': { + 'recreation': 0.0, + 'aesthetic': 0.0, + 'education': 0.0, + 'spiritual': 0.0 + } + } + + # 系统状态 + self.system_state = { + 'total_energy': 10000.0, # 总能量 + 'available_nutrients': 500.0, # 可用营养物质 + 'biodiversity_index': 0.6, # 生物多样性指数 + 'stability_index': 0.7, # 稳定性指数 + 'health_index': 0.8 # 健康指数 + } + + # 动态过程 + self.dynamic_processes = { + 'succession': { + 'active': True, + 'stage': 'mature', # 'pioneer', 'intermediate', 'mature', 'climax' + 'progress': 0.8 + }, + 'disturbance': { + 'frequency': 0.01, # 干扰频率 + 'intensity': 0.2, # 干扰强度 + 'recovery_time': 365.0 # 恢复时间(天) + } + } + + # 统计信息 + self.stats = { + 'energy_flows': 0, + 'nutrient_cycles': 0, + 'feedback_events': 0, + 'stability_changes': 0 + } + + print("✓ 生态系统动力学模块已创建") + + def initialize(self) -> bool: + """ + 初始化生态系统动力学 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 生态系统动力学初始化完成") + return True + + except Exception as e: + print(f"✗ 生态系统动力学初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用生态系统动力学 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 生态系统动力学未初始化") + return False + + self.enabled = True + print("✓ 生态系统动力学已启用") + return True + + except Exception as e: + print(f"✗ 生态系统动力学启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用生态系统动力学""" + try: + self.enabled = False + print("✓ 生态系统动力学已禁用") + + except Exception as e: + print(f"✗ 生态系统动力学禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理生态系统动力学资源""" + try: + self.disable() + self.initialized = False + print("✓ 生态系统动力学资源已清理") + + except Exception as e: + print(f"✗ 生态系统动力学资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新生态系统动力学状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新能量流动 + self._update_energy_flow(dt) + + # 更新物质循环 + self._update_nutrient_cycles(dt) + + # 更新系统状态 + self._update_system_state(dt) + + # 检查反馈机制 + self._check_feedback_mechanisms(dt) + + except Exception as e: + print(f"✗ 生态系统动力学更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_energy_flow(self, dt: float): + """ + 更新能量流动 + + Args: + dt: 时间增量 + """ + try: + # 模拟光合作用产生的初级生产力 + photosynthesis = self.energy_flow_model['primary_production'] * dt / 86400 + + # 计算呼吸损失 + respiration_loss = photosynthesis * self.energy_flow_model['respiration_loss'] + + # 计算净初级生产力 + npp = photosynthesis - respiration_loss + + # 更新系统总能量 + energy_loss = self.system_state['total_energy'] * 0.001 * dt / 86400 # 基础能量流失 + self.system_state['total_energy'] += npp - energy_loss + + # 更新统计信息 + self.stats['energy_flows'] += 1 + + except Exception as e: + print(f"✗ 能量流动更新失败: {e}") + + def _update_nutrient_cycles(self, dt: float): + """ + 更新物质循环 + + Args: + dt: 时间增量 + """ + try: + # 更新碳循环 + carbon_change = ( + self.nutrient_cycle_model['carbon_cycle']['photosynthesis_rate'] - + self.nutrient_cycle_model['carbon_cycle']['respiration_rate'] - + self.nutrient_cycle_model['carbon_cycle']['decomposition_rate'] + ) * dt / 86400 + + # 更新氮循环 + nitrogen_change = ( + self.nutrient_cycle_model['nitrogen_cycle']['nitrogen_fixation'] + + self.nutrient_cycle_model['nitrogen_cycle']['nitrification'] - + self.nutrient_cycle_model['nitrogen_cycle']['denitrification'] - + self.nutrient_cycle_model['nitrogen_cycle']['plant_uptake'] + ) * dt / 86400 + + # 更新磷循环 + phosphorus_change = ( + self.nutrient_cycle_model['phosphorus_cycle']['weathering_rate'] + + self.nutrient_cycle_model['phosphorus_cycle']['mineralization'] - + self.nutrient_cycle_model['phosphorus_cycle']['plant_uptake'] - + self.nutrient_cycle_model['phosphorus_cycle']['leaching'] + ) * dt / 86400 + + # 更新可用营养物质 + nutrient_change = carbon_change + nitrogen_change + phosphorus_change + self.system_state['available_nutrients'] = max(0, self.system_state['available_nutrients'] + nutrient_change) + + # 更新统计信息 + self.stats['nutrient_cycles'] += 1 + + except Exception as e: + print(f"✗ 物质循环更新失败: {e}") + + def _update_system_state(self, dt: float): + """ + 更新系统状态 + + Args: + dt: 时间增量 + """ + try: + # 基于植被和动物数量更新生物多样性指数 + if self.plugin.vegetation_manager and self.plugin.animal_simulator: + veg_count = self.plugin.vegetation_manager.get_stats()['total_vegetation'] + animal_count = self.plugin.animal_simulator.get_stats()['total_animals'] + + diversity_score = min(1.0, (veg_count + animal_count) / 1000.0) + self.system_state['biodiversity_index'] = diversity_score + + # 更新健康指数 + health_factors = [ + self.system_state['biodiversity_index'], + min(1.0, self.system_state['available_nutrients'] / 1000.0), + min(1.0, self.system_state['total_energy'] / 20000.0) + ] + + self.system_state['health_index'] = sum(health_factors) / len(health_factors) + + except Exception as e: + print(f"✗ 系统状态更新失败: {e}") + + def _check_feedback_mechanisms(self, dt: float): + """ + 检查反馈机制 + + Args: + dt: 时间增量 + """ + try: + # 检查是否触发正反馈或负反馈 + feedback_probability = self.dynamic_parameters['stochastic_factor'] + if random.random() < feedback_probability * dt / 3600: # 每小时检查 + feedback_type = random.choice(['positive', 'negative']) + self._trigger_feedback(feedback_type) + + except Exception as e: + print(f"✗ 反馈机制检查失败: {e}") + + def _trigger_feedback(self, feedback_type: str): + """ + 触发反馈机制 + + Args: + feedback_type: 反馈类型 ('positive' 或 'negative') + """ + try: + print(f"✓ {feedback_type.capitalize()} feedback triggered in ecosystem") + + # 根据反馈类型调整系统参数 + if feedback_type == 'positive': + # 增强系统变化 + self.system_state['total_energy'] *= 1.05 + else: + # 抑制系统变化 + self.system_state['total_energy'] *= 0.95 + + # 更新统计信息 + self.stats['feedback_events'] += 1 + + except Exception as e: + print(f"✗ 反馈机制触发失败: {e}") + + def get_energy_flow_model(self) -> Dict[str, float]: + """ + 获取能量流动模型参数 + + Returns: + 能量流动模型参数字典 + """ + return self.energy_flow_model.copy() + + def get_nutrient_cycle_model(self) -> Dict[str, Dict[str, float]]: + """ + 获取物质循环模型参数 + + Returns: + 物质循环模型参数字典 + """ + return self.nutrient_cycle_model.copy() + + def set_energy_flow_parameter(self, parameter: str, value: float): + """ + 设置能量流动参数 + + Args: + parameter: 参数名称 + value: 参数值 + """ + try: + if parameter in self.energy_flow_model: + self.energy_flow_model[parameter] = value + print(f"✓ 能量流动参数已设置: {parameter} = {value}") + else: + print(f"✗ 无效的能量流动参数: {parameter}") + + except Exception as e: + print(f"✗ 能量流动参数设置失败: {e}") + + def set_nutrient_cycle_parameter(self, cycle: str, parameter: str, value: float): + """ + 设置物质循环参数 + + Args: + cycle: 循环类型 ('carbon_cycle', 'nitrogen_cycle', 'phosphorus_cycle') + parameter: 参数名称 + value: 参数值 + """ + try: + if cycle in self.nutrient_cycle_model and parameter in self.nutrient_cycle_model[cycle]: + self.nutrient_cycle_model[cycle][parameter] = value + print(f"✓ 物质循环参数已设置: {cycle}.{parameter} = {value}") + else: + print(f"✗ 无效的物质循环参数: {cycle}.{parameter}") + + except Exception as e: + print(f"✗ 物质循环参数设置失败: {e}") + + def get_system_state(self) -> Dict[str, float]: + """ + 获取系统状态 + + Returns: + 系统状态字典 + """ + return self.system_state.copy() + + def get_stability_indicators(self) -> Dict[str, float]: + """ + 获取稳定性指标 + + Returns: + 稳定性指标字典 + """ + return self.stability_indicators.copy() + + def update_stability_indicator(self, indicator: str, value: float): + """ + 更新稳定性指标 + + Args: + indicator: 指标名称 + value: 指标值 + """ + try: + if indicator in self.stability_indicators: + self.stability_indicators[indicator] = max(0.0, min(1.0, value)) + self.stats['stability_changes'] += 1 + print(f"✓ 稳定性指标已更新: {indicator} = {value}") + else: + print(f"✗ 无效的稳定性指标: {indicator}") + + except Exception as e: + print(f"✗ 稳定性指标更新失败: {e}") + + def get_ecosystem_services(self) -> Dict[str, Dict[str, float]]: + """ + 获取生态系统服务 + + Returns: + 生态系统服务字典 + """ + return self.ecosystem_services.copy() + + def calculate_ecosystem_service_value(self, service_category: str, service: str) -> float: + """ + 计算生态系统服务价值 + + Args: + service_category: 服务类别 + service: 服务名称 + + Returns: + 服务价值 + """ + try: + if service_category in self.ecosystem_services and service in self.ecosystem_services[service_category]: + base_value = self.ecosystem_services[service_category][service] + + # 基于系统健康度调整价值 + health_factor = self.system_state['health_index'] + adjusted_value = base_value * health_factor + + return adjusted_value + return 0.0 + + except Exception as e: + print(f"✗ 生态系统服务价值计算失败: {e}") + return 0.0 + + def get_dynamic_processes(self) -> Dict[str, Dict[str, Any]]: + """ + 获取动态过程信息 + + Returns: + 动态过程字典 + """ + return self.dynamic_processes.copy() + + def set_dynamic_parameter(self, parameter: str, value: float): + """ + 设置动态参数 + + Args: + parameter: 参数名称 + value: 参数值 + """ + try: + if parameter in self.dynamic_parameters: + self.dynamic_parameters[parameter] = value + print(f"✓ 动态参数已设置: {parameter} = {value}") + else: + print(f"✗ 无效的动态参数: {parameter}") + + except Exception as e: + print(f"✗ 动态参数设置失败: {e}") + + def get_dynamic_parameters(self) -> Dict[str, float]: + """ + 获取动态参数 + + Returns: + 动态参数字典 + """ + return self.dynamic_parameters.copy() + + def trigger_disturbance(self, intensity: float = 0.5): + """ + 触发干扰事件 + + Args: + intensity: 干扰强度 (0.0-1.0) + """ + try: + # 减少系统能量和营养物质 + energy_loss = self.system_state['total_energy'] * intensity * 0.3 + nutrient_loss = self.system_state['available_nutrients'] * intensity * 0.2 + + self.system_state['total_energy'] = max(0, self.system_state['total_energy'] - energy_loss) + self.system_state['available_nutrients'] = max(0, self.system_state['available_nutrients'] - nutrient_loss) + + # 降低健康指数 + self.system_state['health_index'] *= (1.0 - intensity * 0.3) + + print(f"✓ 干扰事件已触发,强度: {intensity}") + + except Exception as e: + print(f"✗ 干扰事件触发失败: {e}") + + def get_feedback_mechanisms(self) -> Dict[str, Dict[str, Any]]: + """ + 获取反馈机制信息 + + Returns: + 反馈机制字典 + """ + return self.feedback_mechanisms.copy() + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'energy_flows': 0, + 'nutrient_cycles': 0, + 'feedback_events': 0, + 'stability_changes': 0 + } + print("✓ 生态系统动力学统计信息已重置") + + def calculate_carrying_capacity(self) -> float: + """ + 计算环境承载能力 + + Returns: + 承载能力值 + """ + try: + # 基于可用营养物质和能量计算承载能力 + energy_factor = self.system_state['total_energy'] / 10000.0 + nutrient_factor = self.system_state['available_nutrients'] / 500.0 + health_factor = self.system_state['health_index'] + + carrying_capacity = (energy_factor + nutrient_factor + health_factor) / 3.0 + return max(0.0, min(1.0, carrying_capacity)) + + except Exception as e: + print(f"✗ 承载能力计算失败: {e}") return 0.5 \ No newline at end of file diff --git a/plugins/user/vegetation_ecosystem/editor/eco_editor.py b/plugins/user/vegetation_ecosystem/editor/eco_editor.py index 20428b23..b8044e17 100644 --- a/plugins/user/vegetation_ecosystem/editor/eco_editor.py +++ b/plugins/user/vegetation_ecosystem/editor/eco_editor.py @@ -1,1131 +1,1131 @@ -""" -生态编辑器 -提供可视化界面用于编辑和调整生态系统参数 -""" - -import time -from typing import Dict, Any, List, Optional -import json - -class EcoEditor: - """ - 生态编辑器 - 提供可视化界面用于编辑和调整生态系统参数,包括植被、动物、环境等 - """ - - def __init__(self, plugin): - """ - 初始化生态编辑器 - - Args: - plugin: 植被和生态系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 编辑器配置 - self.editor_config = { - 'auto_preview': True, - 'realtime_update': True, - 'preview_quality': 'medium', - 'history_limit': 50, - 'snap_to_grid': True, - 'grid_size': 1.0 - } - - # 编辑模式 - self.edit_modes = { - 'vegetation': {'name': '植被编辑', 'description': '编辑植被参数和分布'}, - 'animals': {'name': '动物编辑', 'description': '编辑动物种群和行为'}, - 'environment': {'name': '环境编辑', 'description': '编辑环境因子和地形影响'}, - 'ecosystem': {'name': '生态系统编辑', 'description': '编辑生态系统参数和关系'}, - 'seasons': {'name': '季节编辑', 'description': '编辑季节参数和效果'}, - 'interactions': {'name': '交互编辑', 'description': '编辑生态系统交互参数'}, - 'visualization': {'name': '可视化编辑', 'description': '编辑可视化效果参数'} - } - - self.current_edit_mode = 'vegetation' - - # 参数分组 - self.parameter_groups = { - 'vegetation': ['growth_rate', 'reproduction_rate', 'preferred_conditions', 'competition_factor'], - 'animals': ['reproduction_rate', 'mortality_rate', 'mobility', 'preferred_food', 'territory_size'], - 'environment': ['temperature', 'humidity', 'soil_fertility', 'light_level', 'water_level'], - 'ecosystem': ['biodiversity_index', 'stability_index', 'productivity_rate', 'carrying_capacity'], - 'seasons': ['season_duration', 'temperature_modifier', 'growth_factor', 'light_duration'], - 'interactions': ['herbivory_rate', 'seed_dispersal_rate', 'pollination_rate', 'competition_intensity'], - 'visualization': ['render_distance', 'effect_intensity', 'color_scheme', 'transparency'] - } - - # 编辑历史 - self.edit_history = [] - self.history_position = -1 - - # 选择系统 - self.selected_objects = [] - self.selection_mode = 'single' # 'single', 'multiple', 'area' - - # 工具系统 - self.tools = { - 'select': {'name': '选择工具', 'description': '选择和编辑对象'}, - 'spawn': {'name': '生成工具', 'description': '生成植被或动物'}, - 'paint': {'name': '绘制工具', 'description': '绘制植被分布'}, - 'erase': {'name': '擦除工具', 'description': '擦除植被或动物'}, - 'modify': {'name': '修改工具', 'description': '修改对象属性'}, - 'analyze': {'name': '分析工具', 'description': '分析生态系统状态'} - } - - self.current_tool = 'select' - - # 预览数据 - self.preview_data = None - self.preview_dirty = False - - # 工具状态 - self.is_editing = False - self.selected_parameter = None - - # 分析数据 - self.analysis_data = { - 'last_analysis': None, - 'analysis_timestamp': 0, - 'analysis_results': {} - } - - # 统计信息 - self.stats = { - 'edits_made': 0, - 'undos_performed': 0, - 'redos_performed': 0, - 'total_edit_time': 0.0, - 'average_edit_time': 0.0, - 'objects_selected': 0 - } - - # UI组件引用(在实际实现中会连接到GUI) - self.ui_components = { - 'toolbar': None, - 'properties_panel': None, - 'preview_window': None, - 'parameter_sliders': None, - 'history_panel': None, - 'selection_info': None, - 'analysis_panel': None - } - - print("✓ 生态编辑器已创建") - - def initialize(self) -> bool: - """ - 初始化生态编辑器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 生态编辑器初始化完成") - return True - - except Exception as e: - print(f"✗ 生态编辑器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用生态编辑器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 生态编辑器未初始化") - return False - - self.enabled = True - print("✓ 生态编辑器已启用") - return True - - except Exception as e: - print(f"✗ 生态编辑器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用生态编辑器""" - try: - self.enabled = False - print("✓ 生态编辑器已禁用") - - except Exception as e: - print(f"✗ 生态编辑器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理生态编辑器资源""" - try: - self.disable() - self.initialized = False - print("✓ 生态编辑器资源已清理") - - except Exception as e: - print(f"✗ 生态编辑器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新生态编辑器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新预览 - if self.preview_dirty and self.editor_config['auto_preview']: - self._update_preview() - - # 处理实时更新 - if self.editor_config['realtime_update']: - self._handle_realtime_updates() - - except Exception as e: - print(f"✗ 生态编辑器更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_preview(self): - """更新预览显示""" - try: - # 在实际实现中,这里会更新预览窗口 - self.preview_dirty = False - except Exception as e: - print(f"✗ 预览更新失败: {e}") - - def _handle_realtime_updates(self): - """处理实时更新""" - try: - # 在实际实现中,这里会处理实时编辑更新 - pass - except Exception as e: - print(f"✗ 实时更新处理失败: {e}") - - def set_edit_mode(self, mode: str): - """ - 设置编辑模式 - - Args: - mode: 编辑模式 - """ - if mode in self.edit_modes: - self.current_edit_mode = mode - print(f"✓ 编辑模式设置为: {self.edit_modes[mode]['name']}") - else: - print(f"✗ 无效的编辑模式: {mode}") - - def get_edit_modes(self) -> Dict[str, Dict[str, str]]: - """ - 获取可用的编辑模式 - - Returns: - 编辑模式字典 - """ - return self.edit_modes.copy() - - def get_current_edit_mode(self) -> str: - """ - 获取当前编辑模式 - - Returns: - 当前编辑模式 - """ - return self.current_edit_mode - - def set_parameter(self, parameter_name: str, value: Any): - """ - 设置参数值 - - Args: - parameter_name: 参数名称 - value: 参数值 - """ - try: - # 保存当前状态到历史记录 - self._save_state_to_history() - - # 根据编辑模式应用参数 - if self.current_edit_mode == 'vegetation' and self.plugin.vegetation_manager: - # 处理植被参数 - veg_types = self.plugin.vegetation_manager.get_vegetation_types() - for veg_type in veg_types: - veg_info = self.plugin.vegetation_manager.get_vegetation_info(veg_type) - if veg_info and parameter_name in veg_info: - # 在实际实现中会更新植被参数 - pass - - elif self.current_edit_mode == 'animals' and self.plugin.animal_simulator: - # 处理动物参数 - animal_species = self.plugin.animal_simulator.get_animal_species() - for species in animal_species: - animal_info = self.plugin.animal_simulator.get_animal_info(species) - if animal_info and parameter_name in animal_info: - # 在实际实现中会更新动物参数 - pass - - elif self.current_edit_mode == 'environment' and self.plugin.vegetation_manager: - # 处理环境参数 - env_factors = self.plugin.vegetation_manager.get_environment_factors() - if parameter_name in env_factors: - env_factors[parameter_name] = value - self.plugin.vegetation_manager.update_environment_factors(env_factors) - - elif self.current_edit_mode == 'ecosystem' and self.plugin.ecosystem_manager: - # 处理生态系统参数 - self.plugin.ecosystem_manager.set_ecosystem_parameter(parameter_name, value) - - elif self.current_edit_mode == 'seasons' and self.plugin.seasonal_effects: - # 处理季节参数 - if parameter_name == 'season_duration': - current_season = self.plugin.seasonal_effects.get_current_season() - self.plugin.seasonal_effects.set_season_duration(current_season, value) - - elif self.current_edit_mode == 'interactions' and self.plugin.ecosystem_interactions: - # 处理交互参数 - self.plugin.ecosystem_interactions.set_interaction_parameter(parameter_name, value) - - elif self.current_edit_mode == 'visualization': - # 处理可视化参数 - if self.plugin.eco_visualizer: - # 处理基础可视化参数 - pass - - if self.plugin.advanced_visuals: - # 处理高级可视化参数 - if parameter_name in ['render_distance', 'effect_intensity', 'transparency']: - config = self.plugin.advanced_visuals.get_visualization_config() - config[parameter_name] = value - self.plugin.advanced_visuals.set_visualization_config(config) - elif parameter_name == 'color_scheme': - self.plugin.advanced_visuals.set_color_scheme(value) - - # 标记预览需要更新 - self.preview_dirty = True - - # 更新统计信息 - self.stats['edits_made'] += 1 - - print(f"✓ 参数 '{parameter_name}' 设置为: {value}") - - except Exception as e: - print(f"✗ 参数设置失败: {e}") - import traceback - traceback.print_exc() - - def get_parameter(self, parameter_name: str) -> Any: - """ - 获取参数值 - - Args: - parameter_name: 参数名称 - - Returns: - 参数值 - """ - try: - # 根据编辑模式获取参数 - if self.current_edit_mode == 'vegetation' and self.plugin.vegetation_manager: - # 获取植被参数 - pass - - elif self.current_edit_mode == 'animals' and self.plugin.animal_simulator: - # 获取动物参数 - pass - - elif self.current_edit_mode == 'environment' and self.plugin.vegetation_manager: - env_factors = self.plugin.vegetation_manager.get_environment_factors() - return env_factors.get(parameter_name) - - elif self.current_edit_mode == 'ecosystem' and self.plugin.ecosystem_manager: - return self.plugin.ecosystem_manager.get_ecosystem_parameter(parameter_name) - - elif self.current_edit_mode == 'seasons' and self.plugin.seasonal_effects: - if parameter_name == 'season_duration': - current_season = self.plugin.seasonal_effects.get_current_season() - season_info = self.plugin.seasonal_effects.get_season_info(current_season) - if season_info: - return season_info.get('duration', 90) - - elif self.current_edit_mode == 'interactions' and self.plugin.ecosystem_interactions: - params = self.plugin.ecosystem_interactions.get_interaction_parameters() - return params.get(parameter_name) - - elif self.current_edit_mode == 'visualization': - if self.plugin.advanced_visuals: - config = self.plugin.advanced_visuals.get_visualization_config() - if parameter_name in config: - return config[parameter_name] - elif parameter_name == 'color_scheme': - return config.get('color_scheme', 'default') - - except Exception as e: - print(f"✗ 参数获取失败: {e}") - import traceback - traceback.print_exc() - - return None - - def _save_state_to_history(self): - """保存当前状态到历史记录""" - try: - # 创建状态快照 - state_snapshot = { - 'timestamp': time.time(), - 'edit_mode': self.current_edit_mode, - 'vegetation_params': {}, - 'animal_params': {}, - 'environment_params': {}, - 'ecosystem_params': {}, - 'season_params': {}, - 'interaction_params': {}, - 'visualization_params': {} - } - - # 保存植被参数 - if self.plugin.vegetation_manager: - veg_types = self.plugin.vegetation_manager.get_vegetation_types() - for veg_type in veg_types: - veg_info = self.plugin.vegetation_manager.get_vegetation_info(veg_type) - if veg_info: - state_snapshot['vegetation_params'][veg_type] = veg_info.copy() - - # 保存动物参数 - if self.plugin.animal_simulator: - animal_species = self.plugin.animal_simulator.get_animal_species() - for species in animal_species: - animal_info = self.plugin.animal_simulator.get_animal_info(species) - if animal_info: - state_snapshot['animal_params'][species] = animal_info.copy() - - # 保存环境参数 - if self.plugin.vegetation_manager: - state_snapshot['environment_params'] = self.plugin.vegetation_manager.get_environment_factors().copy() - - # 保存生态系统参数 - if self.plugin.ecosystem_manager: - state_snapshot['ecosystem_params'] = self.plugin.ecosystem_manager.get_ecosystem_parameters().copy() - - # 保存季节参数 - if self.plugin.seasonal_effects: - current_season = self.plugin.seasonal_effects.get_current_season() - season_info = self.plugin.seasonal_effects.get_season_info(current_season) - if season_info: - state_snapshot['season_params'] = season_info.copy() - - # 保存交互参数 - if self.plugin.ecosystem_interactions: - state_snapshot['interaction_params'] = self.plugin.ecosystem_interactions.get_interaction_parameters().copy() - - # 保存可视化参数 - if self.plugin.advanced_visuals: - state_snapshot['visualization_params'] = self.plugin.advanced_visuals.get_visualization_config().copy() - - # 添加到历史记录 - if self.history_position < len(self.edit_history) - 1: - # 如果当前不在历史记录末尾,截断后续记录 - self.edit_history = self.edit_history[:self.history_position + 1] - - self.edit_history.append(state_snapshot) - self.history_position += 1 - - # 限制历史记录数量 - if len(self.edit_history) > self.editor_config['history_limit']: - self.edit_history.pop(0) - self.history_position -= 1 - - except Exception as e: - print(f"✗ 状态保存失败: {e}") - import traceback - traceback.print_exc() - - def undo(self) -> bool: - """ - 撤销操作 - - Returns: - 是否撤销成功 - """ - try: - if not self.enabled: - print("✗ 生态编辑器未启用") - return False - - if self.history_position <= 0: - print("✗ 没有可撤销的操作") - return False - - # 移动到前一个历史状态 - self.history_position -= 1 - - # 恢复状态 - self._restore_state_from_history() - - # 更新统计信息 - self.stats['undos_performed'] += 1 - - print("✓ 撤销操作完成") - return True - - except Exception as e: - print(f"✗ 撤销操作失败: {e}") - import traceback - traceback.print_exc() - return False - - def redo(self) -> bool: - """ - 重做操作 - - Returns: - 是否重做成功 - """ - try: - if not self.enabled: - print("✗ 生态编辑器未启用") - return False - - if self.history_position >= len(self.edit_history) - 1: - print("✗ 没有可重做的操作") - return False - - # 移动到下一个历史状态 - self.history_position += 1 - - # 恢复状态 - self._restore_state_from_history() - - # 更新统计信息 - self.stats['redos_performed'] += 1 - - print("✓ 重做操作完成") - return True - - except Exception as e: - print(f"✗ 重做操作失败: {e}") - import traceback - traceback.print_exc() - return False - - def _restore_state_from_history(self): - """从历史记录恢复状态""" - try: - if self.history_position < 0: - return - - # 获取历史状态 - state = self.edit_history[self.history_position] - - # 恢复参数 - if self.plugin.vegetation_manager and state['vegetation_params']: - # 在实际实现中会恢复植被参数 - pass - - if self.plugin.animal_simulator and state['animal_params']: - # 在实际实现中会恢复动物参数 - pass - - if self.plugin.vegetation_manager and state['environment_params']: - self.plugin.vegetation_manager.update_environment_factors(state['environment_params']) - - if self.plugin.ecosystem_manager and state['ecosystem_params']: - for param_name, param_value in state['ecosystem_params'].items(): - self.plugin.ecosystem_manager.set_ecosystem_parameter(param_name, param_value) - - if self.plugin.seasonal_effects and state['season_params']: - # 在实际实现中会恢复季节参数 - pass - - if self.plugin.ecosystem_interactions and state['interaction_params']: - # 恢复交互参数 - for param_name, param_value in state['interaction_params'].items(): - self.plugin.ecosystem_interactions.set_interaction_parameter(param_name, param_value) - - if self.plugin.advanced_visuals and state['visualization_params']: - # 恢复可视化参数 - self.plugin.advanced_visuals.set_visualization_config(state['visualization_params']) - - # 标记预览需要更新 - self.preview_dirty = True - - except Exception as e: - print(f"✗ 状态恢复失败: {e}") - import traceback - traceback.print_exc() - - def clear_history(self): - """清空历史记录""" - try: - self.edit_history.clear() - self.history_position = -1 - print("✓ 历史记录已清空") - except Exception as e: - print(f"✗ 历史记录清空失败: {e}") - import traceback - traceback.print_exc() - - def get_parameter_groups(self) -> Dict[str, List[str]]: - """ - 获取参数分组 - - Returns: - 参数分组字典 - """ - return self.parameter_groups.copy() - - def get_parameters_in_group(self, group_name: str) -> List[str]: - """ - 获取组中的参数列表 - - Args: - group_name: 组名称 - - Returns: - 参数列表 - """ - return self.parameter_groups.get(group_name, []).copy() - - def set_editor_config(self, config: Dict[str, Any]): - """ - 设置编辑器配置 - - Args: - config: 配置字典 - """ - self.editor_config.update(config) - print(f"✓ 编辑器配置已更新: {self.editor_config}") - - def get_editor_config(self) -> Dict[str, Any]: - """ - 获取编辑器配置 - - Returns: - 配置字典 - """ - return self.editor_config.copy() - - def set_tool(self, tool_name: str): - """ - 设置当前工具 - - Args: - tool_name: 工具名称 - """ - if tool_name in self.tools: - self.current_tool = tool_name - print(f"✓ 当前工具设置为: {self.tools[tool_name]['name']}") - else: - print(f"✗ 无效的工具: {tool_name}") - - def get_tools(self) -> Dict[str, Dict[str, str]]: - """ - 获取可用工具 - - Returns: - 工具字典 - """ - return self.tools.copy() - - def get_current_tool(self) -> str: - """ - 获取当前工具 - - Returns: - 当前工具名称 - """ - return self.current_tool - - def select_object(self, object_id: Any, multi_select: bool = False): - """ - 选择对象 - - Args: - object_id: 对象ID - multi_select: 是否多选 - """ - try: - if multi_select: - if object_id not in self.selected_objects: - self.selected_objects.append(object_id) - else: - self.selected_objects = [object_id] - - self.stats['objects_selected'] = len(self.selected_objects) - print(f"✓ 对象已选择: {object_id}") - - except Exception as e: - print(f"✗ 对象选择失败: {e}") - - def deselect_object(self, object_id: Any): - """ - 取消选择对象 - - Args: - object_id: 对象ID - """ - try: - if object_id in self.selected_objects: - self.selected_objects.remove(object_id) - self.stats['objects_selected'] = len(self.selected_objects) - print(f"✓ 对象已取消选择: {object_id}") - - except Exception as e: - print(f"✗ 对象取消选择失败: {e}") - - def clear_selection(self): - """清空选择""" - try: - self.selected_objects.clear() - self.stats['objects_selected'] = 0 - print("✓ 选择已清空") - except Exception as e: - print(f"✗ 选择清空失败: {e}") - - def get_selected_objects(self) -> List[Any]: - """ - 获取选中的对象 - - Returns: - 选中的对象列表 - """ - return self.selected_objects.copy() - - def set_selection_mode(self, mode: str): - """ - 设置选择模式 - - Args: - mode: 选择模式 ('single', 'multiple', 'area') - """ - if mode in ['single', 'multiple', 'area']: - self.selection_mode = mode - print(f"✓ 选择模式设置为: {mode}") - else: - print(f"✗ 无效的选择模式: {mode}") - - def get_selection_mode(self) -> str: - """ - 获取选择模式 - - Returns: - 选择模式 - """ - return self.selection_mode - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - # 更新平均编辑时间 - total_edits = self.stats['edits_made'] + self.stats['undos_performed'] + self.stats['redos_performed'] - if total_edits > 0: - self.stats['average_edit_time'] = self.stats['total_edit_time'] / total_edits - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'edits_made': 0, - 'undos_performed': 0, - 'redos_performed': 0, - 'total_edit_time': 0.0, - 'average_edit_time': 0.0, - 'objects_selected': 0 - } - print("✓ 生态编辑器统计信息已重置") - - def export_editor_state(self, filename: str) -> bool: - """ - 导出编辑器状态 - - Args: - filename: 文件名 - - Returns: - 是否导出成功 - """ - try: - # 创建状态数据 - state_data = { - 'editor_config': self.editor_config, - 'current_edit_mode': self.current_edit_mode, - 'current_tool': self.current_tool, - 'parameter_groups': self.parameter_groups, - 'timestamp': time.time() - } - - # 保存为JSON文件 - with open(filename, 'w', encoding='utf-8') as f: - json.dump(state_data, f, ensure_ascii=False, indent=2) - - print(f"✓ 编辑器状态已导出到: {filename}") - return True - - except Exception as e: - print(f"✗ 编辑器状态导出失败: {e}") - import traceback - traceback.print_exc() - return False - - def import_editor_state(self, filename: str) -> bool: - """ - 导入编辑器状态 - - Args: - filename: 文件名 - - Returns: - 是否导入成功 - """ - try: - # 读取JSON文件 - with open(filename, 'r', encoding='utf-8') as f: - state_data = json.load(f) - - # 应用状态 - if 'editor_config' in state_data: - self.set_editor_config(state_data['editor_config']) - if 'current_edit_mode' in state_data: - self.set_edit_mode(state_data['current_edit_mode']) - if 'current_tool' in state_data: - self.set_tool(state_data['current_tool']) - - print(f"✓ 编辑器状态已从 {filename} 导入") - return True - - except Exception as e: - print(f"✗ 编辑器状态导入失败: {e}") - import traceback - traceback.print_exc() - return False - - def set_ui_component(self, component_name: str, component: Any): - """ - 设置UI组件引用 - - Args: - component_name: 组件名称 - component: 组件引用 - """ - if component_name in self.ui_components: - self.ui_components[component_name] = component - print(f"✓ UI组件 '{component_name}' 已设置") - else: - print(f"✗ 无效的UI组件名称: {component_name}") - - def refresh_ui(self): - """刷新UI显示""" - try: - # 在实际实现中,这里会更新所有UI组件的显示 - print("✓ UI刷新完成") - except Exception as e: - print(f"✗ UI刷新失败: {e}") - - def show_preview_window(self): - """显示预览窗口""" - try: - # 在实际实现中,这里会创建并显示预览窗口 - print("✓ 预览窗口已显示") - except Exception as e: - print(f"✗ 预览窗口显示失败: {e}") - - def hide_preview_window(self): - """隐藏预览窗口""" - try: - # 在实际实现中,这里会隐藏预览窗口 - print("✓ 预览窗口已隐藏") - except Exception as e: - print(f"✗ 预览窗口隐藏失败: {e}") - - def update_preview_window(self): - """更新预览窗口内容""" - try: - # 在实际实现中,这里会更新预览窗口的内容 - print("✓ 预览窗口已更新") - except Exception as e: - print(f"✗ 预览窗口更新失败: {e}") - - def apply_preset(self, preset_name: str): - """ - 应用预设 - - Args: - preset_name: 预设名称 - """ - try: - if self.plugin.preset_manager: - self.plugin.preset_manager.apply_preset(preset_name) - self.preview_dirty = True - print(f"✓ 预设 '{preset_name}' 已应用") - else: - print("✗ 预设管理器不可用") - except Exception as e: - print(f"✗ 预设应用失败: {e}") - import traceback - traceback.print_exc() - - def save_current_as_preset(self, preset_name: str, description: str = '') -> str: - """ - 将当前配置保存为预设 - - Args: - preset_name: 预设名称 - description: 预设描述 - - Returns: - 预设ID - """ - try: - if self.plugin.preset_manager: - preset_id = self.plugin.preset_manager.create_terrain_preset_from_current(preset_name, description) - print(f"✓ 当前配置已保存为预设: {preset_name}") - return preset_id - else: - print("✗ 预设管理器不可用") - return '' - except Exception as e: - print(f"✗ 预设保存失败: {e}") - import traceback - traceback.print_exc() - return '' - - def spawn_vegetation(self, vegetation_type: str, position: tuple, count: int = 1): - """ - 生成植被 - - Args: - vegetation_type: 植被类型 - position: 位置 - count: 数量 - """ - try: - if not self.plugin.vegetation_manager: - print("✗ 植被管理器不可用") - return - - if vegetation_type not in self.plugin.vegetation_manager.get_vegetation_types(): - print(f"✗ 无效的植被类型: {vegetation_type}") - return - - # 保存当前状态到历史记录 - self._save_state_to_history() - - spawned_count = 0 - for i in range(count): - # 添加随机偏移 - if count > 1: - offset_x = random.uniform(-2.0, 2.0) - offset_z = random.uniform(-2.0, 2.0) - spawn_position = (position[0] + offset_x, position[1], position[2] + offset_z) - else: - spawn_position = position - - instance_id = self.plugin.vegetation_manager.create_vegetation_instance( - vegetation_type, spawn_position - ) - - if instance_id >= 0: - spawned_count += 1 - - print(f"✓ 已生成 {spawned_count} 个 {vegetation_type}") - self.stats['edits_made'] += 1 - - except Exception as e: - print(f"✗ 植被生成失败: {e}") - import traceback - traceback.print_exc() - - def remove_selected_objects(self): - """移除选中的对象""" - try: - if not self.plugin.vegetation_manager: - print("✗ 植被管理器不可用") - return - - # 保存当前状态到历史记录 - self._save_state_to_history() - - removed_count = 0 - for obj_id in self.selected_objects: - # 假设所有选中对象都是植被实例 - if self.plugin.vegetation_manager.remove_vegetation_instance(obj_id): - removed_count += 1 - - self.clear_selection() - print(f"✓ 已移除 {removed_count} 个对象") - self.stats['edits_made'] += 1 - - except Exception as e: - print(f"✗ 对象移除失败: {e}") - import traceback - traceback.print_exc() - - def analyze_ecosystem(self) -> Dict[str, Any]: - """ - 分析生态系统状态 - - Returns: - 分析结果字典 - """ - try: - analysis_results = { - 'timestamp': time.time(), - 'biodiversity': 0.0, - 'health_index': 0.0, - 'stability': 0.0, - 'total_species': 0, - 'total_population': 0, - 'resource_availability': 0.0 - } - - # 收集生态系统数据 - if self.plugin.ecosystem_manager: - system_state = self.plugin.ecosystem_manager.get_system_state() - analysis_results['health_index'] = system_state.get('health_index', 0.0) - analysis_results['stability'] = self.plugin.ecosystem_manager.get_stability_index() - analysis_results['biodiversity'] = self.plugin.ecosystem_manager.get_biodiversity_index() - - if self.plugin.vegetation_manager: - veg_stats = self.plugin.vegetation_manager.get_stats() - analysis_results['total_species'] += len(veg_stats.get('vegetation_by_type', {})) - analysis_results['total_population'] += veg_stats.get('total_vegetation', 0) - - if self.plugin.animal_simulator: - animal_stats = self.plugin.animal_simulator.get_stats() - analysis_results['total_species'] += len(animal_stats.get('animals_by_species', {})) - analysis_results['total_population'] += animal_stats.get('total_animals', 0) - - if self.plugin.vegetation_manager: - env_factors = self.plugin.vegetation_manager.get_environment_factors() - # 简化的资源可用性计算 - resource_score = ( - env_factors.get('temperature', 50) / 100 * 0.25 + - env_factors.get('humidity', 50) / 100 * 0.25 + - env_factors.get('soil_fertility', 0.5) * 0.25 + - env_factors.get('water_level', 0.5) * 0.25 - ) - analysis_results['resource_availability'] = resource_score - - # 保存分析结果 - self.analysis_data['last_analysis'] = analysis_results - self.analysis_data['analysis_timestamp'] = analysis_results['timestamp'] - self.analysis_data['analysis_results'] = analysis_results - - print("✓ 生态系统分析完成") - return analysis_results - - except Exception as e: - print(f"✗ 生态系统分析失败: {e}") - import traceback - traceback.print_exc() - return {} - - def get_last_analysis(self) -> Optional[Dict[str, Any]]: - """ - 获取上次分析结果 - - Returns: - 上次分析结果或None - """ - return self.analysis_data['last_analysis'] - - def export_analysis_report(self, filename: str) -> bool: - """ - 导出分析报告 - - Args: - filename: 文件名 - - Returns: - 是否导出成功 - """ - try: - analysis = self.get_last_analysis() - if not analysis: - print("✗ 没有可用的分析数据") - return False - - # 创建报告数据 - report_data = { - 'analysis_report': analysis, - 'editor_state': { - 'edit_mode': self.current_edit_mode, - 'tool': self.current_tool, - 'timestamp': time.time() - } - } - - # 保存为JSON文件 - with open(filename, 'w', encoding='utf-8') as f: - json.dump(report_data, f, ensure_ascii=False, indent=2) - - print(f"✓ 分析报告已导出到: {filename}") - return True - - except Exception as e: - print(f"✗ 分析报告导出失败: {e}") - import traceback - traceback.print_exc() - return False - - def toggle_visualization_type(self, visualization_type: str): - """ - 切换可视化类型启用状态 - - Args: - visualization_type: 可视化类型 - """ - try: - if self.plugin.advanced_visuals: - # 获取当前状态 - vis_types = self.plugin.advanced_visuals.get_visualization_types() - if visualization_type in vis_types: - current_state = vis_types[visualization_type]['enabled'] - self.plugin.advanced_visuals.set_visualization_type_enabled(visualization_type, not current_state) - print(f"✓ 可视化类型 '{visualization_type}' 已{'启用' if not current_state else '禁用'}") - else: - print(f"✗ 无效的可视化类型: {visualization_type}") - else: - print("✗ 高级可视化模块不可用") - except Exception as e: - print(f"✗ 可视化类型切换失败: {e}") - - def capture_visualization_screenshot(self, filename: str = None) -> str: - """ - 捕获可视化截图 - - Args: - filename: 文件名 - - Returns: - 截图文件路径 - """ - try: - if self.plugin.advanced_visuals: - return self.plugin.advanced_visuals.capture_visualization_screenshot(filename) - else: - print("✗ 高级可视化模块不可用") - return "" - except Exception as e: - print(f"✗ 可视化截图捕获失败: {e}") +""" +生态编辑器 +提供可视化界面用于编辑和调整生态系统参数 +""" + +import time +from typing import Dict, Any, List, Optional +import json + +class EcoEditor: + """ + 生态编辑器 + 提供可视化界面用于编辑和调整生态系统参数,包括植被、动物、环境等 + """ + + def __init__(self, plugin): + """ + 初始化生态编辑器 + + Args: + plugin: 植被和生态系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 编辑器配置 + self.editor_config = { + 'auto_preview': True, + 'realtime_update': True, + 'preview_quality': 'medium', + 'history_limit': 50, + 'snap_to_grid': True, + 'grid_size': 1.0 + } + + # 编辑模式 + self.edit_modes = { + 'vegetation': {'name': '植被编辑', 'description': '编辑植被参数和分布'}, + 'animals': {'name': '动物编辑', 'description': '编辑动物种群和行为'}, + 'environment': {'name': '环境编辑', 'description': '编辑环境因子和地形影响'}, + 'ecosystem': {'name': '生态系统编辑', 'description': '编辑生态系统参数和关系'}, + 'seasons': {'name': '季节编辑', 'description': '编辑季节参数和效果'}, + 'interactions': {'name': '交互编辑', 'description': '编辑生态系统交互参数'}, + 'visualization': {'name': '可视化编辑', 'description': '编辑可视化效果参数'} + } + + self.current_edit_mode = 'vegetation' + + # 参数分组 + self.parameter_groups = { + 'vegetation': ['growth_rate', 'reproduction_rate', 'preferred_conditions', 'competition_factor'], + 'animals': ['reproduction_rate', 'mortality_rate', 'mobility', 'preferred_food', 'territory_size'], + 'environment': ['temperature', 'humidity', 'soil_fertility', 'light_level', 'water_level'], + 'ecosystem': ['biodiversity_index', 'stability_index', 'productivity_rate', 'carrying_capacity'], + 'seasons': ['season_duration', 'temperature_modifier', 'growth_factor', 'light_duration'], + 'interactions': ['herbivory_rate', 'seed_dispersal_rate', 'pollination_rate', 'competition_intensity'], + 'visualization': ['render_distance', 'effect_intensity', 'color_scheme', 'transparency'] + } + + # 编辑历史 + self.edit_history = [] + self.history_position = -1 + + # 选择系统 + self.selected_objects = [] + self.selection_mode = 'single' # 'single', 'multiple', 'area' + + # 工具系统 + self.tools = { + 'select': {'name': '选择工具', 'description': '选择和编辑对象'}, + 'spawn': {'name': '生成工具', 'description': '生成植被或动物'}, + 'paint': {'name': '绘制工具', 'description': '绘制植被分布'}, + 'erase': {'name': '擦除工具', 'description': '擦除植被或动物'}, + 'modify': {'name': '修改工具', 'description': '修改对象属性'}, + 'analyze': {'name': '分析工具', 'description': '分析生态系统状态'} + } + + self.current_tool = 'select' + + # 预览数据 + self.preview_data = None + self.preview_dirty = False + + # 工具状态 + self.is_editing = False + self.selected_parameter = None + + # 分析数据 + self.analysis_data = { + 'last_analysis': None, + 'analysis_timestamp': 0, + 'analysis_results': {} + } + + # 统计信息 + self.stats = { + 'edits_made': 0, + 'undos_performed': 0, + 'redos_performed': 0, + 'total_edit_time': 0.0, + 'average_edit_time': 0.0, + 'objects_selected': 0 + } + + # UI组件引用(在实际实现中会连接到GUI) + self.ui_components = { + 'toolbar': None, + 'properties_panel': None, + 'preview_window': None, + 'parameter_sliders': None, + 'history_panel': None, + 'selection_info': None, + 'analysis_panel': None + } + + print("✓ 生态编辑器已创建") + + def initialize(self) -> bool: + """ + 初始化生态编辑器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 生态编辑器初始化完成") + return True + + except Exception as e: + print(f"✗ 生态编辑器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用生态编辑器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 生态编辑器未初始化") + return False + + self.enabled = True + print("✓ 生态编辑器已启用") + return True + + except Exception as e: + print(f"✗ 生态编辑器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用生态编辑器""" + try: + self.enabled = False + print("✓ 生态编辑器已禁用") + + except Exception as e: + print(f"✗ 生态编辑器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理生态编辑器资源""" + try: + self.disable() + self.initialized = False + print("✓ 生态编辑器资源已清理") + + except Exception as e: + print(f"✗ 生态编辑器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新生态编辑器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新预览 + if self.preview_dirty and self.editor_config['auto_preview']: + self._update_preview() + + # 处理实时更新 + if self.editor_config['realtime_update']: + self._handle_realtime_updates() + + except Exception as e: + print(f"✗ 生态编辑器更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_preview(self): + """更新预览显示""" + try: + # 在实际实现中,这里会更新预览窗口 + self.preview_dirty = False + except Exception as e: + print(f"✗ 预览更新失败: {e}") + + def _handle_realtime_updates(self): + """处理实时更新""" + try: + # 在实际实现中,这里会处理实时编辑更新 + pass + except Exception as e: + print(f"✗ 实时更新处理失败: {e}") + + def set_edit_mode(self, mode: str): + """ + 设置编辑模式 + + Args: + mode: 编辑模式 + """ + if mode in self.edit_modes: + self.current_edit_mode = mode + print(f"✓ 编辑模式设置为: {self.edit_modes[mode]['name']}") + else: + print(f"✗ 无效的编辑模式: {mode}") + + def get_edit_modes(self) -> Dict[str, Dict[str, str]]: + """ + 获取可用的编辑模式 + + Returns: + 编辑模式字典 + """ + return self.edit_modes.copy() + + def get_current_edit_mode(self) -> str: + """ + 获取当前编辑模式 + + Returns: + 当前编辑模式 + """ + return self.current_edit_mode + + def set_parameter(self, parameter_name: str, value: Any): + """ + 设置参数值 + + Args: + parameter_name: 参数名称 + value: 参数值 + """ + try: + # 保存当前状态到历史记录 + self._save_state_to_history() + + # 根据编辑模式应用参数 + if self.current_edit_mode == 'vegetation' and self.plugin.vegetation_manager: + # 处理植被参数 + veg_types = self.plugin.vegetation_manager.get_vegetation_types() + for veg_type in veg_types: + veg_info = self.plugin.vegetation_manager.get_vegetation_info(veg_type) + if veg_info and parameter_name in veg_info: + # 在实际实现中会更新植被参数 + pass + + elif self.current_edit_mode == 'animals' and self.plugin.animal_simulator: + # 处理动物参数 + animal_species = self.plugin.animal_simulator.get_animal_species() + for species in animal_species: + animal_info = self.plugin.animal_simulator.get_animal_info(species) + if animal_info and parameter_name in animal_info: + # 在实际实现中会更新动物参数 + pass + + elif self.current_edit_mode == 'environment' and self.plugin.vegetation_manager: + # 处理环境参数 + env_factors = self.plugin.vegetation_manager.get_environment_factors() + if parameter_name in env_factors: + env_factors[parameter_name] = value + self.plugin.vegetation_manager.update_environment_factors(env_factors) + + elif self.current_edit_mode == 'ecosystem' and self.plugin.ecosystem_manager: + # 处理生态系统参数 + self.plugin.ecosystem_manager.set_ecosystem_parameter(parameter_name, value) + + elif self.current_edit_mode == 'seasons' and self.plugin.seasonal_effects: + # 处理季节参数 + if parameter_name == 'season_duration': + current_season = self.plugin.seasonal_effects.get_current_season() + self.plugin.seasonal_effects.set_season_duration(current_season, value) + + elif self.current_edit_mode == 'interactions' and self.plugin.ecosystem_interactions: + # 处理交互参数 + self.plugin.ecosystem_interactions.set_interaction_parameter(parameter_name, value) + + elif self.current_edit_mode == 'visualization': + # 处理可视化参数 + if self.plugin.eco_visualizer: + # 处理基础可视化参数 + pass + + if self.plugin.advanced_visuals: + # 处理高级可视化参数 + if parameter_name in ['render_distance', 'effect_intensity', 'transparency']: + config = self.plugin.advanced_visuals.get_visualization_config() + config[parameter_name] = value + self.plugin.advanced_visuals.set_visualization_config(config) + elif parameter_name == 'color_scheme': + self.plugin.advanced_visuals.set_color_scheme(value) + + # 标记预览需要更新 + self.preview_dirty = True + + # 更新统计信息 + self.stats['edits_made'] += 1 + + print(f"✓ 参数 '{parameter_name}' 设置为: {value}") + + except Exception as e: + print(f"✗ 参数设置失败: {e}") + import traceback + traceback.print_exc() + + def get_parameter(self, parameter_name: str) -> Any: + """ + 获取参数值 + + Args: + parameter_name: 参数名称 + + Returns: + 参数值 + """ + try: + # 根据编辑模式获取参数 + if self.current_edit_mode == 'vegetation' and self.plugin.vegetation_manager: + # 获取植被参数 + pass + + elif self.current_edit_mode == 'animals' and self.plugin.animal_simulator: + # 获取动物参数 + pass + + elif self.current_edit_mode == 'environment' and self.plugin.vegetation_manager: + env_factors = self.plugin.vegetation_manager.get_environment_factors() + return env_factors.get(parameter_name) + + elif self.current_edit_mode == 'ecosystem' and self.plugin.ecosystem_manager: + return self.plugin.ecosystem_manager.get_ecosystem_parameter(parameter_name) + + elif self.current_edit_mode == 'seasons' and self.plugin.seasonal_effects: + if parameter_name == 'season_duration': + current_season = self.plugin.seasonal_effects.get_current_season() + season_info = self.plugin.seasonal_effects.get_season_info(current_season) + if season_info: + return season_info.get('duration', 90) + + elif self.current_edit_mode == 'interactions' and self.plugin.ecosystem_interactions: + params = self.plugin.ecosystem_interactions.get_interaction_parameters() + return params.get(parameter_name) + + elif self.current_edit_mode == 'visualization': + if self.plugin.advanced_visuals: + config = self.plugin.advanced_visuals.get_visualization_config() + if parameter_name in config: + return config[parameter_name] + elif parameter_name == 'color_scheme': + return config.get('color_scheme', 'default') + + except Exception as e: + print(f"✗ 参数获取失败: {e}") + import traceback + traceback.print_exc() + + return None + + def _save_state_to_history(self): + """保存当前状态到历史记录""" + try: + # 创建状态快照 + state_snapshot = { + 'timestamp': time.time(), + 'edit_mode': self.current_edit_mode, + 'vegetation_params': {}, + 'animal_params': {}, + 'environment_params': {}, + 'ecosystem_params': {}, + 'season_params': {}, + 'interaction_params': {}, + 'visualization_params': {} + } + + # 保存植被参数 + if self.plugin.vegetation_manager: + veg_types = self.plugin.vegetation_manager.get_vegetation_types() + for veg_type in veg_types: + veg_info = self.plugin.vegetation_manager.get_vegetation_info(veg_type) + if veg_info: + state_snapshot['vegetation_params'][veg_type] = veg_info.copy() + + # 保存动物参数 + if self.plugin.animal_simulator: + animal_species = self.plugin.animal_simulator.get_animal_species() + for species in animal_species: + animal_info = self.plugin.animal_simulator.get_animal_info(species) + if animal_info: + state_snapshot['animal_params'][species] = animal_info.copy() + + # 保存环境参数 + if self.plugin.vegetation_manager: + state_snapshot['environment_params'] = self.plugin.vegetation_manager.get_environment_factors().copy() + + # 保存生态系统参数 + if self.plugin.ecosystem_manager: + state_snapshot['ecosystem_params'] = self.plugin.ecosystem_manager.get_ecosystem_parameters().copy() + + # 保存季节参数 + if self.plugin.seasonal_effects: + current_season = self.plugin.seasonal_effects.get_current_season() + season_info = self.plugin.seasonal_effects.get_season_info(current_season) + if season_info: + state_snapshot['season_params'] = season_info.copy() + + # 保存交互参数 + if self.plugin.ecosystem_interactions: + state_snapshot['interaction_params'] = self.plugin.ecosystem_interactions.get_interaction_parameters().copy() + + # 保存可视化参数 + if self.plugin.advanced_visuals: + state_snapshot['visualization_params'] = self.plugin.advanced_visuals.get_visualization_config().copy() + + # 添加到历史记录 + if self.history_position < len(self.edit_history) - 1: + # 如果当前不在历史记录末尾,截断后续记录 + self.edit_history = self.edit_history[:self.history_position + 1] + + self.edit_history.append(state_snapshot) + self.history_position += 1 + + # 限制历史记录数量 + if len(self.edit_history) > self.editor_config['history_limit']: + self.edit_history.pop(0) + self.history_position -= 1 + + except Exception as e: + print(f"✗ 状态保存失败: {e}") + import traceback + traceback.print_exc() + + def undo(self) -> bool: + """ + 撤销操作 + + Returns: + 是否撤销成功 + """ + try: + if not self.enabled: + print("✗ 生态编辑器未启用") + return False + + if self.history_position <= 0: + print("✗ 没有可撤销的操作") + return False + + # 移动到前一个历史状态 + self.history_position -= 1 + + # 恢复状态 + self._restore_state_from_history() + + # 更新统计信息 + self.stats['undos_performed'] += 1 + + print("✓ 撤销操作完成") + return True + + except Exception as e: + print(f"✗ 撤销操作失败: {e}") + import traceback + traceback.print_exc() + return False + + def redo(self) -> bool: + """ + 重做操作 + + Returns: + 是否重做成功 + """ + try: + if not self.enabled: + print("✗ 生态编辑器未启用") + return False + + if self.history_position >= len(self.edit_history) - 1: + print("✗ 没有可重做的操作") + return False + + # 移动到下一个历史状态 + self.history_position += 1 + + # 恢复状态 + self._restore_state_from_history() + + # 更新统计信息 + self.stats['redos_performed'] += 1 + + print("✓ 重做操作完成") + return True + + except Exception as e: + print(f"✗ 重做操作失败: {e}") + import traceback + traceback.print_exc() + return False + + def _restore_state_from_history(self): + """从历史记录恢复状态""" + try: + if self.history_position < 0: + return + + # 获取历史状态 + state = self.edit_history[self.history_position] + + # 恢复参数 + if self.plugin.vegetation_manager and state['vegetation_params']: + # 在实际实现中会恢复植被参数 + pass + + if self.plugin.animal_simulator and state['animal_params']: + # 在实际实现中会恢复动物参数 + pass + + if self.plugin.vegetation_manager and state['environment_params']: + self.plugin.vegetation_manager.update_environment_factors(state['environment_params']) + + if self.plugin.ecosystem_manager and state['ecosystem_params']: + for param_name, param_value in state['ecosystem_params'].items(): + self.plugin.ecosystem_manager.set_ecosystem_parameter(param_name, param_value) + + if self.plugin.seasonal_effects and state['season_params']: + # 在实际实现中会恢复季节参数 + pass + + if self.plugin.ecosystem_interactions and state['interaction_params']: + # 恢复交互参数 + for param_name, param_value in state['interaction_params'].items(): + self.plugin.ecosystem_interactions.set_interaction_parameter(param_name, param_value) + + if self.plugin.advanced_visuals and state['visualization_params']: + # 恢复可视化参数 + self.plugin.advanced_visuals.set_visualization_config(state['visualization_params']) + + # 标记预览需要更新 + self.preview_dirty = True + + except Exception as e: + print(f"✗ 状态恢复失败: {e}") + import traceback + traceback.print_exc() + + def clear_history(self): + """清空历史记录""" + try: + self.edit_history.clear() + self.history_position = -1 + print("✓ 历史记录已清空") + except Exception as e: + print(f"✗ 历史记录清空失败: {e}") + import traceback + traceback.print_exc() + + def get_parameter_groups(self) -> Dict[str, List[str]]: + """ + 获取参数分组 + + Returns: + 参数分组字典 + """ + return self.parameter_groups.copy() + + def get_parameters_in_group(self, group_name: str) -> List[str]: + """ + 获取组中的参数列表 + + Args: + group_name: 组名称 + + Returns: + 参数列表 + """ + return self.parameter_groups.get(group_name, []).copy() + + def set_editor_config(self, config: Dict[str, Any]): + """ + 设置编辑器配置 + + Args: + config: 配置字典 + """ + self.editor_config.update(config) + print(f"✓ 编辑器配置已更新: {self.editor_config}") + + def get_editor_config(self) -> Dict[str, Any]: + """ + 获取编辑器配置 + + Returns: + 配置字典 + """ + return self.editor_config.copy() + + def set_tool(self, tool_name: str): + """ + 设置当前工具 + + Args: + tool_name: 工具名称 + """ + if tool_name in self.tools: + self.current_tool = tool_name + print(f"✓ 当前工具设置为: {self.tools[tool_name]['name']}") + else: + print(f"✗ 无效的工具: {tool_name}") + + def get_tools(self) -> Dict[str, Dict[str, str]]: + """ + 获取可用工具 + + Returns: + 工具字典 + """ + return self.tools.copy() + + def get_current_tool(self) -> str: + """ + 获取当前工具 + + Returns: + 当前工具名称 + """ + return self.current_tool + + def select_object(self, object_id: Any, multi_select: bool = False): + """ + 选择对象 + + Args: + object_id: 对象ID + multi_select: 是否多选 + """ + try: + if multi_select: + if object_id not in self.selected_objects: + self.selected_objects.append(object_id) + else: + self.selected_objects = [object_id] + + self.stats['objects_selected'] = len(self.selected_objects) + print(f"✓ 对象已选择: {object_id}") + + except Exception as e: + print(f"✗ 对象选择失败: {e}") + + def deselect_object(self, object_id: Any): + """ + 取消选择对象 + + Args: + object_id: 对象ID + """ + try: + if object_id in self.selected_objects: + self.selected_objects.remove(object_id) + self.stats['objects_selected'] = len(self.selected_objects) + print(f"✓ 对象已取消选择: {object_id}") + + except Exception as e: + print(f"✗ 对象取消选择失败: {e}") + + def clear_selection(self): + """清空选择""" + try: + self.selected_objects.clear() + self.stats['objects_selected'] = 0 + print("✓ 选择已清空") + except Exception as e: + print(f"✗ 选择清空失败: {e}") + + def get_selected_objects(self) -> List[Any]: + """ + 获取选中的对象 + + Returns: + 选中的对象列表 + """ + return self.selected_objects.copy() + + def set_selection_mode(self, mode: str): + """ + 设置选择模式 + + Args: + mode: 选择模式 ('single', 'multiple', 'area') + """ + if mode in ['single', 'multiple', 'area']: + self.selection_mode = mode + print(f"✓ 选择模式设置为: {mode}") + else: + print(f"✗ 无效的选择模式: {mode}") + + def get_selection_mode(self) -> str: + """ + 获取选择模式 + + Returns: + 选择模式 + """ + return self.selection_mode + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + # 更新平均编辑时间 + total_edits = self.stats['edits_made'] + self.stats['undos_performed'] + self.stats['redos_performed'] + if total_edits > 0: + self.stats['average_edit_time'] = self.stats['total_edit_time'] / total_edits + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'edits_made': 0, + 'undos_performed': 0, + 'redos_performed': 0, + 'total_edit_time': 0.0, + 'average_edit_time': 0.0, + 'objects_selected': 0 + } + print("✓ 生态编辑器统计信息已重置") + + def export_editor_state(self, filename: str) -> bool: + """ + 导出编辑器状态 + + Args: + filename: 文件名 + + Returns: + 是否导出成功 + """ + try: + # 创建状态数据 + state_data = { + 'editor_config': self.editor_config, + 'current_edit_mode': self.current_edit_mode, + 'current_tool': self.current_tool, + 'parameter_groups': self.parameter_groups, + 'timestamp': time.time() + } + + # 保存为JSON文件 + with open(filename, 'w', encoding='utf-8') as f: + json.dump(state_data, f, ensure_ascii=False, indent=2) + + print(f"✓ 编辑器状态已导出到: {filename}") + return True + + except Exception as e: + print(f"✗ 编辑器状态导出失败: {e}") + import traceback + traceback.print_exc() + return False + + def import_editor_state(self, filename: str) -> bool: + """ + 导入编辑器状态 + + Args: + filename: 文件名 + + Returns: + 是否导入成功 + """ + try: + # 读取JSON文件 + with open(filename, 'r', encoding='utf-8') as f: + state_data = json.load(f) + + # 应用状态 + if 'editor_config' in state_data: + self.set_editor_config(state_data['editor_config']) + if 'current_edit_mode' in state_data: + self.set_edit_mode(state_data['current_edit_mode']) + if 'current_tool' in state_data: + self.set_tool(state_data['current_tool']) + + print(f"✓ 编辑器状态已从 {filename} 导入") + return True + + except Exception as e: + print(f"✗ 编辑器状态导入失败: {e}") + import traceback + traceback.print_exc() + return False + + def set_ui_component(self, component_name: str, component: Any): + """ + 设置UI组件引用 + + Args: + component_name: 组件名称 + component: 组件引用 + """ + if component_name in self.ui_components: + self.ui_components[component_name] = component + print(f"✓ UI组件 '{component_name}' 已设置") + else: + print(f"✗ 无效的UI组件名称: {component_name}") + + def refresh_ui(self): + """刷新UI显示""" + try: + # 在实际实现中,这里会更新所有UI组件的显示 + print("✓ UI刷新完成") + except Exception as e: + print(f"✗ UI刷新失败: {e}") + + def show_preview_window(self): + """显示预览窗口""" + try: + # 在实际实现中,这里会创建并显示预览窗口 + print("✓ 预览窗口已显示") + except Exception as e: + print(f"✗ 预览窗口显示失败: {e}") + + def hide_preview_window(self): + """隐藏预览窗口""" + try: + # 在实际实现中,这里会隐藏预览窗口 + print("✓ 预览窗口已隐藏") + except Exception as e: + print(f"✗ 预览窗口隐藏失败: {e}") + + def update_preview_window(self): + """更新预览窗口内容""" + try: + # 在实际实现中,这里会更新预览窗口的内容 + print("✓ 预览窗口已更新") + except Exception as e: + print(f"✗ 预览窗口更新失败: {e}") + + def apply_preset(self, preset_name: str): + """ + 应用预设 + + Args: + preset_name: 预设名称 + """ + try: + if self.plugin.preset_manager: + self.plugin.preset_manager.apply_preset(preset_name) + self.preview_dirty = True + print(f"✓ 预设 '{preset_name}' 已应用") + else: + print("✗ 预设管理器不可用") + except Exception as e: + print(f"✗ 预设应用失败: {e}") + import traceback + traceback.print_exc() + + def save_current_as_preset(self, preset_name: str, description: str = '') -> str: + """ + 将当前配置保存为预设 + + Args: + preset_name: 预设名称 + description: 预设描述 + + Returns: + 预设ID + """ + try: + if self.plugin.preset_manager: + preset_id = self.plugin.preset_manager.create_terrain_preset_from_current(preset_name, description) + print(f"✓ 当前配置已保存为预设: {preset_name}") + return preset_id + else: + print("✗ 预设管理器不可用") + return '' + except Exception as e: + print(f"✗ 预设保存失败: {e}") + import traceback + traceback.print_exc() + return '' + + def spawn_vegetation(self, vegetation_type: str, position: tuple, count: int = 1): + """ + 生成植被 + + Args: + vegetation_type: 植被类型 + position: 位置 + count: 数量 + """ + try: + if not self.plugin.vegetation_manager: + print("✗ 植被管理器不可用") + return + + if vegetation_type not in self.plugin.vegetation_manager.get_vegetation_types(): + print(f"✗ 无效的植被类型: {vegetation_type}") + return + + # 保存当前状态到历史记录 + self._save_state_to_history() + + spawned_count = 0 + for i in range(count): + # 添加随机偏移 + if count > 1: + offset_x = random.uniform(-2.0, 2.0) + offset_z = random.uniform(-2.0, 2.0) + spawn_position = (position[0] + offset_x, position[1], position[2] + offset_z) + else: + spawn_position = position + + instance_id = self.plugin.vegetation_manager.create_vegetation_instance( + vegetation_type, spawn_position + ) + + if instance_id >= 0: + spawned_count += 1 + + print(f"✓ 已生成 {spawned_count} 个 {vegetation_type}") + self.stats['edits_made'] += 1 + + except Exception as e: + print(f"✗ 植被生成失败: {e}") + import traceback + traceback.print_exc() + + def remove_selected_objects(self): + """移除选中的对象""" + try: + if not self.plugin.vegetation_manager: + print("✗ 植被管理器不可用") + return + + # 保存当前状态到历史记录 + self._save_state_to_history() + + removed_count = 0 + for obj_id in self.selected_objects: + # 假设所有选中对象都是植被实例 + if self.plugin.vegetation_manager.remove_vegetation_instance(obj_id): + removed_count += 1 + + self.clear_selection() + print(f"✓ 已移除 {removed_count} 个对象") + self.stats['edits_made'] += 1 + + except Exception as e: + print(f"✗ 对象移除失败: {e}") + import traceback + traceback.print_exc() + + def analyze_ecosystem(self) -> Dict[str, Any]: + """ + 分析生态系统状态 + + Returns: + 分析结果字典 + """ + try: + analysis_results = { + 'timestamp': time.time(), + 'biodiversity': 0.0, + 'health_index': 0.0, + 'stability': 0.0, + 'total_species': 0, + 'total_population': 0, + 'resource_availability': 0.0 + } + + # 收集生态系统数据 + if self.plugin.ecosystem_manager: + system_state = self.plugin.ecosystem_manager.get_system_state() + analysis_results['health_index'] = system_state.get('health_index', 0.0) + analysis_results['stability'] = self.plugin.ecosystem_manager.get_stability_index() + analysis_results['biodiversity'] = self.plugin.ecosystem_manager.get_biodiversity_index() + + if self.plugin.vegetation_manager: + veg_stats = self.plugin.vegetation_manager.get_stats() + analysis_results['total_species'] += len(veg_stats.get('vegetation_by_type', {})) + analysis_results['total_population'] += veg_stats.get('total_vegetation', 0) + + if self.plugin.animal_simulator: + animal_stats = self.plugin.animal_simulator.get_stats() + analysis_results['total_species'] += len(animal_stats.get('animals_by_species', {})) + analysis_results['total_population'] += animal_stats.get('total_animals', 0) + + if self.plugin.vegetation_manager: + env_factors = self.plugin.vegetation_manager.get_environment_factors() + # 简化的资源可用性计算 + resource_score = ( + env_factors.get('temperature', 50) / 100 * 0.25 + + env_factors.get('humidity', 50) / 100 * 0.25 + + env_factors.get('soil_fertility', 0.5) * 0.25 + + env_factors.get('water_level', 0.5) * 0.25 + ) + analysis_results['resource_availability'] = resource_score + + # 保存分析结果 + self.analysis_data['last_analysis'] = analysis_results + self.analysis_data['analysis_timestamp'] = analysis_results['timestamp'] + self.analysis_data['analysis_results'] = analysis_results + + print("✓ 生态系统分析完成") + return analysis_results + + except Exception as e: + print(f"✗ 生态系统分析失败: {e}") + import traceback + traceback.print_exc() + return {} + + def get_last_analysis(self) -> Optional[Dict[str, Any]]: + """ + 获取上次分析结果 + + Returns: + 上次分析结果或None + """ + return self.analysis_data['last_analysis'] + + def export_analysis_report(self, filename: str) -> bool: + """ + 导出分析报告 + + Args: + filename: 文件名 + + Returns: + 是否导出成功 + """ + try: + analysis = self.get_last_analysis() + if not analysis: + print("✗ 没有可用的分析数据") + return False + + # 创建报告数据 + report_data = { + 'analysis_report': analysis, + 'editor_state': { + 'edit_mode': self.current_edit_mode, + 'tool': self.current_tool, + 'timestamp': time.time() + } + } + + # 保存为JSON文件 + with open(filename, 'w', encoding='utf-8') as f: + json.dump(report_data, f, ensure_ascii=False, indent=2) + + print(f"✓ 分析报告已导出到: {filename}") + return True + + except Exception as e: + print(f"✗ 分析报告导出失败: {e}") + import traceback + traceback.print_exc() + return False + + def toggle_visualization_type(self, visualization_type: str): + """ + 切换可视化类型启用状态 + + Args: + visualization_type: 可视化类型 + """ + try: + if self.plugin.advanced_visuals: + # 获取当前状态 + vis_types = self.plugin.advanced_visuals.get_visualization_types() + if visualization_type in vis_types: + current_state = vis_types[visualization_type]['enabled'] + self.plugin.advanced_visuals.set_visualization_type_enabled(visualization_type, not current_state) + print(f"✓ 可视化类型 '{visualization_type}' 已{'启用' if not current_state else '禁用'}") + else: + print(f"✗ 无效的可视化类型: {visualization_type}") + else: + print("✗ 高级可视化模块不可用") + except Exception as e: + print(f"✗ 可视化类型切换失败: {e}") + + def capture_visualization_screenshot(self, filename: str = None) -> str: + """ + 捕获可视化截图 + + Args: + filename: 文件名 + + Returns: + 截图文件路径 + """ + try: + if self.plugin.advanced_visuals: + return self.plugin.advanced_visuals.capture_visualization_screenshot(filename) + else: + print("✗ 高级可视化模块不可用") + return "" + except Exception as e: + print(f"✗ 可视化截图捕获失败: {e}") return "" \ No newline at end of file diff --git a/plugins/user/vegetation_ecosystem/editor/preset_manager.py b/plugins/user/vegetation_ecosystem/editor/preset_manager.py index ff136005..5fcfb618 100644 --- a/plugins/user/vegetation_ecosystem/editor/preset_manager.py +++ b/plugins/user/vegetation_ecosystem/editor/preset_manager.py @@ -1,1083 +1,1083 @@ -""" -预设管理器 -管理植被和生态系统插件的预设配置 -""" - -import json -import os -from typing import Dict, Any, List, Optional -import time - -class PresetManager: - """ - 预设管理器 - 管理植被和生态系统插件的预设配置,包括创建、保存、加载和应用预设 - """ - - def __init__(self, plugin): - """ - 初始化预设管理器 - - Args: - plugin: 植被和生态系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 预设配置 - self.preset_config = { - 'presets_directory': 'presets/ecosystem', - 'default_preset': 'default', - 'auto_save_enabled': True, - 'max_presets': 100 - } - - # 预设数据 - self.presets = {} - self.current_preset = None - - # 预设类别 - self.preset_categories = { - 'vegetation': {'name': '植被', 'description': '植被配置预设'}, - 'animals': {'name': '动物', 'description': '动物配置预设'}, - 'environment': {'name': '环境', 'description': '环境配置预设'}, - 'ecosystem': {'name': '生态系统', 'description': '生态系统配置预设'}, - 'seasons': {'name': '季节', 'description': '季节配置预设'}, - 'mixed': {'name': '混合', 'description': '混合配置预设'} - } - - # 统计信息 - self.stats = { - 'presets_loaded': 0, - 'presets_saved': 0, - 'presets_applied': 0, - 'total_load_time': 0.0, - 'total_save_time': 0.0 - } - - print("✓ 预设管理器已创建") - - def initialize(self) -> bool: - """ - 初始化预设管理器 - - Returns: - 是否初始化成功 - """ - try: - # 创建预设目录 - presets_dir = self.preset_config['presets_directory'] - if not os.path.exists(presets_dir): - os.makedirs(presets_dir) - - # 加载默认预设 - self._load_default_presets() - - # 加载用户预设 - self._load_user_presets() - - self.initialized = True - print("✓ 预设管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 预设管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用预设管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 预设管理器未初始化") - return False - - self.enabled = True - print("✓ 预设管理器已启用") - return True - - except Exception as e: - print(f"✗ 预设管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用预设管理器""" - try: - self.enabled = False - print("✓ 预设管理器已禁用") - - except Exception as e: - print(f"✗ 预设管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理预设管理器资源""" - try: - self.disable() - - # 自动保存当前预设 - if self.preset_config['auto_save_enabled'] and self.current_preset: - self.save_preset(self.current_preset) - - self.initialized = False - print("✓ 预设管理器资源已清理") - - except Exception as e: - print(f"✗ 预设管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新预设管理器状态 - - Args: - dt: 时间增量 - """ - # 处理更新逻辑 - pass - - def _load_default_presets(self): - """加载默认预设""" - try: - # 创建默认植被预设 - self.presets['default_vegetation'] = { - 'name': '默认植被', - 'category': 'vegetation', - 'description': '默认的植被配置参数', - 'parameters': { - 'grass_density': 0.8, - 'tree_density': 0.3, - 'flower_density': 0.2 - }, - 'timestamp': time.time(), - 'is_default': True - } - - # 创建默认动物预设 - self.presets['default_animals'] = { - 'name': '默认动物', - 'category': 'animals', - 'description': '默认的动物配置参数', - 'parameters': { - 'rabbit_population': 20, - 'deer_population': 5, - 'bird_population': 30 - }, - 'timestamp': time.time(), - 'is_default': True - } - - # 创建默认环境预设 - self.presets['default_environment'] = { - 'name': '默认环境', - 'category': 'environment', - 'description': '默认的环境配置参数', - 'parameters': { - 'temperature': 20.0, - 'humidity': 60.0, - 'soil_fertility': 0.5 - }, - 'timestamp': time.time(), - 'is_default': True - } - - # 创建默认生态系统预设 - self.presets['default_ecosystem'] = { - 'name': '默认生态系统', - 'category': 'ecosystem', - 'description': '默认的生态系统配置参数', - 'parameters': { - 'biodiversity_index': 0.6, - 'stability_index': 0.7, - 'productivity_rate': 1.0 - }, - 'timestamp': time.time(), - 'is_default': True - } - - # 创建默认季节预设 - self.presets['default_seasons'] = { - 'name': '默认季节', - 'category': 'seasons', - 'description': '默认的季节配置参数', - 'parameters': { - 'spring_duration': 90, - 'summer_duration': 90, - 'autumn_duration': 90, - 'winter_duration': 90 - }, - 'timestamp': time.time(), - 'is_default': True - } - - # 创建默认混合预设 - self.presets['default_mixed'] = { - 'name': '默认混合', - 'category': 'mixed', - 'description': '默认的混合配置参数', - 'parameters': { - 'vegetation_config': 'default_vegetation', - 'animal_config': 'default_animals', - 'environment_config': 'default_environment' - }, - 'timestamp': time.time(), - 'is_default': True - } - - print("✓ 默认预设加载完成") - - except Exception as e: - print(f"✗ 默认预设加载失败: {e}") - import traceback - traceback.print_exc() - - def _load_user_presets(self): - """加载用户预设""" - try: - load_start_time = time.time() - - presets_dir = self.preset_config['presets_directory'] - if not os.path.exists(presets_dir): - print("✓ 用户预设目录不存在,跳过加载") - return - - # 遍历预设目录 - for filename in os.listdir(presets_dir): - if filename.endswith('.json'): - try: - filepath = os.path.join(presets_dir, filename) - with open(filepath, 'r', encoding='utf-8') as f: - preset_data = json.load(f) - - # 使用文件名(不含扩展名)作为预设ID - preset_id = os.path.splitext(filename)[0] - self.presets[preset_id] = preset_data - - except Exception as e: - print(f"✗ 预设文件加载失败 {filename}: {e}") - - load_time = time.time() - load_start_time - self.stats['presets_loaded'] = len(self.presets) - self.stats['total_load_time'] = load_time - - print(f"✓ 用户预设加载完成,共加载 {len(self.presets)} 个预设,耗时: {load_time:.3f}秒") - - except Exception as e: - print(f"✗ 用户预设加载失败: {e}") - import traceback - traceback.print_exc() - - def save_preset(self, preset_id: str, preset_data: Dict[str, Any] = None) -> bool: - """ - 保存预设 - - Args: - preset_id: 预设ID - preset_data: 预设数据(如果为None,则保存当前配置) - - Returns: - 是否保存成功 - """ - try: - save_start_time = time.time() - - if not self.enabled: - print("✗ 预设管理器未启用") - return False - - # 如果未提供预设数据,则创建当前配置的快照 - if preset_data is None: - preset_data = self._create_current_preset_snapshot(preset_id) - - # 保存预设数据 - self.presets[preset_id] = preset_data - - # 保存到文件 - presets_dir = self.preset_config['presets_directory'] - filepath = os.path.join(presets_dir, f"{preset_id}.json") - - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(preset_data, f, ensure_ascii=False, indent=2) - - # 更新统计信息 - save_time = time.time() - save_start_time - self.stats['presets_saved'] += 1 - self.stats['total_save_time'] += save_time - - print(f"✓ 预设 '{preset_id}' 保存完成,耗时: {save_time:.3f}秒") - return True - - except Exception as e: - print(f"✗ 预设保存失败: {e}") - import traceback - traceback.print_exc() - return False - - def _create_current_preset_snapshot(self, preset_id: str) -> Dict[str, Any]: - """ - 创建当前配置的预设快照 - - Args: - preset_id: 预设ID - - Returns: - 预设数据字典 - """ - try: - # 获取当前插件配置 - preset_data = { - 'name': preset_id, - 'category': 'mixed', # 默认类别 - 'description': '用户自定义预设', - 'parameters': {}, - 'timestamp': time.time(), - 'is_default': False - } - - # 添加植被管理器配置 - if self.plugin.vegetation_manager: - vm = self.plugin.vegetation_manager - preset_data['parameters'].update({ - 'environment_factors': vm.get_environment_factors(), - 'distribution_config': vm.get_distribution_config() - }) - - # 添加动物模拟器配置 - if self.plugin.animal_simulator: - asim = self.plugin.animal_simulator - preset_data['parameters'].update({ - 'animal_environment_factors': asim.get_environment_factors(), - 'animal_behavior_config': asim.get_behavior_config() - }) - - # 添加生态系统管理器配置 - if self.plugin.ecosystem_manager: - em = self.plugin.ecosystem_manager - preset_data['parameters'].update({ - 'ecosystem_parameters': em.get_ecosystem_parameters(), - 'time_config': em.get_time_config() - }) - - # 添加季节效应配置 - if self.plugin.seasonal_effects: - se = self.plugin.seasonal_effects - preset_data['parameters'].update({ - 'season_config': se.get_time_config(), - 'current_season': se.get_current_season() - }) - - return preset_data - - except Exception as e: - print(f"✗ 预设快照创建失败: {e}") - import traceback - traceback.print_exc() - return { - 'name': preset_id, - 'category': 'mixed', - 'description': '默认预设', - 'parameters': {}, - 'timestamp': time.time(), - 'is_default': False - } - - def load_preset(self, preset_id: str) -> Optional[Dict[str, Any]]: - """ - 加载预设 - - Args: - preset_id: 预设ID - - Returns: - 预设数据字典或None - """ - try: - if not self.enabled: - print("✗ 预设管理器未启用") - return None - - if preset_id not in self.presets: - print(f"✗ 预设不存在: {preset_id}") - return None - - preset_data = self.presets[preset_id] - - # 更新统计信息 - self.stats['presets_applied'] += 1 - self.current_preset = preset_id - - print(f"✓ 预设 '{preset_id}' 加载完成") - return preset_data - - except Exception as e: - print(f"✗ 预设加载失败: {e}") - import traceback - traceback.print_exc() - return None - - def apply_preset(self, preset_id: str) -> bool: - """ - 应用预设到当前配置 - - Args: - preset_id: 预设ID - - Returns: - 是否应用成功 - """ - try: - if not self.enabled: - print("✗ 预设管理器未启用") - return False - - preset_data = self.load_preset(preset_id) - if preset_data is None: - return False - - parameters = preset_data.get('parameters', {}) - - # 应用参数到各个组件 - self._apply_parameters_to_components(parameters) - - # 更新统计信息 - self.stats['presets_applied'] += 1 - self.current_preset = preset_id - - print(f"✓ 预设 '{preset_id}' 应用完成") - return True - - except Exception as e: - print(f"✗ 预设应用失败: {e}") - import traceback - traceback.print_exc() - return False - - def _apply_parameters_to_components(self, parameters: Dict[str, Any]): - """ - 应用参数到各个组件 - - Args: - parameters: 参数字典 - """ - try: - # 应用到植被管理器 - if self.plugin.vegetation_manager: - if 'environment_factors' in parameters: - self.plugin.vegetation_manager.update_environment_factors(parameters['environment_factors']) - if 'distribution_config' in parameters: - self.plugin.vegetation_manager.set_distribution_config(parameters['distribution_config']) - - # 应用到动物模拟器 - if self.plugin.animal_simulator: - if 'animal_environment_factors' in parameters: - self.plugin.animal_simulator.update_environment_factors(parameters['animal_environment_factors']) - if 'animal_behavior_config' in parameters: - self.plugin.animal_simulator.set_behavior_config(parameters['animal_behavior_config']) - - # 应用到生态系统管理器 - if self.plugin.ecosystem_manager: - if 'ecosystem_parameters' in parameters: - for param_name, param_value in parameters['ecosystem_parameters'].items(): - self.plugin.ecosystem_manager.set_ecosystem_parameter(param_name, param_value) - if 'time_config' in parameters: - self.plugin.ecosystem_manager.set_time_config(parameters['time_config']) - - # 应用到季节效应系统 - if self.plugin.seasonal_effects: - if 'season_config' in parameters: - self.plugin.seasonal_effects.set_time_config(parameters['season_config']) - if 'current_season' in parameters: - self.plugin.seasonal_effects.set_current_season(parameters['current_season']) - - except Exception as e: - print(f"✗ 参数应用失败: {e}") - import traceback - traceback.print_exc() - - def delete_preset(self, preset_id: str) -> bool: - """ - 删除预设 - - Args: - preset_id: 预设ID - - Returns: - 是否删除成功 - """ - try: - if not self.enabled: - print("✗ 预设管理器未启用") - return False - - if preset_id not in self.presets: - print(f"✗ 预设不存在: {preset_id}") - return False - - # 检查是否为默认预设 - if self.presets[preset_id].get('is_default', False): - print(f"✗ 无法删除默认预设: {preset_id}") - return False - - # 从内存中删除 - del self.presets[preset_id] - - # 从文件系统中删除 - presets_dir = self.preset_config['presets_directory'] - filepath = os.path.join(presets_dir, f"{preset_id}.json") - if os.path.exists(filepath): - os.remove(filepath) - - # 如果删除的是当前预设,清空当前预设 - if self.current_preset == preset_id: - self.current_preset = None - - print(f"✓ 预设 '{preset_id}' 删除完成") - return True - - except Exception as e: - print(f"✗ 预设删除失败: {e}") - import traceback - traceback.print_exc() - return False - - def list_presets(self, category: str = None) -> List[str]: - """ - 列出预设 - - Args: - category: 预设类别(如果为None,则列出所有预设) - - Returns: - 预设ID列表 - """ - try: - if category is None: - return list(self.presets.keys()) - else: - return [preset_id for preset_id, preset_data in self.presets.items() - if preset_data.get('category') == category] - - except Exception as e: - print(f"✗ 预设列表获取失败: {e}") - import traceback - traceback.print_exc() - return [] - - def get_preset_info(self, preset_id: str) -> Optional[Dict[str, Any]]: - """ - 获取预设信息 - - Args: - preset_id: 预设ID - - Returns: - 预设信息字典或None - """ - try: - if preset_id not in self.presets: - return None - - preset_data = self.presets[preset_id] - return { - 'name': preset_data.get('name', preset_id), - 'category': preset_data.get('category', 'unknown'), - 'description': preset_data.get('description', ''), - 'timestamp': preset_data.get('timestamp', 0), - 'is_default': preset_data.get('is_default', False), - 'parameters_count': len(preset_data.get('parameters', {})) - } - - except Exception as e: - print(f"✗ 预设信息获取失败: {e}") - import traceback - traceback.print_exc() - return None - - def get_preset_categories(self) -> Dict[str, Dict[str, str]]: - """ - 获取预设类别 - - Returns: - 预设类别字典 - """ - return self.preset_categories.copy() - - def create_preset_category(self, category_id: str, name: str, description: str = '') -> bool: - """ - 创建预设类别 - - Args: - category_id: 类别ID - name: 类别名称 - description: 类别描述 - - Returns: - 是否创建成功 - """ - try: - self.preset_categories[category_id] = { - 'name': name, - 'description': description - } - - print(f"✓ 预设类别 '{name}' 创建完成") - return True - - except Exception as e: - print(f"✗ 预设类别创建失败: {e}") - import traceback - traceback.print_exc() - return False - - def export_preset(self, preset_id: str, filepath: str) -> bool: - """ - 导出预设到文件 - - Args: - preset_id: 预设ID - filepath: 导出文件路径 - - Returns: - 是否导出成功 - """ - try: - if preset_id not in self.presets: - print(f"✗ 预设不存在: {preset_id}") - return False - - preset_data = self.presets[preset_id] - - # 保存到指定文件 - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(preset_data, f, ensure_ascii=False, indent=2) - - print(f"✓ 预设 '{preset_id}' 导出到 {filepath}") - return True - - except Exception as e: - print(f"✗ 预设导出失败: {e}") - import traceback - traceback.print_exc() - return False - - def import_preset(self, filepath: str, preset_id: str = None) -> bool: - """ - 从文件导入预设 - - Args: - filepath: 导入文件路径 - preset_id: 预设ID(如果为None,则使用文件名) - - Returns: - 是否导入成功 - """ - try: - # 读取预设文件 - with open(filepath, 'r', encoding='utf-8') as f: - preset_data = json.load(f) - - # 确定预设ID - if preset_id is None: - preset_id = os.path.splitext(os.path.basename(filepath))[0] - - # 保存预设 - self.presets[preset_id] = preset_data - - # 保存到预设目录 - presets_dir = self.preset_config['presets_directory'] - save_filepath = os.path.join(presets_dir, f"{preset_id}.json") - - with open(save_filepath, 'w', encoding='utf-8') as f: - json.dump(preset_data, f, ensure_ascii=False, indent=2) - - print(f"✓ 预设从 {filepath} 导入完成,ID: {preset_id}") - return True - - except Exception as e: - print(f"✗ 预设导入失败: {e}") - import traceback - traceback.print_exc() - return False - - def set_preset_config(self, config: Dict[str, Any]): - """ - 设置预设配置 - - Args: - config: 配置字典 - """ - self.preset_config.update(config) - print(f"✓ 预设配置已更新: {self.preset_config}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'presets_loaded': 0, - 'presets_saved': 0, - 'presets_applied': 0, - 'total_load_time': 0.0, - 'total_save_time': 0.0 - } - print("✓ 预设管理器统计信息已重置") - - def get_current_preset(self) -> Optional[str]: - """ - 获取当前预设 - - Returns: - 当前预设ID或None - """ - return self.current_preset - - def set_current_preset(self, preset_id: str): - """ - 设置当前预设 - - Args: - preset_id: 预设ID - """ - if preset_id in self.presets: - self.current_preset = preset_id - print(f"✓ 当前预设设置为: {preset_id}") - else: - print(f"✗ 预设不存在: {preset_id}") - - def create_terrain_preset_from_current(self, preset_name: str, description: str = '') -> str: - """ - 从当前配置创建预设 - - Args: - preset_name: 预设名称 - description: 预设描述 - - Returns: - 预设ID - """ - try: - # 生成预设ID - preset_id = preset_name.lower().replace(' ', '_') - - # 创建预设数据 - preset_data = { - 'name': preset_name, - 'category': 'mixed', - 'description': description, - 'parameters': {}, - 'timestamp': time.time(), - 'is_default': False - } - - # 获取当前配置参数 - if self.plugin.vegetation_manager: - vm = self.plugin.vegetation_manager - preset_data['parameters'].update({ - 'environment_factors': vm.get_environment_factors(), - 'distribution_config': vm.get_distribution_config() - }) - - if self.plugin.ecosystem_manager: - em = self.plugin.ecosystem_manager - preset_data['parameters'].update({ - 'ecosystem_parameters': em.get_ecosystem_parameters() - }) - - # 保存预设 - self.save_preset(preset_id, preset_data) - - print(f"✓ 从当前配置创建预设: {preset_name}") - return preset_id - - except Exception as e: - print(f"✗ 从当前配置创建预设失败: {e}") - import traceback - traceback.print_exc() - return '' - - def batch_apply_presets(self, preset_ids: List[str]) -> bool: - """ - 批量应用预设 - - Args: - preset_ids: 预设ID列表 - - Returns: - 是否应用成功 - """ - try: - if not self.enabled: - print("✗ 预设管理器未启用") - return False - - success_count = 0 - for preset_id in preset_ids: - if self.apply_preset(preset_id): - success_count += 1 - - print(f"✓ 批量应用预设完成: {success_count}/{len(preset_ids)} 个预设应用成功") - return success_count == len(preset_ids) - - except Exception as e: - print(f"✗ 批量应用预设失败: {e}") - import traceback - traceback.print_exc() - return False - - def search_presets(self, keyword: str, category: str = None) -> List[str]: - """ - 搜索预设 - - Args: - keyword: 搜索关键词 - category: 预设类别(可选) - - Returns: - 匹配的预设ID列表 - """ - try: - matching_presets = [] - - for preset_id, preset_data in self.presets.items(): - # 检查类别筛选 - if category and preset_data.get('category') != category: - continue - - # 检查关键词匹配 - if (keyword.lower() in preset_data.get('name', '').lower() or - keyword.lower() in preset_data.get('description', '').lower()): - matching_presets.append(preset_id) - - return matching_presets - - except Exception as e: - print(f"✗ 预设搜索失败: {e}") - import traceback - traceback.print_exc() - return [] - - def duplicate_preset(self, source_preset_id: str, new_preset_id: str, new_name: str = None) -> bool: - """ - 复制预设 - - Args: - source_preset_id: 源预设ID - new_preset_id: 新预设ID - new_name: 新预设名称(如果为None,则使用源预设名称) - - Returns: - 是否复制成功 - """ - try: - if source_preset_id not in self.presets: - print(f"✗ 源预设不存在: {source_preset_id}") - return False - - # 复制预设数据 - preset_data = self.presets[source_preset_id].copy() - - # 更新名称 - if new_name: - preset_data['name'] = new_name - else: - preset_data['name'] = f"{preset_data['name']} (副本)" - - # 更新时间戳 - preset_data['timestamp'] = time.time() - - # 标记为非默认预设 - preset_data['is_default'] = False - - # 保存新预设 - return self.save_preset(new_preset_id, preset_data) - - except Exception as e: - print(f"✗ 预设复制失败: {e}") - import traceback - traceback.print_exc() - return False - - def update_preset_parameters(self, preset_id: str, parameters: Dict[str, Any]) -> bool: - """ - 更新预设参数 - - Args: - preset_id: 预设ID - parameters: 要更新的参数 - - Returns: - 是否更新成功 - """ - try: - if preset_id not in self.presets: - print(f"✗ 预设不存在: {preset_id}") - return False - - # 更新参数 - self.presets[preset_id]['parameters'].update(parameters) - - # 更新时间戳 - self.presets[preset_id]['timestamp'] = time.time() - - # 保存到文件 - presets_dir = self.preset_config['presets_directory'] - filepath = os.path.join(presets_dir, f"{preset_id}.json") - - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(self.presets[preset_id], f, ensure_ascii=False, indent=2) - - print(f"✓ 预设 '{preset_id}' 参数更新完成") - return True - - except Exception as e: - print(f"✗ 预设参数更新失败: {e}") - import traceback - traceback.print_exc() - return False - - def get_preset_parameters(self, preset_id: str) -> Optional[Dict[str, Any]]: - """ - 获取预设参数 - - Args: - preset_id: 预设ID - - Returns: - 参数字典或None - """ - try: - if preset_id not in self.presets: - return None - - return self.presets[preset_id].get('parameters', {}).copy() - - except Exception as e: - print(f"✗ 预设参数获取失败: {e}") - import traceback - traceback.print_exc() - return None - - def validate_preset(self, preset_id: str) -> bool: - """ - 验证预设完整性 - - Args: - preset_id: 预设ID - - Returns: - 预设是否有效 - """ - try: - if preset_id not in self.presets: - return False - - preset_data = self.presets[preset_id] - - # 检查必需字段 - required_fields = ['name', 'category', 'parameters'] - for field in required_fields: - if field not in preset_data: - print(f"✗ 预设 '{preset_id}' 缺少必需字段: {field}") - return False - - # 检查类别有效性 - if preset_data['category'] not in self.preset_categories: - print(f"✗ 预设 '{preset_id}' 包含无效类别: {preset_data['category']}") - return False - - return True - - except Exception as e: - print(f"✗ 预设验证失败: {e}") - import traceback - traceback.print_exc() - return False - - def backup_presets(self, backup_dir: str = 'presets_backup') -> bool: - """ - 备份所有预设 - - Args: - backup_dir: 备份目录 - - Returns: - 是否备份成功 - """ - try: - if not os.path.exists(backup_dir): - os.makedirs(backup_dir) - - backup_count = 0 - for preset_id, preset_data in self.presets.items(): - try: - filepath = os.path.join(backup_dir, f"{preset_id}.json") - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(preset_data, f, ensure_ascii=False, indent=2) - backup_count += 1 - except Exception as e: - print(f"✗ 预设 '{preset_id}' 备份失败: {e}") - - print(f"✓ 预设备份完成: {backup_count}/{len(self.presets)} 个预设已备份到 {backup_dir}") - return True - - except Exception as e: - print(f"✗ 预设备份失败: {e}") - import traceback - traceback.print_exc() - return False - - def restore_presets(self, backup_dir: str = 'presets_backup') -> bool: - """ - 从备份恢复预设 - - Args: - backup_dir: 备份目录 - - Returns: - 是否恢复成功 - """ - try: - if not os.path.exists(backup_dir): - print(f"✗ 备份目录不存在: {backup_dir}") - return False - - restore_count = 0 - for filename in os.listdir(backup_dir): - if filename.endswith('.json'): - try: - filepath = os.path.join(backup_dir, filename) - with open(filepath, 'r', encoding='utf-8') as f: - preset_data = json.load(f) - - preset_id = os.path.splitext(filename)[0] - self.presets[preset_id] = preset_data - restore_count += 1 - except Exception as e: - print(f"✗ 预设文件 '{filename}' 恢复失败: {e}") - - print(f"✓ 预设恢复完成: {restore_count} 个预设已从 {backup_dir} 恢复") - return True - - except Exception as e: - print(f"✗ 预设恢复失败: {e}") - import traceback - traceback.print_exc() +""" +预设管理器 +管理植被和生态系统插件的预设配置 +""" + +import json +import os +from typing import Dict, Any, List, Optional +import time + +class PresetManager: + """ + 预设管理器 + 管理植被和生态系统插件的预设配置,包括创建、保存、加载和应用预设 + """ + + def __init__(self, plugin): + """ + 初始化预设管理器 + + Args: + plugin: 植被和生态系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 预设配置 + self.preset_config = { + 'presets_directory': 'presets/ecosystem', + 'default_preset': 'default', + 'auto_save_enabled': True, + 'max_presets': 100 + } + + # 预设数据 + self.presets = {} + self.current_preset = None + + # 预设类别 + self.preset_categories = { + 'vegetation': {'name': '植被', 'description': '植被配置预设'}, + 'animals': {'name': '动物', 'description': '动物配置预设'}, + 'environment': {'name': '环境', 'description': '环境配置预设'}, + 'ecosystem': {'name': '生态系统', 'description': '生态系统配置预设'}, + 'seasons': {'name': '季节', 'description': '季节配置预设'}, + 'mixed': {'name': '混合', 'description': '混合配置预设'} + } + + # 统计信息 + self.stats = { + 'presets_loaded': 0, + 'presets_saved': 0, + 'presets_applied': 0, + 'total_load_time': 0.0, + 'total_save_time': 0.0 + } + + print("✓ 预设管理器已创建") + + def initialize(self) -> bool: + """ + 初始化预设管理器 + + Returns: + 是否初始化成功 + """ + try: + # 创建预设目录 + presets_dir = self.preset_config['presets_directory'] + if not os.path.exists(presets_dir): + os.makedirs(presets_dir) + + # 加载默认预设 + self._load_default_presets() + + # 加载用户预设 + self._load_user_presets() + + self.initialized = True + print("✓ 预设管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 预设管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用预设管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 预设管理器未初始化") + return False + + self.enabled = True + print("✓ 预设管理器已启用") + return True + + except Exception as e: + print(f"✗ 预设管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用预设管理器""" + try: + self.enabled = False + print("✓ 预设管理器已禁用") + + except Exception as e: + print(f"✗ 预设管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理预设管理器资源""" + try: + self.disable() + + # 自动保存当前预设 + if self.preset_config['auto_save_enabled'] and self.current_preset: + self.save_preset(self.current_preset) + + self.initialized = False + print("✓ 预设管理器资源已清理") + + except Exception as e: + print(f"✗ 预设管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新预设管理器状态 + + Args: + dt: 时间增量 + """ + # 处理更新逻辑 + pass + + def _load_default_presets(self): + """加载默认预设""" + try: + # 创建默认植被预设 + self.presets['default_vegetation'] = { + 'name': '默认植被', + 'category': 'vegetation', + 'description': '默认的植被配置参数', + 'parameters': { + 'grass_density': 0.8, + 'tree_density': 0.3, + 'flower_density': 0.2 + }, + 'timestamp': time.time(), + 'is_default': True + } + + # 创建默认动物预设 + self.presets['default_animals'] = { + 'name': '默认动物', + 'category': 'animals', + 'description': '默认的动物配置参数', + 'parameters': { + 'rabbit_population': 20, + 'deer_population': 5, + 'bird_population': 30 + }, + 'timestamp': time.time(), + 'is_default': True + } + + # 创建默认环境预设 + self.presets['default_environment'] = { + 'name': '默认环境', + 'category': 'environment', + 'description': '默认的环境配置参数', + 'parameters': { + 'temperature': 20.0, + 'humidity': 60.0, + 'soil_fertility': 0.5 + }, + 'timestamp': time.time(), + 'is_default': True + } + + # 创建默认生态系统预设 + self.presets['default_ecosystem'] = { + 'name': '默认生态系统', + 'category': 'ecosystem', + 'description': '默认的生态系统配置参数', + 'parameters': { + 'biodiversity_index': 0.6, + 'stability_index': 0.7, + 'productivity_rate': 1.0 + }, + 'timestamp': time.time(), + 'is_default': True + } + + # 创建默认季节预设 + self.presets['default_seasons'] = { + 'name': '默认季节', + 'category': 'seasons', + 'description': '默认的季节配置参数', + 'parameters': { + 'spring_duration': 90, + 'summer_duration': 90, + 'autumn_duration': 90, + 'winter_duration': 90 + }, + 'timestamp': time.time(), + 'is_default': True + } + + # 创建默认混合预设 + self.presets['default_mixed'] = { + 'name': '默认混合', + 'category': 'mixed', + 'description': '默认的混合配置参数', + 'parameters': { + 'vegetation_config': 'default_vegetation', + 'animal_config': 'default_animals', + 'environment_config': 'default_environment' + }, + 'timestamp': time.time(), + 'is_default': True + } + + print("✓ 默认预设加载完成") + + except Exception as e: + print(f"✗ 默认预设加载失败: {e}") + import traceback + traceback.print_exc() + + def _load_user_presets(self): + """加载用户预设""" + try: + load_start_time = time.time() + + presets_dir = self.preset_config['presets_directory'] + if not os.path.exists(presets_dir): + print("✓ 用户预设目录不存在,跳过加载") + return + + # 遍历预设目录 + for filename in os.listdir(presets_dir): + if filename.endswith('.json'): + try: + filepath = os.path.join(presets_dir, filename) + with open(filepath, 'r', encoding='utf-8') as f: + preset_data = json.load(f) + + # 使用文件名(不含扩展名)作为预设ID + preset_id = os.path.splitext(filename)[0] + self.presets[preset_id] = preset_data + + except Exception as e: + print(f"✗ 预设文件加载失败 {filename}: {e}") + + load_time = time.time() - load_start_time + self.stats['presets_loaded'] = len(self.presets) + self.stats['total_load_time'] = load_time + + print(f"✓ 用户预设加载完成,共加载 {len(self.presets)} 个预设,耗时: {load_time:.3f}秒") + + except Exception as e: + print(f"✗ 用户预设加载失败: {e}") + import traceback + traceback.print_exc() + + def save_preset(self, preset_id: str, preset_data: Dict[str, Any] = None) -> bool: + """ + 保存预设 + + Args: + preset_id: 预设ID + preset_data: 预设数据(如果为None,则保存当前配置) + + Returns: + 是否保存成功 + """ + try: + save_start_time = time.time() + + if not self.enabled: + print("✗ 预设管理器未启用") + return False + + # 如果未提供预设数据,则创建当前配置的快照 + if preset_data is None: + preset_data = self._create_current_preset_snapshot(preset_id) + + # 保存预设数据 + self.presets[preset_id] = preset_data + + # 保存到文件 + presets_dir = self.preset_config['presets_directory'] + filepath = os.path.join(presets_dir, f"{preset_id}.json") + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(preset_data, f, ensure_ascii=False, indent=2) + + # 更新统计信息 + save_time = time.time() - save_start_time + self.stats['presets_saved'] += 1 + self.stats['total_save_time'] += save_time + + print(f"✓ 预设 '{preset_id}' 保存完成,耗时: {save_time:.3f}秒") + return True + + except Exception as e: + print(f"✗ 预设保存失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_current_preset_snapshot(self, preset_id: str) -> Dict[str, Any]: + """ + 创建当前配置的预设快照 + + Args: + preset_id: 预设ID + + Returns: + 预设数据字典 + """ + try: + # 获取当前插件配置 + preset_data = { + 'name': preset_id, + 'category': 'mixed', # 默认类别 + 'description': '用户自定义预设', + 'parameters': {}, + 'timestamp': time.time(), + 'is_default': False + } + + # 添加植被管理器配置 + if self.plugin.vegetation_manager: + vm = self.plugin.vegetation_manager + preset_data['parameters'].update({ + 'environment_factors': vm.get_environment_factors(), + 'distribution_config': vm.get_distribution_config() + }) + + # 添加动物模拟器配置 + if self.plugin.animal_simulator: + asim = self.plugin.animal_simulator + preset_data['parameters'].update({ + 'animal_environment_factors': asim.get_environment_factors(), + 'animal_behavior_config': asim.get_behavior_config() + }) + + # 添加生态系统管理器配置 + if self.plugin.ecosystem_manager: + em = self.plugin.ecosystem_manager + preset_data['parameters'].update({ + 'ecosystem_parameters': em.get_ecosystem_parameters(), + 'time_config': em.get_time_config() + }) + + # 添加季节效应配置 + if self.plugin.seasonal_effects: + se = self.plugin.seasonal_effects + preset_data['parameters'].update({ + 'season_config': se.get_time_config(), + 'current_season': se.get_current_season() + }) + + return preset_data + + except Exception as e: + print(f"✗ 预设快照创建失败: {e}") + import traceback + traceback.print_exc() + return { + 'name': preset_id, + 'category': 'mixed', + 'description': '默认预设', + 'parameters': {}, + 'timestamp': time.time(), + 'is_default': False + } + + def load_preset(self, preset_id: str) -> Optional[Dict[str, Any]]: + """ + 加载预设 + + Args: + preset_id: 预设ID + + Returns: + 预设数据字典或None + """ + try: + if not self.enabled: + print("✗ 预设管理器未启用") + return None + + if preset_id not in self.presets: + print(f"✗ 预设不存在: {preset_id}") + return None + + preset_data = self.presets[preset_id] + + # 更新统计信息 + self.stats['presets_applied'] += 1 + self.current_preset = preset_id + + print(f"✓ 预设 '{preset_id}' 加载完成") + return preset_data + + except Exception as e: + print(f"✗ 预设加载失败: {e}") + import traceback + traceback.print_exc() + return None + + def apply_preset(self, preset_id: str) -> bool: + """ + 应用预设到当前配置 + + Args: + preset_id: 预设ID + + Returns: + 是否应用成功 + """ + try: + if not self.enabled: + print("✗ 预设管理器未启用") + return False + + preset_data = self.load_preset(preset_id) + if preset_data is None: + return False + + parameters = preset_data.get('parameters', {}) + + # 应用参数到各个组件 + self._apply_parameters_to_components(parameters) + + # 更新统计信息 + self.stats['presets_applied'] += 1 + self.current_preset = preset_id + + print(f"✓ 预设 '{preset_id}' 应用完成") + return True + + except Exception as e: + print(f"✗ 预设应用失败: {e}") + import traceback + traceback.print_exc() + return False + + def _apply_parameters_to_components(self, parameters: Dict[str, Any]): + """ + 应用参数到各个组件 + + Args: + parameters: 参数字典 + """ + try: + # 应用到植被管理器 + if self.plugin.vegetation_manager: + if 'environment_factors' in parameters: + self.plugin.vegetation_manager.update_environment_factors(parameters['environment_factors']) + if 'distribution_config' in parameters: + self.plugin.vegetation_manager.set_distribution_config(parameters['distribution_config']) + + # 应用到动物模拟器 + if self.plugin.animal_simulator: + if 'animal_environment_factors' in parameters: + self.plugin.animal_simulator.update_environment_factors(parameters['animal_environment_factors']) + if 'animal_behavior_config' in parameters: + self.plugin.animal_simulator.set_behavior_config(parameters['animal_behavior_config']) + + # 应用到生态系统管理器 + if self.plugin.ecosystem_manager: + if 'ecosystem_parameters' in parameters: + for param_name, param_value in parameters['ecosystem_parameters'].items(): + self.plugin.ecosystem_manager.set_ecosystem_parameter(param_name, param_value) + if 'time_config' in parameters: + self.plugin.ecosystem_manager.set_time_config(parameters['time_config']) + + # 应用到季节效应系统 + if self.plugin.seasonal_effects: + if 'season_config' in parameters: + self.plugin.seasonal_effects.set_time_config(parameters['season_config']) + if 'current_season' in parameters: + self.plugin.seasonal_effects.set_current_season(parameters['current_season']) + + except Exception as e: + print(f"✗ 参数应用失败: {e}") + import traceback + traceback.print_exc() + + def delete_preset(self, preset_id: str) -> bool: + """ + 删除预设 + + Args: + preset_id: 预设ID + + Returns: + 是否删除成功 + """ + try: + if not self.enabled: + print("✗ 预设管理器未启用") + return False + + if preset_id not in self.presets: + print(f"✗ 预设不存在: {preset_id}") + return False + + # 检查是否为默认预设 + if self.presets[preset_id].get('is_default', False): + print(f"✗ 无法删除默认预设: {preset_id}") + return False + + # 从内存中删除 + del self.presets[preset_id] + + # 从文件系统中删除 + presets_dir = self.preset_config['presets_directory'] + filepath = os.path.join(presets_dir, f"{preset_id}.json") + if os.path.exists(filepath): + os.remove(filepath) + + # 如果删除的是当前预设,清空当前预设 + if self.current_preset == preset_id: + self.current_preset = None + + print(f"✓ 预设 '{preset_id}' 删除完成") + return True + + except Exception as e: + print(f"✗ 预设删除失败: {e}") + import traceback + traceback.print_exc() + return False + + def list_presets(self, category: str = None) -> List[str]: + """ + 列出预设 + + Args: + category: 预设类别(如果为None,则列出所有预设) + + Returns: + 预设ID列表 + """ + try: + if category is None: + return list(self.presets.keys()) + else: + return [preset_id for preset_id, preset_data in self.presets.items() + if preset_data.get('category') == category] + + except Exception as e: + print(f"✗ 预设列表获取失败: {e}") + import traceback + traceback.print_exc() + return [] + + def get_preset_info(self, preset_id: str) -> Optional[Dict[str, Any]]: + """ + 获取预设信息 + + Args: + preset_id: 预设ID + + Returns: + 预设信息字典或None + """ + try: + if preset_id not in self.presets: + return None + + preset_data = self.presets[preset_id] + return { + 'name': preset_data.get('name', preset_id), + 'category': preset_data.get('category', 'unknown'), + 'description': preset_data.get('description', ''), + 'timestamp': preset_data.get('timestamp', 0), + 'is_default': preset_data.get('is_default', False), + 'parameters_count': len(preset_data.get('parameters', {})) + } + + except Exception as e: + print(f"✗ 预设信息获取失败: {e}") + import traceback + traceback.print_exc() + return None + + def get_preset_categories(self) -> Dict[str, Dict[str, str]]: + """ + 获取预设类别 + + Returns: + 预设类别字典 + """ + return self.preset_categories.copy() + + def create_preset_category(self, category_id: str, name: str, description: str = '') -> bool: + """ + 创建预设类别 + + Args: + category_id: 类别ID + name: 类别名称 + description: 类别描述 + + Returns: + 是否创建成功 + """ + try: + self.preset_categories[category_id] = { + 'name': name, + 'description': description + } + + print(f"✓ 预设类别 '{name}' 创建完成") + return True + + except Exception as e: + print(f"✗ 预设类别创建失败: {e}") + import traceback + traceback.print_exc() + return False + + def export_preset(self, preset_id: str, filepath: str) -> bool: + """ + 导出预设到文件 + + Args: + preset_id: 预设ID + filepath: 导出文件路径 + + Returns: + 是否导出成功 + """ + try: + if preset_id not in self.presets: + print(f"✗ 预设不存在: {preset_id}") + return False + + preset_data = self.presets[preset_id] + + # 保存到指定文件 + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(preset_data, f, ensure_ascii=False, indent=2) + + print(f"✓ 预设 '{preset_id}' 导出到 {filepath}") + return True + + except Exception as e: + print(f"✗ 预设导出失败: {e}") + import traceback + traceback.print_exc() + return False + + def import_preset(self, filepath: str, preset_id: str = None) -> bool: + """ + 从文件导入预设 + + Args: + filepath: 导入文件路径 + preset_id: 预设ID(如果为None,则使用文件名) + + Returns: + 是否导入成功 + """ + try: + # 读取预设文件 + with open(filepath, 'r', encoding='utf-8') as f: + preset_data = json.load(f) + + # 确定预设ID + if preset_id is None: + preset_id = os.path.splitext(os.path.basename(filepath))[0] + + # 保存预设 + self.presets[preset_id] = preset_data + + # 保存到预设目录 + presets_dir = self.preset_config['presets_directory'] + save_filepath = os.path.join(presets_dir, f"{preset_id}.json") + + with open(save_filepath, 'w', encoding='utf-8') as f: + json.dump(preset_data, f, ensure_ascii=False, indent=2) + + print(f"✓ 预设从 {filepath} 导入完成,ID: {preset_id}") + return True + + except Exception as e: + print(f"✗ 预设导入失败: {e}") + import traceback + traceback.print_exc() + return False + + def set_preset_config(self, config: Dict[str, Any]): + """ + 设置预设配置 + + Args: + config: 配置字典 + """ + self.preset_config.update(config) + print(f"✓ 预设配置已更新: {self.preset_config}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'presets_loaded': 0, + 'presets_saved': 0, + 'presets_applied': 0, + 'total_load_time': 0.0, + 'total_save_time': 0.0 + } + print("✓ 预设管理器统计信息已重置") + + def get_current_preset(self) -> Optional[str]: + """ + 获取当前预设 + + Returns: + 当前预设ID或None + """ + return self.current_preset + + def set_current_preset(self, preset_id: str): + """ + 设置当前预设 + + Args: + preset_id: 预设ID + """ + if preset_id in self.presets: + self.current_preset = preset_id + print(f"✓ 当前预设设置为: {preset_id}") + else: + print(f"✗ 预设不存在: {preset_id}") + + def create_terrain_preset_from_current(self, preset_name: str, description: str = '') -> str: + """ + 从当前配置创建预设 + + Args: + preset_name: 预设名称 + description: 预设描述 + + Returns: + 预设ID + """ + try: + # 生成预设ID + preset_id = preset_name.lower().replace(' ', '_') + + # 创建预设数据 + preset_data = { + 'name': preset_name, + 'category': 'mixed', + 'description': description, + 'parameters': {}, + 'timestamp': time.time(), + 'is_default': False + } + + # 获取当前配置参数 + if self.plugin.vegetation_manager: + vm = self.plugin.vegetation_manager + preset_data['parameters'].update({ + 'environment_factors': vm.get_environment_factors(), + 'distribution_config': vm.get_distribution_config() + }) + + if self.plugin.ecosystem_manager: + em = self.plugin.ecosystem_manager + preset_data['parameters'].update({ + 'ecosystem_parameters': em.get_ecosystem_parameters() + }) + + # 保存预设 + self.save_preset(preset_id, preset_data) + + print(f"✓ 从当前配置创建预设: {preset_name}") + return preset_id + + except Exception as e: + print(f"✗ 从当前配置创建预设失败: {e}") + import traceback + traceback.print_exc() + return '' + + def batch_apply_presets(self, preset_ids: List[str]) -> bool: + """ + 批量应用预设 + + Args: + preset_ids: 预设ID列表 + + Returns: + 是否应用成功 + """ + try: + if not self.enabled: + print("✗ 预设管理器未启用") + return False + + success_count = 0 + for preset_id in preset_ids: + if self.apply_preset(preset_id): + success_count += 1 + + print(f"✓ 批量应用预设完成: {success_count}/{len(preset_ids)} 个预设应用成功") + return success_count == len(preset_ids) + + except Exception as e: + print(f"✗ 批量应用预设失败: {e}") + import traceback + traceback.print_exc() + return False + + def search_presets(self, keyword: str, category: str = None) -> List[str]: + """ + 搜索预设 + + Args: + keyword: 搜索关键词 + category: 预设类别(可选) + + Returns: + 匹配的预设ID列表 + """ + try: + matching_presets = [] + + for preset_id, preset_data in self.presets.items(): + # 检查类别筛选 + if category and preset_data.get('category') != category: + continue + + # 检查关键词匹配 + if (keyword.lower() in preset_data.get('name', '').lower() or + keyword.lower() in preset_data.get('description', '').lower()): + matching_presets.append(preset_id) + + return matching_presets + + except Exception as e: + print(f"✗ 预设搜索失败: {e}") + import traceback + traceback.print_exc() + return [] + + def duplicate_preset(self, source_preset_id: str, new_preset_id: str, new_name: str = None) -> bool: + """ + 复制预设 + + Args: + source_preset_id: 源预设ID + new_preset_id: 新预设ID + new_name: 新预设名称(如果为None,则使用源预设名称) + + Returns: + 是否复制成功 + """ + try: + if source_preset_id not in self.presets: + print(f"✗ 源预设不存在: {source_preset_id}") + return False + + # 复制预设数据 + preset_data = self.presets[source_preset_id].copy() + + # 更新名称 + if new_name: + preset_data['name'] = new_name + else: + preset_data['name'] = f"{preset_data['name']} (副本)" + + # 更新时间戳 + preset_data['timestamp'] = time.time() + + # 标记为非默认预设 + preset_data['is_default'] = False + + # 保存新预设 + return self.save_preset(new_preset_id, preset_data) + + except Exception as e: + print(f"✗ 预设复制失败: {e}") + import traceback + traceback.print_exc() + return False + + def update_preset_parameters(self, preset_id: str, parameters: Dict[str, Any]) -> bool: + """ + 更新预设参数 + + Args: + preset_id: 预设ID + parameters: 要更新的参数 + + Returns: + 是否更新成功 + """ + try: + if preset_id not in self.presets: + print(f"✗ 预设不存在: {preset_id}") + return False + + # 更新参数 + self.presets[preset_id]['parameters'].update(parameters) + + # 更新时间戳 + self.presets[preset_id]['timestamp'] = time.time() + + # 保存到文件 + presets_dir = self.preset_config['presets_directory'] + filepath = os.path.join(presets_dir, f"{preset_id}.json") + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(self.presets[preset_id], f, ensure_ascii=False, indent=2) + + print(f"✓ 预设 '{preset_id}' 参数更新完成") + return True + + except Exception as e: + print(f"✗ 预设参数更新失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_preset_parameters(self, preset_id: str) -> Optional[Dict[str, Any]]: + """ + 获取预设参数 + + Args: + preset_id: 预设ID + + Returns: + 参数字典或None + """ + try: + if preset_id not in self.presets: + return None + + return self.presets[preset_id].get('parameters', {}).copy() + + except Exception as e: + print(f"✗ 预设参数获取失败: {e}") + import traceback + traceback.print_exc() + return None + + def validate_preset(self, preset_id: str) -> bool: + """ + 验证预设完整性 + + Args: + preset_id: 预设ID + + Returns: + 预设是否有效 + """ + try: + if preset_id not in self.presets: + return False + + preset_data = self.presets[preset_id] + + # 检查必需字段 + required_fields = ['name', 'category', 'parameters'] + for field in required_fields: + if field not in preset_data: + print(f"✗ 预设 '{preset_id}' 缺少必需字段: {field}") + return False + + # 检查类别有效性 + if preset_data['category'] not in self.preset_categories: + print(f"✗ 预设 '{preset_id}' 包含无效类别: {preset_data['category']}") + return False + + return True + + except Exception as e: + print(f"✗ 预设验证失败: {e}") + import traceback + traceback.print_exc() + return False + + def backup_presets(self, backup_dir: str = 'presets_backup') -> bool: + """ + 备份所有预设 + + Args: + backup_dir: 备份目录 + + Returns: + 是否备份成功 + """ + try: + if not os.path.exists(backup_dir): + os.makedirs(backup_dir) + + backup_count = 0 + for preset_id, preset_data in self.presets.items(): + try: + filepath = os.path.join(backup_dir, f"{preset_id}.json") + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(preset_data, f, ensure_ascii=False, indent=2) + backup_count += 1 + except Exception as e: + print(f"✗ 预设 '{preset_id}' 备份失败: {e}") + + print(f"✓ 预设备份完成: {backup_count}/{len(self.presets)} 个预设已备份到 {backup_dir}") + return True + + except Exception as e: + print(f"✗ 预设备份失败: {e}") + import traceback + traceback.print_exc() + return False + + def restore_presets(self, backup_dir: str = 'presets_backup') -> bool: + """ + 从备份恢复预设 + + Args: + backup_dir: 备份目录 + + Returns: + 是否恢复成功 + """ + try: + if not os.path.exists(backup_dir): + print(f"✗ 备份目录不存在: {backup_dir}") + return False + + restore_count = 0 + for filename in os.listdir(backup_dir): + if filename.endswith('.json'): + try: + filepath = os.path.join(backup_dir, filename) + with open(filepath, 'r', encoding='utf-8') as f: + preset_data = json.load(f) + + preset_id = os.path.splitext(filename)[0] + self.presets[preset_id] = preset_data + restore_count += 1 + except Exception as e: + print(f"✗ 预设文件 '{filename}' 恢复失败: {e}") + + print(f"✓ 预设恢复完成: {restore_count} 个预设已从 {backup_dir} 恢复") + return True + + except Exception as e: + print(f"✗ 预设恢复失败: {e}") + import traceback + traceback.print_exc() return False \ No newline at end of file diff --git a/plugins/user/vegetation_ecosystem/plugin.py b/plugins/user/vegetation_ecosystem/plugin.py index d7d62059..3296f30f 100644 --- a/plugins/user/vegetation_ecosystem/plugin.py +++ b/plugins/user/vegetation_ecosystem/plugin.py @@ -1,303 +1,303 @@ -""" -植被分布和生态模拟插件 -为EG引擎提供完整的植被分布和生态系统模拟功能 -""" - -from .core.vegetation_manager import VegetationManager -from .core.ecosystem_manager import EcosystemManager -from .ecosystem.animal_simulator import AnimalSimulator -from .ecosystem.ecosystem_dynamics import EcosystemDynamics -from .ecosystem.eco_interactions import EcoInteractions -from .vegetation.vegetation_spawner import VegetationSpawner -from .vegetation.growth_simulator import GrowthSimulator -from .terrain.terrain_influence import TerrainInfluence -from .seasons.seasonal_effects import SeasonalEffects -from .visualization.eco_visualizer import EcoVisualizer -from .visualization.advanced_visuals import AdvancedVisuals -from .editor.eco_editor import EcoEditor -from .editor.preset_manager import PresetManager -from .utils.eco_utils import EcoUtils - -class VegetationEcosystemPlugin: - """ - 植被分布和生态模拟插件主类 - 整合所有植被和生态系统功能模块 - """ - - def __init__(self, engine): - """ - 初始化植被分布和生态模拟插件 - - Args: - engine: EG引擎实例 - """ - self.engine = engine - self.name = "VegetationEcosystem" - self.version = "1.0.0" - self.author = "EG Plugin System" - - # 功能模块 - self.vegetation_manager = None - self.ecosystem_manager = None - self.animal_simulator = None - self.ecosystem_dynamics = None - self.ecosystem_interactions = None - self.vegetation_spawner = None - self.growth_simulator = None - self.terrain_influence = None - self.seasonal_effects = None - self.eco_visualizer = None - self.advanced_visuals = None - self.eco_editor = None - self.preset_manager = None - self.eco_utils = None - - # 插件状态 - self.enabled = False - self.initialized = False - - # 性能统计 - self.stats = { - 'vegetation_spawned': 0, - 'ecosystem_updates': 0, - 'animals_simulated': 0, - 'growth_cycles': 0 - } - - print(f"✓ 植被分布和生态模拟插件 v{self.version} 已创建") - - def initialize(self) -> bool: - """ - 初始化插件 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化植被分布和生态模拟插件...") - - # 初始化核心组件 - self.vegetation_manager = VegetationManager(self) - self.ecosystem_manager = EcosystemManager(self) - self.animal_simulator = AnimalSimulator(self) - self.ecosystem_dynamics = EcosystemDynamics(self) - self.ecosystem_interactions = EcoInteractions(self) - self.vegetation_spawner = VegetationSpawner(self) - self.growth_simulator = GrowthSimulator(self) - self.terrain_influence = TerrainInfluence(self) - self.seasonal_effects = SeasonalEffects(self) - self.eco_visualizer = EcoVisualizer(self) - self.advanced_visuals = AdvancedVisuals(self) - self.eco_editor = EcoEditor(self) - self.preset_manager = PresetManager(self) - self.eco_utils = EcoUtils(self) - - # 初始化各个模块 - modules = [ - self.vegetation_manager, - self.ecosystem_manager, - self.animal_simulator, - self.ecosystem_dynamics, - self.ecosystem_interactions, - self.vegetation_spawner, - self.growth_simulator, - self.terrain_influence, - self.seasonal_effects, - self.eco_visualizer, - self.advanced_visuals, - self.eco_editor, - self.preset_manager, - self.eco_utils - ] - - for module in modules: - if module and not module.initialize(): - print(f"✗ 模块初始化失败: {module.__class__.__name__}") - return False - - self.initialized = True - print("✓ 植被分布和生态模拟插件初始化完成") - return True - - except Exception as e: - print(f"✗ 植被分布和生态模拟插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 插件未初始化") - return False - - print("正在启用植被分布和生态模拟插件...") - - # 启用各个模块 - modules = [ - self.vegetation_manager, - self.ecosystem_manager, - self.animal_simulator, - self.ecosystem_dynamics, - self.ecosystem_interactions, - self.vegetation_spawner, - self.growth_simulator, - self.terrain_influence, - self.seasonal_effects, - self.eco_visualizer, - self.advanced_visuals, - self.eco_editor, - self.preset_manager, - self.eco_utils - ] - - for module in modules: - if module and not module.enable(): - print(f"✗ 模块启用失败: {module.__class__.__name__}") - return False - - self.enabled = True - print("✓ 植被分布和生态模拟插件已启用") - return True - - except Exception as e: - print(f"✗ 植被分布和生态模拟插件启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用插件""" - try: - if not self.enabled: - return - - print("正在禁用植被分布和生态模拟插件...") - - # 禁用各个模块 - modules = [ - self.vegetation_manager, - self.ecosystem_manager, - self.animal_simulator, - self.ecosystem_dynamics, - self.ecosystem_interactions, - self.vegetation_spawner, - self.growth_simulator, - self.terrain_influence, - self.seasonal_effects, - self.eco_visualizer, - self.advanced_visuals, - self.eco_editor, - self.preset_manager, - self.eco_utils - ] - - for module in modules: - if module: - module.disable() - - self.enabled = False - print("✓ 植被分布和生态模拟插件已禁用") - - except Exception as e: - print(f"✗ 植被分布和生态模拟插件禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理插件资源""" - try: - print("正在清理植被分布和生态模拟插件资源...") - - # 清理各个模块 - modules = [ - self.vegetation_manager, - self.ecosystem_manager, - self.animal_simulator, - self.ecosystem_dynamics, - self.ecosystem_interactions, - self.vegetation_spawner, - self.growth_simulator, - self.terrain_influence, - self.seasonal_effects, - self.eco_visualizer, - self.advanced_visuals, - self.eco_editor, - self.preset_manager, - self.eco_utils - ] - - for module in modules: - if module: - module.finalize() - - self.initialized = False - print("✓ 植被分布和生态模拟插件资源已清理") - - except Exception as e: - print(f"✗ 植被分布和生态模拟插件资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新插件状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - # 更新各个模块 - modules = [ - self.vegetation_manager, - self.ecosystem_manager, - self.animal_simulator, - self.ecosystem_dynamics, - self.ecosystem_interactions, - self.vegetation_spawner, - self.growth_simulator, - self.terrain_influence, - self.seasonal_effects, - self.eco_visualizer, - self.advanced_visuals, - self.eco_editor, - self.preset_manager, - self.eco_utils - ] - - for module in modules: - if module: - module.update(dt) - - except Exception as e: - print(f"✗ 植被分布和生态模拟插件更新失败: {e}") - import traceback - traceback.print_exc() - - def get_stats(self) -> dict: - """ - 获取插件统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'vegetation_spawned': 0, - 'ecosystem_updates': 0, - 'animals_simulated': 0, - 'growth_cycles': 0 - } +""" +植被分布和生态模拟插件 +为EG引擎提供完整的植被分布和生态系统模拟功能 +""" + +from .core.vegetation_manager import VegetationManager +from .core.ecosystem_manager import EcosystemManager +from .ecosystem.animal_simulator import AnimalSimulator +from .ecosystem.ecosystem_dynamics import EcosystemDynamics +from .ecosystem.eco_interactions import EcoInteractions +from .vegetation.vegetation_spawner import VegetationSpawner +from .vegetation.growth_simulator import GrowthSimulator +from .terrain.terrain_influence import TerrainInfluence +from .seasons.seasonal_effects import SeasonalEffects +from .visualization.eco_visualizer import EcoVisualizer +from .visualization.advanced_visuals import AdvancedVisuals +from .editor.eco_editor import EcoEditor +from .editor.preset_manager import PresetManager +from .utils.eco_utils import EcoUtils + +class VegetationEcosystemPlugin: + """ + 植被分布和生态模拟插件主类 + 整合所有植被和生态系统功能模块 + """ + + def __init__(self, engine): + """ + 初始化植被分布和生态模拟插件 + + Args: + engine: EG引擎实例 + """ + self.engine = engine + self.name = "VegetationEcosystem" + self.version = "1.0.0" + self.author = "EG Plugin System" + + # 功能模块 + self.vegetation_manager = None + self.ecosystem_manager = None + self.animal_simulator = None + self.ecosystem_dynamics = None + self.ecosystem_interactions = None + self.vegetation_spawner = None + self.growth_simulator = None + self.terrain_influence = None + self.seasonal_effects = None + self.eco_visualizer = None + self.advanced_visuals = None + self.eco_editor = None + self.preset_manager = None + self.eco_utils = None + + # 插件状态 + self.enabled = False + self.initialized = False + + # 性能统计 + self.stats = { + 'vegetation_spawned': 0, + 'ecosystem_updates': 0, + 'animals_simulated': 0, + 'growth_cycles': 0 + } + + print(f"✓ 植被分布和生态模拟插件 v{self.version} 已创建") + + def initialize(self) -> bool: + """ + 初始化插件 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化植被分布和生态模拟插件...") + + # 初始化核心组件 + self.vegetation_manager = VegetationManager(self) + self.ecosystem_manager = EcosystemManager(self) + self.animal_simulator = AnimalSimulator(self) + self.ecosystem_dynamics = EcosystemDynamics(self) + self.ecosystem_interactions = EcoInteractions(self) + self.vegetation_spawner = VegetationSpawner(self) + self.growth_simulator = GrowthSimulator(self) + self.terrain_influence = TerrainInfluence(self) + self.seasonal_effects = SeasonalEffects(self) + self.eco_visualizer = EcoVisualizer(self) + self.advanced_visuals = AdvancedVisuals(self) + self.eco_editor = EcoEditor(self) + self.preset_manager = PresetManager(self) + self.eco_utils = EcoUtils(self) + + # 初始化各个模块 + modules = [ + self.vegetation_manager, + self.ecosystem_manager, + self.animal_simulator, + self.ecosystem_dynamics, + self.ecosystem_interactions, + self.vegetation_spawner, + self.growth_simulator, + self.terrain_influence, + self.seasonal_effects, + self.eco_visualizer, + self.advanced_visuals, + self.eco_editor, + self.preset_manager, + self.eco_utils + ] + + for module in modules: + if module and not module.initialize(): + print(f"✗ 模块初始化失败: {module.__class__.__name__}") + return False + + self.initialized = True + print("✓ 植被分布和生态模拟插件初始化完成") + return True + + except Exception as e: + print(f"✗ 植被分布和生态模拟插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 插件未初始化") + return False + + print("正在启用植被分布和生态模拟插件...") + + # 启用各个模块 + modules = [ + self.vegetation_manager, + self.ecosystem_manager, + self.animal_simulator, + self.ecosystem_dynamics, + self.ecosystem_interactions, + self.vegetation_spawner, + self.growth_simulator, + self.terrain_influence, + self.seasonal_effects, + self.eco_visualizer, + self.advanced_visuals, + self.eco_editor, + self.preset_manager, + self.eco_utils + ] + + for module in modules: + if module and not module.enable(): + print(f"✗ 模块启用失败: {module.__class__.__name__}") + return False + + self.enabled = True + print("✓ 植被分布和生态模拟插件已启用") + return True + + except Exception as e: + print(f"✗ 植被分布和生态模拟插件启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用插件""" + try: + if not self.enabled: + return + + print("正在禁用植被分布和生态模拟插件...") + + # 禁用各个模块 + modules = [ + self.vegetation_manager, + self.ecosystem_manager, + self.animal_simulator, + self.ecosystem_dynamics, + self.ecosystem_interactions, + self.vegetation_spawner, + self.growth_simulator, + self.terrain_influence, + self.seasonal_effects, + self.eco_visualizer, + self.advanced_visuals, + self.eco_editor, + self.preset_manager, + self.eco_utils + ] + + for module in modules: + if module: + module.disable() + + self.enabled = False + print("✓ 植被分布和生态模拟插件已禁用") + + except Exception as e: + print(f"✗ 植被分布和生态模拟插件禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理插件资源""" + try: + print("正在清理植被分布和生态模拟插件资源...") + + # 清理各个模块 + modules = [ + self.vegetation_manager, + self.ecosystem_manager, + self.animal_simulator, + self.ecosystem_dynamics, + self.ecosystem_interactions, + self.vegetation_spawner, + self.growth_simulator, + self.terrain_influence, + self.seasonal_effects, + self.eco_visualizer, + self.advanced_visuals, + self.eco_editor, + self.preset_manager, + self.eco_utils + ] + + for module in modules: + if module: + module.finalize() + + self.initialized = False + print("✓ 植被分布和生态模拟插件资源已清理") + + except Exception as e: + print(f"✗ 植被分布和生态模拟插件资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新插件状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + # 更新各个模块 + modules = [ + self.vegetation_manager, + self.ecosystem_manager, + self.animal_simulator, + self.ecosystem_dynamics, + self.ecosystem_interactions, + self.vegetation_spawner, + self.growth_simulator, + self.terrain_influence, + self.seasonal_effects, + self.eco_visualizer, + self.advanced_visuals, + self.eco_editor, + self.preset_manager, + self.eco_utils + ] + + for module in modules: + if module: + module.update(dt) + + except Exception as e: + print(f"✗ 植被分布和生态模拟插件更新失败: {e}") + import traceback + traceback.print_exc() + + def get_stats(self) -> dict: + """ + 获取插件统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'vegetation_spawned': 0, + 'ecosystem_updates': 0, + 'animals_simulated': 0, + 'growth_cycles': 0 + } print("✓ 植被分布和生态模拟统计信息已重置") \ No newline at end of file diff --git a/plugins/user/vegetation_ecosystem/seasons/seasonal_effects.py b/plugins/user/vegetation_ecosystem/seasons/seasonal_effects.py index 359761e3..aa00c275 100644 --- a/plugins/user/vegetation_ecosystem/seasons/seasonal_effects.py +++ b/plugins/user/vegetation_ecosystem/seasons/seasonal_effects.py @@ -1,772 +1,772 @@ -""" -季节效应系统 -负责模拟季节变化对植被和生态系统的影响 -""" - -import time -from typing import Dict, Any, List, Optional -import math -import random - -class SeasonalEffects: - """ - 季节效应系统 - 负责模拟季节变化对植被生长、动物行为和生态系统动态的影响 - """ - - def __init__(self, plugin): - """ - 初始化季节效应系统 - - Args: - plugin: 植被和生态系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 季节定义 - self.seasons = { - 'spring': { - 'name': '春季', - 'description': '万物复苏的季节', - 'temperature_modifier': 10.0, # 温度修正 - 'humidity_modifier': 15.0, # 湿度修正 - 'light_duration': 13.0, # 光照时长(小时) - 'growth_factor': 1.5, # 生长因子 - 'reproduction_factor': 2.0, # 繁殖因子 - 'color_tint': (1.0, 1.0, 0.8), # 颜色色调 - 'duration': 90 # 持续天数 - }, - 'summer': { - 'name': '夏季', - 'description': '炎热的季节', - 'temperature_modifier': 20.0, - 'humidity_modifier': 10.0, - 'light_duration': 15.0, - 'growth_factor': 1.2, - 'reproduction_factor': 1.5, - 'color_tint': (1.0, 0.9, 0.7), - 'duration': 90 - }, - 'autumn': { - 'name': '秋季', - 'description': '收获的季节', - 'temperature_modifier': 5.0, - 'humidity_modifier': 5.0, - 'light_duration': 11.0, - 'growth_factor': 0.5, - 'reproduction_factor': 0.8, - 'color_tint': (1.0, 0.7, 0.5), - 'duration': 90 - }, - 'winter': { - 'name': '冬季', - 'description': '寒冷的季节', - 'temperature_modifier': -10.0, - 'humidity_modifier': 0.0, - 'light_duration': 9.0, - 'growth_factor': 0.1, - 'reproduction_factor': 0.2, - 'color_tint': (0.8, 0.8, 1.0), - 'duration': 90 - } - } - - # 当前季节状态 - self.current_season = 'spring' - self.season_progress = 0.0 # 季节进度 (0.0-1.0) - self.season_day = 0 # 当前季节中的天数 - self.total_days = 0 # 总天数 - - # 季节过渡参数 - self.transition_params = { - 'duration': 7.0, # 过渡持续时间(天) - 'transition_active': False, - 'transition_progress': 0.0, - 'target_season': 'spring' - } - - # 季节性事件 - self.seasonal_events = { - 'spring': ['花开', '鸟类迁徙回归', '动物繁殖季开始'], - 'summer': ['高温', '干旱', '虫害高发'], - 'autumn': ['叶色变化', '果实成熟', '动物准备过冬'], - 'winter': ['降雪', '动物冬眠', '植物休眠'] - } - - # 季节性动物行为 - self.animal_behaviors = { - 'migration': { - 'spring': 0.8, # 春季迁徙概率 - 'summer': 0.1, - 'autumn': 0.9, - 'winter': 0.2 - }, - 'hibernation': { - 'spring': 0.1, - 'summer': 0.05, - 'autumn': 0.3, - 'winter': 0.9 - }, - 'breeding': { - 'spring': 0.9, - 'summer': 0.7, - 'autumn': 0.3, - 'winter': 0.1 - } - } - - # 季节性植被变化 - self.vegetation_changes = { - 'leaf_color': { - 'spring': (0.3, 0.7, 0.3), # 绿色 - 'summer': (0.2, 0.8, 0.2), - 'autumn': (0.8, 0.5, 0.2), # 橙色 - 'winter': (0.7, 0.7, 0.7) # 灰色 - }, - 'flowering': { - 'spring': 0.8, - 'summer': 0.4, - 'autumn': 0.1, - 'winter': 0.0 - }, - 'fruiting': { - 'spring': 0.0, - 'summer': 0.2, - 'autumn': 0.9, - 'winter': 0.0 - } - } - - # 时间配置 - self.time_config = { - 'day_length': 24.0, # 一天长度(小时) - 'simulation_speed': 1.0, # 模拟速度 - 'current_time': 0.0 # 当前时间 - } - - # 统计信息 - self.season_stats = { - 'seasons_passed': 0, - 'transitions_completed': 0, - 'events_triggered': 0, - 'total_simulation_time': 0.0 - } - - print("✓ 季节效应系统已创建") - - def initialize(self) -> bool: - """ - 初始化季节效应系统 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 季节效应系统初始化完成") - return True - - except Exception as e: - print(f"✗ 季节效应系统初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用季节效应系统 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 季节效应系统未初始化") - return False - - self.enabled = True - print("✓ 季节效应系统已启用") - return True - - except Exception as e: - print(f"✗ 季节效应系统启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用季节效应系统""" - try: - self.enabled = False - print("✓ 季节效应系统已禁用") - - except Exception as e: - print(f"✗ 季节效应系统禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理季节效应系统资源""" - try: - self.disable() - self.initialized = False - print("✓ 季节效应系统资源已清理") - - except Exception as e: - print(f"✗ 季节效应系统资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新季节效应系统状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新时间 - self.time_config['current_time'] += dt * self.time_config['simulation_speed'] - - # 更新季节进度 - self._update_season_progress(dt) - - # 更新季节过渡 - if self.transition_params['transition_active']: - self._update_season_transition(dt) - - # 更新统计信息 - self.season_stats['total_simulation_time'] += dt - - except Exception as e: - print(f"✗ 季节效应系统更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_season_progress(self, dt: float): - """ - 更新季节进度 - - Args: - dt: 时间增量 - """ - try: - # 将时间增量转换为小时 - hours_passed = dt / 3600 * self.time_config['simulation_speed'] - - # 更新当前季节中的天数 - self.season_day += hours_passed / 24.0 - self.total_days += hours_passed / 24.0 - - # 更新季节进度 - current_season_info = self.seasons[self.current_season] - self.season_progress = min(1.0, self.season_day / current_season_info['duration']) - - # 检查是否需要切换季节 - if self.season_day >= current_season_info['duration']: - self._transition_to_next_season() - - except Exception as e: - print(f"✗ 季节进度更新失败: {e}") - - def _transition_to_next_season(self): - """过渡到下一个季节""" - try: - # 季节顺序 - season_order = ['spring', 'summer', 'autumn', 'winter'] - - # 找到当前季节在顺序中的位置 - current_index = season_order.index(self.current_season) - - # 计算下一个季节 - next_index = (current_index + 1) % len(season_order) - next_season = season_order[next_index] - - # 开始季节过渡 - self.start_season_transition(next_season) - - except Exception as e: - print(f"✗ 季节切换失败: {e}") - - def start_season_transition(self, target_season: str): - """ - 开始季节过渡 - - Args: - target_season: 目标季节 - """ - try: - if target_season not in self.seasons: - print(f"✗ 无效的目标季节: {target_season}") - return - - if target_season == self.current_season: - return # 季节未变化 - - self.transition_params['target_season'] = target_season - self.transition_params['transition_active'] = True - self.transition_params['transition_progress'] = 0.0 - - print(f"✓ 开始季节过渡: {self.seasons[self.current_season]['name']} -> {self.seasons[target_season]['name']}") - - except Exception as e: - print(f"✗ 季节过渡启动失败: {e}") - - def _update_season_transition(self, dt: float): - """ - 更新季节过渡 - - Args: - dt: 时间增量 - """ - try: - if not self.transition_params['transition_active']: - return - - # 计算过渡进度增量 - transition_duration = self.transition_params['duration'] - if transition_duration > 0: - progress_delta = dt / (transition_duration * 86400) * self.time_config['simulation_speed'] - self.transition_params['transition_progress'] += progress_delta - - # 检查过渡是否完成 - if self.transition_params['transition_progress'] >= 1.0: - self._complete_season_transition() - - except Exception as e: - print(f"✗ 季节过渡更新失败: {e}") - - def _complete_season_transition(self): - """完成季节过渡""" - try: - # 更新当前季节 - self.current_season = self.transition_params['target_season'] - - # 重置季节内天数 - self.season_day = 0 - self.season_progress = 0.0 - - # 重置过渡状态 - self.transition_params['transition_active'] = False - self.transition_params['transition_progress'] = 0.0 - - # 更新统计信息 - self.season_stats['seasons_passed'] += 1 - self.season_stats['transitions_completed'] += 1 - - print(f"✓ 季节过渡完成: 现在是{self.seasons[self.current_season]['name']}") - - # 触发季节事件 - self._trigger_seasonal_events() - - # 通知其他系统季节已变化 - self._notify_systems_of_season_change() - - except Exception as e: - print(f"✗ 季节过渡完成失败: {e}") - - def _trigger_seasonal_events(self): - """触发季节性事件""" - try: - events = self.seasonal_events.get(self.current_season, []) - if events: - # 随机触发一个事件 - event = random.choice(events) - print(f"✓ 季节性事件触发: {event}") - self.season_stats['events_triggered'] += 1 - - except Exception as e: - print(f"✗ 季节性事件触发失败: {e}") - - def _notify_systems_of_season_change(self): - """通知其他系统季节已变化""" - try: - # 通知植被管理器 - if self.plugin.vegetation_manager: - # 更新环境因子 - current_season_info = self.seasons[self.current_season] - environment_factors = { - 'temperature': 20.0 + current_season_info['temperature_modifier'], - 'humidity': 50.0 + current_season_info['humidity_modifier'] - } - self.plugin.vegetation_manager.update_environment_factors(environment_factors) - - except Exception as e: - print(f"✗ 系统通知失败: {e}") - - def get_current_season(self) -> str: - """ - 获取当前季节 - - Returns: - 当前季节名称 - """ - return self.current_season - - def get_season_info(self, season: str = None) -> Optional[Dict[str, Any]]: - """ - 获取季节信息 - - Args: - season: 季节名称(如果为None,则返回当前季节信息) - - Returns: - 季节信息字典或None - """ - try: - if season is None: - season = self.current_season - - if season in self.seasons: - return self.seasons[season].copy() - return None - - except Exception as e: - print(f"✗ 季节信息获取失败: {e}") - return None - - def get_season_progress(self) -> float: - """ - 获取季节进度 - - Returns: - 季节进度 (0.0-1.0) - """ - return self.season_progress - - def get_season_day(self) -> int: - """ - 获取当前季节中的天数 - - Returns: - 当前季节中的天数 - """ - return int(self.season_day) - - def set_current_season(self, season: str): - """ - 设置当前季节 - - Args: - season: 季节名称 - """ - try: - if season in self.seasons: - self.current_season = season - self.season_day = 0 - self.season_progress = 0.0 - print(f"✓ 当前季节设置为: {self.seasons[season]['name']}") - else: - print(f"✗ 无效的季节: {season}") - - except Exception as e: - print(f"✗ 当前季节设置失败: {e}") - - def set_season_duration(self, season: str, duration: int): - """ - 设置季节持续时间 - - Args: - season: 季节名称 - duration: 持续时间(天) - """ - try: - if season in self.seasons: - self.seasons[season]['duration'] = max(1, duration) - print(f"✓ {self.seasons[season]['name']} 持续时间设置为: {duration} 天") - else: - print(f"✗ 无效的季节: {season}") - - except Exception as e: - print(f"✗ 季节持续时间设置失败: {e}") - - def get_interpolated_season_params(self) -> Dict[str, Any]: - """ - 获取插值后的季节参数(考虑季节过渡) - - Returns: - 插值后的季节参数字典 - """ - try: - if not self.transition_params['transition_active'] or self.transition_params['transition_progress'] <= 0.0: - # 没有过渡,返回当前季节参数 - return self.seasons[self.current_season].copy() - - if self.transition_params['transition_progress'] >= 1.0: - # 过渡完成,返回目标季节参数 - return self.seasons[self.transition_params['target_season']].copy() - - # 进行插值计算 - current_params = self.seasons[self.current_season] - target_params = self.seasons[self.transition_params['target_season']] - - interpolated_params = {} - - # 对数值参数进行线性插值 - numeric_params = [ - 'temperature_modifier', 'humidity_modifier', 'light_duration', - 'growth_factor', 'reproduction_factor' - ] - - for param in numeric_params: - current_value = current_params[param] - target_value = target_params[param] - interpolated_value = current_value + (target_value - current_value) * self.transition_params['transition_progress'] - interpolated_params[param] = interpolated_value - - # 对颜色进行插值 - current_color = current_params['color_tint'] - target_color = target_params['color_tint'] - interpolated_color = tuple( - current_color[i] + (target_color[i] - current_color[i]) * self.transition_params['transition_progress'] - for i in range(3) - ) - interpolated_params['color_tint'] = interpolated_color - - # 复制非数值参数 - interpolated_params['name'] = f"{current_params['name']} → {target_params['name']}" - interpolated_params['description'] = f"过渡季节 ({self.transition_params['transition_progress']*100:.1f}%)" - interpolated_params['duration'] = target_params['duration'] - - return interpolated_params - - except Exception as e: - print(f"✗ 插值季节参数获取失败: {e}") - return self.seasons[self.current_season].copy() - - def get_seasonal_events(self, season: str = None) -> List[str]: - """ - 获取季节性事件 - - Args: - season: 季节名称(如果为None,则返回当前季节事件) - - Returns: - 季节性事件列表 - """ - try: - if season is None: - season = self.current_season - - return self.seasonal_events.get(season, []).copy() - - except Exception as e: - print(f"✗ 季节性事件获取失败: {e}") - return [] - - def add_seasonal_event(self, season: str, event: str): - """ - 添加季节性事件 - - Args: - season: 季节名称 - event: 事件名称 - """ - try: - if season in self.seasonal_events: - if event not in self.seasonal_events[season]: - self.seasonal_events[season].append(event) - print(f"✓ 季节性事件已添加: {season} - {event}") - else: - print(f"✗ 事件已存在: {event}") - else: - print(f"✗ 无效的季节: {season}") - - except Exception as e: - print(f"✗ 季节性事件添加失败: {e}") - - def remove_seasonal_event(self, season: str, event: str): - """ - 移除季节性事件 - - Args: - season: 季节名称 - event: 事件名称 - """ - try: - if season in self.seasonal_events and event in self.seasonal_events[season]: - self.seasonal_events[season].remove(event) - print(f"✓ 季节性事件已移除: {season} - {event}") - else: - print(f"✗ 事件不存在: {season} - {event}") - - except Exception as e: - print(f"✗ 季节性事件移除失败: {e}") - - def get_animal_behavior_factor(self, behavior: str) -> float: - """ - 获取动物行为因子 - - Args: - behavior: 行为类型 - - Returns: - 行为因子 (0.0-1.0) - """ - try: - if behavior in self.animal_behaviors and self.current_season in self.animal_behaviors[behavior]: - return self.animal_behaviors[behavior][self.current_season] - return 0.5 - - except Exception as e: - print(f"✗ 动物行为因子获取失败: {e}") - return 0.5 - - def get_vegetation_change_factor(self, change_type: str) -> float: - """ - 获取植被变化因子 - - Args: - change_type: 变化类型 - - Returns: - 变化因子 (0.0-1.0) - """ - try: - if change_type in self.vegetation_changes and self.current_season in self.vegetation_changes[change_type]: - if change_type == 'leaf_color': - # 颜色返回RGB元组 - return self.vegetation_changes[change_type][self.current_season] - else: - # 其他因子返回数值 - return self.vegetation_changes[change_type][self.current_season] - return 0.5 if change_type != 'leaf_color' else (0.5, 0.5, 0.5) - - except Exception as e: - print(f"✗ 植被变化因子获取失败: {e}") - return 0.5 if change_type != 'leaf_color' else (0.5, 0.5, 0.5) - - def set_time_config(self, config: Dict[str, float]): - """ - 设置时间配置 - - Args: - config: 时间配置字典 - """ - try: - self.time_config.update(config) - print(f"✓ 时间配置已更新: {self.time_config}") - except Exception as e: - print(f"✗ 时间配置更新失败: {e}") - - def get_time_config(self) -> Dict[str, float]: - """ - 获取时间配置 - - Returns: - 时间配置字典 - """ - return self.time_config.copy() - - def set_transition_duration(self, duration: float): - """ - 设置过渡持续时间 - - Args: - duration: 持续时间(天) - """ - try: - self.transition_params['duration'] = max(0.1, duration) - print(f"✓ 过渡持续时间设置为: {duration} 天") - except Exception as e: - print(f"✗ 过渡持续时间设置失败: {e}") - - def get_transition_progress(self) -> float: - """ - 获取季节过渡进度 - - Returns: - 过渡进度 (0.0-1.0) - """ - return self.transition_params['transition_progress'] - - def is_transition_active(self) -> bool: - """ - 检查是否正在进行季节过渡 - - Returns: - 是否正在进行季节过渡 - """ - return self.transition_params['transition_active'] - - def get_season_list(self) -> List[str]: - """ - 获取所有季节列表 - - Returns: - 季节名称列表 - """ - return list(self.seasons.keys()) - - def get_season_name(self, season: str) -> str: - """ - 获取季节显示名称 - - Args: - season: 季节名称 - - Returns: - 季节显示名称 - """ - if season in self.seasons: - return self.seasons[season]['name'] - return season - - def get_season_stats(self) -> Dict[str, Any]: - """ - 获取季节统计信息 - - Returns: - 统计信息字典 - """ - return self.season_stats.copy() - - def reset_season_stats(self): - """重置季节统计信息""" - try: - self.season_stats = { - 'seasons_passed': 0, - 'transitions_completed': 0, - 'events_triggered': 0, - 'total_simulation_time': 0.0 - } - print("✓ 季节统计信息已重置") - except Exception as e: - print(f"✗ 季节统计信息重置失败: {e}") - - def fast_forward_to_season(self, target_season: str): - """ - 快进到目标季节 - - Args: - target_season: 目标季节 - """ - try: - if target_season not in self.seasons: - print(f"✗ 无效的目标季节: {target_season}") - return - - # 直接设置季节 - self.current_season = target_season - self.season_day = 0 - self.season_progress = 0.0 - - # 重置过渡状态 - self.transition_params['transition_active'] = False - self.transition_params['transition_progress'] = 0.0 - - print(f"✓ 快进到季节: {self.seasons[target_season]['name']}") - - # 触发季节事件 - self._trigger_seasonal_events() - - # 通知其他系统 - self._notify_systems_of_season_change() - - except Exception as e: +""" +季节效应系统 +负责模拟季节变化对植被和生态系统的影响 +""" + +import time +from typing import Dict, Any, List, Optional +import math +import random + +class SeasonalEffects: + """ + 季节效应系统 + 负责模拟季节变化对植被生长、动物行为和生态系统动态的影响 + """ + + def __init__(self, plugin): + """ + 初始化季节效应系统 + + Args: + plugin: 植被和生态系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 季节定义 + self.seasons = { + 'spring': { + 'name': '春季', + 'description': '万物复苏的季节', + 'temperature_modifier': 10.0, # 温度修正 + 'humidity_modifier': 15.0, # 湿度修正 + 'light_duration': 13.0, # 光照时长(小时) + 'growth_factor': 1.5, # 生长因子 + 'reproduction_factor': 2.0, # 繁殖因子 + 'color_tint': (1.0, 1.0, 0.8), # 颜色色调 + 'duration': 90 # 持续天数 + }, + 'summer': { + 'name': '夏季', + 'description': '炎热的季节', + 'temperature_modifier': 20.0, + 'humidity_modifier': 10.0, + 'light_duration': 15.0, + 'growth_factor': 1.2, + 'reproduction_factor': 1.5, + 'color_tint': (1.0, 0.9, 0.7), + 'duration': 90 + }, + 'autumn': { + 'name': '秋季', + 'description': '收获的季节', + 'temperature_modifier': 5.0, + 'humidity_modifier': 5.0, + 'light_duration': 11.0, + 'growth_factor': 0.5, + 'reproduction_factor': 0.8, + 'color_tint': (1.0, 0.7, 0.5), + 'duration': 90 + }, + 'winter': { + 'name': '冬季', + 'description': '寒冷的季节', + 'temperature_modifier': -10.0, + 'humidity_modifier': 0.0, + 'light_duration': 9.0, + 'growth_factor': 0.1, + 'reproduction_factor': 0.2, + 'color_tint': (0.8, 0.8, 1.0), + 'duration': 90 + } + } + + # 当前季节状态 + self.current_season = 'spring' + self.season_progress = 0.0 # 季节进度 (0.0-1.0) + self.season_day = 0 # 当前季节中的天数 + self.total_days = 0 # 总天数 + + # 季节过渡参数 + self.transition_params = { + 'duration': 7.0, # 过渡持续时间(天) + 'transition_active': False, + 'transition_progress': 0.0, + 'target_season': 'spring' + } + + # 季节性事件 + self.seasonal_events = { + 'spring': ['花开', '鸟类迁徙回归', '动物繁殖季开始'], + 'summer': ['高温', '干旱', '虫害高发'], + 'autumn': ['叶色变化', '果实成熟', '动物准备过冬'], + 'winter': ['降雪', '动物冬眠', '植物休眠'] + } + + # 季节性动物行为 + self.animal_behaviors = { + 'migration': { + 'spring': 0.8, # 春季迁徙概率 + 'summer': 0.1, + 'autumn': 0.9, + 'winter': 0.2 + }, + 'hibernation': { + 'spring': 0.1, + 'summer': 0.05, + 'autumn': 0.3, + 'winter': 0.9 + }, + 'breeding': { + 'spring': 0.9, + 'summer': 0.7, + 'autumn': 0.3, + 'winter': 0.1 + } + } + + # 季节性植被变化 + self.vegetation_changes = { + 'leaf_color': { + 'spring': (0.3, 0.7, 0.3), # 绿色 + 'summer': (0.2, 0.8, 0.2), + 'autumn': (0.8, 0.5, 0.2), # 橙色 + 'winter': (0.7, 0.7, 0.7) # 灰色 + }, + 'flowering': { + 'spring': 0.8, + 'summer': 0.4, + 'autumn': 0.1, + 'winter': 0.0 + }, + 'fruiting': { + 'spring': 0.0, + 'summer': 0.2, + 'autumn': 0.9, + 'winter': 0.0 + } + } + + # 时间配置 + self.time_config = { + 'day_length': 24.0, # 一天长度(小时) + 'simulation_speed': 1.0, # 模拟速度 + 'current_time': 0.0 # 当前时间 + } + + # 统计信息 + self.season_stats = { + 'seasons_passed': 0, + 'transitions_completed': 0, + 'events_triggered': 0, + 'total_simulation_time': 0.0 + } + + print("✓ 季节效应系统已创建") + + def initialize(self) -> bool: + """ + 初始化季节效应系统 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 季节效应系统初始化完成") + return True + + except Exception as e: + print(f"✗ 季节效应系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用季节效应系统 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 季节效应系统未初始化") + return False + + self.enabled = True + print("✓ 季节效应系统已启用") + return True + + except Exception as e: + print(f"✗ 季节效应系统启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用季节效应系统""" + try: + self.enabled = False + print("✓ 季节效应系统已禁用") + + except Exception as e: + print(f"✗ 季节效应系统禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理季节效应系统资源""" + try: + self.disable() + self.initialized = False + print("✓ 季节效应系统资源已清理") + + except Exception as e: + print(f"✗ 季节效应系统资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新季节效应系统状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新时间 + self.time_config['current_time'] += dt * self.time_config['simulation_speed'] + + # 更新季节进度 + self._update_season_progress(dt) + + # 更新季节过渡 + if self.transition_params['transition_active']: + self._update_season_transition(dt) + + # 更新统计信息 + self.season_stats['total_simulation_time'] += dt + + except Exception as e: + print(f"✗ 季节效应系统更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_season_progress(self, dt: float): + """ + 更新季节进度 + + Args: + dt: 时间增量 + """ + try: + # 将时间增量转换为小时 + hours_passed = dt / 3600 * self.time_config['simulation_speed'] + + # 更新当前季节中的天数 + self.season_day += hours_passed / 24.0 + self.total_days += hours_passed / 24.0 + + # 更新季节进度 + current_season_info = self.seasons[self.current_season] + self.season_progress = min(1.0, self.season_day / current_season_info['duration']) + + # 检查是否需要切换季节 + if self.season_day >= current_season_info['duration']: + self._transition_to_next_season() + + except Exception as e: + print(f"✗ 季节进度更新失败: {e}") + + def _transition_to_next_season(self): + """过渡到下一个季节""" + try: + # 季节顺序 + season_order = ['spring', 'summer', 'autumn', 'winter'] + + # 找到当前季节在顺序中的位置 + current_index = season_order.index(self.current_season) + + # 计算下一个季节 + next_index = (current_index + 1) % len(season_order) + next_season = season_order[next_index] + + # 开始季节过渡 + self.start_season_transition(next_season) + + except Exception as e: + print(f"✗ 季节切换失败: {e}") + + def start_season_transition(self, target_season: str): + """ + 开始季节过渡 + + Args: + target_season: 目标季节 + """ + try: + if target_season not in self.seasons: + print(f"✗ 无效的目标季节: {target_season}") + return + + if target_season == self.current_season: + return # 季节未变化 + + self.transition_params['target_season'] = target_season + self.transition_params['transition_active'] = True + self.transition_params['transition_progress'] = 0.0 + + print(f"✓ 开始季节过渡: {self.seasons[self.current_season]['name']} -> {self.seasons[target_season]['name']}") + + except Exception as e: + print(f"✗ 季节过渡启动失败: {e}") + + def _update_season_transition(self, dt: float): + """ + 更新季节过渡 + + Args: + dt: 时间增量 + """ + try: + if not self.transition_params['transition_active']: + return + + # 计算过渡进度增量 + transition_duration = self.transition_params['duration'] + if transition_duration > 0: + progress_delta = dt / (transition_duration * 86400) * self.time_config['simulation_speed'] + self.transition_params['transition_progress'] += progress_delta + + # 检查过渡是否完成 + if self.transition_params['transition_progress'] >= 1.0: + self._complete_season_transition() + + except Exception as e: + print(f"✗ 季节过渡更新失败: {e}") + + def _complete_season_transition(self): + """完成季节过渡""" + try: + # 更新当前季节 + self.current_season = self.transition_params['target_season'] + + # 重置季节内天数 + self.season_day = 0 + self.season_progress = 0.0 + + # 重置过渡状态 + self.transition_params['transition_active'] = False + self.transition_params['transition_progress'] = 0.0 + + # 更新统计信息 + self.season_stats['seasons_passed'] += 1 + self.season_stats['transitions_completed'] += 1 + + print(f"✓ 季节过渡完成: 现在是{self.seasons[self.current_season]['name']}") + + # 触发季节事件 + self._trigger_seasonal_events() + + # 通知其他系统季节已变化 + self._notify_systems_of_season_change() + + except Exception as e: + print(f"✗ 季节过渡完成失败: {e}") + + def _trigger_seasonal_events(self): + """触发季节性事件""" + try: + events = self.seasonal_events.get(self.current_season, []) + if events: + # 随机触发一个事件 + event = random.choice(events) + print(f"✓ 季节性事件触发: {event}") + self.season_stats['events_triggered'] += 1 + + except Exception as e: + print(f"✗ 季节性事件触发失败: {e}") + + def _notify_systems_of_season_change(self): + """通知其他系统季节已变化""" + try: + # 通知植被管理器 + if self.plugin.vegetation_manager: + # 更新环境因子 + current_season_info = self.seasons[self.current_season] + environment_factors = { + 'temperature': 20.0 + current_season_info['temperature_modifier'], + 'humidity': 50.0 + current_season_info['humidity_modifier'] + } + self.plugin.vegetation_manager.update_environment_factors(environment_factors) + + except Exception as e: + print(f"✗ 系统通知失败: {e}") + + def get_current_season(self) -> str: + """ + 获取当前季节 + + Returns: + 当前季节名称 + """ + return self.current_season + + def get_season_info(self, season: str = None) -> Optional[Dict[str, Any]]: + """ + 获取季节信息 + + Args: + season: 季节名称(如果为None,则返回当前季节信息) + + Returns: + 季节信息字典或None + """ + try: + if season is None: + season = self.current_season + + if season in self.seasons: + return self.seasons[season].copy() + return None + + except Exception as e: + print(f"✗ 季节信息获取失败: {e}") + return None + + def get_season_progress(self) -> float: + """ + 获取季节进度 + + Returns: + 季节进度 (0.0-1.0) + """ + return self.season_progress + + def get_season_day(self) -> int: + """ + 获取当前季节中的天数 + + Returns: + 当前季节中的天数 + """ + return int(self.season_day) + + def set_current_season(self, season: str): + """ + 设置当前季节 + + Args: + season: 季节名称 + """ + try: + if season in self.seasons: + self.current_season = season + self.season_day = 0 + self.season_progress = 0.0 + print(f"✓ 当前季节设置为: {self.seasons[season]['name']}") + else: + print(f"✗ 无效的季节: {season}") + + except Exception as e: + print(f"✗ 当前季节设置失败: {e}") + + def set_season_duration(self, season: str, duration: int): + """ + 设置季节持续时间 + + Args: + season: 季节名称 + duration: 持续时间(天) + """ + try: + if season in self.seasons: + self.seasons[season]['duration'] = max(1, duration) + print(f"✓ {self.seasons[season]['name']} 持续时间设置为: {duration} 天") + else: + print(f"✗ 无效的季节: {season}") + + except Exception as e: + print(f"✗ 季节持续时间设置失败: {e}") + + def get_interpolated_season_params(self) -> Dict[str, Any]: + """ + 获取插值后的季节参数(考虑季节过渡) + + Returns: + 插值后的季节参数字典 + """ + try: + if not self.transition_params['transition_active'] or self.transition_params['transition_progress'] <= 0.0: + # 没有过渡,返回当前季节参数 + return self.seasons[self.current_season].copy() + + if self.transition_params['transition_progress'] >= 1.0: + # 过渡完成,返回目标季节参数 + return self.seasons[self.transition_params['target_season']].copy() + + # 进行插值计算 + current_params = self.seasons[self.current_season] + target_params = self.seasons[self.transition_params['target_season']] + + interpolated_params = {} + + # 对数值参数进行线性插值 + numeric_params = [ + 'temperature_modifier', 'humidity_modifier', 'light_duration', + 'growth_factor', 'reproduction_factor' + ] + + for param in numeric_params: + current_value = current_params[param] + target_value = target_params[param] + interpolated_value = current_value + (target_value - current_value) * self.transition_params['transition_progress'] + interpolated_params[param] = interpolated_value + + # 对颜色进行插值 + current_color = current_params['color_tint'] + target_color = target_params['color_tint'] + interpolated_color = tuple( + current_color[i] + (target_color[i] - current_color[i]) * self.transition_params['transition_progress'] + for i in range(3) + ) + interpolated_params['color_tint'] = interpolated_color + + # 复制非数值参数 + interpolated_params['name'] = f"{current_params['name']} → {target_params['name']}" + interpolated_params['description'] = f"过渡季节 ({self.transition_params['transition_progress']*100:.1f}%)" + interpolated_params['duration'] = target_params['duration'] + + return interpolated_params + + except Exception as e: + print(f"✗ 插值季节参数获取失败: {e}") + return self.seasons[self.current_season].copy() + + def get_seasonal_events(self, season: str = None) -> List[str]: + """ + 获取季节性事件 + + Args: + season: 季节名称(如果为None,则返回当前季节事件) + + Returns: + 季节性事件列表 + """ + try: + if season is None: + season = self.current_season + + return self.seasonal_events.get(season, []).copy() + + except Exception as e: + print(f"✗ 季节性事件获取失败: {e}") + return [] + + def add_seasonal_event(self, season: str, event: str): + """ + 添加季节性事件 + + Args: + season: 季节名称 + event: 事件名称 + """ + try: + if season in self.seasonal_events: + if event not in self.seasonal_events[season]: + self.seasonal_events[season].append(event) + print(f"✓ 季节性事件已添加: {season} - {event}") + else: + print(f"✗ 事件已存在: {event}") + else: + print(f"✗ 无效的季节: {season}") + + except Exception as e: + print(f"✗ 季节性事件添加失败: {e}") + + def remove_seasonal_event(self, season: str, event: str): + """ + 移除季节性事件 + + Args: + season: 季节名称 + event: 事件名称 + """ + try: + if season in self.seasonal_events and event in self.seasonal_events[season]: + self.seasonal_events[season].remove(event) + print(f"✓ 季节性事件已移除: {season} - {event}") + else: + print(f"✗ 事件不存在: {season} - {event}") + + except Exception as e: + print(f"✗ 季节性事件移除失败: {e}") + + def get_animal_behavior_factor(self, behavior: str) -> float: + """ + 获取动物行为因子 + + Args: + behavior: 行为类型 + + Returns: + 行为因子 (0.0-1.0) + """ + try: + if behavior in self.animal_behaviors and self.current_season in self.animal_behaviors[behavior]: + return self.animal_behaviors[behavior][self.current_season] + return 0.5 + + except Exception as e: + print(f"✗ 动物行为因子获取失败: {e}") + return 0.5 + + def get_vegetation_change_factor(self, change_type: str) -> float: + """ + 获取植被变化因子 + + Args: + change_type: 变化类型 + + Returns: + 变化因子 (0.0-1.0) + """ + try: + if change_type in self.vegetation_changes and self.current_season in self.vegetation_changes[change_type]: + if change_type == 'leaf_color': + # 颜色返回RGB元组 + return self.vegetation_changes[change_type][self.current_season] + else: + # 其他因子返回数值 + return self.vegetation_changes[change_type][self.current_season] + return 0.5 if change_type != 'leaf_color' else (0.5, 0.5, 0.5) + + except Exception as e: + print(f"✗ 植被变化因子获取失败: {e}") + return 0.5 if change_type != 'leaf_color' else (0.5, 0.5, 0.5) + + def set_time_config(self, config: Dict[str, float]): + """ + 设置时间配置 + + Args: + config: 时间配置字典 + """ + try: + self.time_config.update(config) + print(f"✓ 时间配置已更新: {self.time_config}") + except Exception as e: + print(f"✗ 时间配置更新失败: {e}") + + def get_time_config(self) -> Dict[str, float]: + """ + 获取时间配置 + + Returns: + 时间配置字典 + """ + return self.time_config.copy() + + def set_transition_duration(self, duration: float): + """ + 设置过渡持续时间 + + Args: + duration: 持续时间(天) + """ + try: + self.transition_params['duration'] = max(0.1, duration) + print(f"✓ 过渡持续时间设置为: {duration} 天") + except Exception as e: + print(f"✗ 过渡持续时间设置失败: {e}") + + def get_transition_progress(self) -> float: + """ + 获取季节过渡进度 + + Returns: + 过渡进度 (0.0-1.0) + """ + return self.transition_params['transition_progress'] + + def is_transition_active(self) -> bool: + """ + 检查是否正在进行季节过渡 + + Returns: + 是否正在进行季节过渡 + """ + return self.transition_params['transition_active'] + + def get_season_list(self) -> List[str]: + """ + 获取所有季节列表 + + Returns: + 季节名称列表 + """ + return list(self.seasons.keys()) + + def get_season_name(self, season: str) -> str: + """ + 获取季节显示名称 + + Args: + season: 季节名称 + + Returns: + 季节显示名称 + """ + if season in self.seasons: + return self.seasons[season]['name'] + return season + + def get_season_stats(self) -> Dict[str, Any]: + """ + 获取季节统计信息 + + Returns: + 统计信息字典 + """ + return self.season_stats.copy() + + def reset_season_stats(self): + """重置季节统计信息""" + try: + self.season_stats = { + 'seasons_passed': 0, + 'transitions_completed': 0, + 'events_triggered': 0, + 'total_simulation_time': 0.0 + } + print("✓ 季节统计信息已重置") + except Exception as e: + print(f"✗ 季节统计信息重置失败: {e}") + + def fast_forward_to_season(self, target_season: str): + """ + 快进到目标季节 + + Args: + target_season: 目标季节 + """ + try: + if target_season not in self.seasons: + print(f"✗ 无效的目标季节: {target_season}") + return + + # 直接设置季节 + self.current_season = target_season + self.season_day = 0 + self.season_progress = 0.0 + + # 重置过渡状态 + self.transition_params['transition_active'] = False + self.transition_params['transition_progress'] = 0.0 + + print(f"✓ 快进到季节: {self.seasons[target_season]['name']}") + + # 触发季节事件 + self._trigger_seasonal_events() + + # 通知其他系统 + self._notify_systems_of_season_change() + + except Exception as e: print(f"✗ 快进到季节失败: {e}") \ No newline at end of file diff --git a/plugins/user/vegetation_ecosystem/terrain/terrain_influence.py b/plugins/user/vegetation_ecosystem/terrain/terrain_influence.py index 18a1c4f5..38d26cca 100644 --- a/plugins/user/vegetation_ecosystem/terrain/terrain_influence.py +++ b/plugins/user/vegetation_ecosystem/terrain/terrain_influence.py @@ -1,664 +1,664 @@ -""" -地形影响系统 -负责模拟地形特征对植被分布和生态系统的影响 -""" - -import time -from typing import Dict, Any, List, Optional, Tuple -import math -import random - -class TerrainInfluence: - """ - 地形影响系统 - 负责模拟地形特征(坡度、海拔、朝向等)对植被分布和生态系统的影响 - """ - - def __init__(self, plugin): - """ - 初始化地形影响系统 - - Args: - plugin: 植被和生态系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 地形因子定义 - self.terrain_factors = { - 'elevation': { - 'name': '海拔', - 'description': '海拔高度对植被的影响', - 'optimal_range': (0, 2000), # 最佳海拔范围(米) - 'stress_threshold': 3000, # 压力阈值(米) - 'impact_weight': 0.3 # 影响权重 - }, - 'slope': { - 'name': '坡度', - 'description': '坡度对植被的影响', - 'optimal_range': (0, 30), # 最佳坡度范围(度) - 'stress_threshold': 60, # 压力阈值(度) - 'impact_weight': 0.2 - }, - 'aspect': { - 'name': '朝向', - 'description': '坡面朝向对植被的影响', - 'optimal_range': (135, 225), # 最佳朝向(东南-西南) - 'stress_direction': (0, 180), # 不利朝向(北向) - 'impact_weight': 0.15 - }, - 'soil_depth': { - 'name': '土壤深度', - 'description': '土壤深度对植被根系的影响', - 'optimal_range': (0.5, 2.0), # 最佳土壤深度(米) - 'stress_threshold': 0.2, # 压力阈值(米) - 'impact_weight': 0.25 - }, - 'water_accumulation': { - 'name': '积水', - 'description': '地表积水对植被的影响', - 'optimal_range': (0, 0.3), # 最佳积水深度(米) - 'stress_threshold': 0.5, # 压力阈值(米) - 'impact_weight': 0.1 - } - } - - # 地形影响模型 - self.influence_models = { - 'linear': { - 'name': '线性模型', - 'description': '线性影响关系' - }, - 'exponential': { - 'name': '指数模型', - 'description': '指数影响关系' - }, - 'threshold': { - 'name': '阈值模型', - 'description': '基于阈值的影响' - }, - 'combined': { - 'name': '组合模型', - 'description': '综合多种因子的影响' - } - } - - # 地形数据缓存 - self.terrain_cache = {} - self.cache_config = { - 'max_size': 1000, # 最大缓存大小 - 'expiration_time': 300, # 过期时间(秒) - 'update_interval': 60 # 更新间隔(秒) - } - - # 地形微气候效应 - self.microclimate_effects = { - 'temperature_variation': 0.0, # 温度变化 - 'humidity_variation': 0.0, # 湿度变化 - 'wind_shelter': 0.0, # 风力遮蔽 - 'radiation_exposure': 0.0 # 辐射暴露 - } - - # 地形适应性参数 - self.adaptation_params = { - 'adaptation_rate': 0.01, # 适应速率 - 'genetic_diversity': 0.7, # 遗传多样性 - 'migration_capability': 0.3, # 迁移能力 - 'stress_tolerance': 0.5 # 压力耐受性 - } - - # 统计信息 - self.terrain_stats = { - 'influence_calculations': 0, - 'cache_hits': 0, - 'cache_misses': 0, - 'microclimate_updates': 0 - } - - print("✓ 地形影响系统已创建") - - def initialize(self) -> bool: - """ - 初始化地形影响系统 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 地形影响系统初始化完成") - return True - - except Exception as e: - print(f"✗ 地形影响系统初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用地形影响系统 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 地形影响系统未初始化") - return False - - self.enabled = True - print("✓ 地形影响系统已启用") - return True - - except Exception as e: - print(f"✗ 地形影响系统启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用地形影响系统""" - try: - self.enabled = False - print("✓ 地形影响系统已禁用") - - except Exception as e: - print(f"✗ 地形影响系统禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理地形影响系统资源""" - try: - self.disable() - self.terrain_cache.clear() - self.initialized = False - print("✓ 地形影响系统资源已清理") - - except Exception as e: - print(f"✗ 地形影响系统资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新地形影响系统状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新地形缓存 - self._update_terrain_cache() - - # 更新微气候效应 - self._update_microclimate_effects() - - except Exception as e: - print(f"✗ 地形影响系统更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_terrain_cache(self): - """更新地形数据缓存""" - try: - current_time = time.time() - - # 清理过期缓存 - expired_keys = [] - for key, (timestamp, _) in self.terrain_cache.items(): - if current_time - timestamp > self.cache_config['expiration_time']: - expired_keys.append(key) - - for key in expired_keys: - del self.terrain_cache[key] - - except Exception as e: - print(f"✗ 地形缓存更新失败: {e}") - - def _update_microclimate_effects(self): - """更新微气候效应""" - try: - # 在实际实现中,这里会根据地形特征更新微气候效应 - self.terrain_stats['microclimate_updates'] += 1 - except Exception as e: - print(f"✗ 微气候效应更新失败: {e}") - - def calculate_terrain_influence(self, position: Tuple[float, float, float], - vegetation_type: str = None) -> Dict[str, float]: - """ - 计算地形对植被的影响 - - Args: - position: 位置坐标 (x, y, z) - vegetation_type: 植被类型(可选) - - Returns: - 地形影响因子字典 - """ - try: - # 检查缓存 - cache_key = f"{position}_{vegetation_type or 'general'}" - current_time = time.time() - - if cache_key in self.terrain_cache: - timestamp, cached_result = self.terrain_cache[cache_key] - if current_time - timestamp < self.cache_config['expiration_time']: - self.terrain_stats['cache_hits'] += 1 - return cached_result - - self.terrain_stats['cache_misses'] += 1 - - # 获取地形数据 - terrain_data = self._get_terrain_data(position) - - # 计算各因子影响 - influence_factors = {} - total_weight = 0.0 - weighted_sum = 0.0 - - for factor_name, factor_info in self.terrain_factors.items(): - if factor_name in terrain_data: - factor_value = terrain_data[factor_name] - factor_influence = self._calculate_factor_influence( - factor_name, factor_value, factor_info - ) - - influence_factors[factor_name] = factor_influence - weight = factor_info['impact_weight'] - weighted_sum += factor_influence * weight - total_weight += weight - - # 计算综合影响 - if total_weight > 0: - overall_influence = weighted_sum / total_weight - else: - overall_influence = 1.0 - - influence_factors['overall'] = overall_influence - - # 缓存结果 - self.terrain_cache[cache_key] = (current_time, influence_factors) - - # 限制缓存大小 - if len(self.terrain_cache) > self.cache_config['max_size']: - # 移除最旧的条目 - oldest_key = min(self.terrain_cache.keys(), - key=lambda k: self.terrain_cache[k][0]) - del self.terrain_cache[oldest_key] - - self.terrain_stats['influence_calculations'] += 1 - - return influence_factors - - except Exception as e: - print(f"✗ 地形影响计算失败: {e}") - return {'overall': 1.0} - - def _get_terrain_data(self, position: Tuple[float, float, float]) -> Dict[str, float]: - """ - 获取指定位置的地形数据 - - Args: - position: 位置坐标 (x, y, z) - - Returns: - 地形数据字典 - """ - try: - x, y, z = position - - # 简化的地形数据生成 - # 在实际实现中,这里会从真实的地形数据源获取信息 - terrain_data = { - 'elevation': y, # 假设y坐标为海拔 - 'slope': random.uniform(0, 45), # 随机坡度 - 'aspect': random.uniform(0, 360), # 随机朝向 - 'soil_depth': random.uniform(0.1, 3.0), # 随机土壤深度 - 'water_accumulation': random.uniform(0, 0.8) # 随机积水深度 - } - - return terrain_data - - except Exception as e: - print(f"✗ 地形数据获取失败: {e}") - return {} - - def _calculate_factor_influence(self, factor_name: str, factor_value: float, - factor_info: Dict[str, Any]) -> float: - """ - 计算单个地形因子的影响 - - Args: - factor_name: 因子名称 - factor_value: 因子值 - factor_info: 因子信息 - - Returns: - 影响因子 (0.0-2.0) - """ - try: - optimal_range = factor_info['optimal_range'] - impact_weight = factor_info['impact_weight'] - - min_optimal, max_optimal = optimal_range - - # 计算在最优范围内的适宜性 - if min_optimal <= factor_value <= max_optimal: - suitability = 1.0 - else: - # 计算距离最优范围的距离 - if factor_value < min_optimal: - distance = min_optimal - factor_value - else: - distance = factor_value - max_optimal - - # 指数衰减函数 - suitability = max(0.0, math.exp(-distance / (max_optimal - min_optimal))) - - # 应用压力阈值 - if 'stress_threshold' in factor_info: - stress_threshold = factor_info['stress_threshold'] - if factor_name in ['elevation', 'slope', 'water_accumulation']: - if factor_value > stress_threshold: - # 超过压力阈值时进一步降低适宜性 - excess = factor_value - stress_threshold - stress_factor = max(0.0, 1.0 - (excess / stress_threshold)) - suitability *= stress_factor - - return max(0.0, min(2.0, suitability)) - - except Exception as e: - print(f"✗ 因子影响计算失败: {e}") - return 1.0 - - def get_terrain_factors(self) -> Dict[str, Dict[str, Any]]: - """ - 获取地形因子定义 - - Returns: - 地形因子字典 - """ - return self.terrain_factors.copy() - - def get_terrain_factor_info(self, factor_name: str) -> Optional[Dict[str, Any]]: - """ - 获取特定地形因子的信息 - - Args: - factor_name: 因子名称 - - Returns: - 因子信息字典或None - """ - return self.terrain_factors.get(factor_name) - - def set_terrain_factor_parameter(self, factor_name: str, parameter: str, value: Any): - """ - 设置地形因子参数 - - Args: - factor_name: 因子名称 - parameter: 参数名称 - value: 参数值 - """ - try: - if factor_name in self.terrain_factors and parameter in self.terrain_factors[factor_name]: - self.terrain_factors[factor_name][parameter] = value - print(f"✓ 地形因子参数已设置: {factor_name}.{parameter} = {value}") - else: - print(f"✗ 无效的地形因子参数: {factor_name}.{parameter}") - - except Exception as e: - print(f"✗ 地形因子参数设置失败: {e}") - - def get_influence_models(self) -> Dict[str, Dict[str, str]]: - """ - 获取影响模型 - - Returns: - 影响模型字典 - """ - return self.influence_models.copy() - - def set_influence_model(self, model_name: str): - """ - 设置影响模型 - - Args: - model_name: 模型名称 - """ - try: - if model_name in self.influence_models: - # 在实际实现中,这里会设置当前使用的模型 - print(f"✓ 影响模型设置为: {self.influence_models[model_name]['name']}") - else: - print(f"✗ 无效的影响模型: {model_name}") - - except Exception as e: - print(f"✗ 影响模型设置失败: {e}") - - def set_cache_config(self, config: Dict[str, Any]): - """ - 设置缓存配置 - - Args: - config: 缓存配置字典 - """ - try: - self.cache_config.update(config) - print(f"✓ 缓存配置已更新: {self.cache_config}") - except Exception as e: - print(f"✗ 缓存配置更新失败: {e}") - - def get_cache_config(self) -> Dict[str, Any]: - """ - 获取缓存配置 - - Returns: - 缓存配置字典 - """ - return self.cache_config.copy() - - def clear_terrain_cache(self): - """清空地形缓存""" - try: - self.terrain_cache.clear() - print("✓ 地形缓存已清空") - except Exception as e: - print(f"✗ 地形缓存清空失败: {e}") - - def get_terrain_cache_info(self) -> Dict[str, Any]: - """ - 获取地形缓存信息 - - Returns: - 缓存信息字典 - """ - try: - return { - 'cache_size': len(self.terrain_cache), - 'max_size': self.cache_config['max_size'], - 'hit_rate': (self.terrain_stats['cache_hits'] / - max(1, self.terrain_stats['cache_hits'] + self.terrain_stats['cache_misses'])) - } - except Exception as e: - print(f"✗ 缓存信息获取失败: {e}") - return {} - - def update_microclimate_effect(self, effect_name: str, value: float): - """ - 更新微气候效应 - - Args: - effect_name: 效应名称 - value: 效应值 - """ - try: - if effect_name in self.microclimate_effects: - self.microclimate_effects[effect_name] = value - print(f"✓ 微气候效应已更新: {effect_name} = {value}") - else: - print(f"✗ 无效的微气候效应: {effect_name}") - - except Exception as e: - print(f"✗ 微气候效应更新失败: {e}") - - def get_microclimate_effects(self) -> Dict[str, float]: - """ - 获取微气候效应 - - Returns: - 微气候效应字典 - """ - return self.microclimate_effects.copy() - - def set_adaptation_parameter(self, parameter: str, value: float): - """ - 设置适应性参数 - - Args: - parameter: 参数名称 - value: 参数值 - """ - try: - if parameter in self.adaptation_params: - self.adaptation_params[parameter] = max(0.0, min(1.0, value)) - print(f"✓ 适应性参数已设置: {parameter} = {value}") - else: - print(f"✗ 无效的适应性参数: {parameter}") - - except Exception as e: - print(f"✗ 适应性参数设置失败: {e}") - - def get_adaptation_parameters(self) -> Dict[str, float]: - """ - 获取适应性参数 - - Returns: - 适应性参数字典 - """ - return self.adaptation_params.copy() - - def simulate_terrain_adaptation(self, vegetation_type: str, - terrain_conditions: Dict[str, float]) -> float: - """ - 模拟植被对地形的适应过程 - - Args: - vegetation_type: 植被类型 - terrain_conditions: 地形条件 - - Returns: - 适应性评分 (0.0-1.0) - """ - try: - # 获取植被的地形偏好 - if self.plugin.vegetation_manager: - veg_info = self.plugin.vegetation_manager.get_vegetation_info(vegetation_type) - if veg_info: - # 基于植被类型和地形条件计算适应性 - adaptation_score = self._calculate_adaptation_score(vegetation_type, terrain_conditions) - return adaptation_score - - # 默认适应性评分 - return 0.5 - - except Exception as e: - print(f"✗ 地形适应模拟失败: {e}") - return 0.5 - - def _calculate_adaptation_score(self, vegetation_type: str, - terrain_conditions: Dict[str, float]) -> float: - """ - 计算植被对地形的适应性评分 - - Args: - vegetation_type: 植被类型 - terrain_conditions: 地形条件 - - Returns: - 适应性评分 (0.0-1.0) - """ - try: - # 简化的适应性计算 - scores = [] - - for factor_name, factor_value in terrain_conditions.items(): - if factor_name in self.terrain_factors: - factor_info = self.terrain_factors[factor_name] - factor_influence = self._calculate_factor_influence( - factor_name, factor_value, factor_info - ) - scores.append(factor_influence) - - if scores: - return sum(scores) / len(scores) - return 0.5 - - except Exception as e: - print(f"✗ 适应性评分计算失败: {e}") - return 0.5 - - def get_terrain_stats(self) -> Dict[str, int]: - """ - 获取地形统计信息 - - Returns: - 统计信息字典 - """ - return self.terrain_stats.copy() - - def reset_terrain_stats(self): - """重置地形统计信息""" - try: - self.terrain_stats = { - 'influence_calculations': 0, - 'cache_hits': 0, - 'cache_misses': 0, - 'microclimate_updates': 0 - } - print("✓ 地形统计信息已重置") - except Exception as e: - print(f"✗ 地形统计信息重置失败: {e}") - - def get_terrain_suitability_map(self, area: Tuple[Tuple[float, float], Tuple[float, float]], - resolution: float = 1.0) -> Dict[Tuple[float, float], float]: - """ - 获取区域内地形适宜性地图 - - Args: - area: 区域范围 ((min_x, min_z), (max_x, max_z)) - resolution: 分辨率 - - Returns: - 适宜性地图字典 {(x, z): suitability} - """ - try: - (min_x, min_z), (max_x, max_z) = area - suitability_map = {} - - x = min_x - while x <= max_x: - z = min_z - while z <= max_z: - # 计算位置的适宜性 - position = (x, 0, z) # 假设y坐标为0 - influence = self.calculate_terrain_influence(position) - suitability = influence.get('overall', 1.0) - - suitability_map[(x, z)] = suitability - - z += resolution - x += resolution - - return suitability_map - - except Exception as e: - print(f"✗ 适宜性地图生成失败: {e}") +""" +地形影响系统 +负责模拟地形特征对植被分布和生态系统的影响 +""" + +import time +from typing import Dict, Any, List, Optional, Tuple +import math +import random + +class TerrainInfluence: + """ + 地形影响系统 + 负责模拟地形特征(坡度、海拔、朝向等)对植被分布和生态系统的影响 + """ + + def __init__(self, plugin): + """ + 初始化地形影响系统 + + Args: + plugin: 植被和生态系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 地形因子定义 + self.terrain_factors = { + 'elevation': { + 'name': '海拔', + 'description': '海拔高度对植被的影响', + 'optimal_range': (0, 2000), # 最佳海拔范围(米) + 'stress_threshold': 3000, # 压力阈值(米) + 'impact_weight': 0.3 # 影响权重 + }, + 'slope': { + 'name': '坡度', + 'description': '坡度对植被的影响', + 'optimal_range': (0, 30), # 最佳坡度范围(度) + 'stress_threshold': 60, # 压力阈值(度) + 'impact_weight': 0.2 + }, + 'aspect': { + 'name': '朝向', + 'description': '坡面朝向对植被的影响', + 'optimal_range': (135, 225), # 最佳朝向(东南-西南) + 'stress_direction': (0, 180), # 不利朝向(北向) + 'impact_weight': 0.15 + }, + 'soil_depth': { + 'name': '土壤深度', + 'description': '土壤深度对植被根系的影响', + 'optimal_range': (0.5, 2.0), # 最佳土壤深度(米) + 'stress_threshold': 0.2, # 压力阈值(米) + 'impact_weight': 0.25 + }, + 'water_accumulation': { + 'name': '积水', + 'description': '地表积水对植被的影响', + 'optimal_range': (0, 0.3), # 最佳积水深度(米) + 'stress_threshold': 0.5, # 压力阈值(米) + 'impact_weight': 0.1 + } + } + + # 地形影响模型 + self.influence_models = { + 'linear': { + 'name': '线性模型', + 'description': '线性影响关系' + }, + 'exponential': { + 'name': '指数模型', + 'description': '指数影响关系' + }, + 'threshold': { + 'name': '阈值模型', + 'description': '基于阈值的影响' + }, + 'combined': { + 'name': '组合模型', + 'description': '综合多种因子的影响' + } + } + + # 地形数据缓存 + self.terrain_cache = {} + self.cache_config = { + 'max_size': 1000, # 最大缓存大小 + 'expiration_time': 300, # 过期时间(秒) + 'update_interval': 60 # 更新间隔(秒) + } + + # 地形微气候效应 + self.microclimate_effects = { + 'temperature_variation': 0.0, # 温度变化 + 'humidity_variation': 0.0, # 湿度变化 + 'wind_shelter': 0.0, # 风力遮蔽 + 'radiation_exposure': 0.0 # 辐射暴露 + } + + # 地形适应性参数 + self.adaptation_params = { + 'adaptation_rate': 0.01, # 适应速率 + 'genetic_diversity': 0.7, # 遗传多样性 + 'migration_capability': 0.3, # 迁移能力 + 'stress_tolerance': 0.5 # 压力耐受性 + } + + # 统计信息 + self.terrain_stats = { + 'influence_calculations': 0, + 'cache_hits': 0, + 'cache_misses': 0, + 'microclimate_updates': 0 + } + + print("✓ 地形影响系统已创建") + + def initialize(self) -> bool: + """ + 初始化地形影响系统 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 地形影响系统初始化完成") + return True + + except Exception as e: + print(f"✗ 地形影响系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用地形影响系统 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 地形影响系统未初始化") + return False + + self.enabled = True + print("✓ 地形影响系统已启用") + return True + + except Exception as e: + print(f"✗ 地形影响系统启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用地形影响系统""" + try: + self.enabled = False + print("✓ 地形影响系统已禁用") + + except Exception as e: + print(f"✗ 地形影响系统禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理地形影响系统资源""" + try: + self.disable() + self.terrain_cache.clear() + self.initialized = False + print("✓ 地形影响系统资源已清理") + + except Exception as e: + print(f"✗ 地形影响系统资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新地形影响系统状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新地形缓存 + self._update_terrain_cache() + + # 更新微气候效应 + self._update_microclimate_effects() + + except Exception as e: + print(f"✗ 地形影响系统更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_terrain_cache(self): + """更新地形数据缓存""" + try: + current_time = time.time() + + # 清理过期缓存 + expired_keys = [] + for key, (timestamp, _) in self.terrain_cache.items(): + if current_time - timestamp > self.cache_config['expiration_time']: + expired_keys.append(key) + + for key in expired_keys: + del self.terrain_cache[key] + + except Exception as e: + print(f"✗ 地形缓存更新失败: {e}") + + def _update_microclimate_effects(self): + """更新微气候效应""" + try: + # 在实际实现中,这里会根据地形特征更新微气候效应 + self.terrain_stats['microclimate_updates'] += 1 + except Exception as e: + print(f"✗ 微气候效应更新失败: {e}") + + def calculate_terrain_influence(self, position: Tuple[float, float, float], + vegetation_type: str = None) -> Dict[str, float]: + """ + 计算地形对植被的影响 + + Args: + position: 位置坐标 (x, y, z) + vegetation_type: 植被类型(可选) + + Returns: + 地形影响因子字典 + """ + try: + # 检查缓存 + cache_key = f"{position}_{vegetation_type or 'general'}" + current_time = time.time() + + if cache_key in self.terrain_cache: + timestamp, cached_result = self.terrain_cache[cache_key] + if current_time - timestamp < self.cache_config['expiration_time']: + self.terrain_stats['cache_hits'] += 1 + return cached_result + + self.terrain_stats['cache_misses'] += 1 + + # 获取地形数据 + terrain_data = self._get_terrain_data(position) + + # 计算各因子影响 + influence_factors = {} + total_weight = 0.0 + weighted_sum = 0.0 + + for factor_name, factor_info in self.terrain_factors.items(): + if factor_name in terrain_data: + factor_value = terrain_data[factor_name] + factor_influence = self._calculate_factor_influence( + factor_name, factor_value, factor_info + ) + + influence_factors[factor_name] = factor_influence + weight = factor_info['impact_weight'] + weighted_sum += factor_influence * weight + total_weight += weight + + # 计算综合影响 + if total_weight > 0: + overall_influence = weighted_sum / total_weight + else: + overall_influence = 1.0 + + influence_factors['overall'] = overall_influence + + # 缓存结果 + self.terrain_cache[cache_key] = (current_time, influence_factors) + + # 限制缓存大小 + if len(self.terrain_cache) > self.cache_config['max_size']: + # 移除最旧的条目 + oldest_key = min(self.terrain_cache.keys(), + key=lambda k: self.terrain_cache[k][0]) + del self.terrain_cache[oldest_key] + + self.terrain_stats['influence_calculations'] += 1 + + return influence_factors + + except Exception as e: + print(f"✗ 地形影响计算失败: {e}") + return {'overall': 1.0} + + def _get_terrain_data(self, position: Tuple[float, float, float]) -> Dict[str, float]: + """ + 获取指定位置的地形数据 + + Args: + position: 位置坐标 (x, y, z) + + Returns: + 地形数据字典 + """ + try: + x, y, z = position + + # 简化的地形数据生成 + # 在实际实现中,这里会从真实的地形数据源获取信息 + terrain_data = { + 'elevation': y, # 假设y坐标为海拔 + 'slope': random.uniform(0, 45), # 随机坡度 + 'aspect': random.uniform(0, 360), # 随机朝向 + 'soil_depth': random.uniform(0.1, 3.0), # 随机土壤深度 + 'water_accumulation': random.uniform(0, 0.8) # 随机积水深度 + } + + return terrain_data + + except Exception as e: + print(f"✗ 地形数据获取失败: {e}") + return {} + + def _calculate_factor_influence(self, factor_name: str, factor_value: float, + factor_info: Dict[str, Any]) -> float: + """ + 计算单个地形因子的影响 + + Args: + factor_name: 因子名称 + factor_value: 因子值 + factor_info: 因子信息 + + Returns: + 影响因子 (0.0-2.0) + """ + try: + optimal_range = factor_info['optimal_range'] + impact_weight = factor_info['impact_weight'] + + min_optimal, max_optimal = optimal_range + + # 计算在最优范围内的适宜性 + if min_optimal <= factor_value <= max_optimal: + suitability = 1.0 + else: + # 计算距离最优范围的距离 + if factor_value < min_optimal: + distance = min_optimal - factor_value + else: + distance = factor_value - max_optimal + + # 指数衰减函数 + suitability = max(0.0, math.exp(-distance / (max_optimal - min_optimal))) + + # 应用压力阈值 + if 'stress_threshold' in factor_info: + stress_threshold = factor_info['stress_threshold'] + if factor_name in ['elevation', 'slope', 'water_accumulation']: + if factor_value > stress_threshold: + # 超过压力阈值时进一步降低适宜性 + excess = factor_value - stress_threshold + stress_factor = max(0.0, 1.0 - (excess / stress_threshold)) + suitability *= stress_factor + + return max(0.0, min(2.0, suitability)) + + except Exception as e: + print(f"✗ 因子影响计算失败: {e}") + return 1.0 + + def get_terrain_factors(self) -> Dict[str, Dict[str, Any]]: + """ + 获取地形因子定义 + + Returns: + 地形因子字典 + """ + return self.terrain_factors.copy() + + def get_terrain_factor_info(self, factor_name: str) -> Optional[Dict[str, Any]]: + """ + 获取特定地形因子的信息 + + Args: + factor_name: 因子名称 + + Returns: + 因子信息字典或None + """ + return self.terrain_factors.get(factor_name) + + def set_terrain_factor_parameter(self, factor_name: str, parameter: str, value: Any): + """ + 设置地形因子参数 + + Args: + factor_name: 因子名称 + parameter: 参数名称 + value: 参数值 + """ + try: + if factor_name in self.terrain_factors and parameter in self.terrain_factors[factor_name]: + self.terrain_factors[factor_name][parameter] = value + print(f"✓ 地形因子参数已设置: {factor_name}.{parameter} = {value}") + else: + print(f"✗ 无效的地形因子参数: {factor_name}.{parameter}") + + except Exception as e: + print(f"✗ 地形因子参数设置失败: {e}") + + def get_influence_models(self) -> Dict[str, Dict[str, str]]: + """ + 获取影响模型 + + Returns: + 影响模型字典 + """ + return self.influence_models.copy() + + def set_influence_model(self, model_name: str): + """ + 设置影响模型 + + Args: + model_name: 模型名称 + """ + try: + if model_name in self.influence_models: + # 在实际实现中,这里会设置当前使用的模型 + print(f"✓ 影响模型设置为: {self.influence_models[model_name]['name']}") + else: + print(f"✗ 无效的影响模型: {model_name}") + + except Exception as e: + print(f"✗ 影响模型设置失败: {e}") + + def set_cache_config(self, config: Dict[str, Any]): + """ + 设置缓存配置 + + Args: + config: 缓存配置字典 + """ + try: + self.cache_config.update(config) + print(f"✓ 缓存配置已更新: {self.cache_config}") + except Exception as e: + print(f"✗ 缓存配置更新失败: {e}") + + def get_cache_config(self) -> Dict[str, Any]: + """ + 获取缓存配置 + + Returns: + 缓存配置字典 + """ + return self.cache_config.copy() + + def clear_terrain_cache(self): + """清空地形缓存""" + try: + self.terrain_cache.clear() + print("✓ 地形缓存已清空") + except Exception as e: + print(f"✗ 地形缓存清空失败: {e}") + + def get_terrain_cache_info(self) -> Dict[str, Any]: + """ + 获取地形缓存信息 + + Returns: + 缓存信息字典 + """ + try: + return { + 'cache_size': len(self.terrain_cache), + 'max_size': self.cache_config['max_size'], + 'hit_rate': (self.terrain_stats['cache_hits'] / + max(1, self.terrain_stats['cache_hits'] + self.terrain_stats['cache_misses'])) + } + except Exception as e: + print(f"✗ 缓存信息获取失败: {e}") + return {} + + def update_microclimate_effect(self, effect_name: str, value: float): + """ + 更新微气候效应 + + Args: + effect_name: 效应名称 + value: 效应值 + """ + try: + if effect_name in self.microclimate_effects: + self.microclimate_effects[effect_name] = value + print(f"✓ 微气候效应已更新: {effect_name} = {value}") + else: + print(f"✗ 无效的微气候效应: {effect_name}") + + except Exception as e: + print(f"✗ 微气候效应更新失败: {e}") + + def get_microclimate_effects(self) -> Dict[str, float]: + """ + 获取微气候效应 + + Returns: + 微气候效应字典 + """ + return self.microclimate_effects.copy() + + def set_adaptation_parameter(self, parameter: str, value: float): + """ + 设置适应性参数 + + Args: + parameter: 参数名称 + value: 参数值 + """ + try: + if parameter in self.adaptation_params: + self.adaptation_params[parameter] = max(0.0, min(1.0, value)) + print(f"✓ 适应性参数已设置: {parameter} = {value}") + else: + print(f"✗ 无效的适应性参数: {parameter}") + + except Exception as e: + print(f"✗ 适应性参数设置失败: {e}") + + def get_adaptation_parameters(self) -> Dict[str, float]: + """ + 获取适应性参数 + + Returns: + 适应性参数字典 + """ + return self.adaptation_params.copy() + + def simulate_terrain_adaptation(self, vegetation_type: str, + terrain_conditions: Dict[str, float]) -> float: + """ + 模拟植被对地形的适应过程 + + Args: + vegetation_type: 植被类型 + terrain_conditions: 地形条件 + + Returns: + 适应性评分 (0.0-1.0) + """ + try: + # 获取植被的地形偏好 + if self.plugin.vegetation_manager: + veg_info = self.plugin.vegetation_manager.get_vegetation_info(vegetation_type) + if veg_info: + # 基于植被类型和地形条件计算适应性 + adaptation_score = self._calculate_adaptation_score(vegetation_type, terrain_conditions) + return adaptation_score + + # 默认适应性评分 + return 0.5 + + except Exception as e: + print(f"✗ 地形适应模拟失败: {e}") + return 0.5 + + def _calculate_adaptation_score(self, vegetation_type: str, + terrain_conditions: Dict[str, float]) -> float: + """ + 计算植被对地形的适应性评分 + + Args: + vegetation_type: 植被类型 + terrain_conditions: 地形条件 + + Returns: + 适应性评分 (0.0-1.0) + """ + try: + # 简化的适应性计算 + scores = [] + + for factor_name, factor_value in terrain_conditions.items(): + if factor_name in self.terrain_factors: + factor_info = self.terrain_factors[factor_name] + factor_influence = self._calculate_factor_influence( + factor_name, factor_value, factor_info + ) + scores.append(factor_influence) + + if scores: + return sum(scores) / len(scores) + return 0.5 + + except Exception as e: + print(f"✗ 适应性评分计算失败: {e}") + return 0.5 + + def get_terrain_stats(self) -> Dict[str, int]: + """ + 获取地形统计信息 + + Returns: + 统计信息字典 + """ + return self.terrain_stats.copy() + + def reset_terrain_stats(self): + """重置地形统计信息""" + try: + self.terrain_stats = { + 'influence_calculations': 0, + 'cache_hits': 0, + 'cache_misses': 0, + 'microclimate_updates': 0 + } + print("✓ 地形统计信息已重置") + except Exception as e: + print(f"✗ 地形统计信息重置失败: {e}") + + def get_terrain_suitability_map(self, area: Tuple[Tuple[float, float], Tuple[float, float]], + resolution: float = 1.0) -> Dict[Tuple[float, float], float]: + """ + 获取区域内地形适宜性地图 + + Args: + area: 区域范围 ((min_x, min_z), (max_x, max_z)) + resolution: 分辨率 + + Returns: + 适宜性地图字典 {(x, z): suitability} + """ + try: + (min_x, min_z), (max_x, max_z) = area + suitability_map = {} + + x = min_x + while x <= max_x: + z = min_z + while z <= max_z: + # 计算位置的适宜性 + position = (x, 0, z) # 假设y坐标为0 + influence = self.calculate_terrain_influence(position) + suitability = influence.get('overall', 1.0) + + suitability_map[(x, z)] = suitability + + z += resolution + x += resolution + + return suitability_map + + except Exception as e: + print(f"✗ 适宜性地图生成失败: {e}") return {} \ No newline at end of file diff --git a/plugins/user/vegetation_ecosystem/utils/eco_utils.py b/plugins/user/vegetation_ecosystem/utils/eco_utils.py index f4c59583..c9873243 100644 --- a/plugins/user/vegetation_ecosystem/utils/eco_utils.py +++ b/plugins/user/vegetation_ecosystem/utils/eco_utils.py @@ -1,668 +1,668 @@ -""" -生态系统工具类 -提供各种实用工具函数和辅助功能 -""" - -import time -import json -import math -import random -from typing import Dict, Any, List, Optional, Callable -import hashlib - -class EcoUtils: - """ - 生态系统工具类 - 提供各种实用工具函数和辅助功能,包括数据处理、性能监控、日志记录等 - """ - - def __init__(self, plugin): - """ - 初始化生态系统工具类 - - Args: - plugin: 植被和生态系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 性能监控 - self.performance_monitor = { - 'frame_times': [], - 'update_times': [], - 'render_times': [], - 'memory_usage': 0, - 'object_counts': {} - } - - # 数据缓存 - self.data_cache = {} - self.cache_config = { - 'max_size': 1000, - 'expiration_time': 300 # 5分钟 - } - - # 日志配置 - self.log_config = { - 'level': 'INFO', - 'file_logging': True, - 'console_logging': True, - 'max_log_size': 10 * 1024 * 1024, # 10MB - 'log_file': 'eco_system.log' - } - - # 随机数生成器 - self.random_generator = random.Random() - self.random_seed = 12345 - - # 数据序列化 - self.serialization_config = { - 'format': 'json', - 'compression': False, - 'pretty_print': True - } - - print("✓ 生态系统工具类已创建") - - def initialize(self) -> bool: - """ - 初始化生态系统工具类 - - Returns: - 是否初始化成功 - """ - try: - # 设置随机种子 - self.random_generator.seed(self.random_seed) - - self.initialized = True - print("✓ 生态系统工具类初始化完成") - return True - - except Exception as e: - print(f"✗ 生态系统工具类初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用生态系统工具类 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 生态系统工具类未初始化") - return False - - self.enabled = True - print("✓ 生态系统工具类已启用") - return True - - except Exception as e: - print(f"✗ 生态系统工具类启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用生态系统工具类""" - try: - self.enabled = False - print("✓ 生态系统工具类已禁用") - - except Exception as e: - print(f"✗ 生态系统工具类禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理生态系统工具类资源""" - try: - self.disable() - self.data_cache.clear() - self.initialized = False - print("✓ 生态系统工具类资源已清理") - - except Exception as e: - print(f"✗ 生态系统工具类资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新工具类状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新性能监控 - self._update_performance_monitor(dt) - - # 清理缓存 - self._cleanup_cache() - - except Exception as e: - print(f"✗ 生态系统工具类更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_performance_monitor(self, dt: float): - """更新性能监控数据""" - try: - # 记录帧时间 - self.performance_monitor['frame_times'].append(dt) - - # 保持最近100帧的数据 - if len(self.performance_monitor['frame_times']) > 100: - self.performance_monitor['frame_times'].pop(0) - - except Exception as e: - print(f"✗ 性能监控更新失败: {e}") - - def _cleanup_cache(self): - """清理过期缓存""" - try: - current_time = time.time() - expired_keys = [] - - for key, (timestamp, _) in self.data_cache.items(): - if current_time - timestamp > self.cache_config['expiration_time']: - expired_keys.append(key) - - for key in expired_keys: - del self.data_cache[key] - - except Exception as e: - print(f"✗ 缓存清理失败: {e}") - - def log_message(self, message: str, level: str = 'INFO'): - """ - 记录日志消息 - - Args: - message: 日志消息 - level: 日志级别 - """ - try: - # 检查日志级别 - levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] - if levels.index(level) < levels.index(self.log_config['level']): - return - - timestamp = time.strftime('%Y-%m-%d %H:%M:%S') - log_entry = f"[{timestamp}] {level}: {message}" - - # 控制台输出 - if self.log_config['console_logging']: - print(log_entry) - - # 文件输出 - if self.log_config['file_logging']: - try: - with open(self.log_config['log_file'], 'a', encoding='utf-8') as f: - f.write(log_entry + '\n') - except Exception as e: - print(f"✗ 日志文件写入失败: {e}") - - except Exception as e: - print(f"✗ 日志记录失败: {e}") - - def set_log_level(self, level: str): - """ - 设置日志级别 - - Args: - level: 日志级别 - """ - valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] - if level in valid_levels: - self.log_config['level'] = level - self.log_message(f"日志级别设置为: {level}") - else: - self.log_message(f"无效的日志级别: {level}", 'WARNING') - - def get_performance_stats(self) -> Dict[str, Any]: - """ - 获取性能统计信息 - - Returns: - 性能统计信息字典 - """ - try: - if not self.performance_monitor['frame_times']: - return {'fps': 0, 'avg_frame_time': 0, 'min_frame_time': 0, 'max_frame_time': 0} - - frame_times = self.performance_monitor['frame_times'] - avg_frame_time = sum(frame_times) / len(frame_times) - fps = 1.0 / avg_frame_time if avg_frame_time > 0 else 0 - - return { - 'fps': fps, - 'avg_frame_time': avg_frame_time, - 'min_frame_time': min(frame_times), - 'max_frame_time': max(frame_times), - 'frame_count': len(frame_times) - } - - except Exception as e: - self.log_message(f"性能统计获取失败: {e}", 'ERROR') - return {} - - def reset_performance_stats(self): - """重置性能统计信息""" - try: - self.performance_monitor['frame_times'].clear() - self.performance_monitor['update_times'].clear() - self.performance_monitor['render_times'].clear() - self.log_message("性能统计信息已重置") - except Exception as e: - self.log_message(f"性能统计重置失败: {e}", 'ERROR') - - def cache_data(self, key: str, data: Any): - """ - 缓存数据 - - Args: - key: 缓存键 - data: 要缓存的数据 - """ - try: - # 检查缓存大小 - if len(self.data_cache) >= self.cache_config['max_size']: - # 移除最旧的条目 - oldest_key = min(self.data_cache.keys(), - key=lambda k: self.data_cache[k][0]) - del self.data_cache[oldest_key] - - # 缓存数据 - self.data_cache[key] = (time.time(), data) - - except Exception as e: - self.log_message(f"数据缓存失败: {e}", 'ERROR') - - def get_cached_data(self, key: str) -> Optional[Any]: - """ - 获取缓存数据 - - Args: - key: 缓存键 - - Returns: - 缓存的数据或None - """ - try: - if key in self.data_cache: - timestamp, data = self.data_cache[key] - if time.time() - timestamp < self.cache_config['expiration_time']: - return data - else: - # 缓存过期,删除条目 - del self.data_cache[key] - return None - - except Exception as e: - self.log_message(f"缓存数据获取失败: {e}", 'ERROR') - return None - - def clear_cache(self): - """清空缓存""" - try: - self.data_cache.clear() - self.log_message("缓存已清空") - except Exception as e: - self.log_message(f"缓存清空失败: {e}", 'ERROR') - - def serialize_data(self, data: Any, filename: str = None) -> str: - """ - 序列化数据 - - Args: - data: 要序列化的数据 - filename: 文件名(可选) - - Returns: - 序列化后的字符串 - """ - try: - if self.serialization_config['format'] == 'json': - serialized_data = json.dumps( - data, - ensure_ascii=False, - indent=2 if self.serialization_config['pretty_print'] else None - ) - - if filename: - with open(filename, 'w', encoding='utf-8') as f: - f.write(serialized_data) - - return serialized_data - - except Exception as e: - self.log_message(f"数据序列化失败: {e}", 'ERROR') - return "" - - def deserialize_data(self, data_str: str) -> Any: - """ - 反序列化数据 - - Args: - data_str: 序列化后的字符串 - - Returns: - 反序列化的数据 - """ - try: - if self.serialization_config['format'] == 'json': - return json.loads(data_str) - - except Exception as e: - self.log_message(f"数据反序列化失败: {e}", 'ERROR') - return None - - def load_data_from_file(self, filename: str) -> Any: - """ - 从文件加载数据 - - Args: - filename: 文件名 - - Returns: - 加载的数据 - """ - try: - with open(filename, 'r', encoding='utf-8') as f: - data_str = f.read() - return self.deserialize_data(data_str) - - except Exception as e: - self.log_message(f"文件数据加载失败: {e}", 'ERROR') - return None - - def save_data_to_file(self, data: Any, filename: str) -> bool: - """ - 保存数据到文件 - - Args: - data: 要保存的数据 - filename: 文件名 - - Returns: - 是否保存成功 - """ - try: - self.serialize_data(data, filename) - self.log_message(f"数据已保存到文件: {filename}") - return True - - except Exception as e: - self.log_message(f"文件数据保存失败: {e}", 'ERROR') - return False - - def generate_hash(self, data: Any) -> str: - """ - 生成数据哈希值 - - Args: - data: 要生成哈希的数据 - - Returns: - 哈希值字符串 - """ - try: - data_str = str(data).encode('utf-8') - return hashlib.md5(data_str).hexdigest() - - except Exception as e: - self.log_message(f"哈希值生成失败: {e}", 'ERROR') - return "" - - def interpolate_values(self, start_value: float, end_value: float, progress: float) -> float: - """ - 插值计算 - - Args: - start_value: 起始值 - end_value: 结束值 - progress: 进度 (0.0-1.0) - - Returns: - 插值结果 - """ - try: - return start_value + (end_value - start_value) * max(0.0, min(1.0, progress)) - - except Exception as e: - self.log_message(f"插值计算失败: {e}", 'ERROR') - return start_value - - def clamp_value(self, value: float, min_value: float, max_value: float) -> float: - """ - 限制值范围 - - Args: - value: 值 - min_value: 最小值 - max_value: 最大值 - - Returns: - 限制后的值 - """ - try: - return max(min_value, min(max_value, value)) - - except Exception as e: - self.log_message(f"值范围限制失败: {e}", 'ERROR') - return value - - def normalize_vector(self, vector: tuple) -> tuple: - """ - 标准化向量 - - Args: - vector: 向量 (x, y, z) - - Returns: - 标准化后的向量 - """ - try: - length = math.sqrt(vector[0]**2 + vector[1]**2 + vector[2]**2) - if length > 0: - return (vector[0] / length, vector[1] / length, vector[2] / length) - return (0, 0, 0) - - except Exception as e: - self.log_message(f"向量标准化失败: {e}", 'ERROR') - return (0, 0, 0) - - def distance_3d(self, pos1: tuple, pos2: tuple) -> float: - """ - 计算3D空间中两点间的距离 - - Args: - pos1: 位置1 (x, y, z) - pos2: 位置2 (x, y, z) - - Returns: - 距离 - """ - try: - return math.sqrt((pos1[0] - pos2[0])**2 + - (pos1[1] - pos2[1])**2 + - (pos1[2] - pos2[2])**2) - - except Exception as e: - self.log_message(f"距离计算失败: {e}", 'ERROR') - return 0.0 - - def random_float(self, min_value: float, max_value: float) -> float: - """ - 生成随机浮点数 - - Args: - min_value: 最小值 - max_value: 最大值 - - Returns: - 随机浮点数 - """ - try: - return self.random_generator.uniform(min_value, max_value) - - except Exception as e: - self.log_message(f"随机数生成失败: {e}", 'ERROR') - return 0.0 - - def random_int(self, min_value: int, max_value: int) -> int: - """ - 生成随机整数 - - Args: - min_value: 最小值 - max_value: 最大值 - - Returns: - 随机整数 - """ - try: - return self.random_generator.randint(min_value, max_value) - - except Exception as e: - self.log_message(f"随机整数生成失败: {e}", 'ERROR') - return 0 - - def set_random_seed(self, seed: int): - """ - 设置随机种子 - - Args: - seed: 随机种子 - """ - try: - self.random_seed = seed - self.random_generator.seed(seed) - self.log_message(f"随机种子设置为: {seed}") - except Exception as e: - self.log_message(f"随机种子设置失败: {e}", 'ERROR') - - def get_system_info(self) -> Dict[str, Any]: - """ - 获取系统信息 - - Returns: - 系统信息字典 - """ - try: - return { - 'plugin_version': getattr(self.plugin, 'version', 'Unknown'), - 'plugin_name': getattr(self.plugin, 'name', 'Unknown'), - 'cache_size': len(self.data_cache), - 'log_level': self.log_config['level'], - 'random_seed': self.random_seed - } - - except Exception as e: - self.log_message(f"系统信息获取失败: {e}", 'ERROR') - return {} - - def create_timer(self) -> Callable[[], float]: - """ - 创建计时器 - - Returns: - 计时器函数,调用时返回经过的时间(秒) - """ - try: - start_time = time.time() - - def get_elapsed_time(): - return time.time() - start_time - - return get_elapsed_time - - except Exception as e: - self.log_message(f"计时器创建失败: {e}", 'ERROR') - - def dummy_timer(): - return 0.0 - - return dummy_timer - - def format_time(self, seconds: float) -> str: - """ - 格式化时间显示 - - Args: - seconds: 秒数 - - Returns: - 格式化后的时间字符串 - """ - try: - hours = int(seconds // 3600) - minutes = int((seconds % 3600) // 60) - secs = int(seconds % 60) - - if hours > 0: - return f"{hours:02d}:{minutes:02d}:{secs:02d}" - else: - return f"{minutes:02d}:{secs:02d}" - - except Exception as e: - self.log_message(f"时间格式化失败: {e}", 'ERROR') - return "00:00" - - def get_memory_usage(self) -> int: - """ - 获取内存使用情况(简化实现) - - Returns: - 内存使用量(字节) - """ - try: - # 简化实现,实际项目中可能需要使用更精确的方法 - import sys - return sys.getsizeof(self) - - except Exception as e: - self.log_message(f"内存使用量获取失败: {e}", 'ERROR') - return 0 - - def benchmark_function(self, func: Callable, *args, **kwargs) -> Dict[str, Any]: - """ - 基准测试函数 - - Args: - func: 要测试的函数 - *args: 函数参数 - **kwargs: 函数关键字参数 - - Returns: - 测试结果字典 - """ - try: - timer = self.create_timer() - result = func(*args, **kwargs) - elapsed_time = timer() - - return { - 'result': result, - 'execution_time': elapsed_time, - 'success': True - } - - except Exception as e: - self.log_message(f"函数基准测试失败: {e}", 'ERROR') - return { - 'result': None, - 'execution_time': 0, - 'success': False, - 'error': str(e) +""" +生态系统工具类 +提供各种实用工具函数和辅助功能 +""" + +import time +import json +import math +import random +from typing import Dict, Any, List, Optional, Callable +import hashlib + +class EcoUtils: + """ + 生态系统工具类 + 提供各种实用工具函数和辅助功能,包括数据处理、性能监控、日志记录等 + """ + + def __init__(self, plugin): + """ + 初始化生态系统工具类 + + Args: + plugin: 植被和生态系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 性能监控 + self.performance_monitor = { + 'frame_times': [], + 'update_times': [], + 'render_times': [], + 'memory_usage': 0, + 'object_counts': {} + } + + # 数据缓存 + self.data_cache = {} + self.cache_config = { + 'max_size': 1000, + 'expiration_time': 300 # 5分钟 + } + + # 日志配置 + self.log_config = { + 'level': 'INFO', + 'file_logging': True, + 'console_logging': True, + 'max_log_size': 10 * 1024 * 1024, # 10MB + 'log_file': 'eco_system.log' + } + + # 随机数生成器 + self.random_generator = random.Random() + self.random_seed = 12345 + + # 数据序列化 + self.serialization_config = { + 'format': 'json', + 'compression': False, + 'pretty_print': True + } + + print("✓ 生态系统工具类已创建") + + def initialize(self) -> bool: + """ + 初始化生态系统工具类 + + Returns: + 是否初始化成功 + """ + try: + # 设置随机种子 + self.random_generator.seed(self.random_seed) + + self.initialized = True + print("✓ 生态系统工具类初始化完成") + return True + + except Exception as e: + print(f"✗ 生态系统工具类初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用生态系统工具类 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 生态系统工具类未初始化") + return False + + self.enabled = True + print("✓ 生态系统工具类已启用") + return True + + except Exception as e: + print(f"✗ 生态系统工具类启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用生态系统工具类""" + try: + self.enabled = False + print("✓ 生态系统工具类已禁用") + + except Exception as e: + print(f"✗ 生态系统工具类禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理生态系统工具类资源""" + try: + self.disable() + self.data_cache.clear() + self.initialized = False + print("✓ 生态系统工具类资源已清理") + + except Exception as e: + print(f"✗ 生态系统工具类资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新工具类状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新性能监控 + self._update_performance_monitor(dt) + + # 清理缓存 + self._cleanup_cache() + + except Exception as e: + print(f"✗ 生态系统工具类更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_performance_monitor(self, dt: float): + """更新性能监控数据""" + try: + # 记录帧时间 + self.performance_monitor['frame_times'].append(dt) + + # 保持最近100帧的数据 + if len(self.performance_monitor['frame_times']) > 100: + self.performance_monitor['frame_times'].pop(0) + + except Exception as e: + print(f"✗ 性能监控更新失败: {e}") + + def _cleanup_cache(self): + """清理过期缓存""" + try: + current_time = time.time() + expired_keys = [] + + for key, (timestamp, _) in self.data_cache.items(): + if current_time - timestamp > self.cache_config['expiration_time']: + expired_keys.append(key) + + for key in expired_keys: + del self.data_cache[key] + + except Exception as e: + print(f"✗ 缓存清理失败: {e}") + + def log_message(self, message: str, level: str = 'INFO'): + """ + 记录日志消息 + + Args: + message: 日志消息 + level: 日志级别 + """ + try: + # 检查日志级别 + levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] + if levels.index(level) < levels.index(self.log_config['level']): + return + + timestamp = time.strftime('%Y-%m-%d %H:%M:%S') + log_entry = f"[{timestamp}] {level}: {message}" + + # 控制台输出 + if self.log_config['console_logging']: + print(log_entry) + + # 文件输出 + if self.log_config['file_logging']: + try: + with open(self.log_config['log_file'], 'a', encoding='utf-8') as f: + f.write(log_entry + '\n') + except Exception as e: + print(f"✗ 日志文件写入失败: {e}") + + except Exception as e: + print(f"✗ 日志记录失败: {e}") + + def set_log_level(self, level: str): + """ + 设置日志级别 + + Args: + level: 日志级别 + """ + valid_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] + if level in valid_levels: + self.log_config['level'] = level + self.log_message(f"日志级别设置为: {level}") + else: + self.log_message(f"无效的日志级别: {level}", 'WARNING') + + def get_performance_stats(self) -> Dict[str, Any]: + """ + 获取性能统计信息 + + Returns: + 性能统计信息字典 + """ + try: + if not self.performance_monitor['frame_times']: + return {'fps': 0, 'avg_frame_time': 0, 'min_frame_time': 0, 'max_frame_time': 0} + + frame_times = self.performance_monitor['frame_times'] + avg_frame_time = sum(frame_times) / len(frame_times) + fps = 1.0 / avg_frame_time if avg_frame_time > 0 else 0 + + return { + 'fps': fps, + 'avg_frame_time': avg_frame_time, + 'min_frame_time': min(frame_times), + 'max_frame_time': max(frame_times), + 'frame_count': len(frame_times) + } + + except Exception as e: + self.log_message(f"性能统计获取失败: {e}", 'ERROR') + return {} + + def reset_performance_stats(self): + """重置性能统计信息""" + try: + self.performance_monitor['frame_times'].clear() + self.performance_monitor['update_times'].clear() + self.performance_monitor['render_times'].clear() + self.log_message("性能统计信息已重置") + except Exception as e: + self.log_message(f"性能统计重置失败: {e}", 'ERROR') + + def cache_data(self, key: str, data: Any): + """ + 缓存数据 + + Args: + key: 缓存键 + data: 要缓存的数据 + """ + try: + # 检查缓存大小 + if len(self.data_cache) >= self.cache_config['max_size']: + # 移除最旧的条目 + oldest_key = min(self.data_cache.keys(), + key=lambda k: self.data_cache[k][0]) + del self.data_cache[oldest_key] + + # 缓存数据 + self.data_cache[key] = (time.time(), data) + + except Exception as e: + self.log_message(f"数据缓存失败: {e}", 'ERROR') + + def get_cached_data(self, key: str) -> Optional[Any]: + """ + 获取缓存数据 + + Args: + key: 缓存键 + + Returns: + 缓存的数据或None + """ + try: + if key in self.data_cache: + timestamp, data = self.data_cache[key] + if time.time() - timestamp < self.cache_config['expiration_time']: + return data + else: + # 缓存过期,删除条目 + del self.data_cache[key] + return None + + except Exception as e: + self.log_message(f"缓存数据获取失败: {e}", 'ERROR') + return None + + def clear_cache(self): + """清空缓存""" + try: + self.data_cache.clear() + self.log_message("缓存已清空") + except Exception as e: + self.log_message(f"缓存清空失败: {e}", 'ERROR') + + def serialize_data(self, data: Any, filename: str = None) -> str: + """ + 序列化数据 + + Args: + data: 要序列化的数据 + filename: 文件名(可选) + + Returns: + 序列化后的字符串 + """ + try: + if self.serialization_config['format'] == 'json': + serialized_data = json.dumps( + data, + ensure_ascii=False, + indent=2 if self.serialization_config['pretty_print'] else None + ) + + if filename: + with open(filename, 'w', encoding='utf-8') as f: + f.write(serialized_data) + + return serialized_data + + except Exception as e: + self.log_message(f"数据序列化失败: {e}", 'ERROR') + return "" + + def deserialize_data(self, data_str: str) -> Any: + """ + 反序列化数据 + + Args: + data_str: 序列化后的字符串 + + Returns: + 反序列化的数据 + """ + try: + if self.serialization_config['format'] == 'json': + return json.loads(data_str) + + except Exception as e: + self.log_message(f"数据反序列化失败: {e}", 'ERROR') + return None + + def load_data_from_file(self, filename: str) -> Any: + """ + 从文件加载数据 + + Args: + filename: 文件名 + + Returns: + 加载的数据 + """ + try: + with open(filename, 'r', encoding='utf-8') as f: + data_str = f.read() + return self.deserialize_data(data_str) + + except Exception as e: + self.log_message(f"文件数据加载失败: {e}", 'ERROR') + return None + + def save_data_to_file(self, data: Any, filename: str) -> bool: + """ + 保存数据到文件 + + Args: + data: 要保存的数据 + filename: 文件名 + + Returns: + 是否保存成功 + """ + try: + self.serialize_data(data, filename) + self.log_message(f"数据已保存到文件: {filename}") + return True + + except Exception as e: + self.log_message(f"文件数据保存失败: {e}", 'ERROR') + return False + + def generate_hash(self, data: Any) -> str: + """ + 生成数据哈希值 + + Args: + data: 要生成哈希的数据 + + Returns: + 哈希值字符串 + """ + try: + data_str = str(data).encode('utf-8') + return hashlib.md5(data_str).hexdigest() + + except Exception as e: + self.log_message(f"哈希值生成失败: {e}", 'ERROR') + return "" + + def interpolate_values(self, start_value: float, end_value: float, progress: float) -> float: + """ + 插值计算 + + Args: + start_value: 起始值 + end_value: 结束值 + progress: 进度 (0.0-1.0) + + Returns: + 插值结果 + """ + try: + return start_value + (end_value - start_value) * max(0.0, min(1.0, progress)) + + except Exception as e: + self.log_message(f"插值计算失败: {e}", 'ERROR') + return start_value + + def clamp_value(self, value: float, min_value: float, max_value: float) -> float: + """ + 限制值范围 + + Args: + value: 值 + min_value: 最小值 + max_value: 最大值 + + Returns: + 限制后的值 + """ + try: + return max(min_value, min(max_value, value)) + + except Exception as e: + self.log_message(f"值范围限制失败: {e}", 'ERROR') + return value + + def normalize_vector(self, vector: tuple) -> tuple: + """ + 标准化向量 + + Args: + vector: 向量 (x, y, z) + + Returns: + 标准化后的向量 + """ + try: + length = math.sqrt(vector[0]**2 + vector[1]**2 + vector[2]**2) + if length > 0: + return (vector[0] / length, vector[1] / length, vector[2] / length) + return (0, 0, 0) + + except Exception as e: + self.log_message(f"向量标准化失败: {e}", 'ERROR') + return (0, 0, 0) + + def distance_3d(self, pos1: tuple, pos2: tuple) -> float: + """ + 计算3D空间中两点间的距离 + + Args: + pos1: 位置1 (x, y, z) + pos2: 位置2 (x, y, z) + + Returns: + 距离 + """ + try: + return math.sqrt((pos1[0] - pos2[0])**2 + + (pos1[1] - pos2[1])**2 + + (pos1[2] - pos2[2])**2) + + except Exception as e: + self.log_message(f"距离计算失败: {e}", 'ERROR') + return 0.0 + + def random_float(self, min_value: float, max_value: float) -> float: + """ + 生成随机浮点数 + + Args: + min_value: 最小值 + max_value: 最大值 + + Returns: + 随机浮点数 + """ + try: + return self.random_generator.uniform(min_value, max_value) + + except Exception as e: + self.log_message(f"随机数生成失败: {e}", 'ERROR') + return 0.0 + + def random_int(self, min_value: int, max_value: int) -> int: + """ + 生成随机整数 + + Args: + min_value: 最小值 + max_value: 最大值 + + Returns: + 随机整数 + """ + try: + return self.random_generator.randint(min_value, max_value) + + except Exception as e: + self.log_message(f"随机整数生成失败: {e}", 'ERROR') + return 0 + + def set_random_seed(self, seed: int): + """ + 设置随机种子 + + Args: + seed: 随机种子 + """ + try: + self.random_seed = seed + self.random_generator.seed(seed) + self.log_message(f"随机种子设置为: {seed}") + except Exception as e: + self.log_message(f"随机种子设置失败: {e}", 'ERROR') + + def get_system_info(self) -> Dict[str, Any]: + """ + 获取系统信息 + + Returns: + 系统信息字典 + """ + try: + return { + 'plugin_version': getattr(self.plugin, 'version', 'Unknown'), + 'plugin_name': getattr(self.plugin, 'name', 'Unknown'), + 'cache_size': len(self.data_cache), + 'log_level': self.log_config['level'], + 'random_seed': self.random_seed + } + + except Exception as e: + self.log_message(f"系统信息获取失败: {e}", 'ERROR') + return {} + + def create_timer(self) -> Callable[[], float]: + """ + 创建计时器 + + Returns: + 计时器函数,调用时返回经过的时间(秒) + """ + try: + start_time = time.time() + + def get_elapsed_time(): + return time.time() - start_time + + return get_elapsed_time + + except Exception as e: + self.log_message(f"计时器创建失败: {e}", 'ERROR') + + def dummy_timer(): + return 0.0 + + return dummy_timer + + def format_time(self, seconds: float) -> str: + """ + 格式化时间显示 + + Args: + seconds: 秒数 + + Returns: + 格式化后的时间字符串 + """ + try: + hours = int(seconds // 3600) + minutes = int((seconds % 3600) // 60) + secs = int(seconds % 60) + + if hours > 0: + return f"{hours:02d}:{minutes:02d}:{secs:02d}" + else: + return f"{minutes:02d}:{secs:02d}" + + except Exception as e: + self.log_message(f"时间格式化失败: {e}", 'ERROR') + return "00:00" + + def get_memory_usage(self) -> int: + """ + 获取内存使用情况(简化实现) + + Returns: + 内存使用量(字节) + """ + try: + # 简化实现,实际项目中可能需要使用更精确的方法 + import sys + return sys.getsizeof(self) + + except Exception as e: + self.log_message(f"内存使用量获取失败: {e}", 'ERROR') + return 0 + + def benchmark_function(self, func: Callable, *args, **kwargs) -> Dict[str, Any]: + """ + 基准测试函数 + + Args: + func: 要测试的函数 + *args: 函数参数 + **kwargs: 函数关键字参数 + + Returns: + 测试结果字典 + """ + try: + timer = self.create_timer() + result = func(*args, **kwargs) + elapsed_time = timer() + + return { + 'result': result, + 'execution_time': elapsed_time, + 'success': True + } + + except Exception as e: + self.log_message(f"函数基准测试失败: {e}", 'ERROR') + return { + 'result': None, + 'execution_time': 0, + 'success': False, + 'error': str(e) } \ No newline at end of file diff --git a/plugins/user/vegetation_ecosystem/vegetation/growth_simulator.py b/plugins/user/vegetation_ecosystem/vegetation/growth_simulator.py index 999702db..080ce1c7 100644 --- a/plugins/user/vegetation_ecosystem/vegetation/growth_simulator.py +++ b/plugins/user/vegetation_ecosystem/vegetation/growth_simulator.py @@ -1,776 +1,776 @@ -""" -生长模拟器 -负责模拟植被的生长、繁殖和死亡过程 -""" - -import time -from typing import Dict, Any, List, Optional -import math -import random - -class GrowthSimulator: - """ - 生长模拟器 - 负责模拟植被的生长、繁殖、死亡和生命周期过程 - """ - - def __init__(self, plugin): - """ - 初始化生长模拟器 - - Args: - plugin: 植被和生态系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 生长模型参数 - self.growth_models = { - 'exponential': { - 'name': '指数生长', - 'description': '快速增长后趋于稳定', - 'formula': 'size = max_size * (1 - exp(-rate * time))' - }, - 'logistic': { - 'name': '逻辑生长', - 'description': 'S型生长曲线', - 'formula': 'size = max_size / (1 + exp(-rate * (time - midpoint)))' - }, - 'linear': { - 'name': '线性生长', - 'description': '恒定速率生长', - 'formula': 'size = rate * time' - }, - 'seasonal': { - 'name': '季节性生长', - 'description': '受季节影响的生长', - 'formula': 'size = base_rate * (1 + amplitude * sin(frequency * time + phase))' - } - } - - # 默认生长参数 - self.default_growth_params = { - 'model': 'logistic', # 默认生长模型 - 'base_rate': 0.1, # 基础生长速率 - 'max_size': 1.0, # 最大尺寸 - 'optimal_conditions': { # 最佳生长条件 - 'temperature': (15, 30), - 'humidity': (50, 80), - 'light': (0.6, 1.0), - 'nutrients': (0.5, 1.0) - }, - 'stress_factors': { # 压力因子 - 'drought_tolerance': 0.3, - 'cold_tolerance': 0.2, - 'heat_tolerance': 0.2, - 'shade_tolerance': 0.4 - } - } - - # 繁殖参数 - self.reproduction_params = { - 'rate': 0.05, # 繁殖速率 - 'method': 'seeds', # 繁殖方式: 'seeds', 'sprouting', 'spores' - 'dispersal_distance': 10.0, # 传播距离 - 'seasonal_factor': 1.0, # 季节性因子 - 'competition_radius': 2.0 # 竞争半径 - } - - # 死亡参数 - self.mortality_params = { - 'natural_lifespan': 365, # 自然寿命(天) - 'stress_mortality': 0.01, # 压力导致的死亡率 - 'disease_factor': 0.05, # 疾病因子 - 'competition_factor': 0.1 # 竞争因子 - } - - # 环境影响因子 - self.environment_impacts = { - 'temperature_impact': 1.0, - 'humidity_impact': 1.0, - 'light_impact': 1.0, - 'nutrient_impact': 1.0, - 'pollution_impact': 0.0 - } - - # 生长阶段定义 - self.growth_stages = { - 'seedling': { - 'name': '幼苗期', - 'size_range': (0.0, 0.2), - 'duration': 30, # 天 - 'vulnerability': 0.8 - }, - 'juvenile': { - 'name': '幼年期', - 'size_range': (0.2, 0.5), - 'duration': 90, - 'vulnerability': 0.5 - }, - 'mature': { - 'name': '成熟期', - 'size_range': (0.5, 0.9), - 'duration': 180, - 'vulnerability': 0.2 - }, - 'old_age': { - 'name': '老年期', - 'size_range': (0.9, 1.0), - 'duration': 90, - 'vulnerability': 0.4 - } - } - - # 时间配置 - self.time_config = { - 'simulation_speed': 1.0, # 模拟速度 - 'update_interval': 60.0, # 更新间隔(秒) - 'last_update': 0.0 - } - - # 统计信息 - self.growth_stats = { - 'total_growth_cycles': 0, - 'successful_reproductions': 0, - 'natural_deaths': 0, - 'stress_deaths': 0, - 'average_growth_rate': 0.0 - } - - print("✓ 生长模拟器已创建") - - def initialize(self) -> bool: - """ - 初始化生长模拟器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 生长模拟器初始化完成") - return True - - except Exception as e: - print(f"✗ 生长模拟器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用生长模拟器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 生长模拟器未初始化") - return False - - self.enabled = True - print("✓ 生长模拟器已启用") - return True - - except Exception as e: - print(f"✗ 生长模拟器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用生长模拟器""" - try: - self.enabled = False - print("✓ 生长模拟器已禁用") - - except Exception as e: - print(f"✗ 生长模拟器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理生长模拟器资源""" - try: - self.disable() - self.initialized = False - print("✓ 生长模拟器资源已清理") - - except Exception as e: - print(f"✗ 生长模拟器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新生长模拟器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新时间 - self.time_config['last_update'] += dt - - # 检查是否需要更新 - if self.time_config['last_update'] >= self.time_config['update_interval']: - # 执行生长更新 - self._update_growth(dt) - - # 重置更新计时器 - self.time_config['last_update'] = 0.0 - - except Exception as e: - print(f"✗ 生长模拟器更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_growth(self, dt: float): - """ - 更新植被生长 - - Args: - dt: 时间增量 - """ - try: - if not self.plugin.vegetation_manager: - return - - # 获取所有植被实例 - instances = self.plugin.vegetation_manager.get_all_vegetation_instances() - - for instance_id, instance in instances.items(): - if not instance['alive']: - continue - - # 获取植被类型信息 - veg_type = instance['type'] - veg_info = self.plugin.vegetation_manager.get_vegetation_info(veg_type) - - if not veg_info: - continue - - # 计算环境影响 - environment_factors = self.plugin.vegetation_manager.get_environment_factors() - environmental_modifier = self._calculate_environmental_modifier(veg_type, environment_factors) - - # 更新年龄 - age_increase = dt / 86400 * self.time_config['simulation_speed'] # 转换为天 - instance['age'] += age_increase - - # 计算生长速率 - growth_rate = veg_info['growth_rate'] * environmental_modifier - - # 更新大小 - size_increase = growth_rate * dt / 86400 - instance['size'] = min(1.0, instance['size'] + size_increase) - - # 更新健康度 - health_change = self._calculate_health_change(instance, veg_info, environment_factors) - self.plugin.vegetation_manager.update_vegetation_health(instance_id, health_change) - - # 检查是否应该繁殖 - if random.random() < veg_info['reproduction_rate'] * dt / 3600: # 每小时检查 - self._attempt_reproduction(instance_id, instance) - - # 检查是否应该死亡 - if self._should_die(instance, veg_info): - self.plugin.vegetation_manager.remove_vegetation_instance(instance_id) - self.growth_stats['natural_deaths'] += 1 - - # 更新统计信息 - self.growth_stats['total_growth_cycles'] += 1 - - except Exception as e: - print(f"✗ 生长更新失败: {e}") - - def _calculate_environmental_modifier(self, veg_type: str, environment_factors: Dict[str, float]) -> float: - """ - 计算环境影响因子 - - Args: - veg_type: 植被类型 - environment_factors: 环境因子 - - Returns: - 环境影响因子 (0.0-2.0) - """ - try: - if veg_type not in self.plugin.vegetation_manager.vegetation_types: - return 1.0 - - veg_info = self.plugin.vegetation_manager.vegetation_types[veg_type] - preferred_conditions = veg_info['preferred_conditions'] - - # 计算各因子的适宜性 - temperature_suitability = self._calculate_suitability( - environment_factors['temperature'], - preferred_conditions['temperature'] - ) - - humidity_suitability = self._calculate_suitability( - environment_factors['humidity'], - preferred_conditions['humidity'] - ) - - # 综合环境影响因子 - modifier = (temperature_suitability + humidity_suitability) / 2.0 - - # 应用环境影响 - for factor, impact in self.environment_impacts.items(): - modifier *= (1.0 + impact * 0.1) # 轻微影响 - - return max(0.0, min(2.0, modifier)) - - except Exception as e: - print(f"✗ 环境影响因子计算失败: {e}") - return 1.0 - - def _calculate_suitability(self, current_value: float, preferred_range: tuple) -> float: - """ - 计算环境因子适宜性 - - Args: - current_value: 当前值 - preferred_range: 偏好范围 - - Returns: - 适宜性评分 (0.0-1.0) - """ - try: - min_val, max_val = preferred_range - - if min_val <= current_value <= max_val: - return 1.0 - elif current_value < min_val: - if min_val > 0: - return max(0.0, 1.0 - (min_val - current_value) / min_val) - return 0.0 - else: # current_value > max_val - if max_val > 0: - return max(0.0, 1.0 - (current_value - max_val) / max_val) - return 0.0 - - except Exception as e: - print(f"✗ 适宜性计算失败: {e}") - return 0.5 - - def _calculate_health_change(self, instance: Dict[str, Any], veg_info: Dict[str, Any], - environment_factors: Dict[str, float]) -> float: - """ - 计算健康度变化 - - Args: - instance: 植被实例 - veg_info: 植被信息 - environment_factors: 环境因子 - - Returns: - 健康度变化值 - """ - try: - health_change = 0.0 - - # 基于年龄的影响 - age_factor = self._calculate_age_factor(instance['age'], veg_info['lifespan']) - health_change += age_factor - - # 基于环境的压力 - stress_factor = self._calculate_stress_factor(environment_factors, veg_info) - health_change += stress_factor - - # 自然恢复 - health_change += 0.001 # 缓慢恢复 - - return max(-0.1, min(0.1, health_change)) - - except Exception as e: - print(f"✗ 健康度变化计算失败: {e}") - return 0.0 - - def _calculate_age_factor(self, age: float, lifespan: float) -> float: - """ - 计算年龄因子对健康的影响 - - Args: - age: 年龄 - lifespan: 寿命 - - Returns: - 年龄因子 - """ - try: - if lifespan <= 0: - return 0.0 - - age_ratio = age / lifespan - - # 幼年期增长,成年期稳定,老年期下降 - if age_ratio < 0.2: # 幼年期 - return 0.02 # 快速增长 - elif age_ratio < 0.8: # 成年期 - return 0.005 # 缓慢增长 - else: # 老年期 - return -0.03 # 快速下降 - - except Exception as e: - print(f"✗ 年龄因子计算失败: {e}") - return 0.0 - - def _calculate_stress_factor(self, environment_factors: Dict[str, float], veg_info: Dict[str, Any]) -> float: - """ - 计算环境压力因子 - - Args: - environment_factors: 环境因子 - veg_info: 植被信息 - - Returns: - 压力因子 - """ - try: - stress = 0.0 - - # 温度压力 - temp = environment_factors['temperature'] - preferred_temp = veg_info['preferred_conditions']['temperature'] - if temp < preferred_temp[0] or temp > preferred_temp[1]: - stress -= 0.01 - - # 湿度压力 - humidity = environment_factors['humidity'] - preferred_humidity = veg_info['preferred_conditions']['humidity'] - if humidity < preferred_humidity[0] or humidity > preferred_humidity[1]: - stress -= 0.01 - - # 光照压力 - light = environment_factors['light_level'] - # 假设光照适宜范围是0.5-1.0 - if light < 0.5: - stress -= 0.005 - - return stress - - except Exception as e: - print(f"✗ 压力因子计算失败: {e}") - return 0.0 - - def _should_die(self, instance: Dict[str, Any], veg_info: Dict[str, Any]) -> bool: - """ - 判断植被是否应该死亡 - - Args: - instance: 植被实例 - veg_info: 植被信息 - - Returns: - 是否应该死亡 - """ - try: - # 年龄导致的死亡 - if instance['age'] > veg_info['lifespan']: - return True - - # 健康度过低导致的死亡 - if instance['health'] <= 0.0: - return True - - # 压力导致的死亡 - stress_death_probability = 0.001 * (1.0 - instance['health']) - if random.random() < stress_death_probability: - return True - - return False - - except Exception as e: - print(f"✗ 死亡判断失败: {e}") - return False - - def _attempt_reproduction(self, parent_id: int, parent_instance: Dict[str, Any]): - """ - 尝试繁殖 - - Args: - parent_id: 父代ID - parent_instance: 父代实例 - """ - try: - if not self.plugin.vegetation_manager: - return - - # 检查繁殖条件 - if parent_instance['health'] < 0.3: # 健康度过低无法繁殖 - return - - if parent_instance['size'] < 0.2: # 太小无法繁殖 - return - - # 计算繁殖位置(在父代附近) - parent_pos = parent_instance['position'] - dispersal_distance = self.reproduction_params['dispersal_distance'] - - angle = random.uniform(0, 2 * math.pi) - distance = random.uniform(0, dispersal_distance) - - new_x = parent_pos[0] + distance * math.cos(angle) - new_z = parent_pos[2] + distance * math.sin(angle) - - # 检查竞争 - if self._check_competition(new_x, new_z): - return - - # 创建新实例 - new_instance_id = self.plugin.vegetation_manager.create_vegetation_instance( - parent_instance['type'], - (new_x, parent_pos[1], new_z), - initial_age=0.0, - initial_health=0.5 - ) - - if new_instance_id >= 0: - self.growth_stats['successful_reproductions'] += 1 - - except Exception as e: - print(f"✗ 繁殖尝试失败: {e}") - - def _check_competition(self, x: float, z: float) -> bool: - """ - 检查新位置的竞争情况 - - Args: - x: X坐标 - z: Z坐标 - - Returns: - 是否存在竞争 - """ - try: - if not self.plugin.vegetation_manager: - return False - - instances = self.plugin.vegetation_manager.get_all_vegetation_instances() - competition_radius = self.reproduction_params['competition_radius'] - - for instance in instances.values(): - if not instance['alive']: - continue - - instance_x, _, instance_z = instance['position'] - distance = math.sqrt((x - instance_x)**2 + (z - instance_z)**2) - - if distance < competition_radius: - return True # 存在竞争 - - return False - - except Exception as e: - print(f"✗ 竞争检查失败: {e}") - return False - - def get_growth_models(self) -> Dict[str, Dict[str, str]]: - """ - 获取生长模型 - - Returns: - 生长模型字典 - """ - return self.growth_models.copy() - - def set_growth_model(self, model: str): - """ - 设置生长模型 - - Args: - model: 模型名称 - """ - try: - if model in self.growth_models: - self.default_growth_params['model'] = model - print(f"✓ 生长模型设置为: {self.growth_models[model]['name']}") - else: - print(f"✗ 无效的生长模型: {model}") - - except Exception as e: - print(f"✗ 生长模型设置失败: {e}") - - def set_growth_parameter(self, parameter: str, value: Any): - """ - 设置生长参数 - - Args: - parameter: 参数名称 - value: 参数值 - """ - try: - if parameter in self.default_growth_params: - self.default_growth_params[parameter] = value - print(f"✓ 生长参数已设置: {parameter} = {value}") - else: - print(f"✗ 无效的生长参数: {parameter}") - - except Exception as e: - print(f"✗ 生长参数设置失败: {e}") - - def get_growth_parameters(self) -> Dict[str, Any]: - """ - 获取生长参数 - - Returns: - 生长参数字典 - """ - return self.default_growth_params.copy() - - def set_reproduction_parameter(self, parameter: str, value: Any): - """ - 设置繁殖参数 - - Args: - parameter: 参数名称 - value: 参数值 - """ - try: - if parameter in self.reproduction_params: - self.reproduction_params[parameter] = value - print(f"✓ 繁殖参数已设置: {parameter} = {value}") - else: - print(f"✗ 无效的繁殖参数: {parameter}") - - except Exception as e: - print(f"✗ 繁殖参数设置失败: {e}") - - def get_reproduction_parameters(self) -> Dict[str, Any]: - """ - 获取繁殖参数 - - Returns: - 繁殖参数字典 - """ - return self.reproduction_params.copy() - - def set_mortality_parameter(self, parameter: str, value: Any): - """ - 设置死亡参数 - - Args: - parameter: 参数名称 - value: 参数值 - """ - try: - if parameter in self.mortality_params: - self.mortality_params[parameter] = value - print(f"✓ 死亡参数已设置: {parameter} = {value}") - else: - print(f"✗ 无效的死亡参数: {parameter}") - - except Exception as e: - print(f"✗ 死亡参数设置失败: {e}") - - def get_mortality_parameters(self) -> Dict[str, Any]: - """ - 获取死亡参数 - - Returns: - 死亡参数字典 - """ - return self.mortality_params.copy() - - def set_environment_impact(self, factor: str, impact: float): - """ - 设置环境影响因子 - - Args: - factor: 影响因子名称 - impact: 影响值 - """ - try: - if factor in self.environment_impacts: - self.environment_impacts[factor] = impact - print(f"✓ 环境影响因子已设置: {factor} = {impact}") - else: - print(f"✗ 无效的环境影响因子: {factor}") - - except Exception as e: - print(f"✗ 环境影响因子设置失败: {e}") - - def get_environment_impacts(self) -> Dict[str, float]: - """ - 获取环境影响因子 - - Returns: - 环境影响因子字典 - """ - return self.environment_impacts.copy() - - def set_time_config(self, config: Dict[str, float]): - """ - 设置时间配置 - - Args: - config: 时间配置字典 - """ - try: - self.time_config.update(config) - print(f"✓ 时间配置已更新: {self.time_config}") - except Exception as e: - print(f"✗ 时间配置更新失败: {e}") - - def get_time_config(self) -> Dict[str, float]: - """ - 获取时间配置 - - Returns: - 时间配置字典 - """ - return self.time_config.copy() - - def get_growth_stats(self) -> Dict[str, Any]: - """ - 获取生长统计信息 - - Returns: - 生长统计信息字典 - """ - return self.growth_stats.copy() - - def reset_growth_stats(self): - """重置生长统计信息""" - try: - self.growth_stats = { - 'total_growth_cycles': 0, - 'successful_reproductions': 0, - 'natural_deaths': 0, - 'stress_deaths': 0, - 'average_growth_rate': 0.0 - } - print("✓ 生长统计信息已重置") - except Exception as e: - print(f"✗ 生长统计信息重置失败: {e}") - - def simulate_seasonal_growth(self, season: str) -> float: - """ - 模拟季节性生长 - - Args: - season: 季节名称 - - Returns: - 季节性生长修正因子 - """ - try: - seasonal_factors = { - 'spring': 1.5, # 春季生长旺盛 - 'summer': 1.2, # 夏季生长良好 - 'autumn': 0.8, # 秋季生长减缓 - 'winter': 0.3 # 冬季生长缓慢 - } - - return seasonal_factors.get(season, 1.0) - - except Exception as e: - print(f"✗ 季节性生长模拟失败: {e}") +""" +生长模拟器 +负责模拟植被的生长、繁殖和死亡过程 +""" + +import time +from typing import Dict, Any, List, Optional +import math +import random + +class GrowthSimulator: + """ + 生长模拟器 + 负责模拟植被的生长、繁殖、死亡和生命周期过程 + """ + + def __init__(self, plugin): + """ + 初始化生长模拟器 + + Args: + plugin: 植被和生态系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 生长模型参数 + self.growth_models = { + 'exponential': { + 'name': '指数生长', + 'description': '快速增长后趋于稳定', + 'formula': 'size = max_size * (1 - exp(-rate * time))' + }, + 'logistic': { + 'name': '逻辑生长', + 'description': 'S型生长曲线', + 'formula': 'size = max_size / (1 + exp(-rate * (time - midpoint)))' + }, + 'linear': { + 'name': '线性生长', + 'description': '恒定速率生长', + 'formula': 'size = rate * time' + }, + 'seasonal': { + 'name': '季节性生长', + 'description': '受季节影响的生长', + 'formula': 'size = base_rate * (1 + amplitude * sin(frequency * time + phase))' + } + } + + # 默认生长参数 + self.default_growth_params = { + 'model': 'logistic', # 默认生长模型 + 'base_rate': 0.1, # 基础生长速率 + 'max_size': 1.0, # 最大尺寸 + 'optimal_conditions': { # 最佳生长条件 + 'temperature': (15, 30), + 'humidity': (50, 80), + 'light': (0.6, 1.0), + 'nutrients': (0.5, 1.0) + }, + 'stress_factors': { # 压力因子 + 'drought_tolerance': 0.3, + 'cold_tolerance': 0.2, + 'heat_tolerance': 0.2, + 'shade_tolerance': 0.4 + } + } + + # 繁殖参数 + self.reproduction_params = { + 'rate': 0.05, # 繁殖速率 + 'method': 'seeds', # 繁殖方式: 'seeds', 'sprouting', 'spores' + 'dispersal_distance': 10.0, # 传播距离 + 'seasonal_factor': 1.0, # 季节性因子 + 'competition_radius': 2.0 # 竞争半径 + } + + # 死亡参数 + self.mortality_params = { + 'natural_lifespan': 365, # 自然寿命(天) + 'stress_mortality': 0.01, # 压力导致的死亡率 + 'disease_factor': 0.05, # 疾病因子 + 'competition_factor': 0.1 # 竞争因子 + } + + # 环境影响因子 + self.environment_impacts = { + 'temperature_impact': 1.0, + 'humidity_impact': 1.0, + 'light_impact': 1.0, + 'nutrient_impact': 1.0, + 'pollution_impact': 0.0 + } + + # 生长阶段定义 + self.growth_stages = { + 'seedling': { + 'name': '幼苗期', + 'size_range': (0.0, 0.2), + 'duration': 30, # 天 + 'vulnerability': 0.8 + }, + 'juvenile': { + 'name': '幼年期', + 'size_range': (0.2, 0.5), + 'duration': 90, + 'vulnerability': 0.5 + }, + 'mature': { + 'name': '成熟期', + 'size_range': (0.5, 0.9), + 'duration': 180, + 'vulnerability': 0.2 + }, + 'old_age': { + 'name': '老年期', + 'size_range': (0.9, 1.0), + 'duration': 90, + 'vulnerability': 0.4 + } + } + + # 时间配置 + self.time_config = { + 'simulation_speed': 1.0, # 模拟速度 + 'update_interval': 60.0, # 更新间隔(秒) + 'last_update': 0.0 + } + + # 统计信息 + self.growth_stats = { + 'total_growth_cycles': 0, + 'successful_reproductions': 0, + 'natural_deaths': 0, + 'stress_deaths': 0, + 'average_growth_rate': 0.0 + } + + print("✓ 生长模拟器已创建") + + def initialize(self) -> bool: + """ + 初始化生长模拟器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 生长模拟器初始化完成") + return True + + except Exception as e: + print(f"✗ 生长模拟器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用生长模拟器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 生长模拟器未初始化") + return False + + self.enabled = True + print("✓ 生长模拟器已启用") + return True + + except Exception as e: + print(f"✗ 生长模拟器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用生长模拟器""" + try: + self.enabled = False + print("✓ 生长模拟器已禁用") + + except Exception as e: + print(f"✗ 生长模拟器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理生长模拟器资源""" + try: + self.disable() + self.initialized = False + print("✓ 生长模拟器资源已清理") + + except Exception as e: + print(f"✗ 生长模拟器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新生长模拟器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新时间 + self.time_config['last_update'] += dt + + # 检查是否需要更新 + if self.time_config['last_update'] >= self.time_config['update_interval']: + # 执行生长更新 + self._update_growth(dt) + + # 重置更新计时器 + self.time_config['last_update'] = 0.0 + + except Exception as e: + print(f"✗ 生长模拟器更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_growth(self, dt: float): + """ + 更新植被生长 + + Args: + dt: 时间增量 + """ + try: + if not self.plugin.vegetation_manager: + return + + # 获取所有植被实例 + instances = self.plugin.vegetation_manager.get_all_vegetation_instances() + + for instance_id, instance in instances.items(): + if not instance['alive']: + continue + + # 获取植被类型信息 + veg_type = instance['type'] + veg_info = self.plugin.vegetation_manager.get_vegetation_info(veg_type) + + if not veg_info: + continue + + # 计算环境影响 + environment_factors = self.plugin.vegetation_manager.get_environment_factors() + environmental_modifier = self._calculate_environmental_modifier(veg_type, environment_factors) + + # 更新年龄 + age_increase = dt / 86400 * self.time_config['simulation_speed'] # 转换为天 + instance['age'] += age_increase + + # 计算生长速率 + growth_rate = veg_info['growth_rate'] * environmental_modifier + + # 更新大小 + size_increase = growth_rate * dt / 86400 + instance['size'] = min(1.0, instance['size'] + size_increase) + + # 更新健康度 + health_change = self._calculate_health_change(instance, veg_info, environment_factors) + self.plugin.vegetation_manager.update_vegetation_health(instance_id, health_change) + + # 检查是否应该繁殖 + if random.random() < veg_info['reproduction_rate'] * dt / 3600: # 每小时检查 + self._attempt_reproduction(instance_id, instance) + + # 检查是否应该死亡 + if self._should_die(instance, veg_info): + self.plugin.vegetation_manager.remove_vegetation_instance(instance_id) + self.growth_stats['natural_deaths'] += 1 + + # 更新统计信息 + self.growth_stats['total_growth_cycles'] += 1 + + except Exception as e: + print(f"✗ 生长更新失败: {e}") + + def _calculate_environmental_modifier(self, veg_type: str, environment_factors: Dict[str, float]) -> float: + """ + 计算环境影响因子 + + Args: + veg_type: 植被类型 + environment_factors: 环境因子 + + Returns: + 环境影响因子 (0.0-2.0) + """ + try: + if veg_type not in self.plugin.vegetation_manager.vegetation_types: + return 1.0 + + veg_info = self.plugin.vegetation_manager.vegetation_types[veg_type] + preferred_conditions = veg_info['preferred_conditions'] + + # 计算各因子的适宜性 + temperature_suitability = self._calculate_suitability( + environment_factors['temperature'], + preferred_conditions['temperature'] + ) + + humidity_suitability = self._calculate_suitability( + environment_factors['humidity'], + preferred_conditions['humidity'] + ) + + # 综合环境影响因子 + modifier = (temperature_suitability + humidity_suitability) / 2.0 + + # 应用环境影响 + for factor, impact in self.environment_impacts.items(): + modifier *= (1.0 + impact * 0.1) # 轻微影响 + + return max(0.0, min(2.0, modifier)) + + except Exception as e: + print(f"✗ 环境影响因子计算失败: {e}") + return 1.0 + + def _calculate_suitability(self, current_value: float, preferred_range: tuple) -> float: + """ + 计算环境因子适宜性 + + Args: + current_value: 当前值 + preferred_range: 偏好范围 + + Returns: + 适宜性评分 (0.0-1.0) + """ + try: + min_val, max_val = preferred_range + + if min_val <= current_value <= max_val: + return 1.0 + elif current_value < min_val: + if min_val > 0: + return max(0.0, 1.0 - (min_val - current_value) / min_val) + return 0.0 + else: # current_value > max_val + if max_val > 0: + return max(0.0, 1.0 - (current_value - max_val) / max_val) + return 0.0 + + except Exception as e: + print(f"✗ 适宜性计算失败: {e}") + return 0.5 + + def _calculate_health_change(self, instance: Dict[str, Any], veg_info: Dict[str, Any], + environment_factors: Dict[str, float]) -> float: + """ + 计算健康度变化 + + Args: + instance: 植被实例 + veg_info: 植被信息 + environment_factors: 环境因子 + + Returns: + 健康度变化值 + """ + try: + health_change = 0.0 + + # 基于年龄的影响 + age_factor = self._calculate_age_factor(instance['age'], veg_info['lifespan']) + health_change += age_factor + + # 基于环境的压力 + stress_factor = self._calculate_stress_factor(environment_factors, veg_info) + health_change += stress_factor + + # 自然恢复 + health_change += 0.001 # 缓慢恢复 + + return max(-0.1, min(0.1, health_change)) + + except Exception as e: + print(f"✗ 健康度变化计算失败: {e}") + return 0.0 + + def _calculate_age_factor(self, age: float, lifespan: float) -> float: + """ + 计算年龄因子对健康的影响 + + Args: + age: 年龄 + lifespan: 寿命 + + Returns: + 年龄因子 + """ + try: + if lifespan <= 0: + return 0.0 + + age_ratio = age / lifespan + + # 幼年期增长,成年期稳定,老年期下降 + if age_ratio < 0.2: # 幼年期 + return 0.02 # 快速增长 + elif age_ratio < 0.8: # 成年期 + return 0.005 # 缓慢增长 + else: # 老年期 + return -0.03 # 快速下降 + + except Exception as e: + print(f"✗ 年龄因子计算失败: {e}") + return 0.0 + + def _calculate_stress_factor(self, environment_factors: Dict[str, float], veg_info: Dict[str, Any]) -> float: + """ + 计算环境压力因子 + + Args: + environment_factors: 环境因子 + veg_info: 植被信息 + + Returns: + 压力因子 + """ + try: + stress = 0.0 + + # 温度压力 + temp = environment_factors['temperature'] + preferred_temp = veg_info['preferred_conditions']['temperature'] + if temp < preferred_temp[0] or temp > preferred_temp[1]: + stress -= 0.01 + + # 湿度压力 + humidity = environment_factors['humidity'] + preferred_humidity = veg_info['preferred_conditions']['humidity'] + if humidity < preferred_humidity[0] or humidity > preferred_humidity[1]: + stress -= 0.01 + + # 光照压力 + light = environment_factors['light_level'] + # 假设光照适宜范围是0.5-1.0 + if light < 0.5: + stress -= 0.005 + + return stress + + except Exception as e: + print(f"✗ 压力因子计算失败: {e}") + return 0.0 + + def _should_die(self, instance: Dict[str, Any], veg_info: Dict[str, Any]) -> bool: + """ + 判断植被是否应该死亡 + + Args: + instance: 植被实例 + veg_info: 植被信息 + + Returns: + 是否应该死亡 + """ + try: + # 年龄导致的死亡 + if instance['age'] > veg_info['lifespan']: + return True + + # 健康度过低导致的死亡 + if instance['health'] <= 0.0: + return True + + # 压力导致的死亡 + stress_death_probability = 0.001 * (1.0 - instance['health']) + if random.random() < stress_death_probability: + return True + + return False + + except Exception as e: + print(f"✗ 死亡判断失败: {e}") + return False + + def _attempt_reproduction(self, parent_id: int, parent_instance: Dict[str, Any]): + """ + 尝试繁殖 + + Args: + parent_id: 父代ID + parent_instance: 父代实例 + """ + try: + if not self.plugin.vegetation_manager: + return + + # 检查繁殖条件 + if parent_instance['health'] < 0.3: # 健康度过低无法繁殖 + return + + if parent_instance['size'] < 0.2: # 太小无法繁殖 + return + + # 计算繁殖位置(在父代附近) + parent_pos = parent_instance['position'] + dispersal_distance = self.reproduction_params['dispersal_distance'] + + angle = random.uniform(0, 2 * math.pi) + distance = random.uniform(0, dispersal_distance) + + new_x = parent_pos[0] + distance * math.cos(angle) + new_z = parent_pos[2] + distance * math.sin(angle) + + # 检查竞争 + if self._check_competition(new_x, new_z): + return + + # 创建新实例 + new_instance_id = self.plugin.vegetation_manager.create_vegetation_instance( + parent_instance['type'], + (new_x, parent_pos[1], new_z), + initial_age=0.0, + initial_health=0.5 + ) + + if new_instance_id >= 0: + self.growth_stats['successful_reproductions'] += 1 + + except Exception as e: + print(f"✗ 繁殖尝试失败: {e}") + + def _check_competition(self, x: float, z: float) -> bool: + """ + 检查新位置的竞争情况 + + Args: + x: X坐标 + z: Z坐标 + + Returns: + 是否存在竞争 + """ + try: + if not self.plugin.vegetation_manager: + return False + + instances = self.plugin.vegetation_manager.get_all_vegetation_instances() + competition_radius = self.reproduction_params['competition_radius'] + + for instance in instances.values(): + if not instance['alive']: + continue + + instance_x, _, instance_z = instance['position'] + distance = math.sqrt((x - instance_x)**2 + (z - instance_z)**2) + + if distance < competition_radius: + return True # 存在竞争 + + return False + + except Exception as e: + print(f"✗ 竞争检查失败: {e}") + return False + + def get_growth_models(self) -> Dict[str, Dict[str, str]]: + """ + 获取生长模型 + + Returns: + 生长模型字典 + """ + return self.growth_models.copy() + + def set_growth_model(self, model: str): + """ + 设置生长模型 + + Args: + model: 模型名称 + """ + try: + if model in self.growth_models: + self.default_growth_params['model'] = model + print(f"✓ 生长模型设置为: {self.growth_models[model]['name']}") + else: + print(f"✗ 无效的生长模型: {model}") + + except Exception as e: + print(f"✗ 生长模型设置失败: {e}") + + def set_growth_parameter(self, parameter: str, value: Any): + """ + 设置生长参数 + + Args: + parameter: 参数名称 + value: 参数值 + """ + try: + if parameter in self.default_growth_params: + self.default_growth_params[parameter] = value + print(f"✓ 生长参数已设置: {parameter} = {value}") + else: + print(f"✗ 无效的生长参数: {parameter}") + + except Exception as e: + print(f"✗ 生长参数设置失败: {e}") + + def get_growth_parameters(self) -> Dict[str, Any]: + """ + 获取生长参数 + + Returns: + 生长参数字典 + """ + return self.default_growth_params.copy() + + def set_reproduction_parameter(self, parameter: str, value: Any): + """ + 设置繁殖参数 + + Args: + parameter: 参数名称 + value: 参数值 + """ + try: + if parameter in self.reproduction_params: + self.reproduction_params[parameter] = value + print(f"✓ 繁殖参数已设置: {parameter} = {value}") + else: + print(f"✗ 无效的繁殖参数: {parameter}") + + except Exception as e: + print(f"✗ 繁殖参数设置失败: {e}") + + def get_reproduction_parameters(self) -> Dict[str, Any]: + """ + 获取繁殖参数 + + Returns: + 繁殖参数字典 + """ + return self.reproduction_params.copy() + + def set_mortality_parameter(self, parameter: str, value: Any): + """ + 设置死亡参数 + + Args: + parameter: 参数名称 + value: 参数值 + """ + try: + if parameter in self.mortality_params: + self.mortality_params[parameter] = value + print(f"✓ 死亡参数已设置: {parameter} = {value}") + else: + print(f"✗ 无效的死亡参数: {parameter}") + + except Exception as e: + print(f"✗ 死亡参数设置失败: {e}") + + def get_mortality_parameters(self) -> Dict[str, Any]: + """ + 获取死亡参数 + + Returns: + 死亡参数字典 + """ + return self.mortality_params.copy() + + def set_environment_impact(self, factor: str, impact: float): + """ + 设置环境影响因子 + + Args: + factor: 影响因子名称 + impact: 影响值 + """ + try: + if factor in self.environment_impacts: + self.environment_impacts[factor] = impact + print(f"✓ 环境影响因子已设置: {factor} = {impact}") + else: + print(f"✗ 无效的环境影响因子: {factor}") + + except Exception as e: + print(f"✗ 环境影响因子设置失败: {e}") + + def get_environment_impacts(self) -> Dict[str, float]: + """ + 获取环境影响因子 + + Returns: + 环境影响因子字典 + """ + return self.environment_impacts.copy() + + def set_time_config(self, config: Dict[str, float]): + """ + 设置时间配置 + + Args: + config: 时间配置字典 + """ + try: + self.time_config.update(config) + print(f"✓ 时间配置已更新: {self.time_config}") + except Exception as e: + print(f"✗ 时间配置更新失败: {e}") + + def get_time_config(self) -> Dict[str, float]: + """ + 获取时间配置 + + Returns: + 时间配置字典 + """ + return self.time_config.copy() + + def get_growth_stats(self) -> Dict[str, Any]: + """ + 获取生长统计信息 + + Returns: + 生长统计信息字典 + """ + return self.growth_stats.copy() + + def reset_growth_stats(self): + """重置生长统计信息""" + try: + self.growth_stats = { + 'total_growth_cycles': 0, + 'successful_reproductions': 0, + 'natural_deaths': 0, + 'stress_deaths': 0, + 'average_growth_rate': 0.0 + } + print("✓ 生长统计信息已重置") + except Exception as e: + print(f"✗ 生长统计信息重置失败: {e}") + + def simulate_seasonal_growth(self, season: str) -> float: + """ + 模拟季节性生长 + + Args: + season: 季节名称 + + Returns: + 季节性生长修正因子 + """ + try: + seasonal_factors = { + 'spring': 1.5, # 春季生长旺盛 + 'summer': 1.2, # 夏季生长良好 + 'autumn': 0.8, # 秋季生长减缓 + 'winter': 0.3 # 冬季生长缓慢 + } + + return seasonal_factors.get(season, 1.0) + + except Exception as e: + print(f"✗ 季节性生长模拟失败: {e}") return 1.0 \ No newline at end of file diff --git a/plugins/user/vegetation_ecosystem/vegetation/vegetation_spawner.py b/plugins/user/vegetation_ecosystem/vegetation/vegetation_spawner.py index 5013268b..bed09c02 100644 --- a/plugins/user/vegetation_ecosystem/vegetation/vegetation_spawner.py +++ b/plugins/user/vegetation_ecosystem/vegetation/vegetation_spawner.py @@ -1,789 +1,789 @@ -""" -植被生成器 -负责根据环境条件和生态规则生成植被分布 -""" - -import time -from typing import Dict, Any, List, Optional, Tuple -import math -import random - -class VegetationSpawner: - """ - 植被生成器 - 负责根据环境条件、地形特征和生态规则生成植被分布 - """ - - def __init__(self, plugin): - """ - 初始化植被生成器 - - Args: - plugin: 植被和生态系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 植被分布算法 - self.distribution_algorithms = { - 'random': { - 'name': '随机分布', - 'description': '完全随机的植被分布' - }, - 'clustered': { - 'name': '聚集分布', - 'description': '植被以群落形式聚集分布' - }, - 'gradient': { - 'name': '梯度分布', - 'description': '根据环境因子梯度分布植被' - }, - 'competitive': { - 'name': '竞争分布', - 'description': '考虑植物间竞争关系的分布' - }, - 'environmental': { - 'name': '环境适应分布', - 'description': '基于环境适应性的分布' - } - } - - # 生成配置 - self.spawn_config = { - 'algorithm': 'environmental', # 默认算法 - 'density': 1.0, # 植被密度 - 'clumping_factor': 0.7, # 聚集因子 - 'random_seed': 12345, # 随机种子 - 'spawn_area': ((-100, -100), (100, 100)), # 生成区域 - 'max_instances': 10000 # 最大实例数 - } - - # 环境因子权重 - self.environment_weights = { - 'temperature': 0.25, - 'humidity': 0.25, - 'soil_fertility': 0.25, - 'light_level': 0.15, - 'water_level': 0.10 - } - - # 植被生态位偏好 - self.ecological_preferences = { - 'grass': { - 'temperature_preference': (10, 30), - 'humidity_preference': (40, 80), - 'fertility_preference': (0.3, 1.0), - 'light_preference': (0.5, 1.0), - 'water_preference': (0.4, 0.8) - }, - 'bush': { - 'temperature_preference': (5, 25), - 'humidity_preference': (30, 70), - 'fertility_preference': (0.4, 1.0), - 'light_preference': (0.6, 1.0), - 'water_preference': (0.3, 0.7) - }, - 'tree': { - 'temperature_preference': (0, 30), - 'humidity_preference': (40, 90), - 'fertility_preference': (0.5, 1.0), - 'light_preference': (0.7, 1.0), - 'water_preference': (0.5, 1.0) - }, - 'flower': { - 'temperature_preference': (15, 25), - 'humidity_preference': (50, 80), - 'fertility_preference': (0.6, 1.0), - 'light_preference': (0.8, 1.0), - 'water_preference': (0.6, 0.9) - }, - 'fern': { - 'temperature_preference': (10, 20), - 'humidity_preference': (70, 100), - 'fertility_preference': (0.4, 0.8), - 'light_preference': (0.2, 0.6), - 'water_preference': (0.7, 1.0) - }, - 'cactus': { - 'temperature_preference': (20, 40), - 'humidity_preference': (10, 30), - 'fertility_preference': (0.1, 0.5), - 'light_preference': (0.8, 1.0), - 'water_preference': (0.1, 0.3) - }, - 'moss': { - 'temperature_preference': (5, 20), - 'humidity_preference': (80, 100), - 'fertility_preference': (0.1, 0.6), - 'light_preference': (0.1, 0.4), - 'water_preference': (0.8, 1.0) - }, - 'vine': { - 'temperature_preference': (15, 30), - 'humidity_preference': (60, 90), - 'fertility_preference': (0.3, 0.8), - 'light_preference': (0.4, 0.8), - 'water_preference': (0.6, 0.9) - } - } - - # 空间分布参数 - self.spatial_parameters = { - 'min_distance': 0.5, # 最小间距 - 'max_cluster_size': 10, # 最大集群大小 - 'cluster_radius': 5.0, # 集群半径 - 'edge_blending': 0.1 # 边缘混合 - } - - # 生成统计 - self.spawn_stats = { - 'total_spawned': 0, - 'spawned_by_type': {}, - 'failed_spawns': 0, - 'last_spawn_time': 0.0 - } - - # 生成历史 - self.spawn_history = [] - self.max_history_size = 100 - - print("✓ 植被生成器已创建") - - def initialize(self) -> bool: - """ - 初始化植被生成器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化统计数据 - if self.plugin.vegetation_manager: - veg_types = self.plugin.vegetation_manager.get_vegetation_types() - self.spawn_stats['spawned_by_type'] = {vt: 0 for vt in veg_types} - - self.initialized = True - print("✓ 植被生成器初始化完成") - return True - - except Exception as e: - print(f"✗ 植被生成器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用植被生成器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 植被生成器未初始化") - return False - - self.enabled = True - print("✓ 植被生成器已启用") - return True - - except Exception as e: - print(f"✗ 植被生成器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用植被生成器""" - try: - self.enabled = False - print("✓ 植被生成器已禁用") - - except Exception as e: - print(f"✗ 植被生成器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理植被生成器资源""" - try: - self.disable() - self.spawn_history.clear() - self.initialized = False - print("✓ 植被生成器资源已清理") - - except Exception as e: - print(f"✗ 植被生成器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新植被生成器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 可以在这里添加动态生成逻辑 - pass - - except Exception as e: - print(f"✗ 植被生成器更新失败: {e}") - import traceback - traceback.print_exc() - - def spawn_vegetation(self, area: Tuple[Tuple[float, float], Tuple[float, float]] = None, - density: float = None, algorithm: str = None) -> int: - """ - 生成植被 - - Args: - area: 生成区域 ((min_x, min_y), (max_x, max_y)) - density: 植被密度 - algorithm: 生成算法 - - Returns: - 生成的植被实例数量 - """ - try: - if not self.plugin.vegetation_manager: - print("✗ 植被管理器不可用") - return 0 - - # 使用默认值或传入值 - spawn_area = area if area else self.spawn_config['spawn_area'] - spawn_density = density if density else self.spawn_config['density'] - spawn_algorithm = algorithm if algorithm else self.spawn_config['algorithm'] - - # 检查算法有效性 - if spawn_algorithm not in self.distribution_algorithms: - print(f"✗ 无效的生成算法: {spawn_algorithm}") - return 0 - - # 根据算法生成植被 - spawned_count = 0 - if spawn_algorithm == 'random': - spawned_count = self._spawn_random(spawn_area, spawn_density) - elif spawn_algorithm == 'clustered': - spawned_count = self._spawn_clustered(spawn_area, spawn_density) - elif spawn_algorithm == 'gradient': - spawned_count = self._spawn_gradient(spawn_area, spawn_density) - elif spawn_algorithm == 'competitive': - spawned_count = self._spawn_competitive(spawn_area, spawn_density) - elif spawn_algorithm == 'environmental': - spawned_count = self._spawn_environmental(spawn_area, spawn_density) - - # 更新统计信息 - self.spawn_stats['total_spawned'] += spawned_count - self.spawn_stats['last_spawn_time'] = time.time() - - # 记录生成历史 - self._record_spawn_event(spawn_algorithm, spawned_count, spawn_area) - - print(f"✓ 植被生成完成: {spawned_count} 个实例,使用算法: {self.distribution_algorithms[spawn_algorithm]['name']}") - return spawned_count - - except Exception as e: - print(f"✗ 植被生成失败: {e}") - import traceback - traceback.print_exc() - self.spawn_stats['failed_spawns'] += 1 - return 0 - - def _spawn_random(self, area: Tuple[Tuple[float, float], Tuple[float, float]], density: float) -> int: - """ - 随机生成植被 - - Args: - area: 生成区域 - density: 植被密度 - - Returns: - 生成的植被实例数量 - """ - try: - (min_x, min_y), (max_x, max_y) = area - area_size = (max_x - min_x) * (max_y - min_y) - target_count = int(area_size * density * 0.1) # 调整密度系数 - - spawned_count = 0 - veg_types = self.plugin.vegetation_manager.get_vegetation_types() - - for _ in range(target_count): - if self._check_max_instances(): - break - - # 随机选择植被类型 - veg_type = random.choice(veg_types) - - # 随机生成位置 - x = random.uniform(min_x, max_x) - y = 0.0 # 假设地面高度为0 - z = random.uniform(min_y, max_y) - - # 创建植被实例 - instance_id = self.plugin.vegetation_manager.create_vegetation_instance( - veg_type, (x, y, z) - ) - - if instance_id >= 0: - self.spawn_stats['spawned_by_type'][veg_type] += 1 - spawned_count += 1 - - return spawned_count - - except Exception as e: - print(f"✗ 随机植被生成失败: {e}") - return 0 - - def _spawn_clustered(self, area: Tuple[Tuple[float, float], Tuple[float, float]], density: float) -> int: - """ - 聚集生成植被 - - Args: - area: 生成区域 - density: 植被密度 - - Returns: - 生成的植被实例数量 - """ - try: - (min_x, min_y), (max_x, max_y) = area - area_size = (max_x - min_x) * (max_y - min_y) - target_count = int(area_size * density * 0.1) - - spawned_count = 0 - veg_types = self.plugin.vegetation_manager.get_vegetation_types() - - # 生成集群 - clusters = max(1, int(target_count * (1 - self.spawn_config['clumping_factor']))) - - for _ in range(clusters): - if self._check_max_instances(): - break - - # 随机选择集群中心 - center_x = random.uniform(min_x, max_x) - center_z = random.uniform(min_y, max_y) - - # 随机选择集群大小 - cluster_size = random.randint(1, self.spatial_parameters['max_cluster_size']) - - # 随机选择植被类型 - veg_type = random.choice(veg_types) - - # 在集群中心周围生成植被 - for _ in range(cluster_size): - if self._check_max_instances(): - break - - # 在集群半径内随机生成位置 - angle = random.uniform(0, 2 * math.pi) - radius = random.uniform(0, self.spatial_parameters['cluster_radius']) - x = center_x + radius * math.cos(angle) - z = center_z + radius * math.sin(angle) - - # 检查是否在生成区域内 - if min_x <= x <= max_x and min_y <= z <= max_y: - instance_id = self.plugin.vegetation_manager.create_vegetation_instance( - veg_type, (x, 0.0, z) - ) - - if instance_id >= 0: - self.spawn_stats['spawned_by_type'][veg_type] += 1 - spawned_count += 1 - - return spawned_count - - except Exception as e: - print(f"✗ 聚集植被生成失败: {e}") - return 0 - - def _spawn_gradient(self, area: Tuple[Tuple[float, float], Tuple[float, float]], density: float) -> int: - """ - 梯度生成植被 - - Args: - area: 生成区域 - density: 植被密度 - - Returns: - 生成的植被实例数量 - """ - try: - # 简化实现,实际中会根据环境梯度生成 - return self._spawn_random(area, density) - - except Exception as e: - print(f"✗ 梯度植被生成失败: {e}") - return 0 - - def _spawn_competitive(self, area: Tuple[Tuple[float, float], Tuple[float, float]], density: float) -> int: - """ - 竞争生成植被 - - Args: - area: 生成区域 - density: 植被密度 - - Returns: - 生成的植被实例数量 - """ - try: - # 简化实现,实际中会考虑植物间竞争关系 - return self._spawn_clustered(area, density) - - except Exception as e: - print(f"✗ 竞争植被生成失败: {e}") - return 0 - - def _spawn_environmental(self, area: Tuple[Tuple[float, float], Tuple[float, float]], density: float) -> int: - """ - 环境适应生成植被 - - Args: - area: 生成区域 - density: 植被密度 - - Returns: - 生成的植被实例数量 - """ - try: - (min_x, min_y), (max_x, max_y) = area - area_size = (max_x - min_x) * (max_y - min_y) - target_count = int(area_size * density * 0.1) - - spawned_count = 0 - if not self.plugin.vegetation_manager: - return 0 - - # 获取环境因子 - environment_factors = self.plugin.vegetation_manager.get_environment_factors() - - for _ in range(target_count): - if self._check_max_instances(): - break - - # 根据环境因子选择最适宜的植被类型 - veg_type = self._select_suitable_vegetation(environment_factors) - - # 随机生成位置 - x = random.uniform(min_x, max_x) - z = random.uniform(min_y, max_y) - - # 创建植被实例 - instance_id = self.plugin.vegetation_manager.create_vegetation_instance( - veg_type, (x, 0.0, z) - ) - - if instance_id >= 0: - self.spawn_stats['spawned_by_type'][veg_type] += 1 - spawned_count += 1 - - return spawned_count - - except Exception as e: - print(f"✗ 环境适应植被生成失败: {e}") - return 0 - - def _select_suitable_vegetation(self, environment_factors: Dict[str, float]) -> str: - """ - 根据环境因子选择最适宜的植被类型 - - Args: - environment_factors: 环境因子字典 - - Returns: - 最适宜的植被类型 - """ - try: - if not self.plugin.vegetation_manager: - veg_types = list(self.ecological_preferences.keys()) - return random.choice(veg_types) - - veg_types = self.plugin.vegetation_manager.get_vegetation_types() - if not veg_types: - return 'grass' - - # 计算每种植被类型的适宜性评分 - suitability_scores = {} - - for veg_type in veg_types: - if veg_type in self.ecological_preferences: - preferences = self.ecological_preferences[veg_type] - score = self._calculate_suitability_score(environment_factors, preferences) - suitability_scores[veg_type] = score - - # 选择适宜性评分最高的植被类型 - if suitability_scores: - return max(suitability_scores, key=suitability_scores.get) - else: - return random.choice(veg_types) - - except Exception as e: - print(f"✗ 适宜植被选择失败: {e}") - veg_types = self.plugin.vegetation_manager.get_vegetation_types() if self.plugin.vegetation_manager else ['grass'] - return random.choice(veg_types) - - def _calculate_suitability_score(self, environment_factors: Dict[str, float], - preferences: Dict[str, Tuple[float, float]]) -> float: - """ - 计算植被类型的环境适宜性评分 - - Args: - environment_factors: 环境因子 - preferences: 植被偏好 - - Returns: - 适宜性评分 (0.0-1.0) - """ - try: - total_score = 0.0 - weight_sum = 0.0 - - for factor, weight in self.environment_weights.items(): - if factor in environment_factors and f"{factor}_preference" in preferences: - current_value = environment_factors[factor] - preferred_range = preferences[f"{factor}_preference"] - - # 计算因子适宜性 - factor_suitability = self._calculate_factor_suitability( - current_value, preferred_range - ) - - total_score += factor_suitability * weight - weight_sum += weight - - if weight_sum > 0: - return total_score / weight_sum - return 0.5 - - except Exception as e: - print(f"✗ 适宜性评分计算失败: {e}") - return 0.5 - - def _calculate_factor_suitability(self, current_value: float, preferred_range: Tuple[float, float]) -> float: - """ - 计算单个环境因子的适宜性 - - Args: - current_value: 当前值 - preferred_range: 偏好范围 - - Returns: - 适宜性评分 (0.0-1.0) - """ - try: - min_val, max_val = preferred_range - - if min_val <= current_value <= max_val: - return 1.0 - elif current_value < min_val: - if min_val > 0: - return max(0.0, 1.0 - (min_val - current_value) / min_val) - return 0.0 - else: # current_value > max_val - if max_val > 0: - return max(0.0, 1.0 - (current_value - max_val) / max_val) - return 0.0 - - except Exception as e: - print(f"✗ 因子适宜性计算失败: {e}") - return 0.5 - - def _check_max_instances(self) -> bool: - """ - 检查是否达到最大实例数限制 - - Returns: - 是否达到限制 - """ - try: - if self.plugin.vegetation_manager: - current_count = self.plugin.vegetation_manager.get_stats()['total_vegetation'] - return current_count >= self.spawn_config['max_instances'] - return False - - except Exception as e: - print(f"✗ 实例数检查失败: {e}") - return False - - def _record_spawn_event(self, algorithm: str, count: int, area: Tuple[Tuple[float, float], Tuple[float, float]]): - """ - 记录生成事件 - - Args: - algorithm: 算法名称 - count: 生成数量 - area: 生成区域 - """ - try: - event_record = { - 'timestamp': time.time(), - 'algorithm': algorithm, - 'count': count, - 'area': area - } - - self.spawn_history.append(event_record) - - # 限制历史记录大小 - if len(self.spawn_history) > self.max_history_size: - self.spawn_history.pop(0) - - except Exception as e: - print(f"✗ 生成事件记录失败: {e}") - - def get_distribution_algorithms(self) -> Dict[str, Dict[str, str]]: - """ - 获取可用的分布算法 - - Returns: - 分布算法字典 - """ - return self.distribution_algorithms.copy() - - def set_spawn_config(self, config: Dict[str, Any]): - """ - 设置生成配置 - - Args: - config: 配置字典 - """ - try: - self.spawn_config.update(config) - print(f"✓ 生成配置已更新: {self.spawn_config}") - except Exception as e: - print(f"✗ 生成配置更新失败: {e}") - - def get_spawn_config(self) -> Dict[str, Any]: - """ - 获取生成配置 - - Returns: - 生成配置字典 - """ - return self.spawn_config.copy() - - def set_environment_weights(self, weights: Dict[str, float]): - """ - 设置环境因子权重 - - Args: - weights: 权重字典 - """ - try: - self.environment_weights.update(weights) - print(f"✓ 环境因子权重已更新: {self.environment_weights}") - except Exception as e: - print(f"✗ 环境因子权重更新失败: {e}") - - def get_environment_weights(self) -> Dict[str, float]: - """ - 获取环境因子权重 - - Returns: - 环境因子权重字典 - """ - return self.environment_weights.copy() - - def set_spatial_parameters(self, parameters: Dict[str, float]): - """ - 设置空间分布参数 - - Args: - parameters: 参数字典 - """ - try: - self.spatial_parameters.update(parameters) - print(f"✓ 空间分布参数已更新: {self.spatial_parameters}") - except Exception as e: - print(f"✗ 空间分布参数更新失败: {e}") - - def get_spatial_parameters(self) -> Dict[str, float]: - """ - 获取空间分布参数 - - Returns: - 空间分布参数字典 - """ - return self.spatial_parameters.copy() - - def get_spawn_stats(self) -> Dict[str, Any]: - """ - 获取生成统计信息 - - Returns: - 生成统计信息字典 - """ - return self.spawn_stats.copy() - - def reset_spawn_stats(self): - """重置生成统计信息""" - try: - self.spawn_stats = { - 'total_spawned': 0, - 'spawned_by_type': {}, - 'failed_spawns': 0, - 'last_spawn_time': 0.0 - } - - if self.plugin.vegetation_manager: - veg_types = self.plugin.vegetation_manager.get_vegetation_types() - self.spawn_stats['spawned_by_type'] = {vt: 0 for vt in veg_types} - - print("✓ 植被生成统计信息已重置") - except Exception as e: - print(f"✗ 植被生成统计信息重置失败: {e}") - - def get_spawn_history(self) -> List[Dict[str, Any]]: - """ - 获取生成历史 - - Returns: - 生成历史列表 - """ - return self.spawn_history.copy() - - def clear_spawn_history(self): - """清空生成历史""" - try: - self.spawn_history.clear() - print("✓ 生成历史已清空") - except Exception as e: - print(f"✗ 生成历史清空失败: {e}") - - def regenerate_vegetation(self, clear_existing: bool = True) -> int: - """ - 重新生成植被 - - Args: - clear_existing: 是否清除现有植被 - - Returns: - 生成的植被实例数量 - """ - try: - if clear_existing and self.plugin.vegetation_manager: - # 清除现有植被实例 - instances = self.plugin.vegetation_manager.get_all_vegetation_instances() - for instance_id in list(instances.keys()): - self.plugin.vegetation_manager.remove_vegetation_instance(instance_id) - - # 重置统计信息 - self.reset_spawn_stats() - - # 使用当前配置重新生成植被 - spawned_count = self.spawn_vegetation() - print(f"✓ 植被重新生成完成: {spawned_count} 个实例") - return spawned_count - - except Exception as e: - print(f"✗ 植被重新生成失败: {e}") +""" +植被生成器 +负责根据环境条件和生态规则生成植被分布 +""" + +import time +from typing import Dict, Any, List, Optional, Tuple +import math +import random + +class VegetationSpawner: + """ + 植被生成器 + 负责根据环境条件、地形特征和生态规则生成植被分布 + """ + + def __init__(self, plugin): + """ + 初始化植被生成器 + + Args: + plugin: 植被和生态系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 植被分布算法 + self.distribution_algorithms = { + 'random': { + 'name': '随机分布', + 'description': '完全随机的植被分布' + }, + 'clustered': { + 'name': '聚集分布', + 'description': '植被以群落形式聚集分布' + }, + 'gradient': { + 'name': '梯度分布', + 'description': '根据环境因子梯度分布植被' + }, + 'competitive': { + 'name': '竞争分布', + 'description': '考虑植物间竞争关系的分布' + }, + 'environmental': { + 'name': '环境适应分布', + 'description': '基于环境适应性的分布' + } + } + + # 生成配置 + self.spawn_config = { + 'algorithm': 'environmental', # 默认算法 + 'density': 1.0, # 植被密度 + 'clumping_factor': 0.7, # 聚集因子 + 'random_seed': 12345, # 随机种子 + 'spawn_area': ((-100, -100), (100, 100)), # 生成区域 + 'max_instances': 10000 # 最大实例数 + } + + # 环境因子权重 + self.environment_weights = { + 'temperature': 0.25, + 'humidity': 0.25, + 'soil_fertility': 0.25, + 'light_level': 0.15, + 'water_level': 0.10 + } + + # 植被生态位偏好 + self.ecological_preferences = { + 'grass': { + 'temperature_preference': (10, 30), + 'humidity_preference': (40, 80), + 'fertility_preference': (0.3, 1.0), + 'light_preference': (0.5, 1.0), + 'water_preference': (0.4, 0.8) + }, + 'bush': { + 'temperature_preference': (5, 25), + 'humidity_preference': (30, 70), + 'fertility_preference': (0.4, 1.0), + 'light_preference': (0.6, 1.0), + 'water_preference': (0.3, 0.7) + }, + 'tree': { + 'temperature_preference': (0, 30), + 'humidity_preference': (40, 90), + 'fertility_preference': (0.5, 1.0), + 'light_preference': (0.7, 1.0), + 'water_preference': (0.5, 1.0) + }, + 'flower': { + 'temperature_preference': (15, 25), + 'humidity_preference': (50, 80), + 'fertility_preference': (0.6, 1.0), + 'light_preference': (0.8, 1.0), + 'water_preference': (0.6, 0.9) + }, + 'fern': { + 'temperature_preference': (10, 20), + 'humidity_preference': (70, 100), + 'fertility_preference': (0.4, 0.8), + 'light_preference': (0.2, 0.6), + 'water_preference': (0.7, 1.0) + }, + 'cactus': { + 'temperature_preference': (20, 40), + 'humidity_preference': (10, 30), + 'fertility_preference': (0.1, 0.5), + 'light_preference': (0.8, 1.0), + 'water_preference': (0.1, 0.3) + }, + 'moss': { + 'temperature_preference': (5, 20), + 'humidity_preference': (80, 100), + 'fertility_preference': (0.1, 0.6), + 'light_preference': (0.1, 0.4), + 'water_preference': (0.8, 1.0) + }, + 'vine': { + 'temperature_preference': (15, 30), + 'humidity_preference': (60, 90), + 'fertility_preference': (0.3, 0.8), + 'light_preference': (0.4, 0.8), + 'water_preference': (0.6, 0.9) + } + } + + # 空间分布参数 + self.spatial_parameters = { + 'min_distance': 0.5, # 最小间距 + 'max_cluster_size': 10, # 最大集群大小 + 'cluster_radius': 5.0, # 集群半径 + 'edge_blending': 0.1 # 边缘混合 + } + + # 生成统计 + self.spawn_stats = { + 'total_spawned': 0, + 'spawned_by_type': {}, + 'failed_spawns': 0, + 'last_spawn_time': 0.0 + } + + # 生成历史 + self.spawn_history = [] + self.max_history_size = 100 + + print("✓ 植被生成器已创建") + + def initialize(self) -> bool: + """ + 初始化植被生成器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化统计数据 + if self.plugin.vegetation_manager: + veg_types = self.plugin.vegetation_manager.get_vegetation_types() + self.spawn_stats['spawned_by_type'] = {vt: 0 for vt in veg_types} + + self.initialized = True + print("✓ 植被生成器初始化完成") + return True + + except Exception as e: + print(f"✗ 植被生成器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用植被生成器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 植被生成器未初始化") + return False + + self.enabled = True + print("✓ 植被生成器已启用") + return True + + except Exception as e: + print(f"✗ 植被生成器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用植被生成器""" + try: + self.enabled = False + print("✓ 植被生成器已禁用") + + except Exception as e: + print(f"✗ 植被生成器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理植被生成器资源""" + try: + self.disable() + self.spawn_history.clear() + self.initialized = False + print("✓ 植被生成器资源已清理") + + except Exception as e: + print(f"✗ 植被生成器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新植被生成器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 可以在这里添加动态生成逻辑 + pass + + except Exception as e: + print(f"✗ 植被生成器更新失败: {e}") + import traceback + traceback.print_exc() + + def spawn_vegetation(self, area: Tuple[Tuple[float, float], Tuple[float, float]] = None, + density: float = None, algorithm: str = None) -> int: + """ + 生成植被 + + Args: + area: 生成区域 ((min_x, min_y), (max_x, max_y)) + density: 植被密度 + algorithm: 生成算法 + + Returns: + 生成的植被实例数量 + """ + try: + if not self.plugin.vegetation_manager: + print("✗ 植被管理器不可用") + return 0 + + # 使用默认值或传入值 + spawn_area = area if area else self.spawn_config['spawn_area'] + spawn_density = density if density else self.spawn_config['density'] + spawn_algorithm = algorithm if algorithm else self.spawn_config['algorithm'] + + # 检查算法有效性 + if spawn_algorithm not in self.distribution_algorithms: + print(f"✗ 无效的生成算法: {spawn_algorithm}") + return 0 + + # 根据算法生成植被 + spawned_count = 0 + if spawn_algorithm == 'random': + spawned_count = self._spawn_random(spawn_area, spawn_density) + elif spawn_algorithm == 'clustered': + spawned_count = self._spawn_clustered(spawn_area, spawn_density) + elif spawn_algorithm == 'gradient': + spawned_count = self._spawn_gradient(spawn_area, spawn_density) + elif spawn_algorithm == 'competitive': + spawned_count = self._spawn_competitive(spawn_area, spawn_density) + elif spawn_algorithm == 'environmental': + spawned_count = self._spawn_environmental(spawn_area, spawn_density) + + # 更新统计信息 + self.spawn_stats['total_spawned'] += spawned_count + self.spawn_stats['last_spawn_time'] = time.time() + + # 记录生成历史 + self._record_spawn_event(spawn_algorithm, spawned_count, spawn_area) + + print(f"✓ 植被生成完成: {spawned_count} 个实例,使用算法: {self.distribution_algorithms[spawn_algorithm]['name']}") + return spawned_count + + except Exception as e: + print(f"✗ 植被生成失败: {e}") + import traceback + traceback.print_exc() + self.spawn_stats['failed_spawns'] += 1 + return 0 + + def _spawn_random(self, area: Tuple[Tuple[float, float], Tuple[float, float]], density: float) -> int: + """ + 随机生成植被 + + Args: + area: 生成区域 + density: 植被密度 + + Returns: + 生成的植被实例数量 + """ + try: + (min_x, min_y), (max_x, max_y) = area + area_size = (max_x - min_x) * (max_y - min_y) + target_count = int(area_size * density * 0.1) # 调整密度系数 + + spawned_count = 0 + veg_types = self.plugin.vegetation_manager.get_vegetation_types() + + for _ in range(target_count): + if self._check_max_instances(): + break + + # 随机选择植被类型 + veg_type = random.choice(veg_types) + + # 随机生成位置 + x = random.uniform(min_x, max_x) + y = 0.0 # 假设地面高度为0 + z = random.uniform(min_y, max_y) + + # 创建植被实例 + instance_id = self.plugin.vegetation_manager.create_vegetation_instance( + veg_type, (x, y, z) + ) + + if instance_id >= 0: + self.spawn_stats['spawned_by_type'][veg_type] += 1 + spawned_count += 1 + + return spawned_count + + except Exception as e: + print(f"✗ 随机植被生成失败: {e}") + return 0 + + def _spawn_clustered(self, area: Tuple[Tuple[float, float], Tuple[float, float]], density: float) -> int: + """ + 聚集生成植被 + + Args: + area: 生成区域 + density: 植被密度 + + Returns: + 生成的植被实例数量 + """ + try: + (min_x, min_y), (max_x, max_y) = area + area_size = (max_x - min_x) * (max_y - min_y) + target_count = int(area_size * density * 0.1) + + spawned_count = 0 + veg_types = self.plugin.vegetation_manager.get_vegetation_types() + + # 生成集群 + clusters = max(1, int(target_count * (1 - self.spawn_config['clumping_factor']))) + + for _ in range(clusters): + if self._check_max_instances(): + break + + # 随机选择集群中心 + center_x = random.uniform(min_x, max_x) + center_z = random.uniform(min_y, max_y) + + # 随机选择集群大小 + cluster_size = random.randint(1, self.spatial_parameters['max_cluster_size']) + + # 随机选择植被类型 + veg_type = random.choice(veg_types) + + # 在集群中心周围生成植被 + for _ in range(cluster_size): + if self._check_max_instances(): + break + + # 在集群半径内随机生成位置 + angle = random.uniform(0, 2 * math.pi) + radius = random.uniform(0, self.spatial_parameters['cluster_radius']) + x = center_x + radius * math.cos(angle) + z = center_z + radius * math.sin(angle) + + # 检查是否在生成区域内 + if min_x <= x <= max_x and min_y <= z <= max_y: + instance_id = self.plugin.vegetation_manager.create_vegetation_instance( + veg_type, (x, 0.0, z) + ) + + if instance_id >= 0: + self.spawn_stats['spawned_by_type'][veg_type] += 1 + spawned_count += 1 + + return spawned_count + + except Exception as e: + print(f"✗ 聚集植被生成失败: {e}") + return 0 + + def _spawn_gradient(self, area: Tuple[Tuple[float, float], Tuple[float, float]], density: float) -> int: + """ + 梯度生成植被 + + Args: + area: 生成区域 + density: 植被密度 + + Returns: + 生成的植被实例数量 + """ + try: + # 简化实现,实际中会根据环境梯度生成 + return self._spawn_random(area, density) + + except Exception as e: + print(f"✗ 梯度植被生成失败: {e}") + return 0 + + def _spawn_competitive(self, area: Tuple[Tuple[float, float], Tuple[float, float]], density: float) -> int: + """ + 竞争生成植被 + + Args: + area: 生成区域 + density: 植被密度 + + Returns: + 生成的植被实例数量 + """ + try: + # 简化实现,实际中会考虑植物间竞争关系 + return self._spawn_clustered(area, density) + + except Exception as e: + print(f"✗ 竞争植被生成失败: {e}") + return 0 + + def _spawn_environmental(self, area: Tuple[Tuple[float, float], Tuple[float, float]], density: float) -> int: + """ + 环境适应生成植被 + + Args: + area: 生成区域 + density: 植被密度 + + Returns: + 生成的植被实例数量 + """ + try: + (min_x, min_y), (max_x, max_y) = area + area_size = (max_x - min_x) * (max_y - min_y) + target_count = int(area_size * density * 0.1) + + spawned_count = 0 + if not self.plugin.vegetation_manager: + return 0 + + # 获取环境因子 + environment_factors = self.plugin.vegetation_manager.get_environment_factors() + + for _ in range(target_count): + if self._check_max_instances(): + break + + # 根据环境因子选择最适宜的植被类型 + veg_type = self._select_suitable_vegetation(environment_factors) + + # 随机生成位置 + x = random.uniform(min_x, max_x) + z = random.uniform(min_y, max_y) + + # 创建植被实例 + instance_id = self.plugin.vegetation_manager.create_vegetation_instance( + veg_type, (x, 0.0, z) + ) + + if instance_id >= 0: + self.spawn_stats['spawned_by_type'][veg_type] += 1 + spawned_count += 1 + + return spawned_count + + except Exception as e: + print(f"✗ 环境适应植被生成失败: {e}") + return 0 + + def _select_suitable_vegetation(self, environment_factors: Dict[str, float]) -> str: + """ + 根据环境因子选择最适宜的植被类型 + + Args: + environment_factors: 环境因子字典 + + Returns: + 最适宜的植被类型 + """ + try: + if not self.plugin.vegetation_manager: + veg_types = list(self.ecological_preferences.keys()) + return random.choice(veg_types) + + veg_types = self.plugin.vegetation_manager.get_vegetation_types() + if not veg_types: + return 'grass' + + # 计算每种植被类型的适宜性评分 + suitability_scores = {} + + for veg_type in veg_types: + if veg_type in self.ecological_preferences: + preferences = self.ecological_preferences[veg_type] + score = self._calculate_suitability_score(environment_factors, preferences) + suitability_scores[veg_type] = score + + # 选择适宜性评分最高的植被类型 + if suitability_scores: + return max(suitability_scores, key=suitability_scores.get) + else: + return random.choice(veg_types) + + except Exception as e: + print(f"✗ 适宜植被选择失败: {e}") + veg_types = self.plugin.vegetation_manager.get_vegetation_types() if self.plugin.vegetation_manager else ['grass'] + return random.choice(veg_types) + + def _calculate_suitability_score(self, environment_factors: Dict[str, float], + preferences: Dict[str, Tuple[float, float]]) -> float: + """ + 计算植被类型的环境适宜性评分 + + Args: + environment_factors: 环境因子 + preferences: 植被偏好 + + Returns: + 适宜性评分 (0.0-1.0) + """ + try: + total_score = 0.0 + weight_sum = 0.0 + + for factor, weight in self.environment_weights.items(): + if factor in environment_factors and f"{factor}_preference" in preferences: + current_value = environment_factors[factor] + preferred_range = preferences[f"{factor}_preference"] + + # 计算因子适宜性 + factor_suitability = self._calculate_factor_suitability( + current_value, preferred_range + ) + + total_score += factor_suitability * weight + weight_sum += weight + + if weight_sum > 0: + return total_score / weight_sum + return 0.5 + + except Exception as e: + print(f"✗ 适宜性评分计算失败: {e}") + return 0.5 + + def _calculate_factor_suitability(self, current_value: float, preferred_range: Tuple[float, float]) -> float: + """ + 计算单个环境因子的适宜性 + + Args: + current_value: 当前值 + preferred_range: 偏好范围 + + Returns: + 适宜性评分 (0.0-1.0) + """ + try: + min_val, max_val = preferred_range + + if min_val <= current_value <= max_val: + return 1.0 + elif current_value < min_val: + if min_val > 0: + return max(0.0, 1.0 - (min_val - current_value) / min_val) + return 0.0 + else: # current_value > max_val + if max_val > 0: + return max(0.0, 1.0 - (current_value - max_val) / max_val) + return 0.0 + + except Exception as e: + print(f"✗ 因子适宜性计算失败: {e}") + return 0.5 + + def _check_max_instances(self) -> bool: + """ + 检查是否达到最大实例数限制 + + Returns: + 是否达到限制 + """ + try: + if self.plugin.vegetation_manager: + current_count = self.plugin.vegetation_manager.get_stats()['total_vegetation'] + return current_count >= self.spawn_config['max_instances'] + return False + + except Exception as e: + print(f"✗ 实例数检查失败: {e}") + return False + + def _record_spawn_event(self, algorithm: str, count: int, area: Tuple[Tuple[float, float], Tuple[float, float]]): + """ + 记录生成事件 + + Args: + algorithm: 算法名称 + count: 生成数量 + area: 生成区域 + """ + try: + event_record = { + 'timestamp': time.time(), + 'algorithm': algorithm, + 'count': count, + 'area': area + } + + self.spawn_history.append(event_record) + + # 限制历史记录大小 + if len(self.spawn_history) > self.max_history_size: + self.spawn_history.pop(0) + + except Exception as e: + print(f"✗ 生成事件记录失败: {e}") + + def get_distribution_algorithms(self) -> Dict[str, Dict[str, str]]: + """ + 获取可用的分布算法 + + Returns: + 分布算法字典 + """ + return self.distribution_algorithms.copy() + + def set_spawn_config(self, config: Dict[str, Any]): + """ + 设置生成配置 + + Args: + config: 配置字典 + """ + try: + self.spawn_config.update(config) + print(f"✓ 生成配置已更新: {self.spawn_config}") + except Exception as e: + print(f"✗ 生成配置更新失败: {e}") + + def get_spawn_config(self) -> Dict[str, Any]: + """ + 获取生成配置 + + Returns: + 生成配置字典 + """ + return self.spawn_config.copy() + + def set_environment_weights(self, weights: Dict[str, float]): + """ + 设置环境因子权重 + + Args: + weights: 权重字典 + """ + try: + self.environment_weights.update(weights) + print(f"✓ 环境因子权重已更新: {self.environment_weights}") + except Exception as e: + print(f"✗ 环境因子权重更新失败: {e}") + + def get_environment_weights(self) -> Dict[str, float]: + """ + 获取环境因子权重 + + Returns: + 环境因子权重字典 + """ + return self.environment_weights.copy() + + def set_spatial_parameters(self, parameters: Dict[str, float]): + """ + 设置空间分布参数 + + Args: + parameters: 参数字典 + """ + try: + self.spatial_parameters.update(parameters) + print(f"✓ 空间分布参数已更新: {self.spatial_parameters}") + except Exception as e: + print(f"✗ 空间分布参数更新失败: {e}") + + def get_spatial_parameters(self) -> Dict[str, float]: + """ + 获取空间分布参数 + + Returns: + 空间分布参数字典 + """ + return self.spatial_parameters.copy() + + def get_spawn_stats(self) -> Dict[str, Any]: + """ + 获取生成统计信息 + + Returns: + 生成统计信息字典 + """ + return self.spawn_stats.copy() + + def reset_spawn_stats(self): + """重置生成统计信息""" + try: + self.spawn_stats = { + 'total_spawned': 0, + 'spawned_by_type': {}, + 'failed_spawns': 0, + 'last_spawn_time': 0.0 + } + + if self.plugin.vegetation_manager: + veg_types = self.plugin.vegetation_manager.get_vegetation_types() + self.spawn_stats['spawned_by_type'] = {vt: 0 for vt in veg_types} + + print("✓ 植被生成统计信息已重置") + except Exception as e: + print(f"✗ 植被生成统计信息重置失败: {e}") + + def get_spawn_history(self) -> List[Dict[str, Any]]: + """ + 获取生成历史 + + Returns: + 生成历史列表 + """ + return self.spawn_history.copy() + + def clear_spawn_history(self): + """清空生成历史""" + try: + self.spawn_history.clear() + print("✓ 生成历史已清空") + except Exception as e: + print(f"✗ 生成历史清空失败: {e}") + + def regenerate_vegetation(self, clear_existing: bool = True) -> int: + """ + 重新生成植被 + + Args: + clear_existing: 是否清除现有植被 + + Returns: + 生成的植被实例数量 + """ + try: + if clear_existing and self.plugin.vegetation_manager: + # 清除现有植被实例 + instances = self.plugin.vegetation_manager.get_all_vegetation_instances() + for instance_id in list(instances.keys()): + self.plugin.vegetation_manager.remove_vegetation_instance(instance_id) + + # 重置统计信息 + self.reset_spawn_stats() + + # 使用当前配置重新生成植被 + spawned_count = self.spawn_vegetation() + print(f"✓ 植被重新生成完成: {spawned_count} 个实例") + return spawned_count + + except Exception as e: + print(f"✗ 植被重新生成失败: {e}") return 0 \ No newline at end of file diff --git a/plugins/user/vegetation_ecosystem/visualization/advanced_visuals.py b/plugins/user/vegetation_ecosystem/visualization/advanced_visuals.py index 4484ec8e..0b2a6c69 100644 --- a/plugins/user/vegetation_ecosystem/visualization/advanced_visuals.py +++ b/plugins/user/vegetation_ecosystem/visualization/advanced_visuals.py @@ -1,792 +1,792 @@ -""" -高级可视化模块 -负责处理更复杂的生态系统可视化效果,如生态系统健康度可视化、交互可视化等 -""" - -import time -from typing import Dict, Any, List, Optional -import math -import random - -class AdvancedVisuals: - """ - 高级可视化系统 - 负责处理更复杂的生态系统可视化效果 - """ - - def __init__(self, plugin): - """ - 初始化高级可视化系统 - - Args: - plugin: 植被和生态系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 可视化效果类型 - self.visualization_types = { - 'ecosystem_health': { - 'name': '生态系统健康度可视化', - 'description': '可视化显示生态系统的整体健康状况', - 'enabled': True - }, - 'interaction_trails': { - 'name': '交互轨迹可视化', - 'description': '可视化显示动物与植被的交互轨迹', - 'enabled': True - }, - 'biodiversity_map': { - 'name': '生物多样性地图', - 'description': '生成并显示生物多样性热力图', - 'enabled': True - }, - 'resource_distribution': { - 'name': '资源分布可视化', - 'description': '可视化显示环境资源分布', - 'enabled': True - }, - 'population_density': { - 'name': '种群密度可视化', - 'description': '可视化显示动物种群密度', - 'enabled': True - }, - 'growth_patterns': { - 'name': '生长模式可视化', - 'description': '可视化显示植被生长模式', - 'enabled': True - } - } - - # 可视化配置 - self.visualization_config = { - 'update_interval': 5.0, # 更新间隔(秒) - 'render_distance': 150.0, # 渲染距离 - 'effect_intensity': 1.0, # 效果强度 - 'show_labels': True, # 显示标签 - 'transparency': 0.7, # 透明度 - 'color_scheme': 'default' # 颜色方案 - } - - # 可视化数据缓存 - self.visualization_cache = {} - self.cache_config = { - 'max_size': 200, - 'expiration_time': 300 # 5分钟 - } - - # 特效参数 - self.effect_params = { - 'health_visualization': { - 'color_gradient': [(1.0, 0.0, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0)], # 红-黄-绿 - 'pulse_rate': 0.5, # 脉冲频率 - 'size_variation': 0.2 # 大小变化 - }, - 'trail_effects': { - 'trail_lifetime': 10.0, # 轨迹生命周期 - 'trail_segments': 20, # 轨迹分段数 - 'trail_width': 0.5 # 轨迹宽度 - }, - 'heatmap_settings': { - 'gradient_colors': [(0.0, 0.0, 1.0), (0.0, 1.0, 0.0), (1.0, 1.0, 0.0), (1.0, 0.0, 0.0)], - 'opacity_range': (0.3, 0.8), - 'resolution': 50 # 热力图分辨率 - } - } - - # 可视化对象 - self.visualization_objects = {} - self.object_counter = 0 - - # 时间配置 - self.time_config = { - 'last_update': 0.0, - 'effect_time': 0.0 - } - - # 统计信息 - self.visual_stats = { - 'visualizations_created': 0, - 'visualizations_updated': 0, - 'cache_hits': 0, - 'cache_misses': 0 - } - - print("✓ 高级可视化系统已创建") - - def initialize(self) -> bool: - """ - 初始化高级可视化系统 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 高级可视化系统初始化完成") - return True - - except Exception as e: - print(f"✗ 高级可视化系统初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用高级可视化系统 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 高级可视化系统未初始化") - return False - - self.enabled = True - print("✓ 高级可视化系统已启用") - return True - - except Exception as e: - print(f"✗ 高级可视化系统启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用高级可视化系统""" - try: - self.enabled = False - print("✓ 高级可视化系统已禁用") - - except Exception as e: - print(f"✗ 高级可视化系统禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理高级可视化系统资源""" - try: - self.disable() - self.visualization_cache.clear() - self.visualization_objects.clear() - self.initialized = False - print("✓ 高级可视化系统资源已清理") - - except Exception as e: - print(f"✗ 高级可视化系统资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新高级可视化系统状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新时间 - self.time_config['last_update'] += dt - self.time_config['effect_time'] += dt - - # 检查是否需要更新 - if self.time_config['last_update'] >= self.visualization_config['update_interval']: - # 更新可视化效果 - self._update_visualizations() - - # 重置更新计时器 - self.time_config['last_update'] = 0.0 - - # 更新特效时间 - self._update_visual_effects(dt) - - except Exception as e: - print(f"✗ 高级可视化系统更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_visualizations(self): - """更新可视化效果""" - try: - # 更新生态系统健康度可视化 - if self.visualization_types['ecosystem_health']['enabled']: - self._update_ecosystem_health_visualization() - - # 更新交互轨迹可视化 - if self.visualization_types['interaction_trails']['enabled']: - self._update_interaction_trails_visualization() - - # 更新生物多样性地图 - if self.visualization_types['biodiversity_map']['enabled']: - self._update_biodiversity_map_visualization() - - # 更新统计信息 - self.visual_stats['visualizations_updated'] += 1 - - except Exception as e: - print(f"✗ 可视化效果更新失败: {e}") - - def _update_visual_effects(self, dt: float): - """更新视觉特效""" - try: - # 在实际实现中,这里会更新各种视觉特效 - # 如脉冲效果、颜色变化等 - pass - except Exception as e: - print(f"✗ 视觉特效更新失败: {e}") - - def _update_ecosystem_health_visualization(self): - """更新生态系统健康度可视化""" - try: - if not self.plugin.ecosystem_manager: - return - - # 获取生态系统状态 - system_state = self.plugin.ecosystem_manager.get_system_state() - health_index = system_state.get('health_index', 0.5) - - # 在实际实现中,这里会创建或更新表示生态系统健康度的可视化对象 - # 比如在场景中显示一个颜色编码的球体或平面 - - # 缓存结果 - self._cache_visualization_data('ecosystem_health', health_index) - - except Exception as e: - print(f"✗ 生态系统健康度可视化更新失败: {e}") - - def _update_interaction_trails_visualization(self): - """更新交互轨迹可视化""" - try: - if not self.plugin.ecosystem_interactions: - return - - # 获取交互历史 - interaction_history = self.plugin.ecosystem_interactions.get_interaction_history(20) - - # 在实际实现中,这里会根据交互历史创建轨迹可视化 - # 比如显示动物移动路径、觅食路径等 - - # 缓存结果 - self._cache_visualization_data('interaction_trails', len(interaction_history)) - - except Exception as e: - print(f"✗ 交互轨迹可视化更新失败: {e}") - - def _update_biodiversity_map_visualization(self): - """更新生物多样性地图可视化""" - try: - # 在实际实现中,这里会生成生物多样性热力图 - # 基于植被和动物的分布密度计算多样性指数 - - biodiversity_index = 0.5 # 简化的默认值 - - if self.plugin.vegetation_manager and self.plugin.animal_simulator: - # 计算更准确的生物多样性指数 - veg_stats = self.plugin.vegetation_manager.get_stats() - animal_stats = self.plugin.animal_simulator.get_stats() - - veg_count = veg_stats.get('total_vegetation', 0) - animal_count = animal_stats.get('total_animals', 0) - - # 简单的多样性计算 - biodiversity_index = min(1.0, (veg_count + animal_count) / 1000.0) - - # 缓存结果 - self._cache_visualization_data('biodiversity_map', biodiversity_index) - - except Exception as e: - print(f"✗ 生物多样性地图可视化更新失败: {e}") - - def _cache_visualization_data(self, key: str, data: Any): - """ - 缓存可视化数据 - - Args: - key: 缓存键 - data: 要缓存的数据 - """ - try: - # 检查缓存大小 - if len(self.visualization_cache) >= self.cache_config['max_size']: - # 移除最旧的条目 - oldest_key = min(self.visualization_cache.keys(), - key=lambda k: self.visualization_cache[k][0]) - del self.visualization_cache[oldest_key] - - # 缓存数据 - self.visualization_cache[key] = (time.time(), data) - - except Exception as e: - print(f"✗ 可视化数据缓存失败: {e}") - - def get_cached_visualization_data(self, key: str) -> Optional[Any]: - """ - 获取缓存的可视化数据 - - Args: - key: 缓存键 - - Returns: - 缓存的数据或None - """ - try: - if key in self.visualization_cache: - timestamp, data = self.visualization_cache[key] - if time.time() - timestamp < self.cache_config['expiration_time']: - self.visual_stats['cache_hits'] += 1 - return data - else: - # 缓存过期,删除条目 - del self.visualization_cache[key] - - self.visual_stats['cache_misses'] += 1 - return None - - except Exception as e: - print(f"✗ 缓存可视化数据获取失败: {e}") - return None - - def create_visualization_object(self, object_type: str, properties: Dict[str, Any]) -> int: - """ - 创建可视化对象 - - Args: - object_type: 对象类型 - properties: 对象属性 - - Returns: - 对象ID - """ - try: - object_id = self.object_counter - self.object_counter += 1 - - visualization_object = { - 'id': object_id, - 'type': object_type, - 'properties': properties, - 'created_time': time.time(), - 'last_updated': time.time() - } - - self.visualization_objects[object_id] = visualization_object - - # 更新统计信息 - self.visual_stats['visualizations_created'] += 1 - - print(f"✓ 可视化对象已创建: {object_type} (ID: {object_id})") - return object_id - - except Exception as e: - print(f"✗ 可视化对象创建失败: {e}") - return -1 - - def update_visualization_object(self, object_id: int, properties: Dict[str, Any]) -> bool: - """ - 更新可视化对象 - - Args: - object_id: 对象ID - properties: 对象属性 - - Returns: - 是否更新成功 - """ - try: - if object_id not in self.visualization_objects: - print(f"✗ 可视化对象不存在: {object_id}") - return False - - # 更新属性 - self.visualization_objects[object_id]['properties'].update(properties) - self.visualization_objects[object_id]['last_updated'] = time.time() - - print(f"✓ 可视化对象已更新: ID {object_id}") - return True - - except Exception as e: - print(f"✗ 可视化对象更新失败: {e}") - return False - - def remove_visualization_object(self, object_id: int) -> bool: - """ - 移除可视化对象 - - Args: - object_id: 对象ID - - Returns: - 是否移除成功 - """ - try: - if object_id not in self.visualization_objects: - print(f"✗ 可视化对象不存在: {object_id}") - return False - - del self.visualization_objects[object_id] - - print(f"✓ 可视化对象已移除: ID {object_id}") - return True - - except Exception as e: - print(f"✗ 可视化对象移除失败: {e}") - return False - - def get_visualization_object(self, object_id: int) -> Optional[Dict[str, Any]]: - """ - 获取可视化对象 - - Args: - object_id: 对象ID - - Returns: - 可视化对象或None - """ - return self.visualization_objects.get(object_id) - - def get_all_visualization_objects(self) -> Dict[int, Dict[str, Any]]: - """ - 获取所有可视化对象 - - Returns: - 所有可视化对象字典 - """ - return self.visualization_objects.copy() - - def set_visualization_type_enabled(self, visualization_type: str, enabled: bool): - """ - 设置可视化类型启用状态 - - Args: - visualization_type: 可视化类型 - enabled: 是否启用 - """ - try: - if visualization_type in self.visualization_types: - self.visualization_types[visualization_type]['enabled'] = enabled - print(f"✓ 可视化类型 '{visualization_type}' 已{'启用' if enabled else '禁用'}") - else: - print(f"✗ 无效的可视化类型: {visualization_type}") - - except Exception as e: - print(f"✗ 可视化类型设置失败: {e}") - - def get_visualization_types(self) -> Dict[str, Dict[str, Any]]: - """ - 获取可视化类型 - - Returns: - 可视化类型字典 - """ - return self.visualization_types.copy() - - def set_visualization_config(self, config: Dict[str, Any]): - """ - 设置可视化配置 - - Args: - config: 配置字典 - """ - try: - self.visualization_config.update(config) - print(f"✓ 可视化配置已更新: {self.visualization_config}") - except Exception as e: - print(f"✗ 可视化配置更新失败: {e}") - - def get_visualization_config(self) -> Dict[str, Any]: - """ - 获取可视化配置 - - Returns: - 可视化配置字典 - """ - return self.visualization_config.copy() - - def set_effect_parameter(self, effect: str, parameter: str, value: Any): - """ - 设置特效参数 - - Args: - effect: 特效名称 - parameter: 参数名称 - value: 参数值 - """ - try: - if effect in self.effect_params and parameter in self.effect_params[effect]: - self.effect_params[effect][parameter] = value - print(f"✓ 特效参数已设置: {effect}.{parameter} = {value}") - else: - print(f"✗ 无效的特效参数: {effect}.{parameter}") - - except Exception as e: - print(f"✗ 特效参数设置失败: {e}") - - def get_effect_parameters(self) -> Dict[str, Dict[str, Any]]: - """ - 获取特效参数 - - Returns: - 特效参数字典 - """ - return self.effect_params.copy() - - def render_ecosystem_health(self): - """渲染生态系统健康度可视化""" - try: - if not self.enabled or not self.visualization_types['ecosystem_health']['enabled']: - return - - # 获取健康度数据 - health_data = self.get_cached_visualization_data('ecosystem_health') - if health_data is None: - # 如果没有缓存数据,计算新的数据 - if self.plugin.ecosystem_manager: - system_state = self.plugin.ecosystem_manager.get_system_state() - health_data = system_state.get('health_index', 0.5) - - if health_data is not None: - # 在实际实现中,这里会渲染健康度可视化效果 - # 比如根据健康度值改变颜色、大小等 - - # 根据健康度值选择颜色 - color = self._get_health_color(health_data) - - # 应用可视化效果 - print(f"✓ 渲染生态系统健康度可视化: 健康度={health_data:.2f}, 颜色={color}") - - except Exception as e: - print(f"✗ 生态系统健康度可视化渲染失败: {e}") - - def _get_health_color(self, health: float) -> tuple: - """ - 根据健康度获取颜色 - - Args: - health: 健康度值 (0.0-1.0) - - Returns: - RGB颜色元组 - """ - try: - # 使用预定义的颜色渐变 - gradient = self.effect_params['health_visualization']['color_gradient'] - - # 确保健康度在有效范围内 - health = max(0.0, min(1.0, health)) - - # 简单的三色插值 - if health < 0.5: - # 红色到黄色 - t = health * 2.0 - r = gradient[0][0] + (gradient[1][0] - gradient[0][0]) * t - g = gradient[0][1] + (gradient[1][1] - gradient[0][1]) * t - b = gradient[0][2] + (gradient[1][2] - gradient[0][2]) * t - else: - # 黄色到绿色 - t = (health - 0.5) * 2.0 - r = gradient[1][0] + (gradient[2][0] - gradient[1][0]) * t - g = gradient[1][1] + (gradient[2][1] - gradient[1][1]) * t - b = gradient[1][2] + (gradient[2][2] - gradient[1][2]) * t - - return (r, g, b) - - except Exception as e: - print(f"✗ 健康度颜色计算失败: {e}") - return (0.5, 0.5, 0.5) - - def render_biodiversity_map(self): - """渲染生物多样性地图""" - try: - if not self.enabled or not self.visualization_types['biodiversity_map']['enabled']: - return - - # 获取多样性数据 - biodiversity_data = self.get_cached_visualization_data('biodiversity_map') - if biodiversity_data is None: - # 计算多样性数据 - biodiversity_data = 0.5 # 默认值 - - if self.plugin.vegetation_manager and self.plugin.animal_simulator: - veg_stats = self.plugin.vegetation_manager.get_stats() - animal_stats = self.plugin.animal_simulator.get_stats() - - veg_count = veg_stats.get('total_vegetation', 0) - animal_count = animal_stats.get('total_animals', 0) - - biodiversity_data = min(1.0, (veg_count + animal_count) / 1000.0) - - # 在实际实现中,这里会渲染多样性热力图 - print(f"✓ 渲染生物多样性地图: 多样性指数={biodiversity_data:.2f}") - - except Exception as e: - print(f"✗ 生物多样性地图渲染失败: {e}") - - def render_interaction_trails(self): - """渲染交互轨迹""" - try: - if not self.enabled or not self.visualization_types['interaction_trails']['enabled']: - return - - # 在实际实现中,这里会渲染交互轨迹效果 - print("✓ 渲染交互轨迹可视化") - - except Exception as e: - print(f"✗ 交互轨迹渲染失败: {e}") - - def get_visualization_stats(self) -> Dict[str, Any]: - """ - 获取可视化统计信息 - - Returns: - 统计信息字典 - """ - # 计算缓存命中率 - total_cache_accesses = self.visual_stats['cache_hits'] + self.visual_stats['cache_misses'] - cache_hit_rate = 0.0 - if total_cache_accesses > 0: - cache_hit_rate = self.visual_stats['cache_hits'] / total_cache_accesses - - stats = self.visual_stats.copy() - stats['cache_hit_rate'] = cache_hit_rate - stats['active_objects'] = len(self.visualization_objects) - - return stats - - def reset_visualization_stats(self): - """重置可视化统计信息""" - try: - self.visual_stats = { - 'visualizations_created': 0, - 'visualizations_updated': 0, - 'cache_hits': 0, - 'cache_misses': 0 - } - print("✓ 可视化统计信息已重置") - except Exception as e: - print(f"✗ 可视化统计信息重置失败: {e}") - - def clear_visualization_cache(self): - """清空可视化缓存""" - try: - self.visualization_cache.clear() - print("✓ 可视化缓存已清空") - except Exception as e: - print(f"✗ 可视化缓存清空失败: {e}") - - def cleanup_expired_cache(self): - """清理过期缓存""" - try: - current_time = time.time() - expired_keys = [] - - for key, (timestamp, _) in self.visualization_cache.items(): - if current_time - timestamp > self.cache_config['expiration_time']: - expired_keys.append(key) - - for key in expired_keys: - del self.visualization_cache[key] - - if expired_keys: - print(f"✓ 已清理 {len(expired_keys)} 个过期缓存项") - - except Exception as e: - print(f"✗ 过期缓存清理失败: {e}") - - def set_color_scheme(self, scheme: str): - """ - 设置颜色方案 - - Args: - scheme: 颜色方案名称 - """ - try: - self.visualization_config['color_scheme'] = scheme - print(f"✓ 颜色方案已设置为: {scheme}") - except Exception as e: - print(f"✗ 颜色方案设置失败: {e}") - - def get_available_color_schemes(self) -> List[str]: - """ - 获取可用的颜色方案 - - Returns: - 颜色方案列表 - """ - # 在实际实现中,这里会返回支持的颜色方案列表 - return ['default', 'warm', 'cool', 'contrast'] - - def capture_visualization_screenshot(self, filename: str = None) -> str: - """ - 捕获可视化截图 - - Args: - filename: 文件名 - - Returns: - 截图文件路径 - """ - try: - if filename is None: - timestamp = int(time.time()) - filename = f"ecovisual_{timestamp}.png" - - # 在实际实现中,这里会调用渲染API捕获屏幕 - print(f"✓ 可视化截图已保存: {filename}") - return filename - - except Exception as e: - print(f"✗ 可视化截图捕获失败: {e}") - return "" - - def export_visualization_data(self, filename: str) -> bool: - """ - 导出可视化数据 - - Args: - filename: 文件名 - - Returns: - 是否导出成功 - """ - try: - # 创建导出数据 - export_data = { - 'visualization_config': self.visualization_config, - 'effect_params': self.effect_params, - 'visualization_types': self.visualization_types, - 'stats': self.get_visualization_stats(), - 'timestamp': time.time() - } - - # 在实际实现中,这里会将数据保存到文件 - print(f"✓ 可视化数据已导出到: {filename}") - return True - - except Exception as e: - print(f"✗ 可视化数据导出失败: {e}") - return False - - def import_visualization_data(self, filename: str) -> bool: - """ - 导入可视化数据 - - Args: - filename: 文件名 - - Returns: - 是否导入成功 - """ - try: - # 在实际实现中,这里会从文件加载数据 - print(f"✓ 可视化数据已从 {filename} 导入") - return True - - except Exception as e: - print(f"✗ 可视化数据导入失败: {e}") +""" +高级可视化模块 +负责处理更复杂的生态系统可视化效果,如生态系统健康度可视化、交互可视化等 +""" + +import time +from typing import Dict, Any, List, Optional +import math +import random + +class AdvancedVisuals: + """ + 高级可视化系统 + 负责处理更复杂的生态系统可视化效果 + """ + + def __init__(self, plugin): + """ + 初始化高级可视化系统 + + Args: + plugin: 植被和生态系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 可视化效果类型 + self.visualization_types = { + 'ecosystem_health': { + 'name': '生态系统健康度可视化', + 'description': '可视化显示生态系统的整体健康状况', + 'enabled': True + }, + 'interaction_trails': { + 'name': '交互轨迹可视化', + 'description': '可视化显示动物与植被的交互轨迹', + 'enabled': True + }, + 'biodiversity_map': { + 'name': '生物多样性地图', + 'description': '生成并显示生物多样性热力图', + 'enabled': True + }, + 'resource_distribution': { + 'name': '资源分布可视化', + 'description': '可视化显示环境资源分布', + 'enabled': True + }, + 'population_density': { + 'name': '种群密度可视化', + 'description': '可视化显示动物种群密度', + 'enabled': True + }, + 'growth_patterns': { + 'name': '生长模式可视化', + 'description': '可视化显示植被生长模式', + 'enabled': True + } + } + + # 可视化配置 + self.visualization_config = { + 'update_interval': 5.0, # 更新间隔(秒) + 'render_distance': 150.0, # 渲染距离 + 'effect_intensity': 1.0, # 效果强度 + 'show_labels': True, # 显示标签 + 'transparency': 0.7, # 透明度 + 'color_scheme': 'default' # 颜色方案 + } + + # 可视化数据缓存 + self.visualization_cache = {} + self.cache_config = { + 'max_size': 200, + 'expiration_time': 300 # 5分钟 + } + + # 特效参数 + self.effect_params = { + 'health_visualization': { + 'color_gradient': [(1.0, 0.0, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0)], # 红-黄-绿 + 'pulse_rate': 0.5, # 脉冲频率 + 'size_variation': 0.2 # 大小变化 + }, + 'trail_effects': { + 'trail_lifetime': 10.0, # 轨迹生命周期 + 'trail_segments': 20, # 轨迹分段数 + 'trail_width': 0.5 # 轨迹宽度 + }, + 'heatmap_settings': { + 'gradient_colors': [(0.0, 0.0, 1.0), (0.0, 1.0, 0.0), (1.0, 1.0, 0.0), (1.0, 0.0, 0.0)], + 'opacity_range': (0.3, 0.8), + 'resolution': 50 # 热力图分辨率 + } + } + + # 可视化对象 + self.visualization_objects = {} + self.object_counter = 0 + + # 时间配置 + self.time_config = { + 'last_update': 0.0, + 'effect_time': 0.0 + } + + # 统计信息 + self.visual_stats = { + 'visualizations_created': 0, + 'visualizations_updated': 0, + 'cache_hits': 0, + 'cache_misses': 0 + } + + print("✓ 高级可视化系统已创建") + + def initialize(self) -> bool: + """ + 初始化高级可视化系统 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 高级可视化系统初始化完成") + return True + + except Exception as e: + print(f"✗ 高级可视化系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用高级可视化系统 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 高级可视化系统未初始化") + return False + + self.enabled = True + print("✓ 高级可视化系统已启用") + return True + + except Exception as e: + print(f"✗ 高级可视化系统启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用高级可视化系统""" + try: + self.enabled = False + print("✓ 高级可视化系统已禁用") + + except Exception as e: + print(f"✗ 高级可视化系统禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理高级可视化系统资源""" + try: + self.disable() + self.visualization_cache.clear() + self.visualization_objects.clear() + self.initialized = False + print("✓ 高级可视化系统资源已清理") + + except Exception as e: + print(f"✗ 高级可视化系统资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新高级可视化系统状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新时间 + self.time_config['last_update'] += dt + self.time_config['effect_time'] += dt + + # 检查是否需要更新 + if self.time_config['last_update'] >= self.visualization_config['update_interval']: + # 更新可视化效果 + self._update_visualizations() + + # 重置更新计时器 + self.time_config['last_update'] = 0.0 + + # 更新特效时间 + self._update_visual_effects(dt) + + except Exception as e: + print(f"✗ 高级可视化系统更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_visualizations(self): + """更新可视化效果""" + try: + # 更新生态系统健康度可视化 + if self.visualization_types['ecosystem_health']['enabled']: + self._update_ecosystem_health_visualization() + + # 更新交互轨迹可视化 + if self.visualization_types['interaction_trails']['enabled']: + self._update_interaction_trails_visualization() + + # 更新生物多样性地图 + if self.visualization_types['biodiversity_map']['enabled']: + self._update_biodiversity_map_visualization() + + # 更新统计信息 + self.visual_stats['visualizations_updated'] += 1 + + except Exception as e: + print(f"✗ 可视化效果更新失败: {e}") + + def _update_visual_effects(self, dt: float): + """更新视觉特效""" + try: + # 在实际实现中,这里会更新各种视觉特效 + # 如脉冲效果、颜色变化等 + pass + except Exception as e: + print(f"✗ 视觉特效更新失败: {e}") + + def _update_ecosystem_health_visualization(self): + """更新生态系统健康度可视化""" + try: + if not self.plugin.ecosystem_manager: + return + + # 获取生态系统状态 + system_state = self.plugin.ecosystem_manager.get_system_state() + health_index = system_state.get('health_index', 0.5) + + # 在实际实现中,这里会创建或更新表示生态系统健康度的可视化对象 + # 比如在场景中显示一个颜色编码的球体或平面 + + # 缓存结果 + self._cache_visualization_data('ecosystem_health', health_index) + + except Exception as e: + print(f"✗ 生态系统健康度可视化更新失败: {e}") + + def _update_interaction_trails_visualization(self): + """更新交互轨迹可视化""" + try: + if not self.plugin.ecosystem_interactions: + return + + # 获取交互历史 + interaction_history = self.plugin.ecosystem_interactions.get_interaction_history(20) + + # 在实际实现中,这里会根据交互历史创建轨迹可视化 + # 比如显示动物移动路径、觅食路径等 + + # 缓存结果 + self._cache_visualization_data('interaction_trails', len(interaction_history)) + + except Exception as e: + print(f"✗ 交互轨迹可视化更新失败: {e}") + + def _update_biodiversity_map_visualization(self): + """更新生物多样性地图可视化""" + try: + # 在实际实现中,这里会生成生物多样性热力图 + # 基于植被和动物的分布密度计算多样性指数 + + biodiversity_index = 0.5 # 简化的默认值 + + if self.plugin.vegetation_manager and self.plugin.animal_simulator: + # 计算更准确的生物多样性指数 + veg_stats = self.plugin.vegetation_manager.get_stats() + animal_stats = self.plugin.animal_simulator.get_stats() + + veg_count = veg_stats.get('total_vegetation', 0) + animal_count = animal_stats.get('total_animals', 0) + + # 简单的多样性计算 + biodiversity_index = min(1.0, (veg_count + animal_count) / 1000.0) + + # 缓存结果 + self._cache_visualization_data('biodiversity_map', biodiversity_index) + + except Exception as e: + print(f"✗ 生物多样性地图可视化更新失败: {e}") + + def _cache_visualization_data(self, key: str, data: Any): + """ + 缓存可视化数据 + + Args: + key: 缓存键 + data: 要缓存的数据 + """ + try: + # 检查缓存大小 + if len(self.visualization_cache) >= self.cache_config['max_size']: + # 移除最旧的条目 + oldest_key = min(self.visualization_cache.keys(), + key=lambda k: self.visualization_cache[k][0]) + del self.visualization_cache[oldest_key] + + # 缓存数据 + self.visualization_cache[key] = (time.time(), data) + + except Exception as e: + print(f"✗ 可视化数据缓存失败: {e}") + + def get_cached_visualization_data(self, key: str) -> Optional[Any]: + """ + 获取缓存的可视化数据 + + Args: + key: 缓存键 + + Returns: + 缓存的数据或None + """ + try: + if key in self.visualization_cache: + timestamp, data = self.visualization_cache[key] + if time.time() - timestamp < self.cache_config['expiration_time']: + self.visual_stats['cache_hits'] += 1 + return data + else: + # 缓存过期,删除条目 + del self.visualization_cache[key] + + self.visual_stats['cache_misses'] += 1 + return None + + except Exception as e: + print(f"✗ 缓存可视化数据获取失败: {e}") + return None + + def create_visualization_object(self, object_type: str, properties: Dict[str, Any]) -> int: + """ + 创建可视化对象 + + Args: + object_type: 对象类型 + properties: 对象属性 + + Returns: + 对象ID + """ + try: + object_id = self.object_counter + self.object_counter += 1 + + visualization_object = { + 'id': object_id, + 'type': object_type, + 'properties': properties, + 'created_time': time.time(), + 'last_updated': time.time() + } + + self.visualization_objects[object_id] = visualization_object + + # 更新统计信息 + self.visual_stats['visualizations_created'] += 1 + + print(f"✓ 可视化对象已创建: {object_type} (ID: {object_id})") + return object_id + + except Exception as e: + print(f"✗ 可视化对象创建失败: {e}") + return -1 + + def update_visualization_object(self, object_id: int, properties: Dict[str, Any]) -> bool: + """ + 更新可视化对象 + + Args: + object_id: 对象ID + properties: 对象属性 + + Returns: + 是否更新成功 + """ + try: + if object_id not in self.visualization_objects: + print(f"✗ 可视化对象不存在: {object_id}") + return False + + # 更新属性 + self.visualization_objects[object_id]['properties'].update(properties) + self.visualization_objects[object_id]['last_updated'] = time.time() + + print(f"✓ 可视化对象已更新: ID {object_id}") + return True + + except Exception as e: + print(f"✗ 可视化对象更新失败: {e}") + return False + + def remove_visualization_object(self, object_id: int) -> bool: + """ + 移除可视化对象 + + Args: + object_id: 对象ID + + Returns: + 是否移除成功 + """ + try: + if object_id not in self.visualization_objects: + print(f"✗ 可视化对象不存在: {object_id}") + return False + + del self.visualization_objects[object_id] + + print(f"✓ 可视化对象已移除: ID {object_id}") + return True + + except Exception as e: + print(f"✗ 可视化对象移除失败: {e}") + return False + + def get_visualization_object(self, object_id: int) -> Optional[Dict[str, Any]]: + """ + 获取可视化对象 + + Args: + object_id: 对象ID + + Returns: + 可视化对象或None + """ + return self.visualization_objects.get(object_id) + + def get_all_visualization_objects(self) -> Dict[int, Dict[str, Any]]: + """ + 获取所有可视化对象 + + Returns: + 所有可视化对象字典 + """ + return self.visualization_objects.copy() + + def set_visualization_type_enabled(self, visualization_type: str, enabled: bool): + """ + 设置可视化类型启用状态 + + Args: + visualization_type: 可视化类型 + enabled: 是否启用 + """ + try: + if visualization_type in self.visualization_types: + self.visualization_types[visualization_type]['enabled'] = enabled + print(f"✓ 可视化类型 '{visualization_type}' 已{'启用' if enabled else '禁用'}") + else: + print(f"✗ 无效的可视化类型: {visualization_type}") + + except Exception as e: + print(f"✗ 可视化类型设置失败: {e}") + + def get_visualization_types(self) -> Dict[str, Dict[str, Any]]: + """ + 获取可视化类型 + + Returns: + 可视化类型字典 + """ + return self.visualization_types.copy() + + def set_visualization_config(self, config: Dict[str, Any]): + """ + 设置可视化配置 + + Args: + config: 配置字典 + """ + try: + self.visualization_config.update(config) + print(f"✓ 可视化配置已更新: {self.visualization_config}") + except Exception as e: + print(f"✗ 可视化配置更新失败: {e}") + + def get_visualization_config(self) -> Dict[str, Any]: + """ + 获取可视化配置 + + Returns: + 可视化配置字典 + """ + return self.visualization_config.copy() + + def set_effect_parameter(self, effect: str, parameter: str, value: Any): + """ + 设置特效参数 + + Args: + effect: 特效名称 + parameter: 参数名称 + value: 参数值 + """ + try: + if effect in self.effect_params and parameter in self.effect_params[effect]: + self.effect_params[effect][parameter] = value + print(f"✓ 特效参数已设置: {effect}.{parameter} = {value}") + else: + print(f"✗ 无效的特效参数: {effect}.{parameter}") + + except Exception as e: + print(f"✗ 特效参数设置失败: {e}") + + def get_effect_parameters(self) -> Dict[str, Dict[str, Any]]: + """ + 获取特效参数 + + Returns: + 特效参数字典 + """ + return self.effect_params.copy() + + def render_ecosystem_health(self): + """渲染生态系统健康度可视化""" + try: + if not self.enabled or not self.visualization_types['ecosystem_health']['enabled']: + return + + # 获取健康度数据 + health_data = self.get_cached_visualization_data('ecosystem_health') + if health_data is None: + # 如果没有缓存数据,计算新的数据 + if self.plugin.ecosystem_manager: + system_state = self.plugin.ecosystem_manager.get_system_state() + health_data = system_state.get('health_index', 0.5) + + if health_data is not None: + # 在实际实现中,这里会渲染健康度可视化效果 + # 比如根据健康度值改变颜色、大小等 + + # 根据健康度值选择颜色 + color = self._get_health_color(health_data) + + # 应用可视化效果 + print(f"✓ 渲染生态系统健康度可视化: 健康度={health_data:.2f}, 颜色={color}") + + except Exception as e: + print(f"✗ 生态系统健康度可视化渲染失败: {e}") + + def _get_health_color(self, health: float) -> tuple: + """ + 根据健康度获取颜色 + + Args: + health: 健康度值 (0.0-1.0) + + Returns: + RGB颜色元组 + """ + try: + # 使用预定义的颜色渐变 + gradient = self.effect_params['health_visualization']['color_gradient'] + + # 确保健康度在有效范围内 + health = max(0.0, min(1.0, health)) + + # 简单的三色插值 + if health < 0.5: + # 红色到黄色 + t = health * 2.0 + r = gradient[0][0] + (gradient[1][0] - gradient[0][0]) * t + g = gradient[0][1] + (gradient[1][1] - gradient[0][1]) * t + b = gradient[0][2] + (gradient[1][2] - gradient[0][2]) * t + else: + # 黄色到绿色 + t = (health - 0.5) * 2.0 + r = gradient[1][0] + (gradient[2][0] - gradient[1][0]) * t + g = gradient[1][1] + (gradient[2][1] - gradient[1][1]) * t + b = gradient[1][2] + (gradient[2][2] - gradient[1][2]) * t + + return (r, g, b) + + except Exception as e: + print(f"✗ 健康度颜色计算失败: {e}") + return (0.5, 0.5, 0.5) + + def render_biodiversity_map(self): + """渲染生物多样性地图""" + try: + if not self.enabled or not self.visualization_types['biodiversity_map']['enabled']: + return + + # 获取多样性数据 + biodiversity_data = self.get_cached_visualization_data('biodiversity_map') + if biodiversity_data is None: + # 计算多样性数据 + biodiversity_data = 0.5 # 默认值 + + if self.plugin.vegetation_manager and self.plugin.animal_simulator: + veg_stats = self.plugin.vegetation_manager.get_stats() + animal_stats = self.plugin.animal_simulator.get_stats() + + veg_count = veg_stats.get('total_vegetation', 0) + animal_count = animal_stats.get('total_animals', 0) + + biodiversity_data = min(1.0, (veg_count + animal_count) / 1000.0) + + # 在实际实现中,这里会渲染多样性热力图 + print(f"✓ 渲染生物多样性地图: 多样性指数={biodiversity_data:.2f}") + + except Exception as e: + print(f"✗ 生物多样性地图渲染失败: {e}") + + def render_interaction_trails(self): + """渲染交互轨迹""" + try: + if not self.enabled or not self.visualization_types['interaction_trails']['enabled']: + return + + # 在实际实现中,这里会渲染交互轨迹效果 + print("✓ 渲染交互轨迹可视化") + + except Exception as e: + print(f"✗ 交互轨迹渲染失败: {e}") + + def get_visualization_stats(self) -> Dict[str, Any]: + """ + 获取可视化统计信息 + + Returns: + 统计信息字典 + """ + # 计算缓存命中率 + total_cache_accesses = self.visual_stats['cache_hits'] + self.visual_stats['cache_misses'] + cache_hit_rate = 0.0 + if total_cache_accesses > 0: + cache_hit_rate = self.visual_stats['cache_hits'] / total_cache_accesses + + stats = self.visual_stats.copy() + stats['cache_hit_rate'] = cache_hit_rate + stats['active_objects'] = len(self.visualization_objects) + + return stats + + def reset_visualization_stats(self): + """重置可视化统计信息""" + try: + self.visual_stats = { + 'visualizations_created': 0, + 'visualizations_updated': 0, + 'cache_hits': 0, + 'cache_misses': 0 + } + print("✓ 可视化统计信息已重置") + except Exception as e: + print(f"✗ 可视化统计信息重置失败: {e}") + + def clear_visualization_cache(self): + """清空可视化缓存""" + try: + self.visualization_cache.clear() + print("✓ 可视化缓存已清空") + except Exception as e: + print(f"✗ 可视化缓存清空失败: {e}") + + def cleanup_expired_cache(self): + """清理过期缓存""" + try: + current_time = time.time() + expired_keys = [] + + for key, (timestamp, _) in self.visualization_cache.items(): + if current_time - timestamp > self.cache_config['expiration_time']: + expired_keys.append(key) + + for key in expired_keys: + del self.visualization_cache[key] + + if expired_keys: + print(f"✓ 已清理 {len(expired_keys)} 个过期缓存项") + + except Exception as e: + print(f"✗ 过期缓存清理失败: {e}") + + def set_color_scheme(self, scheme: str): + """ + 设置颜色方案 + + Args: + scheme: 颜色方案名称 + """ + try: + self.visualization_config['color_scheme'] = scheme + print(f"✓ 颜色方案已设置为: {scheme}") + except Exception as e: + print(f"✗ 颜色方案设置失败: {e}") + + def get_available_color_schemes(self) -> List[str]: + """ + 获取可用的颜色方案 + + Returns: + 颜色方案列表 + """ + # 在实际实现中,这里会返回支持的颜色方案列表 + return ['default', 'warm', 'cool', 'contrast'] + + def capture_visualization_screenshot(self, filename: str = None) -> str: + """ + 捕获可视化截图 + + Args: + filename: 文件名 + + Returns: + 截图文件路径 + """ + try: + if filename is None: + timestamp = int(time.time()) + filename = f"ecovisual_{timestamp}.png" + + # 在实际实现中,这里会调用渲染API捕获屏幕 + print(f"✓ 可视化截图已保存: {filename}") + return filename + + except Exception as e: + print(f"✗ 可视化截图捕获失败: {e}") + return "" + + def export_visualization_data(self, filename: str) -> bool: + """ + 导出可视化数据 + + Args: + filename: 文件名 + + Returns: + 是否导出成功 + """ + try: + # 创建导出数据 + export_data = { + 'visualization_config': self.visualization_config, + 'effect_params': self.effect_params, + 'visualization_types': self.visualization_types, + 'stats': self.get_visualization_stats(), + 'timestamp': time.time() + } + + # 在实际实现中,这里会将数据保存到文件 + print(f"✓ 可视化数据已导出到: {filename}") + return True + + except Exception as e: + print(f"✗ 可视化数据导出失败: {e}") + return False + + def import_visualization_data(self, filename: str) -> bool: + """ + 导入可视化数据 + + Args: + filename: 文件名 + + Returns: + 是否导入成功 + """ + try: + # 在实际实现中,这里会从文件加载数据 + print(f"✓ 可视化数据已从 {filename} 导入") + return True + + except Exception as e: + print(f"✗ 可视化数据导入失败: {e}") return False \ No newline at end of file diff --git a/plugins/user/vegetation_ecosystem/visualization/eco_visualizer.py b/plugins/user/vegetation_ecosystem/visualization/eco_visualizer.py index 1ba759c6..f8413604 100644 --- a/plugins/user/vegetation_ecosystem/visualization/eco_visualizer.py +++ b/plugins/user/vegetation_ecosystem/visualization/eco_visualizer.py @@ -1,690 +1,690 @@ -""" -生态系统可视化器 -负责提供植被和生态系统的可视化表示 -""" - -import time -from typing import Dict, Any, List, Optional -import math -import random - -class EcoVisualizer: - """ - 生态系统可视化器 - 负责提供植被和生态系统的可视化表示,包括渲染、着色和特效 - """ - - def __init__(self, plugin): - """ - 初始化生态系统可视化器 - - Args: - plugin: 植被和生态系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 渲染配置 - self.render_config = { - 'render_distance': 100.0, # 渲染距离 - 'lod_levels': 5, # LOD级别数 - 'max_instances': 5000, # 最大实例数 - 'billboard_threshold': 50.0, # 公告牌阈值 - 'fade_distance': 80.0, # 淡出距离 - 'render_vegetation': True, # 渲染植被 - 'render_animals': True, # 渲染动物 - 'render_effects': True # 渲染特效 - } - - # 着色配置 - self.shading_config = { - 'use_dynamic_lighting': True, # 使用动态光照 - 'shadow_quality': 'medium', # 阴影质量 - 'wind_effect': 0.5, # 风效强度 - 'seasonal_tint': True, # 季节色调 - 'health_based_color': True, # 基于健康的颜色 - 'specular_intensity': 0.3 # 镜面反射强度 - } - - # 特效配置 - self.effect_config = { - 'particle_effects': True, # 粒子特效 - 'bloom_effect': True, # 泛光效果 - 'depth_of_field': False, # 景深效果 - 'motion_blur': False, # 运动模糊 - 'color_grading': 'natural', # 色彩分级 - 'fog_effect': True # 雾效 - } - - # LOD系统 - self.lod_system = { - 'levels': [ - {'distance': 0, 'detail': 'high', 'polygon_count': 1000}, - {'distance': 20, 'detail': 'medium', 'polygon_count': 500}, - {'distance': 40, 'detail': 'low', 'polygon_count': 200}, - {'distance': 60, 'detail': 'very_low', 'polygon_count': 50}, - {'distance': 80, 'detail': 'billboard', 'polygon_count': 1} - ], - 'transition_smoothness': 0.5 # LOD过渡平滑度 - } - - # 颜色方案 - self.color_schemes = { - 'vegetation': { - 'healthy': (0.2, 0.8, 0.2), # 健康绿色 - 'stressed': (0.8, 0.8, 0.2), # 压力黄色 - 'dying': (0.8, 0.2, 0.2), # 濒死红色 - 'dormant': (0.5, 0.5, 0.5) # 休眠灰色 - }, - 'seasons': { - 'spring': (1.0, 1.0, 0.8), # 春季色调 - 'summer': (1.0, 0.9, 0.7), # 夏季色调 - 'autumn': (1.0, 0.7, 0.5), # 秋季色调 - 'winter': (0.8, 0.8, 1.0) # 冬季色调 - } - } - - # 相机配置 - self.camera_config = { - 'position': (0, 0, 0), - 'rotation': (0, 0, 0), - 'fov': 60.0, - 'near_clip': 0.1, - 'far_clip': 1000.0 - } - - # 可视化统计 - self.visualization_stats = { - 'rendered_instances': 0, - 'draw_calls': 0, - 'render_time': 0.0, - 'lod_transitions': 0, - 'effects_applied': 0 - } - - # 缓存系统 - self.render_cache = {} - self.cache_config = { - 'max_cache_size': 100, - 'cache_timeout': 300 # 5分钟 - } - - print("✓ 生态系统可视化器已创建") - - def initialize(self) -> bool: - """ - 初始化生态系统可视化器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 生态系统可视化器初始化完成") - return True - - except Exception as e: - print(f"✗ 生态系统可视化器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用生态系统可视化器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 生态系统可视化器未初始化") - return False - - self.enabled = True - print("✓ 生态系统可视化器已启用") - return True - - except Exception as e: - print(f"✗ 生态系统可视化器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用生态系统可视化器""" - try: - self.enabled = False - print("✓ 生态系统可视化器已禁用") - - except Exception as e: - print(f"✗ 生态系统可视化器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理生态系统可视化器资源""" - try: - self.disable() - self.render_cache.clear() - self.initialized = False - print("✓ 生态系统可视化器资源已清理") - - except Exception as e: - print(f"✗ 生态系统可视化器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新可视化器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新相机位置 - self._update_camera(dt) - - # 更新LOD系统 - self._update_lod_system(dt) - - # 更新特效 - self._update_effects(dt) - - # 清理缓存 - self._cleanup_cache() - - except Exception as e: - print(f"✗ 生态系统可视化器更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_camera(self, dt: float): - """更新相机状态""" - try: - # 在实际实现中,这里会根据用户输入或脚本更新相机位置 - pass - except Exception as e: - print(f"✗ 相机更新失败: {e}") - - def _update_lod_system(self, dt: float): - """更新LOD系统""" - try: - # 在实际实现中,这里会根据相机距离更新LOD级别 - pass - except Exception as e: - print(f"✗ LOD系统更新失败: {e}") - - def _update_effects(self, dt: float): - """更新特效系统""" - try: - # 在实际实现中,这里会更新各种视觉特效 - pass - except Exception as e: - print(f"✗ 特效系统更新失败: {e}") - - def _cleanup_cache(self): - """清理渲染缓存""" - try: - current_time = time.time() - expired_keys = [] - - for key, (timestamp, _) in self.render_cache.items(): - if current_time - timestamp > self.cache_config['cache_timeout']: - expired_keys.append(key) - - for key in expired_keys: - del self.render_cache[key] - - except Exception as e: - print(f"✗ 缓存清理失败: {e}") - - def render_vegetation(self): - """渲染植被""" - try: - if not self.enabled or not self.render_config['render_vegetation']: - return - - if not self.plugin.vegetation_manager: - return - - # 获取所有植被实例 - instances = self.plugin.vegetation_manager.get_all_vegetation_instances() - - rendered_count = 0 - for instance_id, instance in instances.items(): - if not instance['alive']: - continue - - if rendered_count >= self.render_config['max_instances']: - break - - # 计算LOD级别 - lod_level = self._calculate_lod_level(instance['position']) - - # 获取植被类型信息 - veg_type = instance['type'] - veg_info = self.plugin.vegetation_manager.get_vegetation_info(veg_type) - - if not veg_info: - continue - - # 计算颜色 - color = self._calculate_vegetation_color(instance, veg_info) - - # 应用季节色调 - if self.shading_config['seasonal_tint'] and self.plugin.seasonal_effects: - seasonal_color = self.plugin.seasonal_effects.get_vegetation_change_factor('leaf_color') - if isinstance(seasonal_color, tuple) and len(seasonal_color) == 3: - color = tuple(color[i] * seasonal_color[i] for i in range(3)) - - # 应用风效 - wind_effect = self.shading_config['wind_effect'] - - # 在实际实现中,这里会调用渲染API进行渲染 - rendered_count += 1 - - self.visualization_stats['rendered_instances'] = rendered_count - self.visualization_stats['draw_calls'] += rendered_count - - except Exception as e: - print(f"✗ 植被渲染失败: {e}") - - def _calculate_lod_level(self, position: tuple) -> int: - """ - 计算LOD级别 - - Args: - position: 位置坐标 - - Returns: - LOD级别索引 - """ - try: - camera_pos = self.camera_config['position'] - distance = math.sqrt( - (position[0] - camera_pos[0])**2 + - (position[1] - camera_pos[1])**2 + - (position[2] - camera_pos[2])**2 - ) - - # 根据距离选择LOD级别 - for i, level in enumerate(self.lod_system['levels']): - if distance <= level['distance']: - return min(i, len(self.lod_system['levels']) - 1) - - return len(self.lod_system['levels']) - 1 - - except Exception as e: - print(f"✗ LOD级别计算失败: {e}") - return 0 - - def _calculate_vegetation_color(self, instance: Dict[str, Any], veg_info: Dict[str, Any]) -> tuple: - """ - 计算植被颜色 - - Args: - instance: 植被实例 - veg_info: 植被信息 - - Returns: - RGB颜色元组 - """ - try: - base_color = veg_info.get('color', (0.2, 0.8, 0.2)) - - # 基于健康度调整颜色 - if self.shading_config['health_based_color']: - health = instance['health'] - if health > 0.7: - color = self.color_schemes['vegetation']['healthy'] - elif health > 0.3: - color = self.color_schemes['vegetation']['stressed'] - elif health > 0.1: - color = self.color_schemes['vegetation']['dying'] - else: - color = self.color_schemes['vegetation']['dormant'] - - # 混合基础颜色和健康颜色 - color = tuple((base_color[i] + color[i]) / 2 for i in range(3)) - else: - color = base_color - - return color - - except Exception as e: - print(f"✗ 植被颜色计算失败: {e}") - return (0.2, 0.8, 0.2) - - def render_animals(self): - """渲染动物""" - try: - if not self.enabled or not self.render_config['render_animals']: - return - - if not self.plugin.animal_simulator: - return - - # 获取所有动物实例 - instances = self.plugin.animal_simulator.get_all_animal_instances() - - for instance_id, instance in instances.items(): - if not instance['alive']: - continue - - # 获取动物种类信息 - species = instance['species'] - species_info = self.plugin.animal_simulator.get_animal_info(species) - - if not species_info: - continue - - # 计算LOD级别 - lod_level = self._calculate_lod_level(instance['position']) - - # 在实际实现中,这里会调用渲染API进行渲染 - - self.visualization_stats['draw_calls'] += len(instances) - - except Exception as e: - print(f"✗ 动物渲染失败: {e}") - - def render_ecosystem_effects(self): - """渲染生态系统特效""" - try: - if not self.enabled or not self.render_config['render_effects']: - return - - # 在实际实现中,这里会渲染各种生态系统特效 - # 如花粉飘散、落叶、动物足迹等 - - self.visualization_stats['effects_applied'] += 1 - - except Exception as e: - print(f"✗ 生态系统特效渲染失败: {e}") - - def set_render_config(self, config: Dict[str, Any]): - """ - 设置渲染配置 - - Args: - config: 渲染配置字典 - """ - try: - self.render_config.update(config) - print(f"✓ 渲染配置已更新: {self.render_config}") - except Exception as e: - print(f"✗ 渲染配置更新失败: {e}") - - def get_render_config(self) -> Dict[str, Any]: - """ - 获取渲染配置 - - Returns: - 渲染配置字典 - """ - return self.render_config.copy() - - def set_shading_config(self, config: Dict[str, Any]): - """ - 设置着色配置 - - Args: - config: 着色配置字典 - """ - try: - self.shading_config.update(config) - print(f"✓ 着色配置已更新: {self.shading_config}") - except Exception as e: - print(f"✗ 着色配置更新失败: {e}") - - def get_shading_config(self) -> Dict[str, Any]: - """ - 获取着色配置 - - Returns: - 着色配置字典 - """ - return self.shading_config.copy() - - def set_effect_config(self, config: Dict[str, Any]): - """ - 设置特效配置 - - Args: - config: 特效配置字典 - """ - try: - self.effect_config.update(config) - print(f"✓ 特效配置已更新: {self.effect_config}") - except Exception as e: - print(f"✗ 特效配置更新失败: {e}") - - def get_effect_config(self) -> Dict[str, Any]: - """ - 获取特效配置 - - Returns: - 特效配置字典 - """ - return self.effect_config.copy() - - def set_camera_config(self, config: Dict[str, Any]): - """ - 设置相机配置 - - Args: - config: 相机配置字典 - """ - try: - self.camera_config.update(config) - print(f"✓ 相机配置已更新: {self.camera_config}") - except Exception as e: - print(f"✗ 相机配置更新失败: {e}") - - def get_camera_config(self) -> Dict[str, Any]: - """ - 获取相机配置 - - Returns: - 相机配置字典 - """ - return self.camera_config.copy() - - def get_lod_system(self) -> Dict[str, Any]: - """ - 获取LOD系统配置 - - Returns: - LOD系统配置字典 - """ - return self.lod_system.copy() - - def set_lod_level(self, level: int, distance: float, detail: str, polygon_count: int): - """ - 设置LOD级别 - - Args: - level: LOD级别索引 - distance: 距离阈值 - detail: 细节级别 - polygon_count: 多边形数量 - """ - try: - if 0 <= level < len(self.lod_system['levels']): - self.lod_system['levels'][level] = { - 'distance': distance, - 'detail': detail, - 'polygon_count': polygon_count - } - print(f"✓ LOD级别 {level} 已更新") - else: - print(f"✗ 无效的LOD级别: {level}") - - except Exception as e: - print(f"✗ LOD级别设置失败: {e}") - - def get_color_schemes(self) -> Dict[str, Dict[str, tuple]]: - """ - 获取颜色方案 - - Returns: - 颜色方案字典 - """ - return self.color_schemes.copy() - - def set_color_scheme(self, category: str, scheme_name: str, color: tuple): - """ - 设置颜色方案 - - Args: - category: 颜色类别 - scheme_name: 方案名称 - color: RGB颜色元组 - """ - try: - if category in self.color_schemes: - self.color_schemes[category][scheme_name] = color - print(f"✓ 颜色方案已更新: {category}.{scheme_name}") - else: - print(f"✗ 无效的颜色类别: {category}") - - except Exception as e: - print(f"✗ 颜色方案设置失败: {e}") - - def get_visualization_stats(self) -> Dict[str, Any]: - """ - 获取可视化统计信息 - - Returns: - 统计信息字典 - """ - return self.visualization_stats.copy() - - def reset_visualization_stats(self): - """重置可视化统计信息""" - try: - self.visualization_stats = { - 'rendered_instances': 0, - 'draw_calls': 0, - 'render_time': 0.0, - 'lod_transitions': 0, - 'effects_applied': 0 - } - print("✓ 可视化统计信息已重置") - except Exception as e: - print(f"✗ 可视化统计信息重置失败: {e}") - - def clear_render_cache(self): - """清空渲染缓存""" - try: - self.render_cache.clear() - print("✓ 渲染缓存已清空") - except Exception as e: - print(f"✗ 渲染缓存清空失败: {e}") - - def get_cache_info(self) -> Dict[str, Any]: - """ - 获取缓存信息 - - Returns: - 缓存信息字典 - """ - try: - return { - 'cache_size': len(self.render_cache), - 'max_size': self.cache_config['max_cache_size'], - 'timeout': self.cache_config['cache_timeout'] - } - except Exception as e: - print(f"✗ 缓存信息获取失败: {e}") - return {} - - def set_cache_config(self, config: Dict[str, Any]): - """ - 设置缓存配置 - - Args: - config: 缓存配置字典 - """ - try: - self.cache_config.update(config) - print(f"✓ 缓存配置已更新: {self.cache_config}") - except Exception as e: - print(f"✗ 缓存配置更新失败: {e}") - - def capture_screenshot(self, filename: str = None) -> str: - """ - 截取屏幕截图 - - Args: - filename: 文件名(如果为None则自动生成) - - Returns: - 截图文件路径 - """ - try: - if filename is None: - timestamp = int(time.time()) - filename = f"ecosystem_screenshot_{timestamp}.png" - - # 在实际实现中,这里会调用渲染API截取屏幕 - print(f"✓ 屏幕截图已保存: {filename}") - return filename - - except Exception as e: - print(f"✗ 屏幕截图失败: {e}") - return "" - - def render_heightmap(self) -> bool: - """ - 渲染高度图 - - Returns: - 是否渲染成功 - """ - try: - # 在实际实现中,这里会渲染地形高度图 - print("✓ 高度图渲染完成") - return True - - except Exception as e: - print(f"✗ 高度图渲染失败: {e}") - return False - - def render_vegetation_density_map(self) -> bool: - """ - 渲染植被密度图 - - Returns: - 是否渲染成功 - """ - try: - # 在实际实现中,这里会渲染植被密度图 - print("✓ 植被密度图渲染完成") - return True - - except Exception as e: - print(f"✗ 植被密度图渲染失败: {e}") - return False - - def toggle_wireframe_mode(self): - """切换线框模式""" - try: - # 在实际实现中,这里会切换渲染模式 - print("✓ 线框模式已切换") - except Exception as e: - print(f"✗ 线框模式切换失败: {e}") - - def toggle_debug_info(self): - """切换调试信息显示""" - try: - # 在实际实现中,这里会切换调试信息显示 - print("✓ 调试信息显示已切换") - except Exception as e: +""" +生态系统可视化器 +负责提供植被和生态系统的可视化表示 +""" + +import time +from typing import Dict, Any, List, Optional +import math +import random + +class EcoVisualizer: + """ + 生态系统可视化器 + 负责提供植被和生态系统的可视化表示,包括渲染、着色和特效 + """ + + def __init__(self, plugin): + """ + 初始化生态系统可视化器 + + Args: + plugin: 植被和生态系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 渲染配置 + self.render_config = { + 'render_distance': 100.0, # 渲染距离 + 'lod_levels': 5, # LOD级别数 + 'max_instances': 5000, # 最大实例数 + 'billboard_threshold': 50.0, # 公告牌阈值 + 'fade_distance': 80.0, # 淡出距离 + 'render_vegetation': True, # 渲染植被 + 'render_animals': True, # 渲染动物 + 'render_effects': True # 渲染特效 + } + + # 着色配置 + self.shading_config = { + 'use_dynamic_lighting': True, # 使用动态光照 + 'shadow_quality': 'medium', # 阴影质量 + 'wind_effect': 0.5, # 风效强度 + 'seasonal_tint': True, # 季节色调 + 'health_based_color': True, # 基于健康的颜色 + 'specular_intensity': 0.3 # 镜面反射强度 + } + + # 特效配置 + self.effect_config = { + 'particle_effects': True, # 粒子特效 + 'bloom_effect': True, # 泛光效果 + 'depth_of_field': False, # 景深效果 + 'motion_blur': False, # 运动模糊 + 'color_grading': 'natural', # 色彩分级 + 'fog_effect': True # 雾效 + } + + # LOD系统 + self.lod_system = { + 'levels': [ + {'distance': 0, 'detail': 'high', 'polygon_count': 1000}, + {'distance': 20, 'detail': 'medium', 'polygon_count': 500}, + {'distance': 40, 'detail': 'low', 'polygon_count': 200}, + {'distance': 60, 'detail': 'very_low', 'polygon_count': 50}, + {'distance': 80, 'detail': 'billboard', 'polygon_count': 1} + ], + 'transition_smoothness': 0.5 # LOD过渡平滑度 + } + + # 颜色方案 + self.color_schemes = { + 'vegetation': { + 'healthy': (0.2, 0.8, 0.2), # 健康绿色 + 'stressed': (0.8, 0.8, 0.2), # 压力黄色 + 'dying': (0.8, 0.2, 0.2), # 濒死红色 + 'dormant': (0.5, 0.5, 0.5) # 休眠灰色 + }, + 'seasons': { + 'spring': (1.0, 1.0, 0.8), # 春季色调 + 'summer': (1.0, 0.9, 0.7), # 夏季色调 + 'autumn': (1.0, 0.7, 0.5), # 秋季色调 + 'winter': (0.8, 0.8, 1.0) # 冬季色调 + } + } + + # 相机配置 + self.camera_config = { + 'position': (0, 0, 0), + 'rotation': (0, 0, 0), + 'fov': 60.0, + 'near_clip': 0.1, + 'far_clip': 1000.0 + } + + # 可视化统计 + self.visualization_stats = { + 'rendered_instances': 0, + 'draw_calls': 0, + 'render_time': 0.0, + 'lod_transitions': 0, + 'effects_applied': 0 + } + + # 缓存系统 + self.render_cache = {} + self.cache_config = { + 'max_cache_size': 100, + 'cache_timeout': 300 # 5分钟 + } + + print("✓ 生态系统可视化器已创建") + + def initialize(self) -> bool: + """ + 初始化生态系统可视化器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 生态系统可视化器初始化完成") + return True + + except Exception as e: + print(f"✗ 生态系统可视化器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用生态系统可视化器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 生态系统可视化器未初始化") + return False + + self.enabled = True + print("✓ 生态系统可视化器已启用") + return True + + except Exception as e: + print(f"✗ 生态系统可视化器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用生态系统可视化器""" + try: + self.enabled = False + print("✓ 生态系统可视化器已禁用") + + except Exception as e: + print(f"✗ 生态系统可视化器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理生态系统可视化器资源""" + try: + self.disable() + self.render_cache.clear() + self.initialized = False + print("✓ 生态系统可视化器资源已清理") + + except Exception as e: + print(f"✗ 生态系统可视化器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新可视化器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新相机位置 + self._update_camera(dt) + + # 更新LOD系统 + self._update_lod_system(dt) + + # 更新特效 + self._update_effects(dt) + + # 清理缓存 + self._cleanup_cache() + + except Exception as e: + print(f"✗ 生态系统可视化器更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_camera(self, dt: float): + """更新相机状态""" + try: + # 在实际实现中,这里会根据用户输入或脚本更新相机位置 + pass + except Exception as e: + print(f"✗ 相机更新失败: {e}") + + def _update_lod_system(self, dt: float): + """更新LOD系统""" + try: + # 在实际实现中,这里会根据相机距离更新LOD级别 + pass + except Exception as e: + print(f"✗ LOD系统更新失败: {e}") + + def _update_effects(self, dt: float): + """更新特效系统""" + try: + # 在实际实现中,这里会更新各种视觉特效 + pass + except Exception as e: + print(f"✗ 特效系统更新失败: {e}") + + def _cleanup_cache(self): + """清理渲染缓存""" + try: + current_time = time.time() + expired_keys = [] + + for key, (timestamp, _) in self.render_cache.items(): + if current_time - timestamp > self.cache_config['cache_timeout']: + expired_keys.append(key) + + for key in expired_keys: + del self.render_cache[key] + + except Exception as e: + print(f"✗ 缓存清理失败: {e}") + + def render_vegetation(self): + """渲染植被""" + try: + if not self.enabled or not self.render_config['render_vegetation']: + return + + if not self.plugin.vegetation_manager: + return + + # 获取所有植被实例 + instances = self.plugin.vegetation_manager.get_all_vegetation_instances() + + rendered_count = 0 + for instance_id, instance in instances.items(): + if not instance['alive']: + continue + + if rendered_count >= self.render_config['max_instances']: + break + + # 计算LOD级别 + lod_level = self._calculate_lod_level(instance['position']) + + # 获取植被类型信息 + veg_type = instance['type'] + veg_info = self.plugin.vegetation_manager.get_vegetation_info(veg_type) + + if not veg_info: + continue + + # 计算颜色 + color = self._calculate_vegetation_color(instance, veg_info) + + # 应用季节色调 + if self.shading_config['seasonal_tint'] and self.plugin.seasonal_effects: + seasonal_color = self.plugin.seasonal_effects.get_vegetation_change_factor('leaf_color') + if isinstance(seasonal_color, tuple) and len(seasonal_color) == 3: + color = tuple(color[i] * seasonal_color[i] for i in range(3)) + + # 应用风效 + wind_effect = self.shading_config['wind_effect'] + + # 在实际实现中,这里会调用渲染API进行渲染 + rendered_count += 1 + + self.visualization_stats['rendered_instances'] = rendered_count + self.visualization_stats['draw_calls'] += rendered_count + + except Exception as e: + print(f"✗ 植被渲染失败: {e}") + + def _calculate_lod_level(self, position: tuple) -> int: + """ + 计算LOD级别 + + Args: + position: 位置坐标 + + Returns: + LOD级别索引 + """ + try: + camera_pos = self.camera_config['position'] + distance = math.sqrt( + (position[0] - camera_pos[0])**2 + + (position[1] - camera_pos[1])**2 + + (position[2] - camera_pos[2])**2 + ) + + # 根据距离选择LOD级别 + for i, level in enumerate(self.lod_system['levels']): + if distance <= level['distance']: + return min(i, len(self.lod_system['levels']) - 1) + + return len(self.lod_system['levels']) - 1 + + except Exception as e: + print(f"✗ LOD级别计算失败: {e}") + return 0 + + def _calculate_vegetation_color(self, instance: Dict[str, Any], veg_info: Dict[str, Any]) -> tuple: + """ + 计算植被颜色 + + Args: + instance: 植被实例 + veg_info: 植被信息 + + Returns: + RGB颜色元组 + """ + try: + base_color = veg_info.get('color', (0.2, 0.8, 0.2)) + + # 基于健康度调整颜色 + if self.shading_config['health_based_color']: + health = instance['health'] + if health > 0.7: + color = self.color_schemes['vegetation']['healthy'] + elif health > 0.3: + color = self.color_schemes['vegetation']['stressed'] + elif health > 0.1: + color = self.color_schemes['vegetation']['dying'] + else: + color = self.color_schemes['vegetation']['dormant'] + + # 混合基础颜色和健康颜色 + color = tuple((base_color[i] + color[i]) / 2 for i in range(3)) + else: + color = base_color + + return color + + except Exception as e: + print(f"✗ 植被颜色计算失败: {e}") + return (0.2, 0.8, 0.2) + + def render_animals(self): + """渲染动物""" + try: + if not self.enabled or not self.render_config['render_animals']: + return + + if not self.plugin.animal_simulator: + return + + # 获取所有动物实例 + instances = self.plugin.animal_simulator.get_all_animal_instances() + + for instance_id, instance in instances.items(): + if not instance['alive']: + continue + + # 获取动物种类信息 + species = instance['species'] + species_info = self.plugin.animal_simulator.get_animal_info(species) + + if not species_info: + continue + + # 计算LOD级别 + lod_level = self._calculate_lod_level(instance['position']) + + # 在实际实现中,这里会调用渲染API进行渲染 + + self.visualization_stats['draw_calls'] += len(instances) + + except Exception as e: + print(f"✗ 动物渲染失败: {e}") + + def render_ecosystem_effects(self): + """渲染生态系统特效""" + try: + if not self.enabled or not self.render_config['render_effects']: + return + + # 在实际实现中,这里会渲染各种生态系统特效 + # 如花粉飘散、落叶、动物足迹等 + + self.visualization_stats['effects_applied'] += 1 + + except Exception as e: + print(f"✗ 生态系统特效渲染失败: {e}") + + def set_render_config(self, config: Dict[str, Any]): + """ + 设置渲染配置 + + Args: + config: 渲染配置字典 + """ + try: + self.render_config.update(config) + print(f"✓ 渲染配置已更新: {self.render_config}") + except Exception as e: + print(f"✗ 渲染配置更新失败: {e}") + + def get_render_config(self) -> Dict[str, Any]: + """ + 获取渲染配置 + + Returns: + 渲染配置字典 + """ + return self.render_config.copy() + + def set_shading_config(self, config: Dict[str, Any]): + """ + 设置着色配置 + + Args: + config: 着色配置字典 + """ + try: + self.shading_config.update(config) + print(f"✓ 着色配置已更新: {self.shading_config}") + except Exception as e: + print(f"✗ 着色配置更新失败: {e}") + + def get_shading_config(self) -> Dict[str, Any]: + """ + 获取着色配置 + + Returns: + 着色配置字典 + """ + return self.shading_config.copy() + + def set_effect_config(self, config: Dict[str, Any]): + """ + 设置特效配置 + + Args: + config: 特效配置字典 + """ + try: + self.effect_config.update(config) + print(f"✓ 特效配置已更新: {self.effect_config}") + except Exception as e: + print(f"✗ 特效配置更新失败: {e}") + + def get_effect_config(self) -> Dict[str, Any]: + """ + 获取特效配置 + + Returns: + 特效配置字典 + """ + return self.effect_config.copy() + + def set_camera_config(self, config: Dict[str, Any]): + """ + 设置相机配置 + + Args: + config: 相机配置字典 + """ + try: + self.camera_config.update(config) + print(f"✓ 相机配置已更新: {self.camera_config}") + except Exception as e: + print(f"✗ 相机配置更新失败: {e}") + + def get_camera_config(self) -> Dict[str, Any]: + """ + 获取相机配置 + + Returns: + 相机配置字典 + """ + return self.camera_config.copy() + + def get_lod_system(self) -> Dict[str, Any]: + """ + 获取LOD系统配置 + + Returns: + LOD系统配置字典 + """ + return self.lod_system.copy() + + def set_lod_level(self, level: int, distance: float, detail: str, polygon_count: int): + """ + 设置LOD级别 + + Args: + level: LOD级别索引 + distance: 距离阈值 + detail: 细节级别 + polygon_count: 多边形数量 + """ + try: + if 0 <= level < len(self.lod_system['levels']): + self.lod_system['levels'][level] = { + 'distance': distance, + 'detail': detail, + 'polygon_count': polygon_count + } + print(f"✓ LOD级别 {level} 已更新") + else: + print(f"✗ 无效的LOD级别: {level}") + + except Exception as e: + print(f"✗ LOD级别设置失败: {e}") + + def get_color_schemes(self) -> Dict[str, Dict[str, tuple]]: + """ + 获取颜色方案 + + Returns: + 颜色方案字典 + """ + return self.color_schemes.copy() + + def set_color_scheme(self, category: str, scheme_name: str, color: tuple): + """ + 设置颜色方案 + + Args: + category: 颜色类别 + scheme_name: 方案名称 + color: RGB颜色元组 + """ + try: + if category in self.color_schemes: + self.color_schemes[category][scheme_name] = color + print(f"✓ 颜色方案已更新: {category}.{scheme_name}") + else: + print(f"✗ 无效的颜色类别: {category}") + + except Exception as e: + print(f"✗ 颜色方案设置失败: {e}") + + def get_visualization_stats(self) -> Dict[str, Any]: + """ + 获取可视化统计信息 + + Returns: + 统计信息字典 + """ + return self.visualization_stats.copy() + + def reset_visualization_stats(self): + """重置可视化统计信息""" + try: + self.visualization_stats = { + 'rendered_instances': 0, + 'draw_calls': 0, + 'render_time': 0.0, + 'lod_transitions': 0, + 'effects_applied': 0 + } + print("✓ 可视化统计信息已重置") + except Exception as e: + print(f"✗ 可视化统计信息重置失败: {e}") + + def clear_render_cache(self): + """清空渲染缓存""" + try: + self.render_cache.clear() + print("✓ 渲染缓存已清空") + except Exception as e: + print(f"✗ 渲染缓存清空失败: {e}") + + def get_cache_info(self) -> Dict[str, Any]: + """ + 获取缓存信息 + + Returns: + 缓存信息字典 + """ + try: + return { + 'cache_size': len(self.render_cache), + 'max_size': self.cache_config['max_cache_size'], + 'timeout': self.cache_config['cache_timeout'] + } + except Exception as e: + print(f"✗ 缓存信息获取失败: {e}") + return {} + + def set_cache_config(self, config: Dict[str, Any]): + """ + 设置缓存配置 + + Args: + config: 缓存配置字典 + """ + try: + self.cache_config.update(config) + print(f"✓ 缓存配置已更新: {self.cache_config}") + except Exception as e: + print(f"✗ 缓存配置更新失败: {e}") + + def capture_screenshot(self, filename: str = None) -> str: + """ + 截取屏幕截图 + + Args: + filename: 文件名(如果为None则自动生成) + + Returns: + 截图文件路径 + """ + try: + if filename is None: + timestamp = int(time.time()) + filename = f"ecosystem_screenshot_{timestamp}.png" + + # 在实际实现中,这里会调用渲染API截取屏幕 + print(f"✓ 屏幕截图已保存: {filename}") + return filename + + except Exception as e: + print(f"✗ 屏幕截图失败: {e}") + return "" + + def render_heightmap(self) -> bool: + """ + 渲染高度图 + + Returns: + 是否渲染成功 + """ + try: + # 在实际实现中,这里会渲染地形高度图 + print("✓ 高度图渲染完成") + return True + + except Exception as e: + print(f"✗ 高度图渲染失败: {e}") + return False + + def render_vegetation_density_map(self) -> bool: + """ + 渲染植被密度图 + + Returns: + 是否渲染成功 + """ + try: + # 在实际实现中,这里会渲染植被密度图 + print("✓ 植被密度图渲染完成") + return True + + except Exception as e: + print(f"✗ 植被密度图渲染失败: {e}") + return False + + def toggle_wireframe_mode(self): + """切换线框模式""" + try: + # 在实际实现中,这里会切换渲染模式 + print("✓ 线框模式已切换") + except Exception as e: + print(f"✗ 线框模式切换失败: {e}") + + def toggle_debug_info(self): + """切换调试信息显示""" + try: + # 在实际实现中,这里会切换调试信息显示 + print("✓ 调试信息显示已切换") + except Exception as e: print(f"✗ 调试信息显示切换失败: {e}") \ No newline at end of file diff --git a/plugins/user/voice_control/README.md b/plugins/user/voice_control/README.md index 0b05a2b6..a0d38e5c 100644 --- a/plugins/user/voice_control/README.md +++ b/plugins/user/voice_control/README.md @@ -1,190 +1,190 @@ -# 语音控制插件 - -## 简介 - -语音控制插件为EG引擎提供完整的语音识别和语音合成功能,支持自然语言处理、命令映射、音频设备管理等。该插件能够识别用户的语音指令并执行相应的操作,同时可以通过语音合成提供反馈。 - -## 功能特性 - -### 1. 语音识别 -- 实时语音识别功能 -- 唤醒词检测(如"小艾") -- 连续语音识别支持 -- 音频级别检测和静音检测 -- 多语言支持(中文、英文等) - -### 2. 语音合成 -- 文字转语音功能 -- 多种语音选择(男声、女声、童声等) -- 语速、音量、音调调节 -- SSML标记语言支持 -- 实时音频播放 - -### 3. 命令处理 -- 语音命令映射系统 -- 模糊匹配和精确匹配 -- 上下文感知命令处理 -- 命令链支持 -- 命令确认机制 - -### 4. 自然语言处理 -- 意图识别 -- 实体提取 -- 情感分析 -- 上下文管理 -- 多语言支持 - -### 5. 音频管理 -- 音频输入/输出设备管理 -- 音频流控制 -- 回声消除 -- 噪声抑制 -- 自动增益控制 - -### 6. 事件系统 -- 事件队列管理 -- 异步事件处理 -- 事件过滤和去重 -- 事件监听器系统 - -### 7. 配置管理 -- 完整的配置管理系统 -- 配置文件读写 -- 配置备份和恢复 -- 配置验证 -- 实时配置更新 - -### 8. 编辑器支持 -- 可视化配置界面 -- 实时监控面板 -- 语音测试工具 -- 性能统计显示 -- 综合测试功能 - -## 目录结构 - -``` -voice_control/ -├── plugin.py # 插件主文件 -├── core/ -│ └── voice_manager.py # 语音管理器 -├── recognition/ -│ └── speech_recognizer.py # 语音识别器 -├── synthesis/ -│ └── speech_synthesizer.py# 语音合成器 -├── commands/ -│ └── command_processor.py # 命令处理器 -├── audio/ -│ └── audio_manager.py # 音频管理器 -├── events/ -│ └── event_handler.py # 事件处理器 -├── nlp/ -│ └── nlp_processor.py # 自然语言处理器 -├── config/ -│ └── config_manager.py # 配置管理器 -├── editor/ -│ └── voice_editor.py # 语音编辑器 -├── utils/ -│ └── voice_utils.py # 工具类 -└── README.md # 说明文档 -``` - -## 核心模块 - -### 语音管理器 (VoiceManager) -负责管理语音识别和合成的核心功能,协调各子模块工作。 - -### 语音识别器 (SpeechRecognizer) -实现语音识别功能,处理音频输入和语音转文字。 - -### 语音合成器 (SpeechSynthesizer) -实现文字转语音功能,处理语音输出和音频播放。 - -### 命令处理器 (CommandProcessor) -处理识别到的语音命令,执行相应的操作。 - -### 音频管理器 (AudioManager) -管理音频输入输出设备,控制音频流。 - -### 事件处理器 (VoiceEventHandler) -处理语音控制相关的事件,提供事件队列和异步处理。 - -### 自然语言处理器 (NLPProcessor) -提供自然语言处理功能,包括意图识别和实体提取。 - -### 配置管理器 (VoiceConfigManager) -管理插件配置,提供配置读写和验证功能。 - -### 语音编辑器 (VoiceEditor) -提供可视化界面用于配置和测试语音控制功能。 - -### 工具类 (VoiceUtils) -提供实用工具函数,包括性能监控、日志记录、数据转换等。 - -## 使用示例 - -```python -# 初始化插件 -plugin = VoiceControlPlugin(engine) -plugin.initialize() -plugin.enable() - -# 开始语音识别 -plugin.voice_manager.start_listening() - -# 语音合成 -plugin.voice_manager.speak_text("你好,欢迎使用语音控制系统") - -# 处理命令 -plugin.command_processor.process_command("前进", 0.95) - -# 更新配置 -plugin.config_manager.set_config("general", "language", "zh-CN") -``` - -## API参考 - -详细的API文档请参考各模块的源代码注释。 - -## 配置说明 - -插件配置文件位于 `config/voice_config.json`,支持以下配置项: - -- 通用设置:启用状态、语言、唤醒词等 -- 识别设置:语言、采样率、灵敏度等 -- 合成设置:语音、语速、音量等 -- 命令设置:模糊匹配、上下文超时等 -- 音频设置:设备、回声消除、噪声抑制等 -- NLP设置:语言、意图识别阈值等 -- 事件设置:队列大小、事件过滤等 - -## 支持的设备 - -- 系统默认麦克风和扬声器 -- USB音频设备 -- 蓝牙音频设备 -- 专业音频接口 - -## 性能优化 - -- 多线程处理避免阻塞主线程 -- 智能音频级别检测减少CPU占用 -- 缓存机制提高处理速度 -- 异步事件处理提高响应性 - -## 扩展开发 - -插件采用模块化设计,易于扩展新功能: - -1. 添加新的命令类型 -2. 支持新的语言和方言 -3. 扩展自然语言处理能力 -4. 添加新的音频处理效果 - -## 许可证 - -本插件遵循EG引擎许可证协议。 - -## 支持与反馈 - +# 语音控制插件 + +## 简介 + +语音控制插件为EG引擎提供完整的语音识别和语音合成功能,支持自然语言处理、命令映射、音频设备管理等。该插件能够识别用户的语音指令并执行相应的操作,同时可以通过语音合成提供反馈。 + +## 功能特性 + +### 1. 语音识别 +- 实时语音识别功能 +- 唤醒词检测(如"小艾") +- 连续语音识别支持 +- 音频级别检测和静音检测 +- 多语言支持(中文、英文等) + +### 2. 语音合成 +- 文字转语音功能 +- 多种语音选择(男声、女声、童声等) +- 语速、音量、音调调节 +- SSML标记语言支持 +- 实时音频播放 + +### 3. 命令处理 +- 语音命令映射系统 +- 模糊匹配和精确匹配 +- 上下文感知命令处理 +- 命令链支持 +- 命令确认机制 + +### 4. 自然语言处理 +- 意图识别 +- 实体提取 +- 情感分析 +- 上下文管理 +- 多语言支持 + +### 5. 音频管理 +- 音频输入/输出设备管理 +- 音频流控制 +- 回声消除 +- 噪声抑制 +- 自动增益控制 + +### 6. 事件系统 +- 事件队列管理 +- 异步事件处理 +- 事件过滤和去重 +- 事件监听器系统 + +### 7. 配置管理 +- 完整的配置管理系统 +- 配置文件读写 +- 配置备份和恢复 +- 配置验证 +- 实时配置更新 + +### 8. 编辑器支持 +- 可视化配置界面 +- 实时监控面板 +- 语音测试工具 +- 性能统计显示 +- 综合测试功能 + +## 目录结构 + +``` +voice_control/ +├── plugin.py # 插件主文件 +├── core/ +│ └── voice_manager.py # 语音管理器 +├── recognition/ +│ └── speech_recognizer.py # 语音识别器 +├── synthesis/ +│ └── speech_synthesizer.py# 语音合成器 +├── commands/ +│ └── command_processor.py # 命令处理器 +├── audio/ +│ └── audio_manager.py # 音频管理器 +├── events/ +│ └── event_handler.py # 事件处理器 +├── nlp/ +│ └── nlp_processor.py # 自然语言处理器 +├── config/ +│ └── config_manager.py # 配置管理器 +├── editor/ +│ └── voice_editor.py # 语音编辑器 +├── utils/ +│ └── voice_utils.py # 工具类 +└── README.md # 说明文档 +``` + +## 核心模块 + +### 语音管理器 (VoiceManager) +负责管理语音识别和合成的核心功能,协调各子模块工作。 + +### 语音识别器 (SpeechRecognizer) +实现语音识别功能,处理音频输入和语音转文字。 + +### 语音合成器 (SpeechSynthesizer) +实现文字转语音功能,处理语音输出和音频播放。 + +### 命令处理器 (CommandProcessor) +处理识别到的语音命令,执行相应的操作。 + +### 音频管理器 (AudioManager) +管理音频输入输出设备,控制音频流。 + +### 事件处理器 (VoiceEventHandler) +处理语音控制相关的事件,提供事件队列和异步处理。 + +### 自然语言处理器 (NLPProcessor) +提供自然语言处理功能,包括意图识别和实体提取。 + +### 配置管理器 (VoiceConfigManager) +管理插件配置,提供配置读写和验证功能。 + +### 语音编辑器 (VoiceEditor) +提供可视化界面用于配置和测试语音控制功能。 + +### 工具类 (VoiceUtils) +提供实用工具函数,包括性能监控、日志记录、数据转换等。 + +## 使用示例 + +```python +# 初始化插件 +plugin = VoiceControlPlugin(engine) +plugin.initialize() +plugin.enable() + +# 开始语音识别 +plugin.voice_manager.start_listening() + +# 语音合成 +plugin.voice_manager.speak_text("你好,欢迎使用语音控制系统") + +# 处理命令 +plugin.command_processor.process_command("前进", 0.95) + +# 更新配置 +plugin.config_manager.set_config("general", "language", "zh-CN") +``` + +## API参考 + +详细的API文档请参考各模块的源代码注释。 + +## 配置说明 + +插件配置文件位于 `config/voice_config.json`,支持以下配置项: + +- 通用设置:启用状态、语言、唤醒词等 +- 识别设置:语言、采样率、灵敏度等 +- 合成设置:语音、语速、音量等 +- 命令设置:模糊匹配、上下文超时等 +- 音频设置:设备、回声消除、噪声抑制等 +- NLP设置:语言、意图识别阈值等 +- 事件设置:队列大小、事件过滤等 + +## 支持的设备 + +- 系统默认麦克风和扬声器 +- USB音频设备 +- 蓝牙音频设备 +- 专业音频接口 + +## 性能优化 + +- 多线程处理避免阻塞主线程 +- 智能音频级别检测减少CPU占用 +- 缓存机制提高处理速度 +- 异步事件处理提高响应性 + +## 扩展开发 + +插件采用模块化设计,易于扩展新功能: + +1. 添加新的命令类型 +2. 支持新的语言和方言 +3. 扩展自然语言处理能力 +4. 添加新的音频处理效果 + +## 许可证 + +本插件遵循EG引擎许可证协议。 + +## 支持与反馈 + 如有问题或建议,请联系开发团队或提交GitHub issue。 \ No newline at end of file diff --git a/plugins/user/voice_control/audio/audio_manager.py b/plugins/user/voice_control/audio/audio_manager.py index 5b9174c4..6d236810 100644 --- a/plugins/user/voice_control/audio/audio_manager.py +++ b/plugins/user/voice_control/audio/audio_manager.py @@ -1,752 +1,752 @@ -""" -音频管理器模块 -负责管理音频输入输出设备 -""" - -import time -from typing import Dict, Any, List, Optional -import threading - -class AudioManager: - """ - 音频管理器 - 负责管理音频输入输出设备 - """ - - def __init__(self, plugin): - """ - 初始化音频管理器 - - Args: - plugin: 语音控制插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 音频设备列表 - self.audio_devices = { - 'input': {}, - 'output': {} - } - - # 当前设备配置 - self.current_devices = { - 'input': None, - 'output': None - } - - # 音频流管理 - self.audio_streams = { - 'input': None, - 'output': None - } - - # 音频配置 - self.audio_config = { - 'sample_rate': 16000, - 'channels': 1, - 'format': 'int16', - 'input_buffer_size': 1024, - 'output_buffer_size': 1024, - 'latency': 'low', - 'enable_echo_cancellation': True, - 'enable_noise_reduction': True, - 'enable_auto_gain': True - } - - # 音频状态 - self.audio_state = { - 'input_level': 0.0, - 'output_level': 0.0, - 'is_recording': False, - 'is_playing': False, - 'last_input_time': 0.0, - 'last_output_time': 0.0 - } - - # 设备统计 - self.device_stats = { - 'devices_found': 0, - 'input_devices': 0, - 'output_devices': 0, - 'devices_connected': 0, - 'devices_disconnected': 0, - 'data_transferred': 0 - } - - # 线程管理 - self.monitor_thread = None - self.running = False - - # 回调函数 - self.audio_callbacks = { - 'device_connected': [], - 'device_disconnected': [], - 'input_level_changed': [], - 'output_level_changed': [], - 'recording_started': [], - 'recording_stopped': [], - 'playback_started': [], - 'playback_stopped': [] - } - - # 时间戳记录 - self.last_device_scan = 0.0 - self.last_level_update = 0.0 - - print("✓ 音频管理器已创建") - - def initialize(self) -> bool: - """ - 初始化音频管理器 - - Returns: - 是否初始化成功 - """ - try: - # 扫描音频设备 - self._scan_audio_devices() - - # 初始化默认设备 - self._initialize_default_devices() - - self.initialized = True - print("✓ 音频管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 音频管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用音频管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 音频管理器未初始化") - return False - - self.enabled = True - self.running = True - - # 启动监控线程 - self.monitor_thread = threading.Thread(target=self._monitor_loop, daemon=True) - self.monitor_thread.start() - - print("✓ 音频管理器已启用") - return True - - except Exception as e: - print(f"✗ 音频管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用音频管理器""" - try: - self.enabled = False - self.running = False - - # 等待监控线程结束 - if self.monitor_thread and self.monitor_thread.is_alive(): - self.monitor_thread.join(timeout=2.0) - - # 关闭所有音频流 - self._close_all_streams() - - print("✓ 音频管理器已禁用") - - except Exception as e: - print(f"✗ 音频管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理音频管理器资源""" - try: - self.disable() - self.audio_callbacks.clear() - self.audio_devices.clear() - self.initialized = False - print("✓ 音频管理器资源已清理") - - except Exception as e: - print(f"✗ 音频管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新音频管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - current_time = time.time() - - # 定期扫描设备 - if current_time - self.last_device_scan > 5.0: # 每5秒扫描一次 - self._scan_audio_devices() - self.last_device_scan = current_time - - self.last_level_update = current_time - - except Exception as e: - print(f"✗ 音频管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _scan_audio_devices(self): - """扫描音频设备""" - try: - # 模拟设备扫描 - # 在实际实现中,这里会调用平台特定的API来发现音频设备 - - # 模拟输入设备 - input_devices = { - 0: { - 'id': 0, - 'name': '系统默认麦克风', - 'type': 'input', - 'channels': 1, - 'sample_rates': [16000, 44100, 48000], - 'is_default': True, - 'connected': True - }, - 1: { - 'id': 1, - 'name': 'USB 麦克风', - 'type': 'input', - 'channels': 2, - 'sample_rates': [16000, 44100, 48000], - 'is_default': False, - 'connected': True - }, - 2: { - 'id': 2, - 'name': '蓝牙耳机麦克风', - 'type': 'input', - 'channels': 1, - 'sample_rates': [16000, 44100], - 'is_default': False, - 'connected': True - } - } - - # 模拟输出设备 - output_devices = { - 0: { - 'id': 0, - 'name': '系统默认扬声器', - 'type': 'output', - 'channels': 2, - 'sample_rates': [16000, 44100, 48000], - 'is_default': True, - 'connected': True - }, - 1: { - 'id': 1, - 'name': 'USB 音响', - 'type': 'output', - 'channels': 2, - 'sample_rates': [16000, 44100, 48000, 96000], - 'is_default': False, - 'connected': True - }, - 2: { - 'id': 2, - 'name': '蓝牙耳机', - 'type': 'output', - 'channels': 2, - 'sample_rates': [16000, 44100], - 'is_default': False, - 'connected': True - } - } - - # 更新设备列表 - self.audio_devices['input'] = input_devices - self.audio_devices['output'] = output_devices - - # 更新统计信息 - self.device_stats['devices_found'] = len(input_devices) + len(output_devices) - self.device_stats['input_devices'] = len(input_devices) - self.device_stats['output_devices'] = len(output_devices) - - print(f"✓ 音频设备扫描完成: {len(input_devices)}个输入设备, {len(output_devices)}个输出设备") - - except Exception as e: - print(f"✗ 音频设备扫描失败: {e}") - - def _initialize_default_devices(self): - """初始化默认设备""" - try: - # 查找默认输入设备 - for device_id, device_info in self.audio_devices['input'].items(): - if device_info.get('is_default', False): - self.current_devices['input'] = device_id - break - - # 查找默认输出设备 - for device_id, device_info in self.audio_devices['output'].items(): - if device_info.get('is_default', False): - self.current_devices['output'] = device_id - break - - print("✓ 默认音频设备初始化完成") - - except Exception as e: - print(f"✗ 默认音频设备初始化失败: {e}") - - def _monitor_loop(self): - """监控循环线程""" - try: - monitor_interval = 0.1 # 100ms - - while self.running: - if self.enabled: - self._update_audio_levels() - self._check_device_status() - - time.sleep(monitor_interval) - - except Exception as e: - print(f"✗ 音频监控循环失败: {e}") - - def _update_audio_levels(self): - """更新音频级别""" - try: - current_time = time.time() - - # 模拟输入级别更新 - if self.audio_state['is_recording']: - import random - new_input_level = random.uniform(0.0, 1.0) - if abs(new_input_level - self.audio_state['input_level']) > 0.01: - self.audio_state['input_level'] = new_input_level - self._trigger_audio_callback('input_level_changed', { - 'level': new_input_level, - 'timestamp': current_time - }) - - # 模拟输出级别更新 - if self.audio_state['is_playing']: - import random - new_output_level = random.uniform(0.0, 1.0) - if abs(new_output_level - self.audio_state['output_level']) > 0.01: - self.audio_state['output_level'] = new_output_level - self._trigger_audio_callback('output_level_changed', { - 'level': new_output_level, - 'timestamp': current_time - }) - - self.last_level_update = current_time - - except Exception as e: - print(f"✗ 音频级别更新失败: {e}") - - def _check_device_status(self): - """检查设备状态""" - try: - # 在实际实现中,这里会检查设备连接状态 - # 目前只是模拟 - pass - except Exception as e: - print(f"✗ 设备状态检查失败: {e}") - - def _close_all_streams(self): - """关闭所有音频流""" - try: - self.audio_streams['input'] = None - self.audio_streams['output'] = None - print("✓ 所有音频流已关闭") - except Exception as e: - print(f"✗ 音频流关闭失败: {e}") - - def get_input_devices(self) -> Dict[int, Dict[str, Any]]: - """ - 获取输入设备列表 - - Returns: - 输入设备字典 - """ - return self.audio_devices['input'].copy() - - def get_output_devices(self) -> Dict[int, Dict[str, Any]]: - """ - 获取输出设备列表 - - Returns: - 输出设备字典 - """ - return self.audio_devices['output'].copy() - - def get_all_devices(self) -> Dict[str, Dict[int, Dict[str, Any]]]: - """ - 获取所有设备列表 - - Returns: - 所有设备字典 - """ - return { - 'input': self.audio_devices['input'].copy(), - 'output': self.audio_devices['output'].copy() - } - - def set_input_device(self, device_id: int) -> bool: - """ - 设置输入设备 - - Args: - device_id: 设备ID - - Returns: - 是否设置成功 - """ - try: - if device_id in self.audio_devices['input']: - old_device = self.current_devices['input'] - self.current_devices['input'] = device_id - - print(f"✓ 输入设备已设置: {self.audio_devices['input'][device_id]['name']}") - - # 触发设备变更回调 - self._trigger_audio_callback('device_connected', { - 'device_type': 'input', - 'device_id': device_id, - 'device_name': self.audio_devices['input'][device_id]['name'], - 'old_device_id': old_device - }) - - return True - else: - print(f"✗ 输入设备不存在: ID {device_id}") - return False - - except Exception as e: - print(f"✗ 输入设备设置失败: {e}") - return False - - def set_output_device(self, device_id: int) -> bool: - """ - 设置输出设备 - - Args: - device_id: 设备ID - - Returns: - 是否设置成功 - """ - try: - if device_id in self.audio_devices['output']: - old_device = self.current_devices['output'] - self.current_devices['output'] = device_id - - print(f"✓ 输出设备已设置: {self.audio_devices['output'][device_id]['name']}") - - # 触发设备变更回调 - self._trigger_audio_callback('device_connected', { - 'device_type': 'output', - 'device_id': device_id, - 'device_name': self.audio_devices['output'][device_id]['name'], - 'old_device_id': old_device - }) - - return True - else: - print(f"✗ 输出设备不存在: ID {device_id}") - return False - - except Exception as e: - print(f"✗ 输出设备设置失败: {e}") - return False - - def get_current_input_device(self) -> Optional[Dict[str, Any]]: - """ - 获取当前输入设备 - - Returns: - 当前输入设备信息或None - """ - try: - device_id = self.current_devices['input'] - if device_id is not None and device_id in self.audio_devices['input']: - return self.audio_devices['input'][device_id] - return None - except Exception as e: - print(f"✗ 当前输入设备获取失败: {e}") - return None - - def get_current_output_device(self) -> Optional[Dict[str, Any]]: - """ - 获取当前输出设备 - - Returns: - 当前输出设备信息或None - """ - try: - device_id = self.current_devices['output'] - if device_id is not None and device_id in self.audio_devices['output']: - return self.audio_devices['output'][device_id] - return None - except Exception as e: - print(f"✗ 当前输出设备获取失败: {e}") - return None - - def start_recording(self) -> bool: - """ - 开始录音 - - Returns: - 是否开始成功 - """ - try: - if not self.enabled: - print("✗ 音频管理器未启用") - return False - - # 模拟开始录音 - self.audio_state['is_recording'] = True - self.audio_state['last_input_time'] = time.time() - - # 触发录音开始回调 - self._trigger_audio_callback('recording_started', { - 'device_id': self.current_devices['input'], - 'timestamp': time.time() - }) - - print("✓ 录音已开始") - return True - - except Exception as e: - print(f"✗ 录音开始失败: {e}") - return False - - def stop_recording(self) -> bool: - """ - 停止录音 - - Returns: - 是否停止成功 - """ - try: - self.audio_state['is_recording'] = False - - # 触发录音停止回调 - self._trigger_audio_callback('recording_stopped', { - 'device_id': self.current_devices['input'], - 'timestamp': time.time() - }) - - print("✓ 录音已停止") - return True - - except Exception as e: - print(f"✗ 录音停止失败: {e}") - return False - - def start_playback(self) -> bool: - """ - 开始播放 - - Returns: - 是否开始成功 - """ - try: - if not self.enabled: - print("✗ 音频管理器未启用") - return False - - # 模拟开始播放 - self.audio_state['is_playing'] = True - self.audio_state['last_output_time'] = time.time() - - # 触发播放开始回调 - self._trigger_audio_callback('playback_started', { - 'device_id': self.current_devices['output'], - 'timestamp': time.time() - }) - - print("✓ 播放已开始") - return True - - except Exception as e: - print(f"✗ 播放开始失败: {e}") - return False - - def stop_playback(self) -> bool: - """ - 停止播放 - - Returns: - 是否停止成功 - """ - try: - self.audio_state['is_playing'] = False - - # 触发播放停止回调 - self._trigger_audio_callback('playback_stopped', { - 'device_id': self.current_devices['output'], - 'timestamp': time.time() - }) - - print("✓ 播放已停止") - return True - - except Exception as e: - print(f"✗ 播放停止失败: {e}") - return False - - def get_audio_level(self, stream_type: str) -> float: - """ - 获取音频级别 - - Args: - stream_type: 流类型('input'或'output') - - Returns: - 音频级别(0.0-1.0) - """ - try: - if stream_type == 'input': - return self.audio_state['input_level'] - elif stream_type == 'output': - return self.audio_state['output_level'] - else: - print(f"✗ 无效的流类型: {stream_type}") - return 0.0 - except Exception as e: - print(f"✗ 音频级别获取失败: {e}") - return 0.0 - - def set_audio_config(self, config: Dict[str, Any]) -> bool: - """ - 设置音频配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.audio_config.update(config) - print(f"✓ 音频配置已更新: {self.audio_config}") - return True - except Exception as e: - print(f"✗ 音频配置设置失败: {e}") - return False - - def get_audio_config(self) -> Dict[str, Any]: - """ - 获取音频配置 - - Returns: - 配置字典 - """ - return self.audio_config.copy() - - def is_recording(self) -> bool: - """ - 检查是否正在录音 - - Returns: - 是否正在录音 - """ - return self.audio_state['is_recording'] - - def is_playing(self) -> bool: - """ - 检查是否正在播放 - - Returns: - 是否正在播放 - """ - return self.audio_state['is_playing'] - - def _trigger_audio_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发音频回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.audio_callbacks: - for callback in self.audio_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 音频回调执行失败: {e}") - except Exception as e: - print(f"✗ 音频回调触发失败: {e}") - - def register_audio_callback(self, callback_type: str, callback: callable): - """ - 注册音频回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.audio_callbacks: - self.audio_callbacks[callback_type].append(callback) - print(f"✓ 音频回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 音频回调注册失败: {e}") - - def unregister_audio_callback(self, callback_type: str, callback: callable): - """ - 注销音频回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.audio_callbacks: - if callback in self.audio_callbacks[callback_type]: - self.audio_callbacks[callback_type].remove(callback) - print(f"✓ 音频回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 音频回调注销失败: {e}") - - def get_device_stats(self) -> Dict[str, int]: - """ - 获取设备统计信息 - - Returns: - 统计信息字典 - """ - return self.device_stats.copy() - - def reset_device_stats(self): - """重置设备统计信息""" - try: - self.device_stats = { - 'devices_found': 0, - 'input_devices': 0, - 'output_devices': 0, - 'devices_connected': 0, - 'devices_disconnected': 0, - 'data_transferred': 0 - } - print("✓ 音频管理器统计信息已重置") - except Exception as e: +""" +音频管理器模块 +负责管理音频输入输出设备 +""" + +import time +from typing import Dict, Any, List, Optional +import threading + +class AudioManager: + """ + 音频管理器 + 负责管理音频输入输出设备 + """ + + def __init__(self, plugin): + """ + 初始化音频管理器 + + Args: + plugin: 语音控制插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 音频设备列表 + self.audio_devices = { + 'input': {}, + 'output': {} + } + + # 当前设备配置 + self.current_devices = { + 'input': None, + 'output': None + } + + # 音频流管理 + self.audio_streams = { + 'input': None, + 'output': None + } + + # 音频配置 + self.audio_config = { + 'sample_rate': 16000, + 'channels': 1, + 'format': 'int16', + 'input_buffer_size': 1024, + 'output_buffer_size': 1024, + 'latency': 'low', + 'enable_echo_cancellation': True, + 'enable_noise_reduction': True, + 'enable_auto_gain': True + } + + # 音频状态 + self.audio_state = { + 'input_level': 0.0, + 'output_level': 0.0, + 'is_recording': False, + 'is_playing': False, + 'last_input_time': 0.0, + 'last_output_time': 0.0 + } + + # 设备统计 + self.device_stats = { + 'devices_found': 0, + 'input_devices': 0, + 'output_devices': 0, + 'devices_connected': 0, + 'devices_disconnected': 0, + 'data_transferred': 0 + } + + # 线程管理 + self.monitor_thread = None + self.running = False + + # 回调函数 + self.audio_callbacks = { + 'device_connected': [], + 'device_disconnected': [], + 'input_level_changed': [], + 'output_level_changed': [], + 'recording_started': [], + 'recording_stopped': [], + 'playback_started': [], + 'playback_stopped': [] + } + + # 时间戳记录 + self.last_device_scan = 0.0 + self.last_level_update = 0.0 + + print("✓ 音频管理器已创建") + + def initialize(self) -> bool: + """ + 初始化音频管理器 + + Returns: + 是否初始化成功 + """ + try: + # 扫描音频设备 + self._scan_audio_devices() + + # 初始化默认设备 + self._initialize_default_devices() + + self.initialized = True + print("✓ 音频管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 音频管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用音频管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 音频管理器未初始化") + return False + + self.enabled = True + self.running = True + + # 启动监控线程 + self.monitor_thread = threading.Thread(target=self._monitor_loop, daemon=True) + self.monitor_thread.start() + + print("✓ 音频管理器已启用") + return True + + except Exception as e: + print(f"✗ 音频管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用音频管理器""" + try: + self.enabled = False + self.running = False + + # 等待监控线程结束 + if self.monitor_thread and self.monitor_thread.is_alive(): + self.monitor_thread.join(timeout=2.0) + + # 关闭所有音频流 + self._close_all_streams() + + print("✓ 音频管理器已禁用") + + except Exception as e: + print(f"✗ 音频管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理音频管理器资源""" + try: + self.disable() + self.audio_callbacks.clear() + self.audio_devices.clear() + self.initialized = False + print("✓ 音频管理器资源已清理") + + except Exception as e: + print(f"✗ 音频管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新音频管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + current_time = time.time() + + # 定期扫描设备 + if current_time - self.last_device_scan > 5.0: # 每5秒扫描一次 + self._scan_audio_devices() + self.last_device_scan = current_time + + self.last_level_update = current_time + + except Exception as e: + print(f"✗ 音频管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _scan_audio_devices(self): + """扫描音频设备""" + try: + # 模拟设备扫描 + # 在实际实现中,这里会调用平台特定的API来发现音频设备 + + # 模拟输入设备 + input_devices = { + 0: { + 'id': 0, + 'name': '系统默认麦克风', + 'type': 'input', + 'channels': 1, + 'sample_rates': [16000, 44100, 48000], + 'is_default': True, + 'connected': True + }, + 1: { + 'id': 1, + 'name': 'USB 麦克风', + 'type': 'input', + 'channels': 2, + 'sample_rates': [16000, 44100, 48000], + 'is_default': False, + 'connected': True + }, + 2: { + 'id': 2, + 'name': '蓝牙耳机麦克风', + 'type': 'input', + 'channels': 1, + 'sample_rates': [16000, 44100], + 'is_default': False, + 'connected': True + } + } + + # 模拟输出设备 + output_devices = { + 0: { + 'id': 0, + 'name': '系统默认扬声器', + 'type': 'output', + 'channels': 2, + 'sample_rates': [16000, 44100, 48000], + 'is_default': True, + 'connected': True + }, + 1: { + 'id': 1, + 'name': 'USB 音响', + 'type': 'output', + 'channels': 2, + 'sample_rates': [16000, 44100, 48000, 96000], + 'is_default': False, + 'connected': True + }, + 2: { + 'id': 2, + 'name': '蓝牙耳机', + 'type': 'output', + 'channels': 2, + 'sample_rates': [16000, 44100], + 'is_default': False, + 'connected': True + } + } + + # 更新设备列表 + self.audio_devices['input'] = input_devices + self.audio_devices['output'] = output_devices + + # 更新统计信息 + self.device_stats['devices_found'] = len(input_devices) + len(output_devices) + self.device_stats['input_devices'] = len(input_devices) + self.device_stats['output_devices'] = len(output_devices) + + print(f"✓ 音频设备扫描完成: {len(input_devices)}个输入设备, {len(output_devices)}个输出设备") + + except Exception as e: + print(f"✗ 音频设备扫描失败: {e}") + + def _initialize_default_devices(self): + """初始化默认设备""" + try: + # 查找默认输入设备 + for device_id, device_info in self.audio_devices['input'].items(): + if device_info.get('is_default', False): + self.current_devices['input'] = device_id + break + + # 查找默认输出设备 + for device_id, device_info in self.audio_devices['output'].items(): + if device_info.get('is_default', False): + self.current_devices['output'] = device_id + break + + print("✓ 默认音频设备初始化完成") + + except Exception as e: + print(f"✗ 默认音频设备初始化失败: {e}") + + def _monitor_loop(self): + """监控循环线程""" + try: + monitor_interval = 0.1 # 100ms + + while self.running: + if self.enabled: + self._update_audio_levels() + self._check_device_status() + + time.sleep(monitor_interval) + + except Exception as e: + print(f"✗ 音频监控循环失败: {e}") + + def _update_audio_levels(self): + """更新音频级别""" + try: + current_time = time.time() + + # 模拟输入级别更新 + if self.audio_state['is_recording']: + import random + new_input_level = random.uniform(0.0, 1.0) + if abs(new_input_level - self.audio_state['input_level']) > 0.01: + self.audio_state['input_level'] = new_input_level + self._trigger_audio_callback('input_level_changed', { + 'level': new_input_level, + 'timestamp': current_time + }) + + # 模拟输出级别更新 + if self.audio_state['is_playing']: + import random + new_output_level = random.uniform(0.0, 1.0) + if abs(new_output_level - self.audio_state['output_level']) > 0.01: + self.audio_state['output_level'] = new_output_level + self._trigger_audio_callback('output_level_changed', { + 'level': new_output_level, + 'timestamp': current_time + }) + + self.last_level_update = current_time + + except Exception as e: + print(f"✗ 音频级别更新失败: {e}") + + def _check_device_status(self): + """检查设备状态""" + try: + # 在实际实现中,这里会检查设备连接状态 + # 目前只是模拟 + pass + except Exception as e: + print(f"✗ 设备状态检查失败: {e}") + + def _close_all_streams(self): + """关闭所有音频流""" + try: + self.audio_streams['input'] = None + self.audio_streams['output'] = None + print("✓ 所有音频流已关闭") + except Exception as e: + print(f"✗ 音频流关闭失败: {e}") + + def get_input_devices(self) -> Dict[int, Dict[str, Any]]: + """ + 获取输入设备列表 + + Returns: + 输入设备字典 + """ + return self.audio_devices['input'].copy() + + def get_output_devices(self) -> Dict[int, Dict[str, Any]]: + """ + 获取输出设备列表 + + Returns: + 输出设备字典 + """ + return self.audio_devices['output'].copy() + + def get_all_devices(self) -> Dict[str, Dict[int, Dict[str, Any]]]: + """ + 获取所有设备列表 + + Returns: + 所有设备字典 + """ + return { + 'input': self.audio_devices['input'].copy(), + 'output': self.audio_devices['output'].copy() + } + + def set_input_device(self, device_id: int) -> bool: + """ + 设置输入设备 + + Args: + device_id: 设备ID + + Returns: + 是否设置成功 + """ + try: + if device_id in self.audio_devices['input']: + old_device = self.current_devices['input'] + self.current_devices['input'] = device_id + + print(f"✓ 输入设备已设置: {self.audio_devices['input'][device_id]['name']}") + + # 触发设备变更回调 + self._trigger_audio_callback('device_connected', { + 'device_type': 'input', + 'device_id': device_id, + 'device_name': self.audio_devices['input'][device_id]['name'], + 'old_device_id': old_device + }) + + return True + else: + print(f"✗ 输入设备不存在: ID {device_id}") + return False + + except Exception as e: + print(f"✗ 输入设备设置失败: {e}") + return False + + def set_output_device(self, device_id: int) -> bool: + """ + 设置输出设备 + + Args: + device_id: 设备ID + + Returns: + 是否设置成功 + """ + try: + if device_id in self.audio_devices['output']: + old_device = self.current_devices['output'] + self.current_devices['output'] = device_id + + print(f"✓ 输出设备已设置: {self.audio_devices['output'][device_id]['name']}") + + # 触发设备变更回调 + self._trigger_audio_callback('device_connected', { + 'device_type': 'output', + 'device_id': device_id, + 'device_name': self.audio_devices['output'][device_id]['name'], + 'old_device_id': old_device + }) + + return True + else: + print(f"✗ 输出设备不存在: ID {device_id}") + return False + + except Exception as e: + print(f"✗ 输出设备设置失败: {e}") + return False + + def get_current_input_device(self) -> Optional[Dict[str, Any]]: + """ + 获取当前输入设备 + + Returns: + 当前输入设备信息或None + """ + try: + device_id = self.current_devices['input'] + if device_id is not None and device_id in self.audio_devices['input']: + return self.audio_devices['input'][device_id] + return None + except Exception as e: + print(f"✗ 当前输入设备获取失败: {e}") + return None + + def get_current_output_device(self) -> Optional[Dict[str, Any]]: + """ + 获取当前输出设备 + + Returns: + 当前输出设备信息或None + """ + try: + device_id = self.current_devices['output'] + if device_id is not None and device_id in self.audio_devices['output']: + return self.audio_devices['output'][device_id] + return None + except Exception as e: + print(f"✗ 当前输出设备获取失败: {e}") + return None + + def start_recording(self) -> bool: + """ + 开始录音 + + Returns: + 是否开始成功 + """ + try: + if not self.enabled: + print("✗ 音频管理器未启用") + return False + + # 模拟开始录音 + self.audio_state['is_recording'] = True + self.audio_state['last_input_time'] = time.time() + + # 触发录音开始回调 + self._trigger_audio_callback('recording_started', { + 'device_id': self.current_devices['input'], + 'timestamp': time.time() + }) + + print("✓ 录音已开始") + return True + + except Exception as e: + print(f"✗ 录音开始失败: {e}") + return False + + def stop_recording(self) -> bool: + """ + 停止录音 + + Returns: + 是否停止成功 + """ + try: + self.audio_state['is_recording'] = False + + # 触发录音停止回调 + self._trigger_audio_callback('recording_stopped', { + 'device_id': self.current_devices['input'], + 'timestamp': time.time() + }) + + print("✓ 录音已停止") + return True + + except Exception as e: + print(f"✗ 录音停止失败: {e}") + return False + + def start_playback(self) -> bool: + """ + 开始播放 + + Returns: + 是否开始成功 + """ + try: + if not self.enabled: + print("✗ 音频管理器未启用") + return False + + # 模拟开始播放 + self.audio_state['is_playing'] = True + self.audio_state['last_output_time'] = time.time() + + # 触发播放开始回调 + self._trigger_audio_callback('playback_started', { + 'device_id': self.current_devices['output'], + 'timestamp': time.time() + }) + + print("✓ 播放已开始") + return True + + except Exception as e: + print(f"✗ 播放开始失败: {e}") + return False + + def stop_playback(self) -> bool: + """ + 停止播放 + + Returns: + 是否停止成功 + """ + try: + self.audio_state['is_playing'] = False + + # 触发播放停止回调 + self._trigger_audio_callback('playback_stopped', { + 'device_id': self.current_devices['output'], + 'timestamp': time.time() + }) + + print("✓ 播放已停止") + return True + + except Exception as e: + print(f"✗ 播放停止失败: {e}") + return False + + def get_audio_level(self, stream_type: str) -> float: + """ + 获取音频级别 + + Args: + stream_type: 流类型('input'或'output') + + Returns: + 音频级别(0.0-1.0) + """ + try: + if stream_type == 'input': + return self.audio_state['input_level'] + elif stream_type == 'output': + return self.audio_state['output_level'] + else: + print(f"✗ 无效的流类型: {stream_type}") + return 0.0 + except Exception as e: + print(f"✗ 音频级别获取失败: {e}") + return 0.0 + + def set_audio_config(self, config: Dict[str, Any]) -> bool: + """ + 设置音频配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.audio_config.update(config) + print(f"✓ 音频配置已更新: {self.audio_config}") + return True + except Exception as e: + print(f"✗ 音频配置设置失败: {e}") + return False + + def get_audio_config(self) -> Dict[str, Any]: + """ + 获取音频配置 + + Returns: + 配置字典 + """ + return self.audio_config.copy() + + def is_recording(self) -> bool: + """ + 检查是否正在录音 + + Returns: + 是否正在录音 + """ + return self.audio_state['is_recording'] + + def is_playing(self) -> bool: + """ + 检查是否正在播放 + + Returns: + 是否正在播放 + """ + return self.audio_state['is_playing'] + + def _trigger_audio_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发音频回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.audio_callbacks: + for callback in self.audio_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 音频回调执行失败: {e}") + except Exception as e: + print(f"✗ 音频回调触发失败: {e}") + + def register_audio_callback(self, callback_type: str, callback: callable): + """ + 注册音频回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.audio_callbacks: + self.audio_callbacks[callback_type].append(callback) + print(f"✓ 音频回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 音频回调注册失败: {e}") + + def unregister_audio_callback(self, callback_type: str, callback: callable): + """ + 注销音频回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.audio_callbacks: + if callback in self.audio_callbacks[callback_type]: + self.audio_callbacks[callback_type].remove(callback) + print(f"✓ 音频回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 音频回调注销失败: {e}") + + def get_device_stats(self) -> Dict[str, int]: + """ + 获取设备统计信息 + + Returns: + 统计信息字典 + """ + return self.device_stats.copy() + + def reset_device_stats(self): + """重置设备统计信息""" + try: + self.device_stats = { + 'devices_found': 0, + 'input_devices': 0, + 'output_devices': 0, + 'devices_connected': 0, + 'devices_disconnected': 0, + 'data_transferred': 0 + } + print("✓ 音频管理器统计信息已重置") + except Exception as e: print(f"✗ 音频管理器统计信息重置失败: {e}") \ No newline at end of file diff --git a/plugins/user/voice_control/commands/command_processor.py b/plugins/user/voice_control/commands/command_processor.py index e0e3806a..2c917e08 100644 --- a/plugins/user/voice_control/commands/command_processor.py +++ b/plugins/user/voice_control/commands/command_processor.py @@ -1,1202 +1,1202 @@ -""" -命令处理器模块 -负责处理语音命令并执行相应操作 -""" - -import time -import re -from typing import Dict, Any, List, Optional, Callable -import threading - -class CommandProcessor: - """ - 命令处理器 - 负责处理语音命令并执行相应操作 - """ - - def __init__(self, plugin): - """ - 初始化命令处理器 - - Args: - plugin: 语音控制插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 命令映射 - self.command_mappings = {} - - # 命令组 - self.command_groups = {} - - # 命令历史 - self.command_history = [] - self.max_history_size = 1000 - - # 命令配置 - self.command_config = { - 'enable_fuzzy_matching': True, - 'fuzzy_threshold': 0.7, - 'enable_context_commands': True, - 'context_timeout': 30.0, - 'enable_chained_commands': True, - 'max_chain_length': 5, - 'enable_confirmation': True, - 'confirmation_threshold': 0.9 - } - - # 上下文管理 - self.command_context = { - 'current_context': 'default', - 'context_stack': [], - 'context_start_time': 0.0, - 'context_variables': {} - } - - # 命令链 - self.command_chain = { - 'is_active': False, - 'commands': [], - 'chain_start_time': 0.0 - } - - # 统计信息 - self.stats = { - 'commands_processed': 0, - 'commands_executed': 0, - 'commands_failed': 0, - 'fuzzy_matches': 0, - 'exact_matches': 0, - 'context_commands': 0, - 'chained_commands': 0 - } - - # 回调函数 - self.command_callbacks = { - 'command_received': [], - 'command_processed': [], - 'command_executed': [], - 'command_failed': [], - 'context_changed': [], - 'chain_started': [], - 'chain_completed': [] - } - - # 时间戳记录 - self.last_command_time = 0.0 - self.last_context_change = 0.0 - - print("✓ 命令处理器已创建") - - def initialize(self) -> bool: - """ - 初始化命令处理器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化默认命令映射 - self._initialize_default_commands() - - # 初始化命令组 - self._initialize_command_groups() - - self.initialized = True - print("✓ 命令处理器初始化完成") - return True - - except Exception as e: - print(f"✗ 命令处理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用命令处理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 命令处理器未初始化") - return False - - self.enabled = True - print("✓ 命令处理器已启用") - return True - - except Exception as e: - print(f"✗ 命令处理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用命令处理器""" - try: - self.enabled = False - print("✓ 命令处理器已禁用") - - except Exception as e: - print(f"✗ 命令处理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理命令处理器资源""" - try: - self.disable() - self.command_mappings.clear() - self.command_groups.clear() - self.command_history.clear() - self.command_callbacks.clear() - self.initialized = False - print("✓ 命令处理器资源已清理") - - except Exception as e: - print(f"✗ 命令处理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新命令处理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.last_command_time = current_time - - # 检查上下文超时 - self._check_context_timeout(current_time) - - # 检查命令链超时 - self._check_chain_timeout(current_time) - - except Exception as e: - print(f"✗ 命令处理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _initialize_default_commands(self): - """初始化默认命令映射""" - try: - # 基础移动命令 - self.register_command('前进', self._move_forward, 'movement', '向前移动') - self.register_command('后退', self._move_backward, 'movement', '向后移动') - self.register_command('左转', self._turn_left, 'movement', '向左转') - self.register_command('右转', self._turn_right, 'movement', '向右转') - self.register_command('停止', self._stop_movement, 'movement', '停止移动') - - # 系统控制命令 - self.register_command('打开灯光', self._toggle_lights, 'system', '切换灯光状态') - self.register_command('关闭灯光', self._toggle_lights, 'system', '切换灯光状态') - self.register_command('增加音量', self._increase_volume, 'system', '增加系统音量') - self.register_command('减少音量', self._decrease_volume, 'system', '减少系统音量') - self.register_command('播放音乐', self._play_music, 'entertainment', '播放音乐') - self.register_command('暂停播放', self._pause_music, 'entertainment', '暂停音乐播放') - self.register_command('下一首', self._next_track, 'entertainment', '播放下一首') - self.register_command('上一首', self._previous_track, 'entertainment', '播放上一首') - - # 交互命令 - self.register_command('你好', self._greet_user, 'interaction', '打招呼') - self.register_command('谢谢', self._acknowledge_thanks, 'interaction', '回应感谢') - self.register_command('再见', self._say_goodbye, 'interaction', '道别') - self.register_command('帮助', self._provide_help, 'interaction', '提供帮助信息') - self.register_command('关于', self._show_about, 'interaction', '显示系统信息') - - print("✓ 默认命令映射初始化完成") - - except Exception as e: - print(f"✗ 默认命令映射初始化失败: {e}") - - def _initialize_command_groups(self): - """初始化命令组""" - try: - self.command_groups = { - 'movement': { - 'name': '移动控制', - 'description': '控制物体移动的命令', - 'commands': ['前进', '后退', '左转', '右转', '停止'] - }, - 'system': { - 'name': '系统控制', - 'description': '控制系统功能的命令', - 'commands': ['打开灯光', '关闭灯光', '增加音量', '减少音量'] - }, - 'entertainment': { - 'name': '娱乐控制', - 'description': '控制娱乐功能的命令', - 'commands': ['播放音乐', '暂停播放', '下一首', '上一首'] - }, - 'interaction': { - 'name': '交互命令', - 'description': '日常交互的命令', - 'commands': ['你好', '谢谢', '再见', '帮助', '关于'] - } - } - - print("✓ 命令组初始化完成") - - except Exception as e: - print(f"✗ 命令组初始化失败: {e}") - - def register_command(self, command_text: str, handler: Callable, - group: str = 'default', description: str = '') -> bool: - """ - 注册命令 - - Args: - command_text: 命令文本 - handler: 命令处理函数 - group: 命令组 - description: 命令描述 - - Returns: - 是否注册成功 - """ - try: - self.command_mappings[command_text] = { - 'handler': handler, - 'group': group, - 'description': description, - 'registered_time': time.time() - } - - # 添加到命令组 - if group in self.command_groups: - if command_text not in self.command_groups[group]['commands']: - self.command_groups[group]['commands'].append(command_text) - else: - self.command_groups[group] = { - 'name': group, - 'description': f'命令组: {group}', - 'commands': [command_text] - } - - print(f"✓ 命令已注册: {command_text}") - return True - - except Exception as e: - print(f"✗ 命令注册失败: {e}") - return False - - def unregister_command(self, command_text: str) -> bool: - """ - 注销命令 - - Args: - command_text: 命令文本 - - Returns: - 是否注销成功 - """ - try: - if command_text in self.command_mappings: - command_info = self.command_mappings[command_text] - group = command_info['group'] - - # 从命令组中移除 - if group in self.command_groups: - if command_text in self.command_groups[group]['commands']: - self.command_groups[group]['commands'].remove(command_text) - - del self.command_mappings[command_text] - print(f"✓ 命令已注销: {command_text}") - return True - else: - print(f"✗ 命令不存在: {command_text}") - return False - - except Exception as e: - print(f"✗ 命令注销失败: {e}") - return False - - def process_command(self, text: str, confidence: float = 1.0) -> bool: - """ - 处理语音命令 - - Args: - text: 命令文本 - confidence: 置信度(0.0-1.0) - - Returns: - 是否处理成功 - """ - try: - if not self.enabled: - print("✗ 命令处理器未启用") - return False - - # 记录命令历史 - command_entry = { - 'text': text, - 'confidence': confidence, - 'timestamp': time.time(), - 'context': self.command_context['current_context'] - } - - self.command_history.append(command_entry) - - # 保持历史记录大小 - if len(self.command_history) > self.max_history_size: - self.command_history.pop(0) - - # 触发命令接收回调 - self._trigger_command_callback('command_received', { - 'text': text, - 'confidence': confidence, - 'command_entry': command_entry - }) - - # 检查是否在命令链中 - if self.command_chain['is_active']: - return self._process_chained_command(text, confidence) - - # 查找匹配的命令 - matched_command = self._find_matching_command(text, confidence) - - if matched_command: - command_text = matched_command['command'] - command_info = self.command_mappings[command_text] - handler = command_info['handler'] - - # 更新统计信息 - if matched_command['type'] == 'exact': - self.stats['exact_matches'] += 1 - else: - self.stats['fuzzy_matches'] += 1 - - # 检查是否需要确认 - if (self.command_config['enable_confirmation'] and - confidence < self.command_config['confirmation_threshold']): - return self._request_confirmation(text, command_text, handler, confidence) - - # 执行命令 - return self._execute_command(command_text, handler, text, confidence, matched_command) - else: - # 未找到匹配命令 - print(f"✗ 未找到匹配命令: {text}") - self.stats['commands_failed'] += 1 - - # 触发命令失败回调 - self._trigger_command_callback('command_failed', { - 'text': text, - 'confidence': confidence, - 'reason': 'no_match' - }) - - # 请求帮助 - if self._is_help_request(text): - self._provide_help() - - return False - - except Exception as e: - print(f"✗ 命令处理失败: {e}") - import traceback - traceback.print_exc() - self.stats['commands_failed'] += 1 - return False - - def _find_matching_command(self, text: str, confidence: float) -> Optional[Dict[str, Any]]: - """ - 查找匹配的命令 - - Args: - text: 命令文本 - confidence: 置信度 - - Returns: - 匹配的命令信息或None - """ - try: - # 首先尝试精确匹配 - if text in self.command_mappings: - return { - 'command': text, - 'type': 'exact', - 'confidence': confidence - } - - # 尝试上下文命令 - if (self.command_config['enable_context_commands'] and - self.command_context['current_context'] != 'default'): - context_commands = self.command_context['context_variables'].get('commands', []) - for command in context_commands: - if command in self.command_mappings and text in command: - return { - 'command': command, - 'type': 'context', - 'confidence': confidence * 0.9 - } - self.stats['context_commands'] += 1 - - # 尝试模糊匹配 - if self.command_config['enable_fuzzy_matching']: - best_match = self._fuzzy_match_command(text) - if best_match: - return best_match - - return None - - except Exception as e: - print(f"✗ 命令匹配失败: {e}") - return None - - def _fuzzy_match_command(self, text: str) -> Optional[Dict[str, Any]]: - """ - 模糊匹配命令 - - Args: - text: 命令文本 - - Returns: - 匹配的命令信息或None - """ - try: - best_match = None - best_score = 0.0 - - for command_text in self.command_mappings: - score = self._calculate_similarity(text, command_text) - if score > best_score and score >= self.command_config['fuzzy_threshold']: - best_score = score - best_match = { - 'command': command_text, - 'type': 'fuzzy', - 'confidence': score - } - - return best_match - - except Exception as e: - print(f"✗ 模糊匹配失败: {e}") - return None - - def _calculate_similarity(self, text1: str, text2: str) -> float: - """ - 计算文本相似度 - - Args: - text1: 文本1 - text2: 文本2 - - Returns: - 相似度(0.0-1.0) - """ - try: - # 简单的相似度计算(实际应用中可以使用更复杂的算法) - set1 = set(text1) - set2 = set(text2) - - intersection = len(set1.intersection(set2)) - union = len(set1.union(set2)) - - if union == 0: - return 0.0 - - return intersection / union - - except Exception as e: - print(f"✗ 相似度计算失败: {e}") - return 0.0 - - def _process_chained_command(self, text: str, confidence: float) -> bool: - """ - 处理命令链中的命令 - - Args: - text: 命令文本 - confidence: 置信度 - - Returns: - 是否处理成功 - """ - try: - # 添加到命令链 - self.command_chain['commands'].append({ - 'text': text, - 'confidence': confidence, - 'timestamp': time.time() - }) - - # 检查是否完成命令链 - if self._is_chain_completion(text): - return self._execute_command_chain() - - return True - - except Exception as e: - print(f"✗ 命令链处理失败: {e}") - return False - - def _is_chain_completion(self, text: str) -> bool: - """ - 检查是否完成命令链 - - Args: - text: 命令文本 - - Returns: - 是否完成命令链 - """ - try: - completion_keywords = ['完成', '结束', '执行', '确定', '确认'] - return any(keyword in text for keyword in completion_keywords) - except Exception as e: - print(f"✗ 命令链完成检查失败: {e}") - return False - - def _execute_command_chain(self) -> bool: - """ - 执行命令链 - - Returns: - 是否执行成功 - """ - try: - # 触发命令链完成回调 - self._trigger_command_callback('chain_completed', { - 'commands': self.command_chain['commands'] - }) - - # 重置命令链 - self.command_chain['is_active'] = False - self.command_chain['commands'] = [] - self.command_chain['chain_start_time'] = 0.0 - - self.stats['chained_commands'] += 1 - return True - - except Exception as e: - print(f"✗ 命令链执行失败: {e}") - return False - - def _request_confirmation(self, original_text: str, command_text: str, - handler: Callable, confidence: float) -> bool: - """ - 请求命令确认 - - Args: - original_text: 原始文本 - command_text: 命令文本 - handler: 处理函数 - confidence: 置信度 - - Returns: - 是否请求成功 - """ - try: - confirmation_text = f"您是想执行 {command_text} 吗?请确认。" - - # 通过语音合成器播放确认请求 - if self.plugin.speech_synthesizer: - self.plugin.speech_synthesizer.synthesize_speech(confirmation_text) - - # 存储待确认的命令 - self.pending_confirmation = { - 'original_text': original_text, - 'command_text': command_text, - 'handler': handler, - 'confidence': confidence, - 'timestamp': time.time() - } - - return True - - except Exception as e: - print(f"✗ 确认请求失败: {e}") - return False - - def _execute_command(self, command_text: str, handler: Callable, - original_text: str, confidence: float, - match_info: Dict[str, Any]) -> bool: - """ - 执行命令 - - Args: - command_text: 命令文本 - handler: 处理函数 - original_text: 原始文本 - confidence: 置信度 - match_info: 匹配信息 - - Returns: - 是否执行成功 - """ - try: - # 触发命令处理回调 - self._trigger_command_callback('command_processed', { - 'command_text': command_text, - 'original_text': original_text, - 'confidence': confidence, - 'match_type': match_info['type'] - }) - - # 执行命令处理函数 - result = handler(original_text, confidence) - - if result: - self.stats['commands_executed'] += 1 - - # 触发命令执行回调 - self._trigger_command_callback('command_executed', { - 'command_text': command_text, - 'original_text': original_text, - 'confidence': confidence - }) - - # 检查是否需要进入特定上下文 - self._check_context_transition(command_text) - - print(f"✓ 命令执行成功: {command_text}") - return True - else: - self.stats['commands_failed'] += 1 - - # 触发命令失败回调 - self._trigger_command_callback('command_failed', { - 'command_text': command_text, - 'original_text': original_text, - 'confidence': confidence, - 'reason': 'execution_failed' - }) - - print(f"✗ 命令执行失败: {command_text}") - return False - - except Exception as e: - print(f"✗ 命令执行异常: {e}") - import traceback - traceback.print_exc() - self.stats['commands_failed'] += 1 - - # 触发命令失败回调 - self._trigger_command_callback('command_failed', { - 'command_text': command_text, - 'original_text': original_text, - 'confidence': confidence, - 'reason': 'exception', - 'exception': str(e) - }) - - return False - - def _check_context_transition(self, command_text: str): - """ - 检查上下文转换 - - Args: - command_text: 命令文本 - """ - try: - # 根据命令类型设置上下文 - context_transitions = { - '播放音乐': 'music', - '暂停播放': 'music', - '下一首': 'music', - '上一首': 'music' - } - - if command_text in context_transitions: - self.set_context(context_transitions[command_text]) - - except Exception as e: - print(f"✗ 上下文转换检查失败: {e}") - - def _check_context_timeout(self, current_time: float): - """ - 检查上下文超时 - - Args: - current_time: 当前时间 - """ - try: - if (self.command_context['current_context'] != 'default' and - current_time - self.command_context['context_start_time'] > - self.command_config['context_timeout']): - self.reset_context() - - except Exception as e: - print(f"✗ 上下文超时检查失败: {e}") - - def _check_chain_timeout(self, current_time: float): - """ - 检查命令链超时 - - Args: - current_time: 当前时间 - """ - try: - if (self.command_chain['is_active'] and - current_time - self.command_chain['chain_start_time'] > 10.0): # 10秒超时 - self.command_chain['is_active'] = False - self.command_chain['commands'] = [] - self.command_chain['chain_start_time'] = 0.0 - - except Exception as e: - print(f"✗ 命令链超时检查失败: {e}") - - def set_context(self, context_name: str, variables: Dict[str, Any] = None) -> bool: - """ - 设置命令上下文 - - Args: - context_name: 上下文名称 - variables: 上下文变量 - - Returns: - 是否设置成功 - """ - try: - # 保存当前上下文到栈中 - self.command_context['context_stack'].append({ - 'context': self.command_context['current_context'], - 'variables': self.command_context['context_variables'].copy(), - 'timestamp': self.command_context['context_start_time'] - }) - - # 设置新上下文 - self.command_context['current_context'] = context_name - self.command_context['context_start_time'] = time.time() - if variables: - self.command_context['context_variables'] = variables - else: - self.command_context['context_variables'] = {} - - # 触发上下文变更回调 - self._trigger_command_callback('context_changed', { - 'old_context': self.command_context['context_stack'][-1]['context'] if self.command_context['context_stack'] else 'default', - 'new_context': context_name, - 'variables': variables - }) - - self.last_context_change = time.time() - print(f"✓ 上下文已设置为: {context_name}") - return True - - except Exception as e: - print(f"✗ 上下文设置失败: {e}") - return False - - def reset_context(self) -> bool: - """ - 重置命令上下文 - - Returns: - 是否重置成功 - """ - try: - if self.command_context['context_stack']: - # 恢复上一个上下文 - previous_context = self.command_context['context_stack'].pop() - self.command_context['current_context'] = previous_context['context'] - self.command_context['context_variables'] = previous_context['variables'] - self.command_context['context_start_time'] = previous_context['timestamp'] - else: - # 重置为默认上下文 - self.command_context['current_context'] = 'default' - self.command_context['context_variables'] = {} - self.command_context['context_start_time'] = time.time() - - # 触发上下文变更回调 - self._trigger_command_callback('context_changed', { - 'old_context': 'previous', - 'new_context': self.command_context['current_context'], - 'variables': self.command_context['context_variables'] - }) - - print("✓ 上下文已重置") - return True - - except Exception as e: - print(f"✗ 上下文重置失败: {e}") - return False - - def start_command_chain(self) -> bool: - """ - 开始命令链 - - Returns: - 是否开始成功 - """ - try: - self.command_chain['is_active'] = True - self.command_chain['commands'] = [] - self.command_chain['chain_start_time'] = time.time() - - # 触发命令链开始回调 - self._trigger_command_callback('chain_started', {}) - - print("✓ 命令链已开始") - return True - - except Exception as e: - print(f"✗ 命令链开始失败: {e}") - return False - - def _is_help_request(self, text: str) -> bool: - """ - 检查是否为帮助请求 - - Args: - text: 文本 - - Returns: - 是否为帮助请求 - """ - try: - help_keywords = ['帮助', 'help', '怎么用', '使用方法', '功能'] - return any(keyword in text for keyword in help_keywords) - except Exception as e: - print(f"✗ 帮助请求检查失败: {e}") - return False - - # 默认命令处理函数 - def _move_forward(self, text: str, confidence: float) -> bool: - """向前移动""" - try: - print("→ 执行向前移动命令") - if self.plugin.speech_synthesizer: - self.plugin.speech_synthesizer.synthesize_speech("正在向前移动") - return True - except Exception as e: - print(f"✗ 向前移动命令执行失败: {e}") - return False - - def _move_backward(self, text: str, confidence: float) -> bool: - """向后移动""" - try: - print("← 执行向后移动命令") - if self.plugin.speech_synthesizer: - self.plugin.speech_synthesizer.synthesize_speech("正在向后移动") - return True - except Exception as e: - print(f"✗ 向后移动命令执行失败: {e}") - return False - - def _turn_left(self, text: str, confidence: float) -> bool: - """向左转""" - try: - print("↺ 执行向左转命令") - if self.plugin.speech_synthesizer: - self.plugin.speech_synthesizer.synthesize_speech("正在向左转") - return True - except Exception as e: - print(f"✗ 向左转命令执行失败: {e}") - return False - - def _turn_right(self, text: str, confidence: float) -> bool: - """向右转""" - try: - print("↻ 执行向右转命令") - if self.plugin.speech_synthesizer: - self.plugin.speech_synthesizer.synthesize_speech("正在向右转") - return True - except Exception as e: - print(f"✗ 向右转命令执行失败: {e}") - return False - - def _stop_movement(self, text: str, confidence: float) -> bool: - """停止移动""" - try: - print("⏹ 执行停止移动命令") - if self.plugin.speech_synthesizer: - self.plugin.speech_synthesizer.synthesize_speech("已停止移动") - return True - except Exception as e: - print(f"✗ 停止移动命令执行失败: {e}") - return False - - def _toggle_lights(self, text: str, confidence: float) -> bool: - """切换灯光""" - try: - action = "打开" if "打开" in text else "关闭" - print(f"💡 执行{action}灯光命令") - if self.plugin.speech_synthesizer: - self.plugin.speech_synthesizer.synthesize_speech(f"已{action}灯光") - return True - except Exception as e: - print(f"✗ 灯光控制命令执行失败: {e}") - return False - - def _increase_volume(self, text: str, confidence: float) -> bool: - """增加音量""" - try: - print("🔊 执行增加音量命令") - if self.plugin.speech_synthesizer: - self.plugin.speech_synthesizer.synthesize_speech("音量已增加") - return True - except Exception as e: - print(f"✗ 增加音量命令执行失败: {e}") - return False - - def _decrease_volume(self, text: str, confidence: float) -> bool: - """减少音量""" - try: - print("🔉 执行减少音量命令") - if self.plugin.speech_synthesizer: - self.plugin.speech_synthesizer.synthesize_speech("音量已减少") - return True - except Exception as e: - print(f"✗ 减少音量命令执行失败: {e}") - return False - - def _play_music(self, text: str, confidence: float) -> bool: - """播放音乐""" - try: - print("🎵 执行播放音乐命令") - if self.plugin.speech_synthesizer: - self.plugin.speech_synthesizer.synthesize_speech("正在播放音乐") - return True - except Exception as e: - print(f"✗ 播放音乐命令执行失败: {e}") - return False - - def _pause_music(self, text: str, confidence: float) -> bool: - """暂停音乐""" - try: - print("⏸ 执行暂停音乐命令") - if self.plugin.speech_synthesizer: - self.plugin.speech_synthesizer.synthesize_speech("音乐已暂停") - return True - except Exception as e: - print(f"✗ 暂停音乐命令执行失败: {e}") - return False - - def _next_track(self, text: str, confidence: float) -> bool: - """下一首""" - try: - print("⏭ 执行播放下一首命令") - if self.plugin.speech_synthesizer: - self.plugin.speech_synthesizer.synthesize_speech("正在播放下一首") - return True - except Exception as e: - print(f"✗ 播放下一首命令执行失败: {e}") - return False - - def _previous_track(self, text: str, confidence: float) -> bool: - """上一首""" - try: - print("⏮ 执行播放上一首命令") - if self.plugin.speech_synthesizer: - self.plugin.speech_synthesizer.synthesize_speech("正在播放上一首") - return True - except Exception as e: - print(f"✗ 播放上一首命令执行失败: {e}") - return False - - def _greet_user(self, text: str, confidence: float) -> bool: - """打招呼""" - try: - print("👋 执行打招呼命令") - if self.plugin.speech_synthesizer: - self.plugin.speech_synthesizer.synthesize_speech("您好!有什么可以帮助您的吗?") - return True - except Exception as e: - print(f"✗ 打招呼命令执行失败: {e}") - return False - - def _acknowledge_thanks(self, text: str, confidence: float) -> bool: - """回应感谢""" - try: - print("🙏 执行回应感谢命令") - if self.plugin.speech_synthesizer: - self.plugin.speech_synthesizer.synthesize_speech("不客气!") - return True - except Exception as e: - print(f"✗ 回应感谢命令执行失败: {e}") - return False - - def _say_goodbye(self, text: str, confidence: float) -> bool: - """道别""" - try: - print("👋 执行道别命令") - if self.plugin.speech_synthesizer: - self.plugin.speech_synthesizer.synthesize_speech("再见!") - return True - except Exception as e: - print(f"✗ 道别命令执行失败: {e}") - return False - - def _provide_help(self, text: str = None, confidence: float = 1.0) -> bool: - """提供帮助""" - try: - help_text = "我可以帮您控制移动、灯光、音量和音乐播放。请说'前进'、'后退'、'左转'、'右转'、'停止'来控制移动;说'打开灯光'或'关闭灯光'来控制灯光;说'增加音量'或'减少音量'来调节音量;说'播放音乐'、'暂停播放'、'下一首'、'上一首'来控制音乐。" - print("ℹ️ 执行提供帮助命令") - if self.plugin.speech_synthesizer: - self.plugin.speech_synthesizer.synthesize_speech(help_text) - return True - except Exception as e: - print(f"✗ 提供帮助命令执行失败: {e}") - return False - - def _show_about(self, text: str, confidence: float) -> bool: - """显示关于信息""" - try: - about_text = "语音控制系统版本1.0.0,基于EG引擎开发。" - print("ℹ️ 执行显示关于信息命令") - if self.plugin.speech_synthesizer: - self.plugin.speech_synthesizer.synthesize_speech(about_text) - return True - except Exception as e: - print(f"✗ 显示关于信息命令执行失败: {e}") - return False - - def get_command_mappings(self) -> Dict[str, Dict[str, Any]]: - """ - 获取命令映射 - - Returns: - 命令映射字典 - """ - return self.command_mappings.copy() - - def get_command_groups(self) -> Dict[str, Dict[str, Any]]: - """ - 获取命令组 - - Returns: - 命令组字典 - """ - return self.command_groups.copy() - - def get_command_history(self) -> List[Dict[str, Any]]: - """ - 获取命令历史 - - Returns: - 命令历史列表 - """ - return self.command_history.copy() - - def clear_command_history(self): - """清空命令历史""" - try: - self.command_history.clear() - print("✓ 命令历史已清空") - except Exception as e: - print(f"✗ 命令历史清空失败: {e}") - - def set_command_config(self, config: Dict[str, Any]) -> bool: - """ - 设置命令配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.command_config.update(config) - print(f"✓ 命令配置已更新: {self.command_config}") - return True - except Exception as e: - print(f"✗ 命令配置设置失败: {e}") - return False - - def get_command_config(self) -> Dict[str, Any]: - """ - 获取命令配置 - - Returns: - 配置字典 - """ - return self.command_config.copy() - - def get_context(self) -> Dict[str, Any]: - """ - 获取当前上下文 - - Returns: - 上下文字典 - """ - return self.command_context.copy() - - def _trigger_command_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发命令回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.command_callbacks: - for callback in self.command_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 命令回调执行失败: {e}") - except Exception as e: - print(f"✗ 命令回调触发失败: {e}") - - def register_command_callback(self, callback_type: str, callback: Callable): - """ - 注册命令回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.command_callbacks: - self.command_callbacks[callback_type].append(callback) - print(f"✓ 命令回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 命令回调注册失败: {e}") - - def unregister_command_callback(self, callback_type: str, callback: Callable): - """ - 注销命令回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.command_callbacks: - if callback in self.command_callbacks[callback_type]: - self.command_callbacks[callback_type].remove(callback) - print(f"✓ 命令回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 命令回调注销失败: {e}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - try: - self.stats = { - 'commands_processed': 0, - 'commands_executed': 0, - 'commands_failed': 0, - 'fuzzy_matches': 0, - 'exact_matches': 0, - 'context_commands': 0, - 'chained_commands': 0 - } - print("✓ 命令处理器统计信息已重置") - except Exception as e: +""" +命令处理器模块 +负责处理语音命令并执行相应操作 +""" + +import time +import re +from typing import Dict, Any, List, Optional, Callable +import threading + +class CommandProcessor: + """ + 命令处理器 + 负责处理语音命令并执行相应操作 + """ + + def __init__(self, plugin): + """ + 初始化命令处理器 + + Args: + plugin: 语音控制插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 命令映射 + self.command_mappings = {} + + # 命令组 + self.command_groups = {} + + # 命令历史 + self.command_history = [] + self.max_history_size = 1000 + + # 命令配置 + self.command_config = { + 'enable_fuzzy_matching': True, + 'fuzzy_threshold': 0.7, + 'enable_context_commands': True, + 'context_timeout': 30.0, + 'enable_chained_commands': True, + 'max_chain_length': 5, + 'enable_confirmation': True, + 'confirmation_threshold': 0.9 + } + + # 上下文管理 + self.command_context = { + 'current_context': 'default', + 'context_stack': [], + 'context_start_time': 0.0, + 'context_variables': {} + } + + # 命令链 + self.command_chain = { + 'is_active': False, + 'commands': [], + 'chain_start_time': 0.0 + } + + # 统计信息 + self.stats = { + 'commands_processed': 0, + 'commands_executed': 0, + 'commands_failed': 0, + 'fuzzy_matches': 0, + 'exact_matches': 0, + 'context_commands': 0, + 'chained_commands': 0 + } + + # 回调函数 + self.command_callbacks = { + 'command_received': [], + 'command_processed': [], + 'command_executed': [], + 'command_failed': [], + 'context_changed': [], + 'chain_started': [], + 'chain_completed': [] + } + + # 时间戳记录 + self.last_command_time = 0.0 + self.last_context_change = 0.0 + + print("✓ 命令处理器已创建") + + def initialize(self) -> bool: + """ + 初始化命令处理器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化默认命令映射 + self._initialize_default_commands() + + # 初始化命令组 + self._initialize_command_groups() + + self.initialized = True + print("✓ 命令处理器初始化完成") + return True + + except Exception as e: + print(f"✗ 命令处理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用命令处理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 命令处理器未初始化") + return False + + self.enabled = True + print("✓ 命令处理器已启用") + return True + + except Exception as e: + print(f"✗ 命令处理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用命令处理器""" + try: + self.enabled = False + print("✓ 命令处理器已禁用") + + except Exception as e: + print(f"✗ 命令处理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理命令处理器资源""" + try: + self.disable() + self.command_mappings.clear() + self.command_groups.clear() + self.command_history.clear() + self.command_callbacks.clear() + self.initialized = False + print("✓ 命令处理器资源已清理") + + except Exception as e: + print(f"✗ 命令处理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新命令处理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.last_command_time = current_time + + # 检查上下文超时 + self._check_context_timeout(current_time) + + # 检查命令链超时 + self._check_chain_timeout(current_time) + + except Exception as e: + print(f"✗ 命令处理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _initialize_default_commands(self): + """初始化默认命令映射""" + try: + # 基础移动命令 + self.register_command('前进', self._move_forward, 'movement', '向前移动') + self.register_command('后退', self._move_backward, 'movement', '向后移动') + self.register_command('左转', self._turn_left, 'movement', '向左转') + self.register_command('右转', self._turn_right, 'movement', '向右转') + self.register_command('停止', self._stop_movement, 'movement', '停止移动') + + # 系统控制命令 + self.register_command('打开灯光', self._toggle_lights, 'system', '切换灯光状态') + self.register_command('关闭灯光', self._toggle_lights, 'system', '切换灯光状态') + self.register_command('增加音量', self._increase_volume, 'system', '增加系统音量') + self.register_command('减少音量', self._decrease_volume, 'system', '减少系统音量') + self.register_command('播放音乐', self._play_music, 'entertainment', '播放音乐') + self.register_command('暂停播放', self._pause_music, 'entertainment', '暂停音乐播放') + self.register_command('下一首', self._next_track, 'entertainment', '播放下一首') + self.register_command('上一首', self._previous_track, 'entertainment', '播放上一首') + + # 交互命令 + self.register_command('你好', self._greet_user, 'interaction', '打招呼') + self.register_command('谢谢', self._acknowledge_thanks, 'interaction', '回应感谢') + self.register_command('再见', self._say_goodbye, 'interaction', '道别') + self.register_command('帮助', self._provide_help, 'interaction', '提供帮助信息') + self.register_command('关于', self._show_about, 'interaction', '显示系统信息') + + print("✓ 默认命令映射初始化完成") + + except Exception as e: + print(f"✗ 默认命令映射初始化失败: {e}") + + def _initialize_command_groups(self): + """初始化命令组""" + try: + self.command_groups = { + 'movement': { + 'name': '移动控制', + 'description': '控制物体移动的命令', + 'commands': ['前进', '后退', '左转', '右转', '停止'] + }, + 'system': { + 'name': '系统控制', + 'description': '控制系统功能的命令', + 'commands': ['打开灯光', '关闭灯光', '增加音量', '减少音量'] + }, + 'entertainment': { + 'name': '娱乐控制', + 'description': '控制娱乐功能的命令', + 'commands': ['播放音乐', '暂停播放', '下一首', '上一首'] + }, + 'interaction': { + 'name': '交互命令', + 'description': '日常交互的命令', + 'commands': ['你好', '谢谢', '再见', '帮助', '关于'] + } + } + + print("✓ 命令组初始化完成") + + except Exception as e: + print(f"✗ 命令组初始化失败: {e}") + + def register_command(self, command_text: str, handler: Callable, + group: str = 'default', description: str = '') -> bool: + """ + 注册命令 + + Args: + command_text: 命令文本 + handler: 命令处理函数 + group: 命令组 + description: 命令描述 + + Returns: + 是否注册成功 + """ + try: + self.command_mappings[command_text] = { + 'handler': handler, + 'group': group, + 'description': description, + 'registered_time': time.time() + } + + # 添加到命令组 + if group in self.command_groups: + if command_text not in self.command_groups[group]['commands']: + self.command_groups[group]['commands'].append(command_text) + else: + self.command_groups[group] = { + 'name': group, + 'description': f'命令组: {group}', + 'commands': [command_text] + } + + print(f"✓ 命令已注册: {command_text}") + return True + + except Exception as e: + print(f"✗ 命令注册失败: {e}") + return False + + def unregister_command(self, command_text: str) -> bool: + """ + 注销命令 + + Args: + command_text: 命令文本 + + Returns: + 是否注销成功 + """ + try: + if command_text in self.command_mappings: + command_info = self.command_mappings[command_text] + group = command_info['group'] + + # 从命令组中移除 + if group in self.command_groups: + if command_text in self.command_groups[group]['commands']: + self.command_groups[group]['commands'].remove(command_text) + + del self.command_mappings[command_text] + print(f"✓ 命令已注销: {command_text}") + return True + else: + print(f"✗ 命令不存在: {command_text}") + return False + + except Exception as e: + print(f"✗ 命令注销失败: {e}") + return False + + def process_command(self, text: str, confidence: float = 1.0) -> bool: + """ + 处理语音命令 + + Args: + text: 命令文本 + confidence: 置信度(0.0-1.0) + + Returns: + 是否处理成功 + """ + try: + if not self.enabled: + print("✗ 命令处理器未启用") + return False + + # 记录命令历史 + command_entry = { + 'text': text, + 'confidence': confidence, + 'timestamp': time.time(), + 'context': self.command_context['current_context'] + } + + self.command_history.append(command_entry) + + # 保持历史记录大小 + if len(self.command_history) > self.max_history_size: + self.command_history.pop(0) + + # 触发命令接收回调 + self._trigger_command_callback('command_received', { + 'text': text, + 'confidence': confidence, + 'command_entry': command_entry + }) + + # 检查是否在命令链中 + if self.command_chain['is_active']: + return self._process_chained_command(text, confidence) + + # 查找匹配的命令 + matched_command = self._find_matching_command(text, confidence) + + if matched_command: + command_text = matched_command['command'] + command_info = self.command_mappings[command_text] + handler = command_info['handler'] + + # 更新统计信息 + if matched_command['type'] == 'exact': + self.stats['exact_matches'] += 1 + else: + self.stats['fuzzy_matches'] += 1 + + # 检查是否需要确认 + if (self.command_config['enable_confirmation'] and + confidence < self.command_config['confirmation_threshold']): + return self._request_confirmation(text, command_text, handler, confidence) + + # 执行命令 + return self._execute_command(command_text, handler, text, confidence, matched_command) + else: + # 未找到匹配命令 + print(f"✗ 未找到匹配命令: {text}") + self.stats['commands_failed'] += 1 + + # 触发命令失败回调 + self._trigger_command_callback('command_failed', { + 'text': text, + 'confidence': confidence, + 'reason': 'no_match' + }) + + # 请求帮助 + if self._is_help_request(text): + self._provide_help() + + return False + + except Exception as e: + print(f"✗ 命令处理失败: {e}") + import traceback + traceback.print_exc() + self.stats['commands_failed'] += 1 + return False + + def _find_matching_command(self, text: str, confidence: float) -> Optional[Dict[str, Any]]: + """ + 查找匹配的命令 + + Args: + text: 命令文本 + confidence: 置信度 + + Returns: + 匹配的命令信息或None + """ + try: + # 首先尝试精确匹配 + if text in self.command_mappings: + return { + 'command': text, + 'type': 'exact', + 'confidence': confidence + } + + # 尝试上下文命令 + if (self.command_config['enable_context_commands'] and + self.command_context['current_context'] != 'default'): + context_commands = self.command_context['context_variables'].get('commands', []) + for command in context_commands: + if command in self.command_mappings and text in command: + return { + 'command': command, + 'type': 'context', + 'confidence': confidence * 0.9 + } + self.stats['context_commands'] += 1 + + # 尝试模糊匹配 + if self.command_config['enable_fuzzy_matching']: + best_match = self._fuzzy_match_command(text) + if best_match: + return best_match + + return None + + except Exception as e: + print(f"✗ 命令匹配失败: {e}") + return None + + def _fuzzy_match_command(self, text: str) -> Optional[Dict[str, Any]]: + """ + 模糊匹配命令 + + Args: + text: 命令文本 + + Returns: + 匹配的命令信息或None + """ + try: + best_match = None + best_score = 0.0 + + for command_text in self.command_mappings: + score = self._calculate_similarity(text, command_text) + if score > best_score and score >= self.command_config['fuzzy_threshold']: + best_score = score + best_match = { + 'command': command_text, + 'type': 'fuzzy', + 'confidence': score + } + + return best_match + + except Exception as e: + print(f"✗ 模糊匹配失败: {e}") + return None + + def _calculate_similarity(self, text1: str, text2: str) -> float: + """ + 计算文本相似度 + + Args: + text1: 文本1 + text2: 文本2 + + Returns: + 相似度(0.0-1.0) + """ + try: + # 简单的相似度计算(实际应用中可以使用更复杂的算法) + set1 = set(text1) + set2 = set(text2) + + intersection = len(set1.intersection(set2)) + union = len(set1.union(set2)) + + if union == 0: + return 0.0 + + return intersection / union + + except Exception as e: + print(f"✗ 相似度计算失败: {e}") + return 0.0 + + def _process_chained_command(self, text: str, confidence: float) -> bool: + """ + 处理命令链中的命令 + + Args: + text: 命令文本 + confidence: 置信度 + + Returns: + 是否处理成功 + """ + try: + # 添加到命令链 + self.command_chain['commands'].append({ + 'text': text, + 'confidence': confidence, + 'timestamp': time.time() + }) + + # 检查是否完成命令链 + if self._is_chain_completion(text): + return self._execute_command_chain() + + return True + + except Exception as e: + print(f"✗ 命令链处理失败: {e}") + return False + + def _is_chain_completion(self, text: str) -> bool: + """ + 检查是否完成命令链 + + Args: + text: 命令文本 + + Returns: + 是否完成命令链 + """ + try: + completion_keywords = ['完成', '结束', '执行', '确定', '确认'] + return any(keyword in text for keyword in completion_keywords) + except Exception as e: + print(f"✗ 命令链完成检查失败: {e}") + return False + + def _execute_command_chain(self) -> bool: + """ + 执行命令链 + + Returns: + 是否执行成功 + """ + try: + # 触发命令链完成回调 + self._trigger_command_callback('chain_completed', { + 'commands': self.command_chain['commands'] + }) + + # 重置命令链 + self.command_chain['is_active'] = False + self.command_chain['commands'] = [] + self.command_chain['chain_start_time'] = 0.0 + + self.stats['chained_commands'] += 1 + return True + + except Exception as e: + print(f"✗ 命令链执行失败: {e}") + return False + + def _request_confirmation(self, original_text: str, command_text: str, + handler: Callable, confidence: float) -> bool: + """ + 请求命令确认 + + Args: + original_text: 原始文本 + command_text: 命令文本 + handler: 处理函数 + confidence: 置信度 + + Returns: + 是否请求成功 + """ + try: + confirmation_text = f"您是想执行 {command_text} 吗?请确认。" + + # 通过语音合成器播放确认请求 + if self.plugin.speech_synthesizer: + self.plugin.speech_synthesizer.synthesize_speech(confirmation_text) + + # 存储待确认的命令 + self.pending_confirmation = { + 'original_text': original_text, + 'command_text': command_text, + 'handler': handler, + 'confidence': confidence, + 'timestamp': time.time() + } + + return True + + except Exception as e: + print(f"✗ 确认请求失败: {e}") + return False + + def _execute_command(self, command_text: str, handler: Callable, + original_text: str, confidence: float, + match_info: Dict[str, Any]) -> bool: + """ + 执行命令 + + Args: + command_text: 命令文本 + handler: 处理函数 + original_text: 原始文本 + confidence: 置信度 + match_info: 匹配信息 + + Returns: + 是否执行成功 + """ + try: + # 触发命令处理回调 + self._trigger_command_callback('command_processed', { + 'command_text': command_text, + 'original_text': original_text, + 'confidence': confidence, + 'match_type': match_info['type'] + }) + + # 执行命令处理函数 + result = handler(original_text, confidence) + + if result: + self.stats['commands_executed'] += 1 + + # 触发命令执行回调 + self._trigger_command_callback('command_executed', { + 'command_text': command_text, + 'original_text': original_text, + 'confidence': confidence + }) + + # 检查是否需要进入特定上下文 + self._check_context_transition(command_text) + + print(f"✓ 命令执行成功: {command_text}") + return True + else: + self.stats['commands_failed'] += 1 + + # 触发命令失败回调 + self._trigger_command_callback('command_failed', { + 'command_text': command_text, + 'original_text': original_text, + 'confidence': confidence, + 'reason': 'execution_failed' + }) + + print(f"✗ 命令执行失败: {command_text}") + return False + + except Exception as e: + print(f"✗ 命令执行异常: {e}") + import traceback + traceback.print_exc() + self.stats['commands_failed'] += 1 + + # 触发命令失败回调 + self._trigger_command_callback('command_failed', { + 'command_text': command_text, + 'original_text': original_text, + 'confidence': confidence, + 'reason': 'exception', + 'exception': str(e) + }) + + return False + + def _check_context_transition(self, command_text: str): + """ + 检查上下文转换 + + Args: + command_text: 命令文本 + """ + try: + # 根据命令类型设置上下文 + context_transitions = { + '播放音乐': 'music', + '暂停播放': 'music', + '下一首': 'music', + '上一首': 'music' + } + + if command_text in context_transitions: + self.set_context(context_transitions[command_text]) + + except Exception as e: + print(f"✗ 上下文转换检查失败: {e}") + + def _check_context_timeout(self, current_time: float): + """ + 检查上下文超时 + + Args: + current_time: 当前时间 + """ + try: + if (self.command_context['current_context'] != 'default' and + current_time - self.command_context['context_start_time'] > + self.command_config['context_timeout']): + self.reset_context() + + except Exception as e: + print(f"✗ 上下文超时检查失败: {e}") + + def _check_chain_timeout(self, current_time: float): + """ + 检查命令链超时 + + Args: + current_time: 当前时间 + """ + try: + if (self.command_chain['is_active'] and + current_time - self.command_chain['chain_start_time'] > 10.0): # 10秒超时 + self.command_chain['is_active'] = False + self.command_chain['commands'] = [] + self.command_chain['chain_start_time'] = 0.0 + + except Exception as e: + print(f"✗ 命令链超时检查失败: {e}") + + def set_context(self, context_name: str, variables: Dict[str, Any] = None) -> bool: + """ + 设置命令上下文 + + Args: + context_name: 上下文名称 + variables: 上下文变量 + + Returns: + 是否设置成功 + """ + try: + # 保存当前上下文到栈中 + self.command_context['context_stack'].append({ + 'context': self.command_context['current_context'], + 'variables': self.command_context['context_variables'].copy(), + 'timestamp': self.command_context['context_start_time'] + }) + + # 设置新上下文 + self.command_context['current_context'] = context_name + self.command_context['context_start_time'] = time.time() + if variables: + self.command_context['context_variables'] = variables + else: + self.command_context['context_variables'] = {} + + # 触发上下文变更回调 + self._trigger_command_callback('context_changed', { + 'old_context': self.command_context['context_stack'][-1]['context'] if self.command_context['context_stack'] else 'default', + 'new_context': context_name, + 'variables': variables + }) + + self.last_context_change = time.time() + print(f"✓ 上下文已设置为: {context_name}") + return True + + except Exception as e: + print(f"✗ 上下文设置失败: {e}") + return False + + def reset_context(self) -> bool: + """ + 重置命令上下文 + + Returns: + 是否重置成功 + """ + try: + if self.command_context['context_stack']: + # 恢复上一个上下文 + previous_context = self.command_context['context_stack'].pop() + self.command_context['current_context'] = previous_context['context'] + self.command_context['context_variables'] = previous_context['variables'] + self.command_context['context_start_time'] = previous_context['timestamp'] + else: + # 重置为默认上下文 + self.command_context['current_context'] = 'default' + self.command_context['context_variables'] = {} + self.command_context['context_start_time'] = time.time() + + # 触发上下文变更回调 + self._trigger_command_callback('context_changed', { + 'old_context': 'previous', + 'new_context': self.command_context['current_context'], + 'variables': self.command_context['context_variables'] + }) + + print("✓ 上下文已重置") + return True + + except Exception as e: + print(f"✗ 上下文重置失败: {e}") + return False + + def start_command_chain(self) -> bool: + """ + 开始命令链 + + Returns: + 是否开始成功 + """ + try: + self.command_chain['is_active'] = True + self.command_chain['commands'] = [] + self.command_chain['chain_start_time'] = time.time() + + # 触发命令链开始回调 + self._trigger_command_callback('chain_started', {}) + + print("✓ 命令链已开始") + return True + + except Exception as e: + print(f"✗ 命令链开始失败: {e}") + return False + + def _is_help_request(self, text: str) -> bool: + """ + 检查是否为帮助请求 + + Args: + text: 文本 + + Returns: + 是否为帮助请求 + """ + try: + help_keywords = ['帮助', 'help', '怎么用', '使用方法', '功能'] + return any(keyword in text for keyword in help_keywords) + except Exception as e: + print(f"✗ 帮助请求检查失败: {e}") + return False + + # 默认命令处理函数 + def _move_forward(self, text: str, confidence: float) -> bool: + """向前移动""" + try: + print("→ 执行向前移动命令") + if self.plugin.speech_synthesizer: + self.plugin.speech_synthesizer.synthesize_speech("正在向前移动") + return True + except Exception as e: + print(f"✗ 向前移动命令执行失败: {e}") + return False + + def _move_backward(self, text: str, confidence: float) -> bool: + """向后移动""" + try: + print("← 执行向后移动命令") + if self.plugin.speech_synthesizer: + self.plugin.speech_synthesizer.synthesize_speech("正在向后移动") + return True + except Exception as e: + print(f"✗ 向后移动命令执行失败: {e}") + return False + + def _turn_left(self, text: str, confidence: float) -> bool: + """向左转""" + try: + print("↺ 执行向左转命令") + if self.plugin.speech_synthesizer: + self.plugin.speech_synthesizer.synthesize_speech("正在向左转") + return True + except Exception as e: + print(f"✗ 向左转命令执行失败: {e}") + return False + + def _turn_right(self, text: str, confidence: float) -> bool: + """向右转""" + try: + print("↻ 执行向右转命令") + if self.plugin.speech_synthesizer: + self.plugin.speech_synthesizer.synthesize_speech("正在向右转") + return True + except Exception as e: + print(f"✗ 向右转命令执行失败: {e}") + return False + + def _stop_movement(self, text: str, confidence: float) -> bool: + """停止移动""" + try: + print("⏹ 执行停止移动命令") + if self.plugin.speech_synthesizer: + self.plugin.speech_synthesizer.synthesize_speech("已停止移动") + return True + except Exception as e: + print(f"✗ 停止移动命令执行失败: {e}") + return False + + def _toggle_lights(self, text: str, confidence: float) -> bool: + """切换灯光""" + try: + action = "打开" if "打开" in text else "关闭" + print(f"💡 执行{action}灯光命令") + if self.plugin.speech_synthesizer: + self.plugin.speech_synthesizer.synthesize_speech(f"已{action}灯光") + return True + except Exception as e: + print(f"✗ 灯光控制命令执行失败: {e}") + return False + + def _increase_volume(self, text: str, confidence: float) -> bool: + """增加音量""" + try: + print("🔊 执行增加音量命令") + if self.plugin.speech_synthesizer: + self.plugin.speech_synthesizer.synthesize_speech("音量已增加") + return True + except Exception as e: + print(f"✗ 增加音量命令执行失败: {e}") + return False + + def _decrease_volume(self, text: str, confidence: float) -> bool: + """减少音量""" + try: + print("🔉 执行减少音量命令") + if self.plugin.speech_synthesizer: + self.plugin.speech_synthesizer.synthesize_speech("音量已减少") + return True + except Exception as e: + print(f"✗ 减少音量命令执行失败: {e}") + return False + + def _play_music(self, text: str, confidence: float) -> bool: + """播放音乐""" + try: + print("🎵 执行播放音乐命令") + if self.plugin.speech_synthesizer: + self.plugin.speech_synthesizer.synthesize_speech("正在播放音乐") + return True + except Exception as e: + print(f"✗ 播放音乐命令执行失败: {e}") + return False + + def _pause_music(self, text: str, confidence: float) -> bool: + """暂停音乐""" + try: + print("⏸ 执行暂停音乐命令") + if self.plugin.speech_synthesizer: + self.plugin.speech_synthesizer.synthesize_speech("音乐已暂停") + return True + except Exception as e: + print(f"✗ 暂停音乐命令执行失败: {e}") + return False + + def _next_track(self, text: str, confidence: float) -> bool: + """下一首""" + try: + print("⏭ 执行播放下一首命令") + if self.plugin.speech_synthesizer: + self.plugin.speech_synthesizer.synthesize_speech("正在播放下一首") + return True + except Exception as e: + print(f"✗ 播放下一首命令执行失败: {e}") + return False + + def _previous_track(self, text: str, confidence: float) -> bool: + """上一首""" + try: + print("⏮ 执行播放上一首命令") + if self.plugin.speech_synthesizer: + self.plugin.speech_synthesizer.synthesize_speech("正在播放上一首") + return True + except Exception as e: + print(f"✗ 播放上一首命令执行失败: {e}") + return False + + def _greet_user(self, text: str, confidence: float) -> bool: + """打招呼""" + try: + print("👋 执行打招呼命令") + if self.plugin.speech_synthesizer: + self.plugin.speech_synthesizer.synthesize_speech("您好!有什么可以帮助您的吗?") + return True + except Exception as e: + print(f"✗ 打招呼命令执行失败: {e}") + return False + + def _acknowledge_thanks(self, text: str, confidence: float) -> bool: + """回应感谢""" + try: + print("🙏 执行回应感谢命令") + if self.plugin.speech_synthesizer: + self.plugin.speech_synthesizer.synthesize_speech("不客气!") + return True + except Exception as e: + print(f"✗ 回应感谢命令执行失败: {e}") + return False + + def _say_goodbye(self, text: str, confidence: float) -> bool: + """道别""" + try: + print("👋 执行道别命令") + if self.plugin.speech_synthesizer: + self.plugin.speech_synthesizer.synthesize_speech("再见!") + return True + except Exception as e: + print(f"✗ 道别命令执行失败: {e}") + return False + + def _provide_help(self, text: str = None, confidence: float = 1.0) -> bool: + """提供帮助""" + try: + help_text = "我可以帮您控制移动、灯光、音量和音乐播放。请说'前进'、'后退'、'左转'、'右转'、'停止'来控制移动;说'打开灯光'或'关闭灯光'来控制灯光;说'增加音量'或'减少音量'来调节音量;说'播放音乐'、'暂停播放'、'下一首'、'上一首'来控制音乐。" + print("ℹ️ 执行提供帮助命令") + if self.plugin.speech_synthesizer: + self.plugin.speech_synthesizer.synthesize_speech(help_text) + return True + except Exception as e: + print(f"✗ 提供帮助命令执行失败: {e}") + return False + + def _show_about(self, text: str, confidence: float) -> bool: + """显示关于信息""" + try: + about_text = "语音控制系统版本1.0.0,基于EG引擎开发。" + print("ℹ️ 执行显示关于信息命令") + if self.plugin.speech_synthesizer: + self.plugin.speech_synthesizer.synthesize_speech(about_text) + return True + except Exception as e: + print(f"✗ 显示关于信息命令执行失败: {e}") + return False + + def get_command_mappings(self) -> Dict[str, Dict[str, Any]]: + """ + 获取命令映射 + + Returns: + 命令映射字典 + """ + return self.command_mappings.copy() + + def get_command_groups(self) -> Dict[str, Dict[str, Any]]: + """ + 获取命令组 + + Returns: + 命令组字典 + """ + return self.command_groups.copy() + + def get_command_history(self) -> List[Dict[str, Any]]: + """ + 获取命令历史 + + Returns: + 命令历史列表 + """ + return self.command_history.copy() + + def clear_command_history(self): + """清空命令历史""" + try: + self.command_history.clear() + print("✓ 命令历史已清空") + except Exception as e: + print(f"✗ 命令历史清空失败: {e}") + + def set_command_config(self, config: Dict[str, Any]) -> bool: + """ + 设置命令配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.command_config.update(config) + print(f"✓ 命令配置已更新: {self.command_config}") + return True + except Exception as e: + print(f"✗ 命令配置设置失败: {e}") + return False + + def get_command_config(self) -> Dict[str, Any]: + """ + 获取命令配置 + + Returns: + 配置字典 + """ + return self.command_config.copy() + + def get_context(self) -> Dict[str, Any]: + """ + 获取当前上下文 + + Returns: + 上下文字典 + """ + return self.command_context.copy() + + def _trigger_command_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发命令回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.command_callbacks: + for callback in self.command_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 命令回调执行失败: {e}") + except Exception as e: + print(f"✗ 命令回调触发失败: {e}") + + def register_command_callback(self, callback_type: str, callback: Callable): + """ + 注册命令回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.command_callbacks: + self.command_callbacks[callback_type].append(callback) + print(f"✓ 命令回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 命令回调注册失败: {e}") + + def unregister_command_callback(self, callback_type: str, callback: Callable): + """ + 注销命令回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.command_callbacks: + if callback in self.command_callbacks[callback_type]: + self.command_callbacks[callback_type].remove(callback) + print(f"✓ 命令回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 命令回调注销失败: {e}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + try: + self.stats = { + 'commands_processed': 0, + 'commands_executed': 0, + 'commands_failed': 0, + 'fuzzy_matches': 0, + 'exact_matches': 0, + 'context_commands': 0, + 'chained_commands': 0 + } + print("✓ 命令处理器统计信息已重置") + except Exception as e: print(f"✗ 命令处理器统计信息重置失败: {e}") \ No newline at end of file diff --git a/plugins/user/voice_control/config/config_manager.py b/plugins/user/voice_control/config/config_manager.py index b2c00c2e..9bf34c02 100644 --- a/plugins/user/voice_control/config/config_manager.py +++ b/plugins/user/voice_control/config/config_manager.py @@ -1,859 +1,859 @@ -""" -配置管理器模块 -负责管理语音控制插件的配置 -""" - -import time -import json -import os -from typing import Dict, Any, List, Optional -import threading - -class VoiceConfigManager: - """ - 语音配置管理器 - 负责管理语音控制插件的配置 - """ - - def __init__(self, plugin): - """ - 初始化配置管理器 - - Args: - plugin: 语音控制插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 配置文件路径 - self.config_path = "/home/hello/EG/plugins/user/voice_control/config/voice_config.json" - - # 默认配置 - self.default_config = { - "general": { - "enable_voice_control": True, - "enable_voice_feedback": True, - "language": "zh-CN", - "wake_word": "小艾", - "enable_wake_word": True, - "continuous_listening": False, - "silence_timeout": 3.0 - }, - "recognition": { - "language": "zh-CN", - "sample_rate": 16000, - "channels": 1, - "sensitivity": 0.5, - "enable_keyword_spotting": True, - "enable_continuous_recognition": True, - "silence_threshold": 0.01, - "speech_threshold": 0.02, - "max_recording_duration": 10.0 - }, - "synthesis": { - "language": "zh-CN", - "voice": "default", - "speed": 1.0, - "volume": 1.0, - "pitch": 1.0, - "enable_ssml": False - }, - "commands": { - "enable_fuzzy_matching": True, - "fuzzy_threshold": 0.7, - "enable_context_commands": True, - "context_timeout": 30.0, - "enable_chained_commands": True, - "max_chain_length": 5, - "enable_confirmation": True, - "confirmation_threshold": 0.9 - }, - "audio": { - "input_device": "default", - "output_device": "default", - "sample_rate": 22050, - "channels": 1, - "enable_echo_cancellation": True, - "enable_noise_reduction": True, - "enable_auto_gain": True - }, - "nlp": { - "primary_language": "zh-CN", - "enable_intent_recognition": True, - "enable_entity_extraction": True, - "enable_sentiment_analysis": True, - "confidence_threshold": 0.6 - }, - "events": { - "enable_event_queue": True, - "max_queue_size": 100, - "enable_event_filtering": True, - "filter_duplicate_events": True, - "duplicate_time_window": 1.0 - } - } - - # 当前配置 - self.current_config = {} - - # 配置变更监听器 - self.config_listeners = {} - - # 配置备份 - self.config_backup = {} - - # 配置统计 - self.config_stats = { - "loads": 0, - "saves": 0, - "changes": 0, - "errors": 0, - "backups": 0 - } - - # 线程锁 - self.config_lock = threading.RLock() - - # 回调函数 - self.config_callbacks = { - "config_loaded": [], - "config_saved": [], - "config_changed": [], - "config_error": [] - } - - # 时间戳记录 - self.last_load_time = 0.0 - self.last_save_time = 0.0 - self.last_change_time = 0.0 - - print("✓ 语音配置管理器已创建") - - def initialize(self) -> bool: - """ - 初始化配置管理器 - - Returns: - 是否初始化成功 - """ - try: - # 创建配置目录 - config_dir = os.path.dirname(self.config_path) - if not os.path.exists(config_dir): - os.makedirs(config_dir) - - # 加载配置 - self.load_config() - - # 如果配置为空,使用默认配置 - if not self.current_config: - self.current_config = self.default_config.copy() - self.save_config() - - self.initialized = True - print("✓ 语音配置管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 语音配置管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用配置管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 配置管理器未初始化") - return False - - self.enabled = True - print("✓ 语音配置管理器已启用") - return True - - except Exception as e: - print(f"✗ 语音配置管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用配置管理器""" - try: - self.enabled = False - print("✓ 语音配置管理器已禁用") - - except Exception as e: - print(f"✗ 语音配置管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理配置管理器资源""" - try: - self.disable() - self.config_listeners.clear() - self.config_callbacks.clear() - self.initialized = False - print("✓ 语音配置管理器资源已清理") - - except Exception as e: - print(f"✗ 语音配置管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新配置管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - current_time = time.time() - # 配置管理器不需要频繁更新 - pass - - except Exception as e: - print(f"✗ 语音配置管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def load_config(self) -> bool: - """ - 加载配置 - - Returns: - 是否加载成功 - """ - try: - with self.config_lock: - if os.path.exists(self.config_path): - with open(self.config_path, 'r', encoding='utf-8') as f: - self.current_config = json.load(f) - else: - # 使用默认配置 - self.current_config = self.default_config.copy() - - self.config_stats["loads"] += 1 - self.last_load_time = time.time() - - # 触发配置加载回调 - self._trigger_config_callback("config_loaded", { - "config": self.current_config, - "timestamp": self.last_load_time - }) - - print("✓ 配置加载完成") - return True - - except Exception as e: - print(f"✗ 配置加载失败: {e}") - self.config_stats["errors"] += 1 - - # 触发配置错误回调 - self._trigger_config_callback("config_error", { - "error": str(e), - "operation": "load" - }) - - return False - - def save_config(self) -> bool: - """ - 保存配置 - - Returns: - 是否保存成功 - """ - try: - with self.config_lock: - # 创建配置目录 - config_dir = os.path.dirname(self.config_path) - if not os.path.exists(config_dir): - os.makedirs(config_dir) - - # 保存配置 - with open(self.config_path, 'w', encoding='utf-8') as f: - json.dump(self.current_config, f, ensure_ascii=False, indent=2) - - self.config_stats["saves"] += 1 - self.last_save_time = time.time() - - # 触发配置保存回调 - self._trigger_config_callback("config_saved", { - "config": self.current_config, - "timestamp": self.last_save_time - }) - - print("✓ 配置保存完成") - return True - - except Exception as e: - print(f"✗ 配置保存失败: {e}") - self.config_stats["errors"] += 1 - - # 触发配置错误回调 - self._trigger_config_callback("config_error", { - "error": str(e), - "operation": "save" - }) - - return False - - def get_config(self, section: str = None, key: str = None, default: Any = None) -> Any: - """ - 获取配置值 - - Args: - section: 配置节名称 - key: 配置键名称 - default: 默认值 - - Returns: - 配置值 - """ - try: - with self.config_lock: - if section is None: - return self.current_config.copy() - elif key is None: - return self.current_config.get(section, {}).copy() - else: - return self.current_config.get(section, {}).get(key, default) - - except Exception as e: - print(f"✗ 配置获取失败: {e}") - return default - - def set_config(self, section: str, key: str = None, value: Any = None, save: bool = True) -> bool: - """ - 设置配置值 - - Args: - section: 配置节名称 - key: 配置键名称 - value: 配置值 - save: 是否保存到文件 - - Returns: - 是否设置成功 - """ - try: - with self.config_lock: - # 备份当前配置 - self._backup_config() - - if key is None: - # 设置整个节 - if isinstance(value, dict): - self.current_config[section] = value - else: - print(f"✗ 配置值必须是字典类型: {section}") - return False - else: - # 设置特定键值 - if section not in self.current_config: - self.current_config[section] = {} - - old_value = self.current_config[section].get(key) - self.current_config[section][key] = value - - # 触发配置变更回调 - self._trigger_config_callback("config_changed", { - "section": section, - "key": key, - "old_value": old_value, - "new_value": value, - "timestamp": time.time() - }) - - self.config_stats["changes"] += 1 - self.last_change_time = time.time() - - # 保存配置 - if save: - return self.save_config() - else: - return True - - except Exception as e: - print(f"✗ 配置设置失败: {e}") - self.config_stats["errors"] += 1 - - # 触发配置错误回调 - self._trigger_config_callback("config_error", { - "error": str(e), - "operation": "set" - }) - - return False - - def update_config(self, updates: Dict[str, Any], save: bool = True) -> bool: - """ - 批量更新配置 - - Args: - updates: 更新字典 - save: 是否保存到文件 - - Returns: - 是否更新成功 - """ - try: - with self.config_lock: - # 备份当前配置 - self._backup_config() - - old_values = {} - - # 应用更新 - for section, section_data in updates.items(): - if isinstance(section_data, dict): - if section not in self.current_config: - self.current_config[section] = {} - - for key, value in section_data.items(): - old_values[f"{section}.{key}"] = self.current_config[section].get(key) - self.current_config[section][key] = value - else: - old_values[section] = self.current_config.get(section) - self.current_config[section] = section_data - - self.config_stats["changes"] += 1 - self.last_change_time = time.time() - - # 触发配置变更回调 - self._trigger_config_callback("config_changed", { - "updates": updates, - "old_values": old_values, - "timestamp": self.last_change_time - }) - - # 保存配置 - if save: - return self.save_config() - else: - return True - - except Exception as e: - print(f"✗ 配置批量更新失败: {e}") - self.config_stats["errors"] += 1 - - # 触发配置错误回调 - self._trigger_config_callback("config_error", { - "error": str(e), - "operation": "update" - }) - - return False - - def reset_config(self, section: str = None) -> bool: - """ - 重置配置 - - Args: - section: 要重置的配置节,None表示重置所有配置 - - Returns: - 是否重置成功 - """ - try: - with self.config_lock: - # 备份当前配置 - self._backup_config() - - if section is None: - # 重置所有配置 - self.current_config = self.default_config.copy() - elif section in self.default_config: - # 重置特定节 - self.current_config[section] = self.default_config[section].copy() - else: - print(f"✗ 无效的配置节: {section}") - return False - - self.config_stats["changes"] += 1 - self.last_change_time = time.time() - - # 触发配置变更回调 - self._trigger_config_callback("config_changed", { - "operation": "reset", - "section": section, - "timestamp": self.last_change_time - }) - - # 保存配置 - return self.save_config() - - except Exception as e: - print(f"✗ 配置重置失败: {e}") - self.config_stats["errors"] += 1 - - # 触发配置错误回调 - self._trigger_config_callback("config_error", { - "error": str(e), - "operation": "reset" - }) - - return False - - def _backup_config(self): - """备份当前配置""" - try: - self.config_backup = json.loads(json.dumps(self.current_config)) - self.config_stats["backups"] += 1 - print("✓ 配置已备份") - except Exception as e: - print(f"✗ 配置备份失败: {e}") - - def restore_config(self) -> bool: - """ - 恢复配置备份 - - Returns: - 是否恢复成功 - """ - try: - with self.config_lock: - if self.config_backup: - self.current_config = json.loads(json.dumps(self.config_backup)) - self.config_stats["changes"] += 1 - self.last_change_time = time.time() - - # 触发配置变更回调 - self._trigger_config_callback("config_changed", { - "operation": "restore", - "timestamp": self.last_change_time - }) - - # 保存配置 - return self.save_config() - else: - print("✗ 没有可用的配置备份") - return False - - except Exception as e: - print(f"✗ 配置恢复失败: {e}") - self.config_stats["errors"] += 1 - - # 触发配置错误回调 - self._trigger_config_callback("config_error", { - "error": str(e), - "operation": "restore" - }) - - return False - - def validate_config(self) -> bool: - """ - 验证配置 - - Returns: - 配置是否有效 - """ - try: - # 这里可以添加配置验证逻辑 - # 例如:检查必需的配置项是否存在,值是否在有效范围内等 - - # 检查基本结构 - required_sections = ["general", "recognition", "synthesis", "commands"] - for section in required_sections: - if section not in self.current_config: - print(f"✗ 缺少必需的配置节: {section}") - return False - - # 检查关键配置值范围 - general = self.current_config.get("general", {}) - if "language" in general and general["language"] not in ["zh-CN", "en-US", "ja-JP"]: - print("✗ 无效的语言设置") - return False - - recognition = self.current_config.get("recognition", {}) - if "sample_rate" in recognition and recognition["sample_rate"] not in [8000, 16000, 22050, 44100, 48000]: - print("✗ 无效的采样率设置") - return False - - synthesis = self.current_config.get("synthesis", {}) - if "speed" in synthesis and not (0.5 <= synthesis["speed"] <= 2.0): - print("✗ 语速设置超出有效范围 (0.5-2.0)") - return False - - if "volume" in synthesis and not (0.0 <= synthesis["volume"] <= 1.0): - print("✗ 音量设置超出有效范围 (0.0-1.0)") - return False - - print("✓ 配置验证通过") - return True - - except Exception as e: - print(f"✗ 配置验证失败: {e}") - return False - - def export_config(self, file_path: str) -> bool: - """ - 导出配置到文件 - - Args: - file_path: 导出文件路径 - - Returns: - 是否导出成功 - """ - try: - with self.config_lock: - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(self.current_config, f, ensure_ascii=False, indent=2) - - print(f"✓ 配置已导出到: {file_path}") - return True - - except Exception as e: - print(f"✗ 配置导出失败: {e}") - return False - - def import_config(self, file_path: str) -> bool: - """ - 从文件导入配置 - - Args: - file_path: 导入文件路径 - - Returns: - 是否导入成功 - """ - try: - with self.config_lock: - # 备份当前配置 - self._backup_config() - - with open(file_path, 'r', encoding='utf-8') as f: - imported_config = json.load(f) - - # 验证导入的配置 - temp_manager = VoiceConfigManager(self.plugin) - temp_manager.current_config = imported_config - if not temp_manager.validate_config(): - print("✗ 导入的配置无效") - return False - - # 应用导入的配置 - self.current_config = imported_config - self.config_stats["changes"] += 1 - self.last_change_time = time.time() - - # 触发配置变更回调 - self._trigger_config_callback("config_changed", { - "operation": "import", - "source": file_path, - "timestamp": self.last_change_time - }) - - # 保存配置 - return self.save_config() - - except Exception as e: - print(f"✗ 配置导入失败: {e}") - self.config_stats["errors"] += 1 - - # 触发配置错误回调 - self._trigger_config_callback("config_error", { - "error": str(e), - "operation": "import" - }) - - return False - - def get_default_config(self) -> Dict[str, Any]: - """ - 获取默认配置 - - Returns: - 默认配置字典 - """ - return self.default_config.copy() - - def get_config_stats(self) -> Dict[str, int]: - """ - 获取配置统计信息 - - Returns: - 统计信息字典 - """ - return self.config_stats.copy() - - def reset_config_stats(self): - """重置配置统计信息""" - try: - self.config_stats = { - "loads": 0, - "saves": 0, - "changes": 0, - "errors": 0, - "backups": 0 - } - print("✓ 配置统计信息已重置") - except Exception as e: - print(f"✗ 配置统计信息重置失败: {e}") - - def register_config_listener(self, section: str, key: str, listener: callable) -> bool: - """ - 注册配置监听器 - - Args: - section: 配置节名称 - key: 配置键名称 - listener: 监听器函数 - - Returns: - 是否注册成功 - """ - try: - listener_key = f"{section}.{key}" - if listener_key not in self.config_listeners: - self.config_listeners[listener_key] = [] - - self.config_listeners[listener_key].append(listener) - print(f"✓ 配置监听器已注册: {listener_key}") - return True - - except Exception as e: - print(f"✗ 配置监听器注册失败: {e}") - return False - - def unregister_config_listener(self, section: str, key: str, listener: callable) -> bool: - """ - 注销配置监听器 - - Args: - section: 配置节名称 - key: 配置键名称 - listener: 监听器函数 - - Returns: - 是否注销成功 - """ - try: - listener_key = f"{section}.{key}" - if listener_key in self.config_listeners: - if listener in self.config_listeners[listener_key]: - self.config_listeners[listener_key].remove(listener) - print(f"✓ 配置监听器已注销: {listener_key}") - return True - - print(f"✗ 配置监听器不存在: {listener_key}") - return False - - except Exception as e: - print(f"✗ 配置监听器注销失败: {e}") - return False - - def _trigger_config_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发配置回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.config_callbacks: - for callback in self.config_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 配置回调执行失败: {e}") - except Exception as e: - print(f"✗ 配置回调触发失败: {e}") - - def register_config_callback(self, callback_type: str, callback: callable): - """ - 注册配置回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.config_callbacks: - self.config_callbacks[callback_type].append(callback) - print(f"✓ 配置回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 配置回调注册失败: {e}") - - def unregister_config_callback(self, callback_type: str, callback: callable): - """ - 注销配置回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.config_callbacks: - if callback in self.config_callbacks[callback_type]: - self.config_callbacks[callback_type].remove(callback) - print(f"✓ 配置回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 配置回调注销失败: {e}") - - def apply_config_to_modules(self): - """ - 将配置应用到各个模块 - """ - try: - if not self.enabled: - return - - # 应用配置到语音管理器 - if self.plugin.voice_manager: - general_config = self.get_config("general") - self.plugin.voice_manager.set_voice_config(general_config) - - # 应用配置到语音识别器 - if self.plugin.speech_recognizer: - recognition_config = self.get_config("recognition") - self.plugin.speech_recognizer.set_recognition_config(recognition_config) - - # 应用配置到语音合成器 - if self.plugin.speech_synthesizer: - synthesis_config = self.get_config("synthesis") - self.plugin.speech_synthesizer.set_synthesis_config(synthesis_config) - - # 应用配置到命令处理器 - if self.plugin.command_processor: - commands_config = self.get_config("commands") - self.plugin.command_processor.set_command_config(commands_config) - - # 应用配置到音频管理器 - if self.plugin.audio_manager: - audio_config = self.get_config("audio") - self.plugin.audio_manager.set_audio_config(audio_config) - - # 应用配置到NLP处理器 - if self.plugin.nlp_processor: - nlp_config = self.get_config("nlp") - self.plugin.nlp_processor.set_nlp_config(nlp_config) - - # 应用配置到事件处理器 - if self.plugin.event_handler: - events_config = self.get_config("events") - self.plugin.event_handler.set_event_config(events_config) - - print("✓ 配置已应用到所有模块") - - except Exception as e: - print(f"✗ 配置应用失败: {e}") - import traceback +""" +配置管理器模块 +负责管理语音控制插件的配置 +""" + +import time +import json +import os +from typing import Dict, Any, List, Optional +import threading + +class VoiceConfigManager: + """ + 语音配置管理器 + 负责管理语音控制插件的配置 + """ + + def __init__(self, plugin): + """ + 初始化配置管理器 + + Args: + plugin: 语音控制插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 配置文件路径 + self.config_path = "/home/hello/EG/plugins/user/voice_control/config/voice_config.json" + + # 默认配置 + self.default_config = { + "general": { + "enable_voice_control": True, + "enable_voice_feedback": True, + "language": "zh-CN", + "wake_word": "小艾", + "enable_wake_word": True, + "continuous_listening": False, + "silence_timeout": 3.0 + }, + "recognition": { + "language": "zh-CN", + "sample_rate": 16000, + "channels": 1, + "sensitivity": 0.5, + "enable_keyword_spotting": True, + "enable_continuous_recognition": True, + "silence_threshold": 0.01, + "speech_threshold": 0.02, + "max_recording_duration": 10.0 + }, + "synthesis": { + "language": "zh-CN", + "voice": "default", + "speed": 1.0, + "volume": 1.0, + "pitch": 1.0, + "enable_ssml": False + }, + "commands": { + "enable_fuzzy_matching": True, + "fuzzy_threshold": 0.7, + "enable_context_commands": True, + "context_timeout": 30.0, + "enable_chained_commands": True, + "max_chain_length": 5, + "enable_confirmation": True, + "confirmation_threshold": 0.9 + }, + "audio": { + "input_device": "default", + "output_device": "default", + "sample_rate": 22050, + "channels": 1, + "enable_echo_cancellation": True, + "enable_noise_reduction": True, + "enable_auto_gain": True + }, + "nlp": { + "primary_language": "zh-CN", + "enable_intent_recognition": True, + "enable_entity_extraction": True, + "enable_sentiment_analysis": True, + "confidence_threshold": 0.6 + }, + "events": { + "enable_event_queue": True, + "max_queue_size": 100, + "enable_event_filtering": True, + "filter_duplicate_events": True, + "duplicate_time_window": 1.0 + } + } + + # 当前配置 + self.current_config = {} + + # 配置变更监听器 + self.config_listeners = {} + + # 配置备份 + self.config_backup = {} + + # 配置统计 + self.config_stats = { + "loads": 0, + "saves": 0, + "changes": 0, + "errors": 0, + "backups": 0 + } + + # 线程锁 + self.config_lock = threading.RLock() + + # 回调函数 + self.config_callbacks = { + "config_loaded": [], + "config_saved": [], + "config_changed": [], + "config_error": [] + } + + # 时间戳记录 + self.last_load_time = 0.0 + self.last_save_time = 0.0 + self.last_change_time = 0.0 + + print("✓ 语音配置管理器已创建") + + def initialize(self) -> bool: + """ + 初始化配置管理器 + + Returns: + 是否初始化成功 + """ + try: + # 创建配置目录 + config_dir = os.path.dirname(self.config_path) + if not os.path.exists(config_dir): + os.makedirs(config_dir) + + # 加载配置 + self.load_config() + + # 如果配置为空,使用默认配置 + if not self.current_config: + self.current_config = self.default_config.copy() + self.save_config() + + self.initialized = True + print("✓ 语音配置管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 语音配置管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用配置管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 配置管理器未初始化") + return False + + self.enabled = True + print("✓ 语音配置管理器已启用") + return True + + except Exception as e: + print(f"✗ 语音配置管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用配置管理器""" + try: + self.enabled = False + print("✓ 语音配置管理器已禁用") + + except Exception as e: + print(f"✗ 语音配置管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理配置管理器资源""" + try: + self.disable() + self.config_listeners.clear() + self.config_callbacks.clear() + self.initialized = False + print("✓ 语音配置管理器资源已清理") + + except Exception as e: + print(f"✗ 语音配置管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新配置管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + current_time = time.time() + # 配置管理器不需要频繁更新 + pass + + except Exception as e: + print(f"✗ 语音配置管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def load_config(self) -> bool: + """ + 加载配置 + + Returns: + 是否加载成功 + """ + try: + with self.config_lock: + if os.path.exists(self.config_path): + with open(self.config_path, 'r', encoding='utf-8') as f: + self.current_config = json.load(f) + else: + # 使用默认配置 + self.current_config = self.default_config.copy() + + self.config_stats["loads"] += 1 + self.last_load_time = time.time() + + # 触发配置加载回调 + self._trigger_config_callback("config_loaded", { + "config": self.current_config, + "timestamp": self.last_load_time + }) + + print("✓ 配置加载完成") + return True + + except Exception as e: + print(f"✗ 配置加载失败: {e}") + self.config_stats["errors"] += 1 + + # 触发配置错误回调 + self._trigger_config_callback("config_error", { + "error": str(e), + "operation": "load" + }) + + return False + + def save_config(self) -> bool: + """ + 保存配置 + + Returns: + 是否保存成功 + """ + try: + with self.config_lock: + # 创建配置目录 + config_dir = os.path.dirname(self.config_path) + if not os.path.exists(config_dir): + os.makedirs(config_dir) + + # 保存配置 + with open(self.config_path, 'w', encoding='utf-8') as f: + json.dump(self.current_config, f, ensure_ascii=False, indent=2) + + self.config_stats["saves"] += 1 + self.last_save_time = time.time() + + # 触发配置保存回调 + self._trigger_config_callback("config_saved", { + "config": self.current_config, + "timestamp": self.last_save_time + }) + + print("✓ 配置保存完成") + return True + + except Exception as e: + print(f"✗ 配置保存失败: {e}") + self.config_stats["errors"] += 1 + + # 触发配置错误回调 + self._trigger_config_callback("config_error", { + "error": str(e), + "operation": "save" + }) + + return False + + def get_config(self, section: str = None, key: str = None, default: Any = None) -> Any: + """ + 获取配置值 + + Args: + section: 配置节名称 + key: 配置键名称 + default: 默认值 + + Returns: + 配置值 + """ + try: + with self.config_lock: + if section is None: + return self.current_config.copy() + elif key is None: + return self.current_config.get(section, {}).copy() + else: + return self.current_config.get(section, {}).get(key, default) + + except Exception as e: + print(f"✗ 配置获取失败: {e}") + return default + + def set_config(self, section: str, key: str = None, value: Any = None, save: bool = True) -> bool: + """ + 设置配置值 + + Args: + section: 配置节名称 + key: 配置键名称 + value: 配置值 + save: 是否保存到文件 + + Returns: + 是否设置成功 + """ + try: + with self.config_lock: + # 备份当前配置 + self._backup_config() + + if key is None: + # 设置整个节 + if isinstance(value, dict): + self.current_config[section] = value + else: + print(f"✗ 配置值必须是字典类型: {section}") + return False + else: + # 设置特定键值 + if section not in self.current_config: + self.current_config[section] = {} + + old_value = self.current_config[section].get(key) + self.current_config[section][key] = value + + # 触发配置变更回调 + self._trigger_config_callback("config_changed", { + "section": section, + "key": key, + "old_value": old_value, + "new_value": value, + "timestamp": time.time() + }) + + self.config_stats["changes"] += 1 + self.last_change_time = time.time() + + # 保存配置 + if save: + return self.save_config() + else: + return True + + except Exception as e: + print(f"✗ 配置设置失败: {e}") + self.config_stats["errors"] += 1 + + # 触发配置错误回调 + self._trigger_config_callback("config_error", { + "error": str(e), + "operation": "set" + }) + + return False + + def update_config(self, updates: Dict[str, Any], save: bool = True) -> bool: + """ + 批量更新配置 + + Args: + updates: 更新字典 + save: 是否保存到文件 + + Returns: + 是否更新成功 + """ + try: + with self.config_lock: + # 备份当前配置 + self._backup_config() + + old_values = {} + + # 应用更新 + for section, section_data in updates.items(): + if isinstance(section_data, dict): + if section not in self.current_config: + self.current_config[section] = {} + + for key, value in section_data.items(): + old_values[f"{section}.{key}"] = self.current_config[section].get(key) + self.current_config[section][key] = value + else: + old_values[section] = self.current_config.get(section) + self.current_config[section] = section_data + + self.config_stats["changes"] += 1 + self.last_change_time = time.time() + + # 触发配置变更回调 + self._trigger_config_callback("config_changed", { + "updates": updates, + "old_values": old_values, + "timestamp": self.last_change_time + }) + + # 保存配置 + if save: + return self.save_config() + else: + return True + + except Exception as e: + print(f"✗ 配置批量更新失败: {e}") + self.config_stats["errors"] += 1 + + # 触发配置错误回调 + self._trigger_config_callback("config_error", { + "error": str(e), + "operation": "update" + }) + + return False + + def reset_config(self, section: str = None) -> bool: + """ + 重置配置 + + Args: + section: 要重置的配置节,None表示重置所有配置 + + Returns: + 是否重置成功 + """ + try: + with self.config_lock: + # 备份当前配置 + self._backup_config() + + if section is None: + # 重置所有配置 + self.current_config = self.default_config.copy() + elif section in self.default_config: + # 重置特定节 + self.current_config[section] = self.default_config[section].copy() + else: + print(f"✗ 无效的配置节: {section}") + return False + + self.config_stats["changes"] += 1 + self.last_change_time = time.time() + + # 触发配置变更回调 + self._trigger_config_callback("config_changed", { + "operation": "reset", + "section": section, + "timestamp": self.last_change_time + }) + + # 保存配置 + return self.save_config() + + except Exception as e: + print(f"✗ 配置重置失败: {e}") + self.config_stats["errors"] += 1 + + # 触发配置错误回调 + self._trigger_config_callback("config_error", { + "error": str(e), + "operation": "reset" + }) + + return False + + def _backup_config(self): + """备份当前配置""" + try: + self.config_backup = json.loads(json.dumps(self.current_config)) + self.config_stats["backups"] += 1 + print("✓ 配置已备份") + except Exception as e: + print(f"✗ 配置备份失败: {e}") + + def restore_config(self) -> bool: + """ + 恢复配置备份 + + Returns: + 是否恢复成功 + """ + try: + with self.config_lock: + if self.config_backup: + self.current_config = json.loads(json.dumps(self.config_backup)) + self.config_stats["changes"] += 1 + self.last_change_time = time.time() + + # 触发配置变更回调 + self._trigger_config_callback("config_changed", { + "operation": "restore", + "timestamp": self.last_change_time + }) + + # 保存配置 + return self.save_config() + else: + print("✗ 没有可用的配置备份") + return False + + except Exception as e: + print(f"✗ 配置恢复失败: {e}") + self.config_stats["errors"] += 1 + + # 触发配置错误回调 + self._trigger_config_callback("config_error", { + "error": str(e), + "operation": "restore" + }) + + return False + + def validate_config(self) -> bool: + """ + 验证配置 + + Returns: + 配置是否有效 + """ + try: + # 这里可以添加配置验证逻辑 + # 例如:检查必需的配置项是否存在,值是否在有效范围内等 + + # 检查基本结构 + required_sections = ["general", "recognition", "synthesis", "commands"] + for section in required_sections: + if section not in self.current_config: + print(f"✗ 缺少必需的配置节: {section}") + return False + + # 检查关键配置值范围 + general = self.current_config.get("general", {}) + if "language" in general and general["language"] not in ["zh-CN", "en-US", "ja-JP"]: + print("✗ 无效的语言设置") + return False + + recognition = self.current_config.get("recognition", {}) + if "sample_rate" in recognition and recognition["sample_rate"] not in [8000, 16000, 22050, 44100, 48000]: + print("✗ 无效的采样率设置") + return False + + synthesis = self.current_config.get("synthesis", {}) + if "speed" in synthesis and not (0.5 <= synthesis["speed"] <= 2.0): + print("✗ 语速设置超出有效范围 (0.5-2.0)") + return False + + if "volume" in synthesis and not (0.0 <= synthesis["volume"] <= 1.0): + print("✗ 音量设置超出有效范围 (0.0-1.0)") + return False + + print("✓ 配置验证通过") + return True + + except Exception as e: + print(f"✗ 配置验证失败: {e}") + return False + + def export_config(self, file_path: str) -> bool: + """ + 导出配置到文件 + + Args: + file_path: 导出文件路径 + + Returns: + 是否导出成功 + """ + try: + with self.config_lock: + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(self.current_config, f, ensure_ascii=False, indent=2) + + print(f"✓ 配置已导出到: {file_path}") + return True + + except Exception as e: + print(f"✗ 配置导出失败: {e}") + return False + + def import_config(self, file_path: str) -> bool: + """ + 从文件导入配置 + + Args: + file_path: 导入文件路径 + + Returns: + 是否导入成功 + """ + try: + with self.config_lock: + # 备份当前配置 + self._backup_config() + + with open(file_path, 'r', encoding='utf-8') as f: + imported_config = json.load(f) + + # 验证导入的配置 + temp_manager = VoiceConfigManager(self.plugin) + temp_manager.current_config = imported_config + if not temp_manager.validate_config(): + print("✗ 导入的配置无效") + return False + + # 应用导入的配置 + self.current_config = imported_config + self.config_stats["changes"] += 1 + self.last_change_time = time.time() + + # 触发配置变更回调 + self._trigger_config_callback("config_changed", { + "operation": "import", + "source": file_path, + "timestamp": self.last_change_time + }) + + # 保存配置 + return self.save_config() + + except Exception as e: + print(f"✗ 配置导入失败: {e}") + self.config_stats["errors"] += 1 + + # 触发配置错误回调 + self._trigger_config_callback("config_error", { + "error": str(e), + "operation": "import" + }) + + return False + + def get_default_config(self) -> Dict[str, Any]: + """ + 获取默认配置 + + Returns: + 默认配置字典 + """ + return self.default_config.copy() + + def get_config_stats(self) -> Dict[str, int]: + """ + 获取配置统计信息 + + Returns: + 统计信息字典 + """ + return self.config_stats.copy() + + def reset_config_stats(self): + """重置配置统计信息""" + try: + self.config_stats = { + "loads": 0, + "saves": 0, + "changes": 0, + "errors": 0, + "backups": 0 + } + print("✓ 配置统计信息已重置") + except Exception as e: + print(f"✗ 配置统计信息重置失败: {e}") + + def register_config_listener(self, section: str, key: str, listener: callable) -> bool: + """ + 注册配置监听器 + + Args: + section: 配置节名称 + key: 配置键名称 + listener: 监听器函数 + + Returns: + 是否注册成功 + """ + try: + listener_key = f"{section}.{key}" + if listener_key not in self.config_listeners: + self.config_listeners[listener_key] = [] + + self.config_listeners[listener_key].append(listener) + print(f"✓ 配置监听器已注册: {listener_key}") + return True + + except Exception as e: + print(f"✗ 配置监听器注册失败: {e}") + return False + + def unregister_config_listener(self, section: str, key: str, listener: callable) -> bool: + """ + 注销配置监听器 + + Args: + section: 配置节名称 + key: 配置键名称 + listener: 监听器函数 + + Returns: + 是否注销成功 + """ + try: + listener_key = f"{section}.{key}" + if listener_key in self.config_listeners: + if listener in self.config_listeners[listener_key]: + self.config_listeners[listener_key].remove(listener) + print(f"✓ 配置监听器已注销: {listener_key}") + return True + + print(f"✗ 配置监听器不存在: {listener_key}") + return False + + except Exception as e: + print(f"✗ 配置监听器注销失败: {e}") + return False + + def _trigger_config_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发配置回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.config_callbacks: + for callback in self.config_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 配置回调执行失败: {e}") + except Exception as e: + print(f"✗ 配置回调触发失败: {e}") + + def register_config_callback(self, callback_type: str, callback: callable): + """ + 注册配置回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.config_callbacks: + self.config_callbacks[callback_type].append(callback) + print(f"✓ 配置回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 配置回调注册失败: {e}") + + def unregister_config_callback(self, callback_type: str, callback: callable): + """ + 注销配置回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.config_callbacks: + if callback in self.config_callbacks[callback_type]: + self.config_callbacks[callback_type].remove(callback) + print(f"✓ 配置回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 配置回调注销失败: {e}") + + def apply_config_to_modules(self): + """ + 将配置应用到各个模块 + """ + try: + if not self.enabled: + return + + # 应用配置到语音管理器 + if self.plugin.voice_manager: + general_config = self.get_config("general") + self.plugin.voice_manager.set_voice_config(general_config) + + # 应用配置到语音识别器 + if self.plugin.speech_recognizer: + recognition_config = self.get_config("recognition") + self.plugin.speech_recognizer.set_recognition_config(recognition_config) + + # 应用配置到语音合成器 + if self.plugin.speech_synthesizer: + synthesis_config = self.get_config("synthesis") + self.plugin.speech_synthesizer.set_synthesis_config(synthesis_config) + + # 应用配置到命令处理器 + if self.plugin.command_processor: + commands_config = self.get_config("commands") + self.plugin.command_processor.set_command_config(commands_config) + + # 应用配置到音频管理器 + if self.plugin.audio_manager: + audio_config = self.get_config("audio") + self.plugin.audio_manager.set_audio_config(audio_config) + + # 应用配置到NLP处理器 + if self.plugin.nlp_processor: + nlp_config = self.get_config("nlp") + self.plugin.nlp_processor.set_nlp_config(nlp_config) + + # 应用配置到事件处理器 + if self.plugin.event_handler: + events_config = self.get_config("events") + self.plugin.event_handler.set_event_config(events_config) + + print("✓ 配置已应用到所有模块") + + except Exception as e: + print(f"✗ 配置应用失败: {e}") + import traceback traceback.print_exc() \ No newline at end of file diff --git a/plugins/user/voice_control/core/voice_manager.py b/plugins/user/voice_control/core/voice_manager.py index ea5b4d78..c774e501 100644 --- a/plugins/user/voice_control/core/voice_manager.py +++ b/plugins/user/voice_control/core/voice_manager.py @@ -1,659 +1,659 @@ -""" -语音管理器模块 -负责管理语音识别和合成的核心功能 -""" - -import time -from typing import Dict, Any, List, Optional -import threading - -class VoiceManager: - """ - 语音管理器 - 负责管理语音识别和合成的核心功能 - """ - - def __init__(self, plugin): - """ - 初始化语音管理器 - - Args: - plugin: 语音控制插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 语音识别状态 - self.recognition_state = { - 'is_listening': False, - 'is_processing': False, - 'last_recognition_time': 0.0, - 'current_text': '', - 'confidence': 0.0 - } - - # 语音合成状态 - self.synthesis_state = { - 'is_speaking': False, - 'is_processing': False, - 'last_synthesis_time': 0.0, - 'current_text': '', - 'progress': 0.0 - } - - # 配置参数 - self.voice_config = { - 'enable_voice_control': True, - 'enable_voice_feedback': True, - 'recognition_language': 'zh-CN', - 'synthesis_language': 'zh-CN', - 'synthesis_voice': 'default', - 'recognition_sensitivity': 0.5, - 'synthesis_speed': 1.0, - 'synthesis_volume': 1.0, - 'enable_wake_word': True, - 'wake_word': '小艾', - 'enable_continuous_listening': True, - 'silence_timeout': 3.0 - } - - # 命令历史 - self.command_history = [] - self.max_history_size = 100 - - # 关键词列表 - self.keywords = [] - - # 统计信息 - self.stats = { - 'commands_processed': 0, - 'speech_recognized': 0, - 'speech_synthesized': 0, - 'recognition_errors': 0, - 'synthesis_errors': 0, - 'wake_word_detected': 0 - } - - # 线程管理 - self.update_thread = None - self.running = False - - # 回调函数 - self.voice_callbacks = { - 'speech_recognized': [], - 'speech_synthesis_started': [], - 'speech_synthesis_completed': [], - 'command_processed': [], - 'wake_word_detected': [] - } - - # 时间戳记录 - self.last_update_time = 0.0 - self.last_wake_word_time = 0.0 - - print("✓ 语音管理器已创建") - - def initialize(self) -> bool: - """ - 初始化语音管理器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化关键词列表 - self._initialize_keywords() - - self.initialized = True - print("✓ 语音管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 语音管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用语音管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 语音管理器未初始化") - return False - - self.enabled = True - self.running = True - - # 启动更新线程 - self.update_thread = threading.Thread(target=self._update_loop, daemon=True) - self.update_thread.start() - - print("✓ 语音管理器已启用") - return True - - except Exception as e: - print(f"✗ 语音管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用语音管理器""" - try: - self.enabled = False - self.running = False - - # 等待更新线程结束 - if self.update_thread and self.update_thread.is_alive(): - self.update_thread.join(timeout=1.0) - - print("✓ 语音管理器已禁用") - - except Exception as e: - print(f"✗ 语音管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理语音管理器资源""" - try: - self.disable() - self.voice_callbacks.clear() - self.command_history.clear() - self.keywords.clear() - self.initialized = False - print("✓ 语音管理器资源已清理") - - except Exception as e: - print(f"✗ 语音管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新语音管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.last_update_time = current_time - - except Exception as e: - print(f"✗ 语音管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _initialize_keywords(self): - """初始化关键词列表""" - try: - # 添加默认关键词 - default_keywords = [ - '前进', '后退', '左转', '右转', '停止', - '开始', '结束', '暂停', '继续', '取消', - '打开', '关闭', '增加', '减少', '设置', - '帮助', '关于', '退出', '返回', '确认' - ] - - self.keywords.extend(default_keywords) - print("✓ 关键词列表初始化完成") - except Exception as e: - print(f"✗ 关键词列表初始化失败: {e}") - - def _update_loop(self): - """更新循环线程""" - try: - update_interval = 1.0 / 30.0 # 30Hz - - while self.running: - if self.enabled: - self._update_voice_state() - - time.sleep(update_interval) - - except Exception as e: - print(f"✗ 语音管理器更新循环失败: {e}") - - def _update_voice_state(self): - """更新语音状态""" - try: - current_time = time.time() - - # 检查静默超时 - if self.recognition_state['is_listening']: - if current_time - self.recognition_state['last_recognition_time'] > self.voice_config['silence_timeout']: - self.recognition_state['is_listening'] = False - - except Exception as e: - print(f"✗ 语音状态更新失败: {e}") - - def start_listening(self) -> bool: - """ - 开始语音识别监听 - - Returns: - 是否开始成功 - """ - try: - if not self.enabled: - print("✗ 语音管理器未启用") - return False - - self.recognition_state['is_listening'] = True - self.recognition_state['last_recognition_time'] = time.time() - - print("✓ 语音识别监听已开始") - return True - - except Exception as e: - print(f"✗ 语音识别监听开始失败: {e}") - self.stats['recognition_errors'] += 1 - return False - - def stop_listening(self) -> bool: - """ - 停止语音识别监听 - - Returns: - 是否停止成功 - """ - try: - self.recognition_state['is_listening'] = False - print("✓ 语音识别监听已停止") - return True - - except Exception as e: - print(f"✗ 语音识别监听停止失败: {e}") - return False - - def process_speech(self, text: str, confidence: float = 1.0) -> bool: - """ - 处理识别到的语音文本 - - Args: - text: 识别到的文本 - confidence: 置信度(0.0-1.0) - - Returns: - 是否处理成功 - """ - try: - if not self.enabled: - print("✗ 语音管理器未启用") - return False - - # 更新识别状态 - self.recognition_state['current_text'] = text - self.recognition_state['confidence'] = confidence - self.recognition_state['last_recognition_time'] = time.time() - - # 检查唤醒词 - if self.voice_config['enable_wake_word']: - if self._check_wake_word(text): - self._trigger_voice_callback('wake_word_detected', { - 'text': text, - 'confidence': confidence - }) - self.stats['wake_word_detected'] += 1 - self.last_wake_word_time = time.time() - - # 如果启用了连续监听,开始处理命令 - if self.voice_config['enable_continuous_listening']: - # 移除唤醒词并处理剩余文本 - command_text = self._remove_wake_word(text) - return self._process_command(command_text, confidence) - return True - - # 处理命令 - if not self.voice_config['enable_wake_word'] or \ - (time.time() - self.last_wake_word_time) < 5.0: # 5秒内有效 - return self._process_command(text, confidence) - - self.stats['speech_recognized'] += 1 - return True - - except Exception as e: - print(f"✗ 语音文本处理失败: {e}") - self.stats['recognition_errors'] += 1 - return False - - def _check_wake_word(self, text: str) -> bool: - """ - 检查是否包含唤醒词 - - Args: - text: 识别到的文本 - - Returns: - 是否包含唤醒词 - """ - try: - wake_word = self.voice_config['wake_word'] - return wake_word in text - except Exception as e: - print(f"✗ 唤醒词检查失败: {e}") - return False - - def _remove_wake_word(self, text: str) -> str: - """ - 移除唤醒词 - - Args: - text: 包含唤醒词的文本 - - Returns: - 移除唤醒词后的文本 - """ - try: - wake_word = self.voice_config['wake_word'] - return text.replace(wake_word, '').strip() - except Exception as e: - print(f"✗ 唤醒词移除失败: {e}") - return text - - def _process_command(self, text: str, confidence: float) -> bool: - """ - 处理语音命令 - - Args: - text: 命令文本 - confidence: 置信度 - - Returns: - 是否处理成功 - """ - try: - # 添加到命令历史 - command_entry = { - 'text': text, - 'confidence': confidence, - 'timestamp': time.time() - } - - self.command_history.append(command_entry) - - # 保持历史记录大小 - if len(self.command_history) > self.max_history_size: - self.command_history.pop(0) - - # 触发命令处理回调 - self._trigger_voice_callback('command_processed', { - 'text': text, - 'confidence': confidence, - 'command_entry': command_entry - }) - - # 交给命令处理器处理 - if self.plugin.command_processor: - self.plugin.command_processor.process_command(text, confidence) - - self.stats['commands_processed'] += 1 - return True - - except Exception as e: - print(f"✗ 命令处理失败: {e}") - return False - - def speak_text(self, text: str, blocking: bool = False) -> bool: - """ - 语音合成播放文本 - - Args: - text: 要合成的文本 - blocking: 是否阻塞等待播放完成 - - Returns: - 是否播放成功 - """ - try: - if not self.enabled: - print("✗ 语音管理器未启用") - return False - - if not self.voice_config['enable_voice_feedback']: - print("✗ 语音反馈未启用") - return False - - # 更新合成状态 - self.synthesis_state['is_speaking'] = True - self.synthesis_state['is_processing'] = True - self.synthesis_state['current_text'] = text - self.synthesis_state['last_synthesis_time'] = time.time() - - # 触发合成开始回调 - self._trigger_voice_callback('speech_synthesis_started', { - 'text': text - }) - - # 交给语音合成器处理 - if self.plugin.speech_synthesizer: - result = self.plugin.speech_synthesizer.synthesize_speech( - text, blocking, - self.voice_config['synthesis_speed'], - self.voice_config['synthesis_volume'] - ) - - if result: - self.stats['speech_synthesized'] += 1 - return True - else: - self.stats['synthesis_errors'] += 1 - return False - else: - print("✗ 语音合成器未初始化") - self.stats['synthesis_errors'] += 1 - return False - - except Exception as e: - print(f"✗ 文本语音合成失败: {e}") - self.stats['synthesis_errors'] += 1 - return False - finally: - # 更新合成状态 - self.synthesis_state['is_speaking'] = False - self.synthesis_state['is_processing'] = False - - # 触发合成完成回调 - self._trigger_voice_callback('speech_synthesis_completed', { - 'text': text - }) - - def add_keyword(self, keyword: str) -> bool: - """ - 添加关键词 - - Args: - keyword: 关键词 - - Returns: - 是否添加成功 - """ - try: - if keyword not in self.keywords: - self.keywords.append(keyword) - print(f"✓ 关键词已添加: {keyword}") - return True - else: - print(f"⚠ 关键词已存在: {keyword}") - return True - except Exception as e: - print(f"✗ 关键词添加失败: {e}") - return False - - def remove_keyword(self, keyword: str) -> bool: - """ - 移除关键词 - - Args: - keyword: 关键词 - - Returns: - 是否移除成功 - """ - try: - if keyword in self.keywords: - self.keywords.remove(keyword) - print(f"✓ 关键词已移除: {keyword}") - return True - else: - print(f"⚠ 关键词不存在: {keyword}") - return False - except Exception as e: - print(f"✗ 关键词移除失败: {e}") - return False - - def get_keywords(self) -> List[str]: - """ - 获取关键词列表 - - Returns: - 关键词列表 - """ - return self.keywords.copy() - - def set_voice_config(self, config: Dict[str, Any]) -> bool: - """ - 设置语音配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.voice_config.update(config) - print(f"✓ 语音配置已更新: {self.voice_config}") - return True - except Exception as e: - print(f"✗ 语音配置设置失败: {e}") - return False - - def get_voice_config(self) -> Dict[str, Any]: - """ - 获取语音配置 - - Returns: - 配置字典 - """ - return self.voice_config.copy() - - def get_recognition_state(self) -> Dict[str, Any]: - """ - 获取识别状态 - - Returns: - 识别状态字典 - """ - return self.recognition_state.copy() - - def get_synthesis_state(self) -> Dict[str, Any]: - """ - 获取合成状态 - - Returns: - 合成状态字典 - """ - return self.synthesis_state.copy() - - def get_command_history(self) -> List[Dict[str, Any]]: - """ - 获取命令历史 - - Returns: - 命令历史列表 - """ - return self.command_history.copy() - - def clear_command_history(self): - """清空命令历史""" - try: - self.command_history.clear() - print("✓ 命令历史已清空") - except Exception as e: - print(f"✗ 命令历史清空失败: {e}") - - def _trigger_voice_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发语音回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.voice_callbacks: - for callback in self.voice_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 语音回调执行失败: {e}") - except Exception as e: - print(f"✗ 语音回调触发失败: {e}") - - def register_voice_callback(self, callback_type: str, callback: callable): - """ - 注册语音回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.voice_callbacks: - self.voice_callbacks[callback_type].append(callback) - print(f"✓ 语音回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 语音回调注册失败: {e}") - - def unregister_voice_callback(self, callback_type: str, callback: callable): - """ - 注销语音回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.voice_callbacks: - if callback in self.voice_callbacks[callback_type]: - self.voice_callbacks[callback_type].remove(callback) - print(f"✓ 语音回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 语音回调注销失败: {e}") - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - try: - self.stats = { - 'commands_processed': 0, - 'speech_recognized': 0, - 'speech_synthesized': 0, - 'recognition_errors': 0, - 'synthesis_errors': 0, - 'wake_word_detected': 0 - } - print("✓ 语音管理器统计信息已重置") - except Exception as e: +""" +语音管理器模块 +负责管理语音识别和合成的核心功能 +""" + +import time +from typing import Dict, Any, List, Optional +import threading + +class VoiceManager: + """ + 语音管理器 + 负责管理语音识别和合成的核心功能 + """ + + def __init__(self, plugin): + """ + 初始化语音管理器 + + Args: + plugin: 语音控制插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 语音识别状态 + self.recognition_state = { + 'is_listening': False, + 'is_processing': False, + 'last_recognition_time': 0.0, + 'current_text': '', + 'confidence': 0.0 + } + + # 语音合成状态 + self.synthesis_state = { + 'is_speaking': False, + 'is_processing': False, + 'last_synthesis_time': 0.0, + 'current_text': '', + 'progress': 0.0 + } + + # 配置参数 + self.voice_config = { + 'enable_voice_control': True, + 'enable_voice_feedback': True, + 'recognition_language': 'zh-CN', + 'synthesis_language': 'zh-CN', + 'synthesis_voice': 'default', + 'recognition_sensitivity': 0.5, + 'synthesis_speed': 1.0, + 'synthesis_volume': 1.0, + 'enable_wake_word': True, + 'wake_word': '小艾', + 'enable_continuous_listening': True, + 'silence_timeout': 3.0 + } + + # 命令历史 + self.command_history = [] + self.max_history_size = 100 + + # 关键词列表 + self.keywords = [] + + # 统计信息 + self.stats = { + 'commands_processed': 0, + 'speech_recognized': 0, + 'speech_synthesized': 0, + 'recognition_errors': 0, + 'synthesis_errors': 0, + 'wake_word_detected': 0 + } + + # 线程管理 + self.update_thread = None + self.running = False + + # 回调函数 + self.voice_callbacks = { + 'speech_recognized': [], + 'speech_synthesis_started': [], + 'speech_synthesis_completed': [], + 'command_processed': [], + 'wake_word_detected': [] + } + + # 时间戳记录 + self.last_update_time = 0.0 + self.last_wake_word_time = 0.0 + + print("✓ 语音管理器已创建") + + def initialize(self) -> bool: + """ + 初始化语音管理器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化关键词列表 + self._initialize_keywords() + + self.initialized = True + print("✓ 语音管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 语音管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用语音管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 语音管理器未初始化") + return False + + self.enabled = True + self.running = True + + # 启动更新线程 + self.update_thread = threading.Thread(target=self._update_loop, daemon=True) + self.update_thread.start() + + print("✓ 语音管理器已启用") + return True + + except Exception as e: + print(f"✗ 语音管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用语音管理器""" + try: + self.enabled = False + self.running = False + + # 等待更新线程结束 + if self.update_thread and self.update_thread.is_alive(): + self.update_thread.join(timeout=1.0) + + print("✓ 语音管理器已禁用") + + except Exception as e: + print(f"✗ 语音管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理语音管理器资源""" + try: + self.disable() + self.voice_callbacks.clear() + self.command_history.clear() + self.keywords.clear() + self.initialized = False + print("✓ 语音管理器资源已清理") + + except Exception as e: + print(f"✗ 语音管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新语音管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.last_update_time = current_time + + except Exception as e: + print(f"✗ 语音管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _initialize_keywords(self): + """初始化关键词列表""" + try: + # 添加默认关键词 + default_keywords = [ + '前进', '后退', '左转', '右转', '停止', + '开始', '结束', '暂停', '继续', '取消', + '打开', '关闭', '增加', '减少', '设置', + '帮助', '关于', '退出', '返回', '确认' + ] + + self.keywords.extend(default_keywords) + print("✓ 关键词列表初始化完成") + except Exception as e: + print(f"✗ 关键词列表初始化失败: {e}") + + def _update_loop(self): + """更新循环线程""" + try: + update_interval = 1.0 / 30.0 # 30Hz + + while self.running: + if self.enabled: + self._update_voice_state() + + time.sleep(update_interval) + + except Exception as e: + print(f"✗ 语音管理器更新循环失败: {e}") + + def _update_voice_state(self): + """更新语音状态""" + try: + current_time = time.time() + + # 检查静默超时 + if self.recognition_state['is_listening']: + if current_time - self.recognition_state['last_recognition_time'] > self.voice_config['silence_timeout']: + self.recognition_state['is_listening'] = False + + except Exception as e: + print(f"✗ 语音状态更新失败: {e}") + + def start_listening(self) -> bool: + """ + 开始语音识别监听 + + Returns: + 是否开始成功 + """ + try: + if not self.enabled: + print("✗ 语音管理器未启用") + return False + + self.recognition_state['is_listening'] = True + self.recognition_state['last_recognition_time'] = time.time() + + print("✓ 语音识别监听已开始") + return True + + except Exception as e: + print(f"✗ 语音识别监听开始失败: {e}") + self.stats['recognition_errors'] += 1 + return False + + def stop_listening(self) -> bool: + """ + 停止语音识别监听 + + Returns: + 是否停止成功 + """ + try: + self.recognition_state['is_listening'] = False + print("✓ 语音识别监听已停止") + return True + + except Exception as e: + print(f"✗ 语音识别监听停止失败: {e}") + return False + + def process_speech(self, text: str, confidence: float = 1.0) -> bool: + """ + 处理识别到的语音文本 + + Args: + text: 识别到的文本 + confidence: 置信度(0.0-1.0) + + Returns: + 是否处理成功 + """ + try: + if not self.enabled: + print("✗ 语音管理器未启用") + return False + + # 更新识别状态 + self.recognition_state['current_text'] = text + self.recognition_state['confidence'] = confidence + self.recognition_state['last_recognition_time'] = time.time() + + # 检查唤醒词 + if self.voice_config['enable_wake_word']: + if self._check_wake_word(text): + self._trigger_voice_callback('wake_word_detected', { + 'text': text, + 'confidence': confidence + }) + self.stats['wake_word_detected'] += 1 + self.last_wake_word_time = time.time() + + # 如果启用了连续监听,开始处理命令 + if self.voice_config['enable_continuous_listening']: + # 移除唤醒词并处理剩余文本 + command_text = self._remove_wake_word(text) + return self._process_command(command_text, confidence) + return True + + # 处理命令 + if not self.voice_config['enable_wake_word'] or \ + (time.time() - self.last_wake_word_time) < 5.0: # 5秒内有效 + return self._process_command(text, confidence) + + self.stats['speech_recognized'] += 1 + return True + + except Exception as e: + print(f"✗ 语音文本处理失败: {e}") + self.stats['recognition_errors'] += 1 + return False + + def _check_wake_word(self, text: str) -> bool: + """ + 检查是否包含唤醒词 + + Args: + text: 识别到的文本 + + Returns: + 是否包含唤醒词 + """ + try: + wake_word = self.voice_config['wake_word'] + return wake_word in text + except Exception as e: + print(f"✗ 唤醒词检查失败: {e}") + return False + + def _remove_wake_word(self, text: str) -> str: + """ + 移除唤醒词 + + Args: + text: 包含唤醒词的文本 + + Returns: + 移除唤醒词后的文本 + """ + try: + wake_word = self.voice_config['wake_word'] + return text.replace(wake_word, '').strip() + except Exception as e: + print(f"✗ 唤醒词移除失败: {e}") + return text + + def _process_command(self, text: str, confidence: float) -> bool: + """ + 处理语音命令 + + Args: + text: 命令文本 + confidence: 置信度 + + Returns: + 是否处理成功 + """ + try: + # 添加到命令历史 + command_entry = { + 'text': text, + 'confidence': confidence, + 'timestamp': time.time() + } + + self.command_history.append(command_entry) + + # 保持历史记录大小 + if len(self.command_history) > self.max_history_size: + self.command_history.pop(0) + + # 触发命令处理回调 + self._trigger_voice_callback('command_processed', { + 'text': text, + 'confidence': confidence, + 'command_entry': command_entry + }) + + # 交给命令处理器处理 + if self.plugin.command_processor: + self.plugin.command_processor.process_command(text, confidence) + + self.stats['commands_processed'] += 1 + return True + + except Exception as e: + print(f"✗ 命令处理失败: {e}") + return False + + def speak_text(self, text: str, blocking: bool = False) -> bool: + """ + 语音合成播放文本 + + Args: + text: 要合成的文本 + blocking: 是否阻塞等待播放完成 + + Returns: + 是否播放成功 + """ + try: + if not self.enabled: + print("✗ 语音管理器未启用") + return False + + if not self.voice_config['enable_voice_feedback']: + print("✗ 语音反馈未启用") + return False + + # 更新合成状态 + self.synthesis_state['is_speaking'] = True + self.synthesis_state['is_processing'] = True + self.synthesis_state['current_text'] = text + self.synthesis_state['last_synthesis_time'] = time.time() + + # 触发合成开始回调 + self._trigger_voice_callback('speech_synthesis_started', { + 'text': text + }) + + # 交给语音合成器处理 + if self.plugin.speech_synthesizer: + result = self.plugin.speech_synthesizer.synthesize_speech( + text, blocking, + self.voice_config['synthesis_speed'], + self.voice_config['synthesis_volume'] + ) + + if result: + self.stats['speech_synthesized'] += 1 + return True + else: + self.stats['synthesis_errors'] += 1 + return False + else: + print("✗ 语音合成器未初始化") + self.stats['synthesis_errors'] += 1 + return False + + except Exception as e: + print(f"✗ 文本语音合成失败: {e}") + self.stats['synthesis_errors'] += 1 + return False + finally: + # 更新合成状态 + self.synthesis_state['is_speaking'] = False + self.synthesis_state['is_processing'] = False + + # 触发合成完成回调 + self._trigger_voice_callback('speech_synthesis_completed', { + 'text': text + }) + + def add_keyword(self, keyword: str) -> bool: + """ + 添加关键词 + + Args: + keyword: 关键词 + + Returns: + 是否添加成功 + """ + try: + if keyword not in self.keywords: + self.keywords.append(keyword) + print(f"✓ 关键词已添加: {keyword}") + return True + else: + print(f"⚠ 关键词已存在: {keyword}") + return True + except Exception as e: + print(f"✗ 关键词添加失败: {e}") + return False + + def remove_keyword(self, keyword: str) -> bool: + """ + 移除关键词 + + Args: + keyword: 关键词 + + Returns: + 是否移除成功 + """ + try: + if keyword in self.keywords: + self.keywords.remove(keyword) + print(f"✓ 关键词已移除: {keyword}") + return True + else: + print(f"⚠ 关键词不存在: {keyword}") + return False + except Exception as e: + print(f"✗ 关键词移除失败: {e}") + return False + + def get_keywords(self) -> List[str]: + """ + 获取关键词列表 + + Returns: + 关键词列表 + """ + return self.keywords.copy() + + def set_voice_config(self, config: Dict[str, Any]) -> bool: + """ + 设置语音配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.voice_config.update(config) + print(f"✓ 语音配置已更新: {self.voice_config}") + return True + except Exception as e: + print(f"✗ 语音配置设置失败: {e}") + return False + + def get_voice_config(self) -> Dict[str, Any]: + """ + 获取语音配置 + + Returns: + 配置字典 + """ + return self.voice_config.copy() + + def get_recognition_state(self) -> Dict[str, Any]: + """ + 获取识别状态 + + Returns: + 识别状态字典 + """ + return self.recognition_state.copy() + + def get_synthesis_state(self) -> Dict[str, Any]: + """ + 获取合成状态 + + Returns: + 合成状态字典 + """ + return self.synthesis_state.copy() + + def get_command_history(self) -> List[Dict[str, Any]]: + """ + 获取命令历史 + + Returns: + 命令历史列表 + """ + return self.command_history.copy() + + def clear_command_history(self): + """清空命令历史""" + try: + self.command_history.clear() + print("✓ 命令历史已清空") + except Exception as e: + print(f"✗ 命令历史清空失败: {e}") + + def _trigger_voice_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发语音回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.voice_callbacks: + for callback in self.voice_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 语音回调执行失败: {e}") + except Exception as e: + print(f"✗ 语音回调触发失败: {e}") + + def register_voice_callback(self, callback_type: str, callback: callable): + """ + 注册语音回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.voice_callbacks: + self.voice_callbacks[callback_type].append(callback) + print(f"✓ 语音回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 语音回调注册失败: {e}") + + def unregister_voice_callback(self, callback_type: str, callback: callable): + """ + 注销语音回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.voice_callbacks: + if callback in self.voice_callbacks[callback_type]: + self.voice_callbacks[callback_type].remove(callback) + print(f"✓ 语音回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 语音回调注销失败: {e}") + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + try: + self.stats = { + 'commands_processed': 0, + 'speech_recognized': 0, + 'speech_synthesized': 0, + 'recognition_errors': 0, + 'synthesis_errors': 0, + 'wake_word_detected': 0 + } + print("✓ 语音管理器统计信息已重置") + except Exception as e: print(f"✗ 语音管理器统计信息重置失败: {e}") \ No newline at end of file diff --git a/plugins/user/voice_control/editor/voice_editor.py b/plugins/user/voice_control/editor/voice_editor.py index 0f4ca05b..42e5ed00 100644 --- a/plugins/user/voice_control/editor/voice_editor.py +++ b/plugins/user/voice_control/editor/voice_editor.py @@ -1,1227 +1,1227 @@ -""" -语音编辑器模块 -提供可视化界面用于配置和测试语音控制功能 -""" - -import time -from typing import Dict, Any, List, Optional -import threading - -class EditorMode: - """编辑器模式枚举""" - CONFIGURATION = "configuration" - COMMAND_MAPPING = "command_mapping" - VOICE_TRAINING = "voice_training" - TESTING = "testing" - MONITORING = "monitoring" - PRESETS = "presets" - -class VoiceEditor: - """ - 语音编辑器 - 提供可视化界面用于配置和测试语音控制功能 - """ - - def __init__(self, plugin): - """ - 初始化语音编辑器 - - Args: - plugin: 语音控制插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 编辑器配置 - self.editor_config = { - "window_width": 1024, - "window_height": 768, - "refresh_rate": 30.0, - "enable_preview": True, - "enable_realtime_update": True, - "show_debug_info": False, - "theme": "dark" - } - - # 当前编辑器状态 - self.editor_state = { - "current_mode": EditorMode.CONFIGURATION, - "selected_tab": "general", - "is_recording": False, - "is_testing": False, - "preview_active": False, - "monitoring_active": True - } - - # UI组件 - self.ui_components = { - "main_window": None, - "toolbar": None, - "property_panel": None, - "preview_panel": None, - "status_bar": None, - "config_view": None, - "command_view": None, - "training_view": None, - "test_view": None, - "monitor_view": None, - "preset_view": None - } - - # 编辑器数据 - self.editor_data = { - "config_cache": {}, - "command_cache": {}, - "preset_cache": {}, - "live_data": {}, - "test_history": [] - } - - # 编辑器操作历史 - self.operation_history = [] - self.history_pointer = -1 - self.max_history_size = 100 - - # 实时监控数据 - self.monitor_data = { - "audio_levels": [], - "recognition_results": [], - "synthesis_events": [], - "command_executions": [], - "performance_metrics": {} - } - - # 测试数据 - self.test_data = { - "test_cases": [], - "test_results": [], - "current_test": None - } - - # 回调函数 - self.editor_callbacks = { - "mode_changed": [], - "tab_changed": [], - "data_updated": [], - "test_started": [], - "test_completed": [], - "recording_started": [], - "recording_stopped": [] - } - - # 统计信息 - self.editor_stats = { - "windows_opened": 0, - "operations_performed": 0, - "undo_operations": 0, - "redo_operations": 0, - "data_refreshed": 0, - "preview_updates": 0, - "tests_run": 0 - } - - # 线程管理 - self.editor_thread = None - self.monitor_thread = None - self.running = False - - # 时间戳记录 - self.last_refresh_time = 0.0 - self.last_preview_update = 0.0 - self.last_monitor_update = 0.0 - - print("✓ 语音编辑器已创建") - - def initialize(self) -> bool: - """ - 初始化语音编辑器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化UI组件(模拟) - self._initialize_ui_components() - - # 加载缓存数据 - self._load_editor_cache() - - self.initialized = True - print("✓ 语音编辑器初始化完成") - return True - - except Exception as e: - print(f"✗ 语音编辑器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用语音编辑器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 语音编辑器未初始化") - return False - - self.enabled = True - self.running = True - - # 启动编辑器线程 - self.editor_thread = threading.Thread(target=self._editor_update_loop, daemon=True) - self.editor_thread.start() - - # 启动监控线程 - if self.editor_config["enable_realtime_update"]: - self.monitor_thread = threading.Thread(target=self._monitor_update_loop, daemon=True) - self.monitor_thread.start() - - print("✓ 语音编辑器已启用") - return True - - except Exception as e: - print(f"✗ 语音编辑器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用语音编辑器""" - try: - self.enabled = False - self.running = False - - # 等待线程结束 - if self.editor_thread and self.editor_thread.is_alive(): - self.editor_thread.join(timeout=2.0) - - if self.monitor_thread and self.monitor_thread.is_alive(): - self.monitor_thread.join(timeout=2.0) - - # 关闭UI组件 - self._close_ui_components() - - print("✓ 语音编辑器已禁用") - - except Exception as e: - print(f"✗ 语音编辑器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理语音编辑器资源""" - try: - self.disable() - self.ui_components.clear() - self.editor_data.clear() - self.operation_history.clear() - self.monitor_data.clear() - self.test_data.clear() - self.editor_callbacks.clear() - self.initialized = False - print("✓ 语音编辑器资源已清理") - - except Exception as e: - print(f"✗ 语音编辑器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新语音编辑器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新统计信息 - pass - - except Exception as e: - print(f"✗ 语音编辑器更新失败: {e}") - import traceback - traceback.print_exc() - - def _editor_update_loop(self): - """编辑器更新循环""" - try: - refresh_interval = 1.0 / self.editor_config["refresh_rate"] - - while self.running: - if self.enabled: - self._update_editor_display() - - time.sleep(refresh_interval) - - except Exception as e: - print(f"✗ 编辑器更新循环失败: {e}") - - def _monitor_update_loop(self): - """监控更新循环""" - try: - monitor_interval = 0.1 # 100ms - - while self.running: - if self.enabled and self.editor_config["enable_realtime_update"]: - self._update_monitor_data() - - time.sleep(monitor_interval) - - except Exception as e: - print(f"✗ 监控更新循环失败: {e}") - - def _initialize_ui_components(self): - """初始化UI组件""" - try: - # 在实际实现中,这里会创建实际的UI组件 - # 由于这是模拟实现,我们只创建占位符 - self.ui_components["main_window"] = "MainWindow" - self.ui_components["toolbar"] = "Toolbar" - self.ui_components["property_panel"] = "PropertyPanel" - self.ui_components["preview_panel"] = "PreviewPanel" - self.ui_components["status_bar"] = "StatusBar" - self.ui_components["config_view"] = "ConfigView" - self.ui_components["command_view"] = "CommandView" - self.ui_components["training_view"] = "TrainingView" - self.ui_components["test_view"] = "TestView" - self.ui_components["monitor_view"] = "MonitorView" - self.ui_components["preset_view"] = "PresetView" - - print("✓ UI组件初始化完成") - except Exception as e: - print(f"✗ UI组件初始化失败: {e}") - - def _close_ui_components(self): - """关闭UI组件""" - try: - # 在实际实现中,这里会关闭实际的UI组件 - self.ui_components.clear() - print("✓ UI组件已关闭") - except Exception as e: - print(f"✗ UI组件关闭失败: {e}") - - def _load_editor_cache(self): - """加载编辑器缓存""" - try: - # 模拟加载缓存数据 - if self.plugin.config_manager: - self.editor_data["config_cache"] = self.plugin.config_manager.get_config() - - if self.plugin.command_processor: - self.editor_data["command_cache"] = self.plugin.command_processor.get_command_mappings() - - print("✓ 编辑器缓存加载完成") - except Exception as e: - print(f"✗ 编辑器缓存加载失败: {e}") - - def _update_editor_display(self): - """更新编辑器显示""" - try: - current_time = time.time() - - # 检查是否需要刷新 - if current_time - self.last_refresh_time >= 1.0 / self.editor_config["refresh_rate"]: - # 更新配置视图 - self._refresh_config_view() - - # 更新属性面板 - self._refresh_property_panel() - - # 更新预览面板 - if self.editor_config["enable_preview"]: - self._update_preview_panel() - - # 更新状态栏 - self._update_status_bar() - - self.last_refresh_time = current_time - self.editor_stats["data_refreshed"] += 1 - - except Exception as e: - print(f"✗ 编辑器显示更新失败: {e}") - - def _update_monitor_data(self): - """更新监控数据""" - try: - # 收集实时数据 - current_time = time.time() - - # 收集音频级别数据 - if self.plugin.audio_manager: - input_level = self.plugin.audio_manager.get_audio_level("input") - output_level = self.plugin.audio_manager.get_audio_level("output") - - self.monitor_data["audio_levels"].append({ - "input": input_level, - "output": output_level, - "timestamp": current_time - }) - - # 保持数据大小 - if len(self.monitor_data["audio_levels"]) > 100: - self.monitor_data["audio_levels"] = self.monitor_data["audio_levels"][-100:] - - # 收集识别结果 - if self.plugin.voice_manager: - recognition_state = self.plugin.voice_manager.get_recognition_state() - if recognition_state.get("current_text"): - self.monitor_data["recognition_results"].append({ - "text": recognition_state["current_text"], - "confidence": recognition_state["confidence"], - "timestamp": current_time - }) - - # 保持数据大小 - if len(self.monitor_data["recognition_results"]) > 50: - self.monitor_data["recognition_results"] = self.monitor_data["recognition_results"][-50:] - - # 收集合成事件 - if self.plugin.voice_manager: - synthesis_state = self.plugin.voice_manager.get_synthesis_state() - if synthesis_state.get("current_text"): - # 这里只是示例,实际应该监听合成事件 - pass - - # 收集性能指标 - self._collect_performance_metrics() - - except Exception as e: - print(f"✗ 监控数据更新失败: {e}") - - def _collect_performance_metrics(self): - """收集性能指标""" - try: - metrics = {} - - # 收集各模块统计信息 - if self.plugin.voice_manager: - metrics["voice_manager"] = self.plugin.voice_manager.get_stats() - - if self.plugin.speech_recognizer: - metrics["speech_recognizer"] = self.plugin.speech_recognizer.get_stats() - - if self.plugin.speech_synthesizer: - metrics["speech_synthesizer"] = self.plugin.speech_synthesizer.get_stats() - - if self.plugin.command_processor: - metrics["command_processor"] = self.plugin.command_processor.get_stats() - - if self.plugin.audio_manager: - metrics["audio_manager"] = self.plugin.audio_manager.get_device_stats() - - if self.plugin.nlp_processor: - metrics["nlp_processor"] = self.plugin.nlp_processor.get_stats() - - if self.plugin.event_handler: - metrics["event_handler"] = self.plugin.event_handler.get_stats() - - if self.plugin.config_manager: - metrics["config_manager"] = self.plugin.config_manager.get_config_stats() - - self.monitor_data["performance_metrics"] = metrics - - except Exception as e: - print(f"✗ 性能指标收集失败: {e}") - - def _refresh_config_view(self): - """刷新配置视图""" - try: - # 在实际实现中,这里会更新配置视图的显示内容 - pass - except Exception as e: - print(f"✗ 配置视图刷新失败: {e}") - - def _refresh_property_panel(self): - """刷新属性面板""" - try: - # 根据当前模式更新属性面板 - current_mode = self.editor_state["current_mode"] - - # 在实际实现中,这里会更新属性面板的显示内容 - pass - except Exception as e: - print(f"✗ 属性面板刷新失败: {e}") - - def _update_preview_panel(self): - """更新预览面板""" - try: - current_time = time.time() - if current_time - self.last_preview_update >= 0.1: # 100ms - # 更新预览数据 - self.editor_data["live_data"] = self._collect_live_data() - - # 在实际实现中,这里会更新预览面板的显示 - self.editor_stats["preview_updates"] += 1 - self.last_preview_update = current_time - - except Exception as e: - print(f"✗ 预览面板更新失败: {e}") - - def _collect_live_data(self) -> Dict[str, Any]: - """ - 收集实时数据 - - Returns: - 实时数据字典 - """ - try: - live_data = { - "timestamp": time.time(), - "audio": {}, - "recognition": {}, - "synthesis": {}, - "commands": {}, - "performance": {} - } - - # 收集音频数据 - if self.plugin.audio_manager: - live_data["audio"] = { - "input_level": self.plugin.audio_manager.get_audio_level("input"), - "output_level": self.plugin.audio_manager.get_audio_level("output"), - "is_recording": self.plugin.audio_manager.is_recording(), - "is_playing": self.plugin.audio_manager.is_playing() - } - - # 收集识别数据 - if self.plugin.voice_manager: - live_data["recognition"] = self.plugin.voice_manager.get_recognition_state() - - # 收集合成数据 - if self.plugin.voice_manager: - live_data["synthesis"] = self.plugin.voice_manager.get_synthesis_state() - - # 收集命令数据 - if self.plugin.command_processor: - live_data["commands"] = { - "history": self.plugin.command_processor.get_command_history()[-10:] # 最近10条 - } - - # 收集性能数据 - live_data["performance"] = self._collect_performance_metrics() - - return live_data - - except Exception as e: - print(f"✗ 实时数据收集失败: {e}") - return {} - - def _update_status_bar(self): - """更新状态栏""" - try: - # 在实际实现中,这里会更新状态栏的显示内容 - pass - except Exception as e: - print(f"✗ 状态栏更新失败: {e}") - - def set_editor_mode(self, mode: str) -> bool: - """ - 设置编辑器模式 - - Args: - mode: 编辑器模式 - - Returns: - 是否设置成功 - """ - try: - old_mode = self.editor_state["current_mode"] - self.editor_state["current_mode"] = mode - - # 触发模式变更回调 - self._trigger_editor_callback("mode_changed", { - "old_mode": old_mode, - "new_mode": mode - }) - - print(f"✓ 编辑器模式已设置: {mode}") - return True - - except Exception as e: - print(f"✗ 编辑器模式设置失败: {e}") - return False - - def set_selected_tab(self, tab: str) -> bool: - """ - 设置选中标签页 - - Args: - tab: 标签页名称 - - Returns: - 是否设置成功 - """ - try: - old_tab = self.editor_state["selected_tab"] - self.editor_state["selected_tab"] = tab - - # 触发标签页变更回调 - self._trigger_editor_callback("tab_changed", { - "old_tab": old_tab, - "new_tab": tab - }) - - print(f"✓ 选中标签页已设置: {tab}") - return True - - except Exception as e: - print(f"✗ 选中标签页设置失败: {e}") - return False - - def start_recording(self) -> bool: - """ - 开始录制 - - Returns: - 是否开始成功 - """ - try: - if not self.plugin.audio_manager: - print("✗ 音频管理器未初始化") - return False - - if self.plugin.audio_manager.start_recording(): - self.editor_state["is_recording"] = True - - # 触发录制开始回调 - self._trigger_editor_callback("recording_started", { - "timestamp": time.time() - }) - - print("✓ 录制已开始") - return True - else: - print("✗ 录制开始失败") - return False - - except Exception as e: - print(f"✗ 录制开始失败: {e}") - return False - - def stop_recording(self) -> bool: - """ - 停止录制 - - Returns: - 是否停止成功 - """ - try: - if not self.plugin.audio_manager: - print("✗ 音频管理器未初始化") - return False - - if self.plugin.audio_manager.stop_recording(): - self.editor_state["is_recording"] = False - - # 触发录制停止回调 - self._trigger_editor_callback("recording_stopped", { - "timestamp": time.time() - }) - - print("✓ 录制已停止") - return True - else: - print("✗ 录制停止失败") - return False - - except Exception as e: - print(f"✗ 录制停止失败: {e}") - return False - - def start_testing(self, test_case: Dict[str, Any] = None) -> bool: - """ - 开始测试 - - Args: - test_case: 测试用例 - - Returns: - 是否开始成功 - """ - try: - self.editor_state["is_testing"] = True - - # 添加到测试历史 - if test_case: - self.test_data["current_test"] = test_case - self.test_data["test_cases"].append(test_case) - - # 触发测试开始回调 - self._trigger_editor_callback("test_started", { - "test_case": test_case, - "timestamp": time.time() - }) - - self.editor_stats["tests_run"] += 1 - print("✓ 测试已开始") - return True - - except Exception as e: - print(f"✗ 测试开始失败: {e}") - return False - - def stop_testing(self) -> bool: - """ - 停止测试 - - Returns: - 是否停止成功 - """ - try: - self.editor_state["is_testing"] = False - - # 记录测试结果 - if self.test_data["current_test"]: - test_result = { - "test_case": self.test_data["current_test"], - "end_time": time.time(), - "status": "completed" - } - self.test_data["test_results"].append(test_result) - self.test_data["current_test"] = None - - # 触发测试完成回调 - self._trigger_editor_callback("test_completed", { - "timestamp": time.time() - }) - - print("✓ 测试已停止") - return True - - except Exception as e: - print(f"✗ 测试停止失败: {e}") - return False - - def test_speech_recognition(self, audio_file: str = None) -> bool: - """ - 测试语音识别 - - Args: - audio_file: 音频文件路径(None表示使用麦克风输入) - - Returns: - 是否测试成功 - """ - try: - print("✓ 语音识别测试已开始") - - # 在实际实现中,这里会执行语音识别测试 - # 可能包括加载音频文件、执行识别、验证结果等 - - # 添加到测试历史 - test_entry = { - "type": "speech_recognition", - "audio_file": audio_file, - "start_time": time.time(), - "parameters": { - "language": self.plugin.config_manager.get_config("recognition", "language", "zh-CN") if self.plugin.config_manager else "zh-CN" - } - } - - self.editor_data["test_history"].append(test_entry) - - return True - - except Exception as e: - print(f"✗ 语音识别测试失败: {e}") - return False - - def test_speech_synthesis(self, text: str, language: str = None) -> bool: - """ - 测试语音合成 - - Args: - text: 要合成的文本 - language: 语言代码 - - Returns: - 是否测试成功 - """ - try: - if not self.plugin.speech_synthesizer: - print("✗ 语音合成器未初始化") - return False - - if not language: - language = self.plugin.config_manager.get_config("synthesis", "language", "zh-CN") if self.plugin.config_manager else "zh-CN" - - print(f"✓ 语音合成测试已开始: '{text}'") - - # 执行语音合成 - result = self.plugin.speech_synthesizer.synthesize_speech( - text, - blocking=False, - speed=self.plugin.config_manager.get_config("synthesis", "speed", 1.0) if self.plugin.config_manager else 1.0, - volume=self.plugin.config_manager.get_config("synthesis", "volume", 1.0) if self.plugin.config_manager else 1.0 - ) - - # 添加到测试历史 - test_entry = { - "type": "speech_synthesis", - "text": text, - "language": language, - "start_time": time.time(), - "success": result, - "parameters": { - "speed": self.plugin.config_manager.get_config("synthesis", "speed", 1.0) if self.plugin.config_manager else 1.0, - "volume": self.plugin.config_manager.get_config("synthesis", "volume", 1.0) if self.plugin.config_manager else 1.0 - } - } - - self.editor_data["test_history"].append(test_entry) - - return result - - except Exception as e: - print(f"✗ 语音合成测试失败: {e}") - return False - - def test_command_execution(self, command_text: str) -> bool: - """ - 测试命令执行 - - Args: - command_text: 命令文本 - - Returns: - 是否测试成功 - """ - try: - if not self.plugin.command_processor: - print("✗ 命令处理器未初始化") - return False - - print(f"✓ 命令执行测试已开始: '{command_text}'") - - # 执行命令处理 - result = self.plugin.command_processor.process_command(command_text, 1.0) - - # 添加到测试历史 - test_entry = { - "type": "command_execution", - "command": command_text, - "start_time": time.time(), - "success": result - } - - self.editor_data["test_history"].append(test_entry) - - return result - - except Exception as e: - print(f"✗ 命令执行测试失败: {e}") - return False - - def toggle_preview(self, enable: bool = None) -> bool: - """ - 切换预览 - - Args: - enable: 是否启用预览(None表示切换) - - Returns: - 是否切换成功 - """ - try: - if enable is None: - enable = not self.editor_state["preview_active"] - - self.editor_state["preview_active"] = enable - self.editor_config["enable_preview"] = enable - - status = "启用" if enable else "禁用" - print(f"✓ 预览已{status}") - return True - - except Exception as e: - print(f"✗ 预览切换失败: {e}") - return False - - def toggle_monitoring(self, enable: bool = None) -> bool: - """ - 切换监控 - - Args: - enable: 是否启用监控(None表示切换) - - Returns: - 是否切换成功 - """ - try: - if enable is None: - enable = not self.editor_state["monitoring_active"] - - self.editor_state["monitoring_active"] = enable - - status = "启用" if enable else "禁用" - print(f"✓ 监控已{status}") - return True - - except Exception as e: - print(f"✗ 监控切换失败: {e}") - return False - - def undo_operation(self) -> bool: - """ - 撤销操作 - - Returns: - 是否撤销成功 - """ - try: - if self.history_pointer >= 0 and self.operation_history: - # 执行撤销操作 - operation = self.operation_history[self.history_pointer] - self.history_pointer -= 1 - self.editor_stats["undo_operations"] += 1 - - print("✓ 操作已撤销") - return True - else: - print("⚠ 没有可撤销的操作") - return False - - except Exception as e: - print(f"✗ 操作撤销失败: {e}") - return False - - def redo_operation(self) -> bool: - """ - 重做操作 - - Returns: - 是否重做成功 - """ - try: - if self.history_pointer < len(self.operation_history) - 1: - # 执行重做操作 - self.history_pointer += 1 - operation = self.operation_history[self.history_pointer] - self.editor_stats["redo_operations"] += 1 - - print("✓ 操作已重做") - return True - else: - print("⚠ 没有可重做的操作") - return False - - except Exception as e: - print(f"✗ 操作重做失败: {e}") - return False - - def add_operation_to_history(self, operation: Dict[str, Any]): - """ - 添加操作到历史 - - Args: - operation: 操作数据 - """ - try: - # 添加到历史记录 - self.operation_history.append(operation) - self.history_pointer = len(self.operation_history) - 1 - - # 保持历史记录大小 - if len(self.operation_history) > self.max_history_size: - remove_count = len(self.operation_history) - self.max_history_size - self.operation_history = self.operation_history[remove_count:] - self.history_pointer -= remove_count - self.history_pointer = max(0, self.history_pointer) - - self.editor_stats["operations_performed"] += 1 - - except Exception as e: - print(f"✗ 操作添加到历史失败: {e}") - - def _trigger_editor_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发编辑器回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.editor_callbacks: - for callback in self.editor_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 编辑器回调执行失败: {e}") - except Exception as e: - print(f"✗ 编辑器回调触发失败: {e}") - - def register_editor_callback(self, callback_type: str, callback: callable): - """ - 注册编辑器回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.editor_callbacks: - self.editor_callbacks[callback_type].append(callback) - print(f"✓ 编辑器回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 编辑器回调注册失败: {e}") - - def unregister_editor_callback(self, callback_type: str, callback: callable): - """ - 注销编辑器回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.editor_callbacks: - if callback in self.editor_callbacks[callback_type]: - self.editor_callbacks[callback_type].remove(callback) - print(f"✓ 编辑器回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 编辑器回调注销失败: {e}") - - def get_editor_state(self) -> Dict[str, Any]: - """ - 获取编辑器状态 - - Returns: - 编辑器状态字典 - """ - return self.editor_state.copy() - - def get_editor_config(self) -> Dict[str, Any]: - """ - 获取编辑器配置 - - Returns: - 编辑器配置字典 - """ - return self.editor_config.copy() - - def set_editor_config(self, config: Dict[str, Any]) -> bool: - """ - 设置编辑器配置 - - Args: - config: 编辑器配置字典 - - Returns: - 是否设置成功 - """ - try: - self.editor_config.update(config) - print(f"✓ 编辑器配置已更新: {self.editor_config}") - return True - except Exception as e: - print(f"✗ 编辑器配置设置失败: {e}") - return False - - def get_editor_data(self, data_type: str) -> Optional[Dict[str, Any]]: - """ - 获取编辑器数据 - - Args: - data_type: 数据类型 - - Returns: - 数据字典或None - """ - return self.editor_data.get(data_type) - - def get_monitor_data(self) -> Dict[str, Any]: - """ - 获取监控数据 - - Returns: - 监控数据字典 - """ - return self.monitor_data.copy() - - def get_test_data(self) -> Dict[str, Any]: - """ - 获取测试数据 - - Returns: - 测试数据字典 - """ - return self.test_data.copy() - - def get_editor_stats(self) -> Dict[str, int]: - """ - 获取编辑器统计信息 - - Returns: - 编辑器统计字典 - """ - return self.editor_stats.copy() - - def reset_editor_stats(self): - """重置编辑器统计信息""" - try: - self.editor_stats = { - "windows_opened": 0, - "operations_performed": 0, - "undo_operations": 0, - "redo_operations": 0, - "data_refreshed": 0, - "preview_updates": 0, - "tests_run": 0 - } - print("✓ 编辑器统计信息已重置") - except Exception as e: - print(f"✗ 编辑器统计信息重置失败: {e}") - - def refresh_editor_data(self): - """刷新编辑器数据""" - try: - self._load_editor_cache() - self._update_editor_display() - print("✓ 编辑器数据已刷新") - except Exception as e: - print(f"✗ 编辑器数据刷新失败: {e}") - - def show_editor_window(self) -> bool: - """ - 显示编辑器窗口 - - Returns: - 是否显示成功 - """ - try: - # 在实际实现中,这里会显示实际的编辑器窗口 - self.editor_stats["windows_opened"] += 1 - print("✓ 编辑器窗口已显示") - return True - except Exception as e: - print(f"✗ 编辑器窗口显示失败: {e}") - return False - - def hide_editor_window(self) -> bool: - """ - 隐藏编辑器窗口 - - Returns: - 是否隐藏成功 - """ - try: - # 在实际实现中,这里会隐藏实际的编辑器窗口 - print("✓ 编辑器窗口已隐藏") - return True - except Exception as e: - print(f"✗ 编辑器窗口隐藏失败: {e}") - return False - - def export_editor_data(self, file_path: str) -> bool: - """ - 导出编辑器数据 - - Args: - file_path: 导出文件路径 - - Returns: - 是否导出成功 - """ - try: - # 在实际实现中,这里会导出编辑器数据到文件 - print(f"✓ 编辑器数据已导出: {file_path}") - return True - except Exception as e: - print(f"✗ 编辑器数据导出失败: {e}") - return False - - def import_editor_data(self, file_path: str) -> bool: - """ - 导入编辑器数据 - - Args: - file_path: 导入文件路径 - - Returns: - 是否导入成功 - """ - try: - # 在实际实现中,这里会从文件导入编辑器数据 - print(f"✓ 编辑器数据已导入: {file_path}") - return True - except Exception as e: - print(f"✗ 编辑器数据导入失败: {e}") - return False - - def run_comprehensive_test(self) -> Dict[str, Any]: - """ - 运行综合测试 - - Returns: - 测试结果字典 - """ - try: - print("✓ 开始综合测试...") - - test_results = { - "timestamp": time.time(), - "tests": {}, - "summary": { - "total_tests": 0, - "passed_tests": 0, - "failed_tests": 0, - "success_rate": 0.0 - } - } - - # 测试语音识别 - test_results["tests"]["speech_recognition"] = { - "status": "passed" if self.test_speech_recognition() else "failed", - "details": "语音识别功能测试" - } - - # 测试语音合成 - test_results["tests"]["speech_synthesis"] = { - "status": "passed" if self.test_speech_synthesis("测试语音合成功能") else "failed", - "details": "语音合成功能测试" - } - - # 测试命令执行 - test_results["tests"]["command_execution"] = { - "status": "passed" if self.test_command_execution("前进") else "failed", - "details": "命令执行功能测试" - } - - # 统计结果 - total_tests = len(test_results["tests"]) - passed_tests = sum(1 for test in test_results["tests"].values() if test["status"] == "passed") - - test_results["summary"]["total_tests"] = total_tests - test_results["summary"]["passed_tests"] = passed_tests - test_results["summary"]["failed_tests"] = total_tests - passed_tests - test_results["summary"]["success_rate"] = passed_tests / total_tests if total_tests > 0 else 0.0 - - print(f"✓ 综合测试完成: {passed_tests}/{total_tests} 通过") - return test_results - - except Exception as e: - print(f"✗ 综合测试失败: {e}") - import traceback - traceback.print_exc() - return { - "timestamp": time.time(), - "tests": {}, - "summary": { - "total_tests": 0, - "passed_tests": 0, - "failed_tests": 0, - "success_rate": 0.0, - "error": str(e) - } +""" +语音编辑器模块 +提供可视化界面用于配置和测试语音控制功能 +""" + +import time +from typing import Dict, Any, List, Optional +import threading + +class EditorMode: + """编辑器模式枚举""" + CONFIGURATION = "configuration" + COMMAND_MAPPING = "command_mapping" + VOICE_TRAINING = "voice_training" + TESTING = "testing" + MONITORING = "monitoring" + PRESETS = "presets" + +class VoiceEditor: + """ + 语音编辑器 + 提供可视化界面用于配置和测试语音控制功能 + """ + + def __init__(self, plugin): + """ + 初始化语音编辑器 + + Args: + plugin: 语音控制插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 编辑器配置 + self.editor_config = { + "window_width": 1024, + "window_height": 768, + "refresh_rate": 30.0, + "enable_preview": True, + "enable_realtime_update": True, + "show_debug_info": False, + "theme": "dark" + } + + # 当前编辑器状态 + self.editor_state = { + "current_mode": EditorMode.CONFIGURATION, + "selected_tab": "general", + "is_recording": False, + "is_testing": False, + "preview_active": False, + "monitoring_active": True + } + + # UI组件 + self.ui_components = { + "main_window": None, + "toolbar": None, + "property_panel": None, + "preview_panel": None, + "status_bar": None, + "config_view": None, + "command_view": None, + "training_view": None, + "test_view": None, + "monitor_view": None, + "preset_view": None + } + + # 编辑器数据 + self.editor_data = { + "config_cache": {}, + "command_cache": {}, + "preset_cache": {}, + "live_data": {}, + "test_history": [] + } + + # 编辑器操作历史 + self.operation_history = [] + self.history_pointer = -1 + self.max_history_size = 100 + + # 实时监控数据 + self.monitor_data = { + "audio_levels": [], + "recognition_results": [], + "synthesis_events": [], + "command_executions": [], + "performance_metrics": {} + } + + # 测试数据 + self.test_data = { + "test_cases": [], + "test_results": [], + "current_test": None + } + + # 回调函数 + self.editor_callbacks = { + "mode_changed": [], + "tab_changed": [], + "data_updated": [], + "test_started": [], + "test_completed": [], + "recording_started": [], + "recording_stopped": [] + } + + # 统计信息 + self.editor_stats = { + "windows_opened": 0, + "operations_performed": 0, + "undo_operations": 0, + "redo_operations": 0, + "data_refreshed": 0, + "preview_updates": 0, + "tests_run": 0 + } + + # 线程管理 + self.editor_thread = None + self.monitor_thread = None + self.running = False + + # 时间戳记录 + self.last_refresh_time = 0.0 + self.last_preview_update = 0.0 + self.last_monitor_update = 0.0 + + print("✓ 语音编辑器已创建") + + def initialize(self) -> bool: + """ + 初始化语音编辑器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化UI组件(模拟) + self._initialize_ui_components() + + # 加载缓存数据 + self._load_editor_cache() + + self.initialized = True + print("✓ 语音编辑器初始化完成") + return True + + except Exception as e: + print(f"✗ 语音编辑器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用语音编辑器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 语音编辑器未初始化") + return False + + self.enabled = True + self.running = True + + # 启动编辑器线程 + self.editor_thread = threading.Thread(target=self._editor_update_loop, daemon=True) + self.editor_thread.start() + + # 启动监控线程 + if self.editor_config["enable_realtime_update"]: + self.monitor_thread = threading.Thread(target=self._monitor_update_loop, daemon=True) + self.monitor_thread.start() + + print("✓ 语音编辑器已启用") + return True + + except Exception as e: + print(f"✗ 语音编辑器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用语音编辑器""" + try: + self.enabled = False + self.running = False + + # 等待线程结束 + if self.editor_thread and self.editor_thread.is_alive(): + self.editor_thread.join(timeout=2.0) + + if self.monitor_thread and self.monitor_thread.is_alive(): + self.monitor_thread.join(timeout=2.0) + + # 关闭UI组件 + self._close_ui_components() + + print("✓ 语音编辑器已禁用") + + except Exception as e: + print(f"✗ 语音编辑器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理语音编辑器资源""" + try: + self.disable() + self.ui_components.clear() + self.editor_data.clear() + self.operation_history.clear() + self.monitor_data.clear() + self.test_data.clear() + self.editor_callbacks.clear() + self.initialized = False + print("✓ 语音编辑器资源已清理") + + except Exception as e: + print(f"✗ 语音编辑器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新语音编辑器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新统计信息 + pass + + except Exception as e: + print(f"✗ 语音编辑器更新失败: {e}") + import traceback + traceback.print_exc() + + def _editor_update_loop(self): + """编辑器更新循环""" + try: + refresh_interval = 1.0 / self.editor_config["refresh_rate"] + + while self.running: + if self.enabled: + self._update_editor_display() + + time.sleep(refresh_interval) + + except Exception as e: + print(f"✗ 编辑器更新循环失败: {e}") + + def _monitor_update_loop(self): + """监控更新循环""" + try: + monitor_interval = 0.1 # 100ms + + while self.running: + if self.enabled and self.editor_config["enable_realtime_update"]: + self._update_monitor_data() + + time.sleep(monitor_interval) + + except Exception as e: + print(f"✗ 监控更新循环失败: {e}") + + def _initialize_ui_components(self): + """初始化UI组件""" + try: + # 在实际实现中,这里会创建实际的UI组件 + # 由于这是模拟实现,我们只创建占位符 + self.ui_components["main_window"] = "MainWindow" + self.ui_components["toolbar"] = "Toolbar" + self.ui_components["property_panel"] = "PropertyPanel" + self.ui_components["preview_panel"] = "PreviewPanel" + self.ui_components["status_bar"] = "StatusBar" + self.ui_components["config_view"] = "ConfigView" + self.ui_components["command_view"] = "CommandView" + self.ui_components["training_view"] = "TrainingView" + self.ui_components["test_view"] = "TestView" + self.ui_components["monitor_view"] = "MonitorView" + self.ui_components["preset_view"] = "PresetView" + + print("✓ UI组件初始化完成") + except Exception as e: + print(f"✗ UI组件初始化失败: {e}") + + def _close_ui_components(self): + """关闭UI组件""" + try: + # 在实际实现中,这里会关闭实际的UI组件 + self.ui_components.clear() + print("✓ UI组件已关闭") + except Exception as e: + print(f"✗ UI组件关闭失败: {e}") + + def _load_editor_cache(self): + """加载编辑器缓存""" + try: + # 模拟加载缓存数据 + if self.plugin.config_manager: + self.editor_data["config_cache"] = self.plugin.config_manager.get_config() + + if self.plugin.command_processor: + self.editor_data["command_cache"] = self.plugin.command_processor.get_command_mappings() + + print("✓ 编辑器缓存加载完成") + except Exception as e: + print(f"✗ 编辑器缓存加载失败: {e}") + + def _update_editor_display(self): + """更新编辑器显示""" + try: + current_time = time.time() + + # 检查是否需要刷新 + if current_time - self.last_refresh_time >= 1.0 / self.editor_config["refresh_rate"]: + # 更新配置视图 + self._refresh_config_view() + + # 更新属性面板 + self._refresh_property_panel() + + # 更新预览面板 + if self.editor_config["enable_preview"]: + self._update_preview_panel() + + # 更新状态栏 + self._update_status_bar() + + self.last_refresh_time = current_time + self.editor_stats["data_refreshed"] += 1 + + except Exception as e: + print(f"✗ 编辑器显示更新失败: {e}") + + def _update_monitor_data(self): + """更新监控数据""" + try: + # 收集实时数据 + current_time = time.time() + + # 收集音频级别数据 + if self.plugin.audio_manager: + input_level = self.plugin.audio_manager.get_audio_level("input") + output_level = self.plugin.audio_manager.get_audio_level("output") + + self.monitor_data["audio_levels"].append({ + "input": input_level, + "output": output_level, + "timestamp": current_time + }) + + # 保持数据大小 + if len(self.monitor_data["audio_levels"]) > 100: + self.monitor_data["audio_levels"] = self.monitor_data["audio_levels"][-100:] + + # 收集识别结果 + if self.plugin.voice_manager: + recognition_state = self.plugin.voice_manager.get_recognition_state() + if recognition_state.get("current_text"): + self.monitor_data["recognition_results"].append({ + "text": recognition_state["current_text"], + "confidence": recognition_state["confidence"], + "timestamp": current_time + }) + + # 保持数据大小 + if len(self.monitor_data["recognition_results"]) > 50: + self.monitor_data["recognition_results"] = self.monitor_data["recognition_results"][-50:] + + # 收集合成事件 + if self.plugin.voice_manager: + synthesis_state = self.plugin.voice_manager.get_synthesis_state() + if synthesis_state.get("current_text"): + # 这里只是示例,实际应该监听合成事件 + pass + + # 收集性能指标 + self._collect_performance_metrics() + + except Exception as e: + print(f"✗ 监控数据更新失败: {e}") + + def _collect_performance_metrics(self): + """收集性能指标""" + try: + metrics = {} + + # 收集各模块统计信息 + if self.plugin.voice_manager: + metrics["voice_manager"] = self.plugin.voice_manager.get_stats() + + if self.plugin.speech_recognizer: + metrics["speech_recognizer"] = self.plugin.speech_recognizer.get_stats() + + if self.plugin.speech_synthesizer: + metrics["speech_synthesizer"] = self.plugin.speech_synthesizer.get_stats() + + if self.plugin.command_processor: + metrics["command_processor"] = self.plugin.command_processor.get_stats() + + if self.plugin.audio_manager: + metrics["audio_manager"] = self.plugin.audio_manager.get_device_stats() + + if self.plugin.nlp_processor: + metrics["nlp_processor"] = self.plugin.nlp_processor.get_stats() + + if self.plugin.event_handler: + metrics["event_handler"] = self.plugin.event_handler.get_stats() + + if self.plugin.config_manager: + metrics["config_manager"] = self.plugin.config_manager.get_config_stats() + + self.monitor_data["performance_metrics"] = metrics + + except Exception as e: + print(f"✗ 性能指标收集失败: {e}") + + def _refresh_config_view(self): + """刷新配置视图""" + try: + # 在实际实现中,这里会更新配置视图的显示内容 + pass + except Exception as e: + print(f"✗ 配置视图刷新失败: {e}") + + def _refresh_property_panel(self): + """刷新属性面板""" + try: + # 根据当前模式更新属性面板 + current_mode = self.editor_state["current_mode"] + + # 在实际实现中,这里会更新属性面板的显示内容 + pass + except Exception as e: + print(f"✗ 属性面板刷新失败: {e}") + + def _update_preview_panel(self): + """更新预览面板""" + try: + current_time = time.time() + if current_time - self.last_preview_update >= 0.1: # 100ms + # 更新预览数据 + self.editor_data["live_data"] = self._collect_live_data() + + # 在实际实现中,这里会更新预览面板的显示 + self.editor_stats["preview_updates"] += 1 + self.last_preview_update = current_time + + except Exception as e: + print(f"✗ 预览面板更新失败: {e}") + + def _collect_live_data(self) -> Dict[str, Any]: + """ + 收集实时数据 + + Returns: + 实时数据字典 + """ + try: + live_data = { + "timestamp": time.time(), + "audio": {}, + "recognition": {}, + "synthesis": {}, + "commands": {}, + "performance": {} + } + + # 收集音频数据 + if self.plugin.audio_manager: + live_data["audio"] = { + "input_level": self.plugin.audio_manager.get_audio_level("input"), + "output_level": self.plugin.audio_manager.get_audio_level("output"), + "is_recording": self.plugin.audio_manager.is_recording(), + "is_playing": self.plugin.audio_manager.is_playing() + } + + # 收集识别数据 + if self.plugin.voice_manager: + live_data["recognition"] = self.plugin.voice_manager.get_recognition_state() + + # 收集合成数据 + if self.plugin.voice_manager: + live_data["synthesis"] = self.plugin.voice_manager.get_synthesis_state() + + # 收集命令数据 + if self.plugin.command_processor: + live_data["commands"] = { + "history": self.plugin.command_processor.get_command_history()[-10:] # 最近10条 + } + + # 收集性能数据 + live_data["performance"] = self._collect_performance_metrics() + + return live_data + + except Exception as e: + print(f"✗ 实时数据收集失败: {e}") + return {} + + def _update_status_bar(self): + """更新状态栏""" + try: + # 在实际实现中,这里会更新状态栏的显示内容 + pass + except Exception as e: + print(f"✗ 状态栏更新失败: {e}") + + def set_editor_mode(self, mode: str) -> bool: + """ + 设置编辑器模式 + + Args: + mode: 编辑器模式 + + Returns: + 是否设置成功 + """ + try: + old_mode = self.editor_state["current_mode"] + self.editor_state["current_mode"] = mode + + # 触发模式变更回调 + self._trigger_editor_callback("mode_changed", { + "old_mode": old_mode, + "new_mode": mode + }) + + print(f"✓ 编辑器模式已设置: {mode}") + return True + + except Exception as e: + print(f"✗ 编辑器模式设置失败: {e}") + return False + + def set_selected_tab(self, tab: str) -> bool: + """ + 设置选中标签页 + + Args: + tab: 标签页名称 + + Returns: + 是否设置成功 + """ + try: + old_tab = self.editor_state["selected_tab"] + self.editor_state["selected_tab"] = tab + + # 触发标签页变更回调 + self._trigger_editor_callback("tab_changed", { + "old_tab": old_tab, + "new_tab": tab + }) + + print(f"✓ 选中标签页已设置: {tab}") + return True + + except Exception as e: + print(f"✗ 选中标签页设置失败: {e}") + return False + + def start_recording(self) -> bool: + """ + 开始录制 + + Returns: + 是否开始成功 + """ + try: + if not self.plugin.audio_manager: + print("✗ 音频管理器未初始化") + return False + + if self.plugin.audio_manager.start_recording(): + self.editor_state["is_recording"] = True + + # 触发录制开始回调 + self._trigger_editor_callback("recording_started", { + "timestamp": time.time() + }) + + print("✓ 录制已开始") + return True + else: + print("✗ 录制开始失败") + return False + + except Exception as e: + print(f"✗ 录制开始失败: {e}") + return False + + def stop_recording(self) -> bool: + """ + 停止录制 + + Returns: + 是否停止成功 + """ + try: + if not self.plugin.audio_manager: + print("✗ 音频管理器未初始化") + return False + + if self.plugin.audio_manager.stop_recording(): + self.editor_state["is_recording"] = False + + # 触发录制停止回调 + self._trigger_editor_callback("recording_stopped", { + "timestamp": time.time() + }) + + print("✓ 录制已停止") + return True + else: + print("✗ 录制停止失败") + return False + + except Exception as e: + print(f"✗ 录制停止失败: {e}") + return False + + def start_testing(self, test_case: Dict[str, Any] = None) -> bool: + """ + 开始测试 + + Args: + test_case: 测试用例 + + Returns: + 是否开始成功 + """ + try: + self.editor_state["is_testing"] = True + + # 添加到测试历史 + if test_case: + self.test_data["current_test"] = test_case + self.test_data["test_cases"].append(test_case) + + # 触发测试开始回调 + self._trigger_editor_callback("test_started", { + "test_case": test_case, + "timestamp": time.time() + }) + + self.editor_stats["tests_run"] += 1 + print("✓ 测试已开始") + return True + + except Exception as e: + print(f"✗ 测试开始失败: {e}") + return False + + def stop_testing(self) -> bool: + """ + 停止测试 + + Returns: + 是否停止成功 + """ + try: + self.editor_state["is_testing"] = False + + # 记录测试结果 + if self.test_data["current_test"]: + test_result = { + "test_case": self.test_data["current_test"], + "end_time": time.time(), + "status": "completed" + } + self.test_data["test_results"].append(test_result) + self.test_data["current_test"] = None + + # 触发测试完成回调 + self._trigger_editor_callback("test_completed", { + "timestamp": time.time() + }) + + print("✓ 测试已停止") + return True + + except Exception as e: + print(f"✗ 测试停止失败: {e}") + return False + + def test_speech_recognition(self, audio_file: str = None) -> bool: + """ + 测试语音识别 + + Args: + audio_file: 音频文件路径(None表示使用麦克风输入) + + Returns: + 是否测试成功 + """ + try: + print("✓ 语音识别测试已开始") + + # 在实际实现中,这里会执行语音识别测试 + # 可能包括加载音频文件、执行识别、验证结果等 + + # 添加到测试历史 + test_entry = { + "type": "speech_recognition", + "audio_file": audio_file, + "start_time": time.time(), + "parameters": { + "language": self.plugin.config_manager.get_config("recognition", "language", "zh-CN") if self.plugin.config_manager else "zh-CN" + } + } + + self.editor_data["test_history"].append(test_entry) + + return True + + except Exception as e: + print(f"✗ 语音识别测试失败: {e}") + return False + + def test_speech_synthesis(self, text: str, language: str = None) -> bool: + """ + 测试语音合成 + + Args: + text: 要合成的文本 + language: 语言代码 + + Returns: + 是否测试成功 + """ + try: + if not self.plugin.speech_synthesizer: + print("✗ 语音合成器未初始化") + return False + + if not language: + language = self.plugin.config_manager.get_config("synthesis", "language", "zh-CN") if self.plugin.config_manager else "zh-CN" + + print(f"✓ 语音合成测试已开始: '{text}'") + + # 执行语音合成 + result = self.plugin.speech_synthesizer.synthesize_speech( + text, + blocking=False, + speed=self.plugin.config_manager.get_config("synthesis", "speed", 1.0) if self.plugin.config_manager else 1.0, + volume=self.plugin.config_manager.get_config("synthesis", "volume", 1.0) if self.plugin.config_manager else 1.0 + ) + + # 添加到测试历史 + test_entry = { + "type": "speech_synthesis", + "text": text, + "language": language, + "start_time": time.time(), + "success": result, + "parameters": { + "speed": self.plugin.config_manager.get_config("synthesis", "speed", 1.0) if self.plugin.config_manager else 1.0, + "volume": self.plugin.config_manager.get_config("synthesis", "volume", 1.0) if self.plugin.config_manager else 1.0 + } + } + + self.editor_data["test_history"].append(test_entry) + + return result + + except Exception as e: + print(f"✗ 语音合成测试失败: {e}") + return False + + def test_command_execution(self, command_text: str) -> bool: + """ + 测试命令执行 + + Args: + command_text: 命令文本 + + Returns: + 是否测试成功 + """ + try: + if not self.plugin.command_processor: + print("✗ 命令处理器未初始化") + return False + + print(f"✓ 命令执行测试已开始: '{command_text}'") + + # 执行命令处理 + result = self.plugin.command_processor.process_command(command_text, 1.0) + + # 添加到测试历史 + test_entry = { + "type": "command_execution", + "command": command_text, + "start_time": time.time(), + "success": result + } + + self.editor_data["test_history"].append(test_entry) + + return result + + except Exception as e: + print(f"✗ 命令执行测试失败: {e}") + return False + + def toggle_preview(self, enable: bool = None) -> bool: + """ + 切换预览 + + Args: + enable: 是否启用预览(None表示切换) + + Returns: + 是否切换成功 + """ + try: + if enable is None: + enable = not self.editor_state["preview_active"] + + self.editor_state["preview_active"] = enable + self.editor_config["enable_preview"] = enable + + status = "启用" if enable else "禁用" + print(f"✓ 预览已{status}") + return True + + except Exception as e: + print(f"✗ 预览切换失败: {e}") + return False + + def toggle_monitoring(self, enable: bool = None) -> bool: + """ + 切换监控 + + Args: + enable: 是否启用监控(None表示切换) + + Returns: + 是否切换成功 + """ + try: + if enable is None: + enable = not self.editor_state["monitoring_active"] + + self.editor_state["monitoring_active"] = enable + + status = "启用" if enable else "禁用" + print(f"✓ 监控已{status}") + return True + + except Exception as e: + print(f"✗ 监控切换失败: {e}") + return False + + def undo_operation(self) -> bool: + """ + 撤销操作 + + Returns: + 是否撤销成功 + """ + try: + if self.history_pointer >= 0 and self.operation_history: + # 执行撤销操作 + operation = self.operation_history[self.history_pointer] + self.history_pointer -= 1 + self.editor_stats["undo_operations"] += 1 + + print("✓ 操作已撤销") + return True + else: + print("⚠ 没有可撤销的操作") + return False + + except Exception as e: + print(f"✗ 操作撤销失败: {e}") + return False + + def redo_operation(self) -> bool: + """ + 重做操作 + + Returns: + 是否重做成功 + """ + try: + if self.history_pointer < len(self.operation_history) - 1: + # 执行重做操作 + self.history_pointer += 1 + operation = self.operation_history[self.history_pointer] + self.editor_stats["redo_operations"] += 1 + + print("✓ 操作已重做") + return True + else: + print("⚠ 没有可重做的操作") + return False + + except Exception as e: + print(f"✗ 操作重做失败: {e}") + return False + + def add_operation_to_history(self, operation: Dict[str, Any]): + """ + 添加操作到历史 + + Args: + operation: 操作数据 + """ + try: + # 添加到历史记录 + self.operation_history.append(operation) + self.history_pointer = len(self.operation_history) - 1 + + # 保持历史记录大小 + if len(self.operation_history) > self.max_history_size: + remove_count = len(self.operation_history) - self.max_history_size + self.operation_history = self.operation_history[remove_count:] + self.history_pointer -= remove_count + self.history_pointer = max(0, self.history_pointer) + + self.editor_stats["operations_performed"] += 1 + + except Exception as e: + print(f"✗ 操作添加到历史失败: {e}") + + def _trigger_editor_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发编辑器回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.editor_callbacks: + for callback in self.editor_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 编辑器回调执行失败: {e}") + except Exception as e: + print(f"✗ 编辑器回调触发失败: {e}") + + def register_editor_callback(self, callback_type: str, callback: callable): + """ + 注册编辑器回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.editor_callbacks: + self.editor_callbacks[callback_type].append(callback) + print(f"✓ 编辑器回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 编辑器回调注册失败: {e}") + + def unregister_editor_callback(self, callback_type: str, callback: callable): + """ + 注销编辑器回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.editor_callbacks: + if callback in self.editor_callbacks[callback_type]: + self.editor_callbacks[callback_type].remove(callback) + print(f"✓ 编辑器回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 编辑器回调注销失败: {e}") + + def get_editor_state(self) -> Dict[str, Any]: + """ + 获取编辑器状态 + + Returns: + 编辑器状态字典 + """ + return self.editor_state.copy() + + def get_editor_config(self) -> Dict[str, Any]: + """ + 获取编辑器配置 + + Returns: + 编辑器配置字典 + """ + return self.editor_config.copy() + + def set_editor_config(self, config: Dict[str, Any]) -> bool: + """ + 设置编辑器配置 + + Args: + config: 编辑器配置字典 + + Returns: + 是否设置成功 + """ + try: + self.editor_config.update(config) + print(f"✓ 编辑器配置已更新: {self.editor_config}") + return True + except Exception as e: + print(f"✗ 编辑器配置设置失败: {e}") + return False + + def get_editor_data(self, data_type: str) -> Optional[Dict[str, Any]]: + """ + 获取编辑器数据 + + Args: + data_type: 数据类型 + + Returns: + 数据字典或None + """ + return self.editor_data.get(data_type) + + def get_monitor_data(self) -> Dict[str, Any]: + """ + 获取监控数据 + + Returns: + 监控数据字典 + """ + return self.monitor_data.copy() + + def get_test_data(self) -> Dict[str, Any]: + """ + 获取测试数据 + + Returns: + 测试数据字典 + """ + return self.test_data.copy() + + def get_editor_stats(self) -> Dict[str, int]: + """ + 获取编辑器统计信息 + + Returns: + 编辑器统计字典 + """ + return self.editor_stats.copy() + + def reset_editor_stats(self): + """重置编辑器统计信息""" + try: + self.editor_stats = { + "windows_opened": 0, + "operations_performed": 0, + "undo_operations": 0, + "redo_operations": 0, + "data_refreshed": 0, + "preview_updates": 0, + "tests_run": 0 + } + print("✓ 编辑器统计信息已重置") + except Exception as e: + print(f"✗ 编辑器统计信息重置失败: {e}") + + def refresh_editor_data(self): + """刷新编辑器数据""" + try: + self._load_editor_cache() + self._update_editor_display() + print("✓ 编辑器数据已刷新") + except Exception as e: + print(f"✗ 编辑器数据刷新失败: {e}") + + def show_editor_window(self) -> bool: + """ + 显示编辑器窗口 + + Returns: + 是否显示成功 + """ + try: + # 在实际实现中,这里会显示实际的编辑器窗口 + self.editor_stats["windows_opened"] += 1 + print("✓ 编辑器窗口已显示") + return True + except Exception as e: + print(f"✗ 编辑器窗口显示失败: {e}") + return False + + def hide_editor_window(self) -> bool: + """ + 隐藏编辑器窗口 + + Returns: + 是否隐藏成功 + """ + try: + # 在实际实现中,这里会隐藏实际的编辑器窗口 + print("✓ 编辑器窗口已隐藏") + return True + except Exception as e: + print(f"✗ 编辑器窗口隐藏失败: {e}") + return False + + def export_editor_data(self, file_path: str) -> bool: + """ + 导出编辑器数据 + + Args: + file_path: 导出文件路径 + + Returns: + 是否导出成功 + """ + try: + # 在实际实现中,这里会导出编辑器数据到文件 + print(f"✓ 编辑器数据已导出: {file_path}") + return True + except Exception as e: + print(f"✗ 编辑器数据导出失败: {e}") + return False + + def import_editor_data(self, file_path: str) -> bool: + """ + 导入编辑器数据 + + Args: + file_path: 导入文件路径 + + Returns: + 是否导入成功 + """ + try: + # 在实际实现中,这里会从文件导入编辑器数据 + print(f"✓ 编辑器数据已导入: {file_path}") + return True + except Exception as e: + print(f"✗ 编辑器数据导入失败: {e}") + return False + + def run_comprehensive_test(self) -> Dict[str, Any]: + """ + 运行综合测试 + + Returns: + 测试结果字典 + """ + try: + print("✓ 开始综合测试...") + + test_results = { + "timestamp": time.time(), + "tests": {}, + "summary": { + "total_tests": 0, + "passed_tests": 0, + "failed_tests": 0, + "success_rate": 0.0 + } + } + + # 测试语音识别 + test_results["tests"]["speech_recognition"] = { + "status": "passed" if self.test_speech_recognition() else "failed", + "details": "语音识别功能测试" + } + + # 测试语音合成 + test_results["tests"]["speech_synthesis"] = { + "status": "passed" if self.test_speech_synthesis("测试语音合成功能") else "failed", + "details": "语音合成功能测试" + } + + # 测试命令执行 + test_results["tests"]["command_execution"] = { + "status": "passed" if self.test_command_execution("前进") else "failed", + "details": "命令执行功能测试" + } + + # 统计结果 + total_tests = len(test_results["tests"]) + passed_tests = sum(1 for test in test_results["tests"].values() if test["status"] == "passed") + + test_results["summary"]["total_tests"] = total_tests + test_results["summary"]["passed_tests"] = passed_tests + test_results["summary"]["failed_tests"] = total_tests - passed_tests + test_results["summary"]["success_rate"] = passed_tests / total_tests if total_tests > 0 else 0.0 + + print(f"✓ 综合测试完成: {passed_tests}/{total_tests} 通过") + return test_results + + except Exception as e: + print(f"✗ 综合测试失败: {e}") + import traceback + traceback.print_exc() + return { + "timestamp": time.time(), + "tests": {}, + "summary": { + "total_tests": 0, + "passed_tests": 0, + "failed_tests": 0, + "success_rate": 0.0, + "error": str(e) + } } \ No newline at end of file diff --git a/plugins/user/voice_control/events/event_handler.py b/plugins/user/voice_control/events/event_handler.py index 97b7b571..8dcf07dc 100644 --- a/plugins/user/voice_control/events/event_handler.py +++ b/plugins/user/voice_control/events/event_handler.py @@ -1,739 +1,739 @@ -""" -事件处理器模块 -负责处理语音控制相关的事件 -""" - -import time -from typing import Dict, Any, List, Optional, Callable -import threading -import queue - -class VoiceEventHandler: - """ - 事件处理器 - 负责处理语音控制相关的事件 - """ - - def __init__(self, plugin): - """ - 初始化事件处理器 - - Args: - plugin: 语音控制插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 事件队列 - self.event_queue = queue.Queue(maxsize=100) - - # 事件监听器 - self.event_listeners = {} - - # 事件配置 - self.event_config = { - 'enable_event_queue': True, - 'max_queue_size': 100, - 'enable_event_filtering': True, - 'filter_duplicate_events': True, - 'duplicate_time_window': 1.0, - 'enable_async_processing': True, - 'max_processing_threads': 4 - } - - # 事件统计 - self.event_stats = { - 'events_received': 0, - 'events_processed': 0, - 'events_dropped': 0, - 'events_filtered': 0, - 'processing_errors': 0, - 'average_processing_time': 0.0 - } - - # 事件历史 - self.event_history = [] - self.max_history_size = 1000 - - # 线程管理 - self.processing_threads = [] - self.running = False - - # 回调函数 - self.event_callbacks = { - 'event_received': [], - 'event_processed': [], - 'event_error': [], - 'event_queue_full': [] - } - - # 时间戳记录 - self.last_event_time = 0.0 - self.last_processing_time = 0.0 - - # 重复事件跟踪 - self.recent_events = {} - - print("✓ 事件处理器已创建") - - def initialize(self) -> bool: - """ - 初始化事件处理器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 事件处理器初始化完成") - return True - - except Exception as e: - print(f"✗ 事件处理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用事件处理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 事件处理器未初始化") - return False - - self.enabled = True - self.running = True - - # 启动处理线程 - if self.event_config['enable_async_processing']: - thread_count = min(self.event_config['max_processing_threads'], 4) - for i in range(thread_count): - thread = threading.Thread(target=self._processing_loop, daemon=True, - name=f"EventProcessor-{i}") - self.processing_threads.append(thread) - thread.start() - - print("✓ 事件处理器已启用") - return True - - except Exception as e: - print(f"✗ 事件处理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用事件处理器""" - try: - self.enabled = False - self.running = False - - # 等待处理线程结束 - for thread in self.processing_threads: - if thread.is_alive(): - thread.join(timeout=1.0) - - self.processing_threads.clear() - - print("✓ 事件处理器已禁用") - - except Exception as e: - print(f"✗ 事件处理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理事件处理器资源""" - try: - self.disable() - self.event_listeners.clear() - self.event_callbacks.clear() - - # 清空事件队列 - while not self.event_queue.empty(): - try: - self.event_queue.get_nowait() - except queue.Empty: - break - - self.event_history.clear() - self.recent_events.clear() - self.initialized = False - print("✓ 事件处理器资源已清理") - - except Exception as e: - print(f"✗ 事件处理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新事件处理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.last_event_time = current_time - - # 清理过期的重复事件记录 - self._cleanup_recent_events(current_time) - - except Exception as e: - print(f"✗ 事件处理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _processing_loop(self): - """事件处理循环""" - try: - while self.running and self.enabled: - try: - # 从队列获取事件 - if not self.event_queue.empty(): - event_data = self.event_queue.get_nowait() - self._process_event(event_data) - else: - time.sleep(0.01) - - except queue.Empty: - time.sleep(0.01) - except Exception as e: - print(f"✗ 事件处理循环错误: {e}") - time.sleep(0.1) - - except Exception as e: - print(f"✗ 事件处理循环失败: {e}") - - def _cleanup_recent_events(self, current_time: float): - """清理过期的重复事件记录""" - try: - expired_events = [ - event_key for event_key, event_time in self.recent_events.items() - if current_time - event_time > self.event_config['duplicate_time_window'] - ] - - for event_key in expired_events: - del self.recent_events[event_key] - - except Exception as e: - print(f"✗ 重复事件清理失败: {e}") - - def post_event(self, event_type: str, event_data: Dict[str, Any] = None) -> bool: - """ - 发布事件 - - Args: - event_type: 事件类型 - event_data: 事件数据 - - Returns: - 是否发布成功 - """ - try: - if not self.enabled: - print("✗ 事件处理器未启用") - return False - - # 创建事件对象 - event = { - 'type': event_type, - 'data': event_data or {}, - 'timestamp': time.time(), - 'id': f"{event_type}_{int(time.time() * 1000000)}" - } - - # 触发事件接收回调 - self._trigger_event_callback('event_received', event) - - # 更新统计信息 - self.event_stats['events_received'] += 1 - - # 事件过滤 - if self.event_config['enable_event_filtering']: - if not self._filter_event(event): - self.event_stats['events_filtered'] += 1 - return True - - # 检查重复事件 - if self.event_config['filter_duplicate_events']: - if self._is_duplicate_event(event): - self.event_stats['events_filtered'] += 1 - return True - - # 添加到事件队列 - if self.event_config['enable_event_queue']: - try: - self.event_queue.put_nowait(event) - - # 记录事件历史 - self._record_event_history(event) - - return True - except queue.Full: - self.event_stats['events_dropped'] += 1 - print("✗ 事件队列已满,丢弃事件") - - # 触发队列满回调 - self._trigger_event_callback('event_queue_full', { - 'event': event, - 'queue_size': self.event_queue.qsize() - }) - - return False - else: - # 直接处理事件 - return self._process_event(event) - - except Exception as e: - print(f"✗ 事件发布失败: {e}") - import traceback - traceback.print_exc() - return False - - def _filter_event(self, event: Dict[str, Any]) -> bool: - """ - 过滤事件 - - Args: - event: 事件对象 - - Returns: - 是否通过过滤 - """ - try: - # 这里可以实现具体的事件过滤逻辑 - # 例如:根据事件类型、优先级等进行过滤 - - # 示例:过滤掉低置信度的语音识别事件 - if event['type'] == 'speech_recognized': - confidence = event['data'].get('confidence', 1.0) - if confidence < 0.3: # 置信度阈值 - return False - - return True - - except Exception as e: - print(f"✗ 事件过滤失败: {e}") - return True # 默认通过过滤 - - def _is_duplicate_event(self, event: Dict[str, Any]) -> bool: - """ - 检查是否为重复事件 - - Args: - event: 事件对象 - - Returns: - 是否为重复事件 - """ - try: - # 创建事件键 - event_key = f"{event['type']}_{hash(str(event['data']))}" - current_time = time.time() - - # 检查是否在时间窗口内已存在相同事件 - if event_key in self.recent_events: - last_time = self.recent_events[event_key] - if current_time - last_time < self.event_config['duplicate_time_window']: - return True - - # 记录当前事件时间 - self.recent_events[event_key] = current_time - return False - - except Exception as e: - print(f"✗ 重复事件检查失败: {e}") - return False - - def _process_event(self, event: Dict[str, Any]) -> bool: - """ - 处理事件 - - Args: - event: 事件对象 - - Returns: - 是否处理成功 - """ - try: - start_time = time.time() - - event_type = event['type'] - event_data = event['data'] - - # 查找事件监听器 - listeners = self.event_listeners.get(event_type, []) - - if listeners: - # 调用所有监听器 - for listener in listeners: - try: - listener(event_type, event_data) - except Exception as e: - print(f"✗ 事件监听器执行失败: {e}") - self.event_stats['processing_errors'] += 1 - - # 触发事件错误回调 - self._trigger_event_callback('event_error', { - 'event': event, - 'error': str(e), - 'listener': listener.__name__ if hasattr(listener, '__name__') else 'unknown' - }) - - # 更新统计信息 - self.event_stats['events_processed'] += 1 - - # 触发事件处理回调 - self._trigger_event_callback('event_processed', event) - - # 计算处理时间 - processing_time = time.time() - start_time - self.event_stats['average_processing_time'] = ( - (self.event_stats['average_processing_time'] * (self.event_stats['events_processed'] - 1) + - processing_time) / self.event_stats['events_processed'] - ) - - return True - else: - # 没有监听器,但事件仍被处理 - self.event_stats['events_processed'] += 1 - self._trigger_event_callback('event_processed', event) - return True - - except Exception as e: - print(f"✗ 事件处理失败: {e}") - import traceback - traceback.print_exc() - self.event_stats['processing_errors'] += 1 - - # 触发事件错误回调 - self._trigger_event_callback('event_error', { - 'event': event, - 'error': str(e), - 'listener': 'processor' - }) - - return False - - def _record_event_history(self, event: Dict[str, Any]): - """ - 记录事件历史 - - Args: - event: 事件对象 - """ - try: - self.event_history.append(event) - - # 保持历史记录大小 - if len(self.event_history) > self.max_history_size: - self.event_history.pop(0) - - except Exception as e: - print(f"✗ 事件历史记录失败: {e}") - - def register_event_listener(self, event_type: str, listener: Callable) -> bool: - """ - 注册事件监听器 - - Args: - event_type: 事件类型 - listener: 监听器函数 - - Returns: - 是否注册成功 - """ - try: - if event_type not in self.event_listeners: - self.event_listeners[event_type] = [] - - self.event_listeners[event_type].append(listener) - print(f"✓ 事件监听器已注册: {event_type}") - return True - - except Exception as e: - print(f"✗ 事件监听器注册失败: {e}") - return False - - def unregister_event_listener(self, event_type: str, listener: Callable) -> bool: - """ - 注销事件监听器 - - Args: - event_type: 事件类型 - listener: 监听器函数 - - Returns: - 是否注销成功 - """ - try: - if event_type in self.event_listeners: - if listener in self.event_listeners[event_type]: - self.event_listeners[event_type].remove(listener) - print(f"✓ 事件监听器已注销: {event_type}") - return True - - print(f"✗ 事件监听器不存在: {event_type}") - return False - - except Exception as e: - print(f"✗ 事件监听器注销失败: {e}") - return False - - def get_event_listeners(self) -> Dict[str, List[Callable]]: - """ - 获取事件监听器 - - Returns: - 事件监听器字典 - """ - return {k: v.copy() for k, v in self.event_listeners.items()} - - def get_event_queue_size(self) -> int: - """ - 获取事件队列大小 - - Returns: - 队列大小 - """ - return self.event_queue.qsize() - - def clear_event_queue(self) -> bool: - """ - 清空事件队列 - - Returns: - 是否清空成功 - """ - try: - while not self.event_queue.empty(): - try: - self.event_queue.get_nowait() - except queue.Empty: - break - - print("✓ 事件队列已清空") - return True - - except Exception as e: - print(f"✗ 事件队列清空失败: {e}") - return False - - def get_event_history(self) -> List[Dict[str, Any]]: - """ - 获取事件历史 - - Returns: - 事件历史列表 - """ - return self.event_history.copy() - - def clear_event_history(self): - """清空事件历史""" - try: - self.event_history.clear() - print("✓ 事件历史已清空") - except Exception as e: - print(f"✗ 事件历史清空失败: {e}") - - def set_event_config(self, config: Dict[str, Any]) -> bool: - """ - 设置事件配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.event_config.update(config) - print(f"✓ 事件配置已更新: {self.event_config}") - return True - except Exception as e: - print(f"✗ 事件配置设置失败: {e}") - return False - - def get_event_config(self) -> Dict[str, Any]: - """ - 获取事件配置 - - Returns: - 配置字典 - """ - return self.event_config.copy() - - def _trigger_event_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发事件回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.event_callbacks: - for callback in self.event_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 事件回调执行失败: {e}") - except Exception as e: - print(f"✗ 事件回调触发失败: {e}") - - def register_event_callback(self, callback_type: str, callback: Callable): - """ - 注册事件回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.event_callbacks: - self.event_callbacks[callback_type].append(callback) - print(f"✓ 事件回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 事件回调注册失败: {e}") - - def unregister_event_callback(self, callback_type: str, callback: Callable): - """ - 注销事件回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.event_callbacks: - if callback in self.event_callbacks[callback_type]: - self.event_callbacks[callback_type].remove(callback) - print(f"✓ 事件回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 事件回调注销失败: {e}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.event_stats.copy() - - def reset_stats(self): - """重置统计信息""" - try: - self.event_stats = { - 'events_received': 0, - 'events_processed': 0, - 'events_dropped': 0, - 'events_filtered': 0, - 'processing_errors': 0, - 'average_processing_time': 0.0 - } - print("✓ 事件处理器统计信息已重置") - except Exception as e: - print(f"✗ 事件处理器统计信息重置失败: {e}") - - # 常用事件处理方法 - def handle_speech_recognized(self, event_type: str, event_data: Dict[str, Any]): - """ - 处理语音识别事件 - - Args: - event_type: 事件类型 - event_data: 事件数据 - """ - try: - text = event_data.get('text', '') - confidence = event_data.get('confidence', 0.0) - - print(f"🎤 语音识别事件: '{text}' (置信度: {confidence:.2f})") - - # 传递给命令处理器 - if self.plugin.command_processor: - self.plugin.command_processor.process_command(text, confidence) - - except Exception as e: - print(f"✗ 语音识别事件处理失败: {e}") - - def handle_speech_synthesis_started(self, event_type: str, event_data: Dict[str, Any]): - """ - 处理语音合成开始事件 - - Args: - event_type: 事件类型 - event_data: 事件数据 - """ - try: - text = event_data.get('text', '') - print(f"🔊 语音合成开始: '{text}'") - except Exception as e: - print(f"✗ 语音合成开始事件处理失败: {e}") - - def handle_speech_synthesis_completed(self, event_type: str, event_data: Dict[str, Any]): - """ - 处理语音合成完成事件 - - Args: - event_type: 事件类型 - event_data: 事件数据 - """ - try: - text = event_data.get('text', '') - print(f"✅ 语音合成完成: '{text}'") - except Exception as e: - print(f"✗ 语音合成完成事件处理失败: {e}") - - def handle_wake_word_detected(self, event_type: str, event_data: Dict[str, Any]): - """ - 处理唤醒词检测事件 - - Args: - event_type: 事件类型 - event_data: 事件数据 - """ - try: - text = event_data.get('text', '') - confidence = event_data.get('confidence', 0.0) - print(f"🔔 唤醒词检测: '{text}' (置信度: {confidence:.2f})") - - # 可以在这里添加特殊处理逻辑 - if self.plugin.speech_synthesizer: - # 简单确认音 - pass - - except Exception as e: - print(f"✗ 唤醒词检测事件处理失败: {e}") - - def handle_command_processed(self, event_type: str, event_data: Dict[str, Any]): - """ - 处理命令处理事件 - - Args: - event_type: 事件类型 - event_data: 事件数据 - """ - try: - command_text = event_data.get('command_text', '') - original_text = event_data.get('original_text', '') - confidence = event_data.get('confidence', 0.0) - match_type = event_data.get('match_type', 'unknown') - - print(f"⚙️ 命令处理: '{command_text}' (原始: '{original_text}', 匹配: {match_type}, 置信度: {confidence:.2f})") - except Exception as e: +""" +事件处理器模块 +负责处理语音控制相关的事件 +""" + +import time +from typing import Dict, Any, List, Optional, Callable +import threading +import queue + +class VoiceEventHandler: + """ + 事件处理器 + 负责处理语音控制相关的事件 + """ + + def __init__(self, plugin): + """ + 初始化事件处理器 + + Args: + plugin: 语音控制插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 事件队列 + self.event_queue = queue.Queue(maxsize=100) + + # 事件监听器 + self.event_listeners = {} + + # 事件配置 + self.event_config = { + 'enable_event_queue': True, + 'max_queue_size': 100, + 'enable_event_filtering': True, + 'filter_duplicate_events': True, + 'duplicate_time_window': 1.0, + 'enable_async_processing': True, + 'max_processing_threads': 4 + } + + # 事件统计 + self.event_stats = { + 'events_received': 0, + 'events_processed': 0, + 'events_dropped': 0, + 'events_filtered': 0, + 'processing_errors': 0, + 'average_processing_time': 0.0 + } + + # 事件历史 + self.event_history = [] + self.max_history_size = 1000 + + # 线程管理 + self.processing_threads = [] + self.running = False + + # 回调函数 + self.event_callbacks = { + 'event_received': [], + 'event_processed': [], + 'event_error': [], + 'event_queue_full': [] + } + + # 时间戳记录 + self.last_event_time = 0.0 + self.last_processing_time = 0.0 + + # 重复事件跟踪 + self.recent_events = {} + + print("✓ 事件处理器已创建") + + def initialize(self) -> bool: + """ + 初始化事件处理器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 事件处理器初始化完成") + return True + + except Exception as e: + print(f"✗ 事件处理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用事件处理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 事件处理器未初始化") + return False + + self.enabled = True + self.running = True + + # 启动处理线程 + if self.event_config['enable_async_processing']: + thread_count = min(self.event_config['max_processing_threads'], 4) + for i in range(thread_count): + thread = threading.Thread(target=self._processing_loop, daemon=True, + name=f"EventProcessor-{i}") + self.processing_threads.append(thread) + thread.start() + + print("✓ 事件处理器已启用") + return True + + except Exception as e: + print(f"✗ 事件处理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用事件处理器""" + try: + self.enabled = False + self.running = False + + # 等待处理线程结束 + for thread in self.processing_threads: + if thread.is_alive(): + thread.join(timeout=1.0) + + self.processing_threads.clear() + + print("✓ 事件处理器已禁用") + + except Exception as e: + print(f"✗ 事件处理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理事件处理器资源""" + try: + self.disable() + self.event_listeners.clear() + self.event_callbacks.clear() + + # 清空事件队列 + while not self.event_queue.empty(): + try: + self.event_queue.get_nowait() + except queue.Empty: + break + + self.event_history.clear() + self.recent_events.clear() + self.initialized = False + print("✓ 事件处理器资源已清理") + + except Exception as e: + print(f"✗ 事件处理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新事件处理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.last_event_time = current_time + + # 清理过期的重复事件记录 + self._cleanup_recent_events(current_time) + + except Exception as e: + print(f"✗ 事件处理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _processing_loop(self): + """事件处理循环""" + try: + while self.running and self.enabled: + try: + # 从队列获取事件 + if not self.event_queue.empty(): + event_data = self.event_queue.get_nowait() + self._process_event(event_data) + else: + time.sleep(0.01) + + except queue.Empty: + time.sleep(0.01) + except Exception as e: + print(f"✗ 事件处理循环错误: {e}") + time.sleep(0.1) + + except Exception as e: + print(f"✗ 事件处理循环失败: {e}") + + def _cleanup_recent_events(self, current_time: float): + """清理过期的重复事件记录""" + try: + expired_events = [ + event_key for event_key, event_time in self.recent_events.items() + if current_time - event_time > self.event_config['duplicate_time_window'] + ] + + for event_key in expired_events: + del self.recent_events[event_key] + + except Exception as e: + print(f"✗ 重复事件清理失败: {e}") + + def post_event(self, event_type: str, event_data: Dict[str, Any] = None) -> bool: + """ + 发布事件 + + Args: + event_type: 事件类型 + event_data: 事件数据 + + Returns: + 是否发布成功 + """ + try: + if not self.enabled: + print("✗ 事件处理器未启用") + return False + + # 创建事件对象 + event = { + 'type': event_type, + 'data': event_data or {}, + 'timestamp': time.time(), + 'id': f"{event_type}_{int(time.time() * 1000000)}" + } + + # 触发事件接收回调 + self._trigger_event_callback('event_received', event) + + # 更新统计信息 + self.event_stats['events_received'] += 1 + + # 事件过滤 + if self.event_config['enable_event_filtering']: + if not self._filter_event(event): + self.event_stats['events_filtered'] += 1 + return True + + # 检查重复事件 + if self.event_config['filter_duplicate_events']: + if self._is_duplicate_event(event): + self.event_stats['events_filtered'] += 1 + return True + + # 添加到事件队列 + if self.event_config['enable_event_queue']: + try: + self.event_queue.put_nowait(event) + + # 记录事件历史 + self._record_event_history(event) + + return True + except queue.Full: + self.event_stats['events_dropped'] += 1 + print("✗ 事件队列已满,丢弃事件") + + # 触发队列满回调 + self._trigger_event_callback('event_queue_full', { + 'event': event, + 'queue_size': self.event_queue.qsize() + }) + + return False + else: + # 直接处理事件 + return self._process_event(event) + + except Exception as e: + print(f"✗ 事件发布失败: {e}") + import traceback + traceback.print_exc() + return False + + def _filter_event(self, event: Dict[str, Any]) -> bool: + """ + 过滤事件 + + Args: + event: 事件对象 + + Returns: + 是否通过过滤 + """ + try: + # 这里可以实现具体的事件过滤逻辑 + # 例如:根据事件类型、优先级等进行过滤 + + # 示例:过滤掉低置信度的语音识别事件 + if event['type'] == 'speech_recognized': + confidence = event['data'].get('confidence', 1.0) + if confidence < 0.3: # 置信度阈值 + return False + + return True + + except Exception as e: + print(f"✗ 事件过滤失败: {e}") + return True # 默认通过过滤 + + def _is_duplicate_event(self, event: Dict[str, Any]) -> bool: + """ + 检查是否为重复事件 + + Args: + event: 事件对象 + + Returns: + 是否为重复事件 + """ + try: + # 创建事件键 + event_key = f"{event['type']}_{hash(str(event['data']))}" + current_time = time.time() + + # 检查是否在时间窗口内已存在相同事件 + if event_key in self.recent_events: + last_time = self.recent_events[event_key] + if current_time - last_time < self.event_config['duplicate_time_window']: + return True + + # 记录当前事件时间 + self.recent_events[event_key] = current_time + return False + + except Exception as e: + print(f"✗ 重复事件检查失败: {e}") + return False + + def _process_event(self, event: Dict[str, Any]) -> bool: + """ + 处理事件 + + Args: + event: 事件对象 + + Returns: + 是否处理成功 + """ + try: + start_time = time.time() + + event_type = event['type'] + event_data = event['data'] + + # 查找事件监听器 + listeners = self.event_listeners.get(event_type, []) + + if listeners: + # 调用所有监听器 + for listener in listeners: + try: + listener(event_type, event_data) + except Exception as e: + print(f"✗ 事件监听器执行失败: {e}") + self.event_stats['processing_errors'] += 1 + + # 触发事件错误回调 + self._trigger_event_callback('event_error', { + 'event': event, + 'error': str(e), + 'listener': listener.__name__ if hasattr(listener, '__name__') else 'unknown' + }) + + # 更新统计信息 + self.event_stats['events_processed'] += 1 + + # 触发事件处理回调 + self._trigger_event_callback('event_processed', event) + + # 计算处理时间 + processing_time = time.time() - start_time + self.event_stats['average_processing_time'] = ( + (self.event_stats['average_processing_time'] * (self.event_stats['events_processed'] - 1) + + processing_time) / self.event_stats['events_processed'] + ) + + return True + else: + # 没有监听器,但事件仍被处理 + self.event_stats['events_processed'] += 1 + self._trigger_event_callback('event_processed', event) + return True + + except Exception as e: + print(f"✗ 事件处理失败: {e}") + import traceback + traceback.print_exc() + self.event_stats['processing_errors'] += 1 + + # 触发事件错误回调 + self._trigger_event_callback('event_error', { + 'event': event, + 'error': str(e), + 'listener': 'processor' + }) + + return False + + def _record_event_history(self, event: Dict[str, Any]): + """ + 记录事件历史 + + Args: + event: 事件对象 + """ + try: + self.event_history.append(event) + + # 保持历史记录大小 + if len(self.event_history) > self.max_history_size: + self.event_history.pop(0) + + except Exception as e: + print(f"✗ 事件历史记录失败: {e}") + + def register_event_listener(self, event_type: str, listener: Callable) -> bool: + """ + 注册事件监听器 + + Args: + event_type: 事件类型 + listener: 监听器函数 + + Returns: + 是否注册成功 + """ + try: + if event_type not in self.event_listeners: + self.event_listeners[event_type] = [] + + self.event_listeners[event_type].append(listener) + print(f"✓ 事件监听器已注册: {event_type}") + return True + + except Exception as e: + print(f"✗ 事件监听器注册失败: {e}") + return False + + def unregister_event_listener(self, event_type: str, listener: Callable) -> bool: + """ + 注销事件监听器 + + Args: + event_type: 事件类型 + listener: 监听器函数 + + Returns: + 是否注销成功 + """ + try: + if event_type in self.event_listeners: + if listener in self.event_listeners[event_type]: + self.event_listeners[event_type].remove(listener) + print(f"✓ 事件监听器已注销: {event_type}") + return True + + print(f"✗ 事件监听器不存在: {event_type}") + return False + + except Exception as e: + print(f"✗ 事件监听器注销失败: {e}") + return False + + def get_event_listeners(self) -> Dict[str, List[Callable]]: + """ + 获取事件监听器 + + Returns: + 事件监听器字典 + """ + return {k: v.copy() for k, v in self.event_listeners.items()} + + def get_event_queue_size(self) -> int: + """ + 获取事件队列大小 + + Returns: + 队列大小 + """ + return self.event_queue.qsize() + + def clear_event_queue(self) -> bool: + """ + 清空事件队列 + + Returns: + 是否清空成功 + """ + try: + while not self.event_queue.empty(): + try: + self.event_queue.get_nowait() + except queue.Empty: + break + + print("✓ 事件队列已清空") + return True + + except Exception as e: + print(f"✗ 事件队列清空失败: {e}") + return False + + def get_event_history(self) -> List[Dict[str, Any]]: + """ + 获取事件历史 + + Returns: + 事件历史列表 + """ + return self.event_history.copy() + + def clear_event_history(self): + """清空事件历史""" + try: + self.event_history.clear() + print("✓ 事件历史已清空") + except Exception as e: + print(f"✗ 事件历史清空失败: {e}") + + def set_event_config(self, config: Dict[str, Any]) -> bool: + """ + 设置事件配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.event_config.update(config) + print(f"✓ 事件配置已更新: {self.event_config}") + return True + except Exception as e: + print(f"✗ 事件配置设置失败: {e}") + return False + + def get_event_config(self) -> Dict[str, Any]: + """ + 获取事件配置 + + Returns: + 配置字典 + """ + return self.event_config.copy() + + def _trigger_event_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发事件回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.event_callbacks: + for callback in self.event_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 事件回调执行失败: {e}") + except Exception as e: + print(f"✗ 事件回调触发失败: {e}") + + def register_event_callback(self, callback_type: str, callback: Callable): + """ + 注册事件回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.event_callbacks: + self.event_callbacks[callback_type].append(callback) + print(f"✓ 事件回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 事件回调注册失败: {e}") + + def unregister_event_callback(self, callback_type: str, callback: Callable): + """ + 注销事件回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.event_callbacks: + if callback in self.event_callbacks[callback_type]: + self.event_callbacks[callback_type].remove(callback) + print(f"✓ 事件回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 事件回调注销失败: {e}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.event_stats.copy() + + def reset_stats(self): + """重置统计信息""" + try: + self.event_stats = { + 'events_received': 0, + 'events_processed': 0, + 'events_dropped': 0, + 'events_filtered': 0, + 'processing_errors': 0, + 'average_processing_time': 0.0 + } + print("✓ 事件处理器统计信息已重置") + except Exception as e: + print(f"✗ 事件处理器统计信息重置失败: {e}") + + # 常用事件处理方法 + def handle_speech_recognized(self, event_type: str, event_data: Dict[str, Any]): + """ + 处理语音识别事件 + + Args: + event_type: 事件类型 + event_data: 事件数据 + """ + try: + text = event_data.get('text', '') + confidence = event_data.get('confidence', 0.0) + + print(f"🎤 语音识别事件: '{text}' (置信度: {confidence:.2f})") + + # 传递给命令处理器 + if self.plugin.command_processor: + self.plugin.command_processor.process_command(text, confidence) + + except Exception as e: + print(f"✗ 语音识别事件处理失败: {e}") + + def handle_speech_synthesis_started(self, event_type: str, event_data: Dict[str, Any]): + """ + 处理语音合成开始事件 + + Args: + event_type: 事件类型 + event_data: 事件数据 + """ + try: + text = event_data.get('text', '') + print(f"🔊 语音合成开始: '{text}'") + except Exception as e: + print(f"✗ 语音合成开始事件处理失败: {e}") + + def handle_speech_synthesis_completed(self, event_type: str, event_data: Dict[str, Any]): + """ + 处理语音合成完成事件 + + Args: + event_type: 事件类型 + event_data: 事件数据 + """ + try: + text = event_data.get('text', '') + print(f"✅ 语音合成完成: '{text}'") + except Exception as e: + print(f"✗ 语音合成完成事件处理失败: {e}") + + def handle_wake_word_detected(self, event_type: str, event_data: Dict[str, Any]): + """ + 处理唤醒词检测事件 + + Args: + event_type: 事件类型 + event_data: 事件数据 + """ + try: + text = event_data.get('text', '') + confidence = event_data.get('confidence', 0.0) + print(f"🔔 唤醒词检测: '{text}' (置信度: {confidence:.2f})") + + # 可以在这里添加特殊处理逻辑 + if self.plugin.speech_synthesizer: + # 简单确认音 + pass + + except Exception as e: + print(f"✗ 唤醒词检测事件处理失败: {e}") + + def handle_command_processed(self, event_type: str, event_data: Dict[str, Any]): + """ + 处理命令处理事件 + + Args: + event_type: 事件类型 + event_data: 事件数据 + """ + try: + command_text = event_data.get('command_text', '') + original_text = event_data.get('original_text', '') + confidence = event_data.get('confidence', 0.0) + match_type = event_data.get('match_type', 'unknown') + + print(f"⚙️ 命令处理: '{command_text}' (原始: '{original_text}', 匹配: {match_type}, 置信度: {confidence:.2f})") + except Exception as e: print(f"✗ 命令处理事件处理失败: {e}") \ No newline at end of file diff --git a/plugins/user/voice_control/nlp/nlp_processor.py b/plugins/user/voice_control/nlp/nlp_processor.py index 3628b32d..10f2686e 100644 --- a/plugins/user/voice_control/nlp/nlp_processor.py +++ b/plugins/user/voice_control/nlp/nlp_processor.py @@ -1,1010 +1,1010 @@ -""" -自然语言处理模块 -负责处理和理解自然语言命令 -""" - -import time -import re -from typing import Dict, Any, List, Optional, Tuple -import threading - -class NLPProcessor: - """ - 自然语言处理器 - 负责处理和理解自然语言命令 - """ - - def __init__(self, plugin): - """ - 初始化自然语言处理器 - - Args: - plugin: 语音控制插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 语言配置 - self.language_config = { - 'primary_language': 'zh-CN', - 'supported_languages': ['zh-CN', 'en-US', 'ja-JP'], - 'enable_multi_language': True, - 'enable_dialect_detection': True - } - - # 意图识别 - self.intents = {} - - # 实体识别 - self.entities = {} - - # 语法分析器 - self.grammar_rules = {} - - # 词汇表 - self.vocabularies = {} - - # 上下文管理 - self.context_manager = { - 'current_context': 'default', - 'context_history': [], - 'context_variables': {}, - 'max_context_history': 10 - } - - # NLP配置 - self.nlp_config = { - 'enable_intent_recognition': True, - 'enable_entity_extraction': True, - 'enable_sentiment_analysis': True, - 'enable_context_awareness': True, - 'confidence_threshold': 0.6, - 'enable_fallback_processing': True, - 'fallback_threshold': 0.3 - } - - # 统计信息 - self.nlp_stats = { - 'texts_processed': 0, - 'intents_recognized': 0, - 'entities_extracted': 0, - 'processing_errors': 0, - 'fallback_used': 0, - 'average_processing_time': 0.0 - } - - # 缓存管理 - self.processing_cache = {} - self.cache_max_size = 1000 - - # 回调函数 - self.nlp_callbacks = { - 'text_processed': [], - 'intent_recognized': [], - 'entity_extracted': [], - 'context_updated': [], - 'processing_error': [] - } - - # 时间戳记录 - self.last_processing_time = 0.0 - self.last_cache_cleanup = 0.0 - - print("✓ 自然语言处理器已创建") - - def initialize(self) -> bool: - """ - 初始化自然语言处理器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化意图识别 - self._initialize_intents() - - # 初始化实体识别 - self._initialize_entities() - - # 初始化语法规则 - self._initialize_grammar_rules() - - # 初始化词汇表 - self._initialize_vocabularies() - - self.initialized = True - print("✓ 自然语言处理器初始化完成") - return True - - except Exception as e: - print(f"✗ 自然语言处理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用自然语言处理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 自然语言处理器未初始化") - return False - - self.enabled = True - print("✓ 自然语言处理器已启用") - return True - - except Exception as e: - print(f"✗ 自然语言处理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用自然语言处理器""" - try: - self.enabled = False - print("✓ 自然语言处理器已禁用") - - except Exception as e: - print(f"✗ 自然语言处理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理自然语言处理器资源""" - try: - self.disable() - self.intents.clear() - self.entities.clear() - self.grammar_rules.clear() - self.vocabularies.clear() - self.nlp_callbacks.clear() - self.processing_cache.clear() - self.initialized = False - print("✓ 自然语言处理器资源已清理") - - except Exception as e: - print(f"✗ 自然语言处理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新自然语言处理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.last_processing_time = current_time - - # 定期清理缓存 - if current_time - self.last_cache_cleanup > 60.0: # 每分钟清理一次 - self._cleanup_cache() - self.last_cache_cleanup = current_time - - except Exception as e: - print(f"✗ 自然语言处理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _initialize_intents(self): - """初始化意图识别""" - try: - self.intents = { - 'navigation': { - 'name': '导航', - 'description': '移动控制相关意图', - 'patterns': [ - r'(向前|往前|前进).*', - r'(向后|往后|后退).*', - r'(向左|左转).*', - r'(向右|右转).*', - r'(停止|停).*' - ], - 'actions': ['move_forward', 'move_backward', 'turn_left', 'turn_right', 'stop'] - }, - 'system_control': { - 'name': '系统控制', - 'description': '系统控制相关意图', - 'patterns': [ - r'(打开|开启).*灯.*', - r'(关闭|关掉).*灯.*', - r'(增加|调高|提高).*音量.*', - r'(减少|调低|降低).*音量.*' - ], - 'actions': ['toggle_lights', 'adjust_volume'] - }, - 'entertainment': { - 'name': '娱乐控制', - 'description': '娱乐控制相关意图', - 'patterns': [ - r'(播放|开始播放).*音乐.*', - r'(暂停|停止播放).*音乐.*', - r'(下一首|下一首歌).*', - r'(上一首|上一首歌).*' - ], - 'actions': ['play_music', 'pause_music', 'next_track', 'previous_track'] - }, - 'interaction': { - 'name': '交互', - 'description': '日常交互相关意图', - 'patterns': [ - r'(你好|您好|hello).*', - r'(谢谢|感谢).*', - r'(再见|拜拜|bye).*', - r'(帮助|help).*', - r'(关于|信息).*' - ], - 'actions': ['greet', 'thank', 'goodbye', 'help', 'about'] - }, - 'query': { - 'name': '查询', - 'description': '信息查询相关意图', - 'patterns': [ - r'(时间|几点).*', - r'(日期|今天).*', - r'(天气|气温).*', - r'(状态|情况).*' - ], - 'actions': ['query_time', 'query_date', 'query_weather', 'query_status'] - } - } - - print("✓ 意图识别初始化完成") - - except Exception as e: - print(f"✗ 意图识别初始化失败: {e}") - - def _initialize_entities(self): - """初始化实体识别""" - try: - self.entities = { - 'direction': { - 'name': '方向', - 'values': ['前', '后', '左', '右', 'forward', 'backward', 'left', 'right'] - }, - 'object': { - 'name': '对象', - 'values': ['灯', '音乐', '音量', 'light', 'music', 'volume'] - }, - 'action': { - 'name': '动作', - 'values': ['打开', '关闭', '增加', '减少', '播放', '暂停', 'open', 'close', 'increase', 'decrease', 'play', 'pause'] - }, - 'time_unit': { - 'name': '时间单位', - 'values': ['秒', '分钟', '小时', 'second', 'minute', 'hour'] - } - } - - print("✓ 实体识别初始化完成") - - except Exception as e: - print(f"✗ 实体识别初始化失败: {e}") - - def _initialize_grammar_rules(self): - """初始化语法规则""" - try: - self.grammar_rules = { - 'command_structure': { - 'name': '命令结构', - 'rules': [ - '动词+对象', - '对象+动词', - '动词+形容词+对象', - '请+动词+对象' - ] - }, - 'query_structure': { - 'name': '查询结构', - 'rules': [ - '疑问词+动词', - '疑问词+名词', - '动词+疑问词' - ] - } - } - - print("✓ 语法规则初始化完成") - - except Exception as e: - print(f"✗ 语法规则初始化失败: {e}") - - def _initialize_vocabularies(self): - """初始化词汇表""" - try: - self.vocabularies = { - 'zh-CN': { - 'greetings': ['你好', '您好', '早上好', '下午好', '晚上好'], - 'affirmatives': ['是', '对', '好', '可以', '行'], - 'negatives': ['不', '否', '不要', '不行', '不可以'], - 'confirmations': ['确定', '确认', '好的', '明白'], - 'questions': ['什么', '怎么', '为什么', '何时', '哪里'] - }, - 'en-US': { - 'greetings': ['hello', 'hi', 'good morning', 'good afternoon', 'good evening'], - 'affirmatives': ['yes', 'yeah', 'yep', 'okay', 'ok', 'sure'], - 'negatives': ['no', 'nope', 'not'], - 'confirmations': ['confirm', 'okay', 'alright', 'got it'], - 'questions': ['what', 'how', 'why', 'when', 'where'] - } - } - - print("✓ 词汇表初始化完成") - - except Exception as e: - print(f"✗ 词汇表初始化失败: {e}") - - def _cleanup_cache(self): - """清理缓存""" - try: - current_time = time.time() - expired_keys = [ - key for key, value in self.processing_cache.items() - if current_time - value['timestamp'] > 300.0 # 5分钟过期 - ] - - for key in expired_keys: - del self.processing_cache[key] - - # 保持缓存大小 - if len(self.processing_cache) > self.cache_max_size: - # 移除最旧的一半 - sorted_items = sorted(self.processing_cache.items(), - key=lambda x: x[1]['timestamp']) - keys_to_remove = [item[0] for item in sorted_items[:self.cache_max_size//2]] - - for key in keys_to_remove: - del self.processing_cache[key] - - except Exception as e: - print(f"✗ 缓存清理失败: {e}") - - def process_text(self, text: str, language: str = None) -> Dict[str, Any]: - """ - 处理文本 - - Args: - text: 要处理的文本 - language: 语言代码 - - Returns: - 处理结果字典 - """ - try: - if not self.enabled: - print("✗ 自然语言处理器未启用") - return self._create_error_result("NLP processor not enabled") - - start_time = time.time() - - # 使用默认语言 - if language is None: - language = self.language_config['primary_language'] - - # 检查缓存 - cache_key = f"{text}_{language}" - if cache_key in self.processing_cache: - cached_result = self.processing_cache[cache_key] - # 更新时间戳 - cached_result['timestamp'] = time.time() - self.nlp_stats['texts_processed'] += 1 - return cached_result['result'] - - # 文本预处理 - processed_text = self._preprocess_text(text, language) - - # 语言检测 - detected_language = self._detect_language(processed_text) - if detected_language != language and self.language_config['enable_dialect_detection']: - language = detected_language - - # 意图识别 - intent_result = self._recognize_intent(processed_text, language) - - # 实体提取 - entity_result = self._extract_entities(processed_text, language) - - # 上下文分析 - context_result = self._analyze_context(processed_text, language) - - # 情感分析 - sentiment_result = self._analyze_sentiment(processed_text, language) - - # 构建结果 - result = { - 'text': text, - 'processed_text': processed_text, - 'language': language, - 'intent': intent_result, - 'entities': entity_result, - 'context': context_result, - 'sentiment': sentiment_result, - 'timestamp': time.time(), - 'processing_time': time.time() - start_time - } - - # 缓存结果 - self.processing_cache[cache_key] = { - 'result': result, - 'timestamp': time.time() - } - - # 更新统计信息 - self.nlp_stats['texts_processed'] += 1 - if intent_result['confidence'] > self.nlp_config['confidence_threshold']: - self.nlp_stats['intents_recognized'] += 1 - self.nlp_stats['entities_extracted'] += len(entity_result.get('entities', [])) - - # 计算平均处理时间 - self.nlp_stats['average_processing_time'] = ( - (self.nlp_stats['average_processing_time'] * (self.nlp_stats['texts_processed'] - 1) + - result['processing_time']) / self.nlp_stats['texts_processed'] - ) - - # 触发文本处理回调 - self._trigger_nlp_callback('text_processed', result) - - return result - - except Exception as e: - print(f"✗ 文本处理失败: {e}") - import traceback - traceback.print_exc() - self.nlp_stats['processing_errors'] += 1 - - # 触发处理错误回调 - error_data = { - 'text': text, - 'error': str(e), - 'timestamp': time.time() - } - self._trigger_nlp_callback('processing_error', error_data) - - return self._create_error_result(str(e)) - - def _preprocess_text(self, text: str, language: str) -> str: - """ - 文本预处理 - - Args: - text: 原始文本 - language: 语言代码 - - Returns: - 预处理后的文本 - """ - try: - # 转换为小写(仅适用于英文) - if language == 'en-US': - text = text.lower() - - # 移除多余空格 - text = re.sub(r'\s+', ' ', text).strip() - - # 移除特殊字符(保留中文、英文、数字和基本标点) - if language == 'zh-CN': - text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9\s\.,!?;:]', '', text) - else: - text = re.sub(r'[^a-zA-Z0-9\s\.,!?;:]', '', text) - - return text - - except Exception as e: - print(f"✗ 文本预处理失败: {e}") - return text - - def _detect_language(self, text: str) -> str: - """ - 检测语言 - - Args: - text: 文本 - - Returns: - 语言代码 - """ - try: - # 简单的语言检测(基于字符集) - chinese_chars = len(re.findall(r'[\u4e00-\u9fa5]', text)) - english_chars = len(re.findall(r'[a-zA-Z]', text)) - - if chinese_chars > english_chars: - return 'zh-CN' - else: - return 'en-US' - - except Exception as e: - print(f"✗ 语言检测失败: {e}") - return self.language_config['primary_language'] - - def _recognize_intent(self, text: str, language: str) -> Dict[str, Any]: - """ - 意图识别 - - Args: - text: 文本 - language: 语言代码 - - Returns: - 意图识别结果 - """ - try: - best_intent = None - best_confidence = 0.0 - - # 遍历所有意图模式 - for intent_key, intent_data in self.intents.items(): - patterns = intent_data.get('patterns', []) - - for pattern in patterns: - if re.search(pattern, text, re.IGNORECASE): - # 计算匹配度 - confidence = self._calculate_match_confidence(text, pattern) - - if confidence > best_confidence: - best_confidence = confidence - best_intent = { - 'name': intent_key, - 'display_name': intent_data['name'], - 'description': intent_data['description'], - 'confidence': confidence, - 'actions': intent_data.get('actions', []) - } - - if best_intent and best_confidence >= self.nlp_config['confidence_threshold']: - # 触发意图识别回调 - self._trigger_nlp_callback('intent_recognized', best_intent) - return best_intent - elif (best_intent and - best_confidence >= self.nlp_config['fallback_threshold'] and - self.nlp_config['enable_fallback_processing']): - # 使用回退处理 - self.nlp_stats['fallback_used'] += 1 - best_intent['confidence'] = best_confidence - return best_intent - else: - return { - 'name': 'unknown', - 'display_name': '未知意图', - 'description': '无法识别的意图', - 'confidence': 0.0, - 'actions': [] - } - - except Exception as e: - print(f"✗ 意图识别失败: {e}") - return { - 'name': 'error', - 'display_name': '处理错误', - 'description': f'意图识别错误: {e}', - 'confidence': 0.0, - 'actions': [] - } - - def _calculate_match_confidence(self, text: str, pattern: str) -> float: - """ - 计算匹配置信度 - - Args: - text: 文本 - pattern: 模式 - - Returns: - 置信度(0.0-1.0) - """ - try: - # 简单的匹配度计算 - match = re.search(pattern, text, re.IGNORECASE) - if match: - # 基于匹配长度和文本长度的比例 - match_length = len(match.group(0)) - text_length = len(text) - if text_length > 0: - return min(1.0, match_length / text_length) - return 0.0 - except Exception as e: - print(f"✗ 匹配置信度计算失败: {e}") - return 0.0 - - def _extract_entities(self, text: str, language: str) -> Dict[str, Any]: - """ - 实体提取 - - Args: - text: 文本 - language: 语言代码 - - Returns: - 实体提取结果 - """ - try: - entities = [] - - # 遍历所有实体类型 - for entity_key, entity_data in self.entities.items(): - values = entity_data.get('values', []) - - for value in values: - if value in text: - entities.append({ - 'type': entity_key, - 'display_name': entity_data['name'], - 'value': value, - 'position': text.find(value) - }) - - if entities: - # 触发实体提取回调 - for entity in entities: - self._trigger_nlp_callback('entity_extracted', entity) - - return { - 'entities': entities, - 'count': len(entities) - } - - except Exception as e: - print(f"✗ 实体提取失败: {e}") - return { - 'entities': [], - 'count': 0 - } - - def _analyze_context(self, text: str, language: str) -> Dict[str, Any]: - """ - 上下文分析 - - Args: - text: 文本 - language: 语言代码 - - Returns: - 上下文分析结果 - """ - try: - # 检查是否与当前上下文相关 - context_relevant = False - - # 这里可以实现更复杂的上下文分析逻辑 - # 例如:检查是否提及之前的对话内容等 - - return { - 'current_context': self.context_manager['current_context'], - 'context_relevant': context_relevant, - 'context_variables': self.context_manager['context_variables'].copy() - } - - except Exception as e: - print(f"✗ 上下文分析失败: {e}") - return { - 'current_context': 'default', - 'context_relevant': False, - 'context_variables': {} - } - - def _analyze_sentiment(self, text: str, language: str) -> Dict[str, Any]: - """ - 情感分析 - - Args: - text: 文本 - language: 语言代码 - - Returns: - 情感分析结果 - """ - try: - # 简单的情感分析 - positive_words = ['好', '棒', '喜欢', '开心', '高兴', 'good', 'great', 'happy', 'like'] - negative_words = ['坏', '差', '讨厌', '生气', '难过', 'bad', 'terrible', 'angry', 'sad'] - - positive_count = sum(1 for word in positive_words if word in text) - negative_count = sum(1 for word in negative_words if word in text) - - if positive_count > negative_count: - sentiment = 'positive' - confidence = positive_count / (positive_count + negative_count + 1) - elif negative_count > positive_count: - sentiment = 'negative' - confidence = negative_count / (positive_count + negative_count + 1) - else: - sentiment = 'neutral' - confidence = 0.5 - - return { - 'sentiment': sentiment, - 'confidence': confidence, - 'positive_words': positive_count, - 'negative_words': negative_count - } - - except Exception as e: - print(f"✗ 情感分析失败: {e}") - return { - 'sentiment': 'neutral', - 'confidence': 0.0, - 'positive_words': 0, - 'negative_words': 0 - } - - def _create_error_result(self, error_message: str) -> Dict[str, Any]: - """ - 创建错误结果 - - Args: - error_message: 错误消息 - - Returns: - 错误结果字典 - """ - return { - 'text': '', - 'processed_text': '', - 'language': self.language_config['primary_language'], - 'intent': { - 'name': 'error', - 'display_name': '处理错误', - 'description': error_message, - 'confidence': 0.0, - 'actions': [] - }, - 'entities': { - 'entities': [], - 'count': 0 - }, - 'context': { - 'current_context': 'default', - 'context_relevant': False, - 'context_variables': {} - }, - 'sentiment': { - 'sentiment': 'neutral', - 'confidence': 0.0, - 'positive_words': 0, - 'negative_words': 0 - }, - 'timestamp': time.time(), - 'processing_time': 0.0, - 'error': error_message - } - - def update_context(self, context_name: str, variables: Dict[str, Any] = None) -> bool: - """ - 更新上下文 - - Args: - context_name: 上下文名称 - variables: 上下文变量 - - Returns: - 是否更新成功 - """ - try: - # 保存当前上下文到历史 - self.context_manager['context_history'].append({ - 'context': self.context_manager['current_context'], - 'variables': self.context_manager['context_variables'].copy(), - 'timestamp': time.time() - }) - - # 保持历史记录大小 - if len(self.context_manager['context_history']) > self.context_manager['max_context_history']: - self.context_manager['context_history'].pop(0) - - # 更新当前上下文 - self.context_manager['current_context'] = context_name - if variables: - self.context_manager['context_variables'] = variables - else: - self.context_manager['context_variables'] = {} - - # 触发上下文更新回调 - self._trigger_nlp_callback('context_updated', { - 'context': context_name, - 'variables': variables - }) - - print(f"✓ 上下文已更新为: {context_name}") - return True - - except Exception as e: - print(f"✗ 上下文更新失败: {e}") - return False - - def get_current_context(self) -> Dict[str, Any]: - """ - 获取当前上下文 - - Returns: - 当前上下文字典 - """ - return { - 'context': self.context_manager['current_context'], - 'variables': self.context_manager['context_variables'].copy(), - 'history': self.context_manager['context_history'].copy() - } - - def clear_context(self) -> bool: - """ - 清空上下文 - - Returns: - 是否清空成功 - """ - try: - self.context_manager['current_context'] = 'default' - self.context_manager['context_variables'] = {} - self.context_manager['context_history'] = [] - print("✓ 上下文已清空") - return True - except Exception as e: - print(f"✗ 上下文清空失败: {e}") - return False - - def get_intents(self) -> Dict[str, Any]: - """ - 获取意图定义 - - Returns: - 意图定义字典 - """ - return self.intents.copy() - - def get_entities(self) -> Dict[str, Any]: - """ - 获取实体定义 - - Returns: - 实体定义字典 - """ - return self.entities.copy() - - def get_vocabularies(self) -> Dict[str, Any]: - """ - 获取词汇表 - - Returns: - 词汇表字典 - """ - return self.vocabularies.copy() - - def set_language_config(self, config: Dict[str, Any]) -> bool: - """ - 设置语言配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.language_config.update(config) - print(f"✓ 语言配置已更新: {self.language_config}") - return True - except Exception as e: - print(f"✗ 语言配置设置失败: {e}") - return False - - def get_language_config(self) -> Dict[str, Any]: - """ - 获取语言配置 - - Returns: - 语言配置字典 - """ - return self.language_config.copy() - - def set_nlp_config(self, config: Dict[str, Any]) -> bool: - """ - 设置NLP配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.nlp_config.update(config) - print(f"✓ NLP配置已更新: {self.nlp_config}") - return True - except Exception as e: - print(f"✗ NLP配置设置失败: {e}") - return False - - def get_nlp_config(self) -> Dict[str, Any]: - """ - 获取NLP配置 - - Returns: - NLP配置字典 - """ - return self.nlp_config.copy() - - def _trigger_nlp_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发NLP回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.nlp_callbacks: - for callback in self.nlp_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ NLP回调执行失败: {e}") - except Exception as e: - print(f"✗ NLP回调触发失败: {e}") - - def register_nlp_callback(self, callback_type: str, callback: callable): - """ - 注册NLP回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.nlp_callbacks: - self.nlp_callbacks[callback_type].append(callback) - print(f"✓ NLP回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ NLP回调注册失败: {e}") - - def unregister_nlp_callback(self, callback_type: str, callback: callable): - """ - 注销NLP回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.nlp_callbacks: - if callback in self.nlp_callbacks[callback_type]: - self.nlp_callbacks[callback_type].remove(callback) - print(f"✓ NLP回调已注销: {callback_type}") - except Exception as e: - print(f"✗ NLP回调注销失败: {e}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.nlp_stats.copy() - - def reset_stats(self): - """重置统计信息""" - try: - self.nlp_stats = { - 'texts_processed': 0, - 'intents_recognized': 0, - 'entities_extracted': 0, - 'processing_errors': 0, - 'fallback_used': 0, - 'average_processing_time': 0.0 - } - print("✓ NLP处理器统计信息已重置") - except Exception as e: +""" +自然语言处理模块 +负责处理和理解自然语言命令 +""" + +import time +import re +from typing import Dict, Any, List, Optional, Tuple +import threading + +class NLPProcessor: + """ + 自然语言处理器 + 负责处理和理解自然语言命令 + """ + + def __init__(self, plugin): + """ + 初始化自然语言处理器 + + Args: + plugin: 语音控制插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 语言配置 + self.language_config = { + 'primary_language': 'zh-CN', + 'supported_languages': ['zh-CN', 'en-US', 'ja-JP'], + 'enable_multi_language': True, + 'enable_dialect_detection': True + } + + # 意图识别 + self.intents = {} + + # 实体识别 + self.entities = {} + + # 语法分析器 + self.grammar_rules = {} + + # 词汇表 + self.vocabularies = {} + + # 上下文管理 + self.context_manager = { + 'current_context': 'default', + 'context_history': [], + 'context_variables': {}, + 'max_context_history': 10 + } + + # NLP配置 + self.nlp_config = { + 'enable_intent_recognition': True, + 'enable_entity_extraction': True, + 'enable_sentiment_analysis': True, + 'enable_context_awareness': True, + 'confidence_threshold': 0.6, + 'enable_fallback_processing': True, + 'fallback_threshold': 0.3 + } + + # 统计信息 + self.nlp_stats = { + 'texts_processed': 0, + 'intents_recognized': 0, + 'entities_extracted': 0, + 'processing_errors': 0, + 'fallback_used': 0, + 'average_processing_time': 0.0 + } + + # 缓存管理 + self.processing_cache = {} + self.cache_max_size = 1000 + + # 回调函数 + self.nlp_callbacks = { + 'text_processed': [], + 'intent_recognized': [], + 'entity_extracted': [], + 'context_updated': [], + 'processing_error': [] + } + + # 时间戳记录 + self.last_processing_time = 0.0 + self.last_cache_cleanup = 0.0 + + print("✓ 自然语言处理器已创建") + + def initialize(self) -> bool: + """ + 初始化自然语言处理器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化意图识别 + self._initialize_intents() + + # 初始化实体识别 + self._initialize_entities() + + # 初始化语法规则 + self._initialize_grammar_rules() + + # 初始化词汇表 + self._initialize_vocabularies() + + self.initialized = True + print("✓ 自然语言处理器初始化完成") + return True + + except Exception as e: + print(f"✗ 自然语言处理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用自然语言处理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 自然语言处理器未初始化") + return False + + self.enabled = True + print("✓ 自然语言处理器已启用") + return True + + except Exception as e: + print(f"✗ 自然语言处理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用自然语言处理器""" + try: + self.enabled = False + print("✓ 自然语言处理器已禁用") + + except Exception as e: + print(f"✗ 自然语言处理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理自然语言处理器资源""" + try: + self.disable() + self.intents.clear() + self.entities.clear() + self.grammar_rules.clear() + self.vocabularies.clear() + self.nlp_callbacks.clear() + self.processing_cache.clear() + self.initialized = False + print("✓ 自然语言处理器资源已清理") + + except Exception as e: + print(f"✗ 自然语言处理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新自然语言处理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.last_processing_time = current_time + + # 定期清理缓存 + if current_time - self.last_cache_cleanup > 60.0: # 每分钟清理一次 + self._cleanup_cache() + self.last_cache_cleanup = current_time + + except Exception as e: + print(f"✗ 自然语言处理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _initialize_intents(self): + """初始化意图识别""" + try: + self.intents = { + 'navigation': { + 'name': '导航', + 'description': '移动控制相关意图', + 'patterns': [ + r'(向前|往前|前进).*', + r'(向后|往后|后退).*', + r'(向左|左转).*', + r'(向右|右转).*', + r'(停止|停).*' + ], + 'actions': ['move_forward', 'move_backward', 'turn_left', 'turn_right', 'stop'] + }, + 'system_control': { + 'name': '系统控制', + 'description': '系统控制相关意图', + 'patterns': [ + r'(打开|开启).*灯.*', + r'(关闭|关掉).*灯.*', + r'(增加|调高|提高).*音量.*', + r'(减少|调低|降低).*音量.*' + ], + 'actions': ['toggle_lights', 'adjust_volume'] + }, + 'entertainment': { + 'name': '娱乐控制', + 'description': '娱乐控制相关意图', + 'patterns': [ + r'(播放|开始播放).*音乐.*', + r'(暂停|停止播放).*音乐.*', + r'(下一首|下一首歌).*', + r'(上一首|上一首歌).*' + ], + 'actions': ['play_music', 'pause_music', 'next_track', 'previous_track'] + }, + 'interaction': { + 'name': '交互', + 'description': '日常交互相关意图', + 'patterns': [ + r'(你好|您好|hello).*', + r'(谢谢|感谢).*', + r'(再见|拜拜|bye).*', + r'(帮助|help).*', + r'(关于|信息).*' + ], + 'actions': ['greet', 'thank', 'goodbye', 'help', 'about'] + }, + 'query': { + 'name': '查询', + 'description': '信息查询相关意图', + 'patterns': [ + r'(时间|几点).*', + r'(日期|今天).*', + r'(天气|气温).*', + r'(状态|情况).*' + ], + 'actions': ['query_time', 'query_date', 'query_weather', 'query_status'] + } + } + + print("✓ 意图识别初始化完成") + + except Exception as e: + print(f"✗ 意图识别初始化失败: {e}") + + def _initialize_entities(self): + """初始化实体识别""" + try: + self.entities = { + 'direction': { + 'name': '方向', + 'values': ['前', '后', '左', '右', 'forward', 'backward', 'left', 'right'] + }, + 'object': { + 'name': '对象', + 'values': ['灯', '音乐', '音量', 'light', 'music', 'volume'] + }, + 'action': { + 'name': '动作', + 'values': ['打开', '关闭', '增加', '减少', '播放', '暂停', 'open', 'close', 'increase', 'decrease', 'play', 'pause'] + }, + 'time_unit': { + 'name': '时间单位', + 'values': ['秒', '分钟', '小时', 'second', 'minute', 'hour'] + } + } + + print("✓ 实体识别初始化完成") + + except Exception as e: + print(f"✗ 实体识别初始化失败: {e}") + + def _initialize_grammar_rules(self): + """初始化语法规则""" + try: + self.grammar_rules = { + 'command_structure': { + 'name': '命令结构', + 'rules': [ + '动词+对象', + '对象+动词', + '动词+形容词+对象', + '请+动词+对象' + ] + }, + 'query_structure': { + 'name': '查询结构', + 'rules': [ + '疑问词+动词', + '疑问词+名词', + '动词+疑问词' + ] + } + } + + print("✓ 语法规则初始化完成") + + except Exception as e: + print(f"✗ 语法规则初始化失败: {e}") + + def _initialize_vocabularies(self): + """初始化词汇表""" + try: + self.vocabularies = { + 'zh-CN': { + 'greetings': ['你好', '您好', '早上好', '下午好', '晚上好'], + 'affirmatives': ['是', '对', '好', '可以', '行'], + 'negatives': ['不', '否', '不要', '不行', '不可以'], + 'confirmations': ['确定', '确认', '好的', '明白'], + 'questions': ['什么', '怎么', '为什么', '何时', '哪里'] + }, + 'en-US': { + 'greetings': ['hello', 'hi', 'good morning', 'good afternoon', 'good evening'], + 'affirmatives': ['yes', 'yeah', 'yep', 'okay', 'ok', 'sure'], + 'negatives': ['no', 'nope', 'not'], + 'confirmations': ['confirm', 'okay', 'alright', 'got it'], + 'questions': ['what', 'how', 'why', 'when', 'where'] + } + } + + print("✓ 词汇表初始化完成") + + except Exception as e: + print(f"✗ 词汇表初始化失败: {e}") + + def _cleanup_cache(self): + """清理缓存""" + try: + current_time = time.time() + expired_keys = [ + key for key, value in self.processing_cache.items() + if current_time - value['timestamp'] > 300.0 # 5分钟过期 + ] + + for key in expired_keys: + del self.processing_cache[key] + + # 保持缓存大小 + if len(self.processing_cache) > self.cache_max_size: + # 移除最旧的一半 + sorted_items = sorted(self.processing_cache.items(), + key=lambda x: x[1]['timestamp']) + keys_to_remove = [item[0] for item in sorted_items[:self.cache_max_size//2]] + + for key in keys_to_remove: + del self.processing_cache[key] + + except Exception as e: + print(f"✗ 缓存清理失败: {e}") + + def process_text(self, text: str, language: str = None) -> Dict[str, Any]: + """ + 处理文本 + + Args: + text: 要处理的文本 + language: 语言代码 + + Returns: + 处理结果字典 + """ + try: + if not self.enabled: + print("✗ 自然语言处理器未启用") + return self._create_error_result("NLP processor not enabled") + + start_time = time.time() + + # 使用默认语言 + if language is None: + language = self.language_config['primary_language'] + + # 检查缓存 + cache_key = f"{text}_{language}" + if cache_key in self.processing_cache: + cached_result = self.processing_cache[cache_key] + # 更新时间戳 + cached_result['timestamp'] = time.time() + self.nlp_stats['texts_processed'] += 1 + return cached_result['result'] + + # 文本预处理 + processed_text = self._preprocess_text(text, language) + + # 语言检测 + detected_language = self._detect_language(processed_text) + if detected_language != language and self.language_config['enable_dialect_detection']: + language = detected_language + + # 意图识别 + intent_result = self._recognize_intent(processed_text, language) + + # 实体提取 + entity_result = self._extract_entities(processed_text, language) + + # 上下文分析 + context_result = self._analyze_context(processed_text, language) + + # 情感分析 + sentiment_result = self._analyze_sentiment(processed_text, language) + + # 构建结果 + result = { + 'text': text, + 'processed_text': processed_text, + 'language': language, + 'intent': intent_result, + 'entities': entity_result, + 'context': context_result, + 'sentiment': sentiment_result, + 'timestamp': time.time(), + 'processing_time': time.time() - start_time + } + + # 缓存结果 + self.processing_cache[cache_key] = { + 'result': result, + 'timestamp': time.time() + } + + # 更新统计信息 + self.nlp_stats['texts_processed'] += 1 + if intent_result['confidence'] > self.nlp_config['confidence_threshold']: + self.nlp_stats['intents_recognized'] += 1 + self.nlp_stats['entities_extracted'] += len(entity_result.get('entities', [])) + + # 计算平均处理时间 + self.nlp_stats['average_processing_time'] = ( + (self.nlp_stats['average_processing_time'] * (self.nlp_stats['texts_processed'] - 1) + + result['processing_time']) / self.nlp_stats['texts_processed'] + ) + + # 触发文本处理回调 + self._trigger_nlp_callback('text_processed', result) + + return result + + except Exception as e: + print(f"✗ 文本处理失败: {e}") + import traceback + traceback.print_exc() + self.nlp_stats['processing_errors'] += 1 + + # 触发处理错误回调 + error_data = { + 'text': text, + 'error': str(e), + 'timestamp': time.time() + } + self._trigger_nlp_callback('processing_error', error_data) + + return self._create_error_result(str(e)) + + def _preprocess_text(self, text: str, language: str) -> str: + """ + 文本预处理 + + Args: + text: 原始文本 + language: 语言代码 + + Returns: + 预处理后的文本 + """ + try: + # 转换为小写(仅适用于英文) + if language == 'en-US': + text = text.lower() + + # 移除多余空格 + text = re.sub(r'\s+', ' ', text).strip() + + # 移除特殊字符(保留中文、英文、数字和基本标点) + if language == 'zh-CN': + text = re.sub(r'[^\u4e00-\u9fa5a-zA-Z0-9\s\.,!?;:]', '', text) + else: + text = re.sub(r'[^a-zA-Z0-9\s\.,!?;:]', '', text) + + return text + + except Exception as e: + print(f"✗ 文本预处理失败: {e}") + return text + + def _detect_language(self, text: str) -> str: + """ + 检测语言 + + Args: + text: 文本 + + Returns: + 语言代码 + """ + try: + # 简单的语言检测(基于字符集) + chinese_chars = len(re.findall(r'[\u4e00-\u9fa5]', text)) + english_chars = len(re.findall(r'[a-zA-Z]', text)) + + if chinese_chars > english_chars: + return 'zh-CN' + else: + return 'en-US' + + except Exception as e: + print(f"✗ 语言检测失败: {e}") + return self.language_config['primary_language'] + + def _recognize_intent(self, text: str, language: str) -> Dict[str, Any]: + """ + 意图识别 + + Args: + text: 文本 + language: 语言代码 + + Returns: + 意图识别结果 + """ + try: + best_intent = None + best_confidence = 0.0 + + # 遍历所有意图模式 + for intent_key, intent_data in self.intents.items(): + patterns = intent_data.get('patterns', []) + + for pattern in patterns: + if re.search(pattern, text, re.IGNORECASE): + # 计算匹配度 + confidence = self._calculate_match_confidence(text, pattern) + + if confidence > best_confidence: + best_confidence = confidence + best_intent = { + 'name': intent_key, + 'display_name': intent_data['name'], + 'description': intent_data['description'], + 'confidence': confidence, + 'actions': intent_data.get('actions', []) + } + + if best_intent and best_confidence >= self.nlp_config['confidence_threshold']: + # 触发意图识别回调 + self._trigger_nlp_callback('intent_recognized', best_intent) + return best_intent + elif (best_intent and + best_confidence >= self.nlp_config['fallback_threshold'] and + self.nlp_config['enable_fallback_processing']): + # 使用回退处理 + self.nlp_stats['fallback_used'] += 1 + best_intent['confidence'] = best_confidence + return best_intent + else: + return { + 'name': 'unknown', + 'display_name': '未知意图', + 'description': '无法识别的意图', + 'confidence': 0.0, + 'actions': [] + } + + except Exception as e: + print(f"✗ 意图识别失败: {e}") + return { + 'name': 'error', + 'display_name': '处理错误', + 'description': f'意图识别错误: {e}', + 'confidence': 0.0, + 'actions': [] + } + + def _calculate_match_confidence(self, text: str, pattern: str) -> float: + """ + 计算匹配置信度 + + Args: + text: 文本 + pattern: 模式 + + Returns: + 置信度(0.0-1.0) + """ + try: + # 简单的匹配度计算 + match = re.search(pattern, text, re.IGNORECASE) + if match: + # 基于匹配长度和文本长度的比例 + match_length = len(match.group(0)) + text_length = len(text) + if text_length > 0: + return min(1.0, match_length / text_length) + return 0.0 + except Exception as e: + print(f"✗ 匹配置信度计算失败: {e}") + return 0.0 + + def _extract_entities(self, text: str, language: str) -> Dict[str, Any]: + """ + 实体提取 + + Args: + text: 文本 + language: 语言代码 + + Returns: + 实体提取结果 + """ + try: + entities = [] + + # 遍历所有实体类型 + for entity_key, entity_data in self.entities.items(): + values = entity_data.get('values', []) + + for value in values: + if value in text: + entities.append({ + 'type': entity_key, + 'display_name': entity_data['name'], + 'value': value, + 'position': text.find(value) + }) + + if entities: + # 触发实体提取回调 + for entity in entities: + self._trigger_nlp_callback('entity_extracted', entity) + + return { + 'entities': entities, + 'count': len(entities) + } + + except Exception as e: + print(f"✗ 实体提取失败: {e}") + return { + 'entities': [], + 'count': 0 + } + + def _analyze_context(self, text: str, language: str) -> Dict[str, Any]: + """ + 上下文分析 + + Args: + text: 文本 + language: 语言代码 + + Returns: + 上下文分析结果 + """ + try: + # 检查是否与当前上下文相关 + context_relevant = False + + # 这里可以实现更复杂的上下文分析逻辑 + # 例如:检查是否提及之前的对话内容等 + + return { + 'current_context': self.context_manager['current_context'], + 'context_relevant': context_relevant, + 'context_variables': self.context_manager['context_variables'].copy() + } + + except Exception as e: + print(f"✗ 上下文分析失败: {e}") + return { + 'current_context': 'default', + 'context_relevant': False, + 'context_variables': {} + } + + def _analyze_sentiment(self, text: str, language: str) -> Dict[str, Any]: + """ + 情感分析 + + Args: + text: 文本 + language: 语言代码 + + Returns: + 情感分析结果 + """ + try: + # 简单的情感分析 + positive_words = ['好', '棒', '喜欢', '开心', '高兴', 'good', 'great', 'happy', 'like'] + negative_words = ['坏', '差', '讨厌', '生气', '难过', 'bad', 'terrible', 'angry', 'sad'] + + positive_count = sum(1 for word in positive_words if word in text) + negative_count = sum(1 for word in negative_words if word in text) + + if positive_count > negative_count: + sentiment = 'positive' + confidence = positive_count / (positive_count + negative_count + 1) + elif negative_count > positive_count: + sentiment = 'negative' + confidence = negative_count / (positive_count + negative_count + 1) + else: + sentiment = 'neutral' + confidence = 0.5 + + return { + 'sentiment': sentiment, + 'confidence': confidence, + 'positive_words': positive_count, + 'negative_words': negative_count + } + + except Exception as e: + print(f"✗ 情感分析失败: {e}") + return { + 'sentiment': 'neutral', + 'confidence': 0.0, + 'positive_words': 0, + 'negative_words': 0 + } + + def _create_error_result(self, error_message: str) -> Dict[str, Any]: + """ + 创建错误结果 + + Args: + error_message: 错误消息 + + Returns: + 错误结果字典 + """ + return { + 'text': '', + 'processed_text': '', + 'language': self.language_config['primary_language'], + 'intent': { + 'name': 'error', + 'display_name': '处理错误', + 'description': error_message, + 'confidence': 0.0, + 'actions': [] + }, + 'entities': { + 'entities': [], + 'count': 0 + }, + 'context': { + 'current_context': 'default', + 'context_relevant': False, + 'context_variables': {} + }, + 'sentiment': { + 'sentiment': 'neutral', + 'confidence': 0.0, + 'positive_words': 0, + 'negative_words': 0 + }, + 'timestamp': time.time(), + 'processing_time': 0.0, + 'error': error_message + } + + def update_context(self, context_name: str, variables: Dict[str, Any] = None) -> bool: + """ + 更新上下文 + + Args: + context_name: 上下文名称 + variables: 上下文变量 + + Returns: + 是否更新成功 + """ + try: + # 保存当前上下文到历史 + self.context_manager['context_history'].append({ + 'context': self.context_manager['current_context'], + 'variables': self.context_manager['context_variables'].copy(), + 'timestamp': time.time() + }) + + # 保持历史记录大小 + if len(self.context_manager['context_history']) > self.context_manager['max_context_history']: + self.context_manager['context_history'].pop(0) + + # 更新当前上下文 + self.context_manager['current_context'] = context_name + if variables: + self.context_manager['context_variables'] = variables + else: + self.context_manager['context_variables'] = {} + + # 触发上下文更新回调 + self._trigger_nlp_callback('context_updated', { + 'context': context_name, + 'variables': variables + }) + + print(f"✓ 上下文已更新为: {context_name}") + return True + + except Exception as e: + print(f"✗ 上下文更新失败: {e}") + return False + + def get_current_context(self) -> Dict[str, Any]: + """ + 获取当前上下文 + + Returns: + 当前上下文字典 + """ + return { + 'context': self.context_manager['current_context'], + 'variables': self.context_manager['context_variables'].copy(), + 'history': self.context_manager['context_history'].copy() + } + + def clear_context(self) -> bool: + """ + 清空上下文 + + Returns: + 是否清空成功 + """ + try: + self.context_manager['current_context'] = 'default' + self.context_manager['context_variables'] = {} + self.context_manager['context_history'] = [] + print("✓ 上下文已清空") + return True + except Exception as e: + print(f"✗ 上下文清空失败: {e}") + return False + + def get_intents(self) -> Dict[str, Any]: + """ + 获取意图定义 + + Returns: + 意图定义字典 + """ + return self.intents.copy() + + def get_entities(self) -> Dict[str, Any]: + """ + 获取实体定义 + + Returns: + 实体定义字典 + """ + return self.entities.copy() + + def get_vocabularies(self) -> Dict[str, Any]: + """ + 获取词汇表 + + Returns: + 词汇表字典 + """ + return self.vocabularies.copy() + + def set_language_config(self, config: Dict[str, Any]) -> bool: + """ + 设置语言配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.language_config.update(config) + print(f"✓ 语言配置已更新: {self.language_config}") + return True + except Exception as e: + print(f"✗ 语言配置设置失败: {e}") + return False + + def get_language_config(self) -> Dict[str, Any]: + """ + 获取语言配置 + + Returns: + 语言配置字典 + """ + return self.language_config.copy() + + def set_nlp_config(self, config: Dict[str, Any]) -> bool: + """ + 设置NLP配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.nlp_config.update(config) + print(f"✓ NLP配置已更新: {self.nlp_config}") + return True + except Exception as e: + print(f"✗ NLP配置设置失败: {e}") + return False + + def get_nlp_config(self) -> Dict[str, Any]: + """ + 获取NLP配置 + + Returns: + NLP配置字典 + """ + return self.nlp_config.copy() + + def _trigger_nlp_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发NLP回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.nlp_callbacks: + for callback in self.nlp_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ NLP回调执行失败: {e}") + except Exception as e: + print(f"✗ NLP回调触发失败: {e}") + + def register_nlp_callback(self, callback_type: str, callback: callable): + """ + 注册NLP回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.nlp_callbacks: + self.nlp_callbacks[callback_type].append(callback) + print(f"✓ NLP回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ NLP回调注册失败: {e}") + + def unregister_nlp_callback(self, callback_type: str, callback: callable): + """ + 注销NLP回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.nlp_callbacks: + if callback in self.nlp_callbacks[callback_type]: + self.nlp_callbacks[callback_type].remove(callback) + print(f"✓ NLP回调已注销: {callback_type}") + except Exception as e: + print(f"✗ NLP回调注销失败: {e}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.nlp_stats.copy() + + def reset_stats(self): + """重置统计信息""" + try: + self.nlp_stats = { + 'texts_processed': 0, + 'intents_recognized': 0, + 'entities_extracted': 0, + 'processing_errors': 0, + 'fallback_used': 0, + 'average_processing_time': 0.0 + } + print("✓ NLP处理器统计信息已重置") + except Exception as e: print(f"✗ NLP处理器统计信息重置失败: {e}") \ No newline at end of file diff --git a/plugins/user/voice_control/plugin.py b/plugins/user/voice_control/plugin.py index 56c85109..3c37dbc0 100644 --- a/plugins/user/voice_control/plugin.py +++ b/plugins/user/voice_control/plugin.py @@ -1,275 +1,275 @@ -""" -语音控制插件 -为EG引擎提供完整的语音识别和语音合成功能 -""" - -from .core.voice_manager import VoiceManager -from .recognition.speech_recognizer import SpeechRecognizer -from .synthesis.speech_synthesizer import SpeechSynthesizer -from .commands.command_processor import CommandProcessor -from .audio.audio_manager import AudioManager -from .events.event_handler import VoiceEventHandler -from .nlp.nlp_processor import NLPProcessor -from .config.config_manager import VoiceConfigManager -from .editor.voice_editor import VoiceEditor -from .utils.voice_utils import VoiceUtils - -class VoiceControlPlugin: - """ - 语音控制插件主类 - 整合所有语音控制功能模块 - """ - - def __init__(self, engine): - """ - 初始化语音控制插件 - - Args: - engine: EG引擎实例 - """ - self.engine = engine - self.name = "VoiceControl" - self.version = "1.0.0" - self.author = "EG Plugin System" - - # 功能模块 - self.voice_manager = None - self.speech_recognizer = None - self.speech_synthesizer = None - self.command_processor = None - self.audio_manager = None - self.event_handler = None - self.nlp_processor = None - self.config_manager = None - self.voice_editor = None - self.voice_utils = None - - # 插件状态 - self.enabled = False - self.initialized = False - - # 性能统计 - self.stats = { - 'commands_processed': 0, - 'speech_recognized': 0, - 'speech_synthesized': 0, - 'audio_devices': 0, - 'recognition_errors': 0, - 'synthesis_errors': 0 - } - - print(f"✓ 语音控制插件 v{self.version} 已创建") - - def initialize(self) -> bool: - """ - 初始化插件 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化语音控制插件...") - - # 初始化核心组件 - self.voice_manager = VoiceManager(self) - self.speech_recognizer = SpeechRecognizer(self) - self.speech_synthesizer = SpeechSynthesizer(self) - self.command_processor = CommandProcessor(self) - self.audio_manager = AudioManager(self) - self.event_handler = VoiceEventHandler(self) - self.nlp_processor = NLPProcessor(self) - self.config_manager = VoiceConfigManager(self) - self.voice_editor = VoiceEditor(self) - self.voice_utils = VoiceUtils(self) - - # 初始化各个模块 - modules = [ - self.voice_manager, - self.speech_recognizer, - self.speech_synthesizer, - self.command_processor, - self.audio_manager, - self.event_handler, - self.nlp_processor, - self.config_manager, - self.voice_editor, - self.voice_utils - ] - - for module in modules: - if module and not module.initialize(): - print(f"✗ 模块初始化失败: {module.__class__.__name__}") - return False - - self.initialized = True - print("✓ 语音控制插件初始化完成") - return True - - except Exception as e: - print(f"✗ 语音控制插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 插件未初始化") - return False - - print("正在启用语音控制插件...") - - # 启用各个模块 - modules = [ - self.voice_manager, - self.speech_recognizer, - self.speech_synthesizer, - self.command_processor, - self.audio_manager, - self.event_handler, - self.nlp_processor, - self.config_manager, - self.voice_editor, - self.voice_utils - ] - - for module in modules: - if module and not module.enable(): - print(f"✗ 模块启用失败: {module.__class__.__name__}") - return False - - self.enabled = True - print("✓ 语音控制插件已启用") - return True - - except Exception as e: - print(f"✗ 语音控制插件启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用插件""" - try: - if not self.enabled: - return - - print("正在禁用语音控制插件...") - - # 禁用各个模块 - modules = [ - self.voice_manager, - self.speech_recognizer, - self.speech_synthesizer, - self.command_processor, - self.audio_manager, - self.event_handler, - self.nlp_processor, - self.config_manager, - self.voice_editor, - self.voice_utils - ] - - for module in modules: - if module: - module.disable() - - self.enabled = False - print("✓ 语音控制插件已禁用") - - except Exception as e: - print(f"✗ 语音控制插件禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理插件资源""" - try: - print("正在清理语音控制插件资源...") - - # 清理各个模块 - modules = [ - self.voice_manager, - self.speech_recognizer, - self.speech_synthesizer, - self.command_processor, - self.audio_manager, - self.event_handler, - self.nlp_processor, - self.config_manager, - self.voice_editor, - self.voice_utils - ] - - for module in modules: - if module: - module.finalize() - - self.initialized = False - print("✓ 语音控制插件资源已清理") - - except Exception as e: - print(f"✗ 语音控制插件资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新插件状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - # 更新各个模块 - modules = [ - self.voice_manager, - self.speech_recognizer, - self.speech_synthesizer, - self.command_processor, - self.audio_manager, - self.event_handler, - self.nlp_processor, - self.config_manager, - self.voice_editor, - self.voice_utils - ] - - for module in modules: - if module: - module.update(dt) - - except Exception as e: - print(f"✗ 语音控制插件更新失败: {e}") - import traceback - traceback.print_exc() - - def get_stats(self) -> dict: - """ - 获取插件统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'commands_processed': 0, - 'speech_recognized': 0, - 'speech_synthesized': 0, - 'audio_devices': 0, - 'recognition_errors': 0, - 'synthesis_errors': 0 - } +""" +语音控制插件 +为EG引擎提供完整的语音识别和语音合成功能 +""" + +from .core.voice_manager import VoiceManager +from .recognition.speech_recognizer import SpeechRecognizer +from .synthesis.speech_synthesizer import SpeechSynthesizer +from .commands.command_processor import CommandProcessor +from .audio.audio_manager import AudioManager +from .events.event_handler import VoiceEventHandler +from .nlp.nlp_processor import NLPProcessor +from .config.config_manager import VoiceConfigManager +from .editor.voice_editor import VoiceEditor +from .utils.voice_utils import VoiceUtils + +class VoiceControlPlugin: + """ + 语音控制插件主类 + 整合所有语音控制功能模块 + """ + + def __init__(self, engine): + """ + 初始化语音控制插件 + + Args: + engine: EG引擎实例 + """ + self.engine = engine + self.name = "VoiceControl" + self.version = "1.0.0" + self.author = "EG Plugin System" + + # 功能模块 + self.voice_manager = None + self.speech_recognizer = None + self.speech_synthesizer = None + self.command_processor = None + self.audio_manager = None + self.event_handler = None + self.nlp_processor = None + self.config_manager = None + self.voice_editor = None + self.voice_utils = None + + # 插件状态 + self.enabled = False + self.initialized = False + + # 性能统计 + self.stats = { + 'commands_processed': 0, + 'speech_recognized': 0, + 'speech_synthesized': 0, + 'audio_devices': 0, + 'recognition_errors': 0, + 'synthesis_errors': 0 + } + + print(f"✓ 语音控制插件 v{self.version} 已创建") + + def initialize(self) -> bool: + """ + 初始化插件 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化语音控制插件...") + + # 初始化核心组件 + self.voice_manager = VoiceManager(self) + self.speech_recognizer = SpeechRecognizer(self) + self.speech_synthesizer = SpeechSynthesizer(self) + self.command_processor = CommandProcessor(self) + self.audio_manager = AudioManager(self) + self.event_handler = VoiceEventHandler(self) + self.nlp_processor = NLPProcessor(self) + self.config_manager = VoiceConfigManager(self) + self.voice_editor = VoiceEditor(self) + self.voice_utils = VoiceUtils(self) + + # 初始化各个模块 + modules = [ + self.voice_manager, + self.speech_recognizer, + self.speech_synthesizer, + self.command_processor, + self.audio_manager, + self.event_handler, + self.nlp_processor, + self.config_manager, + self.voice_editor, + self.voice_utils + ] + + for module in modules: + if module and not module.initialize(): + print(f"✗ 模块初始化失败: {module.__class__.__name__}") + return False + + self.initialized = True + print("✓ 语音控制插件初始化完成") + return True + + except Exception as e: + print(f"✗ 语音控制插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 插件未初始化") + return False + + print("正在启用语音控制插件...") + + # 启用各个模块 + modules = [ + self.voice_manager, + self.speech_recognizer, + self.speech_synthesizer, + self.command_processor, + self.audio_manager, + self.event_handler, + self.nlp_processor, + self.config_manager, + self.voice_editor, + self.voice_utils + ] + + for module in modules: + if module and not module.enable(): + print(f"✗ 模块启用失败: {module.__class__.__name__}") + return False + + self.enabled = True + print("✓ 语音控制插件已启用") + return True + + except Exception as e: + print(f"✗ 语音控制插件启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用插件""" + try: + if not self.enabled: + return + + print("正在禁用语音控制插件...") + + # 禁用各个模块 + modules = [ + self.voice_manager, + self.speech_recognizer, + self.speech_synthesizer, + self.command_processor, + self.audio_manager, + self.event_handler, + self.nlp_processor, + self.config_manager, + self.voice_editor, + self.voice_utils + ] + + for module in modules: + if module: + module.disable() + + self.enabled = False + print("✓ 语音控制插件已禁用") + + except Exception as e: + print(f"✗ 语音控制插件禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理插件资源""" + try: + print("正在清理语音控制插件资源...") + + # 清理各个模块 + modules = [ + self.voice_manager, + self.speech_recognizer, + self.speech_synthesizer, + self.command_processor, + self.audio_manager, + self.event_handler, + self.nlp_processor, + self.config_manager, + self.voice_editor, + self.voice_utils + ] + + for module in modules: + if module: + module.finalize() + + self.initialized = False + print("✓ 语音控制插件资源已清理") + + except Exception as e: + print(f"✗ 语音控制插件资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新插件状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + # 更新各个模块 + modules = [ + self.voice_manager, + self.speech_recognizer, + self.speech_synthesizer, + self.command_processor, + self.audio_manager, + self.event_handler, + self.nlp_processor, + self.config_manager, + self.voice_editor, + self.voice_utils + ] + + for module in modules: + if module: + module.update(dt) + + except Exception as e: + print(f"✗ 语音控制插件更新失败: {e}") + import traceback + traceback.print_exc() + + def get_stats(self) -> dict: + """ + 获取插件统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'commands_processed': 0, + 'speech_recognized': 0, + 'speech_synthesized': 0, + 'audio_devices': 0, + 'recognition_errors': 0, + 'synthesis_errors': 0 + } print("✓ 语音控制统计信息已重置") \ No newline at end of file diff --git a/plugins/user/voice_control/recognition/speech_recognizer.py b/plugins/user/voice_control/recognition/speech_recognizer.py index fb46d3b7..a2caa4a9 100644 --- a/plugins/user/voice_control/recognition/speech_recognizer.py +++ b/plugins/user/voice_control/recognition/speech_recognizer.py @@ -1,707 +1,707 @@ -""" -语音识别器模块 -负责实时语音识别功能 -""" - -import time -import threading -import queue -import math -from typing import Dict, Any, List, Optional - -class SpeechRecognizer: - """ - 语音识别器 - 负责实时语音识别功能 - """ - - def __init__(self, plugin): - """ - 初始化语音识别器 - - Args: - plugin: 语音控制插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 音频输入配置 - self.audio_config = { - 'sample_rate': 16000, - 'channels': 1, - 'format': 'int16', - 'chunk_size': 1024, - 'buffer_size': 4096 - } - - # 识别配置 - self.recognition_config = { - 'language': 'zh-CN', - 'enable_keyword_spotting': True, - 'enable_continuous_recognition': True, - 'silence_threshold': 0.01, - 'speech_threshold': 0.02, - 'silence_duration': 1.0, - 'max_recording_duration': 10.0 - } - - # 音频缓冲区 - self.audio_buffer = queue.Queue(maxsize=100) - self.processed_buffer = queue.Queue(maxsize=10) - - # 识别状态 - self.recognition_state = { - 'is_recording': False, - 'is_processing': False, - 'is_speech_detected': False, - 'last_speech_time': 0.0, - 'recording_start_time': 0.0, - 'audio_level': 0.0 - } - - # 麦克风管理 - self.microphone = None - self.audio_stream = None - - # 线程管理 - self.recording_thread = None - self.processing_thread = None - self.running = False - - # 统计信息 - self.stats = { - 'audio_chunks_processed': 0, - 'speech_segments_detected': 0, - 'recognition_attempts': 0, - 'successful_recognitions': 0, - 'failed_recognitions': 0, - 'processing_time_total': 0.0 - } - - # 回调函数 - self.recognition_callbacks = { - 'audio_chunk_received': [], - 'speech_detected': [], - 'silence_detected': [], - 'recognition_result': [], - 'recognition_error': [] - } - - # 时间戳记录 - self.last_processing_time = 0.0 - self.last_recognition_time = 0.0 - - print("✓ 语音识别器已创建") - - def initialize(self) -> bool: - """ - 初始化语音识别器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化音频输入设备 - self._initialize_audio_input() - - self.initialized = True - print("✓ 语音识别器初始化完成") - return True - - except Exception as e: - print(f"✗ 语音识别器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用语音识别器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 语音识别器未初始化") - return False - - self.enabled = True - self.running = True - - # 启动录音线程 - self.recording_thread = threading.Thread(target=self._recording_loop, daemon=True) - self.recording_thread.start() - - # 启动处理线程 - self.processing_thread = threading.Thread(target=self._processing_loop, daemon=True) - self.processing_thread.start() - - print("✓ 语音识别器已启用") - return True - - except Exception as e: - print(f"✗ 语音识别器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用语音识别器""" - try: - self.enabled = False - self.running = False - - # 等待线程结束 - if self.recording_thread and self.recording_thread.is_alive(): - self.recording_thread.join(timeout=2.0) - - if self.processing_thread and self.processing_thread.is_alive(): - self.processing_thread.join(timeout=2.0) - - # 停止音频流 - self._stop_audio_stream() - - print("✓ 语音识别器已禁用") - - except Exception as e: - print(f"✗ 语音识别器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理语音识别器资源""" - try: - self.disable() - self.recognition_callbacks.clear() - - # 清理音频缓冲区 - while not self.audio_buffer.empty(): - try: - self.audio_buffer.get_nowait() - except queue.Empty: - break - - while not self.processed_buffer.empty(): - try: - self.processed_buffer.get_nowait() - except queue.Empty: - break - - self.initialized = False - print("✓ 语音识别器资源已清理") - - except Exception as e: - print(f"✗ 语音识别器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新语音识别器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.last_processing_time = current_time - - except Exception as e: - print(f"✗ 语音识别器更新失败: {e}") - import traceback - traceback.print_exc() - - def _initialize_audio_input(self): - """初始化音频输入设备""" - try: - # 这里应该初始化实际的音频输入设备 - # 由于这是一个模拟实现,我们只打印信息 - print("✓ 音频输入设备初始化完成") - print(f" 采样率: {self.audio_config['sample_rate']} Hz") - print(f" 声道数: {self.audio_config['channels']}") - print(f" 格式: {self.audio_config['format']}") - - except Exception as e: - print(f"✗ 音频输入设备初始化失败: {e}") - - def _start_audio_stream(self) -> bool: - """ - 启动音频流 - - Returns: - 是否启动成功 - """ - try: - # 这里应该启动实际的音频流 - # 由于这是一个模拟实现,我们只设置状态 - self.audio_stream = "simulated_audio_stream" - print("✓ 音频流已启动") - return True - except Exception as e: - print(f"✗ 音频流启动失败: {e}") - return False - - def _stop_audio_stream(self): - """停止音频流""" - try: - # 这里应该停止实际的音频流 - self.audio_stream = None - print("✓ 音频流已停止") - except Exception as e: - print(f"✗ 音频流停止失败: {e}") - - def _recording_loop(self): - """录音循环线程""" - try: - # 启动音频流 - if not self._start_audio_stream(): - return - - chunk_duration = self.audio_config['chunk_size'] / self.audio_config['sample_rate'] - - while self.running and self.enabled: - try: - # 模拟读取音频数据 - audio_chunk = self._read_audio_chunk() - if audio_chunk is not None: - # 计算音频级别 - audio_level = self._calculate_audio_level(audio_chunk) - - # 更新状态 - self.recognition_state['audio_level'] = audio_level - - # 检测语音活动 - is_speech = audio_level > self.recognition_config['speech_threshold'] - was_speech = self.recognition_state['is_speech_detected'] - - # 更新语音检测状态 - self.recognition_state['is_speech_detected'] = is_speech - if is_speech: - self.recognition_state['last_speech_time'] = time.time() - - # 触发回调 - self._trigger_recognition_callback('audio_chunk_received', { - 'audio_chunk': audio_chunk, - 'audio_level': audio_level, - 'is_speech': is_speech - }) - - # 检测语音/静音转换 - if is_speech and not was_speech: - self._trigger_recognition_callback('speech_detected', { - 'audio_level': audio_level - }) - elif not is_speech and was_speech: - self._trigger_recognition_callback('silence_detected', { - 'audio_level': audio_level - }) - - # 添加到缓冲区 - try: - self.audio_buffer.put_nowait({ - 'data': audio_chunk, - 'level': audio_level, - 'timestamp': time.time() - }) - self.stats['audio_chunks_processed'] += 1 - except queue.Full: - # 缓冲区满,丢弃最旧的数据 - try: - self.audio_buffer.get_nowait() - self.audio_buffer.put_nowait({ - 'data': audio_chunk, - 'level': audio_level, - 'timestamp': time.time() - }) - except queue.Empty: - pass - - # 控制录音频率 - time.sleep(chunk_duration) - - except Exception as e: - print(f"✗ 录音循环错误: {e}") - time.sleep(0.1) - - except Exception as e: - print(f"✗ 录音循环失败: {e}") - - def _processing_loop(self): - """处理循环线程""" - try: - while self.running and self.enabled: - try: - # 从缓冲区获取音频数据 - if not self.audio_buffer.empty(): - audio_data = self.audio_buffer.get_nowait() - self._process_audio_chunk(audio_data) - - # 控制处理频率 - time.sleep(0.01) - - except queue.Empty: - time.sleep(0.01) - except Exception as e: - print(f"✗ 处理循环错误: {e}") - time.sleep(0.1) - - except Exception as e: - print(f"✗ 处理循环失败: {e}") - - def _read_audio_chunk(self) -> Optional[bytes]: - """ - 读取音频块 - - Returns: - 音频数据或None - """ - try: - # 模拟生成音频数据 - import random - import struct - - chunk_size = self.audio_config['chunk_size'] - audio_data = bytearray() - - for _ in range(chunk_size): - # 生成模拟的音频样本 - sample = int(random.randint(-32768, 32767) * 0.1) - audio_data.extend(struct.pack(' float: - """ - 计算音频级别 - - Args: - audio_chunk: 音频数据块 - - Returns: - 音频级别(0.0-1.0) - """ - try: - import struct - - # 解析音频数据 - samples = struct.unpack(f'<{len(audio_chunk)//2}h', audio_chunk) - - # 计算RMS - if len(samples) > 0: - rms = math.sqrt(sum(sample**2 for sample in samples) / len(samples)) - # 归一化到0-1范围(假设16位音频) - level = rms / 32768.0 - return min(1.0, level) - else: - return 0.0 - - except Exception as e: - print(f"✗ 音频级别计算失败: {e}") - return 0.0 - - def _process_audio_chunk(self, audio_data: Dict[str, Any]): - """ - 处理音频块 - - Args: - audio_data: 音频数据字典 - """ - try: - start_time = time.time() - - # 模拟语音识别处理 - processing_result = self._simulate_recognition(audio_data) - - if processing_result: - # 添加到处理结果缓冲区 - try: - self.processed_buffer.put_nowait(processing_result) - except queue.Full: - try: - self.processed_buffer.get_nowait() - self.processed_buffer.put_nowait(processing_result) - except queue.Empty: - pass - - # 触发识别结果回调 - self._trigger_recognition_callback('recognition_result', processing_result) - - # 传递给语音管理器处理 - if self.plugin.voice_manager: - self.plugin.voice_manager.process_speech( - processing_result['text'], - processing_result['confidence'] - ) - - self.stats['successful_recognitions'] += 1 - - # 更新统计信息 - processing_time = time.time() - start_time - self.stats['processing_time_total'] += processing_time - self.stats['recognition_attempts'] += 1 - self.last_recognition_time = time.time() - - except Exception as e: - print(f"✗ 音频块处理失败: {e}") - self.stats['failed_recognitions'] += 1 - - # 触发错误回调 - self._trigger_recognition_callback('recognition_error', { - 'error': str(e), - 'timestamp': time.time() - }) - - def _simulate_recognition(self, audio_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: - """ - 模拟语音识别 - - Args: - audio_data: 音频数据 - - Returns: - 识别结果或None - """ - try: - # 基于音频级别和时间模拟识别结果 - audio_level = audio_data.get('level', 0.0) - timestamp = audio_data.get('timestamp', time.time()) - - # 只有在音频级别足够高时才模拟识别 - if audio_level > self.recognition_config['speech_threshold']: - import random - - # 模拟识别文本 - sample_texts = [ - "前进", "后退", "左转", "右转", "停止", - "打开灯光", "关闭灯光", "增加音量", "减少音量", - "播放音乐", "暂停播放", "下一首", "上一首", - "你好", "谢谢", "再见", "帮助" - ] - - # 根据音频级别设置置信度 - confidence = min(1.0, audio_level * 2) # 简单映射 - - # 有一定概率生成识别结果 - if random.random() < 0.1: # 10%概率 - text = random.choice(sample_texts) - return { - 'text': text, - 'confidence': confidence, - 'timestamp': timestamp, - 'audio_level': audio_level - } - - return None - - except Exception as e: - print(f"✗ 识别模拟失败: {e}") - return None - - def start_recording(self) -> bool: - """ - 开始录音 - - Returns: - 是否开始成功 - """ - try: - if not self.enabled: - print("✗ 语音识别器未启用") - return False - - self.recognition_state['is_recording'] = True - self.recognition_state['recording_start_time'] = time.time() - - print("✓ 录音已开始") - return True - - except Exception as e: - print(f"✗ 录音开始失败: {e}") - return False - - def stop_recording(self) -> bool: - """ - 停止录音 - - Returns: - 是否停止成功 - """ - try: - self.recognition_state['is_recording'] = False - print("✓ 录音已停止") - return True - - except Exception as e: - print(f"✗ 录音停止失败: {e}") - return False - - def is_recording(self) -> bool: - """ - 检查是否正在录音 - - Returns: - 是否正在录音 - """ - return self.recognition_state['is_recording'] - - def is_speech_detected(self) -> bool: - """ - 检查是否检测到语音 - - Returns: - 是否检测到语音 - """ - return self.recognition_state['is_speech_detected'] - - def get_audio_level(self) -> float: - """ - 获取当前音频级别 - - Returns: - 音频级别(0.0-1.0) - """ - return self.recognition_state['audio_level'] - - def set_recognition_config(self, config: Dict[str, Any]) -> bool: - """ - 设置识别配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.recognition_config.update(config) - print(f"✓ 识别配置已更新: {self.recognition_config}") - return True - except Exception as e: - print(f"✗ 识别配置设置失败: {e}") - return False - - def get_recognition_config(self) -> Dict[str, Any]: - """ - 获取识别配置 - - Returns: - 配置字典 - """ - return self.recognition_config.copy() - - def set_audio_config(self, config: Dict[str, Any]) -> bool: - """ - 设置音频配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.audio_config.update(config) - print(f"✓ 音频配置已更新: {self.audio_config}") - return True - except Exception as e: - print(f"✗ 音频配置设置失败: {e}") - return False - - def get_audio_config(self) -> Dict[str, Any]: - """ - 获取音频配置 - - Returns: - 配置字典 - """ - return self.audio_config.copy() - - def _trigger_recognition_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发识别回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.recognition_callbacks: - for callback in self.recognition_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 识别回调执行失败: {e}") - except Exception as e: - print(f"✗ 识别回调触发失败: {e}") - - def register_recognition_callback(self, callback_type: str, callback: callable): - """ - 注册识别回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.recognition_callbacks: - self.recognition_callbacks[callback_type].append(callback) - print(f"✓ 识别回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 识别回调注册失败: {e}") - - def unregister_recognition_callback(self, callback_type: str, callback: callable): - """ - 注销识别回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.recognition_callbacks: - if callback in self.recognition_callbacks[callback_type]: - self.recognition_callbacks[callback_type].remove(callback) - print(f"✓ 识别回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 识别回调注销失败: {e}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - stats = self.stats.copy() - if self.stats['successful_recognitions'] > 0: - stats['average_processing_time'] = ( - self.stats['processing_time_total'] / self.stats['successful_recognitions'] - ) - else: - stats['average_processing_time'] = 0.0 - return stats - - def reset_stats(self): - """重置统计信息""" - try: - self.stats = { - 'audio_chunks_processed': 0, - 'speech_segments_detected': 0, - 'recognition_attempts': 0, - 'successful_recognitions': 0, - 'failed_recognitions': 0, - 'processing_time_total': 0.0 - } - print("✓ 语音识别器统计信息已重置") - except Exception as e: +""" +语音识别器模块 +负责实时语音识别功能 +""" + +import time +import threading +import queue +import math +from typing import Dict, Any, List, Optional + +class SpeechRecognizer: + """ + 语音识别器 + 负责实时语音识别功能 + """ + + def __init__(self, plugin): + """ + 初始化语音识别器 + + Args: + plugin: 语音控制插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 音频输入配置 + self.audio_config = { + 'sample_rate': 16000, + 'channels': 1, + 'format': 'int16', + 'chunk_size': 1024, + 'buffer_size': 4096 + } + + # 识别配置 + self.recognition_config = { + 'language': 'zh-CN', + 'enable_keyword_spotting': True, + 'enable_continuous_recognition': True, + 'silence_threshold': 0.01, + 'speech_threshold': 0.02, + 'silence_duration': 1.0, + 'max_recording_duration': 10.0 + } + + # 音频缓冲区 + self.audio_buffer = queue.Queue(maxsize=100) + self.processed_buffer = queue.Queue(maxsize=10) + + # 识别状态 + self.recognition_state = { + 'is_recording': False, + 'is_processing': False, + 'is_speech_detected': False, + 'last_speech_time': 0.0, + 'recording_start_time': 0.0, + 'audio_level': 0.0 + } + + # 麦克风管理 + self.microphone = None + self.audio_stream = None + + # 线程管理 + self.recording_thread = None + self.processing_thread = None + self.running = False + + # 统计信息 + self.stats = { + 'audio_chunks_processed': 0, + 'speech_segments_detected': 0, + 'recognition_attempts': 0, + 'successful_recognitions': 0, + 'failed_recognitions': 0, + 'processing_time_total': 0.0 + } + + # 回调函数 + self.recognition_callbacks = { + 'audio_chunk_received': [], + 'speech_detected': [], + 'silence_detected': [], + 'recognition_result': [], + 'recognition_error': [] + } + + # 时间戳记录 + self.last_processing_time = 0.0 + self.last_recognition_time = 0.0 + + print("✓ 语音识别器已创建") + + def initialize(self) -> bool: + """ + 初始化语音识别器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化音频输入设备 + self._initialize_audio_input() + + self.initialized = True + print("✓ 语音识别器初始化完成") + return True + + except Exception as e: + print(f"✗ 语音识别器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用语音识别器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 语音识别器未初始化") + return False + + self.enabled = True + self.running = True + + # 启动录音线程 + self.recording_thread = threading.Thread(target=self._recording_loop, daemon=True) + self.recording_thread.start() + + # 启动处理线程 + self.processing_thread = threading.Thread(target=self._processing_loop, daemon=True) + self.processing_thread.start() + + print("✓ 语音识别器已启用") + return True + + except Exception as e: + print(f"✗ 语音识别器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用语音识别器""" + try: + self.enabled = False + self.running = False + + # 等待线程结束 + if self.recording_thread and self.recording_thread.is_alive(): + self.recording_thread.join(timeout=2.0) + + if self.processing_thread and self.processing_thread.is_alive(): + self.processing_thread.join(timeout=2.0) + + # 停止音频流 + self._stop_audio_stream() + + print("✓ 语音识别器已禁用") + + except Exception as e: + print(f"✗ 语音识别器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理语音识别器资源""" + try: + self.disable() + self.recognition_callbacks.clear() + + # 清理音频缓冲区 + while not self.audio_buffer.empty(): + try: + self.audio_buffer.get_nowait() + except queue.Empty: + break + + while not self.processed_buffer.empty(): + try: + self.processed_buffer.get_nowait() + except queue.Empty: + break + + self.initialized = False + print("✓ 语音识别器资源已清理") + + except Exception as e: + print(f"✗ 语音识别器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新语音识别器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.last_processing_time = current_time + + except Exception as e: + print(f"✗ 语音识别器更新失败: {e}") + import traceback + traceback.print_exc() + + def _initialize_audio_input(self): + """初始化音频输入设备""" + try: + # 这里应该初始化实际的音频输入设备 + # 由于这是一个模拟实现,我们只打印信息 + print("✓ 音频输入设备初始化完成") + print(f" 采样率: {self.audio_config['sample_rate']} Hz") + print(f" 声道数: {self.audio_config['channels']}") + print(f" 格式: {self.audio_config['format']}") + + except Exception as e: + print(f"✗ 音频输入设备初始化失败: {e}") + + def _start_audio_stream(self) -> bool: + """ + 启动音频流 + + Returns: + 是否启动成功 + """ + try: + # 这里应该启动实际的音频流 + # 由于这是一个模拟实现,我们只设置状态 + self.audio_stream = "simulated_audio_stream" + print("✓ 音频流已启动") + return True + except Exception as e: + print(f"✗ 音频流启动失败: {e}") + return False + + def _stop_audio_stream(self): + """停止音频流""" + try: + # 这里应该停止实际的音频流 + self.audio_stream = None + print("✓ 音频流已停止") + except Exception as e: + print(f"✗ 音频流停止失败: {e}") + + def _recording_loop(self): + """录音循环线程""" + try: + # 启动音频流 + if not self._start_audio_stream(): + return + + chunk_duration = self.audio_config['chunk_size'] / self.audio_config['sample_rate'] + + while self.running and self.enabled: + try: + # 模拟读取音频数据 + audio_chunk = self._read_audio_chunk() + if audio_chunk is not None: + # 计算音频级别 + audio_level = self._calculate_audio_level(audio_chunk) + + # 更新状态 + self.recognition_state['audio_level'] = audio_level + + # 检测语音活动 + is_speech = audio_level > self.recognition_config['speech_threshold'] + was_speech = self.recognition_state['is_speech_detected'] + + # 更新语音检测状态 + self.recognition_state['is_speech_detected'] = is_speech + if is_speech: + self.recognition_state['last_speech_time'] = time.time() + + # 触发回调 + self._trigger_recognition_callback('audio_chunk_received', { + 'audio_chunk': audio_chunk, + 'audio_level': audio_level, + 'is_speech': is_speech + }) + + # 检测语音/静音转换 + if is_speech and not was_speech: + self._trigger_recognition_callback('speech_detected', { + 'audio_level': audio_level + }) + elif not is_speech and was_speech: + self._trigger_recognition_callback('silence_detected', { + 'audio_level': audio_level + }) + + # 添加到缓冲区 + try: + self.audio_buffer.put_nowait({ + 'data': audio_chunk, + 'level': audio_level, + 'timestamp': time.time() + }) + self.stats['audio_chunks_processed'] += 1 + except queue.Full: + # 缓冲区满,丢弃最旧的数据 + try: + self.audio_buffer.get_nowait() + self.audio_buffer.put_nowait({ + 'data': audio_chunk, + 'level': audio_level, + 'timestamp': time.time() + }) + except queue.Empty: + pass + + # 控制录音频率 + time.sleep(chunk_duration) + + except Exception as e: + print(f"✗ 录音循环错误: {e}") + time.sleep(0.1) + + except Exception as e: + print(f"✗ 录音循环失败: {e}") + + def _processing_loop(self): + """处理循环线程""" + try: + while self.running and self.enabled: + try: + # 从缓冲区获取音频数据 + if not self.audio_buffer.empty(): + audio_data = self.audio_buffer.get_nowait() + self._process_audio_chunk(audio_data) + + # 控制处理频率 + time.sleep(0.01) + + except queue.Empty: + time.sleep(0.01) + except Exception as e: + print(f"✗ 处理循环错误: {e}") + time.sleep(0.1) + + except Exception as e: + print(f"✗ 处理循环失败: {e}") + + def _read_audio_chunk(self) -> Optional[bytes]: + """ + 读取音频块 + + Returns: + 音频数据或None + """ + try: + # 模拟生成音频数据 + import random + import struct + + chunk_size = self.audio_config['chunk_size'] + audio_data = bytearray() + + for _ in range(chunk_size): + # 生成模拟的音频样本 + sample = int(random.randint(-32768, 32767) * 0.1) + audio_data.extend(struct.pack(' float: + """ + 计算音频级别 + + Args: + audio_chunk: 音频数据块 + + Returns: + 音频级别(0.0-1.0) + """ + try: + import struct + + # 解析音频数据 + samples = struct.unpack(f'<{len(audio_chunk)//2}h', audio_chunk) + + # 计算RMS + if len(samples) > 0: + rms = math.sqrt(sum(sample**2 for sample in samples) / len(samples)) + # 归一化到0-1范围(假设16位音频) + level = rms / 32768.0 + return min(1.0, level) + else: + return 0.0 + + except Exception as e: + print(f"✗ 音频级别计算失败: {e}") + return 0.0 + + def _process_audio_chunk(self, audio_data: Dict[str, Any]): + """ + 处理音频块 + + Args: + audio_data: 音频数据字典 + """ + try: + start_time = time.time() + + # 模拟语音识别处理 + processing_result = self._simulate_recognition(audio_data) + + if processing_result: + # 添加到处理结果缓冲区 + try: + self.processed_buffer.put_nowait(processing_result) + except queue.Full: + try: + self.processed_buffer.get_nowait() + self.processed_buffer.put_nowait(processing_result) + except queue.Empty: + pass + + # 触发识别结果回调 + self._trigger_recognition_callback('recognition_result', processing_result) + + # 传递给语音管理器处理 + if self.plugin.voice_manager: + self.plugin.voice_manager.process_speech( + processing_result['text'], + processing_result['confidence'] + ) + + self.stats['successful_recognitions'] += 1 + + # 更新统计信息 + processing_time = time.time() - start_time + self.stats['processing_time_total'] += processing_time + self.stats['recognition_attempts'] += 1 + self.last_recognition_time = time.time() + + except Exception as e: + print(f"✗ 音频块处理失败: {e}") + self.stats['failed_recognitions'] += 1 + + # 触发错误回调 + self._trigger_recognition_callback('recognition_error', { + 'error': str(e), + 'timestamp': time.time() + }) + + def _simulate_recognition(self, audio_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: + """ + 模拟语音识别 + + Args: + audio_data: 音频数据 + + Returns: + 识别结果或None + """ + try: + # 基于音频级别和时间模拟识别结果 + audio_level = audio_data.get('level', 0.0) + timestamp = audio_data.get('timestamp', time.time()) + + # 只有在音频级别足够高时才模拟识别 + if audio_level > self.recognition_config['speech_threshold']: + import random + + # 模拟识别文本 + sample_texts = [ + "前进", "后退", "左转", "右转", "停止", + "打开灯光", "关闭灯光", "增加音量", "减少音量", + "播放音乐", "暂停播放", "下一首", "上一首", + "你好", "谢谢", "再见", "帮助" + ] + + # 根据音频级别设置置信度 + confidence = min(1.0, audio_level * 2) # 简单映射 + + # 有一定概率生成识别结果 + if random.random() < 0.1: # 10%概率 + text = random.choice(sample_texts) + return { + 'text': text, + 'confidence': confidence, + 'timestamp': timestamp, + 'audio_level': audio_level + } + + return None + + except Exception as e: + print(f"✗ 识别模拟失败: {e}") + return None + + def start_recording(self) -> bool: + """ + 开始录音 + + Returns: + 是否开始成功 + """ + try: + if not self.enabled: + print("✗ 语音识别器未启用") + return False + + self.recognition_state['is_recording'] = True + self.recognition_state['recording_start_time'] = time.time() + + print("✓ 录音已开始") + return True + + except Exception as e: + print(f"✗ 录音开始失败: {e}") + return False + + def stop_recording(self) -> bool: + """ + 停止录音 + + Returns: + 是否停止成功 + """ + try: + self.recognition_state['is_recording'] = False + print("✓ 录音已停止") + return True + + except Exception as e: + print(f"✗ 录音停止失败: {e}") + return False + + def is_recording(self) -> bool: + """ + 检查是否正在录音 + + Returns: + 是否正在录音 + """ + return self.recognition_state['is_recording'] + + def is_speech_detected(self) -> bool: + """ + 检查是否检测到语音 + + Returns: + 是否检测到语音 + """ + return self.recognition_state['is_speech_detected'] + + def get_audio_level(self) -> float: + """ + 获取当前音频级别 + + Returns: + 音频级别(0.0-1.0) + """ + return self.recognition_state['audio_level'] + + def set_recognition_config(self, config: Dict[str, Any]) -> bool: + """ + 设置识别配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.recognition_config.update(config) + print(f"✓ 识别配置已更新: {self.recognition_config}") + return True + except Exception as e: + print(f"✗ 识别配置设置失败: {e}") + return False + + def get_recognition_config(self) -> Dict[str, Any]: + """ + 获取识别配置 + + Returns: + 配置字典 + """ + return self.recognition_config.copy() + + def set_audio_config(self, config: Dict[str, Any]) -> bool: + """ + 设置音频配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.audio_config.update(config) + print(f"✓ 音频配置已更新: {self.audio_config}") + return True + except Exception as e: + print(f"✗ 音频配置设置失败: {e}") + return False + + def get_audio_config(self) -> Dict[str, Any]: + """ + 获取音频配置 + + Returns: + 配置字典 + """ + return self.audio_config.copy() + + def _trigger_recognition_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发识别回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.recognition_callbacks: + for callback in self.recognition_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 识别回调执行失败: {e}") + except Exception as e: + print(f"✗ 识别回调触发失败: {e}") + + def register_recognition_callback(self, callback_type: str, callback: callable): + """ + 注册识别回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.recognition_callbacks: + self.recognition_callbacks[callback_type].append(callback) + print(f"✓ 识别回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 识别回调注册失败: {e}") + + def unregister_recognition_callback(self, callback_type: str, callback: callable): + """ + 注销识别回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.recognition_callbacks: + if callback in self.recognition_callbacks[callback_type]: + self.recognition_callbacks[callback_type].remove(callback) + print(f"✓ 识别回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 识别回调注销失败: {e}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + stats = self.stats.copy() + if self.stats['successful_recognitions'] > 0: + stats['average_processing_time'] = ( + self.stats['processing_time_total'] / self.stats['successful_recognitions'] + ) + else: + stats['average_processing_time'] = 0.0 + return stats + + def reset_stats(self): + """重置统计信息""" + try: + self.stats = { + 'audio_chunks_processed': 0, + 'speech_segments_detected': 0, + 'recognition_attempts': 0, + 'successful_recognitions': 0, + 'failed_recognitions': 0, + 'processing_time_total': 0.0 + } + print("✓ 语音识别器统计信息已重置") + except Exception as e: print(f"✗ 语音识别器统计信息重置失败: {e}") \ No newline at end of file diff --git a/plugins/user/voice_control/synthesis/speech_synthesizer.py b/plugins/user/voice_control/synthesis/speech_synthesizer.py index 7ad41329..9c7307bb 100644 --- a/plugins/user/voice_control/synthesis/speech_synthesizer.py +++ b/plugins/user/voice_control/synthesis/speech_synthesizer.py @@ -1,837 +1,837 @@ -""" -语音合成器模块 -负责文字转语音功能 -""" - -import time -import threading -import queue -from typing import Dict, Any, List, Optional - -class SpeechSynthesizer: - """ - 语音合成器 - 负责文字转语音功能 - """ - - def __init__(self, plugin): - """ - 初始化语音合成器 - - Args: - plugin: 语音控制插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 音频输出配置 - self.audio_config = { - 'sample_rate': 22050, - 'channels': 1, - 'format': 'int16', - 'buffer_size': 4096 - } - - # 合成配置 - self.synthesis_config = { - 'language': 'zh-CN', - 'voice': 'default', - 'speed': 1.0, - 'volume': 1.0, - 'pitch': 1.0, - 'enable_ssml': False, - 'output_device': 'default' - } - - # 合成队列 - self.synthesis_queue = queue.Queue(maxsize=10) - self.playback_queue = queue.Queue(maxsize=5) - - # 合成状态 - self.synthesis_state = { - 'is_synthesizing': False, - 'is_playing': False, - 'current_text': '', - 'progress': 0.0, - 'last_synthesis_time': 0.0 - } - - # 音频输出管理 - self.audio_output = None - self.audio_stream = None - - # 线程管理 - self.synthesis_thread = None - self.playback_thread = None - self.running = False - - # 语音库 - self.voice_library = {} - - # 统计信息 - self.stats = { - 'texts_synthesized': 0, - 'synthesis_attempts': 0, - 'successful_syntheses': 0, - 'failed_syntheses': 0, - 'playback_count': 0, - 'synthesis_time_total': 0.0 - } - - # 回调函数 - self.synthesis_callbacks = { - 'synthesis_started': [], - 'synthesis_completed': [], - 'synthesis_error': [], - 'playback_started': [], - 'playback_completed': [], - 'playback_error': [] - } - - # 时间戳记录 - self.last_synthesis_time = 0.0 - self.last_playback_time = 0.0 - - print("✓ 语音合成器已创建") - - def initialize(self) -> bool: - """ - 初始化语音合成器 - - Returns: - 是否初始化成功 - """ - try: - # 初始化音频输出设备 - self._initialize_audio_output() - - # 初始化语音库 - self._initialize_voice_library() - - self.initialized = True - print("✓ 语音合成器初始化完成") - return True - - except Exception as e: - print(f"✗ 语音合成器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用语音合成器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 语音合成器未初始化") - return False - - self.enabled = True - self.running = True - - # 启动合成线程 - self.synthesis_thread = threading.Thread(target=self._synthesis_loop, daemon=True) - self.synthesis_thread.start() - - # 启动播放线程 - self.playback_thread = threading.Thread(target=self._playback_loop, daemon=True) - self.playback_thread.start() - - print("✓ 语音合成器已启用") - return True - - except Exception as e: - print(f"✗ 语音合成器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用语音合成器""" - try: - self.enabled = False - self.running = False - - # 等待线程结束 - if self.synthesis_thread and self.synthesis_thread.is_alive(): - self.synthesis_thread.join(timeout=2.0) - - if self.playback_thread and self.playback_thread.is_alive(): - self.playback_thread.join(timeout=2.0) - - # 停止音频流 - self._stop_audio_stream() - - print("✓ 语音合成器已禁用") - - except Exception as e: - print(f"✗ 语音合成器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理语音合成器资源""" - try: - self.disable() - self.synthesis_callbacks.clear() - - # 清理队列 - while not self.synthesis_queue.empty(): - try: - self.synthesis_queue.get_nowait() - except queue.Empty: - break - - while not self.playback_queue.empty(): - try: - self.playback_queue.get_nowait() - except queue.Empty: - break - - self.initialized = False - print("✓ 语音合成器资源已清理") - - except Exception as e: - print(f"✗ 语音合成器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新语音合成器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - current_time = time.time() - self.last_synthesis_time = current_time - - except Exception as e: - print(f"✗ 语音合成器更新失败: {e}") - import traceback - traceback.print_exc() - - def _initialize_audio_output(self): - """初始化音频输出设备""" - try: - # 这里应该初始化实际的音频输出设备 - # 由于这是一个模拟实现,我们只打印信息 - print("✓ 音频输出设备初始化完成") - print(f" 采样率: {self.audio_config['sample_rate']} Hz") - print(f" 声道数: {self.audio_config['channels']}") - print(f" 格式: {self.audio_config['format']}") - - except Exception as e: - print(f"✗ 音频输出设备初始化失败: {e}") - - def _initialize_voice_library(self): - """初始化语音库""" - try: - # 创建模拟语音库 - self.voice_library = { - 'default': { - 'name': '默认语音', - 'language': 'zh-CN', - 'gender': 'neutral', - 'description': '系统默认语音' - }, - 'male': { - 'name': '男声', - 'language': 'zh-CN', - 'gender': 'male', - 'description': '标准男声' - }, - 'female': { - 'name': '女声', - 'language': 'zh-CN', - 'gender': 'female', - 'description': '标准女声' - }, - 'child': { - 'name': '童声', - 'language': 'zh-CN', - 'gender': 'child', - 'description': '儿童语音' - } - } - - print("✓ 语音库初始化完成") - print(f" 可用语音: {list(self.voice_library.keys())}") - - except Exception as e: - print(f"✗ 语音库初始化失败: {e}") - - def _start_audio_stream(self) -> bool: - """ - 启动音频流 - - Returns: - 是否启动成功 - """ - try: - # 这里应该启动实际的音频流 - # 由于这是一个模拟实现,我们只设置状态 - self.audio_stream = "simulated_audio_stream" - print("✓ 音频流已启动") - return True - except Exception as e: - print(f"✗ 音频流启动失败: {e}") - return False - - def _stop_audio_stream(self): - """停止音频流""" - try: - # 这里应该停止实际的音频流 - self.audio_stream = None - print("✓ 音频流已停止") - except Exception as e: - print(f"✗ 音频流停止失败: {e}") - - def _synthesis_loop(self): - """合成循环线程""" - try: - # 启动音频流 - if not self._start_audio_stream(): - return - - while self.running and self.enabled: - try: - # 从队列获取合成任务 - if not self.synthesis_queue.empty(): - synthesis_task = self.synthesis_queue.get_nowait() - self._process_synthesis_task(synthesis_task) - - # 控制处理频率 - time.sleep(0.01) - - except queue.Empty: - time.sleep(0.01) - except Exception as e: - print(f"✗ 合成循环错误: {e}") - time.sleep(0.1) - - except Exception as e: - print(f"✗ 合成循环失败: {e}") - - def _playback_loop(self): - """播放循环线程""" - try: - while self.running and self.enabled: - try: - # 从队列获取播放任务 - if not self.playback_queue.empty(): - playback_task = self.playback_queue.get_nowait() - self._process_playback_task(playback_task) - - # 控制播放频率 - time.sleep(0.01) - - except queue.Empty: - time.sleep(0.01) - except Exception as e: - print(f"✗ 播放循环错误: {e}") - time.sleep(0.1) - - except Exception as e: - print(f"✗ 播放循环失败: {e}") - - def _process_synthesis_task(self, task: Dict[str, Any]): - """ - 处理合成任务 - - Args: - task: 合成任务字典 - """ - try: - start_time = time.time() - - text = task.get('text', '') - speed = task.get('speed', 1.0) - volume = task.get('volume', 1.0) - blocking = task.get('blocking', False) - - # 更新合成状态 - self.synthesis_state['is_synthesizing'] = True - self.synthesis_state['current_text'] = text - self.synthesis_state['last_synthesis_time'] = start_time - - # 触发合成开始回调 - self._trigger_synthesis_callback('synthesis_started', { - 'text': text, - 'speed': speed, - 'volume': volume - }) - - # 模拟语音合成过程 - synthesized_audio = self._synthesize_text(text, speed, volume) - - if synthesized_audio is not None: - # 添加到播放队列 - try: - self.playback_queue.put_nowait({ - 'audio': synthesized_audio, - 'text': text, - 'blocking': blocking, - 'timestamp': time.time() - }) - - # 更新统计信息 - self.stats['successful_syntheses'] += 1 - - # 触发合成完成回调 - self._trigger_synthesis_callback('synthesis_completed', { - 'text': text, - 'audio_length': len(synthesized_audio), - 'synthesis_time': time.time() - start_time - }) - - except queue.Full: - print("✗ 播放队列已满,丢弃合成结果") - self.stats['failed_syntheses'] += 1 - - # 触发合成错误回调 - self._trigger_synthesis_callback('synthesis_error', { - 'text': text, - 'error': 'Playback queue full' - }) - else: - self.stats['failed_syntheses'] += 1 - - # 触发合成错误回调 - self._trigger_synthesis_callback('synthesis_error', { - 'text': text, - 'error': 'Synthesis failed' - }) - - # 更新统计信息 - synthesis_time = time.time() - start_time - self.stats['synthesis_time_total'] += synthesis_time - self.stats['synthesis_attempts'] += 1 - self.last_synthesis_time = time.time() - - except Exception as e: - print(f"✗ 合成任务处理失败: {e}") - self.stats['failed_syntheses'] += 1 - - finally: - # 重置合成状态 - self.synthesis_state['is_synthesizing'] = False - self.synthesis_state['progress'] = 0.0 - - def _process_playback_task(self, task: Dict[str, Any]): - """ - 处理播放任务 - - Args: - task: 播放任务字典 - """ - try: - audio_data = task.get('audio', b'') - text = task.get('text', '') - blocking = task.get('blocking', False) - - # 更新播放状态 - self.synthesis_state['is_playing'] = True - self.synthesis_state['current_text'] = text - - # 触发播放开始回调 - self._trigger_synthesis_callback('playback_started', { - 'text': text, - 'audio_length': len(audio_data) - }) - - # 模拟音频播放 - playback_success = self._play_audio(audio_data, blocking) - - if playback_success: - self.stats['playback_count'] += 1 - - # 触发播放完成回调 - self._trigger_synthesis_callback('playback_completed', { - 'text': text, - 'playback_time': len(audio_data) / self.audio_config['sample_rate'] - }) - else: - # 触发播放错误回调 - self._trigger_synthesis_callback('playback_error', { - 'text': text, - 'error': 'Playback failed' - }) - - self.last_playback_time = time.time() - - except Exception as e: - print(f"✗ 播放任务处理失败: {e}") - - # 触发播放错误回调 - self._trigger_synthesis_callback('playback_error', { - 'text': task.get('text', ''), - 'error': str(e) - }) - - finally: - # 重置播放状态 - self.synthesis_state['is_playing'] = False - - def _synthesize_text(self, text: str, speed: float, volume: float) -> Optional[bytes]: - """ - 合成文本为音频 - - Args: - text: 要合成的文本 - speed: 语速 - volume: 音量 - - Returns: - 音频数据或None - """ - try: - # 模拟文本到语音的转换过程 - import random - import struct - - # 根据文本长度计算音频长度 - char_count = len(text) - # 假设每个字符大约需要0.1秒 - duration_seconds = char_count * 0.1 * (1.0 / speed) - - # 计算样本数 - sample_rate = self.audio_config['sample_rate'] - total_samples = int(duration_seconds * sample_rate) - - # 生成模拟的音频数据 - audio_data = bytearray() - - # 生成简单的正弦波作为语音模拟 - frequency = 440.0 # A音 - for i in range(total_samples): - # 添加一些变化使声音更自然 - t = i / sample_rate - wave_value = math.sin(2 * math.pi * frequency * t) - - # 添加谐波 - wave_value += 0.3 * math.sin(2 * math.pi * frequency * 2 * t) - wave_value += 0.2 * math.sin(2 * math.pi * frequency * 3 * t) - - # 应用音量 - wave_value *= volume - - # 转换为16位整数 - sample = int(wave_value * 32767 * 0.3) # 降低整体音量 - audio_data.extend(struct.pack(' bool: - """ - 播放音频数据 - - Args: - audio_data: 音频数据 - blocking: 是否阻塞等待播放完成 - - Returns: - 是否播放成功 - """ - try: - # 模拟音频播放 - import time - import math - - # 计算播放时间 - sample_count = len(audio_data) // 2 # 16位音频 - duration = sample_count / self.audio_config['sample_rate'] - - print(f"✓ 播放音频: {duration:.2f}秒") - - # 如果是阻塞模式,等待播放完成 - if blocking: - time.sleep(duration) - - return True - - except Exception as e: - print(f"✗ 音频播放失败: {e}") - return False - - def synthesize_speech(self, text: str, blocking: bool = False, - speed: float = 1.0, volume: float = 1.0) -> bool: - """ - 合成并播放语音 - - Args: - text: 要合成的文本 - blocking: 是否阻塞等待播放完成 - speed: 语速(0.5-2.0) - volume: 音量(0.0-1.0) - - Returns: - 是否合成成功 - """ - try: - if not self.enabled: - print("✗ 语音合成器未启用") - return False - - # 限制参数范围 - speed = max(0.5, min(2.0, speed)) - volume = max(0.0, min(1.0, volume)) - - # 创建合成任务 - synthesis_task = { - 'text': text, - 'speed': speed, - 'volume': volume, - 'blocking': blocking, - 'timestamp': time.time() - } - - # 添加到合成队列 - try: - self.synthesis_queue.put_nowait(synthesis_task) - self.stats['texts_synthesized'] += 1 - return True - except queue.Full: - print("✗ 合成队列已满") - return False - - except Exception as e: - print(f"✗ 语音合成失败: {e}") - self.stats['failed_syntheses'] += 1 - return False - - def stop_speech(self) -> bool: - """ - 停止当前语音播放 - - Returns: - 是否停止成功 - """ - try: - # 清空播放队列 - while not self.playback_queue.empty(): - try: - self.playback_queue.get_nowait() - except queue.Empty: - break - - # 重置播放状态 - self.synthesis_state['is_playing'] = False - self.synthesis_state['current_text'] = '' - - print("✓ 语音播放已停止") - return True - - except Exception as e: - print(f"✗ 语音播放停止失败: {e}") - return False - - def get_available_voices(self) -> Dict[str, Dict[str, Any]]: - """ - 获取可用语音列表 - - Returns: - 语音信息字典 - """ - return self.voice_library.copy() - - def set_voice(self, voice_name: str) -> bool: - """ - 设置语音 - - Args: - voice_name: 语音名称 - - Returns: - 是否设置成功 - """ - try: - if voice_name in self.voice_library: - self.synthesis_config['voice'] = voice_name - print(f"✓ 语音已设置为: {voice_name}") - return True - else: - print(f"✗ 语音不存在: {voice_name}") - return False - except Exception as e: - print(f"✗ 语音设置失败: {e}") - return False - - def set_synthesis_config(self, config: Dict[str, Any]) -> bool: - """ - 设置合成配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.synthesis_config.update(config) - print(f"✓ 合成配置已更新: {self.synthesis_config}") - return True - except Exception as e: - print(f"✗ 合成配置设置失败: {e}") - return False - - def get_synthesis_config(self) -> Dict[str, Any]: - """ - 获取合成配置 - - Returns: - 配置字典 - """ - return self.synthesis_config.copy() - - def set_audio_config(self, config: Dict[str, Any]) -> bool: - """ - 设置音频配置 - - Args: - config: 配置字典 - - Returns: - 是否设置成功 - """ - try: - self.audio_config.update(config) - print(f"✓ 音频配置已更新: {self.audio_config}") - return True - except Exception as e: - print(f"✗ 音频配置设置失败: {e}") - return False - - def get_audio_config(self) -> Dict[str, Any]: - """ - 获取音频配置 - - Returns: - 配置字典 - """ - return self.audio_config.copy() - - def is_speaking(self) -> bool: - """ - 检查是否正在播放语音 - - Returns: - 是否正在播放语音 - """ - return self.synthesis_state['is_playing'] - - def is_synthesizing(self) -> bool: - """ - 检查是否正在合成语音 - - Returns: - 是否正在合成语音 - """ - return self.synthesis_state['is_synthesizing'] - - def get_synthesis_progress(self) -> float: - """ - 获取合成进度 - - Returns: - 合成进度(0.0-1.0) - """ - return self.synthesis_state['progress'] - - def _trigger_synthesis_callback(self, callback_type: str, data: Dict[str, Any]): - """ - 触发合成回调 - - Args: - callback_type: 回调类型 - data: 回调数据 - """ - try: - if callback_type in self.synthesis_callbacks: - for callback in self.synthesis_callbacks[callback_type]: - try: - callback(data) - except Exception as e: - print(f"✗ 合成回调执行失败: {e}") - except Exception as e: - print(f"✗ 合成回调触发失败: {e}") - - def register_synthesis_callback(self, callback_type: str, callback: callable): - """ - 注册合成回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.synthesis_callbacks: - self.synthesis_callbacks[callback_type].append(callback) - print(f"✓ 合成回调已注册: {callback_type}") - else: - print(f"✗ 无效的回调类型: {callback_type}") - except Exception as e: - print(f"✗ 合成回调注册失败: {e}") - - def unregister_synthesis_callback(self, callback_type: str, callback: callable): - """ - 注销合成回调 - - Args: - callback_type: 回调类型 - callback: 回调函数 - """ - try: - if callback_type in self.synthesis_callbacks: - if callback in self.synthesis_callbacks[callback_type]: - self.synthesis_callbacks[callback_type].remove(callback) - print(f"✓ 合成回调已注销: {callback_type}") - except Exception as e: - print(f"✗ 合成回调注销失败: {e}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - stats = self.stats.copy() - if self.stats['successful_syntheses'] > 0: - stats['average_synthesis_time'] = ( - self.stats['synthesis_time_total'] / self.stats['successful_syntheses'] - ) - else: - stats['average_synthesis_time'] = 0.0 - return stats - - def reset_stats(self): - """重置统计信息""" - try: - self.stats = { - 'texts_synthesized': 0, - 'synthesis_attempts': 0, - 'successful_syntheses': 0, - 'failed_syntheses': 0, - 'playback_count': 0, - 'synthesis_time_total': 0.0 - } - print("✓ 语音合成器统计信息已重置") - except Exception as e: - print(f"✗ 语音合成器统计信息重置失败: {e}") - -# 导入math模块用于正弦波计算 +""" +语音合成器模块 +负责文字转语音功能 +""" + +import time +import threading +import queue +from typing import Dict, Any, List, Optional + +class SpeechSynthesizer: + """ + 语音合成器 + 负责文字转语音功能 + """ + + def __init__(self, plugin): + """ + 初始化语音合成器 + + Args: + plugin: 语音控制插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 音频输出配置 + self.audio_config = { + 'sample_rate': 22050, + 'channels': 1, + 'format': 'int16', + 'buffer_size': 4096 + } + + # 合成配置 + self.synthesis_config = { + 'language': 'zh-CN', + 'voice': 'default', + 'speed': 1.0, + 'volume': 1.0, + 'pitch': 1.0, + 'enable_ssml': False, + 'output_device': 'default' + } + + # 合成队列 + self.synthesis_queue = queue.Queue(maxsize=10) + self.playback_queue = queue.Queue(maxsize=5) + + # 合成状态 + self.synthesis_state = { + 'is_synthesizing': False, + 'is_playing': False, + 'current_text': '', + 'progress': 0.0, + 'last_synthesis_time': 0.0 + } + + # 音频输出管理 + self.audio_output = None + self.audio_stream = None + + # 线程管理 + self.synthesis_thread = None + self.playback_thread = None + self.running = False + + # 语音库 + self.voice_library = {} + + # 统计信息 + self.stats = { + 'texts_synthesized': 0, + 'synthesis_attempts': 0, + 'successful_syntheses': 0, + 'failed_syntheses': 0, + 'playback_count': 0, + 'synthesis_time_total': 0.0 + } + + # 回调函数 + self.synthesis_callbacks = { + 'synthesis_started': [], + 'synthesis_completed': [], + 'synthesis_error': [], + 'playback_started': [], + 'playback_completed': [], + 'playback_error': [] + } + + # 时间戳记录 + self.last_synthesis_time = 0.0 + self.last_playback_time = 0.0 + + print("✓ 语音合成器已创建") + + def initialize(self) -> bool: + """ + 初始化语音合成器 + + Returns: + 是否初始化成功 + """ + try: + # 初始化音频输出设备 + self._initialize_audio_output() + + # 初始化语音库 + self._initialize_voice_library() + + self.initialized = True + print("✓ 语音合成器初始化完成") + return True + + except Exception as e: + print(f"✗ 语音合成器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用语音合成器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 语音合成器未初始化") + return False + + self.enabled = True + self.running = True + + # 启动合成线程 + self.synthesis_thread = threading.Thread(target=self._synthesis_loop, daemon=True) + self.synthesis_thread.start() + + # 启动播放线程 + self.playback_thread = threading.Thread(target=self._playback_loop, daemon=True) + self.playback_thread.start() + + print("✓ 语音合成器已启用") + return True + + except Exception as e: + print(f"✗ 语音合成器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用语音合成器""" + try: + self.enabled = False + self.running = False + + # 等待线程结束 + if self.synthesis_thread and self.synthesis_thread.is_alive(): + self.synthesis_thread.join(timeout=2.0) + + if self.playback_thread and self.playback_thread.is_alive(): + self.playback_thread.join(timeout=2.0) + + # 停止音频流 + self._stop_audio_stream() + + print("✓ 语音合成器已禁用") + + except Exception as e: + print(f"✗ 语音合成器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理语音合成器资源""" + try: + self.disable() + self.synthesis_callbacks.clear() + + # 清理队列 + while not self.synthesis_queue.empty(): + try: + self.synthesis_queue.get_nowait() + except queue.Empty: + break + + while not self.playback_queue.empty(): + try: + self.playback_queue.get_nowait() + except queue.Empty: + break + + self.initialized = False + print("✓ 语音合成器资源已清理") + + except Exception as e: + print(f"✗ 语音合成器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新语音合成器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + current_time = time.time() + self.last_synthesis_time = current_time + + except Exception as e: + print(f"✗ 语音合成器更新失败: {e}") + import traceback + traceback.print_exc() + + def _initialize_audio_output(self): + """初始化音频输出设备""" + try: + # 这里应该初始化实际的音频输出设备 + # 由于这是一个模拟实现,我们只打印信息 + print("✓ 音频输出设备初始化完成") + print(f" 采样率: {self.audio_config['sample_rate']} Hz") + print(f" 声道数: {self.audio_config['channels']}") + print(f" 格式: {self.audio_config['format']}") + + except Exception as e: + print(f"✗ 音频输出设备初始化失败: {e}") + + def _initialize_voice_library(self): + """初始化语音库""" + try: + # 创建模拟语音库 + self.voice_library = { + 'default': { + 'name': '默认语音', + 'language': 'zh-CN', + 'gender': 'neutral', + 'description': '系统默认语音' + }, + 'male': { + 'name': '男声', + 'language': 'zh-CN', + 'gender': 'male', + 'description': '标准男声' + }, + 'female': { + 'name': '女声', + 'language': 'zh-CN', + 'gender': 'female', + 'description': '标准女声' + }, + 'child': { + 'name': '童声', + 'language': 'zh-CN', + 'gender': 'child', + 'description': '儿童语音' + } + } + + print("✓ 语音库初始化完成") + print(f" 可用语音: {list(self.voice_library.keys())}") + + except Exception as e: + print(f"✗ 语音库初始化失败: {e}") + + def _start_audio_stream(self) -> bool: + """ + 启动音频流 + + Returns: + 是否启动成功 + """ + try: + # 这里应该启动实际的音频流 + # 由于这是一个模拟实现,我们只设置状态 + self.audio_stream = "simulated_audio_stream" + print("✓ 音频流已启动") + return True + except Exception as e: + print(f"✗ 音频流启动失败: {e}") + return False + + def _stop_audio_stream(self): + """停止音频流""" + try: + # 这里应该停止实际的音频流 + self.audio_stream = None + print("✓ 音频流已停止") + except Exception as e: + print(f"✗ 音频流停止失败: {e}") + + def _synthesis_loop(self): + """合成循环线程""" + try: + # 启动音频流 + if not self._start_audio_stream(): + return + + while self.running and self.enabled: + try: + # 从队列获取合成任务 + if not self.synthesis_queue.empty(): + synthesis_task = self.synthesis_queue.get_nowait() + self._process_synthesis_task(synthesis_task) + + # 控制处理频率 + time.sleep(0.01) + + except queue.Empty: + time.sleep(0.01) + except Exception as e: + print(f"✗ 合成循环错误: {e}") + time.sleep(0.1) + + except Exception as e: + print(f"✗ 合成循环失败: {e}") + + def _playback_loop(self): + """播放循环线程""" + try: + while self.running and self.enabled: + try: + # 从队列获取播放任务 + if not self.playback_queue.empty(): + playback_task = self.playback_queue.get_nowait() + self._process_playback_task(playback_task) + + # 控制播放频率 + time.sleep(0.01) + + except queue.Empty: + time.sleep(0.01) + except Exception as e: + print(f"✗ 播放循环错误: {e}") + time.sleep(0.1) + + except Exception as e: + print(f"✗ 播放循环失败: {e}") + + def _process_synthesis_task(self, task: Dict[str, Any]): + """ + 处理合成任务 + + Args: + task: 合成任务字典 + """ + try: + start_time = time.time() + + text = task.get('text', '') + speed = task.get('speed', 1.0) + volume = task.get('volume', 1.0) + blocking = task.get('blocking', False) + + # 更新合成状态 + self.synthesis_state['is_synthesizing'] = True + self.synthesis_state['current_text'] = text + self.synthesis_state['last_synthesis_time'] = start_time + + # 触发合成开始回调 + self._trigger_synthesis_callback('synthesis_started', { + 'text': text, + 'speed': speed, + 'volume': volume + }) + + # 模拟语音合成过程 + synthesized_audio = self._synthesize_text(text, speed, volume) + + if synthesized_audio is not None: + # 添加到播放队列 + try: + self.playback_queue.put_nowait({ + 'audio': synthesized_audio, + 'text': text, + 'blocking': blocking, + 'timestamp': time.time() + }) + + # 更新统计信息 + self.stats['successful_syntheses'] += 1 + + # 触发合成完成回调 + self._trigger_synthesis_callback('synthesis_completed', { + 'text': text, + 'audio_length': len(synthesized_audio), + 'synthesis_time': time.time() - start_time + }) + + except queue.Full: + print("✗ 播放队列已满,丢弃合成结果") + self.stats['failed_syntheses'] += 1 + + # 触发合成错误回调 + self._trigger_synthesis_callback('synthesis_error', { + 'text': text, + 'error': 'Playback queue full' + }) + else: + self.stats['failed_syntheses'] += 1 + + # 触发合成错误回调 + self._trigger_synthesis_callback('synthesis_error', { + 'text': text, + 'error': 'Synthesis failed' + }) + + # 更新统计信息 + synthesis_time = time.time() - start_time + self.stats['synthesis_time_total'] += synthesis_time + self.stats['synthesis_attempts'] += 1 + self.last_synthesis_time = time.time() + + except Exception as e: + print(f"✗ 合成任务处理失败: {e}") + self.stats['failed_syntheses'] += 1 + + finally: + # 重置合成状态 + self.synthesis_state['is_synthesizing'] = False + self.synthesis_state['progress'] = 0.0 + + def _process_playback_task(self, task: Dict[str, Any]): + """ + 处理播放任务 + + Args: + task: 播放任务字典 + """ + try: + audio_data = task.get('audio', b'') + text = task.get('text', '') + blocking = task.get('blocking', False) + + # 更新播放状态 + self.synthesis_state['is_playing'] = True + self.synthesis_state['current_text'] = text + + # 触发播放开始回调 + self._trigger_synthesis_callback('playback_started', { + 'text': text, + 'audio_length': len(audio_data) + }) + + # 模拟音频播放 + playback_success = self._play_audio(audio_data, blocking) + + if playback_success: + self.stats['playback_count'] += 1 + + # 触发播放完成回调 + self._trigger_synthesis_callback('playback_completed', { + 'text': text, + 'playback_time': len(audio_data) / self.audio_config['sample_rate'] + }) + else: + # 触发播放错误回调 + self._trigger_synthesis_callback('playback_error', { + 'text': text, + 'error': 'Playback failed' + }) + + self.last_playback_time = time.time() + + except Exception as e: + print(f"✗ 播放任务处理失败: {e}") + + # 触发播放错误回调 + self._trigger_synthesis_callback('playback_error', { + 'text': task.get('text', ''), + 'error': str(e) + }) + + finally: + # 重置播放状态 + self.synthesis_state['is_playing'] = False + + def _synthesize_text(self, text: str, speed: float, volume: float) -> Optional[bytes]: + """ + 合成文本为音频 + + Args: + text: 要合成的文本 + speed: 语速 + volume: 音量 + + Returns: + 音频数据或None + """ + try: + # 模拟文本到语音的转换过程 + import random + import struct + + # 根据文本长度计算音频长度 + char_count = len(text) + # 假设每个字符大约需要0.1秒 + duration_seconds = char_count * 0.1 * (1.0 / speed) + + # 计算样本数 + sample_rate = self.audio_config['sample_rate'] + total_samples = int(duration_seconds * sample_rate) + + # 生成模拟的音频数据 + audio_data = bytearray() + + # 生成简单的正弦波作为语音模拟 + frequency = 440.0 # A音 + for i in range(total_samples): + # 添加一些变化使声音更自然 + t = i / sample_rate + wave_value = math.sin(2 * math.pi * frequency * t) + + # 添加谐波 + wave_value += 0.3 * math.sin(2 * math.pi * frequency * 2 * t) + wave_value += 0.2 * math.sin(2 * math.pi * frequency * 3 * t) + + # 应用音量 + wave_value *= volume + + # 转换为16位整数 + sample = int(wave_value * 32767 * 0.3) # 降低整体音量 + audio_data.extend(struct.pack(' bool: + """ + 播放音频数据 + + Args: + audio_data: 音频数据 + blocking: 是否阻塞等待播放完成 + + Returns: + 是否播放成功 + """ + try: + # 模拟音频播放 + import time + import math + + # 计算播放时间 + sample_count = len(audio_data) // 2 # 16位音频 + duration = sample_count / self.audio_config['sample_rate'] + + print(f"✓ 播放音频: {duration:.2f}秒") + + # 如果是阻塞模式,等待播放完成 + if blocking: + time.sleep(duration) + + return True + + except Exception as e: + print(f"✗ 音频播放失败: {e}") + return False + + def synthesize_speech(self, text: str, blocking: bool = False, + speed: float = 1.0, volume: float = 1.0) -> bool: + """ + 合成并播放语音 + + Args: + text: 要合成的文本 + blocking: 是否阻塞等待播放完成 + speed: 语速(0.5-2.0) + volume: 音量(0.0-1.0) + + Returns: + 是否合成成功 + """ + try: + if not self.enabled: + print("✗ 语音合成器未启用") + return False + + # 限制参数范围 + speed = max(0.5, min(2.0, speed)) + volume = max(0.0, min(1.0, volume)) + + # 创建合成任务 + synthesis_task = { + 'text': text, + 'speed': speed, + 'volume': volume, + 'blocking': blocking, + 'timestamp': time.time() + } + + # 添加到合成队列 + try: + self.synthesis_queue.put_nowait(synthesis_task) + self.stats['texts_synthesized'] += 1 + return True + except queue.Full: + print("✗ 合成队列已满") + return False + + except Exception as e: + print(f"✗ 语音合成失败: {e}") + self.stats['failed_syntheses'] += 1 + return False + + def stop_speech(self) -> bool: + """ + 停止当前语音播放 + + Returns: + 是否停止成功 + """ + try: + # 清空播放队列 + while not self.playback_queue.empty(): + try: + self.playback_queue.get_nowait() + except queue.Empty: + break + + # 重置播放状态 + self.synthesis_state['is_playing'] = False + self.synthesis_state['current_text'] = '' + + print("✓ 语音播放已停止") + return True + + except Exception as e: + print(f"✗ 语音播放停止失败: {e}") + return False + + def get_available_voices(self) -> Dict[str, Dict[str, Any]]: + """ + 获取可用语音列表 + + Returns: + 语音信息字典 + """ + return self.voice_library.copy() + + def set_voice(self, voice_name: str) -> bool: + """ + 设置语音 + + Args: + voice_name: 语音名称 + + Returns: + 是否设置成功 + """ + try: + if voice_name in self.voice_library: + self.synthesis_config['voice'] = voice_name + print(f"✓ 语音已设置为: {voice_name}") + return True + else: + print(f"✗ 语音不存在: {voice_name}") + return False + except Exception as e: + print(f"✗ 语音设置失败: {e}") + return False + + def set_synthesis_config(self, config: Dict[str, Any]) -> bool: + """ + 设置合成配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.synthesis_config.update(config) + print(f"✓ 合成配置已更新: {self.synthesis_config}") + return True + except Exception as e: + print(f"✗ 合成配置设置失败: {e}") + return False + + def get_synthesis_config(self) -> Dict[str, Any]: + """ + 获取合成配置 + + Returns: + 配置字典 + """ + return self.synthesis_config.copy() + + def set_audio_config(self, config: Dict[str, Any]) -> bool: + """ + 设置音频配置 + + Args: + config: 配置字典 + + Returns: + 是否设置成功 + """ + try: + self.audio_config.update(config) + print(f"✓ 音频配置已更新: {self.audio_config}") + return True + except Exception as e: + print(f"✗ 音频配置设置失败: {e}") + return False + + def get_audio_config(self) -> Dict[str, Any]: + """ + 获取音频配置 + + Returns: + 配置字典 + """ + return self.audio_config.copy() + + def is_speaking(self) -> bool: + """ + 检查是否正在播放语音 + + Returns: + 是否正在播放语音 + """ + return self.synthesis_state['is_playing'] + + def is_synthesizing(self) -> bool: + """ + 检查是否正在合成语音 + + Returns: + 是否正在合成语音 + """ + return self.synthesis_state['is_synthesizing'] + + def get_synthesis_progress(self) -> float: + """ + 获取合成进度 + + Returns: + 合成进度(0.0-1.0) + """ + return self.synthesis_state['progress'] + + def _trigger_synthesis_callback(self, callback_type: str, data: Dict[str, Any]): + """ + 触发合成回调 + + Args: + callback_type: 回调类型 + data: 回调数据 + """ + try: + if callback_type in self.synthesis_callbacks: + for callback in self.synthesis_callbacks[callback_type]: + try: + callback(data) + except Exception as e: + print(f"✗ 合成回调执行失败: {e}") + except Exception as e: + print(f"✗ 合成回调触发失败: {e}") + + def register_synthesis_callback(self, callback_type: str, callback: callable): + """ + 注册合成回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.synthesis_callbacks: + self.synthesis_callbacks[callback_type].append(callback) + print(f"✓ 合成回调已注册: {callback_type}") + else: + print(f"✗ 无效的回调类型: {callback_type}") + except Exception as e: + print(f"✗ 合成回调注册失败: {e}") + + def unregister_synthesis_callback(self, callback_type: str, callback: callable): + """ + 注销合成回调 + + Args: + callback_type: 回调类型 + callback: 回调函数 + """ + try: + if callback_type in self.synthesis_callbacks: + if callback in self.synthesis_callbacks[callback_type]: + self.synthesis_callbacks[callback_type].remove(callback) + print(f"✓ 合成回调已注销: {callback_type}") + except Exception as e: + print(f"✗ 合成回调注销失败: {e}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + stats = self.stats.copy() + if self.stats['successful_syntheses'] > 0: + stats['average_synthesis_time'] = ( + self.stats['synthesis_time_total'] / self.stats['successful_syntheses'] + ) + else: + stats['average_synthesis_time'] = 0.0 + return stats + + def reset_stats(self): + """重置统计信息""" + try: + self.stats = { + 'texts_synthesized': 0, + 'synthesis_attempts': 0, + 'successful_syntheses': 0, + 'failed_syntheses': 0, + 'playback_count': 0, + 'synthesis_time_total': 0.0 + } + print("✓ 语音合成器统计信息已重置") + except Exception as e: + print(f"✗ 语音合成器统计信息重置失败: {e}") + +# 导入math模块用于正弦波计算 import math \ No newline at end of file diff --git a/plugins/user/voice_control/utils/voice_utils.py b/plugins/user/voice_control/utils/voice_utils.py index 05b341ef..2968b181 100644 --- a/plugins/user/voice_control/utils/voice_utils.py +++ b/plugins/user/voice_control/utils/voice_utils.py @@ -1,882 +1,882 @@ -""" -工具类模块 -提供语音控制插件所需的实用工具函数 -""" - -import time -import math -import os -import json -import threading -from typing import Dict, Any, List, Optional, Union -import hashlib - -class VoiceUtils: - """ - 语音工具类 - 提供语音控制插件所需的实用工具函数 - """ - - def __init__(self, plugin): - """ - 初始化语音工具类 - - Args: - plugin: 语音控制插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 性能监控 - self.performance_metrics = { - "update_times": [], - "processing_times": [], - "memory_usage": 0, - "cpu_usage": 0 - } - - # 日志配置 - self.log_config = { - "enable_logging": True, - "log_level": "INFO", - "log_file": "/home/hello/EG/plugins/user/voice_control/logs/voice.log", - "max_log_size": 10 * 1024 * 1024, # 10MB - "log_format": "[{timestamp}] {level}: {message}" - } - - # 数据转换配置 - self.conversion_config = { - "audio_level_curve": "linear", # linear, exponential, logarithmic - "confidence_mapping": "linear", - "duration_scaling": "linear" - } - - # 数学工具 - self.math_utils = MathUtils() - - # 缓存管理 - self.cache_manager = CacheManager() - - # 调试工具 - self.debug_tools = DebugTools() - - # 文件管理 - self.file_manager = FileManager() - - # 统计信息 - self.stats = { - "functions_called": 0, - "data_processed": 0, - "cache_hits": 0, - "cache_misses": 0, - "files_processed": 0 - } - - # 线程锁 - self.utils_lock = threading.RLock() - - print("✓ 语音工具类已创建") - - def initialize(self) -> bool: - """ - 初始化语音工具类 - - Returns: - 是否初始化成功 - """ - try: - # 创建日志目录 - log_dir = "/home/hello/EG/plugins/user/voice_control/logs" - if not os.path.exists(log_dir): - os.makedirs(log_dir) - - # 初始化子工具 - self.math_utils.initialize() - self.cache_manager.initialize() - self.debug_tools.initialize() - self.file_manager.initialize() - - self.initialized = True - print("✓ 语音工具类初始化完成") - return True - - except Exception as e: - print(f"✗ 语音工具类初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用语音工具类 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 语音工具类未初始化") - return False - - self.enabled = True - - # 启用子工具 - self.math_utils.enable() - self.cache_manager.enable() - self.debug_tools.enable() - self.file_manager.enable() - - print("✓ 语音工具类已启用") - return True - - except Exception as e: - print(f"✗ 语音工具类启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用语音工具类""" - try: - self.enabled = False - - # 禁用子工具 - self.math_utils.disable() - self.cache_manager.disable() - self.debug_tools.disable() - self.file_manager.disable() - - print("✓ 语音工具类已禁用") - - except Exception as e: - print(f"✗ 语音工具类禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理语音工具类资源""" - try: - self.disable() - self.performance_metrics.clear() - self.cache_manager.clear() - self.debug_tools.clear() - self.file_manager.clear() - self.initialized = False - print("✓ 语音工具类资源已清理") - - except Exception as e: - print(f"✗ 语音工具类资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新语音工具类状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新性能指标 - self._update_performance_metrics(dt) - - # 更新子工具 - self.cache_manager.update(dt) - self.debug_tools.update(dt) - self.file_manager.update(dt) - - except Exception as e: - print(f"✗ 语音工具类更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_performance_metrics(self, dt: float): - """更新性能指标""" - try: - # 记录更新时间 - self.performance_metrics["update_times"].append(dt) - - # 保持历史记录大小 - if len(self.performance_metrics["update_times"]) > 1000: - self.performance_metrics["update_times"] = self.performance_metrics["update_times"][-1000:] - - except Exception as e: - print(f"✗ 性能指标更新失败: {e}") - - def log_message(self, message: str, level: str = "INFO"): - """ - 记录日志消息 - - Args: - message: 日志消息 - level: 日志级别 (DEBUG, INFO, WARNING, ERROR) - """ - try: - if not self.log_config["enable_logging"]: - return - - # 检查日志级别 - level_priority = {"DEBUG": 0, "INFO": 1, "WARNING": 2, "ERROR": 3} - config_level = self.log_config["log_level"] - - if level_priority.get(level, 1) < level_priority.get(config_level, 1): - return - - # 格式化日志消息 - timestamp = time.strftime("%Y-%m-%d %H:%M:%S") - formatted_message = self.log_config["log_format"].format( - timestamp=timestamp, - level=level, - message=message - ) - - # 输出到控制台 - print(formatted_message) - - # 写入日志文件 - try: - with self.utils_lock: - # 检查文件大小 - if os.path.exists(self.log_config["log_file"]): - if os.path.getsize(self.log_config["log_file"]) > self.log_config["max_log_size"]: - # 重命名旧日志文件 - backup_file = self.log_config["log_file"] + ".old" - if os.path.exists(backup_file): - os.remove(backup_file) - os.rename(self.log_config["log_file"], backup_file) - - with open(self.log_config["log_file"], "a", encoding="utf-8") as f: - f.write(formatted_message + "\n") - except Exception as e: - pass # 忽略文件写入错误 - - except Exception as e: - pass # 忽略日志记录错误 - - def measure_performance(self, func: callable, *args, **kwargs) -> tuple: - """ - 测量函数性能 - - Args: - func: 要测量的函数 - *args: 函数参数 - **kwargs: 函数关键字参数 - - Returns: - (结果, 执行时间) - """ - try: - start_time = time.perf_counter() - result = func(*args, **kwargs) - end_time = time.perf_counter() - - execution_time = end_time - start_time - - # 记录到性能指标 - metric_name = f"{func.__name__}_times" - if metric_name not in self.performance_metrics: - self.performance_metrics[metric_name] = [] - - self.performance_metrics[metric_name].append(execution_time) - - # 保持历史记录大小 - if len(self.performance_metrics[metric_name]) > 1000: - self.performance_metrics[metric_name] = self.performance_metrics[metric_name][-1000:] - - self.stats["functions_called"] += 1 - return (result, execution_time) - - except Exception as e: - self.log_message(f"性能测量失败: {e}", "ERROR") - raise - - def convert_audio_level(self, level: float, curve: str = None) -> float: - """ - 转换音频级别值 - - Args: - level: 原始级别值 (0.0-1.0) - curve: 转换曲线类型 - - Returns: - 转换后的级别值 - """ - try: - curve_type = curve or self.conversion_config["audio_level_curve"] - level = max(0.0, min(1.0, level)) - - if curve_type == "exponential": - # 指数曲线 - return math.pow(level, 2) - elif curve_type == "logarithmic": - # 对数曲线 - return math.log(level + 1) / math.log(2) - else: - # 线性曲线 - return level - - except Exception as e: - self.log_message(f"音频级别转换失败: {e}", "ERROR") - return level - - def convert_confidence(self, confidence: float, mapping: str = None) -> float: - """ - 转换置信度值 - - Args: - confidence: 原始置信度值 (0.0-1.0) - mapping: 映射类型 - - Returns: - 转换后的置信度值 - """ - try: - mapping_type = mapping or self.conversion_config["confidence_mapping"] - confidence = max(0.0, min(1.0, confidence)) - - if mapping_type == "exponential": - # 指数映射 - return math.pow(confidence, 2) - elif mapping_type == "logarithmic": - # 对数映射 - return math.log(confidence * 9 + 1) / math.log(10) - else: - # 线性映射 - return confidence - - except Exception as e: - self.log_message(f"置信度转换失败: {e}", "ERROR") - return confidence - - def scale_duration(self, duration: float, scale_factor: float = 1.0, - scaling_type: str = None) -> float: - """ - 缩放持续时间 - - Args: - duration: 原始持续时间 - scale_factor: 缩放因子 - scaling_type: 缩放类型 - - Returns: - 缩放后的持续时间 - """ - try: - scaling = scaling_type or self.conversion_config["duration_scaling"] - duration = max(0.0, duration) - scale_factor = max(0.0, scale_factor) - - if scaling == "exponential": - return duration * math.pow(scale_factor, 2) - elif scaling == "logarithmic": - return duration * math.log(scale_factor + 1) - else: - return duration * scale_factor - - except Exception as e: - self.log_message(f"持续时间缩放失败: {e}", "ERROR") - return duration - - def interpolate_values(self, start_value: float, end_value: float, - progress: float, interpolation_type: str = "linear") -> float: - """ - 插值计算 - - Args: - start_value: 起始值 - end_value: 结束值 - progress: 进度 (0.0-1.0) - interpolation_type: 插值类型 - - Returns: - 插值结果 - """ - try: - progress = max(0.0, min(1.0, progress)) - - if interpolation_type == "ease_in": - progress = math.pow(progress, 2) - elif interpolation_type == "ease_out": - progress = 1.0 - math.pow(1.0 - progress, 2) - elif interpolation_type == "ease_in_out": - progress = 0.5 * (1 - math.cos(progress * math.pi)) - # linear 为默认情况,无需特殊处理 - - return start_value + (end_value - start_value) * progress - - except Exception as e: - self.log_message(f"插值计算失败: {e}", "ERROR") - return start_value - - def calculate_similarity(self, text1: str, text2: str) -> float: - """ - 计算文本相似度 - - Args: - text1: 文本1 - text2: 文本2 - - Returns: - 相似度值 (0.0-1.0) - """ - try: - # 使用简单的字符级相似度计算 - set1 = set(text1) - set2 = set(text2) - - intersection = len(set1.intersection(set2)) - union = len(set1.union(set2)) - - if union == 0: - return 1.0 if text1 == text2 else 0.0 - - return intersection / union - - except Exception as e: - self.log_message(f"相似度计算失败: {e}", "ERROR") - return 0.0 - - def normalize_text(self, text: str, language: str = "zh-CN") -> str: - """ - 标准化文本 - - Args: - text: 原始文本 - language: 语言代码 - - Returns: - 标准化后的文本 - """ - try: - # 移除多余空格 - text = " ".join(text.split()) - - # 转换为小写(仅适用于英文) - if language == "en-US": - text = text.lower() - - # 移除特殊字符(保留字母、数字和基本标点) - import re - if language == "zh-CN": - text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9\s\.,!?;:]", "", text) - else: - text = re.sub(r"[^a-zA-Z0-9\s\.,!?;:]", "", text) - - return text.strip() - - except Exception as e: - self.log_message(f"文本标准化失败: {e}", "ERROR") - return text - - def generate_hash(self, data: Union[str, bytes]) -> str: - """ - 生成数据哈希值 - - Args: - data: 要哈希的数据 - - Returns: - 哈希值字符串 - """ - try: - if isinstance(data, str): - data = data.encode("utf-8") - - return hashlib.md5(data).hexdigest() - - except Exception as e: - self.log_message(f"哈希生成失败: {e}", "ERROR") - return "" - - def format_time(self, seconds: float) -> str: - """ - 格式化时间显示 - - Args: - seconds: 秒数 - - Returns: - 格式化的时间字符串 - """ - try: - if seconds < 60: - return f"{seconds:.1f}秒" - elif seconds < 3600: - minutes = int(seconds // 60) - secs = seconds % 60 - return f"{minutes}分{secs:.1f}秒" - else: - hours = int(seconds // 3600) - minutes = int((seconds % 3600) // 60) - secs = seconds % 60 - return f"{hours}时{minutes}分{secs:.1f}秒" - - except Exception as e: - self.log_message(f"时间格式化失败: {e}", "ERROR") - return f"{seconds:.1f}秒" - - def get_performance_stats(self) -> Dict[str, Any]: - """ - 获取性能统计信息 - - Returns: - 性能统计字典 - """ - try: - stats = {} - for key, times in self.performance_metrics.items(): - if times: - stats[key] = { - "count": len(times), - "average": sum(times) / len(times), - "min": min(times), - "max": max(times), - "total": sum(times) - } - return stats - except Exception as e: - self.log_message(f"性能统计获取失败: {e}", "ERROR") - return {} - - def get_system_info(self) -> Dict[str, Any]: - """ - 获取系统信息 - - Returns: - 系统信息字典 - """ - try: - import platform - import psutil - - return { - "platform": platform.system(), - "platform_version": platform.version(), - "processor": platform.processor(), - "cpu_count": psutil.cpu_count(), - "memory_total": psutil.virtual_memory().total, - "memory_available": psutil.virtual_memory().available, - "plugin_version": getattr(self.plugin, "version", "unknown") - } - except Exception as e: - self.log_message(f"系统信息获取失败: {e}", "ERROR") - return {} - - def get_stats(self) -> Dict[str, int]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - try: - self.stats = { - "functions_called": 0, - "data_processed": 0, - "cache_hits": 0, - "cache_misses": 0, - "files_processed": 0 - } - print("✓ 工具类统计信息已重置") - except Exception as e: - self.log_message(f"统计信息重置失败: {e}", "ERROR") - - def set_log_config(self, config: Dict[str, Any]) -> bool: - """ - 设置日志配置 - - Args: - config: 日志配置字典 - - Returns: - 是否设置成功 - """ - try: - self.log_config.update(config) - self.log_message(f"日志配置已更新: {self.log_config}", "INFO") - return True - except Exception as e: - self.log_message(f"日志配置设置失败: {e}", "ERROR") - return False - - def set_conversion_config(self, config: Dict[str, Any]) -> bool: - """ - 设置转换配置 - - Args: - config: 转换配置字典 - - Returns: - 是否设置成功 - """ - try: - self.conversion_config.update(config) - self.log_message(f"转换配置已更新: {self.conversion_config}", "INFO") - return True - except Exception as e: - self.log_message(f"转换配置设置失败: {e}", "ERROR") - return False - - def get_cache_manager(self): - """ - 获取缓存管理器 - - Returns: - 缓存管理器实例 - """ - return self.cache_manager - - def get_debug_tools(self): - """ - 获取调试工具 - - Returns: - 调试工具实例 - """ - return self.debug_tools - - def get_file_manager(self): - """ - 获取文件管理器 - - Returns: - 文件管理器实例 - """ - return self.file_manager - -class MathUtils: - """数学工具类""" - - def __init__(self): - self.enabled = False - self.initialized = False - - def initialize(self): - """初始化数学工具""" - self.initialized = True - - def enable(self): - """启用数学工具""" - self.enabled = True - - def disable(self): - """禁用数学工具""" - self.enabled = False - - @staticmethod - def clamp(value: float, min_value: float, max_value: float) -> float: - """限制值在指定范围内""" - return max(min_value, min(max_value, value)) - - @staticmethod - def lerp(start: float, end: float, t: float) -> float: - """线性插值""" - return start + (end - start) * t - - @staticmethod - def smoothstep(edge0: float, edge1: float, x: float) -> float: - """平滑插值""" - t = MathUtils.clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0) - return t * t * (3.0 - 2.0 * t) - - @staticmethod - def distance(pos1: tuple, pos2: tuple) -> float: - """计算两点间距离""" - try: - dx = pos2[0] - pos1[0] - dy = pos2[1] - pos1[1] - dz = pos2[2] - pos1[2] if len(pos1) > 2 and len(pos2) > 2 else 0 - return math.sqrt(dx*dx + dy*dy + dz*dz) - except Exception: - return 0.0 - -class CacheManager: - """缓存管理器""" - - def __init__(self): - self.enabled = False - self.initialized = False - self.cache = {} - self.access_times = {} - self.max_size = 1000 - - def initialize(self): - """初始化缓存管理器""" - self.initialized = True - - def enable(self): - """启用缓存管理器""" - self.enabled = True - - def disable(self): - """禁用缓存管理器""" - self.enabled = False - - def update(self, dt: float): - """更新缓存管理器""" - if self.enabled: - self._cleanup() - - def get(self, key: str) -> Optional[Any]: - """获取缓存值""" - if key in self.cache: - self.access_times[key] = time.time() - return self.cache[key] - return None - - def set(self, key: str, value: Any, ttl: float = 300.0): - """设置缓存值""" - self.cache[key] = value - self.access_times[key] = time.time() - - # 检查缓存大小 - if len(self.cache) > self.max_size: - self._cleanup() - - def _cleanup(self): - """清理过期缓存""" - current_time = time.time() - expired_keys = [ - key for key, access_time in self.access_times.items() - if current_time - access_time > 300.0 - ] - - for key in expired_keys: - del self.cache[key] - del self.access_times[key] - - def clear(self): - """清空缓存""" - self.cache.clear() - self.access_times.clear() - -class DebugTools: - """调试工具类""" - - def __init__(self): - self.enabled = False - self.initialized = False - self.debug_enabled = False - self.debug_data = {} - - def initialize(self): - """初始化调试工具""" - self.initialized = True - - def enable(self): - """启用调试工具""" - self.enabled = True - - def disable(self): - """禁用调试工具""" - self.enabled = False - - def update(self, dt: float): - """更新调试工具""" - pass - - def enable_debug(self): - """启用调试模式""" - self.debug_enabled = True - - def disable_debug(self): - """禁用调试模式""" - self.debug_enabled = False - - def log_debug_data(self, key: str, data: Any): - """记录调试数据""" - if self.debug_enabled: - self.debug_data[key] = data - - def get_debug_data(self, key: str = None) -> Union[Dict[str, Any], Any]: - """获取调试数据""" - if key: - return self.debug_data.get(key) - return self.debug_data.copy() - - def clear_debug_data(self): - """清空调试数据""" - self.debug_data.clear() - - def clear(self): - """清理调试工具""" - self.debug_data.clear() - -class FileManager: - """文件管理器""" - - def __init__(self): - self.enabled = False - self.initialized = False - self.watched_files = {} - - def initialize(self): - """初始化文件管理器""" - self.initialized = True - - def enable(self): - """启用文件管理器""" - self.enabled = True - - def disable(self): - """禁用文件管理器""" - self.enabled = False - - def update(self, dt: float): - """更新文件管理器""" - if self.enabled: - self._check_file_changes() - - def _check_file_changes(self): - """检查文件变化""" - try: - for file_path, last_modified in list(self.watched_files.items()): - if os.path.exists(file_path): - current_modified = os.path.getmtime(file_path) - if current_modified > last_modified: - self.watched_files[file_path] = current_modified - # 触发文件变化事件 - except Exception as e: - pass - - def watch_file(self, file_path: str): - """监视文件变化""" - try: - if os.path.exists(file_path): - self.watched_files[file_path] = os.path.getmtime(file_path) - except Exception as e: - pass - - def unwatch_file(self, file_path: str): - """取消监视文件""" - if file_path in self.watched_files: - del self.watched_files[file_path] - - def read_json_file(self, file_path: str) -> Optional[Dict[str, Any]]: - """读取JSON文件""" - try: - with open(file_path, "r", encoding="utf-8") as f: - return json.load(f) - except Exception as e: - return None - - def write_json_file(self, file_path: str, data: Dict[str, Any]) -> bool: - """写入JSON文件""" - try: - with open(file_path, "w", encoding="utf-8") as f: - json.dump(data, f, ensure_ascii=False, indent=2) - return True - except Exception as e: - return False - - def clear(self): - """清理文件管理器""" +""" +工具类模块 +提供语音控制插件所需的实用工具函数 +""" + +import time +import math +import os +import json +import threading +from typing import Dict, Any, List, Optional, Union +import hashlib + +class VoiceUtils: + """ + 语音工具类 + 提供语音控制插件所需的实用工具函数 + """ + + def __init__(self, plugin): + """ + 初始化语音工具类 + + Args: + plugin: 语音控制插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 性能监控 + self.performance_metrics = { + "update_times": [], + "processing_times": [], + "memory_usage": 0, + "cpu_usage": 0 + } + + # 日志配置 + self.log_config = { + "enable_logging": True, + "log_level": "INFO", + "log_file": "/home/hello/EG/plugins/user/voice_control/logs/voice.log", + "max_log_size": 10 * 1024 * 1024, # 10MB + "log_format": "[{timestamp}] {level}: {message}" + } + + # 数据转换配置 + self.conversion_config = { + "audio_level_curve": "linear", # linear, exponential, logarithmic + "confidence_mapping": "linear", + "duration_scaling": "linear" + } + + # 数学工具 + self.math_utils = MathUtils() + + # 缓存管理 + self.cache_manager = CacheManager() + + # 调试工具 + self.debug_tools = DebugTools() + + # 文件管理 + self.file_manager = FileManager() + + # 统计信息 + self.stats = { + "functions_called": 0, + "data_processed": 0, + "cache_hits": 0, + "cache_misses": 0, + "files_processed": 0 + } + + # 线程锁 + self.utils_lock = threading.RLock() + + print("✓ 语音工具类已创建") + + def initialize(self) -> bool: + """ + 初始化语音工具类 + + Returns: + 是否初始化成功 + """ + try: + # 创建日志目录 + log_dir = "/home/hello/EG/plugins/user/voice_control/logs" + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + # 初始化子工具 + self.math_utils.initialize() + self.cache_manager.initialize() + self.debug_tools.initialize() + self.file_manager.initialize() + + self.initialized = True + print("✓ 语音工具类初始化完成") + return True + + except Exception as e: + print(f"✗ 语音工具类初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用语音工具类 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 语音工具类未初始化") + return False + + self.enabled = True + + # 启用子工具 + self.math_utils.enable() + self.cache_manager.enable() + self.debug_tools.enable() + self.file_manager.enable() + + print("✓ 语音工具类已启用") + return True + + except Exception as e: + print(f"✗ 语音工具类启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用语音工具类""" + try: + self.enabled = False + + # 禁用子工具 + self.math_utils.disable() + self.cache_manager.disable() + self.debug_tools.disable() + self.file_manager.disable() + + print("✓ 语音工具类已禁用") + + except Exception as e: + print(f"✗ 语音工具类禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理语音工具类资源""" + try: + self.disable() + self.performance_metrics.clear() + self.cache_manager.clear() + self.debug_tools.clear() + self.file_manager.clear() + self.initialized = False + print("✓ 语音工具类资源已清理") + + except Exception as e: + print(f"✗ 语音工具类资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新语音工具类状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新性能指标 + self._update_performance_metrics(dt) + + # 更新子工具 + self.cache_manager.update(dt) + self.debug_tools.update(dt) + self.file_manager.update(dt) + + except Exception as e: + print(f"✗ 语音工具类更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_performance_metrics(self, dt: float): + """更新性能指标""" + try: + # 记录更新时间 + self.performance_metrics["update_times"].append(dt) + + # 保持历史记录大小 + if len(self.performance_metrics["update_times"]) > 1000: + self.performance_metrics["update_times"] = self.performance_metrics["update_times"][-1000:] + + except Exception as e: + print(f"✗ 性能指标更新失败: {e}") + + def log_message(self, message: str, level: str = "INFO"): + """ + 记录日志消息 + + Args: + message: 日志消息 + level: 日志级别 (DEBUG, INFO, WARNING, ERROR) + """ + try: + if not self.log_config["enable_logging"]: + return + + # 检查日志级别 + level_priority = {"DEBUG": 0, "INFO": 1, "WARNING": 2, "ERROR": 3} + config_level = self.log_config["log_level"] + + if level_priority.get(level, 1) < level_priority.get(config_level, 1): + return + + # 格式化日志消息 + timestamp = time.strftime("%Y-%m-%d %H:%M:%S") + formatted_message = self.log_config["log_format"].format( + timestamp=timestamp, + level=level, + message=message + ) + + # 输出到控制台 + print(formatted_message) + + # 写入日志文件 + try: + with self.utils_lock: + # 检查文件大小 + if os.path.exists(self.log_config["log_file"]): + if os.path.getsize(self.log_config["log_file"]) > self.log_config["max_log_size"]: + # 重命名旧日志文件 + backup_file = self.log_config["log_file"] + ".old" + if os.path.exists(backup_file): + os.remove(backup_file) + os.rename(self.log_config["log_file"], backup_file) + + with open(self.log_config["log_file"], "a", encoding="utf-8") as f: + f.write(formatted_message + "\n") + except Exception as e: + pass # 忽略文件写入错误 + + except Exception as e: + pass # 忽略日志记录错误 + + def measure_performance(self, func: callable, *args, **kwargs) -> tuple: + """ + 测量函数性能 + + Args: + func: 要测量的函数 + *args: 函数参数 + **kwargs: 函数关键字参数 + + Returns: + (结果, 执行时间) + """ + try: + start_time = time.perf_counter() + result = func(*args, **kwargs) + end_time = time.perf_counter() + + execution_time = end_time - start_time + + # 记录到性能指标 + metric_name = f"{func.__name__}_times" + if metric_name not in self.performance_metrics: + self.performance_metrics[metric_name] = [] + + self.performance_metrics[metric_name].append(execution_time) + + # 保持历史记录大小 + if len(self.performance_metrics[metric_name]) > 1000: + self.performance_metrics[metric_name] = self.performance_metrics[metric_name][-1000:] + + self.stats["functions_called"] += 1 + return (result, execution_time) + + except Exception as e: + self.log_message(f"性能测量失败: {e}", "ERROR") + raise + + def convert_audio_level(self, level: float, curve: str = None) -> float: + """ + 转换音频级别值 + + Args: + level: 原始级别值 (0.0-1.0) + curve: 转换曲线类型 + + Returns: + 转换后的级别值 + """ + try: + curve_type = curve or self.conversion_config["audio_level_curve"] + level = max(0.0, min(1.0, level)) + + if curve_type == "exponential": + # 指数曲线 + return math.pow(level, 2) + elif curve_type == "logarithmic": + # 对数曲线 + return math.log(level + 1) / math.log(2) + else: + # 线性曲线 + return level + + except Exception as e: + self.log_message(f"音频级别转换失败: {e}", "ERROR") + return level + + def convert_confidence(self, confidence: float, mapping: str = None) -> float: + """ + 转换置信度值 + + Args: + confidence: 原始置信度值 (0.0-1.0) + mapping: 映射类型 + + Returns: + 转换后的置信度值 + """ + try: + mapping_type = mapping or self.conversion_config["confidence_mapping"] + confidence = max(0.0, min(1.0, confidence)) + + if mapping_type == "exponential": + # 指数映射 + return math.pow(confidence, 2) + elif mapping_type == "logarithmic": + # 对数映射 + return math.log(confidence * 9 + 1) / math.log(10) + else: + # 线性映射 + return confidence + + except Exception as e: + self.log_message(f"置信度转换失败: {e}", "ERROR") + return confidence + + def scale_duration(self, duration: float, scale_factor: float = 1.0, + scaling_type: str = None) -> float: + """ + 缩放持续时间 + + Args: + duration: 原始持续时间 + scale_factor: 缩放因子 + scaling_type: 缩放类型 + + Returns: + 缩放后的持续时间 + """ + try: + scaling = scaling_type or self.conversion_config["duration_scaling"] + duration = max(0.0, duration) + scale_factor = max(0.0, scale_factor) + + if scaling == "exponential": + return duration * math.pow(scale_factor, 2) + elif scaling == "logarithmic": + return duration * math.log(scale_factor + 1) + else: + return duration * scale_factor + + except Exception as e: + self.log_message(f"持续时间缩放失败: {e}", "ERROR") + return duration + + def interpolate_values(self, start_value: float, end_value: float, + progress: float, interpolation_type: str = "linear") -> float: + """ + 插值计算 + + Args: + start_value: 起始值 + end_value: 结束值 + progress: 进度 (0.0-1.0) + interpolation_type: 插值类型 + + Returns: + 插值结果 + """ + try: + progress = max(0.0, min(1.0, progress)) + + if interpolation_type == "ease_in": + progress = math.pow(progress, 2) + elif interpolation_type == "ease_out": + progress = 1.0 - math.pow(1.0 - progress, 2) + elif interpolation_type == "ease_in_out": + progress = 0.5 * (1 - math.cos(progress * math.pi)) + # linear 为默认情况,无需特殊处理 + + return start_value + (end_value - start_value) * progress + + except Exception as e: + self.log_message(f"插值计算失败: {e}", "ERROR") + return start_value + + def calculate_similarity(self, text1: str, text2: str) -> float: + """ + 计算文本相似度 + + Args: + text1: 文本1 + text2: 文本2 + + Returns: + 相似度值 (0.0-1.0) + """ + try: + # 使用简单的字符级相似度计算 + set1 = set(text1) + set2 = set(text2) + + intersection = len(set1.intersection(set2)) + union = len(set1.union(set2)) + + if union == 0: + return 1.0 if text1 == text2 else 0.0 + + return intersection / union + + except Exception as e: + self.log_message(f"相似度计算失败: {e}", "ERROR") + return 0.0 + + def normalize_text(self, text: str, language: str = "zh-CN") -> str: + """ + 标准化文本 + + Args: + text: 原始文本 + language: 语言代码 + + Returns: + 标准化后的文本 + """ + try: + # 移除多余空格 + text = " ".join(text.split()) + + # 转换为小写(仅适用于英文) + if language == "en-US": + text = text.lower() + + # 移除特殊字符(保留字母、数字和基本标点) + import re + if language == "zh-CN": + text = re.sub(r"[^\u4e00-\u9fa5a-zA-Z0-9\s\.,!?;:]", "", text) + else: + text = re.sub(r"[^a-zA-Z0-9\s\.,!?;:]", "", text) + + return text.strip() + + except Exception as e: + self.log_message(f"文本标准化失败: {e}", "ERROR") + return text + + def generate_hash(self, data: Union[str, bytes]) -> str: + """ + 生成数据哈希值 + + Args: + data: 要哈希的数据 + + Returns: + 哈希值字符串 + """ + try: + if isinstance(data, str): + data = data.encode("utf-8") + + return hashlib.md5(data).hexdigest() + + except Exception as e: + self.log_message(f"哈希生成失败: {e}", "ERROR") + return "" + + def format_time(self, seconds: float) -> str: + """ + 格式化时间显示 + + Args: + seconds: 秒数 + + Returns: + 格式化的时间字符串 + """ + try: + if seconds < 60: + return f"{seconds:.1f}秒" + elif seconds < 3600: + minutes = int(seconds // 60) + secs = seconds % 60 + return f"{minutes}分{secs:.1f}秒" + else: + hours = int(seconds // 3600) + minutes = int((seconds % 3600) // 60) + secs = seconds % 60 + return f"{hours}时{minutes}分{secs:.1f}秒" + + except Exception as e: + self.log_message(f"时间格式化失败: {e}", "ERROR") + return f"{seconds:.1f}秒" + + def get_performance_stats(self) -> Dict[str, Any]: + """ + 获取性能统计信息 + + Returns: + 性能统计字典 + """ + try: + stats = {} + for key, times in self.performance_metrics.items(): + if times: + stats[key] = { + "count": len(times), + "average": sum(times) / len(times), + "min": min(times), + "max": max(times), + "total": sum(times) + } + return stats + except Exception as e: + self.log_message(f"性能统计获取失败: {e}", "ERROR") + return {} + + def get_system_info(self) -> Dict[str, Any]: + """ + 获取系统信息 + + Returns: + 系统信息字典 + """ + try: + import platform + import psutil + + return { + "platform": platform.system(), + "platform_version": platform.version(), + "processor": platform.processor(), + "cpu_count": psutil.cpu_count(), + "memory_total": psutil.virtual_memory().total, + "memory_available": psutil.virtual_memory().available, + "plugin_version": getattr(self.plugin, "version", "unknown") + } + except Exception as e: + self.log_message(f"系统信息获取失败: {e}", "ERROR") + return {} + + def get_stats(self) -> Dict[str, int]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + try: + self.stats = { + "functions_called": 0, + "data_processed": 0, + "cache_hits": 0, + "cache_misses": 0, + "files_processed": 0 + } + print("✓ 工具类统计信息已重置") + except Exception as e: + self.log_message(f"统计信息重置失败: {e}", "ERROR") + + def set_log_config(self, config: Dict[str, Any]) -> bool: + """ + 设置日志配置 + + Args: + config: 日志配置字典 + + Returns: + 是否设置成功 + """ + try: + self.log_config.update(config) + self.log_message(f"日志配置已更新: {self.log_config}", "INFO") + return True + except Exception as e: + self.log_message(f"日志配置设置失败: {e}", "ERROR") + return False + + def set_conversion_config(self, config: Dict[str, Any]) -> bool: + """ + 设置转换配置 + + Args: + config: 转换配置字典 + + Returns: + 是否设置成功 + """ + try: + self.conversion_config.update(config) + self.log_message(f"转换配置已更新: {self.conversion_config}", "INFO") + return True + except Exception as e: + self.log_message(f"转换配置设置失败: {e}", "ERROR") + return False + + def get_cache_manager(self): + """ + 获取缓存管理器 + + Returns: + 缓存管理器实例 + """ + return self.cache_manager + + def get_debug_tools(self): + """ + 获取调试工具 + + Returns: + 调试工具实例 + """ + return self.debug_tools + + def get_file_manager(self): + """ + 获取文件管理器 + + Returns: + 文件管理器实例 + """ + return self.file_manager + +class MathUtils: + """数学工具类""" + + def __init__(self): + self.enabled = False + self.initialized = False + + def initialize(self): + """初始化数学工具""" + self.initialized = True + + def enable(self): + """启用数学工具""" + self.enabled = True + + def disable(self): + """禁用数学工具""" + self.enabled = False + + @staticmethod + def clamp(value: float, min_value: float, max_value: float) -> float: + """限制值在指定范围内""" + return max(min_value, min(max_value, value)) + + @staticmethod + def lerp(start: float, end: float, t: float) -> float: + """线性插值""" + return start + (end - start) * t + + @staticmethod + def smoothstep(edge0: float, edge1: float, x: float) -> float: + """平滑插值""" + t = MathUtils.clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0) + return t * t * (3.0 - 2.0 * t) + + @staticmethod + def distance(pos1: tuple, pos2: tuple) -> float: + """计算两点间距离""" + try: + dx = pos2[0] - pos1[0] + dy = pos2[1] - pos1[1] + dz = pos2[2] - pos1[2] if len(pos1) > 2 and len(pos2) > 2 else 0 + return math.sqrt(dx*dx + dy*dy + dz*dz) + except Exception: + return 0.0 + +class CacheManager: + """缓存管理器""" + + def __init__(self): + self.enabled = False + self.initialized = False + self.cache = {} + self.access_times = {} + self.max_size = 1000 + + def initialize(self): + """初始化缓存管理器""" + self.initialized = True + + def enable(self): + """启用缓存管理器""" + self.enabled = True + + def disable(self): + """禁用缓存管理器""" + self.enabled = False + + def update(self, dt: float): + """更新缓存管理器""" + if self.enabled: + self._cleanup() + + def get(self, key: str) -> Optional[Any]: + """获取缓存值""" + if key in self.cache: + self.access_times[key] = time.time() + return self.cache[key] + return None + + def set(self, key: str, value: Any, ttl: float = 300.0): + """设置缓存值""" + self.cache[key] = value + self.access_times[key] = time.time() + + # 检查缓存大小 + if len(self.cache) > self.max_size: + self._cleanup() + + def _cleanup(self): + """清理过期缓存""" + current_time = time.time() + expired_keys = [ + key for key, access_time in self.access_times.items() + if current_time - access_time > 300.0 + ] + + for key in expired_keys: + del self.cache[key] + del self.access_times[key] + + def clear(self): + """清空缓存""" + self.cache.clear() + self.access_times.clear() + +class DebugTools: + """调试工具类""" + + def __init__(self): + self.enabled = False + self.initialized = False + self.debug_enabled = False + self.debug_data = {} + + def initialize(self): + """初始化调试工具""" + self.initialized = True + + def enable(self): + """启用调试工具""" + self.enabled = True + + def disable(self): + """禁用调试工具""" + self.enabled = False + + def update(self, dt: float): + """更新调试工具""" + pass + + def enable_debug(self): + """启用调试模式""" + self.debug_enabled = True + + def disable_debug(self): + """禁用调试模式""" + self.debug_enabled = False + + def log_debug_data(self, key: str, data: Any): + """记录调试数据""" + if self.debug_enabled: + self.debug_data[key] = data + + def get_debug_data(self, key: str = None) -> Union[Dict[str, Any], Any]: + """获取调试数据""" + if key: + return self.debug_data.get(key) + return self.debug_data.copy() + + def clear_debug_data(self): + """清空调试数据""" + self.debug_data.clear() + + def clear(self): + """清理调试工具""" + self.debug_data.clear() + +class FileManager: + """文件管理器""" + + def __init__(self): + self.enabled = False + self.initialized = False + self.watched_files = {} + + def initialize(self): + """初始化文件管理器""" + self.initialized = True + + def enable(self): + """启用文件管理器""" + self.enabled = True + + def disable(self): + """禁用文件管理器""" + self.enabled = False + + def update(self, dt: float): + """更新文件管理器""" + if self.enabled: + self._check_file_changes() + + def _check_file_changes(self): + """检查文件变化""" + try: + for file_path, last_modified in list(self.watched_files.items()): + if os.path.exists(file_path): + current_modified = os.path.getmtime(file_path) + if current_modified > last_modified: + self.watched_files[file_path] = current_modified + # 触发文件变化事件 + except Exception as e: + pass + + def watch_file(self, file_path: str): + """监视文件变化""" + try: + if os.path.exists(file_path): + self.watched_files[file_path] = os.path.getmtime(file_path) + except Exception as e: + pass + + def unwatch_file(self, file_path: str): + """取消监视文件""" + if file_path in self.watched_files: + del self.watched_files[file_path] + + def read_json_file(self, file_path: str) -> Optional[Dict[str, Any]]: + """读取JSON文件""" + try: + with open(file_path, "r", encoding="utf-8") as f: + return json.load(f) + except Exception as e: + return None + + def write_json_file(self, file_path: str, data: Dict[str, Any]) -> bool: + """写入JSON文件""" + try: + with open(file_path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + return True + except Exception as e: + return False + + def clear(self): + """清理文件管理器""" self.watched_files.clear() \ No newline at end of file diff --git a/plugins/user/weather_season_system/README.md b/plugins/user/weather_season_system/README.md index c711f8cd..44d12f1a 100644 --- a/plugins/user/weather_season_system/README.md +++ b/plugins/user/weather_season_system/README.md @@ -1,229 +1,229 @@ -# 天气和季节系统插件 - -## 概述 - -天气和季节系统插件为EG引擎提供完整的天气和季节模拟功能。该插件包含丰富的天气类型、四季变化、环境效果、粒子系统和音频效果,能够创建逼真的自然环境模拟。 - -## 功能特性 - -### 1. 季节系统 -- **四季变化**:春季、夏季、秋季、冬季循环 -- **季节过渡**:平滑的季节过渡效果 -- **季节影响**:温度、湿度、日照时间等环境参数变化 -- **植被生长**:季节性植被状态变化 - -### 2. 天气系统 -- **多种天气类型**: - - 晴朗 (Clear) - - 多云 (Cloudy) - - 下雨 (Rain) - - 暴风雨 (Storm) - - 下雪 (Snow) - - 雾天 (Fog) - - 大风 (Windy) - - 冰雹 (Hail) -- **天气过渡**:自然的天气变化和过渡效果 -- **天气影响**:温度、湿度、能见度等环境参数变化 - -### 3. 环境效果 -- **光照系统**:动态光照和阴影 -- **雾效系统**:线性、指数级雾效 -- **风效系统**:风向和风速模拟 -- **后处理效果**:曝光、饱和度、对比度调整 - -### 4. 粒子效果 -- **降水粒子**:雨滴、雪花、冰雹 -- **大气粒子**:雾气、尘土 -- **季节粒子**:落叶等 -- **物理模拟**:重力、风力影响 - -### 5. 音频系统 -- **环境音效**:风声、雨声、雷声等 -- **音频过渡**:淡入淡出效果 -- **音量控制**:主音量、效果音量、环境音量 - -### 6. 物理效果 -- **表面状态**:湿润度、结冰状态 -- **移动影响**:摩擦系数、移动速度 -- **声音传播**:声音衰减效果 - -## 系统架构 - -``` -weather_season_system/ -├── plugin.py # 插件主文件 -├── core/ -│ ├── season_manager.py # 季节管理器 -│ └── weather_manager.py # 天气管理器 -├── effects/ -│ ├── environment_effects.py # 环境效果 -│ └── visual_effects.py # 视觉效果 -├── audio/ -│ └── audio_manager.py # 音频管理器 -├── weather/ -│ └── weather_system.py # 天气系统 -├── seasons/ -│ └── season_system.py # 季节系统 -├── particles/ -│ └── particle_effects.py # 粒子效果 -├── editor/ -│ ├── weather_editor.py # 天气编辑器 -│ └── preset_manager.py # 预设管理器 -└── utils/ - └── weather_utils.py # 工具类 -``` - -## 使用方法 - -### 初始化插件 -```python -# 创建插件实例 -weather_plugin = WeatherSeasonSystemPlugin(engine) - -# 初始化插件 -if weather_plugin.initialize(): - # 启用插件 - if weather_plugin.enable(): - print("天气和季节系统插件已启用") -``` - -### 设置季节 -```python -# 设置当前季节 -season_manager = weather_plugin.season_manager -season_manager.set_current_season('spring') - -# 启用季节循环 -season_manager.enable_season_cycle(True) -``` - -### 控制天气 -```python -# 设置当前天气 -weather_manager = weather_plugin.weather_manager -weather_manager.set_current_weather('rain') - -# 启用天气变化 -weather_manager.enable_weather_change(True) -``` - -### 调整环境效果 -```python -# 设置时间 -environment_effects = weather_plugin.environment_effects -environment_effects.set_time(12.0) # 正午 - -# 设置地理位置 -environment_effects.set_location(30.0, 120.0) # 纬度30°,经度120° -``` - -### 粒子效果控制 -```python -# 激活粒子效果 -visual_effects = weather_plugin.visual_effects -visual_effects.activate_effect('rain') -``` - -### 音频控制 -```python -# 设置音量 -audio_manager = weather_plugin.audio_manager -audio_manager.set_master_volume(0.8) -audio_manager.set_effects_volume(0.7) -``` - -## 配置选项 - -### 季节配置 -- `season_duration`:每个季节持续天数 -- `transition_duration`:季节过渡时间 -- `season_cycle_enabled`:是否启用季节循环 - -### 天气配置 -- `weather_duration_min`:天气最短持续时间 -- `weather_duration_max`:天气最长持续时间 -- `weather_change_probability`:天气变化概率 - -### 粒子系统配置 -- `max_particles`:最大粒子数 -- `quality_level`:粒子质量等级 -- `collision_detection`:是否启用碰撞检测 - -### 音频配置 -- `master_volume`:主音量 -- `effects_volume`:效果音量 -- `ambient_volume`:环境音量 - -## API参考 - -### 主要类和方法 - -#### WeatherSeasonSystemPlugin -- `initialize()`:初始化插件 -- `enable()`:启用插件 -- `disable()`:禁用插件 -- `update(dt)`:更新插件状态 - -#### SeasonManager -- `set_current_season(season)`:设置当前季节 -- `enable_season_cycle(enabled)`:启用/禁用季节循环 -- `get_current_season()`:获取当前季节 -- `get_season_info(season)`:获取季节信息 - -#### WeatherManager -- `set_current_weather(weather)`:设置当前天气 -- `enable_weather_change(enabled)`:启用/禁用天气变化 -- `get_current_weather()`:获取当前天气 -- `get_weather_info(weather)`:获取天气信息 - -#### EnvironmentEffects -- `set_time(hour)`:设置当前时间 -- `set_location(latitude, longitude)`:设置地理位置 -- `get_current_effects()`:获取当前环境效果 - -#### VisualEffects -- `activate_effect(effect_name)`:激活视觉效果 -- `deactivate_effect(effect_name)`:停用视觉效果 -- `get_active_effects()`:获取激活的效果列表 - -#### AudioManager -- `set_master_volume(volume)`:设置主音量 -- `set_effects_volume(volume)`:设置效果音量 -- `play_sound(sound_name)`:播放音频 -- `stop_sound(sound_name)`:停止音频 - -## 性能优化 - -1. **LOD系统**:根据距离调整粒子质量和效果细节 -2. **缓存机制**:缓存计算结果以提高性能 -3. **批量更新**:批量处理粒子和效果更新 -4. **资源管理**:及时释放不需要的资源 - -## 扩展性 - -### 添加新的天气类型 -1. 在`WeatherManager`中添加天气类型定义 -2. 在`VisualEffects`中添加对应的粒子效果 -3. 在`AudioManager`中添加对应的音频效果 - -### 添加新的季节 -1. 在`SeasonManager`中添加季节定义 -2. 调整季节影响的环境参数 -3. 添加季节特有的视觉和音频效果 - -### 自定义粒子效果 -1. 在`ParticleEffects`中定义新的粒子类型 -2. 创建对应的粒子发射器 -3. 设置粒子的物理属性和行为 - -## 许可证 - -本插件遵循EG引擎的许可证协议。 - -## 更新日志 - -### v1.0.0 -- 初始版本发布 -- 实现完整的季节和天气系统 -- 添加环境效果、粒子效果和音频系统 +# 天气和季节系统插件 + +## 概述 + +天气和季节系统插件为EG引擎提供完整的天气和季节模拟功能。该插件包含丰富的天气类型、四季变化、环境效果、粒子系统和音频效果,能够创建逼真的自然环境模拟。 + +## 功能特性 + +### 1. 季节系统 +- **四季变化**:春季、夏季、秋季、冬季循环 +- **季节过渡**:平滑的季节过渡效果 +- **季节影响**:温度、湿度、日照时间等环境参数变化 +- **植被生长**:季节性植被状态变化 + +### 2. 天气系统 +- **多种天气类型**: + - 晴朗 (Clear) + - 多云 (Cloudy) + - 下雨 (Rain) + - 暴风雨 (Storm) + - 下雪 (Snow) + - 雾天 (Fog) + - 大风 (Windy) + - 冰雹 (Hail) +- **天气过渡**:自然的天气变化和过渡效果 +- **天气影响**:温度、湿度、能见度等环境参数变化 + +### 3. 环境效果 +- **光照系统**:动态光照和阴影 +- **雾效系统**:线性、指数级雾效 +- **风效系统**:风向和风速模拟 +- **后处理效果**:曝光、饱和度、对比度调整 + +### 4. 粒子效果 +- **降水粒子**:雨滴、雪花、冰雹 +- **大气粒子**:雾气、尘土 +- **季节粒子**:落叶等 +- **物理模拟**:重力、风力影响 + +### 5. 音频系统 +- **环境音效**:风声、雨声、雷声等 +- **音频过渡**:淡入淡出效果 +- **音量控制**:主音量、效果音量、环境音量 + +### 6. 物理效果 +- **表面状态**:湿润度、结冰状态 +- **移动影响**:摩擦系数、移动速度 +- **声音传播**:声音衰减效果 + +## 系统架构 + +``` +weather_season_system/ +├── plugin.py # 插件主文件 +├── core/ +│ ├── season_manager.py # 季节管理器 +│ └── weather_manager.py # 天气管理器 +├── effects/ +│ ├── environment_effects.py # 环境效果 +│ └── visual_effects.py # 视觉效果 +├── audio/ +│ └── audio_manager.py # 音频管理器 +├── weather/ +│ └── weather_system.py # 天气系统 +├── seasons/ +│ └── season_system.py # 季节系统 +├── particles/ +│ └── particle_effects.py # 粒子效果 +├── editor/ +│ ├── weather_editor.py # 天气编辑器 +│ └── preset_manager.py # 预设管理器 +└── utils/ + └── weather_utils.py # 工具类 +``` + +## 使用方法 + +### 初始化插件 +```python +# 创建插件实例 +weather_plugin = WeatherSeasonSystemPlugin(engine) + +# 初始化插件 +if weather_plugin.initialize(): + # 启用插件 + if weather_plugin.enable(): + print("天气和季节系统插件已启用") +``` + +### 设置季节 +```python +# 设置当前季节 +season_manager = weather_plugin.season_manager +season_manager.set_current_season('spring') + +# 启用季节循环 +season_manager.enable_season_cycle(True) +``` + +### 控制天气 +```python +# 设置当前天气 +weather_manager = weather_plugin.weather_manager +weather_manager.set_current_weather('rain') + +# 启用天气变化 +weather_manager.enable_weather_change(True) +``` + +### 调整环境效果 +```python +# 设置时间 +environment_effects = weather_plugin.environment_effects +environment_effects.set_time(12.0) # 正午 + +# 设置地理位置 +environment_effects.set_location(30.0, 120.0) # 纬度30°,经度120° +``` + +### 粒子效果控制 +```python +# 激活粒子效果 +visual_effects = weather_plugin.visual_effects +visual_effects.activate_effect('rain') +``` + +### 音频控制 +```python +# 设置音量 +audio_manager = weather_plugin.audio_manager +audio_manager.set_master_volume(0.8) +audio_manager.set_effects_volume(0.7) +``` + +## 配置选项 + +### 季节配置 +- `season_duration`:每个季节持续天数 +- `transition_duration`:季节过渡时间 +- `season_cycle_enabled`:是否启用季节循环 + +### 天气配置 +- `weather_duration_min`:天气最短持续时间 +- `weather_duration_max`:天气最长持续时间 +- `weather_change_probability`:天气变化概率 + +### 粒子系统配置 +- `max_particles`:最大粒子数 +- `quality_level`:粒子质量等级 +- `collision_detection`:是否启用碰撞检测 + +### 音频配置 +- `master_volume`:主音量 +- `effects_volume`:效果音量 +- `ambient_volume`:环境音量 + +## API参考 + +### 主要类和方法 + +#### WeatherSeasonSystemPlugin +- `initialize()`:初始化插件 +- `enable()`:启用插件 +- `disable()`:禁用插件 +- `update(dt)`:更新插件状态 + +#### SeasonManager +- `set_current_season(season)`:设置当前季节 +- `enable_season_cycle(enabled)`:启用/禁用季节循环 +- `get_current_season()`:获取当前季节 +- `get_season_info(season)`:获取季节信息 + +#### WeatherManager +- `set_current_weather(weather)`:设置当前天气 +- `enable_weather_change(enabled)`:启用/禁用天气变化 +- `get_current_weather()`:获取当前天气 +- `get_weather_info(weather)`:获取天气信息 + +#### EnvironmentEffects +- `set_time(hour)`:设置当前时间 +- `set_location(latitude, longitude)`:设置地理位置 +- `get_current_effects()`:获取当前环境效果 + +#### VisualEffects +- `activate_effect(effect_name)`:激活视觉效果 +- `deactivate_effect(effect_name)`:停用视觉效果 +- `get_active_effects()`:获取激活的效果列表 + +#### AudioManager +- `set_master_volume(volume)`:设置主音量 +- `set_effects_volume(volume)`:设置效果音量 +- `play_sound(sound_name)`:播放音频 +- `stop_sound(sound_name)`:停止音频 + +## 性能优化 + +1. **LOD系统**:根据距离调整粒子质量和效果细节 +2. **缓存机制**:缓存计算结果以提高性能 +3. **批量更新**:批量处理粒子和效果更新 +4. **资源管理**:及时释放不需要的资源 + +## 扩展性 + +### 添加新的天气类型 +1. 在`WeatherManager`中添加天气类型定义 +2. 在`VisualEffects`中添加对应的粒子效果 +3. 在`AudioManager`中添加对应的音频效果 + +### 添加新的季节 +1. 在`SeasonManager`中添加季节定义 +2. 调整季节影响的环境参数 +3. 添加季节特有的视觉和音频效果 + +### 自定义粒子效果 +1. 在`ParticleEffects`中定义新的粒子类型 +2. 创建对应的粒子发射器 +3. 设置粒子的物理属性和行为 + +## 许可证 + +本插件遵循EG引擎的许可证协议。 + +## 更新日志 + +### v1.0.0 +- 初始版本发布 +- 实现完整的季节和天气系统 +- 添加环境效果、粒子效果和音频系统 - 提供编辑器和预设管理功能 \ No newline at end of file diff --git a/plugins/user/weather_season_system/audio/audio_manager.py b/plugins/user/weather_season_system/audio/audio_manager.py index 6cb0fb1f..5f48087b 100644 --- a/plugins/user/weather_season_system/audio/audio_manager.py +++ b/plugins/user/weather_season_system/audio/audio_manager.py @@ -1,650 +1,650 @@ -""" -音频管理系统 -负责管理天气和季节的音频效果,如环境音、天气音效等 -""" - -import time -import random -from typing import Dict, Any, List, Optional -import math - -class AudioManager: - """ - 音频管理系统 - 负责管理天气和季节的音频效果,包括环境音、天气音效、音频过渡等 - """ - - def __init__(self, plugin): - """ - 初始化音频管理系统 - - Args: - plugin: 天气和季节系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 音频效果配置 - self.audio_effects = { - 'wind': { - 'name': '风声', - 'file': 'audio/wind.wav', - 'volume_range': (0.1, 0.5), - 'pitch_range': (0.8, 1.2), - 'loop': True, - 'fade_time': 2.0, - 'priority': 1 - }, - 'rain': { - 'name': '雨声', - 'file': 'audio/rain.wav', - 'volume_range': (0.2, 0.7), - 'pitch_range': (0.9, 1.1), - 'loop': True, - 'fade_time': 3.0, - 'priority': 2 - }, - 'thunderstorm': { - 'name': '雷暴声', - 'file': 'audio/thunderstorm.wav', - 'volume_range': (0.3, 1.0), - 'pitch_range': (0.7, 1.3), - 'loop': True, - 'fade_time': 4.0, - 'priority': 3 - }, - 'snow': { - 'name': '雪声', - 'file': 'audio/snow.wav', - 'volume_range': (0.05, 0.3), - 'pitch_range': (0.8, 1.0), - 'loop': True, - 'fade_time': 2.5, - 'priority': 1 - }, - 'hail': { - 'name': '冰雹声', - 'file': 'audio/hail.wav', - 'volume_range': (0.2, 0.8), - 'pitch_range': (0.9, 1.2), - 'loop': True, - 'fade_time': 2.0, - 'priority': 2 - }, - 'birds': { - 'name': '鸟鸣声', - 'file': 'audio/birds.wav', - 'volume_range': (0.1, 0.4), - 'pitch_range': (0.9, 1.1), - 'loop': True, - 'fade_time': 3.0, - 'priority': 1 - }, - 'crickets': { - 'name': '蟋蟀声', - 'file': 'audio/crickets.wav', - 'volume_range': (0.1, 0.3), - 'pitch_range': (0.8, 1.0), - 'loop': True, - 'fade_time': 2.5, - 'priority': 1 - }, - 'ocean': { - 'name': '海浪声', - 'file': 'audio/ocean.wav', - 'volume_range': (0.2, 0.6), - 'pitch_range': (0.9, 1.1), - 'loop': True, - 'fade_time': 3.5, - 'priority': 1 - } - } - - # 当前播放的音频 - self.active_audio = {} - - # 环境音配置 - self.ambient_sounds = { - 'day_ambient': { - 'name': '白天环境音', - 'file': 'audio/day_ambient.wav', - 'volume': 0.3, - 'priority': 0 - }, - 'night_ambient': { - 'name': '夜晚环境音', - 'file': 'audio/night_ambient.wav', - 'volume': 0.2, - 'priority': 0 - } - } - - # 音频过渡配置 - self.transition_config = { - 'crossfade_time': 2.0, - 'max_concurrent_sounds': 5, - 'distance_attenuation': True, - 'occlusion_effect': True - } - - # 音频参数 - self.audio_parameters = { - 'master_volume': 1.0, - 'effects_volume': 1.0, - 'ambient_volume': 1.0, - 'music_volume': 1.0, - 'doppler_effect': True, - 'reverb_enabled': True - } - - # 音频更新配置 - self.update_interval = 0.1 # 更新间隔(秒) - self.last_update_time = 0.0 - - # 统计信息 - self.stats = { - 'sounds_played': 0, - 'transitions_made': 0, - 'sounds_stopped': 0, - 'total_audio_time': 0.0 - } - - print("✓ 音频管理系统已创建") - - def initialize(self) -> bool: - """ - 初始化音频管理系统 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 音频管理系统初始化完成") - return True - - except Exception as e: - print(f"✗ 音频管理系统初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用音频管理系统 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 音频管理系统未初始化") - return False - - self.enabled = True - print("✓ 音频管理系统已启用") - return True - - except Exception as e: - print(f"✗ 音频管理系统启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用音频管理系统""" - try: - self.enabled = False - self._stop_all_audio() - print("✓ 音频管理系统已禁用") - - except Exception as e: - print(f"✗ 音频管理系统禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理音频管理系统资源""" - try: - self.disable() - self.initialized = False - print("✓ 音频管理系统资源已清理") - - except Exception as e: - print(f"✗ 音频管理系统资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新音频管理系统状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新计时器 - self.last_update_time += dt - if self.last_update_time < self.update_interval: - return - - # 重置更新计时器 - self.last_update_time = 0.0 - - # 更新激活的音频 - self._update_active_audio(dt) - - # 根据当前天气和时间更新音频效果 - self._update_weather_audio() - self._update_time_audio() - - # 更新统计信息 - self.stats['total_audio_time'] += dt - - except Exception as e: - print(f"✗ 音频管理系统更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_active_audio(self, dt: float): - """ - 更新激活的音频 - - Args: - dt: 时间增量 - """ - try: - # 在实际实现中,这里会更新音频播放状态、音量淡入淡出等 - pass - - except Exception as e: - print(f"✗ 激活音频更新失败: {e}") - - def _update_weather_audio(self): - """根据当前天气更新音频效果""" - try: - if not self.plugin.weather_manager: - return - - current_weather = self.plugin.weather_manager.get_current_weather() - weather_info = self.plugin.weather_manager.get_weather_info(current_weather) - - if not weather_info: - return - - # 获取天气对应的音频效果 - audio_effect = weather_info.get('audio_effect') - - # 检查是否需要激活或更新音频效果 - if audio_effect and audio_effect in self.audio_effects: - # 检查效果是否已经激活 - if audio_effect not in self.active_audio: - # 激活音频效果 - self._play_audio_effect(audio_effect) - else: - # 更新现有音频效果的参数 - self._update_audio_effect_parameters(audio_effect) - else: - # 检查是否需要停用音频效果 - weather_effects = [ - name for name in self.active_audio.keys() - if name in self.audio_effects and name != 'birds' and name != 'crickets' - ] - - for effect_name in weather_effects: - self._stop_audio_effect(effect_name) - - except Exception as e: - print(f"✗ 天气音频更新失败: {e}") - - def _update_time_audio(self): - """根据当前时间更新环境音频""" - try: - if not self.plugin.environment_effects: - return - - time_config = getattr(self.plugin.environment_effects, 'time_config', {'day_time': 12.0}) - current_time = time_config.get('day_time', 12.0) - - # 根据时间播放不同的环境音 - if 6 <= current_time <= 18: # 白天 - if 'night_ambient' in self.active_audio: - self._stop_audio_effect('night_ambient') - if 'day_ambient' not in self.active_audio: - self._play_ambient_sound('day_ambient') - else: # 夜晚 - if 'day_ambient' in self.active_audio: - self._stop_audio_effect('day_ambient') - if 'night_ambient' not in self.active_audio: - self._play_ambient_sound('night_ambient') - - except Exception as e: - print(f"✗ 时间音频更新失败: {e}") - - def _play_audio_effect(self, effect_name: str): - """ - 播放音频效果 - - Args: - effect_name: 音频效果名称 - """ - try: - if effect_name not in self.audio_effects: - return - - effect_config = self.audio_effects[effect_name] - - # 创建音频数据 - audio_data = { - 'name': effect_config['name'], - 'file': effect_config['file'], - 'config': effect_config, - 'start_time': time.time(), - 'volume': effect_config['volume_range'][0], - 'target_volume': effect_config['volume_range'][1], - 'state': 'fading_in', # 'fading_in', 'playing', 'fading_out' - 'fade_progress': 0.0 - } - - # 添加到激活音频列表 - self.active_audio[effect_name] = audio_data - - # 更新统计信息 - self.stats['sounds_played'] += 1 - - print(f"✓ 音频效果开始播放: {effect_config['name']}") - - except Exception as e: - print(f"✗ 音频效果播放失败: {e}") - - def _play_ambient_sound(self, sound_name: str): - """ - 播放环境音 - - Args: - sound_name: 环境音名称 - """ - try: - if sound_name not in self.ambient_sounds: - return - - sound_config = self.ambient_sounds[sound_name] - - # 创建音频数据 - audio_data = { - 'name': sound_config['name'], - 'file': sound_config['file'], - 'config': sound_config, - 'start_time': time.time(), - 'volume': sound_config['volume'], - 'state': 'playing', - 'fade_progress': 1.0 - } - - # 添加到激活音频列表 - self.active_audio[sound_name] = audio_data - - # 更新统计信息 - self.stats['sounds_played'] += 1 - - print(f"✓ 环境音开始播放: {sound_config['name']}") - - except Exception as e: - print(f"✗ 环境音播放失败: {e}") - - def _update_audio_effect_parameters(self, effect_name: str): - """ - 更新音频效果参数 - - Args: - effect_name: 音频效果名称 - """ - try: - if effect_name not in self.active_audio: - return - - # 在实际实现中,这里会根据当前天气强度等因素更新音频参数 - # 例如:根据风速调整风声的音量和音调 - pass - - except Exception as e: - print(f"✗ 音频效果参数更新失败: {e}") - - def _stop_audio_effect(self, effect_name: str): - """ - 停止音频效果 - - Args: - effect_name: 音频效果名称 - """ - try: - if effect_name in self.active_audio: - audio_data = self.active_audio.pop(effect_name) - - # 更新统计信息 - self.stats['sounds_stopped'] += 1 - - print(f"✓ 音频效果已停止: {audio_data['name']}") - - except Exception as e: - print(f"✗ 音频效果停止失败: {e}") - - def _stop_all_audio(self): - """停止所有音频""" - try: - audio_names = list(self.active_audio.keys()) - for audio_name in audio_names: - self._stop_audio_effect(audio_name) - - except Exception as e: - print(f"✗ 所有音频停止失败: {e}") - - def play_sound(self, sound_name: str, volume: float = 1.0, pitch: float = 1.0): - """ - 播放音频 - - Args: - sound_name: 音频名称 - volume: 音量(0-1) - pitch: 音调(0.5-2.0) - """ - try: - # 检查是否是已知的音频效果 - if sound_name in self.audio_effects or sound_name in self.ambient_sounds: - if sound_name in self.audio_effects: - self._play_audio_effect(sound_name) - else: - self._play_ambient_sound(sound_name) - else: - # 播放一次性音频 - print(f"✓ 一次性音频已播放: {sound_name}") - self.stats['sounds_played'] += 1 - - except Exception as e: - print(f"✗ 音频播放失败: {e}") - - def stop_sound(self, sound_name: str): - """ - 停止音频 - - Args: - sound_name: 音频名称 - """ - self._stop_audio_effect(sound_name) - - def set_master_volume(self, volume: float): - """ - 设置主音量 - - Args: - volume: 音量(0-1) - """ - self.audio_parameters['master_volume'] = max(0.0, min(1.0, volume)) - print(f"✓ 主音量设置为: {volume}") - - def set_effects_volume(self, volume: float): - """ - 设置效果音量 - - Args: - volume: 音量(0-1) - """ - self.audio_parameters['effects_volume'] = max(0.0, min(1.0, volume)) - print(f"✓ 效果音量设置为: {volume}") - - def set_ambient_volume(self, volume: float): - """ - 设置环境音量 - - Args: - volume: 音量(0-1) - """ - self.audio_parameters['ambient_volume'] = max(0.0, min(1.0, volume)) - print(f"✓ 环境音量设置为: {volume}") - - def get_active_audio(self) -> List[str]: - """ - 获取激活的音频列表 - - Returns: - 激活的音频名称列表 - """ - return list(self.active_audio.keys()) - - def get_audio_info(self, audio_name: str) -> Optional[Dict[str, Any]]: - """ - 获取音频信息 - - Args: - audio_name: 音频名称 - - Returns: - 音频信息字典或None - """ - try: - # 检查音频效果 - if audio_name in self.audio_effects: - return self.audio_effects[audio_name].copy() - - # 检查环境音 - if audio_name in self.ambient_sounds: - return self.ambient_sounds[audio_name].copy() - - # 检查激活的音频 - if audio_name in self.active_audio: - return self.active_audio[audio_name].copy() - - return None - - except Exception as e: - print(f"✗ 音频信息获取失败: {e}") - return None - - def get_audio_effects_list(self) -> List[str]: - """ - 获取所有音频效果列表 - - Returns: - 音频效果名称列表 - """ - return list(self.audio_effects.keys()) - - def get_ambient_sounds_list(self) -> List[str]: - """ - 获取所有环境音列表 - - Returns: - 环境音名称列表 - """ - return list(self.ambient_sounds.keys()) - - def set_audio_parameter(self, parameter: str, value: Any): - """ - 设置音频参数 - - Args: - parameter: 参数名称 - value: 参数值 - """ - try: - if parameter in self.audio_parameters: - self.audio_parameters[parameter] = value - print(f"✓ 音频参数已设置: {parameter} = {value}") - else: - print(f"✗ 无效的音频参数: {parameter}") - - except Exception as e: - print(f"✗ 音频参数设置失败: {e}") - - def get_audio_parameters(self) -> Dict[str, Any]: - """ - 获取音频参数 - - Returns: - 音频参数字典 - """ - return self.audio_parameters.copy() - - def set_transition_config(self, config: Dict[str, Any]): - """ - 设置音频过渡配置 - - Args: - config: 配置字典 - """ - self.transition_config.update(config) - print(f"✓ 音频过渡配置已更新: {self.transition_config}") - - def get_transition_config(self) -> Dict[str, Any]: - """ - 获取音频过渡配置 - - Returns: - 音频过渡配置字典 - """ - return self.transition_config.copy() - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'sounds_played': 0, - 'transitions_made': 0, - 'sounds_stopped': 0, - 'total_audio_time': 0.0 - } - print("✓ 音频管理系统统计信息已重置") - - def set_update_interval(self, interval: float): - """ - 设置更新间隔 - - Args: - interval: 更新间隔(秒) - """ - self.update_interval = max(0.01, interval) - print(f"✓ 音频更新间隔设置为: {interval} 秒") - - def is_audio_active(self, audio_name: str) -> bool: - """ - 检查音频是否激活 - - Args: - audio_name: 音频名称 - - Returns: - 音频是否激活 - """ +""" +音频管理系统 +负责管理天气和季节的音频效果,如环境音、天气音效等 +""" + +import time +import random +from typing import Dict, Any, List, Optional +import math + +class AudioManager: + """ + 音频管理系统 + 负责管理天气和季节的音频效果,包括环境音、天气音效、音频过渡等 + """ + + def __init__(self, plugin): + """ + 初始化音频管理系统 + + Args: + plugin: 天气和季节系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 音频效果配置 + self.audio_effects = { + 'wind': { + 'name': '风声', + 'file': 'audio/wind.wav', + 'volume_range': (0.1, 0.5), + 'pitch_range': (0.8, 1.2), + 'loop': True, + 'fade_time': 2.0, + 'priority': 1 + }, + 'rain': { + 'name': '雨声', + 'file': 'audio/rain.wav', + 'volume_range': (0.2, 0.7), + 'pitch_range': (0.9, 1.1), + 'loop': True, + 'fade_time': 3.0, + 'priority': 2 + }, + 'thunderstorm': { + 'name': '雷暴声', + 'file': 'audio/thunderstorm.wav', + 'volume_range': (0.3, 1.0), + 'pitch_range': (0.7, 1.3), + 'loop': True, + 'fade_time': 4.0, + 'priority': 3 + }, + 'snow': { + 'name': '雪声', + 'file': 'audio/snow.wav', + 'volume_range': (0.05, 0.3), + 'pitch_range': (0.8, 1.0), + 'loop': True, + 'fade_time': 2.5, + 'priority': 1 + }, + 'hail': { + 'name': '冰雹声', + 'file': 'audio/hail.wav', + 'volume_range': (0.2, 0.8), + 'pitch_range': (0.9, 1.2), + 'loop': True, + 'fade_time': 2.0, + 'priority': 2 + }, + 'birds': { + 'name': '鸟鸣声', + 'file': 'audio/birds.wav', + 'volume_range': (0.1, 0.4), + 'pitch_range': (0.9, 1.1), + 'loop': True, + 'fade_time': 3.0, + 'priority': 1 + }, + 'crickets': { + 'name': '蟋蟀声', + 'file': 'audio/crickets.wav', + 'volume_range': (0.1, 0.3), + 'pitch_range': (0.8, 1.0), + 'loop': True, + 'fade_time': 2.5, + 'priority': 1 + }, + 'ocean': { + 'name': '海浪声', + 'file': 'audio/ocean.wav', + 'volume_range': (0.2, 0.6), + 'pitch_range': (0.9, 1.1), + 'loop': True, + 'fade_time': 3.5, + 'priority': 1 + } + } + + # 当前播放的音频 + self.active_audio = {} + + # 环境音配置 + self.ambient_sounds = { + 'day_ambient': { + 'name': '白天环境音', + 'file': 'audio/day_ambient.wav', + 'volume': 0.3, + 'priority': 0 + }, + 'night_ambient': { + 'name': '夜晚环境音', + 'file': 'audio/night_ambient.wav', + 'volume': 0.2, + 'priority': 0 + } + } + + # 音频过渡配置 + self.transition_config = { + 'crossfade_time': 2.0, + 'max_concurrent_sounds': 5, + 'distance_attenuation': True, + 'occlusion_effect': True + } + + # 音频参数 + self.audio_parameters = { + 'master_volume': 1.0, + 'effects_volume': 1.0, + 'ambient_volume': 1.0, + 'music_volume': 1.0, + 'doppler_effect': True, + 'reverb_enabled': True + } + + # 音频更新配置 + self.update_interval = 0.1 # 更新间隔(秒) + self.last_update_time = 0.0 + + # 统计信息 + self.stats = { + 'sounds_played': 0, + 'transitions_made': 0, + 'sounds_stopped': 0, + 'total_audio_time': 0.0 + } + + print("✓ 音频管理系统已创建") + + def initialize(self) -> bool: + """ + 初始化音频管理系统 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 音频管理系统初始化完成") + return True + + except Exception as e: + print(f"✗ 音频管理系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用音频管理系统 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 音频管理系统未初始化") + return False + + self.enabled = True + print("✓ 音频管理系统已启用") + return True + + except Exception as e: + print(f"✗ 音频管理系统启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用音频管理系统""" + try: + self.enabled = False + self._stop_all_audio() + print("✓ 音频管理系统已禁用") + + except Exception as e: + print(f"✗ 音频管理系统禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理音频管理系统资源""" + try: + self.disable() + self.initialized = False + print("✓ 音频管理系统资源已清理") + + except Exception as e: + print(f"✗ 音频管理系统资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新音频管理系统状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新计时器 + self.last_update_time += dt + if self.last_update_time < self.update_interval: + return + + # 重置更新计时器 + self.last_update_time = 0.0 + + # 更新激活的音频 + self._update_active_audio(dt) + + # 根据当前天气和时间更新音频效果 + self._update_weather_audio() + self._update_time_audio() + + # 更新统计信息 + self.stats['total_audio_time'] += dt + + except Exception as e: + print(f"✗ 音频管理系统更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_active_audio(self, dt: float): + """ + 更新激活的音频 + + Args: + dt: 时间增量 + """ + try: + # 在实际实现中,这里会更新音频播放状态、音量淡入淡出等 + pass + + except Exception as e: + print(f"✗ 激活音频更新失败: {e}") + + def _update_weather_audio(self): + """根据当前天气更新音频效果""" + try: + if not self.plugin.weather_manager: + return + + current_weather = self.plugin.weather_manager.get_current_weather() + weather_info = self.plugin.weather_manager.get_weather_info(current_weather) + + if not weather_info: + return + + # 获取天气对应的音频效果 + audio_effect = weather_info.get('audio_effect') + + # 检查是否需要激活或更新音频效果 + if audio_effect and audio_effect in self.audio_effects: + # 检查效果是否已经激活 + if audio_effect not in self.active_audio: + # 激活音频效果 + self._play_audio_effect(audio_effect) + else: + # 更新现有音频效果的参数 + self._update_audio_effect_parameters(audio_effect) + else: + # 检查是否需要停用音频效果 + weather_effects = [ + name for name in self.active_audio.keys() + if name in self.audio_effects and name != 'birds' and name != 'crickets' + ] + + for effect_name in weather_effects: + self._stop_audio_effect(effect_name) + + except Exception as e: + print(f"✗ 天气音频更新失败: {e}") + + def _update_time_audio(self): + """根据当前时间更新环境音频""" + try: + if not self.plugin.environment_effects: + return + + time_config = getattr(self.plugin.environment_effects, 'time_config', {'day_time': 12.0}) + current_time = time_config.get('day_time', 12.0) + + # 根据时间播放不同的环境音 + if 6 <= current_time <= 18: # 白天 + if 'night_ambient' in self.active_audio: + self._stop_audio_effect('night_ambient') + if 'day_ambient' not in self.active_audio: + self._play_ambient_sound('day_ambient') + else: # 夜晚 + if 'day_ambient' in self.active_audio: + self._stop_audio_effect('day_ambient') + if 'night_ambient' not in self.active_audio: + self._play_ambient_sound('night_ambient') + + except Exception as e: + print(f"✗ 时间音频更新失败: {e}") + + def _play_audio_effect(self, effect_name: str): + """ + 播放音频效果 + + Args: + effect_name: 音频效果名称 + """ + try: + if effect_name not in self.audio_effects: + return + + effect_config = self.audio_effects[effect_name] + + # 创建音频数据 + audio_data = { + 'name': effect_config['name'], + 'file': effect_config['file'], + 'config': effect_config, + 'start_time': time.time(), + 'volume': effect_config['volume_range'][0], + 'target_volume': effect_config['volume_range'][1], + 'state': 'fading_in', # 'fading_in', 'playing', 'fading_out' + 'fade_progress': 0.0 + } + + # 添加到激活音频列表 + self.active_audio[effect_name] = audio_data + + # 更新统计信息 + self.stats['sounds_played'] += 1 + + print(f"✓ 音频效果开始播放: {effect_config['name']}") + + except Exception as e: + print(f"✗ 音频效果播放失败: {e}") + + def _play_ambient_sound(self, sound_name: str): + """ + 播放环境音 + + Args: + sound_name: 环境音名称 + """ + try: + if sound_name not in self.ambient_sounds: + return + + sound_config = self.ambient_sounds[sound_name] + + # 创建音频数据 + audio_data = { + 'name': sound_config['name'], + 'file': sound_config['file'], + 'config': sound_config, + 'start_time': time.time(), + 'volume': sound_config['volume'], + 'state': 'playing', + 'fade_progress': 1.0 + } + + # 添加到激活音频列表 + self.active_audio[sound_name] = audio_data + + # 更新统计信息 + self.stats['sounds_played'] += 1 + + print(f"✓ 环境音开始播放: {sound_config['name']}") + + except Exception as e: + print(f"✗ 环境音播放失败: {e}") + + def _update_audio_effect_parameters(self, effect_name: str): + """ + 更新音频效果参数 + + Args: + effect_name: 音频效果名称 + """ + try: + if effect_name not in self.active_audio: + return + + # 在实际实现中,这里会根据当前天气强度等因素更新音频参数 + # 例如:根据风速调整风声的音量和音调 + pass + + except Exception as e: + print(f"✗ 音频效果参数更新失败: {e}") + + def _stop_audio_effect(self, effect_name: str): + """ + 停止音频效果 + + Args: + effect_name: 音频效果名称 + """ + try: + if effect_name in self.active_audio: + audio_data = self.active_audio.pop(effect_name) + + # 更新统计信息 + self.stats['sounds_stopped'] += 1 + + print(f"✓ 音频效果已停止: {audio_data['name']}") + + except Exception as e: + print(f"✗ 音频效果停止失败: {e}") + + def _stop_all_audio(self): + """停止所有音频""" + try: + audio_names = list(self.active_audio.keys()) + for audio_name in audio_names: + self._stop_audio_effect(audio_name) + + except Exception as e: + print(f"✗ 所有音频停止失败: {e}") + + def play_sound(self, sound_name: str, volume: float = 1.0, pitch: float = 1.0): + """ + 播放音频 + + Args: + sound_name: 音频名称 + volume: 音量(0-1) + pitch: 音调(0.5-2.0) + """ + try: + # 检查是否是已知的音频效果 + if sound_name in self.audio_effects or sound_name in self.ambient_sounds: + if sound_name in self.audio_effects: + self._play_audio_effect(sound_name) + else: + self._play_ambient_sound(sound_name) + else: + # 播放一次性音频 + print(f"✓ 一次性音频已播放: {sound_name}") + self.stats['sounds_played'] += 1 + + except Exception as e: + print(f"✗ 音频播放失败: {e}") + + def stop_sound(self, sound_name: str): + """ + 停止音频 + + Args: + sound_name: 音频名称 + """ + self._stop_audio_effect(sound_name) + + def set_master_volume(self, volume: float): + """ + 设置主音量 + + Args: + volume: 音量(0-1) + """ + self.audio_parameters['master_volume'] = max(0.0, min(1.0, volume)) + print(f"✓ 主音量设置为: {volume}") + + def set_effects_volume(self, volume: float): + """ + 设置效果音量 + + Args: + volume: 音量(0-1) + """ + self.audio_parameters['effects_volume'] = max(0.0, min(1.0, volume)) + print(f"✓ 效果音量设置为: {volume}") + + def set_ambient_volume(self, volume: float): + """ + 设置环境音量 + + Args: + volume: 音量(0-1) + """ + self.audio_parameters['ambient_volume'] = max(0.0, min(1.0, volume)) + print(f"✓ 环境音量设置为: {volume}") + + def get_active_audio(self) -> List[str]: + """ + 获取激活的音频列表 + + Returns: + 激活的音频名称列表 + """ + return list(self.active_audio.keys()) + + def get_audio_info(self, audio_name: str) -> Optional[Dict[str, Any]]: + """ + 获取音频信息 + + Args: + audio_name: 音频名称 + + Returns: + 音频信息字典或None + """ + try: + # 检查音频效果 + if audio_name in self.audio_effects: + return self.audio_effects[audio_name].copy() + + # 检查环境音 + if audio_name in self.ambient_sounds: + return self.ambient_sounds[audio_name].copy() + + # 检查激活的音频 + if audio_name in self.active_audio: + return self.active_audio[audio_name].copy() + + return None + + except Exception as e: + print(f"✗ 音频信息获取失败: {e}") + return None + + def get_audio_effects_list(self) -> List[str]: + """ + 获取所有音频效果列表 + + Returns: + 音频效果名称列表 + """ + return list(self.audio_effects.keys()) + + def get_ambient_sounds_list(self) -> List[str]: + """ + 获取所有环境音列表 + + Returns: + 环境音名称列表 + """ + return list(self.ambient_sounds.keys()) + + def set_audio_parameter(self, parameter: str, value: Any): + """ + 设置音频参数 + + Args: + parameter: 参数名称 + value: 参数值 + """ + try: + if parameter in self.audio_parameters: + self.audio_parameters[parameter] = value + print(f"✓ 音频参数已设置: {parameter} = {value}") + else: + print(f"✗ 无效的音频参数: {parameter}") + + except Exception as e: + print(f"✗ 音频参数设置失败: {e}") + + def get_audio_parameters(self) -> Dict[str, Any]: + """ + 获取音频参数 + + Returns: + 音频参数字典 + """ + return self.audio_parameters.copy() + + def set_transition_config(self, config: Dict[str, Any]): + """ + 设置音频过渡配置 + + Args: + config: 配置字典 + """ + self.transition_config.update(config) + print(f"✓ 音频过渡配置已更新: {self.transition_config}") + + def get_transition_config(self) -> Dict[str, Any]: + """ + 获取音频过渡配置 + + Returns: + 音频过渡配置字典 + """ + return self.transition_config.copy() + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'sounds_played': 0, + 'transitions_made': 0, + 'sounds_stopped': 0, + 'total_audio_time': 0.0 + } + print("✓ 音频管理系统统计信息已重置") + + def set_update_interval(self, interval: float): + """ + 设置更新间隔 + + Args: + interval: 更新间隔(秒) + """ + self.update_interval = max(0.01, interval) + print(f"✓ 音频更新间隔设置为: {interval} 秒") + + def is_audio_active(self, audio_name: str) -> bool: + """ + 检查音频是否激活 + + Args: + audio_name: 音频名称 + + Returns: + 音频是否激活 + """ return audio_name in self.active_audio \ No newline at end of file diff --git a/plugins/user/weather_season_system/core/season_manager.py b/plugins/user/weather_season_system/core/season_manager.py index b62c0f0f..28313e17 100644 --- a/plugins/user/weather_season_system/core/season_manager.py +++ b/plugins/user/weather_season_system/core/season_manager.py @@ -1,615 +1,615 @@ -""" -季节管理器 -负责管理季节变化和季节相关参数 -""" - -import time -from typing import Dict, Any, List, Optional -import math - -class SeasonManager: - """ - 季节管理器 - 负责管理季节变化、季节参数和季节过渡效果 - """ - - def __init__(self, plugin): - """ - 初始化季节管理器 - - Args: - plugin: 天气和季节系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 季节配置 - self.seasons = { - 'spring': { - 'name': '春季', - 'description': '万物复苏的季节', - 'temperature': 15.0, # 平均温度(摄氏度) - 'humidity': 60.0, # 平均湿度(%) - 'day_length': 12.5, # 日照时长(小时) - 'transition_duration': 7.0, # 季节过渡持续时间(天) - 'color_tint': (1.0, 1.0, 0.8), # 颜色色调(RGB) - 'vegetation_growth': 0.7, # 植被生长状态(0-1) - 'precipitation_frequency': 0.3 # 降水频率(0-1) - }, - 'summer': { - 'name': '夏季', - 'description': '炎热的季节', - 'temperature': 28.0, - 'humidity': 70.0, - 'day_length': 14.5, - 'transition_duration': 7.0, - 'color_tint': (1.0, 0.9, 0.7), - 'vegetation_growth': 1.0, - 'precipitation_frequency': 0.4 - }, - 'autumn': { - 'name': '秋季', - 'description': '收获的季节', - 'temperature': 12.0, - 'humidity': 55.0, - 'day_length': 11.5, - 'transition_duration': 7.0, - 'color_tint': (1.0, 0.8, 0.6), - 'vegetation_growth': 0.5, - 'precipitation_frequency': 0.25 - }, - 'winter': { - 'name': '冬季', - 'description': '寒冷的季节', - 'temperature': -2.0, - 'humidity': 45.0, - 'day_length': 9.0, - 'transition_duration': 7.0, - 'color_tint': (0.8, 0.8, 1.0), - 'vegetation_growth': 0.1, - 'precipitation_frequency': 0.2 - } - } - - # 当前季节状态 - self.current_season = 'spring' - self.target_season = 'spring' - self.season_transition_progress = 0.0 # 季节过渡进度(0-1) - self.season_transition_active = False - - # 季节时间配置 - self.season_duration = 90 # 每个季节持续天数 - self.current_day_in_season = 0 # 当前季节中的天数 - self.total_days = 0 # 总天数 - - # 季节循环配置 - self.season_cycle_enabled = True - self.season_order = ['spring', 'summer', 'autumn', 'winter'] - - # 统计信息 - self.stats = { - 'seasons_changed': 0, - 'transitions_completed': 0, - 'total_season_time': 0.0 - } - - # 编辑器相关 - self.is_being_edited = False - self.edit_timestamp = 0 - self.edit_history = [] - self.max_history_size = 100 - - print("✓ 季节管理器已创建") - - def initialize(self) -> bool: - """ - 初始化季节管理器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 季节管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 季节管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用季节管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 季节管理器未初始化") - return False - - self.enabled = True - print("✓ 季节管理器已启用") - return True - - except Exception as e: - print(f"✗ 季节管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用季节管理器""" - try: - self.enabled = False - print("✓ 季节管理器已禁用") - - except Exception as e: - print(f"✗ 季节管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理季节管理器资源""" - try: - self.disable() - self.initialized = False - print("✓ 季节管理器资源已清理") - - except Exception as e: - print(f"✗ 季节管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新季节管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新季节时间 - self._update_season_time(dt) - - # 更新季节过渡 - if self.season_transition_active: - self._update_season_transition(dt) - - except Exception as e: - print(f"✗ 季节管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_season_time(self, dt: float): - """ - 更新季节时间 - - Args: - dt: 时间增量 - """ - try: - # 假设1秒代表1小时游戏时间 - hours_passed = dt / 1.0 - - # 更新当前季节中的天数 - self.current_day_in_season += hours_passed / 24.0 - self.total_days += hours_passed / 24.0 - - # 检查是否需要切换季节 - if self.current_day_in_season >= self.season_duration and self.season_cycle_enabled: - self._transition_to_next_season() - - except Exception as e: - print(f"✗ 季节时间更新失败: {e}") - - def _transition_to_next_season(self): - """过渡到下一个季节""" - try: - # 找到当前季节在顺序中的位置 - current_index = self.season_order.index(self.current_season) - - # 计算下一个季节 - next_index = (current_index + 1) % len(self.season_order) - self.target_season = self.season_order[next_index] - - # 启动季节过渡 - self.start_season_transition() - - except Exception as e: - print(f"✗ 季节切换失败: {e}") - - def start_season_transition(self): - """开始季节过渡""" - try: - if self.current_season == self.target_season: - return - - self.season_transition_active = True - self.season_transition_progress = 0.0 - - print(f"✓ 开始季节过渡: {self.current_season} -> {self.target_season}") - - except Exception as e: - print(f"✗ 季节过渡启动失败: {e}") - - def _update_season_transition(self, dt: float): - """ - 更新季节过渡 - - Args: - dt: 时间增量 - """ - try: - if not self.season_transition_active: - return - - # 假设1秒代表1小时游戏时间 - hours_passed = dt / 1.0 - - # 获取过渡持续时间(天) - transition_duration = self.seasons[self.target_season]['transition_duration'] - - # 计算进度增量(每小时进度) - progress_per_hour = 1.0 / (transition_duration * 24.0) - progress_delta = progress_per_hour * hours_passed - - # 更新过渡进度 - self.season_transition_progress += progress_delta - - # 检查过渡是否完成 - if self.season_transition_progress >= 1.0: - self._complete_season_transition() - - except Exception as e: - print(f"✗ 季节过渡更新失败: {e}") - - def _complete_season_transition(self): - """完成季节过渡""" - try: - # 更新当前季节 - self.current_season = self.target_season - - # 重置季节内天数 - self.current_day_in_season = 0 - - # 重置过渡状态 - self.season_transition_active = False - self.season_transition_progress = 0.0 - - # 更新统计信息 - self.stats['seasons_changed'] += 1 - self.stats['transitions_completed'] += 1 - - print(f"✓ 季节过渡完成: 现在是{self.seasons[self.current_season]['name']}") - - # 触发季节变化事件 - self._trigger_season_change_event() - - except Exception as e: - print(f"✗ 季节过渡完成失败: {e}") - - def _trigger_season_change_event(self): - """触发季节变化事件""" - try: - # 在实际实现中,这里会触发一个事件通知其他系统季节已变化 - pass - except Exception as e: - print(f"✗ 季节变化事件触发失败: {e}") - - def set_current_season(self, season: str): - """ - 设置当前季节 - - Args: - season: 季节名称 ('spring', 'summer', 'autumn', 'winter') - """ - try: - if season not in self.seasons: - print(f"✗ 无效的季节: {season}") - return - - self.current_season = season - self.current_day_in_season = 0 - - print(f"✓ 当前季节设置为: {self.seasons[season]['name']}") - - except Exception as e: - print(f"✗ 当前季节设置失败: {e}") - - def set_target_season(self, season: str): - """ - 设置目标季节 - - Args: - season: 季节名称 ('spring', 'summer', 'autumn', 'winter') - """ - try: - if season not in self.seasons: - print(f"✗ 无效的季节: {season}") - return - - self.target_season = season - self.start_season_transition() - - except Exception as e: - print(f"✗ 目标季节设置失败: {e}") - - def set_season_duration(self, days: int): - """ - 设置季节持续时间 - - Args: - days: 持续天数 - """ - if days > 0: - self.season_duration = days - print(f"✓ 季节持续时间设置为: {days} 天") - else: - print("✗ 无效的季节持续时间") - - def enable_season_cycle(self, enabled: bool): - """ - 启用/禁用季节循环 - - Args: - enabled: 是否启用季节循环 - """ - self.season_cycle_enabled = enabled - print(f"✓ 季节循环{'已启用' if enabled else '已禁用'}") - - def get_current_season(self) -> str: - """ - 获取当前季节 - - Returns: - 当前季节名称 - """ - return self.current_season - - def get_season_info(self, season: str = None) -> Optional[Dict[str, Any]]: - """ - 获取季节信息 - - Args: - season: 季节名称(如果为None,则返回当前季节信息) - - Returns: - 季节信息字典或None - """ - try: - if season is None: - season = self.current_season - - if season not in self.seasons: - return None - - return self.seasons[season].copy() - - except Exception as e: - print(f"✗ 季节信息获取失败: {e}") - return None - - def get_interpolated_season_params(self) -> Dict[str, Any]: - """ - 获取插值后的季节参数(考虑季节过渡) - - Returns: - 插值后的季节参数字典 - """ - try: - if not self.season_transition_active or self.season_transition_progress <= 0.0: - # 没有过渡,返回当前季节参数 - return self.seasons[self.current_season].copy() - - if self.season_transition_progress >= 1.0: - # 过渡完成,返回目标季节参数 - return self.seasons[self.target_season].copy() - - # 进行插值计算 - current_params = self.seasons[self.current_season] - target_params = self.seasons[self.target_season] - - interpolated_params = {} - - # 对数值参数进行线性插值 - numeric_params = ['temperature', 'humidity', 'day_length', 'vegetation_growth', 'precipitation_frequency'] - for param in numeric_params: - current_value = current_params[param] - target_value = target_params[param] - interpolated_value = current_value + (target_value - current_value) * self.season_transition_progress - interpolated_params[param] = interpolated_value - - # 对颜色进行插值 - current_color = current_params['color_tint'] - target_color = target_params['color_tint'] - interpolated_color = tuple( - current_color[i] + (target_color[i] - current_color[i]) * self.season_transition_progress - for i in range(3) - ) - interpolated_params['color_tint'] = interpolated_color - - # 复制非数值参数 - interpolated_params['name'] = f"{current_params['name']} → {target_params['name']}" - interpolated_params['description'] = f"过渡季节 ({self.season_transition_progress*100:.1f}%)" - interpolated_params['transition_duration'] = target_params['transition_duration'] - - return interpolated_params - - except Exception as e: - print(f"✗ 插值季节参数获取失败: {e}") - return self.seasons[self.current_season].copy() - - def get_season_progress(self) -> float: - """ - 获取当前季节进度 - - Returns: - 季节进度 (0-1) - """ - if self.season_duration > 0: - return min(1.0, self.current_day_in_season / self.season_duration) - return 0.0 - - def get_transition_progress(self) -> float: - """ - 获取季节过渡进度 - - Returns: - 过渡进度 (0-1) - """ - return self.season_transition_progress - - def is_transition_active(self) -> bool: - """ - 检查是否正在进行季节过渡 - - Returns: - 是否正在进行季节过渡 - """ - return self.season_transition_active - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'seasons_changed': 0, - 'transitions_completed': 0, - 'total_season_time': 0.0 - } - print("✓ 季节管理器统计信息已重置") - - def get_season_list(self) -> List[str]: - """ - 获取所有季节列表 - - Returns: - 季节名称列表 - """ - return list(self.seasons.keys()) - - def get_season_name(self, season: str) -> str: - """ - 获取季节显示名称 - - Args: - season: 季节名称 - - Returns: - 季节显示名称 - """ - if season in self.seasons: - return self.seasons[season]['name'] - return season - - def _save_edit_history(self): - """保存编辑历史""" - try: - # 创建当前状态快照 - snapshot = { - 'timestamp': time.time(), - 'current_season': self.current_season, - 'target_season': self.target_season, - 'season_duration': self.season_duration, - 'season_cycle_enabled': self.season_cycle_enabled, - 'season_transition_active': self.season_transition_active, - 'season_transition_progress': self.season_transition_progress - } - - # 添加到历史记录 - self.edit_history.append(snapshot) - - # 限制历史记录大小 - if len(self.edit_history) > self.max_history_size: - self.edit_history.pop(0) - - except Exception as e: - print(f"✗ 编辑历史保存失败: {e}") - - def undo_edit(self) -> bool: - """ - 撤销编辑 - - Returns: - 是否撤销成功 - """ - try: - if not self.edit_history: - print("✗ 没有可撤销的编辑") - return False - - # 恢复到上一个状态 - last_state = self.edit_history.pop() - - # 恢复参数 - self.current_season = last_state['current_season'] - self.target_season = last_state['target_season'] - self.season_duration = last_state['season_duration'] - self.season_cycle_enabled = last_state['season_cycle_enabled'] - self.season_transition_active = last_state['season_transition_active'] - self.season_transition_progress = last_state['season_transition_progress'] - - print("✓ 编辑已撤销") - return True - - except Exception as e: - print(f"✗ 编辑撤销失败: {e}") - import traceback - traceback.print_exc() - return False - - def get_edit_history_size(self) -> int: - """ - 获取编辑历史大小 - - Returns: - 编辑历史记录数量 - """ - return len(self.edit_history) - - def clear_edit_history(self): - """清空编辑历史""" - try: - self.edit_history.clear() - print("✓ 编辑历史已清空") - except Exception as e: - print(f"✗ 编辑历史清空失败: {e}") - - def get_current_state_info(self) -> Dict[str, Any]: - """ - 获取当前状态信息 - - Returns: - 当前状态信息字典 - """ - try: - return { - 'current_season': self.current_season, - 'target_season': self.target_season, - 'season_duration': self.season_duration, - 'season_cycle_enabled': self.season_cycle_enabled, - 'season_transition_active': self.season_transition_active, - 'season_transition_progress': self.season_transition_progress, - 'current_day_in_season': self.current_day_in_season, - 'is_being_edited': self.is_being_edited, - 'edit_timestamp': self.edit_timestamp, - 'edit_history_size': len(self.edit_history) - } - except Exception as e: - print(f"✗ 当前状态信息获取失败: {e}") +""" +季节管理器 +负责管理季节变化和季节相关参数 +""" + +import time +from typing import Dict, Any, List, Optional +import math + +class SeasonManager: + """ + 季节管理器 + 负责管理季节变化、季节参数和季节过渡效果 + """ + + def __init__(self, plugin): + """ + 初始化季节管理器 + + Args: + plugin: 天气和季节系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 季节配置 + self.seasons = { + 'spring': { + 'name': '春季', + 'description': '万物复苏的季节', + 'temperature': 15.0, # 平均温度(摄氏度) + 'humidity': 60.0, # 平均湿度(%) + 'day_length': 12.5, # 日照时长(小时) + 'transition_duration': 7.0, # 季节过渡持续时间(天) + 'color_tint': (1.0, 1.0, 0.8), # 颜色色调(RGB) + 'vegetation_growth': 0.7, # 植被生长状态(0-1) + 'precipitation_frequency': 0.3 # 降水频率(0-1) + }, + 'summer': { + 'name': '夏季', + 'description': '炎热的季节', + 'temperature': 28.0, + 'humidity': 70.0, + 'day_length': 14.5, + 'transition_duration': 7.0, + 'color_tint': (1.0, 0.9, 0.7), + 'vegetation_growth': 1.0, + 'precipitation_frequency': 0.4 + }, + 'autumn': { + 'name': '秋季', + 'description': '收获的季节', + 'temperature': 12.0, + 'humidity': 55.0, + 'day_length': 11.5, + 'transition_duration': 7.0, + 'color_tint': (1.0, 0.8, 0.6), + 'vegetation_growth': 0.5, + 'precipitation_frequency': 0.25 + }, + 'winter': { + 'name': '冬季', + 'description': '寒冷的季节', + 'temperature': -2.0, + 'humidity': 45.0, + 'day_length': 9.0, + 'transition_duration': 7.0, + 'color_tint': (0.8, 0.8, 1.0), + 'vegetation_growth': 0.1, + 'precipitation_frequency': 0.2 + } + } + + # 当前季节状态 + self.current_season = 'spring' + self.target_season = 'spring' + self.season_transition_progress = 0.0 # 季节过渡进度(0-1) + self.season_transition_active = False + + # 季节时间配置 + self.season_duration = 90 # 每个季节持续天数 + self.current_day_in_season = 0 # 当前季节中的天数 + self.total_days = 0 # 总天数 + + # 季节循环配置 + self.season_cycle_enabled = True + self.season_order = ['spring', 'summer', 'autumn', 'winter'] + + # 统计信息 + self.stats = { + 'seasons_changed': 0, + 'transitions_completed': 0, + 'total_season_time': 0.0 + } + + # 编辑器相关 + self.is_being_edited = False + self.edit_timestamp = 0 + self.edit_history = [] + self.max_history_size = 100 + + print("✓ 季节管理器已创建") + + def initialize(self) -> bool: + """ + 初始化季节管理器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 季节管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 季节管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用季节管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 季节管理器未初始化") + return False + + self.enabled = True + print("✓ 季节管理器已启用") + return True + + except Exception as e: + print(f"✗ 季节管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用季节管理器""" + try: + self.enabled = False + print("✓ 季节管理器已禁用") + + except Exception as e: + print(f"✗ 季节管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理季节管理器资源""" + try: + self.disable() + self.initialized = False + print("✓ 季节管理器资源已清理") + + except Exception as e: + print(f"✗ 季节管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新季节管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新季节时间 + self._update_season_time(dt) + + # 更新季节过渡 + if self.season_transition_active: + self._update_season_transition(dt) + + except Exception as e: + print(f"✗ 季节管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_season_time(self, dt: float): + """ + 更新季节时间 + + Args: + dt: 时间增量 + """ + try: + # 假设1秒代表1小时游戏时间 + hours_passed = dt / 1.0 + + # 更新当前季节中的天数 + self.current_day_in_season += hours_passed / 24.0 + self.total_days += hours_passed / 24.0 + + # 检查是否需要切换季节 + if self.current_day_in_season >= self.season_duration and self.season_cycle_enabled: + self._transition_to_next_season() + + except Exception as e: + print(f"✗ 季节时间更新失败: {e}") + + def _transition_to_next_season(self): + """过渡到下一个季节""" + try: + # 找到当前季节在顺序中的位置 + current_index = self.season_order.index(self.current_season) + + # 计算下一个季节 + next_index = (current_index + 1) % len(self.season_order) + self.target_season = self.season_order[next_index] + + # 启动季节过渡 + self.start_season_transition() + + except Exception as e: + print(f"✗ 季节切换失败: {e}") + + def start_season_transition(self): + """开始季节过渡""" + try: + if self.current_season == self.target_season: + return + + self.season_transition_active = True + self.season_transition_progress = 0.0 + + print(f"✓ 开始季节过渡: {self.current_season} -> {self.target_season}") + + except Exception as e: + print(f"✗ 季节过渡启动失败: {e}") + + def _update_season_transition(self, dt: float): + """ + 更新季节过渡 + + Args: + dt: 时间增量 + """ + try: + if not self.season_transition_active: + return + + # 假设1秒代表1小时游戏时间 + hours_passed = dt / 1.0 + + # 获取过渡持续时间(天) + transition_duration = self.seasons[self.target_season]['transition_duration'] + + # 计算进度增量(每小时进度) + progress_per_hour = 1.0 / (transition_duration * 24.0) + progress_delta = progress_per_hour * hours_passed + + # 更新过渡进度 + self.season_transition_progress += progress_delta + + # 检查过渡是否完成 + if self.season_transition_progress >= 1.0: + self._complete_season_transition() + + except Exception as e: + print(f"✗ 季节过渡更新失败: {e}") + + def _complete_season_transition(self): + """完成季节过渡""" + try: + # 更新当前季节 + self.current_season = self.target_season + + # 重置季节内天数 + self.current_day_in_season = 0 + + # 重置过渡状态 + self.season_transition_active = False + self.season_transition_progress = 0.0 + + # 更新统计信息 + self.stats['seasons_changed'] += 1 + self.stats['transitions_completed'] += 1 + + print(f"✓ 季节过渡完成: 现在是{self.seasons[self.current_season]['name']}") + + # 触发季节变化事件 + self._trigger_season_change_event() + + except Exception as e: + print(f"✗ 季节过渡完成失败: {e}") + + def _trigger_season_change_event(self): + """触发季节变化事件""" + try: + # 在实际实现中,这里会触发一个事件通知其他系统季节已变化 + pass + except Exception as e: + print(f"✗ 季节变化事件触发失败: {e}") + + def set_current_season(self, season: str): + """ + 设置当前季节 + + Args: + season: 季节名称 ('spring', 'summer', 'autumn', 'winter') + """ + try: + if season not in self.seasons: + print(f"✗ 无效的季节: {season}") + return + + self.current_season = season + self.current_day_in_season = 0 + + print(f"✓ 当前季节设置为: {self.seasons[season]['name']}") + + except Exception as e: + print(f"✗ 当前季节设置失败: {e}") + + def set_target_season(self, season: str): + """ + 设置目标季节 + + Args: + season: 季节名称 ('spring', 'summer', 'autumn', 'winter') + """ + try: + if season not in self.seasons: + print(f"✗ 无效的季节: {season}") + return + + self.target_season = season + self.start_season_transition() + + except Exception as e: + print(f"✗ 目标季节设置失败: {e}") + + def set_season_duration(self, days: int): + """ + 设置季节持续时间 + + Args: + days: 持续天数 + """ + if days > 0: + self.season_duration = days + print(f"✓ 季节持续时间设置为: {days} 天") + else: + print("✗ 无效的季节持续时间") + + def enable_season_cycle(self, enabled: bool): + """ + 启用/禁用季节循环 + + Args: + enabled: 是否启用季节循环 + """ + self.season_cycle_enabled = enabled + print(f"✓ 季节循环{'已启用' if enabled else '已禁用'}") + + def get_current_season(self) -> str: + """ + 获取当前季节 + + Returns: + 当前季节名称 + """ + return self.current_season + + def get_season_info(self, season: str = None) -> Optional[Dict[str, Any]]: + """ + 获取季节信息 + + Args: + season: 季节名称(如果为None,则返回当前季节信息) + + Returns: + 季节信息字典或None + """ + try: + if season is None: + season = self.current_season + + if season not in self.seasons: + return None + + return self.seasons[season].copy() + + except Exception as e: + print(f"✗ 季节信息获取失败: {e}") + return None + + def get_interpolated_season_params(self) -> Dict[str, Any]: + """ + 获取插值后的季节参数(考虑季节过渡) + + Returns: + 插值后的季节参数字典 + """ + try: + if not self.season_transition_active or self.season_transition_progress <= 0.0: + # 没有过渡,返回当前季节参数 + return self.seasons[self.current_season].copy() + + if self.season_transition_progress >= 1.0: + # 过渡完成,返回目标季节参数 + return self.seasons[self.target_season].copy() + + # 进行插值计算 + current_params = self.seasons[self.current_season] + target_params = self.seasons[self.target_season] + + interpolated_params = {} + + # 对数值参数进行线性插值 + numeric_params = ['temperature', 'humidity', 'day_length', 'vegetation_growth', 'precipitation_frequency'] + for param in numeric_params: + current_value = current_params[param] + target_value = target_params[param] + interpolated_value = current_value + (target_value - current_value) * self.season_transition_progress + interpolated_params[param] = interpolated_value + + # 对颜色进行插值 + current_color = current_params['color_tint'] + target_color = target_params['color_tint'] + interpolated_color = tuple( + current_color[i] + (target_color[i] - current_color[i]) * self.season_transition_progress + for i in range(3) + ) + interpolated_params['color_tint'] = interpolated_color + + # 复制非数值参数 + interpolated_params['name'] = f"{current_params['name']} → {target_params['name']}" + interpolated_params['description'] = f"过渡季节 ({self.season_transition_progress*100:.1f}%)" + interpolated_params['transition_duration'] = target_params['transition_duration'] + + return interpolated_params + + except Exception as e: + print(f"✗ 插值季节参数获取失败: {e}") + return self.seasons[self.current_season].copy() + + def get_season_progress(self) -> float: + """ + 获取当前季节进度 + + Returns: + 季节进度 (0-1) + """ + if self.season_duration > 0: + return min(1.0, self.current_day_in_season / self.season_duration) + return 0.0 + + def get_transition_progress(self) -> float: + """ + 获取季节过渡进度 + + Returns: + 过渡进度 (0-1) + """ + return self.season_transition_progress + + def is_transition_active(self) -> bool: + """ + 检查是否正在进行季节过渡 + + Returns: + 是否正在进行季节过渡 + """ + return self.season_transition_active + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'seasons_changed': 0, + 'transitions_completed': 0, + 'total_season_time': 0.0 + } + print("✓ 季节管理器统计信息已重置") + + def get_season_list(self) -> List[str]: + """ + 获取所有季节列表 + + Returns: + 季节名称列表 + """ + return list(self.seasons.keys()) + + def get_season_name(self, season: str) -> str: + """ + 获取季节显示名称 + + Args: + season: 季节名称 + + Returns: + 季节显示名称 + """ + if season in self.seasons: + return self.seasons[season]['name'] + return season + + def _save_edit_history(self): + """保存编辑历史""" + try: + # 创建当前状态快照 + snapshot = { + 'timestamp': time.time(), + 'current_season': self.current_season, + 'target_season': self.target_season, + 'season_duration': self.season_duration, + 'season_cycle_enabled': self.season_cycle_enabled, + 'season_transition_active': self.season_transition_active, + 'season_transition_progress': self.season_transition_progress + } + + # 添加到历史记录 + self.edit_history.append(snapshot) + + # 限制历史记录大小 + if len(self.edit_history) > self.max_history_size: + self.edit_history.pop(0) + + except Exception as e: + print(f"✗ 编辑历史保存失败: {e}") + + def undo_edit(self) -> bool: + """ + 撤销编辑 + + Returns: + 是否撤销成功 + """ + try: + if not self.edit_history: + print("✗ 没有可撤销的编辑") + return False + + # 恢复到上一个状态 + last_state = self.edit_history.pop() + + # 恢复参数 + self.current_season = last_state['current_season'] + self.target_season = last_state['target_season'] + self.season_duration = last_state['season_duration'] + self.season_cycle_enabled = last_state['season_cycle_enabled'] + self.season_transition_active = last_state['season_transition_active'] + self.season_transition_progress = last_state['season_transition_progress'] + + print("✓ 编辑已撤销") + return True + + except Exception as e: + print(f"✗ 编辑撤销失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_edit_history_size(self) -> int: + """ + 获取编辑历史大小 + + Returns: + 编辑历史记录数量 + """ + return len(self.edit_history) + + def clear_edit_history(self): + """清空编辑历史""" + try: + self.edit_history.clear() + print("✓ 编辑历史已清空") + except Exception as e: + print(f"✗ 编辑历史清空失败: {e}") + + def get_current_state_info(self) -> Dict[str, Any]: + """ + 获取当前状态信息 + + Returns: + 当前状态信息字典 + """ + try: + return { + 'current_season': self.current_season, + 'target_season': self.target_season, + 'season_duration': self.season_duration, + 'season_cycle_enabled': self.season_cycle_enabled, + 'season_transition_active': self.season_transition_active, + 'season_transition_progress': self.season_transition_progress, + 'current_day_in_season': self.current_day_in_season, + 'is_being_edited': self.is_being_edited, + 'edit_timestamp': self.edit_timestamp, + 'edit_history_size': len(self.edit_history) + } + except Exception as e: + print(f"✗ 当前状态信息获取失败: {e}") return {} \ No newline at end of file diff --git a/plugins/user/weather_season_system/core/weather_manager.py b/plugins/user/weather_season_system/core/weather_manager.py index 1266b4d6..de31e962 100644 --- a/plugins/user/weather_season_system/core/weather_manager.py +++ b/plugins/user/weather_season_system/core/weather_manager.py @@ -1,851 +1,851 @@ -""" -天气管理器 -负责管理天气系统和天气变化 -""" - -import time -import random -from typing import Dict, Any, List, Optional -import math - -class WeatherManager: - """ - 天气管理器 - 负责管理天气系统、天气变化和天气过渡效果 - """ - - def __init__(self, plugin): - """ - 初始化天气管理器 - - Args: - plugin: 天气和季节系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 天气类型定义 - self.weather_types = { - 'clear': { - 'name': '晴朗', - 'description': '晴朗无云的天气', - 'temperature_modifier': 0.0, - 'humidity_modifier': -10.0, - 'wind_speed': 2.0, - 'visibility': 1.0, - 'precipitation': 0.0, - 'cloud_cover': 0.1, - 'light_intensity': 1.0, - 'particle_effect': None, - 'audio_effect': None, - 'transition_time': 300.0, # 过渡时间(秒) - 'probability_weight': 30.0 # 概率权重 - }, - 'cloudy': { - 'name': '多云', - 'description': '多云天气,阳光时隐时现', - 'temperature_modifier': -2.0, - 'humidity_modifier': 5.0, - 'wind_speed': 4.0, - 'visibility': 0.9, - 'precipitation': 0.0, - 'cloud_cover': 0.7, - 'light_intensity': 0.7, - 'particle_effect': 'light_fog', - 'audio_effect': 'wind', - 'transition_time': 600.0, - 'probability_weight': 20.0 - }, - 'rain': { - 'name': '下雨', - 'description': '降雨天气', - 'temperature_modifier': -3.0, - 'humidity_modifier': 20.0, - 'wind_speed': 6.0, - 'visibility': 0.7, - 'precipitation': 1.0, - 'cloud_cover': 0.9, - 'light_intensity': 0.5, - 'particle_effect': 'rain', - 'audio_effect': 'rain', - 'transition_time': 120.0, - 'probability_weight': 15.0 - }, - 'storm': { - 'name': '暴风雨', - 'description': '强烈的雷暴天气', - 'temperature_modifier': -5.0, - 'humidity_modifier': 30.0, - 'wind_speed': 15.0, - 'visibility': 0.4, - 'precipitation': 1.0, - 'cloud_cover': 1.0, - 'light_intensity': 0.3, - 'particle_effect': 'storm', - 'audio_effect': 'thunderstorm', - 'transition_time': 90.0, - 'probability_weight': 5.0 - }, - 'snow': { - 'name': '下雪', - 'description': '降雪天气', - 'temperature_modifier': -10.0, - 'humidity_modifier': 15.0, - 'wind_speed': 5.0, - 'visibility': 0.6, - 'precipitation': 0.8, - 'cloud_cover': 0.8, - 'light_intensity': 0.6, - 'particle_effect': 'snow', - 'audio_effect': 'wind', - 'transition_time': 180.0, - 'probability_weight': 8.0 - }, - 'fog': { - 'name': '雾天', - 'description': '大雾天气,能见度低', - 'temperature_modifier': -1.0, - 'humidity_modifier': 25.0, - 'wind_speed': 1.0, - 'visibility': 0.3, - 'precipitation': 0.0, - 'cloud_cover': 0.5, - 'light_intensity': 0.4, - 'particle_effect': 'heavy_fog', - 'audio_effect': 'wind', - 'transition_time': 300.0, - 'probability_weight': 7.0 - }, - 'windy': { - 'name': '大风', - 'description': '强风天气', - 'temperature_modifier': -2.0, - 'humidity_modifier': 0.0, - 'wind_speed': 12.0, - 'visibility': 0.8, - 'precipitation': 0.0, - 'cloud_cover': 0.4, - 'light_intensity': 0.9, - 'particle_effect': 'wind_debris', - 'audio_effect': 'strong_wind', - 'transition_time': 150.0, - 'probability_weight': 10.0 - }, - 'hail': { - 'name': '冰雹', - 'description': '降冰雹天气', - 'temperature_modifier': -4.0, - 'humidity_modifier': 20.0, - 'wind_speed': 8.0, - 'visibility': 0.5, - 'precipitation': 1.0, - 'cloud_cover': 0.9, - 'light_intensity': 0.4, - 'particle_effect': 'hail', - 'audio_effect': 'hail', - 'transition_time': 120.0, - 'probability_weight': 3.0 - } - } - - # 当前天气状态 - self.current_weather = 'clear' - self.target_weather = 'clear' - self.weather_transition_progress = 0.0 - self.weather_transition_active = False - - # 天气时间配置 - self.weather_duration_min = 300.0 # 最短天气持续时间(秒) - self.weather_duration_max = 3600.0 # 最长天气持续时间(秒) - self.current_weather_time = 0.0 # 当前天气持续时间(秒) - self.target_weather_duration = 0.0 # 目标天气持续时间(秒) - - # 天气变化配置 - self.weather_change_enabled = True - self.weather_change_probability = 0.01 # 每秒天气变化概率 - self.season_influence_enabled = True # 季节对天气的影响 - - # 天气物理参数 - self.base_temperature = 20.0 # 基础温度(摄氏度) - self.base_humidity = 50.0 # 基础湿度(%) - self.base_wind_speed = 3.0 # 基础风速(m/s) - self.base_visibility = 1.0 # 基础能见度(0-1) - - # 统计信息 - self.stats = { - 'weather_changes': 0, - 'transitions_completed': 0, - 'total_weather_time': 0.0 - } - - # 编辑器相关 - self.is_being_edited = False - self.edit_timestamp = 0 - self.edit_history = [] - self.max_history_size = 100 - - print("✓ 天气管理器已创建") - - def initialize(self) -> bool: - """ - 初始化天气管理器 - - Returns: - 是否初始化成功 - """ - try: - # 设置初始天气持续时间 - self.target_weather_duration = random.uniform( - self.weather_duration_min, - self.weather_duration_max - ) - - self.initialized = True - print("✓ 天气管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 天气管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用天气管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 天气管理器未初始化") - return False - - self.enabled = True - print("✓ 天气管理器已启用") - return True - - except Exception as e: - print(f"✗ 天气管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用天气管理器""" - try: - self.enabled = False - print("✓ 天气管理器已禁用") - - except Exception as e: - print(f"✗ 天气管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理天气管理器资源""" - try: - self.disable() - self.initialized = False - print("✓ 天气管理器资源已清理") - - except Exception as e: - print(f"✗ 天气管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新天气管理器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新天气时间 - self._update_weather_time(dt) - - # 更新天气过渡 - if self.weather_transition_active: - self._update_weather_transition(dt) - else: - # 检查是否需要自动变化天气 - self._check_auto_weather_change(dt) - - except Exception as e: - print(f"✗ 天气管理器更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_weather_time(self, dt: float): - """ - 更新天气时间 - - Args: - dt: 时间增量 - """ - try: - self.current_weather_time += dt - self.stats['total_weather_time'] += dt - - # 检查是否达到目标持续时间 - if self.current_weather_time >= self.target_weather_duration and not self.weather_transition_active: - self._change_weather_randomly() - - except Exception as e: - print(f"✗ 天气时间更新失败: {e}") - - def _change_weather_randomly(self): - """随机改变天气""" - try: - # 获取可能的天气类型 - possible_weathers = self._get_possible_weathers() - - if not possible_weathers: - return - - # 根据权重选择天气 - weights = [self.weather_types[w]['probability_weight'] for w in possible_weathers] - target_weather = random.choices(possible_weathers, weights=weights)[0] - - # 设置为目标天气 - self.set_target_weather(target_weather) - - except Exception as e: - print(f"✗ 随机天气变化失败: {e}") - - def _get_possible_weathers(self) -> List[str]: - """ - 获取可能的天气类型(根据季节影响) - - Returns: - 可能的天气类型列表 - """ - try: - # 基本所有天气都可能 - possible_weathers = list(self.weather_types.keys()) - - # 如果启用了季节影响,则根据当前季节调整权重 - if self.season_influence_enabled and self.plugin.season_manager: - current_season = self.plugin.season_manager.get_current_season() - - # 根据季节过滤不太可能的天气 - if current_season == 'summer': - # 夏季不太可能下雪 - if 'snow' in possible_weathers: - possible_weathers.remove('snow') - elif current_season == 'winter': - # 冬季可能下雪 - pass - elif current_season == 'spring': - # 春季可能下雨 - pass - elif current_season == 'autumn': - # 秋季可能下雨和起雾 - pass - - return possible_weathers - - except Exception as e: - print(f"✗ 可能天气获取失败: {e}") - return list(self.weather_types.keys()) - - def _check_auto_weather_change(self, dt: float): - """ - 检查自动天气变化 - - Args: - dt: 时间增量 - """ - try: - if not self.weather_change_enabled: - return - - # 计算变化概率(考虑时间增量) - change_probability = 1 - math.pow(1 - self.weather_change_probability, dt) - - # 检查是否发生天气变化 - if random.random() < change_probability: - self._change_weather_randomly() - - except Exception as e: - print(f"✗ 自动天气变化检查失败: {e}") - - def set_target_weather(self, weather: str): - """ - 设置目标天气 - - Args: - weather: 天气类型 - """ - try: - if weather not in self.weather_types: - print(f"✗ 无效的天气类型: {weather}") - return - - if weather == self.current_weather: - return # 天气未变化 - - self.target_weather = weather - self.start_weather_transition() - - except Exception as e: - print(f"✗ 目标天气设置失败: {e}") - - def start_weather_transition(self): - """开始天气过渡""" - try: - if self.current_weather == self.target_weather: - return - - self.weather_transition_active = True - self.weather_transition_progress = 0.0 - - # 设置新的目标持续时间 - self.target_weather_duration = random.uniform( - self.weather_duration_min, - self.weather_duration_max - ) - self.current_weather_time = 0.0 - - print(f"✓ 开始天气过渡: {self.weather_types[self.current_weather]['name']} -> {self.weather_types[self.target_weather]['name']}") - - except Exception as e: - print(f"✗ 天气过渡启动失败: {e}") - - def _update_weather_transition(self, dt: float): - """ - 更新天气过渡 - - Args: - dt: 时间增量 - """ - try: - if not self.weather_transition_active: - return - - # 获取过渡时间 - transition_time = self.weather_types[self.target_weather]['transition_time'] - - # 计算进度增量 - if transition_time > 0: - progress_delta = dt / transition_time - self.weather_transition_progress += progress_delta - - # 检查过渡是否完成 - if self.weather_transition_progress >= 1.0: - self._complete_weather_transition() - - except Exception as e: - print(f"✗ 天气过渡更新失败: {e}") - - def _complete_weather_transition(self): - """完成天气过渡""" - try: - # 更新当前天气 - self.current_weather = self.target_weather - - # 重置过渡状态 - self.weather_transition_active = False - self.weather_transition_progress = 0.0 - - # 更新统计信息 - self.stats['weather_changes'] += 1 - self.stats['transitions_completed'] += 1 - - print(f"✓ 天气过渡完成: 现在是{self.weather_types[self.current_weather]['name']}") - - # 触发天气变化事件 - self._trigger_weather_change_event() - - except Exception as e: - print(f"✗ 天气过渡完成失败: {e}") - - def _trigger_weather_change_event(self): - """触发天气变化事件""" - try: - # 在实际实现中,这里会触发一个事件通知其他系统天气已变化 - pass - except Exception as e: - print(f"✗ 天气变化事件触发失败: {e}") - - def set_current_weather(self, weather: str): - """ - 立即设置当前天气(无过渡) - - Args: - weather: 天气类型 - """ - try: - if weather not in self.weather_types: - print(f"✗ 无效的天气类型: {weather}") - return - - self.current_weather = weather - self.target_weather = weather - self.weather_transition_active = False - self.weather_transition_progress = 0.0 - - # 设置新的目标持续时间 - self.target_weather_duration = random.uniform( - self.weather_duration_min, - self.weather_duration_max - ) - self.current_weather_time = 0.0 - - print(f"✓ 当前天气设置为: {self.weather_types[weather]['name']}") - - except Exception as e: - print(f"✗ 当前天气设置失败: {e}") - - def get_current_weather(self) -> str: - """ - 获取当前天气 - - Returns: - 当前天气类型 - """ - return self.current_weather - - def get_weather_info(self, weather: str = None) -> Optional[Dict[str, Any]]: - """ - 获取天气信息 - - Args: - weather: 天气类型(如果为None,则返回当前天气信息) - - Returns: - 天气信息字典或None - """ - try: - if weather is None: - weather = self.current_weather - - if weather not in self.weather_types: - return None - - return self.weather_types[weather].copy() - - except Exception as e: - print(f"✗ 天气信息获取失败: {e}") - return None - - def get_interpolated_weather_params(self) -> Dict[str, Any]: - """ - 获取插值后的天气参数(考虑天气过渡) - - Returns: - 插值后的天气参数字典 - """ - try: - if not self.weather_transition_active or self.weather_transition_progress <= 0.0: - # 没有过渡,返回当前天气参数 - return self.weather_types[self.current_weather].copy() - - if self.weather_transition_progress >= 1.0: - # 过渡完成,返回目标天气参数 - return self.weather_types[self.target_weather].copy() - - # 进行插值计算 - current_params = self.weather_types[self.current_weather] - target_params = self.weather_types[self.target_weather] - - interpolated_params = {} - - # 对数值参数进行线性插值 - numeric_params = [ - 'temperature_modifier', 'humidity_modifier', 'wind_speed', - 'visibility', 'precipitation', 'cloud_cover', 'light_intensity' - ] - - for param in numeric_params: - current_value = current_params[param] - target_value = target_params[param] - interpolated_value = current_value + (target_value - current_value) * self.weather_transition_progress - interpolated_params[param] = interpolated_value - - # 复制非数值参数(使用目标天气的参数) - interpolated_params['name'] = f"{current_params['name']} → {target_params['name']}" - interpolated_params['description'] = f"过渡天气 ({self.weather_transition_progress*100:.1f}%)" - interpolated_params['particle_effect'] = target_params['particle_effect'] - interpolated_params['audio_effect'] = target_params['audio_effect'] - interpolated_params['transition_time'] = target_params['transition_time'] - interpolated_params['probability_weight'] = target_params['probability_weight'] - - return interpolated_params - - except Exception as e: - print(f"✗ 插值天气参数获取失败: {e}") - return self.weather_types[self.current_weather].copy() - - def get_weather_transition_progress(self) -> float: - """ - 获取天气过渡进度 - - Returns: - 过渡进度 (0-1) - """ - return self.weather_transition_progress - - def is_transition_active(self) -> bool: - """ - 检查是否正在进行天气过渡 - - Returns: - 是否正在进行天气过渡 - """ - return self.weather_transition_active - - def get_base_environment_params(self) -> Dict[str, float]: - """ - 获取基础环境参数 - - Returns: - 基础环境参数字典 - """ - return { - 'temperature': self.base_temperature, - 'humidity': self.base_humidity, - 'wind_speed': self.base_wind_speed, - 'visibility': self.base_visibility - } - - def set_base_temperature(self, temperature: float): - """ - 设置基础温度 - - Args: - temperature: 温度值(摄氏度) - """ - self.base_temperature = temperature - self.is_being_edited = True - self.edit_timestamp = time.time() - print(f"✓ 基础温度设置为: {temperature}°C") - - def set_base_humidity(self, humidity: float): - """ - 设置基础湿度 - - Args: - humidity: 湿度值(%) - """ - self.base_humidity = max(0.0, min(100.0, humidity)) - self.is_being_edited = True - self.edit_timestamp = time.time() - print(f"✓ 基础湿度设置为: {humidity}%") - - def set_base_wind_speed(self, wind_speed: float): - """ - 设置基础风速 - - Args: - wind_speed: 风速(m/s) - """ - self.base_wind_speed = max(0.0, wind_speed) - self.is_being_edited = True - self.edit_timestamp = time.time() - print(f"✓ 基础风速设置为: {wind_speed} m/s") - - def set_base_visibility(self, visibility: float): - """ - 设置基础能见度 - - Args: - visibility: 能见度(0-1) - """ - self.base_visibility = max(0.0, min(1.0, visibility)) - self.is_being_edited = True - self.edit_timestamp = time.time() - print(f"✓ 基础能见度设置为: {visibility}") - - def _save_edit_history(self): - """保存编辑历史""" - try: - # 创建当前状态快照 - snapshot = { - 'timestamp': time.time(), - 'base_temperature': self.base_temperature, - 'base_humidity': self.base_humidity, - 'base_wind_speed': self.base_wind_speed, - 'base_visibility': self.base_visibility, - 'current_weather': self.current_weather, - 'target_weather': self.target_weather - } - - # 添加到历史记录 - self.edit_history.append(snapshot) - - # 限制历史记录大小 - if len(self.edit_history) > self.max_history_size: - self.edit_history.pop(0) - - except Exception as e: - print(f"✗ 编辑历史保存失败: {e}") - - def undo_edit(self) -> bool: - """ - 撤销编辑 - - Returns: - 是否撤销成功 - """ - try: - if not self.edit_history: - print("✗ 没有可撤销的编辑") - return False - - # 恢复到上一个状态 - last_state = self.edit_history.pop() - - # 恢复参数 - self.base_temperature = last_state['base_temperature'] - self.base_humidity = last_state['base_humidity'] - self.base_wind_speed = last_state['base_wind_speed'] - self.base_visibility = last_state['base_visibility'] - self.current_weather = last_state['current_weather'] - self.target_weather = last_state['target_weather'] - - print("✓ 编辑已撤销") - return True - - except Exception as e: - print(f"✗ 编辑撤销失败: {e}") - import traceback - traceback.print_exc() - return False - - def get_edit_history_size(self) -> int: - """ - 获取编辑历史大小 - - Returns: - 编辑历史记录数量 - """ - return len(self.edit_history) - - def clear_edit_history(self): - """清空编辑历史""" - try: - self.edit_history.clear() - print("✓ 编辑历史已清空") - except Exception as e: - print(f"✗ 编辑历史清空失败: {e}") - - def get_current_state_info(self) -> Dict[str, Any]: - """ - 获取当前状态信息 - - Returns: - 当前状态信息字典 - """ - try: - return { - 'current_weather': self.current_weather, - 'target_weather': self.target_weather, - 'base_temperature': self.base_temperature, - 'base_humidity': self.base_humidity, - 'base_wind_speed': self.base_wind_speed, - 'base_visibility': self.base_visibility, - 'is_being_edited': self.is_being_edited, - 'edit_timestamp': self.edit_timestamp, - 'edit_history_size': len(self.edit_history) - } - except Exception as e: - print(f"✗ 当前状态信息获取失败: {e}") - return {} - - def get_current_environment_params(self) -> Dict[str, float]: - """ - 获取当前环境参数(结合季节和天气影响) - - Returns: - 当前环境参数字典 - """ - try: - # 获取基础参数 - params = self.get_base_environment_params() - - # 应用季节影响 - if self.plugin.season_manager: - season_params = self.plugin.season_manager.get_interpolated_season_params() - params['temperature'] += season_params.get('temperature', 0.0) - params['humidity'] += season_params.get('humidity', 0.0) - - # 应用天气影响 - weather_params = self.get_interpolated_weather_params() - params['temperature'] += weather_params.get('temperature_modifier', 0.0) - params['humidity'] += weather_params.get('humidity_modifier', 0.0) - params['wind_speed'] = weather_params.get('wind_speed', params['wind_speed']) - params['visibility'] = weather_params.get('visibility', params['visibility']) - - return params - - except Exception as e: - print(f"✗ 当前环境参数获取失败: {e}") - return self.get_base_environment_params() - - def get_weather_list(self) -> List[str]: - """ - 获取所有天气类型列表 - - Returns: - 天气类型列表 - """ - return list(self.weather_types.keys()) - - def get_weather_name(self, weather: str) -> str: - """ - 获取天气显示名称 - - Args: - weather: 天气类型 - - Returns: - 天气显示名称 - """ - if weather in self.weather_types: - return self.weather_types[weather]['name'] - return weather - - def enable_weather_change(self, enabled: bool): - """ - 启用/禁用天气变化 - - Args: - enabled: 是否启用天气变化 - """ - self.weather_change_enabled = enabled - print(f"✓ 天气变化{'已启用' if enabled else '已禁用'}") - - def set_weather_change_probability(self, probability: float): - """ - 设置天气变化概率 - - Args: - probability: 每秒天气变化概率 - """ - self.weather_change_probability = max(0.0, min(1.0, probability)) - print(f"✓ 天气变化概率设置为: {probability}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'weather_changes': 0, - 'transitions_completed': 0, - 'total_weather_time': 0.0 - } +""" +天气管理器 +负责管理天气系统和天气变化 +""" + +import time +import random +from typing import Dict, Any, List, Optional +import math + +class WeatherManager: + """ + 天气管理器 + 负责管理天气系统、天气变化和天气过渡效果 + """ + + def __init__(self, plugin): + """ + 初始化天气管理器 + + Args: + plugin: 天气和季节系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 天气类型定义 + self.weather_types = { + 'clear': { + 'name': '晴朗', + 'description': '晴朗无云的天气', + 'temperature_modifier': 0.0, + 'humidity_modifier': -10.0, + 'wind_speed': 2.0, + 'visibility': 1.0, + 'precipitation': 0.0, + 'cloud_cover': 0.1, + 'light_intensity': 1.0, + 'particle_effect': None, + 'audio_effect': None, + 'transition_time': 300.0, # 过渡时间(秒) + 'probability_weight': 30.0 # 概率权重 + }, + 'cloudy': { + 'name': '多云', + 'description': '多云天气,阳光时隐时现', + 'temperature_modifier': -2.0, + 'humidity_modifier': 5.0, + 'wind_speed': 4.0, + 'visibility': 0.9, + 'precipitation': 0.0, + 'cloud_cover': 0.7, + 'light_intensity': 0.7, + 'particle_effect': 'light_fog', + 'audio_effect': 'wind', + 'transition_time': 600.0, + 'probability_weight': 20.0 + }, + 'rain': { + 'name': '下雨', + 'description': '降雨天气', + 'temperature_modifier': -3.0, + 'humidity_modifier': 20.0, + 'wind_speed': 6.0, + 'visibility': 0.7, + 'precipitation': 1.0, + 'cloud_cover': 0.9, + 'light_intensity': 0.5, + 'particle_effect': 'rain', + 'audio_effect': 'rain', + 'transition_time': 120.0, + 'probability_weight': 15.0 + }, + 'storm': { + 'name': '暴风雨', + 'description': '强烈的雷暴天气', + 'temperature_modifier': -5.0, + 'humidity_modifier': 30.0, + 'wind_speed': 15.0, + 'visibility': 0.4, + 'precipitation': 1.0, + 'cloud_cover': 1.0, + 'light_intensity': 0.3, + 'particle_effect': 'storm', + 'audio_effect': 'thunderstorm', + 'transition_time': 90.0, + 'probability_weight': 5.0 + }, + 'snow': { + 'name': '下雪', + 'description': '降雪天气', + 'temperature_modifier': -10.0, + 'humidity_modifier': 15.0, + 'wind_speed': 5.0, + 'visibility': 0.6, + 'precipitation': 0.8, + 'cloud_cover': 0.8, + 'light_intensity': 0.6, + 'particle_effect': 'snow', + 'audio_effect': 'wind', + 'transition_time': 180.0, + 'probability_weight': 8.0 + }, + 'fog': { + 'name': '雾天', + 'description': '大雾天气,能见度低', + 'temperature_modifier': -1.0, + 'humidity_modifier': 25.0, + 'wind_speed': 1.0, + 'visibility': 0.3, + 'precipitation': 0.0, + 'cloud_cover': 0.5, + 'light_intensity': 0.4, + 'particle_effect': 'heavy_fog', + 'audio_effect': 'wind', + 'transition_time': 300.0, + 'probability_weight': 7.0 + }, + 'windy': { + 'name': '大风', + 'description': '强风天气', + 'temperature_modifier': -2.0, + 'humidity_modifier': 0.0, + 'wind_speed': 12.0, + 'visibility': 0.8, + 'precipitation': 0.0, + 'cloud_cover': 0.4, + 'light_intensity': 0.9, + 'particle_effect': 'wind_debris', + 'audio_effect': 'strong_wind', + 'transition_time': 150.0, + 'probability_weight': 10.0 + }, + 'hail': { + 'name': '冰雹', + 'description': '降冰雹天气', + 'temperature_modifier': -4.0, + 'humidity_modifier': 20.0, + 'wind_speed': 8.0, + 'visibility': 0.5, + 'precipitation': 1.0, + 'cloud_cover': 0.9, + 'light_intensity': 0.4, + 'particle_effect': 'hail', + 'audio_effect': 'hail', + 'transition_time': 120.0, + 'probability_weight': 3.0 + } + } + + # 当前天气状态 + self.current_weather = 'clear' + self.target_weather = 'clear' + self.weather_transition_progress = 0.0 + self.weather_transition_active = False + + # 天气时间配置 + self.weather_duration_min = 300.0 # 最短天气持续时间(秒) + self.weather_duration_max = 3600.0 # 最长天气持续时间(秒) + self.current_weather_time = 0.0 # 当前天气持续时间(秒) + self.target_weather_duration = 0.0 # 目标天气持续时间(秒) + + # 天气变化配置 + self.weather_change_enabled = True + self.weather_change_probability = 0.01 # 每秒天气变化概率 + self.season_influence_enabled = True # 季节对天气的影响 + + # 天气物理参数 + self.base_temperature = 20.0 # 基础温度(摄氏度) + self.base_humidity = 50.0 # 基础湿度(%) + self.base_wind_speed = 3.0 # 基础风速(m/s) + self.base_visibility = 1.0 # 基础能见度(0-1) + + # 统计信息 + self.stats = { + 'weather_changes': 0, + 'transitions_completed': 0, + 'total_weather_time': 0.0 + } + + # 编辑器相关 + self.is_being_edited = False + self.edit_timestamp = 0 + self.edit_history = [] + self.max_history_size = 100 + + print("✓ 天气管理器已创建") + + def initialize(self) -> bool: + """ + 初始化天气管理器 + + Returns: + 是否初始化成功 + """ + try: + # 设置初始天气持续时间 + self.target_weather_duration = random.uniform( + self.weather_duration_min, + self.weather_duration_max + ) + + self.initialized = True + print("✓ 天气管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 天气管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用天气管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 天气管理器未初始化") + return False + + self.enabled = True + print("✓ 天气管理器已启用") + return True + + except Exception as e: + print(f"✗ 天气管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用天气管理器""" + try: + self.enabled = False + print("✓ 天气管理器已禁用") + + except Exception as e: + print(f"✗ 天气管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理天气管理器资源""" + try: + self.disable() + self.initialized = False + print("✓ 天气管理器资源已清理") + + except Exception as e: + print(f"✗ 天气管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新天气管理器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新天气时间 + self._update_weather_time(dt) + + # 更新天气过渡 + if self.weather_transition_active: + self._update_weather_transition(dt) + else: + # 检查是否需要自动变化天气 + self._check_auto_weather_change(dt) + + except Exception as e: + print(f"✗ 天气管理器更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_weather_time(self, dt: float): + """ + 更新天气时间 + + Args: + dt: 时间增量 + """ + try: + self.current_weather_time += dt + self.stats['total_weather_time'] += dt + + # 检查是否达到目标持续时间 + if self.current_weather_time >= self.target_weather_duration and not self.weather_transition_active: + self._change_weather_randomly() + + except Exception as e: + print(f"✗ 天气时间更新失败: {e}") + + def _change_weather_randomly(self): + """随机改变天气""" + try: + # 获取可能的天气类型 + possible_weathers = self._get_possible_weathers() + + if not possible_weathers: + return + + # 根据权重选择天气 + weights = [self.weather_types[w]['probability_weight'] for w in possible_weathers] + target_weather = random.choices(possible_weathers, weights=weights)[0] + + # 设置为目标天气 + self.set_target_weather(target_weather) + + except Exception as e: + print(f"✗ 随机天气变化失败: {e}") + + def _get_possible_weathers(self) -> List[str]: + """ + 获取可能的天气类型(根据季节影响) + + Returns: + 可能的天气类型列表 + """ + try: + # 基本所有天气都可能 + possible_weathers = list(self.weather_types.keys()) + + # 如果启用了季节影响,则根据当前季节调整权重 + if self.season_influence_enabled and self.plugin.season_manager: + current_season = self.plugin.season_manager.get_current_season() + + # 根据季节过滤不太可能的天气 + if current_season == 'summer': + # 夏季不太可能下雪 + if 'snow' in possible_weathers: + possible_weathers.remove('snow') + elif current_season == 'winter': + # 冬季可能下雪 + pass + elif current_season == 'spring': + # 春季可能下雨 + pass + elif current_season == 'autumn': + # 秋季可能下雨和起雾 + pass + + return possible_weathers + + except Exception as e: + print(f"✗ 可能天气获取失败: {e}") + return list(self.weather_types.keys()) + + def _check_auto_weather_change(self, dt: float): + """ + 检查自动天气变化 + + Args: + dt: 时间增量 + """ + try: + if not self.weather_change_enabled: + return + + # 计算变化概率(考虑时间增量) + change_probability = 1 - math.pow(1 - self.weather_change_probability, dt) + + # 检查是否发生天气变化 + if random.random() < change_probability: + self._change_weather_randomly() + + except Exception as e: + print(f"✗ 自动天气变化检查失败: {e}") + + def set_target_weather(self, weather: str): + """ + 设置目标天气 + + Args: + weather: 天气类型 + """ + try: + if weather not in self.weather_types: + print(f"✗ 无效的天气类型: {weather}") + return + + if weather == self.current_weather: + return # 天气未变化 + + self.target_weather = weather + self.start_weather_transition() + + except Exception as e: + print(f"✗ 目标天气设置失败: {e}") + + def start_weather_transition(self): + """开始天气过渡""" + try: + if self.current_weather == self.target_weather: + return + + self.weather_transition_active = True + self.weather_transition_progress = 0.0 + + # 设置新的目标持续时间 + self.target_weather_duration = random.uniform( + self.weather_duration_min, + self.weather_duration_max + ) + self.current_weather_time = 0.0 + + print(f"✓ 开始天气过渡: {self.weather_types[self.current_weather]['name']} -> {self.weather_types[self.target_weather]['name']}") + + except Exception as e: + print(f"✗ 天气过渡启动失败: {e}") + + def _update_weather_transition(self, dt: float): + """ + 更新天气过渡 + + Args: + dt: 时间增量 + """ + try: + if not self.weather_transition_active: + return + + # 获取过渡时间 + transition_time = self.weather_types[self.target_weather]['transition_time'] + + # 计算进度增量 + if transition_time > 0: + progress_delta = dt / transition_time + self.weather_transition_progress += progress_delta + + # 检查过渡是否完成 + if self.weather_transition_progress >= 1.0: + self._complete_weather_transition() + + except Exception as e: + print(f"✗ 天气过渡更新失败: {e}") + + def _complete_weather_transition(self): + """完成天气过渡""" + try: + # 更新当前天气 + self.current_weather = self.target_weather + + # 重置过渡状态 + self.weather_transition_active = False + self.weather_transition_progress = 0.0 + + # 更新统计信息 + self.stats['weather_changes'] += 1 + self.stats['transitions_completed'] += 1 + + print(f"✓ 天气过渡完成: 现在是{self.weather_types[self.current_weather]['name']}") + + # 触发天气变化事件 + self._trigger_weather_change_event() + + except Exception as e: + print(f"✗ 天气过渡完成失败: {e}") + + def _trigger_weather_change_event(self): + """触发天气变化事件""" + try: + # 在实际实现中,这里会触发一个事件通知其他系统天气已变化 + pass + except Exception as e: + print(f"✗ 天气变化事件触发失败: {e}") + + def set_current_weather(self, weather: str): + """ + 立即设置当前天气(无过渡) + + Args: + weather: 天气类型 + """ + try: + if weather not in self.weather_types: + print(f"✗ 无效的天气类型: {weather}") + return + + self.current_weather = weather + self.target_weather = weather + self.weather_transition_active = False + self.weather_transition_progress = 0.0 + + # 设置新的目标持续时间 + self.target_weather_duration = random.uniform( + self.weather_duration_min, + self.weather_duration_max + ) + self.current_weather_time = 0.0 + + print(f"✓ 当前天气设置为: {self.weather_types[weather]['name']}") + + except Exception as e: + print(f"✗ 当前天气设置失败: {e}") + + def get_current_weather(self) -> str: + """ + 获取当前天气 + + Returns: + 当前天气类型 + """ + return self.current_weather + + def get_weather_info(self, weather: str = None) -> Optional[Dict[str, Any]]: + """ + 获取天气信息 + + Args: + weather: 天气类型(如果为None,则返回当前天气信息) + + Returns: + 天气信息字典或None + """ + try: + if weather is None: + weather = self.current_weather + + if weather not in self.weather_types: + return None + + return self.weather_types[weather].copy() + + except Exception as e: + print(f"✗ 天气信息获取失败: {e}") + return None + + def get_interpolated_weather_params(self) -> Dict[str, Any]: + """ + 获取插值后的天气参数(考虑天气过渡) + + Returns: + 插值后的天气参数字典 + """ + try: + if not self.weather_transition_active or self.weather_transition_progress <= 0.0: + # 没有过渡,返回当前天气参数 + return self.weather_types[self.current_weather].copy() + + if self.weather_transition_progress >= 1.0: + # 过渡完成,返回目标天气参数 + return self.weather_types[self.target_weather].copy() + + # 进行插值计算 + current_params = self.weather_types[self.current_weather] + target_params = self.weather_types[self.target_weather] + + interpolated_params = {} + + # 对数值参数进行线性插值 + numeric_params = [ + 'temperature_modifier', 'humidity_modifier', 'wind_speed', + 'visibility', 'precipitation', 'cloud_cover', 'light_intensity' + ] + + for param in numeric_params: + current_value = current_params[param] + target_value = target_params[param] + interpolated_value = current_value + (target_value - current_value) * self.weather_transition_progress + interpolated_params[param] = interpolated_value + + # 复制非数值参数(使用目标天气的参数) + interpolated_params['name'] = f"{current_params['name']} → {target_params['name']}" + interpolated_params['description'] = f"过渡天气 ({self.weather_transition_progress*100:.1f}%)" + interpolated_params['particle_effect'] = target_params['particle_effect'] + interpolated_params['audio_effect'] = target_params['audio_effect'] + interpolated_params['transition_time'] = target_params['transition_time'] + interpolated_params['probability_weight'] = target_params['probability_weight'] + + return interpolated_params + + except Exception as e: + print(f"✗ 插值天气参数获取失败: {e}") + return self.weather_types[self.current_weather].copy() + + def get_weather_transition_progress(self) -> float: + """ + 获取天气过渡进度 + + Returns: + 过渡进度 (0-1) + """ + return self.weather_transition_progress + + def is_transition_active(self) -> bool: + """ + 检查是否正在进行天气过渡 + + Returns: + 是否正在进行天气过渡 + """ + return self.weather_transition_active + + def get_base_environment_params(self) -> Dict[str, float]: + """ + 获取基础环境参数 + + Returns: + 基础环境参数字典 + """ + return { + 'temperature': self.base_temperature, + 'humidity': self.base_humidity, + 'wind_speed': self.base_wind_speed, + 'visibility': self.base_visibility + } + + def set_base_temperature(self, temperature: float): + """ + 设置基础温度 + + Args: + temperature: 温度值(摄氏度) + """ + self.base_temperature = temperature + self.is_being_edited = True + self.edit_timestamp = time.time() + print(f"✓ 基础温度设置为: {temperature}°C") + + def set_base_humidity(self, humidity: float): + """ + 设置基础湿度 + + Args: + humidity: 湿度值(%) + """ + self.base_humidity = max(0.0, min(100.0, humidity)) + self.is_being_edited = True + self.edit_timestamp = time.time() + print(f"✓ 基础湿度设置为: {humidity}%") + + def set_base_wind_speed(self, wind_speed: float): + """ + 设置基础风速 + + Args: + wind_speed: 风速(m/s) + """ + self.base_wind_speed = max(0.0, wind_speed) + self.is_being_edited = True + self.edit_timestamp = time.time() + print(f"✓ 基础风速设置为: {wind_speed} m/s") + + def set_base_visibility(self, visibility: float): + """ + 设置基础能见度 + + Args: + visibility: 能见度(0-1) + """ + self.base_visibility = max(0.0, min(1.0, visibility)) + self.is_being_edited = True + self.edit_timestamp = time.time() + print(f"✓ 基础能见度设置为: {visibility}") + + def _save_edit_history(self): + """保存编辑历史""" + try: + # 创建当前状态快照 + snapshot = { + 'timestamp': time.time(), + 'base_temperature': self.base_temperature, + 'base_humidity': self.base_humidity, + 'base_wind_speed': self.base_wind_speed, + 'base_visibility': self.base_visibility, + 'current_weather': self.current_weather, + 'target_weather': self.target_weather + } + + # 添加到历史记录 + self.edit_history.append(snapshot) + + # 限制历史记录大小 + if len(self.edit_history) > self.max_history_size: + self.edit_history.pop(0) + + except Exception as e: + print(f"✗ 编辑历史保存失败: {e}") + + def undo_edit(self) -> bool: + """ + 撤销编辑 + + Returns: + 是否撤销成功 + """ + try: + if not self.edit_history: + print("✗ 没有可撤销的编辑") + return False + + # 恢复到上一个状态 + last_state = self.edit_history.pop() + + # 恢复参数 + self.base_temperature = last_state['base_temperature'] + self.base_humidity = last_state['base_humidity'] + self.base_wind_speed = last_state['base_wind_speed'] + self.base_visibility = last_state['base_visibility'] + self.current_weather = last_state['current_weather'] + self.target_weather = last_state['target_weather'] + + print("✓ 编辑已撤销") + return True + + except Exception as e: + print(f"✗ 编辑撤销失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_edit_history_size(self) -> int: + """ + 获取编辑历史大小 + + Returns: + 编辑历史记录数量 + """ + return len(self.edit_history) + + def clear_edit_history(self): + """清空编辑历史""" + try: + self.edit_history.clear() + print("✓ 编辑历史已清空") + except Exception as e: + print(f"✗ 编辑历史清空失败: {e}") + + def get_current_state_info(self) -> Dict[str, Any]: + """ + 获取当前状态信息 + + Returns: + 当前状态信息字典 + """ + try: + return { + 'current_weather': self.current_weather, + 'target_weather': self.target_weather, + 'base_temperature': self.base_temperature, + 'base_humidity': self.base_humidity, + 'base_wind_speed': self.base_wind_speed, + 'base_visibility': self.base_visibility, + 'is_being_edited': self.is_being_edited, + 'edit_timestamp': self.edit_timestamp, + 'edit_history_size': len(self.edit_history) + } + except Exception as e: + print(f"✗ 当前状态信息获取失败: {e}") + return {} + + def get_current_environment_params(self) -> Dict[str, float]: + """ + 获取当前环境参数(结合季节和天气影响) + + Returns: + 当前环境参数字典 + """ + try: + # 获取基础参数 + params = self.get_base_environment_params() + + # 应用季节影响 + if self.plugin.season_manager: + season_params = self.plugin.season_manager.get_interpolated_season_params() + params['temperature'] += season_params.get('temperature', 0.0) + params['humidity'] += season_params.get('humidity', 0.0) + + # 应用天气影响 + weather_params = self.get_interpolated_weather_params() + params['temperature'] += weather_params.get('temperature_modifier', 0.0) + params['humidity'] += weather_params.get('humidity_modifier', 0.0) + params['wind_speed'] = weather_params.get('wind_speed', params['wind_speed']) + params['visibility'] = weather_params.get('visibility', params['visibility']) + + return params + + except Exception as e: + print(f"✗ 当前环境参数获取失败: {e}") + return self.get_base_environment_params() + + def get_weather_list(self) -> List[str]: + """ + 获取所有天气类型列表 + + Returns: + 天气类型列表 + """ + return list(self.weather_types.keys()) + + def get_weather_name(self, weather: str) -> str: + """ + 获取天气显示名称 + + Args: + weather: 天气类型 + + Returns: + 天气显示名称 + """ + if weather in self.weather_types: + return self.weather_types[weather]['name'] + return weather + + def enable_weather_change(self, enabled: bool): + """ + 启用/禁用天气变化 + + Args: + enabled: 是否启用天气变化 + """ + self.weather_change_enabled = enabled + print(f"✓ 天气变化{'已启用' if enabled else '已禁用'}") + + def set_weather_change_probability(self, probability: float): + """ + 设置天气变化概率 + + Args: + probability: 每秒天气变化概率 + """ + self.weather_change_probability = max(0.0, min(1.0, probability)) + print(f"✓ 天气变化概率设置为: {probability}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'weather_changes': 0, + 'transitions_completed': 0, + 'total_weather_time': 0.0 + } print("✓ 天气管理器统计信息已重置") \ No newline at end of file diff --git a/plugins/user/weather_season_system/editor/preset_manager.py b/plugins/user/weather_season_system/editor/preset_manager.py index 8716254f..fbc6b0af 100644 --- a/plugins/user/weather_season_system/editor/preset_manager.py +++ b/plugins/user/weather_season_system/editor/preset_manager.py @@ -1,1095 +1,1095 @@ -""" -预设管理器 -管理天气和季节系统的预设配置 -""" - -import json -import os -from typing import Dict, Any, List, Optional -import time - -class PresetManager: - """ - 预设管理器 - 管理天气和季节系统的预设配置,包括创建、保存、加载和应用预设 - """ - - def __init__(self, plugin): - """ - 初始化预设管理器 - - Args: - plugin: 天气和季节系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 预设配置 - self.preset_config = { - 'presets_directory': 'presets/weather', - 'default_preset': 'default', - 'auto_save_enabled': True, - 'max_presets': 100 - } - - # 预设数据 - self.presets = {} - self.current_preset = None - - # 预设类别 - self.preset_categories = { - 'weather': {'name': '天气', 'description': '天气配置预设'}, - 'season': {'name': '季节', 'description': '季节配置预设'}, - 'environment': {'name': '环境', 'description': '环境效果预设'}, - 'particles': {'name': '粒子', 'description': '粒子效果预设'}, - 'audio': {'name': '音频', 'description': '音频效果预设'}, - 'mixed': {'name': '混合', 'description': '混合配置预设'} - } - - # 统计信息 - self.stats = { - 'presets_loaded': 0, - 'presets_saved': 0, - 'presets_applied': 0, - 'total_load_time': 0.0, - 'total_save_time': 0.0 - } - - print("✓ 预设管理器已创建") - - def initialize(self) -> bool: - """ - 初始化预设管理器 - - Returns: - 是否初始化成功 - """ - try: - # 创建预设目录 - presets_dir = self.preset_config['presets_directory'] - if not os.path.exists(presets_dir): - os.makedirs(presets_dir) - - # 加载默认预设 - self._load_default_presets() - - # 加载用户预设 - self._load_user_presets() - - self.initialized = True - print("✓ 预设管理器初始化完成") - return True - - except Exception as e: - print(f"✗ 预设管理器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用预设管理器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 预设管理器未初始化") - return False - - self.enabled = True - print("✓ 预设管理器已启用") - return True - - except Exception as e: - print(f"✗ 预设管理器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用预设管理器""" - try: - self.enabled = False - print("✓ 预设管理器已禁用") - - except Exception as e: - print(f"✗ 预设管理器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理预设管理器资源""" - try: - self.disable() - - # 自动保存当前预设 - if self.preset_config['auto_save_enabled'] and self.current_preset: - self.save_preset(self.current_preset) - - self.initialized = False - print("✓ 预设管理器资源已清理") - - except Exception as e: - print(f"✗ 预设管理器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新预设管理器状态 - - Args: - dt: 时间增量 - """ - # 处理更新逻辑 - pass - - def _load_default_presets(self): - """加载默认预设""" - try: - # 创建默认天气预设 - self.presets['default_weather'] = { - 'name': '默认天气', - 'category': 'weather', - 'description': '默认的天气配置参数', - 'parameters': { - 'temperature': 20.0, - 'humidity': 50.0, - 'wind_speed': 3.0, - 'visibility': 1.0 - }, - 'timestamp': time.time(), - 'is_default': True - } - - # 创建默认季节预设 - self.presets['default_season'] = { - 'name': '默认季节', - 'category': 'season', - 'description': '默认的季节配置参数', - 'parameters': { - 'season_duration': 90, - 'transition_duration': 7.0, - 'temperature_influence': 15.0 - }, - 'timestamp': time.time(), - 'is_default': True - } - - # 创建默认环境预设 - self.presets['default_environment'] = { - 'name': '默认环境', - 'category': 'environment', - 'description': '默认的环境效果参数', - 'parameters': { - 'light_intensity': 1.0, - 'fog_density': 0.0, - 'wind_turbulence': 0.1 - }, - 'timestamp': time.time(), - 'is_default': True - } - - # 创建默认粒子预设 - self.presets['default_particles'] = { - 'name': '默认粒子', - 'category': 'particles', - 'description': '默认的粒子效果参数', - 'parameters': { - 'particle_count': 1000, - 'particle_size': 1.0, - 'emission_rate': 100 - }, - 'timestamp': time.time(), - 'is_default': True - } - - # 创建默认音频预设 - self.presets['default_audio'] = { - 'name': '默认音频', - 'category': 'audio', - 'description': '默认的音频效果参数', - 'parameters': { - 'master_volume': 1.0, - 'effects_volume': 1.0, - 'ambient_volume': 1.0 - }, - 'timestamp': time.time(), - 'is_default': True - } - - # 创建默认混合预设 - self.presets['default_mixed'] = { - 'name': '默认混合', - 'category': 'mixed', - 'description': '默认的混合配置参数', - 'parameters': { - 'weather': 'clear', - 'season': 'spring', - 'time_of_day': 12.0 - }, - 'timestamp': time.time(), - 'is_default': True - } - - print("✓ 默认预设加载完成") - - except Exception as e: - print(f"✗ 默认预设加载失败: {e}") - import traceback - traceback.print_exc() - - def _load_user_presets(self): - """加载用户预设""" - try: - load_start_time = time.time() - - presets_dir = self.preset_config['presets_directory'] - if not os.path.exists(presets_dir): - print("✓ 用户预设目录不存在,跳过加载") - return - - # 遍历预设目录 - for filename in os.listdir(presets_dir): - if filename.endswith('.json'): - try: - filepath = os.path.join(presets_dir, filename) - with open(filepath, 'r', encoding='utf-8') as f: - preset_data = json.load(f) - - # 使用文件名(不含扩展名)作为预设ID - preset_id = os.path.splitext(filename)[0] - self.presets[preset_id] = preset_data - - except Exception as e: - print(f"✗ 预设文件加载失败 {filename}: {e}") - - load_time = time.time() - load_start_time - self.stats['presets_loaded'] = len(self.presets) - self.stats['total_load_time'] = load_time - - print(f"✓ 用户预设加载完成,共加载 {len(self.presets)} 个预设,耗时: {load_time:.3f}秒") - - except Exception as e: - print(f"✗ 用户预设加载失败: {e}") - import traceback - traceback.print_exc() - - def save_preset(self, preset_id: str, preset_data: Dict[str, Any] = None) -> bool: - """ - 保存预设 - - Args: - preset_id: 预设ID - preset_data: 预设数据(如果为None,则保存当前配置) - - Returns: - 是否保存成功 - """ - try: - save_start_time = time.time() - - if not self.enabled: - print("✗ 预设管理器未启用") - return False - - # 如果未提供预设数据,则创建当前配置的快照 - if preset_data is None: - preset_data = self._create_current_preset_snapshot(preset_id) - - # 保存预设数据 - self.presets[preset_id] = preset_data - - # 保存到文件 - presets_dir = self.preset_config['presets_directory'] - filepath = os.path.join(presets_dir, f"{preset_id}.json") - - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(preset_data, f, ensure_ascii=False, indent=2) - - # 更新统计信息 - save_time = time.time() - save_start_time - self.stats['presets_saved'] += 1 - self.stats['total_save_time'] += save_time - - print(f"✓ 预设 '{preset_id}' 保存完成,耗时: {save_time:.3f}秒") - return True - - except Exception as e: - print(f"✗ 预设保存失败: {e}") - import traceback - traceback.print_exc() - return False - - def _create_current_preset_snapshot(self, preset_id: str) -> Dict[str, Any]: - """ - 创建当前配置的预设快照 - - Args: - preset_id: 预设ID - - Returns: - 预设数据字典 - """ - try: - # 获取当前插件配置 - preset_data = { - 'name': preset_id, - 'category': 'mixed', # 默认类别 - 'description': '用户自定义预设', - 'parameters': {}, - 'timestamp': time.time(), - 'is_default': False - } - - # 添加天气管理器配置 - if self.plugin.weather_manager: - wm = self.plugin.weather_manager - preset_data['parameters'].update({ - 'base_temperature': wm.base_temperature, - 'base_humidity': wm.base_humidity, - 'base_wind_speed': wm.base_wind_speed, - 'base_visibility': wm.base_visibility - }) - - # 添加季节管理器配置 - if self.plugin.season_manager: - sm = self.plugin.season_manager - preset_data['parameters'].update({ - 'season_duration': sm.season_duration, - 'current_season': sm.current_season, - 'season_cycle_enabled': sm.season_cycle_enabled - }) - - # 添加环境效果配置 - if self.plugin.environment_effects: - ee = self.plugin.environment_effects - preset_data['parameters'].update({ - 'time_of_day': ee.time_config['day_time'], - 'day_speed': ee.time_config['day_speed'] - }) - - # 添加音频管理器配置 - if self.plugin.audio_manager: - am = self.plugin.audio_manager - audio_params = am.get_audio_parameters() - preset_data['parameters'].update(audio_params) - - return preset_data - - except Exception as e: - print(f"✗ 预设快照创建失败: {e}") - import traceback - traceback.print_exc() - return { - 'name': preset_id, - 'category': 'mixed', - 'description': '默认预设', - 'parameters': {}, - 'timestamp': time.time(), - 'is_default': False - } - - def load_preset(self, preset_id: str) -> Optional[Dict[str, Any]]: - """ - 加载预设 - - Args: - preset_id: 预设ID - - Returns: - 预设数据字典或None - """ - try: - if not self.enabled: - print("✗ 预设管理器未启用") - return None - - if preset_id not in self.presets: - print(f"✗ 预设不存在: {preset_id}") - return None - - preset_data = self.presets[preset_id] - - # 更新统计信息 - self.stats['presets_applied'] += 1 - self.current_preset = preset_id - - print(f"✓ 预设 '{preset_id}' 加载完成") - return preset_data - - except Exception as e: - print(f"✗ 预设加载失败: {e}") - import traceback - traceback.print_exc() - return None - - def apply_preset(self, preset_id: str) -> bool: - """ - 应用预设到当前配置 - - Args: - preset_id: 预设ID - - Returns: - 是否应用成功 - """ - try: - if not self.enabled: - print("✗ 预设管理器未启用") - return False - - preset_data = self.load_preset(preset_id) - if preset_data is None: - return False - - parameters = preset_data.get('parameters', {}) - - # 应用参数到各个组件 - self._apply_parameters_to_components(parameters) - - # 更新统计信息 - self.stats['presets_applied'] += 1 - self.current_preset = preset_id - - print(f"✓ 预设 '{preset_id}' 应用完成") - return True - - except Exception as e: - print(f"✗ 预设应用失败: {e}") - import traceback - traceback.print_exc() - return False - - def _apply_parameters_to_components(self, parameters: Dict[str, Any]): - """ - 应用参数到各个组件 - - Args: - parameters: 参数字典 - """ - try: - # 应用到天气管理器 - if self.plugin.weather_manager: - wm = self.plugin.weather_manager - if 'base_temperature' in parameters: - wm.set_base_temperature(parameters['base_temperature']) - if 'base_humidity' in parameters: - wm.set_base_humidity(parameters['base_humidity']) - if 'base_wind_speed' in parameters: - wm.set_base_wind_speed(parameters['base_wind_speed']) - if 'base_visibility' in parameters: - wm.set_base_visibility(parameters['base_visibility']) - - # 应用到季节管理器 - if self.plugin.season_manager: - sm = self.plugin.season_manager - if 'season_duration' in parameters: - sm.set_season_duration(parameters['season_duration']) - if 'current_season' in parameters: - sm.set_current_season(parameters['current_season']) - if 'season_cycle_enabled' in parameters: - sm.enable_season_cycle(parameters['season_cycle_enabled']) - - # 应用到环境效果 - if self.plugin.environment_effects: - ee = self.plugin.environment_effects - if 'time_of_day' in parameters: - ee.set_time(parameters['time_of_day']) - if 'day_speed' in parameters: - ee.set_day_speed(parameters['day_speed']) - - # 应用到音频管理器 - if self.plugin.audio_manager: - am = self.plugin.audio_manager - for param_name, param_value in parameters.items(): - if param_name.startswith(('master_', 'effects_', 'ambient_')): - am.set_audio_parameter(param_name, param_value) - - except Exception as e: - print(f"✗ 参数应用失败: {e}") - import traceback - traceback.print_exc() - - def delete_preset(self, preset_id: str) -> bool: - """ - 删除预设 - - Args: - preset_id: 预设ID - - Returns: - 是否删除成功 - """ - try: - if not self.enabled: - print("✗ 预设管理器未启用") - return False - - if preset_id not in self.presets: - print(f"✗ 预设不存在: {preset_id}") - return False - - # 检查是否为默认预设 - if self.presets[preset_id].get('is_default', False): - print(f"✗ 无法删除默认预设: {preset_id}") - return False - - # 从内存中删除 - del self.presets[preset_id] - - # 从文件系统中删除 - presets_dir = self.preset_config['presets_directory'] - filepath = os.path.join(presets_dir, f"{preset_id}.json") - if os.path.exists(filepath): - os.remove(filepath) - - # 如果删除的是当前预设,清空当前预设 - if self.current_preset == preset_id: - self.current_preset = None - - print(f"✓ 预设 '{preset_id}' 删除完成") - return True - - except Exception as e: - print(f"✗ 预设删除失败: {e}") - import traceback - traceback.print_exc() - return False - - def list_presets(self, category: str = None) -> List[str]: - """ - 列出预设 - - Args: - category: 预设类别(如果为None,则列出所有预设) - - Returns: - 预设ID列表 - """ - try: - if category is None: - return list(self.presets.keys()) - else: - return [preset_id for preset_id, preset_data in self.presets.items() - if preset_data.get('category') == category] - - except Exception as e: - print(f"✗ 预设列表获取失败: {e}") - import traceback - traceback.print_exc() - return [] - - def get_preset_info(self, preset_id: str) -> Optional[Dict[str, Any]]: - """ - 获取预设信息 - - Args: - preset_id: 预设ID - - Returns: - 预设信息字典或None - """ - try: - if preset_id not in self.presets: - return None - - preset_data = self.presets[preset_id] - return { - 'name': preset_data.get('name', preset_id), - 'category': preset_data.get('category', 'unknown'), - 'description': preset_data.get('description', ''), - 'timestamp': preset_data.get('timestamp', 0), - 'is_default': preset_data.get('is_default', False), - 'parameters_count': len(preset_data.get('parameters', {})) - } - - except Exception as e: - print(f"✗ 预设信息获取失败: {e}") - import traceback - traceback.print_exc() - return None - - def get_preset_categories(self) -> Dict[str, Dict[str, str]]: - """ - 获取预设类别 - - Returns: - 预设类别字典 - """ - return self.preset_categories.copy() - - def create_preset_category(self, category_id: str, name: str, description: str = '') -> bool: - """ - 创建预设类别 - - Args: - category_id: 类别ID - name: 类别名称 - description: 类别描述 - - Returns: - 是否创建成功 - """ - try: - self.preset_categories[category_id] = { - 'name': name, - 'description': description - } - - print(f"✓ 预设类别 '{name}' 创建完成") - return True - - except Exception as e: - print(f"✗ 预设类别创建失败: {e}") - import traceback - traceback.print_exc() - return False - - def export_preset(self, preset_id: str, filepath: str) -> bool: - """ - 导出预设到文件 - - Args: - preset_id: 预设ID - filepath: 导出文件路径 - - Returns: - 是否导出成功 - """ - try: - if preset_id not in self.presets: - print(f"✗ 预设不存在: {preset_id}") - return False - - preset_data = self.presets[preset_id] - - # 保存到指定文件 - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(preset_data, f, ensure_ascii=False, indent=2) - - print(f"✓ 预设 '{preset_id}' 导出到 {filepath}") - return True - - except Exception as e: - print(f"✗ 预设导出失败: {e}") - import traceback - traceback.print_exc() - return False - - def import_preset(self, filepath: str, preset_id: str = None) -> bool: - """ - 从文件导入预设 - - Args: - filepath: 导入文件路径 - preset_id: 预设ID(如果为None,则使用文件名) - - Returns: - 是否导入成功 - """ - try: - # 读取预设文件 - with open(filepath, 'r', encoding='utf-8') as f: - preset_data = json.load(f) - - # 确定预设ID - if preset_id is None: - preset_id = os.path.splitext(os.path.basename(filepath))[0] - - # 保存预设 - self.presets[preset_id] = preset_data - - # 保存到预设目录 - presets_dir = self.preset_config['presets_directory'] - save_filepath = os.path.join(presets_dir, f"{preset_id}.json") - - with open(save_filepath, 'w', encoding='utf-8') as f: - json.dump(preset_data, f, ensure_ascii=False, indent=2) - - print(f"✓ 预设从 {filepath} 导入完成,ID: {preset_id}") - return True - - except Exception as e: - print(f"✗ 预设导入失败: {e}") - import traceback - traceback.print_exc() - return False - - def set_preset_config(self, config: Dict[str, Any]): - """ - 设置预设配置 - - Args: - config: 配置字典 - """ - self.preset_config.update(config) - print(f"✓ 预设配置已更新: {self.preset_config}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'presets_loaded': 0, - 'presets_saved': 0, - 'presets_applied': 0, - 'total_load_time': 0.0, - 'total_save_time': 0.0 - } - print("✓ 预设管理器统计信息已重置") - - def get_current_preset(self) -> Optional[str]: - """ - 获取当前预设 - - Returns: - 当前预设ID或None - """ - return self.current_preset - - def set_current_preset(self, preset_id: str): - """ - 设置当前预设 - - Args: - preset_id: 预设ID - """ - if preset_id in self.presets: - self.current_preset = preset_id - print(f"✓ 当前预设设置为: {preset_id}") - else: - print(f"✗ 预设不存在: {preset_id}") - - def create_terrain_preset_from_current(self, preset_name: str, description: str = '') -> str: - """ - 从当前配置创建预设 - - Args: - preset_name: 预设名称 - description: 预设描述 - - Returns: - 预设ID - """ - try: - # 生成预设ID - preset_id = preset_name.lower().replace(' ', '_') - - # 创建预设数据 - preset_data = { - 'name': preset_name, - 'category': 'mixed', - 'description': description, - 'parameters': {}, - 'timestamp': time.time(), - 'is_default': False - } - - # 获取当前配置参数 - if self.plugin.weather_manager: - wm = self.plugin.weather_manager - preset_data['parameters'].update({ - 'base_temperature': wm.base_temperature, - 'base_humidity': wm.base_humidity, - 'base_wind_speed': wm.base_wind_speed, - 'base_visibility': wm.base_visibility - }) - - if self.plugin.season_manager: - sm = self.plugin.season_manager - preset_data['parameters'].update({ - 'season_duration': sm.season_duration, - 'current_season': sm.current_season - }) - - # 保存预设 - self.save_preset(preset_id, preset_data) - - print(f"✓ 从当前配置创建预设: {preset_name}") - return preset_id - - except Exception as e: - print(f"✗ 从当前配置创建预设失败: {e}") - import traceback - traceback.print_exc() - return '' - - def batch_apply_presets(self, preset_ids: List[str]) -> bool: - """ - 批量应用预设 - - Args: - preset_ids: 预设ID列表 - - Returns: - 是否应用成功 - """ - try: - if not self.enabled: - print("✗ 预设管理器未启用") - return False - - success_count = 0 - for preset_id in preset_ids: - if self.apply_preset(preset_id): - success_count += 1 - - print(f"✓ 批量应用预设完成: {success_count}/{len(preset_ids)} 个预设应用成功") - return success_count == len(preset_ids) - - except Exception as e: - print(f"✗ 批量应用预设失败: {e}") - import traceback - traceback.print_exc() - return False - - def search_presets(self, keyword: str, category: str = None) -> List[str]: - """ - 搜索预设 - - Args: - keyword: 搜索关键词 - category: 预设类别(可选) - - Returns: - 匹配的预设ID列表 - """ - try: - matching_presets = [] - - for preset_id, preset_data in self.presets.items(): - # 检查类别筛选 - if category and preset_data.get('category') != category: - continue - - # 检查关键词匹配 - if (keyword.lower() in preset_data.get('name', '').lower() or - keyword.lower() in preset_data.get('description', '').lower()): - matching_presets.append(preset_id) - - return matching_presets - - except Exception as e: - print(f"✗ 预设搜索失败: {e}") - import traceback - traceback.print_exc() - return [] - - def duplicate_preset(self, source_preset_id: str, new_preset_id: str, new_name: str = None) -> bool: - """ - 复制预设 - - Args: - source_preset_id: 源预设ID - new_preset_id: 新预设ID - new_name: 新预设名称(如果为None,则使用源预设名称) - - Returns: - 是否复制成功 - """ - try: - if source_preset_id not in self.presets: - print(f"✗ 源预设不存在: {source_preset_id}") - return False - - # 复制预设数据 - preset_data = self.presets[source_preset_id].copy() - - # 更新名称 - if new_name: - preset_data['name'] = new_name - else: - preset_data['name'] = f"{preset_data['name']} (副本)" - - # 更新时间戳 - preset_data['timestamp'] = time.time() - - # 标记为非默认预设 - preset_data['is_default'] = False - - # 保存新预设 - return self.save_preset(new_preset_id, preset_data) - - except Exception as e: - print(f"✗ 预设复制失败: {e}") - import traceback - traceback.print_exc() - return False - - def update_preset_parameters(self, preset_id: str, parameters: Dict[str, Any]) -> bool: - """ - 更新预设参数 - - Args: - preset_id: 预设ID - parameters: 要更新的参数 - - Returns: - 是否更新成功 - """ - try: - if preset_id not in self.presets: - print(f"✗ 预设不存在: {preset_id}") - return False - - # 更新参数 - self.presets[preset_id]['parameters'].update(parameters) - - # 更新时间戳 - self.presets[preset_id]['timestamp'] = time.time() - - # 保存到文件 - presets_dir = self.preset_config['presets_directory'] - filepath = os.path.join(presets_dir, f"{preset_id}.json") - - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(self.presets[preset_id], f, ensure_ascii=False, indent=2) - - print(f"✓ 预设 '{preset_id}' 参数更新完成") - return True - - except Exception as e: - print(f"✗ 预设参数更新失败: {e}") - import traceback - traceback.print_exc() - return False - - def get_preset_parameters(self, preset_id: str) -> Optional[Dict[str, Any]]: - """ - 获取预设参数 - - Args: - preset_id: 预设ID - - Returns: - 参数字典或None - """ - try: - if preset_id not in self.presets: - return None - - return self.presets[preset_id].get('parameters', {}).copy() - - except Exception as e: - print(f"✗ 预设参数获取失败: {e}") - import traceback - traceback.print_exc() - return None - - def validate_preset(self, preset_id: str) -> bool: - """ - 验证预设完整性 - - Args: - preset_id: 预设ID - - Returns: - 预设是否有效 - """ - try: - if preset_id not in self.presets: - return False - - preset_data = self.presets[preset_id] - - # 检查必需字段 - required_fields = ['name', 'category', 'parameters'] - for field in required_fields: - if field not in preset_data: - print(f"✗ 预设 '{preset_id}' 缺少必需字段: {field}") - return False - - # 检查类别有效性 - if preset_data['category'] not in self.preset_categories: - print(f"✗ 预设 '{preset_id}' 包含无效类别: {preset_data['category']}") - return False - - return True - - except Exception as e: - print(f"✗ 预设验证失败: {e}") - import traceback - traceback.print_exc() - return False - - def backup_presets(self, backup_dir: str = 'presets_backup') -> bool: - """ - 备份所有预设 - - Args: - backup_dir: 备份目录 - - Returns: - 是否备份成功 - """ - try: - if not os.path.exists(backup_dir): - os.makedirs(backup_dir) - - backup_count = 0 - for preset_id, preset_data in self.presets.items(): - try: - filepath = os.path.join(backup_dir, f"{preset_id}.json") - with open(filepath, 'w', encoding='utf-8') as f: - json.dump(preset_data, f, ensure_ascii=False, indent=2) - backup_count += 1 - except Exception as e: - print(f"✗ 预设 '{preset_id}' 备份失败: {e}") - - print(f"✓ 预设备份完成: {backup_count}/{len(self.presets)} 个预设已备份到 {backup_dir}") - return True - - except Exception as e: - print(f"✗ 预设备份失败: {e}") - import traceback - traceback.print_exc() - return False - - def restore_presets(self, backup_dir: str = 'presets_backup') -> bool: - """ - 从备份恢复预设 - - Args: - backup_dir: 备份目录 - - Returns: - 是否恢复成功 - """ - try: - if not os.path.exists(backup_dir): - print(f"✗ 备份目录不存在: {backup_dir}") - return False - - restore_count = 0 - for filename in os.listdir(backup_dir): - if filename.endswith('.json'): - try: - filepath = os.path.join(backup_dir, filename) - with open(filepath, 'r', encoding='utf-8') as f: - preset_data = json.load(f) - - preset_id = os.path.splitext(filename)[0] - self.presets[preset_id] = preset_data - restore_count += 1 - except Exception as e: - print(f"✗ 预设文件 '{filename}' 恢复失败: {e}") - - print(f"✓ 预设恢复完成: {restore_count} 个预设已从 {backup_dir} 恢复") - return True - - except Exception as e: - print(f"✗ 预设恢复失败: {e}") - import traceback - traceback.print_exc() +""" +预设管理器 +管理天气和季节系统的预设配置 +""" + +import json +import os +from typing import Dict, Any, List, Optional +import time + +class PresetManager: + """ + 预设管理器 + 管理天气和季节系统的预设配置,包括创建、保存、加载和应用预设 + """ + + def __init__(self, plugin): + """ + 初始化预设管理器 + + Args: + plugin: 天气和季节系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 预设配置 + self.preset_config = { + 'presets_directory': 'presets/weather', + 'default_preset': 'default', + 'auto_save_enabled': True, + 'max_presets': 100 + } + + # 预设数据 + self.presets = {} + self.current_preset = None + + # 预设类别 + self.preset_categories = { + 'weather': {'name': '天气', 'description': '天气配置预设'}, + 'season': {'name': '季节', 'description': '季节配置预设'}, + 'environment': {'name': '环境', 'description': '环境效果预设'}, + 'particles': {'name': '粒子', 'description': '粒子效果预设'}, + 'audio': {'name': '音频', 'description': '音频效果预设'}, + 'mixed': {'name': '混合', 'description': '混合配置预设'} + } + + # 统计信息 + self.stats = { + 'presets_loaded': 0, + 'presets_saved': 0, + 'presets_applied': 0, + 'total_load_time': 0.0, + 'total_save_time': 0.0 + } + + print("✓ 预设管理器已创建") + + def initialize(self) -> bool: + """ + 初始化预设管理器 + + Returns: + 是否初始化成功 + """ + try: + # 创建预设目录 + presets_dir = self.preset_config['presets_directory'] + if not os.path.exists(presets_dir): + os.makedirs(presets_dir) + + # 加载默认预设 + self._load_default_presets() + + # 加载用户预设 + self._load_user_presets() + + self.initialized = True + print("✓ 预设管理器初始化完成") + return True + + except Exception as e: + print(f"✗ 预设管理器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用预设管理器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 预设管理器未初始化") + return False + + self.enabled = True + print("✓ 预设管理器已启用") + return True + + except Exception as e: + print(f"✗ 预设管理器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用预设管理器""" + try: + self.enabled = False + print("✓ 预设管理器已禁用") + + except Exception as e: + print(f"✗ 预设管理器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理预设管理器资源""" + try: + self.disable() + + # 自动保存当前预设 + if self.preset_config['auto_save_enabled'] and self.current_preset: + self.save_preset(self.current_preset) + + self.initialized = False + print("✓ 预设管理器资源已清理") + + except Exception as e: + print(f"✗ 预设管理器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新预设管理器状态 + + Args: + dt: 时间增量 + """ + # 处理更新逻辑 + pass + + def _load_default_presets(self): + """加载默认预设""" + try: + # 创建默认天气预设 + self.presets['default_weather'] = { + 'name': '默认天气', + 'category': 'weather', + 'description': '默认的天气配置参数', + 'parameters': { + 'temperature': 20.0, + 'humidity': 50.0, + 'wind_speed': 3.0, + 'visibility': 1.0 + }, + 'timestamp': time.time(), + 'is_default': True + } + + # 创建默认季节预设 + self.presets['default_season'] = { + 'name': '默认季节', + 'category': 'season', + 'description': '默认的季节配置参数', + 'parameters': { + 'season_duration': 90, + 'transition_duration': 7.0, + 'temperature_influence': 15.0 + }, + 'timestamp': time.time(), + 'is_default': True + } + + # 创建默认环境预设 + self.presets['default_environment'] = { + 'name': '默认环境', + 'category': 'environment', + 'description': '默认的环境效果参数', + 'parameters': { + 'light_intensity': 1.0, + 'fog_density': 0.0, + 'wind_turbulence': 0.1 + }, + 'timestamp': time.time(), + 'is_default': True + } + + # 创建默认粒子预设 + self.presets['default_particles'] = { + 'name': '默认粒子', + 'category': 'particles', + 'description': '默认的粒子效果参数', + 'parameters': { + 'particle_count': 1000, + 'particle_size': 1.0, + 'emission_rate': 100 + }, + 'timestamp': time.time(), + 'is_default': True + } + + # 创建默认音频预设 + self.presets['default_audio'] = { + 'name': '默认音频', + 'category': 'audio', + 'description': '默认的音频效果参数', + 'parameters': { + 'master_volume': 1.0, + 'effects_volume': 1.0, + 'ambient_volume': 1.0 + }, + 'timestamp': time.time(), + 'is_default': True + } + + # 创建默认混合预设 + self.presets['default_mixed'] = { + 'name': '默认混合', + 'category': 'mixed', + 'description': '默认的混合配置参数', + 'parameters': { + 'weather': 'clear', + 'season': 'spring', + 'time_of_day': 12.0 + }, + 'timestamp': time.time(), + 'is_default': True + } + + print("✓ 默认预设加载完成") + + except Exception as e: + print(f"✗ 默认预设加载失败: {e}") + import traceback + traceback.print_exc() + + def _load_user_presets(self): + """加载用户预设""" + try: + load_start_time = time.time() + + presets_dir = self.preset_config['presets_directory'] + if not os.path.exists(presets_dir): + print("✓ 用户预设目录不存在,跳过加载") + return + + # 遍历预设目录 + for filename in os.listdir(presets_dir): + if filename.endswith('.json'): + try: + filepath = os.path.join(presets_dir, filename) + with open(filepath, 'r', encoding='utf-8') as f: + preset_data = json.load(f) + + # 使用文件名(不含扩展名)作为预设ID + preset_id = os.path.splitext(filename)[0] + self.presets[preset_id] = preset_data + + except Exception as e: + print(f"✗ 预设文件加载失败 {filename}: {e}") + + load_time = time.time() - load_start_time + self.stats['presets_loaded'] = len(self.presets) + self.stats['total_load_time'] = load_time + + print(f"✓ 用户预设加载完成,共加载 {len(self.presets)} 个预设,耗时: {load_time:.3f}秒") + + except Exception as e: + print(f"✗ 用户预设加载失败: {e}") + import traceback + traceback.print_exc() + + def save_preset(self, preset_id: str, preset_data: Dict[str, Any] = None) -> bool: + """ + 保存预设 + + Args: + preset_id: 预设ID + preset_data: 预设数据(如果为None,则保存当前配置) + + Returns: + 是否保存成功 + """ + try: + save_start_time = time.time() + + if not self.enabled: + print("✗ 预设管理器未启用") + return False + + # 如果未提供预设数据,则创建当前配置的快照 + if preset_data is None: + preset_data = self._create_current_preset_snapshot(preset_id) + + # 保存预设数据 + self.presets[preset_id] = preset_data + + # 保存到文件 + presets_dir = self.preset_config['presets_directory'] + filepath = os.path.join(presets_dir, f"{preset_id}.json") + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(preset_data, f, ensure_ascii=False, indent=2) + + # 更新统计信息 + save_time = time.time() - save_start_time + self.stats['presets_saved'] += 1 + self.stats['total_save_time'] += save_time + + print(f"✓ 预设 '{preset_id}' 保存完成,耗时: {save_time:.3f}秒") + return True + + except Exception as e: + print(f"✗ 预设保存失败: {e}") + import traceback + traceback.print_exc() + return False + + def _create_current_preset_snapshot(self, preset_id: str) -> Dict[str, Any]: + """ + 创建当前配置的预设快照 + + Args: + preset_id: 预设ID + + Returns: + 预设数据字典 + """ + try: + # 获取当前插件配置 + preset_data = { + 'name': preset_id, + 'category': 'mixed', # 默认类别 + 'description': '用户自定义预设', + 'parameters': {}, + 'timestamp': time.time(), + 'is_default': False + } + + # 添加天气管理器配置 + if self.plugin.weather_manager: + wm = self.plugin.weather_manager + preset_data['parameters'].update({ + 'base_temperature': wm.base_temperature, + 'base_humidity': wm.base_humidity, + 'base_wind_speed': wm.base_wind_speed, + 'base_visibility': wm.base_visibility + }) + + # 添加季节管理器配置 + if self.plugin.season_manager: + sm = self.plugin.season_manager + preset_data['parameters'].update({ + 'season_duration': sm.season_duration, + 'current_season': sm.current_season, + 'season_cycle_enabled': sm.season_cycle_enabled + }) + + # 添加环境效果配置 + if self.plugin.environment_effects: + ee = self.plugin.environment_effects + preset_data['parameters'].update({ + 'time_of_day': ee.time_config['day_time'], + 'day_speed': ee.time_config['day_speed'] + }) + + # 添加音频管理器配置 + if self.plugin.audio_manager: + am = self.plugin.audio_manager + audio_params = am.get_audio_parameters() + preset_data['parameters'].update(audio_params) + + return preset_data + + except Exception as e: + print(f"✗ 预设快照创建失败: {e}") + import traceback + traceback.print_exc() + return { + 'name': preset_id, + 'category': 'mixed', + 'description': '默认预设', + 'parameters': {}, + 'timestamp': time.time(), + 'is_default': False + } + + def load_preset(self, preset_id: str) -> Optional[Dict[str, Any]]: + """ + 加载预设 + + Args: + preset_id: 预设ID + + Returns: + 预设数据字典或None + """ + try: + if not self.enabled: + print("✗ 预设管理器未启用") + return None + + if preset_id not in self.presets: + print(f"✗ 预设不存在: {preset_id}") + return None + + preset_data = self.presets[preset_id] + + # 更新统计信息 + self.stats['presets_applied'] += 1 + self.current_preset = preset_id + + print(f"✓ 预设 '{preset_id}' 加载完成") + return preset_data + + except Exception as e: + print(f"✗ 预设加载失败: {e}") + import traceback + traceback.print_exc() + return None + + def apply_preset(self, preset_id: str) -> bool: + """ + 应用预设到当前配置 + + Args: + preset_id: 预设ID + + Returns: + 是否应用成功 + """ + try: + if not self.enabled: + print("✗ 预设管理器未启用") + return False + + preset_data = self.load_preset(preset_id) + if preset_data is None: + return False + + parameters = preset_data.get('parameters', {}) + + # 应用参数到各个组件 + self._apply_parameters_to_components(parameters) + + # 更新统计信息 + self.stats['presets_applied'] += 1 + self.current_preset = preset_id + + print(f"✓ 预设 '{preset_id}' 应用完成") + return True + + except Exception as e: + print(f"✗ 预设应用失败: {e}") + import traceback + traceback.print_exc() + return False + + def _apply_parameters_to_components(self, parameters: Dict[str, Any]): + """ + 应用参数到各个组件 + + Args: + parameters: 参数字典 + """ + try: + # 应用到天气管理器 + if self.plugin.weather_manager: + wm = self.plugin.weather_manager + if 'base_temperature' in parameters: + wm.set_base_temperature(parameters['base_temperature']) + if 'base_humidity' in parameters: + wm.set_base_humidity(parameters['base_humidity']) + if 'base_wind_speed' in parameters: + wm.set_base_wind_speed(parameters['base_wind_speed']) + if 'base_visibility' in parameters: + wm.set_base_visibility(parameters['base_visibility']) + + # 应用到季节管理器 + if self.plugin.season_manager: + sm = self.plugin.season_manager + if 'season_duration' in parameters: + sm.set_season_duration(parameters['season_duration']) + if 'current_season' in parameters: + sm.set_current_season(parameters['current_season']) + if 'season_cycle_enabled' in parameters: + sm.enable_season_cycle(parameters['season_cycle_enabled']) + + # 应用到环境效果 + if self.plugin.environment_effects: + ee = self.plugin.environment_effects + if 'time_of_day' in parameters: + ee.set_time(parameters['time_of_day']) + if 'day_speed' in parameters: + ee.set_day_speed(parameters['day_speed']) + + # 应用到音频管理器 + if self.plugin.audio_manager: + am = self.plugin.audio_manager + for param_name, param_value in parameters.items(): + if param_name.startswith(('master_', 'effects_', 'ambient_')): + am.set_audio_parameter(param_name, param_value) + + except Exception as e: + print(f"✗ 参数应用失败: {e}") + import traceback + traceback.print_exc() + + def delete_preset(self, preset_id: str) -> bool: + """ + 删除预设 + + Args: + preset_id: 预设ID + + Returns: + 是否删除成功 + """ + try: + if not self.enabled: + print("✗ 预设管理器未启用") + return False + + if preset_id not in self.presets: + print(f"✗ 预设不存在: {preset_id}") + return False + + # 检查是否为默认预设 + if self.presets[preset_id].get('is_default', False): + print(f"✗ 无法删除默认预设: {preset_id}") + return False + + # 从内存中删除 + del self.presets[preset_id] + + # 从文件系统中删除 + presets_dir = self.preset_config['presets_directory'] + filepath = os.path.join(presets_dir, f"{preset_id}.json") + if os.path.exists(filepath): + os.remove(filepath) + + # 如果删除的是当前预设,清空当前预设 + if self.current_preset == preset_id: + self.current_preset = None + + print(f"✓ 预设 '{preset_id}' 删除完成") + return True + + except Exception as e: + print(f"✗ 预设删除失败: {e}") + import traceback + traceback.print_exc() + return False + + def list_presets(self, category: str = None) -> List[str]: + """ + 列出预设 + + Args: + category: 预设类别(如果为None,则列出所有预设) + + Returns: + 预设ID列表 + """ + try: + if category is None: + return list(self.presets.keys()) + else: + return [preset_id for preset_id, preset_data in self.presets.items() + if preset_data.get('category') == category] + + except Exception as e: + print(f"✗ 预设列表获取失败: {e}") + import traceback + traceback.print_exc() + return [] + + def get_preset_info(self, preset_id: str) -> Optional[Dict[str, Any]]: + """ + 获取预设信息 + + Args: + preset_id: 预设ID + + Returns: + 预设信息字典或None + """ + try: + if preset_id not in self.presets: + return None + + preset_data = self.presets[preset_id] + return { + 'name': preset_data.get('name', preset_id), + 'category': preset_data.get('category', 'unknown'), + 'description': preset_data.get('description', ''), + 'timestamp': preset_data.get('timestamp', 0), + 'is_default': preset_data.get('is_default', False), + 'parameters_count': len(preset_data.get('parameters', {})) + } + + except Exception as e: + print(f"✗ 预设信息获取失败: {e}") + import traceback + traceback.print_exc() + return None + + def get_preset_categories(self) -> Dict[str, Dict[str, str]]: + """ + 获取预设类别 + + Returns: + 预设类别字典 + """ + return self.preset_categories.copy() + + def create_preset_category(self, category_id: str, name: str, description: str = '') -> bool: + """ + 创建预设类别 + + Args: + category_id: 类别ID + name: 类别名称 + description: 类别描述 + + Returns: + 是否创建成功 + """ + try: + self.preset_categories[category_id] = { + 'name': name, + 'description': description + } + + print(f"✓ 预设类别 '{name}' 创建完成") + return True + + except Exception as e: + print(f"✗ 预设类别创建失败: {e}") + import traceback + traceback.print_exc() + return False + + def export_preset(self, preset_id: str, filepath: str) -> bool: + """ + 导出预设到文件 + + Args: + preset_id: 预设ID + filepath: 导出文件路径 + + Returns: + 是否导出成功 + """ + try: + if preset_id not in self.presets: + print(f"✗ 预设不存在: {preset_id}") + return False + + preset_data = self.presets[preset_id] + + # 保存到指定文件 + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(preset_data, f, ensure_ascii=False, indent=2) + + print(f"✓ 预设 '{preset_id}' 导出到 {filepath}") + return True + + except Exception as e: + print(f"✗ 预设导出失败: {e}") + import traceback + traceback.print_exc() + return False + + def import_preset(self, filepath: str, preset_id: str = None) -> bool: + """ + 从文件导入预设 + + Args: + filepath: 导入文件路径 + preset_id: 预设ID(如果为None,则使用文件名) + + Returns: + 是否导入成功 + """ + try: + # 读取预设文件 + with open(filepath, 'r', encoding='utf-8') as f: + preset_data = json.load(f) + + # 确定预设ID + if preset_id is None: + preset_id = os.path.splitext(os.path.basename(filepath))[0] + + # 保存预设 + self.presets[preset_id] = preset_data + + # 保存到预设目录 + presets_dir = self.preset_config['presets_directory'] + save_filepath = os.path.join(presets_dir, f"{preset_id}.json") + + with open(save_filepath, 'w', encoding='utf-8') as f: + json.dump(preset_data, f, ensure_ascii=False, indent=2) + + print(f"✓ 预设从 {filepath} 导入完成,ID: {preset_id}") + return True + + except Exception as e: + print(f"✗ 预设导入失败: {e}") + import traceback + traceback.print_exc() + return False + + def set_preset_config(self, config: Dict[str, Any]): + """ + 设置预设配置 + + Args: + config: 配置字典 + """ + self.preset_config.update(config) + print(f"✓ 预设配置已更新: {self.preset_config}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'presets_loaded': 0, + 'presets_saved': 0, + 'presets_applied': 0, + 'total_load_time': 0.0, + 'total_save_time': 0.0 + } + print("✓ 预设管理器统计信息已重置") + + def get_current_preset(self) -> Optional[str]: + """ + 获取当前预设 + + Returns: + 当前预设ID或None + """ + return self.current_preset + + def set_current_preset(self, preset_id: str): + """ + 设置当前预设 + + Args: + preset_id: 预设ID + """ + if preset_id in self.presets: + self.current_preset = preset_id + print(f"✓ 当前预设设置为: {preset_id}") + else: + print(f"✗ 预设不存在: {preset_id}") + + def create_terrain_preset_from_current(self, preset_name: str, description: str = '') -> str: + """ + 从当前配置创建预设 + + Args: + preset_name: 预设名称 + description: 预设描述 + + Returns: + 预设ID + """ + try: + # 生成预设ID + preset_id = preset_name.lower().replace(' ', '_') + + # 创建预设数据 + preset_data = { + 'name': preset_name, + 'category': 'mixed', + 'description': description, + 'parameters': {}, + 'timestamp': time.time(), + 'is_default': False + } + + # 获取当前配置参数 + if self.plugin.weather_manager: + wm = self.plugin.weather_manager + preset_data['parameters'].update({ + 'base_temperature': wm.base_temperature, + 'base_humidity': wm.base_humidity, + 'base_wind_speed': wm.base_wind_speed, + 'base_visibility': wm.base_visibility + }) + + if self.plugin.season_manager: + sm = self.plugin.season_manager + preset_data['parameters'].update({ + 'season_duration': sm.season_duration, + 'current_season': sm.current_season + }) + + # 保存预设 + self.save_preset(preset_id, preset_data) + + print(f"✓ 从当前配置创建预设: {preset_name}") + return preset_id + + except Exception as e: + print(f"✗ 从当前配置创建预设失败: {e}") + import traceback + traceback.print_exc() + return '' + + def batch_apply_presets(self, preset_ids: List[str]) -> bool: + """ + 批量应用预设 + + Args: + preset_ids: 预设ID列表 + + Returns: + 是否应用成功 + """ + try: + if not self.enabled: + print("✗ 预设管理器未启用") + return False + + success_count = 0 + for preset_id in preset_ids: + if self.apply_preset(preset_id): + success_count += 1 + + print(f"✓ 批量应用预设完成: {success_count}/{len(preset_ids)} 个预设应用成功") + return success_count == len(preset_ids) + + except Exception as e: + print(f"✗ 批量应用预设失败: {e}") + import traceback + traceback.print_exc() + return False + + def search_presets(self, keyword: str, category: str = None) -> List[str]: + """ + 搜索预设 + + Args: + keyword: 搜索关键词 + category: 预设类别(可选) + + Returns: + 匹配的预设ID列表 + """ + try: + matching_presets = [] + + for preset_id, preset_data in self.presets.items(): + # 检查类别筛选 + if category and preset_data.get('category') != category: + continue + + # 检查关键词匹配 + if (keyword.lower() in preset_data.get('name', '').lower() or + keyword.lower() in preset_data.get('description', '').lower()): + matching_presets.append(preset_id) + + return matching_presets + + except Exception as e: + print(f"✗ 预设搜索失败: {e}") + import traceback + traceback.print_exc() + return [] + + def duplicate_preset(self, source_preset_id: str, new_preset_id: str, new_name: str = None) -> bool: + """ + 复制预设 + + Args: + source_preset_id: 源预设ID + new_preset_id: 新预设ID + new_name: 新预设名称(如果为None,则使用源预设名称) + + Returns: + 是否复制成功 + """ + try: + if source_preset_id not in self.presets: + print(f"✗ 源预设不存在: {source_preset_id}") + return False + + # 复制预设数据 + preset_data = self.presets[source_preset_id].copy() + + # 更新名称 + if new_name: + preset_data['name'] = new_name + else: + preset_data['name'] = f"{preset_data['name']} (副本)" + + # 更新时间戳 + preset_data['timestamp'] = time.time() + + # 标记为非默认预设 + preset_data['is_default'] = False + + # 保存新预设 + return self.save_preset(new_preset_id, preset_data) + + except Exception as e: + print(f"✗ 预设复制失败: {e}") + import traceback + traceback.print_exc() + return False + + def update_preset_parameters(self, preset_id: str, parameters: Dict[str, Any]) -> bool: + """ + 更新预设参数 + + Args: + preset_id: 预设ID + parameters: 要更新的参数 + + Returns: + 是否更新成功 + """ + try: + if preset_id not in self.presets: + print(f"✗ 预设不存在: {preset_id}") + return False + + # 更新参数 + self.presets[preset_id]['parameters'].update(parameters) + + # 更新时间戳 + self.presets[preset_id]['timestamp'] = time.time() + + # 保存到文件 + presets_dir = self.preset_config['presets_directory'] + filepath = os.path.join(presets_dir, f"{preset_id}.json") + + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(self.presets[preset_id], f, ensure_ascii=False, indent=2) + + print(f"✓ 预设 '{preset_id}' 参数更新完成") + return True + + except Exception as e: + print(f"✗ 预设参数更新失败: {e}") + import traceback + traceback.print_exc() + return False + + def get_preset_parameters(self, preset_id: str) -> Optional[Dict[str, Any]]: + """ + 获取预设参数 + + Args: + preset_id: 预设ID + + Returns: + 参数字典或None + """ + try: + if preset_id not in self.presets: + return None + + return self.presets[preset_id].get('parameters', {}).copy() + + except Exception as e: + print(f"✗ 预设参数获取失败: {e}") + import traceback + traceback.print_exc() + return None + + def validate_preset(self, preset_id: str) -> bool: + """ + 验证预设完整性 + + Args: + preset_id: 预设ID + + Returns: + 预设是否有效 + """ + try: + if preset_id not in self.presets: + return False + + preset_data = self.presets[preset_id] + + # 检查必需字段 + required_fields = ['name', 'category', 'parameters'] + for field in required_fields: + if field not in preset_data: + print(f"✗ 预设 '{preset_id}' 缺少必需字段: {field}") + return False + + # 检查类别有效性 + if preset_data['category'] not in self.preset_categories: + print(f"✗ 预设 '{preset_id}' 包含无效类别: {preset_data['category']}") + return False + + return True + + except Exception as e: + print(f"✗ 预设验证失败: {e}") + import traceback + traceback.print_exc() + return False + + def backup_presets(self, backup_dir: str = 'presets_backup') -> bool: + """ + 备份所有预设 + + Args: + backup_dir: 备份目录 + + Returns: + 是否备份成功 + """ + try: + if not os.path.exists(backup_dir): + os.makedirs(backup_dir) + + backup_count = 0 + for preset_id, preset_data in self.presets.items(): + try: + filepath = os.path.join(backup_dir, f"{preset_id}.json") + with open(filepath, 'w', encoding='utf-8') as f: + json.dump(preset_data, f, ensure_ascii=False, indent=2) + backup_count += 1 + except Exception as e: + print(f"✗ 预设 '{preset_id}' 备份失败: {e}") + + print(f"✓ 预设备份完成: {backup_count}/{len(self.presets)} 个预设已备份到 {backup_dir}") + return True + + except Exception as e: + print(f"✗ 预设备份失败: {e}") + import traceback + traceback.print_exc() + return False + + def restore_presets(self, backup_dir: str = 'presets_backup') -> bool: + """ + 从备份恢复预设 + + Args: + backup_dir: 备份目录 + + Returns: + 是否恢复成功 + """ + try: + if not os.path.exists(backup_dir): + print(f"✗ 备份目录不存在: {backup_dir}") + return False + + restore_count = 0 + for filename in os.listdir(backup_dir): + if filename.endswith('.json'): + try: + filepath = os.path.join(backup_dir, filename) + with open(filepath, 'r', encoding='utf-8') as f: + preset_data = json.load(f) + + preset_id = os.path.splitext(filename)[0] + self.presets[preset_id] = preset_data + restore_count += 1 + except Exception as e: + print(f"✗ 预设文件 '{filename}' 恢复失败: {e}") + + print(f"✓ 预设恢复完成: {restore_count} 个预设已从 {backup_dir} 恢复") + return True + + except Exception as e: + print(f"✗ 预设恢复失败: {e}") + import traceback + traceback.print_exc() return False \ No newline at end of file diff --git a/plugins/user/weather_season_system/editor/weather_editor.py b/plugins/user/weather_season_system/editor/weather_editor.py index 8b578fd9..e9ff6688 100644 --- a/plugins/user/weather_season_system/editor/weather_editor.py +++ b/plugins/user/weather_season_system/editor/weather_editor.py @@ -1,694 +1,694 @@ -""" -天气编辑器 -提供可视化界面用于编辑和调整天气和季节参数 -""" - -import time -from typing import Dict, Any, List, Optional -import json - -class WeatherEditor: - """ - 天气编辑器 - 提供可视化界面用于编辑和调整天气和季节参数,包括实时预览、参数调整等功能 - """ - - def __init__(self, plugin): - """ - 初始化天气编辑器 - - Args: - plugin: 天气和季节系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 编辑器配置 - self.editor_config = { - 'auto_preview': True, - 'realtime_update': True, - 'preview_quality': 'medium', - 'history_limit': 50 - } - - # 编辑模式 - self.edit_modes = { - 'weather': {'name': '天气编辑', 'description': '编辑天气参数'}, - 'season': {'name': '季节编辑', 'description': '编辑季节参数'}, - 'environment': {'name': '环境编辑', 'description': '编辑环境效果参数'}, - 'particles': {'name': '粒子编辑', 'description': '编辑粒子效果参数'}, - 'audio': {'name': '音频编辑', 'description': '编辑音频效果参数'} - } - - self.current_edit_mode = 'weather' - - # 参数分组 - self.parameter_groups = { - 'weather': ['temperature', 'humidity', 'wind_speed', 'precipitation', 'visibility'], - 'season': ['season_progress', 'day_length', 'temperature_range', 'vegetation_growth'], - 'environment': ['light_intensity', 'fog_density', 'wind_turbulence'], - 'particles': ['particle_count', 'particle_size', 'emission_rate'], - 'audio': ['master_volume', 'effects_volume', 'ambient_volume'] - } - - # 编辑历史 - self.edit_history = [] - self.history_position = -1 - - # 预览数据 - self.preview_data = None - self.preview_dirty = False - - # 工具状态 - self.is_editing = False - self.selected_parameter = None - - # 统计信息 - self.stats = { - 'edits_made': 0, - 'undos_performed': 0, - 'redos_performed': 0, - 'total_edit_time': 0.0, - 'average_edit_time': 0.0 - } - - # UI组件引用(在实际实现中会连接到GUI) - self.ui_components = { - 'toolbar': None, - 'properties_panel': None, - 'preview_window': None, - 'parameter_sliders': None, - 'history_panel': None - } - - print("✓ 天气编辑器已创建") - - def initialize(self) -> bool: - """ - 初始化天气编辑器 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 天气编辑器初始化完成") - return True - - except Exception as e: - print(f"✗ 天气编辑器初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用天气编辑器 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 天气编辑器未初始化") - return False - - self.enabled = True - print("✓ 天气编辑器已启用") - return True - - except Exception as e: - print(f"✗ 天气编辑器启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用天气编辑器""" - try: - self.enabled = False - print("✓ 天气编辑器已禁用") - - except Exception as e: - print(f"✗ 天气编辑器禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理天气编辑器资源""" - try: - self.disable() - self.initialized = False - print("✓ 天气编辑器资源已清理") - - except Exception as e: - print(f"✗ 天气编辑器资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新天气编辑器状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新预览 - if self.preview_dirty and self.editor_config['auto_preview']: - self._update_preview() - - # 处理实时更新 - if self.editor_config['realtime_update']: - self._handle_realtime_updates() - - except Exception as e: - print(f"✗ 天气编辑器更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_preview(self): - """更新预览显示""" - try: - # 在实际实现中,这里会更新预览窗口 - self.preview_dirty = False - except Exception as e: - print(f"✗ 预览更新失败: {e}") - - def _handle_realtime_updates(self): - """处理实时更新""" - try: - # 在实际实现中,这里会处理实时编辑更新 - pass - except Exception as e: - print(f"✗ 实时更新处理失败: {e}") - - def set_edit_mode(self, mode: str): - """ - 设置编辑模式 - - Args: - mode: 编辑模式 - """ - if mode in self.edit_modes: - self.current_edit_mode = mode - print(f"✓ 编辑模式设置为: {self.edit_modes[mode]['name']}") - else: - print(f"✗ 无效的编辑模式: {mode}") - - def get_edit_modes(self) -> Dict[str, Dict[str, str]]: - """ - 获取可用的编辑模式 - - Returns: - 编辑模式字典 - """ - return self.edit_modes.copy() - - def get_current_edit_mode(self) -> str: - """ - 获取当前编辑模式 - - Returns: - 当前编辑模式 - """ - return self.current_edit_mode - - def set_parameter(self, parameter_name: str, value: Any): - """ - 设置参数值 - - Args: - parameter_name: 参数名称 - value: 参数值 - """ - try: - # 保存当前状态到历史记录 - self._save_state_to_history() - - # 根据编辑模式应用参数 - if self.current_edit_mode == 'weather' and self.plugin.weather_manager: - # 处理天气参数 - if parameter_name == 'temperature': - self.plugin.weather_manager.set_base_temperature(value) - elif parameter_name == 'humidity': - self.plugin.weather_manager.set_base_humidity(value) - elif parameter_name == 'wind_speed': - self.plugin.weather_manager.set_base_wind_speed(value) - elif parameter_name == 'visibility': - # 在实际实现中会调用相应方法 - pass - - elif self.current_edit_mode == 'season' and self.plugin.season_manager: - # 处理季节参数 - if parameter_name == 'season_progress': - # 在实际实现中会调用相应方法 - pass - - elif self.current_edit_mode == 'environment' and self.plugin.environment_effects: - # 处理环境参数 - self.plugin.environment_effects.set_effect_parameter('lighting', parameter_name, value) - - elif self.current_edit_mode == 'particles' and self.plugin.particle_effects: - # 处理粒子参数 - # 在实际实现中会调用相应方法 - pass - - elif self.current_edit_mode == 'audio' and self.plugin.audio_manager: - # 处理音频参数 - self.plugin.audio_manager.set_audio_parameter(parameter_name, value) - - # 标记预览需要更新 - self.preview_dirty = True - - # 更新统计信息 - self.stats['edits_made'] += 1 - - print(f"✓ 参数 '{parameter_name}' 设置为: {value}") - - except Exception as e: - print(f"✗ 参数设置失败: {e}") - import traceback - traceback.print_exc() - - def get_parameter(self, parameter_name: str) -> Any: - """ - 获取参数值 - - Args: - parameter_name: 参数名称 - - Returns: - 参数值 - """ - try: - # 根据编辑模式获取参数 - if self.current_edit_mode == 'weather' and self.plugin.weather_manager: - env_params = self.plugin.weather_manager.get_current_environment_params() - return env_params.get(parameter_name) - - elif self.current_edit_mode == 'season' and self.plugin.season_manager: - # 在实际实现中会获取相应参数 - pass - - elif self.current_edit_mode == 'environment' and self.plugin.environment_effects: - effects = self.plugin.environment_effects.get_current_effects() - # 简化实现,实际中需要更复杂的逻辑 - for effect_type, params in effects.items(): - if parameter_name in params: - return params[parameter_name] - - elif self.current_edit_mode == 'particles' and self.plugin.particle_effects: - # 在实际实现中会获取相应参数 - pass - - elif self.current_edit_mode == 'audio' and self.plugin.audio_manager: - audio_params = self.plugin.audio_manager.get_audio_parameters() - return audio_params.get(parameter_name) - - except Exception as e: - print(f"✗ 参数获取失败: {e}") - import traceback - traceback.print_exc() - - return None - - def _save_state_to_history(self): - """保存当前状态到历史记录""" - try: - # 创建状态快照 - state_snapshot = { - 'timestamp': time.time(), - 'edit_mode': self.current_edit_mode, - 'weather_params': {}, - 'season_params': {}, - 'environment_params': {}, - 'audio_params': {} - } - - # 保存天气参数 - if self.plugin.weather_manager: - state_snapshot['weather_params'] = self.plugin.weather_manager.get_current_environment_params() - - # 保存季节参数 - if self.plugin.season_manager: - state_snapshot['season_params'] = self.plugin.season_manager.get_interpolated_season_params() - - # 保存环境参数 - if self.plugin.environment_effects: - state_snapshot['environment_params'] = self.plugin.environment_effects.get_current_effects() - - # 保存音频参数 - if self.plugin.audio_manager: - state_snapshot['audio_params'] = self.plugin.audio_manager.get_audio_parameters() - - # 添加到历史记录 - if self.history_position < len(self.edit_history) - 1: - # 如果当前不在历史记录末尾,截断后续记录 - self.edit_history = self.edit_history[:self.history_position + 1] - - self.edit_history.append(state_snapshot) - self.history_position += 1 - - # 限制历史记录数量 - if len(self.edit_history) > self.editor_config['history_limit']: - self.edit_history.pop(0) - self.history_position -= 1 - - except Exception as e: - print(f"✗ 状态保存失败: {e}") - import traceback - traceback.print_exc() - - def undo(self) -> bool: - """ - 撤销操作 - - Returns: - 是否撤销成功 - """ - try: - if not self.enabled: - print("✗ 天气编辑器未启用") - return False - - if self.history_position <= 0: - print("✗ 没有可撤销的操作") - return False - - # 移动到前一个历史状态 - self.history_position -= 1 - - # 恢复状态 - self._restore_state_from_history() - - # 更新统计信息 - self.stats['undos_performed'] += 1 - - print("✓ 撤销操作完成") - return True - - except Exception as e: - print(f"✗ 撤销操作失败: {e}") - import traceback - traceback.print_exc() - return False - - def redo(self) -> bool: - """ - 重做操作 - - Returns: - 是否重做成功 - """ - try: - if not self.enabled: - print("✗ 天气编辑器未启用") - return False - - if self.history_position >= len(self.edit_history) - 1: - print("✗ 没有可重做的操作") - return False - - # 移动到下一个历史状态 - self.history_position += 1 - - # 恢复状态 - self._restore_state_from_history() - - # 更新统计信息 - self.stats['redos_performed'] += 1 - - print("✓ 重做操作完成") - return True - - except Exception as e: - print(f"✗ 重做操作失败: {e}") - import traceback - traceback.print_exc() - return False - - def _restore_state_from_history(self): - """从历史记录恢复状态""" - try: - if self.history_position < 0: - return - - # 获取历史状态 - state = self.edit_history[self.history_position] - - # 恢复参数 - if self.plugin.weather_manager and state['weather_params']: - # 恢复天气参数 - weather_params = state['weather_params'] - if 'temperature' in weather_params: - self.plugin.weather_manager.set_base_temperature(weather_params['temperature']) - if 'humidity' in weather_params: - self.plugin.weather_manager.set_base_humidity(weather_params['humidity']) - if 'wind_speed' in weather_params: - self.plugin.weather_manager.set_base_wind_speed(weather_params['wind_speed']) - - if self.plugin.audio_manager and state['audio_params']: - # 恢复音频参数 - audio_params = state['audio_params'] - for param_name, param_value in audio_params.items(): - self.plugin.audio_manager.set_audio_parameter(param_name, param_value) - - # 标记预览需要更新 - self.preview_dirty = True - - except Exception as e: - print(f"✗ 状态恢复失败: {e}") - import traceback - traceback.print_exc() - - def clear_history(self): - """清空历史记录""" - try: - self.edit_history.clear() - self.history_position = -1 - print("✓ 历史记录已清空") - except Exception as e: - print(f"✗ 历史记录清空失败: {e}") - import traceback - traceback.print_exc() - - def get_parameter_groups(self) -> Dict[str, List[str]]: - """ - 获取参数分组 - - Returns: - 参数分组字典 - """ - return self.parameter_groups.copy() - - def get_parameters_in_group(self, group_name: str) -> List[str]: - """ - 获取组中的参数列表 - - Args: - group_name: 组名称 - - Returns: - 参数列表 - """ - return self.parameter_groups.get(group_name, []).copy() - - def set_editor_config(self, config: Dict[str, Any]): - """ - 设置编辑器配置 - - Args: - config: 配置字典 - """ - self.editor_config.update(config) - print(f"✓ 编辑器配置已更新: {self.editor_config}") - - def get_editor_config(self) -> Dict[str, Any]: - """ - 获取编辑器配置 - - Returns: - 配置字典 - """ - return self.editor_config.copy() - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - # 更新平均编辑时间 - total_edits = self.stats['edits_made'] + self.stats['undos_performed'] + self.stats['redos_performed'] - if total_edits > 0: - self.stats['average_edit_time'] = self.stats['total_edit_time'] / total_edits - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'edits_made': 0, - 'undos_performed': 0, - 'redos_performed': 0, - 'total_edit_time': 0.0, - 'average_edit_time': 0.0 - } - print("✓ 天气编辑器统计信息已重置") - - def export_editor_state(self, filename: str) -> bool: - """ - 导出编辑器状态 - - Args: - filename: 文件名 - - Returns: - 是否导出成功 - """ - try: - # 创建状态数据 - state_data = { - 'editor_config': self.editor_config, - 'current_edit_mode': self.current_edit_mode, - 'parameter_groups': self.parameter_groups, - 'timestamp': time.time() - } - - # 保存为JSON文件 - with open(filename, 'w', encoding='utf-8') as f: - json.dump(state_data, f, ensure_ascii=False, indent=2) - - print(f"✓ 编辑器状态已导出到: {filename}") - return True - - except Exception as e: - print(f"✗ 编辑器状态导出失败: {e}") - import traceback - traceback.print_exc() - return False - - def import_editor_state(self, filename: str) -> bool: - """ - 导入编辑器状态 - - Args: - filename: 文件名 - - Returns: - 是否导入成功 - """ - try: - # 读取JSON文件 - with open(filename, 'r', encoding='utf-8') as f: - state_data = json.load(f) - - # 应用状态 - if 'editor_config' in state_data: - self.set_editor_config(state_data['editor_config']) - if 'current_edit_mode' in state_data: - self.set_edit_mode(state_data['current_edit_mode']) - - print(f"✓ 编辑器状态已从 {filename} 导入") - return True - - except Exception as e: - print(f"✗ 编辑器状态导入失败: {e}") - import traceback - traceback.print_exc() - return False - - def set_ui_component(self, component_name: str, component: Any): - """ - 设置UI组件引用 - - Args: - component_name: 组件名称 - component: 组件引用 - """ - if component_name in self.ui_components: - self.ui_components[component_name] = component - print(f"✓ UI组件 '{component_name}' 已设置") - else: - print(f"✗ 无效的UI组件名称: {component_name}") - - def refresh_ui(self): - """刷新UI显示""" - try: - # 在实际实现中,这里会更新所有UI组件的显示 - print("✓ UI刷新完成") - except Exception as e: - print(f"✗ UI刷新失败: {e}") - - def show_preview_window(self): - """显示预览窗口""" - try: - # 在实际实现中,这里会创建并显示预览窗口 - print("✓ 预览窗口已显示") - except Exception as e: - print(f"✗ 预览窗口显示失败: {e}") - - def hide_preview_window(self): - """隐藏预览窗口""" - try: - # 在实际实现中,这里会隐藏预览窗口 - print("✓ 预览窗口已隐藏") - except Exception as e: - print(f"✗ 预览窗口隐藏失败: {e}") - - def update_preview_window(self): - """更新预览窗口内容""" - try: - # 在实际实现中,这里会更新预览窗口的内容 - print("✓ 预览窗口已更新") - except Exception as e: - print(f"✗ 预览窗口更新失败: {e}") - - def apply_preset(self, preset_name: str): - """ - 应用预设 - - Args: - preset_name: 预设名称 - """ - try: - if self.plugin.preset_manager: - self.plugin.preset_manager.apply_preset(preset_name) - self.preview_dirty = True - print(f"✓ 预设 '{preset_name}' 已应用") - else: - print("✗ 预设管理器不可用") - except Exception as e: - print(f"✗ 预设应用失败: {e}") - import traceback - traceback.print_exc() - - def save_current_as_preset(self, preset_name: str, description: str = '') -> str: - """ - 将当前配置保存为预设 - - Args: - preset_name: 预设名称 - description: 预设描述 - - Returns: - 预设ID - """ - try: - if self.plugin.preset_manager: - preset_id = self.plugin.preset_manager.create_terrain_preset_from_current(preset_name, description) - print(f"✓ 当前配置已保存为预设: {preset_name}") - return preset_id - else: - print("✗ 预设管理器不可用") - return '' - except Exception as e: - print(f"✗ 预设保存失败: {e}") - import traceback - traceback.print_exc() +""" +天气编辑器 +提供可视化界面用于编辑和调整天气和季节参数 +""" + +import time +from typing import Dict, Any, List, Optional +import json + +class WeatherEditor: + """ + 天气编辑器 + 提供可视化界面用于编辑和调整天气和季节参数,包括实时预览、参数调整等功能 + """ + + def __init__(self, plugin): + """ + 初始化天气编辑器 + + Args: + plugin: 天气和季节系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 编辑器配置 + self.editor_config = { + 'auto_preview': True, + 'realtime_update': True, + 'preview_quality': 'medium', + 'history_limit': 50 + } + + # 编辑模式 + self.edit_modes = { + 'weather': {'name': '天气编辑', 'description': '编辑天气参数'}, + 'season': {'name': '季节编辑', 'description': '编辑季节参数'}, + 'environment': {'name': '环境编辑', 'description': '编辑环境效果参数'}, + 'particles': {'name': '粒子编辑', 'description': '编辑粒子效果参数'}, + 'audio': {'name': '音频编辑', 'description': '编辑音频效果参数'} + } + + self.current_edit_mode = 'weather' + + # 参数分组 + self.parameter_groups = { + 'weather': ['temperature', 'humidity', 'wind_speed', 'precipitation', 'visibility'], + 'season': ['season_progress', 'day_length', 'temperature_range', 'vegetation_growth'], + 'environment': ['light_intensity', 'fog_density', 'wind_turbulence'], + 'particles': ['particle_count', 'particle_size', 'emission_rate'], + 'audio': ['master_volume', 'effects_volume', 'ambient_volume'] + } + + # 编辑历史 + self.edit_history = [] + self.history_position = -1 + + # 预览数据 + self.preview_data = None + self.preview_dirty = False + + # 工具状态 + self.is_editing = False + self.selected_parameter = None + + # 统计信息 + self.stats = { + 'edits_made': 0, + 'undos_performed': 0, + 'redos_performed': 0, + 'total_edit_time': 0.0, + 'average_edit_time': 0.0 + } + + # UI组件引用(在实际实现中会连接到GUI) + self.ui_components = { + 'toolbar': None, + 'properties_panel': None, + 'preview_window': None, + 'parameter_sliders': None, + 'history_panel': None + } + + print("✓ 天气编辑器已创建") + + def initialize(self) -> bool: + """ + 初始化天气编辑器 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 天气编辑器初始化完成") + return True + + except Exception as e: + print(f"✗ 天气编辑器初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用天气编辑器 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 天气编辑器未初始化") + return False + + self.enabled = True + print("✓ 天气编辑器已启用") + return True + + except Exception as e: + print(f"✗ 天气编辑器启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用天气编辑器""" + try: + self.enabled = False + print("✓ 天气编辑器已禁用") + + except Exception as e: + print(f"✗ 天气编辑器禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理天气编辑器资源""" + try: + self.disable() + self.initialized = False + print("✓ 天气编辑器资源已清理") + + except Exception as e: + print(f"✗ 天气编辑器资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新天气编辑器状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新预览 + if self.preview_dirty and self.editor_config['auto_preview']: + self._update_preview() + + # 处理实时更新 + if self.editor_config['realtime_update']: + self._handle_realtime_updates() + + except Exception as e: + print(f"✗ 天气编辑器更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_preview(self): + """更新预览显示""" + try: + # 在实际实现中,这里会更新预览窗口 + self.preview_dirty = False + except Exception as e: + print(f"✗ 预览更新失败: {e}") + + def _handle_realtime_updates(self): + """处理实时更新""" + try: + # 在实际实现中,这里会处理实时编辑更新 + pass + except Exception as e: + print(f"✗ 实时更新处理失败: {e}") + + def set_edit_mode(self, mode: str): + """ + 设置编辑模式 + + Args: + mode: 编辑模式 + """ + if mode in self.edit_modes: + self.current_edit_mode = mode + print(f"✓ 编辑模式设置为: {self.edit_modes[mode]['name']}") + else: + print(f"✗ 无效的编辑模式: {mode}") + + def get_edit_modes(self) -> Dict[str, Dict[str, str]]: + """ + 获取可用的编辑模式 + + Returns: + 编辑模式字典 + """ + return self.edit_modes.copy() + + def get_current_edit_mode(self) -> str: + """ + 获取当前编辑模式 + + Returns: + 当前编辑模式 + """ + return self.current_edit_mode + + def set_parameter(self, parameter_name: str, value: Any): + """ + 设置参数值 + + Args: + parameter_name: 参数名称 + value: 参数值 + """ + try: + # 保存当前状态到历史记录 + self._save_state_to_history() + + # 根据编辑模式应用参数 + if self.current_edit_mode == 'weather' and self.plugin.weather_manager: + # 处理天气参数 + if parameter_name == 'temperature': + self.plugin.weather_manager.set_base_temperature(value) + elif parameter_name == 'humidity': + self.plugin.weather_manager.set_base_humidity(value) + elif parameter_name == 'wind_speed': + self.plugin.weather_manager.set_base_wind_speed(value) + elif parameter_name == 'visibility': + # 在实际实现中会调用相应方法 + pass + + elif self.current_edit_mode == 'season' and self.plugin.season_manager: + # 处理季节参数 + if parameter_name == 'season_progress': + # 在实际实现中会调用相应方法 + pass + + elif self.current_edit_mode == 'environment' and self.plugin.environment_effects: + # 处理环境参数 + self.plugin.environment_effects.set_effect_parameter('lighting', parameter_name, value) + + elif self.current_edit_mode == 'particles' and self.plugin.particle_effects: + # 处理粒子参数 + # 在实际实现中会调用相应方法 + pass + + elif self.current_edit_mode == 'audio' and self.plugin.audio_manager: + # 处理音频参数 + self.plugin.audio_manager.set_audio_parameter(parameter_name, value) + + # 标记预览需要更新 + self.preview_dirty = True + + # 更新统计信息 + self.stats['edits_made'] += 1 + + print(f"✓ 参数 '{parameter_name}' 设置为: {value}") + + except Exception as e: + print(f"✗ 参数设置失败: {e}") + import traceback + traceback.print_exc() + + def get_parameter(self, parameter_name: str) -> Any: + """ + 获取参数值 + + Args: + parameter_name: 参数名称 + + Returns: + 参数值 + """ + try: + # 根据编辑模式获取参数 + if self.current_edit_mode == 'weather' and self.plugin.weather_manager: + env_params = self.plugin.weather_manager.get_current_environment_params() + return env_params.get(parameter_name) + + elif self.current_edit_mode == 'season' and self.plugin.season_manager: + # 在实际实现中会获取相应参数 + pass + + elif self.current_edit_mode == 'environment' and self.plugin.environment_effects: + effects = self.plugin.environment_effects.get_current_effects() + # 简化实现,实际中需要更复杂的逻辑 + for effect_type, params in effects.items(): + if parameter_name in params: + return params[parameter_name] + + elif self.current_edit_mode == 'particles' and self.plugin.particle_effects: + # 在实际实现中会获取相应参数 + pass + + elif self.current_edit_mode == 'audio' and self.plugin.audio_manager: + audio_params = self.plugin.audio_manager.get_audio_parameters() + return audio_params.get(parameter_name) + + except Exception as e: + print(f"✗ 参数获取失败: {e}") + import traceback + traceback.print_exc() + + return None + + def _save_state_to_history(self): + """保存当前状态到历史记录""" + try: + # 创建状态快照 + state_snapshot = { + 'timestamp': time.time(), + 'edit_mode': self.current_edit_mode, + 'weather_params': {}, + 'season_params': {}, + 'environment_params': {}, + 'audio_params': {} + } + + # 保存天气参数 + if self.plugin.weather_manager: + state_snapshot['weather_params'] = self.plugin.weather_manager.get_current_environment_params() + + # 保存季节参数 + if self.plugin.season_manager: + state_snapshot['season_params'] = self.plugin.season_manager.get_interpolated_season_params() + + # 保存环境参数 + if self.plugin.environment_effects: + state_snapshot['environment_params'] = self.plugin.environment_effects.get_current_effects() + + # 保存音频参数 + if self.plugin.audio_manager: + state_snapshot['audio_params'] = self.plugin.audio_manager.get_audio_parameters() + + # 添加到历史记录 + if self.history_position < len(self.edit_history) - 1: + # 如果当前不在历史记录末尾,截断后续记录 + self.edit_history = self.edit_history[:self.history_position + 1] + + self.edit_history.append(state_snapshot) + self.history_position += 1 + + # 限制历史记录数量 + if len(self.edit_history) > self.editor_config['history_limit']: + self.edit_history.pop(0) + self.history_position -= 1 + + except Exception as e: + print(f"✗ 状态保存失败: {e}") + import traceback + traceback.print_exc() + + def undo(self) -> bool: + """ + 撤销操作 + + Returns: + 是否撤销成功 + """ + try: + if not self.enabled: + print("✗ 天气编辑器未启用") + return False + + if self.history_position <= 0: + print("✗ 没有可撤销的操作") + return False + + # 移动到前一个历史状态 + self.history_position -= 1 + + # 恢复状态 + self._restore_state_from_history() + + # 更新统计信息 + self.stats['undos_performed'] += 1 + + print("✓ 撤销操作完成") + return True + + except Exception as e: + print(f"✗ 撤销操作失败: {e}") + import traceback + traceback.print_exc() + return False + + def redo(self) -> bool: + """ + 重做操作 + + Returns: + 是否重做成功 + """ + try: + if not self.enabled: + print("✗ 天气编辑器未启用") + return False + + if self.history_position >= len(self.edit_history) - 1: + print("✗ 没有可重做的操作") + return False + + # 移动到下一个历史状态 + self.history_position += 1 + + # 恢复状态 + self._restore_state_from_history() + + # 更新统计信息 + self.stats['redos_performed'] += 1 + + print("✓ 重做操作完成") + return True + + except Exception as e: + print(f"✗ 重做操作失败: {e}") + import traceback + traceback.print_exc() + return False + + def _restore_state_from_history(self): + """从历史记录恢复状态""" + try: + if self.history_position < 0: + return + + # 获取历史状态 + state = self.edit_history[self.history_position] + + # 恢复参数 + if self.plugin.weather_manager and state['weather_params']: + # 恢复天气参数 + weather_params = state['weather_params'] + if 'temperature' in weather_params: + self.plugin.weather_manager.set_base_temperature(weather_params['temperature']) + if 'humidity' in weather_params: + self.plugin.weather_manager.set_base_humidity(weather_params['humidity']) + if 'wind_speed' in weather_params: + self.plugin.weather_manager.set_base_wind_speed(weather_params['wind_speed']) + + if self.plugin.audio_manager and state['audio_params']: + # 恢复音频参数 + audio_params = state['audio_params'] + for param_name, param_value in audio_params.items(): + self.plugin.audio_manager.set_audio_parameter(param_name, param_value) + + # 标记预览需要更新 + self.preview_dirty = True + + except Exception as e: + print(f"✗ 状态恢复失败: {e}") + import traceback + traceback.print_exc() + + def clear_history(self): + """清空历史记录""" + try: + self.edit_history.clear() + self.history_position = -1 + print("✓ 历史记录已清空") + except Exception as e: + print(f"✗ 历史记录清空失败: {e}") + import traceback + traceback.print_exc() + + def get_parameter_groups(self) -> Dict[str, List[str]]: + """ + 获取参数分组 + + Returns: + 参数分组字典 + """ + return self.parameter_groups.copy() + + def get_parameters_in_group(self, group_name: str) -> List[str]: + """ + 获取组中的参数列表 + + Args: + group_name: 组名称 + + Returns: + 参数列表 + """ + return self.parameter_groups.get(group_name, []).copy() + + def set_editor_config(self, config: Dict[str, Any]): + """ + 设置编辑器配置 + + Args: + config: 配置字典 + """ + self.editor_config.update(config) + print(f"✓ 编辑器配置已更新: {self.editor_config}") + + def get_editor_config(self) -> Dict[str, Any]: + """ + 获取编辑器配置 + + Returns: + 配置字典 + """ + return self.editor_config.copy() + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + # 更新平均编辑时间 + total_edits = self.stats['edits_made'] + self.stats['undos_performed'] + self.stats['redos_performed'] + if total_edits > 0: + self.stats['average_edit_time'] = self.stats['total_edit_time'] / total_edits + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'edits_made': 0, + 'undos_performed': 0, + 'redos_performed': 0, + 'total_edit_time': 0.0, + 'average_edit_time': 0.0 + } + print("✓ 天气编辑器统计信息已重置") + + def export_editor_state(self, filename: str) -> bool: + """ + 导出编辑器状态 + + Args: + filename: 文件名 + + Returns: + 是否导出成功 + """ + try: + # 创建状态数据 + state_data = { + 'editor_config': self.editor_config, + 'current_edit_mode': self.current_edit_mode, + 'parameter_groups': self.parameter_groups, + 'timestamp': time.time() + } + + # 保存为JSON文件 + with open(filename, 'w', encoding='utf-8') as f: + json.dump(state_data, f, ensure_ascii=False, indent=2) + + print(f"✓ 编辑器状态已导出到: {filename}") + return True + + except Exception as e: + print(f"✗ 编辑器状态导出失败: {e}") + import traceback + traceback.print_exc() + return False + + def import_editor_state(self, filename: str) -> bool: + """ + 导入编辑器状态 + + Args: + filename: 文件名 + + Returns: + 是否导入成功 + """ + try: + # 读取JSON文件 + with open(filename, 'r', encoding='utf-8') as f: + state_data = json.load(f) + + # 应用状态 + if 'editor_config' in state_data: + self.set_editor_config(state_data['editor_config']) + if 'current_edit_mode' in state_data: + self.set_edit_mode(state_data['current_edit_mode']) + + print(f"✓ 编辑器状态已从 {filename} 导入") + return True + + except Exception as e: + print(f"✗ 编辑器状态导入失败: {e}") + import traceback + traceback.print_exc() + return False + + def set_ui_component(self, component_name: str, component: Any): + """ + 设置UI组件引用 + + Args: + component_name: 组件名称 + component: 组件引用 + """ + if component_name in self.ui_components: + self.ui_components[component_name] = component + print(f"✓ UI组件 '{component_name}' 已设置") + else: + print(f"✗ 无效的UI组件名称: {component_name}") + + def refresh_ui(self): + """刷新UI显示""" + try: + # 在实际实现中,这里会更新所有UI组件的显示 + print("✓ UI刷新完成") + except Exception as e: + print(f"✗ UI刷新失败: {e}") + + def show_preview_window(self): + """显示预览窗口""" + try: + # 在实际实现中,这里会创建并显示预览窗口 + print("✓ 预览窗口已显示") + except Exception as e: + print(f"✗ 预览窗口显示失败: {e}") + + def hide_preview_window(self): + """隐藏预览窗口""" + try: + # 在实际实现中,这里会隐藏预览窗口 + print("✓ 预览窗口已隐藏") + except Exception as e: + print(f"✗ 预览窗口隐藏失败: {e}") + + def update_preview_window(self): + """更新预览窗口内容""" + try: + # 在实际实现中,这里会更新预览窗口的内容 + print("✓ 预览窗口已更新") + except Exception as e: + print(f"✗ 预览窗口更新失败: {e}") + + def apply_preset(self, preset_name: str): + """ + 应用预设 + + Args: + preset_name: 预设名称 + """ + try: + if self.plugin.preset_manager: + self.plugin.preset_manager.apply_preset(preset_name) + self.preview_dirty = True + print(f"✓ 预设 '{preset_name}' 已应用") + else: + print("✗ 预设管理器不可用") + except Exception as e: + print(f"✗ 预设应用失败: {e}") + import traceback + traceback.print_exc() + + def save_current_as_preset(self, preset_name: str, description: str = '') -> str: + """ + 将当前配置保存为预设 + + Args: + preset_name: 预设名称 + description: 预设描述 + + Returns: + 预设ID + """ + try: + if self.plugin.preset_manager: + preset_id = self.plugin.preset_manager.create_terrain_preset_from_current(preset_name, description) + print(f"✓ 当前配置已保存为预设: {preset_name}") + return preset_id + else: + print("✗ 预设管理器不可用") + return '' + except Exception as e: + print(f"✗ 预设保存失败: {e}") + import traceback + traceback.print_exc() return '' \ No newline at end of file diff --git a/plugins/user/weather_season_system/effects/environment_effects.py b/plugins/user/weather_season_system/effects/environment_effects.py index d43c5c0d..db7a98db 100644 --- a/plugins/user/weather_season_system/effects/environment_effects.py +++ b/plugins/user/weather_season_system/effects/environment_effects.py @@ -1,596 +1,596 @@ -""" -环境效果系统 -负责管理天气和季节对环境的影响效果 -""" - -import time -from typing import Dict, Any, Tuple -import math - -class EnvironmentEffects: - """ - 环境效果系统 - 负责管理天气和季节对环境的影响,包括光照、雾效、风效等 - """ - - def __init__(self, plugin): - """ - 初始化环境效果系统 - - Args: - plugin: 天气和季节系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 光照效果配置 - self.lighting_config = { - 'sun_color': (1.0, 1.0, 1.0), - 'sun_intensity': 1.0, - 'ambient_color': (0.3, 0.3, 0.3), - 'ambient_intensity': 0.3, - 'shadow_strength': 1.0, - 'light_temperature': 6500.0 # 色温(K) - } - - # 雾效配置 - self.fog_config = { - 'fog_color': (0.5, 0.5, 0.5), - 'fog_density': 0.0, - 'fog_start': 100.0, - 'fog_end': 1000.0, - 'fog_mode': 'linear' # 'linear', 'exp', 'exp2' - } - - # 风效配置 - self.wind_config = { - 'wind_direction': (1.0, 0.0, 0.0), - 'wind_speed': 0.0, - 'wind_turbulence': 0.1, - 'wind_gust_strength': 0.0, - 'wind_gust_frequency': 0.1 - } - - # 后处理效果配置 - self.post_processing_config = { - 'exposure': 1.0, - 'saturation': 1.0, - 'contrast': 1.0, - 'brightness': 0.0, - 'bloom_intensity': 0.5, - 'lens_flare': 0.0 - } - - # 时间配置 - self.time_config = { - 'day_time': 12.0, # 当前时间(小时) - 'day_speed': 1.0, # 时间流逝速度 - 'latitude': 0.0, # 纬度(影响日照) - 'longitude': 0.0 # 经度 - } - - # 当前效果状态 - self.current_effects = { - 'lighting': self.lighting_config.copy(), - 'fog': self.fog_config.copy(), - 'wind': self.wind_config.copy(), - 'post_processing': self.post_processing_config.copy() - } - - # 效果更新配置 - self.update_interval = 0.1 # 更新间隔(秒) - self.last_update_time = 0.0 - - # 统计信息 - self.stats = { - 'effects_updated': 0, - 'lighting_updates': 0, - 'fog_updates': 0, - 'wind_updates': 0 - } - - print("✓ 环境效果系统已创建") - - def initialize(self) -> bool: - """ - 初始化环境效果系统 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 环境效果系统初始化完成") - return True - - except Exception as e: - print(f"✗ 环境效果系统初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用环境效果系统 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 环境效果系统未初始化") - return False - - self.enabled = True - print("✓ 环境效果系统已启用") - return True - - except Exception as e: - print(f"✗ 环境效果系统启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用环境效果系统""" - try: - self.enabled = False - print("✓ 环境效果系统已禁用") - - except Exception as e: - print(f"✗ 环境效果系统禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理环境效果系统资源""" - try: - self.disable() - self.initialized = False - print("✓ 环境效果系统资源已清理") - - except Exception as e: - print(f"✗ 环境效果系统资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新环境效果系统状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 检查是否需要更新 - self.last_update_time += dt - if self.last_update_time < self.update_interval: - return - - # 重置更新计时器 - self.last_update_time = 0.0 - - # 更新各种环境效果 - self._update_lighting_effects() - self._update_fog_effects() - self._update_wind_effects() - self._update_post_processing_effects() - - # 更新统计信息 - self.stats['effects_updated'] += 1 - - except Exception as e: - print(f"✗ 环境效果系统更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_lighting_effects(self): - """更新光照效果""" - try: - # 获取当前环境参数 - if self.plugin.weather_manager: - env_params = self.plugin.weather_manager.get_current_environment_params() - else: - env_params = { - 'temperature': 20.0, - 'humidity': 50.0, - 'wind_speed': 3.0, - 'visibility': 1.0 - } - - # 获取当前季节参数 - if self.plugin.season_manager: - season_params = self.plugin.season_manager.get_interpolated_season_params() - else: - season_params = { - 'temperature': 15.0, - 'humidity': 60.0, - 'day_length': 12.5, - 'color_tint': (1.0, 1.0, 0.8) - } - - # 获取当前天气参数 - if self.plugin.weather_manager: - weather_params = self.plugin.weather_manager.get_interpolated_weather_params() - else: - weather_params = { - 'cloud_cover': 0.1, - 'light_intensity': 1.0, - 'temperature_modifier': 0.0, - 'humidity_modifier': 0.0 - } - - # 计算太阳颜色(基于时间、天气和季节) - sun_color = self._calculate_sun_color( - self.time_config['day_time'], - weather_params['cloud_cover'], - season_params['color_tint'] - ) - - # 计算光照强度(基于时间、天气) - sun_intensity = self._calculate_sun_intensity( - self.time_config['day_time'], - weather_params['light_intensity'], - season_params['day_length'] - ) - - # 计算环境光 - ambient_intensity = 0.3 * weather_params['light_intensity'] - - # 更新光照配置 - self.current_effects['lighting']['sun_color'] = sun_color - self.current_effects['lighting']['sun_intensity'] = sun_intensity - self.current_effects['lighting']['ambient_intensity'] = ambient_intensity - - # 更新统计信息 - self.stats['lighting_updates'] += 1 - - except Exception as e: - print(f"✗ 光照效果更新失败: {e}") - - def _calculate_sun_color(self, time_of_day: float, cloud_cover: float, season_tint: Tuple[float, float, float]) -> Tuple[float, float, float]: - """ - 计算太阳颜色 - - Args: - time_of_day: 当前时间(小时) - cloud_cover: 云层覆盖率 - season_tint: 季节色调 - - Returns: - 太阳颜色(RGB) - """ - try: - # 基础太阳颜色(根据时间变化) - if 6 <= time_of_day <= 18: # 白天 - # 正午最白,早晚偏黄 - hour_offset = abs(time_of_day - 12) - warmth = max(0.0, 1.0 - hour_offset / 6.0) # 12点最暖(1.0),6点和18点最冷(0.0) - base_color = ( - 1.0, - 0.9 + 0.1 * warmth, # 绿色分量 - 0.8 + 0.2 * warmth # 蓝色分量 - ) - else: # 夜晚 - base_color = (0.3, 0.3, 0.5) # 夜晚偏蓝 - - # 应用云层影响 - cloud_factor = 1.0 - cloud_cover * 0.7 # 云层使颜色变暗 - colored_sun = tuple(c * cloud_factor for c in base_color) - - # 应用季节色调 - final_color = tuple(colored_sun[i] * season_tint[i] for i in range(3)) - - return final_color - - except Exception as e: - print(f"✗ 太阳颜色计算失败: {e}") - return (1.0, 1.0, 1.0) - - def _calculate_sun_intensity(self, time_of_day: float, weather_intensity: float, day_length: float) -> float: - """ - 计算太阳强度 - - Args: - time_of_day: 当前时间(小时) - weather_intensity: 天气光照强度 - day_length: 日照时长(小时) - - Returns: - 太阳强度 - """ - try: - # 计算日出和日落时间 - sunrise = 12.0 - day_length / 2.0 - sunset = 12.0 + day_length / 2.0 - - # 夜晚强度为0 - if time_of_day < sunrise or time_of_day > sunset: - return 0.0 - - # 白天计算强度(正弦曲线) - day_progress = (time_of_day - sunrise) / (sunset - sunrise) - angle = day_progress * math.pi - base_intensity = math.sin(angle) - - # 应用天气影响 - final_intensity = base_intensity * weather_intensity - - return max(0.0, final_intensity) - - except Exception as e: - print(f"✗ 太阳强度计算失败: {e}") - return 1.0 - - def _update_fog_effects(self): - """更新雾效""" - try: - # 获取当前天气参数 - if self.plugin.weather_manager: - weather_params = self.plugin.weather_manager.get_interpolated_weather_params() - else: - weather_params = { - 'visibility': 1.0, - 'precipitation': 0.0 - } - - # 根据能见度计算雾密度 - visibility = weather_params['visibility'] - fog_density = (1.0 - visibility) * 0.1 - - # 根据降水增加雾密度 - precipitation = weather_params['precipitation'] - fog_density += precipitation * 0.05 - - # 更新雾效配置 - self.current_effects['fog']['fog_density'] = fog_density - - # 根据能见度调整雾的起始和结束距离 - self.current_effects['fog']['fog_start'] = 50.0 * visibility - self.current_effects['fog']['fog_end'] = 500.0 * visibility - - # 更新统计信息 - self.stats['fog_updates'] += 1 - - except Exception as e: - print(f"✗ 雾效更新失败: {e}") - - def _update_wind_effects(self): - """更新风效""" - try: - # 获取当前环境参数 - if self.plugin.weather_manager: - env_params = self.plugin.weather_manager.get_current_environment_params() - else: - env_params = { - 'wind_speed': 3.0 - } - - # 获取当前天气参数 - if self.plugin.weather_manager: - weather_params = self.plugin.weather_manager.get_interpolated_weather_params() - else: - weather_params = { - 'wind_speed': 2.0 - } - - # 结合环境和天气风速 - wind_speed = env_params['wind_speed'] * weather_params['wind_speed'] / 2.0 - - # 添加一些随机扰动 - turbulence = 0.1 + random.uniform(-0.05, 0.05) - - # 更新风效配置 - self.current_effects['wind']['wind_speed'] = wind_speed - self.current_effects['wind']['wind_turbulence'] = turbulence - - # 更新统计信息 - self.stats['wind_updates'] += 1 - - except Exception as e: - print(f"✗ 风效更新失败: {e}") - - def _update_post_processing_effects(self): - """更新后处理效果""" - try: - # 获取当前天气参数 - if self.plugin.weather_manager: - weather_params = self.plugin.weather_manager.get_interpolated_weather_params() - else: - weather_params = { - 'light_intensity': 1.0, - 'visibility': 1.0 - } - - # 获取当前季节参数 - if self.plugin.season_manager: - season_params = self.plugin.season_manager.get_interpolated_season_params() - else: - season_params = { - 'color_tint': (1.0, 1.0, 0.8) - } - - # 根据光照强度调整曝光 - light_intensity = weather_params['light_intensity'] - exposure = 0.5 + 0.5 * light_intensity - - # 根据能见度调整对比度和饱和度 - visibility = weather_params['visibility'] - contrast = 0.8 + 0.2 * visibility - saturation = 0.7 + 0.3 * visibility - - # 根据季节色调调整颜色 - color_tint = season_params['color_tint'] - - # 更新后处理配置 - self.current_effects['post_processing']['exposure'] = exposure - self.current_effects['post_processing']['contrast'] = contrast - self.current_effects['post_processing']['saturation'] = saturation - - # 根据天气类型调整辉光效果 - current_weather = self.plugin.weather_manager.get_current_weather() if self.plugin.weather_manager else 'clear' - if current_weather in ['clear', 'cloudy']: - bloom = 0.5 * light_intensity - else: - bloom = 0.2 * light_intensity - - self.current_effects['post_processing']['bloom_intensity'] = bloom - - except Exception as e: - print(f"✗ 后处理效果更新失败: {e}") - - def get_current_effects(self) -> Dict[str, Any]: - """ - 获取当前环境效果 - - Returns: - 当前环境效果字典 - """ - return self.current_effects.copy() - - def get_lighting_effects(self) -> Dict[str, Any]: - """ - 获取当前光照效果 - - Returns: - 当前光照效果字典 - """ - return self.current_effects['lighting'].copy() - - def get_fog_effects(self) -> Dict[str, Any]: - """ - 获取当前雾效 - - Returns: - 当前雾效字典 - """ - return self.current_effects['fog'].copy() - - def get_wind_effects(self) -> Dict[str, Any]: - """ - 获取当前风效 - - Returns: - 当前风效字典 - """ - return self.current_effects['wind'].copy() - - def get_post_processing_effects(self) -> Dict[str, Any]: - """ - 获取当前后处理效果 - - Returns: - 当前后处理效果字典 - """ - return self.current_effects['post_processing'].copy() - - def set_time(self, hour: float): - """ - 设置当前时间 - - Args: - hour: 小时(0-24) - """ - self.time_config['day_time'] = max(0.0, min(24.0, hour)) - print(f"✓ 当前时间设置为: {hour:.1f} 小时") - - def set_day_speed(self, speed: float): - """ - 设置时间流逝速度 - - Args: - speed: 时间流逝速度倍数 - """ - self.time_config['day_speed'] = max(0.0, speed) - print(f"✓ 时间流逝速度设置为: {speed}x") - - def set_location(self, latitude: float, longitude: float): - """ - 设置地理位置 - - Args: - latitude: 纬度 - longitude: 经度 - """ - self.time_config['latitude'] = latitude - self.time_config['longitude'] = longitude - print(f"✓ 地理位置设置为: ({latitude}, {longitude})") - - def apply_immediate_effects(self): - """立即应用所有环境效果""" - try: - self._update_lighting_effects() - self._update_fog_effects() - self._update_wind_effects() - self._update_post_processing_effects() - print("✓ 环境效果已立即更新") - except Exception as e: - print(f"✗ 环境效果立即应用失败: {e}") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'effects_updated': 0, - 'lighting_updates': 0, - 'fog_updates': 0, - 'wind_updates': 0 - } - print("✓ 环境效果系统统计信息已重置") - - def set_update_interval(self, interval: float): - """ - 设置更新间隔 - - Args: - interval: 更新间隔(秒) - """ - self.update_interval = max(0.01, interval) - print(f"✓ 环境效果更新间隔设置为: {interval} 秒") - - def get_effect_parameters(self, effect_type: str) -> Dict[str, Any]: - """ - 获取特定效果类型的参数 - - Args: - effect_type: 效果类型('lighting', 'fog', 'wind', 'post_processing') - - Returns: - 效果参数字典 - """ - if effect_type in self.current_effects: - return self.current_effects[effect_type].copy() - return {} - - def set_effect_parameter(self, effect_type: str, parameter: str, value: Any): - """ - 设置特定效果参数 - - Args: - effect_type: 效果类型 - parameter: 参数名称 - value: 参数值 - """ - try: - if effect_type in self.current_effects: - if parameter in self.current_effects[effect_type]: - self.current_effects[effect_type][parameter] = value - print(f"✓ {effect_type}.{parameter} 设置为: {value}") - else: - print(f"✗ 无效的参数: {parameter}") - else: - print(f"✗ 无效的效果类型: {effect_type}") - except Exception as e: +""" +环境效果系统 +负责管理天气和季节对环境的影响效果 +""" + +import time +from typing import Dict, Any, Tuple +import math + +class EnvironmentEffects: + """ + 环境效果系统 + 负责管理天气和季节对环境的影响,包括光照、雾效、风效等 + """ + + def __init__(self, plugin): + """ + 初始化环境效果系统 + + Args: + plugin: 天气和季节系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 光照效果配置 + self.lighting_config = { + 'sun_color': (1.0, 1.0, 1.0), + 'sun_intensity': 1.0, + 'ambient_color': (0.3, 0.3, 0.3), + 'ambient_intensity': 0.3, + 'shadow_strength': 1.0, + 'light_temperature': 6500.0 # 色温(K) + } + + # 雾效配置 + self.fog_config = { + 'fog_color': (0.5, 0.5, 0.5), + 'fog_density': 0.0, + 'fog_start': 100.0, + 'fog_end': 1000.0, + 'fog_mode': 'linear' # 'linear', 'exp', 'exp2' + } + + # 风效配置 + self.wind_config = { + 'wind_direction': (1.0, 0.0, 0.0), + 'wind_speed': 0.0, + 'wind_turbulence': 0.1, + 'wind_gust_strength': 0.0, + 'wind_gust_frequency': 0.1 + } + + # 后处理效果配置 + self.post_processing_config = { + 'exposure': 1.0, + 'saturation': 1.0, + 'contrast': 1.0, + 'brightness': 0.0, + 'bloom_intensity': 0.5, + 'lens_flare': 0.0 + } + + # 时间配置 + self.time_config = { + 'day_time': 12.0, # 当前时间(小时) + 'day_speed': 1.0, # 时间流逝速度 + 'latitude': 0.0, # 纬度(影响日照) + 'longitude': 0.0 # 经度 + } + + # 当前效果状态 + self.current_effects = { + 'lighting': self.lighting_config.copy(), + 'fog': self.fog_config.copy(), + 'wind': self.wind_config.copy(), + 'post_processing': self.post_processing_config.copy() + } + + # 效果更新配置 + self.update_interval = 0.1 # 更新间隔(秒) + self.last_update_time = 0.0 + + # 统计信息 + self.stats = { + 'effects_updated': 0, + 'lighting_updates': 0, + 'fog_updates': 0, + 'wind_updates': 0 + } + + print("✓ 环境效果系统已创建") + + def initialize(self) -> bool: + """ + 初始化环境效果系统 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 环境效果系统初始化完成") + return True + + except Exception as e: + print(f"✗ 环境效果系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用环境效果系统 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 环境效果系统未初始化") + return False + + self.enabled = True + print("✓ 环境效果系统已启用") + return True + + except Exception as e: + print(f"✗ 环境效果系统启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用环境效果系统""" + try: + self.enabled = False + print("✓ 环境效果系统已禁用") + + except Exception as e: + print(f"✗ 环境效果系统禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理环境效果系统资源""" + try: + self.disable() + self.initialized = False + print("✓ 环境效果系统资源已清理") + + except Exception as e: + print(f"✗ 环境效果系统资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新环境效果系统状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 检查是否需要更新 + self.last_update_time += dt + if self.last_update_time < self.update_interval: + return + + # 重置更新计时器 + self.last_update_time = 0.0 + + # 更新各种环境效果 + self._update_lighting_effects() + self._update_fog_effects() + self._update_wind_effects() + self._update_post_processing_effects() + + # 更新统计信息 + self.stats['effects_updated'] += 1 + + except Exception as e: + print(f"✗ 环境效果系统更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_lighting_effects(self): + """更新光照效果""" + try: + # 获取当前环境参数 + if self.plugin.weather_manager: + env_params = self.plugin.weather_manager.get_current_environment_params() + else: + env_params = { + 'temperature': 20.0, + 'humidity': 50.0, + 'wind_speed': 3.0, + 'visibility': 1.0 + } + + # 获取当前季节参数 + if self.plugin.season_manager: + season_params = self.plugin.season_manager.get_interpolated_season_params() + else: + season_params = { + 'temperature': 15.0, + 'humidity': 60.0, + 'day_length': 12.5, + 'color_tint': (1.0, 1.0, 0.8) + } + + # 获取当前天气参数 + if self.plugin.weather_manager: + weather_params = self.plugin.weather_manager.get_interpolated_weather_params() + else: + weather_params = { + 'cloud_cover': 0.1, + 'light_intensity': 1.0, + 'temperature_modifier': 0.0, + 'humidity_modifier': 0.0 + } + + # 计算太阳颜色(基于时间、天气和季节) + sun_color = self._calculate_sun_color( + self.time_config['day_time'], + weather_params['cloud_cover'], + season_params['color_tint'] + ) + + # 计算光照强度(基于时间、天气) + sun_intensity = self._calculate_sun_intensity( + self.time_config['day_time'], + weather_params['light_intensity'], + season_params['day_length'] + ) + + # 计算环境光 + ambient_intensity = 0.3 * weather_params['light_intensity'] + + # 更新光照配置 + self.current_effects['lighting']['sun_color'] = sun_color + self.current_effects['lighting']['sun_intensity'] = sun_intensity + self.current_effects['lighting']['ambient_intensity'] = ambient_intensity + + # 更新统计信息 + self.stats['lighting_updates'] += 1 + + except Exception as e: + print(f"✗ 光照效果更新失败: {e}") + + def _calculate_sun_color(self, time_of_day: float, cloud_cover: float, season_tint: Tuple[float, float, float]) -> Tuple[float, float, float]: + """ + 计算太阳颜色 + + Args: + time_of_day: 当前时间(小时) + cloud_cover: 云层覆盖率 + season_tint: 季节色调 + + Returns: + 太阳颜色(RGB) + """ + try: + # 基础太阳颜色(根据时间变化) + if 6 <= time_of_day <= 18: # 白天 + # 正午最白,早晚偏黄 + hour_offset = abs(time_of_day - 12) + warmth = max(0.0, 1.0 - hour_offset / 6.0) # 12点最暖(1.0),6点和18点最冷(0.0) + base_color = ( + 1.0, + 0.9 + 0.1 * warmth, # 绿色分量 + 0.8 + 0.2 * warmth # 蓝色分量 + ) + else: # 夜晚 + base_color = (0.3, 0.3, 0.5) # 夜晚偏蓝 + + # 应用云层影响 + cloud_factor = 1.0 - cloud_cover * 0.7 # 云层使颜色变暗 + colored_sun = tuple(c * cloud_factor for c in base_color) + + # 应用季节色调 + final_color = tuple(colored_sun[i] * season_tint[i] for i in range(3)) + + return final_color + + except Exception as e: + print(f"✗ 太阳颜色计算失败: {e}") + return (1.0, 1.0, 1.0) + + def _calculate_sun_intensity(self, time_of_day: float, weather_intensity: float, day_length: float) -> float: + """ + 计算太阳强度 + + Args: + time_of_day: 当前时间(小时) + weather_intensity: 天气光照强度 + day_length: 日照时长(小时) + + Returns: + 太阳强度 + """ + try: + # 计算日出和日落时间 + sunrise = 12.0 - day_length / 2.0 + sunset = 12.0 + day_length / 2.0 + + # 夜晚强度为0 + if time_of_day < sunrise or time_of_day > sunset: + return 0.0 + + # 白天计算强度(正弦曲线) + day_progress = (time_of_day - sunrise) / (sunset - sunrise) + angle = day_progress * math.pi + base_intensity = math.sin(angle) + + # 应用天气影响 + final_intensity = base_intensity * weather_intensity + + return max(0.0, final_intensity) + + except Exception as e: + print(f"✗ 太阳强度计算失败: {e}") + return 1.0 + + def _update_fog_effects(self): + """更新雾效""" + try: + # 获取当前天气参数 + if self.plugin.weather_manager: + weather_params = self.plugin.weather_manager.get_interpolated_weather_params() + else: + weather_params = { + 'visibility': 1.0, + 'precipitation': 0.0 + } + + # 根据能见度计算雾密度 + visibility = weather_params['visibility'] + fog_density = (1.0 - visibility) * 0.1 + + # 根据降水增加雾密度 + precipitation = weather_params['precipitation'] + fog_density += precipitation * 0.05 + + # 更新雾效配置 + self.current_effects['fog']['fog_density'] = fog_density + + # 根据能见度调整雾的起始和结束距离 + self.current_effects['fog']['fog_start'] = 50.0 * visibility + self.current_effects['fog']['fog_end'] = 500.0 * visibility + + # 更新统计信息 + self.stats['fog_updates'] += 1 + + except Exception as e: + print(f"✗ 雾效更新失败: {e}") + + def _update_wind_effects(self): + """更新风效""" + try: + # 获取当前环境参数 + if self.plugin.weather_manager: + env_params = self.plugin.weather_manager.get_current_environment_params() + else: + env_params = { + 'wind_speed': 3.0 + } + + # 获取当前天气参数 + if self.plugin.weather_manager: + weather_params = self.plugin.weather_manager.get_interpolated_weather_params() + else: + weather_params = { + 'wind_speed': 2.0 + } + + # 结合环境和天气风速 + wind_speed = env_params['wind_speed'] * weather_params['wind_speed'] / 2.0 + + # 添加一些随机扰动 + turbulence = 0.1 + random.uniform(-0.05, 0.05) + + # 更新风效配置 + self.current_effects['wind']['wind_speed'] = wind_speed + self.current_effects['wind']['wind_turbulence'] = turbulence + + # 更新统计信息 + self.stats['wind_updates'] += 1 + + except Exception as e: + print(f"✗ 风效更新失败: {e}") + + def _update_post_processing_effects(self): + """更新后处理效果""" + try: + # 获取当前天气参数 + if self.plugin.weather_manager: + weather_params = self.plugin.weather_manager.get_interpolated_weather_params() + else: + weather_params = { + 'light_intensity': 1.0, + 'visibility': 1.0 + } + + # 获取当前季节参数 + if self.plugin.season_manager: + season_params = self.plugin.season_manager.get_interpolated_season_params() + else: + season_params = { + 'color_tint': (1.0, 1.0, 0.8) + } + + # 根据光照强度调整曝光 + light_intensity = weather_params['light_intensity'] + exposure = 0.5 + 0.5 * light_intensity + + # 根据能见度调整对比度和饱和度 + visibility = weather_params['visibility'] + contrast = 0.8 + 0.2 * visibility + saturation = 0.7 + 0.3 * visibility + + # 根据季节色调调整颜色 + color_tint = season_params['color_tint'] + + # 更新后处理配置 + self.current_effects['post_processing']['exposure'] = exposure + self.current_effects['post_processing']['contrast'] = contrast + self.current_effects['post_processing']['saturation'] = saturation + + # 根据天气类型调整辉光效果 + current_weather = self.plugin.weather_manager.get_current_weather() if self.plugin.weather_manager else 'clear' + if current_weather in ['clear', 'cloudy']: + bloom = 0.5 * light_intensity + else: + bloom = 0.2 * light_intensity + + self.current_effects['post_processing']['bloom_intensity'] = bloom + + except Exception as e: + print(f"✗ 后处理效果更新失败: {e}") + + def get_current_effects(self) -> Dict[str, Any]: + """ + 获取当前环境效果 + + Returns: + 当前环境效果字典 + """ + return self.current_effects.copy() + + def get_lighting_effects(self) -> Dict[str, Any]: + """ + 获取当前光照效果 + + Returns: + 当前光照效果字典 + """ + return self.current_effects['lighting'].copy() + + def get_fog_effects(self) -> Dict[str, Any]: + """ + 获取当前雾效 + + Returns: + 当前雾效字典 + """ + return self.current_effects['fog'].copy() + + def get_wind_effects(self) -> Dict[str, Any]: + """ + 获取当前风效 + + Returns: + 当前风效字典 + """ + return self.current_effects['wind'].copy() + + def get_post_processing_effects(self) -> Dict[str, Any]: + """ + 获取当前后处理效果 + + Returns: + 当前后处理效果字典 + """ + return self.current_effects['post_processing'].copy() + + def set_time(self, hour: float): + """ + 设置当前时间 + + Args: + hour: 小时(0-24) + """ + self.time_config['day_time'] = max(0.0, min(24.0, hour)) + print(f"✓ 当前时间设置为: {hour:.1f} 小时") + + def set_day_speed(self, speed: float): + """ + 设置时间流逝速度 + + Args: + speed: 时间流逝速度倍数 + """ + self.time_config['day_speed'] = max(0.0, speed) + print(f"✓ 时间流逝速度设置为: {speed}x") + + def set_location(self, latitude: float, longitude: float): + """ + 设置地理位置 + + Args: + latitude: 纬度 + longitude: 经度 + """ + self.time_config['latitude'] = latitude + self.time_config['longitude'] = longitude + print(f"✓ 地理位置设置为: ({latitude}, {longitude})") + + def apply_immediate_effects(self): + """立即应用所有环境效果""" + try: + self._update_lighting_effects() + self._update_fog_effects() + self._update_wind_effects() + self._update_post_processing_effects() + print("✓ 环境效果已立即更新") + except Exception as e: + print(f"✗ 环境效果立即应用失败: {e}") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'effects_updated': 0, + 'lighting_updates': 0, + 'fog_updates': 0, + 'wind_updates': 0 + } + print("✓ 环境效果系统统计信息已重置") + + def set_update_interval(self, interval: float): + """ + 设置更新间隔 + + Args: + interval: 更新间隔(秒) + """ + self.update_interval = max(0.01, interval) + print(f"✓ 环境效果更新间隔设置为: {interval} 秒") + + def get_effect_parameters(self, effect_type: str) -> Dict[str, Any]: + """ + 获取特定效果类型的参数 + + Args: + effect_type: 效果类型('lighting', 'fog', 'wind', 'post_processing') + + Returns: + 效果参数字典 + """ + if effect_type in self.current_effects: + return self.current_effects[effect_type].copy() + return {} + + def set_effect_parameter(self, effect_type: str, parameter: str, value: Any): + """ + 设置特定效果参数 + + Args: + effect_type: 效果类型 + parameter: 参数名称 + value: 参数值 + """ + try: + if effect_type in self.current_effects: + if parameter in self.current_effects[effect_type]: + self.current_effects[effect_type][parameter] = value + print(f"✓ {effect_type}.{parameter} 设置为: {value}") + else: + print(f"✗ 无效的参数: {parameter}") + else: + print(f"✗ 无效的效果类型: {effect_type}") + except Exception as e: print(f"✗ 效果参数设置失败: {e}") \ No newline at end of file diff --git a/plugins/user/weather_season_system/effects/visual_effects.py b/plugins/user/weather_season_system/effects/visual_effects.py index 54908eba..996dc25e 100644 --- a/plugins/user/weather_season_system/effects/visual_effects.py +++ b/plugins/user/weather_season_system/effects/visual_effects.py @@ -1,624 +1,624 @@ -""" -视觉效果系统 -负责管理天气和季节的视觉效果,如粒子效果、光照变化等 -""" - -import time -import random -from typing import Dict, Any, List, Optional -import math - -class VisualEffects: - """ - 视觉效果系统 - 负责管理天气和季节的视觉效果,包括粒子效果、光照变化、材质变化等 - """ - - def __init__(self, plugin): - """ - 初始化视觉效果系统 - - Args: - plugin: 天气和季节系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 粒子效果配置 - self.particle_effects = { - 'rain': { - 'name': '雨滴', - 'particle_count': 1000, - 'particle_size': (0.1, 0.5), - 'particle_speed': (5.0, 15.0), - 'particle_lifetime': (0.5, 2.0), - 'particle_color': (0.7, 0.7, 1.0, 0.8), - 'emission_rate': 100, - 'spread': (100.0, 50.0), # 水平和垂直扩散范围 - 'affected_by_wind': True - }, - 'snow': { - 'name': '雪花', - 'particle_count': 800, - 'particle_size': (0.2, 1.0), - 'particle_speed': (0.5, 3.0), - 'particle_lifetime': (3.0, 8.0), - 'particle_color': (1.0, 1.0, 1.0, 0.9), - 'emission_rate': 80, - 'spread': (100.0, 50.0), - 'affected_by_wind': True - }, - 'storm': { - 'name': '暴风雨', - 'particle_count': 1500, - 'particle_size': (0.1, 0.8), - 'particle_speed': (8.0, 25.0), - 'particle_lifetime': (0.3, 1.5), - 'particle_color': (0.5, 0.5, 0.8, 0.7), - 'emission_rate': 200, - 'spread': (150.0, 80.0), - 'affected_by_wind': True - }, - 'hail': { - 'name': '冰雹', - 'particle_count': 500, - 'particle_size': (0.3, 1.5), - 'particle_speed': (10.0, 20.0), - 'particle_lifetime': (1.0, 3.0), - 'particle_color': (0.9, 0.9, 1.0, 0.9), - 'emission_rate': 50, - 'spread': (80.0, 60.0), - 'affected_by_wind': True - }, - 'light_fog': { - 'name': '薄雾', - 'particle_count': 200, - 'particle_size': (5.0, 15.0), - 'particle_speed': (0.1, 0.5), - 'particle_lifetime': (5.0, 15.0), - 'particle_color': (0.8, 0.8, 0.8, 0.3), - 'emission_rate': 20, - 'spread': (200.0, 100.0), - 'affected_by_wind': True - }, - 'heavy_fog': { - 'name': '浓雾', - 'particle_count': 500, - 'particle_size': (10.0, 30.0), - 'particle_speed': (0.05, 0.3), - 'particle_lifetime': (10.0, 30.0), - 'particle_color': (0.7, 0.7, 0.7, 0.5), - 'emission_rate': 50, - 'spread': (300.0, 150.0), - 'affected_by_wind': True - }, - 'wind_debris': { - 'name': '风中碎片', - 'particle_count': 100, - 'particle_size': (0.5, 2.0), - 'particle_speed': (2.0, 10.0), - 'particle_lifetime': (1.0, 5.0), - 'particle_color': (0.6, 0.5, 0.4, 0.7), - 'emission_rate': 10, - 'spread': (100.0, 30.0), - 'affected_by_wind': True - } - } - - # 当前激活的视觉效果 - self.active_effects = {} - - # 材质变化配置 - self.material_effects = { - 'wet_surface': { - 'name': '湿润表面', - 'roughness_multiplier': 0.7, - 'metallic_multiplier': 1.2, - 'specular_multiplier': 1.5, - 'normal_strength': 0.8 - }, - 'snow_cover': { - 'name': '积雪覆盖', - 'roughness_multiplier': 1.3, - 'metallic_multiplier': 0.8, - 'specular_multiplier': 0.5, - 'normal_strength': 1.2 - } - } - - # 光照效果配置 - self.lighting_effects = { - 'lightning': { - 'name': '闪电', - 'intensity': 5.0, - 'color': (0.8, 0.8, 1.0), - 'duration': 0.1, - 'frequency': 0.05 # 每秒闪电概率 - } - } - - # 视觉效果更新配置 - self.update_interval = 0.05 # 更新间隔(秒) - self.last_update_time = 0.0 - - # 粒子系统配置 - self.particle_system_config = { - 'max_particles': 10000, - 'particle_lod': 'medium', # 'low', 'medium', 'high' - 'distance_fade': True, - 'collision_detection': False - } - - # 统计信息 - self.stats = { - 'effects_activated': 0, - 'particles_generated': 0, - 'effects_deactivated': 0, - 'total_effect_time': 0.0 - } - - print("✓ 视觉效果系统已创建") - - def initialize(self) -> bool: - """ - 初始化视觉效果系统 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 视觉效果系统初始化完成") - return True - - except Exception as e: - print(f"✗ 视觉效果系统初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用视觉效果系统 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 视觉效果系统未初始化") - return False - - self.enabled = True - print("✓ 视觉效果系统已启用") - return True - - except Exception as e: - print(f"✗ 视觉效果系统启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用视觉效果系统""" - try: - self.enabled = False - self._deactivate_all_effects() - print("✓ 视觉效果系统已禁用") - - except Exception as e: - print(f"✗ 视觉效果系统禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理视觉效果系统资源""" - try: - self.disable() - self.initialized = False - print("✓ 视觉效果系统资源已清理") - - except Exception as e: - print(f"✗ 视觉效果系统资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新视觉效果系统状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新计时器 - self.last_update_time += dt - if self.last_update_time < self.update_interval: - return - - # 重置更新计时器 - self.last_update_time = 0.0 - - # 更新激活的视觉效果 - self._update_active_effects(dt) - - # 根据当前天气激活相应的视觉效果 - self._update_weather_effects() - - # 更新统计信息 - self.stats['total_effect_time'] += dt - - except Exception as e: - print(f"✗ 视觉效果系统更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_active_effects(self, dt: float): - """ - 更新激活的视觉效果 - - Args: - dt: 时间增量 - """ - try: - effects_to_remove = [] - - for effect_name, effect_data in self.active_effects.items(): - # 更新效果持续时间 - effect_data['duration'] -= dt - - # 检查效果是否应该结束 - if effect_data['duration'] <= 0 and not effect_data['persistent']: - effects_to_remove.append(effect_name) - - # 移除过期的效果 - for effect_name in effects_to_remove: - self._deactivate_effect(effect_name) - - except Exception as e: - print(f"✗ 激活效果更新失败: {e}") - - def _update_weather_effects(self): - """根据当前天气更新视觉效果""" - try: - if not self.plugin.weather_manager: - return - - current_weather = self.plugin.weather_manager.get_current_weather() - weather_info = self.plugin.weather_manager.get_weather_info(current_weather) - - if not weather_info: - return - - # 获取天气对应的粒子效果 - particle_effect = weather_info.get('particle_effect') - - # 检查是否需要激活或更新粒子效果 - if particle_effect and particle_effect in self.particle_effects: - # 检查效果是否已经激活 - if particle_effect not in self.active_effects: - # 激活粒子效果 - self._activate_particle_effect(particle_effect) - else: - # 更新现有效果的参数 - self._update_particle_effect_parameters(particle_effect) - else: - # 检查是否需要停用粒子效果 - active_particle_effects = [ - name for name in self.active_effects.keys() - if name in self.particle_effects - ] - - for effect_name in active_particle_effects: - self._deactivate_effect(effect_name) - - except Exception as e: - print(f"✗ 天气效果更新失败: {e}") - - def _activate_particle_effect(self, effect_name: str): - """ - 激活粒子效果 - - Args: - effect_name: 粒子效果名称 - """ - try: - if effect_name not in self.particle_effects: - return - - effect_config = self.particle_effects[effect_name] - - # 创建效果数据 - effect_data = { - 'name': effect_config['name'], - 'type': 'particle', - 'config': effect_config, - 'start_time': time.time(), - 'duration': float('inf'), # 持续到手动停用 - 'persistent': True, - 'intensity': 1.0 - } - - # 添加到激活效果列表 - self.active_effects[effect_name] = effect_data - - # 更新统计信息 - self.stats['effects_activated'] += 1 - self.stats['particles_generated'] += effect_config['particle_count'] - - print(f"✓ 粒子效果已激活: {effect_config['name']}") - - except Exception as e: - print(f"✗ 粒子效果激活失败: {e}") - - def _update_particle_effect_parameters(self, effect_name: str): - """ - 更新粒子效果参数 - - Args: - effect_name: 粒子效果名称 - """ - try: - if effect_name not in self.active_effects: - return - - # 在实际实现中,这里会根据当前天气强度等因素更新粒子效果参数 - # 例如:根据降水强度调整雨滴数量和速度 - pass - - except Exception as e: - print(f"✗ 粒子效果参数更新失败: {e}") - - def _deactivate_effect(self, effect_name: str): - """ - 停用视觉效果 - - Args: - effect_name: 效果名称 - """ - try: - if effect_name in self.active_effects: - effect_data = self.active_effects.pop(effect_name) - - # 更新统计信息 - self.stats['effects_deactivated'] += 1 - - print(f"✓ 视觉效果已停用: {effect_data['name']}") - - except Exception as e: - print(f"✗ 视觉效果停用失败: {e}") - - def _deactivate_all_effects(self): - """停用所有视觉效果""" - try: - effect_names = list(self.active_effects.keys()) - for effect_name in effect_names: - self._deactivate_effect(effect_name) - - except Exception as e: - print(f"✗ 所有效果停用失败: {e}") - - def activate_effect(self, effect_name: str, duration: float = float('inf'), intensity: float = 1.0): - """ - 激活视觉效果 - - Args: - effect_name: 效果名称 - duration: 持续时间(秒),默认为无限 - intensity: 效果强度(0-1) - """ - try: - # 检查效果类型 - if effect_name in self.particle_effects: - self._activate_particle_effect(effect_name) - else: - # 创建通用效果数据 - effect_data = { - 'name': effect_name, - 'type': 'generic', - 'start_time': time.time(), - 'duration': duration, - 'persistent': duration == float('inf'), - 'intensity': max(0.0, min(1.0, intensity)) - } - - self.active_effects[effect_name] = effect_data - self.stats['effects_activated'] += 1 - print(f"✓ 视觉效果已激活: {effect_name}") - - except Exception as e: - print(f"✗ 视觉效果激活失败: {e}") - - def deactivate_effect(self, effect_name: str): - """ - 停用视觉效果 - - Args: - effect_name: 效果名称 - """ - self._deactivate_effect(effect_name) - - def set_effect_intensity(self, effect_name: str, intensity: float): - """ - 设置效果强度 - - Args: - effect_name: 效果名称 - intensity: 效果强度(0-1) - """ - try: - if effect_name in self.active_effects: - self.active_effects[effect_name]['intensity'] = max(0.0, min(1.0, intensity)) - print(f"✓ 效果强度已设置: {effect_name} = {intensity}") - else: - print(f"✗ 效果未激活: {effect_name}") - - except Exception as e: - print(f"✗ 效果强度设置失败: {e}") - - def get_active_effects(self) -> List[str]: - """ - 获取激活的视觉效果列表 - - Returns: - 激活的效果名称列表 - """ - return list(self.active_effects.keys()) - - def get_effect_info(self, effect_name: str) -> Optional[Dict[str, Any]]: - """ - 获取效果信息 - - Args: - effect_name: 效果名称 - - Returns: - 效果信息字典或None - """ - try: - # 检查粒子效果 - if effect_name in self.particle_effects: - return self.particle_effects[effect_name].copy() - - # 检查激活的效果 - if effect_name in self.active_effects: - return self.active_effects[effect_name].copy() - - return None - - except Exception as e: - print(f"✗ 效果信息获取失败: {e}") - return None - - def get_particle_effects_list(self) -> List[str]: - """ - 获取所有粒子效果列表 - - Returns: - 粒子效果名称列表 - """ - return list(self.particle_effects.keys()) - - def get_material_effects_list(self) -> List[str]: - """ - 获取所有材质效果列表 - - Returns: - 材质效果名称列表 - """ - return list(self.material_effects.keys()) - - def apply_material_effect(self, material_name: str, effect_name: str): - """ - 应用材质效果 - - Args: - material_name: 材质名称 - effect_name: 效果名称 - """ - try: - if effect_name in self.material_effects: - effect_config = self.material_effects[effect_name] - # 在实际实现中,这里会应用材质效果到指定材质 - print(f"✓ 材质效果已应用: {material_name} + {effect_config['name']}") - else: - print(f"✗ 无效的材质效果: {effect_name}") - - except Exception as e: - print(f"✗ 材质效果应用失败: {e}") - - def remove_material_effect(self, material_name: str, effect_name: str): - """ - 移除材质效果 - - Args: - material_name: 材质名称 - effect_name: 效果名称 - """ - try: - # 在实际实现中,这里会从指定材质移除效果 - print(f"✓ 材质效果已移除: {material_name} - {effect_name}") - except Exception as e: - print(f"✗ 材质效果移除失败: {e}") - - def trigger_lighting_effect(self, effect_name: str): - """ - 触发瞬时光照效果 - - Args: - effect_name: 光照效果名称 - """ - try: - if effect_name in self.lighting_effects: - effect_config = self.lighting_effects[effect_name] - # 在实际实现中,这里会触发瞬时光照效果 - print(f"✓ 光照效果已触发: {effect_config['name']}") - else: - print(f"✗ 无效的光照效果: {effect_name}") - - except Exception as e: - print(f"✗ 光照效果触发失败: {e}") - - def set_particle_system_config(self, config: Dict[str, Any]): - """ - 设置粒子系统配置 - - Args: - config: 配置字典 - """ - self.particle_system_config.update(config) - print(f"✓ 粒子系统配置已更新: {self.particle_system_config}") - - def get_particle_system_config(self) -> Dict[str, Any]: - """ - 获取粒子系统配置 - - Returns: - 粒子系统配置字典 - """ - return self.particle_system_config.copy() - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'effects_activated': 0, - 'particles_generated': 0, - 'effects_deactivated': 0, - 'total_effect_time': 0.0 - } - print("✓ 视觉效果系统统计信息已重置") - - def set_update_interval(self, interval: float): - """ - 设置更新间隔 - - Args: - interval: 更新间隔(秒) - """ - self.update_interval = max(0.01, interval) - print(f"✓ 视觉效果更新间隔设置为: {interval} 秒") - - def is_effect_active(self, effect_name: str) -> bool: - """ - 检查效果是否激活 - - Args: - effect_name: 效果名称 - - Returns: - 效果是否激活 - """ +""" +视觉效果系统 +负责管理天气和季节的视觉效果,如粒子效果、光照变化等 +""" + +import time +import random +from typing import Dict, Any, List, Optional +import math + +class VisualEffects: + """ + 视觉效果系统 + 负责管理天气和季节的视觉效果,包括粒子效果、光照变化、材质变化等 + """ + + def __init__(self, plugin): + """ + 初始化视觉效果系统 + + Args: + plugin: 天气和季节系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 粒子效果配置 + self.particle_effects = { + 'rain': { + 'name': '雨滴', + 'particle_count': 1000, + 'particle_size': (0.1, 0.5), + 'particle_speed': (5.0, 15.0), + 'particle_lifetime': (0.5, 2.0), + 'particle_color': (0.7, 0.7, 1.0, 0.8), + 'emission_rate': 100, + 'spread': (100.0, 50.0), # 水平和垂直扩散范围 + 'affected_by_wind': True + }, + 'snow': { + 'name': '雪花', + 'particle_count': 800, + 'particle_size': (0.2, 1.0), + 'particle_speed': (0.5, 3.0), + 'particle_lifetime': (3.0, 8.0), + 'particle_color': (1.0, 1.0, 1.0, 0.9), + 'emission_rate': 80, + 'spread': (100.0, 50.0), + 'affected_by_wind': True + }, + 'storm': { + 'name': '暴风雨', + 'particle_count': 1500, + 'particle_size': (0.1, 0.8), + 'particle_speed': (8.0, 25.0), + 'particle_lifetime': (0.3, 1.5), + 'particle_color': (0.5, 0.5, 0.8, 0.7), + 'emission_rate': 200, + 'spread': (150.0, 80.0), + 'affected_by_wind': True + }, + 'hail': { + 'name': '冰雹', + 'particle_count': 500, + 'particle_size': (0.3, 1.5), + 'particle_speed': (10.0, 20.0), + 'particle_lifetime': (1.0, 3.0), + 'particle_color': (0.9, 0.9, 1.0, 0.9), + 'emission_rate': 50, + 'spread': (80.0, 60.0), + 'affected_by_wind': True + }, + 'light_fog': { + 'name': '薄雾', + 'particle_count': 200, + 'particle_size': (5.0, 15.0), + 'particle_speed': (0.1, 0.5), + 'particle_lifetime': (5.0, 15.0), + 'particle_color': (0.8, 0.8, 0.8, 0.3), + 'emission_rate': 20, + 'spread': (200.0, 100.0), + 'affected_by_wind': True + }, + 'heavy_fog': { + 'name': '浓雾', + 'particle_count': 500, + 'particle_size': (10.0, 30.0), + 'particle_speed': (0.05, 0.3), + 'particle_lifetime': (10.0, 30.0), + 'particle_color': (0.7, 0.7, 0.7, 0.5), + 'emission_rate': 50, + 'spread': (300.0, 150.0), + 'affected_by_wind': True + }, + 'wind_debris': { + 'name': '风中碎片', + 'particle_count': 100, + 'particle_size': (0.5, 2.0), + 'particle_speed': (2.0, 10.0), + 'particle_lifetime': (1.0, 5.0), + 'particle_color': (0.6, 0.5, 0.4, 0.7), + 'emission_rate': 10, + 'spread': (100.0, 30.0), + 'affected_by_wind': True + } + } + + # 当前激活的视觉效果 + self.active_effects = {} + + # 材质变化配置 + self.material_effects = { + 'wet_surface': { + 'name': '湿润表面', + 'roughness_multiplier': 0.7, + 'metallic_multiplier': 1.2, + 'specular_multiplier': 1.5, + 'normal_strength': 0.8 + }, + 'snow_cover': { + 'name': '积雪覆盖', + 'roughness_multiplier': 1.3, + 'metallic_multiplier': 0.8, + 'specular_multiplier': 0.5, + 'normal_strength': 1.2 + } + } + + # 光照效果配置 + self.lighting_effects = { + 'lightning': { + 'name': '闪电', + 'intensity': 5.0, + 'color': (0.8, 0.8, 1.0), + 'duration': 0.1, + 'frequency': 0.05 # 每秒闪电概率 + } + } + + # 视觉效果更新配置 + self.update_interval = 0.05 # 更新间隔(秒) + self.last_update_time = 0.0 + + # 粒子系统配置 + self.particle_system_config = { + 'max_particles': 10000, + 'particle_lod': 'medium', # 'low', 'medium', 'high' + 'distance_fade': True, + 'collision_detection': False + } + + # 统计信息 + self.stats = { + 'effects_activated': 0, + 'particles_generated': 0, + 'effects_deactivated': 0, + 'total_effect_time': 0.0 + } + + print("✓ 视觉效果系统已创建") + + def initialize(self) -> bool: + """ + 初始化视觉效果系统 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 视觉效果系统初始化完成") + return True + + except Exception as e: + print(f"✗ 视觉效果系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用视觉效果系统 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 视觉效果系统未初始化") + return False + + self.enabled = True + print("✓ 视觉效果系统已启用") + return True + + except Exception as e: + print(f"✗ 视觉效果系统启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用视觉效果系统""" + try: + self.enabled = False + self._deactivate_all_effects() + print("✓ 视觉效果系统已禁用") + + except Exception as e: + print(f"✗ 视觉效果系统禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理视觉效果系统资源""" + try: + self.disable() + self.initialized = False + print("✓ 视觉效果系统资源已清理") + + except Exception as e: + print(f"✗ 视觉效果系统资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新视觉效果系统状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新计时器 + self.last_update_time += dt + if self.last_update_time < self.update_interval: + return + + # 重置更新计时器 + self.last_update_time = 0.0 + + # 更新激活的视觉效果 + self._update_active_effects(dt) + + # 根据当前天气激活相应的视觉效果 + self._update_weather_effects() + + # 更新统计信息 + self.stats['total_effect_time'] += dt + + except Exception as e: + print(f"✗ 视觉效果系统更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_active_effects(self, dt: float): + """ + 更新激活的视觉效果 + + Args: + dt: 时间增量 + """ + try: + effects_to_remove = [] + + for effect_name, effect_data in self.active_effects.items(): + # 更新效果持续时间 + effect_data['duration'] -= dt + + # 检查效果是否应该结束 + if effect_data['duration'] <= 0 and not effect_data['persistent']: + effects_to_remove.append(effect_name) + + # 移除过期的效果 + for effect_name in effects_to_remove: + self._deactivate_effect(effect_name) + + except Exception as e: + print(f"✗ 激活效果更新失败: {e}") + + def _update_weather_effects(self): + """根据当前天气更新视觉效果""" + try: + if not self.plugin.weather_manager: + return + + current_weather = self.plugin.weather_manager.get_current_weather() + weather_info = self.plugin.weather_manager.get_weather_info(current_weather) + + if not weather_info: + return + + # 获取天气对应的粒子效果 + particle_effect = weather_info.get('particle_effect') + + # 检查是否需要激活或更新粒子效果 + if particle_effect and particle_effect in self.particle_effects: + # 检查效果是否已经激活 + if particle_effect not in self.active_effects: + # 激活粒子效果 + self._activate_particle_effect(particle_effect) + else: + # 更新现有效果的参数 + self._update_particle_effect_parameters(particle_effect) + else: + # 检查是否需要停用粒子效果 + active_particle_effects = [ + name for name in self.active_effects.keys() + if name in self.particle_effects + ] + + for effect_name in active_particle_effects: + self._deactivate_effect(effect_name) + + except Exception as e: + print(f"✗ 天气效果更新失败: {e}") + + def _activate_particle_effect(self, effect_name: str): + """ + 激活粒子效果 + + Args: + effect_name: 粒子效果名称 + """ + try: + if effect_name not in self.particle_effects: + return + + effect_config = self.particle_effects[effect_name] + + # 创建效果数据 + effect_data = { + 'name': effect_config['name'], + 'type': 'particle', + 'config': effect_config, + 'start_time': time.time(), + 'duration': float('inf'), # 持续到手动停用 + 'persistent': True, + 'intensity': 1.0 + } + + # 添加到激活效果列表 + self.active_effects[effect_name] = effect_data + + # 更新统计信息 + self.stats['effects_activated'] += 1 + self.stats['particles_generated'] += effect_config['particle_count'] + + print(f"✓ 粒子效果已激活: {effect_config['name']}") + + except Exception as e: + print(f"✗ 粒子效果激活失败: {e}") + + def _update_particle_effect_parameters(self, effect_name: str): + """ + 更新粒子效果参数 + + Args: + effect_name: 粒子效果名称 + """ + try: + if effect_name not in self.active_effects: + return + + # 在实际实现中,这里会根据当前天气强度等因素更新粒子效果参数 + # 例如:根据降水强度调整雨滴数量和速度 + pass + + except Exception as e: + print(f"✗ 粒子效果参数更新失败: {e}") + + def _deactivate_effect(self, effect_name: str): + """ + 停用视觉效果 + + Args: + effect_name: 效果名称 + """ + try: + if effect_name in self.active_effects: + effect_data = self.active_effects.pop(effect_name) + + # 更新统计信息 + self.stats['effects_deactivated'] += 1 + + print(f"✓ 视觉效果已停用: {effect_data['name']}") + + except Exception as e: + print(f"✗ 视觉效果停用失败: {e}") + + def _deactivate_all_effects(self): + """停用所有视觉效果""" + try: + effect_names = list(self.active_effects.keys()) + for effect_name in effect_names: + self._deactivate_effect(effect_name) + + except Exception as e: + print(f"✗ 所有效果停用失败: {e}") + + def activate_effect(self, effect_name: str, duration: float = float('inf'), intensity: float = 1.0): + """ + 激活视觉效果 + + Args: + effect_name: 效果名称 + duration: 持续时间(秒),默认为无限 + intensity: 效果强度(0-1) + """ + try: + # 检查效果类型 + if effect_name in self.particle_effects: + self._activate_particle_effect(effect_name) + else: + # 创建通用效果数据 + effect_data = { + 'name': effect_name, + 'type': 'generic', + 'start_time': time.time(), + 'duration': duration, + 'persistent': duration == float('inf'), + 'intensity': max(0.0, min(1.0, intensity)) + } + + self.active_effects[effect_name] = effect_data + self.stats['effects_activated'] += 1 + print(f"✓ 视觉效果已激活: {effect_name}") + + except Exception as e: + print(f"✗ 视觉效果激活失败: {e}") + + def deactivate_effect(self, effect_name: str): + """ + 停用视觉效果 + + Args: + effect_name: 效果名称 + """ + self._deactivate_effect(effect_name) + + def set_effect_intensity(self, effect_name: str, intensity: float): + """ + 设置效果强度 + + Args: + effect_name: 效果名称 + intensity: 效果强度(0-1) + """ + try: + if effect_name in self.active_effects: + self.active_effects[effect_name]['intensity'] = max(0.0, min(1.0, intensity)) + print(f"✓ 效果强度已设置: {effect_name} = {intensity}") + else: + print(f"✗ 效果未激活: {effect_name}") + + except Exception as e: + print(f"✗ 效果强度设置失败: {e}") + + def get_active_effects(self) -> List[str]: + """ + 获取激活的视觉效果列表 + + Returns: + 激活的效果名称列表 + """ + return list(self.active_effects.keys()) + + def get_effect_info(self, effect_name: str) -> Optional[Dict[str, Any]]: + """ + 获取效果信息 + + Args: + effect_name: 效果名称 + + Returns: + 效果信息字典或None + """ + try: + # 检查粒子效果 + if effect_name in self.particle_effects: + return self.particle_effects[effect_name].copy() + + # 检查激活的效果 + if effect_name in self.active_effects: + return self.active_effects[effect_name].copy() + + return None + + except Exception as e: + print(f"✗ 效果信息获取失败: {e}") + return None + + def get_particle_effects_list(self) -> List[str]: + """ + 获取所有粒子效果列表 + + Returns: + 粒子效果名称列表 + """ + return list(self.particle_effects.keys()) + + def get_material_effects_list(self) -> List[str]: + """ + 获取所有材质效果列表 + + Returns: + 材质效果名称列表 + """ + return list(self.material_effects.keys()) + + def apply_material_effect(self, material_name: str, effect_name: str): + """ + 应用材质效果 + + Args: + material_name: 材质名称 + effect_name: 效果名称 + """ + try: + if effect_name in self.material_effects: + effect_config = self.material_effects[effect_name] + # 在实际实现中,这里会应用材质效果到指定材质 + print(f"✓ 材质效果已应用: {material_name} + {effect_config['name']}") + else: + print(f"✗ 无效的材质效果: {effect_name}") + + except Exception as e: + print(f"✗ 材质效果应用失败: {e}") + + def remove_material_effect(self, material_name: str, effect_name: str): + """ + 移除材质效果 + + Args: + material_name: 材质名称 + effect_name: 效果名称 + """ + try: + # 在实际实现中,这里会从指定材质移除效果 + print(f"✓ 材质效果已移除: {material_name} - {effect_name}") + except Exception as e: + print(f"✗ 材质效果移除失败: {e}") + + def trigger_lighting_effect(self, effect_name: str): + """ + 触发瞬时光照效果 + + Args: + effect_name: 光照效果名称 + """ + try: + if effect_name in self.lighting_effects: + effect_config = self.lighting_effects[effect_name] + # 在实际实现中,这里会触发瞬时光照效果 + print(f"✓ 光照效果已触发: {effect_config['name']}") + else: + print(f"✗ 无效的光照效果: {effect_name}") + + except Exception as e: + print(f"✗ 光照效果触发失败: {e}") + + def set_particle_system_config(self, config: Dict[str, Any]): + """ + 设置粒子系统配置 + + Args: + config: 配置字典 + """ + self.particle_system_config.update(config) + print(f"✓ 粒子系统配置已更新: {self.particle_system_config}") + + def get_particle_system_config(self) -> Dict[str, Any]: + """ + 获取粒子系统配置 + + Returns: + 粒子系统配置字典 + """ + return self.particle_system_config.copy() + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'effects_activated': 0, + 'particles_generated': 0, + 'effects_deactivated': 0, + 'total_effect_time': 0.0 + } + print("✓ 视觉效果系统统计信息已重置") + + def set_update_interval(self, interval: float): + """ + 设置更新间隔 + + Args: + interval: 更新间隔(秒) + """ + self.update_interval = max(0.01, interval) + print(f"✓ 视觉效果更新间隔设置为: {interval} 秒") + + def is_effect_active(self, effect_name: str) -> bool: + """ + 检查效果是否激活 + + Args: + effect_name: 效果名称 + + Returns: + 效果是否激活 + """ return effect_name in self.active_effects \ No newline at end of file diff --git a/plugins/user/weather_season_system/particles/particle_effects.py b/plugins/user/weather_season_system/particles/particle_effects.py index d5bf8e63..5275be62 100644 --- a/plugins/user/weather_season_system/particles/particle_effects.py +++ b/plugins/user/weather_season_system/particles/particle_effects.py @@ -1,745 +1,745 @@ -""" -粒子效果系统 -负责管理天气和季节的粒子效果,如雨滴、雪花、雾气等 -""" - -import time -import random -from typing import Dict, Any, List, Optional, Tuple -import math - -class ParticleEffects: - """ - 粒子效果系统 - 负责管理天气和季节的粒子效果,包括粒子生成、更新和渲染 - """ - - def __init__(self, plugin): - """ - 初始化粒子效果系统 - - Args: - plugin: 天气和季节系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 粒子类型配置 - self.particle_types = { - 'rain_drop': { - 'name': '雨滴', - 'shape': 'line', # 'point', 'line', 'sprite' - 'size_range': (0.1, 0.5), - 'speed_range': (10.0, 25.0), - 'lifetime_range': (0.5, 2.0), - 'color': (0.7, 0.7, 1.0, 0.8), - 'gravity': 9.8, - 'wind_influence': 0.5, - 'spawn_rate': 100, # 每秒生成数量 - 'max_particles': 2000 - }, - 'snow_flake': { - 'name': '雪花', - 'shape': 'sprite', - 'size_range': (0.2, 1.0), - 'speed_range': (0.5, 3.0), - 'lifetime_range': (3.0, 8.0), - 'color': (1.0, 1.0, 1.0, 0.9), - 'gravity': 0.1, - 'wind_influence': 1.0, - 'spawn_rate': 80, - 'max_particles': 1500 - }, - 'hail_stone': { - 'name': '冰雹', - 'shape': 'sprite', - 'size_range': (0.3, 1.5), - 'speed_range': (15.0, 30.0), - 'lifetime_range': (1.0, 4.0), - 'color': (0.9, 0.9, 1.0, 0.95), - 'gravity': 12.0, - 'wind_influence': 0.3, - 'spawn_rate': 50, - 'max_particles': 1000 - }, - 'fog_particle': { - 'name': '雾气', - 'shape': 'sprite', - 'size_range': (5.0, 15.0), - 'speed_range': (0.1, 0.5), - 'lifetime_range': (8.0, 20.0), - 'color': (0.8, 0.8, 0.8, 0.2), - 'gravity': 0.0, - 'wind_influence': 0.8, - 'spawn_rate': 20, - 'max_particles': 500 - }, - 'leaf_particle': { - 'name': '落叶', - 'shape': 'sprite', - 'size_range': (0.5, 2.0), - 'speed_range': (1.0, 5.0), - 'lifetime_range': (5.0, 15.0), - 'color': (0.6, 0.5, 0.3, 0.7), - 'gravity': 1.0, - 'wind_influence': 1.5, - 'spawn_rate': 10, - 'max_particles': 200 - }, - 'dust_particle': { - 'name': '尘土', - 'shape': 'sprite', - 'size_range': (0.1, 0.8), - 'speed_range': (0.5, 3.0), - 'lifetime_range': (2.0, 8.0), - 'color': (0.8, 0.7, 0.6, 0.5), - 'gravity': 0.2, - 'wind_influence': 1.2, - 'spawn_rate': 30, - 'max_particles': 800 - } - } - - # 粒子池 - self.particle_pool = [] - self.active_particles = [] - - # 粒子发射器 - self.particle_emitters = {} - - # 粒子系统配置 - self.system_config = { - 'max_total_particles': 10000, - 'lod_levels': { - 'low': 0.3, - 'medium': 0.6, - 'high': 1.0 - }, - 'quality_level': 'medium', - 'distance_fade': True, - 'collision_detection': False, - 'particle_lod_distance': 50.0 - } - - # 粒子物理配置 - self.physics_config = { - 'gravity': (0.0, -9.8, 0.0), - 'air_resistance': 0.1, - 'wind_vector': (1.0, 0.0, 0.0), - 'wind_strength': 1.0 - } - - # 粒子更新配置 - self.update_interval = 0.016 # 60 FPS - self.last_update_time = 0.0 - self.accumulator = 0.0 - - # 统计信息 - self.stats = { - 'particles_spawned': 0, - 'particles_destroyed': 0, - 'active_particles': 0, - 'emitters_active': 0, - 'total_update_time': 0.0 - } - - print("✓ 粒子效果系统已创建") - - def initialize(self) -> bool: - """ - 初始化粒子效果系统 - - Returns: - 是否初始化成功 - """ - try: - # 初始化粒子池 - self._initialize_particle_pool() - - self.initialized = True - print("✓ 粒子效果系统初始化完成") - return True - - except Exception as e: - print(f"✗ 粒子效果系统初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用粒子效果系统 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 粒子效果系统未初始化") - return False - - self.enabled = True - print("✓ 粒子效果系统已启用") - return True - - except Exception as e: - print(f"✗ 粒子效果系统启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用粒子效果系统""" - try: - self.enabled = False - self._clear_all_particles() - print("✓ 粒子效果系统已禁用") - - except Exception as e: - print(f"✗ 粒子效果系统禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理粒子效果系统资源""" - try: - self.disable() - self.initialized = False - print("✓ 粒子效果系统资源已清理") - - except Exception as e: - print(f"✗ 粒子效果系统资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新粒子效果系统状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 累积时间 - self.accumulator += dt - - # 固定时间步长更新 - while self.accumulator >= self.update_interval: - self._update_particles(self.update_interval) - self._update_emitters(self.update_interval) - self.accumulator -= self.update_interval - - # 更新统计信息 - self.stats['total_update_time'] += dt - - except Exception as e: - print(f"✗ 粒子效果系统更新失败: {e}") - import traceback - traceback.print_exc() - - def _initialize_particle_pool(self): - """初始化粒子池""" - try: - pool_size = self.system_config['max_total_particles'] - - for i in range(pool_size): - particle = { - 'id': i, - 'type': None, - 'position': [0.0, 0.0, 0.0], - 'velocity': [0.0, 0.0, 0.0], - 'size': 1.0, - 'color': [1.0, 1.0, 1.0, 1.0], - 'lifetime': 1.0, - 'age': 0.0, - 'active': False, - 'gravity': 0.0, - 'wind_influence': 0.0 - } - self.particle_pool.append(particle) - - print(f"✓ 粒子池初始化完成,大小: {pool_size}") - - except Exception as e: - print(f"✗ 粒子池初始化失败: {e}") - - def _update_particles(self, dt: float): - """更新粒子""" - try: - # 获取当前风力 - wind_vector = self.physics_config['wind_vector'] - wind_strength = self.physics_config['wind_strength'] - - # 获取重力 - gravity = self.physics_config['gravity'] - - particles_to_remove = [] - - for particle in self.active_particles: - if not particle['active']: - continue - - # 更新年龄 - particle['age'] += dt - - # 检查是否超出生命周期 - if particle['age'] >= particle['lifetime']: - particles_to_remove.append(particle) - continue - - # 应用重力 - particle['velocity'][0] += gravity[0] * particle['gravity'] * dt - particle['velocity'][1] += gravity[1] * particle['gravity'] * dt - particle['velocity'][2] += gravity[2] * particle['gravity'] * dt - - # 应用风力影响 - wind_effect = wind_strength * particle['wind_influence'] - particle['velocity'][0] += wind_vector[0] * wind_effect * dt - particle['velocity'][1] += wind_vector[1] * wind_effect * dt - particle['velocity'][2] += wind_vector[2] * wind_effect * dt - - # 更新位置 - particle['position'][0] += particle['velocity'][0] * dt - particle['position'][1] += particle['velocity'][1] * dt - particle['position'][2] += particle['velocity'][2] * dt - - # 应用空气阻力 - air_resistance = self.physics_config['air_resistance'] - particle['velocity'][0] *= (1.0 - air_resistance * dt) - particle['velocity'][1] *= (1.0 - air_resistance * dt) - particle['velocity'][2] *= (1.0 - air_resistance * dt) - - # 移除死亡粒子 - for particle in particles_to_remove: - self._deactivate_particle(particle) - - except Exception as e: - print(f"✗ 粒子更新失败: {e}") - - def _update_emitters(self, dt: float): - """更新粒子发射器""" - try: - # 根据当前天气激活相应的粒子发射器 - self._update_weather_emitters() - - # 更新所有激活的发射器 - for emitter_name, emitter in self.particle_emitters.items(): - if emitter['active']: - self._spawn_particles_from_emitter(emitter, dt) - - except Exception as e: - print(f"✗ 发射器更新失败: {e}") - - def _update_weather_emitters(self): - """根据当前天气更新粒子发射器""" - try: - if not self.plugin.weather_manager: - return - - current_weather = self.plugin.weather_manager.get_current_weather() - weather_info = self.plugin.weather_manager.get_weather_info(current_weather) - - if not weather_info: - return - - # 获取天气对应的粒子效果 - particle_effect = weather_info.get('particle_effect') - - # 检查是否需要激活或更新粒子发射器 - if particle_effect and particle_effect in self.particle_types: - # 检查发射器是否已经存在 - if particle_effect not in self.particle_emitters: - # 创建粒子发射器 - self._create_particle_emitter(particle_effect) - else: - # 确保发射器处于激活状态 - self.particle_emitters[particle_effect]['active'] = True - else: - # 停用所有天气相关的发射器 - for emitter_name in list(self.particle_emitters.keys()): - if emitter_name in self.particle_types: - self.particle_emitters[emitter_name]['active'] = False - - except Exception as e: - print(f"✗ 天气发射器更新失败: {e}") - - def _create_particle_emitter(self, particle_type: str): - """ - 创建粒子发射器 - - Args: - particle_type: 粒子类型 - """ - try: - if particle_type not in self.particle_types: - return - - emitter_config = self.particle_types[particle_type] - - emitter = { - 'name': particle_type, - 'type': particle_type, - 'config': emitter_config, - 'position': [0.0, 50.0, 0.0], # 默认在空中 - 'area': [100.0, 0.0, 100.0], # 发射区域大小 - 'active': True, - 'last_spawn_time': 0.0, - 'spawn_accumulator': 0.0 - } - - self.particle_emitters[particle_type] = emitter - print(f"✓ 粒子发射器已创建: {emitter_config['name']}") - - except Exception as e: - print(f"✗ 粒子发射器创建失败: {e}") - - def _spawn_particles_from_emitter(self, emitter: Dict[str, Any], dt: float): - """ - 从发射器生成粒子 - - Args: - emitter: 发射器数据 - dt: 时间增量 - """ - try: - config = emitter['config'] - - # 计算应该生成的粒子数量 - emitter['spawn_accumulator'] += dt - spawn_interval = 1.0 / config['spawn_rate'] - - particles_to_spawn = int(emitter['spawn_accumulator'] / spawn_interval) - emitter['spawn_accumulator'] -= particles_to_spawn * spawn_interval - - # 限制生成数量不超过最大粒子数 - active_count = len([p for p in self.active_particles if p['type'] == emitter['type']]) - max_particles = config['max_particles'] - - particles_to_spawn = min(particles_to_spawn, max_particles - active_count) - - # 生成粒子 - for i in range(particles_to_spawn): - self._spawn_particle(emitter) - - except Exception as e: - print(f"✗ 粒子生成失败: {e}") - - def _spawn_particle(self, emitter: Dict[str, Any]): - """ - 生成单个粒子 - - Args: - emitter: 发射器数据 - """ - try: - # 检查是否还有可用的粒子 - available_particle = None - for particle in self.particle_pool: - if not particle['active']: - available_particle = particle - break - - if not available_particle: - # 粒子池已满,移除最老的粒子 - if self.active_particles: - oldest_particle = self.active_particles[0] - self._deactivate_particle(oldest_particle) - available_particle = oldest_particle - else: - return # 无法生成新粒子 - - config = emitter['config'] - - # 初始化粒子属性 - available_particle['type'] = emitter['type'] - available_particle['active'] = True - - # 随机位置(在发射器区域内) - half_area = [emitter['area'][i] / 2 for i in range(3)] - available_particle['position'][0] = emitter['position'][0] + random.uniform(-half_area[0], half_area[0]) - available_particle['position'][1] = emitter['position'][1] # Y轴保持不变 - available_particle['position'][2] = emitter['position'][2] + random.uniform(-half_area[2], half_area[2]) - - # 随机速度 - speed = random.uniform(config['speed_range'][0], config['speed_range'][1]) - angle = random.uniform(0, 2 * math.pi) - available_particle['velocity'][0] = math.cos(angle) * speed - available_particle['velocity'][1] = -random.uniform(0.5, 1.0) * speed # 主要向下 - available_particle['velocity'][2] = math.sin(angle) * speed - - # 随机大小 - available_particle['size'] = random.uniform(config['size_range'][0], config['size_range'][1]) - - # 颜色 - available_particle['color'] = list(config['color']) - - # 生命周期 - available_particle['lifetime'] = random.uniform(config['lifetime_range'][0], config['lifetime_range'][1]) - available_particle['age'] = 0.0 - - # 物理属性 - available_particle['gravity'] = config['gravity'] - available_particle['wind_influence'] = config['wind_influence'] - - # 添加到激活列表 - if available_particle not in self.active_particles: - self.active_particles.append(available_particle) - - # 更新统计信息 - self.stats['particles_spawned'] += 1 - - except Exception as e: - print(f"✗ 单个粒子生成失败: {e}") - - def _deactivate_particle(self, particle: Dict[str, Any]): - """ - 停用粒子 - - Args: - particle: 粒子数据 - """ - try: - particle['active'] = False - - # 从激活列表中移除 - if particle in self.active_particles: - self.active_particles.remove(particle) - - # 更新统计信息 - self.stats['particles_destroyed'] += 1 - - except Exception as e: - print(f"✗ 粒子停用失败: {e}") - - def _clear_all_particles(self): - """清除所有粒子""" - try: - for particle in self.active_particles: - particle['active'] = False - - self.active_particles.clear() - print("✓ 所有粒子已清除") - - except Exception as e: - print(f"✗ 粒子清除失败: {e}") - - def get_active_particles(self) -> List[Dict[str, Any]]: - """ - 获取激活的粒子列表 - - Returns: - 激活的粒子列表 - """ - return [p for p in self.active_particles if p['active']] - - def get_particle_types_list(self) -> List[str]: - """ - 获取所有粒子类型列表 - - Returns: - 粒子类型名称列表 - """ - return list(self.particle_types.keys()) - - def get_particle_type_info(self, particle_type: str) -> Optional[Dict[str, Any]]: - """ - 获取粒子类型信息 - - Args: - particle_type: 粒子类型名称 - - Returns: - 粒子类型信息字典或None - """ - try: - if particle_type in self.particle_types: - return self.particle_types[particle_type].copy() - return None - - except Exception as e: - print(f"✗ 粒子类型信息获取失败: {e}") - return None - - def create_custom_emitter(self, emitter_name: str, config: Dict[str, Any]): - """ - 创建自定义粒子发射器 - - Args: - emitter_name: 发射器名称 - config: 发射器配置 - """ - try: - emitter = { - 'name': emitter_name, - 'type': 'custom', - 'config': config, - 'position': config.get('position', [0.0, 0.0, 0.0]), - 'area': config.get('area', [10.0, 0.0, 10.0]), - 'active': config.get('active', True), - 'last_spawn_time': 0.0, - 'spawn_accumulator': 0.0 - } - - self.particle_emitters[emitter_name] = emitter - print(f"✓ 自定义粒子发射器已创建: {emitter_name}") - - except Exception as e: - print(f"✗ 自定义粒子发射器创建失败: {e}") - - def activate_emitter(self, emitter_name: str): - """ - 激活粒子发射器 - - Args: - emitter_name: 发射器名称 - """ - try: - if emitter_name in self.particle_emitters: - self.particle_emitters[emitter_name]['active'] = True - print(f"✓ 粒子发射器已激活: {emitter_name}") - else: - print(f"✗ 粒子发射器不存在: {emitter_name}") - - except Exception as e: - print(f"✗ 粒子发射器激活失败: {e}") - - def deactivate_emitter(self, emitter_name: str): - """ - 停用粒子发射器 - - Args: - emitter_name: 发射器名称 - """ - try: - if emitter_name in self.particle_emitters: - self.particle_emitters[emitter_name]['active'] = False - print(f"✓ 粒子发射器已停用: {emitter_name}") - else: - print(f"✗ 粒子发射器不存在: {emitter_name}") - - except Exception as e: - print(f"✗ 粒子发射器停用失败: {e}") - - def set_emitter_position(self, emitter_name: str, position: Tuple[float, float, float]): - """ - 设置发射器位置 - - Args: - emitter_name: 发射器名称 - position: 位置坐标(x, y, z) - """ - try: - if emitter_name in self.particle_emitters: - self.particle_emitters[emitter_name]['position'] = list(position) - print(f"✓ 发射器位置已设置: {emitter_name} = {position}") - else: - print(f"✗ 粒子发射器不存在: {emitter_name}") - - except Exception as e: - print(f"✗ 发射器位置设置失败: {e}") - - def set_emitter_area(self, emitter_name: str, area: Tuple[float, float, float]): - """ - 设置发射器区域大小 - - Args: - emitter_name: 发射器名称 - area: 区域大小(x, y, z) - """ - try: - if emitter_name in self.particle_emitters: - self.particle_emitters[emitter_name]['area'] = list(area) - print(f"✓ 发射器区域已设置: {emitter_name} = {area}") - else: - print(f"✗ 粒子发射器不存在: {emitter_name}") - - except Exception as e: - print(f"✗ 发射器区域设置失败: {e}") - - def set_physics_config(self, config: Dict[str, Any]): - """ - 设置物理配置 - - Args: - config: 物理配置字典 - """ - self.physics_config.update(config) - print(f"✓ 物理配置已更新: {self.physics_config}") - - def get_physics_config(self) -> Dict[str, Any]: - """ - 获取物理配置 - - Returns: - 物理配置字典 - """ - return self.physics_config.copy() - - def set_system_config(self, config: Dict[str, Any]): - """ - 设置系统配置 - - Args: - config: 系统配置字典 - """ - self.system_config.update(config) - print(f"✓ 系统配置已更新: {self.system_config}") - - def get_system_config(self) -> Dict[str, Any]: - """ - 获取系统配置 - - Returns: - 系统配置字典 - """ - return self.system_config.copy() - - def set_update_interval(self, interval: float): - """ - 设置更新间隔 - - Args: - interval: 更新间隔(秒) - """ - self.update_interval = max(0.001, min(0.1, interval)) - print(f"✓ 粒子更新间隔设置为: {interval} 秒") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - self.stats['active_particles'] = len(self.active_particles) - self.stats['emitters_active'] = len([e for e in self.particle_emitters.values() if e['active']]) - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'particles_spawned': 0, - 'particles_destroyed': 0, - 'active_particles': 0, - 'emitters_active': 0, - 'total_update_time': 0.0 - } - print("✓ 粒子效果系统统计信息已重置") - - def is_emitter_active(self, emitter_name: str) -> bool: - """ - 检查发射器是否激活 - - Args: - emitter_name: 发射器名称 - - Returns: - 发射器是否激活 - """ - if emitter_name in self.particle_emitters: - return self.particle_emitters[emitter_name]['active'] +""" +粒子效果系统 +负责管理天气和季节的粒子效果,如雨滴、雪花、雾气等 +""" + +import time +import random +from typing import Dict, Any, List, Optional, Tuple +import math + +class ParticleEffects: + """ + 粒子效果系统 + 负责管理天气和季节的粒子效果,包括粒子生成、更新和渲染 + """ + + def __init__(self, plugin): + """ + 初始化粒子效果系统 + + Args: + plugin: 天气和季节系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 粒子类型配置 + self.particle_types = { + 'rain_drop': { + 'name': '雨滴', + 'shape': 'line', # 'point', 'line', 'sprite' + 'size_range': (0.1, 0.5), + 'speed_range': (10.0, 25.0), + 'lifetime_range': (0.5, 2.0), + 'color': (0.7, 0.7, 1.0, 0.8), + 'gravity': 9.8, + 'wind_influence': 0.5, + 'spawn_rate': 100, # 每秒生成数量 + 'max_particles': 2000 + }, + 'snow_flake': { + 'name': '雪花', + 'shape': 'sprite', + 'size_range': (0.2, 1.0), + 'speed_range': (0.5, 3.0), + 'lifetime_range': (3.0, 8.0), + 'color': (1.0, 1.0, 1.0, 0.9), + 'gravity': 0.1, + 'wind_influence': 1.0, + 'spawn_rate': 80, + 'max_particles': 1500 + }, + 'hail_stone': { + 'name': '冰雹', + 'shape': 'sprite', + 'size_range': (0.3, 1.5), + 'speed_range': (15.0, 30.0), + 'lifetime_range': (1.0, 4.0), + 'color': (0.9, 0.9, 1.0, 0.95), + 'gravity': 12.0, + 'wind_influence': 0.3, + 'spawn_rate': 50, + 'max_particles': 1000 + }, + 'fog_particle': { + 'name': '雾气', + 'shape': 'sprite', + 'size_range': (5.0, 15.0), + 'speed_range': (0.1, 0.5), + 'lifetime_range': (8.0, 20.0), + 'color': (0.8, 0.8, 0.8, 0.2), + 'gravity': 0.0, + 'wind_influence': 0.8, + 'spawn_rate': 20, + 'max_particles': 500 + }, + 'leaf_particle': { + 'name': '落叶', + 'shape': 'sprite', + 'size_range': (0.5, 2.0), + 'speed_range': (1.0, 5.0), + 'lifetime_range': (5.0, 15.0), + 'color': (0.6, 0.5, 0.3, 0.7), + 'gravity': 1.0, + 'wind_influence': 1.5, + 'spawn_rate': 10, + 'max_particles': 200 + }, + 'dust_particle': { + 'name': '尘土', + 'shape': 'sprite', + 'size_range': (0.1, 0.8), + 'speed_range': (0.5, 3.0), + 'lifetime_range': (2.0, 8.0), + 'color': (0.8, 0.7, 0.6, 0.5), + 'gravity': 0.2, + 'wind_influence': 1.2, + 'spawn_rate': 30, + 'max_particles': 800 + } + } + + # 粒子池 + self.particle_pool = [] + self.active_particles = [] + + # 粒子发射器 + self.particle_emitters = {} + + # 粒子系统配置 + self.system_config = { + 'max_total_particles': 10000, + 'lod_levels': { + 'low': 0.3, + 'medium': 0.6, + 'high': 1.0 + }, + 'quality_level': 'medium', + 'distance_fade': True, + 'collision_detection': False, + 'particle_lod_distance': 50.0 + } + + # 粒子物理配置 + self.physics_config = { + 'gravity': (0.0, -9.8, 0.0), + 'air_resistance': 0.1, + 'wind_vector': (1.0, 0.0, 0.0), + 'wind_strength': 1.0 + } + + # 粒子更新配置 + self.update_interval = 0.016 # 60 FPS + self.last_update_time = 0.0 + self.accumulator = 0.0 + + # 统计信息 + self.stats = { + 'particles_spawned': 0, + 'particles_destroyed': 0, + 'active_particles': 0, + 'emitters_active': 0, + 'total_update_time': 0.0 + } + + print("✓ 粒子效果系统已创建") + + def initialize(self) -> bool: + """ + 初始化粒子效果系统 + + Returns: + 是否初始化成功 + """ + try: + # 初始化粒子池 + self._initialize_particle_pool() + + self.initialized = True + print("✓ 粒子效果系统初始化完成") + return True + + except Exception as e: + print(f"✗ 粒子效果系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用粒子效果系统 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 粒子效果系统未初始化") + return False + + self.enabled = True + print("✓ 粒子效果系统已启用") + return True + + except Exception as e: + print(f"✗ 粒子效果系统启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用粒子效果系统""" + try: + self.enabled = False + self._clear_all_particles() + print("✓ 粒子效果系统已禁用") + + except Exception as e: + print(f"✗ 粒子效果系统禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理粒子效果系统资源""" + try: + self.disable() + self.initialized = False + print("✓ 粒子效果系统资源已清理") + + except Exception as e: + print(f"✗ 粒子效果系统资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新粒子效果系统状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 累积时间 + self.accumulator += dt + + # 固定时间步长更新 + while self.accumulator >= self.update_interval: + self._update_particles(self.update_interval) + self._update_emitters(self.update_interval) + self.accumulator -= self.update_interval + + # 更新统计信息 + self.stats['total_update_time'] += dt + + except Exception as e: + print(f"✗ 粒子效果系统更新失败: {e}") + import traceback + traceback.print_exc() + + def _initialize_particle_pool(self): + """初始化粒子池""" + try: + pool_size = self.system_config['max_total_particles'] + + for i in range(pool_size): + particle = { + 'id': i, + 'type': None, + 'position': [0.0, 0.0, 0.0], + 'velocity': [0.0, 0.0, 0.0], + 'size': 1.0, + 'color': [1.0, 1.0, 1.0, 1.0], + 'lifetime': 1.0, + 'age': 0.0, + 'active': False, + 'gravity': 0.0, + 'wind_influence': 0.0 + } + self.particle_pool.append(particle) + + print(f"✓ 粒子池初始化完成,大小: {pool_size}") + + except Exception as e: + print(f"✗ 粒子池初始化失败: {e}") + + def _update_particles(self, dt: float): + """更新粒子""" + try: + # 获取当前风力 + wind_vector = self.physics_config['wind_vector'] + wind_strength = self.physics_config['wind_strength'] + + # 获取重力 + gravity = self.physics_config['gravity'] + + particles_to_remove = [] + + for particle in self.active_particles: + if not particle['active']: + continue + + # 更新年龄 + particle['age'] += dt + + # 检查是否超出生命周期 + if particle['age'] >= particle['lifetime']: + particles_to_remove.append(particle) + continue + + # 应用重力 + particle['velocity'][0] += gravity[0] * particle['gravity'] * dt + particle['velocity'][1] += gravity[1] * particle['gravity'] * dt + particle['velocity'][2] += gravity[2] * particle['gravity'] * dt + + # 应用风力影响 + wind_effect = wind_strength * particle['wind_influence'] + particle['velocity'][0] += wind_vector[0] * wind_effect * dt + particle['velocity'][1] += wind_vector[1] * wind_effect * dt + particle['velocity'][2] += wind_vector[2] * wind_effect * dt + + # 更新位置 + particle['position'][0] += particle['velocity'][0] * dt + particle['position'][1] += particle['velocity'][1] * dt + particle['position'][2] += particle['velocity'][2] * dt + + # 应用空气阻力 + air_resistance = self.physics_config['air_resistance'] + particle['velocity'][0] *= (1.0 - air_resistance * dt) + particle['velocity'][1] *= (1.0 - air_resistance * dt) + particle['velocity'][2] *= (1.0 - air_resistance * dt) + + # 移除死亡粒子 + for particle in particles_to_remove: + self._deactivate_particle(particle) + + except Exception as e: + print(f"✗ 粒子更新失败: {e}") + + def _update_emitters(self, dt: float): + """更新粒子发射器""" + try: + # 根据当前天气激活相应的粒子发射器 + self._update_weather_emitters() + + # 更新所有激活的发射器 + for emitter_name, emitter in self.particle_emitters.items(): + if emitter['active']: + self._spawn_particles_from_emitter(emitter, dt) + + except Exception as e: + print(f"✗ 发射器更新失败: {e}") + + def _update_weather_emitters(self): + """根据当前天气更新粒子发射器""" + try: + if not self.plugin.weather_manager: + return + + current_weather = self.plugin.weather_manager.get_current_weather() + weather_info = self.plugin.weather_manager.get_weather_info(current_weather) + + if not weather_info: + return + + # 获取天气对应的粒子效果 + particle_effect = weather_info.get('particle_effect') + + # 检查是否需要激活或更新粒子发射器 + if particle_effect and particle_effect in self.particle_types: + # 检查发射器是否已经存在 + if particle_effect not in self.particle_emitters: + # 创建粒子发射器 + self._create_particle_emitter(particle_effect) + else: + # 确保发射器处于激活状态 + self.particle_emitters[particle_effect]['active'] = True + else: + # 停用所有天气相关的发射器 + for emitter_name in list(self.particle_emitters.keys()): + if emitter_name in self.particle_types: + self.particle_emitters[emitter_name]['active'] = False + + except Exception as e: + print(f"✗ 天气发射器更新失败: {e}") + + def _create_particle_emitter(self, particle_type: str): + """ + 创建粒子发射器 + + Args: + particle_type: 粒子类型 + """ + try: + if particle_type not in self.particle_types: + return + + emitter_config = self.particle_types[particle_type] + + emitter = { + 'name': particle_type, + 'type': particle_type, + 'config': emitter_config, + 'position': [0.0, 50.0, 0.0], # 默认在空中 + 'area': [100.0, 0.0, 100.0], # 发射区域大小 + 'active': True, + 'last_spawn_time': 0.0, + 'spawn_accumulator': 0.0 + } + + self.particle_emitters[particle_type] = emitter + print(f"✓ 粒子发射器已创建: {emitter_config['name']}") + + except Exception as e: + print(f"✗ 粒子发射器创建失败: {e}") + + def _spawn_particles_from_emitter(self, emitter: Dict[str, Any], dt: float): + """ + 从发射器生成粒子 + + Args: + emitter: 发射器数据 + dt: 时间增量 + """ + try: + config = emitter['config'] + + # 计算应该生成的粒子数量 + emitter['spawn_accumulator'] += dt + spawn_interval = 1.0 / config['spawn_rate'] + + particles_to_spawn = int(emitter['spawn_accumulator'] / spawn_interval) + emitter['spawn_accumulator'] -= particles_to_spawn * spawn_interval + + # 限制生成数量不超过最大粒子数 + active_count = len([p for p in self.active_particles if p['type'] == emitter['type']]) + max_particles = config['max_particles'] + + particles_to_spawn = min(particles_to_spawn, max_particles - active_count) + + # 生成粒子 + for i in range(particles_to_spawn): + self._spawn_particle(emitter) + + except Exception as e: + print(f"✗ 粒子生成失败: {e}") + + def _spawn_particle(self, emitter: Dict[str, Any]): + """ + 生成单个粒子 + + Args: + emitter: 发射器数据 + """ + try: + # 检查是否还有可用的粒子 + available_particle = None + for particle in self.particle_pool: + if not particle['active']: + available_particle = particle + break + + if not available_particle: + # 粒子池已满,移除最老的粒子 + if self.active_particles: + oldest_particle = self.active_particles[0] + self._deactivate_particle(oldest_particle) + available_particle = oldest_particle + else: + return # 无法生成新粒子 + + config = emitter['config'] + + # 初始化粒子属性 + available_particle['type'] = emitter['type'] + available_particle['active'] = True + + # 随机位置(在发射器区域内) + half_area = [emitter['area'][i] / 2 for i in range(3)] + available_particle['position'][0] = emitter['position'][0] + random.uniform(-half_area[0], half_area[0]) + available_particle['position'][1] = emitter['position'][1] # Y轴保持不变 + available_particle['position'][2] = emitter['position'][2] + random.uniform(-half_area[2], half_area[2]) + + # 随机速度 + speed = random.uniform(config['speed_range'][0], config['speed_range'][1]) + angle = random.uniform(0, 2 * math.pi) + available_particle['velocity'][0] = math.cos(angle) * speed + available_particle['velocity'][1] = -random.uniform(0.5, 1.0) * speed # 主要向下 + available_particle['velocity'][2] = math.sin(angle) * speed + + # 随机大小 + available_particle['size'] = random.uniform(config['size_range'][0], config['size_range'][1]) + + # 颜色 + available_particle['color'] = list(config['color']) + + # 生命周期 + available_particle['lifetime'] = random.uniform(config['lifetime_range'][0], config['lifetime_range'][1]) + available_particle['age'] = 0.0 + + # 物理属性 + available_particle['gravity'] = config['gravity'] + available_particle['wind_influence'] = config['wind_influence'] + + # 添加到激活列表 + if available_particle not in self.active_particles: + self.active_particles.append(available_particle) + + # 更新统计信息 + self.stats['particles_spawned'] += 1 + + except Exception as e: + print(f"✗ 单个粒子生成失败: {e}") + + def _deactivate_particle(self, particle: Dict[str, Any]): + """ + 停用粒子 + + Args: + particle: 粒子数据 + """ + try: + particle['active'] = False + + # 从激活列表中移除 + if particle in self.active_particles: + self.active_particles.remove(particle) + + # 更新统计信息 + self.stats['particles_destroyed'] += 1 + + except Exception as e: + print(f"✗ 粒子停用失败: {e}") + + def _clear_all_particles(self): + """清除所有粒子""" + try: + for particle in self.active_particles: + particle['active'] = False + + self.active_particles.clear() + print("✓ 所有粒子已清除") + + except Exception as e: + print(f"✗ 粒子清除失败: {e}") + + def get_active_particles(self) -> List[Dict[str, Any]]: + """ + 获取激活的粒子列表 + + Returns: + 激活的粒子列表 + """ + return [p for p in self.active_particles if p['active']] + + def get_particle_types_list(self) -> List[str]: + """ + 获取所有粒子类型列表 + + Returns: + 粒子类型名称列表 + """ + return list(self.particle_types.keys()) + + def get_particle_type_info(self, particle_type: str) -> Optional[Dict[str, Any]]: + """ + 获取粒子类型信息 + + Args: + particle_type: 粒子类型名称 + + Returns: + 粒子类型信息字典或None + """ + try: + if particle_type in self.particle_types: + return self.particle_types[particle_type].copy() + return None + + except Exception as e: + print(f"✗ 粒子类型信息获取失败: {e}") + return None + + def create_custom_emitter(self, emitter_name: str, config: Dict[str, Any]): + """ + 创建自定义粒子发射器 + + Args: + emitter_name: 发射器名称 + config: 发射器配置 + """ + try: + emitter = { + 'name': emitter_name, + 'type': 'custom', + 'config': config, + 'position': config.get('position', [0.0, 0.0, 0.0]), + 'area': config.get('area', [10.0, 0.0, 10.0]), + 'active': config.get('active', True), + 'last_spawn_time': 0.0, + 'spawn_accumulator': 0.0 + } + + self.particle_emitters[emitter_name] = emitter + print(f"✓ 自定义粒子发射器已创建: {emitter_name}") + + except Exception as e: + print(f"✗ 自定义粒子发射器创建失败: {e}") + + def activate_emitter(self, emitter_name: str): + """ + 激活粒子发射器 + + Args: + emitter_name: 发射器名称 + """ + try: + if emitter_name in self.particle_emitters: + self.particle_emitters[emitter_name]['active'] = True + print(f"✓ 粒子发射器已激活: {emitter_name}") + else: + print(f"✗ 粒子发射器不存在: {emitter_name}") + + except Exception as e: + print(f"✗ 粒子发射器激活失败: {e}") + + def deactivate_emitter(self, emitter_name: str): + """ + 停用粒子发射器 + + Args: + emitter_name: 发射器名称 + """ + try: + if emitter_name in self.particle_emitters: + self.particle_emitters[emitter_name]['active'] = False + print(f"✓ 粒子发射器已停用: {emitter_name}") + else: + print(f"✗ 粒子发射器不存在: {emitter_name}") + + except Exception as e: + print(f"✗ 粒子发射器停用失败: {e}") + + def set_emitter_position(self, emitter_name: str, position: Tuple[float, float, float]): + """ + 设置发射器位置 + + Args: + emitter_name: 发射器名称 + position: 位置坐标(x, y, z) + """ + try: + if emitter_name in self.particle_emitters: + self.particle_emitters[emitter_name]['position'] = list(position) + print(f"✓ 发射器位置已设置: {emitter_name} = {position}") + else: + print(f"✗ 粒子发射器不存在: {emitter_name}") + + except Exception as e: + print(f"✗ 发射器位置设置失败: {e}") + + def set_emitter_area(self, emitter_name: str, area: Tuple[float, float, float]): + """ + 设置发射器区域大小 + + Args: + emitter_name: 发射器名称 + area: 区域大小(x, y, z) + """ + try: + if emitter_name in self.particle_emitters: + self.particle_emitters[emitter_name]['area'] = list(area) + print(f"✓ 发射器区域已设置: {emitter_name} = {area}") + else: + print(f"✗ 粒子发射器不存在: {emitter_name}") + + except Exception as e: + print(f"✗ 发射器区域设置失败: {e}") + + def set_physics_config(self, config: Dict[str, Any]): + """ + 设置物理配置 + + Args: + config: 物理配置字典 + """ + self.physics_config.update(config) + print(f"✓ 物理配置已更新: {self.physics_config}") + + def get_physics_config(self) -> Dict[str, Any]: + """ + 获取物理配置 + + Returns: + 物理配置字典 + """ + return self.physics_config.copy() + + def set_system_config(self, config: Dict[str, Any]): + """ + 设置系统配置 + + Args: + config: 系统配置字典 + """ + self.system_config.update(config) + print(f"✓ 系统配置已更新: {self.system_config}") + + def get_system_config(self) -> Dict[str, Any]: + """ + 获取系统配置 + + Returns: + 系统配置字典 + """ + return self.system_config.copy() + + def set_update_interval(self, interval: float): + """ + 设置更新间隔 + + Args: + interval: 更新间隔(秒) + """ + self.update_interval = max(0.001, min(0.1, interval)) + print(f"✓ 粒子更新间隔设置为: {interval} 秒") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + self.stats['active_particles'] = len(self.active_particles) + self.stats['emitters_active'] = len([e for e in self.particle_emitters.values() if e['active']]) + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'particles_spawned': 0, + 'particles_destroyed': 0, + 'active_particles': 0, + 'emitters_active': 0, + 'total_update_time': 0.0 + } + print("✓ 粒子效果系统统计信息已重置") + + def is_emitter_active(self, emitter_name: str) -> bool: + """ + 检查发射器是否激活 + + Args: + emitter_name: 发射器名称 + + Returns: + 发射器是否激活 + """ + if emitter_name in self.particle_emitters: + return self.particle_emitters[emitter_name]['active'] return False \ No newline at end of file diff --git a/plugins/user/weather_season_system/plugin.py b/plugins/user/weather_season_system/plugin.py index 4b60f1a0..b3650185 100644 --- a/plugins/user/weather_season_system/plugin.py +++ b/plugins/user/weather_season_system/plugin.py @@ -1,284 +1,284 @@ -""" -天气和季节系统插件 -为EG引擎提供完整的天气和季节变化系统 -""" - -from .core.season_manager import SeasonManager -from .core.weather_manager import WeatherManager -from .effects.environment_effects import EnvironmentEffects -from .effects.visual_effects import VisualEffects -from .audio.audio_manager import AudioManager -from .weather.weather_system import WeatherSystem -from .seasons.season_system import SeasonSystem -from .particles.particle_effects import ParticleEffects -from .editor.weather_editor import WeatherEditor -from .editor.preset_manager import PresetManager -from .utils.weather_utils import WeatherUtils - -class WeatherSeasonSystemPlugin: - """ - 天气和季节系统插件主类 - 整合所有天气和季节功能模块 - """ - - def __init__(self, engine): - """ - 初始化天气和季节系统插件 - - Args: - engine: EG引擎实例 - """ - self.engine = engine - self.name = "WeatherSeasonSystem" - self.version = "1.0.0" - self.author = "EG Plugin System" - - # 功能模块 - self.season_manager = None - self.weather_manager = None - self.environment_effects = None - self.visual_effects = None - self.audio_manager = None - self.weather_system = None - self.season_system = None - self.particle_effects = None - self.weather_editor = None - self.preset_manager = None - self.weather_utils = None - - # 插件状态 - self.enabled = False - self.initialized = False - - # 性能统计 - self.stats = { - 'weather_changes': 0, - 'season_changes': 0, - 'effects_updated': 0, - 'particles_generated': 0 - } - - print(f"✓ 天气和季节系统插件 v{self.version} 已创建") - - def initialize(self) -> bool: - """ - 初始化插件 - - Returns: - 是否初始化成功 - """ - try: - print("正在初始化天气和季节系统插件...") - - # 初始化核心组件 - self.season_manager = SeasonManager(self) - self.weather_manager = WeatherManager(self) - self.environment_effects = EnvironmentEffects(self) - self.visual_effects = VisualEffects(self) - self.audio_manager = AudioManager(self) - self.weather_system = WeatherSystem(self) - self.season_system = SeasonSystem(self) - self.particle_effects = ParticleEffects(self) - self.weather_utils = WeatherUtils(self) - - # 初始化新增的编辑器组件 - from .editor.weather_editor import WeatherEditor - from .editor.preset_manager import PresetManager - - self.weather_editor = WeatherEditor(self) - self.preset_manager = PresetManager(self) - - # 初始化各个模块 - modules = [ - self.season_manager, - self.weather_manager, - self.environment_effects, - self.visual_effects, - self.audio_manager, - self.weather_system, - self.season_system, - self.particle_effects, - self.weather_utils, - self.weather_editor, - self.preset_manager - ] - - for module in modules: - if module and not module.initialize(): - print(f"✗ 模块初始化失败: {module.__class__.__name__}") - return False - - self.initialized = True - print("✓ 天气和季节系统插件初始化完成") - return True - - except Exception as e: - print(f"✗ 天气和季节系统插件初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用插件 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 插件未初始化") - return False - - print("正在启用天气和季节系统插件...") - - # 启用各个模块 - modules = [ - self.season_manager, - self.weather_manager, - self.environment_effects, - self.visual_effects, - self.audio_manager, - self.weather_system, - self.season_system, - self.particle_effects, - self.weather_utils, - self.weather_editor, - self.preset_manager - ] - - for module in modules: - if module and not module.enable(): - print(f"✗ 模块启用失败: {module.__class__.__name__}") - return False - - self.enabled = True - print("✓ 天气和季节系统插件已启用") - return True - - except Exception as e: - print(f"✗ 天气和季节系统插件启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用插件""" - try: - if not self.enabled: - return - - print("正在禁用天气和季节系统插件...") - - # 禁用各个模块 - modules = [ - self.season_manager, - self.weather_manager, - self.environment_effects, - self.visual_effects, - self.audio_manager, - self.weather_system, - self.season_system, - self.particle_effects, - self.weather_utils, - self.weather_editor, - self.preset_manager - ] - - for module in modules: - if module: - module.disable() - - self.enabled = False - print("✓ 天气和季节系统插件已禁用") - - except Exception as e: - print(f"✗ 天气和季节系统插件禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理插件资源""" - try: - print("正在清理天气和季节系统插件资源...") - - # 清理各个模块 - modules = [ - self.season_manager, - self.weather_manager, - self.environment_effects, - self.visual_effects, - self.audio_manager, - self.weather_system, - self.season_system, - self.particle_effects, - self.weather_utils, - self.weather_editor, - self.preset_manager - ] - - for module in modules: - if module: - module.finalize() - - self.initialized = False - print("✓ 天气和季节系统插件资源已清理") - - except Exception as e: - print(f"✗ 天气和季节系统插件资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新插件状态 - - Args: - dt: 时间增量(秒) - """ - try: - if not self.enabled: - return - - # 更新各个模块 - modules = [ - self.season_manager, - self.weather_manager, - self.environment_effects, - self.visual_effects, - self.audio_manager, - self.weather_system, - self.season_system, - self.particle_effects, - self.weather_utils, - self.weather_editor, - self.preset_manager - ] - - for module in modules: - if module: - module.update(dt) - - except Exception as e: - print(f"✗ 天气和季节系统插件更新失败: {e}") - import traceback - traceback.print_exc() - - def get_stats(self) -> dict: - """ - 获取插件统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'weather_changes': 0, - 'season_changes': 0, - 'effects_updated': 0, - 'particles_generated': 0 - } +""" +天气和季节系统插件 +为EG引擎提供完整的天气和季节变化系统 +""" + +from .core.season_manager import SeasonManager +from .core.weather_manager import WeatherManager +from .effects.environment_effects import EnvironmentEffects +from .effects.visual_effects import VisualEffects +from .audio.audio_manager import AudioManager +from .weather.weather_system import WeatherSystem +from .seasons.season_system import SeasonSystem +from .particles.particle_effects import ParticleEffects +from .editor.weather_editor import WeatherEditor +from .editor.preset_manager import PresetManager +from .utils.weather_utils import WeatherUtils + +class WeatherSeasonSystemPlugin: + """ + 天气和季节系统插件主类 + 整合所有天气和季节功能模块 + """ + + def __init__(self, engine): + """ + 初始化天气和季节系统插件 + + Args: + engine: EG引擎实例 + """ + self.engine = engine + self.name = "WeatherSeasonSystem" + self.version = "1.0.0" + self.author = "EG Plugin System" + + # 功能模块 + self.season_manager = None + self.weather_manager = None + self.environment_effects = None + self.visual_effects = None + self.audio_manager = None + self.weather_system = None + self.season_system = None + self.particle_effects = None + self.weather_editor = None + self.preset_manager = None + self.weather_utils = None + + # 插件状态 + self.enabled = False + self.initialized = False + + # 性能统计 + self.stats = { + 'weather_changes': 0, + 'season_changes': 0, + 'effects_updated': 0, + 'particles_generated': 0 + } + + print(f"✓ 天气和季节系统插件 v{self.version} 已创建") + + def initialize(self) -> bool: + """ + 初始化插件 + + Returns: + 是否初始化成功 + """ + try: + print("正在初始化天气和季节系统插件...") + + # 初始化核心组件 + self.season_manager = SeasonManager(self) + self.weather_manager = WeatherManager(self) + self.environment_effects = EnvironmentEffects(self) + self.visual_effects = VisualEffects(self) + self.audio_manager = AudioManager(self) + self.weather_system = WeatherSystem(self) + self.season_system = SeasonSystem(self) + self.particle_effects = ParticleEffects(self) + self.weather_utils = WeatherUtils(self) + + # 初始化新增的编辑器组件 + from .editor.weather_editor import WeatherEditor + from .editor.preset_manager import PresetManager + + self.weather_editor = WeatherEditor(self) + self.preset_manager = PresetManager(self) + + # 初始化各个模块 + modules = [ + self.season_manager, + self.weather_manager, + self.environment_effects, + self.visual_effects, + self.audio_manager, + self.weather_system, + self.season_system, + self.particle_effects, + self.weather_utils, + self.weather_editor, + self.preset_manager + ] + + for module in modules: + if module and not module.initialize(): + print(f"✗ 模块初始化失败: {module.__class__.__name__}") + return False + + self.initialized = True + print("✓ 天气和季节系统插件初始化完成") + return True + + except Exception as e: + print(f"✗ 天气和季节系统插件初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用插件 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 插件未初始化") + return False + + print("正在启用天气和季节系统插件...") + + # 启用各个模块 + modules = [ + self.season_manager, + self.weather_manager, + self.environment_effects, + self.visual_effects, + self.audio_manager, + self.weather_system, + self.season_system, + self.particle_effects, + self.weather_utils, + self.weather_editor, + self.preset_manager + ] + + for module in modules: + if module and not module.enable(): + print(f"✗ 模块启用失败: {module.__class__.__name__}") + return False + + self.enabled = True + print("✓ 天气和季节系统插件已启用") + return True + + except Exception as e: + print(f"✗ 天气和季节系统插件启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用插件""" + try: + if not self.enabled: + return + + print("正在禁用天气和季节系统插件...") + + # 禁用各个模块 + modules = [ + self.season_manager, + self.weather_manager, + self.environment_effects, + self.visual_effects, + self.audio_manager, + self.weather_system, + self.season_system, + self.particle_effects, + self.weather_utils, + self.weather_editor, + self.preset_manager + ] + + for module in modules: + if module: + module.disable() + + self.enabled = False + print("✓ 天气和季节系统插件已禁用") + + except Exception as e: + print(f"✗ 天气和季节系统插件禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理插件资源""" + try: + print("正在清理天气和季节系统插件资源...") + + # 清理各个模块 + modules = [ + self.season_manager, + self.weather_manager, + self.environment_effects, + self.visual_effects, + self.audio_manager, + self.weather_system, + self.season_system, + self.particle_effects, + self.weather_utils, + self.weather_editor, + self.preset_manager + ] + + for module in modules: + if module: + module.finalize() + + self.initialized = False + print("✓ 天气和季节系统插件资源已清理") + + except Exception as e: + print(f"✗ 天气和季节系统插件资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新插件状态 + + Args: + dt: 时间增量(秒) + """ + try: + if not self.enabled: + return + + # 更新各个模块 + modules = [ + self.season_manager, + self.weather_manager, + self.environment_effects, + self.visual_effects, + self.audio_manager, + self.weather_system, + self.season_system, + self.particle_effects, + self.weather_utils, + self.weather_editor, + self.preset_manager + ] + + for module in modules: + if module: + module.update(dt) + + except Exception as e: + print(f"✗ 天气和季节系统插件更新失败: {e}") + import traceback + traceback.print_exc() + + def get_stats(self) -> dict: + """ + 获取插件统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'weather_changes': 0, + 'season_changes': 0, + 'effects_updated': 0, + 'particles_generated': 0 + } print("✓ 天气和季节系统统计信息已重置") \ No newline at end of file diff --git a/plugins/user/weather_season_system/seasons/season_system.py b/plugins/user/weather_season_system/seasons/season_system.py index a4b9ee74..5c49e37d 100644 --- a/plugins/user/weather_season_system/seasons/season_system.py +++ b/plugins/user/weather_season_system/seasons/season_system.py @@ -1,635 +1,635 @@ -""" -季节系统 -负责管理季节变化、季节效果和季节对环境的影响 -""" - -import time -import random -from typing import Dict, Any, List, Optional -import math - -class SeasonSystem: - """ - 季节系统 - 负责管理季节变化、季节效果和季节对环境的影响 - """ - - def __init__(self, plugin): - """ - 初始化季节系统 - - Args: - plugin: 天气和季节系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 季节配置 - self.seasons = { - 'spring': { - 'name': '春季', - 'description': '万物复苏的季节', - 'temperature_range': (5.0, 20.0), - 'vegetation_growth': 0.7, - 'flower_bloom': 0.8, - 'day_length_variation': 2.0, # 日长变化(小时) - 'weather_influence': { - 'rain': 1.5, - 'storm': 1.2, - 'fog': 1.1 - }, - 'color_palette': { - 'foliage': (0.3, 0.7, 0.3), - 'sky': (0.5, 0.7, 1.0), - 'overall': (1.0, 1.0, 0.8) - }, - 'transition_duration': 7.0 # 过渡持续时间(天) - }, - 'summer': { - 'name': '夏季', - 'description': '炎热的季节', - 'temperature_range': (20.0, 35.0), - 'vegetation_growth': 1.0, - 'flower_bloom': 0.6, - 'day_length_variation': 4.0, - 'weather_influence': { - 'rain': 1.0, - 'storm': 1.5, - 'fog': 0.5 - }, - 'color_palette': { - 'foliage': (0.2, 0.8, 0.2), - 'sky': (0.3, 0.6, 1.0), - 'overall': (1.0, 0.9, 0.7) - }, - 'transition_duration': 7.0 - }, - 'autumn': { - 'name': '秋季', - 'description': '收获的季节', - 'temperature_range': (0.0, 20.0), - 'vegetation_growth': 0.5, - 'flower_bloom': 0.2, - 'day_length_variation': 0.0, - 'weather_influence': { - 'rain': 1.2, - 'storm': 1.1, - 'fog': 1.3 - }, - 'color_palette': { - 'foliage': (0.8, 0.5, 0.2), - 'sky': (0.6, 0.7, 0.9), - 'overall': (1.0, 0.8, 0.6) - }, - 'transition_duration': 7.0 - }, - 'winter': { - 'name': '冬季', - 'description': '寒冷的季节', - 'temperature_range': (-15.0, 5.0), - 'vegetation_growth': 0.1, - 'flower_bloom': 0.0, - 'day_length_variation': -3.0, - 'weather_influence': { - 'rain': 0.5, - 'snow': 2.0, - 'fog': 1.5 - }, - 'color_palette': { - 'foliage': (0.7, 0.7, 0.7), - 'sky': (0.7, 0.7, 0.8), - 'overall': (0.8, 0.8, 1.0) - }, - 'transition_duration': 7.0 - } - } - - # 当前季节状态 - self.current_season = 'spring' - self.season_progress = 0.0 # 季节进度(0-1) - self.season_day = 0 # 当前季节中的天数 - - # 季节效果 - self.seasonal_effects = { - 'vegetation_state': 0.0, # 植被状态(0-1) - 'flower_bloom': 0.0, # 花朵盛开(0-1) - 'leaf_color': (0.5, 0.5, 0.5), # 叶子颜色 - 'water_temperature': 10.0, # 水温(°C) - 'animal_activity': 0.5 # 动物活动(0-1) - } - - # 季节时间配置 - self.days_per_season = 90 # 每个季节的天数 - self.season_cycle_enabled = True - self.season_transition_active = False - self.transition_progress = 0.0 - - # 季节活动和事件 - self.seasonal_events = { - 'spring': ['植树节', '春分', '复活节'], - 'summer': ['夏至', '独立日', '暑假'], - 'autumn': ['秋分', '万圣节', '感恩节'], - 'winter': ['冬至', '圣诞节', '新年'] - } - - # 季节更新配置 - self.update_interval = 60.0 # 更新间隔(秒) - self.last_update_time = 0.0 - - # 统计信息 - self.stats = { - 'seasons_passed': 0, - 'transitions_completed': 0, - 'events_occurred': 0, - 'total_season_time': 0.0 - } - - print("✓ 季节系统已创建") - - def initialize(self) -> bool: - """ - 初始化季节系统 - - Returns: - 是否初始化成功 - """ - try: - self.initialized = True - print("✓ 季节系统初始化完成") - return True - - except Exception as e: - print(f"✗ 季节系统初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用季节系统 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 季节系统未初始化") - return False - - self.enabled = True - print("✓ 季节系统已启用") - return True - - except Exception as e: - print(f"✗ 季节系统启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用季节系统""" - try: - self.enabled = False - print("✓ 季节系统已禁用") - - except Exception as e: - print(f"✗ 季节系统禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理季节系统资源""" - try: - self.disable() - self.initialized = False - print("✓ 季节系统资源已清理") - - except Exception as e: - print(f"✗ 季节系统资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新季节系统状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新计时器 - self.last_update_time += dt - if self.last_update_time < self.update_interval: - return - - # 重置更新计时器 - self.last_update_time = 0.0 - - # 更新季节进度 - self._update_season_progress(dt) - - # 更新季节效果 - self._update_seasonal_effects() - - # 检查季节事件 - self._check_seasonal_events() - - # 更新统计信息 - self.stats['total_season_time'] += dt - - except Exception as e: - print(f"✗ 季节系统更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_season_progress(self, dt: float): - """ - 更新季节进度 - - Args: - dt: 时间增量 - """ - try: - # 假设1秒代表1小时游戏时间 - hours_passed = dt / 1.0 - days_passed = hours_passed / 24.0 - - # 更新当前季节中的天数 - self.season_day += days_passed - - # 更新季节进度 - self.season_progress = min(1.0, self.season_day / self.days_per_season) - - # 检查是否需要切换季节 - if self.season_day >= self.days_per_season and self.season_cycle_enabled: - self._transition_to_next_season() - - except Exception as e: - print(f"✗ 季节进度更新失败: {e}") - - def _transition_to_next_season(self): - """过渡到下一个季节""" - try: - # 季节顺序 - season_order = ['spring', 'summer', 'autumn', 'winter'] - - # 找到当前季节在顺序中的位置 - current_index = season_order.index(self.current_season) - - # 计算下一个季节 - next_index = (current_index + 1) % len(season_order) - next_season = season_order[next_index] - - # 开始季节过渡 - self._start_season_transition(next_season) - - except Exception as e: - print(f"✗ 季节切换失败: {e}") - - def _start_season_transition(self, target_season: str): - """ - 开始季节过渡 - - Args: - target_season: 目标季节 - """ - try: - self.season_transition_active = True - self.transition_progress = 0.0 - self.target_season = target_season - - print(f"✓ 开始季节过渡: {self.seasons[self.current_season]['name']} -> {self.seasons[target_season]['name']}") - - except Exception as e: - print(f"✗ 季节过渡启动失败: {e}") - - def _update_seasonal_effects(self): - """更新季节效果""" - try: - current_season_data = self.seasons[self.current_season] - - # 更新植被状态 - self.seasonal_effects['vegetation_state'] = current_season_data['vegetation_growth'] - - # 更新花朵盛开 - self.seasonal_effects['flower_bloom'] = current_season_data['flower_bloom'] - - # 更新叶子颜色 - self.seasonal_effects['leaf_color'] = current_season_data['color_palette']['foliage'] - - # 更新水温(简化模型) - temp_range = current_season_data['temperature_range'] - avg_temp = (temp_range[0] + temp_range[1]) / 2 - self.seasonal_effects['water_temperature'] = max(0.0, avg_temp * 0.7) - - # 更新动物活动 - # 春季和夏季动物活动较高 - if self.current_season in ['spring', 'summer']: - self.seasonal_effects['animal_activity'] = 0.8 - else: - self.seasonal_effects['animal_activity'] = 0.3 - - except Exception as e: - print(f"✗ 季节效果更新失败: {e}") - - def _check_seasonal_events(self): - """检查季节事件""" - try: - # 在实际实现中,这里会检查是否有季节性事件发生 - # 例如:根据日期触发特殊事件 - - # 简化实现:随机触发事件 - if random.random() < 0.001: # 很低的概率 - events = self.seasonal_events[self.current_season] - if events: - event = random.choice(events) - print(f"✓ 季节事件发生: {event}") - self.stats['events_occurred'] += 1 - - except Exception as e: - print(f"✗ 季节事件检查失败: {e}") - - def set_current_season(self, season: str): - """ - 设置当前季节 - - Args: - season: 季节名称 - """ - try: - if season in self.seasons: - self.current_season = season - self.season_day = 0 - self.season_progress = 0.0 - print(f"✓ 当前季节设置为: {self.seasons[season]['name']}") - else: - print(f"✗ 无效的季节: {season}") - - except Exception as e: - print(f"✗ 当前季节设置失败: {e}") - - def get_current_season(self) -> str: - """ - 获取当前季节 - - Returns: - 当前季节名称 - """ - return self.current_season - - def get_season_info(self, season: str = None) -> Optional[Dict[str, Any]]: - """ - 获取季节信息 - - Args: - season: 季节名称(如果为None,则返回当前季节信息) - - Returns: - 季节信息字典或None - """ - try: - if season is None: - season = self.current_season - - if season in self.seasons: - return self.seasons[season].copy() - return None - - except Exception as e: - print(f"✗ 季节信息获取失败: {e}") - return None - - def get_seasonal_effects(self) -> Dict[str, Any]: - """ - 获取当前季节效果 - - Returns: - 季节效果字典 - """ - return self.seasonal_effects.copy() - - def get_season_progress(self) -> float: - """ - 获取季节进度 - - Returns: - 季节进度(0-1) - """ - return self.season_progress - - def get_season_day(self) -> int: - """ - 获取当前季节中的天数 - - Returns: - 当前季节中的天数 - """ - return int(self.season_day) - - def set_days_per_season(self, days: int): - """ - 设置每个季节的天数 - - Args: - days: 天数 - """ - if days > 0: - self.days_per_season = days - print(f"✓ 每个季节的天数设置为: {days}") - else: - print("✗ 无效的天数") - - def enable_season_cycle(self, enabled: bool): - """ - 启用/禁用季节循环 - - Args: - enabled: 是否启用季节循环 - """ - self.season_cycle_enabled = enabled - print(f"✓ 季节循环{'已启用' if enabled else '已禁用'}") - - def get_season_list(self) -> List[str]: - """ - 获取所有季节列表 - - Returns: - 季节名称列表 - """ - return list(self.seasons.keys()) - - def get_season_name(self, season: str) -> str: - """ - 获取季节显示名称 - - Args: - season: 季节名称 - - Returns: - 季节显示名称 - """ - if season in self.seasons: - return self.seasons[season]['name'] - return season - - def get_interpolated_season_params(self) -> Dict[str, Any]: - """ - 获取插值后的季节参数(考虑季节过渡) - - Returns: - 插值后的季节参数字典 - """ - try: - if not self.season_transition_active or self.transition_progress <= 0.0: - # 没有过渡,返回当前季节参数 - return self.seasons[self.current_season].copy() - - if self.transition_progress >= 1.0: - # 过渡完成,返回目标季节参数 - return self.seasons[self.target_season].copy() - - # 进行插值计算 - current_params = self.seasons[self.current_season] - target_params = self.seasons[self.target_season] - - interpolated_params = {} - - # 对数值参数进行线性插值 - numeric_params = ['vegetation_growth', 'flower_bloom', 'day_length_variation'] - for param in numeric_params: - current_value = current_params[param] - target_value = target_params[param] - interpolated_value = current_value + (target_value - current_value) * self.transition_progress - interpolated_params[param] = interpolated_value - - # 对温度范围进行插值 - current_temp_range = current_params['temperature_range'] - target_temp_range = target_params['temperature_range'] - interpolated_temp_range = ( - current_temp_range[0] + (target_temp_range[0] - current_temp_range[0]) * self.transition_progress, - current_temp_range[1] + (target_temp_range[1] - current_temp_range[1]) * self.transition_progress - ) - interpolated_params['temperature_range'] = interpolated_temp_range - - # 复制其他参数(使用目标季节的参数) - interpolated_params['name'] = f"{current_params['name']} → {target_params['name']}" - interpolated_params['description'] = f"过渡季节 ({self.transition_progress*100:.1f}%)" - interpolated_params['weather_influence'] = target_params['weather_influence'] - interpolated_params['color_palette'] = target_params['color_palette'] - interpolated_params['transition_duration'] = target_params['transition_duration'] - - return interpolated_params - - except Exception as e: - print(f"✗ 插值季节参数获取失败: {e}") - return self.seasons[self.current_season].copy() - - def get_seasonal_events(self, season: str = None) -> List[str]: - """ - 获取季节性事件 - - Args: - season: 季节名称(如果为None,则返回当前季节事件) - - Returns: - 季节性事件列表 - """ - try: - if season is None: - season = self.current_season - - return self.seasonal_events.get(season, []).copy() - - except Exception as e: - print(f"✗ 季节性事件获取失败: {e}") - return [] - - def add_seasonal_event(self, season: str, event: str): - """ - 添加季节性事件 - - Args: - season: 季节名称 - event: 事件名称 - """ - try: - if season in self.seasonal_events: - if event not in self.seasonal_events[season]: - self.seasonal_events[season].append(event) - print(f"✓ 季节性事件已添加: {season} - {event}") - else: - print(f"✗ 事件已存在: {event}") - else: - print(f"✗ 无效的季节: {season}") - - except Exception as e: - print(f"✗ 季节性事件添加失败: {e}") - - def remove_seasonal_event(self, season: str, event: str): - """ - 移除季节性事件 - - Args: - season: 季节名称 - event: 事件名称 - """ - try: - if season in self.seasonal_events and event in self.seasonal_events[season]: - self.seasonal_events[season].remove(event) - print(f"✓ 季节性事件已移除: {season} - {event}") - else: - print(f"✗ 事件不存在: {season} - {event}") - - except Exception as e: - print(f"✗ 季节性事件移除失败: {e}") - - def set_update_interval(self, interval: float): - """ - 设置更新间隔 - - Args: - interval: 更新间隔(秒) - """ - self.update_interval = max(1.0, interval) - print(f"✓ 季节更新间隔设置为: {interval} 秒") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'seasons_passed': 0, - 'transitions_completed': 0, - 'events_occurred': 0, - 'total_season_time': 0.0 - } - print("✓ 季节系统统计信息已重置") - - def is_transition_active(self) -> bool: - """ - 检查是否正在进行季节过渡 - - Returns: - 是否正在进行季节过渡 - """ - return self.season_transition_active - - def get_transition_progress(self) -> float: - """ - 获取季节过渡进度 - - Returns: - 季节过渡进度(0-1) - """ +""" +季节系统 +负责管理季节变化、季节效果和季节对环境的影响 +""" + +import time +import random +from typing import Dict, Any, List, Optional +import math + +class SeasonSystem: + """ + 季节系统 + 负责管理季节变化、季节效果和季节对环境的影响 + """ + + def __init__(self, plugin): + """ + 初始化季节系统 + + Args: + plugin: 天气和季节系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 季节配置 + self.seasons = { + 'spring': { + 'name': '春季', + 'description': '万物复苏的季节', + 'temperature_range': (5.0, 20.0), + 'vegetation_growth': 0.7, + 'flower_bloom': 0.8, + 'day_length_variation': 2.0, # 日长变化(小时) + 'weather_influence': { + 'rain': 1.5, + 'storm': 1.2, + 'fog': 1.1 + }, + 'color_palette': { + 'foliage': (0.3, 0.7, 0.3), + 'sky': (0.5, 0.7, 1.0), + 'overall': (1.0, 1.0, 0.8) + }, + 'transition_duration': 7.0 # 过渡持续时间(天) + }, + 'summer': { + 'name': '夏季', + 'description': '炎热的季节', + 'temperature_range': (20.0, 35.0), + 'vegetation_growth': 1.0, + 'flower_bloom': 0.6, + 'day_length_variation': 4.0, + 'weather_influence': { + 'rain': 1.0, + 'storm': 1.5, + 'fog': 0.5 + }, + 'color_palette': { + 'foliage': (0.2, 0.8, 0.2), + 'sky': (0.3, 0.6, 1.0), + 'overall': (1.0, 0.9, 0.7) + }, + 'transition_duration': 7.0 + }, + 'autumn': { + 'name': '秋季', + 'description': '收获的季节', + 'temperature_range': (0.0, 20.0), + 'vegetation_growth': 0.5, + 'flower_bloom': 0.2, + 'day_length_variation': 0.0, + 'weather_influence': { + 'rain': 1.2, + 'storm': 1.1, + 'fog': 1.3 + }, + 'color_palette': { + 'foliage': (0.8, 0.5, 0.2), + 'sky': (0.6, 0.7, 0.9), + 'overall': (1.0, 0.8, 0.6) + }, + 'transition_duration': 7.0 + }, + 'winter': { + 'name': '冬季', + 'description': '寒冷的季节', + 'temperature_range': (-15.0, 5.0), + 'vegetation_growth': 0.1, + 'flower_bloom': 0.0, + 'day_length_variation': -3.0, + 'weather_influence': { + 'rain': 0.5, + 'snow': 2.0, + 'fog': 1.5 + }, + 'color_palette': { + 'foliage': (0.7, 0.7, 0.7), + 'sky': (0.7, 0.7, 0.8), + 'overall': (0.8, 0.8, 1.0) + }, + 'transition_duration': 7.0 + } + } + + # 当前季节状态 + self.current_season = 'spring' + self.season_progress = 0.0 # 季节进度(0-1) + self.season_day = 0 # 当前季节中的天数 + + # 季节效果 + self.seasonal_effects = { + 'vegetation_state': 0.0, # 植被状态(0-1) + 'flower_bloom': 0.0, # 花朵盛开(0-1) + 'leaf_color': (0.5, 0.5, 0.5), # 叶子颜色 + 'water_temperature': 10.0, # 水温(°C) + 'animal_activity': 0.5 # 动物活动(0-1) + } + + # 季节时间配置 + self.days_per_season = 90 # 每个季节的天数 + self.season_cycle_enabled = True + self.season_transition_active = False + self.transition_progress = 0.0 + + # 季节活动和事件 + self.seasonal_events = { + 'spring': ['植树节', '春分', '复活节'], + 'summer': ['夏至', '独立日', '暑假'], + 'autumn': ['秋分', '万圣节', '感恩节'], + 'winter': ['冬至', '圣诞节', '新年'] + } + + # 季节更新配置 + self.update_interval = 60.0 # 更新间隔(秒) + self.last_update_time = 0.0 + + # 统计信息 + self.stats = { + 'seasons_passed': 0, + 'transitions_completed': 0, + 'events_occurred': 0, + 'total_season_time': 0.0 + } + + print("✓ 季节系统已创建") + + def initialize(self) -> bool: + """ + 初始化季节系统 + + Returns: + 是否初始化成功 + """ + try: + self.initialized = True + print("✓ 季节系统初始化完成") + return True + + except Exception as e: + print(f"✗ 季节系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用季节系统 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 季节系统未初始化") + return False + + self.enabled = True + print("✓ 季节系统已启用") + return True + + except Exception as e: + print(f"✗ 季节系统启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用季节系统""" + try: + self.enabled = False + print("✓ 季节系统已禁用") + + except Exception as e: + print(f"✗ 季节系统禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理季节系统资源""" + try: + self.disable() + self.initialized = False + print("✓ 季节系统资源已清理") + + except Exception as e: + print(f"✗ 季节系统资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新季节系统状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新计时器 + self.last_update_time += dt + if self.last_update_time < self.update_interval: + return + + # 重置更新计时器 + self.last_update_time = 0.0 + + # 更新季节进度 + self._update_season_progress(dt) + + # 更新季节效果 + self._update_seasonal_effects() + + # 检查季节事件 + self._check_seasonal_events() + + # 更新统计信息 + self.stats['total_season_time'] += dt + + except Exception as e: + print(f"✗ 季节系统更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_season_progress(self, dt: float): + """ + 更新季节进度 + + Args: + dt: 时间增量 + """ + try: + # 假设1秒代表1小时游戏时间 + hours_passed = dt / 1.0 + days_passed = hours_passed / 24.0 + + # 更新当前季节中的天数 + self.season_day += days_passed + + # 更新季节进度 + self.season_progress = min(1.0, self.season_day / self.days_per_season) + + # 检查是否需要切换季节 + if self.season_day >= self.days_per_season and self.season_cycle_enabled: + self._transition_to_next_season() + + except Exception as e: + print(f"✗ 季节进度更新失败: {e}") + + def _transition_to_next_season(self): + """过渡到下一个季节""" + try: + # 季节顺序 + season_order = ['spring', 'summer', 'autumn', 'winter'] + + # 找到当前季节在顺序中的位置 + current_index = season_order.index(self.current_season) + + # 计算下一个季节 + next_index = (current_index + 1) % len(season_order) + next_season = season_order[next_index] + + # 开始季节过渡 + self._start_season_transition(next_season) + + except Exception as e: + print(f"✗ 季节切换失败: {e}") + + def _start_season_transition(self, target_season: str): + """ + 开始季节过渡 + + Args: + target_season: 目标季节 + """ + try: + self.season_transition_active = True + self.transition_progress = 0.0 + self.target_season = target_season + + print(f"✓ 开始季节过渡: {self.seasons[self.current_season]['name']} -> {self.seasons[target_season]['name']}") + + except Exception as e: + print(f"✗ 季节过渡启动失败: {e}") + + def _update_seasonal_effects(self): + """更新季节效果""" + try: + current_season_data = self.seasons[self.current_season] + + # 更新植被状态 + self.seasonal_effects['vegetation_state'] = current_season_data['vegetation_growth'] + + # 更新花朵盛开 + self.seasonal_effects['flower_bloom'] = current_season_data['flower_bloom'] + + # 更新叶子颜色 + self.seasonal_effects['leaf_color'] = current_season_data['color_palette']['foliage'] + + # 更新水温(简化模型) + temp_range = current_season_data['temperature_range'] + avg_temp = (temp_range[0] + temp_range[1]) / 2 + self.seasonal_effects['water_temperature'] = max(0.0, avg_temp * 0.7) + + # 更新动物活动 + # 春季和夏季动物活动较高 + if self.current_season in ['spring', 'summer']: + self.seasonal_effects['animal_activity'] = 0.8 + else: + self.seasonal_effects['animal_activity'] = 0.3 + + except Exception as e: + print(f"✗ 季节效果更新失败: {e}") + + def _check_seasonal_events(self): + """检查季节事件""" + try: + # 在实际实现中,这里会检查是否有季节性事件发生 + # 例如:根据日期触发特殊事件 + + # 简化实现:随机触发事件 + if random.random() < 0.001: # 很低的概率 + events = self.seasonal_events[self.current_season] + if events: + event = random.choice(events) + print(f"✓ 季节事件发生: {event}") + self.stats['events_occurred'] += 1 + + except Exception as e: + print(f"✗ 季节事件检查失败: {e}") + + def set_current_season(self, season: str): + """ + 设置当前季节 + + Args: + season: 季节名称 + """ + try: + if season in self.seasons: + self.current_season = season + self.season_day = 0 + self.season_progress = 0.0 + print(f"✓ 当前季节设置为: {self.seasons[season]['name']}") + else: + print(f"✗ 无效的季节: {season}") + + except Exception as e: + print(f"✗ 当前季节设置失败: {e}") + + def get_current_season(self) -> str: + """ + 获取当前季节 + + Returns: + 当前季节名称 + """ + return self.current_season + + def get_season_info(self, season: str = None) -> Optional[Dict[str, Any]]: + """ + 获取季节信息 + + Args: + season: 季节名称(如果为None,则返回当前季节信息) + + Returns: + 季节信息字典或None + """ + try: + if season is None: + season = self.current_season + + if season in self.seasons: + return self.seasons[season].copy() + return None + + except Exception as e: + print(f"✗ 季节信息获取失败: {e}") + return None + + def get_seasonal_effects(self) -> Dict[str, Any]: + """ + 获取当前季节效果 + + Returns: + 季节效果字典 + """ + return self.seasonal_effects.copy() + + def get_season_progress(self) -> float: + """ + 获取季节进度 + + Returns: + 季节进度(0-1) + """ + return self.season_progress + + def get_season_day(self) -> int: + """ + 获取当前季节中的天数 + + Returns: + 当前季节中的天数 + """ + return int(self.season_day) + + def set_days_per_season(self, days: int): + """ + 设置每个季节的天数 + + Args: + days: 天数 + """ + if days > 0: + self.days_per_season = days + print(f"✓ 每个季节的天数设置为: {days}") + else: + print("✗ 无效的天数") + + def enable_season_cycle(self, enabled: bool): + """ + 启用/禁用季节循环 + + Args: + enabled: 是否启用季节循环 + """ + self.season_cycle_enabled = enabled + print(f"✓ 季节循环{'已启用' if enabled else '已禁用'}") + + def get_season_list(self) -> List[str]: + """ + 获取所有季节列表 + + Returns: + 季节名称列表 + """ + return list(self.seasons.keys()) + + def get_season_name(self, season: str) -> str: + """ + 获取季节显示名称 + + Args: + season: 季节名称 + + Returns: + 季节显示名称 + """ + if season in self.seasons: + return self.seasons[season]['name'] + return season + + def get_interpolated_season_params(self) -> Dict[str, Any]: + """ + 获取插值后的季节参数(考虑季节过渡) + + Returns: + 插值后的季节参数字典 + """ + try: + if not self.season_transition_active or self.transition_progress <= 0.0: + # 没有过渡,返回当前季节参数 + return self.seasons[self.current_season].copy() + + if self.transition_progress >= 1.0: + # 过渡完成,返回目标季节参数 + return self.seasons[self.target_season].copy() + + # 进行插值计算 + current_params = self.seasons[self.current_season] + target_params = self.seasons[self.target_season] + + interpolated_params = {} + + # 对数值参数进行线性插值 + numeric_params = ['vegetation_growth', 'flower_bloom', 'day_length_variation'] + for param in numeric_params: + current_value = current_params[param] + target_value = target_params[param] + interpolated_value = current_value + (target_value - current_value) * self.transition_progress + interpolated_params[param] = interpolated_value + + # 对温度范围进行插值 + current_temp_range = current_params['temperature_range'] + target_temp_range = target_params['temperature_range'] + interpolated_temp_range = ( + current_temp_range[0] + (target_temp_range[0] - current_temp_range[0]) * self.transition_progress, + current_temp_range[1] + (target_temp_range[1] - current_temp_range[1]) * self.transition_progress + ) + interpolated_params['temperature_range'] = interpolated_temp_range + + # 复制其他参数(使用目标季节的参数) + interpolated_params['name'] = f"{current_params['name']} → {target_params['name']}" + interpolated_params['description'] = f"过渡季节 ({self.transition_progress*100:.1f}%)" + interpolated_params['weather_influence'] = target_params['weather_influence'] + interpolated_params['color_palette'] = target_params['color_palette'] + interpolated_params['transition_duration'] = target_params['transition_duration'] + + return interpolated_params + + except Exception as e: + print(f"✗ 插值季节参数获取失败: {e}") + return self.seasons[self.current_season].copy() + + def get_seasonal_events(self, season: str = None) -> List[str]: + """ + 获取季节性事件 + + Args: + season: 季节名称(如果为None,则返回当前季节事件) + + Returns: + 季节性事件列表 + """ + try: + if season is None: + season = self.current_season + + return self.seasonal_events.get(season, []).copy() + + except Exception as e: + print(f"✗ 季节性事件获取失败: {e}") + return [] + + def add_seasonal_event(self, season: str, event: str): + """ + 添加季节性事件 + + Args: + season: 季节名称 + event: 事件名称 + """ + try: + if season in self.seasonal_events: + if event not in self.seasonal_events[season]: + self.seasonal_events[season].append(event) + print(f"✓ 季节性事件已添加: {season} - {event}") + else: + print(f"✗ 事件已存在: {event}") + else: + print(f"✗ 无效的季节: {season}") + + except Exception as e: + print(f"✗ 季节性事件添加失败: {e}") + + def remove_seasonal_event(self, season: str, event: str): + """ + 移除季节性事件 + + Args: + season: 季节名称 + event: 事件名称 + """ + try: + if season in self.seasonal_events and event in self.seasonal_events[season]: + self.seasonal_events[season].remove(event) + print(f"✓ 季节性事件已移除: {season} - {event}") + else: + print(f"✗ 事件不存在: {season} - {event}") + + except Exception as e: + print(f"✗ 季节性事件移除失败: {e}") + + def set_update_interval(self, interval: float): + """ + 设置更新间隔 + + Args: + interval: 更新间隔(秒) + """ + self.update_interval = max(1.0, interval) + print(f"✓ 季节更新间隔设置为: {interval} 秒") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'seasons_passed': 0, + 'transitions_completed': 0, + 'events_occurred': 0, + 'total_season_time': 0.0 + } + print("✓ 季节系统统计信息已重置") + + def is_transition_active(self) -> bool: + """ + 检查是否正在进行季节过渡 + + Returns: + 是否正在进行季节过渡 + """ + return self.season_transition_active + + def get_transition_progress(self) -> float: + """ + 获取季节过渡进度 + + Returns: + 季节过渡进度(0-1) + """ return self.transition_progress \ No newline at end of file diff --git a/plugins/user/weather_season_system/utils/weather_utils.py b/plugins/user/weather_season_system/utils/weather_utils.py index 9dcc5c5a..45de3afb 100644 --- a/plugins/user/weather_season_system/utils/weather_utils.py +++ b/plugins/user/weather_season_system/utils/weather_utils.py @@ -1,743 +1,743 @@ -""" -天气和季节系统工具类 -提供各种实用工具函数和辅助方法 -""" - -import time -import random -from typing import Dict, Any, List, Optional, Tuple -import math -import json -import os - -class WeatherUtils: - """ - 天气和季节系统工具类 - 提供各种实用工具函数和辅助方法 - """ - - def __init__(self, plugin): - """ - 初始化工具类 - - Args: - plugin: 天气和季节系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 配置文件路径 - self.config_directory = "config/weather" - self.preset_directory = "presets/weather" - - # 数据缓存 - self.data_cache = {} - self.cache_expiry = 300 # 缓存过期时间(秒) - - # 日志配置 - self.log_enabled = True - self.log_level = "INFO" - self.log_file = "weather_system.log" - - # 性能监控 - self.performance_stats = { - 'function_calls': {}, - 'execution_times': {}, - 'memory_usage': 0.0 - } - - print("✓ 天气和季节系统工具类已创建") - - def initialize(self) -> bool: - """ - 初始化工具类 - - Returns: - 是否初始化成功 - """ - try: - # 创建必要的目录 - directories = [self.config_directory, self.preset_directory] - for directory in directories: - if not os.path.exists(directory): - os.makedirs(directory) - - self.initialized = True - print("✓ 天气和季节系统工具类初始化完成") - return True - - except Exception as e: - print(f"✗ 天气和季节系统工具类初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用工具类 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 工具类未初始化") - return False - - self.enabled = True - print("✓ 天气和季节系统工具类已启用") - return True - - except Exception as e: - print(f"✗ 天气和季节系统工具类启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用工具类""" - try: - self.enabled = False - print("✓ 天气和季节系统工具类已禁用") - - except Exception as e: - print(f"✗ 天气和季节系统工具类禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理工具类资源""" - try: - self.disable() - self._save_cache() - self.initialized = False - print("✓ 天气和季节系统工具类资源已清理") - - except Exception as e: - print(f"✗ 天气和季节系统工具类资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新工具类状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 清理过期缓存 - self._cleanup_expired_cache() - - except Exception as e: - print(f"✗ 天气和季节系统工具类更新失败: {e}") - import traceback - traceback.print_exc() - - def _cleanup_expired_cache(self): - """清理过期缓存""" - try: - current_time = time.time() - expired_keys = [] - - for key, (timestamp, _) in self.data_cache.items(): - if current_time - timestamp > self.cache_expiry: - expired_keys.append(key) - - for key in expired_keys: - del self.data_cache[key] - - except Exception as e: - if self.log_enabled and self.log_level == "DEBUG": - print(f"✗ 缓存清理失败: {e}") - - def _save_cache(self): - """保存缓存到文件""" - try: - cache_file = os.path.join(self.config_directory, "cache.json") - cache_data = {} - - # 只保存可序列化的数据 - for key, (timestamp, value) in self.data_cache.items(): - try: - json.dumps(value) # 测试是否可序列化 - cache_data[key] = (timestamp, value) - except (TypeError, ValueError): - continue # 跳过不可序列化的数据 - - with open(cache_file, 'w', encoding='utf-8') as f: - json.dump(cache_data, f, ensure_ascii=False, indent=2) - - except Exception as e: - if self.log_enabled and self.log_level == "DEBUG": - print(f"✗ 缓存保存失败: {e}") - - def load_cache(self): - """从文件加载缓存""" - try: - cache_file = os.path.join(self.config_directory, "cache.json") - if os.path.exists(cache_file): - with open(cache_file, 'r', encoding='utf-8') as f: - cache_data = json.load(f) - self.data_cache.update(cache_data) - - except Exception as e: - if self.log_enabled and self.log_level == "DEBUG": - print(f"✗ 缓存加载失败: {e}") - - def log_message(self, level: str, message: str): - """ - 记录日志消息 - - Args: - level: 日志级别 - message: 日志消息 - """ - try: - if not self.log_enabled: - return - - levels = ["DEBUG", "INFO", "WARNING", "ERROR"] - if levels.index(level) < levels.index(self.log_level): - return - - timestamp = time.strftime("%Y-%m-%d %H:%M:%S") - log_entry = f"[{timestamp}] {level}: {message}\n" - - print(log_entry.strip()) - - # 写入日志文件 - try: - with open(self.log_file, 'a', encoding='utf-8') as f: - f.write(log_entry) - except Exception: - pass # 忽略文件写入错误 - - except Exception as e: - pass # 忽略日志记录错误 - - def performance_monitor(self, func_name: str): - """ - 性能监控装饰器 - - Args: - func_name: 函数名称 - """ - def decorator(func): - def wrapper(*args, **kwargs): - start_time = time.time() - - try: - result = func(*args, **kwargs) - end_time = time.time() - - # 记录执行时间 - execution_time = end_time - start_time - if func_name not in self.performance_stats['function_calls']: - self.performance_stats['function_calls'][func_name] = 0 - self.performance_stats['execution_times'][func_name] = 0.0 - - self.performance_stats['function_calls'][func_name] += 1 - self.performance_stats['execution_times'][func_name] += execution_time - - return result - - except Exception as e: - end_time = time.time() - execution_time = end_time - start_time - - self.log_message("ERROR", f"函数 {func_name} 执行失败: {e}") - raise - - return wrapper - return decorator - - def get_weather_icon(self, weather_type: str) -> str: - """ - 获取天气图标 - - Args: - weather_type: 天气类型 - - Returns: - 天气图标字符 - """ - icons = { - 'clear': '☀️', - 'cloudy': '☁️', - 'rain': '🌧️', - 'storm': '⛈️', - 'snow': '❄️', - 'fog': '🌫️', - 'windy': '💨', - 'hail': '🌨️' - } - - return icons.get(weather_type, '🌈') - - def get_season_icon(self, season: str) -> str: - """ - 获取季节图标 - - Args: - season: 季节名称 - - Returns: - 季节图标字符 - """ - icons = { - 'spring': '🌸', - 'summer': '☀️', - 'autumn': '🍂', - 'winter': '❄️' - } - - return icons.get(season, '🌍') - - def temperature_to_string(self, temperature: float) -> str: - """ - 将温度转换为字符串 - - Args: - temperature: 温度值(摄氏度) - - Returns: - 温度字符串 - """ - return f"{temperature:.1f}°C" - - def wind_speed_to_string(self, wind_speed: float) -> str: - """ - 将风速转换为字符串 - - Args: - wind_speed: 风速(m/s) - - Returns: - 风速字符串 - """ - return f"{wind_speed:.1f} m/s" - - def precipitation_to_string(self, precipitation: float) -> str: - """ - 将降水强度转换为字符串 - - Args: - precipitation: 降水强度(0-1) - - Returns: - 降水强度字符串 - """ - if precipitation <= 0: - return "无降水" - elif precipitation < 0.3: - return "小雨" - elif precipitation < 0.6: - return "中雨" - elif precipitation < 0.9: - return "大雨" - else: - return "暴雨" - - def visibility_to_string(self, visibility: float) -> str: - """ - 将能见度转换为字符串 - - Args: - visibility: 能见度(米) - - Returns: - 能见度字符串 - """ - if visibility >= 5000: - return "良好" - elif visibility >= 1000: - return "一般" - elif visibility >= 500: - return "较差" - else: - return "极差" - - def humidity_to_string(self, humidity: float) -> str: - """ - 将湿度转换为字符串 - - Args: - humidity: 湿度(%) - - Returns: - 湿度字符串 - """ - return f"{humidity:.0f}%" - - def pressure_to_string(self, pressure: float) -> str: - """ - 将气压转换为字符串 - - Args: - pressure: 气压(hPa) - - Returns: - 气压字符串 - """ - return f"{pressure:.1f} hPa" - - def uv_index_to_string(self, uv_index: float) -> str: - """ - 将UV指数转换为字符串 - - Args: - uv_index: UV指数 - - Returns: - UV指数字符串 - """ - if uv_index <= 2: - return "低" - elif uv_index <= 5: - return "中等" - elif uv_index <= 7: - return "高" - elif uv_index <= 10: - return "很高" - else: - return "极高" - - def get_weather_description(self, weather_type: str) -> str: - """ - 获取天气描述 - - Args: - weather_type: 天气类型 - - Returns: - 天气描述 - """ - descriptions = { - 'clear': '晴朗无云', - 'cloudy': '多云天气', - 'rain': '正在下雨', - 'storm': '雷暴天气', - 'snow': '正在下雪', - 'fog': '大雾天气', - 'windy': '风力强劲', - 'hail': '正在下冰雹' - } - - return descriptions.get(weather_type, '未知天气') - - def get_season_description(self, season: str) -> str: - """ - 获取季节描述 - - Args: - season: 季节名称 - - Returns: - 季节描述 - """ - descriptions = { - 'spring': '万物复苏的春季', - 'summer': '炎热的夏季', - 'autumn': '收获的秋季', - 'winter': '寒冷的冬季' - } - - return descriptions.get(season, '未知季节') - - def calculate_weather_severity(self, conditions: Dict[str, Any]) -> float: - """ - 计算天气严重程度 - - Args: - conditions: 天气条件字典 - - Returns: - 严重程度(0-1) - """ - try: - severity = 0.0 - - # 降水影响 - precipitation = conditions.get('precipitation', 0.0) - severity += precipitation * 0.3 - - # 风速影响 - wind_speed = conditions.get('wind_speed', 0.0) - severity += min(1.0, wind_speed / 20.0) * 0.2 - - # 温度影响(极端温度) - temperature = conditions.get('temperature', 20.0) - if temperature > 35 or temperature < 0: - severity += 0.2 - - # 能见度影响 - visibility = conditions.get('visibility', 10000.0) - severity += (1.0 - min(1.0, visibility / 10000.0)) * 0.3 - - return min(1.0, severity) - - except Exception as e: - self.log_message("ERROR", f"天气严重程度计算失败: {e}") - return 0.0 - - def interpolate_values(self, start_value: float, end_value: float, progress: float) -> float: - """ - 插值计算 - - Args: - start_value: 起始值 - end_value: 结束值 - progress: 进度(0-1) - - Returns: - 插值结果 - """ - return start_value + (end_value - start_value) * progress - - def clamp_value(self, value: float, min_value: float, max_value: float) -> float: - """ - 限制值范围 - - Args: - value: 值 - min_value: 最小值 - max_value: 最大值 - - Returns: - 限制后的值 - """ - return max(min_value, min(max_value, value)) - - def random_range(self, min_value: float, max_value: float) -> float: - """ - 生成随机范围值 - - Args: - min_value: 最小值 - max_value: 最大值 - - Returns: - 随机值 - """ - return random.uniform(min_value, max_value) - - def normalize_vector(self, vector: List[float]) -> List[float]: - """ - 归一化向量 - - Args: - vector: 向量 - - Returns: - 归一化后的向量 - """ - try: - magnitude = math.sqrt(sum(x * x for x in vector)) - if magnitude > 0: - return [x / magnitude for x in vector] - return [0.0] * len(vector) - except Exception as e: - self.log_message("ERROR", f"向量归一化失败: {e}") - return vector - - def distance_3d(self, pos1: Tuple[float, float, float], pos2: Tuple[float, float, float]) -> float: - """ - 计算3D空间距离 - - Args: - pos1: 位置1 - pos2: 位置2 - - Returns: - 距离 - """ - try: - dx = pos1[0] - pos2[0] - dy = pos1[1] - pos2[1] - dz = pos1[2] - pos2[2] - return math.sqrt(dx * dx + dy * dy + dz * dz) - except Exception as e: - self.log_message("ERROR", f"距离计算失败: {e}") - return 0.0 - - def angle_between_vectors(self, v1: List[float], v2: List[float]) -> float: - """ - 计算两个向量之间的夹角 - - Args: - v1: 向量1 - v2: 向量2 - - Returns: - 夹角(弧度) - """ - try: - dot_product = sum(a * b for a, b in zip(v1, v2)) - magnitude1 = math.sqrt(sum(a * a for a in v1)) - magnitude2 = math.sqrt(sum(b * b for b in v2)) - - if magnitude1 > 0 and magnitude2 > 0: - cos_angle = dot_product / (magnitude1 * magnitude2) - cos_angle = max(-1.0, min(1.0, cos_angle)) # 限制在[-1, 1]范围内 - return math.acos(cos_angle) - return 0.0 - except Exception as e: - self.log_message("ERROR", f"向量夹角计算失败: {e}") - return 0.0 - - def save_to_json(self, data: Dict[str, Any], filename: str) -> bool: - """ - 保存数据到JSON文件 - - Args: - data: 数据字典 - filename: 文件名 - - Returns: - 是否保存成功 - """ - try: - with open(filename, 'w', encoding='utf-8') as f: - json.dump(data, f, ensure_ascii=False, indent=2) - return True - except Exception as e: - self.log_message("ERROR", f"数据保存失败: {e}") - return False - - def load_from_json(self, filename: str) -> Optional[Dict[str, Any]]: - """ - 从JSON文件加载数据 - - Args: - filename: 文件名 - - Returns: - 数据字典或None - """ - try: - if os.path.exists(filename): - with open(filename, 'r', encoding='utf-8') as f: - return json.load(f) - return None - except Exception as e: - self.log_message("ERROR", f"数据加载失败: {e}") - return None - - def get_cache(self, key: str) -> Any: - """ - 获取缓存数据 - - Args: - key: 缓存键 - - Returns: - 缓存数据或None - """ - try: - if key in self.data_cache: - timestamp, value = self.data_cache[key] - if time.time() - timestamp <= self.cache_expiry: - return value - else: - del self.data_cache[key] - return None - except Exception as e: - self.log_message("ERROR", f"缓存获取失败: {e}") - return None - - def set_cache(self, key: str, value: Any): - """ - 设置缓存数据 - - Args: - key: 缓存键 - value: 缓存值 - """ - try: - self.data_cache[key] = (time.time(), value) - except Exception as e: - self.log_message("ERROR", f"缓存设置失败: {e}") - - def clear_cache(self): - """清空缓存""" - try: - self.data_cache.clear() - except Exception as e: - self.log_message("ERROR", f"缓存清空失败: {e}") - - def get_performance_stats(self) -> Dict[str, Any]: - """ - 获取性能统计信息 - - Returns: - 性能统计信息字典 - """ - try: - stats = self.performance_stats.copy() - - # 计算平均执行时间 - average_times = {} - for func_name, total_time in stats['execution_times'].items(): - calls = stats['function_calls'][func_name] - if calls > 0: - average_times[func_name] = total_time / calls - - stats['average_times'] = average_times - return stats - except Exception as e: - self.log_message("ERROR", f"性能统计获取失败: {e}") - return {} - - def reset_performance_stats(self): - """重置性能统计信息""" - try: - self.performance_stats = { - 'function_calls': {}, - 'execution_times': {}, - 'memory_usage': 0.0 - } - except Exception as e: - self.log_message("ERROR", f"性能统计重置失败: {e}") - - def format_time(self, seconds: float) -> str: - """ - 格式化时间 - - Args: - seconds: 秒数 - - Returns: - 格式化的时间字符串 - """ - try: - if seconds < 60: - return f"{seconds:.1f}秒" - elif seconds < 3600: - minutes = seconds / 60 - return f"{minutes:.1f}分钟" - else: - hours = seconds / 3600 - return f"{hours:.1f}小时" - except Exception as e: - self.log_message("ERROR", f"时间格式化失败: {e}") - return f"{seconds:.1f}秒" - - def generate_unique_id(self) -> str: - """ - 生成唯一ID - - Returns: - 唯一ID字符串 - """ - try: - timestamp = int(time.time() * 1000000) # 微秒时间戳 - random_part = random.randint(1000, 9999) - return f"{timestamp}_{random_part}" - except Exception as e: - self.log_message("ERROR", f"唯一ID生成失败: {e}") +""" +天气和季节系统工具类 +提供各种实用工具函数和辅助方法 +""" + +import time +import random +from typing import Dict, Any, List, Optional, Tuple +import math +import json +import os + +class WeatherUtils: + """ + 天气和季节系统工具类 + 提供各种实用工具函数和辅助方法 + """ + + def __init__(self, plugin): + """ + 初始化工具类 + + Args: + plugin: 天气和季节系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 配置文件路径 + self.config_directory = "config/weather" + self.preset_directory = "presets/weather" + + # 数据缓存 + self.data_cache = {} + self.cache_expiry = 300 # 缓存过期时间(秒) + + # 日志配置 + self.log_enabled = True + self.log_level = "INFO" + self.log_file = "weather_system.log" + + # 性能监控 + self.performance_stats = { + 'function_calls': {}, + 'execution_times': {}, + 'memory_usage': 0.0 + } + + print("✓ 天气和季节系统工具类已创建") + + def initialize(self) -> bool: + """ + 初始化工具类 + + Returns: + 是否初始化成功 + """ + try: + # 创建必要的目录 + directories = [self.config_directory, self.preset_directory] + for directory in directories: + if not os.path.exists(directory): + os.makedirs(directory) + + self.initialized = True + print("✓ 天气和季节系统工具类初始化完成") + return True + + except Exception as e: + print(f"✗ 天气和季节系统工具类初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用工具类 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 工具类未初始化") + return False + + self.enabled = True + print("✓ 天气和季节系统工具类已启用") + return True + + except Exception as e: + print(f"✗ 天气和季节系统工具类启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用工具类""" + try: + self.enabled = False + print("✓ 天气和季节系统工具类已禁用") + + except Exception as e: + print(f"✗ 天气和季节系统工具类禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理工具类资源""" + try: + self.disable() + self._save_cache() + self.initialized = False + print("✓ 天气和季节系统工具类资源已清理") + + except Exception as e: + print(f"✗ 天气和季节系统工具类资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新工具类状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 清理过期缓存 + self._cleanup_expired_cache() + + except Exception as e: + print(f"✗ 天气和季节系统工具类更新失败: {e}") + import traceback + traceback.print_exc() + + def _cleanup_expired_cache(self): + """清理过期缓存""" + try: + current_time = time.time() + expired_keys = [] + + for key, (timestamp, _) in self.data_cache.items(): + if current_time - timestamp > self.cache_expiry: + expired_keys.append(key) + + for key in expired_keys: + del self.data_cache[key] + + except Exception as e: + if self.log_enabled and self.log_level == "DEBUG": + print(f"✗ 缓存清理失败: {e}") + + def _save_cache(self): + """保存缓存到文件""" + try: + cache_file = os.path.join(self.config_directory, "cache.json") + cache_data = {} + + # 只保存可序列化的数据 + for key, (timestamp, value) in self.data_cache.items(): + try: + json.dumps(value) # 测试是否可序列化 + cache_data[key] = (timestamp, value) + except (TypeError, ValueError): + continue # 跳过不可序列化的数据 + + with open(cache_file, 'w', encoding='utf-8') as f: + json.dump(cache_data, f, ensure_ascii=False, indent=2) + + except Exception as e: + if self.log_enabled and self.log_level == "DEBUG": + print(f"✗ 缓存保存失败: {e}") + + def load_cache(self): + """从文件加载缓存""" + try: + cache_file = os.path.join(self.config_directory, "cache.json") + if os.path.exists(cache_file): + with open(cache_file, 'r', encoding='utf-8') as f: + cache_data = json.load(f) + self.data_cache.update(cache_data) + + except Exception as e: + if self.log_enabled and self.log_level == "DEBUG": + print(f"✗ 缓存加载失败: {e}") + + def log_message(self, level: str, message: str): + """ + 记录日志消息 + + Args: + level: 日志级别 + message: 日志消息 + """ + try: + if not self.log_enabled: + return + + levels = ["DEBUG", "INFO", "WARNING", "ERROR"] + if levels.index(level) < levels.index(self.log_level): + return + + timestamp = time.strftime("%Y-%m-%d %H:%M:%S") + log_entry = f"[{timestamp}] {level}: {message}\n" + + print(log_entry.strip()) + + # 写入日志文件 + try: + with open(self.log_file, 'a', encoding='utf-8') as f: + f.write(log_entry) + except Exception: + pass # 忽略文件写入错误 + + except Exception as e: + pass # 忽略日志记录错误 + + def performance_monitor(self, func_name: str): + """ + 性能监控装饰器 + + Args: + func_name: 函数名称 + """ + def decorator(func): + def wrapper(*args, **kwargs): + start_time = time.time() + + try: + result = func(*args, **kwargs) + end_time = time.time() + + # 记录执行时间 + execution_time = end_time - start_time + if func_name not in self.performance_stats['function_calls']: + self.performance_stats['function_calls'][func_name] = 0 + self.performance_stats['execution_times'][func_name] = 0.0 + + self.performance_stats['function_calls'][func_name] += 1 + self.performance_stats['execution_times'][func_name] += execution_time + + return result + + except Exception as e: + end_time = time.time() + execution_time = end_time - start_time + + self.log_message("ERROR", f"函数 {func_name} 执行失败: {e}") + raise + + return wrapper + return decorator + + def get_weather_icon(self, weather_type: str) -> str: + """ + 获取天气图标 + + Args: + weather_type: 天气类型 + + Returns: + 天气图标字符 + """ + icons = { + 'clear': '☀️', + 'cloudy': '☁️', + 'rain': '🌧️', + 'storm': '⛈️', + 'snow': '❄️', + 'fog': '🌫️', + 'windy': '💨', + 'hail': '🌨️' + } + + return icons.get(weather_type, '🌈') + + def get_season_icon(self, season: str) -> str: + """ + 获取季节图标 + + Args: + season: 季节名称 + + Returns: + 季节图标字符 + """ + icons = { + 'spring': '🌸', + 'summer': '☀️', + 'autumn': '🍂', + 'winter': '❄️' + } + + return icons.get(season, '🌍') + + def temperature_to_string(self, temperature: float) -> str: + """ + 将温度转换为字符串 + + Args: + temperature: 温度值(摄氏度) + + Returns: + 温度字符串 + """ + return f"{temperature:.1f}°C" + + def wind_speed_to_string(self, wind_speed: float) -> str: + """ + 将风速转换为字符串 + + Args: + wind_speed: 风速(m/s) + + Returns: + 风速字符串 + """ + return f"{wind_speed:.1f} m/s" + + def precipitation_to_string(self, precipitation: float) -> str: + """ + 将降水强度转换为字符串 + + Args: + precipitation: 降水强度(0-1) + + Returns: + 降水强度字符串 + """ + if precipitation <= 0: + return "无降水" + elif precipitation < 0.3: + return "小雨" + elif precipitation < 0.6: + return "中雨" + elif precipitation < 0.9: + return "大雨" + else: + return "暴雨" + + def visibility_to_string(self, visibility: float) -> str: + """ + 将能见度转换为字符串 + + Args: + visibility: 能见度(米) + + Returns: + 能见度字符串 + """ + if visibility >= 5000: + return "良好" + elif visibility >= 1000: + return "一般" + elif visibility >= 500: + return "较差" + else: + return "极差" + + def humidity_to_string(self, humidity: float) -> str: + """ + 将湿度转换为字符串 + + Args: + humidity: 湿度(%) + + Returns: + 湿度字符串 + """ + return f"{humidity:.0f}%" + + def pressure_to_string(self, pressure: float) -> str: + """ + 将气压转换为字符串 + + Args: + pressure: 气压(hPa) + + Returns: + 气压字符串 + """ + return f"{pressure:.1f} hPa" + + def uv_index_to_string(self, uv_index: float) -> str: + """ + 将UV指数转换为字符串 + + Args: + uv_index: UV指数 + + Returns: + UV指数字符串 + """ + if uv_index <= 2: + return "低" + elif uv_index <= 5: + return "中等" + elif uv_index <= 7: + return "高" + elif uv_index <= 10: + return "很高" + else: + return "极高" + + def get_weather_description(self, weather_type: str) -> str: + """ + 获取天气描述 + + Args: + weather_type: 天气类型 + + Returns: + 天气描述 + """ + descriptions = { + 'clear': '晴朗无云', + 'cloudy': '多云天气', + 'rain': '正在下雨', + 'storm': '雷暴天气', + 'snow': '正在下雪', + 'fog': '大雾天气', + 'windy': '风力强劲', + 'hail': '正在下冰雹' + } + + return descriptions.get(weather_type, '未知天气') + + def get_season_description(self, season: str) -> str: + """ + 获取季节描述 + + Args: + season: 季节名称 + + Returns: + 季节描述 + """ + descriptions = { + 'spring': '万物复苏的春季', + 'summer': '炎热的夏季', + 'autumn': '收获的秋季', + 'winter': '寒冷的冬季' + } + + return descriptions.get(season, '未知季节') + + def calculate_weather_severity(self, conditions: Dict[str, Any]) -> float: + """ + 计算天气严重程度 + + Args: + conditions: 天气条件字典 + + Returns: + 严重程度(0-1) + """ + try: + severity = 0.0 + + # 降水影响 + precipitation = conditions.get('precipitation', 0.0) + severity += precipitation * 0.3 + + # 风速影响 + wind_speed = conditions.get('wind_speed', 0.0) + severity += min(1.0, wind_speed / 20.0) * 0.2 + + # 温度影响(极端温度) + temperature = conditions.get('temperature', 20.0) + if temperature > 35 or temperature < 0: + severity += 0.2 + + # 能见度影响 + visibility = conditions.get('visibility', 10000.0) + severity += (1.0 - min(1.0, visibility / 10000.0)) * 0.3 + + return min(1.0, severity) + + except Exception as e: + self.log_message("ERROR", f"天气严重程度计算失败: {e}") + return 0.0 + + def interpolate_values(self, start_value: float, end_value: float, progress: float) -> float: + """ + 插值计算 + + Args: + start_value: 起始值 + end_value: 结束值 + progress: 进度(0-1) + + Returns: + 插值结果 + """ + return start_value + (end_value - start_value) * progress + + def clamp_value(self, value: float, min_value: float, max_value: float) -> float: + """ + 限制值范围 + + Args: + value: 值 + min_value: 最小值 + max_value: 最大值 + + Returns: + 限制后的值 + """ + return max(min_value, min(max_value, value)) + + def random_range(self, min_value: float, max_value: float) -> float: + """ + 生成随机范围值 + + Args: + min_value: 最小值 + max_value: 最大值 + + Returns: + 随机值 + """ + return random.uniform(min_value, max_value) + + def normalize_vector(self, vector: List[float]) -> List[float]: + """ + 归一化向量 + + Args: + vector: 向量 + + Returns: + 归一化后的向量 + """ + try: + magnitude = math.sqrt(sum(x * x for x in vector)) + if magnitude > 0: + return [x / magnitude for x in vector] + return [0.0] * len(vector) + except Exception as e: + self.log_message("ERROR", f"向量归一化失败: {e}") + return vector + + def distance_3d(self, pos1: Tuple[float, float, float], pos2: Tuple[float, float, float]) -> float: + """ + 计算3D空间距离 + + Args: + pos1: 位置1 + pos2: 位置2 + + Returns: + 距离 + """ + try: + dx = pos1[0] - pos2[0] + dy = pos1[1] - pos2[1] + dz = pos1[2] - pos2[2] + return math.sqrt(dx * dx + dy * dy + dz * dz) + except Exception as e: + self.log_message("ERROR", f"距离计算失败: {e}") + return 0.0 + + def angle_between_vectors(self, v1: List[float], v2: List[float]) -> float: + """ + 计算两个向量之间的夹角 + + Args: + v1: 向量1 + v2: 向量2 + + Returns: + 夹角(弧度) + """ + try: + dot_product = sum(a * b for a, b in zip(v1, v2)) + magnitude1 = math.sqrt(sum(a * a for a in v1)) + magnitude2 = math.sqrt(sum(b * b for b in v2)) + + if magnitude1 > 0 and magnitude2 > 0: + cos_angle = dot_product / (magnitude1 * magnitude2) + cos_angle = max(-1.0, min(1.0, cos_angle)) # 限制在[-1, 1]范围内 + return math.acos(cos_angle) + return 0.0 + except Exception as e: + self.log_message("ERROR", f"向量夹角计算失败: {e}") + return 0.0 + + def save_to_json(self, data: Dict[str, Any], filename: str) -> bool: + """ + 保存数据到JSON文件 + + Args: + data: 数据字典 + filename: 文件名 + + Returns: + 是否保存成功 + """ + try: + with open(filename, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + return True + except Exception as e: + self.log_message("ERROR", f"数据保存失败: {e}") + return False + + def load_from_json(self, filename: str) -> Optional[Dict[str, Any]]: + """ + 从JSON文件加载数据 + + Args: + filename: 文件名 + + Returns: + 数据字典或None + """ + try: + if os.path.exists(filename): + with open(filename, 'r', encoding='utf-8') as f: + return json.load(f) + return None + except Exception as e: + self.log_message("ERROR", f"数据加载失败: {e}") + return None + + def get_cache(self, key: str) -> Any: + """ + 获取缓存数据 + + Args: + key: 缓存键 + + Returns: + 缓存数据或None + """ + try: + if key in self.data_cache: + timestamp, value = self.data_cache[key] + if time.time() - timestamp <= self.cache_expiry: + return value + else: + del self.data_cache[key] + return None + except Exception as e: + self.log_message("ERROR", f"缓存获取失败: {e}") + return None + + def set_cache(self, key: str, value: Any): + """ + 设置缓存数据 + + Args: + key: 缓存键 + value: 缓存值 + """ + try: + self.data_cache[key] = (time.time(), value) + except Exception as e: + self.log_message("ERROR", f"缓存设置失败: {e}") + + def clear_cache(self): + """清空缓存""" + try: + self.data_cache.clear() + except Exception as e: + self.log_message("ERROR", f"缓存清空失败: {e}") + + def get_performance_stats(self) -> Dict[str, Any]: + """ + 获取性能统计信息 + + Returns: + 性能统计信息字典 + """ + try: + stats = self.performance_stats.copy() + + # 计算平均执行时间 + average_times = {} + for func_name, total_time in stats['execution_times'].items(): + calls = stats['function_calls'][func_name] + if calls > 0: + average_times[func_name] = total_time / calls + + stats['average_times'] = average_times + return stats + except Exception as e: + self.log_message("ERROR", f"性能统计获取失败: {e}") + return {} + + def reset_performance_stats(self): + """重置性能统计信息""" + try: + self.performance_stats = { + 'function_calls': {}, + 'execution_times': {}, + 'memory_usage': 0.0 + } + except Exception as e: + self.log_message("ERROR", f"性能统计重置失败: {e}") + + def format_time(self, seconds: float) -> str: + """ + 格式化时间 + + Args: + seconds: 秒数 + + Returns: + 格式化的时间字符串 + """ + try: + if seconds < 60: + return f"{seconds:.1f}秒" + elif seconds < 3600: + minutes = seconds / 60 + return f"{minutes:.1f}分钟" + else: + hours = seconds / 3600 + return f"{hours:.1f}小时" + except Exception as e: + self.log_message("ERROR", f"时间格式化失败: {e}") + return f"{seconds:.1f}秒" + + def generate_unique_id(self) -> str: + """ + 生成唯一ID + + Returns: + 唯一ID字符串 + """ + try: + timestamp = int(time.time() * 1000000) # 微秒时间戳 + random_part = random.randint(1000, 9999) + return f"{timestamp}_{random_part}" + except Exception as e: + self.log_message("ERROR", f"唯一ID生成失败: {e}") return str(random.randint(10000000, 99999999)) \ No newline at end of file diff --git a/plugins/user/weather_season_system/weather/weather_system.py b/plugins/user/weather_season_system/weather/weather_system.py index 4cae05f8..26cdb9cf 100644 --- a/plugins/user/weather_season_system/weather/weather_system.py +++ b/plugins/user/weather_season_system/weather/weather_system.py @@ -1,697 +1,697 @@ -""" -天气系统 -负责管理天气现象、天气变化和天气物理效果 -""" - -import time -import random -from typing import Dict, Any, List, Optional, Tuple -import math - -class WeatherSystem: - """ - 天气系统 - 负责管理天气现象、天气变化和天气物理效果 - """ - - def __init__(self, plugin): - """ - 初始化天气系统 - - Args: - plugin: 天气和季节系统插件实例 - """ - self.plugin = plugin - self.enabled = False - self.initialized = False - - # 天气现象配置 - self.weather_phenomena = { - 'precipitation': { - 'name': '降水', - 'types': ['rain', 'snow', 'hail'], - 'intensity_range': (0.0, 1.0), - 'accumulation_rate': 0.1, # 每秒积累量 - 'melt_rate': 0.05, # 雪融化速率 - 'splash_effect': True - }, - 'wind': { - 'name': '风', - 'speed_range': (0.0, 50.0), # 风速范围(m/s) - 'direction_variability': 0.1, # 方向变化性 - 'gust_factor': 2.0, # 阵风倍数 - 'turbulence': 0.2 - }, - 'temperature': { - 'name': '温度', - 'range': (-30.0, 50.0), # 温度范围(°C) - 'change_rate': 0.1, # 温度变化速率 - 'daily_variation': 10.0, # 日变化幅度 - 'seasonal_variation': 20.0 # 季节变化幅度 - }, - 'humidity': { - 'name': '湿度', - 'range': (0.0, 100.0), # 湿度范围(%) - 'change_rate': 0.5, # 湿度变化速率 - 'precipitation_effect': 10.0 # 降水对湿度的影响 - }, - 'pressure': { - 'name': '气压', - 'range': (950.0, 1050.0), # 气压范围(hPa) - 'change_rate': 0.1, # 气压变化速率 - 'weather_indicator': True # 是否指示天气变化 - }, - 'visibility': { - 'name': '能见度', - 'range': (10.0, 10000.0), # 能见度范围(米) - 'fog_effect': 0.8, # 雾对能见度的影响 - 'precipitation_effect': 0.6 # 降水对能见度的影响 - } - } - - # 当前天气状态 - self.current_conditions = { - 'temperature': 20.0, # 温度(°C) - 'humidity': 50.0, # 湿度(%) - 'pressure': 1013.25, # 气压(hPa) - 'wind_speed': 3.0, # 风速(m/s) - 'wind_direction': 0.0, # 风向(度) - 'precipitation': 0.0, # 降水强度(0-1) - 'precipitation_type': 'none', # 降水类型 - 'visibility': 10000.0, # 能见度(米) - 'cloud_cover': 0.1, # 云层覆盖率(0-1) - 'uv_index': 2.0 # UV指数 - } - - # 天气物理效果 - self.physical_effects = { - 'friction_modifier': 1.0, # 摩擦系数修改器 - 'movement_speed_modifier': 1.0, # 移动速度修改器 - 'sound_attenuation': 1.0, # 声音衰减 - 'light_scattering': 0.0, # 光线散射 - 'surface_wetness': 0.0, # 表面湿润度 - 'surface_ice': 0.0 # 表面结冰 - } - - # 天气更新配置 - self.update_interval = 1.0 # 更新间隔(秒) - self.last_update_time = 0.0 - - # 天气预测 - self.weather_forecast = [] - self.forecast_duration = 24 # 预测时长(小时) - - # 天气区域配置 - self.weather_zones = {} - self.zone_influence_radius = 100.0 - - # 统计信息 - self.stats = { - 'conditions_updated': 0, - 'weather_changes': 0, - 'phenomena_occurred': 0, - 'total_weather_time': 0.0 - } - - print("✓ 天气系统已创建") - - def initialize(self) -> bool: - """ - 初始化天气系统 - - Returns: - 是否初始化成功 - """ - try: - # 生成初始天气预报 - self._generate_weather_forecast() - - self.initialized = True - print("✓ 天气系统初始化完成") - return True - - except Exception as e: - print(f"✗ 天气系统初始化失败: {e}") - import traceback - traceback.print_exc() - return False - - def enable(self) -> bool: - """ - 启用天气系统 - - Returns: - 是否启用成功 - """ - try: - if not self.initialized: - print("✗ 天气系统未初始化") - return False - - self.enabled = True - print("✓ 天气系统已启用") - return True - - except Exception as e: - print(f"✗ 天气系统启用失败: {e}") - import traceback - traceback.print_exc() - return False - - def disable(self): - """禁用天气系统""" - try: - self.enabled = False - print("✓ 天气系统已禁用") - - except Exception as e: - print(f"✗ 天气系统禁用失败: {e}") - import traceback - traceback.print_exc() - - def finalize(self): - """清理天气系统资源""" - try: - self.disable() - self.initialized = False - print("✓ 天气系统资源已清理") - - except Exception as e: - print(f"✗ 天气系统资源清理失败: {e}") - import traceback - traceback.print_exc() - - def update(self, dt: float): - """ - 更新天气系统状态 - - Args: - dt: 时间增量 - """ - try: - if not self.enabled: - return - - # 更新计时器 - self.last_update_time += dt - if self.last_update_time < self.update_interval: - return - - # 重置更新计时器 - self.last_update_time = 0.0 - - # 更新天气条件 - self._update_weather_conditions(dt) - - # 更新天气物理效果 - self._update_physical_effects() - - # 更新天气预报 - self._update_weather_forecast() - - # 更新统计信息 - self.stats['total_weather_time'] += dt - - except Exception as e: - print(f"✗ 天气系统更新失败: {e}") - import traceback - traceback.print_exc() - - def _update_weather_conditions(self, dt: float): - """ - 更新天气条件 - - Args: - dt: 时间增量 - """ - try: - # 获取当前季节和天气参数 - if self.plugin.season_manager: - season_params = self.plugin.season_manager.get_interpolated_season_params() - else: - season_params = {} - - if self.plugin.weather_manager: - weather_params = self.plugin.weather_manager.get_interpolated_weather_params() - else: - weather_params = {} - - # 更新温度 - self._update_temperature(season_params, weather_params, dt) - - # 更新湿度 - self._update_humidity(weather_params, dt) - - # 更新风 - self._update_wind(weather_params, dt) - - # 更新降水 - self._update_precipitation(weather_params, dt) - - # 更新气压 - self._update_pressure(weather_params, dt) - - # 更新能见度 - self._update_visibility(weather_params) - - # 更新云层覆盖率 - self.current_conditions['cloud_cover'] = weather_params.get('cloud_cover', 0.1) - - # 更新UV指数 - self._update_uv_index() - - # 更新统计信息 - self.stats['conditions_updated'] += 1 - - except Exception as e: - print(f"✗ 天气条件更新失败: {e}") - - def _update_temperature(self, season_params: Dict, weather_params: Dict, dt: float): - """ - 更新温度 - - Args: - season_params: 季节参数 - weather_params: 天气参数 - dt: 时间增量 - """ - try: - # 基础温度受季节影响 - base_temp = season_params.get('temperature', 20.0) - - # 天气影响 - temp_modifier = weather_params.get('temperature_modifier', 0.0) - - # 日变化(简化模型) - if self.plugin.environment_effects: - time_config = getattr(self.plugin.environment_effects, 'time_config', {'day_time': 12.0}) - current_time = time_config.get('day_time', 12.0) - # 正弦变化,12点最高,0点最低 - daily_variation = math.sin((current_time - 6) * math.pi / 12) * 5.0 - else: - daily_variation = 0.0 - - # 计算最终温度 - target_temp = base_temp + temp_modifier + daily_variation - - # 平滑过渡到目标温度 - current_temp = self.current_conditions['temperature'] - change_rate = self.weather_phenomena['temperature']['change_rate'] - self.current_conditions['temperature'] = current_temp + (target_temp - current_temp) * change_rate * dt - - except Exception as e: - print(f"✗ 温度更新失败: {e}") - - def _update_humidity(self, weather_params: Dict, dt: float): - """ - 更新湿度 - - Args: - weather_params: 天气参数 - dt: 时间增量 - """ - try: - # 基础湿度受季节影响 - if self.plugin.season_manager: - season_params = self.plugin.season_manager.get_interpolated_season_params() - base_humidity = season_params.get('humidity', 50.0) - else: - base_humidity = 50.0 - - # 天气影响 - humidity_modifier = weather_params.get('humidity_modifier', 0.0) - - # 降水影响 - precipitation = self.current_conditions['precipitation'] - precipitation_effect = self.weather_phenomena['humidity']['precipitation_effect'] - - # 计算目标湿度 - target_humidity = base_humidity + humidity_modifier + precipitation * precipitation_effect - target_humidity = max(0.0, min(100.0, target_humidity)) - - # 平滑过渡到目标湿度 - current_humidity = self.current_conditions['humidity'] - change_rate = self.weather_phenomena['humidity']['change_rate'] - self.current_conditions['humidity'] = current_humidity + (target_humidity - current_humidity) * change_rate * dt - - except Exception as e: - print(f"✗ 湿度更新失败: {e}") - - def _update_wind(self, weather_params: Dict, dt: float): - """ - 更新风 - - Args: - weather_params: 天气参数 - dt: 时间增量 - """ - try: - # 基础风速受天气影响 - base_wind_speed = weather_params.get('wind_speed', 3.0) - - # 添加随机扰动 - turbulence = self.weather_phenomena['wind']['turbulence'] - wind_variation = random.uniform(-turbulence, turbulence) * base_wind_speed - - # 更新风速 - self.current_conditions['wind_speed'] = max(0.0, base_wind_speed + wind_variation) - - # 更新风向(缓慢变化) - direction_variability = self.weather_phenomena['wind']['direction_variability'] - direction_change = random.uniform(-direction_variability, direction_variability) * 180.0 * dt - self.current_conditions['wind_direction'] = (self.current_conditions['wind_direction'] + direction_change) % 360.0 - - except Exception as e: - print(f"✗ 风更新失败: {e}") - - def _update_precipitation(self, weather_params: Dict, dt: float): - """ - 更新降水 - - Args: - weather_params: 天气参数 - dt: 时间增量 - """ - try: - # 获取降水强度 - precipitation = weather_params.get('precipitation', 0.0) - self.current_conditions['precipitation'] = precipitation - - # 根据天气类型设置降水类型 - current_weather = self.plugin.weather_manager.get_current_weather() if self.plugin.weather_manager else 'clear' - - if precipitation > 0: - if current_weather == 'snow': - self.current_conditions['precipitation_type'] = 'snow' - elif current_weather == 'hail': - self.current_conditions['precipitation_type'] = 'hail' - else: - self.current_conditions['precipitation_type'] = 'rain' - else: - self.current_conditions['precipitation_type'] = 'none' - - except Exception as e: - print(f"✗ 降水更新失败: {e}") - - def _update_pressure(self, weather_params: Dict, dt: float): - """ - 更新气压 - - Args: - weather_params: 天气参数 - dt: 时间增量 - """ - try: - # 基础气压 - base_pressure = self.current_conditions['pressure'] - - # 添加随机变化 - pressure_change = random.uniform(-0.5, 0.5) - self.current_conditions['pressure'] = max(950.0, min(1050.0, base_pressure + pressure_change)) - - except Exception as e: - print(f"✗ 气压更新失败: {e}") - - def _update_visibility(self, weather_params: Dict): - """ - 更新能见度 - - Args: - weather_params: 天气参数 - """ - try: - # 基础能见度受天气影响 - visibility_factor = weather_params.get('visibility', 1.0) - base_visibility = self.weather_phenomena['visibility']['range'][1] # 最大能见度 - - # 计算最终能见度 - final_visibility = base_visibility * visibility_factor - - # 降水影响 - precipitation = self.current_conditions['precipitation'] - precipitation_effect = self.weather_phenomena['visibility']['precipitation_effect'] - final_visibility *= (1.0 - precipitation * precipitation_effect) - - self.current_conditions['visibility'] = max(10.0, final_visibility) - - except Exception as e: - print(f"✗ 能见度更新失败: {e}") - - def _update_uv_index(self): - """更新UV指数""" - try: - # 简化的UV指数计算 - if self.plugin.environment_effects: - time_config = getattr(self.plugin.environment_effects, 'time_config', {'day_time': 12.0}) - current_time = time_config.get('day_time', 12.0) - - # 正午最高,早晚为0 - if 8 <= current_time <= 16: - hour_factor = 1.0 - abs(current_time - 12.0) / 4.0 - else: - hour_factor = 0.0 - - # 晴天时UV指数最高 - current_weather = self.plugin.weather_manager.get_current_weather() if self.plugin.weather_manager else 'clear' - weather_factor = 1.0 if current_weather == 'clear' else 0.5 - - self.current_conditions['uv_index'] = max(0.0, 10.0 * hour_factor * weather_factor) - else: - self.current_conditions['uv_index'] = 2.0 - - except Exception as e: - print(f"✗ UV指数更新失败: {e}") - - def _update_physical_effects(self): - """更新天气物理效果""" - try: - precipitation = self.current_conditions['precipitation'] - precipitation_type = self.current_conditions['precipitation_type'] - temperature = self.current_conditions['temperature'] - wind_speed = self.current_conditions['wind_speed'] - - # 更新表面湿润度 - if precipitation_type in ['rain', 'hail']: - self.physical_effects['surface_wetness'] = min(1.0, precipitation * 2.0) - elif precipitation_type == 'snow': - self.physical_effects['surface_wetness'] = min(0.3, precipitation) - else: - # 无降水时逐渐干燥 - self.physical_effects['surface_wetness'] = max(0.0, self.physical_effects['surface_wetness'] - 0.01) - - # 更新表面结冰 - if temperature < 0 and self.physical_effects['surface_wetness'] > 0.1: - self.physical_effects['surface_ice'] = min(1.0, self.physical_effects['surface_wetness'] * 0.5) - else: - self.physical_effects['surface_ice'] = max(0.0, self.physical_effects['surface_ice'] - 0.02) - - # 更新摩擦系数 - wetness = self.physical_effects['surface_wetness'] - ice = self.physical_effects['surface_ice'] - self.physical_effects['friction_modifier'] = max(0.1, 1.0 - wetness * 0.5 - ice * 0.8) - - # 更新移动速度 - self.physical_effects['movement_speed_modifier'] = self.physical_effects['friction_modifier'] * 0.8 + 0.2 - - # 更新声音衰减 - self.physical_effects['sound_attenuation'] = 1.0 - precipitation * 0.3 - - # 更新光线散射 - self.physical_effects['light_scattering'] = precipitation * 0.5 + (1.0 - self.current_conditions['visibility'] / 10000.0) * 0.5 - - except Exception as e: - print(f"✗ 物理效果更新失败: {e}") - - def _generate_weather_forecast(self): - """生成天气预报""" - try: - self.weather_forecast = [] - - # 获取当前天气 - current_weather = self.plugin.weather_manager.get_current_weather() if self.plugin.weather_manager else 'clear' - - # 生成未来几小时的预报 - for i in range(self.forecast_duration): - # 简化的预报生成 - forecast_entry = { - 'hour': i, - 'weather': current_weather, - 'temperature': self.current_conditions['temperature'] + random.uniform(-2, 2), - 'precipitation': self.current_conditions['precipitation'] - } - self.weather_forecast.append(forecast_entry) - - except Exception as e: - print(f"✗ 天气预报生成失败: {e}") - - def _update_weather_forecast(self): - """更新天气预报""" - try: - # 移除过期的预报 - if self.weather_forecast: - self.weather_forecast.pop(0) - - # 添加新的预报 - current_weather = self.plugin.weather_manager.get_current_weather() if self.plugin.weather_manager else 'clear' - new_entry = { - 'hour': len(self.weather_forecast), - 'weather': current_weather, - 'temperature': self.current_conditions['temperature'] + random.uniform(-2, 2), - 'precipitation': self.current_conditions['precipitation'] - } - self.weather_forecast.append(new_entry) - - except Exception as e: - print(f"✗ 天气预报更新失败: {e}") - - def get_current_conditions(self) -> Dict[str, Any]: - """ - 获取当前天气条件 - - Returns: - 当前天气条件字典 - """ - return self.current_conditions.copy() - - def get_physical_effects(self) -> Dict[str, float]: - """ - 获取天气物理效果 - - Returns: - 天气物理效果字典 - """ - return self.physical_effects.copy() - - def get_weather_phenomena_list(self) -> List[str]: - """ - 获取天气现象列表 - - Returns: - 天气现象名称列表 - """ - return list(self.weather_phenomena.keys()) - - def get_weather_forecast(self, hours: int = 24) -> List[Dict[str, Any]]: - """ - 获取天气预报 - - Args: - hours: 预报时长(小时) - - Returns: - 天气预报列表 - """ - return self.weather_forecast[:hours] - - def get_weather_zone(self, zone_id: str) -> Optional[Dict[str, Any]]: - """ - 获取天气区域信息 - - Args: - zone_id: 区域ID - - Returns: - 天气区域信息或None - """ - return self.weather_zones.get(zone_id) - - def set_weather_zone(self, zone_id: str, zone_data: Dict[str, Any]): - """ - 设置天气区域 - - Args: - zone_id: 区域ID - zone_data: 区域数据 - """ - self.weather_zones[zone_id] = zone_data - print(f"✓ 天气区域已设置: {zone_id}") - - def remove_weather_zone(self, zone_id: str): - """ - 移除天气区域 - - Args: - zone_id: 区域ID - """ - if zone_id in self.weather_zones: - del self.weather_zones[zone_id] - print(f"✓ 天气区域已移除: {zone_id}") - - def get_weather_impact_on_object(self, object_position: Tuple[float, float, float]) -> Dict[str, Any]: - """ - 获取天气对特定位置物体的影响 - - Args: - object_position: 物体位置(x, y, z) - - Returns: - 天气影响字典 - """ - try: - # 简化实现,实际中需要考虑物体与天气区域的关系 - impact = { - 'wetness': self.physical_effects['surface_wetness'], - 'icing': self.physical_effects['surface_ice'], - 'wind_effect': self.current_conditions['wind_speed'] / 50.0, - 'visibility_factor': self.current_conditions['visibility'] / 10000.0, - 'temperature': self.current_conditions['temperature'] - } - - return impact - - except Exception as e: - print(f"✗ 物体天气影响计算失败: {e}") - return {} - - def set_update_interval(self, interval: float): - """ - 设置更新间隔 - - Args: - interval: 更新间隔(秒) - """ - self.update_interval = max(0.1, interval) - print(f"✓ 天气更新间隔设置为: {interval} 秒") - - def get_stats(self) -> Dict[str, Any]: - """ - 获取统计信息 - - Returns: - 统计信息字典 - """ - return self.stats.copy() - - def reset_stats(self): - """重置统计信息""" - self.stats = { - 'conditions_updated': 0, - 'weather_changes': 0, - 'phenomena_occurred': 0, - 'total_weather_time': 0.0 - } - print("✓ 天气系统统计信息已重置") - - def is_weather_phenomenon_active(self, phenomenon: str) -> bool: - """ - 检查天气现象是否活跃 - - Args: - phenomenon: 天气现象名称 - - Returns: - 天气现象是否活跃 - """ - try: - if phenomenon == 'precipitation': - return self.current_conditions['precipitation'] > 0 - elif phenomenon == 'wind': - return self.current_conditions['wind_speed'] > 1.0 - elif phenomenon == 'extreme_temperature': - temp = self.current_conditions['temperature'] - return temp < 0 or temp > 35 - return False - - except Exception as e: - print(f"✗ 天气现象检查失败: {e}") +""" +天气系统 +负责管理天气现象、天气变化和天气物理效果 +""" + +import time +import random +from typing import Dict, Any, List, Optional, Tuple +import math + +class WeatherSystem: + """ + 天气系统 + 负责管理天气现象、天气变化和天气物理效果 + """ + + def __init__(self, plugin): + """ + 初始化天气系统 + + Args: + plugin: 天气和季节系统插件实例 + """ + self.plugin = plugin + self.enabled = False + self.initialized = False + + # 天气现象配置 + self.weather_phenomena = { + 'precipitation': { + 'name': '降水', + 'types': ['rain', 'snow', 'hail'], + 'intensity_range': (0.0, 1.0), + 'accumulation_rate': 0.1, # 每秒积累量 + 'melt_rate': 0.05, # 雪融化速率 + 'splash_effect': True + }, + 'wind': { + 'name': '风', + 'speed_range': (0.0, 50.0), # 风速范围(m/s) + 'direction_variability': 0.1, # 方向变化性 + 'gust_factor': 2.0, # 阵风倍数 + 'turbulence': 0.2 + }, + 'temperature': { + 'name': '温度', + 'range': (-30.0, 50.0), # 温度范围(°C) + 'change_rate': 0.1, # 温度变化速率 + 'daily_variation': 10.0, # 日变化幅度 + 'seasonal_variation': 20.0 # 季节变化幅度 + }, + 'humidity': { + 'name': '湿度', + 'range': (0.0, 100.0), # 湿度范围(%) + 'change_rate': 0.5, # 湿度变化速率 + 'precipitation_effect': 10.0 # 降水对湿度的影响 + }, + 'pressure': { + 'name': '气压', + 'range': (950.0, 1050.0), # 气压范围(hPa) + 'change_rate': 0.1, # 气压变化速率 + 'weather_indicator': True # 是否指示天气变化 + }, + 'visibility': { + 'name': '能见度', + 'range': (10.0, 10000.0), # 能见度范围(米) + 'fog_effect': 0.8, # 雾对能见度的影响 + 'precipitation_effect': 0.6 # 降水对能见度的影响 + } + } + + # 当前天气状态 + self.current_conditions = { + 'temperature': 20.0, # 温度(°C) + 'humidity': 50.0, # 湿度(%) + 'pressure': 1013.25, # 气压(hPa) + 'wind_speed': 3.0, # 风速(m/s) + 'wind_direction': 0.0, # 风向(度) + 'precipitation': 0.0, # 降水强度(0-1) + 'precipitation_type': 'none', # 降水类型 + 'visibility': 10000.0, # 能见度(米) + 'cloud_cover': 0.1, # 云层覆盖率(0-1) + 'uv_index': 2.0 # UV指数 + } + + # 天气物理效果 + self.physical_effects = { + 'friction_modifier': 1.0, # 摩擦系数修改器 + 'movement_speed_modifier': 1.0, # 移动速度修改器 + 'sound_attenuation': 1.0, # 声音衰减 + 'light_scattering': 0.0, # 光线散射 + 'surface_wetness': 0.0, # 表面湿润度 + 'surface_ice': 0.0 # 表面结冰 + } + + # 天气更新配置 + self.update_interval = 1.0 # 更新间隔(秒) + self.last_update_time = 0.0 + + # 天气预测 + self.weather_forecast = [] + self.forecast_duration = 24 # 预测时长(小时) + + # 天气区域配置 + self.weather_zones = {} + self.zone_influence_radius = 100.0 + + # 统计信息 + self.stats = { + 'conditions_updated': 0, + 'weather_changes': 0, + 'phenomena_occurred': 0, + 'total_weather_time': 0.0 + } + + print("✓ 天气系统已创建") + + def initialize(self) -> bool: + """ + 初始化天气系统 + + Returns: + 是否初始化成功 + """ + try: + # 生成初始天气预报 + self._generate_weather_forecast() + + self.initialized = True + print("✓ 天气系统初始化完成") + return True + + except Exception as e: + print(f"✗ 天气系统初始化失败: {e}") + import traceback + traceback.print_exc() + return False + + def enable(self) -> bool: + """ + 启用天气系统 + + Returns: + 是否启用成功 + """ + try: + if not self.initialized: + print("✗ 天气系统未初始化") + return False + + self.enabled = True + print("✓ 天气系统已启用") + return True + + except Exception as e: + print(f"✗ 天气系统启用失败: {e}") + import traceback + traceback.print_exc() + return False + + def disable(self): + """禁用天气系统""" + try: + self.enabled = False + print("✓ 天气系统已禁用") + + except Exception as e: + print(f"✗ 天气系统禁用失败: {e}") + import traceback + traceback.print_exc() + + def finalize(self): + """清理天气系统资源""" + try: + self.disable() + self.initialized = False + print("✓ 天气系统资源已清理") + + except Exception as e: + print(f"✗ 天气系统资源清理失败: {e}") + import traceback + traceback.print_exc() + + def update(self, dt: float): + """ + 更新天气系统状态 + + Args: + dt: 时间增量 + """ + try: + if not self.enabled: + return + + # 更新计时器 + self.last_update_time += dt + if self.last_update_time < self.update_interval: + return + + # 重置更新计时器 + self.last_update_time = 0.0 + + # 更新天气条件 + self._update_weather_conditions(dt) + + # 更新天气物理效果 + self._update_physical_effects() + + # 更新天气预报 + self._update_weather_forecast() + + # 更新统计信息 + self.stats['total_weather_time'] += dt + + except Exception as e: + print(f"✗ 天气系统更新失败: {e}") + import traceback + traceback.print_exc() + + def _update_weather_conditions(self, dt: float): + """ + 更新天气条件 + + Args: + dt: 时间增量 + """ + try: + # 获取当前季节和天气参数 + if self.plugin.season_manager: + season_params = self.plugin.season_manager.get_interpolated_season_params() + else: + season_params = {} + + if self.plugin.weather_manager: + weather_params = self.plugin.weather_manager.get_interpolated_weather_params() + else: + weather_params = {} + + # 更新温度 + self._update_temperature(season_params, weather_params, dt) + + # 更新湿度 + self._update_humidity(weather_params, dt) + + # 更新风 + self._update_wind(weather_params, dt) + + # 更新降水 + self._update_precipitation(weather_params, dt) + + # 更新气压 + self._update_pressure(weather_params, dt) + + # 更新能见度 + self._update_visibility(weather_params) + + # 更新云层覆盖率 + self.current_conditions['cloud_cover'] = weather_params.get('cloud_cover', 0.1) + + # 更新UV指数 + self._update_uv_index() + + # 更新统计信息 + self.stats['conditions_updated'] += 1 + + except Exception as e: + print(f"✗ 天气条件更新失败: {e}") + + def _update_temperature(self, season_params: Dict, weather_params: Dict, dt: float): + """ + 更新温度 + + Args: + season_params: 季节参数 + weather_params: 天气参数 + dt: 时间增量 + """ + try: + # 基础温度受季节影响 + base_temp = season_params.get('temperature', 20.0) + + # 天气影响 + temp_modifier = weather_params.get('temperature_modifier', 0.0) + + # 日变化(简化模型) + if self.plugin.environment_effects: + time_config = getattr(self.plugin.environment_effects, 'time_config', {'day_time': 12.0}) + current_time = time_config.get('day_time', 12.0) + # 正弦变化,12点最高,0点最低 + daily_variation = math.sin((current_time - 6) * math.pi / 12) * 5.0 + else: + daily_variation = 0.0 + + # 计算最终温度 + target_temp = base_temp + temp_modifier + daily_variation + + # 平滑过渡到目标温度 + current_temp = self.current_conditions['temperature'] + change_rate = self.weather_phenomena['temperature']['change_rate'] + self.current_conditions['temperature'] = current_temp + (target_temp - current_temp) * change_rate * dt + + except Exception as e: + print(f"✗ 温度更新失败: {e}") + + def _update_humidity(self, weather_params: Dict, dt: float): + """ + 更新湿度 + + Args: + weather_params: 天气参数 + dt: 时间增量 + """ + try: + # 基础湿度受季节影响 + if self.plugin.season_manager: + season_params = self.plugin.season_manager.get_interpolated_season_params() + base_humidity = season_params.get('humidity', 50.0) + else: + base_humidity = 50.0 + + # 天气影响 + humidity_modifier = weather_params.get('humidity_modifier', 0.0) + + # 降水影响 + precipitation = self.current_conditions['precipitation'] + precipitation_effect = self.weather_phenomena['humidity']['precipitation_effect'] + + # 计算目标湿度 + target_humidity = base_humidity + humidity_modifier + precipitation * precipitation_effect + target_humidity = max(0.0, min(100.0, target_humidity)) + + # 平滑过渡到目标湿度 + current_humidity = self.current_conditions['humidity'] + change_rate = self.weather_phenomena['humidity']['change_rate'] + self.current_conditions['humidity'] = current_humidity + (target_humidity - current_humidity) * change_rate * dt + + except Exception as e: + print(f"✗ 湿度更新失败: {e}") + + def _update_wind(self, weather_params: Dict, dt: float): + """ + 更新风 + + Args: + weather_params: 天气参数 + dt: 时间增量 + """ + try: + # 基础风速受天气影响 + base_wind_speed = weather_params.get('wind_speed', 3.0) + + # 添加随机扰动 + turbulence = self.weather_phenomena['wind']['turbulence'] + wind_variation = random.uniform(-turbulence, turbulence) * base_wind_speed + + # 更新风速 + self.current_conditions['wind_speed'] = max(0.0, base_wind_speed + wind_variation) + + # 更新风向(缓慢变化) + direction_variability = self.weather_phenomena['wind']['direction_variability'] + direction_change = random.uniform(-direction_variability, direction_variability) * 180.0 * dt + self.current_conditions['wind_direction'] = (self.current_conditions['wind_direction'] + direction_change) % 360.0 + + except Exception as e: + print(f"✗ 风更新失败: {e}") + + def _update_precipitation(self, weather_params: Dict, dt: float): + """ + 更新降水 + + Args: + weather_params: 天气参数 + dt: 时间增量 + """ + try: + # 获取降水强度 + precipitation = weather_params.get('precipitation', 0.0) + self.current_conditions['precipitation'] = precipitation + + # 根据天气类型设置降水类型 + current_weather = self.plugin.weather_manager.get_current_weather() if self.plugin.weather_manager else 'clear' + + if precipitation > 0: + if current_weather == 'snow': + self.current_conditions['precipitation_type'] = 'snow' + elif current_weather == 'hail': + self.current_conditions['precipitation_type'] = 'hail' + else: + self.current_conditions['precipitation_type'] = 'rain' + else: + self.current_conditions['precipitation_type'] = 'none' + + except Exception as e: + print(f"✗ 降水更新失败: {e}") + + def _update_pressure(self, weather_params: Dict, dt: float): + """ + 更新气压 + + Args: + weather_params: 天气参数 + dt: 时间增量 + """ + try: + # 基础气压 + base_pressure = self.current_conditions['pressure'] + + # 添加随机变化 + pressure_change = random.uniform(-0.5, 0.5) + self.current_conditions['pressure'] = max(950.0, min(1050.0, base_pressure + pressure_change)) + + except Exception as e: + print(f"✗ 气压更新失败: {e}") + + def _update_visibility(self, weather_params: Dict): + """ + 更新能见度 + + Args: + weather_params: 天气参数 + """ + try: + # 基础能见度受天气影响 + visibility_factor = weather_params.get('visibility', 1.0) + base_visibility = self.weather_phenomena['visibility']['range'][1] # 最大能见度 + + # 计算最终能见度 + final_visibility = base_visibility * visibility_factor + + # 降水影响 + precipitation = self.current_conditions['precipitation'] + precipitation_effect = self.weather_phenomena['visibility']['precipitation_effect'] + final_visibility *= (1.0 - precipitation * precipitation_effect) + + self.current_conditions['visibility'] = max(10.0, final_visibility) + + except Exception as e: + print(f"✗ 能见度更新失败: {e}") + + def _update_uv_index(self): + """更新UV指数""" + try: + # 简化的UV指数计算 + if self.plugin.environment_effects: + time_config = getattr(self.plugin.environment_effects, 'time_config', {'day_time': 12.0}) + current_time = time_config.get('day_time', 12.0) + + # 正午最高,早晚为0 + if 8 <= current_time <= 16: + hour_factor = 1.0 - abs(current_time - 12.0) / 4.0 + else: + hour_factor = 0.0 + + # 晴天时UV指数最高 + current_weather = self.plugin.weather_manager.get_current_weather() if self.plugin.weather_manager else 'clear' + weather_factor = 1.0 if current_weather == 'clear' else 0.5 + + self.current_conditions['uv_index'] = max(0.0, 10.0 * hour_factor * weather_factor) + else: + self.current_conditions['uv_index'] = 2.0 + + except Exception as e: + print(f"✗ UV指数更新失败: {e}") + + def _update_physical_effects(self): + """更新天气物理效果""" + try: + precipitation = self.current_conditions['precipitation'] + precipitation_type = self.current_conditions['precipitation_type'] + temperature = self.current_conditions['temperature'] + wind_speed = self.current_conditions['wind_speed'] + + # 更新表面湿润度 + if precipitation_type in ['rain', 'hail']: + self.physical_effects['surface_wetness'] = min(1.0, precipitation * 2.0) + elif precipitation_type == 'snow': + self.physical_effects['surface_wetness'] = min(0.3, precipitation) + else: + # 无降水时逐渐干燥 + self.physical_effects['surface_wetness'] = max(0.0, self.physical_effects['surface_wetness'] - 0.01) + + # 更新表面结冰 + if temperature < 0 and self.physical_effects['surface_wetness'] > 0.1: + self.physical_effects['surface_ice'] = min(1.0, self.physical_effects['surface_wetness'] * 0.5) + else: + self.physical_effects['surface_ice'] = max(0.0, self.physical_effects['surface_ice'] - 0.02) + + # 更新摩擦系数 + wetness = self.physical_effects['surface_wetness'] + ice = self.physical_effects['surface_ice'] + self.physical_effects['friction_modifier'] = max(0.1, 1.0 - wetness * 0.5 - ice * 0.8) + + # 更新移动速度 + self.physical_effects['movement_speed_modifier'] = self.physical_effects['friction_modifier'] * 0.8 + 0.2 + + # 更新声音衰减 + self.physical_effects['sound_attenuation'] = 1.0 - precipitation * 0.3 + + # 更新光线散射 + self.physical_effects['light_scattering'] = precipitation * 0.5 + (1.0 - self.current_conditions['visibility'] / 10000.0) * 0.5 + + except Exception as e: + print(f"✗ 物理效果更新失败: {e}") + + def _generate_weather_forecast(self): + """生成天气预报""" + try: + self.weather_forecast = [] + + # 获取当前天气 + current_weather = self.plugin.weather_manager.get_current_weather() if self.plugin.weather_manager else 'clear' + + # 生成未来几小时的预报 + for i in range(self.forecast_duration): + # 简化的预报生成 + forecast_entry = { + 'hour': i, + 'weather': current_weather, + 'temperature': self.current_conditions['temperature'] + random.uniform(-2, 2), + 'precipitation': self.current_conditions['precipitation'] + } + self.weather_forecast.append(forecast_entry) + + except Exception as e: + print(f"✗ 天气预报生成失败: {e}") + + def _update_weather_forecast(self): + """更新天气预报""" + try: + # 移除过期的预报 + if self.weather_forecast: + self.weather_forecast.pop(0) + + # 添加新的预报 + current_weather = self.plugin.weather_manager.get_current_weather() if self.plugin.weather_manager else 'clear' + new_entry = { + 'hour': len(self.weather_forecast), + 'weather': current_weather, + 'temperature': self.current_conditions['temperature'] + random.uniform(-2, 2), + 'precipitation': self.current_conditions['precipitation'] + } + self.weather_forecast.append(new_entry) + + except Exception as e: + print(f"✗ 天气预报更新失败: {e}") + + def get_current_conditions(self) -> Dict[str, Any]: + """ + 获取当前天气条件 + + Returns: + 当前天气条件字典 + """ + return self.current_conditions.copy() + + def get_physical_effects(self) -> Dict[str, float]: + """ + 获取天气物理效果 + + Returns: + 天气物理效果字典 + """ + return self.physical_effects.copy() + + def get_weather_phenomena_list(self) -> List[str]: + """ + 获取天气现象列表 + + Returns: + 天气现象名称列表 + """ + return list(self.weather_phenomena.keys()) + + def get_weather_forecast(self, hours: int = 24) -> List[Dict[str, Any]]: + """ + 获取天气预报 + + Args: + hours: 预报时长(小时) + + Returns: + 天气预报列表 + """ + return self.weather_forecast[:hours] + + def get_weather_zone(self, zone_id: str) -> Optional[Dict[str, Any]]: + """ + 获取天气区域信息 + + Args: + zone_id: 区域ID + + Returns: + 天气区域信息或None + """ + return self.weather_zones.get(zone_id) + + def set_weather_zone(self, zone_id: str, zone_data: Dict[str, Any]): + """ + 设置天气区域 + + Args: + zone_id: 区域ID + zone_data: 区域数据 + """ + self.weather_zones[zone_id] = zone_data + print(f"✓ 天气区域已设置: {zone_id}") + + def remove_weather_zone(self, zone_id: str): + """ + 移除天气区域 + + Args: + zone_id: 区域ID + """ + if zone_id in self.weather_zones: + del self.weather_zones[zone_id] + print(f"✓ 天气区域已移除: {zone_id}") + + def get_weather_impact_on_object(self, object_position: Tuple[float, float, float]) -> Dict[str, Any]: + """ + 获取天气对特定位置物体的影响 + + Args: + object_position: 物体位置(x, y, z) + + Returns: + 天气影响字典 + """ + try: + # 简化实现,实际中需要考虑物体与天气区域的关系 + impact = { + 'wetness': self.physical_effects['surface_wetness'], + 'icing': self.physical_effects['surface_ice'], + 'wind_effect': self.current_conditions['wind_speed'] / 50.0, + 'visibility_factor': self.current_conditions['visibility'] / 10000.0, + 'temperature': self.current_conditions['temperature'] + } + + return impact + + except Exception as e: + print(f"✗ 物体天气影响计算失败: {e}") + return {} + + def set_update_interval(self, interval: float): + """ + 设置更新间隔 + + Args: + interval: 更新间隔(秒) + """ + self.update_interval = max(0.1, interval) + print(f"✓ 天气更新间隔设置为: {interval} 秒") + + def get_stats(self) -> Dict[str, Any]: + """ + 获取统计信息 + + Returns: + 统计信息字典 + """ + return self.stats.copy() + + def reset_stats(self): + """重置统计信息""" + self.stats = { + 'conditions_updated': 0, + 'weather_changes': 0, + 'phenomena_occurred': 0, + 'total_weather_time': 0.0 + } + print("✓ 天气系统统计信息已重置") + + def is_weather_phenomenon_active(self, phenomenon: str) -> bool: + """ + 检查天气现象是否活跃 + + Args: + phenomenon: 天气现象名称 + + Returns: + 天气现象是否活跃 + """ + try: + if phenomenon == 'precipitation': + return self.current_conditions['precipitation'] > 0 + elif phenomenon == 'wind': + return self.current_conditions['wind_speed'] > 1.0 + elif phenomenon == 'extreme_temperature': + temp = self.current_conditions['temperature'] + return temp < 0 or temp > 35 + return False + + except Exception as e: + print(f"✗ 天气现象检查失败: {e}") return False \ No newline at end of file diff --git a/project/__init__.py b/project/__init__.py index eb6b69e2..70296623 100644 --- a/project/__init__.py +++ b/project/__init__.py @@ -1,14 +1,14 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -项目管理包 - 提供项目生命周期管理功能 -""" - -from .project_manager import ProjectManager, NewProjectDialog - -__all__ = ['ProjectManager', 'NewProjectDialog'] - -# 版本信息 -__version__ = '1.0.0' +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +项目管理包 - 提供项目生命周期管理功能 +""" + +from .project_manager import ProjectManager, NewProjectDialog + +__all__ = ['ProjectManager', 'NewProjectDialog'] + +# 版本信息 +__version__ = '1.0.0' __author__ = 'Project Management System' \ No newline at end of file diff --git a/project/project_manager.py b/project/project_manager.py index adba33e0..b4272e8e 100644 --- a/project/project_manager.py +++ b/project/project_manager.py @@ -1,1230 +1,1230 @@ -import os -import sys -import json -import datetime -import subprocess -import shutil -from PyQt5.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QGroupBox, QLineEdit, QPushButton, - QLabel, QDialogButtonBox, QFileDialog, QMessageBox, QMainWindow -) -from PyQt5.QtCore import Qt - -# 导入自定义对话框 -from ui.widgets import NewProjectDialog, UniversalMessageDialog - -class ProjectManager: - - def __init__(self, world): - - self.world = world - self.current_project_path = None - self.project_config = None - - print("✓ 项目管理系统初始化完成") - - # ==================== 项目生命周期管理 ==================== - - def createNewProject(self, parent_window): - """创建新项目""" - dialog = NewProjectDialog(parent_window) - if dialog.exec_() != QDialog.Accepted: - return False - - project_path = dialog.projectPath - project_name = dialog.projectName - # full_project_path = os.path.join(project_path, project_name) - full_project_path = os.path.normpath(os.path.join(project_path, project_name)) - print(f"full_project_path: {full_project_path}") - - try: - # 创建项目文件夹结构 - os.makedirs(full_project_path) - os.makedirs(os.path.join(full_project_path, "models")) # 模型文件夹 - os.makedirs(os.path.join(full_project_path, "textures")) # 贴图文件夹 - scenes_path = os.path.join(full_project_path, "scenes") # 场景文件夹 - os.makedirs(scenes_path) - - # 创建项目配置文件 - project_config = { - "name": project_name, - "path": full_project_path, - "created_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "last_modified": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "version": "1.0.0", - "engine_version": "1.0.0" - } - - config_file = os.path.join(full_project_path, "project.json") - with open(config_file, "w", encoding="utf-8") as f: - json.dump(project_config, f, ensure_ascii=False, indent=4) - - print(f"项目配置文件已创建: {config_file}") - - # 清空当前场景 - self._clearCurrentScene() - - # 自动保存初始场景 - scene_file = os.path.join(scenes_path, "scene.bam") - if self.world.scene_manager.saveScene(scene_file, project_path): - # 更新配置文件中的场景路径 - project_config["scene_file"] = os.path.relpath(scene_file, full_project_path) - with open(config_file, "w", encoding="utf-8") as f: - json.dump(project_config, f, ensure_ascii=False, indent=4) - print(f"初始场景已保存到: {scene_file}") - else: - print("初始场景保存失败") - - # 更新项目状态 - self.current_project_path = full_project_path - self.project_config = project_config - - # 更新文件浏览器到新项目路径 - if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'): - parent_window.fileView.setRootIndex(parent_window.fileModel.index(full_project_path)) - - # 更新窗口标题 - self.updateWindowTitle(parent_window, project_name) - - # 保存当前项目路径到主窗口 - parent_window.current_project_path = full_project_path - - # 显示成功消息 - UniversalMessageDialog.show_success( - parent_window, - "成功", - f"项目 '{project_name}' 创建成功!", - show_cancel=False, - confirm_text="确认" - ) - return True - - except Exception as e: - UniversalMessageDialog.show_error( - parent_window, - "错误", - f"创建项目失败: {str(e)}", - show_cancel=False, - confirm_text="确认" - ) - return False - - def openProject(self, parent_window): - """打开项目""" - try: - # 获取项目路径 - project_path = QFileDialog.getExistingDirectory( - parent_window, - "选择项目文件夹", - "", - QFileDialog.ShowDirsOnly - ) - - if not project_path: - return False - - # 检查是否是有效的项目文件夹 - config_file = os.path.join(project_path, "project.json") - if not os.path.exists(config_file): - UniversalMessageDialog.show_warning( - parent_window, - "警告", - "选择的不是有效的项目文件夹!", - show_cancel=False, - confirm_text="确认" - ) - return False - - # 读取项目配置 - with open(config_file, "r", encoding="utf-8") as f: - project_config = json.load(f) - - # 检查场景文件 - scene_file = os.path.join(project_path, "scenes", "scene.bam") - if os.path.exists(scene_file): - # 加载场景 - if self.world.scene_manager.loadScene(scene_file): - # 更新项目配置 - project_config["scene_file"] = os.path.relpath(scene_file, project_path) - - project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - with open(config_file, "w", encoding="utf-8") as f: - json.dump(project_config, f, ensure_ascii=False, indent=4) - - # 更新项目状态 - self.current_project_path = project_path - self.project_config = project_config - - # 保存当前项目路径到主窗口 - parent_window.current_project_path = project_path - - # 更新窗口标题 - project_name = os.path.basename(project_path) - self.updateWindowTitle(parent_window, project_name) - - # 更新文件浏览器 - if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'): - parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path)) - - UniversalMessageDialog.show_success( - parent_window, - "成功", - "项目加载成功!", - show_cancel=False, - confirm_text="确认" - ) - return True - # 检查场景文件 - # scene_file = os.path.join(project_path, "scenes", "scene.bam") - # if not os.path.exists(scene_file): - # QMessageBox.warning(parent_window, "警告", "没有找到场景文件!") - # return False - # - # # 加载场景 - # if self.world.scene_manager.loadScene(scene_file): - # # 更新项目配置 - # project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - # project_config["scene_file"] = os.path.relpath(scene_file, project_path) - # - # with open(config_file, "w", encoding="utf-8") as f: - # json.dump(project_config, f, ensure_ascii=False, indent=4) - # - # # 更新项目状态 - # self.current_project_path = project_path - # self.project_config = project_config - # - # # 保存当前项目路径到主窗口 - # parent_window.current_project_path = project_path - # - # # 更新窗口标题 - # project_name = os.path.basename(project_path) - # self.updateWindowTitle(parent_window, project_name) - # - # # 更新文件浏览器 - # if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'): - # parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path)) - # - # QMessageBox.information(parent_window, "成功", "项目加载成功!") - # return True - # else: - # QMessageBox.warning(parent_window, "错误", "加载场景失败!") - # return False - - except Exception as e: - UniversalMessageDialog.show_error( - parent_window, - "错误", - f"加载项目时发生错误:{str(e)}", - show_cancel=False, - confirm_text="确认" - ) - return False - - def openProjectForPath(self, project_path, parent_window=None): - """通过路径打开项目 - - Args: - project_path: 项目路径 - parent_window: 父窗口对象(可选) - """ - try: - if not project_path: - return False - # 检查是否是有效的项目文件夹 - config_file = os.path.join(project_path, "project.json") - if not os.path.exists(config_file): - if parent_window: - UniversalMessageDialog.show_warning( - parent_window, - "警告", - f"选择的不是有效的项目文件夹!{project_path}", - show_cancel=False, - confirm_text="确认" - ) - else: - print("警告: 选择的不是有效的项目文件夹!") - return False - - # 读取项目配置 - with open(config_file, "r", encoding="utf-8") as f: - project_config = json.load(f) - - # 检查场景文件 - scene_file = os.path.join(project_path, "scenes", "scene.bam") - if os.path.exists(scene_file): - # 加载场景 - if self.world.scene_manager.loadScene(scene_file): - # 更新项目配置 - project_config["scene_file"] = os.path.relpath(scene_file, project_path) - - project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - with open(config_file, "w", encoding="utf-8") as f: - json.dump(project_config, f, ensure_ascii=False, indent=4) - - # 更新项目状态 - self.current_project_path = project_path - self.project_config = project_config - - # 如果有父窗口,更新相关UI元素 - if parent_window: - # 保存当前项目路径到主窗口 - parent_window.current_project_path = project_path - - # 更新窗口标题 - project_name = os.path.basename(project_path) - self.updateWindowTitle(parent_window, project_name) - - # 更新文件浏览器 - if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'): - parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path)) - - UniversalMessageDialog.show_success( - parent_window, - "成功", - "项目加载成功!", - show_cancel=False, - confirm_text="确认" - ) - - print(f"项目 '{project_path}' 加载成功!") - return True - else: - if parent_window: - UniversalMessageDialog.show_warning( - parent_window, - "错误", - "加载场景失败!", - show_cancel=False, - confirm_text="确认" - ) - else: - print("错误: 加载场景失败!") - return False - - except Exception as e: - error_msg = f"加载项目时发生错误:{str(e)}" - if parent_window: - UniversalMessageDialog.show_error( - parent_window, - "错误", - error_msg, - show_cancel=False, - confirm_text="确认" - ) - else: - print(error_msg) - return False - - def saveProject(self, parent_window): - """保存项目""" - try: - # 检查是否有当前项目路径 - if not self.current_project_path: - # 尝试从主窗口获取 - if hasattr(parent_window, 'current_project_path'): - self.current_project_path = parent_window.current_project_path - else: - UniversalMessageDialog.show_warning( - parent_window, - "警告", - "请先创建或打开一个项目!", - show_cancel=False, - confirm_text="确认" - ) - return False - - project_path = self.current_project_path - scenes_path = os.path.join(project_path, "scenes") - - # 固定的场景文件名 - scene_file = os.path.join(scenes_path, "scene.bam") - - # 如果存在旧文件,先删除 - if os.path.exists(scene_file): - try: - os.remove(scene_file) - print(f"已删除旧场景文件: {scene_file}") - except Exception as e: - print(f"删除旧场景文件失败: {str(e)}") - return False - - # 保存场景 - if self.world.scene_manager.saveScene(scene_file, project_path): - # 更新项目配置文件 - config_file = os.path.join(project_path, "project.json") - if os.path.exists(config_file): - with open(config_file, "r", encoding="utf-8") as f: - project_config = json.load(f) - - # 更新最后修改时间 - project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - # 记录场景文件路径 - project_config["scene_file"] = os.path.relpath(scene_file, project_path) - - with open(config_file, "w", encoding="utf-8") as f: - json.dump(project_config, f, ensure_ascii=False, indent=4) - - # 更新项目配置 - self.project_config = project_config - - # 更新窗口标题 - project_name = os.path.basename(project_path) - self.updateWindowTitle(parent_window, project_name) - - UniversalMessageDialog.show_success( - parent_window, - "成功", - "项目保存成功!", - show_cancel=False, - confirm_text="确认" - ) - return True - else: - UniversalMessageDialog.show_warning( - parent_window, - "错误", - "保存场景失败!", - show_cancel=False, - confirm_text="确认" - ) - return False - - except Exception as e: - UniversalMessageDialog.show_error( - parent_window, - "错误", - f"保存项目时发生错误:{str(e)}", - show_cancel=False, - confirm_text="确认" - ) - return False - - # ==================== 项目打包功能 ==================== - - def buildPackage(self, parent_window): - """打包项目为可执行文件 - 按照Panda3D官方标准方法""" - try: - # 检查是否有当前项目路径 - if not self.current_project_path: - if hasattr(parent_window, 'current_project_path'): - self.current_project_path = parent_window.current_project_path - else: - QMessageBox.warning(parent_window, "警告", "请先创建或打开一个项目!") - return False - - project_path = self.current_project_path - scenes_path = os.path.join(project_path, "scenes") - scene_file = os.path.join(scenes_path, "scene.bam") - - # 检查场景文件是否存在 - if not os.path.exists(scene_file): - QMessageBox.warning(parent_window, "警告", "请先保存场景!") - return False - - if hasattr(self.world,'selection') and self.world.selection: - self.world.selection.clearSelection() - print("已取消场景中的物体选中状态") - - if self.world.scene_manager.saveScene(scene_file,project_path): - print("场景保存成功") - config_file = os.path.join(project_path,"project.json") - if os.path.exists(config_file): - with open(config_file,"r",encoding="utf-8") as f: - project_config = json.load(f) - - project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - project_config["scene_file"] = os.path.relpath(scene_file,project_path) - - with open(config_file,"w",encoding = "utf-8") as f: - json.dump(project_config,f,ensure_ascii = False,indent=4) - - self.project_config = project_config - - project_name = os.path.basename(project_path) - self.updateWindowTitle(parent_window,project_name) - else: - QMessageBox.warning(parent_window,"错误","保存场景失败!") - return False - - build_dir = QFileDialog.getExistingDirectory( - parent_window, - "选择打包输出目录", - project_path, - QFileDialog.ShowDirsOnly - ) - - if not build_dir: - return False - - # 创建构建目录 - build_dir = os.path.join(build_dir, "build") - - if not os.path.exists(build_dir): - os.makedirs(build_dir) - - # 创建标准的打包文件 - self._createStandardBuildFiles(build_dir, project_path, scene_file) - - # 执行打包命令 - success = self._executeStandardBuild(build_dir, parent_window) - - if success: - # 根据操作系统创建对应的启动脚本文件 - try: - import stat - # 检查操作系统类型 - if os.name == 'nt': # Windows系统 - run_bat_path = os.path.join(build_dir, "run.bat") - with open(run_bat_path, "w") as f: - f.write("@echo off\n") - f.write("python main.py %*\n") - else: # Unix-like系统 (Linux, macOS等) - run_sh_path = os.path.join(build_dir, "run.sh") - with open(run_sh_path, "w") as f: - f.write("#!/bin/bash\n") - f.write("python3 main.py \"$@\"\n") - - # 为Unix-like系统添加执行权限 - st = os.stat(run_sh_path) - os.chmod(run_sh_path, st.st_mode | stat.S_IEXEC) - except Exception as e: - print(f"创建启动脚本文件失败: {str(e)}") - - QMessageBox.information(parent_window, "成功", - "打包完成!\n可执行文件在 build 目录中。\n") - return True - else: - return False - - except Exception as e: - QMessageBox.critical(parent_window, "错误", f"打包过程出错:{str(e)}") - return False - - def _createStandardBuildFiles(self, build_dir, project_path, scene_file): - """创建标准的Panda3D打包文件""" - project_name = os.path.basename(project_path) - - # 确保构建目录存在 - if not os.path.exists(build_dir): - os.makedirs(build_dir) - - # 复制场景文件到构建目录 - shutil.copy2(scene_file, os.path.join(build_dir, "scene.bam")) - - # 复制Resources文件夹到build目录 - source_resources = os.path.join(project_path, "scenes", "resources") - self.copy_folder(source_resources, build_dir) - - self._saveGUIElementsToJSON(build_dir, project_path) - - self._copyScriptsToBuild(build_dir, project_path) - - self._copyScriptSystemToBuild(build_dir) - - source_render_pipeline = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),"RenderPipelineFile") - dest_render_pipeline = os.path.join(build_dir,"RenderPipelineFile") - - if os.path.exists(source_render_pipeline): - if os.path.exists(dest_render_pipeline): - shutil.rmtree(dest_render_pipeline) - - shutil.copytree( - source_render_pipeline, - dest_render_pipeline, - ignore=shutil.ignore_patterns('__pycache__','*.pyc','.git','.vscode','*.log','samples') - ) - print("✓ RenderPipelineFile文件夹已复制到build目录") - else: - print("⚠️ RenderPipelineFile文件夹未找到") - - # 创建标准的应用程序入口文件 - self._createAppFile(build_dir, project_name) - - # 创建标准的setup.py文件 - self._createStandardSetupFile(build_dir, project_name) - - #创建requirements.txt文件 - self._createRequirementsFile(build_dir) - - def _copyScriptsToBuild(self, build_dir, project_path): - """复制脚本文件到构建目录的scripts文件夹""" - try: - # 创建目标scripts目录 - scripts_dest = os.path.join(build_dir, "scripts") - - # 正确的源scripts目录路径 - scripts_src = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts") - - # 如果上面的路径不存在,尝试项目路径下的scripts目录 - if not os.path.exists(scripts_src): - scripts_src = os.path.join(project_path, "scripts") - - if os.path.exists(scripts_src): - # 直接复制整个scripts目录 - if os.path.exists(scripts_dest): - shutil.rmtree(scripts_dest) - - shutil.copytree( - scripts_src, - scripts_dest, - ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log') - ) - print("✓ Scripts目录已复制到build/scripts") - else: - # 创建空的scripts目录 - if not os.path.exists(scripts_dest): - os.makedirs(scripts_dest) - print("⚠️ 项目中没有scripts目录") - - except Exception as e: - print(f"⚠️ 复制脚本文件时出错: {str(e)}") - - def _copyScriptSystemToBuild(self,build_dir): - core_files = [ - "script_system.py", - "InfoPanelManager.py", - "CustomMouseController.py" - ] - - source_core_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),"core") - - core_dest = os.path.join(build_dir,"core") - - if not os.path.exists(core_dest): - os.makedirs(core_dest) - - for file_name in core_files: - source_file = os.path.join(source_core_dir,file_name) - - if os.path.exists(source_file): - shutil.copy2(source_file,os.path.join(core_dest,file_name)) - - def _saveGUIElementsToJSON(self, build_dir, project_path): - """保存GUI元素到JSON文件,内容与_collectGUIElementInfo保持一致""" - try: - # 创建目标gui目录 - gui_dest = os.path.join(build_dir, "gui") - if not os.path.exists(gui_dest): - os.makedirs(gui_dest) - - # 收集所有GUI元素信息 - gui_data = [] - - # 获取当前场景中的GUI元素 - if hasattr(self.world, 'gui_elements'): - for gui_node in self.world.gui_elements: - if gui_node and not gui_node.isEmpty(): - # 使用_collectGUIElementInfo方法收集信息 - gui_info = self.world.scene_manager._collectGUIElementInfo(gui_node) - if gui_info: - self._updateResourcePaths(gui_info,project_path,build_dir) - gui_data.append(gui_info) - print(f"收集GUI元素信息: {gui_info['name']}") - - # 保存GUI信息到JSON文件 - gui_file_path = os.path.join(gui_dest, "gui_elements.json") - with open(gui_file_path, "w", encoding="utf-8") as f: - json.dump(gui_data, f, ensure_ascii=False, indent=4) - - print(f"✓ GUI元素数据已保存到 {gui_file_path},共 {len(gui_data)} 个元素") - return True - - except Exception as e: - print(f"⚠️ 保存GUI元素时出错: {str(e)}") - import traceback - traceback.print_exc() - return False - - def _updateResourcePaths(self,gui_info,project_path,build_dir): - def normalize_resource_path(old_path): - if not old_path: - return old_path - - if old_path.startswith(("http://","https://")): - return old_path - - if os.path.isabs(old_path): - try: - rel_path = os.path.relpath(old_path,project_path) - return os.path.join("resources",os.path.basename(rel_path)).replace("\\","/") - except Exception: - return os.path.join("resources",os.path.basename(old_path)).replace("\\","/") - else: - return os.path.join("resources",os.path.basename(old_path)).replace("\\","/") - if "image_path" in gui_info and gui_info["image_path"]: - gui_info["image_path"] = normalize_resource_path(gui_info["image_path"]) - if "video_path" in gui_info and gui_info["video_path"]: - gui_info["video_path"] = normalize_resource_path(gui_info["video_path"]) - if "bg_image_path" in gui_info and gui_info["bg_image_path"]: - gui_info["bg_image_path"] = normalize_resource_path(gui_info["bg_image_path"]) - - def _createRequirementsFile(self,build_dir): - requirements_content = """panda3d>=1.10.13""" - - requirements_path = os.path.join(build_dir,"requirements.txt") - with open(requirements_path,"w",encoding="utf-8") as f: - f.write(requirements_content) - - def copy_folder(self, source_folder, destination_folder): - """将一个文件夹从源路径复制到目标路径下的resources文件夹中 - - Args: - source_folder (str): 源文件夹路径 - destination_folder (str): 目标文件夹路径 - """ - try: - # 创建resources文件夹作为目标 - resources_dest = os.path.join(destination_folder, "resources") - - # 确保目标目录存在 - if not os.path.exists(destination_folder): - os.makedirs(destination_folder) - - # 如果目标resources文件夹已存在,先删除 - if os.path.exists(resources_dest): - shutil.rmtree(resources_dest) - - # 检查源文件夹是否存在 - if os.path.exists(source_folder): - # 复制整个文件夹到resources目录下 - shutil.copytree( - source_folder, - resources_dest, - ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log') - ) - print(f"✓ 文件夹已从 {source_folder} 复制到 {resources_dest}") - return True - else: - print(f"⚠️ 源文件夹不存在: {source_folder}") - # 即使源文件夹不存在,也创建空的resources目录 - if not os.path.exists(resources_dest): - os.makedirs(resources_dest) - return False - - except Exception as e: - print(f"⚠️ 复制文件夹时出错: {str(e)}") - return False - - def _copyResourcesToBuild(self, build_dir, project_path): - """复制GUI资源到构建目录的resources文件夹""" - try: - # 创建目标resources目录 - resources_dest = os.path.join(build_dir, "resources") - - # 源Resources目录 - resources_src = os.path.join(project_path, "Resources") - - if os.path.exists(resources_src): - # 直接复制整个Resources目录 - if os.path.exists(resources_dest): - shutil.rmtree(resources_dest) - - shutil.copytree( - resources_src, - resources_dest, - ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log') - ) - print("✓ Resources目录已复制到build/resources") - - # 统计复制的文件数量 - file_count = 0 - for root, dirs, files in os.walk(resources_dest): - file_count += len(files) - print(f"✓ 共复制了 {file_count} 个资源文件") - else: - # 创建空的resources目录 - if not os.path.exists(resources_dest): - os.makedirs(resources_dest) - print("⚠️ 项目中没有Resources目录") - - except Exception as e: - print(f"⚠️ 复制资源文件时出错: {str(e)}") - - def _collectResourceFiles(self, project_path): - """收集项目中GUI使用的资源文件""" - resource_files = set() - - try: - # 收集Resources目录中的所有文件(这是最主要的资源来源) - resources_dir = os.path.join(project_path, "Resources") - if os.path.exists(resources_dir): - for root, dirs, files in os.walk(resources_dir): - for file in files: - file_path = os.path.join(root, file) - # 收集所有文件,不仅仅是媒体文件 - resource_files.add(file_path) - - # 同时收集场景中引用的特定资源 - scene_file = os.path.join(project_path, "scenes", "scene.bam") - if os.path.exists(scene_file): - # 从场景文件中提取资源引用 - referenced_files = self._extractResourcesFromScene(scene_file, project_path) - for file_path in referenced_files: - if os.path.isabs(file_path): - if os.path.exists(file_path): - resource_files.add(file_path) - else: - # 相对路径 - full_path = os.path.join(project_path, file_path) - if os.path.exists(full_path): - resource_files.add(full_path) - - except Exception as e: - print(f"收集资源文件时出错: {str(e)}") - - return list(resource_files) - - def _extractResourcesFromScene(self, scene_file, project_path): - """从场景文件中提取资源引用""" - referenced_files = [] - - try: - # 这里应该实现从BAM文件中提取贴图、视频等资源引用的逻辑 - # 由于直接解析BAM文件比较复杂,我们采用间接方式 - - # 检查项目配置文件或其他元数据文件中可能包含的资源引用 - config_file = os.path.join(project_path, "project.json") - if os.path.exists(config_file): - try: - with open(config_file, "r", encoding="utf-8") as f: - project_config = json.load(f) - - # 如果配置中有资源列表信息,可以在这里处理 - # 这里暂时保持简单实现 - except Exception as e: - print(f"读取项目配置时出错: {str(e)}") - - except Exception as e: - print(f"从场景提取资源引用时出错: {str(e)}") - - return referenced_files - - def _isMediaFile(self, file_path): - """判断是否为媒体文件(图片或视频)""" - media_extensions = { - # 图片格式 - '.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tga', '.tiff', - # 视频格式 - '.mp4', '.avi', '.mov', '.wmv', '.mkv', '.webm', '.flv' - } - - _, ext = os.path.splitext(file_path.lower()) - return ext in media_extensions - - def _createAppFile(self, build_dir, project_name): - """创建应用程序主文件 - 通过复制模板文件""" - # 获取模板文件路径(假设模板文件在项目根目录下的templates文件夹中) - template_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), - "templates", "main_template.py") - - # 目标文件路径 - app_path = os.path.join(build_dir, "main.py") - - # 检查模板文件是否存在 - if os.path.exists(template_path): - # 直接复制模板文件 - shutil.copy2(template_path, app_path) - print(f"✓ 应用程序主文件已从模板创建: {app_path}") - - def _createStandardSetupFile(self, build_dir, project_name): - """创建优化的标准setup.py文件""" - setup_code = f'''#!/usr/bin/env python3 - # -*- coding: utf-8 -*- - - """ - {project_name} 打包配置文件 - 使用 Panda3D 标准打包工具 - """ - - from setuptools import setup - - # 应用程序配置 - APP_NAME = "{project_name}" - APP_VERSION = "1.0.0" - MAIN_SCRIPT = "main.py" - - setup( - name=APP_NAME, - version=APP_VERSION, - - # Panda3D 打包选项 - options={{ - 'build_apps': {{ - # GUI应用程序 - 'gui_apps': {{ - APP_NAME: MAIN_SCRIPT, - }}, - - # 包含的文件模式 - 'include_patterns': [ - '*.bam', # 场景文件 - '*.egg', # 模型文件 - '*.jpg', '*.png', '*.tga', '*.dds', # 纹理文件 - '*.wav', '*.ogg', '*.mp3', # 音频文件 - '*.ttf', '*.otf', # 字体文件 - 'config/*.yaml', # 配置文件 - 'shaders/**/*.glsl', # 着色器文件 - ], - - # 排除的文件模式 - 'exclude_patterns': [ - '*.pyc', - '__pycache__/**', - '.git/**', - '.vscode/**', - '*.log', - '**/*.tmp', - '**/temp/**', - 'samples/**', # 示例文件 - '**/.DS_Store', - '**/Thumbs.db', - ], - - # Panda3D 插件 - 'plugins': [ - 'pandagl', # OpenGL渲染器 - 'pandaegl', # EGL支持 - 'p3assimp', # Assimp模型导入 - 'p3ffmpeg', # FFmpeg视频支持 - 'p3openal_audio', # OpenAL音频 - ], - - # 包含的Python模块 - 'include_modules': {{ - '*': [ - # 核心模块 - 'direct.showbase.ShowBase', - 'direct.task', - 'direct.actor', - 'direct.interval', - 'direct.gui', - 'direct.stdpy.file', - 'direct.stdpy.pickle', - 'panda3d.core', - 'panda3d.direct', - - # 渲染管线相关 - 'rpcore', - 'rpcore.util.movement_controller', - 'rpcore.native', - 'rpcore.render_pipeline', - 'rplibs', - 'rpplugins', - - # 标准库 - 'json', - 'os', - 'sys', - 'math', - 'random', - 'collections', - 'copy', - 'itertools', - 'importlib', - ], - }}, - - # 排除的Python模块(减小体积) - 'exclude_modules': {{ - '*': [ - # GUI框架 - 'tkinter', - 'matplotlib', - 'numpy', - 'scipy', - 'PIL', - 'wx', - 'PyQt5', - 'PySide2', - 'kivy', - - # 开发工具 - 'setuptools', - 'pip', - 'wheel', - 'twine', - 'pytest', - 'unittest', - - # 其他不必要的模块 - 'sqlite3', - 'xmlrpc', - 'ftplib', - 'telnetlib', - ], - }}, - - # 平台设置 - 'platforms': [ - 'win_amd64', # Windows 64位 - 'linux_x86_64', # Linux 64位 - ], - - # 优化设置 - 'strip_docstrings': True, # 移除文档字符串 - 'optimize': 1, # 字节码优化级别 - }}, - }}, - - # 标准setuptools选项 - author="Panda3D Engine Editor User", - author_email="user@example.com", - description=f"{{APP_NAME}} - 使用Panda3D创建的3D应用程序", - long_description="这是一个使用Panda3D引擎编辑器创建的3D应用程序。", - - # 依赖项 - install_requires=[ - 'panda3d>=1.10.13', - ], - - # Python版本要求 - python_requires='>=3.7', - - # 分类信息 - classifiers=[ - 'Development Status :: 4 - Beta', - 'Intended Audience :: End Users/Desktop', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Topic :: Games/Entertainment', - 'Topic :: Multimedia :: Graphics :: 3D Rendering', - ], - - license='MIT', - ) - ''' - - setup_path = os.path.join(build_dir, "setup.py") - with open(setup_path, "w", encoding="utf-8") as f: - f.write(setup_code) - - def _executeStandardBuild(self, build_dir, parent_window): - """执行标准的Panda3D打包命令 - 增强版""" - try: - print(f"开始打包,工作目录: {build_dir}") - - # 显示打包进度提示 - QMessageBox.information(parent_window, "打包进行中", - "正在打包项目,请稍候...\n" - "此过程可能需要几分钟时间。\n" - "请勿关闭程序。") - - # 使用线程执行打包以避免界面冻结 - import threading - from PyQt5.QtCore import pyqtSignal, QObject - - class BuildSignals(QObject): - finished = pyqtSignal(bool, str) - progress = pyqtSignal(str) - - signals = BuildSignals() - - def build_process(): - try: - print("执行标准打包命令: python setup.py bdist_apps") - signals.progress.emit("正在初始化打包过程...") - - process = subprocess.Popen( - [sys.executable, "setup.py", "bdist_apps"], - cwd=build_dir, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - encoding='utf-8' - ) - - while True: - output = process.stdout.readline() - if output == '' and process.poll() is not None: - break - if output: - print(output.strip()) - if "copying" in output.lower(): - signals.progress.emit(f"正在处理文件: {output.strip()[-50:]}") - elif "building" in output.lower(): - signals.progress.emit(output.strip()) - - # 获取错误输出 - stderr = process.stderr.read() - if stderr: - print("错误输出:", stderr) - - except Exception as e: - print(f"执行打包命令时出错: {str(e)}") - signals.finished.emit(False, f"打包失败:{str(e)}") - - # 连接信号 - def on_finished(success, message): - if success: - QMessageBox.information(parent_window, "成功", message) - else: - QMessageBox.critical(parent_window, "错误", message) - - def on_progress(message): - print(f"[打包进度] {message}") - - signals.finished.connect(on_finished) - signals.progress.connect(on_progress) - - # 启动打包线程 - build_thread = threading.Thread(target=build_process) - build_thread.daemon = True - build_thread.start() - - return True # 线程已启动 - - except Exception as e: - print(f"启动打包过程时出错: {str(e)}") - QMessageBox.critical(parent_window, "错误", f"启动打包失败:{str(e)}") - return False - - def _tryBuildAppsThreaded(self, build_dir, signals): - """尝试使用 build_apps 命令(线程安全版本)""" - try: - print("执行备用打包命令: python setup.py build_apps") - signals.progress.emit("正在执行备用打包方法...") - - process = subprocess.Popen( - [sys.executable, "setup.py", "build_apps"], - cwd=build_dir, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - encoding='utf-8' - ) - - # 实时显示输出 - while True: - output = process.stdout.readline() - if output == '' and process.poll() is not None: - break - if output: - print(output.strip()) - signals.progress.emit(output.strip()) - - stderr = process.stderr.read() - if stderr: - print("错误输出:", stderr) - - if process.returncode == 0: - print("✓ build_apps 成功完成") - signals.finished.emit(True, "打包成功完成!\n可执行文件在 dist/ 目录中。") - else: - error_msg = f"打包失败,返回码:{process.returncode}" - if stderr: - error_msg += f"\n错误信息:{stderr}" - signals.finished.emit(False, error_msg) - - except Exception as e: - signals.finished.emit(False, f"执行 build_apps 失败:{str(e)}") - - def _tryBuildApps(self, build_dir, parent_window): - """尝试使用 build_apps 命令""" - try: - print("执行备用打包命令: python setup.py build_apps") - - process = subprocess.Popen( - [sys.executable, "setup.py", "build_apps"], - cwd=build_dir, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - encoding='utf-8' - ) - - # 实时显示输出 - while True: - output = process.stdout.readline() - if output == '' and process.poll() is not None: - break - if output: - print(output.strip()) - - stderr = process.stderr.read() - if stderr: - print("错误输出:", stderr) - - if process.returncode == 0: - print("✓ build_apps 成功完成") - return True - else: - error_msg = f"打包失败,返回码:{process.returncode}" - if stderr: - error_msg += f"\\n错误信息:{stderr}" - QMessageBox.critical(parent_window, "错误", error_msg) - return False - - except Exception as e: - QMessageBox.critical(parent_window, "错误", f"执行 build_apps 失败:{str(e)}") - return False - - # ==================== 工具方法 ==================== - - def updateWindowTitle(self, window, project_name=None): - """更新窗口标题""" - base_title = "引擎编辑器" - if project_name: - window.setWindowTitle(f"{base_title} - {project_name}") - else: - window.setWindowTitle(base_title) - - def _clearCurrentScene(self): - """清空当前场景""" - # 移除所有模型 - for model in self.world.models: - model.removeNode() - self.world.models.clear() - - # 清空GUI元素 - for gui_element in self.world.gui_elements: - gui_element.removeNode() - self.world.gui_elements.clear() - - # 更新场景树 - if hasattr(self.world, 'updateSceneTree'): - self.world.updateSceneTree() - - def getCurrentProjectPath(self): - """获取当前项目路径""" - return self.current_project_path - - def getCurrentProjectConfig(self): - """获取当前项目配置""" - return self.project_config - - def isProjectOpen(self): - """检查是否有项目打开""" - return self.current_project_path is not None - - def getProjectName(self): - """获取当前项目名称""" - if self.current_project_path: - return os.path.basename(self.current_project_path) - return None - - def getProjectInfo(self): - """获取项目信息""" - if not self.project_config: - return None - - return { - "name": self.project_config.get("name", "未知项目"), - "path": self.project_config.get("path", ""), - "created_at": self.project_config.get("created_at", ""), - "last_modified": self.project_config.get("last_modified", ""), - "version": self.project_config.get("version", "1.0.0"), - "engine_version": self.project_config.get("engine_version", "1.0.0") - } - -# ==================== 便利函数 ==================== - -def updateWindowTitle(window, project_name=None): - """更新窗口标题 - 独立便利函数""" - base_title = "引擎编辑器" - if project_name: - window.setWindowTitle(f"{base_title} - {project_name}") - else: - window.setWindowTitle(base_title) +import os +import sys +import json +import datetime +import subprocess +import shutil +from PyQt5.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QGroupBox, QLineEdit, QPushButton, + QLabel, QDialogButtonBox, QFileDialog, QMessageBox, QMainWindow +) +from PyQt5.QtCore import Qt + +# 导入自定义对话框 +from ui.widgets import NewProjectDialog, UniversalMessageDialog + +class ProjectManager: + + def __init__(self, world): + + self.world = world + self.current_project_path = None + self.project_config = None + + print("✓ 项目管理系统初始化完成") + + # ==================== 项目生命周期管理 ==================== + + def createNewProject(self, parent_window): + """创建新项目""" + dialog = NewProjectDialog(parent_window) + if dialog.exec_() != QDialog.Accepted: + return False + + project_path = dialog.projectPath + project_name = dialog.projectName + # full_project_path = os.path.join(project_path, project_name) + full_project_path = os.path.normpath(os.path.join(project_path, project_name)) + print(f"full_project_path: {full_project_path}") + + try: + # 创建项目文件夹结构 + os.makedirs(full_project_path) + os.makedirs(os.path.join(full_project_path, "models")) # 模型文件夹 + os.makedirs(os.path.join(full_project_path, "textures")) # 贴图文件夹 + scenes_path = os.path.join(full_project_path, "scenes") # 场景文件夹 + os.makedirs(scenes_path) + + # 创建项目配置文件 + project_config = { + "name": project_name, + "path": full_project_path, + "created_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "last_modified": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "version": "1.0.0", + "engine_version": "1.0.0" + } + + config_file = os.path.join(full_project_path, "project.json") + with open(config_file, "w", encoding="utf-8") as f: + json.dump(project_config, f, ensure_ascii=False, indent=4) + + print(f"项目配置文件已创建: {config_file}") + + # 清空当前场景 + self._clearCurrentScene() + + # 自动保存初始场景 + scene_file = os.path.join(scenes_path, "scene.bam") + if self.world.scene_manager.saveScene(scene_file, project_path): + # 更新配置文件中的场景路径 + project_config["scene_file"] = os.path.relpath(scene_file, full_project_path) + with open(config_file, "w", encoding="utf-8") as f: + json.dump(project_config, f, ensure_ascii=False, indent=4) + print(f"初始场景已保存到: {scene_file}") + else: + print("初始场景保存失败") + + # 更新项目状态 + self.current_project_path = full_project_path + self.project_config = project_config + + # 更新文件浏览器到新项目路径 + if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'): + parent_window.fileView.setRootIndex(parent_window.fileModel.index(full_project_path)) + + # 更新窗口标题 + self.updateWindowTitle(parent_window, project_name) + + # 保存当前项目路径到主窗口 + parent_window.current_project_path = full_project_path + + # 显示成功消息 + UniversalMessageDialog.show_success( + parent_window, + "成功", + f"项目 '{project_name}' 创建成功!", + show_cancel=False, + confirm_text="确认" + ) + return True + + except Exception as e: + UniversalMessageDialog.show_error( + parent_window, + "错误", + f"创建项目失败: {str(e)}", + show_cancel=False, + confirm_text="确认" + ) + return False + + def openProject(self, parent_window): + """打开项目""" + try: + # 获取项目路径 + project_path = QFileDialog.getExistingDirectory( + parent_window, + "选择项目文件夹", + "", + QFileDialog.ShowDirsOnly + ) + + if not project_path: + return False + + # 检查是否是有效的项目文件夹 + config_file = os.path.join(project_path, "project.json") + if not os.path.exists(config_file): + UniversalMessageDialog.show_warning( + parent_window, + "警告", + "选择的不是有效的项目文件夹!", + show_cancel=False, + confirm_text="确认" + ) + return False + + # 读取项目配置 + with open(config_file, "r", encoding="utf-8") as f: + project_config = json.load(f) + + # 检查场景文件 + scene_file = os.path.join(project_path, "scenes", "scene.bam") + if os.path.exists(scene_file): + # 加载场景 + if self.world.scene_manager.loadScene(scene_file): + # 更新项目配置 + project_config["scene_file"] = os.path.relpath(scene_file, project_path) + + project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + with open(config_file, "w", encoding="utf-8") as f: + json.dump(project_config, f, ensure_ascii=False, indent=4) + + # 更新项目状态 + self.current_project_path = project_path + self.project_config = project_config + + # 保存当前项目路径到主窗口 + parent_window.current_project_path = project_path + + # 更新窗口标题 + project_name = os.path.basename(project_path) + self.updateWindowTitle(parent_window, project_name) + + # 更新文件浏览器 + if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'): + parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path)) + + UniversalMessageDialog.show_success( + parent_window, + "成功", + "项目加载成功!", + show_cancel=False, + confirm_text="确认" + ) + return True + # 检查场景文件 + # scene_file = os.path.join(project_path, "scenes", "scene.bam") + # if not os.path.exists(scene_file): + # QMessageBox.warning(parent_window, "警告", "没有找到场景文件!") + # return False + # + # # 加载场景 + # if self.world.scene_manager.loadScene(scene_file): + # # 更新项目配置 + # project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + # project_config["scene_file"] = os.path.relpath(scene_file, project_path) + # + # with open(config_file, "w", encoding="utf-8") as f: + # json.dump(project_config, f, ensure_ascii=False, indent=4) + # + # # 更新项目状态 + # self.current_project_path = project_path + # self.project_config = project_config + # + # # 保存当前项目路径到主窗口 + # parent_window.current_project_path = project_path + # + # # 更新窗口标题 + # project_name = os.path.basename(project_path) + # self.updateWindowTitle(parent_window, project_name) + # + # # 更新文件浏览器 + # if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'): + # parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path)) + # + # QMessageBox.information(parent_window, "成功", "项目加载成功!") + # return True + # else: + # QMessageBox.warning(parent_window, "错误", "加载场景失败!") + # return False + + except Exception as e: + UniversalMessageDialog.show_error( + parent_window, + "错误", + f"加载项目时发生错误:{str(e)}", + show_cancel=False, + confirm_text="确认" + ) + return False + + def openProjectForPath(self, project_path, parent_window=None): + """通过路径打开项目 + + Args: + project_path: 项目路径 + parent_window: 父窗口对象(可选) + """ + try: + if not project_path: + return False + # 检查是否是有效的项目文件夹 + config_file = os.path.join(project_path, "project.json") + if not os.path.exists(config_file): + if parent_window: + UniversalMessageDialog.show_warning( + parent_window, + "警告", + f"选择的不是有效的项目文件夹!{project_path}", + show_cancel=False, + confirm_text="确认" + ) + else: + print("警告: 选择的不是有效的项目文件夹!") + return False + + # 读取项目配置 + with open(config_file, "r", encoding="utf-8") as f: + project_config = json.load(f) + + # 检查场景文件 + scene_file = os.path.join(project_path, "scenes", "scene.bam") + if os.path.exists(scene_file): + # 加载场景 + if self.world.scene_manager.loadScene(scene_file): + # 更新项目配置 + project_config["scene_file"] = os.path.relpath(scene_file, project_path) + + project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + with open(config_file, "w", encoding="utf-8") as f: + json.dump(project_config, f, ensure_ascii=False, indent=4) + + # 更新项目状态 + self.current_project_path = project_path + self.project_config = project_config + + # 如果有父窗口,更新相关UI元素 + if parent_window: + # 保存当前项目路径到主窗口 + parent_window.current_project_path = project_path + + # 更新窗口标题 + project_name = os.path.basename(project_path) + self.updateWindowTitle(parent_window, project_name) + + # 更新文件浏览器 + if hasattr(parent_window, 'fileView') and hasattr(parent_window, 'fileModel'): + parent_window.fileView.setRootIndex(parent_window.fileModel.index(project_path)) + + UniversalMessageDialog.show_success( + parent_window, + "成功", + "项目加载成功!", + show_cancel=False, + confirm_text="确认" + ) + + print(f"项目 '{project_path}' 加载成功!") + return True + else: + if parent_window: + UniversalMessageDialog.show_warning( + parent_window, + "错误", + "加载场景失败!", + show_cancel=False, + confirm_text="确认" + ) + else: + print("错误: 加载场景失败!") + return False + + except Exception as e: + error_msg = f"加载项目时发生错误:{str(e)}" + if parent_window: + UniversalMessageDialog.show_error( + parent_window, + "错误", + error_msg, + show_cancel=False, + confirm_text="确认" + ) + else: + print(error_msg) + return False + + def saveProject(self, parent_window): + """保存项目""" + try: + # 检查是否有当前项目路径 + if not self.current_project_path: + # 尝试从主窗口获取 + if hasattr(parent_window, 'current_project_path'): + self.current_project_path = parent_window.current_project_path + else: + UniversalMessageDialog.show_warning( + parent_window, + "警告", + "请先创建或打开一个项目!", + show_cancel=False, + confirm_text="确认" + ) + return False + + project_path = self.current_project_path + scenes_path = os.path.join(project_path, "scenes") + + # 固定的场景文件名 + scene_file = os.path.join(scenes_path, "scene.bam") + + # 如果存在旧文件,先删除 + if os.path.exists(scene_file): + try: + os.remove(scene_file) + print(f"已删除旧场景文件: {scene_file}") + except Exception as e: + print(f"删除旧场景文件失败: {str(e)}") + return False + + # 保存场景 + if self.world.scene_manager.saveScene(scene_file, project_path): + # 更新项目配置文件 + config_file = os.path.join(project_path, "project.json") + if os.path.exists(config_file): + with open(config_file, "r", encoding="utf-8") as f: + project_config = json.load(f) + + # 更新最后修改时间 + project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + # 记录场景文件路径 + project_config["scene_file"] = os.path.relpath(scene_file, project_path) + + with open(config_file, "w", encoding="utf-8") as f: + json.dump(project_config, f, ensure_ascii=False, indent=4) + + # 更新项目配置 + self.project_config = project_config + + # 更新窗口标题 + project_name = os.path.basename(project_path) + self.updateWindowTitle(parent_window, project_name) + + UniversalMessageDialog.show_success( + parent_window, + "成功", + "项目保存成功!", + show_cancel=False, + confirm_text="确认" + ) + return True + else: + UniversalMessageDialog.show_warning( + parent_window, + "错误", + "保存场景失败!", + show_cancel=False, + confirm_text="确认" + ) + return False + + except Exception as e: + UniversalMessageDialog.show_error( + parent_window, + "错误", + f"保存项目时发生错误:{str(e)}", + show_cancel=False, + confirm_text="确认" + ) + return False + + # ==================== 项目打包功能 ==================== + + def buildPackage(self, parent_window): + """打包项目为可执行文件 - 按照Panda3D官方标准方法""" + try: + # 检查是否有当前项目路径 + if not self.current_project_path: + if hasattr(parent_window, 'current_project_path'): + self.current_project_path = parent_window.current_project_path + else: + QMessageBox.warning(parent_window, "警告", "请先创建或打开一个项目!") + return False + + project_path = self.current_project_path + scenes_path = os.path.join(project_path, "scenes") + scene_file = os.path.join(scenes_path, "scene.bam") + + # 检查场景文件是否存在 + if not os.path.exists(scene_file): + QMessageBox.warning(parent_window, "警告", "请先保存场景!") + return False + + if hasattr(self.world,'selection') and self.world.selection: + self.world.selection.clearSelection() + print("已取消场景中的物体选中状态") + + if self.world.scene_manager.saveScene(scene_file,project_path): + print("场景保存成功") + config_file = os.path.join(project_path,"project.json") + if os.path.exists(config_file): + with open(config_file,"r",encoding="utf-8") as f: + project_config = json.load(f) + + project_config["last_modified"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + project_config["scene_file"] = os.path.relpath(scene_file,project_path) + + with open(config_file,"w",encoding = "utf-8") as f: + json.dump(project_config,f,ensure_ascii = False,indent=4) + + self.project_config = project_config + + project_name = os.path.basename(project_path) + self.updateWindowTitle(parent_window,project_name) + else: + QMessageBox.warning(parent_window,"错误","保存场景失败!") + return False + + build_dir = QFileDialog.getExistingDirectory( + parent_window, + "选择打包输出目录", + project_path, + QFileDialog.ShowDirsOnly + ) + + if not build_dir: + return False + + # 创建构建目录 + build_dir = os.path.join(build_dir, "build") + + if not os.path.exists(build_dir): + os.makedirs(build_dir) + + # 创建标准的打包文件 + self._createStandardBuildFiles(build_dir, project_path, scene_file) + + # 执行打包命令 + success = self._executeStandardBuild(build_dir, parent_window) + + if success: + # 根据操作系统创建对应的启动脚本文件 + try: + import stat + # 检查操作系统类型 + if os.name == 'nt': # Windows系统 + run_bat_path = os.path.join(build_dir, "run.bat") + with open(run_bat_path, "w") as f: + f.write("@echo off\n") + f.write("python main.py %*\n") + else: # Unix-like系统 (Linux, macOS等) + run_sh_path = os.path.join(build_dir, "run.sh") + with open(run_sh_path, "w") as f: + f.write("#!/bin/bash\n") + f.write("python3.10 main.py \"$@\"\n") + + # 为Unix-like系统添加执行权限 + st = os.stat(run_sh_path) + os.chmod(run_sh_path, st.st_mode | stat.S_IEXEC) + except Exception as e: + print(f"创建启动脚本文件失败: {str(e)}") + + QMessageBox.information(parent_window, "成功", + "打包完成!\n可执行文件在 build 目录中。\n") + return True + else: + return False + + except Exception as e: + QMessageBox.critical(parent_window, "错误", f"打包过程出错:{str(e)}") + return False + + def _createStandardBuildFiles(self, build_dir, project_path, scene_file): + """创建标准的Panda3D打包文件""" + project_name = os.path.basename(project_path) + + # 确保构建目录存在 + if not os.path.exists(build_dir): + os.makedirs(build_dir) + + # 复制场景文件到构建目录 + shutil.copy2(scene_file, os.path.join(build_dir, "scene.bam")) + + # 复制Resources文件夹到build目录 + source_resources = os.path.join(project_path, "scenes", "resources") + self.copy_folder(source_resources, build_dir) + + self._saveGUIElementsToJSON(build_dir, project_path) + + self._copyScriptsToBuild(build_dir, project_path) + + self._copyScriptSystemToBuild(build_dir) + + source_render_pipeline = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),"RenderPipelineFile") + dest_render_pipeline = os.path.join(build_dir,"RenderPipelineFile") + + if os.path.exists(source_render_pipeline): + if os.path.exists(dest_render_pipeline): + shutil.rmtree(dest_render_pipeline) + + shutil.copytree( + source_render_pipeline, + dest_render_pipeline, + ignore=shutil.ignore_patterns('__pycache__','*.pyc','.git','.vscode','*.log','samples') + ) + print("✓ RenderPipelineFile文件夹已复制到build目录") + else: + print("⚠️ RenderPipelineFile文件夹未找到") + + # 创建标准的应用程序入口文件 + self._createAppFile(build_dir, project_name) + + # 创建标准的setup.py文件 + self._createStandardSetupFile(build_dir, project_name) + + #创建requirements.txt文件 + self._createRequirementsFile(build_dir) + + def _copyScriptsToBuild(self, build_dir, project_path): + """复制脚本文件到构建目录的scripts文件夹""" + try: + # 创建目标scripts目录 + scripts_dest = os.path.join(build_dir, "scripts") + + # 正确的源scripts目录路径 + scripts_src = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts") + + # 如果上面的路径不存在,尝试项目路径下的scripts目录 + if not os.path.exists(scripts_src): + scripts_src = os.path.join(project_path, "scripts") + + if os.path.exists(scripts_src): + # 直接复制整个scripts目录 + if os.path.exists(scripts_dest): + shutil.rmtree(scripts_dest) + + shutil.copytree( + scripts_src, + scripts_dest, + ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log') + ) + print("✓ Scripts目录已复制到build/scripts") + else: + # 创建空的scripts目录 + if not os.path.exists(scripts_dest): + os.makedirs(scripts_dest) + print("⚠️ 项目中没有scripts目录") + + except Exception as e: + print(f"⚠️ 复制脚本文件时出错: {str(e)}") + + def _copyScriptSystemToBuild(self,build_dir): + core_files = [ + "script_system.py", + "InfoPanelManager.py", + "CustomMouseController.py" + ] + + source_core_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),"core") + + core_dest = os.path.join(build_dir,"core") + + if not os.path.exists(core_dest): + os.makedirs(core_dest) + + for file_name in core_files: + source_file = os.path.join(source_core_dir,file_name) + + if os.path.exists(source_file): + shutil.copy2(source_file,os.path.join(core_dest,file_name)) + + def _saveGUIElementsToJSON(self, build_dir, project_path): + """保存GUI元素到JSON文件,内容与_collectGUIElementInfo保持一致""" + try: + # 创建目标gui目录 + gui_dest = os.path.join(build_dir, "gui") + if not os.path.exists(gui_dest): + os.makedirs(gui_dest) + + # 收集所有GUI元素信息 + gui_data = [] + + # 获取当前场景中的GUI元素 + if hasattr(self.world, 'gui_elements'): + for gui_node in self.world.gui_elements: + if gui_node and not gui_node.isEmpty(): + # 使用_collectGUIElementInfo方法收集信息 + gui_info = self.world.scene_manager._collectGUIElementInfo(gui_node) + if gui_info: + self._updateResourcePaths(gui_info,project_path,build_dir) + gui_data.append(gui_info) + print(f"收集GUI元素信息: {gui_info['name']}") + + # 保存GUI信息到JSON文件 + gui_file_path = os.path.join(gui_dest, "gui_elements.json") + with open(gui_file_path, "w", encoding="utf-8") as f: + json.dump(gui_data, f, ensure_ascii=False, indent=4) + + print(f"✓ GUI元素数据已保存到 {gui_file_path},共 {len(gui_data)} 个元素") + return True + + except Exception as e: + print(f"⚠️ 保存GUI元素时出错: {str(e)}") + import traceback + traceback.print_exc() + return False + + def _updateResourcePaths(self,gui_info,project_path,build_dir): + def normalize_resource_path(old_path): + if not old_path: + return old_path + + if old_path.startswith(("http://","https://")): + return old_path + + if os.path.isabs(old_path): + try: + rel_path = os.path.relpath(old_path,project_path) + return os.path.join("resources",os.path.basename(rel_path)).replace("\\","/") + except Exception: + return os.path.join("resources",os.path.basename(old_path)).replace("\\","/") + else: + return os.path.join("resources",os.path.basename(old_path)).replace("\\","/") + if "image_path" in gui_info and gui_info["image_path"]: + gui_info["image_path"] = normalize_resource_path(gui_info["image_path"]) + if "video_path" in gui_info and gui_info["video_path"]: + gui_info["video_path"] = normalize_resource_path(gui_info["video_path"]) + if "bg_image_path" in gui_info and gui_info["bg_image_path"]: + gui_info["bg_image_path"] = normalize_resource_path(gui_info["bg_image_path"]) + + def _createRequirementsFile(self,build_dir): + requirements_content = """panda3d>=1.10.13""" + + requirements_path = os.path.join(build_dir,"requirements.txt") + with open(requirements_path,"w",encoding="utf-8") as f: + f.write(requirements_content) + + def copy_folder(self, source_folder, destination_folder): + """将一个文件夹从源路径复制到目标路径下的resources文件夹中 + + Args: + source_folder (str): 源文件夹路径 + destination_folder (str): 目标文件夹路径 + """ + try: + # 创建resources文件夹作为目标 + resources_dest = os.path.join(destination_folder, "resources") + + # 确保目标目录存在 + if not os.path.exists(destination_folder): + os.makedirs(destination_folder) + + # 如果目标resources文件夹已存在,先删除 + if os.path.exists(resources_dest): + shutil.rmtree(resources_dest) + + # 检查源文件夹是否存在 + if os.path.exists(source_folder): + # 复制整个文件夹到resources目录下 + shutil.copytree( + source_folder, + resources_dest, + ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log') + ) + print(f"✓ 文件夹已从 {source_folder} 复制到 {resources_dest}") + return True + else: + print(f"⚠️ 源文件夹不存在: {source_folder}") + # 即使源文件夹不存在,也创建空的resources目录 + if not os.path.exists(resources_dest): + os.makedirs(resources_dest) + return False + + except Exception as e: + print(f"⚠️ 复制文件夹时出错: {str(e)}") + return False + + def _copyResourcesToBuild(self, build_dir, project_path): + """复制GUI资源到构建目录的resources文件夹""" + try: + # 创建目标resources目录 + resources_dest = os.path.join(build_dir, "resources") + + # 源Resources目录 + resources_src = os.path.join(project_path, "Resources") + + if os.path.exists(resources_src): + # 直接复制整个Resources目录 + if os.path.exists(resources_dest): + shutil.rmtree(resources_dest) + + shutil.copytree( + resources_src, + resources_dest, + ignore=shutil.ignore_patterns('__pycache__', '*.pyc', '.git', '.vscode', '*.log') + ) + print("✓ Resources目录已复制到build/resources") + + # 统计复制的文件数量 + file_count = 0 + for root, dirs, files in os.walk(resources_dest): + file_count += len(files) + print(f"✓ 共复制了 {file_count} 个资源文件") + else: + # 创建空的resources目录 + if not os.path.exists(resources_dest): + os.makedirs(resources_dest) + print("⚠️ 项目中没有Resources目录") + + except Exception as e: + print(f"⚠️ 复制资源文件时出错: {str(e)}") + + def _collectResourceFiles(self, project_path): + """收集项目中GUI使用的资源文件""" + resource_files = set() + + try: + # 收集Resources目录中的所有文件(这是最主要的资源来源) + resources_dir = os.path.join(project_path, "Resources") + if os.path.exists(resources_dir): + for root, dirs, files in os.walk(resources_dir): + for file in files: + file_path = os.path.join(root, file) + # 收集所有文件,不仅仅是媒体文件 + resource_files.add(file_path) + + # 同时收集场景中引用的特定资源 + scene_file = os.path.join(project_path, "scenes", "scene.bam") + if os.path.exists(scene_file): + # 从场景文件中提取资源引用 + referenced_files = self._extractResourcesFromScene(scene_file, project_path) + for file_path in referenced_files: + if os.path.isabs(file_path): + if os.path.exists(file_path): + resource_files.add(file_path) + else: + # 相对路径 + full_path = os.path.join(project_path, file_path) + if os.path.exists(full_path): + resource_files.add(full_path) + + except Exception as e: + print(f"收集资源文件时出错: {str(e)}") + + return list(resource_files) + + def _extractResourcesFromScene(self, scene_file, project_path): + """从场景文件中提取资源引用""" + referenced_files = [] + + try: + # 这里应该实现从BAM文件中提取贴图、视频等资源引用的逻辑 + # 由于直接解析BAM文件比较复杂,我们采用间接方式 + + # 检查项目配置文件或其他元数据文件中可能包含的资源引用 + config_file = os.path.join(project_path, "project.json") + if os.path.exists(config_file): + try: + with open(config_file, "r", encoding="utf-8") as f: + project_config = json.load(f) + + # 如果配置中有资源列表信息,可以在这里处理 + # 这里暂时保持简单实现 + except Exception as e: + print(f"读取项目配置时出错: {str(e)}") + + except Exception as e: + print(f"从场景提取资源引用时出错: {str(e)}") + + return referenced_files + + def _isMediaFile(self, file_path): + """判断是否为媒体文件(图片或视频)""" + media_extensions = { + # 图片格式 + '.png', '.jpg', '.jpeg', '.bmp', '.gif', '.tga', '.tiff', + # 视频格式 + '.mp4', '.avi', '.mov', '.wmv', '.mkv', '.webm', '.flv' + } + + _, ext = os.path.splitext(file_path.lower()) + return ext in media_extensions + + def _createAppFile(self, build_dir, project_name): + """创建应用程序主文件 - 通过复制模板文件""" + # 获取模板文件路径(假设模板文件在项目根目录下的templates文件夹中) + template_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), + "templates", "main_template.py") + + # 目标文件路径 + app_path = os.path.join(build_dir, "main.py") + + # 检查模板文件是否存在 + if os.path.exists(template_path): + # 直接复制模板文件 + shutil.copy2(template_path, app_path) + print(f"✓ 应用程序主文件已从模板创建: {app_path}") + + def _createStandardSetupFile(self, build_dir, project_name): + """创建优化的标准setup.py文件""" + setup_code = f'''#!/usr/bin/env python3 + # -*- coding: utf-8 -*- + + """ + {project_name} 打包配置文件 + 使用 Panda3D 标准打包工具 + """ + + from setuptools import setup + + # 应用程序配置 + APP_NAME = "{project_name}" + APP_VERSION = "1.0.0" + MAIN_SCRIPT = "main.py" + + setup( + name=APP_NAME, + version=APP_VERSION, + + # Panda3D 打包选项 + options={{ + 'build_apps': {{ + # GUI应用程序 + 'gui_apps': {{ + APP_NAME: MAIN_SCRIPT, + }}, + + # 包含的文件模式 + 'include_patterns': [ + '*.bam', # 场景文件 + '*.egg', # 模型文件 + '*.jpg', '*.png', '*.tga', '*.dds', # 纹理文件 + '*.wav', '*.ogg', '*.mp3', # 音频文件 + '*.ttf', '*.otf', # 字体文件 + 'config/*.yaml', # 配置文件 + 'shaders/**/*.glsl', # 着色器文件 + ], + + # 排除的文件模式 + 'exclude_patterns': [ + '*.pyc', + '__pycache__/**', + '.git/**', + '.vscode/**', + '*.log', + '**/*.tmp', + '**/temp/**', + 'samples/**', # 示例文件 + '**/.DS_Store', + '**/Thumbs.db', + ], + + # Panda3D 插件 + 'plugins': [ + 'pandagl', # OpenGL渲染器 + 'pandaegl', # EGL支持 + 'p3assimp', # Assimp模型导入 + 'p3ffmpeg', # FFmpeg视频支持 + 'p3openal_audio', # OpenAL音频 + ], + + # 包含的Python模块 + 'include_modules': {{ + '*': [ + # 核心模块 + 'direct.showbase.ShowBase', + 'direct.task', + 'direct.actor', + 'direct.interval', + 'direct.gui', + 'direct.stdpy.file', + 'direct.stdpy.pickle', + 'panda3d.core', + 'panda3d.direct', + + # 渲染管线相关 + 'rpcore', + 'rpcore.util.movement_controller', + 'rpcore.native', + 'rpcore.render_pipeline', + 'rplibs', + 'rpplugins', + + # 标准库 + 'json', + 'os', + 'sys', + 'math', + 'random', + 'collections', + 'copy', + 'itertools', + 'importlib', + ], + }}, + + # 排除的Python模块(减小体积) + 'exclude_modules': {{ + '*': [ + # GUI框架 + 'tkinter', + 'matplotlib', + 'numpy', + 'scipy', + 'PIL', + 'wx', + 'PyQt5', + 'PySide2', + 'kivy', + + # 开发工具 + 'setuptools', + 'pip', + 'wheel', + 'twine', + 'pytest', + 'unittest', + + # 其他不必要的模块 + 'sqlite3', + 'xmlrpc', + 'ftplib', + 'telnetlib', + ], + }}, + + # 平台设置 + 'platforms': [ + 'win_amd64', # Windows 64位 + 'linux_x86_64', # Linux 64位 + ], + + # 优化设置 + 'strip_docstrings': True, # 移除文档字符串 + 'optimize': 1, # 字节码优化级别 + }}, + }}, + + # 标准setuptools选项 + author="Panda3D Engine Editor User", + author_email="user@example.com", + description=f"{{APP_NAME}} - 使用Panda3D创建的3D应用程序", + long_description="这是一个使用Panda3D引擎编辑器创建的3D应用程序。", + + # 依赖项 + install_requires=[ + 'panda3d>=1.10.13', + ], + + # Python版本要求 + python_requires='>=3.7', + + # 分类信息 + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: End Users/Desktop', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Topic :: Games/Entertainment', + 'Topic :: Multimedia :: Graphics :: 3D Rendering', + ], + + license='MIT', + ) + ''' + + setup_path = os.path.join(build_dir, "setup.py") + with open(setup_path, "w", encoding="utf-8") as f: + f.write(setup_code) + + def _executeStandardBuild(self, build_dir, parent_window): + """执行标准的Panda3D打包命令 - 增强版""" + try: + print(f"开始打包,工作目录: {build_dir}") + + # 显示打包进度提示 + QMessageBox.information(parent_window, "打包进行中", + "正在打包项目,请稍候...\n" + "此过程可能需要几分钟时间。\n" + "请勿关闭程序。") + + # 使用线程执行打包以避免界面冻结 + import threading + from PyQt5.QtCore import pyqtSignal, QObject + + class BuildSignals(QObject): + finished = pyqtSignal(bool, str) + progress = pyqtSignal(str) + + signals = BuildSignals() + + def build_process(): + try: + print("执行标准打包命令: python setup.py bdist_apps") + signals.progress.emit("正在初始化打包过程...") + + process = subprocess.Popen( + [sys.executable, "setup.py", "bdist_apps"], + cwd=build_dir, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + encoding='utf-8' + ) + + while True: + output = process.stdout.readline() + if output == '' and process.poll() is not None: + break + if output: + print(output.strip()) + if "copying" in output.lower(): + signals.progress.emit(f"正在处理文件: {output.strip()[-50:]}") + elif "building" in output.lower(): + signals.progress.emit(output.strip()) + + # 获取错误输出 + stderr = process.stderr.read() + if stderr: + print("错误输出:", stderr) + + except Exception as e: + print(f"执行打包命令时出错: {str(e)}") + signals.finished.emit(False, f"打包失败:{str(e)}") + + # 连接信号 + def on_finished(success, message): + if success: + QMessageBox.information(parent_window, "成功", message) + else: + QMessageBox.critical(parent_window, "错误", message) + + def on_progress(message): + print(f"[打包进度] {message}") + + signals.finished.connect(on_finished) + signals.progress.connect(on_progress) + + # 启动打包线程 + build_thread = threading.Thread(target=build_process) + build_thread.daemon = True + build_thread.start() + + return True # 线程已启动 + + except Exception as e: + print(f"启动打包过程时出错: {str(e)}") + QMessageBox.critical(parent_window, "错误", f"启动打包失败:{str(e)}") + return False + + def _tryBuildAppsThreaded(self, build_dir, signals): + """尝试使用 build_apps 命令(线程安全版本)""" + try: + print("执行备用打包命令: python setup.py build_apps") + signals.progress.emit("正在执行备用打包方法...") + + process = subprocess.Popen( + [sys.executable, "setup.py", "build_apps"], + cwd=build_dir, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + encoding='utf-8' + ) + + # 实时显示输出 + while True: + output = process.stdout.readline() + if output == '' and process.poll() is not None: + break + if output: + print(output.strip()) + signals.progress.emit(output.strip()) + + stderr = process.stderr.read() + if stderr: + print("错误输出:", stderr) + + if process.returncode == 0: + print("✓ build_apps 成功完成") + signals.finished.emit(True, "打包成功完成!\n可执行文件在 dist/ 目录中。") + else: + error_msg = f"打包失败,返回码:{process.returncode}" + if stderr: + error_msg += f"\n错误信息:{stderr}" + signals.finished.emit(False, error_msg) + + except Exception as e: + signals.finished.emit(False, f"执行 build_apps 失败:{str(e)}") + + def _tryBuildApps(self, build_dir, parent_window): + """尝试使用 build_apps 命令""" + try: + print("执行备用打包命令: python setup.py build_apps") + + process = subprocess.Popen( + [sys.executable, "setup.py", "build_apps"], + cwd=build_dir, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + encoding='utf-8' + ) + + # 实时显示输出 + while True: + output = process.stdout.readline() + if output == '' and process.poll() is not None: + break + if output: + print(output.strip()) + + stderr = process.stderr.read() + if stderr: + print("错误输出:", stderr) + + if process.returncode == 0: + print("✓ build_apps 成功完成") + return True + else: + error_msg = f"打包失败,返回码:{process.returncode}" + if stderr: + error_msg += f"\\n错误信息:{stderr}" + QMessageBox.critical(parent_window, "错误", error_msg) + return False + + except Exception as e: + QMessageBox.critical(parent_window, "错误", f"执行 build_apps 失败:{str(e)}") + return False + + # ==================== 工具方法 ==================== + + def updateWindowTitle(self, window, project_name=None): + """更新窗口标题""" + base_title = "引擎编辑器" + if project_name: + window.setWindowTitle(f"{base_title} - {project_name}") + else: + window.setWindowTitle(base_title) + + def _clearCurrentScene(self): + """清空当前场景""" + # 移除所有模型 + for model in self.world.models: + model.removeNode() + self.world.models.clear() + + # 清空GUI元素 + for gui_element in self.world.gui_elements: + gui_element.removeNode() + self.world.gui_elements.clear() + + # 更新场景树 + if hasattr(self.world, 'updateSceneTree'): + self.world.updateSceneTree() + + def getCurrentProjectPath(self): + """获取当前项目路径""" + return self.current_project_path + + def getCurrentProjectConfig(self): + """获取当前项目配置""" + return self.project_config + + def isProjectOpen(self): + """检查是否有项目打开""" + return self.current_project_path is not None + + def getProjectName(self): + """获取当前项目名称""" + if self.current_project_path: + return os.path.basename(self.current_project_path) + return None + + def getProjectInfo(self): + """获取项目信息""" + if not self.project_config: + return None + + return { + "name": self.project_config.get("name", "未知项目"), + "path": self.project_config.get("path", ""), + "created_at": self.project_config.get("created_at", ""), + "last_modified": self.project_config.get("last_modified", ""), + "version": self.project_config.get("version", "1.0.0"), + "engine_version": self.project_config.get("engine_version", "1.0.0") + } + +# ==================== 便利函数 ==================== + +def updateWindowTitle(window, project_name=None): + """更新窗口标题 - 独立便利函数""" + base_title = "引擎编辑器" + if project_name: + window.setWindowTitle(f"{base_title} - {project_name}") + else: + window.setWindowTitle(base_title) diff --git a/requirements/DEPLOYMENT_README.md b/requirements/DEPLOYMENT_README.md index c92cb2f9..1100873f 100644 --- a/requirements/DEPLOYMENT_README.md +++ b/requirements/DEPLOYMENT_README.md @@ -1,90 +1,90 @@ -# 项目部署指南 - -## 🚀 在新电脑上运行此项目 - -### 方法1:使用Conda(推荐) - -```bash -# 1. 安装Miniconda -wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -bash Miniconda3-latest-Linux-x86_64.sh - -# 2. 克隆项目 -git clone -cd eg - -# 3. 创建conda环境 -conda env create -f environment.yml - -# 4. 激活环境 -conda activate eg-project - -# 5. 运行项目 -python main.py -``` - -### 方法2:使用virtualenv + pip - -```bash -# 1. 克隆项目 -git clone -cd eg - -# 2. 创建虚拟环境 -python -m venv venv -source venv/bin/activate # Linux/Mac -# 或 venv\Scripts\activate # Windows - -# 3. 安装依赖 -pip install -r clean-requirements.txt - -# 4. 运行项目 -python main.py -``` - -## 📁 需要复制的文件 - -**必须复制:** -- `main.py` - 主程序 -- `gui_preview_window.py` - GUI窗口 -- `demo/` - 演示文件夹 -- `environment.yml` - Conda环境配置 -- `clean-requirements.txt` - 核心依赖 - -**不要复制:** -- `.conda/` - 虚拟环境文件夹 -- `__pycache__/` - Python缓存 -- `requirements.txt` - 包含系统包的混乱依赖 - -## 🔧 Cursor IDE 设置 - -1. **打开项目**:在Cursor中打开项目文件夹 -2. **选择解释器**: - - `Ctrl+Shift+P` → "Python: Select Interpreter" - - 选择虚拟环境中的Python路径 -3. **验证环境**:检查状态栏显示正确的Python版本 - -## 📦 项目依赖说明 - -- **Panda3D**: 3D图形引擎 -- **PyQt5/PySide6**: GUI框架 -- **Pillow**: 图像处理 -- **python-dotenv**: 环境变量管理 -- **pyassimp**: 3D模型加载 - -## 🌍 跨平台注意事项 - -- Linux/Mac: 使用 `source venv/bin/activate` -- Windows: 使用 `venv\Scripts\activate` -- 某些依赖可能需要系统级安装(如OpenGL库) - -## 🐛 常见问题 - -**Q: 无法安装Panda3D?** -A: 确保系统有OpenGL支持,或使用conda安装 - -**Q: PyQt5无法运行?** -A: 可能需要安装系统级GUI依赖 - -**Q: 导入错误?** +# 项目部署指南 + +## 🚀 在新电脑上运行此项目 + +### 方法1:使用Conda(推荐) + +```bash +# 1. 安装Miniconda +wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh +bash Miniconda3-latest-Linux-x86_64.sh + +# 2. 克隆项目 +git clone +cd eg + +# 3. 创建conda环境 +conda env create -f environment.yml + +# 4. 激活环境 +conda activate eg-project + +# 5. 运行项目 +python main.py +``` + +### 方法2:使用virtualenv + pip + +```bash +# 1. 克隆项目 +git clone +cd eg + +# 2. 创建虚拟环境 +python -m venv venv +source venv/bin/activate # Linux/Mac +# 或 venv\Scripts\activate # Windows + +# 3. 安装依赖 +pip install -r clean-requirements.txt + +# 4. 运行项目 +python main.py +``` + +## 📁 需要复制的文件 + +**必须复制:** +- `main.py` - 主程序 +- `gui_preview_window.py` - GUI窗口 +- `demo/` - 演示文件夹 +- `environment.yml` - Conda环境配置 +- `clean-requirements.txt` - 核心依赖 + +**不要复制:** +- `.conda/` - 虚拟环境文件夹 +- `__pycache__/` - Python缓存 +- `requirements.txt` - 包含系统包的混乱依赖 + +## 🔧 Cursor IDE 设置 + +1. **打开项目**:在Cursor中打开项目文件夹 +2. **选择解释器**: + - `Ctrl+Shift+P` → "Python: Select Interpreter" + - 选择虚拟环境中的Python路径 +3. **验证环境**:检查状态栏显示正确的Python版本 + +## 📦 项目依赖说明 + +- **Panda3D**: 3D图形引擎 +- **PyQt5/PySide6**: GUI框架 +- **Pillow**: 图像处理 +- **python-dotenv**: 环境变量管理 +- **pyassimp**: 3D模型加载 + +## 🌍 跨平台注意事项 + +- Linux/Mac: 使用 `source venv/bin/activate` +- Windows: 使用 `venv\Scripts\activate` +- 某些依赖可能需要系统级安装(如OpenGL库) + +## 🐛 常见问题 + +**Q: 无法安装Panda3D?** +A: 确保系统有OpenGL支持,或使用conda安装 + +**Q: PyQt5无法运行?** +A: 可能需要安装系统级GUI依赖 + +**Q: 导入错误?** A: 检查Python版本(需要3.10+) \ No newline at end of file diff --git a/requirements/clean-requirements.txt b/requirements/clean-requirements.txt index 4f824c02..55a12ec3 100644 --- a/requirements/clean-requirements.txt +++ b/requirements/clean-requirements.txt @@ -1,6 +1,6 @@ -Panda3D>=1.10.15 -PyQt5>=5.15.9 -PySide6>=6.8.1 -Pillow>=9.0.1 -python-dotenv>=1.0.1 -pyassimp>=5.2.5 +Panda3D>=1.10.15 +PyQt5>=5.15.9 +PySide6>=6.8.1 +Pillow>=9.0.1 +python-dotenv>=1.0.1 +pyassimp>=5.2.5 diff --git a/requirements/conda-requirements.txt b/requirements/conda-requirements.txt index 74878bb0..ac769986 100644 --- a/requirements/conda-requirements.txt +++ b/requirements/conda-requirements.txt @@ -1,34 +1,34 @@ -# This file may be used to create an environment using: -# $ conda create --name --file -# platform: linux-64 -# created-by: conda 25.5.1 -@EXPLICIT -https://repo.anaconda.com/pkgs/main/linux-64/_libgcc_mutex-0.1-main.conda -https://repo.anaconda.com/pkgs/main/linux-64/ca-certificates-2025.2.25-h06a4308_0.conda -https://repo.anaconda.com/pkgs/main/linux-64/ld_impl_linux-64-2.40-h12ee557_0.conda -https://repo.anaconda.com/pkgs/main/linux-64/libstdcxx-ng-11.2.0-h1234567_1.conda -https://repo.anaconda.com/pkgs/main/noarch/tzdata-2025b-h04d1e81_0.conda -https://repo.anaconda.com/pkgs/main/linux-64/libgomp-11.2.0-h1234567_1.conda -https://repo.anaconda.com/pkgs/main/linux-64/_openmp_mutex-5.1-1_gnu.conda -https://repo.anaconda.com/pkgs/main/linux-64/libgcc-ng-11.2.0-h1234567_1.conda -https://repo.anaconda.com/pkgs/main/linux-64/bzip2-1.0.8-h5eee18b_6.conda -https://repo.anaconda.com/pkgs/main/linux-64/expat-2.7.1-h6a678d5_0.conda -https://repo.anaconda.com/pkgs/main/linux-64/libffi-3.4.4-h6a678d5_1.conda -https://repo.anaconda.com/pkgs/main/linux-64/libuuid-1.41.5-h5eee18b_0.conda -https://repo.anaconda.com/pkgs/main/linux-64/ncurses-6.4-h6a678d5_0.conda -https://repo.anaconda.com/pkgs/main/linux-64/openssl-3.0.16-h5eee18b_0.conda -https://repo.anaconda.com/pkgs/main/linux-64/pthread-stubs-0.3-h0ce48e5_1.conda -https://repo.anaconda.com/pkgs/main/linux-64/xorg-libxau-1.0.12-h9b100fa_0.conda -https://repo.anaconda.com/pkgs/main/linux-64/xorg-libxdmcp-1.1.5-h9b100fa_0.conda -https://repo.anaconda.com/pkgs/main/linux-64/xorg-xorgproto-2024.1-h5eee18b_1.conda -https://repo.anaconda.com/pkgs/main/linux-64/xz-5.6.4-h5eee18b_1.conda -https://repo.anaconda.com/pkgs/main/linux-64/zlib-1.2.13-h5eee18b_1.conda -https://repo.anaconda.com/pkgs/main/linux-64/libxcb-1.17.0-h9b100fa_0.conda -https://repo.anaconda.com/pkgs/main/linux-64/readline-8.2-h5eee18b_0.conda -https://repo.anaconda.com/pkgs/main/linux-64/sqlite-3.45.3-h5eee18b_0.conda -https://repo.anaconda.com/pkgs/main/linux-64/xorg-libx11-1.8.12-h9b100fa_1.conda -https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.14-h993c535_1.conda -https://repo.anaconda.com/pkgs/main/linux-64/python-3.10.18-h1a3bd86_0.conda -https://repo.anaconda.com/pkgs/main/linux-64/setuptools-78.1.1-py310h06a4308_0.conda -https://repo.anaconda.com/pkgs/main/linux-64/wheel-0.45.1-py310h06a4308_0.conda -https://repo.anaconda.com/pkgs/main/noarch/pip-25.1-pyhc872135_2.conda +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: linux-64 +# created-by: conda 25.5.1 +@EXPLICIT +https://repo.anaconda.com/pkgs/main/linux-64/_libgcc_mutex-0.1-main.conda +https://repo.anaconda.com/pkgs/main/linux-64/ca-certificates-2025.2.25-h06a4308_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/ld_impl_linux-64-2.40-h12ee557_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/libstdcxx-ng-11.2.0-h1234567_1.conda +https://repo.anaconda.com/pkgs/main/noarch/tzdata-2025b-h04d1e81_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/libgomp-11.2.0-h1234567_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/_openmp_mutex-5.1-1_gnu.conda +https://repo.anaconda.com/pkgs/main/linux-64/libgcc-ng-11.2.0-h1234567_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/bzip2-1.0.8-h5eee18b_6.conda +https://repo.anaconda.com/pkgs/main/linux-64/expat-2.7.1-h6a678d5_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/libffi-3.4.4-h6a678d5_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/libuuid-1.41.5-h5eee18b_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/ncurses-6.4-h6a678d5_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/openssl-3.0.16-h5eee18b_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/pthread-stubs-0.3-h0ce48e5_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/xorg-libxau-1.0.12-h9b100fa_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/xorg-libxdmcp-1.1.5-h9b100fa_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/xorg-xorgproto-2024.1-h5eee18b_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/xz-5.6.4-h5eee18b_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/zlib-1.2.13-h5eee18b_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/libxcb-1.17.0-h9b100fa_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/readline-8.2-h5eee18b_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/sqlite-3.45.3-h5eee18b_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/xorg-libx11-1.8.12-h9b100fa_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/tk-8.6.14-h993c535_1.conda +https://repo.anaconda.com/pkgs/main/linux-64/python-3.10.18-h1a3bd86_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/setuptools-78.1.1-py310h06a4308_0.conda +https://repo.anaconda.com/pkgs/main/linux-64/wheel-0.45.1-py310h06a4308_0.conda +https://repo.anaconda.com/pkgs/main/noarch/pip-25.1-pyhc872135_2.conda diff --git a/requirements/environment.yml b/requirements/environment.yml index 8f4ae727..e2800e1e 100644 --- a/requirements/environment.yml +++ b/requirements/environment.yml @@ -1,15 +1,15 @@ -name: eg-project -channels: - - conda-forge - - defaults -dependencies: - - python=3.10 - - pip - - pip: - - Panda3D>=1.10.15 - - PyQt5>=5.15.9 - - PySide6>=6.8.1 - - Pillow>=9.0.1 - - python-dotenv>=1.0.1 - - pyassimp>=5.2.5 - - six +name: eg-project +channels: + - conda-forge + - defaults +dependencies: + - python=3.10 + - pip + - pip: + - Panda3D>=1.10.15 + - PyQt5>=5.15.9 + - PySide6>=6.8.1 + - Pillow>=9.0.1 + - python-dotenv>=1.0.1 + - pyassimp>=5.2.5 + - six diff --git a/requirements/requirements.txt b/requirements/requirements.txt index a4b7d40c..2bb40970 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,101 +1,101 @@ -apturl==0.5.2 -bcrypt==3.2.0 -beautifulsoup4==4.10.0 -blinker==1.4 -Brlapi==0.8.3 -certifi==2020.6.20 -chardet==4.0.0 -click==8.0.3 -colorama==0.4.4 -command-not-found==0.3 -cryptography==3.4.8 -cupshelpers==1.0 -dbus-python==1.2.18 -defer==1.0.6 -DirectFolderBrowser==22.1 -DirectGuiDesigner==22.4.1 -DirectGuiExtension==22.9 -distro==1.7.0 -distro-info==1.1+ubuntu0.2 -duplicity==0.8.21 -fasteners==0.14.1 -future==0.18.2 -html5lib==1.1 -httplib2==0.20.2 -idna==3.3 -importlib-metadata==4.6.4 -jeepney==0.7.1 -keyring==23.5.0 -language-selector==0.1 -launchpadlib==1.10.16 -lazr.restfulclient==0.14.4 -lazr.uri==1.0.6 -lockfile==0.12.2 -louis==3.20.0 -lxml==4.8.0 -macaroonbakery==1.3.1 -Mako==1.1.3 -MarkupSafe==2.0.1 -monotonic==1.6 -more-itertools==8.10.0 -netifaces==0.11.0 -oauthlib==3.2.0 -olefile==0.46 -Panda3D==1.10.15 -panda3d-frame==22.10.post1 -Panda3DNodeEditor==22.5 -paramiko==2.9.3 -pexpect==4.8.0 -Pillow==9.0.1 -platformdirs==4.3.6 -protobuf==3.12.4 -ptyprocess==0.7.0 -pyassimp==5.2.5 -pycairo==1.20.1 -pycups==2.0.1 -Pygments==2.11.2 -PyGObject==3.42.1 -PyJWT==2.3.0 -pymacaroons==0.13.0 -PyNaCl==1.5.0 -pyparsing==2.4.7 -PyQt5==5.15.9 -pyqt5-plugins==5.15.9.2.3 -PyQt5-Qt5==5.15.2 -pyqt5-tools==5.15.9.3.3 -PyQt5_sip==12.16.1 -pyRFC3339==1.1 -PySide6==6.8.1 -PySide6_Addons==6.8.1 -PySide6_Essentials==6.8.1 -python-apt==2.4.0+ubuntu4 -python-dateutil==2.8.1 -python-debian==0.1.43+ubuntu1.1 -python-dotenv==1.0.1 -pytz==2022.1 -pyxdg==0.27 -PyYAML==5.4.1 -QPanda3D==0.2.10 -qt5-applications==5.15.2.2.3 -qt5-tools==5.15.2.1.3 -reportlab==3.6.8 -requests==2.25.1 -SceneEditor==22.5 -screen-resolution-extra==0.0.0 -SecretStorage==3.3.1 -shiboken6==6.8.1 -six==1.16.0 -soupsieve==2.3.1 -systemd-python==234 -ubuntu-drivers-common==0.0.0 -ubuntu-pro-client==8001 -ufw==0.36.1 -unattended-upgrades==0.1 -urllib3==1.26.5 -usb-creator==0.3.7 -wadllib==1.3.6 -webencodings==0.5.1 -xdg==5 -xkit==0.0.0 -zipp==1.0.0 -openvr==2.2.0 +apturl==0.5.2 +bcrypt==3.2.0 +beautifulsoup4==4.10.0 +blinker==1.4 +Brlapi==0.8.3 +certifi==2020.6.20 +chardet==4.0.0 +click==8.0.3 +colorama==0.4.4 +command-not-found==0.3 +cryptography==3.4.8 +cupshelpers==1.0 +dbus-python==1.2.18 +defer==1.0.6 +DirectFolderBrowser==22.1 +DirectGuiDesigner==22.4.1 +DirectGuiExtension==22.9 +distro==1.7.0 +distro-info==1.1+ubuntu0.2 +duplicity==0.8.21 +fasteners==0.14.1 +future==0.18.2 +html5lib==1.1 +httplib2==0.20.2 +idna==3.3 +importlib-metadata==4.6.4 +jeepney==0.7.1 +keyring==23.5.0 +language-selector==0.1 +launchpadlib==1.10.16 +lazr.restfulclient==0.14.4 +lazr.uri==1.0.6 +lockfile==0.12.2 +louis==3.20.0 +lxml==4.8.0 +macaroonbakery==1.3.1 +Mako==1.1.3 +MarkupSafe==2.0.1 +monotonic==1.6 +more-itertools==8.10.0 +netifaces==0.11.0 +oauthlib==3.2.0 +olefile==0.46 +Panda3D==1.10.15 +panda3d-frame==22.10.post1 +Panda3DNodeEditor==22.5 +paramiko==2.9.3 +pexpect==4.8.0 +Pillow==9.0.1 +platformdirs==4.3.6 +protobuf==3.12.4 +ptyprocess==0.7.0 +pyassimp==5.2.5 +pycairo==1.20.1 +pycups==2.0.1 +Pygments==2.11.2 +PyGObject==3.42.1 +PyJWT==2.3.0 +pymacaroons==0.13.0 +PyNaCl==1.5.0 +pyparsing==2.4.7 +PyQt5==5.15.9 +pyqt5-plugins==5.15.9.2.3 +PyQt5-Qt5==5.15.2 +pyqt5-tools==5.15.9.3.3 +PyQt5_sip==12.16.1 +pyRFC3339==1.1 +PySide6==6.8.1 +PySide6_Addons==6.8.1 +PySide6_Essentials==6.8.1 +python-apt==2.4.0+ubuntu4 +python-dateutil==2.8.1 +python-debian==0.1.43+ubuntu1.1 +python-dotenv==1.0.1 +pytz==2022.1 +pyxdg==0.27 +PyYAML==5.4.1 +QPanda3D==0.2.10 +qt5-applications==5.15.2.2.3 +qt5-tools==5.15.2.1.3 +reportlab==3.6.8 +requests==2.25.1 +SceneEditor==22.5 +screen-resolution-extra==0.0.0 +SecretStorage==3.3.1 +shiboken6==6.8.1 +six==1.16.0 +soupsieve==2.3.1 +systemd-python==234 +ubuntu-drivers-common==0.0.0 +ubuntu-pro-client==8001 +ufw==0.36.1 +unattended-upgrades==0.1 +urllib3==1.26.5 +usb-creator==0.3.7 +wadllib==1.3.6 +webencodings==0.5.1 +xdg==5 +xkit==0.0.0 +zipp==1.0.0 +openvr==2.2.0 diff --git a/run_complete_analysis.py b/run_complete_analysis.py index 2be62ed0..2c5b5a15 100644 --- a/run_complete_analysis.py +++ b/run_complete_analysis.py @@ -1,354 +1,354 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -完整的开源率分析脚本 -该脚本将执行完整的测试流程并生成准确报告 -""" - -import json -import os -import subprocess -import sys - -def run_command(command, ignore_failure=False): - """运行命令并返回结果""" - try: - print(f"执行命令: {command}") - result = subprocess.run(command, shell=True, capture_output=True, text=True) - if result.returncode != 0 and not ignore_failure: - print(f"命令执行失败: {result.stderr}") - return False - print("命令执行成功") - return True - except Exception as e: - print(f"执行命令时出错: {e}") - return False - -def load_cloc_data(path="cloc.json"): - """加载并解析cloc统计数据""" - if not os.path.exists(path): - print(f"❌ 未找到 {path} 文件") - return None - with open(path, "r", encoding="utf-8") as f: - return json.load(f) - -def load_scancode_data(path="summary.json"): - """加载并解析ScanCode统计数据""" - if not os.path.exists(path): - print(f"❌ 未找到 {path} 文件") - return None - with open(path, "r", encoding="utf-8") as f: - return json.load(f) - -def get_licensed_files_details(scancode_data): - """获取含许可证文件的详细信息""" - if not scancode_data: - return [] - - files = scancode_data.get("files", []) - licensed_files = [] - - # 定义需要排除的目录和文件模式 - exclude_patterns = [ - "/venv/", - "/.git/", - "/__pycache__/", - "/.idea/", - "/.vscode/", - "/build/", - "/dist/", - ".egg-info", - "/Resources/", - "/icons/", - "/tex/", - "cloc.json", - "detailed_cloc.txt", - "summary.json", - "完整开源率分析报告.txt", - "run_complete_analysis.py", - ] - - for file in files: - # 获取文件路径 - file_path = file.get("path", "") - - # 只处理类型为"file"的条目 - if file.get("type") != "file": - continue - - # 检查是否应该排除该文件 - should_exclude = False - for pattern in exclude_patterns: - if pattern in file_path: - should_exclude = True - break - - # 如果应该排除,则跳过该文件 - if should_exclude: - continue - - # 检查是否有许可证信息 - if file.get("detected_license_expression") or file.get("license_detections"): - licensed_files.append({ - "path": file_path, - "license": file.get("detected_license_expression", "Unknown"), - "detections": file.get("license_detections", []) - }) - - return licensed_files - -def get_file_code_lines(): - """从detailed_cloc.txt获取文件代码行数""" - file_code_lines = {} - - if not os.path.exists("detailed_cloc.txt"): - print("❌ 未找到 detailed_cloc.txt 文件") - return file_code_lines - - with open("detailed_cloc.txt", "r") as f: - cloc_lines = f.readlines() - - # 解析cloc输出,创建文件路径到代码行数的映射 - for line in cloc_lines[3:]: # 跳过标题行 - parts = line.strip().split() - if len(parts) >= 4: - try: - file_path = parts[0] - code_lines = int(parts[-1]) - # 标准化路径格式 - if file_path.startswith('./'): - file_path = file_path[2:] - file_code_lines[file_path] = code_lines - except ValueError: - continue - - return file_code_lines - -def calculate_accurate_open_source_lines(licensed_files_details, file_code_lines): - """计算准确的开源代码行数""" - total_licensed_code_lines = 0 - found_files = 0 - detailed_files = [] - - for file_info in licensed_files_details: - file_path = file_info["path"] - - # 将文件路径标准化 - normalized_path = file_path - if normalized_path.startswith('EG/'): - normalized_path = normalized_path[3:] # 去掉开头的EG/ - - if normalized_path in file_code_lines: - code_lines = file_code_lines[normalized_path] - total_licensed_code_lines += code_lines - found_files += 1 - detailed_files.append({ - "path": file_path, - "code_lines": code_lines, - "license": file_info["license"] - }) - else: - # 尝试其他可能的路径格式 - alt_path1 = './' + normalized_path - alt_path2 = 'EG/' + normalized_path - if alt_path1 in file_code_lines: - code_lines = file_code_lines[alt_path1] - total_licensed_code_lines += code_lines - found_files += 1 - detailed_files.append({ - "path": file_path, - "code_lines": code_lines, - "license": file_info["license"] - }) - elif alt_path2 in file_code_lines: - code_lines = file_code_lines[alt_path2] - total_licensed_code_lines += code_lines - found_files += 1 - detailed_files.append({ - "path": file_path, - "code_lines": code_lines, - "license": file_info["license"] - }) - - return total_licensed_code_lines, detailed_files - -def generate_detailed_report(): - """生成详细报告""" - # 加载数据 - cloc_data = load_cloc_data("cloc.json") - scancode_data = load_scancode_data("summary.json") - file_code_lines = get_file_code_lines() - - if not cloc_data or not scancode_data: - print("无法加载必要数据文件") - return False - - # 获取含许可证文件详情 - licensed_files_details = get_licensed_files_details(scancode_data) - - # 计算准确的开源代码行数 - accurate_open_source_lines, detailed_files = calculate_accurate_open_source_lines( - licensed_files_details, file_code_lines) - - # 获取统计数据 - total_code_lines = cloc_data.get("SUM", {}).get("code", 0) - total_files = 1075 # 根据脚本分析得出的实际文件数 - licensed_files = len(licensed_files_details) - - # 计算开源率 - open_source_rate = (accurate_open_source_lines / total_code_lines) * 100 if total_code_lines > 0 else 0 - - # 创建报告内容 - report_content = [] - report_content.append("项目开源率分析完整报告") - report_content.append("=" * 50) - report_content.append("") - - report_content.append("1. 报告概览") - report_content.append("-" * 20) - report_content.append(f"项目总文件数: {total_files}") - report_content.append(f"含许可证文件数: {licensed_files}") - report_content.append(f"项目总代码行数: {total_code_lines}") - report_content.append(f"准确开源代码行数: {accurate_open_source_lines}") - report_content.append(f"代码开源率: {open_source_rate:.2f}%") - report_content.append("") - - report_content.append("2. 各语言代码行数分布(包含文件路径)") - report_content.append("-" * 40) - - # 按语言分组显示文件 - lang_files = {} - with open("detailed_cloc.txt", "r") as f: - cloc_lines = f.readlines() - - for line in cloc_lines[3:]: # 跳过标题行 - parts = line.strip().split() - if len(parts) >= 4: - try: - file_path = parts[0] - # 从文件路径推断语言(简化处理) - if file_path.endswith('.py'): - lang = 'Python' - elif file_path.endswith('.js'): - lang = 'JavaScript' - elif file_path.endswith('.cpp') or file_path.endswith('.cc'): - lang = 'C++' - elif file_path.endswith('.h'): - lang = 'C/C++ Header' - elif file_path.endswith('.glsl'): - lang = 'GLSL' - elif file_path.endswith('.qml'): - lang = 'QML' - elif file_path.endswith('.xml'): - lang = 'XML' - elif file_path.endswith('.json'): - lang = 'JSON' - elif file_path.endswith('.md'): - lang = 'Markdown' - elif file_path.endswith('.html'): - lang = 'HTML' - elif file_path.endswith('.css'): - lang = 'CSS' - elif file_path.endswith('.sh'): - lang = 'Shell' - elif file_path.endswith('.yml') or file_path.endswith('.yaml'): - lang = 'YAML' - else: - lang = 'Other' - - if lang not in lang_files: - lang_files[lang] = [] - lang_files[lang].append((file_path, int(parts[-1]))) - except ValueError: - continue - - for lang, files in lang_files.items(): - report_content.append(f"\n{lang}语言文件:") - report_content.append(f" 文件总数: {len(files)}") - total_lines = sum([f[1] for f in files]) - report_content.append(f" 代码行数: {total_lines}") - report_content.append(" 文件列表:") - for file_path, code_lines in files[:10]: # 只显示前10个文件 - report_content.append(f" {file_path}: {code_lines} 行") - if len(files) > 10: - report_content.append(f" ... 还有 {len(files) - 10} 个文件") - - report_content.append("") - report_content.append("3. 含许可证的开源文件详情") - report_content.append("-" * 30) - - # 按许可证类型分组显示文件 - files_by_license = {} - for file_info in detailed_files: - license_type = file_info.get("license", "Unknown") - if license_type not in files_by_license: - files_by_license[license_type] = [] - files_by_license[license_type].append(file_info) - - for license_type, files in files_by_license.items(): - report_content.append(f"\n许可证类型: {license_type}") - report_content.append(f" 文件数量: {len(files)}") - total_lines = sum([f["code_lines"] for f in files]) - report_content.append(f" 代码行数: {total_lines}") - report_content.append(" 文件列表:") - for file_info in files: - report_content.append(f" {file_info['path']}: {file_info['code_lines']} 行") - - # 保存报告 - with open("完整开源率分析报告.txt", "w", encoding="utf-8") as f: - f.write("\n".join(report_content)) - - print("完整报告已生成:完整开源率分析报告.txt") - return True - -def main(): - """主函数""" - print("开始执行完整的开源率分析流程...") - - # 步骤1: 执行cloc统计代码行数 - print("\n步骤1: 执行cloc统计代码行数") - cloc_cmd = "cloc --json --fullpath --not-match-d='(venv|\\.git|__pycache__|\\.idea|\\.vscode|build|dist|.*\\.egg-info|Resources/animations|Resources/materials|Resources/models|Resources/textures|icons|tex)' --not-match-f='(cloc.json|detailed_cloc.txt|summary.json|完整开源率分析报告.txt|run_complete_analysis.py)' . > cloc.json" - if not run_command(cloc_cmd): - print("❌ cloc统计失败") - return False - - # 步骤2: 生成详细文件列表 - print("\n步骤2: 生成详细文件列表") - detailed_cloc_cmd = "cloc --by-file --fullpath --not-match-d='(venv|\\.git|__pycache__|\\.idea|\\.vscode|build|dist|.*\\.egg-info|Resources/animations|Resources/materials|Resources/models|Resources/textures|icons|tex)' --not-match-f='(cloc.json|detailed_cloc.txt|summary.json|完整开源率分析报告.txt|run_complete_analysis.py)' . | grep -v \"^\\s*$\" | grep -E \"(\\.py|\\.js|\\.cpp|\\.h|\\.glsl|\\.qml|\\.xml|\\.html|\\.css|\\.java|\\.cs|\\.php)\" > detailed_cloc.txt" - if not run_command(detailed_cloc_cmd): - print("❌ 生成详细文件列表失败") - return False - - # 步骤3: 执行ScanCode扫描许可证 - print("\n步骤3: 执行ScanCode扫描许可证") - scancode_cmd = "scancode --license --classify --summary --json-pp summary.json . --ignore \"venv\" --ignore \".git\" --ignore \"__pycache__\" --ignore \".idea\" --ignore \".vscode\" --ignore \"build\" --ignore \"dist\" --ignore \"*.egg-info\" --ignore \"Resources\" --ignore \"icons\" --ignore \"tex\" --ignore \"cloc.json\" --ignore \"detailed_cloc.txt\" --ignore \"完整开源率分析报告.txt\" --ignore \"run_complete_analysis.py\"" - # 忽略失败,因为ScanCode会尝试扫描自己生成的summary.json文件导致"失败" - run_command(scancode_cmd, ignore_failure=True) - - # 检查summary.json是否生成 - if not os.path.exists("summary.json"): - print("❌ ScanCode未生成summary.json文件") - return False - - # 步骤4: 生成详细报告 - print("\n步骤4: 生成详细报告") - if not generate_detailed_report(): - print("❌ 生成报告失败") - return False - - print("\n✅ 完整分析流程执行完成!") - print("生成的文件:") - print(" - cloc.json: 代码行数统计") - print(" - detailed_cloc.txt: 详细文件列表") - print(" - summary.json: 许可证扫描结果") - print(" - 完整开源率分析报告.txt: 最终报告") - return True - -if __name__ == "__main__": - success = main() - if success: - print("\n🎉 所有步骤执行成功!") - sys.exit(0) - else: - print("\n❌ 执行过程中出现错误!") +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +完整的开源率分析脚本 +该脚本将执行完整的测试流程并生成准确报告 +""" + +import json +import os +import subprocess +import sys + +def run_command(command, ignore_failure=False): + """运行命令并返回结果""" + try: + print(f"执行命令: {command}") + result = subprocess.run(command, shell=True, capture_output=True, text=True) + if result.returncode != 0 and not ignore_failure: + print(f"命令执行失败: {result.stderr}") + return False + print("命令执行成功") + return True + except Exception as e: + print(f"执行命令时出错: {e}") + return False + +def load_cloc_data(path="cloc.json"): + """加载并解析cloc统计数据""" + if not os.path.exists(path): + print(f"❌ 未找到 {path} 文件") + return None + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + +def load_scancode_data(path="summary.json"): + """加载并解析ScanCode统计数据""" + if not os.path.exists(path): + print(f"❌ 未找到 {path} 文件") + return None + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + +def get_licensed_files_details(scancode_data): + """获取含许可证文件的详细信息""" + if not scancode_data: + return [] + + files = scancode_data.get("files", []) + licensed_files = [] + + # 定义需要排除的目录和文件模式 + exclude_patterns = [ + "/venv/", + "/.git/", + "/__pycache__/", + "/.idea/", + "/.vscode/", + "/build/", + "/dist/", + ".egg-info", + "/Resources/", + "/icons/", + "/tex/", + "cloc.json", + "detailed_cloc.txt", + "summary.json", + "完整开源率分析报告.txt", + "run_complete_analysis.py", + ] + + for file in files: + # 获取文件路径 + file_path = file.get("path", "") + + # 只处理类型为"file"的条目 + if file.get("type") != "file": + continue + + # 检查是否应该排除该文件 + should_exclude = False + for pattern in exclude_patterns: + if pattern in file_path: + should_exclude = True + break + + # 如果应该排除,则跳过该文件 + if should_exclude: + continue + + # 检查是否有许可证信息 + if file.get("detected_license_expression") or file.get("license_detections"): + licensed_files.append({ + "path": file_path, + "license": file.get("detected_license_expression", "Unknown"), + "detections": file.get("license_detections", []) + }) + + return licensed_files + +def get_file_code_lines(): + """从detailed_cloc.txt获取文件代码行数""" + file_code_lines = {} + + if not os.path.exists("detailed_cloc.txt"): + print("❌ 未找到 detailed_cloc.txt 文件") + return file_code_lines + + with open("detailed_cloc.txt", "r") as f: + cloc_lines = f.readlines() + + # 解析cloc输出,创建文件路径到代码行数的映射 + for line in cloc_lines[3:]: # 跳过标题行 + parts = line.strip().split() + if len(parts) >= 4: + try: + file_path = parts[0] + code_lines = int(parts[-1]) + # 标准化路径格式 + if file_path.startswith('./'): + file_path = file_path[2:] + file_code_lines[file_path] = code_lines + except ValueError: + continue + + return file_code_lines + +def calculate_accurate_open_source_lines(licensed_files_details, file_code_lines): + """计算准确的开源代码行数""" + total_licensed_code_lines = 0 + found_files = 0 + detailed_files = [] + + for file_info in licensed_files_details: + file_path = file_info["path"] + + # 将文件路径标准化 + normalized_path = file_path + if normalized_path.startswith('EG/'): + normalized_path = normalized_path[3:] # 去掉开头的EG/ + + if normalized_path in file_code_lines: + code_lines = file_code_lines[normalized_path] + total_licensed_code_lines += code_lines + found_files += 1 + detailed_files.append({ + "path": file_path, + "code_lines": code_lines, + "license": file_info["license"] + }) + else: + # 尝试其他可能的路径格式 + alt_path1 = './' + normalized_path + alt_path2 = 'EG/' + normalized_path + if alt_path1 in file_code_lines: + code_lines = file_code_lines[alt_path1] + total_licensed_code_lines += code_lines + found_files += 1 + detailed_files.append({ + "path": file_path, + "code_lines": code_lines, + "license": file_info["license"] + }) + elif alt_path2 in file_code_lines: + code_lines = file_code_lines[alt_path2] + total_licensed_code_lines += code_lines + found_files += 1 + detailed_files.append({ + "path": file_path, + "code_lines": code_lines, + "license": file_info["license"] + }) + + return total_licensed_code_lines, detailed_files + +def generate_detailed_report(): + """生成详细报告""" + # 加载数据 + cloc_data = load_cloc_data("cloc.json") + scancode_data = load_scancode_data("summary.json") + file_code_lines = get_file_code_lines() + + if not cloc_data or not scancode_data: + print("无法加载必要数据文件") + return False + + # 获取含许可证文件详情 + licensed_files_details = get_licensed_files_details(scancode_data) + + # 计算准确的开源代码行数 + accurate_open_source_lines, detailed_files = calculate_accurate_open_source_lines( + licensed_files_details, file_code_lines) + + # 获取统计数据 + total_code_lines = cloc_data.get("SUM", {}).get("code", 0) + total_files = 1075 # 根据脚本分析得出的实际文件数 + licensed_files = len(licensed_files_details) + + # 计算开源率 + open_source_rate = (accurate_open_source_lines / total_code_lines) * 100 if total_code_lines > 0 else 0 + + # 创建报告内容 + report_content = [] + report_content.append("项目开源率分析完整报告") + report_content.append("=" * 50) + report_content.append("") + + report_content.append("1. 报告概览") + report_content.append("-" * 20) + report_content.append(f"项目总文件数: {total_files}") + report_content.append(f"含许可证文件数: {licensed_files}") + report_content.append(f"项目总代码行数: {total_code_lines}") + report_content.append(f"准确开源代码行数: {accurate_open_source_lines}") + report_content.append(f"代码开源率: {open_source_rate:.2f}%") + report_content.append("") + + report_content.append("2. 各语言代码行数分布(包含文件路径)") + report_content.append("-" * 40) + + # 按语言分组显示文件 + lang_files = {} + with open("detailed_cloc.txt", "r") as f: + cloc_lines = f.readlines() + + for line in cloc_lines[3:]: # 跳过标题行 + parts = line.strip().split() + if len(parts) >= 4: + try: + file_path = parts[0] + # 从文件路径推断语言(简化处理) + if file_path.endswith('.py'): + lang = 'Python' + elif file_path.endswith('.js'): + lang = 'JavaScript' + elif file_path.endswith('.cpp') or file_path.endswith('.cc'): + lang = 'C++' + elif file_path.endswith('.h'): + lang = 'C/C++ Header' + elif file_path.endswith('.glsl'): + lang = 'GLSL' + elif file_path.endswith('.qml'): + lang = 'QML' + elif file_path.endswith('.xml'): + lang = 'XML' + elif file_path.endswith('.json'): + lang = 'JSON' + elif file_path.endswith('.md'): + lang = 'Markdown' + elif file_path.endswith('.html'): + lang = 'HTML' + elif file_path.endswith('.css'): + lang = 'CSS' + elif file_path.endswith('.sh'): + lang = 'Shell' + elif file_path.endswith('.yml') or file_path.endswith('.yaml'): + lang = 'YAML' + else: + lang = 'Other' + + if lang not in lang_files: + lang_files[lang] = [] + lang_files[lang].append((file_path, int(parts[-1]))) + except ValueError: + continue + + for lang, files in lang_files.items(): + report_content.append(f"\n{lang}语言文件:") + report_content.append(f" 文件总数: {len(files)}") + total_lines = sum([f[1] for f in files]) + report_content.append(f" 代码行数: {total_lines}") + report_content.append(" 文件列表:") + for file_path, code_lines in files[:10]: # 只显示前10个文件 + report_content.append(f" {file_path}: {code_lines} 行") + if len(files) > 10: + report_content.append(f" ... 还有 {len(files) - 10} 个文件") + + report_content.append("") + report_content.append("3. 含许可证的开源文件详情") + report_content.append("-" * 30) + + # 按许可证类型分组显示文件 + files_by_license = {} + for file_info in detailed_files: + license_type = file_info.get("license", "Unknown") + if license_type not in files_by_license: + files_by_license[license_type] = [] + files_by_license[license_type].append(file_info) + + for license_type, files in files_by_license.items(): + report_content.append(f"\n许可证类型: {license_type}") + report_content.append(f" 文件数量: {len(files)}") + total_lines = sum([f["code_lines"] for f in files]) + report_content.append(f" 代码行数: {total_lines}") + report_content.append(" 文件列表:") + for file_info in files: + report_content.append(f" {file_info['path']}: {file_info['code_lines']} 行") + + # 保存报告 + with open("完整开源率分析报告.txt", "w", encoding="utf-8") as f: + f.write("\n".join(report_content)) + + print("完整报告已生成:完整开源率分析报告.txt") + return True + +def main(): + """主函数""" + print("开始执行完整的开源率分析流程...") + + # 步骤1: 执行cloc统计代码行数 + print("\n步骤1: 执行cloc统计代码行数") + cloc_cmd = "cloc --json --fullpath --not-match-d='(venv|\\.git|__pycache__|\\.idea|\\.vscode|build|dist|.*\\.egg-info|Resources/animations|Resources/materials|Resources/models|Resources/textures|icons|tex)' --not-match-f='(cloc.json|detailed_cloc.txt|summary.json|完整开源率分析报告.txt|run_complete_analysis.py)' . > cloc.json" + if not run_command(cloc_cmd): + print("❌ cloc统计失败") + return False + + # 步骤2: 生成详细文件列表 + print("\n步骤2: 生成详细文件列表") + detailed_cloc_cmd = "cloc --by-file --fullpath --not-match-d='(venv|\\.git|__pycache__|\\.idea|\\.vscode|build|dist|.*\\.egg-info|Resources/animations|Resources/materials|Resources/models|Resources/textures|icons|tex)' --not-match-f='(cloc.json|detailed_cloc.txt|summary.json|完整开源率分析报告.txt|run_complete_analysis.py)' . | grep -v \"^\\s*$\" | grep -E \"(\\.py|\\.js|\\.cpp|\\.h|\\.glsl|\\.qml|\\.xml|\\.html|\\.css|\\.java|\\.cs|\\.php)\" > detailed_cloc.txt" + if not run_command(detailed_cloc_cmd): + print("❌ 生成详细文件列表失败") + return False + + # 步骤3: 执行ScanCode扫描许可证 + print("\n步骤3: 执行ScanCode扫描许可证") + scancode_cmd = "scancode --license --classify --summary --json-pp summary.json . --ignore \"venv\" --ignore \".git\" --ignore \"__pycache__\" --ignore \".idea\" --ignore \".vscode\" --ignore \"build\" --ignore \"dist\" --ignore \"*.egg-info\" --ignore \"Resources\" --ignore \"icons\" --ignore \"tex\" --ignore \"cloc.json\" --ignore \"detailed_cloc.txt\" --ignore \"完整开源率分析报告.txt\" --ignore \"run_complete_analysis.py\"" + # 忽略失败,因为ScanCode会尝试扫描自己生成的summary.json文件导致"失败" + run_command(scancode_cmd, ignore_failure=True) + + # 检查summary.json是否生成 + if not os.path.exists("summary.json"): + print("❌ ScanCode未生成summary.json文件") + return False + + # 步骤4: 生成详细报告 + print("\n步骤4: 生成详细报告") + if not generate_detailed_report(): + print("❌ 生成报告失败") + return False + + print("\n✅ 完整分析流程执行完成!") + print("生成的文件:") + print(" - cloc.json: 代码行数统计") + print(" - detailed_cloc.txt: 详细文件列表") + print(" - summary.json: 许可证扫描结果") + print(" - 完整开源率分析报告.txt: 最终报告") + return True + +if __name__ == "__main__": + success = main() + if success: + print("\n🎉 所有步骤执行成功!") + sys.exit(0) + else: + print("\n❌ 执行过程中出现错误!") sys.exit(1) \ No newline at end of file diff --git a/scene/__init__.py b/scene/__init__.py index dbf89ca4..212595c8 100644 --- a/scene/__init__.py +++ b/scene/__init__.py @@ -1,14 +1,14 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -场景管理包 - 提供场景和模型管理功能 -""" - -from .scene_manager import SceneManager - -__all__ = ['SceneManager'] - -# 版本信息 -__version__ = '1.0.0' +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +场景管理包 - 提供场景和模型管理功能 +""" + +from .scene_manager import SceneManager + +__all__ = ['SceneManager'] + +# 版本信息 +__version__ = '1.0.0' __author__ = 'Scene Management System' \ No newline at end of file diff --git a/scene/util.py b/scene/util.py index 06ad4b23..a19193a7 100644 --- a/scene/util.py +++ b/scene/util.py @@ -1,296 +1,296 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -跨平台路径处理工具 -""" - -import os -import platform -from pathlib import Path -from panda3d.core import Filename - - -class CrossPlatformPathHandler: - """跨平台路径处理器""" - - def __init__(self): - self.system = platform.system() - self.is_windows = self.system == "Windows" - self.is_linux = self.system == "Linux" - self.is_mac = self.system == "Darwin" - - print(f"路径处理器初始化 - 系统: {self.system}") - - def normalize_model_path(self, filepath): - """标准化模型文件路径""" - try: - #print(f"\n=== 路径标准化处理 ===") - #print(f"原始路径: {filepath}") - #print(f"当前系统: {self.system}") - - # 步骤1: 检查原始路径是否存在 - if self._check_file_exists(filepath): - return self._panda3d_normalize(filepath) - - # 步骤2: 路径修复尝试 - fixed_path = self._attempt_path_fixes(filepath) - if fixed_path: - return self._panda3d_normalize(fixed_path) - - # 步骤3: 智能搜索 - found_path = self._smart_file_search(filepath) - if found_path: - return self._panda3d_normalize(found_path) - - # 步骤4: 用户友好的错误处理 - self._handle_path_not_found(filepath) - return filepath - - except Exception as e: - print(f"❌ 路径标准化失败: {e}") - return filepath - - def _check_file_exists(self, filepath): - """检查文件是否存在""" - exists = os.path.exists(filepath) - return exists - - def _panda3d_normalize(self, filepath): - """使用Panda3D标准化路径""" - try: - panda_filename = Filename.from_os_specific(filepath) - normalized_path = panda_filename.get_fullpath() - print(f"✓ Panda3D标准化: {normalized_path}") - return normalized_path - except Exception as e: - print(f"⚠️ Panda3D标准化失败: {e}") - return filepath - - def _attempt_path_fixes(self, filepath): - """尝试各种路径修复方法""" - print("🔧 尝试路径修复...") - - # 修复方法1: 处理Windows/Linux路径分隔符 - fixed_path = self._fix_path_separators(filepath) - if fixed_path and self._check_file_exists(fixed_path): - return fixed_path - - # 修复方法2: 处理绝对路径到相对路径的转换 - relative_path = self._convert_to_relative_path(filepath) - if relative_path and self._check_file_exists(relative_path): - return relative_path - - # 修复方法3: 处理路径中的特殊字符 - cleaned_path = self._clean_special_characters(filepath) - if cleaned_path and self._check_file_exists(cleaned_path): - return cleaned_path - - return None - - def _fix_path_separators(self, filepath): - """修复路径分隔符""" - try: - if self.is_windows: - # Windows: 将正斜杠转换为反斜杠 - fixed = filepath.replace('/', '\\') - else: - # Linux/Mac: 将反斜杠转换为正斜杠 - fixed = filepath.replace('\\', '/') - - if fixed != filepath: - print(f" 路径分隔符修复: {fixed}") - return fixed - return None - except Exception as e: - print(f" 路径分隔符修复失败: {e}") - return None - - def _convert_to_relative_path(self, filepath): - """转换绝对路径为相对路径""" - try: - # 如果是绝对路径,尝试转换为相对路径 - if os.path.isabs(filepath): - filename = os.path.basename(filepath) - print(f" 尝试相对路径: {filename}") - return filename - return None - except Exception as e: - print(f" 相对路径转换失败: {e}") - return None - - def _clean_special_characters(self, filepath): - """清理路径中的特殊字符""" - try: - # 处理路径中的特殊字符(如中文路径中的+号) - import urllib.parse - - # URL解码 - decoded = urllib.parse.unquote(filepath) - if decoded != filepath: - print(f" URL解码: {decoded}") - return decoded - - return None - except Exception as e: - print(f" 特殊字符清理失败: {e}") - return None - - def _smart_file_search(self, filepath): - """智能文件搜索""" - print("🔍 开始智能文件搜索...") - - filename = os.path.basename(filepath) - print(f" 搜索文件名: {filename}") - - # 搜索策略1: 在常见目录中搜索 - found_path = self._search_in_common_directories(filename) - if found_path: - return found_path - - # 搜索策略2: 递归搜索当前目录 - found_path = self._recursive_search(filename) - if found_path: - return found_path - - # 搜索策略3: 搜索用户常用目录 - found_path = self._search_user_directories(filename) - if found_path: - return found_path - - return None - - def _search_in_common_directories(self, filename): - """在常见目录中搜索""" - common_dirs = [ - "models", - "assets", - "assets/models", - "resources", - "data", - "scene", - ".", - "..", - "../models", - "../assets" - ] - - for directory in common_dirs: - if os.path.exists(directory): - potential_path = os.path.join(directory, filename) - if os.path.exists(potential_path): - print(f" ✓ 在常见目录中找到: {potential_path}") - return potential_path - - return None - - def _recursive_search(self, filename, max_depth=3): - """递归搜索文件""" - try: - current_depth = 0 - for root, dirs, files in os.walk("."): - # 限制搜索深度 - depth = root.replace(".", "").count(os.sep) - if depth > max_depth: - continue - - if filename in files: - found_path = os.path.join(root, filename) - print(f" ✓ 递归搜索找到: {found_path}") - return found_path - - return None - except Exception as e: - print(f" 递归搜索失败: {e}") - return None - - def _search_user_directories(self, filename): - """搜索用户常用目录""" - try: - user_dirs = [] - - if self.is_windows: - # Windows常用目录 - user_dirs.extend([ - os.path.expanduser("~/Desktop"), - os.path.expanduser("~/Documents"), - "C:/模型", - "D:/模型", - "E:/模型" - ]) - else: - # Linux/Mac常用目录 - user_dirs.extend([ - os.path.expanduser("~/Desktop"), - os.path.expanduser("~/Documents"), - os.path.expanduser("~/models"), - "/tmp", - "/var/tmp" - ]) - - for directory in user_dirs: - if os.path.exists(directory): - potential_path = os.path.join(directory, filename) - if os.path.exists(potential_path): - print(f" ✓ 在用户目录中找到: {potential_path}") - return potential_path - - return None - except Exception as e: - print(f" 用户目录搜索失败: {e}") - return None - - def _handle_path_not_found(self, filepath): - """处理路径未找到的情况""" - print(f"\n❌ 无法找到文件: {filepath}") - print("💡 建议解决方案:") - print("1. 检查文件路径是否正确") - print("2. 确保文件确实存在") - print("3. 尝试使用相对路径而不是绝对路径") - print("4. 检查文件名中是否包含特殊字符") - print("5. 将模型文件复制到项目的 models/ 目录下") - - # 显示当前工作目录 - print(f"当前工作目录: {os.getcwd()}") - - # 显示Panda3D模型搜索路径 - try: - from panda3d.core import getModelPath - model_path = getModelPath() - print(f"Panda3D模型搜索路径: {model_path}") - except: - pass - - def suggest_file_placement(self, filename): - """建议文件放置位置""" - suggestions = [] - - # 创建推荐的目录结构 - recommended_dirs = ["models", "assets/models", "resources"] - - for directory in recommended_dirs: - if not os.path.exists(directory): - try: - os.makedirs(directory, exist_ok=True) - suggestions.append(f"已创建目录: {directory}") - except: - pass - - suggested_path = os.path.join(directory, filename) - suggestions.append(f"建议放置位置: {suggested_path}") - - return suggestions - - -# 全局路径处理器实例 -path_handler = CrossPlatformPathHandler() - - -def normalize_model_path(filepath): - """便捷函数:标准化模型路径""" - return path_handler.normalize_model_path(filepath) - - -def suggest_file_placement(filename): - """便捷函数:建议文件放置位置""" +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +跨平台路径处理工具 +""" + +import os +import platform +from pathlib import Path +from panda3d.core import Filename + + +class CrossPlatformPathHandler: + """跨平台路径处理器""" + + def __init__(self): + self.system = platform.system() + self.is_windows = self.system == "Windows" + self.is_linux = self.system == "Linux" + self.is_mac = self.system == "Darwin" + + print(f"路径处理器初始化 - 系统: {self.system}") + + def normalize_model_path(self, filepath): + """标准化模型文件路径""" + try: + #print(f"\n=== 路径标准化处理 ===") + #print(f"原始路径: {filepath}") + #print(f"当前系统: {self.system}") + + # 步骤1: 检查原始路径是否存在 + if self._check_file_exists(filepath): + return self._panda3d_normalize(filepath) + + # 步骤2: 路径修复尝试 + fixed_path = self._attempt_path_fixes(filepath) + if fixed_path: + return self._panda3d_normalize(fixed_path) + + # 步骤3: 智能搜索 + found_path = self._smart_file_search(filepath) + if found_path: + return self._panda3d_normalize(found_path) + + # 步骤4: 用户友好的错误处理 + self._handle_path_not_found(filepath) + return filepath + + except Exception as e: + print(f"❌ 路径标准化失败: {e}") + return filepath + + def _check_file_exists(self, filepath): + """检查文件是否存在""" + exists = os.path.exists(filepath) + return exists + + def _panda3d_normalize(self, filepath): + """使用Panda3D标准化路径""" + try: + panda_filename = Filename.from_os_specific(filepath) + normalized_path = panda_filename.get_fullpath() + print(f"✓ Panda3D标准化: {normalized_path}") + return normalized_path + except Exception as e: + print(f"⚠️ Panda3D标准化失败: {e}") + return filepath + + def _attempt_path_fixes(self, filepath): + """尝试各种路径修复方法""" + print("🔧 尝试路径修复...") + + # 修复方法1: 处理Windows/Linux路径分隔符 + fixed_path = self._fix_path_separators(filepath) + if fixed_path and self._check_file_exists(fixed_path): + return fixed_path + + # 修复方法2: 处理绝对路径到相对路径的转换 + relative_path = self._convert_to_relative_path(filepath) + if relative_path and self._check_file_exists(relative_path): + return relative_path + + # 修复方法3: 处理路径中的特殊字符 + cleaned_path = self._clean_special_characters(filepath) + if cleaned_path and self._check_file_exists(cleaned_path): + return cleaned_path + + return None + + def _fix_path_separators(self, filepath): + """修复路径分隔符""" + try: + if self.is_windows: + # Windows: 将正斜杠转换为反斜杠 + fixed = filepath.replace('/', '\\') + else: + # Linux/Mac: 将反斜杠转换为正斜杠 + fixed = filepath.replace('\\', '/') + + if fixed != filepath: + print(f" 路径分隔符修复: {fixed}") + return fixed + return None + except Exception as e: + print(f" 路径分隔符修复失败: {e}") + return None + + def _convert_to_relative_path(self, filepath): + """转换绝对路径为相对路径""" + try: + # 如果是绝对路径,尝试转换为相对路径 + if os.path.isabs(filepath): + filename = os.path.basename(filepath) + print(f" 尝试相对路径: {filename}") + return filename + return None + except Exception as e: + print(f" 相对路径转换失败: {e}") + return None + + def _clean_special_characters(self, filepath): + """清理路径中的特殊字符""" + try: + # 处理路径中的特殊字符(如中文路径中的+号) + import urllib.parse + + # URL解码 + decoded = urllib.parse.unquote(filepath) + if decoded != filepath: + print(f" URL解码: {decoded}") + return decoded + + return None + except Exception as e: + print(f" 特殊字符清理失败: {e}") + return None + + def _smart_file_search(self, filepath): + """智能文件搜索""" + print("🔍 开始智能文件搜索...") + + filename = os.path.basename(filepath) + print(f" 搜索文件名: {filename}") + + # 搜索策略1: 在常见目录中搜索 + found_path = self._search_in_common_directories(filename) + if found_path: + return found_path + + # 搜索策略2: 递归搜索当前目录 + found_path = self._recursive_search(filename) + if found_path: + return found_path + + # 搜索策略3: 搜索用户常用目录 + found_path = self._search_user_directories(filename) + if found_path: + return found_path + + return None + + def _search_in_common_directories(self, filename): + """在常见目录中搜索""" + common_dirs = [ + "models", + "assets", + "assets/models", + "resources", + "data", + "scene", + ".", + "..", + "../models", + "../assets" + ] + + for directory in common_dirs: + if os.path.exists(directory): + potential_path = os.path.join(directory, filename) + if os.path.exists(potential_path): + print(f" ✓ 在常见目录中找到: {potential_path}") + return potential_path + + return None + + def _recursive_search(self, filename, max_depth=3): + """递归搜索文件""" + try: + current_depth = 0 + for root, dirs, files in os.walk("."): + # 限制搜索深度 + depth = root.replace(".", "").count(os.sep) + if depth > max_depth: + continue + + if filename in files: + found_path = os.path.join(root, filename) + print(f" ✓ 递归搜索找到: {found_path}") + return found_path + + return None + except Exception as e: + print(f" 递归搜索失败: {e}") + return None + + def _search_user_directories(self, filename): + """搜索用户常用目录""" + try: + user_dirs = [] + + if self.is_windows: + # Windows常用目录 + user_dirs.extend([ + os.path.expanduser("~/Desktop"), + os.path.expanduser("~/Documents"), + "C:/模型", + "D:/模型", + "E:/模型" + ]) + else: + # Linux/Mac常用目录 + user_dirs.extend([ + os.path.expanduser("~/Desktop"), + os.path.expanduser("~/Documents"), + os.path.expanduser("~/models"), + "/tmp", + "/var/tmp" + ]) + + for directory in user_dirs: + if os.path.exists(directory): + potential_path = os.path.join(directory, filename) + if os.path.exists(potential_path): + print(f" ✓ 在用户目录中找到: {potential_path}") + return potential_path + + return None + except Exception as e: + print(f" 用户目录搜索失败: {e}") + return None + + def _handle_path_not_found(self, filepath): + """处理路径未找到的情况""" + print(f"\n❌ 无法找到文件: {filepath}") + print("💡 建议解决方案:") + print("1. 检查文件路径是否正确") + print("2. 确保文件确实存在") + print("3. 尝试使用相对路径而不是绝对路径") + print("4. 检查文件名中是否包含特殊字符") + print("5. 将模型文件复制到项目的 models/ 目录下") + + # 显示当前工作目录 + print(f"当前工作目录: {os.getcwd()}") + + # 显示Panda3D模型搜索路径 + try: + from panda3d.core import getModelPath + model_path = getModelPath() + print(f"Panda3D模型搜索路径: {model_path}") + except: + pass + + def suggest_file_placement(self, filename): + """建议文件放置位置""" + suggestions = [] + + # 创建推荐的目录结构 + recommended_dirs = ["models", "assets/models", "resources"] + + for directory in recommended_dirs: + if not os.path.exists(directory): + try: + os.makedirs(directory, exist_ok=True) + suggestions.append(f"已创建目录: {directory}") + except: + pass + + suggested_path = os.path.join(directory, filename) + suggestions.append(f"建议放置位置: {suggested_path}") + + return suggestions + + +# 全局路径处理器实例 +path_handler = CrossPlatformPathHandler() + + +def normalize_model_path(filepath): + """便捷函数:标准化模型路径""" + return path_handler.normalize_model_path(filepath) + + +def suggest_file_placement(filename): + """便捷函数:建议文件放置位置""" return path_handler.suggest_file_placement(filename) \ No newline at end of file diff --git a/scripts/BouncerScript.py b/scripts/BouncerScript.py index 60fb4170..146723d7 100644 --- a/scripts/BouncerScript.py +++ b/scripts/BouncerScript.py @@ -1,102 +1,102 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -跳跃脚本 - 让对象产生上下跳跃效果 -""" - -from core.script_system import ScriptBase -import math - -class BouncerScript(ScriptBase): - """跳跃脚本类""" - - def __init__(self): - super().__init__() - - # 跳跃参数 - self.jump_height = 2.0 # 跳跃高度 - self.jump_speed = 3.0 # 跳跃速度 (跳跃/秒) - self.bounce_type = "sine" # 跳跃类型: "sine", "abs_sine", "square" - - # 内部变量 - self.time_accumulator = 0.0 # 时间累积器 - self.original_y = None # 原始Y位置 - self.is_bouncing = True # 是否正在跳跃 - self.bounce_direction = 1 # 跳跃方向 - - def start(self): - """脚本开始时调用""" - self.log("跳跃脚本启动!") - self.log(f"跳跃参数: 高度={self.jump_height}, 速度={self.jump_speed}, 类型={self.bounce_type}") - - # 记录原始Y位置 - self.original_y = self.gameObject.getZ() # Z轴是高度 - self.log(f"原始高度: {self.original_y}") - - def update(self, dt): - """每帧更新""" - if not self.is_bouncing: - return - - # 累积时间 - self.time_accumulator += dt * self.bounce_direction - - # 根据类型计算跳跃高度 - if self.bounce_type == "sine": - # 标准正弦波跳跃 - height_offset = math.sin(self.time_accumulator * self.jump_speed * 2 * math.pi) * self.jump_height - elif self.bounce_type == "abs_sine": - # 绝对值正弦波(始终向上) - height_offset = abs(math.sin(self.time_accumulator * self.jump_speed * 2 * math.pi)) * self.jump_height - elif self.bounce_type == "square": - # 方波跳跃(突然跳起落下) - sine_val = math.sin(self.time_accumulator * self.jump_speed * 2 * math.pi) - height_offset = self.jump_height if sine_val > 0 else 0 - else: - height_offset = 0 - - # 应用跳跃 - current_pos = self.gameObject.getPos() - new_z = self.original_y + height_offset - self.gameObject.setPos(current_pos.getX(), current_pos.getY(), new_z) - - def set_bounce_parameters(self, height=None, speed=None, bounce_type=None): - """设置跳跃参数""" - if height is not None: - self.jump_height = height - if speed is not None: - self.jump_speed = speed - if bounce_type is not None and bounce_type in ["sine", "abs_sine", "square"]: - self.bounce_type = bounce_type - - self.log(f"跳跃参数更新: 高度={self.jump_height}, 速度={self.jump_speed}, 类型={self.bounce_type}") - - def toggle_bouncing(self): - """切换跳跃状态""" - self.is_bouncing = not self.is_bouncing - status = "恢复" if self.is_bouncing else "暂停" - self.log(f"跳跃{status}") - - def reverse_direction(self): - """反转跳跃方向""" - self.bounce_direction *= -1 - direction = "正向" if self.bounce_direction > 0 else "反向" - self.log(f"跳跃方向改为{direction}") - - def reset_position(self): - """重置到原始高度""" - if self.original_y is not None: - current_pos = self.gameObject.getPos() - self.gameObject.setPos(current_pos.getX(), current_pos.getY(), self.original_y) - self.time_accumulator = 0.0 - self.log("位置已重置到原始高度") - - def jump_once(self): - """执行一次跳跃""" - self.time_accumulator = 0.0 - self.log("执行单次跳跃") - - def on_destroy(self): - """脚本销毁时调用""" +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +跳跃脚本 - 让对象产生上下跳跃效果 +""" + +from core.script_system import ScriptBase +import math + +class BouncerScript(ScriptBase): + """跳跃脚本类""" + + def __init__(self): + super().__init__() + + # 跳跃参数 + self.jump_height = 2.0 # 跳跃高度 + self.jump_speed = 3.0 # 跳跃速度 (跳跃/秒) + self.bounce_type = "sine" # 跳跃类型: "sine", "abs_sine", "square" + + # 内部变量 + self.time_accumulator = 0.0 # 时间累积器 + self.original_y = None # 原始Y位置 + self.is_bouncing = True # 是否正在跳跃 + self.bounce_direction = 1 # 跳跃方向 + + def start(self): + """脚本开始时调用""" + self.log("跳跃脚本启动!") + self.log(f"跳跃参数: 高度={self.jump_height}, 速度={self.jump_speed}, 类型={self.bounce_type}") + + # 记录原始Y位置 + self.original_y = self.gameObject.getZ() # Z轴是高度 + self.log(f"原始高度: {self.original_y}") + + def update(self, dt): + """每帧更新""" + if not self.is_bouncing: + return + + # 累积时间 + self.time_accumulator += dt * self.bounce_direction + + # 根据类型计算跳跃高度 + if self.bounce_type == "sine": + # 标准正弦波跳跃 + height_offset = math.sin(self.time_accumulator * self.jump_speed * 2 * math.pi) * self.jump_height + elif self.bounce_type == "abs_sine": + # 绝对值正弦波(始终向上) + height_offset = abs(math.sin(self.time_accumulator * self.jump_speed * 2 * math.pi)) * self.jump_height + elif self.bounce_type == "square": + # 方波跳跃(突然跳起落下) + sine_val = math.sin(self.time_accumulator * self.jump_speed * 2 * math.pi) + height_offset = self.jump_height if sine_val > 0 else 0 + else: + height_offset = 0 + + # 应用跳跃 + current_pos = self.gameObject.getPos() + new_z = self.original_y + height_offset + self.gameObject.setPos(current_pos.getX(), current_pos.getY(), new_z) + + def set_bounce_parameters(self, height=None, speed=None, bounce_type=None): + """设置跳跃参数""" + if height is not None: + self.jump_height = height + if speed is not None: + self.jump_speed = speed + if bounce_type is not None and bounce_type in ["sine", "abs_sine", "square"]: + self.bounce_type = bounce_type + + self.log(f"跳跃参数更新: 高度={self.jump_height}, 速度={self.jump_speed}, 类型={self.bounce_type}") + + def toggle_bouncing(self): + """切换跳跃状态""" + self.is_bouncing = not self.is_bouncing + status = "恢复" if self.is_bouncing else "暂停" + self.log(f"跳跃{status}") + + def reverse_direction(self): + """反转跳跃方向""" + self.bounce_direction *= -1 + direction = "正向" if self.bounce_direction > 0 else "反向" + self.log(f"跳跃方向改为{direction}") + + def reset_position(self): + """重置到原始高度""" + if self.original_y is not None: + current_pos = self.gameObject.getPos() + self.gameObject.setPos(current_pos.getX(), current_pos.getY(), self.original_y) + self.time_accumulator = 0.0 + self.log("位置已重置到原始高度") + + def jump_once(self): + """执行一次跳跃""" + self.time_accumulator = 0.0 + self.log("执行单次跳跃") + + def on_destroy(self): + """脚本销毁时调用""" self.log("跳跃脚本停止") \ No newline at end of file diff --git a/scripts/ColorChangerScript.py b/scripts/ColorChangerScript.py index 45dd59cb..ac302f70 100644 --- a/scripts/ColorChangerScript.py +++ b/scripts/ColorChangerScript.py @@ -1,160 +1,160 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -颜色变化脚本 - 让对象颜色产生循环变化 -""" - -from core.script_system import ScriptBase -from panda3d.core import Vec4 -import math - -class ColorChangerScript(ScriptBase): - """颜色变化脚本类""" - - def __init__(self): - super().__init__() - - # 颜色参数 - self.color_speed = 1.0 # 颜色变化速度 (周期/秒) - self.color_mode = "rainbow" # 颜色模式: "rainbow", "pulse", "fade", "strobe" - self.base_color = Vec4(1, 1, 1, 1) # 基础颜色 - self.intensity = 1.0 # 颜色强度 - - # 内部变量 - self.time_accumulator = 0.0 # 时间累积器 - self.original_color = None # 原始颜色 - self.is_changing = True # 是否正在变化 - self.strobe_state = False # 闪烁状态 - - def start(self): - """脚本开始时调用""" - self.log("颜色变化脚本启动!") - self.log(f"颜色参数: 速度={self.color_speed}, 模式={self.color_mode}, 强度={self.intensity}") - - # 记录原始颜色 - self.original_color = self.gameObject.getColor() - self.log(f"原始颜色: {self.original_color}") - - def update(self, dt): - """每帧更新""" - if not self.is_changing: - return - - # 累积时间 - self.time_accumulator += dt - - # 根据模式计算新颜色 - if self.color_mode == "rainbow": - new_color = self._calculate_rainbow_color() - elif self.color_mode == "pulse": - new_color = self._calculate_pulse_color() - elif self.color_mode == "fade": - new_color = self._calculate_fade_color() - elif self.color_mode == "strobe": - new_color = self._calculate_strobe_color() - else: - new_color = self.base_color - - # 应用颜色 - self.gameObject.setColor(new_color) - - def _calculate_rainbow_color(self): - """计算彩虹颜色""" - # 使用HSV到RGB的转换创建彩虹效果 - hue = (self.time_accumulator * self.color_speed) % 1.0 - - # 简单的HSV到RGB转换 - i = int(hue * 6.0) - f = (hue * 6.0) - i - p = 0.0 - q = 1.0 - f - t = f - - if i % 6 == 0: - r, g, b = 1.0, t, p - elif i % 6 == 1: - r, g, b = q, 1.0, p - elif i % 6 == 2: - r, g, b = p, 1.0, t - elif i % 6 == 3: - r, g, b = p, q, 1.0 - elif i % 6 == 4: - r, g, b = t, p, 1.0 - else: - r, g, b = 1.0, p, q - - return Vec4(r * self.intensity, g * self.intensity, b * self.intensity, 1.0) - - def _calculate_pulse_color(self): - """计算脉冲颜色""" - pulse = (math.sin(self.time_accumulator * self.color_speed * 2 * math.pi) + 1.0) / 2.0 - multiplier = pulse * self.intensity - return Vec4( - self.base_color.getX() * multiplier, - self.base_color.getY() * multiplier, - self.base_color.getZ() * multiplier, - self.base_color.getW() - ) - - def _calculate_fade_color(self): - """计算淡入淡出颜色""" - fade = (math.sin(self.time_accumulator * self.color_speed * 2 * math.pi) + 1.0) / 2.0 - alpha = fade * self.intensity - return Vec4( - self.base_color.getX(), - self.base_color.getY(), - self.base_color.getZ(), - alpha - ) - - def _calculate_strobe_color(self): - """计算闪烁颜色""" - # 根据时间间隔切换状态 - interval = 1.0 / (self.color_speed * 2) # 闪烁间隔 - if int(self.time_accumulator / interval) % 2 == 0: - return Vec4( - self.base_color.getX() * self.intensity, - self.base_color.getY() * self.intensity, - self.base_color.getZ() * self.intensity, - self.base_color.getW() - ) - else: - return Vec4(0.1, 0.1, 0.1, self.base_color.getW()) # 暗色状态 - - def set_color_parameters(self, speed=None, mode=None, base_color=None, intensity=None): - """设置颜色参数""" - if speed is not None: - self.color_speed = speed - if mode is not None and mode in ["rainbow", "pulse", "fade", "strobe"]: - self.color_mode = mode - if base_color is not None: - self.base_color = base_color - if intensity is not None: - self.intensity = intensity - - self.log(f"颜色参数更新: 速度={self.color_speed}, 模式={self.color_mode}, 强度={self.intensity}") - - def toggle_color_change(self): - """切换颜色变化状态""" - self.is_changing = not self.is_changing - status = "恢复" if self.is_changing else "暂停" - self.log(f"颜色变化{status}") - - def reset_color(self): - """重置到原始颜色""" - if self.original_color: - self.gameObject.setColor(self.original_color) - self.time_accumulator = 0.0 - self.log("颜色已重置到原始值") - - def set_solid_color(self, r=1.0, g=1.0, b=1.0, a=1.0): - """设置固定颜色""" - color = Vec4(r, g, b, a) - self.gameObject.setColor(color) - self.base_color = color - self.log(f"设置固定颜色: {color}") - - def on_destroy(self): - """脚本销毁时调用""" +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +颜色变化脚本 - 让对象颜色产生循环变化 +""" + +from core.script_system import ScriptBase +from panda3d.core import Vec4 +import math + +class ColorChangerScript(ScriptBase): + """颜色变化脚本类""" + + def __init__(self): + super().__init__() + + # 颜色参数 + self.color_speed = 1.0 # 颜色变化速度 (周期/秒) + self.color_mode = "rainbow" # 颜色模式: "rainbow", "pulse", "fade", "strobe" + self.base_color = Vec4(1, 1, 1, 1) # 基础颜色 + self.intensity = 1.0 # 颜色强度 + + # 内部变量 + self.time_accumulator = 0.0 # 时间累积器 + self.original_color = None # 原始颜色 + self.is_changing = True # 是否正在变化 + self.strobe_state = False # 闪烁状态 + + def start(self): + """脚本开始时调用""" + self.log("颜色变化脚本启动!") + self.log(f"颜色参数: 速度={self.color_speed}, 模式={self.color_mode}, 强度={self.intensity}") + + # 记录原始颜色 + self.original_color = self.gameObject.getColor() + self.log(f"原始颜色: {self.original_color}") + + def update(self, dt): + """每帧更新""" + if not self.is_changing: + return + + # 累积时间 + self.time_accumulator += dt + + # 根据模式计算新颜色 + if self.color_mode == "rainbow": + new_color = self._calculate_rainbow_color() + elif self.color_mode == "pulse": + new_color = self._calculate_pulse_color() + elif self.color_mode == "fade": + new_color = self._calculate_fade_color() + elif self.color_mode == "strobe": + new_color = self._calculate_strobe_color() + else: + new_color = self.base_color + + # 应用颜色 + self.gameObject.setColor(new_color) + + def _calculate_rainbow_color(self): + """计算彩虹颜色""" + # 使用HSV到RGB的转换创建彩虹效果 + hue = (self.time_accumulator * self.color_speed) % 1.0 + + # 简单的HSV到RGB转换 + i = int(hue * 6.0) + f = (hue * 6.0) - i + p = 0.0 + q = 1.0 - f + t = f + + if i % 6 == 0: + r, g, b = 1.0, t, p + elif i % 6 == 1: + r, g, b = q, 1.0, p + elif i % 6 == 2: + r, g, b = p, 1.0, t + elif i % 6 == 3: + r, g, b = p, q, 1.0 + elif i % 6 == 4: + r, g, b = t, p, 1.0 + else: + r, g, b = 1.0, p, q + + return Vec4(r * self.intensity, g * self.intensity, b * self.intensity, 1.0) + + def _calculate_pulse_color(self): + """计算脉冲颜色""" + pulse = (math.sin(self.time_accumulator * self.color_speed * 2 * math.pi) + 1.0) / 2.0 + multiplier = pulse * self.intensity + return Vec4( + self.base_color.getX() * multiplier, + self.base_color.getY() * multiplier, + self.base_color.getZ() * multiplier, + self.base_color.getW() + ) + + def _calculate_fade_color(self): + """计算淡入淡出颜色""" + fade = (math.sin(self.time_accumulator * self.color_speed * 2 * math.pi) + 1.0) / 2.0 + alpha = fade * self.intensity + return Vec4( + self.base_color.getX(), + self.base_color.getY(), + self.base_color.getZ(), + alpha + ) + + def _calculate_strobe_color(self): + """计算闪烁颜色""" + # 根据时间间隔切换状态 + interval = 1.0 / (self.color_speed * 2) # 闪烁间隔 + if int(self.time_accumulator / interval) % 2 == 0: + return Vec4( + self.base_color.getX() * self.intensity, + self.base_color.getY() * self.intensity, + self.base_color.getZ() * self.intensity, + self.base_color.getW() + ) + else: + return Vec4(0.1, 0.1, 0.1, self.base_color.getW()) # 暗色状态 + + def set_color_parameters(self, speed=None, mode=None, base_color=None, intensity=None): + """设置颜色参数""" + if speed is not None: + self.color_speed = speed + if mode is not None and mode in ["rainbow", "pulse", "fade", "strobe"]: + self.color_mode = mode + if base_color is not None: + self.base_color = base_color + if intensity is not None: + self.intensity = intensity + + self.log(f"颜色参数更新: 速度={self.color_speed}, 模式={self.color_mode}, 强度={self.intensity}") + + def toggle_color_change(self): + """切换颜色变化状态""" + self.is_changing = not self.is_changing + status = "恢复" if self.is_changing else "暂停" + self.log(f"颜色变化{status}") + + def reset_color(self): + """重置到原始颜色""" + if self.original_color: + self.gameObject.setColor(self.original_color) + self.time_accumulator = 0.0 + self.log("颜色已重置到原始值") + + def set_solid_color(self, r=1.0, g=1.0, b=1.0, a=1.0): + """设置固定颜色""" + color = Vec4(r, g, b, a) + self.gameObject.setColor(color) + self.base_color = color + self.log(f"设置固定颜色: {color}") + + def on_destroy(self): + """脚本销毁时调用""" self.log("颜色变化脚本停止") \ No newline at end of file diff --git a/scripts/ComboAnimatorScript.py b/scripts/ComboAnimatorScript.py index a39c2a78..7e9991dd 100644 --- a/scripts/ComboAnimatorScript.py +++ b/scripts/ComboAnimatorScript.py @@ -1,39 +1,39 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -复合动画脚本 - 结合旋转和跳跃效果 -""" - -from core.script_system import ScriptBase -import math - -class ComboAnimatorScript(ScriptBase): - def __init__(self): - super().__init__() - self.time = 0.0 - self.original_pos = None - self.is_active = True - - def start(self): - self.log("复合动画脚本启动!") - self.original_pos = self.gameObject.getPos() - - def update(self, dt): - if not self.is_active: - return - - self.time += dt - - # 旋转效果 - current_hpr = self.gameObject.getHpr() - new_h = current_hpr.getX() + 45.0 * dt - self.gameObject.setHpr(new_h, current_hpr.getY(), current_hpr.getZ()) - - # 跳跃效果 - if self.original_pos: - bounce_offset = abs(math.sin(self.time * 3.0)) * 1.0 - self.gameObject.setZ(self.original_pos.getZ() + bounce_offset) - - def on_destroy(self): +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +复合动画脚本 - 结合旋转和跳跃效果 +""" + +from core.script_system import ScriptBase +import math + +class ComboAnimatorScript(ScriptBase): + def __init__(self): + super().__init__() + self.time = 0.0 + self.original_pos = None + self.is_active = True + + def start(self): + self.log("复合动画脚本启动!") + self.original_pos = self.gameObject.getPos() + + def update(self, dt): + if not self.is_active: + return + + self.time += dt + + # 旋转效果 + current_hpr = self.gameObject.getHpr() + new_h = current_hpr.getX() + 45.0 * dt + self.gameObject.setHpr(new_h, current_hpr.getY(), current_hpr.getZ()) + + # 跳跃效果 + if self.original_pos: + bounce_offset = abs(math.sin(self.time * 3.0)) * 1.0 + self.gameObject.setZ(self.original_pos.getZ() + bounce_offset) + + def on_destroy(self): self.log("复合动画脚本停止") \ No newline at end of file diff --git a/scripts/FollowerScript.py b/scripts/FollowerScript.py index 893406a2..5595b35c 100644 --- a/scripts/FollowerScript.py +++ b/scripts/FollowerScript.py @@ -1,69 +1,69 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -跟随脚本 - 让对象跟随指定的目标对象 -""" - -from core.script_system import ScriptBase -from panda3d.core import Vec3 - -class FollowerScript(ScriptBase): - """跟随脚本类""" - - def __init__(self): - super().__init__() - self.target = None # 跟随目标 - self.follow_speed = 5.0 # 跟随速度 - self.follow_distance = 2.0 # 跟随距离 - self.is_following = True # 是否正在跟随 - - def start(self): - """脚本开始时调用""" - self.log("跟随脚本启动!") - self.log(f"跟随参数: 速度={self.follow_speed}, 距离={self.follow_distance}") - - def update(self, dt): - """每帧更新""" - if not self.is_following or self.target is None: - return - - target_pos = self.target.getPos() - current_pos = self.gameObject.getPos() - - # 计算目标方向 - direction = target_pos - current_pos - distance = direction.length() - - # 如果距离大于跟随距离,则移动 - if distance > self.follow_distance: - if distance > 0: - direction.normalize() - - # 计算目标位置(保持跟随距离) - target_follow_pos = target_pos - direction * self.follow_distance - - # 平滑移动到目标位置 - move_direction = target_follow_pos - current_pos - move_distance = move_direction.length() - - if move_distance > 0: - move_direction.normalize() - move_amount = min(self.follow_speed * dt, move_distance) - new_pos = current_pos + move_direction * move_amount - self.gameObject.setPos(new_pos) - - # 朝向目标 - self.gameObject.lookAt(target_pos) - - def set_target(self, target): - """设置跟随目标""" - self.target = target - if target: - self.log(f"设置跟随目标: {target.getName()}") - else: - self.log("清除跟随目标") - - def on_destroy(self): - """脚本销毁时调用""" +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +跟随脚本 - 让对象跟随指定的目标对象 +""" + +from core.script_system import ScriptBase +from panda3d.core import Vec3 + +class FollowerScript(ScriptBase): + """跟随脚本类""" + + def __init__(self): + super().__init__() + self.target = None # 跟随目标 + self.follow_speed = 5.0 # 跟随速度 + self.follow_distance = 2.0 # 跟随距离 + self.is_following = True # 是否正在跟随 + + def start(self): + """脚本开始时调用""" + self.log("跟随脚本启动!") + self.log(f"跟随参数: 速度={self.follow_speed}, 距离={self.follow_distance}") + + def update(self, dt): + """每帧更新""" + if not self.is_following or self.target is None: + return + + target_pos = self.target.getPos() + current_pos = self.gameObject.getPos() + + # 计算目标方向 + direction = target_pos - current_pos + distance = direction.length() + + # 如果距离大于跟随距离,则移动 + if distance > self.follow_distance: + if distance > 0: + direction.normalize() + + # 计算目标位置(保持跟随距离) + target_follow_pos = target_pos - direction * self.follow_distance + + # 平滑移动到目标位置 + move_direction = target_follow_pos - current_pos + move_distance = move_direction.length() + + if move_distance > 0: + move_direction.normalize() + move_amount = min(self.follow_speed * dt, move_distance) + new_pos = current_pos + move_direction * move_amount + self.gameObject.setPos(new_pos) + + # 朝向目标 + self.gameObject.lookAt(target_pos) + + def set_target(self, target): + """设置跟随目标""" + self.target = target + if target: + self.log(f"设置跟随目标: {target.getName()}") + else: + self.log("清除跟随目标") + + def on_destroy(self): + """脚本销毁时调用""" self.log("跟随脚本停止") \ No newline at end of file diff --git a/scripts/MoverScript.py b/scripts/MoverScript.py index b5c24099..1988600e 100644 --- a/scripts/MoverScript.py +++ b/scripts/MoverScript.py @@ -1,91 +1,91 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -移动脚本 - 让对象在指定方向上来回移动 -""" - -from core.script_system import ScriptBase -import math - -class MoverScript(ScriptBase): - """移动脚本类""" - - def __init__(self): - super().__init__() - - # 移动参数 - self.move_distance = 5.0 # 移动距离 - self.move_speed = 2.0 # 移动速度 (单位/秒) - self.move_axis = "x" # 移动轴: "x", "y", "z" - - # 内部变量 - self.start_position = None # 起始位置 - self.current_direction = 1 # 当前移动方向: 1或-1 - self.current_distance = 0.0 # 当前移动距离 - self.is_moving = True # 是否正在移动 - - def start(self): - """脚本开始时调用""" - self.log("移动脚本启动!") - self.log(f"移动参数: 距离={self.move_distance}, 速度={self.move_speed}, 轴={self.move_axis}") - - # 记录起始位置 - self.start_position = self.gameObject.getPos() - self.log(f"起始位置: {self.start_position}") - - def update(self, dt): - """每帧更新""" - if not self.is_moving or self.start_position is None: - return - - # 计算移动增量 - move_delta = self.move_speed * dt * self.current_direction - self.current_distance += abs(move_delta) - - # 检查是否需要改变方向 - if self.current_distance >= self.move_distance: - self.current_direction *= -1 - self.current_distance = 0.0 - - # 应用移动 - current_pos = self.gameObject.getPos() - new_pos = [current_pos.getX(), current_pos.getY(), current_pos.getZ()] - - if self.move_axis == "x": - new_pos[0] += move_delta - elif self.move_axis == "y": - new_pos[1] += move_delta - elif self.move_axis == "z": - new_pos[2] += move_delta - - self.gameObject.setPos(new_pos[0], new_pos[1], new_pos[2]) - - def set_move_parameters(self, distance=None, speed=None, axis=None): - """设置移动参数""" - if distance is not None: - self.move_distance = distance - if speed is not None: - self.move_speed = speed - if axis is not None and axis in ["x", "y", "z"]: - self.move_axis = axis - - self.log(f"移动参数更新: 距离={self.move_distance}, 速度={self.move_speed}, 轴={self.move_axis}") - - def toggle_movement(self): - """切换移动状态""" - self.is_moving = not self.is_moving - status = "恢复" if self.is_moving else "暂停" - self.log(f"移动{status}") - - def reset_position(self): - """重置到起始位置""" - if self.start_position: - self.gameObject.setPos(self.start_position) - self.current_distance = 0.0 - self.current_direction = 1 - self.log("位置已重置到起始点") - - def on_destroy(self): - """脚本销毁时调用""" +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +移动脚本 - 让对象在指定方向上来回移动 +""" + +from core.script_system import ScriptBase +import math + +class MoverScript(ScriptBase): + """移动脚本类""" + + def __init__(self): + super().__init__() + + # 移动参数 + self.move_distance = 5.0 # 移动距离 + self.move_speed = 2.0 # 移动速度 (单位/秒) + self.move_axis = "x" # 移动轴: "x", "y", "z" + + # 内部变量 + self.start_position = None # 起始位置 + self.current_direction = 1 # 当前移动方向: 1或-1 + self.current_distance = 0.0 # 当前移动距离 + self.is_moving = True # 是否正在移动 + + def start(self): + """脚本开始时调用""" + self.log("移动脚本启动!") + self.log(f"移动参数: 距离={self.move_distance}, 速度={self.move_speed}, 轴={self.move_axis}") + + # 记录起始位置 + self.start_position = self.gameObject.getPos() + self.log(f"起始位置: {self.start_position}") + + def update(self, dt): + """每帧更新""" + if not self.is_moving or self.start_position is None: + return + + # 计算移动增量 + move_delta = self.move_speed * dt * self.current_direction + self.current_distance += abs(move_delta) + + # 检查是否需要改变方向 + if self.current_distance >= self.move_distance: + self.current_direction *= -1 + self.current_distance = 0.0 + + # 应用移动 + current_pos = self.gameObject.getPos() + new_pos = [current_pos.getX(), current_pos.getY(), current_pos.getZ()] + + if self.move_axis == "x": + new_pos[0] += move_delta + elif self.move_axis == "y": + new_pos[1] += move_delta + elif self.move_axis == "z": + new_pos[2] += move_delta + + self.gameObject.setPos(new_pos[0], new_pos[1], new_pos[2]) + + def set_move_parameters(self, distance=None, speed=None, axis=None): + """设置移动参数""" + if distance is not None: + self.move_distance = distance + if speed is not None: + self.move_speed = speed + if axis is not None and axis in ["x", "y", "z"]: + self.move_axis = axis + + self.log(f"移动参数更新: 距离={self.move_distance}, 速度={self.move_speed}, 轴={self.move_axis}") + + def toggle_movement(self): + """切换移动状态""" + self.is_moving = not self.is_moving + status = "恢复" if self.is_moving else "暂停" + self.log(f"移动{status}") + + def reset_position(self): + """重置到起始位置""" + if self.start_position: + self.gameObject.setPos(self.start_position) + self.current_distance = 0.0 + self.current_direction = 1 + self.log("位置已重置到起始点") + + def on_destroy(self): + """脚本销毁时调用""" self.log("移动脚本停止") \ No newline at end of file diff --git a/scripts/R_P.py b/scripts/R_P.py index ba6ae6b9..7287da95 100644 --- a/scripts/R_P.py +++ b/scripts/R_P.py @@ -1,133 +1,133 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -旋转脚本 - 让对象持续旋转 -""" - -from core.script_system import ScriptBase - - -class RotatorScript(ScriptBase): - """旋转脚本类""" - - def __init__(self): - super().__init__() - self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) - self.max_angle = 30.0 # 最大旋转角度(相对于初始角度) - self.direction = 1 - self.current_offset = 0.0 # 当前相对于初始角度的偏移 - self.initial_angle = None # 模型的初始角度 - self.is_rotating = True # 是否正在旋转 - - def start(self): - """脚本开始时调用""" - self.log("旋转脚本启动!") - self.log(f"旋转速度: {self.rotation_speed_y}度/秒") - self.log(f"最大旋转角度: ±{self.max_angle}度") - - # 记录模型的初始角度 - if self.gameObject: - initial_hpr = self.gameObject.getHpr() - self.initial_angle = initial_hpr.getZ() # 记录Z轴的初始角度 - self.log(f"模型初始角度: {self.initial_angle}度") - self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") - else: - self.log("⚠️ 无法获取游戏对象,使用默认初始角度0") - self.initial_angle = 0.0 - - def update(self, dt): - """每帧更新""" - if not self.is_rotating or self.initial_angle is None: - return - - # 计算角度变化量 - delta_angle = self.rotation_speed_y * dt * self.direction - self.current_offset += delta_angle - - # 如果超出角度范围,则反向并限制在边界 - if self.current_offset > self.max_angle: - self.current_offset = self.max_angle - self.direction *= -1 - elif self.current_offset < -self.max_angle: - self.current_offset = -self.max_angle - self.direction *= -1 - - # 计算最终角度(初始角度 + 偏移量) - final_angle = self.initial_angle + self.current_offset - - # 设置新的旋转(只改变Z轴,保持其他不变) - current_hpr = self.gameObject.getHpr() - self.gameObject.setHpr(current_hpr.getX(), final_angle, current_hpr.getZ()) - - # if not self.is_rotating: - # return - # - # # 获取当前旋转并应用增量 - # current_hpr = self.gameObject.getHpr() - # new_r = current_hpr.getZ() + self.rotation_speed_y * dt - # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) - - def on_destroy(self): - """脚本销毁时调用""" - self.log("旋转脚本停止") - - # ==================== 控制方法 ==================== - - def set_max_angle(self, new_max_angle): - """ - 设置新的最大旋转角度 - - Args: - new_max_angle: 新的最大角度值 - """ - self.max_angle = new_max_angle - self.log(f"最大旋转角度已设置为: ±{self.max_angle}度") - if self.initial_angle is not None: - self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") - - def set_rotation_speed(self, new_speed): - """ - 设置新的旋转速度 - - Args: - new_speed: 新的旋转速度(度/秒) - """ - self.rotation_speed_y = new_speed - self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒") - - def pause_rotation(self): - """暂停旋转""" - self.is_rotating = False - self.log("旋转已暂停") - - def resume_rotation(self): - """恢复旋转""" - self.is_rotating = True - self.log("旋转已恢复") - - def reset_to_initial_angle(self): - """重置到初始角度""" - if self.initial_angle is not None and self.gameObject: - current_hpr = self.gameObject.getHpr() - self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), self.initial_angle) - self.current_offset = 0.0 - self.direction = 1 - self.log(f"已重置到初始角度: {self.initial_angle}度") - - def get_current_info(self): - """获取当前旋转信息""" - if self.gameObject and self.initial_angle is not None: - current_hpr = self.gameObject.getHpr() - current_angle = current_hpr.getZ() - self.log("=== 当前旋转信息 ===") - self.log(f"初始角度: {self.initial_angle}度") - self.log(f"当前角度: {current_angle}度") - self.log(f"偏移量: {self.current_offset}度") - self.log(f"最大角度: ±{self.max_angle}度") - self.log(f"旋转速度: {self.rotation_speed_y}度/秒") - self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}") - self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}") - self.log("=== 信息结束 ===") - else: +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.max_angle = 30.0 # 最大旋转角度(相对于初始角度) + self.direction = 1 + self.current_offset = 0.0 # 当前相对于初始角度的偏移 + self.initial_angle = None # 模型的初始角度 + self.is_rotating = True # 是否正在旋转 + + def start(self): + """脚本开始时调用""" + self.log("旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"最大旋转角度: ±{self.max_angle}度") + + # 记录模型的初始角度 + if self.gameObject: + initial_hpr = self.gameObject.getHpr() + self.initial_angle = initial_hpr.getZ() # 记录Z轴的初始角度 + self.log(f"模型初始角度: {self.initial_angle}度") + self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + else: + self.log("⚠️ 无法获取游戏对象,使用默认初始角度0") + self.initial_angle = 0.0 + + def update(self, dt): + """每帧更新""" + if not self.is_rotating or self.initial_angle is None: + return + + # 计算角度变化量 + delta_angle = self.rotation_speed_y * dt * self.direction + self.current_offset += delta_angle + + # 如果超出角度范围,则反向并限制在边界 + if self.current_offset > self.max_angle: + self.current_offset = self.max_angle + self.direction *= -1 + elif self.current_offset < -self.max_angle: + self.current_offset = -self.max_angle + self.direction *= -1 + + # 计算最终角度(初始角度 + 偏移量) + final_angle = self.initial_angle + self.current_offset + + # 设置新的旋转(只改变Z轴,保持其他不变) + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), final_angle, current_hpr.getZ()) + + # if not self.is_rotating: + # return + # + # # 获取当前旋转并应用增量 + # current_hpr = self.gameObject.getHpr() + # new_r = current_hpr.getZ() + self.rotation_speed_y * dt + # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("旋转脚本停止") + + # ==================== 控制方法 ==================== + + def set_max_angle(self, new_max_angle): + """ + 设置新的最大旋转角度 + + Args: + new_max_angle: 新的最大角度值 + """ + self.max_angle = new_max_angle + self.log(f"最大旋转角度已设置为: ±{self.max_angle}度") + if self.initial_angle is not None: + self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + + def set_rotation_speed(self, new_speed): + """ + 设置新的旋转速度 + + Args: + new_speed: 新的旋转速度(度/秒) + """ + self.rotation_speed_y = new_speed + self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒") + + def pause_rotation(self): + """暂停旋转""" + self.is_rotating = False + self.log("旋转已暂停") + + def resume_rotation(self): + """恢复旋转""" + self.is_rotating = True + self.log("旋转已恢复") + + def reset_to_initial_angle(self): + """重置到初始角度""" + if self.initial_angle is not None and self.gameObject: + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), self.initial_angle) + self.current_offset = 0.0 + self.direction = 1 + self.log(f"已重置到初始角度: {self.initial_angle}度") + + def get_current_info(self): + """获取当前旋转信息""" + if self.gameObject and self.initial_angle is not None: + current_hpr = self.gameObject.getHpr() + current_angle = current_hpr.getZ() + self.log("=== 当前旋转信息 ===") + self.log(f"初始角度: {self.initial_angle}度") + self.log(f"当前角度: {current_angle}度") + self.log(f"偏移量: {self.current_offset}度") + self.log(f"最大角度: ±{self.max_angle}度") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}") + self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}") + self.log("=== 信息结束 ===") + else: self.log("无法获取旋转信息") \ No newline at end of file diff --git a/scripts/R_R.py b/scripts/R_R.py index c78e88cb..dad3b574 100644 --- a/scripts/R_R.py +++ b/scripts/R_R.py @@ -1,133 +1,133 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -旋转脚本 - 让对象持续旋转 -""" - -from core.script_system import ScriptBase - - -class RotatorScript(ScriptBase): - """旋转脚本类""" - - def __init__(self): - super().__init__() - self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) - self.max_angle = 30.0 # 最大旋转角度(相对于初始角度) - self.direction = 1 - self.current_offset = 0.0 # 当前相对于初始角度的偏移 - self.initial_angle = None # 模型的初始角度 - self.is_rotating = True # 是否正在旋转 - - def start(self): - """脚本开始时调用""" - self.log("旋转脚本启动!") - self.log(f"旋转速度: {self.rotation_speed_y}度/秒") - self.log(f"最大旋转角度: ±{self.max_angle}度") - - # 记录模型的初始角度 - if self.gameObject: - initial_hpr = self.gameObject.getHpr() - self.initial_angle = initial_hpr.getZ() # 记录Z轴的初始角度 - self.log(f"模型初始角度: {self.initial_angle}度") - self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") - else: - self.log("⚠️ 无法获取游戏对象,使用默认初始角度0") - self.initial_angle = 0.0 - - def update(self, dt): - """每帧更新""" - if not self.is_rotating or self.initial_angle is None: - return - - # 计算角度变化量 - delta_angle = self.rotation_speed_y * dt * self.direction - self.current_offset += delta_angle - - # 如果超出角度范围,则反向并限制在边界 - if self.current_offset > self.max_angle: - self.current_offset = self.max_angle - self.direction *= -1 - elif self.current_offset < -self.max_angle: - self.current_offset = -self.max_angle - self.direction *= -1 - - # 计算最终角度(初始角度 + 偏移量) - final_angle = self.initial_angle + self.current_offset - - # 设置新的旋转(只改变Z轴,保持其他不变) - current_hpr = self.gameObject.getHpr() - self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), final_angle) - - # if not self.is_rotating: - # return - # - # # 获取当前旋转并应用增量 - # current_hpr = self.gameObject.getHpr() - # new_r = current_hpr.getZ() + self.rotation_speed_y * dt - # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) - - def on_destroy(self): - """脚本销毁时调用""" - self.log("旋转脚本停止") - - # ==================== 控制方法 ==================== - - def set_max_angle(self, new_max_angle): - """ - 设置新的最大旋转角度 - - Args: - new_max_angle: 新的最大角度值 - """ - self.max_angle = new_max_angle - self.log(f"最大旋转角度已设置为: ±{self.max_angle}度") - if self.initial_angle is not None: - self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") - - def set_rotation_speed(self, new_speed): - """ - 设置新的旋转速度 - - Args: - new_speed: 新的旋转速度(度/秒) - """ - self.rotation_speed_y = new_speed - self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒") - - def pause_rotation(self): - """暂停旋转""" - self.is_rotating = False - self.log("旋转已暂停") - - def resume_rotation(self): - """恢复旋转""" - self.is_rotating = True - self.log("旋转已恢复") - - def reset_to_initial_angle(self): - """重置到初始角度""" - if self.initial_angle is not None and self.gameObject: - current_hpr = self.gameObject.getHpr() - self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), self.initial_angle) - self.current_offset = 0.0 - self.direction = 1 - self.log(f"已重置到初始角度: {self.initial_angle}度") - - def get_current_info(self): - """获取当前旋转信息""" - if self.gameObject and self.initial_angle is not None: - current_hpr = self.gameObject.getHpr() - current_angle = current_hpr.getZ() - self.log("=== 当前旋转信息 ===") - self.log(f"初始角度: {self.initial_angle}度") - self.log(f"当前角度: {current_angle}度") - self.log(f"偏移量: {self.current_offset}度") - self.log(f"最大角度: ±{self.max_angle}度") - self.log(f"旋转速度: {self.rotation_speed_y}度/秒") - self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}") - self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}") - self.log("=== 信息结束 ===") - else: +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.max_angle = 30.0 # 最大旋转角度(相对于初始角度) + self.direction = 1 + self.current_offset = 0.0 # 当前相对于初始角度的偏移 + self.initial_angle = None # 模型的初始角度 + self.is_rotating = True # 是否正在旋转 + + def start(self): + """脚本开始时调用""" + self.log("旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"最大旋转角度: ±{self.max_angle}度") + + # 记录模型的初始角度 + if self.gameObject: + initial_hpr = self.gameObject.getHpr() + self.initial_angle = initial_hpr.getZ() # 记录Z轴的初始角度 + self.log(f"模型初始角度: {self.initial_angle}度") + self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + else: + self.log("⚠️ 无法获取游戏对象,使用默认初始角度0") + self.initial_angle = 0.0 + + def update(self, dt): + """每帧更新""" + if not self.is_rotating or self.initial_angle is None: + return + + # 计算角度变化量 + delta_angle = self.rotation_speed_y * dt * self.direction + self.current_offset += delta_angle + + # 如果超出角度范围,则反向并限制在边界 + if self.current_offset > self.max_angle: + self.current_offset = self.max_angle + self.direction *= -1 + elif self.current_offset < -self.max_angle: + self.current_offset = -self.max_angle + self.direction *= -1 + + # 计算最终角度(初始角度 + 偏移量) + final_angle = self.initial_angle + self.current_offset + + # 设置新的旋转(只改变Z轴,保持其他不变) + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), final_angle) + + # if not self.is_rotating: + # return + # + # # 获取当前旋转并应用增量 + # current_hpr = self.gameObject.getHpr() + # new_r = current_hpr.getZ() + self.rotation_speed_y * dt + # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("旋转脚本停止") + + # ==================== 控制方法 ==================== + + def set_max_angle(self, new_max_angle): + """ + 设置新的最大旋转角度 + + Args: + new_max_angle: 新的最大角度值 + """ + self.max_angle = new_max_angle + self.log(f"最大旋转角度已设置为: ±{self.max_angle}度") + if self.initial_angle is not None: + self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + + def set_rotation_speed(self, new_speed): + """ + 设置新的旋转速度 + + Args: + new_speed: 新的旋转速度(度/秒) + """ + self.rotation_speed_y = new_speed + self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒") + + def pause_rotation(self): + """暂停旋转""" + self.is_rotating = False + self.log("旋转已暂停") + + def resume_rotation(self): + """恢复旋转""" + self.is_rotating = True + self.log("旋转已恢复") + + def reset_to_initial_angle(self): + """重置到初始角度""" + if self.initial_angle is not None and self.gameObject: + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), self.initial_angle) + self.current_offset = 0.0 + self.direction = 1 + self.log(f"已重置到初始角度: {self.initial_angle}度") + + def get_current_info(self): + """获取当前旋转信息""" + if self.gameObject and self.initial_angle is not None: + current_hpr = self.gameObject.getHpr() + current_angle = current_hpr.getZ() + self.log("=== 当前旋转信息 ===") + self.log(f"初始角度: {self.initial_angle}度") + self.log(f"当前角度: {current_angle}度") + self.log(f"偏移量: {self.current_offset}度") + self.log(f"最大角度: ±{self.max_angle}度") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}") + self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}") + self.log("=== 信息结束 ===") + else: self.log("无法获取旋转信息") \ No newline at end of file diff --git a/scripts/Rotate_H_Script.py b/scripts/Rotate_H_Script.py index ffe0e294..05797936 100644 --- a/scripts/Rotate_H_Script.py +++ b/scripts/Rotate_H_Script.py @@ -1,215 +1,215 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -旋转脚本 - 让对象持续旋转 -""" - -from core.script_system import ScriptBase - - -class RotatorScript(ScriptBase): - """旋转脚本类""" - - def __init__(self): - super().__init__() - self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) - self.max_angle = 30.0 # 最大旋转角度(相对于初始角度) - self.direction = 1 - self.current_offset = 0.0 # 当前相对于初始角度的偏移 - self.initial_angle = None # 模型的初始角度 - self.is_rotating = True # 是否正在旋转 - - # 机器人式停顿参数 - self.pause_duration = 0.5 # 停顿时间(秒) - self.current_pause_time = 0.0 # 当前停顿计时 - self.is_paused = False # 是否正在停顿 - self.robot_mode = True # 是否启用机器人模式 - - def start(self): - """脚本开始时调用""" - self.log("机器人旋转脚本启动!") - self.log(f"旋转速度: {self.rotation_speed_y}度/秒") - self.log(f"最大旋转角度: ±{self.max_angle}度") - self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}") - if self.robot_mode: - self.log(f"停顿时间: {self.pause_duration}秒") - - # 记录模型的初始角度 - if self.gameObject: - initial_hpr = self.gameObject.getHpr() - self.initial_angle = initial_hpr.getY() # 记录Y轴的初始角度(Pitch) - self.log(f"模型初始角度: {self.initial_angle}度") - self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") - else: - self.log("⚠️ 无法获取游戏对象,使用默认初始角度0") - self.initial_angle = 0.0 - - def update(self, dt): - """每帧更新 - 机器人式旋转""" - if not self.is_rotating or self.initial_angle is None: - return - - # 如果正在停顿中 - if self.is_paused: - self.current_pause_time += dt - if self.current_pause_time >= self.pause_duration: - # 停顿结束,继续旋转 - self.is_paused = False - self.current_pause_time = 0.0 - self.log(f"停顿结束,继续旋转,方向: {'正向' if self.direction > 0 else '反向'}") - return - - # 计算角度变化量 - delta_angle = self.rotation_speed_y * dt * self.direction - self.current_offset += delta_angle - - # 检查是否到达边界 - reached_boundary = False - if self.current_offset > self.max_angle: - self.current_offset = self.max_angle - self.direction *= -1 - reached_boundary = True - elif self.current_offset < -self.max_angle: - self.current_offset = -self.max_angle - self.direction *= -1 - reached_boundary = True - - # 如果到达边界且启用机器人模式,开始停顿 - if reached_boundary and self.robot_mode: - self.is_paused = True - self.current_pause_time = 0.0 - self.log(f"到达边界 ({self.current_offset}°),开始停顿 {self.pause_duration}秒") - - # 计算最终角度(初始角度 + 偏移量) - final_angle = self.initial_angle + self.current_offset - - # 设置新的旋转(只改变Y轴,保持其他不变) - current_hpr = self.gameObject.getHpr() - self.gameObject.setHpr(final_angle, current_hpr.getY(), current_hpr.getZ()) - - # if not self.is_rotating: - # return - # - # # 获取当前旋转并应用增量 - # current_hpr = self.gameObject.getHpr() - # new_r = current_hpr.getZ() + self.rotation_speed_y * dt - # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) - - def on_destroy(self): - """脚本销毁时调用""" - self.log("机器人旋转脚本停止") - - # ==================== 机器人模式控制方法 ==================== - - def set_robot_mode(self, enabled=True): - """ - 启用或禁用机器人模式 - - Args: - enabled: 是否启用机器人模式 - """ - self.robot_mode = enabled - self.log(f"机器人模式: {'开启' if enabled else '关闭'}") - if not enabled: - self.is_paused = False # 如果禁用机器人模式,立即结束停顿 - - def set_pause_duration(self, duration): - """ - 设置停顿时间 - - Args: - duration: 停顿时间(秒) - """ - self.pause_duration = max(0.1, duration) # 最小0.1秒 - self.log(f"停顿时间已设置为: {self.pause_duration}秒") - - def set_max_angle(self, new_max_angle): - """ - 设置新的最大旋转角度 - - Args: - new_max_angle: 新的最大角度值 - """ - self.max_angle = new_max_angle - self.log(f"最大旋转角度已设置为: ±{self.max_angle}度") - if self.initial_angle is not None: - self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") - - def set_rotation_speed(self, new_speed): - """ - 设置新的旋转速度 - - Args: - new_speed: 新的旋转速度(度/秒) - """ - self.rotation_speed_y = new_speed - self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒") - - def pause_rotation(self): - """暂停旋转""" - self.is_rotating = False - self.log("旋转已暂停") - - def resume_rotation(self): - """恢复旋转""" - self.is_rotating = True - self.is_paused = False # 同时结束停顿状态 - self.log("旋转已恢复") - - def reset_to_initial_angle(self): - """重置到初始角度""" - if self.initial_angle is not None and self.gameObject: - current_hpr = self.gameObject.getHpr() - self.gameObject.setHpr(current_hpr.getX(), self.initial_angle, current_hpr.getZ()) - self.current_offset = 0.0 - self.direction = 1 - self.is_paused = False - self.current_pause_time = 0.0 - self.log(f"已重置到初始角度: {self.initial_angle}度") - - def get_current_info(self): - """获取当前旋转信息""" - if self.gameObject and self.initial_angle is not None: - current_hpr = self.gameObject.getHpr() - current_angle = current_hpr.getY() - self.log("=== 机器人旋转信息 ===") - self.log(f"初始角度: {self.initial_angle}度") - self.log(f"当前角度: {current_angle}度") - self.log(f"偏移量: {self.current_offset}度") - self.log(f"最大角度: ±{self.max_angle}度") - self.log(f"旋转速度: {self.rotation_speed_y}度/秒") - self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}") - self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}") - self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}") - if self.robot_mode: - self.log(f"停顿时间: {self.pause_duration}秒") - self.log(f"当前状态: {'停顿中' if self.is_paused else '旋转中'}") - if self.is_paused: - remaining_time = self.pause_duration - self.current_pause_time - self.log(f"剩余停顿时间: {remaining_time:.1f}秒") - self.log("=== 信息结束 ===") - else: - self.log("无法获取旋转信息") - - # ==================== 预设配置方法 ==================== - - def set_slow_robot_mode(self): - """预设:慢速机器人模式""" - self.set_rotation_speed(15.0) - self.set_pause_duration(1.0) - self.set_robot_mode(True) - self.log("已设置为慢速机器人模式") - - def set_fast_robot_mode(self): - """预设:快速机器人模式""" - self.set_rotation_speed(45.0) - self.set_pause_duration(0.3) - self.set_robot_mode(True) - self.log("已设置为快速机器人模式") - - def set_smooth_mode(self): - """预设:平滑模式(非机器人)""" - self.set_robot_mode(False) - self.set_rotation_speed(30.0) +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.max_angle = 30.0 # 最大旋转角度(相对于初始角度) + self.direction = 1 + self.current_offset = 0.0 # 当前相对于初始角度的偏移 + self.initial_angle = None # 模型的初始角度 + self.is_rotating = True # 是否正在旋转 + + # 机器人式停顿参数 + self.pause_duration = 0.5 # 停顿时间(秒) + self.current_pause_time = 0.0 # 当前停顿计时 + self.is_paused = False # 是否正在停顿 + self.robot_mode = True # 是否启用机器人模式 + + def start(self): + """脚本开始时调用""" + self.log("机器人旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"最大旋转角度: ±{self.max_angle}度") + self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}") + if self.robot_mode: + self.log(f"停顿时间: {self.pause_duration}秒") + + # 记录模型的初始角度 + if self.gameObject: + initial_hpr = self.gameObject.getHpr() + self.initial_angle = initial_hpr.getY() # 记录Y轴的初始角度(Pitch) + self.log(f"模型初始角度: {self.initial_angle}度") + self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + else: + self.log("⚠️ 无法获取游戏对象,使用默认初始角度0") + self.initial_angle = 0.0 + + def update(self, dt): + """每帧更新 - 机器人式旋转""" + if not self.is_rotating or self.initial_angle is None: + return + + # 如果正在停顿中 + if self.is_paused: + self.current_pause_time += dt + if self.current_pause_time >= self.pause_duration: + # 停顿结束,继续旋转 + self.is_paused = False + self.current_pause_time = 0.0 + self.log(f"停顿结束,继续旋转,方向: {'正向' if self.direction > 0 else '反向'}") + return + + # 计算角度变化量 + delta_angle = self.rotation_speed_y * dt * self.direction + self.current_offset += delta_angle + + # 检查是否到达边界 + reached_boundary = False + if self.current_offset > self.max_angle: + self.current_offset = self.max_angle + self.direction *= -1 + reached_boundary = True + elif self.current_offset < -self.max_angle: + self.current_offset = -self.max_angle + self.direction *= -1 + reached_boundary = True + + # 如果到达边界且启用机器人模式,开始停顿 + if reached_boundary and self.robot_mode: + self.is_paused = True + self.current_pause_time = 0.0 + self.log(f"到达边界 ({self.current_offset}°),开始停顿 {self.pause_duration}秒") + + # 计算最终角度(初始角度 + 偏移量) + final_angle = self.initial_angle + self.current_offset + + # 设置新的旋转(只改变Y轴,保持其他不变) + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(final_angle, current_hpr.getY(), current_hpr.getZ()) + + # if not self.is_rotating: + # return + # + # # 获取当前旋转并应用增量 + # current_hpr = self.gameObject.getHpr() + # new_r = current_hpr.getZ() + self.rotation_speed_y * dt + # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("机器人旋转脚本停止") + + # ==================== 机器人模式控制方法 ==================== + + def set_robot_mode(self, enabled=True): + """ + 启用或禁用机器人模式 + + Args: + enabled: 是否启用机器人模式 + """ + self.robot_mode = enabled + self.log(f"机器人模式: {'开启' if enabled else '关闭'}") + if not enabled: + self.is_paused = False # 如果禁用机器人模式,立即结束停顿 + + def set_pause_duration(self, duration): + """ + 设置停顿时间 + + Args: + duration: 停顿时间(秒) + """ + self.pause_duration = max(0.1, duration) # 最小0.1秒 + self.log(f"停顿时间已设置为: {self.pause_duration}秒") + + def set_max_angle(self, new_max_angle): + """ + 设置新的最大旋转角度 + + Args: + new_max_angle: 新的最大角度值 + """ + self.max_angle = new_max_angle + self.log(f"最大旋转角度已设置为: ±{self.max_angle}度") + if self.initial_angle is not None: + self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + + def set_rotation_speed(self, new_speed): + """ + 设置新的旋转速度 + + Args: + new_speed: 新的旋转速度(度/秒) + """ + self.rotation_speed_y = new_speed + self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒") + + def pause_rotation(self): + """暂停旋转""" + self.is_rotating = False + self.log("旋转已暂停") + + def resume_rotation(self): + """恢复旋转""" + self.is_rotating = True + self.is_paused = False # 同时结束停顿状态 + self.log("旋转已恢复") + + def reset_to_initial_angle(self): + """重置到初始角度""" + if self.initial_angle is not None and self.gameObject: + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), self.initial_angle, current_hpr.getZ()) + self.current_offset = 0.0 + self.direction = 1 + self.is_paused = False + self.current_pause_time = 0.0 + self.log(f"已重置到初始角度: {self.initial_angle}度") + + def get_current_info(self): + """获取当前旋转信息""" + if self.gameObject and self.initial_angle is not None: + current_hpr = self.gameObject.getHpr() + current_angle = current_hpr.getY() + self.log("=== 机器人旋转信息 ===") + self.log(f"初始角度: {self.initial_angle}度") + self.log(f"当前角度: {current_angle}度") + self.log(f"偏移量: {self.current_offset}度") + self.log(f"最大角度: ±{self.max_angle}度") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}") + self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}") + self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}") + if self.robot_mode: + self.log(f"停顿时间: {self.pause_duration}秒") + self.log(f"当前状态: {'停顿中' if self.is_paused else '旋转中'}") + if self.is_paused: + remaining_time = self.pause_duration - self.current_pause_time + self.log(f"剩余停顿时间: {remaining_time:.1f}秒") + self.log("=== 信息结束 ===") + else: + self.log("无法获取旋转信息") + + # ==================== 预设配置方法 ==================== + + def set_slow_robot_mode(self): + """预设:慢速机器人模式""" + self.set_rotation_speed(15.0) + self.set_pause_duration(1.0) + self.set_robot_mode(True) + self.log("已设置为慢速机器人模式") + + def set_fast_robot_mode(self): + """预设:快速机器人模式""" + self.set_rotation_speed(45.0) + self.set_pause_duration(0.3) + self.set_robot_mode(True) + self.log("已设置为快速机器人模式") + + def set_smooth_mode(self): + """预设:平滑模式(非机器人)""" + self.set_robot_mode(False) + self.set_rotation_speed(30.0) self.log("已设置为平滑旋转模式") \ No newline at end of file diff --git a/scripts/Rotate_P_Script.py b/scripts/Rotate_P_Script.py index 98d02cdf..070d8621 100644 --- a/scripts/Rotate_P_Script.py +++ b/scripts/Rotate_P_Script.py @@ -1,215 +1,215 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -旋转脚本 - 让对象持续旋转 -""" - -from core.script_system import ScriptBase - - -class RotatorScript(ScriptBase): - """旋转脚本类""" - - def __init__(self): - super().__init__() - self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) - self.max_angle = 25.0 # 最大旋转角度(相对于初始角度) - self.direction = 1 - self.current_offset = 0.0 # 当前相对于初始角度的偏移 - self.initial_angle = None # 模型的初始角度 - self.is_rotating = True # 是否正在旋转 - - # 机器人式停顿参数 - self.pause_duration = 0.5 # 停顿时间(秒) - self.current_pause_time = 0.0 # 当前停顿计时 - self.is_paused = False # 是否正在停顿 - self.robot_mode = True # 是否启用机器人模式 - - def start(self): - """脚本开始时调用""" - self.log("机器人旋转脚本启动!") - self.log(f"旋转速度: {self.rotation_speed_y}度/秒") - self.log(f"最大旋转角度: ±{self.max_angle}度") - self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}") - if self.robot_mode: - self.log(f"停顿时间: {self.pause_duration}秒") - - # 记录模型的初始角度 - if self.gameObject: - initial_hpr = self.gameObject.getHpr() - self.initial_angle = initial_hpr.getY() # 记录Y轴的初始角度(Pitch) - self.log(f"模型初始角度: {self.initial_angle}度") - self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") - else: - self.log("⚠️ 无法获取游戏对象,使用默认初始角度0") - self.initial_angle = 0.0 - - def update(self, dt): - """每帧更新 - 机器人式旋转""" - if not self.is_rotating or self.initial_angle is None: - return - - # 如果正在停顿中 - if self.is_paused: - self.current_pause_time += dt - if self.current_pause_time >= self.pause_duration: - # 停顿结束,继续旋转 - self.is_paused = False - self.current_pause_time = 0.0 - self.log(f"停顿结束,继续旋转,方向: {'正向' if self.direction > 0 else '反向'}") - return - - # 计算角度变化量 - delta_angle = self.rotation_speed_y * dt * self.direction - self.current_offset += delta_angle - - # 检查是否到达边界 - reached_boundary = False - if self.current_offset > self.max_angle: - self.current_offset = self.max_angle - self.direction *= -1 - reached_boundary = True - elif self.current_offset < -self.max_angle: - self.current_offset = -self.max_angle - self.direction *= -1 - reached_boundary = True - - # 如果到达边界且启用机器人模式,开始停顿 - if reached_boundary and self.robot_mode: - self.is_paused = True - self.current_pause_time = 0.0 - self.log(f"到达边界 ({self.current_offset}°),开始停顿 {self.pause_duration}秒") - - # 计算最终角度(初始角度 + 偏移量) - final_angle = self.initial_angle + self.current_offset - - # 设置新的旋转(只改变Y轴,保持其他不变) - current_hpr = self.gameObject.getHpr() - self.gameObject.setHpr(current_hpr.getX(), final_angle, current_hpr.getZ()) - - # if not self.is_rotating: - # return - # - # # 获取当前旋转并应用增量 - # current_hpr = self.gameObject.getHpr() - # new_r = current_hpr.getZ() + self.rotation_speed_y * dt - # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) - - def on_destroy(self): - """脚本销毁时调用""" - self.log("机器人旋转脚本停止") - - # ==================== 机器人模式控制方法 ==================== - - def set_robot_mode(self, enabled=True): - """ - 启用或禁用机器人模式 - - Args: - enabled: 是否启用机器人模式 - """ - self.robot_mode = enabled - self.log(f"机器人模式: {'开启' if enabled else '关闭'}") - if not enabled: - self.is_paused = False # 如果禁用机器人模式,立即结束停顿 - - def set_pause_duration(self, duration): - """ - 设置停顿时间 - - Args: - duration: 停顿时间(秒) - """ - self.pause_duration = max(0.1, duration) # 最小0.1秒 - self.log(f"停顿时间已设置为: {self.pause_duration}秒") - - def set_max_angle(self, new_max_angle): - """ - 设置新的最大旋转角度 - - Args: - new_max_angle: 新的最大角度值 - """ - self.max_angle = new_max_angle - self.log(f"最大旋转角度已设置为: ±{self.max_angle}度") - if self.initial_angle is not None: - self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") - - def set_rotation_speed(self, new_speed): - """ - 设置新的旋转速度 - - Args: - new_speed: 新的旋转速度(度/秒) - """ - self.rotation_speed_y = new_speed - self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒") - - def pause_rotation(self): - """暂停旋转""" - self.is_rotating = False - self.log("旋转已暂停") - - def resume_rotation(self): - """恢复旋转""" - self.is_rotating = True - self.is_paused = False # 同时结束停顿状态 - self.log("旋转已恢复") - - def reset_to_initial_angle(self): - """重置到初始角度""" - if self.initial_angle is not None and self.gameObject: - current_hpr = self.gameObject.getHpr() - self.gameObject.setHpr(current_hpr.getX(), self.initial_angle, current_hpr.getZ()) - self.current_offset = 0.0 - self.direction = 1 - self.is_paused = False - self.current_pause_time = 0.0 - self.log(f"已重置到初始角度: {self.initial_angle}度") - - def get_current_info(self): - """获取当前旋转信息""" - if self.gameObject and self.initial_angle is not None: - current_hpr = self.gameObject.getHpr() - current_angle = current_hpr.getY() - self.log("=== 机器人旋转信息 ===") - self.log(f"初始角度: {self.initial_angle}度") - self.log(f"当前角度: {current_angle}度") - self.log(f"偏移量: {self.current_offset}度") - self.log(f"最大角度: ±{self.max_angle}度") - self.log(f"旋转速度: {self.rotation_speed_y}度/秒") - self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}") - self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}") - self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}") - if self.robot_mode: - self.log(f"停顿时间: {self.pause_duration}秒") - self.log(f"当前状态: {'停顿中' if self.is_paused else '旋转中'}") - if self.is_paused: - remaining_time = self.pause_duration - self.current_pause_time - self.log(f"剩余停顿时间: {remaining_time:.1f}秒") - self.log("=== 信息结束 ===") - else: - self.log("无法获取旋转信息") - - # ==================== 预设配置方法 ==================== - - def set_slow_robot_mode(self): - """预设:慢速机器人模式""" - self.set_rotation_speed(15.0) - self.set_pause_duration(1.0) - self.set_robot_mode(True) - self.log("已设置为慢速机器人模式") - - def set_fast_robot_mode(self): - """预设:快速机器人模式""" - self.set_rotation_speed(45.0) - self.set_pause_duration(0.3) - self.set_robot_mode(True) - self.log("已设置为快速机器人模式") - - def set_smooth_mode(self): - """预设:平滑模式(非机器人)""" - self.set_robot_mode(False) - self.set_rotation_speed(30.0) +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.max_angle = 25.0 # 最大旋转角度(相对于初始角度) + self.direction = 1 + self.current_offset = 0.0 # 当前相对于初始角度的偏移 + self.initial_angle = None # 模型的初始角度 + self.is_rotating = True # 是否正在旋转 + + # 机器人式停顿参数 + self.pause_duration = 0.5 # 停顿时间(秒) + self.current_pause_time = 0.0 # 当前停顿计时 + self.is_paused = False # 是否正在停顿 + self.robot_mode = True # 是否启用机器人模式 + + def start(self): + """脚本开始时调用""" + self.log("机器人旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"最大旋转角度: ±{self.max_angle}度") + self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}") + if self.robot_mode: + self.log(f"停顿时间: {self.pause_duration}秒") + + # 记录模型的初始角度 + if self.gameObject: + initial_hpr = self.gameObject.getHpr() + self.initial_angle = initial_hpr.getY() # 记录Y轴的初始角度(Pitch) + self.log(f"模型初始角度: {self.initial_angle}度") + self.log(f"旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + else: + self.log("⚠️ 无法获取游戏对象,使用默认初始角度0") + self.initial_angle = 0.0 + + def update(self, dt): + """每帧更新 - 机器人式旋转""" + if not self.is_rotating or self.initial_angle is None: + return + + # 如果正在停顿中 + if self.is_paused: + self.current_pause_time += dt + if self.current_pause_time >= self.pause_duration: + # 停顿结束,继续旋转 + self.is_paused = False + self.current_pause_time = 0.0 + self.log(f"停顿结束,继续旋转,方向: {'正向' if self.direction > 0 else '反向'}") + return + + # 计算角度变化量 + delta_angle = self.rotation_speed_y * dt * self.direction + self.current_offset += delta_angle + + # 检查是否到达边界 + reached_boundary = False + if self.current_offset > self.max_angle: + self.current_offset = self.max_angle + self.direction *= -1 + reached_boundary = True + elif self.current_offset < -self.max_angle: + self.current_offset = -self.max_angle + self.direction *= -1 + reached_boundary = True + + # 如果到达边界且启用机器人模式,开始停顿 + if reached_boundary and self.robot_mode: + self.is_paused = True + self.current_pause_time = 0.0 + self.log(f"到达边界 ({self.current_offset}°),开始停顿 {self.pause_duration}秒") + + # 计算最终角度(初始角度 + 偏移量) + final_angle = self.initial_angle + self.current_offset + + # 设置新的旋转(只改变Y轴,保持其他不变) + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), final_angle, current_hpr.getZ()) + + # if not self.is_rotating: + # return + # + # # 获取当前旋转并应用增量 + # current_hpr = self.gameObject.getHpr() + # new_r = current_hpr.getZ() + self.rotation_speed_y * dt + # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("机器人旋转脚本停止") + + # ==================== 机器人模式控制方法 ==================== + + def set_robot_mode(self, enabled=True): + """ + 启用或禁用机器人模式 + + Args: + enabled: 是否启用机器人模式 + """ + self.robot_mode = enabled + self.log(f"机器人模式: {'开启' if enabled else '关闭'}") + if not enabled: + self.is_paused = False # 如果禁用机器人模式,立即结束停顿 + + def set_pause_duration(self, duration): + """ + 设置停顿时间 + + Args: + duration: 停顿时间(秒) + """ + self.pause_duration = max(0.1, duration) # 最小0.1秒 + self.log(f"停顿时间已设置为: {self.pause_duration}秒") + + def set_max_angle(self, new_max_angle): + """ + 设置新的最大旋转角度 + + Args: + new_max_angle: 新的最大角度值 + """ + self.max_angle = new_max_angle + self.log(f"最大旋转角度已设置为: ±{self.max_angle}度") + if self.initial_angle is not None: + self.log(f"新的旋转范围: {self.initial_angle - self.max_angle}° 到 {self.initial_angle + self.max_angle}°") + + def set_rotation_speed(self, new_speed): + """ + 设置新的旋转速度 + + Args: + new_speed: 新的旋转速度(度/秒) + """ + self.rotation_speed_y = new_speed + self.log(f"旋转速度已设置为: {self.rotation_speed_y}度/秒") + + def pause_rotation(self): + """暂停旋转""" + self.is_rotating = False + self.log("旋转已暂停") + + def resume_rotation(self): + """恢复旋转""" + self.is_rotating = True + self.is_paused = False # 同时结束停顿状态 + self.log("旋转已恢复") + + def reset_to_initial_angle(self): + """重置到初始角度""" + if self.initial_angle is not None and self.gameObject: + current_hpr = self.gameObject.getHpr() + self.gameObject.setHpr(current_hpr.getX(), self.initial_angle, current_hpr.getZ()) + self.current_offset = 0.0 + self.direction = 1 + self.is_paused = False + self.current_pause_time = 0.0 + self.log(f"已重置到初始角度: {self.initial_angle}度") + + def get_current_info(self): + """获取当前旋转信息""" + if self.gameObject and self.initial_angle is not None: + current_hpr = self.gameObject.getHpr() + current_angle = current_hpr.getY() + self.log("=== 机器人旋转信息 ===") + self.log(f"初始角度: {self.initial_angle}度") + self.log(f"当前角度: {current_angle}度") + self.log(f"偏移量: {self.current_offset}度") + self.log(f"最大角度: ±{self.max_angle}度") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + self.log(f"旋转方向: {'正向' if self.direction > 0 else '反向'}") + self.log(f"旋转状态: {'运行中' if self.is_rotating else '已暂停'}") + self.log(f"机器人模式: {'开启' if self.robot_mode else '关闭'}") + if self.robot_mode: + self.log(f"停顿时间: {self.pause_duration}秒") + self.log(f"当前状态: {'停顿中' if self.is_paused else '旋转中'}") + if self.is_paused: + remaining_time = self.pause_duration - self.current_pause_time + self.log(f"剩余停顿时间: {remaining_time:.1f}秒") + self.log("=== 信息结束 ===") + else: + self.log("无法获取旋转信息") + + # ==================== 预设配置方法 ==================== + + def set_slow_robot_mode(self): + """预设:慢速机器人模式""" + self.set_rotation_speed(15.0) + self.set_pause_duration(1.0) + self.set_robot_mode(True) + self.log("已设置为慢速机器人模式") + + def set_fast_robot_mode(self): + """预设:快速机器人模式""" + self.set_rotation_speed(45.0) + self.set_pause_duration(0.3) + self.set_robot_mode(True) + self.log("已设置为快速机器人模式") + + def set_smooth_mode(self): + """预设:平滑模式(非机器人)""" + self.set_robot_mode(False) + self.set_rotation_speed(30.0) self.log("已设置为平滑旋转模式") \ No newline at end of file diff --git a/scripts/Rotate_R_Script.py b/scripts/Rotate_R_Script.py index 4d22edac..f6dc1ab0 100644 --- a/scripts/Rotate_R_Script.py +++ b/scripts/Rotate_R_Script.py @@ -1,56 +1,56 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -旋转脚本 - 让对象持续旋转 -""" - -from core.script_system import ScriptBase - - -class RotatorScript(ScriptBase): - """旋转脚本类""" - - def __init__(self): - super().__init__() - self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) - self.max_angle = 15.0 - self.direction = 1 - self.current_angle = 0.0 - self.is_rotating = True # 是否正在旋转 - - def start(self): - """脚本开始时调用""" - self.log("旋转脚本启动!") - self.log(f"旋转速度: {self.rotation_speed_y}度/秒") - - def update(self, dt): - """每帧更新""" - - delta_angle = self.rotation_speed_y * dt * self.direction - self.current_angle += delta_angle - - # 如果超出角度范围,则反向 - if self.current_angle > self.max_angle: - self.current_angle = self.max_angle - self.direction *= -1 - elif self.current_angle < -self.max_angle: - self.current_angle = -self.max_angle - self.direction *= -1 - - # 设置新的旋转(只改变Z轴,保持其他不变) - base_hpr = self.gameObject.getHpr() - new_r = self.current_angle - self.gameObject.setHpr(base_hpr.getX(), base_hpr.getY(), new_r) - - # if not self.is_rotating: - # return - # - # # 获取当前旋转并应用增量 - # current_hpr = self.gameObject.getHpr() - # new_r = current_hpr.getZ() + self.rotation_speed_y * dt - # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) - - def on_destroy(self): - """脚本销毁时调用""" +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.max_angle = 15.0 + self.direction = 1 + self.current_angle = 0.0 + self.is_rotating = True # 是否正在旋转 + + def start(self): + """脚本开始时调用""" + self.log("旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + + def update(self, dt): + """每帧更新""" + + delta_angle = self.rotation_speed_y * dt * self.direction + self.current_angle += delta_angle + + # 如果超出角度范围,则反向 + if self.current_angle > self.max_angle: + self.current_angle = self.max_angle + self.direction *= -1 + elif self.current_angle < -self.max_angle: + self.current_angle = -self.max_angle + self.direction *= -1 + + # 设置新的旋转(只改变Z轴,保持其他不变) + base_hpr = self.gameObject.getHpr() + new_r = self.current_angle + self.gameObject.setHpr(base_hpr.getX(), base_hpr.getY(), new_r) + + # if not self.is_rotating: + # return + # + # # 获取当前旋转并应用增量 + # current_hpr = self.gameObject.getHpr() + # new_r = current_hpr.getZ() + self.rotation_speed_y * dt + # self.gameObject.setHpr(current_hpr.getX(), current_hpr.getY(), new_r) + + def on_destroy(self): + """脚本销毁时调用""" self.log("旋转脚本停止") \ No newline at end of file diff --git a/scripts/RotatorScript.py b/scripts/RotatorScript.py index bdc49495..d70ab7a8 100644 --- a/scripts/RotatorScript.py +++ b/scripts/RotatorScript.py @@ -1,39 +1,39 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -旋转脚本 - 让对象持续旋转 -""" - -from core.script_system import ScriptBase - -class RotatorScript(ScriptBase): - """旋转脚本类""" - - def __init__(self): - super().__init__() - self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) - self.is_rotating = True # 是否正在旋转 - - def start(self): - """脚本开始时调用""" - self.log("旋转脚本启动!") - self.log(f"旋转速度: {self.rotation_speed_y}度/秒") - - def update(self, dt): - # 检查 gameObject 是否存在且不为空 - if not self.gameObject or self.gameObject.isEmpty(): - print("RotatorScript: gameObject is empty or None, skipping update") - return - """每帧更新""" - if not self.is_rotating: - return - - # 获取当前旋转并应用增量 - current_hpr = self.gameObject.getHpr() - new_h = current_hpr.getX() + self.rotation_speed_y * dt - self.gameObject.setHpr(new_h, current_hpr.getY(), current_hpr.getZ()) - - def on_destroy(self): - """脚本销毁时调用""" +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +旋转脚本 - 让对象持续旋转 +""" + +from core.script_system import ScriptBase + +class RotatorScript(ScriptBase): + """旋转脚本类""" + + def __init__(self): + super().__init__() + self.rotation_speed_y = 30.0 # Y轴旋转速度 (度/秒) + self.is_rotating = True # 是否正在旋转 + + def start(self): + """脚本开始时调用""" + self.log("旋转脚本启动!") + self.log(f"旋转速度: {self.rotation_speed_y}度/秒") + + def update(self, dt): + # 检查 gameObject 是否存在且不为空 + if not self.gameObject or self.gameObject.isEmpty(): + print("RotatorScript: gameObject is empty or None, skipping update") + return + """每帧更新""" + if not self.is_rotating: + return + + # 获取当前旋转并应用增量 + current_hpr = self.gameObject.getHpr() + new_h = current_hpr.getX() + self.rotation_speed_y * dt + self.gameObject.setHpr(new_h, current_hpr.getY(), current_hpr.getZ()) + + def on_destroy(self): + """脚本销毁时调用""" self.log("旋转脚本停止") \ No newline at end of file diff --git a/scripts/ScalerScript.py b/scripts/ScalerScript.py index 406d65fa..2f43de9a 100644 --- a/scripts/ScalerScript.py +++ b/scripts/ScalerScript.py @@ -1,91 +1,91 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -缩放脚本 - 让对象产生呼吸般的缩放效果 -""" - -from core.script_system import ScriptBase -import math - -class ScalerScript(ScriptBase): - """缩放脚本类""" - - def __init__(self): - super().__init__() - - # 缩放参数 - self.base_scale = 1.0 # 基础缩放 - self.scale_amplitude = 0.3 # 缩放幅度 - self.scale_speed = 2.0 # 缩放速度 (周期/秒) - self.uniform_scale = True # 是否统一缩放(所有轴) - - # 内部变量 - self.time_accumulator = 0.0 # 时间累积器 - self.original_scale = None # 原始缩放 - self.is_scaling = True # 是否正在缩放 - - def start(self): - """脚本开始时调用""" - self.log("缩放脚本启动!") - self.log(f"缩放参数: 基础={self.base_scale}, 幅度={self.scale_amplitude}, 速度={self.scale_speed}") - - # 记录原始缩放 - self.original_scale = self.gameObject.getScale() - self.log(f"原始缩放: {self.original_scale}") - - def update(self, dt): - """每帧更新""" - if not self.is_scaling: - return - - # 累积时间 - self.time_accumulator += dt - - # 计算正弦波缩放值 - sine_value = math.sin(self.time_accumulator * self.scale_speed * 2 * math.pi) - scale_factor = self.base_scale + (self.scale_amplitude * sine_value) - - # 应用缩放 - if self.uniform_scale: - # 统一缩放 - self.gameObject.setScale(scale_factor) - else: - # 非统一缩放(仅Z轴) - current_scale = self.gameObject.getScale() - self.gameObject.setScale(current_scale.getX(), current_scale.getY(), scale_factor) - - def set_scale_parameters(self, base=None, amplitude=None, speed=None, uniform=None): - """设置缩放参数""" - if base is not None: - self.base_scale = base - if amplitude is not None: - self.scale_amplitude = amplitude - if speed is not None: - self.scale_speed = speed - if uniform is not None: - self.uniform_scale = uniform - - self.log(f"缩放参数更新: 基础={self.base_scale}, 幅度={self.scale_amplitude}, 速度={self.scale_speed}") - - def toggle_scaling(self): - """切换缩放状态""" - self.is_scaling = not self.is_scaling - status = "恢复" if self.is_scaling else "暂停" - self.log(f"缩放{status}") - - def reset_scale(self): - """重置到原始缩放""" - if self.original_scale: - self.gameObject.setScale(self.original_scale) - self.time_accumulator = 0.0 - self.log("缩放已重置到原始值") - - def pulse_once(self): - """执行一次脉冲缩放""" - self.time_accumulator = 0.0 - self.log("执行脉冲缩放") - - def on_destroy(self): - """脚本销毁时调用""" +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +缩放脚本 - 让对象产生呼吸般的缩放效果 +""" + +from core.script_system import ScriptBase +import math + +class ScalerScript(ScriptBase): + """缩放脚本类""" + + def __init__(self): + super().__init__() + + # 缩放参数 + self.base_scale = 1.0 # 基础缩放 + self.scale_amplitude = 0.3 # 缩放幅度 + self.scale_speed = 2.0 # 缩放速度 (周期/秒) + self.uniform_scale = True # 是否统一缩放(所有轴) + + # 内部变量 + self.time_accumulator = 0.0 # 时间累积器 + self.original_scale = None # 原始缩放 + self.is_scaling = True # 是否正在缩放 + + def start(self): + """脚本开始时调用""" + self.log("缩放脚本启动!") + self.log(f"缩放参数: 基础={self.base_scale}, 幅度={self.scale_amplitude}, 速度={self.scale_speed}") + + # 记录原始缩放 + self.original_scale = self.gameObject.getScale() + self.log(f"原始缩放: {self.original_scale}") + + def update(self, dt): + """每帧更新""" + if not self.is_scaling: + return + + # 累积时间 + self.time_accumulator += dt + + # 计算正弦波缩放值 + sine_value = math.sin(self.time_accumulator * self.scale_speed * 2 * math.pi) + scale_factor = self.base_scale + (self.scale_amplitude * sine_value) + + # 应用缩放 + if self.uniform_scale: + # 统一缩放 + self.gameObject.setScale(scale_factor) + else: + # 非统一缩放(仅Z轴) + current_scale = self.gameObject.getScale() + self.gameObject.setScale(current_scale.getX(), current_scale.getY(), scale_factor) + + def set_scale_parameters(self, base=None, amplitude=None, speed=None, uniform=None): + """设置缩放参数""" + if base is not None: + self.base_scale = base + if amplitude is not None: + self.scale_amplitude = amplitude + if speed is not None: + self.scale_speed = speed + if uniform is not None: + self.uniform_scale = uniform + + self.log(f"缩放参数更新: 基础={self.base_scale}, 幅度={self.scale_amplitude}, 速度={self.scale_speed}") + + def toggle_scaling(self): + """切换缩放状态""" + self.is_scaling = not self.is_scaling + status = "恢复" if self.is_scaling else "暂停" + self.log(f"缩放{status}") + + def reset_scale(self): + """重置到原始缩放""" + if self.original_scale: + self.gameObject.setScale(self.original_scale) + self.time_accumulator = 0.0 + self.log("缩放已重置到原始值") + + def pulse_once(self): + """执行一次脉冲缩放""" + self.time_accumulator = 0.0 + self.log("执行脉冲缩放") + + def on_destroy(self): + """脚本销毁时调用""" self.log("缩放脚本停止") \ No newline at end of file diff --git a/scripts/TestMover.py b/scripts/TestMover.py index 4116b1e3..6ae01a0e 100644 --- a/scripts/TestMover.py +++ b/scripts/TestMover.py @@ -1,41 +1,41 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -TestMover - 移动脚本 -""" - -from core.script_system import ScriptBase - -class Testmover(ScriptBase): - """移动脚本类""" - - def __init__(self): - super().__init__() - self.speed = 5.0 # 移动速度 - self.direction = [1, 0, 0] # 移动方向 - - def start(self): - """脚本开始时调用""" - self.log("移动脚本开始运行!") - - def update(self, dt): - """每帧更新""" - if self.transform: - # 计算移动偏移 - offset_x = self.direction[0] * self.speed * dt - offset_y = self.direction[1] * self.speed * dt - offset_z = self.direction[2] * self.speed * dt - - # 更新位置 - current_pos = self.transform.getPos() - new_pos = ( - current_pos.x + offset_x, - current_pos.y + offset_y, - current_pos.z + offset_z - ) - self.transform.setPos(*new_pos) - - def on_destroy(self): - """脚本销毁时调用""" - self.log("移动脚本被销毁") +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +TestMover - 移动脚本 +""" + +from core.script_system import ScriptBase + +class Testmover(ScriptBase): + """移动脚本类""" + + def __init__(self): + super().__init__() + self.speed = 5.0 # 移动速度 + self.direction = [1, 0, 0] # 移动方向 + + def start(self): + """脚本开始时调用""" + self.log("移动脚本开始运行!") + + def update(self, dt): + """每帧更新""" + if self.transform: + # 计算移动偏移 + offset_x = self.direction[0] * self.speed * dt + offset_y = self.direction[1] * self.speed * dt + offset_z = self.direction[2] * self.speed * dt + + # 更新位置 + current_pos = self.transform.getPos() + new_pos = ( + current_pos.x + offset_x, + current_pos.y + offset_y, + current_pos.z + offset_z + ) + self.transform.setPos(*new_pos) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("移动脚本被销毁") diff --git a/scripts/TestRotator.py b/scripts/TestRotator.py index 83042e31..2a1531cb 100644 --- a/scripts/TestRotator.py +++ b/scripts/TestRotator.py @@ -1,28 +1,28 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -TestRotator - 自定义脚本 -""" - -from core.script_system import ScriptBase - -class Testrotator(ScriptBase): - """自定义脚本类""" - - def __init__(self): - super().__init__() - # 在这里初始化您的变量 - - def start(self): - """脚本开始时调用""" - self.log("脚本开始运行!") - - def update(self, dt): - """每帧更新""" - # 在这里编写更新逻辑 - pass - - def on_destroy(self): - """脚本销毁时调用""" - self.log("脚本被销毁") +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +TestRotator - 自定义脚本 +""" + +from core.script_system import ScriptBase + +class Testrotator(ScriptBase): + """自定义脚本类""" + + def __init__(self): + super().__init__() + # 在这里初始化您的变量 + + def start(self): + """脚本开始时调用""" + self.log("脚本开始运行!") + + def update(self, dt): + """每帧更新""" + # 在这里编写更新逻辑 + pass + + def on_destroy(self): + """脚本销毁时调用""" + self.log("脚本被销毁") diff --git a/scripts/TestScaler.py b/scripts/TestScaler.py index 536883d7..a06d2779 100644 --- a/scripts/TestScaler.py +++ b/scripts/TestScaler.py @@ -1,28 +1,28 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -TestScaler - 自定义脚本 -""" - -from core.script_system import ScriptBase - -class Testscaler(ScriptBase): - """自定义脚本类""" - - def __init__(self): - super().__init__() - # 在这里初始化您的变量 - - def start(self): - """脚本开始时调用""" - self.log("脚本开始运行!") - - def update(self, dt): - """每帧更新""" - # 在这里编写更新逻辑 - pass - - def on_destroy(self): - """脚本销毁时调用""" - self.log("脚本被销毁") +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +TestScaler - 自定义脚本 +""" + +from core.script_system import ScriptBase + +class Testscaler(ScriptBase): + """自定义脚本类""" + + def __init__(self): + super().__init__() + # 在这里初始化您的变量 + + def start(self): + """脚本开始时调用""" + self.log("脚本开始运行!") + + def update(self, dt): + """每帧更新""" + # 在这里编写更新逻辑 + pass + + def on_destroy(self): + """脚本销毁时调用""" + self.log("脚本被销毁") diff --git a/scripts/a.py b/scripts/a.py index 34bd5408..1e6f67f5 100644 --- a/scripts/a.py +++ b/scripts/a.py @@ -1,28 +1,28 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -a - 自定义脚本 -""" - -from core.script_system import ScriptBase - -class A(ScriptBase): - """自定义脚本类""" - - def __init__(self): - super().__init__() - # 在这里初始化您的变量 - - def start(self): - """脚本开始时调用""" - self.log("脚本开始运行!") - - def update(self, dt): - """每帧更新""" - # 在这里编写更新逻辑 - pass - - def on_destroy(self): - """脚本销毁时调用""" - self.log("脚本被销毁") +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +a - 自定义脚本 +""" + +from core.script_system import ScriptBase + +class A(ScriptBase): + """自定义脚本类""" + + def __init__(self): + super().__init__() + # 在这里初始化您的变量 + + def start(self): + """脚本开始时调用""" + self.log("脚本开始运行!") + + def update(self, dt): + """每帧更新""" + # 在这里编写更新逻辑 + pass + + def on_destroy(self): + """脚本销毁时调用""" + self.log("脚本被销毁") diff --git a/scripts/example_script.py b/scripts/example_script.py index ecba0da9..70e4a163 100644 --- a/scripts/example_script.py +++ b/scripts/example_script.py @@ -1,47 +1,47 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -示例脚本 - 演示如何编写脚本 -""" - -from core.script_system import ScriptBase - -class ExampleScript(ScriptBase): - """示例脚本类""" - - def __init__(self): - super().__init__() - self.counter = 0 - self.rotation_speed = 30.0 # 度/秒 - - def start(self): - """脚本开始时调用""" - self.log("示例脚本开始运行!") - self.log(f"挂载到对象: {self.gameObject.getName()}") - - def update(self, dt): - """每帧更新""" - self.counter += 1 - - # 每60帧输出一次信息 - if self.counter % 60 == 0: - self.log(f"运行了 {self.counter} 帧") - - # 让对象旋转 - if self.transform: - current_h = self.transform.getH() - new_h = current_h + self.rotation_speed * dt - self.transform.setH(new_h) - - def on_destroy(self): - """脚本销毁时调用""" - self.log("示例脚本被销毁") - - def on_enable(self): - """脚本启用时调用""" - self.log("示例脚本被启用") - - def on_disable(self): - """脚本禁用时调用""" - self.log("示例脚本被禁用") +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +示例脚本 - 演示如何编写脚本 +""" + +from core.script_system import ScriptBase + +class ExampleScript(ScriptBase): + """示例脚本类""" + + def __init__(self): + super().__init__() + self.counter = 0 + self.rotation_speed = 30.0 # 度/秒 + + def start(self): + """脚本开始时调用""" + self.log("示例脚本开始运行!") + self.log(f"挂载到对象: {self.gameObject.getName()}") + + def update(self, dt): + """每帧更新""" + self.counter += 1 + + # 每60帧输出一次信息 + if self.counter % 60 == 0: + self.log(f"运行了 {self.counter} 帧") + + # 让对象旋转 + if self.transform: + current_h = self.transform.getH() + new_h = current_h + self.rotation_speed * dt + self.transform.setH(new_h) + + def on_destroy(self): + """脚本销毁时调用""" + self.log("示例脚本被销毁") + + def on_enable(self): + """脚本启用时调用""" + self.log("示例脚本被启用") + + def on_disable(self): + """脚本禁用时调用""" + self.log("示例脚本被禁用") diff --git a/scripts/test_quick_script.py b/scripts/test_quick_script.py index 716070a3..54c7f0e1 100644 --- a/scripts/test_quick_script.py +++ b/scripts/test_quick_script.py @@ -1,28 +1,28 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -test_quick_script - 自定义脚本 -""" - -from core.script_system import ScriptBase - -class TestQuickScript(ScriptBase): - """自定义脚本类""" - - def __init__(self): - super().__init__() - # 在这里初始化您的变量 - - def start(self): - """脚本开始时调用""" - self.log("脚本开始运行!") - - def update(self, dt): - """每帧更新""" - # 在这里编写更新逻辑 - pass - - def on_destroy(self): - """脚本销毁时调用""" - self.log("脚本被销毁") +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +test_quick_script - 自定义脚本 +""" + +from core.script_system import ScriptBase + +class TestQuickScript(ScriptBase): + """自定义脚本类""" + + def __init__(self): + super().__init__() + # 在这里初始化您的变量 + + def start(self): + """脚本开始时调用""" + self.log("脚本开始运行!") + + def update(self, dt): + """每帧更新""" + # 在这里编写更新逻辑 + pass + + def on_destroy(self): + """脚本销毁时调用""" + self.log("脚本被销毁") diff --git a/templates/main_template.py b/templates/main_template.py index 70b6f42d..66377f43 100644 --- a/templates/main_template.py +++ b/templates/main_template.py @@ -1,1572 +1,1572 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -测试动画模型 - Panda3D应用程序 -使用Panda3D引擎编辑器创建 -""" - -from __future__ import print_function - -import json - -from direct.actor.Actor import Actor -from direct.showbase.ShowBaseGlobal import globalClock -from panda3d.core import TextNode, CardMaker, TextureStage, NodePath, Texture, TransparencyAttrib, CollisionTraverser, \ - Point3 -from core.InfoPanelManager import InfoPanelManager -# 获取渲染管线路径 -# 在文件开头添加sys导入(如果还没有的话) -import sys -import os - -# 修改工作目录设置部分 -if getattr(sys, 'frozen', False): - # 打包后的环境 - project_root = os.path.dirname(sys.executable) -else: - # 开发环境 - try: - project_root = os.path.dirname(os.path.abspath(__file__)) - except NameError: - project_root = os.getcwd() - -os.chdir(project_root) - -render_pipeline_path = 'RenderPipelineFile' -sys.path.insert(0, render_pipeline_path) - -# 改进路径设置逻辑 -pipeline_path = os.path.join(project_root, "RenderPipelineFile") -if os.path.exists(pipeline_path) and pipeline_path not in sys.path: - sys.path.insert(0, pipeline_path) - # 同时添加子目录以确保所有模块都能正确导入 - for root, dirs, files in os.walk(pipeline_path): - if root not in sys.path: - sys.path.insert(0, root) - - -import math -from random import random, randint, seed -from panda3d.core import Vec3, load_prc_file_data, Filename -from direct.showbase.ShowBase import ShowBase -from direct.task.TaskManagerGlobal import taskMgr - -# os.chdir(os.path.dirname(os.path.realpath(__file__))) -from core.script_system import ScriptManager -from core.CustomMouseController import CustomMouseController -from panda3d.core import CollisionTraverser - -class MainApp(ShowBase): - def __init__(self): - # 在调用父类构造函数前确保必要的属性存在 - if not hasattr(self, 'appRunner'): - self.appRunner = None - - load_prc_file_data("", """ - win-size 1380 750 - window-title - support-threads #t - """) - - # 简化 sys.path 设置逻辑 - pipeline_path = os.path.join(project_root, "RenderPipelineFile") - if os.path.exists(pipeline_path): - if pipeline_path not in sys.path: - sys.path.insert(0, pipeline_path) - else: - print(f"错误: 找不到渲染管线目录: {pipeline_path}") - return - - try: - from rpcore import RenderPipeline - self.render_pipeline = RenderPipeline() - self.render_pipeline.create(self) - - except ImportError as e: - - print(f"导入RenderPipeline模块失败: {e}") - import traceback - traceback.print_exc() - ShowBase.__init__(self) - self.render_pipeline = None - return - - self.script_manager = ScriptManager(self) - self.script_manager.start_system() - - # 加载所有脚本e - self.script_manager.load_all_scripts_from_directory() - - self.info_panel_manager = InfoPanelManager(self) - - try: - # 再导入controller模块 - from rpcore.util.movement_controller import MovementController - - self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam - - self.controller = MovementController(self) - self.camLens.set_fov(80) - self.controller.set_initial_position( - Vec3(0, -50, 20), Vec3(0, 0, 0)) - self.controller.setup() - except ImportError as e: - print(f"导入MovementController失败: {e}") - self.controller = None - - self._last_click_time = 0 - self._last_clicked_node = None - self._double_click_threshold = 0.3 - - self.cameraSpeed = 20.0 - self.cameraRotateSpeed=10.0 - - self.lastMouseX=0 - self.lastMouseY=0 - self.mouseRightPressed=False - - self._loadFont() - self.loadFullScene() - self.loadGUIFromJSON() - self.setupMouseClickHandler() - self.cTrav = CollisionTraverser() - - self.createFrameRateDisplay() - - self.women_actor = None - self.current_actor = None - - if hasattr(self, 'accept'): - base.accept("l", self.tour) - - def _loadFont(self): - """加载中文字体""" - self.chinese_font = None - try: - self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') - if not self.chinese_font: - print("警告: 无法加载中文字体,将使用默认字体") - else: - print("✓ 中文字体加载成功") - except: - print("警告: 无法加载中文字体,将使用默认字体") - self.chinese_font = None - - def getChineseFont(self): - """获取中文字体""" - return self.chinese_font - - def getResourcePath(self,relative_path): - if getattr(sys,'frozen',False): - base_path = os.path.dirname(sys.executable) - else: - base_path = os.path.dirname(os.path.abspath(__file__)) - - return os.path.join(base_path,relative_path) - - def loadFullScene(self): - if not hasattr(self, 'loader') or not hasattr(self, 'render'): - print("错误: Panda3D核心组件未正确初始化") - return - try: - scene_file = self.getResourcePath("scene.bam") - - if os.path.exists(scene_file): - # 使用readBamFile加载完整场景 - from panda3d.core import BamCache - BamCache.getGlobalPtr().setActive(False) # 禁用缓存以避免问题 - - scene = self.loader.loadModel(Filename.fromOsSpecific(scene_file)) - if scene: - scene.reparentTo(self.render) - self.render_pipeline.prepare_scene(scene) - print("✓ 完整场景加载成功") - - # 检测并播放模型动画 - #self._processModelAnimations(scene) - - # 处理场景中的各种元素 - self.processSceneElements(scene) - else: - print("⚠️ 场景文件加载失败") - else: - print("⚠️ 未找到场景文件") - except Exception as e: - print(f"加载完整场景时出错: {str(e)}") - import traceback - traceback.print_exc() - - def checkAnimationStatus(self): - """检查场景中所有Actor的动画状态""" - try: - all_actors = self.render.findAllMatches("**/+ActorNode") - print(f"=== 动画状态检查 ===") - print(f"场景中总共有 {len(all_actors)} 个Actor") - - for i, actor_np in enumerate(all_actors): - actor_node = actor_np.node() - if isinstance(actor_node, Actor): - # 确保Actor可见 - actor_np.show() - - current_anim = actor_node.getCurrentAnim() - anim_names = actor_node.getAnimNames() - print(f"Actor {i + 1} ({actor_np.getName()}):") - print(f" 位置: {actor_np.getPos()}") - print(f" 可见性: {actor_np.isHidden()}") - print(f" 可用动画: {anim_names}") - print(f" 当前播放: {current_anim}") - if current_anim: - frame = actor_node.getCurrentFrame(current_anim) - num_frames = actor_node.getNumFrames(current_anim) - play_rate = actor_node.getPlayRate(current_anim) - print(f" 播放进度: {frame}/{num_frames} 帧") - print(f" 播放速度: {play_rate}") - else: - print(f"节点 {actor_np.getName()} 不是Actor类型") - print("==================") - except Exception as e: - print(f"检查动画状态时出错: {str(e)}") - - def _processModelAnimations(self, node_path): - """处理节点中的动画模型""" - try: - # 查找场景中所有可能的动画模型 - char_nodes = node_path.findAllMatches("**/+Character") - - for char_node in char_nodes: - try: - # 获取父节点(通常是模型根节点) - model_root = char_node.getParent() - model_name = model_root.getName() - - print(f"检测到可能的动画模型: {model_name}") - - # 尝试创建Actor - actor = Actor(model_root) - actor.reparentTo(self.render) - - actor.setPos(node_path.getPos()) - actor.setHpr(node_path.getHpr()) - actor.setScale(node_path.getScale()) - - - - # 获取动画名称 - anim_names = actor.getAnimNames() - if anim_names: - print(f"✓ 成功创建动画模型: {model_name}") - print(f" 可用动画: {anim_names}") - - # 循环播放所有动画 - for anim_name in anim_names: - print(f" 循环播放动画: {anim_name}") - actor.loop(anim_name) - break # 只播放第一个动画,避免同时播放多个动画 - - # 替换原始模型 - model_root.detachNode() - - else: - # 没有动画,使用原始模型 - print(f"模型 {model_name} 不包含动画") - actor.detachNode() # 移除创建的Actor - - except Exception as e: - print(f"处理动画模型 {char_node.getName()} 时出错: {str(e)}") - - except Exception as e: - print(f"处理模型动画时出错: {str(e)}") - import traceback - traceback.print_exc() - - def processSceneElements(self, scene): - """处理场景中的各种元素""" - try: - processed_lights = [] - loaded_nodes = {} - - def processNode(nodePath, depth=0): - loaded_nodes[nodePath.getName()] = nodePath - - # 为模型添加碰撞体 - 修复版本 - if nodePath.hasTag("is_scene_element") and not nodePath.hasTag("is_gizmo"): - if nodePath.hasTag("gui_type"): - gui_type = nodePath.getTag("gui_type") - if gui_type in ["video_screen"]: - print(f"移除GUI视频节点: {nodePath.getName()}") - nodePath.removeNode() # 移除视频屏幕节点 - return - # 使用更精确的包围盒 - from panda3d.core import CollisionNode, CollisionBox - bounds = nodePath.getBounds() - if not bounds.isEmpty(): - min_point = bounds.getMin() - max_point = bounds.getMax() - - # 创建碰撞节点 - collision_node = CollisionNode(f'collision_{nodePath.getName()}') - collision_node.addSolid(CollisionBox(min_point, max_point)) - collision_np = nodePath.attachNewNode(collision_node) - # 隐藏碰撞体 - collision_np.hide() - - if nodePath.hasTag("scripts_info"): - try: - import json - scripts_info = json.loads(nodePath.getTag("scripts_info")) - self.processScripts(nodePath, scripts_info) - except Exception as e: - print(f"处理节点 {nodePath.getName()} 的脚本时出错: {str(e)}") - - if nodePath.hasTag("light_type"): - light_type = nodePath.getTag("light_type") - - if nodePath.hasTag("is_auxiliary_light") and nodePath.getTag("is_auxiliary_light").lower() == "true": - print(f"跳过辅助灯光节点:{nodePath.getName()}") - return - - if nodePath not in processed_lights: - if light_type == "spot_light": - self._recreateSpotLight(nodePath) - elif light_type == "point_light": - self._recreatePointLight(nodePath) - processed_lights.append(nodePath) - - for child in nodePath.getChildren(): - processNode(child, depth + 1) - - processNode(scene) - - except Exception as e: - print(f"处理场景元素时出错: {str(e)}") - - def _recreateSpotLight(self,light_node): - try: - from RenderPipelineFile.rpcore import SpotLight - from panda3d.core import Vec3 - - light = SpotLight() - light.direction = Vec3(0,0,-1) - light.fov = 70 - light.set_color_from_temperature(5*1000.0) - - if light_node.hasTag("light_energy"): - light.energy = float(light_node.getTag("light_energy")) - else: - light.energy = 5000 - - light.radius = 1000 - light.casts_shadows = True - light.shadow_map_resolution = 256 - - light_pos = light_node.getPos() - light.setPos(light_pos) - - self.render_pipeline.add_light(light) - except Exception as e: - print(f"创建点光源 {light_node.getName()} 失败: {str(e)}") - import traceback - traceback.print_exc() - - def _recreatePointLight(self,light_node): - try: - from RenderPipelineFile.rpcore import PointLight - - light = PointLight() - - if light_node.hasTag("light_energy"): - light.energy = float(light_node.getTag("light_energy")) - else: - light.energy = 5000 - - light.radius = 1000 - light.inner_radius = 0.4 - light.set_color_from_temperature(5*1000.0) - light.casts_shadows = True - light.shadow_map_resolution = 256 - - light.setPos(light_node.getPos()) - - self.render_pipeline.add_light(light) - except Exception as e: - print(f"创建点光源 {light_node.getName()} 失败: {str(e)}") - import traceback - traceback.print_exc() - - def processGUIElements(self, scene): - """处理场景中的GUI元素""" - try: - # 查找并处理2D图像 - images_2d = scene.findAllMatches("**/=gui_type=image_2d") - for img_node in images_2d: - try: - # GUI元素通常在场景加载时自动处理 - print(f"✓ 2D图像 {img_node.getName()} 已加载") - except Exception as e: - print(f"处理2D图像 {img_node.getName()} 失败: {str(e)}") - - except Exception as e: - print(f"处理GUI元素时出错: {str(e)}") - - def tour(self): - mopath = ( - (Vec3(-10.8645000458, 9.76458263397, 2.13306283951), Vec3(-133.556228638, -4.23447799683, 0.0)), - (Vec3(-10.6538448334, -5.98406457901, 1.68028640747), Vec3(-59.3999938965, -3.32706642151, 0.0)), - (Vec3(9.58458328247, -5.63625621796, 2.63269257545), Vec3(58.7906494141, -9.40668964386, 0.0)), - (Vec3(6.8135137558, 11.0153560638, 2.25509500504), Vec3(148.762527466, -6.41223621368, 0.0)), - (Vec3(-9.07093334198, 3.65908527374, 1.42396306992), Vec3(245.362503052, -3.59927511215, 0.0)), - (Vec3(-8.75390911102, -3.82727789879, 0.990055501461), Vec3(296.090484619, -0.604830980301, 0.0)), - ) - self.controller.play_motion_path(mopath, 3.0) - - def loadGUIFromJSON(self): - try: - gui_json_path = self.getResourcePath("gui/gui_elements.json") - if os.path.exists(gui_json_path): - with open(gui_json_path, 'r', encoding='utf-8') as f: - content = f.read().strip() - if content: - gui_data = json.loads(content) - self.createGUIElement(gui_data) - else: - print("GUI配置文件为空 ") - except Exception as e: - print(f"加载GUI元素时出错: {str(e)}") - import traceback - traceback.print_exc() - - def createGUIElement(self, element_data): - try: - processed_names = set() - element_original_data = {} - - for i, gui_info in enumerate(element_data): - name = gui_info.get("name", f"gui_element_{i}") - element_original_data[name] = { - "scale": gui_info.get("scale", [1, 1, 1]), - "position": gui_info.get("position", [0, 0, 0]), - "parent_name": gui_info.get("parent_name") - } - valid_parents = set() - for gui_info in element_data: - name = gui_info.get("name", f"gui_element_{gui_info.get('index', 0)}") - valid_parents.add(name) - - for i, gui_info in enumerate(element_data): - try: - gui_type = gui_info.get("type", "unknown") - name = gui_info.get("name", f"gui_element_{i}") - position = gui_info.get("position", [0, 0, 0]) - scale = gui_info.get("scale", [1, 1, 1]) - tags = gui_info.get("tags", {}) - text = gui_info.get("text", "") - image_path = gui_info.get("image_path", "") - video_path = gui_info.get("video_path", "") - bg_image_path = gui_info.get("bg_image_path", "") - parent_name = gui_info.get("parent_name") - - if name in processed_names: - continue - - processed_names.add(name) - - absolute_position = list(position) - absolute_scale = list(scale) - - if parent_name and parent_name in element_original_data: - parent_data = element_original_data[parent_name] - parent_scale = parent_data["scale"] - - if gui_type in ["3d_text", "3d_image", "button", "label", "entry", "2d_image", - "2d_video_screen"]: - # 位置需要乘以父级缩放来得到绝对位置 - for j in range(min(len(absolute_position), len(parent_scale))): - absolute_position[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] - - # 缩放需要乘以父级缩放来得到绝对缩放 - for j in range(min(len(absolute_scale), len(parent_scale))): - absolute_scale[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] - - new_element = None - - if gui_type == "3d_text": - size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5 - new_element = self.createGUI3DText( - pos=tuple(absolute_position), - text=text, - size=size - ) - elif gui_type == "button": - new_element = self.createGUIButton( - pos=tuple(absolute_position), - text=text, - size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0, - command=self.playModelAnimation - ) - elif gui_type == "label": - new_element = self.createGUILabel( - pos=tuple(absolute_position), - text=text, - size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 - ) - elif gui_type == "entry": - new_element = self.createGUIEntry( - pos=tuple(absolute_position), - placeholder=text, - size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0, - command=self.onGUIEntrySubmit - ) - elif gui_type == "2d_image": - - scale_value = absolute_scale[0] - print(f"2d_image{scale_value}") - new_element = self.createGUI2DImage( - pos=tuple(absolute_position), - image_path=image_path, - size=absolute_scale - ) - elif gui_type == "2d_video_screen": - new_element = self.createGUI2DVideoScreen( - pos=tuple(absolute_position), - video_path=video_path, - size=absolute_scale - ) - elif gui_type == "video_screen": - new_element = self.createGUIVideoScreen( - pos=tuple(absolute_position), - video_path=video_path, - size=absolute_scale - ) - elif gui_type == "info_panel": - new_element = self.info_panel_manager.onCreateSampleInfoPanel() - - if "scripts" in gui_info and new_element: - self.processScripts(new_element,gui_info["scripts"]) - - except Exception as e: - print(f"重建GUI元素失败 {name}: {e}") - import traceback - traceback.print_exc() - continue - - except Exception as e: - print(f"创建GUI元素时出错: {str(e)}") - - def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1, command=None): - from direct.gui.DirectGui import DirectButton - - button = DirectButton( - text=text, - pos=(pos[0], pos[1], pos[2]), # 保持正确的坐标格式 - scale=size, # size 应该是数值而不是元组 - frameColor=(0.2, 0.6, 0.8, 1), - text_font=self.getChineseFont() if self.getChineseFont() else None, - rolloverSound=None, - clickSound=None, - parent=None, - command=command - ) - - def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): - """创建3D文本GUI元素""" - try: - # 创建文本节点 - text_node = TextNode("gui_3d_text") - text_node.setText(text) - text_node.setAlign(TextNode.ACenter) - - # 设置字体(如果可用) - if self.getChineseFont(): - text_node.setFont(self.getChineseFont()) - - # 创建节点路径并添加到场景 - text_np = self.render.attachNewNode(text_node) - - # 设置位置和大小 - text_np.setPos(Vec3(pos[0], pos[1], pos[2])) - text_np.setScale(size) - - # 设置面向摄像机 - # text_np.setBillboardPointEye() - - # 设置渲染属性 - text_np.setBin("fixed", 40) - text_np.setDepthWrite(False) - - return text_np - except Exception as e: - print(f"❌ 创建3D文本失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08): - from direct.gui.DirectGui import DirectLabel - label = DirectLabel( - text=text, - pos=(pos[0], pos[1], pos[2]), - scale=size, - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 1, 1), - text_font=self.getChineseFont() if self.getChineseFont() else None, - text_align=TextNode.ACenter, - text_wordwrap=None, - text_mayChange=True, - parent=None - ) - - def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08, command=None): - from direct.gui.DirectGui import DirectEntry - - entry = DirectEntry( - text="", - pos=(pos[0], pos[1], pos[2]), - scale=size, - command=command, - initialText=placeholder, - numLines=1, - width=12, - focus=0, - frameColor=(0, 0, 0, 0), - text_fg=(1, 1, 1, 1), - text_font=self.getChineseFont() if self.getChineseFont() else None, - text_align=TextNode.ACenter, - text_wordwrap=None, - text_mayChange=True, - parent=None, - rolloverSound=None, - clickSound=None, - # 添加焦点管理命令 - focusInCommand=self.disableCameraControl, - focusOutCommand=self.enableCameraControl, - # 确保输入框能正确捕获所有键盘事件 - suppressKeys=True, # 这个参数很重要,它会阻止按键事件传播到其他处理器 - suppressMouse=True - ) - return entry - - def disableCameraControl(self): - """禁用相机控制""" - try: - if hasattr(self, 'controller'): - # 如果控制器有内置的禁用方法 - if hasattr(self.controller, 'disable'): - self.controller.disable() - else: - # 否则手动禁用事件监听 - self.controller.ignoreAll() # 忽略所有已注册的事件 - print("相机控制已禁用") - except Exception as e: - print(f"禁用相机控制时出错: {e}") - - def enableCameraControl(self): - """启用相机控制""" - try: - if hasattr(self, 'controller'): - # 如果控制器有内置的启用方法 - if hasattr(self.controller, 'enable'): - self.controller.enable() - else: - # 重新设置控制器 - self.controller.setup() - print("相机控制已启用") - except Exception as e: - print(f"启用相机控制时出错: {e}") - - def onGUIEntrySubmit(self, text, entry_id=None): - """GUI输入框提交事件处理""" - try: - print(f"GUI输入框提交: {entry_id} = {text}") - - # 重新启用相机控制 - self.enableCameraControl() - - # 清除输入框焦点(如果需要) - # base.win.focus() - - # 在这里添加您需要的文本处理逻辑 - # 例如保存文本、更新UI等 - - except Exception as e: - print(f"处理输入框提交时出错: {e}") - import traceback - traceback.print_exc() - - def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=0.2): - # 添加属性检查 - if not hasattr(self, 'aspect2d'): - print("错误: aspect2d未初始化") - return None - if image_path and not os.path.isabs(image_path): - image_path = self.getResourcePath(image_path) - # 处理非均匀缩放 - if isinstance(size, (list, tuple)) and len(size) >= 2: - # 分别处理宽度和高度的缩放 - width_scale = size[0] * 0.2 - height_scale = size[2] * 0.2 - else: - # 如果只提供了一个缩放值,则使用相同值 - width_scale = size * 0.1 if isinstance(size, (int, float)) else 0.2 - height_scale = width_scale - - cm = CardMaker("gui-2d-image") - cm.setFrame(-width_scale, width_scale, -height_scale, height_scale) - #cm.setFrame(-size, size, -size, size) - - image_node = self.aspect2d.attachNewNode(cm.generate()) - image_node.setPos(pos) - image_node.setBin("fixed", 0) - image_node.setDepthWrite(False) - image_node.setDepthTest(False) - image_node.setColor(1, 1, 1, 1) - - # 设置透明度支持 - image_node.setTransparency(TransparencyAttrib.MAlpha) - - if image_path: - try: - texture = self.loader.loadTexture(image_path) - if texture: - image_node.setTexture(texture, 1) - texture.setWrapU(Texture.WM_clamp) - texture.setWrapV(Texture.WM_clamp) - texture.setMinfilter(Texture.FT_linear) - texture.setMagfilter(Texture.FT_linear) - image_node.setColor(1, 1, 1, 1) - else: - print(f"无法加载图片: {image_path}") - except Exception as e: - print(f"加载图片时出错: {e}") - return image_node - - def createGUIVideoScreen(self,pos=(0,0,0),size=1,video_path=None): - import os - from panda3d.core import TransparencyAttrib,Texture,TextureStage - - if isinstance(size,(list,tuple)) and len(size) >= 2: - width_scale = size[0] - height_scale = size[2] - else: - width_scale = size * 0.1 if isinstance(size, (int, float)) else 0.2 - height_scale = width_scale - - cm = CardMaker("gui-video-screen") - cm.setFrame(-width_scale,width_scale,-height_scale,height_scale) - - video_screen = self.render.attachNewNode(cm.generate()) - video_screen.setPos(pos) - - video_screen.setBin("fixed", 0) - - #video_screen.setTransparency(TransparencyAttrib.MAlpha) - - video_screen.setColor(1, 1, 1, 1) - - self._ensureVideoScreenMaterial(video_screen) - - if video_path: - if video_path.startswith(("http://", "https://")): - try: - success = self._loadVideoFromURLWithOpenCV3D(video_screen, video_path) - if success: - print("视频已从URL加载成功") - else: - print("从URL加载视频时出错") - except Exception as e: - print(f"从URL加载视频时出错: {e}") - elif os.path.exists(video_path): - movie_texture = self._loadMovieTexture(video_path) - if movie_texture: - video_screen.setTexture(movie_texture, 1) - else: - print(f"视频文件不存在: {video_path}") - else: - # 设置占位符纹理 - placeholder_texture = Texture() - placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) - placeholder_data = b'\x19\x19\x4c' # 深蓝色占位符 - placeholder_texture.setRamImage(placeholder_data) - video_screen.setTexture(placeholder_texture, 1) - - return video_screen - - def createGUI2DVideoScreen(self, pos=(0, 0, 0), size=0.2, video_path=None): - import os - from direct.gui.DirectGui import DirectFrame - from panda3d.core import TransparencyAttrib, Texture, TextureStage - - if isinstance(size,(list,tuple)) and len(size) >= 2: - width_scale = size[0]*0.2 - height_scale = size[2]*0.2 - else: - width_scale = size*0.1 if isinstance(size,(int,float)) else 0.2 - height_scale = width_scale - - video_screen = DirectFrame( - frameSize=(-width_scale, width_scale, -height_scale, height_scale), - frameColor=(1, 1, 1, 1), - pos=pos, - parent=None, - suppressMouse=True - ) - video_screen.setTransparency(TransparencyAttrib.MAlpha) - - placeholder_texture = Texture() - placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) - placeholder_data = b'\x19\x19\x4c' - placeholder_texture.setRamImage(placeholder_data) - - if video_path: - if video_path.startswith(("http://","https://")): - try: - success = self._loadVideoFromURLWithOpenCV(video_screen,video_path) - if success: - print("视频已从URL加载成功") - else: - print("从URL加载视频时出错") - except Exception as e: - print(f"从URL加载视频时出错: {e}") - elif os.path.exists(video_path): - movie_texture = self._loadMovieTexture(video_path) - if movie_texture: - video_screen["frameTexture"] = movie_texture - - return video_screen - - def _loadMovieTexture(self, video_path): - from panda3d.core import Texture, MovieTexture, Filename - import os - - # Convert Windows path to Panda3D compatible path format - panda_path = Filename.fromOsSpecific(video_path) - converted_path = str(panda_path) - - movie_texture = MovieTexture(converted_path) - if movie_texture.read(converted_path): - self._configureVideoTexture(movie_texture) - return movie_texture - - def _configureVideoTexture(self, texture): - from panda3d.core import Texture - - texture.setWrapU(Texture.WM_clamp) - texture.setWrapV(Texture.WM_clamp) - texture.setMinfilter(Texture.FT_linear) - texture.setMagfilter(Texture.FT_linear) - - texture.setFormat(Texture.FRgb8) - - if hasattr(texture, 'set_loop') and hasattr(texture, 'set_play_rate'): - texture.set_loop(True) - texture.set_play_rate(1.0) - - def _loadVideoFromURLWithOpenCV(self, video_screen, url): - try: - import cv2 - import threading - from panda3d.core import Texture,PNMImage - import numpy as np - import time - - def video_stream_thread(): - cap = cv2.VideoCapture(url) - - if not cap.isOpened(): - print(f"无法打开视频流: {url}") - return False - - cap.set(cv2.CAP_PROP_BUFFERSIZE,1) - - fps = cap.get(cv2.CAP_PROP_FPS) - frame_delay = 1.0 / fps if fps > 0 else 0.033 # 默认30fps - - while hasattr(self, 'video_stream_active') and self.video_stream_active: - ret, frame = cap.read() - - if not ret: - print("视频流读取失败,尝试重新连接...") - cap.release() - time.sleep(1) - cap = cv2.VideoCapture(url) - continue - - frame_height,frame_width = frame.shape[:2] - if frame_width > 256: - scale = 256.0/frame_width - new_width = int(frame_width * scale) - new_height = int(frame_height * scale) - frame = cv2.resize(frame,(new_width,new_height)) - - frame_rgb = cv2.cvtColor(frame,cv2.COLOR_BGR2RGB) - - height,width = frame_rgb.shape[:2] - - img = PNMImage(width,height,3) - img.set_maxval(255) - - for y in range(height): - for x in range(width): - r,g,b = frame_rgb[y,x] - img.setXelVal(x,y,r,g,b) - - texture = Texture("video_texture") - texture.setMagfilter(Texture.FTLinear) - texture.setMinfilter(Texture.FTLinear) - texture.setWrapU(Texture.WMClamp) - texture.setWrapV(Texture.WMClamp) - texture.load(img) - - # 在主线程中更新GUI纹理 - def update_texture(): - if hasattr(self, 'aspect2d') and not video_screen.isEmpty(): - video_screen["frameTexture"] = texture - - # 使用taskMgr在主线程中更新纹理 - if hasattr(self, 'taskMgr'): - self.taskMgr.doMethodLater(0, lambda task: update_texture() or task.done, - f"updateVideoTexture_{time.time()}") - - # 控制帧率 - time.sleep(frame_delay) - - cap.release() - print("视频流线程结束") - return True - - # 启动视频流线程 - self.video_stream_active = True - self.video_thread = threading.Thread(target=video_stream_thread, daemon=True) - self.video_thread.start() - - return True - - except Exception as e: - print(f"加载视频流失败: {e}") - import traceback - traceback.print_exc() - return False - - def _loadVideoFromURLWithOpenCV3D(self,video_screen,url): - try: - import cv2 - import threading - from panda3d.core import Texture,PNMImage - import time - - def video_stream_thread(): - cap = cv2.VideoCapture(url) - - if not cap.isOpened(): - return False - - cap.set(cv2.CAP_PROP_BUFFERSIZE,1) - - fps = cap.get(cv2.CAP_PROP_FPS) - frame_delay = 1.0 / fps if fps > 0 else 0.033 - - while hasattr(self,'video_stream_active') and self.video_stream_active: - ret,frame = cap.read() - - if not ret: - cap.release() - time.sleep(1) - cap = cv2.VideoCapture(url) - continue - - frame_height,frame_width = frame.shape[:2] - if frame_width > 256: - scale = 256.0/frame_width - new_width = int(frame_width*scale) - new_height = int(frame_height*scale) - frame = cv2.resize(frame,(new_width,new_height)) - - frame_rgb = cv2.cvtColor(frame,cv2.COLOR_BGR2RGB) - - height,width = frame_rgb.shape[:2] - - img = PNMImage(width,height,3) - img.set_maxval(255) - - for y in range(height): - for x in range(width): - r,g,b = frame_rgb[y,x] - img.setXelVal(x,y,r,g,b) - - texture = Texture("video_texture_3d") - texture.setMagfilter(Texture.FTLinear) - texture.setMinfilter(Texture.FTLinear) - texture.setWrapU(Texture.WMClamp) - texture.setWrapV(Texture.WMClamp) - texture.load(img) - - def update_texture(): - if hasattr(self,'render') and not video_screen.isEmpty(): - video_screen.setTexture(texture,1) - - if hasattr(self,'taskMgr'): - self.taskMgr.doMethodLater(0,lambda task:update_texture() or task.done,f"updateVideoTexture_{time.time()}") - - time.sleep(frame_delay) - - cap.release() - return True - self.video_stream_active = True - self.video_thread_3d = threading.Thread(target=video_stream_thread,daemon=True) - self.video_thread_3d.start() - - return True - except Exception as e: - print(f"加载3D视频流失败: {e}") - import traceback - traceback.print_exc() - return False - - def _ensureVideoScreenMaterial(self, video_screen): - """确保视频屏幕有正确的材质设置""" - try: - from panda3d.core import Material, LColor - - if not video_screen.hasMaterial(): - material = Material(f"video-material-{video_screen.getName()}") - material.setBaseColor(LColor(1, 1, 1, 1)) - material.setDiffuse(LColor(1, 1, 1, 1)) - material.setAmbient(LColor(1, 1, 1, 1)) # 确保环境光为白色 - material.setEmission(LColor(0, 0, 0, 1)) - material.setSpecular(LColor(0, 0, 0, 1)) - material.setShininess(0) - video_screen.setMaterial(material, 1) - else: - # 更新现有材质确保正确设置 - material = video_screen.getMaterial() - material.setBaseColor(LColor(1, 1, 1, 1)) - material.setAmbient(LColor(1, 1, 1, 1)) # 确保环境光为白色 - video_screen.setMaterial(material, 1) - - except Exception as e: - print(f"⚠️ 设置视频屏幕材质时出错: {e}") - - def playModelAnimation(self): - """播放场景中所有 .glb 模型的动画(仅转换有动画的模型为 Actor)""" - try: - glb_models = self.render.findAllMatches("**/*.glb*") - - # 修复过滤逻辑,确保正确排除碰撞体节点 - filtered_models = [] - for model in glb_models: - model_name = model.getName().lower() - # 排除碰撞体节点 - if ("collision_" not in model_name and - "modelcollision_" not in model_name and - not model_name.endswith("_bounds")): - filtered_models.append(model) - - if not filtered_models: - print("⚠️ 场景中没有找到 .glb 模型") - return - - print(f"找到 {len(filtered_models)} 个 glb 模型") - - self.actors = [] # 存储所有 Actor,避免被垃圾回收 - - for model in filtered_models: - print(f"正在处理模型: {model.getName()}") - - actor = None - - # 尝试把模型当作Actor加载,判断是否有动画 - try: - # 在创建Actor之前先检查节点是否可能包含动画 - if not self._isValidAnimationNode(model): - print(f"⚠️ {model.getName()} 不是有效的动画节点,跳过") - continue - - test_actor = Actor(model) - anim_names = test_actor.getAnimNames() - except Exception as e: - print(f"⚠️ {model.getName()} 无法作为Actor加载: {str(e)}") - anim_names = [] - - if anim_names: # ✅ 只有有动画才转为Actor - if not isinstance(model, Actor): - model_parent = model.getParent() - model_pos = model.getPos() - model_hpr = model.getHpr() - model_scale = model.getScale() - - actor = Actor(model) - actor.reparentTo(model_parent) - actor.setPos(model_pos) - actor.setHpr(model_hpr) - actor.setScale(model_scale) - - model.detachNode() - else: - actor = model - - # 播放动画 - actor.show() - self.actors.append(actor) - - print(f"✓ {actor.getName()} 可用动画: {anim_names}") - first_anim = anim_names[0] - - actor.stop() - actor.setPlayRate(1.0, first_anim) - actor.loop(first_anim) - print(f"✓ {actor.getName()} 正在播放动画: {first_anim}") - - actor.update() - - else: # 没有动画 - print(f"⚠️ {model.getName()} 没有动画,不转为Actor") - - except Exception as e: - print(f"播放模型动画时出错: {str(e)}") - import traceback - traceback.print_exc() - - def _isValidAnimationNode(self, nodePath): - """检查节点是否可能是有效的动画节点""" - # 排除明显不是动画节点的节点 - name = nodePath.getName().lower() - if ("collision_" in name or - "modelcollision_" in name or - "bound" in name or - name.endswith("_bounds")): - return False - - # 检查节点是否有网格数据(简单判断) - from panda3d.core import GeomNode - geom_nodes = nodePath.findAllMatches("**/+GeomNode") - if not geom_nodes: - return False - - return True - - def focusOnWomenModel(self): - """定位并聚焦到Women模型""" - try: - women_models = self.render.findAllMatches("**/Women_1.glb*") - if women_models: - model = women_models[0] - # 确保模型可见 - model.show() - - # 将模型放置在原点附近 - model.setPos(0, 0, 0) - model.setScale(1.0) - - # 确保是Actor的话设置动画 - if isinstance(model, Actor): - anim_names = model.getAnimNames() - if anim_names: - model.loop(anim_names[0]) - model.setPlayRate(1.0, anim_names[0]) - print(f"为模型设置动画: {anim_names[0]}") - - print(f"已定位模型: {model.getName()}") - else: - print("未找到Women模型") - except Exception as e: - print(f"定位模型时出错: {str(e)}") - - def processScripts(self, element, script_info_list): - """处理元素上挂载的脚本 - 使用新的脚本系统""" - try: - print(f"正在为元素 {element.getName()} 挂载脚本") - print(f"可用脚本列表: {self.script_manager.get_available_scripts()}") - - if not hasattr(self,'script_manager'): - print("脚本管理器未初始化") - return - - for script_info in script_info_list: - script_name = script_info["name"] - script_file = script_info.get("file", "") - - if script_name: - script_component = self.script_manager.add_script_to_object(element,script_name) - - if script_component: - print(f"✓ 脚本 {script_name} 已挂载到元素 {element.getName()}") - else: - print(f"⚠️ 脚本 {script_name} 挂载失败") - # 列出可用脚本帮助调试 - available_scripts = self.script_manager.get_available_scripts() - print(f"当前可用脚本: {available_scripts}") - else: - print(f"⚠️ 脚本信息不完整: {script_info}") - - # # 从文件路径中提取脚本类名 - # if script_file: - # # 获取脚本文件名(不含路径和扩展名) - # script_filename = os.path.basename(script_file) - # script_class_name = os.path.splitext(script_filename)[0] - # print(f"尝试挂载脚本: {script_class_name} (来自文件: {script_file})") - # - # # 使用脚本管理器为元素添加脚本 - # script_component = self.script_manager.add_script_to_object(element, script_class_name) - # - # if script_component: - # print(f"✓ 脚本 {script_class_name} 已挂载到元素 {element.getName()}") - # else: - # print(f"⚠️ 脚本 {script_class_name} 挂载失败") - # # 列出可用脚本帮助调试 - # available_scripts = self.script_manager.get_available_scripts() - # print(f"当前可用脚本: {available_scripts}") - # else: - # print(f"⚠️ 脚本信息不完整: {script_name}") - - except Exception as e: - print(f"挂载脚本到元素 {element.getName()} 失败: {str(e)}") - import traceback - traceback.print_exc() - - def checkDoubleClick(self, nodePath): - """检查是否为双击,返回布尔值 - 改进版本""" - try: - import time - current_time = time.time() - - # 必须是同一节点且在时间阈值内 - is_double_click = (self._last_clicked_node is not None and - self._last_clicked_node == nodePath and - nodePath is not None and - current_time - self._last_click_time < self._double_click_threshold) - - if is_double_click: - # 双击成功,重置状态 - self._last_click_time = 0 - self._last_clicked_node = None - print(f"✓ 检测到双击: {nodePath.getName()}") - return True - else: - # 单击,更新状态 - self._last_click_time = current_time - self._last_clicked_node = nodePath - return False - - except Exception as e: - print(f"双击检测失败: {e}") - return False - - def focusCameraOnNode(self, nodePath): - """带动画效果的聚焦到节点方法""" - try: - if not nodePath or nodePath.isEmpty(): - print("无效的节点") - return - - minPoint = Point3() - maxPoint = Point3() - - if not nodePath.calcTightBounds(minPoint, maxPoint,self.render): - print("无法计算选中节点的边界框,使用节点为位置作为替代方案") - node_pos = nodePath.getPos(self.render) - optimal_distance = 10.0 - current_cam_pos = self.cam.getPos() - view_direction = node_pos - current_cam_pos - if view_direction.length()<0.001: - view_direction = Vec3(5,-5,2) - view_direction.normalize() - target_cam_pos = node_pos-(view_direction * optimal_distance) - - temp_node = self.render.attachNewNode("temp_lookat_target") - temp_node.setPos(node_pos) - dummy_cam = self.render.attachNewNode("dummy_camera") - dummy_cam.setPos(target_cam_pos) - dummy_cam.lookAt(temp_node) - target_cam_hpr = Vec3(dummy_cam.getHpr()) - - temp_node.removeNode() - dummy_cam.removeNode() - - current_cam_pos = Point3(self.cam.getPos()) - current_cam_hpr = Vec3(self.cam.getHpr()) - self._startCameraFocusAnimation(current_cam_pos,target_cam_pos,current_cam_hpr,target_cam_hpr) - print(f"开始聚焦到节点(使用位置): {nodePath.getName()}") - - return - - center = Point3( - (minPoint.x + maxPoint.x)*0.5, - (minPoint.y+maxPoint.y)*0.5, - (minPoint.z+maxPoint.z)*0.5 - ) - - size = (maxPoint - minPoint).length() - if size < 0.01: - size = 1.0 - - current_cam_pos = Point3(self.cam.getPos()) - current_cam_hpr = Vec3(self.cam.getHpr()) - - view_direction = current_cam_pos - center - if view_direction.length() < 0.001: - view_direction = Vec3(5,-5,2) - - view_direction.normalize() - - optimal_distance = max(size * 2.5,1.0) - - target_cam_pos = center + (view_direction * optimal_distance) - - temp_node = self.render.attachNewNode("temp_lookat_target") - temp_node.setPos(center) - - dummy_cam = self.render.attachNewNode("dummy_camera") - dummy_cam.setPos(target_cam_pos) - dummy_cam.lookAt(temp_node) - target_cam_hpr = Vec3(dummy_cam.getHpr()) - - temp_node.removeNode() - dummy_cam.removeNode() - - self._startCameraFocusAnimation(current_cam_pos,target_cam_pos, - current_cam_hpr,target_cam_hpr) - - print(f"开始聚焦到节点: {nodePath.getName()}") - return True - - except Exception as e: - print(f"聚焦相机失败: {str(e)}") - import traceback - traceback.print_exc() - - def _startCameraFocusAnimation(self, start_pos, end_pos, start_hpr, end_hpr): - """启动相机聚焦动画 - 改进版本""" - try: - class CameraFocusData: - def __init__(self, start_pos, end_pos, start_hpr, end_hpr): - self.start_pos = Point3(start_pos) - self.end_pos = Point3(end_pos) - self.start_hpr = Vec3(start_hpr) - self.end_hpr = Vec3(end_hpr) - self.elapsed_time = 0.0 - self.duration = 0.5 # 增加动画时间使移动更平滑 - - self._camera_focus_data = CameraFocusData(start_pos, end_pos, start_hpr, end_hpr) - - from direct.task.TaskManagerGlobal import taskMgr - taskMgr.remove("cameraFocusTask") - - taskMgr.add(self._cameraFocusTask, "cameraFocusTask") - print(f"开始相机聚焦动画: {start_pos} -> {end_pos}") - - except Exception as e: - print(f"相机聚焦动画启动失败: {str(e)}") - - def _normalizeAngle(self, angle): - """规范化角度到-180到180度之间""" - while angle > 180: - angle -= 360 - while angle < -180: - angle += 360 - return angle - - def _cameraFocusTask(self, task): - """摄像机聚焦动画任务""" - try: - if not hasattr(self, '_camera_focus_data'): - return task.done - - data = self._camera_focus_data - from direct.showbase.ShowBaseGlobal import globalClock - dt = globalClock.getDt() - - if dt <= 0: - dt = 0.016 - - data.elapsed_time += dt - t = min(1.0, data.elapsed_time / data.duration) - - # 使用平滑插值 - smooth_t = t * t * (3 - 2 * t) - - # 插值位置和朝向 - current_pos = data.start_pos + (data.end_pos - data.start_pos) * smooth_t - current_hpr = data.start_hpr + (data.end_hpr - data.start_hpr) * smooth_t - - self.cam.setPos(current_pos) - self.cam.setHpr(current_hpr) - - if t >= 1.0: - self.cam.setPos(data.end_pos) - self.cam.setHpr(data.end_hpr) - - print(f"聚焦完成: 位置 {data.end_pos}, 朝向 {data.end_hpr}") - - if hasattr(self, '_camera_focus_data'): - delattr(self, '_camera_focus_data') - - return task.done - - return task.cont - - except Exception as e: - print(f"摄像机聚焦动画任务失败: {e}") - import traceback - traceback.print_exc() - return task.done - - def setupMouseClickHandler(self): - """设置鼠标点击处理器""" - try: - # 设置鼠标监听 - self.accept("mouse1", self.onMouseClick) - - # 启用鼠标观察器 - if not hasattr(self, 'mouseWatcherNode'): - from panda3d.core import MouseWatcher - self.mouseWatcherNode = MouseWatcher() - - except Exception as e: - print(f"设置鼠标点击处理器失败: {e}") - - def onMouseClick(self): - """处理鼠标左键点击""" - try: - if not hasattr(self, 'mouseWatcherNode') or not self.mouseWatcherNode.hasMouse(): - return - - # 获取鼠标位置 - mouse_pos = self.mouseWatcherNode.getMouse() - - # 创建射线进行点击检测 - from panda3d.core import CollisionRay, CollisionNode, CollisionHandlerQueue, BitMask32 - - # 创建射线 - ray = CollisionRay() - ray.setFromLens(self.camNode, mouse_pos.x, mouse_pos.y) - - # 创建碰撞节点 - ray_node = CollisionNode('mouseRay') - ray_node.addSolid(ray) - ray_node.setFromCollideMask(BitMask32.allOn()) - - # 附加到相机 - ray_np = self.camera.attachNewNode(ray_node) - - # 创建碰撞队列 - handler = CollisionHandlerQueue() - - # 确保碰撞遍历器存在 - if not hasattr(self, 'cTrav'): - self.cTrav = CollisionTraverser() - - self.cTrav.addCollider(ray_np, handler) - self.cTrav.traverse(self.render) - - # 检查碰撞结果 - target_node = None - if handler.getNumEntries() > 0: - # 按距离排序 - handler.sortEntries() - - # 遍历所有碰撞结果,找到最近的有效节点 - for i in range(handler.getNumEntries()): - entry = handler.getEntry(i) - clicked_node = entry.getIntoNodePath() - - # 向上遍历找到可选择的父节点 - current_node = clicked_node - found_valid_node = False - - while current_node and not current_node.isEmpty(): - # 检查节点是否应该被选择 - if (current_node.hasTag("is_scene_element") and - not current_node.hasTag("is_gizmo") and - current_node.getName() not in ["render", "camera", "ambient_light", - "directional_light", "mouseRay"] and - not current_node.getName().startswith("collision_") and - "ground" not in current_node.getName().lower()): - target_node = current_node - found_valid_node = True - break - - # 如果当前节点是碰撞体,获取其父节点作为目标 - if current_node.getName().startswith("collision_"): - parent = current_node.getParent() - if (parent.hasTag("is_scene_element") and - not parent.hasTag("is_gizmo") and - parent.getName() not in ["render", "camera", "ambient_light", - "directional_light"] and - "ground" not in parent.getName().lower()): - target_node = parent - found_valid_node = True - break - - current_node = current_node.getParent() - if current_node.getName() == "render": - break - - if found_valid_node: - break - - # 如果找到了目标节点,则进行双击检测 - if target_node: - print(f"找到可选择节点: {target_node.getName()}") - print(f"节点位置: {target_node.getPos()}") - print(f"节点边界: {target_node.getBounds()}") - # 检查是否为双击 - if self.checkDoubleClick(target_node): - print(f"双击节点: {target_node.getName()}") - self.focusCameraOnNode(target_node) - else: - print(f"单击节点: {target_node.getName()}") - else: - print("未找到有效的目标节点") - - # 清理碰撞器 - ray_np.removeNode() - - except Exception as e: - print(f"处理鼠标点击失败: {e}") - import traceback - traceback.print_exc()\ - - def createFrameRateDisplay(self): - from direct.gui.DirectGui import DirectLabel - from panda3d.core import TextNode - - self.fps_label = DirectLabel( - text="FPS:0", - pos=(-1.8,0,0.9), - scale=0.05, - text_fg=(1,1,1,1), - text_align=TextNode.ALeft, - frameSize=(-0.1,0.3,-0.05,0.05), - frameColor=(0,0,0,0.5), - text_font=self.getChineseFont() if self.getChineseFont() else None, - #parent=self.a2dTopLeft - ) - - taskMgr.add(self.updateFrameRate,"updateFrameRateTask") - - def updateFrameRate(self,task): - try: - fps = globalClock.getAverageFrameRate() - - if self.fps_label: - self.fps_label.setText(f"FPS:{fps:.1f}") - except Exception as e: - print(f"更新帧率失败: {e}") - return task.cont - - -# 在 main.py 的最后部分,修改为: -if __name__ == "__main__": - try: - app = MainApp() - if hasattr(app, 'run'): - app.run() - else: - print("应用程序初始化失败") - except Exception as e: - print(f"应用程序启动失败: {str(e)}") - import traceback - traceback.print_exc() - - +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +测试动画模型 - Panda3D应用程序 +使用Panda3D引擎编辑器创建 +""" + +from __future__ import print_function + +import json + +from direct.actor.Actor import Actor +from direct.showbase.ShowBaseGlobal import globalClock +from panda3d.core import TextNode, CardMaker, TextureStage, NodePath, Texture, TransparencyAttrib, CollisionTraverser, \ + Point3 +from core.InfoPanelManager import InfoPanelManager +# 获取渲染管线路径 +# 在文件开头添加sys导入(如果还没有的话) +import sys +import os + +# 修改工作目录设置部分 +if getattr(sys, 'frozen', False): + # 打包后的环境 + project_root = os.path.dirname(sys.executable) +else: + # 开发环境 + try: + project_root = os.path.dirname(os.path.abspath(__file__)) + except NameError: + project_root = os.getcwd() + +os.chdir(project_root) + +render_pipeline_path = 'RenderPipelineFile' +sys.path.insert(0, render_pipeline_path) + +# 改进路径设置逻辑 +pipeline_path = os.path.join(project_root, "RenderPipelineFile") +if os.path.exists(pipeline_path) and pipeline_path not in sys.path: + sys.path.insert(0, pipeline_path) + # 同时添加子目录以确保所有模块都能正确导入 + for root, dirs, files in os.walk(pipeline_path): + if root not in sys.path: + sys.path.insert(0, root) + + +import math +from random import random, randint, seed +from panda3d.core import Vec3, load_prc_file_data, Filename +from direct.showbase.ShowBase import ShowBase +from direct.task.TaskManagerGlobal import taskMgr + +# os.chdir(os.path.dirname(os.path.realpath(__file__))) +from core.script_system import ScriptManager +from core.CustomMouseController import CustomMouseController +from panda3d.core import CollisionTraverser + +class MainApp(ShowBase): + def __init__(self): + # 在调用父类构造函数前确保必要的属性存在 + if not hasattr(self, 'appRunner'): + self.appRunner = None + + load_prc_file_data("", """ + win-size 1380 750 + window-title + support-threads #t + """) + + # 简化 sys.path 设置逻辑 + pipeline_path = os.path.join(project_root, "RenderPipelineFile") + if os.path.exists(pipeline_path): + if pipeline_path not in sys.path: + sys.path.insert(0, pipeline_path) + else: + print(f"错误: 找不到渲染管线目录: {pipeline_path}") + return + + try: + from rpcore import RenderPipeline + self.render_pipeline = RenderPipeline() + self.render_pipeline.create(self) + + except ImportError as e: + + print(f"导入RenderPipeline模块失败: {e}") + import traceback + traceback.print_exc() + ShowBase.__init__(self) + self.render_pipeline = None + return + + self.script_manager = ScriptManager(self) + self.script_manager.start_system() + + # 加载所有脚本e + self.script_manager.load_all_scripts_from_directory() + + self.info_panel_manager = InfoPanelManager(self) + + try: + # 再导入controller模块 + from rpcore.util.movement_controller import MovementController + + self.render_pipeline._showbase.camera = self.render_pipeline._showbase.cam + + self.controller = MovementController(self) + self.camLens.set_fov(80) + self.controller.set_initial_position( + Vec3(0, -50, 20), Vec3(0, 0, 0)) + self.controller.setup() + except ImportError as e: + print(f"导入MovementController失败: {e}") + self.controller = None + + self._last_click_time = 0 + self._last_clicked_node = None + self._double_click_threshold = 0.3 + + self.cameraSpeed = 20.0 + self.cameraRotateSpeed=10.0 + + self.lastMouseX=0 + self.lastMouseY=0 + self.mouseRightPressed=False + + self._loadFont() + self.loadFullScene() + self.loadGUIFromJSON() + self.setupMouseClickHandler() + self.cTrav = CollisionTraverser() + + self.createFrameRateDisplay() + + self.women_actor = None + self.current_actor = None + + if hasattr(self, 'accept'): + base.accept("l", self.tour) + + def _loadFont(self): + """加载中文字体""" + self.chinese_font = None + try: + self.chinese_font = self.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc') + if not self.chinese_font: + print("警告: 无法加载中文字体,将使用默认字体") + else: + print("✓ 中文字体加载成功") + except: + print("警告: 无法加载中文字体,将使用默认字体") + self.chinese_font = None + + def getChineseFont(self): + """获取中文字体""" + return self.chinese_font + + def getResourcePath(self,relative_path): + if getattr(sys,'frozen',False): + base_path = os.path.dirname(sys.executable) + else: + base_path = os.path.dirname(os.path.abspath(__file__)) + + return os.path.join(base_path,relative_path) + + def loadFullScene(self): + if not hasattr(self, 'loader') or not hasattr(self, 'render'): + print("错误: Panda3D核心组件未正确初始化") + return + try: + scene_file = self.getResourcePath("scene.bam") + + if os.path.exists(scene_file): + # 使用readBamFile加载完整场景 + from panda3d.core import BamCache + BamCache.getGlobalPtr().setActive(False) # 禁用缓存以避免问题 + + scene = self.loader.loadModel(Filename.fromOsSpecific(scene_file)) + if scene: + scene.reparentTo(self.render) + self.render_pipeline.prepare_scene(scene) + print("✓ 完整场景加载成功") + + # 检测并播放模型动画 + #self._processModelAnimations(scene) + + # 处理场景中的各种元素 + self.processSceneElements(scene) + else: + print("⚠️ 场景文件加载失败") + else: + print("⚠️ 未找到场景文件") + except Exception as e: + print(f"加载完整场景时出错: {str(e)}") + import traceback + traceback.print_exc() + + def checkAnimationStatus(self): + """检查场景中所有Actor的动画状态""" + try: + all_actors = self.render.findAllMatches("**/+ActorNode") + print(f"=== 动画状态检查 ===") + print(f"场景中总共有 {len(all_actors)} 个Actor") + + for i, actor_np in enumerate(all_actors): + actor_node = actor_np.node() + if isinstance(actor_node, Actor): + # 确保Actor可见 + actor_np.show() + + current_anim = actor_node.getCurrentAnim() + anim_names = actor_node.getAnimNames() + print(f"Actor {i + 1} ({actor_np.getName()}):") + print(f" 位置: {actor_np.getPos()}") + print(f" 可见性: {actor_np.isHidden()}") + print(f" 可用动画: {anim_names}") + print(f" 当前播放: {current_anim}") + if current_anim: + frame = actor_node.getCurrentFrame(current_anim) + num_frames = actor_node.getNumFrames(current_anim) + play_rate = actor_node.getPlayRate(current_anim) + print(f" 播放进度: {frame}/{num_frames} 帧") + print(f" 播放速度: {play_rate}") + else: + print(f"节点 {actor_np.getName()} 不是Actor类型") + print("==================") + except Exception as e: + print(f"检查动画状态时出错: {str(e)}") + + def _processModelAnimations(self, node_path): + """处理节点中的动画模型""" + try: + # 查找场景中所有可能的动画模型 + char_nodes = node_path.findAllMatches("**/+Character") + + for char_node in char_nodes: + try: + # 获取父节点(通常是模型根节点) + model_root = char_node.getParent() + model_name = model_root.getName() + + print(f"检测到可能的动画模型: {model_name}") + + # 尝试创建Actor + actor = Actor(model_root) + actor.reparentTo(self.render) + + actor.setPos(node_path.getPos()) + actor.setHpr(node_path.getHpr()) + actor.setScale(node_path.getScale()) + + + + # 获取动画名称 + anim_names = actor.getAnimNames() + if anim_names: + print(f"✓ 成功创建动画模型: {model_name}") + print(f" 可用动画: {anim_names}") + + # 循环播放所有动画 + for anim_name in anim_names: + print(f" 循环播放动画: {anim_name}") + actor.loop(anim_name) + break # 只播放第一个动画,避免同时播放多个动画 + + # 替换原始模型 + model_root.detachNode() + + else: + # 没有动画,使用原始模型 + print(f"模型 {model_name} 不包含动画") + actor.detachNode() # 移除创建的Actor + + except Exception as e: + print(f"处理动画模型 {char_node.getName()} 时出错: {str(e)}") + + except Exception as e: + print(f"处理模型动画时出错: {str(e)}") + import traceback + traceback.print_exc() + + def processSceneElements(self, scene): + """处理场景中的各种元素""" + try: + processed_lights = [] + loaded_nodes = {} + + def processNode(nodePath, depth=0): + loaded_nodes[nodePath.getName()] = nodePath + + # 为模型添加碰撞体 - 修复版本 + if nodePath.hasTag("is_scene_element") and not nodePath.hasTag("is_gizmo"): + if nodePath.hasTag("gui_type"): + gui_type = nodePath.getTag("gui_type") + if gui_type in ["video_screen"]: + print(f"移除GUI视频节点: {nodePath.getName()}") + nodePath.removeNode() # 移除视频屏幕节点 + return + # 使用更精确的包围盒 + from panda3d.core import CollisionNode, CollisionBox + bounds = nodePath.getBounds() + if not bounds.isEmpty(): + min_point = bounds.getMin() + max_point = bounds.getMax() + + # 创建碰撞节点 + collision_node = CollisionNode(f'collision_{nodePath.getName()}') + collision_node.addSolid(CollisionBox(min_point, max_point)) + collision_np = nodePath.attachNewNode(collision_node) + # 隐藏碰撞体 + collision_np.hide() + + if nodePath.hasTag("scripts_info"): + try: + import json + scripts_info = json.loads(nodePath.getTag("scripts_info")) + self.processScripts(nodePath, scripts_info) + except Exception as e: + print(f"处理节点 {nodePath.getName()} 的脚本时出错: {str(e)}") + + if nodePath.hasTag("light_type"): + light_type = nodePath.getTag("light_type") + + if nodePath.hasTag("is_auxiliary_light") and nodePath.getTag("is_auxiliary_light").lower() == "true": + print(f"跳过辅助灯光节点:{nodePath.getName()}") + return + + if nodePath not in processed_lights: + if light_type == "spot_light": + self._recreateSpotLight(nodePath) + elif light_type == "point_light": + self._recreatePointLight(nodePath) + processed_lights.append(nodePath) + + for child in nodePath.getChildren(): + processNode(child, depth + 1) + + processNode(scene) + + except Exception as e: + print(f"处理场景元素时出错: {str(e)}") + + def _recreateSpotLight(self,light_node): + try: + from RenderPipelineFile.rpcore import SpotLight + from panda3d.core import Vec3 + + light = SpotLight() + light.direction = Vec3(0,0,-1) + light.fov = 70 + light.set_color_from_temperature(5*1000.0) + + if light_node.hasTag("light_energy"): + light.energy = float(light_node.getTag("light_energy")) + else: + light.energy = 5000 + + light.radius = 1000 + light.casts_shadows = True + light.shadow_map_resolution = 256 + + light_pos = light_node.getPos() + light.setPos(light_pos) + + self.render_pipeline.add_light(light) + except Exception as e: + print(f"创建点光源 {light_node.getName()} 失败: {str(e)}") + import traceback + traceback.print_exc() + + def _recreatePointLight(self,light_node): + try: + from RenderPipelineFile.rpcore import PointLight + + light = PointLight() + + if light_node.hasTag("light_energy"): + light.energy = float(light_node.getTag("light_energy")) + else: + light.energy = 5000 + + light.radius = 1000 + light.inner_radius = 0.4 + light.set_color_from_temperature(5*1000.0) + light.casts_shadows = True + light.shadow_map_resolution = 256 + + light.setPos(light_node.getPos()) + + self.render_pipeline.add_light(light) + except Exception as e: + print(f"创建点光源 {light_node.getName()} 失败: {str(e)}") + import traceback + traceback.print_exc() + + def processGUIElements(self, scene): + """处理场景中的GUI元素""" + try: + # 查找并处理2D图像 + images_2d = scene.findAllMatches("**/=gui_type=image_2d") + for img_node in images_2d: + try: + # GUI元素通常在场景加载时自动处理 + print(f"✓ 2D图像 {img_node.getName()} 已加载") + except Exception as e: + print(f"处理2D图像 {img_node.getName()} 失败: {str(e)}") + + except Exception as e: + print(f"处理GUI元素时出错: {str(e)}") + + def tour(self): + mopath = ( + (Vec3(-10.8645000458, 9.76458263397, 2.13306283951), Vec3(-133.556228638, -4.23447799683, 0.0)), + (Vec3(-10.6538448334, -5.98406457901, 1.68028640747), Vec3(-59.3999938965, -3.32706642151, 0.0)), + (Vec3(9.58458328247, -5.63625621796, 2.63269257545), Vec3(58.7906494141, -9.40668964386, 0.0)), + (Vec3(6.8135137558, 11.0153560638, 2.25509500504), Vec3(148.762527466, -6.41223621368, 0.0)), + (Vec3(-9.07093334198, 3.65908527374, 1.42396306992), Vec3(245.362503052, -3.59927511215, 0.0)), + (Vec3(-8.75390911102, -3.82727789879, 0.990055501461), Vec3(296.090484619, -0.604830980301, 0.0)), + ) + self.controller.play_motion_path(mopath, 3.0) + + def loadGUIFromJSON(self): + try: + gui_json_path = self.getResourcePath("gui/gui_elements.json") + if os.path.exists(gui_json_path): + with open(gui_json_path, 'r', encoding='utf-8') as f: + content = f.read().strip() + if content: + gui_data = json.loads(content) + self.createGUIElement(gui_data) + else: + print("GUI配置文件为空 ") + except Exception as e: + print(f"加载GUI元素时出错: {str(e)}") + import traceback + traceback.print_exc() + + def createGUIElement(self, element_data): + try: + processed_names = set() + element_original_data = {} + + for i, gui_info in enumerate(element_data): + name = gui_info.get("name", f"gui_element_{i}") + element_original_data[name] = { + "scale": gui_info.get("scale", [1, 1, 1]), + "position": gui_info.get("position", [0, 0, 0]), + "parent_name": gui_info.get("parent_name") + } + valid_parents = set() + for gui_info in element_data: + name = gui_info.get("name", f"gui_element_{gui_info.get('index', 0)}") + valid_parents.add(name) + + for i, gui_info in enumerate(element_data): + try: + gui_type = gui_info.get("type", "unknown") + name = gui_info.get("name", f"gui_element_{i}") + position = gui_info.get("position", [0, 0, 0]) + scale = gui_info.get("scale", [1, 1, 1]) + tags = gui_info.get("tags", {}) + text = gui_info.get("text", "") + image_path = gui_info.get("image_path", "") + video_path = gui_info.get("video_path", "") + bg_image_path = gui_info.get("bg_image_path", "") + parent_name = gui_info.get("parent_name") + + if name in processed_names: + continue + + processed_names.add(name) + + absolute_position = list(position) + absolute_scale = list(scale) + + if parent_name and parent_name in element_original_data: + parent_data = element_original_data[parent_name] + parent_scale = parent_data["scale"] + + if gui_type in ["3d_text", "3d_image", "button", "label", "entry", "2d_image", + "2d_video_screen"]: + # 位置需要乘以父级缩放来得到绝对位置 + for j in range(min(len(absolute_position), len(parent_scale))): + absolute_position[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] + + # 缩放需要乘以父级缩放来得到绝对缩放 + for j in range(min(len(absolute_scale), len(parent_scale))): + absolute_scale[j] *= parent_scale[j] if len(parent_scale) > j else parent_scale[0] + + new_element = None + + if gui_type == "3d_text": + size = absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 0.5 + new_element = self.createGUI3DText( + pos=tuple(absolute_position), + text=text, + size=size + ) + elif gui_type == "button": + new_element = self.createGUIButton( + pos=tuple(absolute_position), + text=text, + size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0, + command=self.playModelAnimation + ) + elif gui_type == "label": + new_element = self.createGUILabel( + pos=tuple(absolute_position), + text=text, + size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0 + ) + elif gui_type == "entry": + new_element = self.createGUIEntry( + pos=tuple(absolute_position), + placeholder=text, + size=absolute_scale[0] if absolute_scale and len(absolute_scale) > 0 else 1.0, + command=self.onGUIEntrySubmit + ) + elif gui_type == "2d_image": + + scale_value = absolute_scale[0] + print(f"2d_image{scale_value}") + new_element = self.createGUI2DImage( + pos=tuple(absolute_position), + image_path=image_path, + size=absolute_scale + ) + elif gui_type == "2d_video_screen": + new_element = self.createGUI2DVideoScreen( + pos=tuple(absolute_position), + video_path=video_path, + size=absolute_scale + ) + elif gui_type == "video_screen": + new_element = self.createGUIVideoScreen( + pos=tuple(absolute_position), + video_path=video_path, + size=absolute_scale + ) + elif gui_type == "info_panel": + new_element = self.info_panel_manager.onCreateSampleInfoPanel() + + if "scripts" in gui_info and new_element: + self.processScripts(new_element,gui_info["scripts"]) + + except Exception as e: + print(f"重建GUI元素失败 {name}: {e}") + import traceback + traceback.print_exc() + continue + + except Exception as e: + print(f"创建GUI元素时出错: {str(e)}") + + def createGUIButton(self, pos=(0, 0, 0), text="按钮", size=0.1, command=None): + from direct.gui.DirectGui import DirectButton + + button = DirectButton( + text=text, + pos=(pos[0], pos[1], pos[2]), # 保持正确的坐标格式 + scale=size, # size 应该是数值而不是元组 + frameColor=(0.2, 0.6, 0.8, 1), + text_font=self.getChineseFont() if self.getChineseFont() else None, + rolloverSound=None, + clickSound=None, + parent=None, + command=command + ) + + def createGUI3DText(self, pos=(0, 0, 0), text="3D文本", size=0.5): + """创建3D文本GUI元素""" + try: + # 创建文本节点 + text_node = TextNode("gui_3d_text") + text_node.setText(text) + text_node.setAlign(TextNode.ACenter) + + # 设置字体(如果可用) + if self.getChineseFont(): + text_node.setFont(self.getChineseFont()) + + # 创建节点路径并添加到场景 + text_np = self.render.attachNewNode(text_node) + + # 设置位置和大小 + text_np.setPos(Vec3(pos[0], pos[1], pos[2])) + text_np.setScale(size) + + # 设置面向摄像机 + # text_np.setBillboardPointEye() + + # 设置渲染属性 + text_np.setBin("fixed", 40) + text_np.setDepthWrite(False) + + return text_np + except Exception as e: + print(f"❌ 创建3D文本失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def createGUILabel(self, pos=(0, 0, 0), text="标签", size=0.08): + from direct.gui.DirectGui import DirectLabel + label = DirectLabel( + text=text, + pos=(pos[0], pos[1], pos[2]), + scale=size, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1), + text_font=self.getChineseFont() if self.getChineseFont() else None, + text_align=TextNode.ACenter, + text_wordwrap=None, + text_mayChange=True, + parent=None + ) + + def createGUIEntry(self, pos=(0, 0, 0), placeholder="输入文本...", size=0.08, command=None): + from direct.gui.DirectGui import DirectEntry + + entry = DirectEntry( + text="", + pos=(pos[0], pos[1], pos[2]), + scale=size, + command=command, + initialText=placeholder, + numLines=1, + width=12, + focus=0, + frameColor=(0, 0, 0, 0), + text_fg=(1, 1, 1, 1), + text_font=self.getChineseFont() if self.getChineseFont() else None, + text_align=TextNode.ACenter, + text_wordwrap=None, + text_mayChange=True, + parent=None, + rolloverSound=None, + clickSound=None, + # 添加焦点管理命令 + focusInCommand=self.disableCameraControl, + focusOutCommand=self.enableCameraControl, + # 确保输入框能正确捕获所有键盘事件 + suppressKeys=True, # 这个参数很重要,它会阻止按键事件传播到其他处理器 + suppressMouse=True + ) + return entry + + def disableCameraControl(self): + """禁用相机控制""" + try: + if hasattr(self, 'controller'): + # 如果控制器有内置的禁用方法 + if hasattr(self.controller, 'disable'): + self.controller.disable() + else: + # 否则手动禁用事件监听 + self.controller.ignoreAll() # 忽略所有已注册的事件 + print("相机控制已禁用") + except Exception as e: + print(f"禁用相机控制时出错: {e}") + + def enableCameraControl(self): + """启用相机控制""" + try: + if hasattr(self, 'controller'): + # 如果控制器有内置的启用方法 + if hasattr(self.controller, 'enable'): + self.controller.enable() + else: + # 重新设置控制器 + self.controller.setup() + print("相机控制已启用") + except Exception as e: + print(f"启用相机控制时出错: {e}") + + def onGUIEntrySubmit(self, text, entry_id=None): + """GUI输入框提交事件处理""" + try: + print(f"GUI输入框提交: {entry_id} = {text}") + + # 重新启用相机控制 + self.enableCameraControl() + + # 清除输入框焦点(如果需要) + # base.win.focus() + + # 在这里添加您需要的文本处理逻辑 + # 例如保存文本、更新UI等 + + except Exception as e: + print(f"处理输入框提交时出错: {e}") + import traceback + traceback.print_exc() + + def createGUI2DImage(self, pos=(0, 0, 0), image_path=None, size=0.2): + # 添加属性检查 + if not hasattr(self, 'aspect2d'): + print("错误: aspect2d未初始化") + return None + if image_path and not os.path.isabs(image_path): + image_path = self.getResourcePath(image_path) + # 处理非均匀缩放 + if isinstance(size, (list, tuple)) and len(size) >= 2: + # 分别处理宽度和高度的缩放 + width_scale = size[0] * 0.2 + height_scale = size[2] * 0.2 + else: + # 如果只提供了一个缩放值,则使用相同值 + width_scale = size * 0.1 if isinstance(size, (int, float)) else 0.2 + height_scale = width_scale + + cm = CardMaker("gui-2d-image") + cm.setFrame(-width_scale, width_scale, -height_scale, height_scale) + #cm.setFrame(-size, size, -size, size) + + image_node = self.aspect2d.attachNewNode(cm.generate()) + image_node.setPos(pos) + image_node.setBin("fixed", 0) + image_node.setDepthWrite(False) + image_node.setDepthTest(False) + image_node.setColor(1, 1, 1, 1) + + # 设置透明度支持 + image_node.setTransparency(TransparencyAttrib.MAlpha) + + if image_path: + try: + texture = self.loader.loadTexture(image_path) + if texture: + image_node.setTexture(texture, 1) + texture.setWrapU(Texture.WM_clamp) + texture.setWrapV(Texture.WM_clamp) + texture.setMinfilter(Texture.FT_linear) + texture.setMagfilter(Texture.FT_linear) + image_node.setColor(1, 1, 1, 1) + else: + print(f"无法加载图片: {image_path}") + except Exception as e: + print(f"加载图片时出错: {e}") + return image_node + + def createGUIVideoScreen(self,pos=(0,0,0),size=1,video_path=None): + import os + from panda3d.core import TransparencyAttrib,Texture,TextureStage + + if isinstance(size,(list,tuple)) and len(size) >= 2: + width_scale = size[0] + height_scale = size[2] + else: + width_scale = size * 0.1 if isinstance(size, (int, float)) else 0.2 + height_scale = width_scale + + cm = CardMaker("gui-video-screen") + cm.setFrame(-width_scale,width_scale,-height_scale,height_scale) + + video_screen = self.render.attachNewNode(cm.generate()) + video_screen.setPos(pos) + + video_screen.setBin("fixed", 0) + + #video_screen.setTransparency(TransparencyAttrib.MAlpha) + + video_screen.setColor(1, 1, 1, 1) + + self._ensureVideoScreenMaterial(video_screen) + + if video_path: + if video_path.startswith(("http://", "https://")): + try: + success = self._loadVideoFromURLWithOpenCV3D(video_screen, video_path) + if success: + print("视频已从URL加载成功") + else: + print("从URL加载视频时出错") + except Exception as e: + print(f"从URL加载视频时出错: {e}") + elif os.path.exists(video_path): + movie_texture = self._loadMovieTexture(video_path) + if movie_texture: + video_screen.setTexture(movie_texture, 1) + else: + print(f"视频文件不存在: {video_path}") + else: + # 设置占位符纹理 + placeholder_texture = Texture() + placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) + placeholder_data = b'\x19\x19\x4c' # 深蓝色占位符 + placeholder_texture.setRamImage(placeholder_data) + video_screen.setTexture(placeholder_texture, 1) + + return video_screen + + def createGUI2DVideoScreen(self, pos=(0, 0, 0), size=0.2, video_path=None): + import os + from direct.gui.DirectGui import DirectFrame + from panda3d.core import TransparencyAttrib, Texture, TextureStage + + if isinstance(size,(list,tuple)) and len(size) >= 2: + width_scale = size[0]*0.2 + height_scale = size[2]*0.2 + else: + width_scale = size*0.1 if isinstance(size,(int,float)) else 0.2 + height_scale = width_scale + + video_screen = DirectFrame( + frameSize=(-width_scale, width_scale, -height_scale, height_scale), + frameColor=(1, 1, 1, 1), + pos=pos, + parent=None, + suppressMouse=True + ) + video_screen.setTransparency(TransparencyAttrib.MAlpha) + + placeholder_texture = Texture() + placeholder_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) + placeholder_data = b'\x19\x19\x4c' + placeholder_texture.setRamImage(placeholder_data) + + if video_path: + if video_path.startswith(("http://","https://")): + try: + success = self._loadVideoFromURLWithOpenCV(video_screen,video_path) + if success: + print("视频已从URL加载成功") + else: + print("从URL加载视频时出错") + except Exception as e: + print(f"从URL加载视频时出错: {e}") + elif os.path.exists(video_path): + movie_texture = self._loadMovieTexture(video_path) + if movie_texture: + video_screen["frameTexture"] = movie_texture + + return video_screen + + def _loadMovieTexture(self, video_path): + from panda3d.core import Texture, MovieTexture, Filename + import os + + # Convert Windows path to Panda3D compatible path format + panda_path = Filename.fromOsSpecific(video_path) + converted_path = str(panda_path) + + movie_texture = MovieTexture(converted_path) + if movie_texture.read(converted_path): + self._configureVideoTexture(movie_texture) + return movie_texture + + def _configureVideoTexture(self, texture): + from panda3d.core import Texture + + texture.setWrapU(Texture.WM_clamp) + texture.setWrapV(Texture.WM_clamp) + texture.setMinfilter(Texture.FT_linear) + texture.setMagfilter(Texture.FT_linear) + + texture.setFormat(Texture.FRgb8) + + if hasattr(texture, 'set_loop') and hasattr(texture, 'set_play_rate'): + texture.set_loop(True) + texture.set_play_rate(1.0) + + def _loadVideoFromURLWithOpenCV(self, video_screen, url): + try: + import cv2 + import threading + from panda3d.core import Texture,PNMImage + import numpy as np + import time + + def video_stream_thread(): + cap = cv2.VideoCapture(url) + + if not cap.isOpened(): + print(f"无法打开视频流: {url}") + return False + + cap.set(cv2.CAP_PROP_BUFFERSIZE,1) + + fps = cap.get(cv2.CAP_PROP_FPS) + frame_delay = 1.0 / fps if fps > 0 else 0.033 # 默认30fps + + while hasattr(self, 'video_stream_active') and self.video_stream_active: + ret, frame = cap.read() + + if not ret: + print("视频流读取失败,尝试重新连接...") + cap.release() + time.sleep(1) + cap = cv2.VideoCapture(url) + continue + + frame_height,frame_width = frame.shape[:2] + if frame_width > 256: + scale = 256.0/frame_width + new_width = int(frame_width * scale) + new_height = int(frame_height * scale) + frame = cv2.resize(frame,(new_width,new_height)) + + frame_rgb = cv2.cvtColor(frame,cv2.COLOR_BGR2RGB) + + height,width = frame_rgb.shape[:2] + + img = PNMImage(width,height,3) + img.set_maxval(255) + + for y in range(height): + for x in range(width): + r,g,b = frame_rgb[y,x] + img.setXelVal(x,y,r,g,b) + + texture = Texture("video_texture") + texture.setMagfilter(Texture.FTLinear) + texture.setMinfilter(Texture.FTLinear) + texture.setWrapU(Texture.WMClamp) + texture.setWrapV(Texture.WMClamp) + texture.load(img) + + # 在主线程中更新GUI纹理 + def update_texture(): + if hasattr(self, 'aspect2d') and not video_screen.isEmpty(): + video_screen["frameTexture"] = texture + + # 使用taskMgr在主线程中更新纹理 + if hasattr(self, 'taskMgr'): + self.taskMgr.doMethodLater(0, lambda task: update_texture() or task.done, + f"updateVideoTexture_{time.time()}") + + # 控制帧率 + time.sleep(frame_delay) + + cap.release() + print("视频流线程结束") + return True + + # 启动视频流线程 + self.video_stream_active = True + self.video_thread = threading.Thread(target=video_stream_thread, daemon=True) + self.video_thread.start() + + return True + + except Exception as e: + print(f"加载视频流失败: {e}") + import traceback + traceback.print_exc() + return False + + def _loadVideoFromURLWithOpenCV3D(self,video_screen,url): + try: + import cv2 + import threading + from panda3d.core import Texture,PNMImage + import time + + def video_stream_thread(): + cap = cv2.VideoCapture(url) + + if not cap.isOpened(): + return False + + cap.set(cv2.CAP_PROP_BUFFERSIZE,1) + + fps = cap.get(cv2.CAP_PROP_FPS) + frame_delay = 1.0 / fps if fps > 0 else 0.033 + + while hasattr(self,'video_stream_active') and self.video_stream_active: + ret,frame = cap.read() + + if not ret: + cap.release() + time.sleep(1) + cap = cv2.VideoCapture(url) + continue + + frame_height,frame_width = frame.shape[:2] + if frame_width > 256: + scale = 256.0/frame_width + new_width = int(frame_width*scale) + new_height = int(frame_height*scale) + frame = cv2.resize(frame,(new_width,new_height)) + + frame_rgb = cv2.cvtColor(frame,cv2.COLOR_BGR2RGB) + + height,width = frame_rgb.shape[:2] + + img = PNMImage(width,height,3) + img.set_maxval(255) + + for y in range(height): + for x in range(width): + r,g,b = frame_rgb[y,x] + img.setXelVal(x,y,r,g,b) + + texture = Texture("video_texture_3d") + texture.setMagfilter(Texture.FTLinear) + texture.setMinfilter(Texture.FTLinear) + texture.setWrapU(Texture.WMClamp) + texture.setWrapV(Texture.WMClamp) + texture.load(img) + + def update_texture(): + if hasattr(self,'render') and not video_screen.isEmpty(): + video_screen.setTexture(texture,1) + + if hasattr(self,'taskMgr'): + self.taskMgr.doMethodLater(0,lambda task:update_texture() or task.done,f"updateVideoTexture_{time.time()}") + + time.sleep(frame_delay) + + cap.release() + return True + self.video_stream_active = True + self.video_thread_3d = threading.Thread(target=video_stream_thread,daemon=True) + self.video_thread_3d.start() + + return True + except Exception as e: + print(f"加载3D视频流失败: {e}") + import traceback + traceback.print_exc() + return False + + def _ensureVideoScreenMaterial(self, video_screen): + """确保视频屏幕有正确的材质设置""" + try: + from panda3d.core import Material, LColor + + if not video_screen.hasMaterial(): + material = Material(f"video-material-{video_screen.getName()}") + material.setBaseColor(LColor(1, 1, 1, 1)) + material.setDiffuse(LColor(1, 1, 1, 1)) + material.setAmbient(LColor(1, 1, 1, 1)) # 确保环境光为白色 + material.setEmission(LColor(0, 0, 0, 1)) + material.setSpecular(LColor(0, 0, 0, 1)) + material.setShininess(0) + video_screen.setMaterial(material, 1) + else: + # 更新现有材质确保正确设置 + material = video_screen.getMaterial() + material.setBaseColor(LColor(1, 1, 1, 1)) + material.setAmbient(LColor(1, 1, 1, 1)) # 确保环境光为白色 + video_screen.setMaterial(material, 1) + + except Exception as e: + print(f"⚠️ 设置视频屏幕材质时出错: {e}") + + def playModelAnimation(self): + """播放场景中所有 .glb 模型的动画(仅转换有动画的模型为 Actor)""" + try: + glb_models = self.render.findAllMatches("**/*.glb*") + + # 修复过滤逻辑,确保正确排除碰撞体节点 + filtered_models = [] + for model in glb_models: + model_name = model.getName().lower() + # 排除碰撞体节点 + if ("collision_" not in model_name and + "modelcollision_" not in model_name and + not model_name.endswith("_bounds")): + filtered_models.append(model) + + if not filtered_models: + print("⚠️ 场景中没有找到 .glb 模型") + return + + print(f"找到 {len(filtered_models)} 个 glb 模型") + + self.actors = [] # 存储所有 Actor,避免被垃圾回收 + + for model in filtered_models: + print(f"正在处理模型: {model.getName()}") + + actor = None + + # 尝试把模型当作Actor加载,判断是否有动画 + try: + # 在创建Actor之前先检查节点是否可能包含动画 + if not self._isValidAnimationNode(model): + print(f"⚠️ {model.getName()} 不是有效的动画节点,跳过") + continue + + test_actor = Actor(model) + anim_names = test_actor.getAnimNames() + except Exception as e: + print(f"⚠️ {model.getName()} 无法作为Actor加载: {str(e)}") + anim_names = [] + + if anim_names: # ✅ 只有有动画才转为Actor + if not isinstance(model, Actor): + model_parent = model.getParent() + model_pos = model.getPos() + model_hpr = model.getHpr() + model_scale = model.getScale() + + actor = Actor(model) + actor.reparentTo(model_parent) + actor.setPos(model_pos) + actor.setHpr(model_hpr) + actor.setScale(model_scale) + + model.detachNode() + else: + actor = model + + # 播放动画 + actor.show() + self.actors.append(actor) + + print(f"✓ {actor.getName()} 可用动画: {anim_names}") + first_anim = anim_names[0] + + actor.stop() + actor.setPlayRate(1.0, first_anim) + actor.loop(first_anim) + print(f"✓ {actor.getName()} 正在播放动画: {first_anim}") + + actor.update() + + else: # 没有动画 + print(f"⚠️ {model.getName()} 没有动画,不转为Actor") + + except Exception as e: + print(f"播放模型动画时出错: {str(e)}") + import traceback + traceback.print_exc() + + def _isValidAnimationNode(self, nodePath): + """检查节点是否可能是有效的动画节点""" + # 排除明显不是动画节点的节点 + name = nodePath.getName().lower() + if ("collision_" in name or + "modelcollision_" in name or + "bound" in name or + name.endswith("_bounds")): + return False + + # 检查节点是否有网格数据(简单判断) + from panda3d.core import GeomNode + geom_nodes = nodePath.findAllMatches("**/+GeomNode") + if not geom_nodes: + return False + + return True + + def focusOnWomenModel(self): + """定位并聚焦到Women模型""" + try: + women_models = self.render.findAllMatches("**/Women_1.glb*") + if women_models: + model = women_models[0] + # 确保模型可见 + model.show() + + # 将模型放置在原点附近 + model.setPos(0, 0, 0) + model.setScale(1.0) + + # 确保是Actor的话设置动画 + if isinstance(model, Actor): + anim_names = model.getAnimNames() + if anim_names: + model.loop(anim_names[0]) + model.setPlayRate(1.0, anim_names[0]) + print(f"为模型设置动画: {anim_names[0]}") + + print(f"已定位模型: {model.getName()}") + else: + print("未找到Women模型") + except Exception as e: + print(f"定位模型时出错: {str(e)}") + + def processScripts(self, element, script_info_list): + """处理元素上挂载的脚本 - 使用新的脚本系统""" + try: + print(f"正在为元素 {element.getName()} 挂载脚本") + print(f"可用脚本列表: {self.script_manager.get_available_scripts()}") + + if not hasattr(self,'script_manager'): + print("脚本管理器未初始化") + return + + for script_info in script_info_list: + script_name = script_info["name"] + script_file = script_info.get("file", "") + + if script_name: + script_component = self.script_manager.add_script_to_object(element,script_name) + + if script_component: + print(f"✓ 脚本 {script_name} 已挂载到元素 {element.getName()}") + else: + print(f"⚠️ 脚本 {script_name} 挂载失败") + # 列出可用脚本帮助调试 + available_scripts = self.script_manager.get_available_scripts() + print(f"当前可用脚本: {available_scripts}") + else: + print(f"⚠️ 脚本信息不完整: {script_info}") + + # # 从文件路径中提取脚本类名 + # if script_file: + # # 获取脚本文件名(不含路径和扩展名) + # script_filename = os.path.basename(script_file) + # script_class_name = os.path.splitext(script_filename)[0] + # print(f"尝试挂载脚本: {script_class_name} (来自文件: {script_file})") + # + # # 使用脚本管理器为元素添加脚本 + # script_component = self.script_manager.add_script_to_object(element, script_class_name) + # + # if script_component: + # print(f"✓ 脚本 {script_class_name} 已挂载到元素 {element.getName()}") + # else: + # print(f"⚠️ 脚本 {script_class_name} 挂载失败") + # # 列出可用脚本帮助调试 + # available_scripts = self.script_manager.get_available_scripts() + # print(f"当前可用脚本: {available_scripts}") + # else: + # print(f"⚠️ 脚本信息不完整: {script_name}") + + except Exception as e: + print(f"挂载脚本到元素 {element.getName()} 失败: {str(e)}") + import traceback + traceback.print_exc() + + def checkDoubleClick(self, nodePath): + """检查是否为双击,返回布尔值 - 改进版本""" + try: + import time + current_time = time.time() + + # 必须是同一节点且在时间阈值内 + is_double_click = (self._last_clicked_node is not None and + self._last_clicked_node == nodePath and + nodePath is not None and + current_time - self._last_click_time < self._double_click_threshold) + + if is_double_click: + # 双击成功,重置状态 + self._last_click_time = 0 + self._last_clicked_node = None + print(f"✓ 检测到双击: {nodePath.getName()}") + return True + else: + # 单击,更新状态 + self._last_click_time = current_time + self._last_clicked_node = nodePath + return False + + except Exception as e: + print(f"双击检测失败: {e}") + return False + + def focusCameraOnNode(self, nodePath): + """带动画效果的聚焦到节点方法""" + try: + if not nodePath or nodePath.isEmpty(): + print("无效的节点") + return + + minPoint = Point3() + maxPoint = Point3() + + if not nodePath.calcTightBounds(minPoint, maxPoint,self.render): + print("无法计算选中节点的边界框,使用节点为位置作为替代方案") + node_pos = nodePath.getPos(self.render) + optimal_distance = 10.0 + current_cam_pos = self.cam.getPos() + view_direction = node_pos - current_cam_pos + if view_direction.length()<0.001: + view_direction = Vec3(5,-5,2) + view_direction.normalize() + target_cam_pos = node_pos-(view_direction * optimal_distance) + + temp_node = self.render.attachNewNode("temp_lookat_target") + temp_node.setPos(node_pos) + dummy_cam = self.render.attachNewNode("dummy_camera") + dummy_cam.setPos(target_cam_pos) + dummy_cam.lookAt(temp_node) + target_cam_hpr = Vec3(dummy_cam.getHpr()) + + temp_node.removeNode() + dummy_cam.removeNode() + + current_cam_pos = Point3(self.cam.getPos()) + current_cam_hpr = Vec3(self.cam.getHpr()) + self._startCameraFocusAnimation(current_cam_pos,target_cam_pos,current_cam_hpr,target_cam_hpr) + print(f"开始聚焦到节点(使用位置): {nodePath.getName()}") + + return + + center = Point3( + (minPoint.x + maxPoint.x)*0.5, + (minPoint.y+maxPoint.y)*0.5, + (minPoint.z+maxPoint.z)*0.5 + ) + + size = (maxPoint - minPoint).length() + if size < 0.01: + size = 1.0 + + current_cam_pos = Point3(self.cam.getPos()) + current_cam_hpr = Vec3(self.cam.getHpr()) + + view_direction = current_cam_pos - center + if view_direction.length() < 0.001: + view_direction = Vec3(5,-5,2) + + view_direction.normalize() + + optimal_distance = max(size * 2.5,1.0) + + target_cam_pos = center + (view_direction * optimal_distance) + + temp_node = self.render.attachNewNode("temp_lookat_target") + temp_node.setPos(center) + + dummy_cam = self.render.attachNewNode("dummy_camera") + dummy_cam.setPos(target_cam_pos) + dummy_cam.lookAt(temp_node) + target_cam_hpr = Vec3(dummy_cam.getHpr()) + + temp_node.removeNode() + dummy_cam.removeNode() + + self._startCameraFocusAnimation(current_cam_pos,target_cam_pos, + current_cam_hpr,target_cam_hpr) + + print(f"开始聚焦到节点: {nodePath.getName()}") + return True + + except Exception as e: + print(f"聚焦相机失败: {str(e)}") + import traceback + traceback.print_exc() + + def _startCameraFocusAnimation(self, start_pos, end_pos, start_hpr, end_hpr): + """启动相机聚焦动画 - 改进版本""" + try: + class CameraFocusData: + def __init__(self, start_pos, end_pos, start_hpr, end_hpr): + self.start_pos = Point3(start_pos) + self.end_pos = Point3(end_pos) + self.start_hpr = Vec3(start_hpr) + self.end_hpr = Vec3(end_hpr) + self.elapsed_time = 0.0 + self.duration = 0.5 # 增加动画时间使移动更平滑 + + self._camera_focus_data = CameraFocusData(start_pos, end_pos, start_hpr, end_hpr) + + from direct.task.TaskManagerGlobal import taskMgr + taskMgr.remove("cameraFocusTask") + + taskMgr.add(self._cameraFocusTask, "cameraFocusTask") + print(f"开始相机聚焦动画: {start_pos} -> {end_pos}") + + except Exception as e: + print(f"相机聚焦动画启动失败: {str(e)}") + + def _normalizeAngle(self, angle): + """规范化角度到-180到180度之间""" + while angle > 180: + angle -= 360 + while angle < -180: + angle += 360 + return angle + + def _cameraFocusTask(self, task): + """摄像机聚焦动画任务""" + try: + if not hasattr(self, '_camera_focus_data'): + return task.done + + data = self._camera_focus_data + from direct.showbase.ShowBaseGlobal import globalClock + dt = globalClock.getDt() + + if dt <= 0: + dt = 0.016 + + data.elapsed_time += dt + t = min(1.0, data.elapsed_time / data.duration) + + # 使用平滑插值 + smooth_t = t * t * (3 - 2 * t) + + # 插值位置和朝向 + current_pos = data.start_pos + (data.end_pos - data.start_pos) * smooth_t + current_hpr = data.start_hpr + (data.end_hpr - data.start_hpr) * smooth_t + + self.cam.setPos(current_pos) + self.cam.setHpr(current_hpr) + + if t >= 1.0: + self.cam.setPos(data.end_pos) + self.cam.setHpr(data.end_hpr) + + print(f"聚焦完成: 位置 {data.end_pos}, 朝向 {data.end_hpr}") + + if hasattr(self, '_camera_focus_data'): + delattr(self, '_camera_focus_data') + + return task.done + + return task.cont + + except Exception as e: + print(f"摄像机聚焦动画任务失败: {e}") + import traceback + traceback.print_exc() + return task.done + + def setupMouseClickHandler(self): + """设置鼠标点击处理器""" + try: + # 设置鼠标监听 + self.accept("mouse1", self.onMouseClick) + + # 启用鼠标观察器 + if not hasattr(self, 'mouseWatcherNode'): + from panda3d.core import MouseWatcher + self.mouseWatcherNode = MouseWatcher() + + except Exception as e: + print(f"设置鼠标点击处理器失败: {e}") + + def onMouseClick(self): + """处理鼠标左键点击""" + try: + if not hasattr(self, 'mouseWatcherNode') or not self.mouseWatcherNode.hasMouse(): + return + + # 获取鼠标位置 + mouse_pos = self.mouseWatcherNode.getMouse() + + # 创建射线进行点击检测 + from panda3d.core import CollisionRay, CollisionNode, CollisionHandlerQueue, BitMask32 + + # 创建射线 + ray = CollisionRay() + ray.setFromLens(self.camNode, mouse_pos.x, mouse_pos.y) + + # 创建碰撞节点 + ray_node = CollisionNode('mouseRay') + ray_node.addSolid(ray) + ray_node.setFromCollideMask(BitMask32.allOn()) + + # 附加到相机 + ray_np = self.camera.attachNewNode(ray_node) + + # 创建碰撞队列 + handler = CollisionHandlerQueue() + + # 确保碰撞遍历器存在 + if not hasattr(self, 'cTrav'): + self.cTrav = CollisionTraverser() + + self.cTrav.addCollider(ray_np, handler) + self.cTrav.traverse(self.render) + + # 检查碰撞结果 + target_node = None + if handler.getNumEntries() > 0: + # 按距离排序 + handler.sortEntries() + + # 遍历所有碰撞结果,找到最近的有效节点 + for i in range(handler.getNumEntries()): + entry = handler.getEntry(i) + clicked_node = entry.getIntoNodePath() + + # 向上遍历找到可选择的父节点 + current_node = clicked_node + found_valid_node = False + + while current_node and not current_node.isEmpty(): + # 检查节点是否应该被选择 + if (current_node.hasTag("is_scene_element") and + not current_node.hasTag("is_gizmo") and + current_node.getName() not in ["render", "camera", "ambient_light", + "directional_light", "mouseRay"] and + not current_node.getName().startswith("collision_") and + "ground" not in current_node.getName().lower()): + target_node = current_node + found_valid_node = True + break + + # 如果当前节点是碰撞体,获取其父节点作为目标 + if current_node.getName().startswith("collision_"): + parent = current_node.getParent() + if (parent.hasTag("is_scene_element") and + not parent.hasTag("is_gizmo") and + parent.getName() not in ["render", "camera", "ambient_light", + "directional_light"] and + "ground" not in parent.getName().lower()): + target_node = parent + found_valid_node = True + break + + current_node = current_node.getParent() + if current_node.getName() == "render": + break + + if found_valid_node: + break + + # 如果找到了目标节点,则进行双击检测 + if target_node: + print(f"找到可选择节点: {target_node.getName()}") + print(f"节点位置: {target_node.getPos()}") + print(f"节点边界: {target_node.getBounds()}") + # 检查是否为双击 + if self.checkDoubleClick(target_node): + print(f"双击节点: {target_node.getName()}") + self.focusCameraOnNode(target_node) + else: + print(f"单击节点: {target_node.getName()}") + else: + print("未找到有效的目标节点") + + # 清理碰撞器 + ray_np.removeNode() + + except Exception as e: + print(f"处理鼠标点击失败: {e}") + import traceback + traceback.print_exc()\ + + def createFrameRateDisplay(self): + from direct.gui.DirectGui import DirectLabel + from panda3d.core import TextNode + + self.fps_label = DirectLabel( + text="FPS:0", + pos=(-1.8,0,0.9), + scale=0.05, + text_fg=(1,1,1,1), + text_align=TextNode.ALeft, + frameSize=(-0.1,0.3,-0.05,0.05), + frameColor=(0,0,0,0.5), + text_font=self.getChineseFont() if self.getChineseFont() else None, + #parent=self.a2dTopLeft + ) + + taskMgr.add(self.updateFrameRate,"updateFrameRateTask") + + def updateFrameRate(self,task): + try: + fps = globalClock.getAverageFrameRate() + + if self.fps_label: + self.fps_label.setText(f"FPS:{fps:.1f}") + except Exception as e: + print(f"更新帧率失败: {e}") + return task.cont + + +# 在 main.py 的最后部分,修改为: +if __name__ == "__main__": + try: + app = MainApp() + if hasattr(app, 'run'): + app.run() + else: + print("应用程序初始化失败") + except Exception as e: + print(f"应用程序启动失败: {str(e)}") + import traceback + traceback.print_exc() + + diff --git a/tools/open_source_rate.py b/tools/open_source_rate.py index cf389707..e8aaf7bb 100644 --- a/tools/open_source_rate.py +++ b/tools/open_source_rate.py @@ -1,95 +1,95 @@ -import os -from pathlib import Path - -THIRD_PARTY_DIR_NAMES = { - 'node_modules', 'vendor', 'third_party', 'third-party', 'extern', 'external', 'deps', - 'Cesium', 'Cesium-1.132', 'dist', 'build' -} - -CODE_EXTENSIONS = { - '.py', '.js', '.ts', '.tsx', '.jsx', '.css', '.scss', '.html', '.c', '.h', '.cpp', '.hpp', - '.cc', '.hh', '.m', '.mm', '.java', '.go', '.rs', '.cs', '.vue', '.svelte' -} - -SKIP_DIR_NAMES = { - '.git', '.hg', '.svn', '__pycache__', '.mypy_cache', '.pytest_cache', '.idea', '.vscode', - '.venv', 'venv', 'env', '.tox', '.cache', 'Resources', 'icons', 'tex', 'terminal' -} - -MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024 # 2 MiB - - -def is_third_party(path: Path) -> bool: - for part in path.parts: - # normalize case on Windows - name = part.lower() - if name in {n.lower() for n in THIRD_PARTY_DIR_NAMES}: - return True - return False - - -def should_skip_dir(dirname: str) -> bool: - return dirname.lower() in {n.lower() for n in SKIP_DIR_NAMES} - - -def is_code_file(path: Path) -> bool: - return path.suffix.lower() in CODE_EXTENSIONS - - -def count_lines(path: Path) -> int: - try: - if path.stat().st_size > MAX_FILE_SIZE_BYTES: - return 0 - # Try text read with universal newlines, fallback to binary - try: - with path.open('r', encoding='utf-8', errors='ignore') as f: - return sum(1 for _ in f) - except Exception: - with path.open('rb') as f: - return f.read().count(b'\n') - except Exception: - return 0 - - -def main(root: Path) -> None: - third_party_loc = 0 - first_party_loc = 0 - third_party_files = 0 - first_party_files = 0 - - for dirpath, dirnames, filenames in os.walk(root): - # prune directories - dirnames[:] = [d for d in dirnames if not should_skip_dir(d)] - - current = Path(dirpath) - current_is_third = is_third_party(current) - - for fn in filenames: - p = current / fn - if not is_code_file(p): - continue - loc = count_lines(p) - if current_is_third: - third_party_loc += loc - third_party_files += 1 - else: - first_party_loc += loc - first_party_files += 1 - - total_loc = first_party_loc + third_party_loc - open_source_rate = (third_party_loc / total_loc) * 100 if total_loc else 0.0 - - print('Open-Source Rate (by LOC): {:.2f}%'.format(open_source_rate)) - print('\nBreakdown:') - print(' First-party LOC : {} ({} files)'.format(first_party_loc, first_party_files)) - print(' Third-party LOC : {} ({} files)'.format(third_party_loc, third_party_files)) - print(' Total LOC : {}'.format(total_loc)) - print('\nNotes:') - print(' - Third-party directories are heuristically detected by common names: {}'.format(', '.join(sorted(THIRD_PARTY_DIR_NAMES)))) - print(' - Static asset directories are skipped: {}'.format(', '.join(sorted(SKIP_DIR_NAMES)))) - print(' - Counted code extensions: {}'.format(', '.join(sorted(CODE_EXTENSIONS)))) - - -if __name__ == '__main__': - main(Path('.').resolve()) - +import os +from pathlib import Path + +THIRD_PARTY_DIR_NAMES = { + 'node_modules', 'vendor', 'third_party', 'third-party', 'extern', 'external', 'deps', + 'Cesium', 'Cesium-1.132', 'dist', 'build' +} + +CODE_EXTENSIONS = { + '.py', '.js', '.ts', '.tsx', '.jsx', '.css', '.scss', '.html', '.c', '.h', '.cpp', '.hpp', + '.cc', '.hh', '.m', '.mm', '.java', '.go', '.rs', '.cs', '.vue', '.svelte' +} + +SKIP_DIR_NAMES = { + '.git', '.hg', '.svn', '__pycache__', '.mypy_cache', '.pytest_cache', '.idea', '.vscode', + '.venv', 'venv', 'env', '.tox', '.cache', 'Resources', 'icons', 'tex', 'terminal' +} + +MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024 # 2 MiB + + +def is_third_party(path: Path) -> bool: + for part in path.parts: + # normalize case on Windows + name = part.lower() + if name in {n.lower() for n in THIRD_PARTY_DIR_NAMES}: + return True + return False + + +def should_skip_dir(dirname: str) -> bool: + return dirname.lower() in {n.lower() for n in SKIP_DIR_NAMES} + + +def is_code_file(path: Path) -> bool: + return path.suffix.lower() in CODE_EXTENSIONS + + +def count_lines(path: Path) -> int: + try: + if path.stat().st_size > MAX_FILE_SIZE_BYTES: + return 0 + # Try text read with universal newlines, fallback to binary + try: + with path.open('r', encoding='utf-8', errors='ignore') as f: + return sum(1 for _ in f) + except Exception: + with path.open('rb') as f: + return f.read().count(b'\n') + except Exception: + return 0 + + +def main(root: Path) -> None: + third_party_loc = 0 + first_party_loc = 0 + third_party_files = 0 + first_party_files = 0 + + for dirpath, dirnames, filenames in os.walk(root): + # prune directories + dirnames[:] = [d for d in dirnames if not should_skip_dir(d)] + + current = Path(dirpath) + current_is_third = is_third_party(current) + + for fn in filenames: + p = current / fn + if not is_code_file(p): + continue + loc = count_lines(p) + if current_is_third: + third_party_loc += loc + third_party_files += 1 + else: + first_party_loc += loc + first_party_files += 1 + + total_loc = first_party_loc + third_party_loc + open_source_rate = (third_party_loc / total_loc) * 100 if total_loc else 0.0 + + print('Open-Source Rate (by LOC): {:.2f}%'.format(open_source_rate)) + print('\nBreakdown:') + print(' First-party LOC : {} ({} files)'.format(first_party_loc, first_party_files)) + print(' Third-party LOC : {} ({} files)'.format(third_party_loc, third_party_files)) + print(' Total LOC : {}'.format(total_loc)) + print('\nNotes:') + print(' - Third-party directories are heuristically detected by common names: {}'.format(', '.join(sorted(THIRD_PARTY_DIR_NAMES)))) + print(' - Static asset directories are skipped: {}'.format(', '.join(sorted(SKIP_DIR_NAMES)))) + print(' - Counted code extensions: {}'.format(', '.join(sorted(CODE_EXTENSIONS)))) + + +if __name__ == '__main__': + main(Path('.').resolve()) + diff --git a/ui/__init__.py b/ui/__init__.py index 1fc1ae06..938fd368 100644 --- a/ui/__init__.py +++ b/ui/__init__.py @@ -1,18 +1,18 @@ -""" -UI模块 - -包含所有用户界面相关的功能: -- widgets.py: 自定义Qt部件 -- main_window.py: 主窗口设置 -""" - -from .widgets import CustomMeta3DWidget, CustomFileView, CustomTreeWidget -from .main_window import MainWindow, setup_main_window - -__all__ = [ - 'CustomMeta3DWidget', - 'CustomFileView', - 'CustomTreeWidget', - 'MainWindow', - 'setup_main_window' +""" +UI模块 + +包含所有用户界面相关的功能: +- widgets.py: 自定义Qt部件 +- main_window.py: 主窗口设置 +""" + +from .widgets import CustomMeta3DWidget, CustomFileView, CustomTreeWidget +from .main_window import MainWindow, setup_main_window + +__all__ = [ + 'CustomMeta3DWidget', + 'CustomFileView', + 'CustomTreeWidget', + 'MainWindow', + 'setup_main_window' ] \ No newline at end of file diff --git a/ui/assembly_disassembly_config.py b/ui/assembly_disassembly_config.py index e663decc..5eab3758 100644 --- a/ui/assembly_disassembly_config.py +++ b/ui/assembly_disassembly_config.py @@ -1,944 +1,944 @@ -# -*- coding: utf-8 -*- -""" -拆装交互配置界面 -实现模型的分步拆装交互功能配置 -""" - -from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, - QWidget, QLabel, QListWidget, QListWidgetItem, - QPushButton, QComboBox, QSpinBox, QLineEdit, - QTextEdit, QGroupBox, QGridLayout, QCheckBox, - QSplitter, QTreeWidget, QTreeWidgetItem, - QMessageBox, QFileDialog, QScrollArea, - QFrame, QDoubleSpinBox, QSlider, QProgressBar) -from PyQt5.QtCore import Qt, pyqtSignal, QTimer -from PyQt5.QtGui import QIcon, QPixmap, QFont -import os -import json - - -class AssemblyDisassemblyConfigDialog(QDialog): - """拆装配置主对话框""" - - def __init__(self, parent=None, world=None): - super().__init__(parent) - self.world = world - self.config_data = { - 'models': [], # 参与拆装的模型列表 - 'steps': [], # 拆装步骤配置 - 'tools': [], # 使用的工具列表 - 'settings': {} # 全局设置 - } - - self.setupUI() - self.loadSceneModels() - - def setupUI(self): - """设置界面""" - self.setWindowTitle("拆装交互配置") - self.setModal(False) # 设置为非模态对话框 - self.resize(1200, 800) - - # 主布局 - main_layout = QVBoxLayout(self) - - # 创建分割器 - splitter = QSplitter(Qt.Horizontal) - main_layout.addWidget(splitter) - - # 左侧面板 - 模型和工具选择 - left_panel = self.createLeftPanel() - splitter.addWidget(left_panel) - - # 右侧面板 - 步骤配置 - right_panel = self.createRightPanel() - splitter.addWidget(right_panel) - - # 设置分割器比例 - splitter.setSizes([400, 800]) - - # 底部按钮 - button_layout = QHBoxLayout() - - self.save_button = QPushButton("保存配置") - self.load_button = QPushButton("加载配置") - self.preview_button = QPushButton("预览效果") - self.apply_button = QPushButton("应用配置") - - button_layout.addWidget(self.save_button) - button_layout.addWidget(self.load_button) - button_layout.addStretch() - button_layout.addWidget(self.preview_button) - button_layout.addWidget(self.apply_button) - - main_layout.addLayout(button_layout) - - # 连接信号 - self.connectSignals() - - def createLeftPanel(self): - """创建左侧面板""" - left_widget = QWidget() - left_layout = QVBoxLayout(left_widget) - - # 场景模型选择区域 - models_group = QGroupBox("场景模型") - models_layout = QVBoxLayout(models_group) - - # 模型搜索框 - search_layout = QHBoxLayout() - search_label = QLabel("搜索:") - self.model_search_edit = QLineEdit() - self.model_search_edit.setPlaceholderText("输入模型名称搜索...") - search_layout.addWidget(search_label) - search_layout.addWidget(self.model_search_edit) - models_layout.addLayout(search_layout) - - # 场景模型列表 - self.scene_models_list = QTreeWidget() - self.scene_models_list.setHeaderLabel("场景中的模型") - self.scene_models_list.setSelectionMode(QTreeWidget.ExtendedSelection) - models_layout.addWidget(self.scene_models_list) - - # 选中模型操作按钮 - model_buttons_layout = QHBoxLayout() - self.add_to_config_button = QPushButton("添加到配置") - self.remove_from_config_button = QPushButton("从配置移除") - model_buttons_layout.addWidget(self.add_to_config_button) - model_buttons_layout.addWidget(self.remove_from_config_button) - models_layout.addLayout(model_buttons_layout) - - left_layout.addWidget(models_group) - - # 已配置模型列表 - config_models_group = QGroupBox("已配置模型") - config_models_layout = QVBoxLayout(config_models_group) - - self.config_models_list = QListWidget() - config_models_layout.addWidget(self.config_models_list) - - left_layout.addWidget(config_models_group) - - # 工具配置区域 - tools_group = QGroupBox("工具配置") - tools_layout = QVBoxLayout(tools_group) - - # 工具添加 - tool_add_layout = QHBoxLayout() - self.tool_name_edit = QLineEdit() - self.tool_name_edit.setPlaceholderText("工具名称") - self.add_tool_button = QPushButton("添加工具") - tool_add_layout.addWidget(self.tool_name_edit) - tool_add_layout.addWidget(self.add_tool_button) - tools_layout.addLayout(tool_add_layout) - - # 工具列表 - self.tools_list = QListWidget() - tools_layout.addWidget(self.tools_list) - - # 工具操作按钮 - tool_buttons_layout = QHBoxLayout() - self.edit_tool_button = QPushButton("编辑") - self.remove_tool_button = QPushButton("删除") - tool_buttons_layout.addWidget(self.edit_tool_button) - tool_buttons_layout.addWidget(self.remove_tool_button) - tools_layout.addLayout(tool_buttons_layout) - - left_layout.addWidget(tools_group) - - return left_widget - - def createRightPanel(self): - """创建右侧面板""" - right_widget = QWidget() - right_layout = QVBoxLayout(right_widget) - - # 步骤配置标签页 - self.tab_widget = QTabWidget() - - # 步骤列表标签页 - self.steps_tab = self.createStepsTab() - self.tab_widget.addTab(self.steps_tab, "步骤配置") - - # 全局设置标签页 - self.settings_tab = self.createSettingsTab() - self.tab_widget.addTab(self.settings_tab, "全局设置") - - right_layout.addWidget(self.tab_widget) - - return right_widget - - def createStepsTab(self): - """创建步骤配置标签页""" - steps_widget = QWidget() - steps_layout = QHBoxLayout(steps_widget) - - # 左侧步骤列表 - steps_list_layout = QVBoxLayout() - - # 步骤列表标题和操作按钮 - steps_header_layout = QHBoxLayout() - steps_label = QLabel("拆装步骤") - steps_label.setFont(QFont("", 12, QFont.Bold)) - self.add_step_button = QPushButton("添加步骤") - self.remove_step_button = QPushButton("删除步骤") - steps_header_layout.addWidget(steps_label) - steps_header_layout.addStretch() - steps_header_layout.addWidget(self.add_step_button) - steps_header_layout.addWidget(self.remove_step_button) - steps_list_layout.addLayout(steps_header_layout) - - # 步骤列表 - self.steps_list = QListWidget() - steps_list_layout.addWidget(self.steps_list) - - # 步骤顺序调整按钮 - step_order_layout = QHBoxLayout() - self.move_step_up_button = QPushButton("上移") - self.move_step_down_button = QPushButton("下移") - step_order_layout.addWidget(self.move_step_up_button) - step_order_layout.addWidget(self.move_step_down_button) - steps_list_layout.addLayout(step_order_layout) - - # 右侧步骤详细配置 - self.step_config_widget = self.createStepConfigWidget() - - # 添加到布局 - steps_layout.addLayout(steps_list_layout, 1) - steps_layout.addWidget(self.step_config_widget, 2) - - return steps_widget - - def createStepConfigWidget(self): - """创建步骤详细配置部件""" - config_widget = QWidget() - config_layout = QVBoxLayout(config_widget) - - # 滚动区域 - scroll_area = QScrollArea() - scroll_area.setWidgetResizable(True) - scroll_content = QWidget() - scroll_layout = QVBoxLayout(scroll_content) - - # 步骤基本信息 - basic_info_group = QGroupBox("基本信息") - basic_info_layout = QGridLayout(basic_info_group) - - # 步骤名称 - basic_info_layout.addWidget(QLabel("步骤名称:"), 0, 0) - self.step_name_edit = QLineEdit() - basic_info_layout.addWidget(self.step_name_edit, 0, 1) - - # 步骤描述 - basic_info_layout.addWidget(QLabel("步骤描述:"), 1, 0) - self.step_description_edit = QTextEdit() - self.step_description_edit.setMaximumHeight(80) - basic_info_layout.addWidget(self.step_description_edit, 1, 1) - - # 步骤类型 - basic_info_layout.addWidget(QLabel("步骤类型:"), 2, 0) - self.step_type_combo = QComboBox() - self.step_type_combo.addItems(["拆卸", "安装"]) - basic_info_layout.addWidget(self.step_type_combo, 2, 1) - - scroll_layout.addWidget(basic_info_group) - - # 目标模型配置 - target_model_group = QGroupBox("目标模型") - target_model_layout = QGridLayout(target_model_group) - - # 目标模型选择 - target_model_layout.addWidget(QLabel("目标模型:"), 0, 0) - self.target_model_combo = QComboBox() - target_model_layout.addWidget(self.target_model_combo, 0, 1) - - # 交互方式 - target_model_layout.addWidget(QLabel("交互方式:"), 1, 0) - self.interaction_type_combo = QComboBox() - self.interaction_type_combo.addItems(["鼠标拖拽", "点击触发"]) - target_model_layout.addWidget(self.interaction_type_combo, 1, 1) - - scroll_layout.addWidget(target_model_group) - - # 拖拽交互配置 - self.drag_config_group = QGroupBox("拖拽交互配置") - drag_config_layout = QGridLayout(self.drag_config_group) - - # 目标位置 - drag_config_layout.addWidget(QLabel("目标位置 X:"), 0, 0) - self.target_pos_x_spin = QDoubleSpinBox() - self.target_pos_x_spin.setRange(-1000, 1000) - self.target_pos_x_spin.setSingleStep(0.1) - drag_config_layout.addWidget(self.target_pos_x_spin, 0, 1) - - drag_config_layout.addWidget(QLabel("目标位置 Y:"), 1, 0) - self.target_pos_y_spin = QDoubleSpinBox() - self.target_pos_y_spin.setRange(-1000, 1000) - self.target_pos_y_spin.setSingleStep(0.1) - drag_config_layout.addWidget(self.target_pos_y_spin, 1, 1) - - drag_config_layout.addWidget(QLabel("目标位置 Z:"), 2, 0) - self.target_pos_z_spin = QDoubleSpinBox() - self.target_pos_z_spin.setRange(-1000, 1000) - self.target_pos_z_spin.setSingleStep(0.1) - drag_config_layout.addWidget(self.target_pos_z_spin, 2, 1) - - # 拖拽约束 - self.constrain_x_check = QCheckBox("约束X轴") - self.constrain_y_check = QCheckBox("约束Y轴") - self.constrain_z_check = QCheckBox("约束Z轴") - - constraint_layout = QHBoxLayout() - constraint_layout.addWidget(self.constrain_x_check) - constraint_layout.addWidget(self.constrain_y_check) - constraint_layout.addWidget(self.constrain_z_check) - - drag_config_layout.addWidget(QLabel("拖拽约束:"), 3, 0) - drag_config_layout.addLayout(constraint_layout, 3, 1) - - scroll_layout.addWidget(self.drag_config_group) - - # 点击触发配置 - self.click_config_group = QGroupBox("点击触发配置") - click_config_layout = QGridLayout(self.click_config_group) - - # 位移距离 - click_config_layout.addWidget(QLabel("位移距离 X:"), 0, 0) - self.move_distance_x_spin = QDoubleSpinBox() - self.move_distance_x_spin.setRange(-100, 100) - self.move_distance_x_spin.setSingleStep(0.1) - click_config_layout.addWidget(self.move_distance_x_spin, 0, 1) - - click_config_layout.addWidget(QLabel("位移距离 Y:"), 1, 0) - self.move_distance_y_spin = QDoubleSpinBox() - self.move_distance_y_spin.setRange(-100, 100) - self.move_distance_y_spin.setSingleStep(0.1) - click_config_layout.addWidget(self.move_distance_y_spin, 1, 1) - - click_config_layout.addWidget(QLabel("位移距离 Z:"), 2, 0) - self.move_distance_z_spin = QDoubleSpinBox() - self.move_distance_z_spin.setRange(-100, 100) - self.move_distance_z_spin.setSingleStep(0.1) - click_config_layout.addWidget(self.move_distance_z_spin, 2, 1) - - # 位移时间 - click_config_layout.addWidget(QLabel("位移时间(秒):"), 3, 0) - self.move_duration_spin = QDoubleSpinBox() - self.move_duration_spin.setRange(0.1, 10.0) - self.move_duration_spin.setSingleStep(0.1) - self.move_duration_spin.setValue(1.0) - click_config_layout.addWidget(self.move_duration_spin, 3, 1) - - # 完成后隐藏 - self.hide_after_move_check = QCheckBox("位移完成后隐藏模型") - click_config_layout.addWidget(self.hide_after_move_check, 4, 0, 1, 2) - - scroll_layout.addWidget(self.click_config_group) - - # 提示音频配置 - audio_config_group = QGroupBox("步骤提示") - audio_config_layout = QGridLayout(audio_config_group) - - # 提示音频文件 - audio_config_layout.addWidget(QLabel("提示音频:"), 0, 0) - audio_file_layout = QHBoxLayout() - self.audio_file_edit = QLineEdit() - self.browse_audio_button = QPushButton("浏览...") - audio_file_layout.addWidget(self.audio_file_edit) - audio_file_layout.addWidget(self.browse_audio_button) - audio_config_layout.addLayout(audio_file_layout, 0, 1) - - # 提示文字 - audio_config_layout.addWidget(QLabel("提示文字:"), 1, 0) - self.hint_text_edit = QTextEdit() - self.hint_text_edit.setMaximumHeight(60) - audio_config_layout.addWidget(self.hint_text_edit, 1, 1) - - scroll_layout.addWidget(audio_config_group) - - # 使用工具配置 - tool_config_group = QGroupBox("使用工具") - tool_config_layout = QGridLayout(tool_config_group) - - tool_config_layout.addWidget(QLabel("所需工具:"), 0, 0) - self.required_tool_combo = QComboBox() - self.required_tool_combo.addItem("无") - tool_config_layout.addWidget(self.required_tool_combo, 0, 1) - - scroll_layout.addWidget(tool_config_group) - - # 设置滚动区域 - scroll_area.setWidget(scroll_content) - config_layout.addWidget(scroll_area) - - # 默认隐藏点击配置 - self.click_config_group.setVisible(False) - - return config_widget - - def createSettingsTab(self): - """创建全局设置标签页""" - settings_widget = QWidget() - settings_layout = QVBoxLayout(settings_widget) - - # 全局设置组 - global_settings_group = QGroupBox("全局设置") - global_settings_layout = QGridLayout(global_settings_group) - - # 自动播放提示音 - self.auto_play_audio_check = QCheckBox("自动播放步骤提示音") - self.auto_play_audio_check.setChecked(True) - global_settings_layout.addWidget(self.auto_play_audio_check, 0, 0, 1, 2) - - # 显示步骤提示文字 - self.show_hint_text_check = QCheckBox("显示步骤提示文字") - self.show_hint_text_check.setChecked(True) - global_settings_layout.addWidget(self.show_hint_text_check, 1, 0, 1, 2) - - # 步骤完成确认 - self.require_confirmation_check = QCheckBox("步骤完成需要确认") - global_settings_layout.addWidget(self.require_confirmation_check, 2, 0, 1, 2) - - # 拖拽灵敏度 - global_settings_layout.addWidget(QLabel("拖拽灵敏度:"), 3, 0) - self.drag_sensitivity_slider = QSlider(Qt.Horizontal) - self.drag_sensitivity_slider.setRange(1, 10) - self.drag_sensitivity_slider.setValue(5) - global_settings_layout.addWidget(self.drag_sensitivity_slider, 3, 1) - - # 动画速度 - global_settings_layout.addWidget(QLabel("动画速度:"), 4, 0) - self.animation_speed_slider = QSlider(Qt.Horizontal) - self.animation_speed_slider.setRange(1, 10) - self.animation_speed_slider.setValue(5) - global_settings_layout.addWidget(self.animation_speed_slider, 4, 1) - - settings_layout.addWidget(global_settings_group) - - # 预设配置组 - presets_group = QGroupBox("预设配置") - presets_layout = QVBoxLayout(presets_group) - - # 预设列表 - self.presets_list = QListWidget() - presets_layout.addWidget(self.presets_list) - - # 预设操作按钮 - preset_buttons_layout = QHBoxLayout() - self.save_preset_button = QPushButton("保存为预设") - self.load_preset_button = QPushButton("加载预设") - self.delete_preset_button = QPushButton("删除预设") - preset_buttons_layout.addWidget(self.save_preset_button) - preset_buttons_layout.addWidget(self.load_preset_button) - preset_buttons_layout.addWidget(self.delete_preset_button) - presets_layout.addLayout(preset_buttons_layout) - - settings_layout.addWidget(presets_group) - - # 添加弹性空间 - settings_layout.addStretch() - - return settings_widget - - def connectSignals(self): - """连接信号槽""" - # 模型搜索 - self.model_search_edit.textChanged.connect(self.filterSceneModels) - - # 模型操作 - self.add_to_config_button.clicked.connect(self.addModelsToConfig) - self.remove_from_config_button.clicked.connect(self.removeModelsFromConfig) - - # 工具操作 - self.add_tool_button.clicked.connect(self.addTool) - self.edit_tool_button.clicked.connect(self.editTool) - self.remove_tool_button.clicked.connect(self.removeTool) - - # 步骤操作 - self.add_step_button.clicked.connect(self.addStep) - self.remove_step_button.clicked.connect(self.removeStep) - self.move_step_up_button.clicked.connect(self.moveStepUp) - self.move_step_down_button.clicked.connect(self.moveStepDown) - self.steps_list.itemSelectionChanged.connect(self.onStepSelectionChanged) - - # 步骤配置变化 - self.interaction_type_combo.currentTextChanged.connect(self.onInteractionTypeChanged) - self.browse_audio_button.clicked.connect(self.browseAudioFile) - - # 步骤配置实时更新 - self.step_name_edit.textChanged.connect(self.updateCurrentStep) - self.step_description_edit.textChanged.connect(self.updateCurrentStep) - self.step_type_combo.currentTextChanged.connect(self.updateCurrentStep) - self.target_model_combo.currentTextChanged.connect(self.updateCurrentStep) - - # 按钮操作 - self.save_button.clicked.connect(self.saveConfiguration) - self.load_button.clicked.connect(self.loadConfiguration) - self.preview_button.clicked.connect(self.previewConfiguration) - self.apply_button.clicked.connect(self.applyConfiguration) - - def loadSceneModels(self): - """加载场景中的模型""" - if not self.world: - return - - # 清空现有列表 - self.scene_models_list.clear() - - # 遍历场景中的所有节点 - self._addNodeToTree(self.world.render, self.scene_models_list) - - # 展开根节点 - self.scene_models_list.expandAll() - - def _addNodeToTree(self, node, parent_item): - """递归添加节点到树形控件""" - if node.getName() in ['render', 'camera', 'aspect2d', 'pixel2d']: - # 跳过系统节点 - for child in node.getChildren(): - self._addNodeToTree(child, parent_item) - return - - # 创建树形项 - if isinstance(parent_item, QTreeWidget): - item = QTreeWidgetItem(parent_item) - else: - item = QTreeWidgetItem(parent_item) - - item.setText(0, node.getName()) - item.setData(0, Qt.UserRole, node) - - # 添加子节点 - for child in node.getChildren(): - self._addNodeToTree(child, item) - - def filterSceneModels(self, text): - """过滤场景模型列表""" - # 简单的文本过滤实现 - def filterItems(item): - item_text = item.text(0).lower() - text_lower = text.lower() - - # 检查当前项是否匹配 - matches = text_lower in item_text - - # 检查子项 - child_matches = False - for i in range(item.childCount()): - child_item = item.child(i) - if filterItems(child_item): - child_matches = True - - # 显示/隐藏项 - item.setHidden(not (matches or child_matches)) - - return matches or child_matches - - # 从根项开始过滤 - for i in range(self.scene_models_list.topLevelItemCount()): - filterItems(self.scene_models_list.topLevelItem(i)) - - def addModelsToConfig(self): - """添加选中的模型到配置""" - selected_items = self.scene_models_list.selectedItems() - - for item in selected_items: - node = item.data(0, Qt.UserRole) - if node: - model_name = node.getName() - - # 检查是否已经存在 - existing_items = self.config_models_list.findItems(model_name, Qt.MatchExactly) - if not existing_items: - # 添加到配置模型列表 - list_item = QListWidgetItem(model_name) - list_item.setData(Qt.UserRole, node) - self.config_models_list.addItem(list_item) - - # 添加到配置数据 - self.config_data['models'].append({ - 'name': model_name, - 'node': node, - 'original_pos': node.getPos(), - 'original_hpr': node.getHpr() - }) - - # 更新目标模型下拉框 - self.target_model_combo.addItem(model_name) - - def removeModelsFromConfig(self): - """从配置中移除选中的模型""" - selected_items = self.config_models_list.selectedItems() - - for item in selected_items: - model_name = item.text() - - # 从列表中移除 - row = self.config_models_list.row(item) - self.config_models_list.takeItem(row) - - # 从配置数据中移除 - self.config_data['models'] = [ - model for model in self.config_data['models'] - if model['name'] != model_name - ] - - # 从目标模型下拉框中移除 - index = self.target_model_combo.findText(model_name) - if index >= 0: - self.target_model_combo.removeItem(index) - - def addTool(self): - """添加工具""" - tool_name = self.tool_name_edit.text().strip() - if not tool_name: - QMessageBox.warning(self, "警告", "请输入工具名称") - return - - # 检查是否已存在 - existing_items = self.tools_list.findItems(tool_name, Qt.MatchExactly) - if existing_items: - QMessageBox.warning(self, "警告", "工具已存在") - return - - # 添加到工具列表 - self.tools_list.addItem(tool_name) - self.required_tool_combo.addItem(tool_name) - - # 添加到配置数据 - self.config_data['tools'].append({ - 'name': tool_name, - 'description': '', - 'icon': '' - }) - - # 清空输入框 - self.tool_name_edit.clear() - - def editTool(self): - """编辑工具""" - current_item = self.tools_list.currentItem() - if not current_item: - QMessageBox.warning(self, "警告", "请选择要编辑的工具") - return - - # 这里可以添加工具编辑对话框 - QMessageBox.information(self, "提示", "工具编辑功能待实现") - - def removeTool(self): - """删除工具""" - current_item = self.tools_list.currentItem() - if not current_item: - QMessageBox.warning(self, "警告", "请选择要删除的工具") - return - - tool_name = current_item.text() - - # 从列表中移除 - row = self.tools_list.row(current_item) - self.tools_list.takeItem(row) - - # 从下拉框中移除 - index = self.required_tool_combo.findText(tool_name) - if index >= 0: - self.required_tool_combo.removeItem(index) - - # 从配置数据中移除 - self.config_data['tools'] = [ - tool for tool in self.config_data['tools'] - if tool['name'] != tool_name - ] - - def addStep(self): - """添加步骤""" - step_count = len(self.config_data['steps']) + 1 - step_name = f"步骤 {step_count}" - - # 创建新步骤数据 - step_data = { - 'name': step_name, - 'description': '', - 'type': '拆卸', - 'target_model': '', - 'interaction_type': '鼠标拖拽', - 'target_position': [0, 0, 0], - 'constraints': {'x': False, 'y': False, 'z': False}, - 'move_distance': [0, 0, 0], - 'move_duration': 1.0, - 'hide_after_move': False, - 'audio_file': '', - 'hint_text': '', - 'required_tool': '无' - } - - # 添加到配置数据 - self.config_data['steps'].append(step_data) - - # 添加到步骤列表 - list_item = QListWidgetItem(step_name) - list_item.setData(Qt.UserRole, step_data) - self.steps_list.addItem(list_item) - - # 选中新添加的步骤 - self.steps_list.setCurrentItem(list_item) - - def removeStep(self): - """删除步骤""" - current_item = self.steps_list.currentItem() - if not current_item: - QMessageBox.warning(self, "警告", "请选择要删除的步骤") - return - - # 从列表中移除 - row = self.steps_list.row(current_item) - self.steps_list.takeItem(row) - - # 从配置数据中移除 - self.config_data['steps'].pop(row) - - # 重新编号步骤 - self.renumberSteps() - - def moveStepUp(self): - """上移步骤""" - current_row = self.steps_list.currentRow() - if current_row <= 0: - return - - # 交换数据 - self.config_data['steps'][current_row], self.config_data['steps'][current_row - 1] = \ - self.config_data['steps'][current_row - 1], self.config_data['steps'][current_row] - - # 交换列表项 - current_item = self.steps_list.takeItem(current_row) - self.steps_list.insertItem(current_row - 1, current_item) - self.steps_list.setCurrentRow(current_row - 1) - - # 重新编号 - self.renumberSteps() - - def moveStepDown(self): - """下移步骤""" - current_row = self.steps_list.currentRow() - if current_row >= self.steps_list.count() - 1: - return - - # 交换数据 - self.config_data['steps'][current_row], self.config_data['steps'][current_row + 1] = \ - self.config_data['steps'][current_row + 1], self.config_data['steps'][current_row] - - # 交换列表项 - current_item = self.steps_list.takeItem(current_row) - self.steps_list.insertItem(current_row + 1, current_item) - self.steps_list.setCurrentRow(current_row + 1) - - # 重新编号 - self.renumberSteps() - - def renumberSteps(self): - """重新编号步骤""" - for i in range(self.steps_list.count()): - item = self.steps_list.item(i) - step_data = item.data(Qt.UserRole) - new_name = f"步骤 {i + 1}" - step_data['name'] = new_name - item.setText(new_name) - - def onStepSelectionChanged(self): - """步骤选择改变""" - current_item = self.steps_list.currentItem() - if not current_item: - return - - step_data = current_item.data(Qt.UserRole) - if not step_data: - return - - # 更新步骤配置界面 - self.loadStepConfiguration(step_data) - - def loadStepConfiguration(self, step_data): - """加载步骤配置到界面""" - # 基本信息 - self.step_name_edit.setText(step_data.get('name', '')) - self.step_description_edit.setPlainText(step_data.get('description', '')) - self.step_type_combo.setCurrentText(step_data.get('type', '拆卸')) - - # 目标模型 - target_model = step_data.get('target_model', '') - index = self.target_model_combo.findText(target_model) - if index >= 0: - self.target_model_combo.setCurrentIndex(index) - - # 交互方式 - interaction_type = step_data.get('interaction_type', '鼠标拖拽') - self.interaction_type_combo.setCurrentText(interaction_type) - self.onInteractionTypeChanged(interaction_type) - - # 拖拽配置 - target_pos = step_data.get('target_position', [0, 0, 0]) - self.target_pos_x_spin.setValue(target_pos[0]) - self.target_pos_y_spin.setValue(target_pos[1]) - self.target_pos_z_spin.setValue(target_pos[2]) - - constraints = step_data.get('constraints', {'x': False, 'y': False, 'z': False}) - self.constrain_x_check.setChecked(constraints.get('x', False)) - self.constrain_y_check.setChecked(constraints.get('y', False)) - self.constrain_z_check.setChecked(constraints.get('z', False)) - - # 点击触发配置 - move_distance = step_data.get('move_distance', [0, 0, 0]) - self.move_distance_x_spin.setValue(move_distance[0]) - self.move_distance_y_spin.setValue(move_distance[1]) - self.move_distance_z_spin.setValue(move_distance[2]) - - self.move_duration_spin.setValue(step_data.get('move_duration', 1.0)) - self.hide_after_move_check.setChecked(step_data.get('hide_after_move', False)) - - # 提示配置 - self.audio_file_edit.setText(step_data.get('audio_file', '')) - self.hint_text_edit.setPlainText(step_data.get('hint_text', '')) - - # 工具配置 - required_tool = step_data.get('required_tool', '无') - index = self.required_tool_combo.findText(required_tool) - if index >= 0: - self.required_tool_combo.setCurrentIndex(index) - - def onInteractionTypeChanged(self, interaction_type): - """交互方式改变""" - if interaction_type == "鼠标拖拽": - self.drag_config_group.setVisible(True) - self.click_config_group.setVisible(False) - else: # 点击触发 - self.drag_config_group.setVisible(False) - self.click_config_group.setVisible(True) - - def updateCurrentStep(self): - """更新当前步骤数据""" - current_item = self.steps_list.currentItem() - if not current_item: - return - - step_data = current_item.data(Qt.UserRole) - if not step_data: - return - - # 更新步骤数据 - step_data['name'] = self.step_name_edit.text() - step_data['description'] = self.step_description_edit.toPlainText() - step_data['type'] = self.step_type_combo.currentText() - step_data['target_model'] = self.target_model_combo.currentText() - step_data['interaction_type'] = self.interaction_type_combo.currentText() - - # 更新列表项显示 - current_item.setText(step_data['name']) - - def browseAudioFile(self): - """浏览音频文件""" - file_path, _ = QFileDialog.getOpenFileName( - self, "选择音频文件", "", - "音频文件 (*.wav *.mp3 *.ogg *.flac);;所有文件 (*)" - ) - - if file_path: - self.audio_file_edit.setText(file_path) - - def saveConfiguration(self): - """保存配置""" - file_path, _ = QFileDialog.getSaveFileName( - self, "保存拆装配置", "", - "JSON文件 (*.json);;所有文件 (*)" - ) - - if file_path: - try: - # 准备保存的数据(去除不能序列化的对象) - save_data = { - 'models': [ - { - 'name': model['name'], - 'original_pos': list(model['original_pos']), - 'original_hpr': list(model['original_hpr']) - } for model in self.config_data['models'] - ], - 'steps': self.config_data['steps'], - 'tools': self.config_data['tools'], - 'settings': { - 'auto_play_audio': self.auto_play_audio_check.isChecked(), - 'show_hint_text': self.show_hint_text_check.isChecked(), - 'require_confirmation': self.require_confirmation_check.isChecked(), - 'drag_sensitivity': self.drag_sensitivity_slider.value(), - 'animation_speed': self.animation_speed_slider.value() - } - } - - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(save_data, f, ensure_ascii=False, indent=2) - - QMessageBox.information(self, "成功", "配置保存成功") - - except Exception as e: - QMessageBox.critical(self, "错误", f"保存配置失败: {str(e)}") - - def loadConfiguration(self): - """加载配置""" - file_path, _ = QFileDialog.getOpenFileName( - self, "加载拆装配置", "", - "JSON文件 (*.json);;所有文件 (*)" - ) - - if file_path: - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - - # 加载配置数据 - self.loadConfigurationData(data) - QMessageBox.information(self, "成功", "配置加载成功") - - except Exception as e: - QMessageBox.critical(self, "错误", f"加载配置失败: {str(e)}") - - def loadConfigurationData(self, data): - """加载配置数据到界面""" - # 清空现有配置 - self.config_models_list.clear() - self.target_model_combo.clear() - self.steps_list.clear() - - # 加载模型配置 - # 注意:这里需要重新匹配场景中的模型节点 - - # 加载步骤配置 - self.config_data['steps'] = data.get('steps', []) - for step_data in self.config_data['steps']: - list_item = QListWidgetItem(step_data['name']) - list_item.setData(Qt.UserRole, step_data) - self.steps_list.addItem(list_item) - - # 加载工具配置 - self.tools_list.clear() - self.required_tool_combo.clear() - self.required_tool_combo.addItem("无") - - self.config_data['tools'] = data.get('tools', []) - for tool in self.config_data['tools']: - self.tools_list.addItem(tool['name']) - self.required_tool_combo.addItem(tool['name']) - - # 加载全局设置 - settings = data.get('settings', {}) - self.auto_play_audio_check.setChecked(settings.get('auto_play_audio', True)) - self.show_hint_text_check.setChecked(settings.get('show_hint_text', True)) - self.require_confirmation_check.setChecked(settings.get('require_confirmation', False)) - self.drag_sensitivity_slider.setValue(settings.get('drag_sensitivity', 5)) - self.animation_speed_slider.setValue(settings.get('animation_speed', 5)) - - def previewConfiguration(self): - """预览配置效果""" - QMessageBox.information(self, "提示", "预览功能待实现") - - def applyConfiguration(self): - """应用配置""" +# -*- coding: utf-8 -*- +""" +拆装交互配置界面 +实现模型的分步拆装交互功能配置 +""" + +from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, + QWidget, QLabel, QListWidget, QListWidgetItem, + QPushButton, QComboBox, QSpinBox, QLineEdit, + QTextEdit, QGroupBox, QGridLayout, QCheckBox, + QSplitter, QTreeWidget, QTreeWidgetItem, + QMessageBox, QFileDialog, QScrollArea, + QFrame, QDoubleSpinBox, QSlider, QProgressBar) +from PyQt5.QtCore import Qt, pyqtSignal, QTimer +from PyQt5.QtGui import QIcon, QPixmap, QFont +import os +import json + + +class AssemblyDisassemblyConfigDialog(QDialog): + """拆装配置主对话框""" + + def __init__(self, parent=None, world=None): + super().__init__(parent) + self.world = world + self.config_data = { + 'models': [], # 参与拆装的模型列表 + 'steps': [], # 拆装步骤配置 + 'tools': [], # 使用的工具列表 + 'settings': {} # 全局设置 + } + + self.setupUI() + self.loadSceneModels() + + def setupUI(self): + """设置界面""" + self.setWindowTitle("拆装交互配置") + self.setModal(False) # 设置为非模态对话框 + self.resize(1200, 800) + + # 主布局 + main_layout = QVBoxLayout(self) + + # 创建分割器 + splitter = QSplitter(Qt.Horizontal) + main_layout.addWidget(splitter) + + # 左侧面板 - 模型和工具选择 + left_panel = self.createLeftPanel() + splitter.addWidget(left_panel) + + # 右侧面板 - 步骤配置 + right_panel = self.createRightPanel() + splitter.addWidget(right_panel) + + # 设置分割器比例 + splitter.setSizes([400, 800]) + + # 底部按钮 + button_layout = QHBoxLayout() + + self.save_button = QPushButton("保存配置") + self.load_button = QPushButton("加载配置") + self.preview_button = QPushButton("预览效果") + self.apply_button = QPushButton("应用配置") + + button_layout.addWidget(self.save_button) + button_layout.addWidget(self.load_button) + button_layout.addStretch() + button_layout.addWidget(self.preview_button) + button_layout.addWidget(self.apply_button) + + main_layout.addLayout(button_layout) + + # 连接信号 + self.connectSignals() + + def createLeftPanel(self): + """创建左侧面板""" + left_widget = QWidget() + left_layout = QVBoxLayout(left_widget) + + # 场景模型选择区域 + models_group = QGroupBox("场景模型") + models_layout = QVBoxLayout(models_group) + + # 模型搜索框 + search_layout = QHBoxLayout() + search_label = QLabel("搜索:") + self.model_search_edit = QLineEdit() + self.model_search_edit.setPlaceholderText("输入模型名称搜索...") + search_layout.addWidget(search_label) + search_layout.addWidget(self.model_search_edit) + models_layout.addLayout(search_layout) + + # 场景模型列表 + self.scene_models_list = QTreeWidget() + self.scene_models_list.setHeaderLabel("场景中的模型") + self.scene_models_list.setSelectionMode(QTreeWidget.ExtendedSelection) + models_layout.addWidget(self.scene_models_list) + + # 选中模型操作按钮 + model_buttons_layout = QHBoxLayout() + self.add_to_config_button = QPushButton("添加到配置") + self.remove_from_config_button = QPushButton("从配置移除") + model_buttons_layout.addWidget(self.add_to_config_button) + model_buttons_layout.addWidget(self.remove_from_config_button) + models_layout.addLayout(model_buttons_layout) + + left_layout.addWidget(models_group) + + # 已配置模型列表 + config_models_group = QGroupBox("已配置模型") + config_models_layout = QVBoxLayout(config_models_group) + + self.config_models_list = QListWidget() + config_models_layout.addWidget(self.config_models_list) + + left_layout.addWidget(config_models_group) + + # 工具配置区域 + tools_group = QGroupBox("工具配置") + tools_layout = QVBoxLayout(tools_group) + + # 工具添加 + tool_add_layout = QHBoxLayout() + self.tool_name_edit = QLineEdit() + self.tool_name_edit.setPlaceholderText("工具名称") + self.add_tool_button = QPushButton("添加工具") + tool_add_layout.addWidget(self.tool_name_edit) + tool_add_layout.addWidget(self.add_tool_button) + tools_layout.addLayout(tool_add_layout) + + # 工具列表 + self.tools_list = QListWidget() + tools_layout.addWidget(self.tools_list) + + # 工具操作按钮 + tool_buttons_layout = QHBoxLayout() + self.edit_tool_button = QPushButton("编辑") + self.remove_tool_button = QPushButton("删除") + tool_buttons_layout.addWidget(self.edit_tool_button) + tool_buttons_layout.addWidget(self.remove_tool_button) + tools_layout.addLayout(tool_buttons_layout) + + left_layout.addWidget(tools_group) + + return left_widget + + def createRightPanel(self): + """创建右侧面板""" + right_widget = QWidget() + right_layout = QVBoxLayout(right_widget) + + # 步骤配置标签页 + self.tab_widget = QTabWidget() + + # 步骤列表标签页 + self.steps_tab = self.createStepsTab() + self.tab_widget.addTab(self.steps_tab, "步骤配置") + + # 全局设置标签页 + self.settings_tab = self.createSettingsTab() + self.tab_widget.addTab(self.settings_tab, "全局设置") + + right_layout.addWidget(self.tab_widget) + + return right_widget + + def createStepsTab(self): + """创建步骤配置标签页""" + steps_widget = QWidget() + steps_layout = QHBoxLayout(steps_widget) + + # 左侧步骤列表 + steps_list_layout = QVBoxLayout() + + # 步骤列表标题和操作按钮 + steps_header_layout = QHBoxLayout() + steps_label = QLabel("拆装步骤") + steps_label.setFont(QFont("", 12, QFont.Bold)) + self.add_step_button = QPushButton("添加步骤") + self.remove_step_button = QPushButton("删除步骤") + steps_header_layout.addWidget(steps_label) + steps_header_layout.addStretch() + steps_header_layout.addWidget(self.add_step_button) + steps_header_layout.addWidget(self.remove_step_button) + steps_list_layout.addLayout(steps_header_layout) + + # 步骤列表 + self.steps_list = QListWidget() + steps_list_layout.addWidget(self.steps_list) + + # 步骤顺序调整按钮 + step_order_layout = QHBoxLayout() + self.move_step_up_button = QPushButton("上移") + self.move_step_down_button = QPushButton("下移") + step_order_layout.addWidget(self.move_step_up_button) + step_order_layout.addWidget(self.move_step_down_button) + steps_list_layout.addLayout(step_order_layout) + + # 右侧步骤详细配置 + self.step_config_widget = self.createStepConfigWidget() + + # 添加到布局 + steps_layout.addLayout(steps_list_layout, 1) + steps_layout.addWidget(self.step_config_widget, 2) + + return steps_widget + + def createStepConfigWidget(self): + """创建步骤详细配置部件""" + config_widget = QWidget() + config_layout = QVBoxLayout(config_widget) + + # 滚动区域 + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_content = QWidget() + scroll_layout = QVBoxLayout(scroll_content) + + # 步骤基本信息 + basic_info_group = QGroupBox("基本信息") + basic_info_layout = QGridLayout(basic_info_group) + + # 步骤名称 + basic_info_layout.addWidget(QLabel("步骤名称:"), 0, 0) + self.step_name_edit = QLineEdit() + basic_info_layout.addWidget(self.step_name_edit, 0, 1) + + # 步骤描述 + basic_info_layout.addWidget(QLabel("步骤描述:"), 1, 0) + self.step_description_edit = QTextEdit() + self.step_description_edit.setMaximumHeight(80) + basic_info_layout.addWidget(self.step_description_edit, 1, 1) + + # 步骤类型 + basic_info_layout.addWidget(QLabel("步骤类型:"), 2, 0) + self.step_type_combo = QComboBox() + self.step_type_combo.addItems(["拆卸", "安装"]) + basic_info_layout.addWidget(self.step_type_combo, 2, 1) + + scroll_layout.addWidget(basic_info_group) + + # 目标模型配置 + target_model_group = QGroupBox("目标模型") + target_model_layout = QGridLayout(target_model_group) + + # 目标模型选择 + target_model_layout.addWidget(QLabel("目标模型:"), 0, 0) + self.target_model_combo = QComboBox() + target_model_layout.addWidget(self.target_model_combo, 0, 1) + + # 交互方式 + target_model_layout.addWidget(QLabel("交互方式:"), 1, 0) + self.interaction_type_combo = QComboBox() + self.interaction_type_combo.addItems(["鼠标拖拽", "点击触发"]) + target_model_layout.addWidget(self.interaction_type_combo, 1, 1) + + scroll_layout.addWidget(target_model_group) + + # 拖拽交互配置 + self.drag_config_group = QGroupBox("拖拽交互配置") + drag_config_layout = QGridLayout(self.drag_config_group) + + # 目标位置 + drag_config_layout.addWidget(QLabel("目标位置 X:"), 0, 0) + self.target_pos_x_spin = QDoubleSpinBox() + self.target_pos_x_spin.setRange(-1000, 1000) + self.target_pos_x_spin.setSingleStep(0.1) + drag_config_layout.addWidget(self.target_pos_x_spin, 0, 1) + + drag_config_layout.addWidget(QLabel("目标位置 Y:"), 1, 0) + self.target_pos_y_spin = QDoubleSpinBox() + self.target_pos_y_spin.setRange(-1000, 1000) + self.target_pos_y_spin.setSingleStep(0.1) + drag_config_layout.addWidget(self.target_pos_y_spin, 1, 1) + + drag_config_layout.addWidget(QLabel("目标位置 Z:"), 2, 0) + self.target_pos_z_spin = QDoubleSpinBox() + self.target_pos_z_spin.setRange(-1000, 1000) + self.target_pos_z_spin.setSingleStep(0.1) + drag_config_layout.addWidget(self.target_pos_z_spin, 2, 1) + + # 拖拽约束 + self.constrain_x_check = QCheckBox("约束X轴") + self.constrain_y_check = QCheckBox("约束Y轴") + self.constrain_z_check = QCheckBox("约束Z轴") + + constraint_layout = QHBoxLayout() + constraint_layout.addWidget(self.constrain_x_check) + constraint_layout.addWidget(self.constrain_y_check) + constraint_layout.addWidget(self.constrain_z_check) + + drag_config_layout.addWidget(QLabel("拖拽约束:"), 3, 0) + drag_config_layout.addLayout(constraint_layout, 3, 1) + + scroll_layout.addWidget(self.drag_config_group) + + # 点击触发配置 + self.click_config_group = QGroupBox("点击触发配置") + click_config_layout = QGridLayout(self.click_config_group) + + # 位移距离 + click_config_layout.addWidget(QLabel("位移距离 X:"), 0, 0) + self.move_distance_x_spin = QDoubleSpinBox() + self.move_distance_x_spin.setRange(-100, 100) + self.move_distance_x_spin.setSingleStep(0.1) + click_config_layout.addWidget(self.move_distance_x_spin, 0, 1) + + click_config_layout.addWidget(QLabel("位移距离 Y:"), 1, 0) + self.move_distance_y_spin = QDoubleSpinBox() + self.move_distance_y_spin.setRange(-100, 100) + self.move_distance_y_spin.setSingleStep(0.1) + click_config_layout.addWidget(self.move_distance_y_spin, 1, 1) + + click_config_layout.addWidget(QLabel("位移距离 Z:"), 2, 0) + self.move_distance_z_spin = QDoubleSpinBox() + self.move_distance_z_spin.setRange(-100, 100) + self.move_distance_z_spin.setSingleStep(0.1) + click_config_layout.addWidget(self.move_distance_z_spin, 2, 1) + + # 位移时间 + click_config_layout.addWidget(QLabel("位移时间(秒):"), 3, 0) + self.move_duration_spin = QDoubleSpinBox() + self.move_duration_spin.setRange(0.1, 10.0) + self.move_duration_spin.setSingleStep(0.1) + self.move_duration_spin.setValue(1.0) + click_config_layout.addWidget(self.move_duration_spin, 3, 1) + + # 完成后隐藏 + self.hide_after_move_check = QCheckBox("位移完成后隐藏模型") + click_config_layout.addWidget(self.hide_after_move_check, 4, 0, 1, 2) + + scroll_layout.addWidget(self.click_config_group) + + # 提示音频配置 + audio_config_group = QGroupBox("步骤提示") + audio_config_layout = QGridLayout(audio_config_group) + + # 提示音频文件 + audio_config_layout.addWidget(QLabel("提示音频:"), 0, 0) + audio_file_layout = QHBoxLayout() + self.audio_file_edit = QLineEdit() + self.browse_audio_button = QPushButton("浏览...") + audio_file_layout.addWidget(self.audio_file_edit) + audio_file_layout.addWidget(self.browse_audio_button) + audio_config_layout.addLayout(audio_file_layout, 0, 1) + + # 提示文字 + audio_config_layout.addWidget(QLabel("提示文字:"), 1, 0) + self.hint_text_edit = QTextEdit() + self.hint_text_edit.setMaximumHeight(60) + audio_config_layout.addWidget(self.hint_text_edit, 1, 1) + + scroll_layout.addWidget(audio_config_group) + + # 使用工具配置 + tool_config_group = QGroupBox("使用工具") + tool_config_layout = QGridLayout(tool_config_group) + + tool_config_layout.addWidget(QLabel("所需工具:"), 0, 0) + self.required_tool_combo = QComboBox() + self.required_tool_combo.addItem("无") + tool_config_layout.addWidget(self.required_tool_combo, 0, 1) + + scroll_layout.addWidget(tool_config_group) + + # 设置滚动区域 + scroll_area.setWidget(scroll_content) + config_layout.addWidget(scroll_area) + + # 默认隐藏点击配置 + self.click_config_group.setVisible(False) + + return config_widget + + def createSettingsTab(self): + """创建全局设置标签页""" + settings_widget = QWidget() + settings_layout = QVBoxLayout(settings_widget) + + # 全局设置组 + global_settings_group = QGroupBox("全局设置") + global_settings_layout = QGridLayout(global_settings_group) + + # 自动播放提示音 + self.auto_play_audio_check = QCheckBox("自动播放步骤提示音") + self.auto_play_audio_check.setChecked(True) + global_settings_layout.addWidget(self.auto_play_audio_check, 0, 0, 1, 2) + + # 显示步骤提示文字 + self.show_hint_text_check = QCheckBox("显示步骤提示文字") + self.show_hint_text_check.setChecked(True) + global_settings_layout.addWidget(self.show_hint_text_check, 1, 0, 1, 2) + + # 步骤完成确认 + self.require_confirmation_check = QCheckBox("步骤完成需要确认") + global_settings_layout.addWidget(self.require_confirmation_check, 2, 0, 1, 2) + + # 拖拽灵敏度 + global_settings_layout.addWidget(QLabel("拖拽灵敏度:"), 3, 0) + self.drag_sensitivity_slider = QSlider(Qt.Horizontal) + self.drag_sensitivity_slider.setRange(1, 10) + self.drag_sensitivity_slider.setValue(5) + global_settings_layout.addWidget(self.drag_sensitivity_slider, 3, 1) + + # 动画速度 + global_settings_layout.addWidget(QLabel("动画速度:"), 4, 0) + self.animation_speed_slider = QSlider(Qt.Horizontal) + self.animation_speed_slider.setRange(1, 10) + self.animation_speed_slider.setValue(5) + global_settings_layout.addWidget(self.animation_speed_slider, 4, 1) + + settings_layout.addWidget(global_settings_group) + + # 预设配置组 + presets_group = QGroupBox("预设配置") + presets_layout = QVBoxLayout(presets_group) + + # 预设列表 + self.presets_list = QListWidget() + presets_layout.addWidget(self.presets_list) + + # 预设操作按钮 + preset_buttons_layout = QHBoxLayout() + self.save_preset_button = QPushButton("保存为预设") + self.load_preset_button = QPushButton("加载预设") + self.delete_preset_button = QPushButton("删除预设") + preset_buttons_layout.addWidget(self.save_preset_button) + preset_buttons_layout.addWidget(self.load_preset_button) + preset_buttons_layout.addWidget(self.delete_preset_button) + presets_layout.addLayout(preset_buttons_layout) + + settings_layout.addWidget(presets_group) + + # 添加弹性空间 + settings_layout.addStretch() + + return settings_widget + + def connectSignals(self): + """连接信号槽""" + # 模型搜索 + self.model_search_edit.textChanged.connect(self.filterSceneModels) + + # 模型操作 + self.add_to_config_button.clicked.connect(self.addModelsToConfig) + self.remove_from_config_button.clicked.connect(self.removeModelsFromConfig) + + # 工具操作 + self.add_tool_button.clicked.connect(self.addTool) + self.edit_tool_button.clicked.connect(self.editTool) + self.remove_tool_button.clicked.connect(self.removeTool) + + # 步骤操作 + self.add_step_button.clicked.connect(self.addStep) + self.remove_step_button.clicked.connect(self.removeStep) + self.move_step_up_button.clicked.connect(self.moveStepUp) + self.move_step_down_button.clicked.connect(self.moveStepDown) + self.steps_list.itemSelectionChanged.connect(self.onStepSelectionChanged) + + # 步骤配置变化 + self.interaction_type_combo.currentTextChanged.connect(self.onInteractionTypeChanged) + self.browse_audio_button.clicked.connect(self.browseAudioFile) + + # 步骤配置实时更新 + self.step_name_edit.textChanged.connect(self.updateCurrentStep) + self.step_description_edit.textChanged.connect(self.updateCurrentStep) + self.step_type_combo.currentTextChanged.connect(self.updateCurrentStep) + self.target_model_combo.currentTextChanged.connect(self.updateCurrentStep) + + # 按钮操作 + self.save_button.clicked.connect(self.saveConfiguration) + self.load_button.clicked.connect(self.loadConfiguration) + self.preview_button.clicked.connect(self.previewConfiguration) + self.apply_button.clicked.connect(self.applyConfiguration) + + def loadSceneModels(self): + """加载场景中的模型""" + if not self.world: + return + + # 清空现有列表 + self.scene_models_list.clear() + + # 遍历场景中的所有节点 + self._addNodeToTree(self.world.render, self.scene_models_list) + + # 展开根节点 + self.scene_models_list.expandAll() + + def _addNodeToTree(self, node, parent_item): + """递归添加节点到树形控件""" + if node.getName() in ['render', 'camera', 'aspect2d', 'pixel2d']: + # 跳过系统节点 + for child in node.getChildren(): + self._addNodeToTree(child, parent_item) + return + + # 创建树形项 + if isinstance(parent_item, QTreeWidget): + item = QTreeWidgetItem(parent_item) + else: + item = QTreeWidgetItem(parent_item) + + item.setText(0, node.getName()) + item.setData(0, Qt.UserRole, node) + + # 添加子节点 + for child in node.getChildren(): + self._addNodeToTree(child, item) + + def filterSceneModels(self, text): + """过滤场景模型列表""" + # 简单的文本过滤实现 + def filterItems(item): + item_text = item.text(0).lower() + text_lower = text.lower() + + # 检查当前项是否匹配 + matches = text_lower in item_text + + # 检查子项 + child_matches = False + for i in range(item.childCount()): + child_item = item.child(i) + if filterItems(child_item): + child_matches = True + + # 显示/隐藏项 + item.setHidden(not (matches or child_matches)) + + return matches or child_matches + + # 从根项开始过滤 + for i in range(self.scene_models_list.topLevelItemCount()): + filterItems(self.scene_models_list.topLevelItem(i)) + + def addModelsToConfig(self): + """添加选中的模型到配置""" + selected_items = self.scene_models_list.selectedItems() + + for item in selected_items: + node = item.data(0, Qt.UserRole) + if node: + model_name = node.getName() + + # 检查是否已经存在 + existing_items = self.config_models_list.findItems(model_name, Qt.MatchExactly) + if not existing_items: + # 添加到配置模型列表 + list_item = QListWidgetItem(model_name) + list_item.setData(Qt.UserRole, node) + self.config_models_list.addItem(list_item) + + # 添加到配置数据 + self.config_data['models'].append({ + 'name': model_name, + 'node': node, + 'original_pos': node.getPos(), + 'original_hpr': node.getHpr() + }) + + # 更新目标模型下拉框 + self.target_model_combo.addItem(model_name) + + def removeModelsFromConfig(self): + """从配置中移除选中的模型""" + selected_items = self.config_models_list.selectedItems() + + for item in selected_items: + model_name = item.text() + + # 从列表中移除 + row = self.config_models_list.row(item) + self.config_models_list.takeItem(row) + + # 从配置数据中移除 + self.config_data['models'] = [ + model for model in self.config_data['models'] + if model['name'] != model_name + ] + + # 从目标模型下拉框中移除 + index = self.target_model_combo.findText(model_name) + if index >= 0: + self.target_model_combo.removeItem(index) + + def addTool(self): + """添加工具""" + tool_name = self.tool_name_edit.text().strip() + if not tool_name: + QMessageBox.warning(self, "警告", "请输入工具名称") + return + + # 检查是否已存在 + existing_items = self.tools_list.findItems(tool_name, Qt.MatchExactly) + if existing_items: + QMessageBox.warning(self, "警告", "工具已存在") + return + + # 添加到工具列表 + self.tools_list.addItem(tool_name) + self.required_tool_combo.addItem(tool_name) + + # 添加到配置数据 + self.config_data['tools'].append({ + 'name': tool_name, + 'description': '', + 'icon': '' + }) + + # 清空输入框 + self.tool_name_edit.clear() + + def editTool(self): + """编辑工具""" + current_item = self.tools_list.currentItem() + if not current_item: + QMessageBox.warning(self, "警告", "请选择要编辑的工具") + return + + # 这里可以添加工具编辑对话框 + QMessageBox.information(self, "提示", "工具编辑功能待实现") + + def removeTool(self): + """删除工具""" + current_item = self.tools_list.currentItem() + if not current_item: + QMessageBox.warning(self, "警告", "请选择要删除的工具") + return + + tool_name = current_item.text() + + # 从列表中移除 + row = self.tools_list.row(current_item) + self.tools_list.takeItem(row) + + # 从下拉框中移除 + index = self.required_tool_combo.findText(tool_name) + if index >= 0: + self.required_tool_combo.removeItem(index) + + # 从配置数据中移除 + self.config_data['tools'] = [ + tool for tool in self.config_data['tools'] + if tool['name'] != tool_name + ] + + def addStep(self): + """添加步骤""" + step_count = len(self.config_data['steps']) + 1 + step_name = f"步骤 {step_count}" + + # 创建新步骤数据 + step_data = { + 'name': step_name, + 'description': '', + 'type': '拆卸', + 'target_model': '', + 'interaction_type': '鼠标拖拽', + 'target_position': [0, 0, 0], + 'constraints': {'x': False, 'y': False, 'z': False}, + 'move_distance': [0, 0, 0], + 'move_duration': 1.0, + 'hide_after_move': False, + 'audio_file': '', + 'hint_text': '', + 'required_tool': '无' + } + + # 添加到配置数据 + self.config_data['steps'].append(step_data) + + # 添加到步骤列表 + list_item = QListWidgetItem(step_name) + list_item.setData(Qt.UserRole, step_data) + self.steps_list.addItem(list_item) + + # 选中新添加的步骤 + self.steps_list.setCurrentItem(list_item) + + def removeStep(self): + """删除步骤""" + current_item = self.steps_list.currentItem() + if not current_item: + QMessageBox.warning(self, "警告", "请选择要删除的步骤") + return + + # 从列表中移除 + row = self.steps_list.row(current_item) + self.steps_list.takeItem(row) + + # 从配置数据中移除 + self.config_data['steps'].pop(row) + + # 重新编号步骤 + self.renumberSteps() + + def moveStepUp(self): + """上移步骤""" + current_row = self.steps_list.currentRow() + if current_row <= 0: + return + + # 交换数据 + self.config_data['steps'][current_row], self.config_data['steps'][current_row - 1] = \ + self.config_data['steps'][current_row - 1], self.config_data['steps'][current_row] + + # 交换列表项 + current_item = self.steps_list.takeItem(current_row) + self.steps_list.insertItem(current_row - 1, current_item) + self.steps_list.setCurrentRow(current_row - 1) + + # 重新编号 + self.renumberSteps() + + def moveStepDown(self): + """下移步骤""" + current_row = self.steps_list.currentRow() + if current_row >= self.steps_list.count() - 1: + return + + # 交换数据 + self.config_data['steps'][current_row], self.config_data['steps'][current_row + 1] = \ + self.config_data['steps'][current_row + 1], self.config_data['steps'][current_row] + + # 交换列表项 + current_item = self.steps_list.takeItem(current_row) + self.steps_list.insertItem(current_row + 1, current_item) + self.steps_list.setCurrentRow(current_row + 1) + + # 重新编号 + self.renumberSteps() + + def renumberSteps(self): + """重新编号步骤""" + for i in range(self.steps_list.count()): + item = self.steps_list.item(i) + step_data = item.data(Qt.UserRole) + new_name = f"步骤 {i + 1}" + step_data['name'] = new_name + item.setText(new_name) + + def onStepSelectionChanged(self): + """步骤选择改变""" + current_item = self.steps_list.currentItem() + if not current_item: + return + + step_data = current_item.data(Qt.UserRole) + if not step_data: + return + + # 更新步骤配置界面 + self.loadStepConfiguration(step_data) + + def loadStepConfiguration(self, step_data): + """加载步骤配置到界面""" + # 基本信息 + self.step_name_edit.setText(step_data.get('name', '')) + self.step_description_edit.setPlainText(step_data.get('description', '')) + self.step_type_combo.setCurrentText(step_data.get('type', '拆卸')) + + # 目标模型 + target_model = step_data.get('target_model', '') + index = self.target_model_combo.findText(target_model) + if index >= 0: + self.target_model_combo.setCurrentIndex(index) + + # 交互方式 + interaction_type = step_data.get('interaction_type', '鼠标拖拽') + self.interaction_type_combo.setCurrentText(interaction_type) + self.onInteractionTypeChanged(interaction_type) + + # 拖拽配置 + target_pos = step_data.get('target_position', [0, 0, 0]) + self.target_pos_x_spin.setValue(target_pos[0]) + self.target_pos_y_spin.setValue(target_pos[1]) + self.target_pos_z_spin.setValue(target_pos[2]) + + constraints = step_data.get('constraints', {'x': False, 'y': False, 'z': False}) + self.constrain_x_check.setChecked(constraints.get('x', False)) + self.constrain_y_check.setChecked(constraints.get('y', False)) + self.constrain_z_check.setChecked(constraints.get('z', False)) + + # 点击触发配置 + move_distance = step_data.get('move_distance', [0, 0, 0]) + self.move_distance_x_spin.setValue(move_distance[0]) + self.move_distance_y_spin.setValue(move_distance[1]) + self.move_distance_z_spin.setValue(move_distance[2]) + + self.move_duration_spin.setValue(step_data.get('move_duration', 1.0)) + self.hide_after_move_check.setChecked(step_data.get('hide_after_move', False)) + + # 提示配置 + self.audio_file_edit.setText(step_data.get('audio_file', '')) + self.hint_text_edit.setPlainText(step_data.get('hint_text', '')) + + # 工具配置 + required_tool = step_data.get('required_tool', '无') + index = self.required_tool_combo.findText(required_tool) + if index >= 0: + self.required_tool_combo.setCurrentIndex(index) + + def onInteractionTypeChanged(self, interaction_type): + """交互方式改变""" + if interaction_type == "鼠标拖拽": + self.drag_config_group.setVisible(True) + self.click_config_group.setVisible(False) + else: # 点击触发 + self.drag_config_group.setVisible(False) + self.click_config_group.setVisible(True) + + def updateCurrentStep(self): + """更新当前步骤数据""" + current_item = self.steps_list.currentItem() + if not current_item: + return + + step_data = current_item.data(Qt.UserRole) + if not step_data: + return + + # 更新步骤数据 + step_data['name'] = self.step_name_edit.text() + step_data['description'] = self.step_description_edit.toPlainText() + step_data['type'] = self.step_type_combo.currentText() + step_data['target_model'] = self.target_model_combo.currentText() + step_data['interaction_type'] = self.interaction_type_combo.currentText() + + # 更新列表项显示 + current_item.setText(step_data['name']) + + def browseAudioFile(self): + """浏览音频文件""" + file_path, _ = QFileDialog.getOpenFileName( + self, "选择音频文件", "", + "音频文件 (*.wav *.mp3 *.ogg *.flac);;所有文件 (*)" + ) + + if file_path: + self.audio_file_edit.setText(file_path) + + def saveConfiguration(self): + """保存配置""" + file_path, _ = QFileDialog.getSaveFileName( + self, "保存拆装配置", "", + "JSON文件 (*.json);;所有文件 (*)" + ) + + if file_path: + try: + # 准备保存的数据(去除不能序列化的对象) + save_data = { + 'models': [ + { + 'name': model['name'], + 'original_pos': list(model['original_pos']), + 'original_hpr': list(model['original_hpr']) + } for model in self.config_data['models'] + ], + 'steps': self.config_data['steps'], + 'tools': self.config_data['tools'], + 'settings': { + 'auto_play_audio': self.auto_play_audio_check.isChecked(), + 'show_hint_text': self.show_hint_text_check.isChecked(), + 'require_confirmation': self.require_confirmation_check.isChecked(), + 'drag_sensitivity': self.drag_sensitivity_slider.value(), + 'animation_speed': self.animation_speed_slider.value() + } + } + + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(save_data, f, ensure_ascii=False, indent=2) + + QMessageBox.information(self, "成功", "配置保存成功") + + except Exception as e: + QMessageBox.critical(self, "错误", f"保存配置失败: {str(e)}") + + def loadConfiguration(self): + """加载配置""" + file_path, _ = QFileDialog.getOpenFileName( + self, "加载拆装配置", "", + "JSON文件 (*.json);;所有文件 (*)" + ) + + if file_path: + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + # 加载配置数据 + self.loadConfigurationData(data) + QMessageBox.information(self, "成功", "配置加载成功") + + except Exception as e: + QMessageBox.critical(self, "错误", f"加载配置失败: {str(e)}") + + def loadConfigurationData(self, data): + """加载配置数据到界面""" + # 清空现有配置 + self.config_models_list.clear() + self.target_model_combo.clear() + self.steps_list.clear() + + # 加载模型配置 + # 注意:这里需要重新匹配场景中的模型节点 + + # 加载步骤配置 + self.config_data['steps'] = data.get('steps', []) + for step_data in self.config_data['steps']: + list_item = QListWidgetItem(step_data['name']) + list_item.setData(Qt.UserRole, step_data) + self.steps_list.addItem(list_item) + + # 加载工具配置 + self.tools_list.clear() + self.required_tool_combo.clear() + self.required_tool_combo.addItem("无") + + self.config_data['tools'] = data.get('tools', []) + for tool in self.config_data['tools']: + self.tools_list.addItem(tool['name']) + self.required_tool_combo.addItem(tool['name']) + + # 加载全局设置 + settings = data.get('settings', {}) + self.auto_play_audio_check.setChecked(settings.get('auto_play_audio', True)) + self.show_hint_text_check.setChecked(settings.get('show_hint_text', True)) + self.require_confirmation_check.setChecked(settings.get('require_confirmation', False)) + self.drag_sensitivity_slider.setValue(settings.get('drag_sensitivity', 5)) + self.animation_speed_slider.setValue(settings.get('animation_speed', 5)) + + def previewConfiguration(self): + """预览配置效果""" + QMessageBox.information(self, "提示", "预览功能待实现") + + def applyConfiguration(self): + """应用配置""" QMessageBox.information(self, "提示", "应用配置功能待实现") \ No newline at end of file diff --git a/ui/assembly_disassembly_config_simple.py b/ui/assembly_disassembly_config_simple.py index 9397f650..b31f94c7 100644 --- a/ui/assembly_disassembly_config_simple.py +++ b/ui/assembly_disassembly_config_simple.py @@ -1,1069 +1,1069 @@ -# -*- coding: utf-8 -*- -""" -拆装交互配置界面 - 简化版本 -""" - -from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, - QWidget, QLabel, QListWidget, QListWidgetItem, - QPushButton, QComboBox, QLineEdit, QTextEdit, - QGroupBox, QGridLayout, QCheckBox, QSplitter, - QTreeWidget, QTreeWidgetItem, QMessageBox, - QFileDialog, QScrollArea, QDoubleSpinBox) -from PyQt5.QtCore import Qt -from PyQt5.QtGui import QFont -import json - - -class AssemblyDisassemblyConfigDialog(QDialog): - """拆装配置主对话框 - 简化版本""" - - def __init__(self, parent=None, world=None): - super().__init__(parent) - self.world = world - self.config_data = { - 'models': [], # 参与拆装的模型列表 - 'steps': [], # 拆装步骤配置 - 'tools': [], # 使用的工具列表 - 'settings': {} # 全局设置 - } - - self.setupUI() - self.loadSceneModels() - - # 初始化当前编辑的步骤项 - self._current_step_item = None - - def setupUI(self): - """设置界面""" - self.setWindowTitle("拆装交互配置") - self.setModal(False) - self.resize(1000, 700) - - # 主布局 - main_layout = QVBoxLayout(self) - - # 标题 - title_label = QLabel("模型拆装交互配置") - title_label.setFont(QFont("", 16, QFont.Bold)) - title_label.setAlignment(Qt.AlignCenter) - main_layout.addWidget(title_label) - - # 创建标签页 - self.tab_widget = QTabWidget() - main_layout.addWidget(self.tab_widget) - - # 模型选择标签页 - self.models_tab = self.createModelsTab() - self.tab_widget.addTab(self.models_tab, "模型选择") - - # 步骤配置标签页 - self.steps_tab = self.createStepsTab() - self.tab_widget.addTab(self.steps_tab, "步骤配置") - - # 工具配置标签页 - self.tools_tab = self.createToolsTab() - self.tab_widget.addTab(self.tools_tab, "工具配置") - - # 全局设置标签页 - self.settings_tab = self.createSettingsTab() - self.tab_widget.addTab(self.settings_tab, "全局设置") - - # 底部按钮 - button_layout = QHBoxLayout() - - self.save_button = QPushButton("保存配置") - self.load_button = QPushButton("加载配置") - self.preview_button = QPushButton("预览效果") - self.apply_button = QPushButton("应用配置") - - button_layout.addWidget(self.save_button) - button_layout.addWidget(self.load_button) - button_layout.addStretch() - button_layout.addWidget(self.preview_button) - button_layout.addWidget(self.apply_button) - - main_layout.addLayout(button_layout) - - # 连接信号 - self.connectSignals() - - def createModelsTab(self): - """创建模型选择标签页""" - widget = QWidget() - layout = QVBoxLayout(widget) - - # 分割器 - splitter = QSplitter(Qt.Horizontal) - layout.addWidget(splitter) - - # 左侧 - 场景模型 - left_group = QGroupBox("场景模型") - left_layout = QVBoxLayout(left_group) - - # 添加搜索框 - search_layout = QHBoxLayout() - search_layout.addWidget(QLabel("搜索模型:")) - self.model_search_edit = QLineEdit() - self.model_search_edit.setPlaceholderText("输入模型名称进行搜索...") - search_layout.addWidget(self.model_search_edit) - left_layout.addLayout(search_layout) - - self.scene_models_tree = QTreeWidget() - self.scene_models_tree.setHeaderLabel("场景中的模型") - left_layout.addWidget(self.scene_models_tree) - - # 操作按钮 - button_layout = QHBoxLayout() - self.add_model_button = QPushButton("添加到配置 →") - self.remove_model_button = QPushButton("← 从配置移除") - button_layout.addWidget(self.add_model_button) - button_layout.addWidget(self.remove_model_button) - left_layout.addLayout(button_layout) - - splitter.addWidget(left_group) - - # 右侧 - 已配置模型 - right_group = QGroupBox("已配置模型") - right_layout = QVBoxLayout(right_group) - - self.config_models_list = QListWidget() - right_layout.addWidget(self.config_models_list) - - splitter.addWidget(right_group) - - return widget - - def createStepsTab(self): - """创建步骤配置标签页""" - widget = QWidget() - layout = QVBoxLayout(widget) - - # 分割器 - splitter = QSplitter(Qt.Horizontal) - layout.addWidget(splitter) - - # 左侧 - 步骤列表 - left_group = QGroupBox("拆装步骤") - left_layout = QVBoxLayout(left_group) - - # 步骤操作按钮 - step_buttons = QHBoxLayout() - self.add_step_button = QPushButton("添加步骤") - self.remove_step_button = QPushButton("删除步骤") - self.move_up_button = QPushButton("上移") - self.move_down_button = QPushButton("下移") - - step_buttons.addWidget(self.add_step_button) - step_buttons.addWidget(self.remove_step_button) - step_buttons.addWidget(self.move_up_button) - step_buttons.addWidget(self.move_down_button) - left_layout.addLayout(step_buttons) - - self.steps_list = QListWidget() - left_layout.addWidget(self.steps_list) - - splitter.addWidget(left_group) - - # 右侧 - 步骤详细配置 - right_group = QGroupBox("步骤详细配置") - right_layout = QVBoxLayout(right_group) - - # 滚动区域 - scroll_area = QScrollArea() - scroll_area.setWidgetResizable(True) - scroll_content = QWidget() - scroll_layout = QVBoxLayout(scroll_content) - - # 基本信息 - basic_group = QGroupBox("基本信息") - basic_layout = QGridLayout(basic_group) - - basic_layout.addWidget(QLabel("步骤名称:"), 0, 0) - self.step_name_edit = QLineEdit() - basic_layout.addWidget(self.step_name_edit, 0, 1) - - basic_layout.addWidget(QLabel("步骤描述:"), 1, 0) - self.step_desc_edit = QTextEdit() - self.step_desc_edit.setMaximumHeight(80) - basic_layout.addWidget(self.step_desc_edit, 1, 1) - - basic_layout.addWidget(QLabel("步骤类型:"), 2, 0) - self.step_type_combo = QComboBox() - self.step_type_combo.addItems(["拆卸", "安装"]) - basic_layout.addWidget(self.step_type_combo, 2, 1) - - # 步骤分值(考核模式用) - basic_layout.addWidget(QLabel("步骤分值:"), 3, 0) - self.step_score_spin = QDoubleSpinBox() - self.step_score_spin.setRange(0, 100) - self.step_score_spin.setValue(10) # 默认10分 - self.step_score_spin.setSingleStep(1) - self.step_score_spin.setDecimals(0) - self.step_score_spin.setSuffix(" 分") - basic_layout.addWidget(self.step_score_spin, 3, 1) - - # 添加分值说明 - score_note = QLabel("注:在考核模式下,此分值将用于计算该步骤的得分。") - score_note.setWordWrap(True) - score_note.setStyleSheet("color: #666; font-style: italic; font-size: 10px;") - basic_layout.addWidget(score_note, 4, 0, 1, 2) - - scroll_layout.addWidget(basic_group) - - # 目标模型 - target_group = QGroupBox("目标模型") - target_layout = QGridLayout(target_group) - - target_layout.addWidget(QLabel("目标模型:"), 0, 0) - self.target_model_combo = QComboBox() - target_layout.addWidget(self.target_model_combo, 0, 1) - - target_layout.addWidget(QLabel("交互方式:"), 1, 0) - self.interaction_combo = QComboBox() - self.interaction_combo.addItems(["鼠标拖拽", "点击触发"]) - target_layout.addWidget(self.interaction_combo, 1, 1) - - scroll_layout.addWidget(target_group) - - # 位置配置 - pos_group = QGroupBox("位置配置") - pos_layout = QGridLayout(pos_group) - - # 添加说明文字 - pos_note = QLabel("注:拖拽交互支持自由拖拽,无轴向限制。以下为参考目标位置:") - pos_note.setWordWrap(True) - pos_note.setStyleSheet("color: #666; font-style: italic;") - pos_layout.addWidget(pos_note, 0, 0, 1, 2) - - pos_layout.addWidget(QLabel("参考位置 X:"), 1, 0) - self.pos_x_spin = QDoubleSpinBox() - self.pos_x_spin.setRange(-1000, 1000) - pos_layout.addWidget(self.pos_x_spin, 1, 1) - - pos_layout.addWidget(QLabel("参考位置 Y:"), 2, 0) - self.pos_y_spin = QDoubleSpinBox() - self.pos_y_spin.setRange(-1000, 1000) - pos_layout.addWidget(self.pos_y_spin, 2, 1) - - pos_layout.addWidget(QLabel("参考位置 Z:"), 3, 0) - self.pos_z_spin = QDoubleSpinBox() - self.pos_z_spin.setRange(-1000, 1000) - pos_layout.addWidget(self.pos_z_spin, 3, 1) - - scroll_layout.addWidget(pos_group) - - # 动画配置(仅点击触发模式) - animation_group = QGroupBox("动画配置") - animation_layout = QGridLayout(animation_group) - - # 动画时长 - animation_layout.addWidget(QLabel("动画时长(秒):"), 0, 0) - self.animation_duration_spin = QDoubleSpinBox() - self.animation_duration_spin.setRange(0.1, 10.0) - self.animation_duration_spin.setValue(2.0) - self.animation_duration_spin.setSingleStep(0.1) - self.animation_duration_spin.setDecimals(1) - animation_layout.addWidget(self.animation_duration_spin, 0, 1) - - # 说明文字 - animation_note = QLabel("注:仅在'点击触发'交互方式下有效,用于控制模型自动移动的速度。") - animation_note.setWordWrap(True) - animation_note.setStyleSheet("color: #666; font-style: italic;") - animation_layout.addWidget(animation_note, 1, 0, 1, 2) - - scroll_layout.addWidget(animation_group) - - # 提示配置 - hint_group = QGroupBox("提示配置") - hint_layout = QGridLayout(hint_group) - - hint_layout.addWidget(QLabel("提示音频:"), 0, 0) - audio_layout = QHBoxLayout() - self.audio_edit = QLineEdit() - self.browse_audio_button = QPushButton("浏览...") - audio_layout.addWidget(self.audio_edit) - audio_layout.addWidget(self.browse_audio_button) - hint_layout.addLayout(audio_layout, 0, 1) - - hint_layout.addWidget(QLabel("提示文字:"), 1, 0) - self.hint_text_edit = QTextEdit() - self.hint_text_edit.setMaximumHeight(60) - hint_layout.addWidget(self.hint_text_edit, 1, 1) - - scroll_layout.addWidget(hint_group) - - # 工具配置 - tool_group = QGroupBox("使用工具") - tool_layout = QGridLayout(tool_group) - - tool_layout.addWidget(QLabel("所需工具:"), 0, 0) - self.required_tool_combo = QComboBox() - self.required_tool_combo.addItem("无") - tool_layout.addWidget(self.required_tool_combo, 0, 1) - - scroll_layout.addWidget(tool_group) - - scroll_area.setWidget(scroll_content) - right_layout.addWidget(scroll_area) - - splitter.addWidget(right_group) - - return widget - - def createToolsTab(self): - """创建工具配置标签页""" - widget = QWidget() - layout = QVBoxLayout(widget) - - # 说明文字 - info_label = QLabel("配置拆装过程中需要使用的工具:") - layout.addWidget(info_label) - - # 工具添加 - add_layout = QHBoxLayout() - add_layout.addWidget(QLabel("工具名称:")) - self.tool_name_edit = QLineEdit() - self.add_tool_button = QPushButton("添加工具") - add_layout.addWidget(self.tool_name_edit) - add_layout.addWidget(self.add_tool_button) - add_layout.addStretch() - layout.addLayout(add_layout) - - # 工具列表 - self.tools_list = QListWidget() - layout.addWidget(self.tools_list) - - # 工具操作按钮 - tool_buttons = QHBoxLayout() - self.edit_tool_button = QPushButton("编辑工具") - self.delete_tool_button = QPushButton("删除工具") - tool_buttons.addWidget(self.edit_tool_button) - tool_buttons.addWidget(self.delete_tool_button) - tool_buttons.addStretch() - layout.addLayout(tool_buttons) - - return widget - - def createSettingsTab(self): - """创建全局设置标签页""" - widget = QWidget() - layout = QVBoxLayout(widget) - - # 说明文字 - info_label = QLabel("配置拆装交互的全局设置:") - layout.addWidget(info_label) - - # 设置组 - settings_group = QGroupBox("全局设置") - settings_layout = QGridLayout(settings_group) - - # 自动播放提示音 - self.auto_audio_check = QCheckBox("自动播放步骤提示音") - self.auto_audio_check.setChecked(True) - settings_layout.addWidget(self.auto_audio_check, 0, 0, 1, 2) - - # 显示提示文字 - self.show_hint_check = QCheckBox("显示步骤提示文字") - self.show_hint_check.setChecked(True) - settings_layout.addWidget(self.show_hint_check, 1, 0, 1, 2) - - # 步骤完成确认 - self.confirm_check = QCheckBox("步骤完成需要确认") - settings_layout.addWidget(self.confirm_check, 2, 0, 1, 2) - - layout.addWidget(settings_group) - - # 预设配置 - presets_group = QGroupBox("预设配置") - presets_layout = QVBoxLayout(presets_group) - - self.presets_list = QListWidget() - presets_layout.addWidget(self.presets_list) - - preset_buttons = QHBoxLayout() - self.save_preset_button = QPushButton("保存为预设") - self.load_preset_button = QPushButton("加载预设") - self.delete_preset_button = QPushButton("删除预设") - preset_buttons.addWidget(self.save_preset_button) - preset_buttons.addWidget(self.load_preset_button) - preset_buttons.addWidget(self.delete_preset_button) - presets_layout.addLayout(preset_buttons) - - layout.addWidget(presets_group) - - layout.addStretch() - - return widget - - def connectSignals(self): - """连接信号槽""" - # 模型搜索 - self.model_search_edit.textChanged.connect(self.filterSceneModels) - - # 模型操作 - self.add_model_button.clicked.connect(self.addModelsToConfig) - self.remove_model_button.clicked.connect(self.removeModelsFromConfig) - - # 步骤操作 - self.add_step_button.clicked.connect(self.addStep) - self.remove_step_button.clicked.connect(self.removeStep) - self.move_up_button.clicked.connect(self.moveStepUp) - self.move_down_button.clicked.connect(self.moveStepDown) - self.steps_list.itemSelectionChanged.connect(self.onStepSelectionChanged) - - # 步骤配置实时保存 - 使用专门的连接方法 - self.connectStepConfigSignals() - - # 工具操作 - self.add_tool_button.clicked.connect(self.addTool) - self.edit_tool_button.clicked.connect(self.editTool) - self.delete_tool_button.clicked.connect(self.deleteTool) - - # 文件操作 - self.browse_audio_button.clicked.connect(self.browseAudioFile) - - # 按钮操作 - self.save_button.clicked.connect(self.saveConfiguration) - self.load_button.clicked.connect(self.loadConfiguration) - self.preview_button.clicked.connect(self.previewConfiguration) - self.apply_button.clicked.connect(self.applyConfiguration) - - def loadSceneModels(self): - """加载场景中的模型""" - if not self.world: - return - - self.scene_models_tree.clear() - self._addNodeToTree(self.world.render, self.scene_models_tree) - self.scene_models_tree.expandAll() - - def _addNodeToTree(self, node, parent_item): - """递归添加节点到树形控件""" - if node.getName() in ['render', 'camera', 'aspect2d', 'pixel2d']: - for child in node.getChildren(): - self._addNodeToTree(child, parent_item) - return - - if isinstance(parent_item, QTreeWidget): - item = QTreeWidgetItem(parent_item) - else: - item = QTreeWidgetItem(parent_item) - - item.setText(0, node.getName()) - item.setData(0, Qt.UserRole, node) - - for child in node.getChildren(): - self._addNodeToTree(child, item) - - def filterSceneModels(self, search_text): - """根据搜索文本过滤场景模型""" - def filterItems(item): - item_text = item.text(0).lower() - search_lower = search_text.lower() - - # 检查当前项是否匹配 - matches = search_lower in item_text if search_text else True - - # 检查子项 - child_matches = False - for i in range(item.childCount()): - child_item = item.child(i) - if filterItems(child_item): - child_matches = True - - # 显示/隐藏项 - item.setHidden(not (matches or child_matches)) - - return matches or child_matches - - # 从根项开始过滤 - for i in range(self.scene_models_tree.topLevelItemCount()): - filterItems(self.scene_models_tree.topLevelItem(i)) - - def addModelsToConfig(self): - """添加选中的模型到配置""" - selected_items = self.scene_models_tree.selectedItems() - - for item in selected_items: - node = item.data(0, Qt.UserRole) - if node: - model_name = node.getName() - existing_items = self.config_models_list.findItems(model_name, Qt.MatchExactly) - if not existing_items: - list_item = QListWidgetItem(model_name) - list_item.setData(Qt.UserRole, node) - self.config_models_list.addItem(list_item) - - self.config_data['models'].append({ - 'name': model_name, - 'node': node, - 'original_pos': node.getPos(), - 'original_hpr': node.getHpr() - }) - - self.target_model_combo.addItem(model_name) - - def removeModelsFromConfig(self): - """从配置中移除选中的模型""" - selected_items = self.config_models_list.selectedItems() - - for item in selected_items: - model_name = item.text() - row = self.config_models_list.row(item) - self.config_models_list.takeItem(row) - - self.config_data['models'] = [ - model for model in self.config_data['models'] - if model['name'] != model_name - ] - - index = self.target_model_combo.findText(model_name) - if index >= 0: - self.target_model_combo.removeItem(index) - - def addStep(self): - """添加步骤""" - step_count = len(self.config_data['steps']) + 1 - step_name = f"步骤 {step_count}" - - step_data = { - 'name': step_name, - 'description': '', - 'type': '拆卸', - 'target_model': '', - 'interaction_type': '鼠标拖拽', - 'target_position': [0, 0, 0], - 'audio_file': '', - 'hint_text': '', - 'required_tool': '无', - 'score': 10 # 默认分值 - } - - self.config_data['steps'].append(step_data) - - # 显示步骤名称和分值 - display_text = f"{step_name} ({step_data['score']}分)" - list_item = QListWidgetItem(display_text) - list_item.setData(Qt.UserRole, step_data) - self.steps_list.addItem(list_item) - - # 设置当前编辑的步骤项并选中 - self._current_step_item = list_item - self.steps_list.setCurrentItem(list_item) - - # 清空界面并显示新步骤的默认配置 - self.loadStepConfiguration(step_data) - - def removeStep(self): - """删除步骤""" - current_item = self.steps_list.currentItem() - if not current_item: - QMessageBox.warning(self, "警告", "请选择要删除的步骤") - return - - row = self.steps_list.row(current_item) - self.steps_list.takeItem(row) - self.config_data['steps'].pop(row) - - # 如果删除的是当前编辑的步骤,清空当前步骤项 - if self._current_step_item == current_item: - self._current_step_item = None - - self.renumberSteps() - - def moveStepUp(self): - """上移步骤""" - current_row = self.steps_list.currentRow() - if current_row <= 0: - return - - self.config_data['steps'][current_row], self.config_data['steps'][current_row - 1] = \ - self.config_data['steps'][current_row - 1], self.config_data['steps'][current_row] - - current_item = self.steps_list.takeItem(current_row) - self.steps_list.insertItem(current_row - 1, current_item) - self.steps_list.setCurrentRow(current_row - 1) - self.renumberSteps() - - def moveStepDown(self): - """下移步骤""" - current_row = self.steps_list.currentRow() - if current_row >= self.steps_list.count() - 1: - return - - self.config_data['steps'][current_row], self.config_data['steps'][current_row + 1] = \ - self.config_data['steps'][current_row + 1], self.config_data['steps'][current_row] - - current_item = self.steps_list.takeItem(current_row) - self.steps_list.insertItem(current_row + 1, current_item) - self.steps_list.setCurrentRow(current_row + 1) - self.renumberSteps() - - def renumberSteps(self): - """重新编号步骤""" - for i in range(self.steps_list.count()): - item = self.steps_list.item(i) - step_data = item.data(Qt.UserRole) - new_name = f"步骤 {i + 1}" - step_data['name'] = new_name - step_score = step_data.get('score', 10) - display_text = f"{new_name} ({step_score}分)" - item.setText(display_text) - - def onStepSelectionChanged(self): - """步骤选择改变""" - # 简单直接的切换逻辑 - current_item = self.steps_list.currentItem() - if not current_item: - self._current_step_item = None - self._clearStepConfigurationUI() - return - - # 记录当前步骤项并加载配置 - self._current_step_item = current_item - self.loadStepConfiguration(current_item.data(Qt.UserRole)) - - def _clearStepConfigurationUI(self): - """清空步骤配置界面""" - self.disconnectStepConfigSignals() - try: - self.step_name_edit.setText("") - self.step_desc_edit.setPlainText("") - self.step_type_combo.setCurrentIndex(0) - self.target_model_combo.setCurrentIndex(0) - self.interaction_combo.setCurrentIndex(0) - self.pos_x_spin.setValue(0) - self.pos_y_spin.setValue(0) - self.pos_z_spin.setValue(0) - self.audio_edit.setText("") - self.hint_text_edit.setPlainText("") - self.required_tool_combo.setCurrentIndex(0) - finally: - self.connectStepConfigSignals() - - def loadStepConfiguration(self, step_data): - """加载步骤配置到界面""" - # 临时断开信号,避免加载时触发保存 - self.disconnectStepConfigSignals() - try: - # 加载所有配置项到界面 - self.step_name_edit.setText(step_data.get('name', '')) - self.step_desc_edit.setPlainText(step_data.get('description', '')) - self.step_type_combo.setCurrentText(step_data.get('type', '拆卸')) - - target_model = step_data.get('target_model', '') - index = self.target_model_combo.findText(target_model) - if index >= 0: - self.target_model_combo.setCurrentIndex(index) - else: - self.target_model_combo.setCurrentIndex(0) - - self.interaction_combo.setCurrentText(step_data.get('interaction_type', '鼠标拖拽')) - - target_pos = step_data.get('target_position', [0, 0, 0]) - self.pos_x_spin.setValue(target_pos[0]) - self.pos_y_spin.setValue(target_pos[1]) - self.pos_z_spin.setValue(target_pos[2]) - - # 加载动画时长 - self.animation_duration_spin.setValue(step_data.get('animation_duration', 2.0)) - - self.audio_edit.setText(step_data.get('audio_file', '')) - self.hint_text_edit.setPlainText(step_data.get('hint_text', '')) - - # 加载工具配置 - required_tool = step_data.get('required_tool', '无') - index = self.required_tool_combo.findText(required_tool) - if index >= 0: - self.required_tool_combo.setCurrentIndex(index) - else: - self.required_tool_combo.setCurrentIndex(0) - - # 加载分值配置 - self.step_score_spin.setValue(step_data.get('score', 10)) - - finally: - # 确保信号重新连接 - self.connectStepConfigSignals() - - def disconnectStepConfigSignals(self): - """断开步骤配置的信号连接""" - try: - # 精确断开特定的信号连接 - self.step_name_edit.textChanged.disconnect(self.saveCurrentStepConfig) - self.step_desc_edit.textChanged.disconnect(self.saveCurrentStepConfig) - self.step_type_combo.currentTextChanged.disconnect(self.saveCurrentStepConfig) - self.target_model_combo.currentTextChanged.disconnect(self.saveCurrentStepConfig) - self.interaction_combo.currentTextChanged.disconnect(self.saveCurrentStepConfig) - self.pos_x_spin.valueChanged.disconnect(self.saveCurrentStepConfig) - self.pos_y_spin.valueChanged.disconnect(self.saveCurrentStepConfig) - self.pos_z_spin.valueChanged.disconnect(self.saveCurrentStepConfig) - self.animation_duration_spin.valueChanged.disconnect(self.saveCurrentStepConfig) - self.audio_edit.textChanged.disconnect(self.saveCurrentStepConfig) - self.hint_text_edit.textChanged.disconnect(self.saveCurrentStepConfig) - self.required_tool_combo.currentTextChanged.disconnect(self.saveCurrentStepConfig) - self.step_score_spin.valueChanged.disconnect(self.saveCurrentStepConfig) - except: - # 忽略断开连接时的错误 - pass - - def connectStepConfigSignals(self): - """连接步骤配置的信号""" - # 重新连接信号到保存方法 - self.step_name_edit.textChanged.connect(self.saveCurrentStepConfig) - self.step_desc_edit.textChanged.connect(self.saveCurrentStepConfig) - self.step_type_combo.currentTextChanged.connect(self.saveCurrentStepConfig) - self.target_model_combo.currentTextChanged.connect(self.saveCurrentStepConfig) - self.interaction_combo.currentTextChanged.connect(self.saveCurrentStepConfig) - self.pos_x_spin.valueChanged.connect(self.saveCurrentStepConfig) - self.pos_y_spin.valueChanged.connect(self.saveCurrentStepConfig) - self.pos_z_spin.valueChanged.connect(self.saveCurrentStepConfig) - self.animation_duration_spin.valueChanged.connect(self.saveCurrentStepConfig) - self.audio_edit.textChanged.connect(self.saveCurrentStepConfig) - self.hint_text_edit.textChanged.connect(self.saveCurrentStepConfig) - self.required_tool_combo.currentTextChanged.connect(self.saveCurrentStepConfig) - self.step_score_spin.valueChanged.connect(self.saveCurrentStepConfig) - - def saveCurrentStepConfig(self): - """保存当前步骤的配置到list_item的UserRole数据中""" - # 检查当前步骤项是否有效 - if not hasattr(self, '_current_step_item') or self._current_step_item is None: - return - - # 获取当前步骤的数据 - step_data = self._current_step_item.data(Qt.UserRole) - if not step_data: - return - - # 从界面控件收集所有配置数据 - step_data['name'] = self.step_name_edit.text() - step_data['description'] = self.step_desc_edit.toPlainText() - step_data['type'] = self.step_type_combo.currentText() - step_data['target_model'] = self.target_model_combo.currentText() - step_data['interaction_type'] = self.interaction_combo.currentText() - step_data['target_position'] = [ - self.pos_x_spin.value(), - self.pos_y_spin.value(), - self.pos_z_spin.value() - ] - step_data['animation_duration'] = self.animation_duration_spin.value() - step_data['audio_file'] = self.audio_edit.text() - step_data['hint_text'] = self.hint_text_edit.toPlainText() - step_data['required_tool'] = self.required_tool_combo.currentText() - step_data['score'] = int(self.step_score_spin.value()) # 保存分值 - - # 确保数据更新到list_item的UserRole中 - self._current_step_item.setData(Qt.UserRole, step_data) - - # 更新列表项的显示文本,包含分值信息 - step_name = step_data['name'] - step_score = step_data.get('score', 10) - display_text = f"{step_name} ({step_score}分)" - self._current_step_item.setText(display_text) - - # 同时更新config_data中的对应数据(保持数据一致性) - current_row = self.steps_list.row(self._current_step_item) - if 0 <= current_row < len(self.config_data['steps']): - self.config_data['steps'][current_row] = step_data - - def addTool(self): - """添加工具""" - tool_name = self.tool_name_edit.text().strip() - if not tool_name: - QMessageBox.warning(self, "警告", "请输入工具名称") - return - - existing_items = self.tools_list.findItems(tool_name, Qt.MatchExactly) - if existing_items: - QMessageBox.warning(self, "警告", "工具已存在") - return - - self.tools_list.addItem(tool_name) - self.config_data['tools'].append({'name': tool_name, 'description': ''}) - - # 同时添加到步骤配置的工具下拉框中 - self.required_tool_combo.addItem(tool_name) - - self.tool_name_edit.clear() - - def editTool(self): - """编辑工具""" - current_item = self.tools_list.currentItem() - if not current_item: - QMessageBox.warning(self, "警告", "请选择要编辑的工具") - return - QMessageBox.information(self, "提示", "工具编辑功能待实现") - - def deleteTool(self): - """删除工具""" - current_item = self.tools_list.currentItem() - if not current_item: - QMessageBox.warning(self, "警告", "请选择要删除的工具") - return - - tool_name = current_item.text() - row = self.tools_list.row(current_item) - self.tools_list.takeItem(row) - - self.config_data['tools'] = [ - tool for tool in self.config_data['tools'] - if tool['name'] != tool_name - ] - - # 同时从步骤配置的工具下拉框中移除 - index = self.required_tool_combo.findText(tool_name) - if index >= 0: - self.required_tool_combo.removeItem(index) - - def browseAudioFile(self): - """浏览音频文件""" - file_path, _ = QFileDialog.getOpenFileName( - self, "选择音频文件", "", - "音频文件 (*.wav *.mp3 *.ogg);;所有文件 (*)" - ) - - if file_path: - self.audio_edit.setText(file_path) - - def saveConfiguration(self): - """保存配置""" - file_path, _ = QFileDialog.getSaveFileName( - self, "保存拆装配置", "", - "JSON文件 (*.json);;所有文件 (*)" - ) - - if file_path: - try: - save_data = { - 'models': [ - { - 'name': model['name'], - 'original_pos': list(model['original_pos']), - 'original_hpr': list(model['original_hpr']), - 'node_path': self._getNodePath(model['node']), - 'parent_name': model['node'].getParent().getName() if model['node'].getParent() else None, - 'node_type': type(model['node']).__name__ - } for model in self.config_data['models'] - ], - 'steps': self.config_data['steps'], - 'tools': self.config_data['tools'], - 'settings': { - 'auto_play_audio': self.auto_audio_check.isChecked(), - 'show_hint_text': self.show_hint_check.isChecked(), - 'require_confirmation': self.confirm_check.isChecked() - } - } - - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(save_data, f, ensure_ascii=False, indent=2) - - QMessageBox.information(self, "成功", "配置保存成功") - - except Exception as e: - QMessageBox.critical(self, "错误", f"保存配置失败: {str(e)}") - - def loadConfiguration(self): - """加载配置""" - file_path, _ = QFileDialog.getOpenFileName( - self, "加载拆装配置", "", - "JSON文件 (*.json);;所有文件 (*)" - ) - - if file_path: - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - - self.loadConfigurationData(data) - QMessageBox.information(self, "成功", "配置加载成功") - - except Exception as e: - QMessageBox.critical(self, "错误", f"加载配置失败: {str(e)}") - - def loadConfigurationData(self, data): - """加载配置数据到界面""" - # 清空现有配置 - self.config_models_list.clear() - self.target_model_combo.clear() - self.steps_list.clear() - - # 清空配置数据中的模型信息 - self.config_data['models'] = [] - - # 重置当前编辑的步骤项 - self._current_step_item = None - - # 加载模型配置 - self._loadModelConfiguration(data.get('models', [])) - - # 加载步骤配置 - self.config_data['steps'] = data.get('steps', []) - for step_data in self.config_data['steps']: - step_name = step_data['name'] - step_score = step_data.get('score', 10) - display_text = f"{step_name} ({step_score}分)" - list_item = QListWidgetItem(display_text) - list_item.setData(Qt.UserRole, step_data) - self.steps_list.addItem(list_item) - - # 加载工具配置 - self.tools_list.clear() - self.required_tool_combo.clear() - self.required_tool_combo.addItem("无") - - self.config_data['tools'] = data.get('tools', []) - for tool in self.config_data['tools']: - self.tools_list.addItem(tool['name']) - self.required_tool_combo.addItem(tool['name']) - - # 加载全局设置 - settings = data.get('settings', {}) - self.auto_audio_check.setChecked(settings.get('auto_play_audio', True)) - self.show_hint_check.setChecked(settings.get('show_hint_text', True)) - self.confirm_check.setChecked(settings.get('require_confirmation', False)) - - def _loadModelConfiguration(self, saved_models): - """加载模型配置,通过路径精确匹配场景中的模型节点""" - if not saved_models or not self.world: - return - - # 遍历保存的模型配置 - for saved_model in saved_models: - model_name = saved_model.get('name', '') - node_path = saved_model.get('node_path', '') - parent_name = saved_model.get('parent_name', '') - - scene_node = None - - # 方法1: 优先使用路径匹配(最精确) - if node_path: - scene_node = self._findNodeByPath(self.world.render, node_path) - if scene_node: - print(f"通过路径找到模型: {model_name} -> {node_path}") - - # 方法2: 如果路径匹配失败,尝试名称+父节点匹配 - if not scene_node and parent_name: - scene_node = self._findNodeByNameAndParent(self.world.render, model_name, parent_name) - if scene_node: - print(f"通过名称+父节点找到模型: {model_name} (父节点: {parent_name})") - - # 方法3: 最后尝试简单的名称匹配 - if not scene_node: - scene_model_map = {} - self._buildSceneModelMap(self.world.render, scene_model_map) - if model_name in scene_model_map: - scene_node = scene_model_map[model_name] - print(f"通过名称找到模型: {model_name}") - - # 如果找到了对应的节点,添加到配置中 - if scene_node: - # 添加到已配置模型列表 - list_item = QListWidgetItem(model_name) - list_item.setData(Qt.UserRole, scene_node) - self.config_models_list.addItem(list_item) - - # 添加到目标模型下拉框 - self.target_model_combo.addItem(model_name) - - # 更新config_data中的模型信息,保留原始位置信息 - original_pos = saved_model.get('original_pos', [0, 0, 0]) - original_hpr = saved_model.get('original_hpr', [0, 0, 0]) - - self.config_data['models'].append({ - 'name': model_name, - 'node': scene_node, - 'original_pos': tuple(original_pos), - 'original_hpr': tuple(original_hpr) - }) - else: - # 如果场景中找不到对应的模型,给出详细提示 - print(f"警告: 无法在场景中找到模型 '{model_name}'") - if node_path: - print(f" - 尝试的路径: {node_path}") - if parent_name: - print(f" - 预期父节点: {parent_name}") - print(f" - 请检查场景结构是否发生变化") - - def _buildSceneModelMap(self, node, model_map): - """递归构建场景模型名称到节点的映射""" - if node.getName() in ['render', 'camera', 'aspect2d', 'pixel2d']: - # 跳过系统节点,但遍历其子节点 - for child in node.getChildren(): - self._buildSceneModelMap(child, model_map) - return - - # 将当前节点添加到映射中 - model_name = node.getName() - model_map[model_name] = node - - # 递归处理子节点 - for child in node.getChildren(): - self._buildSceneModelMap(child, model_map) - - def _getNodePath(self, node): - """获取节点的完整路径,用于精确定位""" - if not node: - return "" - - path_parts = [] - current_node = node - - # 从当前节点向上遍历到根节点,构建路径 - while current_node and current_node.getName() != 'render': - path_parts.append(current_node.getName()) - current_node = current_node.getParent() - - # 反转路径,从根到叶 - path_parts.reverse() - return '/'.join(path_parts) if path_parts else node.getName() - - def _findNodeByPath(self, root_node, node_path): - """根据路径在场景中查找节点""" - if not node_path: - return None - - # 分割路径 - path_parts = node_path.split('/') - current_node = root_node - - # 沿着路径查找 - for part in path_parts: - found = False - for child in current_node.getChildren(): - if child.getName() == part: - current_node = child - found = True - break - - if not found: - return None - - return current_node - - def _findNodeByNameAndParent(self, root_node, node_name, parent_name): - """根据节点名称和父节点名称查找节点""" - def search_recursive(node): - # 检查当前节点的所有子节点 - for child in node.getChildren(): - # 如果找到匹配的节点名称和父节点名称 - if (child.getName() == node_name and - child.getParent() and - child.getParent().getName() == parent_name): - return child - - # 递归搜索子节点 - result = search_recursive(child) - if result: - return result - - return None - - return search_recursive(root_node) - - def previewConfiguration(self): - """预览配置效果""" - QMessageBox.information(self, "提示", "预览功能待实现") - - def applyConfiguration(self): - """应用配置""" +# -*- coding: utf-8 -*- +""" +拆装交互配置界面 - 简化版本 +""" + +from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, + QWidget, QLabel, QListWidget, QListWidgetItem, + QPushButton, QComboBox, QLineEdit, QTextEdit, + QGroupBox, QGridLayout, QCheckBox, QSplitter, + QTreeWidget, QTreeWidgetItem, QMessageBox, + QFileDialog, QScrollArea, QDoubleSpinBox) +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFont +import json + + +class AssemblyDisassemblyConfigDialog(QDialog): + """拆装配置主对话框 - 简化版本""" + + def __init__(self, parent=None, world=None): + super().__init__(parent) + self.world = world + self.config_data = { + 'models': [], # 参与拆装的模型列表 + 'steps': [], # 拆装步骤配置 + 'tools': [], # 使用的工具列表 + 'settings': {} # 全局设置 + } + + self.setupUI() + self.loadSceneModels() + + # 初始化当前编辑的步骤项 + self._current_step_item = None + + def setupUI(self): + """设置界面""" + self.setWindowTitle("拆装交互配置") + self.setModal(False) + self.resize(1000, 700) + + # 主布局 + main_layout = QVBoxLayout(self) + + # 标题 + title_label = QLabel("模型拆装交互配置") + title_label.setFont(QFont("", 16, QFont.Bold)) + title_label.setAlignment(Qt.AlignCenter) + main_layout.addWidget(title_label) + + # 创建标签页 + self.tab_widget = QTabWidget() + main_layout.addWidget(self.tab_widget) + + # 模型选择标签页 + self.models_tab = self.createModelsTab() + self.tab_widget.addTab(self.models_tab, "模型选择") + + # 步骤配置标签页 + self.steps_tab = self.createStepsTab() + self.tab_widget.addTab(self.steps_tab, "步骤配置") + + # 工具配置标签页 + self.tools_tab = self.createToolsTab() + self.tab_widget.addTab(self.tools_tab, "工具配置") + + # 全局设置标签页 + self.settings_tab = self.createSettingsTab() + self.tab_widget.addTab(self.settings_tab, "全局设置") + + # 底部按钮 + button_layout = QHBoxLayout() + + self.save_button = QPushButton("保存配置") + self.load_button = QPushButton("加载配置") + self.preview_button = QPushButton("预览效果") + self.apply_button = QPushButton("应用配置") + + button_layout.addWidget(self.save_button) + button_layout.addWidget(self.load_button) + button_layout.addStretch() + button_layout.addWidget(self.preview_button) + button_layout.addWidget(self.apply_button) + + main_layout.addLayout(button_layout) + + # 连接信号 + self.connectSignals() + + def createModelsTab(self): + """创建模型选择标签页""" + widget = QWidget() + layout = QVBoxLayout(widget) + + # 分割器 + splitter = QSplitter(Qt.Horizontal) + layout.addWidget(splitter) + + # 左侧 - 场景模型 + left_group = QGroupBox("场景模型") + left_layout = QVBoxLayout(left_group) + + # 添加搜索框 + search_layout = QHBoxLayout() + search_layout.addWidget(QLabel("搜索模型:")) + self.model_search_edit = QLineEdit() + self.model_search_edit.setPlaceholderText("输入模型名称进行搜索...") + search_layout.addWidget(self.model_search_edit) + left_layout.addLayout(search_layout) + + self.scene_models_tree = QTreeWidget() + self.scene_models_tree.setHeaderLabel("场景中的模型") + left_layout.addWidget(self.scene_models_tree) + + # 操作按钮 + button_layout = QHBoxLayout() + self.add_model_button = QPushButton("添加到配置 →") + self.remove_model_button = QPushButton("← 从配置移除") + button_layout.addWidget(self.add_model_button) + button_layout.addWidget(self.remove_model_button) + left_layout.addLayout(button_layout) + + splitter.addWidget(left_group) + + # 右侧 - 已配置模型 + right_group = QGroupBox("已配置模型") + right_layout = QVBoxLayout(right_group) + + self.config_models_list = QListWidget() + right_layout.addWidget(self.config_models_list) + + splitter.addWidget(right_group) + + return widget + + def createStepsTab(self): + """创建步骤配置标签页""" + widget = QWidget() + layout = QVBoxLayout(widget) + + # 分割器 + splitter = QSplitter(Qt.Horizontal) + layout.addWidget(splitter) + + # 左侧 - 步骤列表 + left_group = QGroupBox("拆装步骤") + left_layout = QVBoxLayout(left_group) + + # 步骤操作按钮 + step_buttons = QHBoxLayout() + self.add_step_button = QPushButton("添加步骤") + self.remove_step_button = QPushButton("删除步骤") + self.move_up_button = QPushButton("上移") + self.move_down_button = QPushButton("下移") + + step_buttons.addWidget(self.add_step_button) + step_buttons.addWidget(self.remove_step_button) + step_buttons.addWidget(self.move_up_button) + step_buttons.addWidget(self.move_down_button) + left_layout.addLayout(step_buttons) + + self.steps_list = QListWidget() + left_layout.addWidget(self.steps_list) + + splitter.addWidget(left_group) + + # 右侧 - 步骤详细配置 + right_group = QGroupBox("步骤详细配置") + right_layout = QVBoxLayout(right_group) + + # 滚动区域 + scroll_area = QScrollArea() + scroll_area.setWidgetResizable(True) + scroll_content = QWidget() + scroll_layout = QVBoxLayout(scroll_content) + + # 基本信息 + basic_group = QGroupBox("基本信息") + basic_layout = QGridLayout(basic_group) + + basic_layout.addWidget(QLabel("步骤名称:"), 0, 0) + self.step_name_edit = QLineEdit() + basic_layout.addWidget(self.step_name_edit, 0, 1) + + basic_layout.addWidget(QLabel("步骤描述:"), 1, 0) + self.step_desc_edit = QTextEdit() + self.step_desc_edit.setMaximumHeight(80) + basic_layout.addWidget(self.step_desc_edit, 1, 1) + + basic_layout.addWidget(QLabel("步骤类型:"), 2, 0) + self.step_type_combo = QComboBox() + self.step_type_combo.addItems(["拆卸", "安装"]) + basic_layout.addWidget(self.step_type_combo, 2, 1) + + # 步骤分值(考核模式用) + basic_layout.addWidget(QLabel("步骤分值:"), 3, 0) + self.step_score_spin = QDoubleSpinBox() + self.step_score_spin.setRange(0, 100) + self.step_score_spin.setValue(10) # 默认10分 + self.step_score_spin.setSingleStep(1) + self.step_score_spin.setDecimals(0) + self.step_score_spin.setSuffix(" 分") + basic_layout.addWidget(self.step_score_spin, 3, 1) + + # 添加分值说明 + score_note = QLabel("注:在考核模式下,此分值将用于计算该步骤的得分。") + score_note.setWordWrap(True) + score_note.setStyleSheet("color: #666; font-style: italic; font-size: 10px;") + basic_layout.addWidget(score_note, 4, 0, 1, 2) + + scroll_layout.addWidget(basic_group) + + # 目标模型 + target_group = QGroupBox("目标模型") + target_layout = QGridLayout(target_group) + + target_layout.addWidget(QLabel("目标模型:"), 0, 0) + self.target_model_combo = QComboBox() + target_layout.addWidget(self.target_model_combo, 0, 1) + + target_layout.addWidget(QLabel("交互方式:"), 1, 0) + self.interaction_combo = QComboBox() + self.interaction_combo.addItems(["鼠标拖拽", "点击触发"]) + target_layout.addWidget(self.interaction_combo, 1, 1) + + scroll_layout.addWidget(target_group) + + # 位置配置 + pos_group = QGroupBox("位置配置") + pos_layout = QGridLayout(pos_group) + + # 添加说明文字 + pos_note = QLabel("注:拖拽交互支持自由拖拽,无轴向限制。以下为参考目标位置:") + pos_note.setWordWrap(True) + pos_note.setStyleSheet("color: #666; font-style: italic;") + pos_layout.addWidget(pos_note, 0, 0, 1, 2) + + pos_layout.addWidget(QLabel("参考位置 X:"), 1, 0) + self.pos_x_spin = QDoubleSpinBox() + self.pos_x_spin.setRange(-1000, 1000) + pos_layout.addWidget(self.pos_x_spin, 1, 1) + + pos_layout.addWidget(QLabel("参考位置 Y:"), 2, 0) + self.pos_y_spin = QDoubleSpinBox() + self.pos_y_spin.setRange(-1000, 1000) + pos_layout.addWidget(self.pos_y_spin, 2, 1) + + pos_layout.addWidget(QLabel("参考位置 Z:"), 3, 0) + self.pos_z_spin = QDoubleSpinBox() + self.pos_z_spin.setRange(-1000, 1000) + pos_layout.addWidget(self.pos_z_spin, 3, 1) + + scroll_layout.addWidget(pos_group) + + # 动画配置(仅点击触发模式) + animation_group = QGroupBox("动画配置") + animation_layout = QGridLayout(animation_group) + + # 动画时长 + animation_layout.addWidget(QLabel("动画时长(秒):"), 0, 0) + self.animation_duration_spin = QDoubleSpinBox() + self.animation_duration_spin.setRange(0.1, 10.0) + self.animation_duration_spin.setValue(2.0) + self.animation_duration_spin.setSingleStep(0.1) + self.animation_duration_spin.setDecimals(1) + animation_layout.addWidget(self.animation_duration_spin, 0, 1) + + # 说明文字 + animation_note = QLabel("注:仅在'点击触发'交互方式下有效,用于控制模型自动移动的速度。") + animation_note.setWordWrap(True) + animation_note.setStyleSheet("color: #666; font-style: italic;") + animation_layout.addWidget(animation_note, 1, 0, 1, 2) + + scroll_layout.addWidget(animation_group) + + # 提示配置 + hint_group = QGroupBox("提示配置") + hint_layout = QGridLayout(hint_group) + + hint_layout.addWidget(QLabel("提示音频:"), 0, 0) + audio_layout = QHBoxLayout() + self.audio_edit = QLineEdit() + self.browse_audio_button = QPushButton("浏览...") + audio_layout.addWidget(self.audio_edit) + audio_layout.addWidget(self.browse_audio_button) + hint_layout.addLayout(audio_layout, 0, 1) + + hint_layout.addWidget(QLabel("提示文字:"), 1, 0) + self.hint_text_edit = QTextEdit() + self.hint_text_edit.setMaximumHeight(60) + hint_layout.addWidget(self.hint_text_edit, 1, 1) + + scroll_layout.addWidget(hint_group) + + # 工具配置 + tool_group = QGroupBox("使用工具") + tool_layout = QGridLayout(tool_group) + + tool_layout.addWidget(QLabel("所需工具:"), 0, 0) + self.required_tool_combo = QComboBox() + self.required_tool_combo.addItem("无") + tool_layout.addWidget(self.required_tool_combo, 0, 1) + + scroll_layout.addWidget(tool_group) + + scroll_area.setWidget(scroll_content) + right_layout.addWidget(scroll_area) + + splitter.addWidget(right_group) + + return widget + + def createToolsTab(self): + """创建工具配置标签页""" + widget = QWidget() + layout = QVBoxLayout(widget) + + # 说明文字 + info_label = QLabel("配置拆装过程中需要使用的工具:") + layout.addWidget(info_label) + + # 工具添加 + add_layout = QHBoxLayout() + add_layout.addWidget(QLabel("工具名称:")) + self.tool_name_edit = QLineEdit() + self.add_tool_button = QPushButton("添加工具") + add_layout.addWidget(self.tool_name_edit) + add_layout.addWidget(self.add_tool_button) + add_layout.addStretch() + layout.addLayout(add_layout) + + # 工具列表 + self.tools_list = QListWidget() + layout.addWidget(self.tools_list) + + # 工具操作按钮 + tool_buttons = QHBoxLayout() + self.edit_tool_button = QPushButton("编辑工具") + self.delete_tool_button = QPushButton("删除工具") + tool_buttons.addWidget(self.edit_tool_button) + tool_buttons.addWidget(self.delete_tool_button) + tool_buttons.addStretch() + layout.addLayout(tool_buttons) + + return widget + + def createSettingsTab(self): + """创建全局设置标签页""" + widget = QWidget() + layout = QVBoxLayout(widget) + + # 说明文字 + info_label = QLabel("配置拆装交互的全局设置:") + layout.addWidget(info_label) + + # 设置组 + settings_group = QGroupBox("全局设置") + settings_layout = QGridLayout(settings_group) + + # 自动播放提示音 + self.auto_audio_check = QCheckBox("自动播放步骤提示音") + self.auto_audio_check.setChecked(True) + settings_layout.addWidget(self.auto_audio_check, 0, 0, 1, 2) + + # 显示提示文字 + self.show_hint_check = QCheckBox("显示步骤提示文字") + self.show_hint_check.setChecked(True) + settings_layout.addWidget(self.show_hint_check, 1, 0, 1, 2) + + # 步骤完成确认 + self.confirm_check = QCheckBox("步骤完成需要确认") + settings_layout.addWidget(self.confirm_check, 2, 0, 1, 2) + + layout.addWidget(settings_group) + + # 预设配置 + presets_group = QGroupBox("预设配置") + presets_layout = QVBoxLayout(presets_group) + + self.presets_list = QListWidget() + presets_layout.addWidget(self.presets_list) + + preset_buttons = QHBoxLayout() + self.save_preset_button = QPushButton("保存为预设") + self.load_preset_button = QPushButton("加载预设") + self.delete_preset_button = QPushButton("删除预设") + preset_buttons.addWidget(self.save_preset_button) + preset_buttons.addWidget(self.load_preset_button) + preset_buttons.addWidget(self.delete_preset_button) + presets_layout.addLayout(preset_buttons) + + layout.addWidget(presets_group) + + layout.addStretch() + + return widget + + def connectSignals(self): + """连接信号槽""" + # 模型搜索 + self.model_search_edit.textChanged.connect(self.filterSceneModels) + + # 模型操作 + self.add_model_button.clicked.connect(self.addModelsToConfig) + self.remove_model_button.clicked.connect(self.removeModelsFromConfig) + + # 步骤操作 + self.add_step_button.clicked.connect(self.addStep) + self.remove_step_button.clicked.connect(self.removeStep) + self.move_up_button.clicked.connect(self.moveStepUp) + self.move_down_button.clicked.connect(self.moveStepDown) + self.steps_list.itemSelectionChanged.connect(self.onStepSelectionChanged) + + # 步骤配置实时保存 - 使用专门的连接方法 + self.connectStepConfigSignals() + + # 工具操作 + self.add_tool_button.clicked.connect(self.addTool) + self.edit_tool_button.clicked.connect(self.editTool) + self.delete_tool_button.clicked.connect(self.deleteTool) + + # 文件操作 + self.browse_audio_button.clicked.connect(self.browseAudioFile) + + # 按钮操作 + self.save_button.clicked.connect(self.saveConfiguration) + self.load_button.clicked.connect(self.loadConfiguration) + self.preview_button.clicked.connect(self.previewConfiguration) + self.apply_button.clicked.connect(self.applyConfiguration) + + def loadSceneModels(self): + """加载场景中的模型""" + if not self.world: + return + + self.scene_models_tree.clear() + self._addNodeToTree(self.world.render, self.scene_models_tree) + self.scene_models_tree.expandAll() + + def _addNodeToTree(self, node, parent_item): + """递归添加节点到树形控件""" + if node.getName() in ['render', 'camera', 'aspect2d', 'pixel2d']: + for child in node.getChildren(): + self._addNodeToTree(child, parent_item) + return + + if isinstance(parent_item, QTreeWidget): + item = QTreeWidgetItem(parent_item) + else: + item = QTreeWidgetItem(parent_item) + + item.setText(0, node.getName()) + item.setData(0, Qt.UserRole, node) + + for child in node.getChildren(): + self._addNodeToTree(child, item) + + def filterSceneModels(self, search_text): + """根据搜索文本过滤场景模型""" + def filterItems(item): + item_text = item.text(0).lower() + search_lower = search_text.lower() + + # 检查当前项是否匹配 + matches = search_lower in item_text if search_text else True + + # 检查子项 + child_matches = False + for i in range(item.childCount()): + child_item = item.child(i) + if filterItems(child_item): + child_matches = True + + # 显示/隐藏项 + item.setHidden(not (matches or child_matches)) + + return matches or child_matches + + # 从根项开始过滤 + for i in range(self.scene_models_tree.topLevelItemCount()): + filterItems(self.scene_models_tree.topLevelItem(i)) + + def addModelsToConfig(self): + """添加选中的模型到配置""" + selected_items = self.scene_models_tree.selectedItems() + + for item in selected_items: + node = item.data(0, Qt.UserRole) + if node: + model_name = node.getName() + existing_items = self.config_models_list.findItems(model_name, Qt.MatchExactly) + if not existing_items: + list_item = QListWidgetItem(model_name) + list_item.setData(Qt.UserRole, node) + self.config_models_list.addItem(list_item) + + self.config_data['models'].append({ + 'name': model_name, + 'node': node, + 'original_pos': node.getPos(), + 'original_hpr': node.getHpr() + }) + + self.target_model_combo.addItem(model_name) + + def removeModelsFromConfig(self): + """从配置中移除选中的模型""" + selected_items = self.config_models_list.selectedItems() + + for item in selected_items: + model_name = item.text() + row = self.config_models_list.row(item) + self.config_models_list.takeItem(row) + + self.config_data['models'] = [ + model for model in self.config_data['models'] + if model['name'] != model_name + ] + + index = self.target_model_combo.findText(model_name) + if index >= 0: + self.target_model_combo.removeItem(index) + + def addStep(self): + """添加步骤""" + step_count = len(self.config_data['steps']) + 1 + step_name = f"步骤 {step_count}" + + step_data = { + 'name': step_name, + 'description': '', + 'type': '拆卸', + 'target_model': '', + 'interaction_type': '鼠标拖拽', + 'target_position': [0, 0, 0], + 'audio_file': '', + 'hint_text': '', + 'required_tool': '无', + 'score': 10 # 默认分值 + } + + self.config_data['steps'].append(step_data) + + # 显示步骤名称和分值 + display_text = f"{step_name} ({step_data['score']}分)" + list_item = QListWidgetItem(display_text) + list_item.setData(Qt.UserRole, step_data) + self.steps_list.addItem(list_item) + + # 设置当前编辑的步骤项并选中 + self._current_step_item = list_item + self.steps_list.setCurrentItem(list_item) + + # 清空界面并显示新步骤的默认配置 + self.loadStepConfiguration(step_data) + + def removeStep(self): + """删除步骤""" + current_item = self.steps_list.currentItem() + if not current_item: + QMessageBox.warning(self, "警告", "请选择要删除的步骤") + return + + row = self.steps_list.row(current_item) + self.steps_list.takeItem(row) + self.config_data['steps'].pop(row) + + # 如果删除的是当前编辑的步骤,清空当前步骤项 + if self._current_step_item == current_item: + self._current_step_item = None + + self.renumberSteps() + + def moveStepUp(self): + """上移步骤""" + current_row = self.steps_list.currentRow() + if current_row <= 0: + return + + self.config_data['steps'][current_row], self.config_data['steps'][current_row - 1] = \ + self.config_data['steps'][current_row - 1], self.config_data['steps'][current_row] + + current_item = self.steps_list.takeItem(current_row) + self.steps_list.insertItem(current_row - 1, current_item) + self.steps_list.setCurrentRow(current_row - 1) + self.renumberSteps() + + def moveStepDown(self): + """下移步骤""" + current_row = self.steps_list.currentRow() + if current_row >= self.steps_list.count() - 1: + return + + self.config_data['steps'][current_row], self.config_data['steps'][current_row + 1] = \ + self.config_data['steps'][current_row + 1], self.config_data['steps'][current_row] + + current_item = self.steps_list.takeItem(current_row) + self.steps_list.insertItem(current_row + 1, current_item) + self.steps_list.setCurrentRow(current_row + 1) + self.renumberSteps() + + def renumberSteps(self): + """重新编号步骤""" + for i in range(self.steps_list.count()): + item = self.steps_list.item(i) + step_data = item.data(Qt.UserRole) + new_name = f"步骤 {i + 1}" + step_data['name'] = new_name + step_score = step_data.get('score', 10) + display_text = f"{new_name} ({step_score}分)" + item.setText(display_text) + + def onStepSelectionChanged(self): + """步骤选择改变""" + # 简单直接的切换逻辑 + current_item = self.steps_list.currentItem() + if not current_item: + self._current_step_item = None + self._clearStepConfigurationUI() + return + + # 记录当前步骤项并加载配置 + self._current_step_item = current_item + self.loadStepConfiguration(current_item.data(Qt.UserRole)) + + def _clearStepConfigurationUI(self): + """清空步骤配置界面""" + self.disconnectStepConfigSignals() + try: + self.step_name_edit.setText("") + self.step_desc_edit.setPlainText("") + self.step_type_combo.setCurrentIndex(0) + self.target_model_combo.setCurrentIndex(0) + self.interaction_combo.setCurrentIndex(0) + self.pos_x_spin.setValue(0) + self.pos_y_spin.setValue(0) + self.pos_z_spin.setValue(0) + self.audio_edit.setText("") + self.hint_text_edit.setPlainText("") + self.required_tool_combo.setCurrentIndex(0) + finally: + self.connectStepConfigSignals() + + def loadStepConfiguration(self, step_data): + """加载步骤配置到界面""" + # 临时断开信号,避免加载时触发保存 + self.disconnectStepConfigSignals() + try: + # 加载所有配置项到界面 + self.step_name_edit.setText(step_data.get('name', '')) + self.step_desc_edit.setPlainText(step_data.get('description', '')) + self.step_type_combo.setCurrentText(step_data.get('type', '拆卸')) + + target_model = step_data.get('target_model', '') + index = self.target_model_combo.findText(target_model) + if index >= 0: + self.target_model_combo.setCurrentIndex(index) + else: + self.target_model_combo.setCurrentIndex(0) + + self.interaction_combo.setCurrentText(step_data.get('interaction_type', '鼠标拖拽')) + + target_pos = step_data.get('target_position', [0, 0, 0]) + self.pos_x_spin.setValue(target_pos[0]) + self.pos_y_spin.setValue(target_pos[1]) + self.pos_z_spin.setValue(target_pos[2]) + + # 加载动画时长 + self.animation_duration_spin.setValue(step_data.get('animation_duration', 2.0)) + + self.audio_edit.setText(step_data.get('audio_file', '')) + self.hint_text_edit.setPlainText(step_data.get('hint_text', '')) + + # 加载工具配置 + required_tool = step_data.get('required_tool', '无') + index = self.required_tool_combo.findText(required_tool) + if index >= 0: + self.required_tool_combo.setCurrentIndex(index) + else: + self.required_tool_combo.setCurrentIndex(0) + + # 加载分值配置 + self.step_score_spin.setValue(step_data.get('score', 10)) + + finally: + # 确保信号重新连接 + self.connectStepConfigSignals() + + def disconnectStepConfigSignals(self): + """断开步骤配置的信号连接""" + try: + # 精确断开特定的信号连接 + self.step_name_edit.textChanged.disconnect(self.saveCurrentStepConfig) + self.step_desc_edit.textChanged.disconnect(self.saveCurrentStepConfig) + self.step_type_combo.currentTextChanged.disconnect(self.saveCurrentStepConfig) + self.target_model_combo.currentTextChanged.disconnect(self.saveCurrentStepConfig) + self.interaction_combo.currentTextChanged.disconnect(self.saveCurrentStepConfig) + self.pos_x_spin.valueChanged.disconnect(self.saveCurrentStepConfig) + self.pos_y_spin.valueChanged.disconnect(self.saveCurrentStepConfig) + self.pos_z_spin.valueChanged.disconnect(self.saveCurrentStepConfig) + self.animation_duration_spin.valueChanged.disconnect(self.saveCurrentStepConfig) + self.audio_edit.textChanged.disconnect(self.saveCurrentStepConfig) + self.hint_text_edit.textChanged.disconnect(self.saveCurrentStepConfig) + self.required_tool_combo.currentTextChanged.disconnect(self.saveCurrentStepConfig) + self.step_score_spin.valueChanged.disconnect(self.saveCurrentStepConfig) + except: + # 忽略断开连接时的错误 + pass + + def connectStepConfigSignals(self): + """连接步骤配置的信号""" + # 重新连接信号到保存方法 + self.step_name_edit.textChanged.connect(self.saveCurrentStepConfig) + self.step_desc_edit.textChanged.connect(self.saveCurrentStepConfig) + self.step_type_combo.currentTextChanged.connect(self.saveCurrentStepConfig) + self.target_model_combo.currentTextChanged.connect(self.saveCurrentStepConfig) + self.interaction_combo.currentTextChanged.connect(self.saveCurrentStepConfig) + self.pos_x_spin.valueChanged.connect(self.saveCurrentStepConfig) + self.pos_y_spin.valueChanged.connect(self.saveCurrentStepConfig) + self.pos_z_spin.valueChanged.connect(self.saveCurrentStepConfig) + self.animation_duration_spin.valueChanged.connect(self.saveCurrentStepConfig) + self.audio_edit.textChanged.connect(self.saveCurrentStepConfig) + self.hint_text_edit.textChanged.connect(self.saveCurrentStepConfig) + self.required_tool_combo.currentTextChanged.connect(self.saveCurrentStepConfig) + self.step_score_spin.valueChanged.connect(self.saveCurrentStepConfig) + + def saveCurrentStepConfig(self): + """保存当前步骤的配置到list_item的UserRole数据中""" + # 检查当前步骤项是否有效 + if not hasattr(self, '_current_step_item') or self._current_step_item is None: + return + + # 获取当前步骤的数据 + step_data = self._current_step_item.data(Qt.UserRole) + if not step_data: + return + + # 从界面控件收集所有配置数据 + step_data['name'] = self.step_name_edit.text() + step_data['description'] = self.step_desc_edit.toPlainText() + step_data['type'] = self.step_type_combo.currentText() + step_data['target_model'] = self.target_model_combo.currentText() + step_data['interaction_type'] = self.interaction_combo.currentText() + step_data['target_position'] = [ + self.pos_x_spin.value(), + self.pos_y_spin.value(), + self.pos_z_spin.value() + ] + step_data['animation_duration'] = self.animation_duration_spin.value() + step_data['audio_file'] = self.audio_edit.text() + step_data['hint_text'] = self.hint_text_edit.toPlainText() + step_data['required_tool'] = self.required_tool_combo.currentText() + step_data['score'] = int(self.step_score_spin.value()) # 保存分值 + + # 确保数据更新到list_item的UserRole中 + self._current_step_item.setData(Qt.UserRole, step_data) + + # 更新列表项的显示文本,包含分值信息 + step_name = step_data['name'] + step_score = step_data.get('score', 10) + display_text = f"{step_name} ({step_score}分)" + self._current_step_item.setText(display_text) + + # 同时更新config_data中的对应数据(保持数据一致性) + current_row = self.steps_list.row(self._current_step_item) + if 0 <= current_row < len(self.config_data['steps']): + self.config_data['steps'][current_row] = step_data + + def addTool(self): + """添加工具""" + tool_name = self.tool_name_edit.text().strip() + if not tool_name: + QMessageBox.warning(self, "警告", "请输入工具名称") + return + + existing_items = self.tools_list.findItems(tool_name, Qt.MatchExactly) + if existing_items: + QMessageBox.warning(self, "警告", "工具已存在") + return + + self.tools_list.addItem(tool_name) + self.config_data['tools'].append({'name': tool_name, 'description': ''}) + + # 同时添加到步骤配置的工具下拉框中 + self.required_tool_combo.addItem(tool_name) + + self.tool_name_edit.clear() + + def editTool(self): + """编辑工具""" + current_item = self.tools_list.currentItem() + if not current_item: + QMessageBox.warning(self, "警告", "请选择要编辑的工具") + return + QMessageBox.information(self, "提示", "工具编辑功能待实现") + + def deleteTool(self): + """删除工具""" + current_item = self.tools_list.currentItem() + if not current_item: + QMessageBox.warning(self, "警告", "请选择要删除的工具") + return + + tool_name = current_item.text() + row = self.tools_list.row(current_item) + self.tools_list.takeItem(row) + + self.config_data['tools'] = [ + tool for tool in self.config_data['tools'] + if tool['name'] != tool_name + ] + + # 同时从步骤配置的工具下拉框中移除 + index = self.required_tool_combo.findText(tool_name) + if index >= 0: + self.required_tool_combo.removeItem(index) + + def browseAudioFile(self): + """浏览音频文件""" + file_path, _ = QFileDialog.getOpenFileName( + self, "选择音频文件", "", + "音频文件 (*.wav *.mp3 *.ogg);;所有文件 (*)" + ) + + if file_path: + self.audio_edit.setText(file_path) + + def saveConfiguration(self): + """保存配置""" + file_path, _ = QFileDialog.getSaveFileName( + self, "保存拆装配置", "", + "JSON文件 (*.json);;所有文件 (*)" + ) + + if file_path: + try: + save_data = { + 'models': [ + { + 'name': model['name'], + 'original_pos': list(model['original_pos']), + 'original_hpr': list(model['original_hpr']), + 'node_path': self._getNodePath(model['node']), + 'parent_name': model['node'].getParent().getName() if model['node'].getParent() else None, + 'node_type': type(model['node']).__name__ + } for model in self.config_data['models'] + ], + 'steps': self.config_data['steps'], + 'tools': self.config_data['tools'], + 'settings': { + 'auto_play_audio': self.auto_audio_check.isChecked(), + 'show_hint_text': self.show_hint_check.isChecked(), + 'require_confirmation': self.confirm_check.isChecked() + } + } + + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(save_data, f, ensure_ascii=False, indent=2) + + QMessageBox.information(self, "成功", "配置保存成功") + + except Exception as e: + QMessageBox.critical(self, "错误", f"保存配置失败: {str(e)}") + + def loadConfiguration(self): + """加载配置""" + file_path, _ = QFileDialog.getOpenFileName( + self, "加载拆装配置", "", + "JSON文件 (*.json);;所有文件 (*)" + ) + + if file_path: + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + + self.loadConfigurationData(data) + QMessageBox.information(self, "成功", "配置加载成功") + + except Exception as e: + QMessageBox.critical(self, "错误", f"加载配置失败: {str(e)}") + + def loadConfigurationData(self, data): + """加载配置数据到界面""" + # 清空现有配置 + self.config_models_list.clear() + self.target_model_combo.clear() + self.steps_list.clear() + + # 清空配置数据中的模型信息 + self.config_data['models'] = [] + + # 重置当前编辑的步骤项 + self._current_step_item = None + + # 加载模型配置 + self._loadModelConfiguration(data.get('models', [])) + + # 加载步骤配置 + self.config_data['steps'] = data.get('steps', []) + for step_data in self.config_data['steps']: + step_name = step_data['name'] + step_score = step_data.get('score', 10) + display_text = f"{step_name} ({step_score}分)" + list_item = QListWidgetItem(display_text) + list_item.setData(Qt.UserRole, step_data) + self.steps_list.addItem(list_item) + + # 加载工具配置 + self.tools_list.clear() + self.required_tool_combo.clear() + self.required_tool_combo.addItem("无") + + self.config_data['tools'] = data.get('tools', []) + for tool in self.config_data['tools']: + self.tools_list.addItem(tool['name']) + self.required_tool_combo.addItem(tool['name']) + + # 加载全局设置 + settings = data.get('settings', {}) + self.auto_audio_check.setChecked(settings.get('auto_play_audio', True)) + self.show_hint_check.setChecked(settings.get('show_hint_text', True)) + self.confirm_check.setChecked(settings.get('require_confirmation', False)) + + def _loadModelConfiguration(self, saved_models): + """加载模型配置,通过路径精确匹配场景中的模型节点""" + if not saved_models or not self.world: + return + + # 遍历保存的模型配置 + for saved_model in saved_models: + model_name = saved_model.get('name', '') + node_path = saved_model.get('node_path', '') + parent_name = saved_model.get('parent_name', '') + + scene_node = None + + # 方法1: 优先使用路径匹配(最精确) + if node_path: + scene_node = self._findNodeByPath(self.world.render, node_path) + if scene_node: + print(f"通过路径找到模型: {model_name} -> {node_path}") + + # 方法2: 如果路径匹配失败,尝试名称+父节点匹配 + if not scene_node and parent_name: + scene_node = self._findNodeByNameAndParent(self.world.render, model_name, parent_name) + if scene_node: + print(f"通过名称+父节点找到模型: {model_name} (父节点: {parent_name})") + + # 方法3: 最后尝试简单的名称匹配 + if not scene_node: + scene_model_map = {} + self._buildSceneModelMap(self.world.render, scene_model_map) + if model_name in scene_model_map: + scene_node = scene_model_map[model_name] + print(f"通过名称找到模型: {model_name}") + + # 如果找到了对应的节点,添加到配置中 + if scene_node: + # 添加到已配置模型列表 + list_item = QListWidgetItem(model_name) + list_item.setData(Qt.UserRole, scene_node) + self.config_models_list.addItem(list_item) + + # 添加到目标模型下拉框 + self.target_model_combo.addItem(model_name) + + # 更新config_data中的模型信息,保留原始位置信息 + original_pos = saved_model.get('original_pos', [0, 0, 0]) + original_hpr = saved_model.get('original_hpr', [0, 0, 0]) + + self.config_data['models'].append({ + 'name': model_name, + 'node': scene_node, + 'original_pos': tuple(original_pos), + 'original_hpr': tuple(original_hpr) + }) + else: + # 如果场景中找不到对应的模型,给出详细提示 + print(f"警告: 无法在场景中找到模型 '{model_name}'") + if node_path: + print(f" - 尝试的路径: {node_path}") + if parent_name: + print(f" - 预期父节点: {parent_name}") + print(f" - 请检查场景结构是否发生变化") + + def _buildSceneModelMap(self, node, model_map): + """递归构建场景模型名称到节点的映射""" + if node.getName() in ['render', 'camera', 'aspect2d', 'pixel2d']: + # 跳过系统节点,但遍历其子节点 + for child in node.getChildren(): + self._buildSceneModelMap(child, model_map) + return + + # 将当前节点添加到映射中 + model_name = node.getName() + model_map[model_name] = node + + # 递归处理子节点 + for child in node.getChildren(): + self._buildSceneModelMap(child, model_map) + + def _getNodePath(self, node): + """获取节点的完整路径,用于精确定位""" + if not node: + return "" + + path_parts = [] + current_node = node + + # 从当前节点向上遍历到根节点,构建路径 + while current_node and current_node.getName() != 'render': + path_parts.append(current_node.getName()) + current_node = current_node.getParent() + + # 反转路径,从根到叶 + path_parts.reverse() + return '/'.join(path_parts) if path_parts else node.getName() + + def _findNodeByPath(self, root_node, node_path): + """根据路径在场景中查找节点""" + if not node_path: + return None + + # 分割路径 + path_parts = node_path.split('/') + current_node = root_node + + # 沿着路径查找 + for part in path_parts: + found = False + for child in current_node.getChildren(): + if child.getName() == part: + current_node = child + found = True + break + + if not found: + return None + + return current_node + + def _findNodeByNameAndParent(self, root_node, node_name, parent_name): + """根据节点名称和父节点名称查找节点""" + def search_recursive(node): + # 检查当前节点的所有子节点 + for child in node.getChildren(): + # 如果找到匹配的节点名称和父节点名称 + if (child.getName() == node_name and + child.getParent() and + child.getParent().getName() == parent_name): + return child + + # 递归搜索子节点 + result = search_recursive(child) + if result: + return result + + return None + + return search_recursive(root_node) + + def previewConfiguration(self): + """预览配置效果""" + QMessageBox.information(self, "提示", "预览功能待实现") + + def applyConfiguration(self): + """应用配置""" QMessageBox.information(self, "提示", "应用配置功能待实现") \ No newline at end of file diff --git a/ui/icon_manager.py b/ui/icon_manager.py index 5d07611d..1a63980e 100644 --- a/ui/icon_manager.py +++ b/ui/icon_manager.py @@ -1,342 +1,342 @@ -""" -图标管理工具 - -负责统一管理应用程序中的所有图标: -- 图标路径解析 -- 图标缓存 -- 图标预加载 -- 图标错误处理 -""" -import os -import sys -from typing import Dict, Optional -from PyQt5.QtGui import QIcon, QPixmap -from PyQt5.QtCore import QSize - - -class IconManager: - """图标管理器类""" - - def __init__(self): - """初始化图标管理器""" - self.icon_cache: Dict[str, QIcon] = {} - self.icon_directory = self._get_icon_directory() - self.default_icon = None - - # 预定义的图标映射 - self.icon_map = { - # 主窗口图标 - 'app_logo': 'logo.png', - - # 工具栏图标 - 'select_tool': 'select_tool.png', - 'move_tool': 'move_tool.png', - 'rotate_tool': 'rotate_tool.png', - 'scale_tool': 'scale_tool.png', - - # 菜单图标(如果有的话) - 'new_file': 'new_file.png', - 'open_file': 'open_file.png', - 'save_file': 'save_file.png', - 'exit': 'exit.png', - - # 对象类型图标 - 'object_3d': 'object_3d.png', - 'light': 'light.png', - 'camera': 'camera.png', - 'terrain': 'terrain.png', - 'script': 'script.png', - - # 状态图标 - 'success': 'success.png', - 'warning': 'warning.png', - 'error': 'error.png', - 'info': 'info.png', - - 'up_arrow': 'up_arrows.png', - 'down_arrow': 'down_arrows.png', - 'left_arrow': 'left_arrows.png', - 'right_arrow': 'right_arrows.png', - - 'solid_down_arrows': 'solid_down_arrows.png', - 'solid_right_arrows': 'solid_right_arrows.png', - - 'minimize_icon': 'minimize_icon.png', - 'windowing_icon': 'windowing_icon.png', - 'close_icon': 'close_icon.png', - - # 弹窗图标 - 'success_icon': 'success_icon.png', - 'warning_icon': 'warning_icon.png', - 'fail_icon': 'delete_fail_icon.png', - - # 属性面板图标 - 'property_select_image': 'property_select_image.png', - } - - # 初始化默认图标 - self._create_default_icon() - - # 预加载常用图标 - self._preload_icons() - - def _get_icon_directory(self) -> str: - """获取图标目录的绝对路径""" - # 获取当前文件的目录(ui目录) - current_dir = os.path.dirname(os.path.abspath(__file__)) - # 获取项目根目录(ui的父目录) - project_root = os.path.dirname(current_dir) - # 拼接icons目录路径 - icon_dir = os.path.join(project_root, "icons") - - print(f"🔍 图标目录路径: {icon_dir}") - - # 检查目录是否存在 - if not os.path.exists(icon_dir): - print(f"⚠️ 图标目录不存在: {icon_dir}") - # 尝试创建目录 - try: - os.makedirs(icon_dir, exist_ok=True) - print(f"✅ 已创建图标目录: {icon_dir}") - except Exception as e: - print(f"❌ 创建图标目录失败: {e}") - - return icon_dir - - def _create_default_icon(self): - """创建默认图标""" - # 创建一个简单的默认图标 - pixmap = QPixmap(16, 16) - pixmap.fill() # 填充为白色 - self.default_icon = QIcon(pixmap) - - def _preload_icons(self): - """预加载常用图标""" - print("🔄 开始预加载图标...") - - for icon_name, file_name in self.icon_map.items(): - icon_path = os.path.join(self.icon_directory, file_name) - if os.path.exists(icon_path): - try: - icon = QIcon(icon_path) - self.icon_cache[icon_name] = icon - print(f"✅ 已加载图标: {icon_name} -> {file_name}") - except Exception as e: - print(f"❌ 加载图标失败: {icon_name} -> {file_name}, 错误: {e}") - else: - print(f"⚠️ 图标文件不存在: {icon_path}") - - print(f"📊 预加载完成,共加载 {len(self.icon_cache)} 个图标") - - def get_icon(self, icon_name: str, size: Optional[QSize] = None) -> QIcon: - """ - 获取图标 - - Args: - icon_name: 图标名称(可以是预定义名称或文件名) - size: 图标尺寸 - - Returns: - QIcon对象 - """ - # 首先检查缓存 - if icon_name in self.icon_cache: - icon = self.icon_cache[icon_name] - if size: - # 如果指定了尺寸,返回指定尺寸的图标 - pixmap = icon.pixmap(size) - return QIcon(pixmap) - return icon - - # 如果不在缓存中,尝试从映射中获取 - if icon_name in self.icon_map: - file_name = self.icon_map[icon_name] - icon_path = os.path.join(self.icon_directory, file_name) - else: - # 直接使用文件名 - icon_path = os.path.join(self.icon_directory, icon_name) - if not icon_name.endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')): - icon_path += '.png' # 默认添加.png扩展名 - - # 尝试加载图标 - if os.path.exists(icon_path): - try: - icon = QIcon(icon_path) - # 缓存图标 - self.icon_cache[icon_name] = icon - print(f"✅ 动态加载图标: {icon_name} -> {os.path.basename(icon_path)}") - - if size: - pixmap = icon.pixmap(size) - return QIcon(pixmap) - return icon - except Exception as e: - print(f"❌ 加载图标失败: {icon_path}, 错误: {e}") - else: - print(f"⚠️ 图标文件不存在: {icon_path}") - - # 返回默认图标 - return self.default_icon - - def get_icon_path(self, icon_name: str) -> str: - """ - 获取图标文件的完整路径 - - Args: - icon_name: 图标名称 - - Returns: - 图标文件的完整路径 - """ - if icon_name in self.icon_map: - file_name = self.icon_map[icon_name] - else: - file_name = icon_name - if not file_name.endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')): - file_name += '.png' - - icon_path = os.path.join(self.icon_directory, file_name) - - if os.path.exists(icon_path): - return icon_path - else: - print(f"⚠️ 图标文件不存在: {icon_path}") - return "" - - def has_icon(self, icon_name: str) -> bool: - """ - 检查图标是否存在 - - Args: - icon_name: 图标名称 - - Returns: - 是否存在 - """ - return bool(self.get_icon_path(icon_name)) - - def add_icon(self, icon_name: str, icon_path: str) -> bool: - """ - 添加新图标到缓存 - - Args: - icon_name: 图标名称 - icon_path: 图标文件路径 - - Returns: - 是否添加成功 - """ - try: - if os.path.exists(icon_path): - icon = QIcon(icon_path) - self.icon_cache[icon_name] = icon - print(f"✅ 已添加图标到缓存: {icon_name} -> {icon_path}") - return True - else: - print(f"❌ 图标文件不存在: {icon_path}") - return False - except Exception as e: - print(f"❌ 添加图标失败: {icon_name} -> {icon_path}, 错误: {e}") - return False - - def refresh_cache(self): - """刷新图标缓存""" - print("🔄 刷新图标缓存...") - self.icon_cache.clear() - self._preload_icons() - - def get_available_icons(self) -> list: - """获取所有可用的图标列表""" - available_icons = [] - - # 添加预定义的图标 - available_icons.extend(self.icon_map.keys()) - - # 扫描图标目录中的所有图标文件 - if os.path.exists(self.icon_directory): - for file_name in os.listdir(self.icon_directory): - if file_name.lower().endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')): - icon_name = os.path.splitext(file_name)[0] - if icon_name not in available_icons: - available_icons.append(icon_name) - - return sorted(available_icons) - - def get_cache_info(self) -> dict: - """获取缓存信息""" - return { - 'cached_icons': len(self.icon_cache), - 'icon_directory': self.icon_directory, - 'available_icons': len(self.get_available_icons()), - 'cache_keys': list(self.icon_cache.keys()) - } - - def debug_info(self): - """打印调试信息""" - print("=" * 50) - print("📋 图标管理器调试信息") - print("=" * 50) - - info = self.get_cache_info() - print(f"图标目录: {info['icon_directory']}") - print(f"目录存在: {os.path.exists(info['icon_directory'])}") - print(f"缓存图标数: {info['cached_icons']}") - print(f"可用图标数: {info['available_icons']}") - - if info['cache_keys']: - print("\n已缓存的图标:") - for key in info['cache_keys']: - print(f" - {key}") - - print("\n图标目录内容:") - if os.path.exists(self.icon_directory): - for file_name in os.listdir(self.icon_directory): - file_path = os.path.join(self.icon_directory, file_name) - size = os.path.getsize(file_path) if os.path.isfile(file_path) else 0 - print(f" - {file_name} ({size} bytes)") - else: - print(" 目录不存在") - - print("=" * 50) - - -# 全局图标管理器实例 -_icon_manager = None - - -def get_icon_manager() -> IconManager: - """获取全局图标管理器实例""" - global _icon_manager - if _icon_manager is None: - _icon_manager = IconManager() - return _icon_manager - - -def get_icon(icon_name: str, size: Optional[QSize] = None) -> QIcon: - """便捷函数:获取图标""" - return get_icon_manager().get_icon(icon_name, size) - - -def get_icon_path(icon_name: str) -> str: - """便捷函数:获取图标路径""" - return get_icon_manager().get_icon_path(icon_name) - - -def has_icon(icon_name: str) -> bool: - """便捷函数:检查图标是否存在""" - return get_icon_manager().has_icon(icon_name) - - -if __name__ == "__main__": - # 测试代码 - print("🧪 测试图标管理器...") - - manager = IconManager() - manager.debug_info() - - # 测试获取图标 - logo_icon = manager.get_icon('app_logo') - print(f"\n📱 应用图标是否有效: {not logo_icon.isNull()}") - - move_tool_icon = manager.get_icon('move_tool') +""" +图标管理工具 + +负责统一管理应用程序中的所有图标: +- 图标路径解析 +- 图标缓存 +- 图标预加载 +- 图标错误处理 +""" +import os +import sys +from typing import Dict, Optional +from PyQt5.QtGui import QIcon, QPixmap +from PyQt5.QtCore import QSize + + +class IconManager: + """图标管理器类""" + + def __init__(self): + """初始化图标管理器""" + self.icon_cache: Dict[str, QIcon] = {} + self.icon_directory = self._get_icon_directory() + self.default_icon = None + + # 预定义的图标映射 + self.icon_map = { + # 主窗口图标 + 'app_logo': 'logo.png', + + # 工具栏图标 + 'select_tool': 'select_tool.png', + 'move_tool': 'move_tool.png', + 'rotate_tool': 'rotate_tool.png', + 'scale_tool': 'scale_tool.png', + + # 菜单图标(如果有的话) + 'new_file': 'new_file.png', + 'open_file': 'open_file.png', + 'save_file': 'save_file.png', + 'exit': 'exit.png', + + # 对象类型图标 + 'object_3d': 'object_3d.png', + 'light': 'light.png', + 'camera': 'camera.png', + 'terrain': 'terrain.png', + 'script': 'script.png', + + # 状态图标 + 'success': 'success.png', + 'warning': 'warning.png', + 'error': 'error.png', + 'info': 'info.png', + + 'up_arrow': 'up_arrows.png', + 'down_arrow': 'down_arrows.png', + 'left_arrow': 'left_arrows.png', + 'right_arrow': 'right_arrows.png', + + 'solid_down_arrows': 'solid_down_arrows.png', + 'solid_right_arrows': 'solid_right_arrows.png', + + 'minimize_icon': 'minimize_icon.png', + 'windowing_icon': 'windowing_icon.png', + 'close_icon': 'close_icon.png', + + # 弹窗图标 + 'success_icon': 'success_icon.png', + 'warning_icon': 'warning_icon.png', + 'fail_icon': 'delete_fail_icon.png', + + # 属性面板图标 + 'property_select_image': 'property_select_image.png', + } + + # 初始化默认图标 + self._create_default_icon() + + # 预加载常用图标 + self._preload_icons() + + def _get_icon_directory(self) -> str: + """获取图标目录的绝对路径""" + # 获取当前文件的目录(ui目录) + current_dir = os.path.dirname(os.path.abspath(__file__)) + # 获取项目根目录(ui的父目录) + project_root = os.path.dirname(current_dir) + # 拼接icons目录路径 + icon_dir = os.path.join(project_root, "icons") + + print(f"🔍 图标目录路径: {icon_dir}") + + # 检查目录是否存在 + if not os.path.exists(icon_dir): + print(f"⚠️ 图标目录不存在: {icon_dir}") + # 尝试创建目录 + try: + os.makedirs(icon_dir, exist_ok=True) + print(f"✅ 已创建图标目录: {icon_dir}") + except Exception as e: + print(f"❌ 创建图标目录失败: {e}") + + return icon_dir + + def _create_default_icon(self): + """创建默认图标""" + # 创建一个简单的默认图标 + pixmap = QPixmap(16, 16) + pixmap.fill() # 填充为白色 + self.default_icon = QIcon(pixmap) + + def _preload_icons(self): + """预加载常用图标""" + print("🔄 开始预加载图标...") + + for icon_name, file_name in self.icon_map.items(): + icon_path = os.path.join(self.icon_directory, file_name) + if os.path.exists(icon_path): + try: + icon = QIcon(icon_path) + self.icon_cache[icon_name] = icon + print(f"✅ 已加载图标: {icon_name} -> {file_name}") + except Exception as e: + print(f"❌ 加载图标失败: {icon_name} -> {file_name}, 错误: {e}") + else: + print(f"⚠️ 图标文件不存在: {icon_path}") + + print(f"📊 预加载完成,共加载 {len(self.icon_cache)} 个图标") + + def get_icon(self, icon_name: str, size: Optional[QSize] = None) -> QIcon: + """ + 获取图标 + + Args: + icon_name: 图标名称(可以是预定义名称或文件名) + size: 图标尺寸 + + Returns: + QIcon对象 + """ + # 首先检查缓存 + if icon_name in self.icon_cache: + icon = self.icon_cache[icon_name] + if size: + # 如果指定了尺寸,返回指定尺寸的图标 + pixmap = icon.pixmap(size) + return QIcon(pixmap) + return icon + + # 如果不在缓存中,尝试从映射中获取 + if icon_name in self.icon_map: + file_name = self.icon_map[icon_name] + icon_path = os.path.join(self.icon_directory, file_name) + else: + # 直接使用文件名 + icon_path = os.path.join(self.icon_directory, icon_name) + if not icon_name.endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')): + icon_path += '.png' # 默认添加.png扩展名 + + # 尝试加载图标 + if os.path.exists(icon_path): + try: + icon = QIcon(icon_path) + # 缓存图标 + self.icon_cache[icon_name] = icon + print(f"✅ 动态加载图标: {icon_name} -> {os.path.basename(icon_path)}") + + if size: + pixmap = icon.pixmap(size) + return QIcon(pixmap) + return icon + except Exception as e: + print(f"❌ 加载图标失败: {icon_path}, 错误: {e}") + else: + print(f"⚠️ 图标文件不存在: {icon_path}") + + # 返回默认图标 + return self.default_icon + + def get_icon_path(self, icon_name: str) -> str: + """ + 获取图标文件的完整路径 + + Args: + icon_name: 图标名称 + + Returns: + 图标文件的完整路径 + """ + if icon_name in self.icon_map: + file_name = self.icon_map[icon_name] + else: + file_name = icon_name + if not file_name.endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')): + file_name += '.png' + + icon_path = os.path.join(self.icon_directory, file_name) + + if os.path.exists(icon_path): + return icon_path + else: + print(f"⚠️ 图标文件不存在: {icon_path}") + return "" + + def has_icon(self, icon_name: str) -> bool: + """ + 检查图标是否存在 + + Args: + icon_name: 图标名称 + + Returns: + 是否存在 + """ + return bool(self.get_icon_path(icon_name)) + + def add_icon(self, icon_name: str, icon_path: str) -> bool: + """ + 添加新图标到缓存 + + Args: + icon_name: 图标名称 + icon_path: 图标文件路径 + + Returns: + 是否添加成功 + """ + try: + if os.path.exists(icon_path): + icon = QIcon(icon_path) + self.icon_cache[icon_name] = icon + print(f"✅ 已添加图标到缓存: {icon_name} -> {icon_path}") + return True + else: + print(f"❌ 图标文件不存在: {icon_path}") + return False + except Exception as e: + print(f"❌ 添加图标失败: {icon_name} -> {icon_path}, 错误: {e}") + return False + + def refresh_cache(self): + """刷新图标缓存""" + print("🔄 刷新图标缓存...") + self.icon_cache.clear() + self._preload_icons() + + def get_available_icons(self) -> list: + """获取所有可用的图标列表""" + available_icons = [] + + # 添加预定义的图标 + available_icons.extend(self.icon_map.keys()) + + # 扫描图标目录中的所有图标文件 + if os.path.exists(self.icon_directory): + for file_name in os.listdir(self.icon_directory): + if file_name.lower().endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')): + icon_name = os.path.splitext(file_name)[0] + if icon_name not in available_icons: + available_icons.append(icon_name) + + return sorted(available_icons) + + def get_cache_info(self) -> dict: + """获取缓存信息""" + return { + 'cached_icons': len(self.icon_cache), + 'icon_directory': self.icon_directory, + 'available_icons': len(self.get_available_icons()), + 'cache_keys': list(self.icon_cache.keys()) + } + + def debug_info(self): + """打印调试信息""" + print("=" * 50) + print("📋 图标管理器调试信息") + print("=" * 50) + + info = self.get_cache_info() + print(f"图标目录: {info['icon_directory']}") + print(f"目录存在: {os.path.exists(info['icon_directory'])}") + print(f"缓存图标数: {info['cached_icons']}") + print(f"可用图标数: {info['available_icons']}") + + if info['cache_keys']: + print("\n已缓存的图标:") + for key in info['cache_keys']: + print(f" - {key}") + + print("\n图标目录内容:") + if os.path.exists(self.icon_directory): + for file_name in os.listdir(self.icon_directory): + file_path = os.path.join(self.icon_directory, file_name) + size = os.path.getsize(file_path) if os.path.isfile(file_path) else 0 + print(f" - {file_name} ({size} bytes)") + else: + print(" 目录不存在") + + print("=" * 50) + + +# 全局图标管理器实例 +_icon_manager = None + + +def get_icon_manager() -> IconManager: + """获取全局图标管理器实例""" + global _icon_manager + if _icon_manager is None: + _icon_manager = IconManager() + return _icon_manager + + +def get_icon(icon_name: str, size: Optional[QSize] = None) -> QIcon: + """便捷函数:获取图标""" + return get_icon_manager().get_icon(icon_name, size) + + +def get_icon_path(icon_name: str) -> str: + """便捷函数:获取图标路径""" + return get_icon_manager().get_icon_path(icon_name) + + +def has_icon(icon_name: str) -> bool: + """便捷函数:检查图标是否存在""" + return get_icon_manager().has_icon(icon_name) + + +if __name__ == "__main__": + # 测试代码 + print("🧪 测试图标管理器...") + + manager = IconManager() + manager.debug_info() + + # 测试获取图标 + logo_icon = manager.get_icon('app_logo') + print(f"\n📱 应用图标是否有效: {not logo_icon.isNull()}") + + move_tool_icon = manager.get_icon('move_tool') print(f"🔧 移动工具图标是否有效: {not move_tool_icon.isNull()}") \ No newline at end of file diff --git a/ui/icon_manager_gui.py b/ui/icon_manager_gui.py index bda6524a..48fdedb2 100644 --- a/ui/icon_manager_gui.py +++ b/ui/icon_manager_gui.py @@ -1,405 +1,405 @@ -""" -图标管理器GUI工具 - -提供图形界面来管理和查看图标: -- 显示所有可用图标 -- 图标预览 -- 图标信息 -- 图标刷新 -""" -import os -from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton, - QListWidget, QListWidgetItem, QLabel, QGroupBox, - QTextEdit, QSplitter, QDialog, QDialogButtonBox, - QFileDialog, QMessageBox, QScrollArea, QGridLayout) -from PyQt5.QtGui import QIcon, QPixmap, QFont -from PyQt5.QtCore import Qt, QSize - -from ui.icon_manager import get_icon_manager - - -class IconPreviewWidget(QWidget): - """图标预览控件""" - - def __init__(self): - super().__init__() - self.setupUI() - - def setupUI(self): - """设置UI""" - layout = QVBoxLayout(self) - - # 图标显示区域 - self.icon_label = QLabel() - self.icon_label.setAlignment(Qt.AlignCenter) - self.icon_label.setStyleSheet(""" - QLabel { - border: 2px dashed #8b5cf6; - border-radius: 8px; - background-color: #2d2d44; - color: #e0e0ff; - min-height: 100px; - margin: 10px; - } - """) - self.icon_label.setText("选择图标查看预览") - layout.addWidget(self.icon_label) - - # 图标信息 - self.info_label = QLabel() - self.info_label.setStyleSheet(""" - QLabel { - background-color: #252538; - color: #e0e0ff; - padding: 10px; - border-radius: 4px; - font-family: monospace; - } - """) - self.info_label.setText("图标信息将在此显示") - layout.addWidget(self.info_label) - - def showIcon(self, icon_name: str, icon: QIcon): - """显示图标""" - if not icon.isNull(): - # 显示不同尺寸的图标 - sizes = [16, 24, 32, 48, 64] - pixmaps = [] - - # 创建组合图标显示 - for size in sizes: - pixmap = icon.pixmap(QSize(size, size)) - if not pixmap.isNull(): - pixmaps.append((size, pixmap)) - - if pixmaps: - # 创建合成图片显示多个尺寸 - total_width = sum(size for size, _ in pixmaps) + 20 * (len(pixmaps) - 1) - max_height = max(size for size, _ in pixmaps) - - combined_pixmap = QPixmap(total_width, max_height + 40) - combined_pixmap.fill(Qt.transparent) - - from PyQt5.QtGui import QPainter, QPen - painter = QPainter(combined_pixmap) - - x = 0 - for size, pixmap in pixmaps: - # 绘制图标 - y = (max_height - size) // 2 - painter.drawPixmap(x, y, pixmap) - - # 绘制尺寸标签 - painter.setPen(QPen(Qt.white)) - painter.drawText(x, max_height + 15, f"{size}x{size}") - - x += size + 20 - - painter.end() - - self.icon_label.setPixmap(combined_pixmap) - else: - self.icon_label.setText("无法加载图标") - else: - self.icon_label.setText("图标无效") - - # 更新信息 - info_text = f"图标名称: {icon_name}\n" - info_text += f"图标有效: {'是' if not icon.isNull() else '否'}\n" - - # 获取图标管理器信息 - icon_manager = get_icon_manager() - icon_path = icon_manager.get_icon_path(icon_name) - if icon_path: - info_text += f"文件路径: {icon_path}\n" - if os.path.exists(icon_path): - size = os.path.getsize(icon_path) - info_text += f"文件大小: {size} bytes\n" - - # 获取可用尺寸 - if not icon.isNull(): - available_sizes = icon.availableSizes() - if available_sizes: - sizes_text = ", ".join(f"{s.width()}x{s.height()}" for s in available_sizes) - info_text += f"可用尺寸: {sizes_text}\n" - - self.info_label.setText(info_text) - - -class IconManagerDialog(QDialog): - """图标管理器对话框""" - - def __init__(self, parent=None): - super().__init__(parent) - self.icon_manager = get_icon_manager() - self.setupUI() - self.loadIcons() - - def setupUI(self): - """设置UI""" - self.setWindowTitle("图标管理器") - self.setModal(False) - self.resize(800, 600) - - # 设置样式 - self.setStyleSheet(""" - QDialog { - background-color: #1e1e2e; - color: #e0e0ff; - } - QListWidget { - background-color: #252538; - color: #e0e0ff; - border: 1px solid #3a3a4a; - border-radius: 4px; - alternate-background-color: #2d2d44; - } - QListWidget::item { - padding: 8px; - border-bottom: 1px solid #3a3a4a; - } - QListWidget::item:hover { - background-color: #3a3a4a; - } - QListWidget::item:selected { - background-color: rgba(139, 92, 246, 100); - color: white; - } - QPushButton { - background-color: #8b5cf6; - color: white; - border: none; - padding: 8px 16px; - border-radius: 4px; - font-weight: 500; - } - QPushButton:hover { - background-color: #7c3aed; - } - QPushButton:pressed { - background-color: #6d28d9; - } - QGroupBox { - background-color: #252538; - border: 1px solid #3a3a4a; - border-radius: 6px; - margin-top: 1ex; - color: #e0e0ff; - font-weight: 500; - padding-top: 10px; - } - QGroupBox::title { - subline-offset: -2px; - padding: 0 8px; - color: #c0c0e0; - font-weight: 500; - } - QTextEdit { - background-color: #252538; - color: #e0e0ff; - border: 1px solid #3a3a4a; - border-radius: 4px; - font-family: monospace; - } - """) - - layout = QVBoxLayout(self) - - # 顶部按钮栏 - button_layout = QHBoxLayout() - - self.refresh_btn = QPushButton("刷新图标") - self.refresh_btn.clicked.connect(self.refreshIcons) - button_layout.addWidget(self.refresh_btn) - - self.add_icon_btn = QPushButton("添加图标") - self.add_icon_btn.clicked.connect(self.addIcon) - button_layout.addWidget(self.add_icon_btn) - - self.debug_btn = QPushButton("调试信息") - self.debug_btn.clicked.connect(self.showDebugInfo) - button_layout.addWidget(self.debug_btn) - - button_layout.addStretch() - layout.addLayout(button_layout) - - # 主分割器 - splitter = QSplitter(Qt.Horizontal) - - # 左侧:图标列表 - left_widget = QWidget() - left_layout = QVBoxLayout(left_widget) - - list_group = QGroupBox("可用图标") - list_layout = QVBoxLayout(list_group) - - self.icon_list = QListWidget() - self.icon_list.itemSelectionChanged.connect(self.onIconSelected) - list_layout.addWidget(self.icon_list) - - left_layout.addWidget(list_group) - splitter.addWidget(left_widget) - - # 右侧:图标预览 - right_widget = QWidget() - right_layout = QVBoxLayout(right_widget) - - preview_group = QGroupBox("图标预览") - preview_layout = QVBoxLayout(preview_group) - - self.preview_widget = IconPreviewWidget() - preview_layout.addWidget(self.preview_widget) - - right_layout.addWidget(preview_group) - splitter.addWidget(right_widget) - - # 设置分割器比例 - splitter.setSizes([300, 500]) - layout.addWidget(splitter) - - # 底部按钮 - button_box = QDialogButtonBox(QDialogButtonBox.Close) - button_box.rejected.connect(self.close) - layout.addWidget(button_box) - - def loadIcons(self): - """加载图标列表""" - self.icon_list.clear() - - available_icons = self.icon_manager.get_available_icons() - - for icon_name in available_icons: - item = QListWidgetItem() - - # 获取图标 - icon = self.icon_manager.get_icon(icon_name, QSize(24, 24)) - - # 设置图标和文本 - if not icon.isNull(): - item.setIcon(icon) - item.setText(f"🎨 {icon_name}") - else: - item.setText(f"❌ {icon_name}") - - item.setData(Qt.UserRole, icon_name) - self.icon_list.addItem(item) - - print(f"📊 加载了 {len(available_icons)} 个图标") - - def onIconSelected(self): - """当选择图标时""" - current_item = self.icon_list.currentItem() - if current_item: - icon_name = current_item.data(Qt.UserRole) - icon = self.icon_manager.get_icon(icon_name) - self.preview_widget.showIcon(icon_name, icon) - - def refreshIcons(self): - """刷新图标""" - print("🔄 刷新图标缓存...") - self.icon_manager.refresh_cache() - self.loadIcons() - QMessageBox.information(self, "完成", "图标缓存已刷新") - - def addIcon(self): - """添加新图标""" - file_path, _ = QFileDialog.getOpenFileName( - self, - "选择图标文件", - "", - "图像文件 (*.png *.jpg *.jpeg *.svg *.ico);;所有文件 (*)" - ) - - if file_path: - # 获取文件名作为图标名称 - file_name = os.path.basename(file_path) - icon_name = os.path.splitext(file_name)[0] - - # 复制文件到图标目录 - import shutil - target_path = os.path.join(self.icon_manager.icon_directory, file_name) - - try: - shutil.copy2(file_path, target_path) - - # 添加到缓存 - success = self.icon_manager.add_icon(icon_name, target_path) - - if success: - QMessageBox.information(self, "成功", f"图标 '{icon_name}' 已添加") - self.loadIcons() - else: - QMessageBox.warning(self, "失败", "添加图标失败") - - except Exception as e: - QMessageBox.critical(self, "错误", f"复制文件失败:\n{str(e)}") - - def showDebugInfo(self): - """显示调试信息""" - debug_dialog = QDialog(self) - debug_dialog.setWindowTitle("图标管理器调试信息") - debug_dialog.resize(600, 400) - - layout = QVBoxLayout(debug_dialog) - - text_edit = QTextEdit() - text_edit.setFont(QFont("Consolas", 10)) - - # 获取调试信息 - info = self.icon_manager.get_cache_info() - debug_text = "图标管理器调试信息\n" - debug_text += "=" * 50 + "\n\n" - debug_text += f"图标目录: {info['icon_directory']}\n" - debug_text += f"目录存在: {os.path.exists(info['icon_directory'])}\n" - debug_text += f"缓存图标数: {info['cached_icons']}\n" - debug_text += f"可用图标数: {info['available_icons']}\n\n" - - debug_text += "已缓存的图标:\n" - for key in info['cache_keys']: - debug_text += f" - {key}\n" - - debug_text += "\n图标目录内容:\n" - if os.path.exists(info['icon_directory']): - for file_name in os.listdir(info['icon_directory']): - file_path = os.path.join(info['icon_directory'], file_name) - if os.path.isfile(file_path): - size = os.path.getsize(file_path) - debug_text += f" - {file_name} ({size} bytes)\n" - else: - debug_text += " 目录不存在\n" - - text_edit.setPlainText(debug_text) - layout.addWidget(text_edit) - - button_box = QDialogButtonBox(QDialogButtonBox.Close) - button_box.rejected.connect(debug_dialog.close) - layout.addWidget(button_box) - - debug_dialog.exec_() - - -def show_icon_manager(parent=None): - """显示图标管理器对话框""" - dialog = IconManagerDialog(parent) - dialog.show() - return dialog - - -if __name__ == "__main__": - from PyQt5.QtWidgets import QApplication - import sys - - app = QApplication(sys.argv) - - # 设置全局样式 - app.setStyleSheet(""" - QApplication { - background-color: #1e1e2e; - color: #e0e0ff; - } - """) - - dialog = IconManagerDialog() - dialog.show() - +""" +图标管理器GUI工具 + +提供图形界面来管理和查看图标: +- 显示所有可用图标 +- 图标预览 +- 图标信息 +- 图标刷新 +""" +import os +from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QPushButton, + QListWidget, QListWidgetItem, QLabel, QGroupBox, + QTextEdit, QSplitter, QDialog, QDialogButtonBox, + QFileDialog, QMessageBox, QScrollArea, QGridLayout) +from PyQt5.QtGui import QIcon, QPixmap, QFont +from PyQt5.QtCore import Qt, QSize + +from ui.icon_manager import get_icon_manager + + +class IconPreviewWidget(QWidget): + """图标预览控件""" + + def __init__(self): + super().__init__() + self.setupUI() + + def setupUI(self): + """设置UI""" + layout = QVBoxLayout(self) + + # 图标显示区域 + self.icon_label = QLabel() + self.icon_label.setAlignment(Qt.AlignCenter) + self.icon_label.setStyleSheet(""" + QLabel { + border: 2px dashed #8b5cf6; + border-radius: 8px; + background-color: #2d2d44; + color: #e0e0ff; + min-height: 100px; + margin: 10px; + } + """) + self.icon_label.setText("选择图标查看预览") + layout.addWidget(self.icon_label) + + # 图标信息 + self.info_label = QLabel() + self.info_label.setStyleSheet(""" + QLabel { + background-color: #252538; + color: #e0e0ff; + padding: 10px; + border-radius: 4px; + font-family: monospace; + } + """) + self.info_label.setText("图标信息将在此显示") + layout.addWidget(self.info_label) + + def showIcon(self, icon_name: str, icon: QIcon): + """显示图标""" + if not icon.isNull(): + # 显示不同尺寸的图标 + sizes = [16, 24, 32, 48, 64] + pixmaps = [] + + # 创建组合图标显示 + for size in sizes: + pixmap = icon.pixmap(QSize(size, size)) + if not pixmap.isNull(): + pixmaps.append((size, pixmap)) + + if pixmaps: + # 创建合成图片显示多个尺寸 + total_width = sum(size for size, _ in pixmaps) + 20 * (len(pixmaps) - 1) + max_height = max(size for size, _ in pixmaps) + + combined_pixmap = QPixmap(total_width, max_height + 40) + combined_pixmap.fill(Qt.transparent) + + from PyQt5.QtGui import QPainter, QPen + painter = QPainter(combined_pixmap) + + x = 0 + for size, pixmap in pixmaps: + # 绘制图标 + y = (max_height - size) // 2 + painter.drawPixmap(x, y, pixmap) + + # 绘制尺寸标签 + painter.setPen(QPen(Qt.white)) + painter.drawText(x, max_height + 15, f"{size}x{size}") + + x += size + 20 + + painter.end() + + self.icon_label.setPixmap(combined_pixmap) + else: + self.icon_label.setText("无法加载图标") + else: + self.icon_label.setText("图标无效") + + # 更新信息 + info_text = f"图标名称: {icon_name}\n" + info_text += f"图标有效: {'是' if not icon.isNull() else '否'}\n" + + # 获取图标管理器信息 + icon_manager = get_icon_manager() + icon_path = icon_manager.get_icon_path(icon_name) + if icon_path: + info_text += f"文件路径: {icon_path}\n" + if os.path.exists(icon_path): + size = os.path.getsize(icon_path) + info_text += f"文件大小: {size} bytes\n" + + # 获取可用尺寸 + if not icon.isNull(): + available_sizes = icon.availableSizes() + if available_sizes: + sizes_text = ", ".join(f"{s.width()}x{s.height()}" for s in available_sizes) + info_text += f"可用尺寸: {sizes_text}\n" + + self.info_label.setText(info_text) + + +class IconManagerDialog(QDialog): + """图标管理器对话框""" + + def __init__(self, parent=None): + super().__init__(parent) + self.icon_manager = get_icon_manager() + self.setupUI() + self.loadIcons() + + def setupUI(self): + """设置UI""" + self.setWindowTitle("图标管理器") + self.setModal(False) + self.resize(800, 600) + + # 设置样式 + self.setStyleSheet(""" + QDialog { + background-color: #1e1e2e; + color: #e0e0ff; + } + QListWidget { + background-color: #252538; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + alternate-background-color: #2d2d44; + } + QListWidget::item { + padding: 8px; + border-bottom: 1px solid #3a3a4a; + } + QListWidget::item:hover { + background-color: #3a3a4a; + } + QListWidget::item:selected { + background-color: rgba(139, 92, 246, 100); + color: white; + } + QPushButton { + background-color: #8b5cf6; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: 500; + } + QPushButton:hover { + background-color: #7c3aed; + } + QPushButton:pressed { + background-color: #6d28d9; + } + QGroupBox { + background-color: #252538; + border: 1px solid #3a3a4a; + border-radius: 6px; + margin-top: 1ex; + color: #e0e0ff; + font-weight: 500; + padding-top: 10px; + } + QGroupBox::title { + subline-offset: -2px; + padding: 0 8px; + color: #c0c0e0; + font-weight: 500; + } + QTextEdit { + background-color: #252538; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + font-family: monospace; + } + """) + + layout = QVBoxLayout(self) + + # 顶部按钮栏 + button_layout = QHBoxLayout() + + self.refresh_btn = QPushButton("刷新图标") + self.refresh_btn.clicked.connect(self.refreshIcons) + button_layout.addWidget(self.refresh_btn) + + self.add_icon_btn = QPushButton("添加图标") + self.add_icon_btn.clicked.connect(self.addIcon) + button_layout.addWidget(self.add_icon_btn) + + self.debug_btn = QPushButton("调试信息") + self.debug_btn.clicked.connect(self.showDebugInfo) + button_layout.addWidget(self.debug_btn) + + button_layout.addStretch() + layout.addLayout(button_layout) + + # 主分割器 + splitter = QSplitter(Qt.Horizontal) + + # 左侧:图标列表 + left_widget = QWidget() + left_layout = QVBoxLayout(left_widget) + + list_group = QGroupBox("可用图标") + list_layout = QVBoxLayout(list_group) + + self.icon_list = QListWidget() + self.icon_list.itemSelectionChanged.connect(self.onIconSelected) + list_layout.addWidget(self.icon_list) + + left_layout.addWidget(list_group) + splitter.addWidget(left_widget) + + # 右侧:图标预览 + right_widget = QWidget() + right_layout = QVBoxLayout(right_widget) + + preview_group = QGroupBox("图标预览") + preview_layout = QVBoxLayout(preview_group) + + self.preview_widget = IconPreviewWidget() + preview_layout.addWidget(self.preview_widget) + + right_layout.addWidget(preview_group) + splitter.addWidget(right_widget) + + # 设置分割器比例 + splitter.setSizes([300, 500]) + layout.addWidget(splitter) + + # 底部按钮 + button_box = QDialogButtonBox(QDialogButtonBox.Close) + button_box.rejected.connect(self.close) + layout.addWidget(button_box) + + def loadIcons(self): + """加载图标列表""" + self.icon_list.clear() + + available_icons = self.icon_manager.get_available_icons() + + for icon_name in available_icons: + item = QListWidgetItem() + + # 获取图标 + icon = self.icon_manager.get_icon(icon_name, QSize(24, 24)) + + # 设置图标和文本 + if not icon.isNull(): + item.setIcon(icon) + item.setText(f"🎨 {icon_name}") + else: + item.setText(f"❌ {icon_name}") + + item.setData(Qt.UserRole, icon_name) + self.icon_list.addItem(item) + + print(f"📊 加载了 {len(available_icons)} 个图标") + + def onIconSelected(self): + """当选择图标时""" + current_item = self.icon_list.currentItem() + if current_item: + icon_name = current_item.data(Qt.UserRole) + icon = self.icon_manager.get_icon(icon_name) + self.preview_widget.showIcon(icon_name, icon) + + def refreshIcons(self): + """刷新图标""" + print("🔄 刷新图标缓存...") + self.icon_manager.refresh_cache() + self.loadIcons() + QMessageBox.information(self, "完成", "图标缓存已刷新") + + def addIcon(self): + """添加新图标""" + file_path, _ = QFileDialog.getOpenFileName( + self, + "选择图标文件", + "", + "图像文件 (*.png *.jpg *.jpeg *.svg *.ico);;所有文件 (*)" + ) + + if file_path: + # 获取文件名作为图标名称 + file_name = os.path.basename(file_path) + icon_name = os.path.splitext(file_name)[0] + + # 复制文件到图标目录 + import shutil + target_path = os.path.join(self.icon_manager.icon_directory, file_name) + + try: + shutil.copy2(file_path, target_path) + + # 添加到缓存 + success = self.icon_manager.add_icon(icon_name, target_path) + + if success: + QMessageBox.information(self, "成功", f"图标 '{icon_name}' 已添加") + self.loadIcons() + else: + QMessageBox.warning(self, "失败", "添加图标失败") + + except Exception as e: + QMessageBox.critical(self, "错误", f"复制文件失败:\n{str(e)}") + + def showDebugInfo(self): + """显示调试信息""" + debug_dialog = QDialog(self) + debug_dialog.setWindowTitle("图标管理器调试信息") + debug_dialog.resize(600, 400) + + layout = QVBoxLayout(debug_dialog) + + text_edit = QTextEdit() + text_edit.setFont(QFont("Consolas", 10)) + + # 获取调试信息 + info = self.icon_manager.get_cache_info() + debug_text = "图标管理器调试信息\n" + debug_text += "=" * 50 + "\n\n" + debug_text += f"图标目录: {info['icon_directory']}\n" + debug_text += f"目录存在: {os.path.exists(info['icon_directory'])}\n" + debug_text += f"缓存图标数: {info['cached_icons']}\n" + debug_text += f"可用图标数: {info['available_icons']}\n\n" + + debug_text += "已缓存的图标:\n" + for key in info['cache_keys']: + debug_text += f" - {key}\n" + + debug_text += "\n图标目录内容:\n" + if os.path.exists(info['icon_directory']): + for file_name in os.listdir(info['icon_directory']): + file_path = os.path.join(info['icon_directory'], file_name) + if os.path.isfile(file_path): + size = os.path.getsize(file_path) + debug_text += f" - {file_name} ({size} bytes)\n" + else: + debug_text += " 目录不存在\n" + + text_edit.setPlainText(debug_text) + layout.addWidget(text_edit) + + button_box = QDialogButtonBox(QDialogButtonBox.Close) + button_box.rejected.connect(debug_dialog.close) + layout.addWidget(button_box) + + debug_dialog.exec_() + + +def show_icon_manager(parent=None): + """显示图标管理器对话框""" + dialog = IconManagerDialog(parent) + dialog.show() + return dialog + + +if __name__ == "__main__": + from PyQt5.QtWidgets import QApplication + import sys + + app = QApplication(sys.argv) + + # 设置全局样式 + app.setStyleSheet(""" + QApplication { + background-color: #1e1e2e; + color: #e0e0ff; + } + """) + + dialog = IconManagerDialog() + dialog.show() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/ui/interface_manager.py b/ui/interface_manager.py index 474f0c37..b1c0f651 100644 --- a/ui/interface_manager.py +++ b/ui/interface_manager.py @@ -1,390 +1,390 @@ -from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QMenu, QStyle -from PyQt5.QtGui import QFont -from PyQt5.QtCore import Qt -from PyQt5.sip import delete -from panda3d.core import GeomNode, ModelRoot - - -class InterfaceManager: - """界面管理器 - 处理树形控件和UI交互""" - - def __init__(self, world): - """初始化界面管理器""" - self.world = world - self.treeWidget = None - self._expanded_paths = set() - - def setTreeWidget(self, treeWidget): - """设置树形控件引用并更新场景树""" - self.treeWidget = treeWidget - - # 添加右键菜单 - # self.treeWidget.setContextMenuPolicy(Qt.CustomContextMenu) - # self.treeWidget.customContextMenuRequested.connect(self.showTreeContextMenu) - - # 更新场景树 - self.world.scene_manager.updateSceneTree() - - def onTreeWidgetClicked(self, index): - """处理树形控件点击事件(包括空白区域)""" - # 检查点击的是否是空白区域 - if not self.treeWidget.itemFromIndex(index): # 点击的是空白区域 - self.world.selection.updateSelection(None) - self.world.property_panel.clearPropertyPanel() - print("点击树形控件空白区域,清除选中状态") - - def onTreeCurrentItemChanged(self, current, previous): - """处理树形控件当前选中项改变事件""" - # 当 current 为 None 时,表示点击了空白区域 - if current is None: - self.world.selection.updateSelection(None) - print("点击空白区域,清除选中状态") - # 当 current 不为 None 时,表示选中了某个项目 - else: - # 更新选择状态 - nodePath = current.data(0, Qt.UserRole) - if nodePath: - self.world.selected_np = nodePath - self.world.selection.updateSelection(nodePath) - self.world.property_panel.updatePropertyPanel(current) - print(f"树形控件选中项改变: {current.text(0)}") - - def onTreeItemClicked(self, item, column): - """处理树形控件项目点击事件""" - #print(f"树形控件点击事件触发,item: {item}, column: {column}") - - # 检查是否点击了空白区域 - # 当点击空白区域时,item可能是一个空的QTreeWidgetItem对象 - if not item or (item.text(0) == "" and item.data(0, Qt.UserRole) is None): - self.world.selection.updateSelection(None) - print("点击空白区域,清除选中状态") - return - - self.world.property_panel.updatePropertyPanel(item) - - # 获取节点对象 - nodePath = item.data(0, Qt.UserRole) - if nodePath: - # 更新选择状态 - self.world.selected_np = nodePath - self.world.selection.updateSelection(nodePath) - print(f"树形控件点击: {item.text(0)}") - else: - # 如果没有节点对象,清除选择 - self.world.selection.updateSelection(None) - self.world.property_panel.clearPropertyPanel() - print("点击了无数据项,清除选中状态") - - def isModelOrChild(self, item): - """检查是否是模型节点或其子节点""" - while item and item.parent(): - if item.parent().text(0) == "模型": - return True - item = item.parent() - return False - - def deleteNode(self, nodePath, item): - """删除节点""" - try: - # 如果是灯光节点,直接调用灯光删除方法 - if self.isLightNode(nodePath): - self.deleteLightNode(nodePath, item) - return - - item_data = item.data(0, Qt.UserRole + 1) - if item_data == "terrain": - if hasattr(self.world, 'terrain_manager') and self.world.terrain_manager.terrains: - terrain_to_remove = None - for terrain_info in self.world.terrain_manager.terrains: - if terrain_info['node'] == nodePath: - terrain_to_remove = terrain_info - break - if terrain_to_remove: - self.world.terrain_manager.deleteTerrain(terrain_to_remove) - print(f"成功删除地形节点:{nodePath.getName()}") - # self.updateSceneTree() - # self.world.property_panel.clearPropertyPanel() - # self.world.selection.updateSelection(None) - return - - # 先递归删除所有子节点中的灯光 - self._recursiveRemoveLights(nodePath) - - # 从场景中移除 - self.world.property_panel.removeActorForModel(nodePath) - - # 清除选择状态 - if hasattr(self.world, 'selection'): - if self.world.selection.selectedNode == nodePath: - self.world.selection.clearSelectionBox() - self.world.selection.clearGizmo() - self.world.selection.selectedNode = None - self.world.selection.selectedObject = None - - # 强制删除节点及其所有子节点 - if not nodePath.isEmpty(): - # 先递归删除所有子节点 - children = list(nodePath.getChildren()) - for child in children: - try: - if not child.isEmpty(): - child.removeNode() - except Exception as e: - print(f"删除子节点失败: {str(e)}") - - # 再删除节点本身 - nodePath.removeNode() - - if hasattr(self.world, 'selection'): - self.world.selection.checkAndClearIfTargetDeleted() - - if hasattr(self.world, 'models') and nodePath in self.world.models: - self.world.models.remove(nodePath) - - # 从树形控件中移除 - parentItem = item.parent() - if parentItem: - parentItem.removeChild(item) - - print(f"成功删除节点: {nodePath.getName()}") - - # 清空属性面板和选择框 - self.world.property_panel.clearPropertyPanel() - self.world.selection.updateSelection(None) - - except Exception as e: - print(f"删除节点失败: {str(e)}") - import traceback - traceback.print_exc() - - def _cleanupAllLightsInSubtree(self,parentNode): - try: - if parentNode.isEmpty(): - return - #收集所有子节点 - all_nodes = [] - - def collect_all_nodes(node): - if not node.isEmpty(): - all_nodes.append(node) - for child in node.getChildren(): - collect_all_nodes(child) - - collect_all_nodes(parentNode) - - ligths_processed = 0 - for node in all_nodes: - if self.isLightNode(node): - if hasattr(node,'getPythonTag'): - light_object = node.getPythonTag('rp_light_object') - if light_object and hasattr(self.world,'render_pipeline'): - try: - self.world.render_pipeline.remove_light(light_object) - print(f"✓ 从渲染管线移除灯光: {node.getName()}") - except Exception as e: - print(f"⚠ 从渲染管线移除灯光失败: {str(e)}") - #从灯光列表中移除 - try: - if node in self.world.Spotlight: - self.world.Spotlight.remove(node) - print(f"从Spotlight列表移除{node.getName()}") - if node in self.world.Pointlight: - self.world.Pointlight.remove(node) - print(f"从pointlight列表移除{node.getName()}") - except Exception as e: - print(f"从灯列表移除失败{str(e)}") - ligths_processed += 1 - if ligths_processed>0: - print(f"清理{ligths_processed}个灯光节点") - except Exception as e: - print(f"清理节点树中的灯光失败{str(e)}") - - - def updateSceneTree(self): - """更新场景树显示 - 实际实现""" - if not self.treeWidget: - return - self._expanded_paths.clear() - self._collect_expanded() - - print("\n=== 更新场景树 ===") - self.treeWidget.clear() - - # 创建场景根节点 - sceneRoot = QTreeWidgetItem(self.treeWidget, ['场景']) - sceneRoot.setData(0, Qt.UserRole, self.world.render) - sceneRoot.setData(0, Qt.UserRole + 1, "SCENE_ROOT") - font = QFont("Microsoft YaHei", 10, QFont.Light) # 假设您希望字体大小为10 - sceneRoot.setFont(0, font) - # 添加相机节点 - cameraItem = QTreeWidgetItem(sceneRoot, ['相机']) - cameraItem.setData(0, Qt.UserRole, self.world.cam) - cameraItem.setData(0, Qt.UserRole + 1, "CAMERA_NODE") - print("添加相机节点") - - BLACK_LIST = {'','**','temp','collision'} - - from panda3d.core import CollisionNode - def should_skip(node): - name = node.getName() - return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(),CollisionNode) or isinstance(node.node(),ModelRoot) or name=="" - - def addNodeToTree(node,parentItem,force = False): - if node.isEmpty(): - return - - if not force and should_skip(node): - return - nodeItem = QTreeWidgetItem(parentItem,[node.getName()]) - nodeItem.setData(0,Qt.UserRole,node) - - for child in node.getChildren(): - addNodeToTree(child,nodeItem,force = False) - - for model in self.world.models: - if not model.isEmpty(): - addNodeToTree(model, sceneRoot,force=True) - - # 添加所有GUI元素 - for gui in self.world.gui_elements: - # 检查是否是有效的GUI节点(具有getTag方法的NodePath) - if hasattr(gui, 'getTag') and hasattr(gui, 'getName'): - gui_type = gui.getTag("gui_type") or "unknown" - gui_text = gui.getTag("text") or gui.getTag("gui_text") or "GUI元素" - item = QTreeWidgetItem(sceneRoot, [f"{gui_type}: {gui_text}"]) - item.setData(0, Qt.UserRole, gui) - else: - # 跳过非GUI节点(如QDockWidget等Qt对象) - continue - - # 添加地板节点 - if hasattr(self.world, 'ground') and self.world.ground: - groundItem = QTreeWidgetItem(sceneRoot, ['地板']) - groundItem.setData(0, Qt.UserRole, self.world.ground) - groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE") - - ground_nodes = [ - ('ground2','地板2'), - ('ground3','地板3'), - ('ground4','地板4'), - ('ground5','地板5'), - ('ground6','地板6') - ] - - for attr_name,display_name in ground_nodes: - if hasattr(self.world,attr_name): - ground_node = getattr(self.world,attr_name) - if ground_node: - extraGroundItem = QTreeWidgetItem(sceneRoot,[display_name]) - extraGroundItem.setData(0,Qt.UserRole,ground_node) - extraGroundItem.setData(0,Qt.UserRole+1,"SCENE_NODE") - - - #添加灯光节点 - for light in self.world.Spotlight: - if light: - addNodeToTree(light, sceneRoot, force=True) - - for light in self.world.Pointlight: - if light: - addNodeToTree(light, sceneRoot, force=True) - - # for light in self.world.Spotlight + self.world.Pointlight: - # if not light.isEmpty: - # print(f"33333333333333333333333333333{light}") - # addNodeToTree(light, sceneRoot, force=True) - - #添加 Cesium tilesets - if hasattr(self.world,'scene_manager') and hasattr(self.world.scene_manager,'tilesets'): - for i , tileset_info in enumerate(self.world.scene_manager.tilesets): - tileset_node = tileset_info['node'] - if not tileset_node.isEmpty(): - tileset_url = tileset_info['url'] - tileset_item = QTreeWidgetItem(sceneRoot,[f"Cesium Tileset {i}"]) - tileset_item.setData(0,Qt.UserRole,tileset_node) - addNodeToTree(tileset_node,tileset_item,force=True) - #添加地形节点 - if hasattr(self.world,'terrain_manager') and self.world.terrain_manager.terrains: - for terrain_info in self.world.terrain_manager.terrains: - terrain_node = terrain_info['node'] - terrain_name = terrain_info.get('name','未知地形') - terrain_item = QTreeWidgetItem(sceneRoot,[f"地形:{terrain_name}"]) - terrain_item.setData(0,Qt.UserRole,terrain_node) - terrain_item.setData(0,Qt.UserRole+1,"terrain") # 标记为地形节点 - addNodeToTree(terrain_node,terrain_item,force=True) - - # 展开所有节点 - #self.treeWidget.expandAll() - self._restore_expanded() - print("=== 场景树更新完成 ===\n") - - def findTreeItem(self, node, parentItem): - """在树形控件中查找指定的节点项""" - for i in range(parentItem.childCount()): - item = parentItem.child(i) - itemNode = item.data(0, Qt.UserRole) - if itemNode == node: - return item - # 递归查找子项 - if item.childCount() > 0: - found = self.findTreeItem(node, item) - if found: - return found - return None - - def _collect_expanded(self,item=None,prefix=""): - if item is None: - root = self.treeWidget.invisibleRootItem() - for i in range(root.childCount()): - self._collect_expanded(root.child(i),prefix) - return - path = f"{prefix}/{item.text(0)}" - if item.isExpanded(): - self._expanded_paths.add(path) - - for i in range(item.childCount()): - self._collect_expanded(item.child(i),path) - - def _restore_expanded(self,item=None,prefix=""): - if item is None: - root = self.treeWidget.invisibleRootItem() - for i in range(root.childCount()): - self._restore_expanded(root.child(i),prefix) - return - path = f"{prefix}/{item.text(0)}" - if path in self._expanded_paths: - item.setExpanded(True) - - for i in range(item.childCount()): - self._restore_expanded(item.child(i),path) - - - def syncVisibilityDownward(self): - from collections import deque - - if not self.world.models: - return - q = deque() - - for root in self.world.models: - q.append(root) - - while q: - node = q.popleft() - - visible = node.getPythonTag("visible") - if visible is None: - visible = True - if not visible: - stack = [node] - while stack: - cur = stack.pop() - cur.hide() - - for child in cur.getChildren(): - stack.append(child) - continue - node.show() - - for child in node.getChildren(): - q.append(child) +from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QMenu, QStyle +from PyQt5.QtGui import QFont +from PyQt5.QtCore import Qt +from PyQt5.sip import delete +from panda3d.core import GeomNode, ModelRoot + + +class InterfaceManager: + """界面管理器 - 处理树形控件和UI交互""" + + def __init__(self, world): + """初始化界面管理器""" + self.world = world + self.treeWidget = None + self._expanded_paths = set() + + def setTreeWidget(self, treeWidget): + """设置树形控件引用并更新场景树""" + self.treeWidget = treeWidget + + # 添加右键菜单 + # self.treeWidget.setContextMenuPolicy(Qt.CustomContextMenu) + # self.treeWidget.customContextMenuRequested.connect(self.showTreeContextMenu) + + # 更新场景树 + self.world.scene_manager.updateSceneTree() + + def onTreeWidgetClicked(self, index): + """处理树形控件点击事件(包括空白区域)""" + # 检查点击的是否是空白区域 + if not self.treeWidget.itemFromIndex(index): # 点击的是空白区域 + self.world.selection.updateSelection(None) + self.world.property_panel.clearPropertyPanel() + print("点击树形控件空白区域,清除选中状态") + + def onTreeCurrentItemChanged(self, current, previous): + """处理树形控件当前选中项改变事件""" + # 当 current 为 None 时,表示点击了空白区域 + if current is None: + self.world.selection.updateSelection(None) + print("点击空白区域,清除选中状态") + # 当 current 不为 None 时,表示选中了某个项目 + else: + # 更新选择状态 + nodePath = current.data(0, Qt.UserRole) + if nodePath: + self.world.selected_np = nodePath + self.world.selection.updateSelection(nodePath) + self.world.property_panel.updatePropertyPanel(current) + print(f"树形控件选中项改变: {current.text(0)}") + + def onTreeItemClicked(self, item, column): + """处理树形控件项目点击事件""" + #print(f"树形控件点击事件触发,item: {item}, column: {column}") + + # 检查是否点击了空白区域 + # 当点击空白区域时,item可能是一个空的QTreeWidgetItem对象 + if not item or (item.text(0) == "" and item.data(0, Qt.UserRole) is None): + self.world.selection.updateSelection(None) + print("点击空白区域,清除选中状态") + return + + self.world.property_panel.updatePropertyPanel(item) + + # 获取节点对象 + nodePath = item.data(0, Qt.UserRole) + if nodePath: + # 更新选择状态 + self.world.selected_np = nodePath + self.world.selection.updateSelection(nodePath) + print(f"树形控件点击: {item.text(0)}") + else: + # 如果没有节点对象,清除选择 + self.world.selection.updateSelection(None) + self.world.property_panel.clearPropertyPanel() + print("点击了无数据项,清除选中状态") + + def isModelOrChild(self, item): + """检查是否是模型节点或其子节点""" + while item and item.parent(): + if item.parent().text(0) == "模型": + return True + item = item.parent() + return False + + def deleteNode(self, nodePath, item): + """删除节点""" + try: + # 如果是灯光节点,直接调用灯光删除方法 + if self.isLightNode(nodePath): + self.deleteLightNode(nodePath, item) + return + + item_data = item.data(0, Qt.UserRole + 1) + if item_data == "terrain": + if hasattr(self.world, 'terrain_manager') and self.world.terrain_manager.terrains: + terrain_to_remove = None + for terrain_info in self.world.terrain_manager.terrains: + if terrain_info['node'] == nodePath: + terrain_to_remove = terrain_info + break + if terrain_to_remove: + self.world.terrain_manager.deleteTerrain(terrain_to_remove) + print(f"成功删除地形节点:{nodePath.getName()}") + # self.updateSceneTree() + # self.world.property_panel.clearPropertyPanel() + # self.world.selection.updateSelection(None) + return + + # 先递归删除所有子节点中的灯光 + self._recursiveRemoveLights(nodePath) + + # 从场景中移除 + self.world.property_panel.removeActorForModel(nodePath) + + # 清除选择状态 + if hasattr(self.world, 'selection'): + if self.world.selection.selectedNode == nodePath: + self.world.selection.clearSelectionBox() + self.world.selection.clearGizmo() + self.world.selection.selectedNode = None + self.world.selection.selectedObject = None + + # 强制删除节点及其所有子节点 + if not nodePath.isEmpty(): + # 先递归删除所有子节点 + children = list(nodePath.getChildren()) + for child in children: + try: + if not child.isEmpty(): + child.removeNode() + except Exception as e: + print(f"删除子节点失败: {str(e)}") + + # 再删除节点本身 + nodePath.removeNode() + + if hasattr(self.world, 'selection'): + self.world.selection.checkAndClearIfTargetDeleted() + + if hasattr(self.world, 'models') and nodePath in self.world.models: + self.world.models.remove(nodePath) + + # 从树形控件中移除 + parentItem = item.parent() + if parentItem: + parentItem.removeChild(item) + + print(f"成功删除节点: {nodePath.getName()}") + + # 清空属性面板和选择框 + self.world.property_panel.clearPropertyPanel() + self.world.selection.updateSelection(None) + + except Exception as e: + print(f"删除节点失败: {str(e)}") + import traceback + traceback.print_exc() + + def _cleanupAllLightsInSubtree(self,parentNode): + try: + if parentNode.isEmpty(): + return + #收集所有子节点 + all_nodes = [] + + def collect_all_nodes(node): + if not node.isEmpty(): + all_nodes.append(node) + for child in node.getChildren(): + collect_all_nodes(child) + + collect_all_nodes(parentNode) + + ligths_processed = 0 + for node in all_nodes: + if self.isLightNode(node): + if hasattr(node,'getPythonTag'): + light_object = node.getPythonTag('rp_light_object') + if light_object and hasattr(self.world,'render_pipeline'): + try: + self.world.render_pipeline.remove_light(light_object) + print(f"✓ 从渲染管线移除灯光: {node.getName()}") + except Exception as e: + print(f"⚠ 从渲染管线移除灯光失败: {str(e)}") + #从灯光列表中移除 + try: + if node in self.world.Spotlight: + self.world.Spotlight.remove(node) + print(f"从Spotlight列表移除{node.getName()}") + if node in self.world.Pointlight: + self.world.Pointlight.remove(node) + print(f"从pointlight列表移除{node.getName()}") + except Exception as e: + print(f"从灯列表移除失败{str(e)}") + ligths_processed += 1 + if ligths_processed>0: + print(f"清理{ligths_processed}个灯光节点") + except Exception as e: + print(f"清理节点树中的灯光失败{str(e)}") + + + def updateSceneTree(self): + """更新场景树显示 - 实际实现""" + if not self.treeWidget: + return + self._expanded_paths.clear() + self._collect_expanded() + + print("\n=== 更新场景树 ===") + self.treeWidget.clear() + + # 创建场景根节点 + sceneRoot = QTreeWidgetItem(self.treeWidget, ['场景']) + sceneRoot.setData(0, Qt.UserRole, self.world.render) + sceneRoot.setData(0, Qt.UserRole + 1, "SCENE_ROOT") + font = QFont("Microsoft YaHei", 10, QFont.Light) # 假设您希望字体大小为10 + sceneRoot.setFont(0, font) + # 添加相机节点 + cameraItem = QTreeWidgetItem(sceneRoot, ['相机']) + cameraItem.setData(0, Qt.UserRole, self.world.cam) + cameraItem.setData(0, Qt.UserRole + 1, "CAMERA_NODE") + print("添加相机节点") + + BLACK_LIST = {'','**','temp','collision'} + + from panda3d.core import CollisionNode + def should_skip(node): + name = node.getName() + return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(),CollisionNode) or isinstance(node.node(),ModelRoot) or name=="" + + def addNodeToTree(node,parentItem,force = False): + if node.isEmpty(): + return + + if not force and should_skip(node): + return + nodeItem = QTreeWidgetItem(parentItem,[node.getName()]) + nodeItem.setData(0,Qt.UserRole,node) + + for child in node.getChildren(): + addNodeToTree(child,nodeItem,force = False) + + for model in self.world.models: + if not model.isEmpty(): + addNodeToTree(model, sceneRoot,force=True) + + # 添加所有GUI元素 + for gui in self.world.gui_elements: + # 检查是否是有效的GUI节点(具有getTag方法的NodePath) + if hasattr(gui, 'getTag') and hasattr(gui, 'getName'): + gui_type = gui.getTag("gui_type") or "unknown" + gui_text = gui.getTag("text") or gui.getTag("gui_text") or "GUI元素" + item = QTreeWidgetItem(sceneRoot, [f"{gui_type}: {gui_text}"]) + item.setData(0, Qt.UserRole, gui) + else: + # 跳过非GUI节点(如QDockWidget等Qt对象) + continue + + # 添加地板节点 + if hasattr(self.world, 'ground') and self.world.ground: + groundItem = QTreeWidgetItem(sceneRoot, ['地板']) + groundItem.setData(0, Qt.UserRole, self.world.ground) + groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE") + + ground_nodes = [ + ('ground2','地板2'), + ('ground3','地板3'), + ('ground4','地板4'), + ('ground5','地板5'), + ('ground6','地板6') + ] + + for attr_name,display_name in ground_nodes: + if hasattr(self.world,attr_name): + ground_node = getattr(self.world,attr_name) + if ground_node: + extraGroundItem = QTreeWidgetItem(sceneRoot,[display_name]) + extraGroundItem.setData(0,Qt.UserRole,ground_node) + extraGroundItem.setData(0,Qt.UserRole+1,"SCENE_NODE") + + + #添加灯光节点 + for light in self.world.Spotlight: + if light: + addNodeToTree(light, sceneRoot, force=True) + + for light in self.world.Pointlight: + if light: + addNodeToTree(light, sceneRoot, force=True) + + # for light in self.world.Spotlight + self.world.Pointlight: + # if not light.isEmpty: + # print(f"33333333333333333333333333333{light}") + # addNodeToTree(light, sceneRoot, force=True) + + #添加 Cesium tilesets + if hasattr(self.world,'scene_manager') and hasattr(self.world.scene_manager,'tilesets'): + for i , tileset_info in enumerate(self.world.scene_manager.tilesets): + tileset_node = tileset_info['node'] + if not tileset_node.isEmpty(): + tileset_url = tileset_info['url'] + tileset_item = QTreeWidgetItem(sceneRoot,[f"Cesium Tileset {i}"]) + tileset_item.setData(0,Qt.UserRole,tileset_node) + addNodeToTree(tileset_node,tileset_item,force=True) + #添加地形节点 + if hasattr(self.world,'terrain_manager') and self.world.terrain_manager.terrains: + for terrain_info in self.world.terrain_manager.terrains: + terrain_node = terrain_info['node'] + terrain_name = terrain_info.get('name','未知地形') + terrain_item = QTreeWidgetItem(sceneRoot,[f"地形:{terrain_name}"]) + terrain_item.setData(0,Qt.UserRole,terrain_node) + terrain_item.setData(0,Qt.UserRole+1,"terrain") # 标记为地形节点 + addNodeToTree(terrain_node,terrain_item,force=True) + + # 展开所有节点 + #self.treeWidget.expandAll() + self._restore_expanded() + print("=== 场景树更新完成 ===\n") + + def findTreeItem(self, node, parentItem): + """在树形控件中查找指定的节点项""" + for i in range(parentItem.childCount()): + item = parentItem.child(i) + itemNode = item.data(0, Qt.UserRole) + if itemNode == node: + return item + # 递归查找子项 + if item.childCount() > 0: + found = self.findTreeItem(node, item) + if found: + return found + return None + + def _collect_expanded(self,item=None,prefix=""): + if item is None: + root = self.treeWidget.invisibleRootItem() + for i in range(root.childCount()): + self._collect_expanded(root.child(i),prefix) + return + path = f"{prefix}/{item.text(0)}" + if item.isExpanded(): + self._expanded_paths.add(path) + + for i in range(item.childCount()): + self._collect_expanded(item.child(i),path) + + def _restore_expanded(self,item=None,prefix=""): + if item is None: + root = self.treeWidget.invisibleRootItem() + for i in range(root.childCount()): + self._restore_expanded(root.child(i),prefix) + return + path = f"{prefix}/{item.text(0)}" + if path in self._expanded_paths: + item.setExpanded(True) + + for i in range(item.childCount()): + self._restore_expanded(item.child(i),path) + + + def syncVisibilityDownward(self): + from collections import deque + + if not self.world.models: + return + q = deque() + + for root in self.world.models: + q.append(root) + + while q: + node = q.popleft() + + visible = node.getPythonTag("visible") + if visible is None: + visible = True + if not visible: + stack = [node] + while stack: + cur = stack.pop() + cur.hide() + + for child in cur.getChildren(): + stack.append(child) + continue + node.show() + + for child in node.getChildren(): + q.append(child) diff --git a/ui/maintenance_system.py b/ui/maintenance_system.py index 77d609ea..61479e6f 100644 --- a/ui/maintenance_system.py +++ b/ui/maintenance_system.py @@ -1,658 +1,658 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -维修系统界面模块 -包含登录界面、科目列表、模式选择等功能 -""" - -import os -import json -from PyQt5.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QGroupBox, - QLineEdit, QPushButton, QLabel, QListWidget, QListWidgetItem, - QMessageBox, QRadioButton, QTextEdit, QSplitter -) -from PyQt5.QtCore import Qt, pyqtSignal -from PyQt5.QtGui import QFont, QPixmap - - -class MaintenanceLoginDialog(QDialog): - """维修系统登录对话框""" - - login_success = pyqtSignal() # 登录成功信号 - - def __init__(self, parent=None): - super().__init__(parent) - self.setupUI() - - def setupUI(self): - """设置用户界面""" - self.setWindowTitle("维修系统 - 登录") - self.setFixedSize(400, 300) - self.setModal(True) - - # 应用简化样式 - 确保可见性和可用性 - self.setStyleSheet(""" - QDialog { - background-color: #f5f5f5; - color: #333333; - } - QLabel { - color: #333333; - font-size: 14px; - } - QLineEdit { - background-color: white; - color: #333333; - border: 2px solid #cccccc; - border-radius: 4px; - padding: 8px; - font-size: 14px; - min-height: 20px; - } - QLineEdit:focus { - border-color: #0078d4; - } - QPushButton { - background-color: #0078d4; - color: white; - border: none; - padding: 8px 16px; - border-radius: 4px; - font-size: 14px; - min-height: 30px; - } - QPushButton:hover { - background-color: #106ebe; - } - QPushButton:pressed { - background-color: #005a9e; - } - QGroupBox { - font-size: 16px; - font-weight: bold; - color: #0078d4; - border: 2px solid #cccccc; - border-radius: 4px; - margin-top: 10px; - padding-top: 10px; - } - QGroupBox::title { - subcontrol-origin: margin; - left: 10px; - padding: 0 5px 0 5px; - } - """) - - layout = QVBoxLayout(self) - layout.setSpacing(15) - layout.setContentsMargins(20, 20, 20, 20) - - # 标题 - title_label = QLabel("🔧 维修系统") - title_label.setAlignment(Qt.AlignCenter) - title_label.setStyleSheet(""" - font-size: 20px; - font-weight: bold; - color: #0078d4; - margin: 10px 0px; - """) - layout.addWidget(title_label) - - # 登录表单 - login_group = QGroupBox("身份验证") - login_layout = QFormLayout(login_group) - login_layout.setSpacing(10) - login_layout.setContentsMargins(15, 15, 15, 15) - - # 用户名 - self.username_edit = QLineEdit() - self.username_edit.setPlaceholderText("请输入用户名") - self.username_edit.setText("") # 确保清空 - login_layout.addRow("用户名:", self.username_edit) - - # 密码 - self.password_edit = QLineEdit() - self.password_edit.setEchoMode(QLineEdit.Password) - self.password_edit.setPlaceholderText("请输入密码") - self.password_edit.setText("") # 确保清空 - login_layout.addRow("密码:", self.password_edit) - - layout.addWidget(login_group) - - # 提示信息 - hint_label = QLabel("💡 默认账号密码均为: admin") - hint_label.setAlignment(Qt.AlignCenter) - hint_label.setStyleSheet(""" - color: #666666; - font-size: 12px; - margin: 5px 0px; - """) - layout.addWidget(hint_label) - - # 按钮 - button_layout = QHBoxLayout() - button_layout.setSpacing(10) - - self.cancel_btn = QPushButton("取消") - self.login_btn = QPushButton("登录") - - button_layout.addStretch() - button_layout.addWidget(self.cancel_btn) - button_layout.addWidget(self.login_btn) - - layout.addLayout(button_layout) - - # 连接信号 - self.login_btn.clicked.connect(self.on_login) - self.cancel_btn.clicked.connect(self.reject) - self.password_edit.returnPressed.connect(self.on_login) - - # 设置默认焦点和tab顺序 - self.username_edit.setFocus() - self.setTabOrder(self.username_edit, self.password_edit) - self.setTabOrder(self.password_edit, self.login_btn) - self.setTabOrder(self.login_btn, self.cancel_btn) - - print("🔧 登录界面创建完成") - print(f"📝 用户名输入框: {self.username_edit}") - print(f"📝 密码输入框: {self.password_edit}") - print(f"📝 登录按钮: {self.login_btn}") - print(f"📝 取消按钮: {self.cancel_btn}") - - def on_login(self): - """处理登录""" - username = self.username_edit.text().strip() - password = self.password_edit.text().strip() - - # 验证账号密码 - if username == "admin" and password == "admin": - print("✅ 维修系统登录成功") - self.login_success.emit() - self.accept() - else: - QMessageBox.warning(self, "登录失败", - "用户名或密码错误!\n请使用: admin / admin") - self.password_edit.clear() - self.password_edit.setFocus() - - -class MaintenanceSubjectDialog(QDialog): - """维修系统科目选择对话框""" - - subject_selected = pyqtSignal(str, str) # 科目路径, 模式 - - def __init__(self, project_path, parent=None): - super().__init__(parent) - self.project_path = project_path - self.subjects_path = os.path.join(project_path, "Subjects") if project_path else None - self.current_subject_path = None - self.setupUI() - self.load_subjects() - - def setupUI(self): - """设置用户界面""" - self.setWindowTitle("维修系统 - 科目选择") - self.setFixedSize(800, 600) - self.setModal(True) - - # 应用样式 - self.setStyleSheet(""" - QDialog { - background-color: #1e1e2e; - color: #e0e0ff; - } - QLabel { - color: #e0e0ff; - } - QListWidget { - background-color: #2d2d44; - color: #e0e0ff; - border: 2px solid #3a3a4a; - border-radius: 8px; - padding: 5px; - font-size: 14px; - } - QListWidget::item { - padding: 8px; - border-radius: 4px; - margin: 2px; - } - QListWidget::item:hover { - background-color: #3a3a4a; - } - QListWidget::item:selected { - background-color: #8b5cf6; - color: white; - } - QTextEdit { - background-color: #2d2d44; - color: #e0e0ff; - border: 2px solid #3a3a4a; - border-radius: 8px; - padding: 8px; - font-size: 12px; - } - QPushButton { - background-color: #8b5cf6; - color: white; - border: none; - padding: 10px 20px; - border-radius: 8px; - font-weight: bold; - font-size: 14px; - } - QPushButton:hover { - background-color: #7c3aed; - } - QPushButton:pressed { - background-color: #6d28d9; - } - QPushButton:disabled { - background-color: #4c4c6e; - color: #8888aa; - } - QGroupBox { - font-size: 14px; - font-weight: bold; - color: #8b5cf6; - border: 2px solid #3a3a4a; - border-radius: 8px; - margin: 10px 0px; - padding-top: 15px; - } - QGroupBox::title { - subcontrol-origin: margin; - left: 10px; - padding: 0 5px 0 5px; - } - QRadioButton { - color: #e0e0ff; - font-size: 14px; - spacing: 8px; - } - QRadioButton::indicator { - width: 16px; - height: 16px; - } - QRadioButton::indicator:unchecked { - border: 2px solid #3a3a4a; - border-radius: 8px; - background-color: #2d2d44; - } - QRadioButton::indicator:checked { - border: 2px solid #8b5cf6; - border-radius: 8px; - background-color: #8b5cf6; - } - """) - - layout = QVBoxLayout(self) - - # 标题 - title_label = QLabel("📚 选择维修科目") - title_label.setAlignment(Qt.AlignCenter) - title_label.setStyleSheet(""" - font-size: 20px; - font-weight: bold; - color: #8b5cf6; - margin: 15px 0px; - """) - layout.addWidget(title_label) - - # 主要内容区域 - main_splitter = QSplitter(Qt.Horizontal) - - # 左侧:科目列表 - left_widget = QGroupBox("科目列表") - left_layout = QVBoxLayout(left_widget) - - self.subject_list = QListWidget() - self.subject_list.itemClicked.connect(self.on_subject_selected) - left_layout.addWidget(self.subject_list) - - main_splitter.addWidget(left_widget) - - # 右侧:科目详情和模式选择 - right_widget = QGroupBox("科目详情") - right_layout = QVBoxLayout(right_widget) - - # 科目描述 - self.subject_description = QTextEdit() - self.subject_description.setReadOnly(True) - self.subject_description.setMaximumHeight(150) - self.subject_description.setPlainText("请选择一个科目查看详情...") - right_layout.addWidget(self.subject_description) - - # 模式选择 - mode_group = QGroupBox("选择模式") - mode_layout = QVBoxLayout(mode_group) - - self.training_radio = QRadioButton("🎓 训练模式") - self.training_radio.setChecked(True) - mode_layout.addWidget(self.training_radio) - - training_desc = QLabel("• 显示详细的步骤描述和操作提示\n• 提供工具选择的正确性提示\n• 播放语音指导") - training_desc.setStyleSheet("color: #94a3b8; font-size: 12px; margin-left: 20px;") - mode_layout.addWidget(training_desc) - - self.exam_radio = QRadioButton("📋 考核模式") - mode_layout.addWidget(self.exam_radio) - - exam_desc = QLabel("• 不显示步骤描述\n• 不提供工具选择提示\n• 无语音指导,独立完成操作") - exam_desc.setStyleSheet("color: #94a3b8; font-size: 12px; margin-left: 20px;") - mode_layout.addWidget(exam_desc) - - right_layout.addWidget(mode_group) - - main_splitter.addWidget(right_widget) - - # 设置分割器比例 - main_splitter.setSizes([300, 500]) - layout.addWidget(main_splitter) - - # 底部按钮 - button_layout = QHBoxLayout() - - self.refresh_btn = QPushButton("🔄 刷新") - self.cancel_btn = QPushButton("取消") - self.start_btn = QPushButton("开始") - self.start_btn.setEnabled(False) - - button_layout.addWidget(self.refresh_btn) - button_layout.addStretch() - button_layout.addWidget(self.cancel_btn) - button_layout.addWidget(self.start_btn) - - layout.addLayout(button_layout) - - # 连接信号 - self.refresh_btn.clicked.connect(self.load_subjects) - self.cancel_btn.clicked.connect(self.reject) - self.start_btn.clicked.connect(self.on_start) - - def load_subjects(self): - """加载科目列表""" - self.subject_list.clear() - self.current_subject_path = None - self.start_btn.setEnabled(False) - self.subject_description.setPlainText("请选择一个科目查看详情...") - - if not self.subjects_path or not os.path.exists(self.subjects_path): - # 添加提示项 - item = QListWidgetItem("📁 未找到Subjects目录") - item.setData(Qt.UserRole, None) - self.subject_list.addItem(item) - - if not self.project_path: - self.subject_description.setPlainText( - "❌ 错误:没有打开的项目\n\n" - "请先通过 文件 -> 打开 菜单打开一个项目,\n" - "然后再使用维修系统功能。" - ) - else: - self.subject_description.setPlainText( - f"❌ 错误:找不到Subjects目录\n\n" - f"项目路径: {self.project_path}\n" - f"期望目录: {self.subjects_path}\n\n" - "请在项目根目录下创建Subjects文件夹,\n" - "并在其中放置科目的JSON配置文件。" - ) - return - - # 扫描JSON文件 - json_files = [] - try: - for file in os.listdir(self.subjects_path): - if file.endswith('.json'): - json_path = os.path.join(self.subjects_path, file) - json_files.append((file, json_path)) - except Exception as e: - print(f"❌ 扫描Subjects目录失败: {e}") - item = QListWidgetItem("❌ 读取目录失败") - item.setData(Qt.UserRole, None) - self.subject_list.addItem(item) - return - - if not json_files: - item = QListWidgetItem("📄 目录中没有JSON文件") - item.setData(Qt.UserRole, None) - self.subject_list.addItem(item) - self.subject_description.setPlainText( - f"ℹ️ 提示:Subjects目录为空\n\n" - f"目录路径: {self.subjects_path}\n\n" - "请在此目录中放置维修科目的JSON配置文件。" - ) - return - - # 加载科目 - loaded_count = 0 - for filename, filepath in json_files: - try: - with open(filepath, 'r', encoding='utf-8') as f: - subject_data = json.load(f) - - # 获取科目名称 - subject_name = subject_data.get('name', filename.replace('.json', '')) - - # 创建列表项 - item = QListWidgetItem(f"📋 {subject_name}") - item.setData(Qt.UserRole, { - 'path': filepath, - 'data': subject_data, - 'filename': filename - }) - self.subject_list.addItem(item) - loaded_count += 1 - - except Exception as e: - print(f"❌ 加载科目文件失败 {filename}: {e}") - item = QListWidgetItem(f"❌ {filename} (加载失败)") - item.setData(Qt.UserRole, None) - self.subject_list.addItem(item) - - print(f"✅ 成功加载 {loaded_count} 个维修科目") - - def on_subject_selected(self, item): - """处理科目选择""" - try: - print(f"📋 科目选择事件触发,item: {item}") - - if not item: - print("❌ item为空") - self.current_subject_path = None - self.start_btn.setEnabled(False) - self.subject_description.setPlainText("请选择一个有效的科目。") - return - - subject_info = item.data(Qt.UserRole) - print(f"📋 科目信息: {subject_info}") - - if not subject_info: - print("❌ 科目信息为空") - self.current_subject_path = None - self.start_btn.setEnabled(False) - self.subject_description.setPlainText("此科目无法加载,请检查文件格式。") - return - - self.current_subject_path = subject_info['path'] - self.start_btn.setEnabled(True) - - print(f"✅ 选择科目: {subject_info.get('filename', 'unknown')}") - - # 显示科目详情 - subject_data = subject_info['data'] - description = self.format_subject_description(subject_data, subject_info['filename']) - self.subject_description.setPlainText(description) - - print("✅ 科目详情显示完成") - - except Exception as e: - print(f"❌ 科目选择处理失败: {e}") - import traceback - traceback.print_exc() - - self.current_subject_path = None - self.start_btn.setEnabled(False) - self.subject_description.setPlainText(f"处理科目信息时出错:\n{str(e)}") - - # 显示错误对话框 - QMessageBox.critical(self, "错误", f"处理科目选择时出错:\n{str(e)}") - - def format_subject_description(self, subject_data, filename): - """格式化科目描述""" - try: - print(f"🔧 格式化科目描述: {filename}") - - if not subject_data: - return "❌ 科目数据为空" - - lines = [] - - # 基本信息 - name = subject_data.get('name', '未知科目') - lines.append(f"📋 科目: {name}") - lines.append(f"📁 文件: {filename}") - lines.append("") - - # 描述信息 - if 'description' in subject_data and subject_data['description']: - lines.append(f"📝 描述: {subject_data['description']}") - lines.append("") - - # 步骤信息 - steps = subject_data.get('steps', []) - if steps and isinstance(steps, list): - lines.append(f"📊 步骤数量: {len(steps)}") - - # 计算总分 - try: - total_score = 0 - for step in steps: - if isinstance(step, dict): - score = step.get('score', 0) - if isinstance(score, (int, float)): - total_score += score - - if total_score > 0: - lines.append(f"💯 总分: {total_score} 分") - except Exception as e: - print(f"⚠️ 计算总分时出错: {e}") - - lines.append("") - lines.append("📋 步骤列表:") - - for i, step in enumerate(steps, 1): - try: - if isinstance(step, dict): - step_name = step.get('name', f'步骤{i}') - step_score = step.get('score', 0) - - if isinstance(step_score, (int, float)) and step_score > 0: - lines.append(f" {i}. {step_name} ({step_score}分)") - else: - lines.append(f" {i}. {step_name}") - else: - lines.append(f" {i}. 步骤{i} (格式错误)") - except Exception as e: - print(f"⚠️ 处理步骤{i}时出错: {e}") - lines.append(f" {i}. 步骤{i} (处理错误)") - else: - lines.append("⚠️ 此科目没有配置步骤") - - # 工具信息 - tools = subject_data.get('tools', []) - if tools and isinstance(tools, list): - try: - valid_tools = [str(tool) for tool in tools if tool] - if valid_tools: - lines.append("") - lines.append(f"🔧 可用工具: {', '.join(valid_tools)}") - except Exception as e: - print(f"⚠️ 处理工具列表时出错: {e}") - lines.append("") - lines.append("🔧 可用工具: (处理错误)") - - result = '\n'.join(lines) - print(f"✅ 科目描述格式化完成,长度: {len(result)}") - return result - - except Exception as e: - print(f"❌ 格式化科目描述失败: {e}") - import traceback - traceback.print_exc() - - return f"❌ 格式化科目描述时出错:\n{str(e)}\n\n文件: {filename}" - - def on_start(self): - """开始科目""" - if not self.current_subject_path: - QMessageBox.warning(self, "错误", "请先选择一个科目!") - return - - # 获取选择的模式 - mode = "training" if self.training_radio.isChecked() else "exam" - - print(f"🚀 开始维修科目: {self.current_subject_path}, 模式: {mode}") - - # 发射信号 - self.subject_selected.emit(self.current_subject_path, mode) - self.accept() - - -class MaintenanceSystemManager: - """维修系统管理器""" - - def __init__(self, world): - self.world = world - self.login_dialog = None - self.subject_dialog = None - - def show_maintenance_system(self, parent_window): - """显示维修系统""" - # 创建登录对话框 - self.login_dialog = MaintenanceLoginDialog(parent_window) - self.login_dialog.login_success.connect( - lambda: self.show_subject_selection(parent_window) - ) - self.login_dialog.exec_() - - def show_subject_selection(self, parent_window): - """显示科目选择界面""" - # 获取当前项目路径 - project_path = None - if hasattr(self.world, 'project_manager') and self.world.project_manager: - project_path = self.world.project_manager.getCurrentProjectPath() - - # 创建科目选择对话框 - self.subject_dialog = MaintenanceSubjectDialog(project_path, parent_window) - self.subject_dialog.subject_selected.connect(self.start_maintenance_subject) - self.subject_dialog.exec_() - - def start_maintenance_subject(self, subject_path, mode): - """开始维修科目""" - try: - print(f"🎯 启动维修科目: {subject_path}") - print(f"📝 模式: {mode}") - - # 加载科目配置 - with open(subject_path, 'r', encoding='utf-8') as f: - subject_config = json.load(f) - - # 启动拆装交互系统 - if hasattr(self.world, 'assembly_interaction') and self.world.assembly_interaction: - # 设置配置 - self.world.assembly_interaction.config_data = subject_config - - # 启动交互模式 - self.world.assembly_interaction.start_interaction_mode(mode=mode) - - print(f"✅ 维修科目启动成功") - else: - print("❌ 错误:拆装交互系统未初始化") - - except Exception as e: - print(f"❌ 启动维修科目失败: {e}") - import traceback +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +维修系统界面模块 +包含登录界面、科目列表、模式选择等功能 +""" + +import os +import json +from PyQt5.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QGroupBox, + QLineEdit, QPushButton, QLabel, QListWidget, QListWidgetItem, + QMessageBox, QRadioButton, QTextEdit, QSplitter +) +from PyQt5.QtCore import Qt, pyqtSignal +from PyQt5.QtGui import QFont, QPixmap + + +class MaintenanceLoginDialog(QDialog): + """维修系统登录对话框""" + + login_success = pyqtSignal() # 登录成功信号 + + def __init__(self, parent=None): + super().__init__(parent) + self.setupUI() + + def setupUI(self): + """设置用户界面""" + self.setWindowTitle("维修系统 - 登录") + self.setFixedSize(400, 300) + self.setModal(True) + + # 应用简化样式 - 确保可见性和可用性 + self.setStyleSheet(""" + QDialog { + background-color: #f5f5f5; + color: #333333; + } + QLabel { + color: #333333; + font-size: 14px; + } + QLineEdit { + background-color: white; + color: #333333; + border: 2px solid #cccccc; + border-radius: 4px; + padding: 8px; + font-size: 14px; + min-height: 20px; + } + QLineEdit:focus { + border-color: #0078d4; + } + QPushButton { + background-color: #0078d4; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-size: 14px; + min-height: 30px; + } + QPushButton:hover { + background-color: #106ebe; + } + QPushButton:pressed { + background-color: #005a9e; + } + QGroupBox { + font-size: 16px; + font-weight: bold; + color: #0078d4; + border: 2px solid #cccccc; + border-radius: 4px; + margin-top: 10px; + padding-top: 10px; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 10px; + padding: 0 5px 0 5px; + } + """) + + layout = QVBoxLayout(self) + layout.setSpacing(15) + layout.setContentsMargins(20, 20, 20, 20) + + # 标题 + title_label = QLabel("🔧 维修系统") + title_label.setAlignment(Qt.AlignCenter) + title_label.setStyleSheet(""" + font-size: 20px; + font-weight: bold; + color: #0078d4; + margin: 10px 0px; + """) + layout.addWidget(title_label) + + # 登录表单 + login_group = QGroupBox("身份验证") + login_layout = QFormLayout(login_group) + login_layout.setSpacing(10) + login_layout.setContentsMargins(15, 15, 15, 15) + + # 用户名 + self.username_edit = QLineEdit() + self.username_edit.setPlaceholderText("请输入用户名") + self.username_edit.setText("") # 确保清空 + login_layout.addRow("用户名:", self.username_edit) + + # 密码 + self.password_edit = QLineEdit() + self.password_edit.setEchoMode(QLineEdit.Password) + self.password_edit.setPlaceholderText("请输入密码") + self.password_edit.setText("") # 确保清空 + login_layout.addRow("密码:", self.password_edit) + + layout.addWidget(login_group) + + # 提示信息 + hint_label = QLabel("💡 默认账号密码均为: admin") + hint_label.setAlignment(Qt.AlignCenter) + hint_label.setStyleSheet(""" + color: #666666; + font-size: 12px; + margin: 5px 0px; + """) + layout.addWidget(hint_label) + + # 按钮 + button_layout = QHBoxLayout() + button_layout.setSpacing(10) + + self.cancel_btn = QPushButton("取消") + self.login_btn = QPushButton("登录") + + button_layout.addStretch() + button_layout.addWidget(self.cancel_btn) + button_layout.addWidget(self.login_btn) + + layout.addLayout(button_layout) + + # 连接信号 + self.login_btn.clicked.connect(self.on_login) + self.cancel_btn.clicked.connect(self.reject) + self.password_edit.returnPressed.connect(self.on_login) + + # 设置默认焦点和tab顺序 + self.username_edit.setFocus() + self.setTabOrder(self.username_edit, self.password_edit) + self.setTabOrder(self.password_edit, self.login_btn) + self.setTabOrder(self.login_btn, self.cancel_btn) + + print("🔧 登录界面创建完成") + print(f"📝 用户名输入框: {self.username_edit}") + print(f"📝 密码输入框: {self.password_edit}") + print(f"📝 登录按钮: {self.login_btn}") + print(f"📝 取消按钮: {self.cancel_btn}") + + def on_login(self): + """处理登录""" + username = self.username_edit.text().strip() + password = self.password_edit.text().strip() + + # 验证账号密码 + if username == "admin" and password == "admin": + print("✅ 维修系统登录成功") + self.login_success.emit() + self.accept() + else: + QMessageBox.warning(self, "登录失败", + "用户名或密码错误!\n请使用: admin / admin") + self.password_edit.clear() + self.password_edit.setFocus() + + +class MaintenanceSubjectDialog(QDialog): + """维修系统科目选择对话框""" + + subject_selected = pyqtSignal(str, str) # 科目路径, 模式 + + def __init__(self, project_path, parent=None): + super().__init__(parent) + self.project_path = project_path + self.subjects_path = os.path.join(project_path, "Subjects") if project_path else None + self.current_subject_path = None + self.setupUI() + self.load_subjects() + + def setupUI(self): + """设置用户界面""" + self.setWindowTitle("维修系统 - 科目选择") + self.setFixedSize(800, 600) + self.setModal(True) + + # 应用样式 + self.setStyleSheet(""" + QDialog { + background-color: #1e1e2e; + color: #e0e0ff; + } + QLabel { + color: #e0e0ff; + } + QListWidget { + background-color: #2d2d44; + color: #e0e0ff; + border: 2px solid #3a3a4a; + border-radius: 8px; + padding: 5px; + font-size: 14px; + } + QListWidget::item { + padding: 8px; + border-radius: 4px; + margin: 2px; + } + QListWidget::item:hover { + background-color: #3a3a4a; + } + QListWidget::item:selected { + background-color: #8b5cf6; + color: white; + } + QTextEdit { + background-color: #2d2d44; + color: #e0e0ff; + border: 2px solid #3a3a4a; + border-radius: 8px; + padding: 8px; + font-size: 12px; + } + QPushButton { + background-color: #8b5cf6; + color: white; + border: none; + padding: 10px 20px; + border-radius: 8px; + font-weight: bold; + font-size: 14px; + } + QPushButton:hover { + background-color: #7c3aed; + } + QPushButton:pressed { + background-color: #6d28d9; + } + QPushButton:disabled { + background-color: #4c4c6e; + color: #8888aa; + } + QGroupBox { + font-size: 14px; + font-weight: bold; + color: #8b5cf6; + border: 2px solid #3a3a4a; + border-radius: 8px; + margin: 10px 0px; + padding-top: 15px; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 10px; + padding: 0 5px 0 5px; + } + QRadioButton { + color: #e0e0ff; + font-size: 14px; + spacing: 8px; + } + QRadioButton::indicator { + width: 16px; + height: 16px; + } + QRadioButton::indicator:unchecked { + border: 2px solid #3a3a4a; + border-radius: 8px; + background-color: #2d2d44; + } + QRadioButton::indicator:checked { + border: 2px solid #8b5cf6; + border-radius: 8px; + background-color: #8b5cf6; + } + """) + + layout = QVBoxLayout(self) + + # 标题 + title_label = QLabel("📚 选择维修科目") + title_label.setAlignment(Qt.AlignCenter) + title_label.setStyleSheet(""" + font-size: 20px; + font-weight: bold; + color: #8b5cf6; + margin: 15px 0px; + """) + layout.addWidget(title_label) + + # 主要内容区域 + main_splitter = QSplitter(Qt.Horizontal) + + # 左侧:科目列表 + left_widget = QGroupBox("科目列表") + left_layout = QVBoxLayout(left_widget) + + self.subject_list = QListWidget() + self.subject_list.itemClicked.connect(self.on_subject_selected) + left_layout.addWidget(self.subject_list) + + main_splitter.addWidget(left_widget) + + # 右侧:科目详情和模式选择 + right_widget = QGroupBox("科目详情") + right_layout = QVBoxLayout(right_widget) + + # 科目描述 + self.subject_description = QTextEdit() + self.subject_description.setReadOnly(True) + self.subject_description.setMaximumHeight(150) + self.subject_description.setPlainText("请选择一个科目查看详情...") + right_layout.addWidget(self.subject_description) + + # 模式选择 + mode_group = QGroupBox("选择模式") + mode_layout = QVBoxLayout(mode_group) + + self.training_radio = QRadioButton("🎓 训练模式") + self.training_radio.setChecked(True) + mode_layout.addWidget(self.training_radio) + + training_desc = QLabel("• 显示详细的步骤描述和操作提示\n• 提供工具选择的正确性提示\n• 播放语音指导") + training_desc.setStyleSheet("color: #94a3b8; font-size: 12px; margin-left: 20px;") + mode_layout.addWidget(training_desc) + + self.exam_radio = QRadioButton("📋 考核模式") + mode_layout.addWidget(self.exam_radio) + + exam_desc = QLabel("• 不显示步骤描述\n• 不提供工具选择提示\n• 无语音指导,独立完成操作") + exam_desc.setStyleSheet("color: #94a3b8; font-size: 12px; margin-left: 20px;") + mode_layout.addWidget(exam_desc) + + right_layout.addWidget(mode_group) + + main_splitter.addWidget(right_widget) + + # 设置分割器比例 + main_splitter.setSizes([300, 500]) + layout.addWidget(main_splitter) + + # 底部按钮 + button_layout = QHBoxLayout() + + self.refresh_btn = QPushButton("🔄 刷新") + self.cancel_btn = QPushButton("取消") + self.start_btn = QPushButton("开始") + self.start_btn.setEnabled(False) + + button_layout.addWidget(self.refresh_btn) + button_layout.addStretch() + button_layout.addWidget(self.cancel_btn) + button_layout.addWidget(self.start_btn) + + layout.addLayout(button_layout) + + # 连接信号 + self.refresh_btn.clicked.connect(self.load_subjects) + self.cancel_btn.clicked.connect(self.reject) + self.start_btn.clicked.connect(self.on_start) + + def load_subjects(self): + """加载科目列表""" + self.subject_list.clear() + self.current_subject_path = None + self.start_btn.setEnabled(False) + self.subject_description.setPlainText("请选择一个科目查看详情...") + + if not self.subjects_path or not os.path.exists(self.subjects_path): + # 添加提示项 + item = QListWidgetItem("📁 未找到Subjects目录") + item.setData(Qt.UserRole, None) + self.subject_list.addItem(item) + + if not self.project_path: + self.subject_description.setPlainText( + "❌ 错误:没有打开的项目\n\n" + "请先通过 文件 -> 打开 菜单打开一个项目,\n" + "然后再使用维修系统功能。" + ) + else: + self.subject_description.setPlainText( + f"❌ 错误:找不到Subjects目录\n\n" + f"项目路径: {self.project_path}\n" + f"期望目录: {self.subjects_path}\n\n" + "请在项目根目录下创建Subjects文件夹,\n" + "并在其中放置科目的JSON配置文件。" + ) + return + + # 扫描JSON文件 + json_files = [] + try: + for file in os.listdir(self.subjects_path): + if file.endswith('.json'): + json_path = os.path.join(self.subjects_path, file) + json_files.append((file, json_path)) + except Exception as e: + print(f"❌ 扫描Subjects目录失败: {e}") + item = QListWidgetItem("❌ 读取目录失败") + item.setData(Qt.UserRole, None) + self.subject_list.addItem(item) + return + + if not json_files: + item = QListWidgetItem("📄 目录中没有JSON文件") + item.setData(Qt.UserRole, None) + self.subject_list.addItem(item) + self.subject_description.setPlainText( + f"ℹ️ 提示:Subjects目录为空\n\n" + f"目录路径: {self.subjects_path}\n\n" + "请在此目录中放置维修科目的JSON配置文件。" + ) + return + + # 加载科目 + loaded_count = 0 + for filename, filepath in json_files: + try: + with open(filepath, 'r', encoding='utf-8') as f: + subject_data = json.load(f) + + # 获取科目名称 + subject_name = subject_data.get('name', filename.replace('.json', '')) + + # 创建列表项 + item = QListWidgetItem(f"📋 {subject_name}") + item.setData(Qt.UserRole, { + 'path': filepath, + 'data': subject_data, + 'filename': filename + }) + self.subject_list.addItem(item) + loaded_count += 1 + + except Exception as e: + print(f"❌ 加载科目文件失败 {filename}: {e}") + item = QListWidgetItem(f"❌ {filename} (加载失败)") + item.setData(Qt.UserRole, None) + self.subject_list.addItem(item) + + print(f"✅ 成功加载 {loaded_count} 个维修科目") + + def on_subject_selected(self, item): + """处理科目选择""" + try: + print(f"📋 科目选择事件触发,item: {item}") + + if not item: + print("❌ item为空") + self.current_subject_path = None + self.start_btn.setEnabled(False) + self.subject_description.setPlainText("请选择一个有效的科目。") + return + + subject_info = item.data(Qt.UserRole) + print(f"📋 科目信息: {subject_info}") + + if not subject_info: + print("❌ 科目信息为空") + self.current_subject_path = None + self.start_btn.setEnabled(False) + self.subject_description.setPlainText("此科目无法加载,请检查文件格式。") + return + + self.current_subject_path = subject_info['path'] + self.start_btn.setEnabled(True) + + print(f"✅ 选择科目: {subject_info.get('filename', 'unknown')}") + + # 显示科目详情 + subject_data = subject_info['data'] + description = self.format_subject_description(subject_data, subject_info['filename']) + self.subject_description.setPlainText(description) + + print("✅ 科目详情显示完成") + + except Exception as e: + print(f"❌ 科目选择处理失败: {e}") + import traceback + traceback.print_exc() + + self.current_subject_path = None + self.start_btn.setEnabled(False) + self.subject_description.setPlainText(f"处理科目信息时出错:\n{str(e)}") + + # 显示错误对话框 + QMessageBox.critical(self, "错误", f"处理科目选择时出错:\n{str(e)}") + + def format_subject_description(self, subject_data, filename): + """格式化科目描述""" + try: + print(f"🔧 格式化科目描述: {filename}") + + if not subject_data: + return "❌ 科目数据为空" + + lines = [] + + # 基本信息 + name = subject_data.get('name', '未知科目') + lines.append(f"📋 科目: {name}") + lines.append(f"📁 文件: {filename}") + lines.append("") + + # 描述信息 + if 'description' in subject_data and subject_data['description']: + lines.append(f"📝 描述: {subject_data['description']}") + lines.append("") + + # 步骤信息 + steps = subject_data.get('steps', []) + if steps and isinstance(steps, list): + lines.append(f"📊 步骤数量: {len(steps)}") + + # 计算总分 + try: + total_score = 0 + for step in steps: + if isinstance(step, dict): + score = step.get('score', 0) + if isinstance(score, (int, float)): + total_score += score + + if total_score > 0: + lines.append(f"💯 总分: {total_score} 分") + except Exception as e: + print(f"⚠️ 计算总分时出错: {e}") + + lines.append("") + lines.append("📋 步骤列表:") + + for i, step in enumerate(steps, 1): + try: + if isinstance(step, dict): + step_name = step.get('name', f'步骤{i}') + step_score = step.get('score', 0) + + if isinstance(step_score, (int, float)) and step_score > 0: + lines.append(f" {i}. {step_name} ({step_score}分)") + else: + lines.append(f" {i}. {step_name}") + else: + lines.append(f" {i}. 步骤{i} (格式错误)") + except Exception as e: + print(f"⚠️ 处理步骤{i}时出错: {e}") + lines.append(f" {i}. 步骤{i} (处理错误)") + else: + lines.append("⚠️ 此科目没有配置步骤") + + # 工具信息 + tools = subject_data.get('tools', []) + if tools and isinstance(tools, list): + try: + valid_tools = [str(tool) for tool in tools if tool] + if valid_tools: + lines.append("") + lines.append(f"🔧 可用工具: {', '.join(valid_tools)}") + except Exception as e: + print(f"⚠️ 处理工具列表时出错: {e}") + lines.append("") + lines.append("🔧 可用工具: (处理错误)") + + result = '\n'.join(lines) + print(f"✅ 科目描述格式化完成,长度: {len(result)}") + return result + + except Exception as e: + print(f"❌ 格式化科目描述失败: {e}") + import traceback + traceback.print_exc() + + return f"❌ 格式化科目描述时出错:\n{str(e)}\n\n文件: {filename}" + + def on_start(self): + """开始科目""" + if not self.current_subject_path: + QMessageBox.warning(self, "错误", "请先选择一个科目!") + return + + # 获取选择的模式 + mode = "training" if self.training_radio.isChecked() else "exam" + + print(f"🚀 开始维修科目: {self.current_subject_path}, 模式: {mode}") + + # 发射信号 + self.subject_selected.emit(self.current_subject_path, mode) + self.accept() + + +class MaintenanceSystemManager: + """维修系统管理器""" + + def __init__(self, world): + self.world = world + self.login_dialog = None + self.subject_dialog = None + + def show_maintenance_system(self, parent_window): + """显示维修系统""" + # 创建登录对话框 + self.login_dialog = MaintenanceLoginDialog(parent_window) + self.login_dialog.login_success.connect( + lambda: self.show_subject_selection(parent_window) + ) + self.login_dialog.exec_() + + def show_subject_selection(self, parent_window): + """显示科目选择界面""" + # 获取当前项目路径 + project_path = None + if hasattr(self.world, 'project_manager') and self.world.project_manager: + project_path = self.world.project_manager.getCurrentProjectPath() + + # 创建科目选择对话框 + self.subject_dialog = MaintenanceSubjectDialog(project_path, parent_window) + self.subject_dialog.subject_selected.connect(self.start_maintenance_subject) + self.subject_dialog.exec_() + + def start_maintenance_subject(self, subject_path, mode): + """开始维修科目""" + try: + print(f"🎯 启动维修科目: {subject_path}") + print(f"📝 模式: {mode}") + + # 加载科目配置 + with open(subject_path, 'r', encoding='utf-8') as f: + subject_config = json.load(f) + + # 启动拆装交互系统 + if hasattr(self.world, 'assembly_interaction') and self.world.assembly_interaction: + # 设置配置 + self.world.assembly_interaction.config_data = subject_config + + # 启动交互模式 + self.world.assembly_interaction.start_interaction_mode(mode=mode) + + print(f"✅ 维修科目启动成功") + else: + print("❌ 错误:拆装交互系统未初始化") + + except Exception as e: + print(f"❌ 启动维修科目失败: {e}") + import traceback traceback.print_exc() \ No newline at end of file diff --git a/ui/property_panel.py b/ui/property_panel.py index 004f1265..4a561c0b 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -1,11403 +1,11403 @@ -import os -from collections import deque -from traceback import print_exc, print_last -from types import new_class -from typing import Hashable - -from PyQt5.QtGui import QColor -from PyQt5.QtWidgets import (QLabel, QLineEdit, QDoubleSpinBox, QPushButton, - QTreeWidget, QTreeWidgetItem, QMenu, QCheckBox, QComboBox, QHBoxLayout, QWidget, - QVBoxLayout, QGroupBox, QGridLayout, QSpinBox, QFileDialog, QMessageBox, QSizePolicy) -from PyQt5.QtCore import Qt, QTimer -from deploy_libs.unicodedata import normalize -from direct.actor.Actor import Actor -from direct.gui import DirectGui -from direct.task.TaskManagerGlobal import taskMgr -from idna import check_label -from jinja2.compiler import has_safe_repr -from panda3d.core import Vec3, Vec4, transpose, TransparencyAttrib, PartGroup, ColorAttrib, NodePath, Point3 -from scene import util -from direct.gui.DirectGui import DirectLabel, DirectFrame -from panda3d.core import TextNode -from ui.icon_manager import get_icon_manager, has_icon, get_icon - -class PropertyPanelManager: - """属性面板管理器""" - - def __init__(self, world): - """初始化属性面板管理器""" - self.world = world - self._propertyLayout = None - self._actor_cache = {} - self._spherical_video_controls = {} - self._transform_monitor_timer = None - self._last_transform_values = {} - - self.column_minimum_width = 85 - - # 初始化地形编辑参数 - if not hasattr(self.world, 'terrain_edit_radius'): - self.world.terrain_edit_radius = 3.0 - if not hasattr(self.world, 'terrain_edit_strength'): - self.world.terrain_edit_strength = 0.3 - if not hasattr(self.world, 'terrain_edit_operation'): # 这里原来是 terrain_edit_opertaion - self.world.terrain_edit_operation = "add" - - # 初始化碰撞参数加载标志位 - self._loading_collision_params = False - - # 定义现代化紧凑样式 - self.compact_style = """ - QDoubleSpinBox, QSpinBox { - min-width: 60px; - background-color: rgba(89, 100, 113, 0.15); - border: 1px solid rgba(76, 92, 110, 0.4); - border-radius: 4px; - padding: 4px 6px; - font-size: 10px; - - } - QDoubleSpinBox:focus, QSpinBox:focus { - border: 1px solid #4d74bd; - background-color: rgba(77, 116, 189, 0.1); - } - QDoubleSpinBox::up-button, QSpinBox::up-button { - background-color: rgba(77, 116, 189, 0.2); - border: none; - border-radius: 2px; - width: 14px; - subcontrol-origin: border; - subcontrol-position: top right; - } - QDoubleSpinBox::down-button, QSpinBox::down-button { - background-color: rgba(77, 116, 189, 0.2); - border: none; - border-radius: 2px; - width: 14px; - subcontrol-origin: border; - subcontrol-position: bottom right; - } - QDoubleSpinBox::up-button:hover, QSpinBox::up-button:hover, - QDoubleSpinBox::down-button:hover, QSpinBox::down-button:hover { - background-color: rgba(77, 116, 189, 0.4); - } - QDoubleSpinBox::up-arrow, QSpinBox::up-arrow { - image: url(icons/up_arrows.png); - width: 10px; - height: 10px; - } - QDoubleSpinBox::down-arrow, QSpinBox::down-arrow { - image: url(icons/down_arrows.png); - width: 10px; - height: 10px; - } - QDoubleSpinBox::up-arrow:hover, QSpinBox::up-arrow:hover { - image: url(icons/up_arrows.png); - } - QDoubleSpinBox::down-arrow:hover, QSpinBox::down-arrow:hover { - image: url(icons/down_arrows.png); - } - QPushButton { - min-width: 60px; - background-color: rgba(89, 100, 113, 0.4); - color: rgba(255, 255, 255, 0.8); - border: 1px solid rgba(76, 92, 110, 0.4); - border-radius: 4px; - padding: 6px 10px; - font-size: 10px; - font-weight: 400; - } - QPushButton:hover { - background-color: rgba(89, 100, 113, 0.6); - border: 1px solid rgba(77, 116, 189, 0.6); - color: #ffffff; - } - QPushButton:pressed, QPushButton:checked { - background-color: rgba(77, 116, 189, 0.8); - border: 1px solid #4d74bd; - color: #ffffff; - } - QPushButton:disabled { - background-color: rgba(89, 100, 113, 0.2); - color: rgba(235, 235, 235, 0.4); - border: 1px solid rgba(76, 92, 110, 0.2); - } - QComboBox { - min-width: 80px; - background-color: rgba(89, 100, 113, 0.15); - border: 1px solid rgba(76, 92, 110, 0.4); - border-radius: 4px; - padding: 4px 6px; - font-size: 10px; - } - QComboBox:focus { - border: 1px solid #4d74bd; - } - QComboBox::drop-down { - border: none; - border-left: 1px solid rgba(76, 92, 110, 0.4); - background-color: rgba(77, 116, 189, 0.2); - border-radius: 0 4px 4px 0; - width: 16px; - } - QComboBox::drop-down:hover { - background-color: rgba(77, 116, 189, 0.4); - } - QComboBox::down-arrow { - image: url(icons/down_arrows.png); - width: 10px; - height: 10px; - } - QLineEdit { - min-width: 80px; - background-color: rgba(89, 100, 113, 0.15); - border: 1px solid rgba(76, 92, 110, 0.4); - border-radius: 4px; - padding: 4px 6px; - font-size: 10px; - } - QLineEdit:focus { - border: 1px solid #4d74bd; - background-color: rgba(77, 116, 189, 0.1); - } - QCheckBox { - min-width: 20px; - font-size: 10px; - spacing: 6px; - } - QCheckBox::indicator { - width: 14px; - height: 14px; - border: 1px solid rgba(76, 92, 110, 0.6); - border-radius: 3px; - background-color: rgba(89, 100, 113, 0.15); - } - QCheckBox::indicator:checked { - background-color: #4d74bd; - border: 1px solid #4d74bd; - } - """ - self.compact_style += """ - QGroupBox { - margin-left: 0px; - padding-left: 0px; - } - QGroupBox::title { - left: 0px; - padding: 8px 5px 5px 0; - margin-top: 0px; - } - """ - - # 定义现代化状态徽章样式 - self.badge_style_blue = """ - QLabel { - background-color: rgba(77, 116, 189, 0.15); - border: 1px solid rgba(77, 116, 189, 0.6); - color: #4d74bd; - border-radius: 6px; - padding: 4px 10px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 9px; - font-weight: 500; - letter-spacing: 0.5px; - } - """ - - self.badge_style_green = """ - QLabel { - background-color: rgba(45, 255, 196, 0.15); - border: 1px solid rgba(45, 255, 196, 0.6); - color: #2dffc4; - border-radius: 6px; - padding: 4px 10px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 9px; - font-weight: 500; - letter-spacing: 0.5px; - } - """ - - # 添加新的徽章样式 - self.badge_style_orange = """ - QLabel { - background-color: rgba(255, 165, 0, 0.15); - border: 1px solid rgba(255, 165, 0, 0.6); - color: #ffa500; - border-radius: 6px; - padding: 4px 10px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 9px; - font-weight: 500; - letter-spacing: 0.5px; - } - """ - - self.badge_style_red = """ - QLabel { - background-color: rgba(255, 99, 99, 0.15); - border: 1px solid rgba(255, 99, 99, 0.6); - color: #ff6363; - border-radius: 6px; - padding: 4px 10px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 9px; - font-weight: 500; - letter-spacing: 0.5px; - } - """ - - # 固定宽度的徽章样式(用于碰撞检测状态) - self.badge_style_green_fixed = """ - QLabel { - background-color: rgba(45, 255, 196, 0.15); - border: 1px solid rgba(45, 255, 196, 0.6); - color: #2dffc4; - border-radius: 6px; - padding: 4px 10px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 9px; - font-weight: 500; - letter-spacing: 0.5px; - min-width: 50px; - max-width: 50px; - text-align: center; - } - """ - - self.badge_style_red_fixed = """ - QLabel { - background-color: rgba(255, 99, 99, 0.15); - border: 1px solid rgba(255, 99, 99, 0.6); - color: #ff6363; - border-radius: 6px; - padding: 4px 10px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 9px; - font-weight: 500; - letter-spacing: 0.5px; - min-width: 50px; - max-width: 50px; - text-align: center; - } - """ - - def createStatusBadge(self, text, badge_type="green"): - """创建现代化状态徽章 - - Args: - text: 徽章文字 - badge_type: 徽章类型,支持 "green", "blue", "orange", "red" - - Returns: - QLabel: 配置好样式的标签 - """ - badge = QLabel(text) - if badge_type == "green": - badge.setStyleSheet(self.badge_style_green) - elif badge_type == "blue": - badge.setStyleSheet(self.badge_style_blue) - elif badge_type == "orange": - badge.setStyleSheet(self.badge_style_orange) - elif badge_type == "red": - badge.setStyleSheet(self.badge_style_red) - else: - badge.setStyleSheet(self.badge_style_blue) # 默认蓝色 - return badge - - def createFixedStatusBadge(self, text, badge_type="green"): - """创建固定宽度的状态徽章(用于碰撞检测等需要保持一致宽度的场景) - - Args: - text: 徽章文字 - badge_type: 徽章类型,支持 "green", "red" - - Returns: - QLabel: 配置好固定宽度样式的标签 - """ - badge = QLabel(text) - badge.setAlignment(Qt.AlignCenter) # 确保文字居中 - - if badge_type == "green": - badge.setStyleSheet(self.badge_style_green_fixed) - elif badge_type == "red": - badge.setStyleSheet(self.badge_style_red_fixed) - else: - badge.setStyleSheet(self.badge_style_green_fixed) # 默认绿色 - - return badge - - def createModernButton(self, text, button_type="default"): - """创建现代化按钮 - - Args: - text: 按钮文字 - button_type: 按钮类型 - "default", "primary", "success", "warning", "danger" - - Returns: - QPushButton: 配置好样式的按钮 - """ - button = QPushButton(text) - - if button_type == "primary": - button.setStyleSheet(""" - QPushButton { - background-color: rgba(77, 116, 189, 0.8); - color: #ffffff; - border: 1px solid #4d74bd; - border-radius: 4px; - padding: 8px 12px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 10px; - font-weight: 500; - min-height: 16px; - } - QPushButton:hover { - background-color: rgba(77, 116, 189, 1.0); - border: 1px solid rgba(77, 116, 189, 1.0); - } - QPushButton:pressed { - background-color: rgba(61, 96, 169, 1.0); - border: 1px solid rgba(61, 96, 169, 1.0); - } - """) - elif button_type == "success": - button.setStyleSheet(""" - QPushButton { - background-color: rgba(45, 255, 196, 0.15); - color: #2dffc4; - border: 1px solid rgba(45, 255, 196, 0.6); - border-radius: 4px; - padding: 8px 12px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 10px; - font-weight: 400; - min-height: 16px; - } - QPushButton:hover { - background-color: rgba(45, 255, 196, 0.25); - border: 1px solid #2dffc4; - } - QPushButton:pressed { - background-color: rgba(45, 255, 196, 0.4); - color: #ffffff; - } - """) - elif button_type == "warning": - button.setStyleSheet(""" - QPushButton { - background-color: rgba(255, 165, 0, 0.15); - color: #ffa500; - border: 1px solid rgba(255, 165, 0, 0.6); - border-radius: 4px; - padding: 8px 12px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 10px; - font-weight: 400; - min-height: 16px; - } - QPushButton:hover { - background-color: rgba(255, 165, 0, 0.25); - border: 1px solid #ffa500; - } - QPushButton:pressed { - background-color: rgba(255, 165, 0, 0.4); - color: #ffffff; - } - """) - elif button_type == "danger": - button.setStyleSheet(""" - QPushButton { - background-color: rgba(255, 99, 99, 0.15); - color: #ff6363; - border: 1px solid rgba(255, 99, 99, 0.6); - border-radius: 4px; - padding: 8px 12px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 10px; - font-weight: 400; - min-height: 16px; - } - QPushButton:hover { - background-color: rgba(255, 99, 99, 0.25); - border: 1px solid #ff6363; - } - QPushButton:pressed { - background-color: rgba(255, 99, 99, 0.4); - color: #ffffff; - } - """) - else: # default - # 使用全局默认样式 - pass - return button - - def _collision_button_style(self): - """返回碰撞检测面板按钮统一样式""" - return """ - QPushButton { - background-color: rgba(89, 98, 118, 0.4); - color: rgba(235, 235, 235, 0.85); - border: 1px solid rgba(89, 98, 118, 0.65); - border-radius: 4px; - padding: 8px 12px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 10px; - font-weight: 400; - min-height: 16px; - } - QPushButton:hover { - background-color: rgba(89, 98, 118, 0.55); - border: 1px solid rgba(89, 98, 118, 0.9); - color: #ffffff; - } - QPushButton:pressed { - background-color: rgba(89, 98, 118, 0.7); - border: 1px solid rgba(89, 98, 118, 0.95); - color: #ffffff; - } - QPushButton:disabled { - background-color: rgba(89, 98, 118, 0.18); - border: 1px solid rgba(89, 98, 118, 0.25); - color: rgba(235, 235, 235, 0.35); - } - """ - - def createModernGroupBox(self, title, parent_layout): - """创建现代化的分组框 - - Args: - title: 分组框标题 - parent_layout: 父布局 - - Returns: - tuple: (QGroupBox, QVBoxLayout) 分组框和其内部布局 - """ - group_box = QGroupBox(title) - group_layout = QVBoxLayout(group_box) - group_layout.setSpacing(0) # 减少内部间距 - group_layout.setContentsMargins(0, 3, 0, 6) # 减少内容边距 - - # 应用现代化样式,使用更紧凑的间距 - group_box.setStyleSheet(""" - QGroupBox { - font-weight: 500; - font-size: 11px; - color: rgba(255, 255, 255, 0.9); - margin-top: 12px; - margin-bottom: 4px; - padding-top: 15px; - border-top: 1px solid rgba(77, 116, 189, 0.3); - } - QGroupBox::title { - subcontrol-origin: padding; - subcontrol-position: top left; - left: 0px; - padding: 8px 5px 5px 0; - margin-top: 0px; - } - """) - - parent_layout.addWidget(group_box) - return group_box, group_layout - - def _getFigmaSpinBoxStyle(self): - """获取符合Figma设计的SpinBox样式""" - return """ - QDoubleSpinBox, QSpinBox { - background-color: rgba(89, 100, 113, 0.2); - color: #ebebeb; - border: 1px solid rgba(76, 92, 110, 0.6); - border-radius: 2px; - padding: 3px 6px; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 10px; - font-weight: 300; - letter-spacing: 0.5px; - min-width: 70px; - max-width: 70px; - min-height: 18px; - max-height: 18px; - } - QDoubleSpinBox:focus, QSpinBox:focus { - border: 1px solid #4d74bd; - background-color: rgba(77, 116, 189, 0.1); - } - QDoubleSpinBox:hover, QSpinBox:hover { - border: 1px solid rgba(77, 116, 189, 0.6); - background-color: rgba(89, 100, 113, 0.25); - } - QDoubleSpinBox::up-button, QSpinBox::up-button { - background-color: rgba(77, 116, 189, 0.2); - border: none; - border-radius: 2px; - width: 14px; - subcontrol-origin: border; - subcontrol-position: top right; - } - QDoubleSpinBox::down-button, QSpinBox::down-button { - background-color: rgba(77, 116, 189, 0.2); - border: none; - border-radius: 2px; - width: 14px; - subcontrol-origin: border; - subcontrol-position: bottom right; - } - QDoubleSpinBox::up-button:hover, QSpinBox::up-button:hover, - QDoubleSpinBox::down-button:hover, QSpinBox::down-button:hover { - background-color: rgba(77, 116, 189, 0.4); - } - QDoubleSpinBox::up-arrow, QSpinBox::up-arrow { - image: url(icons/up_arrows.png); - width: 10px; - height: 10px; - } - QDoubleSpinBox::down-arrow, QSpinBox::down-arrow { - image: url(icons/down_arrows.png); - width: 10px; - height: 10px; - } - """ - - def createPropertyRow(self, label_text, widget, layout, tooltip=None): - """创建属性行 - - Args: - label_text: 标签文字 - widget: 控件 - layout: 布局 - tooltip: 工具提示 - """ - row_layout = QHBoxLayout() - row_layout.setContentsMargins(0, 0, 0, 0) - row_layout.setSpacing(8) - - # 创建标签 - label = QLabel(label_text) - label.setMinimumWidth(60) - label.setStyleSheet(""" - QLabel { - color: rgba(255, 255, 255, 0.8); - font-size: 10px; - font-weight: 300; - } - """) - - if tooltip: - label.setToolTip(tooltip) - widget.setToolTip(tooltip) - - row_layout.addWidget(label) - row_layout.addWidget(widget) - row_layout.addStretch() - - layout.addLayout(row_layout) - - def setPropertyLayout(self, layout): - """设置属性面板布局引用""" - print("开始设置属性布局") - print(f"布局类型: {type(layout)}") - - # 保存布局引用 - self._propertyLayout = layout - - # 确保布局有父部件 - if not layout.parent(): - print("布局没有父部件,创建新的容器") - from PyQt5.QtWidgets import QWidget - container = QWidget() - container.setObjectName("PropertyContainer") - container.setLayout(layout) - - print(f"布局父部件: {self._propertyLayout.parent().objectName() if self._propertyLayout.parent() else 'None'}") - print(f"布局项目数: {self._propertyLayout.count()}") - - return True - - def clearPropertyPanel(self): - """清空属性面板""" - # 停止变换监控 - self.stopTransformMonitoring() - - if self._propertyLayout: - while self._propertyLayout.count(): - item = self._propertyLayout.takeAt(0) - if item.widget(): - item.widget().deleteLater() - - def updateNodeVisibilityAfterDrag(self, item): - """拖拽结束后更新节点的可见性状态""" - node = item.data(0, Qt.UserRole) - if not node: - return - - # 当节点被拖拽后,需要根据新父节点的状态来更新可见性 - self._syncEffectiveVisibility(node) - self._syncSceneVisibility() - - def _syncSceneVisibility(self): - scene_root = self.world.render - self._syncEffectiveVisibility(scene_root) - - def updatePropertyPanel(self, item): - """更新属性面板显示""" - # if not self._propertyLayout or not self._propertyLayout.parent(): - # print("属性布局未设置或没有父部件!") - # return - #更健壮的有效性检查 - - self._cleanupAllReferences() - self.clearPropertyPanel() - - # 应用紧凑样式到属性面板容器 - if self._propertyLayout.parent(): - self._propertyLayout.parent().setStyleSheet(self.compact_style) - - itemText = item.text(0) - - # 如果点击的是场景根节点,显示提示信息 - if itemText == "场景": - tipLabel = QLabel("") - tipLabel.setStyleSheet("color: gray;") - # self._propertyLayout.addRow(tipLabel) - self._propertyLayout.addWidget(tipLabel) - return - - model = item.data(0, Qt.UserRole) - - user_visible = True - if model: - user_visible = model.getPythonTag("user_visible") - if user_visible is None: - user_visible = True - model.setPythonTag("user_visible", True) - - self.name_group = QGroupBox("物体名称") - self.name_group.setProperty("groupRole", "first") - name_layout = QGridLayout() - self.active_check = QCheckBox() - # 根据模型的实际可见性状态设置复选框 - self.active_check.setChecked(user_visible) - self.active_check.stateChanged.connect(lambda state: self._toggleModelVisibility(model, state)) - self.name_input = QLineEdit(itemText) - - self.name_input.returnPressed.connect( - lambda: self.world.treeWidget.update_item_name(self.name_input.text(), item) - ) - - name_layout.addWidget(self.active_check, 0, 0) - name_layout.setColumnMinimumWidth(0, self.column_minimum_width) - name_layout.addWidget(self.name_input, 0, 1, 1, 3) - self.name_group.setLayout(name_layout) - self._propertyLayout.addWidget(self.name_group) - - # if model: - # try: - # self.active_check.stateChanged.disconnect() - # except TypeError: - # pass - # self.active_check.stateChanged.connect( - # lambda state, m=model: self._setUserVisible(m, state == Qt.Checked) - # ) - # nameLabel = QLabel("名称:") - # nameEdit = QLineEdit(itemText) - # self._propertyLayout.addRow(nameLabel, nameEdit) - - # 获取节点对象 - model = item.data(0, Qt.UserRole) - - if self._isTerrainNode(model, item): - self._showTerrainProperties(model, item) - elif model and hasattr(model, 'getTag') and model.getTag("element_type") == "cesium_tileset": - self._showCesiumTilesetProperties(model, item) - elif model and hasattr(model, 'getTag') and model.getTag("gui_type"): - self.updateGUIPropertyPanel(model, item) - elif model and hasattr(model, 'getTag') and model.getTag("light_type"): - self.updateLightPropertyPanel(model) - elif model: - self._updateModelPropertyPanel(model) - # 启动变换监控 - self.startTransformMonitoring(model) - - self._propertyLayout.addStretch() - - # 重置碰撞相关标志,确保下次选择时正常显示 - self._just_added_collision = False - - # 强制更新布局 - if self._propertyLayout: - self._propertyLayout.update() - propertyWidget = self._propertyLayout.parentWidget() - if propertyWidget: - propertyWidget.update() - - def _isTerrainNode(self, node, item): - """检查是否是地形节点""" - item_data = item.data(0, Qt.UserRole + 1) - if item_data == "terrain": - return True - - if hasattr(self.world, 'terrain_manager') and self.world.terrain_manager.terrains: - for terrain_info in self.world.terrain_manager.terrains: - if terrain_info['node'] == node: - return True - return False - - def _showTerrainProperties(self, terrain_node, item): - """显示地形属性面板""" - try: - terrain_info = None - if hasattr(self.world, 'terrain_manager'): - for info in self.world.terrain_manager.terrains: - if info['node'] == terrain_node: - terrain_info = info - break - if not terrain_info: - no_info_label = QLabel("未找到地形信息") - no_info_label.setStyleSheet("color:red;font-weight:bold;") - self._propertyLayout.addWidget(no_info_label) - return - - info_group = QGroupBox("地形信息") - info_layout = QGridLayout() - info_layout.setColumnMinimumWidth(0, self.column_minimum_width) - info_layout.addWidget(QLabel("名称:"), 0, 0) - name_label = QLabel(terrain_info.get('name', '未知')) - info_layout.addWidget(name_label, 0, 1, 1, 3) - - info_layout.addWidget(QLabel("类型:"), 1, 0) - type_label = QLabel("高度图地形" if terrain_info.get('heightmap') else "平面地形") - info_layout.addWidget(type_label, 1, 1, 1, 3) - - if terrain_info.get('heightmap'): - info_layout.addWidget(QLabel("高度图:"), 2, 0) - heightmap_label = QLabel(os.path.basename(terrain_info['heightmap'])) - heightmap_label.setWordWrap(True) - info_layout.addWidget(heightmap_label, 2, 1, 1, 3) - info_group.setLayout(info_layout) - self._propertyLayout.addWidget(info_group) - - # 变换属性 - self._updateTerrainTransformPanel(terrain_node) - # 地形编辑控制面板 - self._createTerrainEditPanel(terrain_info) - # 材质属性 - self._updateTerrainMaterialPanel(terrain_node, terrain_info) - - # #删除按钮 - # delete_btn = QPushButton("删除地形") - # delete_btn.setStyleSheet(""" - # QPushButton{ - # background-color:#ff4444; - # color:white; - # border:none; - # padding:8px; - # border-radius:4px; - # margin-top:10px - # } - # QPushButton:hover{ - # background-color:#ff6666; - # } - # """) - # delete_btn.clicked.connect(lambda:self._deleteTerrain(terrain_info,item)) - # self._propertyLayout.addWidget(delete_btn) - except Exception as e: - print(f"显示地形属性时出错: {e}") - import traceback - traceback.print_exc() - - def _updateTerrainTransformPanel(self, terrain_node): - """更新地形变化属性面板""" - try: - transform_group = QGroupBox("变换 Transform") - transform_layout = QGridLayout() - transform_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - pos = terrain_node.getPos() - scale = terrain_node.getScale() - - transform_layout.addWidget(QLabel("位置"), 0, 0) - - position_row = QHBoxLayout() - position_row.setContentsMargins(0, 0, 0, 0) - position_row.setSpacing(4) - - self.pos_x = QDoubleSpinBox() - self.pos_y = QDoubleSpinBox() - self.pos_z = QDoubleSpinBox() - - for pos_widget in [self.pos_x, self.pos_y, self.pos_z]: - pos_widget.setRange(-1000000.0, 1000000.0) - self.pos_x.setValue(pos.getX()) - self.pos_y.setValue(pos.getY()) - self.pos_z.setValue(pos.getZ()) - - def updateXPosition(value): - terrain_node.setX(value) - self.refreshModelValues(terrain_node) - - self.pos_x.valueChanged.connect(updateXPosition) - - def updateYPosition(value): - terrain_node.setY(value) - self.refreshModelValues(terrain_node) - - self.pos_y.valueChanged.connect(updateYPosition) - - def updateZPosition(value): - terrain_node.setZ(value) - self.refreshModelValues(terrain_node) - - self.pos_z.valueChanged.connect(updateZPosition) - - for axis_label, widget in (("X:", self.pos_x), ("Y:", self.pos_y), ("Z:", self.pos_z)): - axis = QLabel(axis_label) - axis.setAlignment(Qt.AlignVCenter) - axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - position_row.addWidget(axis) - position_row.addWidget(widget) - - transform_layout.addLayout(position_row, 0, 1, 1, 3) - - transform_layout.addWidget(QLabel("旋转"), 1, 0) - rotation_row = QHBoxLayout() - rotation_row.setContentsMargins(0, 0, 0, 0) - rotation_row.setSpacing(4) - - self.rot_x = QDoubleSpinBox() - self.rot_y = QDoubleSpinBox() - self.rot_z = QDoubleSpinBox() - - for rot_widget in [self.rot_x, self.rot_y, self.rot_z]: - rot_widget.setRange(-360, 360) - - self.rot_x.setValue(terrain_node.getH()) - self.rot_y.setValue(terrain_node.getP()) - self.rot_z.setValue(terrain_node.getR()) - - self.rot_x.valueChanged.connect(lambda v: terrain_node.setH(v)) - self.rot_y.valueChanged.connect(lambda v: terrain_node.setP(v)) - self.rot_z.valueChanged.connect(lambda v: terrain_node.setR(v)) - - for axis_label, widget in (("H:", self.rot_x), ("P:", self.rot_y), ("R:", self.rot_z)): - axis = QLabel(axis_label) - axis.setAlignment(Qt.AlignVCenter) - axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - rotation_row.addWidget(axis) - rotation_row.addWidget(widget) - - transform_layout.addLayout(rotation_row, 1, 1, 1, 3) - - transform_layout.addWidget(QLabel("缩放"), 2, 0) - - scale_row = QHBoxLayout() - scale_row.setContentsMargins(0, 0, 0, 0) - scale_row.setSpacing(4) - - self.scale_x = QDoubleSpinBox() - self.scale_y = QDoubleSpinBox() - self.scale_z = QDoubleSpinBox() - - for scale_widget in [self.scale_x, self.scale_y, self.scale_z]: - scale_widget.setRange(-1000, 1000) - scale_widget.setSingleStep(0.1) - self.scale_x.setValue(scale.getX()) - self.scale_y.setValue(scale.getY()) - self.scale_z.setValue(scale.getZ()) - - self.scale_x.valueChanged.connect(lambda v: self._updateXScale(terrain_node, v)) - self.scale_y.valueChanged.connect(lambda v: self._updateYScale(terrain_node, v)) - self.scale_z.valueChanged.connect(lambda v: self._updateZScale(terrain_node, v)) - - for axis_label, widget in (("X:", self.scale_x), ("Y:", self.scale_y), ("Z:", self.scale_z)): - axis = QLabel(axis_label) - axis.setAlignment(Qt.AlignVCenter) - axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - scale_row.addWidget(axis) - scale_row.addWidget(widget) - - transform_layout.addLayout(scale_row, 2, 1, 1, 3) - - transform_group.setLayout(transform_layout) - self._propertyLayout.addWidget(transform_group) - except Exception as e: - print(f"更新地形变换面板时出错: {e}") - - def _createTerrainEditPanel(self, terrain_info): - try: - edit_group = QGroupBox("地形编辑") - edit_layout = QGridLayout() - edit_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - # 编辑半径 - edit_layout.addWidget(QLabel("编辑半径:"), 0, 0) - self.terrain_radius_spin = QDoubleSpinBox() - self.terrain_radius_spin.setRange(0.1, 50.0) - self.terrain_radius_spin.setSingleStep(0.5) - self.terrain_radius_spin.setValue(getattr(self.world, 'terrain_edit_radius', 3.0)) - self.terrain_radius_spin.valueChanged.connect(self._onTerrainRadiusChanged) - edit_layout.addWidget(self.terrain_radius_spin, 0, 1, 1, 3) - - # 编辑强度 - edit_layout.addWidget(QLabel("编辑强度:"), 1, 0) - self.terrain_strength_spin = QDoubleSpinBox() - self.terrain_strength_spin.setRange(0.01, 50) - self.terrain_strength_spin.setSingleStep(0.1) - self.terrain_strength_spin.setValue(getattr(self.world, 'terrain_edit_strength', 0.3)) - self.terrain_strength_spin.valueChanged.connect(self._onTerrainStrengthChanged) - edit_layout.addWidget(self.terrain_strength_spin, 1, 1, 1, 3) - - edit_layout.addWidget(QLabel("操作类型:"), 2, 0) - self.terrain_operation_combo = QComboBox() - self.terrain_operation_combo.addItems(["升高地形", "降低地形", "平坦化"]) - current_op = getattr(self.world, 'terrain_edit_operation', "add") - if current_op == "add": - self.terrain_operation_combo.setCurrentText("升高地形") - elif current_op == "subtract": - self.terrain_operation_combo.setCurrentText("降低地形") - else: - self.terrain_operation_combo.setCurrentText("平坦化") - self.terrain_operation_combo.currentTextChanged.connect(self._onTerrainOperationChanged) - edit_layout.addWidget(self.terrain_operation_combo, 2, 1, 1, 3) - - help_label = QLabel("在地形编辑模式下,使用鼠标左键升高地形,右键降低地形") - help_label.setWordWrap(True) - help_label.setStyleSheet("font-size:10px;color:gray;") - edit_layout.addWidget(help_label, 3, 0, 1, 4) - - edit_group.setLayout(edit_layout) - self._propertyLayout.addWidget(edit_group) - - # 确保初始值被正确设置到 world 对象上 - self._onTerrainRadiusChanged(self.terrain_radius_spin.value()) - self._onTerrainStrengthChanged(self.terrain_strength_spin.value()) - self._onTerrainOperationChanged(self.terrain_operation_combo.currentText()) - except Exception as e: - print(f"创建地形编辑面板时出错: {e}") - - def _onTerrainRadiusChanged(self, value): - """地形编辑半径改变""" - if hasattr(self.world, 'terrain_edit_radius'): - self.world.terrain_edit_radius = float(value) - - def _onTerrainStrengthChanged(self, value): - """地形编辑强度改变""" - if hasattr(self.world, 'terrain_edit_strength'): - self.world.terrain_edit_strength = float(value) - - def _onTerrainOperationChanged(self, text): - """地形编辑操作类型改变""" - operation_map = { - "升高地形": "add", - "降低地形": "subtract", - "平坦化": "set" - } - if hasattr(self.world, 'terrain_edit_operation'): - # 正确设置地形编辑操作属性 - self.world.terrain_edit_operation = operation_map.get(text, "add") - print(f"地形编辑操作已设置为: {self.world.terrain_edit_operation}") - - def _updateTerrainMaterialPanel(self, terrain_node, terrain_info): - """更新地形材质属性面板""" - try: - material_group = QGroupBox("材质属性") - material_layout = QGridLayout() - material_layout.setColumnMinimumWidth(0, self.column_minimum_width) - # 颜色设置 - material_layout.addWidget(QLabel("颜色:"), 0, 0) - color_button = QPushButton("选择颜色") - color_button.clicked.connect(lambda: self._selectTerrainColor(terrain_info)) - material_layout.addWidget(color_button, 0, 1, 1, 3) - # 纹理设置 - material_layout.addWidget(QLabel("纹理:"), 1, 0) - texture_button = QPushButton("选择纹理") - texture_button.clicked.connect(lambda: self._selectTerrainTexture(terrain_info)) - material_layout.addWidget(texture_button, 1, 1, 1, 3) - - material_group.setLayout(material_layout) - self._propertyLayout.addWidget(material_group) - except Exception as e: - print(f"更新材质面板时出错{e}") - - def _selectTerrainColor(self, terrain_info): - try: - from PyQt5.QtWidgets import QColorDialog - from PyQt5.QtGui import QColor - - current_color = QColor(255, 255, 255) - - color = QColorDialog.getColor(current_color, None, "选择地形颜色") - if color.isValid(): - r, g, b = color.red() / 255.0, color.green() / 255.0, color.blue() / 255.0 - if hasattr(self.world, 'terrain_manager'): - self.world.terrain_manager.setTerrainColor(terrain_info, (r, g, b)) - except Exception as e: - print(f"选择地形颜色时出错: {e}") - - def _selectTerrainTexture(self, terrain_info): - """选择地形纹理""" - try: - from PyQt5.QtWidgets import QFileDialog - file_path, _ = QFileDialog.getOpenFileName( - None, - "选择地形纹理", - "", - "图像文件(*.png *.jpg *.jpeg *.bmp *.tga *.dds)" - ) - - if file_path and os.path.exists(file_path): - if hasattr(self.world, 'terrain_manager'): - success = self.world.terrain_manager.setTerrainTexture(terrain_info, file_path) - if success: - print(f"地形纹理已应用{file_path}") - else: - print(f"应用地形纹理失败") - except Exception as e: - print(f"选择地形纹理时出错: {e}") - - def _deleteTerrain(self, terrain_info, item): - """删除地形""" - try: - from PyQt5.QtWidgets import QMessageBox - - reply = QMessageBox.question( - None, - '确认删除', - f'确定要删除地形 "{terrain_info.get("name", "未知地形")}" 吗?', - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No - ) - - if reply == QMessageBox.Yes: - # 调用 interface_manager 中已经实现的 deleteNode 方法 - if hasattr(self.world, 'interface_manager') and self.world.interface_manager.treeWidget: - self.world.interface_manager.deleteNode(terrain_info['node'], item) - else: - # 如果 interface_manager 不可用,使用原来的删除逻辑 - if hasattr(self.world, 'terrain_manager'): - success = self.world.terrain_manager.deleteTerrain(terrain_info) - if success: - # 更新场景树 - # if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, - # 'updateSceneTree'): - # self.world.scene_manager.updateSceneTree() - # - # # 清空属性面板 - # self.clearPropertyPanel() - - print(f"✓ 地形已删除: {terrain_info.get('name', '未知')}") - else: - print("✗ 删除地形失败") - - except Exception as e: - print(f"删除地形时出错: {e}") - - def _cleanupAllReferences(self): - """清理所有控件引用""" - # 停止变换监控 - self.stopTransformMonitoring() - - # 清理变换控件引用 - self._cleanupTransformControls() - - # 清理信息面板监控 - if hasattr(self, '_info_panel_timer'): - self._info_panel_timer.stop() - self._info_panel_timer.deleteLater() - del self._info_panel_timer - - # 清理信息面板控件引用 - if hasattr(self, '_info_panel_controls'): - self._info_panel_controls.clear() - del self._info_panel_controls - - if hasattr(self, '_current_info_panel'): - del self._current_info_panel - - # 清理碰撞相关控件引用 - collision_controls = [ - 'collision_status_badge', 'collision_shape_combo', 'collision_shape_label', - 'collision_visibility_button', 'collision_button', 'collision_layout', 'collision_group', - 'collision_pos_x', 'collision_pos_y', 'collision_pos_z', - 'collision_radius', 'collision_width', 'collision_length', 'collision_height', - 'collision_capsule_radius', 'collision_capsule_height', - 'collision_normal_x', 'collision_normal_y', 'collision_normal_z' - ] - for name in collision_controls: - if hasattr(self, name): - delattr(self, name) - - # 清理其他可能的控件引用 - other_controls = ['scale_x', 'scale_y', 'scale_z', 'pos_x', 'pos_y', 'pos_z'] - for name in other_controls: - if hasattr(self, name): - setattr(self, name, None) - - def _setUserVisible(self, node, visible): - node.setPythonTag("user_visible", visible) - self._syncEffectiveVisibility(node) - - def _setUserVisible_light(self, node, visible): - """设置用户可见性状态""" - try: - # 保存可见性状态 - node.setPythonTag("user_visible", visible) - - except Exception as e: - print(f"设置用户可见性失败: {e}") - - def _syncEffectiveVisibility(self, start_node): - """广度优先,确保父隐藏则子一定隐藏""" - # 获取起始节点的父节点 - parent_node = start_node.getParent() - - # 确定父节点的有效可见性 - parent_effective_visible = True - if parent_node: - parent_effective_visible = parent_node.getPythonTag("effective_visible") - if parent_effective_visible is None: - parent_effective_visible = True - - q = deque([(start_node, parent_effective_visible)]) # (node, parent_effective_visible) - - while q: - node, parent_eff = q.popleft() - user = node.getPythonTag("user_visible") - if user is None: - user = True - eff = parent_eff and user - node.setPythonTag("effective_visible", eff) - - # 特殊处理:检查是否为碰撞体节点 - - if node.getName().startswith("modelCollision_"): - node.hide() - else: - if eff: - node.show() - else: - node.hide() - - for child in node.getChildren(): - q.append((child, eff)) - - def _toggleModelVisibility(self, model, state): - """切换模型可见性状态""" - try: - # 特殊处理灯光对象 - scene_manager = None - if hasattr(self.world, 'scene_manager'): - scene_manager = self.world.scene_manager - - if scene_manager and hasattr(scene_manager, 'isLightObject'): - is_light = scene_manager.isLightObject(model) - - if is_light: - visible = (state == Qt.Checked) - self._setUserVisible_light(model, visible) - if hasattr(scene_manager, 'toggleLightVisibility'): - scene_manager.toggleLightVisibility(model, visible) - return # 关键:这里必须return,避免执行下面的普通模型逻辑 - else: - visible = (state == Qt.Checked) - self._setUserVisible(model, visible) - - collision_nodes = model.findAllMatches("**/modelCollision_*") - for collision_node in collision_nodes: - collision_node.hide() - - except Exception as e: - print(f"切换模型可见性失败: {str(e)}") - import traceback - traceback.print_exc() - - def refreshModelValues(self, nodePath): - """刷新模型值显示""" - if not nodePath or self._propertyLayout is None: - return - - # 检查面板是否仍然有效 - if not self._isPropertyPanelValid(): - return - - try: - # 检查是否是GUI元素 - is_gui_element = (hasattr(nodePath, 'getTag') and - nodePath.getTag("is_gui_element") == "1") - - if is_gui_element: - # 对于GUI元素,更新所有相关属性 - self._refreshGUIElementValues(nodePath) - else: - # 对于普通3D模型,更新位置、旋转、缩放 - self._refreshModelValues(nodePath) - - except Exception as e: - print(f"刷新模型值显示失败: {e}") - - def startTransformMonitoring(self, nodePath): - """开始监控节点的变换变化""" - # 如果已有监控器在运行,先停止它 - self.stopTransformMonitoring() - - # 保存初始变换值 - self._saveCurrentTransformValues(nodePath) - - # 创建并启动定时器 - self._transform_monitor_timer = QTimer() - self._transform_monitor_timer.timeout.connect(lambda: self._checkTransformChanges(nodePath)) - self._transform_monitor_timer.start(100) # 每100毫秒检查一次 - - def stopTransformMonitoring(self): - """停止监控节点的变换变化""" - if self._transform_monitor_timer: - self._transform_monitor_timer.stop() - self._transform_monitor_timer.deleteLater() - self._transform_monitor_timer = None - - # 清除保存的变换值 - self._last_transform_values.clear() - - def _saveCurrentTransformValues(self, nodePath): - """保存当前节点的变换值""" - try: - pos = nodePath.getPos() - hpr = nodePath.getHpr() - scale = nodePath.getScale() - - self._last_transform_values = { - 'pos': (pos.getX(), pos.getY(), pos.getZ()), - 'hpr': (hpr.getX(), hpr.getY(), hpr.getZ()), - 'scale': (scale.getX(), scale.getY(), scale.getZ()) - } - except Exception as e: - print(f"保存变换值失败: {e}") - - def _checkTransformChanges(self, nodePath): - """检查节点变换是否发生变化""" - try: - # 获取当前变换值 - pos = nodePath.getPos() - hpr = nodePath.getHpr() - scale = nodePath.getScale() - - current_values = { - 'pos': (pos.getX(), pos.getY(), pos.getZ()), - 'hpr': (hpr.getX(), hpr.getY(), hpr.getZ()), - 'scale': (scale.getX(), scale.getY(), scale.getZ()) - } - - # 比较变换值是否发生变化 - if current_values != self._last_transform_values: - # 变换已更改,刷新属性面板 - self.refreshModelValues(nodePath) - # 更新保存的变换值 - self._last_transform_values = current_values - except Exception as e: - print(f"检查变换变化失败: {e}") - - def _refreshGUIElementValues(self, gui_element): - """刷新GUI元素值显示""" - try: - # 更新位置属性 - if hasattr(self, 'pos_x') and self.pos_x: - pos = gui_element.getPos() - gui_type = gui_element.getTag("gui_type") - - if gui_type in ["button", "label", "entry", "2d_image"]: - # 2D GUI组件使用屏幕坐标 - logical_x = pos.getX() / 0.1 - logical_z = pos.getZ() / 0.1 - - self._safeUpdateSpinBox('pos_x', logical_x) - self._safeUpdateSpinBox('pos_z', logical_z) - else: - # 3D GUI组件使用世界坐标(包括 video_screen) - pos = gui_element.getPos() - - # 修复:确保 video_screen 也能正确更新位置控件 - if hasattr(self, 'pos_x') and self.pos_x: - self._safeUpdateSpinBox('pos_x', pos.getX()) - if hasattr(self, 'pos_y') and self.pos_y: - self._safeUpdateSpinBox('pos_y', pos.getY()) - if hasattr(self, 'pos_z') and self.pos_z: - self._safeUpdateSpinBox('pos_z', pos.getZ()) - - # 更新缩放属性 - if hasattr(self, 'scale_x') and self.scale_x: - scale = gui_element.getScale() - self._safeUpdateSpinBox('scale_x', scale.getX()) - if hasattr(self, 'scale_y') and self.scale_y: - self._safeUpdateSpinBox('scale_y', scale.getY()) - if hasattr(self, 'scale_z') and self.scale_z: - self._safeUpdateSpinBox('scale_z', scale.getZ()) - - # 更新旋转属性(如果存在) - if hasattr(self, 'rot_x') and self.rot_x: - hpr = gui_element.getHpr() - self._safeUpdateSpinBox('rot_x', hpr.getX()) - self._safeUpdateSpinBox('rot_y', hpr.getY()) - self._safeUpdateSpinBox('rot_z', hpr.getZ()) - - except Exception as e: - print(f"刷新GUI元素值显示失败: {e}") - - def _refreshModelValues(self, nodePath): - """刷新普通模型值显示""" - try: - parent = nodePath.getParent() - render = self.world.render - relPos = nodePath.getPos(parent) if parent else nodePath.getPos() - - # 安全地更新位置控件 - self._safeUpdateSpinBox('pos_x', relPos.getX()) - self._safeUpdateSpinBox('pos_y', relPos.getY()) - self._safeUpdateSpinBox('pos_z', relPos.getZ()) - - # 安全地更新世界位置控件 - worldPos = nodePath.getPos(render) - self._safeUpdateSpinBox('world_pos_x', worldPos.getX()) - self._safeUpdateSpinBox('world_pos_y', worldPos.getY()) - self._safeUpdateSpinBox('world_pos_z', worldPos.getZ()) - - # 安全地更新旋转控件(如果存在) - if hasattr(self, 'rot_x') or hasattr(self, 'rot_y') or hasattr(self, 'rot_z'): - hpr = nodePath.getHpr() - self._safeUpdateSpinBox('rot_x', hpr.getX()) - self._safeUpdateSpinBox('rot_y', hpr.getY()) - self._safeUpdateSpinBox('rot_z', hpr.getZ()) - - # 安全地更新缩放控件(如果存在) - if hasattr(self, 'scale_x') or hasattr(self, 'scale_y') or hasattr(self, 'scale_z'): - scale = nodePath.getScale() - self._safeUpdateSpinBox('scale_x', scale.getX()) - self._safeUpdateSpinBox('scale_y', scale.getY()) - self._safeUpdateSpinBox('scale_z', scale.getZ()) - - except Exception as e: - print(f"刷新模型值显示失败: {e}") - - def _isPropertyPanelValid(self): - """检查属性面板是否仍然有效""" - try: - # 检查布局是否仍然存在且有效 - if not self._propertyLayout: - return False - - # 检查父控件是否仍然存在 - parent = self._propertyLayout.parent() - if not parent: - return False - - # 检查父控件是否仍然在窗口中 - return parent.isVisible() - except: - return False - - def _safeUpdateSpinBox(self, attr_name, value): - """安全地更新数值框""" - try: - spinbox = getattr(self, attr_name, None) - if spinbox and not spinbox.isHidden(): # 检查控件是否仍然存在且可见 - # 检查对象是否仍然有效 - spinbox.blockSignals(True) - spinbox.setKeyboardTracking(False) # 确保禁用键盘跟踪 - spinbox.setValue(value) - spinbox.blockSignals(False) - except RuntimeError as e: - # 对象已被删除 - setattr(self, attr_name, None) - print(f"警告: 数值框 {attr_name} 已被删除: {e}") - except Exception as e: - print(f"更新数值框 {attr_name} 时出错: {e}") - - # 在创建位置和变换控件时,增加安全检查 - def _createTransformControls(self, nodePath): - """创建变换控制控件""" - try: - # 清理旧的引用 - self._cleanupTransformControls() - - # 创建新的控件引用 - transform_layout = QGridLayout() - - # 位置控件 - transform_layout.addWidget(QLabel("相对位置"), 0, 0) - - # X坐标 - transform_layout.addWidget(QLabel("X:"), 1, 0) - self.pos_x = QLineEdit() - self.pos_x.setText(str(round(nodePath.getX(), 6))) - self.pos_x.editingFinished.connect(lambda: self._onPositionEditFinished(nodePath, 'x')) - transform_layout.addWidget(self.pos_x, 1, 1) - - # Y坐标 - transform_layout.addWidget(QLabel("Y:"), 1, 2) - self.pos_y = QLineEdit() - self.pos_y.setText(str(round(nodePath.getY(), 6))) - self.pos_y.editingFinished.connect(lambda: self._onPositionEditFinished(nodePath, 'y')) - transform_layout.addWidget(self.pos_y, 1, 3) - - # Z坐标 - transform_layout.addWidget(QLabel("Z:"), 1, 4) - self.pos_z = QLineEdit() - self.pos_z.setText(str(round(nodePath.getZ(), 6))) - self.pos_z.editingFinished.connect(lambda: self._onPositionEditFinished(nodePath, 'z')) - transform_layout.addWidget(self.pos_z, 1, 5) - - return transform_layout - - except Exception as e: - print(f"创建变换控件时出错: {e}") - return None - - def _onPositionEditFinished(self, nodePath, axis): - """位置编辑完成时的处理""" - try: - # 检查控件是否仍然有效 - if not hasattr(self, f'pos_{axis}') or getattr(self, f'pos_{axis}') is None: - return - - line_edit = getattr(self, f'pos_{axis}') - if line_edit is None or line_edit.isHidden(): - return - - # 获取文本并转换为数值 - text = line_edit.text() - try: - new_value = float(text) - except ValueError: - print(f"无效的数值输入: {text}") - # 恢复原来的值 - if axis == 'x': - line_edit.setText(str(round(nodePath.getX(), 6))) - elif axis == 'y': - line_edit.setText(str(round(nodePath.getY(), 6))) - elif axis == 'z': - line_edit.setText(str(round(nodePath.getZ(), 6))) - return - - # 根据轴设置位置 - if axis == 'x': - nodePath.setX(new_value) - elif axis == 'y': - nodePath.setY(new_value) - elif axis == 'z': - nodePath.setZ(new_value) - - print(f"位置已更新: {nodePath.getName()} {axis.upper()} = {new_value}") - - # 如果是坐标轴节点,需要更新坐标轴位置 - if hasattr(self.world, 'selection_manager'): - selection_manager = self.world.selection_manager - if (hasattr(selection_manager, 'gizmoTarget') and - selection_manager.gizmoTarget == nodePath): - # 更新坐标轴位置 - selection_manager._updateGizmoPositionAndOrientation() - - except Exception as e: - print(f"更新位置时出错: {e}") - def _createSafeSpinBox(self, min_val, max_val, read_only=False): - """创建安全的数值框""" - try: - spinbox = QDoubleSpinBox() - spinbox.setRange(min_val, max_val) - spinbox.setSingleStep(0.1) - if read_only: - spinbox.setReadOnly(True) - spinbox.setStyleSheet("background-color: #f0f0f0;") - return spinbox - except Exception as e: - print(f"创建数值框失败: {e}") - return None - - def _cleanupTransformControls(self): - """清理变换控件引用""" - control_names = ['pos_x', 'pos_y', 'pos_z', 'world_pos_x', 'world_pos_y', 'world_pos_z'] - for name in control_names: - if hasattr(self, name): - control = getattr(self, name) - if control: - try: - # 断开所有信号连接 - control.valueChanged.disconnect() - except: - pass - setattr(self, name, None) - - def _refreshWorldPos(self, model): - if not hasattr(self, 'worldXSpin'): - return - world = model.getPos(self.world.render) - self._worldXSpin.setValue(world.x) - self._worldYSpin.setValue(world.y) - self._worldZSpin.setValue(world.z) - - def _showCesiumTilesetProperties(self, nodePath, item): - """显示 Cesium tileset 属性""" - from PyQt5.QtWidgets import QLabel, QDoubleSpinBox, QPushButton, QGroupBox, QFormLayout - from PyQt5.QtCore import Qt - - print(f"显示 Cesium tileset 属性: {nodePath.getName()}") - - # 标题 - tileset_group = QGroupBox("Cesium 3D Tiles") - tileset_layout = QGridLayout() - tileset_layout.setColumnMinimumWidth(0, self.column_minimum_width) - # self._propertyLayout.addWidget(title) - - # URL 信息 - if nodePath.hasTag("tileset_url"): - url_label = QLabel("URL:") - tileset_layout.addWidget(url_label, 0, 0) - url_value = QLabel(nodePath.getTag("tileset_url")) - url_value.setWordWrap(True) - url_value.setStyleSheet("font-size: 9px; color: #666;") - tileset_layout.addWidget(url_value, 0, 1, 1, 3) - # self._propertyLayout.addWidget(url_label) - # self._propertyLayout.addWidget(url_value) - tileset_group.setLayout(tileset_layout) - self._propertyLayout.addWidget(tileset_group) - - # 位置控制 - pos_group = QGroupBox("位置") - pos_layout = QGridLayout() - pos_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - pos_row = QHBoxLayout() - pos_row.setContentsMargins(0, 0, 0, 0) - pos_row.setSpacing(4) - pos_layout.addWidget(QLabel("位置"), 0, 0) - - # X 坐标 - x_spin = QDoubleSpinBox() - x_spin.setRange(-10000, 10000) - x_spin.setSingleStep(1.0) - x_spin.setDecimals(2) - pos = nodePath.getPos() - x_spin.setValue(pos.getX()) - x_spin.valueChanged.connect(lambda v: self._updateTilesetPosition(nodePath, 'x', v)) - x_label = QLabel("X:") - x_label.setAlignment(Qt.AlignVCenter) - x_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - pos_row.addWidget(x_label) - pos_row.addWidget(x_spin) - - # Y 坐标 - y_spin = QDoubleSpinBox() - y_spin.setRange(-10000, 10000) - y_spin.setSingleStep(1.0) - y_spin.setDecimals(2) - y_spin.setValue(pos.getY()) - y_spin.valueChanged.connect(lambda v: self._updateTilesetPosition(nodePath, 'y', v)) - y_label = QLabel("Y:") - y_label.setAlignment(Qt.AlignVCenter) - y_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - pos_row.addWidget(y_label) - pos_row.addWidget(y_spin) - - # Z 坐标 - z_spin = QDoubleSpinBox() - z_spin.setRange(-10000, 10000) - z_spin.setSingleStep(1.0) - z_spin.setDecimals(2) - z_spin.setValue(pos.getZ()) - z_spin.valueChanged.connect(lambda v: self._updateTilesetPosition(nodePath, 'z', v)) - z_label = QLabel("Z:") - z_label.setAlignment(Qt.AlignVCenter) - z_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - pos_row.addWidget(z_label) - pos_row.addWidget(z_spin) - - pos_layout.addLayout(pos_row, 0, 1, 1, 3) - - pos_group.setLayout(pos_layout) - - # 缩放控制 - scale_label = QLabel("缩放") - pos_layout.addWidget(scale_label, 1, 0) - scale_spin = QDoubleSpinBox() - scale_spin.setRange(0.01, 1000) - scale_spin.setSingleStep(0.1) - scale_spin.setDecimals(2) - scale_spin.setValue(nodePath.getScale().getX()) - scale_spin.valueChanged.connect(lambda v: self._updateTilesetScale(nodePath, v)) - pos_layout.addWidget(scale_spin, 1, 1, 1, 3) - - self._propertyLayout.addWidget(pos_group) - - # # 删除按钮 - # delete_btn = QPushButton("删除 Tileset") - # delete_btn.setStyleSheet(""" - # QPushButton { - # background-color: #ff4444; - # color: white; - # border: none; - # padding: 8px; - # border-radius: 4px; - # margin-top: 10px; - # } - # QPushButton:hover { - # background-color: #ff6666; - # } - # """) - # delete_btn.clicked.connect(lambda: self._deleteCesiumTileset(nodePath, item)) - # self._propertyLayout.addWidget(delete_btn) - - # 添加弹性空间 - self._propertyLayout.addStretch() - - def _createPositionControl(self, label, nodePath, axis): - """创建位置控制控件""" - from PyQt5.QtWidgets import QHBoxLayout, QLabel, QDoubleSpinBox - - layout = QHBoxLayout() - - axis_label = QLabel(label) - axis_label.setFixedWidth(20) - layout.addWidget(axis_label) - - spinbox = QDoubleSpinBox() - spinbox.setRange(-10000, 10000) - spinbox.setSingleStep(1.0) - spinbox.setDecimals(2) - - # 获取当前坐标值 - pos = nodePath.getPos() - if axis == 'x': - spinbox.setValue(pos.getX()) - elif axis == 'y': - spinbox.setValue(pos.getY()) - elif axis == 'z': - spinbox.setValue(pos.getZ()) - - # 连接值变化信号 - def onValueChanged(value): - self._updateTilesetPosition(nodePath, axis, value) - - spinbox.valueChanged.connect(onValueChanged) - layout.addWidget(spinbox) - - return layout - - def _createScaleControl(self, nodePath): - """创建缩放控制控件""" - from PyQt5.QtWidgets import QHBoxLayout, QLabel, QDoubleSpinBox - - layout = QHBoxLayout() - - scale_label = QLabel("缩放:") - scale_label.setFixedWidth(40) - layout.addWidget(scale_label) - - spinbox = QDoubleSpinBox() - spinbox.setRange(0.01, 1000) - spinbox.setSingleStep(0.1) - spinbox.setDecimals(2) - spinbox.setValue(nodePath.getScale().getX()) # 假设均匀缩放 - - def onScaleChanged(value): - self._updateTilesetScale(nodePath, value) - - spinbox.valueChanged.connect(onScaleChanged) - layout.addWidget(spinbox) - - return layout - - def _updateTilesetPosition(self, nodePath, axis, value): - """更新 tileset 位置""" - try: - pos = nodePath.getPos() - if axis == 'x': - nodePath.setPos(value, pos.getY(), pos.getZ()) - elif axis == 'y': - nodePath.setPos(pos.getX(), value, pos.getZ()) - elif axis == 'z': - nodePath.setPos(pos.getX(), pos.getY(), value) - print(f"更新 {nodePath.getName()} 位置: {axis} = {value}") - except Exception as e: - print(f"更新位置失败: {e}") - - def _updateTilesetScale(self, nodePath, value): - """更新 tileset 缩放""" - try: - nodePath.setScale(value) - print(f"更新 {nodePath.getName()} 缩放: {value}") - except Exception as e: - print(f"更新缩放失败: {e}") - - def _deleteCesiumTileset(self, nodePath, item): - """删除 Cesium tileset""" - try: - from PyQt5.QtWidgets import QMessageBox - - reply = QMessageBox.question( - None, - '确认删除', - f'确定要删除 Cesium tileset "{nodePath.getName()}" 吗?', - QMessageBox.Yes | QMessageBox.No, - QMessageBox.No - ) - - if reply == QMessageBox.Yes: - tree_widget = self.world.treeWidget - tree_widget.delete_item(nodePath) - # # 从场景中移除 - # nodePath.removeNode() - # - # # 从 tilesets 列表中移除 - # if hasattr(self.world, 'scene_manager'): - # tilesets_to_remove = [] - # for i, tileset_info in enumerate(self.world.scene_manager.tilesets): - # if tileset_info['node'] == nodePath: - # tilesets_to_remove.append(i) - # - # # 从后往前删除,避免索引问题 - # for i in reversed(tilesets_to_remove): - # del self.world.scene_manager.tilesets[i] - # - # # 更新场景树 - # self.world.scene_manager.updateSceneTree() - # - # # 清空属性面板 - # self.clearPropertyPanel() - - # print(f"成功删除 Cesium tileset: {nodePath.getName()}") - - except Exception as e: - print(f"删除 Cesium tileset 失败: {str(e)}") - - def _updateModelPropertyPanel(self, model): - """更新模型属性面板""" - - if hasattr(model, 'getTag') and model.getTag("is_gui_element") == "1": - self.updateGUIPropertyPanel(model) - return - - # 获取父节点 - parent = model.getParent() - - # 变换属性部分 (Transform) - self.transform_group = QGroupBox("变换 Transform") - transform_layout = QGridLayout() - transform_layout.setColumnMinimumWidth(0, self.column_minimum_width) - # transform_layout.setColumnStretch(1, 1) - # transform_layout.setColumnStretch(2, 1) - # transform_layout.setColumnStretch(3, 1) - - - # 获取当前值 - relativePos = model.getPos(parent) if parent else model.getPos() - worldPos = model.getPos(self.world.render) - - position_label = QLabel("位置") - transform_layout.addWidget(position_label, 0, 0) - - # 位置 (Position) - position_row = QHBoxLayout() - position_row.setContentsMargins(0, 0, 0, 0) - position_row.setSpacing(4) - # position_row.addWidget(QLabel("位置")) - - self.pos_x = QDoubleSpinBox() - self.pos_y = QDoubleSpinBox() - self.pos_z = QDoubleSpinBox() - - # 设置位置控件属性 - for pos_widget in [self.pos_x, self.pos_y, self.pos_z]: - pos_widget.setRange(-1000000.0, 1000000.0) - - self.pos_x.setValue(relativePos.getX()) - self.pos_y.setValue(relativePos.getY()) - self.pos_z.setValue(relativePos.getZ()) - - # 连接位置变化事件 - def updateXPosition(value): - model.setX(value) - self.refreshModelValues(model) - - self.pos_x.valueChanged.connect(updateXPosition) - - def updateYPosition(value): - model.setY(value) - self.refreshModelValues(model) - - self.pos_y.valueChanged.connect(updateYPosition) - - def updateZPosition(value): - model.setZ(value) - self.refreshModelValues(model) - - self.pos_z.valueChanged.connect(updateZPosition) - - # 水平排布 X, Y, Z - for axis_label, widget in (("X:", self.pos_x), ("Y:", self.pos_y), ("Z:", self.pos_z)): - axis = QLabel(axis_label) - axis.setAlignment(Qt.AlignVCenter) - axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - position_row.addWidget(axis) - position_row.addWidget(widget) - - transform_layout.addLayout(position_row, 0, 1, 1, 3) - - # 世界位置 (只读) - world_position_row = QHBoxLayout() - world_position_row.setContentsMargins(0, 0, 0, 0) - world_position_row.setSpacing(4) - # world_position_row.addWidget(QLabel("世界位置")) - world_position_label = QLabel("世界位置") - transform_layout.addWidget(world_position_label, 1, 0) - - self.world_pos_x = QDoubleSpinBox() - self.world_pos_y = QDoubleSpinBox() - self.world_pos_z = QDoubleSpinBox() - - for world_pos_widget in [self.world_pos_x, self.world_pos_y, self.world_pos_z]: - world_pos_widget.setRange(-1000000.0, 1000000.0) - world_pos_widget.setReadOnly(True) - - self.world_pos_x.setValue(worldPos.getX()) - self.world_pos_y.setValue(worldPos.getY()) - self.world_pos_z.setValue(worldPos.getZ()) - - for axis_label, widget in (("X:", self.world_pos_x), ("Y:", self.world_pos_y), ("Z:", self.world_pos_z)): - axis = QLabel(axis_label) - axis.setAlignment(Qt.AlignVCenter) - axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - world_position_row.addWidget(axis) - world_position_row.addWidget(widget) - - transform_layout.addLayout(world_position_row, 1, 1, 1, 3) - - # 旋转 (Rotation) - rotation_row = QHBoxLayout() - rotation_row.setContentsMargins(0, 0, 0, 0) - rotation_row.setSpacing(4) - rotation_label = QLabel("旋转") - transform_layout.addWidget(rotation_label, 2, 0) - - self.rot_x = QDoubleSpinBox() - self.rot_y = QDoubleSpinBox() - self.rot_z = QDoubleSpinBox() - - for rot_widget in [self.rot_x, self.rot_y, self.rot_z]: - rot_widget.setRange(-360, 360) - - self.rot_x.setValue(model.getH()) - self.rot_y.setValue(model.getP()) - self.rot_z.setValue(model.getR()) - - self.rot_x.valueChanged.connect(lambda v: model.setH(v)) - self.rot_y.valueChanged.connect(lambda v: model.setP(v)) - self.rot_z.valueChanged.connect(lambda v: model.setR(v)) - - for axis_label, widget in (("H:", self.rot_x), ("P:", self.rot_y), ("R:", self.rot_z)): - axis = QLabel(axis_label) - axis.setAlignment(Qt.AlignVCenter) - axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - rotation_row.addWidget(axis) - rotation_row.addWidget(widget) - - transform_layout.addLayout(rotation_row, 2, 1, 1, 3) - - # 缩放 (Scale) - scale_row = QHBoxLayout() - scale_row.setContentsMargins(0, 0, 0, 0) - scale_row.setSpacing(4) - scale_label = QLabel("缩放") - transform_layout.addWidget(scale_label, 3, 0) - - self.scale_x = QDoubleSpinBox() - self.scale_y = QDoubleSpinBox() - self.scale_z = QDoubleSpinBox() - - current_scale = model.getScale() - - for scale_widget, scale_value in zip([self.scale_x, self.scale_y, self.scale_z], - [current_scale.getX(), current_scale.getY(), - current_scale.getZ()]): - scale_widget.setRange(-1000, 1000) - scale_widget.setSingleStep(0.1) - scale_widget.setValue(scale_value) - - self.scale_x.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_x, value)) - self.scale_y.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_y, value)) - self.scale_z.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_z, value)) - - self.scale_x.valueChanged.connect(lambda value: self._updateXScale(model, value)) - self.scale_y.valueChanged.connect(lambda value: self._updateYScale(model, value)) - self.scale_z.valueChanged.connect(lambda value: self._updateZScale(model, value)) - - for axis_label, widget in (("X:", self.scale_x), ("Y:", self.scale_y), ("Z:", self.scale_z)): - axis = QLabel(axis_label) - axis.setAlignment(Qt.AlignVCenter) - axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - scale_row.addWidget(axis) - scale_row.addWidget(widget) - - transform_layout.addLayout(scale_row, 3, 1, 1, 3) - - self.transform_group.setLayout(transform_layout) - self._propertyLayout.addWidget(self.transform_group) - - # 碰撞检测面板 - self._addCollisionPanel(model) - - # 动画和太阳方位角面板 - self._addAnimationPanel(model) - self._addSunAzimuthPanel() - # - # 材质属性组 - self._updateModelMaterialPanel(model) - - def _onScaleValueChanged(self, scale_widget, value): - pass - - def _updateXScale(self, model, value): - """更新X轴缩放值""" - # 确保值不为0 - if value == 0: - sender = None - # 通过遍历找到发出信号的控件 - for widget in [self.scale_x, self.scale_y, self.scale_z]: - if widget.value() == value: - sender = widget - break - if sender: - self._onScaleValueChanged(sender, value) - return - - # 更新模型的X轴缩放 - current_scale = model.getScale() - model.setScale(value, current_scale.getY(), current_scale.getZ()) - self.refreshModelValues(model) - - def _updateYScale(self, model, value): - """更新Y轴缩放值""" - # 确保值不为0 - if value == 0: - sender = None - # 通过遍历找到发出信号的控件 - for widget in [self.scale_x, self.scale_y, self.scale_z]: - if widget.value() == value: - sender = widget - break - if sender: - self._onScaleValueChanged(sender, value) - return - - # 更新模型的Y轴缩放 - current_scale = model.getScale() - model.setScale(current_scale.getX(), value, current_scale.getZ()) - self.refreshModelValues(model) - - def _updateZScale(self, model, value): - """更新Z轴缩放值""" - # 确保值不为0 - if value == 0: - sender = None - # 通过遍历找到发出信号的控件 - for widget in [self.scale_x, self.scale_y, self.scale_z]: - if widget.value() == value: - sender = widget - break - if sender: - self._onScaleValueChanged(sender, value) - return - - # 更新模型的Z轴缩放 - current_scale = model.getScale() - model.setScale(current_scale.getX(), current_scale.getY(), value) - self.refreshModelValues(model) - - def updateGUIPropertyPanel(self, gui_element, item): - """更新GUI元素属性面板""" - self.clearPropertyPanel() - - itemText = gui_element.getTag("name") or "未命名GUI元素" - - user_visible = True - user_visible = gui_element.getPythonTag("user_visible") - if user_visible is None: - user_visible = True - gui_element.setPythonTag("user_visible", True) - - self.name_group = QGroupBox("物体名称") - name_layout = QGridLayout() - self.active_check = QCheckBox() - # 根据元素的实际可见性状态设置复选框 - self.active_check.setChecked(user_visible) - self.name_input = QLineEdit(itemText) - - # 注意:对于GUI元素,我们需要特殊处理名称更新 - def updateGUIName(text): - # 更新GUI元素的标签 - gui_element.setTag("name", text) - self.world.treeWidget.update_item_name(self.name_input.text(), item) - # gui_element.setName(text) - # 如果有场景管理器,也需要更新场景树 - # if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): - # self.world.scene_manager.updateSceneTree() - - self.name_input.returnPressed.connect(lambda: updateGUIName(self.name_input.text())) - - # 如果失去焦点也更新名称 - self.name_input.editingFinished.connect(lambda: updateGUIName(self.name_input.text())) - - name_layout.addWidget(self.active_check, 0, 0) - name_layout.setColumnMinimumWidth(0, self.column_minimum_width) - name_layout.addWidget(self.name_input, 0, 1, 1, 3) - self.name_group.setLayout(name_layout) - self._propertyLayout.addWidget(self.name_group) - - if gui_element: - try: - self.active_check.stateChanged.disconnect() - except TypeError: - pass - self.active_check.stateChanged.connect( - lambda state, elem=gui_element: self._setUserVisible(elem, state == Qt.Checked) - ) - - gui_type = gui_element.getTag("gui_type") - gui_text = gui_element.getTag("gui_text") - - # GUI基本信息组 - gui_info_group = QGroupBox("GUI信息") - gui_info_layout = QGridLayout() - gui_info_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - # GUI类型显示 - gui_info_layout.addWidget(QLabel("GUI类型:"), 0, 0) - typeValue = QLabel(gui_type) - # typeValue.setStyleSheet("color: #00AAFF; font-weight: bold;") - gui_info_layout.addWidget(typeValue, 0, 1, 1, 3) - - # 修改 updateGUIPropertyPanel 中的文本属性部分 - # 文本属性(如果适用) - if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]: - gui_info_layout.addWidget(QLabel("文本:"), 1, 0) - textEdit = QLineEdit(gui_text or "") - - # 使用编辑完成信号而不是文本变化信号 - def updateText(): - text = textEdit.text() - success = self.world.gui_manager.editGUIElement(gui_element, "text", text) - # if success: - # # 更新场景树显示的名称 - # if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): - # self.world.scene_manager.updateSceneTree() - - # 只在按下回车键或失去焦点时更新 - textEdit.returnPressed.connect(updateText) - textEdit.editingFinished.connect(updateText) - - gui_info_layout.addWidget(textEdit, 1, 1, 1, 3) - - gui_info_group.setLayout(gui_info_layout) - self._propertyLayout.addWidget(gui_info_group) - - # 变换属性组(合并位置和变换) - if hasattr(gui_element, 'getPos'): - # 根据GUI类型设置组名—— - if gui_type in ["button", "label", "entry", "2d_image", "2d_video_screen"]: - transform_group = QGroupBox("变换 Rect Transform") - else: - transform_group = QGroupBox("变换 Transform") - - transform_layout = QGridLayout() - transform_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - pos = gui_element.getPos() - - # 根据GUI类型决定位置编辑方式 - if gui_type in ["button", "label", "entry", "2d_image", "2d_video_screen"]: - # 2D GUI组件使用屏幕坐标 - logical_x = pos.getX() / 0.1 # 反向转换为逻辑坐标 - logical_z = pos.getZ() / 0.1 - - position_row = QHBoxLayout() - position_row.setContentsMargins(0, 0, 0, 0) - position_row.setSpacing(4) - - screen_title = QLabel("屏幕位置") - transform_layout.addWidget(screen_title, 0, 0) - - self.pos_x = QDoubleSpinBox() - self.pos_x.setRange(-1000, 1000) - self.pos_x.setValue(logical_x) - self.pos_z = QDoubleSpinBox() - self.pos_z.setRange(-1000, 1000) - self.pos_z.setValue(logical_z) - - axis_labels = [] - for col, (axis_label, widget) in enumerate((("X:", self.pos_x), ("Z:", self.pos_z)), start=1): - axis = QLabel(axis_label) - axis.setAlignment(Qt.AlignRight | Qt.AlignVCenter) - axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - axis_labels.append(axis) - position_row.addWidget(axis) - position_row.addWidget(widget) - - transform_layout.addLayout(position_row, 0, 1, 1, 3) - - # axis_width = max(label.sizeHint().width() for label in axis_labels) - # for label in axis_labels: - # label.setMinimumWidth(axis_width) - - # screen_position_row.setColumnStretch(2, 1) - # screen_position_row.setColumnStretch(4, 1) - - for i in range(4): - transform_layout.setColumnStretch(i, 1) - actual_position_row = QGridLayout() - - actual_row = QHBoxLayout() - actual_row.setContentsMargins(0, 0, 0, 0) - actual_row.setSpacing(4) - - actual_title = QLabel("实际坐标") - # actual_row.addWidget(actual_title) - transform_layout.addWidget(actual_title, 1, 0) - - self.actual_pos_x_label = QLabel(f"{pos.getX():.3f}") - self.actual_pos_x_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) - self.actual_pos_x_label.setStyleSheet("color: gray; font-size: 10px;") - self.actual_pos_z_label = QLabel(f"{pos.getZ():.3f}") - self.actual_pos_z_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) - self.actual_pos_z_label.setStyleSheet("color: gray; font-size: 10px;") - - for col, (axis_label, widget) in enumerate((("X:", self.actual_pos_x_label), - ("Z:", self.actual_pos_z_label)), start=1): - axis = QLabel(axis_label) - axis.setAlignment(Qt.AlignRight | Qt.AlignVCenter) - axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - # axis.setMinimumWidth(axis_width) - # actual_position_row.addWidget(axis, 0, col * 2 - 1, Qt.AlignLeft | Qt.AlignVCenter) - # actual_position_row.addWidget(widget, 0, col * 2, Qt.AlignLeft | Qt.AlignVCenter) - actual_row.addWidget(axis) - actual_row.addWidget(widget) - - # actual_position_row.setColumnStretch(2, 1) - # actual_position_row.setColumnStretch(4, 1) - - transform_layout.addLayout(actual_row, 1, 1, 1, 3) - - def _update_actual_position_labels(): - current_pos = gui_element.getPos() - self.actual_pos_x_label.setText(f"{current_pos.getX():.3f}") - self.actual_pos_z_label.setText(f"{current_pos.getZ():.3f}") - - _update_actual_position_labels() - - def _on_gui_pos_x_changed(value): - self.world.gui_manager.editGUI2DPosition(gui_element, "x", value) - _update_actual_position_labels() - - def _on_gui_pos_z_changed(value): - self.world.gui_manager.editGUI2DPosition(gui_element, "z", value) - _update_actual_position_labels() - - self.pos_x.valueChanged.connect(_on_gui_pos_x_changed) - self.pos_z.valueChanged.connect(_on_gui_pos_z_changed) - - if gui_type in ["2d_image", "2d_video_screen"]: - scale = gui_element.getScale() - width = scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, - (tuple, list)) else scale - height = scale.getZ() if hasattr(scale, 'getZ') else scale[1] if isinstance(scale, - (tuple, list)) and len( - scale) > 1 else scale - - scale_row = QHBoxLayout() - scale_row.setContentsMargins(0, 0, 0, 0) - scale_row.setSpacing(4) - transform_layout.addWidget(QLabel("缩放"), 2, 0) - - self.scale_x = QDoubleSpinBox() - self.scale_x.setRange(0.1, 100) - self.scale_x.setSingleStep(0.01) - self.scale_x.setValue(width) - self.scale_x.valueChanged.connect(lambda v: self.editGUIScale(gui_element, "x", v)) - - self.scale_z = QDoubleSpinBox() - self.scale_z.setRange(0.1, 10) - self.scale_z.setSingleStep(0.01) - self.scale_z.setValue(height) - self.scale_z.valueChanged.connect(lambda v: self.editGUIScale(gui_element, "z", v)) - - for axis_label, widget in (("X:", self.scale_x), ("Z:", self.scale_z)): - axis = QLabel(axis_label) - axis.setAlignment(Qt.AlignVCenter) - axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - scale_row.addWidget(axis) - scale_row.addWidget(widget) - - transform_layout.addLayout(scale_row, 2, 1, 1, 3) - - else: - scale_row = QHBoxLayout() - scale_row.setContentsMargins(0, 0, 0, 0) - scale_row.setSpacing(4) - transform_layout.addWidget(QLabel("缩放"), 2, 0) - - scaleSpinBox = QDoubleSpinBox() - scaleSpinBox.setRange(0.01, 100) - scaleSpinBox.setSingleStep(0.01) - scaleSpinBox.setValue(gui_element.getScale().getX() * 2) - scaleSpinBox.valueChanged.connect(lambda v: self._update2DImageScale(gui_element, v)) - - axis = QLabel("X:") - axis.setAlignment(Qt.AlignVCenter) - axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - scale_row.addWidget(axis) - scale_row.addWidget(scaleSpinBox) - transform_layout.addLayout(scale_row, 2, 1, 1, 3) - - else: - # 3D GUI组件使用世界坐标 - position_row = QHBoxLayout() - position_row.setContentsMargins(0, 0, 0, 0) - position_row.setSpacing(4) - # position_row.addWidget(QLabel("位置")) - position_label = QLabel("位置") - transform_layout.addWidget(position_label, 0, 0) - - self.pos_x = QDoubleSpinBox() - self.pos_x.setRange(-100, 100) - self.pos_x.setValue(pos.getX()) - self.pos_x.setSingleStep(0.01) - self.pos_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "x", v)) - - self.pos_y = QDoubleSpinBox() - self.pos_y.setRange(-100, 100) - self.pos_y.setValue(pos.getY()) - self.pos_y.setSingleStep(0.01) - self.pos_y.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "y", v)) - - self.pos_z = QDoubleSpinBox() - self.pos_z.setRange(-100, 100) - self.pos_z.setValue(pos.getZ()) - self.pos_z.setSingleStep(0.01) - self.pos_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "z", v)) - - for axis_label, widget in (("X:", self.pos_x), ("Y:", self.pos_y), ("Z:", self.pos_z)): - axis = QLabel(axis_label) - axis.setAlignment(Qt.AlignVCenter) - axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - position_row.addWidget(axis) - position_row.addWidget(widget) - - transform_layout.addLayout(position_row, 0, 1, 1, 3) - - scale = gui_element.getScale() - scale_row = QHBoxLayout() - scale_row.setContentsMargins(0, 0, 0, 0) - scale_row.setSpacing(4) - # scale_row.addWidget(QLabel("缩放")) - scale_label = QLabel("缩放") - transform_layout.addWidget(scale_label, 1, 0) - - self.scale_x = QDoubleSpinBox() - self.scale_x.setRange(0.01, 100) - self.scale_x.setSingleStep(0.01) - self.scale_x.setValue( - scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, (tuple, list)) else scale) - self.scale_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "x", v)) - - self.scale_y = QDoubleSpinBox() - self.scale_y.setRange(0.01, 100) - self.scale_y.setSingleStep(0.01) - self.scale_y.setValue( - scale.getY() if hasattr(scale, 'getY') else scale[1] if isinstance(scale, (tuple, list)) and len( - scale) > 1 else scale) - self.scale_y.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "y", v)) - - self.scale_z = QDoubleSpinBox() - self.scale_z.setRange(0.01, 100) - self.scale_z.setSingleStep(0.01) - self.scale_z.setValue( - scale.getZ() if hasattr(scale, 'getZ') else scale[2] if isinstance(scale, (tuple, list)) and len( - scale) > 2 else scale) - self.scale_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v)) - - for axis_label, widget in (("X:", self.scale_x), ("Y:", self.scale_y), ("Z:", self.scale_z)): - axis = QLabel(axis_label) - axis.setAlignment(Qt.AlignVCenter) - axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - scale_row.addWidget(axis) - scale_row.addWidget(widget) - - transform_layout.addLayout(scale_row, 1, 1, 1, 3) - - transform_group.setLayout(transform_layout) - self._propertyLayout.addWidget(transform_group) - - # 为2D图像和视频屏幕添加Sort属性 - if gui_type in ["2d_image", "2d_video_screen", "info_panel"]: - sort_group = QGroupBox("显示顺序") - sort_layout = QGridLayout() - sort_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - # 获取当前sort值,如果没有设置则默认为0 - current_sort = int(gui_element.getTag("sort") or "0") - - sort_layout.addWidget(QLabel("显示优先度:"), 0, 0) - sort_spin = QSpinBox() - sort_spin.setRange(-1000, 1000) - sort_spin.setValue(current_sort) - - def updateSort(value): - try: - # 设置标签 - gui_element.setTag("sort", str(value)) - # 应用sort到节点 - gui_element.setBin("fixed", value) - print(f"✓ 更新{gui_type}渲染顺序: {value}") - except Exception as e: - print(f"✗ 更新{gui_type}渲染顺序失败: {e}") - - sort_spin.valueChanged.connect(updateSort) - sort_layout.addWidget(sort_spin, 0, 1, 1, 3) - - sort_group.setLayout(sort_layout) - self._propertyLayout.addWidget(sort_group) - - # 外观属性组 - 添加字体颜色选择 - if gui_type in ["button", "label", "3d_text"]: - appearance_group = QGroupBox("外观属性") - appearance_layout = QGridLayout() - appearance_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - # 字体颜色选择 - appearance_layout.addWidget(QLabel("字体颜色:"), 0, 0) - colorButton = QPushButton("选择颜色") - colorButton.clicked.connect(lambda checked, elem=gui_element: self._selectGUIColor(elem)) - appearance_layout.addWidget(colorButton, 0, 1, 1, 3) - - appearance_group.setLayout(appearance_layout) - self._propertyLayout.addWidget(appearance_group) - - # 外观属性组 - if gui_type in ["button", "label"]: - appearance_group = QGroupBox("外观属性") - appearance_layout = QGridLayout() - appearance_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - appearance_layout.addWidget(QLabel("背景颜色:"), 0, 0) - - colorButton = QPushButton("选择颜色") - colorButton.clicked.connect(lambda: self.world.gui_manager.selectGUIColor(gui_element)) - appearance_layout.addWidget(colorButton, 0, 1, 1, 3) - - appearance_group.setLayout(appearance_layout) - self._propertyLayout.addWidget(appearance_group) - - if gui_type == "3d_image": - image_group = QGroupBox("图片设置") - image_layout = QGridLayout() - image_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - # 当前图片路径标签和 - current_image_label = QLabel("当前图片:") - image_layout.addWidget(current_image_label, 0, 0) - - # 显示当前贴图路径(简化显示) - current_texture_path = gui_element.getTag("texture_path") or "未设置" - if current_texture_path != "未设置": - display_path = current_texture_path if len(current_texture_path) <= 10 else current_texture_path[ - :7] + "..." - else: - display_path = current_texture_path - texture_label = QLabel(display_path) - texture_label.setWordWrap(True) - texture_label.setToolTip(current_texture_path if current_texture_path != "未设置" else "") - image_layout.addWidget(texture_label, 0, 1, 1, 3) - - # 选择图片按钮 - if has_icon("property_select_image"): - select_texture_button = QPushButton(get_icon("property_select_image"), "选择图片...") - else: - select_texture_button = QPushButton("选择图片...") - image_layout.addWidget(select_texture_button, 1, 0, 1, 4) - - def onSelectTexture(): - from PyQt5.QtWidgets import QFileDialog - file_path, _ = QFileDialog.getOpenFileName( - None, - "选择图片", - "", - "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga *.dds)" - ) - if file_path: - # 应用新纹理到 3D Image - file_path = util.normalize_model_path(file_path) - success = self.world.gui_manager.update3DImageTexture(gui_element, file_path) - if success: - # 保存路径到 Tag - gui_element.setTag("texture_path", file_path) - gui_element.setTag("gui_image_path", file_path) - # 更新显示 - texture_label.setText(file_path) - # 可选:刷新场景树或其他 UI - # self.world.scene_manager.updateSceneTree() - - select_texture_button.clicked.connect(onSelectTexture) - - image_group.setLayout(image_layout) - self._propertyLayout.addWidget(image_group) - - if gui_type in ["2d_image"]: - image_group = QGroupBox("2D图片设置") - image_layout = QGridLayout() - image_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - # 当前图片路径标签 - current_image_label = QLabel("当前图片:") - image_layout.addWidget(current_image_label, 0, 0) - - # 显示当前贴图路径(简化显示) - current_texture_path = gui_element.getTag("texture_path") or gui_element.getTag("image_path") or "未设置" - if current_texture_path != "未设置": - display_path = current_texture_path if len(current_texture_path) <= 10 else current_texture_path[ - :7] + "..." - else: - display_path = current_texture_path - texture_label = QLabel(display_path) - texture_label.setWordWrap(True) - image_layout.addWidget(texture_label, 0, 1, 1, 3) - - # 选择图片按钮 - if has_icon("property_select_image"): - select_texture_button = QPushButton(get_icon("property_select_image"), "选择图片...") - else: - select_texture_button = QPushButton("选择图片...") - # select_texture_button = QPushButton("选择图片...") - image_layout.addWidget(select_texture_button, 1, 0, 1, 4) - - def onSelect2DTexture(): - from PyQt5.QtWidgets import QFileDialog - file_path, _ = QFileDialog.getOpenFileName( - None, - "选择图片", - "", - "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga *.dds)" - ) - if file_path: - # 应用新纹理到 2D Image - file_path = util.normalize_model_path(file_path) - success = self.update2DImageTexture(gui_element, file_path) - if success: - # 保存路径到 Tag - gui_element.setTag("texture_path", file_path) - gui_element.setTag("image_path", file_path) - # 更新显示 - texture_label.setText(file_path) - # 可选:刷新场景树或其他 UI - # self.world.scene_manager.updateSceneTree() - - select_texture_button.clicked.connect(onSelect2DTexture) - - image_group.setLayout(image_layout) - self._propertyLayout.addWidget(image_group) - - # 添加弹性空间 - - if gui_type == "video_screen": - self._addVideoScreenProperties(gui_element, item) - elif gui_type == "2d_video_screen": - self._add2DVideoScreenProperties(gui_element, item) - elif gui_type == "spherical_video": - self._addSphericalVideoProperties(gui_element) - elif gui_type in ['info_panel', 'info_panel_3d']: - self._addInfoPanelProperties(gui_element) - - self._propertyLayout.addStretch() - - # 强制更新布局 - if self._propertyLayout: - self._propertyLayout.update() - propertyWidget = self._propertyLayout.parentWidget() - if propertyWidget: - propertyWidget.update() - - def _addInfoPanelProperties(self, info_panel): - """为信息面板添加属性控制面板 - 同时适配2D和3D信息面板""" - try: - from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, - QDoubleSpinBox, QColorDialog, QSpinBox, QTextEdit) - from PyQt5.QtCore import Qt, QTimer - from PyQt5.QtGui import QColor - import os - - panel_id = info_panel.getTag("panel_id") - if not panel_id: - print("无法找到信息面板ID") - return - - # 检测面板类型 (2D或3D) - gui_type = info_panel.getTag("gui_type") - is_3d_panel = gui_type == "info_panel_3d" - - # 获取面板当前属性 - current_size = (1.0, 0.6) - current_position = (0, 0) if not is_3d_panel else (0, 0, 0) - current_bg_color = (0.15, 0.15, 0.15, 0.9) - current_border_color = (0.3, 0.3, 0.3, 1.0) - current_title_color = (1.0, 1.0, 0.0, 1.0) - current_content_color = (0.9, 0.9, 0.9, 1.0) - current_title_size = 0.06 # 默认值 - current_content_size = 0.045 # 默认值 - current_title_text = "信息面板" - current_content_text = "这是一个信息面板" - current_bg_image = "" - current_sort = 0 - - # 标题和内容位置默认值 - current_title_pos = (0, 0) - current_content_pos = (-0.45, 0) - - # 从info_panel_manager获取当前属性 - if hasattr(self.world, 'info_panel_manager'): - panel_data = self.world.info_panel_manager.panels.get(panel_id) - if panel_data: - # 获取基本属性 - if 'properties' in panel_data: - props = panel_data['properties'] - current_size = props.get('size', current_size) - current_position = props.get('position', current_position) - current_bg_color = props.get('bg_color', current_bg_color) - current_border_color = props.get('border_color', current_border_color) - current_title_color = props.get('title_color', current_title_color) - current_content_color = props.get('content_color', current_content_color) - current_bg_image = props.get('bg_image', current_bg_image) - - # 获取文本内容和位置(根据面板类型区分处理) - if not is_3d_panel: # 2D面板 - if 'title_label' in panel_data and panel_data['title_label']: - current_title_text = panel_data['title_label']['text'] - # 获取标题位置 - title_pos = panel_data['title_label'].getPos() - current_title_pos = (title_pos.getX(), title_pos.getZ()) - - if 'content_label' in panel_data and panel_data['content_label']: - current_content_text = panel_data['content_label']['text'] - # 获取内容位置 - content_pos = panel_data['content_label'].getPos() - current_content_pos = (content_pos.getX(), content_pos.getZ()) - - # 获取字体大小 - if 'title_label' in panel_data and panel_data['title_label']: - try: - title_scale = panel_data['title_label']['text_scale'] - # 如果是元组,取第一个值 - if isinstance(title_scale, (tuple, list)): - current_title_size = title_scale[0] if len(title_scale) > 0 else 0.06 - else: - current_title_size = title_scale if isinstance(title_scale, (int, float)) else 0.06 - except: - current_title_size = 0.06 - - if 'content_label' in panel_data and panel_data['content_label']: - try: - content_scale = panel_data['content_label']['text_scale'] - # 如果是元组,取第一个值 - if isinstance(content_scale, (tuple, list)): - current_content_size = content_scale[0] if len(content_scale) > 0 else 0.045 - else: - current_content_size = content_scale if isinstance(content_scale, - (int, float)) else 0.045 - except: - current_content_size = 0.045 - else: # 3D面板 - if 'title_node' in panel_data and panel_data['title_node']: - current_title_text = panel_data['title_node'].getText() - - if 'content_node' in panel_data and panel_data['content_node']: - current_content_text = panel_data['content_node'].getText() - - if 'title_text' in panel_data and panel_data['title_text']: - current_title_size = panel_data['title_text'].getScale().getX() - - if 'content_text' in panel_data and panel_data['content_text']: - current_content_size = panel_data['content_text'].getScale().getX() - - # 获取sort值 - if info_panel.hasTag("sort"): - try: - current_sort = int(info_panel.getTag("sort")) - except: - current_sort = 0 - - # 背景图片组 - image_group = QGroupBox("背景图片设置") - image_layout = QGridLayout() - image_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - # 当前背景图片路径标签 - current_image_label = QLabel("当前背景图片:") - image_layout.addWidget(current_image_label, 0, 0) - - # 显示当前背景图片路径(简化显示) - if current_bg_image: - display_path = current_bg_image if len(current_bg_image) <= 12 else current_bg_image[:9] + "..." - else: - display_path = "未设置" - texture_label = QLabel(display_path) - texture_label.setWordWrap(True) - texture_label.setToolTip(current_bg_image if current_bg_image else "") - image_layout.addWidget(texture_label, 0, 1, 1, 3) - - # 选择背景图片按钮 - select_texture_button = QPushButton("选择背景图片...") - image_layout.addWidget(select_texture_button, 1, 0, 1, 4) - - # 清除背景图片按钮 - clear_texture_button = QPushButton("清除背景图片") - clear_texture_button.setStyleSheet("color: red;") - image_layout.addWidget(clear_texture_button, 2, 0, 1, 4) - - def onSelectBackgroundImage(): - """选择背景图片""" - from PyQt5.QtWidgets import QFileDialog - file_path, _ = QFileDialog.getOpenFileName( - None, - "选择背景图片", - "", - "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga *.dds)" - ) - if file_path: - # 应用新背景图片到信息面板 - file_path = util.normalize_model_path(file_path) - success = self.updateInfoPanelBackgroundImage(info_panel, file_path) - if success: - # 保存路径到 Tag - info_panel.setTag("bg_image_path", file_path) - # 更新显示 - display_path = file_path if len(file_path) <= 12 else file_path[:9] + "..." - texture_label.setText(display_path) - texture_label.setToolTip(file_path) - - def onClearBackgroundImage(): - """清除背景图片""" - success = self.updateInfoPanelBackgroundImage(info_panel, None) - if success: - # 清除路径标签 - info_panel.clearTag("bg_image_path") - texture_label.setText("未设置") - texture_label.setToolTip("") - - select_texture_button.clicked.connect(onSelectBackgroundImage) - clear_texture_button.clicked.connect(onClearBackgroundImage) - - image_group.setLayout(image_layout) - self._propertyLayout.addWidget(image_group) - - # 显示顺序组 - sort_group = QGroupBox("显示顺序") - sort_layout = QGridLayout() - sort_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - sort_layout.addWidget(QLabel("显示优先度:"), 0, 0) - sort_spin = QSpinBox() - sort_spin.setRange(-1000, 1000) - sort_spin.setValue(current_sort) - - def updateSort(value): - try: - # 设置标签 - info_panel.setTag("sort", str(value)) - # 应用sort到节点 - info_panel.setBin("fixed", value) - print(f"✓ 更新信息面板渲染顺序: {value}") - except Exception as e: - print(f"✗ 更新信息面板渲染顺序失败: {e}") - - sort_spin.valueChanged.connect(updateSort) - sort_layout.addWidget(sort_spin, 0, 1, 1, 3) - - sort_group.setLayout(sort_layout) - self._propertyLayout.addWidget(sort_group) - - # 面板大小设置组 - size_group = QGroupBox() - size_group.setTitle("面板大小设置") - size_layout = QGridLayout() - size_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - size_row = QHBoxLayout() - size_row.setContentsMargins(0, 0, 0, 0) - size_row.setSpacing(4) - - size_title = QLabel("面板大小设置:") - size_layout.addWidget(size_title, 0, 0) - - width_spin = QDoubleSpinBox() - width_spin.setRange(0.1, 5.0) - width_spin.setSingleStep(0.1) - width_spin.setValue(current_size[0]) - - height_spin = QDoubleSpinBox() - height_spin.setRange(0.1, 5.0) - height_spin.setSingleStep(0.1) - height_spin.setValue(current_size[1]) - - for label_text, spinbox in (("宽度:", width_spin), ("高度:", height_spin)): - label = QLabel(label_text) - label.setAlignment(Qt.AlignVCenter) - label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - size_row.addWidget(label) - size_row.addWidget(spinbox) - - size_layout.addLayout(size_row, 0, 1, 1, 3) - - # 连接大小变化信号 - def onSizeChanged(): - if hasattr(self.world, 'info_panel_manager'): - panel_width = width_spin.value() - panel_height = height_spin.value() - - # 根据面板类型选择更新方法 - if is_3d_panel: - self.world.info_panel_manager.update3DPanelProperties( - panel_id, size=(panel_width, panel_height)) - else: - self.world.info_panel_manager.updatePanelProperties( - panel_id, size=(panel_width, panel_height)) - - # 同步更新内容的换行设置 - self._adjustContentWordwrap(panel_id, panel_width, content_size_spin.value()) - - width_spin.valueChanged.connect(onSizeChanged) - height_spin.valueChanged.connect(onSizeChanged) - - size_group.setLayout(size_layout) - self._propertyLayout.addWidget(size_group) - - # 位置设置:X:[box] Y:[box]/Z:[box],右对齐可伸缩 - position_group = QGroupBox() - position_group.setTitle("位置设置") - position_layout = QGridLayout() - position_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - position_row = QHBoxLayout() - position_row.setContentsMargins(0, 0, 0, 0) - position_row.setSpacing(4) - - position_title = QLabel("位置设置:") - position_layout.addWidget(position_title, 0, 0) - - if is_3d_panel: - # 3D 面板位置:X、Y、Z - pos_x_spin = QDoubleSpinBox() - pos_x_spin.setRange(-1000, 1000) - pos_x_spin.setSingleStep(0.1) - pos_x_spin.setValue(current_position[0] if len(current_position) > 0 else 0) - - pos_y_spin = QDoubleSpinBox() - pos_y_spin.setRange(-1000, 1000) - pos_y_spin.setSingleStep(0.1) - pos_y_spin.setValue(current_position[1] if len(current_position) > 1 else 0) - - pos_z_spin = QDoubleSpinBox() - pos_z_spin.setRange(-1000, 1000) - pos_z_spin.setSingleStep(0.1) - pos_z_spin.setValue(current_position[2] if len(current_position) > 2 else 0) - - axis_widgets = (("X:", pos_x_spin), ("Y:", pos_y_spin), ("Z:", pos_z_spin)) - - def on3DPositionChanged(): - if hasattr(self.world, "info_panel_manager"): - self.world.info_panel_manager.update3DPanelProperties( - panel_id, position=(pos_x_spin.value(), pos_y_spin.value(), pos_z_spin.value())) - - pos_x_spin.valueChanged.connect(on3DPositionChanged) - pos_y_spin.valueChanged.connect(on3DPositionChanged) - pos_z_spin.valueChanged.connect(on3DPositionChanged) - else: - # 2D 面板位置:X、Z - pos_x_spin = QDoubleSpinBox() - pos_x_spin.setRange(-1000, 1000) - pos_x_spin.setSingleStep(0.1) - pos_x_spin.setValue(current_position[0] if len(current_position) > 0 else 0) - - pos_z_spin = QDoubleSpinBox() - pos_z_spin.setRange(-1000, 1000) - pos_z_spin.setSingleStep(0.1) - pos_z_spin.setValue(current_position[1] if len(current_position) > 1 else 0) - - axis_widgets = (("X:", pos_x_spin), ("Z:", pos_z_spin)) - - def on2DPositionChanged(): - if hasattr(self.world, "info_panel_manager"): - self.world.info_panel_manager.updatePanelProperties( - panel_id, position=(pos_x_spin.value(), pos_z_spin.value())) - - pos_x_spin.valueChanged.connect(on2DPositionChanged) - pos_z_spin.valueChanged.connect(on2DPositionChanged) - - for axis_label, spinbox in axis_widgets: - axis = QLabel(axis_label) - axis.setAlignment(Qt.AlignVCenter) - axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - position_row.addWidget(axis) - position_row.addWidget(spinbox) - - position_layout.addLayout(position_row, 0, 1, 1, 3) - position_group.setLayout(position_layout) - self._propertyLayout.addWidget(position_group) - - # 边框属性组 - border_group = QGroupBox("边框属性") - border_layout = QGridLayout() - border_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - border_layout.addWidget(QLabel("边框颜色:"), 0, 0) - # border_layout.addWidget(border_color_label, 0, 0) - - border_row_top = QHBoxLayout() - border_row_top.setContentsMargins(0, 0, 0, 0) - border_row_top.setSpacing(4) - - border_r_spin = QDoubleSpinBox() - border_r_spin.setRange(0.0, 1.0) - border_r_spin.setSingleStep(0.01) - border_r_spin.setValue(current_border_color[0]) - color_r_bg = QLabel("R:") - color_r_bg.setAlignment(Qt.AlignVCenter) - color_r_bg.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - border_row_top.addWidget(color_r_bg) - border_row_top.addWidget(border_r_spin) - - border_g_spin = QDoubleSpinBox() - border_g_spin.setRange(0.0, 1.0) - border_g_spin.setSingleStep(0.01) - border_g_spin.setValue(current_border_color[1]) - color_g = QLabel("G:") - color_g.setAlignment(Qt.AlignVCenter) - color_g.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - border_row_top.addWidget(color_g) - border_row_top.addWidget(border_g_spin) - - border_layout.addLayout(border_row_top, 0, 1, 1, 3) - - indent_placeholder = QLabel("") - border_layout.addWidget(indent_placeholder, 1, 0) - # indent_placeholder.setFixedWidth(border_color_label.sizeHint().width()) - # border_layout.addWidget(indent_placeholder, 1, 0) - - border_row_bottom = QHBoxLayout() - border_row_bottom.setContentsMargins(0, 0, 0, 0) - border_row_bottom.setSpacing(4) - - border_b_spin = QDoubleSpinBox() - border_b_spin.setRange(0.0, 1.0) - border_b_spin.setSingleStep(0.01) - border_b_spin.setValue(current_border_color[2]) - color_b = QLabel("B:") - color_b.setAlignment(Qt.AlignVCenter) - color_b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - border_row_bottom.addWidget(color_b) - border_row_bottom.addWidget(border_b_spin) - - border_a_spin = QDoubleSpinBox() - border_a_spin.setRange(0.0, 1.0) - border_a_spin.setSingleStep(0.01) - border_a_spin.setValue(current_border_color[3]) - color_a = QLabel("A:") - color_a.setAlignment(Qt.AlignVCenter) - color_a.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - border_row_bottom.addWidget(color_a) - border_row_bottom.addWidget(border_a_spin) - - border_layout.addLayout(border_row_bottom, 1, 1, 1, 3) - - border_color_button = QPushButton("选择边框颜色") - border_color_button.clicked.connect(lambda: self._selectInfoPanelBorderColor( - info_panel, border_r_spin, border_g_spin, border_b_spin, border_a_spin)) - border_layout.addWidget(border_color_button, 2, 0, 1, 4) - - # 连接边框颜色变化信号 - def onBorderColorChanged(): - if hasattr(self.world, 'info_panel_manager'): - border_color = ( - border_r_spin.value(), - border_g_spin.value(), - border_b_spin.value(), - border_a_spin.value() - ) - # 根据面板类型选择更新方法 - if is_3d_panel: - self.world.info_panel_manager.update3DPanelProperties( - panel_id, border_color=border_color) - else: - self.world.info_panel_manager.updatePanelProperties( - panel_id, border_color=border_color) - - border_r_spin.valueChanged.connect(onBorderColorChanged) - border_g_spin.valueChanged.connect(onBorderColorChanged) - border_b_spin.valueChanged.connect(onBorderColorChanged) - border_a_spin.valueChanged.connect(onBorderColorChanged) - - border_group.setLayout(border_layout) - self._propertyLayout.addWidget(border_group) - - bg_color_group = QGroupBox("背景颜色设置") - bg_color_layout = QGridLayout() - bg_color_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - bg_color_label = QLabel("背景颜色:") - bg_color_layout.addWidget(bg_color_label, 0, 0) - bg_color_row_top = QHBoxLayout() - bg_color_row_top.setContentsMargins(0, 0, 0, 0) - bg_color_row_top.setSpacing(4) - - # R分量 - color_r_bg = QLabel("R:") - color_r_bg.setAlignment(Qt.AlignVCenter) - color_r_bg.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - r_spin = QDoubleSpinBox() - r_spin.setRange(0.0, 1.0) - r_spin.setSingleStep(0.01) - r_spin.setValue(current_bg_color[0]) - bg_color_row_top.addWidget(color_r_bg) - bg_color_row_top.addWidget(r_spin) - - # G分量 - color_g_bg = QLabel("G:") - color_g_bg.setAlignment(Qt.AlignVCenter) - color_g_bg.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - g_spin = QDoubleSpinBox() - g_spin.setRange(0.0, 1.0) - g_spin.setSingleStep(0.01) - g_spin.setValue(current_bg_color[1]) - bg_color_row_top.addWidget(color_g_bg) - bg_color_row_top.addWidget(g_spin) - - bg_color_layout.addLayout(bg_color_row_top, 0, 1, 1, 3) - - indent_placeholder = QLabel("") - bg_color_layout.addWidget(indent_placeholder, 1, 0) - - bg_color_row_bottom = QHBoxLayout() - bg_color_row_bottom.setContentsMargins(0, 0, 0, 0) - bg_color_row_bottom.setSpacing(4) - - # B分量 - color_b_bg = QLabel("B:") - color_b_bg.setAlignment(Qt.AlignVCenter) - color_b_bg.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - b_spin = QDoubleSpinBox() - b_spin.setRange(0.0, 1.0) - b_spin.setSingleStep(0.01) - b_spin.setValue(current_bg_color[2]) - bg_color_row_bottom.addWidget(color_b_bg) - bg_color_row_bottom.addWidget(b_spin) - - # A分量 - color_a_bg = QLabel("A:") - color_a_bg.setAlignment(Qt.AlignVCenter) - color_a_bg.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - a_spin = QDoubleSpinBox() - a_spin.setRange(0.0, 1.0) - a_spin.setSingleStep(0.01) - a_spin.setValue(current_bg_color[3]) - bg_color_row_bottom.addWidget(color_a_bg) - bg_color_row_bottom.addWidget(a_spin) - - bg_color_layout.addLayout(bg_color_row_bottom, 1, 1, 1, 3) - - # 颜色选择按钮 - color_button = QPushButton("选择颜色") - color_button.clicked.connect(lambda: self._selectInfoPanelBackgroundColor( - info_panel, r_spin, g_spin, b_spin, a_spin)) - bg_color_layout.addWidget(color_button, 2, 0, 1, 4) - - # 连接颜色数值变化信号,自动更新背景颜色 - def onBackgroundColorChanged(): - self._applyInfoPanelBackgroundColor( - info_panel, r_spin.value(), g_spin.value(), b_spin.value(), a_spin.value()) - - r_spin.valueChanged.connect(onBackgroundColorChanged) - g_spin.valueChanged.connect(onBackgroundColorChanged) - b_spin.valueChanged.connect(onBackgroundColorChanged) - a_spin.valueChanged.connect(onBackgroundColorChanged) - - bg_color_group.setLayout(bg_color_layout) - self._propertyLayout.addWidget(bg_color_group) - - # 标题属性组 - title_group = QGroupBox("标题属性") - title_layout = QGridLayout() - title_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - # 标题文本 - title_layout.addWidget(QLabel("标题文本:"), 0, 0) - title_text_edit = QTextEdit() - title_text_edit.setMaximumHeight(60) - title_text_edit.setPlainText(current_title_text) - title_layout.addWidget(title_text_edit, 0, 1, 1, 3) - - # 标题颜色 - title_layout.addWidget(QLabel("标题颜色:"), 1, 0) - - title_color_row = QHBoxLayout() # 占位,保持对齐 - title_color_row.setContentsMargins(0, 0, 0, 0) - title_color_row.setSpacing(4) - - # 标题R分量 - title_color_label_r = QLabel("R:") - title_color_label_r.setAlignment(Qt.AlignVCenter) - title_color_label_r.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - title_color_row.addWidget(title_color_label_r) - title_r_spin = QDoubleSpinBox() - title_r_spin.setRange(0.0, 1.0) - title_r_spin.setSingleStep(0.01) - title_r_spin.setValue(current_title_color[0]) - title_color_row.addWidget(title_r_spin) - - # 标题G分量 - title_color_label_g = QLabel("G:") - title_color_label_g.setAlignment(Qt.AlignVCenter) - title_color_label_g.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - title_color_row.addWidget(title_color_label_g) - title_g_spin = QDoubleSpinBox() - title_g_spin.setRange(0.0, 1.0) - title_g_spin.setSingleStep(0.01) - title_g_spin.setValue(current_title_color[1]) - title_color_row.addWidget(title_g_spin) - - title_layout.addLayout(title_color_row, 1, 1, 1, 3) - - title_layout.addWidget(QLabel(""), 2, 0) # 占位 - title_color_row_ = QHBoxLayout() # 占位,保持对齐 - title_color_row_.setContentsMargins(0, 0, 0, 0) - title_color_row_.setSpacing(4) - - # 标题B分量 - title_color_label_b = QLabel("B:") - title_color_label_b.setAlignment(Qt.AlignVCenter) - title_color_label_b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - title_color_row_.addWidget(title_color_label_b) - title_b_spin = QDoubleSpinBox() - title_b_spin.setRange(0.0, 1.0) - title_b_spin.setSingleStep(0.01) - title_b_spin.setValue(current_title_color[2]) - title_color_row_.addWidget(title_b_spin) - - # 标题A分量 - title_color_label_a = QLabel("A:") - title_color_label_a.setAlignment(Qt.AlignVCenter) - title_color_label_a.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - title_color_row_.addWidget(title_color_label_a) - title_a_spin = QDoubleSpinBox() - title_a_spin.setRange(0.0, 1.0) - title_a_spin.setSingleStep(0.01) - title_a_spin.setValue(current_title_color[3]) - title_color_row_.addWidget(title_a_spin) - - title_layout.addLayout(title_color_row_, 2, 1, 1, 3) - - # 标题颜色选择按钮 - title_color_button = QPushButton("选择标题颜色") - title_color_button.clicked.connect(lambda: self._selectInfoPanelTitleColor( - info_panel, title_r_spin, title_g_spin, title_b_spin, title_a_spin)) - title_layout.addWidget(title_color_button, 4, 0, 1, 4) - - # 标题字体大小 - title_layout.addWidget(QLabel("标题字体大小:"), 5, 0) - title_size_spin = QDoubleSpinBox() - title_size_spin.setRange(0.01, 1.0) - title_size_spin.setSingleStep(0.01) - title_size_spin.setValue(current_title_size) - title_layout.addWidget(title_size_spin, 5, 1, 1, 3) - - # 标题位置控制 (仅对2D面板) - if not is_3d_panel: - title_position_row = QHBoxLayout() - title_position_row.setContentsMargins(0, 0, 0, 0) - title_position_row.setSpacing(4) - - title_position_label = QLabel("标题位置") - title_layout.addWidget(title_position_label, 6, 0) - - title_x_spin = QDoubleSpinBox() - title_x_spin.setRange(-1.0, 1.0) - title_x_spin.setSingleStep(0.01) - title_x_spin.setValue(current_title_pos[0]) - - title_y_spin = QDoubleSpinBox() - title_y_spin.setRange(-1.0, 1.0) - title_y_spin.setSingleStep(0.01) - title_y_spin.setValue(current_title_pos[1]) - - for axis_label, spinbox in (("X:", title_x_spin), ("Y:", title_y_spin)): - label = QLabel(axis_label) - label.setAlignment(Qt.AlignVCenter) - label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - title_position_row.addWidget(label) - title_position_row.addWidget(spinbox) - - title_layout.addLayout(title_position_row, 6, 1, 1, 3) - - # 连接标题属性变化信号 - def onTitleTextChanged(): - if hasattr(self.world, 'info_panel_manager'): - # 根据面板类型选择更新方法 - if is_3d_panel: - self.world.info_panel_manager.update3DPanelContent( - panel_id, title=title_text_edit.toPlainText()) - else: - self.world.info_panel_manager.updatePanelContent( - panel_id, title=title_text_edit.toPlainText()) - - def onTitlePropertyChanged(): - # 根据面板类型选择更新方法 - if is_3d_panel: - self._applyInfoPanel3DTitleProperties( - info_panel, title_r_spin.value(), title_g_spin.value(), title_b_spin.value(), - title_a_spin.value(), title_size_spin.value()) - else: - self._applyInfoPanelTitleProperties( - info_panel, title_r_spin.value(), title_g_spin.value(), title_b_spin.value(), - title_a_spin.value(), title_size_spin.value()) - - def onTitlePositionChanged(): - # 仅对2D面板更新标题位置 - if not is_3d_panel and hasattr(self.world, 'info_panel_manager'): - panel_data = self.world.info_panel_manager.panels.get(panel_id) - if panel_data and 'title_label' in panel_data: - # 设置标题位置 - panel_data['title_label'].setPos(title_x_spin.value(), 0, title_y_spin.value()) - - title_text_edit.textChanged.connect(onTitleTextChanged) - title_r_spin.valueChanged.connect(onTitlePropertyChanged) - title_g_spin.valueChanged.connect(onTitlePropertyChanged) - title_b_spin.valueChanged.connect(onTitlePropertyChanged) - title_a_spin.valueChanged.connect(onTitlePropertyChanged) - title_size_spin.valueChanged.connect(onTitlePropertyChanged) - - if not is_3d_panel: # 仅对2D面板连接位置变化信号 - title_x_spin.valueChanged.connect(onTitlePositionChanged) - title_y_spin.valueChanged.connect(onTitlePositionChanged) - - title_group.setLayout(title_layout) - self._propertyLayout.addWidget(title_group) - - # 内容属性组 - content_group = QGroupBox("内容属性") - content_layout = QGridLayout() - content_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - # 内容文本 - content_layout.addWidget(QLabel("内容文本:"), 0, 0) - content_text_edit = QTextEdit() - content_text_edit.setMaximumHeight(100) - content_text_edit.setPlainText(current_content_text) - content_layout.addWidget(content_text_edit, 0, 1, 1, 3) - - # 内容颜色 - content_layout.addWidget(QLabel("内容颜色:"), 1, 0) - content_color_row = QHBoxLayout() - content_color_row.setContentsMargins(0, 0, 0, 0) - content_color_row.setSpacing(4) - - - # 内容R分量 - content_r = QLabel("R:") - content_r.setAlignment(Qt.AlignVCenter) - content_r.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - content_color_row.addWidget(content_r) - content_r_spin = QDoubleSpinBox() - content_r_spin.setRange(0.0, 1.0) - content_r_spin.setSingleStep(0.01) - content_r_spin.setValue(current_content_color[0]) - content_color_row.addWidget(content_r_spin) - - # 内容G分量 - content_g = QLabel("G:") - content_g.setAlignment(Qt.AlignVCenter) - content_g.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - content_color_row.addWidget(content_g) - content_g_spin = QDoubleSpinBox() - content_g_spin.setRange(0.0, 1.0) - content_g_spin.setSingleStep(0.01) - content_g_spin.setValue(current_content_color[1]) - content_color_row.addWidget(content_g_spin) - - content_layout.addLayout(content_color_row, 1, 1, 1, 3) - - content_color_row_ = QHBoxLayout() - content_color_row_.setContentsMargins(0, 0, 0, 0) - content_color_row_.setSpacing(4) - content_layout.addWidget(QLabel(":"), 2, 0) - - # 内容B分量 - content_b = QLabel("B:") - content_b.setAlignment(Qt.AlignVCenter) - content_b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - content_color_row_.addWidget(content_b) - content_b_spin = QDoubleSpinBox() - content_b_spin.setRange(0.0, 1.0) - content_b_spin.setSingleStep(0.01) - content_b_spin.setValue(current_content_color[2]) - content_color_row_.addWidget(content_b_spin) - - # 内容A分量 - content_a = QLabel("A:") - content_a.setAlignment(Qt.AlignVCenter) - content_a.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - content_color_row_.addWidget(content_a) - content_a_spin = QDoubleSpinBox() - content_a_spin.setRange(0.0, 1.0) - content_a_spin.setSingleStep(0.01) - content_a_spin.setValue(current_content_color[3]) - content_color_row_.addWidget(content_a_spin) - - content_layout.addLayout(content_color_row_, 2, 1, 1, 3) - - # 内容颜色选择按钮 - content_color_button = QPushButton("选择内容颜色") - content_color_button.clicked.connect(lambda: self._selectInfoPanelContentColor( - info_panel, content_r_spin, content_g_spin, content_b_spin, content_a_spin)) - content_layout.addWidget(content_color_button, 4, 0, 1, 4) - - # 内容字体大小 - content_layout.addWidget(QLabel("内容字体大小:"), 5, 0) - content_size_spin = QDoubleSpinBox() - content_size_spin.setRange(0.01, 1.0) - content_size_spin.setSingleStep(0.01) - content_size_spin.setValue(current_content_size) - content_layout.addWidget(content_size_spin, 5, 1, 1, 3) - - # 内容位置控制 (仅对2D面板) - if not is_3d_panel: - content_layout.addLayout(QLabel("内容位置:"), 6, 0) - content_pos_layout = QHBoxLayout() - content_pos_layout.setContentsMargins(0, 0, 0, 0) - content_pos_layout.setSpacing(4) - - content_x = QLabel("X:") - content_x.setAlignment(Qt.AlignVCenter) - content_x.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - content_pos_layout.addWidget(content_x) - content_x_spin = QDoubleSpinBox() - content_x_spin.setRange(-1.0, 1.0) - content_x_spin.setSingleStep(0.01) - content_x_spin.setValue(current_content_pos[0]) - content_pos_layout.addWidget(content_x_spin) - - content_y = QLabel("Y:") - content_y.setAlignment(Qt.AlignVCenter) - content_y.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - content_pos_layout.addWidget(content_y) - content_y_spin = QDoubleSpinBox() - content_y_spin.setRange(-1.0, 1.0) - content_y_spin.setSingleStep(0.01) - content_y_spin.setValue(current_content_pos[1]) - content_pos_layout.addWidget(content_y_spin) - - content_layout.addLayout(content_pos_layout, 6, 1, 1, 3) - - # 连接内容属性变化信号 - def onContentTextChanged(): - if hasattr(self.world, 'info_panel_manager'): - # 根据面板类型选择更新方法 - if is_3d_panel: - self.world.info_panel_manager.update3DPanelContent( - panel_id, content=content_text_edit.toPlainText()) - else: - self.world.info_panel_manager.updatePanelContent( - panel_id, content=content_text_edit.toPlainText()) - - def onContentPropertyChanged(): - # 根据面板类型选择更新方法 - if is_3d_panel: - self._applyInfoPanel3DContentProperties( - info_panel, content_r_spin.value(), content_g_spin.value(), content_b_spin.value(), - content_a_spin.value(), content_size_spin.value()) - else: - self._applyInfoPanelContentProperties( - info_panel, content_r_spin.value(), content_g_spin.value(), content_b_spin.value(), - content_a_spin.value(), content_size_spin.value()) - - def onContentPositionChanged(): - # 仅对2D面板更新内容位置 - if not is_3d_panel and hasattr(self.world, 'info_panel_manager'): - panel_data = self.world.info_panel_manager.panels.get(panel_id) - if panel_data and 'content_label' in panel_data: - # 设置内容位置 - panel_data['content_label'].setPos(content_x_spin.value(), 0, content_y_spin.value()) - - def adjustContentWordwrap(): - """根据面板宽度和字体大小调整内容换行""" - # 仅对2D面板调整换行 - if not is_3d_panel: - panel_width = width_spin.value() - font_size = content_size_spin.value() - self._adjustContentWordwrap(panel_id, panel_width, font_size) - - # 连接相关信号 - content_r_spin.valueChanged.connect(onContentPropertyChanged) - content_g_spin.valueChanged.connect(onContentPropertyChanged) - content_b_spin.valueChanged.connect(onContentPropertyChanged) - content_a_spin.valueChanged.connect(onContentPropertyChanged) - - # 内容字体大小变化信号连接 - content_size_spin.valueChanged.connect(onContentPropertyChanged) - - # 内容文本变化信号连接 - content_text_edit.textChanged.connect(onContentTextChanged) - - # 标题相关信号连接 - title_text_edit.textChanged.connect(onTitleTextChanged) - title_r_spin.valueChanged.connect(onTitlePropertyChanged) - title_g_spin.valueChanged.connect(onTitlePropertyChanged) - title_b_spin.valueChanged.connect(onTitlePropertyChanged) - title_a_spin.valueChanged.connect(onTitlePropertyChanged) - title_size_spin.valueChanged.connect(onTitlePropertyChanged) - - if not is_3d_panel: # 仅对2D面板连接位置变化信号 - title_x_spin.valueChanged.connect(onTitlePositionChanged) - title_y_spin.valueChanged.connect(onTitlePositionChanged) - content_x_spin.valueChanged.connect(onContentPositionChanged) - content_y_spin.valueChanged.connect(onContentPositionChanged) - - content_group.setLayout(content_layout) - self._propertyLayout.addWidget(content_group) - - # 存储控件引用,用于后续更新 - self._current_info_panel = info_panel - self._info_panel_controls = { - 'title_text_edit': title_text_edit, - 'content_text_edit': content_text_edit, - 'title_r_spin': title_r_spin, - 'title_g_spin': title_g_spin, - 'title_b_spin': title_b_spin, - 'title_a_spin': title_a_spin, - 'content_r_spin': content_r_spin, - 'content_g_spin': content_g_spin, - 'content_b_spin': content_b_spin, - 'content_a_spin': content_a_spin, - 'title_size_spin': title_size_spin, - 'content_size_spin': content_size_spin, - 'width_spin': width_spin, - 'height_spin': height_spin - } - - # 对于2D面板,添加位置控件引用 - if not is_3d_panel: - self._info_panel_controls.update({ - 'title_x_spin': title_x_spin, - 'title_y_spin': title_y_spin, - 'content_x_spin': content_x_spin, - 'content_y_spin': content_y_spin - }) - - # 启动定时器定期检查信息面板内容变化 - self._startInfoPanelMonitoring(info_panel, panel_id) - - # 初始调整内容换行 (仅对2D面板) - if not is_3d_panel: - adjustContentWordwrap() - - print(f"✅ 信息面板属性面板已添加 (类型: {'3D' if is_3d_panel else '2D'})") - - except Exception as e: - print(f"❌ 添加信息面板属性面板失败: {e}") - import traceback - traceback.print_exc() - - def _applyInfoPanel3DTitleProperties(self, info_panel, r, g, b, a, size): - """应用3D信息面板标题属性""" - try: - panel_id = info_panel.getTag("panel_id") - if not panel_id or not hasattr(self.world, 'info_panel_manager'): - return - - # 更新3D面板标题属性 - self.world.info_panel_manager.update3DPanelProperties( - panel_id, - title_color=(r, g, b, a), - title_size=size - ) - except Exception as e: - print(f"应用3D信息面板标题属性失败: {e}") - - def _applyInfoPanel3DContentProperties(self, info_panel, r, g, b, a, size): - """应用3D信息面板内容属性""" - try: - panel_id = info_panel.getTag("panel_id") - if not panel_id or not hasattr(self.world, 'info_panel_manager'): - return - - # 更新3D面板内容属性 - self.world.info_panel_manager.update3DPanelProperties( - panel_id, - content_color=(r, g, b, a), - content_size=size - ) - except Exception as e: - print(f"应用3D信息面板内容属性失败: {e}") - - def _adjustContentWordwrap(self, panel_id, panel_width, font_size): - """调整信息面板内容的换行设置""" - try: - if hasattr(self.world, 'info_panel_manager'): - panel_data = self.world.info_panel_manager.panels.get(panel_id) - if panel_data and 'content_label' in panel_data: - # 计算合适的换行值 - 根据面板宽度和字体大小动态调整 - base_wordwrap = panel_width * 10 # 基础换行值 - # 根据字体大小调整换行值(字体越大,每行字符越少) - size_factor = 0.045 / font_size if font_size > 0 else 1.0 - adjusted_wordwrap = base_wordwrap * size_factor - - # 确保换行值在合理范围内 - wordwrap_value = max(5.0, min(100.0, adjusted_wordwrap)) - - # 应用换行设置 - panel_data['content_label']['text_wordwrap'] = wordwrap_value - print( - f"调整内容换行: 面板宽度={panel_width:.2f}, 字体大小={font_size:.3f}, 换行值={wordwrap_value:.2f}") - except Exception as e: - print(f"调整内容换行失败: {e}") - - def _startInfoPanelMonitoring(self, info_panel, panel_id): - """启动信息面板内容监控""" - try: - # 使用 QTimer 定期检查信息面板内容变化 - if not hasattr(self, '_info_panel_timer'): - from PyQt5.QtCore import QTimer - self._info_panel_timer = QTimer() - self._info_panel_timer.timeout.connect(lambda: self._checkInfoPanelUpdates(panel_id)) - self._info_panel_timer.start(500) # 每500毫秒检查一次 - except Exception as e: - print(f"❌ 启动信息面板监控失败: {e}") - - def _checkInfoPanelUpdates(self, panel_id): - """检查信息面板内容更新""" - try: - # 检查是否仍有有效的控件引用 - if not hasattr(self, '_info_panel_controls') or not self._info_panel_controls: - return - - # 检查是否仍有有效的info_panel_manager - if not hasattr(self.world, 'info_panel_manager'): - return - - # 获取当前面板数据 - panel_data = self.world.info_panel_manager.panels.get(panel_id) - if not panel_data: - return - - # 获取控件引用 - controls = self._info_panel_controls - - # 检查并更新标题文本 - if 'title_label' in panel_data and panel_data['title_label']: - current_title_text = panel_data['title_label']['text'] - title_text_edit = controls.get('title_text_edit') - if title_text_edit and title_text_edit.toPlainText() != current_title_text: - title_text_edit.blockSignals(True) - title_text_edit.setPlainText(current_title_text) - title_text_edit.blockSignals(False) - - # 检查并更新内容文本 - if 'content_label' in panel_data and panel_data['content_label']: - current_content_text = panel_data['content_label']['text'] - content_text_edit = controls.get('content_text_edit') - if content_text_edit and content_text_edit.toPlainText() != current_content_text: - content_text_edit.blockSignals(True) - content_text_edit.setPlainText(current_content_text) - content_text_edit.blockSignals(False) - - # 检查并更新标题位置 - if 'title_label' in panel_data and panel_data['title_label']: - title_pos = panel_data['title_label'].getPos() - current_title_pos = (title_pos.getX(), title_pos.getZ()) - - title_x_spin = controls.get('title_x_spin') - title_y_spin = controls.get('title_y_spin') - - if title_x_spin and abs(title_x_spin.value() - current_title_pos[0]) > 0.001: - title_x_spin.blockSignals(True) - title_x_spin.setValue(current_title_pos[0]) - title_x_spin.blockSignals(False) - - if title_y_spin and abs(title_y_spin.value() - current_title_pos[1]) > 0.001: - title_y_spin.blockSignals(True) - title_y_spin.setValue(current_title_pos[1]) - title_y_spin.blockSignals(False) - - # 检查并更新内容位置 - if 'content_label' in panel_data and panel_data['content_label']: - content_pos = panel_data['content_label'].getPos() - current_content_pos = (content_pos.getX(), content_pos.getZ()) - - content_x_spin = controls.get('content_x_spin') - content_y_spin = controls.get('content_y_spin') - - if content_x_spin and abs(content_x_spin.value() - current_content_pos[0]) > 0.001: - content_x_spin.blockSignals(True) - content_x_spin.setValue(current_content_pos[0]) - content_x_spin.blockSignals(False) - - if content_y_spin and abs(content_y_spin.value() - current_content_pos[1]) > 0.001: - content_y_spin.blockSignals(True) - content_y_spin.setValue(current_content_pos[1]) - content_y_spin.blockSignals(False) - - except Exception as e: - print(f"❌ 检查信息面板更新失败: {e}") - - def _selectInfoPanelBorderColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): - """选择信息面板边框颜色""" - try: - from PyQt5.QtWidgets import QColorDialog - from PyQt5.QtGui import QColor - - # 获取当前颜色值 - current_color = QColor( - int(r_spin.value() * 255), - int(g_spin.value() * 255), - int(b_spin.value() * 255), - int(a_spin.value() * 255) - ) - - # 打开颜色选择对话框 - color = QColorDialog.getColor(current_color, None, "选择边框颜色") - if color.isValid(): - # 更新数值框 - r_spin.setValue(color.redF()) - g_spin.setValue(color.greenF()) - b_spin.setValue(color.blueF()) - a_spin.setValue(color.alphaF()) - - except Exception as e: - print(f"❌ 选择边框颜色失败: {e}") - - def updateInfoPanelBackgroundImage(self, info_panel, image_path): - """更新信息面板背景图片 - 修复版""" - try: - panel_id = info_panel.getTag("panel_id") - if not panel_id: - print("❌ 无法找到信息面板ID") - return False - - # 通过 info_panel_manager 更新背景图片 - if hasattr(self.world, 'info_panel_manager'): - info_panel_manager = self.world.info_panel_manager - - # 检查是否是3D信息面板 - gui_type = info_panel.getTag("gui_type") - is_3d_panel = gui_type == "info_panel_3d" - - if is_3d_panel: - # 对于3D信息面板,使用专门的方法 - return self._update3DInfoPanelBackgroundImage(panel_id, info_panel, image_path) - else: - # 对于2D信息面板,使用原有方法 - if image_path: - # 设置新的背景图片 - success = info_panel_manager.setPanelBackgroundImage(panel_id, image_path) - if success: - print(f"✅ 成功设置信息面板背景图片: {image_path}") - return True - else: - print(f"❌ 设置信息面板背景图片失败: {image_path}") - return False - else: - # 清除背景图片 - success = info_panel_manager.setPanelBackgroundImage(panel_id, None) - if success: - print("✅ 成功清除信息面板背景图片") - return True - else: - print("❌ 清除信息面板背景图片失败") - return False - else: - print("❌ 未找到 info_panel_manager") - return False - - except Exception as e: - print(f"❌ 更新信息面板背景图片失败: {e}") - import traceback - traceback.print_exc() - return False - - def _applyInfoPanelBackgroundImage(self, panel_node, image_path, panel_data): - """应用信息面板背景图片 - 改进版""" - try: - from panda3d.core import CardMaker, TransparencyAttrib, Vec4 - - # 如果已有背景图片,先移除 - if 'bg_image' in panel_data and panel_data['bg_image']: - try: - panel_data['bg_image'].removeNode() - except: - pass - panel_data['bg_image'] = None - - # 加载纹理 - texture = self.world.loader.loadTexture(image_path) - if not texture: - print(f"❌ 无法加载背景图片: {image_path}") - return False - - # 设置纹理属性 - texture.setMagfilter(texture.FTLinear) - texture.setMinfilter(texture.FTLinearMipmapLinear) - - # 获取面板尺寸 - size = panel_data.get('size', (1.0, 0.6)) - - # 创建背景卡片 - cm = CardMaker('info_panel_background') - cm.setFrame(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2) - - # 生成几何体并创建节点 - bg_node = panel_node.attachNewNode(cm.generate()) - - # 应用纹理 - bg_node.setTexture(texture, 1) - - # 设置渲染属性 - bg_node.setTransparency(TransparencyAttrib.MAlpha) - bg_node.setBin("background", -10) - bg_node.setDepthWrite(False) - bg_node.setDepthTest(False) - bg_node.setLightOff() - bg_node.setTwoSided(True) - - # 确保在最底层 - bg_node.setZ(-0.1) - - # 保存引用 - panel_data['bg_image'] = bg_node - - print(f"✅ 成功应用信息面板背景图片: {image_path}") - return True - - except Exception as e: - print(f"❌ 应用信息面板背景图片失败: {e}") - import traceback - traceback.print_exc() - return False - - def _update3DInfoPanelBackgroundImage(self, panel_id, info_panel, image_path): - """更新3D信息面板背景图片 - 完整修复版""" - try: - # 从info_panel_manager获取面板数据 - if not hasattr(self.world, 'info_panel_manager'): - print("❌ 未找到 info_panel_manager") - return False - - panel_data = self.world.info_panel_manager.panels.get(panel_id) - if not panel_data: - print(f"❌ 无法找到面板数据: {panel_id}") - return False - - # 获取面板节点 - panel_node = panel_data.get('node') - if not panel_node: - print("❌ 无法找到面板节点") - return False - - # 如果已有背景图片,先销毁它 - if 'bg_image' in panel_data and panel_data['bg_image']: - try: - # 如果是NodePath对象,使用removeNode方法 - if hasattr(panel_data['bg_image'], 'removeNode'): - panel_data['bg_image'].removeNode() - # 如果是DirectGUI对象,使用destroy方法 - elif hasattr(panel_data['bg_image'], 'destroy'): - panel_data['bg_image'].destroy() - except Exception as e: - print(f"⚠️ 清理旧背景图片时出错: {e}") - panel_data['bg_image'] = None - - # 如果image_path为None,只清除背景图片 - if not image_path: - # 清除节点标签 - if info_panel.hasTag("bg_image_path"): - info_panel.clearTag("bg_image_path") - print("✅ 成功清除3D信息面板背景图片") - return True - - # 使用 world.loader 加载新图片 - texture = self.world.loader.loadTexture(image_path) - if not texture: - print(f"❌ 无法加载图片: {image_path}") - return False - - # 设置纹理过滤 - texture.setMagfilter(texture.FTLinear) - texture.setMinfilter(texture.FTLinearMipmapLinear) - - # 获取面板大小 - size = panel_data['properties'].get('size', (1.0, 0.6)) - - # 使用CardMaker创建卡片几何体 - from panda3d.core import CardMaker, TransparencyAttrib - - cm = CardMaker('info_panel_bg') - # 设置卡片大小,居中放置 - cm.setFrame(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2) - bg_geom = cm.generate() - - # 创建节点并附加几何体,直接附加到面板节点 - bg_node = panel_node.attachNewNode(bg_geom) - - # 应用纹理 - bg_node.setTexture(texture, 1) - - # 设置正确的渲染属性 - bg_node.setTransparency(TransparencyAttrib.MAlpha) - bg_node.setBin("background", -10) # 确保在最底层 - bg_node.setDepthWrite(False) - bg_node.setDepthTest(False) - bg_node.setLightOff() - bg_node.setTwoSided(True) # 双面渲染 - - # 调整位置确保在面板内容后面 - bg_node.setPos(0, 0.1, 0) # 稍微向后一点避免z-fighting - - # 保存引用 - panel_data['bg_image'] = bg_node - panel_data['properties']['bg_image'] = image_path - - # 更新节点标签 - info_panel.setTag("bg_image_path", image_path) - - print(f"✅ 成功设置3D信息面板背景图片: {image_path}") - return True - - except Exception as e: - print(f"❌ 更新3D信息面板背景图片失败: {e}") - import traceback - traceback.print_exc() - return False - - def _selectInfoPanelBackgroundColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): - """选择信息面板背景颜色""" - try: - from PyQt5.QtWidgets import QColorDialog - from PyQt5.QtGui import QColor - - # 获取当前背景颜色 - current_color = QColor( - int(r_spin.value() * 255), - int(g_spin.value() * 255), - int(b_spin.value() * 255), - int(a_spin.value() * 255) - ) - - # 显示颜色选择对话框 - color = QColorDialog.getColor(current_color, None, "选择背景颜色") - if color.isValid(): - # 更新数值框 - r_spin.setValue(color.red() / 255.0) - g_spin.setValue(color.green() / 255.0) - b_spin.setValue(color.blue() / 255.0) - a_spin.setValue(color.alpha() / 255.0) - - except Exception as e: - print(f"选择背景颜色失败: {e}") - - def _applyInfoPanelBackgroundColor(self, info_panel, r, g, b, a): - """应用信息面板背景颜色""" - try: - panel_id = info_panel.getTag("panel_id") - if not panel_id: - return - - # 更新InfoPanelManager中的背景颜色 - if hasattr(self.world, 'info_panel_manager'): - bg_color = (r, g, b, a) - - # 根据面板类型选择更新方法 - gui_type = info_panel.getTag("gui_type") - if gui_type == "info_panel_3d": - self.world.info_panel_manager.update3DPanelProperties( - panel_id, bg_color=bg_color) - else: - self.world.info_panel_manager.updatePanelProperties( - panel_id, bg_color=bg_color) - - print(f"已更新信息面板 {panel_id} 的背景颜色为 ({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f})") - except Exception as e: - print(f"应用背景颜色失败: {e}") - - def _selectInfoPanelTitleColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): - """选择信息面板标题颜色""" - try: - from PyQt5.QtWidgets import QColorDialog - from PyQt5.QtGui import QColor - - # 获取当前颜色值 - current_color = QColor( - int(r_spin.value() * 255), - int(g_spin.value() * 255), - int(b_spin.value() * 255), - int(a_spin.value() * 255) - ) - - # 打开颜色选择对话框 - color = QColorDialog.getColor(current_color, None, "选择标题颜色") - if color.isValid(): - # 更新数值框 - r_spin.setValue(color.redF()) - g_spin.setValue(color.greenF()) - b_spin.setValue(color.blueF()) - a_spin.setValue(color.alphaF()) - - except Exception as e: - print(f"❌ 选择标题颜色失败: {e}") - - def _selectInfoPanelContentColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): - """选择信息面板内容颜色""" - try: - from PyQt5.QtWidgets import QColorDialog - from PyQt5.QtGui import QColor - - # 获取当前颜色值 - current_color = QColor( - int(r_spin.value() * 255), - int(g_spin.value() * 255), - int(b_spin.value() * 255), - int(a_spin.value() * 255) - ) - - # 打开颜色选择对话框 - color = QColorDialog.getColor(current_color, None, "选择内容颜色") - if color.isValid(): - # 更新数值框 - r_spin.setValue(color.redF()) - g_spin.setValue(color.greenF()) - b_spin.setValue(color.blueF()) - a_spin.setValue(color.alphaF()) - - except Exception as e: - print(f"❌ 选择内容颜色失败: {e}") - - def _applyInfoPanelTitleProperties(self, info_panel, r, g, b, a, size): - """应用信息面板标题属性""" - try: - panel_id = info_panel.getTag("panel_id") - if not panel_id: - print("❌ 无法找到信息面板ID") - return False - - # 通过 info_panel_manager 更新标题属性 - if hasattr(self.world, 'info_panel_manager'): - info_panel_manager = self.world.info_panel_manager - - # 根据面板类型选择不同的更新方法 - gui_type = info_panel.getTag("gui_type") - if gui_type == "info_panel_3d": - # 3D面板使用update3DPanelProperties - info_panel_manager.update3DPanelProperties( - panel_id, - title_color=(r, g, b, a), - title_size=size - ) - else: - # 2D面板使用updatePanelProperties - info_panel_manager.updatePanelProperties( - panel_id, - title_color=(r, g, b, a), - title_size=size - ) - - print(f"✓ 信息面板标题属性已更新: 颜色RGBA({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f}), 大小{size:.3f}") - return True - else: - print("❌ 无法找到 info_panel_manager") - return False - - except Exception as e: - print(f"❌ 应用信息面板标题属性失败: {e}") - return False - - def _applyInfoPanelContentProperties(self, info_panel, r, g, b, a, size): - """应用信息面板内容属性""" - try: - panel_id = info_panel.getTag("panel_id") - if not panel_id: - print("❌ 无法找到信息面板ID") - return False - - # 通过 info_panel_manager 更新内容属性 - if hasattr(self.world, 'info_panel_manager'): - info_panel_manager = self.world.info_panel_manager - - # 根据面板类型选择不同的更新方法 - gui_type = info_panel.getTag("gui_type") - if gui_type == "info_panel_3d": - # 3D面板使用update3DPanelProperties - info_panel_manager.update3DPanelProperties( - panel_id, - content_color=(r, g, b, a), - content_size=size - ) - else: - # 2D面板使用updatePanelProperties - info_panel_manager.updatePanelProperties( - panel_id, - content_color=(r, g, b, a), - content_size=size - ) - - # print(f"✓ 信息面板内容属性已更新: 颜色RGBA({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f}), 大小{size:.3f}") - return True - else: - print("❌ 无法找到 info_panel_manager") - return False - - except Exception as e: - print(f"❌ 应用信息面板内容属性失败: {e}") - return False - - def _addSphericalVideoProperties(self, spherical_video): - """为球形视频添加属性控制面板""" - try: - from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, - QDoubleSpinBox, QFileDialog, QHBoxLayout, QSlider) - from PyQt5.QtCore import Qt - import os - - # 视频控制组 - video_group = QGroupBox("视频控制") - video_layout = QGridLayout() - - # 播放控制按钮 - control_layout = QHBoxLayout() - - self.play_button = QPushButton("▶️ 播放") - self.play_button.clicked.connect(lambda: self._toggleSphericalVideoPlayback(spherical_video)) - - self.stop_button = QPushButton("⏹️ 停止") - self.stop_button.clicked.connect(lambda: self._stopSphericalVideo(spherical_video)) - - control_layout.addWidget(self.play_button) - control_layout.addWidget(self.stop_button) - video_layout.addLayout(control_layout, 0, 0, 1, 2) - - # 视频进度控制 - progress_layout = QHBoxLayout() - self.progress_slider = QSlider(Qt.Horizontal) - self.progress_slider.setRange(0, 100) - self.progress_slider.setValue(0) - self.progress_slider.sliderMoved.connect(lambda value: self._seekSphericalVideo(spherical_video, value)) - - self.time_label = QLabel("00:00 / 00:00") - self.time_label.setStyleSheet("font-size: 10px; color: gray;") - - progress_layout.addWidget(self.progress_slider) - progress_layout.addWidget(self.time_label) - video_layout.addLayout(progress_layout, 1, 0, 1, 2) - - # 视频文件选择 - file_layout = QHBoxLayout() - self.select_video_button = QPushButton("选择视频文件") - self.select_video_button.clicked.connect(lambda: self._selectSphericalVideoFile(spherical_video)) - - # 显示当前视频文件 - current_video = spherical_video.getTag("video_path") if spherical_video.hasTag("video_path") else "" - self.video_file_label = QLabel(os.path.basename(current_video) if current_video else "未选择视频") - self.video_file_label.setStyleSheet("font-size: 9px; color: gray;") - self.video_file_label.setWordWrap(True) - - file_layout.addWidget(self.select_video_button) - file_layout.addWidget(self.video_file_label) - video_layout.addLayout(file_layout, 2, 0, 1, 2) - - video_group.setLayout(video_layout) - self._propertyLayout.addWidget(video_group) - - # 球体属性组 - sphere_group = QGroupBox("球体属性") - sphere_layout = QGridLayout() - - # 半径控制 - sphere_layout.addWidget(QLabel("半径:"), 0, 0) - radius_spin = QDoubleSpinBox() - radius_spin.setRange(0.1, 100) - radius_spin.setSingleStep(0.1) - current_radius = float(spherical_video.getTag("original_radius") or "5.0") - radius_spin.setValue(current_radius) - radius_spin.valueChanged.connect(lambda value: self._updateSphericalVideoRadius(spherical_video, value)) - sphere_layout.addWidget(radius_spin, 0, 1, 1, 3) - - sphere_group.setLayout(sphere_layout) - self._propertyLayout.addWidget(sphere_group) - - # 存储控件引用 - self._spherical_video_controls = { - 'play_button': self.play_button, - 'stop_button': self.stop_button, - 'progress_slider': self.progress_slider, - 'time_label': self.time_label, - 'select_button': self.select_video_button, - 'file_label': self.video_file_label - } - - print("✅ 球形视频属性面板已添加") - - except Exception as e: - print(f"❌ 添加球形视频属性面板失败: {e}") - import traceback - traceback.print_exc() - - def _toggleSphericalVideoPlayback(self, spherical_video): - """切换球形视频播放状态""" - try: - # 获取视频纹理 - movie_texture = spherical_video.getPythonTag("movie_texture") - if not movie_texture: - # 尝试从节点获取纹理 - texture = spherical_video.getTexture() - if texture: - movie_texture = texture - - if not movie_texture: - print("❌ 未找到视频纹理") - return - - # 检查当前播放状态 - if not hasattr(self, '_video_playing'): - self._video_playing = {} - - video_id = id(spherical_video) - is_playing = self._video_playing.get(video_id, False) - - if is_playing: - if hasattr(movie_texture, 'stop'): - movie_texture.stop() - if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls: - play_button = self._spherical_video_controls['play_button'] - play_button.setText("▶️ 播放") - print("⏸️ 球形视频已暂停") - else: - if hasattr(movie_texture, 'play'): - movie_texture.play() - if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls: - play_button = self._spherical_video_controls['play_button'] - play_button.setText("⏸️ 暂停") - print("▶️ 球形视频开始播放") - self._video_playing[video_id] = not is_playing - - except Exception as e: - print(f"❌ 切换球形视频播放状态失败: {e}") - - def _stopSphericalVideo(self, spherical_video): - """停止球形视频""" - try: - # 获取视频纹理 - movie_texture = spherical_video.getPythonTag("movie_texture") - if not movie_texture: - # 尝试从节点获取纹理 - texture = spherical_video.getTexture() - if texture: - movie_texture = texture - - if movie_texture: - if hasattr(movie_texture, 'stop'): - movie_texture.stop() - # 重置到开头 - if hasattr(movie_texture, 'setTime'): - movie_texture.setTime(0.0) - - if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls: - play_button = self._spherical_video_controls['play_button'] - play_button.setText("▶️ 播放") - - if hasattr(self, '_video_playing'): - video_id = id(spherical_video) - self._video_playing[video_id] = False - - print("⏹️ 球形视频已停止") - else: - print("❌ 视频纹理不支持停止操作") - else: - print("❌ 未找到视频纹理") - - except Exception as e: - print(f"❌ 停止球形视频失败: {e}") - - def _seekSphericalVideo(self, spherical_video, slider_value): - """跳转到指定时间位置""" - try: - # 获取视频纹理 - movie_texture = spherical_video.getPythonTag("movie_texture") - if not movie_texture: - # 尝试从节点获取纹理 - texture = spherical_video.getTexture() - if texture: - movie_texture = texture - - if movie_texture and hasattr(movie_texture, 'setTime'): - # 假设视频总时长为100秒(实际应该根据视频时长计算) - time_seconds = slider_value - movie_texture.setTime(time_seconds) - print(f"⏰ 跳转到时间: {time_seconds}秒") - - # 更新时间标签 - if hasattr(self, '_spherical_video_controls') and 'time_label' in self._spherical_video_controls: - time_label = self._spherical_video_controls['time_label'] - current_time = f"{int(time_seconds // 60):02d}:{int(time_seconds % 60):02d}" - total_time = "01:40" # 假设总时长100秒 - time_label.setText(f"{current_time} / {total_time}") - - else: - print("❌ 视频纹理不支持时间控制") - - except Exception as e: - print(f"❌ 球形视频跳转失败: {e}") - - def _updateSphericalVideoRadius(self, spherical_video, new_radius): - """更新球形视频半径""" - try: - from panda3d.core import GeomNode - - # 移除旧的几何体 - spherical_video.node().removeAllGeoms() - - # 创建新的球形几何体 - new_geom = self.world.gui_manager._createSphereGeometry(new_radius, segments=32) - spherical_video.node().addGeom(new_geom) - - # 更新标签 - spherical_video.setTag("original_radius", str(new_radius)) - - print(f"🔄 球形视频半径已更新为: {new_radius}") - - except Exception as e: - print(f"❌ 更新球形视频半径失败: {e}") - - def loadSphericalVideoFile(self, spherical_video, video_path): - """为球形视频加载新的视频文件""" - try: - from panda3d.core import TextureStage, Texture - import os - - if not os.path.exists(video_path): - print(f"❌ 球形视频文件不存在: {video_path}") - return False - - # 加载新的视频纹理 - movie_texture = self.world.gui_manager._loadMovieTexture(video_path) - if movie_texture: - # 清除现有的纹理 - spherical_video.clearTexture() - - # 设置视频纹理属性 - movie_texture.setWrapU(Texture.WM_clamp) - movie_texture.setWrapV(Texture.WM_clamp) - movie_texture.setMinfilter(Texture.FT_linear) - movie_texture.setMagfilter(Texture.FT_linear) - - # 为视频纹理创建专用的纹理阶段 - texture_stage = TextureStage("spherical_video") - texture_stage.setMode(TextureStage.MModulate) - - # 应用纹理到球体 - spherical_video.setTexture(texture_stage, movie_texture) - - # 保存视频纹理引用 - spherical_video.setPythonTag("movie_texture", movie_texture) - spherical_video.setTag("video_path", video_path) - - # 设置为白色以正确显示视频 - spherical_video.setColor(1, 1, 1, 1) - - print(f"✅ 成功加载新球形视频: {video_path}") - return True - else: - print(f"❌ 无法加载球形视频文件: {video_path}") - return False - - except Exception as e: - print(f"❌ 加载球形视频文件失败: {e}") - import traceback - traceback.print_exc() - return False - - def _selectSphericalVideoFile(self, spherical_video): - """选择球形视频文件""" - try: - from PyQt5.QtWidgets import QFileDialog - import os - - # 打开文件选择对话框 - file_path, _ = QFileDialog.getOpenFileName( - None, - "选择360度视频文件", - "", - "视频文件 (*.mp4 *.avi *.mov *.wmv *.flv *.mkv *.webm);;所有文件 (*)" - ) - - if file_path and os.path.exists(file_path): - # 加载新视频文件 - 修复方法调用 - success = self.loadSphericalVideoFile(spherical_video, file_path) - if success: - # 更新文件标签显示 - if hasattr(self, '_spherical_video_controls') and 'file_label' in self._spherical_video_controls: - file_label = self._spherical_video_controls['file_label'] - file_label.setText(os.path.basename(file_path)) - print(f"✅ 球形视频文件已更新为: {file_path}") - else: - print(f"❌ 无法加载球形视频文件: {file_path}") - else: - print("❌ 未选择有效的视频文件") - - except Exception as e: - print(f"❌ 选择球形视频文件失败: {e}") - import traceback - traceback.print_exc() - - def _addVideoScreenProperties(self, video_screen, item): - """添加视频屏幕属性面板""" - try: - from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, - QFileDialog, QHBoxLayout, QLineEdit) - from PyQt5.QtCore import Qt - import os - - video_info_group = QGroupBox("视频信息") - video_info_layout = QGridLayout() - video_info_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - # 显示当前视频文件路径 - 改进版本 - video_path = video_screen.getTag("video_path") if video_screen.hasTag("video_path") else "" - if video_path: - if video_path.startswith("http://") or video_path.startswith("https://"): - # 显示URL信息 - video_info_layout.addWidget(QLabel("视频流URL:"), 0, 0) - path_label = QLabel(video_path) - path_label.setWordWrap(True) - if len(video_path)>30: - display_path = video_path[:27]+"..." - path_label.setToolTip(video_path) - else: - display_path = video_path - path_label.setText(display_path) - path_label.setStyleSheet("color: #00AAFF;") - video_info_layout.addWidget(path_label, 0, 1, 1, 3) - elif os.path.exists(video_path): - # 显示本地文件信息 - video_info_layout.addWidget(QLabel("视频文件:"), 0, 0) - path_label = QLabel(os.path.basename(video_path)) - path_label.setWordWrap(True) - path_label.setStyleSheet("color: #00AAFF;") - video_info_layout.addWidget(path_label, 0, 1, 1, 3) - else: - # 文件不存在 - video_info_layout.addWidget(QLabel("状态:"), 0, 0) - status_label = QLabel("文件不存在") - status_label.setStyleSheet("color: red;") - video_info_layout.addWidget(status_label, 0, 1, 1, 3) - else: - # 无视频 - video_info_layout.addWidget(QLabel("状态:"), 0, 0) - status_label = QLabel("无视频文件") - status_label.setStyleSheet("color: red;") - video_info_layout.addWidget(status_label, 0, 1, 1, 3) - - video_info_group.setLayout(video_info_layout) - self._propertyLayout.addWidget(video_info_group) - - video_control_group = QGroupBox("视频控制") - video_control_layout = QGridLayout() - video_control_layout.setColumnMinimumWidth(0, self.column_minimum_width) - video_control_h_layout = QHBoxLayout() - - play_btn = QPushButton("▶️ 播放") - play_btn.clicked.connect(lambda: self.world.gui_manager.playVideo(video_screen)) - video_control_h_layout.addWidget(play_btn) - - # 暂停按钮 - pause_btn = QPushButton("⏸️ 暂停") - pause_btn.clicked.connect(lambda: self.world.gui_manager.pauseVideo(video_screen)) - video_control_h_layout.addWidget(pause_btn) - - # 停止按钮 - stop_btn = QPushButton("⏹️ 停止") - stop_btn.clicked.connect(lambda: self.world.gui_manager.stopVideo(video_screen)) - video_control_h_layout.addWidget(stop_btn) - - video_control_layout.addLayout(video_control_h_layout, 0, 0, 1, 4) - - # 加载新视频按钮 - load_btn = QPushButton("📁 加载新视频...") - load_btn.clicked.connect(lambda: self._loadNewVideo(video_screen, item)) - video_control_layout.addWidget(load_btn, 1, 0, 1, 4) - - video_control_group.setLayout(video_control_layout) - self._propertyLayout.addWidget(video_control_group) - - # 添加URL输入区域 - url_group = QGroupBox("网络视频") - url_layout = QGridLayout() - url_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - self.url_input = QLineEdit() - self.url_input.setPlaceholderText("输入视频流URL") - url_layout.addWidget(self.url_input, 0, 0, 1, 3) - - load_url_btn = QPushButton("加载URL") - self._current_load_url_btn_3d = load_url_btn - - load_url_btn.clicked.connect( - lambda: self._loadVideoFromURLWithOpenCV_3D(video_screen, self.url_input.text().strip())) - url_layout.addWidget(load_url_btn, 0, 3) - - url_group.setLayout(url_layout) - self._propertyLayout.addWidget(url_group) - - except Exception as e: - print(f"添加视频屏幕属性失败: {e}") - - def _add2DVideoScreenProperties(self, video_screen, item): - """为2D视频屏幕添加属性控制面板""" - try: - from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, - QFileDialog, QHBoxLayout, QLineEdit) - import os - - # 视频信息组 - video_info_group = QGroupBox("2D视频信息") - video_info_layout = QGridLayout() - video_info_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - # 显示当前视频文件路径 - # 在显示视频信息时区分本地文件和网络URL - video_path = video_screen.getTag("video_path") - if video_path: - if video_path.startswith("http://") or video_path.startswith("https://"): - # 显示URL信息 - video_info_layout.addWidget(QLabel("视频流URL:"), 0, 0) - display_path = video_path if len(video_path)<=50 else video_path[:47]+"..." - path_label = QLabel(display_path) - path_label.setWordWrap(True) - path_label.setStyleSheet("color: #00AAFF;") - video_info_layout.addWidget(path_label, 0, 1, 1, 3) - elif os.path.exists(video_path): - # 显示本地文件信息 - video_info_layout.addWidget(QLabel("视频文件:"), 0, 0) - path_label = QLabel(os.path.basename(video_path)) - path_label.setWordWrap(True) - path_label.setStyleSheet("color: #00AAFF;") - video_info_layout.addWidget(path_label, 0, 1, 1, 3) - else: - # 文件不存在 - video_info_layout.addWidget(QLabel("状态:"), 0, 0) - status_label = QLabel("文件不存在") - status_label.setStyleSheet("color: red;") - video_info_layout.addWidget(status_label, 0, 1, 1, 3) - else: - # 无视频 - video_info_layout.addWidget(QLabel("状态:"), 0, 0) - status_label = QLabel("无视频文件") - status_label.setStyleSheet("color: red;") - video_info_layout.addWidget(status_label, 0, 1, 1, 3) - - video_screen.setBin("fixed", 0) - - video_info_group.setLayout(video_info_layout) - self._propertyLayout.addWidget(video_info_group) - - # 视频控制组 - video_control_group = QGroupBox("2D视频控制") - video_control_layout = QGridLayout() - video_control_h_layout = QHBoxLayout() - - # 播放按钮 - play_btn = QPushButton("▶️ 播放") - play_btn.clicked.connect(lambda: self.play2DVideo(video_screen)) - video_control_h_layout.addWidget(play_btn) - - # 暂停按钮 - pause_btn = QPushButton("⏸️ 暂停") - pause_btn.clicked.connect(lambda: self.pause2DVideo(video_screen)) - video_control_h_layout.addWidget(pause_btn) - - # 停止按钮 - stop_btn = QPushButton("⏹️ 停止") - stop_btn.clicked.connect(lambda: self._stop2DVideo(video_screen)) - video_control_h_layout.addWidget(stop_btn) - - video_control_layout.addLayout(video_control_h_layout, 0, 0, 1, 4) - - video_control_group.setLayout(video_control_layout) - - # 加载新视频按钮 - load_btn = QPushButton("📁 加载新视频...") - load_btn.clicked.connect(lambda: self._loadNew2DVideo(video_screen, item)) - video_control_layout.addWidget(load_btn, 1, 0, 1, 4) - self._propertyLayout.addWidget(video_control_group) - - # 添加URL输入区域 - url_group = QGroupBox("网络视频") - url_layout = QGridLayout() - - self.url_input = QLineEdit() - self.url_input.setPlaceholderText("输入视频流URL") - url_layout.addWidget(self.url_input, 0, 0, 1, 3) - - load_url_btn = QPushButton("加载URL") - self._current_load_url_btn_2d = load_url_btn - - load_url_btn.clicked.connect( - lambda: self._loadVideoFromURLWithOpenCV(video_screen, self.url_input.text().strip())) - url_layout.addWidget(load_url_btn, 0, 3) - - url_group.setLayout(url_layout) - self._propertyLayout.addWidget(url_group) - - except Exception as e: - print(f"添加2D视频屏幕属性失败: {e}") - - def _loadVideoFromURLWithOpenCV(self, video_screen, url): - """使用OpenCV从URL加载视频流并在2D视频屏幕上显示(推荐版)""" - try: - import cv2 - import threading - from panda3d.core import Texture, PNMImage - import numpy as np - import time - from PyQt5.QtWidgets import QMessageBox - - load_url_btn = None - if hasattr(self, '_current_load_url_btn_2d'): - load_url_btn = self._current_load_url_btn_2d - - # 安全地更新按钮状态 - def update_button_text(text): - try: - if load_url_btn and load_url_btn.isVisible(): - load_url_btn.setText(text) - return True - except RuntimeError: - # 按钮已被删除,忽略错误 - return False - - def update_button_enabled(enabled): - try: - if load_url_btn and load_url_btn.isVisible(): - load_url_btn.setEnabled(enabled) - return True - except RuntimeError: - # 按钮已被删除,忽略错误 - return False - - if load_url_btn: - load_url_btn.setText("⏳ 加载中...") - load_url_btn.setEnabled(False) - - from PyQt5.QtWidgets import QApplication - QApplication.processEvents() - - # 停止之前可能正在播放的视频 - self._stop2DVideo(video_screen) - - # 使用 OpenCV 打开视频流 - cap = cv2.VideoCapture(url) - if not cap.isOpened(): - error_msg = f"无法打开视频流:\n{url}\n\n可能的原因:\n1. 网络连接问题\n2. 视频流地址无效\n3. 视频服务器不可用\n4. 防火墙阻止访问" - QMessageBox.critical(None, "视频流错误", error_msg) - print(f"❌ 无法打开视频流: {url}") - - # 恢复按钮状态 - if load_url_btn: - load_url_btn.setText("加载URL") - load_url_btn.setEnabled(True) - return False - - # 设置视频参数以提高性能 - cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 减少缓冲以降低延迟 - - # 创建纹理对象 - texture = Texture("video_texture") - texture.setMagfilter(Texture.FTLinear) - texture.setMinfilter(Texture.FTLinear) - texture.setWrapU(Texture.WMClamp) - texture.setWrapV(Texture.WMClamp) - - # 保存视频信息 - video_info = { - 'capture': cap, - 'texture': texture, - 'playing': True - } - - # 应用纹理到视频屏幕 - video_screen.setTexture(texture, 1) - video_screen.setPythonTag("video_info", video_info) - video_screen.setTag("video_path", url) - - # 启动视频播放线程 - def update_video_texture(): - target_fps = 60 # 降低目标帧率以减少CPU使用 - frame_time = 1.0 / target_fps - - frame_count = 0 - - while True: - try: - # 检查是否应该继续播放 - if not (video_screen.hasPythonTag("video_info") and - video_screen.getPythonTag("video_info").get('playing', False)): - break - - video_info = video_screen.getPythonTag("video_info") - cap = video_info['capture'] - - ret, frame = cap.read() - if not ret: - # 视频结束,重新开始播放 - cap.set(cv2.CAP_PROP_POS_FRAMES, 0) - continue - - frame_count += 1 - - # 调整帧大小以降低处理负担 - frame_height, frame_width = frame.shape[:2] - if frame_width > 256: # 限制最大宽度 - scale = 256.0 / frame_width - new_width = int(frame_width * scale) - new_height = int(frame_height * scale) - frame = cv2.resize(frame, (new_width, new_height)) - - # 转换颜色格式 (BGR to RGB) - frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - - # 获取帧尺寸 - height, width = frame_rgb.shape[:2] - - # 创建 PNMImage 并设置数据 - img = PNMImage(width, height, 3) - img.set_maxval(255) - - # 使用逐行设置方法(稳定且兼容性好) - for y in range(height): - for x in range(width): - r, g, b = frame_rgb[y, x] - img.setXelVal(x, y, r, g, b) - - # 更新纹理 - texture = video_info['texture'] - if texture: - texture.load(img) - - # 控制帧率 - time.sleep(frame_time) - - except Exception as e: - print(f"视频帧更新错误: {e}") - import traceback - traceback.print_exc() - break - - print("视频播放线程结束") - - # 在单独线程中处理视频流 - thread = threading.Thread(target=update_video_texture, daemon=True) - thread.start() - - print(f"✅ 开始在2D视频屏幕上播放网络视频流: {url}") - - if load_url_btn: - load_url_btn.setText("加载URL") - load_url_btn.setEnabled(True) - return True - - - except ImportError: - error_msg = "缺少必要的库,请安装: pip install opencv-python" - QMessageBox.critical(None, "依赖库缺失", error_msg) - print("❌ 缺少必要的库,请安装: pip install opencv-python") - - # 恢复按钮状态 - try: - if load_url_btn: - load_url_btn.setText("加载URL") - load_url_btn.setEnabled(True) - except RuntimeError: - pass # 按钮已被删除,忽略 - - return False - except Exception as e: - error_msg = f"加载网络视频失败:\n{str(e)}" - QMessageBox.critical(None, "视频加载错误", error_msg) - print(f"❌ 加载网络视频失败: {e}") - import traceback - traceback.print_exc() - - # 恢复按钮状态 - try: - if load_url_btn: - load_url_btn.setText("加载URL") - load_url_btn.setEnabled(True) - except RuntimeError: - pass # 按钮已被删除,忽略 - - return False - - def play2DVideo(self, video_screen): - """播放2D视频""" - try: - # 检查是否已经有视频信息(OpenCV方式) - if video_screen.hasPythonTag("video_info"): - video_info = video_screen.getPythonTag("video_info") - if video_info: - video_info['playing'] = True - print("▶️ 视频已恢复播放") - return True - - # 检查是否是 MovieTexture 方式 - movie_texture = video_screen.getPythonTag("movie_texture") - if movie_texture and hasattr(movie_texture, 'play'): - try: - movie_texture.play() - print("▶️ MovieTexture 视频开始播放") - return True - except Exception as play_error: - print(f"⚠️ MovieTexture 播放视频时出错: {play_error}") - - # 如果没有视频信息,尝试从URL加载 - video_path = video_screen.getTag("video_path") - if video_path: - # 根据路径判断是本地文件还是URL - if video_path.startswith("http://") or video_path.startswith("https://"): - return self._loadVideoFromURLWithOpenCV(video_screen, video_path) - else: - # 本地文件使用 MovieTexture 方式 - return self.world.gui_manager.load2DVideoFile(video_screen, video_path) - else: - print("❌ 没有找到视频源") - return False - - except Exception as e: - print(f"❌ 播放视频失败: {e}") - import traceback - traceback.print_exc() - return False - - def pause2DVideo(self, video_screen): - """暂停2D视频""" - try: - if video_screen.hasPythonTag("video_info"): - video_info = video_screen.getPythonTag("video_info") - if video_info: - video_info['playing'] = False - print("⏸️ 视频已暂停") - return True - print("没有正在播放的视频") - return False - except Exception as e: - print(f"❌ 暂停视频失败: {e}") - return False - - def _stop2DVideo(self, video_screen): - """停止2D视频(内部方法)""" - try: - # 停止视频播放 - if video_screen.hasPythonTag("video_info"): - video_info = video_screen.getPythonTag("video_info") - if video_info: - # 停止播放 - video_info['playing'] = False - - # 释放视频捕获资源 - if 'capture' in video_info and video_info['capture']: - try: - video_info['capture'].release() - except: - pass - - # 清理纹理 - try: - video_screen.clearTexture() - except: - pass - - video_screen.clearPythonTag("video_info") - - # 清理视频路径标签 - if video_screen.hasTag("video_path"): - video_screen.clearTag("video_path") - - print("⏹️ 视频已停止") - return True - except Exception as e: - print(f"❌ 停止视频失败: {e}") - return False - - def _loadNew2DVideo(self, video_screen, item): - """为2D视频屏幕加载新视频文件""" - try: - file_path, _ = QFileDialog.getOpenFileName( - None, - "选择视频文件", - "", - "视频文件 (*.mp4 *.avi *.mov *.mkv *.webm *.ogg)" - ) - if file_path: - success = self.world.gui_manager.load2DVideoFile(video_screen, file_path) - if success: - print(f"成功加载新视频: {file_path}") - # 刷新属性面板以显示新视频信息 - - self.updateGUIPropertyPanel(video_screen, item) - #self._stop2DVideo(video_screen) - return True - except Exception as e: - print(f"加载新视频失败: {e}") - return False - - def _loadVideoFromURLWithOpenCV_3D(self, video_screen, url): - """使用OpenCV从URL加载视频流并在3D视频屏幕上显示""" - try: - import cv2 - import threading - from panda3d.core import Texture, PNMImage, TextureStage - import time - from PyQt5.QtWidgets import QMessageBox - - is_first_load = not video_screen.hasTag("video_path") or not video_screen.getTag("video_path") - - # 确保之前的视频流已停止 - self._stop3DVideo(video_screen) - - load_url_btn = None - if hasattr(self, '_current_load_url_btn_3d'): - load_url_btn = self._current_load_url_btn_3d - - # 安全地更新按钮状态 - def update_button_text(text): - try: - if load_url_btn and load_url_btn.isVisible(): - load_url_btn.setText(text) - return True - except RuntimeError: - # 按钮已被删除,忽略错误 - return False - - def update_button_enabled(enabled): - try: - if load_url_btn and load_url_btn.isVisible(): - load_url_btn.setEnabled(enabled) - return True - except RuntimeError: - # 按钮已被删除,忽略错误 - return False - - # 如果有按钮引用,改变按钮状态为加载中 - if load_url_btn: - update_button_text("⏳ 加载中...") - update_button_enabled(False) - - from PyQt5.QtWidgets import QApplication - QApplication.processEvents() - - # 使用 OpenCV 打开视频流 - cap = cv2.VideoCapture(url) - if not cap.isOpened(): - error_msg = f"无法打开视频流:\n{url}\n\n可能的原因:\n1. 网络连接问题\n2. 视频流地址无效\n3. 视频服务器不可用\n4. 防火墙阻止访问" - QMessageBox.critical(None, "视频流错误", error_msg) - print(f"❌ 无法打开视频流: {url}") - - # 恢复按钮状态 - update_button_text("加载URL") - update_button_enabled(True) - return False - - # 设置视频参数以提高性能 - cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 减少缓冲以降低延迟 - - # 创建纹理对象 - texture = Texture("video_texture_3d") - texture.setMagfilter(Texture.FTLinear) - texture.setMinfilter(Texture.FTLinear) - texture.setWrapU(Texture.WMClamp) - texture.setWrapV(Texture.WMClamp) - - # 保存视频信息 - video_info = { - 'capture': cap, - 'texture': texture, - 'playing': True - } - - # 创建纹理阶段并应用到3D视频屏幕 - texture_stage = TextureStage("video_3d") - texture_stage.setSort(0) - texture_stage.setMode(TextureStage.MModulate) - video_screen.setTexture(texture_stage, texture) - - # 设置为白色以正确显示视频 - video_screen.setColor(1, 1, 1, 1) - - # 保存视频信息到PythonTag - video_screen.setPythonTag("video_info", video_info) - video_screen.setTag("video_path", url) - - # 启动视频播放线程 - def update_video_texture(): - target_fps = 60 # 降低目标帧率以减少CPU使用 - frame_time = 1.0 / target_fps - frame_count = 0 - - while True: - try: - # 检查是否应该继续播放 - if not (video_screen.hasPythonTag("video_info") and - video_screen.getPythonTag("video_info").get('playing', False)): - break - - video_info = video_screen.getPythonTag("video_info") - cap = video_info['capture'] - - ret, frame = cap.read() - if not ret: - # 视频结束,重新开始播放 - cap.set(cv2.CAP_PROP_POS_FRAMES, 0) - continue - - frame_count += 1 - - # 调整帧大小以降低处理负担 - frame_height, frame_width = frame.shape[:2] - if frame_width > 256: # 限制最大宽度 - scale = 256.0 / frame_width - new_width = int(frame_width * scale) - new_height = int(frame_height * scale) - frame = cv2.resize(frame, (new_width, new_height)) - - # 转换颜色格式 (BGR to RGB) - frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) - - # 获取帧尺寸 - height, width = frame_rgb.shape[:2] - - # 创建 PNMImage 并设置数据 - img = PNMImage(width, height, 3) - img.set_maxval(255) - - # 使用逐行设置方法(稳定且兼容性好) - for y in range(height): - for x in range(width): - r, g, b = frame_rgb[y, x] - img.setXelVal(x, y, r, g, b) - - # 更新纹理 - texture = video_info['texture'] - if texture: - texture.load(img) - - # 控制帧率 - time.sleep(frame_time) - - except Exception as e: - print(f"3D视频帧更新错误: {e}") - import traceback - traceback.print_exc() - break - - print("3D视频播放线程结束") - - # 在单独线程中处理视频流 - thread = threading.Thread(target=update_video_texture, daemon=True) - thread.start() - - print(f"✅ 开始在3D视频屏幕上播放网络视频流: {url}") - - # 恢复按钮状态 - update_button_text("加载URL") - update_button_enabled(True) - return True - - except ImportError: - error_msg = "缺少必要的库,请安装: pip install opencv-python" - QMessageBox.critical(None, "依赖库缺失", error_msg) - print("❌ 缺少必要的库,请安装: pip install opencv-python") - - # 恢复按钮状态 - try: - if load_url_btn: - load_url_btn.setText("加载URL") - load_url_btn.setEnabled(True) - except RuntimeError: - pass # 按钮已被删除,忽略 - - return False - except Exception as e: - error_msg = f"加载3D网络视频失败:\n{str(e)}" - QMessageBox.critical(None, "视频加载错误", error_msg) - print(f"❌ 加载3D网络视频失败: {e}") - import traceback - traceback.print_exc() - - # 恢复按钮状态 - try: - if load_url_btn: - load_url_btn.setText("加载URL") - load_url_btn.setEnabled(True) - except RuntimeError: - pass # 按钮已被删除,忽略 - - return False - - def _verifyVideoDisplay(self, video_screen, texture): - try: - applied_texture = video_screen.getTexture() - if applied_texture and applied_texture.getName() == texture.getName(): - print("视频纹理已正确应用到3d模型") - return True - else: - print("视频纹理未正确应用到3d模型") - return False - except Exception as e: - print(f"验证视频显示时出错:{e}") - return False - - def _retryLoadVideoFromURLWithOpenCV_3D(self, video_screen, url): - print("重新加载3D视频流") - return self._loadVideoFromURLWithOpenCV_3D(video_screen, url) - - def _stop3DVideo(self, video_screen): - """停止3D视频(内部方法)""" - try: - # 停止视频播放 - if video_screen.hasPythonTag("video_info"): - video_info = video_screen.getPythonTag("video_info") - if video_info: - # 停止播放 - video_info['playing'] = False - - # 释放视频捕获资源 - if 'capture' in video_info and video_info['capture']: - try: - video_info['capture'].release() - except: - pass - - # 清理纹理 - try: - video_screen.clearTexture() - except: - pass - - video_screen.clearPythonTag("video_info") - - # 清理视频路径标签 - if video_screen.hasTag("video_path"): - video_screen.clearTag("video_path") - - print("⏹️ 3D视频已停止") - return True - except Exception as e: - print(f"❌ 停止3D视频失败: {e}") - return False - - def _loadNewVideo(self, video_screen, item): - try: - file_path, _ = QFileDialog.getOpenFileName( - None, - "选择视频文件", - "", - "视频文件(*.mp4 *.avi *.mov *.mkv *.webm *.ogg)" - ) - if file_path: - success = self.world.gui_manager.loadVideoFile(video_screen, file_path) - if success: - self.updateGUIPropertyPanel(video_screen, item) - return True - except Exception as e: - print(f"加载新视频失败{e}") - return False - - def editGUI2DPosition(self, gui_element, axis, value): - """编辑2D GUI元素位置""" - try: - gui_type = gui_element.getTag("gui_type") - - if gui_type in ["button", "label", "entry", "2d_image"]: - # 2D元素使用屏幕坐标,需要转换 - current_pos = gui_element.getPos() - - if axis == "x": - # 转换逻辑坐标到屏幕坐标 - screen_x = value * 0.1 - new_pos = (screen_x, current_pos.getY(), current_pos.getZ()) - elif axis == "z": - screen_z = value * 0.1 - new_pos = (current_pos.getX(), current_pos.getY(), screen_z) - else: - return False - self.refreshModelValues(gui_element) - gui_element.setPos(*new_pos) - print(f"✓ 更新2D GUI元素位置: {axis}={value}") - return True - else: - print(f"✗ 不支持的GUI类型进行2D位置编辑: {gui_type}") - return False - except Exception as e: - print(f"✗ 更新2D GUI元素位置失败: {e}") - import traceback - traceback.print_exc() - return False - - def editGUIScale(self, gui_element, axis, value): - """编辑GUI元素缩放""" - try: - gui_type = gui_element.getTag("gui_type") - current_scale = gui_element.getScale() - - # 确保缩放值不为0 - if value == 0: - value = 0.01 - - if gui_type in ["3d_text", "3d_image", "2d_image", "video_screen", "2d_video_screen"]: - # 3D元素处理 - if axis == "x": - new_scale = (value, current_scale.getY(), current_scale.getZ()) - elif axis == "y": - new_scale = (current_scale.getX(), value, current_scale.getZ()) - elif axis == "z": - new_scale = (current_scale.getX(), current_scale.getY(), value) - else: - return False - - gui_element.setScale(*new_scale) - else: - # 2D元素处理 - if axis in ["x", "z"]: # 对于2D图像,x和z分别代表宽度和高度 - # 保持原有缩放比例,仅调整指定轴 - if axis == "x": - gui_element.setScale(value, - current_scale.getZ() if hasattr(current_scale, 'getZ') else current_scale[ - 1] if isinstance(current_scale, (list, tuple)) else current_scale) - elif axis == "z": - gui_element.setScale( - current_scale.getX() if hasattr(current_scale, 'getX') else current_scale[0] if isinstance( - current_scale, (list, tuple)) else current_scale, value) - else: - # 对于其他2D元素,使用统一缩放 - gui_element.setScale(value) - - print(f"✓ 更新GUI元素缩放: {axis}={value}") - return True - except Exception as e: - print(f"✗ 更新GUI元素缩放失败: {e}") - import traceback - traceback.print_exc() - return False - - def _update2DImageWidth(self, gui_element, width): - try: - current_scale = gui_element.getScale() - width_scaled = width / 2 - height_scaled = current_scale.getZ() - - gui_element.setScale(width_scaled, current_scale.getY(), height_scaled) - - if hasattr(gui_element, '_height_spinbox'): - gui_element._height_spinbox.blockSignals(True) - gui_element._height_spinbox.setValue(height_scaled * 2) - gui_element._height_spinbox.blockSignals(False) - - print(f"✓ 更新2D图片宽度: {width}") - except Exception as e: - print(f"✗ 更新2D图片宽度失败: {e}") - - def _update2DImageHeight(self, gui_element, height): - """更新2D图片高度""" - try: - # 获取当前缩放 - current_scale = gui_element.getScale() - # 保持宽度不变,只更新高度(Z轴) - height_scaled = height / 2 # 转换为内部缩放值 - width_scaled = current_scale.getX() - - gui_element.setScale(width_scaled, current_scale.getY(), height_scaled) - - # 更新宽度控件的值 - if hasattr(gui_element, '_width_spinbox'): - gui_element._width_spinbox.blockSignals(True) - gui_element._width_spinbox.setValue(width_scaled * 2) - gui_element._width_spinbox.blockSignals(False) - - print(f"✓ 更新2D图片高度: {height}") - except Exception as e: - print(f"✗ 更新2D图片高度失败: {e}") - - def _update2DImageScale(self, gui_element, scale): - try: - scaled_value = scale / 2 - gui_element.setScale(scaled_value, 0, scaled_value) - print(f"✓ 更新2D图片缩放: {scale}") - except Exception as e: - print(f"✗ 更新2D图片缩放失败: {e}") - - def _update3DImageScale(self, gui_element, axis, scale): - """更新3D元素缩放""" - try: - current_scale = gui_element.getScale() - - if axis == 'x': - new_scale = (scale, current_scale.getY(), current_scale.getZ()) - # elif axis == 'y': - # new_scale = (current_scale.getX(), scale, current_scale.getZ()) - elif axis == 'z': - new_scale = (current_scale.getX(), current_scale.getY(), scale) - else: - return - - gui_element.setScale(*new_scale) - print(f"✓ 更新3D元素轴缩放 {axis}: {scale}") - except Exception as e: - print(f"✗ 更新3D元素轴缩放失败: {e}") - - def _selectGUIColor(self, gui_element): - """选择GUI元素的字体颜色""" - from PyQt5.QtWidgets import QColorDialog - from PyQt5.QtGui import QColor - from panda3d.core import Vec4 - - # 获取当前颜色(如果已设置) - current_color = QColor(255, 255, 255) # 默认白色 - - # 尝试获取当前设置的颜色 - gui_type = gui_element.getTag("gui_type") - try: - if gui_type == "3d_text": - if gui_element.hasMaterial(): - material = gui_element.getMaterial() - base_color = material.getBaseColor() - current_color = QColor( - int(base_color.getX() * 255), - int(base_color.getY() * 255), - int(base_color.getZ() * 255), - int(base_color.getW() * 255) - ) - else: - # 从节点颜色获取 - node_color = gui_element.getColor() - current_color = QColor( - int(node_color.getX() * 255), - int(node_color.getY() * 255), - int(node_color.getZ() * 255), - int(node_color.getW() * 255) - ) - # current_color_obj = gui_element.getColor() - # current_color = QColor( - # int(current_color_obj[0] * 255), - # int(current_color_obj[1] * 255), - # int(current_color_obj[2] * 255) - # ) - # 对于其他类型的元素,可以添加类似的获取当前颜色的逻辑 - except: - pass # 使用默认颜色 - - color = QColorDialog.getColor(current_color, None, "选择字体颜色") - if color.isValid(): - r, g, b = color.red() / 255.0, color.green() / 255.0, color.blue() / 255.0 - self._updateGUITextColor(gui_element, (r, g, b, 1.0)) - - def _updateGUITextColor(self, gui_element, color): - """更新GUI元素的字体颜色""" - try: - gui_type = gui_element.getTag("gui_type") - - if gui_type in ["button", "label", "entry"]: - # 对于DirectGUI元素,使用text_fg属性 - gui_element['text_fg'] = color - print(f"✓ 更新DirectGUI元素字体颜色: {gui_type}") - - elif gui_type == "3d_text": - # # 对于3D文本元素,直接设置颜色 - # gui_element.setColor(*color) - # print(f"✓ 更新3D文本字体颜色: {gui_type}") - - from panda3d.core import Material, Vec4 - if not gui_element.hasMaterial(): - material = Material(f"text-material-{gui_element.getName()}") - material.setBaseColor(Vec4(color[0], color[1], color[2], color[3])) - material.setDiffuse(Vec4(color[0], color[1], color[2], color[3])) - material.setAmbient(Vec4(color[0] * 0.5, color[1] * 0.5, color[2] * 0.5, color[3])) - material.setSpecular(Vec4(0.1, 0.1, 0.1, 1.0)) - material.setShininess(10.0) - gui_element.setMaterial(material, 1) - else: - # 更新现有材质 - material = gui_element.getMaterial() - material.setBaseColor(Vec4(color[0], color[1], color[2], color[3])) - material.setDiffuse(Vec4(color[0], color[1], color[2], color[3])) - gui_element.setMaterial(material, 1) - print(f"✓ 更新3D文本材质颜色: {color}") - - gui_element.setColor(*color) - - elif gui_type == "3d_image": - # 对于3D图片,如果有文本标签的话 - # 这里可以根据需要添加特定处理 - pass - - print(f"✓ 更新GUI元素字体颜色: {gui_type}, 颜色: {color}") - except Exception as e: - print(f"✗ 更新GUI元素字体颜色失败: {e}") - import traceback - traceback.print_exc() - - def _updateGUIScaleY(self, gui_element, scale_y): - """更新GUI元素Y轴缩放""" - try: - gui_type = gui_element.getTag("gui_type") - current_scale = gui_element.getScale() - - # 对于不同的GUI类型使用不同的缩放方法 - if gui_type in ["3d_text", "3d_image"]: - # 对于3D元素,直接设置缩放 - new_scale = (current_scale.getX(), scale_y, current_scale.getZ()) - gui_element.setScale(*new_scale) - else: - # 对于2D元素,保持原有的缩放方法 - gui_element.setScale(scale_y) - - print(f"✓ 更新GUI元素Y轴缩放: {scale_y}") - except Exception as e: - print(f"✗ 更新GUI元素Y轴缩放失败: {e}") - - def _updateGUIScaleZ(self, gui_element, scale_z): - """更新GUI元素Z轴缩放""" - try: - gui_type = gui_element.getTag("gui_type") - current_scale = gui_element.getScale() - - # 对于不同的GUI类型使用不同的缩放方法 - if gui_type in ["3d_text", "3d_image"]: - # 对于3D元素,直接设置缩放 - new_scale = (current_scale.getX(), current_scale.getY(), scale_z) - gui_element.setScale(*new_scale) - else: - # 对于2D元素,使用单一缩放值(保持宽高比) - gui_element.setScale(scale_z) - - print(f"✓ 更新GUI元素Z轴缩放: {scale_z}") - except Exception as e: - print(f"✗ 更新GUI元素Z轴缩放失败: {e}") - - def update2DImageTexture(self, image_element, texture_path): - """更新2D图片纹理 - 修复版""" - try: - from panda3d.core import TextureStage, TransparencyAttrib - - # 加载纹理 - texture = self.world.loader.loadTexture(texture_path) - if not texture: - print(f"❌ 无法加载纹理: {texture_path}") - return False - - # 设置纹理过滤 - texture.setMagfilter(texture.FTLinear) - texture.setMinfilter(texture.FTLinearMipmapLinear) - - # 应用纹理到元素 - image_element.setTexture(texture, 1) - - # 设置正确的渲染属性以避免黑色显示 - image_element.setTransparency(TransparencyAttrib.MAlpha) - image_element.setBin("fixed", 20) # 确保在正确层级渲染 - image_element.setDepthWrite(False) - image_element.setDepthTest(False) - image_element.setLightOff() - image_element.setTwoSided(True) - - print(f"✅ 2D图片纹理已更新: {texture_path}") - return True - - except Exception as e: - print(f"❌ 更新2D图片纹理失败: {e}") - import traceback - traceback.print_exc() - return False - - def _updateScriptPropertyPanel(self, game_object): - """更新脚本属性面板""" - # 获取对象上的脚本 - scripts = self.world.getScripts(game_object) - - if scripts: - # 添加脚本信息标题 - scriptTitleLabel = QLabel("已挂载脚本:") - scriptTitleLabel.setStyleSheet("color: #00AAFF; font-weight: bold; font-size: 12px;") - self._propertyLayout.addRow(scriptTitleLabel) - - # 显示每个脚本的信息 - for i, script_component in enumerate(scripts): - script_name = script_component.script_name - enabled = script_component.enabled - - # 脚本名称和状态 - scriptLabel = QLabel(f"脚本 {i + 1}:") - scriptInfo = QLabel(f"{script_name}") - scriptInfo.setStyleSheet("color: green; font-weight: bold;" if enabled else "color: gray;") - self._propertyLayout.addRow(scriptLabel, scriptInfo) - - # 脚本启用/禁用按钮 - enableButton = QPushButton("禁用" if enabled else "启用") - enableButton.setStyleSheet( - "background-color: #FF6B6B; color: white;" if enabled - else "background-color: #4ECDC4; color: white;" - ) - enableButton.clicked.connect( - lambda checked, sc=script_component: self._toggleScriptEnabled(sc) - ) - self._propertyLayout.addRow("状态:", enableButton) - - # 分隔线 - if i < len(scripts) - 1: - separator = QLabel("─" * 20) - separator.setStyleSheet("color: lightgray;") - self._propertyLayout.addRow(separator) - else: - # 显示无脚本信息 - noScriptLabel = QLabel("无挂载脚本") - noScriptLabel.setStyleSheet("color: gray; font-style: italic;") - self._propertyLayout.addRow("脚本:", noScriptLabel) - - def _toggleScriptEnabled(self, script_component): - """切换脚本启用状态""" - script_component.enabled = not script_component.enabled - status = "启用" if script_component.enabled else "禁用" - print(f"脚本 {script_component.script_name} 已{status}") - - # 刷新属性面板显示 - if hasattr(self.world.selection, 'selectedObject') and self.world.selection.selectedObject: - # 找到当前选中项并更新 - tree_widget = self.world.treeWidget - if tree_widget and tree_widget.currentItem(): - self.updatePropertyPanel(tree_widget.currentItem()) - - def updateLightPropertyPanel(self, model): - """更新光源属性面板""" - light_object = model.getPythonTag("rp_light_object") - - if light_object: - # 变换属性组 - transform_group = QGroupBox("变换 Transform") - transform_layout = QGridLayout() - transform_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - transform_layout.addWidget(QLabel("位置"), 0, 0) - - # 位置属性 - current_pos = light_object.pos - position_row = QHBoxLayout() - position_row.setContentsMargins(0, 0, 0, 0) - position_row.setSpacing(4) - - xPos = QDoubleSpinBox() - xPos.setRange(-1000, 1000) - xPos.setValue(current_pos.getX()) - xPos.valueChanged.connect(lambda v: self._updateLightPosition(light_object, model, 'x', v)) - - yPos = QDoubleSpinBox() - yPos.setRange(-1000, 1000) - yPos.setValue(current_pos.getY()) - yPos.valueChanged.connect(lambda v: self._updateLightPosition(light_object, model, 'y', v)) - - zPos = QDoubleSpinBox() - zPos.setRange(-1000, 1000) - zPos.setValue(current_pos.getZ()) - zPos.valueChanged.connect(lambda v: self._updateLightPosition(light_object, model, 'z', v)) - - for axis_label, widget in (("X:", xPos), ("Y:", yPos), ("Z:", zPos)): - axis = QLabel(axis_label) - axis.setAlignment(Qt.AlignVCenter) - axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - position_row.addWidget(axis) - position_row.addWidget(widget) - - transform_layout.addLayout(position_row, 0, 1, 1, 3) - - worldPos = model.getPos(self.world.render) - world_position_row = QHBoxLayout() - world_position_row.setContentsMargins(0, 0, 0, 0) - world_position_row.setSpacing(4) - transform_layout.addWidget(QLabel("世界位置"), 1, 0) - - worldXPos = QDoubleSpinBox() - worldXPos.setRange(-1000, 1000) - worldXPos.setValue(worldPos.getX()) - worldXPos.setReadOnly(True) - - worldYPos = QDoubleSpinBox() - worldYPos.setRange(-1000, 1000) - worldYPos.setValue(worldPos.getY()) - worldYPos.setReadOnly(True) - - worldZPos = QDoubleSpinBox() - worldZPos.setRange(-1000, 1000) - worldZPos.setValue(worldPos.getZ()) - worldZPos.setReadOnly(True) - - for axis_label, widget in (("X:", worldXPos), ("Y:", worldYPos), ("Z:", worldZPos)): - axis = QLabel(axis_label) - axis.setAlignment(Qt.AlignVCenter) - axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - world_position_row.addWidget(axis) - world_position_row.addWidget(widget) - - transform_layout.addLayout(world_position_row, 1, 1, 1, 3) - - # 旋转属性(仅聚光灯) - next_row = 2 - if hasattr(light_object, 'direction'): - current_hpr = model.getHpr() - rotation_row = QHBoxLayout() - rotation_row.setContentsMargins(0, 0, 0, 0) - rotation_row.setSpacing(4) - transform_layout.addWidget(QLabel("旋转"), next_row, 0) - - hRot = QDoubleSpinBox() - hRot.setRange(-180, 180) - hRot.setValue(current_hpr.getX()) - hRot.valueChanged.connect(lambda v: self._updateLightRotation(light_object, model, 'h', v)) - - pRot = QDoubleSpinBox() - pRot.setRange(-180, 180) - pRot.setValue(current_hpr.getY()) - pRot.valueChanged.connect(lambda v: self._updateLightRotation(light_object, model, 'p', v)) - - rRot = QDoubleSpinBox() - rRot.setRange(-180, 180) - rRot.setValue(current_hpr.getZ()) - rRot.valueChanged.connect(lambda v: self._updateLightRotation(light_object, model, 'r', v)) - - for axis_label, widget in (("H:", hRot), ("P:", pRot), ("R:", rRot)): - axis = QLabel(axis_label) - axis.setAlignment(Qt.AlignVCenter) - axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - rotation_row.addWidget(axis) - rotation_row.addWidget(widget) - - transform_layout.addLayout(rotation_row, next_row, 1, 1, 3) - next_row += 1 - - # 缩放属性 - current_scale = model.getScale() - scale_row = QHBoxLayout() - scale_row.setContentsMargins(0, 0, 0, 0) - scale_row.setSpacing(4) - transform_layout.addWidget(QLabel("缩放"), next_row, 0) - - xScaleSpinBox = QDoubleSpinBox() - xScaleSpinBox.setRange(0.01, 100) - xScaleSpinBox.setSingleStep(0.1) - xScaleSpinBox.setValue(current_scale.getX()) - xScaleSpinBox.valueChanged.connect(lambda v: self._updateLightScale(model, 'x', v)) - - yScaleSpinBox = QDoubleSpinBox() - yScaleSpinBox.setRange(0.01, 100) - yScaleSpinBox.setSingleStep(0.1) - yScaleSpinBox.setValue(current_scale.getY()) - yScaleSpinBox.valueChanged.connect(lambda v: self._updateLightScale(model, 'y', v)) - - zScaleSpinBox = QDoubleSpinBox() - zScaleSpinBox.setRange(0.01, 100) - zScaleSpinBox.setSingleStep(0.1) - zScaleSpinBox.setValue(current_scale.getZ()) - zScaleSpinBox.valueChanged.connect(lambda v: self._updateLightScale(model, 'z', v)) - - for axis_label, widget in (("X:", xScaleSpinBox), ("Y:", yScaleSpinBox), ("Z:", zScaleSpinBox)): - axis = QLabel(axis_label) - axis.setAlignment(Qt.AlignVCenter) - axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - scale_row.addWidget(axis) - scale_row.addWidget(widget) - - transform_layout.addLayout(scale_row, next_row, 1, 1, 3) - - transform_group.setLayout(transform_layout) - self._propertyLayout.addWidget(transform_group) - - # 光源属性组 - light_group = QGroupBox("光源属性") - light_layout = QGridLayout() - - current_energy = light_object.energy - stored_energy = None - - if current_energy == 0.0 and model.hasTag("stored_energy"): - try: - stored_energy = float(model.getTag("stored_energy")) - except ValueError: - pass - - display_energy = stored_energy if stored_energy is not None and stored_energy > 0 else current_energy - - # 能量 - light_layout.addWidget(QLabel("能量:"), 0, 0) - energySpinBox = QDoubleSpinBox() - energySpinBox.setRange(0, 10000) - energySpinBox.setValue(display_energy) - energySpinBox.valueChanged.connect(lambda v: self._updateLightEnergy(light_object, v)) - light_layout.addWidget(energySpinBox, 0, 1, 1, 3) - - # 半径 - light_layout.addWidget(QLabel("半径:"), 1, 0) - radiusSpinBox = QDoubleSpinBox() - radiusSpinBox.setRange(1, 2000) - radiusSpinBox.setValue(light_object.radius) - radiusSpinBox.valueChanged.connect(lambda v: self._updateLightRadius(light_object, v)) - light_layout.addWidget(radiusSpinBox, 1, 1, 1, 3) - - # 视野角度(仅聚光灯) - if hasattr(light_object, 'fov'): - light_layout.addWidget(QLabel("视野角度:"), 2, 0) - fovSpinBox = QDoubleSpinBox() - fovSpinBox.setRange(1, 180) - fovSpinBox.setValue(light_object.fov) - fovSpinBox.valueChanged.connect(lambda v: self._updateLightFOV(light_object, v)) - light_layout.addWidget(fovSpinBox, 2, 1, 1, 3) - - light_group.setLayout(light_layout) - self._propertyLayout.addWidget(light_group) - - def _updateLightPosition(self, light_object, node_path, axis, value): - current_pos = light_object.pos - - if axis == 'x': - new_pos = Vec3(value, current_pos.getY(), current_pos.getZ()) - elif axis == 'y': - new_pos = Vec3(current_pos.getX(), value, current_pos.getZ()) - else: # z - new_pos = Vec3(current_pos.getX(), current_pos.getY(), value) - # 更新RenderPipeline光源位置 - light_object.pos = new_pos - - # 同步更新场景节点位置(用于显示) - node_path.setPos(new_pos) - - def _updateLightRotation(self, light_object, node_path, axis, value): - """更新光源旋转""" - from panda3d.core import Vec3 - - current_hpr = node_path.getHpr() - if axis == 'h': - new_hpr = Vec3(value, current_hpr.getY(), current_hpr.getZ()) - elif axis == 'p': - new_hpr = Vec3(current_hpr.getX(), value, current_hpr.getZ()) - else: - new_hpr = Vec3(current_hpr.getX(), current_hpr.getY(), value) - - node_path.setHpr(new_hpr) - - if hasattr(light_object, 'direction'): - direction_mat = node_path.getMat() - new_direction = direction_mat.xformVec(Vec3(0, 1, 0)) - light_object.direction = new_direction - - print(f"光源旋转已更新:{axis}={value}") - - def _updateLightEnergy(self, light_object, value): - """更新光源强度""" - light_object.energy = value - - def _updateLightRadius(self, light_object, value): - """更新光源半径""" - light_object.radius = value - - def _updateLightFOV(self, light_Object, value): - """更新聚光灯视野角度""" - if hasattr(light_Object, 'fov'): - light_Object.fov = value - - def _updateLightTemperature(self, light_object, value): - """更新光源色温""" - light_object.set_color_from_temperature(value) - # 保存色温值以便下次显示 - light_object._temperature = value - - def _updateLightInnerRadius(self, light_object, value): - """更新点光源内半径""" - if hasattr(light_object, 'inner_radius'): - light_object.inner_radius = value - - def _updateLightShaowResolution(self, light_object, value): - """更新阴影分辨率""" - light_object.shadow_map_resolution = value - - def _updateLightNearPlane(self, light_object, value): - """更新近平面距离""" - light_object.near_plane = value - - def _updateLightCastsShadows(self, light_object, casts_shadows): - """更新光源是否投射阴影""" - light_object.casts_shadows = casts_shadows - - def _updateLightScale(self, node_path, axis, value): - """更新光源节点缩放""" - current_scale = node_path.getScale() - - if axis == 'x': - new_scale = Vec3(value, current_scale.getY(), current_scale.getZ()) - elif axis == 'y': - new_scale = Vec3(current_scale.getX(), value, current_scale.getZ()) - else: - new_scale = Vec3(current_scale.getX(), current_scale.getY(), value) - - node_path.setScale(new_scale) - - def _generateUniqueMaterialNames(self, materials, model_name): - """生成唯一的材质名称,避免重复""" - material_names = {} - unique_names = [] - - for i, material in enumerate(materials): - # 获取材质的原始名称 - base_name = material.get_name() if hasattr(material, 'get_name') and material.get_name() else f"材质{i + 1}" - full_name = f"{base_name}:{model_name}" - - # 检查是否重复 - if full_name in material_names: - # 如果重复,增加计数器 - material_names[full_name] += 1 - unique_name = f"{full_name}_{material_names[full_name]}" - else: - # 首次出现,记录并使用原名 - material_names[full_name] = 0 - unique_name = full_name - - unique_names.append(unique_name) - - return unique_names - - def _updateModelMaterialPanel(self, model): - """模型材质属性""" - if model.is_empty(): - print("警告: 无法在空的 NodePath 上查找材质") - no_material_group = QGroupBox("材质信息") - no_material_layout = QGridLayout() - no_material_label = QLabel("无材质") - no_material_label.setStyleSheet("color: gray;font-style:italic;") - no_material_layout.addWidget(no_material_label, 0, 0) - no_material_group.setLayout(no_material_layout) - self._propertyLayout.addWidget(no_material_group) - return - - materials = model.find_all_materials() - - if not materials: - no_material_group = QGroupBox("材质信息") - no_material_layout = QGridLayout() - no_material_label = QLabel("无材质") - no_material_label.setStyleSheet("color: gray;font-style:italic;") - no_material_layout.addWidget(no_material_label, 0, 0) - no_material_group.setLayout(no_material_layout) - self._propertyLayout.addWidget(no_material_group) - return - - model_name = model.getName() or "未命名模型" - name_counter = {} - - # 创建材质到几何节点的映射字典 - self._material_geom_mapping = {} - self._material_display_names = {} - - for i, material in enumerate(materials): - # 查找使用该材质的几何节点,使用几何节点名称作为材质标题 - geom_node = self._findSpecificGeomNodeWithMaterial(model, material) - - if geom_node: - geom_node_name = geom_node.getName() - unique_name = f"{geom_node_name}({model_name})" - print(f"材质 {i}: 使用几何节点名称 '{geom_node_name}'") - else: - material_name = material.get_name() if hasattr(material, - 'get_name') and material.get_name() else f"材质{i + 1}" - unique_name = f"{material_name}({model_name})" - print(f"材质 {i}: 未找到几何节点,使用材质名称 '{material_name}'") - - # 处理重复名称 - if unique_name in name_counter: - name_counter[unique_name] += 1 - display_name = f"{unique_name}_{name_counter[unique_name]}" - else: - name_counter[unique_name] = 1 - display_name = unique_name - - # 存储材质和对应的几何节点信息到映射字典中 - material_id = id(material) - self._material_geom_mapping[material_id] = geom_node - self._material_display_names[material_id] = display_name - - # 材质信息组 - # material_group = QGroupBox(display_name) - material_group = QGroupBox("材质属性") - material_layout = QGridLayout() - material_layout.setColumnMinimumWidth(0, self.column_minimum_width) - - material_layout.addWidget(QLabel("名称:"), 0, 0) - limited_length = 25 - if len(display_name) > limited_length: - display_name = f"{display_name[:limited_length - 3]}..." - name_label = QLabel(display_name) - # name_label.setStyleSheet("color: #FF6B6B; font-weight: bold;") - material_layout.addWidget(name_label, 0, 1, 1, 3) - - # 检查材质类型并显示状态 - material_status = self._getMaterialStatus(material) - if material_status != "标准PBR材质": - material_layout.addWidget(QLabel("状态:"), 1, 0) - status_label = QLabel(material_status) - # status_label.setStyleSheet("color:#FFA500;font-style:italic;font-size:10px;") - material_layout.addWidget(status_label, 1, 1, 1, 3) - - # 基础颜色编辑 - base_color = self._getOrCreateMaterialBaseColor(material) - if base_color is not None: - print(f"材质基础颜色: {base_color}") - - # 基础颜色编辑 - color_row = 2 if material_status != "标准PBR材质" else 1 - - r_spinbox = QDoubleSpinBox() - r_spinbox.setRange(0.0, 1.0) - r_spinbox.setSingleStep(0.01) - r_spinbox.setValue(base_color.x) - r_spinbox.valueChanged.connect(lambda v, mat=material: self._updateMaterialBaseColor(mat, 'r', v)) - - g_spinbox = QDoubleSpinBox() - g_spinbox.setRange(0.0, 1.0) - g_spinbox.setSingleStep(0.01) - g_spinbox.setValue(base_color.y) - g_spinbox.valueChanged.connect(lambda v, mat=material: self._updateMaterialBaseColor(mat, 'g', v)) - - b_spinbox = QDoubleSpinBox() - b_spinbox.setRange(0.0, 1.0) - b_spinbox.setSingleStep(0.01) - b_spinbox.setValue(base_color.z) - b_spinbox.valueChanged.connect(lambda v, mat=material: self._updateMaterialBaseColor(mat, 'b', v)) - - title_label = QLabel("基础颜色") - material_layout.addWidget(title_label, color_row, 0) - - base_color_layout = QHBoxLayout() - base_color_layout.setContentsMargins(0, 0, 0, 0) - base_color_layout.setSpacing(4) - - for label_text, spinbox in (("R:", r_spinbox), ("G:", g_spinbox), ("B:", b_spinbox)): - channel_label = QLabel(label_text) - channel_label.setAlignment(Qt.AlignVCenter) - channel_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - base_color_layout.addWidget(channel_label) - spinbox.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - base_color_layout.addWidget(spinbox) - - # base_color_layout.addStretch(1) - - material_layout.addLayout(base_color_layout, color_row, 1, 1, 3) - else: - no_base_color_label = QLabel("无法获取材质基础颜色") - no_base_color_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") - material_layout.addWidget(QLabel("基础颜色:"), 1, 0) - material_layout.addWidget(no_base_color_label, 1, 1, 1, 3) - - # 材质属性行 - current_row = color_row + 1 if base_color is not None else 2 - - # 粗糙度 - if hasattr(material, 'roughness') and material.roughness is not None: - try: - roughness_value = float(material.roughness) - material_layout.addWidget(QLabel("粗糙度:"), current_row, 0) - roughness_spinbox = QDoubleSpinBox() - roughness_spinbox.setRange(0.0, 1.0) - roughness_spinbox.setSingleStep(0.01) - roughness_spinbox.setValue(roughness_value) - roughness_spinbox.valueChanged.connect( - lambda v, mat=material: self._updateMaterialRoughness(mat, v)) - material_layout.addWidget(roughness_spinbox, current_row, 1, 1, 1) - except (TypeError, ValueError) as e: - print(f"粗糙度值无效: {material.roughness}, 错误: {e}") - material_layout.addWidget(QLabel("粗糙度:"), current_row, 0) - no_roughness_label = QLabel("粗糙度值无效") - no_roughness_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") - material_layout.addWidget(no_roughness_label, current_row, 1, 1, 1) - else: - material_layout.addWidget(QLabel("粗糙度:"), current_row, 0) - no_roughness_label = QLabel("不支持") - no_roughness_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") - material_layout.addWidget(no_roughness_label, current_row, 1, 1, 1) - - current_row += 1 - - # 金属性 - if hasattr(material, 'metallic') and material.metallic is not None: - try: - metallic_value = float(material.metallic) - material_layout.addWidget(QLabel("金属性:"), current_row, 0) - metallic_spinbox = QDoubleSpinBox() - metallic_spinbox.setRange(0.0, 1.0) - metallic_spinbox.setSingleStep(0.01) - metallic_spinbox.setValue(metallic_value) - metallic_spinbox.valueChanged.connect(lambda v, mat=material: self._updateMaterialMetallic(mat, v)) - material_layout.addWidget(metallic_spinbox, current_row, 1, 1, 1) - except (TypeError, ValueError) as e: - print(f"金属性值无效: {material.metallic}, 错误: {e}") - material_layout.addWidget(QLabel("金属性:"), current_row, 0) - no_metallic_label = QLabel("值无效") - no_metallic_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") - material_layout.addWidget(no_metallic_label, current_row, 1, 1, 1) - else: - material_layout.addWidget(QLabel("金属性:"), current_row, 0) - no_metallic_label = QLabel("不支持") - no_metallic_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") - material_layout.addWidget(no_metallic_label, current_row, 1, 1, 1) - - current_row += 1 - - # 折射率 - if hasattr(material, 'refractive_index') and material.refractive_index is not None: - try: - ior_value = float(material.refractive_index) - material_layout.addWidget(QLabel("折射率:"), current_row, 0) - ior_spinbox = QDoubleSpinBox() - ior_spinbox.setRange(1.0, 3.0) - ior_spinbox.setSingleStep(0.01) - ior_spinbox.setValue(ior_value) - ior_spinbox.valueChanged.connect(lambda v, mat=material: self._updateMaterialIOR(mat, v)) - material_layout.addWidget(ior_spinbox, current_row, 1, 1, 1) - except (TypeError, ValueError) as e: - print(f"折射率值无效: {material.refractive_index}, 错误: {e}") - material_layout.addWidget(QLabel("折射率:"), current_row, 0) - no_ior_label = QLabel("折射率值无效") - no_ior_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") - material_layout.addWidget(no_ior_label, current_row, 1, 1, 1) - else: - material_layout.addWidget(QLabel("折射率:"), current_row, 0) - no_ior_label = QLabel("此材质不支持折射率编辑") - no_ior_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") - material_layout.addWidget(no_ior_label, current_row, 1, 1, 1) - - current_row += 1 - - # 纹理贴图标题 - texture_title = QLabel("纹理贴图") - texture_title.setStyleSheet("font-weight:bold;") - material_layout.addWidget(texture_title, current_row, 0, 1, 4) - current_row += 1 - - # 纹理按钮 - 两列布局 - diffuse_button = QPushButton("选择漫反射贴图") - diffuse_button.clicked.connect(lambda checked, title=unique_name: self._selectDiffuseTexture(title)) - material_layout.addWidget(diffuse_button, current_row, 0, 1, 2) - - normal_button = QPushButton("选择法线贴图") - normal_button.clicked.connect(lambda checked, mat=material: self._selectNormalTexture(mat)) - material_layout.addWidget(normal_button, current_row, 2, 1, 2) - current_row += 1 - - roughness_button = QPushButton("选择粗糙度贴图") - roughness_button.clicked.connect(lambda checked, mat=material: self._selectRoughnessTexture(mat)) - material_layout.addWidget(roughness_button, current_row, 0, 1, 2) - - metallic_button = QPushButton("选择金属性贴图") - metallic_button.clicked.connect(lambda checked, mat=material: self._selectMetallicTexture(mat)) - material_layout.addWidget(metallic_button, current_row, 2, 1, 2) - - # #IOR贴图 - # ior_button = QPushButton("选择IOR贴图") - # ior_button.clicked.connect(lambda checked,mat = material:self._selectIORTexture(mat)) - # self._propertyLayout.addRow("IOR贴图",ior_button) - - # # 视差贴图 - # parallax_button = QPushButton("选择视差贴图") - # parallax_button.clicked.connect(lambda checked, mat=material: self._selectParallaxTexture(mat)) - # self._propertyLayout.addRow("视差贴图:", parallax_button) - # - # # 自发光贴图 - # emission_button = QPushButton("选择自发光贴图") - # emission_button.clicked.connect(lambda checked, mat=material: self._selectEmissionTexture(mat)) - # self._propertyLayout.addRow("自发光贴图:", emission_button) - # - # # 环境光遮蔽贴图 - # ao_button = QPushButton("选择AO贴图") - # ao_button.clicked.connect(lambda checked, mat=material: self._selectAOTexture(mat)) - # self._propertyLayout.addRow("AO贴图:", ao_button) - - # # 透明度贴图 - # alpha_button = QPushButton("选择透明度贴图") - # alpha_button.clicked.connect(lambda checked, mat=material: self._selectAlphaTexture(mat)) - # self._propertyLayout.addRow("透明度贴图:", alpha_button) - # - # # 细节贴图 - # detail_button = QPushButton("选择细节贴图") - # detail_button.clicked.connect(lambda checked, mat=material: self._selectDetailTexture(mat)) - # self._propertyLayout.addRow("细节贴图:", detail_button) - # - # # 光泽贴图 - # gloss_button = QPushButton("选择光泽贴图") - # gloss_button.clicked.connect(lambda checked, mat=material: self._selectGlossTexture(mat)) - # self._propertyLayout.addRow("光泽贴图:", gloss_button) - - # 在纹理按钮后添加当前贴图信息显示 - current_row = self._displayCurrentTextures(material, material_layout, current_row) - - current_row = self._addShadingModelPanel(material, material_layout, current_row) - current_row = self._addEmissionPanel(material, material_layout, current_row) - current_row = self._addMaterialPresetPanel(material, material_layout, current_row) - - material_group.setLayout(material_layout) - self._propertyLayout.addWidget(material_group) - - def _updateMaterialBaseColor(self, material, component, value): - """更新材质基础颜色(智能版本)""" - try: - from panda3d.core import Vec4 - - # 获取当前颜色 - current_color = self._getOrCreateMaterialBaseColor(material) - if current_color is None: - print(f"无法获取材质基础颜色,跳过更新") - return - - # 计算新颜色 - if component == 'r': - new_color = Vec4(value, current_color.y, current_color.z, current_color.w) - elif component == 'g': - new_color = Vec4(current_color.x, value, current_color.z, current_color.w) - elif component == 'b': - new_color = Vec4(current_color.x, current_color.y, value, current_color.w) - # elif component == 'a': # Alpha分量处理 - # self._updateMaterialTransparency(material, value) - # return - # new_color = Vec4(current_color.x, current_color.y, current_color.z, value) - else: - print(f"未知的颜色分量: {component}") - return - - # 尝试多种方式设置颜色 - success = False - - # 方法1: 使用set_base_color - if hasattr(material, 'set_base_color'): - try: - material.set_base_color(new_color) - print(f"✓ 通过set_base_color更新: {component}={value}") - success = True - except Exception as e: - print(f"set_base_color失败: {e}") - - # 方法2: 使用setDiffuse作为备选 - if not success and hasattr(material, 'setDiffuse'): - try: - material.setDiffuse(new_color) - print(f"✓ 通过setDiffuse更新: {component}={value}") - success = True - except Exception as e: - print(f"setDiffuse失败: {e}") - - # 方法3: 直接设置属性 - if not success and hasattr(material, 'base_color'): - try: - material.base_color = new_color - print(f"✓ 通过直接属性设置更新: {component}={value}") - success = True - except Exception as e: - print(f"直接属性设置失败: {e}") - - if success: - print(f"材质基础颜色已更新: {new_color}") - else: - print(f"✗ 所有更新方法都失败了") - - except Exception as e: - print(f"更新材质基础颜色失败: {e}") - - def _updateMaterialTransparency(self, material, alpha_value): - try: - from panda3d.core import Vec4 - if hasattr(material, 'emission'): - material.emission = Vec4(3, 0, 0, 0) - print("设置透明着色器模型") - if hasattr(material, 'shading_model_param0'): - material.shading_model_param0 = alpha_value - print(f"设置透明度参数{alpha_value}") - if hasattr(material, 'base_color'): - current_color = material.base_color - material.base_color = Vec4(current_color.x, current_color.y, current_color.z, alpha_value) - print(f"更新基础颜色透明度{alpha_value}") - print(f"材质透明度已更新:{alpha_value}") - except Exception as e: - print(f"更新材质透明度失败: {e}") - - def _updateMaterialRoughness(self, material, value): - """更新材质粗糙度(安全版本)""" - try: - if not hasattr(material, 'roughness') or material.roughness is None: - print(f"材质不支持粗糙度属性或值为None,跳过更新") - return - material.set_roughness(value) - except Exception as e: - print(f"更新材质粗糙度失败: {e}") - - def _updateMaterialMetallic(self, material, value): - """更新材质金属性(安全版本)""" - try: - if not hasattr(material, 'metallic') or material.metallic is None: - print(f"材质不支持金属性属性或值为None,跳过更新") - return - material.set_metallic(value) - except Exception as e: - print(f"更新材质金属性失败: {e}") - - def _updateMaterialIOR(self, material, value): - """更新材质折射率(安全版本)""" - try: - if not hasattr(material, 'refractive_index') or material.refractive_index is None: - print(f"材质不支持折射率属性或值为None,跳过更新") - return - material.set_refractive_index(value) - except Exception as e: - print(f"更新材质折射率失败: {e}") - - def _getMaterialStatus(self, material): - """获取材质状态描述""" - try: - # 检查材质的各种属性 - has_base_color = hasattr(material, 'has_base_color') and material.has_base_color() - has_roughness = hasattr(material, 'has_roughness') and material.has_roughness() - has_metallic = hasattr(material, 'has_metallic') and material.has_metallic() - has_ior = hasattr(material, 'has_refractive_index') and material.has_refractive_index() - - # 检查基本属性是否存在 - has_base_color_attr = hasattr(material, 'base_color') - has_roughness_attr = hasattr(material, 'roughness') - has_metallic_attr = hasattr(material, 'metallic') - has_ior_attr = hasattr(material, 'refractive_index') - - if has_base_color and has_roughness and has_metallic and has_ior: - return "标准PBR材质" - elif has_base_color_attr and has_roughness_attr and has_metallic_attr: - return "PBR材质(部分属性可用)" - elif has_base_color_attr or has_roughness_attr or has_metallic_attr: - return "基础材质(支持部分PBR属性)" - else: - return "传统材质(可转换为PBR)" - - except Exception as e: - print(f"检查材质状态时出错: {e}") - return "未知材质类型(可尝试编辑)" - - def _getTextureModeString(self, mode): - """获取纹理模式的字符串表示""" - from panda3d.core import TextureStage - mode_map = { - TextureStage.MModulate: "Modulate", - TextureStage.MDecal: "Decal", - TextureStage.MBlend: "Blend", - TextureStage.MReplace: "Replace", - TextureStage.MAdd: "Add", - TextureStage.MCombine: "Combine", - TextureStage.MBlendColorScale: "BlendColorScale", - TextureStage.MModulateGlow: "ModulateGlow", - TextureStage.MModulateGloss: "ModulateGloss", - TextureStage.MNormal: "Normal", - TextureStage.MNormalHeight: "NormalHeight", - TextureStage.MGlow: "Glow", - TextureStage.MGloss: "Gloss", - TextureStage.MHeight: "Height", - TextureStage.MSelector: "Selector", - TextureStage.MNormalGloss: "NormalGloss" - } - return mode_map.get(mode, f"Unknown({mode})") - - def _checkAndAdjustMaterialProperty(self, material, property_name, current_value, texture_type): - """检查并智能调整材质属性值""" - if current_value <= 0.01: - print(f"⚠️ 警告:材质{property_name}过低({current_value}),{texture_type}贴图可能无效果") - print(f" RenderPipeline使用公式: 最终{property_name} = 材质{property_name} × 贴图值") - print(f" 当前设置下,即使贴图为白色(1.0),最终效果也只有{current_value}") - - # 询问用户是否要自动调整(在实际应用中,这里可以弹出对话框) - # 目前我们采用保守的自动调整策略 - recommended_value = 0.8 # 推荐值 - - if property_name == "粗糙度": - material.set_roughness(recommended_value) - elif property_name == "金属性": - material.set_metallic(recommended_value) - - print(f"✓ 已自动调整材质{property_name}为 {recommended_value}") - print(f" 现在{texture_type}贴图的效果范围:白色区域={recommended_value},黑色区域=0.0") - return recommended_value - else: - print(f"✓ 材质{property_name}合适: {current_value}") - print(f" {texture_type}贴图效果范围:白色区域={current_value:.2f},黑色区域=0.0") - return current_value - - def _getOrCreateMaterialBaseColor(self, material): - """智能获取或创建材质的基础颜色""" - from panda3d.core import Vec4 - - try: - # 方法1: 尝试获取base_color属性 - if hasattr(material, 'base_color') and material.base_color is not None: - print(f"✓ 找到base_color属性: {material.base_color}") - return material.base_color - - # 方法2: 尝试调用get_base_color方法 - if hasattr(material, 'get_base_color'): - try: - base_color = material.get_base_color() - if base_color is not None: - # print(f"✓ 通过get_base_color()获取: {base_color}") - return base_color - except: - pass - - # 方法3: 尝试从diffuse颜色获取 - if hasattr(material, 'getDiffuse'): - try: - diffuse_color = material.getDiffuse() - if diffuse_color is not None: - print(f"✓ 从diffuse颜色获取: {diffuse_color}") - # 同时设置为base_color - if hasattr(material, 'set_base_color'): - material.set_base_color(diffuse_color) - return diffuse_color - except: - pass - - # 方法4: 尝试从ambient颜色获取 - if hasattr(material, 'getAmbient'): - try: - ambient_color = material.getAmbient() - if ambient_color is not None: - print(f"✓ 从ambient颜色获取: {ambient_color}") - # 同时设置为base_color - if hasattr(material, 'set_base_color'): - material.set_base_color(ambient_color) - return ambient_color - except: - pass - - # 方法5: 创建默认的基础颜色 - print("⚠️ 未找到现有颜色,创建默认基础颜色") - default_color = Vec4(0.8, 0.8, 0.8, 1.0) # 默认灰白色 - - # 尝试设置到材质 - if hasattr(material, 'set_base_color'): - material.set_base_color(default_color) - print(f"✓ 设置默认base_color: {default_color}") - elif hasattr(material, 'setDiffuse'): - material.setDiffuse(default_color) - print(f"✓ 设置默认diffuse: {default_color}") - - return default_color - - except Exception as e: - print(f"✗ 获取材质基础颜色失败: {e}") - return None - - def _selectDiffuseTexture(self, material_title): - """漫反射贴图""" - from PyQt5.QtWidgets import QFileDialog - import os - - file_dialog = QFileDialog(None, "选择漫反射贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") - - if file_dialog.exec_(): - filename = file_dialog.selectedFiles()[0] - if filename: - # 使用跨平台路径标准化 - normalized_path = util.normalize_model_path(filename) - self._applyDiffuseTexture(material_title, normalized_path) - print(f"已选择漫反射贴图:{filename} -> 标准化路径:{normalized_path}") - - def _selectNormalTexture(self, material): - """选择法线贴图""" - from PyQt5.QtWidgets import QFileDialog - - file_dialog = QFileDialog(None, "选择法线贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") - - if file_dialog.exec_(): - filename = file_dialog.selectedFiles()[0] - if filename: - # 使用跨平台路径标准化 - normalized_path = util.normalize_model_path(filename) - self._applyNormalTexture(material, normalized_path) - print(f"已选择法线贴图:{filename} -> 标准化路径:{normalized_path}") - - def _selectRoughnessTexture(self, material): - """选择粗糙度贴图""" - from PyQt5.QtWidgets import QFileDialog - - file_dialog = QFileDialog(None, "选择粗糙度贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") - - if file_dialog.exec_(): - filename = file_dialog.selectedFiles()[0] - if filename: - # 使用跨平台路径标准化 - normalized_path = util.normalize_model_path(filename) - self._applyRoughnessTexture_FINAL(material, normalized_path) - print(f"已选择粗糙度贴图: {filename} -> 标准化路径:{normalized_path}") - - def _selectMetallicTexture(self, material): - """选择金属性贴图""" - from PyQt5.QtWidgets import QFileDialog - - file_dialog = QFileDialog(None, "选择金属性贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") - - if file_dialog.exec_(): - filename = file_dialog.selectedFiles()[0] - if filename: - # 使用跨平台路径标准化 - normalized_path = util.normalize_model_path(filename) - self._applyMetallicTexture_NEW(material, normalized_path) - print(f"已选择金属性贴图: {filename} -> 标准化路径:{normalized_path}") - - # IOR贴图 - def _selectIORTexture(self, material): - """选择IOR贴图""" - from PyQt5.QtWidgets import QFileDialog - - file_dialong = QFileDialog(None, "选择IOR贴图", "", "图像(*.png *.jpg *.jpeg *.tga *.bmp)") - - if file_dialong.exec_(): - filename = file_dialong.selectedFiles()[0] - if filename: - # 使用跨平台路径标准化 - normalized_path = util.normalize_model_path(filename) - self._applyIORTexture(material, normalized_path) - print(f"已选择IOR贴图:{filename} -> 标准化路径:{normalized_path}") - - def _selectParallaxTexture(self, material): - """选择视差贴图""" - from PyQt5.QtWidgets import QFileDialog - - file_dialog = QFileDialog(None, "选择视差贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") - - if file_dialog.exec_(): - filename = file_dialog.selectedFiles()[0] - if filename: - # 使用跨平台路径标准化 - normalized_path = util.normalize_model_path(filename) - self._applyParallaxTexture(material, normalized_path) - print(f"已选择视差贴图:{filename} -> 标准化路径:{normalized_path}") - - def _selectEmissionTexture(self, material): - """选择自发光贴图""" - from PyQt5.QtWidgets import QFileDialog - - file_dialog = QFileDialog(None, "选择自发光贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") - - if file_dialog.exec_(): - filename = file_dialog.selectedFiles()[0] - if filename: - # 使用跨平台路径标准化 - normalized_path = util.normalize_model_path(filename) - self._applyEmissionTexture(material, normalized_path) - print(f"已选择自发光贴图:{filename} -> 标准化路径:{normalized_path}") - - def _selectAOTexture(self, material): - """选择环境光遮蔽贴图""" - from PyQt5.QtWidgets import QFileDialog - - file_dialog = QFileDialog(None, "选择AO贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") - - if file_dialog.exec_(): - filename = file_dialog.selectedFiles()[0] - if filename: - # 使用跨平台路径标准化 - normalized_path = util.normalize_model_path(filename) - self._applyAOTexture(material, normalized_path) - print(f"已选择AO贴图:{filename} -> 标准化路径:{normalized_path}") - - def _selectAlphaTexture(self, material): - """选择透明度贴图""" - from PyQt5.QtWidgets import QFileDialog - - file_dialog = QFileDialog(None, "选择透明度贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") - - if file_dialog.exec_(): - filename = file_dialog.selectedFiles()[0] - if filename: - # 使用跨平台路径标准化 - normalized_path = util.normalize_model_path(filename) - self._applyAlphaTexture(material, normalized_path) - print(f"已选择透明度贴图:{filename} -> 标准化路径:{normalized_path}") - - def _selectDetailTexture(self, material): - """选择细节贴图""" - from PyQt5.QtWidgets import QFileDialog - - file_dialog = QFileDialog(None, "选择细节贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") - - if file_dialog.exec_(): - filename = file_dialog.selectedFiles()[0] - if filename: - # 使用跨平台路径标准化 - normalized_path = util.normalize_model_path(filename) - self._applyDetailTexture(material, normalized_path) - print(f"已选择细节贴图:{filename} -> 标准化路径:{normalized_path}") - - def _selectGlossTexture(self, material): - """选择光泽贴图""" - from PyQt5.QtWidgets import QFileDialog - - file_dialog = QFileDialog(None, "选择光泽贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") - - if file_dialog.exec_(): - filename = file_dialog.selectedFiles()[0] - if filename: - # 使用跨平台路径标准化 - normalized_path = util.normalize_model_path(filename) - self._applyGlossTexture(material, normalized_path) - print(f"已选择光泽贴图:{filename} -> 标准化路径:{normalized_path}") - - # def _applyDiffuseTexture(self, texture_path): - # from panda3d.core import TextureStage - # try: - # from RenderPipelineFile.rpcore.loader import RPLoader - # texture = RPLoader.load_texture(texture_path) - # if not texture: - # print("纹理加载失败") - # return - # - # node = self.world.selected_np - # if not node: - # print("未选中节点") - # return - # - # # 1. 直接给节点挂贴图 - # diffuse_stage = TextureStage("diffuse") - # diffuse_stage.setSort(0) - # node.setTexture(diffuse_stage, texture) - # - # # 2. 再给它刷一个 RenderPipeline 效果 - # effect_file = "effects/default.yaml" - # self.world.render_pipeline.set_effect( - # node, - # effect_file, - # {"diffuse_texture": texture}, - # 100 - # ) - # print("贴图已直接贴到节点:", node.getName()) - # except Exception as e: - # print("贴图失败:", e) - def _applyDiffuseTexture(self, material_title, texture_path): - """应用漫反射贴图""" - try: - from RenderPipelineFile.rpcore.loader import RPLoader - from panda3d.core import TextureStage, TransparencyAttrib - - # 加载纹理 - texture = RPLoader.load_texture(texture_path) - if texture: - # 获取材质所属的节点 - material, node = self._findMaterialAndNodeByTitle(material_title) - if node and material: - print(f"正在为节点 {node.getName()} 应用漫反射贴图") - - # 检查纹理是否包含透明度信息 - has_alpha = False - # 检查纹理格式 - format_name = str(texture.getFormat()) - if 'alpha' in format_name.lower() or 'rgba' in format_name.lower(): - has_alpha = True - - print(f"纹理格式: {texture.getFormat()}, 包含透明通道: {has_alpha}") - - # 检查是否有金属性贴图,选择合适的PBR效果 - print("🔧 检查金属性贴图并选择合适的PBR效果...") - has_metallic = self._hasMetallicTexture(node) - needs_alpha = self._needsAlphaTesting(node) or has_alpha - - if has_metallic: - print("✅ 检测到金属性贴图,使用支持金属性的PBR效果") - effect_file = "effects/pbr_with_metallic.yaml" - else: - print("✅ 没有金属性贴图,使用默认PBR效果") - effect_file = "effects/default.yaml" - - print(f"🔍 透明度测试: {'需要' if needs_alpha else '不需要'}") - - try: - self.world.render_pipeline.set_effect( - node, - effect_file, - { - "normal_mapping": True, # 启用法线映射支持 - "render_gbuffer": True, - "alpha_testing": needs_alpha, # 根据是否需要透明度决定 - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 100 - ) - print(f"✅ {effect_file} 效果已应用(支持透明度和金属性)") - except Exception as e: - print(f"⚠️ PBR效果应用失败: {e}") - - # 根据RenderPipeline的gbuffer.frag.glsl模板: - # p3d_Texture0 用于漫反射贴图 (line 111: texture(p3d_Texture0, texcoord).xyz) - - # 清理可能存在的漫反射贴图 - existing_stages = node.findAllTextureStages() - for stage in existing_stages: - if stage.getSort() == 0 or "diffuse" in stage.getName().lower(): - node.clearTexture(stage) - print(f"清理了现有的漫反射贴图阶段: {stage.getName()}") - - # 创建漫反射贴图纹理阶段,对应p3d_Texture0 - diffuse_stage = TextureStage("diffuse") - diffuse_stage.setSort(0) # 对应p3d_Texture0 - diffuse_stage.setMode(TextureStage.MModulate) # 标准的调制模式 - - # 应用漫反射贴图 - node.setTexture(diffuse_stage, texture) - print("漫反射贴图已应用到p3d_Texture0槽") - - # 如果纹理包含透明度,启用透明度渲染 - if has_alpha: - print("检测到透明纹理,启用透明度渲染") - node.setTransparency(TransparencyAttrib.MAlpha) - node.setDepthWrite(False) # 透明物体通常不写入深度缓冲区 - else: - # 确保关闭透明度(避免之前设置的影响) - node.setTransparency(TransparencyAttrib.MNone) - - # 调试信息:显示当前纹理阶段 - print("=== 漫反射贴图应用后的纹理阶段信息 ===") - all_stages = node.findAllTextureStages() - for i, stage in enumerate(all_stages): - tex = node.getTexture(stage) - mode_name = self._getTextureModeString(stage.getMode()) - print( - f"阶段 {i}: {stage.getName()}, Sort: {stage.getSort()}, 模式: {mode_name}, 纹理: {tex.getName() if tex else 'None'}") - print("==========================================") - - print(f"漫反射贴图已成功应用:{texture_path}") - else: - print(f"未找到材质标题对应的材质或节点: {material_title}") - else: - print("纹理加载失败") - except Exception as e: - print(f"应用漫反射贴图失败{e}") - import traceback - traceback.print_exc() - - def _applyNormalTexture(self, material, texture_path): - """应用法线贴图 - Blender风格效果""" - try: - from RenderPipelineFile.rpcore.loader import RPLoader - from panda3d.core import TextureStage, Vec4 - - texture = RPLoader.load_texture(texture_path) - if not texture: - print("❌ 纹理加载失败") - return - - # 查找使用该材质的具体几何节点 - node = self._findSpecificGeomNodeForMaterial(material) - if not node: - print("❌ 未找到材质对应的节点") - return - - # 清理现有的法线贴图,避免冲突 - print("🧹 清理现有纹理阶段...") - existing_stages = node.findAllTextureStages() - has_diffuse_texture = False - - for stage in existing_stages: - if "normal" in stage.getName().lower() or stage.getSort() == 1: - node.clearTexture(stage) - print(f" ✅ 已清理法线纹理阶段: {stage.getName()}") - elif stage.getSort() == 0: # 检查是否有漫反射贴图 - tex = node.getTexture(stage) - if tex: - has_diffuse_texture = True - print(f" 🔍 发现现有漫反射贴图: {tex.getName()}") - - - # 如果没有漫反射贴图,必须创建白色纹理,否则法线映射会失效 - if not has_diffuse_texture: - print("⚠️ 没有漫反射贴图,创建白色纹理确保法线映射正常工作...") - self._createWhiteDiffuseTexture(node) - - # 检查是否有金属性贴图,选择合适的PBR效果 - print("🔧 检查金属性贴图并选择合适的PBR效果...") - has_metallic = self._hasMetallicTexture(node) - needs_alpha = self._needsAlphaTesting(node) - - if has_metallic: - print("✅ 检测到金属性贴图,使用支持金属性的PBR效果") - effect_file = "effects/pbr_with_metallic.yaml" - else: - print("✅ 没有金属性贴图,使用默认PBR效果") - effect_file = "effects/default.yaml" - - print(f"🔍 透明度测试: {'需要' if needs_alpha else '不需要'}") - - try: - self.world.render_pipeline.set_effect( - node, - effect_file, - { - "normal_mapping": True, # 强制启用法线映射 - "render_gbuffer": True, - "alpha_testing": needs_alpha, # 根据是否需要透明度决定 - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 100 - ) - print(f"✅ {effect_file} 效果已应用(法线映射已启用)") - except Exception as e: - print(f"⚠️ PBR效果应用失败: {e}") - - # 这里不需要检查roughness_stage,因为这是法线贴图方法 - print("🔍 继续法线贴图处理...") - - # 创建法线贴图纹理阶段,对应p3d_Texture1 - print("🎨 创建法线纹理阶段...") - normal_stage = TextureStage("normal_map") - normal_stage.setSort(1) # 对应shader中的p3d_Texture1 - normal_stage.setMode(TextureStage.MModulate) # 使用标准模式,不是MNormal - normal_stage.setTexcoordName("texcoord") - - print(f"📋 法线纹理阶段信息:") - print(f" • 名称: {normal_stage.getName()}") - print(f" • 排序: {normal_stage.getSort()} (对应p3d_Texture1)") - print(f" • 模式: {normal_stage.getMode()} (MModulate)") - - # 应用纹理到正确的纹理槽 - node.setTexture(normal_stage, texture) - print(f"🔗 法线纹理已绑定到p3d_Texture1槽") - - # 设置材质的normalfactor参数(用于法线强度) - current_emission = material.emission - if current_emission is None: - current_emission = Vec4(0, 0, 0, 0) - print("材质emission为None,使用默认值") - - # emission.y 用于存储 normalfactor - new_emission = Vec4(current_emission.x, 1.0, current_emission.z, current_emission.w) - material.set_emission(new_emission) - print(f"🔧 设置法线强度参数: normalfactor = {new_emission.y}") - - # 验证纹理应用 - applied_texture = node.getTexture(normal_stage) - if applied_texture: - print(f"✅ 法线贴图成功应用到p3d_Texture1槽") - print(f"📊 纹理信息:") - print(f" • 纹理名称: {applied_texture.getName()}") - print(f" • 纹理尺寸: {applied_texture.getXSize()}x{applied_texture.getYSize()}") - print(f"📊 Blender风格效果:") - print(f" • 法线贴图将影响表面细节和光照") - print(f" • 不会改变材质颜色,只影响表面法线") - else: - print("❌ 纹理应用验证失败") - - except Exception as e: - print(f"❌ 应用法线贴图失败: {e}") - import traceback - traceback.print_exc() - - def _applyRoughnessTexture_FINAL(self, material, texture_path): - """应用粗糙度贴图 - 先编译后绑定策略""" - try: - from RenderPipelineFile.rpcore.loader import RPLoader - from panda3d.core import TextureStage - import time - - print(f"🎨 应用粗糙度贴图(先编译后绑定): {texture_path}") - - # 1. 加载纹理 - texture = RPLoader.load_texture(texture_path) - if not texture: - print("❌ 纹理加载失败") - return - - # 2. 找到节点 - node = self._findSpecificGeomNodeForMaterial(material) - if not node: - print("❌ 未找到材质对应的节点") - return - - print(f"🎯 目标节点: {node.getName()}") - - # 3. 设置材质粗糙度为1.0 - material.set_roughness(1.0) - print("🔧 材质粗糙度设置为1.0") - - # 4. 检查该材质有没有应用法线贴图,如果没有就先添加一个默认的法线贴图 - print("🔧 步骤1:检查法线贴图...") - has_normal = self._hasNormalTexture(node) - - if not has_normal: - print("⚠️ 检测到材质没有法线贴图,先添加默认法线贴图...") - # self._applyDefaultNormalTexture(node) - self._applyNormalTexture(material, "RenderPipelineFile/Default_NRM_2K.png") - print("✅ 默认法线贴图已添加") - else: - print("✅ 检测到材质已有法线贴图") - - # 5. 检查是否有金属性贴图和透明漫反射贴图,选择合适的PBR效果 - print("🔧 步骤2:检查金属性贴图和透明度设置...") - has_metallic = self._hasMetallicTexture(node) - needs_alpha = self._needsAlphaTesting(node) - - if has_metallic: - print("✅ 检测到金属性贴图,使用支持金属性的PBR效果") - effect_file = "effects/pbr_with_metallic.yaml" - else: - print("✅ 没有金属性贴图,使用默认PBR效果") - effect_file = "effects/default.yaml" - - print(f"🔍 透明度测试: {'需要' if needs_alpha else '不需要'}") - - self.world.render_pipeline.set_effect( - node, - effect_file, - { - "normal_mapping": True, # 始终启用法线映射 - "render_gbuffer": True, - "alpha_testing": needs_alpha, # 根据是否需要透明度决定 - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 100 - ) - print(f"✅ {effect_file} 效果已应用") - # print("✅ 着色器预编译完成") - - # 5. 等待编译完成 - # time.sleep(0.2) # 200ms等待 - # print("⏱️ 等待着色器编译...") - - # 6. 现在绑定纹理到已编译的着色器 - # print("🔧 步骤2:绑定纹理到编译完成的着色器...") - # roughness_stage = TextureStage("roughness_map") - # roughness_stage.setSort(3) # p3d_Texture3 - # roughness_stage.setMode(TextureStage.MModulate) - # node.setTexture(roughness_stage, texture) - # print("✅ 纹理已绑定到预编译着色器") - - print("🧹 清理现有粗糙度贴图...") - existing_stages = node.findAllTextureStages() - for stage in existing_stages: - if stage.getSort() == 3: # p3d_Texture3槽 - node.clearTexture(stage) - print(f" ✅ 已清理现有粗糙度贴图: {stage.getName()}") - - # 添加验证和重试 - for attempt in range(2): # 最多重试3次 - time.sleep(0.1) # 短暂等待 - roughness_stage = TextureStage("roughness_map") - roughness_stage.setSort(3) # p3d_Texture3 - roughness_stage.setMode(TextureStage.MModulate) - node.setTexture(roughness_stage, texture) - - # 7. 验证效果 - applied_texture = node.getTexture(roughness_stage) - if applied_texture: - print(f"✅ 粗糙度贴图应用成功(先编译后绑定)") - print(f" 纹理: {applied_texture.getName()}") - print("🔍 应该立即看到正确的粗糙度效果") - else: - print("❌ 纹理绑定验证失败") - - except Exception as e: - print(f"❌ 应用粗糙度贴图失败: {e}") - import traceback - traceback.print_exc() - - def _hasNormalTexture(self, node): - """检查节点是否有法线贴图""" - try: - all_stages = node.findAllTextureStages() - for stage in all_stages: - if stage.getSort() == 1: # p3d_Texture1 是法线贴图槽 - tex = node.getTexture(stage) - if tex: - print(f"🔍 发现法线贴图: {tex.getName()}") - return True - return False - except Exception as e: - print(f"⚠️ 检查法线贴图失败: {e}") - return False - - def _applySmartPBREffect(self, node, effect_file="effects/default.yaml"): - """智能应用PBR效果,自动检测是否需要启用法线映射""" - try: - # 检查是否有法线贴图 - has_normal = self._hasNormalTexture(node) - - print(f"🔧 应用智能PBR效果 ({effect_file})...") - print(f" 法线映射: {'启用' if has_normal else '禁用'}") - - self.world.render_pipeline.set_effect( - node, - effect_file, - { - "normal_mapping": has_normal, # 根据是否有法线贴图决定 - "render_gbuffer": True, - "alpha_testing": False, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 100 - ) - print("✅ 智能PBR效果已应用") - return True - except Exception as e: - print(f"⚠️ 智能PBR效果应用失败: {e}") - return False - - def _verifyTextureInShader(self, node, texture_type, expected_sort): - """验证纹理是否在shader中正确处理""" - try: - print(f"🔍 验证{texture_type}纹理在shader中的处理...") - - # 检查当前效果 - current_effect = node.getEffect() - if current_effect: - print(f" 当前shader效果: {current_effect}") - else: - print(" ⚠️ 节点没有shader效果") - return False - - # 检查纹理槽 - stages = node.findAllTextureStages() - target_stage = None - for stage in stages: - if stage.getSort() == expected_sort: - target_stage = stage - break - - if target_stage: - tex = node.getTexture(target_stage) - if tex: - print(f" ✅ 找到{texture_type}纹理在sort={expected_sort}槽: {tex.getName()}") - - # 检查纹理数据 - if tex.hasRamImage(): - print(f" ✅ 纹理有RAM图像数据") - else: - print(f" ⚠️ 纹理没有RAM图像数据") - - return True - else: - print(f" ❌ sort={expected_sort}槽没有纹理") - else: - print(f" ❌ 没有找到sort={expected_sort}的纹理阶段") - - return False - - except Exception as e: - print(f" ❌ 验证失败: {e}") - return False - - def _ensureEnhancedPBREffect(self, node): - """确保节点使用增强的PBR效果,支持金属性纹理""" - try: - print("🔧 应用金属性贴图支持的PBR效果...") - self.world.render_pipeline.set_effect( - node, - "effects/pbr_with_metallic.yaml", - { - "normal_mapping": False, # 关闭法线贴图避免干扰 - "render_gbuffer": True, # 必须启用gbuffer渲染 - "alpha_testing": False, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 100 # 高优先级确保应用 - ) - print("✅ 金属性贴图PBR效果已应用") - return True - except Exception as e: - print(f"⚠️ 金属性贴图PBR效果失败: {e}") - # 回退到默认效果 - try: - self.world.render_pipeline.set_effect( - node, - "effects/default.yaml", - { - "normal_mapping": False, - "render_gbuffer": True, - "alpha_testing": False, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 100 - ) - print("✅ 已回退到默认PBR效果(不支持金属性贴图)") - return True - except Exception as e2: - print(f"⚠️ 默认效果也失败: {e2}") - return False - - def _createWhiteDiffuseTexture(self, node): - """创建白色漫反射纹理,确保法线贴图能正常工作""" - try: - from RenderPipelineFile.rpcore.loader import RPLoader - from panda3d.core import TextureStage - - print("🎨 尝试加载白色纹理...") - - # 尝试加载一个白色纹理文件,如果不存在就创建 - white_texture = None - - # 方法1:尝试从RenderPipeline加载空白基础颜色纹理(应该是白色) - try: - white_texture = RPLoader.load_texture("RenderPipelineFile/data/empty_textures/empty_basecolor.png") - if white_texture: - print("✅ 从RenderPipeline空白纹理加载白色纹理") - except Exception as e1: - print(f"⚠️ 加载空白纹理失败: {e1}") - pass - - # 方法2:如果没有默认纹理,创建简单的白色纹理 - if not white_texture: - try: - from panda3d.core import Texture - white_texture = Texture("white_diffuse") - white_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) - # 设置白色像素数据 - white_data = b'\xff\xff\xff' - white_texture.setRamImage(white_data) - print("✅ 创建了程序生成的白色纹理") - except Exception as e2: - print(f"⚠️ 创建程序纹理也失败: {e2}") - return False - - if white_texture: - # 创建漫反射纹理阶段 - diffuse_stage = TextureStage("white_diffuse") - diffuse_stage.setSort(0) # 对应p3d_Texture0 - diffuse_stage.setMode(TextureStage.MModulate) - - # 应用白色纹理 - node.setTexture(diffuse_stage, white_texture) - print("✅ 白色漫反射纹理已应用,材质颜色将正常显示") - return True - - return False - - except Exception as e: - print(f"⚠️ 创建白色纹理失败: {e}") - return False - - def _applyMetallicTexture(self, material, texture_path): - """应用金属性贴图 - Blender风格效果""" - try: - from RenderPipelineFile.rpcore.loader import RPLoader - from panda3d.core import TextureStage - - print(f"🎨 应用金属性贴图: {texture_path}") - - texture = RPLoader.load_texture(texture_path) - if not texture: - print("❌ 纹理加载失败") - return - - # 查找使用该材质的具体几何节点 - node = self._findSpecificGeomNodeForMaterial(material) - if not node: - print("❌ 未找到材质对应的节点") - return - - print(f"🎯 目标节点: {node.getName()}") - - # 保持材质金属性为1.0,让贴图完全控制 - material.set_metallic(1.0) - print("🔧 材质金属性设置为1.0,启用贴图完全控制") - - # 清理现有的金属性贴图,避免冲突 - print("🧹 清理现有纹理阶段...") - existing_stages = node.findAllTextureStages() - print(f"🔍 发现 {len(existing_stages)} 个现有纹理阶段:") - - for stage in existing_stages: - tex = node.getTexture(stage) - tex_name = tex.getName() if tex else "无纹理" - print(f" - {stage.getName()} (sort={stage.getSort()}) -> {tex_name}") - - # 只清理金属性相关的纹理阶段 - if "metallic" in stage.getName().lower() or stage.getSort() == 5: - node.clearTexture(stage) - print(f" ✅ 已清理金属性纹理阶段: {stage.getName()}") - - # 确保没有纹理占用sort=5的槽位 - sort5_stages = [s for s in existing_stages if s.getSort() == 5] - if sort5_stages: - print(f"⚠️ 发现占用sort=5槽位的纹理: {[s.getName() for s in sort5_stages]}") - for stage in sort5_stages: - node.clearTexture(stage) - print(f" ✅ 已强制清理: {stage.getName()}") - - # 使用调试模式来验证金属性纹理是否正确读取 - debug_texture_mode = True # 设为True来调试纹理读取 - - print(f"🔧 应用{'调试' if debug_texture_mode else '增强'}PBR效果...") - - try: - if debug_texture_mode: - # 使用调试效果:将金属性纹理显示为颜色 - self.world.render_pipeline.set_effect( - node, - "effects/test_metallic_debug.yaml", - { - "normal_mapping": False, - "render_gbuffer": True, - "alpha_testing": False, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 100 - ) - print("✅ 调试效果已应用 - 金属性纹理将显示为蓝黄色变化") - print(" 蓝色=非金属区域, 黄色=金属区域") - print(" 如果看到颜色变化,说明纹理读取正确") - print(" 如果看到统一颜色,说明纹理没有正确绑定") - else: - # 使用专门的金属性效果,避免与漫反射贴图混淆 - self.world.render_pipeline.set_effect( - node, - "effects/metallic_only.yaml", - { - "normal_mapping": False, # 关闭法线贴图避免干扰 - "render_gbuffer": True, # 必须启用gbuffer渲染 - "alpha_testing": False, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 100 # 高优先级确保应用 - ) - print("✅ 专门的金属性贴图效果已应用") - - # 验证效果是否正确应用 - current_effect = node.getEffect() - if current_effect: - print(f"🔍 当前效果: {current_effect}") - else: - print("⚠️ 效果应用后为空,可能有问题") - - except Exception as e: - print(f"⚠️ 专门的金属性效果失败: {e}") - print("🔄 回退到直接控制金属性效果...") - # 回退到直接控制效果 - try: - self._ensurePBREffectEnabledWithDirectMetallic(node) - print("✅ 直接控制金属性效果已应用") - except Exception as e2: - print(f"⚠️ 直接控制效果也失败: {e2}") - # 最后的备选方案:使用稳定效果 - try: - self._ensurePBREffectEnabledStable(node) - print("🔄 已应用稳定PBR效果") - except: - pass - - # 创建金属性贴图阶段,对应p3d_Texture5 - print("🎨 创建金属性纹理阶段...") - metallic_stage = TextureStage("metallic_map") - metallic_stage.setSort(5) # 对应shader中的p3d_Texture5 - - # 重要:设置正确的纹理模式 - # MModulate模式用于金属性贴图,不会影响颜色 - metallic_stage.setMode(TextureStage.MModulate) - - # 确保纹理坐标正确 - metallic_stage.setTexcoordName("texcoord") - - print(f"📋 金属性纹理阶段信息:") - print(f" • 名称: {metallic_stage.getName()}") - print(f" • 排序: {metallic_stage.getSort()} (对应p3d_Texture5)") - print(f" • 模式: {metallic_stage.getMode()} (MReplace)") - print(f" • 纹理坐标: {metallic_stage.getTexcoordName()}") - - # 应用纹理到正确的纹理槽 - node.setTexture(metallic_stage, texture) - print(f"🔗 金属性纹理已绑定到p3d_Texture5槽") - - # 强制刷新渲染状态 - node.setRenderModeWireframe() - node.clearRenderMode() - print(f"🔄 已刷新渲染状态") - - # 验证纹理应用 - applied_texture = node.getTexture(metallic_stage) - if applied_texture: - print(f"✅ 金属性贴图成功应用到p3d_Texture5槽") - print(f"📊 纹理信息:") - print(f" • 纹理名称: {applied_texture.getName()}") - print(f" • 纹理尺寸: {applied_texture.getXSize()}x{applied_texture.getYSize()}") - print(f"📊 Blender风格效果:") - print(f" • 白色区域 = 完全金属 (1.0)") - print(f" • 黑色区域 = 非金属 (0.0)") - print(f" • 灰色区域 = 部分金属 (0.5)") - print(f" • 公式: 最终金属性 = 贴图值") - - # 列出节点上的所有纹理阶段 - print(f"🔍 节点上的所有纹理阶段:") - all_stages = node.findAllTextureStages() - for i, stage in enumerate(all_stages): - tex = node.getTexture(stage) - tex_name = tex.getName() if tex else "无纹理" - print(f" {i}: {stage.getName()} (sort={stage.getSort()}) -> {tex_name}") - - else: - print("❌ 纹理应用验证失败") - print("🔍 尝试诊断问题...") - - # 检查纹理是否有效 - if texture: - print(f" 纹理对象有效: {texture.getName()}") - else: - print(" 纹理对象无效") - - # 检查节点是否有效 - if node: - print(f" 节点对象有效: {node.getName()}") - else: - print(" 节点对象无效") - - except Exception as e: - print(f"❌ 应用金属性贴图失败: {e}") - import traceback - traceback.print_exc() - - def _applyIORTexture(self, material, texture_path): - """应用IOR贴图到特定材质""" - try: - from RenderPipelineFile.rpcore.loader import RPLoader - from panda3d.core import TextureStage - - texture = RPLoader.load_texture(texture_path) - if texture: - # 查找使用该材质的具体几何节点 - node = self._findSpecificGeomNodeForMaterial(material) - if node: - print(f"正在为节点 {node.getName()} 应用IOR贴图") - - # 确保启用PBR效果 - self._ensurePBREffectEnabled(node) - - # 根据RenderPipeline的gbuffer.frag.glsl模板: - # p3d_Texture2 用于IOR贴图 (line 87: texture(p3d_Texture2, texcoord).x) - - # 清理现有的IOR贴图 - existing_stages = node.findAllTextureStages() - for stage in existing_stages: - if "ior" in stage.getName().lower() or stage.getSort() == 2: - node.clearTexture(stage) - print(f"清理了现有的IOR贴图阶段: {stage.getName()}") - - # 创建IOR贴图纹理阶段,对应p3d_Texture2 - ior_stage = TextureStage("ior") - ior_stage.setSort(2) # 对应p3d_Texture2 - ior_stage.setMode(TextureStage.MModulate) - - node.setTexture(ior_stage, texture) - print("IOR贴图已应用到p3d_Texture2槽") - - # 不再需要手动刷新渲染状态,避免闪烁 - print(f"IOR贴图已成功应用:{texture_path}") - else: - print("未找到材质对应的节点") - except Exception as e: - print(f"应用IOR贴图失败:{e}") - import traceback - traceback.print_exc() - - def _applyParallaxTexture(self, material, texture_path): - """应用视差贴图""" - try: - from RenderPipelineFile.rpcore.loader import RPLoader - from panda3d.core import TextureStage - - texture = RPLoader.load_texture(texture_path) - if texture: - node = self._findNodeWithMaterial(material) - if node: - print(f"正在为节点 {node.getName()} 应用视差贴图") - - # 确保启用PBR效果,包括视差映射 - self._ensurePBREffectEnabledWithParallax(node) - - # 根据RenderPipeline的gbuffer.frag.glsl模板: - # p3d_Texture4 用于视差贴图 (line 77: get_parallax_texcoord(p3d_Texture4, mInput.normalfactor)) - - # 清理现有的视差贴图 - existing_stages = node.findAllTextureStages() - for stage in existing_stages: - if "parallax" in stage.getName().lower() or stage.getSort() == 4: - node.clearTexture(stage) - print(f"清理了现有的视差贴图阶段: {stage.getName()}") - - # 创建视差贴图纹理阶段,对应p3d_Texture4 - parallax_stage = TextureStage("parallax") - parallax_stage.setSort(4) # 对应p3d_Texture4 - parallax_stage.setMode(TextureStage.MHeight) # 高度贴图模式 - - node.setTexture(parallax_stage, texture) - print("视差贴图已应用到p3d_Texture4槽") - - print(f"视差贴图已成功应用:{texture_path}") - else: - print("未找到材质对应节点") - except Exception as e: - print(f"应用视差贴图失败:{e}") - import traceback - traceback.print_exc() - - def _ensureNormalMappingEnabled(self, model): - """确保模型启用了法线映射功能""" - try: - self.world.render_pipeline.set_effect( - model, - "effects/default.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": True - }, - 30 - ) - print(f"已为模型{model.getName()}启用法线映射") - except Exception as e: - print(f"设置法线映射效果失败:{e}") - - def _ensurePBREffectEnabled(self, model): - """确保模型启用了完整的PBR效果,包括法线映射""" - try: - self.world.render_pipeline.set_effect( - model, - "effects/default.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": True, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 30 - ) - print(f"已为模型{model.getName()}启用PBR效果") - except Exception as e: - print(f"设置PBR效果失败:{e}") - - def _ensurePBREffectEnabledWithParallax(self, model): - """确保模型启用了完整的PBR效果,包括视差映射""" - try: - self.world.render_pipeline.set_effect( - model, - "effects/default.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": True, - "parallax_mapping": True, # 启用视差映射 - "render_shadow": True, - "render_envmap": True - }, - 30 - ) - print(f"已为模型{model.getName()}启用PBR效果(包括视差映射)") - except Exception as e: - print(f"设置PBR效果失败:{e}") - - def _ensurePBREffectEnabledWithMetallic(self, model): - """确保模型启用了支持金属性贴图的PBR效果""" - try: - # 首先尝试使用自定义的金属性贴图效果 - try: - self.world.render_pipeline.set_effect( - model, - "effects/pbr_with_metallic.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": True, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 30 - ) - print(f"已为模型{model.getName()}启用支持金属性贴图的PBR效果") - except Exception as e1: - print(f"自定义金属性效果失败,使用标准PBR效果: {e1}") - # 回退到标准PBR效果 - self._ensurePBREffectEnabled(model) - except Exception as e: - print(f"设置PBR效果失败:{e}") - - def _ensurePBREffectEnabledWithDirectMetallic(self, model): - """确保模型启用了支持金属性贴图直接控制的PBR效果""" - try: - # 首先尝试使用直接控制金属性贴图的效果 - try: - self.world.render_pipeline.set_effect( - model, - "effects/pbr_direct_metallic.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": True, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 30 - ) - print(f"✅ 已为模型{model.getName()}启用直接控制金属性贴图的PBR效果") - except Exception as e1: - print(f"⚠️ 直接控制金属性效果失败,使用标准金属性效果: {e1}") - # 回退到标准金属性贴图效果 - self._ensurePBREffectEnabledWithMetallic(model) - except Exception as e: - print(f"设置PBR效果失败:{e}") - - def _onMetallicModeChanged(self, material, mode_index): - """处理金属性控制模式变化""" - try: - mode_names = ["乘法模式", "直接控制模式", "叠加模式"] - print(f"🔧 金属性控制模式已切换为: {mode_names[mode_index]}") - - # 查找使用该材质的节点 - node = self._findSpecificGeomNodeForMaterial(material) - if node: - if mode_index == 0: - # 乘法模式:使用标准金属性贴图效果 - self._ensurePBREffectEnabledWithMetallic(node) - print(" 📊 效果:最终金属性 = 材质金属性 × 贴图值") - print(" 💡 适用于:微调现有材质的金属性分布") - elif mode_index == 1: - # 直接控制模式:使用直接控制效果 - self._ensurePBREffectEnabledWithDirectMetallic(node) - print(" 📊 效果:最终金属性 = 贴图值") - print(" 💡 适用于:贴图完全控制金属性分布") - elif mode_index == 2: - # 叠加模式:使用叠加效果 - self._ensurePBREffectEnabledWithAdditiveMetallic(node) - print(" 📊 效果:最终金属性 = 材质金属性 + 贴图值 (限制在0-1)") - print(" 💡 适用于:在材质基础上增加金属性区域") - - print(f"✅ 金属性控制模式已应用到节点: {node.getName()}") - else: - print("⚠️ 未找到材质对应的节点") - - except Exception as e: - print(f"切换金属性控制模式失败: {e}") - - def _ensurePBREffectEnabledWithAdditiveMetallic(self, model): - """确保模型启用了支持金属性贴图叠加控制的PBR效果""" - try: - # 首先尝试使用叠加控制金属性贴图的效果 - try: - self.world.render_pipeline.set_effect( - model, - "effects/pbr_additive_metallic.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": True, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 30 - ) - print(f"✅ 已为模型{model.getName()}启用叠加控制金属性贴图的PBR效果") - except Exception as e1: - print(f"⚠️ 叠加控制金属性效果失败,使用直接控制效果: {e1}") - # 回退到直接控制金属性贴图效果 - self._ensurePBREffectEnabledWithDirectMetallic(model) - except Exception as e: - print(f"设置PBR效果失败:{e}") - - def _ensurePBREffectEnabledWithEmission(self, model): - """确保模型启用了支持自发光贴图的PBR效果""" - try: - self.world.render_pipeline.set_effect( - model, - "effects/pbr_with_emission.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": True, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 30 - ) - print(f"已为模型{model.getName()}启用支持自发光贴图的PBR效果") - except Exception as e: - print(f"自定义自发光效果失败,使用标准PBR效果: {e}") - # 回退到标准PBR效果 - self._ensurePBREffectEnabled(model) - - def _ensurePBREffectEnabledWithAlpha(self, model): - """确保模型启用了支持透明度的PBR效果""" - try: - self.world.render_pipeline.set_effect( - model, - "effects/default.yaml", - { - "normal_mapping": True, - "render_gbuffer": False, # 透明物体不渲染到GBuffer - "render_forward": True, # 使用前向渲染 - "alpha_testing": True, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 30 - ) - print(f"已为模型{model.getName()}启用支持透明度的PBR效果") - except Exception as e: - print(f"设置透明度PBR效果失败: {e}") - # 回退到标准PBR效果 - self._ensurePBREffectEnabled(model) - - def _ensurePBREffectEnabledWithRoughness(self, model): - """确保模型启用了支持粗糙度贴图的PBR效果""" - try: - # 首先尝试使用自定义的粗糙度贴图效果 - try: - self.world.render_pipeline.set_effect( - model, - "effects/pbr_with_roughness.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": True, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 30 - ) - print(f"已为模型{model.getName()}启用支持粗糙度贴图的PBR效果") - except Exception as e1: - print(f"自定义粗糙度效果失败,使用标准PBR效果: {e1}") - # 回退到标准PBR效果 - self._ensurePBREffectEnabled(model) - except Exception as e: - print(f"设置PBR效果失败:{e}") - - def _ensurePBREffectEnabledStable(self, model): - """确保模型启用了稳定的PBR效果,避免频繁切换导致闪烁""" - try: - # 检查是否已经有PBR效果 - current_effect = model.getEffect() - if current_effect: - print(f"🔍 当前效果: {current_effect}") - # 如果已经有PBR相关效果,就不要切换了 - effect_name = str(current_effect) - if "pbr" in effect_name.lower() or "default" in effect_name.lower(): - print("✅ 已有PBR效果,保持不变避免闪烁") - return - - # 使用最稳定的默认PBR效果 - print("🔧 应用稳定的默认PBR效果...") - self.world.render_pipeline.set_effect( - model, - "effects/default.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": False, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 50 # 较高优先级 - ) - print(f"✅ 稳定PBR效果已应用到模型: {model.getName()}") - - except Exception as e: - print(f"⚠️ 设置稳定PBR效果失败: {e}") - # 最后的备选方案:清除所有效果 - try: - model.clearEffect() - print("🔄 已清除所有效果,使用默认渲染") - except: - pass - - def _applyEmissionTexture(self, material, texture_path): - """应用自发光贴图""" - try: - from RenderPipelineFile.rpcore.loader import RPLoader - from panda3d.core import TextureStage - - texture = RPLoader.load_texture(texture_path) - if texture: - node = self._findNodeWithMaterial(material) - if node: - print(f"正在为节点 {node.getName()} 应用自发光贴图") - - # 启用自发光效果 - self._ensurePBREffectEnabledWithEmission(node) - - # 清理现有的自发光贴图 - existing_stages = node.findAllTextureStages() - for stage in existing_stages: - if "emission" in stage.getName().lower() or stage.getSort() == 6: - node.clearTexture(stage) - print(f"清理了现有的自发光贴图阶段: {stage.getName()}") - - # 创建自发光贴图纹理阶段,对应p3d_Texture6 - emission_stage = TextureStage("emission") - emission_stage.setSort(6) # 对应p3d_Texture6 - emission_stage.setMode(TextureStage.MModulate) - - node.setTexture(emission_stage, texture) - print("自发光贴图已应用到p3d_Texture6槽") - - # 设置材质为自发光着色模型 - from panda3d.core import Vec4 - current_emission = material.emission - if current_emission is None: - current_emission = Vec4(0, 0, 0, 0) - - # emission.x 用于存储着色模型,1表示自发光 - new_emission = Vec4(1.0, current_emission.y, current_emission.z, current_emission.w) - material.set_emission(new_emission) - print("材质着色模型已设置为自发光") - - print(f"自发光贴图已成功应用:{texture_path}") - else: - print("未找到材质对应的节点") - except Exception as e: - print(f"应用自发光贴图失败:{e}") - import traceback - traceback.print_exc() - - def _applyAOTexture(self, material, texture_path): - """应用环境光遮蔽贴图""" - try: - from RenderPipelineFile.rpcore.loader import RPLoader - from panda3d.core import TextureStage - - texture = RPLoader.load_texture(texture_path) - if texture: - node = self._findNodeWithMaterial(material) - if node: - print(f"正在为节点 {node.getName()} 应用AO贴图") - - # 确保启用PBR效果 - self._ensurePBREffectEnabled(node) - - # 清理现有的AO贴图 - existing_stages = node.findAllTextureStages() - for stage in existing_stages: - if "ao" in stage.getName().lower() or stage.getSort() == 7: - node.clearTexture(stage) - print(f"清理了现有的AO贴图阶段: {stage.getName()}") - - # 创建AO贴图纹理阶段,对应p3d_Texture7 - ao_stage = TextureStage("ao") - ao_stage.setSort(7) # 对应p3d_Texture7 - ao_stage.setMode(TextureStage.MModulate) - - node.setTexture(ao_stage, texture) - print("AO贴图已应用到p3d_Texture7槽") - print("注意:AO贴图需要自定义shader支持才能正确显示") - - print(f"AO贴图已成功应用:{texture_path}") - else: - print("未找到材质对应的节点") - except Exception as e: - print(f"应用AO贴图失败:{e}") - import traceback - traceback.print_exc() - - def _applyAlphaTexture(self, material, texture_path): - """应用透明度贴图""" - try: - from RenderPipelineFile.rpcore.loader import RPLoader - from panda3d.core import TextureStage - - texture = RPLoader.load_texture(texture_path) - if texture: - node = self._findNodeWithMaterial(material) - if node: - print(f"正在为节点 {node.getName()} 应用透明度贴图") - - # 启用透明度测试的PBR效果 - self._ensurePBREffectEnabledWithAlpha(node) - - # 清理现有的透明度贴图 - existing_stages = node.findAllTextureStages() - for stage in existing_stages: - if "alpha" in stage.getName().lower() or stage.getSort() == 8: - node.clearTexture(stage) - print(f"清理了现有的透明度贴图阶段: {stage.getName()}") - - # 创建透明度贴图纹理阶段,对应p3d_Texture8 - alpha_stage = TextureStage("alpha") - alpha_stage.setSort(8) # 对应p3d_Texture8 - alpha_stage.setMode(TextureStage.MModulate) - - node.setTexture(alpha_stage, texture) - print("透明度贴图已应用到p3d_Texture8槽") - - # 设置材质为透明着色模型 - from panda3d.core import Vec4 - current_emission = material.emission - if current_emission is None: - current_emission = Vec4(0, 0, 0, 0) - - # emission.x 用于存储着色模型,3表示透明 - new_emission = Vec4(3.0, current_emission.y, current_emission.z, current_emission.w) - material.set_emission(new_emission) - print("材质着色模型已设置为透明") - - print(f"透明度贴图已成功应用:{texture_path}") - else: - print("未找到材质对应的节点") - except Exception as e: - print(f"应用透明度贴图失败:{e}") - import traceback - traceback.print_exc() - - def _applyDetailTexture(self, material, texture_path): - """应用细节贴图""" - try: - from RenderPipelineFile.rpcore.loader import RPLoader - from panda3d.core import TextureStage - - texture = RPLoader.load_texture(texture_path) - if texture: - node = self._findNodeWithMaterial(material) - if node: - print(f"正在为节点 {node.getName()} 应用细节贴图") - - # 确保启用PBR效果 - self._ensurePBREffectEnabled(node) - - # 清理现有的细节贴图 - existing_stages = node.findAllTextureStages() - for stage in existing_stages: - if "detail" in stage.getName().lower() or stage.getSort() == 9: - node.clearTexture(stage) - print(f"清理了现有的细节贴图阶段: {stage.getName()}") - - # 创建细节贴图纹理阶段,对应p3d_Texture9 - detail_stage = TextureStage("detail") - detail_stage.setSort(9) # 对应p3d_Texture9 - detail_stage.setMode(TextureStage.MModulate) - - node.setTexture(detail_stage, texture) - print("细节贴图已应用到p3d_Texture9槽") - print("注意:细节贴图需要自定义shader支持才能正确显示") - - print(f"细节贴图已成功应用:{texture_path}") - else: - print("未找到材质对应的节点") - except Exception as e: - print(f"应用细节贴图失败:{e}") - import traceback - traceback.print_exc() - - def _applyGlossTexture(self, material, texture_path): - """应用光泽贴图""" - try: - from RenderPipelineFile.rpcore.loader import RPLoader - from panda3d.core import TextureStage - - texture = RPLoader.load_texture(texture_path) - if texture: - node = self._findNodeWithMaterial(material) - if node: - print(f"正在为节点 {node.getName()} 应用光泽贴图") - - # 确保启用PBR效果 - self._ensurePBREffectEnabled(node) - - # 清理现有的光泽贴图 - existing_stages = node.findAllTextureStages() - for stage in existing_stages: - if "gloss" in stage.getName().lower() or stage.getSort() == 10: - node.clearTexture(stage) - print(f"清理了现有的光泽贴图阶段: {stage.getName()}") - - # 创建光泽贴图纹理阶段,对应p3d_Texture10 - gloss_stage = TextureStage("gloss") - gloss_stage.setSort(10) # 对应p3d_Texture10 - gloss_stage.setMode(TextureStage.MGloss) # 光泽模式 - - node.setTexture(gloss_stage, texture) - print("光泽贴图已应用到p3d_Texture10槽") - print("注意:光泽贴图需要自定义shader支持才能正确显示") - - print(f"光泽贴图已成功应用:{texture_path}") - else: - print("未找到材质对应的节点") - except Exception as e: - print(f"应用光泽贴图失败:{e}") - import traceback - traceback.print_exc() - - def _clearConflictingTextureStages(self, node): - """清理可能冲突的纹理阶段""" - try: - from panda3d.core import TextureStage - - # 获取所有纹理阶段 - texture_stages = node.findAllTextureStages() - - # 检查是否有冲突的纹理阶段 - stages_to_clear = [] - for stage in texture_stages: - stage_name = stage.getName() - # 如果发现未命名或冲突的阶段,标记清理 - if stage_name == "" or stage == TextureStage.getDefault(): - # 检查是否有法线贴图在默认阶段 - texture = node.getTexture(stage) - if texture and "normal" in texture.getName().lower(): - stages_to_clear.append(stage) - - # 清理冲突的阶段 - for stage in stages_to_clear: - node.clearTexture(stage) - print(f"清理了冲突的纹理阶段: {stage.getName()}") - - except Exception as e: - print(f"清理纹理阶段时出错: {e}") - - def _findNodeWithMaterial(self, target_material): - """查找使用指定材质的节点""" - # 这里需要根据你的场景结构来实现 - # 遍历场景中的所有节点,找到使用该材质的节点 - # for model in self.world.scene_manager.models: - # materials = model.find_all_materials() - # if target_material in materials: - # return model - - """查找使用指定材质的节点""" - # 首先尝试在当前选中的模型中查找 - current_item = self.world.treeWidget.currentItem() - if current_item: - current_model = current_item.data(0, Qt.UserRole) - if current_model: - materials = current_model.find_all_materials() - if target_material in materials: - return current_model - - # 如果在当前选中模型中没找到,再遍历所有模型 - for model in self.world.scene_manager.models: - materials = model.find_all_materials() - if target_material in materials: - return model - return None - - def _findMaterialAndNodeByTitle(self, material_title): - """根据材质标题查找对应的材质和节点""" - - current_item = self.world.treeWidget.currentItem() - if not current_item: - print("未找到当前选中项") - return None, None - - current_model = current_item.data(0, Qt.UserRole) - if not current_model: - print("未找到当前模型") - return None, None - - materials = current_model.find_all_materials() - model_name = current_model.getName() or "未命名模型" - print(f"模型名称: '{model_name}', 材质数量: {len(materials)}") - - name_counter = {} - for i, material in enumerate(materials): - material_name = material.get_name() if hasattr(material, - 'get_name') and material.get_name() else f"材质{i + 1}" - base_name = f"{material_name}({model_name})" - - if base_name in name_counter: - name_counter[base_name] += 1 - unique_name = f"{base_name}_{name_counter[base_name]}" - else: - name_counter[base_name] = 1 - unique_name = base_name - - print(f"材质 {i}: 生成标题='{unique_name}'") - - if unique_name == material_title: - print(f"找到匹配的材质!") - geom_node = self._findSpecificGeomNodeWithMaterial(current_model, material) - if geom_node: - print(f"找到几何节点: {geom_node.get_name()}") - return material, geom_node - else: - print("未找到对应的几何节点,使用模型节点") - return material, current_model - - print("未找到匹配的材质标题") - return None, None - - def _findSpecificGeomNodeWithMaterial(self, model, target_material): - """查找使用指定材质的具体几何节点""" - from panda3d.core import MaterialAttrib, GeomNode - - # print(f"查找材质: {target_material.get_name() if hasattr(target_material, 'get_name') else 'unnamed'}") - - # 首先尝试查找GeomNode - geom_nodes = model.find_all_matches("**/+GeomNode") - # print(f"找到 {len(geom_nodes)} 个几何节点") - - # 如果没有找到GeomNode,尝试查找所有子节点 - if len(geom_nodes) == 0: - # print("未找到GeomNode,尝试查找所有子节点...") - all_nodes = model.find_all_matches("**") - # print(f"找到 {len(all_nodes)} 个子节点") - - for node_np in all_nodes: - node = node_np.node() - if isinstance(node, GeomNode): - geom_nodes.append(node_np) - print(f"找到GeomNode: {node_np.getName()}") - - for geom_np in geom_nodes: - geom_node = geom_np.node() - geom_count = geom_node.get_num_geoms() - # rint(f"检查几何节点 {geom_node.get_name()}: {geom_count} 个几何体") - - for i in range(geom_count): - state = geom_node.get_geom_state(i) - if state.has_attrib(MaterialAttrib): - material = state.get_attrib(MaterialAttrib).get_material() - if material == target_material: - # print(f"找到匹配的几何节点: {geom_np.get_name()}") - return geom_np - - print("未找到匹配的几何节点") - return None - - def _findSpecificGeomNodeForMaterial(self, target_material): - """查找使用指定材质的具体几何节点(统一方法)""" - material_name = target_material.get_name() if hasattr(target_material, 'get_name') else 'unnamed' - print(f"查找材质对应的几何节点: {material_name}") - - # 优先使用存储的几何节点映射 - material_id = id(target_material) - if hasattr(self, '_material_geom_mapping') and material_id in self._material_geom_mapping: - geom_node = self._material_geom_mapping[material_id] - if geom_node: # 确保节点仍然有效 - print(f"✓ 使用存储的几何节点映射: {geom_node.getName()}") - return geom_node - - # 如果没有存储的映射,使用传统查找方法 - current_item = self.world.treeWidget.currentItem() - if current_item: - current_model = current_item.data(0, Qt.UserRole) - if current_model: - # 使用现有的精确查找方法 - geom_node = self._findSpecificGeomNodeWithMaterial(current_model, target_material) - if geom_node: - # print(f"✓ 找到特定几何节点: {geom_node.getName()}") - # 存储映射以供后续使用 - if not hasattr(self, '_material_geom_mapping'): - self._material_geom_mapping = {} - self._material_geom_mapping[material_id] = geom_node - return geom_node - else: - # print("⚠️ 未找到特定几何节点,使用模型节点(可能影响所有材质)") - return current_model - - print("❌ 未找到当前选中的模型") - return None - - def _findGeomNodeWithMaterial(self, model, target_material): - """查找使用指定材质的具体几何节点""" - from panda3d.core import MaterialAttrib - - # print(f"查找材质: {target_material.get_name() if hasattr(target_material, 'get_name') else 'unnamed'}") - - # 遍历模型下的所有几何节点 - geom_nodes = model.find_all_matches("**/+GeomNode") - # print(f"找到 {len(geom_nodes)} 个几何节点") - - for geom_np in geom_nodes: - geom_node = geom_np.node() - geom_count = geom_node.get_num_geoms() - # print(f"几何节点 {geom_node.get_name()}: {geom_count} 个几何体") - - for i in range(geom_count): - state = geom_node.get_geom_state(i) - if state.has_attrib(MaterialAttrib): - material = state.get_attrib(MaterialAttrib).get_material() - if material == target_material: - # print(f"找到匹配的几何节点: {geom_np.get_name()}") - return geom_np - else: - print(f"几何体 {i} 没有材质属性") - - print("未找到匹配的几何节点") - return None - - def _displayCurrentTextures(self, material, material_layout, current_row): - """显示当前材质的贴图信息""" - node = self._findNodeWithMaterial(material) - - # 当前贴图信息标题 - current_row += 1 - texture_info_title = QLabel("当前贴图信息") - texture_info_title.setStyleSheet("color: #666; font-weight: bold; font-size: 10px; margin-top: 5px;") - material_layout.addWidget(texture_info_title, current_row, 0, 1, 4) - current_row += 1 - - if node: - # 显示当前应用的纹理信息 - texture = node.getTexture() - if texture: - texture_name = texture.getName() or "未命名贴图" - texture_info = QLabel(f"当前贴图: {texture_name}") - texture_info.setStyleSheet("color: #666; font-size: 9px;") - material_layout.addWidget(texture_info, current_row, 0, 1, 2) - else: - no_texture_info = QLabel("当前贴图: 无") - no_texture_info.setStyleSheet("color: #888; font-size: 9px; font-style: italic;") - material_layout.addWidget(no_texture_info, current_row, 0, 1, 2) - else: - no_node_info = QLabel("无法获取贴图信息") - no_node_info.setStyleSheet("color: #888; font-size: 9px; font-style: italic;") - material_layout.addWidget(no_node_info, current_row, 0, 1, 4) - - return current_row + 1 - - def _applyToAllMaterials(self, model, property_name, value): - """将属性应用到模型的所有材质""" - materials = model.find_all_materials() - for material in materials: - if property_name == "base_color": - material.set_base_color(value) - elif property_name == "roughness": - material.set_roughness(value) - elif property_name == "metallic": - material.set_metallic(value) - elif property_name == "ior": - material.set_refractive_index(value) - - def _addShadingModelPanel(self, material, material_layout, current_row): - """添加着色模型选择面板""" - from PyQt5.QtWidgets import QComboBox - - # RenderPipeline 支持的着色模型 - SHADING_MODELS = [ - ("默认", 0), - ("自发光", 1), - ("透明涂层", 2), - ("透明", 3), - ("皮肤", 4), - ("植物", 5), - ] - - shading_title = QLabel("着色模型") - shading_title.setStyleSheet("font-weight:bold;") - material_layout.addWidget(shading_title, current_row, 0, 1, 4) - current_row += 1 - - material_layout.addWidget(QLabel("着色模型:"), current_row, 0) - - shading_combo = QComboBox() - for name, value in SHADING_MODELS: - shading_combo.addItem(name) - - # 安全地获取当前着色模型 - current_model = 0 # 默认值 - try: - if hasattr(material, 'emission') and material.emission is not None: - current_model = int(material.emission.x) - except (AttributeError, TypeError, ValueError): - current_model = 0 - - shading_combo.setCurrentIndex(current_model) - - shading_combo.currentIndexChanged.connect( - lambda idx: self._onShadingModelChanged(material, idx) - ) - material_layout.addWidget(shading_combo, current_row, 1, 1, 3) - current_row += 1 - - # 如果是透明着色模型,添加透明度控制 - try: - if hasattr(material, 'emission') and material.emission is not None and int(material.emission.x) == 3: - current_row = self._addTransparencyPanel(material, material_layout, current_row) - except Exception as e: - print(f"添加透明度面板时出错: {e}") - - return current_row - - def _onShadingModelChanged(self, material, model_index): - """处理着色模型变化""" - print(f"着色模型变化: {model_index}") - - # 更新着色模型 - self._updateShadingModel(material, model_index) - - # 如果切换到透明模式,立即添加透明度面板 - if model_index == 3: - print("切换到透明模式,添加透明度面板...") - # 延迟一点时间让UI更新完成 - from PyQt5.QtCore import QTimer - QTimer.singleShot(100, lambda: self._refreshMaterialUI()) - # QTimer.singleShot(100, lambda: self._addTransparencyPanel(material)) - - def _updateShadingModel(self, material, model_index): - """更新着色模型""" - from panda3d.core import Vec4 - - # 安全地获取当前 emission 值 - current_emission = Vec4(0, 1.0, 0, 0) - if hasattr(material, 'emission') and material.emission is not None: - current_emission = material.emission - - # 根据不同的着色模型设置相应的参数 - if model_index == 1: # 自发光模式 - default_emission_strength = 2.0 if current_emission.z == 0 else current_emission.z - new_emission = Vec4(float(model_index), current_emission.y, default_emission_strength, current_emission.w) - elif model_index == 3: # 透明模式 - print("设置透明着色模型...") - - # 设置默认透明度值 - default_opacity = 0.5 # 设置为较高的值,确保模型可见 - - # 同时在emission.y和base_color.w中设置透明度值 - # emission.y可能被RenderPipeline的某些部分使用 - new_emission = Vec4(float(model_index), default_opacity, current_emission.z, current_emission.w) - - # 透明度通过基础颜色的Alpha通道控制 - self._updateMaterialAlphaForTransparency(material, default_opacity) - - # 应用透明渲染效果 - # self._applyTransparentRenderingEffect() - - print(f"透明着色模型设置完成") - print(f" - emission.x = {model_index} (透明着色模型)") - print(f" - emission.y = {default_opacity} (可能的透明度参数)") - print(f" - base_color.w = {default_opacity} (Alpha透明度)") - else: - new_emission = Vec4(float(model_index), current_emission.y, current_emission.z, current_emission.w) - - material.set_emission(new_emission) - - # 刷新UI以更新相关控件的值 - if model_index in [1, 3]: # 自发光或透明模式 - self._refreshMaterialUI() - - print( - f"着色模型已更新为: {model_index} ({'自发光' if model_index == 1 else '透明' if model_index == 3 else '默认'})") - - def _addTransparencyPanel(self, material, material_layout, current_row): - """添加透明度控制面板""" - transparency_title = QLabel("透明度属性") - transparency_title.setStyleSheet("color: #00BFFF; font-weight:bold;") - material_layout.addWidget(transparency_title, current_row, 0, 1, 4) - current_row += 1 - - # 不透明度滑块(避免混淆,使用不透明度) - material_layout.addWidget(QLabel("不透明度:"), current_row, 0) - opacity_spinbox = QDoubleSpinBox() - opacity_spinbox.setRange(0.0, 1.0) # 最小值0.1,避免完全消失 - opacity_spinbox.setSingleStep(0.01) - - # 安全地获取当前不透明度值(从基础颜色的Alpha通道) - current_opacity = 0.3 # 默认值 - try: - base_color = self._getOrCreateMaterialBaseColor(material) - if base_color is not None: - current_opacity = base_color.w - except Exception as e: - print(f"获取当前透明度失败,使用默认值: {e}") - - opacity_spinbox.setValue(current_opacity) - opacity_spinbox.valueChanged.connect(lambda v: self._updateTransparency(material, v)) - material_layout.addWidget(opacity_spinbox, current_row, 1, 1, 3) - current_row += 1 - - return current_row - - def _updateTransparency(self, material, opacity_value): - """更新不透明度值(同时更新emission.y和base_color.w)""" - try: - from panda3d.core import Vec4 - current_emission = material.emission or Vec4(0, 0, 0, 0) - new_emission = Vec4(current_emission.x, opacity_value, current_emission.z, current_emission.w) - material.set_emission(new_emission) - - # 更新基础颜色的Alpha通道 - self._updateMaterialAlphaForTransparency(material, opacity_value) - - print(f"透明度已更新:") - print(f" - emission.y = {opacity_value}") - print(f" - base_color.w = {opacity_value}") - print(f" - 视觉透明度 = {1.0 - opacity_value:.2f}") - - except Exception as e: - print(f"更新透明度失败: {e}") - - def _updateMaterialAlphaForTransparency(self, material, opacity_slider): - """ - opacity_slider: 0=全透 1=不透 - shader 需要 1-opacity_slider 作为真正的 alpha - """ - from panda3d.core import Vec4, TransparencyAttrib - - current_item = self.world.treeWidget.currentItem() - if not current_item: - return - model = current_item.data(0, Qt.UserRole) - if model.isEmpty(): - return - - model.setTransparency(TransparencyAttrib.MAlpha) - model.setTwoSided(True) - model.setDepthWrite(False) - - alpha = opacity_slider # 反转 - color = self._getOrCreateMaterialBaseColor(material) or Vec4(1, 1, 1, 1) - material.base_color = Vec4(color.x, color.y, color.z, alpha) - material.base_color = Vec4(color.x, color.y, color.z, alpha) - - em = material.emission or Vec4(0, 0, 0, 0) - material.set_emission(Vec4(3.0, alpha, em.z, em.w)) - - self.world.render_pipeline.set_effect( - model, - "effects/simple_transparent.yaml", - options={}, - sort=200 - ) - self.world.render_pipeline.prepare_scene(model) - print(f"[透明] 不透明度={opacity_slider:.2f} 已同步") - - def _applyTransparentRenderingEffect(self): - from panda3d.core import TransparencyAttrib - """为当前选中的模型应用透明渲染效果(简化版本)""" - try: - current_item = self.world.treeWidget.currentItem() - if current_item: - model = current_item.data(0, Qt.UserRole) - if model: - model.setTransparency(TransparencyAttrib.MAlpha) - - self.world.render_pipeline.set_effect( - model, - "effects/default.yaml", - { - "render_gbuffer": True, - "alpha_testing": False, - "normal_mapping": True, - "render_shadow": True, - "render_envmap": True, - }, - sort=100 - ) - - # 让RenderPipeline自动处理透明材质 - # 当emission.x=3时,RenderPipeline会自动设置正确的渲染参数 - self.world.render_pipeline.prepare_scene(model) - print(" - RenderPipeline自动处理: 已完成") - - print(f"✓ 已为模型 {model.getName()} 应用透明渲染效果") - print(" 注意: 使用简化设置避免渲染冲突") - - except Exception as e: - print(f"✗ 应用透明渲染效果失败: {e}") - import traceback - traceback.print_exc() - - def _addEmissionPanel(self, material, material_layout, current_row): - """添加自发光控制面板""" - emission_title = QLabel("自发光属性") - emission_title.setStyleSheet("font-weight:bold;") - material_layout.addWidget(emission_title, current_row, 0, 1, 4) - current_row += 1 - - # 自发光强度标签和控件 - material_layout.addWidget(QLabel("发光强度:"), current_row, 0) - - emission_spinbox = QDoubleSpinBox() - emission_spinbox.setRange(0.0, 10.0) - emission_spinbox.setSingleStep(0.1) - - # 安全地获取当前自发光强度 - current_emission_z = 0.0 # 默认值 - if hasattr(material, 'emission') and material.emission is not None: - current_emission_z = material.emission.z - - emission_spinbox.setValue(current_emission_z) - emission_spinbox.valueChanged.connect(lambda v: self._updateEmissionStrength(material, v)) - material_layout.addWidget(emission_spinbox, current_row, 1, 1, 3) - current_row += 1 - - return current_row - - def _updateEmissionStrength(self, material, strength): - """更新自发光强度""" - from panda3d.core import Vec4 - - # 安全地获取当前 emission 值 - current_emission = Vec4(0, 0, 0, 0) - if hasattr(material, 'emission') and material.emission is not None: - current_emission = material.emission - - # 更新 emission.z 存储发光强度(用于UI显示) - new_emission = Vec4(current_emission.x, current_emission.y, strength, current_emission.w) - material.set_emission(new_emission) - - # 对于自发光材质,直接调整基础颜色的亮度 - if current_emission.x == 1: # 如果是自发光着色模型 - # 获取原始基础颜色(假设存储在某处,或使用当前值的归一化版本) - base_intensity = 0.5 # 基础亮度 - intensity_multiplier = strength / 5.0 # 将0-10范围映射到合理的倍数 - - # 设置发光颜色 - emissive_color = Vec4( - base_intensity * intensity_multiplier, - base_intensity * intensity_multiplier, - base_intensity * intensity_multiplier, - 1.0 - ) - material.set_base_color(emissive_color) - - print(f"自发光强度已更新为: {strength}") - - def _addMaterialPresetPanel(self, material, material_layout, current_row): - """添加材质预设面板""" - from PyQt5.QtWidgets import QComboBox - - preset_title = QLabel("材质预设") - preset_title.setStyleSheet("font-weight:bold;") - material_layout.addWidget(preset_title, current_row, 0, 1, 4) - current_row += 1 - - # 选择预设标签和控件 - material_layout.addWidget(QLabel("选择预设:"), current_row, 0) - - preset_combo = QComboBox() - preset_combo.addItems([ - "自定义", "塑料", "金属", "玻璃", "橡胶", "木材", "陶瓷", "皮革" - ]) - - # 优先检查存储的预设名称 - if hasattr(material, '_applied_preset'): - preset_combo.setCurrentText(material._applied_preset) - else: - # 安全地检测当前材质最接近的预设 - try: - current_preset = self._detectCurrentPreset(material) - preset_combo.setCurrentText(current_preset) - except Exception as e: - print(f"检测材质预设时出错: {e}") - preset_combo.setCurrentText("自定义") - - preset_combo.currentTextChanged.connect( - lambda preset: self._applyMaterialPreset(material, preset) - ) - material_layout.addWidget(preset_combo, current_row, 1, 1, 3) - current_row += 1 - - return current_row - - def _detectCurrentPreset(self, material): - """检测当前材质最接近的预设""" - # 定义预设的精确匹配条件 - - presets = { - "塑料": {"base_color": (0.8, 0.8, 0.8), "roughness": 0.7, "metallic": 0.0, "ior": 1.4}, - "金属": {"base_color": (0.7, 0.7, 0.7), "roughness": 0.1, "metallic": 1.0, "ior": 1.5}, - "玻璃": {"base_color": (0.9, 0.9, 1.0), "roughness": 0.0, "metallic": 0.0, "ior": 1.5}, - "橡胶": {"base_color": (0.2, 0.2, 0.2), "roughness": 0.9, "metallic": 0.0, "ior": 1.3}, - "自发光": {"base_color": (1.0, 1.0, 1.0), "roughness": 0.5, "metallic": 0.0, "ior": 1.0} - } - - # 容差值,用于浮点数比较 - tolerance = 0.05 - - for preset_name, preset_values in presets.items(): - # 安全检查基础颜色 - base_color_match = False - if hasattr(material, 'base_color') and material.base_color is not None: - try: - base_color_match = ( - abs(material.base_color.x - preset_values["base_color"][0]) < tolerance and - abs(material.base_color.y - preset_values["base_color"][1]) < tolerance and - abs(material.base_color.z - preset_values["base_color"][2]) < tolerance - ) - except (AttributeError, TypeError): - base_color_match = False - - # 安全检查其他属性 - roughness_match = False - if hasattr(material, 'roughness') and material.roughness is not None: - try: - roughness_match = abs(float(material.roughness) - preset_values["roughness"]) < tolerance - except (AttributeError, TypeError, ValueError): - roughness_match = False - - metallic_match = False - if hasattr(material, 'metallic') and material.metallic is not None: - try: - metallic_match = abs(float(material.metallic) - preset_values["metallic"]) < tolerance - except (AttributeError, TypeError, ValueError): - metallic_match = False - - ior_match = False - if hasattr(material, 'refractive_index') and material.refractive_index is not None: - try: - ior_match = abs(float(material.refractive_index) - preset_values["ior"]) < tolerance - except (AttributeError, TypeError, ValueError): - ior_match = False - - # 如果所有属性都匹配,返回预设名称 - if base_color_match and roughness_match and metallic_match and ior_match: - return preset_name - - return "自定义" # 如果没有匹配的预设 - - def _applyMaterialPreset(self, material, preset_name): - """应用材质预设""" - presets = { - "塑料": {"base_color": Vec4(0.8, 0.8, 0.8, 1.0), "roughness": 0.7, "metallic": 0.0, "ior": 1.4}, - "金属": {"base_color": Vec4(0.7, 0.7, 0.7, 1.0), "roughness": 0.1, "metallic": 1.0, "ior": 1.5}, - "玻璃": {"base_color": Vec4(0.9, 0.9, 1.0, 0.2), "roughness": 0.0, "metallic": 0.0, "ior": 1.5, - "shading_model": 3, "transparency": 0.2}, - "橡胶": {"base_color": Vec4(0.2, 0.2, 0.2, 1.0), "roughness": 0.9, "metallic": 0.0, "ior": 1.3}, - "木材": {"base_color": Vec4(0.6, 0.4, 0.2, 1.0), "roughness": 0.8, "metallic": 0.0, "ior": 1.3}, - "陶瓷": {"base_color": Vec4(0.9, 0.9, 0.85, 1.0), "roughness": 0.1, "metallic": 0.0, "ior": 1.6}, - "皮革": {"base_color": Vec4(0.4, 0.3, 0.2, 1.0), "roughness": 0.6, "metallic": 0.0, "ior": 1.4} - } - - if preset_name not in presets: - print(f"未知的材质预设: {preset_name}") - return - - preset = presets[preset_name] - - if "shading_model" in preset: - emission = Vec4(float(preset["shading_model"]), 0, 0, 0) - if "transparency" in preset: - emission.y = preset["transparency"] - material.set_emission(emission) - - print(f"设置着色模型: {preset['shading_model']}") - print(f"材质emission值: {material.emission}") - print(f"基础颜色alpha: {preset['base_color'].w}") - - material.set_base_color(preset["base_color"]) - material.set_roughness(preset["roughness"]) - material.set_metallic(preset["metallic"]) - material.set_refractive_index(preset["ior"]) - - if "shading_model" in preset: - emission = Vec4(float(preset["shading_model"]), 0, 0, 0) - if "transparency" in preset: - emission.y = preset["transparency"] - material.set_emission(emission) - - # 关键:为透明材质应用正确的渲染效果 - if preset["shading_model"] == 3: - self._apply_transparent_effect() - - # material._applied_preset = preset_name - self._refreshMaterialUI() - print(f"已应用材质预设: {preset_name}") - - def _apply_transparent_effect(self): - """为当前选中的模型应用透明渲染效果""" - current_item = self.world.treeWidget.currentItem() - if current_item: - model = current_item.data(0, Qt.UserRole) - if model: - # 只调用 prepare_scene,让它自动处理透明材质 - self.world.render_pipeline.set_effect( - model, - "effects/default.yaml", - { - "render_forward": True, - "render_gbuffer": False, - "normal_mapping": True # 明确启用法线映射 - }, - 100 - ) - self.world.render_pipeline.prepare_scene(model) - print("已重新准备场景以应用透明效果") - - def _refreshMaterialUI(self): - """刷新材质 UI 显示""" - # 重新更新当前选中项的属性面板 - if hasattr(self.world, 'treeWidget') and self.world.treeWidget.currentItem(): - current_item = self.world.treeWidget.currentItem() - # 触发属性面板更新 - self.updatePropertyPanel(current_item) - - def _addBatchOperationsPanel(self, model): - """添加批量操作面板""" - batch_title = QLabel("批量操作") - batch_title.setStyleSheet("color: #E91E63; font-weight:bold;") - self._propertyLayout.addRow(batch_title) - - # 批量设置粗糙度 - batch_roughness_btn = QPushButton("统一粗糙度") - batch_roughness_btn.clicked.connect(lambda: self._batchSetRoughness(model)) - self._propertyLayout.addRow("批量粗糙度:", batch_roughness_btn) - - # 批量设置金属性 - batch_metallic_btn = QPushButton("统一金属性") - batch_metallic_btn.clicked.connect(lambda: self._batchSetMetallic(model)) - self._propertyLayout.addRow("批量金属性:", batch_metallic_btn) - - def _batchSetRoughness(self, model): - """批量设置粗糙度""" - from PyQt5.QtWidgets import QInputDialog - - value, ok = QInputDialog.getDouble(None, "批量设置", "粗糙度值:", 0.5, 0.0, 1.0, 2) - if ok: - self._applyToAllMaterials(model, "roughness", value) - print(f"已将所有材质粗糙度设置为: {value}") - - def _applyMetallicTexture_NEW(self, material, texture_path): - """应用金属性贴图 - 简化重写版本""" - try: - from RenderPipelineFile.rpcore.loader import RPLoader - from panda3d.core import TextureStage - - print(f"🎨 应用金属性贴图: {texture_path}") - - # 1. 加载纹理 - texture = RPLoader.load_texture(texture_path) - if not texture: - print("❌ 纹理加载失败") - return - - # 2. 找到节点 - node = self._findSpecificGeomNodeForMaterial(material) - if not node: - print("❌ 未找到材质对应的节点") - return - - print(f"🎯 目标节点: {node.getName()}") - - # 3. 设置材质金属性为1.0 - # material.set_metallic(1.0) - # print("🔧 材质金属性设置为1.0") - - # # 4. 创建纹理阶段 - # metallic_stage = TextureStage("metallic_map") - # metallic_stage.setSort(5) # p3d_Texture5 - # metallic_stage.setMode(TextureStage.MModulate) - # - # # 5. 绑定纹理 - # node.setTexture(metallic_stage, texture) - # print("🔗 纹理已绑定到p3d_Texture5槽") - - # 6. 应用金属性PBR效果,检查是否需要保持法线映射 - has_normal = self._hasNormalTexture(node) - - if not has_normal: - print("⚠️ 检测到材质没有法线贴图,先添加默认法线贴图...") - # self._applyDefaultNormalTexture(node) - self._applyNormalTexture(material, "RenderPipelineFile/Default_NRM_2K.png") - print("✅ 默认法线贴图已添加") - else: - print("✅ 检测到材质已有法线贴图") - - print("清理现有金属度贴图...") - existing_stages = node.findAllTextureStages() - for stage in existing_stages: - if stage.getSort() == 5: # p3d_Texture3槽 - node.clearTexture(stage) - print(f"已清理现有金属度贴图: {stage.getName()}") - - for i in range(4): - metallic_stage = TextureStage("metallic_map") - metallic_stage.setSort(5) # p3d_Texture5 - metallic_stage.setMode(TextureStage.MModulate) - node.setTexture(metallic_stage, texture) - - try: - # 检查是否需要透明度测试 - needs_alpha = self._needsAlphaTesting(node) - print(f"🔍 透明度测试: {'需要' if needs_alpha else '不需要'}") - - self.world.render_pipeline.set_effect( - node, - "effects/pbr_with_metallic.yaml", - { - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": needs_alpha, # 根据是否需要透明度决定 - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 100 - ) - print(f"✅ 金属性PBR效果已应用(法线映射: {'启用' if has_normal else '禁用'})") - - # 温和的shader刷新方法 - print("🔄 温和刷新shader状态...") - try: - # 只重新绑定纹理,不破坏渲染状态 - node.setTexture(metallic_stage, texture) - - # 重新应用效果,但使用相同优先级 - self.world.render_pipeline.set_effect( - node, - "effects/pbr_with_metallic.yaml", - { - # "normal_mapping": has_normal, - "normal_mapping": True, - "render_gbuffer": True, - "alpha_testing": False, - "parallax_mapping": False, - "render_shadow": True, - "render_envmap": True - }, - 100 # 相同优先级 - ) - - print("✅ 金属性PBR效果已应用") - - # 关键修复:多次重新绑定纹理,确保着色器编译完成后生效 - print("🔧 强化纹理绑定机制...") - try: - import time - - # 多次重新绑定,模拟手动"应用两次"的效果 - for i in range(3): - print(f" 第{i + 1}次绑定...") - - # 等待更长时间确保着色器编译完成 - time.sleep(0.05) # 50ms延迟 - - # 强制重新绑定纹理 - node.setTexture(metallic_stage, texture) - - # 强制刷新节点状态 - node.setRenderModeWireframe() - node.clearRenderMode() - - # 验证绑定 - applied_texture = node.getTexture(metallic_stage) - if applied_texture: - print(f" ✅ 第{i + 1}次绑定成功") - else: - print(f" ❌ 第{i + 1}次绑定失败") - - # 最终验证 - final_texture = node.getTexture(metallic_stage) - if final_texture: - print(f"✅ 强化绑定完成: {final_texture.getName()}") - print("🔍 现在应该看到正确的金属性贴图效果") - print("🔍 黑色区域=非金属,白色区域=金属") - else: - print("❌ 强化绑定最终失败") - - except Exception as rebind_error: - print(f"⚠️ 强化纹理绑定失败: {rebind_error}") - - except Exception as refresh_error: - print(f"⚠️ 金属性PBR效果应用失败: {refresh_error}") - - except Exception as e: - print(f"⚠️ PBR效果应用失败: {e}") - - except Exception as e: - print(f"❌ 应用金属性贴图失败: {e}") - import traceback - traceback.print_exc() - - def _hasMetallicTexture(self, node): - """检查节点是否有金属性贴图""" - try: - all_stages = node.findAllTextureStages() - for stage in all_stages: - if stage.getSort() == 5: # p3d_Texture5 是金属性贴图槽 - tex = node.getTexture(stage) - if tex: - print(f"🔍 发现金属性贴图: {tex.getName()}") - return True - return False - except Exception as e: - print(f"⚠️ 检查金属性贴图失败: {e}") - return False - - def _applyDefaultNormalTexture(self, node): - """应用RenderPipeline的默认法线贴图""" - try: - from RenderPipelineFile.rpcore.loader import RPLoader - from panda3d.core import TextureStage - - print("🎨 加载RenderPipeline默认法线贴图...") - - # 使用RenderPipeline自带的默认法线贴图 - default_normal_path = "RenderPipelineFile/Default_NRM_2K.png" - texture = RPLoader.load_texture(default_normal_path) - - if not texture: - print(f"❌ 无法加载默认法线贴图: {default_normal_path}") - # 回退到虚拟法线贴图 - return - - print(f"✅ 默认法线贴图加载成功: {texture.getName()}") - - # 创建法线贴图纹理阶段 - normal_stage = TextureStage("default_normal") - normal_stage.setSort(1) # p3d_Texture1 - normal_stage.setMode(TextureStage.MModulate) - normal_stage.setTexcoordName("texcoord") - - # 应用默认法线贴图 - node.setTexture(normal_stage, texture) - print("✅ RenderPipeline默认法线贴图已应用") - - return True - - except Exception as e: - print(f"⚠️ 应用默认法线贴图失败: {e}") - # 回退到虚拟法线贴图 - return self._createDummyNormalTexture(node) - - def _needsAlphaTesting(self, node): - """检查节点是否需要透明度测试(检查漫反射贴图是否有透明通道)""" - try: - all_stages = node.findAllTextureStages() - for stage in all_stages: - if stage.getSort() == 0: # p3d_Texture0 是漫反射贴图槽 - tex = node.getTexture(stage) - if tex: - # 检查纹理格式是否支持透明度 - format_name = str(tex.getFormat()) - has_alpha = 'alpha' in format_name.lower() or 'rgba' in format_name.lower() - if has_alpha: - print(f"🔍 发现透明漫反射贴图: {tex.getName()}") - return True - return False - except Exception as e: - print(f"⚠️ 检查透明度需求失败: {e}") - return True # 出错时默认启用透明度测试,更安全 - - def _addSunAzimuthPanel(self): - """添加太阳方位角控制面板""" - # 太阳控制组 - sun_group = QGroupBox("太阳控制") - sun_layout = QGridLayout() - - # 太阳方位角 - sun_layout.addWidget(QLabel("方位角:"), 0, 0) - self.azimuthSpinBox = QDoubleSpinBox() - self.azimuthSpinBox.setRange(0, 360) - self.azimuthSpinBox.setSuffix("°") - self.azimuthSpinBox.setValue(180) - self.azimuthSpinBox.valueChanged.connect(self._applySunAzimuth_new) - sun_layout.addWidget(self.azimuthSpinBox, 0, 1, 1, 2) - - # 太阳高度角 - sun_layout.addWidget(QLabel("高度角:"), 1, 0) - self.altitudeSpinBox = QDoubleSpinBox() - self.altitudeSpinBox.setRange(0, 90) - self.altitudeSpinBox.setSuffix("°") - self.altitudeSpinBox.setValue(90) - self.altitudeSpinBox.valueChanged.connect(self._applySunAltitude) - sun_layout.addWidget(self.altitudeSpinBox, 1, 1, 1, 2) - - # 太阳预设 - sun_layout.addWidget(QLabel("预设:"), 2, 0) - presetCombo = QComboBox() - presetCombo.addItems(["日出", "正午", "日落", "午夜"]) - sun_layout.addWidget(presetCombo, 2, 1, 1, 2) - - # 映射 - preset_map = { - "日出": "sunrise", - "正午": "noon", - "日落": "sunset", - "午夜": "midnight" - } - - # 应用预设按钮 - applyPresetBtn = QPushButton("应用预设") - applyPresetBtn.clicked.connect(lambda: self._setSunPreset(preset_map[presetCombo.currentText()])) - sun_layout.addWidget(applyPresetBtn, 2, 3) - - # 实时更新 - # realtimeCheckBox = QCheckBox("实时更新") - # realtimeCheckBox.setChecked(True) - # sun_layout.addWidget(realtimeCheckBox, 3, 0, 1, 3) # 跨三列 - - sun_group.setLayout(sun_layout) - self._propertyLayout.addWidget(sun_group) - - def _onSunAzimuthSliderChanged(self, value): - """滑块值改变时的回调""" - try: - # 同步到数值框 - self.sun_azimuth_spinbox.blockSignals(True) - self.sun_azimuth_spinbox.setValue(value) - self.sun_azimuth_spinbox.blockSignals(False) - - # 应用到Day Time Editor - self._applySunAzimuth_new(value) - - except Exception as e: - print(f"❌ 滑块值改变处理失败: {e}") - - def _onSunAzimuthSpinboxChanged(self, value): - """数值框值改变时的回调""" - try: - # 同步到滑块 - self.sun_azimuth_slider.blockSignals(True) - self.sun_azimuth_slider.setValue(value) - self.sun_azimuth_slider.blockSignals(False) - - # 应用到Day Time Editor - self._applySunAzimuth_new(value) - - except Exception as e: - print(f"❌ 数值框值改变处理失败: {e}") - - def _onSunAltitudeSliderChanged(self, value): - """太阳高度角滑块值改变时的回调""" - try: - # 同步到数值框 - self.sun_altitude_spinbox.blockSignals(True) - self.sun_altitude_spinbox.setValue(value) - self.sun_altitude_spinbox.blockSignals(False) - - # 应用太阳高度角 - self._applySunAltitude(value) - - except Exception as e: - print(f"❌ 太阳高度角滑块值改变处理失败: {e}") - - def _onSunAltitudeSpinboxChanged(self, value): - """太阳高度角数值框值改变时的回调""" - try: - # 同步到滑块 - self.sun_altitude_slider.blockSignals(True) - self.sun_altitude_slider.setValue(value) - self.sun_altitude_slider.blockSignals(False) - - # 应用太阳高度角 - self._applySunAltitude(value) - - except Exception as e: - print(f"❌ 太阳高度角数值框值改变处理失败: {e}") - - def _setSunPreset(self, preset_name): - """设置太阳预设位置""" - try: - presets = { - "sunrise": (90, 45), # 东方,低角度 - "noon": (180, 90), # 南方,天顶 - "sunset": (270, 45), # 西方,低角度 - "midnight": (0, 0) # 北方,地平线 - } - - if preset_name in presets: - azimuth, altitude = presets[preset_name] - - # 更新滑块和数值框 - self.azimuthSpinBox.blockSignals(True) - self.altitudeSpinBox.blockSignals(True) - - self.azimuthSpinBox.setValue(azimuth) - self.altitudeSpinBox.setValue(altitude) - - self.azimuthSpinBox.blockSignals(False) - self.altitudeSpinBox.blockSignals(False) - - # 应用设置 - 优先使用Day Time Editor - azimuth_success = self._updateDayTimeEditorSetting("scattering", "sun_azimuth", azimuth) - altitude_success = self._updateDayTimeEditorSetting("scattering", "sun_altitude", altitude) - - if azimuth_success and altitude_success: - print(f"✅ 通过Day Time Editor设置太阳预设 {preset_name}: 方位角{azimuth}°, 高度角{altitude}°") - elif hasattr(self.world, 'sun_controller'): - self.world.sun_controller.set_sun_position(azimuth, altitude) - print(f"✅ 通过太阳控制器设置预设 {preset_name}: 方位角{azimuth}°, 高度角{altitude}°") - elif hasattr(self.world, 'setSunPosition'): - self.world.setSunPosition(azimuth, altitude) - print(f"✅ 通过主程序方法设置预设 {preset_name}: 方位角{azimuth}°, 高度角{altitude}°") - else: - # 分别应用方位角和高度角 - self._applySunAzimuth_new(azimuth) - self._applySunAltitude(altitude) - print(f"✅ 设置太阳预设 {preset_name}: 方位角{azimuth}°, 高度角{altitude}°") - - except Exception as e: - print(f"❌ 设置太阳预设失败: {e}") - import traceback - traceback.print_exc() - - def _applySunAzimuth_new(self, azimuth_value): - """应用太阳方位角 - 直接控制Day Time Editor""" - try: - # 方法1: 直接控制Day Time Editor中的sun_azimuth节点 - success = self._updateDayTimeEditorSetting("scattering", "sun_azimuth", azimuth_value) - if success: - print(f"✅ 通过Day Time Editor设置方位角: {azimuth_value}°") - return - - # 方法2: 使用我们的太阳控制器作为备用 - if hasattr(self.world, 'sun_controller'): - self.world.sun_controller.set_sun_azimuth(azimuth_value) - print(f"✅ 通过太阳控制器设置方位角: {azimuth_value}°") - return - - # 方法3: 直接调用主程序的太阳控制方法 - if hasattr(self.world, 'setSunAzimuth'): - self.world.setSunAzimuth(azimuth_value) - print(f"✅ 通过主程序方法设置方位角: {azimuth_value}°") - return - - print("⚠️ 所有方位角设置方法都不可用") - - except Exception as e: - print(f"❌ 应用太阳方位角失败: {e}") - import traceback - traceback.print_exc() - - def _applySunAltitude(self, altitude_value): - """应用太阳高度角 - 直接控制Day Time Editor""" - try: - # 方法1: 直接控制Day Time Editor中的sun_altitude节点 - success = self._updateDayTimeEditorSetting("scattering", "sun_altitude", altitude_value) - if success: - print(f"✅ 通过Day Time Editor设置高度角: {altitude_value}°") - return - - # 方法2: 使用我们的太阳控制器作为备用 - if hasattr(self.world, 'sun_controller'): - self.world.sun_controller.set_sun_altitude(altitude_value) - print(f"✅ 通过太阳控制器设置高度角: {altitude_value}°") - return - - # 方法3: 直接调用主程序的太阳控制方法 - if hasattr(self.world, 'setSunAltitude'): - self.world.setSunAltitude(altitude_value) - print(f"✅ 通过主程序方法设置高度角: {altitude_value}°") - return - - print("⚠️ 所有高度角设置方法都不可用") - - except Exception as e: - print(f"❌ 应用太阳高度角失败: {e}") - import traceback - traceback.print_exc() - - def _updateDayTimeEditorSetting(self, plugin_name, setting_name, value): - """直接更新Day Time Editor中的设置节点值""" - try: - # 检查是否有RenderPipeline和插件管理器 - if not hasattr(self.world, 'render_pipeline') or not self.world.render_pipeline: - print("⚠️ RenderPipeline未初始化") - return False - - pipeline = self.world.render_pipeline - if not hasattr(pipeline, 'plugin_mgr') or not pipeline.plugin_mgr: - print("⚠️ 插件管理器未初始化") - return False - - plugin_mgr = pipeline.plugin_mgr - - # 检查插件是否存在 - if plugin_name not in plugin_mgr.instances: - print(f"⚠️ 插件 '{plugin_name}' 不存在") - return False - - # 检查Day Time设置是否存在 - if plugin_name not in plugin_mgr.day_settings: - print(f"⚠️ 插件 '{plugin_name}' 没有Day Time设置") - return False - - day_settings = plugin_mgr.day_settings[plugin_name] - if setting_name not in day_settings: - print(f"⚠️ 设置 '{setting_name}' 在插件 '{plugin_name}' 中不存在") - return False - - setting_handle = day_settings[setting_name] - - # 根据设置类型转换值 - if setting_name == "sun_azimuth": - # 方位角:度数转换为0-1范围 - normalized_value = (value % 360) / 360.0 - elif setting_name == "sun_altitude": - # 高度角:度数转换为0-1范围 - normalized_value = max(0, min(90, value)) / 90.0 - else: - # 其他设置直接使用原值 - normalized_value = value - - # 获取当前时间(如果Day Time Editor正在运行) - current_time = getattr(self, '_current_daytime', 0.5) # 默认中午12点 - - # 更新设置的曲线值 - if hasattr(setting_handle, 'curves') and setting_handle.curves: - # 清除现有的控制点,设置单一值 - setting_handle.curves[0].set_single_value(normalized_value) - #print(f"✅ 更新Day Time设置: {plugin_name}.{setting_name} = {value} (归一化: {normalized_value:.3f})") - - # 保存设置到配置文件 - try: - plugin_mgr.save_daytime_overrides("/$$rpconfig/daytime.yaml") - #print("✅ Day Time设置已保存到配置文件") - except Exception as e: - print(f"⚠️ 保存配置文件失败: {e}") - - # 通知Day Time Editor重新加载配置(如果正在运行) - try: - from RenderPipelineFile.rpcore.util.network_communication import NetworkCommunication - NetworkCommunication.send_async(NetworkCommunication.DAYTIME_PORT, "loadconf") - #print("✅ 已通知Day Time Editor重新加载配置") - except Exception as e: - print(f"⚠️ 通知Day Time Editor失败: {e}") - - return True - else: - print(f"⚠️ 设置 '{setting_name}' 没有可用的曲线") - return False - - except Exception as e: - print(f"❌ 更新Day Time Editor设置失败: {e}") - import traceback - traceback.print_exc() - return False - - def _applySunAzimuth(self, azimuth_degrees): - """应用太阳方位角到Day Time Editor中的Sun Azimuth节点""" - try: - if not hasattr(self, 'world') or not self.world: - print("⚠️ World对象不存在") - return - - # Day Time Editor是独立进程,需要通过进程间通信控制 - print(f"🌞 尝试控制Day Time Editor中的Sun Azimuth: {azimuth_degrees}°") - - # 方法1:通过文件通信 - success = self._sendSunAzimuthViaFile(azimuth_degrees) - if success: - return - - # 方法2:尝试直接控制RenderPipeline的daytime_mgr(如果存在) - # success = self._controlLocalDaytimeManager(azimuth_degrees) - # if success: - # return - - # 方法3:通过socket通信(如果实现了) - # success = self._sendSunAzimuthViaSocket(azimuth_degrees) - # if success: - # return - - print("⚠️ 无法控制Day Time Editor中的Sun Azimuth节点") - print(" 原因:Day Time Editor运行在独立进程中") - print(" 建议:") - print(" 1. 直接在Day Time Editor窗口中调整Sun Azimuth") - print(" 2. 或者实现进程间通信机制") - - except Exception as e: - print(f"❌ 应用太阳方位角失败: {e}") - import traceback - traceback.print_exc() - - def _sendSunAzimuthViaFile(self, azimuth_degrees): - """通过文件通信发送Sun Azimuth值到Day Time Editor""" - try: - import json - import os - import tempfile - - # 创建通信文件 - comm_dir = os.path.join(tempfile.gettempdir(), "daytime_editor_comm") - os.makedirs(comm_dir, exist_ok=True) - - comm_file = os.path.join(comm_dir, "sun_azimuth_command.json") - - command = { - "command": "set_sun_azimuth", - "value": azimuth_degrees, - "timestamp": __import__('time').time() - } - - with open(comm_file, 'w') as f: - json.dump(command, f) - - print(f"📁 已通过文件发送Sun Azimuth命令: {azimuth_degrees}°") - print(f" 通信文件: {comm_file}") - return True - - except Exception as e: - print(f"⚠️ 文件通信失败: {e}") - return False - - def _addAnimationPanel(self, origin_model): - try: - has_animation = False - - # 动画控制组 - animation_group = QGroupBox("动画控制") - animation_layout = QGridLayout() - animation_layout.setColumnMinimumWidth(0, 115) - - - # 首先检测骨骼动画 - has_skeletal_anim = False - try: - actor = self._getActor(origin_model) - if actor and actor.getAnimNames(): - self._buildSkeletalUI(origin_model, actor, animation_layout) - has_animation = True - print(f"[信息] 检测到骨骼动画: {actor.getAnimNames()}") - except Exception as actor_error: - # 忽略 Actor 加载错误,很多模型都不是角色动画 - print(f"[信息] 该模型不包含骨骼动画: {actor_error}") - - # 如果都没有动画 - if not has_animation: - no_anim_label = QLabel("无法识别动画") - no_anim_label.setStyleSheet("color:#888;font-style:italic;") - animation_layout.addWidget(no_anim_label, 0, 0) - - animation_group.setLayout(animation_layout) - self._propertyLayout.addWidget(animation_group) - - except Exception as e: - print("添加动画面板失败:", e) - import traceback - traceback.print_exc() - - def _buildSkeletalUI(self, origin_model, actor, layout): - from PyQt5.QtWidgets import QLabel, QComboBox, QHBoxLayout, QWidget, QPushButton, QDoubleSpinBox - - actor.hide() - origin_model.show() - - # animation_title = QLabel("骨骼动画控制") - # animation_title.setStyleSheet("color:#6B6BFF;font-weight:bold;font-size:14px;margin-top:10px;") - # self._propertyLayout.addRow(animation_title) - - # 获取和分析动画名称 - anim_names = actor.getAnimNames() - processed_names = self._processAnimationNames(origin_model, anim_names) - - # 显示动画信息 - format_info = self._getModelFormat(origin_model) - animation_info = self._analyzeAnimationQuality(actor, anim_names, format_info) - info_text = f"格式: {format_info} | 动画数量: {len(processed_names)}" - if animation_info: - info_text += f" | {animation_info}" - info_label = QLabel(info_text) - info_label.setStyleSheet("color:#888;font-size:10px;") - layout.addWidget(QLabel("信息:"), 0, 0) - layout.addWidget(info_label, 0, 1, 1, 3) - - # 如果是 FBX 且动画有问题,添加转换按钮 - current_row = 1 - if format_info == "FBX" and "⚠️" in animation_info: - convert_btn = QPushButton("🔄 转换FBX动画") - convert_btn.setStyleSheet("background-color:#FFA500;color:white;font-weight:bold;") - convert_btn.clicked.connect(lambda: self._convertFBXManually(origin_model)) - layout.addWidget(QLabel("修复:"), current_row, 0) - layout.addWidget(convert_btn, current_row, 1, 1, 3) - current_row += 1 - - self.animation_combo = QComboBox() - # 使用处理后的名称,但保留原始名称用于播放 - for display_name, original_name in processed_names: - self.animation_combo.addItem(display_name, original_name) - layout.addWidget(QLabel("动画名称:"), current_row, 0) - layout.addWidget(self.animation_combo, current_row, 1, 1, 3) - current_row += 1 - - # btn_box = QWidget() - btn_lay = QHBoxLayout() - for txt, slot in (("播放", self._playAnimation), - ("暂停", self._pauseAnimation), - ("停止", self._stopAnimation), - ("循环", self._loopAnimation)): - btn = QPushButton(txt) - btn.clicked.connect(lambda _, f=slot: f(origin_model)) - btn_lay.addWidget(btn) - layout.addWidget(QLabel("控制:"), current_row, 0) - layout.addLayout(btn_lay, current_row, 1, 1, 3) - current_row += 1 - - self.speed_spinbox = QDoubleSpinBox() - self.speed_spinbox.setRange(0.1, 5.0) - self.speed_spinbox.setSingleStep(0.1) - saved = origin_model.getPythonTag("anim_speed") - self.speed_spinbox.setValue(saved if saved is not None else 1.0) - # self.speed_spinbox.setValue(1.0) - self.speed_spinbox.valueChanged.connect(lambda v: self._setAnimationSpeed(origin_model, v)) - layout.addWidget(QLabel("播放速度:"), current_row, 0) - layout.addWidget(self.speed_spinbox, current_row, 1) - - def _getModelFormat(self, origin_model): - """获取模型格式信息""" - filepath = origin_model.getTag("model_path") - original_path = origin_model.getTag("original_path") - converted_from = origin_model.getTag("converted_from") - - if filepath: - ext = filepath.lower().split('.')[-1] - format_name = ext.upper() - - # 如果是转换后的文件,显示转换信息 - if converted_from and original_path: - original_ext = converted_from.upper() - format_name = f"{format_name} (从{original_ext}转换)" - - return format_name - return "未知" - - def _processAnimationNames(self, origin_model, anim_names): - """处理和分析动画名称,返回 [(显示名称, 原始名称), ...]""" - format_info = self._getModelFormat(origin_model) - processed = [] - - print(f"[动画分析] 格式: {format_info}, 原始动画名称: {anim_names}") - - for name in anim_names: - display_name = name - original_name = name - - if format_info == "GLB": - # GLB 格式通常有真实的动画名称 - if "|" in name: - # 处理类似 'Armature|mixamo.com|Layer0' 的名称 - parts = name.split("|") - if "mixamo" in name.lower(): - # Mixamo 动画 - display_name = f"Mixamo_{parts[-1]}" if len(parts) > 1 else name - elif len(parts) > 2: - # 其他复杂命名 - display_name = f"{parts[0]}_{parts[-1]}" - else: - display_name = parts[-1] - - elif format_info == "FBX": - # FBX 格式可能需要特殊处理 - if self._isLikelyBoneGroup(name): - # 检查是否是骨骼组而非动画 - print(f"[警告] '{name}' 可能不是真正的动画序列,而是骨骼组") - display_name = f"⚠️ {name} (可能非动画)" - else: - display_name = name - - elif format_info in ["EGG", "BAM"]: - # 原生格式通常命名规范 - display_name = name - - processed.append((display_name, original_name)) - print(f"[动画分析] {original_name} → {display_name}") - - return processed - - def _isLikelyBoneGroup(self, name): - """判断动画名称是否更像骨骼组而不是动画序列""" - bone_indicators = ['joints', 'bones', 'skeleton', 'surface', 'mesh', 'beta', 'rig'] - name_lower = name.lower() - - # 如果包含这些关键词,可能是骨骼组 - for indicator in bone_indicators: - if indicator in name_lower: - return True - - # 如果名称太简单(少于3个字符),可能不是动画 - if len(name) < 3: - return True - - return False - - def _analyzeAnimationQuality(self, actor, anim_names, format_info): - """分析动画质量和类型""" - try: - total_frames = 0 - valid_anims = 0 - - for anim_name in anim_names: - try: - control = actor.getAnimControl(anim_name) - if control: - frames = control.getNumFrames() - if frames > 1: - valid_anims += 1 - total_frames += frames - print(f"[动画分析] '{anim_name}': {frames} 帧") - else: - print(f"[动画分析] '{anim_name}': 无有效帧数 ({frames})") - except Exception as e: - print(f"[动画分析] '{anim_name}' 分析失败: {e}") - - if valid_anims == 0: - return "⚠️ 无有效动画序列" - elif valid_anims < len(anim_names): - return f"⚠️ {valid_anims}/{len(anim_names)} 个有效" - else: - avg_frames = total_frames // valid_anims - return f"✓ 平均 {avg_frames} 帧" - - except Exception as e: - print(f"[动画分析] 分析失败: {e}") - return "分析失败" - - def _getActor(self, origin_model): - if origin_model in self._actor_cache: - return self._actor_cache[origin_model] - - # 首先检查是否可以直接从内存中的模型创建Actor - if origin_model.hasTag("can_create_actor_from_memory") and origin_model.getTag("can_create_actor_from_memory").lower() == "true": - try: - print(f"[Actor加载] 直接从内存模型创建Actor: {origin_model.getName()}") - # 直接从内存中的模型创建Actor - test_actor = Actor(origin_model) - anims = test_actor.getAnimNames() - test_actor.reparentTo(self.world.render) - self._actor_cache[origin_model] = test_actor - print(f"[Actor加载] 内存创建检测到动画: {anims}") - if anims: - return test_actor - else: - test_actor.cleanup() - test_actor.removeNode() - except Exception as e: - print(f"从内存模型创建Actor失败: {e}") - - # 如果不能直接从内存创建,再尝试通过文件路径加载 - filepath = origin_model.getTag("model_path") - if not filepath: - return None - - print(f"[Actor加载] 尝试加载: {filepath}") - - # 处理跨平台路径问题 - import os - # 检查路径是否有效,如果无效则尝试修复 - if not os.path.exists(filepath): - original_filepath = filepath - # 尝试多种修复策略 - fixed = False - - import platform - # 策略1: 处理Linux风格路径在Windows上的问题 - if filepath.startswith('/') and platform.system() == "Windows": - print("[路径转换] 尝试处理Linux风格路径:", filepath) - path_parts = filepath.split('/') - print(platform.system()) - if len(path_parts) > 1: - drive_letter = path_parts[1].upper() + ':\\' # 添加反斜杠确保正确路径格式 - remaining_path = '\\'.join(path_parts[2:]) if len(path_parts) > 2 else '' - potential_path = os.path.join(drive_letter, remaining_path) - print(f"[路径转换] 构造的潜在路径: {potential_path}") - if os.path.exists(potential_path): - filepath = potential_path - fixed = True - print(f"[路径转换] 成功: {original_filepath} -> {filepath}") - else: - print(f"[路径转换] 文件不存在: {potential_path}") - - - # 策略2: 处理路径分隔符问题 - if not fixed: - # 尝试规范化路径 - normalized_path = os.path.normpath(filepath) - print(f"[路径规范化] 尝试规范化路径: {filepath} -> {normalized_path}") - if os.path.exists(normalized_path): - filepath = normalized_path - fixed = True - print(f"[路径规范化] 成功: {filepath}") - else: - print(f"[路径规范化] 文件不存在: {normalized_path}") - - # 策略3: 在Resources目录中查找 - if not fixed: - # 尝试在Resources目录中查找文件 - resources_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "Resources") - filename = os.path.basename(filepath) - potential_path = os.path.join(resources_path, filename) - print(f"[Resources查找] 尝试在Resources目录查找: {potential_path}") - if os.path.exists(potential_path): - filepath = potential_path - fixed = True - print(f"[Resources查找] 成功: {filepath}") - else: - print(f"[Resources查找] 文件不存在: {potential_path}") - - if fixed: - print(f"路径修复: {original_filepath} -> {filepath}") - # 更新模型标签 - origin_model.setTag("model_path", filepath) - else: - print(f"[警告] 模型文件不存在: {filepath}") - return None - - # 检查是否是 FBX 文件,如果是,使用专门的 FBX 动画加载器 - if filepath.lower().endswith('.fbx'): - pass - #return self._createFBXActor(origin_model, filepath) - - # 其他格式使用标准 Actor 加载 - try: - import gltf - from panda3d.core import Filename - - # 将Panda3D路径转换为操作系统特定路径 - panda_filename = Filename(filepath) - os_specific_path = panda_filename.to_os_specific() - print(f"[路径转换] {filepath} -> {os_specific_path}") - - print(f"[GLTF加载] 尝试加载: {os_specific_path}") - - # 使用明确的设置确保动画被加载 - gltf_settings = gltf.GltfSettings(skip_animations=False) - # test_actor=Actor(NodePath(gltf._loader.GltfLoader.load_file(filepath,None))) - model_root = gltf.load_model(os_specific_path, gltf_settings) - model_node = NodePath(model_root) - test_actor = Actor(model_node) - anims = test_actor.getAnimNames() - test_actor.reparentTo(self.world.render) - self._actor_cache[origin_model] = test_actor - print(f"[Actor加载] 标准加载检测到动画: {anims}") - if not anims: - test_actor.cleanup() - test_actor.removeNode() - return None - return test_actor - except Exception as e: - print(f"创建Actor失败: {e}") - return None - - def _createFBXActor(self, origin_model, filepath): - """专门为 FBX 文件创建 Actor,使用转换方式获取真实动画""" - try: - print(f"[FBX动画] 开始处理 FBX 动画: {filepath}") - - # 方法1: 尝试转换 FBX 为包含动画的格式 - converted_actor = self._convertFBXToActor(filepath) - if converted_actor: - converted_actor.reparentTo(self.world.render) - self._actor_cache[origin_model] = converted_actor - print(f"[FBX动画] 转换成功,动画: {converted_actor.getAnimNames()}") - return converted_actor - - # 方法2: 直接加载但进行动画数据修复 - actor = Actor(filepath) - if actor: - fixed_actor = self._fixFBXAnimations(actor, filepath) - if fixed_actor and fixed_actor.getAnimNames(): - fixed_actor.reparentTo(self.world.render) - self._actor_cache[origin_model] = fixed_actor - print(f"[FBX动画] 修复成功,动画: {fixed_actor.getAnimNames()}") - return fixed_actor - - print(f"[FBX动画] 无法获取有效动画数据") - return None - - except Exception as e: - print(f"[FBX动画] 处理失败: {e}") - return None - - def _convertFBXToActor(self, fbx_path): - """将 FBX 转换为可用的 Actor""" - try: - import tempfile - import os - - # 创建临时文件用于转换 - temp_dir = tempfile.mkdtemp() - egg_path = os.path.join(temp_dir, "converted.egg") - - print(f"[FBX转换] 转换 {fbx_path} 到 {egg_path}") - - # 使用 Panda3D 转换工具链 - # FBX -> Collada -> EGG - try: - # 检查是否有可用的转换工具 - import subprocess - - # 方法1: 尝试直接使用 assimp (如果安装了) - result = subprocess.run([ - 'assimp', 'export', fbx_path, egg_path - ], capture_output=True, text=True, timeout=30) - - if result.returncode == 0 and os.path.exists(egg_path): - actor = Actor(egg_path) - if actor.getAnimNames(): - print(f"[FBX转换] Assimp 转换成功") - return actor - - except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError): - print(f"[FBX转换] Assimp 转换失败,尝试其他方法") - - # 方法2: 使用 Blender 脚本转换(如果安装了 Blender) - try: - blender_script = f''' -import bpy -import os -bpy.ops.wm.read_factory_settings(use_empty=True) -bpy.ops.import_scene.fbx(filepath="{fbx_path}") -bpy.ops.export_scene.gltf(filepath="{egg_path.replace('.egg', '.gltf')}", export_animations=True) -''' - - script_path = os.path.join(temp_dir, "convert.py") - with open(script_path, 'w') as f: - f.write(blender_script) - - result = subprocess.run([ - 'blender', '--background', '--python', script_path - ], capture_output=True, text=True, timeout=60) - - gltf_path = egg_path.replace('.egg', '.gltf') - if os.path.exists(gltf_path): - # 使用 gltf2bam 转换为 BAM - subprocess.run(['gltf2bam', gltf_path, egg_path.replace('.egg', '.bam')]) - bam_path = egg_path.replace('.egg', '.bam') - if os.path.exists(bam_path): - actor = Actor(bam_path) - if actor.getAnimNames(): - print(f"[FBX转换] Blender 转换成功") - return actor - - except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError): - print(f"[FBX转换] Blender 转换失败") - - return None - - except Exception as e: - print(f"[FBX转换] 转换过程出错: {e}") - return None - - def _fixFBXAnimations(self, actor, fbx_path): - """修复 FBX Actor 的动画数据""" - try: - print(f"[FBX修复] 尝试修复动画数据") - - # 获取所有动画名称 - anim_names = actor.getAnimNames() - print(f"[FBX修复] 原始动画名称: {anim_names}") - - # 检查每个动画是否真的有动画数据 - valid_anims = [] - for anim_name in anim_names: - try: - control = actor.getAnimControl(anim_name) - if control and control.getNumFrames() > 1: - valid_anims.append(anim_name) - print(f"[FBX修复] 有效动画: {anim_name} ({control.getNumFrames()} 帧)") - else: - print(f"[FBX修复] 无效动画: {anim_name} (帧数: {control.getNumFrames() if control else 0})") - except: - print(f"[FBX修复] 无法获取动画控制: {anim_name}") - - if valid_anims: - print(f"[FBX修复] 找到 {len(valid_anims)} 个有效动画") - return actor - else: - print(f"[FBX修复] 没有找到有效动画,尝试重新解析") - - # 尝试重新加载和分析 FBX 文件结构 - return self._deepAnalyzeFBX(fbx_path) - - except Exception as e: - print(f"[FBX修复] 修复失败: {e}") - return None - - def _deepAnalyzeFBX(self, fbx_path): - """深度分析 FBX 文件并尝试提取动画""" - try: - print(f"[FBX深度分析] 分析文件: {fbx_path}") - - # 尝试直接加载为模型,然后手动查找动画节点 - from panda3d.core import Loader - - loader = Loader.getGlobalPtr() - model = loader.loadSync(fbx_path) - - if model: - print(f"[FBX深度分析] 成功加载模型") - - # 查找动画相关节点 - anim_nodes = model.findAllMatches("**/+AnimBundleNode") - char_nodes = model.findAllMatches("**/+CharacterNode") - - print(f"[FBX深度分析] AnimBundleNode: {anim_nodes.getNumPaths()}") - print(f"[FBX深度分析] CharacterNode: {char_nodes.getNumPaths()}") - - if not char_nodes.isEmpty(): - # 尝试基于 CharacterNode 创建 Actor - char_node = char_nodes.getPath(0) - character = char_node.node().getCharacter() - - if character: - # 创建新的 Actor 实例并绑定角色 - actor = Actor() - actor.instance(model, "character") - - # 检查是否有动画 - if actor.getAnimNames(): - print(f"[FBX深度分析] 成功提取动画: {actor.getAnimNames()}") - return actor - - return None - - except Exception as e: - print(f"[FBX深度分析] 分析失败: {e}") - return None - - def _convertFBXManually(self, origin_model): - """手动转换 FBX 动画""" - from PyQt5.QtWidgets import QMessageBox, QProgressDialog - from PyQt5.QtCore import QTimer - - try: - filepath = origin_model.getTag("model_path") - if not filepath or not filepath.lower().endswith('.fbx'): - return - - # 显示进度对话框 - progress = QProgressDialog("正在转换FBX动画...", "取消", 0, 100) - progress.setWindowTitle("FBX动画转换") - progress.show() - - print(f"[手动转换] 开始转换: {filepath}") - - # 尝试使用系统转换工具 - converted_path = self._systemConvertFBX(filepath, progress) - - if converted_path: - # 重新加载转换后的模型 - progress.setLabelText("重新加载模型...") - progress.setValue(80) - - # 清除缓存 - if origin_model in self._actor_cache: - del self._actor_cache[origin_model] - - # 更新模型路径标签 - origin_model.setTag("model_path", converted_path) - - progress.setValue(100) - progress.hide() - - # 显示成功消息 - QMessageBox.information(None, "转换成功", - f"FBX动画转换成功!\n请重新选择模型查看动画。") - - print(f"[手动转换] 转换完成: {converted_path}") - - else: - progress.hide() - - # 显示转换选项 - msg = QMessageBox() - msg.setWindowTitle("转换建议") - msg.setText("自动转换失败,建议使用以下方法:") - msg.setDetailedText(""" -1. 使用 Blender 转换: - - 打开 Blender - - 导入 FBX 文件 - - 导出为 glTF (.gltf) 格式,确保选择"包含动画" - -2. 使用命令行工具: - - gltf2bam your_file.gltf your_file.bam - -3. 检查原始 FBX 文件: - - 确保 FBX 文件确实包含动画数据 - - 尝试在其他软件中验证动画 - """) - msg.exec_() - - except Exception as e: - print(f"[手动转换] 转换失败: {e}") - QMessageBox.warning(None, "转换失败", f"转换过程中出现错误: {e}") - - def _systemConvertFBX(self, fbx_path, progress=None): - """使用系统工具转换 FBX""" - import os - import subprocess - import tempfile - - try: - # 准备输出路径 - base_name = os.path.splitext(os.path.basename(fbx_path))[0] - output_dir = os.path.dirname(fbx_path) - gltf_path = os.path.join(output_dir, f"{base_name}_converted.gltf") - bam_path = os.path.join(output_dir, f"{base_name}_converted.bam") - - if progress: - progress.setValue(20) - progress.setLabelText("检查转换工具...") - - # 方法1: 使用 gltf2bam 的逆向功能(如果支持) - try: - # 首先尝试看看是否有直接的 FBX 支持 - result = subprocess.run(['gltf2bam', '--help'], - capture_output=True, text=True, timeout=10) - print(f"[系统转换] gltf2bam 可用") - except: - print(f"[系统转换] gltf2bam 不可用") - - if progress: - progress.setValue(40) - progress.setLabelText("尝试 Blender 转换...") - - # 方法2: 使用 Blender 无头模式转换 - try: - # 创建 Blender 转换脚本 - script_content = f''' -import bpy -import sys -import os - -# 清理默认场景 -bpy.ops.object.select_all(action='SELECT') -bpy.ops.object.delete(use_global=False) - -# 导入 FBX -try: - bpy.ops.import_scene.fbx(filepath="{fbx_path}") - print("FBX导入成功") - - # 导出为 glTF - bpy.ops.export_scene.gltf( - filepath="{gltf_path}", - export_animations=True, - export_force_sampling=True, - export_frame_range=True - ) - print("glTF导出成功") - -except Exception as e: - print(f"转换失败: {{e}}") - sys.exit(1) -''' - - temp_script = tempfile.mktemp(suffix='.py') - with open(temp_script, 'w') as f: - f.write(script_content) - - if progress: - progress.setValue(60) - progress.setLabelText("执行 Blender 转换...") - - # 执行 Blender 转换 - result = subprocess.run([ - 'blender', '--background', '--python', temp_script - ], capture_output=True, text=True, timeout=120) - - # 清理临时文件 - if os.path.exists(temp_script): - os.remove(temp_script) - - if result.returncode == 0 and os.path.exists(gltf_path): - if progress: - progress.setValue(80) - progress.setLabelText("转换为 BAM 格式...") - - # 转换 glTF 为 BAM - result2 = subprocess.run(['gltf2bam', gltf_path, bam_path], - capture_output=True, text=True, timeout=60) - - if result2.returncode == 0 and os.path.exists(bam_path): - print(f"[系统转换] 成功转换为: {bam_path}") - return bam_path - elif os.path.exists(gltf_path): - print(f"[系统转换] 成功转换为: {gltf_path}") - return gltf_path - - except subprocess.TimeoutExpired: - print(f"[系统转换] Blender 转换超时") - except FileNotFoundError: - print(f"[系统转换] Blender 未安装") - except Exception as e: - print(f"[系统转换] Blender 转换出错: {e}") - - return None - - except Exception as e: - print(f"[系统转换] 系统转换失败: {e}") - return None - - def _playAnimation(self, origin_model): - actor = self._getActor(origin_model) - if not actor: - return - - original_world_pos = origin_model.getPos(self.world.render) - original_world_hpr = origin_model.getHpr(self.world.render) - original_world_scale = origin_model.getScale(self.world.render) - actor.setPos(origin_model.getPos()) - actor.setHpr(origin_model.getHpr()) - actor.setScale(origin_model.getScale()) - - origin_model.hide() - actor.show() - - #创建人物来维持世界坐标不变 - def maintainWorldPosition(task): - try: - if not actor.isEmpty(): - actor.setPos(self.world.render,original_world_pos) - actor.setHpr(self.world.render,original_world_hpr) - actor.setScale(self.world.render,original_world_scale) - return task.cont - else: - return task.done - except: - return task.done - - taskMgr.add(maintainWorldPosition,f"maintain_anim_pos_{id(actor)}") - - if hasattr(self, 'animation_combo'): - # 获取原始动画名称(存储在 userData 中) - current_index = self.animation_combo.currentIndex() - if current_index >= 0: - original_name = self.animation_combo.itemData(current_index) - display_name = self.animation_combo.currentText() - if original_name: - actor.play(original_name) - print(f"『动画播放』:{display_name} (原始名称: {original_name})") - else: - # 兜底:使用显示名称 - actor.play(display_name) - print(f"『动画播放』:{display_name}") - - def _pauseAnimation(self, origin_model): - actor = self._getActor(origin_model) - if not actor: - return - actor.setPos(origin_model.getPos()) - actor.setHpr(origin_model.getHpr()) - actor.setScale(origin_model.getScale()) - - origin_model.hide() - actor.show() - actor.stop() - print("『动画』暂停") - - def _stopAnimation(self, origin_model): - actor = self._getActor(origin_model) - if not actor: - return - actor.stop() - - # 获取原始动画名称 - current_index = self.animation_combo.currentIndex() - if current_index >= 0: - original_name = self.animation_combo.itemData(current_index) - display_name = self.animation_combo.currentText() - anim_name = original_name if original_name else display_name - - if anim_name and actor.getAnimControl(anim_name): - actor.getAnimControl(anim_name).pose(0) - actor.hide() - origin_model.show() - print("『动画』停止切换至原始模型") - - def _loopAnimation(self, origin_model): - actor = self._getActor(origin_model) - if not actor: - return - - actor.setPos(origin_model.getPos()) - actor.setHpr(origin_model.getHpr()) - actor.setScale(origin_model.getScale()) - - origin_model.hide() - actor.show() - - # 获取原始动画名称 - current_index = self.animation_combo.currentIndex() - if current_index >= 0: - original_name = self.animation_combo.itemData(current_index) - display_name = self.animation_combo.currentText() - anim_name = original_name if original_name else display_name - - if anim_name: - actor.loop(anim_name) - print(f"[动画] 循环: {display_name} (原始名称: {anim_name})") - - def _setAnimationSpeed(self, origin_model, speed): - """ - 设置当前动画的播放倍速。 - """ - actor = self._getActor(origin_model) - if not actor: - return - # 获取原始动画名称 - current_index = self.animation_combo.currentIndex() - - if current_index >= 0: - original_name = self.animation_combo.itemData(current_index) - display_name = self.animation_combo.currentText() - anim_name = original_name if original_name else display_name - - if anim_name: - actor.setPlayRate(speed, anim_name) - origin_model.setPythonTag("anim_speed", speed) - print(f"[动画] 速度设为: {speed} ({display_name})") - - def _dispatchAnimCommand(self, origin_model, cmd): - cache = self._actor_cache.get(origin_model) - if not cache: - return - kind, player = cache - - if kind == "actor": - actor = player - anim_name = self.animation_combo.currentText() - actor.setPos(origin_model.getPos()) - actor.setHpr(origin_model.getHpr()) - actor.setScale(origin_model.getScale() / 100) - - if cmd == "play": - origin_model.hide() - actor.show() - actor.play(anim_name) - elif cmd == "pause": - origin_model.hide() - actor.show() - actor.stop() - elif cmd == "stop": - actor.stop() - if anim_name and actor.getAnimControl(anim_name): - actor.getAnimControl(anim_name).pose(0) - actor.hide(); - origin_model.show() - elif cmd == "loop": - origin_model.hide() - actor.show() - actor.loop(anim_name) - elif isinstance(cmd, tuple) and cmd[0] == "speed": - actor.setPlayRate(cmd[1], anim_name) - - def removeActorForModel(self, model): - """删除 model 对应的 Actor(如果存在)""" - actor = self._actor_cache.pop(model, None) - if actor: - actor.stop() - actor.cleanup() - actor.removeNode() - - def _addCollisionPanel(self, model): - """添加碰撞检测面板""" - try: - # 创建碰撞检测组 - collision_group = QGroupBox("碰撞检测") - collision_layout = QGridLayout() - collision_layout.setColumnMinimumWidth(0, self.column_minimum_width) - # collision_layout.setColumnStretch(0, 0) - # collision_layout.setColumnStretch(1, 1) - # collision_layout.setColumnStretch(2, 1) - # collision_layout.setColumnStretch(3, 1) - - # 检查模型是否已有碰撞 - has_collision = self._hasCollision(model) - - # 创建主容器 - main_container = QWidget() - main_layout = QVBoxLayout(main_container) - main_layout.setContentsMargins(8, 8, 8, 8) - main_layout.setSpacing(12) - - # 状态和形状选择区域 - header_container = QWidget() - header_layout = QGridLayout(header_container) - header_layout.setContentsMargins(0, 0, 0, 0) - header_layout.setSpacing(8) - - # 调整列宽,保证控件对齐 - # collision_layout.setColumnStretch(0, 0) - # collision_layout.setColumnStretch(1, 0) - # collision_layout.setColumnStretch(2, 0) - # collision_layout.setColumnStretch(3, 0) - - # 碰撞状态行 - status_layout = QHBoxLayout() - status_label = QLabel("状态:") - status_layout.addWidget(status_label) - - # 状态徽章(使用固定宽度样式) - if has_collision: - self.collision_status_badge = self.createFixedStatusBadge("已启用", "green") - else: - self.collision_status_badge = self.createFixedStatusBadge("未启用", "red") - status_layout.addWidget(self.collision_status_badge) - collision_layout.addLayout(status_layout, 0, 0, 1, 1) - - # 形状选择标签(始终显示) - self.collision_shape_label = QLabel("碰撞形状:") - collision_layout.addWidget(self.collision_shape_label, 1, 0) - - # 形状选择下拉框(始终显示) - self.collision_shape_combo = QComboBox() - self.collision_shape_combo.addItems([ - "球形 (Sphere)", - "盒型 (Box)", - "胶囊体 (Capsule)", - "平面 (Plane)", - "自动选择 (Auto)" - ]) - collision_layout.addWidget(self.collision_shape_combo, 1, 1, 1, 3) - - # 保存布局引用,用于动态添加/移除控件 - self.collision_layout = collision_layout - self.collision_group = collision_group - - current_row = 2 # 下一行的索引 - - # 显示/隐藏切换按钮(只有有碰撞时才显示) - if has_collision: - # 检查碰撞的当前可见性 - is_collision_visible = self._isCollisionVisible(model) - - # 显示当前碰撞类型并设置为只读 - current_shape = self._getCurrentCollisionShape(model) - self._setComboToShape(current_shape) - self.collision_shape_combo.setEnabled(False) - - # 添加碰撞参数调整控件 - current_row = self._addCollisionParameterControls(model, collision_layout, current_row, current_shape) - - # 显示/隐藏切换按钮(使用现代化样式) - visibility_text = "隐藏碰撞" if is_collision_visible else "显示碰撞" - self.collision_visibility_button = self.createModernButton(visibility_text) - self.collision_visibility_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - self.collision_visibility_button.setStyleSheet(self._collision_button_style()) - self.collision_visibility_button.clicked.connect(lambda: self._toggleCollisionVisibility(model)) - collision_layout.addWidget(self.collision_visibility_button, current_row, 0, 1, 4) - current_row += 1 - - # 移除碰撞按钮(使用现代化样式) - self.collision_button = self.createModernButton("移除碰撞") - self.collision_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - self.collision_button.setStyleSheet(self._collision_button_style()) - self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) - collision_layout.addWidget(self.collision_button, current_row, 0, 1, 4) - else: - # 如果没有碰撞,设置默认选择并允许编辑 - self.collision_shape_combo.setCurrentText("球形 (Sphere)") - self.collision_shape_combo.setEnabled(True) - - # 清理之前的参数控件 - self._clearCollisionParameterControls() - - # 隐藏显示/隐藏按钮(如果存在) - if hasattr(self, 'collision_visibility_button'): - self.collision_visibility_button.setVisible(False) - - # 添加碰撞按钮(使用现代化样式) - self.collision_button = self.createModernButton("添加碰撞") - self.collision_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - self.collision_button.setStyleSheet(self._collision_button_style()) - self.collision_button.clicked.connect(lambda: self._addCollisionAndUpdate(model)) - collision_layout.addWidget(self.collision_button, current_row, 0, 1, 4) - collision_group.setLayout(collision_layout) - self._propertyLayout.addWidget(collision_group) - - # 同步一次状态,确保主按钮与可见性按钮齐全 - try: - self._updateCollisionPanelState(model) - except Exception: - pass - - except Exception as e: - print(f"创建碰撞面板失败: {e}") - import traceback - traceback.print_exc() - - def _addCollisionParameterControls(self, model, layout, start_row, shape_type): - """添加碰撞参数调整控件""" - try: - current_row = start_row - - # 位置调整控件(所有类型都有) - pos_label = QLabel("位置偏移:") - pos_label.setAlignment(Qt.AlignLeft | Qt.AlignTop) - pos_label.setVisible(True) # 确保可见 - - # X, Y, Z 位置调整 - self.collision_pos_x = self._createCollisionSpinBox(-100, 100, 2) - self.collision_pos_y = self._createCollisionSpinBox(-100, 100, 2) - self.collision_pos_z = self._createCollisionSpinBox(-100, 100, 2) - - for spinbox in (self.collision_pos_x, self.collision_pos_y, self.collision_pos_z): - spinbox.setMinimumWidth(140) - spinbox.setVisible(True) - - axis_controls = [ - ("X:", self.collision_pos_x), - ("Y:", self.collision_pos_y), - ("Z:", self.collision_pos_z), - ] - - for offset, (axis_text, spinbox) in enumerate(axis_controls): - row = current_row + offset - axis_layout = QHBoxLayout() - if offset == 0: - layout.addWidget(pos_label, row, 0) - # axis_label.addWidget(pos_label) - else: - spacer = QLabel("") - spacer.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) - spacer.setVisible(True) - layout.addWidget(spacer, row, 0) - # axis_layout.addWidget(spacer) - - axis_label = QLabel(axis_text) - axis_label.setAlignment(Qt.AlignVCenter) - axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - axis_layout.addWidget(axis_label) - axis_layout.addWidget(spinbox) - # layout.addWidget(axis_label, row, 1) - # layout.addWidget(spinbox, row, 2, 1, 2) - layout.addLayout(axis_layout, row, 1, 1, 3) - - current_row += len(axis_controls) - - # 连接位置变化信号 - self.collision_pos_x.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'x', v)) - self.collision_pos_y.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'y', v)) - self.collision_pos_z.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'z', v)) - - # 根据形状类型添加特定参数 - if shape_type == 'sphere': - current_row = self._addSphereParameters(model, layout, current_row) - elif shape_type == 'box': - current_row = self._addBoxParameters(model, layout, current_row) - elif shape_type == 'capsule': - current_row = self._addCapsuleParameters(model, layout, current_row) - elif shape_type == 'plane': - current_row = self._addPlaneParameters(model, layout, current_row) - - # 获取并设置当前参数值 - self._loadCurrentCollisionParameters(model, shape_type) - - return current_row - - except Exception as e: - print(f"添加碰撞参数控件失败: {e}") - return start_row - - def _createCollisionSpinBox(self, min_val, max_val, decimals=2): - """创建碰撞参数调整用的SpinBox""" - spinbox = QDoubleSpinBox() - spinbox.setRange(min_val, max_val) - spinbox.setDecimals(decimals) - spinbox.setSingleStep(0.01) - return spinbox - - def _addSphereParameters(self, model, layout, start_row): - """添加球形碰撞参数""" - current_row = start_row - - # 半径调整 - radius_label = QLabel("半径:") - layout.addWidget(radius_label, current_row, 0) - # layout.addWidget(QLabel(""), current_row, 1) - - self.collision_radius = self._createCollisionSpinBox(0.01, 100000, 2) - - # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 - if not self._hasCollision(model): - # 设置基于模型变换后尺寸的默认值 - if hasattr(self.world, 'collision_manager'): - transformed_info = self.world.collision_manager._getTransformedModelInfo(model) - if transformed_info: - default_radius = transformed_info['radius'] - self.collision_radius.setValue(default_radius) - else: - # 回退到原始包围盒 - bounds = model.getBounds() - if not bounds.isEmpty(): - default_radius = bounds.getRadius() - self.collision_radius.setValue(default_radius) - - self.collision_radius.valueChanged.connect(lambda v: self._updateSphereRadius(model, v)) - layout.addWidget(self.collision_radius, current_row, 1, 1, 3) - current_row += 1 - - return current_row - - def _addBoxParameters(self, model, layout, start_row): - """添加盒型碰撞参数""" - current_row = start_row - - size_label = QLabel("尺寸:") - layout.addWidget(size_label, current_row, 0) - # layout.addWidget(QLabel(""), current_row, 1) - # current_row += 1 - - # 宽度、长度、高度 - self.collision_width = self._createCollisionSpinBox(0.001, 100000, 2) - self.collision_length = self._createCollisionSpinBox(0.001, 100000, 2) - self.collision_height = self._createCollisionSpinBox(0.001, 100000, 2) - - # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 - if not self._hasCollision(model): - # 设置基于模型变换后尺寸的默认值 - if hasattr(self.world, 'collision_manager'): - transformed_info = self.world.collision_manager._getTransformedModelInfo(model) - if transformed_info: - actual_size = transformed_info['size'] - self.collision_width.setValue(actual_size.x) - self.collision_length.setValue(actual_size.y) - self.collision_height.setValue(actual_size.z) - else: - # 回退到原始包围盒 - bounds = model.getBounds() - if not bounds.isEmpty(): - model_size = bounds.getMax() - bounds.getMin() - self.collision_width.setValue(model_size.x) - self.collision_length.setValue(model_size.y) - self.collision_height.setValue(model_size.z) - - box_controls = [ - ("宽度:", self.collision_width), - ("长度:", self.collision_length), - ("高度:", self.collision_height), - ] - - for offset, (text, spinbox) in enumerate(box_controls): - text_layout = QHBoxLayout() - if offset == 0: - layout.addWidget(size_label, current_row, 0) - else: - spacer = QLabel("") - spacer.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) - spacer.setVisible(True) - layout.addWidget(spacer, current_row, 0) - # placeholder = QLabel("") - # layout.addWidget(placeholder, current_row, 0) - - axis_label = QLabel(text) - axis_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) - axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - text_layout.addWidget(axis_label) - text_layout.addWidget(spinbox) - layout.addLayout(text_layout, current_row, 1, 1, 3) - # layout.addWidget(axis_label, current_row, 1) - # layout.addWidget(spinbox, current_row, 2) - current_row += 1 - - # 连接信号 - self.collision_width.valueChanged.connect(lambda v: self._updateBoxSize(model, 'width', v)) - self.collision_length.valueChanged.connect(lambda v: self._updateBoxSize(model, 'length', v)) - self.collision_height.valueChanged.connect(lambda v: self._updateBoxSize(model, 'height', v)) - - return current_row - - def _addCapsuleParameters(self, model, layout, start_row): - """添加胶囊体碰撞参数""" - current_row = start_row - - # 半径和高度 - radius_label = QLabel("半径:") - layout.addWidget(radius_label, current_row, 0) - # layout.addWidget(QLabel(""), current_row, 1) - - self.collision_capsule_radius = self._createCollisionSpinBox(0.01, 100000, 2) - - # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 - if not self._hasCollision(model): - # 设置基于模型变换后尺寸的默认值 - if hasattr(self.world, 'collision_manager'): - transformed_info = self.world.collision_manager._getTransformedModelInfo(model) - if transformed_info: - actual_size = transformed_info['size'] - # 更合理的默认半径:基于变换后模型宽度的平均值 - default_radius = min(actual_size.x, actual_size.y) / 2.5 - self.collision_capsule_radius.setValue(default_radius) - else: - # 回退到原始包围盒 - bounds = model.getBounds() - if not bounds.isEmpty(): - model_size = bounds.getMax() - bounds.getMin() - # 更合理的默认半径:基于模型宽度的平均值 - default_radius = min(model_size.x, model_size.y) / 2.5 - self.collision_capsule_radius.setValue(default_radius) - - self.collision_capsule_radius.valueChanged.connect(lambda v: self._updateCapsuleRadius(model, v)) - layout.addWidget(self.collision_capsule_radius, current_row, 1, 1, 3) - current_row += 1 - - height_label = QLabel("高度:") - layout.addWidget(height_label, current_row, 0) - # layout.addWidget(QLabel(""), current_row, 1) - - self.collision_capsule_height = self._createCollisionSpinBox(0.01, 10000, 2) - - # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 - if not self._hasCollision(model): - # 设置基于模型变换后高度的默认值 - if hasattr(self.world, 'collision_manager'): - transformed_info = self.world.collision_manager._getTransformedModelInfo(model) - if transformed_info: - actual_size = transformed_info['size'] - self.collision_capsule_height.setValue(actual_size.z) - else: - # 回退到原始包围盒 - bounds = model.getBounds() - if not bounds.isEmpty(): - model_size = bounds.getMax() - bounds.getMin() - self.collision_capsule_height.setValue(model_size.z) - - self.collision_capsule_height.valueChanged.connect(lambda v: self._updateCapsuleHeight(model, v)) - layout.addWidget(self.collision_capsule_height, current_row, 1, 1, 3) - current_row += 1 - - return current_row - - def _addPlaneParameters(self, model, layout, start_row): - """添加平面碰撞参数""" - current_row = start_row - - # 法向量 - normal_label = QLabel("法向量:") - layout.addWidget(normal_label, current_row, 0) - # layout.addWidget(QLabel(""), current_row, 1) - # current_row += 1 - - self.collision_normal_x = self._createCollisionSpinBox(-1, 1, 2) - self.collision_normal_y = self._createCollisionSpinBox(-1, 1, 2) - self.collision_normal_z = self._createCollisionSpinBox(-1, 1, 2) - - # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 - if not self._hasCollision(model): - # 设置默认法向量(向上) - self.collision_normal_x.setValue(0.0) - self.collision_normal_y.setValue(0.0) - self.collision_normal_z.setValue(1.0) - - # placeholder_nx = QLabel("") - # layout.addWidget(placeholder_nx, current_row, 0) - - hbox_nx = QHBoxLayout() - axis_label_nx = QLabel("Nx:") - axis_label_nx.setAlignment(Qt.AlignRight | Qt.AlignVCenter) - axis_label_nx.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - hbox_nx.addWidget(axis_label_nx) - hbox_nx.addWidget(self.collision_normal_x) - layout.addLayout(hbox_nx, current_row, 1, 1, 3) - current_row += 1 - - placeholder_ny = QLabel("") - layout.addWidget(placeholder_ny, current_row, 0) - - hbox_ny = QHBoxLayout() - axis_label_ny = QLabel("Ny:") - axis_label_ny.setAlignment(Qt.AlignRight | Qt.AlignVCenter) - axis_label_ny.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - hbox_ny.addWidget(axis_label_ny) - hbox_ny.addWidget(self.collision_normal_y) - layout.addLayout(hbox_ny, current_row, 1, 1, 3) - current_row += 1 - - placeholder_nz = QLabel("") - layout.addWidget(placeholder_nz, current_row, 0) - - hbox_nz = QHBoxLayout() - axis_label_nz = QLabel("Nz:") - axis_label_nz.setAlignment(Qt.AlignRight | Qt.AlignVCenter) - axis_label_nz.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - hbox_nz.addWidget(axis_label_nz) - hbox_nz.addWidget(self.collision_normal_z) - layout.addLayout(hbox_nz, current_row, 1, 1, 3) - current_row += 1 - - # 连接信号 - self.collision_normal_x.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'x', v)) - self.collision_normal_y.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'y', v)) - self.collision_normal_z.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'z', v)) - - return current_row - - def _hasCollision(self, model): - """检查模型是否已有碰撞体""" - try: - from panda3d.core import CollisionNode - - # 检查模型及其子节点是否有碰撞节点 - collision_nodes = model.findAllMatches("**/+CollisionNode") - has_collision = collision_nodes.getNumPaths() > 0 - - print( - f"碰撞检查:模型 {model.getName()} - {'有' if has_collision else '无'}碰撞 (找到{collision_nodes.getNumPaths()}个碰撞节点)") - - return has_collision - except Exception as e: - print(f"检查碰撞失败: {e}") - return False - - def _isCollisionVisible(self, model): - """检查碰撞是否可见""" - try: - collision_nodes = model.findAllMatches("**/+CollisionNode") - for collision_np in collision_nodes: - # 检查碰撞节点是否隐藏 - return not collision_np.isHidden() - return False - except Exception as e: - print(f"检查碰撞可见性失败: {e}") - return False - - def _toggleCollisionVisibility(self, model): - """切换碰撞可见性""" - try: - collision_nodes = model.findAllMatches("**/+CollisionNode") - is_visible = self._isCollisionVisible(model) - - for collision_np in collision_nodes: - if is_visible: - collision_np.hide() - print(f"隐藏碰撞:{model.getName()}") - else: - collision_np.show() - print(f"显示碰撞:{model.getName()}") - - # 立即更新按钮状态,并同步更新面板(确保缺失主按钮时补建) - self._updateCollisionVisibilityButton(model) - self._updateCollisionPanelState(model) - - except Exception as e: - print(f"切换碰撞可见性失败: {e}") - - def _updateCollisionVisibilityButton(self, model): - """更新碰撞可见性按钮状态""" - try: - if hasattr(self, 'collision_visibility_button'): - is_visible = self._isCollisionVisible(model) - button_text = "隐藏碰撞" if is_visible else "显示碰撞" - self.collision_visibility_button.setText(button_text) - - # 应用统一按钮样式 - self.collision_visibility_button.setStyleSheet(self._collision_button_style()) - - print(f"更新可见性按钮:{model.getName()} - {'可见' if is_visible else '隐藏'}") - except Exception as e: - print(f"更新碰撞可见性按钮失败: {e}") - - def _getCurrentCollisionShape(self, model): - """获取当前模型的碰撞形状类型""" - try: - from panda3d.core import CollisionNode, CollisionSphere, CollisionBox, CollisionCapsule, CollisionPlane - - collision_nodes = model.findAllMatches("**/+CollisionNode") - for collision_np in collision_nodes: - collision_node = collision_np.node() - if collision_node.getNumSolids() > 0: - solid = collision_node.getSolid(0) - solid_type = type(solid).__name__ - - if solid_type == "CollisionSphere": - return "sphere" - elif solid_type == "CollisionBox": - return "box" - elif solid_type == "CollisionCapsule": - return "capsule" - elif solid_type == "CollisionPlane": - return "plane" - - return "sphere" # 默认返回球形 - except Exception as e: - print(f"获取碰撞形状失败: {e}") - return "sphere" - - def _setComboToShape(self, shape_type): - """根据形状类型设置下拉框选择""" - shape_map = { - "sphere": "球形 (Sphere)", - "box": "盒型 (Box)", - "capsule": "胶囊体 (Capsule)", - "plane": "平面 (Plane)", - "auto": "自动选择 (Auto)" - } - - if shape_type in shape_map: - self.collision_shape_combo.setCurrentText(shape_map[shape_type]) - else: - self.collision_shape_combo.setCurrentText("球形 (Sphere)") - - def _getSelectedCollisionShape(self): - """获取选中的碰撞形状类型""" - if hasattr(self, 'collision_shape_combo'): - shape_text = self.collision_shape_combo.currentText() - # 从显示文本中提取形状类型 - if "球形" in shape_text: - return 'sphere' - elif "盒型" in shape_text: - return 'box' - elif "胶囊体" in shape_text: - return 'capsule' - elif "平面" in shape_text: - return 'plane' - elif "自动选择" in shape_text: - return 'auto' - return 'sphere' # 默认返回球形 - - def _addCollisionAndUpdate(self, model): - """添加指定形状的碰撞体并更新界面""" - try: - # 防止重复调用 - if getattr(self, '_adding_collision', False): - print("正在添加碰撞,跳过重复调用") - return - - self._adding_collision = True - - # 初始化加载参数标志位 - if not hasattr(self, '_loading_collision_params'): - self._loading_collision_params = False - - if hasattr(self.world, 'scene_manager'): - # 获取选中的碰撞形状 - shape_type = self._getSelectedCollisionShape() - - # 参考scene_manager的setupCollision方法 - from panda3d.core import CollisionNode, BitMask32 - - # 创建碰撞节点 - cNode = CollisionNode(f'modelCollision_{model.getName()}') - - # 设置碰撞掩码 - cNode.setIntoCollideMask(BitMask32.bit(2)) # 用于鼠标选择 - - # 如果启用了模型间碰撞检测,添加额外的掩码 - if (hasattr(self.world, 'collision_manager') and - hasattr(self.world.collision_manager, 'model_collision_enabled') and - self.world.collision_manager.model_collision_enabled): - current_mask = cNode.getIntoCollideMask() - model_collision_mask = BitMask32.bit(6) # MODEL_COLLISION - cNode.setIntoCollideMask(current_mask | model_collision_mask) - - # 创建指定形状的碰撞体 - if hasattr(self.world, 'collision_manager'): - collision_shape = self.world.collision_manager.createCollisionShape(model, shape_type) - else: - # 回退方案:创建简单球体 - from panda3d.core import CollisionSphere, Point3 - bounds = model.getBounds() - if bounds.isEmpty(): - collision_shape = CollisionSphere(Point3(0, 0, 0), 1.0) - else: - center = bounds.getCenter() - radius = bounds.getRadius() - if radius <= 0: - radius = 1.0 - collision_shape = CollisionSphere(center, radius) - - cNode.addSolid(collision_shape) - - # 将碰撞节点附加到模型上 - cNodePath = model.attachNewNode(cNode) - - # 根据调试设置决定是否显示碰撞体 - if hasattr(self.world, 'debug_collision') and self.world.debug_collision: - cNodePath.show() - else: - cNodePath.hide() - - # 为模型添加碰撞相关标签 - model.setTag("has_collision", "true") - model.setTag("collision_shape", shape_type) - if 'radius' in locals(): - model.setTag("collision_radius", str(radius)) - - print(f"✅ 为模型 {model.getName()} 添加了 {shape_type} 碰撞体") - - # 重置更新标志,确保状态能够更新 - self._updating_collision_panel = False - - # 更新面板状态 - 添加参数控件和显示/隐藏按钮 - self._updateCollisionPanelState(model) - - else: - print("场景管理器未初始化") - - except Exception as e: - print(f"添加碰撞失败: {e}") - import traceback - traceback.print_exc() - finally: - # 确保标志被重置 - self._adding_collision = False - - def _removeCollisionAndUpdate(self, model): - """移除模型的碰撞体并更新界面""" - try: - # 查找并移除碰撞节点 - collision_nodes = model.findAllMatches("**/+CollisionNode") - for collision_np in collision_nodes: - collision_np.removeNode() - - print(f"移除了模型 {model.getName()} 的碰撞体") - - # 重置状态并更新界面 - self._previous_collision_state = True # 强制刷新 - self._updating_collision_panel = False # 确保状态能够更新 - self._updateCollisionPanelState(model) - - except Exception as e: - print(f"移除碰撞失败: {e}") - - def _findButtonRow(self, layout): - """查找按钮应该在的行数""" - # 简单返回一个合适的行数,基于布局中现有的项目数 - row_count = 0 - for i in range(layout.count()): - item = layout.itemAt(i) - if item and item.widget(): - row, col, rowspan, colspan = layout.getItemPosition(i) - row_count = max(row_count, row + 1) - return row_count - - def _updateCollisionPanelState(self, model): - """更新碰撞面板状态""" - try: - # 防止重复调用 - if getattr(self, '_updating_collision_panel', False): - return - - self._updating_collision_panel = True - print("-------------------------------------") - if hasattr(self, 'collision_button') and hasattr(self, 'collision_status_badge') and hasattr(self, - 'collision_shape_combo'): - has_collision = self._hasCollision(model) - print(f"模型 {model.getName()} 是否有碰撞体: {has_collision}-------------------------------------------------") - - # 更新状态徽章(直接修改现有控件,避免布局冲突) - if hasattr(self, 'collision_status_badge') and self.collision_status_badge: - if has_collision: - self.collision_status_badge.setText("已启用") - # 使用固定宽度的绿色样式,保持尺寸一致 - self.collision_status_badge.setStyleSheet(self.badge_style_green_fixed) - else: - self.collision_status_badge.setText("未启用") - # 使用固定宽度的红色样式,保持尺寸一致 - self.collision_status_badge.setStyleSheet(self.badge_style_red_fixed) - # 触发重绘,确保立即可见 - self.collision_status_badge.update() - - if has_collision: - # 有碰撞:显示移除按钮,下拉框变为只读并显示当前类型 - if hasattr(self, 'collision_button'): - # 更新按钮文本和样式(简单方法:直接设置文本和样式) - self.collision_button.setText("移除碰撞") - # 应用统一按钮样式 - self.collision_button.setStyleSheet(self._collision_button_style()) - - # 重新连接信号 - try: - self.collision_button.clicked.disconnect() - except: - pass - self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) - else: - # 若不存在主按钮,则创建“移除碰撞”按钮并加入布局 - try: - self.collision_button = self.createModernButton("移除碰撞") - self.collision_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - self.collision_button.setStyleSheet(self._collision_button_style()) - self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) - if hasattr(self, 'collision_layout') and self.collision_layout is not None: - row = self.collision_layout.rowCount() - self.collision_layout.addWidget(self.collision_button, row, 0, 1, 4) - except Exception as _e: - print(f"创建移除碰撞按钮失败: {_e}") - - # 获取并显示当前碰撞类型,设置为只读 - current_shape = self._getCurrentCollisionShape(model) - self._setComboToShape(current_shape) - self.collision_shape_combo.setEnabled(False) - - # 确保参数控件存在 - 只在没有参数控件时才添加 - if not hasattr(self, 'collision_pos_x'): - print("添加碰撞参数控件") - self._addParameterControlsToExistingPanel(model, current_shape) - - # 显示/隐藏按钮状态更新 - if not hasattr(self, 'collision_visibility_button'): - # 创建可见性按钮 - self._addVisibilityButtonToExistingPanel(model) - else: - self.collision_visibility_button.setVisible(True) - self._updateCollisionVisibilityButton(model) - - # 确保按钮顺序:可见性按钮在上,主按钮在下 - if hasattr(self, 'collision_layout'): - try: - self._repositionButtons(self.collision_layout.rowCount()) - except Exception: - pass - - print(f"碰撞面板状态更新:有碰撞 - {current_shape}") - - else: - # 无碰撞:显示添加按钮,下拉框变为可编辑 - if hasattr(self, 'collision_button'): - self.collision_button.setText("添加碰撞") - # 应用统一按钮样式 - self.collision_button.setStyleSheet(self._collision_button_style()) - - # 先断开所有连接,再重新连接 - try: - self.collision_button.clicked.disconnect() - except: - pass - self.collision_button.clicked.connect(lambda: self._addCollisionAndUpdate(model)) - else: - # 若不存在主按钮,则创建“添加碰撞”按钮并加入布局 - try: - self.collision_button = self.createModernButton("添加碰撞") - self.collision_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - self.collision_button.setStyleSheet(self._collision_button_style()) - self.collision_button.clicked.connect(lambda: self._addCollisionAndUpdate(model)) - if hasattr(self, 'collision_layout') and self.collision_layout is not None: - row = self.collision_layout.rowCount() - self.collision_layout.addWidget(self.collision_button, row, 0, 1, 4) - except Exception as _e: - print(f"创建添加碰撞按钮失败: {_e}") - - # 恢复为可编辑状态 - self.collision_shape_combo.setEnabled(True) - - # 隐藏并清理参数控件 - 只在有参数控件时才清理 - if hasattr(self, 'collision_pos_x'): - print("清理碰撞参数控件") - self._hideCollisionParameterControls() - - # 隐藏显示/隐藏按钮 - if hasattr(self, 'collision_visibility_button'): - self.collision_visibility_button.setVisible(False) - - # 确保按钮顺序:主按钮位于底部 - if hasattr(self, 'collision_layout'): - try: - self._repositionButtons(self.collision_layout.rowCount()) - except Exception: - pass - - print(f"碰撞面板状态更新:无碰撞 - 可编辑") - - except Exception as e: - print(f"更新碰撞面板状态失败: {e}") - import traceback - traceback.print_exc() - finally: - # 确保标志被重置 - self._updating_collision_panel = False - - def _loadCurrentCollisionParameters(self, model, shape_type): - """加载当前碰撞参数到界面""" - try: - # 设置标志位,防止在加载参数时触发更新 - self._loading_collision_params = True - - collision_nodes = model.findAllMatches("**/+CollisionNode") - for collision_np in collision_nodes: - collision_node = collision_np.node() - if collision_node.getNumSolids() > 0: - solid = collision_node.getSolid(0) - - # 从碰撞体形状中提取位置偏移 - if hasattr(self, 'collision_pos_x'): - # 获取模型的实际中心(考虑变换) - if hasattr(self.world, 'collision_manager'): - transformed_info = self.world.collision_manager._getTransformedModelInfo(model) - if transformed_info: - model_center = transformed_info['center'] - else: - model_center = model.getBounds().getCenter() if not model.getBounds().isEmpty() else Point3(0, 0, 0) - else: - model_center = model.getBounds().getCenter() if not model.getBounds().isEmpty() else Point3(0, 0, 0) - - # 获取碰撞体的中心 - collision_center = self._getCollisionShapeCenter(solid) - if collision_center: - # 计算偏移:碰撞体中心 - 模型中心 - offset_x = collision_center.x - model_center.x - offset_y = collision_center.y - model_center.y - offset_z = collision_center.z - model_center.z - self.collision_pos_x.setValue(offset_x) - self.collision_pos_y.setValue(offset_y) - self.collision_pos_z.setValue(offset_z) - print(f"加载位置偏移: ({offset_x:.2f}, {offset_y:.2f}, {offset_z:.2f})") - else: - # 如果无法计算偏移,设置为0 - self.collision_pos_x.setValue(0.0) - self.collision_pos_y.setValue(0.0) - self.collision_pos_z.setValue(0.0) - print("无法计算位置偏移,设置为0") - - if shape_type == 'sphere': - self._loadSphereParameters(solid) - elif shape_type == 'box': - self._loadBoxParameters(solid) - elif shape_type == 'capsule': - self._loadCapsuleParameters(solid) - elif shape_type == 'plane': - self._loadPlaneParameters(solid) - break - - except Exception as e: - print(f"加载碰撞参数失败: {e}") - finally: - # 重置标志位 - self._loading_collision_params = False - - def _getCollisionShapeCenter(self, solid): - """从碰撞体形状中获取中心点""" - try: - from panda3d.core import CollisionSphere, CollisionBox, CollisionCapsule, CollisionPlane, Point3 - - if isinstance(solid, CollisionSphere): - return solid.getCenter() - elif isinstance(solid, CollisionBox): - # 盒子的中心是最小点和最大点的中点 - min_pt = solid.getMin() - max_pt = solid.getMax() - return Point3((min_pt.x + max_pt.x) * 0.5, - (min_pt.y + max_pt.y) * 0.5, - (min_pt.z + max_pt.z) * 0.5) - elif isinstance(solid, CollisionCapsule): - # 胶囊体的中心是两个端点的中点 - point_a = solid.getPointA() - point_b = solid.getPointB() - return Point3((point_a.x + point_b.x) * 0.5, - (point_a.y + point_b.y) * 0.5, - (point_a.z + point_b.z) * 0.5) - elif isinstance(solid, CollisionPlane): - # 平面没有明确的中心,返回平面上的一个点 - plane = solid.getPlane() - return plane.getPoint() - else: - return None - except Exception as e: - print(f"获取碰撞体中心失败: {e}") - return None - - def _loadSphereParameters(self, solid): - """加载球形参数""" - try: - from panda3d.core import CollisionSphere - if isinstance(solid, CollisionSphere): - radius = solid.getRadius() - self.collision_radius.setValue(radius) - except Exception as e: - print(f"加载球形参数失败: {e}") - - def _loadBoxParameters(self, solid): - """加载盒型参数""" - try: - from panda3d.core import CollisionBox - if isinstance(solid, CollisionBox): - min_point = solid.getMin() - max_point = solid.getMax() - width = max_point.x - min_point.x - length = max_point.y - min_point.y - height = max_point.z - min_point.z - - self.collision_width.setValue(width) - self.collision_length.setValue(length) - self.collision_height.setValue(height) - except Exception as e: - print(f"加载盒型参数失败: {e}") - - def _loadCapsuleParameters(self, solid): - """加载胶囊体参数""" - try: - from panda3d.core import CollisionCapsule - if isinstance(solid, CollisionCapsule): - radius = solid.getRadius() - point_a = solid.getPointA() - point_b = solid.getPointB() - height = (point_b - point_a).length() - - self.collision_capsule_radius.setValue(radius) - self.collision_capsule_height.setValue(height) - except Exception as e: - print(f"加载胶囊体参数失败: {e}") - - def _loadPlaneParameters(self, solid): - """加载平面参数""" - try: - from panda3d.core import CollisionPlane - if isinstance(solid, CollisionPlane): - plane = solid.getPlane() - normal = plane.getNormal() - - self.collision_normal_x.setValue(normal.x) - self.collision_normal_y.setValue(normal.y) - self.collision_normal_z.setValue(normal.z) - except Exception as e: - print(f"加载平面参数失败: {e}") - - def _updateCollisionPosition(self, model, axis, value): - """更新碰撞位置偏移""" - try: - # 防止重复调用导致无限循环,以及在加载参数时防止更新 - if getattr(self, '_updating_collision_position', False) or getattr(self, '_loading_collision_params', False): - return - self._updating_collision_position = True - - # 获取当前所有位置偏移值 - pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 - pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 - pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 - - # 获取当前碰撞形状类型 - current_shape = self._getCurrentCollisionShape(model) - if not current_shape: - return - - # 重新创建碰撞体,传入位置偏移 - from panda3d.core import Vec3 - position_offset = Vec3(pos_x, pos_y, pos_z) - - # 根据形状类型收集其他参数 - if current_shape == 'sphere': - radius = self.collision_radius.value() if hasattr(self, 'collision_radius') else 1.0 - self._recreateCollisionShape(model, current_shape, radius=radius, position_offset=position_offset) - elif current_shape == 'box': - width = self.collision_width.value() if hasattr(self, 'collision_width') else 2.0 - length = self.collision_length.value() if hasattr(self, 'collision_length') else 2.0 - height = self.collision_height.value() if hasattr(self, 'collision_height') else 2.0 - self._recreateCollisionShape(model, current_shape, width=width, length=length, height=height, position_offset=position_offset) - elif current_shape == 'capsule': - cap_radius = self.collision_capsule_radius.value() if hasattr(self, 'collision_capsule_radius') else 0.5 - cap_height = self.collision_capsule_height.value() if hasattr(self, 'collision_capsule_height') else 2.0 - self._recreateCollisionShape(model, current_shape, radius=cap_radius, height=cap_height, position_offset=position_offset) - elif current_shape == 'plane': - normal_x = self.collision_normal_x.value() if hasattr(self, 'collision_normal_x') else 0 - normal_y = self.collision_normal_y.value() if hasattr(self, 'collision_normal_y') else 0 - normal_z = self.collision_normal_z.value() if hasattr(self, 'collision_normal_z') else 1 - self._recreateCollisionShape(model, current_shape, normal=(normal_x, normal_y, normal_z), position_offset=position_offset) - - print(f"更新碰撞位置偏移 {axis}: {value}") - - except Exception as e: - print(f"更新碰撞位置失败: {e}") - finally: - self._updating_collision_position = False - - def _updateSphereRadius(self, model, radius): - """更新球形半径""" - try: - # 防止重复调用导致无限循环,以及在加载参数时防止更新 - if getattr(self, '_updating_sphere_radius', False) or getattr(self, '_loading_collision_params', False): - return - self._updating_sphere_radius = True - - # 获取当前位置偏移 - from panda3d.core import Vec3 - pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 - pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 - pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 - position_offset = Vec3(pos_x, pos_y, pos_z) - - self._recreateCollisionShape(model, 'sphere', radius=radius, position_offset=position_offset) - print(f"更新球形半径: {radius}") - except Exception as e: - print(f"更新球形半径失败: {e}") - finally: - self._updating_sphere_radius = False - - def _updateBoxSize(self, model, dimension, value): - """更新盒型尺寸""" - try: - # 防止重复调用导致无限循环,以及在加载参数时防止更新 - if getattr(self, '_updating_box_size', False) or getattr(self, '_loading_collision_params', False): - return - self._updating_box_size = True - - # 获取当前所有尺寸 - width = self.collision_width.value() if hasattr(self, 'collision_width') else value - length = self.collision_length.value() if hasattr(self, 'collision_length') else value - height = self.collision_height.value() if hasattr(self, 'collision_height') else value - - # 获取当前位置偏移 - from panda3d.core import Vec3 - pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 - pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 - pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 - position_offset = Vec3(pos_x, pos_y, pos_z) - - self._recreateCollisionShape(model, 'box', width=width, length=length, height=height, position_offset=position_offset) - print(f"更新盒型{dimension}: {value}") - except Exception as e: - print(f"更新盒型尺寸失败: {e}") - finally: - self._updating_box_size = False - - def _updateCapsuleRadius(self, model, radius): - """更新胶囊体半径""" - try: - # 防止重复调用导致无限循环,以及在加载参数时防止更新 - if getattr(self, '_updating_capsule_radius', False) or getattr(self, '_loading_collision_params', False): - return - self._updating_capsule_radius = True - - height = self.collision_capsule_height.value() if hasattr(self, 'collision_capsule_height') else 2.0 - - # 获取当前位置偏移 - from panda3d.core import Vec3 - pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 - pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 - pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 - position_offset = Vec3(pos_x, pos_y, pos_z) - - self._recreateCollisionShape(model, 'capsule', radius=radius, height=height, position_offset=position_offset) - print(f"更新胶囊体半径: {radius}") - except Exception as e: - print(f"更新胶囊体半径失败: {e}") - finally: - self._updating_capsule_radius = False - - def _updateCapsuleHeight(self, model, height): - """更新胶囊体高度""" - try: - # 防止重复调用导致无限循环,以及在加载参数时防止更新 - if getattr(self, '_updating_capsule_height', False) or getattr(self, '_loading_collision_params', False): - return - self._updating_capsule_height = True - - radius = self.collision_capsule_radius.value() if hasattr(self, 'collision_capsule_radius') else 0.5 - - # 获取当前位置偏移 - from panda3d.core import Vec3 - pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 - pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 - pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 - position_offset = Vec3(pos_x, pos_y, pos_z) - - self._recreateCollisionShape(model, 'capsule', radius=radius, height=height, position_offset=position_offset) - print(f"更新胶囊体高度: {height}") - except Exception as e: - print(f"更新胶囊体高度失败: {e}") - finally: - self._updating_capsule_height = False - - def _updatePlaneNormal(self, model, axis, value): - """更新平面法向量""" - try: - # 防止重复调用导致无限循环,以及在加载参数时防止更新 - if getattr(self, '_updating_plane_normal', False) or getattr(self, '_loading_collision_params', False): - return - self._updating_plane_normal = True - - # 获取当前法向量 - normal_x = self.collision_normal_x.value() if hasattr(self, 'collision_normal_x') else 0 - normal_y = self.collision_normal_y.value() if hasattr(self, 'collision_normal_y') else 0 - normal_z = self.collision_normal_z.value() if hasattr(self, 'collision_normal_z') else 1 - - # 获取当前位置偏移 - from panda3d.core import Vec3 - pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 - pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 - pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 - position_offset = Vec3(pos_x, pos_y, pos_z) - - self._recreateCollisionShape(model, 'plane', normal=(normal_x, normal_y, normal_z), position_offset=position_offset) - print(f"更新平面法向量 {axis}: {value}") - except Exception as e: - print(f"更新平面法向量失败: {e}") - finally: - self._updating_plane_normal = False - - def _recreateCollisionShape(self, model, shape_type, **kwargs): - """重新创建碰撞形状(保持可见性)""" - try: - # 保存当前状态 - collision_nodes = model.findAllMatches("**/+CollisionNode") - if not collision_nodes: - return - - collision_np = collision_nodes[0] - is_visible = not collision_np.isHidden() - - # 移除旧的碰撞体 - collision_np.removeNode() - - # 创建新的碰撞体 - from panda3d.core import CollisionNode, BitMask32 - cNode = CollisionNode(f'modelCollision_{model.getName()}') - cNode.setIntoCollideMask(BitMask32.bit(2)) - - # 创建新形状(位置偏移已经烘焙在形状中) - if hasattr(self.world, 'collision_manager'): - collision_shape = self.world.collision_manager.createCollisionShape(model, shape_type, **kwargs) - else: - # 回退方案 - from panda3d.core import CollisionSphere, Point3 - collision_shape = CollisionSphere(Point3(0, 0, 0), kwargs.get('radius', 1.0)) - - cNode.addSolid(collision_shape) - - # 重新附加(不设置额外位置,因为位置偏移已经在形状中) - new_collision_np = model.attachNewNode(cNode) - # 碰撞节点默认位置为 (0, 0, 0),位置偏移通过形状几何体处理 - new_collision_np.setPos(0, 0, 0) - - if is_visible: - new_collision_np.show() - else: - new_collision_np.hide() - - except Exception as e: - print(f"重新创建碰撞形状失败: {e}") - import traceback - traceback.print_exc() - - def _clearCollisionParameterControls(self): - """清理碰撞参数控件""" - try: - # 位置控件 - for attr in ['collision_pos_x', 'collision_pos_y', 'collision_pos_z']: - if hasattr(self, attr): - widget = getattr(self, attr) - if widget and widget.parent(): - widget.setParent(None) - widget.deleteLater() - delattr(self, attr) - - # 球形参数控件 - if hasattr(self, 'collision_radius'): - if self.collision_radius and self.collision_radius.parent(): - self.collision_radius.setParent(None) - self.collision_radius.deleteLater() - delattr(self, 'collision_radius') - - # 盒型参数控件 - for attr in ['collision_width', 'collision_length', 'collision_height']: - if hasattr(self, attr): - widget = getattr(self, attr) - if widget and widget.parent(): - widget.setParent(None) - widget.deleteLater() - delattr(self, attr) - - # 胶囊体参数控件 - for attr in ['collision_capsule_radius', 'collision_capsule_height']: - if hasattr(self, attr): - widget = getattr(self, attr) - if widget and widget.parent(): - widget.setParent(None) - widget.deleteLater() - delattr(self, attr) - - # 平面参数控件 - for attr in ['collision_normal_x', 'collision_normal_y', 'collision_normal_z']: - if hasattr(self, attr): - widget = getattr(self, attr) - if widget and widget.parent(): - widget.setParent(None) - widget.deleteLater() - delattr(self, attr) - - print("清理碰撞参数控件完成") - - except Exception as e: - print(f"清理碰撞参数控件失败: {e}") - - def _refreshCollisionPanel(self, model): - """刷新整个碰撞面板(简化版)""" - try: - print("使用简化的面板刷新") - # 直接调用状态更新,不删除面板 - self._updateCollisionPanelState(model) - - except Exception as e: - print(f"刷新碰撞面板失败: {e}") - import traceback - traceback.print_exc() - - def _addParameterControlsToExistingPanel(self, model, shape_type): - """向现有面板添加参数控件""" - try: - if not hasattr(self, 'collision_layout'): - return - - layout = self.collision_layout - - # 首先清理可能存在的旧控件 - self._hideCollisionParameterControls() - - # 找到插入位置(在按钮之前) - current_row = 2 - - # 位置调整控件 - pos_label = QLabel("位置偏移:") - pos_label.setAlignment(Qt.AlignLeft | Qt.AlignTop) - pos_label.setVisible(True) # 确保可见 - - # X, Y, Z 位置调整 - self.collision_pos_x = self._createCollisionSpinBox(-1000000, 1000000, 2) - self.collision_pos_y = self._createCollisionSpinBox(-1000000, 1000000, 2) - self.collision_pos_z = self._createCollisionSpinBox(-1000000, 1000000, 2) - - for spinbox in (self.collision_pos_x, self.collision_pos_y, self.collision_pos_z): - spinbox.setMinimumWidth(140) - spinbox.setVisible(True) - - axis_controls = [ - ("X:", self.collision_pos_x), - ("Y:", self.collision_pos_y), - ("Z:", self.collision_pos_z), - ] - - for offset, (axis_text, spinbox) in enumerate(axis_controls): - row = current_row + offset - axis_layout = QHBoxLayout() - if offset == 0: - layout.addWidget(pos_label, row, 0) - # axis_label.addWidget(pos_label) - else: - spacer = QLabel("") - spacer.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) - spacer.setVisible(True) - layout.addWidget(spacer, row, 0) - # axis_layout.addWidget(spacer) - - axis_label = QLabel(axis_text) - axis_label.setAlignment(Qt.AlignVCenter) - axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - axis_layout.addWidget(axis_label) - axis_layout.addWidget(spinbox) - # layout.addWidget(axis_label, row, 1) - # layout.addWidget(spinbox, row, 2, 1, 2) - layout.addLayout(axis_layout, row, 1, 1, 3) - - current_row += len(axis_controls) - - # 连接位置变化信号 - self.collision_pos_x.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'x', v)) - self.collision_pos_y.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'y', v)) - self.collision_pos_z.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'z', v)) - - # 根据形状类型添加特定参数 - if shape_type == 'sphere': - current_row = self._addSphereParametersToExisting(model, layout, current_row) - elif shape_type == 'box': - current_row = self._addBoxParametersToExisting(model, layout, current_row) - elif shape_type == 'capsule': - current_row = self._addCapsuleParametersToExisting(model, layout, current_row) - elif shape_type == 'plane': - current_row = self._addPlaneParametersToExisting(model, layout, current_row) - - # 重新定位按钮 - self._repositionButtons(current_row) - - # 加载参数值 - self._loadCurrentCollisionParameters(model, shape_type) - - except Exception as e: - print(f"添加参数控件到现有面板失败: {e}") - import traceback - traceback.print_exc() - - def _addSphereParametersToExisting(self, model, layout, start_row): - """向现有面板添加球形参数""" - current_row = start_row - - radius_label = QLabel("半径:") - radius_label.setVisible(True) - layout.addWidget(radius_label, current_row, 0) - layout.addWidget(QLabel(""), current_row, 1) - - self.collision_radius = self._createCollisionSpinBox(0.01, 10000, 2) - self.collision_radius.setVisible(True) - self.collision_radius.valueChanged.connect(lambda v: self._updateSphereRadius(model, v)) - layout.addWidget(self.collision_radius, current_row, 1, 1, 3) - current_row += 1 - - return current_row - - def _addBoxParametersToExisting(self, model, layout, start_row): - """向现有面板添加盒型参数""" - current_row = start_row - - size_label = QLabel("尺寸:") - size_label.setVisible(True) - layout.addWidget(size_label, current_row, 0) - # layout.addWidget(QLabel(""), current_row, 1) - # current_row += 1 - - self.collision_width = self._createCollisionSpinBox(0.01, 10000, 2) - self.collision_length = self._createCollisionSpinBox(0.01, 10000, 2) - self.collision_height = self._createCollisionSpinBox(0.01, 10000, 2) - - box_controls = [ - ("宽度:", self.collision_width), - ("长度:", self.collision_length), - ("高度:", self.collision_height), - ] - - for offset,(text, spinbox) in enumerate(box_controls): - axis_layout = QHBoxLayout() - if offset == 0: - layout.addWidget(size_label, current_row, 0) - else: - spacer = QLabel("") - spacer.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) - spacer.setVisible(True) - layout.addWidget(spacer, current_row, 0) - - axis_label = QLabel(text) - axis_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) - axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - axis_label.setVisible(True) - axis_layout.addWidget(axis_label) - spinbox.setVisible(True) - axis_layout.addWidget(spinbox) - layout.addLayout(axis_layout, current_row, 1, 1, 3) - current_row += 1 - - # 连接信号 - self.collision_width.valueChanged.connect(lambda v: self._updateBoxSize(model, 'width', v)) - self.collision_length.valueChanged.connect(lambda v: self._updateBoxSize(model, 'length', v)) - self.collision_height.valueChanged.connect(lambda v: self._updateBoxSize(model, 'height', v)) - - return current_row - - def _addCapsuleParametersToExisting(self, model, layout, start_row): - """向现有面板添加胶囊体参数""" - current_row = start_row - - radius_label = QLabel("半径:") - layout.addWidget(radius_label, current_row, 0) - - self.collision_capsule_radius = self._createCollisionSpinBox(0.01, 10000, 2) - self.collision_capsule_radius.valueChanged.connect(lambda v: self._updateCapsuleRadius(model, v)) - layout.addWidget(self.collision_capsule_radius, current_row, 1, 1 , 3) - current_row += 1 - - height_label = QLabel("高度:") - layout.addWidget(height_label, current_row, 0) - - self.collision_capsule_height = self._createCollisionSpinBox(0.01, 10000, 2) - self.collision_capsule_height.valueChanged.connect(lambda v: self._updateCapsuleHeight(model, v)) - layout.addWidget(self.collision_capsule_height, current_row, 1, 1, 3) - current_row += 1 - - return current_row - - def _addPlaneParametersToExisting(self, model, layout, start_row): - """向现有面板添加平面参数""" - current_row = start_row - - normal_label = QLabel("法向量:") - layout.addWidget(normal_label, current_row, 0) - - self.collision_normal_x = self._createCollisionSpinBox(-1, 1, 2) - self.collision_normal_y = self._createCollisionSpinBox(-1, 1, 2) - self.collision_normal_z = self._createCollisionSpinBox(-1, 1, 2) - - nx_layout = QHBoxLayout() - axis_label = QLabel("Nx:") - axis_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) - axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - nx_layout.addWidget(axis_label) - nx_layout.addWidget(self.collision_normal_x) - layout.addLayout(nx_layout, current_row, 1, 1, 3) - current_row += 1 - - ny_layout = QHBoxLayout() - placeholder = QLabel("") - layout.addWidget(placeholder, current_row, 0) - - axis_label = QLabel("Ny:") - axis_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) - axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - ny_layout.addWidget(axis_label) - ny_layout.addWidget(self.collision_normal_y) - layout.addLayout(ny_layout, current_row, 1, 1, 3) - current_row += 1 - - nz_layout = QHBoxLayout() - placeholder = QLabel("") - layout.addWidget(placeholder, current_row, 0) - - axis_label = QLabel("Nz:") - axis_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) - axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - nz_layout.addWidget(axis_label) - nz_layout.addWidget(self.collision_normal_z) - layout.addLayout(nz_layout, current_row, 1, 1, 3) - current_row += 1 - - # 连接信号 - self.collision_normal_x.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'x', v)) - self.collision_normal_y.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'y', v)) - self.collision_normal_z.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'z', v)) - - return current_row - - def _addVisibilityButtonToExistingPanel(self, model): - """向现有面板添加可见性按钮""" - try: - if hasattr(self, 'collision_layout'): - layout = self.collision_layout - is_collision_visible = self._isCollisionVisible(model) - - # 放在当前最后一行 - current_row = layout.rowCount() - - self.collision_visibility_button = QPushButton("隐藏碰撞" if is_collision_visible else "显示碰撞") - self.collision_visibility_button.setStyleSheet(self._collision_button_style()) - self.collision_visibility_button.clicked.connect(lambda: self._toggleCollisionVisibility(model)) - layout.addWidget(self.collision_visibility_button, current_row, 0, 1, 4) - - # 确保存在主按钮(有碰撞时应显示移除碰撞) - if self._hasCollision(model) and not hasattr(self, 'collision_button'): - try: - self.collision_button = self.createModernButton("移除碰撞") - self.collision_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - self.collision_button.setStyleSheet(self._collision_button_style()) - self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) - layout.addWidget(self.collision_button, current_row + 1, 0, 1, 4) - except Exception as _e: - print(f"在可见性按钮后创建移除碰撞按钮失败: {_e}") - - except Exception as e: - print(f"添加可见性按钮失败: {e}") - - def _repositionButtons(self, new_row): - """重新定位按钮位置""" - try: - if hasattr(self, 'collision_layout'): - layout = self.collision_layout - - def _remove_if_present(w): - try: - if not w: - return - # 从布局中移除该控件(如果已存在) - for i in range(layout.count()): - item = layout.itemAt(i) - if item and item.widget() is w: - layout.removeWidget(w) - break - except Exception: - pass - - # 计算底部行 - bottom_row = layout.rowCount() - - # 按固定顺序重新放置:先可见性按钮,再主按钮 - if hasattr(self, 'collision_visibility_button') and self.collision_visibility_button: - _remove_if_present(self.collision_visibility_button) - layout.addWidget(self.collision_visibility_button, bottom_row, 0, 1, 4) - bottom_row += 1 - - if hasattr(self, 'collision_button') and self.collision_button: - _remove_if_present(self.collision_button) - layout.addWidget(self.collision_button, bottom_row, 0, 1, 4) - - except Exception as e: - print(f"重新定位按钮失败: {e}") - - def _hideCollisionParameterControls(self): - """隐藏并移除碰撞参数控件与其所在的行(保留状态/形状/按钮行)""" - try: - # 1) 先清理对象属性引用,避免残留信号 - param_attrs = [ - 'collision_pos_x', 'collision_pos_y', 'collision_pos_z', - 'collision_radius', - 'collision_width', 'collision_length', 'collision_height', - 'collision_capsule_radius', 'collision_capsule_height', - 'collision_normal_x', 'collision_normal_y', 'collision_normal_z' - ] - for attr in param_attrs: - if hasattr(self, attr): - widget = getattr(self, attr) - try: - if widget: - widget.setParent(None) - widget.deleteLater() - except Exception: - pass - delattr(self, attr) - - # 2) 深度移除布局中第2行(索引>=2)之后的所有参数相关项,保留按钮 - if hasattr(self, 'collision_layout') and self.collision_layout is not None: - layout = self.collision_layout - - def _remove_layout_recursive(q_layout): - try: - while q_layout.count(): - child = q_layout.takeAt(0) - if child.widget(): - w = child.widget() - w.setParent(None) - w.deleteLater() - elif child.layout(): - _remove_layout_recursive(child.layout()) - except Exception: - pass - - row_count = layout.rowCount() - col_count = layout.columnCount() - # 从参数区域开始清理(行索引2及以后),但跳过我们要保留的按钮 - for i in range(2, row_count): - for j in range(0, col_count): - item = layout.itemAtPosition(i, j) - if not item: - continue - # 若是按钮,且是保留的按钮,则跳过 - w = item.widget() - if w is not None: - try: - if (hasattr(self, 'collision_button') and w == self.collision_button) or (hasattr(self, 'collision_visibility_button') and w == self.collision_visibility_button): - continue - except Exception: - pass - layout.removeWidget(w) - w.setParent(None) - w.deleteLater() - elif item.layout(): - # 移除嵌套布局(包含标签与输入框) - nested = item.layout() - _remove_layout_recursive(nested) - layout.removeItem(nested) - - # 尝试把保留的按钮移动到参数区域第一行(行2)下方,保持整洁 - try: - next_row = 2 - if hasattr(self, 'collision_visibility_button'): - self.collision_visibility_button.setVisible(False) - if hasattr(self, 'collision_button') and self.collision_button is not None: - layout.addWidget(self.collision_button, next_row, 0, 1, 4) - except Exception: - pass - - print("隐藏碰撞参数控件完成(仅保留状态、形状与按钮)") - - except Exception as e: - print(f"隐藏碰撞参数控件失败: {e}") - import traceback - traceback.print_exc() +import os +from collections import deque +from traceback import print_exc, print_last +from types import new_class +from typing import Hashable + +from PyQt5.QtGui import QColor +from PyQt5.QtWidgets import (QLabel, QLineEdit, QDoubleSpinBox, QPushButton, + QTreeWidget, QTreeWidgetItem, QMenu, QCheckBox, QComboBox, QHBoxLayout, QWidget, + QVBoxLayout, QGroupBox, QGridLayout, QSpinBox, QFileDialog, QMessageBox, QSizePolicy) +from PyQt5.QtCore import Qt, QTimer +from deploy_libs.unicodedata import normalize +from direct.actor.Actor import Actor +from direct.gui import DirectGui +from direct.task.TaskManagerGlobal import taskMgr +from idna import check_label +from jinja2.compiler import has_safe_repr +from panda3d.core import Vec3, Vec4, transpose, TransparencyAttrib, PartGroup, ColorAttrib, NodePath, Point3 +from scene import util +from direct.gui.DirectGui import DirectLabel, DirectFrame +from panda3d.core import TextNode +from ui.icon_manager import get_icon_manager, has_icon, get_icon + +class PropertyPanelManager: + """属性面板管理器""" + + def __init__(self, world): + """初始化属性面板管理器""" + self.world = world + self._propertyLayout = None + self._actor_cache = {} + self._spherical_video_controls = {} + self._transform_monitor_timer = None + self._last_transform_values = {} + + self.column_minimum_width = 85 + + # 初始化地形编辑参数 + if not hasattr(self.world, 'terrain_edit_radius'): + self.world.terrain_edit_radius = 3.0 + if not hasattr(self.world, 'terrain_edit_strength'): + self.world.terrain_edit_strength = 0.3 + if not hasattr(self.world, 'terrain_edit_operation'): # 这里原来是 terrain_edit_opertaion + self.world.terrain_edit_operation = "add" + + # 初始化碰撞参数加载标志位 + self._loading_collision_params = False + + # 定义现代化紧凑样式 + self.compact_style = """ + QDoubleSpinBox, QSpinBox { + min-width: 60px; + background-color: rgba(89, 100, 113, 0.15); + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 4px; + padding: 4px 6px; + font-size: 10px; + + } + QDoubleSpinBox:focus, QSpinBox:focus { + border: 1px solid #4d74bd; + background-color: rgba(77, 116, 189, 0.1); + } + QDoubleSpinBox::up-button, QSpinBox::up-button { + background-color: rgba(77, 116, 189, 0.2); + border: none; + border-radius: 2px; + width: 14px; + subcontrol-origin: border; + subcontrol-position: top right; + } + QDoubleSpinBox::down-button, QSpinBox::down-button { + background-color: rgba(77, 116, 189, 0.2); + border: none; + border-radius: 2px; + width: 14px; + subcontrol-origin: border; + subcontrol-position: bottom right; + } + QDoubleSpinBox::up-button:hover, QSpinBox::up-button:hover, + QDoubleSpinBox::down-button:hover, QSpinBox::down-button:hover { + background-color: rgba(77, 116, 189, 0.4); + } + QDoubleSpinBox::up-arrow, QSpinBox::up-arrow { + image: url(icons/up_arrows.png); + width: 10px; + height: 10px; + } + QDoubleSpinBox::down-arrow, QSpinBox::down-arrow { + image: url(icons/down_arrows.png); + width: 10px; + height: 10px; + } + QDoubleSpinBox::up-arrow:hover, QSpinBox::up-arrow:hover { + image: url(icons/up_arrows.png); + } + QDoubleSpinBox::down-arrow:hover, QSpinBox::down-arrow:hover { + image: url(icons/down_arrows.png); + } + QPushButton { + min-width: 60px; + background-color: rgba(89, 100, 113, 0.4); + color: rgba(255, 255, 255, 0.8); + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 4px; + padding: 6px 10px; + font-size: 10px; + font-weight: 400; + } + QPushButton:hover { + background-color: rgba(89, 100, 113, 0.6); + border: 1px solid rgba(77, 116, 189, 0.6); + color: #ffffff; + } + QPushButton:pressed, QPushButton:checked { + background-color: rgba(77, 116, 189, 0.8); + border: 1px solid #4d74bd; + color: #ffffff; + } + QPushButton:disabled { + background-color: rgba(89, 100, 113, 0.2); + color: rgba(235, 235, 235, 0.4); + border: 1px solid rgba(76, 92, 110, 0.2); + } + QComboBox { + min-width: 80px; + background-color: rgba(89, 100, 113, 0.15); + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 4px; + padding: 4px 6px; + font-size: 10px; + } + QComboBox:focus { + border: 1px solid #4d74bd; + } + QComboBox::drop-down { + border: none; + border-left: 1px solid rgba(76, 92, 110, 0.4); + background-color: rgba(77, 116, 189, 0.2); + border-radius: 0 4px 4px 0; + width: 16px; + } + QComboBox::drop-down:hover { + background-color: rgba(77, 116, 189, 0.4); + } + QComboBox::down-arrow { + image: url(icons/down_arrows.png); + width: 10px; + height: 10px; + } + QLineEdit { + min-width: 80px; + background-color: rgba(89, 100, 113, 0.15); + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 4px; + padding: 4px 6px; + font-size: 10px; + } + QLineEdit:focus { + border: 1px solid #4d74bd; + background-color: rgba(77, 116, 189, 0.1); + } + QCheckBox { + min-width: 20px; + font-size: 10px; + spacing: 6px; + } + QCheckBox::indicator { + width: 14px; + height: 14px; + border: 1px solid rgba(76, 92, 110, 0.6); + border-radius: 3px; + background-color: rgba(89, 100, 113, 0.15); + } + QCheckBox::indicator:checked { + background-color: #4d74bd; + border: 1px solid #4d74bd; + } + """ + self.compact_style += """ + QGroupBox { + margin-left: 0px; + padding-left: 0px; + } + QGroupBox::title { + left: 0px; + padding: 8px 5px 5px 0; + margin-top: 0px; + } + """ + + # 定义现代化状态徽章样式 + self.badge_style_blue = """ + QLabel { + background-color: rgba(77, 116, 189, 0.15); + border: 1px solid rgba(77, 116, 189, 0.6); + color: #4d74bd; + border-radius: 6px; + padding: 4px 10px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 9px; + font-weight: 500; + letter-spacing: 0.5px; + } + """ + + self.badge_style_green = """ + QLabel { + background-color: rgba(45, 255, 196, 0.15); + border: 1px solid rgba(45, 255, 196, 0.6); + color: #2dffc4; + border-radius: 6px; + padding: 4px 10px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 9px; + font-weight: 500; + letter-spacing: 0.5px; + } + """ + + # 添加新的徽章样式 + self.badge_style_orange = """ + QLabel { + background-color: rgba(255, 165, 0, 0.15); + border: 1px solid rgba(255, 165, 0, 0.6); + color: #ffa500; + border-radius: 6px; + padding: 4px 10px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 9px; + font-weight: 500; + letter-spacing: 0.5px; + } + """ + + self.badge_style_red = """ + QLabel { + background-color: rgba(255, 99, 99, 0.15); + border: 1px solid rgba(255, 99, 99, 0.6); + color: #ff6363; + border-radius: 6px; + padding: 4px 10px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 9px; + font-weight: 500; + letter-spacing: 0.5px; + } + """ + + # 固定宽度的徽章样式(用于碰撞检测状态) + self.badge_style_green_fixed = """ + QLabel { + background-color: rgba(45, 255, 196, 0.15); + border: 1px solid rgba(45, 255, 196, 0.6); + color: #2dffc4; + border-radius: 6px; + padding: 4px 10px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 9px; + font-weight: 500; + letter-spacing: 0.5px; + min-width: 50px; + max-width: 50px; + text-align: center; + } + """ + + self.badge_style_red_fixed = """ + QLabel { + background-color: rgba(255, 99, 99, 0.15); + border: 1px solid rgba(255, 99, 99, 0.6); + color: #ff6363; + border-radius: 6px; + padding: 4px 10px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 9px; + font-weight: 500; + letter-spacing: 0.5px; + min-width: 50px; + max-width: 50px; + text-align: center; + } + """ + + def createStatusBadge(self, text, badge_type="green"): + """创建现代化状态徽章 + + Args: + text: 徽章文字 + badge_type: 徽章类型,支持 "green", "blue", "orange", "red" + + Returns: + QLabel: 配置好样式的标签 + """ + badge = QLabel(text) + if badge_type == "green": + badge.setStyleSheet(self.badge_style_green) + elif badge_type == "blue": + badge.setStyleSheet(self.badge_style_blue) + elif badge_type == "orange": + badge.setStyleSheet(self.badge_style_orange) + elif badge_type == "red": + badge.setStyleSheet(self.badge_style_red) + else: + badge.setStyleSheet(self.badge_style_blue) # 默认蓝色 + return badge + + def createFixedStatusBadge(self, text, badge_type="green"): + """创建固定宽度的状态徽章(用于碰撞检测等需要保持一致宽度的场景) + + Args: + text: 徽章文字 + badge_type: 徽章类型,支持 "green", "red" + + Returns: + QLabel: 配置好固定宽度样式的标签 + """ + badge = QLabel(text) + badge.setAlignment(Qt.AlignCenter) # 确保文字居中 + + if badge_type == "green": + badge.setStyleSheet(self.badge_style_green_fixed) + elif badge_type == "red": + badge.setStyleSheet(self.badge_style_red_fixed) + else: + badge.setStyleSheet(self.badge_style_green_fixed) # 默认绿色 + + return badge + + def createModernButton(self, text, button_type="default"): + """创建现代化按钮 + + Args: + text: 按钮文字 + button_type: 按钮类型 - "default", "primary", "success", "warning", "danger" + + Returns: + QPushButton: 配置好样式的按钮 + """ + button = QPushButton(text) + + if button_type == "primary": + button.setStyleSheet(""" + QPushButton { + background-color: rgba(77, 116, 189, 0.8); + color: #ffffff; + border: 1px solid #4d74bd; + border-radius: 4px; + padding: 8px 12px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 500; + min-height: 16px; + } + QPushButton:hover { + background-color: rgba(77, 116, 189, 1.0); + border: 1px solid rgba(77, 116, 189, 1.0); + } + QPushButton:pressed { + background-color: rgba(61, 96, 169, 1.0); + border: 1px solid rgba(61, 96, 169, 1.0); + } + """) + elif button_type == "success": + button.setStyleSheet(""" + QPushButton { + background-color: rgba(45, 255, 196, 0.15); + color: #2dffc4; + border: 1px solid rgba(45, 255, 196, 0.6); + border-radius: 4px; + padding: 8px 12px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 400; + min-height: 16px; + } + QPushButton:hover { + background-color: rgba(45, 255, 196, 0.25); + border: 1px solid #2dffc4; + } + QPushButton:pressed { + background-color: rgba(45, 255, 196, 0.4); + color: #ffffff; + } + """) + elif button_type == "warning": + button.setStyleSheet(""" + QPushButton { + background-color: rgba(255, 165, 0, 0.15); + color: #ffa500; + border: 1px solid rgba(255, 165, 0, 0.6); + border-radius: 4px; + padding: 8px 12px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 400; + min-height: 16px; + } + QPushButton:hover { + background-color: rgba(255, 165, 0, 0.25); + border: 1px solid #ffa500; + } + QPushButton:pressed { + background-color: rgba(255, 165, 0, 0.4); + color: #ffffff; + } + """) + elif button_type == "danger": + button.setStyleSheet(""" + QPushButton { + background-color: rgba(255, 99, 99, 0.15); + color: #ff6363; + border: 1px solid rgba(255, 99, 99, 0.6); + border-radius: 4px; + padding: 8px 12px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 400; + min-height: 16px; + } + QPushButton:hover { + background-color: rgba(255, 99, 99, 0.25); + border: 1px solid #ff6363; + } + QPushButton:pressed { + background-color: rgba(255, 99, 99, 0.4); + color: #ffffff; + } + """) + else: # default + # 使用全局默认样式 + pass + return button + + def _collision_button_style(self): + """返回碰撞检测面板按钮统一样式""" + return """ + QPushButton { + background-color: rgba(89, 98, 118, 0.4); + color: rgba(235, 235, 235, 0.85); + border: 1px solid rgba(89, 98, 118, 0.65); + border-radius: 4px; + padding: 8px 12px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 10px; + font-weight: 400; + min-height: 16px; + } + QPushButton:hover { + background-color: rgba(89, 98, 118, 0.55); + border: 1px solid rgba(89, 98, 118, 0.9); + color: #ffffff; + } + QPushButton:pressed { + background-color: rgba(89, 98, 118, 0.7); + border: 1px solid rgba(89, 98, 118, 0.95); + color: #ffffff; + } + QPushButton:disabled { + background-color: rgba(89, 98, 118, 0.18); + border: 1px solid rgba(89, 98, 118, 0.25); + color: rgba(235, 235, 235, 0.35); + } + """ + + def createModernGroupBox(self, title, parent_layout): + """创建现代化的分组框 + + Args: + title: 分组框标题 + parent_layout: 父布局 + + Returns: + tuple: (QGroupBox, QVBoxLayout) 分组框和其内部布局 + """ + group_box = QGroupBox(title) + group_layout = QVBoxLayout(group_box) + group_layout.setSpacing(0) # 减少内部间距 + group_layout.setContentsMargins(0, 3, 0, 6) # 减少内容边距 + + # 应用现代化样式,使用更紧凑的间距 + group_box.setStyleSheet(""" + QGroupBox { + font-weight: 500; + font-size: 11px; + color: rgba(255, 255, 255, 0.9); + margin-top: 12px; + margin-bottom: 4px; + padding-top: 15px; + border-top: 1px solid rgba(77, 116, 189, 0.3); + } + QGroupBox::title { + subcontrol-origin: padding; + subcontrol-position: top left; + left: 0px; + padding: 8px 5px 5px 0; + margin-top: 0px; + } + """) + + parent_layout.addWidget(group_box) + return group_box, group_layout + + def _getFigmaSpinBoxStyle(self): + """获取符合Figma设计的SpinBox样式""" + return """ + QDoubleSpinBox, QSpinBox { + background-color: rgba(89, 100, 113, 0.2); + color: #ebebeb; + border: 1px solid rgba(76, 92, 110, 0.6); + border-radius: 2px; + padding: 3px 6px; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 10px; + font-weight: 300; + letter-spacing: 0.5px; + min-width: 70px; + max-width: 70px; + min-height: 18px; + max-height: 18px; + } + QDoubleSpinBox:focus, QSpinBox:focus { + border: 1px solid #4d74bd; + background-color: rgba(77, 116, 189, 0.1); + } + QDoubleSpinBox:hover, QSpinBox:hover { + border: 1px solid rgba(77, 116, 189, 0.6); + background-color: rgba(89, 100, 113, 0.25); + } + QDoubleSpinBox::up-button, QSpinBox::up-button { + background-color: rgba(77, 116, 189, 0.2); + border: none; + border-radius: 2px; + width: 14px; + subcontrol-origin: border; + subcontrol-position: top right; + } + QDoubleSpinBox::down-button, QSpinBox::down-button { + background-color: rgba(77, 116, 189, 0.2); + border: none; + border-radius: 2px; + width: 14px; + subcontrol-origin: border; + subcontrol-position: bottom right; + } + QDoubleSpinBox::up-button:hover, QSpinBox::up-button:hover, + QDoubleSpinBox::down-button:hover, QSpinBox::down-button:hover { + background-color: rgba(77, 116, 189, 0.4); + } + QDoubleSpinBox::up-arrow, QSpinBox::up-arrow { + image: url(icons/up_arrows.png); + width: 10px; + height: 10px; + } + QDoubleSpinBox::down-arrow, QSpinBox::down-arrow { + image: url(icons/down_arrows.png); + width: 10px; + height: 10px; + } + """ + + def createPropertyRow(self, label_text, widget, layout, tooltip=None): + """创建属性行 + + Args: + label_text: 标签文字 + widget: 控件 + layout: 布局 + tooltip: 工具提示 + """ + row_layout = QHBoxLayout() + row_layout.setContentsMargins(0, 0, 0, 0) + row_layout.setSpacing(8) + + # 创建标签 + label = QLabel(label_text) + label.setMinimumWidth(60) + label.setStyleSheet(""" + QLabel { + color: rgba(255, 255, 255, 0.8); + font-size: 10px; + font-weight: 300; + } + """) + + if tooltip: + label.setToolTip(tooltip) + widget.setToolTip(tooltip) + + row_layout.addWidget(label) + row_layout.addWidget(widget) + row_layout.addStretch() + + layout.addLayout(row_layout) + + def setPropertyLayout(self, layout): + """设置属性面板布局引用""" + print("开始设置属性布局") + print(f"布局类型: {type(layout)}") + + # 保存布局引用 + self._propertyLayout = layout + + # 确保布局有父部件 + if not layout.parent(): + print("布局没有父部件,创建新的容器") + from PyQt5.QtWidgets import QWidget + container = QWidget() + container.setObjectName("PropertyContainer") + container.setLayout(layout) + + print(f"布局父部件: {self._propertyLayout.parent().objectName() if self._propertyLayout.parent() else 'None'}") + print(f"布局项目数: {self._propertyLayout.count()}") + + return True + + def clearPropertyPanel(self): + """清空属性面板""" + # 停止变换监控 + self.stopTransformMonitoring() + + if self._propertyLayout: + while self._propertyLayout.count(): + item = self._propertyLayout.takeAt(0) + if item.widget(): + item.widget().deleteLater() + + def updateNodeVisibilityAfterDrag(self, item): + """拖拽结束后更新节点的可见性状态""" + node = item.data(0, Qt.UserRole) + if not node: + return + + # 当节点被拖拽后,需要根据新父节点的状态来更新可见性 + self._syncEffectiveVisibility(node) + self._syncSceneVisibility() + + def _syncSceneVisibility(self): + scene_root = self.world.render + self._syncEffectiveVisibility(scene_root) + + def updatePropertyPanel(self, item): + """更新属性面板显示""" + # if not self._propertyLayout or not self._propertyLayout.parent(): + # print("属性布局未设置或没有父部件!") + # return + #更健壮的有效性检查 + + self._cleanupAllReferences() + self.clearPropertyPanel() + + # 应用紧凑样式到属性面板容器 + if self._propertyLayout.parent(): + self._propertyLayout.parent().setStyleSheet(self.compact_style) + + itemText = item.text(0) + + # 如果点击的是场景根节点,显示提示信息 + if itemText == "场景": + tipLabel = QLabel("") + tipLabel.setStyleSheet("color: gray;") + # self._propertyLayout.addRow(tipLabel) + self._propertyLayout.addWidget(tipLabel) + return + + model = item.data(0, Qt.UserRole) + + user_visible = True + if model: + user_visible = model.getPythonTag("user_visible") + if user_visible is None: + user_visible = True + model.setPythonTag("user_visible", True) + + self.name_group = QGroupBox("物体名称") + self.name_group.setProperty("groupRole", "first") + name_layout = QGridLayout() + self.active_check = QCheckBox() + # 根据模型的实际可见性状态设置复选框 + self.active_check.setChecked(user_visible) + self.active_check.stateChanged.connect(lambda state: self._toggleModelVisibility(model, state)) + self.name_input = QLineEdit(itemText) + + self.name_input.returnPressed.connect( + lambda: self.world.treeWidget.update_item_name(self.name_input.text(), item) + ) + + name_layout.addWidget(self.active_check, 0, 0) + name_layout.setColumnMinimumWidth(0, self.column_minimum_width) + name_layout.addWidget(self.name_input, 0, 1, 1, 3) + self.name_group.setLayout(name_layout) + self._propertyLayout.addWidget(self.name_group) + + # if model: + # try: + # self.active_check.stateChanged.disconnect() + # except TypeError: + # pass + # self.active_check.stateChanged.connect( + # lambda state, m=model: self._setUserVisible(m, state == Qt.Checked) + # ) + # nameLabel = QLabel("名称:") + # nameEdit = QLineEdit(itemText) + # self._propertyLayout.addRow(nameLabel, nameEdit) + + # 获取节点对象 + model = item.data(0, Qt.UserRole) + + if self._isTerrainNode(model, item): + self._showTerrainProperties(model, item) + elif model and hasattr(model, 'getTag') and model.getTag("element_type") == "cesium_tileset": + self._showCesiumTilesetProperties(model, item) + elif model and hasattr(model, 'getTag') and model.getTag("gui_type"): + self.updateGUIPropertyPanel(model, item) + elif model and hasattr(model, 'getTag') and model.getTag("light_type"): + self.updateLightPropertyPanel(model) + elif model: + self._updateModelPropertyPanel(model) + # 启动变换监控 + self.startTransformMonitoring(model) + + self._propertyLayout.addStretch() + + # 重置碰撞相关标志,确保下次选择时正常显示 + self._just_added_collision = False + + # 强制更新布局 + if self._propertyLayout: + self._propertyLayout.update() + propertyWidget = self._propertyLayout.parentWidget() + if propertyWidget: + propertyWidget.update() + + def _isTerrainNode(self, node, item): + """检查是否是地形节点""" + item_data = item.data(0, Qt.UserRole + 1) + if item_data == "terrain": + return True + + if hasattr(self.world, 'terrain_manager') and self.world.terrain_manager.terrains: + for terrain_info in self.world.terrain_manager.terrains: + if terrain_info['node'] == node: + return True + return False + + def _showTerrainProperties(self, terrain_node, item): + """显示地形属性面板""" + try: + terrain_info = None + if hasattr(self.world, 'terrain_manager'): + for info in self.world.terrain_manager.terrains: + if info['node'] == terrain_node: + terrain_info = info + break + if not terrain_info: + no_info_label = QLabel("未找到地形信息") + no_info_label.setStyleSheet("color:red;font-weight:bold;") + self._propertyLayout.addWidget(no_info_label) + return + + info_group = QGroupBox("地形信息") + info_layout = QGridLayout() + info_layout.setColumnMinimumWidth(0, self.column_minimum_width) + info_layout.addWidget(QLabel("名称:"), 0, 0) + name_label = QLabel(terrain_info.get('name', '未知')) + info_layout.addWidget(name_label, 0, 1, 1, 3) + + info_layout.addWidget(QLabel("类型:"), 1, 0) + type_label = QLabel("高度图地形" if terrain_info.get('heightmap') else "平面地形") + info_layout.addWidget(type_label, 1, 1, 1, 3) + + if terrain_info.get('heightmap'): + info_layout.addWidget(QLabel("高度图:"), 2, 0) + heightmap_label = QLabel(os.path.basename(terrain_info['heightmap'])) + heightmap_label.setWordWrap(True) + info_layout.addWidget(heightmap_label, 2, 1, 1, 3) + info_group.setLayout(info_layout) + self._propertyLayout.addWidget(info_group) + + # 变换属性 + self._updateTerrainTransformPanel(terrain_node) + # 地形编辑控制面板 + self._createTerrainEditPanel(terrain_info) + # 材质属性 + self._updateTerrainMaterialPanel(terrain_node, terrain_info) + + # #删除按钮 + # delete_btn = QPushButton("删除地形") + # delete_btn.setStyleSheet(""" + # QPushButton{ + # background-color:#ff4444; + # color:white; + # border:none; + # padding:8px; + # border-radius:4px; + # margin-top:10px + # } + # QPushButton:hover{ + # background-color:#ff6666; + # } + # """) + # delete_btn.clicked.connect(lambda:self._deleteTerrain(terrain_info,item)) + # self._propertyLayout.addWidget(delete_btn) + except Exception as e: + print(f"显示地形属性时出错: {e}") + import traceback + traceback.print_exc() + + def _updateTerrainTransformPanel(self, terrain_node): + """更新地形变化属性面板""" + try: + transform_group = QGroupBox("变换 Transform") + transform_layout = QGridLayout() + transform_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + pos = terrain_node.getPos() + scale = terrain_node.getScale() + + transform_layout.addWidget(QLabel("位置"), 0, 0) + + position_row = QHBoxLayout() + position_row.setContentsMargins(0, 0, 0, 0) + position_row.setSpacing(4) + + self.pos_x = QDoubleSpinBox() + self.pos_y = QDoubleSpinBox() + self.pos_z = QDoubleSpinBox() + + for pos_widget in [self.pos_x, self.pos_y, self.pos_z]: + pos_widget.setRange(-1000000.0, 1000000.0) + self.pos_x.setValue(pos.getX()) + self.pos_y.setValue(pos.getY()) + self.pos_z.setValue(pos.getZ()) + + def updateXPosition(value): + terrain_node.setX(value) + self.refreshModelValues(terrain_node) + + self.pos_x.valueChanged.connect(updateXPosition) + + def updateYPosition(value): + terrain_node.setY(value) + self.refreshModelValues(terrain_node) + + self.pos_y.valueChanged.connect(updateYPosition) + + def updateZPosition(value): + terrain_node.setZ(value) + self.refreshModelValues(terrain_node) + + self.pos_z.valueChanged.connect(updateZPosition) + + for axis_label, widget in (("X:", self.pos_x), ("Y:", self.pos_y), ("Z:", self.pos_z)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + position_row.addWidget(axis) + position_row.addWidget(widget) + + transform_layout.addLayout(position_row, 0, 1, 1, 3) + + transform_layout.addWidget(QLabel("旋转"), 1, 0) + rotation_row = QHBoxLayout() + rotation_row.setContentsMargins(0, 0, 0, 0) + rotation_row.setSpacing(4) + + self.rot_x = QDoubleSpinBox() + self.rot_y = QDoubleSpinBox() + self.rot_z = QDoubleSpinBox() + + for rot_widget in [self.rot_x, self.rot_y, self.rot_z]: + rot_widget.setRange(-360, 360) + + self.rot_x.setValue(terrain_node.getH()) + self.rot_y.setValue(terrain_node.getP()) + self.rot_z.setValue(terrain_node.getR()) + + self.rot_x.valueChanged.connect(lambda v: terrain_node.setH(v)) + self.rot_y.valueChanged.connect(lambda v: terrain_node.setP(v)) + self.rot_z.valueChanged.connect(lambda v: terrain_node.setR(v)) + + for axis_label, widget in (("H:", self.rot_x), ("P:", self.rot_y), ("R:", self.rot_z)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + rotation_row.addWidget(axis) + rotation_row.addWidget(widget) + + transform_layout.addLayout(rotation_row, 1, 1, 1, 3) + + transform_layout.addWidget(QLabel("缩放"), 2, 0) + + scale_row = QHBoxLayout() + scale_row.setContentsMargins(0, 0, 0, 0) + scale_row.setSpacing(4) + + self.scale_x = QDoubleSpinBox() + self.scale_y = QDoubleSpinBox() + self.scale_z = QDoubleSpinBox() + + for scale_widget in [self.scale_x, self.scale_y, self.scale_z]: + scale_widget.setRange(-1000, 1000) + scale_widget.setSingleStep(0.1) + self.scale_x.setValue(scale.getX()) + self.scale_y.setValue(scale.getY()) + self.scale_z.setValue(scale.getZ()) + + self.scale_x.valueChanged.connect(lambda v: self._updateXScale(terrain_node, v)) + self.scale_y.valueChanged.connect(lambda v: self._updateYScale(terrain_node, v)) + self.scale_z.valueChanged.connect(lambda v: self._updateZScale(terrain_node, v)) + + for axis_label, widget in (("X:", self.scale_x), ("Y:", self.scale_y), ("Z:", self.scale_z)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + scale_row.addWidget(axis) + scale_row.addWidget(widget) + + transform_layout.addLayout(scale_row, 2, 1, 1, 3) + + transform_group.setLayout(transform_layout) + self._propertyLayout.addWidget(transform_group) + except Exception as e: + print(f"更新地形变换面板时出错: {e}") + + def _createTerrainEditPanel(self, terrain_info): + try: + edit_group = QGroupBox("地形编辑") + edit_layout = QGridLayout() + edit_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + # 编辑半径 + edit_layout.addWidget(QLabel("编辑半径:"), 0, 0) + self.terrain_radius_spin = QDoubleSpinBox() + self.terrain_radius_spin.setRange(0.1, 50.0) + self.terrain_radius_spin.setSingleStep(0.5) + self.terrain_radius_spin.setValue(getattr(self.world, 'terrain_edit_radius', 3.0)) + self.terrain_radius_spin.valueChanged.connect(self._onTerrainRadiusChanged) + edit_layout.addWidget(self.terrain_radius_spin, 0, 1, 1, 3) + + # 编辑强度 + edit_layout.addWidget(QLabel("编辑强度:"), 1, 0) + self.terrain_strength_spin = QDoubleSpinBox() + self.terrain_strength_spin.setRange(0.01, 50) + self.terrain_strength_spin.setSingleStep(0.1) + self.terrain_strength_spin.setValue(getattr(self.world, 'terrain_edit_strength', 0.3)) + self.terrain_strength_spin.valueChanged.connect(self._onTerrainStrengthChanged) + edit_layout.addWidget(self.terrain_strength_spin, 1, 1, 1, 3) + + edit_layout.addWidget(QLabel("操作类型:"), 2, 0) + self.terrain_operation_combo = QComboBox() + self.terrain_operation_combo.addItems(["升高地形", "降低地形", "平坦化"]) + current_op = getattr(self.world, 'terrain_edit_operation', "add") + if current_op == "add": + self.terrain_operation_combo.setCurrentText("升高地形") + elif current_op == "subtract": + self.terrain_operation_combo.setCurrentText("降低地形") + else: + self.terrain_operation_combo.setCurrentText("平坦化") + self.terrain_operation_combo.currentTextChanged.connect(self._onTerrainOperationChanged) + edit_layout.addWidget(self.terrain_operation_combo, 2, 1, 1, 3) + + help_label = QLabel("在地形编辑模式下,使用鼠标左键升高地形,右键降低地形") + help_label.setWordWrap(True) + help_label.setStyleSheet("font-size:10px;color:gray;") + edit_layout.addWidget(help_label, 3, 0, 1, 4) + + edit_group.setLayout(edit_layout) + self._propertyLayout.addWidget(edit_group) + + # 确保初始值被正确设置到 world 对象上 + self._onTerrainRadiusChanged(self.terrain_radius_spin.value()) + self._onTerrainStrengthChanged(self.terrain_strength_spin.value()) + self._onTerrainOperationChanged(self.terrain_operation_combo.currentText()) + except Exception as e: + print(f"创建地形编辑面板时出错: {e}") + + def _onTerrainRadiusChanged(self, value): + """地形编辑半径改变""" + if hasattr(self.world, 'terrain_edit_radius'): + self.world.terrain_edit_radius = float(value) + + def _onTerrainStrengthChanged(self, value): + """地形编辑强度改变""" + if hasattr(self.world, 'terrain_edit_strength'): + self.world.terrain_edit_strength = float(value) + + def _onTerrainOperationChanged(self, text): + """地形编辑操作类型改变""" + operation_map = { + "升高地形": "add", + "降低地形": "subtract", + "平坦化": "set" + } + if hasattr(self.world, 'terrain_edit_operation'): + # 正确设置地形编辑操作属性 + self.world.terrain_edit_operation = operation_map.get(text, "add") + print(f"地形编辑操作已设置为: {self.world.terrain_edit_operation}") + + def _updateTerrainMaterialPanel(self, terrain_node, terrain_info): + """更新地形材质属性面板""" + try: + material_group = QGroupBox("材质属性") + material_layout = QGridLayout() + material_layout.setColumnMinimumWidth(0, self.column_minimum_width) + # 颜色设置 + material_layout.addWidget(QLabel("颜色:"), 0, 0) + color_button = QPushButton("选择颜色") + color_button.clicked.connect(lambda: self._selectTerrainColor(terrain_info)) + material_layout.addWidget(color_button, 0, 1, 1, 3) + # 纹理设置 + material_layout.addWidget(QLabel("纹理:"), 1, 0) + texture_button = QPushButton("选择纹理") + texture_button.clicked.connect(lambda: self._selectTerrainTexture(terrain_info)) + material_layout.addWidget(texture_button, 1, 1, 1, 3) + + material_group.setLayout(material_layout) + self._propertyLayout.addWidget(material_group) + except Exception as e: + print(f"更新材质面板时出错{e}") + + def _selectTerrainColor(self, terrain_info): + try: + from PyQt5.QtWidgets import QColorDialog + from PyQt5.QtGui import QColor + + current_color = QColor(255, 255, 255) + + color = QColorDialog.getColor(current_color, None, "选择地形颜色") + if color.isValid(): + r, g, b = color.red() / 255.0, color.green() / 255.0, color.blue() / 255.0 + if hasattr(self.world, 'terrain_manager'): + self.world.terrain_manager.setTerrainColor(terrain_info, (r, g, b)) + except Exception as e: + print(f"选择地形颜色时出错: {e}") + + def _selectTerrainTexture(self, terrain_info): + """选择地形纹理""" + try: + from PyQt5.QtWidgets import QFileDialog + file_path, _ = QFileDialog.getOpenFileName( + None, + "选择地形纹理", + "", + "图像文件(*.png *.jpg *.jpeg *.bmp *.tga *.dds)" + ) + + if file_path and os.path.exists(file_path): + if hasattr(self.world, 'terrain_manager'): + success = self.world.terrain_manager.setTerrainTexture(terrain_info, file_path) + if success: + print(f"地形纹理已应用{file_path}") + else: + print(f"应用地形纹理失败") + except Exception as e: + print(f"选择地形纹理时出错: {e}") + + def _deleteTerrain(self, terrain_info, item): + """删除地形""" + try: + from PyQt5.QtWidgets import QMessageBox + + reply = QMessageBox.question( + None, + '确认删除', + f'确定要删除地形 "{terrain_info.get("name", "未知地形")}" 吗?', + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply == QMessageBox.Yes: + # 调用 interface_manager 中已经实现的 deleteNode 方法 + if hasattr(self.world, 'interface_manager') and self.world.interface_manager.treeWidget: + self.world.interface_manager.deleteNode(terrain_info['node'], item) + else: + # 如果 interface_manager 不可用,使用原来的删除逻辑 + if hasattr(self.world, 'terrain_manager'): + success = self.world.terrain_manager.deleteTerrain(terrain_info) + if success: + # 更新场景树 + # if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, + # 'updateSceneTree'): + # self.world.scene_manager.updateSceneTree() + # + # # 清空属性面板 + # self.clearPropertyPanel() + + print(f"✓ 地形已删除: {terrain_info.get('name', '未知')}") + else: + print("✗ 删除地形失败") + + except Exception as e: + print(f"删除地形时出错: {e}") + + def _cleanupAllReferences(self): + """清理所有控件引用""" + # 停止变换监控 + self.stopTransformMonitoring() + + # 清理变换控件引用 + self._cleanupTransformControls() + + # 清理信息面板监控 + if hasattr(self, '_info_panel_timer'): + self._info_panel_timer.stop() + self._info_panel_timer.deleteLater() + del self._info_panel_timer + + # 清理信息面板控件引用 + if hasattr(self, '_info_panel_controls'): + self._info_panel_controls.clear() + del self._info_panel_controls + + if hasattr(self, '_current_info_panel'): + del self._current_info_panel + + # 清理碰撞相关控件引用 + collision_controls = [ + 'collision_status_badge', 'collision_shape_combo', 'collision_shape_label', + 'collision_visibility_button', 'collision_button', 'collision_layout', 'collision_group', + 'collision_pos_x', 'collision_pos_y', 'collision_pos_z', + 'collision_radius', 'collision_width', 'collision_length', 'collision_height', + 'collision_capsule_radius', 'collision_capsule_height', + 'collision_normal_x', 'collision_normal_y', 'collision_normal_z' + ] + for name in collision_controls: + if hasattr(self, name): + delattr(self, name) + + # 清理其他可能的控件引用 + other_controls = ['scale_x', 'scale_y', 'scale_z', 'pos_x', 'pos_y', 'pos_z'] + for name in other_controls: + if hasattr(self, name): + setattr(self, name, None) + + def _setUserVisible(self, node, visible): + node.setPythonTag("user_visible", visible) + self._syncEffectiveVisibility(node) + + def _setUserVisible_light(self, node, visible): + """设置用户可见性状态""" + try: + # 保存可见性状态 + node.setPythonTag("user_visible", visible) + + except Exception as e: + print(f"设置用户可见性失败: {e}") + + def _syncEffectiveVisibility(self, start_node): + """广度优先,确保父隐藏则子一定隐藏""" + # 获取起始节点的父节点 + parent_node = start_node.getParent() + + # 确定父节点的有效可见性 + parent_effective_visible = True + if parent_node: + parent_effective_visible = parent_node.getPythonTag("effective_visible") + if parent_effective_visible is None: + parent_effective_visible = True + + q = deque([(start_node, parent_effective_visible)]) # (node, parent_effective_visible) + + while q: + node, parent_eff = q.popleft() + user = node.getPythonTag("user_visible") + if user is None: + user = True + eff = parent_eff and user + node.setPythonTag("effective_visible", eff) + + # 特殊处理:检查是否为碰撞体节点 + + if node.getName().startswith("modelCollision_"): + node.hide() + else: + if eff: + node.show() + else: + node.hide() + + for child in node.getChildren(): + q.append((child, eff)) + + def _toggleModelVisibility(self, model, state): + """切换模型可见性状态""" + try: + # 特殊处理灯光对象 + scene_manager = None + if hasattr(self.world, 'scene_manager'): + scene_manager = self.world.scene_manager + + if scene_manager and hasattr(scene_manager, 'isLightObject'): + is_light = scene_manager.isLightObject(model) + + if is_light: + visible = (state == Qt.Checked) + self._setUserVisible_light(model, visible) + if hasattr(scene_manager, 'toggleLightVisibility'): + scene_manager.toggleLightVisibility(model, visible) + return # 关键:这里必须return,避免执行下面的普通模型逻辑 + else: + visible = (state == Qt.Checked) + self._setUserVisible(model, visible) + + collision_nodes = model.findAllMatches("**/modelCollision_*") + for collision_node in collision_nodes: + collision_node.hide() + + except Exception as e: + print(f"切换模型可见性失败: {str(e)}") + import traceback + traceback.print_exc() + + def refreshModelValues(self, nodePath): + """刷新模型值显示""" + if not nodePath or self._propertyLayout is None: + return + + # 检查面板是否仍然有效 + if not self._isPropertyPanelValid(): + return + + try: + # 检查是否是GUI元素 + is_gui_element = (hasattr(nodePath, 'getTag') and + nodePath.getTag("is_gui_element") == "1") + + if is_gui_element: + # 对于GUI元素,更新所有相关属性 + self._refreshGUIElementValues(nodePath) + else: + # 对于普通3D模型,更新位置、旋转、缩放 + self._refreshModelValues(nodePath) + + except Exception as e: + print(f"刷新模型值显示失败: {e}") + + def startTransformMonitoring(self, nodePath): + """开始监控节点的变换变化""" + # 如果已有监控器在运行,先停止它 + self.stopTransformMonitoring() + + # 保存初始变换值 + self._saveCurrentTransformValues(nodePath) + + # 创建并启动定时器 + self._transform_monitor_timer = QTimer() + self._transform_monitor_timer.timeout.connect(lambda: self._checkTransformChanges(nodePath)) + self._transform_monitor_timer.start(100) # 每100毫秒检查一次 + + def stopTransformMonitoring(self): + """停止监控节点的变换变化""" + if self._transform_monitor_timer: + self._transform_monitor_timer.stop() + self._transform_monitor_timer.deleteLater() + self._transform_monitor_timer = None + + # 清除保存的变换值 + self._last_transform_values.clear() + + def _saveCurrentTransformValues(self, nodePath): + """保存当前节点的变换值""" + try: + pos = nodePath.getPos() + hpr = nodePath.getHpr() + scale = nodePath.getScale() + + self._last_transform_values = { + 'pos': (pos.getX(), pos.getY(), pos.getZ()), + 'hpr': (hpr.getX(), hpr.getY(), hpr.getZ()), + 'scale': (scale.getX(), scale.getY(), scale.getZ()) + } + except Exception as e: + print(f"保存变换值失败: {e}") + + def _checkTransformChanges(self, nodePath): + """检查节点变换是否发生变化""" + try: + # 获取当前变换值 + pos = nodePath.getPos() + hpr = nodePath.getHpr() + scale = nodePath.getScale() + + current_values = { + 'pos': (pos.getX(), pos.getY(), pos.getZ()), + 'hpr': (hpr.getX(), hpr.getY(), hpr.getZ()), + 'scale': (scale.getX(), scale.getY(), scale.getZ()) + } + + # 比较变换值是否发生变化 + if current_values != self._last_transform_values: + # 变换已更改,刷新属性面板 + self.refreshModelValues(nodePath) + # 更新保存的变换值 + self._last_transform_values = current_values + except Exception as e: + print(f"检查变换变化失败: {e}") + + def _refreshGUIElementValues(self, gui_element): + """刷新GUI元素值显示""" + try: + # 更新位置属性 + if hasattr(self, 'pos_x') and self.pos_x: + pos = gui_element.getPos() + gui_type = gui_element.getTag("gui_type") + + if gui_type in ["button", "label", "entry", "2d_image"]: + # 2D GUI组件使用屏幕坐标 + logical_x = pos.getX() / 0.1 + logical_z = pos.getZ() / 0.1 + + self._safeUpdateSpinBox('pos_x', logical_x) + self._safeUpdateSpinBox('pos_z', logical_z) + else: + # 3D GUI组件使用世界坐标(包括 video_screen) + pos = gui_element.getPos() + + # 修复:确保 video_screen 也能正确更新位置控件 + if hasattr(self, 'pos_x') and self.pos_x: + self._safeUpdateSpinBox('pos_x', pos.getX()) + if hasattr(self, 'pos_y') and self.pos_y: + self._safeUpdateSpinBox('pos_y', pos.getY()) + if hasattr(self, 'pos_z') and self.pos_z: + self._safeUpdateSpinBox('pos_z', pos.getZ()) + + # 更新缩放属性 + if hasattr(self, 'scale_x') and self.scale_x: + scale = gui_element.getScale() + self._safeUpdateSpinBox('scale_x', scale.getX()) + if hasattr(self, 'scale_y') and self.scale_y: + self._safeUpdateSpinBox('scale_y', scale.getY()) + if hasattr(self, 'scale_z') and self.scale_z: + self._safeUpdateSpinBox('scale_z', scale.getZ()) + + # 更新旋转属性(如果存在) + if hasattr(self, 'rot_x') and self.rot_x: + hpr = gui_element.getHpr() + self._safeUpdateSpinBox('rot_x', hpr.getX()) + self._safeUpdateSpinBox('rot_y', hpr.getY()) + self._safeUpdateSpinBox('rot_z', hpr.getZ()) + + except Exception as e: + print(f"刷新GUI元素值显示失败: {e}") + + def _refreshModelValues(self, nodePath): + """刷新普通模型值显示""" + try: + parent = nodePath.getParent() + render = self.world.render + relPos = nodePath.getPos(parent) if parent else nodePath.getPos() + + # 安全地更新位置控件 + self._safeUpdateSpinBox('pos_x', relPos.getX()) + self._safeUpdateSpinBox('pos_y', relPos.getY()) + self._safeUpdateSpinBox('pos_z', relPos.getZ()) + + # 安全地更新世界位置控件 + worldPos = nodePath.getPos(render) + self._safeUpdateSpinBox('world_pos_x', worldPos.getX()) + self._safeUpdateSpinBox('world_pos_y', worldPos.getY()) + self._safeUpdateSpinBox('world_pos_z', worldPos.getZ()) + + # 安全地更新旋转控件(如果存在) + if hasattr(self, 'rot_x') or hasattr(self, 'rot_y') or hasattr(self, 'rot_z'): + hpr = nodePath.getHpr() + self._safeUpdateSpinBox('rot_x', hpr.getX()) + self._safeUpdateSpinBox('rot_y', hpr.getY()) + self._safeUpdateSpinBox('rot_z', hpr.getZ()) + + # 安全地更新缩放控件(如果存在) + if hasattr(self, 'scale_x') or hasattr(self, 'scale_y') or hasattr(self, 'scale_z'): + scale = nodePath.getScale() + self._safeUpdateSpinBox('scale_x', scale.getX()) + self._safeUpdateSpinBox('scale_y', scale.getY()) + self._safeUpdateSpinBox('scale_z', scale.getZ()) + + except Exception as e: + print(f"刷新模型值显示失败: {e}") + + def _isPropertyPanelValid(self): + """检查属性面板是否仍然有效""" + try: + # 检查布局是否仍然存在且有效 + if not self._propertyLayout: + return False + + # 检查父控件是否仍然存在 + parent = self._propertyLayout.parent() + if not parent: + return False + + # 检查父控件是否仍然在窗口中 + return parent.isVisible() + except: + return False + + def _safeUpdateSpinBox(self, attr_name, value): + """安全地更新数值框""" + try: + spinbox = getattr(self, attr_name, None) + if spinbox and not spinbox.isHidden(): # 检查控件是否仍然存在且可见 + # 检查对象是否仍然有效 + spinbox.blockSignals(True) + spinbox.setKeyboardTracking(False) # 确保禁用键盘跟踪 + spinbox.setValue(value) + spinbox.blockSignals(False) + except RuntimeError as e: + # 对象已被删除 + setattr(self, attr_name, None) + print(f"警告: 数值框 {attr_name} 已被删除: {e}") + except Exception as e: + print(f"更新数值框 {attr_name} 时出错: {e}") + + # 在创建位置和变换控件时,增加安全检查 + def _createTransformControls(self, nodePath): + """创建变换控制控件""" + try: + # 清理旧的引用 + self._cleanupTransformControls() + + # 创建新的控件引用 + transform_layout = QGridLayout() + + # 位置控件 + transform_layout.addWidget(QLabel("相对位置"), 0, 0) + + # X坐标 + transform_layout.addWidget(QLabel("X:"), 1, 0) + self.pos_x = QLineEdit() + self.pos_x.setText(str(round(nodePath.getX(), 6))) + self.pos_x.editingFinished.connect(lambda: self._onPositionEditFinished(nodePath, 'x')) + transform_layout.addWidget(self.pos_x, 1, 1) + + # Y坐标 + transform_layout.addWidget(QLabel("Y:"), 1, 2) + self.pos_y = QLineEdit() + self.pos_y.setText(str(round(nodePath.getY(), 6))) + self.pos_y.editingFinished.connect(lambda: self._onPositionEditFinished(nodePath, 'y')) + transform_layout.addWidget(self.pos_y, 1, 3) + + # Z坐标 + transform_layout.addWidget(QLabel("Z:"), 1, 4) + self.pos_z = QLineEdit() + self.pos_z.setText(str(round(nodePath.getZ(), 6))) + self.pos_z.editingFinished.connect(lambda: self._onPositionEditFinished(nodePath, 'z')) + transform_layout.addWidget(self.pos_z, 1, 5) + + return transform_layout + + except Exception as e: + print(f"创建变换控件时出错: {e}") + return None + + def _onPositionEditFinished(self, nodePath, axis): + """位置编辑完成时的处理""" + try: + # 检查控件是否仍然有效 + if not hasattr(self, f'pos_{axis}') or getattr(self, f'pos_{axis}') is None: + return + + line_edit = getattr(self, f'pos_{axis}') + if line_edit is None or line_edit.isHidden(): + return + + # 获取文本并转换为数值 + text = line_edit.text() + try: + new_value = float(text) + except ValueError: + print(f"无效的数值输入: {text}") + # 恢复原来的值 + if axis == 'x': + line_edit.setText(str(round(nodePath.getX(), 6))) + elif axis == 'y': + line_edit.setText(str(round(nodePath.getY(), 6))) + elif axis == 'z': + line_edit.setText(str(round(nodePath.getZ(), 6))) + return + + # 根据轴设置位置 + if axis == 'x': + nodePath.setX(new_value) + elif axis == 'y': + nodePath.setY(new_value) + elif axis == 'z': + nodePath.setZ(new_value) + + print(f"位置已更新: {nodePath.getName()} {axis.upper()} = {new_value}") + + # 如果是坐标轴节点,需要更新坐标轴位置 + if hasattr(self.world, 'selection_manager'): + selection_manager = self.world.selection_manager + if (hasattr(selection_manager, 'gizmoTarget') and + selection_manager.gizmoTarget == nodePath): + # 更新坐标轴位置 + selection_manager._updateGizmoPositionAndOrientation() + + except Exception as e: + print(f"更新位置时出错: {e}") + def _createSafeSpinBox(self, min_val, max_val, read_only=False): + """创建安全的数值框""" + try: + spinbox = QDoubleSpinBox() + spinbox.setRange(min_val, max_val) + spinbox.setSingleStep(0.1) + if read_only: + spinbox.setReadOnly(True) + spinbox.setStyleSheet("background-color: #f0f0f0;") + return spinbox + except Exception as e: + print(f"创建数值框失败: {e}") + return None + + def _cleanupTransformControls(self): + """清理变换控件引用""" + control_names = ['pos_x', 'pos_y', 'pos_z', 'world_pos_x', 'world_pos_y', 'world_pos_z'] + for name in control_names: + if hasattr(self, name): + control = getattr(self, name) + if control: + try: + # 断开所有信号连接 + control.valueChanged.disconnect() + except: + pass + setattr(self, name, None) + + def _refreshWorldPos(self, model): + if not hasattr(self, 'worldXSpin'): + return + world = model.getPos(self.world.render) + self._worldXSpin.setValue(world.x) + self._worldYSpin.setValue(world.y) + self._worldZSpin.setValue(world.z) + + def _showCesiumTilesetProperties(self, nodePath, item): + """显示 Cesium tileset 属性""" + from PyQt5.QtWidgets import QLabel, QDoubleSpinBox, QPushButton, QGroupBox, QFormLayout + from PyQt5.QtCore import Qt + + print(f"显示 Cesium tileset 属性: {nodePath.getName()}") + + # 标题 + tileset_group = QGroupBox("Cesium 3D Tiles") + tileset_layout = QGridLayout() + tileset_layout.setColumnMinimumWidth(0, self.column_minimum_width) + # self._propertyLayout.addWidget(title) + + # URL 信息 + if nodePath.hasTag("tileset_url"): + url_label = QLabel("URL:") + tileset_layout.addWidget(url_label, 0, 0) + url_value = QLabel(nodePath.getTag("tileset_url")) + url_value.setWordWrap(True) + url_value.setStyleSheet("font-size: 9px; color: #666;") + tileset_layout.addWidget(url_value, 0, 1, 1, 3) + # self._propertyLayout.addWidget(url_label) + # self._propertyLayout.addWidget(url_value) + tileset_group.setLayout(tileset_layout) + self._propertyLayout.addWidget(tileset_group) + + # 位置控制 + pos_group = QGroupBox("位置") + pos_layout = QGridLayout() + pos_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + pos_row = QHBoxLayout() + pos_row.setContentsMargins(0, 0, 0, 0) + pos_row.setSpacing(4) + pos_layout.addWidget(QLabel("位置"), 0, 0) + + # X 坐标 + x_spin = QDoubleSpinBox() + x_spin.setRange(-10000, 10000) + x_spin.setSingleStep(1.0) + x_spin.setDecimals(2) + pos = nodePath.getPos() + x_spin.setValue(pos.getX()) + x_spin.valueChanged.connect(lambda v: self._updateTilesetPosition(nodePath, 'x', v)) + x_label = QLabel("X:") + x_label.setAlignment(Qt.AlignVCenter) + x_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + pos_row.addWidget(x_label) + pos_row.addWidget(x_spin) + + # Y 坐标 + y_spin = QDoubleSpinBox() + y_spin.setRange(-10000, 10000) + y_spin.setSingleStep(1.0) + y_spin.setDecimals(2) + y_spin.setValue(pos.getY()) + y_spin.valueChanged.connect(lambda v: self._updateTilesetPosition(nodePath, 'y', v)) + y_label = QLabel("Y:") + y_label.setAlignment(Qt.AlignVCenter) + y_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + pos_row.addWidget(y_label) + pos_row.addWidget(y_spin) + + # Z 坐标 + z_spin = QDoubleSpinBox() + z_spin.setRange(-10000, 10000) + z_spin.setSingleStep(1.0) + z_spin.setDecimals(2) + z_spin.setValue(pos.getZ()) + z_spin.valueChanged.connect(lambda v: self._updateTilesetPosition(nodePath, 'z', v)) + z_label = QLabel("Z:") + z_label.setAlignment(Qt.AlignVCenter) + z_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + pos_row.addWidget(z_label) + pos_row.addWidget(z_spin) + + pos_layout.addLayout(pos_row, 0, 1, 1, 3) + + pos_group.setLayout(pos_layout) + + # 缩放控制 + scale_label = QLabel("缩放") + pos_layout.addWidget(scale_label, 1, 0) + scale_spin = QDoubleSpinBox() + scale_spin.setRange(0.01, 1000) + scale_spin.setSingleStep(0.1) + scale_spin.setDecimals(2) + scale_spin.setValue(nodePath.getScale().getX()) + scale_spin.valueChanged.connect(lambda v: self._updateTilesetScale(nodePath, v)) + pos_layout.addWidget(scale_spin, 1, 1, 1, 3) + + self._propertyLayout.addWidget(pos_group) + + # # 删除按钮 + # delete_btn = QPushButton("删除 Tileset") + # delete_btn.setStyleSheet(""" + # QPushButton { + # background-color: #ff4444; + # color: white; + # border: none; + # padding: 8px; + # border-radius: 4px; + # margin-top: 10px; + # } + # QPushButton:hover { + # background-color: #ff6666; + # } + # """) + # delete_btn.clicked.connect(lambda: self._deleteCesiumTileset(nodePath, item)) + # self._propertyLayout.addWidget(delete_btn) + + # 添加弹性空间 + self._propertyLayout.addStretch() + + def _createPositionControl(self, label, nodePath, axis): + """创建位置控制控件""" + from PyQt5.QtWidgets import QHBoxLayout, QLabel, QDoubleSpinBox + + layout = QHBoxLayout() + + axis_label = QLabel(label) + axis_label.setFixedWidth(20) + layout.addWidget(axis_label) + + spinbox = QDoubleSpinBox() + spinbox.setRange(-10000, 10000) + spinbox.setSingleStep(1.0) + spinbox.setDecimals(2) + + # 获取当前坐标值 + pos = nodePath.getPos() + if axis == 'x': + spinbox.setValue(pos.getX()) + elif axis == 'y': + spinbox.setValue(pos.getY()) + elif axis == 'z': + spinbox.setValue(pos.getZ()) + + # 连接值变化信号 + def onValueChanged(value): + self._updateTilesetPosition(nodePath, axis, value) + + spinbox.valueChanged.connect(onValueChanged) + layout.addWidget(spinbox) + + return layout + + def _createScaleControl(self, nodePath): + """创建缩放控制控件""" + from PyQt5.QtWidgets import QHBoxLayout, QLabel, QDoubleSpinBox + + layout = QHBoxLayout() + + scale_label = QLabel("缩放:") + scale_label.setFixedWidth(40) + layout.addWidget(scale_label) + + spinbox = QDoubleSpinBox() + spinbox.setRange(0.01, 1000) + spinbox.setSingleStep(0.1) + spinbox.setDecimals(2) + spinbox.setValue(nodePath.getScale().getX()) # 假设均匀缩放 + + def onScaleChanged(value): + self._updateTilesetScale(nodePath, value) + + spinbox.valueChanged.connect(onScaleChanged) + layout.addWidget(spinbox) + + return layout + + def _updateTilesetPosition(self, nodePath, axis, value): + """更新 tileset 位置""" + try: + pos = nodePath.getPos() + if axis == 'x': + nodePath.setPos(value, pos.getY(), pos.getZ()) + elif axis == 'y': + nodePath.setPos(pos.getX(), value, pos.getZ()) + elif axis == 'z': + nodePath.setPos(pos.getX(), pos.getY(), value) + print(f"更新 {nodePath.getName()} 位置: {axis} = {value}") + except Exception as e: + print(f"更新位置失败: {e}") + + def _updateTilesetScale(self, nodePath, value): + """更新 tileset 缩放""" + try: + nodePath.setScale(value) + print(f"更新 {nodePath.getName()} 缩放: {value}") + except Exception as e: + print(f"更新缩放失败: {e}") + + def _deleteCesiumTileset(self, nodePath, item): + """删除 Cesium tileset""" + try: + from PyQt5.QtWidgets import QMessageBox + + reply = QMessageBox.question( + None, + '确认删除', + f'确定要删除 Cesium tileset "{nodePath.getName()}" 吗?', + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + + if reply == QMessageBox.Yes: + tree_widget = self.world.treeWidget + tree_widget.delete_item(nodePath) + # # 从场景中移除 + # nodePath.removeNode() + # + # # 从 tilesets 列表中移除 + # if hasattr(self.world, 'scene_manager'): + # tilesets_to_remove = [] + # for i, tileset_info in enumerate(self.world.scene_manager.tilesets): + # if tileset_info['node'] == nodePath: + # tilesets_to_remove.append(i) + # + # # 从后往前删除,避免索引问题 + # for i in reversed(tilesets_to_remove): + # del self.world.scene_manager.tilesets[i] + # + # # 更新场景树 + # self.world.scene_manager.updateSceneTree() + # + # # 清空属性面板 + # self.clearPropertyPanel() + + # print(f"成功删除 Cesium tileset: {nodePath.getName()}") + + except Exception as e: + print(f"删除 Cesium tileset 失败: {str(e)}") + + def _updateModelPropertyPanel(self, model): + """更新模型属性面板""" + + if hasattr(model, 'getTag') and model.getTag("is_gui_element") == "1": + self.updateGUIPropertyPanel(model) + return + + # 获取父节点 + parent = model.getParent() + + # 变换属性部分 (Transform) + self.transform_group = QGroupBox("变换 Transform") + transform_layout = QGridLayout() + transform_layout.setColumnMinimumWidth(0, self.column_minimum_width) + # transform_layout.setColumnStretch(1, 1) + # transform_layout.setColumnStretch(2, 1) + # transform_layout.setColumnStretch(3, 1) + + + # 获取当前值 + relativePos = model.getPos(parent) if parent else model.getPos() + worldPos = model.getPos(self.world.render) + + position_label = QLabel("位置") + transform_layout.addWidget(position_label, 0, 0) + + # 位置 (Position) + position_row = QHBoxLayout() + position_row.setContentsMargins(0, 0, 0, 0) + position_row.setSpacing(4) + # position_row.addWidget(QLabel("位置")) + + self.pos_x = QDoubleSpinBox() + self.pos_y = QDoubleSpinBox() + self.pos_z = QDoubleSpinBox() + + # 设置位置控件属性 + for pos_widget in [self.pos_x, self.pos_y, self.pos_z]: + pos_widget.setRange(-1000000.0, 1000000.0) + + self.pos_x.setValue(relativePos.getX()) + self.pos_y.setValue(relativePos.getY()) + self.pos_z.setValue(relativePos.getZ()) + + # 连接位置变化事件 + def updateXPosition(value): + model.setX(value) + self.refreshModelValues(model) + + self.pos_x.valueChanged.connect(updateXPosition) + + def updateYPosition(value): + model.setY(value) + self.refreshModelValues(model) + + self.pos_y.valueChanged.connect(updateYPosition) + + def updateZPosition(value): + model.setZ(value) + self.refreshModelValues(model) + + self.pos_z.valueChanged.connect(updateZPosition) + + # 水平排布 X, Y, Z + for axis_label, widget in (("X:", self.pos_x), ("Y:", self.pos_y), ("Z:", self.pos_z)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + position_row.addWidget(axis) + position_row.addWidget(widget) + + transform_layout.addLayout(position_row, 0, 1, 1, 3) + + # 世界位置 (只读) + world_position_row = QHBoxLayout() + world_position_row.setContentsMargins(0, 0, 0, 0) + world_position_row.setSpacing(4) + # world_position_row.addWidget(QLabel("世界位置")) + world_position_label = QLabel("世界位置") + transform_layout.addWidget(world_position_label, 1, 0) + + self.world_pos_x = QDoubleSpinBox() + self.world_pos_y = QDoubleSpinBox() + self.world_pos_z = QDoubleSpinBox() + + for world_pos_widget in [self.world_pos_x, self.world_pos_y, self.world_pos_z]: + world_pos_widget.setRange(-1000000.0, 1000000.0) + world_pos_widget.setReadOnly(True) + + self.world_pos_x.setValue(worldPos.getX()) + self.world_pos_y.setValue(worldPos.getY()) + self.world_pos_z.setValue(worldPos.getZ()) + + for axis_label, widget in (("X:", self.world_pos_x), ("Y:", self.world_pos_y), ("Z:", self.world_pos_z)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + world_position_row.addWidget(axis) + world_position_row.addWidget(widget) + + transform_layout.addLayout(world_position_row, 1, 1, 1, 3) + + # 旋转 (Rotation) + rotation_row = QHBoxLayout() + rotation_row.setContentsMargins(0, 0, 0, 0) + rotation_row.setSpacing(4) + rotation_label = QLabel("旋转") + transform_layout.addWidget(rotation_label, 2, 0) + + self.rot_x = QDoubleSpinBox() + self.rot_y = QDoubleSpinBox() + self.rot_z = QDoubleSpinBox() + + for rot_widget in [self.rot_x, self.rot_y, self.rot_z]: + rot_widget.setRange(-360, 360) + + self.rot_x.setValue(model.getH()) + self.rot_y.setValue(model.getP()) + self.rot_z.setValue(model.getR()) + + self.rot_x.valueChanged.connect(lambda v: model.setH(v)) + self.rot_y.valueChanged.connect(lambda v: model.setP(v)) + self.rot_z.valueChanged.connect(lambda v: model.setR(v)) + + for axis_label, widget in (("H:", self.rot_x), ("P:", self.rot_y), ("R:", self.rot_z)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + rotation_row.addWidget(axis) + rotation_row.addWidget(widget) + + transform_layout.addLayout(rotation_row, 2, 1, 1, 3) + + # 缩放 (Scale) + scale_row = QHBoxLayout() + scale_row.setContentsMargins(0, 0, 0, 0) + scale_row.setSpacing(4) + scale_label = QLabel("缩放") + transform_layout.addWidget(scale_label, 3, 0) + + self.scale_x = QDoubleSpinBox() + self.scale_y = QDoubleSpinBox() + self.scale_z = QDoubleSpinBox() + + current_scale = model.getScale() + + for scale_widget, scale_value in zip([self.scale_x, self.scale_y, self.scale_z], + [current_scale.getX(), current_scale.getY(), + current_scale.getZ()]): + scale_widget.setRange(-1000, 1000) + scale_widget.setSingleStep(0.1) + scale_widget.setValue(scale_value) + + self.scale_x.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_x, value)) + self.scale_y.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_y, value)) + self.scale_z.valueChanged.connect(lambda value: self._onScaleValueChanged(self.scale_z, value)) + + self.scale_x.valueChanged.connect(lambda value: self._updateXScale(model, value)) + self.scale_y.valueChanged.connect(lambda value: self._updateYScale(model, value)) + self.scale_z.valueChanged.connect(lambda value: self._updateZScale(model, value)) + + for axis_label, widget in (("X:", self.scale_x), ("Y:", self.scale_y), ("Z:", self.scale_z)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + scale_row.addWidget(axis) + scale_row.addWidget(widget) + + transform_layout.addLayout(scale_row, 3, 1, 1, 3) + + self.transform_group.setLayout(transform_layout) + self._propertyLayout.addWidget(self.transform_group) + + # 碰撞检测面板 + self._addCollisionPanel(model) + + # 动画和太阳方位角面板 + self._addAnimationPanel(model) + self._addSunAzimuthPanel() + # + # 材质属性组 + self._updateModelMaterialPanel(model) + + def _onScaleValueChanged(self, scale_widget, value): + pass + + def _updateXScale(self, model, value): + """更新X轴缩放值""" + # 确保值不为0 + if value == 0: + sender = None + # 通过遍历找到发出信号的控件 + for widget in [self.scale_x, self.scale_y, self.scale_z]: + if widget.value() == value: + sender = widget + break + if sender: + self._onScaleValueChanged(sender, value) + return + + # 更新模型的X轴缩放 + current_scale = model.getScale() + model.setScale(value, current_scale.getY(), current_scale.getZ()) + self.refreshModelValues(model) + + def _updateYScale(self, model, value): + """更新Y轴缩放值""" + # 确保值不为0 + if value == 0: + sender = None + # 通过遍历找到发出信号的控件 + for widget in [self.scale_x, self.scale_y, self.scale_z]: + if widget.value() == value: + sender = widget + break + if sender: + self._onScaleValueChanged(sender, value) + return + + # 更新模型的Y轴缩放 + current_scale = model.getScale() + model.setScale(current_scale.getX(), value, current_scale.getZ()) + self.refreshModelValues(model) + + def _updateZScale(self, model, value): + """更新Z轴缩放值""" + # 确保值不为0 + if value == 0: + sender = None + # 通过遍历找到发出信号的控件 + for widget in [self.scale_x, self.scale_y, self.scale_z]: + if widget.value() == value: + sender = widget + break + if sender: + self._onScaleValueChanged(sender, value) + return + + # 更新模型的Z轴缩放 + current_scale = model.getScale() + model.setScale(current_scale.getX(), current_scale.getY(), value) + self.refreshModelValues(model) + + def updateGUIPropertyPanel(self, gui_element, item): + """更新GUI元素属性面板""" + self.clearPropertyPanel() + + itemText = gui_element.getTag("name") or "未命名GUI元素" + + user_visible = True + user_visible = gui_element.getPythonTag("user_visible") + if user_visible is None: + user_visible = True + gui_element.setPythonTag("user_visible", True) + + self.name_group = QGroupBox("物体名称") + name_layout = QGridLayout() + self.active_check = QCheckBox() + # 根据元素的实际可见性状态设置复选框 + self.active_check.setChecked(user_visible) + self.name_input = QLineEdit(itemText) + + # 注意:对于GUI元素,我们需要特殊处理名称更新 + def updateGUIName(text): + # 更新GUI元素的标签 + gui_element.setTag("name", text) + self.world.treeWidget.update_item_name(self.name_input.text(), item) + # gui_element.setName(text) + # 如果有场景管理器,也需要更新场景树 + # if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): + # self.world.scene_manager.updateSceneTree() + + self.name_input.returnPressed.connect(lambda: updateGUIName(self.name_input.text())) + + # 如果失去焦点也更新名称 + self.name_input.editingFinished.connect(lambda: updateGUIName(self.name_input.text())) + + name_layout.addWidget(self.active_check, 0, 0) + name_layout.setColumnMinimumWidth(0, self.column_minimum_width) + name_layout.addWidget(self.name_input, 0, 1, 1, 3) + self.name_group.setLayout(name_layout) + self._propertyLayout.addWidget(self.name_group) + + if gui_element: + try: + self.active_check.stateChanged.disconnect() + except TypeError: + pass + self.active_check.stateChanged.connect( + lambda state, elem=gui_element: self._setUserVisible(elem, state == Qt.Checked) + ) + + gui_type = gui_element.getTag("gui_type") + gui_text = gui_element.getTag("gui_text") + + # GUI基本信息组 + gui_info_group = QGroupBox("GUI信息") + gui_info_layout = QGridLayout() + gui_info_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + # GUI类型显示 + gui_info_layout.addWidget(QLabel("GUI类型:"), 0, 0) + typeValue = QLabel(gui_type) + # typeValue.setStyleSheet("color: #00AAFF; font-weight: bold;") + gui_info_layout.addWidget(typeValue, 0, 1, 1, 3) + + # 修改 updateGUIPropertyPanel 中的文本属性部分 + # 文本属性(如果适用) + if gui_type in ["button", "label", "entry", "3d_text", "virtual_screen"]: + gui_info_layout.addWidget(QLabel("文本:"), 1, 0) + textEdit = QLineEdit(gui_text or "") + + # 使用编辑完成信号而不是文本变化信号 + def updateText(): + text = textEdit.text() + success = self.world.gui_manager.editGUIElement(gui_element, "text", text) + # if success: + # # 更新场景树显示的名称 + # if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): + # self.world.scene_manager.updateSceneTree() + + # 只在按下回车键或失去焦点时更新 + textEdit.returnPressed.connect(updateText) + textEdit.editingFinished.connect(updateText) + + gui_info_layout.addWidget(textEdit, 1, 1, 1, 3) + + gui_info_group.setLayout(gui_info_layout) + self._propertyLayout.addWidget(gui_info_group) + + # 变换属性组(合并位置和变换) + if hasattr(gui_element, 'getPos'): + # 根据GUI类型设置组名—— + if gui_type in ["button", "label", "entry", "2d_image", "2d_video_screen"]: + transform_group = QGroupBox("变换 Rect Transform") + else: + transform_group = QGroupBox("变换 Transform") + + transform_layout = QGridLayout() + transform_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + pos = gui_element.getPos() + + # 根据GUI类型决定位置编辑方式 + if gui_type in ["button", "label", "entry", "2d_image", "2d_video_screen"]: + # 2D GUI组件使用屏幕坐标 + logical_x = pos.getX() / 0.1 # 反向转换为逻辑坐标 + logical_z = pos.getZ() / 0.1 + + position_row = QHBoxLayout() + position_row.setContentsMargins(0, 0, 0, 0) + position_row.setSpacing(4) + + screen_title = QLabel("屏幕位置") + transform_layout.addWidget(screen_title, 0, 0) + + self.pos_x = QDoubleSpinBox() + self.pos_x.setRange(-1000, 1000) + self.pos_x.setValue(logical_x) + self.pos_z = QDoubleSpinBox() + self.pos_z.setRange(-1000, 1000) + self.pos_z.setValue(logical_z) + + axis_labels = [] + for col, (axis_label, widget) in enumerate((("X:", self.pos_x), ("Z:", self.pos_z)), start=1): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + axis_labels.append(axis) + position_row.addWidget(axis) + position_row.addWidget(widget) + + transform_layout.addLayout(position_row, 0, 1, 1, 3) + + # axis_width = max(label.sizeHint().width() for label in axis_labels) + # for label in axis_labels: + # label.setMinimumWidth(axis_width) + + # screen_position_row.setColumnStretch(2, 1) + # screen_position_row.setColumnStretch(4, 1) + + for i in range(4): + transform_layout.setColumnStretch(i, 1) + actual_position_row = QGridLayout() + + actual_row = QHBoxLayout() + actual_row.setContentsMargins(0, 0, 0, 0) + actual_row.setSpacing(4) + + actual_title = QLabel("实际坐标") + # actual_row.addWidget(actual_title) + transform_layout.addWidget(actual_title, 1, 0) + + self.actual_pos_x_label = QLabel(f"{pos.getX():.3f}") + self.actual_pos_x_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + self.actual_pos_x_label.setStyleSheet("color: gray; font-size: 10px;") + self.actual_pos_z_label = QLabel(f"{pos.getZ():.3f}") + self.actual_pos_z_label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + self.actual_pos_z_label.setStyleSheet("color: gray; font-size: 10px;") + + for col, (axis_label, widget) in enumerate((("X:", self.actual_pos_x_label), + ("Z:", self.actual_pos_z_label)), start=1): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + # axis.setMinimumWidth(axis_width) + # actual_position_row.addWidget(axis, 0, col * 2 - 1, Qt.AlignLeft | Qt.AlignVCenter) + # actual_position_row.addWidget(widget, 0, col * 2, Qt.AlignLeft | Qt.AlignVCenter) + actual_row.addWidget(axis) + actual_row.addWidget(widget) + + # actual_position_row.setColumnStretch(2, 1) + # actual_position_row.setColumnStretch(4, 1) + + transform_layout.addLayout(actual_row, 1, 1, 1, 3) + + def _update_actual_position_labels(): + current_pos = gui_element.getPos() + self.actual_pos_x_label.setText(f"{current_pos.getX():.3f}") + self.actual_pos_z_label.setText(f"{current_pos.getZ():.3f}") + + _update_actual_position_labels() + + def _on_gui_pos_x_changed(value): + self.world.gui_manager.editGUI2DPosition(gui_element, "x", value) + _update_actual_position_labels() + + def _on_gui_pos_z_changed(value): + self.world.gui_manager.editGUI2DPosition(gui_element, "z", value) + _update_actual_position_labels() + + self.pos_x.valueChanged.connect(_on_gui_pos_x_changed) + self.pos_z.valueChanged.connect(_on_gui_pos_z_changed) + + if gui_type in ["2d_image", "2d_video_screen"]: + scale = gui_element.getScale() + width = scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, + (tuple, list)) else scale + height = scale.getZ() if hasattr(scale, 'getZ') else scale[1] if isinstance(scale, + (tuple, list)) and len( + scale) > 1 else scale + + scale_row = QHBoxLayout() + scale_row.setContentsMargins(0, 0, 0, 0) + scale_row.setSpacing(4) + transform_layout.addWidget(QLabel("缩放"), 2, 0) + + self.scale_x = QDoubleSpinBox() + self.scale_x.setRange(0.1, 100) + self.scale_x.setSingleStep(0.01) + self.scale_x.setValue(width) + self.scale_x.valueChanged.connect(lambda v: self.editGUIScale(gui_element, "x", v)) + + self.scale_z = QDoubleSpinBox() + self.scale_z.setRange(0.1, 10) + self.scale_z.setSingleStep(0.01) + self.scale_z.setValue(height) + self.scale_z.valueChanged.connect(lambda v: self.editGUIScale(gui_element, "z", v)) + + for axis_label, widget in (("X:", self.scale_x), ("Z:", self.scale_z)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + scale_row.addWidget(axis) + scale_row.addWidget(widget) + + transform_layout.addLayout(scale_row, 2, 1, 1, 3) + + else: + scale_row = QHBoxLayout() + scale_row.setContentsMargins(0, 0, 0, 0) + scale_row.setSpacing(4) + transform_layout.addWidget(QLabel("缩放"), 2, 0) + + scaleSpinBox = QDoubleSpinBox() + scaleSpinBox.setRange(0.01, 100) + scaleSpinBox.setSingleStep(0.01) + scaleSpinBox.setValue(gui_element.getScale().getX() * 2) + scaleSpinBox.valueChanged.connect(lambda v: self._update2DImageScale(gui_element, v)) + + axis = QLabel("X:") + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + scale_row.addWidget(axis) + scale_row.addWidget(scaleSpinBox) + transform_layout.addLayout(scale_row, 2, 1, 1, 3) + + else: + # 3D GUI组件使用世界坐标 + position_row = QHBoxLayout() + position_row.setContentsMargins(0, 0, 0, 0) + position_row.setSpacing(4) + # position_row.addWidget(QLabel("位置")) + position_label = QLabel("位置") + transform_layout.addWidget(position_label, 0, 0) + + self.pos_x = QDoubleSpinBox() + self.pos_x.setRange(-100, 100) + self.pos_x.setValue(pos.getX()) + self.pos_x.setSingleStep(0.01) + self.pos_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "x", v)) + + self.pos_y = QDoubleSpinBox() + self.pos_y.setRange(-100, 100) + self.pos_y.setValue(pos.getY()) + self.pos_y.setSingleStep(0.01) + self.pos_y.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "y", v)) + + self.pos_z = QDoubleSpinBox() + self.pos_z.setRange(-100, 100) + self.pos_z.setValue(pos.getZ()) + self.pos_z.setSingleStep(0.01) + self.pos_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUI3DPosition(gui_element, "z", v)) + + for axis_label, widget in (("X:", self.pos_x), ("Y:", self.pos_y), ("Z:", self.pos_z)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + position_row.addWidget(axis) + position_row.addWidget(widget) + + transform_layout.addLayout(position_row, 0, 1, 1, 3) + + scale = gui_element.getScale() + scale_row = QHBoxLayout() + scale_row.setContentsMargins(0, 0, 0, 0) + scale_row.setSpacing(4) + # scale_row.addWidget(QLabel("缩放")) + scale_label = QLabel("缩放") + transform_layout.addWidget(scale_label, 1, 0) + + self.scale_x = QDoubleSpinBox() + self.scale_x.setRange(0.01, 100) + self.scale_x.setSingleStep(0.01) + self.scale_x.setValue( + scale.getX() if hasattr(scale, 'getX') else scale[0] if isinstance(scale, (tuple, list)) else scale) + self.scale_x.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "x", v)) + + self.scale_y = QDoubleSpinBox() + self.scale_y.setRange(0.01, 100) + self.scale_y.setSingleStep(0.01) + self.scale_y.setValue( + scale.getY() if hasattr(scale, 'getY') else scale[1] if isinstance(scale, (tuple, list)) and len( + scale) > 1 else scale) + self.scale_y.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "y", v)) + + self.scale_z = QDoubleSpinBox() + self.scale_z.setRange(0.01, 100) + self.scale_z.setSingleStep(0.01) + self.scale_z.setValue( + scale.getZ() if hasattr(scale, 'getZ') else scale[2] if isinstance(scale, (tuple, list)) and len( + scale) > 2 else scale) + self.scale_z.valueChanged.connect(lambda v: self.world.gui_manager.editGUIScale(gui_element, "z", v)) + + for axis_label, widget in (("X:", self.scale_x), ("Y:", self.scale_y), ("Z:", self.scale_z)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + scale_row.addWidget(axis) + scale_row.addWidget(widget) + + transform_layout.addLayout(scale_row, 1, 1, 1, 3) + + transform_group.setLayout(transform_layout) + self._propertyLayout.addWidget(transform_group) + + # 为2D图像和视频屏幕添加Sort属性 + if gui_type in ["2d_image", "2d_video_screen", "info_panel"]: + sort_group = QGroupBox("显示顺序") + sort_layout = QGridLayout() + sort_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + # 获取当前sort值,如果没有设置则默认为0 + current_sort = int(gui_element.getTag("sort") or "0") + + sort_layout.addWidget(QLabel("显示优先度:"), 0, 0) + sort_spin = QSpinBox() + sort_spin.setRange(-1000, 1000) + sort_spin.setValue(current_sort) + + def updateSort(value): + try: + # 设置标签 + gui_element.setTag("sort", str(value)) + # 应用sort到节点 + gui_element.setBin("fixed", value) + print(f"✓ 更新{gui_type}渲染顺序: {value}") + except Exception as e: + print(f"✗ 更新{gui_type}渲染顺序失败: {e}") + + sort_spin.valueChanged.connect(updateSort) + sort_layout.addWidget(sort_spin, 0, 1, 1, 3) + + sort_group.setLayout(sort_layout) + self._propertyLayout.addWidget(sort_group) + + # 外观属性组 - 添加字体颜色选择 + if gui_type in ["button", "label", "3d_text"]: + appearance_group = QGroupBox("外观属性") + appearance_layout = QGridLayout() + appearance_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + # 字体颜色选择 + appearance_layout.addWidget(QLabel("字体颜色:"), 0, 0) + colorButton = QPushButton("选择颜色") + colorButton.clicked.connect(lambda checked, elem=gui_element: self._selectGUIColor(elem)) + appearance_layout.addWidget(colorButton, 0, 1, 1, 3) + + appearance_group.setLayout(appearance_layout) + self._propertyLayout.addWidget(appearance_group) + + # 外观属性组 + if gui_type in ["button", "label"]: + appearance_group = QGroupBox("外观属性") + appearance_layout = QGridLayout() + appearance_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + appearance_layout.addWidget(QLabel("背景颜色:"), 0, 0) + + colorButton = QPushButton("选择颜色") + colorButton.clicked.connect(lambda: self.world.gui_manager.selectGUIColor(gui_element)) + appearance_layout.addWidget(colorButton, 0, 1, 1, 3) + + appearance_group.setLayout(appearance_layout) + self._propertyLayout.addWidget(appearance_group) + + if gui_type == "3d_image": + image_group = QGroupBox("图片设置") + image_layout = QGridLayout() + image_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + # 当前图片路径标签和 + current_image_label = QLabel("当前图片:") + image_layout.addWidget(current_image_label, 0, 0) + + # 显示当前贴图路径(简化显示) + current_texture_path = gui_element.getTag("texture_path") or "未设置" + if current_texture_path != "未设置": + display_path = current_texture_path if len(current_texture_path) <= 10 else current_texture_path[ + :7] + "..." + else: + display_path = current_texture_path + texture_label = QLabel(display_path) + texture_label.setWordWrap(True) + texture_label.setToolTip(current_texture_path if current_texture_path != "未设置" else "") + image_layout.addWidget(texture_label, 0, 1, 1, 3) + + # 选择图片按钮 + if has_icon("property_select_image"): + select_texture_button = QPushButton(get_icon("property_select_image"), "选择图片...") + else: + select_texture_button = QPushButton("选择图片...") + image_layout.addWidget(select_texture_button, 1, 0, 1, 4) + + def onSelectTexture(): + from PyQt5.QtWidgets import QFileDialog + file_path, _ = QFileDialog.getOpenFileName( + None, + "选择图片", + "", + "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga *.dds)" + ) + if file_path: + # 应用新纹理到 3D Image + file_path = util.normalize_model_path(file_path) + success = self.world.gui_manager.update3DImageTexture(gui_element, file_path) + if success: + # 保存路径到 Tag + gui_element.setTag("texture_path", file_path) + gui_element.setTag("gui_image_path", file_path) + # 更新显示 + texture_label.setText(file_path) + # 可选:刷新场景树或其他 UI + # self.world.scene_manager.updateSceneTree() + + select_texture_button.clicked.connect(onSelectTexture) + + image_group.setLayout(image_layout) + self._propertyLayout.addWidget(image_group) + + if gui_type in ["2d_image"]: + image_group = QGroupBox("2D图片设置") + image_layout = QGridLayout() + image_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + # 当前图片路径标签 + current_image_label = QLabel("当前图片:") + image_layout.addWidget(current_image_label, 0, 0) + + # 显示当前贴图路径(简化显示) + current_texture_path = gui_element.getTag("texture_path") or gui_element.getTag("image_path") or "未设置" + if current_texture_path != "未设置": + display_path = current_texture_path if len(current_texture_path) <= 10 else current_texture_path[ + :7] + "..." + else: + display_path = current_texture_path + texture_label = QLabel(display_path) + texture_label.setWordWrap(True) + image_layout.addWidget(texture_label, 0, 1, 1, 3) + + # 选择图片按钮 + if has_icon("property_select_image"): + select_texture_button = QPushButton(get_icon("property_select_image"), "选择图片...") + else: + select_texture_button = QPushButton("选择图片...") + # select_texture_button = QPushButton("选择图片...") + image_layout.addWidget(select_texture_button, 1, 0, 1, 4) + + def onSelect2DTexture(): + from PyQt5.QtWidgets import QFileDialog + file_path, _ = QFileDialog.getOpenFileName( + None, + "选择图片", + "", + "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga *.dds)" + ) + if file_path: + # 应用新纹理到 2D Image + file_path = util.normalize_model_path(file_path) + success = self.update2DImageTexture(gui_element, file_path) + if success: + # 保存路径到 Tag + gui_element.setTag("texture_path", file_path) + gui_element.setTag("image_path", file_path) + # 更新显示 + texture_label.setText(file_path) + # 可选:刷新场景树或其他 UI + # self.world.scene_manager.updateSceneTree() + + select_texture_button.clicked.connect(onSelect2DTexture) + + image_group.setLayout(image_layout) + self._propertyLayout.addWidget(image_group) + + # 添加弹性空间 + + if gui_type == "video_screen": + self._addVideoScreenProperties(gui_element, item) + elif gui_type == "2d_video_screen": + self._add2DVideoScreenProperties(gui_element, item) + elif gui_type == "spherical_video": + self._addSphericalVideoProperties(gui_element) + elif gui_type in ['info_panel', 'info_panel_3d']: + self._addInfoPanelProperties(gui_element) + + self._propertyLayout.addStretch() + + # 强制更新布局 + if self._propertyLayout: + self._propertyLayout.update() + propertyWidget = self._propertyLayout.parentWidget() + if propertyWidget: + propertyWidget.update() + + def _addInfoPanelProperties(self, info_panel): + """为信息面板添加属性控制面板 - 同时适配2D和3D信息面板""" + try: + from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, + QDoubleSpinBox, QColorDialog, QSpinBox, QTextEdit) + from PyQt5.QtCore import Qt, QTimer + from PyQt5.QtGui import QColor + import os + + panel_id = info_panel.getTag("panel_id") + if not panel_id: + print("无法找到信息面板ID") + return + + # 检测面板类型 (2D或3D) + gui_type = info_panel.getTag("gui_type") + is_3d_panel = gui_type == "info_panel_3d" + + # 获取面板当前属性 + current_size = (1.0, 0.6) + current_position = (0, 0) if not is_3d_panel else (0, 0, 0) + current_bg_color = (0.15, 0.15, 0.15, 0.9) + current_border_color = (0.3, 0.3, 0.3, 1.0) + current_title_color = (1.0, 1.0, 0.0, 1.0) + current_content_color = (0.9, 0.9, 0.9, 1.0) + current_title_size = 0.06 # 默认值 + current_content_size = 0.045 # 默认值 + current_title_text = "信息面板" + current_content_text = "这是一个信息面板" + current_bg_image = "" + current_sort = 0 + + # 标题和内容位置默认值 + current_title_pos = (0, 0) + current_content_pos = (-0.45, 0) + + # 从info_panel_manager获取当前属性 + if hasattr(self.world, 'info_panel_manager'): + panel_data = self.world.info_panel_manager.panels.get(panel_id) + if panel_data: + # 获取基本属性 + if 'properties' in panel_data: + props = panel_data['properties'] + current_size = props.get('size', current_size) + current_position = props.get('position', current_position) + current_bg_color = props.get('bg_color', current_bg_color) + current_border_color = props.get('border_color', current_border_color) + current_title_color = props.get('title_color', current_title_color) + current_content_color = props.get('content_color', current_content_color) + current_bg_image = props.get('bg_image', current_bg_image) + + # 获取文本内容和位置(根据面板类型区分处理) + if not is_3d_panel: # 2D面板 + if 'title_label' in panel_data and panel_data['title_label']: + current_title_text = panel_data['title_label']['text'] + # 获取标题位置 + title_pos = panel_data['title_label'].getPos() + current_title_pos = (title_pos.getX(), title_pos.getZ()) + + if 'content_label' in panel_data and panel_data['content_label']: + current_content_text = panel_data['content_label']['text'] + # 获取内容位置 + content_pos = panel_data['content_label'].getPos() + current_content_pos = (content_pos.getX(), content_pos.getZ()) + + # 获取字体大小 + if 'title_label' in panel_data and panel_data['title_label']: + try: + title_scale = panel_data['title_label']['text_scale'] + # 如果是元组,取第一个值 + if isinstance(title_scale, (tuple, list)): + current_title_size = title_scale[0] if len(title_scale) > 0 else 0.06 + else: + current_title_size = title_scale if isinstance(title_scale, (int, float)) else 0.06 + except: + current_title_size = 0.06 + + if 'content_label' in panel_data and panel_data['content_label']: + try: + content_scale = panel_data['content_label']['text_scale'] + # 如果是元组,取第一个值 + if isinstance(content_scale, (tuple, list)): + current_content_size = content_scale[0] if len(content_scale) > 0 else 0.045 + else: + current_content_size = content_scale if isinstance(content_scale, + (int, float)) else 0.045 + except: + current_content_size = 0.045 + else: # 3D面板 + if 'title_node' in panel_data and panel_data['title_node']: + current_title_text = panel_data['title_node'].getText() + + if 'content_node' in panel_data and panel_data['content_node']: + current_content_text = panel_data['content_node'].getText() + + if 'title_text' in panel_data and panel_data['title_text']: + current_title_size = panel_data['title_text'].getScale().getX() + + if 'content_text' in panel_data and panel_data['content_text']: + current_content_size = panel_data['content_text'].getScale().getX() + + # 获取sort值 + if info_panel.hasTag("sort"): + try: + current_sort = int(info_panel.getTag("sort")) + except: + current_sort = 0 + + # 背景图片组 + image_group = QGroupBox("背景图片设置") + image_layout = QGridLayout() + image_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + # 当前背景图片路径标签 + current_image_label = QLabel("当前背景图片:") + image_layout.addWidget(current_image_label, 0, 0) + + # 显示当前背景图片路径(简化显示) + if current_bg_image: + display_path = current_bg_image if len(current_bg_image) <= 12 else current_bg_image[:9] + "..." + else: + display_path = "未设置" + texture_label = QLabel(display_path) + texture_label.setWordWrap(True) + texture_label.setToolTip(current_bg_image if current_bg_image else "") + image_layout.addWidget(texture_label, 0, 1, 1, 3) + + # 选择背景图片按钮 + select_texture_button = QPushButton("选择背景图片...") + image_layout.addWidget(select_texture_button, 1, 0, 1, 4) + + # 清除背景图片按钮 + clear_texture_button = QPushButton("清除背景图片") + clear_texture_button.setStyleSheet("color: red;") + image_layout.addWidget(clear_texture_button, 2, 0, 1, 4) + + def onSelectBackgroundImage(): + """选择背景图片""" + from PyQt5.QtWidgets import QFileDialog + file_path, _ = QFileDialog.getOpenFileName( + None, + "选择背景图片", + "", + "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga *.dds)" + ) + if file_path: + # 应用新背景图片到信息面板 + file_path = util.normalize_model_path(file_path) + success = self.updateInfoPanelBackgroundImage(info_panel, file_path) + if success: + # 保存路径到 Tag + info_panel.setTag("bg_image_path", file_path) + # 更新显示 + display_path = file_path if len(file_path) <= 12 else file_path[:9] + "..." + texture_label.setText(display_path) + texture_label.setToolTip(file_path) + + def onClearBackgroundImage(): + """清除背景图片""" + success = self.updateInfoPanelBackgroundImage(info_panel, None) + if success: + # 清除路径标签 + info_panel.clearTag("bg_image_path") + texture_label.setText("未设置") + texture_label.setToolTip("") + + select_texture_button.clicked.connect(onSelectBackgroundImage) + clear_texture_button.clicked.connect(onClearBackgroundImage) + + image_group.setLayout(image_layout) + self._propertyLayout.addWidget(image_group) + + # 显示顺序组 + sort_group = QGroupBox("显示顺序") + sort_layout = QGridLayout() + sort_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + sort_layout.addWidget(QLabel("显示优先度:"), 0, 0) + sort_spin = QSpinBox() + sort_spin.setRange(-1000, 1000) + sort_spin.setValue(current_sort) + + def updateSort(value): + try: + # 设置标签 + info_panel.setTag("sort", str(value)) + # 应用sort到节点 + info_panel.setBin("fixed", value) + print(f"✓ 更新信息面板渲染顺序: {value}") + except Exception as e: + print(f"✗ 更新信息面板渲染顺序失败: {e}") + + sort_spin.valueChanged.connect(updateSort) + sort_layout.addWidget(sort_spin, 0, 1, 1, 3) + + sort_group.setLayout(sort_layout) + self._propertyLayout.addWidget(sort_group) + + # 面板大小设置组 + size_group = QGroupBox() + size_group.setTitle("面板大小设置") + size_layout = QGridLayout() + size_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + size_row = QHBoxLayout() + size_row.setContentsMargins(0, 0, 0, 0) + size_row.setSpacing(4) + + size_title = QLabel("面板大小设置:") + size_layout.addWidget(size_title, 0, 0) + + width_spin = QDoubleSpinBox() + width_spin.setRange(0.1, 5.0) + width_spin.setSingleStep(0.1) + width_spin.setValue(current_size[0]) + + height_spin = QDoubleSpinBox() + height_spin.setRange(0.1, 5.0) + height_spin.setSingleStep(0.1) + height_spin.setValue(current_size[1]) + + for label_text, spinbox in (("宽度:", width_spin), ("高度:", height_spin)): + label = QLabel(label_text) + label.setAlignment(Qt.AlignVCenter) + label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + size_row.addWidget(label) + size_row.addWidget(spinbox) + + size_layout.addLayout(size_row, 0, 1, 1, 3) + + # 连接大小变化信号 + def onSizeChanged(): + if hasattr(self.world, 'info_panel_manager'): + panel_width = width_spin.value() + panel_height = height_spin.value() + + # 根据面板类型选择更新方法 + if is_3d_panel: + self.world.info_panel_manager.update3DPanelProperties( + panel_id, size=(panel_width, panel_height)) + else: + self.world.info_panel_manager.updatePanelProperties( + panel_id, size=(panel_width, panel_height)) + + # 同步更新内容的换行设置 + self._adjustContentWordwrap(panel_id, panel_width, content_size_spin.value()) + + width_spin.valueChanged.connect(onSizeChanged) + height_spin.valueChanged.connect(onSizeChanged) + + size_group.setLayout(size_layout) + self._propertyLayout.addWidget(size_group) + + # 位置设置:X:[box] Y:[box]/Z:[box],右对齐可伸缩 + position_group = QGroupBox() + position_group.setTitle("位置设置") + position_layout = QGridLayout() + position_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + position_row = QHBoxLayout() + position_row.setContentsMargins(0, 0, 0, 0) + position_row.setSpacing(4) + + position_title = QLabel("位置设置:") + position_layout.addWidget(position_title, 0, 0) + + if is_3d_panel: + # 3D 面板位置:X、Y、Z + pos_x_spin = QDoubleSpinBox() + pos_x_spin.setRange(-1000, 1000) + pos_x_spin.setSingleStep(0.1) + pos_x_spin.setValue(current_position[0] if len(current_position) > 0 else 0) + + pos_y_spin = QDoubleSpinBox() + pos_y_spin.setRange(-1000, 1000) + pos_y_spin.setSingleStep(0.1) + pos_y_spin.setValue(current_position[1] if len(current_position) > 1 else 0) + + pos_z_spin = QDoubleSpinBox() + pos_z_spin.setRange(-1000, 1000) + pos_z_spin.setSingleStep(0.1) + pos_z_spin.setValue(current_position[2] if len(current_position) > 2 else 0) + + axis_widgets = (("X:", pos_x_spin), ("Y:", pos_y_spin), ("Z:", pos_z_spin)) + + def on3DPositionChanged(): + if hasattr(self.world, "info_panel_manager"): + self.world.info_panel_manager.update3DPanelProperties( + panel_id, position=(pos_x_spin.value(), pos_y_spin.value(), pos_z_spin.value())) + + pos_x_spin.valueChanged.connect(on3DPositionChanged) + pos_y_spin.valueChanged.connect(on3DPositionChanged) + pos_z_spin.valueChanged.connect(on3DPositionChanged) + else: + # 2D 面板位置:X、Z + pos_x_spin = QDoubleSpinBox() + pos_x_spin.setRange(-1000, 1000) + pos_x_spin.setSingleStep(0.1) + pos_x_spin.setValue(current_position[0] if len(current_position) > 0 else 0) + + pos_z_spin = QDoubleSpinBox() + pos_z_spin.setRange(-1000, 1000) + pos_z_spin.setSingleStep(0.1) + pos_z_spin.setValue(current_position[1] if len(current_position) > 1 else 0) + + axis_widgets = (("X:", pos_x_spin), ("Z:", pos_z_spin)) + + def on2DPositionChanged(): + if hasattr(self.world, "info_panel_manager"): + self.world.info_panel_manager.updatePanelProperties( + panel_id, position=(pos_x_spin.value(), pos_z_spin.value())) + + pos_x_spin.valueChanged.connect(on2DPositionChanged) + pos_z_spin.valueChanged.connect(on2DPositionChanged) + + for axis_label, spinbox in axis_widgets: + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + position_row.addWidget(axis) + position_row.addWidget(spinbox) + + position_layout.addLayout(position_row, 0, 1, 1, 3) + position_group.setLayout(position_layout) + self._propertyLayout.addWidget(position_group) + + # 边框属性组 + border_group = QGroupBox("边框属性") + border_layout = QGridLayout() + border_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + border_layout.addWidget(QLabel("边框颜色:"), 0, 0) + # border_layout.addWidget(border_color_label, 0, 0) + + border_row_top = QHBoxLayout() + border_row_top.setContentsMargins(0, 0, 0, 0) + border_row_top.setSpacing(4) + + border_r_spin = QDoubleSpinBox() + border_r_spin.setRange(0.0, 1.0) + border_r_spin.setSingleStep(0.01) + border_r_spin.setValue(current_border_color[0]) + color_r_bg = QLabel("R:") + color_r_bg.setAlignment(Qt.AlignVCenter) + color_r_bg.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + border_row_top.addWidget(color_r_bg) + border_row_top.addWidget(border_r_spin) + + border_g_spin = QDoubleSpinBox() + border_g_spin.setRange(0.0, 1.0) + border_g_spin.setSingleStep(0.01) + border_g_spin.setValue(current_border_color[1]) + color_g = QLabel("G:") + color_g.setAlignment(Qt.AlignVCenter) + color_g.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + border_row_top.addWidget(color_g) + border_row_top.addWidget(border_g_spin) + + border_layout.addLayout(border_row_top, 0, 1, 1, 3) + + indent_placeholder = QLabel("") + border_layout.addWidget(indent_placeholder, 1, 0) + # indent_placeholder.setFixedWidth(border_color_label.sizeHint().width()) + # border_layout.addWidget(indent_placeholder, 1, 0) + + border_row_bottom = QHBoxLayout() + border_row_bottom.setContentsMargins(0, 0, 0, 0) + border_row_bottom.setSpacing(4) + + border_b_spin = QDoubleSpinBox() + border_b_spin.setRange(0.0, 1.0) + border_b_spin.setSingleStep(0.01) + border_b_spin.setValue(current_border_color[2]) + color_b = QLabel("B:") + color_b.setAlignment(Qt.AlignVCenter) + color_b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + border_row_bottom.addWidget(color_b) + border_row_bottom.addWidget(border_b_spin) + + border_a_spin = QDoubleSpinBox() + border_a_spin.setRange(0.0, 1.0) + border_a_spin.setSingleStep(0.01) + border_a_spin.setValue(current_border_color[3]) + color_a = QLabel("A:") + color_a.setAlignment(Qt.AlignVCenter) + color_a.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + border_row_bottom.addWidget(color_a) + border_row_bottom.addWidget(border_a_spin) + + border_layout.addLayout(border_row_bottom, 1, 1, 1, 3) + + border_color_button = QPushButton("选择边框颜色") + border_color_button.clicked.connect(lambda: self._selectInfoPanelBorderColor( + info_panel, border_r_spin, border_g_spin, border_b_spin, border_a_spin)) + border_layout.addWidget(border_color_button, 2, 0, 1, 4) + + # 连接边框颜色变化信号 + def onBorderColorChanged(): + if hasattr(self.world, 'info_panel_manager'): + border_color = ( + border_r_spin.value(), + border_g_spin.value(), + border_b_spin.value(), + border_a_spin.value() + ) + # 根据面板类型选择更新方法 + if is_3d_panel: + self.world.info_panel_manager.update3DPanelProperties( + panel_id, border_color=border_color) + else: + self.world.info_panel_manager.updatePanelProperties( + panel_id, border_color=border_color) + + border_r_spin.valueChanged.connect(onBorderColorChanged) + border_g_spin.valueChanged.connect(onBorderColorChanged) + border_b_spin.valueChanged.connect(onBorderColorChanged) + border_a_spin.valueChanged.connect(onBorderColorChanged) + + border_group.setLayout(border_layout) + self._propertyLayout.addWidget(border_group) + + bg_color_group = QGroupBox("背景颜色设置") + bg_color_layout = QGridLayout() + bg_color_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + bg_color_label = QLabel("背景颜色:") + bg_color_layout.addWidget(bg_color_label, 0, 0) + bg_color_row_top = QHBoxLayout() + bg_color_row_top.setContentsMargins(0, 0, 0, 0) + bg_color_row_top.setSpacing(4) + + # R分量 + color_r_bg = QLabel("R:") + color_r_bg.setAlignment(Qt.AlignVCenter) + color_r_bg.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + r_spin = QDoubleSpinBox() + r_spin.setRange(0.0, 1.0) + r_spin.setSingleStep(0.01) + r_spin.setValue(current_bg_color[0]) + bg_color_row_top.addWidget(color_r_bg) + bg_color_row_top.addWidget(r_spin) + + # G分量 + color_g_bg = QLabel("G:") + color_g_bg.setAlignment(Qt.AlignVCenter) + color_g_bg.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + g_spin = QDoubleSpinBox() + g_spin.setRange(0.0, 1.0) + g_spin.setSingleStep(0.01) + g_spin.setValue(current_bg_color[1]) + bg_color_row_top.addWidget(color_g_bg) + bg_color_row_top.addWidget(g_spin) + + bg_color_layout.addLayout(bg_color_row_top, 0, 1, 1, 3) + + indent_placeholder = QLabel("") + bg_color_layout.addWidget(indent_placeholder, 1, 0) + + bg_color_row_bottom = QHBoxLayout() + bg_color_row_bottom.setContentsMargins(0, 0, 0, 0) + bg_color_row_bottom.setSpacing(4) + + # B分量 + color_b_bg = QLabel("B:") + color_b_bg.setAlignment(Qt.AlignVCenter) + color_b_bg.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + b_spin = QDoubleSpinBox() + b_spin.setRange(0.0, 1.0) + b_spin.setSingleStep(0.01) + b_spin.setValue(current_bg_color[2]) + bg_color_row_bottom.addWidget(color_b_bg) + bg_color_row_bottom.addWidget(b_spin) + + # A分量 + color_a_bg = QLabel("A:") + color_a_bg.setAlignment(Qt.AlignVCenter) + color_a_bg.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + a_spin = QDoubleSpinBox() + a_spin.setRange(0.0, 1.0) + a_spin.setSingleStep(0.01) + a_spin.setValue(current_bg_color[3]) + bg_color_row_bottom.addWidget(color_a_bg) + bg_color_row_bottom.addWidget(a_spin) + + bg_color_layout.addLayout(bg_color_row_bottom, 1, 1, 1, 3) + + # 颜色选择按钮 + color_button = QPushButton("选择颜色") + color_button.clicked.connect(lambda: self._selectInfoPanelBackgroundColor( + info_panel, r_spin, g_spin, b_spin, a_spin)) + bg_color_layout.addWidget(color_button, 2, 0, 1, 4) + + # 连接颜色数值变化信号,自动更新背景颜色 + def onBackgroundColorChanged(): + self._applyInfoPanelBackgroundColor( + info_panel, r_spin.value(), g_spin.value(), b_spin.value(), a_spin.value()) + + r_spin.valueChanged.connect(onBackgroundColorChanged) + g_spin.valueChanged.connect(onBackgroundColorChanged) + b_spin.valueChanged.connect(onBackgroundColorChanged) + a_spin.valueChanged.connect(onBackgroundColorChanged) + + bg_color_group.setLayout(bg_color_layout) + self._propertyLayout.addWidget(bg_color_group) + + # 标题属性组 + title_group = QGroupBox("标题属性") + title_layout = QGridLayout() + title_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + # 标题文本 + title_layout.addWidget(QLabel("标题文本:"), 0, 0) + title_text_edit = QTextEdit() + title_text_edit.setMaximumHeight(60) + title_text_edit.setPlainText(current_title_text) + title_layout.addWidget(title_text_edit, 0, 1, 1, 3) + + # 标题颜色 + title_layout.addWidget(QLabel("标题颜色:"), 1, 0) + + title_color_row = QHBoxLayout() # 占位,保持对齐 + title_color_row.setContentsMargins(0, 0, 0, 0) + title_color_row.setSpacing(4) + + # 标题R分量 + title_color_label_r = QLabel("R:") + title_color_label_r.setAlignment(Qt.AlignVCenter) + title_color_label_r.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + title_color_row.addWidget(title_color_label_r) + title_r_spin = QDoubleSpinBox() + title_r_spin.setRange(0.0, 1.0) + title_r_spin.setSingleStep(0.01) + title_r_spin.setValue(current_title_color[0]) + title_color_row.addWidget(title_r_spin) + + # 标题G分量 + title_color_label_g = QLabel("G:") + title_color_label_g.setAlignment(Qt.AlignVCenter) + title_color_label_g.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + title_color_row.addWidget(title_color_label_g) + title_g_spin = QDoubleSpinBox() + title_g_spin.setRange(0.0, 1.0) + title_g_spin.setSingleStep(0.01) + title_g_spin.setValue(current_title_color[1]) + title_color_row.addWidget(title_g_spin) + + title_layout.addLayout(title_color_row, 1, 1, 1, 3) + + title_layout.addWidget(QLabel(""), 2, 0) # 占位 + title_color_row_ = QHBoxLayout() # 占位,保持对齐 + title_color_row_.setContentsMargins(0, 0, 0, 0) + title_color_row_.setSpacing(4) + + # 标题B分量 + title_color_label_b = QLabel("B:") + title_color_label_b.setAlignment(Qt.AlignVCenter) + title_color_label_b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + title_color_row_.addWidget(title_color_label_b) + title_b_spin = QDoubleSpinBox() + title_b_spin.setRange(0.0, 1.0) + title_b_spin.setSingleStep(0.01) + title_b_spin.setValue(current_title_color[2]) + title_color_row_.addWidget(title_b_spin) + + # 标题A分量 + title_color_label_a = QLabel("A:") + title_color_label_a.setAlignment(Qt.AlignVCenter) + title_color_label_a.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + title_color_row_.addWidget(title_color_label_a) + title_a_spin = QDoubleSpinBox() + title_a_spin.setRange(0.0, 1.0) + title_a_spin.setSingleStep(0.01) + title_a_spin.setValue(current_title_color[3]) + title_color_row_.addWidget(title_a_spin) + + title_layout.addLayout(title_color_row_, 2, 1, 1, 3) + + # 标题颜色选择按钮 + title_color_button = QPushButton("选择标题颜色") + title_color_button.clicked.connect(lambda: self._selectInfoPanelTitleColor( + info_panel, title_r_spin, title_g_spin, title_b_spin, title_a_spin)) + title_layout.addWidget(title_color_button, 4, 0, 1, 4) + + # 标题字体大小 + title_layout.addWidget(QLabel("标题字体大小:"), 5, 0) + title_size_spin = QDoubleSpinBox() + title_size_spin.setRange(0.01, 1.0) + title_size_spin.setSingleStep(0.01) + title_size_spin.setValue(current_title_size) + title_layout.addWidget(title_size_spin, 5, 1, 1, 3) + + # 标题位置控制 (仅对2D面板) + if not is_3d_panel: + title_position_row = QHBoxLayout() + title_position_row.setContentsMargins(0, 0, 0, 0) + title_position_row.setSpacing(4) + + title_position_label = QLabel("标题位置") + title_layout.addWidget(title_position_label, 6, 0) + + title_x_spin = QDoubleSpinBox() + title_x_spin.setRange(-1.0, 1.0) + title_x_spin.setSingleStep(0.01) + title_x_spin.setValue(current_title_pos[0]) + + title_y_spin = QDoubleSpinBox() + title_y_spin.setRange(-1.0, 1.0) + title_y_spin.setSingleStep(0.01) + title_y_spin.setValue(current_title_pos[1]) + + for axis_label, spinbox in (("X:", title_x_spin), ("Y:", title_y_spin)): + label = QLabel(axis_label) + label.setAlignment(Qt.AlignVCenter) + label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + title_position_row.addWidget(label) + title_position_row.addWidget(spinbox) + + title_layout.addLayout(title_position_row, 6, 1, 1, 3) + + # 连接标题属性变化信号 + def onTitleTextChanged(): + if hasattr(self.world, 'info_panel_manager'): + # 根据面板类型选择更新方法 + if is_3d_panel: + self.world.info_panel_manager.update3DPanelContent( + panel_id, title=title_text_edit.toPlainText()) + else: + self.world.info_panel_manager.updatePanelContent( + panel_id, title=title_text_edit.toPlainText()) + + def onTitlePropertyChanged(): + # 根据面板类型选择更新方法 + if is_3d_panel: + self._applyInfoPanel3DTitleProperties( + info_panel, title_r_spin.value(), title_g_spin.value(), title_b_spin.value(), + title_a_spin.value(), title_size_spin.value()) + else: + self._applyInfoPanelTitleProperties( + info_panel, title_r_spin.value(), title_g_spin.value(), title_b_spin.value(), + title_a_spin.value(), title_size_spin.value()) + + def onTitlePositionChanged(): + # 仅对2D面板更新标题位置 + if not is_3d_panel and hasattr(self.world, 'info_panel_manager'): + panel_data = self.world.info_panel_manager.panels.get(panel_id) + if panel_data and 'title_label' in panel_data: + # 设置标题位置 + panel_data['title_label'].setPos(title_x_spin.value(), 0, title_y_spin.value()) + + title_text_edit.textChanged.connect(onTitleTextChanged) + title_r_spin.valueChanged.connect(onTitlePropertyChanged) + title_g_spin.valueChanged.connect(onTitlePropertyChanged) + title_b_spin.valueChanged.connect(onTitlePropertyChanged) + title_a_spin.valueChanged.connect(onTitlePropertyChanged) + title_size_spin.valueChanged.connect(onTitlePropertyChanged) + + if not is_3d_panel: # 仅对2D面板连接位置变化信号 + title_x_spin.valueChanged.connect(onTitlePositionChanged) + title_y_spin.valueChanged.connect(onTitlePositionChanged) + + title_group.setLayout(title_layout) + self._propertyLayout.addWidget(title_group) + + # 内容属性组 + content_group = QGroupBox("内容属性") + content_layout = QGridLayout() + content_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + # 内容文本 + content_layout.addWidget(QLabel("内容文本:"), 0, 0) + content_text_edit = QTextEdit() + content_text_edit.setMaximumHeight(100) + content_text_edit.setPlainText(current_content_text) + content_layout.addWidget(content_text_edit, 0, 1, 1, 3) + + # 内容颜色 + content_layout.addWidget(QLabel("内容颜色:"), 1, 0) + content_color_row = QHBoxLayout() + content_color_row.setContentsMargins(0, 0, 0, 0) + content_color_row.setSpacing(4) + + + # 内容R分量 + content_r = QLabel("R:") + content_r.setAlignment(Qt.AlignVCenter) + content_r.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + content_color_row.addWidget(content_r) + content_r_spin = QDoubleSpinBox() + content_r_spin.setRange(0.0, 1.0) + content_r_spin.setSingleStep(0.01) + content_r_spin.setValue(current_content_color[0]) + content_color_row.addWidget(content_r_spin) + + # 内容G分量 + content_g = QLabel("G:") + content_g.setAlignment(Qt.AlignVCenter) + content_g.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + content_color_row.addWidget(content_g) + content_g_spin = QDoubleSpinBox() + content_g_spin.setRange(0.0, 1.0) + content_g_spin.setSingleStep(0.01) + content_g_spin.setValue(current_content_color[1]) + content_color_row.addWidget(content_g_spin) + + content_layout.addLayout(content_color_row, 1, 1, 1, 3) + + content_color_row_ = QHBoxLayout() + content_color_row_.setContentsMargins(0, 0, 0, 0) + content_color_row_.setSpacing(4) + content_layout.addWidget(QLabel(":"), 2, 0) + + # 内容B分量 + content_b = QLabel("B:") + content_b.setAlignment(Qt.AlignVCenter) + content_b.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + content_color_row_.addWidget(content_b) + content_b_spin = QDoubleSpinBox() + content_b_spin.setRange(0.0, 1.0) + content_b_spin.setSingleStep(0.01) + content_b_spin.setValue(current_content_color[2]) + content_color_row_.addWidget(content_b_spin) + + # 内容A分量 + content_a = QLabel("A:") + content_a.setAlignment(Qt.AlignVCenter) + content_a.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + content_color_row_.addWidget(content_a) + content_a_spin = QDoubleSpinBox() + content_a_spin.setRange(0.0, 1.0) + content_a_spin.setSingleStep(0.01) + content_a_spin.setValue(current_content_color[3]) + content_color_row_.addWidget(content_a_spin) + + content_layout.addLayout(content_color_row_, 2, 1, 1, 3) + + # 内容颜色选择按钮 + content_color_button = QPushButton("选择内容颜色") + content_color_button.clicked.connect(lambda: self._selectInfoPanelContentColor( + info_panel, content_r_spin, content_g_spin, content_b_spin, content_a_spin)) + content_layout.addWidget(content_color_button, 4, 0, 1, 4) + + # 内容字体大小 + content_layout.addWidget(QLabel("内容字体大小:"), 5, 0) + content_size_spin = QDoubleSpinBox() + content_size_spin.setRange(0.01, 1.0) + content_size_spin.setSingleStep(0.01) + content_size_spin.setValue(current_content_size) + content_layout.addWidget(content_size_spin, 5, 1, 1, 3) + + # 内容位置控制 (仅对2D面板) + if not is_3d_panel: + content_layout.addLayout(QLabel("内容位置:"), 6, 0) + content_pos_layout = QHBoxLayout() + content_pos_layout.setContentsMargins(0, 0, 0, 0) + content_pos_layout.setSpacing(4) + + content_x = QLabel("X:") + content_x.setAlignment(Qt.AlignVCenter) + content_x.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + content_pos_layout.addWidget(content_x) + content_x_spin = QDoubleSpinBox() + content_x_spin.setRange(-1.0, 1.0) + content_x_spin.setSingleStep(0.01) + content_x_spin.setValue(current_content_pos[0]) + content_pos_layout.addWidget(content_x_spin) + + content_y = QLabel("Y:") + content_y.setAlignment(Qt.AlignVCenter) + content_y.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + content_pos_layout.addWidget(content_y) + content_y_spin = QDoubleSpinBox() + content_y_spin.setRange(-1.0, 1.0) + content_y_spin.setSingleStep(0.01) + content_y_spin.setValue(current_content_pos[1]) + content_pos_layout.addWidget(content_y_spin) + + content_layout.addLayout(content_pos_layout, 6, 1, 1, 3) + + # 连接内容属性变化信号 + def onContentTextChanged(): + if hasattr(self.world, 'info_panel_manager'): + # 根据面板类型选择更新方法 + if is_3d_panel: + self.world.info_panel_manager.update3DPanelContent( + panel_id, content=content_text_edit.toPlainText()) + else: + self.world.info_panel_manager.updatePanelContent( + panel_id, content=content_text_edit.toPlainText()) + + def onContentPropertyChanged(): + # 根据面板类型选择更新方法 + if is_3d_panel: + self._applyInfoPanel3DContentProperties( + info_panel, content_r_spin.value(), content_g_spin.value(), content_b_spin.value(), + content_a_spin.value(), content_size_spin.value()) + else: + self._applyInfoPanelContentProperties( + info_panel, content_r_spin.value(), content_g_spin.value(), content_b_spin.value(), + content_a_spin.value(), content_size_spin.value()) + + def onContentPositionChanged(): + # 仅对2D面板更新内容位置 + if not is_3d_panel and hasattr(self.world, 'info_panel_manager'): + panel_data = self.world.info_panel_manager.panels.get(panel_id) + if panel_data and 'content_label' in panel_data: + # 设置内容位置 + panel_data['content_label'].setPos(content_x_spin.value(), 0, content_y_spin.value()) + + def adjustContentWordwrap(): + """根据面板宽度和字体大小调整内容换行""" + # 仅对2D面板调整换行 + if not is_3d_panel: + panel_width = width_spin.value() + font_size = content_size_spin.value() + self._adjustContentWordwrap(panel_id, panel_width, font_size) + + # 连接相关信号 + content_r_spin.valueChanged.connect(onContentPropertyChanged) + content_g_spin.valueChanged.connect(onContentPropertyChanged) + content_b_spin.valueChanged.connect(onContentPropertyChanged) + content_a_spin.valueChanged.connect(onContentPropertyChanged) + + # 内容字体大小变化信号连接 + content_size_spin.valueChanged.connect(onContentPropertyChanged) + + # 内容文本变化信号连接 + content_text_edit.textChanged.connect(onContentTextChanged) + + # 标题相关信号连接 + title_text_edit.textChanged.connect(onTitleTextChanged) + title_r_spin.valueChanged.connect(onTitlePropertyChanged) + title_g_spin.valueChanged.connect(onTitlePropertyChanged) + title_b_spin.valueChanged.connect(onTitlePropertyChanged) + title_a_spin.valueChanged.connect(onTitlePropertyChanged) + title_size_spin.valueChanged.connect(onTitlePropertyChanged) + + if not is_3d_panel: # 仅对2D面板连接位置变化信号 + title_x_spin.valueChanged.connect(onTitlePositionChanged) + title_y_spin.valueChanged.connect(onTitlePositionChanged) + content_x_spin.valueChanged.connect(onContentPositionChanged) + content_y_spin.valueChanged.connect(onContentPositionChanged) + + content_group.setLayout(content_layout) + self._propertyLayout.addWidget(content_group) + + # 存储控件引用,用于后续更新 + self._current_info_panel = info_panel + self._info_panel_controls = { + 'title_text_edit': title_text_edit, + 'content_text_edit': content_text_edit, + 'title_r_spin': title_r_spin, + 'title_g_spin': title_g_spin, + 'title_b_spin': title_b_spin, + 'title_a_spin': title_a_spin, + 'content_r_spin': content_r_spin, + 'content_g_spin': content_g_spin, + 'content_b_spin': content_b_spin, + 'content_a_spin': content_a_spin, + 'title_size_spin': title_size_spin, + 'content_size_spin': content_size_spin, + 'width_spin': width_spin, + 'height_spin': height_spin + } + + # 对于2D面板,添加位置控件引用 + if not is_3d_panel: + self._info_panel_controls.update({ + 'title_x_spin': title_x_spin, + 'title_y_spin': title_y_spin, + 'content_x_spin': content_x_spin, + 'content_y_spin': content_y_spin + }) + + # 启动定时器定期检查信息面板内容变化 + self._startInfoPanelMonitoring(info_panel, panel_id) + + # 初始调整内容换行 (仅对2D面板) + if not is_3d_panel: + adjustContentWordwrap() + + print(f"✅ 信息面板属性面板已添加 (类型: {'3D' if is_3d_panel else '2D'})") + + except Exception as e: + print(f"❌ 添加信息面板属性面板失败: {e}") + import traceback + traceback.print_exc() + + def _applyInfoPanel3DTitleProperties(self, info_panel, r, g, b, a, size): + """应用3D信息面板标题属性""" + try: + panel_id = info_panel.getTag("panel_id") + if not panel_id or not hasattr(self.world, 'info_panel_manager'): + return + + # 更新3D面板标题属性 + self.world.info_panel_manager.update3DPanelProperties( + panel_id, + title_color=(r, g, b, a), + title_size=size + ) + except Exception as e: + print(f"应用3D信息面板标题属性失败: {e}") + + def _applyInfoPanel3DContentProperties(self, info_panel, r, g, b, a, size): + """应用3D信息面板内容属性""" + try: + panel_id = info_panel.getTag("panel_id") + if not panel_id or not hasattr(self.world, 'info_panel_manager'): + return + + # 更新3D面板内容属性 + self.world.info_panel_manager.update3DPanelProperties( + panel_id, + content_color=(r, g, b, a), + content_size=size + ) + except Exception as e: + print(f"应用3D信息面板内容属性失败: {e}") + + def _adjustContentWordwrap(self, panel_id, panel_width, font_size): + """调整信息面板内容的换行设置""" + try: + if hasattr(self.world, 'info_panel_manager'): + panel_data = self.world.info_panel_manager.panels.get(panel_id) + if panel_data and 'content_label' in panel_data: + # 计算合适的换行值 - 根据面板宽度和字体大小动态调整 + base_wordwrap = panel_width * 10 # 基础换行值 + # 根据字体大小调整换行值(字体越大,每行字符越少) + size_factor = 0.045 / font_size if font_size > 0 else 1.0 + adjusted_wordwrap = base_wordwrap * size_factor + + # 确保换行值在合理范围内 + wordwrap_value = max(5.0, min(100.0, adjusted_wordwrap)) + + # 应用换行设置 + panel_data['content_label']['text_wordwrap'] = wordwrap_value + print( + f"调整内容换行: 面板宽度={panel_width:.2f}, 字体大小={font_size:.3f}, 换行值={wordwrap_value:.2f}") + except Exception as e: + print(f"调整内容换行失败: {e}") + + def _startInfoPanelMonitoring(self, info_panel, panel_id): + """启动信息面板内容监控""" + try: + # 使用 QTimer 定期检查信息面板内容变化 + if not hasattr(self, '_info_panel_timer'): + from PyQt5.QtCore import QTimer + self._info_panel_timer = QTimer() + self._info_panel_timer.timeout.connect(lambda: self._checkInfoPanelUpdates(panel_id)) + self._info_panel_timer.start(500) # 每500毫秒检查一次 + except Exception as e: + print(f"❌ 启动信息面板监控失败: {e}") + + def _checkInfoPanelUpdates(self, panel_id): + """检查信息面板内容更新""" + try: + # 检查是否仍有有效的控件引用 + if not hasattr(self, '_info_panel_controls') or not self._info_panel_controls: + return + + # 检查是否仍有有效的info_panel_manager + if not hasattr(self.world, 'info_panel_manager'): + return + + # 获取当前面板数据 + panel_data = self.world.info_panel_manager.panels.get(panel_id) + if not panel_data: + return + + # 获取控件引用 + controls = self._info_panel_controls + + # 检查并更新标题文本 + if 'title_label' in panel_data and panel_data['title_label']: + current_title_text = panel_data['title_label']['text'] + title_text_edit = controls.get('title_text_edit') + if title_text_edit and title_text_edit.toPlainText() != current_title_text: + title_text_edit.blockSignals(True) + title_text_edit.setPlainText(current_title_text) + title_text_edit.blockSignals(False) + + # 检查并更新内容文本 + if 'content_label' in panel_data and panel_data['content_label']: + current_content_text = panel_data['content_label']['text'] + content_text_edit = controls.get('content_text_edit') + if content_text_edit and content_text_edit.toPlainText() != current_content_text: + content_text_edit.blockSignals(True) + content_text_edit.setPlainText(current_content_text) + content_text_edit.blockSignals(False) + + # 检查并更新标题位置 + if 'title_label' in panel_data and panel_data['title_label']: + title_pos = panel_data['title_label'].getPos() + current_title_pos = (title_pos.getX(), title_pos.getZ()) + + title_x_spin = controls.get('title_x_spin') + title_y_spin = controls.get('title_y_spin') + + if title_x_spin and abs(title_x_spin.value() - current_title_pos[0]) > 0.001: + title_x_spin.blockSignals(True) + title_x_spin.setValue(current_title_pos[0]) + title_x_spin.blockSignals(False) + + if title_y_spin and abs(title_y_spin.value() - current_title_pos[1]) > 0.001: + title_y_spin.blockSignals(True) + title_y_spin.setValue(current_title_pos[1]) + title_y_spin.blockSignals(False) + + # 检查并更新内容位置 + if 'content_label' in panel_data and panel_data['content_label']: + content_pos = panel_data['content_label'].getPos() + current_content_pos = (content_pos.getX(), content_pos.getZ()) + + content_x_spin = controls.get('content_x_spin') + content_y_spin = controls.get('content_y_spin') + + if content_x_spin and abs(content_x_spin.value() - current_content_pos[0]) > 0.001: + content_x_spin.blockSignals(True) + content_x_spin.setValue(current_content_pos[0]) + content_x_spin.blockSignals(False) + + if content_y_spin and abs(content_y_spin.value() - current_content_pos[1]) > 0.001: + content_y_spin.blockSignals(True) + content_y_spin.setValue(current_content_pos[1]) + content_y_spin.blockSignals(False) + + except Exception as e: + print(f"❌ 检查信息面板更新失败: {e}") + + def _selectInfoPanelBorderColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): + """选择信息面板边框颜色""" + try: + from PyQt5.QtWidgets import QColorDialog + from PyQt5.QtGui import QColor + + # 获取当前颜色值 + current_color = QColor( + int(r_spin.value() * 255), + int(g_spin.value() * 255), + int(b_spin.value() * 255), + int(a_spin.value() * 255) + ) + + # 打开颜色选择对话框 + color = QColorDialog.getColor(current_color, None, "选择边框颜色") + if color.isValid(): + # 更新数值框 + r_spin.setValue(color.redF()) + g_spin.setValue(color.greenF()) + b_spin.setValue(color.blueF()) + a_spin.setValue(color.alphaF()) + + except Exception as e: + print(f"❌ 选择边框颜色失败: {e}") + + def updateInfoPanelBackgroundImage(self, info_panel, image_path): + """更新信息面板背景图片 - 修复版""" + try: + panel_id = info_panel.getTag("panel_id") + if not panel_id: + print("❌ 无法找到信息面板ID") + return False + + # 通过 info_panel_manager 更新背景图片 + if hasattr(self.world, 'info_panel_manager'): + info_panel_manager = self.world.info_panel_manager + + # 检查是否是3D信息面板 + gui_type = info_panel.getTag("gui_type") + is_3d_panel = gui_type == "info_panel_3d" + + if is_3d_panel: + # 对于3D信息面板,使用专门的方法 + return self._update3DInfoPanelBackgroundImage(panel_id, info_panel, image_path) + else: + # 对于2D信息面板,使用原有方法 + if image_path: + # 设置新的背景图片 + success = info_panel_manager.setPanelBackgroundImage(panel_id, image_path) + if success: + print(f"✅ 成功设置信息面板背景图片: {image_path}") + return True + else: + print(f"❌ 设置信息面板背景图片失败: {image_path}") + return False + else: + # 清除背景图片 + success = info_panel_manager.setPanelBackgroundImage(panel_id, None) + if success: + print("✅ 成功清除信息面板背景图片") + return True + else: + print("❌ 清除信息面板背景图片失败") + return False + else: + print("❌ 未找到 info_panel_manager") + return False + + except Exception as e: + print(f"❌ 更新信息面板背景图片失败: {e}") + import traceback + traceback.print_exc() + return False + + def _applyInfoPanelBackgroundImage(self, panel_node, image_path, panel_data): + """应用信息面板背景图片 - 改进版""" + try: + from panda3d.core import CardMaker, TransparencyAttrib, Vec4 + + # 如果已有背景图片,先移除 + if 'bg_image' in panel_data and panel_data['bg_image']: + try: + panel_data['bg_image'].removeNode() + except: + pass + panel_data['bg_image'] = None + + # 加载纹理 + texture = self.world.loader.loadTexture(image_path) + if not texture: + print(f"❌ 无法加载背景图片: {image_path}") + return False + + # 设置纹理属性 + texture.setMagfilter(texture.FTLinear) + texture.setMinfilter(texture.FTLinearMipmapLinear) + + # 获取面板尺寸 + size = panel_data.get('size', (1.0, 0.6)) + + # 创建背景卡片 + cm = CardMaker('info_panel_background') + cm.setFrame(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2) + + # 生成几何体并创建节点 + bg_node = panel_node.attachNewNode(cm.generate()) + + # 应用纹理 + bg_node.setTexture(texture, 1) + + # 设置渲染属性 + bg_node.setTransparency(TransparencyAttrib.MAlpha) + bg_node.setBin("background", -10) + bg_node.setDepthWrite(False) + bg_node.setDepthTest(False) + bg_node.setLightOff() + bg_node.setTwoSided(True) + + # 确保在最底层 + bg_node.setZ(-0.1) + + # 保存引用 + panel_data['bg_image'] = bg_node + + print(f"✅ 成功应用信息面板背景图片: {image_path}") + return True + + except Exception as e: + print(f"❌ 应用信息面板背景图片失败: {e}") + import traceback + traceback.print_exc() + return False + + def _update3DInfoPanelBackgroundImage(self, panel_id, info_panel, image_path): + """更新3D信息面板背景图片 - 完整修复版""" + try: + # 从info_panel_manager获取面板数据 + if not hasattr(self.world, 'info_panel_manager'): + print("❌ 未找到 info_panel_manager") + return False + + panel_data = self.world.info_panel_manager.panels.get(panel_id) + if not panel_data: + print(f"❌ 无法找到面板数据: {panel_id}") + return False + + # 获取面板节点 + panel_node = panel_data.get('node') + if not panel_node: + print("❌ 无法找到面板节点") + return False + + # 如果已有背景图片,先销毁它 + if 'bg_image' in panel_data and panel_data['bg_image']: + try: + # 如果是NodePath对象,使用removeNode方法 + if hasattr(panel_data['bg_image'], 'removeNode'): + panel_data['bg_image'].removeNode() + # 如果是DirectGUI对象,使用destroy方法 + elif hasattr(panel_data['bg_image'], 'destroy'): + panel_data['bg_image'].destroy() + except Exception as e: + print(f"⚠️ 清理旧背景图片时出错: {e}") + panel_data['bg_image'] = None + + # 如果image_path为None,只清除背景图片 + if not image_path: + # 清除节点标签 + if info_panel.hasTag("bg_image_path"): + info_panel.clearTag("bg_image_path") + print("✅ 成功清除3D信息面板背景图片") + return True + + # 使用 world.loader 加载新图片 + texture = self.world.loader.loadTexture(image_path) + if not texture: + print(f"❌ 无法加载图片: {image_path}") + return False + + # 设置纹理过滤 + texture.setMagfilter(texture.FTLinear) + texture.setMinfilter(texture.FTLinearMipmapLinear) + + # 获取面板大小 + size = panel_data['properties'].get('size', (1.0, 0.6)) + + # 使用CardMaker创建卡片几何体 + from panda3d.core import CardMaker, TransparencyAttrib + + cm = CardMaker('info_panel_bg') + # 设置卡片大小,居中放置 + cm.setFrame(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2) + bg_geom = cm.generate() + + # 创建节点并附加几何体,直接附加到面板节点 + bg_node = panel_node.attachNewNode(bg_geom) + + # 应用纹理 + bg_node.setTexture(texture, 1) + + # 设置正确的渲染属性 + bg_node.setTransparency(TransparencyAttrib.MAlpha) + bg_node.setBin("background", -10) # 确保在最底层 + bg_node.setDepthWrite(False) + bg_node.setDepthTest(False) + bg_node.setLightOff() + bg_node.setTwoSided(True) # 双面渲染 + + # 调整位置确保在面板内容后面 + bg_node.setPos(0, 0.1, 0) # 稍微向后一点避免z-fighting + + # 保存引用 + panel_data['bg_image'] = bg_node + panel_data['properties']['bg_image'] = image_path + + # 更新节点标签 + info_panel.setTag("bg_image_path", image_path) + + print(f"✅ 成功设置3D信息面板背景图片: {image_path}") + return True + + except Exception as e: + print(f"❌ 更新3D信息面板背景图片失败: {e}") + import traceback + traceback.print_exc() + return False + + def _selectInfoPanelBackgroundColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): + """选择信息面板背景颜色""" + try: + from PyQt5.QtWidgets import QColorDialog + from PyQt5.QtGui import QColor + + # 获取当前背景颜色 + current_color = QColor( + int(r_spin.value() * 255), + int(g_spin.value() * 255), + int(b_spin.value() * 255), + int(a_spin.value() * 255) + ) + + # 显示颜色选择对话框 + color = QColorDialog.getColor(current_color, None, "选择背景颜色") + if color.isValid(): + # 更新数值框 + r_spin.setValue(color.red() / 255.0) + g_spin.setValue(color.green() / 255.0) + b_spin.setValue(color.blue() / 255.0) + a_spin.setValue(color.alpha() / 255.0) + + except Exception as e: + print(f"选择背景颜色失败: {e}") + + def _applyInfoPanelBackgroundColor(self, info_panel, r, g, b, a): + """应用信息面板背景颜色""" + try: + panel_id = info_panel.getTag("panel_id") + if not panel_id: + return + + # 更新InfoPanelManager中的背景颜色 + if hasattr(self.world, 'info_panel_manager'): + bg_color = (r, g, b, a) + + # 根据面板类型选择更新方法 + gui_type = info_panel.getTag("gui_type") + if gui_type == "info_panel_3d": + self.world.info_panel_manager.update3DPanelProperties( + panel_id, bg_color=bg_color) + else: + self.world.info_panel_manager.updatePanelProperties( + panel_id, bg_color=bg_color) + + print(f"已更新信息面板 {panel_id} 的背景颜色为 ({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f})") + except Exception as e: + print(f"应用背景颜色失败: {e}") + + def _selectInfoPanelTitleColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): + """选择信息面板标题颜色""" + try: + from PyQt5.QtWidgets import QColorDialog + from PyQt5.QtGui import QColor + + # 获取当前颜色值 + current_color = QColor( + int(r_spin.value() * 255), + int(g_spin.value() * 255), + int(b_spin.value() * 255), + int(a_spin.value() * 255) + ) + + # 打开颜色选择对话框 + color = QColorDialog.getColor(current_color, None, "选择标题颜色") + if color.isValid(): + # 更新数值框 + r_spin.setValue(color.redF()) + g_spin.setValue(color.greenF()) + b_spin.setValue(color.blueF()) + a_spin.setValue(color.alphaF()) + + except Exception as e: + print(f"❌ 选择标题颜色失败: {e}") + + def _selectInfoPanelContentColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): + """选择信息面板内容颜色""" + try: + from PyQt5.QtWidgets import QColorDialog + from PyQt5.QtGui import QColor + + # 获取当前颜色值 + current_color = QColor( + int(r_spin.value() * 255), + int(g_spin.value() * 255), + int(b_spin.value() * 255), + int(a_spin.value() * 255) + ) + + # 打开颜色选择对话框 + color = QColorDialog.getColor(current_color, None, "选择内容颜色") + if color.isValid(): + # 更新数值框 + r_spin.setValue(color.redF()) + g_spin.setValue(color.greenF()) + b_spin.setValue(color.blueF()) + a_spin.setValue(color.alphaF()) + + except Exception as e: + print(f"❌ 选择内容颜色失败: {e}") + + def _applyInfoPanelTitleProperties(self, info_panel, r, g, b, a, size): + """应用信息面板标题属性""" + try: + panel_id = info_panel.getTag("panel_id") + if not panel_id: + print("❌ 无法找到信息面板ID") + return False + + # 通过 info_panel_manager 更新标题属性 + if hasattr(self.world, 'info_panel_manager'): + info_panel_manager = self.world.info_panel_manager + + # 根据面板类型选择不同的更新方法 + gui_type = info_panel.getTag("gui_type") + if gui_type == "info_panel_3d": + # 3D面板使用update3DPanelProperties + info_panel_manager.update3DPanelProperties( + panel_id, + title_color=(r, g, b, a), + title_size=size + ) + else: + # 2D面板使用updatePanelProperties + info_panel_manager.updatePanelProperties( + panel_id, + title_color=(r, g, b, a), + title_size=size + ) + + print(f"✓ 信息面板标题属性已更新: 颜色RGBA({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f}), 大小{size:.3f}") + return True + else: + print("❌ 无法找到 info_panel_manager") + return False + + except Exception as e: + print(f"❌ 应用信息面板标题属性失败: {e}") + return False + + def _applyInfoPanelContentProperties(self, info_panel, r, g, b, a, size): + """应用信息面板内容属性""" + try: + panel_id = info_panel.getTag("panel_id") + if not panel_id: + print("❌ 无法找到信息面板ID") + return False + + # 通过 info_panel_manager 更新内容属性 + if hasattr(self.world, 'info_panel_manager'): + info_panel_manager = self.world.info_panel_manager + + # 根据面板类型选择不同的更新方法 + gui_type = info_panel.getTag("gui_type") + if gui_type == "info_panel_3d": + # 3D面板使用update3DPanelProperties + info_panel_manager.update3DPanelProperties( + panel_id, + content_color=(r, g, b, a), + content_size=size + ) + else: + # 2D面板使用updatePanelProperties + info_panel_manager.updatePanelProperties( + panel_id, + content_color=(r, g, b, a), + content_size=size + ) + + # print(f"✓ 信息面板内容属性已更新: 颜色RGBA({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f}), 大小{size:.3f}") + return True + else: + print("❌ 无法找到 info_panel_manager") + return False + + except Exception as e: + print(f"❌ 应用信息面板内容属性失败: {e}") + return False + + def _addSphericalVideoProperties(self, spherical_video): + """为球形视频添加属性控制面板""" + try: + from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, + QDoubleSpinBox, QFileDialog, QHBoxLayout, QSlider) + from PyQt5.QtCore import Qt + import os + + # 视频控制组 + video_group = QGroupBox("视频控制") + video_layout = QGridLayout() + + # 播放控制按钮 + control_layout = QHBoxLayout() + + self.play_button = QPushButton("▶️ 播放") + self.play_button.clicked.connect(lambda: self._toggleSphericalVideoPlayback(spherical_video)) + + self.stop_button = QPushButton("⏹️ 停止") + self.stop_button.clicked.connect(lambda: self._stopSphericalVideo(spherical_video)) + + control_layout.addWidget(self.play_button) + control_layout.addWidget(self.stop_button) + video_layout.addLayout(control_layout, 0, 0, 1, 2) + + # 视频进度控制 + progress_layout = QHBoxLayout() + self.progress_slider = QSlider(Qt.Horizontal) + self.progress_slider.setRange(0, 100) + self.progress_slider.setValue(0) + self.progress_slider.sliderMoved.connect(lambda value: self._seekSphericalVideo(spherical_video, value)) + + self.time_label = QLabel("00:00 / 00:00") + self.time_label.setStyleSheet("font-size: 10px; color: gray;") + + progress_layout.addWidget(self.progress_slider) + progress_layout.addWidget(self.time_label) + video_layout.addLayout(progress_layout, 1, 0, 1, 2) + + # 视频文件选择 + file_layout = QHBoxLayout() + self.select_video_button = QPushButton("选择视频文件") + self.select_video_button.clicked.connect(lambda: self._selectSphericalVideoFile(spherical_video)) + + # 显示当前视频文件 + current_video = spherical_video.getTag("video_path") if spherical_video.hasTag("video_path") else "" + self.video_file_label = QLabel(os.path.basename(current_video) if current_video else "未选择视频") + self.video_file_label.setStyleSheet("font-size: 9px; color: gray;") + self.video_file_label.setWordWrap(True) + + file_layout.addWidget(self.select_video_button) + file_layout.addWidget(self.video_file_label) + video_layout.addLayout(file_layout, 2, 0, 1, 2) + + video_group.setLayout(video_layout) + self._propertyLayout.addWidget(video_group) + + # 球体属性组 + sphere_group = QGroupBox("球体属性") + sphere_layout = QGridLayout() + + # 半径控制 + sphere_layout.addWidget(QLabel("半径:"), 0, 0) + radius_spin = QDoubleSpinBox() + radius_spin.setRange(0.1, 100) + radius_spin.setSingleStep(0.1) + current_radius = float(spherical_video.getTag("original_radius") or "5.0") + radius_spin.setValue(current_radius) + radius_spin.valueChanged.connect(lambda value: self._updateSphericalVideoRadius(spherical_video, value)) + sphere_layout.addWidget(radius_spin, 0, 1, 1, 3) + + sphere_group.setLayout(sphere_layout) + self._propertyLayout.addWidget(sphere_group) + + # 存储控件引用 + self._spherical_video_controls = { + 'play_button': self.play_button, + 'stop_button': self.stop_button, + 'progress_slider': self.progress_slider, + 'time_label': self.time_label, + 'select_button': self.select_video_button, + 'file_label': self.video_file_label + } + + print("✅ 球形视频属性面板已添加") + + except Exception as e: + print(f"❌ 添加球形视频属性面板失败: {e}") + import traceback + traceback.print_exc() + + def _toggleSphericalVideoPlayback(self, spherical_video): + """切换球形视频播放状态""" + try: + # 获取视频纹理 + movie_texture = spherical_video.getPythonTag("movie_texture") + if not movie_texture: + # 尝试从节点获取纹理 + texture = spherical_video.getTexture() + if texture: + movie_texture = texture + + if not movie_texture: + print("❌ 未找到视频纹理") + return + + # 检查当前播放状态 + if not hasattr(self, '_video_playing'): + self._video_playing = {} + + video_id = id(spherical_video) + is_playing = self._video_playing.get(video_id, False) + + if is_playing: + if hasattr(movie_texture, 'stop'): + movie_texture.stop() + if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls: + play_button = self._spherical_video_controls['play_button'] + play_button.setText("▶️ 播放") + print("⏸️ 球形视频已暂停") + else: + if hasattr(movie_texture, 'play'): + movie_texture.play() + if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls: + play_button = self._spherical_video_controls['play_button'] + play_button.setText("⏸️ 暂停") + print("▶️ 球形视频开始播放") + self._video_playing[video_id] = not is_playing + + except Exception as e: + print(f"❌ 切换球形视频播放状态失败: {e}") + + def _stopSphericalVideo(self, spherical_video): + """停止球形视频""" + try: + # 获取视频纹理 + movie_texture = spherical_video.getPythonTag("movie_texture") + if not movie_texture: + # 尝试从节点获取纹理 + texture = spherical_video.getTexture() + if texture: + movie_texture = texture + + if movie_texture: + if hasattr(movie_texture, 'stop'): + movie_texture.stop() + # 重置到开头 + if hasattr(movie_texture, 'setTime'): + movie_texture.setTime(0.0) + + if hasattr(self, '_spherical_video_controls') and 'play_button' in self._spherical_video_controls: + play_button = self._spherical_video_controls['play_button'] + play_button.setText("▶️ 播放") + + if hasattr(self, '_video_playing'): + video_id = id(spherical_video) + self._video_playing[video_id] = False + + print("⏹️ 球形视频已停止") + else: + print("❌ 视频纹理不支持停止操作") + else: + print("❌ 未找到视频纹理") + + except Exception as e: + print(f"❌ 停止球形视频失败: {e}") + + def _seekSphericalVideo(self, spherical_video, slider_value): + """跳转到指定时间位置""" + try: + # 获取视频纹理 + movie_texture = spherical_video.getPythonTag("movie_texture") + if not movie_texture: + # 尝试从节点获取纹理 + texture = spherical_video.getTexture() + if texture: + movie_texture = texture + + if movie_texture and hasattr(movie_texture, 'setTime'): + # 假设视频总时长为100秒(实际应该根据视频时长计算) + time_seconds = slider_value + movie_texture.setTime(time_seconds) + print(f"⏰ 跳转到时间: {time_seconds}秒") + + # 更新时间标签 + if hasattr(self, '_spherical_video_controls') and 'time_label' in self._spherical_video_controls: + time_label = self._spherical_video_controls['time_label'] + current_time = f"{int(time_seconds // 60):02d}:{int(time_seconds % 60):02d}" + total_time = "01:40" # 假设总时长100秒 + time_label.setText(f"{current_time} / {total_time}") + + else: + print("❌ 视频纹理不支持时间控制") + + except Exception as e: + print(f"❌ 球形视频跳转失败: {e}") + + def _updateSphericalVideoRadius(self, spherical_video, new_radius): + """更新球形视频半径""" + try: + from panda3d.core import GeomNode + + # 移除旧的几何体 + spherical_video.node().removeAllGeoms() + + # 创建新的球形几何体 + new_geom = self.world.gui_manager._createSphereGeometry(new_radius, segments=32) + spherical_video.node().addGeom(new_geom) + + # 更新标签 + spherical_video.setTag("original_radius", str(new_radius)) + + print(f"🔄 球形视频半径已更新为: {new_radius}") + + except Exception as e: + print(f"❌ 更新球形视频半径失败: {e}") + + def loadSphericalVideoFile(self, spherical_video, video_path): + """为球形视频加载新的视频文件""" + try: + from panda3d.core import TextureStage, Texture + import os + + if not os.path.exists(video_path): + print(f"❌ 球形视频文件不存在: {video_path}") + return False + + # 加载新的视频纹理 + movie_texture = self.world.gui_manager._loadMovieTexture(video_path) + if movie_texture: + # 清除现有的纹理 + spherical_video.clearTexture() + + # 设置视频纹理属性 + movie_texture.setWrapU(Texture.WM_clamp) + movie_texture.setWrapV(Texture.WM_clamp) + movie_texture.setMinfilter(Texture.FT_linear) + movie_texture.setMagfilter(Texture.FT_linear) + + # 为视频纹理创建专用的纹理阶段 + texture_stage = TextureStage("spherical_video") + texture_stage.setMode(TextureStage.MModulate) + + # 应用纹理到球体 + spherical_video.setTexture(texture_stage, movie_texture) + + # 保存视频纹理引用 + spherical_video.setPythonTag("movie_texture", movie_texture) + spherical_video.setTag("video_path", video_path) + + # 设置为白色以正确显示视频 + spherical_video.setColor(1, 1, 1, 1) + + print(f"✅ 成功加载新球形视频: {video_path}") + return True + else: + print(f"❌ 无法加载球形视频文件: {video_path}") + return False + + except Exception as e: + print(f"❌ 加载球形视频文件失败: {e}") + import traceback + traceback.print_exc() + return False + + def _selectSphericalVideoFile(self, spherical_video): + """选择球形视频文件""" + try: + from PyQt5.QtWidgets import QFileDialog + import os + + # 打开文件选择对话框 + file_path, _ = QFileDialog.getOpenFileName( + None, + "选择360度视频文件", + "", + "视频文件 (*.mp4 *.avi *.mov *.wmv *.flv *.mkv *.webm);;所有文件 (*)" + ) + + if file_path and os.path.exists(file_path): + # 加载新视频文件 - 修复方法调用 + success = self.loadSphericalVideoFile(spherical_video, file_path) + if success: + # 更新文件标签显示 + if hasattr(self, '_spherical_video_controls') and 'file_label' in self._spherical_video_controls: + file_label = self._spherical_video_controls['file_label'] + file_label.setText(os.path.basename(file_path)) + print(f"✅ 球形视频文件已更新为: {file_path}") + else: + print(f"❌ 无法加载球形视频文件: {file_path}") + else: + print("❌ 未选择有效的视频文件") + + except Exception as e: + print(f"❌ 选择球形视频文件失败: {e}") + import traceback + traceback.print_exc() + + def _addVideoScreenProperties(self, video_screen, item): + """添加视频屏幕属性面板""" + try: + from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, + QFileDialog, QHBoxLayout, QLineEdit) + from PyQt5.QtCore import Qt + import os + + video_info_group = QGroupBox("视频信息") + video_info_layout = QGridLayout() + video_info_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + # 显示当前视频文件路径 - 改进版本 + video_path = video_screen.getTag("video_path") if video_screen.hasTag("video_path") else "" + if video_path: + if video_path.startswith("http://") or video_path.startswith("https://"): + # 显示URL信息 + video_info_layout.addWidget(QLabel("视频流URL:"), 0, 0) + path_label = QLabel(video_path) + path_label.setWordWrap(True) + if len(video_path)>30: + display_path = video_path[:27]+"..." + path_label.setToolTip(video_path) + else: + display_path = video_path + path_label.setText(display_path) + path_label.setStyleSheet("color: #00AAFF;") + video_info_layout.addWidget(path_label, 0, 1, 1, 3) + elif os.path.exists(video_path): + # 显示本地文件信息 + video_info_layout.addWidget(QLabel("视频文件:"), 0, 0) + path_label = QLabel(os.path.basename(video_path)) + path_label.setWordWrap(True) + path_label.setStyleSheet("color: #00AAFF;") + video_info_layout.addWidget(path_label, 0, 1, 1, 3) + else: + # 文件不存在 + video_info_layout.addWidget(QLabel("状态:"), 0, 0) + status_label = QLabel("文件不存在") + status_label.setStyleSheet("color: red;") + video_info_layout.addWidget(status_label, 0, 1, 1, 3) + else: + # 无视频 + video_info_layout.addWidget(QLabel("状态:"), 0, 0) + status_label = QLabel("无视频文件") + status_label.setStyleSheet("color: red;") + video_info_layout.addWidget(status_label, 0, 1, 1, 3) + + video_info_group.setLayout(video_info_layout) + self._propertyLayout.addWidget(video_info_group) + + video_control_group = QGroupBox("视频控制") + video_control_layout = QGridLayout() + video_control_layout.setColumnMinimumWidth(0, self.column_minimum_width) + video_control_h_layout = QHBoxLayout() + + play_btn = QPushButton("▶️ 播放") + play_btn.clicked.connect(lambda: self.world.gui_manager.playVideo(video_screen)) + video_control_h_layout.addWidget(play_btn) + + # 暂停按钮 + pause_btn = QPushButton("⏸️ 暂停") + pause_btn.clicked.connect(lambda: self.world.gui_manager.pauseVideo(video_screen)) + video_control_h_layout.addWidget(pause_btn) + + # 停止按钮 + stop_btn = QPushButton("⏹️ 停止") + stop_btn.clicked.connect(lambda: self.world.gui_manager.stopVideo(video_screen)) + video_control_h_layout.addWidget(stop_btn) + + video_control_layout.addLayout(video_control_h_layout, 0, 0, 1, 4) + + # 加载新视频按钮 + load_btn = QPushButton("📁 加载新视频...") + load_btn.clicked.connect(lambda: self._loadNewVideo(video_screen, item)) + video_control_layout.addWidget(load_btn, 1, 0, 1, 4) + + video_control_group.setLayout(video_control_layout) + self._propertyLayout.addWidget(video_control_group) + + # 添加URL输入区域 + url_group = QGroupBox("网络视频") + url_layout = QGridLayout() + url_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + self.url_input = QLineEdit() + self.url_input.setPlaceholderText("输入视频流URL") + url_layout.addWidget(self.url_input, 0, 0, 1, 3) + + load_url_btn = QPushButton("加载URL") + self._current_load_url_btn_3d = load_url_btn + + load_url_btn.clicked.connect( + lambda: self._loadVideoFromURLWithOpenCV_3D(video_screen, self.url_input.text().strip())) + url_layout.addWidget(load_url_btn, 0, 3) + + url_group.setLayout(url_layout) + self._propertyLayout.addWidget(url_group) + + except Exception as e: + print(f"添加视频屏幕属性失败: {e}") + + def _add2DVideoScreenProperties(self, video_screen, item): + """为2D视频屏幕添加属性控制面板""" + try: + from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, + QFileDialog, QHBoxLayout, QLineEdit) + import os + + # 视频信息组 + video_info_group = QGroupBox("2D视频信息") + video_info_layout = QGridLayout() + video_info_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + # 显示当前视频文件路径 + # 在显示视频信息时区分本地文件和网络URL + video_path = video_screen.getTag("video_path") + if video_path: + if video_path.startswith("http://") or video_path.startswith("https://"): + # 显示URL信息 + video_info_layout.addWidget(QLabel("视频流URL:"), 0, 0) + display_path = video_path if len(video_path)<=50 else video_path[:47]+"..." + path_label = QLabel(display_path) + path_label.setWordWrap(True) + path_label.setStyleSheet("color: #00AAFF;") + video_info_layout.addWidget(path_label, 0, 1, 1, 3) + elif os.path.exists(video_path): + # 显示本地文件信息 + video_info_layout.addWidget(QLabel("视频文件:"), 0, 0) + path_label = QLabel(os.path.basename(video_path)) + path_label.setWordWrap(True) + path_label.setStyleSheet("color: #00AAFF;") + video_info_layout.addWidget(path_label, 0, 1, 1, 3) + else: + # 文件不存在 + video_info_layout.addWidget(QLabel("状态:"), 0, 0) + status_label = QLabel("文件不存在") + status_label.setStyleSheet("color: red;") + video_info_layout.addWidget(status_label, 0, 1, 1, 3) + else: + # 无视频 + video_info_layout.addWidget(QLabel("状态:"), 0, 0) + status_label = QLabel("无视频文件") + status_label.setStyleSheet("color: red;") + video_info_layout.addWidget(status_label, 0, 1, 1, 3) + + video_screen.setBin("fixed", 0) + + video_info_group.setLayout(video_info_layout) + self._propertyLayout.addWidget(video_info_group) + + # 视频控制组 + video_control_group = QGroupBox("2D视频控制") + video_control_layout = QGridLayout() + video_control_h_layout = QHBoxLayout() + + # 播放按钮 + play_btn = QPushButton("▶️ 播放") + play_btn.clicked.connect(lambda: self.play2DVideo(video_screen)) + video_control_h_layout.addWidget(play_btn) + + # 暂停按钮 + pause_btn = QPushButton("⏸️ 暂停") + pause_btn.clicked.connect(lambda: self.pause2DVideo(video_screen)) + video_control_h_layout.addWidget(pause_btn) + + # 停止按钮 + stop_btn = QPushButton("⏹️ 停止") + stop_btn.clicked.connect(lambda: self._stop2DVideo(video_screen)) + video_control_h_layout.addWidget(stop_btn) + + video_control_layout.addLayout(video_control_h_layout, 0, 0, 1, 4) + + video_control_group.setLayout(video_control_layout) + + # 加载新视频按钮 + load_btn = QPushButton("📁 加载新视频...") + load_btn.clicked.connect(lambda: self._loadNew2DVideo(video_screen, item)) + video_control_layout.addWidget(load_btn, 1, 0, 1, 4) + self._propertyLayout.addWidget(video_control_group) + + # 添加URL输入区域 + url_group = QGroupBox("网络视频") + url_layout = QGridLayout() + + self.url_input = QLineEdit() + self.url_input.setPlaceholderText("输入视频流URL") + url_layout.addWidget(self.url_input, 0, 0, 1, 3) + + load_url_btn = QPushButton("加载URL") + self._current_load_url_btn_2d = load_url_btn + + load_url_btn.clicked.connect( + lambda: self._loadVideoFromURLWithOpenCV(video_screen, self.url_input.text().strip())) + url_layout.addWidget(load_url_btn, 0, 3) + + url_group.setLayout(url_layout) + self._propertyLayout.addWidget(url_group) + + except Exception as e: + print(f"添加2D视频屏幕属性失败: {e}") + + def _loadVideoFromURLWithOpenCV(self, video_screen, url): + """使用OpenCV从URL加载视频流并在2D视频屏幕上显示(推荐版)""" + try: + import cv2 + import threading + from panda3d.core import Texture, PNMImage + import numpy as np + import time + from PyQt5.QtWidgets import QMessageBox + + load_url_btn = None + if hasattr(self, '_current_load_url_btn_2d'): + load_url_btn = self._current_load_url_btn_2d + + # 安全地更新按钮状态 + def update_button_text(text): + try: + if load_url_btn and load_url_btn.isVisible(): + load_url_btn.setText(text) + return True + except RuntimeError: + # 按钮已被删除,忽略错误 + return False + + def update_button_enabled(enabled): + try: + if load_url_btn and load_url_btn.isVisible(): + load_url_btn.setEnabled(enabled) + return True + except RuntimeError: + # 按钮已被删除,忽略错误 + return False + + if load_url_btn: + load_url_btn.setText("⏳ 加载中...") + load_url_btn.setEnabled(False) + + from PyQt5.QtWidgets import QApplication + QApplication.processEvents() + + # 停止之前可能正在播放的视频 + self._stop2DVideo(video_screen) + + # 使用 OpenCV 打开视频流 + cap = cv2.VideoCapture(url) + if not cap.isOpened(): + error_msg = f"无法打开视频流:\n{url}\n\n可能的原因:\n1. 网络连接问题\n2. 视频流地址无效\n3. 视频服务器不可用\n4. 防火墙阻止访问" + QMessageBox.critical(None, "视频流错误", error_msg) + print(f"❌ 无法打开视频流: {url}") + + # 恢复按钮状态 + if load_url_btn: + load_url_btn.setText("加载URL") + load_url_btn.setEnabled(True) + return False + + # 设置视频参数以提高性能 + cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 减少缓冲以降低延迟 + + # 创建纹理对象 + texture = Texture("video_texture") + texture.setMagfilter(Texture.FTLinear) + texture.setMinfilter(Texture.FTLinear) + texture.setWrapU(Texture.WMClamp) + texture.setWrapV(Texture.WMClamp) + + # 保存视频信息 + video_info = { + 'capture': cap, + 'texture': texture, + 'playing': True + } + + # 应用纹理到视频屏幕 + video_screen.setTexture(texture, 1) + video_screen.setPythonTag("video_info", video_info) + video_screen.setTag("video_path", url) + + # 启动视频播放线程 + def update_video_texture(): + target_fps = 60 # 降低目标帧率以减少CPU使用 + frame_time = 1.0 / target_fps + + frame_count = 0 + + while True: + try: + # 检查是否应该继续播放 + if not (video_screen.hasPythonTag("video_info") and + video_screen.getPythonTag("video_info").get('playing', False)): + break + + video_info = video_screen.getPythonTag("video_info") + cap = video_info['capture'] + + ret, frame = cap.read() + if not ret: + # 视频结束,重新开始播放 + cap.set(cv2.CAP_PROP_POS_FRAMES, 0) + continue + + frame_count += 1 + + # 调整帧大小以降低处理负担 + frame_height, frame_width = frame.shape[:2] + if frame_width > 256: # 限制最大宽度 + scale = 256.0 / frame_width + new_width = int(frame_width * scale) + new_height = int(frame_height * scale) + frame = cv2.resize(frame, (new_width, new_height)) + + # 转换颜色格式 (BGR to RGB) + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + + # 获取帧尺寸 + height, width = frame_rgb.shape[:2] + + # 创建 PNMImage 并设置数据 + img = PNMImage(width, height, 3) + img.set_maxval(255) + + # 使用逐行设置方法(稳定且兼容性好) + for y in range(height): + for x in range(width): + r, g, b = frame_rgb[y, x] + img.setXelVal(x, y, r, g, b) + + # 更新纹理 + texture = video_info['texture'] + if texture: + texture.load(img) + + # 控制帧率 + time.sleep(frame_time) + + except Exception as e: + print(f"视频帧更新错误: {e}") + import traceback + traceback.print_exc() + break + + print("视频播放线程结束") + + # 在单独线程中处理视频流 + thread = threading.Thread(target=update_video_texture, daemon=True) + thread.start() + + print(f"✅ 开始在2D视频屏幕上播放网络视频流: {url}") + + if load_url_btn: + load_url_btn.setText("加载URL") + load_url_btn.setEnabled(True) + return True + + + except ImportError: + error_msg = "缺少必要的库,请安装: pip install opencv-python" + QMessageBox.critical(None, "依赖库缺失", error_msg) + print("❌ 缺少必要的库,请安装: pip install opencv-python") + + # 恢复按钮状态 + try: + if load_url_btn: + load_url_btn.setText("加载URL") + load_url_btn.setEnabled(True) + except RuntimeError: + pass # 按钮已被删除,忽略 + + return False + except Exception as e: + error_msg = f"加载网络视频失败:\n{str(e)}" + QMessageBox.critical(None, "视频加载错误", error_msg) + print(f"❌ 加载网络视频失败: {e}") + import traceback + traceback.print_exc() + + # 恢复按钮状态 + try: + if load_url_btn: + load_url_btn.setText("加载URL") + load_url_btn.setEnabled(True) + except RuntimeError: + pass # 按钮已被删除,忽略 + + return False + + def play2DVideo(self, video_screen): + """播放2D视频""" + try: + # 检查是否已经有视频信息(OpenCV方式) + if video_screen.hasPythonTag("video_info"): + video_info = video_screen.getPythonTag("video_info") + if video_info: + video_info['playing'] = True + print("▶️ 视频已恢复播放") + return True + + # 检查是否是 MovieTexture 方式 + movie_texture = video_screen.getPythonTag("movie_texture") + if movie_texture and hasattr(movie_texture, 'play'): + try: + movie_texture.play() + print("▶️ MovieTexture 视频开始播放") + return True + except Exception as play_error: + print(f"⚠️ MovieTexture 播放视频时出错: {play_error}") + + # 如果没有视频信息,尝试从URL加载 + video_path = video_screen.getTag("video_path") + if video_path: + # 根据路径判断是本地文件还是URL + if video_path.startswith("http://") or video_path.startswith("https://"): + return self._loadVideoFromURLWithOpenCV(video_screen, video_path) + else: + # 本地文件使用 MovieTexture 方式 + return self.world.gui_manager.load2DVideoFile(video_screen, video_path) + else: + print("❌ 没有找到视频源") + return False + + except Exception as e: + print(f"❌ 播放视频失败: {e}") + import traceback + traceback.print_exc() + return False + + def pause2DVideo(self, video_screen): + """暂停2D视频""" + try: + if video_screen.hasPythonTag("video_info"): + video_info = video_screen.getPythonTag("video_info") + if video_info: + video_info['playing'] = False + print("⏸️ 视频已暂停") + return True + print("没有正在播放的视频") + return False + except Exception as e: + print(f"❌ 暂停视频失败: {e}") + return False + + def _stop2DVideo(self, video_screen): + """停止2D视频(内部方法)""" + try: + # 停止视频播放 + if video_screen.hasPythonTag("video_info"): + video_info = video_screen.getPythonTag("video_info") + if video_info: + # 停止播放 + video_info['playing'] = False + + # 释放视频捕获资源 + if 'capture' in video_info and video_info['capture']: + try: + video_info['capture'].release() + except: + pass + + # 清理纹理 + try: + video_screen.clearTexture() + except: + pass + + video_screen.clearPythonTag("video_info") + + # 清理视频路径标签 + if video_screen.hasTag("video_path"): + video_screen.clearTag("video_path") + + print("⏹️ 视频已停止") + return True + except Exception as e: + print(f"❌ 停止视频失败: {e}") + return False + + def _loadNew2DVideo(self, video_screen, item): + """为2D视频屏幕加载新视频文件""" + try: + file_path, _ = QFileDialog.getOpenFileName( + None, + "选择视频文件", + "", + "视频文件 (*.mp4 *.avi *.mov *.mkv *.webm *.ogg)" + ) + if file_path: + success = self.world.gui_manager.load2DVideoFile(video_screen, file_path) + if success: + print(f"成功加载新视频: {file_path}") + # 刷新属性面板以显示新视频信息 + + self.updateGUIPropertyPanel(video_screen, item) + #self._stop2DVideo(video_screen) + return True + except Exception as e: + print(f"加载新视频失败: {e}") + return False + + def _loadVideoFromURLWithOpenCV_3D(self, video_screen, url): + """使用OpenCV从URL加载视频流并在3D视频屏幕上显示""" + try: + import cv2 + import threading + from panda3d.core import Texture, PNMImage, TextureStage + import time + from PyQt5.QtWidgets import QMessageBox + + is_first_load = not video_screen.hasTag("video_path") or not video_screen.getTag("video_path") + + # 确保之前的视频流已停止 + self._stop3DVideo(video_screen) + + load_url_btn = None + if hasattr(self, '_current_load_url_btn_3d'): + load_url_btn = self._current_load_url_btn_3d + + # 安全地更新按钮状态 + def update_button_text(text): + try: + if load_url_btn and load_url_btn.isVisible(): + load_url_btn.setText(text) + return True + except RuntimeError: + # 按钮已被删除,忽略错误 + return False + + def update_button_enabled(enabled): + try: + if load_url_btn and load_url_btn.isVisible(): + load_url_btn.setEnabled(enabled) + return True + except RuntimeError: + # 按钮已被删除,忽略错误 + return False + + # 如果有按钮引用,改变按钮状态为加载中 + if load_url_btn: + update_button_text("⏳ 加载中...") + update_button_enabled(False) + + from PyQt5.QtWidgets import QApplication + QApplication.processEvents() + + # 使用 OpenCV 打开视频流 + cap = cv2.VideoCapture(url) + if not cap.isOpened(): + error_msg = f"无法打开视频流:\n{url}\n\n可能的原因:\n1. 网络连接问题\n2. 视频流地址无效\n3. 视频服务器不可用\n4. 防火墙阻止访问" + QMessageBox.critical(None, "视频流错误", error_msg) + print(f"❌ 无法打开视频流: {url}") + + # 恢复按钮状态 + update_button_text("加载URL") + update_button_enabled(True) + return False + + # 设置视频参数以提高性能 + cap.set(cv2.CAP_PROP_BUFFERSIZE, 1) # 减少缓冲以降低延迟 + + # 创建纹理对象 + texture = Texture("video_texture_3d") + texture.setMagfilter(Texture.FTLinear) + texture.setMinfilter(Texture.FTLinear) + texture.setWrapU(Texture.WMClamp) + texture.setWrapV(Texture.WMClamp) + + # 保存视频信息 + video_info = { + 'capture': cap, + 'texture': texture, + 'playing': True + } + + # 创建纹理阶段并应用到3D视频屏幕 + texture_stage = TextureStage("video_3d") + texture_stage.setSort(0) + texture_stage.setMode(TextureStage.MModulate) + video_screen.setTexture(texture_stage, texture) + + # 设置为白色以正确显示视频 + video_screen.setColor(1, 1, 1, 1) + + # 保存视频信息到PythonTag + video_screen.setPythonTag("video_info", video_info) + video_screen.setTag("video_path", url) + + # 启动视频播放线程 + def update_video_texture(): + target_fps = 60 # 降低目标帧率以减少CPU使用 + frame_time = 1.0 / target_fps + frame_count = 0 + + while True: + try: + # 检查是否应该继续播放 + if not (video_screen.hasPythonTag("video_info") and + video_screen.getPythonTag("video_info").get('playing', False)): + break + + video_info = video_screen.getPythonTag("video_info") + cap = video_info['capture'] + + ret, frame = cap.read() + if not ret: + # 视频结束,重新开始播放 + cap.set(cv2.CAP_PROP_POS_FRAMES, 0) + continue + + frame_count += 1 + + # 调整帧大小以降低处理负担 + frame_height, frame_width = frame.shape[:2] + if frame_width > 256: # 限制最大宽度 + scale = 256.0 / frame_width + new_width = int(frame_width * scale) + new_height = int(frame_height * scale) + frame = cv2.resize(frame, (new_width, new_height)) + + # 转换颜色格式 (BGR to RGB) + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + + # 获取帧尺寸 + height, width = frame_rgb.shape[:2] + + # 创建 PNMImage 并设置数据 + img = PNMImage(width, height, 3) + img.set_maxval(255) + + # 使用逐行设置方法(稳定且兼容性好) + for y in range(height): + for x in range(width): + r, g, b = frame_rgb[y, x] + img.setXelVal(x, y, r, g, b) + + # 更新纹理 + texture = video_info['texture'] + if texture: + texture.load(img) + + # 控制帧率 + time.sleep(frame_time) + + except Exception as e: + print(f"3D视频帧更新错误: {e}") + import traceback + traceback.print_exc() + break + + print("3D视频播放线程结束") + + # 在单独线程中处理视频流 + thread = threading.Thread(target=update_video_texture, daemon=True) + thread.start() + + print(f"✅ 开始在3D视频屏幕上播放网络视频流: {url}") + + # 恢复按钮状态 + update_button_text("加载URL") + update_button_enabled(True) + return True + + except ImportError: + error_msg = "缺少必要的库,请安装: pip install opencv-python" + QMessageBox.critical(None, "依赖库缺失", error_msg) + print("❌ 缺少必要的库,请安装: pip install opencv-python") + + # 恢复按钮状态 + try: + if load_url_btn: + load_url_btn.setText("加载URL") + load_url_btn.setEnabled(True) + except RuntimeError: + pass # 按钮已被删除,忽略 + + return False + except Exception as e: + error_msg = f"加载3D网络视频失败:\n{str(e)}" + QMessageBox.critical(None, "视频加载错误", error_msg) + print(f"❌ 加载3D网络视频失败: {e}") + import traceback + traceback.print_exc() + + # 恢复按钮状态 + try: + if load_url_btn: + load_url_btn.setText("加载URL") + load_url_btn.setEnabled(True) + except RuntimeError: + pass # 按钮已被删除,忽略 + + return False + + def _verifyVideoDisplay(self, video_screen, texture): + try: + applied_texture = video_screen.getTexture() + if applied_texture and applied_texture.getName() == texture.getName(): + print("视频纹理已正确应用到3d模型") + return True + else: + print("视频纹理未正确应用到3d模型") + return False + except Exception as e: + print(f"验证视频显示时出错:{e}") + return False + + def _retryLoadVideoFromURLWithOpenCV_3D(self, video_screen, url): + print("重新加载3D视频流") + return self._loadVideoFromURLWithOpenCV_3D(video_screen, url) + + def _stop3DVideo(self, video_screen): + """停止3D视频(内部方法)""" + try: + # 停止视频播放 + if video_screen.hasPythonTag("video_info"): + video_info = video_screen.getPythonTag("video_info") + if video_info: + # 停止播放 + video_info['playing'] = False + + # 释放视频捕获资源 + if 'capture' in video_info and video_info['capture']: + try: + video_info['capture'].release() + except: + pass + + # 清理纹理 + try: + video_screen.clearTexture() + except: + pass + + video_screen.clearPythonTag("video_info") + + # 清理视频路径标签 + if video_screen.hasTag("video_path"): + video_screen.clearTag("video_path") + + print("⏹️ 3D视频已停止") + return True + except Exception as e: + print(f"❌ 停止3D视频失败: {e}") + return False + + def _loadNewVideo(self, video_screen, item): + try: + file_path, _ = QFileDialog.getOpenFileName( + None, + "选择视频文件", + "", + "视频文件(*.mp4 *.avi *.mov *.mkv *.webm *.ogg)" + ) + if file_path: + success = self.world.gui_manager.loadVideoFile(video_screen, file_path) + if success: + self.updateGUIPropertyPanel(video_screen, item) + return True + except Exception as e: + print(f"加载新视频失败{e}") + return False + + def editGUI2DPosition(self, gui_element, axis, value): + """编辑2D GUI元素位置""" + try: + gui_type = gui_element.getTag("gui_type") + + if gui_type in ["button", "label", "entry", "2d_image"]: + # 2D元素使用屏幕坐标,需要转换 + current_pos = gui_element.getPos() + + if axis == "x": + # 转换逻辑坐标到屏幕坐标 + screen_x = value * 0.1 + new_pos = (screen_x, current_pos.getY(), current_pos.getZ()) + elif axis == "z": + screen_z = value * 0.1 + new_pos = (current_pos.getX(), current_pos.getY(), screen_z) + else: + return False + self.refreshModelValues(gui_element) + gui_element.setPos(*new_pos) + print(f"✓ 更新2D GUI元素位置: {axis}={value}") + return True + else: + print(f"✗ 不支持的GUI类型进行2D位置编辑: {gui_type}") + return False + except Exception as e: + print(f"✗ 更新2D GUI元素位置失败: {e}") + import traceback + traceback.print_exc() + return False + + def editGUIScale(self, gui_element, axis, value): + """编辑GUI元素缩放""" + try: + gui_type = gui_element.getTag("gui_type") + current_scale = gui_element.getScale() + + # 确保缩放值不为0 + if value == 0: + value = 0.01 + + if gui_type in ["3d_text", "3d_image", "2d_image", "video_screen", "2d_video_screen"]: + # 3D元素处理 + if axis == "x": + new_scale = (value, current_scale.getY(), current_scale.getZ()) + elif axis == "y": + new_scale = (current_scale.getX(), value, current_scale.getZ()) + elif axis == "z": + new_scale = (current_scale.getX(), current_scale.getY(), value) + else: + return False + + gui_element.setScale(*new_scale) + else: + # 2D元素处理 + if axis in ["x", "z"]: # 对于2D图像,x和z分别代表宽度和高度 + # 保持原有缩放比例,仅调整指定轴 + if axis == "x": + gui_element.setScale(value, + current_scale.getZ() if hasattr(current_scale, 'getZ') else current_scale[ + 1] if isinstance(current_scale, (list, tuple)) else current_scale) + elif axis == "z": + gui_element.setScale( + current_scale.getX() if hasattr(current_scale, 'getX') else current_scale[0] if isinstance( + current_scale, (list, tuple)) else current_scale, value) + else: + # 对于其他2D元素,使用统一缩放 + gui_element.setScale(value) + + print(f"✓ 更新GUI元素缩放: {axis}={value}") + return True + except Exception as e: + print(f"✗ 更新GUI元素缩放失败: {e}") + import traceback + traceback.print_exc() + return False + + def _update2DImageWidth(self, gui_element, width): + try: + current_scale = gui_element.getScale() + width_scaled = width / 2 + height_scaled = current_scale.getZ() + + gui_element.setScale(width_scaled, current_scale.getY(), height_scaled) + + if hasattr(gui_element, '_height_spinbox'): + gui_element._height_spinbox.blockSignals(True) + gui_element._height_spinbox.setValue(height_scaled * 2) + gui_element._height_spinbox.blockSignals(False) + + print(f"✓ 更新2D图片宽度: {width}") + except Exception as e: + print(f"✗ 更新2D图片宽度失败: {e}") + + def _update2DImageHeight(self, gui_element, height): + """更新2D图片高度""" + try: + # 获取当前缩放 + current_scale = gui_element.getScale() + # 保持宽度不变,只更新高度(Z轴) + height_scaled = height / 2 # 转换为内部缩放值 + width_scaled = current_scale.getX() + + gui_element.setScale(width_scaled, current_scale.getY(), height_scaled) + + # 更新宽度控件的值 + if hasattr(gui_element, '_width_spinbox'): + gui_element._width_spinbox.blockSignals(True) + gui_element._width_spinbox.setValue(width_scaled * 2) + gui_element._width_spinbox.blockSignals(False) + + print(f"✓ 更新2D图片高度: {height}") + except Exception as e: + print(f"✗ 更新2D图片高度失败: {e}") + + def _update2DImageScale(self, gui_element, scale): + try: + scaled_value = scale / 2 + gui_element.setScale(scaled_value, 0, scaled_value) + print(f"✓ 更新2D图片缩放: {scale}") + except Exception as e: + print(f"✗ 更新2D图片缩放失败: {e}") + + def _update3DImageScale(self, gui_element, axis, scale): + """更新3D元素缩放""" + try: + current_scale = gui_element.getScale() + + if axis == 'x': + new_scale = (scale, current_scale.getY(), current_scale.getZ()) + # elif axis == 'y': + # new_scale = (current_scale.getX(), scale, current_scale.getZ()) + elif axis == 'z': + new_scale = (current_scale.getX(), current_scale.getY(), scale) + else: + return + + gui_element.setScale(*new_scale) + print(f"✓ 更新3D元素轴缩放 {axis}: {scale}") + except Exception as e: + print(f"✗ 更新3D元素轴缩放失败: {e}") + + def _selectGUIColor(self, gui_element): + """选择GUI元素的字体颜色""" + from PyQt5.QtWidgets import QColorDialog + from PyQt5.QtGui import QColor + from panda3d.core import Vec4 + + # 获取当前颜色(如果已设置) + current_color = QColor(255, 255, 255) # 默认白色 + + # 尝试获取当前设置的颜色 + gui_type = gui_element.getTag("gui_type") + try: + if gui_type == "3d_text": + if gui_element.hasMaterial(): + material = gui_element.getMaterial() + base_color = material.getBaseColor() + current_color = QColor( + int(base_color.getX() * 255), + int(base_color.getY() * 255), + int(base_color.getZ() * 255), + int(base_color.getW() * 255) + ) + else: + # 从节点颜色获取 + node_color = gui_element.getColor() + current_color = QColor( + int(node_color.getX() * 255), + int(node_color.getY() * 255), + int(node_color.getZ() * 255), + int(node_color.getW() * 255) + ) + # current_color_obj = gui_element.getColor() + # current_color = QColor( + # int(current_color_obj[0] * 255), + # int(current_color_obj[1] * 255), + # int(current_color_obj[2] * 255) + # ) + # 对于其他类型的元素,可以添加类似的获取当前颜色的逻辑 + except: + pass # 使用默认颜色 + + color = QColorDialog.getColor(current_color, None, "选择字体颜色") + if color.isValid(): + r, g, b = color.red() / 255.0, color.green() / 255.0, color.blue() / 255.0 + self._updateGUITextColor(gui_element, (r, g, b, 1.0)) + + def _updateGUITextColor(self, gui_element, color): + """更新GUI元素的字体颜色""" + try: + gui_type = gui_element.getTag("gui_type") + + if gui_type in ["button", "label", "entry"]: + # 对于DirectGUI元素,使用text_fg属性 + gui_element['text_fg'] = color + print(f"✓ 更新DirectGUI元素字体颜色: {gui_type}") + + elif gui_type == "3d_text": + # # 对于3D文本元素,直接设置颜色 + # gui_element.setColor(*color) + # print(f"✓ 更新3D文本字体颜色: {gui_type}") + + from panda3d.core import Material, Vec4 + if not gui_element.hasMaterial(): + material = Material(f"text-material-{gui_element.getName()}") + material.setBaseColor(Vec4(color[0], color[1], color[2], color[3])) + material.setDiffuse(Vec4(color[0], color[1], color[2], color[3])) + material.setAmbient(Vec4(color[0] * 0.5, color[1] * 0.5, color[2] * 0.5, color[3])) + material.setSpecular(Vec4(0.1, 0.1, 0.1, 1.0)) + material.setShininess(10.0) + gui_element.setMaterial(material, 1) + else: + # 更新现有材质 + material = gui_element.getMaterial() + material.setBaseColor(Vec4(color[0], color[1], color[2], color[3])) + material.setDiffuse(Vec4(color[0], color[1], color[2], color[3])) + gui_element.setMaterial(material, 1) + print(f"✓ 更新3D文本材质颜色: {color}") + + gui_element.setColor(*color) + + elif gui_type == "3d_image": + # 对于3D图片,如果有文本标签的话 + # 这里可以根据需要添加特定处理 + pass + + print(f"✓ 更新GUI元素字体颜色: {gui_type}, 颜色: {color}") + except Exception as e: + print(f"✗ 更新GUI元素字体颜色失败: {e}") + import traceback + traceback.print_exc() + + def _updateGUIScaleY(self, gui_element, scale_y): + """更新GUI元素Y轴缩放""" + try: + gui_type = gui_element.getTag("gui_type") + current_scale = gui_element.getScale() + + # 对于不同的GUI类型使用不同的缩放方法 + if gui_type in ["3d_text", "3d_image"]: + # 对于3D元素,直接设置缩放 + new_scale = (current_scale.getX(), scale_y, current_scale.getZ()) + gui_element.setScale(*new_scale) + else: + # 对于2D元素,保持原有的缩放方法 + gui_element.setScale(scale_y) + + print(f"✓ 更新GUI元素Y轴缩放: {scale_y}") + except Exception as e: + print(f"✗ 更新GUI元素Y轴缩放失败: {e}") + + def _updateGUIScaleZ(self, gui_element, scale_z): + """更新GUI元素Z轴缩放""" + try: + gui_type = gui_element.getTag("gui_type") + current_scale = gui_element.getScale() + + # 对于不同的GUI类型使用不同的缩放方法 + if gui_type in ["3d_text", "3d_image"]: + # 对于3D元素,直接设置缩放 + new_scale = (current_scale.getX(), current_scale.getY(), scale_z) + gui_element.setScale(*new_scale) + else: + # 对于2D元素,使用单一缩放值(保持宽高比) + gui_element.setScale(scale_z) + + print(f"✓ 更新GUI元素Z轴缩放: {scale_z}") + except Exception as e: + print(f"✗ 更新GUI元素Z轴缩放失败: {e}") + + def update2DImageTexture(self, image_element, texture_path): + """更新2D图片纹理 - 修复版""" + try: + from panda3d.core import TextureStage, TransparencyAttrib + + # 加载纹理 + texture = self.world.loader.loadTexture(texture_path) + if not texture: + print(f"❌ 无法加载纹理: {texture_path}") + return False + + # 设置纹理过滤 + texture.setMagfilter(texture.FTLinear) + texture.setMinfilter(texture.FTLinearMipmapLinear) + + # 应用纹理到元素 + image_element.setTexture(texture, 1) + + # 设置正确的渲染属性以避免黑色显示 + image_element.setTransparency(TransparencyAttrib.MAlpha) + image_element.setBin("fixed", 20) # 确保在正确层级渲染 + image_element.setDepthWrite(False) + image_element.setDepthTest(False) + image_element.setLightOff() + image_element.setTwoSided(True) + + print(f"✅ 2D图片纹理已更新: {texture_path}") + return True + + except Exception as e: + print(f"❌ 更新2D图片纹理失败: {e}") + import traceback + traceback.print_exc() + return False + + def _updateScriptPropertyPanel(self, game_object): + """更新脚本属性面板""" + # 获取对象上的脚本 + scripts = self.world.getScripts(game_object) + + if scripts: + # 添加脚本信息标题 + scriptTitleLabel = QLabel("已挂载脚本:") + scriptTitleLabel.setStyleSheet("color: #00AAFF; font-weight: bold; font-size: 12px;") + self._propertyLayout.addRow(scriptTitleLabel) + + # 显示每个脚本的信息 + for i, script_component in enumerate(scripts): + script_name = script_component.script_name + enabled = script_component.enabled + + # 脚本名称和状态 + scriptLabel = QLabel(f"脚本 {i + 1}:") + scriptInfo = QLabel(f"{script_name}") + scriptInfo.setStyleSheet("color: green; font-weight: bold;" if enabled else "color: gray;") + self._propertyLayout.addRow(scriptLabel, scriptInfo) + + # 脚本启用/禁用按钮 + enableButton = QPushButton("禁用" if enabled else "启用") + enableButton.setStyleSheet( + "background-color: #FF6B6B; color: white;" if enabled + else "background-color: #4ECDC4; color: white;" + ) + enableButton.clicked.connect( + lambda checked, sc=script_component: self._toggleScriptEnabled(sc) + ) + self._propertyLayout.addRow("状态:", enableButton) + + # 分隔线 + if i < len(scripts) - 1: + separator = QLabel("─" * 20) + separator.setStyleSheet("color: lightgray;") + self._propertyLayout.addRow(separator) + else: + # 显示无脚本信息 + noScriptLabel = QLabel("无挂载脚本") + noScriptLabel.setStyleSheet("color: gray; font-style: italic;") + self._propertyLayout.addRow("脚本:", noScriptLabel) + + def _toggleScriptEnabled(self, script_component): + """切换脚本启用状态""" + script_component.enabled = not script_component.enabled + status = "启用" if script_component.enabled else "禁用" + print(f"脚本 {script_component.script_name} 已{status}") + + # 刷新属性面板显示 + if hasattr(self.world.selection, 'selectedObject') and self.world.selection.selectedObject: + # 找到当前选中项并更新 + tree_widget = self.world.treeWidget + if tree_widget and tree_widget.currentItem(): + self.updatePropertyPanel(tree_widget.currentItem()) + + def updateLightPropertyPanel(self, model): + """更新光源属性面板""" + light_object = model.getPythonTag("rp_light_object") + + if light_object: + # 变换属性组 + transform_group = QGroupBox("变换 Transform") + transform_layout = QGridLayout() + transform_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + transform_layout.addWidget(QLabel("位置"), 0, 0) + + # 位置属性 + current_pos = light_object.pos + position_row = QHBoxLayout() + position_row.setContentsMargins(0, 0, 0, 0) + position_row.setSpacing(4) + + xPos = QDoubleSpinBox() + xPos.setRange(-1000, 1000) + xPos.setValue(current_pos.getX()) + xPos.valueChanged.connect(lambda v: self._updateLightPosition(light_object, model, 'x', v)) + + yPos = QDoubleSpinBox() + yPos.setRange(-1000, 1000) + yPos.setValue(current_pos.getY()) + yPos.valueChanged.connect(lambda v: self._updateLightPosition(light_object, model, 'y', v)) + + zPos = QDoubleSpinBox() + zPos.setRange(-1000, 1000) + zPos.setValue(current_pos.getZ()) + zPos.valueChanged.connect(lambda v: self._updateLightPosition(light_object, model, 'z', v)) + + for axis_label, widget in (("X:", xPos), ("Y:", yPos), ("Z:", zPos)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + position_row.addWidget(axis) + position_row.addWidget(widget) + + transform_layout.addLayout(position_row, 0, 1, 1, 3) + + worldPos = model.getPos(self.world.render) + world_position_row = QHBoxLayout() + world_position_row.setContentsMargins(0, 0, 0, 0) + world_position_row.setSpacing(4) + transform_layout.addWidget(QLabel("世界位置"), 1, 0) + + worldXPos = QDoubleSpinBox() + worldXPos.setRange(-1000, 1000) + worldXPos.setValue(worldPos.getX()) + worldXPos.setReadOnly(True) + + worldYPos = QDoubleSpinBox() + worldYPos.setRange(-1000, 1000) + worldYPos.setValue(worldPos.getY()) + worldYPos.setReadOnly(True) + + worldZPos = QDoubleSpinBox() + worldZPos.setRange(-1000, 1000) + worldZPos.setValue(worldPos.getZ()) + worldZPos.setReadOnly(True) + + for axis_label, widget in (("X:", worldXPos), ("Y:", worldYPos), ("Z:", worldZPos)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + world_position_row.addWidget(axis) + world_position_row.addWidget(widget) + + transform_layout.addLayout(world_position_row, 1, 1, 1, 3) + + # 旋转属性(仅聚光灯) + next_row = 2 + if hasattr(light_object, 'direction'): + current_hpr = model.getHpr() + rotation_row = QHBoxLayout() + rotation_row.setContentsMargins(0, 0, 0, 0) + rotation_row.setSpacing(4) + transform_layout.addWidget(QLabel("旋转"), next_row, 0) + + hRot = QDoubleSpinBox() + hRot.setRange(-180, 180) + hRot.setValue(current_hpr.getX()) + hRot.valueChanged.connect(lambda v: self._updateLightRotation(light_object, model, 'h', v)) + + pRot = QDoubleSpinBox() + pRot.setRange(-180, 180) + pRot.setValue(current_hpr.getY()) + pRot.valueChanged.connect(lambda v: self._updateLightRotation(light_object, model, 'p', v)) + + rRot = QDoubleSpinBox() + rRot.setRange(-180, 180) + rRot.setValue(current_hpr.getZ()) + rRot.valueChanged.connect(lambda v: self._updateLightRotation(light_object, model, 'r', v)) + + for axis_label, widget in (("H:", hRot), ("P:", pRot), ("R:", rRot)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + rotation_row.addWidget(axis) + rotation_row.addWidget(widget) + + transform_layout.addLayout(rotation_row, next_row, 1, 1, 3) + next_row += 1 + + # 缩放属性 + current_scale = model.getScale() + scale_row = QHBoxLayout() + scale_row.setContentsMargins(0, 0, 0, 0) + scale_row.setSpacing(4) + transform_layout.addWidget(QLabel("缩放"), next_row, 0) + + xScaleSpinBox = QDoubleSpinBox() + xScaleSpinBox.setRange(0.01, 100) + xScaleSpinBox.setSingleStep(0.1) + xScaleSpinBox.setValue(current_scale.getX()) + xScaleSpinBox.valueChanged.connect(lambda v: self._updateLightScale(model, 'x', v)) + + yScaleSpinBox = QDoubleSpinBox() + yScaleSpinBox.setRange(0.01, 100) + yScaleSpinBox.setSingleStep(0.1) + yScaleSpinBox.setValue(current_scale.getY()) + yScaleSpinBox.valueChanged.connect(lambda v: self._updateLightScale(model, 'y', v)) + + zScaleSpinBox = QDoubleSpinBox() + zScaleSpinBox.setRange(0.01, 100) + zScaleSpinBox.setSingleStep(0.1) + zScaleSpinBox.setValue(current_scale.getZ()) + zScaleSpinBox.valueChanged.connect(lambda v: self._updateLightScale(model, 'z', v)) + + for axis_label, widget in (("X:", xScaleSpinBox), ("Y:", yScaleSpinBox), ("Z:", zScaleSpinBox)): + axis = QLabel(axis_label) + axis.setAlignment(Qt.AlignVCenter) + axis.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + scale_row.addWidget(axis) + scale_row.addWidget(widget) + + transform_layout.addLayout(scale_row, next_row, 1, 1, 3) + + transform_group.setLayout(transform_layout) + self._propertyLayout.addWidget(transform_group) + + # 光源属性组 + light_group = QGroupBox("光源属性") + light_layout = QGridLayout() + + current_energy = light_object.energy + stored_energy = None + + if current_energy == 0.0 and model.hasTag("stored_energy"): + try: + stored_energy = float(model.getTag("stored_energy")) + except ValueError: + pass + + display_energy = stored_energy if stored_energy is not None and stored_energy > 0 else current_energy + + # 能量 + light_layout.addWidget(QLabel("能量:"), 0, 0) + energySpinBox = QDoubleSpinBox() + energySpinBox.setRange(0, 10000) + energySpinBox.setValue(display_energy) + energySpinBox.valueChanged.connect(lambda v: self._updateLightEnergy(light_object, v)) + light_layout.addWidget(energySpinBox, 0, 1, 1, 3) + + # 半径 + light_layout.addWidget(QLabel("半径:"), 1, 0) + radiusSpinBox = QDoubleSpinBox() + radiusSpinBox.setRange(1, 2000) + radiusSpinBox.setValue(light_object.radius) + radiusSpinBox.valueChanged.connect(lambda v: self._updateLightRadius(light_object, v)) + light_layout.addWidget(radiusSpinBox, 1, 1, 1, 3) + + # 视野角度(仅聚光灯) + if hasattr(light_object, 'fov'): + light_layout.addWidget(QLabel("视野角度:"), 2, 0) + fovSpinBox = QDoubleSpinBox() + fovSpinBox.setRange(1, 180) + fovSpinBox.setValue(light_object.fov) + fovSpinBox.valueChanged.connect(lambda v: self._updateLightFOV(light_object, v)) + light_layout.addWidget(fovSpinBox, 2, 1, 1, 3) + + light_group.setLayout(light_layout) + self._propertyLayout.addWidget(light_group) + + def _updateLightPosition(self, light_object, node_path, axis, value): + current_pos = light_object.pos + + if axis == 'x': + new_pos = Vec3(value, current_pos.getY(), current_pos.getZ()) + elif axis == 'y': + new_pos = Vec3(current_pos.getX(), value, current_pos.getZ()) + else: # z + new_pos = Vec3(current_pos.getX(), current_pos.getY(), value) + # 更新RenderPipeline光源位置 + light_object.pos = new_pos + + # 同步更新场景节点位置(用于显示) + node_path.setPos(new_pos) + + def _updateLightRotation(self, light_object, node_path, axis, value): + """更新光源旋转""" + from panda3d.core import Vec3 + + current_hpr = node_path.getHpr() + if axis == 'h': + new_hpr = Vec3(value, current_hpr.getY(), current_hpr.getZ()) + elif axis == 'p': + new_hpr = Vec3(current_hpr.getX(), value, current_hpr.getZ()) + else: + new_hpr = Vec3(current_hpr.getX(), current_hpr.getY(), value) + + node_path.setHpr(new_hpr) + + if hasattr(light_object, 'direction'): + direction_mat = node_path.getMat() + new_direction = direction_mat.xformVec(Vec3(0, 1, 0)) + light_object.direction = new_direction + + print(f"光源旋转已更新:{axis}={value}") + + def _updateLightEnergy(self, light_object, value): + """更新光源强度""" + light_object.energy = value + + def _updateLightRadius(self, light_object, value): + """更新光源半径""" + light_object.radius = value + + def _updateLightFOV(self, light_Object, value): + """更新聚光灯视野角度""" + if hasattr(light_Object, 'fov'): + light_Object.fov = value + + def _updateLightTemperature(self, light_object, value): + """更新光源色温""" + light_object.set_color_from_temperature(value) + # 保存色温值以便下次显示 + light_object._temperature = value + + def _updateLightInnerRadius(self, light_object, value): + """更新点光源内半径""" + if hasattr(light_object, 'inner_radius'): + light_object.inner_radius = value + + def _updateLightShaowResolution(self, light_object, value): + """更新阴影分辨率""" + light_object.shadow_map_resolution = value + + def _updateLightNearPlane(self, light_object, value): + """更新近平面距离""" + light_object.near_plane = value + + def _updateLightCastsShadows(self, light_object, casts_shadows): + """更新光源是否投射阴影""" + light_object.casts_shadows = casts_shadows + + def _updateLightScale(self, node_path, axis, value): + """更新光源节点缩放""" + current_scale = node_path.getScale() + + if axis == 'x': + new_scale = Vec3(value, current_scale.getY(), current_scale.getZ()) + elif axis == 'y': + new_scale = Vec3(current_scale.getX(), value, current_scale.getZ()) + else: + new_scale = Vec3(current_scale.getX(), current_scale.getY(), value) + + node_path.setScale(new_scale) + + def _generateUniqueMaterialNames(self, materials, model_name): + """生成唯一的材质名称,避免重复""" + material_names = {} + unique_names = [] + + for i, material in enumerate(materials): + # 获取材质的原始名称 + base_name = material.get_name() if hasattr(material, 'get_name') and material.get_name() else f"材质{i + 1}" + full_name = f"{base_name}:{model_name}" + + # 检查是否重复 + if full_name in material_names: + # 如果重复,增加计数器 + material_names[full_name] += 1 + unique_name = f"{full_name}_{material_names[full_name]}" + else: + # 首次出现,记录并使用原名 + material_names[full_name] = 0 + unique_name = full_name + + unique_names.append(unique_name) + + return unique_names + + def _updateModelMaterialPanel(self, model): + """模型材质属性""" + if model.is_empty(): + print("警告: 无法在空的 NodePath 上查找材质") + no_material_group = QGroupBox("材质信息") + no_material_layout = QGridLayout() + no_material_label = QLabel("无材质") + no_material_label.setStyleSheet("color: gray;font-style:italic;") + no_material_layout.addWidget(no_material_label, 0, 0) + no_material_group.setLayout(no_material_layout) + self._propertyLayout.addWidget(no_material_group) + return + + materials = model.find_all_materials() + + if not materials: + no_material_group = QGroupBox("材质信息") + no_material_layout = QGridLayout() + no_material_label = QLabel("无材质") + no_material_label.setStyleSheet("color: gray;font-style:italic;") + no_material_layout.addWidget(no_material_label, 0, 0) + no_material_group.setLayout(no_material_layout) + self._propertyLayout.addWidget(no_material_group) + return + + model_name = model.getName() or "未命名模型" + name_counter = {} + + # 创建材质到几何节点的映射字典 + self._material_geom_mapping = {} + self._material_display_names = {} + + for i, material in enumerate(materials): + # 查找使用该材质的几何节点,使用几何节点名称作为材质标题 + geom_node = self._findSpecificGeomNodeWithMaterial(model, material) + + if geom_node: + geom_node_name = geom_node.getName() + unique_name = f"{geom_node_name}({model_name})" + print(f"材质 {i}: 使用几何节点名称 '{geom_node_name}'") + else: + material_name = material.get_name() if hasattr(material, + 'get_name') and material.get_name() else f"材质{i + 1}" + unique_name = f"{material_name}({model_name})" + print(f"材质 {i}: 未找到几何节点,使用材质名称 '{material_name}'") + + # 处理重复名称 + if unique_name in name_counter: + name_counter[unique_name] += 1 + display_name = f"{unique_name}_{name_counter[unique_name]}" + else: + name_counter[unique_name] = 1 + display_name = unique_name + + # 存储材质和对应的几何节点信息到映射字典中 + material_id = id(material) + self._material_geom_mapping[material_id] = geom_node + self._material_display_names[material_id] = display_name + + # 材质信息组 + # material_group = QGroupBox(display_name) + material_group = QGroupBox("材质属性") + material_layout = QGridLayout() + material_layout.setColumnMinimumWidth(0, self.column_minimum_width) + + material_layout.addWidget(QLabel("名称:"), 0, 0) + limited_length = 25 + if len(display_name) > limited_length: + display_name = f"{display_name[:limited_length - 3]}..." + name_label = QLabel(display_name) + # name_label.setStyleSheet("color: #FF6B6B; font-weight: bold;") + material_layout.addWidget(name_label, 0, 1, 1, 3) + + # 检查材质类型并显示状态 + material_status = self._getMaterialStatus(material) + if material_status != "标准PBR材质": + material_layout.addWidget(QLabel("状态:"), 1, 0) + status_label = QLabel(material_status) + # status_label.setStyleSheet("color:#FFA500;font-style:italic;font-size:10px;") + material_layout.addWidget(status_label, 1, 1, 1, 3) + + # 基础颜色编辑 + base_color = self._getOrCreateMaterialBaseColor(material) + if base_color is not None: + print(f"材质基础颜色: {base_color}") + + # 基础颜色编辑 + color_row = 2 if material_status != "标准PBR材质" else 1 + + r_spinbox = QDoubleSpinBox() + r_spinbox.setRange(0.0, 1.0) + r_spinbox.setSingleStep(0.01) + r_spinbox.setValue(base_color.x) + r_spinbox.valueChanged.connect(lambda v, mat=material: self._updateMaterialBaseColor(mat, 'r', v)) + + g_spinbox = QDoubleSpinBox() + g_spinbox.setRange(0.0, 1.0) + g_spinbox.setSingleStep(0.01) + g_spinbox.setValue(base_color.y) + g_spinbox.valueChanged.connect(lambda v, mat=material: self._updateMaterialBaseColor(mat, 'g', v)) + + b_spinbox = QDoubleSpinBox() + b_spinbox.setRange(0.0, 1.0) + b_spinbox.setSingleStep(0.01) + b_spinbox.setValue(base_color.z) + b_spinbox.valueChanged.connect(lambda v, mat=material: self._updateMaterialBaseColor(mat, 'b', v)) + + title_label = QLabel("基础颜色") + material_layout.addWidget(title_label, color_row, 0) + + base_color_layout = QHBoxLayout() + base_color_layout.setContentsMargins(0, 0, 0, 0) + base_color_layout.setSpacing(4) + + for label_text, spinbox in (("R:", r_spinbox), ("G:", g_spinbox), ("B:", b_spinbox)): + channel_label = QLabel(label_text) + channel_label.setAlignment(Qt.AlignVCenter) + channel_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + base_color_layout.addWidget(channel_label) + spinbox.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + base_color_layout.addWidget(spinbox) + + # base_color_layout.addStretch(1) + + material_layout.addLayout(base_color_layout, color_row, 1, 1, 3) + else: + no_base_color_label = QLabel("无法获取材质基础颜色") + no_base_color_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") + material_layout.addWidget(QLabel("基础颜色:"), 1, 0) + material_layout.addWidget(no_base_color_label, 1, 1, 1, 3) + + # 材质属性行 + current_row = color_row + 1 if base_color is not None else 2 + + # 粗糙度 + if hasattr(material, 'roughness') and material.roughness is not None: + try: + roughness_value = float(material.roughness) + material_layout.addWidget(QLabel("粗糙度:"), current_row, 0) + roughness_spinbox = QDoubleSpinBox() + roughness_spinbox.setRange(0.0, 1.0) + roughness_spinbox.setSingleStep(0.01) + roughness_spinbox.setValue(roughness_value) + roughness_spinbox.valueChanged.connect( + lambda v, mat=material: self._updateMaterialRoughness(mat, v)) + material_layout.addWidget(roughness_spinbox, current_row, 1, 1, 1) + except (TypeError, ValueError) as e: + print(f"粗糙度值无效: {material.roughness}, 错误: {e}") + material_layout.addWidget(QLabel("粗糙度:"), current_row, 0) + no_roughness_label = QLabel("粗糙度值无效") + no_roughness_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") + material_layout.addWidget(no_roughness_label, current_row, 1, 1, 1) + else: + material_layout.addWidget(QLabel("粗糙度:"), current_row, 0) + no_roughness_label = QLabel("不支持") + no_roughness_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") + material_layout.addWidget(no_roughness_label, current_row, 1, 1, 1) + + current_row += 1 + + # 金属性 + if hasattr(material, 'metallic') and material.metallic is not None: + try: + metallic_value = float(material.metallic) + material_layout.addWidget(QLabel("金属性:"), current_row, 0) + metallic_spinbox = QDoubleSpinBox() + metallic_spinbox.setRange(0.0, 1.0) + metallic_spinbox.setSingleStep(0.01) + metallic_spinbox.setValue(metallic_value) + metallic_spinbox.valueChanged.connect(lambda v, mat=material: self._updateMaterialMetallic(mat, v)) + material_layout.addWidget(metallic_spinbox, current_row, 1, 1, 1) + except (TypeError, ValueError) as e: + print(f"金属性值无效: {material.metallic}, 错误: {e}") + material_layout.addWidget(QLabel("金属性:"), current_row, 0) + no_metallic_label = QLabel("值无效") + no_metallic_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") + material_layout.addWidget(no_metallic_label, current_row, 1, 1, 1) + else: + material_layout.addWidget(QLabel("金属性:"), current_row, 0) + no_metallic_label = QLabel("不支持") + no_metallic_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") + material_layout.addWidget(no_metallic_label, current_row, 1, 1, 1) + + current_row += 1 + + # 折射率 + if hasattr(material, 'refractive_index') and material.refractive_index is not None: + try: + ior_value = float(material.refractive_index) + material_layout.addWidget(QLabel("折射率:"), current_row, 0) + ior_spinbox = QDoubleSpinBox() + ior_spinbox.setRange(1.0, 3.0) + ior_spinbox.setSingleStep(0.01) + ior_spinbox.setValue(ior_value) + ior_spinbox.valueChanged.connect(lambda v, mat=material: self._updateMaterialIOR(mat, v)) + material_layout.addWidget(ior_spinbox, current_row, 1, 1, 1) + except (TypeError, ValueError) as e: + print(f"折射率值无效: {material.refractive_index}, 错误: {e}") + material_layout.addWidget(QLabel("折射率:"), current_row, 0) + no_ior_label = QLabel("折射率值无效") + no_ior_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") + material_layout.addWidget(no_ior_label, current_row, 1, 1, 1) + else: + material_layout.addWidget(QLabel("折射率:"), current_row, 0) + no_ior_label = QLabel("此材质不支持折射率编辑") + no_ior_label.setStyleSheet("color:#888;font-style:italic;font-size:10px;") + material_layout.addWidget(no_ior_label, current_row, 1, 1, 1) + + current_row += 1 + + # 纹理贴图标题 + texture_title = QLabel("纹理贴图") + texture_title.setStyleSheet("font-weight:bold;") + material_layout.addWidget(texture_title, current_row, 0, 1, 4) + current_row += 1 + + # 纹理按钮 - 两列布局 + diffuse_button = QPushButton("选择漫反射贴图") + diffuse_button.clicked.connect(lambda checked, title=unique_name: self._selectDiffuseTexture(title)) + material_layout.addWidget(diffuse_button, current_row, 0, 1, 2) + + normal_button = QPushButton("选择法线贴图") + normal_button.clicked.connect(lambda checked, mat=material: self._selectNormalTexture(mat)) + material_layout.addWidget(normal_button, current_row, 2, 1, 2) + current_row += 1 + + roughness_button = QPushButton("选择粗糙度贴图") + roughness_button.clicked.connect(lambda checked, mat=material: self._selectRoughnessTexture(mat)) + material_layout.addWidget(roughness_button, current_row, 0, 1, 2) + + metallic_button = QPushButton("选择金属性贴图") + metallic_button.clicked.connect(lambda checked, mat=material: self._selectMetallicTexture(mat)) + material_layout.addWidget(metallic_button, current_row, 2, 1, 2) + + # #IOR贴图 + # ior_button = QPushButton("选择IOR贴图") + # ior_button.clicked.connect(lambda checked,mat = material:self._selectIORTexture(mat)) + # self._propertyLayout.addRow("IOR贴图",ior_button) + + # # 视差贴图 + # parallax_button = QPushButton("选择视差贴图") + # parallax_button.clicked.connect(lambda checked, mat=material: self._selectParallaxTexture(mat)) + # self._propertyLayout.addRow("视差贴图:", parallax_button) + # + # # 自发光贴图 + # emission_button = QPushButton("选择自发光贴图") + # emission_button.clicked.connect(lambda checked, mat=material: self._selectEmissionTexture(mat)) + # self._propertyLayout.addRow("自发光贴图:", emission_button) + # + # # 环境光遮蔽贴图 + # ao_button = QPushButton("选择AO贴图") + # ao_button.clicked.connect(lambda checked, mat=material: self._selectAOTexture(mat)) + # self._propertyLayout.addRow("AO贴图:", ao_button) + + # # 透明度贴图 + # alpha_button = QPushButton("选择透明度贴图") + # alpha_button.clicked.connect(lambda checked, mat=material: self._selectAlphaTexture(mat)) + # self._propertyLayout.addRow("透明度贴图:", alpha_button) + # + # # 细节贴图 + # detail_button = QPushButton("选择细节贴图") + # detail_button.clicked.connect(lambda checked, mat=material: self._selectDetailTexture(mat)) + # self._propertyLayout.addRow("细节贴图:", detail_button) + # + # # 光泽贴图 + # gloss_button = QPushButton("选择光泽贴图") + # gloss_button.clicked.connect(lambda checked, mat=material: self._selectGlossTexture(mat)) + # self._propertyLayout.addRow("光泽贴图:", gloss_button) + + # 在纹理按钮后添加当前贴图信息显示 + current_row = self._displayCurrentTextures(material, material_layout, current_row) + + current_row = self._addShadingModelPanel(material, material_layout, current_row) + current_row = self._addEmissionPanel(material, material_layout, current_row) + current_row = self._addMaterialPresetPanel(material, material_layout, current_row) + + material_group.setLayout(material_layout) + self._propertyLayout.addWidget(material_group) + + def _updateMaterialBaseColor(self, material, component, value): + """更新材质基础颜色(智能版本)""" + try: + from panda3d.core import Vec4 + + # 获取当前颜色 + current_color = self._getOrCreateMaterialBaseColor(material) + if current_color is None: + print(f"无法获取材质基础颜色,跳过更新") + return + + # 计算新颜色 + if component == 'r': + new_color = Vec4(value, current_color.y, current_color.z, current_color.w) + elif component == 'g': + new_color = Vec4(current_color.x, value, current_color.z, current_color.w) + elif component == 'b': + new_color = Vec4(current_color.x, current_color.y, value, current_color.w) + # elif component == 'a': # Alpha分量处理 + # self._updateMaterialTransparency(material, value) + # return + # new_color = Vec4(current_color.x, current_color.y, current_color.z, value) + else: + print(f"未知的颜色分量: {component}") + return + + # 尝试多种方式设置颜色 + success = False + + # 方法1: 使用set_base_color + if hasattr(material, 'set_base_color'): + try: + material.set_base_color(new_color) + print(f"✓ 通过set_base_color更新: {component}={value}") + success = True + except Exception as e: + print(f"set_base_color失败: {e}") + + # 方法2: 使用setDiffuse作为备选 + if not success and hasattr(material, 'setDiffuse'): + try: + material.setDiffuse(new_color) + print(f"✓ 通过setDiffuse更新: {component}={value}") + success = True + except Exception as e: + print(f"setDiffuse失败: {e}") + + # 方法3: 直接设置属性 + if not success and hasattr(material, 'base_color'): + try: + material.base_color = new_color + print(f"✓ 通过直接属性设置更新: {component}={value}") + success = True + except Exception as e: + print(f"直接属性设置失败: {e}") + + if success: + print(f"材质基础颜色已更新: {new_color}") + else: + print(f"✗ 所有更新方法都失败了") + + except Exception as e: + print(f"更新材质基础颜色失败: {e}") + + def _updateMaterialTransparency(self, material, alpha_value): + try: + from panda3d.core import Vec4 + if hasattr(material, 'emission'): + material.emission = Vec4(3, 0, 0, 0) + print("设置透明着色器模型") + if hasattr(material, 'shading_model_param0'): + material.shading_model_param0 = alpha_value + print(f"设置透明度参数{alpha_value}") + if hasattr(material, 'base_color'): + current_color = material.base_color + material.base_color = Vec4(current_color.x, current_color.y, current_color.z, alpha_value) + print(f"更新基础颜色透明度{alpha_value}") + print(f"材质透明度已更新:{alpha_value}") + except Exception as e: + print(f"更新材质透明度失败: {e}") + + def _updateMaterialRoughness(self, material, value): + """更新材质粗糙度(安全版本)""" + try: + if not hasattr(material, 'roughness') or material.roughness is None: + print(f"材质不支持粗糙度属性或值为None,跳过更新") + return + material.set_roughness(value) + except Exception as e: + print(f"更新材质粗糙度失败: {e}") + + def _updateMaterialMetallic(self, material, value): + """更新材质金属性(安全版本)""" + try: + if not hasattr(material, 'metallic') or material.metallic is None: + print(f"材质不支持金属性属性或值为None,跳过更新") + return + material.set_metallic(value) + except Exception as e: + print(f"更新材质金属性失败: {e}") + + def _updateMaterialIOR(self, material, value): + """更新材质折射率(安全版本)""" + try: + if not hasattr(material, 'refractive_index') or material.refractive_index is None: + print(f"材质不支持折射率属性或值为None,跳过更新") + return + material.set_refractive_index(value) + except Exception as e: + print(f"更新材质折射率失败: {e}") + + def _getMaterialStatus(self, material): + """获取材质状态描述""" + try: + # 检查材质的各种属性 + has_base_color = hasattr(material, 'has_base_color') and material.has_base_color() + has_roughness = hasattr(material, 'has_roughness') and material.has_roughness() + has_metallic = hasattr(material, 'has_metallic') and material.has_metallic() + has_ior = hasattr(material, 'has_refractive_index') and material.has_refractive_index() + + # 检查基本属性是否存在 + has_base_color_attr = hasattr(material, 'base_color') + has_roughness_attr = hasattr(material, 'roughness') + has_metallic_attr = hasattr(material, 'metallic') + has_ior_attr = hasattr(material, 'refractive_index') + + if has_base_color and has_roughness and has_metallic and has_ior: + return "标准PBR材质" + elif has_base_color_attr and has_roughness_attr and has_metallic_attr: + return "PBR材质(部分属性可用)" + elif has_base_color_attr or has_roughness_attr or has_metallic_attr: + return "基础材质(支持部分PBR属性)" + else: + return "传统材质(可转换为PBR)" + + except Exception as e: + print(f"检查材质状态时出错: {e}") + return "未知材质类型(可尝试编辑)" + + def _getTextureModeString(self, mode): + """获取纹理模式的字符串表示""" + from panda3d.core import TextureStage + mode_map = { + TextureStage.MModulate: "Modulate", + TextureStage.MDecal: "Decal", + TextureStage.MBlend: "Blend", + TextureStage.MReplace: "Replace", + TextureStage.MAdd: "Add", + TextureStage.MCombine: "Combine", + TextureStage.MBlendColorScale: "BlendColorScale", + TextureStage.MModulateGlow: "ModulateGlow", + TextureStage.MModulateGloss: "ModulateGloss", + TextureStage.MNormal: "Normal", + TextureStage.MNormalHeight: "NormalHeight", + TextureStage.MGlow: "Glow", + TextureStage.MGloss: "Gloss", + TextureStage.MHeight: "Height", + TextureStage.MSelector: "Selector", + TextureStage.MNormalGloss: "NormalGloss" + } + return mode_map.get(mode, f"Unknown({mode})") + + def _checkAndAdjustMaterialProperty(self, material, property_name, current_value, texture_type): + """检查并智能调整材质属性值""" + if current_value <= 0.01: + print(f"⚠️ 警告:材质{property_name}过低({current_value}),{texture_type}贴图可能无效果") + print(f" RenderPipeline使用公式: 最终{property_name} = 材质{property_name} × 贴图值") + print(f" 当前设置下,即使贴图为白色(1.0),最终效果也只有{current_value}") + + # 询问用户是否要自动调整(在实际应用中,这里可以弹出对话框) + # 目前我们采用保守的自动调整策略 + recommended_value = 0.8 # 推荐值 + + if property_name == "粗糙度": + material.set_roughness(recommended_value) + elif property_name == "金属性": + material.set_metallic(recommended_value) + + print(f"✓ 已自动调整材质{property_name}为 {recommended_value}") + print(f" 现在{texture_type}贴图的效果范围:白色区域={recommended_value},黑色区域=0.0") + return recommended_value + else: + print(f"✓ 材质{property_name}合适: {current_value}") + print(f" {texture_type}贴图效果范围:白色区域={current_value:.2f},黑色区域=0.0") + return current_value + + def _getOrCreateMaterialBaseColor(self, material): + """智能获取或创建材质的基础颜色""" + from panda3d.core import Vec4 + + try: + # 方法1: 尝试获取base_color属性 + if hasattr(material, 'base_color') and material.base_color is not None: + print(f"✓ 找到base_color属性: {material.base_color}") + return material.base_color + + # 方法2: 尝试调用get_base_color方法 + if hasattr(material, 'get_base_color'): + try: + base_color = material.get_base_color() + if base_color is not None: + # print(f"✓ 通过get_base_color()获取: {base_color}") + return base_color + except: + pass + + # 方法3: 尝试从diffuse颜色获取 + if hasattr(material, 'getDiffuse'): + try: + diffuse_color = material.getDiffuse() + if diffuse_color is not None: + print(f"✓ 从diffuse颜色获取: {diffuse_color}") + # 同时设置为base_color + if hasattr(material, 'set_base_color'): + material.set_base_color(diffuse_color) + return diffuse_color + except: + pass + + # 方法4: 尝试从ambient颜色获取 + if hasattr(material, 'getAmbient'): + try: + ambient_color = material.getAmbient() + if ambient_color is not None: + print(f"✓ 从ambient颜色获取: {ambient_color}") + # 同时设置为base_color + if hasattr(material, 'set_base_color'): + material.set_base_color(ambient_color) + return ambient_color + except: + pass + + # 方法5: 创建默认的基础颜色 + print("⚠️ 未找到现有颜色,创建默认基础颜色") + default_color = Vec4(0.8, 0.8, 0.8, 1.0) # 默认灰白色 + + # 尝试设置到材质 + if hasattr(material, 'set_base_color'): + material.set_base_color(default_color) + print(f"✓ 设置默认base_color: {default_color}") + elif hasattr(material, 'setDiffuse'): + material.setDiffuse(default_color) + print(f"✓ 设置默认diffuse: {default_color}") + + return default_color + + except Exception as e: + print(f"✗ 获取材质基础颜色失败: {e}") + return None + + def _selectDiffuseTexture(self, material_title): + """漫反射贴图""" + from PyQt5.QtWidgets import QFileDialog + import os + + file_dialog = QFileDialog(None, "选择漫反射贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") + + if file_dialog.exec_(): + filename = file_dialog.selectedFiles()[0] + if filename: + # 使用跨平台路径标准化 + normalized_path = util.normalize_model_path(filename) + self._applyDiffuseTexture(material_title, normalized_path) + print(f"已选择漫反射贴图:{filename} -> 标准化路径:{normalized_path}") + + def _selectNormalTexture(self, material): + """选择法线贴图""" + from PyQt5.QtWidgets import QFileDialog + + file_dialog = QFileDialog(None, "选择法线贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") + + if file_dialog.exec_(): + filename = file_dialog.selectedFiles()[0] + if filename: + # 使用跨平台路径标准化 + normalized_path = util.normalize_model_path(filename) + self._applyNormalTexture(material, normalized_path) + print(f"已选择法线贴图:{filename} -> 标准化路径:{normalized_path}") + + def _selectRoughnessTexture(self, material): + """选择粗糙度贴图""" + from PyQt5.QtWidgets import QFileDialog + + file_dialog = QFileDialog(None, "选择粗糙度贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") + + if file_dialog.exec_(): + filename = file_dialog.selectedFiles()[0] + if filename: + # 使用跨平台路径标准化 + normalized_path = util.normalize_model_path(filename) + self._applyRoughnessTexture_FINAL(material, normalized_path) + print(f"已选择粗糙度贴图: {filename} -> 标准化路径:{normalized_path}") + + def _selectMetallicTexture(self, material): + """选择金属性贴图""" + from PyQt5.QtWidgets import QFileDialog + + file_dialog = QFileDialog(None, "选择金属性贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") + + if file_dialog.exec_(): + filename = file_dialog.selectedFiles()[0] + if filename: + # 使用跨平台路径标准化 + normalized_path = util.normalize_model_path(filename) + self._applyMetallicTexture_NEW(material, normalized_path) + print(f"已选择金属性贴图: {filename} -> 标准化路径:{normalized_path}") + + # IOR贴图 + def _selectIORTexture(self, material): + """选择IOR贴图""" + from PyQt5.QtWidgets import QFileDialog + + file_dialong = QFileDialog(None, "选择IOR贴图", "", "图像(*.png *.jpg *.jpeg *.tga *.bmp)") + + if file_dialong.exec_(): + filename = file_dialong.selectedFiles()[0] + if filename: + # 使用跨平台路径标准化 + normalized_path = util.normalize_model_path(filename) + self._applyIORTexture(material, normalized_path) + print(f"已选择IOR贴图:{filename} -> 标准化路径:{normalized_path}") + + def _selectParallaxTexture(self, material): + """选择视差贴图""" + from PyQt5.QtWidgets import QFileDialog + + file_dialog = QFileDialog(None, "选择视差贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") + + if file_dialog.exec_(): + filename = file_dialog.selectedFiles()[0] + if filename: + # 使用跨平台路径标准化 + normalized_path = util.normalize_model_path(filename) + self._applyParallaxTexture(material, normalized_path) + print(f"已选择视差贴图:{filename} -> 标准化路径:{normalized_path}") + + def _selectEmissionTexture(self, material): + """选择自发光贴图""" + from PyQt5.QtWidgets import QFileDialog + + file_dialog = QFileDialog(None, "选择自发光贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") + + if file_dialog.exec_(): + filename = file_dialog.selectedFiles()[0] + if filename: + # 使用跨平台路径标准化 + normalized_path = util.normalize_model_path(filename) + self._applyEmissionTexture(material, normalized_path) + print(f"已选择自发光贴图:{filename} -> 标准化路径:{normalized_path}") + + def _selectAOTexture(self, material): + """选择环境光遮蔽贴图""" + from PyQt5.QtWidgets import QFileDialog + + file_dialog = QFileDialog(None, "选择AO贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") + + if file_dialog.exec_(): + filename = file_dialog.selectedFiles()[0] + if filename: + # 使用跨平台路径标准化 + normalized_path = util.normalize_model_path(filename) + self._applyAOTexture(material, normalized_path) + print(f"已选择AO贴图:{filename} -> 标准化路径:{normalized_path}") + + def _selectAlphaTexture(self, material): + """选择透明度贴图""" + from PyQt5.QtWidgets import QFileDialog + + file_dialog = QFileDialog(None, "选择透明度贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") + + if file_dialog.exec_(): + filename = file_dialog.selectedFiles()[0] + if filename: + # 使用跨平台路径标准化 + normalized_path = util.normalize_model_path(filename) + self._applyAlphaTexture(material, normalized_path) + print(f"已选择透明度贴图:{filename} -> 标准化路径:{normalized_path}") + + def _selectDetailTexture(self, material): + """选择细节贴图""" + from PyQt5.QtWidgets import QFileDialog + + file_dialog = QFileDialog(None, "选择细节贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") + + if file_dialog.exec_(): + filename = file_dialog.selectedFiles()[0] + if filename: + # 使用跨平台路径标准化 + normalized_path = util.normalize_model_path(filename) + self._applyDetailTexture(material, normalized_path) + print(f"已选择细节贴图:{filename} -> 标准化路径:{normalized_path}") + + def _selectGlossTexture(self, material): + """选择光泽贴图""" + from PyQt5.QtWidgets import QFileDialog + + file_dialog = QFileDialog(None, "选择光泽贴图", "", "图像文件(*.png *.jpg *.jpeg *.tga *.bmp)") + + if file_dialog.exec_(): + filename = file_dialog.selectedFiles()[0] + if filename: + # 使用跨平台路径标准化 + normalized_path = util.normalize_model_path(filename) + self._applyGlossTexture(material, normalized_path) + print(f"已选择光泽贴图:{filename} -> 标准化路径:{normalized_path}") + + # def _applyDiffuseTexture(self, texture_path): + # from panda3d.core import TextureStage + # try: + # from RenderPipelineFile.rpcore.loader import RPLoader + # texture = RPLoader.load_texture(texture_path) + # if not texture: + # print("纹理加载失败") + # return + # + # node = self.world.selected_np + # if not node: + # print("未选中节点") + # return + # + # # 1. 直接给节点挂贴图 + # diffuse_stage = TextureStage("diffuse") + # diffuse_stage.setSort(0) + # node.setTexture(diffuse_stage, texture) + # + # # 2. 再给它刷一个 RenderPipeline 效果 + # effect_file = "effects/default.yaml" + # self.world.render_pipeline.set_effect( + # node, + # effect_file, + # {"diffuse_texture": texture}, + # 100 + # ) + # print("贴图已直接贴到节点:", node.getName()) + # except Exception as e: + # print("贴图失败:", e) + def _applyDiffuseTexture(self, material_title, texture_path): + """应用漫反射贴图""" + try: + from RenderPipelineFile.rpcore.loader import RPLoader + from panda3d.core import TextureStage, TransparencyAttrib + + # 加载纹理 + texture = RPLoader.load_texture(texture_path) + if texture: + # 获取材质所属的节点 + material, node = self._findMaterialAndNodeByTitle(material_title) + if node and material: + print(f"正在为节点 {node.getName()} 应用漫反射贴图") + + # 检查纹理是否包含透明度信息 + has_alpha = False + # 检查纹理格式 + format_name = str(texture.getFormat()) + if 'alpha' in format_name.lower() or 'rgba' in format_name.lower(): + has_alpha = True + + print(f"纹理格式: {texture.getFormat()}, 包含透明通道: {has_alpha}") + + # 检查是否有金属性贴图,选择合适的PBR效果 + print("🔧 检查金属性贴图并选择合适的PBR效果...") + has_metallic = self._hasMetallicTexture(node) + needs_alpha = self._needsAlphaTesting(node) or has_alpha + + if has_metallic: + print("✅ 检测到金属性贴图,使用支持金属性的PBR效果") + effect_file = "effects/pbr_with_metallic.yaml" + else: + print("✅ 没有金属性贴图,使用默认PBR效果") + effect_file = "effects/default.yaml" + + print(f"🔍 透明度测试: {'需要' if needs_alpha else '不需要'}") + + try: + self.world.render_pipeline.set_effect( + node, + effect_file, + { + "normal_mapping": True, # 启用法线映射支持 + "render_gbuffer": True, + "alpha_testing": needs_alpha, # 根据是否需要透明度决定 + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 100 + ) + print(f"✅ {effect_file} 效果已应用(支持透明度和金属性)") + except Exception as e: + print(f"⚠️ PBR效果应用失败: {e}") + + # 根据RenderPipeline的gbuffer.frag.glsl模板: + # p3d_Texture0 用于漫反射贴图 (line 111: texture(p3d_Texture0, texcoord).xyz) + + # 清理可能存在的漫反射贴图 + existing_stages = node.findAllTextureStages() + for stage in existing_stages: + if stage.getSort() == 0 or "diffuse" in stage.getName().lower(): + node.clearTexture(stage) + print(f"清理了现有的漫反射贴图阶段: {stage.getName()}") + + # 创建漫反射贴图纹理阶段,对应p3d_Texture0 + diffuse_stage = TextureStage("diffuse") + diffuse_stage.setSort(0) # 对应p3d_Texture0 + diffuse_stage.setMode(TextureStage.MModulate) # 标准的调制模式 + + # 应用漫反射贴图 + node.setTexture(diffuse_stage, texture) + print("漫反射贴图已应用到p3d_Texture0槽") + + # 如果纹理包含透明度,启用透明度渲染 + if has_alpha: + print("检测到透明纹理,启用透明度渲染") + node.setTransparency(TransparencyAttrib.MAlpha) + node.setDepthWrite(False) # 透明物体通常不写入深度缓冲区 + else: + # 确保关闭透明度(避免之前设置的影响) + node.setTransparency(TransparencyAttrib.MNone) + + # 调试信息:显示当前纹理阶段 + print("=== 漫反射贴图应用后的纹理阶段信息 ===") + all_stages = node.findAllTextureStages() + for i, stage in enumerate(all_stages): + tex = node.getTexture(stage) + mode_name = self._getTextureModeString(stage.getMode()) + print( + f"阶段 {i}: {stage.getName()}, Sort: {stage.getSort()}, 模式: {mode_name}, 纹理: {tex.getName() if tex else 'None'}") + print("==========================================") + + print(f"漫反射贴图已成功应用:{texture_path}") + else: + print(f"未找到材质标题对应的材质或节点: {material_title}") + else: + print("纹理加载失败") + except Exception as e: + print(f"应用漫反射贴图失败{e}") + import traceback + traceback.print_exc() + + def _applyNormalTexture(self, material, texture_path): + """应用法线贴图 - Blender风格效果""" + try: + from RenderPipelineFile.rpcore.loader import RPLoader + from panda3d.core import TextureStage, Vec4 + + texture = RPLoader.load_texture(texture_path) + if not texture: + print("❌ 纹理加载失败") + return + + # 查找使用该材质的具体几何节点 + node = self._findSpecificGeomNodeForMaterial(material) + if not node: + print("❌ 未找到材质对应的节点") + return + + # 清理现有的法线贴图,避免冲突 + print("🧹 清理现有纹理阶段...") + existing_stages = node.findAllTextureStages() + has_diffuse_texture = False + + for stage in existing_stages: + if "normal" in stage.getName().lower() or stage.getSort() == 1: + node.clearTexture(stage) + print(f" ✅ 已清理法线纹理阶段: {stage.getName()}") + elif stage.getSort() == 0: # 检查是否有漫反射贴图 + tex = node.getTexture(stage) + if tex: + has_diffuse_texture = True + print(f" 🔍 发现现有漫反射贴图: {tex.getName()}") + + + # 如果没有漫反射贴图,必须创建白色纹理,否则法线映射会失效 + if not has_diffuse_texture: + print("⚠️ 没有漫反射贴图,创建白色纹理确保法线映射正常工作...") + self._createWhiteDiffuseTexture(node) + + # 检查是否有金属性贴图,选择合适的PBR效果 + print("🔧 检查金属性贴图并选择合适的PBR效果...") + has_metallic = self._hasMetallicTexture(node) + needs_alpha = self._needsAlphaTesting(node) + + if has_metallic: + print("✅ 检测到金属性贴图,使用支持金属性的PBR效果") + effect_file = "effects/pbr_with_metallic.yaml" + else: + print("✅ 没有金属性贴图,使用默认PBR效果") + effect_file = "effects/default.yaml" + + print(f"🔍 透明度测试: {'需要' if needs_alpha else '不需要'}") + + try: + self.world.render_pipeline.set_effect( + node, + effect_file, + { + "normal_mapping": True, # 强制启用法线映射 + "render_gbuffer": True, + "alpha_testing": needs_alpha, # 根据是否需要透明度决定 + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 100 + ) + print(f"✅ {effect_file} 效果已应用(法线映射已启用)") + except Exception as e: + print(f"⚠️ PBR效果应用失败: {e}") + + # 这里不需要检查roughness_stage,因为这是法线贴图方法 + print("🔍 继续法线贴图处理...") + + # 创建法线贴图纹理阶段,对应p3d_Texture1 + print("🎨 创建法线纹理阶段...") + normal_stage = TextureStage("normal_map") + normal_stage.setSort(1) # 对应shader中的p3d_Texture1 + normal_stage.setMode(TextureStage.MModulate) # 使用标准模式,不是MNormal + normal_stage.setTexcoordName("texcoord") + + print(f"📋 法线纹理阶段信息:") + print(f" • 名称: {normal_stage.getName()}") + print(f" • 排序: {normal_stage.getSort()} (对应p3d_Texture1)") + print(f" • 模式: {normal_stage.getMode()} (MModulate)") + + # 应用纹理到正确的纹理槽 + node.setTexture(normal_stage, texture) + print(f"🔗 法线纹理已绑定到p3d_Texture1槽") + + # 设置材质的normalfactor参数(用于法线强度) + current_emission = material.emission + if current_emission is None: + current_emission = Vec4(0, 0, 0, 0) + print("材质emission为None,使用默认值") + + # emission.y 用于存储 normalfactor + new_emission = Vec4(current_emission.x, 1.0, current_emission.z, current_emission.w) + material.set_emission(new_emission) + print(f"🔧 设置法线强度参数: normalfactor = {new_emission.y}") + + # 验证纹理应用 + applied_texture = node.getTexture(normal_stage) + if applied_texture: + print(f"✅ 法线贴图成功应用到p3d_Texture1槽") + print(f"📊 纹理信息:") + print(f" • 纹理名称: {applied_texture.getName()}") + print(f" • 纹理尺寸: {applied_texture.getXSize()}x{applied_texture.getYSize()}") + print(f"📊 Blender风格效果:") + print(f" • 法线贴图将影响表面细节和光照") + print(f" • 不会改变材质颜色,只影响表面法线") + else: + print("❌ 纹理应用验证失败") + + except Exception as e: + print(f"❌ 应用法线贴图失败: {e}") + import traceback + traceback.print_exc() + + def _applyRoughnessTexture_FINAL(self, material, texture_path): + """应用粗糙度贴图 - 先编译后绑定策略""" + try: + from RenderPipelineFile.rpcore.loader import RPLoader + from panda3d.core import TextureStage + import time + + print(f"🎨 应用粗糙度贴图(先编译后绑定): {texture_path}") + + # 1. 加载纹理 + texture = RPLoader.load_texture(texture_path) + if not texture: + print("❌ 纹理加载失败") + return + + # 2. 找到节点 + node = self._findSpecificGeomNodeForMaterial(material) + if not node: + print("❌ 未找到材质对应的节点") + return + + print(f"🎯 目标节点: {node.getName()}") + + # 3. 设置材质粗糙度为1.0 + material.set_roughness(1.0) + print("🔧 材质粗糙度设置为1.0") + + # 4. 检查该材质有没有应用法线贴图,如果没有就先添加一个默认的法线贴图 + print("🔧 步骤1:检查法线贴图...") + has_normal = self._hasNormalTexture(node) + + if not has_normal: + print("⚠️ 检测到材质没有法线贴图,先添加默认法线贴图...") + # self._applyDefaultNormalTexture(node) + self._applyNormalTexture(material, "RenderPipelineFile/Default_NRM_2K.png") + print("✅ 默认法线贴图已添加") + else: + print("✅ 检测到材质已有法线贴图") + + # 5. 检查是否有金属性贴图和透明漫反射贴图,选择合适的PBR效果 + print("🔧 步骤2:检查金属性贴图和透明度设置...") + has_metallic = self._hasMetallicTexture(node) + needs_alpha = self._needsAlphaTesting(node) + + if has_metallic: + print("✅ 检测到金属性贴图,使用支持金属性的PBR效果") + effect_file = "effects/pbr_with_metallic.yaml" + else: + print("✅ 没有金属性贴图,使用默认PBR效果") + effect_file = "effects/default.yaml" + + print(f"🔍 透明度测试: {'需要' if needs_alpha else '不需要'}") + + self.world.render_pipeline.set_effect( + node, + effect_file, + { + "normal_mapping": True, # 始终启用法线映射 + "render_gbuffer": True, + "alpha_testing": needs_alpha, # 根据是否需要透明度决定 + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 100 + ) + print(f"✅ {effect_file} 效果已应用") + # print("✅ 着色器预编译完成") + + # 5. 等待编译完成 + # time.sleep(0.2) # 200ms等待 + # print("⏱️ 等待着色器编译...") + + # 6. 现在绑定纹理到已编译的着色器 + # print("🔧 步骤2:绑定纹理到编译完成的着色器...") + # roughness_stage = TextureStage("roughness_map") + # roughness_stage.setSort(3) # p3d_Texture3 + # roughness_stage.setMode(TextureStage.MModulate) + # node.setTexture(roughness_stage, texture) + # print("✅ 纹理已绑定到预编译着色器") + + print("🧹 清理现有粗糙度贴图...") + existing_stages = node.findAllTextureStages() + for stage in existing_stages: + if stage.getSort() == 3: # p3d_Texture3槽 + node.clearTexture(stage) + print(f" ✅ 已清理现有粗糙度贴图: {stage.getName()}") + + # 添加验证和重试 + for attempt in range(2): # 最多重试3次 + time.sleep(0.1) # 短暂等待 + roughness_stage = TextureStage("roughness_map") + roughness_stage.setSort(3) # p3d_Texture3 + roughness_stage.setMode(TextureStage.MModulate) + node.setTexture(roughness_stage, texture) + + # 7. 验证效果 + applied_texture = node.getTexture(roughness_stage) + if applied_texture: + print(f"✅ 粗糙度贴图应用成功(先编译后绑定)") + print(f" 纹理: {applied_texture.getName()}") + print("🔍 应该立即看到正确的粗糙度效果") + else: + print("❌ 纹理绑定验证失败") + + except Exception as e: + print(f"❌ 应用粗糙度贴图失败: {e}") + import traceback + traceback.print_exc() + + def _hasNormalTexture(self, node): + """检查节点是否有法线贴图""" + try: + all_stages = node.findAllTextureStages() + for stage in all_stages: + if stage.getSort() == 1: # p3d_Texture1 是法线贴图槽 + tex = node.getTexture(stage) + if tex: + print(f"🔍 发现法线贴图: {tex.getName()}") + return True + return False + except Exception as e: + print(f"⚠️ 检查法线贴图失败: {e}") + return False + + def _applySmartPBREffect(self, node, effect_file="effects/default.yaml"): + """智能应用PBR效果,自动检测是否需要启用法线映射""" + try: + # 检查是否有法线贴图 + has_normal = self._hasNormalTexture(node) + + print(f"🔧 应用智能PBR效果 ({effect_file})...") + print(f" 法线映射: {'启用' if has_normal else '禁用'}") + + self.world.render_pipeline.set_effect( + node, + effect_file, + { + "normal_mapping": has_normal, # 根据是否有法线贴图决定 + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 100 + ) + print("✅ 智能PBR效果已应用") + return True + except Exception as e: + print(f"⚠️ 智能PBR效果应用失败: {e}") + return False + + def _verifyTextureInShader(self, node, texture_type, expected_sort): + """验证纹理是否在shader中正确处理""" + try: + print(f"🔍 验证{texture_type}纹理在shader中的处理...") + + # 检查当前效果 + current_effect = node.getEffect() + if current_effect: + print(f" 当前shader效果: {current_effect}") + else: + print(" ⚠️ 节点没有shader效果") + return False + + # 检查纹理槽 + stages = node.findAllTextureStages() + target_stage = None + for stage in stages: + if stage.getSort() == expected_sort: + target_stage = stage + break + + if target_stage: + tex = node.getTexture(target_stage) + if tex: + print(f" ✅ 找到{texture_type}纹理在sort={expected_sort}槽: {tex.getName()}") + + # 检查纹理数据 + if tex.hasRamImage(): + print(f" ✅ 纹理有RAM图像数据") + else: + print(f" ⚠️ 纹理没有RAM图像数据") + + return True + else: + print(f" ❌ sort={expected_sort}槽没有纹理") + else: + print(f" ❌ 没有找到sort={expected_sort}的纹理阶段") + + return False + + except Exception as e: + print(f" ❌ 验证失败: {e}") + return False + + def _ensureEnhancedPBREffect(self, node): + """确保节点使用增强的PBR效果,支持金属性纹理""" + try: + print("🔧 应用金属性贴图支持的PBR效果...") + self.world.render_pipeline.set_effect( + node, + "effects/pbr_with_metallic.yaml", + { + "normal_mapping": False, # 关闭法线贴图避免干扰 + "render_gbuffer": True, # 必须启用gbuffer渲染 + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 100 # 高优先级确保应用 + ) + print("✅ 金属性贴图PBR效果已应用") + return True + except Exception as e: + print(f"⚠️ 金属性贴图PBR效果失败: {e}") + # 回退到默认效果 + try: + self.world.render_pipeline.set_effect( + node, + "effects/default.yaml", + { + "normal_mapping": False, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 100 + ) + print("✅ 已回退到默认PBR效果(不支持金属性贴图)") + return True + except Exception as e2: + print(f"⚠️ 默认效果也失败: {e2}") + return False + + def _createWhiteDiffuseTexture(self, node): + """创建白色漫反射纹理,确保法线贴图能正常工作""" + try: + from RenderPipelineFile.rpcore.loader import RPLoader + from panda3d.core import TextureStage + + print("🎨 尝试加载白色纹理...") + + # 尝试加载一个白色纹理文件,如果不存在就创建 + white_texture = None + + # 方法1:尝试从RenderPipeline加载空白基础颜色纹理(应该是白色) + try: + white_texture = RPLoader.load_texture("RenderPipelineFile/data/empty_textures/empty_basecolor.png") + if white_texture: + print("✅ 从RenderPipeline空白纹理加载白色纹理") + except Exception as e1: + print(f"⚠️ 加载空白纹理失败: {e1}") + pass + + # 方法2:如果没有默认纹理,创建简单的白色纹理 + if not white_texture: + try: + from panda3d.core import Texture + white_texture = Texture("white_diffuse") + white_texture.setup2dTexture(1, 1, Texture.TUnsignedByte, Texture.FRgb) + # 设置白色像素数据 + white_data = b'\xff\xff\xff' + white_texture.setRamImage(white_data) + print("✅ 创建了程序生成的白色纹理") + except Exception as e2: + print(f"⚠️ 创建程序纹理也失败: {e2}") + return False + + if white_texture: + # 创建漫反射纹理阶段 + diffuse_stage = TextureStage("white_diffuse") + diffuse_stage.setSort(0) # 对应p3d_Texture0 + diffuse_stage.setMode(TextureStage.MModulate) + + # 应用白色纹理 + node.setTexture(diffuse_stage, white_texture) + print("✅ 白色漫反射纹理已应用,材质颜色将正常显示") + return True + + return False + + except Exception as e: + print(f"⚠️ 创建白色纹理失败: {e}") + return False + + def _applyMetallicTexture(self, material, texture_path): + """应用金属性贴图 - Blender风格效果""" + try: + from RenderPipelineFile.rpcore.loader import RPLoader + from panda3d.core import TextureStage + + print(f"🎨 应用金属性贴图: {texture_path}") + + texture = RPLoader.load_texture(texture_path) + if not texture: + print("❌ 纹理加载失败") + return + + # 查找使用该材质的具体几何节点 + node = self._findSpecificGeomNodeForMaterial(material) + if not node: + print("❌ 未找到材质对应的节点") + return + + print(f"🎯 目标节点: {node.getName()}") + + # 保持材质金属性为1.0,让贴图完全控制 + material.set_metallic(1.0) + print("🔧 材质金属性设置为1.0,启用贴图完全控制") + + # 清理现有的金属性贴图,避免冲突 + print("🧹 清理现有纹理阶段...") + existing_stages = node.findAllTextureStages() + print(f"🔍 发现 {len(existing_stages)} 个现有纹理阶段:") + + for stage in existing_stages: + tex = node.getTexture(stage) + tex_name = tex.getName() if tex else "无纹理" + print(f" - {stage.getName()} (sort={stage.getSort()}) -> {tex_name}") + + # 只清理金属性相关的纹理阶段 + if "metallic" in stage.getName().lower() or stage.getSort() == 5: + node.clearTexture(stage) + print(f" ✅ 已清理金属性纹理阶段: {stage.getName()}") + + # 确保没有纹理占用sort=5的槽位 + sort5_stages = [s for s in existing_stages if s.getSort() == 5] + if sort5_stages: + print(f"⚠️ 发现占用sort=5槽位的纹理: {[s.getName() for s in sort5_stages]}") + for stage in sort5_stages: + node.clearTexture(stage) + print(f" ✅ 已强制清理: {stage.getName()}") + + # 使用调试模式来验证金属性纹理是否正确读取 + debug_texture_mode = True # 设为True来调试纹理读取 + + print(f"🔧 应用{'调试' if debug_texture_mode else '增强'}PBR效果...") + + try: + if debug_texture_mode: + # 使用调试效果:将金属性纹理显示为颜色 + self.world.render_pipeline.set_effect( + node, + "effects/test_metallic_debug.yaml", + { + "normal_mapping": False, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 100 + ) + print("✅ 调试效果已应用 - 金属性纹理将显示为蓝黄色变化") + print(" 蓝色=非金属区域, 黄色=金属区域") + print(" 如果看到颜色变化,说明纹理读取正确") + print(" 如果看到统一颜色,说明纹理没有正确绑定") + else: + # 使用专门的金属性效果,避免与漫反射贴图混淆 + self.world.render_pipeline.set_effect( + node, + "effects/metallic_only.yaml", + { + "normal_mapping": False, # 关闭法线贴图避免干扰 + "render_gbuffer": True, # 必须启用gbuffer渲染 + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 100 # 高优先级确保应用 + ) + print("✅ 专门的金属性贴图效果已应用") + + # 验证效果是否正确应用 + current_effect = node.getEffect() + if current_effect: + print(f"🔍 当前效果: {current_effect}") + else: + print("⚠️ 效果应用后为空,可能有问题") + + except Exception as e: + print(f"⚠️ 专门的金属性效果失败: {e}") + print("🔄 回退到直接控制金属性效果...") + # 回退到直接控制效果 + try: + self._ensurePBREffectEnabledWithDirectMetallic(node) + print("✅ 直接控制金属性效果已应用") + except Exception as e2: + print(f"⚠️ 直接控制效果也失败: {e2}") + # 最后的备选方案:使用稳定效果 + try: + self._ensurePBREffectEnabledStable(node) + print("🔄 已应用稳定PBR效果") + except: + pass + + # 创建金属性贴图阶段,对应p3d_Texture5 + print("🎨 创建金属性纹理阶段...") + metallic_stage = TextureStage("metallic_map") + metallic_stage.setSort(5) # 对应shader中的p3d_Texture5 + + # 重要:设置正确的纹理模式 + # MModulate模式用于金属性贴图,不会影响颜色 + metallic_stage.setMode(TextureStage.MModulate) + + # 确保纹理坐标正确 + metallic_stage.setTexcoordName("texcoord") + + print(f"📋 金属性纹理阶段信息:") + print(f" • 名称: {metallic_stage.getName()}") + print(f" • 排序: {metallic_stage.getSort()} (对应p3d_Texture5)") + print(f" • 模式: {metallic_stage.getMode()} (MReplace)") + print(f" • 纹理坐标: {metallic_stage.getTexcoordName()}") + + # 应用纹理到正确的纹理槽 + node.setTexture(metallic_stage, texture) + print(f"🔗 金属性纹理已绑定到p3d_Texture5槽") + + # 强制刷新渲染状态 + node.setRenderModeWireframe() + node.clearRenderMode() + print(f"🔄 已刷新渲染状态") + + # 验证纹理应用 + applied_texture = node.getTexture(metallic_stage) + if applied_texture: + print(f"✅ 金属性贴图成功应用到p3d_Texture5槽") + print(f"📊 纹理信息:") + print(f" • 纹理名称: {applied_texture.getName()}") + print(f" • 纹理尺寸: {applied_texture.getXSize()}x{applied_texture.getYSize()}") + print(f"📊 Blender风格效果:") + print(f" • 白色区域 = 完全金属 (1.0)") + print(f" • 黑色区域 = 非金属 (0.0)") + print(f" • 灰色区域 = 部分金属 (0.5)") + print(f" • 公式: 最终金属性 = 贴图值") + + # 列出节点上的所有纹理阶段 + print(f"🔍 节点上的所有纹理阶段:") + all_stages = node.findAllTextureStages() + for i, stage in enumerate(all_stages): + tex = node.getTexture(stage) + tex_name = tex.getName() if tex else "无纹理" + print(f" {i}: {stage.getName()} (sort={stage.getSort()}) -> {tex_name}") + + else: + print("❌ 纹理应用验证失败") + print("🔍 尝试诊断问题...") + + # 检查纹理是否有效 + if texture: + print(f" 纹理对象有效: {texture.getName()}") + else: + print(" 纹理对象无效") + + # 检查节点是否有效 + if node: + print(f" 节点对象有效: {node.getName()}") + else: + print(" 节点对象无效") + + except Exception as e: + print(f"❌ 应用金属性贴图失败: {e}") + import traceback + traceback.print_exc() + + def _applyIORTexture(self, material, texture_path): + """应用IOR贴图到特定材质""" + try: + from RenderPipelineFile.rpcore.loader import RPLoader + from panda3d.core import TextureStage + + texture = RPLoader.load_texture(texture_path) + if texture: + # 查找使用该材质的具体几何节点 + node = self._findSpecificGeomNodeForMaterial(material) + if node: + print(f"正在为节点 {node.getName()} 应用IOR贴图") + + # 确保启用PBR效果 + self._ensurePBREffectEnabled(node) + + # 根据RenderPipeline的gbuffer.frag.glsl模板: + # p3d_Texture2 用于IOR贴图 (line 87: texture(p3d_Texture2, texcoord).x) + + # 清理现有的IOR贴图 + existing_stages = node.findAllTextureStages() + for stage in existing_stages: + if "ior" in stage.getName().lower() or stage.getSort() == 2: + node.clearTexture(stage) + print(f"清理了现有的IOR贴图阶段: {stage.getName()}") + + # 创建IOR贴图纹理阶段,对应p3d_Texture2 + ior_stage = TextureStage("ior") + ior_stage.setSort(2) # 对应p3d_Texture2 + ior_stage.setMode(TextureStage.MModulate) + + node.setTexture(ior_stage, texture) + print("IOR贴图已应用到p3d_Texture2槽") + + # 不再需要手动刷新渲染状态,避免闪烁 + print(f"IOR贴图已成功应用:{texture_path}") + else: + print("未找到材质对应的节点") + except Exception as e: + print(f"应用IOR贴图失败:{e}") + import traceback + traceback.print_exc() + + def _applyParallaxTexture(self, material, texture_path): + """应用视差贴图""" + try: + from RenderPipelineFile.rpcore.loader import RPLoader + from panda3d.core import TextureStage + + texture = RPLoader.load_texture(texture_path) + if texture: + node = self._findNodeWithMaterial(material) + if node: + print(f"正在为节点 {node.getName()} 应用视差贴图") + + # 确保启用PBR效果,包括视差映射 + self._ensurePBREffectEnabledWithParallax(node) + + # 根据RenderPipeline的gbuffer.frag.glsl模板: + # p3d_Texture4 用于视差贴图 (line 77: get_parallax_texcoord(p3d_Texture4, mInput.normalfactor)) + + # 清理现有的视差贴图 + existing_stages = node.findAllTextureStages() + for stage in existing_stages: + if "parallax" in stage.getName().lower() or stage.getSort() == 4: + node.clearTexture(stage) + print(f"清理了现有的视差贴图阶段: {stage.getName()}") + + # 创建视差贴图纹理阶段,对应p3d_Texture4 + parallax_stage = TextureStage("parallax") + parallax_stage.setSort(4) # 对应p3d_Texture4 + parallax_stage.setMode(TextureStage.MHeight) # 高度贴图模式 + + node.setTexture(parallax_stage, texture) + print("视差贴图已应用到p3d_Texture4槽") + + print(f"视差贴图已成功应用:{texture_path}") + else: + print("未找到材质对应节点") + except Exception as e: + print(f"应用视差贴图失败:{e}") + import traceback + traceback.print_exc() + + def _ensureNormalMappingEnabled(self, model): + """确保模型启用了法线映射功能""" + try: + self.world.render_pipeline.set_effect( + model, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": True + }, + 30 + ) + print(f"已为模型{model.getName()}启用法线映射") + except Exception as e: + print(f"设置法线映射效果失败:{e}") + + def _ensurePBREffectEnabled(self, model): + """确保模型启用了完整的PBR效果,包括法线映射""" + try: + self.world.render_pipeline.set_effect( + model, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": True, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 30 + ) + print(f"已为模型{model.getName()}启用PBR效果") + except Exception as e: + print(f"设置PBR效果失败:{e}") + + def _ensurePBREffectEnabledWithParallax(self, model): + """确保模型启用了完整的PBR效果,包括视差映射""" + try: + self.world.render_pipeline.set_effect( + model, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": True, + "parallax_mapping": True, # 启用视差映射 + "render_shadow": True, + "render_envmap": True + }, + 30 + ) + print(f"已为模型{model.getName()}启用PBR效果(包括视差映射)") + except Exception as e: + print(f"设置PBR效果失败:{e}") + + def _ensurePBREffectEnabledWithMetallic(self, model): + """确保模型启用了支持金属性贴图的PBR效果""" + try: + # 首先尝试使用自定义的金属性贴图效果 + try: + self.world.render_pipeline.set_effect( + model, + "effects/pbr_with_metallic.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": True, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 30 + ) + print(f"已为模型{model.getName()}启用支持金属性贴图的PBR效果") + except Exception as e1: + print(f"自定义金属性效果失败,使用标准PBR效果: {e1}") + # 回退到标准PBR效果 + self._ensurePBREffectEnabled(model) + except Exception as e: + print(f"设置PBR效果失败:{e}") + + def _ensurePBREffectEnabledWithDirectMetallic(self, model): + """确保模型启用了支持金属性贴图直接控制的PBR效果""" + try: + # 首先尝试使用直接控制金属性贴图的效果 + try: + self.world.render_pipeline.set_effect( + model, + "effects/pbr_direct_metallic.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": True, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 30 + ) + print(f"✅ 已为模型{model.getName()}启用直接控制金属性贴图的PBR效果") + except Exception as e1: + print(f"⚠️ 直接控制金属性效果失败,使用标准金属性效果: {e1}") + # 回退到标准金属性贴图效果 + self._ensurePBREffectEnabledWithMetallic(model) + except Exception as e: + print(f"设置PBR效果失败:{e}") + + def _onMetallicModeChanged(self, material, mode_index): + """处理金属性控制模式变化""" + try: + mode_names = ["乘法模式", "直接控制模式", "叠加模式"] + print(f"🔧 金属性控制模式已切换为: {mode_names[mode_index]}") + + # 查找使用该材质的节点 + node = self._findSpecificGeomNodeForMaterial(material) + if node: + if mode_index == 0: + # 乘法模式:使用标准金属性贴图效果 + self._ensurePBREffectEnabledWithMetallic(node) + print(" 📊 效果:最终金属性 = 材质金属性 × 贴图值") + print(" 💡 适用于:微调现有材质的金属性分布") + elif mode_index == 1: + # 直接控制模式:使用直接控制效果 + self._ensurePBREffectEnabledWithDirectMetallic(node) + print(" 📊 效果:最终金属性 = 贴图值") + print(" 💡 适用于:贴图完全控制金属性分布") + elif mode_index == 2: + # 叠加模式:使用叠加效果 + self._ensurePBREffectEnabledWithAdditiveMetallic(node) + print(" 📊 效果:最终金属性 = 材质金属性 + 贴图值 (限制在0-1)") + print(" 💡 适用于:在材质基础上增加金属性区域") + + print(f"✅ 金属性控制模式已应用到节点: {node.getName()}") + else: + print("⚠️ 未找到材质对应的节点") + + except Exception as e: + print(f"切换金属性控制模式失败: {e}") + + def _ensurePBREffectEnabledWithAdditiveMetallic(self, model): + """确保模型启用了支持金属性贴图叠加控制的PBR效果""" + try: + # 首先尝试使用叠加控制金属性贴图的效果 + try: + self.world.render_pipeline.set_effect( + model, + "effects/pbr_additive_metallic.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": True, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 30 + ) + print(f"✅ 已为模型{model.getName()}启用叠加控制金属性贴图的PBR效果") + except Exception as e1: + print(f"⚠️ 叠加控制金属性效果失败,使用直接控制效果: {e1}") + # 回退到直接控制金属性贴图效果 + self._ensurePBREffectEnabledWithDirectMetallic(model) + except Exception as e: + print(f"设置PBR效果失败:{e}") + + def _ensurePBREffectEnabledWithEmission(self, model): + """确保模型启用了支持自发光贴图的PBR效果""" + try: + self.world.render_pipeline.set_effect( + model, + "effects/pbr_with_emission.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": True, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 30 + ) + print(f"已为模型{model.getName()}启用支持自发光贴图的PBR效果") + except Exception as e: + print(f"自定义自发光效果失败,使用标准PBR效果: {e}") + # 回退到标准PBR效果 + self._ensurePBREffectEnabled(model) + + def _ensurePBREffectEnabledWithAlpha(self, model): + """确保模型启用了支持透明度的PBR效果""" + try: + self.world.render_pipeline.set_effect( + model, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": False, # 透明物体不渲染到GBuffer + "render_forward": True, # 使用前向渲染 + "alpha_testing": True, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 30 + ) + print(f"已为模型{model.getName()}启用支持透明度的PBR效果") + except Exception as e: + print(f"设置透明度PBR效果失败: {e}") + # 回退到标准PBR效果 + self._ensurePBREffectEnabled(model) + + def _ensurePBREffectEnabledWithRoughness(self, model): + """确保模型启用了支持粗糙度贴图的PBR效果""" + try: + # 首先尝试使用自定义的粗糙度贴图效果 + try: + self.world.render_pipeline.set_effect( + model, + "effects/pbr_with_roughness.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": True, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 30 + ) + print(f"已为模型{model.getName()}启用支持粗糙度贴图的PBR效果") + except Exception as e1: + print(f"自定义粗糙度效果失败,使用标准PBR效果: {e1}") + # 回退到标准PBR效果 + self._ensurePBREffectEnabled(model) + except Exception as e: + print(f"设置PBR效果失败:{e}") + + def _ensurePBREffectEnabledStable(self, model): + """确保模型启用了稳定的PBR效果,避免频繁切换导致闪烁""" + try: + # 检查是否已经有PBR效果 + current_effect = model.getEffect() + if current_effect: + print(f"🔍 当前效果: {current_effect}") + # 如果已经有PBR相关效果,就不要切换了 + effect_name = str(current_effect) + if "pbr" in effect_name.lower() or "default" in effect_name.lower(): + print("✅ 已有PBR效果,保持不变避免闪烁") + return + + # 使用最稳定的默认PBR效果 + print("🔧 应用稳定的默认PBR效果...") + self.world.render_pipeline.set_effect( + model, + "effects/default.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 50 # 较高优先级 + ) + print(f"✅ 稳定PBR效果已应用到模型: {model.getName()}") + + except Exception as e: + print(f"⚠️ 设置稳定PBR效果失败: {e}") + # 最后的备选方案:清除所有效果 + try: + model.clearEffect() + print("🔄 已清除所有效果,使用默认渲染") + except: + pass + + def _applyEmissionTexture(self, material, texture_path): + """应用自发光贴图""" + try: + from RenderPipelineFile.rpcore.loader import RPLoader + from panda3d.core import TextureStage + + texture = RPLoader.load_texture(texture_path) + if texture: + node = self._findNodeWithMaterial(material) + if node: + print(f"正在为节点 {node.getName()} 应用自发光贴图") + + # 启用自发光效果 + self._ensurePBREffectEnabledWithEmission(node) + + # 清理现有的自发光贴图 + existing_stages = node.findAllTextureStages() + for stage in existing_stages: + if "emission" in stage.getName().lower() or stage.getSort() == 6: + node.clearTexture(stage) + print(f"清理了现有的自发光贴图阶段: {stage.getName()}") + + # 创建自发光贴图纹理阶段,对应p3d_Texture6 + emission_stage = TextureStage("emission") + emission_stage.setSort(6) # 对应p3d_Texture6 + emission_stage.setMode(TextureStage.MModulate) + + node.setTexture(emission_stage, texture) + print("自发光贴图已应用到p3d_Texture6槽") + + # 设置材质为自发光着色模型 + from panda3d.core import Vec4 + current_emission = material.emission + if current_emission is None: + current_emission = Vec4(0, 0, 0, 0) + + # emission.x 用于存储着色模型,1表示自发光 + new_emission = Vec4(1.0, current_emission.y, current_emission.z, current_emission.w) + material.set_emission(new_emission) + print("材质着色模型已设置为自发光") + + print(f"自发光贴图已成功应用:{texture_path}") + else: + print("未找到材质对应的节点") + except Exception as e: + print(f"应用自发光贴图失败:{e}") + import traceback + traceback.print_exc() + + def _applyAOTexture(self, material, texture_path): + """应用环境光遮蔽贴图""" + try: + from RenderPipelineFile.rpcore.loader import RPLoader + from panda3d.core import TextureStage + + texture = RPLoader.load_texture(texture_path) + if texture: + node = self._findNodeWithMaterial(material) + if node: + print(f"正在为节点 {node.getName()} 应用AO贴图") + + # 确保启用PBR效果 + self._ensurePBREffectEnabled(node) + + # 清理现有的AO贴图 + existing_stages = node.findAllTextureStages() + for stage in existing_stages: + if "ao" in stage.getName().lower() or stage.getSort() == 7: + node.clearTexture(stage) + print(f"清理了现有的AO贴图阶段: {stage.getName()}") + + # 创建AO贴图纹理阶段,对应p3d_Texture7 + ao_stage = TextureStage("ao") + ao_stage.setSort(7) # 对应p3d_Texture7 + ao_stage.setMode(TextureStage.MModulate) + + node.setTexture(ao_stage, texture) + print("AO贴图已应用到p3d_Texture7槽") + print("注意:AO贴图需要自定义shader支持才能正确显示") + + print(f"AO贴图已成功应用:{texture_path}") + else: + print("未找到材质对应的节点") + except Exception as e: + print(f"应用AO贴图失败:{e}") + import traceback + traceback.print_exc() + + def _applyAlphaTexture(self, material, texture_path): + """应用透明度贴图""" + try: + from RenderPipelineFile.rpcore.loader import RPLoader + from panda3d.core import TextureStage + + texture = RPLoader.load_texture(texture_path) + if texture: + node = self._findNodeWithMaterial(material) + if node: + print(f"正在为节点 {node.getName()} 应用透明度贴图") + + # 启用透明度测试的PBR效果 + self._ensurePBREffectEnabledWithAlpha(node) + + # 清理现有的透明度贴图 + existing_stages = node.findAllTextureStages() + for stage in existing_stages: + if "alpha" in stage.getName().lower() or stage.getSort() == 8: + node.clearTexture(stage) + print(f"清理了现有的透明度贴图阶段: {stage.getName()}") + + # 创建透明度贴图纹理阶段,对应p3d_Texture8 + alpha_stage = TextureStage("alpha") + alpha_stage.setSort(8) # 对应p3d_Texture8 + alpha_stage.setMode(TextureStage.MModulate) + + node.setTexture(alpha_stage, texture) + print("透明度贴图已应用到p3d_Texture8槽") + + # 设置材质为透明着色模型 + from panda3d.core import Vec4 + current_emission = material.emission + if current_emission is None: + current_emission = Vec4(0, 0, 0, 0) + + # emission.x 用于存储着色模型,3表示透明 + new_emission = Vec4(3.0, current_emission.y, current_emission.z, current_emission.w) + material.set_emission(new_emission) + print("材质着色模型已设置为透明") + + print(f"透明度贴图已成功应用:{texture_path}") + else: + print("未找到材质对应的节点") + except Exception as e: + print(f"应用透明度贴图失败:{e}") + import traceback + traceback.print_exc() + + def _applyDetailTexture(self, material, texture_path): + """应用细节贴图""" + try: + from RenderPipelineFile.rpcore.loader import RPLoader + from panda3d.core import TextureStage + + texture = RPLoader.load_texture(texture_path) + if texture: + node = self._findNodeWithMaterial(material) + if node: + print(f"正在为节点 {node.getName()} 应用细节贴图") + + # 确保启用PBR效果 + self._ensurePBREffectEnabled(node) + + # 清理现有的细节贴图 + existing_stages = node.findAllTextureStages() + for stage in existing_stages: + if "detail" in stage.getName().lower() or stage.getSort() == 9: + node.clearTexture(stage) + print(f"清理了现有的细节贴图阶段: {stage.getName()}") + + # 创建细节贴图纹理阶段,对应p3d_Texture9 + detail_stage = TextureStage("detail") + detail_stage.setSort(9) # 对应p3d_Texture9 + detail_stage.setMode(TextureStage.MModulate) + + node.setTexture(detail_stage, texture) + print("细节贴图已应用到p3d_Texture9槽") + print("注意:细节贴图需要自定义shader支持才能正确显示") + + print(f"细节贴图已成功应用:{texture_path}") + else: + print("未找到材质对应的节点") + except Exception as e: + print(f"应用细节贴图失败:{e}") + import traceback + traceback.print_exc() + + def _applyGlossTexture(self, material, texture_path): + """应用光泽贴图""" + try: + from RenderPipelineFile.rpcore.loader import RPLoader + from panda3d.core import TextureStage + + texture = RPLoader.load_texture(texture_path) + if texture: + node = self._findNodeWithMaterial(material) + if node: + print(f"正在为节点 {node.getName()} 应用光泽贴图") + + # 确保启用PBR效果 + self._ensurePBREffectEnabled(node) + + # 清理现有的光泽贴图 + existing_stages = node.findAllTextureStages() + for stage in existing_stages: + if "gloss" in stage.getName().lower() or stage.getSort() == 10: + node.clearTexture(stage) + print(f"清理了现有的光泽贴图阶段: {stage.getName()}") + + # 创建光泽贴图纹理阶段,对应p3d_Texture10 + gloss_stage = TextureStage("gloss") + gloss_stage.setSort(10) # 对应p3d_Texture10 + gloss_stage.setMode(TextureStage.MGloss) # 光泽模式 + + node.setTexture(gloss_stage, texture) + print("光泽贴图已应用到p3d_Texture10槽") + print("注意:光泽贴图需要自定义shader支持才能正确显示") + + print(f"光泽贴图已成功应用:{texture_path}") + else: + print("未找到材质对应的节点") + except Exception as e: + print(f"应用光泽贴图失败:{e}") + import traceback + traceback.print_exc() + + def _clearConflictingTextureStages(self, node): + """清理可能冲突的纹理阶段""" + try: + from panda3d.core import TextureStage + + # 获取所有纹理阶段 + texture_stages = node.findAllTextureStages() + + # 检查是否有冲突的纹理阶段 + stages_to_clear = [] + for stage in texture_stages: + stage_name = stage.getName() + # 如果发现未命名或冲突的阶段,标记清理 + if stage_name == "" or stage == TextureStage.getDefault(): + # 检查是否有法线贴图在默认阶段 + texture = node.getTexture(stage) + if texture and "normal" in texture.getName().lower(): + stages_to_clear.append(stage) + + # 清理冲突的阶段 + for stage in stages_to_clear: + node.clearTexture(stage) + print(f"清理了冲突的纹理阶段: {stage.getName()}") + + except Exception as e: + print(f"清理纹理阶段时出错: {e}") + + def _findNodeWithMaterial(self, target_material): + """查找使用指定材质的节点""" + # 这里需要根据你的场景结构来实现 + # 遍历场景中的所有节点,找到使用该材质的节点 + # for model in self.world.scene_manager.models: + # materials = model.find_all_materials() + # if target_material in materials: + # return model + + """查找使用指定材质的节点""" + # 首先尝试在当前选中的模型中查找 + current_item = self.world.treeWidget.currentItem() + if current_item: + current_model = current_item.data(0, Qt.UserRole) + if current_model: + materials = current_model.find_all_materials() + if target_material in materials: + return current_model + + # 如果在当前选中模型中没找到,再遍历所有模型 + for model in self.world.scene_manager.models: + materials = model.find_all_materials() + if target_material in materials: + return model + return None + + def _findMaterialAndNodeByTitle(self, material_title): + """根据材质标题查找对应的材质和节点""" + + current_item = self.world.treeWidget.currentItem() + if not current_item: + print("未找到当前选中项") + return None, None + + current_model = current_item.data(0, Qt.UserRole) + if not current_model: + print("未找到当前模型") + return None, None + + materials = current_model.find_all_materials() + model_name = current_model.getName() or "未命名模型" + print(f"模型名称: '{model_name}', 材质数量: {len(materials)}") + + name_counter = {} + for i, material in enumerate(materials): + material_name = material.get_name() if hasattr(material, + 'get_name') and material.get_name() else f"材质{i + 1}" + base_name = f"{material_name}({model_name})" + + if base_name in name_counter: + name_counter[base_name] += 1 + unique_name = f"{base_name}_{name_counter[base_name]}" + else: + name_counter[base_name] = 1 + unique_name = base_name + + print(f"材质 {i}: 生成标题='{unique_name}'") + + if unique_name == material_title: + print(f"找到匹配的材质!") + geom_node = self._findSpecificGeomNodeWithMaterial(current_model, material) + if geom_node: + print(f"找到几何节点: {geom_node.get_name()}") + return material, geom_node + else: + print("未找到对应的几何节点,使用模型节点") + return material, current_model + + print("未找到匹配的材质标题") + return None, None + + def _findSpecificGeomNodeWithMaterial(self, model, target_material): + """查找使用指定材质的具体几何节点""" + from panda3d.core import MaterialAttrib, GeomNode + + # print(f"查找材质: {target_material.get_name() if hasattr(target_material, 'get_name') else 'unnamed'}") + + # 首先尝试查找GeomNode + geom_nodes = model.find_all_matches("**/+GeomNode") + # print(f"找到 {len(geom_nodes)} 个几何节点") + + # 如果没有找到GeomNode,尝试查找所有子节点 + if len(geom_nodes) == 0: + # print("未找到GeomNode,尝试查找所有子节点...") + all_nodes = model.find_all_matches("**") + # print(f"找到 {len(all_nodes)} 个子节点") + + for node_np in all_nodes: + node = node_np.node() + if isinstance(node, GeomNode): + geom_nodes.append(node_np) + print(f"找到GeomNode: {node_np.getName()}") + + for geom_np in geom_nodes: + geom_node = geom_np.node() + geom_count = geom_node.get_num_geoms() + # rint(f"检查几何节点 {geom_node.get_name()}: {geom_count} 个几何体") + + for i in range(geom_count): + state = geom_node.get_geom_state(i) + if state.has_attrib(MaterialAttrib): + material = state.get_attrib(MaterialAttrib).get_material() + if material == target_material: + # print(f"找到匹配的几何节点: {geom_np.get_name()}") + return geom_np + + print("未找到匹配的几何节点") + return None + + def _findSpecificGeomNodeForMaterial(self, target_material): + """查找使用指定材质的具体几何节点(统一方法)""" + material_name = target_material.get_name() if hasattr(target_material, 'get_name') else 'unnamed' + print(f"查找材质对应的几何节点: {material_name}") + + # 优先使用存储的几何节点映射 + material_id = id(target_material) + if hasattr(self, '_material_geom_mapping') and material_id in self._material_geom_mapping: + geom_node = self._material_geom_mapping[material_id] + if geom_node: # 确保节点仍然有效 + print(f"✓ 使用存储的几何节点映射: {geom_node.getName()}") + return geom_node + + # 如果没有存储的映射,使用传统查找方法 + current_item = self.world.treeWidget.currentItem() + if current_item: + current_model = current_item.data(0, Qt.UserRole) + if current_model: + # 使用现有的精确查找方法 + geom_node = self._findSpecificGeomNodeWithMaterial(current_model, target_material) + if geom_node: + # print(f"✓ 找到特定几何节点: {geom_node.getName()}") + # 存储映射以供后续使用 + if not hasattr(self, '_material_geom_mapping'): + self._material_geom_mapping = {} + self._material_geom_mapping[material_id] = geom_node + return geom_node + else: + # print("⚠️ 未找到特定几何节点,使用模型节点(可能影响所有材质)") + return current_model + + print("❌ 未找到当前选中的模型") + return None + + def _findGeomNodeWithMaterial(self, model, target_material): + """查找使用指定材质的具体几何节点""" + from panda3d.core import MaterialAttrib + + # print(f"查找材质: {target_material.get_name() if hasattr(target_material, 'get_name') else 'unnamed'}") + + # 遍历模型下的所有几何节点 + geom_nodes = model.find_all_matches("**/+GeomNode") + # print(f"找到 {len(geom_nodes)} 个几何节点") + + for geom_np in geom_nodes: + geom_node = geom_np.node() + geom_count = geom_node.get_num_geoms() + # print(f"几何节点 {geom_node.get_name()}: {geom_count} 个几何体") + + for i in range(geom_count): + state = geom_node.get_geom_state(i) + if state.has_attrib(MaterialAttrib): + material = state.get_attrib(MaterialAttrib).get_material() + if material == target_material: + # print(f"找到匹配的几何节点: {geom_np.get_name()}") + return geom_np + else: + print(f"几何体 {i} 没有材质属性") + + print("未找到匹配的几何节点") + return None + + def _displayCurrentTextures(self, material, material_layout, current_row): + """显示当前材质的贴图信息""" + node = self._findNodeWithMaterial(material) + + # 当前贴图信息标题 + current_row += 1 + texture_info_title = QLabel("当前贴图信息") + texture_info_title.setStyleSheet("color: #666; font-weight: bold; font-size: 10px; margin-top: 5px;") + material_layout.addWidget(texture_info_title, current_row, 0, 1, 4) + current_row += 1 + + if node: + # 显示当前应用的纹理信息 + texture = node.getTexture() + if texture: + texture_name = texture.getName() or "未命名贴图" + texture_info = QLabel(f"当前贴图: {texture_name}") + texture_info.setStyleSheet("color: #666; font-size: 9px;") + material_layout.addWidget(texture_info, current_row, 0, 1, 2) + else: + no_texture_info = QLabel("当前贴图: 无") + no_texture_info.setStyleSheet("color: #888; font-size: 9px; font-style: italic;") + material_layout.addWidget(no_texture_info, current_row, 0, 1, 2) + else: + no_node_info = QLabel("无法获取贴图信息") + no_node_info.setStyleSheet("color: #888; font-size: 9px; font-style: italic;") + material_layout.addWidget(no_node_info, current_row, 0, 1, 4) + + return current_row + 1 + + def _applyToAllMaterials(self, model, property_name, value): + """将属性应用到模型的所有材质""" + materials = model.find_all_materials() + for material in materials: + if property_name == "base_color": + material.set_base_color(value) + elif property_name == "roughness": + material.set_roughness(value) + elif property_name == "metallic": + material.set_metallic(value) + elif property_name == "ior": + material.set_refractive_index(value) + + def _addShadingModelPanel(self, material, material_layout, current_row): + """添加着色模型选择面板""" + from PyQt5.QtWidgets import QComboBox + + # RenderPipeline 支持的着色模型 + SHADING_MODELS = [ + ("默认", 0), + ("自发光", 1), + ("透明涂层", 2), + ("透明", 3), + ("皮肤", 4), + ("植物", 5), + ] + + shading_title = QLabel("着色模型") + shading_title.setStyleSheet("font-weight:bold;") + material_layout.addWidget(shading_title, current_row, 0, 1, 4) + current_row += 1 + + material_layout.addWidget(QLabel("着色模型:"), current_row, 0) + + shading_combo = QComboBox() + for name, value in SHADING_MODELS: + shading_combo.addItem(name) + + # 安全地获取当前着色模型 + current_model = 0 # 默认值 + try: + if hasattr(material, 'emission') and material.emission is not None: + current_model = int(material.emission.x) + except (AttributeError, TypeError, ValueError): + current_model = 0 + + shading_combo.setCurrentIndex(current_model) + + shading_combo.currentIndexChanged.connect( + lambda idx: self._onShadingModelChanged(material, idx) + ) + material_layout.addWidget(shading_combo, current_row, 1, 1, 3) + current_row += 1 + + # 如果是透明着色模型,添加透明度控制 + try: + if hasattr(material, 'emission') and material.emission is not None and int(material.emission.x) == 3: + current_row = self._addTransparencyPanel(material, material_layout, current_row) + except Exception as e: + print(f"添加透明度面板时出错: {e}") + + return current_row + + def _onShadingModelChanged(self, material, model_index): + """处理着色模型变化""" + print(f"着色模型变化: {model_index}") + + # 更新着色模型 + self._updateShadingModel(material, model_index) + + # 如果切换到透明模式,立即添加透明度面板 + if model_index == 3: + print("切换到透明模式,添加透明度面板...") + # 延迟一点时间让UI更新完成 + from PyQt5.QtCore import QTimer + QTimer.singleShot(100, lambda: self._refreshMaterialUI()) + # QTimer.singleShot(100, lambda: self._addTransparencyPanel(material)) + + def _updateShadingModel(self, material, model_index): + """更新着色模型""" + from panda3d.core import Vec4 + + # 安全地获取当前 emission 值 + current_emission = Vec4(0, 1.0, 0, 0) + if hasattr(material, 'emission') and material.emission is not None: + current_emission = material.emission + + # 根据不同的着色模型设置相应的参数 + if model_index == 1: # 自发光模式 + default_emission_strength = 2.0 if current_emission.z == 0 else current_emission.z + new_emission = Vec4(float(model_index), current_emission.y, default_emission_strength, current_emission.w) + elif model_index == 3: # 透明模式 + print("设置透明着色模型...") + + # 设置默认透明度值 + default_opacity = 0.5 # 设置为较高的值,确保模型可见 + + # 同时在emission.y和base_color.w中设置透明度值 + # emission.y可能被RenderPipeline的某些部分使用 + new_emission = Vec4(float(model_index), default_opacity, current_emission.z, current_emission.w) + + # 透明度通过基础颜色的Alpha通道控制 + self._updateMaterialAlphaForTransparency(material, default_opacity) + + # 应用透明渲染效果 + # self._applyTransparentRenderingEffect() + + print(f"透明着色模型设置完成") + print(f" - emission.x = {model_index} (透明着色模型)") + print(f" - emission.y = {default_opacity} (可能的透明度参数)") + print(f" - base_color.w = {default_opacity} (Alpha透明度)") + else: + new_emission = Vec4(float(model_index), current_emission.y, current_emission.z, current_emission.w) + + material.set_emission(new_emission) + + # 刷新UI以更新相关控件的值 + if model_index in [1, 3]: # 自发光或透明模式 + self._refreshMaterialUI() + + print( + f"着色模型已更新为: {model_index} ({'自发光' if model_index == 1 else '透明' if model_index == 3 else '默认'})") + + def _addTransparencyPanel(self, material, material_layout, current_row): + """添加透明度控制面板""" + transparency_title = QLabel("透明度属性") + transparency_title.setStyleSheet("color: #00BFFF; font-weight:bold;") + material_layout.addWidget(transparency_title, current_row, 0, 1, 4) + current_row += 1 + + # 不透明度滑块(避免混淆,使用不透明度) + material_layout.addWidget(QLabel("不透明度:"), current_row, 0) + opacity_spinbox = QDoubleSpinBox() + opacity_spinbox.setRange(0.0, 1.0) # 最小值0.1,避免完全消失 + opacity_spinbox.setSingleStep(0.01) + + # 安全地获取当前不透明度值(从基础颜色的Alpha通道) + current_opacity = 0.3 # 默认值 + try: + base_color = self._getOrCreateMaterialBaseColor(material) + if base_color is not None: + current_opacity = base_color.w + except Exception as e: + print(f"获取当前透明度失败,使用默认值: {e}") + + opacity_spinbox.setValue(current_opacity) + opacity_spinbox.valueChanged.connect(lambda v: self._updateTransparency(material, v)) + material_layout.addWidget(opacity_spinbox, current_row, 1, 1, 3) + current_row += 1 + + return current_row + + def _updateTransparency(self, material, opacity_value): + """更新不透明度值(同时更新emission.y和base_color.w)""" + try: + from panda3d.core import Vec4 + current_emission = material.emission or Vec4(0, 0, 0, 0) + new_emission = Vec4(current_emission.x, opacity_value, current_emission.z, current_emission.w) + material.set_emission(new_emission) + + # 更新基础颜色的Alpha通道 + self._updateMaterialAlphaForTransparency(material, opacity_value) + + print(f"透明度已更新:") + print(f" - emission.y = {opacity_value}") + print(f" - base_color.w = {opacity_value}") + print(f" - 视觉透明度 = {1.0 - opacity_value:.2f}") + + except Exception as e: + print(f"更新透明度失败: {e}") + + def _updateMaterialAlphaForTransparency(self, material, opacity_slider): + """ + opacity_slider: 0=全透 1=不透 + shader 需要 1-opacity_slider 作为真正的 alpha + """ + from panda3d.core import Vec4, TransparencyAttrib + + current_item = self.world.treeWidget.currentItem() + if not current_item: + return + model = current_item.data(0, Qt.UserRole) + if model.isEmpty(): + return + + model.setTransparency(TransparencyAttrib.MAlpha) + model.setTwoSided(True) + model.setDepthWrite(False) + + alpha = opacity_slider # 反转 + color = self._getOrCreateMaterialBaseColor(material) or Vec4(1, 1, 1, 1) + material.base_color = Vec4(color.x, color.y, color.z, alpha) + material.base_color = Vec4(color.x, color.y, color.z, alpha) + + em = material.emission or Vec4(0, 0, 0, 0) + material.set_emission(Vec4(3.0, alpha, em.z, em.w)) + + self.world.render_pipeline.set_effect( + model, + "effects/simple_transparent.yaml", + options={}, + sort=200 + ) + self.world.render_pipeline.prepare_scene(model) + print(f"[透明] 不透明度={opacity_slider:.2f} 已同步") + + def _applyTransparentRenderingEffect(self): + from panda3d.core import TransparencyAttrib + """为当前选中的模型应用透明渲染效果(简化版本)""" + try: + current_item = self.world.treeWidget.currentItem() + if current_item: + model = current_item.data(0, Qt.UserRole) + if model: + model.setTransparency(TransparencyAttrib.MAlpha) + + self.world.render_pipeline.set_effect( + model, + "effects/default.yaml", + { + "render_gbuffer": True, + "alpha_testing": False, + "normal_mapping": True, + "render_shadow": True, + "render_envmap": True, + }, + sort=100 + ) + + # 让RenderPipeline自动处理透明材质 + # 当emission.x=3时,RenderPipeline会自动设置正确的渲染参数 + self.world.render_pipeline.prepare_scene(model) + print(" - RenderPipeline自动处理: 已完成") + + print(f"✓ 已为模型 {model.getName()} 应用透明渲染效果") + print(" 注意: 使用简化设置避免渲染冲突") + + except Exception as e: + print(f"✗ 应用透明渲染效果失败: {e}") + import traceback + traceback.print_exc() + + def _addEmissionPanel(self, material, material_layout, current_row): + """添加自发光控制面板""" + emission_title = QLabel("自发光属性") + emission_title.setStyleSheet("font-weight:bold;") + material_layout.addWidget(emission_title, current_row, 0, 1, 4) + current_row += 1 + + # 自发光强度标签和控件 + material_layout.addWidget(QLabel("发光强度:"), current_row, 0) + + emission_spinbox = QDoubleSpinBox() + emission_spinbox.setRange(0.0, 10.0) + emission_spinbox.setSingleStep(0.1) + + # 安全地获取当前自发光强度 + current_emission_z = 0.0 # 默认值 + if hasattr(material, 'emission') and material.emission is not None: + current_emission_z = material.emission.z + + emission_spinbox.setValue(current_emission_z) + emission_spinbox.valueChanged.connect(lambda v: self._updateEmissionStrength(material, v)) + material_layout.addWidget(emission_spinbox, current_row, 1, 1, 3) + current_row += 1 + + return current_row + + def _updateEmissionStrength(self, material, strength): + """更新自发光强度""" + from panda3d.core import Vec4 + + # 安全地获取当前 emission 值 + current_emission = Vec4(0, 0, 0, 0) + if hasattr(material, 'emission') and material.emission is not None: + current_emission = material.emission + + # 更新 emission.z 存储发光强度(用于UI显示) + new_emission = Vec4(current_emission.x, current_emission.y, strength, current_emission.w) + material.set_emission(new_emission) + + # 对于自发光材质,直接调整基础颜色的亮度 + if current_emission.x == 1: # 如果是自发光着色模型 + # 获取原始基础颜色(假设存储在某处,或使用当前值的归一化版本) + base_intensity = 0.5 # 基础亮度 + intensity_multiplier = strength / 5.0 # 将0-10范围映射到合理的倍数 + + # 设置发光颜色 + emissive_color = Vec4( + base_intensity * intensity_multiplier, + base_intensity * intensity_multiplier, + base_intensity * intensity_multiplier, + 1.0 + ) + material.set_base_color(emissive_color) + + print(f"自发光强度已更新为: {strength}") + + def _addMaterialPresetPanel(self, material, material_layout, current_row): + """添加材质预设面板""" + from PyQt5.QtWidgets import QComboBox + + preset_title = QLabel("材质预设") + preset_title.setStyleSheet("font-weight:bold;") + material_layout.addWidget(preset_title, current_row, 0, 1, 4) + current_row += 1 + + # 选择预设标签和控件 + material_layout.addWidget(QLabel("选择预设:"), current_row, 0) + + preset_combo = QComboBox() + preset_combo.addItems([ + "自定义", "塑料", "金属", "玻璃", "橡胶", "木材", "陶瓷", "皮革" + ]) + + # 优先检查存储的预设名称 + if hasattr(material, '_applied_preset'): + preset_combo.setCurrentText(material._applied_preset) + else: + # 安全地检测当前材质最接近的预设 + try: + current_preset = self._detectCurrentPreset(material) + preset_combo.setCurrentText(current_preset) + except Exception as e: + print(f"检测材质预设时出错: {e}") + preset_combo.setCurrentText("自定义") + + preset_combo.currentTextChanged.connect( + lambda preset: self._applyMaterialPreset(material, preset) + ) + material_layout.addWidget(preset_combo, current_row, 1, 1, 3) + current_row += 1 + + return current_row + + def _detectCurrentPreset(self, material): + """检测当前材质最接近的预设""" + # 定义预设的精确匹配条件 + + presets = { + "塑料": {"base_color": (0.8, 0.8, 0.8), "roughness": 0.7, "metallic": 0.0, "ior": 1.4}, + "金属": {"base_color": (0.7, 0.7, 0.7), "roughness": 0.1, "metallic": 1.0, "ior": 1.5}, + "玻璃": {"base_color": (0.9, 0.9, 1.0), "roughness": 0.0, "metallic": 0.0, "ior": 1.5}, + "橡胶": {"base_color": (0.2, 0.2, 0.2), "roughness": 0.9, "metallic": 0.0, "ior": 1.3}, + "自发光": {"base_color": (1.0, 1.0, 1.0), "roughness": 0.5, "metallic": 0.0, "ior": 1.0} + } + + # 容差值,用于浮点数比较 + tolerance = 0.05 + + for preset_name, preset_values in presets.items(): + # 安全检查基础颜色 + base_color_match = False + if hasattr(material, 'base_color') and material.base_color is not None: + try: + base_color_match = ( + abs(material.base_color.x - preset_values["base_color"][0]) < tolerance and + abs(material.base_color.y - preset_values["base_color"][1]) < tolerance and + abs(material.base_color.z - preset_values["base_color"][2]) < tolerance + ) + except (AttributeError, TypeError): + base_color_match = False + + # 安全检查其他属性 + roughness_match = False + if hasattr(material, 'roughness') and material.roughness is not None: + try: + roughness_match = abs(float(material.roughness) - preset_values["roughness"]) < tolerance + except (AttributeError, TypeError, ValueError): + roughness_match = False + + metallic_match = False + if hasattr(material, 'metallic') and material.metallic is not None: + try: + metallic_match = abs(float(material.metallic) - preset_values["metallic"]) < tolerance + except (AttributeError, TypeError, ValueError): + metallic_match = False + + ior_match = False + if hasattr(material, 'refractive_index') and material.refractive_index is not None: + try: + ior_match = abs(float(material.refractive_index) - preset_values["ior"]) < tolerance + except (AttributeError, TypeError, ValueError): + ior_match = False + + # 如果所有属性都匹配,返回预设名称 + if base_color_match and roughness_match and metallic_match and ior_match: + return preset_name + + return "自定义" # 如果没有匹配的预设 + + def _applyMaterialPreset(self, material, preset_name): + """应用材质预设""" + presets = { + "塑料": {"base_color": Vec4(0.8, 0.8, 0.8, 1.0), "roughness": 0.7, "metallic": 0.0, "ior": 1.4}, + "金属": {"base_color": Vec4(0.7, 0.7, 0.7, 1.0), "roughness": 0.1, "metallic": 1.0, "ior": 1.5}, + "玻璃": {"base_color": Vec4(0.9, 0.9, 1.0, 0.2), "roughness": 0.0, "metallic": 0.0, "ior": 1.5, + "shading_model": 3, "transparency": 0.2}, + "橡胶": {"base_color": Vec4(0.2, 0.2, 0.2, 1.0), "roughness": 0.9, "metallic": 0.0, "ior": 1.3}, + "木材": {"base_color": Vec4(0.6, 0.4, 0.2, 1.0), "roughness": 0.8, "metallic": 0.0, "ior": 1.3}, + "陶瓷": {"base_color": Vec4(0.9, 0.9, 0.85, 1.0), "roughness": 0.1, "metallic": 0.0, "ior": 1.6}, + "皮革": {"base_color": Vec4(0.4, 0.3, 0.2, 1.0), "roughness": 0.6, "metallic": 0.0, "ior": 1.4} + } + + if preset_name not in presets: + print(f"未知的材质预设: {preset_name}") + return + + preset = presets[preset_name] + + if "shading_model" in preset: + emission = Vec4(float(preset["shading_model"]), 0, 0, 0) + if "transparency" in preset: + emission.y = preset["transparency"] + material.set_emission(emission) + + print(f"设置着色模型: {preset['shading_model']}") + print(f"材质emission值: {material.emission}") + print(f"基础颜色alpha: {preset['base_color'].w}") + + material.set_base_color(preset["base_color"]) + material.set_roughness(preset["roughness"]) + material.set_metallic(preset["metallic"]) + material.set_refractive_index(preset["ior"]) + + if "shading_model" in preset: + emission = Vec4(float(preset["shading_model"]), 0, 0, 0) + if "transparency" in preset: + emission.y = preset["transparency"] + material.set_emission(emission) + + # 关键:为透明材质应用正确的渲染效果 + if preset["shading_model"] == 3: + self._apply_transparent_effect() + + # material._applied_preset = preset_name + self._refreshMaterialUI() + print(f"已应用材质预设: {preset_name}") + + def _apply_transparent_effect(self): + """为当前选中的模型应用透明渲染效果""" + current_item = self.world.treeWidget.currentItem() + if current_item: + model = current_item.data(0, Qt.UserRole) + if model: + # 只调用 prepare_scene,让它自动处理透明材质 + self.world.render_pipeline.set_effect( + model, + "effects/default.yaml", + { + "render_forward": True, + "render_gbuffer": False, + "normal_mapping": True # 明确启用法线映射 + }, + 100 + ) + self.world.render_pipeline.prepare_scene(model) + print("已重新准备场景以应用透明效果") + + def _refreshMaterialUI(self): + """刷新材质 UI 显示""" + # 重新更新当前选中项的属性面板 + if hasattr(self.world, 'treeWidget') and self.world.treeWidget.currentItem(): + current_item = self.world.treeWidget.currentItem() + # 触发属性面板更新 + self.updatePropertyPanel(current_item) + + def _addBatchOperationsPanel(self, model): + """添加批量操作面板""" + batch_title = QLabel("批量操作") + batch_title.setStyleSheet("color: #E91E63; font-weight:bold;") + self._propertyLayout.addRow(batch_title) + + # 批量设置粗糙度 + batch_roughness_btn = QPushButton("统一粗糙度") + batch_roughness_btn.clicked.connect(lambda: self._batchSetRoughness(model)) + self._propertyLayout.addRow("批量粗糙度:", batch_roughness_btn) + + # 批量设置金属性 + batch_metallic_btn = QPushButton("统一金属性") + batch_metallic_btn.clicked.connect(lambda: self._batchSetMetallic(model)) + self._propertyLayout.addRow("批量金属性:", batch_metallic_btn) + + def _batchSetRoughness(self, model): + """批量设置粗糙度""" + from PyQt5.QtWidgets import QInputDialog + + value, ok = QInputDialog.getDouble(None, "批量设置", "粗糙度值:", 0.5, 0.0, 1.0, 2) + if ok: + self._applyToAllMaterials(model, "roughness", value) + print(f"已将所有材质粗糙度设置为: {value}") + + def _applyMetallicTexture_NEW(self, material, texture_path): + """应用金属性贴图 - 简化重写版本""" + try: + from RenderPipelineFile.rpcore.loader import RPLoader + from panda3d.core import TextureStage + + print(f"🎨 应用金属性贴图: {texture_path}") + + # 1. 加载纹理 + texture = RPLoader.load_texture(texture_path) + if not texture: + print("❌ 纹理加载失败") + return + + # 2. 找到节点 + node = self._findSpecificGeomNodeForMaterial(material) + if not node: + print("❌ 未找到材质对应的节点") + return + + print(f"🎯 目标节点: {node.getName()}") + + # 3. 设置材质金属性为1.0 + # material.set_metallic(1.0) + # print("🔧 材质金属性设置为1.0") + + # # 4. 创建纹理阶段 + # metallic_stage = TextureStage("metallic_map") + # metallic_stage.setSort(5) # p3d_Texture5 + # metallic_stage.setMode(TextureStage.MModulate) + # + # # 5. 绑定纹理 + # node.setTexture(metallic_stage, texture) + # print("🔗 纹理已绑定到p3d_Texture5槽") + + # 6. 应用金属性PBR效果,检查是否需要保持法线映射 + has_normal = self._hasNormalTexture(node) + + if not has_normal: + print("⚠️ 检测到材质没有法线贴图,先添加默认法线贴图...") + # self._applyDefaultNormalTexture(node) + self._applyNormalTexture(material, "RenderPipelineFile/Default_NRM_2K.png") + print("✅ 默认法线贴图已添加") + else: + print("✅ 检测到材质已有法线贴图") + + print("清理现有金属度贴图...") + existing_stages = node.findAllTextureStages() + for stage in existing_stages: + if stage.getSort() == 5: # p3d_Texture3槽 + node.clearTexture(stage) + print(f"已清理现有金属度贴图: {stage.getName()}") + + for i in range(4): + metallic_stage = TextureStage("metallic_map") + metallic_stage.setSort(5) # p3d_Texture5 + metallic_stage.setMode(TextureStage.MModulate) + node.setTexture(metallic_stage, texture) + + try: + # 检查是否需要透明度测试 + needs_alpha = self._needsAlphaTesting(node) + print(f"🔍 透明度测试: {'需要' if needs_alpha else '不需要'}") + + self.world.render_pipeline.set_effect( + node, + "effects/pbr_with_metallic.yaml", + { + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": needs_alpha, # 根据是否需要透明度决定 + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 100 + ) + print(f"✅ 金属性PBR效果已应用(法线映射: {'启用' if has_normal else '禁用'})") + + # 温和的shader刷新方法 + print("🔄 温和刷新shader状态...") + try: + # 只重新绑定纹理,不破坏渲染状态 + node.setTexture(metallic_stage, texture) + + # 重新应用效果,但使用相同优先级 + self.world.render_pipeline.set_effect( + node, + "effects/pbr_with_metallic.yaml", + { + # "normal_mapping": has_normal, + "normal_mapping": True, + "render_gbuffer": True, + "alpha_testing": False, + "parallax_mapping": False, + "render_shadow": True, + "render_envmap": True + }, + 100 # 相同优先级 + ) + + print("✅ 金属性PBR效果已应用") + + # 关键修复:多次重新绑定纹理,确保着色器编译完成后生效 + print("🔧 强化纹理绑定机制...") + try: + import time + + # 多次重新绑定,模拟手动"应用两次"的效果 + for i in range(3): + print(f" 第{i + 1}次绑定...") + + # 等待更长时间确保着色器编译完成 + time.sleep(0.05) # 50ms延迟 + + # 强制重新绑定纹理 + node.setTexture(metallic_stage, texture) + + # 强制刷新节点状态 + node.setRenderModeWireframe() + node.clearRenderMode() + + # 验证绑定 + applied_texture = node.getTexture(metallic_stage) + if applied_texture: + print(f" ✅ 第{i + 1}次绑定成功") + else: + print(f" ❌ 第{i + 1}次绑定失败") + + # 最终验证 + final_texture = node.getTexture(metallic_stage) + if final_texture: + print(f"✅ 强化绑定完成: {final_texture.getName()}") + print("🔍 现在应该看到正确的金属性贴图效果") + print("🔍 黑色区域=非金属,白色区域=金属") + else: + print("❌ 强化绑定最终失败") + + except Exception as rebind_error: + print(f"⚠️ 强化纹理绑定失败: {rebind_error}") + + except Exception as refresh_error: + print(f"⚠️ 金属性PBR效果应用失败: {refresh_error}") + + except Exception as e: + print(f"⚠️ PBR效果应用失败: {e}") + + except Exception as e: + print(f"❌ 应用金属性贴图失败: {e}") + import traceback + traceback.print_exc() + + def _hasMetallicTexture(self, node): + """检查节点是否有金属性贴图""" + try: + all_stages = node.findAllTextureStages() + for stage in all_stages: + if stage.getSort() == 5: # p3d_Texture5 是金属性贴图槽 + tex = node.getTexture(stage) + if tex: + print(f"🔍 发现金属性贴图: {tex.getName()}") + return True + return False + except Exception as e: + print(f"⚠️ 检查金属性贴图失败: {e}") + return False + + def _applyDefaultNormalTexture(self, node): + """应用RenderPipeline的默认法线贴图""" + try: + from RenderPipelineFile.rpcore.loader import RPLoader + from panda3d.core import TextureStage + + print("🎨 加载RenderPipeline默认法线贴图...") + + # 使用RenderPipeline自带的默认法线贴图 + default_normal_path = "RenderPipelineFile/Default_NRM_2K.png" + texture = RPLoader.load_texture(default_normal_path) + + if not texture: + print(f"❌ 无法加载默认法线贴图: {default_normal_path}") + # 回退到虚拟法线贴图 + return + + print(f"✅ 默认法线贴图加载成功: {texture.getName()}") + + # 创建法线贴图纹理阶段 + normal_stage = TextureStage("default_normal") + normal_stage.setSort(1) # p3d_Texture1 + normal_stage.setMode(TextureStage.MModulate) + normal_stage.setTexcoordName("texcoord") + + # 应用默认法线贴图 + node.setTexture(normal_stage, texture) + print("✅ RenderPipeline默认法线贴图已应用") + + return True + + except Exception as e: + print(f"⚠️ 应用默认法线贴图失败: {e}") + # 回退到虚拟法线贴图 + return self._createDummyNormalTexture(node) + + def _needsAlphaTesting(self, node): + """检查节点是否需要透明度测试(检查漫反射贴图是否有透明通道)""" + try: + all_stages = node.findAllTextureStages() + for stage in all_stages: + if stage.getSort() == 0: # p3d_Texture0 是漫反射贴图槽 + tex = node.getTexture(stage) + if tex: + # 检查纹理格式是否支持透明度 + format_name = str(tex.getFormat()) + has_alpha = 'alpha' in format_name.lower() or 'rgba' in format_name.lower() + if has_alpha: + print(f"🔍 发现透明漫反射贴图: {tex.getName()}") + return True + return False + except Exception as e: + print(f"⚠️ 检查透明度需求失败: {e}") + return True # 出错时默认启用透明度测试,更安全 + + def _addSunAzimuthPanel(self): + """添加太阳方位角控制面板""" + # 太阳控制组 + sun_group = QGroupBox("太阳控制") + sun_layout = QGridLayout() + + # 太阳方位角 + sun_layout.addWidget(QLabel("方位角:"), 0, 0) + self.azimuthSpinBox = QDoubleSpinBox() + self.azimuthSpinBox.setRange(0, 360) + self.azimuthSpinBox.setSuffix("°") + self.azimuthSpinBox.setValue(180) + self.azimuthSpinBox.valueChanged.connect(self._applySunAzimuth_new) + sun_layout.addWidget(self.azimuthSpinBox, 0, 1, 1, 2) + + # 太阳高度角 + sun_layout.addWidget(QLabel("高度角:"), 1, 0) + self.altitudeSpinBox = QDoubleSpinBox() + self.altitudeSpinBox.setRange(0, 90) + self.altitudeSpinBox.setSuffix("°") + self.altitudeSpinBox.setValue(90) + self.altitudeSpinBox.valueChanged.connect(self._applySunAltitude) + sun_layout.addWidget(self.altitudeSpinBox, 1, 1, 1, 2) + + # 太阳预设 + sun_layout.addWidget(QLabel("预设:"), 2, 0) + presetCombo = QComboBox() + presetCombo.addItems(["日出", "正午", "日落", "午夜"]) + sun_layout.addWidget(presetCombo, 2, 1, 1, 2) + + # 映射 + preset_map = { + "日出": "sunrise", + "正午": "noon", + "日落": "sunset", + "午夜": "midnight" + } + + # 应用预设按钮 + applyPresetBtn = QPushButton("应用预设") + applyPresetBtn.clicked.connect(lambda: self._setSunPreset(preset_map[presetCombo.currentText()])) + sun_layout.addWidget(applyPresetBtn, 2, 3) + + # 实时更新 + # realtimeCheckBox = QCheckBox("实时更新") + # realtimeCheckBox.setChecked(True) + # sun_layout.addWidget(realtimeCheckBox, 3, 0, 1, 3) # 跨三列 + + sun_group.setLayout(sun_layout) + self._propertyLayout.addWidget(sun_group) + + def _onSunAzimuthSliderChanged(self, value): + """滑块值改变时的回调""" + try: + # 同步到数值框 + self.sun_azimuth_spinbox.blockSignals(True) + self.sun_azimuth_spinbox.setValue(value) + self.sun_azimuth_spinbox.blockSignals(False) + + # 应用到Day Time Editor + self._applySunAzimuth_new(value) + + except Exception as e: + print(f"❌ 滑块值改变处理失败: {e}") + + def _onSunAzimuthSpinboxChanged(self, value): + """数值框值改变时的回调""" + try: + # 同步到滑块 + self.sun_azimuth_slider.blockSignals(True) + self.sun_azimuth_slider.setValue(value) + self.sun_azimuth_slider.blockSignals(False) + + # 应用到Day Time Editor + self._applySunAzimuth_new(value) + + except Exception as e: + print(f"❌ 数值框值改变处理失败: {e}") + + def _onSunAltitudeSliderChanged(self, value): + """太阳高度角滑块值改变时的回调""" + try: + # 同步到数值框 + self.sun_altitude_spinbox.blockSignals(True) + self.sun_altitude_spinbox.setValue(value) + self.sun_altitude_spinbox.blockSignals(False) + + # 应用太阳高度角 + self._applySunAltitude(value) + + except Exception as e: + print(f"❌ 太阳高度角滑块值改变处理失败: {e}") + + def _onSunAltitudeSpinboxChanged(self, value): + """太阳高度角数值框值改变时的回调""" + try: + # 同步到滑块 + self.sun_altitude_slider.blockSignals(True) + self.sun_altitude_slider.setValue(value) + self.sun_altitude_slider.blockSignals(False) + + # 应用太阳高度角 + self._applySunAltitude(value) + + except Exception as e: + print(f"❌ 太阳高度角数值框值改变处理失败: {e}") + + def _setSunPreset(self, preset_name): + """设置太阳预设位置""" + try: + presets = { + "sunrise": (90, 45), # 东方,低角度 + "noon": (180, 90), # 南方,天顶 + "sunset": (270, 45), # 西方,低角度 + "midnight": (0, 0) # 北方,地平线 + } + + if preset_name in presets: + azimuth, altitude = presets[preset_name] + + # 更新滑块和数值框 + self.azimuthSpinBox.blockSignals(True) + self.altitudeSpinBox.blockSignals(True) + + self.azimuthSpinBox.setValue(azimuth) + self.altitudeSpinBox.setValue(altitude) + + self.azimuthSpinBox.blockSignals(False) + self.altitudeSpinBox.blockSignals(False) + + # 应用设置 - 优先使用Day Time Editor + azimuth_success = self._updateDayTimeEditorSetting("scattering", "sun_azimuth", azimuth) + altitude_success = self._updateDayTimeEditorSetting("scattering", "sun_altitude", altitude) + + if azimuth_success and altitude_success: + print(f"✅ 通过Day Time Editor设置太阳预设 {preset_name}: 方位角{azimuth}°, 高度角{altitude}°") + elif hasattr(self.world, 'sun_controller'): + self.world.sun_controller.set_sun_position(azimuth, altitude) + print(f"✅ 通过太阳控制器设置预设 {preset_name}: 方位角{azimuth}°, 高度角{altitude}°") + elif hasattr(self.world, 'setSunPosition'): + self.world.setSunPosition(azimuth, altitude) + print(f"✅ 通过主程序方法设置预设 {preset_name}: 方位角{azimuth}°, 高度角{altitude}°") + else: + # 分别应用方位角和高度角 + self._applySunAzimuth_new(azimuth) + self._applySunAltitude(altitude) + print(f"✅ 设置太阳预设 {preset_name}: 方位角{azimuth}°, 高度角{altitude}°") + + except Exception as e: + print(f"❌ 设置太阳预设失败: {e}") + import traceback + traceback.print_exc() + + def _applySunAzimuth_new(self, azimuth_value): + """应用太阳方位角 - 直接控制Day Time Editor""" + try: + # 方法1: 直接控制Day Time Editor中的sun_azimuth节点 + success = self._updateDayTimeEditorSetting("scattering", "sun_azimuth", azimuth_value) + if success: + print(f"✅ 通过Day Time Editor设置方位角: {azimuth_value}°") + return + + # 方法2: 使用我们的太阳控制器作为备用 + if hasattr(self.world, 'sun_controller'): + self.world.sun_controller.set_sun_azimuth(azimuth_value) + print(f"✅ 通过太阳控制器设置方位角: {azimuth_value}°") + return + + # 方法3: 直接调用主程序的太阳控制方法 + if hasattr(self.world, 'setSunAzimuth'): + self.world.setSunAzimuth(azimuth_value) + print(f"✅ 通过主程序方法设置方位角: {azimuth_value}°") + return + + print("⚠️ 所有方位角设置方法都不可用") + + except Exception as e: + print(f"❌ 应用太阳方位角失败: {e}") + import traceback + traceback.print_exc() + + def _applySunAltitude(self, altitude_value): + """应用太阳高度角 - 直接控制Day Time Editor""" + try: + # 方法1: 直接控制Day Time Editor中的sun_altitude节点 + success = self._updateDayTimeEditorSetting("scattering", "sun_altitude", altitude_value) + if success: + print(f"✅ 通过Day Time Editor设置高度角: {altitude_value}°") + return + + # 方法2: 使用我们的太阳控制器作为备用 + if hasattr(self.world, 'sun_controller'): + self.world.sun_controller.set_sun_altitude(altitude_value) + print(f"✅ 通过太阳控制器设置高度角: {altitude_value}°") + return + + # 方法3: 直接调用主程序的太阳控制方法 + if hasattr(self.world, 'setSunAltitude'): + self.world.setSunAltitude(altitude_value) + print(f"✅ 通过主程序方法设置高度角: {altitude_value}°") + return + + print("⚠️ 所有高度角设置方法都不可用") + + except Exception as e: + print(f"❌ 应用太阳高度角失败: {e}") + import traceback + traceback.print_exc() + + def _updateDayTimeEditorSetting(self, plugin_name, setting_name, value): + """直接更新Day Time Editor中的设置节点值""" + try: + # 检查是否有RenderPipeline和插件管理器 + if not hasattr(self.world, 'render_pipeline') or not self.world.render_pipeline: + print("⚠️ RenderPipeline未初始化") + return False + + pipeline = self.world.render_pipeline + if not hasattr(pipeline, 'plugin_mgr') or not pipeline.plugin_mgr: + print("⚠️ 插件管理器未初始化") + return False + + plugin_mgr = pipeline.plugin_mgr + + # 检查插件是否存在 + if plugin_name not in plugin_mgr.instances: + print(f"⚠️ 插件 '{plugin_name}' 不存在") + return False + + # 检查Day Time设置是否存在 + if plugin_name not in plugin_mgr.day_settings: + print(f"⚠️ 插件 '{plugin_name}' 没有Day Time设置") + return False + + day_settings = plugin_mgr.day_settings[plugin_name] + if setting_name not in day_settings: + print(f"⚠️ 设置 '{setting_name}' 在插件 '{plugin_name}' 中不存在") + return False + + setting_handle = day_settings[setting_name] + + # 根据设置类型转换值 + if setting_name == "sun_azimuth": + # 方位角:度数转换为0-1范围 + normalized_value = (value % 360) / 360.0 + elif setting_name == "sun_altitude": + # 高度角:度数转换为0-1范围 + normalized_value = max(0, min(90, value)) / 90.0 + else: + # 其他设置直接使用原值 + normalized_value = value + + # 获取当前时间(如果Day Time Editor正在运行) + current_time = getattr(self, '_current_daytime', 0.5) # 默认中午12点 + + # 更新设置的曲线值 + if hasattr(setting_handle, 'curves') and setting_handle.curves: + # 清除现有的控制点,设置单一值 + setting_handle.curves[0].set_single_value(normalized_value) + #print(f"✅ 更新Day Time设置: {plugin_name}.{setting_name} = {value} (归一化: {normalized_value:.3f})") + + # 保存设置到配置文件 + try: + plugin_mgr.save_daytime_overrides("/$$rpconfig/daytime.yaml") + #print("✅ Day Time设置已保存到配置文件") + except Exception as e: + print(f"⚠️ 保存配置文件失败: {e}") + + # 通知Day Time Editor重新加载配置(如果正在运行) + try: + from RenderPipelineFile.rpcore.util.network_communication import NetworkCommunication + NetworkCommunication.send_async(NetworkCommunication.DAYTIME_PORT, "loadconf") + #print("✅ 已通知Day Time Editor重新加载配置") + except Exception as e: + print(f"⚠️ 通知Day Time Editor失败: {e}") + + return True + else: + print(f"⚠️ 设置 '{setting_name}' 没有可用的曲线") + return False + + except Exception as e: + print(f"❌ 更新Day Time Editor设置失败: {e}") + import traceback + traceback.print_exc() + return False + + def _applySunAzimuth(self, azimuth_degrees): + """应用太阳方位角到Day Time Editor中的Sun Azimuth节点""" + try: + if not hasattr(self, 'world') or not self.world: + print("⚠️ World对象不存在") + return + + # Day Time Editor是独立进程,需要通过进程间通信控制 + print(f"🌞 尝试控制Day Time Editor中的Sun Azimuth: {azimuth_degrees}°") + + # 方法1:通过文件通信 + success = self._sendSunAzimuthViaFile(azimuth_degrees) + if success: + return + + # 方法2:尝试直接控制RenderPipeline的daytime_mgr(如果存在) + # success = self._controlLocalDaytimeManager(azimuth_degrees) + # if success: + # return + + # 方法3:通过socket通信(如果实现了) + # success = self._sendSunAzimuthViaSocket(azimuth_degrees) + # if success: + # return + + print("⚠️ 无法控制Day Time Editor中的Sun Azimuth节点") + print(" 原因:Day Time Editor运行在独立进程中") + print(" 建议:") + print(" 1. 直接在Day Time Editor窗口中调整Sun Azimuth") + print(" 2. 或者实现进程间通信机制") + + except Exception as e: + print(f"❌ 应用太阳方位角失败: {e}") + import traceback + traceback.print_exc() + + def _sendSunAzimuthViaFile(self, azimuth_degrees): + """通过文件通信发送Sun Azimuth值到Day Time Editor""" + try: + import json + import os + import tempfile + + # 创建通信文件 + comm_dir = os.path.join(tempfile.gettempdir(), "daytime_editor_comm") + os.makedirs(comm_dir, exist_ok=True) + + comm_file = os.path.join(comm_dir, "sun_azimuth_command.json") + + command = { + "command": "set_sun_azimuth", + "value": azimuth_degrees, + "timestamp": __import__('time').time() + } + + with open(comm_file, 'w') as f: + json.dump(command, f) + + print(f"📁 已通过文件发送Sun Azimuth命令: {azimuth_degrees}°") + print(f" 通信文件: {comm_file}") + return True + + except Exception as e: + print(f"⚠️ 文件通信失败: {e}") + return False + + def _addAnimationPanel(self, origin_model): + try: + has_animation = False + + # 动画控制组 + animation_group = QGroupBox("动画控制") + animation_layout = QGridLayout() + animation_layout.setColumnMinimumWidth(0, 115) + + + # 首先检测骨骼动画 + has_skeletal_anim = False + try: + actor = self._getActor(origin_model) + if actor and actor.getAnimNames(): + self._buildSkeletalUI(origin_model, actor, animation_layout) + has_animation = True + print(f"[信息] 检测到骨骼动画: {actor.getAnimNames()}") + except Exception as actor_error: + # 忽略 Actor 加载错误,很多模型都不是角色动画 + print(f"[信息] 该模型不包含骨骼动画: {actor_error}") + + # 如果都没有动画 + if not has_animation: + no_anim_label = QLabel("无法识别动画") + no_anim_label.setStyleSheet("color:#888;font-style:italic;") + animation_layout.addWidget(no_anim_label, 0, 0) + + animation_group.setLayout(animation_layout) + self._propertyLayout.addWidget(animation_group) + + except Exception as e: + print("添加动画面板失败:", e) + import traceback + traceback.print_exc() + + def _buildSkeletalUI(self, origin_model, actor, layout): + from PyQt5.QtWidgets import QLabel, QComboBox, QHBoxLayout, QWidget, QPushButton, QDoubleSpinBox + + actor.hide() + origin_model.show() + + # animation_title = QLabel("骨骼动画控制") + # animation_title.setStyleSheet("color:#6B6BFF;font-weight:bold;font-size:14px;margin-top:10px;") + # self._propertyLayout.addRow(animation_title) + + # 获取和分析动画名称 + anim_names = actor.getAnimNames() + processed_names = self._processAnimationNames(origin_model, anim_names) + + # 显示动画信息 + format_info = self._getModelFormat(origin_model) + animation_info = self._analyzeAnimationQuality(actor, anim_names, format_info) + info_text = f"格式: {format_info} | 动画数量: {len(processed_names)}" + if animation_info: + info_text += f" | {animation_info}" + info_label = QLabel(info_text) + info_label.setStyleSheet("color:#888;font-size:10px;") + layout.addWidget(QLabel("信息:"), 0, 0) + layout.addWidget(info_label, 0, 1, 1, 3) + + # 如果是 FBX 且动画有问题,添加转换按钮 + current_row = 1 + if format_info == "FBX" and "⚠️" in animation_info: + convert_btn = QPushButton("🔄 转换FBX动画") + convert_btn.setStyleSheet("background-color:#FFA500;color:white;font-weight:bold;") + convert_btn.clicked.connect(lambda: self._convertFBXManually(origin_model)) + layout.addWidget(QLabel("修复:"), current_row, 0) + layout.addWidget(convert_btn, current_row, 1, 1, 3) + current_row += 1 + + self.animation_combo = QComboBox() + # 使用处理后的名称,但保留原始名称用于播放 + for display_name, original_name in processed_names: + self.animation_combo.addItem(display_name, original_name) + layout.addWidget(QLabel("动画名称:"), current_row, 0) + layout.addWidget(self.animation_combo, current_row, 1, 1, 3) + current_row += 1 + + # btn_box = QWidget() + btn_lay = QHBoxLayout() + for txt, slot in (("播放", self._playAnimation), + ("暂停", self._pauseAnimation), + ("停止", self._stopAnimation), + ("循环", self._loopAnimation)): + btn = QPushButton(txt) + btn.clicked.connect(lambda _, f=slot: f(origin_model)) + btn_lay.addWidget(btn) + layout.addWidget(QLabel("控制:"), current_row, 0) + layout.addLayout(btn_lay, current_row, 1, 1, 3) + current_row += 1 + + self.speed_spinbox = QDoubleSpinBox() + self.speed_spinbox.setRange(0.1, 5.0) + self.speed_spinbox.setSingleStep(0.1) + saved = origin_model.getPythonTag("anim_speed") + self.speed_spinbox.setValue(saved if saved is not None else 1.0) + # self.speed_spinbox.setValue(1.0) + self.speed_spinbox.valueChanged.connect(lambda v: self._setAnimationSpeed(origin_model, v)) + layout.addWidget(QLabel("播放速度:"), current_row, 0) + layout.addWidget(self.speed_spinbox, current_row, 1) + + def _getModelFormat(self, origin_model): + """获取模型格式信息""" + filepath = origin_model.getTag("model_path") + original_path = origin_model.getTag("original_path") + converted_from = origin_model.getTag("converted_from") + + if filepath: + ext = filepath.lower().split('.')[-1] + format_name = ext.upper() + + # 如果是转换后的文件,显示转换信息 + if converted_from and original_path: + original_ext = converted_from.upper() + format_name = f"{format_name} (从{original_ext}转换)" + + return format_name + return "未知" + + def _processAnimationNames(self, origin_model, anim_names): + """处理和分析动画名称,返回 [(显示名称, 原始名称), ...]""" + format_info = self._getModelFormat(origin_model) + processed = [] + + print(f"[动画分析] 格式: {format_info}, 原始动画名称: {anim_names}") + + for name in anim_names: + display_name = name + original_name = name + + if format_info == "GLB": + # GLB 格式通常有真实的动画名称 + if "|" in name: + # 处理类似 'Armature|mixamo.com|Layer0' 的名称 + parts = name.split("|") + if "mixamo" in name.lower(): + # Mixamo 动画 + display_name = f"Mixamo_{parts[-1]}" if len(parts) > 1 else name + elif len(parts) > 2: + # 其他复杂命名 + display_name = f"{parts[0]}_{parts[-1]}" + else: + display_name = parts[-1] + + elif format_info == "FBX": + # FBX 格式可能需要特殊处理 + if self._isLikelyBoneGroup(name): + # 检查是否是骨骼组而非动画 + print(f"[警告] '{name}' 可能不是真正的动画序列,而是骨骼组") + display_name = f"⚠️ {name} (可能非动画)" + else: + display_name = name + + elif format_info in ["EGG", "BAM"]: + # 原生格式通常命名规范 + display_name = name + + processed.append((display_name, original_name)) + print(f"[动画分析] {original_name} → {display_name}") + + return processed + + def _isLikelyBoneGroup(self, name): + """判断动画名称是否更像骨骼组而不是动画序列""" + bone_indicators = ['joints', 'bones', 'skeleton', 'surface', 'mesh', 'beta', 'rig'] + name_lower = name.lower() + + # 如果包含这些关键词,可能是骨骼组 + for indicator in bone_indicators: + if indicator in name_lower: + return True + + # 如果名称太简单(少于3个字符),可能不是动画 + if len(name) < 3: + return True + + return False + + def _analyzeAnimationQuality(self, actor, anim_names, format_info): + """分析动画质量和类型""" + try: + total_frames = 0 + valid_anims = 0 + + for anim_name in anim_names: + try: + control = actor.getAnimControl(anim_name) + if control: + frames = control.getNumFrames() + if frames > 1: + valid_anims += 1 + total_frames += frames + print(f"[动画分析] '{anim_name}': {frames} 帧") + else: + print(f"[动画分析] '{anim_name}': 无有效帧数 ({frames})") + except Exception as e: + print(f"[动画分析] '{anim_name}' 分析失败: {e}") + + if valid_anims == 0: + return "⚠️ 无有效动画序列" + elif valid_anims < len(anim_names): + return f"⚠️ {valid_anims}/{len(anim_names)} 个有效" + else: + avg_frames = total_frames // valid_anims + return f"✓ 平均 {avg_frames} 帧" + + except Exception as e: + print(f"[动画分析] 分析失败: {e}") + return "分析失败" + + def _getActor(self, origin_model): + if origin_model in self._actor_cache: + return self._actor_cache[origin_model] + + # 首先检查是否可以直接从内存中的模型创建Actor + if origin_model.hasTag("can_create_actor_from_memory") and origin_model.getTag("can_create_actor_from_memory").lower() == "true": + try: + print(f"[Actor加载] 直接从内存模型创建Actor: {origin_model.getName()}") + # 直接从内存中的模型创建Actor + test_actor = Actor(origin_model) + anims = test_actor.getAnimNames() + test_actor.reparentTo(self.world.render) + self._actor_cache[origin_model] = test_actor + print(f"[Actor加载] 内存创建检测到动画: {anims}") + if anims: + return test_actor + else: + test_actor.cleanup() + test_actor.removeNode() + except Exception as e: + print(f"从内存模型创建Actor失败: {e}") + + # 如果不能直接从内存创建,再尝试通过文件路径加载 + filepath = origin_model.getTag("model_path") + if not filepath: + return None + + print(f"[Actor加载] 尝试加载: {filepath}") + + # 处理跨平台路径问题 + import os + # 检查路径是否有效,如果无效则尝试修复 + if not os.path.exists(filepath): + original_filepath = filepath + # 尝试多种修复策略 + fixed = False + + import platform + # 策略1: 处理Linux风格路径在Windows上的问题 + if filepath.startswith('/') and platform.system() == "Windows": + print("[路径转换] 尝试处理Linux风格路径:", filepath) + path_parts = filepath.split('/') + print(platform.system()) + if len(path_parts) > 1: + drive_letter = path_parts[1].upper() + ':\\' # 添加反斜杠确保正确路径格式 + remaining_path = '\\'.join(path_parts[2:]) if len(path_parts) > 2 else '' + potential_path = os.path.join(drive_letter, remaining_path) + print(f"[路径转换] 构造的潜在路径: {potential_path}") + if os.path.exists(potential_path): + filepath = potential_path + fixed = True + print(f"[路径转换] 成功: {original_filepath} -> {filepath}") + else: + print(f"[路径转换] 文件不存在: {potential_path}") + + + # 策略2: 处理路径分隔符问题 + if not fixed: + # 尝试规范化路径 + normalized_path = os.path.normpath(filepath) + print(f"[路径规范化] 尝试规范化路径: {filepath} -> {normalized_path}") + if os.path.exists(normalized_path): + filepath = normalized_path + fixed = True + print(f"[路径规范化] 成功: {filepath}") + else: + print(f"[路径规范化] 文件不存在: {normalized_path}") + + # 策略3: 在Resources目录中查找 + if not fixed: + # 尝试在Resources目录中查找文件 + resources_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "Resources") + filename = os.path.basename(filepath) + potential_path = os.path.join(resources_path, filename) + print(f"[Resources查找] 尝试在Resources目录查找: {potential_path}") + if os.path.exists(potential_path): + filepath = potential_path + fixed = True + print(f"[Resources查找] 成功: {filepath}") + else: + print(f"[Resources查找] 文件不存在: {potential_path}") + + if fixed: + print(f"路径修复: {original_filepath} -> {filepath}") + # 更新模型标签 + origin_model.setTag("model_path", filepath) + else: + print(f"[警告] 模型文件不存在: {filepath}") + return None + + # 检查是否是 FBX 文件,如果是,使用专门的 FBX 动画加载器 + if filepath.lower().endswith('.fbx'): + pass + #return self._createFBXActor(origin_model, filepath) + + # 其他格式使用标准 Actor 加载 + try: + import gltf + from panda3d.core import Filename + + # 将Panda3D路径转换为操作系统特定路径 + panda_filename = Filename(filepath) + os_specific_path = panda_filename.to_os_specific() + print(f"[路径转换] {filepath} -> {os_specific_path}") + + print(f"[GLTF加载] 尝试加载: {os_specific_path}") + + # 使用明确的设置确保动画被加载 + gltf_settings = gltf.GltfSettings(skip_animations=False) + # test_actor=Actor(NodePath(gltf._loader.GltfLoader.load_file(filepath,None))) + model_root = gltf.load_model(os_specific_path, gltf_settings) + model_node = NodePath(model_root) + test_actor = Actor(model_node) + anims = test_actor.getAnimNames() + test_actor.reparentTo(self.world.render) + self._actor_cache[origin_model] = test_actor + print(f"[Actor加载] 标准加载检测到动画: {anims}") + if not anims: + test_actor.cleanup() + test_actor.removeNode() + return None + return test_actor + except Exception as e: + print(f"创建Actor失败: {e}") + return None + + def _createFBXActor(self, origin_model, filepath): + """专门为 FBX 文件创建 Actor,使用转换方式获取真实动画""" + try: + print(f"[FBX动画] 开始处理 FBX 动画: {filepath}") + + # 方法1: 尝试转换 FBX 为包含动画的格式 + converted_actor = self._convertFBXToActor(filepath) + if converted_actor: + converted_actor.reparentTo(self.world.render) + self._actor_cache[origin_model] = converted_actor + print(f"[FBX动画] 转换成功,动画: {converted_actor.getAnimNames()}") + return converted_actor + + # 方法2: 直接加载但进行动画数据修复 + actor = Actor(filepath) + if actor: + fixed_actor = self._fixFBXAnimations(actor, filepath) + if fixed_actor and fixed_actor.getAnimNames(): + fixed_actor.reparentTo(self.world.render) + self._actor_cache[origin_model] = fixed_actor + print(f"[FBX动画] 修复成功,动画: {fixed_actor.getAnimNames()}") + return fixed_actor + + print(f"[FBX动画] 无法获取有效动画数据") + return None + + except Exception as e: + print(f"[FBX动画] 处理失败: {e}") + return None + + def _convertFBXToActor(self, fbx_path): + """将 FBX 转换为可用的 Actor""" + try: + import tempfile + import os + + # 创建临时文件用于转换 + temp_dir = tempfile.mkdtemp() + egg_path = os.path.join(temp_dir, "converted.egg") + + print(f"[FBX转换] 转换 {fbx_path} 到 {egg_path}") + + # 使用 Panda3D 转换工具链 + # FBX -> Collada -> EGG + try: + # 检查是否有可用的转换工具 + import subprocess + + # 方法1: 尝试直接使用 assimp (如果安装了) + result = subprocess.run([ + 'assimp', 'export', fbx_path, egg_path + ], capture_output=True, text=True, timeout=30) + + if result.returncode == 0 and os.path.exists(egg_path): + actor = Actor(egg_path) + if actor.getAnimNames(): + print(f"[FBX转换] Assimp 转换成功") + return actor + + except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError): + print(f"[FBX转换] Assimp 转换失败,尝试其他方法") + + # 方法2: 使用 Blender 脚本转换(如果安装了 Blender) + try: + blender_script = f''' +import bpy +import os +bpy.ops.wm.read_factory_settings(use_empty=True) +bpy.ops.import_scene.fbx(filepath="{fbx_path}") +bpy.ops.export_scene.gltf(filepath="{egg_path.replace('.egg', '.gltf')}", export_animations=True) +''' + + script_path = os.path.join(temp_dir, "convert.py") + with open(script_path, 'w') as f: + f.write(blender_script) + + result = subprocess.run([ + 'blender', '--background', '--python', script_path + ], capture_output=True, text=True, timeout=60) + + gltf_path = egg_path.replace('.egg', '.gltf') + if os.path.exists(gltf_path): + # 使用 gltf2bam 转换为 BAM + subprocess.run(['gltf2bam', gltf_path, egg_path.replace('.egg', '.bam')]) + bam_path = egg_path.replace('.egg', '.bam') + if os.path.exists(bam_path): + actor = Actor(bam_path) + if actor.getAnimNames(): + print(f"[FBX转换] Blender 转换成功") + return actor + + except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError): + print(f"[FBX转换] Blender 转换失败") + + return None + + except Exception as e: + print(f"[FBX转换] 转换过程出错: {e}") + return None + + def _fixFBXAnimations(self, actor, fbx_path): + """修复 FBX Actor 的动画数据""" + try: + print(f"[FBX修复] 尝试修复动画数据") + + # 获取所有动画名称 + anim_names = actor.getAnimNames() + print(f"[FBX修复] 原始动画名称: {anim_names}") + + # 检查每个动画是否真的有动画数据 + valid_anims = [] + for anim_name in anim_names: + try: + control = actor.getAnimControl(anim_name) + if control and control.getNumFrames() > 1: + valid_anims.append(anim_name) + print(f"[FBX修复] 有效动画: {anim_name} ({control.getNumFrames()} 帧)") + else: + print(f"[FBX修复] 无效动画: {anim_name} (帧数: {control.getNumFrames() if control else 0})") + except: + print(f"[FBX修复] 无法获取动画控制: {anim_name}") + + if valid_anims: + print(f"[FBX修复] 找到 {len(valid_anims)} 个有效动画") + return actor + else: + print(f"[FBX修复] 没有找到有效动画,尝试重新解析") + + # 尝试重新加载和分析 FBX 文件结构 + return self._deepAnalyzeFBX(fbx_path) + + except Exception as e: + print(f"[FBX修复] 修复失败: {e}") + return None + + def _deepAnalyzeFBX(self, fbx_path): + """深度分析 FBX 文件并尝试提取动画""" + try: + print(f"[FBX深度分析] 分析文件: {fbx_path}") + + # 尝试直接加载为模型,然后手动查找动画节点 + from panda3d.core import Loader + + loader = Loader.getGlobalPtr() + model = loader.loadSync(fbx_path) + + if model: + print(f"[FBX深度分析] 成功加载模型") + + # 查找动画相关节点 + anim_nodes = model.findAllMatches("**/+AnimBundleNode") + char_nodes = model.findAllMatches("**/+CharacterNode") + + print(f"[FBX深度分析] AnimBundleNode: {anim_nodes.getNumPaths()}") + print(f"[FBX深度分析] CharacterNode: {char_nodes.getNumPaths()}") + + if not char_nodes.isEmpty(): + # 尝试基于 CharacterNode 创建 Actor + char_node = char_nodes.getPath(0) + character = char_node.node().getCharacter() + + if character: + # 创建新的 Actor 实例并绑定角色 + actor = Actor() + actor.instance(model, "character") + + # 检查是否有动画 + if actor.getAnimNames(): + print(f"[FBX深度分析] 成功提取动画: {actor.getAnimNames()}") + return actor + + return None + + except Exception as e: + print(f"[FBX深度分析] 分析失败: {e}") + return None + + def _convertFBXManually(self, origin_model): + """手动转换 FBX 动画""" + from PyQt5.QtWidgets import QMessageBox, QProgressDialog + from PyQt5.QtCore import QTimer + + try: + filepath = origin_model.getTag("model_path") + if not filepath or not filepath.lower().endswith('.fbx'): + return + + # 显示进度对话框 + progress = QProgressDialog("正在转换FBX动画...", "取消", 0, 100) + progress.setWindowTitle("FBX动画转换") + progress.show() + + print(f"[手动转换] 开始转换: {filepath}") + + # 尝试使用系统转换工具 + converted_path = self._systemConvertFBX(filepath, progress) + + if converted_path: + # 重新加载转换后的模型 + progress.setLabelText("重新加载模型...") + progress.setValue(80) + + # 清除缓存 + if origin_model in self._actor_cache: + del self._actor_cache[origin_model] + + # 更新模型路径标签 + origin_model.setTag("model_path", converted_path) + + progress.setValue(100) + progress.hide() + + # 显示成功消息 + QMessageBox.information(None, "转换成功", + f"FBX动画转换成功!\n请重新选择模型查看动画。") + + print(f"[手动转换] 转换完成: {converted_path}") + + else: + progress.hide() + + # 显示转换选项 + msg = QMessageBox() + msg.setWindowTitle("转换建议") + msg.setText("自动转换失败,建议使用以下方法:") + msg.setDetailedText(""" +1. 使用 Blender 转换: + - 打开 Blender + - 导入 FBX 文件 + - 导出为 glTF (.gltf) 格式,确保选择"包含动画" + +2. 使用命令行工具: + - gltf2bam your_file.gltf your_file.bam + +3. 检查原始 FBX 文件: + - 确保 FBX 文件确实包含动画数据 + - 尝试在其他软件中验证动画 + """) + msg.exec_() + + except Exception as e: + print(f"[手动转换] 转换失败: {e}") + QMessageBox.warning(None, "转换失败", f"转换过程中出现错误: {e}") + + def _systemConvertFBX(self, fbx_path, progress=None): + """使用系统工具转换 FBX""" + import os + import subprocess + import tempfile + + try: + # 准备输出路径 + base_name = os.path.splitext(os.path.basename(fbx_path))[0] + output_dir = os.path.dirname(fbx_path) + gltf_path = os.path.join(output_dir, f"{base_name}_converted.gltf") + bam_path = os.path.join(output_dir, f"{base_name}_converted.bam") + + if progress: + progress.setValue(20) + progress.setLabelText("检查转换工具...") + + # 方法1: 使用 gltf2bam 的逆向功能(如果支持) + try: + # 首先尝试看看是否有直接的 FBX 支持 + result = subprocess.run(['gltf2bam', '--help'], + capture_output=True, text=True, timeout=10) + print(f"[系统转换] gltf2bam 可用") + except: + print(f"[系统转换] gltf2bam 不可用") + + if progress: + progress.setValue(40) + progress.setLabelText("尝试 Blender 转换...") + + # 方法2: 使用 Blender 无头模式转换 + try: + # 创建 Blender 转换脚本 + script_content = f''' +import bpy +import sys +import os + +# 清理默认场景 +bpy.ops.object.select_all(action='SELECT') +bpy.ops.object.delete(use_global=False) + +# 导入 FBX +try: + bpy.ops.import_scene.fbx(filepath="{fbx_path}") + print("FBX导入成功") + + # 导出为 glTF + bpy.ops.export_scene.gltf( + filepath="{gltf_path}", + export_animations=True, + export_force_sampling=True, + export_frame_range=True + ) + print("glTF导出成功") + +except Exception as e: + print(f"转换失败: {{e}}") + sys.exit(1) +''' + + temp_script = tempfile.mktemp(suffix='.py') + with open(temp_script, 'w') as f: + f.write(script_content) + + if progress: + progress.setValue(60) + progress.setLabelText("执行 Blender 转换...") + + # 执行 Blender 转换 + result = subprocess.run([ + 'blender', '--background', '--python', temp_script + ], capture_output=True, text=True, timeout=120) + + # 清理临时文件 + if os.path.exists(temp_script): + os.remove(temp_script) + + if result.returncode == 0 and os.path.exists(gltf_path): + if progress: + progress.setValue(80) + progress.setLabelText("转换为 BAM 格式...") + + # 转换 glTF 为 BAM + result2 = subprocess.run(['gltf2bam', gltf_path, bam_path], + capture_output=True, text=True, timeout=60) + + if result2.returncode == 0 and os.path.exists(bam_path): + print(f"[系统转换] 成功转换为: {bam_path}") + return bam_path + elif os.path.exists(gltf_path): + print(f"[系统转换] 成功转换为: {gltf_path}") + return gltf_path + + except subprocess.TimeoutExpired: + print(f"[系统转换] Blender 转换超时") + except FileNotFoundError: + print(f"[系统转换] Blender 未安装") + except Exception as e: + print(f"[系统转换] Blender 转换出错: {e}") + + return None + + except Exception as e: + print(f"[系统转换] 系统转换失败: {e}") + return None + + def _playAnimation(self, origin_model): + actor = self._getActor(origin_model) + if not actor: + return + + original_world_pos = origin_model.getPos(self.world.render) + original_world_hpr = origin_model.getHpr(self.world.render) + original_world_scale = origin_model.getScale(self.world.render) + actor.setPos(origin_model.getPos()) + actor.setHpr(origin_model.getHpr()) + actor.setScale(origin_model.getScale()) + + origin_model.hide() + actor.show() + + #创建人物来维持世界坐标不变 + def maintainWorldPosition(task): + try: + if not actor.isEmpty(): + actor.setPos(self.world.render,original_world_pos) + actor.setHpr(self.world.render,original_world_hpr) + actor.setScale(self.world.render,original_world_scale) + return task.cont + else: + return task.done + except: + return task.done + + taskMgr.add(maintainWorldPosition,f"maintain_anim_pos_{id(actor)}") + + if hasattr(self, 'animation_combo'): + # 获取原始动画名称(存储在 userData 中) + current_index = self.animation_combo.currentIndex() + if current_index >= 0: + original_name = self.animation_combo.itemData(current_index) + display_name = self.animation_combo.currentText() + if original_name: + actor.play(original_name) + print(f"『动画播放』:{display_name} (原始名称: {original_name})") + else: + # 兜底:使用显示名称 + actor.play(display_name) + print(f"『动画播放』:{display_name}") + + def _pauseAnimation(self, origin_model): + actor = self._getActor(origin_model) + if not actor: + return + actor.setPos(origin_model.getPos()) + actor.setHpr(origin_model.getHpr()) + actor.setScale(origin_model.getScale()) + + origin_model.hide() + actor.show() + actor.stop() + print("『动画』暂停") + + def _stopAnimation(self, origin_model): + actor = self._getActor(origin_model) + if not actor: + return + actor.stop() + + # 获取原始动画名称 + current_index = self.animation_combo.currentIndex() + if current_index >= 0: + original_name = self.animation_combo.itemData(current_index) + display_name = self.animation_combo.currentText() + anim_name = original_name if original_name else display_name + + if anim_name and actor.getAnimControl(anim_name): + actor.getAnimControl(anim_name).pose(0) + actor.hide() + origin_model.show() + print("『动画』停止切换至原始模型") + + def _loopAnimation(self, origin_model): + actor = self._getActor(origin_model) + if not actor: + return + + actor.setPos(origin_model.getPos()) + actor.setHpr(origin_model.getHpr()) + actor.setScale(origin_model.getScale()) + + origin_model.hide() + actor.show() + + # 获取原始动画名称 + current_index = self.animation_combo.currentIndex() + if current_index >= 0: + original_name = self.animation_combo.itemData(current_index) + display_name = self.animation_combo.currentText() + anim_name = original_name if original_name else display_name + + if anim_name: + actor.loop(anim_name) + print(f"[动画] 循环: {display_name} (原始名称: {anim_name})") + + def _setAnimationSpeed(self, origin_model, speed): + """ + 设置当前动画的播放倍速。 + """ + actor = self._getActor(origin_model) + if not actor: + return + # 获取原始动画名称 + current_index = self.animation_combo.currentIndex() + + if current_index >= 0: + original_name = self.animation_combo.itemData(current_index) + display_name = self.animation_combo.currentText() + anim_name = original_name if original_name else display_name + + if anim_name: + actor.setPlayRate(speed, anim_name) + origin_model.setPythonTag("anim_speed", speed) + print(f"[动画] 速度设为: {speed} ({display_name})") + + def _dispatchAnimCommand(self, origin_model, cmd): + cache = self._actor_cache.get(origin_model) + if not cache: + return + kind, player = cache + + if kind == "actor": + actor = player + anim_name = self.animation_combo.currentText() + actor.setPos(origin_model.getPos()) + actor.setHpr(origin_model.getHpr()) + actor.setScale(origin_model.getScale() / 100) + + if cmd == "play": + origin_model.hide() + actor.show() + actor.play(anim_name) + elif cmd == "pause": + origin_model.hide() + actor.show() + actor.stop() + elif cmd == "stop": + actor.stop() + if anim_name and actor.getAnimControl(anim_name): + actor.getAnimControl(anim_name).pose(0) + actor.hide(); + origin_model.show() + elif cmd == "loop": + origin_model.hide() + actor.show() + actor.loop(anim_name) + elif isinstance(cmd, tuple) and cmd[0] == "speed": + actor.setPlayRate(cmd[1], anim_name) + + def removeActorForModel(self, model): + """删除 model 对应的 Actor(如果存在)""" + actor = self._actor_cache.pop(model, None) + if actor: + actor.stop() + actor.cleanup() + actor.removeNode() + + def _addCollisionPanel(self, model): + """添加碰撞检测面板""" + try: + # 创建碰撞检测组 + collision_group = QGroupBox("碰撞检测") + collision_layout = QGridLayout() + collision_layout.setColumnMinimumWidth(0, self.column_minimum_width) + # collision_layout.setColumnStretch(0, 0) + # collision_layout.setColumnStretch(1, 1) + # collision_layout.setColumnStretch(2, 1) + # collision_layout.setColumnStretch(3, 1) + + # 检查模型是否已有碰撞 + has_collision = self._hasCollision(model) + + # 创建主容器 + main_container = QWidget() + main_layout = QVBoxLayout(main_container) + main_layout.setContentsMargins(8, 8, 8, 8) + main_layout.setSpacing(12) + + # 状态和形状选择区域 + header_container = QWidget() + header_layout = QGridLayout(header_container) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.setSpacing(8) + + # 调整列宽,保证控件对齐 + # collision_layout.setColumnStretch(0, 0) + # collision_layout.setColumnStretch(1, 0) + # collision_layout.setColumnStretch(2, 0) + # collision_layout.setColumnStretch(3, 0) + + # 碰撞状态行 + status_layout = QHBoxLayout() + status_label = QLabel("状态:") + status_layout.addWidget(status_label) + + # 状态徽章(使用固定宽度样式) + if has_collision: + self.collision_status_badge = self.createFixedStatusBadge("已启用", "green") + else: + self.collision_status_badge = self.createFixedStatusBadge("未启用", "red") + status_layout.addWidget(self.collision_status_badge) + collision_layout.addLayout(status_layout, 0, 0, 1, 1) + + # 形状选择标签(始终显示) + self.collision_shape_label = QLabel("碰撞形状:") + collision_layout.addWidget(self.collision_shape_label, 1, 0) + + # 形状选择下拉框(始终显示) + self.collision_shape_combo = QComboBox() + self.collision_shape_combo.addItems([ + "球形 (Sphere)", + "盒型 (Box)", + "胶囊体 (Capsule)", + "平面 (Plane)", + "自动选择 (Auto)" + ]) + collision_layout.addWidget(self.collision_shape_combo, 1, 1, 1, 3) + + # 保存布局引用,用于动态添加/移除控件 + self.collision_layout = collision_layout + self.collision_group = collision_group + + current_row = 2 # 下一行的索引 + + # 显示/隐藏切换按钮(只有有碰撞时才显示) + if has_collision: + # 检查碰撞的当前可见性 + is_collision_visible = self._isCollisionVisible(model) + + # 显示当前碰撞类型并设置为只读 + current_shape = self._getCurrentCollisionShape(model) + self._setComboToShape(current_shape) + self.collision_shape_combo.setEnabled(False) + + # 添加碰撞参数调整控件 + current_row = self._addCollisionParameterControls(model, collision_layout, current_row, current_shape) + + # 显示/隐藏切换按钮(使用现代化样式) + visibility_text = "隐藏碰撞" if is_collision_visible else "显示碰撞" + self.collision_visibility_button = self.createModernButton(visibility_text) + self.collision_visibility_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.collision_visibility_button.setStyleSheet(self._collision_button_style()) + self.collision_visibility_button.clicked.connect(lambda: self._toggleCollisionVisibility(model)) + collision_layout.addWidget(self.collision_visibility_button, current_row, 0, 1, 4) + current_row += 1 + + # 移除碰撞按钮(使用现代化样式) + self.collision_button = self.createModernButton("移除碰撞") + self.collision_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.collision_button.setStyleSheet(self._collision_button_style()) + self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) + collision_layout.addWidget(self.collision_button, current_row, 0, 1, 4) + else: + # 如果没有碰撞,设置默认选择并允许编辑 + self.collision_shape_combo.setCurrentText("球形 (Sphere)") + self.collision_shape_combo.setEnabled(True) + + # 清理之前的参数控件 + self._clearCollisionParameterControls() + + # 隐藏显示/隐藏按钮(如果存在) + if hasattr(self, 'collision_visibility_button'): + self.collision_visibility_button.setVisible(False) + + # 添加碰撞按钮(使用现代化样式) + self.collision_button = self.createModernButton("添加碰撞") + self.collision_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.collision_button.setStyleSheet(self._collision_button_style()) + self.collision_button.clicked.connect(lambda: self._addCollisionAndUpdate(model)) + collision_layout.addWidget(self.collision_button, current_row, 0, 1, 4) + collision_group.setLayout(collision_layout) + self._propertyLayout.addWidget(collision_group) + + # 同步一次状态,确保主按钮与可见性按钮齐全 + try: + self._updateCollisionPanelState(model) + except Exception: + pass + + except Exception as e: + print(f"创建碰撞面板失败: {e}") + import traceback + traceback.print_exc() + + def _addCollisionParameterControls(self, model, layout, start_row, shape_type): + """添加碰撞参数调整控件""" + try: + current_row = start_row + + # 位置调整控件(所有类型都有) + pos_label = QLabel("位置偏移:") + pos_label.setAlignment(Qt.AlignLeft | Qt.AlignTop) + pos_label.setVisible(True) # 确保可见 + + # X, Y, Z 位置调整 + self.collision_pos_x = self._createCollisionSpinBox(-100, 100, 2) + self.collision_pos_y = self._createCollisionSpinBox(-100, 100, 2) + self.collision_pos_z = self._createCollisionSpinBox(-100, 100, 2) + + for spinbox in (self.collision_pos_x, self.collision_pos_y, self.collision_pos_z): + spinbox.setMinimumWidth(140) + spinbox.setVisible(True) + + axis_controls = [ + ("X:", self.collision_pos_x), + ("Y:", self.collision_pos_y), + ("Z:", self.collision_pos_z), + ] + + for offset, (axis_text, spinbox) in enumerate(axis_controls): + row = current_row + offset + axis_layout = QHBoxLayout() + if offset == 0: + layout.addWidget(pos_label, row, 0) + # axis_label.addWidget(pos_label) + else: + spacer = QLabel("") + spacer.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + spacer.setVisible(True) + layout.addWidget(spacer, row, 0) + # axis_layout.addWidget(spacer) + + axis_label = QLabel(axis_text) + axis_label.setAlignment(Qt.AlignVCenter) + axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + axis_layout.addWidget(axis_label) + axis_layout.addWidget(spinbox) + # layout.addWidget(axis_label, row, 1) + # layout.addWidget(spinbox, row, 2, 1, 2) + layout.addLayout(axis_layout, row, 1, 1, 3) + + current_row += len(axis_controls) + + # 连接位置变化信号 + self.collision_pos_x.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'x', v)) + self.collision_pos_y.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'y', v)) + self.collision_pos_z.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'z', v)) + + # 根据形状类型添加特定参数 + if shape_type == 'sphere': + current_row = self._addSphereParameters(model, layout, current_row) + elif shape_type == 'box': + current_row = self._addBoxParameters(model, layout, current_row) + elif shape_type == 'capsule': + current_row = self._addCapsuleParameters(model, layout, current_row) + elif shape_type == 'plane': + current_row = self._addPlaneParameters(model, layout, current_row) + + # 获取并设置当前参数值 + self._loadCurrentCollisionParameters(model, shape_type) + + return current_row + + except Exception as e: + print(f"添加碰撞参数控件失败: {e}") + return start_row + + def _createCollisionSpinBox(self, min_val, max_val, decimals=2): + """创建碰撞参数调整用的SpinBox""" + spinbox = QDoubleSpinBox() + spinbox.setRange(min_val, max_val) + spinbox.setDecimals(decimals) + spinbox.setSingleStep(0.01) + return spinbox + + def _addSphereParameters(self, model, layout, start_row): + """添加球形碰撞参数""" + current_row = start_row + + # 半径调整 + radius_label = QLabel("半径:") + layout.addWidget(radius_label, current_row, 0) + # layout.addWidget(QLabel(""), current_row, 1) + + self.collision_radius = self._createCollisionSpinBox(0.01, 100000, 2) + + # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 + if not self._hasCollision(model): + # 设置基于模型变换后尺寸的默认值 + if hasattr(self.world, 'collision_manager'): + transformed_info = self.world.collision_manager._getTransformedModelInfo(model) + if transformed_info: + default_radius = transformed_info['radius'] + self.collision_radius.setValue(default_radius) + else: + # 回退到原始包围盒 + bounds = model.getBounds() + if not bounds.isEmpty(): + default_radius = bounds.getRadius() + self.collision_radius.setValue(default_radius) + + self.collision_radius.valueChanged.connect(lambda v: self._updateSphereRadius(model, v)) + layout.addWidget(self.collision_radius, current_row, 1, 1, 3) + current_row += 1 + + return current_row + + def _addBoxParameters(self, model, layout, start_row): + """添加盒型碰撞参数""" + current_row = start_row + + size_label = QLabel("尺寸:") + layout.addWidget(size_label, current_row, 0) + # layout.addWidget(QLabel(""), current_row, 1) + # current_row += 1 + + # 宽度、长度、高度 + self.collision_width = self._createCollisionSpinBox(0.001, 100000, 2) + self.collision_length = self._createCollisionSpinBox(0.001, 100000, 2) + self.collision_height = self._createCollisionSpinBox(0.001, 100000, 2) + + # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 + if not self._hasCollision(model): + # 设置基于模型变换后尺寸的默认值 + if hasattr(self.world, 'collision_manager'): + transformed_info = self.world.collision_manager._getTransformedModelInfo(model) + if transformed_info: + actual_size = transformed_info['size'] + self.collision_width.setValue(actual_size.x) + self.collision_length.setValue(actual_size.y) + self.collision_height.setValue(actual_size.z) + else: + # 回退到原始包围盒 + bounds = model.getBounds() + if not bounds.isEmpty(): + model_size = bounds.getMax() - bounds.getMin() + self.collision_width.setValue(model_size.x) + self.collision_length.setValue(model_size.y) + self.collision_height.setValue(model_size.z) + + box_controls = [ + ("宽度:", self.collision_width), + ("长度:", self.collision_length), + ("高度:", self.collision_height), + ] + + for offset, (text, spinbox) in enumerate(box_controls): + text_layout = QHBoxLayout() + if offset == 0: + layout.addWidget(size_label, current_row, 0) + else: + spacer = QLabel("") + spacer.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + spacer.setVisible(True) + layout.addWidget(spacer, current_row, 0) + # placeholder = QLabel("") + # layout.addWidget(placeholder, current_row, 0) + + axis_label = QLabel(text) + axis_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + text_layout.addWidget(axis_label) + text_layout.addWidget(spinbox) + layout.addLayout(text_layout, current_row, 1, 1, 3) + # layout.addWidget(axis_label, current_row, 1) + # layout.addWidget(spinbox, current_row, 2) + current_row += 1 + + # 连接信号 + self.collision_width.valueChanged.connect(lambda v: self._updateBoxSize(model, 'width', v)) + self.collision_length.valueChanged.connect(lambda v: self._updateBoxSize(model, 'length', v)) + self.collision_height.valueChanged.connect(lambda v: self._updateBoxSize(model, 'height', v)) + + return current_row + + def _addCapsuleParameters(self, model, layout, start_row): + """添加胶囊体碰撞参数""" + current_row = start_row + + # 半径和高度 + radius_label = QLabel("半径:") + layout.addWidget(radius_label, current_row, 0) + # layout.addWidget(QLabel(""), current_row, 1) + + self.collision_capsule_radius = self._createCollisionSpinBox(0.01, 100000, 2) + + # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 + if not self._hasCollision(model): + # 设置基于模型变换后尺寸的默认值 + if hasattr(self.world, 'collision_manager'): + transformed_info = self.world.collision_manager._getTransformedModelInfo(model) + if transformed_info: + actual_size = transformed_info['size'] + # 更合理的默认半径:基于变换后模型宽度的平均值 + default_radius = min(actual_size.x, actual_size.y) / 2.5 + self.collision_capsule_radius.setValue(default_radius) + else: + # 回退到原始包围盒 + bounds = model.getBounds() + if not bounds.isEmpty(): + model_size = bounds.getMax() - bounds.getMin() + # 更合理的默认半径:基于模型宽度的平均值 + default_radius = min(model_size.x, model_size.y) / 2.5 + self.collision_capsule_radius.setValue(default_radius) + + self.collision_capsule_radius.valueChanged.connect(lambda v: self._updateCapsuleRadius(model, v)) + layout.addWidget(self.collision_capsule_radius, current_row, 1, 1, 3) + current_row += 1 + + height_label = QLabel("高度:") + layout.addWidget(height_label, current_row, 0) + # layout.addWidget(QLabel(""), current_row, 1) + + self.collision_capsule_height = self._createCollisionSpinBox(0.01, 10000, 2) + + # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 + if not self._hasCollision(model): + # 设置基于模型变换后高度的默认值 + if hasattr(self.world, 'collision_manager'): + transformed_info = self.world.collision_manager._getTransformedModelInfo(model) + if transformed_info: + actual_size = transformed_info['size'] + self.collision_capsule_height.setValue(actual_size.z) + else: + # 回退到原始包围盒 + bounds = model.getBounds() + if not bounds.isEmpty(): + model_size = bounds.getMax() - bounds.getMin() + self.collision_capsule_height.setValue(model_size.z) + + self.collision_capsule_height.valueChanged.connect(lambda v: self._updateCapsuleHeight(model, v)) + layout.addWidget(self.collision_capsule_height, current_row, 1, 1, 3) + current_row += 1 + + return current_row + + def _addPlaneParameters(self, model, layout, start_row): + """添加平面碰撞参数""" + current_row = start_row + + # 法向量 + normal_label = QLabel("法向量:") + layout.addWidget(normal_label, current_row, 0) + # layout.addWidget(QLabel(""), current_row, 1) + # current_row += 1 + + self.collision_normal_x = self._createCollisionSpinBox(-1, 1, 2) + self.collision_normal_y = self._createCollisionSpinBox(-1, 1, 2) + self.collision_normal_z = self._createCollisionSpinBox(-1, 1, 2) + + # 只在没有现有碰撞时设置默认值,否则由_loadCurrentCollisionParameters加载实际值 + if not self._hasCollision(model): + # 设置默认法向量(向上) + self.collision_normal_x.setValue(0.0) + self.collision_normal_y.setValue(0.0) + self.collision_normal_z.setValue(1.0) + + # placeholder_nx = QLabel("") + # layout.addWidget(placeholder_nx, current_row, 0) + + hbox_nx = QHBoxLayout() + axis_label_nx = QLabel("Nx:") + axis_label_nx.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + axis_label_nx.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + hbox_nx.addWidget(axis_label_nx) + hbox_nx.addWidget(self.collision_normal_x) + layout.addLayout(hbox_nx, current_row, 1, 1, 3) + current_row += 1 + + placeholder_ny = QLabel("") + layout.addWidget(placeholder_ny, current_row, 0) + + hbox_ny = QHBoxLayout() + axis_label_ny = QLabel("Ny:") + axis_label_ny.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + axis_label_ny.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + hbox_ny.addWidget(axis_label_ny) + hbox_ny.addWidget(self.collision_normal_y) + layout.addLayout(hbox_ny, current_row, 1, 1, 3) + current_row += 1 + + placeholder_nz = QLabel("") + layout.addWidget(placeholder_nz, current_row, 0) + + hbox_nz = QHBoxLayout() + axis_label_nz = QLabel("Nz:") + axis_label_nz.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + axis_label_nz.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + hbox_nz.addWidget(axis_label_nz) + hbox_nz.addWidget(self.collision_normal_z) + layout.addLayout(hbox_nz, current_row, 1, 1, 3) + current_row += 1 + + # 连接信号 + self.collision_normal_x.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'x', v)) + self.collision_normal_y.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'y', v)) + self.collision_normal_z.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'z', v)) + + return current_row + + def _hasCollision(self, model): + """检查模型是否已有碰撞体""" + try: + from panda3d.core import CollisionNode + + # 检查模型及其子节点是否有碰撞节点 + collision_nodes = model.findAllMatches("**/+CollisionNode") + has_collision = collision_nodes.getNumPaths() > 0 + + print( + f"碰撞检查:模型 {model.getName()} - {'有' if has_collision else '无'}碰撞 (找到{collision_nodes.getNumPaths()}个碰撞节点)") + + return has_collision + except Exception as e: + print(f"检查碰撞失败: {e}") + return False + + def _isCollisionVisible(self, model): + """检查碰撞是否可见""" + try: + collision_nodes = model.findAllMatches("**/+CollisionNode") + for collision_np in collision_nodes: + # 检查碰撞节点是否隐藏 + return not collision_np.isHidden() + return False + except Exception as e: + print(f"检查碰撞可见性失败: {e}") + return False + + def _toggleCollisionVisibility(self, model): + """切换碰撞可见性""" + try: + collision_nodes = model.findAllMatches("**/+CollisionNode") + is_visible = self._isCollisionVisible(model) + + for collision_np in collision_nodes: + if is_visible: + collision_np.hide() + print(f"隐藏碰撞:{model.getName()}") + else: + collision_np.show() + print(f"显示碰撞:{model.getName()}") + + # 立即更新按钮状态,并同步更新面板(确保缺失主按钮时补建) + self._updateCollisionVisibilityButton(model) + self._updateCollisionPanelState(model) + + except Exception as e: + print(f"切换碰撞可见性失败: {e}") + + def _updateCollisionVisibilityButton(self, model): + """更新碰撞可见性按钮状态""" + try: + if hasattr(self, 'collision_visibility_button'): + is_visible = self._isCollisionVisible(model) + button_text = "隐藏碰撞" if is_visible else "显示碰撞" + self.collision_visibility_button.setText(button_text) + + # 应用统一按钮样式 + self.collision_visibility_button.setStyleSheet(self._collision_button_style()) + + print(f"更新可见性按钮:{model.getName()} - {'可见' if is_visible else '隐藏'}") + except Exception as e: + print(f"更新碰撞可见性按钮失败: {e}") + + def _getCurrentCollisionShape(self, model): + """获取当前模型的碰撞形状类型""" + try: + from panda3d.core import CollisionNode, CollisionSphere, CollisionBox, CollisionCapsule, CollisionPlane + + collision_nodes = model.findAllMatches("**/+CollisionNode") + for collision_np in collision_nodes: + collision_node = collision_np.node() + if collision_node.getNumSolids() > 0: + solid = collision_node.getSolid(0) + solid_type = type(solid).__name__ + + if solid_type == "CollisionSphere": + return "sphere" + elif solid_type == "CollisionBox": + return "box" + elif solid_type == "CollisionCapsule": + return "capsule" + elif solid_type == "CollisionPlane": + return "plane" + + return "sphere" # 默认返回球形 + except Exception as e: + print(f"获取碰撞形状失败: {e}") + return "sphere" + + def _setComboToShape(self, shape_type): + """根据形状类型设置下拉框选择""" + shape_map = { + "sphere": "球形 (Sphere)", + "box": "盒型 (Box)", + "capsule": "胶囊体 (Capsule)", + "plane": "平面 (Plane)", + "auto": "自动选择 (Auto)" + } + + if shape_type in shape_map: + self.collision_shape_combo.setCurrentText(shape_map[shape_type]) + else: + self.collision_shape_combo.setCurrentText("球形 (Sphere)") + + def _getSelectedCollisionShape(self): + """获取选中的碰撞形状类型""" + if hasattr(self, 'collision_shape_combo'): + shape_text = self.collision_shape_combo.currentText() + # 从显示文本中提取形状类型 + if "球形" in shape_text: + return 'sphere' + elif "盒型" in shape_text: + return 'box' + elif "胶囊体" in shape_text: + return 'capsule' + elif "平面" in shape_text: + return 'plane' + elif "自动选择" in shape_text: + return 'auto' + return 'sphere' # 默认返回球形 + + def _addCollisionAndUpdate(self, model): + """添加指定形状的碰撞体并更新界面""" + try: + # 防止重复调用 + if getattr(self, '_adding_collision', False): + print("正在添加碰撞,跳过重复调用") + return + + self._adding_collision = True + + # 初始化加载参数标志位 + if not hasattr(self, '_loading_collision_params'): + self._loading_collision_params = False + + if hasattr(self.world, 'scene_manager'): + # 获取选中的碰撞形状 + shape_type = self._getSelectedCollisionShape() + + # 参考scene_manager的setupCollision方法 + from panda3d.core import CollisionNode, BitMask32 + + # 创建碰撞节点 + cNode = CollisionNode(f'modelCollision_{model.getName()}') + + # 设置碰撞掩码 + cNode.setIntoCollideMask(BitMask32.bit(2)) # 用于鼠标选择 + + # 如果启用了模型间碰撞检测,添加额外的掩码 + if (hasattr(self.world, 'collision_manager') and + hasattr(self.world.collision_manager, 'model_collision_enabled') and + self.world.collision_manager.model_collision_enabled): + current_mask = cNode.getIntoCollideMask() + model_collision_mask = BitMask32.bit(6) # MODEL_COLLISION + cNode.setIntoCollideMask(current_mask | model_collision_mask) + + # 创建指定形状的碰撞体 + if hasattr(self.world, 'collision_manager'): + collision_shape = self.world.collision_manager.createCollisionShape(model, shape_type) + else: + # 回退方案:创建简单球体 + from panda3d.core import CollisionSphere, Point3 + bounds = model.getBounds() + if bounds.isEmpty(): + collision_shape = CollisionSphere(Point3(0, 0, 0), 1.0) + else: + center = bounds.getCenter() + radius = bounds.getRadius() + if radius <= 0: + radius = 1.0 + collision_shape = CollisionSphere(center, radius) + + cNode.addSolid(collision_shape) + + # 将碰撞节点附加到模型上 + cNodePath = model.attachNewNode(cNode) + + # 根据调试设置决定是否显示碰撞体 + if hasattr(self.world, 'debug_collision') and self.world.debug_collision: + cNodePath.show() + else: + cNodePath.hide() + + # 为模型添加碰撞相关标签 + model.setTag("has_collision", "true") + model.setTag("collision_shape", shape_type) + if 'radius' in locals(): + model.setTag("collision_radius", str(radius)) + + print(f"✅ 为模型 {model.getName()} 添加了 {shape_type} 碰撞体") + + # 重置更新标志,确保状态能够更新 + self._updating_collision_panel = False + + # 更新面板状态 - 添加参数控件和显示/隐藏按钮 + self._updateCollisionPanelState(model) + + else: + print("场景管理器未初始化") + + except Exception as e: + print(f"添加碰撞失败: {e}") + import traceback + traceback.print_exc() + finally: + # 确保标志被重置 + self._adding_collision = False + + def _removeCollisionAndUpdate(self, model): + """移除模型的碰撞体并更新界面""" + try: + # 查找并移除碰撞节点 + collision_nodes = model.findAllMatches("**/+CollisionNode") + for collision_np in collision_nodes: + collision_np.removeNode() + + print(f"移除了模型 {model.getName()} 的碰撞体") + + # 重置状态并更新界面 + self._previous_collision_state = True # 强制刷新 + self._updating_collision_panel = False # 确保状态能够更新 + self._updateCollisionPanelState(model) + + except Exception as e: + print(f"移除碰撞失败: {e}") + + def _findButtonRow(self, layout): + """查找按钮应该在的行数""" + # 简单返回一个合适的行数,基于布局中现有的项目数 + row_count = 0 + for i in range(layout.count()): + item = layout.itemAt(i) + if item and item.widget(): + row, col, rowspan, colspan = layout.getItemPosition(i) + row_count = max(row_count, row + 1) + return row_count + + def _updateCollisionPanelState(self, model): + """更新碰撞面板状态""" + try: + # 防止重复调用 + if getattr(self, '_updating_collision_panel', False): + return + + self._updating_collision_panel = True + print("-------------------------------------") + if hasattr(self, 'collision_button') and hasattr(self, 'collision_status_badge') and hasattr(self, + 'collision_shape_combo'): + has_collision = self._hasCollision(model) + print(f"模型 {model.getName()} 是否有碰撞体: {has_collision}-------------------------------------------------") + + # 更新状态徽章(直接修改现有控件,避免布局冲突) + if hasattr(self, 'collision_status_badge') and self.collision_status_badge: + if has_collision: + self.collision_status_badge.setText("已启用") + # 使用固定宽度的绿色样式,保持尺寸一致 + self.collision_status_badge.setStyleSheet(self.badge_style_green_fixed) + else: + self.collision_status_badge.setText("未启用") + # 使用固定宽度的红色样式,保持尺寸一致 + self.collision_status_badge.setStyleSheet(self.badge_style_red_fixed) + # 触发重绘,确保立即可见 + self.collision_status_badge.update() + + if has_collision: + # 有碰撞:显示移除按钮,下拉框变为只读并显示当前类型 + if hasattr(self, 'collision_button'): + # 更新按钮文本和样式(简单方法:直接设置文本和样式) + self.collision_button.setText("移除碰撞") + # 应用统一按钮样式 + self.collision_button.setStyleSheet(self._collision_button_style()) + + # 重新连接信号 + try: + self.collision_button.clicked.disconnect() + except: + pass + self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) + else: + # 若不存在主按钮,则创建“移除碰撞”按钮并加入布局 + try: + self.collision_button = self.createModernButton("移除碰撞") + self.collision_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.collision_button.setStyleSheet(self._collision_button_style()) + self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) + if hasattr(self, 'collision_layout') and self.collision_layout is not None: + row = self.collision_layout.rowCount() + self.collision_layout.addWidget(self.collision_button, row, 0, 1, 4) + except Exception as _e: + print(f"创建移除碰撞按钮失败: {_e}") + + # 获取并显示当前碰撞类型,设置为只读 + current_shape = self._getCurrentCollisionShape(model) + self._setComboToShape(current_shape) + self.collision_shape_combo.setEnabled(False) + + # 确保参数控件存在 - 只在没有参数控件时才添加 + if not hasattr(self, 'collision_pos_x'): + print("添加碰撞参数控件") + self._addParameterControlsToExistingPanel(model, current_shape) + + # 显示/隐藏按钮状态更新 + if not hasattr(self, 'collision_visibility_button'): + # 创建可见性按钮 + self._addVisibilityButtonToExistingPanel(model) + else: + self.collision_visibility_button.setVisible(True) + self._updateCollisionVisibilityButton(model) + + # 确保按钮顺序:可见性按钮在上,主按钮在下 + if hasattr(self, 'collision_layout'): + try: + self._repositionButtons(self.collision_layout.rowCount()) + except Exception: + pass + + print(f"碰撞面板状态更新:有碰撞 - {current_shape}") + + else: + # 无碰撞:显示添加按钮,下拉框变为可编辑 + if hasattr(self, 'collision_button'): + self.collision_button.setText("添加碰撞") + # 应用统一按钮样式 + self.collision_button.setStyleSheet(self._collision_button_style()) + + # 先断开所有连接,再重新连接 + try: + self.collision_button.clicked.disconnect() + except: + pass + self.collision_button.clicked.connect(lambda: self._addCollisionAndUpdate(model)) + else: + # 若不存在主按钮,则创建“添加碰撞”按钮并加入布局 + try: + self.collision_button = self.createModernButton("添加碰撞") + self.collision_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.collision_button.setStyleSheet(self._collision_button_style()) + self.collision_button.clicked.connect(lambda: self._addCollisionAndUpdate(model)) + if hasattr(self, 'collision_layout') and self.collision_layout is not None: + row = self.collision_layout.rowCount() + self.collision_layout.addWidget(self.collision_button, row, 0, 1, 4) + except Exception as _e: + print(f"创建添加碰撞按钮失败: {_e}") + + # 恢复为可编辑状态 + self.collision_shape_combo.setEnabled(True) + + # 隐藏并清理参数控件 - 只在有参数控件时才清理 + if hasattr(self, 'collision_pos_x'): + print("清理碰撞参数控件") + self._hideCollisionParameterControls() + + # 隐藏显示/隐藏按钮 + if hasattr(self, 'collision_visibility_button'): + self.collision_visibility_button.setVisible(False) + + # 确保按钮顺序:主按钮位于底部 + if hasattr(self, 'collision_layout'): + try: + self._repositionButtons(self.collision_layout.rowCount()) + except Exception: + pass + + print(f"碰撞面板状态更新:无碰撞 - 可编辑") + + except Exception as e: + print(f"更新碰撞面板状态失败: {e}") + import traceback + traceback.print_exc() + finally: + # 确保标志被重置 + self._updating_collision_panel = False + + def _loadCurrentCollisionParameters(self, model, shape_type): + """加载当前碰撞参数到界面""" + try: + # 设置标志位,防止在加载参数时触发更新 + self._loading_collision_params = True + + collision_nodes = model.findAllMatches("**/+CollisionNode") + for collision_np in collision_nodes: + collision_node = collision_np.node() + if collision_node.getNumSolids() > 0: + solid = collision_node.getSolid(0) + + # 从碰撞体形状中提取位置偏移 + if hasattr(self, 'collision_pos_x'): + # 获取模型的实际中心(考虑变换) + if hasattr(self.world, 'collision_manager'): + transformed_info = self.world.collision_manager._getTransformedModelInfo(model) + if transformed_info: + model_center = transformed_info['center'] + else: + model_center = model.getBounds().getCenter() if not model.getBounds().isEmpty() else Point3(0, 0, 0) + else: + model_center = model.getBounds().getCenter() if not model.getBounds().isEmpty() else Point3(0, 0, 0) + + # 获取碰撞体的中心 + collision_center = self._getCollisionShapeCenter(solid) + if collision_center: + # 计算偏移:碰撞体中心 - 模型中心 + offset_x = collision_center.x - model_center.x + offset_y = collision_center.y - model_center.y + offset_z = collision_center.z - model_center.z + self.collision_pos_x.setValue(offset_x) + self.collision_pos_y.setValue(offset_y) + self.collision_pos_z.setValue(offset_z) + print(f"加载位置偏移: ({offset_x:.2f}, {offset_y:.2f}, {offset_z:.2f})") + else: + # 如果无法计算偏移,设置为0 + self.collision_pos_x.setValue(0.0) + self.collision_pos_y.setValue(0.0) + self.collision_pos_z.setValue(0.0) + print("无法计算位置偏移,设置为0") + + if shape_type == 'sphere': + self._loadSphereParameters(solid) + elif shape_type == 'box': + self._loadBoxParameters(solid) + elif shape_type == 'capsule': + self._loadCapsuleParameters(solid) + elif shape_type == 'plane': + self._loadPlaneParameters(solid) + break + + except Exception as e: + print(f"加载碰撞参数失败: {e}") + finally: + # 重置标志位 + self._loading_collision_params = False + + def _getCollisionShapeCenter(self, solid): + """从碰撞体形状中获取中心点""" + try: + from panda3d.core import CollisionSphere, CollisionBox, CollisionCapsule, CollisionPlane, Point3 + + if isinstance(solid, CollisionSphere): + return solid.getCenter() + elif isinstance(solid, CollisionBox): + # 盒子的中心是最小点和最大点的中点 + min_pt = solid.getMin() + max_pt = solid.getMax() + return Point3((min_pt.x + max_pt.x) * 0.5, + (min_pt.y + max_pt.y) * 0.5, + (min_pt.z + max_pt.z) * 0.5) + elif isinstance(solid, CollisionCapsule): + # 胶囊体的中心是两个端点的中点 + point_a = solid.getPointA() + point_b = solid.getPointB() + return Point3((point_a.x + point_b.x) * 0.5, + (point_a.y + point_b.y) * 0.5, + (point_a.z + point_b.z) * 0.5) + elif isinstance(solid, CollisionPlane): + # 平面没有明确的中心,返回平面上的一个点 + plane = solid.getPlane() + return plane.getPoint() + else: + return None + except Exception as e: + print(f"获取碰撞体中心失败: {e}") + return None + + def _loadSphereParameters(self, solid): + """加载球形参数""" + try: + from panda3d.core import CollisionSphere + if isinstance(solid, CollisionSphere): + radius = solid.getRadius() + self.collision_radius.setValue(radius) + except Exception as e: + print(f"加载球形参数失败: {e}") + + def _loadBoxParameters(self, solid): + """加载盒型参数""" + try: + from panda3d.core import CollisionBox + if isinstance(solid, CollisionBox): + min_point = solid.getMin() + max_point = solid.getMax() + width = max_point.x - min_point.x + length = max_point.y - min_point.y + height = max_point.z - min_point.z + + self.collision_width.setValue(width) + self.collision_length.setValue(length) + self.collision_height.setValue(height) + except Exception as e: + print(f"加载盒型参数失败: {e}") + + def _loadCapsuleParameters(self, solid): + """加载胶囊体参数""" + try: + from panda3d.core import CollisionCapsule + if isinstance(solid, CollisionCapsule): + radius = solid.getRadius() + point_a = solid.getPointA() + point_b = solid.getPointB() + height = (point_b - point_a).length() + + self.collision_capsule_radius.setValue(radius) + self.collision_capsule_height.setValue(height) + except Exception as e: + print(f"加载胶囊体参数失败: {e}") + + def _loadPlaneParameters(self, solid): + """加载平面参数""" + try: + from panda3d.core import CollisionPlane + if isinstance(solid, CollisionPlane): + plane = solid.getPlane() + normal = plane.getNormal() + + self.collision_normal_x.setValue(normal.x) + self.collision_normal_y.setValue(normal.y) + self.collision_normal_z.setValue(normal.z) + except Exception as e: + print(f"加载平面参数失败: {e}") + + def _updateCollisionPosition(self, model, axis, value): + """更新碰撞位置偏移""" + try: + # 防止重复调用导致无限循环,以及在加载参数时防止更新 + if getattr(self, '_updating_collision_position', False) or getattr(self, '_loading_collision_params', False): + return + self._updating_collision_position = True + + # 获取当前所有位置偏移值 + pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 + pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 + pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 + + # 获取当前碰撞形状类型 + current_shape = self._getCurrentCollisionShape(model) + if not current_shape: + return + + # 重新创建碰撞体,传入位置偏移 + from panda3d.core import Vec3 + position_offset = Vec3(pos_x, pos_y, pos_z) + + # 根据形状类型收集其他参数 + if current_shape == 'sphere': + radius = self.collision_radius.value() if hasattr(self, 'collision_radius') else 1.0 + self._recreateCollisionShape(model, current_shape, radius=radius, position_offset=position_offset) + elif current_shape == 'box': + width = self.collision_width.value() if hasattr(self, 'collision_width') else 2.0 + length = self.collision_length.value() if hasattr(self, 'collision_length') else 2.0 + height = self.collision_height.value() if hasattr(self, 'collision_height') else 2.0 + self._recreateCollisionShape(model, current_shape, width=width, length=length, height=height, position_offset=position_offset) + elif current_shape == 'capsule': + cap_radius = self.collision_capsule_radius.value() if hasattr(self, 'collision_capsule_radius') else 0.5 + cap_height = self.collision_capsule_height.value() if hasattr(self, 'collision_capsule_height') else 2.0 + self._recreateCollisionShape(model, current_shape, radius=cap_radius, height=cap_height, position_offset=position_offset) + elif current_shape == 'plane': + normal_x = self.collision_normal_x.value() if hasattr(self, 'collision_normal_x') else 0 + normal_y = self.collision_normal_y.value() if hasattr(self, 'collision_normal_y') else 0 + normal_z = self.collision_normal_z.value() if hasattr(self, 'collision_normal_z') else 1 + self._recreateCollisionShape(model, current_shape, normal=(normal_x, normal_y, normal_z), position_offset=position_offset) + + print(f"更新碰撞位置偏移 {axis}: {value}") + + except Exception as e: + print(f"更新碰撞位置失败: {e}") + finally: + self._updating_collision_position = False + + def _updateSphereRadius(self, model, radius): + """更新球形半径""" + try: + # 防止重复调用导致无限循环,以及在加载参数时防止更新 + if getattr(self, '_updating_sphere_radius', False) or getattr(self, '_loading_collision_params', False): + return + self._updating_sphere_radius = True + + # 获取当前位置偏移 + from panda3d.core import Vec3 + pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 + pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 + pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 + position_offset = Vec3(pos_x, pos_y, pos_z) + + self._recreateCollisionShape(model, 'sphere', radius=radius, position_offset=position_offset) + print(f"更新球形半径: {radius}") + except Exception as e: + print(f"更新球形半径失败: {e}") + finally: + self._updating_sphere_radius = False + + def _updateBoxSize(self, model, dimension, value): + """更新盒型尺寸""" + try: + # 防止重复调用导致无限循环,以及在加载参数时防止更新 + if getattr(self, '_updating_box_size', False) or getattr(self, '_loading_collision_params', False): + return + self._updating_box_size = True + + # 获取当前所有尺寸 + width = self.collision_width.value() if hasattr(self, 'collision_width') else value + length = self.collision_length.value() if hasattr(self, 'collision_length') else value + height = self.collision_height.value() if hasattr(self, 'collision_height') else value + + # 获取当前位置偏移 + from panda3d.core import Vec3 + pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 + pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 + pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 + position_offset = Vec3(pos_x, pos_y, pos_z) + + self._recreateCollisionShape(model, 'box', width=width, length=length, height=height, position_offset=position_offset) + print(f"更新盒型{dimension}: {value}") + except Exception as e: + print(f"更新盒型尺寸失败: {e}") + finally: + self._updating_box_size = False + + def _updateCapsuleRadius(self, model, radius): + """更新胶囊体半径""" + try: + # 防止重复调用导致无限循环,以及在加载参数时防止更新 + if getattr(self, '_updating_capsule_radius', False) or getattr(self, '_loading_collision_params', False): + return + self._updating_capsule_radius = True + + height = self.collision_capsule_height.value() if hasattr(self, 'collision_capsule_height') else 2.0 + + # 获取当前位置偏移 + from panda3d.core import Vec3 + pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 + pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 + pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 + position_offset = Vec3(pos_x, pos_y, pos_z) + + self._recreateCollisionShape(model, 'capsule', radius=radius, height=height, position_offset=position_offset) + print(f"更新胶囊体半径: {radius}") + except Exception as e: + print(f"更新胶囊体半径失败: {e}") + finally: + self._updating_capsule_radius = False + + def _updateCapsuleHeight(self, model, height): + """更新胶囊体高度""" + try: + # 防止重复调用导致无限循环,以及在加载参数时防止更新 + if getattr(self, '_updating_capsule_height', False) or getattr(self, '_loading_collision_params', False): + return + self._updating_capsule_height = True + + radius = self.collision_capsule_radius.value() if hasattr(self, 'collision_capsule_radius') else 0.5 + + # 获取当前位置偏移 + from panda3d.core import Vec3 + pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 + pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 + pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 + position_offset = Vec3(pos_x, pos_y, pos_z) + + self._recreateCollisionShape(model, 'capsule', radius=radius, height=height, position_offset=position_offset) + print(f"更新胶囊体高度: {height}") + except Exception as e: + print(f"更新胶囊体高度失败: {e}") + finally: + self._updating_capsule_height = False + + def _updatePlaneNormal(self, model, axis, value): + """更新平面法向量""" + try: + # 防止重复调用导致无限循环,以及在加载参数时防止更新 + if getattr(self, '_updating_plane_normal', False) or getattr(self, '_loading_collision_params', False): + return + self._updating_plane_normal = True + + # 获取当前法向量 + normal_x = self.collision_normal_x.value() if hasattr(self, 'collision_normal_x') else 0 + normal_y = self.collision_normal_y.value() if hasattr(self, 'collision_normal_y') else 0 + normal_z = self.collision_normal_z.value() if hasattr(self, 'collision_normal_z') else 1 + + # 获取当前位置偏移 + from panda3d.core import Vec3 + pos_x = self.collision_pos_x.value() if hasattr(self, 'collision_pos_x') else 0.0 + pos_y = self.collision_pos_y.value() if hasattr(self, 'collision_pos_y') else 0.0 + pos_z = self.collision_pos_z.value() if hasattr(self, 'collision_pos_z') else 0.0 + position_offset = Vec3(pos_x, pos_y, pos_z) + + self._recreateCollisionShape(model, 'plane', normal=(normal_x, normal_y, normal_z), position_offset=position_offset) + print(f"更新平面法向量 {axis}: {value}") + except Exception as e: + print(f"更新平面法向量失败: {e}") + finally: + self._updating_plane_normal = False + + def _recreateCollisionShape(self, model, shape_type, **kwargs): + """重新创建碰撞形状(保持可见性)""" + try: + # 保存当前状态 + collision_nodes = model.findAllMatches("**/+CollisionNode") + if not collision_nodes: + return + + collision_np = collision_nodes[0] + is_visible = not collision_np.isHidden() + + # 移除旧的碰撞体 + collision_np.removeNode() + + # 创建新的碰撞体 + from panda3d.core import CollisionNode, BitMask32 + cNode = CollisionNode(f'modelCollision_{model.getName()}') + cNode.setIntoCollideMask(BitMask32.bit(2)) + + # 创建新形状(位置偏移已经烘焙在形状中) + if hasattr(self.world, 'collision_manager'): + collision_shape = self.world.collision_manager.createCollisionShape(model, shape_type, **kwargs) + else: + # 回退方案 + from panda3d.core import CollisionSphere, Point3 + collision_shape = CollisionSphere(Point3(0, 0, 0), kwargs.get('radius', 1.0)) + + cNode.addSolid(collision_shape) + + # 重新附加(不设置额外位置,因为位置偏移已经在形状中) + new_collision_np = model.attachNewNode(cNode) + # 碰撞节点默认位置为 (0, 0, 0),位置偏移通过形状几何体处理 + new_collision_np.setPos(0, 0, 0) + + if is_visible: + new_collision_np.show() + else: + new_collision_np.hide() + + except Exception as e: + print(f"重新创建碰撞形状失败: {e}") + import traceback + traceback.print_exc() + + def _clearCollisionParameterControls(self): + """清理碰撞参数控件""" + try: + # 位置控件 + for attr in ['collision_pos_x', 'collision_pos_y', 'collision_pos_z']: + if hasattr(self, attr): + widget = getattr(self, attr) + if widget and widget.parent(): + widget.setParent(None) + widget.deleteLater() + delattr(self, attr) + + # 球形参数控件 + if hasattr(self, 'collision_radius'): + if self.collision_radius and self.collision_radius.parent(): + self.collision_radius.setParent(None) + self.collision_radius.deleteLater() + delattr(self, 'collision_radius') + + # 盒型参数控件 + for attr in ['collision_width', 'collision_length', 'collision_height']: + if hasattr(self, attr): + widget = getattr(self, attr) + if widget and widget.parent(): + widget.setParent(None) + widget.deleteLater() + delattr(self, attr) + + # 胶囊体参数控件 + for attr in ['collision_capsule_radius', 'collision_capsule_height']: + if hasattr(self, attr): + widget = getattr(self, attr) + if widget and widget.parent(): + widget.setParent(None) + widget.deleteLater() + delattr(self, attr) + + # 平面参数控件 + for attr in ['collision_normal_x', 'collision_normal_y', 'collision_normal_z']: + if hasattr(self, attr): + widget = getattr(self, attr) + if widget and widget.parent(): + widget.setParent(None) + widget.deleteLater() + delattr(self, attr) + + print("清理碰撞参数控件完成") + + except Exception as e: + print(f"清理碰撞参数控件失败: {e}") + + def _refreshCollisionPanel(self, model): + """刷新整个碰撞面板(简化版)""" + try: + print("使用简化的面板刷新") + # 直接调用状态更新,不删除面板 + self._updateCollisionPanelState(model) + + except Exception as e: + print(f"刷新碰撞面板失败: {e}") + import traceback + traceback.print_exc() + + def _addParameterControlsToExistingPanel(self, model, shape_type): + """向现有面板添加参数控件""" + try: + if not hasattr(self, 'collision_layout'): + return + + layout = self.collision_layout + + # 首先清理可能存在的旧控件 + self._hideCollisionParameterControls() + + # 找到插入位置(在按钮之前) + current_row = 2 + + # 位置调整控件 + pos_label = QLabel("位置偏移:") + pos_label.setAlignment(Qt.AlignLeft | Qt.AlignTop) + pos_label.setVisible(True) # 确保可见 + + # X, Y, Z 位置调整 + self.collision_pos_x = self._createCollisionSpinBox(-1000000, 1000000, 2) + self.collision_pos_y = self._createCollisionSpinBox(-1000000, 1000000, 2) + self.collision_pos_z = self._createCollisionSpinBox(-1000000, 1000000, 2) + + for spinbox in (self.collision_pos_x, self.collision_pos_y, self.collision_pos_z): + spinbox.setMinimumWidth(140) + spinbox.setVisible(True) + + axis_controls = [ + ("X:", self.collision_pos_x), + ("Y:", self.collision_pos_y), + ("Z:", self.collision_pos_z), + ] + + for offset, (axis_text, spinbox) in enumerate(axis_controls): + row = current_row + offset + axis_layout = QHBoxLayout() + if offset == 0: + layout.addWidget(pos_label, row, 0) + # axis_label.addWidget(pos_label) + else: + spacer = QLabel("") + spacer.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + spacer.setVisible(True) + layout.addWidget(spacer, row, 0) + # axis_layout.addWidget(spacer) + + axis_label = QLabel(axis_text) + axis_label.setAlignment(Qt.AlignVCenter) + axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + axis_layout.addWidget(axis_label) + axis_layout.addWidget(spinbox) + # layout.addWidget(axis_label, row, 1) + # layout.addWidget(spinbox, row, 2, 1, 2) + layout.addLayout(axis_layout, row, 1, 1, 3) + + current_row += len(axis_controls) + + # 连接位置变化信号 + self.collision_pos_x.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'x', v)) + self.collision_pos_y.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'y', v)) + self.collision_pos_z.valueChanged.connect(lambda v: self._updateCollisionPosition(model, 'z', v)) + + # 根据形状类型添加特定参数 + if shape_type == 'sphere': + current_row = self._addSphereParametersToExisting(model, layout, current_row) + elif shape_type == 'box': + current_row = self._addBoxParametersToExisting(model, layout, current_row) + elif shape_type == 'capsule': + current_row = self._addCapsuleParametersToExisting(model, layout, current_row) + elif shape_type == 'plane': + current_row = self._addPlaneParametersToExisting(model, layout, current_row) + + # 重新定位按钮 + self._repositionButtons(current_row) + + # 加载参数值 + self._loadCurrentCollisionParameters(model, shape_type) + + except Exception as e: + print(f"添加参数控件到现有面板失败: {e}") + import traceback + traceback.print_exc() + + def _addSphereParametersToExisting(self, model, layout, start_row): + """向现有面板添加球形参数""" + current_row = start_row + + radius_label = QLabel("半径:") + radius_label.setVisible(True) + layout.addWidget(radius_label, current_row, 0) + layout.addWidget(QLabel(""), current_row, 1) + + self.collision_radius = self._createCollisionSpinBox(0.01, 10000, 2) + self.collision_radius.setVisible(True) + self.collision_radius.valueChanged.connect(lambda v: self._updateSphereRadius(model, v)) + layout.addWidget(self.collision_radius, current_row, 1, 1, 3) + current_row += 1 + + return current_row + + def _addBoxParametersToExisting(self, model, layout, start_row): + """向现有面板添加盒型参数""" + current_row = start_row + + size_label = QLabel("尺寸:") + size_label.setVisible(True) + layout.addWidget(size_label, current_row, 0) + # layout.addWidget(QLabel(""), current_row, 1) + # current_row += 1 + + self.collision_width = self._createCollisionSpinBox(0.01, 10000, 2) + self.collision_length = self._createCollisionSpinBox(0.01, 10000, 2) + self.collision_height = self._createCollisionSpinBox(0.01, 10000, 2) + + box_controls = [ + ("宽度:", self.collision_width), + ("长度:", self.collision_length), + ("高度:", self.collision_height), + ] + + for offset,(text, spinbox) in enumerate(box_controls): + axis_layout = QHBoxLayout() + if offset == 0: + layout.addWidget(size_label, current_row, 0) + else: + spacer = QLabel("") + spacer.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) + spacer.setVisible(True) + layout.addWidget(spacer, current_row, 0) + + axis_label = QLabel(text) + axis_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + axis_label.setVisible(True) + axis_layout.addWidget(axis_label) + spinbox.setVisible(True) + axis_layout.addWidget(spinbox) + layout.addLayout(axis_layout, current_row, 1, 1, 3) + current_row += 1 + + # 连接信号 + self.collision_width.valueChanged.connect(lambda v: self._updateBoxSize(model, 'width', v)) + self.collision_length.valueChanged.connect(lambda v: self._updateBoxSize(model, 'length', v)) + self.collision_height.valueChanged.connect(lambda v: self._updateBoxSize(model, 'height', v)) + + return current_row + + def _addCapsuleParametersToExisting(self, model, layout, start_row): + """向现有面板添加胶囊体参数""" + current_row = start_row + + radius_label = QLabel("半径:") + layout.addWidget(radius_label, current_row, 0) + + self.collision_capsule_radius = self._createCollisionSpinBox(0.01, 10000, 2) + self.collision_capsule_radius.valueChanged.connect(lambda v: self._updateCapsuleRadius(model, v)) + layout.addWidget(self.collision_capsule_radius, current_row, 1, 1 , 3) + current_row += 1 + + height_label = QLabel("高度:") + layout.addWidget(height_label, current_row, 0) + + self.collision_capsule_height = self._createCollisionSpinBox(0.01, 10000, 2) + self.collision_capsule_height.valueChanged.connect(lambda v: self._updateCapsuleHeight(model, v)) + layout.addWidget(self.collision_capsule_height, current_row, 1, 1, 3) + current_row += 1 + + return current_row + + def _addPlaneParametersToExisting(self, model, layout, start_row): + """向现有面板添加平面参数""" + current_row = start_row + + normal_label = QLabel("法向量:") + layout.addWidget(normal_label, current_row, 0) + + self.collision_normal_x = self._createCollisionSpinBox(-1, 1, 2) + self.collision_normal_y = self._createCollisionSpinBox(-1, 1, 2) + self.collision_normal_z = self._createCollisionSpinBox(-1, 1, 2) + + nx_layout = QHBoxLayout() + axis_label = QLabel("Nx:") + axis_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + nx_layout.addWidget(axis_label) + nx_layout.addWidget(self.collision_normal_x) + layout.addLayout(nx_layout, current_row, 1, 1, 3) + current_row += 1 + + ny_layout = QHBoxLayout() + placeholder = QLabel("") + layout.addWidget(placeholder, current_row, 0) + + axis_label = QLabel("Ny:") + axis_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + ny_layout.addWidget(axis_label) + ny_layout.addWidget(self.collision_normal_y) + layout.addLayout(ny_layout, current_row, 1, 1, 3) + current_row += 1 + + nz_layout = QHBoxLayout() + placeholder = QLabel("") + layout.addWidget(placeholder, current_row, 0) + + axis_label = QLabel("Nz:") + axis_label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) + axis_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + nz_layout.addWidget(axis_label) + nz_layout.addWidget(self.collision_normal_z) + layout.addLayout(nz_layout, current_row, 1, 1, 3) + current_row += 1 + + # 连接信号 + self.collision_normal_x.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'x', v)) + self.collision_normal_y.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'y', v)) + self.collision_normal_z.valueChanged.connect(lambda v: self._updatePlaneNormal(model, 'z', v)) + + return current_row + + def _addVisibilityButtonToExistingPanel(self, model): + """向现有面板添加可见性按钮""" + try: + if hasattr(self, 'collision_layout'): + layout = self.collision_layout + is_collision_visible = self._isCollisionVisible(model) + + # 放在当前最后一行 + current_row = layout.rowCount() + + self.collision_visibility_button = QPushButton("隐藏碰撞" if is_collision_visible else "显示碰撞") + self.collision_visibility_button.setStyleSheet(self._collision_button_style()) + self.collision_visibility_button.clicked.connect(lambda: self._toggleCollisionVisibility(model)) + layout.addWidget(self.collision_visibility_button, current_row, 0, 1, 4) + + # 确保存在主按钮(有碰撞时应显示移除碰撞) + if self._hasCollision(model) and not hasattr(self, 'collision_button'): + try: + self.collision_button = self.createModernButton("移除碰撞") + self.collision_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.collision_button.setStyleSheet(self._collision_button_style()) + self.collision_button.clicked.connect(lambda: self._removeCollisionAndUpdate(model)) + layout.addWidget(self.collision_button, current_row + 1, 0, 1, 4) + except Exception as _e: + print(f"在可见性按钮后创建移除碰撞按钮失败: {_e}") + + except Exception as e: + print(f"添加可见性按钮失败: {e}") + + def _repositionButtons(self, new_row): + """重新定位按钮位置""" + try: + if hasattr(self, 'collision_layout'): + layout = self.collision_layout + + def _remove_if_present(w): + try: + if not w: + return + # 从布局中移除该控件(如果已存在) + for i in range(layout.count()): + item = layout.itemAt(i) + if item and item.widget() is w: + layout.removeWidget(w) + break + except Exception: + pass + + # 计算底部行 + bottom_row = layout.rowCount() + + # 按固定顺序重新放置:先可见性按钮,再主按钮 + if hasattr(self, 'collision_visibility_button') and self.collision_visibility_button: + _remove_if_present(self.collision_visibility_button) + layout.addWidget(self.collision_visibility_button, bottom_row, 0, 1, 4) + bottom_row += 1 + + if hasattr(self, 'collision_button') and self.collision_button: + _remove_if_present(self.collision_button) + layout.addWidget(self.collision_button, bottom_row, 0, 1, 4) + + except Exception as e: + print(f"重新定位按钮失败: {e}") + + def _hideCollisionParameterControls(self): + """隐藏并移除碰撞参数控件与其所在的行(保留状态/形状/按钮行)""" + try: + # 1) 先清理对象属性引用,避免残留信号 + param_attrs = [ + 'collision_pos_x', 'collision_pos_y', 'collision_pos_z', + 'collision_radius', + 'collision_width', 'collision_length', 'collision_height', + 'collision_capsule_radius', 'collision_capsule_height', + 'collision_normal_x', 'collision_normal_y', 'collision_normal_z' + ] + for attr in param_attrs: + if hasattr(self, attr): + widget = getattr(self, attr) + try: + if widget: + widget.setParent(None) + widget.deleteLater() + except Exception: + pass + delattr(self, attr) + + # 2) 深度移除布局中第2行(索引>=2)之后的所有参数相关项,保留按钮 + if hasattr(self, 'collision_layout') and self.collision_layout is not None: + layout = self.collision_layout + + def _remove_layout_recursive(q_layout): + try: + while q_layout.count(): + child = q_layout.takeAt(0) + if child.widget(): + w = child.widget() + w.setParent(None) + w.deleteLater() + elif child.layout(): + _remove_layout_recursive(child.layout()) + except Exception: + pass + + row_count = layout.rowCount() + col_count = layout.columnCount() + # 从参数区域开始清理(行索引2及以后),但跳过我们要保留的按钮 + for i in range(2, row_count): + for j in range(0, col_count): + item = layout.itemAtPosition(i, j) + if not item: + continue + # 若是按钮,且是保留的按钮,则跳过 + w = item.widget() + if w is not None: + try: + if (hasattr(self, 'collision_button') and w == self.collision_button) or (hasattr(self, 'collision_visibility_button') and w == self.collision_visibility_button): + continue + except Exception: + pass + layout.removeWidget(w) + w.setParent(None) + w.deleteLater() + elif item.layout(): + # 移除嵌套布局(包含标签与输入框) + nested = item.layout() + _remove_layout_recursive(nested) + layout.removeItem(nested) + + # 尝试把保留的按钮移动到参数区域第一行(行2)下方,保持整洁 + try: + next_row = 2 + if hasattr(self, 'collision_visibility_button'): + self.collision_visibility_button.setVisible(False) + if hasattr(self, 'collision_button') and self.collision_button is not None: + layout.addWidget(self.collision_button, next_row, 0, 1, 4) + except Exception: + pass + + print("隐藏碰撞参数控件完成(仅保留状态、形状与按钮)") + + except Exception as e: + print(f"隐藏碰撞参数控件失败: {e}") + import traceback + traceback.print_exc() diff --git a/ui/simple_maintenance_login.py b/ui/simple_maintenance_login.py index 2cf5ba45..63e9aff8 100644 --- a/ui/simple_maintenance_login.py +++ b/ui/simple_maintenance_login.py @@ -1,220 +1,220 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -""" -简化的维修系统登录界面 -解决显示和输入问题 -""" - -from PyQt5.QtWidgets import ( - QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QGroupBox, - QLineEdit, QPushButton, QLabel, QMessageBox -) -from PyQt5.QtCore import Qt, pyqtSignal - - -class SimpleMaintenanceLoginDialog(QDialog): - """简化的维修系统登录对话框""" - - login_success = pyqtSignal() # 登录成功信号 - - def __init__(self, parent=None): - super().__init__(parent) - self.setupUI() - - def setupUI(self): - """设置用户界面""" - self.setWindowTitle("维修系统 - 登录") - self.resize(400, 250) - self.setModal(True) - - # 设置简洁的样式 - self.setStyleSheet(""" - QDialog { - background-color: #ffffff; - font-family: "Microsoft YaHei", Arial, sans-serif; - } - QGroupBox { - font-size: 14px; - font-weight: bold; - color: #2c3e50; - border: 2px solid #bdc3c7; - border-radius: 5px; - margin-top: 10px; - padding-top: 10px; - } - QGroupBox::title { - subcontrol-origin: margin; - left: 10px; - padding: 0 5px; - } - QLabel { - color: #2c3e50; - font-size: 14px; - } - QLineEdit { - background-color: #ffffff; - border: 2px solid #bdc3c7; - border-radius: 3px; - padding: 6px 8px; - font-size: 14px; - color: #2c3e50; - min-height: 16px; - } - QLineEdit:focus { - border-color: #3498db; - background-color: #ffffff; - } - QPushButton { - background-color: #3498db; - color: white; - border: none; - padding: 8px 20px; - border-radius: 3px; - font-size: 14px; - font-weight: bold; - min-height: 16px; - } - QPushButton:hover { - background-color: #2980b9; - } - QPushButton:pressed { - background-color: #21618c; - } - QPushButton#cancel_btn { - background-color: #95a5a6; - } - QPushButton#cancel_btn:hover { - background-color: #7f8c8d; - } - """) - - # 主布局 - main_layout = QVBoxLayout(self) - main_layout.setSpacing(20) - main_layout.setContentsMargins(30, 30, 30, 30) - - # 标题 - title_label = QLabel("🔧 维修系统") - title_label.setAlignment(Qt.AlignCenter) - title_label.setStyleSheet(""" - font-size: 18px; - font-weight: bold; - color: #2c3e50; - margin: 10px 0px; - """) - main_layout.addWidget(title_label) - - # 登录表单组 - form_group = QGroupBox("身份验证") - form_layout = QFormLayout(form_group) - form_layout.setSpacing(15) - form_layout.setContentsMargins(20, 20, 20, 20) - - # 用户名输入框 - self.username_edit = QLineEdit() - self.username_edit.setPlaceholderText("请输入用户名") - form_layout.addRow("用户名:", self.username_edit) - - # 密码输入框 - self.password_edit = QLineEdit() - self.password_edit.setEchoMode(QLineEdit.Password) - self.password_edit.setPlaceholderText("请输入密码") - form_layout.addRow("密码:", self.password_edit) - - main_layout.addWidget(form_group) - - # 提示信息 - hint_label = QLabel("💡 默认账号密码均为: admin") - hint_label.setAlignment(Qt.AlignCenter) - hint_label.setStyleSheet(""" - color: #7f8c8d; - font-size: 12px; - margin: 5px 0px; - """) - main_layout.addWidget(hint_label) - - # 按钮布局 - button_layout = QHBoxLayout() - button_layout.setSpacing(10) - - self.cancel_btn = QPushButton("取消") - self.cancel_btn.setObjectName("cancel_btn") - - self.login_btn = QPushButton("登录") - - button_layout.addStretch() - button_layout.addWidget(self.cancel_btn) - button_layout.addWidget(self.login_btn) - - main_layout.addLayout(button_layout) - - # 连接信号 - self.login_btn.clicked.connect(self.handle_login) - self.cancel_btn.clicked.connect(self.reject) - self.password_edit.returnPressed.connect(self.handle_login) - - # 设置焦点和Tab顺序 - self.username_edit.setFocus() - self.setTabOrder(self.username_edit, self.password_edit) - self.setTabOrder(self.password_edit, self.login_btn) - self.setTabOrder(self.login_btn, self.cancel_btn) - - print("✅ 简化登录界面创建完成") - print(f"📝 窗口大小: {self.size().width()}x{self.size().height()}") - - def handle_login(self): - """处理登录""" - username = self.username_edit.text().strip() - password = self.password_edit.text().strip() - - print(f"🔍 登录尝试: 用户名='{username}', 密码='{password}'") - - # 验证账号密码 - if username == "admin" and password == "admin": - print("✅ 登录验证成功") - self.login_success.emit() - self.accept() - else: - print("❌ 登录验证失败") - QMessageBox.warning( - self, - "登录失败", - "用户名或密码错误!\n\n请使用:\n用户名: admin\n密码: admin" - ) - self.password_edit.clear() - self.password_edit.setFocus() - - -def test_simple_login(): - """测试简化登录界面""" - import sys - from PyQt5.QtWidgets import QApplication - - print("🧪 测试简化登录界面") - print("=" * 30) - - app = QApplication(sys.argv) - - dialog = SimpleMaintenanceLoginDialog() - - print("💡 请测试以下功能:") - print("1. 检查是否显示完整的登录界面") - print("2. 在用户名框输入 'admin'") - print("3. 在密码框输入 'admin'") - print("4. 点击登录按钮") - - def on_success(): - print("🎉 登录成功!") - QMessageBox.information(None, "成功", "登录测试成功!") - - dialog.login_success.connect(on_success) - - result = dialog.exec_() - print(f"📊 测试结果: {'成功' if result == QDialog.Accepted else '取消'}") - - return result == QDialog.Accepted - - -if __name__ == "__main__": +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +简化的维修系统登录界面 +解决显示和输入问题 +""" + +from PyQt5.QtWidgets import ( + QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QGroupBox, + QLineEdit, QPushButton, QLabel, QMessageBox +) +from PyQt5.QtCore import Qt, pyqtSignal + + +class SimpleMaintenanceLoginDialog(QDialog): + """简化的维修系统登录对话框""" + + login_success = pyqtSignal() # 登录成功信号 + + def __init__(self, parent=None): + super().__init__(parent) + self.setupUI() + + def setupUI(self): + """设置用户界面""" + self.setWindowTitle("维修系统 - 登录") + self.resize(400, 250) + self.setModal(True) + + # 设置简洁的样式 + self.setStyleSheet(""" + QDialog { + background-color: #ffffff; + font-family: "Microsoft YaHei", Arial, sans-serif; + } + QGroupBox { + font-size: 14px; + font-weight: bold; + color: #2c3e50; + border: 2px solid #bdc3c7; + border-radius: 5px; + margin-top: 10px; + padding-top: 10px; + } + QGroupBox::title { + subcontrol-origin: margin; + left: 10px; + padding: 0 5px; + } + QLabel { + color: #2c3e50; + font-size: 14px; + } + QLineEdit { + background-color: #ffffff; + border: 2px solid #bdc3c7; + border-radius: 3px; + padding: 6px 8px; + font-size: 14px; + color: #2c3e50; + min-height: 16px; + } + QLineEdit:focus { + border-color: #3498db; + background-color: #ffffff; + } + QPushButton { + background-color: #3498db; + color: white; + border: none; + padding: 8px 20px; + border-radius: 3px; + font-size: 14px; + font-weight: bold; + min-height: 16px; + } + QPushButton:hover { + background-color: #2980b9; + } + QPushButton:pressed { + background-color: #21618c; + } + QPushButton#cancel_btn { + background-color: #95a5a6; + } + QPushButton#cancel_btn:hover { + background-color: #7f8c8d; + } + """) + + # 主布局 + main_layout = QVBoxLayout(self) + main_layout.setSpacing(20) + main_layout.setContentsMargins(30, 30, 30, 30) + + # 标题 + title_label = QLabel("🔧 维修系统") + title_label.setAlignment(Qt.AlignCenter) + title_label.setStyleSheet(""" + font-size: 18px; + font-weight: bold; + color: #2c3e50; + margin: 10px 0px; + """) + main_layout.addWidget(title_label) + + # 登录表单组 + form_group = QGroupBox("身份验证") + form_layout = QFormLayout(form_group) + form_layout.setSpacing(15) + form_layout.setContentsMargins(20, 20, 20, 20) + + # 用户名输入框 + self.username_edit = QLineEdit() + self.username_edit.setPlaceholderText("请输入用户名") + form_layout.addRow("用户名:", self.username_edit) + + # 密码输入框 + self.password_edit = QLineEdit() + self.password_edit.setEchoMode(QLineEdit.Password) + self.password_edit.setPlaceholderText("请输入密码") + form_layout.addRow("密码:", self.password_edit) + + main_layout.addWidget(form_group) + + # 提示信息 + hint_label = QLabel("💡 默认账号密码均为: admin") + hint_label.setAlignment(Qt.AlignCenter) + hint_label.setStyleSheet(""" + color: #7f8c8d; + font-size: 12px; + margin: 5px 0px; + """) + main_layout.addWidget(hint_label) + + # 按钮布局 + button_layout = QHBoxLayout() + button_layout.setSpacing(10) + + self.cancel_btn = QPushButton("取消") + self.cancel_btn.setObjectName("cancel_btn") + + self.login_btn = QPushButton("登录") + + button_layout.addStretch() + button_layout.addWidget(self.cancel_btn) + button_layout.addWidget(self.login_btn) + + main_layout.addLayout(button_layout) + + # 连接信号 + self.login_btn.clicked.connect(self.handle_login) + self.cancel_btn.clicked.connect(self.reject) + self.password_edit.returnPressed.connect(self.handle_login) + + # 设置焦点和Tab顺序 + self.username_edit.setFocus() + self.setTabOrder(self.username_edit, self.password_edit) + self.setTabOrder(self.password_edit, self.login_btn) + self.setTabOrder(self.login_btn, self.cancel_btn) + + print("✅ 简化登录界面创建完成") + print(f"📝 窗口大小: {self.size().width()}x{self.size().height()}") + + def handle_login(self): + """处理登录""" + username = self.username_edit.text().strip() + password = self.password_edit.text().strip() + + print(f"🔍 登录尝试: 用户名='{username}', 密码='{password}'") + + # 验证账号密码 + if username == "admin" and password == "admin": + print("✅ 登录验证成功") + self.login_success.emit() + self.accept() + else: + print("❌ 登录验证失败") + QMessageBox.warning( + self, + "登录失败", + "用户名或密码错误!\n\n请使用:\n用户名: admin\n密码: admin" + ) + self.password_edit.clear() + self.password_edit.setFocus() + + +def test_simple_login(): + """测试简化登录界面""" + import sys + from PyQt5.QtWidgets import QApplication + + print("🧪 测试简化登录界面") + print("=" * 30) + + app = QApplication(sys.argv) + + dialog = SimpleMaintenanceLoginDialog() + + print("💡 请测试以下功能:") + print("1. 检查是否显示完整的登录界面") + print("2. 在用户名框输入 'admin'") + print("3. 在密码框输入 'admin'") + print("4. 点击登录按钮") + + def on_success(): + print("🎉 登录成功!") + QMessageBox.information(None, "成功", "登录测试成功!") + + dialog.login_success.connect(on_success) + + result = dialog.exec_() + print(f"📊 测试结果: {'成功' if result == QDialog.Accepted else '取消'}") + + return result == QDialog.Accepted + + +if __name__ == "__main__": test_simple_login() \ No newline at end of file diff --git a/ui/widgets.py b/ui/widgets.py index dffa7eb5..b9d434c9 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -1,4309 +1,4309 @@ -""" -自定义Qt部件模块 - -包含所有自定义的Qt界面组件: -- NewProjectDialog: 新建项目对话框 -- CustomPanda3DWidget: 自定义Panda3D显示部件 -- CustomFileView: 自定义文件浏览器 -- CustomTreeWidget: 自定义场景树部件 -""" - -import os -import re -from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, - QLineEdit, QPushButton, QLabel, - QTreeView, QTreeWidget, QTreeWidgetItem, QWidget, - QFileDialog, QMessageBox, QAbstractItemView, QMenu, QDockWidget, QButtonGroup, QToolButton, QFrame, QSizePolicy) -from PyQt5.QtCore import Qt, QUrl, QMimeData, QPoint, QSize -from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush, QFont -from PyQt5.sip import wrapinstance -from direct.showbase.ShowBaseGlobal import aspect2d -from panda3d.core import ModelRoot, NodePath, CollisionNode - -from QMeta3D.QMeta3DWidget import QMeta3DWidget -from scene import util -from ui.icon_manager import get_icon_manager - -class NewProjectDialog(QDialog): - """新建项目对话框""" - - def __init__(self, parent=None): - super().__init__(parent) - self.setWindowTitle("新建项目") - self.setObjectName("newProjectDialog") - self.resize(508, 274) - self.setMinimumSize(508, 274) - self.setModal(True) - - # 移除默认窗口装饰,使用自定义顶部栏 - self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) - self.setAttribute(Qt.WA_TranslucentBackground, True) - - # 拖拽和窗口状态 - self.dragging = False - self.drag_position = QPoint() - self.icon_manager = get_icon_manager() - self._title_icon_size = QSize(18, 18) - self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size) - - # 设置严格按照Figma设计的样式 - self.setStyleSheet(""" - QDialog#newProjectDialog { - background-color: transparent; - color: #EBEBEB; - border: none; - } - QFrame#baseFrame { - background-color: #000000; - border: 1px solid #3E3E42; - border-radius: 5px; - } - QWidget#titleBar { - background-color: transparent; - border: none; - border-radius: 5px 5px 0px 0px; - min-height: 32px; - max-height: 32px; - } - QWidget#titleBar QWidget { - background-color: transparent; - border: none; - } - QLabel#titleLabel { - color: #FFFFFF; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 14px; - font-weight: 500; - letter-spacing: 0.7px; - } - QWidget#controlButtons QPushButton { - background-color: transparent; - border: none; - color: #EBEBEB; - font-size: 14px; - min-width: 18px; - max-width: 18px; - min-height: 18px; - max-height: 18px; - padding: 0px; - border-radius: 3px; - } - QWidget#controlButtons QPushButton:hover { - background-color: #2A2D2E; - color: #FFFFFF; - } - QPushButton#closeButton { - border-radius: 0px 5px 0px 0px; - } - QPushButton#closeButton:hover { - background-color: #2A2D2E; - color: #FFFFFF; - } - QWidget#contentWidget { - background-color: transparent; - border-radius: 0px 0px 5px 5px; - } - QFrame#contentContainer { - background-color: #19191B; - border: 1px solid #2C2F36; - border-radius: 5px; - } - QLabel[role="sectionTitle"] { - color: #EBEBEB; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 12px; - font-weight: 500; - letter-spacing: 0.6px; - padding: 0px; - margin-bottom: 0px; - } - QLabel[role="hint"] { - color: rgba(235, 235, 235, 0.6); - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 11px; - font-weight: 300; - letter-spacing: 0.55px; - margin-top: 0px; - padding: 0px; - } - QLineEdit { - background-color: rgba(89, 100, 113, 0.2); - color: #EBEBEB; - border: 1px solid rgba(76, 92, 110, 0.6); - border-radius: 2px; - padding: 6px 10px; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 11px; - font-weight: 300; - letter-spacing: 0.55px; - min-height: 14px; - max-height: 30px; - } - QLineEdit:focus { - border: 1px solid #3067C0; - background-color: rgba(48, 103, 192, 0.1); - } - QLineEdit:hover { - border: 1px solid #3067C0; - background-color: rgba(89, 100, 113, 0.3); - } - QLineEdit:disabled { - background-color: rgba(89, 100, 113, 0.1); - color: rgba(235, 235, 235, 0.4); - border: 1px solid rgba(76, 92, 110, 0.2); - } - QPushButton { - background-color: rgba(89, 98, 118, 0.5); - color: #EBEBEB; - border: none; - border-radius: 2px; - padding: 0px 0px; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-weight: 300; - font-size: 10px; - letter-spacing: 0.5px; - min-width: 90px; - min-height: 30px; - max-height: 30px; - } - QPushButton:hover { - background-color: #3067C0; - color: #FFFFFF; - } - QPushButton:pressed { - background-color: #2556A0; - color: #FFFFFF; - } - QPushButton:disabled { - background-color: rgba(89, 98, 118, 0.3); - color: rgba(235, 235, 235, 0.4); - } - QPushButton#primaryButton { - background-color: rgba(89, 98, 118, 0.5); - border: none; - color: #EBEBEB; - font-weight: 300; - min-width: 120px; - max-width: 120px; - } - QPushButton#primaryButton:hover { - background-color: #2556A0; - } - QPushButton#primaryButton:pressed { - background-color: #1E4A8C; - } - QPushButton#secondaryButton { - background-color: rgba(89, 98, 118, 0.5); - border: none; - color: #EBEBEB; - min-width: 120px; - max-width: 120px; - } - QPushButton#secondaryButton:hover { - background-color: #3067C0; - color: #FFFFFF; - } - QPushButton#secondaryButton:pressed { - background-color: #2556A0; - color: #FFFFFF; - } - QPushButton#browseButton { - min-width: 90px; - max-width: 90px; - min-height: 28px; - max-height: 28px; - } - """) - - # 创建主布局 - main_layout = QVBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setSpacing(0) - - # 创建基础容器,负责绘制圆角和边框 - base_frame = QFrame() - base_frame.setObjectName('baseFrame') - base_frame.setFrameShape(QFrame.NoFrame) - base_frame.setAttribute(Qt.WA_StyledBackground, True) - - base_layout = QVBoxLayout(base_frame) - base_layout.setContentsMargins(0, 0, 0, 0) - base_layout.setSpacing(0) - - # 创建自定义顶部栏 - self.createTitleBar() - base_layout.addWidget(self.title_bar) - - # 创建内容区域 - content_widget = QWidget() - content_widget.setObjectName('contentWidget') - content_layout = QVBoxLayout(content_widget) - content_layout.setContentsMargins(10, 10, 10, 10) - content_layout.setSpacing(0) - - content_container = QFrame() - content_container.setObjectName('contentContainer') - content_container.setFrameShape(QFrame.NoFrame) - content_container.setAttribute(Qt.WA_StyledBackground, True) - content_container.setFixedWidth(488) - content_container.setMinimumHeight(213) - - container_layout = QVBoxLayout(content_container) - container_layout.setContentsMargins(15, 10, 15, 10) - container_layout.setSpacing(10) - - pathLabel = QLabel('项目路径') - pathLabel.setProperty('role', 'sectionTitle') - container_layout.addWidget(pathLabel) - - path_row_widget = QWidget() - path_row_layout = QHBoxLayout(path_row_widget) - path_row_layout.setContentsMargins(0, 0, 0, 0) - path_row_layout.setSpacing(10) - path_row_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - self.pathEdit = QLineEdit() - self.pathEdit.setReadOnly(True) - self.pathEdit.setPlaceholderText('请选择项目路径') - self.pathEdit.setMinimumWidth(358) - self.pathEdit.setFixedHeight(30) - path_row_layout.addWidget(self.pathEdit) - - browseButton = QPushButton('浏览...') - browseButton.setObjectName('browseButton') - browseButton.clicked.connect(self.browsePath) - path_row_layout.addWidget(browseButton) - container_layout.addWidget(path_row_widget) - - nameLabel = QLabel('项目名称') - nameLabel.setProperty('role', 'sectionTitle') - container_layout.addWidget(nameLabel) - - self.nameEdit = QLineEdit() - self.nameEdit.setText('新项目') - self.nameEdit.setPlaceholderText('请输入项目名称') - self.nameEdit.selectAll() - self.nameEdit.setFixedHeight(30) - container_layout.addWidget(self.nameEdit) - - self.tipLabel = QLabel('项目名称只能包含字母、数字、下划线、中划线和中文') - self.tipLabel.setProperty('role', 'hint') - container_layout.addWidget(self.tipLabel) - - separator = QFrame() - separator.setFrameShape(QFrame.HLine) - separator.setFrameShadow(QFrame.Plain) - separator.setFixedHeight(1) - separator.setStyleSheet('background-color: #2C2F36; border: none;') - container_layout.addWidget(separator) - - button_row_widget = QWidget() - button_row_layout = QHBoxLayout(button_row_widget) - button_row_layout.setContentsMargins(0, 0, 0, 0) - button_row_layout.setSpacing(10) - button_row_layout.addStretch() - button_row_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - - self.cancelButton = QPushButton('取消') - self.cancelButton.setObjectName('secondaryButton') - self.cancelButton.clicked.connect(self.reject) - self.cancelButton.setFixedSize(120, 30) - button_row_layout.addWidget(self.cancelButton) - - self.confirmButton = QPushButton('确认') - self.confirmButton.setObjectName('primaryButton') - self.confirmButton.clicked.connect(self.validate) - self.confirmButton.setFixedSize(120, 30) - button_row_layout.addWidget(self.confirmButton) - - container_layout.addWidget(button_row_widget) - - self.confirmButton.setDefault(True) - self.confirmButton.setAutoDefault(True) - self.cancelButton.setAutoDefault(False) - - content_layout.addWidget(content_container, 0, Qt.AlignTop) - base_layout.addWidget(content_widget) - main_layout.addWidget(base_frame) - - self.projectPath = "" - self.projectName = "" - - def createTitleBar(self): - """创建自定义顶部栏""" - self.title_bar = QFrame() - self.title_bar.setObjectName("titleBar") - - title_layout = QHBoxLayout(self.title_bar) - title_layout.setContentsMargins(12, 0, 12, 0) - title_layout.setSpacing(0) - - # Control buttons area - controls = QWidget() - controls.setObjectName("controlButtons") - controls_layout = QHBoxLayout(controls) - controls_layout.setContentsMargins(0, 0, 0, 0) - controls_layout.setSpacing(0) - - self.close_button = QPushButton() - self.close_button.setObjectName("closeButton") - self.close_button.clicked.connect(self.reject) - self.close_button.setFocusPolicy(Qt.NoFocus) - controls_layout.addWidget(self.close_button) - - self._applyTitleBarIcons() - - # Reserve left space equal to control buttons to keep title centered - left_placeholder = QWidget() - left_placeholder.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - title_layout.addWidget(left_placeholder) - - self.title_label = QLabel(self.windowTitle()) - self.title_label.setObjectName("titleLabel") - self.title_label.setAlignment(Qt.AlignCenter) - title_layout.addWidget(self.title_label, 1) - - title_layout.addWidget(controls) - - left_placeholder.setFixedWidth(controls.sizeHint().width()) - - - def _applyTitleBarIcons(self): - """为标题栏按钮应用图标""" - if self._icon_close: - self.close_button.setIcon(self._icon_close) - self.close_button.setIconSize(self._title_icon_size) - self.close_button.setText("") - self.close_button.setToolTip("关闭") - - - def toggleMaximize(self): - """切换窗口最大化/还原""" - if self.isMaximized(): - self.showNormal() - else: - self.showMaximized() - - - def setWindowTitle(self, title): - super().setWindowTitle(title) - if hasattr(self, "title_label"): - self.title_label.setText(title) - - - def mousePressEvent(self, event): - """鼠标按下事件 - 用于拖拽窗口""" - if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): - if self.isMaximized(): - self.showNormal() - self.dragging = True - self.drag_position = event.globalPos() - self.frameGeometry().topLeft() - event.accept() - super().mousePressEvent(event) - - def mouseDoubleClickEvent(self, event): - """双击标题栏切换最大化""" - if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): - self.toggleMaximize() - event.accept() - return - super().mouseDoubleClickEvent(event) - - - def mouseMoveEvent(self, event): - """鼠标移动事件 - 用于拖拽窗口""" - if event.buttons() == Qt.LeftButton and self.dragging: - self.move(event.globalPos() - self.drag_position) - event.accept() - super().mouseMoveEvent(event) - - def mouseReleaseEvent(self, event): - """鼠标释放事件 - 停止拖拽""" - if event.button() == Qt.LeftButton: - self.dragging = False - super().mouseReleaseEvent(event) - - def browsePath(self): - """浏览选择项目路径""" - path = QFileDialog.getExistingDirectory(self, "选择项目路径") - if path: - self.pathEdit.setText(path) - - def validate(self): - """验证输入并关闭对话框""" - # 获取并验证路径 - self.projectPath = self.pathEdit.text() - if not self.projectPath: - UniversalMessageDialog.show_warning(self, "错误", "请选择项目路径!", show_cancel=False, confirm_text="确认") - return - - # 获取并验证项目名称 - self.projectName = self.nameEdit.text().strip() - if not self.projectName: - UniversalMessageDialog.show_warning(self, "错误", "请输入项目名称!", show_cancel=False, confirm_text="确认") - return - - # 验证项目名称格式 - if not re.match(r'^[a-zA-Z0-9_\-\u4e00-\u9fa5]+$', self.projectName): - UniversalMessageDialog.show_warning(self, "错误", - "项目名称只能包含字母、数字、下划线、中划线和中文!", show_cancel=False, confirm_text="确认") - return - - # 检查项目是否已存在 - full_path = os.path.join(self.projectPath, self.projectName) - if os.path.exists(full_path): - UniversalMessageDialog.show_warning(self, "错误", "项目已存在!", show_cancel=False, confirm_text="确认") - return - - self.accept() - -class CustomMeta3DWidget(QMeta3DWidget): - """自定义Panda3D显示部件""" - - def __init__(self, world, parent=None): - if parent is None: - parent = wrapinstance(0, QWidget) - super().__init__(world, parent) - self.world = world - self.setFocusPolicy(Qt.StrongFocus) # 允许接收键盘焦点 - self.setAcceptDrops(True) # 允许接收拖放 - self.setMouseTracking(True) # 启用鼠标追踪 - - # 让world引用这个widget,以便获取准确的尺寸 - if hasattr(world, 'setQtWidget'): - world.setQtWidget(self) - - def getActualSize(self): - """获取Qt部件的实际渲染尺寸""" - return (self.width(), self.height()) - - def dragEnterEvent(self, event): - """处理拖拽进入事件""" - # 检查是否是文件拖拽 - if event.mimeData().hasUrls(): - # 检查是否包含支持的模型文件 - for url in event.mimeData().urls(): - filepath = url.toLocalFile() - if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - event.acceptProposedAction() - return - event.ignore() - - def dragMoveEvent(self, event): - """处理拖拽移动事件""" - if event.mimeData().hasUrls(): - event.acceptProposedAction() - else: - event.ignore() - - def dropEvent(self, event): - """处理拖放事件""" - if event.mimeData().hasUrls(): - for url in event.mimeData().urls(): - filepath = url.toLocalFile() - if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - # 使用关键字参数确保兼容性 - self.world.importModel(filepath) - event.acceptProposedAction() - else: - event.ignore() - - def wheelEvent(self, event): - """处理滚轮事件""" - if event.angleDelta().y() > 0: - # 滚轮向前滚动 - self.world.wheelForward() - else: - # 滚轮向后滚动 - self.world.wheelBackward() - event.accept() - - def mousePressEvent(self, event): - """处理 Qt 鼠标按下事件""" - if event.button() == Qt.LeftButton: - self.world.mousePressEventLeft({ - 'x': event.x(), - 'y': event.y() - }) - elif event.button() == Qt.RightButton: - self.world.mousePressEventRight({ - 'x': event.x(), - 'y': event.y() - }) - elif event.button() == Qt.MiddleButton: # 添加滑轮按下事件处理 - self.world.mousePressEventMiddle({ - 'x': event.x(), - 'y': event.y() - }) - event.accept() - - def mouseReleaseEvent(self, event): - """处理 Qt 鼠标释放事件""" - if event.button() == Qt.LeftButton: - self.world.mouseReleaseEventLeft({ - 'x': event.x(), - 'y': event.y() - }) - elif event.button() == Qt.RightButton: - self.world.mouseReleaseEventRight({ - 'x': event.x(), - 'y': event.y() - }) - elif event.button() == Qt.MiddleButton: # 添加滑轮释放事件处理 - self.world.mouseReleaseEventMiddle({ - 'x': event.x(), - 'y': event.y() - }) - event.accept() - - def mouseMoveEvent(self, event): - """处理 Qt 鼠标移动事件""" - self.world.mouseMoveEvent({ - 'x': event.x(), - 'y': event.y() - }) - event.accept() - - def cleanup(self): - """清理Panda3D资源""" - try: - print("🧹 清理CustomPanda3DWidget资源...") - - # 清理world资源 - if hasattr(self, 'world') and self.world: - # 如果world有cleanup方法,调用它 - if hasattr(self.world, 'cleanup'): - self.world.cleanup() - # 清理world引用 - self.world = None - - # 调用父类的清理方法(如果存在) - if hasattr(super(), 'cleanup'): - super().cleanup() - - print("✅ CustomPanda3DWidget资源清理完成") - - except Exception as e: - print(f"⚠️ 清理CustomPanda3DWidget资源时出错: {e}") - -class CustomFileView(QTreeView): - """自定义文件浏览器""" - - def __init__(self, world, parent=None): - if parent is None: - parent = wrapinstance(0, QWidget) - super().__init__(parent) - base_font = self.font() - base_font.setFamily("Inter") - base_font.setPointSize(10) - base_font.setWeight(QFont.Normal) - self.setFont(base_font) - if self.model(): - self.model().rowsInserted.connect(self._handle_rows_inserted) - self.world = world - self.setupUI() - self.setupDragDrop() - - def setupUI(self): - """初始化UI设置""" - self.setHeaderHidden(True) - # 启用多选和拖拽 - self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) - self.setDropIndicatorShown(True) # 启用拖放指示线 - - def setupDragDrop(self): - """设置拖拽功能""" - # 使用自定义拖拽模式 - self.setDragDropMode(QTreeView.DragOnly) # 只允许拖出,不允许拖入 - self.setDefaultDropAction(Qt.DropAction.MoveAction) - self.setDragEnabled(True) - self.setAcceptDrops(True) - - def startDrag(self, supportedActions): - """开始拖拽操作""" - # 获取选中的文件 - indexes = self.selectedIndexes() - if not indexes: - return - - # 只处理文件名列 - indexes = [idx for idx in indexes if idx.column() == 0] - - # 创建 MIME 数据 - mimeData = self.model().mimeData(indexes) - - # 检查是否包含支持的模型文件 - urls = [] - for index in indexes: - filepath = self.model().filePath(index) - if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - urls.append(QUrl.fromLocalFile(filepath)) - - if not urls: - return - - # 设置 URL 列表 - mimeData.setUrls(urls) - - # 创建拖拽对象 - drag = QDrag(self) - drag.setMimeData(mimeData) - - # 设置拖拽图标(可选) - pixmap = QPixmap(32, 32) - pixmap.fill(Qt.transparent) - painter = QPainter(pixmap) - painter.drawText(pixmap.rect(), Qt.AlignCenter, str(len(urls))) - painter.end() - drag.setPixmap(pixmap) - - # 执行拖拽 - drag.exec_(supportedActions) - - def mouseDoubleClickEvent(self, event): - """双击标题栏切换最大化""" - index = self.indexAt(event.pos()) - if index.isValid(): - model = self.model() - filepath = model.filePath(index) - - # 检查是否是模型文件 - if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - self.world.importModel(filepath) - else: - print("不支持的文件类型") - super().mouseDoubleClickEvent(event) - -from PyQt5.QtCore import QFileSystemWatcher,QTimer -import os - -class CustomAssetsTreeWidget(QTreeWidget): - def __init__(self, world, parent=None): - if parent is None: - parent = wrapinstance(0, QWidget) - super().__init__(parent) - self.world = world - self.root_path = None - self.setupUI() - self.setupDragDrop() - - #添加文件系统监控器 - self.file_watcher = QFileSystemWatcher() - self.file_watcher.directoryChanged.connect(self.onDirectoryChanged) - self.file_watcher.fileChanged.connect(self.onFileChanged) - - self.refresh_timer = QTimer() - self.refresh_timer.setSingleShot(True) - self.refresh_timer.timeout.connect(self.refreshView) - - #存储监控的目录 - self.watched_directories = set() - - # 默认加载项目根路径 - self.load_file_tree() - # 设置右键菜单 - self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - self.customContextMenuRequested.connect(self.showContextMenu) - - def showContextMenu(self, position): - """显示右键菜单""" - item = self.itemAt(position) - if not item: - return - - filepath = item.data(0, Qt.UserRole) - is_folder = item.data(0, Qt.UserRole + 1) - - menu = QMenu(self) - - if is_folder: - # 文件夹右键菜单 - menu.addAction("📁 新建文件夹", lambda: self.createNewFolder(item)) - menu.addAction("📄 新建文件", lambda: self.createNewFile(item)) - menu.addSeparator() - menu.addAction("✏️ 重命名", lambda: self.renameItem(item)) - menu.addAction("🗑️ 删除", lambda: self.deleteItem(item)) - menu.addSeparator() - menu.addAction("📋 复制路径", lambda: self.copyPath(filepath)) - menu.addAction("🔍 查看属性", lambda: self.showProperties(item)) - else: - # 文件右键菜单 - menu.addAction("📂 打开文件", lambda: self.openFile(filepath)) - menu.addSeparator() - menu.addAction("✏️ 重命名", lambda: self.renameItem(item)) - menu.addAction("🗑️ 删除", lambda: self.deleteItem(item)) - menu.addSeparator() - menu.addAction("📋 复制路径", lambda: self.copyPath(filepath)) - menu.addAction("🔍 查看属性", lambda: self.showProperties(item)) - - # 显示菜单 - menu.exec_(self.mapToGlobal(position)) - - def _saveExpandedState(self): - """保存展开状态""" - expanded_paths = set() - - def collectExpanded(item): - if item.isExpanded(): - path = item.data(0, Qt.UserRole) - if path: - expanded_paths.add(path) - - for i in range(item.childCount()): - collectExpanded(item.child(i)) - - # 遍历所有顶级项目 - for i in range(self.topLevelItemCount()): - collectExpanded(self.topLevelItem(i)) - - return expanded_paths - - def _restoreExpandedState(self, expanded_paths): - """恢复展开状态""" - def restoreExpanded(item): - path = item.data(0, Qt.UserRole) - if path in expanded_paths: - item.setExpanded(True) - - for i in range(item.childCount()): - restoreExpanded(item.child(i)) - - # 遍历所有顶级项目 - for i in range(self.topLevelItemCount()): - restoreExpanded(self.topLevelItem(i)) - - def _refreshWithStatePreservation(self): - """刷新树形视图并保持状态""" - # 保存当前状态 - expanded_paths = self._saveExpandedState() - - # 刷新树形结构 - self.load_file_tree() - - # 恢复展开状态 - self._restoreExpandedState(expanded_paths) - - def createNewFolder(self, parent_item): - """新建文件夹""" - import os - - parent_path = parent_item.data(0, Qt.UserRole) - dialog = StyledTextInputDialog(self, "新建文件夹", "文件夹名称", "请输入文件夹名称") - if dialog.exec_() != QDialog.Accepted: - return - - folder_name = dialog.text() - if not folder_name: - return - - new_folder_path = os.path.join(parent_path, folder_name) - try: - os.makedirs(new_folder_path, exist_ok=True) - self._refreshWithStatePreservation() - print(f"新建文件夹: {new_folder_path}") - except OSError as e: - print(f"新建文件夹失败: {e}") - - - def createNewFile(self, parent_item): - """新建文件""" - import os - - parent_path = parent_item.data(0, Qt.UserRole) - dialog = StyledTextInputDialog(self, "新建文件", "文件名称", "请输入文件名称") - if dialog.exec_() != QDialog.Accepted: - return - - file_name = dialog.text() - if not file_name: - return - - new_file_path = os.path.join(parent_path, file_name) - try: - with open(new_file_path, "w", encoding="utf-8") as f: - f.write("") - self._refreshWithStatePreservation() - print(f"新建文件: {new_file_path}") - except OSError as e: - print(f"新建文件失败: {e}") - - - def renameItem(self, item): - """重命名文件或文件夹""" - import os - - old_path = item.data(0, Qt.UserRole) - old_name = os.path.basename(old_path) - - dialog = StyledTextInputDialog(self, "重命名", "新名称", "请输入新名称", old_name) - if dialog.exec_() != QDialog.Accepted: - return - - new_name = dialog.text() - if not new_name or new_name == old_name: - return - - parent_dir = os.path.dirname(old_path) - new_path = os.path.join(parent_dir, new_name) - - try: - os.rename(old_path, new_path) - item.setText(0, new_name) - item.setData(0, Qt.UserRole, new_path) - self._refreshWithStatePreservation() - except OSError as e: - print(f"重命名失败: {e}") - - - def deleteItem(self, item): - """删除文件或文件夹""" - import os - import shutil - - filepath = item.data(0, Qt.UserRole) - is_folder = item.data(0, Qt.UserRole + 1) - - item_type = "文件夹" if is_folder else "文件" - result = UniversalMessageDialog.show_warning( - self, - "确认删除", - f"确定要删除这个{item_type}吗?\n{filepath}", - show_cancel=True, - confirm_text="删除", - cancel_text="取消" - ) - - if result == QDialog.Accepted: - try: - if is_folder: - shutil.rmtree(filepath) - else: - os.remove(filepath) - self._refreshWithStatePreservation() - print(f"删除{item_type}: {filepath}") - except OSError as e: - print(f"删除{item_type}失败: {e}") - UniversalMessageDialog.show_error( - self, - "错误", - f"删除{item_type}失败: {e}", - show_cancel=False, - confirm_text="确认" - ) - - def copyPath(self, filepath): - """复制路径到剪贴板""" - from PyQt5.QtWidgets import QApplication - - clipboard = QApplication.clipboard() - clipboard.setText(filepath) - print(f"已复制路径: {filepath}") - - def openFile(self, filepath): - """打开文件""" - import os - import subprocess - import platform - - try: - system = platform.system() - if system == "Windows": - os.startfile(filepath) - elif system == "Darwin": # macOS - subprocess.run(["open", filepath]) - else: # Linux - subprocess.run(["xdg-open", filepath]) - print(f"打开文件: {filepath}") - except Exception as e: - print(f"打开文件失败: {e}") - - def showProperties(self, item): - """显示属性面板""" - import os - - filepath = item.data(0, Qt.UserRole) - is_folder = item.data(0, Qt.UserRole + 1) - - try: - stat = os.stat(filepath) - size = stat.st_size - modified = os.path.getmtime(filepath) - - import datetime - modified_str = datetime.datetime.fromtimestamp(modified).strftime('%Y-%m-%d %H:%M:%S') - - item_type = "文件夹" if is_folder else "文件" - size_str = f"{size} 字节" if not is_folder else "文件夹" - - properties = f""" - 路径: {filepath} - 类型: {item_type} - 大小: {size_str} - 修改时间: {modified_str} - """ - - UniversalMessageDialog.show_info( - self, - "属性", - properties.strip(), - show_cancel=False, - confirm_text="确认" - ) - - except OSError as e: - UniversalMessageDialog.show_warning( - self, - "错误", - f"无法获取属性: {e}", - show_cancel=False, - confirm_text="确认" - ) - - - # def mouseDoubleClickEvent(self, event): - # """处理双击事件""" - # item = self.itemAt(event.pos()) - # if item: - # filepath = item.data(0, Qt.UserRole) - # is_folder = item.data(0, Qt.UserRole + 1) - # - # if is_folder: - # # 文件夹:展开/折叠 - # item.setExpanded(not item.isExpanded()) - # else: - # # 文件:检查是否是模型文件 - # if filepath and filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - # self.world.importModel(filepath) - # else: - # # 其他文件:用系统默认程序打开 - # self.openFile(filepath) - # - # super().mouseDoubleClickEvent(event) - - def setupUI(self): - """初始化UI设置""" - self.setHeaderHidden(True) - # 启用多选和拖拽 - self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) - self.setDropIndicatorShown(True) # 启用拖放指示线 - - def setupDragDrop(self): - """设置拖拽功能""" - # 使用InternalMove模式以正确显示插入指示线 - self.setDragDropMode(QAbstractItemView.InternalMove) - self.setDefaultDropAction(Qt.DropAction.MoveAction) - self.setDragEnabled(True) - self.setAcceptDrops(True) - self.setDropIndicatorShown(True) - - def getProjectRootPath(self): - """获取项目根路径下的Resources文件夹,考虑跨平台""" - import os - - # 获取当前文件所在目录,然后向上查找项目根目录 - current_dir = os.path.dirname(os.path.abspath(__file__)) - - # 向上查找直到找到项目根目录(包含特定标识文件或文件夹) - project_root = current_dir - max_depth = 10 # 限制向上查找的深度 - depth = 0 - - while depth < max_depth: - # 检查是否是项目根目录(可以根据实际情况调整判断条件) - if (os.path.exists(os.path.join(project_root, "main.py")) or - os.path.exists(os.path.join(project_root, "setup.py")) or - os.path.exists(os.path.join(project_root, ".git"))): - break - parent_dir = os.path.dirname(project_root) - if parent_dir == project_root: # 已经到达文件系统根目录 - # 回退到使用当前工作目录 - project_root = os.getcwd() - break - project_root = parent_dir - depth += 1 - - # 构建Resources文件夹路径(跨平台) - resources_path = os.path.join(project_root, "Resources") - - # 如果Resources文件夹不存在,创建它 - if not os.path.exists(resources_path): - try: - os.makedirs(resources_path, exist_ok=True) - print(f"创建Resources文件夹: {resources_path}") - except OSError as e: - print(f"无法创建Resources文件夹: {e}") - # 如果无法创建,回退到项目根路径 - return project_root - - return resources_path - - # def getProjectRootPath(self): - # """获取项目根路径下的Resources文件夹,考虑跨平台""" - # import os - # - # # 获取项目根路径 - # project_root = os.getcwd() - # - # # 构建Resources文件夹路径(跨平台) - # resources_path = os.path.join(project_root, "Resources") - # - # # 如果Resources文件夹不存在,创建它 - # if not os.path.exists(resources_path): - # try: - # os.makedirs(resources_path, exist_ok=True) - # print(f"创建Resources文件夹: {resources_path}") - # except OSError as e: - # print(f"无法创建Resources文件夹: {e}") - # # 如果无法创建,回退到项目根路径 - # return project_root - # - # return resources_path - - def load_file_tree(self): - """加载树形视图""" - self.clear() - self.current_path = self.getProjectRootPath() - try: - # 创建当前目录的根节点 - root_name = os.path.basename(self.current_path) or self.current_path - root_item = QTreeWidgetItem([f"📁 {root_name}"]) - root_item.setData(0, Qt.UserRole, self.current_path) - root_item.setData(0, Qt.UserRole + 1, True) - font = QFont("Microsoft YaHei", 10, QFont.Light) # 假设您希望字体大小为10 - root_item.setFont(0, font) - self.addTopLevelItem(root_item) - - # 加载当前目录内容 - self.load_directory_tree(self.current_path, root_item) - - #添加目录到监控器 - self.addWatchedDirectory(self.current_path) - - # 展开根节点 - root_item.setExpanded(True) - - except PermissionError: - error_item = QTreeWidgetItem(["❌ 无权限访问此目录"]) - self.addTopLevelItem(error_item) - - def addWatchedDirectory(self,directory): - """添加监控目录""" - if os.path.exists(directory) and directory not in self.watched_directories: - if self.file_watcher.addPath(directory): - self.watched_directories.add(directory) - #print(f"开始监控目录:{directory}") - try: - for item in os.listdir(directory): - item_path = os.path.join(directory,item) - if os.path.isdir(item_path): - self.addWatchedDirectory(item_path) - except Exception as e: - pass - else: - print(f"无法监控目录:{directory}") - - def removeWatchedDirectory(self,directory): - """移除监控目录""" - if directory in self.watched_directories: - if self.file_watcher.removePath(directory): - self.watched_directories.discard(directory) - print(f"停止监控目录:{directory}") - else: - print(f"无法停止监控目录:{directory}") - - def onDirectoryChanged(self,path): - """目录发生变化时处理""" - print(f"目录变化{path}") - if not self.refresh_timer.isActive(): - self.refresh_timer.start(1000) - - def onFileChanged(self,path): - """目录发生变化时的处理""" - print(f"目录变化{path}") - if not self.refresh_timer.isActive(): - self.refresh_timer.start(1000) - - def refreshView(self): - """刷新视图""" - print("刷新资源视图...") - try: - expanded_paths = self._saveExpandedState() - self.load_file_tree() - self._restoreExpandedState(expanded_paths) - print("资源视图刷新完成") - except Exception as e: - print(f"刷新资源视图失败{e}") - - def load_directory_tree(self, path, parent_item, max_depth=3, current_depth=0): - """递归加载目录树(类似左侧导航面板)""" - if current_depth >= max_depth: - return - - try: - items = os.listdir(path) - items.sort() - - # 分别处理文件夹和文件 - folders = [] - files = [] - - for item in items: - # 跳过隐藏文件和系统文件 - if item.startswith('.') or item.startswith('__'): - continue - - item_path = os.path.join(path, item) - if os.path.isdir(item_path): - folders.append(item) - elif os.path.isfile(item_path): - files.append(item) - - # 先添加文件夹 - for folder in folders: - folder_path = os.path.join(path, folder) - folder_item = self.create_simple_tree_item(folder, folder_path, True) - parent_item.addChild(folder_item) - - # 递归加载子目录(限制深度) - if current_depth < max_depth - 1: - self.load_directory_tree(folder_path, folder_item, max_depth, current_depth + 1) - - # 再添加文件(显示重要文件类型,包括图片和模型) - important_extensions = { - # 编程文件 - '.py', '.js', '.html', '.css', '.java', '.cpp', '.c', '.php', '.rb', '.go', '.rs', - # 文档文件 - '.md', '.txt', '.pdf', '.doc', '.docx', '.rtf', - # 配置和数据文件 - '.json', '.xml', '.yaml', '.yml', '.ini', '.cfg', '.toml', - # 图片文件 - '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.ico', '.webp', '.tiff', '.tif', - # 3D模型文件 - '.fbx', '.obj', '.3ds', '.max', '.blend', '.dae', '.gltf', '.glb', '.stl', '.ply', - # 音视频文件 - '.mp3', '.wav', '.mp4', '.avi', '.mov', '.wmv', '.flac', '.aac', - # 压缩文件 - '.zip', '.rar', '.7z', '.tar', '.gz', - # 材质和纹理 - '.mtl', '.mat', '.exr', '.hdr', '.hdri', '.dds', - # CAD文件 - '.dwg', '.dxf', '.step', '.stp', '.iges', '.sldprt', '.sldasm', - # 其他重要文件 - '.exe', '.dll', '.bat', '.sh', '.log' - } - - # 重要文件名(不依赖扩展名) - important_files = {'requirements.txt', 'README.md', 'main.py', 'LICENSE', 'CHANGELOG.md', 'INSTALL.md'} - - for file in files: - file_ext = os.path.splitext(file)[1].lower() - if file_ext in important_extensions or file in important_files: - file_path = os.path.join(path, file) - file_item = self.create_simple_tree_item(file, file_path, False) - parent_item.addChild(file_item) - - except (OSError, PermissionError): - pass - - def create_simple_tree_item(self, name, path, is_folder): - """创建简单的树形项目""" - try: - if is_folder: - # 文件夹项目,展开/折叠图标由CSS样式控制 - item = QTreeWidgetItem([f"📁 {name}"]) - item.setData(0, Qt.UserRole + 1, True) # 标记为文件夹 - else: - icon = self.get_file_icon(name) - item = QTreeWidgetItem([f"{icon} {name}"]) - item.setData(0, Qt.UserRole + 1, False) # 标记为文件 - - item.setData(0, Qt.UserRole, path) - - return item - - except (OSError, PermissionError): - item = QTreeWidgetItem([name]) - item.setData(0, Qt.UserRole, path) - return item - - def get_file_icon(self, filename): - """ - 根据文件扩展名获取图标 - - 这个函数根据文件的扩展名返回对应的Unicode图标字符。 - 如果文件类型不在映射表中,返回默认的文档图标。 - - 参数: - filename (str): 文件名(包含扩展名) - - 返回值: - str: 对应的Unicode图标字符 - """ - # 获取文件扩展名并转换为小写 - ext = os.path.splitext(filename)[1].lower() - - # 文件扩展名到图标的映射表 - icon_map = { - # 编程语言文件 - '.py': '🐍', # Python文件 - '.js': '⚡', # JavaScript文件 - '.html': '🌐', # HTML文件 - '.css': '🎨', # CSS样式文件 - '.java': '☕', # Java文件 - '.cpp': '⚙️', # C++文件 - '.c': '⚙️', # C文件 - '.h': '📋', # 头文件 - '.php': '🐘', # PHP文件 - '.rb': '💎', # Ruby文件 - '.go': '🐹', # Go文件 - '.rs': '🦀', # Rust文件 - '.swift': '🐦', # Swift文件 - '.kt': '🎯', # Kotlin文件 - - # 文档文件 - '.txt': '📄', # 纯文本文件 - '.md': '📝', # Markdown文档 - '.rst': '📝', # reStructuredText文档 - '.pdf': '📕', # PDF文档 - '.doc': '📘', # Word文档 - '.docx': '📘', # Word文档 - '.rtf': '📄', # RTF文档 - '.odt': '📄', # OpenDocument文本 - - # 数据文件 - '.json': '📋', # JSON数据文件 - '.xml': '📋', # XML文件 - '.yaml': '📋', # YAML文件 - '.yml': '📋', # YAML文件 - '.csv': '📊', # CSV表格文件 - '.xls': '📗', # Excel表格 - '.xlsx': '📗', # Excel表格 - '.ods': '📊', # OpenDocument表格 - - # 图像文件 - '.jpg': '🖼️', # JPEG图像 - '.jpeg': '🖼️', # JPEG图像 - '.png': '🖼️', # PNG图像 - '.gif': '🖼️', # GIF图像 - '.bmp': '🖼️', # BMP图像 - '.svg': '🎨', # SVG矢量图 - '.ico': '🎯', # 图标文件 - '.webp': '🖼️', # WebP图像 - '.tiff': '🖼️', # TIFF图像 - '.tif': '🖼️', # TIFF图像 - - # 音视频文件 - '.mp4': '🎬', # MP4视频 - '.avi': '🎬', # AVI视频 - '.mov': '🎬', # MOV视频 - '.wmv': '🎬', # WMV视频 - '.flv': '🎬', # FLV视频 - '.webm': '🎬', # WebM视频 - '.mp3': '🎵', # MP3音频 - '.wav': '🎵', # WAV音频 - '.flac': '🎵', # FLAC音频 - '.aac': '🎵', # AAC音频 - '.ogg': '🎵', # OGG音频 - - # 压缩文件 - '.zip': '📦', # ZIP压缩包 - '.rar': '📦', # RAR压缩包 - '.7z': '📦', # 7Z压缩包 - '.tar': '📦', # TAR归档 - '.gz': '📦', # GZIP压缩 - '.bz2': '📦', # BZIP2压缩 - '.xz': '📦', # XZ压缩 - - # 可执行文件 - '.exe': '⚙️', # Windows可执行文件 - '.msi': '📦', # Windows安装包 - '.deb': '📦', # Debian包 - '.rpm': '📦', # RPM包 - '.dmg': '💿', # macOS磁盘镜像 - '.app': '📱', # macOS应用程序 - - # 系统文件 - '.dll': '🔧', # 动态链接库 - '.so': '🔧', # 共享库 - '.dylib': '🔧', # macOS动态库 - '.lib': '📚', # 静态库 - - # 脚本文件 - '.bat': '📜', # Windows批处理 - '.cmd': '📜', # Windows命令脚本 - '.sh': '📜', # Shell脚本 - '.ps1': '💙', # PowerShell脚本 - '.vbs': '📜', # VBScript脚本 - - # 配置文件 - '.ini': '⚙️', # INI配置文件 - '.cfg': '⚙️', # 配置文件 - '.conf': '⚙️', # 配置文件 - '.config': '⚙️', # 配置文件 - '.toml': '⚙️', # TOML配置文件 - - # 3D模型文件 - '.fbx': '🎭', # FBX模型文件 - '.obj': '🎭', # OBJ模型文件 - '.3ds': '🎭', # 3DS Max模型 - '.max': '🎭', # 3DS Max场景 - '.blend': '🎭', # Blender模型 - '.dae': '🎭', # COLLADA模型 - '.gltf': '🎭', # glTF模型 - '.glb': '🎭', # glTF二进制模型 - '.x3d': '🎭', # X3D模型 - '.ply': '🎭', # PLY模型 - '.stl': '🎭', # STL模型(3D打印) - '.off': '🎭', # OFF模型 - '.3mf': '🎭', # 3MF模型 - '.amf': '🎭', # AMF模型 - '.x': '🎭', # DirectX模型 - '.md2': '🎭', # Quake II模型 - '.md3': '🎭', # Quake III模型 - '.mdl': '🎭', # Source引擎模型 - '.mesh': '🎭', # OGRE模型 - '.scene': '🎭', # OGRE场景 - '.ac': '🎭', # AC3D模型 - '.ase': '🎭', # ASCII Scene Export - '.assbin': '🎭', # Assimp二进制 - '.b3d': '🎭', # Blitz3D模型 - '.bvh': '🎭', # BioVision层次 - '.csm': '🎭', # CharacterStudio Motion - '.hmp': '🎭', # 3D GameStudio模型 - '.irr': '🎭', # Irrlicht场景 - '.irrmesh': '🎭', # Irrlicht网格 - '.lwo': '🎭', # LightWave对象 - '.lws': '🎭', # LightWave场景 - '.ms3d': '🎭', # MilkShape 3D - '.nff': '🎭', # Neutral文件格式 - '.q3o': '🎭', # Quick3D对象 - '.q3s': '🎭', # Quick3D场景 - '.raw': '🎭', # RAW三角形 - '.smd': '🎭', # Valve SMD - '.ter': '🎭', # Terragen地形 - '.uc': '🎭', # Unreal模型 - '.vta': '🎭', # Valve VTA - '.xgl': '🎭', # XGL模型 - '.zgl': '🎭', # ZGL模型 - - # 纹理和材质文件 - '.mtl': '🎨', # OBJ材质文件 - '.mat': '🎨', # 材质文件 - '.sbsar': '🎨', # Substance Archive - '.sbs': '🎨', # Substance Designer - '.sbsm': '🎨', # Substance材质 - '.exr': '🖼️', # OpenEXR高动态范围图像 - '.hdr': '🖼️', # HDR图像 - '.hdri': '🖼️', # HDRI环境贴图 - '.dds': '🖼️', # DirectDraw Surface - '.ktx': '🖼️', # Khronos纹理 - '.astc': '🖼️', # ASTC纹理 - '.pvr': '🖼️', # PowerVR纹理 - '.etc1': '🖼️', # ETC1纹理 - '.etc2': '🖼️', # ETC2纹理 - - # 动画文件 - '.anim': '🎬', # 动画文件 - '.fbx': '🎬', # FBX动画(也可以是模型) - '.bip': '🎬', # Character Studio Biped - '.cal3d': '🎬', # Cal3D动画 - '.motion': '🎬', # 动作文件 - '.mocap': '🎬', # 动作捕捉数据 - - # 其他常见文件 - '.log': '📋', # 日志文件 - '.tmp': '🗂️', # 临时文件 - '.bak': '💾', # 备份文件 - '.old': '📦', # 旧文件 - } - - # 返回对应的图标,如果找不到则返回默认文档图标 - return icon_map.get(ext, '📄') - - def startDrag(self, supportedActions): - """开始拖拽操作""" - selected_items = self.selectedItems() - if not selected_items: - return - - # 创建 MIME 数据 - mimeData = QMimeData() - - # 收集文件路径用于向外拖拽 - urls = [] - internal_paths = [] - - for item in selected_items: - filepath = item.data(0, Qt.UserRole) - if filepath: - internal_paths.append(filepath) - # 检查是否是模型文件(用于向外拖拽) - if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - urls.append(QUrl.fromLocalFile(filepath)) - - # 设置内部拖拽数据 - mimeData.setText('\n'.join(internal_paths)) - - # 设置向外拖拽数据 - if urls: - mimeData.setUrls(urls) - - # 创建拖拽对象 - drag = QDrag(self) - drag.setMimeData(mimeData) - - # 设置拖拽图标 - pixmap = QPixmap(32, 32) - pixmap.fill(Qt.transparent) - painter = QPainter(pixmap) - painter.drawText(pixmap.rect(), Qt.AlignCenter, str(len(selected_items))) - painter.end() - drag.setPixmap(pixmap) - - # 执行拖拽 - drag.exec_(supportedActions) - - def dragEnterEvent(self, event): - """处理拖拽进入事件""" - # 检查是否是内部拖拽 - if event.mimeData().hasText(): - event.acceptProposedAction() - else: - event.ignore() - - def dragMoveEvent(self, event): - """处理拖拽移动事件""" - if not event.mimeData().hasText(): - event.ignore() - return - - # 获取目标项 - target_item = self.itemAt(event.pos()) - selected_items = self.selectedItems() - - # 检查是否拖拽到多选区域内的项目 - if target_item and target_item in selected_items: - event.ignore() - return - - # 检查是否拖拽到自己的子级 - if target_item: - for selected_item in selected_items: - if self._isChildOf(target_item, selected_item): - event.ignore() - return - - # 如果拖到文件夹,允许拖到文件夹内部 - if target_item: - is_folder = target_item.data(0, Qt.UserRole + 1) - if is_folder: - # 接受拖放,显示指示框 - event.acceptProposedAction() - return - - - # 调用父类方法处理插入指示线 - event.accept() - super().dragMoveEvent(event) - - def _isChildOf(self, potential_child, potential_parent): - """检查 potential_child 是否是 potential_parent 的子级""" - current = potential_child.parent() - while current: - if current == potential_parent: - return True - current = current.parent() - return False - - def dropEvent(self, event): - """处理拖放事件""" - if not event.mimeData().hasText(): - event.ignore() - return - - drag_paths = event.mimeData().text().split('\n') - if not drag_paths: - event.ignore() - return - - # 获取目标项 - target_item = self.itemAt(event.pos()) - if not target_item: - # 如果拖到空白处,默认放到根目录 - target_path = self.current_path - else: - # 如果是文件夹,就放到里面 - is_folder = target_item.data(0, Qt.UserRole + 1) - if is_folder: - target_path = target_item.data(0, Qt.UserRole) - else: - # 如果是文件,则放到其父目录 - parent_item = target_item.parent() - if parent_item: - target_path = parent_item.data(0, Qt.UserRole) - else: - target_path = self.current_path - - # 执行移动 - self._moveFiles(drag_paths, target_path) - event.acceptProposedAction() - # 让 Qt 更新界面 - super().dropEvent(event) - - def _moveFiles(self, source_paths, target_dir): - """移动文件到目标目录""" - import os - import shutil - from PyQt5.QtWidgets import QMessageBox - - moved_files = [] - failed_files = [] - - for source_path in source_paths: - if not source_path or not os.path.exists(source_path): - continue - - if os.path.isdir(source_path) and target_dir.startswith(source_path): - failed_files.append(f"{source_path} (不能移动到子目录)") - continue - - if os.path.dirname(source_path) == target_dir: - continue - - filename = os.path.basename(source_path) - target_path = os.path.join(target_dir, filename) - - if os.path.exists(target_path): - failed_files.append(f"{filename} (目标已存在)") - continue - - try: - shutil.move(source_path, target_path) - moved_files.append(filename) - print(f"移动文件: {source_path} -> {target_path}") - except Exception as e: - failed_files.append(f"{filename} ({str(e)})") - - # 使用状态保持刷新 - if moved_files: - self._refreshWithStatePreservation() - - if failed_files: - StyledMessageBox.warning( - self, - "移动失败", - f"以下文件移动失败:\n" + "\n".join(failed_files) - ) - - if moved_files: - print(f"成功移动 {len(moved_files)} 个文件") - - # def mouseDoubleClickEvent(self, event): - # """处理双击事件""" - # item = self.itemAt(event.pos()) - # if item: - # filepath = item.data(0, Qt.UserRole) - # is_folder = item.data(0, Qt.UserRole + 1) - # - # if is_folder: - # # 文件夹:展开/折叠 - # item.setExpanded(not item.isExpanded()) - # else: - # # 文件:检查是否是模型文件 - # if filepath and filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): - # self.world.importModel(filepath) - # else: - # # 其他文件:用系统默认程序打开 - # self.openFile(filepath) - # - # super().mouseDoubleClickEvent(event) - - -class CustomConsoleDockWidget(QWidget): - """自定义控制台停靠部件""" - - def __init__(self, world, parent=None): - if parent is None: - parent = wrapinstance(0, QWidget) - super().__init__(parent) - self.world = world - self.setupUI() - self.setupConsoleRedirect() - - def setupUI(self): - """初始化控制台UI""" - layout = QVBoxLayout(self) - - # 控制台工具栏 - toolbar = QHBoxLayout() - - # 清空按钮 - self.clearBtn = QPushButton("清空") - self.clearBtn.setStyleSheet(""" - QPushButton { - background-color: rgba(84, 89, 98, 0.5); - color: #ffffff; - border: none; - padding: 4px 12px; - border-radius: 2px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 12px; - font-weight: 300; - letter-spacing: 0.7px; - } - QPushButton:hover { - background-color: rgba(84, 89, 98, 0.7); - } - QPushButton:pressed { - background-color: rgba(84, 89, 98, 0.9); - } - """) - self.clearBtn.clicked.connect(self.clearConsole) - toolbar.addWidget(self.clearBtn) - - # 自动滚动开关 - self.autoScrollBtn = QPushButton("自动滚动") - self.autoScrollBtn.setCheckable(True) - self.autoScrollBtn.setChecked(True) - self.autoScrollBtn.setStyleSheet(""" - QPushButton { - background-color: rgba(84, 89, 98, 0.5); - color: #ffffff; - border: none; - padding: 4px 12px; - border-radius: 2px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 12px; - font-weight: 300; - letter-spacing: 0.7px; - } - QPushButton:checked { - background-color: #3067c0; - color: #ffffff; - } - QPushButton:hover { - background-color: rgba(84, 89, 98, 0.7); - } - QPushButton:pressed { - background-color: rgba(84, 89, 98, 0.9); - } - """) - toolbar.addWidget(self.autoScrollBtn) - - # 时间戳开关 - self.timestampBtn = QPushButton("显示时间") - self.timestampBtn.setCheckable(True) - self.timestampBtn.setChecked(True) - self.timestampBtn.setStyleSheet(""" - QPushButton { - background-color: rgba(84, 89, 98, 0.5); - color: #ffffff; - border: none; - padding: 4px 12px; - border-radius: 2px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 12px; - font-weight: 300; - letter-spacing: 0.7px; - } - QPushButton:checked { - background-color: #3067c0; - color: #ffffff; - } - QPushButton:hover { - background-color: rgba(84, 89, 98, 0.7); - } - QPushButton:pressed { - background-color: rgba(84, 89, 98, 0.9); - } - """) - toolbar.addWidget(self.timestampBtn) - - self.fpsLabel = QLabel("FPS:0.0") - self.fpsLabel.setStyleSheet(""" - QLabel { - background-color: #3067c0; - color: #ffffff; - border: none; - padding: 4px 12px; - border-radius: 2px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 12px; - font-weight: 300; - letter-spacing: 0.7px; - } - """) - self.fpsLabel.setMinimumWidth(100) - self.fpsLabel.setAlignment(Qt.AlignCenter) - toolbar.addWidget(self.fpsLabel) - - # 帧率更新定时器 - self.fpsTimer = QTimer() - self.fpsTimer.timeout.connect(self.updateFPS) - self.fpsTimer.start(105) # 每秒更新一次 - - toolbar.addStretch() - layout.addLayout(toolbar) - - # 控制台文本区域 - from PyQt5.QtWidgets import QTextEdit - self.consoleText = QTextEdit() - self.consoleText.setReadOnly(True) - self.consoleText.setStyleSheet(""" - QTextEdit { - background-color: #19191b; - color: #ebebeb; - font-family: 'Consolas', 'Monaco', 'Microsoft YaHei', monospace; - font-size: 10px; - font-weight: 300; - border: none; - letter-spacing: 0.5px; - line-height: 12px; - } - """) - layout.addWidget(self.consoleText) - - # # 命令输入区域 - # inputLayout = QHBoxLayout() - # inputLayout.addWidget(QLabel(">>> ")) - # - # self.commandInput = QLineEdit() - # self.commandInput.setStyleSheet(""" - # QLineEdit { - # background-color: #2d2d2d; - # color: #ffffff; - # font-family: 'Consolas', 'Monaco', monospace; - # font-size: 10pt; - # border: 1px solid #3e3e3e; - # padding: 5px; - # } - # """) - # self.commandInput.returnPressed.connect(self.executeCommand) - # inputLayout.addWidget(self.commandInput) - # - # layout.addLayout(inputLayout) - - # 添加欢迎信息 - self.addMessage("🎮 编辑器控制台已启动", "INFO") - - def updateFPS(self): - try: - if hasattr(self.world,'clock'): - fps = self.world.clock.getAverageFrameRate() - self.fpsLabel.setText(f"FPS:{fps:.1f}") - - # 根据帧率设置颜色 - if fps >= 50: - color = "#80ff80" # 绿色 - 优秀 - elif fps >= 30: - color = "#ffff80" # 黄色 - 一般 - else: - color = "#ff8080" # 红色 - 较差 - - self.fpsLabel.setStyleSheet(f""" - QLabel {{ - background-color: #3067c0; - color: {color}; - border: 1px solid #3a3a4a; - padding: 6px 12px; - border-radius: 4px; - font-weight: 500; - font-family: 'Consolas', 'Monaco', monospace; - }} - """) - except Exception as e: - pass # 静默处理错误,避免影响控制台功能 - - def setupConsoleRedirect(self): - """设置控制台重定向""" - import sys - - # 保存原始的stdout和stderr - self.original_stdout = sys.stdout - self.original_stderr = sys.stderr - - # 创建自定义输出流 - sys.stdout = ConsoleRedirect(self, "STDOUT") - sys.stderr = ConsoleRedirect(self, "STDERR") - - def addMessage(self, message, msg_type="INFO"): - """添加消息到控制台""" - import datetime - - # 获取当前时间 - timestamp = datetime.datetime.now().strftime("%H:%M:%S") - - # 根据消息类型设置颜色 - color_map = { - "INFO": "#ffffff", # 白色 - "WARNING": "#ffaa00", # 橙色 - "ERROR": "#ff4444", # 红色 - "SUCCESS": "#44ff44", # 绿色 - "STDOUT": "#cccccc", # 浅灰色 - "STDERR": "#ff6666", # 浅红色 - } - - color = color_map.get(msg_type, "#ffffff") - - # 构建HTML格式的消息 - if self.timestampBtn.isChecked(): - html_message = f'[{timestamp}] {message}' - else: - html_message = f'{message}' - - # 添加到控制台 - self.consoleText.append(html_message) - - # 自动滚动到底部 - if self.autoScrollBtn.isChecked(): - scrollbar = self.consoleText.verticalScrollBar() - scrollbar.setValue(scrollbar.maximum()) - - def clearConsole(self): - """清空控制台""" - self.consoleText.clear() - self.addMessage("控制台已清空", "INFO") - - def executeCommand(self): - """执行命令""" - command = self.commandInput.text().strip() - if not command: - return - - # 显示输入的命令 - self.addMessage(f">>> {command}", "INFO") - self.commandInput.clear() - - try: - # 执行Python命令 - if hasattr(self.world, 'executeCommand'): - result = self.world.executeCommand(command) - if result: - self.addMessage(str(result), "SUCCESS") - else: - # 简单的eval执行 - result = eval(command) - if result is not None: - self.addMessage(str(result), "SUCCESS") - - except Exception as e: - self.addMessage(f"错误: {str(e)}", "ERROR") - - def cleanup(self): - """清理资源""" - import sys - - # 恢复原始的stdout和stderr - if hasattr(self, 'original_stdout'): - sys.stdout = self.original_stdout - if hasattr(self, 'original_stderr'): - sys.stderr = self.original_stderr - - -class ConsoleRedirect: - """控制台重定向类""" - - def __init__(self, console_widget, stream_type): - self.console_widget = console_widget - self.stream_type = stream_type - - def write(self, text): - """重定向写入""" - if text.strip(): # 忽略空行 - self.console_widget.addMessage(text.strip(), self.stream_type) - - def flush(self): - """刷新缓冲区""" - pass - -class StyledMessageBox: - """统一样式的消息框辅助类""" - - MESSAGEBOX_STYLE = """ - QMessageBox { - background-color: #1e1e1f; - color: #ebebeb; - border: 1px solid rgba(77, 116, 189, 0.3); - border-radius: 8px; - padding: 20px; - min-width: 300px; - } - QMessageBox QLabel { - color: #ffffff; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 13px; - font-weight: 400; - line-height: 1.4; - padding: 10px 0px; - } - QMessageBox QPushButton { - background-color: rgba(89, 100, 113, 0.4); - color: rgba(255, 255, 255, 0.8); - border: 1px solid rgba(76, 92, 110, 0.4); - border-radius: 6px; - padding: 8px 16px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-weight: 400; - font-size: 11px; - min-width: 80px; - min-height: 28px; - } - QMessageBox QPushButton:hover { - background-color: rgba(89, 100, 113, 0.6); - border: 1px solid rgba(77, 116, 189, 0.6); - color: #ffffff; - } - QMessageBox QPushButton:pressed, QMessageBox QPushButton:checked { - background-color: rgba(77, 116, 189, 0.8); - border: 1px solid #4d74bd; - color: #ffffff; - } - QMessageBox QPushButton:disabled { - background-color: rgba(89, 100, 113, 0.2); - color: rgba(235, 235, 235, 0.4); - border: 1px solid rgba(76, 92, 110, 0.2); - } - QMessageBox QPushButton:default { - background-color: rgba(77, 116, 189, 0.8); - border: 1px solid #4d74bd; - color: #ffffff; - font-weight: 500; - } - QMessageBox QPushButton:default:hover { - background-color: rgba(77, 116, 189, 1.0); - border: 1px solid rgba(77, 116, 189, 1.0); - } - QMessageBox QIcon { - padding: 0px 10px 0px 0px; - } - """ - - INPUTDIALOG_STYLE = """ - QInputDialog { - background-color: #1e1e1f; - color: #ebebeb; - border: 1px solid rgba(77, 116, 189, 0.3); - border-radius: 8px; - padding: 20px; - min-width: 350px; - } - QLabel { - color: #ffffff; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-weight: 500; - font-size: 13px; - padding: 0px 0px 10px 0px; - } - QLineEdit { - background-color: rgba(89, 100, 113, 0.15); - color: #ebebeb; - border: 1px solid rgba(76, 92, 110, 0.4); - border-radius: 6px; - padding: 10px 12px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-size: 11px; - font-weight: 400; - min-height: 16px; - } - QLineEdit:focus { - border: 1px solid #4d74bd; - background-color: rgba(77, 116, 189, 0.1); - } - QLineEdit:hover { - border: 1px solid rgba(77, 116, 189, 0.6); - background-color: rgba(89, 100, 113, 0.2); - } - QPushButton { - background-color: rgba(89, 100, 113, 0.4); - color: rgba(255, 255, 255, 0.8); - border: 1px solid rgba(76, 92, 110, 0.4); - border-radius: 6px; - padding: 8px 16px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-weight: 400; - font-size: 11px; - min-width: 80px; - min-height: 28px; - } - QPushButton:hover { - background-color: rgba(89, 100, 113, 0.6); - border: 1px solid rgba(77, 116, 189, 0.6); - color: #ffffff; - } - QPushButton:pressed, QPushButton:checked { - background-color: rgba(77, 116, 189, 0.8); - border: 1px solid #4d74bd; - color: #ffffff; - } - QPushButton:disabled { - background-color: rgba(89, 100, 113, 0.2); - color: rgba(235, 235, 235, 0.4); - border: 1px solid rgba(76, 92, 110, 0.2); - } - QPushButton:default { - background-color: rgba(77, 116, 189, 0.8); - border: 1px solid #4d74bd; - color: #ffffff; - font-weight: 500; - } - """ - - BUTTON_STYLE = """ - QPushButton { - background-color: rgba(89, 100, 113, 0.4); - color: rgba(255, 255, 255, 0.8); - border: 1px solid rgba(76, 92, 110, 0.4); - border-radius: 6px; - padding: 8px 16px; - font-family: 'Microsoft YaHei', 'Inter', sans-serif; - font-weight: 400; - font-size: 11px; - min-width: 80px; - min-height: 28px; - } - QPushButton:hover { - background-color: rgba(89, 100, 113, 0.6); - border: 1px solid rgba(77, 116, 189, 0.6); - color: #ffffff; - } - QPushButton:pressed, QPushButton:checked { - background-color: rgba(77, 116, 189, 0.8); - border: 1px solid #4d74bd; - color: #ffffff; - } - QPushButton:disabled { - background-color: rgba(89, 100, 113, 0.2); - color: rgba(235, 235, 235, 0.4); - border: 1px solid rgba(76, 92, 110, 0.2); - } - """ - - @staticmethod - def information(parent, title, message): - """显示信息提示框""" - msg = QMessageBox(QMessageBox.Information, title, message, QMessageBox.Ok, parent) - msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) - # 强制设置所有按钮的样式 - for button in msg.buttons(): - button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) - return msg.exec_() - - @staticmethod - def question(parent, title, message, buttons=QMessageBox.Yes | QMessageBox.No, defaultButton=QMessageBox.No): - """显示确认对话框""" - msg = QMessageBox(QMessageBox.Question, title, message, buttons, parent) - msg.setDefaultButton(defaultButton) - msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) - # 强制设置所有按钮的样式 - for button in msg.buttons(): - button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) - return msg.exec_() - - @staticmethod - def warning(parent, title, message): - """显示警告对话框""" - msg = QMessageBox(QMessageBox.Warning, title, message, QMessageBox.Ok, parent) - msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) - # 强制设置所有按钮的样式 - for button in msg.buttons(): - button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) - return msg.exec_() - - @staticmethod - def critical(parent, title, message): - """显示错误对话框""" - msg = QMessageBox(QMessageBox.Critical, title, message, QMessageBox.Ok, parent) - msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) - # 强制设置所有按钮的样式 - for button in msg.buttons(): - button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) - return msg.exec_() - - @staticmethod - def getText(parent, title, label, text=""): - """显示样式统一的文本输入对话框""" - from PyQt5.QtWidgets import QInputDialog - dialog = QInputDialog(parent) - dialog.setWindowTitle(title) - dialog.setLabelText(label) - dialog.setTextValue(text) - dialog.setStyleSheet(StyledMessageBox.INPUTDIALOG_STYLE) - - # 强制设置所有按钮的样式 - for button in dialog.findChildren(QPushButton): - button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) - - ok = dialog.exec_() - return dialog.textValue(), ok - -class UniversalMessageDialog(QDialog): - """通用消息对话框类 - 支持不同图标和按钮配置""" - - # 消息类型枚举 - SUCCESS = "success_icon" - WARNING = "warning_icon" - ERROR = "fail_icon" - INFO = "info" - - def __init__(self, parent, title, message, message_type=INFO, show_cancel=True, - confirm_text="确认", cancel_text="取消", icon_size=QSize(20, 20)): - """ - 初始化通用消息对话框 - - Args: - parent: 父窗口 - title: 对话框标题 - message: 消息内容 - message_type: 消息类型 (SUCCESS, WARNING, ERROR, INFO) - show_cancel: 是否显示取消按钮 - confirm_text: 确认按钮文字 - cancel_text: 取消按钮文字 - icon_size: 图标尺寸 - """ - super().__init__(parent) - self.setWindowTitle(title) - self.setObjectName("universalMessageDialog") - self.setModal(True) - self.resize(508, 134) - self.setMinimumSize(508, 134) - self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) - self.setAttribute(Qt.WA_TranslucentBackground, True) - - # 对话框配置 - self.message_type = message_type - self.show_cancel = show_cancel - self.confirm_text = confirm_text - self.cancel_text = cancel_text - self.icon_size = icon_size - - # 拖拽相关 - self.dragging = False - self.drag_position = QPoint() - - # 图标管理 - self.icon_manager = get_icon_manager() - self._title_icon_size = QSize(18, 18) - self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size) - - # 根据消息类型获取对应图标 - self._message_icon = self._get_message_icon() - - # 设置样式 - self._setup_styles() - - # 创建UI - self._create_ui(message) - - def _get_message_icon(self): - """根据消息类型获取对应图标""" - icon_map = { - self.SUCCESS: 'success_icon', - self.WARNING: 'warning_icon', - self.ERROR: 'fail_icon', - self.INFO: 'success_icon' # 默认使用成功图标 - } - - icon_name = icon_map.get(self.message_type, 'success_icon') - return self.icon_manager.get_icon(icon_name, self.icon_size) - - def _setup_styles(self): - """设置对话框样式""" - self.setStyleSheet(""" - QDialog#universalMessageDialog { - background-color: transparent; - border: none; - } - QFrame#baseFrame { - background-color: #19191B; - border: 1px solid #3E3E42; - border-radius: 5px; - } - QWidget#titleBar { - background-color: transparent; - border: none; - border-radius: 5px 5px 0px 0px; - min-height: 32px; - max-height: 32px; - } - QWidget#titleBar QWidget { - background-color: transparent; - border: none; - } - QLabel#titleLabel { - color: #FFFFFF; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 14px; - font-weight: 500; - letter-spacing: 0.7px; - } - QWidget#controlButtons QPushButton { - background-color: transparent; - border: none; - color: #EBEBEB; - font-size: 14px; - min-width: 18px; - max-width: 18px; - min-height: 18px; - max-height: 18px; - padding: 0px; - border-radius: 3px; - } - QWidget#controlButtons QPushButton:hover { - background-color: #2A2D2E; - color: #FFFFFF; - } - QPushButton#closeButton { - border-radius: 0px 5px 0px 0px; - } - QPushButton#closeButton:hover { - background-color: #2A2D2E; - color: #FFFFFF; - } - QFrame#titleSeparator { - background-color: #3E3E42; - border: none; - min-height: 1px; - max-height: 1px; - } - QWidget#contentWidget { - background-color: transparent; - border: none; - } - QLabel#messageLabel { - color: #EBEBEB; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 13px; - font-weight: 400; - letter-spacing: 0.6px; - line-height: 1.4; - padding: 0px; - margin: 0px; - } - QPushButton { - background-color: rgba(89, 98, 118, 0.5); - color: #EBEBEB; - border: none; - border-radius: 2px; - padding: 0px 12px; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-weight: 300; - font-size: 10px; - letter-spacing: 0.5px; - min-width: 90px; - max-width: 90px; - min-height: 28px; - max-height: 28px; - } - QPushButton:hover { - background-color: #3067C0; - color: #FFFFFF; - } - QPushButton:pressed { - background-color: #2556A0; - color: #FFFFFF; - } - QPushButton#primaryButton { - background-color: rgba(89, 98, 118, 0.5); - border: none; - color: #EBEBEB; - font-weight: 300; - } - QPushButton#primaryButton:hover { - background-color: #2556A0; - } - QPushButton#primaryButton:pressed { - background-color: #1E4A8C; - } - QPushButton#secondaryButton { - background-color: rgba(89, 98, 118, 0.5); - border: none; - color: #EBEBEB; - } - QPushButton#secondaryButton:hover { - background-color: #3067C0; - color: #FFFFFF; - } - QPushButton#secondaryButton:pressed { - background-color: #2556A0; - color: #FFFFFF; - } - """) - - def _create_ui(self, message): - """构建用户界面""" - main_layout = QVBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setSpacing(0) - - base_frame = QFrame() - base_frame.setObjectName('baseFrame') - base_frame.setFrameShape(QFrame.NoFrame) - base_frame.setAttribute(Qt.WA_StyledBackground, True) - - base_layout = QVBoxLayout(base_frame) - base_layout.setContentsMargins(25, 14, 25, 14) - base_layout.setSpacing(0) - - self._create_title_bar() - base_layout.addWidget(self.title_bar) - base_layout.addSpacing(4) - - title_separator = QFrame() - title_separator.setObjectName('titleSeparator') - title_separator.setFrameShape(QFrame.HLine) - title_separator.setFrameShadow(QFrame.Plain) - base_layout.addWidget(title_separator) - base_layout.addSpacing(10) - - content_widget = QWidget() - content_widget.setObjectName('contentWidget') - content_layout = QVBoxLayout(content_widget) - content_layout.setContentsMargins(0, 0, 0, 0) - content_layout.setSpacing(10) - - message_area = QHBoxLayout() - message_area.setContentsMargins(0, 0, 0, 0) - message_area.setSpacing(10) - - # 用一个垂直布局包裹icon_label,确保顶部对齐 - icon_vbox = QVBoxLayout() - icon_vbox.setContentsMargins(0, 0, 0, 0) - icon_vbox.setSpacing(0) - icon_label = QLabel() - if self._message_icon and not self._message_icon.isNull(): - icon_label.setPixmap(self._message_icon.pixmap(self.icon_size)) - icon_label.setAlignment(Qt.AlignTop | Qt.AlignLeft) - icon_label.setFixedSize(self.icon_size) - icon_vbox.addWidget(icon_label, alignment=Qt.AlignTop) - icon_vbox.addStretch() - message_area.addLayout(icon_vbox) - - self.message_label = QLabel(message) - self.message_label.setObjectName('messageLabel') - self.message_label.setWordWrap(True) - self.message_label.setAlignment(Qt.AlignTop | Qt.AlignLeft) - self.message_label.setMinimumHeight(self.icon_size.height()) - self.message_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) - message_area.addWidget(self.message_label, 1) - message_area.addStretch() - - content_layout.addLayout(message_area) - base_layout.addWidget(content_widget) - base_layout.addSpacing(10) - - button_widget = QWidget() - button_layout = QHBoxLayout(button_widget) - button_layout.setContentsMargins(0, 0, 0, 0) - button_layout.setSpacing(10) - button_layout.addStretch() - - if self.show_cancel: - self.cancel_button = QPushButton(self.cancel_text) - self.cancel_button.setObjectName("secondaryButton") - self.cancel_button.clicked.connect(self.reject) - self.cancel_button.setFixedSize(90, 28) - button_layout.addWidget(self.cancel_button) - - self.confirm_button = QPushButton(self.confirm_text) - self.confirm_button.setObjectName("primaryButton") - self.confirm_button.clicked.connect(self.accept) - self.confirm_button.setFixedSize(90, 28) - button_layout.addWidget(self.confirm_button) - - self.confirm_button.setDefault(True) - self.confirm_button.setAutoDefault(True) - if self.show_cancel: - self.cancel_button.setAutoDefault(False) - - base_layout.addWidget(button_widget) - - main_layout.addWidget(base_frame) - - def _create_title_bar(self): - """创建自定义标题栏""" - self.title_bar = QFrame() - self.title_bar.setObjectName("titleBar") - - title_layout = QHBoxLayout(self.title_bar) - title_layout.setContentsMargins(0, 0, 0, 0) - title_layout.setSpacing(0) - - self.title_label = QLabel(self.windowTitle()) - self.title_label.setObjectName("titleLabel") - self.title_label.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) - title_layout.addWidget(self.title_label, 1) - - title_layout.addStretch() - - controls = QWidget() - controls.setObjectName("controlButtons") - controls_layout = QHBoxLayout(controls) - controls_layout.setContentsMargins(0, 0, 0, 0) - controls_layout.setSpacing(0) - - self.close_button = QPushButton() - self.close_button.setObjectName("closeButton") - self.close_button.clicked.connect(self.reject) - self.close_button.setFocusPolicy(Qt.NoFocus) - controls_layout.addWidget(self.close_button) - - self._apply_title_bar_icons() - - title_layout.addWidget(controls) - - - def _apply_title_bar_icons(self): - """应用标题栏图标""" - if self._icon_close: - self.close_button.setIcon(self._icon_close) - self.close_button.setIconSize(self._title_icon_size) - self.close_button.setText("") - self.close_button.setToolTip("关闭") - - def setWindowTitle(self, title): - """设置窗口标题""" - super().setWindowTitle(title) - if hasattr(self, "title_label"): - self.title_label.setText(title) - - def mousePressEvent(self, event): - """鼠标按下事件 - 用于拖拽窗口""" - if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): - self.dragging = True - self.drag_position = event.globalPos() - self.frameGeometry().topLeft() - event.accept() - super().mousePressEvent(event) - - def mouseMoveEvent(self, event): - """鼠标移动事件 - 用于拖拽窗口""" - if event.buttons() == Qt.LeftButton and self.dragging: - self.move(event.globalPos() - self.drag_position) - event.accept() - super().mouseMoveEvent(event) - - def mouseReleaseEvent(self, event): - """鼠标释放事件 - 停止拖拽""" - if event.button() == Qt.LeftButton: - self.dragging = False - super().mouseReleaseEvent(event) - - @staticmethod - def show_success(parent, title, message, show_cancel=False, confirm_text="确认"): - """显示成功消息对话框""" - dialog = UniversalMessageDialog( - parent, title, message, - UniversalMessageDialog.SUCCESS, - show_cancel, confirm_text - ) - return dialog.exec_() - - @staticmethod - def show_warning(parent, title, message, show_cancel=True, confirm_text="确认", cancel_text="取消"): - """显示警告消息对话框""" - dialog = UniversalMessageDialog( - parent, title, message, - UniversalMessageDialog.WARNING, - show_cancel, confirm_text, cancel_text - ) - return dialog.exec_() - - @staticmethod - def show_error(parent, title, message, show_cancel=False, confirm_text="确认"): - """显示错误消息对话框""" - dialog = UniversalMessageDialog( - parent, title, message, - UniversalMessageDialog.ERROR, - show_cancel, confirm_text - ) - return dialog.exec_() - - @staticmethod - def show_info(parent, title, message, show_cancel=True, confirm_text="确认", cancel_text="取消"): - """显示信息消息对话框""" - dialog = UniversalMessageDialog( - parent, title, message, - UniversalMessageDialog.INFO, - show_cancel, confirm_text, cancel_text - ) - return dialog.exec_() - - -class StyledTextInputDialog(QDialog): - """与新建项目样式一致的文本输入对话框""" - - def __init__(self, parent, title, label_text="", placeholder="", default_text=""): - super().__init__(parent) - self.setWindowTitle(title) - self.setObjectName("styledTextInputDialog") - self.setModal(True) - self.resize(420, 186) - self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) - self.setAttribute(Qt.WA_TranslucentBackground, True) - - self.dragging = False - self.drag_position = QPoint() - self.icon_manager = get_icon_manager() - self._title_icon_size = QSize(18, 18) - self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size) - - self.setStyleSheet(""" - QDialog#styledTextInputDialog { - background-color: transparent; - border: none; - } - QFrame#baseFrame { - background-color: #000000; - border: 1px solid #3E3E42; - border-radius: 5px; - } - QWidget#titleBar { - background-color: transparent; - border: none; - border-radius: 5px 5px 0px 0px; - min-height: 32px; - max-height: 32px; - } - QWidget#titleBar QWidget { - background-color: transparent; - border: none; - } - QLabel#titleLabel { - color: #FFFFFF; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 14px; - font-weight: 500; - letter-spacing: 0.7px; - } - QWidget#controlButtons QPushButton { - background-color: transparent; - border: none; - color: #EBEBEB; - font-size: 14px; - min-width: 18px; - max-width: 18px; - min-height: 18px; - max-height: 18px; - padding: 0px; - border-radius: 3px; - } - QWidget#controlButtons QPushButton:hover { - background-color: #2A2D2E; - color: #FFFFFF; - } - QPushButton#closeButton { - border-radius: 0px 5px 0px 0px; - } - QPushButton#closeButton:hover { - background-color: #2A2D2E; - color: #FFFFFF; - } - QWidget#contentWidget { - background-color: transparent; - border-radius: 0px 0px 5px 5px; - } - QFrame#contentContainer { - background-color: #19191B; - border: 1px solid #2C2F36; - border-radius: 5px; - } - QLabel[role="fieldLabel"] { - color: #EBEBEB; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 12px; - font-weight: 400; - letter-spacing: 0.6px; - } - QLineEdit { - background-color: rgba(89, 100, 113, 0.2); - color: #EBEBEB; - border: 1px solid rgba(76, 92, 110, 0.6); - border-radius: 2px; - padding: 0px 10px; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-size: 11px; - font-weight: 300; - letter-spacing: 0.55px; - min-height: 30px; - max-height: 30px; - } - QLineEdit:focus { - border: 1px solid #3067C0; - background-color: rgba(48, 103, 192, 0.1); - } - QLineEdit:hover { - border: 1px solid #3067C0; - background-color: rgba(89, 100, 113, 0.3); - } - QPushButton { - background-color: rgba(89, 98, 118, 0.5); - color: #EBEBEB; - border: none; - border-radius: 2px; - padding: 0px 12px; - font-family: 'Inter', 'Microsoft YaHei', sans-serif; - font-weight: 300; - font-size: 10px; - letter-spacing: 0.5px; - min-width: 120px; - min-height: 30px; - max-height: 30px; - } - QPushButton:hover { - background-color: #3067C0; - color: #FFFFFF; - } - QPushButton:pressed { - background-color: #2556A0; - color: #FFFFFF; - } - """) - - main_layout = QVBoxLayout(self) - main_layout.setContentsMargins(0, 0, 0, 0) - main_layout.setSpacing(0) - - base_frame = QFrame() - base_frame.setObjectName('baseFrame') - base_frame.setFrameShape(QFrame.NoFrame) - base_frame.setAttribute(Qt.WA_StyledBackground, True) - - base_layout = QVBoxLayout(base_frame) - base_layout.setContentsMargins(0, 0, 0, 0) - base_layout.setSpacing(0) - - self._createTitleBar() - base_layout.addWidget(self.title_bar) - base_layout.addSpacing(10) - - content_widget = QWidget() - content_widget.setObjectName('contentWidget') - content_layout = QVBoxLayout(content_widget) - content_layout.setContentsMargins(10, 0, 10, 10) - content_layout.setSpacing(0) - - content_container = QFrame() - content_container.setObjectName('contentContainer') - content_container.setFrameShape(QFrame.NoFrame) - content_container.setAttribute(Qt.WA_StyledBackground, True) - content_container.setFixedWidth(400) - - container_layout = QVBoxLayout(content_container) - container_layout.setContentsMargins(15, 10, 15, 10) - container_layout.setSpacing(10) - - if label_text: - label = QLabel(label_text) - label.setProperty('role', 'fieldLabel') - container_layout.addWidget(label) - - self.line_edit = QLineEdit() - self.line_edit.setPlaceholderText(placeholder) - self.line_edit.setText(default_text) - container_layout.addWidget(self.line_edit) - - separator = QFrame() - separator.setFrameShape(QFrame.HLine) - separator.setFrameShadow(QFrame.Plain) - separator.setFixedHeight(1) - separator.setStyleSheet("background-color: #2C2F36; border: none;") - container_layout.addWidget(separator) - - button_row = QHBoxLayout() - button_row.setContentsMargins(0, 0, 0, 0) - button_row.setSpacing(10) - button_row.addStretch() - - self.confirmButton = QPushButton("确认") - self.confirmButton.setObjectName("primaryButton") - self.confirmButton.clicked.connect(self._onAccept) - button_row.addWidget(self.confirmButton) - - self.cancelButton = QPushButton("取消") - self.cancelButton.setObjectName("secondaryButton") - self.cancelButton.clicked.connect(self.reject) - button_row.addWidget(self.cancelButton) - - container_layout.addLayout(button_row) - - content_layout.addWidget(content_container, 0, Qt.AlignTop) - base_layout.addWidget(content_widget) - main_layout.addWidget(base_frame) - - self.confirmButton.setDefault(True) - self.confirmButton.setAutoDefault(True) - self.line_edit.selectAll() - self.line_edit.setFocus() - - def _createTitleBar(self): - self.title_bar = QFrame() - self.title_bar.setObjectName("titleBar") - - title_layout = QHBoxLayout(self.title_bar) - title_layout.setContentsMargins(12, 6, 12, 6) - title_layout.setSpacing(0) - - controls = QWidget() - controls.setObjectName("controlButtons") - controls_layout = QHBoxLayout(controls) - controls_layout.setContentsMargins(0, 0, 0, 0) - controls_layout.setSpacing(0) - - self.close_button = QPushButton() - self.close_button.setObjectName("closeButton") - self.close_button.clicked.connect(self.reject) - self.close_button.setFocusPolicy(Qt.NoFocus) - self.close_button.setFixedSize(18, 18) - controls_layout.addWidget(self.close_button) - - self._applyTitleBarIcons() - - left_placeholder = QWidget() - left_placeholder.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) - title_layout.addWidget(left_placeholder) - - self.title_label = QLabel(self.windowTitle()) - self.title_label.setObjectName("titleLabel") - self.title_label.setAlignment(Qt.AlignCenter) - title_layout.addWidget(self.title_label, 1) - - title_layout.addWidget(controls) - left_placeholder.setFixedWidth(controls.sizeHint().width()) - - def _applyTitleBarIcons(self): - if self._icon_close: - self.close_button.setIcon(self._icon_close) - self.close_button.setIconSize(self._title_icon_size) - self.close_button.setText("") - self.close_button.setToolTip("关闭") - - def setWindowTitle(self, title): - super().setWindowTitle(title) - if hasattr(self, "title_label"): - self.title_label.setText(title) - - def _onAccept(self): - if self.line_edit.text().strip(): - self.accept() - - def text(self): - return self.line_edit.text().strip() - - def mousePressEvent(self, event): - if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): - self.dragging = True - self.drag_position = event.globalPos() - self.frameGeometry().topLeft() - event.accept() - super().mousePressEvent(event) - - def mouseMoveEvent(self, event): - if event.buttons() == Qt.LeftButton and self.dragging: - self.move(event.globalPos() - self.drag_position) - event.accept() - super().mouseMoveEvent(event) - - def mouseReleaseEvent(self, event): - if event.button() == Qt.LeftButton: - self.dragging = False - super().mouseReleaseEvent(event) - - -class CustomTreeWidget(QTreeWidget): - """自定义场景树部件""" - - def __init__(self, world, parent=None): - if parent is None: - parent = wrapinstance(0, QWidget) - super().__init__(parent) - self.world = world - # self.selectedItems = None - self.initData() - self.setupUI() # 初始化界面 - self.setupContextMenu() # 初始化右键菜单 - self.setupDragDrop() # 设置拖拽功能 - self.original_scales={} - - def initData(self): - """初始化变量""" - # 定义2D GUI元素类型 - self.gui_2d_types = { - "GUI_BUTTON", # GUI 按钮 - "GUI_LABEL", # GUI 标签 - "GUI_ENTRY", # GUI 输入框 - "GUI_IMAGE", # GUI 图片 - "GUI_2D_VIDEO_SCREEN", # GUI 2D视频 - "GUI_SPHERICAL_VIDEO", # GUI 3D球形视频 - "GUI_NODE" # 其他2D GUI容器 - } - - # 定义3D GUI元素类型 - self.gui_3d_types = { - "GUI_3DTEXT", # 3D 文本节点 - "GUI_3DIMAGE", # 3D 图片节点 - "GUI_VIRTUAL_SCREEN", # 3D视频 - "GUI_VirtualScreen" # 3D虚拟视频 - } - - # 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素) - self.scene_3d_types = { - "SCENE_ROOT", - "SCENE_NODE", - "LIGHT_NODE", # 灯节点 - "CAMERA_NODE", - "IMPORTED_MODEL_NODE", # 导入模型节点 - "MODEL_NODE", - "TERRAIN_NODE", # 地形节点 - "CESIUM_TILESET_NODE" # 3D Tileset - } - - # 这是一个最佳实践,它让代码的意图变得非常清晰。 - self.valid_3d_parent_types = self.scene_3d_types.union(self.gui_3d_types) - - def setupUI(self): - """初始化UI设置""" - self.setHeaderHidden(True) - # 启用多选和拖拽 - self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) - self.setDropIndicatorShown(True) # 启用拖放指示线 - - def setupDragDrop(self): - """设置拖拽功能""" - # 使用自定义拖拽模式 - self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) # 或者使用 DragDrop - self.setDefaultDropAction(Qt.DropAction.MoveAction) - self.setDragEnabled(True) - self.setAcceptDrops(True) - - def setupContextMenu(self): - """设置右键菜单""" - self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - self.customContextMenuRequested.connect(self.showContextMenu) - - def showContextMenu(self, position): - """显示右键菜单 - 复用主菜单的创建动作""" - if self.selectedItems(): - item = self.selectedItems()[0] - print(f"为项目 '{item.text(0)}' 显示右键菜单") - - # 创建右键菜单 - menu = QMenu(self) - - # 获取主窗口的创建菜单动作 - 关键修改 - if hasattr(self.world, 'main_window'): - main_window = self.world.main_window - create_actions = main_window.getCreateMenuActions() - - # 创建子菜单 - 复用主菜单结构 - createMenu = menu.addMenu('创建') - - # 基础对象 - createMenu.addAction(create_actions['createEmpty']) - - # 3D对象菜单 - create3dObjectMenu = createMenu.addMenu('3D对象') - - # 3D GUI菜单 - create3dGUIMenu = createMenu.addMenu('3D GUI') - create3dGUIMenu.addAction(create_actions['create3DText']) - create3dGUIMenu.addAction(create_actions['create3DImage']) - - # GUI菜单 - createGUIMenu = createMenu.addMenu('GUI') - createGUIMenu.addAction(create_actions['createButton']) - createGUIMenu.addAction(create_actions['createLabel']) - createGUIMenu.addAction(create_actions['createEntry']) - createGUIMenu.addAction(create_actions['createImage']) - createGUIMenu.addSeparator() - createGUIMenu.addAction(create_actions['createVideoScreen']) - createGUIMenu.addAction(create_actions['createSphericalVideo']) - createGUIMenu.addSeparator() - createGUIMenu.addAction(create_actions['createVirtualScreen']) - - # 光源菜单 - createLightMenu = createMenu.addMenu('光源') - createLightMenu.addAction(create_actions['createSpotLight']) - createLightMenu.addAction(create_actions['createPointLight']) - - else: - # 备用方案:如果无法获取主窗口动作,显示提示 - createMenu = menu.addMenu('创建') - noActionItem = createMenu.addAction('功能不可用') - noActionItem.setEnabled(False) - - # 添加删除选项 - menu.addSeparator() - deleteAction = menu.addAction('删除') - if hasattr(self, 'delete_items'): - deleteAction.triggered.connect(lambda: self.delete_items(self.selectedItems())) - - # 显示菜单 - global_pos = self.mapToGlobal(position) - action = menu.exec_(global_pos) - - print(f"右键菜单已显示在位置: {global_pos}") - if action: - print(f"用户选择了动作: {action.text()}") - else: - print("用户取消了菜单选择") - - # 在 CustomTreeWidget 类的 dropEvent 方法中替换缩放处理部分 - def dropEvent(self, event): - # 1. 获取所有被拖拽的项 - dragged_items = self.selectedItems() - if not dragged_items: - event.ignore() - return - - # 2. 在执行Qt的默认拖拽前,记录所有拖拽项的原始状态 - drag_info = [] - for item in dragged_items: - panda_node = item.data(0, Qt.UserRole) - if not panda_node or panda_node.is_empty(): - continue # 跳过无效节点 - - drag_info.append({ - "item": item, - "node": panda_node, - "old_parent_node": item.parent().data(0, Qt.UserRole) if item.parent() else None - }) - - # 3. 执行Qt的默认拖拽,让UI树先行更新 - # 这一步会自动处理移动或复制,并将项目从旧父节点移除,添加到新父节点 - super().dropEvent(event) - - # 4. 遍历记录下的信息,同步每一个Panda3D节点的状态 - try: - for info in drag_info: - dragged_item = info["item"] - dragged_node = info["node"] - old_parent_node = info["old_parent_node"] - - # 获取拖拽后的新父节点 - new_parent_item = dragged_item.parent() - new_parent_node = new_parent_item.data(0, Qt.UserRole) if new_parent_item else None - - # 仅当父节点实际发生变化时才执行重新父化 - if old_parent_node != new_parent_node: - print(f"跨层级拖拽:从 {old_parent_node} 移动到 {new_parent_node}") - - # # 保存世界坐标位置 - # world_pos = dragged_node.getPos(self.world.render) - # world_hpr = dragged_node.getHpr(self.world.render) - # world_scale = dragged_node.getScale(self.world.render) - - # 检查是否是2D GUI元素 - dragged_type = dragged_item.data(0, Qt.UserRole + 1) - is_2d_gui = dragged_type in self.gui_2d_types - - # 重新父化到新的父节点 - if new_parent_node: - if is_2d_gui: - # 2D GUI元素需要特殊处理 - if hasattr(new_parent_node, 'getTag') and new_parent_node.getTag("is_gui_element") == "1": - # 目标是GUI元素,直接重新父化 - dragged_node.wrtReparentTo(new_parent_node) - else: - # 目标是3D节点,保持GUI特性,重新父化到aspect2d - # from direct.showbase.ShowBase import aspect2d - dragged_node.wrtReparentTo(self.world.aspect2d) - print(f"2D GUI元素 {dragged_item.text(0)} 保持在aspect2d下") - else: - # 非GUI元素正常重新父化 - dragged_node.wrtReparentTo(new_parent_node) - else: - # 如果新父节点为None,根据元素类型决定父节点 - if is_2d_gui: - # from direct.showbase.ShowBase import aspect2d - dragged_node.wrtReparentTo(self.world.aspect2d) - print(f"2D GUI元素 {dragged_item.text(0)} 重新父化到aspect2d") - else: - dragged_node.wrtReparentTo(self.world.render) - - # # 恢复世界坐标位置(对于2D GUI可能需要调整) - # if is_2d_gui: - # # 2D GUI元素使用屏幕坐标系,可能需要特殊处理 - # dragged_node.setPos(world_pos) - # dragged_node.setHpr(world_hpr) - # dragged_node.setScale(world_scale) - # else: - # dragged_node.setPos(self.world.render, world_pos) - # dragged_node.setHpr(self.world.render, world_hpr) - # dragged_node.setScale(self.world.render, world_scale) - - print(f"✅ Panda3D父子关系已更新") - else: - print(f"同层级移动:父节点未变化,跳过Panda3D重新父化") - - except Exception as e: - print(f"⚠️ 同步Panda3D场景图失败: {e}") - # 不影响Qt树的更新,继续执行 - - # 事后验证:确保节点仍在"场景"根节点下 - self._ensureUnderSceneRoot(dragged_item) - self.world.property_panel._syncEffectiveVisibility(dragged_node) - event.accept() - - def _ensureUnderSceneRoot(self, item): - """确保节点在场景根节点下,如果不是则自动修正""" - if not item: - return - - # 检查是否成为了顶级节点 - if not item.parent(): - # 通过数据标识判断是否是场景根节点 - scene_root_marker = item.data(0, Qt.UserRole + 1) - if scene_root_marker != "SCENE_ROOT": - print(f"⚠️ 检测到节点 {item.text(0)} 意外成为顶级节点,正在修正...") - - # 找到场景根节点 - scene_root = None - for i in range(self.topLevelItemCount()): - top_item = self.topLevelItem(i) - if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": - scene_root = top_item - break - - if scene_root: - # 将节点移回场景根节点下 - self.takeTopLevelItem(self.indexOfTopLevelItem(item)) - scene_root.addChild(item) - print(f"✅ 已将节点 {item.text(0)} 移回场景根节点下") - - def isValidParentChild(self, dragged_item, target_item): - """检查是否是有效的父子关系(防止循环)+ GUI类型验证""" - - # 1. 禁止拖放到自身 - if dragged_item == target_item: - return False - - # 2. 禁止拖到根节点之外(根节点本身除外) - target_root = self._getRootNode(target_item) - if not target_root or target_root.data(0, Qt.UserRole + 1) != "SCENE_ROOT": - print(f"❌ 目标节点 {target_item.text(0)} 不在场景下") - return False - - # 3. 禁止拖拽场景根节点 - dragged_root = self._getRootNode(dragged_item) - if (dragged_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT" or - not dragged_root or dragged_root.data(0, Qt.UserRole + 1) != "SCENE_ROOT"): - print(f"❌ 禁止拖拽场景根节点或根节点外的节点") - return False - - # 4. Qt 树循环检查 - current = target_item - while current: - if current == dragged_item: - print(f"❌ Qt 树检测:{target_item.text(0)} 是 {dragged_item.text(0)} 的后代") - return False - current = current.parent() - - # 5. GUI元素类型验证 - 新增功能 - if not self._validateGUITypeCompatibility(dragged_item, target_item): - return False - - return True - - def _validateGUITypeCompatibility(self, dragged_item, target_item): - """验证GUI元素类型兼容性""" - try: - # 获取节点类型标识 - dragged_type = dragged_item.data(0, Qt.UserRole + 1) - target_type = target_item.data(0, Qt.UserRole + 1) - - # 定义2D GUI元素类型 - gui_2d_types = self.gui_2d_types - - # 定义3D GUI元素类型 - gui_3d_types = self.gui_3d_types - - # 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素) - scene_3d_types = self.scene_3d_types - - # 检查拖拽元素的类型 - is_dragged_2d_gui = dragged_type in gui_2d_types - is_dragged_3d_gui = dragged_type in gui_3d_types - is_dragged_3d_scene = dragged_type in scene_3d_types - - # 检查目标的类型 - is_target_2d_gui = target_type in gui_2d_types - is_target_3d_gui = target_type in gui_3d_types - is_target_3d_scene = target_type in scene_3d_types - - # === 严格的类型隔离验证逻辑 === - - # 1. 2D GUI元素的拖拽限制 - 只能拖拽到其他2D GUI元素下 - if is_dragged_2d_gui: - if target_type == "SCENE_ROOT": - return True - elif is_target_2d_gui: - print(f"✅ 2D GUI元素 {dragged_item.text(0)} 可以拖拽到2D GUI父节点 {target_item.text(0)}") - return True - elif is_target_3d_gui: - print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到3D GUI元素 {target_item.text(0)} 下") - print(" 💡 提示: 2D GUI元素只能作为其他2D GUI元素的子节点") - return False - elif is_target_3d_scene: - print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到3D场景节点 {target_item.text(0)} 下") - print(" 💡 提示: 2D GUI元素应该保持在2D GUI层级结构中") - return False - else: - print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") - return False - - # 2. 3D GUI元素的拖拽限制 - 只能拖拽到3D场景节点下 - elif is_dragged_3d_gui: - if is_target_3d_scene: - print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}") - return False - elif is_target_2d_gui: - print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下") - print(" 💡 提示: 3D GUI元素不能与2D GUI元素建立父子关系") - return False - elif is_target_3d_gui: - print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到其他3D GUI元素 {target_item.text(0)} 下") - print(" 💡 提示: 允许3D GUI元素之间建立父子关系") - return True - else: - print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") - return False - - # 3. 3D场景元素的拖拽限制 - 只能拖拽到3D场景节点或3D GUI元素下 - elif is_dragged_3d_scene: - if is_target_3d_scene: - print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}") - return True - elif is_target_2d_gui: - print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下") - print(" 💡 提示: 3D场景元素不能与2D GUI元素建立父子关系") - print(" 💡 建议: 将3D场景元素拖拽到其他3D场景节点或场景根节点下") - return False - elif is_target_3d_gui: - print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D GUI元素 {target_item.text(0)} 下") - print(" 💡 提示: 允许3D场景元素挂载在3D GUI元素下") - return False - else: - print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") - return False - - # 4. 其他未分类元素 - 严格禁止拖拽到2D GUI下 - else: - if is_target_2d_gui: - print(f"❌ 元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下") - print(" 💡 提示: 只有2D GUI元素才能作为其他2D GUI元素的子节点") - return False - elif is_target_3d_gui or is_target_3d_scene: - print(f"✅ 允许元素 {dragged_item.text(0)} 拖拽到3D节点 {target_item.text(0)}") - return True - else: - print(f"❌ 元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") - return False - - # 默认情况(理论上不应该到达这里) - print(f"⚠️ GUI类型验证遇到未处理的情况: {dragged_type} -> {target_type}") - return False - - except Exception as e: - print(f"❌ GUI类型兼容性验证失败: {str(e)}") - # 出错时采用保守策略,禁止拖拽 - return False - - def _getRootNode(self, item): - """获取树中节点的根节点项""" - if not item: - return None - - current = item - while current.parent(): - current = current.parent() - return current - - def dragEnterEvent(self, event): - """处理拖入事件""" - if event.source() == self: - event.accept() - else: - event.ignore() - - def dragMoveEvent(self, event): - """处理拖动事件""" - indicator_pos = self.dropIndicatorPosition() - indicator_str = "Unknown" - - if indicator_pos == QAbstractItemView.DropIndicatorPosition.OnItem: - indicator_str = "OnItem" - elif indicator_pos == QAbstractItemView.DropIndicatorPosition.AboveItem: - indicator_str = "AboveItem" - elif indicator_pos == QAbstractItemView.DropIndicatorPosition.BelowItem: - indicator_str = "BelowItem" - elif indicator_pos == QAbstractItemView.DropIndicatorPosition.OnViewport: - indicator_str = "OnViewport" - - #print(f'indicator pos: {indicator_str} (value: {int(indicator_pos)})') - - if event.source() != self: - event.ignore() - return - - # 获取当前拖拽的项目和目标位置 - target_item = self.itemAt(event.pos()) - selected_items = self.selectedItems() - - # 检查是否拖拽到多选区域内的项目 - if target_item and target_item in selected_items: - event.ignore() - return - - # 检查其他禁止条件 - if target_item and selected_items: - for dragged_item in selected_items: - if not self.isValidParentChild(dragged_item, target_item): - event.ignore() - return - - super().dragMoveEvent(event) - event.accept() - - def keyPressEvent(self, event): - """处理键盘按键事件""" - if event.key() == Qt.Key_Delete: - # currentItem = self.currentItem() - # if currentItem and currentItem.parent(): - # # 检查是否是模型节点或其子节点 - # if self.world.interface_manager.isModelOrChild(currentItem): - # nodePath = currentItem.data(0, Qt.UserRole) - # if nodePath: - # print("正在删除节点...") - # self.world.interface_manager.deleteNode(nodePath, currentItem) - # print("删除完成") - selected_items = self.selectedItems() - if selected_items: - # 执行删除操作 - # if selected_items.data(0, Qt.UserRole + 1) == "LIGHT_NODE": - # self._preprocess_light_items_for_deletion(selected_items) - self.delete_items(selected_items) - else: - # 没有选中任何项目,执行默认操作 - super().keyPressEvent(event) - else: - super().keyPressEvent(event) - - def _preprocess_light_items_for_deletion(self, selected_items): - """预处理灯光节点删除,特别处理最后一个灯光节点的问题""" - if not selected_items: - return selected_items - - # 检查选中的项目中是否包含灯光节点 - light_items = [] - for item in selected_items: - node_type = item.data(0, Qt.UserRole + 1) - if node_type == "LIGHT_NODE": - light_items.append(item) - - # 如果没有灯光节点,直接返回 - if not light_items: - return selected_items - - # 检查是否只有最后一个灯光节点被选中 - processed_items = list(selected_items) # 创建副本 - - for item in light_items: - panda_node = item.data(0, Qt.UserRole) - if not panda_node: - continue - - # 获取灯光类型 - if hasattr(panda_node, 'getTag'): - light_type = panda_node.getTag("light_type") - - # 检查是否是最后一个Spotlight - if (light_type == "spot_light" and hasattr(self.world, 'Spotlight') and - self.world.Spotlight and self.world.Spotlight[-1] == panda_node and - len(self.world.Spotlight) > 1): - - print(f"⚠️ 检测到选中最后一个Spotlight节点: {item.text(0)}") - # 这里可以添加特殊处理逻辑,比如提示用户或阻止删除 - - # 检查是否是最后一个Pointlight - elif (light_type == "point_light" and hasattr(self.world, 'Pointlight') and - self.world.Pointlight and self.world.Pointlight[-1] == panda_node and - len(self.world.Pointlight) > 1): - - print(f"⚠️ 检测到选中最后一个Pointlight节点: {item.text(0)}") - # 这里可以添加特殊处理逻辑,比如提示用户或阻止删除 - - return processed_items - - def delete_items(self, selected_items): - """删除选中的item - 简化版本""" - if not selected_items: - return - - # 1. 过滤掉不能删除的节点 - deletable_items = [] - for item in selected_items: - node_type = item.data(0, Qt.UserRole + 1) - panda_node = item.data(0, Qt.UserRole) - - # 跳过场景根节点和主相机 - if (node_type == "SCENE_ROOT" or - (panda_node and hasattr(self.world, 'cam') and panda_node == self.world.cam)): - continue - deletable_items.append(item) - - if not deletable_items: - StyledMessageBox.information(self, "提示", "没有可删除的节点") - return - - # 2. 确认删除 - item_count = len(deletable_items) - if item_count == 1: - message = f"确定要删除节点 \"{deletable_items[0].text(0)}\" 吗?" - else: - message = f"确定要删除 {item_count} 个节点吗?" - - dialog = UniversalMessageDialog( - self, - "确认删除", - message, - message_type=UniversalMessageDialog.WARNING, - show_cancel=True, - confirm_text="确认", - cancel_text="取消" - ) - - if dialog.exec_() != QDialog.Accepted: - return - - # 默认选中场景根节点,通常是第一个顶级节点 - #next_item_to_select = self.topLevelItem(0) - - # 3. 执行删除循环 - deleted_count = 0 - for item in deletable_items: - try: - # 在删除前,记录其父节点,作为删除后的新选择 - # 选择最后一个被删除项的父节点作为新的焦点 - if item.parent(): - next_item_to_select = item.parent() - panda_node = item.data(0, Qt.UserRole) - - if panda_node: - # 清理选择状态 - if (hasattr(self.world, 'selection') and - hasattr(self.world.selection, 'selectedNode') and - self.world.selection.selectedNode == panda_node): - self.world.selection.updateSelection(None) - - # 清理特殊资源(参考interface_manager.py的逻辑) - if hasattr(self.world, 'property_panel'): - self.world.property_panel.removeActorForModel(panda_node) - - # 清理灯光 - if hasattr(panda_node, 'getPythonTag'): - light_object = panda_node.getPythonTag('rp_light_object') - if light_object and hasattr(self.world, 'render_pipeline'): - self.world.render_pipeline.remove_light(light_object) - - # 从world列表中移除 - if hasattr(self.world, 'gui_elements') and panda_node in self.world.gui_elements: - self.world.gui_elements.remove(panda_node) - if hasattr(self.world, 'models') and panda_node in self.world.models: - self.world.models.remove(panda_node) - if hasattr(self.world, 'Spotlight') and panda_node in self.world.Spotlight: - self.world.Spotlight.remove(panda_node) - if hasattr(self.world, 'Pointlight') and panda_node in self.world.Pointlight: - self.world.Pointlight.remove(panda_node) - if hasattr(self.world, 'terrains') and panda_node in self.world.terrains: - self.world.terrains.remove(panda_node) - if hasattr(self.world, 'tilesets') and panda_node in self.world.tilesets: - # self.world.tilesets.remove(panda_node) - # 从 tilesets 列表中移除 - if hasattr(self.world, 'scene_manager'): - tilesets_to_remove = [] - for i, tileset_info in enumerate(self.world.scene_manager.tilesets): - if tileset_info['node'] == panda_node: - tilesets_to_remove.append(i) - - # 从后往前删除,避免索引问题 - for i in reversed(tilesets_to_remove): - del self.world.scene_manager.tilesets[i] - - # 从Panda3D场景中移除 - try: - if not panda_node.isEmpty(): - panda_node.removeNode() - except Exception as e: - print(f"❌ 删除节点 {item.text(0)} 失败: {str(e)}") - - # 从Qt树中移除 - parent_item = item.parent() - if parent_item: - parent_item.removeChild(item) - else: - index = self.indexOfTopLevelItem(item) - if index >= 0: - self.takeTopLevelItem(index) - - deleted_count += 1 - print(f"✅ 删除节点: {item.text(0)}") - - except Exception as e: - print(f"❌ 删除节点 {item.text(0)} 失败: {str(e)}") - import traceback - traceback.print_exc() - - # 最终清理 - # if hasattr(self.world, 'property_panel'): - # self.world.property_panel.clearPropertyPanel() - - # 4. 删除操作完成后,更新UI --- - if deleted_count > 0: - print(f"🎉 成功删除 {deleted_count} 个节点。正在更新UI...") - self.update_selection_and_properties(None, None) - - def delete_item(self, panda_node): - """删除指定节点 panda3D(node)- 优化和修复版本""" - if not panda_node or panda_node.is_empty(): - print("ℹ️ 尝试删除一个空的或无效的节点,操作取消。") - return - - # #如果有命令管理系统,则使用命令系统 - # if hasattr(self.world,'command_manager') and self.world.command_manager: - # from core.Command_System import DeleteNodeCommand - # parent_node = panda_node.getParent() - # command = DeleteNodeCommand(panda_node,parent_node) - # self.world.command_manager.execute_command(command) - # return - - # --- 关键修复:在操作前,安全地获取节点名字 --- - node_name_for_logging = panda_node.getName() - - # 1. 寻找对应的Qt Item - item = self.world.interface_manager.findTreeItem(panda_node, self._findSceneRoot()) - - # 场景清理(无论是否找到item,都应该执行) - self._cleanup_panda_node_resources(panda_node) - panda_node.removeNode() - - # 如果没有找到item,说明UI已经移除或不同步,清理完Panda3D资源后即可退出 - if not item: - print(f"✅ Panda3D节点 '{node_name_for_logging}' 已清理并移除。UI树中未找到对应项。") - return - try: - # 2. 过滤受保护节点 - node_type = item.data(0, Qt.UserRole + 1) - if node_type == "SCENE_ROOT": # 相机检查已包含在panda_node判空中 - print(f"ℹ️ 节点 {item.text(0)} 是受保护节点,无法删除。") - return - - # 3. 从UI树中移除 - parent_for_next_selection = item.parent() - if item.parent(): - item.parent().removeChild(item) - else: - index = self.indexOfTopLevelItem(item) - if index >= 0: - self.takeTopLevelItem(index) - - print(f"✅ 成功删除节点: {node_name_for_logging}") - - # 4. 更新UI - print(f"🔄 正在更新UI...") - if parent_for_next_selection and self.indexFromItem(parent_for_next_selection).isValid(): - new_selection_item = parent_for_next_selection - else: - new_selection_item = self.topLevelItem(0) - - if new_selection_item: - self.setCurrentItem(new_selection_item) - new_panda_node_to_select = new_selection_item.data(0, Qt.UserRole) - self.update_selection_and_properties(new_panda_node_to_select, new_selection_item) - else: - self.update_selection_and_properties(None, None) - - except Exception as e: - print(f"❌ 删除节点 {node_name_for_logging} 时发生意外错误: {str(e)}") - import traceback - traceback.print_exc() - - def clear_tree(self): - """清空UI树""" - print("Clear") - self.clear() - # 创建场景根节点 - sceneRoot = QTreeWidgetItem(self, ['场景']) - sceneRoot.setData(0, Qt.UserRole, self.world.render) - sceneRoot.setData(0, Qt.UserRole + 1, "SCENE_ROOT") - self._apply_font_to_item(sceneRoot) - # 添加相机节点 - cameraItem = QTreeWidgetItem(sceneRoot, ['相机']) - cameraItem.setData(0, Qt.UserRole, self.world.cam) - cameraItem.setData(0, Qt.UserRole + 1, "CAMERA_NODE") - self._apply_font_to_item(cameraItem) - # 添加地板节点 - if hasattr(self.world, 'ground') and self.world.ground: - groundItem = QTreeWidgetItem(sceneRoot, ['地板']) - groundItem.setData(0, Qt.UserRole, self.world.ground) - groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE") - self._apply_font_to_item(groundItem) - - def _apply_font_to_item(self, item): - """根据节点层级设置字体大小""" - if not item: - return - font = QFont(self.font()) - marker = item.data(0, Qt.UserRole + 1) - if item.parent() is None and (marker == "SCENE_ROOT" or item.text(0) == '场景'): - font = QFont("Microsoft YaHei", 12) - font.setWeight(QFont.Light) - else: - font = QFont("Microsoft YaHei", 10) - font.setWeight(QFont.Light) - item.setFont(0, font) - - def _apply_font_recursively(self, item): - """递归应用字体样式""" - if not item: - return - self._apply_font_to_item(item) - for index in range(item.childCount()): - self._apply_font_recursively(item.child(index)) - - def _handle_rows_inserted(self, parent_index, first, last): - """在插入新节点时自动调整字体""" - model = self.model() - if not model: - return - for row in range(first, last + 1): - index = model.index(row, 0, parent_index) - item = self.itemFromIndex(index) - if item: - self._apply_font_recursively(item) - - def _cleanup_panda_node_resources(self, panda_node): - """一个集中的辅助函数,用于清理与Panda3D节点相关的所有资源。""" - if not panda_node or panda_node.is_empty(): - return - try: - # 清理选择状态 - if hasattr(self.world, 'selection') and self.world.selection.selectedNode == panda_node: - self.world.selection.updateSelection(None) - # 清理属性面板 - if hasattr(self.world, 'property_panel'): - self.world.property_panel.removeActorForModel(panda_node) - # 清理灯光 - if hasattr(panda_node, 'getPythonTag'): - light_object = panda_node.getPythonTag('rp_light_object') - if light_object and hasattr(self.world, 'render_pipeline'): - self.world.render_pipeline.remove_light(light_object) - # 从各种world管理列表中移除 - lists_to_check = ['gui_elements', 'models', 'Spotlight', 'Pointlight', 'terrains'] - for list_name in lists_to_check: - if hasattr(self.world, list_name): - world_list = getattr(self.world, list_name) - if panda_node in world_list: - world_list.remove(panda_node) - # 特殊处理tilesets - if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'tilesets'): - tilesets_to_remove = [i for i, info in enumerate(self.world.scene_manager.tilesets) if - info.get('node') == panda_node] - for i in reversed(tilesets_to_remove): - del self.world.scene_manager.tilesets[i] - print(f"🧹 已清理节点 {panda_node.getName()} 的所有关联资源。") - except Exception as e: - # 即便这里出错,也要打印信息,但不要让整个删除流程中断 - print(f"⚠️ 清理节点 {panda_node.getName()} 资源时出错: {e}") - - # def mousePressEvent(self, event): - # """鼠标按下事件""" - # if event.button() == Qt.LeftButton: - # if self.currentItem(): - # print(f"self.currentItem() = {self.currentItem()}") - # else: - # print(f"self.currentItem() = None") - # - # # 调用父类处理其他事件 - # super().mousePressEvent(event) - - def update_item_name(self, text, item): - """ 树节点名字 """ - if not item: - return - try: - # 正确的代码 - node = item.data(0, Qt.UserRole) - - item.setText(0, text) - node.setName(text) - except Exception as e: - print(e) - - def _findSceneRoot(self): - """查找场景根节点""" - for i in range(self.topLevelItemCount()): - top_item = self.topLevelItem(i) - if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": - return top_item - return None - - def create_model_items(self, model: NodePath): - """ - 【此函数保持不变】 - 创建模型项。 - 只寻找模型下一层带有 'is_scene_element' 标签的子节点作为分支的根, - 然后完整地展示这些分支。 - """ - if not model: - print("传入的参数model为空") - return - - # 找到场景树的根节点,我们将把模型节点添加到这里 - root_item = self._findSceneRoot() - if not root_item: - print("错误:未能找到场景根节点项") - return - - # 1. 在模型的第一层子节点中进行筛选 - for child_node in model.getChildren(): - if child_node.hasTag("is_scene_element"): - print(f"找到带标签的根节点:{child_node.getName()}") - if (child_node.hasTag("gui_type")and - child_node.getTag("gui_type") in ["3d_text","3d_image","video_screen"]): - print(f"跳过3dGUI节点{child_node.getName()}") - continue - - # 为这个带标签的节点创建一个树项 - child_item = QTreeWidgetItem(root_item) - child_item.setText(0, child_node.getName() or "Unnamed Tagged Node") - child_item.setData(0, Qt.UserRole, child_node) - child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type")) - # self._add_node_info(child_item, child_node) # 可选信息 - - # 2. 对这个节点的所有后代进行“无条件”递归添加 (但会跳过碰撞体) - self._add_all_children_unconditionally(child_item, child_node) - - def _add_all_children_unconditionally(self, parent_item: QTreeWidgetItem, node_path: NodePath): - """ - 【此函数已更新】 - 无条件地、递归地添加一个节点下的所有子节点,但会跳过碰撞节点。 - """ - for child_node in node_path.getChildren(): - - # 新增:检查节点是否为碰撞节点 - if isinstance(child_node.node(), CollisionNode): - # print(f"跳过碰撞节点: {child_node.getName()}") # 用于调试 - continue # 如果是,则跳过此节点及其所有子节点 - - # 创建子项 - child_item = QTreeWidgetItem(parent_item) - child_item.setText(0, child_node.getName() or "Unnamed Child") - child_item.setData(0, Qt.UserRole, child_node) - child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type")) - # self._add_node_info(child_item, child_node) # 可选信息 - - # 继续无条件地递归 - if not child_node.is_empty(): - self._add_all_children_unconditionally(child_item, child_node) - - # ==================== 辅助方法 ==================== - def _findSceneRoot(self): - """查找场景根节点""" - for i in range(self.topLevelItemCount()): - top_item = self.topLevelItem(i) - if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": - return top_item - return - - def _generateUniqueNodeName(self, base_name, parent_node): - """生成唯一的节点名称""" - # 获取父节点下所有子节点的名称 - existing_names = set() - for child in parent_node.getChildren(): - existing_names.add(child.getName()) - - # 如果基础名称不存在,直接使用 - if base_name not in existing_names: - return base_name - - # 否则添加数字后缀 - counter = 1 - while f"{base_name}_{counter}" in existing_names: - counter += 1 - - return f"{base_name}_{counter}" - - def add_node_to_tree_widget(self, node, parent_item, node_type): - """将node元素添加到树形控件""" - if hasattr(node, 'getTag'): - if node.hasTag('tree_item_type'): - print(f"node0: {node.getName()},{node.getTag('tree_item_type')}") - tree_type = node.getTag('tree_item_type') - else: - node.setTag('tree_item_type', node_type) - else: - print(f"node2: {node.getName()},{node_type}") - tree_type = node_type - - - # BLACK_LIST 和依赖项导入保持不变 - BLACK_LIST = {'', '**', 'temp', 'collision'} - from panda3d.core import CollisionNode, ModelRoot - from PyQt5.QtWidgets import QTreeWidgetItem - from PyQt5.QtCore import Qt - - # 1. 修改内部函数,让它返回创建的节点 - def addNodeToTree(node, parentItem, force=False): - """内部递归函数,现在会返回创建的顶级节点项""" - if not force and should_skip(node): - return None # 如果跳过,返回None - - nodeItem = QTreeWidgetItem(parentItem, [node.getName()]) - nodeItem.setData(0, Qt.UserRole, node) - nodeItem.setData(0, Qt.UserRole + 1, tree_type) - - for child in node.getChildren(): - # 递归调用,但我们只关心顶级的nodeItem - addNodeToTree(child, nodeItem, force=False) - - return nodeItem # <-- 新增:返回创建的QTreeWidgetItem - - def should_skip(node): - name = node.getName() - return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(), CollisionNode) or isinstance( - node.node(), ModelRoot) or name == "" - - # 使用一个变量来确保无论哪个分支都有返回值 - new_qt_item = None - node_name = "" - - try: - if tree_type == "IMPORTED_MODEL_NODE": - # getTag('file') 可能是你自己设置的tag,这里假设它存在 - node_name = node.getTag("file") if hasattr(node, 'getTag') and node.hasTag("file") else node.getName() - - # 2. 接收 addNodeToTree 的返回值 - new_qt_item = addNodeToTree(node, parent_item, force=True) - - else: - node_name = node.getName() if hasattr(node, 'getName') else "node" - new_qt_item = QTreeWidgetItem(parent_item, [node_name]) - new_qt_item.setData(0, Qt.UserRole, node) - new_qt_item.setData(0, Qt.UserRole + 1, tree_type) - - # 确保 new_qt_item 成功创建后再继续操作 - if new_qt_item: - # 展开父节点 - if hasattr(parent_item, 'setExpanded'): - parent_item.setExpanded(True) - - print(f"✅ Qt树节点添加成功: {node_name}") - return new_qt_item - else: - # 如果 addNodeToTree 因为 should_skip 返回了 None - print(f"ℹ️ 节点 {node_name} 被跳过,未添加到树中。") - return None - - except Exception as e: - import traceback - print(f"❌ 添加node到树形控件失败: {str(e)}") - traceback.print_exc() # 打印更详细的错误堆栈,方便调试 - return None - - def update_selection_and_properties(self, node, qt_item): - """更新选择状态和属性面板""" - try: - # 更新选择状态 - if hasattr(self.world, 'selection'): - self.world.selection.updateSelection(node) - - # 更新属性面板 - if hasattr(self.world, 'property_panel'): - self.world.property_panel.updatePropertyPanel(qt_item) - elif hasattr(self.world, 'updatePropertyPanel'): - self.world.updatePropertyPanel(qt_item) - - print(f"✅ 更新选择和属性面板: {qt_item.text(0)}") - - except Exception as e: - print(f"❌ 更新选择和属性面板失败: {str(e)}") - - # ==================== 3D辅助方法 ==================== - def get_target_parents_for_creation(self): - """获取创建目标的父节点列表""" - from PyQt5.QtCore import Qt - - target_parents = [] - - try: - selected_items = self.selectedItems() - - # 检查选中的项目,找出所有有效的父节点 - for item in selected_items: - if self._isValidParentForNewNode(item): - parent_node = item.data(0, Qt.UserRole) - if parent_node: - target_parents.append((item, parent_node)) - print(f"📍 找到有效父节点: {item.text(0)}") - - # 如果没有有效的选中节点,使用场景根节点 - if not target_parents: - print("⚠️ 没有有效选中节点,使用场景根节点") - scene_root = self._findSceneRoot() - if scene_root: - parent_node = scene_root.data(0, Qt.UserRole) - if parent_node: - target_parents.append((scene_root, parent_node)) - else: - # 如果没有_findSceneRoot方法,使用world.render作为默认父节点 - if hasattr(self.world, 'render'): - # 创建一个虚拟的树项目来表示render节点 - class MockTreeItem: - def text(self, column): - return "render" - - mock_item = MockTreeItem() - target_parents.append((mock_item, self.world.render)) - - print(f"📊 总共找到 {len(target_parents)} 个目标父节点") - return target_parents - - except Exception as e: - print(f"❌ 获取目标父节点失败: {str(e)}") - return [] - - def _isValidParentForNewNode(self, item): - """检查节点是否适合作为新节点的父节点-3d""" - if not item: - return False - - # 获取节点类型标识 - node_type = item.data(0, Qt.UserRole + 1) - - # 场景根节点和普通场景节点可以作为父节点 - if node_type in self.valid_3d_parent_types: - return True - - # # 模型节点也可以作为父节点 - # panda_node = item.data(0, Qt.UserRole) - # if panda_node and panda_node.hasTag("is_model_root"): - # return True - # - # # 其他类型的节点也可以,但排除一些特殊情况 - # if panda_node: - # # # 排除相机节点 - # # if panda_node == self.world.cam: - # # return False - # # 排除碰撞节点 - # from panda3d.core import CollisionNode - # if isinstance(panda_node.node(), CollisionNode): - # return False - # return True - - return False - - - def get_target_parents_for_gui_creation(self): - """获取GUI创建目标的父节点列表 - 支持GUI元素作为父节点""" - from PyQt5.QtCore import Qt - - target_parents = [] - - try: - selected_items = self.selectedItems() - - # 检查选中的项目,找出所有有效的父节点 - for item in selected_items: - if self.isValidParentForGUI(item): - parent_node = item.data(0, Qt.UserRole) - if parent_node: - target_parents.append((item, parent_node)) # 修复:确保添加到列表 - print(f"📍 找到有效GUI父节点: {item.text(0)}") - else: - print(f"⚠️ GUI父节点 {item.text(0)} 的Panda3D数据为空") - - # 如果没有有效的选中节点,使用场景根节点 - if not target_parents: - print("⚠️ 没有有效选中节点,使用场景根节点") - scene_root = self._findSceneRoot() - if scene_root: - parent_node = scene_root.data(0, Qt.UserRole) - if parent_node: - target_parents.append((scene_root, parent_node)) - else: - if hasattr(self.world, 'render'): - class MockTreeItem: - def text(self, column): - return "render" - - mock_item = MockTreeItem() - target_parents.append((mock_item, self.world.render)) - - print(f"📊 总共找到 {len(target_parents)} 个目标父节点") - return target_parents - - except Exception as e: - print(f"❌ 获取目标父节点失败: {str(e)}") - return [] - - def isValidParentForGUI(self, item): - """检查节点是否适合作为GUI元素的父节点""" - if not item: - return False - - # 获取节点类型标识 - node_type = item.data(0, Qt.UserRole + 1) - - # GUI元素可以作为其他GUI元素的父节点 - if node_type in self.gui_2d_types: - return True - - # 场景根节点和普通场景节点也可以作为父节点 - if node_type in ["SCENE_ROOT"]: - return True - - return False - - def is_gui_element(self, node): - """判断节点是否是GUI元素""" - if not node: - return False - - # 检查是否有GUI标签 - if hasattr(node, 'getTag'): - return node.getTag("is_gui_element") == "1" or node.getTag("gui_type") in ["info_panel", "button", "label", "entry", "3d_text", "virtual_screen"] - - # 检查是否是DirectGUI对象 - try: - from direct.gui.DirectGuiBase import DirectGuiBase - return isinstance(node, DirectGuiBase) - except ImportError: - return False - - def calculate_relative_gui_position(self, pos, parent_gui): - """计算相对于GUI父节点的位置""" - try: - # 对于GUI子元素,使用相对坐标 - # 这里使用较小的缩放因子,因为是相对于父GUI的位置 - relative_pos = (pos[0] * 0.05, 0, pos[2] * 0.05) - print(f"📐 计算GUI相对位置: {pos} -> {relative_pos}") - return relative_pos - except Exception as e: - print(f"❌ 计算GUI相对位置失败: {str(e)}") - # 如果计算失败,返回默认的屏幕坐标 - return (pos[0] * 0.1, 0, pos[2] * 0.1) - - #---------------------------暂时无用------------------------------- - - def add_existing_node(self, panda_node, node_type="SCENE_NODE", parent_item=None): - """将已存在的Panda3D节点添加到Qt树形控件中 - - Args: - panda_node: 已创建的Panda3D节点 - node_type: 节点类型标识 - parent_item: 父Qt项目,如果为None则使用当前选中项或根节点 - - Returns: - QTreeWidgetItem: 创建的Qt树项目 - """ - try: - if not panda_node: - print("❌ 传入的Panda3D节点为空") - return None - - # 确定父项目 - 保持简单,只处理单个父节点 - if parent_item is None: - # 优先使用当前选中的第一个有效节点作为父节点 - selected_items = self.selectedItems() - if selected_items: - # 找到第一个有效的父节点 - for potential_parent in selected_items: - if self._isValidParentForNewNode(potential_parent): - parent_item = potential_parent - print(f"📍 使用选中节点作为父节点: {parent_item.text(0)}") - break - - # 如果没有找到有效的选中节点 - if not parent_item: - print("⚠️ 所有选中节点都不适合作为父节点,查找场景根节点") - parent_item = self._findSceneRoot() - else: - # 没有选中任何节点,使用场景根节点 - print("📍 没有选中节点,使用场景根节点作为父节点") - parent_item = self._findSceneRoot() - - # 如果场景根节点也找不到,最后尝试render节点 - if not parent_item: - print("📍 场景根节点未找到,尝试使用render节点") - parent_item = self._findRenderItem() - - if not parent_item: - print("❌ 无法找到合适的父节点") - return None - - # 创建Qt树项目 - node_name = panda_node.getName() - new_qt_item = QTreeWidgetItem(parent_item, [node_name]) - new_qt_item.setData(0, Qt.UserRole, panda_node) - new_qt_item.setData(0, Qt.UserRole + 1, node_type) - - # 展开父节点 - parent_item.setExpanded(True) - - print(f"✅ 成功将现有节点添加到树形控件: {node_name} -> 父节点: {parent_item.text(0)}") - return new_qt_item - - except Exception as e: - print(f"❌ 添加现有节点到树形控件失败: {str(e)}") - import traceback - traceback.print_exc() - return None - - def _findRenderItem(self): - """查找render根节点项目""" - try: - root = self.invisibleRootItem() - for i in range(root.childCount()): - item = root.child(i) - if item.text(0) == "render": - return item - - # 如果没找到render节点,返回第一个子项目 - if root.childCount() > 0: - return root.child(0) - - return None - except Exception as e: - print(f"查找render节点失败: {e}") - return None - - def create_item(self, node_type="empty", selected_items=None): - """创建不同类型的场景节点 - - Args: - node_type: 节点类型 ("empty", "spot_light", "point_light") - selected_items: 选中的父节点项目列表,如果为None则使用当前选中项或根节点 - """ - try: - # 确定父节点 - parent_items = self._determineParentItems(selected_items) - if not parent_items: - print("⚠️ 无法确定父节点") - return [] - - created_nodes = [] - - for parent_item in parent_items: - # 验证父节点的有效性 - if not self._isValidParentForNewNode(parent_item): - print(f"⚠️ 节点 {parent_item.text(0)} 不适合作为父节点") - continue - - # 获取父节点的Panda3D对象 - parent_node = parent_item.data(0, Qt.UserRole) - if not parent_node: - print(f"⚠️ 父节点 {parent_item.text(0)} 没有对应的Panda3D对象") - continue - - # 根据节点类型创建不同的节点 - if node_type == "empty": - new_node = self._createEmptyNode(parent_node, parent_item) - elif node_type == "spot_light": - new_node = self._createSpotLightNode(parent_node, parent_item) - elif node_type == "point_light": - new_node = self._createPointLightNode(parent_node, parent_item) - else: - print(f"❌ 不支持的节点类型: {node_type}") - continue - - if new_node: - created_nodes.append(new_node) - - # 如果只创建了一个节点,自动选中它 - if len(created_nodes) == 1: - _, qt_item = created_nodes[0] - self.setCurrentItem(qt_item) - # 更新选择和属性面板 - if hasattr(self.world, 'selection'): - self.world.selection.updateSelection(qt_item.data(0, Qt.UserRole)) - if hasattr(self.world, 'property_panel'): - self.world.property_panel.updatePropertyPanel(qt_item) - - print(f"✅ 总共创建了 {len(created_nodes)} 个 {node_type} 节点") - return created_nodes - - except Exception as e: - print(f"❌ 创建 {node_type} 节点失败: {str(e)}") - import traceback - traceback.print_exc() - return [] - - def _determineParentItems(self, selected_items): - """确定父节点项目列表""" - if selected_items is not None: - return selected_items - - # 使用当前选中的项目 - current_selected = self.selectedItems() - if current_selected: - return current_selected - - # 如果没有选中任何项目,使用场景根节点 - scene_root = self._findSceneRoot() - if scene_root: - return [scene_root] - - return [] - - def _setupNewNodeDefaults(self, node): - """设置新节点的默认属性""" - # 设置默认位置(相对于父节点) - node.setPos(0, 0, 0) - node.setHpr(0, 0, 0) - node.setScale(1, 1, 1) - - # 设置默认可见性 - node.show() - - # 可以根据需要添加更多默认设置 - # 例如:默认材质、碰撞检测等 - - # ==================== 创建节点 ==================== - def _createEmptyNode(self, parent_node, parent_item): - """创建空节点""" - # 生成唯一的节点名称 - node_name = self._generateUniqueNodeName("空节点", parent_node) - - # 在Panda3D场景中创建新节点 - new_panda_node = parent_node.attachNewNode(node_name) - - # 设置新节点的默认属性 - self._setupNewNodeDefaults(new_panda_node) - - # 设置节点标签 - new_panda_node.setTag("is_scene_element", "1") - new_panda_node.setTag("node_type", "empty_node") - new_panda_node.setTag("created_by_user", "1") - - # 在Qt树中创建对应的项目 - new_qt_item = QTreeWidgetItem(parent_item, [node_name]) - new_qt_item.setData(0, Qt.UserRole, new_panda_node) - new_qt_item.setData(0, Qt.UserRole + 1, "SCENE_NODE") - - # 展开父节点 - parent_item.setExpanded(True) - - print(f"✅ 成功创建空节点: {node_name}") - return (new_panda_node, new_qt_item) - - def _createSpotLightNode(self, parent_node, parent_item): - """创建聚光灯节点""" - from RenderPipelineFile.rpcore import SpotLight - from QMeta3D.Meta3DWorld import get_render_pipeline - from panda3d.core import Vec3, NodePath - - try: - render_pipeline = get_render_pipeline() - - # 生成唯一的节点名称 - light_name = self._generateUniqueNodeName(f"Spotlight_{len(self.world.Spotlight)}", parent_node) - - # 创建挂载节点 - light_np = NodePath(light_name) - light_np.reparentTo(parent_node) - - # 创建聚光灯对象 - light = SpotLight() - light.direction = Vec3(0, 0, -1) - light.fov = 70 - light.set_color_from_temperature(5 * 1000.0) - light.energy = 5000 - light.radius = 1000 - light.casts_shadows = True - light.shadow_map_resolution = 256 - light.setPos(0, 0, 0) # 相对于父节点的位置 - - # 添加到渲染管线 - render_pipeline.add_light(light) - - # 设置节点属性和标签 - light_np.setTag("light_type", "spot_light") - light_np.setTag("is_scene_element", "1") - light_np.setTag("light_energy", str(light.energy)) - light_np.setTag("created_by_user", "1") - - # 保存光源对象引用 - light_np.setPythonTag("rp_light_object", light) - - # 添加到管理列表 - self.world.Spotlight.append(light_np) - - # 在Qt树中创建对应的项目 - new_qt_item = QTreeWidgetItem(parent_item, [light_name]) - new_qt_item.setData(0, Qt.UserRole, light_np) - new_qt_item.setData(0, Qt.UserRole + 1, "LIGHT_NODE") - - # 展开父节点 - parent_item.setExpanded(True) - - print(f"✅ 成功创建聚光灯: {light_name}") - - except Exception as e: - print(f"❌ 创建聚光灯失败: {str(e)}") - return None - - def _createPointLightNode(self, parent_node, parent_item): - """创建点光源节点""" - from RenderPipelineFile.rpcore import PointLight - from QMeta3D.Meta3DWorld import get_render_pipeline - from panda3d.core import Vec3, NodePath - - try: - render_pipeline = get_render_pipeline() - - # 生成唯一的节点名称 - light_name = self._generateUniqueNodeName(f"Pointlight_{len(self.world.Pointlight)}", parent_node) - - # 创建挂载节点 - light_np = NodePath(light_name) - light_np.reparentTo(parent_node) - - # 创建点光源对象 - light = PointLight() - light.setPos(0, 0, 0) # 相对于父节点的位置 - light.energy = 5000 - light.radius = 1000 - light.inner_radius = 0.4 - light.set_color_from_temperature(5 * 1000.0) - light.casts_shadows = True - light.shadow_map_resolution = 256 - - # 添加到渲染管线 - render_pipeline.add_light(light) - - # 设置节点属性和标签 - light_np.setTag("light_type", "point_light") - light_np.setTag("is_scene_element", "1") - light_np.setTag("light_energy", str(light.energy)) - light_np.setTag("created_by_user", "1") - - # 保存光源对象引用 - light_np.setPythonTag("rp_light_object", light) - - # 添加到管理列表 - self.world.Pointlight.append(light_np) - - # 在Qt树中创建对应的项目 - new_qt_item = QTreeWidgetItem(parent_item, [light_name]) - new_qt_item.setData(0, Qt.UserRole, light_np) - new_qt_item.setData(0, Qt.UserRole + 1, "LIGHT_NODE") - - # 展开父节点 - parent_item.setExpanded(True) - - print(f"✅ 成功创建点光源: {light_name}") - return (light_np, new_qt_item) - - except Exception as e: - print(f"❌ 创建点光源失败: {str(e)}") - return None - - - +""" +自定义Qt部件模块 + +包含所有自定义的Qt界面组件: +- NewProjectDialog: 新建项目对话框 +- CustomPanda3DWidget: 自定义Panda3D显示部件 +- CustomFileView: 自定义文件浏览器 +- CustomTreeWidget: 自定义场景树部件 +""" + +import os +import re +from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, + QLineEdit, QPushButton, QLabel, + QTreeView, QTreeWidget, QTreeWidgetItem, QWidget, + QFileDialog, QMessageBox, QAbstractItemView, QMenu, QDockWidget, QButtonGroup, QToolButton, QFrame, QSizePolicy) +from PyQt5.QtCore import Qt, QUrl, QMimeData, QPoint, QSize +from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush, QFont +from PyQt5.sip import wrapinstance +from direct.showbase.ShowBaseGlobal import aspect2d +from panda3d.core import ModelRoot, NodePath, CollisionNode + +from QMeta3D.QMeta3DWidget import QMeta3DWidget +from scene import util +from ui.icon_manager import get_icon_manager + +class NewProjectDialog(QDialog): + """新建项目对话框""" + + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("新建项目") + self.setObjectName("newProjectDialog") + self.resize(508, 274) + self.setMinimumSize(508, 274) + self.setModal(True) + + # 移除默认窗口装饰,使用自定义顶部栏 + self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) + self.setAttribute(Qt.WA_TranslucentBackground, True) + + # 拖拽和窗口状态 + self.dragging = False + self.drag_position = QPoint() + self.icon_manager = get_icon_manager() + self._title_icon_size = QSize(18, 18) + self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size) + + # 设置严格按照Figma设计的样式 + self.setStyleSheet(""" + QDialog#newProjectDialog { + background-color: transparent; + color: #EBEBEB; + border: none; + } + QFrame#baseFrame { + background-color: #000000; + border: 1px solid #3E3E42; + border-radius: 5px; + } + QWidget#titleBar { + background-color: transparent; + border: none; + border-radius: 5px 5px 0px 0px; + min-height: 32px; + max-height: 32px; + } + QWidget#titleBar QWidget { + background-color: transparent; + border: none; + } + QLabel#titleLabel { + color: #FFFFFF; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 14px; + font-weight: 500; + letter-spacing: 0.7px; + } + QWidget#controlButtons QPushButton { + background-color: transparent; + border: none; + color: #EBEBEB; + font-size: 14px; + min-width: 18px; + max-width: 18px; + min-height: 18px; + max-height: 18px; + padding: 0px; + border-radius: 3px; + } + QWidget#controlButtons QPushButton:hover { + background-color: #2A2D2E; + color: #FFFFFF; + } + QPushButton#closeButton { + border-radius: 0px 5px 0px 0px; + } + QPushButton#closeButton:hover { + background-color: #2A2D2E; + color: #FFFFFF; + } + QWidget#contentWidget { + background-color: transparent; + border-radius: 0px 0px 5px 5px; + } + QFrame#contentContainer { + background-color: #19191B; + border: 1px solid #2C2F36; + border-radius: 5px; + } + QLabel[role="sectionTitle"] { + color: #EBEBEB; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 12px; + font-weight: 500; + letter-spacing: 0.6px; + padding: 0px; + margin-bottom: 0px; + } + QLabel[role="hint"] { + color: rgba(235, 235, 235, 0.6); + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 11px; + font-weight: 300; + letter-spacing: 0.55px; + margin-top: 0px; + padding: 0px; + } + QLineEdit { + background-color: rgba(89, 100, 113, 0.2); + color: #EBEBEB; + border: 1px solid rgba(76, 92, 110, 0.6); + border-radius: 2px; + padding: 6px 10px; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 11px; + font-weight: 300; + letter-spacing: 0.55px; + min-height: 14px; + max-height: 30px; + } + QLineEdit:focus { + border: 1px solid #3067C0; + background-color: rgba(48, 103, 192, 0.1); + } + QLineEdit:hover { + border: 1px solid #3067C0; + background-color: rgba(89, 100, 113, 0.3); + } + QLineEdit:disabled { + background-color: rgba(89, 100, 113, 0.1); + color: rgba(235, 235, 235, 0.4); + border: 1px solid rgba(76, 92, 110, 0.2); + } + QPushButton { + background-color: rgba(89, 98, 118, 0.5); + color: #EBEBEB; + border: none; + border-radius: 2px; + padding: 0px 0px; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-weight: 300; + font-size: 10px; + letter-spacing: 0.5px; + min-width: 90px; + min-height: 30px; + max-height: 30px; + } + QPushButton:hover { + background-color: #3067C0; + color: #FFFFFF; + } + QPushButton:pressed { + background-color: #2556A0; + color: #FFFFFF; + } + QPushButton:disabled { + background-color: rgba(89, 98, 118, 0.3); + color: rgba(235, 235, 235, 0.4); + } + QPushButton#primaryButton { + background-color: rgba(89, 98, 118, 0.5); + border: none; + color: #EBEBEB; + font-weight: 300; + min-width: 120px; + max-width: 120px; + } + QPushButton#primaryButton:hover { + background-color: #2556A0; + } + QPushButton#primaryButton:pressed { + background-color: #1E4A8C; + } + QPushButton#secondaryButton { + background-color: rgba(89, 98, 118, 0.5); + border: none; + color: #EBEBEB; + min-width: 120px; + max-width: 120px; + } + QPushButton#secondaryButton:hover { + background-color: #3067C0; + color: #FFFFFF; + } + QPushButton#secondaryButton:pressed { + background-color: #2556A0; + color: #FFFFFF; + } + QPushButton#browseButton { + min-width: 90px; + max-width: 90px; + min-height: 28px; + max-height: 28px; + } + """) + + # 创建主布局 + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + + # 创建基础容器,负责绘制圆角和边框 + base_frame = QFrame() + base_frame.setObjectName('baseFrame') + base_frame.setFrameShape(QFrame.NoFrame) + base_frame.setAttribute(Qt.WA_StyledBackground, True) + + base_layout = QVBoxLayout(base_frame) + base_layout.setContentsMargins(0, 0, 0, 0) + base_layout.setSpacing(0) + + # 创建自定义顶部栏 + self.createTitleBar() + base_layout.addWidget(self.title_bar) + + # 创建内容区域 + content_widget = QWidget() + content_widget.setObjectName('contentWidget') + content_layout = QVBoxLayout(content_widget) + content_layout.setContentsMargins(10, 10, 10, 10) + content_layout.setSpacing(0) + + content_container = QFrame() + content_container.setObjectName('contentContainer') + content_container.setFrameShape(QFrame.NoFrame) + content_container.setAttribute(Qt.WA_StyledBackground, True) + content_container.setFixedWidth(488) + content_container.setMinimumHeight(213) + + container_layout = QVBoxLayout(content_container) + container_layout.setContentsMargins(15, 10, 15, 10) + container_layout.setSpacing(10) + + pathLabel = QLabel('项目路径') + pathLabel.setProperty('role', 'sectionTitle') + container_layout.addWidget(pathLabel) + + path_row_widget = QWidget() + path_row_layout = QHBoxLayout(path_row_widget) + path_row_layout.setContentsMargins(0, 0, 0, 0) + path_row_layout.setSpacing(10) + path_row_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.pathEdit = QLineEdit() + self.pathEdit.setReadOnly(True) + self.pathEdit.setPlaceholderText('请选择项目路径') + self.pathEdit.setMinimumWidth(358) + self.pathEdit.setFixedHeight(30) + path_row_layout.addWidget(self.pathEdit) + + browseButton = QPushButton('浏览...') + browseButton.setObjectName('browseButton') + browseButton.clicked.connect(self.browsePath) + path_row_layout.addWidget(browseButton) + container_layout.addWidget(path_row_widget) + + nameLabel = QLabel('项目名称') + nameLabel.setProperty('role', 'sectionTitle') + container_layout.addWidget(nameLabel) + + self.nameEdit = QLineEdit() + self.nameEdit.setText('新项目') + self.nameEdit.setPlaceholderText('请输入项目名称') + self.nameEdit.selectAll() + self.nameEdit.setFixedHeight(30) + container_layout.addWidget(self.nameEdit) + + self.tipLabel = QLabel('项目名称只能包含字母、数字、下划线、中划线和中文') + self.tipLabel.setProperty('role', 'hint') + container_layout.addWidget(self.tipLabel) + + separator = QFrame() + separator.setFrameShape(QFrame.HLine) + separator.setFrameShadow(QFrame.Plain) + separator.setFixedHeight(1) + separator.setStyleSheet('background-color: #2C2F36; border: none;') + container_layout.addWidget(separator) + + button_row_widget = QWidget() + button_row_layout = QHBoxLayout(button_row_widget) + button_row_layout.setContentsMargins(0, 0, 0, 0) + button_row_layout.setSpacing(10) + button_row_layout.addStretch() + button_row_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + + self.cancelButton = QPushButton('取消') + self.cancelButton.setObjectName('secondaryButton') + self.cancelButton.clicked.connect(self.reject) + self.cancelButton.setFixedSize(120, 30) + button_row_layout.addWidget(self.cancelButton) + + self.confirmButton = QPushButton('确认') + self.confirmButton.setObjectName('primaryButton') + self.confirmButton.clicked.connect(self.validate) + self.confirmButton.setFixedSize(120, 30) + button_row_layout.addWidget(self.confirmButton) + + container_layout.addWidget(button_row_widget) + + self.confirmButton.setDefault(True) + self.confirmButton.setAutoDefault(True) + self.cancelButton.setAutoDefault(False) + + content_layout.addWidget(content_container, 0, Qt.AlignTop) + base_layout.addWidget(content_widget) + main_layout.addWidget(base_frame) + + self.projectPath = "" + self.projectName = "" + + def createTitleBar(self): + """创建自定义顶部栏""" + self.title_bar = QFrame() + self.title_bar.setObjectName("titleBar") + + title_layout = QHBoxLayout(self.title_bar) + title_layout.setContentsMargins(12, 0, 12, 0) + title_layout.setSpacing(0) + + # Control buttons area + controls = QWidget() + controls.setObjectName("controlButtons") + controls_layout = QHBoxLayout(controls) + controls_layout.setContentsMargins(0, 0, 0, 0) + controls_layout.setSpacing(0) + + self.close_button = QPushButton() + self.close_button.setObjectName("closeButton") + self.close_button.clicked.connect(self.reject) + self.close_button.setFocusPolicy(Qt.NoFocus) + controls_layout.addWidget(self.close_button) + + self._applyTitleBarIcons() + + # Reserve left space equal to control buttons to keep title centered + left_placeholder = QWidget() + left_placeholder.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + title_layout.addWidget(left_placeholder) + + self.title_label = QLabel(self.windowTitle()) + self.title_label.setObjectName("titleLabel") + self.title_label.setAlignment(Qt.AlignCenter) + title_layout.addWidget(self.title_label, 1) + + title_layout.addWidget(controls) + + left_placeholder.setFixedWidth(controls.sizeHint().width()) + + + def _applyTitleBarIcons(self): + """为标题栏按钮应用图标""" + if self._icon_close: + self.close_button.setIcon(self._icon_close) + self.close_button.setIconSize(self._title_icon_size) + self.close_button.setText("") + self.close_button.setToolTip("关闭") + + + def toggleMaximize(self): + """切换窗口最大化/还原""" + if self.isMaximized(): + self.showNormal() + else: + self.showMaximized() + + + def setWindowTitle(self, title): + super().setWindowTitle(title) + if hasattr(self, "title_label"): + self.title_label.setText(title) + + + def mousePressEvent(self, event): + """鼠标按下事件 - 用于拖拽窗口""" + if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): + if self.isMaximized(): + self.showNormal() + self.dragging = True + self.drag_position = event.globalPos() - self.frameGeometry().topLeft() + event.accept() + super().mousePressEvent(event) + + def mouseDoubleClickEvent(self, event): + """双击标题栏切换最大化""" + if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): + self.toggleMaximize() + event.accept() + return + super().mouseDoubleClickEvent(event) + + + def mouseMoveEvent(self, event): + """鼠标移动事件 - 用于拖拽窗口""" + if event.buttons() == Qt.LeftButton and self.dragging: + self.move(event.globalPos() - self.drag_position) + event.accept() + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + """鼠标释放事件 - 停止拖拽""" + if event.button() == Qt.LeftButton: + self.dragging = False + super().mouseReleaseEvent(event) + + def browsePath(self): + """浏览选择项目路径""" + path = QFileDialog.getExistingDirectory(self, "选择项目路径") + if path: + self.pathEdit.setText(path) + + def validate(self): + """验证输入并关闭对话框""" + # 获取并验证路径 + self.projectPath = self.pathEdit.text() + if not self.projectPath: + UniversalMessageDialog.show_warning(self, "错误", "请选择项目路径!", show_cancel=False, confirm_text="确认") + return + + # 获取并验证项目名称 + self.projectName = self.nameEdit.text().strip() + if not self.projectName: + UniversalMessageDialog.show_warning(self, "错误", "请输入项目名称!", show_cancel=False, confirm_text="确认") + return + + # 验证项目名称格式 + if not re.match(r'^[a-zA-Z0-9_\-\u4e00-\u9fa5]+$', self.projectName): + UniversalMessageDialog.show_warning(self, "错误", + "项目名称只能包含字母、数字、下划线、中划线和中文!", show_cancel=False, confirm_text="确认") + return + + # 检查项目是否已存在 + full_path = os.path.join(self.projectPath, self.projectName) + if os.path.exists(full_path): + UniversalMessageDialog.show_warning(self, "错误", "项目已存在!", show_cancel=False, confirm_text="确认") + return + + self.accept() + +class CustomMeta3DWidget(QMeta3DWidget): + """自定义Panda3D显示部件""" + + def __init__(self, world, parent=None): + if parent is None: + parent = wrapinstance(0, QWidget) + super().__init__(world, parent) + self.world = world + self.setFocusPolicy(Qt.StrongFocus) # 允许接收键盘焦点 + self.setAcceptDrops(True) # 允许接收拖放 + self.setMouseTracking(True) # 启用鼠标追踪 + + # 让world引用这个widget,以便获取准确的尺寸 + if hasattr(world, 'setQtWidget'): + world.setQtWidget(self) + + def getActualSize(self): + """获取Qt部件的实际渲染尺寸""" + return (self.width(), self.height()) + + def dragEnterEvent(self, event): + """处理拖拽进入事件""" + # 检查是否是文件拖拽 + if event.mimeData().hasUrls(): + # 检查是否包含支持的模型文件 + for url in event.mimeData().urls(): + filepath = url.toLocalFile() + if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): + event.acceptProposedAction() + return + event.ignore() + + def dragMoveEvent(self, event): + """处理拖拽移动事件""" + if event.mimeData().hasUrls(): + event.acceptProposedAction() + else: + event.ignore() + + def dropEvent(self, event): + """处理拖放事件""" + if event.mimeData().hasUrls(): + for url in event.mimeData().urls(): + filepath = url.toLocalFile() + if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): + # 使用关键字参数确保兼容性 + self.world.importModel(filepath) + event.acceptProposedAction() + else: + event.ignore() + + def wheelEvent(self, event): + """处理滚轮事件""" + if event.angleDelta().y() > 0: + # 滚轮向前滚动 + self.world.wheelForward() + else: + # 滚轮向后滚动 + self.world.wheelBackward() + event.accept() + + def mousePressEvent(self, event): + """处理 Qt 鼠标按下事件""" + if event.button() == Qt.LeftButton: + self.world.mousePressEventLeft({ + 'x': event.x(), + 'y': event.y() + }) + elif event.button() == Qt.RightButton: + self.world.mousePressEventRight({ + 'x': event.x(), + 'y': event.y() + }) + elif event.button() == Qt.MiddleButton: # 添加滑轮按下事件处理 + self.world.mousePressEventMiddle({ + 'x': event.x(), + 'y': event.y() + }) + event.accept() + + def mouseReleaseEvent(self, event): + """处理 Qt 鼠标释放事件""" + if event.button() == Qt.LeftButton: + self.world.mouseReleaseEventLeft({ + 'x': event.x(), + 'y': event.y() + }) + elif event.button() == Qt.RightButton: + self.world.mouseReleaseEventRight({ + 'x': event.x(), + 'y': event.y() + }) + elif event.button() == Qt.MiddleButton: # 添加滑轮释放事件处理 + self.world.mouseReleaseEventMiddle({ + 'x': event.x(), + 'y': event.y() + }) + event.accept() + + def mouseMoveEvent(self, event): + """处理 Qt 鼠标移动事件""" + self.world.mouseMoveEvent({ + 'x': event.x(), + 'y': event.y() + }) + event.accept() + + def cleanup(self): + """清理Panda3D资源""" + try: + print("🧹 清理CustomPanda3DWidget资源...") + + # 清理world资源 + if hasattr(self, 'world') and self.world: + # 如果world有cleanup方法,调用它 + if hasattr(self.world, 'cleanup'): + self.world.cleanup() + # 清理world引用 + self.world = None + + # 调用父类的清理方法(如果存在) + if hasattr(super(), 'cleanup'): + super().cleanup() + + print("✅ CustomPanda3DWidget资源清理完成") + + except Exception as e: + print(f"⚠️ 清理CustomPanda3DWidget资源时出错: {e}") + +class CustomFileView(QTreeView): + """自定义文件浏览器""" + + def __init__(self, world, parent=None): + if parent is None: + parent = wrapinstance(0, QWidget) + super().__init__(parent) + base_font = self.font() + base_font.setFamily("Inter") + base_font.setPointSize(10) + base_font.setWeight(QFont.Normal) + self.setFont(base_font) + if self.model(): + self.model().rowsInserted.connect(self._handle_rows_inserted) + self.world = world + self.setupUI() + self.setupDragDrop() + + def setupUI(self): + """初始化UI设置""" + self.setHeaderHidden(True) + # 启用多选和拖拽 + self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + self.setDropIndicatorShown(True) # 启用拖放指示线 + + def setupDragDrop(self): + """设置拖拽功能""" + # 使用自定义拖拽模式 + self.setDragDropMode(QTreeView.DragOnly) # 只允许拖出,不允许拖入 + self.setDefaultDropAction(Qt.DropAction.MoveAction) + self.setDragEnabled(True) + self.setAcceptDrops(True) + + def startDrag(self, supportedActions): + """开始拖拽操作""" + # 获取选中的文件 + indexes = self.selectedIndexes() + if not indexes: + return + + # 只处理文件名列 + indexes = [idx for idx in indexes if idx.column() == 0] + + # 创建 MIME 数据 + mimeData = self.model().mimeData(indexes) + + # 检查是否包含支持的模型文件 + urls = [] + for index in indexes: + filepath = self.model().filePath(index) + if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): + urls.append(QUrl.fromLocalFile(filepath)) + + if not urls: + return + + # 设置 URL 列表 + mimeData.setUrls(urls) + + # 创建拖拽对象 + drag = QDrag(self) + drag.setMimeData(mimeData) + + # 设置拖拽图标(可选) + pixmap = QPixmap(32, 32) + pixmap.fill(Qt.transparent) + painter = QPainter(pixmap) + painter.drawText(pixmap.rect(), Qt.AlignCenter, str(len(urls))) + painter.end() + drag.setPixmap(pixmap) + + # 执行拖拽 + drag.exec_(supportedActions) + + def mouseDoubleClickEvent(self, event): + """双击标题栏切换最大化""" + index = self.indexAt(event.pos()) + if index.isValid(): + model = self.model() + filepath = model.filePath(index) + + # 检查是否是模型文件 + if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): + self.world.importModel(filepath) + else: + print("不支持的文件类型") + super().mouseDoubleClickEvent(event) + +from PyQt5.QtCore import QFileSystemWatcher,QTimer +import os + +class CustomAssetsTreeWidget(QTreeWidget): + def __init__(self, world, parent=None): + if parent is None: + parent = wrapinstance(0, QWidget) + super().__init__(parent) + self.world = world + self.root_path = None + self.setupUI() + self.setupDragDrop() + + #添加文件系统监控器 + self.file_watcher = QFileSystemWatcher() + self.file_watcher.directoryChanged.connect(self.onDirectoryChanged) + self.file_watcher.fileChanged.connect(self.onFileChanged) + + self.refresh_timer = QTimer() + self.refresh_timer.setSingleShot(True) + self.refresh_timer.timeout.connect(self.refreshView) + + #存储监控的目录 + self.watched_directories = set() + + # 默认加载项目根路径 + self.load_file_tree() + # 设置右键菜单 + self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.customContextMenuRequested.connect(self.showContextMenu) + + def showContextMenu(self, position): + """显示右键菜单""" + item = self.itemAt(position) + if not item: + return + + filepath = item.data(0, Qt.UserRole) + is_folder = item.data(0, Qt.UserRole + 1) + + menu = QMenu(self) + + if is_folder: + # 文件夹右键菜单 + menu.addAction("📁 新建文件夹", lambda: self.createNewFolder(item)) + menu.addAction("📄 新建文件", lambda: self.createNewFile(item)) + menu.addSeparator() + menu.addAction("✏️ 重命名", lambda: self.renameItem(item)) + menu.addAction("🗑️ 删除", lambda: self.deleteItem(item)) + menu.addSeparator() + menu.addAction("📋 复制路径", lambda: self.copyPath(filepath)) + menu.addAction("🔍 查看属性", lambda: self.showProperties(item)) + else: + # 文件右键菜单 + menu.addAction("📂 打开文件", lambda: self.openFile(filepath)) + menu.addSeparator() + menu.addAction("✏️ 重命名", lambda: self.renameItem(item)) + menu.addAction("🗑️ 删除", lambda: self.deleteItem(item)) + menu.addSeparator() + menu.addAction("📋 复制路径", lambda: self.copyPath(filepath)) + menu.addAction("🔍 查看属性", lambda: self.showProperties(item)) + + # 显示菜单 + menu.exec_(self.mapToGlobal(position)) + + def _saveExpandedState(self): + """保存展开状态""" + expanded_paths = set() + + def collectExpanded(item): + if item.isExpanded(): + path = item.data(0, Qt.UserRole) + if path: + expanded_paths.add(path) + + for i in range(item.childCount()): + collectExpanded(item.child(i)) + + # 遍历所有顶级项目 + for i in range(self.topLevelItemCount()): + collectExpanded(self.topLevelItem(i)) + + return expanded_paths + + def _restoreExpandedState(self, expanded_paths): + """恢复展开状态""" + def restoreExpanded(item): + path = item.data(0, Qt.UserRole) + if path in expanded_paths: + item.setExpanded(True) + + for i in range(item.childCount()): + restoreExpanded(item.child(i)) + + # 遍历所有顶级项目 + for i in range(self.topLevelItemCount()): + restoreExpanded(self.topLevelItem(i)) + + def _refreshWithStatePreservation(self): + """刷新树形视图并保持状态""" + # 保存当前状态 + expanded_paths = self._saveExpandedState() + + # 刷新树形结构 + self.load_file_tree() + + # 恢复展开状态 + self._restoreExpandedState(expanded_paths) + + def createNewFolder(self, parent_item): + """新建文件夹""" + import os + + parent_path = parent_item.data(0, Qt.UserRole) + dialog = StyledTextInputDialog(self, "新建文件夹", "文件夹名称", "请输入文件夹名称") + if dialog.exec_() != QDialog.Accepted: + return + + folder_name = dialog.text() + if not folder_name: + return + + new_folder_path = os.path.join(parent_path, folder_name) + try: + os.makedirs(new_folder_path, exist_ok=True) + self._refreshWithStatePreservation() + print(f"新建文件夹: {new_folder_path}") + except OSError as e: + print(f"新建文件夹失败: {e}") + + + def createNewFile(self, parent_item): + """新建文件""" + import os + + parent_path = parent_item.data(0, Qt.UserRole) + dialog = StyledTextInputDialog(self, "新建文件", "文件名称", "请输入文件名称") + if dialog.exec_() != QDialog.Accepted: + return + + file_name = dialog.text() + if not file_name: + return + + new_file_path = os.path.join(parent_path, file_name) + try: + with open(new_file_path, "w", encoding="utf-8") as f: + f.write("") + self._refreshWithStatePreservation() + print(f"新建文件: {new_file_path}") + except OSError as e: + print(f"新建文件失败: {e}") + + + def renameItem(self, item): + """重命名文件或文件夹""" + import os + + old_path = item.data(0, Qt.UserRole) + old_name = os.path.basename(old_path) + + dialog = StyledTextInputDialog(self, "重命名", "新名称", "请输入新名称", old_name) + if dialog.exec_() != QDialog.Accepted: + return + + new_name = dialog.text() + if not new_name or new_name == old_name: + return + + parent_dir = os.path.dirname(old_path) + new_path = os.path.join(parent_dir, new_name) + + try: + os.rename(old_path, new_path) + item.setText(0, new_name) + item.setData(0, Qt.UserRole, new_path) + self._refreshWithStatePreservation() + except OSError as e: + print(f"重命名失败: {e}") + + + def deleteItem(self, item): + """删除文件或文件夹""" + import os + import shutil + + filepath = item.data(0, Qt.UserRole) + is_folder = item.data(0, Qt.UserRole + 1) + + item_type = "文件夹" if is_folder else "文件" + result = UniversalMessageDialog.show_warning( + self, + "确认删除", + f"确定要删除这个{item_type}吗?\n{filepath}", + show_cancel=True, + confirm_text="删除", + cancel_text="取消" + ) + + if result == QDialog.Accepted: + try: + if is_folder: + shutil.rmtree(filepath) + else: + os.remove(filepath) + self._refreshWithStatePreservation() + print(f"删除{item_type}: {filepath}") + except OSError as e: + print(f"删除{item_type}失败: {e}") + UniversalMessageDialog.show_error( + self, + "错误", + f"删除{item_type}失败: {e}", + show_cancel=False, + confirm_text="确认" + ) + + def copyPath(self, filepath): + """复制路径到剪贴板""" + from PyQt5.QtWidgets import QApplication + + clipboard = QApplication.clipboard() + clipboard.setText(filepath) + print(f"已复制路径: {filepath}") + + def openFile(self, filepath): + """打开文件""" + import os + import subprocess + import platform + + try: + system = platform.system() + if system == "Windows": + os.startfile(filepath) + elif system == "Darwin": # macOS + subprocess.run(["open", filepath]) + else: # Linux + subprocess.run(["xdg-open", filepath]) + print(f"打开文件: {filepath}") + except Exception as e: + print(f"打开文件失败: {e}") + + def showProperties(self, item): + """显示属性面板""" + import os + + filepath = item.data(0, Qt.UserRole) + is_folder = item.data(0, Qt.UserRole + 1) + + try: + stat = os.stat(filepath) + size = stat.st_size + modified = os.path.getmtime(filepath) + + import datetime + modified_str = datetime.datetime.fromtimestamp(modified).strftime('%Y-%m-%d %H:%M:%S') + + item_type = "文件夹" if is_folder else "文件" + size_str = f"{size} 字节" if not is_folder else "文件夹" + + properties = f""" + 路径: {filepath} + 类型: {item_type} + 大小: {size_str} + 修改时间: {modified_str} + """ + + UniversalMessageDialog.show_info( + self, + "属性", + properties.strip(), + show_cancel=False, + confirm_text="确认" + ) + + except OSError as e: + UniversalMessageDialog.show_warning( + self, + "错误", + f"无法获取属性: {e}", + show_cancel=False, + confirm_text="确认" + ) + + + # def mouseDoubleClickEvent(self, event): + # """处理双击事件""" + # item = self.itemAt(event.pos()) + # if item: + # filepath = item.data(0, Qt.UserRole) + # is_folder = item.data(0, Qt.UserRole + 1) + # + # if is_folder: + # # 文件夹:展开/折叠 + # item.setExpanded(not item.isExpanded()) + # else: + # # 文件:检查是否是模型文件 + # if filepath and filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): + # self.world.importModel(filepath) + # else: + # # 其他文件:用系统默认程序打开 + # self.openFile(filepath) + # + # super().mouseDoubleClickEvent(event) + + def setupUI(self): + """初始化UI设置""" + self.setHeaderHidden(True) + # 启用多选和拖拽 + self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + self.setDropIndicatorShown(True) # 启用拖放指示线 + + def setupDragDrop(self): + """设置拖拽功能""" + # 使用InternalMove模式以正确显示插入指示线 + self.setDragDropMode(QAbstractItemView.InternalMove) + self.setDefaultDropAction(Qt.DropAction.MoveAction) + self.setDragEnabled(True) + self.setAcceptDrops(True) + self.setDropIndicatorShown(True) + + def getProjectRootPath(self): + """获取项目根路径下的Resources文件夹,考虑跨平台""" + import os + + # 获取当前文件所在目录,然后向上查找项目根目录 + current_dir = os.path.dirname(os.path.abspath(__file__)) + + # 向上查找直到找到项目根目录(包含特定标识文件或文件夹) + project_root = current_dir + max_depth = 10 # 限制向上查找的深度 + depth = 0 + + while depth < max_depth: + # 检查是否是项目根目录(可以根据实际情况调整判断条件) + if (os.path.exists(os.path.join(project_root, "main.py")) or + os.path.exists(os.path.join(project_root, "setup.py")) or + os.path.exists(os.path.join(project_root, ".git"))): + break + parent_dir = os.path.dirname(project_root) + if parent_dir == project_root: # 已经到达文件系统根目录 + # 回退到使用当前工作目录 + project_root = os.getcwd() + break + project_root = parent_dir + depth += 1 + + # 构建Resources文件夹路径(跨平台) + resources_path = os.path.join(project_root, "Resources") + + # 如果Resources文件夹不存在,创建它 + if not os.path.exists(resources_path): + try: + os.makedirs(resources_path, exist_ok=True) + print(f"创建Resources文件夹: {resources_path}") + except OSError as e: + print(f"无法创建Resources文件夹: {e}") + # 如果无法创建,回退到项目根路径 + return project_root + + return resources_path + + # def getProjectRootPath(self): + # """获取项目根路径下的Resources文件夹,考虑跨平台""" + # import os + # + # # 获取项目根路径 + # project_root = os.getcwd() + # + # # 构建Resources文件夹路径(跨平台) + # resources_path = os.path.join(project_root, "Resources") + # + # # 如果Resources文件夹不存在,创建它 + # if not os.path.exists(resources_path): + # try: + # os.makedirs(resources_path, exist_ok=True) + # print(f"创建Resources文件夹: {resources_path}") + # except OSError as e: + # print(f"无法创建Resources文件夹: {e}") + # # 如果无法创建,回退到项目根路径 + # return project_root + # + # return resources_path + + def load_file_tree(self): + """加载树形视图""" + self.clear() + self.current_path = self.getProjectRootPath() + try: + # 创建当前目录的根节点 + root_name = os.path.basename(self.current_path) or self.current_path + root_item = QTreeWidgetItem([f"📁 {root_name}"]) + root_item.setData(0, Qt.UserRole, self.current_path) + root_item.setData(0, Qt.UserRole + 1, True) + font = QFont("Microsoft YaHei", 10, QFont.Light) # 假设您希望字体大小为10 + root_item.setFont(0, font) + self.addTopLevelItem(root_item) + + # 加载当前目录内容 + self.load_directory_tree(self.current_path, root_item) + + #添加目录到监控器 + self.addWatchedDirectory(self.current_path) + + # 展开根节点 + root_item.setExpanded(True) + + except PermissionError: + error_item = QTreeWidgetItem(["❌ 无权限访问此目录"]) + self.addTopLevelItem(error_item) + + def addWatchedDirectory(self,directory): + """添加监控目录""" + if os.path.exists(directory) and directory not in self.watched_directories: + if self.file_watcher.addPath(directory): + self.watched_directories.add(directory) + #print(f"开始监控目录:{directory}") + try: + for item in os.listdir(directory): + item_path = os.path.join(directory,item) + if os.path.isdir(item_path): + self.addWatchedDirectory(item_path) + except Exception as e: + pass + else: + print(f"无法监控目录:{directory}") + + def removeWatchedDirectory(self,directory): + """移除监控目录""" + if directory in self.watched_directories: + if self.file_watcher.removePath(directory): + self.watched_directories.discard(directory) + print(f"停止监控目录:{directory}") + else: + print(f"无法停止监控目录:{directory}") + + def onDirectoryChanged(self,path): + """目录发生变化时处理""" + print(f"目录变化{path}") + if not self.refresh_timer.isActive(): + self.refresh_timer.start(1000) + + def onFileChanged(self,path): + """目录发生变化时的处理""" + print(f"目录变化{path}") + if not self.refresh_timer.isActive(): + self.refresh_timer.start(1000) + + def refreshView(self): + """刷新视图""" + print("刷新资源视图...") + try: + expanded_paths = self._saveExpandedState() + self.load_file_tree() + self._restoreExpandedState(expanded_paths) + print("资源视图刷新完成") + except Exception as e: + print(f"刷新资源视图失败{e}") + + def load_directory_tree(self, path, parent_item, max_depth=3, current_depth=0): + """递归加载目录树(类似左侧导航面板)""" + if current_depth >= max_depth: + return + + try: + items = os.listdir(path) + items.sort() + + # 分别处理文件夹和文件 + folders = [] + files = [] + + for item in items: + # 跳过隐藏文件和系统文件 + if item.startswith('.') or item.startswith('__'): + continue + + item_path = os.path.join(path, item) + if os.path.isdir(item_path): + folders.append(item) + elif os.path.isfile(item_path): + files.append(item) + + # 先添加文件夹 + for folder in folders: + folder_path = os.path.join(path, folder) + folder_item = self.create_simple_tree_item(folder, folder_path, True) + parent_item.addChild(folder_item) + + # 递归加载子目录(限制深度) + if current_depth < max_depth - 1: + self.load_directory_tree(folder_path, folder_item, max_depth, current_depth + 1) + + # 再添加文件(显示重要文件类型,包括图片和模型) + important_extensions = { + # 编程文件 + '.py', '.js', '.html', '.css', '.java', '.cpp', '.c', '.php', '.rb', '.go', '.rs', + # 文档文件 + '.md', '.txt', '.pdf', '.doc', '.docx', '.rtf', + # 配置和数据文件 + '.json', '.xml', '.yaml', '.yml', '.ini', '.cfg', '.toml', + # 图片文件 + '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg', '.ico', '.webp', '.tiff', '.tif', + # 3D模型文件 + '.fbx', '.obj', '.3ds', '.max', '.blend', '.dae', '.gltf', '.glb', '.stl', '.ply', + # 音视频文件 + '.mp3', '.wav', '.mp4', '.avi', '.mov', '.wmv', '.flac', '.aac', + # 压缩文件 + '.zip', '.rar', '.7z', '.tar', '.gz', + # 材质和纹理 + '.mtl', '.mat', '.exr', '.hdr', '.hdri', '.dds', + # CAD文件 + '.dwg', '.dxf', '.step', '.stp', '.iges', '.sldprt', '.sldasm', + # 其他重要文件 + '.exe', '.dll', '.bat', '.sh', '.log' + } + + # 重要文件名(不依赖扩展名) + important_files = {'requirements.txt', 'README.md', 'main.py', 'LICENSE', 'CHANGELOG.md', 'INSTALL.md'} + + for file in files: + file_ext = os.path.splitext(file)[1].lower() + if file_ext in important_extensions or file in important_files: + file_path = os.path.join(path, file) + file_item = self.create_simple_tree_item(file, file_path, False) + parent_item.addChild(file_item) + + except (OSError, PermissionError): + pass + + def create_simple_tree_item(self, name, path, is_folder): + """创建简单的树形项目""" + try: + if is_folder: + # 文件夹项目,展开/折叠图标由CSS样式控制 + item = QTreeWidgetItem([f"📁 {name}"]) + item.setData(0, Qt.UserRole + 1, True) # 标记为文件夹 + else: + icon = self.get_file_icon(name) + item = QTreeWidgetItem([f"{icon} {name}"]) + item.setData(0, Qt.UserRole + 1, False) # 标记为文件 + + item.setData(0, Qt.UserRole, path) + + return item + + except (OSError, PermissionError): + item = QTreeWidgetItem([name]) + item.setData(0, Qt.UserRole, path) + return item + + def get_file_icon(self, filename): + """ + 根据文件扩展名获取图标 + + 这个函数根据文件的扩展名返回对应的Unicode图标字符。 + 如果文件类型不在映射表中,返回默认的文档图标。 + + 参数: + filename (str): 文件名(包含扩展名) + + 返回值: + str: 对应的Unicode图标字符 + """ + # 获取文件扩展名并转换为小写 + ext = os.path.splitext(filename)[1].lower() + + # 文件扩展名到图标的映射表 + icon_map = { + # 编程语言文件 + '.py': '🐍', # Python文件 + '.js': '⚡', # JavaScript文件 + '.html': '🌐', # HTML文件 + '.css': '🎨', # CSS样式文件 + '.java': '☕', # Java文件 + '.cpp': '⚙️', # C++文件 + '.c': '⚙️', # C文件 + '.h': '📋', # 头文件 + '.php': '🐘', # PHP文件 + '.rb': '💎', # Ruby文件 + '.go': '🐹', # Go文件 + '.rs': '🦀', # Rust文件 + '.swift': '🐦', # Swift文件 + '.kt': '🎯', # Kotlin文件 + + # 文档文件 + '.txt': '📄', # 纯文本文件 + '.md': '📝', # Markdown文档 + '.rst': '📝', # reStructuredText文档 + '.pdf': '📕', # PDF文档 + '.doc': '📘', # Word文档 + '.docx': '📘', # Word文档 + '.rtf': '📄', # RTF文档 + '.odt': '📄', # OpenDocument文本 + + # 数据文件 + '.json': '📋', # JSON数据文件 + '.xml': '📋', # XML文件 + '.yaml': '📋', # YAML文件 + '.yml': '📋', # YAML文件 + '.csv': '📊', # CSV表格文件 + '.xls': '📗', # Excel表格 + '.xlsx': '📗', # Excel表格 + '.ods': '📊', # OpenDocument表格 + + # 图像文件 + '.jpg': '🖼️', # JPEG图像 + '.jpeg': '🖼️', # JPEG图像 + '.png': '🖼️', # PNG图像 + '.gif': '🖼️', # GIF图像 + '.bmp': '🖼️', # BMP图像 + '.svg': '🎨', # SVG矢量图 + '.ico': '🎯', # 图标文件 + '.webp': '🖼️', # WebP图像 + '.tiff': '🖼️', # TIFF图像 + '.tif': '🖼️', # TIFF图像 + + # 音视频文件 + '.mp4': '🎬', # MP4视频 + '.avi': '🎬', # AVI视频 + '.mov': '🎬', # MOV视频 + '.wmv': '🎬', # WMV视频 + '.flv': '🎬', # FLV视频 + '.webm': '🎬', # WebM视频 + '.mp3': '🎵', # MP3音频 + '.wav': '🎵', # WAV音频 + '.flac': '🎵', # FLAC音频 + '.aac': '🎵', # AAC音频 + '.ogg': '🎵', # OGG音频 + + # 压缩文件 + '.zip': '📦', # ZIP压缩包 + '.rar': '📦', # RAR压缩包 + '.7z': '📦', # 7Z压缩包 + '.tar': '📦', # TAR归档 + '.gz': '📦', # GZIP压缩 + '.bz2': '📦', # BZIP2压缩 + '.xz': '📦', # XZ压缩 + + # 可执行文件 + '.exe': '⚙️', # Windows可执行文件 + '.msi': '📦', # Windows安装包 + '.deb': '📦', # Debian包 + '.rpm': '📦', # RPM包 + '.dmg': '💿', # macOS磁盘镜像 + '.app': '📱', # macOS应用程序 + + # 系统文件 + '.dll': '🔧', # 动态链接库 + '.so': '🔧', # 共享库 + '.dylib': '🔧', # macOS动态库 + '.lib': '📚', # 静态库 + + # 脚本文件 + '.bat': '📜', # Windows批处理 + '.cmd': '📜', # Windows命令脚本 + '.sh': '📜', # Shell脚本 + '.ps1': '💙', # PowerShell脚本 + '.vbs': '📜', # VBScript脚本 + + # 配置文件 + '.ini': '⚙️', # INI配置文件 + '.cfg': '⚙️', # 配置文件 + '.conf': '⚙️', # 配置文件 + '.config': '⚙️', # 配置文件 + '.toml': '⚙️', # TOML配置文件 + + # 3D模型文件 + '.fbx': '🎭', # FBX模型文件 + '.obj': '🎭', # OBJ模型文件 + '.3ds': '🎭', # 3DS Max模型 + '.max': '🎭', # 3DS Max场景 + '.blend': '🎭', # Blender模型 + '.dae': '🎭', # COLLADA模型 + '.gltf': '🎭', # glTF模型 + '.glb': '🎭', # glTF二进制模型 + '.x3d': '🎭', # X3D模型 + '.ply': '🎭', # PLY模型 + '.stl': '🎭', # STL模型(3D打印) + '.off': '🎭', # OFF模型 + '.3mf': '🎭', # 3MF模型 + '.amf': '🎭', # AMF模型 + '.x': '🎭', # DirectX模型 + '.md2': '🎭', # Quake II模型 + '.md3': '🎭', # Quake III模型 + '.mdl': '🎭', # Source引擎模型 + '.mesh': '🎭', # OGRE模型 + '.scene': '🎭', # OGRE场景 + '.ac': '🎭', # AC3D模型 + '.ase': '🎭', # ASCII Scene Export + '.assbin': '🎭', # Assimp二进制 + '.b3d': '🎭', # Blitz3D模型 + '.bvh': '🎭', # BioVision层次 + '.csm': '🎭', # CharacterStudio Motion + '.hmp': '🎭', # 3D GameStudio模型 + '.irr': '🎭', # Irrlicht场景 + '.irrmesh': '🎭', # Irrlicht网格 + '.lwo': '🎭', # LightWave对象 + '.lws': '🎭', # LightWave场景 + '.ms3d': '🎭', # MilkShape 3D + '.nff': '🎭', # Neutral文件格式 + '.q3o': '🎭', # Quick3D对象 + '.q3s': '🎭', # Quick3D场景 + '.raw': '🎭', # RAW三角形 + '.smd': '🎭', # Valve SMD + '.ter': '🎭', # Terragen地形 + '.uc': '🎭', # Unreal模型 + '.vta': '🎭', # Valve VTA + '.xgl': '🎭', # XGL模型 + '.zgl': '🎭', # ZGL模型 + + # 纹理和材质文件 + '.mtl': '🎨', # OBJ材质文件 + '.mat': '🎨', # 材质文件 + '.sbsar': '🎨', # Substance Archive + '.sbs': '🎨', # Substance Designer + '.sbsm': '🎨', # Substance材质 + '.exr': '🖼️', # OpenEXR高动态范围图像 + '.hdr': '🖼️', # HDR图像 + '.hdri': '🖼️', # HDRI环境贴图 + '.dds': '🖼️', # DirectDraw Surface + '.ktx': '🖼️', # Khronos纹理 + '.astc': '🖼️', # ASTC纹理 + '.pvr': '🖼️', # PowerVR纹理 + '.etc1': '🖼️', # ETC1纹理 + '.etc2': '🖼️', # ETC2纹理 + + # 动画文件 + '.anim': '🎬', # 动画文件 + '.fbx': '🎬', # FBX动画(也可以是模型) + '.bip': '🎬', # Character Studio Biped + '.cal3d': '🎬', # Cal3D动画 + '.motion': '🎬', # 动作文件 + '.mocap': '🎬', # 动作捕捉数据 + + # 其他常见文件 + '.log': '📋', # 日志文件 + '.tmp': '🗂️', # 临时文件 + '.bak': '💾', # 备份文件 + '.old': '📦', # 旧文件 + } + + # 返回对应的图标,如果找不到则返回默认文档图标 + return icon_map.get(ext, '📄') + + def startDrag(self, supportedActions): + """开始拖拽操作""" + selected_items = self.selectedItems() + if not selected_items: + return + + # 创建 MIME 数据 + mimeData = QMimeData() + + # 收集文件路径用于向外拖拽 + urls = [] + internal_paths = [] + + for item in selected_items: + filepath = item.data(0, Qt.UserRole) + if filepath: + internal_paths.append(filepath) + # 检查是否是模型文件(用于向外拖拽) + if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): + urls.append(QUrl.fromLocalFile(filepath)) + + # 设置内部拖拽数据 + mimeData.setText('\n'.join(internal_paths)) + + # 设置向外拖拽数据 + if urls: + mimeData.setUrls(urls) + + # 创建拖拽对象 + drag = QDrag(self) + drag.setMimeData(mimeData) + + # 设置拖拽图标 + pixmap = QPixmap(32, 32) + pixmap.fill(Qt.transparent) + painter = QPainter(pixmap) + painter.drawText(pixmap.rect(), Qt.AlignCenter, str(len(selected_items))) + painter.end() + drag.setPixmap(pixmap) + + # 执行拖拽 + drag.exec_(supportedActions) + + def dragEnterEvent(self, event): + """处理拖拽进入事件""" + # 检查是否是内部拖拽 + if event.mimeData().hasText(): + event.acceptProposedAction() + else: + event.ignore() + + def dragMoveEvent(self, event): + """处理拖拽移动事件""" + if not event.mimeData().hasText(): + event.ignore() + return + + # 获取目标项 + target_item = self.itemAt(event.pos()) + selected_items = self.selectedItems() + + # 检查是否拖拽到多选区域内的项目 + if target_item and target_item in selected_items: + event.ignore() + return + + # 检查是否拖拽到自己的子级 + if target_item: + for selected_item in selected_items: + if self._isChildOf(target_item, selected_item): + event.ignore() + return + + # 如果拖到文件夹,允许拖到文件夹内部 + if target_item: + is_folder = target_item.data(0, Qt.UserRole + 1) + if is_folder: + # 接受拖放,显示指示框 + event.acceptProposedAction() + return + + + # 调用父类方法处理插入指示线 + event.accept() + super().dragMoveEvent(event) + + def _isChildOf(self, potential_child, potential_parent): + """检查 potential_child 是否是 potential_parent 的子级""" + current = potential_child.parent() + while current: + if current == potential_parent: + return True + current = current.parent() + return False + + def dropEvent(self, event): + """处理拖放事件""" + if not event.mimeData().hasText(): + event.ignore() + return + + drag_paths = event.mimeData().text().split('\n') + if not drag_paths: + event.ignore() + return + + # 获取目标项 + target_item = self.itemAt(event.pos()) + if not target_item: + # 如果拖到空白处,默认放到根目录 + target_path = self.current_path + else: + # 如果是文件夹,就放到里面 + is_folder = target_item.data(0, Qt.UserRole + 1) + if is_folder: + target_path = target_item.data(0, Qt.UserRole) + else: + # 如果是文件,则放到其父目录 + parent_item = target_item.parent() + if parent_item: + target_path = parent_item.data(0, Qt.UserRole) + else: + target_path = self.current_path + + # 执行移动 + self._moveFiles(drag_paths, target_path) + event.acceptProposedAction() + # 让 Qt 更新界面 + super().dropEvent(event) + + def _moveFiles(self, source_paths, target_dir): + """移动文件到目标目录""" + import os + import shutil + from PyQt5.QtWidgets import QMessageBox + + moved_files = [] + failed_files = [] + + for source_path in source_paths: + if not source_path or not os.path.exists(source_path): + continue + + if os.path.isdir(source_path) and target_dir.startswith(source_path): + failed_files.append(f"{source_path} (不能移动到子目录)") + continue + + if os.path.dirname(source_path) == target_dir: + continue + + filename = os.path.basename(source_path) + target_path = os.path.join(target_dir, filename) + + if os.path.exists(target_path): + failed_files.append(f"{filename} (目标已存在)") + continue + + try: + shutil.move(source_path, target_path) + moved_files.append(filename) + print(f"移动文件: {source_path} -> {target_path}") + except Exception as e: + failed_files.append(f"{filename} ({str(e)})") + + # 使用状态保持刷新 + if moved_files: + self._refreshWithStatePreservation() + + if failed_files: + StyledMessageBox.warning( + self, + "移动失败", + f"以下文件移动失败:\n" + "\n".join(failed_files) + ) + + if moved_files: + print(f"成功移动 {len(moved_files)} 个文件") + + # def mouseDoubleClickEvent(self, event): + # """处理双击事件""" + # item = self.itemAt(event.pos()) + # if item: + # filepath = item.data(0, Qt.UserRole) + # is_folder = item.data(0, Qt.UserRole + 1) + # + # if is_folder: + # # 文件夹:展开/折叠 + # item.setExpanded(not item.isExpanded()) + # else: + # # 文件:检查是否是模型文件 + # if filepath and filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')): + # self.world.importModel(filepath) + # else: + # # 其他文件:用系统默认程序打开 + # self.openFile(filepath) + # + # super().mouseDoubleClickEvent(event) + + +class CustomConsoleDockWidget(QWidget): + """自定义控制台停靠部件""" + + def __init__(self, world, parent=None): + if parent is None: + parent = wrapinstance(0, QWidget) + super().__init__(parent) + self.world = world + self.setupUI() + self.setupConsoleRedirect() + + def setupUI(self): + """初始化控制台UI""" + layout = QVBoxLayout(self) + + # 控制台工具栏 + toolbar = QHBoxLayout() + + # 清空按钮 + self.clearBtn = QPushButton("清空") + self.clearBtn.setStyleSheet(""" + QPushButton { + background-color: rgba(84, 89, 98, 0.5); + color: #ffffff; + border: none; + padding: 4px 12px; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 12px; + font-weight: 300; + letter-spacing: 0.7px; + } + QPushButton:hover { + background-color: rgba(84, 89, 98, 0.7); + } + QPushButton:pressed { + background-color: rgba(84, 89, 98, 0.9); + } + """) + self.clearBtn.clicked.connect(self.clearConsole) + toolbar.addWidget(self.clearBtn) + + # 自动滚动开关 + self.autoScrollBtn = QPushButton("自动滚动") + self.autoScrollBtn.setCheckable(True) + self.autoScrollBtn.setChecked(True) + self.autoScrollBtn.setStyleSheet(""" + QPushButton { + background-color: rgba(84, 89, 98, 0.5); + color: #ffffff; + border: none; + padding: 4px 12px; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 12px; + font-weight: 300; + letter-spacing: 0.7px; + } + QPushButton:checked { + background-color: #3067c0; + color: #ffffff; + } + QPushButton:hover { + background-color: rgba(84, 89, 98, 0.7); + } + QPushButton:pressed { + background-color: rgba(84, 89, 98, 0.9); + } + """) + toolbar.addWidget(self.autoScrollBtn) + + # 时间戳开关 + self.timestampBtn = QPushButton("显示时间") + self.timestampBtn.setCheckable(True) + self.timestampBtn.setChecked(True) + self.timestampBtn.setStyleSheet(""" + QPushButton { + background-color: rgba(84, 89, 98, 0.5); + color: #ffffff; + border: none; + padding: 4px 12px; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 12px; + font-weight: 300; + letter-spacing: 0.7px; + } + QPushButton:checked { + background-color: #3067c0; + color: #ffffff; + } + QPushButton:hover { + background-color: rgba(84, 89, 98, 0.7); + } + QPushButton:pressed { + background-color: rgba(84, 89, 98, 0.9); + } + """) + toolbar.addWidget(self.timestampBtn) + + self.fpsLabel = QLabel("FPS:0.0") + self.fpsLabel.setStyleSheet(""" + QLabel { + background-color: #3067c0; + color: #ffffff; + border: none; + padding: 4px 12px; + border-radius: 2px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 12px; + font-weight: 300; + letter-spacing: 0.7px; + } + """) + self.fpsLabel.setMinimumWidth(100) + self.fpsLabel.setAlignment(Qt.AlignCenter) + toolbar.addWidget(self.fpsLabel) + + # 帧率更新定时器 + self.fpsTimer = QTimer() + self.fpsTimer.timeout.connect(self.updateFPS) + self.fpsTimer.start(105) # 每秒更新一次 + + toolbar.addStretch() + layout.addLayout(toolbar) + + # 控制台文本区域 + from PyQt5.QtWidgets import QTextEdit + self.consoleText = QTextEdit() + self.consoleText.setReadOnly(True) + self.consoleText.setStyleSheet(""" + QTextEdit { + background-color: #19191b; + color: #ebebeb; + font-family: 'Consolas', 'Monaco', 'Microsoft YaHei', monospace; + font-size: 10px; + font-weight: 300; + border: none; + letter-spacing: 0.5px; + line-height: 12px; + } + """) + layout.addWidget(self.consoleText) + + # # 命令输入区域 + # inputLayout = QHBoxLayout() + # inputLayout.addWidget(QLabel(">>> ")) + # + # self.commandInput = QLineEdit() + # self.commandInput.setStyleSheet(""" + # QLineEdit { + # background-color: #2d2d2d; + # color: #ffffff; + # font-family: 'Consolas', 'Monaco', monospace; + # font-size: 10pt; + # border: 1px solid #3e3e3e; + # padding: 5px; + # } + # """) + # self.commandInput.returnPressed.connect(self.executeCommand) + # inputLayout.addWidget(self.commandInput) + # + # layout.addLayout(inputLayout) + + # 添加欢迎信息 + self.addMessage("🎮 编辑器控制台已启动", "INFO") + + def updateFPS(self): + try: + if hasattr(self.world,'clock'): + fps = self.world.clock.getAverageFrameRate() + self.fpsLabel.setText(f"FPS:{fps:.1f}") + + # 根据帧率设置颜色 + if fps >= 50: + color = "#80ff80" # 绿色 - 优秀 + elif fps >= 30: + color = "#ffff80" # 黄色 - 一般 + else: + color = "#ff8080" # 红色 - 较差 + + self.fpsLabel.setStyleSheet(f""" + QLabel {{ + background-color: #3067c0; + color: {color}; + border: 1px solid #3a3a4a; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + font-family: 'Consolas', 'Monaco', monospace; + }} + """) + except Exception as e: + pass # 静默处理错误,避免影响控制台功能 + + def setupConsoleRedirect(self): + """设置控制台重定向""" + import sys + + # 保存原始的stdout和stderr + self.original_stdout = sys.stdout + self.original_stderr = sys.stderr + + # 创建自定义输出流 + sys.stdout = ConsoleRedirect(self, "STDOUT") + sys.stderr = ConsoleRedirect(self, "STDERR") + + def addMessage(self, message, msg_type="INFO"): + """添加消息到控制台""" + import datetime + + # 获取当前时间 + timestamp = datetime.datetime.now().strftime("%H:%M:%S") + + # 根据消息类型设置颜色 + color_map = { + "INFO": "#ffffff", # 白色 + "WARNING": "#ffaa00", # 橙色 + "ERROR": "#ff4444", # 红色 + "SUCCESS": "#44ff44", # 绿色 + "STDOUT": "#cccccc", # 浅灰色 + "STDERR": "#ff6666", # 浅红色 + } + + color = color_map.get(msg_type, "#ffffff") + + # 构建HTML格式的消息 + if self.timestampBtn.isChecked(): + html_message = f'[{timestamp}] {message}' + else: + html_message = f'{message}' + + # 添加到控制台 + self.consoleText.append(html_message) + + # 自动滚动到底部 + if self.autoScrollBtn.isChecked(): + scrollbar = self.consoleText.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) + + def clearConsole(self): + """清空控制台""" + self.consoleText.clear() + self.addMessage("控制台已清空", "INFO") + + def executeCommand(self): + """执行命令""" + command = self.commandInput.text().strip() + if not command: + return + + # 显示输入的命令 + self.addMessage(f">>> {command}", "INFO") + self.commandInput.clear() + + try: + # 执行Python命令 + if hasattr(self.world, 'executeCommand'): + result = self.world.executeCommand(command) + if result: + self.addMessage(str(result), "SUCCESS") + else: + # 简单的eval执行 + result = eval(command) + if result is not None: + self.addMessage(str(result), "SUCCESS") + + except Exception as e: + self.addMessage(f"错误: {str(e)}", "ERROR") + + def cleanup(self): + """清理资源""" + import sys + + # 恢复原始的stdout和stderr + if hasattr(self, 'original_stdout'): + sys.stdout = self.original_stdout + if hasattr(self, 'original_stderr'): + sys.stderr = self.original_stderr + + +class ConsoleRedirect: + """控制台重定向类""" + + def __init__(self, console_widget, stream_type): + self.console_widget = console_widget + self.stream_type = stream_type + + def write(self, text): + """重定向写入""" + if text.strip(): # 忽略空行 + self.console_widget.addMessage(text.strip(), self.stream_type) + + def flush(self): + """刷新缓冲区""" + pass + +class StyledMessageBox: + """统一样式的消息框辅助类""" + + MESSAGEBOX_STYLE = """ + QMessageBox { + background-color: #1e1e1f; + color: #ebebeb; + border: 1px solid rgba(77, 116, 189, 0.3); + border-radius: 8px; + padding: 20px; + min-width: 300px; + } + QMessageBox QLabel { + color: #ffffff; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 13px; + font-weight: 400; + line-height: 1.4; + padding: 10px 0px; + } + QMessageBox QPushButton { + background-color: rgba(89, 100, 113, 0.4); + color: rgba(255, 255, 255, 0.8); + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 6px; + padding: 8px 16px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 400; + font-size: 11px; + min-width: 80px; + min-height: 28px; + } + QMessageBox QPushButton:hover { + background-color: rgba(89, 100, 113, 0.6); + border: 1px solid rgba(77, 116, 189, 0.6); + color: #ffffff; + } + QMessageBox QPushButton:pressed, QMessageBox QPushButton:checked { + background-color: rgba(77, 116, 189, 0.8); + border: 1px solid #4d74bd; + color: #ffffff; + } + QMessageBox QPushButton:disabled { + background-color: rgba(89, 100, 113, 0.2); + color: rgba(235, 235, 235, 0.4); + border: 1px solid rgba(76, 92, 110, 0.2); + } + QMessageBox QPushButton:default { + background-color: rgba(77, 116, 189, 0.8); + border: 1px solid #4d74bd; + color: #ffffff; + font-weight: 500; + } + QMessageBox QPushButton:default:hover { + background-color: rgba(77, 116, 189, 1.0); + border: 1px solid rgba(77, 116, 189, 1.0); + } + QMessageBox QIcon { + padding: 0px 10px 0px 0px; + } + """ + + INPUTDIALOG_STYLE = """ + QInputDialog { + background-color: #1e1e1f; + color: #ebebeb; + border: 1px solid rgba(77, 116, 189, 0.3); + border-radius: 8px; + padding: 20px; + min-width: 350px; + } + QLabel { + color: #ffffff; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 500; + font-size: 13px; + padding: 0px 0px 10px 0px; + } + QLineEdit { + background-color: rgba(89, 100, 113, 0.15); + color: #ebebeb; + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 6px; + padding: 10px 12px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-size: 11px; + font-weight: 400; + min-height: 16px; + } + QLineEdit:focus { + border: 1px solid #4d74bd; + background-color: rgba(77, 116, 189, 0.1); + } + QLineEdit:hover { + border: 1px solid rgba(77, 116, 189, 0.6); + background-color: rgba(89, 100, 113, 0.2); + } + QPushButton { + background-color: rgba(89, 100, 113, 0.4); + color: rgba(255, 255, 255, 0.8); + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 6px; + padding: 8px 16px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 400; + font-size: 11px; + min-width: 80px; + min-height: 28px; + } + QPushButton:hover { + background-color: rgba(89, 100, 113, 0.6); + border: 1px solid rgba(77, 116, 189, 0.6); + color: #ffffff; + } + QPushButton:pressed, QPushButton:checked { + background-color: rgba(77, 116, 189, 0.8); + border: 1px solid #4d74bd; + color: #ffffff; + } + QPushButton:disabled { + background-color: rgba(89, 100, 113, 0.2); + color: rgba(235, 235, 235, 0.4); + border: 1px solid rgba(76, 92, 110, 0.2); + } + QPushButton:default { + background-color: rgba(77, 116, 189, 0.8); + border: 1px solid #4d74bd; + color: #ffffff; + font-weight: 500; + } + """ + + BUTTON_STYLE = """ + QPushButton { + background-color: rgba(89, 100, 113, 0.4); + color: rgba(255, 255, 255, 0.8); + border: 1px solid rgba(76, 92, 110, 0.4); + border-radius: 6px; + padding: 8px 16px; + font-family: 'Microsoft YaHei', 'Inter', sans-serif; + font-weight: 400; + font-size: 11px; + min-width: 80px; + min-height: 28px; + } + QPushButton:hover { + background-color: rgba(89, 100, 113, 0.6); + border: 1px solid rgba(77, 116, 189, 0.6); + color: #ffffff; + } + QPushButton:pressed, QPushButton:checked { + background-color: rgba(77, 116, 189, 0.8); + border: 1px solid #4d74bd; + color: #ffffff; + } + QPushButton:disabled { + background-color: rgba(89, 100, 113, 0.2); + color: rgba(235, 235, 235, 0.4); + border: 1px solid rgba(76, 92, 110, 0.2); + } + """ + + @staticmethod + def information(parent, title, message): + """显示信息提示框""" + msg = QMessageBox(QMessageBox.Information, title, message, QMessageBox.Ok, parent) + msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) + # 强制设置所有按钮的样式 + for button in msg.buttons(): + button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) + return msg.exec_() + + @staticmethod + def question(parent, title, message, buttons=QMessageBox.Yes | QMessageBox.No, defaultButton=QMessageBox.No): + """显示确认对话框""" + msg = QMessageBox(QMessageBox.Question, title, message, buttons, parent) + msg.setDefaultButton(defaultButton) + msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) + # 强制设置所有按钮的样式 + for button in msg.buttons(): + button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) + return msg.exec_() + + @staticmethod + def warning(parent, title, message): + """显示警告对话框""" + msg = QMessageBox(QMessageBox.Warning, title, message, QMessageBox.Ok, parent) + msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) + # 强制设置所有按钮的样式 + for button in msg.buttons(): + button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) + return msg.exec_() + + @staticmethod + def critical(parent, title, message): + """显示错误对话框""" + msg = QMessageBox(QMessageBox.Critical, title, message, QMessageBox.Ok, parent) + msg.setStyleSheet(StyledMessageBox.MESSAGEBOX_STYLE) + # 强制设置所有按钮的样式 + for button in msg.buttons(): + button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) + return msg.exec_() + + @staticmethod + def getText(parent, title, label, text=""): + """显示样式统一的文本输入对话框""" + from PyQt5.QtWidgets import QInputDialog + dialog = QInputDialog(parent) + dialog.setWindowTitle(title) + dialog.setLabelText(label) + dialog.setTextValue(text) + dialog.setStyleSheet(StyledMessageBox.INPUTDIALOG_STYLE) + + # 强制设置所有按钮的样式 + for button in dialog.findChildren(QPushButton): + button.setStyleSheet(StyledMessageBox.BUTTON_STYLE) + + ok = dialog.exec_() + return dialog.textValue(), ok + +class UniversalMessageDialog(QDialog): + """通用消息对话框类 - 支持不同图标和按钮配置""" + + # 消息类型枚举 + SUCCESS = "success_icon" + WARNING = "warning_icon" + ERROR = "fail_icon" + INFO = "info" + + def __init__(self, parent, title, message, message_type=INFO, show_cancel=True, + confirm_text="确认", cancel_text="取消", icon_size=QSize(20, 20)): + """ + 初始化通用消息对话框 + + Args: + parent: 父窗口 + title: 对话框标题 + message: 消息内容 + message_type: 消息类型 (SUCCESS, WARNING, ERROR, INFO) + show_cancel: 是否显示取消按钮 + confirm_text: 确认按钮文字 + cancel_text: 取消按钮文字 + icon_size: 图标尺寸 + """ + super().__init__(parent) + self.setWindowTitle(title) + self.setObjectName("universalMessageDialog") + self.setModal(True) + self.resize(508, 134) + self.setMinimumSize(508, 134) + self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) + self.setAttribute(Qt.WA_TranslucentBackground, True) + + # 对话框配置 + self.message_type = message_type + self.show_cancel = show_cancel + self.confirm_text = confirm_text + self.cancel_text = cancel_text + self.icon_size = icon_size + + # 拖拽相关 + self.dragging = False + self.drag_position = QPoint() + + # 图标管理 + self.icon_manager = get_icon_manager() + self._title_icon_size = QSize(18, 18) + self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size) + + # 根据消息类型获取对应图标 + self._message_icon = self._get_message_icon() + + # 设置样式 + self._setup_styles() + + # 创建UI + self._create_ui(message) + + def _get_message_icon(self): + """根据消息类型获取对应图标""" + icon_map = { + self.SUCCESS: 'success_icon', + self.WARNING: 'warning_icon', + self.ERROR: 'fail_icon', + self.INFO: 'success_icon' # 默认使用成功图标 + } + + icon_name = icon_map.get(self.message_type, 'success_icon') + return self.icon_manager.get_icon(icon_name, self.icon_size) + + def _setup_styles(self): + """设置对话框样式""" + self.setStyleSheet(""" + QDialog#universalMessageDialog { + background-color: transparent; + border: none; + } + QFrame#baseFrame { + background-color: #19191B; + border: 1px solid #3E3E42; + border-radius: 5px; + } + QWidget#titleBar { + background-color: transparent; + border: none; + border-radius: 5px 5px 0px 0px; + min-height: 32px; + max-height: 32px; + } + QWidget#titleBar QWidget { + background-color: transparent; + border: none; + } + QLabel#titleLabel { + color: #FFFFFF; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 14px; + font-weight: 500; + letter-spacing: 0.7px; + } + QWidget#controlButtons QPushButton { + background-color: transparent; + border: none; + color: #EBEBEB; + font-size: 14px; + min-width: 18px; + max-width: 18px; + min-height: 18px; + max-height: 18px; + padding: 0px; + border-radius: 3px; + } + QWidget#controlButtons QPushButton:hover { + background-color: #2A2D2E; + color: #FFFFFF; + } + QPushButton#closeButton { + border-radius: 0px 5px 0px 0px; + } + QPushButton#closeButton:hover { + background-color: #2A2D2E; + color: #FFFFFF; + } + QFrame#titleSeparator { + background-color: #3E3E42; + border: none; + min-height: 1px; + max-height: 1px; + } + QWidget#contentWidget { + background-color: transparent; + border: none; + } + QLabel#messageLabel { + color: #EBEBEB; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 13px; + font-weight: 400; + letter-spacing: 0.6px; + line-height: 1.4; + padding: 0px; + margin: 0px; + } + QPushButton { + background-color: rgba(89, 98, 118, 0.5); + color: #EBEBEB; + border: none; + border-radius: 2px; + padding: 0px 12px; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-weight: 300; + font-size: 10px; + letter-spacing: 0.5px; + min-width: 90px; + max-width: 90px; + min-height: 28px; + max-height: 28px; + } + QPushButton:hover { + background-color: #3067C0; + color: #FFFFFF; + } + QPushButton:pressed { + background-color: #2556A0; + color: #FFFFFF; + } + QPushButton#primaryButton { + background-color: rgba(89, 98, 118, 0.5); + border: none; + color: #EBEBEB; + font-weight: 300; + } + QPushButton#primaryButton:hover { + background-color: #2556A0; + } + QPushButton#primaryButton:pressed { + background-color: #1E4A8C; + } + QPushButton#secondaryButton { + background-color: rgba(89, 98, 118, 0.5); + border: none; + color: #EBEBEB; + } + QPushButton#secondaryButton:hover { + background-color: #3067C0; + color: #FFFFFF; + } + QPushButton#secondaryButton:pressed { + background-color: #2556A0; + color: #FFFFFF; + } + """) + + def _create_ui(self, message): + """构建用户界面""" + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + + base_frame = QFrame() + base_frame.setObjectName('baseFrame') + base_frame.setFrameShape(QFrame.NoFrame) + base_frame.setAttribute(Qt.WA_StyledBackground, True) + + base_layout = QVBoxLayout(base_frame) + base_layout.setContentsMargins(25, 14, 25, 14) + base_layout.setSpacing(0) + + self._create_title_bar() + base_layout.addWidget(self.title_bar) + base_layout.addSpacing(4) + + title_separator = QFrame() + title_separator.setObjectName('titleSeparator') + title_separator.setFrameShape(QFrame.HLine) + title_separator.setFrameShadow(QFrame.Plain) + base_layout.addWidget(title_separator) + base_layout.addSpacing(10) + + content_widget = QWidget() + content_widget.setObjectName('contentWidget') + content_layout = QVBoxLayout(content_widget) + content_layout.setContentsMargins(0, 0, 0, 0) + content_layout.setSpacing(10) + + message_area = QHBoxLayout() + message_area.setContentsMargins(0, 0, 0, 0) + message_area.setSpacing(10) + + # 用一个垂直布局包裹icon_label,确保顶部对齐 + icon_vbox = QVBoxLayout() + icon_vbox.setContentsMargins(0, 0, 0, 0) + icon_vbox.setSpacing(0) + icon_label = QLabel() + if self._message_icon and not self._message_icon.isNull(): + icon_label.setPixmap(self._message_icon.pixmap(self.icon_size)) + icon_label.setAlignment(Qt.AlignTop | Qt.AlignLeft) + icon_label.setFixedSize(self.icon_size) + icon_vbox.addWidget(icon_label, alignment=Qt.AlignTop) + icon_vbox.addStretch() + message_area.addLayout(icon_vbox) + + self.message_label = QLabel(message) + self.message_label.setObjectName('messageLabel') + self.message_label.setWordWrap(True) + self.message_label.setAlignment(Qt.AlignTop | Qt.AlignLeft) + self.message_label.setMinimumHeight(self.icon_size.height()) + self.message_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) + message_area.addWidget(self.message_label, 1) + message_area.addStretch() + + content_layout.addLayout(message_area) + base_layout.addWidget(content_widget) + base_layout.addSpacing(10) + + button_widget = QWidget() + button_layout = QHBoxLayout(button_widget) + button_layout.setContentsMargins(0, 0, 0, 0) + button_layout.setSpacing(10) + button_layout.addStretch() + + if self.show_cancel: + self.cancel_button = QPushButton(self.cancel_text) + self.cancel_button.setObjectName("secondaryButton") + self.cancel_button.clicked.connect(self.reject) + self.cancel_button.setFixedSize(90, 28) + button_layout.addWidget(self.cancel_button) + + self.confirm_button = QPushButton(self.confirm_text) + self.confirm_button.setObjectName("primaryButton") + self.confirm_button.clicked.connect(self.accept) + self.confirm_button.setFixedSize(90, 28) + button_layout.addWidget(self.confirm_button) + + self.confirm_button.setDefault(True) + self.confirm_button.setAutoDefault(True) + if self.show_cancel: + self.cancel_button.setAutoDefault(False) + + base_layout.addWidget(button_widget) + + main_layout.addWidget(base_frame) + + def _create_title_bar(self): + """创建自定义标题栏""" + self.title_bar = QFrame() + self.title_bar.setObjectName("titleBar") + + title_layout = QHBoxLayout(self.title_bar) + title_layout.setContentsMargins(0, 0, 0, 0) + title_layout.setSpacing(0) + + self.title_label = QLabel(self.windowTitle()) + self.title_label.setObjectName("titleLabel") + self.title_label.setAlignment(Qt.AlignVCenter | Qt.AlignLeft) + title_layout.addWidget(self.title_label, 1) + + title_layout.addStretch() + + controls = QWidget() + controls.setObjectName("controlButtons") + controls_layout = QHBoxLayout(controls) + controls_layout.setContentsMargins(0, 0, 0, 0) + controls_layout.setSpacing(0) + + self.close_button = QPushButton() + self.close_button.setObjectName("closeButton") + self.close_button.clicked.connect(self.reject) + self.close_button.setFocusPolicy(Qt.NoFocus) + controls_layout.addWidget(self.close_button) + + self._apply_title_bar_icons() + + title_layout.addWidget(controls) + + + def _apply_title_bar_icons(self): + """应用标题栏图标""" + if self._icon_close: + self.close_button.setIcon(self._icon_close) + self.close_button.setIconSize(self._title_icon_size) + self.close_button.setText("") + self.close_button.setToolTip("关闭") + + def setWindowTitle(self, title): + """设置窗口标题""" + super().setWindowTitle(title) + if hasattr(self, "title_label"): + self.title_label.setText(title) + + def mousePressEvent(self, event): + """鼠标按下事件 - 用于拖拽窗口""" + if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): + self.dragging = True + self.drag_position = event.globalPos() - self.frameGeometry().topLeft() + event.accept() + super().mousePressEvent(event) + + def mouseMoveEvent(self, event): + """鼠标移动事件 - 用于拖拽窗口""" + if event.buttons() == Qt.LeftButton and self.dragging: + self.move(event.globalPos() - self.drag_position) + event.accept() + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + """鼠标释放事件 - 停止拖拽""" + if event.button() == Qt.LeftButton: + self.dragging = False + super().mouseReleaseEvent(event) + + @staticmethod + def show_success(parent, title, message, show_cancel=False, confirm_text="确认"): + """显示成功消息对话框""" + dialog = UniversalMessageDialog( + parent, title, message, + UniversalMessageDialog.SUCCESS, + show_cancel, confirm_text + ) + return dialog.exec_() + + @staticmethod + def show_warning(parent, title, message, show_cancel=True, confirm_text="确认", cancel_text="取消"): + """显示警告消息对话框""" + dialog = UniversalMessageDialog( + parent, title, message, + UniversalMessageDialog.WARNING, + show_cancel, confirm_text, cancel_text + ) + return dialog.exec_() + + @staticmethod + def show_error(parent, title, message, show_cancel=False, confirm_text="确认"): + """显示错误消息对话框""" + dialog = UniversalMessageDialog( + parent, title, message, + UniversalMessageDialog.ERROR, + show_cancel, confirm_text + ) + return dialog.exec_() + + @staticmethod + def show_info(parent, title, message, show_cancel=True, confirm_text="确认", cancel_text="取消"): + """显示信息消息对话框""" + dialog = UniversalMessageDialog( + parent, title, message, + UniversalMessageDialog.INFO, + show_cancel, confirm_text, cancel_text + ) + return dialog.exec_() + + +class StyledTextInputDialog(QDialog): + """与新建项目样式一致的文本输入对话框""" + + def __init__(self, parent, title, label_text="", placeholder="", default_text=""): + super().__init__(parent) + self.setWindowTitle(title) + self.setObjectName("styledTextInputDialog") + self.setModal(True) + self.resize(420, 186) + self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) + self.setAttribute(Qt.WA_TranslucentBackground, True) + + self.dragging = False + self.drag_position = QPoint() + self.icon_manager = get_icon_manager() + self._title_icon_size = QSize(18, 18) + self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size) + + self.setStyleSheet(""" + QDialog#styledTextInputDialog { + background-color: transparent; + border: none; + } + QFrame#baseFrame { + background-color: #000000; + border: 1px solid #3E3E42; + border-radius: 5px; + } + QWidget#titleBar { + background-color: transparent; + border: none; + border-radius: 5px 5px 0px 0px; + min-height: 32px; + max-height: 32px; + } + QWidget#titleBar QWidget { + background-color: transparent; + border: none; + } + QLabel#titleLabel { + color: #FFFFFF; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 14px; + font-weight: 500; + letter-spacing: 0.7px; + } + QWidget#controlButtons QPushButton { + background-color: transparent; + border: none; + color: #EBEBEB; + font-size: 14px; + min-width: 18px; + max-width: 18px; + min-height: 18px; + max-height: 18px; + padding: 0px; + border-radius: 3px; + } + QWidget#controlButtons QPushButton:hover { + background-color: #2A2D2E; + color: #FFFFFF; + } + QPushButton#closeButton { + border-radius: 0px 5px 0px 0px; + } + QPushButton#closeButton:hover { + background-color: #2A2D2E; + color: #FFFFFF; + } + QWidget#contentWidget { + background-color: transparent; + border-radius: 0px 0px 5px 5px; + } + QFrame#contentContainer { + background-color: #19191B; + border: 1px solid #2C2F36; + border-radius: 5px; + } + QLabel[role="fieldLabel"] { + color: #EBEBEB; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 12px; + font-weight: 400; + letter-spacing: 0.6px; + } + QLineEdit { + background-color: rgba(89, 100, 113, 0.2); + color: #EBEBEB; + border: 1px solid rgba(76, 92, 110, 0.6); + border-radius: 2px; + padding: 0px 10px; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-size: 11px; + font-weight: 300; + letter-spacing: 0.55px; + min-height: 30px; + max-height: 30px; + } + QLineEdit:focus { + border: 1px solid #3067C0; + background-color: rgba(48, 103, 192, 0.1); + } + QLineEdit:hover { + border: 1px solid #3067C0; + background-color: rgba(89, 100, 113, 0.3); + } + QPushButton { + background-color: rgba(89, 98, 118, 0.5); + color: #EBEBEB; + border: none; + border-radius: 2px; + padding: 0px 12px; + font-family: 'Inter', 'Microsoft YaHei', sans-serif; + font-weight: 300; + font-size: 10px; + letter-spacing: 0.5px; + min-width: 120px; + min-height: 30px; + max-height: 30px; + } + QPushButton:hover { + background-color: #3067C0; + color: #FFFFFF; + } + QPushButton:pressed { + background-color: #2556A0; + color: #FFFFFF; + } + """) + + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + + base_frame = QFrame() + base_frame.setObjectName('baseFrame') + base_frame.setFrameShape(QFrame.NoFrame) + base_frame.setAttribute(Qt.WA_StyledBackground, True) + + base_layout = QVBoxLayout(base_frame) + base_layout.setContentsMargins(0, 0, 0, 0) + base_layout.setSpacing(0) + + self._createTitleBar() + base_layout.addWidget(self.title_bar) + base_layout.addSpacing(10) + + content_widget = QWidget() + content_widget.setObjectName('contentWidget') + content_layout = QVBoxLayout(content_widget) + content_layout.setContentsMargins(10, 0, 10, 10) + content_layout.setSpacing(0) + + content_container = QFrame() + content_container.setObjectName('contentContainer') + content_container.setFrameShape(QFrame.NoFrame) + content_container.setAttribute(Qt.WA_StyledBackground, True) + content_container.setFixedWidth(400) + + container_layout = QVBoxLayout(content_container) + container_layout.setContentsMargins(15, 10, 15, 10) + container_layout.setSpacing(10) + + if label_text: + label = QLabel(label_text) + label.setProperty('role', 'fieldLabel') + container_layout.addWidget(label) + + self.line_edit = QLineEdit() + self.line_edit.setPlaceholderText(placeholder) + self.line_edit.setText(default_text) + container_layout.addWidget(self.line_edit) + + separator = QFrame() + separator.setFrameShape(QFrame.HLine) + separator.setFrameShadow(QFrame.Plain) + separator.setFixedHeight(1) + separator.setStyleSheet("background-color: #2C2F36; border: none;") + container_layout.addWidget(separator) + + button_row = QHBoxLayout() + button_row.setContentsMargins(0, 0, 0, 0) + button_row.setSpacing(10) + button_row.addStretch() + + self.confirmButton = QPushButton("确认") + self.confirmButton.setObjectName("primaryButton") + self.confirmButton.clicked.connect(self._onAccept) + button_row.addWidget(self.confirmButton) + + self.cancelButton = QPushButton("取消") + self.cancelButton.setObjectName("secondaryButton") + self.cancelButton.clicked.connect(self.reject) + button_row.addWidget(self.cancelButton) + + container_layout.addLayout(button_row) + + content_layout.addWidget(content_container, 0, Qt.AlignTop) + base_layout.addWidget(content_widget) + main_layout.addWidget(base_frame) + + self.confirmButton.setDefault(True) + self.confirmButton.setAutoDefault(True) + self.line_edit.selectAll() + self.line_edit.setFocus() + + def _createTitleBar(self): + self.title_bar = QFrame() + self.title_bar.setObjectName("titleBar") + + title_layout = QHBoxLayout(self.title_bar) + title_layout.setContentsMargins(12, 6, 12, 6) + title_layout.setSpacing(0) + + controls = QWidget() + controls.setObjectName("controlButtons") + controls_layout = QHBoxLayout(controls) + controls_layout.setContentsMargins(0, 0, 0, 0) + controls_layout.setSpacing(0) + + self.close_button = QPushButton() + self.close_button.setObjectName("closeButton") + self.close_button.clicked.connect(self.reject) + self.close_button.setFocusPolicy(Qt.NoFocus) + self.close_button.setFixedSize(18, 18) + controls_layout.addWidget(self.close_button) + + self._applyTitleBarIcons() + + left_placeholder = QWidget() + left_placeholder.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) + title_layout.addWidget(left_placeholder) + + self.title_label = QLabel(self.windowTitle()) + self.title_label.setObjectName("titleLabel") + self.title_label.setAlignment(Qt.AlignCenter) + title_layout.addWidget(self.title_label, 1) + + title_layout.addWidget(controls) + left_placeholder.setFixedWidth(controls.sizeHint().width()) + + def _applyTitleBarIcons(self): + if self._icon_close: + self.close_button.setIcon(self._icon_close) + self.close_button.setIconSize(self._title_icon_size) + self.close_button.setText("") + self.close_button.setToolTip("关闭") + + def setWindowTitle(self, title): + super().setWindowTitle(title) + if hasattr(self, "title_label"): + self.title_label.setText(title) + + def _onAccept(self): + if self.line_edit.text().strip(): + self.accept() + + def text(self): + return self.line_edit.text().strip() + + def mousePressEvent(self, event): + if event.button() == Qt.LeftButton and self.title_bar.geometry().contains(event.pos()): + self.dragging = True + self.drag_position = event.globalPos() - self.frameGeometry().topLeft() + event.accept() + super().mousePressEvent(event) + + def mouseMoveEvent(self, event): + if event.buttons() == Qt.LeftButton and self.dragging: + self.move(event.globalPos() - self.drag_position) + event.accept() + super().mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + if event.button() == Qt.LeftButton: + self.dragging = False + super().mouseReleaseEvent(event) + + +class CustomTreeWidget(QTreeWidget): + """自定义场景树部件""" + + def __init__(self, world, parent=None): + if parent is None: + parent = wrapinstance(0, QWidget) + super().__init__(parent) + self.world = world + # self.selectedItems = None + self.initData() + self.setupUI() # 初始化界面 + self.setupContextMenu() # 初始化右键菜单 + self.setupDragDrop() # 设置拖拽功能 + self.original_scales={} + + def initData(self): + """初始化变量""" + # 定义2D GUI元素类型 + self.gui_2d_types = { + "GUI_BUTTON", # GUI 按钮 + "GUI_LABEL", # GUI 标签 + "GUI_ENTRY", # GUI 输入框 + "GUI_IMAGE", # GUI 图片 + "GUI_2D_VIDEO_SCREEN", # GUI 2D视频 + "GUI_SPHERICAL_VIDEO", # GUI 3D球形视频 + "GUI_NODE" # 其他2D GUI容器 + } + + # 定义3D GUI元素类型 + self.gui_3d_types = { + "GUI_3DTEXT", # 3D 文本节点 + "GUI_3DIMAGE", # 3D 图片节点 + "GUI_VIRTUAL_SCREEN", # 3D视频 + "GUI_VirtualScreen" # 3D虚拟视频 + } + + # 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素) + self.scene_3d_types = { + "SCENE_ROOT", + "SCENE_NODE", + "LIGHT_NODE", # 灯节点 + "CAMERA_NODE", + "IMPORTED_MODEL_NODE", # 导入模型节点 + "MODEL_NODE", + "TERRAIN_NODE", # 地形节点 + "CESIUM_TILESET_NODE" # 3D Tileset + } + + # 这是一个最佳实践,它让代码的意图变得非常清晰。 + self.valid_3d_parent_types = self.scene_3d_types.union(self.gui_3d_types) + + def setupUI(self): + """初始化UI设置""" + self.setHeaderHidden(True) + # 启用多选和拖拽 + self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) + self.setDropIndicatorShown(True) # 启用拖放指示线 + + def setupDragDrop(self): + """设置拖拽功能""" + # 使用自定义拖拽模式 + self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) # 或者使用 DragDrop + self.setDefaultDropAction(Qt.DropAction.MoveAction) + self.setDragEnabled(True) + self.setAcceptDrops(True) + + def setupContextMenu(self): + """设置右键菜单""" + self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.customContextMenuRequested.connect(self.showContextMenu) + + def showContextMenu(self, position): + """显示右键菜单 - 复用主菜单的创建动作""" + if self.selectedItems(): + item = self.selectedItems()[0] + print(f"为项目 '{item.text(0)}' 显示右键菜单") + + # 创建右键菜单 + menu = QMenu(self) + + # 获取主窗口的创建菜单动作 - 关键修改 + if hasattr(self.world, 'main_window'): + main_window = self.world.main_window + create_actions = main_window.getCreateMenuActions() + + # 创建子菜单 - 复用主菜单结构 + createMenu = menu.addMenu('创建') + + # 基础对象 + createMenu.addAction(create_actions['createEmpty']) + + # 3D对象菜单 + create3dObjectMenu = createMenu.addMenu('3D对象') + + # 3D GUI菜单 + create3dGUIMenu = createMenu.addMenu('3D GUI') + create3dGUIMenu.addAction(create_actions['create3DText']) + create3dGUIMenu.addAction(create_actions['create3DImage']) + + # GUI菜单 + createGUIMenu = createMenu.addMenu('GUI') + createGUIMenu.addAction(create_actions['createButton']) + createGUIMenu.addAction(create_actions['createLabel']) + createGUIMenu.addAction(create_actions['createEntry']) + createGUIMenu.addAction(create_actions['createImage']) + createGUIMenu.addSeparator() + createGUIMenu.addAction(create_actions['createVideoScreen']) + createGUIMenu.addAction(create_actions['createSphericalVideo']) + createGUIMenu.addSeparator() + createGUIMenu.addAction(create_actions['createVirtualScreen']) + + # 光源菜单 + createLightMenu = createMenu.addMenu('光源') + createLightMenu.addAction(create_actions['createSpotLight']) + createLightMenu.addAction(create_actions['createPointLight']) + + else: + # 备用方案:如果无法获取主窗口动作,显示提示 + createMenu = menu.addMenu('创建') + noActionItem = createMenu.addAction('功能不可用') + noActionItem.setEnabled(False) + + # 添加删除选项 + menu.addSeparator() + deleteAction = menu.addAction('删除') + if hasattr(self, 'delete_items'): + deleteAction.triggered.connect(lambda: self.delete_items(self.selectedItems())) + + # 显示菜单 + global_pos = self.mapToGlobal(position) + action = menu.exec_(global_pos) + + print(f"右键菜单已显示在位置: {global_pos}") + if action: + print(f"用户选择了动作: {action.text()}") + else: + print("用户取消了菜单选择") + + # 在 CustomTreeWidget 类的 dropEvent 方法中替换缩放处理部分 + def dropEvent(self, event): + # 1. 获取所有被拖拽的项 + dragged_items = self.selectedItems() + if not dragged_items: + event.ignore() + return + + # 2. 在执行Qt的默认拖拽前,记录所有拖拽项的原始状态 + drag_info = [] + for item in dragged_items: + panda_node = item.data(0, Qt.UserRole) + if not panda_node or panda_node.is_empty(): + continue # 跳过无效节点 + + drag_info.append({ + "item": item, + "node": panda_node, + "old_parent_node": item.parent().data(0, Qt.UserRole) if item.parent() else None + }) + + # 3. 执行Qt的默认拖拽,让UI树先行更新 + # 这一步会自动处理移动或复制,并将项目从旧父节点移除,添加到新父节点 + super().dropEvent(event) + + # 4. 遍历记录下的信息,同步每一个Panda3D节点的状态 + try: + for info in drag_info: + dragged_item = info["item"] + dragged_node = info["node"] + old_parent_node = info["old_parent_node"] + + # 获取拖拽后的新父节点 + new_parent_item = dragged_item.parent() + new_parent_node = new_parent_item.data(0, Qt.UserRole) if new_parent_item else None + + # 仅当父节点实际发生变化时才执行重新父化 + if old_parent_node != new_parent_node: + print(f"跨层级拖拽:从 {old_parent_node} 移动到 {new_parent_node}") + + # # 保存世界坐标位置 + # world_pos = dragged_node.getPos(self.world.render) + # world_hpr = dragged_node.getHpr(self.world.render) + # world_scale = dragged_node.getScale(self.world.render) + + # 检查是否是2D GUI元素 + dragged_type = dragged_item.data(0, Qt.UserRole + 1) + is_2d_gui = dragged_type in self.gui_2d_types + + # 重新父化到新的父节点 + if new_parent_node: + if is_2d_gui: + # 2D GUI元素需要特殊处理 + if hasattr(new_parent_node, 'getTag') and new_parent_node.getTag("is_gui_element") == "1": + # 目标是GUI元素,直接重新父化 + dragged_node.wrtReparentTo(new_parent_node) + else: + # 目标是3D节点,保持GUI特性,重新父化到aspect2d + # from direct.showbase.ShowBase import aspect2d + dragged_node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素 {dragged_item.text(0)} 保持在aspect2d下") + else: + # 非GUI元素正常重新父化 + dragged_node.wrtReparentTo(new_parent_node) + else: + # 如果新父节点为None,根据元素类型决定父节点 + if is_2d_gui: + # from direct.showbase.ShowBase import aspect2d + dragged_node.wrtReparentTo(self.world.aspect2d) + print(f"2D GUI元素 {dragged_item.text(0)} 重新父化到aspect2d") + else: + dragged_node.wrtReparentTo(self.world.render) + + # # 恢复世界坐标位置(对于2D GUI可能需要调整) + # if is_2d_gui: + # # 2D GUI元素使用屏幕坐标系,可能需要特殊处理 + # dragged_node.setPos(world_pos) + # dragged_node.setHpr(world_hpr) + # dragged_node.setScale(world_scale) + # else: + # dragged_node.setPos(self.world.render, world_pos) + # dragged_node.setHpr(self.world.render, world_hpr) + # dragged_node.setScale(self.world.render, world_scale) + + print(f"✅ Panda3D父子关系已更新") + else: + print(f"同层级移动:父节点未变化,跳过Panda3D重新父化") + + except Exception as e: + print(f"⚠️ 同步Panda3D场景图失败: {e}") + # 不影响Qt树的更新,继续执行 + + # 事后验证:确保节点仍在"场景"根节点下 + self._ensureUnderSceneRoot(dragged_item) + self.world.property_panel._syncEffectiveVisibility(dragged_node) + event.accept() + + def _ensureUnderSceneRoot(self, item): + """确保节点在场景根节点下,如果不是则自动修正""" + if not item: + return + + # 检查是否成为了顶级节点 + if not item.parent(): + # 通过数据标识判断是否是场景根节点 + scene_root_marker = item.data(0, Qt.UserRole + 1) + if scene_root_marker != "SCENE_ROOT": + print(f"⚠️ 检测到节点 {item.text(0)} 意外成为顶级节点,正在修正...") + + # 找到场景根节点 + scene_root = None + for i in range(self.topLevelItemCount()): + top_item = self.topLevelItem(i) + if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": + scene_root = top_item + break + + if scene_root: + # 将节点移回场景根节点下 + self.takeTopLevelItem(self.indexOfTopLevelItem(item)) + scene_root.addChild(item) + print(f"✅ 已将节点 {item.text(0)} 移回场景根节点下") + + def isValidParentChild(self, dragged_item, target_item): + """检查是否是有效的父子关系(防止循环)+ GUI类型验证""" + + # 1. 禁止拖放到自身 + if dragged_item == target_item: + return False + + # 2. 禁止拖到根节点之外(根节点本身除外) + target_root = self._getRootNode(target_item) + if not target_root or target_root.data(0, Qt.UserRole + 1) != "SCENE_ROOT": + print(f"❌ 目标节点 {target_item.text(0)} 不在场景下") + return False + + # 3. 禁止拖拽场景根节点 + dragged_root = self._getRootNode(dragged_item) + if (dragged_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT" or + not dragged_root or dragged_root.data(0, Qt.UserRole + 1) != "SCENE_ROOT"): + print(f"❌ 禁止拖拽场景根节点或根节点外的节点") + return False + + # 4. Qt 树循环检查 + current = target_item + while current: + if current == dragged_item: + print(f"❌ Qt 树检测:{target_item.text(0)} 是 {dragged_item.text(0)} 的后代") + return False + current = current.parent() + + # 5. GUI元素类型验证 - 新增功能 + if not self._validateGUITypeCompatibility(dragged_item, target_item): + return False + + return True + + def _validateGUITypeCompatibility(self, dragged_item, target_item): + """验证GUI元素类型兼容性""" + try: + # 获取节点类型标识 + dragged_type = dragged_item.data(0, Qt.UserRole + 1) + target_type = target_item.data(0, Qt.UserRole + 1) + + # 定义2D GUI元素类型 + gui_2d_types = self.gui_2d_types + + # 定义3D GUI元素类型 + gui_3d_types = self.gui_3d_types + + # 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素) + scene_3d_types = self.scene_3d_types + + # 检查拖拽元素的类型 + is_dragged_2d_gui = dragged_type in gui_2d_types + is_dragged_3d_gui = dragged_type in gui_3d_types + is_dragged_3d_scene = dragged_type in scene_3d_types + + # 检查目标的类型 + is_target_2d_gui = target_type in gui_2d_types + is_target_3d_gui = target_type in gui_3d_types + is_target_3d_scene = target_type in scene_3d_types + + # === 严格的类型隔离验证逻辑 === + + # 1. 2D GUI元素的拖拽限制 - 只能拖拽到其他2D GUI元素下 + if is_dragged_2d_gui: + if target_type == "SCENE_ROOT": + return True + elif is_target_2d_gui: + print(f"✅ 2D GUI元素 {dragged_item.text(0)} 可以拖拽到2D GUI父节点 {target_item.text(0)}") + return True + elif is_target_3d_gui: + print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到3D GUI元素 {target_item.text(0)} 下") + print(" 💡 提示: 2D GUI元素只能作为其他2D GUI元素的子节点") + return False + elif is_target_3d_scene: + print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到3D场景节点 {target_item.text(0)} 下") + print(" 💡 提示: 2D GUI元素应该保持在2D GUI层级结构中") + return False + else: + print(f"❌ 2D GUI元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") + return False + + # 2. 3D GUI元素的拖拽限制 - 只能拖拽到3D场景节点下 + elif is_dragged_3d_gui: + if is_target_3d_scene: + print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}") + return False + elif is_target_2d_gui: + print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下") + print(" 💡 提示: 3D GUI元素不能与2D GUI元素建立父子关系") + return False + elif is_target_3d_gui: + print(f"✅ 3D GUI元素 {dragged_item.text(0)} 可以拖拽到其他3D GUI元素 {target_item.text(0)} 下") + print(" 💡 提示: 允许3D GUI元素之间建立父子关系") + return True + else: + print(f"❌ 3D GUI元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") + return False + + # 3. 3D场景元素的拖拽限制 - 只能拖拽到3D场景节点或3D GUI元素下 + elif is_dragged_3d_scene: + if is_target_3d_scene: + print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D场景节点 {target_item.text(0)}") + return True + elif is_target_2d_gui: + print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下") + print(" 💡 提示: 3D场景元素不能与2D GUI元素建立父子关系") + print(" 💡 建议: 将3D场景元素拖拽到其他3D场景节点或场景根节点下") + return False + elif is_target_3d_gui: + print(f"✅ 3D场景元素 {dragged_item.text(0)} 可以拖拽到3D GUI元素 {target_item.text(0)} 下") + print(" 💡 提示: 允许3D场景元素挂载在3D GUI元素下") + return False + else: + print(f"❌ 3D场景元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") + return False + + # 4. 其他未分类元素 - 严格禁止拖拽到2D GUI下 + else: + if is_target_2d_gui: + print(f"❌ 元素 {dragged_item.text(0)} 不能拖拽到2D GUI元素 {target_item.text(0)} 下") + print(" 💡 提示: 只有2D GUI元素才能作为其他2D GUI元素的子节点") + return False + elif is_target_3d_gui or is_target_3d_scene: + print(f"✅ 允许元素 {dragged_item.text(0)} 拖拽到3D节点 {target_item.text(0)}") + return True + else: + print(f"❌ 元素 {dragged_item.text(0)} 不能拖拽到未知类型节点 {target_item.text(0)} 下") + return False + + # 默认情况(理论上不应该到达这里) + print(f"⚠️ GUI类型验证遇到未处理的情况: {dragged_type} -> {target_type}") + return False + + except Exception as e: + print(f"❌ GUI类型兼容性验证失败: {str(e)}") + # 出错时采用保守策略,禁止拖拽 + return False + + def _getRootNode(self, item): + """获取树中节点的根节点项""" + if not item: + return None + + current = item + while current.parent(): + current = current.parent() + return current + + def dragEnterEvent(self, event): + """处理拖入事件""" + if event.source() == self: + event.accept() + else: + event.ignore() + + def dragMoveEvent(self, event): + """处理拖动事件""" + indicator_pos = self.dropIndicatorPosition() + indicator_str = "Unknown" + + if indicator_pos == QAbstractItemView.DropIndicatorPosition.OnItem: + indicator_str = "OnItem" + elif indicator_pos == QAbstractItemView.DropIndicatorPosition.AboveItem: + indicator_str = "AboveItem" + elif indicator_pos == QAbstractItemView.DropIndicatorPosition.BelowItem: + indicator_str = "BelowItem" + elif indicator_pos == QAbstractItemView.DropIndicatorPosition.OnViewport: + indicator_str = "OnViewport" + + #print(f'indicator pos: {indicator_str} (value: {int(indicator_pos)})') + + if event.source() != self: + event.ignore() + return + + # 获取当前拖拽的项目和目标位置 + target_item = self.itemAt(event.pos()) + selected_items = self.selectedItems() + + # 检查是否拖拽到多选区域内的项目 + if target_item and target_item in selected_items: + event.ignore() + return + + # 检查其他禁止条件 + if target_item and selected_items: + for dragged_item in selected_items: + if not self.isValidParentChild(dragged_item, target_item): + event.ignore() + return + + super().dragMoveEvent(event) + event.accept() + + def keyPressEvent(self, event): + """处理键盘按键事件""" + if event.key() == Qt.Key_Delete: + # currentItem = self.currentItem() + # if currentItem and currentItem.parent(): + # # 检查是否是模型节点或其子节点 + # if self.world.interface_manager.isModelOrChild(currentItem): + # nodePath = currentItem.data(0, Qt.UserRole) + # if nodePath: + # print("正在删除节点...") + # self.world.interface_manager.deleteNode(nodePath, currentItem) + # print("删除完成") + selected_items = self.selectedItems() + if selected_items: + # 执行删除操作 + # if selected_items.data(0, Qt.UserRole + 1) == "LIGHT_NODE": + # self._preprocess_light_items_for_deletion(selected_items) + self.delete_items(selected_items) + else: + # 没有选中任何项目,执行默认操作 + super().keyPressEvent(event) + else: + super().keyPressEvent(event) + + def _preprocess_light_items_for_deletion(self, selected_items): + """预处理灯光节点删除,特别处理最后一个灯光节点的问题""" + if not selected_items: + return selected_items + + # 检查选中的项目中是否包含灯光节点 + light_items = [] + for item in selected_items: + node_type = item.data(0, Qt.UserRole + 1) + if node_type == "LIGHT_NODE": + light_items.append(item) + + # 如果没有灯光节点,直接返回 + if not light_items: + return selected_items + + # 检查是否只有最后一个灯光节点被选中 + processed_items = list(selected_items) # 创建副本 + + for item in light_items: + panda_node = item.data(0, Qt.UserRole) + if not panda_node: + continue + + # 获取灯光类型 + if hasattr(panda_node, 'getTag'): + light_type = panda_node.getTag("light_type") + + # 检查是否是最后一个Spotlight + if (light_type == "spot_light" and hasattr(self.world, 'Spotlight') and + self.world.Spotlight and self.world.Spotlight[-1] == panda_node and + len(self.world.Spotlight) > 1): + + print(f"⚠️ 检测到选中最后一个Spotlight节点: {item.text(0)}") + # 这里可以添加特殊处理逻辑,比如提示用户或阻止删除 + + # 检查是否是最后一个Pointlight + elif (light_type == "point_light" and hasattr(self.world, 'Pointlight') and + self.world.Pointlight and self.world.Pointlight[-1] == panda_node and + len(self.world.Pointlight) > 1): + + print(f"⚠️ 检测到选中最后一个Pointlight节点: {item.text(0)}") + # 这里可以添加特殊处理逻辑,比如提示用户或阻止删除 + + return processed_items + + def delete_items(self, selected_items): + """删除选中的item - 简化版本""" + if not selected_items: + return + + # 1. 过滤掉不能删除的节点 + deletable_items = [] + for item in selected_items: + node_type = item.data(0, Qt.UserRole + 1) + panda_node = item.data(0, Qt.UserRole) + + # 跳过场景根节点和主相机 + if (node_type == "SCENE_ROOT" or + (panda_node and hasattr(self.world, 'cam') and panda_node == self.world.cam)): + continue + deletable_items.append(item) + + if not deletable_items: + StyledMessageBox.information(self, "提示", "没有可删除的节点") + return + + # 2. 确认删除 + item_count = len(deletable_items) + if item_count == 1: + message = f"确定要删除节点 \"{deletable_items[0].text(0)}\" 吗?" + else: + message = f"确定要删除 {item_count} 个节点吗?" + + dialog = UniversalMessageDialog( + self, + "确认删除", + message, + message_type=UniversalMessageDialog.WARNING, + show_cancel=True, + confirm_text="确认", + cancel_text="取消" + ) + + if dialog.exec_() != QDialog.Accepted: + return + + # 默认选中场景根节点,通常是第一个顶级节点 + #next_item_to_select = self.topLevelItem(0) + + # 3. 执行删除循环 + deleted_count = 0 + for item in deletable_items: + try: + # 在删除前,记录其父节点,作为删除后的新选择 + # 选择最后一个被删除项的父节点作为新的焦点 + if item.parent(): + next_item_to_select = item.parent() + panda_node = item.data(0, Qt.UserRole) + + if panda_node: + # 清理选择状态 + if (hasattr(self.world, 'selection') and + hasattr(self.world.selection, 'selectedNode') and + self.world.selection.selectedNode == panda_node): + self.world.selection.updateSelection(None) + + # 清理特殊资源(参考interface_manager.py的逻辑) + if hasattr(self.world, 'property_panel'): + self.world.property_panel.removeActorForModel(panda_node) + + # 清理灯光 + if hasattr(panda_node, 'getPythonTag'): + light_object = panda_node.getPythonTag('rp_light_object') + if light_object and hasattr(self.world, 'render_pipeline'): + self.world.render_pipeline.remove_light(light_object) + + # 从world列表中移除 + if hasattr(self.world, 'gui_elements') and panda_node in self.world.gui_elements: + self.world.gui_elements.remove(panda_node) + if hasattr(self.world, 'models') and panda_node in self.world.models: + self.world.models.remove(panda_node) + if hasattr(self.world, 'Spotlight') and panda_node in self.world.Spotlight: + self.world.Spotlight.remove(panda_node) + if hasattr(self.world, 'Pointlight') and panda_node in self.world.Pointlight: + self.world.Pointlight.remove(panda_node) + if hasattr(self.world, 'terrains') and panda_node in self.world.terrains: + self.world.terrains.remove(panda_node) + if hasattr(self.world, 'tilesets') and panda_node in self.world.tilesets: + # self.world.tilesets.remove(panda_node) + # 从 tilesets 列表中移除 + if hasattr(self.world, 'scene_manager'): + tilesets_to_remove = [] + for i, tileset_info in enumerate(self.world.scene_manager.tilesets): + if tileset_info['node'] == panda_node: + tilesets_to_remove.append(i) + + # 从后往前删除,避免索引问题 + for i in reversed(tilesets_to_remove): + del self.world.scene_manager.tilesets[i] + + # 从Panda3D场景中移除 + try: + if not panda_node.isEmpty(): + panda_node.removeNode() + except Exception as e: + print(f"❌ 删除节点 {item.text(0)} 失败: {str(e)}") + + # 从Qt树中移除 + parent_item = item.parent() + if parent_item: + parent_item.removeChild(item) + else: + index = self.indexOfTopLevelItem(item) + if index >= 0: + self.takeTopLevelItem(index) + + deleted_count += 1 + print(f"✅ 删除节点: {item.text(0)}") + + except Exception as e: + print(f"❌ 删除节点 {item.text(0)} 失败: {str(e)}") + import traceback + traceback.print_exc() + + # 最终清理 + # if hasattr(self.world, 'property_panel'): + # self.world.property_panel.clearPropertyPanel() + + # 4. 删除操作完成后,更新UI --- + if deleted_count > 0: + print(f"🎉 成功删除 {deleted_count} 个节点。正在更新UI...") + self.update_selection_and_properties(None, None) + + def delete_item(self, panda_node): + """删除指定节点 panda3D(node)- 优化和修复版本""" + if not panda_node or panda_node.is_empty(): + print("ℹ️ 尝试删除一个空的或无效的节点,操作取消。") + return + + # #如果有命令管理系统,则使用命令系统 + # if hasattr(self.world,'command_manager') and self.world.command_manager: + # from core.Command_System import DeleteNodeCommand + # parent_node = panda_node.getParent() + # command = DeleteNodeCommand(panda_node,parent_node) + # self.world.command_manager.execute_command(command) + # return + + # --- 关键修复:在操作前,安全地获取节点名字 --- + node_name_for_logging = panda_node.getName() + + # 1. 寻找对应的Qt Item + item = self.world.interface_manager.findTreeItem(panda_node, self._findSceneRoot()) + + # 场景清理(无论是否找到item,都应该执行) + self._cleanup_panda_node_resources(panda_node) + panda_node.removeNode() + + # 如果没有找到item,说明UI已经移除或不同步,清理完Panda3D资源后即可退出 + if not item: + print(f"✅ Panda3D节点 '{node_name_for_logging}' 已清理并移除。UI树中未找到对应项。") + return + try: + # 2. 过滤受保护节点 + node_type = item.data(0, Qt.UserRole + 1) + if node_type == "SCENE_ROOT": # 相机检查已包含在panda_node判空中 + print(f"ℹ️ 节点 {item.text(0)} 是受保护节点,无法删除。") + return + + # 3. 从UI树中移除 + parent_for_next_selection = item.parent() + if item.parent(): + item.parent().removeChild(item) + else: + index = self.indexOfTopLevelItem(item) + if index >= 0: + self.takeTopLevelItem(index) + + print(f"✅ 成功删除节点: {node_name_for_logging}") + + # 4. 更新UI + print(f"🔄 正在更新UI...") + if parent_for_next_selection and self.indexFromItem(parent_for_next_selection).isValid(): + new_selection_item = parent_for_next_selection + else: + new_selection_item = self.topLevelItem(0) + + if new_selection_item: + self.setCurrentItem(new_selection_item) + new_panda_node_to_select = new_selection_item.data(0, Qt.UserRole) + self.update_selection_and_properties(new_panda_node_to_select, new_selection_item) + else: + self.update_selection_and_properties(None, None) + + except Exception as e: + print(f"❌ 删除节点 {node_name_for_logging} 时发生意外错误: {str(e)}") + import traceback + traceback.print_exc() + + def clear_tree(self): + """清空UI树""" + print("Clear") + self.clear() + # 创建场景根节点 + sceneRoot = QTreeWidgetItem(self, ['场景']) + sceneRoot.setData(0, Qt.UserRole, self.world.render) + sceneRoot.setData(0, Qt.UserRole + 1, "SCENE_ROOT") + self._apply_font_to_item(sceneRoot) + # 添加相机节点 + cameraItem = QTreeWidgetItem(sceneRoot, ['相机']) + cameraItem.setData(0, Qt.UserRole, self.world.cam) + cameraItem.setData(0, Qt.UserRole + 1, "CAMERA_NODE") + self._apply_font_to_item(cameraItem) + # 添加地板节点 + if hasattr(self.world, 'ground') and self.world.ground: + groundItem = QTreeWidgetItem(sceneRoot, ['地板']) + groundItem.setData(0, Qt.UserRole, self.world.ground) + groundItem.setData(0,Qt.UserRole + 1, "SCENE_NODE") + self._apply_font_to_item(groundItem) + + def _apply_font_to_item(self, item): + """根据节点层级设置字体大小""" + if not item: + return + font = QFont(self.font()) + marker = item.data(0, Qt.UserRole + 1) + if item.parent() is None and (marker == "SCENE_ROOT" or item.text(0) == '场景'): + font = QFont("Microsoft YaHei", 12) + font.setWeight(QFont.Light) + else: + font = QFont("Microsoft YaHei", 10) + font.setWeight(QFont.Light) + item.setFont(0, font) + + def _apply_font_recursively(self, item): + """递归应用字体样式""" + if not item: + return + self._apply_font_to_item(item) + for index in range(item.childCount()): + self._apply_font_recursively(item.child(index)) + + def _handle_rows_inserted(self, parent_index, first, last): + """在插入新节点时自动调整字体""" + model = self.model() + if not model: + return + for row in range(first, last + 1): + index = model.index(row, 0, parent_index) + item = self.itemFromIndex(index) + if item: + self._apply_font_recursively(item) + + def _cleanup_panda_node_resources(self, panda_node): + """一个集中的辅助函数,用于清理与Panda3D节点相关的所有资源。""" + if not panda_node or panda_node.is_empty(): + return + try: + # 清理选择状态 + if hasattr(self.world, 'selection') and self.world.selection.selectedNode == panda_node: + self.world.selection.updateSelection(None) + # 清理属性面板 + if hasattr(self.world, 'property_panel'): + self.world.property_panel.removeActorForModel(panda_node) + # 清理灯光 + if hasattr(panda_node, 'getPythonTag'): + light_object = panda_node.getPythonTag('rp_light_object') + if light_object and hasattr(self.world, 'render_pipeline'): + self.world.render_pipeline.remove_light(light_object) + # 从各种world管理列表中移除 + lists_to_check = ['gui_elements', 'models', 'Spotlight', 'Pointlight', 'terrains'] + for list_name in lists_to_check: + if hasattr(self.world, list_name): + world_list = getattr(self.world, list_name) + if panda_node in world_list: + world_list.remove(panda_node) + # 特殊处理tilesets + if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'tilesets'): + tilesets_to_remove = [i for i, info in enumerate(self.world.scene_manager.tilesets) if + info.get('node') == panda_node] + for i in reversed(tilesets_to_remove): + del self.world.scene_manager.tilesets[i] + print(f"🧹 已清理节点 {panda_node.getName()} 的所有关联资源。") + except Exception as e: + # 即便这里出错,也要打印信息,但不要让整个删除流程中断 + print(f"⚠️ 清理节点 {panda_node.getName()} 资源时出错: {e}") + + # def mousePressEvent(self, event): + # """鼠标按下事件""" + # if event.button() == Qt.LeftButton: + # if self.currentItem(): + # print(f"self.currentItem() = {self.currentItem()}") + # else: + # print(f"self.currentItem() = None") + # + # # 调用父类处理其他事件 + # super().mousePressEvent(event) + + def update_item_name(self, text, item): + """ 树节点名字 """ + if not item: + return + try: + # 正确的代码 + node = item.data(0, Qt.UserRole) + + item.setText(0, text) + node.setName(text) + except Exception as e: + print(e) + + def _findSceneRoot(self): + """查找场景根节点""" + for i in range(self.topLevelItemCount()): + top_item = self.topLevelItem(i) + if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": + return top_item + return None + + def create_model_items(self, model: NodePath): + """ + 【此函数保持不变】 + 创建模型项。 + 只寻找模型下一层带有 'is_scene_element' 标签的子节点作为分支的根, + 然后完整地展示这些分支。 + """ + if not model: + print("传入的参数model为空") + return + + # 找到场景树的根节点,我们将把模型节点添加到这里 + root_item = self._findSceneRoot() + if not root_item: + print("错误:未能找到场景根节点项") + return + + # 1. 在模型的第一层子节点中进行筛选 + for child_node in model.getChildren(): + if child_node.hasTag("is_scene_element"): + print(f"找到带标签的根节点:{child_node.getName()}") + if (child_node.hasTag("gui_type")and + child_node.getTag("gui_type") in ["3d_text","3d_image","video_screen"]): + print(f"跳过3dGUI节点{child_node.getName()}") + continue + + # 为这个带标签的节点创建一个树项 + child_item = QTreeWidgetItem(root_item) + child_item.setText(0, child_node.getName() or "Unnamed Tagged Node") + child_item.setData(0, Qt.UserRole, child_node) + child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type")) + # self._add_node_info(child_item, child_node) # 可选信息 + + # 2. 对这个节点的所有后代进行“无条件”递归添加 (但会跳过碰撞体) + self._add_all_children_unconditionally(child_item, child_node) + + def _add_all_children_unconditionally(self, parent_item: QTreeWidgetItem, node_path: NodePath): + """ + 【此函数已更新】 + 无条件地、递归地添加一个节点下的所有子节点,但会跳过碰撞节点。 + """ + for child_node in node_path.getChildren(): + + # 新增:检查节点是否为碰撞节点 + if isinstance(child_node.node(), CollisionNode): + # print(f"跳过碰撞节点: {child_node.getName()}") # 用于调试 + continue # 如果是,则跳过此节点及其所有子节点 + + # 创建子项 + child_item = QTreeWidgetItem(parent_item) + child_item.setText(0, child_node.getName() or "Unnamed Child") + child_item.setData(0, Qt.UserRole, child_node) + child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type")) + # self._add_node_info(child_item, child_node) # 可选信息 + + # 继续无条件地递归 + if not child_node.is_empty(): + self._add_all_children_unconditionally(child_item, child_node) + + # ==================== 辅助方法 ==================== + def _findSceneRoot(self): + """查找场景根节点""" + for i in range(self.topLevelItemCount()): + top_item = self.topLevelItem(i) + if top_item.data(0, Qt.UserRole + 1) == "SCENE_ROOT": + return top_item + return + + def _generateUniqueNodeName(self, base_name, parent_node): + """生成唯一的节点名称""" + # 获取父节点下所有子节点的名称 + existing_names = set() + for child in parent_node.getChildren(): + existing_names.add(child.getName()) + + # 如果基础名称不存在,直接使用 + if base_name not in existing_names: + return base_name + + # 否则添加数字后缀 + counter = 1 + while f"{base_name}_{counter}" in existing_names: + counter += 1 + + return f"{base_name}_{counter}" + + def add_node_to_tree_widget(self, node, parent_item, node_type): + """将node元素添加到树形控件""" + if hasattr(node, 'getTag'): + if node.hasTag('tree_item_type'): + print(f"node0: {node.getName()},{node.getTag('tree_item_type')}") + tree_type = node.getTag('tree_item_type') + else: + node.setTag('tree_item_type', node_type) + else: + print(f"node2: {node.getName()},{node_type}") + tree_type = node_type + + + # BLACK_LIST 和依赖项导入保持不变 + BLACK_LIST = {'', '**', 'temp', 'collision'} + from panda3d.core import CollisionNode, ModelRoot + from PyQt5.QtWidgets import QTreeWidgetItem + from PyQt5.QtCore import Qt + + # 1. 修改内部函数,让它返回创建的节点 + def addNodeToTree(node, parentItem, force=False): + """内部递归函数,现在会返回创建的顶级节点项""" + if not force and should_skip(node): + return None # 如果跳过,返回None + + nodeItem = QTreeWidgetItem(parentItem, [node.getName()]) + nodeItem.setData(0, Qt.UserRole, node) + nodeItem.setData(0, Qt.UserRole + 1, tree_type) + + for child in node.getChildren(): + # 递归调用,但我们只关心顶级的nodeItem + addNodeToTree(child, nodeItem, force=False) + + return nodeItem # <-- 新增:返回创建的QTreeWidgetItem + + def should_skip(node): + name = node.getName() + return name in BLACK_LIST or name.startswith('__') or isinstance(node.node(), CollisionNode) or isinstance( + node.node(), ModelRoot) or name == "" + + # 使用一个变量来确保无论哪个分支都有返回值 + new_qt_item = None + node_name = "" + + try: + if tree_type == "IMPORTED_MODEL_NODE": + # getTag('file') 可能是你自己设置的tag,这里假设它存在 + node_name = node.getTag("file") if hasattr(node, 'getTag') and node.hasTag("file") else node.getName() + + # 2. 接收 addNodeToTree 的返回值 + new_qt_item = addNodeToTree(node, parent_item, force=True) + + else: + node_name = node.getName() if hasattr(node, 'getName') else "node" + new_qt_item = QTreeWidgetItem(parent_item, [node_name]) + new_qt_item.setData(0, Qt.UserRole, node) + new_qt_item.setData(0, Qt.UserRole + 1, tree_type) + + # 确保 new_qt_item 成功创建后再继续操作 + if new_qt_item: + # 展开父节点 + if hasattr(parent_item, 'setExpanded'): + parent_item.setExpanded(True) + + print(f"✅ Qt树节点添加成功: {node_name}") + return new_qt_item + else: + # 如果 addNodeToTree 因为 should_skip 返回了 None + print(f"ℹ️ 节点 {node_name} 被跳过,未添加到树中。") + return None + + except Exception as e: + import traceback + print(f"❌ 添加node到树形控件失败: {str(e)}") + traceback.print_exc() # 打印更详细的错误堆栈,方便调试 + return None + + def update_selection_and_properties(self, node, qt_item): + """更新选择状态和属性面板""" + try: + # 更新选择状态 + if hasattr(self.world, 'selection'): + self.world.selection.updateSelection(node) + + # 更新属性面板 + if hasattr(self.world, 'property_panel'): + self.world.property_panel.updatePropertyPanel(qt_item) + elif hasattr(self.world, 'updatePropertyPanel'): + self.world.updatePropertyPanel(qt_item) + + print(f"✅ 更新选择和属性面板: {qt_item.text(0)}") + + except Exception as e: + print(f"❌ 更新选择和属性面板失败: {str(e)}") + + # ==================== 3D辅助方法 ==================== + def get_target_parents_for_creation(self): + """获取创建目标的父节点列表""" + from PyQt5.QtCore import Qt + + target_parents = [] + + try: + selected_items = self.selectedItems() + + # 检查选中的项目,找出所有有效的父节点 + for item in selected_items: + if self._isValidParentForNewNode(item): + parent_node = item.data(0, Qt.UserRole) + if parent_node: + target_parents.append((item, parent_node)) + print(f"📍 找到有效父节点: {item.text(0)}") + + # 如果没有有效的选中节点,使用场景根节点 + if not target_parents: + print("⚠️ 没有有效选中节点,使用场景根节点") + scene_root = self._findSceneRoot() + if scene_root: + parent_node = scene_root.data(0, Qt.UserRole) + if parent_node: + target_parents.append((scene_root, parent_node)) + else: + # 如果没有_findSceneRoot方法,使用world.render作为默认父节点 + if hasattr(self.world, 'render'): + # 创建一个虚拟的树项目来表示render节点 + class MockTreeItem: + def text(self, column): + return "render" + + mock_item = MockTreeItem() + target_parents.append((mock_item, self.world.render)) + + print(f"📊 总共找到 {len(target_parents)} 个目标父节点") + return target_parents + + except Exception as e: + print(f"❌ 获取目标父节点失败: {str(e)}") + return [] + + def _isValidParentForNewNode(self, item): + """检查节点是否适合作为新节点的父节点-3d""" + if not item: + return False + + # 获取节点类型标识 + node_type = item.data(0, Qt.UserRole + 1) + + # 场景根节点和普通场景节点可以作为父节点 + if node_type in self.valid_3d_parent_types: + return True + + # # 模型节点也可以作为父节点 + # panda_node = item.data(0, Qt.UserRole) + # if panda_node and panda_node.hasTag("is_model_root"): + # return True + # + # # 其他类型的节点也可以,但排除一些特殊情况 + # if panda_node: + # # # 排除相机节点 + # # if panda_node == self.world.cam: + # # return False + # # 排除碰撞节点 + # from panda3d.core import CollisionNode + # if isinstance(panda_node.node(), CollisionNode): + # return False + # return True + + return False + + + def get_target_parents_for_gui_creation(self): + """获取GUI创建目标的父节点列表 - 支持GUI元素作为父节点""" + from PyQt5.QtCore import Qt + + target_parents = [] + + try: + selected_items = self.selectedItems() + + # 检查选中的项目,找出所有有效的父节点 + for item in selected_items: + if self.isValidParentForGUI(item): + parent_node = item.data(0, Qt.UserRole) + if parent_node: + target_parents.append((item, parent_node)) # 修复:确保添加到列表 + print(f"📍 找到有效GUI父节点: {item.text(0)}") + else: + print(f"⚠️ GUI父节点 {item.text(0)} 的Panda3D数据为空") + + # 如果没有有效的选中节点,使用场景根节点 + if not target_parents: + print("⚠️ 没有有效选中节点,使用场景根节点") + scene_root = self._findSceneRoot() + if scene_root: + parent_node = scene_root.data(0, Qt.UserRole) + if parent_node: + target_parents.append((scene_root, parent_node)) + else: + if hasattr(self.world, 'render'): + class MockTreeItem: + def text(self, column): + return "render" + + mock_item = MockTreeItem() + target_parents.append((mock_item, self.world.render)) + + print(f"📊 总共找到 {len(target_parents)} 个目标父节点") + return target_parents + + except Exception as e: + print(f"❌ 获取目标父节点失败: {str(e)}") + return [] + + def isValidParentForGUI(self, item): + """检查节点是否适合作为GUI元素的父节点""" + if not item: + return False + + # 获取节点类型标识 + node_type = item.data(0, Qt.UserRole + 1) + + # GUI元素可以作为其他GUI元素的父节点 + if node_type in self.gui_2d_types: + return True + + # 场景根节点和普通场景节点也可以作为父节点 + if node_type in ["SCENE_ROOT"]: + return True + + return False + + def is_gui_element(self, node): + """判断节点是否是GUI元素""" + if not node: + return False + + # 检查是否有GUI标签 + if hasattr(node, 'getTag'): + return node.getTag("is_gui_element") == "1" or node.getTag("gui_type") in ["info_panel", "button", "label", "entry", "3d_text", "virtual_screen"] + + # 检查是否是DirectGUI对象 + try: + from direct.gui.DirectGuiBase import DirectGuiBase + return isinstance(node, DirectGuiBase) + except ImportError: + return False + + def calculate_relative_gui_position(self, pos, parent_gui): + """计算相对于GUI父节点的位置""" + try: + # 对于GUI子元素,使用相对坐标 + # 这里使用较小的缩放因子,因为是相对于父GUI的位置 + relative_pos = (pos[0] * 0.05, 0, pos[2] * 0.05) + print(f"📐 计算GUI相对位置: {pos} -> {relative_pos}") + return relative_pos + except Exception as e: + print(f"❌ 计算GUI相对位置失败: {str(e)}") + # 如果计算失败,返回默认的屏幕坐标 + return (pos[0] * 0.1, 0, pos[2] * 0.1) + + #---------------------------暂时无用------------------------------- + + def add_existing_node(self, panda_node, node_type="SCENE_NODE", parent_item=None): + """将已存在的Panda3D节点添加到Qt树形控件中 + + Args: + panda_node: 已创建的Panda3D节点 + node_type: 节点类型标识 + parent_item: 父Qt项目,如果为None则使用当前选中项或根节点 + + Returns: + QTreeWidgetItem: 创建的Qt树项目 + """ + try: + if not panda_node: + print("❌ 传入的Panda3D节点为空") + return None + + # 确定父项目 - 保持简单,只处理单个父节点 + if parent_item is None: + # 优先使用当前选中的第一个有效节点作为父节点 + selected_items = self.selectedItems() + if selected_items: + # 找到第一个有效的父节点 + for potential_parent in selected_items: + if self._isValidParentForNewNode(potential_parent): + parent_item = potential_parent + print(f"📍 使用选中节点作为父节点: {parent_item.text(0)}") + break + + # 如果没有找到有效的选中节点 + if not parent_item: + print("⚠️ 所有选中节点都不适合作为父节点,查找场景根节点") + parent_item = self._findSceneRoot() + else: + # 没有选中任何节点,使用场景根节点 + print("📍 没有选中节点,使用场景根节点作为父节点") + parent_item = self._findSceneRoot() + + # 如果场景根节点也找不到,最后尝试render节点 + if not parent_item: + print("📍 场景根节点未找到,尝试使用render节点") + parent_item = self._findRenderItem() + + if not parent_item: + print("❌ 无法找到合适的父节点") + return None + + # 创建Qt树项目 + node_name = panda_node.getName() + new_qt_item = QTreeWidgetItem(parent_item, [node_name]) + new_qt_item.setData(0, Qt.UserRole, panda_node) + new_qt_item.setData(0, Qt.UserRole + 1, node_type) + + # 展开父节点 + parent_item.setExpanded(True) + + print(f"✅ 成功将现有节点添加到树形控件: {node_name} -> 父节点: {parent_item.text(0)}") + return new_qt_item + + except Exception as e: + print(f"❌ 添加现有节点到树形控件失败: {str(e)}") + import traceback + traceback.print_exc() + return None + + def _findRenderItem(self): + """查找render根节点项目""" + try: + root = self.invisibleRootItem() + for i in range(root.childCount()): + item = root.child(i) + if item.text(0) == "render": + return item + + # 如果没找到render节点,返回第一个子项目 + if root.childCount() > 0: + return root.child(0) + + return None + except Exception as e: + print(f"查找render节点失败: {e}") + return None + + def create_item(self, node_type="empty", selected_items=None): + """创建不同类型的场景节点 + + Args: + node_type: 节点类型 ("empty", "spot_light", "point_light") + selected_items: 选中的父节点项目列表,如果为None则使用当前选中项或根节点 + """ + try: + # 确定父节点 + parent_items = self._determineParentItems(selected_items) + if not parent_items: + print("⚠️ 无法确定父节点") + return [] + + created_nodes = [] + + for parent_item in parent_items: + # 验证父节点的有效性 + if not self._isValidParentForNewNode(parent_item): + print(f"⚠️ 节点 {parent_item.text(0)} 不适合作为父节点") + continue + + # 获取父节点的Panda3D对象 + parent_node = parent_item.data(0, Qt.UserRole) + if not parent_node: + print(f"⚠️ 父节点 {parent_item.text(0)} 没有对应的Panda3D对象") + continue + + # 根据节点类型创建不同的节点 + if node_type == "empty": + new_node = self._createEmptyNode(parent_node, parent_item) + elif node_type == "spot_light": + new_node = self._createSpotLightNode(parent_node, parent_item) + elif node_type == "point_light": + new_node = self._createPointLightNode(parent_node, parent_item) + else: + print(f"❌ 不支持的节点类型: {node_type}") + continue + + if new_node: + created_nodes.append(new_node) + + # 如果只创建了一个节点,自动选中它 + if len(created_nodes) == 1: + _, qt_item = created_nodes[0] + self.setCurrentItem(qt_item) + # 更新选择和属性面板 + if hasattr(self.world, 'selection'): + self.world.selection.updateSelection(qt_item.data(0, Qt.UserRole)) + if hasattr(self.world, 'property_panel'): + self.world.property_panel.updatePropertyPanel(qt_item) + + print(f"✅ 总共创建了 {len(created_nodes)} 个 {node_type} 节点") + return created_nodes + + except Exception as e: + print(f"❌ 创建 {node_type} 节点失败: {str(e)}") + import traceback + traceback.print_exc() + return [] + + def _determineParentItems(self, selected_items): + """确定父节点项目列表""" + if selected_items is not None: + return selected_items + + # 使用当前选中的项目 + current_selected = self.selectedItems() + if current_selected: + return current_selected + + # 如果没有选中任何项目,使用场景根节点 + scene_root = self._findSceneRoot() + if scene_root: + return [scene_root] + + return [] + + def _setupNewNodeDefaults(self, node): + """设置新节点的默认属性""" + # 设置默认位置(相对于父节点) + node.setPos(0, 0, 0) + node.setHpr(0, 0, 0) + node.setScale(1, 1, 1) + + # 设置默认可见性 + node.show() + + # 可以根据需要添加更多默认设置 + # 例如:默认材质、碰撞检测等 + + # ==================== 创建节点 ==================== + def _createEmptyNode(self, parent_node, parent_item): + """创建空节点""" + # 生成唯一的节点名称 + node_name = self._generateUniqueNodeName("空节点", parent_node) + + # 在Panda3D场景中创建新节点 + new_panda_node = parent_node.attachNewNode(node_name) + + # 设置新节点的默认属性 + self._setupNewNodeDefaults(new_panda_node) + + # 设置节点标签 + new_panda_node.setTag("is_scene_element", "1") + new_panda_node.setTag("node_type", "empty_node") + new_panda_node.setTag("created_by_user", "1") + + # 在Qt树中创建对应的项目 + new_qt_item = QTreeWidgetItem(parent_item, [node_name]) + new_qt_item.setData(0, Qt.UserRole, new_panda_node) + new_qt_item.setData(0, Qt.UserRole + 1, "SCENE_NODE") + + # 展开父节点 + parent_item.setExpanded(True) + + print(f"✅ 成功创建空节点: {node_name}") + return (new_panda_node, new_qt_item) + + def _createSpotLightNode(self, parent_node, parent_item): + """创建聚光灯节点""" + from RenderPipelineFile.rpcore import SpotLight + from QMeta3D.Meta3DWorld import get_render_pipeline + from panda3d.core import Vec3, NodePath + + try: + render_pipeline = get_render_pipeline() + + # 生成唯一的节点名称 + light_name = self._generateUniqueNodeName(f"Spotlight_{len(self.world.Spotlight)}", parent_node) + + # 创建挂载节点 + light_np = NodePath(light_name) + light_np.reparentTo(parent_node) + + # 创建聚光灯对象 + light = SpotLight() + light.direction = Vec3(0, 0, -1) + light.fov = 70 + light.set_color_from_temperature(5 * 1000.0) + light.energy = 5000 + light.radius = 1000 + light.casts_shadows = True + light.shadow_map_resolution = 256 + light.setPos(0, 0, 0) # 相对于父节点的位置 + + # 添加到渲染管线 + render_pipeline.add_light(light) + + # 设置节点属性和标签 + light_np.setTag("light_type", "spot_light") + light_np.setTag("is_scene_element", "1") + light_np.setTag("light_energy", str(light.energy)) + light_np.setTag("created_by_user", "1") + + # 保存光源对象引用 + light_np.setPythonTag("rp_light_object", light) + + # 添加到管理列表 + self.world.Spotlight.append(light_np) + + # 在Qt树中创建对应的项目 + new_qt_item = QTreeWidgetItem(parent_item, [light_name]) + new_qt_item.setData(0, Qt.UserRole, light_np) + new_qt_item.setData(0, Qt.UserRole + 1, "LIGHT_NODE") + + # 展开父节点 + parent_item.setExpanded(True) + + print(f"✅ 成功创建聚光灯: {light_name}") + + except Exception as e: + print(f"❌ 创建聚光灯失败: {str(e)}") + return None + + def _createPointLightNode(self, parent_node, parent_item): + """创建点光源节点""" + from RenderPipelineFile.rpcore import PointLight + from QMeta3D.Meta3DWorld import get_render_pipeline + from panda3d.core import Vec3, NodePath + + try: + render_pipeline = get_render_pipeline() + + # 生成唯一的节点名称 + light_name = self._generateUniqueNodeName(f"Pointlight_{len(self.world.Pointlight)}", parent_node) + + # 创建挂载节点 + light_np = NodePath(light_name) + light_np.reparentTo(parent_node) + + # 创建点光源对象 + light = PointLight() + light.setPos(0, 0, 0) # 相对于父节点的位置 + light.energy = 5000 + light.radius = 1000 + light.inner_radius = 0.4 + light.set_color_from_temperature(5 * 1000.0) + light.casts_shadows = True + light.shadow_map_resolution = 256 + + # 添加到渲染管线 + render_pipeline.add_light(light) + + # 设置节点属性和标签 + light_np.setTag("light_type", "point_light") + light_np.setTag("is_scene_element", "1") + light_np.setTag("light_energy", str(light.energy)) + light_np.setTag("created_by_user", "1") + + # 保存光源对象引用 + light_np.setPythonTag("rp_light_object", light) + + # 添加到管理列表 + self.world.Pointlight.append(light_np) + + # 在Qt树中创建对应的项目 + new_qt_item = QTreeWidgetItem(parent_item, [light_name]) + new_qt_item.setData(0, Qt.UserRole, light_np) + new_qt_item.setData(0, Qt.UserRole + 1, "LIGHT_NODE") + + # 展开父节点 + parent_item.setExpanded(True) + + print(f"✅ 成功创建点光源: {light_name}") + return (light_np, new_qt_item) + + except Exception as e: + print(f"❌ 创建点光源失败: {str(e)}") + return None + + + diff --git a/vr_actions/actions.json b/vr_actions/actions.json index 7e55618f..639a0f38 100644 --- a/vr_actions/actions.json +++ b/vr_actions/actions.json @@ -1,89 +1,89 @@ -{ - "actions": [ - { - "name": "/actions/default/in/Pose", - "type": "pose" - }, - { - "name": "/actions/default/in/Trigger", - "type": "boolean" - }, - { - "name": "/actions/default/in/Grip", - "type": "boolean" - }, - { - "name": "/actions/default/in/Menu", - "type": "boolean" - }, - { - "name": "/actions/default/in/System", - "type": "boolean" - }, - { - "name": "/actions/default/in/TrackpadClick", - "type": "boolean" - }, - { - "name": "/actions/default/in/TrackpadTouch", - "type": "boolean" - }, - { - "name": "/actions/default/in/AButton", - "type": "boolean" - }, - { - "name": "/actions/default/in/BButton", - "type": "boolean" - }, - { - "name": "/actions/default/in/Trackpad", - "type": "vector2" - }, - { - "name": "/actions/default/in/Joystick", - "type": "vector2" - }, - { - "name": "/actions/default/in/Squeeze", - "type": "vector1" - }, - { - "name": "/actions/default/out/Haptic", - "type": "vibration" - } - ], - "action_sets": [ - { - "name": "/actions/default", - "usage": "single" - } - ], - "default_bindings": [ - { - "controller_type": "vive_controller", - "binding_url": "bindings_vive.json" - }, - { - "controller_type": "oculus_touch", - "binding_url": "bindings_oculus.json" - }, - { - "controller_type": "knuckles", - "binding_url": "bindings_index.json" - } - ], - "localization": [ - { - "language_tag": "zh_CN", - "/actions/default/in/Trigger": "扳机", - "/actions/default/in/Grip": "握把", - "/actions/default/in/Menu": "菜单", - "/actions/default/in/System": "系统", - "/actions/default/in/TrackpadClick": "触摸板点击", - "/actions/default/in/TrackpadTouch": "触摸板触摸", - "/actions/default/in/Pose": "手部姿态", - "/actions/default/out/Haptic": "震动反馈" - } - ] +{ + "actions": [ + { + "name": "/actions/default/in/Pose", + "type": "pose" + }, + { + "name": "/actions/default/in/Trigger", + "type": "boolean" + }, + { + "name": "/actions/default/in/Grip", + "type": "boolean" + }, + { + "name": "/actions/default/in/Menu", + "type": "boolean" + }, + { + "name": "/actions/default/in/System", + "type": "boolean" + }, + { + "name": "/actions/default/in/TrackpadClick", + "type": "boolean" + }, + { + "name": "/actions/default/in/TrackpadTouch", + "type": "boolean" + }, + { + "name": "/actions/default/in/AButton", + "type": "boolean" + }, + { + "name": "/actions/default/in/BButton", + "type": "boolean" + }, + { + "name": "/actions/default/in/Trackpad", + "type": "vector2" + }, + { + "name": "/actions/default/in/Joystick", + "type": "vector2" + }, + { + "name": "/actions/default/in/Squeeze", + "type": "vector1" + }, + { + "name": "/actions/default/out/Haptic", + "type": "vibration" + } + ], + "action_sets": [ + { + "name": "/actions/default", + "usage": "single" + } + ], + "default_bindings": [ + { + "controller_type": "vive_controller", + "binding_url": "bindings_vive.json" + }, + { + "controller_type": "oculus_touch", + "binding_url": "bindings_oculus.json" + }, + { + "controller_type": "knuckles", + "binding_url": "bindings_index.json" + } + ], + "localization": [ + { + "language_tag": "zh_CN", + "/actions/default/in/Trigger": "扳机", + "/actions/default/in/Grip": "握把", + "/actions/default/in/Menu": "菜单", + "/actions/default/in/System": "系统", + "/actions/default/in/TrackpadClick": "触摸板点击", + "/actions/default/in/TrackpadTouch": "触摸板触摸", + "/actions/default/in/Pose": "手部姿态", + "/actions/default/out/Haptic": "震动反馈" + } + ] } \ No newline at end of file diff --git a/vr_actions/bindings_index.json b/vr_actions/bindings_index.json index f7198f13..71dd2f28 100644 --- a/vr_actions/bindings_index.json +++ b/vr_actions/bindings_index.json @@ -1,133 +1,133 @@ -{ - "controller_type": "knuckles", - "description": "Valve Index\u63a7\u5236\u5668\u7ed1\u5b9a", - "name": "EG VR Editor - Index", - "bindings": { - "/actions/default": { - "sources": [ - { - "inputs": { - "click": { - "output": "/actions/default/in/Trigger" - } - }, - "mode": "button", - "path": "/user/hand/left/input/trigger" - }, - { - "inputs": { - "click": { - "output": "/actions/default/in/Trigger" - } - }, - "mode": "button", - "path": "/user/hand/right/input/trigger" - }, - { - "inputs": { - "value": { - "output": "/actions/default/in/Squeeze" - } - }, - "mode": "trigger", - "path": "/user/hand/left/input/grip" - }, - { - "inputs": { - "value": { - "output": "/actions/default/in/Squeeze" - } - }, - "mode": "trigger", - "path": "/user/hand/right/input/grip" - }, - { - "inputs": { - "click": { - "output": "/actions/default/in/AButton" - } - }, - "mode": "button", - "path": "/user/hand/right/input/a" - }, - { - "inputs": { - "click": { - "output": "/actions/default/in/BButton" - } - }, - "mode": "button", - "path": "/user/hand/right/input/b" - }, - { - "inputs": { - "position": { - "output": "/actions/default/in/Trackpad" - }, - "click": { - "output": "/actions/default/in/TrackpadClick" - }, - "touch": { - "output": "/actions/default/in/TrackpadTouch" - } - }, - "mode": "trackpad", - "path": "/user/hand/left/input/trackpad" - }, - { - "inputs": { - "position": { - "output": "/actions/default/in/Trackpad" - }, - "click": { - "output": "/actions/default/in/TrackpadClick" - }, - "touch": { - "output": "/actions/default/in/TrackpadTouch" - } - }, - "mode": "trackpad", - "path": "/user/hand/right/input/trackpad" - }, - { - "inputs": { - "position": { - "output": "/actions/default/in/Joystick" - } - }, - "mode": "joystick", - "path": "/user/hand/left/input/thumbstick" - }, - { - "inputs": { - "position": { - "output": "/actions/default/in/Joystick" - } - }, - "mode": "joystick", - "path": "/user/hand/right/input/thumbstick" - } - ], - "poses": [ - { - "output": "/actions/default/in/Pose", - "path": "/user/hand/left/pose/raw" - }, - { - "output": "/actions/default/in/Pose", - "path": "/user/hand/right/pose/raw" - } - ], - "haptics": [ - { - "output": "/actions/default/out/Haptic", - "path": "/user/hand/left/output/haptic" - }, - { - "output": "/actions/default/out/Haptic", - "path": "/user/hand/right/output/haptic" - } - ] - } - } +{ + "controller_type": "knuckles", + "description": "Valve Index\u63a7\u5236\u5668\u7ed1\u5b9a", + "name": "EG VR Editor - Index", + "bindings": { + "/actions/default": { + "sources": [ + { + "inputs": { + "click": { + "output": "/actions/default/in/Trigger" + } + }, + "mode": "button", + "path": "/user/hand/left/input/trigger" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Trigger" + } + }, + "mode": "button", + "path": "/user/hand/right/input/trigger" + }, + { + "inputs": { + "value": { + "output": "/actions/default/in/Squeeze" + } + }, + "mode": "trigger", + "path": "/user/hand/left/input/grip" + }, + { + "inputs": { + "value": { + "output": "/actions/default/in/Squeeze" + } + }, + "mode": "trigger", + "path": "/user/hand/right/input/grip" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/AButton" + } + }, + "mode": "button", + "path": "/user/hand/right/input/a" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/BButton" + } + }, + "mode": "button", + "path": "/user/hand/right/input/b" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Trackpad" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + }, + "touch": { + "output": "/actions/default/in/TrackpadTouch" + } + }, + "mode": "trackpad", + "path": "/user/hand/left/input/trackpad" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Trackpad" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + }, + "touch": { + "output": "/actions/default/in/TrackpadTouch" + } + }, + "mode": "trackpad", + "path": "/user/hand/right/input/trackpad" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Joystick" + } + }, + "mode": "joystick", + "path": "/user/hand/left/input/thumbstick" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Joystick" + } + }, + "mode": "joystick", + "path": "/user/hand/right/input/thumbstick" + } + ], + "poses": [ + { + "output": "/actions/default/in/Pose", + "path": "/user/hand/left/pose/raw" + }, + { + "output": "/actions/default/in/Pose", + "path": "/user/hand/right/pose/raw" + } + ], + "haptics": [ + { + "output": "/actions/default/out/Haptic", + "path": "/user/hand/left/output/haptic" + }, + { + "output": "/actions/default/out/Haptic", + "path": "/user/hand/right/output/haptic" + } + ] + } + } } \ No newline at end of file diff --git a/vr_actions/bindings_oculus.json b/vr_actions/bindings_oculus.json index 71a697b7..dc1fdc7a 100644 --- a/vr_actions/bindings_oculus.json +++ b/vr_actions/bindings_oculus.json @@ -1,118 +1,118 @@ -{ - "controller_type": "oculus_touch", - "description": "Oculus Touch\u63a7\u5236\u5668\u7ed1\u5b9a", - "name": "EG VR Editor - Oculus Touch", - "bindings": { - "/actions/default": { - "sources": [ - { - "inputs": { - "click": { - "output": "/actions/default/in/Trigger" - } - }, - "mode": "button", - "path": "/user/hand/left/input/trigger" - }, - { - "inputs": { - "click": { - "output": "/actions/default/in/Trigger" - } - }, - "mode": "button", - "path": "/user/hand/right/input/trigger" - }, - { - "inputs": { - "value": { - "output": "/actions/default/in/Squeeze" - } - }, - "mode": "trigger", - "path": "/user/hand/left/input/grip" - }, - { - "inputs": { - "value": { - "output": "/actions/default/in/Squeeze" - } - }, - "mode": "trigger", - "path": "/user/hand/right/input/grip" - }, - { - "inputs": { - "click": { - "output": "/actions/default/in/AButton" - } - }, - "mode": "button", - "path": "/user/hand/right/input/a" - }, - { - "inputs": { - "click": { - "output": "/actions/default/in/BButton" - } - }, - "mode": "button", - "path": "/user/hand/right/input/b" - }, - { - "inputs": { - "position": { - "output": "/actions/default/in/Joystick" - }, - "click": { - "output": "/actions/default/in/TrackpadClick" - } - }, - "mode": "joystick", - "path": "/user/hand/left/input/thumbstick" - }, - { - "inputs": { - "position": { - "output": "/actions/default/in/Joystick" - }, - "click": { - "output": "/actions/default/in/TrackpadClick" - } - }, - "mode": "joystick", - "path": "/user/hand/right/input/thumbstick" - }, - { - "inputs": { - "click": { - "output": "/actions/default/in/Menu" - } - }, - "mode": "button", - "path": "/user/hand/left/input/menu" - } - ], - "poses": [ - { - "output": "/actions/default/in/Pose", - "path": "/user/hand/left/pose/raw" - }, - { - "output": "/actions/default/in/Pose", - "path": "/user/hand/right/pose/raw" - } - ], - "haptics": [ - { - "output": "/actions/default/out/Haptic", - "path": "/user/hand/left/output/haptic" - }, - { - "output": "/actions/default/out/Haptic", - "path": "/user/hand/right/output/haptic" - } - ] - } - } +{ + "controller_type": "oculus_touch", + "description": "Oculus Touch\u63a7\u5236\u5668\u7ed1\u5b9a", + "name": "EG VR Editor - Oculus Touch", + "bindings": { + "/actions/default": { + "sources": [ + { + "inputs": { + "click": { + "output": "/actions/default/in/Trigger" + } + }, + "mode": "button", + "path": "/user/hand/left/input/trigger" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Trigger" + } + }, + "mode": "button", + "path": "/user/hand/right/input/trigger" + }, + { + "inputs": { + "value": { + "output": "/actions/default/in/Squeeze" + } + }, + "mode": "trigger", + "path": "/user/hand/left/input/grip" + }, + { + "inputs": { + "value": { + "output": "/actions/default/in/Squeeze" + } + }, + "mode": "trigger", + "path": "/user/hand/right/input/grip" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/AButton" + } + }, + "mode": "button", + "path": "/user/hand/right/input/a" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/BButton" + } + }, + "mode": "button", + "path": "/user/hand/right/input/b" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Joystick" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + } + }, + "mode": "joystick", + "path": "/user/hand/left/input/thumbstick" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Joystick" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + } + }, + "mode": "joystick", + "path": "/user/hand/right/input/thumbstick" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Menu" + } + }, + "mode": "button", + "path": "/user/hand/left/input/menu" + } + ], + "poses": [ + { + "output": "/actions/default/in/Pose", + "path": "/user/hand/left/pose/raw" + }, + { + "output": "/actions/default/in/Pose", + "path": "/user/hand/right/pose/raw" + } + ], + "haptics": [ + { + "output": "/actions/default/out/Haptic", + "path": "/user/hand/left/output/haptic" + }, + { + "output": "/actions/default/out/Haptic", + "path": "/user/hand/right/output/haptic" + } + ] + } + } } \ No newline at end of file diff --git a/vr_actions/bindings_vive.json b/vr_actions/bindings_vive.json index 2dd50b51..23144736 100644 --- a/vr_actions/bindings_vive.json +++ b/vr_actions/bindings_vive.json @@ -1,106 +1,106 @@ -{ - "controller_type": "vive_controller", - "description": "Vive\u63a7\u5236\u5668\u7ed1\u5b9a", - "name": "EG VR Editor - Vive", - "bindings": { - "/actions/default": { - "sources": [ - { - "inputs": { - "click": { - "output": "/actions/default/in/Trigger" - } - }, - "mode": "button", - "path": "/user/hand/left/input/trigger" - }, - { - "inputs": { - "click": { - "output": "/actions/default/in/Trigger" - } - }, - "mode": "button", - "path": "/user/hand/right/input/trigger" - }, - { - "inputs": { - "click": { - "output": "/actions/default/in/Grip" - } - }, - "mode": "button", - "path": "/user/hand/left/input/grip" - }, - { - "inputs": { - "click": { - "output": "/actions/default/in/Grip" - } - }, - "mode": "button", - "path": "/user/hand/right/input/grip" - }, - { - "inputs": { - "click": { - "output": "/actions/default/in/Menu" - } - }, - "mode": "button", - "path": "/user/hand/left/input/menu" - }, - { - "inputs": { - "position": { - "output": "/actions/default/in/Trackpad" - }, - "click": { - "output": "/actions/default/in/TrackpadClick" - }, - "touch": { - "output": "/actions/default/in/TrackpadTouch" - } - }, - "mode": "trackpad", - "path": "/user/hand/left/input/trackpad" - }, - { - "inputs": { - "position": { - "output": "/actions/default/in/Trackpad" - }, - "click": { - "output": "/actions/default/in/TrackpadClick" - }, - "touch": { - "output": "/actions/default/in/TrackpadTouch" - } - }, - "mode": "trackpad", - "path": "/user/hand/right/input/trackpad" - } - ], - "poses": [ - { - "output": "/actions/default/in/Pose", - "path": "/user/hand/left/pose/raw" - }, - { - "output": "/actions/default/in/Pose", - "path": "/user/hand/right/pose/raw" - } - ], - "haptics": [ - { - "output": "/actions/default/out/Haptic", - "path": "/user/hand/left/output/haptic" - }, - { - "output": "/actions/default/out/Haptic", - "path": "/user/hand/right/output/haptic" - } - ] - } - } +{ + "controller_type": "vive_controller", + "description": "Vive\u63a7\u5236\u5668\u7ed1\u5b9a", + "name": "EG VR Editor - Vive", + "bindings": { + "/actions/default": { + "sources": [ + { + "inputs": { + "click": { + "output": "/actions/default/in/Trigger" + } + }, + "mode": "button", + "path": "/user/hand/left/input/trigger" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Trigger" + } + }, + "mode": "button", + "path": "/user/hand/right/input/trigger" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Grip" + } + }, + "mode": "button", + "path": "/user/hand/left/input/grip" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Grip" + } + }, + "mode": "button", + "path": "/user/hand/right/input/grip" + }, + { + "inputs": { + "click": { + "output": "/actions/default/in/Menu" + } + }, + "mode": "button", + "path": "/user/hand/left/input/menu" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Trackpad" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + }, + "touch": { + "output": "/actions/default/in/TrackpadTouch" + } + }, + "mode": "trackpad", + "path": "/user/hand/left/input/trackpad" + }, + { + "inputs": { + "position": { + "output": "/actions/default/in/Trackpad" + }, + "click": { + "output": "/actions/default/in/TrackpadClick" + }, + "touch": { + "output": "/actions/default/in/TrackpadTouch" + } + }, + "mode": "trackpad", + "path": "/user/hand/right/input/trackpad" + } + ], + "poses": [ + { + "output": "/actions/default/in/Pose", + "path": "/user/hand/left/pose/raw" + }, + { + "output": "/actions/default/in/Pose", + "path": "/user/hand/right/pose/raw" + } + ], + "haptics": [ + { + "output": "/actions/default/out/Haptic", + "path": "/user/hand/left/output/haptic" + }, + { + "output": "/actions/default/out/Haptic", + "path": "/user/hand/right/output/haptic" + } + ] + } + } } \ No newline at end of file